diff --git a/convolution.ts b/convolution.ts new file mode 100644 index 0000000..5f4ea7f --- /dev/null +++ b/convolution.ts @@ -0,0 +1,398 @@ +// convolution.ts - Convolution operations for 1D and 2D data + +export interface ConvolutionOptions { + mode?: 'full' | 'same' | 'valid'; + boundary?: 'zero' | 'reflect' | 'symmetric'; +} + +export interface ConvolutionResult1D { + values: number[]; + originalLength: number; + kernelLength: number; + mode: string; +} + +export interface ConvolutionResult2D { + matrix: number[][]; + originalDimensions: [number, number]; + kernelDimensions: [number, number]; + mode: string; +} + +/** + * Validates input array for convolution operations + */ +function validateArray(arr: number[], name: string): void { + if (!Array.isArray(arr) || arr.length === 0) { + throw new Error(`${name} must be a non-empty array`); + } + if (arr.some(val => typeof val !== 'number' || !isFinite(val))) { + throw new Error(`${name} must contain only finite numbers`); + } +} + +/** + * Validates 2D matrix for convolution operations + */ +function validateMatrix(matrix: number[][], name: string): void { + if (!Array.isArray(matrix) || matrix.length === 0) { + throw new Error(`${name} must be a non-empty 2D array`); + } + + const rowLength = matrix[0].length; + if (rowLength === 0) { + throw new Error(`${name} rows must be non-empty`); + } + + for (let i = 0; i < matrix.length; i++) { + if (!Array.isArray(matrix[i]) || matrix[i].length !== rowLength) { + throw new Error(`${name} must be a rectangular matrix`); + } + if (matrix[i].some(val => typeof val !== 'number' || !isFinite(val))) { + throw new Error(`${name} must contain only finite numbers`); + } + } +} + +/** + * Applies boundary conditions to extend an array + */ +function applyBoundary1D(signal: number[], padding: number, boundary: string): number[] { + if (padding <= 0) return signal; + + let result = [...signal]; + + switch (boundary) { + case 'zero': + result = new Array(padding).fill(0).concat(result).concat(new Array(padding).fill(0)); + break; + case 'reflect': + const leftPad = []; + const rightPad = []; + for (let i = 0; i < padding; i++) { + leftPad.unshift(signal[Math.min(i + 1, signal.length - 1)]); + rightPad.push(signal[Math.max(signal.length - 2 - i, 0)]); + } + result = leftPad.concat(result).concat(rightPad); + break; + case 'symmetric': + const leftSymPad = []; + const rightSymPad = []; + for (let i = 0; i < padding; i++) { + leftSymPad.unshift(signal[Math.min(i, signal.length - 1)]); + rightSymPad.push(signal[Math.max(signal.length - 1 - i, 0)]); + } + result = leftSymPad.concat(result).concat(rightSymPad); + break; + default: + throw new Error(`Unsupported boundary condition: ${boundary}`); + } + + return result; +} + +/** + * Performs 1D convolution between signal and kernel + * + * @param signal - Input signal array + * @param kernel - Convolution kernel array + * @param options - Convolution options (mode, boundary) + * @returns Convolution result with metadata + */ +export function convolve1D( + signal: number[], + kernel: number[], + options: ConvolutionOptions = {} +): ConvolutionResult1D { + validateArray(signal, 'Signal'); + validateArray(kernel, 'Kernel'); + + const { mode = 'full', boundary = 'zero' } = options; + + // Flip kernel for convolution (not correlation) + const flippedKernel = [...kernel].reverse(); + + const signalLen = signal.length; + const kernelLen = flippedKernel.length; + + let result: number[] = []; + let paddedSignal = signal; + + // Apply boundary conditions based on mode + if (mode === 'same' || mode === 'full') { + const padding = mode === 'same' ? Math.floor(kernelLen / 2) : kernelLen - 1; + paddedSignal = applyBoundary1D(signal, padding, boundary); + } + + // Perform convolution + const outputLength = mode === 'full' ? signalLen + kernelLen - 1 : + mode === 'same' ? signalLen : + signalLen - kernelLen + 1; + + const startIdx = mode === 'valid' ? 0 : + mode === 'same' ? Math.floor(kernelLen / 2) : 0; + + for (let i = 0; i < outputLength; i++) { + let sum = 0; + for (let j = 0; j < kernelLen; j++) { + const signalIdx = startIdx + i + j; + if (signalIdx >= 0 && signalIdx < paddedSignal.length) { + sum += paddedSignal[signalIdx] * flippedKernel[j]; + } + } + result.push(sum); + } + + return { + values: result, + originalLength: signalLen, + kernelLength: kernelLen, + mode + }; +} + +/** + * Performs 2D convolution between matrix and kernel + * + * @param matrix - Input 2D matrix + * @param kernel - 2D convolution kernel + * @param options - Convolution options (mode, boundary) + * @returns 2D convolution result with metadata + */ +export function convolve2D( + matrix: number[][], + kernel: number[][], + options: ConvolutionOptions = {} +): ConvolutionResult2D { + validateMatrix(matrix, 'Matrix'); + validateMatrix(kernel, 'Kernel'); + + const { mode = 'full', boundary = 'zero' } = options; + + // Flip kernel for convolution + const flippedKernel = kernel.map(row => [...row].reverse()).reverse(); + + const matrixRows = matrix.length; + const matrixCols = matrix[0].length; + const kernelRows = flippedKernel.length; + const kernelCols = flippedKernel[0].length; + + // Calculate output dimensions + let outputRows: number, outputCols: number; + let padTop: number, padLeft: number; + + switch (mode) { + case 'full': + outputRows = matrixRows + kernelRows - 1; + outputCols = matrixCols + kernelCols - 1; + padTop = kernelRows - 1; + padLeft = kernelCols - 1; + break; + case 'same': + outputRows = matrixRows; + outputCols = matrixCols; + padTop = Math.floor(kernelRows / 2); + padLeft = Math.floor(kernelCols / 2); + break; + case 'valid': + outputRows = Math.max(0, matrixRows - kernelRows + 1); + outputCols = Math.max(0, matrixCols - kernelCols + 1); + padTop = 0; + padLeft = 0; + break; + default: + throw new Error(`Unsupported convolution mode: ${mode}`); + } + + // Create padded matrix based on boundary conditions + const totalPadRows = mode === 'valid' ? 0 : kernelRows - 1; + const totalPadCols = mode === 'valid' ? 0 : kernelCols - 1; + + const paddedMatrix: number[][] = []; + + // Initialize padded matrix with boundary conditions + for (let i = -padTop; i < matrixRows + totalPadRows - padTop; i++) { + const row: number[] = []; + for (let j = -padLeft; j < matrixCols + totalPadCols - padLeft; j++) { + let value = 0; + + if (i >= 0 && i < matrixRows && j >= 0 && j < matrixCols) { + value = matrix[i][j]; + } else if (boundary !== 'zero') { + // Apply boundary conditions + let boundaryI = i; + let boundaryJ = j; + + if (boundary === 'reflect') { + boundaryI = i < 0 ? -i - 1 : i >= matrixRows ? 2 * matrixRows - i - 1 : i; + boundaryJ = j < 0 ? -j - 1 : j >= matrixCols ? 2 * matrixCols - j - 1 : j; + } else if (boundary === 'symmetric') { + boundaryI = i < 0 ? -i : i >= matrixRows ? 2 * matrixRows - i - 2 : i; + boundaryJ = j < 0 ? -j : j >= matrixCols ? 2 * matrixCols - j - 2 : j; + } + + boundaryI = Math.max(0, Math.min(boundaryI, matrixRows - 1)); + boundaryJ = Math.max(0, Math.min(boundaryJ, matrixCols - 1)); + value = matrix[boundaryI][boundaryJ]; + } + + row.push(value); + } + paddedMatrix.push(row); + } + + // Perform 2D convolution + const result: number[][] = []; + + for (let i = 0; i < outputRows; i++) { + const row: number[] = []; + for (let j = 0; j < outputCols; j++) { + let sum = 0; + + for (let ki = 0; ki < kernelRows; ki++) { + for (let kj = 0; kj < kernelCols; kj++) { + const matrixI = i + ki; + const matrixJ = j + kj; + + if (matrixI >= 0 && matrixI < paddedMatrix.length && + matrixJ >= 0 && matrixJ < paddedMatrix[0].length) { + sum += paddedMatrix[matrixI][matrixJ] * flippedKernel[ki][kj]; + } + } + } + + row.push(sum); + } + result.push(row); + } + + return { + matrix: result, + originalDimensions: [matrixRows, matrixCols], + kernelDimensions: [kernelRows, kernelCols], + mode + }; +} + +/** + * Creates common convolution kernels + */ +export class ConvolutionKernels { + /** + * Creates a Gaussian blur kernel + */ + static gaussian(size: number, sigma: number = 1.0): number[][] { + if (size % 2 === 0) { + throw new Error('Kernel size must be odd'); + } + + const kernel: number[][] = []; + const center = Math.floor(size / 2); + let sum = 0; + + for (let i = 0; i < size; i++) { + const row: number[] = []; + for (let j = 0; j < size; j++) { + const x = i - center; + const y = j - center; + const value = Math.exp(-(x * x + y * y) / (2 * sigma * sigma)); + row.push(value); + sum += value; + } + kernel.push(row); + } + + // Normalize kernel + return kernel.map(row => row.map(val => val / sum)); + } + + /** + * Creates a Sobel edge detection kernel + */ + static sobel(direction: 'x' | 'y' = 'x'): number[][] { + if (direction === 'x') { + return [ + [-1, 0, 1], + [-2, 0, 2], + [-1, 0, 1] + ]; + } else { + return [ + [-1, -2, -1], + [ 0, 0, 0], + [ 1, 2, 1] + ]; + } + } + + /** + * Creates a Laplacian edge detection kernel + */ + static laplacian(): number[][] { + return [ + [ 0, -1, 0], + [-1, 4, -1], + [ 0, -1, 0] + ]; + } + + /** + * Creates a box/average blur kernel + */ + static box(size: number): number[][] { + if (size % 2 === 0) { + throw new Error('Kernel size must be odd'); + } + + const value = 1 / (size * size); + const kernel: number[][] = []; + + for (let i = 0; i < size; i++) { + kernel.push(new Array(size).fill(value)); + } + + return kernel; + } + + /** + * Creates a 1D Gaussian kernel + */ + static gaussian1D(size: number, sigma: number = 1.0): number[] { + if (size % 2 === 0) { + throw new Error('Kernel size must be odd'); + } + + const kernel: number[] = []; + const center = Math.floor(size / 2); + let sum = 0; + + for (let i = 0; i < size; i++) { + const x = i - center; + const value = Math.exp(-(x * x) / (2 * sigma * sigma)); + kernel.push(value); + sum += value; + } + + // Normalize kernel + return kernel.map(val => val / sum); + } + + /** + * Creates a 1D difference kernel for edge detection + */ + static difference1D(): number[] { + return [-1, 0, 1]; + } + + /** + * Creates a 1D moving average kernel + */ + static average1D(size: number): number[] { + if (size <= 0) { + throw new Error('Kernel size must be positive'); + } + + const value = 1 / size; + return new Array(size).fill(value); + } +} \ No newline at end of file diff --git a/server_addconvolution.ts b/server_addconvolution.ts new file mode 100644 index 0000000..f4076f8 --- /dev/null +++ b/server_addconvolution.ts @@ -0,0 +1,3201 @@ +// server.ts - Simplified main server file +// package.json dependencies needed: +// npm install express mathjs lodash date-fns +// npm install -D @types/express @types/node @types/lodash typescript ts-node + +import express from 'express'; +import swaggerJsdoc from 'swagger-jsdoc'; +import swaggerUi from 'swagger-ui-express'; +import * as math from 'mathjs'; +import * as _ from 'lodash'; +import { KMeans, KMeansOptions } from './kmeans'; +import { getWeekNumber, getSameWeekDayLastYear } from './time-helper'; +import { calculateLinearRegression, generateForecast, calculatePredictionIntervals, ForecastResult } from './prediction'; +import { convolve1D, convolve2D, ConvolutionKernels, ConvolutionOptions, ConvolutionResult1D, ConvolutionResult2D } from './convolution'; + +const app = express(); +app.use(express.json()); +const PORT = process.env.PORT || 3000; + +const swaggerOptions = { + swaggerDefinition: { + openapi: '3.0.0', + info: { + title: 'My Express API', + version: '1.0.0', + description: 'API documentation for my awesome Express app', + }, + servers: [ + { + url: `http://localhost:${PORT}`, + }, + ], + }, + // Paths to files containing OpenAPI definitions + apis: ["./*.ts"], // Make sure this path is correct +}; + +const swaggerSpec = swaggerJsdoc(swaggerOptions); + +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); + +// ======================================== +// TYPE DEFINITIONS +// ======================================== + +interface DataSeries { + values: number[]; + labels?: string[]; +} + +interface DataMatrix { + data: number[][]; + columns?: string[]; + rows?: string[]; +} + +interface Condition { + field: string; + operator: '>' | '<' | '=' | '>=' | '<=' | '!='; + value: number | string; +} + +interface ApiResponse { + success: boolean; + data?: T; + error?: string; +} + +// ======================================== +// HELPER FUNCTIONS +// ======================================== + +const handleError = (error: unknown): string => { + return error instanceof Error ? error.message : 'Unknown error'; +}; + +const validateSeries = (series: DataSeries): void => { + if (!series || !Array.isArray(series.values) || series.values.length === 0) { + throw new Error('Series must contain at least one value'); + } +}; + +const validateMatrix = (matrix: DataMatrix): void => { + if (!matrix || !Array.isArray(matrix.data) || matrix.data.length === 0) { + throw new Error('Matrix must contain at least one row'); + } +}; + +/** + * A helper class to provide a fluent API for rolling window calculations. + */ +class RollingWindow { + private windows: number[][]; + + constructor(windows: number[][]) { + this.windows = windows; + } + + mean(): number[] { + return this.windows.map(window => Number(math.mean(window))); + } + + sum(): number[] { + return this.windows.map(window => _.sum(window)); + } + + min(): number[] { + return this.windows.map(window => Math.min(...window)); + } + + max(): number[] { + return this.windows.map(window => Math.max(...window)); + } + + toArray(): number[][] { + return this.windows; + } +} + +// ======================================== +// ANALYTICS ENGINE (Simplified) +// ======================================== + +class AnalyticsEngine { + + private applyConditions(series: DataSeries, conditions: Condition[] = []): number[] { + if (conditions.length === 0) return series.values; + return series.values; // TODO: Implement filtering + } + + // Basic statistical functions + unique(series: DataSeries): number[] { + validateSeries(series); + return _.uniq(series.values); + } + + mean(series: DataSeries, conditions: Condition[] = []): number { + validateSeries(series); + const filteredValues = this.applyConditions(series, conditions); + if (filteredValues.length === 0) throw new Error('No data points match conditions'); + return Number(math.mean(filteredValues)); + } + + count(series: DataSeries, conditions: Condition[] = []): number { + validateSeries(series); + const filteredValues = this.applyConditions(series, conditions); + if (filteredValues.length === 0) throw new Error('No data points match conditions'); + return filteredValues.length; + } + + variance(series: DataSeries, conditions: Condition[] = []): number { + validateSeries(series); + const filteredValues = this.applyConditions(series, conditions); + if (filteredValues.length === 0) throw new Error('No data points match conditions'); + return Number(math.variance(filteredValues)); + } + + standardDeviation(series: DataSeries, conditions: Condition[] = []): number { + validateSeries(series); + const filteredValues = this.applyConditions(series, conditions); + if (filteredValues.length === 0) throw new Error('No data points match conditions'); + return Number(math.std(filteredValues)); + } + + percentile(series: DataSeries, percent: number, ascending: boolean = true, conditions: Condition[] = []): number { + validateSeries(series); + const filteredValues = this.applyConditions(series, conditions); + if (filteredValues.length === 0) throw new Error('No data points match conditions'); + + const sorted = ascending ? _.sortBy(filteredValues) : _.sortBy(filteredValues).reverse(); + const index = (percent / 100) * (sorted.length - 1); + const lower = Math.floor(index); + const upper = Math.ceil(index); + const weight = index % 1; + + return sorted[lower] * (1 - weight) + sorted[upper] * weight; + } + + median(series: DataSeries, conditions: Condition[] = []): number { + return this.percentile(series, 50, true, conditions); + } + + mode(series: DataSeries, conditions: Condition[] = []): number[] { + validateSeries(series); + const filteredValues = this.applyConditions(series, conditions); + const frequency = _.countBy(filteredValues); + const maxFreq = Math.max(...Object.values(frequency)); + + return Object.keys(frequency) + .filter(key => frequency[key] === maxFreq) + .map(Number); + } + + max(series: DataSeries, conditions: Condition[] = []): number { + validateSeries(series); + const filteredValues = this.applyConditions(series, conditions); + if (filteredValues.length === 0) throw new Error('No data points match conditions'); + return Math.max(...filteredValues); + } + + min(series: DataSeries, conditions: Condition[] = []): number { + validateSeries(series); + const filteredValues = this.applyConditions(series, conditions); + if (filteredValues.length === 0) throw new Error('No data points match conditions'); + return Math.min(...filteredValues); + } + + correlation(series1: DataSeries, series2: DataSeries): number { + validateSeries(series1); + validateSeries(series2); + + if (series1.values.length !== series2.values.length) { + throw new Error('Series must have same length for correlation'); + } + + const x = series1.values; + const y = series2.values; + const n = x.length; + + const sumX = _.sum(x); + const sumY = _.sum(y); + const sumXY = _.sum(x.map((xi, i) => xi * y[i])); + const sumX2 = _.sum(x.map(xi => xi * xi)); + const sumY2 = _.sum(y.map(yi => yi * yi)); + + const numerator = n * sumXY - sumX * sumY; + const denominator = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY)); + + return numerator / denominator; + } + + // Rolling window functions + rolling(series: DataSeries, windowSize: number): RollingWindow { + validateSeries(series); + if (windowSize <= 0) { + throw new Error('Window size must be a positive number.'); + } + if (series.values.length < windowSize) { + return new RollingWindow([]); + } + + const windows: number[][] = []; + for (let i = 0; i <= series.values.length - windowSize; i++) { + const window = series.values.slice(i, i + windowSize); + windows.push(window); + } + return new RollingWindow(windows); + } + + movingAverage(series: DataSeries, windowSize: number): number[] { + return this.rolling(series, windowSize).mean(); + } + + // K-means wrapper (uses imported KMeans class) + kmeans(matrix: DataMatrix, nClusters: number, options: KMeansOptions = {}): { clusters: number[][][], centroids: number[][] } { + validateMatrix(matrix); + const points: number[][] = matrix.data; + + // Use the new MiniBatchKMeans class + const kmeans = new KMeans(points, nClusters, options); + const result = kmeans.run(); + + const centroids = result.clusters.map(c => c.centroid); + const clusters = result.clusters.map(c => c.points); + + return { clusters, centroids }; + } + + // Time helper wrapper functions + getWeekNumber(dateString: string): number { + return getWeekNumber(dateString); + } + + getSameWeekDayLastYear(dateString: string): string { + return getSameWeekDayLastYear(dateString); + } + + // Retail functions + purchaseRate(productPurchases: number, totalTransactions: number): number { + if (totalTransactions === 0) throw new Error('Total transactions cannot be zero'); + return (productPurchases / totalTransactions) * 100; + } + + liftValue(jointPurchaseRate: number, productAPurchaseRate: number, productBPurchaseRate: number): number { + const expectedJointRate = productAPurchaseRate * productBPurchaseRate; + if (expectedJointRate === 0) throw new Error('Expected joint rate cannot be zero'); + return jointPurchaseRate / expectedJointRate; + } + + costRatio(cost: number, salePrice: number): number { + if (salePrice === 0) throw new Error('Sale price cannot be zero'); + return cost / salePrice; + } + + grossMarginRate(salePrice: number, cost: number): number { + if (salePrice === 0) throw new Error('Sale price cannot be zero'); + return (salePrice - cost) / salePrice; + } + + averageSpendPerCustomer(totalRevenue: number, numberOfCustomers: number): number { + if (numberOfCustomers === 0) { + throw new Error('Number of customers cannot be zero'); + } + return totalRevenue / numberOfCustomers; + } + + purchaseIndex(totalItemsSold: number, numberOfCustomers: number): number { + if (numberOfCustomers === 0) { + throw new Error('Number of customers cannot be zero'); + } + return (totalItemsSold / numberOfCustomers) * 1000; + } + + // ======================================== + // Prediction functions + // ======================================== + + timeSeriesForecast(series: DataSeries, forecastPeriods: number): ForecastResult { + validateSeries(series); + + const model = calculateLinearRegression(series.values); + const forecast = generateForecast(model, series.values.length, forecastPeriods); + const predictionIntervals = calculatePredictionIntervals(series.values, model, forecast); + + return { + forecast, + predictionIntervals, + modelParameters: { + slope: model.slope, + intercept: model.intercept, + }, + }; + } + + // ======================================== + // Time Series Convolution Methods + // ======================================== + + /** + * Simple Moving Average using convolution + * Equivalent to existing movingAverage but using convolution approach + */ + movingAverageConvolution(series: DataSeries, windowSize: number): number[] { + validateSeries(series); + const kernel = ConvolutionKernels.average1D(windowSize); + const result = convolve1D(series.values, kernel, { mode: 'valid' }); + return result.values; + } + + /** + * Exponentially Weighted Moving Average using convolution approximation + * Creates an approximation of EWMA using a finite impulse response + */ + exponentialMovingAverageConvolution(series: DataSeries, alpha: number, windowSize: number = 10): number[] { + validateSeries(series); + if (alpha <= 0 || alpha >= 1) { + throw new Error('Alpha must be between 0 and 1'); + } + + // Create exponential decay kernel + const kernel: number[] = []; + let sum = 0; + + for (let i = 0; i < windowSize; i++) { + const weight = alpha * Math.pow(1 - alpha, i); + kernel.unshift(weight); // Reverse order for convolution + sum += weight; + } + + // Normalize kernel + const normalizedKernel = kernel.map(w => w / sum); + + const result = convolve1D(series.values, normalizedKernel, { mode: 'valid' }); + return result.values; + } + + /** + * Weighted Moving Average using convolution + */ + weightedMovingAverageConvolution(series: DataSeries, weights: number[]): number[] { + validateSeries(series); + if (!Array.isArray(weights) || weights.length === 0) { + throw new Error('Weights must be a non-empty array'); + } + + // Normalize weights + const weightSum = weights.reduce((sum, w) => sum + w, 0); + const normalizedWeights = weights.map(w => w / weightSum); + + const result = convolve1D(series.values, normalizedWeights, { mode: 'valid' }); + return result.values; + } + + /** + * Gaussian smoothing for time series + * Creates smooth trend line using Gaussian kernel + */ + gaussianSmoothing(series: DataSeries, windowSize: number, sigma: number = 1.0): number[] { + validateSeries(series); + const kernel = ConvolutionKernels.gaussian1D(windowSize, sigma); + const result = convolve1D(series.values, kernel, { mode: 'same', boundary: 'reflect' }); + return result.values; + } + + /** + * Time series differentiation (rate of change) + * Uses difference kernel to compute derivatives + */ + timeSeriesDerivative(series: DataSeries): number[] { + validateSeries(series); + const kernel = ConvolutionKernels.difference1D(); // [-1, 0, 1] + const result = convolve1D(series.values, kernel, { mode: 'same', boundary: 'symmetric' }); + return result.values; + } + + /** + * Peak detection using Laplacian-like 1D kernel + * Detects local maxima and minima in time series + */ + peakDetection(series: DataSeries): number[] { + validateSeries(series); + const laplacian1D = [-1, 2, -1]; // 1D Laplacian for peak detection + const result = convolve1D(series.values, laplacian1D, { mode: 'same', boundary: 'symmetric' }); + return result.values; + } + + /** + * Trend extraction using low-pass filter + * Removes high-frequency noise to extract underlying trend + */ + extractTrend(series: DataSeries, cutoffRatio: number = 0.1): number[] { + validateSeries(series); + + // Create low-pass filter kernel based on cutoff ratio + const windowSize = Math.max(3, Math.floor(series.values.length * cutoffRatio)); + const adjustedSize = windowSize % 2 === 0 ? windowSize + 1 : windowSize; // Ensure odd + + const kernel = ConvolutionKernels.gaussian1D(adjustedSize, adjustedSize / 6); + const result = convolve1D(series.values, kernel, { mode: 'same', boundary: 'reflect' }); + return result.values; + } + + /** + * Noise reduction using bilateral-like filtering + * Preserves edges while reducing noise + */ + denoiseTimeSeries(series: DataSeries, windowSize: number = 5, threshold: number = 1.0): number[] { + validateSeries(series); + + const result: number[] = []; + const halfWindow = Math.floor(windowSize / 2); + + for (let i = 0; i < series.values.length; i++) { + let weightedSum = 0; + let totalWeight = 0; + + for (let j = -halfWindow; j <= halfWindow; j++) { + const idx = i + j; + if (idx >= 0 && idx < series.values.length) { + const valueDiff = Math.abs(series.values[i] - series.values[idx]); + const spatialWeight = Math.exp(-(j * j) / (2 * (halfWindow / 3) ** 2)); + const rangeWeight = Math.exp(-(valueDiff * valueDiff) / (2 * threshold * threshold)); + const weight = spatialWeight * rangeWeight; + + weightedSum += series.values[idx] * weight; + totalWeight += weight; + } + } + + result.push(totalWeight > 0 ? weightedSum / totalWeight : series.values[i]); + } + + return result; + } + + /** + * Seasonal decomposition using convolution filters + * Separates trend, seasonal, and residual components + */ + seasonalDecomposition(series: DataSeries, seasonalPeriod: number): { + trend: number[], + seasonal: number[], + residual: number[] + } { + validateSeries(series); + if (seasonalPeriod <= 1 || seasonalPeriod >= series.values.length / 2) { + throw new Error('Seasonal period must be between 1 and half the series length'); + } + + // Extract trend using moving average + const trendKernel = ConvolutionKernels.average1D(seasonalPeriod); + const trendResult = convolve1D(series.values, trendKernel, { mode: 'same', boundary: 'reflect' }); + const trend = trendResult.values; + + // Calculate detrended series + const detrended = series.values.map((val, i) => val - trend[i]); + + // Extract seasonal component by averaging over periods + const seasonal = new Array(series.values.length); + for (let i = 0; i < series.values.length; i++) { + let sum = 0; + let count = 0; + + // Average all values at the same seasonal position + for (let j = i % seasonalPeriod; j < detrended.length; j += seasonalPeriod) { + sum += detrended[j]; + count++; + } + + seasonal[i] = count > 0 ? sum / count : 0; + } + + // Calculate residual + const residual = series.values.map((val, i) => val - trend[i] - seasonal[i]); + + return { trend, seasonal, residual }; + } + + /** + * Auto-correlation using convolution + * Measures how correlated a series is with a delayed copy of itself + */ + autoCorrelation(series: DataSeries, maxLag: number): number[] { + validateSeries(series); + + const n = series.values.length; + const mean = this.mean(series); + + // Center the data + const centered = series.values.map(x => x - mean); + + // Compute auto-correlation using convolution + const result: number[] = []; + + for (let lag = 0; lag <= Math.min(maxLag, n - 1); lag++) { + let correlation = 0; + let count = 0; + + for (let i = 0; i < n - lag; i++) { + correlation += centered[i] * centered[i + lag]; + count++; + } + + // Normalize by variance and count + const variance = this.variance(series); + result.push(count > 0 ? correlation / (count * variance) : 0); + } + + return result; + } + + // ======================================== + // Convolution functions + // ======================================== + + convolve1D(signal: number[], kernel: number[], options: ConvolutionOptions = {}): ConvolutionResult1D { + if (!Array.isArray(signal) || signal.length === 0) { + throw new Error('Signal must be a non-empty array'); + } + if (!Array.isArray(kernel) || kernel.length === 0) { + throw new Error('Kernel must be a non-empty array'); + } + return convolve1D(signal, kernel, options); + } + + convolve2D(matrix: number[][], kernel: number[][], options: ConvolutionOptions = {}): ConvolutionResult2D { + if (!Array.isArray(matrix) || matrix.length === 0) { + throw new Error('Matrix must be a non-empty 2D array'); + } + if (!Array.isArray(kernel) || kernel.length === 0) { + throw new Error('Kernel must be a non-empty 2D array'); + } + return convolve2D(matrix, kernel, options); + } + + // Kernel generation methods + generateGaussianKernel(size: number, sigma: number = 1.0): number[][] { + return ConvolutionKernels.gaussian(size, sigma); + } + + generateGaussianKernel1D(size: number, sigma: number = 1.0): number[] { + return ConvolutionKernels.gaussian1D(size, sigma); + } + + generateSobelKernel(direction: 'x' | 'y' = 'x'): number[][] { + return ConvolutionKernels.sobel(direction); + } + + generateLaplacianKernel(): number[][] { + return ConvolutionKernels.laplacian(); + } + + generateBoxKernel(size: number): number[][] { + return ConvolutionKernels.box(size); + } + + generateAverageKernel1D(size: number): number[] { + return ConvolutionKernels.average1D(size); + } + + generateDifferenceKernel1D(): number[] { + return ConvolutionKernels.difference1D(); + } +} + +// Initialize analytics engine +const analytics = new AnalyticsEngine(); + +// ======================================== +// API ROUTES +// ======================================== + +/** + * @swagger + * /api/health: + * get: + * summary: Health check endpoint + * description: Returns the health status of the API + * responses: + * 200: + * description: API is healthy + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: OK + * timestamp: + * type: string + * format: date-time + */ +app.get('/api/health', (req, res) => { + res.status(200).json({ status: 'OK', timestamp: new Date().toISOString() }); +}); + +/** + * @swagger + * /api/unique: + * post: + * summary: Get unique values from a data series + * description: Returns an array of unique values from the provided data series + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * responses: + * 200: + * description: Unique values calculated successfully + * 400: + * description: Invalid input data + */ +app.post('/api/unique', (req, res) => { + try { + const result = analytics.unique(req.body.series); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/mean: + * post: + * summary: Calculate mean of a data series + * description: Returns the arithmetic mean of the provided data series, optionally filtered by conditions + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * conditions: + * type: array + * items: + * $ref: '#/components/schemas/Condition' + * examples: + * value: + * series: + * values: [10, 20, 30, 40, 50] + * labels: ["Q1", "Q2", "Q3", "Q4", "Q5"] + * responses: + * 200: + * description: Mean calculated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiResponse' + * examples: + * successful_calculation: + * summary: Successful mean calculation + * description: Mean of [10, 20, 30, 40, 50] is 30 + * value: + * success: true + * data: 30.0 + * 400: + * description: Invalid input data + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiResponse' + * examples: + * empty_series: + * summary: Empty series error + * value: + * success: false + * error: "Series must contain at least one value" + * invalid_format: + * summary: Invalid data format + * value: + * success: false + * error: "Series values must be an array of numbers" + */ +app.post('/api/mean', (req, res) => { + try { + const result = analytics.mean(req.body.series, req.body.conditions); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/count: + * post: + * summary: Count data points in a series + * description: Returns the count of data points in the series, optionally filtered by conditions + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * conditions: + * type: array + * items: + * $ref: '#/components/schemas/Condition' + * responses: + * 200: + * description: Count calculated successfully + * 400: + * description: Invalid input data + */ +app.post('/api/count', (req, res) => { + try { + const result = analytics.count(req.body.series, req.body.conditions); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/variance: + * post: + * summary: Calculate variance of a data series + * description: Returns the variance of the provided data series + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * conditions: + * type: array + * items: + * $ref: '#/components/schemas/Condition' + * responses: + * 200: + * description: Variance calculated successfully + * 400: + * description: Invalid input data + */ +app.post('/api/variance', (req, res) => { + try { + const result = analytics.variance(req.body.series, req.body.conditions); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/std: + * post: + * summary: Calculate standard deviation of a data series + * description: Returns the standard deviation of the provided data series + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * conditions: + * type: array + * items: + * $ref: '#/components/schemas/Condition' + * responses: + * 200: + * description: Standard deviation calculated successfully + * 400: + * description: Invalid input data + */ +app.post('/api/std', (req, res) => { + try { + const result = analytics.standardDeviation(req.body.series, req.body.conditions); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/percentile: + * post: + * summary: Calculate percentile of a data series + * description: Returns the specified percentile of the provided data series + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * percent: + * type: number + * description: Percentile to calculate (0-100) + * example: 95 + * ascending: + * type: boolean + * description: Sort order + * default: true + * conditions: + * type: array + * items: + * $ref: '#/components/schemas/Condition' + * responses: + * 200: + * description: Percentile calculated successfully + * 400: + * description: Invalid input data + */ +app.post('/api/percentile', (req, res) => { + try { + const result = analytics.percentile(req.body.series, req.body.percent, req.body.ascending, req.body.conditions); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/median: + * post: + * summary: Calculate median of a data series + * description: Returns the median (50th percentile) of the provided data series + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * conditions: + * type: array + * items: + * $ref: '#/components/schemas/Condition' + * responses: + * 200: + * description: Median calculated successfully + * 400: + * description: Invalid input data + */ +app.post('/api/median', (req, res) => { + try { + const result = analytics.median(req.body.series, req.body.conditions); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/mode: + * post: + * summary: Calculate mode of a data series + * description: Returns the mode (most frequent values) of the provided data series + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * conditions: + * type: array + * items: + * $ref: '#/components/schemas/Condition' + * responses: + * 200: + * description: Mode calculated successfully + * 400: + * description: Invalid input data + */ +app.post('/api/mode', (req, res) => { + try { + const result = analytics.mode(req.body.series, req.body.conditions); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/max: + * post: + * summary: Find maximum value in a data series + * description: Returns the maximum value from the provided data series + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * conditions: + * type: array + * items: + * $ref: '#/components/schemas/Condition' + * responses: + * 200: + * description: Maximum value found successfully + * 400: + * description: Invalid input data + */ +app.post('/api/max', (req, res) => { + try { + const result = analytics.max(req.body.series, req.body.conditions); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/min: + * post: + * summary: Find minimum value in a data series + * description: Returns the minimum value from the provided data series + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * conditions: + * type: array + * items: + * $ref: '#/components/schemas/Condition' + * responses: + * 200: + * description: Minimum value found successfully + * 400: + * description: Invalid input data + */ +app.post('/api/min', (req, res) => { + try { + const result = analytics.min(req.body.series, req.body.conditions); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/correlation: + * post: + * summary: Calculate correlation between two data series + * description: Returns the Pearson correlation coefficient between two data series + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series1: + * $ref: '#/components/schemas/DataSeries' + * series2: + * $ref: '#/components/schemas/DataSeries' + * examples: + * value: + * series1: + * values: [1, 2, 3, 4, 5] + * labels: ["Day 1", "Day 2", "Day 3", "Day 4", "Day 5"] + * series2: + * values: [2, 4, 6, 8, 10] + * labels: ["Day 1", "Day 2", "Day 3", "Day 4", "Day 5"] + * responses: + * 200: + * description: Correlation calculated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiResponse' + * examples: + * perfect_positive: + * value: + * success: true + * data: 1 + * 400: + * description: Invalid input data or series have different lengths + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiResponse' + * examples: + * different_lengths: + * summary: Series length mismatch + * value: + * success: false + * error: "Series must have same length for correlation" + * insufficient_data: + * summary: Insufficient data points + * value: + * success: false + * error: "At least 2 data points required for correlation" + */ +app.post('/api/correlation', (req, res) => { + try { + const result = analytics.correlation(req.body.series1, req.body.series2); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/series/moving-average: + * post: + * summary: Calculate moving average of a data series + * description: Returns the moving average of the provided data series with specified window size + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * windowSize: + * type: integer + * description: Size of the moving window + * minimum: 1 + * example: 3 + * examples: + * stock_prices: + * summary: Stock price data + * value: + * series: + * values: [100, 102, 98, 105, 110, 108, 112, 115] + * labels: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon"] + * windowSize: 3 + * responses: + * 200: + * description: Moving average calculated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: array + * items: + * type: number + * examples: + * stock_moving_avg: + * summary: 3-day moving average result + * description: Moving average for stock prices with window size 3 + * value: + * success: true + * data: [100.0, 101.67, 104.33, 107.67, 110.0, 111.67] + * 400: + * description: Invalid input data or window size + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiResponse' + * examples: + * window_too_large: + * summary: Window size exceeds data length + * value: + * success: false + * error: "Window size cannot be larger than series length" + * invalid_window_size: + * summary: Invalid window size + * value: + * success: false + * error: "Window size must be a positive number" + */ +app.post('/api/series/moving-average', (req, res) => { + try { + const { series, windowSize } = req.body; + const result = analytics.movingAverage(series, windowSize); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/series/rolling: + * post: + * summary: Get rolling windows of a data series + * description: Returns rolling windows of the provided data series with specified window size + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * windowSize: + * type: integer + * description: Size of the rolling window + * minimum: 1 + * example: 3 + * responses: + * 200: + * description: Rolling windows calculated successfully + * 400: + * description: Invalid input data or window size + */ +app.post('/api/series/rolling', (req, res) => { + try { + const { series, windowSize } = req.body; + const result = analytics.rolling(series, windowSize).toArray(); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/ml/kmeans: + * post: + * summary: Perform K-means clustering + * description: Performs K-means clustering on the provided data matrix + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * matrix: + * $ref: '#/components/schemas/DataMatrix' + * nClusters: + * type: integer + * description: Number of clusters + * minimum: 1 + * example: 3 + * options: + * type: object + * description: K-means options + * examples: + * customer_segmentation: + * summary: Customer segmentation data + * value: + * matrix: + * data: [[25, 50000], [30, 60000], [35, 70000], [40, 45000], [22, 35000], [28, 55000]] + * columns: ["age", "income"] + * nClusters: 2 + * options: + * maxIterations: 100 + * responses: + * 200: + * description: K-means clustering completed successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * clusters: + * type: array + * items: + * type: array + * items: + * type: array + * items: + * type: number + * centroids: + * type: array + * items: + * type: array + * items: + * type: number + * examples: + * customer_clusters: + * summary: Customer segmentation result + * description: Customers grouped into 2 clusters by age and income + * value: + * success: true + * data: + * clusters: + * - [[30, 60000], [35, 70000], [28, 55000]] + * - [[25, 50000], [40, 45000], [22, 35000]] + * centroids: + * - [31.000000000000004, 61666.666666666664] + * - [29.000000000000007, 43333.33333333333] + * 400: + * description: Invalid input data + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiResponse' + * examples: + * too_many_clusters: + * summary: More clusters than data points + * value: + * success: false + * error: "Number of clusters cannot exceed number of data points" + * invalid_data: + * summary: Invalid matrix data + * value: + * success: false + * error: "Matrix must contain at least one row" + */ +app.post('/api/ml/kmeans', (req, res) => { + try { + const result = analytics.kmeans(req.body.matrix, req.body.nClusters, req.body.options); + res.status(200).json({ success: true, data: result } as ApiResponse<{ clusters: number[][][], centroids: number[][] }>); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse<{ clusters: number[][][], centroids: number[][] }>); + } +}); + +/** + * @swagger + * /api/time/week-number: + * post: + * summary: Get week number from date + * description: Returns the ISO week number for the provided date string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * date: + * type: string + * format: date + * description: Date string in ISO format + * example: "2024-03-15" + * responses: + * 200: + * description: Week number calculated successfully + * 400: + * description: Invalid date format + */ +app.post('/api/time/week-number', (req, res) => { + try { + const { date } = req.body; + const result = analytics.getWeekNumber(date); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/time/same-day-last-year: + * post: + * summary: Get same day of week from last year + * description: Returns the date string for the same day of the week from the previous year + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * date: + * type: string + * format: date + * description: Date string in ISO format + * example: "2024-03-15" + * responses: + * 200: + * description: Same day last year calculated successfully + * 400: + * description: Invalid date format + */ +app.post('/api/time/same-day-last-year', (req, res) => { + try { + const { date } = req.body; + const result = analytics.getSameWeekDayLastYear(date); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/retail/purchase-rate: + * post: + * summary: Calculate purchase rate + * description: Calculates the purchase rate as a percentage of product purchases over total transactions + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * productPurchases: + * type: number + * description: Number (or amount) of product purchases + * example: 150 + * totalTransactions: + * type: number + * description: Total number (or amount) of transactions + * example: 1000 + * examples: + * value: + * productPurchases: 320 + * totalTransactions: 2500 + * responses: + * 200: + * description: Purchase rate calculated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiResponse' + * examples: + * successful_calculation: + * description: 320 purchases out of 2500 transactions = 12.8% + * value: + * success: true + * data: 12.8 + * 400: + * description: Invalid input data or division by zero + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiResponse' + * examples: + * zero_transactions: + * summary: Zero total transactions + * value: + * success: false + * error: "Total transactions cannot be zero" + * negative_values: + * summary: Negative input values + * value: + * success: false + * error: "Purchase count and transactions must be positive numbers" + */ +app.post('/api/retail/purchase-rate', (req, res) => { + try { + const result = analytics.purchaseRate(req.body.productPurchases, req.body.totalTransactions); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/retail/lift-value: + * post: + * summary: Calculate lift value + * description: Calculates the lift value for market basket analysis + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * jointPurchaseRate: + * type: number + * description: Joint purchase rate of both products + * example: 0.05 + * productAPurchaseRate: + * type: number + * description: Purchase rate of product A + * example: 0.2 + * productBPurchaseRate: + * type: number + * description: Purchase rate of product B + * example: 0.3 + * responses: + * 200: + * description: Lift value calculated successfully + * 400: + * description: Invalid input data or division by zero + */ +app.post('/api/retail/lift-value', (req, res) => { + try { + const result = analytics.liftValue(req.body.jointPurchaseRate, req.body.productAPurchaseRate, req.body.productBPurchaseRate); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/retail/cost-ratio: + * post: + * summary: Calculate cost ratio + * description: Calculates the cost ratio (cost divided by sale price) + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * cost: + * type: number + * description: Cost of the product + * example: 50 + * salePrice: + * type: number + * description: Sale price of the product + * example: 100 + * responses: + * 200: + * description: Cost ratio calculated successfully + * 400: + * description: Invalid input data or division by zero + */ +app.post('/api/retail/cost-ratio', (req, res) => { + try { + const result = analytics.costRatio(req.body.cost, req.body.salePrice); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/retail/gross-margin: + * post: + * summary: Calculate gross margin rate + * description: Calculates the gross margin rate as a percentage + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * salePrice: + * type: number + * description: Sale price of the product + * example: 100 + * cost: + * type: number + * description: Cost of the product + * example: 60 + * responses: + * 200: + * description: Gross margin rate calculated successfully + * 400: + * description: Invalid input data or division by zero + */ +app.post('/api/retail/gross-margin', (req, res) => { + try { + const result = analytics.grossMarginRate(req.body.salePrice, req.body.cost); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/retail/average-spend: + * post: + * summary: Calculate average spend per customer + * description: Calculates the average amount spent per customer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * totalRevenue: + * type: number + * description: Total revenue + * example: 50000 + * numberOfCustomers: + * type: number + * description: Number of customers + * example: 500 + * responses: + * 200: + * description: Average spend calculated successfully + * 400: + * description: Invalid input data or division by zero + */ +app.post('/api/retail/average-spend', (req, res) => { + try { + const { totalRevenue, numberOfCustomers } = req.body; + const result = analytics.averageSpendPerCustomer(totalRevenue, numberOfCustomers); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/retail/purchase-index: + * post: + * summary: Calculate purchase index + * description: Calculates the purchase index (items per 1000 customers) + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * totalItemsSold: + * type: number + * description: Total number of items sold + * example: 2500 + * numberOfCustomers: + * type: number + * description: Number of customers + * example: 1000 + * responses: + * 200: + * description: Purchase index calculated successfully + * 400: + * description: Invalid input data or division by zero + */ +app.post('/api/retail/purchase-index', (req, res) => { + try { + const { totalItemsSold, numberOfCustomers } = req.body; + const result = analytics.purchaseIndex(totalItemsSold, numberOfCustomers); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/predict/forecast: + * post: + * summary: Generate time series forecast + * description: Generates a forecast for time series data using linear regression + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * forecastPeriods: + * type: integer + * description: Number of periods to forecast + * minimum: 1 + * example: 5 + * examples: + * sales_forecast: + * summary: Monthly sales forecast + * value: + * series: + * values: [1000, 1200, 1100, 1300, 1250, 1400, 1350, 1500] + * labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"] + * forecastPeriods: 3 + * responses: + * 200: + * description: Forecast generated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * forecast: + * type: array + * items: + * type: number + * predictionIntervals: + * type: object + * properties: + * lower: + * type: array + * items: + * type: number + * upper: + * type: array + * items: + * type: number + * modelParameters: + * type: object + * properties: + * slope: + * type: number + * intercept: + * type: number + * examples: + * sales_forecast_result: + * summary: 3-month sales forecast + * description: Forecast shows upward trend in sales + * value: + * success: true + * data: + * forecast: [1535.7142857142858, 1596.4285714285716, 1657.142857142857] + * predictionIntervals: + * lower: [1399.6187310477208, 1460.3330167620065, 1521.047302476292] + * upper: [1671.8098403808508, 1732.5241260951366, 1793.2384118094221] + * modelParameters: + * slope: 60.714285714285715 + * intercept: 1050 + * 400: + * description: Invalid input data + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiResponse' + * examples: + * insufficient_data: + * summary: Not enough data points + * value: + * success: false + * error: "At least 2 data points required for forecasting" + * invalid_periods: + * summary: Invalid forecast periods + * value: + * success: false + * error: "Forecast periods must be a positive integer" + */ +app.post('/api/predict/forecast', (req, res) => { + try { + const { series, forecastPeriods } = req.body; + const result = analytics.timeSeriesForecast(series, forecastPeriods); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/timeseries/gaussian-smoothing: + * post: + * summary: Apply Gaussian smoothing to time series + * description: Smooths time series data using a Gaussian kernel to reduce noise while preserving trends + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * windowSize: + * type: integer + * description: Size of Gaussian window (must be odd) + * example: 7 + * sigma: + * type: number + * description: Standard deviation for Gaussian kernel + * default: 1.0 + * example: 1.5 + * examples: + * stock_smoothing: + * summary: Stock price smoothing + * value: + * series: + * values: [100, 102, 98, 105, 110, 95, 112, 115, 108, 120] + * labels: ["Day1", "Day2", "Day3", "Day4", "Day5", "Day6", "Day7", "Day8", "Day9", "Day10"] + * windowSize: 5 + * sigma: 1.0 + * responses: + * 200: + * description: Time series smoothed successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiResponse' + * examples: + * smoothed_result: + * value: + * success: true + * data: [ + * 101.44905634573055, + * 103.80324441684816, + * 104.5710863144389, + * 104.44910348059275, + * 108.25424910465686, + * 111.74065631172301, + * 112.85778023626, + * 113.59428094642597, + * 106.75504756669999, + * 79.2136809751448 + * ] + * 400: + * description: Invalid input parameters + */ +app.post('/api/timeseries/gaussian-smoothing', (req, res) => { + try { + const { series, windowSize, sigma = 1.0 } = req.body; + const result = analytics.gaussianSmoothing(series, windowSize, sigma); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/timeseries/derivative: + * post: + * summary: Calculate time series derivative + * description: Computes the rate of change (derivative) of a time series using convolution + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * examples: + * temperature_change: + * summary: Temperature rate of change + * value: + * series: + * values: [20, 22, 25, 23, 21, 24, 27, 29, 26, 24] + * labels: ["Hour1", "Hour2", "Hour3", "Hour4", "Hour5", "Hour6", "Hour7", "Hour8", "Hour9", "Hour10"] + * responses: + * 200: + * description: Time series derivative calculated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiResponse' + * examples: + * derivative_result: + * value: + * success: true + * data: [-5, -1, 4, -1, -6, -5, 1, 5, 2, 24] + * 400: + * description: Invalid input data + */ +app.post('/api/timeseries/derivative', (req, res) => { + try { + const { series } = req.body; + const result = analytics.timeSeriesDerivative(series); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/timeseries/peak-detection: + * post: + * summary: Detect peaks in time series + * description: Uses convolution-based approach to detect local maxima and minima + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * responses: + * 200: + * description: Peak detection completed successfully + * 400: + * description: Invalid input data + */ +app.post('/api/timeseries/peak-detection', (req, res) => { + try { + const { series } = req.body; + const result = analytics.peakDetection(series); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/timeseries/extract-trend: + * post: + * summary: Extract trend from time series + * description: Removes high-frequency noise to extract underlying trend using low-pass filtering + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * cutoffRatio: + * type: number + * description: Cutoff ratio for low-pass filter (0-1) + * default: 0.1 + * example: 0.15 + * examples: + * sales_trend: + * summary: Monthly sales trend extraction + * value: + * series: + * values: [1000, 1200, 1100, 1300, 1250, 1400, 1350, 1500, 1450, 1600, 1550, 1700] + * labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + * cutoffRatio: 0.2 + * responses: + * 200: + * description: Trend extracted successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiResponse' + * examples: + * trend_result: + * value: + * success: true + * data: [1050, 1150, 1200, 1280, 1320, 1380, 1420, 1480, 1520, 1580, 1620, 1650] + * 400: + * description: Invalid input parameters + */ +app.post('/api/timeseries/extract-trend', (req, res) => { + try { + const { series, cutoffRatio = 0.1 } = req.body; + const result = analytics.extractTrend(series, cutoffRatio); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/timeseries/denoise: + * post: + * summary: Denoise time series data + * description: Reduces noise while preserving important features using bilateral-like filtering + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * windowSize: + * type: integer + * description: Size of denoising window + * default: 5 + * example: 7 + * threshold: + * type: number + * description: Threshold for edge preservation + * default: 1.0 + * example: 2.0 + * responses: + * 200: + * description: Time series denoised successfully + * 400: + * description: Invalid input parameters + */ +app.post('/api/timeseries/denoise', (req, res) => { + try { + const { series, windowSize = 5, threshold = 1.0 } = req.body; + const result = analytics.denoiseTimeSeries(series, windowSize, threshold); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/timeseries/seasonal-decomposition: + * post: + * summary: Decompose time series into trend, seasonal, and residual components + * description: Separates a time series into its constituent components using convolution-based methods + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * seasonalPeriod: + * type: integer + * description: Length of seasonal period + * example: 12 + * examples: + * monthly_sales: + * summary: Monthly sales with yearly seasonality + * value: + * series: + * values: [100, 120, 110, 130, 150, 180, 200, 190, 160, 140, 110, 105, 110, 130, 120, 140, 160, 190, 210, 200, 170, 150, 120, 115] + * labels: ["Jan Y1", "Feb Y1", "Mar Y1", "Apr Y1", "May Y1", "Jun Y1", "Jul Y1", "Aug Y1", "Sep Y1", "Oct Y1", "Nov Y1", "Dec Y1", "Jan Y2", "Feb Y2", "Mar Y2", "Apr Y2", "May Y2", "Jun Y2", "Jul Y2", "Aug Y2", "Sep Y2", "Oct Y2", "Nov Y2", "Dec Y2"] + * seasonalPeriod: 12 + * responses: + * 200: + * description: Seasonal decomposition completed successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * trend: + * type: array + * items: + * type: number + * description: Trend component + * seasonal: + * type: array + * items: + * type: number + * description: Seasonal component + * residual: + * type: array + * items: + * type: number + * description: Residual component + * examples: + * decomposition_result: + * value: + * success: true + * data: + * trend: [145.0, 145.5, 146.0, 146.5, 147.0, 147.5, 148.0, 148.5, 149.0, 149.5, 150.0, 150.5] + * seasonal: [-45, -25, -35, -15, 5, 35, 55, 45, 15, -5, -35, -45] + * residual: [0.5, -0.5, -1.0, -1.5, -2.0, -2.5, -3.0, -3.5, -4.0, -4.5, -5.0, -0.5] + * 400: + * description: Invalid input parameters + */ +app.post('/api/timeseries/seasonal-decomposition', (req, res) => { + try { + const { series, seasonalPeriod } = req.body; + const result = analytics.seasonalDecomposition(series, seasonalPeriod); + res.status(200).json({ success: true, data: result } as ApiResponse<{ + trend: number[], + seasonal: number[], + residual: number[] + }>); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse<{ + trend: number[], + seasonal: number[], + residual: number[] + }>); + } +}); + +/** + * @swagger + * /api/timeseries/auto-correlation: + * post: + * summary: Calculate auto-correlation function + * description: Computes auto-correlation to measure how a time series correlates with lagged versions of itself + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * maxLag: + * type: integer + * description: Maximum lag to compute correlation for + * example: 10 + * responses: + * 200: + * description: Auto-correlation calculated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiResponse' + * examples: + * autocorr_result: + * value: + * success: true + * data: [1.0, 0.8, 0.6, 0.4, 0.2, 0.1, 0.05, 0.02, 0.01, 0.005, 0.002] + * 400: + * description: Invalid input parameters + */ +app.post('/api/timeseries/auto-correlation', (req, res) => { + try { + const { series, maxLag } = req.body; + const result = analytics.autoCorrelation(series, maxLag); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/timeseries/ewma-convolution: + * post: + * summary: Exponentially Weighted Moving Average using convolution + * description: Computes EWMA using convolution approximation with finite impulse response + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * alpha: + * type: number + * description: Smoothing factor (0 < alpha < 1) + * example: 0.3 + * windowSize: + * type: integer + * description: Size of finite impulse response approximation + * default: 10 + * example: 15 + * responses: + * 200: + * description: EWMA calculated successfully using convolution + * 400: + * description: Invalid alpha value or input parameters + */ +app.post('/api/timeseries/ewma-convolution', (req, res) => { + try { + const { series, alpha, windowSize = 10 } = req.body; + const result = analytics.exponentialMovingAverageConvolution(series, alpha, windowSize); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/timeseries/weighted-ma-convolution: + * post: + * summary: Weighted Moving Average using convolution + * description: Computes weighted moving average with custom weights using convolution + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * weights: + * type: array + * items: + * type: number + * description: Custom weights for moving average + * example: [0.1, 0.2, 0.4, 0.2, 0.1] + * examples: + * custom_weights: + * summary: Custom weighted moving average + * value: + * series: + * values: [10, 15, 12, 18, 20, 16, 22, 19, 25, 21] + * labels: ["Week1", "Week2", "Week3", "Week4", "Week5", "Week6", "Week7", "Week8", "Week9", "Week10"] + * weights: [0.1, 0.2, 0.4, 0.2, 0.1] + * responses: + * 200: + * description: Weighted moving average calculated successfully + * 400: + * description: Invalid weights or input parameters + */ +app.post('/api/timeseries/weighted-ma-convolution', (req, res) => { + try { + const { series, weights } = req.body; + const result = analytics.weightedMovingAverageConvolution(series, weights); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/signal/convolve1d: + * post: + * summary: Perform 1D convolution + * description: Performs 1D convolution between a signal and kernel with various boundary conditions + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * signal: + * type: array + * items: + * type: number + * description: Input signal array + * example: [1, 2, 3, 4, 5] + * kernel: + * type: array + * items: + * type: number + * description: Convolution kernel array + * example: [0.25, 0.5, 0.25] + * options: + * type: object + * properties: + * mode: + * type: string + * enum: ['full', 'same', 'valid'] + * description: Convolution mode + * default: 'full' + * boundary: + * type: string + * enum: ['zero', 'reflect', 'symmetric'] + * description: Boundary condition + * default: 'zero' + * examples: + * smoothing: + * summary: Signal smoothing with Gaussian kernel + * value: + * signal: [1, 4, 2, 8, 3, 7, 1, 5] + * kernel: [0.25, 0.5, 0.25] + * options: + * mode: "same" + * + * SeasonalDecompositionResult: + * type: object + * properties: + * trend: + * type: array + * items: + * type: number + * description: Trend component of the time series + * seasonal: + * type: array + * items: + * type: number + * description: Seasonal component of the time series + * residual: + * type: array + * items: + * type: number + * description: Residual component of the time series + * example: + * trend: [145.0, 145.5, 146.0, 146.5, 147.0, 147.5] + * seasonal: [-45, -25, -35, -15, 5, 35] + * residual: [0.5, -0.5, -1.0, -1.5, -2.0, -2.5] + * + * ConvolutionOptions: + * type: object + * properties: + * mode: + * type: string + * enum: ['full', 'same', 'valid'] + * description: Convolution mode + * default: 'full' + * boundary: + * type: string + * enum: ['zero', 'reflect', 'symmetric'] + * description: Boundary condition for padding + * default: 'zero' + * example: + * mode: "same" + * boundary: "reflect" + * boundary: "reflect" + * responses: + * 200: + * description: 1D convolution completed successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * $ref: '#/components/schemas/ConvolutionResult1D' + * 400: + * description: Invalid input data + */ +app.post('/api/signal/convolve1d', (req, res) => { + try { + const { signal, kernel, options } = req.body; + const result = analytics.convolve1D(signal, kernel, options); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/image/convolve2d: + * post: + * summary: Perform 2D convolution + * description: Performs 2D convolution between a matrix and kernel for image processing or filtering + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * matrix: + * type: array + * items: + * type: array + * items: + * type: number + * description: Input 2D matrix + * example: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + * kernel: + * type: array + * items: + * type: array + * items: + * type: number + * description: 2D convolution kernel + * example: [[0, -1, 0], [-1, 4, -1], [0, -1, 0]] + * options: + * type: object + * properties: + * mode: + * type: string + * enum: ['full', 'same', 'valid'] + * description: Convolution mode + * default: 'full' + * boundary: + * type: string + * enum: ['zero', 'reflect', 'symmetric'] + * description: Boundary condition + * default: 'zero' + * examples: + * edge_detection: + * summary: Edge detection with Laplacian kernel + * value: + * matrix: [[10, 20, 30], [40, 50, 60], [70, 80, 90]] + * kernel: [[0, -1, 0], [-1, 4, -1], [0, -1, 0]] + * options: + * mode: "same" + * boundary: "zero" + * responses: + * 200: + * description: 2D convolution completed successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * $ref: '#/components/schemas/ConvolutionResult2D' + * 400: + * description: Invalid input data + */ +app.post('/api/image/convolve2d', (req, res) => { + try { + const { matrix, kernel, options } = req.body; + const result = analytics.convolve2D(matrix, kernel, options); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/kernels/gaussian2d: + * post: + * summary: Generate 2D Gaussian kernel + * description: Creates a 2D Gaussian blur kernel with specified size and sigma + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * size: + * type: integer + * description: Kernel size (must be odd) + * example: 5 + * sigma: + * type: number + * description: Standard deviation for Gaussian + * default: 1.0 + * example: 1.5 + * responses: + * 200: + * description: Gaussian kernel generated successfully + * 400: + * description: Invalid size (must be odd and positive) + */ +app.post('/api/kernels/gaussian2d', (req, res) => { + try { + const { size, sigma = 1.0 } = req.body; + const result = analytics.generateGaussianKernel(size, sigma); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/kernels/gaussian1d: + * post: + * summary: Generate 1D Gaussian kernel + * description: Creates a 1D Gaussian smoothing kernel + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * size: + * type: integer + * description: Kernel size (must be odd) + * example: 7 + * sigma: + * type: number + * description: Standard deviation for Gaussian + * default: 1.0 + * example: 2.0 + * responses: + * 200: + * description: 1D Gaussian kernel generated successfully + * 400: + * description: Invalid parameters + */ +app.post('/api/kernels/gaussian1d', (req, res) => { + try { + const { size, sigma = 1.0 } = req.body; + const result = analytics.generateGaussianKernel1D(size, sigma); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/kernels/sobel: + * post: + * summary: Generate Sobel edge detection kernel + * description: Creates a Sobel kernel for edge detection in X or Y direction + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * direction: + * type: string + * enum: ['x', 'y'] + * description: Edge detection direction + * default: 'x' + * example: 'x' + * responses: + * 200: + * description: Sobel kernel generated successfully + * 400: + * description: Invalid direction + */ +app.post('/api/kernels/sobel', (req, res) => { + try { + const { direction = 'x' } = req.body; + const result = analytics.generateSobelKernel(direction as 'x' | 'y'); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/kernels/laplacian: + * get: + * summary: Generate Laplacian kernel + * description: Creates a Laplacian edge detection kernel + * responses: + * 200: + * description: Laplacian kernel generated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: array + * items: + * type: array + * items: + * type: number + * example: + * success: true + * data: [[0, -1, 0], [-1, 4, -1], [0, -1, 0]] + */ +app.get('/api/kernels/laplacian', (req, res) => { + try { + const result = analytics.generateLaplacianKernel(); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/kernels/box: + * post: + * summary: Generate box/average blur kernel + * description: Creates a box blur kernel with uniform weights + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * size: + * type: integer + * description: Kernel size (must be odd and positive) + * example: 3 + * responses: + * 200: + * description: Box kernel generated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: array + * items: + * type: array + * items: + * type: number + * example: + * success: true + * data: [[0.111, 0.111, 0.111], [0.111, 0.111, 0.111], [0.111, 0.111, 0.111]] + * 400: + * description: Invalid size parameter + */ +app.post('/api/kernels/box', (req, res) => { + try { + const { size } = req.body; + const result = analytics.generateBoxKernel(size); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/kernels/average1d: + * post: + * summary: Generate 1D average kernel + * description: Creates a 1D moving average kernel + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * size: + * type: integer + * description: Kernel size (must be positive) + * example: 5 + * responses: + * 200: + * description: 1D average kernel generated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: array + * items: + * type: number + * example: + * success: true + * data: [0.2, 0.2, 0.2, 0.2, 0.2] + * 400: + * description: Invalid size parameter + */ +app.post('/api/kernels/average1d', (req, res) => { + try { + const { size } = req.body; + const result = analytics.generateAverageKernel1D(size); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + +/** + * @swagger + * /api/kernels/difference1d: + * get: + * summary: Generate 1D difference kernel + * description: Creates a 1D difference kernel for edge detection [-1, 0, 1] + * responses: + * 200: + * description: 1D difference kernel generated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: array + * items: + * type: number + * example: + * success: true + * data: [-1, 0, 1] + */ +app.get('/api/kernels/difference1d', (req, res) => { + try { + const result = analytics.generateDifferenceKernel1D(); + res.status(200).json({ success: true, data: result } as ApiResponse); + } catch (error) { + const errorMessage = handleError(error); + res.status(400).json({ success: false, error: errorMessage } as ApiResponse); + } +}); + + +// ======================================== +// SWAGGER COMPONENTS +// ======================================== + +/** + * @swagger + * components: + * schemas: + * DataSeries: + * type: object + * required: + * - values + * properties: + * values: + * type: array + * items: + * type: number + * description: Array of numerical values + * example: [1, 2, 3, 4, 5] + * labels: + * type: array + * items: + * type: string + * description: Optional labels for the values + * example: ["Jan", "Feb", "Mar", "Apr", "May"] + * + * DataMatrix: + * type: object + * required: + * - data + * properties: + * data: + * type: array + * items: + * type: array + * items: + * type: number + * description: 2D array of numerical values + * example: [[1, 2], [3, 4], [5, 6]] + * columns: + * type: array + * items: + * type: string + * description: Optional column names + * example: ["x", "y"] + * rows: + * type: array + * items: + * type: string + * description: Optional row names + * example: ["row1", "row2", "row3"] + * + * Condition: + * type: object + * required: + * - field + * - operator + * - value + * properties: + * field: + * type: string + * description: Field name to apply condition on + * example: "value" + * operator: + * type: string + * enum: [">", "<", "=", ">=", "<=", "!="] + * description: Comparison operator + * example: ">" + * value: + * oneOf: + * - type: number + * - type: string + * description: Value to compare against + * example: 10 + * + * ApiResponse: + * type: object + * properties: + * success: + * type: boolean + * description: Whether the request was successful + * data: + * description: Response data (varies by endpoint) + * error: + * type: string + * description: Error message if success is false + * + * TimeSeriesOptions: + * type: object + * properties: + * windowSize: + * type: integer + * description: Size of processing window + * minimum: 1 + * sigma: + * type: number + * description: Standard deviation parameter + * minimum: 0 + * threshold: + * type: number + * description: Threshold parameter for filtering + * minimum: 0 + * cutoffRatio: + * type: number + * description: Cutoff ratio for filtering (0-1) + * minimum: 0 + * maximum: 1 + * example: + * windowSize: 5 + * sigma: 1.0 + * threshold: 2.0 + * cutoffRatio: 0.1 + * + * WeightedMARequest: + * type: object + * required: + * - series + * - weights + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * weights: + * type: array + * items: + * type: number + * description: Custom weights for moving average (will be normalized) + * example: [0.1, 0.2, 0.4, 0.2, 0.1] + * + * EWMARequest: + * type: object + * required: + * - series + * - alpha + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * alpha: + * type: number + * description: Smoothing factor (0 < alpha < 1) + * minimum: 0 + * maximum: 1 + * example: 0.3 + * windowSize: + * type: integer + * description: Finite impulse response approximation size + * minimum: 1 + * default: 10 + * example: 15 + * + * AutoCorrelationRequest: + * type: object + * required: + * - series + * - maxLag + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * maxLag: + * type: integer + * description: Maximum lag to compute correlation for + * minimum: 1 + * example: 20 + * + * PeakDetectionResult: + * type: object + * properties: + * values: + * type: array + * items: + * type: number + * description: Peak detection response values (positive = peak, negative = valley) + * peaks: + * type: array + * items: + * type: object + * properties: + * index: + * type: integer + * description: Index position of peak + * value: + * type: number + * description: Original value at peak + * intensity: + * type: number + * description: Peak intensity from convolution + * description: Detected peak positions and intensities + * example: + * values: [-1.5, 2.8, -0.5, 3.2, -2.1, 1.9] + * peaks: [{"index": 1, "value": 25.5, "intensity": 2.8}, {"index": 3, "value": 28.2, "intensity": 3.2}] + * + * GaussianKernelRequest: + * type: object + * required: + * - size + * properties: + * size: + * type: integer + * description: Kernel size (must be odd and positive) + * minimum: 3 + * example: 7 + * sigma: + * type: number + * description: Standard deviation for Gaussian distribution + * minimum: 0 + * default: 1.0 + * example: 1.5 + * + * SobelKernelRequest: + * type: object + * properties: + * direction: + * type: string + * enum: ['x', 'y'] + * description: Direction for edge detection + * default: 'x' + * example: 'x' + * + * BoxKernelRequest: + * type: object + * required: + * - size + * properties: + * size: + * type: integer + * description: Size of box kernel (must be odd and positive) + * minimum: 1 + * example: 5 + * + * AverageKernel1DRequest: + * type: object + * required: + * - size + * properties: + * size: + * type: integer + * description: Size of average kernel (must be positive) + * minimum: 1 + * example: 3 + * + * Convolution1DRequest: + * type: object + * required: + * - signal + * - kernel + * properties: + * signal: + * type: array + * items: + * type: number + * description: Input signal array + * example: [1, 4, 2, 8, 3, 7, 1, 5] + * kernel: + * type: array + * items: + * type: number + * description: Convolution kernel array + * example: [0.25, 0.5, 0.25] + * options: + * $ref: '#/components/schemas/ConvolutionOptions' + * + * Convolution2DRequest: + * type: object + * required: + * - matrix + * - kernel + * properties: + * matrix: + * type: array + * items: + * type: array + * items: + * type: number + * description: Input 2D matrix + * example: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + * kernel: + * type: array + * items: + * type: array + * items: + * type: number + * description: 2D convolution kernel + * example: [[0, -1, 0], [-1, 4, -1], [0, -1, 0]] + * options: + * $ref: '#/components/schemas/ConvolutionOptions' + * + * SeasonalDecompositionRequest: + * type: object + * required: + * - series + * - seasonalPeriod + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * seasonalPeriod: + * type: integer + * description: Length of seasonal period (e.g., 12 for monthly data with yearly seasonality) + * minimum: 2 + * example: 12 + * + * TimeSeriesProcessingRequest: + * type: object + * required: + * - series + * properties: + * series: + * $ref: '#/components/schemas/DataSeries' + * windowSize: + * type: integer + * description: Processing window size + * minimum: 1 + * example: 7 + * sigma: + * type: number + * description: Standard deviation parameter + * minimum: 0 + * example: 1.0 + * threshold: + * type: number + * description: Threshold parameter + * minimum: 0 + * example: 2.0 + * cutoffRatio: + * type: number + * description: Cutoff ratio (0-1) + * minimum: 0 + * maximum: 1 + * example: 0.15 + * + * # Response wrapper for arrays + * ArrayApiResponse: + * type: object + * properties: + * success: + * type: boolean + * description: Whether the request was successful + * data: + * type: array + * items: + * type: number + * description: Array of numerical results + * error: + * type: string + * description: Error message if success is false + * example: + * success: true + * data: [1.5, 2.3, 3.1, 2.8, 4.2] + * + * # Response wrapper for 2D arrays + * Matrix2DApiResponse: + * type: object + * properties: + * success: + * type: boolean + * description: Whether the request was successful + * data: + * type: array + * items: + * type: array + * items: + * type: number + * description: 2D array of numerical results + * error: + * type: string + * description: Error message if success is false + * example: + * success: true + * data: [[0.111, 0.111, 0.111], [0.111, 0.111, 0.111], [0.111, 0.111, 0.111]] + * + * # Specific response types + * ConvolutionResult1DResponse: + * allOf: + * - $ref: '#/components/schemas/ApiResponse' + * - type: object + * properties: + * data: + * $ref: '#/components/schemas/ConvolutionResult1D' + * + * ConvolutionResult2DResponse: + * allOf: + * - $ref: '#/components/schemas/ApiResponse' + * - type: object + * properties: + * data: + * $ref: '#/components/schemas/ConvolutionResult2D' + * + * SeasonalDecompositionResponse: + * allOf: + * - $ref: '#/components/schemas/ApiResponse' + * - type: object + * properties: + * data: + * $ref: '#/components/schemas/SeasonalDecompositionResult' + */ + +/** + * @swagger + * /api/docs/export/json: + * get: + * summary: Export API documentation as JSON + * description: Returns the complete OpenAPI specification in JSON format + * responses: + * 200: + * description: OpenAPI specification in JSON format + * content: + * application/json: + * schema: + * type: object + */ +app.get('/api/docs/export/json', (req, res) => { + res.setHeader('Content-Disposition', 'attachment; filename="api-documentation.json"'); + res.setHeader('Content-Type', 'application/json'); + res.json(swaggerSpec); +}); + +/** + * @swagger + * /api/docs/export/yaml: + * get: + * summary: Export API documentation as YAML + * description: Returns the complete OpenAPI specification in YAML format + * responses: + * 200: + * description: OpenAPI specification in YAML format + * content: + * text/yaml: + * schema: + * type: string + */ +app.get('/api/docs/export/yaml', (req, res) => { + const yaml = require('js-yaml'); + const yamlString = yaml.dump(swaggerSpec, { indent: 2 }); + + res.setHeader('Content-Disposition', 'attachment; filename="api-documentation.yaml"'); + res.setHeader('Content-Type', 'text/yaml'); + res.send(yamlString); +}); + +/** + * @swagger + * /api/docs/export/html: + * get: + * summary: Export API documentation as HTML + * description: Returns a standalone HTML file with the complete API documentation + * responses: + * 200: + * description: Standalone HTML documentation + * content: + * text/html: + * schema: + * type: string + */ +app.get('/api/docs/export/html', (req, res) => { + const htmlTemplate = ` + + + + + + API Documentation + + + + +
+ + + + +`; + + res.setHeader('Content-Disposition', 'attachment; filename="api-documentation.html"'); + res.setHeader('Content-Type', 'text/html'); + res.send(htmlTemplate); +}); + + +// ======================================== +// ERROR HANDLING +// ======================================== + +app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { + console.error(err.stack); + res.status(500).json({ success: false, error: 'Internal server error' } as ApiResponse); +}); + +app.use('*', (req, res) => { + res.status(404).json({ success: false, error: 'Endpoint not found' }); +}); + +// ======================================== +// SERVER STARTUP +// ======================================== + +app.listen(PORT, () => { + console.log(`Analytics API server running on port ${PORT}`); + console.log(`Health check: http://localhost:${PORT}/api/health`); + console.log(`API Documentation: http://localhost:${PORT}/api-docs`); +}); + +export default app; \ No newline at end of file