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.
Files changed (172) hide show
  1. package/.github/workflows/ci.yml +185 -0
  2. package/.vscode/c_cpp_properties.json +17 -0
  3. package/.vscode/settings.json +68 -0
  4. package/.vscode/tasks.json +28 -0
  5. package/DISCLAIMER.md +32 -0
  6. package/LICENSE +21 -0
  7. package/README.md +1803 -0
  8. package/ROADMAP.md +192 -0
  9. package/TECHNICAL_DEBT.md +165 -0
  10. package/binding.gyp +65 -0
  11. package/docs/ADVANCED_LOGGER_FEATURES.md +598 -0
  12. package/docs/AUTHENTICATION_SECURITY.md +396 -0
  13. package/docs/BACKEND_IMPROVEMENTS.md +399 -0
  14. package/docs/CHEBYSHEV_BIQUAD_EQ_IMPLEMENTATION.md +405 -0
  15. package/docs/FFT_IMPLEMENTATION.md +490 -0
  16. package/docs/FFT_IMPROVEMENTS_SUMMARY.md +387 -0
  17. package/docs/FFT_USER_GUIDE.md +494 -0
  18. package/docs/FILTERS_IMPLEMENTATION.md +260 -0
  19. package/docs/FILTER_API_GUIDE.md +418 -0
  20. package/docs/FIR_SIMD_OPTIMIZATION.md +175 -0
  21. package/docs/LOGGER_API_REFERENCE.md +350 -0
  22. package/docs/NOTCH_FILTER_QUICK_REF.md +121 -0
  23. package/docs/PHASE2_TESTS_AND_NOTCH_FILTER.md +341 -0
  24. package/docs/PHASES_5_7_SUMMARY.md +403 -0
  25. package/docs/PIPELINE_FILTER_INTEGRATION.md +446 -0
  26. package/docs/SIMD_OPTIMIZATIONS.md +211 -0
  27. package/docs/TEST_MIGRATION_SUMMARY.md +173 -0
  28. package/docs/TIMESERIES_IMPLEMENTATION_SUMMARY.md +322 -0
  29. package/docs/TIMESERIES_QUICK_REF.md +85 -0
  30. package/docs/advanced.md +559 -0
  31. package/docs/time-series-guide.md +617 -0
  32. package/docs/time-series-migration.md +376 -0
  33. package/jest.config.js +37 -0
  34. package/package.json +42 -0
  35. package/prebuilds/linux-x64/dsp-ts-redis.node +0 -0
  36. package/prebuilds/win32-x64/dsp-ts-redis.node +0 -0
  37. package/scripts/test.js +24 -0
  38. package/src/build/dsp-ts-redis.node +0 -0
  39. package/src/native/DspPipeline.cc +675 -0
  40. package/src/native/DspPipeline.h +44 -0
  41. package/src/native/FftBindings.cc +817 -0
  42. package/src/native/FilterBindings.cc +1001 -0
  43. package/src/native/IDspStage.h +53 -0
  44. package/src/native/adapters/InterpolatorStage.h +201 -0
  45. package/src/native/adapters/MeanAbsoluteValueStage.h +289 -0
  46. package/src/native/adapters/MovingAverageStage.h +306 -0
  47. package/src/native/adapters/RectifyStage.h +88 -0
  48. package/src/native/adapters/ResamplerStage.h +238 -0
  49. package/src/native/adapters/RmsStage.h +299 -0
  50. package/src/native/adapters/SscStage.h +121 -0
  51. package/src/native/adapters/VarianceStage.h +307 -0
  52. package/src/native/adapters/WampStage.h +114 -0
  53. package/src/native/adapters/WaveformLengthStage.h +115 -0
  54. package/src/native/adapters/ZScoreNormalizeStage.h +326 -0
  55. package/src/native/core/FftEngine.cc +441 -0
  56. package/src/native/core/FftEngine.h +224 -0
  57. package/src/native/core/FirFilter.cc +324 -0
  58. package/src/native/core/FirFilter.h +149 -0
  59. package/src/native/core/IirFilter.cc +576 -0
  60. package/src/native/core/IirFilter.h +210 -0
  61. package/src/native/core/MovingAbsoluteValueFilter.cc +17 -0
  62. package/src/native/core/MovingAbsoluteValueFilter.h +135 -0
  63. package/src/native/core/MovingAverageFilter.cc +18 -0
  64. package/src/native/core/MovingAverageFilter.h +135 -0
  65. package/src/native/core/MovingFftFilter.cc +291 -0
  66. package/src/native/core/MovingFftFilter.h +203 -0
  67. package/src/native/core/MovingVarianceFilter.cc +194 -0
  68. package/src/native/core/MovingVarianceFilter.h +114 -0
  69. package/src/native/core/MovingZScoreFilter.cc +215 -0
  70. package/src/native/core/MovingZScoreFilter.h +113 -0
  71. package/src/native/core/Policies.h +352 -0
  72. package/src/native/core/RmsFilter.cc +18 -0
  73. package/src/native/core/RmsFilter.h +131 -0
  74. package/src/native/core/SscFilter.cc +16 -0
  75. package/src/native/core/SscFilter.h +137 -0
  76. package/src/native/core/WampFilter.cc +16 -0
  77. package/src/native/core/WampFilter.h +101 -0
  78. package/src/native/core/WaveformLengthFilter.cc +17 -0
  79. package/src/native/core/WaveformLengthFilter.h +98 -0
  80. package/src/native/utils/CircularBufferArray.cc +336 -0
  81. package/src/native/utils/CircularBufferArray.h +62 -0
  82. package/src/native/utils/CircularBufferVector.cc +145 -0
  83. package/src/native/utils/CircularBufferVector.h +45 -0
  84. package/src/native/utils/NapiUtils.cc +53 -0
  85. package/src/native/utils/NapiUtils.h +21 -0
  86. package/src/native/utils/SimdOps.h +870 -0
  87. package/src/native/utils/SlidingWindowFilter.cc +239 -0
  88. package/src/native/utils/SlidingWindowFilter.h +159 -0
  89. package/src/native/utils/TimeSeriesBuffer.cc +205 -0
  90. package/src/native/utils/TimeSeriesBuffer.h +140 -0
  91. package/src/ts/CircularLogBuffer.ts +87 -0
  92. package/src/ts/DriftDetector.ts +331 -0
  93. package/src/ts/TopicRouter.ts +428 -0
  94. package/src/ts/__tests__/AdvancedDsp.test.ts +585 -0
  95. package/src/ts/__tests__/AuthAndEdgeCases.test.ts +241 -0
  96. package/src/ts/__tests__/Chaining.test.ts +387 -0
  97. package/src/ts/__tests__/ChebyshevBiquad.test.ts +229 -0
  98. package/src/ts/__tests__/CircularLogBuffer.test.ts +158 -0
  99. package/src/ts/__tests__/DriftDetector.test.ts +389 -0
  100. package/src/ts/__tests__/Fft.test.ts +484 -0
  101. package/src/ts/__tests__/ListState.test.ts +153 -0
  102. package/src/ts/__tests__/Logger.test.ts +208 -0
  103. package/src/ts/__tests__/LoggerAdvanced.test.ts +319 -0
  104. package/src/ts/__tests__/LoggerMinor.test.ts +247 -0
  105. package/src/ts/__tests__/MeanAbsoluteValue.test.ts +398 -0
  106. package/src/ts/__tests__/MovingAverage.test.ts +322 -0
  107. package/src/ts/__tests__/RMS.test.ts +315 -0
  108. package/src/ts/__tests__/Rectify.test.ts +272 -0
  109. package/src/ts/__tests__/Redis.test.ts +456 -0
  110. package/src/ts/__tests__/SlopeSignChange.test.ts +166 -0
  111. package/src/ts/__tests__/Tap.test.ts +164 -0
  112. package/src/ts/__tests__/TimeBasedExpiration.test.ts +124 -0
  113. package/src/ts/__tests__/TimeBasedRmsAndMav.test.ts +231 -0
  114. package/src/ts/__tests__/TimeBasedVarianceAndZScore.test.ts +284 -0
  115. package/src/ts/__tests__/TimeSeries.test.ts +254 -0
  116. package/src/ts/__tests__/TopicRouter.test.ts +332 -0
  117. package/src/ts/__tests__/TopicRouterAdvanced.test.ts +483 -0
  118. package/src/ts/__tests__/TopicRouterPriority.test.ts +487 -0
  119. package/src/ts/__tests__/Variance.test.ts +509 -0
  120. package/src/ts/__tests__/WaveformLength.test.ts +147 -0
  121. package/src/ts/__tests__/WillisonAmplitude.test.ts +197 -0
  122. package/src/ts/__tests__/ZScoreNormalize.test.ts +459 -0
  123. package/src/ts/advanced-dsp.ts +566 -0
  124. package/src/ts/backends.ts +1137 -0
  125. package/src/ts/bindings.ts +1225 -0
  126. package/src/ts/easter-egg.ts +42 -0
  127. package/src/ts/examples/MeanAbsoluteValue/test-state.ts +99 -0
  128. package/src/ts/examples/MeanAbsoluteValue/test-streaming.ts +269 -0
  129. package/src/ts/examples/MovingAverage/test-state.ts +85 -0
  130. package/src/ts/examples/MovingAverage/test-streaming.ts +188 -0
  131. package/src/ts/examples/RMS/test-state.ts +97 -0
  132. package/src/ts/examples/RMS/test-streaming.ts +253 -0
  133. package/src/ts/examples/Rectify/test-state.ts +107 -0
  134. package/src/ts/examples/Rectify/test-streaming.ts +242 -0
  135. package/src/ts/examples/Variance/test-state.ts +195 -0
  136. package/src/ts/examples/Variance/test-streaming.ts +260 -0
  137. package/src/ts/examples/ZScoreNormalize/test-state.ts +277 -0
  138. package/src/ts/examples/ZScoreNormalize/test-streaming.ts +306 -0
  139. package/src/ts/examples/advanced-dsp-examples.ts +397 -0
  140. package/src/ts/examples/callbacks/advanced-router-features.ts +326 -0
  141. package/src/ts/examples/callbacks/benchmark-circular-buffer.ts +109 -0
  142. package/src/ts/examples/callbacks/monitoring-example.ts +265 -0
  143. package/src/ts/examples/callbacks/pipeline-callbacks-example.ts +137 -0
  144. package/src/ts/examples/callbacks/pooled-callbacks-example.ts +274 -0
  145. package/src/ts/examples/callbacks/priority-routing-example.ts +277 -0
  146. package/src/ts/examples/callbacks/production-topic-router.ts +214 -0
  147. package/src/ts/examples/callbacks/topic-based-logging.ts +161 -0
  148. package/src/ts/examples/chaining/test-chaining-redis.ts +113 -0
  149. package/src/ts/examples/chaining/test-chaining.ts +52 -0
  150. package/src/ts/examples/emg-features-example.ts +284 -0
  151. package/src/ts/examples/fft-example.ts +309 -0
  152. package/src/ts/examples/fft-examples.ts +349 -0
  153. package/src/ts/examples/filter-examples.ts +320 -0
  154. package/src/ts/examples/list-state-example.ts +131 -0
  155. package/src/ts/examples/logger-example.ts +91 -0
  156. package/src/ts/examples/notch-filter-examples.ts +243 -0
  157. package/src/ts/examples/phase5/drift-detection-example.ts +290 -0
  158. package/src/ts/examples/phase6-7/production-observability.ts +476 -0
  159. package/src/ts/examples/phase6-7/redis-timeseries-integration.ts +446 -0
  160. package/src/ts/examples/redis/redis-example.ts +202 -0
  161. package/src/ts/examples/redis-example.ts +202 -0
  162. package/src/ts/examples/simd-benchmark.ts +126 -0
  163. package/src/ts/examples/tap-debugging.ts +230 -0
  164. package/src/ts/examples/timeseries/comparison-example.ts +290 -0
  165. package/src/ts/examples/timeseries/iot-sensor-example.ts +143 -0
  166. package/src/ts/examples/timeseries/redis-streaming-example.ts +233 -0
  167. package/src/ts/examples/waveform-length-example.ts +139 -0
  168. package/src/ts/fft.ts +722 -0
  169. package/src/ts/filters.ts +1078 -0
  170. package/src/ts/index.ts +120 -0
  171. package/src/ts/types.ts +589 -0
  172. package/tsconfig.json +15 -0
