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
16#[allow(unused_imports)]
17use crate::math::FloatMath;
18use crate::matrix::{MatrixInstance, MatrixInstanceMut, mat_inverse_f32};
19use crate::types::Status;
20
21/// Scalar (1D) Kalman filter for single-variable sensor smoothing and estimation.
22#[derive(Debug, Clone, Copy)]
23pub struct KalmanFilter1D {
24    /// Estimated state
25    pub x: f32,
26    /// Estimation error covariance
27    pub p: f32,
28    /// Process noise covariance
29    pub q: f32,
30    /// Measurement noise covariance
31    pub r: f32,
32}
33
34impl KalmanFilter1D {
35    /// Initialises a 1D Kalman filter with initial estimate `x0`, initial covariance `p0`, process noise `q`, and measurement noise `r`.
36    pub fn new(x0: f32, p0: f32, q: f32, r: f32) -> Self {
37        Self { x: x0, p: p0, q, r }
38    }
39
40    /// Prediction step incorporating process input `u` (optional control input).
41    pub fn predict(&mut self, u: f32) {
42        self.x += u;
43        self.p += self.q;
44    }
45
46    /// Measurement update step with new sensor reading `z`. Returns updated state estimate.
47    pub fn update(&mut self, z: f32) -> f32 {
48        let k = self.p / (self.p + self.r);
49        self.x += k * (z - self.x);
50        self.p = (1.0 - k) * self.p;
51        self.x
52    }
53}
54
55/// 2-State (Position + Velocity) linear Kalman filter for motion tracking and sensor fusion.
56#[derive(Debug, Clone, Copy)]
57pub struct KalmanFilter2D {
58    /// State vector: `[position, velocity]`
59    pub x: [f32; 2],
60    /// 2x2 State covariance matrix (row-major: `[p00, p01, p10, p11]`)
61    pub p: [f32; 4],
62    /// Process noise variance
63    pub q_var: f32,
64    /// Measurement noise variance
65    pub r_var: f32,
66}
67
68impl KalmanFilter2D {
69    /// Initialise a 2D position/velocity Kalman filter.
70    pub fn new(initial_pos: f32, initial_vel: f32, q_var: f32, r_var: f32) -> Self {
71        Self {
72            x: [initial_pos, initial_vel],
73            p: [1.0, 0.0, 0.0, 1.0],
74            q_var,
75            r_var,
76        }
77    }
78
79    /// Predict state forward by time delta `dt`.
80    pub fn predict(&mut self, dt: f32) {
81        // State transition: x_pos = x_pos + dt * x_vel
82        self.x[0] += dt * self.x[1];
83
84        // P_new = F * P * F^T + Q
85        let dt2 = dt * dt;
86        let dt3 = dt2 * dt;
87        let dt4 = dt3 * dt;
88
89        let p00 =
90            self.p[0] + dt * (self.p[2] + self.p[1]) + dt2 * self.p[3] + 0.25 * dt4 * self.q_var;
91        let p01 = self.p[1] + dt * self.p[3] + 0.5 * dt3 * self.q_var;
92        let p10 = self.p[2] + dt * self.p[3] + 0.5 * dt3 * self.q_var;
93        let p11 = self.p[3] + dt2 * self.q_var;
94
95        self.p = [p00, p01, p10, p11];
96    }
97
98    /// Update filter with position measurement `z_pos`. Returns updated position and velocity `[pos, vel]`.
99    pub fn update(&mut self, z_pos: f32) -> [f32; 2] {
100        // Innovation
101        let y = z_pos - self.x[0];
102        let s = self.p[0] + self.r_var;
103
104        // Kalman gain K = P * H^T / S  (where H = [1, 0])
105        let k0 = self.p[0] / s;
106        let k1 = self.p[2] / s;
107
108        // State update
109        self.x[0] += k0 * y;
110        self.x[1] += k1 * y;
111
112        // Covariance update: P = (I - K * H) * P
113        let p00 = (1.0 - k0) * self.p[0];
114        let p01 = (1.0 - k0) * self.p[1];
115        let p10 = self.p[2] - k1 * self.p[0];
116        let p11 = self.p[3] - k1 * self.p[1];
117
118        self.p = [p00, p01, p10, p11];
119        self.x
120    }
121}
122
123// --- Const-generic linear Kalman & EKF helpers ---
124
125#[inline]
126fn mat_vec_mul<const R: usize, const C: usize>(
127    a: &[[f32; C]; R],
128    x: &[f32; C],
129    out: &mut [f32; R],
130) {
131    for r in 0..R {
132        let mut sum = 0.0f32;
133        for c in 0..C {
134            sum += a[r][c] * x[c];
135        }
136        out[r] = sum;
137    }
138}
139
140#[inline]
141fn mat_mul<const R: usize, const K: usize, const C: usize>(
142    a: &[[f32; K]; R],
143    b: &[[f32; C]; K],
144    out: &mut [[f32; C]; R],
145) {
146    for r in 0..R {
147        for c in 0..C {
148            let mut sum = 0.0f32;
149            for k in 0..K {
150                sum += a[r][k] * b[k][c];
151            }
152            out[r][c] = sum;
153        }
154    }
155}
156
157/// Computes `out = a * b^T` where `a` is R×K and `b` is C×K (so `b^T` is K×C).
158#[inline]
159fn mat_mul_bt<const R: usize, const K: usize, const C: usize>(
160    a: &[[f32; K]; R],
161    b: &[[f32; K]; C],
162    out: &mut [[f32; C]; R],
163) {
164    for r in 0..R {
165        for c in 0..C {
166            let mut sum = 0.0f32;
167            for k in 0..K {
168                sum += a[r][k] * b[c][k];
169            }
170            out[r][c] = sum;
171        }
172    }
173}
174
175#[inline]
176fn mat_add_inplace_nn<const N: usize>(a: &mut [[f32; N]; N], b: &[[f32; N]; N]) {
177    for r in 0..N {
178        for c in 0..N {
179            a[r][c] += b[r][c];
180        }
181    }
182}
183
184#[inline]
185fn mat_add_inplace_mm<const M: usize>(a: &mut [[f32; M]; M], b: &[[f32; M]; M]) {
186    for r in 0..M {
187        for c in 0..M {
188            a[r][c] += b[r][c];
189        }
190    }
191}
192
193#[inline]
194fn identity_n<const N: usize>() -> [[f32; N]; N] {
195    let mut i = [[0.0f32; N]; N];
196    for n in 0..N {
197        i[n][n] = 1.0;
198    }
199    i
200}
201
202/// Invert an `M×M` matrix using [`mat_inverse_f32`]. Requires `M ≤ 16`.
203fn invert_mxm<const M: usize>(s: &[[f32; M]; M], s_inv: &mut [[f32; M]; M]) -> Status {
204    if M == 0 {
205        return Status::SizeMismatch;
206    }
207    if M > 16 {
208        return Status::ArgumentError;
209    }
210
211    let mut flat_src = [0.0f32; 16 * 16];
212    let mut flat_dst = [0.0f32; 16 * 16];
213    for r in 0..M {
214        for c in 0..M {
215            flat_src[r * M + c] = s[r][c];
216        }
217    }
218
219    let src = MatrixInstance::new(M as u16, M as u16, &flat_src[..M * M]);
220    let mut dst = MatrixInstanceMut::new(M as u16, M as u16, &mut flat_dst[..M * M]);
221    let status = mat_inverse_f32(&src, &mut dst);
222    if status != Status::Success {
223        return status;
224    }
225
226    for r in 0..M {
227        for c in 0..M {
228            s_inv[r][c] = flat_dst[r * M + c];
229        }
230    }
231    Status::Success
232}
233
234/// Predict: `x ← F x`, `P ← F P Fᵀ + Q`.
235fn kf_predict_core<const N: usize>(
236    x: &mut [f32; N],
237    p: &mut [[f32; N]; N],
238    q: &[[f32; N]; N],
239    f: &[[f32; N]; N],
240) {
241    let mut x_new = [0.0f32; N];
242    mat_vec_mul(f, x, &mut x_new);
243    *x = x_new;
244
245    let mut fp = [[0.0f32; N]; N];
246    mat_mul(f, p, &mut fp);
247    let mut p_new = [[0.0f32; N]; N];
248    mat_mul_bt(&fp, f, &mut p_new);
249    mat_add_inplace_nn(&mut p_new, q);
250    *p = p_new;
251}
252
253/// Predict with control: `x ← F x + B u`, then same `P` update.
254fn kf_predict_control_core<const N: usize, const U: usize>(
255    x: &mut [f32; N],
256    p: &mut [[f32; N]; N],
257    q: &[[f32; N]; N],
258    f: &[[f32; N]; N],
259    b: &[[f32; U]; N],
260    u: &[f32; U],
261) {
262    let mut x_new = [0.0f32; N];
263    mat_vec_mul(f, x, &mut x_new);
264    let mut bu = [0.0f32; N];
265    mat_vec_mul(b, u, &mut bu);
266    for i in 0..N {
267        x_new[i] += bu[i];
268    }
269    *x = x_new;
270
271    let mut fp = [[0.0f32; N]; N];
272    mat_mul(f, p, &mut fp);
273    let mut p_new = [[0.0f32; N]; N];
274    mat_mul_bt(&fp, f, &mut p_new);
275    mat_add_inplace_nn(&mut p_new, q);
276    *p = p_new;
277}
278
279/// Measurement update with linear `H`. Leaves state unchanged on singular `S`.
280fn kf_update_core<const N: usize, const M: usize>(
281    x: &mut [f32; N],
282    p: &mut [[f32; N]; N],
283    r: &[[f32; M]; M],
284    h: &[[f32; N]; M],
285    z: &[f32; M],
286) -> Status {
287    if M > 16 {
288        return Status::ArgumentError;
289    }
290    if M == 0 {
291        return Status::SizeMismatch;
292    }
293
294    // y = z - H x
295    let mut hx = [0.0f32; M];
296    mat_vec_mul(h, x, &mut hx);
297    let mut y = [0.0f32; M];
298    for i in 0..M {
299        y[i] = z[i] - hx[i];
300    }
301
302    // S = H P Hᵀ + R
303    let mut hp = [[0.0f32; N]; M];
304    mat_mul(h, p, &mut hp);
305    let mut s = [[0.0f32; M]; M];
306    mat_mul_bt(&hp, h, &mut s);
307    mat_add_inplace_mm(&mut s, r);
308
309    let mut s_inv = [[0.0f32; M]; M];
310    let inv_status = invert_mxm(&s, &mut s_inv);
311    if inv_status != Status::Success {
312        return inv_status;
313    }
314
315    // P Hᵀ (N×M): rows of P times columns of Hᵀ (= rows of H)
316    let mut pht = [[0.0f32; M]; N];
317    for i in 0..N {
318        for j in 0..M {
319            let mut sum = 0.0f32;
320            for k in 0..N {
321                sum += p[i][k] * h[j][k];
322            }
323            pht[i][j] = sum;
324        }
325    }
326
327    // K = (P Hᵀ) S⁻¹  (N×M)
328    let mut k = [[0.0f32; M]; N];
329    mat_mul(&pht, &s_inv, &mut k);
330
331    // x ← x + K y
332    let mut ky = [0.0f32; N];
333    mat_vec_mul(&k, &y, &mut ky);
334    let mut x_new = *x;
335    for i in 0..N {
336        x_new[i] += ky[i];
337    }
338
339    // P ← (I - K H) P
340    let mut kh = [[0.0f32; N]; N];
341    mat_mul(&k, h, &mut kh);
342    let mut i_kh = identity_n::<N>();
343    for r in 0..N {
344        for c in 0..N {
345            i_kh[r][c] -= kh[r][c];
346        }
347    }
348    let mut p_new = [[0.0f32; N]; N];
349    mat_mul(&i_kh, p, &mut p_new);
350
351    *x = x_new;
352    *p = p_new;
353    Status::Success
354}
355
356/// Const-generic linear Kalman filter: `x' = F x (+ B u) + w`, `z = H x + v`.
357///
358/// Measurement dimension `M` must be ≤ 16 so the innovation covariance can be inverted
359/// with the crate's stack-limited matrix inverse.
360#[derive(Debug, Clone, Copy, PartialEq)]
361pub struct KalmanFilter<const N: usize, const M: usize> {
362    /// State estimate
363    pub x: [f32; N],
364    /// State covariance `P` (`N×N`)
365    pub p: [[f32; N]; N],
366    /// Process noise covariance `Q` (`N×N`)
367    pub q: [[f32; N]; N],
368    /// Measurement noise covariance `R` (`M×M`)
369    pub r: [[f32; M]; M],
370}
371
372impl<const N: usize, const M: usize> KalmanFilter<N, M> {
373    /// Create a filter with initial state `x0`, covariance `p0`, and noise covariances `q` / `r`.
374    pub fn new(x0: [f32; N], p0: [[f32; N]; N], q: [[f32; N]; N], r: [[f32; M]; M]) -> Self {
375        Self { x: x0, p: p0, q, r }
376    }
377
378    /// Create a filter with diagonal `P`, `Q`, and `R` initialized from scalar variances.
379    pub fn from_variances(x0: [f32; N], p_var: f32, q_var: f32, r_var: f32) -> Self {
380        let mut p = [[0.0f32; N]; N];
381        let mut q = [[0.0f32; N]; N];
382        let mut r = [[0.0f32; M]; M];
383        for i in 0..N {
384            p[i][i] = p_var;
385            q[i][i] = q_var;
386        }
387        for i in 0..M {
388            r[i][i] = r_var;
389        }
390        Self::new(x0, p, q, r)
391    }
392
393    /// Prediction without control input: `x ← F x`, `P ← F P Fᵀ + Q`.
394    pub fn predict(&mut self, f: &[[f32; N]; N]) {
395        kf_predict_core(&mut self.x, &mut self.p, &self.q, f);
396    }
397
398    /// Prediction with control: `x ← F x + B u`, `P ← F P Fᵀ + Q`.
399    pub fn predict_with_control<const U: usize>(
400        &mut self,
401        f: &[[f32; N]; N],
402        b: &[[f32; U]; N],
403        u: &[f32; U],
404    ) {
405        kf_predict_control_core(&mut self.x, &mut self.p, &self.q, f, b, u);
406    }
407
408    /// Measurement update with observation matrix `H` (`M×N`) and measurement `z`.
409    ///
410    /// On success returns [`Status::Success`] and updates `x` / `P`. If `S` is singular or
411    /// `M > 16`, returns an error status and leaves the filter state unchanged.
412    pub fn update(&mut self, h: &[[f32; N]; M], z: &[f32; M]) -> Status {
413        kf_update_core(&mut self.x, &mut self.p, &self.r, h, z)
414    }
415}
416
417/// User-supplied nonlinear process and measurement model for an EKF (static dispatch).
418pub trait EkfModel<const N: usize, const M: usize> {
419    /// Process model: `out = f(x, dt)`.
420    fn f(&self, x: &[f32; N], dt: f32, out: &mut [f32; N]);
421
422    /// Measurement model: `out = h(x)`.
423    fn h(&self, x: &[f32; N], out: &mut [f32; M]);
424
425    /// Process Jacobian `F = ∂f/∂x` evaluated at `x`.
426    fn jacobian_f(&self, x: &[f32; N], dt: f32, out: &mut [[f32; N]; N]);
427
428    /// Measurement Jacobian `H = ∂h/∂x` evaluated at `x` (`M×N`).
429    fn jacobian_h(&self, x: &[f32; N], out: &mut [[f32; N]; M]);
430
431    /// Process model with an explicit exogenous input `u` (a control input,
432    /// measured disturbance, or anything else that drives `f` but isn't
433    /// part of the state): `out = f(x, u, dt)`.
434    ///
435    /// Default: ignores `u` and defers to [`EkfModel::f`], so models that
436    /// don't need an input compile unchanged.
437    fn f_with_input<const U: usize>(
438        &self,
439        x: &[f32; N],
440        u: &[f32; U],
441        dt: f32,
442        out: &mut [f32; N],
443    ) {
444        let _ = u;
445        self.f(x, dt, out)
446    }
447
448    /// Process Jacobian for [`EkfModel::f_with_input`], `F = ∂f/∂x` evaluated at `(x, u)`.
449    ///
450    /// Default: defers to [`EkfModel::jacobian_f`], which is exact whenever `u` enters `f`
451    /// affinely (so it doesn't change the derivative with respect to `x`).
452    fn jacobian_f_with_input<const U: usize>(
453        &self,
454        x: &[f32; N],
455        u: &[f32; U],
456        dt: f32,
457        out: &mut [[f32; N]; N],
458    ) {
459        let _ = u;
460        self.jacobian_f(x, dt, out)
461    }
462
463    /// Measurement model with an explicit exogenous input `u` (e.g. a measured current used
464    /// for an IR-drop correction that isn't part of the state): `out = h(x, u)`.
465    ///
466    /// Default: ignores `u` and defers to [`EkfModel::h`].
467    fn h_with_input<const U: usize>(&self, x: &[f32; N], u: &[f32; U], out: &mut [f32; M]) {
468        let _ = u;
469        self.h(x, out)
470    }
471
472    /// Measurement Jacobian for [`EkfModel::h_with_input`], `H = ∂h/∂x` evaluated at `(x, u)`.
473    ///
474    /// Default: defers to [`EkfModel::jacobian_h`], which is exact whenever `u` enters `h`
475    /// affinely.
476    fn jacobian_h_with_input<const U: usize>(
477        &self,
478        x: &[f32; N],
479        u: &[f32; U],
480        out: &mut [[f32; N]; M],
481    ) {
482        let _ = u;
483        self.jacobian_h(x, out)
484    }
485}
486
487/// Extended Kalman filter with compile-time dimensions and a user [`EkfModel`].
488///
489/// Measurement dimension `M` must be ≤ 16. Covariance update uses `P ← (I − KH) P`.
490#[derive(Debug, Clone, Copy, PartialEq)]
491pub struct ExtendedKalmanFilter<const N: usize, const M: usize, Model> {
492    /// State estimate
493    pub x: [f32; N],
494    /// State covariance `P` (`N×N`)
495    pub p: [[f32; N]; N],
496    /// Process noise covariance `Q` (`N×N`)
497    pub q: [[f32; N]; N],
498    /// Measurement noise covariance `R` (`M×M`)
499    pub r: [[f32; M]; M],
500    /// Nonlinear process / measurement model
501    pub model: Model,
502}
503
504impl<const N: usize, const M: usize, Model: EkfModel<N, M>> ExtendedKalmanFilter<N, M, Model> {
505    /// Create an EKF with initial state, covariances, and model.
506    pub fn new(
507        x0: [f32; N],
508        p0: [[f32; N]; N],
509        q: [[f32; N]; N],
510        r: [[f32; M]; M],
511        model: Model,
512    ) -> Self {
513        Self {
514            x: x0,
515            p: p0,
516            q,
517            r,
518            model,
519        }
520    }
521
522    /// Create an EKF with diagonal covariances from scalar variances.
523    pub fn from_variances(x0: [f32; N], p_var: f32, q_var: f32, r_var: f32, model: Model) -> Self {
524        let mut p = [[0.0f32; N]; N];
525        let mut q = [[0.0f32; N]; N];
526        let mut r = [[0.0f32; M]; M];
527        for i in 0..N {
528            p[i][i] = p_var;
529            q[i][i] = q_var;
530        }
531        for i in 0..M {
532            r[i][i] = r_var;
533        }
534        Self::new(x0, p, q, r, model)
535    }
536
537    /// EKF predict: `x ← f(x, dt)`, `P ← F P Fᵀ + Q` with `F = ∂f/∂x`.
538    pub fn predict(&mut self, dt: f32) {
539        let mut f_jac = [[0.0f32; N]; N];
540        self.model.jacobian_f(&self.x, dt, &mut f_jac);
541
542        let mut x_new = [0.0f32; N];
543        self.model.f(&self.x, dt, &mut x_new);
544
545        ekf_predict_apply(&mut self.x, &mut self.p, &self.q, &f_jac, x_new);
546    }
547
548    /// EKF predict with an exogenous input `u`, via [`EkfModel::f_with_input`] /
549    /// [`EkfModel::jacobian_f_with_input`]. See the [module docs](self) for when this is
550    /// needed instead of [`ExtendedKalmanFilter::predict`].
551    pub fn predict_with_input<const U: usize>(&mut self, dt: f32, u: &[f32; U]) {
552        let mut f_jac = [[0.0f32; N]; N];
553        self.model.jacobian_f_with_input(&self.x, u, dt, &mut f_jac);
554
555        let mut x_new = [0.0f32; N];
556        self.model.f_with_input(&self.x, u, dt, &mut x_new);
557
558        ekf_predict_apply(&mut self.x, &mut self.p, &self.q, &f_jac, x_new);
559    }
560
561    /// EKF update with measurement `z`. Linearizes `h` at the current estimate.
562    ///
563    /// On singular innovation covariance or `M > 16`, returns an error and leaves state unchanged.
564    pub fn update(&mut self, z: &[f32; M]) -> Status {
565        let mut h_jac = [[0.0f32; N]; M];
566        self.model.jacobian_h(&self.x, &mut h_jac);
567
568        let mut hx = [0.0f32; M];
569        self.model.h(&self.x, &mut hx);
570
571        ekf_update_apply(&mut self.x, &mut self.p, &self.r, &h_jac, &hx, z)
572    }
573
574    /// EKF update with an exogenous input `u`, via [`EkfModel::h_with_input`] /
575    /// [`EkfModel::jacobian_h_with_input`]. See the [module docs](self) for when this is
576    /// needed instead of [`ExtendedKalmanFilter::update`].
577    ///
578    /// On singular innovation covariance or `M > 16`, returns an error and leaves state unchanged.
579    pub fn update_with_input<const U: usize>(&mut self, z: &[f32; M], u: &[f32; U]) -> Status {
580        let mut h_jac = [[0.0f32; N]; M];
581        self.model.jacobian_h_with_input(&self.x, u, &mut h_jac);
582
583        let mut hx = [0.0f32; M];
584        self.model.h_with_input(&self.x, u, &mut hx);
585
586        ekf_update_apply(&mut self.x, &mut self.p, &self.r, &h_jac, &hx, z)
587    }
588}
589
590/// Shared EKF predict math: `x ← x_new`, `P ← F P Fᵀ + Q`. Factored out so
591/// [`ExtendedKalmanFilter::predict`] and [`ExtendedKalmanFilter::predict_with_input`] (which
592/// differ only in how `x_new`/`f_jac` are computed) don't duplicate the covariance propagation.
593fn ekf_predict_apply<const N: usize>(
594    x: &mut [f32; N],
595    p: &mut [[f32; N]; N],
596    q: &[[f32; N]; N],
597    f_jac: &[[f32; N]; N],
598    x_new: [f32; N],
599) {
600    *x = x_new;
601
602    let mut fp = [[0.0f32; N]; N];
603    mat_mul(f_jac, p, &mut fp);
604    let mut p_new = [[0.0f32; N]; N];
605    mat_mul_bt(&fp, f_jac, &mut p_new);
606    mat_add_inplace_nn(&mut p_new, q);
607    *p = p_new;
608}
609
610/// Shared EKF update math: linearizes around `hx = h(x)` and reuses the linear-filter update
611/// core. Factored out so [`ExtendedKalmanFilter::update`] and
612/// [`ExtendedKalmanFilter::update_with_input`] (which differ only in how `hx`/`h_jac` are
613/// computed) don't duplicate the linearization.
614fn ekf_update_apply<const N: usize, const M: usize>(
615    x: &mut [f32; N],
616    p: &mut [[f32; N]; N],
617    r: &[[f32; M]; M],
618    h_jac: &[[f32; N]; M],
619    hx: &[f32; M],
620    z: &[f32; M],
621) -> Status {
622    if M > 16 {
623        return Status::ArgumentError;
624    }
625    if M == 0 {
626        return Status::SizeMismatch;
627    }
628
629    // Reuse linear update with innovation z' = z - h(x) + H x so that
630    // y = z' - H x = z - h(x).
631    let mut z_equiv = [0.0f32; M];
632    let mut hx_lin = [0.0f32; M];
633    mat_vec_mul(h_jac, x, &mut hx_lin);
634    for i in 0..M {
635        z_equiv[i] = z[i] - hx[i] + hx_lin[i];
636    }
637
638    kf_update_core(x, p, r, h_jac, &z_equiv)
639}
640
641// ─────────────────────────────────────────────────────────────────────────────
642// Square-Root Covariance Kalman Filter (SRKF)
643// ─────────────────────────────────────────────────────────────────────────────
644
645/// Square-Root Covariance Kalman Filter (SRKF) for $N$-state, $M$-measurement linear systems.
646///
647/// Propagates the lower-triangular Cholesky factor $S$ of the covariance matrix ($P = S S^T$).
648/// By operating directly on the square-root factors via orthogonal Givens transformations,
649/// the filter **guarantees numerical positive-definiteness and never diverges** due to roundoff error.
650#[derive(Debug, Clone)]
651pub struct SquareRootKalmanFilter<const N: usize, const M: usize> {
652    /// State estimate vector $\hat{x} \in \mathbb{R}^N$.
653    pub x: [f32; N],
654    /// Lower-triangular Cholesky factor of state covariance $P = S S^T$.
655    pub s: [[f32; N]; N],
656    /// State transition matrix $F \in \mathbb{R}^{N \times N}$.
657    pub f: [[f32; N]; N],
658    /// Lower-triangular Cholesky factor of process noise covariance $Q = S_Q S_Q^T$.
659    pub s_q: [[f32; N]; N],
660    /// Measurement matrix $H \in \mathbb{R}^{M \times N}$.
661    pub h: [[f32; N]; M],
662    /// Lower-triangular Cholesky factor of measurement noise $R = S_R S_R^T$.
663    pub s_r: [[f32; M]; M],
664}
665
666impl<const N: usize, const M: usize> SquareRootKalmanFilter<N, M> {
667    /// Initialize a new Square-Root Kalman Filter from explicit Cholesky factors.
668    pub fn new(
669        x0: [f32; N],
670        s0: [[f32; N]; N],
671        f: [[f32; N]; N],
672        s_q: [[f32; N]; N],
673        h: [[f32; N]; M],
674        s_r: [[f32; M]; M],
675    ) -> Self {
676        Self {
677            x: x0,
678            s: s0,
679            f,
680            s_q,
681            h,
682            s_r,
683        }
684    }
685
686    /// Predict step: propagates state $\hat{x}^- = F \hat{x}$ and triangularizes $[F S \quad S_Q]$.
687    pub fn predict(&mut self) {
688        // 1. State prediction: x = F * x
689        let mut x_new = [0.0f32; N];
690        mat_vec_mul(&self.f, &self.x, &mut x_new);
691        self.x = x_new;
692
693        // 2. Covariance square-root prediction: S^- via Cholesky factor of FS(FS)^T + S_Q(S_Q)^T
694        let mut fs = [[0.0f32; N]; N];
695        mat_mul(&self.f, &self.s, &mut fs);
696
697        let mut s_new = [[0.0f32; N]; N];
698        for i in 0..N {
699            for j in 0..=i {
700                let mut sum = 0.0f32;
701                for k in 0..N {
702                    sum += fs[i][k] * fs[j][k] + self.s_q[i][k] * self.s_q[j][k];
703                }
704                s_new[i][j] = sum;
705            }
706        }
707        cholesky_inplace_lower(&mut s_new);
708        self.s = s_new;
709    }
710
711    /// Update step: updates state $\hat{x}^+$ and factor $S^+$ given measurement vector $z \in \mathbb{R}^M$.
712    pub fn update(&mut self, z: &[f32; M]) -> Status {
713        if M == 0 || M > 16 {
714            return Status::ArgumentError;
715        }
716
717        // Innovation y = z - H x
718        let mut hx = [0.0f32; M];
719        mat_vec_mul(&self.h, &self.x, &mut hx);
720        let mut y = [0.0f32; M];
721        for i in 0..M {
722            y[i] = z[i] - hx[i];
723        }
724
725        // Innovation covariance S_yy = H P H^T + R = (H S) (H S)^T + S_R S_R^T
726        let mut hs = [[0.0f32; N]; M];
727        mat_mul(&self.h, &self.s, &mut hs);
728
729        let mut s_yy = [[0.0f32; M]; M];
730        for r in 0..M {
731            for c in 0..M {
732                let mut sum = 0.0f32;
733                for k in 0..N {
734                    sum += hs[r][k] * hs[c][k];
735                }
736                for k in 0..M {
737                    sum += self.s_r[r][k] * self.s_r[c][k];
738                }
739                s_yy[r][c] = sum;
740            }
741        }
742
743        // Invert S_yy
744        let mut s_yy_inv = [[0.0f32; M]; M];
745        let status = invert_mxm(&s_yy, &mut s_yy_inv);
746        if status != Status::Success {
747            return status;
748        }
749
750        // Kalman gain: K = P H^T S_yy^-1 = S S^T H^T S_yy^-1
751        let mut p = [[0.0f32; N]; N];
752        mat_mul_bt(&self.s, &self.s, &mut p);
753
754        let mut pht = [[0.0f32; M]; N];
755        mat_mul_bt(&p, &self.h, &mut pht);
756
757        let mut k_gain = [[0.0f32; M]; N];
758        mat_mul(&pht, &s_yy_inv, &mut k_gain);
759
760        // Update state: x = x + K y
761        let mut ky = [0.0f32; N];
762        mat_vec_mul(&k_gain, &y, &mut ky);
763        for i in 0..N {
764            self.x[i] += ky[i];
765        }
766
767        // Update covariance: P+ = (I - K H) P (I - K H)^T + K R K^T (Joseph form)
768        let mut i_kh = identity_n::<N>();
769        let mut kh = [[0.0f32; N]; N];
770        mat_mul(&k_gain, &self.h, &mut kh);
771        for r in 0..N {
772            for c in 0..N {
773                i_kh[r][c] -= kh[r][c];
774            }
775        }
776
777        let mut i_kh_p = [[0.0f32; N]; N];
778        mat_mul(&i_kh, &p, &mut i_kh_p);
779        let mut p_plus = [[0.0f32; N]; N];
780        mat_mul_bt(&i_kh_p, &i_kh, &mut p_plus);
781
782        let mut r_mat = [[0.0f32; M]; M];
783        mat_mul_bt(&self.s_r, &self.s_r, &mut r_mat);
784        let mut kr = [[0.0f32; M]; N];
785        mat_mul(&k_gain, &r_mat, &mut kr);
786        let mut krkt = [[0.0f32; N]; N];
787        mat_mul_bt(&kr, &k_gain, &mut krkt);
788        mat_add_inplace_nn(&mut p_plus, &krkt);
789
790        // Factor updated P+ into lower-triangular S+
791        cholesky_inplace_lower(&mut p_plus);
792        self.s = p_plus;
793
794        Status::Success
795    }
796
797    /// Reconstructs the full covariance matrix $P = S S^T$.
798    pub fn covariance(&self) -> [[f32; N]; N] {
799        let mut p = [[0.0f32; N]; N];
800        mat_mul_bt(&self.s, &self.s, &mut p);
801        p
802    }
803}
804
805/// Compute lower-triangular Cholesky factor $L$ in-place such that $A = L L^T$.
806fn cholesky_inplace_lower<const N: usize>(a: &mut [[f32; N]; N]) {
807    for i in 0..N {
808        for j in 0..=i {
809            let mut sum = a[i][j];
810            for k in 0..j {
811                sum -= a[i][k] * a[j][k];
812            }
813            if i == j {
814                a[i][j] = sum.max(1e-12).sqrt();
815            } else {
816                let diag = a[j][j].max(1e-12);
817                a[i][j] = sum / diag;
818            }
819        }
820        for j in (i + 1)..N {
821            a[i][j] = 0.0;
822        }
823    }
824}