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
8use crate::matrix::{mat_inverse_f32, MatrixInstance, MatrixInstanceMut};
9use crate::types::Status;
10
11/// Scalar (1D) Kalman filter for single-variable sensor smoothing and estimation.
12#[derive(Debug, Clone, Copy)]
13pub struct KalmanFilter1D {
14    /// Estimated state
15    pub x: f32,
16    /// Estimation error covariance
17    pub p: f32,
18    /// Process noise covariance
19    pub q: f32,
20    /// Measurement noise covariance
21    pub r: f32,
22}
23
24impl KalmanFilter1D {
25    /// Initialises a 1D Kalman filter with initial estimate `x0`, initial covariance `p0`, process noise `q`, and measurement noise `r`.
26    pub fn new(x0: f32, p0: f32, q: f32, r: f32) -> Self {
27        Self { x: x0, p: p0, q, r }
28    }
29
30    /// Prediction step incorporating process input `u` (optional control input).
31    pub fn predict(&mut self, u: f32) {
32        self.x += u;
33        self.p += self.q;
34    }
35
36    /// Measurement update step with new sensor reading `z`. Returns updated state estimate.
37    pub fn update(&mut self, z: f32) -> f32 {
38        let k = self.p / (self.p + self.r);
39        self.x += k * (z - self.x);
40        self.p = (1.0 - k) * self.p;
41        self.x
42    }
43}
44
45/// 2-State (Position + Velocity) linear Kalman filter for motion tracking and sensor fusion.
46#[derive(Debug, Clone, Copy)]
47pub struct KalmanFilter2D {
48    /// State vector: `[position, velocity]`
49    pub x: [f32; 2],
50    /// 2x2 State covariance matrix (row-major: `[p00, p01, p10, p11]`)
51    pub p: [f32; 4],
52    /// Process noise variance
53    pub q_var: f32,
54    /// Measurement noise variance
55    pub r_var: f32,
56}
57
58impl KalmanFilter2D {
59    /// Initialise a 2D position/velocity Kalman filter.
60    pub fn new(initial_pos: f32, initial_vel: f32, q_var: f32, r_var: f32) -> Self {
61        Self {
62            x: [initial_pos, initial_vel],
63            p: [1.0, 0.0, 0.0, 1.0],
64            q_var,
65            r_var,
66        }
67    }
68
69    /// Predict state forward by time delta `dt`.
70    pub fn predict(&mut self, dt: f32) {
71        // State transition: x_pos = x_pos + dt * x_vel
72        self.x[0] += dt * self.x[1];
73
74        // P_new = F * P * F^T + Q
75        let dt2 = dt * dt;
76        let dt3 = dt2 * dt;
77        let dt4 = dt3 * dt;
78
79        let p00 =
80            self.p[0] + dt * (self.p[2] + self.p[1]) + dt2 * self.p[3] + 0.25 * dt4 * self.q_var;
81        let p01 = self.p[1] + dt * self.p[3] + 0.5 * dt3 * self.q_var;
82        let p10 = self.p[2] + dt * self.p[3] + 0.5 * dt3 * self.q_var;
83        let p11 = self.p[3] + dt2 * self.q_var;
84
85        self.p = [p00, p01, p10, p11];
86    }
87
88    /// Update filter with position measurement `z_pos`. Returns updated position and velocity `[pos, vel]`.
89    pub fn update(&mut self, z_pos: f32) -> [f32; 2] {
90        // Innovation
91        let y = z_pos - self.x[0];
92        let s = self.p[0] + self.r_var;
93
94        // Kalman gain K = P * H^T / S  (where H = [1, 0])
95        let k0 = self.p[0] / s;
96        let k1 = self.p[2] / s;
97
98        // State update
99        self.x[0] += k0 * y;
100        self.x[1] += k1 * y;
101
102        // Covariance update: P = (I - K * H) * P
103        let p00 = (1.0 - k0) * self.p[0];
104        let p01 = (1.0 - k0) * self.p[1];
105        let p10 = self.p[2] - k1 * self.p[0];
106        let p11 = self.p[3] - k1 * self.p[1];
107
108        self.p = [p00, p01, p10, p11];
109        self.x
110    }
111}
112
113// --- Const-generic linear Kalman & EKF helpers ---
114
115#[inline]
116fn mat_vec_mul<const R: usize, const C: usize>(
117    a: &[[f32; C]; R],
118    x: &[f32; C],
119    out: &mut [f32; R],
120) {
121    for r in 0..R {
122        let mut sum = 0.0f32;
123        for c in 0..C {
124            sum += a[r][c] * x[c];
125        }
126        out[r] = sum;
127    }
128}
129
130#[inline]
131fn mat_mul<const R: usize, const K: usize, const C: usize>(
132    a: &[[f32; K]; R],
133    b: &[[f32; C]; K],
134    out: &mut [[f32; C]; R],
135) {
136    for r in 0..R {
137        for c in 0..C {
138            let mut sum = 0.0f32;
139            for k in 0..K {
140                sum += a[r][k] * b[k][c];
141            }
142            out[r][c] = sum;
143        }
144    }
145}
146
147/// Computes `out = a * b^T` where `a` is R×K and `b` is C×K (so `b^T` is K×C).
148#[inline]
149fn mat_mul_bt<const R: usize, const K: usize, const C: usize>(
150    a: &[[f32; K]; R],
151    b: &[[f32; K]; C],
152    out: &mut [[f32; C]; R],
153) {
154    for r in 0..R {
155        for c in 0..C {
156            let mut sum = 0.0f32;
157            for k in 0..K {
158                sum += a[r][k] * b[c][k];
159            }
160            out[r][c] = sum;
161        }
162    }
163}
164
165#[inline]
166fn mat_add_inplace_nn<const N: usize>(a: &mut [[f32; N]; N], b: &[[f32; N]; N]) {
167    for r in 0..N {
168        for c in 0..N {
169            a[r][c] += b[r][c];
170        }
171    }
172}
173
174#[inline]
175fn mat_add_inplace_mm<const M: usize>(a: &mut [[f32; M]; M], b: &[[f32; M]; M]) {
176    for r in 0..M {
177        for c in 0..M {
178            a[r][c] += b[r][c];
179        }
180    }
181}
182
183#[inline]
184fn identity_n<const N: usize>() -> [[f32; N]; N] {
185    let mut i = [[0.0f32; N]; N];
186    for n in 0..N {
187        i[n][n] = 1.0;
188    }
189    i
190}
191
192/// Invert an `M×M` matrix using [`mat_inverse_f32`]. Requires `M ≤ 16`.
193fn invert_mxm<const M: usize>(s: &[[f32; M]; M], s_inv: &mut [[f32; M]; M]) -> Status {
194    if M == 0 {
195        return Status::SizeMismatch;
196    }
197    if M > 16 {
198        return Status::ArgumentError;
199    }
200
201    let mut flat_src = [0.0f32; 16 * 16];
202    let mut flat_dst = [0.0f32; 16 * 16];
203    for r in 0..M {
204        for c in 0..M {
205            flat_src[r * M + c] = s[r][c];
206        }
207    }
208
209    let src = MatrixInstance::new(M as u16, M as u16, &flat_src[..M * M]);
210    let mut dst = MatrixInstanceMut::new(M as u16, M as u16, &mut flat_dst[..M * M]);
211    let status = mat_inverse_f32(&src, &mut dst);
212    if status != Status::Success {
213        return status;
214    }
215
216    for r in 0..M {
217        for c in 0..M {
218            s_inv[r][c] = flat_dst[r * M + c];
219        }
220    }
221    Status::Success
222}
223
224/// Predict: `x ← F x`, `P ← F P Fᵀ + Q`.
225fn kf_predict_core<const N: usize>(
226    x: &mut [f32; N],
227    p: &mut [[f32; N]; N],
228    q: &[[f32; N]; N],
229    f: &[[f32; N]; N],
230) {
231    let mut x_new = [0.0f32; N];
232    mat_vec_mul(f, x, &mut x_new);
233    *x = x_new;
234
235    let mut fp = [[0.0f32; N]; N];
236    mat_mul(f, p, &mut fp);
237    let mut p_new = [[0.0f32; N]; N];
238    mat_mul_bt(&fp, f, &mut p_new);
239    mat_add_inplace_nn(&mut p_new, q);
240    *p = p_new;
241}
242
243/// Predict with control: `x ← F x + B u`, then same `P` update.
244fn kf_predict_control_core<const N: usize, const U: usize>(
245    x: &mut [f32; N],
246    p: &mut [[f32; N]; N],
247    q: &[[f32; N]; N],
248    f: &[[f32; N]; N],
249    b: &[[f32; U]; N],
250    u: &[f32; U],
251) {
252    let mut x_new = [0.0f32; N];
253    mat_vec_mul(f, x, &mut x_new);
254    let mut bu = [0.0f32; N];
255    mat_vec_mul(b, u, &mut bu);
256    for i in 0..N {
257        x_new[i] += bu[i];
258    }
259    *x = x_new;
260
261    let mut fp = [[0.0f32; N]; N];
262    mat_mul(f, p, &mut fp);
263    let mut p_new = [[0.0f32; N]; N];
264    mat_mul_bt(&fp, f, &mut p_new);
265    mat_add_inplace_nn(&mut p_new, q);
266    *p = p_new;
267}
268
269/// Measurement update with linear `H`. Leaves state unchanged on singular `S`.
270fn kf_update_core<const N: usize, const M: usize>(
271    x: &mut [f32; N],
272    p: &mut [[f32; N]; N],
273    r: &[[f32; M]; M],
274    h: &[[f32; N]; M],
275    z: &[f32; M],
276) -> Status {
277    if M > 16 {
278        return Status::ArgumentError;
279    }
280    if M == 0 {
281        return Status::SizeMismatch;
282    }
283
284    // y = z - H x
285    let mut hx = [0.0f32; M];
286    mat_vec_mul(h, x, &mut hx);
287    let mut y = [0.0f32; M];
288    for i in 0..M {
289        y[i] = z[i] - hx[i];
290    }
291
292    // S = H P Hᵀ + R
293    let mut hp = [[0.0f32; N]; M];
294    mat_mul(h, p, &mut hp);
295    let mut s = [[0.0f32; M]; M];
296    mat_mul_bt(&hp, h, &mut s);
297    mat_add_inplace_mm(&mut s, r);
298
299    let mut s_inv = [[0.0f32; M]; M];
300    let inv_status = invert_mxm(&s, &mut s_inv);
301    if inv_status != Status::Success {
302        return inv_status;
303    }
304
305    // P Hᵀ (N×M): rows of P times columns of Hᵀ (= rows of H)
306    let mut pht = [[0.0f32; M]; N];
307    for i in 0..N {
308        for j in 0..M {
309            let mut sum = 0.0f32;
310            for k in 0..N {
311                sum += p[i][k] * h[j][k];
312            }
313            pht[i][j] = sum;
314        }
315    }
316
317    // K = (P Hᵀ) S⁻¹  (N×M)
318    let mut k = [[0.0f32; M]; N];
319    mat_mul(&pht, &s_inv, &mut k);
320
321    // x ← x + K y
322    let mut ky = [0.0f32; N];
323    mat_vec_mul(&k, &y, &mut ky);
324    let mut x_new = *x;
325    for i in 0..N {
326        x_new[i] += ky[i];
327    }
328
329    // P ← (I - K H) P
330    let mut kh = [[0.0f32; N]; N];
331    mat_mul(&k, h, &mut kh);
332    let mut i_kh = identity_n::<N>();
333    for r in 0..N {
334        for c in 0..N {
335            i_kh[r][c] -= kh[r][c];
336        }
337    }
338    let mut p_new = [[0.0f32; N]; N];
339    mat_mul(&i_kh, p, &mut p_new);
340
341    *x = x_new;
342    *p = p_new;
343    Status::Success
344}
345
346/// Const-generic linear Kalman filter: `x' = F x (+ B u) + w`, `z = H x + v`.
347///
348/// Measurement dimension `M` must be ≤ 16 so the innovation covariance can be inverted
349/// with the crate's stack-limited matrix inverse.
350#[derive(Debug, Clone, Copy, PartialEq)]
351pub struct KalmanFilter<const N: usize, const M: usize> {
352    /// State estimate
353    pub x: [f32; N],
354    /// State covariance `P` (`N×N`)
355    pub p: [[f32; N]; N],
356    /// Process noise covariance `Q` (`N×N`)
357    pub q: [[f32; N]; N],
358    /// Measurement noise covariance `R` (`M×M`)
359    pub r: [[f32; M]; M],
360}
361
362impl<const N: usize, const M: usize> KalmanFilter<N, M> {
363    /// Create a filter with initial state `x0`, covariance `p0`, and noise covariances `q` / `r`.
364    pub fn new(x0: [f32; N], p0: [[f32; N]; N], q: [[f32; N]; N], r: [[f32; M]; M]) -> Self {
365        Self {
366            x: x0,
367            p: p0,
368            q,
369            r,
370        }
371    }
372
373    /// Create a filter with diagonal `P`, `Q`, and `R` initialized from scalar variances.
374    pub fn from_variances(x0: [f32; N], p_var: f32, q_var: f32, r_var: f32) -> Self {
375        let mut p = [[0.0f32; N]; N];
376        let mut q = [[0.0f32; N]; N];
377        let mut r = [[0.0f32; M]; M];
378        for i in 0..N {
379            p[i][i] = p_var;
380            q[i][i] = q_var;
381        }
382        for i in 0..M {
383            r[i][i] = r_var;
384        }
385        Self::new(x0, p, q, r)
386    }
387
388    /// Prediction without control input: `x ← F x`, `P ← F P Fᵀ + Q`.
389    pub fn predict(&mut self, f: &[[f32; N]; N]) {
390        kf_predict_core(&mut self.x, &mut self.p, &self.q, f);
391    }
392
393    /// Prediction with control: `x ← F x + B u`, `P ← F P Fᵀ + Q`.
394    pub fn predict_with_control<const U: usize>(
395        &mut self,
396        f: &[[f32; N]; N],
397        b: &[[f32; U]; N],
398        u: &[f32; U],
399    ) {
400        kf_predict_control_core(&mut self.x, &mut self.p, &self.q, f, b, u);
401    }
402
403    /// Measurement update with observation matrix `H` (`M×N`) and measurement `z`.
404    ///
405    /// On success returns [`Status::Success`] and updates `x` / `P`. If `S` is singular or
406    /// `M > 16`, returns an error status and leaves the filter state unchanged.
407    pub fn update(&mut self, h: &[[f32; N]; M], z: &[f32; M]) -> Status {
408        kf_update_core(&mut self.x, &mut self.p, &self.r, h, z)
409    }
410}
411
412/// User-supplied nonlinear process and measurement model for an EKF (static dispatch).
413pub trait EkfModel<const N: usize, const M: usize> {
414    /// Process model: `out = f(x, dt)`.
415    fn f(&self, x: &[f32; N], dt: f32, out: &mut [f32; N]);
416
417    /// Measurement model: `out = h(x)`.
418    fn h(&self, x: &[f32; N], out: &mut [f32; M]);
419
420    /// Process Jacobian `F = ∂f/∂x` evaluated at `x`.
421    fn jacobian_f(&self, x: &[f32; N], dt: f32, out: &mut [[f32; N]; N]);
422
423    /// Measurement Jacobian `H = ∂h/∂x` evaluated at `x` (`M×N`).
424    fn jacobian_h(&self, x: &[f32; N], out: &mut [[f32; N]; M]);
425}
426
427/// Extended Kalman filter with compile-time dimensions and a user [`EkfModel`].
428///
429/// Measurement dimension `M` must be ≤ 16. Covariance update uses `P ← (I − KH) P`.
430#[derive(Debug, Clone, Copy, PartialEq)]
431pub struct ExtendedKalmanFilter<const N: usize, const M: usize, Model> {
432    /// State estimate
433    pub x: [f32; N],
434    /// State covariance `P` (`N×N`)
435    pub p: [[f32; N]; N],
436    /// Process noise covariance `Q` (`N×N`)
437    pub q: [[f32; N]; N],
438    /// Measurement noise covariance `R` (`M×M`)
439    pub r: [[f32; M]; M],
440    /// Nonlinear process / measurement model
441    pub model: Model,
442}
443
444impl<const N: usize, const M: usize, Model: EkfModel<N, M>> ExtendedKalmanFilter<N, M, Model> {
445    /// Create an EKF with initial state, covariances, and model.
446    pub fn new(
447        x0: [f32; N],
448        p0: [[f32; N]; N],
449        q: [[f32; N]; N],
450        r: [[f32; M]; M],
451        model: Model,
452    ) -> Self {
453        Self {
454            x: x0,
455            p: p0,
456            q,
457            r,
458            model,
459        }
460    }
461
462    /// Create an EKF with diagonal covariances from scalar variances.
463    pub fn from_variances(x0: [f32; N], p_var: f32, q_var: f32, r_var: f32, model: Model) -> Self {
464        let mut p = [[0.0f32; N]; N];
465        let mut q = [[0.0f32; N]; N];
466        let mut r = [[0.0f32; M]; M];
467        for i in 0..N {
468            p[i][i] = p_var;
469            q[i][i] = q_var;
470        }
471        for i in 0..M {
472            r[i][i] = r_var;
473        }
474        Self::new(x0, p, q, r, model)
475    }
476
477    /// EKF predict: `x ← f(x, dt)`, `P ← F P Fᵀ + Q` with `F = ∂f/∂x`.
478    pub fn predict(&mut self, dt: f32) {
479        let mut f_jac = [[0.0f32; N]; N];
480        self.model.jacobian_f(&self.x, dt, &mut f_jac);
481
482        let mut x_new = [0.0f32; N];
483        self.model.f(&self.x, dt, &mut x_new);
484        self.x = x_new;
485
486        let mut fp = [[0.0f32; N]; N];
487        mat_mul(&f_jac, &self.p, &mut fp);
488        let mut p_new = [[0.0f32; N]; N];
489        mat_mul_bt(&fp, &f_jac, &mut p_new);
490        mat_add_inplace_nn(&mut p_new, &self.q);
491        self.p = p_new;
492    }
493
494    /// EKF update with measurement `z`. Linearizes `h` at the current estimate.
495    ///
496    /// On singular innovation covariance or `M > 16`, returns an error and leaves state unchanged.
497    pub fn update(&mut self, z: &[f32; M]) -> Status {
498        if M > 16 {
499            return Status::ArgumentError;
500        }
501        if M == 0 {
502            return Status::SizeMismatch;
503        }
504
505        let mut h_jac = [[0.0f32; N]; M];
506        self.model.jacobian_h(&self.x, &mut h_jac);
507
508        let mut hx = [0.0f32; M];
509        self.model.h(&self.x, &mut hx);
510
511        // Reuse linear update with innovation z' = z - h(x) + H x so that
512        // y = z' - H x = z - h(x).
513        let mut z_equiv = [0.0f32; M];
514        let mut hx_lin = [0.0f32; M];
515        mat_vec_mul(&h_jac, &self.x, &mut hx_lin);
516        for i in 0..M {
517            z_equiv[i] = z[i] - hx[i] + hx_lin[i];
518        }
519
520        kf_update_core(&mut self.x, &mut self.p, &self.r, &h_jac, &z_equiv)
521    }
522}