@@ -0,0 +1,585 @@
1
+ /**
2
+ * Tests for Advanced DSP Functions
3
+ * - Hjorth Parameters (Activity, Mobility, Complexity)
4
+ * - Spectral Features (Centroid, Rolloff, Flux)
5
+ * - Entropy Measures (Shannon, SampEn, ApEn)
6
+ */
7
+
8
+ import { describe, it } from "node:test";
9
+ import assert from "node:assert";
10
+ import {
11
+ calculateHjorthParameters,
12
+ HjorthTracker,
13
+ calculateSpectralCentroid,
14
+ calculateSpectralRolloff,
15
+ calculateSpectralFlux,
16
+ calculateSpectralFeatures,
17
+ SpectralFeaturesTracker,
18
+ calculateShannonEntropy,
19
+ calculateSampleEntropy,
20
+ calculateApproximateEntropy,
21
+ EntropyTracker,
22
+ FftProcessor,
23
+ } from "../index.js";
24
+
25
+ describe("Hjorth Parameters - Basic Calculation", () => {
26
+ it("should calculate Hjorth parameters for simple sine wave", () => {
27
+ const size = 1000;
28
+ const signal = new Float32Array(size);
29
+
30
+ // Simple sine wave - predictable complexity
31
+ for (let i = 0; i < size; i++) {
32
+ signal[i] = Math.sin((2 * Math.PI * 5 * i) / size);
33
+ }
34
+
35
+ const hjorth = calculateHjorthParameters(signal);
36
+
37
+ assert.ok(hjorth.activity > 0, "Activity should be positive");
38
+ assert.ok(hjorth.mobility > 0, "Mobility should be positive");
39
+ assert.ok(hjorth.complexity > 0, "Complexity should be positive");
40
+
41
+ // Simple sine should have relatively low complexity
42
+ assert.ok(hjorth.complexity < 2, "Sine wave should have low complexity");
43
+ });
44
+
45
+ it("should calculate higher complexity for noisy signal", () => {
46
+ const size = 1000;
47
+ const clean = new Float32Array(size);
48
+ const noisy = new Float32Array(size);
49
+
50
+ // Create clean and noisy versions
51
+ for (let i = 0; i < size; i++) {
52
+ clean[i] = Math.sin((2 * Math.PI * 5 * i) / size);
53
+ noisy[i] = clean[i] + (Math.random() - 0.5) * 0.5; // Add noise
54
+ }
55
+
56
+ const hjorthClean = calculateHjorthParameters(clean);
57
+ const hjorthNoisy = calculateHjorthParameters(noisy);
58
+
59
+ // Noisy signal should have higher activity and complexity
60
+ assert.ok(
61
+ hjorthNoisy.activity > hjorthClean.activity,
62
+ "Noisy signal should have higher activity"
63
+ );
64
+ assert.ok(
65
+ hjorthNoisy.complexity > hjorthClean.complexity,
66
+ "Noisy signal should have higher complexity"
67
+ );
68
+ });
69
+
70
+ it("should handle constant signal", () => {
71
+ const size = 100;
72
+ const signal = new Float32Array(size);
73
+ signal.fill(1.0);
74
+
75
+ const hjorth = calculateHjorthParameters(signal);
76
+
77
+ // Constant signal has zero variance - mobility and complexity may be NaN or 0
78
+ assert.strictEqual(hjorth.activity, 0, "Constant signal has zero activity");
79
+ assert.ok(
80
+ hjorth.mobility === 0 || Number.isNaN(hjorth.mobility),
81
+ "Constant signal has zero or NaN mobility"
82
+ );
83
+ assert.ok(
84
+ hjorth.complexity === 0 || Number.isNaN(hjorth.complexity),
85
+ "Constant signal has zero or NaN complexity"
86
+ );
87
+ });
88
+
89
+ it("should throw error for too short signal", () => {
90
+ const signal = new Float32Array(2); // Need at least 3 samples
91
+ signal[0] = 1;
92
+ signal[1] = 2;
93
+
94
+ assert.throws(
95
+ () => calculateHjorthParameters(signal),
96
+ /at least 3 samples/,
97
+ "Should throw for insufficient samples"
98
+ );
99
+ });
100
+ });
101
+
102
+ describe("HjorthTracker - Real-time Tracking", () => {
103
+ it("should track Hjorth parameters with sliding window", () => {
104
+ const windowSize = 100;
105
+ const tracker = new HjorthTracker(windowSize);
106
+
107
+ // Fill the window
108
+ let result = null;
109
+ for (let i = 0; i < windowSize; i++) {
110
+ const sample = Math.sin((2 * Math.PI * 5 * i) / windowSize);
111
+ result = tracker.update(sample);
112
+ }
113
+
114
+ // Should return result after window is full
115
+ assert.ok(result !== null, "Should return result after window is full");
116
+ assert.ok(result!.activity > 0);
117
+ assert.ok(result!.mobility > 0);
118
+ assert.ok(result!.complexity > 0);
119
+ });
120
+
121
+ it("should return null until window is full", () => {
122
+ const windowSize = 50;
123
+ const tracker = new HjorthTracker(windowSize);
124
+
125
+ // Add samples but don't fill window
126
+ for (let i = 0; i < windowSize - 1; i++) {
127
+ const result = tracker.update(Math.random());
128
+ assert.strictEqual(
129
+ result,
130
+ null,
131
+ "Should return null until window is full"
132
+ );
133
+ }
134
+
135
+ // Fill window completely
136
+ const result = tracker.update(Math.random());
137
+ assert.ok(result !== null, "Should return result when window is full");
138
+ });
139
+
140
+ it("should reset state correctly", () => {
141
+ const windowSize = 50;
142
+ const tracker = new HjorthTracker(windowSize);
143
+
144
+ // Fill window
145
+ for (let i = 0; i < windowSize; i++) {
146
+ tracker.update(Math.random());
147
+ }
148
+
149
+ // Reset
150
+ tracker.reset();
151
+
152
+ // Should need to fill window again
153
+ for (let i = 0; i < windowSize - 1; i++) {
154
+ const result = tracker.update(Math.random());
155
+ assert.strictEqual(result, null, "Should return null after reset");
156
+ }
157
+ });
158
+ });
159
+
160
+ describe("Spectral Features - Centroid", () => {
161
+ it("should calculate spectral centroid correctly", () => {
162
+ const sampleRate = 1000;
163
+ const fftSize = 128;
164
+
165
+ // Create signal with peak at 100 Hz
166
+ const signal = new Float32Array(fftSize);
167
+ for (let i = 0; i < fftSize; i++) {
168
+ signal[i] = Math.sin((2 * Math.PI * 100 * i) / sampleRate);
169
+ }
170
+
171
+ const fft = new FftProcessor(fftSize);
172
+ const spectrum = fft.rfft(signal);
173
+ const magnitude = fft.getMagnitude(spectrum);
174
+
175
+ const centroid = calculateSpectralCentroid(magnitude, sampleRate);
176
+
177
+ // Centroid should be near 100 Hz for single-frequency signal
178
+ // Allow larger tolerance due to FFT bin resolution and spectral leakage
179
+ assert.ok(
180
+ Math.abs(centroid - 100) < 50,
181
+ `Centroid should be near 100 Hz, got ${centroid}`
182
+ );
183
+ });
184
+
185
+ it("should calculate higher centroid for high-frequency signal", () => {
186
+ const sampleRate = 1000;
187
+ const fftSize = 256;
188
+ const fft = new FftProcessor(fftSize);
189
+
190
+ // Low frequency signal
191
+ const lowFreq = new Float32Array(fftSize);
192
+ for (let i = 0; i < fftSize; i++) {
193
+ lowFreq[i] = Math.sin((2 * Math.PI * 50 * i) / sampleRate);
194
+ }
195
+ const spectrumLow = fft.rfft(lowFreq);
196
+ const magnitudeLow = fft.getMagnitude(spectrumLow);
197
+ const centroidLow = calculateSpectralCentroid(magnitudeLow, sampleRate);
198
+
199
+ // High frequency signal
200
+ const highFreq = new Float32Array(fftSize);
201
+ for (let i = 0; i < fftSize; i++) {
202
+ highFreq[i] = Math.sin((2 * Math.PI * 200 * i) / sampleRate);
203
+ }
204
+ const spectrumHigh = fft.rfft(highFreq);
205
+ const magnitudeHigh = fft.getMagnitude(spectrumHigh);
206
+ const centroidHigh = calculateSpectralCentroid(magnitudeHigh, sampleRate);
207
+
208
+ assert.ok(
209
+ centroidHigh > centroidLow,
210
+ `High-frequency signal should have higher centroid: ${centroidHigh} > ${centroidLow}`
211
+ );
212
+ });
213
+ });
214
+
215
+ describe("Spectral Features - Rolloff", () => {
216
+ it("should calculate spectral rolloff", () => {
217
+ const sampleRate = 1000;
218
+ const fftSize = 128;
219
+
220
+ const signal = new Float32Array(fftSize);
221
+ for (let i = 0; i < fftSize; i++) {
222
+ signal[i] = Math.sin((2 * Math.PI * 100 * i) / sampleRate);
223
+ }
224
+
225
+ const fft = new FftProcessor(fftSize);
226
+ const spectrum = fft.rfft(signal);
227
+ const magnitude = fft.getMagnitude(spectrum);
228
+
229
+ const rolloff85 = calculateSpectralRolloff(magnitude, sampleRate, 85);
230
+ const rolloff95 = calculateSpectralRolloff(magnitude, sampleRate, 95);
231
+
232
+ // 95% rolloff should be higher than 85% rolloff
233
+ assert.ok(rolloff95 >= rolloff85, "95% rolloff should be >= 85% rolloff");
234
+
235
+ // Both should be positive
236
+ assert.ok(rolloff85 > 0, "Rolloff should be positive");
237
+ assert.ok(rolloff95 > 0, "Rolloff should be positive");
238
+ });
239
+ });
240
+
241
+ describe("Spectral Features - Flux", () => {
242
+ it("should calculate zero flux for identical spectra", () => {
243
+ const magnitude = new Float32Array([1, 2, 3, 4, 5]);
244
+ const flux = calculateSpectralFlux(magnitude, magnitude);
245
+
246
+ assert.strictEqual(flux, 0, "Flux should be zero for identical spectra");
247
+ });
248
+
249
+ it("should calculate positive flux for different spectra", () => {
250
+ const mag1 = new Float32Array([1, 2, 3, 4, 5]);
251
+ const mag2 = new Float32Array([2, 3, 4, 5, 6]);
252
+
253
+ const flux = calculateSpectralFlux(mag2, mag1);
254
+
255
+ assert.ok(flux > 0, "Flux should be positive for different spectra");
256
+ });
257
+
258
+ it("should return 0 when no previous spectrum provided", () => {
259
+ const magnitude = new Float32Array([1, 2, 3, 4, 5]);
260
+ const flux = calculateSpectralFlux(magnitude, null);
261
+
262
+ assert.strictEqual(flux, 0, "Flux should be 0 without previous spectrum");
263
+ });
264
+ });
265
+
266
+ describe("Spectral Features - Unified Interface", () => {
267
+ it("should calculate all spectral features", () => {
268
+ const sampleRate = 1000;
269
+ const fftSize = 128;
270
+
271
+ const signal = new Float32Array(fftSize);
272
+ for (let i = 0; i < fftSize; i++) {
273
+ signal[i] = Math.sin((2 * Math.PI * 100 * i) / sampleRate);
274
+ }
275
+
276
+ const fft = new FftProcessor(fftSize);
277
+ const spectrum = fft.rfft(signal);
278
+ const magnitude = fft.getMagnitude(spectrum);
279
+
280
+ const features = calculateSpectralFeatures(magnitude, sampleRate);
281
+
282
+ assert.ok(features.centroid > 0, "Centroid should be positive");
283
+ assert.ok(features.rolloff > 0, "Rolloff should be positive");
284
+ assert.strictEqual(features.flux, 0, "Flux should be 0 without previous");
285
+ });
286
+
287
+ it("should calculate flux when previous spectrum provided", () => {
288
+ const sampleRate = 1000;
289
+ const magnitude = new Float32Array(65);
290
+ const previous = new Float32Array(65);
291
+
292
+ // Different spectra
293
+ for (let i = 0; i < 65; i++) {
294
+ magnitude[i] = Math.random();
295
+ previous[i] = Math.random();
296
+ }
297
+
298
+ const features = calculateSpectralFeatures(magnitude, sampleRate, previous);
299
+
300
+ assert.ok(
301
+ features.flux > 0,
302
+ "Flux should be positive with different spectra"
303
+ );
304
+ });
305
+ });
306
+
307
+ describe("SpectralFeaturesTracker - Frame Tracking", () => {
308
+ it("should track spectral features frame-by-frame", () => {
309
+ const sampleRate = 1000;
310
+ const fftSize = 128;
311
+ const tracker = new SpectralFeaturesTracker();
312
+ const fft = new FftProcessor(fftSize);
313
+
314
+ // First frame
315
+ const signal1 = new Float32Array(fftSize);
316
+ for (let i = 0; i < fftSize; i++) {
317
+ signal1[i] = Math.sin((2 * Math.PI * 100 * i) / sampleRate);
318
+ }
319
+ const spectrum1 = fft.rfft(signal1);
320
+ const magnitude1 = fft.getMagnitude(spectrum1);
321
+
322
+ const features1 = tracker.calculate(magnitude1, sampleRate);
323
+ assert.strictEqual(features1.flux, 0, "First frame should have zero flux");
324
+
325
+ // Second frame (different)
326
+ const signal2 = new Float32Array(fftSize);
327
+ for (let i = 0; i < fftSize; i++) {
328
+ signal2[i] = Math.sin((2 * Math.PI * 150 * i) / sampleRate);
329
+ }
330
+ const spectrum2 = fft.rfft(signal2);
331
+ const magnitude2 = fft.getMagnitude(spectrum2);
332
+
333
+ const features2 = tracker.calculate(magnitude2, sampleRate);
334
+ assert.ok(features2.flux > 0, "Second frame should have positive flux");
335
+ });
336
+
337
+ it("should reset previous spectrum", () => {
338
+ const tracker = new SpectralFeaturesTracker();
339
+ const magnitude = new Float32Array(65);
340
+ magnitude.fill(1.0);
341
+
342
+ // Calculate twice
343
+ tracker.calculate(magnitude, 1000);
344
+ tracker.reset();
345
+ const features = tracker.calculate(magnitude, 1000);
346
+
347
+ assert.strictEqual(features.flux, 0, "Flux should be 0 after reset");
348
+ });
349
+ });
350
+
351
+ describe("Shannon Entropy", () => {
352
+ it("should calculate zero entropy for constant signal", () => {
353
+ const signal = new Float32Array(100);
354
+ signal.fill(1.0);
355
+
356
+ const entropy = calculateShannonEntropy(signal);
357
+
358
+ assert.strictEqual(entropy, 0, "Constant signal should have zero entropy");
359
+ });
360
+
361
+ it("should calculate higher entropy for random signal", () => {
362
+ const deterministic = new Float32Array(100);
363
+ for (let i = 0; i < 100; i++) {
364
+ deterministic[i] = Math.sin((2 * Math.PI * i) / 100);
365
+ }
366
+
367
+ const random = new Float32Array(100);
368
+ for (let i = 0; i < 100; i++) {
369
+ random[i] = Math.random();
370
+ }
371
+
372
+ const entropyDet = calculateShannonEntropy(deterministic);
373
+ const entropyRand = calculateShannonEntropy(random);
374
+
375
+ assert.ok(
376
+ entropyRand > entropyDet,
377
+ "Random signal should have higher entropy than deterministic"
378
+ );
379
+ });
380
+
381
+ it("should handle different bin counts", () => {
382
+ const signal = new Float32Array(1000);
383
+ for (let i = 0; i < 1000; i++) {
384
+ signal[i] = Math.random();
385
+ }
386
+
387
+ const entropy128 = calculateShannonEntropy(signal, 128);
388
+ const entropy256 = calculateShannonEntropy(signal, 256);
389
+
390
+ // Both should be positive
391
+ assert.ok(entropy128 > 0, "Entropy with 128 bins should be positive");
392
+ assert.ok(entropy256 > 0, "Entropy with 256 bins should be positive");
393
+ });
394
+ });
395
+
396
+ describe("Sample Entropy (SampEn)", () => {
397
+ it("should calculate low SampEn for regular signal", () => {
398
+ const signal = new Float32Array(200);
399
+ for (let i = 0; i < 200; i++) {
400
+ signal[i] = Math.sin((2 * Math.PI * 10 * i) / 200);
401
+ }
402
+
403
+ const sampEn = calculateSampleEntropy(signal, 2);
404
+
405
+ assert.ok(sampEn >= 0, "SampEn should be non-negative");
406
+ assert.ok(sampEn < 2, "Regular signal should have low SampEn");
407
+ });
408
+
409
+ it("should calculate higher SampEn for irregular signal", () => {
410
+ const regular = new Float32Array(200);
411
+ for (let i = 0; i < 200; i++) {
412
+ regular[i] = Math.sin((2 * Math.PI * 10 * i) / 200);
413
+ }
414
+
415
+ const irregular = new Float32Array(200);
416
+ for (let i = 0; i < 200; i++) {
417
+ irregular[i] = Math.random();
418
+ }
419
+
420
+ const sampEnRegular = calculateSampleEntropy(regular, 2);
421
+ const sampEnIrregular = calculateSampleEntropy(irregular, 2);
422
+
423
+ assert.ok(
424
+ sampEnIrregular > sampEnRegular,
425
+ "Irregular signal should have higher SampEn"
426
+ );
427
+ });
428
+
429
+ it("should use automatic tolerance when not provided", () => {
430
+ const signal = new Float32Array(100);
431
+ for (let i = 0; i < 100; i++) {
432
+ signal[i] = Math.sin((2 * Math.PI * 5 * i) / 100);
433
+ }
434
+
435
+ const sampEn = calculateSampleEntropy(signal, 2); // No r parameter
436
+
437
+ assert.ok(sampEn >= 0, "SampEn should work with automatic tolerance");
438
+ });
439
+ });
440
+
441
+ describe("Approximate Entropy (ApEn)", () => {
442
+ it("should calculate low ApEn for regular signal", () => {
443
+ const signal = new Float32Array(200);
444
+ for (let i = 0; i < 200; i++) {
445
+ signal[i] = Math.sin((2 * Math.PI * 10 * i) / 200);
446
+ }
447
+
448
+ const apEn = calculateApproximateEntropy(signal, 2);
449
+
450
+ assert.ok(apEn >= 0, "ApEn should be non-negative");
451
+ assert.ok(apEn < 2, "Regular signal should have low ApEn");
452
+ });
453
+
454
+ it("should calculate higher ApEn for random signal", () => {
455
+ const regular = new Float32Array(200);
456
+ for (let i = 0; i < 200; i++) {
457
+ regular[i] = Math.sin((2 * Math.PI * 10 * i) / 200);
458
+ }
459
+
460
+ const random = new Float32Array(200);
461
+ for (let i = 0; i < 200; i++) {
462
+ random[i] = Math.random();
463
+ }
464
+
465
+ const apEnRegular = calculateApproximateEntropy(regular, 2);
466
+ const apEnRandom = calculateApproximateEntropy(random, 2);
467
+
468
+ assert.ok(
469
+ apEnRandom > apEnRegular,
470
+ "Random signal should have higher ApEn"
471
+ );
472
+ });
473
+
474
+ it("should handle custom tolerance", () => {
475
+ const signal = new Float32Array(150);
476
+ for (let i = 0; i < 150; i++) {
477
+ signal[i] = Math.sin((2 * Math.PI * 5 * i) / 150);
478
+ }
479
+
480
+ const apEnDefault = calculateApproximateEntropy(signal, 2);
481
+ const apEnCustom = calculateApproximateEntropy(signal, 2, 0.1);
482
+
483
+ // Both should be valid
484
+ assert.ok(apEnDefault >= 0, "ApEn with default r should be valid");
485
+ assert.ok(apEnCustom >= 0, "ApEn with custom r should be valid");
486
+ });
487
+ });
488
+
489
+ describe("EntropyTracker - Real-time Entropy", () => {
490
+ it("should track Shannon entropy with sliding window", () => {
491
+ const windowSize = 100;
492
+ const tracker = new EntropyTracker(windowSize, 128);
493
+
494
+ // Fill window
495
+ let result = null;
496
+ for (let i = 0; i < windowSize; i++) {
497
+ result = tracker.update(Math.random());
498
+ }
499
+
500
+ assert.ok(result !== null, "Should return result after window is full");
501
+ assert.ok(result! > 0, "Entropy should be positive for random data");
502
+ });
503
+
504
+ it("should return null until window is full", () => {
505
+ const windowSize = 50;
506
+ const tracker = new EntropyTracker(windowSize);
507
+
508
+ for (let i = 0; i < windowSize - 1; i++) {
509
+ const result = tracker.update(Math.random());
510
+ assert.strictEqual(
511
+ result,
512
+ null,
513
+ "Should return null until window is full"
514
+ );
515
+ }
516
+
517
+ const result = tracker.update(Math.random());
518
+ assert.ok(result !== null, "Should return result when window is full");
519
+ });
520
+
521
+ it("should reset correctly", () => {
522
+ const windowSize = 50;
523
+ const tracker = new EntropyTracker(windowSize);
524
+
525
+ // Fill window
526
+ for (let i = 0; i < windowSize; i++) {
527
+ tracker.update(Math.random());
528
+ }
529
+
530
+ tracker.reset();
531
+
532
+ // Should need to fill again
533
+ for (let i = 0; i < windowSize - 1; i++) {
534
+ const result = tracker.update(Math.random());
535
+ assert.strictEqual(result, null, "Should return null after reset");
536
+ }
537
+ });
538
+ });
539
+
540
+ describe("Advanced DSP - Edge Cases", () => {
541
+ it("should handle NaN values in Hjorth (may produce NaN result)", () => {
542
+ const signal = new Float32Array([1, 2, NaN, 4, 5]);
543
+
544
+ const hjorth = calculateHjorthParameters(signal);
545
+
546
+ // NaN in input produces NaN in output
547
+ assert.ok(
548
+ Number.isNaN(hjorth.activity) || hjorth.activity >= 0,
549
+ "Should handle NaN in input (may produce NaN)"
550
+ );
551
+ });
552
+
553
+ it("should handle empty spectrum in spectral features", () => {
554
+ const magnitude = new Float32Array(0);
555
+
556
+ const centroid = calculateSpectralCentroid(magnitude, 1000);
557
+
558
+ // Empty spectrum returns 0
559
+ assert.strictEqual(centroid, 0, "Empty spectrum should return 0");
560
+ });
561
+
562
+ it("should handle mismatched spectrum lengths in flux", () => {
563
+ const mag1 = new Float32Array(100);
564
+ const mag2 = new Float32Array(50);
565
+
566
+ // Fill with some values
567
+ mag1.fill(1.0);
568
+ mag2.fill(1.0);
569
+
570
+ const flux = calculateSpectralFlux(mag1, mag2);
571
+
572
+ // Mismatched lengths - flux uses minimum length
573
+ assert.ok(flux >= 0, "Should handle mismatched lengths gracefully");
574
+ });
575
+
576
+ it("should handle very small signal in entropy", () => {
577
+ const signal = new Float32Array(10);
578
+ for (let i = 0; i < 10; i++) {
579
+ signal[i] = 0.001 * Math.random();
580
+ }
581
+
582
+ const entropy = calculateShannonEntropy(signal);
583
+ assert.ok(entropy >= 0, "Should handle very small signal");
584
+ });
585
+ });