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,617 @@
|
|
|
1
|
+
# Time-Series Processing Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The DSP library supports **time-series processing** with explicit timestamps, enabling:
|
|
6
|
+
|
|
7
|
+
- ✅ Explicit timestamp tracking for sample metadata
|
|
8
|
+
- ✅ Intuitive time-based window specification (via `windowDuration`)
|
|
9
|
+
- ✅ **True time-based sample expiration** - samples expire by actual age, not just count
|
|
10
|
+
- ✅ Proper state persistence with timestamps
|
|
11
|
+
- ✅ Uniform and irregular sampling data processing
|
|
12
|
+
- ✅ 100% backwards compatibility with sample-based API
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### Legacy Sample-Based (Still Works!)
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { createDspPipeline } from "dspx";
|
|
22
|
+
|
|
23
|
+
const pipeline = createDspPipeline();
|
|
24
|
+
pipeline.MovingAverage({ mode: "moving", windowSize: 100 });
|
|
25
|
+
|
|
26
|
+
const samples = new Float32Array(1000);
|
|
27
|
+
// ... fill with sensor data ...
|
|
28
|
+
|
|
29
|
+
await pipeline.process(samples, {
|
|
30
|
+
sampleRate: 100, // 100 Hz
|
|
31
|
+
channels: 1,
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### New Time-Based Processing
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { createDspPipeline } from "dspx";
|
|
39
|
+
|
|
40
|
+
const pipeline = createDspPipeline();
|
|
41
|
+
// Use time-based window instead of sample count
|
|
42
|
+
pipeline.MovingAverage({ mode: "moving", windowDuration: 5000 }); // 5 seconds
|
|
43
|
+
|
|
44
|
+
const samples = new Float32Array([1.2, 3.4, 2.1, 4.5, 3.3]);
|
|
45
|
+
const timestamps = new Float32Array([0, 100, 250, 400, 500]); // milliseconds
|
|
46
|
+
|
|
47
|
+
await pipeline.process(samples, timestamps, { channels: 1 });
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Three Processing Modes
|
|
53
|
+
|
|
54
|
+
### 1. Legacy Mode (Auto-Generated Timestamps from Sample Rate)
|
|
55
|
+
|
|
56
|
+
**Best for:** Uniform sampling, backwards compatibility
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
const pipeline = createDspPipeline();
|
|
60
|
+
pipeline.MovingAverage({ mode: "moving", windowSize: 50 });
|
|
61
|
+
|
|
62
|
+
await pipeline.process(samples, {
|
|
63
|
+
sampleRate: 100, // 100 Hz = 10ms per sample
|
|
64
|
+
channels: 1,
|
|
65
|
+
});
|
|
66
|
+
// Internally generates timestamps: [0, 10, 20, 30, ...]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 2. Time-Based Mode (Explicit Timestamps)
|
|
70
|
+
|
|
71
|
+
**Best for:** Tracking sample timing, time-based window specification
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const pipeline = createDspPipeline();
|
|
75
|
+
pipeline.MovingAverage({ mode: "moving", windowDuration: 5000 }); // 5 seconds (converted to sample count)
|
|
76
|
+
|
|
77
|
+
// Real timestamps from your sensors/network
|
|
78
|
+
const timestamps = new Float32Array([
|
|
79
|
+
1698889200000, // Unix timestamp in ms
|
|
80
|
+
1698889200150,
|
|
81
|
+
1698889200320,
|
|
82
|
+
1698889200500,
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
await pipeline.process(samples, timestamps, { channels: 1 });
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 3. Auto-Sequential Mode (No Sample Rate)
|
|
89
|
+
|
|
90
|
+
**Best for:** Sample-count based processing without time awareness
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
const pipeline = createDspPipeline();
|
|
94
|
+
pipeline.MovingAverage({ mode: "moving", windowSize: 10 });
|
|
95
|
+
|
|
96
|
+
await pipeline.process(samples, { channels: 1 });
|
|
97
|
+
// Internally generates timestamps: [0, 1, 2, 3, ...]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Window Parameters: windowSize vs windowDuration
|
|
103
|
+
|
|
104
|
+
### windowSize (Legacy - Sample Count)
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
pipeline.MovingAverage({
|
|
108
|
+
mode: "moving",
|
|
109
|
+
windowSize: 100, // Last 100 samples
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Pros:**
|
|
114
|
+
|
|
115
|
+
- Predictable memory usage
|
|
116
|
+
- Consistent across sample rates
|
|
117
|
+
- Faster (no timestamp checks)
|
|
118
|
+
|
|
119
|
+
**Cons:**
|
|
120
|
+
|
|
121
|
+
- Not intuitive for time-based analysis
|
|
122
|
+
- ~~Breaks with irregular sampling~~
|
|
123
|
+
- Requires manual sample rate calculations
|
|
124
|
+
|
|
125
|
+
### windowDuration (Time-Based Expiration)
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
pipeline.MovingAverage({
|
|
129
|
+
mode: "moving",
|
|
130
|
+
windowDuration: 5000, // Last 5 seconds of data (true time-based)
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Pros:**
|
|
135
|
+
|
|
136
|
+
- **True time-based expiration** - samples automatically expire when they're older than the window duration
|
|
137
|
+
- Intuitive ("5 seconds of data")
|
|
138
|
+
- Works correctly with irregular sampling (samples expire by age, not count)
|
|
139
|
+
- Independent of sample rate
|
|
140
|
+
- Accurate representation of data within the time window
|
|
141
|
+
|
|
142
|
+
**Cons:**
|
|
143
|
+
|
|
144
|
+
- Requires timestamps to be provided
|
|
145
|
+
- Slightly more memory overhead (stores timestamps alongside samples)
|
|
146
|
+
- Requires buffer size estimation at initialization (uses 3x safety factor)
|
|
147
|
+
|
|
148
|
+
**Implementation:**
|
|
149
|
+
|
|
150
|
+
When `windowDuration` is specified, the library implements **true time-based expiration**:
|
|
151
|
+
|
|
152
|
+
1. A circular buffer is created with capacity = 3× estimated sample count (safety margin)
|
|
153
|
+
2. Each sample is stored with its timestamp
|
|
154
|
+
3. Before adding new samples, old samples are expired using `expireOld(currentTimestamp)`
|
|
155
|
+
4. Samples are removed when: `sample_timestamp < current_timestamp - windowDuration`
|
|
156
|
+
|
|
157
|
+
**Example - Irregular Sampling:**
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
pipeline.MovingAverage({ mode: "moving", windowDuration: 1000 }); // 1 second window
|
|
161
|
+
|
|
162
|
+
// Samples arrive at irregular intervals
|
|
163
|
+
const samples = new Float32Array([10, 20, 30, 40]);
|
|
164
|
+
const timestamps = new Float32Array([
|
|
165
|
+
0, // Sample at 0ms
|
|
166
|
+
50, // Sample at 50ms
|
|
167
|
+
600, // Sample at 600ms (550ms gap!)
|
|
168
|
+
650, // Sample at 650ms
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
await pipeline.process(samples, timestamps, { channels: 1 });
|
|
172
|
+
|
|
173
|
+
// At 650ms:
|
|
174
|
+
// - Sample at 0ms is EXPIRED (650 - 0 = 650ms > 1000ms window) ❌
|
|
175
|
+
// - Sample at 50ms is EXPIRED (650 - 50 = 600ms > 1000ms window) ❌
|
|
176
|
+
// - Sample at 600ms is KEPT (650 - 600 = 50ms < 1000ms) ✓
|
|
177
|
+
// - Sample at 650ms is KEPT (current sample) ✓
|
|
178
|
+
// Result: Moving average of [30, 40] = 35
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
This is fundamentally different from sample-count windows, where exactly N samples would be kept regardless of their timestamps.
|
|
182
|
+
|
|
183
|
+
**Buffer Size Estimation:**
|
|
184
|
+
|
|
185
|
+
The circular buffer capacity is estimated as `3 × (windowDuration_ms / 1000) × estimated_sample_rate`:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
// Example: 5 second window at 100 Hz
|
|
189
|
+
// windowDuration = 5000ms
|
|
190
|
+
// sample_rate = 100 Hz
|
|
191
|
+
// capacity = 3 × (5000 / 1000) × 100 = 1500 samples
|
|
192
|
+
|
|
193
|
+
// Why 3x?
|
|
194
|
+
// - Handles bursts (multiple samples arriving together)
|
|
195
|
+
// - Safety margin for sample rate variations
|
|
196
|
+
// - Prevents buffer overflow before expiration
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Important:** True time-based expiration means the number of samples in the window varies dynamically based on sample timing, not a fixed count.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## All Filters Support Time-Series
|
|
204
|
+
|
|
205
|
+
All filters support both `windowSize` and `windowDuration`:
|
|
206
|
+
|
|
207
|
+
### MovingAverage ✅ Time-based expiration
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
// Sample-based
|
|
211
|
+
pipeline.MovingAverage({ mode: "moving", windowSize: 50 });
|
|
212
|
+
|
|
213
|
+
// Time-based (true time-based expiration)
|
|
214
|
+
pipeline.MovingAverage({ mode: "moving", windowDuration: 10000 }); // 10 seconds
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### RMS (Root Mean Square) ✅ Time-based expiration
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// Time-based RMS over last 100ms
|
|
221
|
+
pipeline.Rms({ mode: "moving", windowDuration: 100 });
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### MeanAbsoluteValue ✅ Time-based expiration
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// Time-based MAV over last 250ms
|
|
228
|
+
pipeline.MeanAbsoluteValue({ mode: "moving", windowDuration: 250 });
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Variance ✅ Time-based expiration
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
// Time-based Variance over last 3 seconds (true time-based expiration)
|
|
235
|
+
pipeline.Variance({ mode: "moving", windowDuration: 3000 });
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Z-Score Normalization ✅ Time-based expiration
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
// Time-based Z-Score over last 30 seconds (true time-based expiration)
|
|
242
|
+
pipeline.ZScoreNormalize({
|
|
243
|
+
mode: "moving",
|
|
244
|
+
windowDuration: 30000,
|
|
245
|
+
epsilon: 1e-6,
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Multi-Channel Processing with Timestamps
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
const pipeline = createDspPipeline();
|
|
255
|
+
pipeline.MovingAverage({ mode: "moving", windowDuration: 5000 });
|
|
256
|
+
|
|
257
|
+
// 2 channels, interleaved: [ch0_sample0, ch1_sample0, ch0_sample1, ch1_sample1, ...]
|
|
258
|
+
const samples = new Float32Array([
|
|
259
|
+
1.0,
|
|
260
|
+
10.0, // Sample 0: ch0=1.0, ch1=10.0
|
|
261
|
+
2.0,
|
|
262
|
+
20.0, // Sample 1: ch0=2.0, ch1=20.0
|
|
263
|
+
3.0,
|
|
264
|
+
30.0, // Sample 2: ch0=3.0, ch1=30.0
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
// One timestamp per SAMPLE (not per channel value)
|
|
268
|
+
const timestamps = new Float32Array([0, 100, 200]);
|
|
269
|
+
|
|
270
|
+
await pipeline.process(samples, timestamps, { channels: 2 });
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Important:** The timestamps array length should equal `samples.length / channels`.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Real-World Examples
|
|
278
|
+
|
|
279
|
+
### IoT Sensor with Network Jitter
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
import { createDspPipeline } from "dspx";
|
|
283
|
+
|
|
284
|
+
const pipeline = createDspPipeline();
|
|
285
|
+
pipeline.MovingAverage({ mode: "moving", windowDuration: 10000 }); // 10 second window
|
|
286
|
+
|
|
287
|
+
async function processSensorData(sensorReadings) {
|
|
288
|
+
const samples = new Float32Array(sensorReadings.length);
|
|
289
|
+
const timestamps = new Float32Array(sensorReadings.length);
|
|
290
|
+
|
|
291
|
+
sensorReadings.forEach((reading, i) => {
|
|
292
|
+
samples[i] = reading.value;
|
|
293
|
+
timestamps[i] = reading.timestamp; // Unix timestamp in ms
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const smoothed = await pipeline.process(samples, timestamps, { channels: 1 });
|
|
297
|
+
return smoothed;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Readings might arrive at irregular intervals due to network latency
|
|
301
|
+
const readings = [
|
|
302
|
+
{ value: 23.5, timestamp: 1698889200000 },
|
|
303
|
+
{ value: 24.1, timestamp: 1698889200150 }, // 150ms later
|
|
304
|
+
{ value: 23.8, timestamp: 1698889200380 }, // 230ms later (jitter)
|
|
305
|
+
{ value: 24.5, timestamp: 1698889200500 }, // 120ms later
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
const result = await processSensorData(readings);
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Financial Data (Irregular Market Hours)
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
const pipeline = createDspPipeline();
|
|
315
|
+
pipeline.MovingAverage({ mode: "moving", windowDuration: 3600000 }); // 1 hour
|
|
316
|
+
|
|
317
|
+
// Stock ticks at irregular intervals
|
|
318
|
+
const prices = new Float32Array([100.5, 100.7, 100.3, 101.2, 100.9]);
|
|
319
|
+
const tickTimestamps = new Float32Array([
|
|
320
|
+
1698889200000, // 9:00:00 AM
|
|
321
|
+
1698889260000, // 9:01:00 AM
|
|
322
|
+
1698889290000, // 9:01:30 AM (30 sec later)
|
|
323
|
+
1698889320000, // 9:02:00 AM
|
|
324
|
+
1698889500000, // 9:05:00 AM (3 min gap)
|
|
325
|
+
]);
|
|
326
|
+
|
|
327
|
+
const movingAvg = await pipeline.process(prices, tickTimestamps, {
|
|
328
|
+
channels: 1,
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### EMG/ECG Signal Processing
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
const pipeline = createDspPipeline();
|
|
336
|
+
pipeline
|
|
337
|
+
.Rectify({ mode: "full" })
|
|
338
|
+
.MovingAverage({ mode: "moving", windowDuration: 250 }) // 250ms window
|
|
339
|
+
.Rms({ mode: "moving", windowDuration: 100 }); // 100ms RMS
|
|
340
|
+
|
|
341
|
+
// High-rate biosignal sampling
|
|
342
|
+
const emgSignal = new Float32Array(1000);
|
|
343
|
+
const timestamps = new Float32Array(1000);
|
|
344
|
+
|
|
345
|
+
// Fill with actual sensor data at ~1000 Hz
|
|
346
|
+
for (let i = 0; i < 1000; i++) {
|
|
347
|
+
emgSignal[i] = Math.sin(i * 0.1) + Math.random() * 0.1;
|
|
348
|
+
timestamps[i] = i; // 1ms per sample
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const processed = await pipeline.process(emgSignal, timestamps, {
|
|
352
|
+
channels: 1,
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## State Persistence with Redis
|
|
359
|
+
|
|
360
|
+
Time-series processing works seamlessly with Redis state persistence:
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
import { createDspPipeline } from "dspx";
|
|
364
|
+
import { createClient } from "redis";
|
|
365
|
+
|
|
366
|
+
const redis = createClient();
|
|
367
|
+
await redis.connect();
|
|
368
|
+
|
|
369
|
+
const pipeline = createDspPipeline();
|
|
370
|
+
pipeline.MovingAverage({ mode: "moving", windowDuration: 5000 });
|
|
371
|
+
|
|
372
|
+
// Process streaming data
|
|
373
|
+
async function processChunk(samples, timestamps) {
|
|
374
|
+
// Restore previous state
|
|
375
|
+
const savedState = await redis.get("dsp:sensor:123");
|
|
376
|
+
if (savedState) {
|
|
377
|
+
await pipeline.loadState(savedState);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Process new chunk
|
|
381
|
+
const result = await pipeline.process(samples, timestamps, { channels: 1 });
|
|
382
|
+
|
|
383
|
+
// Save state for next chunk
|
|
384
|
+
const newState = await pipeline.saveState();
|
|
385
|
+
await redis.set("dsp:sensor:123", newState, { EX: 3600 }); // 1 hour TTL
|
|
386
|
+
|
|
387
|
+
return result;
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## Migration Guide
|
|
394
|
+
|
|
395
|
+
### Migrating from Sample-Based to Time-Based
|
|
396
|
+
|
|
397
|
+
#### Before (Sample-Based)
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
const pipeline = createDspPipeline();
|
|
401
|
+
pipeline.MovingAverage({ mode: "moving", windowSize: 100 }); // 100 samples
|
|
402
|
+
|
|
403
|
+
await pipeline.process(samples, {
|
|
404
|
+
sampleRate: 1000, // 1000 Hz
|
|
405
|
+
channels: 1,
|
|
406
|
+
});
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
#### After (Time-Based)
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
const pipeline = createDspPipeline();
|
|
413
|
+
// 100 samples at 1000 Hz = 100ms window
|
|
414
|
+
pipeline.MovingAverage({ mode: "moving", windowDuration: 100 }); // 100ms
|
|
415
|
+
|
|
416
|
+
// Generate timestamps from your data source
|
|
417
|
+
const timestamps = samples.map((_, i) => i * (1000 / 1000)); // 1ms per sample
|
|
418
|
+
|
|
419
|
+
await pipeline.process(samples, timestamps, { channels: 1 });
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Converting Window Size to Duration
|
|
423
|
+
|
|
424
|
+
If you know your sample rate, convert `windowSize` to `windowDuration`:
|
|
425
|
+
|
|
426
|
+
```
|
|
427
|
+
windowDuration (ms) = (windowSize / sampleRate) * 1000
|
|
428
|
+
|
|
429
|
+
Example:
|
|
430
|
+
windowSize = 500 samples
|
|
431
|
+
sampleRate = 100 Hz
|
|
432
|
+
windowDuration = (500 / 100) * 1000 = 5000 ms (5 seconds)
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## Performance Considerations
|
|
438
|
+
|
|
439
|
+
### Memory Usage
|
|
440
|
+
|
|
441
|
+
- **Sample-based windows:** Fixed memory (`windowSize` samples)
|
|
442
|
+
- **Time-based windows:** Fixed memory (converted to `windowSize` samples at initialization)
|
|
443
|
+
|
|
444
|
+
### Processing Speed
|
|
445
|
+
|
|
446
|
+
- **With timestamps:** Minimal overhead (timestamps are passed but not used for expiration)
|
|
447
|
+
- **Without timestamps:** Fastest (legacy mode)
|
|
448
|
+
|
|
449
|
+
**Recommendation:** Use `windowDuration` when you want to specify windows in time units and have consistent sample rates. Use `windowSize` for maximum control and when sample rate varies significantly.
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## Error Handling
|
|
454
|
+
|
|
455
|
+
### Timestamp Validation
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
// Error: Timestamp array length mismatch
|
|
459
|
+
const samples = new Float32Array(5);
|
|
460
|
+
const timestamps = new Float32Array(3); // Wrong!
|
|
461
|
+
|
|
462
|
+
await pipeline.process(samples, timestamps, { channels: 1 });
|
|
463
|
+
// Throws: "Timestamps length (3) must match samples length (5)"
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Window Parameter Validation
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
// Error: Neither windowSize nor windowDuration specified
|
|
470
|
+
pipeline.MovingAverage({ mode: "moving" }); // Throws!
|
|
471
|
+
|
|
472
|
+
// Error: Invalid windowDuration
|
|
473
|
+
pipeline.MovingAverage({ mode: "moving", windowDuration: -100 }); // Throws!
|
|
474
|
+
|
|
475
|
+
// Error: Invalid windowSize
|
|
476
|
+
pipeline.MovingAverage({ mode: "moving", windowSize: 0 }); // Throws!
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## API Reference
|
|
482
|
+
|
|
483
|
+
### Process Methods
|
|
484
|
+
|
|
485
|
+
#### `process(samples, timestamps, options)`
|
|
486
|
+
|
|
487
|
+
Time-based processing with explicit timestamps.
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
async process(
|
|
491
|
+
samples: Float32Array,
|
|
492
|
+
timestamps: Float32Array,
|
|
493
|
+
options: { channels: number }
|
|
494
|
+
): Promise<Float32Array>
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
#### `process(samples, options)`
|
|
498
|
+
|
|
499
|
+
Legacy sample-based processing (auto-generates timestamps).
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
async process(
|
|
503
|
+
samples: Float32Array,
|
|
504
|
+
options: { sampleRate?: number; channels: number }
|
|
505
|
+
): Promise<Float32Array>
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
#### `processCopy(samples, timestamps, options)`
|
|
509
|
+
|
|
510
|
+
Process a copy (preserves original).
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
async processCopy(
|
|
514
|
+
samples: Float32Array,
|
|
515
|
+
timestamps: Float32Array,
|
|
516
|
+
options: { channels: number }
|
|
517
|
+
): Promise<Float32Array>
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### Filter Parameters
|
|
521
|
+
|
|
522
|
+
All filters accept either `windowSize` or `windowDuration`:
|
|
523
|
+
|
|
524
|
+
```typescript
|
|
525
|
+
interface FilterParams {
|
|
526
|
+
mode: "batch" | "moving";
|
|
527
|
+
windowSize?: number; // Number of samples
|
|
528
|
+
windowDuration?: number; // Milliseconds
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
## Best Practices
|
|
535
|
+
|
|
536
|
+
### ✅ DO
|
|
537
|
+
|
|
538
|
+
- Use `windowDuration` for true time-based windows that adapt to irregular sampling
|
|
539
|
+
- Pass explicit timestamps to enable time-based expiration
|
|
540
|
+
- Use `windowSize` for fixed sample-count windows (faster, no timestamp overhead)
|
|
541
|
+
- Validate timestamp ordering (should be non-decreasing) when using timestamps
|
|
542
|
+
- Use state persistence for streaming applications
|
|
543
|
+
- Provide sufficient buffer capacity when using both `windowSize` and `windowDuration`
|
|
544
|
+
|
|
545
|
+
### ❌ DON'T
|
|
546
|
+
|
|
547
|
+
- Forget to provide timestamps when using `windowDuration` (required for time-based expiration)
|
|
548
|
+
- Expect `windowSize` alone to give time-based behavior (it only controls sample count)
|
|
549
|
+
- Assume time-based windows always contain the same number of samples (they adapt dynamically)
|
|
550
|
+
- Forget to handle timezone conversions when using absolute timestamps
|
|
551
|
+
- Process data with backwards-jumping timestamps (non-monotonic time)
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
555
|
+
## Troubleshooting
|
|
556
|
+
|
|
557
|
+
### Q: My time-based windows aren't working as expected
|
|
558
|
+
|
|
559
|
+
**A:** Time-based windows now implement true time-based expiration! Samples are automatically removed when they're older than `windowDuration` from the current timestamp. Make sure you're providing timestamps with each `process()` call. The number of samples in the window will vary dynamically based on your sampling rate and timing.
|
|
560
|
+
|
|
561
|
+
### Q: How does time-based expiration handle irregular sampling?
|
|
562
|
+
|
|
563
|
+
**A:** Time-based expiration works correctly with irregular sampling. Samples expire based on their actual age (timestamp difference), not their position in the buffer. For example, with a 1-second window:
|
|
564
|
+
|
|
565
|
+
- A burst of samples arriving within 100ms will all be kept
|
|
566
|
+
- A sample from 1.5 seconds ago will be expired
|
|
567
|
+
- The window always contains samples from the last `windowDuration` milliseconds
|
|
568
|
+
|
|
569
|
+
### Q: What's the difference between windowSize and windowDuration?
|
|
570
|
+
|
|
571
|
+
**A:**
|
|
572
|
+
|
|
573
|
+
- **windowSize**: Keeps exactly N samples (fixed count, regardless of time span)
|
|
574
|
+
- **windowDuration**: Keeps samples from the last T milliseconds (variable count, based on timing)
|
|
575
|
+
|
|
576
|
+
Use `windowSize` when you want a fixed number of samples. Use `windowDuration` when you want a fixed time span.
|
|
577
|
+
|
|
578
|
+
### Q: Should I use milliseconds or seconds for timestamps?
|
|
579
|
+
|
|
580
|
+
**A:** Use **milliseconds** for consistency with JavaScript `Date.now()` and Unix timestamps. The library expects milliseconds.
|
|
581
|
+
|
|
582
|
+
### Q: Can I use both windowSize and windowDuration?
|
|
583
|
+
|
|
584
|
+
**A:** Yes! When both are specified, the filter uses time-based expiration (`windowDuration`) but also allocates a buffer sized for `windowSize` samples. This gives you control over both the time window and memory usage. However, typically you'd use one or the other:
|
|
585
|
+
|
|
586
|
+
- Use `windowSize` alone for sample-count windows
|
|
587
|
+
- Use `windowDuration` alone for true time-based windows (buffer size auto-estimated)
|
|
588
|
+
|
|
589
|
+
### Q: What happens if I don't provide timestamps?
|
|
590
|
+
|
|
591
|
+
**A:** The library auto-generates sequential timestamps `[0, 1, 2, ...]`. If you provide `sampleRate`, it generates time-based timestamps.
|
|
592
|
+
|
|
593
|
+
---
|
|
594
|
+
|
|
595
|
+
## Future Enhancements (Roadmap)
|
|
596
|
+
|
|
597
|
+
- [x] **True time-based filtering** - ✅ FULLY IMPLEMENTED for all moving window filters!
|
|
598
|
+
- MovingAverage, RMS, MeanAbsoluteValue, Variance, and ZScoreNormalize all support true time-based expiration
|
|
599
|
+
- [ ] **Polyphase decimation/interpolation** - High-quality resampling to uniform intervals
|
|
600
|
+
- Anti-aliasing lowpass filter before decimation
|
|
601
|
+
- Polyphase FIR filter banks for efficient computation
|
|
602
|
+
- Support for arbitrary rational resampling ratios (L/M)
|
|
603
|
+
- [ ] **Timestamp interpolation** - Fill gaps in irregular data with linear/spline interpolation
|
|
604
|
+
- [ ] **Timestamp-aware state format** - Include timestamps in serialized state
|
|
605
|
+
- [ ] **Window overlap control** - For advanced signal analysis (STFT, spectrograms)
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
## Need Help?
|
|
610
|
+
|
|
611
|
+
- **GitHub Issues:** [Report bugs or request features](https://github.com/A-KGeorge/dspx/issues)
|
|
612
|
+
- **Documentation:** Check `docs/` folder for additional guides
|
|
613
|
+
- **Examples:** See `src/ts/examples/` for working code samples
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
**Happy time-series processing! 📈**
|