dspx 1.3.0 → 1.3.2

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.
@@ -1,5 +1,4 @@
1
1
  #include "FilterBankStage.h"
2
- #include <iostream>
3
2
 
4
3
  namespace dsp::adapters
5
4
  {
@@ -18,31 +17,6 @@ namespace dsp::adapters
18
17
 
19
18
  initializeFilters();
20
19
  }
21
- FilterBankStage::~FilterBankStage()
22
- {
23
- try
24
- {
25
- // Explicitly clear filters first to ensure IirFilter destructors run
26
- // before scratch buffers are destroyed
27
- for (size_t ch = 0; ch < m_filters.size(); ++ch)
28
- {
29
- m_filters[ch].clear(); // Destroys all unique_ptrs in this channel
30
- }
31
- m_filters.clear(); // Clear outer vector
32
-
33
- // Clear scratch buffers
34
- m_planarInput.clear();
35
- m_planarOutput.clear();
36
- }
37
- catch (const std::exception &e)
38
- {
39
- std::cerr << "[FilterBankStage] ERROR in destructor: " << e.what() << std::endl;
40
- }
41
- catch (...)
42
- {
43
- std::cerr << "[FilterBankStage] UNKNOWN ERROR in destructor" << std::endl;
44
- }
45
- }
46
20
 
