dspx 0.1.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +185 -0
- package/.vscode/c_cpp_properties.json +17 -0
- package/.vscode/settings.json +68 -0
- package/.vscode/tasks.json +28 -0
- package/DISCLAIMER.md +32 -0
- package/LICENSE +21 -0
- package/README.md +1803 -0
- package/ROADMAP.md +192 -0
- package/TECHNICAL_DEBT.md +165 -0
- package/binding.gyp +65 -0
- package/docs/ADVANCED_LOGGER_FEATURES.md +598 -0
- package/docs/AUTHENTICATION_SECURITY.md +396 -0
- package/docs/BACKEND_IMPROVEMENTS.md +399 -0
- package/docs/CHEBYSHEV_BIQUAD_EQ_IMPLEMENTATION.md +405 -0
- package/docs/FFT_IMPLEMENTATION.md +490 -0
- package/docs/FFT_IMPROVEMENTS_SUMMARY.md +387 -0
- package/docs/FFT_USER_GUIDE.md +494 -0
- package/docs/FILTERS_IMPLEMENTATION.md +260 -0
- package/docs/FILTER_API_GUIDE.md +418 -0
- package/docs/FIR_SIMD_OPTIMIZATION.md +175 -0
- package/docs/LOGGER_API_REFERENCE.md +350 -0
- package/docs/NOTCH_FILTER_QUICK_REF.md +121 -0
- package/docs/PHASE2_TESTS_AND_NOTCH_FILTER.md +341 -0
- package/docs/PHASES_5_7_SUMMARY.md +403 -0
- package/docs/PIPELINE_FILTER_INTEGRATION.md +446 -0
- package/docs/SIMD_OPTIMIZATIONS.md +211 -0
- package/docs/TEST_MIGRATION_SUMMARY.md +173 -0
- package/docs/TIMESERIES_IMPLEMENTATION_SUMMARY.md +322 -0
- package/docs/TIMESERIES_QUICK_REF.md +85 -0
- package/docs/advanced.md +559 -0
- package/docs/time-series-guide.md +617 -0
- package/docs/time-series-migration.md +376 -0
- package/jest.config.js +37 -0
- package/package.json +42 -0
- package/prebuilds/linux-x64/dsp-ts-redis.node +0 -0
- package/prebuilds/win32-x64/dsp-ts-redis.node +0 -0
- package/scripts/test.js +24 -0
- package/src/build/dsp-ts-redis.node +0 -0
- package/src/native/DspPipeline.cc +675 -0
- package/src/native/DspPipeline.h +44 -0
- package/src/native/FftBindings.cc +817 -0
- package/src/native/FilterBindings.cc +1001 -0
- package/src/native/IDspStage.h +53 -0
- package/src/native/adapters/InterpolatorStage.h +201 -0
- package/src/native/adapters/MeanAbsoluteValueStage.h +289 -0
- package/src/native/adapters/MovingAverageStage.h +306 -0
- package/src/native/adapters/RectifyStage.h +88 -0
- package/src/native/adapters/ResamplerStage.h +238 -0
- package/src/native/adapters/RmsStage.h +299 -0
- package/src/native/adapters/SscStage.h +121 -0
- package/src/native/adapters/VarianceStage.h +307 -0
- package/src/native/adapters/WampStage.h +114 -0
- package/src/native/adapters/WaveformLengthStage.h +115 -0
- package/src/native/adapters/ZScoreNormalizeStage.h +326 -0
- package/src/native/core/FftEngine.cc +441 -0
- package/src/native/core/FftEngine.h +224 -0
- package/src/native/core/FirFilter.cc +324 -0
- package/src/native/core/FirFilter.h +149 -0
- package/src/native/core/IirFilter.cc +576 -0
- package/src/native/core/IirFilter.h +210 -0
- package/src/native/core/MovingAbsoluteValueFilter.cc +17 -0
- package/src/native/core/MovingAbsoluteValueFilter.h +135 -0
- package/src/native/core/MovingAverageFilter.cc +18 -0
- package/src/native/core/MovingAverageFilter.h +135 -0
- package/src/native/core/MovingFftFilter.cc +291 -0
- package/src/native/core/MovingFftFilter.h +203 -0
- package/src/native/core/MovingVarianceFilter.cc +194 -0
- package/src/native/core/MovingVarianceFilter.h +114 -0
- package/src/native/core/MovingZScoreFilter.cc +215 -0
- package/src/native/core/MovingZScoreFilter.h +113 -0
- package/src/native/core/Policies.h +352 -0
- package/src/native/core/RmsFilter.cc +18 -0
- package/src/native/core/RmsFilter.h +131 -0
- package/src/native/core/SscFilter.cc +16 -0
- package/src/native/core/SscFilter.h +137 -0
- package/src/native/core/WampFilter.cc +16 -0
- package/src/native/core/WampFilter.h +101 -0
- package/src/native/core/WaveformLengthFilter.cc +17 -0
- package/src/native/core/WaveformLengthFilter.h +98 -0
- package/src/native/utils/CircularBufferArray.cc +336 -0
- package/src/native/utils/CircularBufferArray.h +62 -0
- package/src/native/utils/CircularBufferVector.cc +145 -0
- package/src/native/utils/CircularBufferVector.h +45 -0
- package/src/native/utils/NapiUtils.cc +53 -0
- package/src/native/utils/NapiUtils.h +21 -0
- package/src/native/utils/SimdOps.h +870 -0
- package/src/native/utils/SlidingWindowFilter.cc +239 -0
- package/src/native/utils/SlidingWindowFilter.h +159 -0
- package/src/native/utils/TimeSeriesBuffer.cc +205 -0
- package/src/native/utils/TimeSeriesBuffer.h +140 -0
- package/src/ts/CircularLogBuffer.ts +87 -0
- package/src/ts/DriftDetector.ts +331 -0
- package/src/ts/TopicRouter.ts +428 -0
- package/src/ts/__tests__/AdvancedDsp.test.ts +585 -0
- package/src/ts/__tests__/AuthAndEdgeCases.test.ts +241 -0
- package/src/ts/__tests__/Chaining.test.ts +387 -0
- package/src/ts/__tests__/ChebyshevBiquad.test.ts +229 -0
- package/src/ts/__tests__/CircularLogBuffer.test.ts +158 -0
- package/src/ts/__tests__/DriftDetector.test.ts +389 -0
- package/src/ts/__tests__/Fft.test.ts +484 -0
- package/src/ts/__tests__/ListState.test.ts +153 -0
- package/src/ts/__tests__/Logger.test.ts +208 -0
- package/src/ts/__tests__/LoggerAdvanced.test.ts +319 -0
- package/src/ts/__tests__/LoggerMinor.test.ts +247 -0
- package/src/ts/__tests__/MeanAbsoluteValue.test.ts +398 -0
- package/src/ts/__tests__/MovingAverage.test.ts +322 -0
- package/src/ts/__tests__/RMS.test.ts +315 -0
- package/src/ts/__tests__/Rectify.test.ts +272 -0
- package/src/ts/__tests__/Redis.test.ts +456 -0
- package/src/ts/__tests__/SlopeSignChange.test.ts +166 -0
- package/src/ts/__tests__/Tap.test.ts +164 -0
- package/src/ts/__tests__/TimeBasedExpiration.test.ts +124 -0
- package/src/ts/__tests__/TimeBasedRmsAndMav.test.ts +231 -0
- package/src/ts/__tests__/TimeBasedVarianceAndZScore.test.ts +284 -0
- package/src/ts/__tests__/TimeSeries.test.ts +254 -0
- package/src/ts/__tests__/TopicRouter.test.ts +332 -0
- package/src/ts/__tests__/TopicRouterAdvanced.test.ts +483 -0
- package/src/ts/__tests__/TopicRouterPriority.test.ts +487 -0
- package/src/ts/__tests__/Variance.test.ts +509 -0
- package/src/ts/__tests__/WaveformLength.test.ts +147 -0
- package/src/ts/__tests__/WillisonAmplitude.test.ts +197 -0
- package/src/ts/__tests__/ZScoreNormalize.test.ts +459 -0
- package/src/ts/advanced-dsp.ts +566 -0
- package/src/ts/backends.ts +1137 -0
- package/src/ts/bindings.ts +1225 -0
- package/src/ts/easter-egg.ts +42 -0
- package/src/ts/examples/MeanAbsoluteValue/test-state.ts +99 -0
- package/src/ts/examples/MeanAbsoluteValue/test-streaming.ts +269 -0
- package/src/ts/examples/MovingAverage/test-state.ts +85 -0
- package/src/ts/examples/MovingAverage/test-streaming.ts +188 -0
- package/src/ts/examples/RMS/test-state.ts +97 -0
- package/src/ts/examples/RMS/test-streaming.ts +253 -0
- package/src/ts/examples/Rectify/test-state.ts +107 -0
- package/src/ts/examples/Rectify/test-streaming.ts +242 -0
- package/src/ts/examples/Variance/test-state.ts +195 -0
- package/src/ts/examples/Variance/test-streaming.ts +260 -0
- package/src/ts/examples/ZScoreNormalize/test-state.ts +277 -0
- package/src/ts/examples/ZScoreNormalize/test-streaming.ts +306 -0
- package/src/ts/examples/advanced-dsp-examples.ts +397 -0
- package/src/ts/examples/callbacks/advanced-router-features.ts +326 -0
- package/src/ts/examples/callbacks/benchmark-circular-buffer.ts +109 -0
- package/src/ts/examples/callbacks/monitoring-example.ts +265 -0
- package/src/ts/examples/callbacks/pipeline-callbacks-example.ts +137 -0
- package/src/ts/examples/callbacks/pooled-callbacks-example.ts +274 -0
- package/src/ts/examples/callbacks/priority-routing-example.ts +277 -0
- package/src/ts/examples/callbacks/production-topic-router.ts +214 -0
- package/src/ts/examples/callbacks/topic-based-logging.ts +161 -0
- package/src/ts/examples/chaining/test-chaining-redis.ts +113 -0
- package/src/ts/examples/chaining/test-chaining.ts +52 -0
- package/src/ts/examples/emg-features-example.ts +284 -0
- package/src/ts/examples/fft-example.ts +309 -0
- package/src/ts/examples/fft-examples.ts +349 -0
- package/src/ts/examples/filter-examples.ts +320 -0
- package/src/ts/examples/list-state-example.ts +131 -0
- package/src/ts/examples/logger-example.ts +91 -0
- package/src/ts/examples/notch-filter-examples.ts +243 -0
- package/src/ts/examples/phase5/drift-detection-example.ts +290 -0
- package/src/ts/examples/phase6-7/production-observability.ts +476 -0
- package/src/ts/examples/phase6-7/redis-timeseries-integration.ts +446 -0
- package/src/ts/examples/redis/redis-example.ts +202 -0
- package/src/ts/examples/redis-example.ts +202 -0
- package/src/ts/examples/simd-benchmark.ts +126 -0
- package/src/ts/examples/tap-debugging.ts +230 -0
- package/src/ts/examples/timeseries/comparison-example.ts +290 -0
- package/src/ts/examples/timeseries/iot-sensor-example.ts +143 -0
- package/src/ts/examples/timeseries/redis-streaming-example.ts +233 -0
- package/src/ts/examples/waveform-length-example.ts +139 -0
- package/src/ts/fft.ts +722 -0
- package/src/ts/filters.ts +1078 -0
- package/src/ts/index.ts +120 -0
- package/src/ts/types.ts +589 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced DSP Operations
|
|
3
|
+
*
|
|
4
|
+
* This module contains TypeScript implementations of advanced DSP operations:
|
|
5
|
+
* - Hjorth Parameters (Activity, Mobility, Complexity)
|
|
6
|
+
* - Spectral Features (Centroid, Rolloff, Flux)
|
|
7
|
+
* - Entropy Measures (Shannon, Sample Entropy, Approximate Entropy)
|
|
8
|
+
*
|
|
9
|
+
* Note: Resampling operations (decimate, interpolate, resample) are being
|
|
10
|
+
* implemented in C++ with polyphase FIR filtering for maximum efficiency.
|
|
11
|
+
* Expected release: within next few days.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
HjorthParameters,
|
|
16
|
+
HjorthParams,
|
|
17
|
+
SpectralFeatures,
|
|
18
|
+
SpectralFeaturesParams,
|
|
19
|
+
EntropyParams,
|
|
20
|
+
SampleEntropyParams,
|
|
21
|
+
ApproximateEntropyParams,
|
|
22
|
+
} from "./types.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Calculate Hjorth parameters for signal complexity analysis
|
|
26
|
+
*
|
|
27
|
+
* Hjorth parameters describe three characteristics of a signal:
|
|
28
|
+
* - Activity: Variance (spread) of the signal
|
|
29
|
+
* - Mobility: How much the signal changes (based on first derivative)
|
|
30
|
+
* - Complexity: How much the signal's change changes (based on second derivative)
|
|
31
|
+
*
|
|
32
|
+
* @param signal - Input signal samples
|
|
33
|
+
* @returns Hjorth parameters object
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* const signal = new Float32Array([1, 2, 3, 4, 5, 4, 3, 2, 1]);
|
|
37
|
+
* const hjorth = calculateHjorthParameters(signal);
|
|
38
|
+
* console.log(`Activity: ${hjorth.activity}`);
|
|
39
|
+
* console.log(`Mobility: ${hjorth.mobility}`);
|
|
40
|
+
* console.log(`Complexity: ${hjorth.complexity}`);
|
|
41
|
+
*/
|
|
42
|
+
export function calculateHjorthParameters(
|
|
43
|
+
signal: Float32Array
|
|
44
|
+
): HjorthParameters {
|
|
45
|
+
const n = signal.length;
|
|
46
|
+
if (n < 3) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
"Signal must have at least 3 samples for Hjorth parameters"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Calculate variance of signal (Activity)
|
|
53
|
+
let mean = 0;
|
|
54
|
+
for (let i = 0; i < n; i++) {
|
|
55
|
+
mean += signal[i];
|
|
56
|
+
}
|
|
57
|
+
mean /= n;
|
|
58
|
+
|
|
59
|
+
let variance = 0;
|
|
60
|
+
for (let i = 0; i < n; i++) {
|
|
61
|
+
const diff = signal[i] - mean;
|
|
62
|
+
variance += diff * diff;
|
|
63
|
+
}
|
|
64
|
+
variance /= n;
|
|
65
|
+
|
|
66
|
+
// Calculate first derivative
|
|
67
|
+
const firstDeriv = new Float32Array(n - 1);
|
|
68
|
+
for (let i = 0; i < n - 1; i++) {
|
|
69
|
+
firstDeriv[i] = signal[i + 1] - signal[i];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Calculate variance of first derivative
|
|
73
|
+
let derivMean = 0;
|
|
74
|
+
for (let i = 0; i < firstDeriv.length; i++) {
|
|
75
|
+
derivMean += firstDeriv[i];
|
|
76
|
+
}
|
|
77
|
+
derivMean /= firstDeriv.length;
|
|
78
|
+
|
|
79
|
+
let derivVariance = 0;
|
|
80
|
+
for (let i = 0; i < firstDeriv.length; i++) {
|
|
81
|
+
const diff = firstDeriv[i] - derivMean;
|
|
82
|
+
derivVariance += diff * diff;
|
|
83
|
+
}
|
|
84
|
+
derivVariance /= firstDeriv.length;
|
|
85
|
+
|
|
86
|
+
// Calculate Mobility
|
|
87
|
+
const mobility = Math.sqrt(derivVariance / variance);
|
|
88
|
+
|
|
89
|
+
// Calculate second derivative
|
|
90
|
+
const secondDeriv = new Float32Array(n - 2);
|
|
91
|
+
for (let i = 0; i < n - 2; i++) {
|
|
92
|
+
secondDeriv[i] = firstDeriv[i + 1] - firstDeriv[i];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Calculate variance of second derivative
|
|
96
|
+
let secondDerivMean = 0;
|
|
97
|
+
for (let i = 0; i < secondDeriv.length; i++) {
|
|
98
|
+
secondDerivMean += secondDeriv[i];
|
|
99
|
+
}
|
|
100
|
+
secondDerivMean /= secondDeriv.length;
|
|
101
|
+
|
|
102
|
+
let secondDerivVariance = 0;
|
|
103
|
+
for (let i = 0; i < secondDeriv.length; i++) {
|
|
104
|
+
const diff = secondDeriv[i] - secondDerivMean;
|
|
105
|
+
secondDerivVariance += diff * diff;
|
|
106
|
+
}
|
|
107
|
+
secondDerivVariance /= secondDeriv.length;
|
|
108
|
+
|
|
109
|
+
// Calculate Mobility of first derivative
|
|
110
|
+
const derivMobility = Math.sqrt(secondDerivVariance / derivVariance);
|
|
111
|
+
|
|
112
|
+
// Calculate Complexity
|
|
113
|
+
const complexity = derivMobility / mobility;
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
activity: variance,
|
|
117
|
+
mobility,
|
|
118
|
+
complexity,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Calculate spectral centroid (center of mass of spectrum)
|
|
124
|
+
*
|
|
125
|
+
* @param magnitudeSpectrum - FFT magnitude spectrum
|
|
126
|
+
* @param sampleRate - Sample rate in Hz
|
|
127
|
+
* @returns Spectral centroid in Hz
|
|
128
|
+
*/
|
|
129
|
+
export function calculateSpectralCentroid(
|
|
130
|
+
magnitudeSpectrum: Float32Array,
|
|
131
|
+
sampleRate: number
|
|
132
|
+
): number {
|
|
133
|
+
const n = magnitudeSpectrum.length;
|
|
134
|
+
const freqResolution = sampleRate / (2 * n);
|
|
135
|
+
|
|
136
|
+
let weightedSum = 0;
|
|
137
|
+
let magnitudeSum = 0;
|
|
138
|
+
|
|
139
|
+
for (let i = 0; i < n; i++) {
|
|
140
|
+
const frequency = i * freqResolution;
|
|
141
|
+
const magnitude = magnitudeSpectrum[i];
|
|
142
|
+
weightedSum += frequency * magnitude;
|
|
143
|
+
magnitudeSum += magnitude;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return magnitudeSum > 0 ? weightedSum / magnitudeSum : 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Calculate spectral rolloff (frequency below which X% of energy is contained)
|
|
151
|
+
*
|
|
152
|
+
* @param magnitudeSpectrum - FFT magnitude spectrum
|
|
153
|
+
* @param sampleRate - Sample rate in Hz
|
|
154
|
+
* @param percentage - Rolloff percentage (0-100, default: 85)
|
|
155
|
+
* @returns Rolloff frequency in Hz
|
|
156
|
+
*/
|
|
157
|
+
export function calculateSpectralRolloff(
|
|
158
|
+
magnitudeSpectrum: Float32Array,
|
|
159
|
+
sampleRate: number,
|
|
160
|
+
percentage: number = 85
|
|
161
|
+
): number {
|
|
162
|
+
const n = magnitudeSpectrum.length;
|
|
163
|
+
const freqResolution = sampleRate / (2 * n);
|
|
164
|
+
|
|
165
|
+
// Calculate total energy
|
|
166
|
+
let totalEnergy = 0;
|
|
167
|
+
for (let i = 0; i < n; i++) {
|
|
168
|
+
totalEnergy += magnitudeSpectrum[i];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const threshold = (percentage / 100) * totalEnergy;
|
|
172
|
+
let cumulativeEnergy = 0;
|
|
173
|
+
|
|
174
|
+
for (let i = 0; i < n; i++) {
|
|
175
|
+
cumulativeEnergy += magnitudeSpectrum[i];
|
|
176
|
+
if (cumulativeEnergy >= threshold) {
|
|
177
|
+
return i * freqResolution;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return (n - 1) * freqResolution;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Calculate spectral flux (change in spectrum from previous frame)
|
|
186
|
+
*
|
|
187
|
+
* @param currentSpectrum - Current frame's magnitude spectrum
|
|
188
|
+
* @param previousSpectrum - Previous frame's magnitude spectrum (or null for first frame)
|
|
189
|
+
* @returns Spectral flux value
|
|
190
|
+
*/
|
|
191
|
+
export function calculateSpectralFlux(
|
|
192
|
+
currentSpectrum: Float32Array,
|
|
193
|
+
previousSpectrum: Float32Array | null
|
|
194
|
+
): number {
|
|
195
|
+
if (!previousSpectrum || previousSpectrum.length !== currentSpectrum.length) {
|
|
196
|
+
return 0; // First frame or size mismatch
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let flux = 0;
|
|
200
|
+
for (let i = 0; i < currentSpectrum.length; i++) {
|
|
201
|
+
const diff = currentSpectrum[i] - previousSpectrum[i];
|
|
202
|
+
flux += diff * diff;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return Math.sqrt(flux);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Calculate all spectral features from magnitude spectrum
|
|
210
|
+
*
|
|
211
|
+
* @param magnitudeSpectrum - FFT magnitude spectrum
|
|
212
|
+
* @param sampleRate - Sample rate in Hz
|
|
213
|
+
* @param previousSpectrum - Previous frame's spectrum for flux calculation
|
|
214
|
+
* @param rolloffPercentage - Rolloff percentage (default: 85)
|
|
215
|
+
* @returns Object containing all spectral features
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* const fft = performFFT(signal, 2048);
|
|
219
|
+
* const features = calculateSpectralFeatures(fft.magnitude, 44100, null, 85);
|
|
220
|
+
* console.log(`Centroid: ${features.centroid} Hz`);
|
|
221
|
+
* console.log(`Rolloff: ${features.rolloff} Hz`);
|
|
222
|
+
* console.log(`Flux: ${features.flux}`);
|
|
223
|
+
*/
|
|
224
|
+
export function calculateSpectralFeatures(
|
|
225
|
+
magnitudeSpectrum: Float32Array,
|
|
226
|
+
sampleRate: number,
|
|
227
|
+
previousSpectrum: Float32Array | null = null,
|
|
228
|
+
rolloffPercentage: number = 85
|
|
229
|
+
): SpectralFeatures {
|
|
230
|
+
return {
|
|
231
|
+
centroid: calculateSpectralCentroid(magnitudeSpectrum, sampleRate),
|
|
232
|
+
rolloff: calculateSpectralRolloff(
|
|
233
|
+
magnitudeSpectrum,
|
|
234
|
+
sampleRate,
|
|
235
|
+
rolloffPercentage
|
|
236
|
+
),
|
|
237
|
+
flux: calculateSpectralFlux(magnitudeSpectrum, previousSpectrum),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Calculate Shannon entropy of a signal
|
|
243
|
+
* Measures the uncertainty/randomness in the signal's amplitude distribution
|
|
244
|
+
*
|
|
245
|
+
* @param signal - Input signal samples
|
|
246
|
+
* @param numBins - Number of histogram bins (default: 256)
|
|
247
|
+
* @returns Shannon entropy value
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* const signal = new Float32Array(1000);
|
|
251
|
+
* // ... fill signal with data
|
|
252
|
+
* const entropy = calculateShannonEntropy(signal, 256);
|
|
253
|
+
* console.log(`Entropy: ${entropy} bits`);
|
|
254
|
+
*/
|
|
255
|
+
export function calculateShannonEntropy(
|
|
256
|
+
signal: Float32Array,
|
|
257
|
+
numBins: number = 256
|
|
258
|
+
): number {
|
|
259
|
+
const n = signal.length;
|
|
260
|
+
if (n === 0) return 0;
|
|
261
|
+
|
|
262
|
+
// Find min and max for binning
|
|
263
|
+
let min = signal[0];
|
|
264
|
+
let max = signal[0];
|
|
265
|
+
for (let i = 1; i < n; i++) {
|
|
266
|
+
if (signal[i] < min) min = signal[i];
|
|
267
|
+
if (signal[i] > max) max = signal[i];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (min === max) return 0; // Constant signal has zero entropy
|
|
271
|
+
|
|
272
|
+
// Create histogram
|
|
273
|
+
const histogram = new Array(numBins).fill(0);
|
|
274
|
+
const range = max - min;
|
|
275
|
+
const binWidth = range / numBins;
|
|
276
|
+
|
|
277
|
+
for (let i = 0; i < n; i++) {
|
|
278
|
+
let binIndex = Math.floor((signal[i] - min) / binWidth);
|
|
279
|
+
if (binIndex >= numBins) binIndex = numBins - 1; // Handle edge case
|
|
280
|
+
histogram[binIndex]++;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Calculate entropy
|
|
284
|
+
let entropy = 0;
|
|
285
|
+
for (let i = 0; i < numBins; i++) {
|
|
286
|
+
if (histogram[i] > 0) {
|
|
287
|
+
const probability = histogram[i] / n;
|
|
288
|
+
entropy -= probability * Math.log2(probability);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return entropy;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Calculate Sample Entropy (SampEn)
|
|
297
|
+
* Measures the likelihood that similar patterns remain similar on next increment
|
|
298
|
+
*
|
|
299
|
+
* @param signal - Input signal samples
|
|
300
|
+
* @param m - Pattern length (default: 2)
|
|
301
|
+
* @param r - Tolerance for matching (default: 0.2 * std deviation)
|
|
302
|
+
* @returns Sample entropy value
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* const signal = new Float32Array(1000);
|
|
306
|
+
* // ... fill signal with data
|
|
307
|
+
* const sampEn = calculateSampleEntropy(signal, 2, 0.2);
|
|
308
|
+
* console.log(`Sample Entropy: ${sampEn}`);
|
|
309
|
+
*/
|
|
310
|
+
export function calculateSampleEntropy(
|
|
311
|
+
signal: Float32Array,
|
|
312
|
+
m: number = 2,
|
|
313
|
+
r?: number
|
|
314
|
+
): number {
|
|
315
|
+
const n = signal.length;
|
|
316
|
+
if (n < m + 1) {
|
|
317
|
+
throw new Error(`Signal too short for pattern length ${m}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Calculate tolerance if not provided
|
|
321
|
+
if (r === undefined) {
|
|
322
|
+
// Calculate standard deviation
|
|
323
|
+
let mean = 0;
|
|
324
|
+
for (let i = 0; i < n; i++) {
|
|
325
|
+
mean += signal[i];
|
|
326
|
+
}
|
|
327
|
+
mean /= n;
|
|
328
|
+
|
|
329
|
+
let variance = 0;
|
|
330
|
+
for (let i = 0; i < n; i++) {
|
|
331
|
+
const diff = signal[i] - mean;
|
|
332
|
+
variance += diff * diff;
|
|
333
|
+
}
|
|
334
|
+
const stdDev = Math.sqrt(variance / n);
|
|
335
|
+
r = 0.2 * stdDev;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Count matches for patterns of length m and m+1
|
|
339
|
+
let countM = 0;
|
|
340
|
+
let countMplus1 = 0;
|
|
341
|
+
|
|
342
|
+
for (let i = 0; i < n - m; i++) {
|
|
343
|
+
for (let j = i + 1; j < n - m; j++) {
|
|
344
|
+
// Check if patterns of length m match
|
|
345
|
+
let matchM = true;
|
|
346
|
+
for (let k = 0; k < m; k++) {
|
|
347
|
+
if (Math.abs(signal[i + k] - signal[j + k]) > r) {
|
|
348
|
+
matchM = false;
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (matchM) {
|
|
354
|
+
countM++;
|
|
355
|
+
|
|
356
|
+
// Check if patterns of length m+1 also match
|
|
357
|
+
if (
|
|
358
|
+
i < n - m - 1 &&
|
|
359
|
+
j < n - m - 1 &&
|
|
360
|
+
Math.abs(signal[i + m] - signal[j + m]) <= r
|
|
361
|
+
) {
|
|
362
|
+
countMplus1++;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Calculate SampEn
|
|
369
|
+
if (countM === 0 || countMplus1 === 0) {
|
|
370
|
+
return Infinity; // Patterns don't repeat
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return -Math.log(countMplus1 / countM);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Calculate Approximate Entropy (ApEn)
|
|
378
|
+
* Similar to SampEn but includes self-matches
|
|
379
|
+
*
|
|
380
|
+
* @param signal - Input signal samples
|
|
381
|
+
* @param m - Pattern length (default: 2)
|
|
382
|
+
* @param r - Tolerance for matching (default: 0.2 * std deviation)
|
|
383
|
+
* @returns Approximate entropy value
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* const signal = new Float32Array(1000);
|
|
387
|
+
* // ... fill signal with data
|
|
388
|
+
* const apEn = calculateApproximateEntropy(signal, 2, 0.2);
|
|
389
|
+
* console.log(`Approximate Entropy: ${apEn}`);
|
|
390
|
+
*/
|
|
391
|
+
export function calculateApproximateEntropy(
|
|
392
|
+
signal: Float32Array,
|
|
393
|
+
m: number = 2,
|
|
394
|
+
r?: number
|
|
395
|
+
): number {
|
|
396
|
+
const n = signal.length;
|
|
397
|
+
if (n < m + 1) {
|
|
398
|
+
throw new Error(`Signal too short for pattern length ${m}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Calculate tolerance if not provided
|
|
402
|
+
if (r === undefined) {
|
|
403
|
+
let mean = 0;
|
|
404
|
+
for (let i = 0; i < n; i++) {
|
|
405
|
+
mean += signal[i];
|
|
406
|
+
}
|
|
407
|
+
mean /= n;
|
|
408
|
+
|
|
409
|
+
let variance = 0;
|
|
410
|
+
for (let i = 0; i < n; i++) {
|
|
411
|
+
const diff = signal[i] - mean;
|
|
412
|
+
variance += diff * diff;
|
|
413
|
+
}
|
|
414
|
+
const stdDev = Math.sqrt(variance / n);
|
|
415
|
+
r = 0.2 * stdDev;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Helper function to calculate phi
|
|
419
|
+
const phi = (patternLength: number): number => {
|
|
420
|
+
const counts = new Array(n - patternLength + 1).fill(0);
|
|
421
|
+
|
|
422
|
+
for (let i = 0; i <= n - patternLength; i++) {
|
|
423
|
+
for (let j = 0; j <= n - patternLength; j++) {
|
|
424
|
+
let match = true;
|
|
425
|
+
for (let k = 0; k < patternLength; k++) {
|
|
426
|
+
if (Math.abs(signal[i + k] - signal[j + k]) > r) {
|
|
427
|
+
match = false;
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (match) {
|
|
432
|
+
counts[i]++;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
let sum = 0;
|
|
438
|
+
for (let i = 0; i <= n - patternLength; i++) {
|
|
439
|
+
if (counts[i] > 0) {
|
|
440
|
+
sum += Math.log(counts[i] / (n - patternLength + 1));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return sum / (n - patternLength + 1);
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
return phi(m) - phi(m + 1);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Stateful class for tracking Hjorth parameters over a sliding window
|
|
452
|
+
*/
|
|
453
|
+
export class HjorthTracker {
|
|
454
|
+
private buffer: Float32Array;
|
|
455
|
+
private writeIndex: number = 0;
|
|
456
|
+
private isFull: boolean = false;
|
|
457
|
+
|
|
458
|
+
constructor(private windowSize: number) {
|
|
459
|
+
this.buffer = new Float32Array(windowSize);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Add a new sample and get updated Hjorth parameters
|
|
464
|
+
*/
|
|
465
|
+
update(sample: number): HjorthParameters | null {
|
|
466
|
+
this.buffer[this.writeIndex] = sample;
|
|
467
|
+
this.writeIndex = (this.writeIndex + 1) % this.windowSize;
|
|
468
|
+
|
|
469
|
+
if (this.writeIndex === 0) {
|
|
470
|
+
this.isFull = true;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (!this.isFull) {
|
|
474
|
+
return null; // Not enough samples yet
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Create properly ordered view of circular buffer
|
|
478
|
+
const orderedBuffer = new Float32Array(this.windowSize);
|
|
479
|
+
for (let i = 0; i < this.windowSize; i++) {
|
|
480
|
+
orderedBuffer[i] = this.buffer[(this.writeIndex + i) % this.windowSize];
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return calculateHjorthParameters(orderedBuffer);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
reset(): void {
|
|
487
|
+
this.writeIndex = 0;
|
|
488
|
+
this.isFull = false;
|
|
489
|
+
this.buffer.fill(0);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Stateful class for tracking spectral features with previous frame memory
|
|
495
|
+
*/
|
|
496
|
+
export class SpectralFeaturesTracker {
|
|
497
|
+
private previousSpectrum: Float32Array | null = null;
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Calculate spectral features, maintaining state for flux calculation
|
|
501
|
+
*/
|
|
502
|
+
calculate(
|
|
503
|
+
magnitudeSpectrum: Float32Array,
|
|
504
|
+
sampleRate: number,
|
|
505
|
+
rolloffPercentage: number = 85
|
|
506
|
+
): SpectralFeatures {
|
|
507
|
+
const features = calculateSpectralFeatures(
|
|
508
|
+
magnitudeSpectrum,
|
|
509
|
+
sampleRate,
|
|
510
|
+
this.previousSpectrum,
|
|
511
|
+
rolloffPercentage
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
// Store current spectrum for next call
|
|
515
|
+
this.previousSpectrum = new Float32Array(magnitudeSpectrum);
|
|
516
|
+
|
|
517
|
+
return features;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
reset(): void {
|
|
521
|
+
this.previousSpectrum = null;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Stateful class for tracking entropy over a sliding window
|
|
527
|
+
*/
|
|
528
|
+
export class EntropyTracker {
|
|
529
|
+
private buffer: Float32Array;
|
|
530
|
+
private writeIndex: number = 0;
|
|
531
|
+
private isFull: boolean = false;
|
|
532
|
+
|
|
533
|
+
constructor(private windowSize: number, private numBins: number = 256) {
|
|
534
|
+
this.buffer = new Float32Array(windowSize);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Add a new sample and get updated Shannon entropy
|
|
539
|
+
*/
|
|
540
|
+
update(sample: number): number | null {
|
|
541
|
+
this.buffer[this.writeIndex] = sample;
|
|
542
|
+
this.writeIndex = (this.writeIndex + 1) % this.windowSize;
|
|
543
|
+
|
|
544
|
+
if (this.writeIndex === 0) {
|
|
545
|
+
this.isFull = true;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (!this.isFull) {
|
|
549
|
+
return null; // Not enough samples yet
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Create properly ordered view of circular buffer
|
|
553
|
+
const orderedBuffer = new Float32Array(this.windowSize);
|
|
554
|
+
for (let i = 0; i < this.windowSize; i++) {
|
|
555
|
+
orderedBuffer[i] = this.buffer[(this.writeIndex + i) % this.windowSize];
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return calculateShannonEntropy(orderedBuffer, this.numBins);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
reset(): void {
|
|
562
|
+
this.writeIndex = 0;
|
|
563
|
+
this.isFull = false;
|
|
564
|
+
this.buffer.fill(0);
|
|
565
|
+
}
|
|
566
|
+
}
|