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,389 @@
1
+ /**
2
+ * Tests for DriftDetector and utilities (Phase 5)
3
+ */
4
+
5
+ import { describe, it } from "node:test";
6
+ import assert from "node:assert";
7
+ import {
8
+ DriftDetector,
9
+ detectGaps,
10
+ validateMonotonicity,
11
+ estimateSampleRate,
12
+ } from "../DriftDetector.js";
13
+
14
+ describe("DriftDetector", () => {
15
+ describe("processSample()", () => {
16
+ it("should not detect drift for consistent timing", () => {
17
+ let driftDetected = false;
18
+
19
+ const detector = new DriftDetector({
20
+ expectedSampleRate: 100,
21
+ driftThreshold: 5.0,
22
+ onDriftDetected: () => {
23
+ driftDetected = true;
24
+ },
25
+ });
26
+
27
+ // Simulate perfect 10ms intervals (100 Hz)
28
+ let timestamp = 1000;
29
+ for (let i = 0; i < 100; i++) {
30
+ detector.processSample(timestamp);
31
+ timestamp += 10;
32
+ }
33
+
34
+ assert.strictEqual(driftDetected, false);
35
+ });
36
+
37
+ it("should detect positive drift (samples arriving too fast)", () => {
38
+ let driftCount = 0;
39
+
40
+ const detector = new DriftDetector({
41
+ expectedSampleRate: 100, // 10ms expected
42
+ driftThreshold: 5.0,
43
+ onDriftDetected: () => {
44
+ driftCount++;
45
+ },
46
+ });
47
+
48
+ // First sample establishes baseline
49
+ detector.processSample(1000);
50
+
51
+ // Samples arriving every 8ms (125 Hz instead of 100 Hz = 25% too fast)
52
+ for (let i = 1; i < 10; i++) {
53
+ detector.processSample(1000 + i * 8);
54
+ }
55
+
56
+ assert.ok(driftCount > 0);
57
+ });
58
+
59
+ it("should detect negative drift (samples arriving too slow)", () => {
60
+ let driftCount = 0;
61
+
62
+ const detector = new DriftDetector({
63
+ expectedSampleRate: 100, // 10ms expected
64
+ driftThreshold: 5.0,
65
+ onDriftDetected: () => {
66
+ driftCount++;
67
+ },
68
+ });
69
+
70
+ detector.processSample(1000);
71
+
72
+ // Samples arriving every 15ms (66.7 Hz instead of 100 Hz = 33% too slow)
73
+ for (let i = 1; i < 10; i++) {
74
+ detector.processSample(1000 + i * 15);
75
+ }
76
+
77
+ assert.ok(driftCount > 0);
78
+ });
79
+
80
+ it("should respect drift threshold", () => {
81
+ let driftCount = 0;
82
+
83
+ const detector = new DriftDetector({
84
+ expectedSampleRate: 100,
85
+ driftThreshold: 10.0, // 10% threshold
86
+ onDriftDetected: () => {
87
+ driftCount++;
88
+ },
89
+ });
90
+
91
+ detector.processSample(1000);
92
+
93
+ // 9% drift should not trigger (10.9ms interval = 91.7 Hz = 8.3% drift)
94
+ for (let i = 1; i < 10; i++) {
95
+ detector.processSample(1000 + i * 10.9);
96
+ }
97
+
98
+ assert.strictEqual(driftCount, 0);
99
+
100
+ // 15% drift should trigger (11.5ms interval = 87 Hz = 13% drift)
101
+ for (let i = 10; i < 20; i++) {
102
+ detector.processSample(1000 + 10 * 10.9 + (i - 10) * 11.5);
103
+ }
104
+
105
+ assert.ok(driftCount > 0);
106
+ });
107
+ });
108
+
109
+ describe("processBatch()", () => {
110
+ it("should process batch of timestamps", () => {
111
+ const detector = new DriftDetector({
112
+ expectedSampleRate: 100,
113
+ driftThreshold: 5.0,
114
+ });
115
+
116
+ const timestamps = new Float32Array(10);
117
+ for (let i = 0; i < 10; i++) {
118
+ timestamps[i] = 1000 + i * 10; // Perfect 10ms intervals
119
+ }
120
+
121
+ detector.processBatch(timestamps);
122
+
123
+ const stats = detector.getMetrics();
124
+ assert.strictEqual(stats.samplesProcessed, 10);
125
+ assert.strictEqual(stats.driftEventsCount, 0);
126
+ });
127
+
128
+ it("should detect drift in batch", () => {
129
+ const detector = new DriftDetector({
130
+ expectedSampleRate: 100,
131
+ driftThreshold: 5.0,
132
+ });
133
+
134
+ const timestamps = new Float32Array(10);
135
+ // First 5 perfect, last 5 with drift
136
+ for (let i = 0; i < 5; i++) {
137
+ timestamps[i] = 1000 + i * 10;
138
+ }
139
+ for (let i = 5; i < 10; i++) {
140
+ timestamps[i] = 1000 + 5 * 10 + (i - 5) * 15; // 50% slower
141
+ }
142
+
143
+ detector.processBatch(timestamps);
144
+
145
+ const stats = detector.getMetrics();
146
+ assert.ok(stats.driftEventsCount > 0);
147
+ });
148
+ });
149
+
150
+ describe("getMetrics()", () => {
151
+ it("should return accurate statistics", () => {
152
+ const detector = new DriftDetector({
153
+ expectedSampleRate: 100,
154
+ driftThreshold: 5.0,
155
+ });
156
+
157
+ const timestamps = new Float32Array([1000, 1010, 1020, 1030, 1040]);
158
+ detector.processBatch(timestamps);
159
+
160
+ const stats = detector.getMetrics();
161
+
162
+ assert.strictEqual(stats.samplesProcessed, 5);
163
+ assert.ok(Math.abs(stats.minDelta - 10) < 0.1);
164
+ assert.ok(Math.abs(stats.maxDelta - 10) < 0.1);
165
+ assert.ok(Math.abs(stats.averageDelta - 10) < 0.1);
166
+ assert.strictEqual(stats.driftEventsCount, 0);
167
+ });
168
+
169
+ it("should track min/max deltas", () => {
170
+ const detector = new DriftDetector({
171
+ expectedSampleRate: 100,
172
+ driftThreshold: 20.0, // High threshold so drift doesn't trigger
173
+ });
174
+
175
+ const timestamps = new Float32Array([1000, 1005, 1020, 1025, 1040]);
176
+ detector.processBatch(timestamps);
177
+
178
+ const stats = detector.getMetrics();
179
+
180
+ assert.ok(Math.abs(stats.minDelta - 5) < 0.1);
181
+ assert.ok(Math.abs(stats.maxDelta - 15) < 0.1);
182
+ assert.ok(Math.abs(stats.averageDelta - 10) < 0.1);
183
+ });
184
+ });
185
+
186
+ describe("reset()", () => {
187
+ it("should reset all statistics", () => {
188
+ const detector = new DriftDetector({
189
+ expectedSampleRate: 100,
190
+ driftThreshold: 5.0,
191
+ });
192
+
193
+ const timestamps = new Float32Array([1000, 1010, 1020]);
194
+ detector.processBatch(timestamps);
195
+
196
+ assert.strictEqual(detector.getMetrics().samplesProcessed, 3);
197
+
198
+ detector.reset();
199
+
200
+ const stats = detector.getMetrics();
201
+ assert.strictEqual(stats.samplesProcessed, 0);
202
+ assert.strictEqual(stats.minDelta, 0);
203
+ assert.strictEqual(stats.maxDelta, 0);
204
+ assert.strictEqual(stats.averageDelta, 0);
205
+ assert.strictEqual(stats.driftEventsCount, 0);
206
+ });
207
+ });
208
+ });
209
+
210
+ describe("detectGaps()", () => {
211
+ it("should detect no gaps in continuous data", () => {
212
+ const timestamps = new Float32Array(10);
213
+ for (let i = 0; i < 10; i++) {
214
+ timestamps[i] = 1000 + i * 10; // Perfect 10ms intervals
215
+ }
216
+
217
+ const gaps = detectGaps(timestamps, 100);
218
+
219
+ assert.strictEqual(gaps.length, 0);
220
+ });
221
+
222
+ it("should detect single gap", () => {
223
+ const timestamps = new Float32Array([1000, 1010, 1020, 1050, 1060]);
224
+ // ^^^^ 30ms gap (3 missing samples)
225
+
226
+ const gaps = detectGaps(timestamps, 100);
227
+
228
+ assert.strictEqual(gaps.length, 1);
229
+ assert.strictEqual(gaps[0].startIndex, 2);
230
+ assert.strictEqual(gaps[0].endIndex, 3);
231
+ assert.ok(Math.abs(gaps[0].durationMs - 30) < 0.1);
232
+ assert.strictEqual(gaps[0].expectedSamples, 2); // Should have had 2 more samples
233
+ });
234
+
235
+ it("should detect multiple gaps", () => {
236
+ const timestamps = new Float32Array([
237
+ 1000, 1010, 1040, 1050, 1080, 1090,
238
+ // ^^^^ gap1 ^^^^ gap2
239
+ ]);
240
+
241
+ const gaps = detectGaps(timestamps, 100);
242
+
243
+ assert.strictEqual(gaps.length, 2);
244
+ assert.ok(Math.abs(gaps[0].durationMs - 30) < 0.1);
245
+ assert.ok(Math.abs(gaps[1].durationMs - 30) < 0.1);
246
+ });
247
+
248
+ it("should respect gap threshold", () => {
249
+ const timestamps = new Float32Array([1000, 1010, 1020, 1025, 1040]);
250
+ // ^^^^ 15ms gap from 1025
251
+
252
+ // With default threshold (2.0x), 15ms gap should not be detected (20ms minimum)
253
+ const gaps1 = detectGaps(timestamps, 100);
254
+ assert.strictEqual(gaps1.length, 0);
255
+
256
+ // With lower threshold (1.0x), 15ms gap should be detected (10ms minimum)
257
+ const gaps2 = detectGaps(timestamps, 100, 1.0);
258
+ assert.ok(gaps2.length > 0);
259
+ });
260
+ });
261
+
262
+ describe("validateMonotonicity()", () => {
263
+ it("should validate monotonically increasing timestamps", () => {
264
+ const timestamps = new Float32Array([1000, 1010, 1020, 1030, 1040]);
265
+
266
+ const violations = validateMonotonicity(timestamps);
267
+
268
+ assert.strictEqual(violations.length, 0);
269
+ });
270
+
271
+ it("should detect backwards timestamps", () => {
272
+ const timestamps = new Float32Array([1000, 1010, 1005, 1020, 1030]);
273
+ // ^^^^ backwards
274
+
275
+ const violations = validateMonotonicity(timestamps);
276
+
277
+ assert.strictEqual(violations.length, 1);
278
+ assert.strictEqual(violations[0].index, 2);
279
+ assert.strictEqual(violations[0].violation, "backwards");
280
+ assert.strictEqual(violations[0].currentTimestamp, 1005);
281
+ assert.strictEqual(violations[0].previousTimestamp, 1010);
282
+ });
283
+
284
+ it("should detect duplicate timestamps", () => {
285
+ const timestamps = new Float32Array([1000, 1010, 1010, 1020, 1030]);
286
+ // ^^^^ duplicate
287
+
288
+ const violations = validateMonotonicity(timestamps);
289
+
290
+ assert.strictEqual(violations.length, 1);
291
+ assert.strictEqual(violations[0].index, 2);
292
+ assert.strictEqual(violations[0].violation, "duplicate");
293
+ assert.strictEqual(violations[0].currentTimestamp, 1010);
294
+ assert.strictEqual(violations[0].previousTimestamp, 1010);
295
+ });
296
+
297
+ it("should detect multiple violations", () => {
298
+ const timestamps = new Float32Array([1000, 1010, 1005, 1020, 1020, 1015]);
299
+ // ^^^^ back ^^^^ dup ^^^^ back
300
+
301
+ const violations = validateMonotonicity(timestamps);
302
+
303
+ assert.strictEqual(violations.length, 3);
304
+ assert.strictEqual(violations[0].violation, "backwards");
305
+ assert.strictEqual(violations[1].violation, "duplicate");
306
+ assert.strictEqual(violations[2].violation, "backwards");
307
+ });
308
+ });
309
+
310
+ describe("estimateSampleRate()", () => {
311
+ it("should estimate correct sample rate", () => {
312
+ const timestamps = new Float32Array(100);
313
+ for (let i = 0; i < 100; i++) {
314
+ timestamps[i] = 1000 + i * 10; // 10ms intervals = 100 Hz
315
+ }
316
+
317
+ const estimate = estimateSampleRate(timestamps);
318
+
319
+ assert.ok(Math.abs(estimate.estimatedRate - 100) < 1);
320
+ assert.ok(Math.abs(estimate.averageInterval - 10) < 0.1);
321
+ assert.strictEqual(estimate.regularity, "excellent");
322
+ });
323
+
324
+ it("should estimate different sample rates", () => {
325
+ const testCases = [
326
+ { intervalMs: 20, expectedRate: 50 }, // 50 Hz
327
+ { intervalMs: 5, expectedRate: 200 }, // 200 Hz
328
+ { intervalMs: 100, expectedRate: 10 }, // 10 Hz
329
+ ];
330
+
331
+ for (const testCase of testCases) {
332
+ const timestamps = new Float32Array(100);
333
+ for (let i = 0; i < 100; i++) {
334
+ timestamps[i] = 1000 + i * testCase.intervalMs;
335
+ }
336
+
337
+ const estimate = estimateSampleRate(timestamps);
338
+ assert.ok(Math.abs(estimate.estimatedRate - testCase.expectedRate) < 1);
339
+ }
340
+ });
341
+
342
+ it("should assess regularity", () => {
343
+ // Perfect regularity
344
+ const perfect = new Float32Array(100);
345
+ for (let i = 0; i < 100; i++) {
346
+ perfect[i] = 1000 + i * 10;
347
+ }
348
+ assert.strictEqual(estimateSampleRate(perfect).regularity, "excellent");
349
+
350
+ // Slight jitter
351
+ const jittery = new Float32Array(100);
352
+ for (let i = 0; i < 100; i++) {
353
+ jittery[i] = 1000 + i * 10 + (Math.random() - 0.5) * 0.5; // ±0.25ms jitter
354
+ }
355
+ const jitterEstimate = estimateSampleRate(jittery);
356
+ assert.ok(["excellent", "good"].includes(jitterEstimate.regularity));
357
+
358
+ // Highly irregular
359
+ const irregular = new Float32Array(100);
360
+ for (let i = 0; i < 100; i++) {
361
+ irregular[i] = 1000 + i * 10 + (Math.random() - 0.5) * 5; // ±2.5ms jitter
362
+ }
363
+ const irregularEstimate = estimateSampleRate(irregular);
364
+ assert.ok(
365
+ ["fair", "poor", "irregular"].includes(irregularEstimate.regularity)
366
+ );
367
+ });
368
+
369
+ it("should handle edge case: too few samples", () => {
370
+ const timestamps = new Float32Array([1000]);
371
+
372
+ const estimate = estimateSampleRate(timestamps);
373
+
374
+ assert.strictEqual(estimate.estimatedRate, 0);
375
+ assert.strictEqual(estimate.regularity, "irregular");
376
+ });
377
+
378
+ it("should calculate coefficient of variation", () => {
379
+ const timestamps = new Float32Array(100);
380
+ for (let i = 0; i < 100; i++) {
381
+ timestamps[i] = 1000 + i * 10;
382
+ }
383
+
384
+ const estimate = estimateSampleRate(timestamps);
385
+
386
+ // Perfect timing should have very low CV
387
+ assert.ok(estimate.coefficientOfVariation < 0.01);
388
+ });
389
+ });