47
21
  void FilterBankStage::processResizing(const float *inputBuffer, size_t inputSize,
48
22
  float *outputBuffer, size_t &outputSize,
@@ -81,8 +55,9 @@ namespace dsp::adapters
81
55
  for (size_t band = 0; band < numBands; ++band)
82
56
  {
83
57
  int outChIndex = (ch * static_cast<int>(numBands)) + static_cast<int>(band);
58
+ int filterIndex = outChIndex; // Same indexing for flat array
84
59
 
85
- m_filters[ch][band]->process(
60
+ m_filters[filterIndex]->process(
86
61
  m_planarInput[ch].data(),
87
62
  m_planarOutput[outChIndex].data(),
88
63
  samplesPerChannel);
@@ -100,18 +75,24 @@ namespace dsp::adapters
100
75
 
101
76
  void FilterBankStage::initializeFilters()
102
77
  {
103
- m_filters.resize(m_numInputChannels);
78
+ size_t numBands = m_definitions.size();
79
+ size_t totalFilters = m_numInputChannels * numBands;
80
+
81
+ m_filters.clear();
82
+ m_filters.reserve(totalFilters);
83
+
104
84
  for (int ch = 0; ch < m_numInputChannels; ++ch)
105
85
  {
106
- m_filters[ch].clear();
107
- for (const auto &def : m_definitions)
86
+ for (size_t band = 0; band < numBands; ++band)
108
87
  {
88
+ const auto &def = m_definitions[band];
89
+
109
90
  // Convert double coefficients to float for IirFilter
110
91
  std::vector<float> b_float(def.b.begin(), def.b.end());
111
92
  std::vector<float> a_float(def.a.begin(), def.a.end());
112
93
 
113
94
  // Create stateful IIR filter for each band
114
- m_filters[ch].push_back(
95
+ m_filters.push_back(
115
96
  std::make_unique<dsp::core::IirFilter<float>>(b_float, a_float, true));
116
97
  }
117
98
  }
@@ -148,12 +129,9 @@ namespace dsp::adapters
148
129
 
149
130
  void FilterBankStage::reset()
150
131
  {
151
- for (auto &ch : m_filters)
132
+ for (auto &filter : m_filters)
152
133
  {
153
- for (auto &filter : ch)
154
- {
155
- filter->reset();
156
- }
134
+ filter->reset();
157
135
  }
158
136
  }
159
137
 
@@ -165,13 +143,16 @@ namespace dsp::adapters
165
143
  state.Set("numBands", m_definitions.size());
166
144
 
167
145
  // Serialize filter states as nested arrays: channels[bands[state]]
168
- Napi::Array channels = Napi::Array::New(env, m_filters.size());
169
- for (size_t ch = 0; ch < m_filters.size(); ++ch)
146
+ size_t numBands = m_definitions.size();
147
+ Napi::Array channels = Napi::Array::New(env, m_numInputChannels);
148
+
149
+ for (int ch = 0; ch < m_numInputChannels; ++ch)
170
150
  {
171
- Napi::Array bands = Napi::Array::New(env, m_filters[ch].size());
172
- for (size_t band = 0; band < m_filters[ch].size(); ++band)
151
+ Napi::Array bands = Napi::Array::New(env, numBands);
152
+ for (size_t band = 0; band < numBands; ++band)
173
153
  {
174
- const auto &filter = m_filters[ch][band];
154
+ int filterIndex = ch * numBands + band;
155
+ const auto &filter = m_filters[filterIndex];
175
156
 
176
157
  Napi::Object filterState = Napi::Object::New(env);
177
158
 
@@ -210,22 +191,24 @@ namespace dsp::adapters
210
191
  throw std::runtime_error("FilterBank: Missing filterStates in serialized data");
211
192
  }
212
193
 
194
+ size_t numBands = m_definitions.size();
213
195
  Napi::Array channels = state.Get("filterStates").As<Napi::Array>();
214
- if (channels.Length() != m_filters.size())
196
+ if (channels.Length() != static_cast<uint32_t>(m_numInputChannels))
215
197
  {
216
198
  throw std::runtime_error("FilterBank: Channel count mismatch in deserialization");
217
199
  }
218
200
 
219
- for (size_t ch = 0; ch < channels.Length(); ++ch)
201
+ for (int ch = 0; ch < m_numInputChannels; ++ch)
220
202
  {
221
203
  Napi::Array bands = channels.Get(ch).As<Napi::Array>();
222
- if (bands.Length() != m_filters[ch].size())
204
+ if (bands.Length() != static_cast<uint32_t>(numBands))
223
205
  {
224
206
  throw std::runtime_error("FilterBank: Band count mismatch in deserialization");
225
207
  }
226
208
 
227
- for (size_t band = 0; band < bands.Length(); ++band)
209
+ for (size_t band = 0; band < numBands; ++band)
228
210
  {
211
+ int filterIndex = ch * numBands + band;
229
212
  Napi::Object filterState = bands.Get(band).As<Napi::Object>();
230
213
 
231
214
  // Deserialize input history
@@ -253,7 +236,7 @@ namespace dsp::adapters
253
236
  }
254
237
 
255
238
  // Restore filter state
256
- m_filters[ch][band]->setState(inputHistory, outputHistory);
239
+ m_filters[filterIndex]->setState(inputHistory, outputHistory);
257
240
  }
258
241
  }
259
242
  }
@@ -265,26 +248,23 @@ namespace dsp::adapters
265
248
  serializer.writeInt32(static_cast<int32_t>(m_definitions.size()));
266
249
 
267
250
  // Serialize each filter's state
268
- for (const auto &ch : m_filters)
251
+ for (const auto &filter : m_filters)
269
252
  {
270
- for (const auto &filter : ch)
271
- {
272
- // Get filter state (input and output history)
273
- auto [inputHistory, outputHistory] = filter->getState();
253
+ // Get filter state (input and output history)
254
+ auto [inputHistory, outputHistory] = filter->getState();
274
255
 
275
- // Write input history
276
- serializer.writeInt32(static_cast<int32_t>(inputHistory.size()));
277
- for (float val : inputHistory)
278
- {
279
- serializer.writeFloat(val);
280
- }
256
+ // Write input history
257
+ serializer.writeInt32(static_cast<int32_t>(inputHistory.size()));
258
+ for (float val : inputHistory)
259
+ {
260
+ serializer.writeFloat(val);
261
+ }
281
262
 
282
- // Write output history
283
- serializer.writeInt32(static_cast<int32_t>(outputHistory.size()));
284
- for (float val : outputHistory)
285
- {
286
- serializer.writeFloat(val);
287
- }
263
+ // Write output history
264
+ serializer.writeInt32(static_cast<int32_t>(outputHistory.size()));
265
+ for (float val : outputHistory)
266
+ {
267
+ serializer.writeFloat(val);
288
268
  }
289
269
  }
290
270
  }
@@ -305,31 +285,28 @@ namespace dsp::adapters
305
285
  }
306
286
 
307
287
  // Restore each filter's state
308
- for (auto &ch : m_filters)
288
+ for (auto &filter : m_filters)
309
289
  {
310
- for (auto &filter : ch)
290
+ // Read input history
291
+ int32_t inputSize = deserializer.readInt32();
292
+ std::vector<float> inputHistory;
293
+ inputHistory.reserve(inputSize);
294
+ for (int32_t i = 0; i < inputSize; ++i)
311
295
  {
312
- // Read input history
313
- int32_t inputSize = deserializer.readInt32();
314
- std::vector<float> inputHistory;
315
- inputHistory.reserve(inputSize);
316
- for (int32_t i = 0; i < inputSize; ++i)
317
- {
318
- inputHistory.push_back(deserializer.readFloat());
319
- }
320
-
321
- // Read output history
322
- int32_t outputSize = deserializer.readInt32();
323
- std::vector<float> outputHistory;
324
- outputHistory.reserve(outputSize);
325
- for (int32_t i = 0; i < outputSize; ++i)
326
- {
327
- outputHistory.push_back(deserializer.readFloat());
328
- }
296
+ inputHistory.push_back(deserializer.readFloat());
297
+ }
329
298
 
330
- // Restore filter state
331
- filter->setState(inputHistory, outputHistory);
299
+ // Read output history
300
+ int32_t outputSize = deserializer.readInt32();
301
+ std::vector<float> outputHistory;
302
+ outputHistory.reserve(outputSize);
303
+ for (int32_t i = 0; i < outputSize; ++i)
304
+ {
305
+ outputHistory.push_back(deserializer.readFloat());
332
306
  }
307
+
308
+ // Restore filter state
309
+ filter->setState(inputHistory, outputHistory);
333
310
  }
334
311
  }
335
312
 
@@ -50,11 +50,7 @@ namespace dsp::adapters
50
50
  * @param numInputChannels Number of input channels
51
51
  */
52
52
  FilterBankStage(const std::vector<FilterDefinition> &bandDefinitions, int numInputChannels);
53
-
54
- /**
55
- * @brief Destructor - ensures proper cleanup of filters and scratch buffers
56
- */
57
- virtual ~FilterBankStage();
53
+ ~FilterBankStage() = default;
58
54
 
59
55
  const char *getType() const override { return "filterBank"; }
60
56
 
@@ -156,8 +152,9 @@ namespace dsp::adapters
156
152
  int m_numInputChannels;
157
153
  std::vector<FilterDefinition> m_definitions;
158
154
 
159
- // 2D Matrix of filters: m_filters[channelIndex][bandIndex]
160
- std::vector<std::vector<std::unique_ptr<dsp::core::IirFilter<float>>>> m_filters;
155
+ // Flattened filter array: filters[channelIndex * numBands + bandIndex]
156
+ // Using flat array instead of 2D vector to avoid nested vector cleanup issues
157
+ std::vector<std::unique_ptr<dsp::core::IirFilter<float>>> m_filters;
161
158
 
162
159
  // Optimization: Persistent scratch buffers in planar layout
163
160
  // m_planarInput[channel][sample] - de-interleaved input
@@ -0,0 +1,366 @@
1
+ #pragma once
2
+
3
+ #include "../IDspStage.h"
4
+ #include "../utils/SimdOps.h"
5
+ #include <Eigen/Dense>
6
+ #include <vector>
7
+ #include <stdexcept>
8
+ #include <cmath>
9
+ #include <string>
10
+ #include <algorithm>
11
+
12
+ namespace dsp::adapters
13
+ {
14
+ /**
15
+ * @brief Kalman Filter Stage for tracking interleaved multi-dimensional data
16
+ *
17
+ * Implements a discrete Kalman filter for position/velocity tracking with support
18
+ * for interleaved data streams (e.g., [x, y, x, y] or [lat, lon, alt, lat, lon, alt]).
19
+ *
20
+ * Perfect for:
21
+ * - GPS tracking (lat/lon pairs)
22
+ * - 3D position tracking (x/y/z coordinates)
23
+ * - Sensor fusion (accelerometer, gyroscope)
24
+ * - Any multi-dimensional time series with noise
25
+ *
26
+ * State vector: [position, velocity] for each dimension
27
+ * Measurement: position only
28
+ *
29
+ * Uses existing SIMD operations for efficient vector math on interleaved data.
30
+ */
31
+ class KalmanFilterStage : public IDspStage
32
+ {
33
+ public:
34
+ /**
35
+ * @brief Constructs a Kalman Filter Stage
36
+ * @param dimensions Number of dimensions (2 for lat/lon, 3 for x/y/z, etc.)
37
+ * @param process_noise Process noise covariance (Q) - models uncertainty in motion
38
+ * @param measurement_noise Measurement noise covariance (R) - models sensor noise
39
+ * @param initial_error Initial state estimation error covariance (P)
40
+ */
41
+ explicit KalmanFilterStage(
42
+ int dimensions = 2,
43
+ float process_noise = 1e-5f,
44
+ float measurement_noise = 1e-2f,
45
+ float initial_error = 1.0f)
46
+ : m_dimensions(dimensions),
47
+ m_process_noise(process_noise),
48
+ m_measurement_noise(measurement_noise),
49
+ m_initial_error(initial_error),
50
+ m_initialized(false),
51
+ m_first_measurement_received(false)
52
+ {
53
+ if (dimensions < 1 || dimensions > 10)
54
+ {
55
+ throw std::invalid_argument("KalmanFilter: dimensions must be between 1 and 10");
56
+ }
57
+
58
+ // State size: 2 * dimensions (position + velocity for each)
59
+ m_state_size = 2 * dimensions;
60
+ }
61
+
62
+ const char *getType() const override
63
+ {
64
+ return "kalmanFilter";
65
+ }
66
+
67
+ void process(float *buffer, size_t numSamples, int numChannels, const float *timestamps = nullptr) override
68
+ {
69
+ if (numChannels != m_dimensions)
70
+ {
71
+ throw std::invalid_argument("KalmanFilter: number of channels must match dimensions");
72
+ }
73
+
74
+ // Initialize filter on first call
75
+ if (!m_initialized)
76
+ {
77
+ initialize();
78
+ }
79
+
80
+ // Process interleaved samples: [x0, y0, x1, y1, x2, y2, ...]
81
+ // Each group of m_dimensions values is one measurement vector
82
+ const size_t samplesPerChannel = numSamples / numChannels;
83
+
84
+ for (size_t i = 0; i < samplesPerChannel; i++)
85
+ {
86
+ // Extract measurement vector from interleaved buffer
87
+ Eigen::VectorXf measurement(m_dimensions);
88
+ for (int d = 0; d < m_dimensions; d++)
89
+ {
90
+ measurement(d) = buffer[i * m_dimensions + d];
91
+ }
92
+
93
+ // Initialize state from first measurement (cold start) - only once ever
94
+ if (!m_first_measurement_received)
95
+ {
96
+ for (int d = 0; d < m_dimensions; d++)
97
+ {
98
+ m_state(d) = measurement(d); // Initialize position from first measurement
99
+ }
100
+ m_first_measurement_received = true;
101
+ }
102
+
103
+ // Predict step
104
+ predict(timestamps ? timestamps[i] : 1.0f / 360.0f); // Default 360 Hz
105
+
106
+ // Update step with measurement
107
+ update(measurement);
108
+
109
+ // Write filtered position back to buffer
110
+ for (int d = 0; d < m_dimensions; d++)
111
+ {
112
+ buffer[i * m_dimensions + d] = m_state(d); // Position component
113
+ }
114
+ }
115
+ }
116
+
117
+ Napi::Object serializeState(Napi::Env env) const override
118
+ {
119
+ Napi::Object state = Napi::Object::New(env);
120
+
121
+ state.Set("dimensions", Napi::Number::New(env, m_dimensions));
122
+ state.Set("processNoise", Napi::Number::New(env, m_process_noise));
123
+ state.Set("measurementNoise", Napi::Number::New(env, m_measurement_noise));
124
+ state.Set("initialized", Napi::Boolean::New(env, m_initialized));
125
+ state.Set("firstMeasurementReceived", Napi::Boolean::New(env, m_first_measurement_received));
126
+
127
+ if (m_initialized)
128
+ {
129
+ // Serialize state vector [pos0, pos1, ..., vel0, vel1, ...]
130
+ Napi::Array stateArray = Napi::Array::New(env, m_state.size());
131
+ for (int i = 0; i < m_state.size(); i++)
132
+ {
133
+ stateArray.Set(static_cast<uint32_t>(i), Napi::Number::New(env, m_state(i)));
134
+ }
135
+ state.Set("state", stateArray);
136
+
137
+ // Serialize covariance matrix P (flattened row-major)
138
+ Napi::Array covArray = Napi::Array::New(env, m_P.size());
139
+ int idx = 0;
140
+ for (int i = 0; i < m_P.rows(); i++)
141
+ {
142
+ for (int j = 0; j < m_P.cols(); j++)
143
+ {
144
+ covArray.Set(static_cast<uint32_t>(idx++), Napi::Number::New(env, m_P(i, j)));
145
+ }
146
+ }
147
+ state.Set("covariance", covArray);
148
+ }
149
+
150
+ return state;
151
+ }
152
+
153
+ void deserializeState(const Napi::Object &state) override
154
+ {
155
+ if (state.Has("dimensions"))
156
+ {
157
+ m_dimensions = state.Get("dimensions").As<Napi::Number>().Int32Value();
158
+ m_state_size = 2 * m_dimensions;
159
+ }
160
+
161
+ if (state.Has("processNoise"))
162
+ {
163
+ m_process_noise = state.Get("processNoise").As<Napi::Number>().FloatValue();
164
+ }
165
+
166
+ if (state.Has("measurementNoise"))
167
+ {
168
+ m_measurement_noise = state.Get("measurementNoise").As<Napi::Number>().FloatValue();
169
+ }
170
+
171
+ if (state.Has("initialized"))
172
+ {
173
+ m_initialized = state.Get("initialized").As<Napi::Boolean>().Value();
174
+ }
175
+ if (state.Has("firstMeasurementReceived"))
176
+ {
177
+ m_first_measurement_received = state.Get("firstMeasurementReceived").As<Napi::Boolean>().Value();
178
+ }
179
+
180
+ if (m_initialized && state.Has("state"))
181
+ {
182
+ Napi::Array stateArray = state.Get("state").As<Napi::Array>();
183
+ m_state.resize(stateArray.Length());
184
+ for (uint32_t i = 0; i < stateArray.Length(); i++)
185
+ {
186
+ m_state(i) = stateArray.Get(i).As<Napi::Number>().FloatValue();
187
+ }
188
+
189
+ if (state.Has("covariance"))
190
+ {
191
+ Napi::Array covArray = state.Get("covariance").As<Napi::Array>();
192
+ m_P.resize(m_state_size, m_state_size);
193
+ int idx = 0;
194
+ for (int i = 0; i < m_state_size; i++)
195
+ {
196
+ for (int j = 0; j < m_state_size; j++)
197
+ {
198
+ m_P(i, j) = covArray.Get(idx++).As<Napi::Number>().FloatValue();
199
+ }
200
+ }
201
+ }
202
+
203
+ // Reinitialize Q and R matrices (not serialized, derived from parameters)
204
+ m_Q = Eigen::MatrixXf::Identity(m_state_size, m_state_size) * m_process_noise;
205
+ m_R = Eigen::MatrixXf::Identity(m_dimensions, m_dimensions) * m_measurement_noise;
206
+ }
207
+ }
208
+
209
+ void reset() override
210
+ {
211
+ // Reset to uninitialized state - will reinitialize on next process() call
212
+ m_initialized = false;
213
+ m_first_measurement_received = false;
214
+ m_state = Eigen::VectorXf();
215
+ m_P = Eigen::MatrixXf();
216
+ m_Q = Eigen::MatrixXf();
217
+ m_R = Eigen::MatrixXf();
218
+ }
219
+
220
+ void serializeToon(toon::Serializer &serializer) const override
221
+ {
222
+ // Serialize configuration
223
+ serializer.writeInt32(m_dimensions);
224
+ serializer.writeFloat(m_process_noise);
225
+ serializer.writeFloat(m_measurement_noise);
226
+ serializer.writeFloat(m_initial_error);
227
+ serializer.writeBool(m_initialized);
228
+ serializer.writeBool(m_first_measurement_received);
229
+
230
+ if (m_initialized)
231
+ {
232
+ // Serialize state vector
233
+ serializer.writeInt32(m_state.size());
234
+ for (int i = 0; i < m_state.size(); i++)
235
+ {
236
+ serializer.writeFloat(m_state(i));
237
+ }
238
+
239
+ // Serialize covariance matrix P
240
+ serializer.writeInt32(m_P.rows());
241
+ serializer.writeInt32(m_P.cols());
242
+ for (int i = 0; i < m_P.rows(); i++)
243
+ {
244
+ for (int j = 0; j < m_P.cols(); j++)
245
+ {
246
+ serializer.writeFloat(m_P(i, j));
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+ void deserializeToon(toon::Deserializer &deserializer) override
253
+ {
254
+ // Deserialize configuration
255
+ m_dimensions = deserializer.readInt32();
256
+ m_process_noise = deserializer.readFloat();
257
+ m_measurement_noise = deserializer.readFloat();
258
+ m_initial_error = deserializer.readFloat();
259
+ m_initialized = deserializer.readBool();
260
+ m_first_measurement_received = deserializer.readBool();
261
+
262
+ m_state_size = 2 * m_dimensions;
263
+
264
+ if (m_initialized)
265
+ {
266
+ // Deserialize state vector
267
+ int stateSize = deserializer.readInt32();
268
+ m_state.resize(stateSize);
269
+ for (int i = 0; i < stateSize; i++)
270
+ {
271
+ m_state(i) = deserializer.readFloat();
272
+ }
273
+
274
+ // Deserialize covariance matrix P
275
+ int rows = deserializer.readInt32();
276
+ int cols = deserializer.readInt32();
277
+ m_P.resize(rows, cols);
278
+ for (int i = 0; i < rows; i++)
279
+ {
280
+ for (int j = 0; j < cols; j++)
281
+ {
282
+ m_P(i, j) = deserializer.readFloat();
283
+ }
284
+ }
285
+
286
+ // Reinitialize Q and R matrices based on configuration
287
+ m_Q = Eigen::MatrixXf::Identity(m_state_size, m_state_size) * m_process_noise;
288
+ m_R = Eigen::MatrixXf::Identity(m_dimensions, m_dimensions) * m_measurement_noise;
289
+ }
290
+ }
291
+
292
+ private:
293
+ void initialize()
294
+ {
295
+ // State: [pos_1, pos_2, ..., pos_n, vel_1, vel_2, ..., vel_n]
296
+ m_state = Eigen::VectorXf::Zero(m_state_size);
297
+
298
+ // Covariance matrix P (state_size x state_size)
299
+ m_P = Eigen::MatrixXf::Identity(m_state_size, m_state_size) * m_initial_error;
300
+
301
+ // Process noise covariance Q (state_size x state_size)
302
+ m_Q = Eigen::MatrixXf::Identity(m_state_size, m_state_size) * m_process_noise;
303
+
304
+ // Measurement noise covariance R (dimensions x dimensions)
305
+ m_R = Eigen::MatrixXf::Identity(m_dimensions, m_dimensions) * m_measurement_noise;
306
+
307
+ m_initialized = true;
308
+ }
309
+
310
+ void predict(float dt)
311
+ {
312
+ // State transition matrix F for constant velocity model
313
+ // F = [I dt*I]
314
+ // [0 I ]
315
+ Eigen::MatrixXf F = Eigen::MatrixXf::Identity(m_state_size, m_state_size);
316
+ for (int i = 0; i < m_dimensions; i++)
317
+ {
318
+ F(i, m_dimensions + i) = dt;
319
+ }
320
+
321
+ // Predict state: x_pred = F * x
322
+ m_state = F * m_state;
323
+
324
+ // Predict covariance: P_pred = F * P * F^T + Q
325
+ m_P = F * m_P * F.transpose() + m_Q;
326
+ }
327
+
328
+ void update(const Eigen::VectorXf &measurement)
329
+ {
330
+ // Measurement matrix H: [I 0] (only observe position, not velocity)
331
+ Eigen::MatrixXf H = Eigen::MatrixXf::Zero(m_dimensions, m_state_size);
332
+ H.block(0, 0, m_dimensions, m_dimensions) = Eigen::MatrixXf::Identity(m_dimensions, m_dimensions);
333
+
334
+ // Innovation: y = z - H * x_pred
335
+ Eigen::VectorXf innovation = measurement - H * m_state;
336
+
337
+ // Innovation covariance: S = H * P * H^T + R
338
+ Eigen::MatrixXf S = H * m_P * H.transpose() + m_R;
339
+
340
+ // Kalman gain: K = P * H^T * S^(-1)
341
+ // Use Eigen's inverse() which handles small matrices efficiently
342
+ Eigen::MatrixXf K = m_P * H.transpose() * S.inverse();
343
+
344
+ // Update state: x = x_pred + K * innovation
345
+ m_state = m_state + K * innovation;
346
+
347
+ // Update covariance: P = (I - K * H) * P
348
+ Eigen::MatrixXf I = Eigen::MatrixXf::Identity(m_state_size, m_state_size);
349
+ m_P = (I - K * H) * m_P;
350
+ }
351
+
352
+ int m_dimensions; // Number of dimensions (2 for lat/lon, 3 for xyz, etc.)
353
+ int m_state_size; // 2 * dimensions (position + velocity)
354
+ float m_process_noise; // Q - process noise covariance
355
+ float m_measurement_noise; // R - measurement noise covariance
356
+ float m_initial_error; // P0 - initial error covariance
357
+ bool m_initialized;
358
+ bool m_first_measurement_received; // Track if we've received first measurement for cold start
359
+
360
+ Eigen::VectorXf m_state; // State vector [pos, vel]
361
+ Eigen::MatrixXf m_P; // Error covariance matrix
362
+ Eigen::MatrixXf m_Q; // Process noise covariance matrix
363
+ Eigen::MatrixXf m_R; // Measurement noise covariance matrix
364
+ };
365
+
366
+ } // namespace dsp::adapters