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:
raymond 2025-09-10 04:09:20 +00:00
parent 4cd58e04d4
commit e8e0e6de2a
3 changed files with 1932 additions and 175 deletions

View file

@ -223,26 +223,6 @@ export class SignalProcessor {
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)
*/
@ -284,145 +264,123 @@ export class SignalProcessor {
/**
* 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: {
method?: 'gradient' | 'laplacian' | 'dog';
smoothWindow?: number;
threshold?: number;
minDistance?: number;
} = {}): { index: number; value: number; strength: number }[] {
const { method = 'gradient', threshold = 0.1, minDistance = 1 } = options;
let edgeResponse: number[];
switch (method) {
case 'gradient':
// 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}`);
} = {}): { index: number; value: number }[] {
const { smoothWindow = 0, threshold = -Infinity, minDistance = 1 } = options;
let processedSignal = signal;
// Optionally smooth the signal first to reduce noise
if (smoothWindow > 1) {
processedSignal = this.smooth(signal, { method: 'gaussian', windowSize: smoothWindow });
}
// Find local maxima in edge response
const peaks: { index: number; value: number; strength: number }[] = [];
for (let i = 1; i < edgeResponse.length - 1; i++) {
const current = edgeResponse[i];
const left = edgeResponse[i - 1];
const right = edgeResponse[i + 1];
// For gradient method, look for positive peaks
// For Laplacian/DoG, look for zero crossings with positive slope
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
});
const peaks: { index: number; value: number }[] = [];
// Find all points that are higher than their immediate neighbors
for (let i = 1; i < processedSignal.length - 1; i++) {
const prev = processedSignal[i - 1];
const curr = processedSignal[i];
const next = processedSignal[i + 1];
if (curr > prev && curr > next && curr > threshold) {
peaks.push({ index: i, value: signal[i] }); // Store index and ORIGINAL value
}
}
// Apply minimum distance constraint
if (minDistance > 1) {
return this.enforceMinDistanceConv(peaks, minDistance);
// Check boundaries: Is the first or last point a peak?
if (processedSignal[0] > processedSignal[1] && processedSignal[0] > threshold) {
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: {
method?: 'gradient' | 'laplacian' | 'dog';
smoothWindow?: number;
threshold?: number;
minDistance?: number;
} = {}): { index: number; value: number; strength: number }[] {
// Invert signal for valley detection
} = {}): { index: number; value: number }[] {
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 valleys.map(valley => ({
...valley,
value: -valley.value
return invertedPeaks.map(peak => ({
index: peak.index,
value: -peak.value,
}));
}
/**
* Detect outliers using convolution-based methods
*/
/**
* [REWRITTEN] Detects outliers using more reliable and statistically sound methods.
*/
static detectOutliersConvolution(signal: number[], options: {
method?: 'gradient_variance' | 'median_diff' | 'local_deviation';
method?: 'local_deviation' | 'mean_diff';
windowSize?: number;
threshold?: 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[];
switch (method) {
case 'gradient_variance':
// Detect outliers using gradient variance
const gradientKernel = [-1, 0, 1];
const gradient = convolve1D(signal, gradientKernel, { mode: 'same' }).values;
// 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]));
case 'mean_diff':
// Detects outliers by their difference from the local mean.
const meanKernel = ConvolutionKernels.average1D(windowSize);
const localMean = convolve1D(signal, meanKernel, { mode: 'same' }).values;
outlierScores = signal.map((val, i) => Math.abs(val - localMean[i]));
break;
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 localMean = convolve1D(signal, avgKernel, { mode: 'same' }).values;
const squaredDiffs = signal.map((val, i) => (val - localMean[i]) ** 2);
const localMeanValues = convolve1D(signal, avgKernel, { mode: 'same' }).values;
const squaredDiffs = signal.map((val, i) => (val - localMeanValues[i]) ** 2);
const localVar = convolve1D(squaredDiffs, avgKernel, { mode: 'same' }).values;
outlierScores = signal.map((val, 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;
@ -430,7 +388,7 @@ export class SignalProcessor {
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 }[] = [];
outlierScores.forEach((score, i) => {
if (score > threshold) {
@ -448,51 +406,32 @@ export class SignalProcessor {
/**
* 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: {
method?: 'curvature' | 'sign_change' | 'momentum';
smoothingWindow?: number;
threshold?: number;
minDistance?: number;
} = {}): { index: number; value: number; type: 'peak' | 'valley'; curvature: number }[] {
} = {}): { index: number; value: number; type: 'peak' | 'valley' }[] {
const {
method = 'curvature',
smoothingWindow = 5,
threshold = 0.001,
threshold = 0, // CORRECTED: Changed default from -Infinity to a sensible 0
minDistance = 3
} = options;
// First, smooth the signal to reduce noise in trend detection
const smoothed = this.smooth(signal, { method: 'gaussian', windowSize: smoothingWindow });
// Create the options object to pass down. The valley function will handle inverting the threshold itself.
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) {
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
}));
return vertices;
}
/**