#![allow(clippy::too_many_arguments, clippy::needless_range_loop)]
use crate::core::matrix::{matmul, matvec, outer, Matrix};
use crate::core::scalar::ControlScalar;
use crate::estimator::rts_smoother::{FilteredState, RtsSmoother, SmootherError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EmError {
SingularMatrix,
SmootherError(SmootherError),
InvalidLength,
}
impl core::fmt::Display for EmError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
EmError::SingularMatrix => write!(f, "EmAlgorithm: singular matrix"),
EmError::SmootherError(e) => write!(f, "EmAlgorithm: smoother error — {e}"),
EmError::InvalidLength => write!(f, "EmAlgorithm: invalid sequence length"),
}
}
}
impl From<SmootherError> for EmError {
fn from(e: SmootherError) -> Self {
EmError::SmootherError(e)
}
}
#[derive(Debug, Clone, Copy)]
pub struct EmModel<S: ControlScalar, const N: usize, const M: usize> {
pub a: Matrix<S, N, N>,
pub c: Matrix<S, M, N>,
pub q: Matrix<S, N, N>,
pub r: Matrix<S, M, M>,
}
fn kf_forward<S, const N: usize, const M: usize, const T: usize>(
a: &Matrix<S, N, N>,
c: &Matrix<S, M, N>,
q: &Matrix<S, N, N>,
r: &Matrix<S, M, M>,
y: &[[S; M]; T],
steps: usize,
x0: &[S; N],
p0: &Matrix<S, N, N>,
smoother: &mut RtsSmoother<S, N, T>,
) -> Result<(), EmError>
where
S: ControlScalar,
{
smoother.reset();
let mut x = *x0;
let mut p = *p0;
let ct = c.transpose();
let at = a.transpose();
for k in 0..steps {
let x_pred = matvec(a, &x);
let ap = matmul(a, &p);
let apat = matmul(&ap, &at);
let p_pred = apat.add_mat(q);
let cx = matvec(c, &x_pred);
let innov: [S; M] = core::array::from_fn(|i| y[k][i] - cx[i]);
let cp = matmul(c, &p_pred);
let cpct = matmul(&cp, &ct);
let s_mat = cpct.add_mat(r);
let s_inv = s_mat.inv().ok_or(EmError::SingularMatrix)?;
let pct = matmul(&p_pred, &ct);
let k_gain = matmul(&pct, &s_inv);
let kv = matvec(&k_gain, &innov);
let x_post: [S; N] = core::array::from_fn(|i| x_pred[i] + kv[i]);
let kc = matmul(&k_gain, c);
let eye = Matrix::<S, N, N>::identity();
let i_kc = eye.sub_mat(&kc);
let p_post = matmul(&i_kc, &p_pred);
smoother.store_forward(FilteredState::new(x_post, p_post, x_pred, p_pred))?;
x = x_post;
p = p_post;
}
Ok(())
}
#[derive(Debug, Clone, Copy)]
pub struct EmAlgorithm<S: ControlScalar, const N: usize, const M: usize, const T: usize> {
pub a_init: Matrix<S, N, N>,
pub c_init: Matrix<S, M, N>,
pub q_init: Matrix<S, N, N>,
pub r_init: Matrix<S, M, M>,
pub x0: [S; N],
pub p0: Matrix<S, N, N>,
}
impl<S: ControlScalar, const N: usize, const M: usize, const T: usize> EmAlgorithm<S, N, M, T> {
pub fn new(
a_init: Matrix<S, N, N>,
c_init: Matrix<S, M, N>,
q_init: Matrix<S, N, N>,
r_init: Matrix<S, M, M>,
x0: [S; N],
p0: Matrix<S, N, N>,
) -> Self {
Self {
a_init,
c_init,
q_init,
r_init,
x0,
p0,
}
}
pub fn fit(
&self,
y: &[[S; M]; T],
steps: usize,
max_iter: usize,
tol: S,
) -> Result<EmModel<S, N, M>, EmError> {
if steps == 0 || steps > T {
return Err(EmError::InvalidLength);
}
let mut a = self.a_init;
let mut c = self.c_init;
let mut q = self.q_init;
let mut r = self.r_init;
let mut smoother = RtsSmoother::<S, N, T>::new();
for _iter in 0..max_iter {
kf_forward(&a, &c, &q, &r, y, steps, &self.x0, &self.p0, &mut smoother)?;
let smoothed = smoother.smooth(&a)?;
let mut pxx: Matrix<S, N, N> = Matrix::zeros(); let mut pxx1: Matrix<S, N, N> = Matrix::zeros(); let mut pxx0: Matrix<S, N, N> = Matrix::zeros();
let mut syx: Matrix<S, M, N> = Matrix::zeros(); let mut sxx: Matrix<S, N, N> = Matrix::zeros(); let mut syy: Matrix<S, M, M> = Matrix::zeros();
for k in 0..steps {
let xk = smoothed.states[k].x;
let pk = smoothed.states[k].p;
let exxk = pk.add_mat(&outer(&xk, &xk));
sxx = sxx.add_mat(&exxk);
let yk = y[k];
let yx_k = outer(&yk, &xk);
syx = syx.add_mat(&yx_k);
let yy_k = outer(&yk, &yk);
syy = syy.add_mat(&yy_k);
if k > 0 {
pxx = pxx.add_mat(&exxk);
}
if k < steps - 1 {
pxx0 = pxx0.add_mat(&exxk);
}
}
let at_cur = a.transpose();
for k in 1..steps {
let fk_prev = smoother
.get_state(k - 1)
.ok_or(EmError::SmootherError(SmootherError::IndexOutOfRange))?;
let p_pred_k = fk_prev.p_pred; let p_km1_post = fk_prev.p;
let p_pred_k_inv = p_pred_k.inv().ok_or(EmError::SingularMatrix)?;
let p_at = matmul(&p_km1_post, &at_cur);
let g_km1 = matmul(&p_at, &p_pred_k_inv);
let pk_smooth = smoothed.states[k].p;
let cross = matmul(&g_km1, &pk_smooth);
let xk = smoothed.states[k].x;
let xkm1 = smoothed.states[k - 1].x;
let exxcross = cross.add_mat(&outer(&xk, &xkm1));
pxx1 = pxx1.add_mat(&exxcross);
}
let a_new = if steps > 1 {
let pxx0_inv = pxx0.inv().ok_or(EmError::SingularMatrix)?;
matmul(&pxx1, &pxx0_inv)
} else {
a
};
let sxx_inv = sxx.inv().ok_or(EmError::SingularMatrix)?;
let c_new = matmul(&syx, &sxx_inv);
let q_new = if steps > 1 {
let t_minus_1 = S::from_f64((steps - 1) as f64);
let pxx1_t = pxx1.transpose();
let a_pxx1t = matmul(&a_new, &pxx1_t);
let diff = pxx.sub_mat(&a_pxx1t);
symmetrize(diff.scale(S::ONE / t_minus_1))
} else {
q
};
let t_f = S::from_f64(steps as f64);
let syx_t = syx.transpose();
let c_syxt = matmul(&c_new, &syx_t);
let r_diff = syy.sub_mat(&c_syxt);
let r_new = symmetrize(r_diff.scale(S::ONE / t_f));
let da = a_new.sub_mat(&a).frob_norm();
let dc = c_new.sub_mat(&c).frob_norm();
a = a_new;
c = c_new;
q = q_new;
r = r_new;
if da < tol && dc < tol {
break;
}
}
Ok(EmModel { a, c, q, r })
}
}
fn symmetrize<S: ControlScalar, const N: usize>(m: Matrix<S, N, N>) -> Matrix<S, N, N> {
let mt = m.transpose();
m.add_mat(&mt).scale(S::HALF)
}
#[cfg(test)]
mod tests {
use super::*;
fn lcg(seed: u64, n: usize, scale: f64) -> [f64; 64] {
let mut out = [0.0_f64; 64];
let mut s = seed;
for item in out.iter_mut().take(n) {
s = s
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
let frac = (s >> 33) as f64 / (u32::MAX as f64);
*item = (frac - 0.5) * 2.0 * scale;
}
out
}
fn make_obs(steps: usize, a_true: f64, q_true: f64, r_true: f64) -> [[f64; 1]; 64] {
let proc_n = lcg(7, steps, q_true.sqrt());
let meas_n = lcg(13, steps, r_true.sqrt());
let mut y = [[0.0_f64; 1]; 64];
let mut x = 0.0_f64;
for k in 0..steps {
x = a_true * x + proc_n[k];
y[k][0] = x + meas_n[k];
}
y
}
fn build_em_1d() -> EmAlgorithm<f64, 1, 1, 64> {
EmAlgorithm::new(
Matrix::<f64, 1, 1>::identity(), Matrix::<f64, 1, 1>::identity(), Matrix::<f64, 1, 1> { data: [[1.0]] }, Matrix::<f64, 1, 1> { data: [[1.0]] }, [0.0_f64; 1], Matrix::<f64, 1, 1> { data: [[10.0]] }, )
}
#[test]
fn em_converges_to_valid_model() {
let y = make_obs(40, 0.95, 0.1, 0.5);
let em = build_em_1d();
let model = em.fit(&y, 40, 80, 1e-5).expect("EM fit");
assert!(
model.q.data[0][0] > 0.0,
"Q must be positive: {}",
model.q.data[0][0]
);
assert!(
model.r.data[0][0] > 0.0,
"R must be positive: {}",
model.r.data[0][0]
);
}
#[test]
fn em_recovers_a_approximately() {
let a_true = 0.9_f64;
let y = make_obs(60, a_true, 0.1, 0.3);
let em = build_em_1d();
let model = em.fit(&y, 60, 120, 1e-6).expect("EM fit");
let a_est = model.a.data[0][0];
assert!(
(a_est - a_true).abs() < 0.4,
"A estimate should be within 0.4 of true {a_true}: got {a_est}"
);
}
#[test]
fn em_recovers_c_approximately() {
let y = make_obs(50, 0.95, 0.05, 0.2);
let em = build_em_1d();
let model = em.fit(&y, 50, 100, 1e-6).expect("EM fit");
let c_est = model.c.data[0][0];
assert!(
c_est.abs() > 0.01 && c_est.abs() < 100.0,
"C estimate out of reasonable range: {c_est}"
);
}
#[test]
fn em_single_step_does_not_panic() {
let y = make_obs(1, 0.95, 0.1, 0.5);
let em = build_em_1d();
let model = em.fit(&y, 1, 10, 1e-6).expect("EM fit single step");
assert!(model.q.data[0][0] > 0.0);
}
#[test]
fn em_invalid_length_returns_error() {
let y = make_obs(10, 0.95, 0.1, 0.5);
let em = build_em_1d();
let res = em.fit(&y, 0, 10, 1e-6);
assert!(matches!(res, Err(EmError::InvalidLength)));
}
}