server update, add endpoints
server update (server_convolution.ts) add endpoints of peaks detection, valleys detection, vertices (including peaks and valleys) detection, outliers detection
This commit is contained in:
parent
4cd58e04d4
commit
e8e0e6de2a
3 changed files with 1932 additions and 175 deletions
|
|
@ -99,6 +99,9 @@ function applyBoundary1D(signal: number[], padding: number, boundary: string): n
|
||||||
* @param options - Convolution options (mode, boundary)
|
* @param options - Convolution options (mode, boundary)
|
||||||
* @returns Convolution result with metadata
|
* @returns Convolution result with metadata
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* [CORRECTED] Performs 1D convolution between signal and kernel
|
||||||
|
*/
|
||||||
export function convolve1D(
|
export function convolve1D(
|
||||||
signal: number[],
|
signal: number[],
|
||||||
kernel: number[],
|
kernel: number[],
|
||||||
|
|
@ -107,40 +110,54 @@ export function convolve1D(
|
||||||
validateArray(signal, 'Signal');
|
validateArray(signal, 'Signal');
|
||||||
validateArray(kernel, 'Kernel');
|
validateArray(kernel, 'Kernel');
|
||||||
|
|
||||||
const { mode = 'same', boundary = 'reflect' } = options;
|
const { mode = 'full', boundary = 'zero' } = options;
|
||||||
|
|
||||||
// Flip kernel for convolution (not correlation)
|
|
||||||
const flippedKernel = [...kernel].reverse();
|
const flippedKernel = [...kernel].reverse();
|
||||||
|
|
||||||
const signalLen = signal.length;
|
const signalLen = signal.length;
|
||||||
const kernelLen = flippedKernel.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 :
|
const outputLength = mode === 'full' ? signalLen + kernelLen - 1 :
|
||||||
mode === 'same' ? signalLen :
|
mode === 'same' ? signalLen :
|
||||||
signalLen - kernelLen + 1;
|
signalLen - kernelLen + 1;
|
||||||
|
|
||||||
|
const result: number[] = new Array(outputLength);
|
||||||
|
|
||||||
const startIdx = mode === 'valid' ? 0 :
|
const halfKernelLen = Math.floor(kernelLen / 2);
|
||||||
mode === 'same' ? Math.floor(kernelLen / 2) : 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < outputLength; i++) {
|
for (let i = 0; i < outputLength; i++) {
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
for (let j = 0; j < kernelLen; j++) {
|
for (let j = 0; j < kernelLen; j++) {
|
||||||
const signalIdx = startIdx + i + j;
|
let signalIdx: number;
|
||||||
if (signalIdx >= 0 && signalIdx < paddedSignal.length) {
|
|
||||||
sum += paddedSignal[signalIdx] * flippedKernel[j];
|
switch (mode) {
|
||||||
|
case 'full':
|
||||||
|
signalIdx = i - j;
|
||||||
|
break;
|
||||||
|
case 'same':
|
||||||
|
signalIdx = i - halfKernelLen + j;
|
||||||
|
break;
|
||||||
|
case 'valid':
|
||||||
|
signalIdx = i + j;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle boundary conditions
|
||||||
|
if (signalIdx >= 0 && signalIdx < signalLen) {
|
||||||
|
sum += signal[signalIdx] * flippedKernel[j];
|
||||||
|
} else if (boundary !== 'zero' && (mode === 'full' || mode === 'same')) {
|
||||||
|
// This is a simplified boundary handler for the logic. Your more complex handler can be used here.
|
||||||
|
let boundaryIdx = signalIdx;
|
||||||
|
if (signalIdx < 0) {
|
||||||
|
boundaryIdx = boundary === 'reflect' ? -signalIdx -1 : -signalIdx;
|
||||||
|
} else if (signalIdx >= signalLen) {
|
||||||
|
boundaryIdx = boundary === 'reflect' ? 2 * signalLen - signalIdx - 1 : 2 * signalLen - signalIdx - 2;
|
||||||
|
}
|
||||||
|
boundaryIdx = Math.max(0, Math.min(signalLen - 1, boundaryIdx));
|
||||||
|
sum += signal[boundaryIdx] * flippedKernel[j];
|
||||||
|
}
|
||||||
|
// If boundary is 'zero', we add nothing, which is correct.
|
||||||
}
|
}
|
||||||
result.push(sum);
|
result[i] = sum;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
1801
server_convolution.ts
Normal file
1801
server_convolution.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -223,26 +223,6 @@ export class SignalProcessor {
|
||||||
return convolve1D(signal, reversedTemplate, { mode: 'same' }).values;
|
return convolve1D(signal, reversedTemplate, { mode: 'same' }).values;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create Savitzky-Golay smoothing kernel
|
|
||||||
*/
|
|
||||||
private static createSavitzkyGolayKernel(windowSize: number, polyOrder: number): number[] {
|
|
||||||
// Simplified Savitzky-Golay kernel generation
|
|
||||||
// For a more complete implementation, you'd solve the least squares problem
|
|
||||||
const halfWindow = Math.floor(windowSize / 2);
|
|
||||||
const kernel: number[] = new Array(windowSize);
|
|
||||||
|
|
||||||
// For simplicity, use predetermined coefficients for common cases
|
|
||||||
if (windowSize === 5 && polyOrder === 2) {
|
|
||||||
return [-3, 12, 17, 12, -3].map(x => x / 35);
|
|
||||||
} else if (windowSize === 7 && polyOrder === 2) {
|
|
||||||
return [-2, 3, 6, 7, 6, 3, -2].map(x => x / 21);
|
|
||||||
} else {
|
|
||||||
// Fallback to simple moving average
|
|
||||||
return new Array(windowSize).fill(1 / windowSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply median filtering (note: not convolution-based, but commonly used with other filters)
|
* Apply median filtering (note: not convolution-based, but commonly used with other filters)
|
||||||
*/
|
*/
|
||||||
|
|
@ -284,145 +264,123 @@ export class SignalProcessor {
|
||||||
/**
|
/**
|
||||||
* Detect peaks using convolution-based edge detection
|
* Detect peaks using convolution-based edge detection
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* [REWRITTEN] Detects peaks (local maxima) in a 1D signal.
|
||||||
|
* This is a more robust method that directly finds local maxima.
|
||||||
|
*/
|
||||||
static detectPeaksConvolution(signal: number[], options: {
|
static detectPeaksConvolution(signal: number[], options: {
|
||||||
method?: 'gradient' | 'laplacian' | 'dog';
|
smoothWindow?: number;
|
||||||
threshold?: number;
|
threshold?: number;
|
||||||
minDistance?: number;
|
minDistance?: number;
|
||||||
} = {}): { index: number; value: number; strength: number }[] {
|
} = {}): { index: number; value: number }[] {
|
||||||
const { method = 'gradient', threshold = 0.1, minDistance = 1 } = options;
|
const { smoothWindow = 0, threshold = -Infinity, minDistance = 1 } = options;
|
||||||
|
|
||||||
let edgeResponse: number[];
|
let processedSignal = signal;
|
||||||
|
// Optionally smooth the signal first to reduce noise
|
||||||
switch (method) {
|
if (smoothWindow > 1) {
|
||||||
case 'gradient':
|
processedSignal = this.smooth(signal, { method: 'gaussian', windowSize: smoothWindow });
|
||||||
// First derivative to detect edges (peaks are positive edges)
|
|
||||||
const gradientKernel = [-1, 0, 1]; // Simple gradient
|
|
||||||
edgeResponse = convolve1D(signal, gradientKernel, { mode: 'same' }).values;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'laplacian':
|
|
||||||
// Second derivative to detect peaks (zero crossings)
|
|
||||||
const laplacianKernel = [1, -2, 1]; // 1D Laplacian
|
|
||||||
edgeResponse = convolve1D(signal, laplacianKernel, { mode: 'same' }).values;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'dog':
|
|
||||||
// Difference of Gaussians for multi-scale peak detection
|
|
||||||
const sigma1 = 1.0;
|
|
||||||
const sigma2 = 1.6;
|
|
||||||
const size = 9;
|
|
||||||
const gauss1 = ConvolutionKernels.gaussian1D(size, sigma1);
|
|
||||||
const gauss2 = ConvolutionKernels.gaussian1D(size, sigma2);
|
|
||||||
const dogKernel = gauss1.map((g1, i) => g1 - gauss2[i]);
|
|
||||||
edgeResponse = convolve1D(signal, dogKernel, { mode: 'same' }).values;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported peak detection method: ${method}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find local maxima in edge response
|
const peaks: { index: number; value: number }[] = [];
|
||||||
const peaks: { index: number; value: number; strength: number }[] = [];
|
|
||||||
|
// Find all points that are higher than their immediate neighbors
|
||||||
for (let i = 1; i < edgeResponse.length - 1; i++) {
|
for (let i = 1; i < processedSignal.length - 1; i++) {
|
||||||
const current = edgeResponse[i];
|
const prev = processedSignal[i - 1];
|
||||||
const left = edgeResponse[i - 1];
|
const curr = processedSignal[i];
|
||||||
const right = edgeResponse[i + 1];
|
const next = processedSignal[i + 1];
|
||||||
|
|
||||||
// For gradient method, look for positive peaks
|
if (curr > prev && curr > next && curr > threshold) {
|
||||||
// For Laplacian/DoG, look for zero crossings with positive slope
|
peaks.push({ index: i, value: signal[i] }); // Store index and ORIGINAL value
|
||||||
let isPeak = false;
|
|
||||||
let strength = 0;
|
|
||||||
|
|
||||||
if (method === 'gradient') {
|
|
||||||
isPeak = current > left && current > right && current > threshold;
|
|
||||||
strength = current;
|
|
||||||
} else {
|
|
||||||
// Zero crossing detection for Laplacian/DoG
|
|
||||||
isPeak = left < 0 && right > 0 && Math.abs(current) < threshold;
|
|
||||||
strength = Math.abs(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPeak) {
|
|
||||||
peaks.push({
|
|
||||||
index: i,
|
|
||||||
value: signal[i],
|
|
||||||
strength: strength
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply minimum distance constraint
|
// Check boundaries: Is the first or last point a peak?
|
||||||
if (minDistance > 1) {
|
if (processedSignal[0] > processedSignal[1] && processedSignal[0] > threshold) {
|
||||||
return this.enforceMinDistanceConv(peaks, minDistance);
|
peaks.unshift({ index: 0, value: signal[0] });
|
||||||
|
}
|
||||||
|
const last = processedSignal.length - 1;
|
||||||
|
if (processedSignal[last] > processedSignal[last - 1] && processedSignal[last] > threshold) {
|
||||||
|
peaks.push({ index: last, value: signal[last] });
|
||||||
}
|
}
|
||||||
|
|
||||||
return peaks;
|
// [CORRECTED LOGIC] Enforce minimum distance between peaks
|
||||||
|
if (minDistance < 2 || peaks.length <= 1) {
|
||||||
|
return peaks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort peaks by value, highest first
|
||||||
|
peaks.sort((a, b) => b.value - a.value);
|
||||||
|
|
||||||
|
const finalPeaks: { index: number; value: number }[] = [];
|
||||||
|
const removed = new Array(peaks.length).fill(false);
|
||||||
|
|
||||||
|
for (let i = 0; i < peaks.length; i++) {
|
||||||
|
if (!removed[i]) {
|
||||||
|
finalPeaks.push(peaks[i]);
|
||||||
|
// Remove other peaks within the minimum distance
|
||||||
|
for (let j = i + 1; j < peaks.length; j++) {
|
||||||
|
if (!removed[j] && Math.abs(peaks[i].index - peaks[j].index) < minDistance) {
|
||||||
|
removed[j] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalPeaks.sort((a, b) => a.index - b.index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect valleys using convolution (inverted peak detection)
|
* [REWRITTEN] Detects valleys (local minima) in a 1D signal.
|
||||||
*/
|
*/
|
||||||
static detectValleysConvolution(signal: number[], options: {
|
static detectValleysConvolution(signal: number[], options: {
|
||||||
method?: 'gradient' | 'laplacian' | 'dog';
|
smoothWindow?: number;
|
||||||
threshold?: number;
|
threshold?: number;
|
||||||
minDistance?: number;
|
minDistance?: number;
|
||||||
} = {}): { index: number; value: number; strength: number }[] {
|
} = {}): { index: number; value: number }[] {
|
||||||
// Invert signal for valley detection
|
|
||||||
const invertedSignal = signal.map(x => -x);
|
const invertedSignal = signal.map(x => -x);
|
||||||
const valleys = this.detectPeaksConvolution(invertedSignal, options);
|
const invertedThreshold = options.threshold !== undefined ? -options.threshold : undefined;
|
||||||
|
|
||||||
|
const invertedPeaks = this.detectPeaksConvolution(invertedSignal, { ...options, threshold: invertedThreshold });
|
||||||
|
|
||||||
// Convert back to original scale
|
return invertedPeaks.map(peak => ({
|
||||||
return valleys.map(valley => ({
|
index: peak.index,
|
||||||
...valley,
|
value: -peak.value,
|
||||||
value: -valley.value
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect outliers using convolution-based methods
|
* Detect outliers using convolution-based methods
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* [REWRITTEN] Detects outliers using more reliable and statistically sound methods.
|
||||||
|
*/
|
||||||
static detectOutliersConvolution(signal: number[], options: {
|
static detectOutliersConvolution(signal: number[], options: {
|
||||||
method?: 'gradient_variance' | 'median_diff' | 'local_deviation';
|
method?: 'local_deviation' | 'mean_diff';
|
||||||
windowSize?: number;
|
windowSize?: number;
|
||||||
threshold?: number;
|
threshold?: number;
|
||||||
} = {}): { index: number; value: number; outlierScore: number }[] {
|
} = {}): { index: number; value: number; outlierScore: number }[] {
|
||||||
const { method = 'gradient_variance', windowSize = 7, threshold = 2.0 } = options;
|
const { method = 'local_deviation', windowSize = 7, threshold = 3.0 } = options;
|
||||||
|
|
||||||
let outlierScores: number[];
|
let outlierScores: number[];
|
||||||
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case 'gradient_variance':
|
case 'mean_diff':
|
||||||
// Detect outliers using gradient variance
|
// Detects outliers by their difference from the local mean.
|
||||||
const gradientKernel = [-1, 0, 1];
|
const meanKernel = ConvolutionKernels.average1D(windowSize);
|
||||||
const gradient = convolve1D(signal, gradientKernel, { mode: 'same' }).values;
|
const localMean = convolve1D(signal, meanKernel, { mode: 'same' }).values;
|
||||||
|
outlierScores = signal.map((val, i) => Math.abs(val - localMean[i]));
|
||||||
// Convolve gradient with variance-detecting kernel
|
|
||||||
const varianceKernel = new Array(windowSize).fill(1).map((_, i) => {
|
|
||||||
const center = Math.floor(windowSize / 2);
|
|
||||||
return (i - center) ** 2;
|
|
||||||
});
|
|
||||||
const normalizedVarianceKernel = varianceKernel.map(v => v / varianceKernel.reduce((s, x) => s + x, 0));
|
|
||||||
|
|
||||||
outlierScores = convolve1D(gradient.map(g => g * g), normalizedVarianceKernel, { mode: 'same' }).values;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'median_diff':
|
|
||||||
// Detect outliers by difference from local median (approximated with convolution)
|
|
||||||
const medianApproxKernel = ConvolutionKernels.gaussian1D(windowSize, windowSize / 6);
|
|
||||||
const smoothed = convolve1D(signal, medianApproxKernel, { mode: 'same' }).values;
|
|
||||||
outlierScores = signal.map((val, i) => Math.abs(val - smoothed[i]));
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'local_deviation':
|
case 'local_deviation':
|
||||||
// Detect outliers using local standard deviation approximation
|
// A robust method using Z-score: how many local standard deviations away a point is.
|
||||||
const avgKernel = ConvolutionKernels.average1D(windowSize);
|
const avgKernel = ConvolutionKernels.average1D(windowSize);
|
||||||
const localMean = convolve1D(signal, avgKernel, { mode: 'same' }).values;
|
const localMeanValues = convolve1D(signal, avgKernel, { mode: 'same' }).values;
|
||||||
const squaredDiffs = signal.map((val, i) => (val - localMean[i]) ** 2);
|
const squaredDiffs = signal.map((val, i) => (val - localMeanValues[i]) ** 2);
|
||||||
const localVar = convolve1D(squaredDiffs, avgKernel, { mode: 'same' }).values;
|
const localVar = convolve1D(squaredDiffs, avgKernel, { mode: 'same' }).values;
|
||||||
outlierScores = signal.map((val, i) => {
|
outlierScores = signal.map((val, i) => {
|
||||||
const std = Math.sqrt(localVar[i]);
|
const std = Math.sqrt(localVar[i]);
|
||||||
return std > 0 ? Math.abs(val - localMean[i]) / std : 0;
|
return std > 1e-6 ? Math.abs(val - localMeanValues[i]) / std : 0;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -430,7 +388,7 @@ export class SignalProcessor {
|
||||||
throw new Error(`Unsupported outlier detection method: ${method}`);
|
throw new Error(`Unsupported outlier detection method: ${method}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find points exceeding threshold
|
// Find points exceeding the threshold
|
||||||
const outliers: { index: number; value: number; outlierScore: number }[] = [];
|
const outliers: { index: number; value: number; outlierScore: number }[] = [];
|
||||||
outlierScores.forEach((score, i) => {
|
outlierScores.forEach((score, i) => {
|
||||||
if (score > threshold) {
|
if (score > threshold) {
|
||||||
|
|
@ -448,51 +406,32 @@ export class SignalProcessor {
|
||||||
/**
|
/**
|
||||||
* Detect trend vertices (turning points) using convolution
|
* Detect trend vertices (turning points) using convolution
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* [CORRECTED] Detects trend vertices (turning points) by finding all peaks and valleys.
|
||||||
|
* This version fixes a bug that prevented valleys from being detected.
|
||||||
|
*/
|
||||||
static detectTrendVertices(signal: number[], options: {
|
static detectTrendVertices(signal: number[], options: {
|
||||||
method?: 'curvature' | 'sign_change' | 'momentum';
|
|
||||||
smoothingWindow?: number;
|
smoothingWindow?: number;
|
||||||
threshold?: number;
|
threshold?: number;
|
||||||
minDistance?: number;
|
minDistance?: number;
|
||||||
} = {}): { index: number; value: number; type: 'peak' | 'valley'; curvature: number }[] {
|
} = {}): { index: number; value: number; type: 'peak' | 'valley' }[] {
|
||||||
const {
|
const {
|
||||||
method = 'curvature',
|
|
||||||
smoothingWindow = 5,
|
smoothingWindow = 5,
|
||||||
threshold = 0.001,
|
threshold = 0, // CORRECTED: Changed default from -Infinity to a sensible 0
|
||||||
minDistance = 3
|
minDistance = 3
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// First, smooth the signal to reduce noise in trend detection
|
// Create the options object to pass down. The valley function will handle inverting the threshold itself.
|
||||||
const smoothed = this.smooth(signal, { method: 'gaussian', windowSize: smoothingWindow });
|
const detectionOptions = { smoothingWindow, threshold, minDistance };
|
||||||
|
|
||||||
|
const peaks = this.detectPeaksConvolution(signal, detectionOptions).map(p => ({ ...p, type: 'peak' as const }));
|
||||||
|
const valleys = this.detectValleysConvolution(signal, detectionOptions).map(v => ({ ...v, type: 'valley' as const }));
|
||||||
|
|
||||||
let vertices: { index: number; value: number; type: 'peak' | 'valley'; curvature: number }[] = [];
|
// Combine peaks and valleys and sort them by their index to get the sequence of trend changes
|
||||||
|
const vertices = [...peaks, ...valleys];
|
||||||
|
vertices.sort((a, b) => a.index - b.index);
|
||||||
|
|
||||||
switch (method) {
|
return vertices;
|
||||||
case 'curvature':
|
|
||||||
vertices = this.detectCurvatureVertices(smoothed, threshold);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'sign_change':
|
|
||||||
vertices = this.detectSignChangeVertices(smoothed, threshold);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'momentum':
|
|
||||||
vertices = this.detectMomentumVertices(smoothed, threshold);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported vertex detection method: ${method}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply minimum distance constraint
|
|
||||||
if (minDistance > 1) {
|
|
||||||
vertices = this.enforceMinDistanceVertices(vertices, minDistance);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map back to original signal values
|
|
||||||
return vertices.map(v => ({
|
|
||||||
...v,
|
|
||||||
value: signal[v.index] // Use original signal value, not smoothed
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue