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,284 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { createDspPipeline } from "../index.js";
|
|
4
|
+
|
|
5
|
+
test("Time-Based Variance", async (t) => {
|
|
6
|
+
await t.test("should expire samples based on age for Variance", async () => {
|
|
7
|
+
const pipeline = createDspPipeline();
|
|
8
|
+
// 1 second window
|
|
9
|
+
pipeline.Variance({ mode: "moving", windowDuration: 1000 });
|
|
10
|
+
|
|
11
|
+
// First 3 samples at 0, 100, 200ms - values 1, 2, 3
|
|
12
|
+
const chunk1 = new Float32Array([1, 2, 3]);
|
|
13
|
+
const ts1 = new Float32Array([0, 100, 200]);
|
|
14
|
+
const result1 = await pipeline.process(chunk1, ts1, { channels: 1 });
|
|
15
|
+
|
|
16
|
+
// At 200ms: Variance([1, 2, 3])
|
|
17
|
+
// Mean = 2, Variance = ((1-2)^2 + (2-2)^2 + (3-2)^2) / 3 = (1 + 0 + 1) / 3 = 0.667
|
|
18
|
+
const expectedVar1 = 2 / 3; // 0.667
|
|
19
|
+
assert.ok(
|
|
20
|
+
Math.abs(result1[2] - expectedVar1) < 0.01,
|
|
21
|
+
`Expected Variance ≈ 0.667, got ${result1[2]}`
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Sample at 2200ms - 2.2 seconds later (>1s window)
|
|
25
|
+
// Samples at 0, 100, 200ms should be EXPIRED
|
|
26
|
+
const chunk2 = new Float32Array([10]);
|
|
27
|
+
const ts2 = new Float32Array([2200]);
|
|
28
|
+
const result2 = await pipeline.process(chunk2, ts2, { channels: 1 });
|
|
29
|
+
|
|
30
|
+
// At 2200ms: only sample at 2200ms remains, Variance([10]) = 0
|
|
31
|
+
assert.ok(
|
|
32
|
+
Math.abs(result2[0] - 0.0) < 0.01,
|
|
33
|
+
`Expected Variance = 0.0, got ${result2[0]}`
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await t.test("should work with irregular sampling for Variance", async () => {
|
|
38
|
+
const pipeline = createDspPipeline();
|
|
39
|
+
// 500ms window
|
|
40
|
+
pipeline.Variance({ mode: "moving", windowDuration: 500 });
|
|
41
|
+
|
|
42
|
+
const samples = new Float32Array([2, 4, 10, 12]);
|
|
43
|
+
const timestamps = new Float32Array([
|
|
44
|
+
0, // Sample at 0ms
|
|
45
|
+
50, // Sample at 50ms
|
|
46
|
+
600, // Sample at 600ms (550ms gap!)
|
|
47
|
+
650, // Sample at 650ms
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const result = await pipeline.process(samples, timestamps, { channels: 1 });
|
|
51
|
+
|
|
52
|
+
// At 650ms with 500ms window:
|
|
53
|
+
// - Sample at 0ms EXPIRED (650 - 0 = 650ms > 500ms)
|
|
54
|
+
// - Sample at 50ms EXPIRED (650 - 50 = 600ms > 500ms)
|
|
55
|
+
// - Sample at 600ms KEPT (650 - 600 = 50ms < 500ms)
|
|
56
|
+
// - Sample at 650ms KEPT (current)
|
|
57
|
+
// Variance([10, 12]) = ((10-11)^2 + (12-11)^2) / 2 = (1 + 1) / 2 = 1
|
|
58
|
+
assert.ok(
|
|
59
|
+
Math.abs(result[3] - 1.0) < 0.01,
|
|
60
|
+
`Expected Variance = 1.0, got ${result[3]}`
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await t.test(
|
|
65
|
+
"should handle streaming with time-based windows for Variance",
|
|
66
|
+
async () => {
|
|
67
|
+
const pipeline = createDspPipeline();
|
|
68
|
+
// 300ms window
|
|
69
|
+
pipeline.Variance({ mode: "moving", windowDuration: 300 });
|
|
70
|
+
|
|
71
|
+
// First chunk: samples at 0, 100, 200ms - values 2, 4, 6
|
|
72
|
+
const chunk1 = new Float32Array([2, 4, 6]);
|
|
73
|
+
const ts1 = new Float32Array([0, 100, 200]);
|
|
74
|
+
const result1 = await pipeline.process(chunk1, ts1, { channels: 1 });
|
|
75
|
+
|
|
76
|
+
// At 200ms: Variance([2, 4, 6])
|
|
77
|
+
// Mean = 4, Variance = ((2-4)^2 + (4-4)^2 + (6-4)^2) / 3 = (4 + 0 + 4) / 3 ≈ 2.667
|
|
78
|
+
assert.ok(
|
|
79
|
+
Math.abs(result1[2] - 8 / 3) < 0.01,
|
|
80
|
+
"First chunk Variance should be ~2.667"
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Second chunk: samples at 250, 600ms
|
|
84
|
+
const chunk2 = new Float32Array([8, 20]);
|
|
85
|
+
const ts2 = new Float32Array([250, 600]);
|
|
86
|
+
const result2 = await pipeline.process(chunk2, ts2, { channels: 1 });
|
|
87
|
+
|
|
88
|
+
// At 250ms: all previous samples still valid (within 300ms)
|
|
89
|
+
// Variance([2, 4, 6, 8])
|
|
90
|
+
// Mean = 5, Variance = ((2-5)^2 + (4-5)^2 + (6-5)^2 + (8-5)^2) / 4 = (9 + 1 + 1 + 9) / 4 = 5
|
|
91
|
+
assert.ok(
|
|
92
|
+
Math.abs(result2[0] - 5.0) < 0.01,
|
|
93
|
+
`At 250ms expected Variance = 5.0, got ${result2[0]}`
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// At 600ms: samples at 0, 100, 200, 250 all expired (>300ms old)
|
|
97
|
+
// Variance([20]) = 0
|
|
98
|
+
assert.ok(
|
|
99
|
+
Math.abs(result2[1] - 0.0) < 0.01,
|
|
100
|
+
`At 600ms expected Variance = 0, got ${result2[1]}`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("Time-Based Z-Score Normalization", async (t) => {
|
|
107
|
+
await t.test("should expire samples based on age for Z-Score", async () => {
|
|
108
|
+
const pipeline = createDspPipeline();
|
|
109
|
+
// 1 second window
|
|
110
|
+
pipeline.ZScoreNormalize({
|
|
111
|
+
mode: "moving",
|
|
112
|
+
windowDuration: 1000,
|
|
113
|
+
epsilon: 1e-6,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// First 3 samples at 0, 100, 200ms - values 10, 20, 30
|
|
117
|
+
const chunk1 = new Float32Array([10, 20, 30]);
|
|
118
|
+
const ts1 = new Float32Array([0, 100, 200]);
|
|
119
|
+
const result1 = await pipeline.process(chunk1, ts1, { channels: 1 });
|
|
120
|
+
|
|
121
|
+
// At 200ms: Z-Score of 30 given [10, 20, 30]
|
|
122
|
+
// Mean = 20, StdDev = sqrt(Variance) = sqrt(66.667) ≈ 8.165
|
|
123
|
+
// Z-Score = (30 - 20) / 8.165 ≈ 1.225
|
|
124
|
+
const mean1 = 20;
|
|
125
|
+
const variance1 = 200 / 3; // ((10-20)^2 + (20-20)^2 + (30-20)^2) / 3
|
|
126
|
+
const stddev1 = Math.sqrt(variance1);
|
|
127
|
+
const expectedZ1 = (30 - mean1) / stddev1;
|
|
128
|
+
assert.ok(
|
|
129
|
+
Math.abs(result1[2] - expectedZ1) < 0.01,
|
|
130
|
+
`Expected Z-Score ≈ ${expectedZ1.toFixed(3)}, got ${result1[2]}`
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Sample at 2200ms - 2.2 seconds later (>1s window)
|
|
134
|
+
// Samples at 0, 100, 200ms should be EXPIRED
|
|
135
|
+
const chunk2 = new Float32Array([50]);
|
|
136
|
+
const ts2 = new Float32Array([2200]);
|
|
137
|
+
const result2 = await pipeline.process(chunk2, ts2, { channels: 1 });
|
|
138
|
+
|
|
139
|
+
// At 2200ms: only sample at 2200ms remains
|
|
140
|
+
// Z-Score([50]) = 0 (stddev is 0, so returns 0)
|
|
141
|
+
assert.ok(
|
|
142
|
+
Math.abs(result2[0] - 0.0) < 0.01,
|
|
143
|
+
`Expected Z-Score = 0.0, got ${result2[0]}`
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await t.test("should work with irregular sampling for Z-Score", async () => {
|
|
148
|
+
const pipeline = createDspPipeline();
|
|
149
|
+
// 500ms window
|
|
150
|
+
pipeline.ZScoreNormalize({
|
|
151
|
+
mode: "moving",
|
|
152
|
+
windowDuration: 500,
|
|
153
|
+
epsilon: 1e-6,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const samples = new Float32Array([10, 20, 100, 110]);
|
|
157
|
+
const timestamps = new Float32Array([
|
|
158
|
+
0, // Sample at 0ms
|
|
159
|
+
50, // Sample at 50ms
|
|
160
|
+
600, // Sample at 600ms (550ms gap!)
|
|
161
|
+
650, // Sample at 650ms
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
const result = await pipeline.process(samples, timestamps, {
|
|
165
|
+
channels: 1,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// At 650ms with 500ms window:
|
|
169
|
+
// - Sample at 0ms EXPIRED (650 - 0 = 650ms > 500ms)
|
|
170
|
+
// - Sample at 50ms EXPIRED (650 - 50 = 600ms > 500ms)
|
|
171
|
+
// - Sample at 600ms KEPT (650 - 600 = 50ms < 500ms)
|
|
172
|
+
// - Sample at 650ms KEPT (current)
|
|
173
|
+
// Z-Score of 110 given [100, 110]
|
|
174
|
+
// Mean = 105, StdDev = sqrt(25) = 5
|
|
175
|
+
// Z-Score = (110 - 105) / 5 = 1.0
|
|
176
|
+
assert.ok(
|
|
177
|
+
Math.abs(result[3] - 1.0) < 0.01,
|
|
178
|
+
`Expected Z-Score = 1.0, got ${result[3]}`
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await t.test(
|
|
183
|
+
"should handle streaming with time-based windows for Z-Score",
|
|
184
|
+
async () => {
|
|
185
|
+
const pipeline = createDspPipeline();
|
|
186
|
+
// 300ms window
|
|
187
|
+
pipeline.ZScoreNormalize({
|
|
188
|
+
mode: "moving",
|
|
189
|
+
windowDuration: 300,
|
|
190
|
+
epsilon: 1e-6,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// First chunk: samples at 0, 100, 200ms - values 0, 10, 20
|
|
194
|
+
const chunk1 = new Float32Array([0, 10, 20]);
|
|
195
|
+
const ts1 = new Float32Array([0, 100, 200]);
|
|
196
|
+
const result1 = await pipeline.process(chunk1, ts1, { channels: 1 });
|
|
197
|
+
|
|
198
|
+
// At 200ms: Z-Score of 20 given [0, 10, 20]
|
|
199
|
+
// Mean = 10, Variance = 66.667, StdDev ≈ 8.165
|
|
200
|
+
// Z-Score = (20 - 10) / 8.165 ≈ 1.225
|
|
201
|
+
const mean1 = 10;
|
|
202
|
+
const variance1 = 200 / 3;
|
|
203
|
+
const stddev1 = Math.sqrt(variance1);
|
|
204
|
+
const expectedZ1 = (20 - mean1) / stddev1;
|
|
205
|
+
assert.ok(
|
|
206
|
+
Math.abs(result1[2] - expectedZ1) < 0.01,
|
|
207
|
+
"First chunk Z-Score should be ~1.225"
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// Second chunk: samples at 250, 600ms
|
|
211
|
+
const chunk2 = new Float32Array([30, 100]);
|
|
212
|
+
const ts2 = new Float32Array([250, 600]);
|
|
213
|
+
const result2 = await pipeline.process(chunk2, ts2, { channels: 1 });
|
|
214
|
+
|
|
215
|
+
// At 250ms: all previous samples still valid (within 300ms)
|
|
216
|
+
// Z-Score of 30 given [0, 10, 20, 30]
|
|
217
|
+
// Mean = 15, Variance = 125, StdDev ≈ 11.18
|
|
218
|
+
// Z-Score = (30 - 15) / 11.18 ≈ 1.342
|
|
219
|
+
const mean2 = 15;
|
|
220
|
+
const variance2 = 125;
|
|
221
|
+
const stddev2 = Math.sqrt(variance2);
|
|
222
|
+
const expectedZ2 = (30 - mean2) / stddev2;
|
|
223
|
+
assert.ok(
|
|
224
|
+
Math.abs(result2[0] - expectedZ2) < 0.01,
|
|
225
|
+
`At 250ms expected Z-Score ≈ ${expectedZ2.toFixed(3)}, got ${
|
|
226
|
+
result2[0]
|
|
227
|
+
}`
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// At 600ms: samples at 0, 100, 200, 250 all expired (>300ms old)
|
|
231
|
+
// Z-Score([100]) = 0 (single sample, stddev = 0)
|
|
232
|
+
assert.ok(
|
|
233
|
+
Math.abs(result2[1] - 0.0) < 0.01,
|
|
234
|
+
`At 600ms expected Z-Score = 0, got ${result2[1]}`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("Backward Compatibility - Variance and Z-Score", async (t) => {
|
|
241
|
+
await t.test("Variance should work without timestamps", async () => {
|
|
242
|
+
const pipeline = createDspPipeline();
|
|
243
|
+
pipeline.Variance({ mode: "moving", windowSize: 3 });
|
|
244
|
+
|
|
245
|
+
const samples = new Float32Array([1, 2, 3, 10]);
|
|
246
|
+
const result = await pipeline.process(samples, { channels: 1 });
|
|
247
|
+
|
|
248
|
+
// Should use sample-count mode (last 3 samples)
|
|
249
|
+
// At sample 4: Variance([2, 3, 10])
|
|
250
|
+
// Mean = 5, Variance = ((2-5)^2 + (3-5)^2 + (10-5)^2) / 3 = (9 + 4 + 25) / 3 ≈ 12.667
|
|
251
|
+
assert.ok(
|
|
252
|
+
Math.abs(result[3] - 38 / 3) < 0.01,
|
|
253
|
+
"Variance should work in sample-count mode"
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
await t.test("Z-Score should work without timestamps", async () => {
|
|
258
|
+
const pipeline = createDspPipeline();
|
|
259
|
+
pipeline.ZScoreNormalize({
|
|
260
|
+
mode: "moving",
|
|
261
|
+
windowSize: 3,
|
|
262
|
+
epsilon: 1e-6,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const samples = new Float32Array([10, 20, 30, 100]);
|
|
266
|
+
const result = await pipeline.process(samples, { channels: 1 });
|
|
267
|
+
|
|
268
|
+
// Should use sample-count mode (last 3 samples)
|
|
269
|
+
// At sample 4: Z-Score of 100 given [20, 30, 100]
|
|
270
|
+
// Mean = 50, Variance = ((20-50)^2 + (30-50)^2 + (100-50)^2) / 3 = (900 + 400 + 2500) / 3 = 3800/3
|
|
271
|
+
// StdDev ≈ 35.59
|
|
272
|
+
// Z-Score = (100 - 50) / 35.59 ≈ 1.405
|
|
273
|
+
const mean = 50;
|
|
274
|
+
const variance = 3800 / 3;
|
|
275
|
+
const stddev = Math.sqrt(variance);
|
|
276
|
+
const expectedZ = (100 - mean) / stddev;
|
|
277
|
+
assert.ok(
|
|
278
|
+
Math.abs(result[3] - expectedZ) < 0.01,
|
|
279
|
+
`Z-Score should work in sample-count mode, expected ${expectedZ.toFixed(
|
|
280
|
+
3
|
|
281
|
+
)}, got ${result[3]}`
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { describe, test } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { createDspPipeline } from "../index.js";
|
|
4
|
+
|
|
5
|
+
describe("Time-Series Processing", () => {
|
|
6
|
+
describe("Process with Timestamps", () => {
|
|
7
|
+
test("should accept timestamps array (legacy sample-based)", async () => {
|
|
8
|
+
const pipeline = createDspPipeline();
|
|
9
|
+
pipeline.MovingAverage({ mode: "moving", windowSize: 3 });
|
|
10
|
+
|
|
11
|
+
const samples = new Float32Array([1, 2, 3, 4, 5]);
|
|
12
|
+
const timestamps = new Float32Array([0, 1, 2, 3, 4]); // Sample indices
|
|
13
|
+
|
|
14
|
+
const output = await pipeline.process(samples, timestamps, {
|
|
15
|
+
channels: 1,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
assert.ok(output instanceof Float32Array);
|
|
19
|
+
assert.strictEqual(output.length, 5);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("should accept timestamps with milliseconds", async () => {
|
|
23
|
+
const pipeline = createDspPipeline();
|
|
24
|
+
pipeline.MovingAverage({ mode: "moving", windowSize: 3 });
|
|
25
|
+
|
|
26
|
+
const samples = new Float32Array([1, 2, 3, 4, 5]);
|
|
27
|
+
// Timestamps at 100ms intervals
|
|
28
|
+
const timestamps = new Float32Array([0, 100, 200, 300, 400]);
|
|
29
|
+
|
|
30
|
+
const output = await pipeline.process(samples, timestamps, {
|
|
31
|
+
channels: 1,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
assert.ok(output instanceof Float32Array);
|
|
35
|
+
assert.strictEqual(output.length, 5);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("should validate timestamp length matches sample length", async () => {
|
|
39
|
+
const pipeline = createDspPipeline();
|
|
40
|
+
pipeline.MovingAverage({ mode: "moving", windowSize: 3 });
|
|
41
|
+
|
|
42
|
+
const samples = new Float32Array([1, 2, 3, 4, 5]);
|
|
43
|
+
const timestamps = new Float32Array([0, 100, 200]); // Wrong length!
|
|
44
|
+
|
|
45
|
+
await assert.rejects(
|
|
46
|
+
async () => {
|
|
47
|
+
await pipeline.process(samples, timestamps, { channels: 1 });
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
message: /Timestamp.*length.*must match.*sample.*length/i,
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("should auto-generate timestamps from sampleRate", async () => {
|
|
56
|
+
const pipeline = createDspPipeline();
|
|
57
|
+
pipeline.MovingAverage({ mode: "moving", windowSize: 3 });
|
|
58
|
+
|
|
59
|
+
const samples = new Float32Array([1, 2, 3, 4, 5]);
|
|
60
|
+
|
|
61
|
+
// Legacy mode: auto-generates timestamps from sampleRate
|
|
62
|
+
const output = await pipeline.process(samples, {
|
|
63
|
+
sampleRate: 100, // 100 Hz = 10ms per sample
|
|
64
|
+
channels: 1,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
assert.ok(output instanceof Float32Array);
|
|
68
|
+
assert.strictEqual(output.length, 5);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("should work with windowDuration parameter", async () => {
|
|
72
|
+
const pipeline = createDspPipeline();
|
|
73
|
+
// Using windowDuration instead of windowSize
|
|
74
|
+
pipeline.MovingAverage({ mode: "moving", windowDuration: 5000 }); // 5 seconds
|
|
75
|
+
|
|
76
|
+
const samples = new Float32Array([1, 2, 3, 4, 5]);
|
|
77
|
+
const timestamps = new Float32Array([0, 1000, 2000, 3000, 4000]);
|
|
78
|
+
|
|
79
|
+
const output = await pipeline.process(samples, timestamps, {
|
|
80
|
+
channels: 1,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
assert.ok(output instanceof Float32Array);
|
|
84
|
+
assert.strictEqual(output.length, 5);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("should work with processCopy and timestamps", async () => {
|
|
88
|
+
const pipeline = createDspPipeline();
|
|
89
|
+
pipeline.MovingAverage({ mode: "moving", windowSize: 3 });
|
|
90
|
+
|
|
91
|
+
const samples = new Float32Array([1, 2, 3, 4, 5]);
|
|
92
|
+
const timestamps = new Float32Array([0, 100, 200, 300, 400]);
|
|
93
|
+
|
|
94
|
+
const output = await pipeline.processCopy(samples, timestamps, {
|
|
95
|
+
channels: 1,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Original should be unchanged
|
|
99
|
+
assert.deepStrictEqual(
|
|
100
|
+
Array.from(samples),
|
|
101
|
+
[1, 2, 3, 4, 5],
|
|
102
|
+
"Original samples should be unchanged"
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Output should be different
|
|
106
|
+
assert.ok(output instanceof Float32Array);
|
|
107
|
+
assert.strictEqual(output.length, 5);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("should support multi-channel with timestamps", async () => {
|
|
111
|
+
const pipeline = createDspPipeline();
|
|
112
|
+
pipeline.MovingAverage({ mode: "moving", windowSize: 3 });
|
|
113
|
+
|
|
114
|
+
// 2 channels, 3 samples per channel = 6 total samples (interleaved)
|
|
115
|
+
const samples = new Float32Array([1, 10, 2, 20, 3, 30]);
|
|
116
|
+
const timestamps = new Float32Array([0, 0, 100, 100, 200, 200]); // Timestamps per sample
|
|
117
|
+
|
|
118
|
+
const output = await pipeline.process(samples, timestamps, {
|
|
119
|
+
channels: 2,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
assert.ok(output instanceof Float32Array);
|
|
123
|
+
assert.strictEqual(output.length, 6);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("Backwards Compatibility", () => {
|
|
128
|
+
test("should work without timestamps (legacy mode)", async () => {
|
|
129
|
+
const pipeline = createDspPipeline();
|
|
130
|
+
pipeline.MovingAverage({ mode: "moving", windowSize: 3 });
|
|
131
|
+
|
|
132
|
+
const samples = new Float32Array([1, 2, 3, 4, 5]);
|
|
133
|
+
|
|
134
|
+
// Legacy API: no timestamps
|
|
135
|
+
const output = await pipeline.process(samples, { channels: 1 });
|
|
136
|
+
|
|
137
|
+
assert.ok(output instanceof Float32Array);
|
|
138
|
+
assert.strictEqual(output.length, 5);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("should maintain same results in legacy mode", async () => {
|
|
142
|
+
const pipeline1 = createDspPipeline();
|
|
143
|
+
pipeline1.MovingAverage({ mode: "moving", windowSize: 3 });
|
|
144
|
+
|
|
145
|
+
const pipeline2 = createDspPipeline();
|
|
146
|
+
pipeline2.MovingAverage({ mode: "moving", windowSize: 3 });
|
|
147
|
+
|
|
148
|
+
const samples1 = new Float32Array([1, 2, 3, 4, 5]);
|
|
149
|
+
const samples2 = new Float32Array([1, 2, 3, 4, 5]);
|
|
150
|
+
|
|
151
|
+
// Legacy mode
|
|
152
|
+
const output1 = await pipeline1.process(samples1, { channels: 1 });
|
|
153
|
+
|
|
154
|
+
// With explicit sequential timestamps (should behave the same)
|
|
155
|
+
const timestamps = new Float32Array([0, 1, 2, 3, 4]);
|
|
156
|
+
const output2 = await pipeline2.process(samples2, timestamps, {
|
|
157
|
+
channels: 1,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Results should be identical
|
|
161
|
+
for (let i = 0; i < output1.length; i++) {
|
|
162
|
+
assert.ok(
|
|
163
|
+
Math.abs(output1[i] - output2[i]) < 0.0001,
|
|
164
|
+
`Expected outputs to match at index ${i}: ${output1[i]} vs ${output2[i]}`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("Filter Parameter Validation", () => {
|
|
171
|
+
test("should accept windowSize (legacy)", () => {
|
|
172
|
+
const pipeline = createDspPipeline();
|
|
173
|
+
assert.doesNotThrow(() => {
|
|
174
|
+
pipeline.MovingAverage({ mode: "moving", windowSize: 10 });
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("should accept windowDuration (new)", () => {
|
|
179
|
+
const pipeline = createDspPipeline();
|
|
180
|
+
assert.doesNotThrow(() => {
|
|
181
|
+
pipeline.MovingAverage({ mode: "moving", windowDuration: 5000 });
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("should accept both windowSize and windowDuration", () => {
|
|
186
|
+
const pipeline = createDspPipeline();
|
|
187
|
+
assert.doesNotThrow(() => {
|
|
188
|
+
pipeline.MovingAverage({
|
|
189
|
+
mode: "moving",
|
|
190
|
+
windowSize: 10,
|
|
191
|
+
windowDuration: 5000,
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("should reject neither windowSize nor windowDuration", () => {
|
|
197
|
+
const pipeline = createDspPipeline();
|
|
198
|
+
assert.throws(
|
|
199
|
+
() => {
|
|
200
|
+
pipeline.MovingAverage({ mode: "moving" } as any);
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "TypeError",
|
|
204
|
+
message:
|
|
205
|
+
/either windowSize or windowDuration must be specified for "moving" mode/,
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("should reject invalid windowDuration", () => {
|
|
211
|
+
const pipeline = createDspPipeline();
|
|
212
|
+
assert.throws(
|
|
213
|
+
() => {
|
|
214
|
+
pipeline.MovingAverage({ mode: "moving", windowDuration: -100 });
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "TypeError",
|
|
218
|
+
message: /windowDuration must be positive/,
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("All Filters Support Time-Series", () => {
|
|
225
|
+
test("Rms should accept windowDuration", () => {
|
|
226
|
+
const pipeline = createDspPipeline();
|
|
227
|
+
assert.doesNotThrow(() => {
|
|
228
|
+
pipeline.Rms({ mode: "moving", windowDuration: 5000 });
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("Variance should accept windowDuration", () => {
|
|
233
|
+
const pipeline = createDspPipeline();
|
|
234
|
+
assert.doesNotThrow(() => {
|
|
235
|
+
pipeline.Variance({ mode: "moving", windowDuration: 5000 });
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("ZScoreNormalize should accept windowDuration", () => {
|
|
240
|
+
const pipeline = createDspPipeline();
|
|
241
|
+
assert.doesNotThrow(() => {
|
|
242
|
+
pipeline.ZScoreNormalize({ mode: "moving", windowDuration: 5000 });
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("MeanAbsoluteValue should accept windowDuration", () => {
|
|
247
|
+
const pipeline = createDspPipeline();
|
|
248
|
+
assert.doesNotThrow(() => {
|
|
249
|
+
pipeline.MeanAbsoluteValue({ mode: "moving", windowDuration: 5000 });
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|