1use crate::solvers::learning::LearningError;
13use crate::solvers::linear_algebra::cholesky::{cholesky_factor, cholesky_solve};
14use crate::solvers::linear_algebra::gemm::{gemm, matvec, Transpose};
15
16#[derive(Debug, Clone)]
18pub struct KalmanFilter {
19 f: Vec<f64>, h: Vec<f64>, q: Vec<f64>, r: Vec<f64>, x: Vec<f64>, p: Vec<f64>, nx: usize,
26 nz: usize,
27}
28
29fn no(
30 m: usize,
31 n: usize,
32 k: usize,
33 a: &[f64],
34 b: &[f64],
35 out: &mut [f64],
36) -> Result<(), LearningError> {
37 gemm(Transpose::No, Transpose::No, m, n, k, 1.0, a, b, 0.0, out).map_err(Into::into)
38}
39fn no_t(
40 m: usize,
41 n: usize,
42 k: usize,
43 a: &[f64],
44 b: &[f64],
45 out: &mut [f64],
46) -> Result<(), LearningError> {
47 gemm(Transpose::No, Transpose::Yes, m, n, k, 1.0, a, b, 0.0, out).map_err(Into::into)
49}
50
51impl KalmanFilter {
52 pub fn new(
54 f: Vec<f64>,
55 h: Vec<f64>,
56 q: Vec<f64>,
57 r: Vec<f64>,
58 x0: Vec<f64>,
59 p0: Vec<f64>,
60 nx: usize,
61 nz: usize,
62 ) -> Result<Self, LearningError> {
63 if nx == 0
64 || nz == 0
65 || f.len() != nx * nx
66 || h.len() != nz * nx
67 || q.len() != nx * nx
68 || r.len() != nz * nz
69 || x0.len() != nx
70 || p0.len() != nx * nx
71 {
72 return Err(LearningError::InvalidDimension);
73 }
74 Ok(Self {
75 f,
76 h,
77 q,
78 r,
79 x: x0,
80 p: p0,
81 nx,
82 nz,
83 })
84 }
85
86 pub fn state(&self) -> &[f64] {
87 &self.x
88 }
89 pub fn covariance(&self) -> &[f64] {
90 &self.p
91 }
92
93 pub fn predict(&mut self) -> Result<(), LearningError> {
95 let nx = self.nx;
96 let mut xn = vec![0.0; nx];
98 matvec(Transpose::No, nx, nx, &self.f, &self.x, &mut xn)?;
99 self.x = xn;
100 let mut fp = vec![0.0; nx * nx];
102 no(nx, nx, nx, &self.f, &self.p, &mut fp)?;
103 let mut pn = vec![0.0; nx * nx];
104 no_t(nx, nx, nx, &fp, &self.f, &mut pn)?;
105 for i in 0..nx * nx {
106 pn[i] += self.q[i];
107 }
108 self.p = pn;
109 Ok(())
110 }
111
112 pub fn update(&mut self, z: &[f64]) -> Result<(), LearningError> {
114 let (nx, nz) = (self.nx, self.nz);
115 if z.len() != nz {
116 return Err(LearningError::InvalidDimension);
117 }
118 let mut hx = vec![0.0; nz];
120 matvec(Transpose::No, nz, nx, &self.h, &self.x, &mut hx)?;
121 let y: Vec<f64> = z.iter().zip(&hx).map(|(zi, hxi)| zi - hxi).collect();
122 let mut hp = vec![0.0; nz * nx];
124 no(nz, nx, nx, &self.h, &self.p, &mut hp)?;
125 let mut s = vec![0.0; nz * nz];
126 no_t(nz, nz, nx, &hp, &self.h, &mut s)?;
127 for i in 0..nz * nz {
128 s[i] += self.r[i];
129 }
130 let mut l = vec![0.0; nz * nz];
132 cholesky_factor(nz, &s, &mut l).map_err(|_| LearningError::Singular)?;
133 let mut s_inv = vec![0.0; nz * nz];
134 let mut ej = vec![0.0; nz];
135 let mut cj = vec![0.0; nz];
136 for j in 0..nz {
137 ej.iter_mut().for_each(|v| *v = 0.0);
138 ej[j] = 1.0;
139 cholesky_solve(nz, &l, &ej, &mut cj)?;
140 for i in 0..nz {
141 s_inv[i * nz + j] = cj[i];
142 }
143 }
144 let mut pht = vec![0.0; nx * nz];
146 no_t(nx, nz, nx, &self.p, &self.h, &mut pht)?;
147 let mut kgain = vec![0.0; nx * nz];
148 no(nx, nz, nz, &pht, &s_inv, &mut kgain)?;
149 let mut ky = vec![0.0; nx];
151 matvec(Transpose::No, nx, nz, &kgain, &y, &mut ky)?;
152 for i in 0..nx {
153 self.x[i] += ky[i];
154 }
155 let mut khp = vec![0.0; nx * nx];
157 no(nx, nx, nz, &kgain, &hp, &mut khp)?;
158 for i in 0..nx * nx {
159 self.p[i] -= khp[i];
160 }
161 Ok(())
162 }
163
164 pub fn filter(&mut self, observations: &[f64], t: usize) -> Result<Vec<f64>, LearningError> {
167 if observations.len() != t * self.nz {
168 return Err(LearningError::InvalidDimension);
169 }
170 let mut out = vec![0.0; t * self.nx];
171 for step in 0..t {
172 self.predict()?;
173 self.update(&observations[step * self.nz..(step + 1) * self.nz])?;
174 out[step * self.nx..(step + 1) * self.nx].copy_from_slice(&self.x);
175 }
176 Ok(out)
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn tracks_a_constant_with_noisy_measurements() {
186 let mut kf = KalmanFilter::new(
188 vec![1.0], vec![1.0], vec![1e-4], vec![1.0], vec![0.0], vec![10.0], 1,
195 1,
196 )
197 .unwrap();
198 let obs = [5.3, 4.7, 5.1, 4.9, 5.2, 4.8, 5.0, 5.1, 4.9, 5.0];
200 let est = kf.filter(&obs, 10).unwrap();
201 assert!((est[9] - 5.0).abs() < 0.3, "estimate {}", est[9]);
203 assert!(kf.covariance()[0] < 1.0, "covariance should shrink");
204 }
205
206 #[test]
207 fn smooths_better_than_raw_measurements() {
208 let mut kf = KalmanFilter::new(
210 vec![1.0],
211 vec![1.0],
212 vec![1e-3],
213 vec![1.0],
214 vec![10.0],
215 vec![1.0],
216 1,
217 1,
218 )
219 .unwrap();
220 let obs = [10.5, 9.4, 10.6, 9.5, 10.4, 9.6, 10.5, 9.5];
221 let est = kf.filter(&obs, 8).unwrap();
222 let var = |v: &[f64]| {
223 let m = v.iter().sum::<f64>() / v.len() as f64;
224 v.iter().map(|x| (x - m) * (x - m)).sum::<f64>() / v.len() as f64
225 };
226 let obs_var = var(&obs);
227 let est_var = var(&est[2..]); assert!(
229 est_var < obs_var,
230 "filter should smooth: {est_var} !< {obs_var}"
231 );
232 }
233
234 #[test]
235 fn guards() {
236 assert_eq!(
237 KalmanFilter::new(
238 vec![1.0],
239 vec![1.0],
240 vec![1.0],
241 vec![1.0],
242 vec![0.0],
243 vec![1.0, 0.0],
244 1,
245 1
246 )
247 .unwrap_err(),
248 LearningError::InvalidDimension
249 );
250 }
251}