Skip to main content

aegis_filter/
linear.rs

1//! Classical Linear & Kinematic Kalman Filters with Joseph stabilized covariance.
2
3use crate::common::{FilterError, FloatOps};
4
5/// 1D Scalar Kalman Filter with Joseph-stabilized variance update.
6#[derive(Debug, Clone, Copy)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8pub struct ScalarKalmanFilter {
9    x: f64,
10    p: f64,
11    q: f64,
12    r: f64,
13}
14
15impl ScalarKalmanFilter {
16    pub fn new(
17        initial_state: f64,
18        initial_variance: f64,
19        process_noise: f64,
20        measurement_noise: f64,
21    ) -> Result<Self, FilterError> {
22        if !initial_state.is_finite()
23            || !initial_variance.is_finite()
24            || !process_noise.is_finite()
25            || !measurement_noise.is_finite()
26        {
27            return Err(FilterError::NonFiniteInput);
28        }
29
30        if initial_variance <= 0.0 || process_noise < 0.0 || measurement_noise <= 0.0 {
31            return Err(FilterError::InvalidCovariance);
32        }
33
34        Ok(Self {
35            x: initial_state,
36            p: initial_variance,
37            q: process_noise,
38            r: measurement_noise,
39        })
40    }
41
42    pub fn predict(&mut self, control_input: f64) -> Result<(), FilterError> {
43        if !control_input.is_finite() {
44            return Err(FilterError::NonFiniteInput);
45        }
46
47        self.x += control_input;
48        self.p += self.q;
49
50        if !self.x.is_finite() || !self.p.is_finite() {
51            return Err(FilterError::NonFiniteInput);
52        }
53
54        Ok(())
55    }
56
57    pub fn update(
58        &mut self,
59        measurement: f64,
60        gate_threshold_sigma: Option<f64>,
61    ) -> Result<f64, FilterError> {
62        if !measurement.is_finite() {
63            return Err(FilterError::NonFiniteInput);
64        }
65
66        let y = measurement - self.x;
67        let s = self.p + self.r;
68        if s <= 1e-15 || !s.is_finite() {
69            return Err(FilterError::SingularInnovation);
70        }
71
72        if let Some(gate_sigma) = gate_threshold_sigma {
73            if gate_sigma > 0.0 {
74                let normalized_residual = y.abs_val() / s.sqrt_val();
75                if normalized_residual > gate_sigma {
76                    return Err(FilterError::MeasurementGated);
77                }
78            }
79        }
80
81        let k = self.p / s;
82        self.x += k * y;
83
84        // Joseph stabilized update: P = (1 - K)^2 * P + K^2 * R
85        let one_minus_k = 1.0 - k;
86        self.p = (one_minus_k * one_minus_k * self.p) + (k * k * self.r);
87
88        if self.p < 1e-15 {
89            self.p = 1e-15;
90        }
91
92        Ok(self.x)
93    }
94
95    #[inline(always)]
96    pub fn state(&self) -> f64 {
97        self.x
98    }
99
100    #[inline(always)]
101    pub fn variance(&self) -> f64 {
102        self.p
103    }
104}
105
106/// Generic N-dimensional Linear Kalman Filter using const-generics.
107#[derive(Debug, Clone)]
108pub struct LinearKalmanFilter<const S: usize, const M: usize> {
109    pub x: [f64; S],
110    pub p: [[f64; S]; S],
111    pub f: [[f64; S]; S],
112    pub q: [[f64; S]; S],
113    pub h: [[f64; S]; M],
114    pub r: [[f64; M]; M],
115}
116
117impl<const S: usize, const M: usize> LinearKalmanFilter<S, M> {
118    pub fn new(
119        initial_x: [f64; S],
120        initial_p: [[f64; S]; S],
121        f: [[f64; S]; S],
122        q: [[f64; S]; S],
123        h: [[f64; S]; M],
124        r: [[f64; M]; M],
125    ) -> Result<Self, FilterError> {
126        for val in initial_x.iter() {
127            if !val.is_finite() {
128                return Err(FilterError::NonFiniteInput);
129            }
130        }
131        Self::validate_matrix(&initial_p)?;
132        Self::validate_matrix(&f)?;
133        Self::validate_matrix(&q)?;
134        Self::validate_matrix(&h)?;
135        Self::validate_matrix(&r)?;
136
137        Ok(Self {
138            x: initial_x,
139            p: initial_p,
140            f,
141            q,
142            h,
143            r,
144        })
145    }
146
147    fn validate_matrix<const R: usize, const C: usize>(
148        matrix: &[[f64; C]; R],
149    ) -> Result<(), FilterError> {
150        for row in matrix.iter() {
151            for val in row.iter() {
152                if !val.is_finite() {
153                    return Err(FilterError::NonFiniteInput);
154                }
155            }
156        }
157        Ok(())
158    }
159
160    #[allow(clippy::needless_range_loop)]
161    pub fn predict(&mut self) -> Result<(), FilterError> {
162        let mut x_pred = [0.0; S];
163        for i in 0..S {
164            let mut sum = 0.0;
165            for j in 0..S {
166                sum += self.f[i][j] * self.x[j];
167            }
168            x_pred[i] = sum;
169        }
170
171        let mut f_p = [[0.0; S]; S];
172        for i in 0..S {
173            for j in 0..S {
174                let mut sum = 0.0;
175                for k in 0..S {
176                    sum += self.f[i][k] * self.p[k][j];
177                }
178                f_p[i][j] = sum;
179            }
180        }
181
182        let mut p_pred = [[0.0; S]; S];
183        for i in 0..S {
184            for j in 0..S {
185                let mut sum = 0.0;
186                for k in 0..S {
187                    sum += f_p[i][k] * self.f[j][k];
188                }
189                p_pred[i][j] = sum + self.q[i][j];
190            }
191        }
192
193        for i in 0..S {
194            for j in (i + 1)..S {
195                let avg = (p_pred[i][j] + p_pred[j][i]) * 0.5;
196                p_pred[i][j] = avg;
197                p_pred[j][i] = avg;
198            }
199        }
200
201        self.x = x_pred;
202        self.p = p_pred;
203        Ok(())
204    }
205}