1use ndarray::{s, Array1, Array2, Array3, ArrayView1, ArrayView2};
31
32pub struct KalmanResult {
34 pub means: Array2<f64>, pub covariances: Array3<f64>, pub pred_means: Array2<f64>, pub pred_covariances: Array3<f64>, pub log_likelihood: f64,
39}
40
41pub fn kalman_filter(
51 observations: ArrayView2<f64>, controls: ArrayView2<f64>, a: ArrayView2<f64>, b: ArrayView2<f64>, c: ArrayView2<f64>, d: ArrayView2<f64>, q: ArrayView2<f64>, r: ArrayView2<f64>, mu_0: ArrayView1<f64>, sigma_0: ArrayView2<f64>, ) -> KalmanResult {
62 let t_len = observations.nrows();
63 let p_dim = observations.ncols();
64 let d_dim = a.nrows();
65 let m_dim = b.ncols();
66
67 let has_control = m_dim > 0;
68
69 let mut means = Array2::<f64>::zeros((t_len, d_dim));
70 let mut covs = Array3::<f64>::zeros((t_len, d_dim, d_dim));
71 let mut pred_means = Array2::<f64>::zeros((t_len, d_dim));
72 let mut pred_covs = Array3::<f64>::zeros((t_len, d_dim, d_dim));
73
74 let mut x_pred: Array1<f64> = mu_0.to_owned();
75 let mut p_pred: Array2<f64> = sigma_0.to_owned();
76
77 let mut log_lik = 0.0_f64;
78 let two_pi_log = (2.0 * std::f64::consts::PI).ln();
79 let i_d = Array2::<f64>::eye(d_dim);
80
81 for t in 0..t_len {
82 pred_means.slice_mut(s![t, ..]).assign(&x_pred);
84 pred_covs.slice_mut(s![t, .., ..]).assign(&p_pred);
85
86 let y_t = observations.slice(s![t, ..]);
87
88 let mut y_hat = c.dot(&x_pred);
90 if has_control {
91 let u_t = controls.slice(s![t, ..]);
92 y_hat = y_hat + d.dot(&u_t);
93 }
94 let innov = &y_t - &y_hat;
95
96 let s_mat = c.dot(&p_pred).dot(&c.t()) + r;
98
99 let s_inv = invert_psd_matrix(&s_mat);
101 let s_inv_innov = s_inv.dot(&innov);
102
103 let logdet_s = log_det_psd(&s_mat);
105 let quad_form = innov.dot(&s_inv_innov);
106 log_lik += -0.5 * (p_dim as f64 * two_pi_log + logdet_s + quad_form);
107
108 let k_gain = p_pred.dot(&c.t()).dot(&s_inv);
110
111 let x_filt = &x_pred + &k_gain.dot(&innov);
113
114 let i_minus_kc = &i_d - &k_gain.dot(&c);
117 let p_filt = i_minus_kc.dot(&p_pred).dot(&i_minus_kc.t()) + k_gain.dot(&r).dot(&k_gain.t());
118
119 means.slice_mut(s![t, ..]).assign(&x_filt);
120 covs.slice_mut(s![t, .., ..]).assign(&p_filt);
121
122 let mut x_next = a.dot(&x_filt);
124 if has_control {
125 let u_t = controls.slice(s![t, ..]);
126 x_next = x_next + b.dot(&u_t);
127 }
128 let p_next = a.dot(&p_filt).dot(&a.t()) + q;
129 x_pred = x_next;
130 p_pred = p_next;
131 }
132
133 KalmanResult {
134 means,
135 covariances: covs,
136 pred_means,
137 pred_covariances: pred_covs,
138 log_likelihood: log_lik,
139 }
140}
141
142fn log_det_psd(m: &Array2<f64>) -> f64 {
147 let l = cholesky(m);
148 let mut acc = 0.0_f64;
149 for i in 0..l.nrows() {
150 let d = l[(i, i)];
151 if d <= 0.0 {
152 return f64::NAN;
153 }
154 acc += d.ln();
155 }
156 2.0 * acc
157}
158
159fn invert_psd_matrix(m: &Array2<f64>) -> Array2<f64> {
164 let n = m.nrows();
165 let l = cholesky(m);
166 let mut inv = Array2::<f64>::eye(n);
167 for k in 0..n {
169 for i in 0..n {
170 let mut sum = inv[(i, k)];
171 for j in 0..i {
172 sum -= l[(i, j)] * inv[(j, k)];
173 }
174 inv[(i, k)] = sum / l[(i, i)];
175 }
176 }
177 let mut out = Array2::<f64>::zeros((n, n));
179 for k in 0..n {
180 for i in (0..n).rev() {
181 let mut sum = inv[(i, k)];
182 for j in (i + 1)..n {
183 sum -= l[(j, i)] * out[(j, k)];
184 }
185 out[(i, k)] = sum / l[(i, i)];
186 }
187 }
188 out
189}
190
191fn cholesky(m: &Array2<f64>) -> Array2<f64> {
195 let n = m.nrows();
196 let mut l = Array2::<f64>::zeros((n, n));
197 for i in 0..n {
198 for j in 0..=i {
199 let mut sum = m[(i, j)];
200 for k in 0..j {
201 sum -= l[(i, k)] * l[(j, k)];
202 }
203 if i == j {
204 l[(i, j)] = sum.sqrt();
205 } else {
206 l[(i, j)] = sum / l[(j, j)];
207 }
208 }
209 }
210 l
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use ndarray::array;
217
218 #[test]
219 fn cholesky_identity() {
220 let i = Array2::<f64>::eye(3);
221 let l = cholesky(&i);
222 for r in 0..3 {
223 for c in 0..3 {
224 let expected = if r == c { 1.0 } else { 0.0 };
225 assert!((l[(r, c)] - expected).abs() < 1e-12);
226 }
227 }
228 }
229
230 #[test]
231 fn invert_psd_matrix_identity() {
232 let i = Array2::<f64>::eye(3);
233 let inv = invert_psd_matrix(&i);
234 for r in 0..3 {
235 for c in 0..3 {
236 let expected = if r == c { 1.0 } else { 0.0 };
237 assert!((inv[(r, c)] - expected).abs() < 1e-12);
238 }
239 }
240 }
241
242 #[test]
243 fn log_det_identity_is_zero() {
244 let i = Array2::<f64>::eye(4);
245 assert!(log_det_psd(&i).abs() < 1e-12);
246 }
247
248 #[test]
249 fn kalman_scalar_random_walk_matches_analytic() {
250 let a = array![[1.0]];
258 let b = array![[]];
259 let c = array![[1.0]];
260 let d = array![[]];
261 let q = array![[0.1]];
262 let r_mat = array![[1.0]];
263 let mu_0 = array![0.0];
264 let sigma_0 = array![[1.0]];
265
266 let obs = array![[1.0_f64]];
267 let controls = Array2::<f64>::zeros((1, 0));
268
269 let result = kalman_filter(
270 obs.view(),
271 controls.view(),
272 a.view(),
273 b.view(),
274 c.view(),
275 d.view(),
276 q.view(),
277 r_mat.view(),
278 mu_0.view(),
279 sigma_0.view(),
280 );
281
282 assert!((result.means[(0, 0)] - 0.5).abs() < 1e-12);
283 assert!((result.covariances[(0, 0, 0)] - 0.5).abs() < 1e-12);
284 }
285
286 #[test]
287 fn kalman_log_likelihood_finite() {
288 let a = array![[0.9, 0.1], [0.0, 0.95]];
290 let b = array![[], []];
291 let c = array![[1.0, 0.0]];
292 let d = array![[]];
293 let q = array![[0.01, 0.0], [0.0, 0.01]];
294 let r_mat = array![[0.1]];
295 let mu_0 = array![0.0, 0.0];
296 let sigma_0 = array![[1.0, 0.0], [0.0, 1.0]];
297
298 let obs = array![
299 [0.1],
300 [0.2],
301 [0.15],
302 [0.18],
303 [0.22],
304 [0.25],
305 [0.21],
306 [0.24],
307 [0.27],
308 [0.26],
309 ];
310 let controls = Array2::<f64>::zeros((10, 0));
311
312 let result = kalman_filter(
313 obs.view(),
314 controls.view(),
315 a.view(),
316 b.view(),
317 c.view(),
318 d.view(),
319 q.view(),
320 r_mat.view(),
321 mu_0.view(),
322 sigma_0.view(),
323 );
324
325 assert!(result.log_likelihood.is_finite());
326 }
327}