use ndarray::{s, Array1, Array2, Array3, ArrayView1, ArrayView2};
pub struct KalmanResult {
pub means: Array2<f64>, pub covariances: Array3<f64>, pub pred_means: Array2<f64>, pub pred_covariances: Array3<f64>, pub log_likelihood: f64,
}
pub fn kalman_filter(
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 {
let t_len = observations.nrows();
let p_dim = observations.ncols();
let d_dim = a.nrows();
let m_dim = b.ncols();
let has_control = m_dim > 0;
let mut means = Array2::<f64>::zeros((t_len, d_dim));
let mut covs = Array3::<f64>::zeros((t_len, d_dim, d_dim));
let mut pred_means = Array2::<f64>::zeros((t_len, d_dim));
let mut pred_covs = Array3::<f64>::zeros((t_len, d_dim, d_dim));
let mut x_pred: Array1<f64> = mu_0.to_owned();
let mut p_pred: Array2<f64> = sigma_0.to_owned();
let mut log_lik = 0.0_f64;
let two_pi_log = (2.0 * std::f64::consts::PI).ln();
let i_d = Array2::<f64>::eye(d_dim);
for t in 0..t_len {
pred_means.slice_mut(s![t, ..]).assign(&x_pred);
pred_covs.slice_mut(s![t, .., ..]).assign(&p_pred);
let y_t = observations.slice(s![t, ..]);
let mut y_hat = c.dot(&x_pred);
if has_control {
let u_t = controls.slice(s![t, ..]);
y_hat = y_hat + d.dot(&u_t);
}
let innov = &y_t - &y_hat;
let s_mat = c.dot(&p_pred).dot(&c.t()) + r;
let s_inv = invert_psd_matrix(&s_mat);
let s_inv_innov = s_inv.dot(&innov);
let logdet_s = log_det_psd(&s_mat);
let quad_form = innov.dot(&s_inv_innov);
log_lik += -0.5 * (p_dim as f64 * two_pi_log + logdet_s + quad_form);
let k_gain = p_pred.dot(&c.t()).dot(&s_inv);
let x_filt = &x_pred + &k_gain.dot(&innov);
let i_minus_kc = &i_d - &k_gain.dot(&c);
let p_filt = i_minus_kc.dot(&p_pred).dot(&i_minus_kc.t()) + k_gain.dot(&r).dot(&k_gain.t());
means.slice_mut(s![t, ..]).assign(&x_filt);
covs.slice_mut(s![t, .., ..]).assign(&p_filt);
let mut x_next = a.dot(&x_filt);
if has_control {
let u_t = controls.slice(s![t, ..]);
x_next = x_next + b.dot(&u_t);
}
let p_next = a.dot(&p_filt).dot(&a.t()) + q;
x_pred = x_next;
p_pred = p_next;
}
KalmanResult {
means,
covariances: covs,
pred_means,
pred_covariances: pred_covs,
log_likelihood: log_lik,
}
}
fn log_det_psd(m: &Array2<f64>) -> f64 {
let l = cholesky(m);
let mut acc = 0.0_f64;
for i in 0..l.nrows() {
let d = l[(i, i)];
if d <= 0.0 {
return f64::NAN;
}
acc += d.ln();
}
2.0 * acc
}
fn invert_psd_matrix(m: &Array2<f64>) -> Array2<f64> {
let n = m.nrows();
let l = cholesky(m);
let mut inv = Array2::<f64>::eye(n);
for k in 0..n {
for i in 0..n {
let mut sum = inv[(i, k)];
for j in 0..i {
sum -= l[(i, j)] * inv[(j, k)];
}
inv[(i, k)] = sum / l[(i, i)];
}
}
let mut out = Array2::<f64>::zeros((n, n));
for k in 0..n {
for i in (0..n).rev() {
let mut sum = inv[(i, k)];
for j in (i + 1)..n {
sum -= l[(j, i)] * out[(j, k)];
}
out[(i, k)] = sum / l[(i, i)];
}
}
out
}
fn cholesky(m: &Array2<f64>) -> Array2<f64> {
let n = m.nrows();
let mut l = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..=i {
let mut sum = m[(i, j)];
for k in 0..j {
sum -= l[(i, k)] * l[(j, k)];
}
if i == j {
l[(i, j)] = sum.sqrt();
} else {
l[(i, j)] = sum / l[(j, j)];
}
}
}
l
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
#[test]
fn cholesky_identity() {
let i = Array2::<f64>::eye(3);
let l = cholesky(&i);
for r in 0..3 {
for c in 0..3 {
let expected = if r == c { 1.0 } else { 0.0 };
assert!((l[(r, c)] - expected).abs() < 1e-12);
}
}
}
#[test]
fn invert_psd_matrix_identity() {
let i = Array2::<f64>::eye(3);
let inv = invert_psd_matrix(&i);
for r in 0..3 {
for c in 0..3 {
let expected = if r == c { 1.0 } else { 0.0 };
assert!((inv[(r, c)] - expected).abs() < 1e-12);
}
}
}
#[test]
fn log_det_identity_is_zero() {
let i = Array2::<f64>::eye(4);
assert!(log_det_psd(&i).abs() < 1e-12);
}
#[test]
fn kalman_scalar_random_walk_matches_analytic() {
let a = array![[1.0]];
let b = array![[]];
let c = array![[1.0]];
let d = array![[]];
let q = array![[0.1]];
let r_mat = array![[1.0]];
let mu_0 = array![0.0];
let sigma_0 = array![[1.0]];
let obs = array![[1.0_f64]];
let controls = Array2::<f64>::zeros((1, 0));
let result = kalman_filter(
obs.view(),
controls.view(),
a.view(),
b.view(),
c.view(),
d.view(),
q.view(),
r_mat.view(),
mu_0.view(),
sigma_0.view(),
);
assert!((result.means[(0, 0)] - 0.5).abs() < 1e-12);
assert!((result.covariances[(0, 0, 0)] - 0.5).abs() < 1e-12);
}
#[test]
fn kalman_log_likelihood_finite() {
let a = array![[0.9, 0.1], [0.0, 0.95]];
let b = array![[], []];
let c = array![[1.0, 0.0]];
let d = array![[]];
let q = array![[0.01, 0.0], [0.0, 0.01]];
let r_mat = array![[0.1]];
let mu_0 = array![0.0, 0.0];
let sigma_0 = array![[1.0, 0.0], [0.0, 1.0]];
let obs = array![
[0.1],
[0.2],
[0.15],
[0.18],
[0.22],
[0.25],
[0.21],
[0.24],
[0.27],
[0.26],
];
let controls = Array2::<f64>::zeros((10, 0));
let result = kalman_filter(
obs.view(),
controls.view(),
a.view(),
b.view(),
c.view(),
d.view(),
q.view(),
r_mat.view(),
mu_0.view(),
sigma_0.view(),
);
assert!(result.log_likelihood.is_finite());
}
}