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,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
|
+
});
|