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
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue