Skip to main content

embedded_dsp/
kalman.rs

1//! Kalman filtering and state estimation algorithms for zero-allocation embedded applications.
2//!
3//! Includes convenience 1D/2D filters, a const-generic linear [`KalmanFilter`], and an
4//! [`ExtendedKalmanFilter`] driven by a user [`EkfModel`]. Measurement dimension `M` must be
5//! ≤ 16 (same limit as [`crate::matrix::mat_inverse_f32`]). Covariance updates use
6//! `P ← (I − KH) P`; a Joseph-form update may be added later for improved numerical stability.
7//!
8//! `EkfModel::f`/`h` only see the state (plus `dt` for `f`), which doesn't fit models whose
9//! process or measurement equations depend on an exogenous input that isn't part of the state
10//! (a commanded actuation, a measured current used for an IR-drop correction, etc). For that,
11//! implement the `_with_input` trait methods and drive the filter with
12//! [`ExtendedKalmanFilter::predict_with_input`] / [`ExtendedKalmanFilter::update_with_input`].
13//! Their default implementations ignore `u` and defer to `f`/`h`/the Jacobians, so existing
14//! [`EkfModel`] implementations keep compiling unchanged.
15
16use crate::matrix::{mat_inverse_f32, MatrixInstance, MatrixInstanceMut};
17use crate::types::Status;
18
19/// Scalar (1D) Kalman filter for single-variable sensor smoothing and estimation.
20#[derive(Debug, Clone, Copy)]
21pub struct KalmanFilter1D {
22    /// Estimated state
23    pub x: f32,
24    /// Estimation error covariance
25    pub p: f32,
26    /// Process noise covariance
27    pub q: f32,
28    /// Measurement noise covariance
29    pub r: f32,
30}
31
32impl KalmanFilter1D {
33    /// Initialises a 1D Kalman filter with initial estimate `x0`, initial covariance `p0`, process noise `q`, and measurement noise `r`.
34    pub fn new(x0: f32, p0: f32, q: f32, r: f32) -> Self {
35        Self { x: x0, p: p0, q, r }
36    }
37
38    /// Prediction step incorporating process input `u` (optional control input).
39    pub fn predict(&mut self, u: f32) {
40        self.x += u;
41        self.p += self.q;
42    }
43
44    /// Measurement update step with new sensor reading `z`. Returns updated state estimate.
45    pub fn update(&mut self, z: f32) -> f32 {
46        let k = self.p / (self.p + self.r);
47        self.x += k * (z - self.x);
48        self.p = (1.0 - k) * self.p;
49        self.x
50    }
51}
52
53/// 2-State (Position + Velocity) linear Kalman filter for motion tracking and sensor fusion.
54#[derive(Debug, Clone, Copy)]
55pub struct KalmanFilter2D {
56    /// State vector: `[position, velocity]`
57    pub x: [f32; 2],
58    /// 2x2 State covariance matrix (row-major: `[p00, p01, p10, p11]`)
59    pub p: [f32; 4],
60    /// Process noise variance
61    pub q_var: f32,
62    /// Measurement noise variance
63    pub r_var: f32,
64}
65
66impl KalmanFilter2D {
67    /// Initialise a 2D position/velocity Kalman filter.
68    pub fn new(initial_pos: f32, initial_vel: f32, q_var: f32, r_var: f32) -> Self {
69        Self {
70            x: [initial_pos, initial_vel],
71            p: [1.0, 0.0, 0.0, 1.0],
72            q_var,
73            r_var,
74        }
75    }
76
77    /// Predict state forward by time delta `dt`.
78    pub fn predict(&mut self, dt: f32) {
79        // State transition: x_pos = x_pos + dt * x_vel
80        self.x[0] += dt * self.x[1];
81
82        // P_new = F * P * F^T + Q
83        let dt2 = dt * dt;
84        let dt3 = dt2 * dt;
85        let dt4 = dt3 * dt;
86
87        let p00 =
88            self.p[0] + dt * (self.p[2] + self.p[1]) + dt2 * self.p[3] + 0.25 * dt4 * self.q_var;
89        let p01 = self.p[1] + dt * self.p[3] + 0.5 * dt3 * self.q_var;
90        let p10 = self.p[2] + dt * self.p[3] + 0.5 * dt3 * self.q_var;
91        let p11 = self.p[3] + dt2 * self.q_var;
92
93        self.p = [p00, p01, p10, p11];
94    }
95
96    /// Update filter with position measurement `z_pos`. Returns updated position and velocity `[pos, vel]`.
97    pub fn update(&mut self, z_pos: f32) -> [f32; 2] {
98        // Innovation
99        let y = z_pos - self.x[0];
100        let s = self.p[0] + self.r_var;
101
102        // Kalman gain K = P * H^T / S  (where H = [1, 0])
103        let k0 = self.p[0] / s;
104        let k1 = self.p[2] / s;
105
106        // State update
107        self.x[0] += k0 * y;
108        self.x[1] += k1 * y;
109
110        // Covariance update: P = (I - K * H) * P
111        let p00 = (1.0 - k0) * self.p[0];
112        let p01 = (1.0 - k0) * self.p[1];
113        let p10 = self.p[2] - k1 * self.p[0];
114        let p11 = self.p[3] - k1 * self.p[1];
115
116        self.p = [p00, p01, p10, p11];
117        self.x
118    }
119}
120
121// --- Const-generic linear Kalman & EKF helpers ---
122
123#[inline]
124fn mat_vec_mul<const R: usize, const C: usize>(
125    a: &[[f32; C]; R],
126    x: &[f32; C],
127    out: &mut [f32; R],
128) {
129    for r in 0..R {
130        let mut sum = 0.0f32;
131        for c in 0..C {
132            sum += a[r][c] * x[c];
133        }
134        out[r] = sum;
135    }
136}
137
138#[inline]
139fn mat_mul<const R: usize, const K: usize, const C: usize>(
140    a: &[[f32; K]; R],
141    b: &[[f32; C]; K],
142    out: &mut [[f32; C]; R],
143) {
144    for r in 0..R {
145        for c in 0..C {
146            let mut sum = 0.0f32;
147            for k in 0..K {
148                sum += a[r][k] * b[k][c];
149            }
150            out[r][c] = sum;
151        }
152    }
153}
154
155/// Computes `out = a * b^T` where `a` is R×K and `b` is C×K (so `b^T` is K×C).
156#[inline]
157fn mat_mul_bt<const R: usize, const K: usize, const C: usize>(
158    a: &[[f32; K]; R],
159    b: &[[f32; K]; C],
160    out: &mut [[f32; C]; R],
161) {
162    for r in 0..R {
163        for c in 0..C {
164            let mut sum = 0.0f32;
165            for k in 0..K {
166                sum += a[r][k] * b[c][k];
167            }
168            out[r][c] = sum;
169        }
170    }
171}
172
173#[inline]
174fn mat_add_inplace_nn<const N: usize>(a: &mut [[f32; N]; N], b: &[[f32; N]; N]) {
175    for r in 0..N {
176        for c in 0..N {
177            a[r][c] += b[r][c];
178        }
179    }
180}
181
182#[inline]
183fn mat_add_inplace_mm<const M: usize>(a: &mut [[f32; M]; M], b: &[[f32; M]; M]) {
184    for r in 0..M {
185        for c in 0..M {
186            a[r][c] += b[r][c];
187        }
188    }
189}
190
191#[inline]
192fn identity_n<const N: usize>() -> [[f32; N]; N] {
193    let mut i = [[0.0f32; N]; N];
194    for n in 0..N {
195        i[n][n] = 1.0;
196    }
197    i
198}
199
200/// Invert an `M×M` matrix using [`mat_inverse_f32`]. Requires `M ≤ 16`.
201fn invert_mxm<const M: usize>(s: &[[f32; M]; M], s_inv: &mut [[f32; M]; M]) -> Status {
202    if M == 0 {
203        return Status::SizeMismatch;
204    }
205    if M > 16 {
206        return Status::ArgumentError;
207    }
208
209    let mut flat_src = [0.0f32; 16 * 16];
210    let mut flat_dst = [0.0f32; 16 * 16];
211    for r in 0..M {
212        for c in 0..M {
213            flat_src[r * M + c] = s[r][c];
214        }
215    }
216
217    let src = MatrixInstance::new(M as u16, M as u16, &flat_src[..M * M]);
218    let mut dst = MatrixInstanceMut::new(M as u16, M as u16, &mut flat_dst[..M * M]);
219    let status = mat_inverse_f32(&src, &mut dst);
220    if status != Status::Success {
221        return status;
222    }
223
224    for r in 0..M {
225        for c in 0..M {
226            s_inv[r][c] = flat_dst[r * M + c];
227        }
228    }
229    Status::Success
230}
231
232/// Predict: `x ← F x`, `P ← F P Fᵀ + Q`.
233fn kf_predict_core<const N: usize>(
234    x: &mut [f32; N],
235    p: &mut [[f32; N]; N],
236    q: &[[f32; N]; N],
237    f: &[[f32; N]; N],
238) {
239    let mut x_new = [0.0f32; N];
240    mat_vec_mul(f, x, &mut x_new);
241    *x = x_new;
242
243    let mut fp = [[0.0f32; N]; N];
244    mat_mul(f, p, &mut fp);
245    let mut p_new = [[0.0f32; N]; N];
246    mat_mul_bt(&fp, f, &mut p_new);
247    mat_add_inplace_nn(&mut p_new, q);
248    *p = p_new;
249}
250
251/// Predict with control: `x ← F x + B u`, then same `P` update.
252fn kf_predict_control_core<const N: usize, const U: usize>(
253    x: &mut [f32; N],
254    p: &mut [[f32; N]; N],
255    q: &[[f32; N]; N],
256    f: &[[f32; N]; N],
257    b: &[[f32; U]; N],
258    u: &[f32; U],
259) {
260    let mut x_new = [0.0f32; N];
261    mat_vec_mul(f, x, &mut x_new);
262    let mut bu = [0.0f32; N];
263    mat_vec_mul(b, u, &mut bu);
264    for i in 0..N {
265        x_new[i] += bu[i];
266    }
267    *x = x_new;
268
269    let mut fp = [[0.0f32; N]; N];
270    mat_mul(f, p, &mut fp);
271    let mut p_new = [[0.0f32; N]; N];
272    mat_mul_bt(&fp, f, &mut p_new);
273    mat_add_inplace_nn(&mut p_new, q);
274    *p = p_new;
275}
276
277/// Measurement update with linear `H`. Leaves state unchanged on singular `S`.
278fn kf_update_core<const N: usize, const M: usize>(
279    x: &mut [f32; N],
280    p: &mut [[f32; N]; N],
281    r: &[[f32; M]; M],
282    h: &[[f32; N]; M],
283    z: &[f32; M],
284) -> Status {
285    if M > 16 {
286        return Status::ArgumentError;
287    }
288    if M == 0 {
289        return Status::SizeMismatch;
290    }
291
292    // y = z - H x
293    let mut hx = [0.0f32; M];
294    mat_vec_mul(h, x, &mut hx);
295    let mut y = [0.0f32; M];
296    for i in 0..M {
297        y[i] = z[i] - hx[i];
298    }
299
300    // S = H P Hᵀ + R
301    let mut hp = [[0.0f32; N]; M];
302    mat_mul(h, p, &mut hp);
303    let mut s = [[0.0f32; M]; M];
304    mat_mul_bt(&hp, h, &mut s);
305    mat_add_inplace_mm(&mut s, r);
306
307    let mut s_inv = [[0.0f32; M]; M];
308    let inv_status = invert_mxm(&s, &mut s_inv);
309    if inv_status != Status::Success {
310        return inv_status;
311    }
312
313    // P Hᵀ (N×M): rows of P times columns of Hᵀ (= rows of H)
314    let mut pht = [[0.0f32; M]; N];
315    for i in 0..N {
316        for j in 0..M {
317            let mut sum = 0.0f32;
318            for k in 0..N {
319                sum += p[i][k] * h[j][k];
320            }
321            pht[i][j] = sum;
322        }
323    }
324
325    // K = (P Hᵀ) S⁻¹  (N×M)
326    let mut k = [[0.0f32; M]; N];
327    mat_mul(&pht, &s_inv, &mut k);
328
329    // x ← x + K y
330    let mut ky = [0.0f32; N];
331    mat_vec_mul(&k, &y, &mut ky);
332    let mut x_new = *x;
333    for i in 0..N {
334        x_new[i] += ky[i];
335    }
336
337    // P ← (I - K H) P
338    let mut kh = [[0.0f32; N]; N];
339    mat_mul(&k, h, &mut kh);
340    let mut i_kh = identity_n::<N>();
341    for r in 0..N {
342        for c in 0..N {
343            i_kh[r][c] -= kh[r][c];
344        }
345    }
346    let mut p_new = [[0.0f32; N]; N];
347    mat_mul(&i_kh, p, &mut p_new);
348
349    *x = x_new;
350    *p = p_new;
351    Status::Success
352}
353
354/// Const-generic linear Kalman filter: `x' = F x (+ B u) + w`, `z = H x + v`.
355///
356/// Measurement dimension `M` must be ≤ 16 so the innovation covariance can be inverted
357/// with the crate's stack-limited matrix inverse.
358#[derive(Debug, Clone, Copy, PartialEq)]
359pub struct KalmanFilter<const N: usize, const M: usize> {
360    /// State estimate
361    pub x: [f32; N],
362    /// State covariance `P` (`N×N`)
363    pub p: [[f32; N]; N],
364    /// Process noise covariance `Q` (`N×N`)
365    pub q: [[f32; N]; N],
366    /// Measurement noise covariance `R` (`M×M`)
367    pub r: [[f32; M]; M],
368}
369
370impl<const N: usize, const M: usize> KalmanFilter<N, M> {
371    /// Create a filter with initial state `x0`, covariance `p0`, and noise covariances `q` / `r`.
372    pub fn new(x0: [f32; N], p0: [[f32; N]; N], q: [[f32; N]; N], r: [[f32; M]; M]) -> Self {
373        Self { x: x0, p: p0, q, r }
374    }
375
376    /// Create a filter with diagonal `P`, `Q`, and `R` initialized from scalar variances.
377    pub fn from_variances(x0: [f32; N], p_var: f32, q_var: f32, r_var: f32) -> Self {
378        let mut p = [[0.0f32; N]; N];
379        let mut q = [[0.0f32; N]; N];
380        let mut r = [[0.0f32; M]; M];
381        for i in 0..N {
382            p[i][i] = p_var;
383            q[i][i] = q_var;
384        }
385        for i in 0..M {
386            r[i][i] = r_var;
387        }
388        Self::new(x0, p, q, r)
389    }
390
391    /// Prediction without control input: `x ← F x`, `P ← F P Fᵀ + Q`.
392    pub fn predict(&mut self, f: &[[f32; N]; N]) {
393        kf_predict_core(&mut self.x, &mut self.p, &self.q, f);
394    }
395
396    /// Prediction with control: `x ← F x + B u`, `P ← F P Fᵀ + Q`.
397    pub fn predict_with_control<const U: usize>(
398        &mut self,
399        f: &[[f32; N]; N],
400        b: &[[f32; U]; N],
401        u: &[f32; U],
402    ) {
403        kf_predict_control_core(&mut self.x, &mut self.p, &self.q, f, b, u);
404    }
405
406    /// Measurement update with observation matrix `H` (`M×N`) and measurement `z`.
407    ///
408    /// On success returns [`Status::Success`] and updates `x` / `P`. If `S` is singular or
409    /// `M > 16`, returns an error status and leaves the filter state unchanged.
410    pub fn update(&mut self, h: &[[f32; N]; M], z: &[f32; M]) -> Status {
411        kf_update_core(&mut self.x, &mut self.p, &self.r, h, z)
412    }
413}
414
415/// User-supplied nonlinear process and measurement model for an EKF (static dispatch).
416pub trait EkfModel<const N: usize, const M: usize> {
417    /// Process model: `out = f(x, dt)`.
418    fn f(&self, x: &[f32; N], dt: f32, out: &mut [f32; N]);
419
420    /// Measurement model: `out = h(x)`.
421    fn h(&self, x: &[f32; N], out: &mut [f32; M]);
422
423    /// Process Jacobian `F = ∂f/∂x` evaluated at `x`.
424    fn jacobian_f(&self, x: &[f32; N], dt: f32, out: &mut [[f32; N]; N]);
425
426    /// Measurement Jacobian `H = ∂h/∂x` evaluated at `x` (`M×N`).
427    fn jacobian_h(&self, x: &[f32; N], out: &mut [[f32; N]; M]);
428
429    /// Process model with an explicit exogenous input `u` (a control input,
430    /// measured disturbance, or anything else that drives `f` but isn't
431    /// part of the state): `out = f(x, u, dt)`.
432    ///
433    /// Default: ignores `u` and defers to [`EkfModel::f`], so models that
434    /// don't need an input compile unchanged.
435    fn f_with_input<const U: usize>(
436        &self,
437        x: &[f32; N],
438        u: &[f32; U],
439        dt: f32,
440        out: &mut [f32; N],
441    ) {
442        let _ = u;
443        self.f(x, dt, out)
444    }
445
446    /// Process Jacobian for [`EkfModel::f_with_input`], `F = ∂f/∂x` evaluated at `(x, u)`.
447    ///
448    /// Default: defers to [`EkfModel::jacobian_f`], which is exact whenever `u` enters `f`
449    /// affinely (so it doesn't change the derivative with respect to `x`).
450    fn jacobian_f_with_input<const U: usize>(
451        &self,
452        x: &[f32; N],
453        u: &[f32; U],
454        dt: f32,
455        out: &mut [[f32; N]; N],
456    ) {
457        let _ = u;
458        self.jacobian_f(x, dt, out)
459    }
460
461    /// Measurement model with an explicit exogenous input `u` (e.g. a measured current used
462    /// for an IR-drop correction that isn't part of the state): `out = h(x, u)`.
463    ///
464    /// Default: ignores `u` and defers to [`EkfModel::h`].
465    fn h_with_input<const U: usize>(&self, x: &[f32; N], u: &[f32; U], out: &mut [f32; M]) {
466        let _ = u;
467        self.h(x, out)
468    }
469
470    /// Measurement Jacobian for [`EkfModel::h_with_input`], `H = ∂h/∂x` evaluated at `(x, u)`.
471    ///
472    /// Default: defers to [`EkfModel::jacobian_h`], which is exact whenever `u` enters `h`
473    /// affinely.
474    fn jacobian_h_with_input<const U: usize>(
475        &self,
476        x: &[f32; N],
477        u: &[f32; U],
478        out: &mut [[f32; N]; M],
479    ) {
480        let _ = u;
481        self.jacobian_h(x, out)
482    }
483}
484
485/// Extended Kalman filter with compile-time dimensions and a user [`EkfModel`].
486///
487/// Measurement dimension `M` must be ≤ 16. Covariance update uses `P ← (I − KH) P`.
488#[derive(Debug, Clone, Copy, PartialEq)]
489pub struct ExtendedKalmanFilter<const N: usize, const M: usize, Model> {
490    /// State estimate
491    pub x: [f32; N],
492    /// State covariance `P` (`N×N`)
493    pub p: [[f32; N]; N],
494    /// Process noise covariance `Q` (`N×N`)
495    pub q: [[f32; N]; N],
496    /// Measurement noise covariance `R` (`M×M`)
497    pub r: [[f32; M]; M],
498    /// Nonlinear process / measurement model
499    pub model: Model,
500}
501
502impl<const N: usize, const M: usize, Model: EkfModel<N, M>> ExtendedKalmanFilter<N, M, Model> {
503    /// Create an EKF with initial state, covariances, and model.
504    pub fn new(
505        x0: [f32; N],
506        p0: [[f32; N]; N],
507        q: [[f32; N]; N],
508        r: [[f32; M]; M],
509        model: Model,
510    ) -> Self {
511        Self {
512            x: x0,
513            p: p0,
514            q,
515            r,
516            model,
517        }
518    }
519
520    /// Create an EKF with diagonal covariances from scalar variances.
521    pub fn from_variances(x0: [f32; N], p_var: f32, q_var: f32, r_var: f32, model: Model) -> Self {
522        let mut p = [[0.0f32; N]; N];
523        let mut q = [[0.0f32; N]; N];
524        let mut r = [[0.0f32; M]; M];
525        for i in 0..N {
526            p[i][i] = p_var;
527            q[i][i] = q_var;
528        }
529        for i in 0..M {
530            r[i][i] = r_var;
531        }
532        Self::new(x0, p, q, r, model)
533    }
534
535    /// EKF predict: `x ← f(x, dt)`, `P ← F P Fᵀ + Q` with `F = ∂f/∂x`.
536    pub fn predict(&mut self, dt: f32) {
537        let mut f_jac = [[0.0f32; N]; N];
538        self.model.jacobian_f(&self.x, dt, &mut f_jac);
539
540        let mut x_new = [0.0f32; N];
541        self.model.f(&self.x, dt, &mut x_new);
542
543        ekf_predict_apply(&mut self.x, &mut self.p, &self.q, &f_jac, x_new);
544    }
545
546    /// EKF predict with an exogenous input `u`, via [`EkfModel::f_with_input`] /
547    /// [`EkfModel::jacobian_f_with_input`]. See the [module docs](self) for when this is
548    /// needed instead of [`ExtendedKalmanFilter::predict`].
549    pub fn predict_with_input<const U: usize>(&mut self, dt: f32, u: &[f32; U]) {
550        let mut f_jac = [[0.0f32; N]; N];
551        self.model.jacobian_f_with_input(&self.x, u, dt, &mut f_jac);
552
553        let mut x_new = [0.0f32; N];
554        self.model.f_with_input(&self.x, u, dt, &mut x_new);
555
556        ekf_predict_apply(&mut self.x, &mut self.p, &self.q, &f_jac, x_new);
557    }
558
559    /// EKF update with measurement `z`. Linearizes `h` at the current estimate.
560    ///
561    /// On singular innovation covariance or `M > 16`, returns an error and leaves state unchanged.
562    pub fn update(&mut self, z: &[f32; M]) -> Status {
563        let mut h_jac = [[0.0f32; N]; M];
564        self.model.jacobian_h(&self.x, &mut h_jac);
565
566        let mut hx = [0.0f32; M];
567        self.model.h(&self.x, &mut hx);
568
569        ekf_update_apply(&mut self.x, &mut self.p, &self.r, &h_jac, &hx, z)
570    }
571
572    /// EKF update with an exogenous input `u`, via [`EkfModel::h_with_input`] /
573    /// [`EkfModel::jacobian_h_with_input`]. See the [module docs](self) for when this is
574    /// needed instead of [`ExtendedKalmanFilter::update`].
575    ///
576    /// On singular innovation covariance or `M > 16`, returns an error and leaves state unchanged.
577    pub fn update_with_input<const U: usize>(&mut self, z: &[f32; M], u: &[f32; U]) -> Status {
578        let mut h_jac = [[0.0f32; N]; M];
579        self.model.jacobian_h_with_input(&self.x, u, &mut h_jac);
580
581        let mut hx = [0.0f32; M];
582        self.model.h_with_input(&self.x, u, &mut hx);
583
584        ekf_update_apply(&mut self.x, &mut self.p, &self.r, &h_jac, &hx, z)
585    }
586}
587
588/// Shared EKF predict math: `x ← x_new`, `P ← F P Fᵀ + Q`. Factored out so
589/// [`ExtendedKalmanFilter::predict`] and [`ExtendedKalmanFilter::predict_with_input`] (which
590/// differ only in how `x_new`/`f_jac` are computed) don't duplicate the covariance propagation.
591fn ekf_predict_apply<const N: usize>(
592    x: &mut [f32; N],
593    p: &mut [[f32; N]; N],
594    q: &[[f32; N]; N],
595    f_jac: &[[f32; N]; N],
596    x_new: [f32; N],
597) {
598    *x = x_new;
599
600    let mut fp = [[0.0f32; N]; N];
601    mat_mul(f_jac, p, &mut fp);
602    let mut p_new = [[0.0f32; N]; N];
603    mat_mul_bt(&fp, f_jac, &mut p_new);
604    mat_add_inplace_nn(&mut p_new, q);
605    *p = p_new;
606}
607
608/// Shared EKF update math: linearizes around `hx = h(x)` and reuses the linear-filter update
609/// core. Factored out so [`ExtendedKalmanFilter::update`] and
610/// [`ExtendedKalmanFilter::update_with_input`] (which differ only in how `hx`/`h_jac` are
611/// computed) don't duplicate the linearization.
612fn ekf_update_apply<const N: usize, const M: usize>(
613    x: &mut [f32; N],
614    p: &mut [[f32; N]; N],
615    r: &[[f32; M]; M],
616    h_jac: &[[f32; N]; M],
617    hx: &[f32; M],
618    z: &[f32; M],
619) -> Status {
620    if M > 16 {
621        return Status::ArgumentError;
622    }
623    if M == 0 {
624        return Status::SizeMismatch;
625    }
626
627    // Reuse linear update with innovation z' = z - h(x) + H x so that
628    // y = z' - H x = z - h(x).
629    let mut z_equiv = [0.0f32; M];
630    let mut hx_lin = [0.0f32; M];
631    mat_vec_mul(h_jac, x, &mut hx_lin);
632    for i in 0..M {
633        z_equiv[i] = z[i] - hx[i] + hx_lin[i];
634    }
635
636    kf_update_core(x, p, r, h_jac, &z_equiv)
637}