#![allow(clippy::too_many_arguments)]
use crate::core::matrix::{matmul, matvec, Matrix};
use crate::core::scalar::ControlScalar;
use crate::estimator::rts_smoother::{FilteredState, RtsSmoother, SmoothedData, SmootherError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EstError {
SingularMatrix,
SmootherError(SmootherError),
LengthMismatch,
OptimizationFailed,
}
impl core::fmt::Display for EstError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
EstError::SingularMatrix => write!(f, "MlEstimator: singular matrix"),
EstError::SmootherError(e) => write!(f, "MlEstimator: smoother error — {e}"),
EstError::LengthMismatch => write!(f, "MlEstimator: data length mismatch"),
EstError::OptimizationFailed => write!(f, "MlEstimator: optimization diverged"),
}
}
}
impl From<SmootherError> for EstError {
fn from(e: SmootherError) -> Self {
EstError::SmootherError(e)
}
}
fn forward_pass<S, const N: usize, const M: usize, const I: usize, const T: usize>(
a: &Matrix<S, N, N>,
b: &Matrix<S, N, I>,
h: &Matrix<S, M, N>,
q: &Matrix<S, N, N>,
r: &Matrix<S, M, M>,
y: &[[S; M]; T],
u: &[[S; I]; T],
steps: usize,
x0: &[S; N],
p0: &Matrix<S, N, N>,
smoother: &mut RtsSmoother<S, N, T>,
) -> Result<S, EstError>
where
S: ControlScalar,
{
smoother.reset();
let mut x = *x0;
let mut p = *p0;
let ht = h.transpose();
let at = a.transpose();
let mut neg_log_lik = S::ZERO;
let log2pi = S::from_f64(core::f64::consts::TAU.ln());
for k in 0..steps {
let ax = matvec(a, &x);
let bu = matvec(b, &u[k]);
let x_pred: [S; N] = core::array::from_fn(|i| ax[i] + bu[i]);
let ap = matmul(a, &p);
let apat = matmul(&ap, &at);
let p_pred = apat.add_mat(q);
let hx = matvec(h, &x_pred);
let innov: [S; M] = core::array::from_fn(|i| y[k][i] - hx[i]);
let hp = matmul(h, &p_pred);
let hpht = matmul(&hp, &ht);
let s_mat = hpht.add_mat(r);
let s_inv = s_mat.inv().ok_or(EstError::SingularMatrix)?;
let pht = matmul(&p_pred, &ht);
let k_gain = matmul(&pht, &s_inv);
let kv = matvec(&k_gain, &innov);
let x_post: [S; N] = core::array::from_fn(|i| x_pred[i] + kv[i]);
let kh = matmul(&k_gain, h);
let eye = Matrix::<S, N, N>::identity();
let i_kh = eye.sub_mat(&kh);
let p_post = matmul(&i_kh, &p_pred);
let nu_s_nu: S = {
let s_inv_nu = matvec(&s_inv, &innov);
let mut acc = S::ZERO;
for i in 0..M {
acc += innov[i] * s_inv_nu[i];
}
acc
};
let log_det_s = log_det_sym(&s_mat);
let m_f = S::from_f64(M as f64);
neg_log_lik += S::HALF * (m_f * log2pi + log_det_s + nu_s_nu);
smoother.store_forward(FilteredState::new(x_post, p_post, x_pred, p_pred))?;
x = x_post;
p = p_post;
}
Ok(neg_log_lik)
}
fn log_det_sym<S: ControlScalar, const N: usize>(m: &Matrix<S, N, N>) -> S {
match m.cholesky() {
Some(l) => {
let mut sum = S::ZERO;
for i in 0..N {
let d = l.data[i][i];
if d > S::ZERO {
sum += d.ln();
} else {
return S::from_f64(1e30_f64);
}
}
sum * S::TWO }
None => S::from_f64(1e30_f64),
}
}
#[derive(Debug, Clone, Copy)]
pub struct MlEstimator<S: ControlScalar, const N: usize, const M: usize, const T: usize> {
pub a: Matrix<S, N, N>,
pub b: Matrix<S, N, N>, pub h: Matrix<S, M, N>,
pub x0: [S; N],
pub p0: Matrix<S, N, N>,
pub fd_eps: S,
pub learn_rate: S,
}
impl<S: ControlScalar, const N: usize, const M: usize, const T: usize> MlEstimator<S, N, M, T> {
pub fn new(
a: Matrix<S, N, N>,
b: Matrix<S, N, N>,
h: Matrix<S, M, N>,
x0: [S; N],
p0: Matrix<S, N, N>,
fd_eps: S,
learn_rate: S,
) -> Self {
Self {
a,
b,
h,
x0,
p0,
fd_eps,
learn_rate,
}
}
pub fn fit(
&self,
y: &[[S; M]; T],
u: &[[S; N]; T],
steps: usize,
max_iter: usize,
) -> Result<(Matrix<S, N, N>, Matrix<S, M, M>), EstError> {
if steps == 0 || steps > T {
return Err(EstError::LengthMismatch);
}
let mut q_diag: [S; N] = [S::ONE; N];
let mut r_diag: [S; M] = [S::ONE; M];
let eps = self.fd_eps;
let lr = self.learn_rate;
let zero_u: [S; N] = [S::ZERO; N];
let mut smoother = RtsSmoother::<S, N, T>::new();
let mut prev_nll = S::from_f64(f64::MAX);
for _iter in 0..max_iter {
let q_cur = diag_matrix::<S, N>(&q_diag);
let r_cur = diag_matrix::<S, M>(&r_diag);
let nll = forward_pass::<S, N, M, N, T>(
&self.a,
&self.b,
&self.h,
&q_cur,
&r_cur,
y,
u,
steps,
&self.x0,
&self.p0,
&mut smoother,
)?;
let delta = (prev_nll - nll).abs();
if delta < S::from_f64(1e-6) {
break;
}
prev_nll = nll;
let mut grad_q = [S::ZERO; N];
for i in 0..N {
let mut qd_p = q_diag;
qd_p[i] += eps;
let q_p = diag_matrix::<S, N>(&qd_p);
let nll_p = forward_pass::<S, N, M, N, T>(
&self.a,
&self.b,
&self.h,
&q_p,
&r_cur,
y,
u,
steps,
&self.x0,
&self.p0,
&mut smoother,
)?;
grad_q[i] = (nll_p - nll) / eps;
}
let mut grad_r = [S::ZERO; M];
for i in 0..M {
let mut rd_p = r_diag;
rd_p[i] += eps;
let r_p = diag_matrix::<S, M>(&rd_p);
let nll_p = forward_pass::<S, N, M, N, T>(
&self.a,
&self.b,
&self.h,
&q_cur,
&r_p,
y,
u,
steps,
&self.x0,
&self.p0,
&mut smoother,
)?;
grad_r[i] = (nll_p - nll) / eps;
}
for i in 0..N {
let new_val = q_diag[i] - lr * grad_q[i];
q_diag[i] = if new_val > S::from_f64(1e-10) {
new_val
} else {
S::from_f64(1e-10)
};
}
for i in 0..M {
let new_val = r_diag[i] - lr * grad_r[i];
r_diag[i] = if new_val > S::from_f64(1e-10) {
new_val
} else {
S::from_f64(1e-10)
};
}
let _ = zero_u;
}
Ok((diag_matrix::<S, N>(&q_diag), diag_matrix::<S, M>(&r_diag)))
}
}
fn diag_matrix<S: ControlScalar, const N: usize>(diag: &[S; N]) -> Matrix<S, N, N> {
Matrix {
data: core::array::from_fn(|r| {
core::array::from_fn(|c| if r == c { diag[r] } else { S::ZERO })
}),
}
}
#[derive(Debug, Clone, Copy)]
pub struct BatchKalmanSmoother<S: ControlScalar, const N: usize, const M: usize, const T: usize> {
pub estimator: MlEstimator<S, N, M, T>,
}
#[derive(Debug, Clone, Copy)]
pub struct BatchSmootherOutput<S: ControlScalar, const N: usize, const M: usize, const T: usize> {
pub smoothed: SmoothedData<S, N, T>,
pub q_est: Matrix<S, N, N>,
pub r_est: Matrix<S, M, M>,
}
impl<S: ControlScalar, const N: usize, const M: usize, const T: usize>
BatchKalmanSmoother<S, N, M, T>
{
pub fn new(estimator: MlEstimator<S, N, M, T>) -> Self {
Self { estimator }
}
pub fn fit_and_smooth(
&self,
y: &[[S; M]; T],
u: &[[S; N]; T],
steps: usize,
max_iter: usize,
) -> Result<BatchSmootherOutput<S, N, M, T>, EstError> {
let (q_est, r_est) = self.estimator.fit(y, u, steps, max_iter)?;
let mut smoother = RtsSmoother::<S, N, T>::new();
forward_pass::<S, N, M, N, T>(
&self.estimator.a,
&self.estimator.b,
&self.estimator.h,
&q_est,
&r_est,
y,
u,
steps,
&self.estimator.x0,
&self.estimator.p0,
&mut smoother,
)?;
let smoothed = smoother.smooth(&self.estimator.a)?;
Ok(BatchSmootherOutput {
smoothed,
q_est,
r_est,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn lcg_noise(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_data_1d(steps: usize, q_true: f64, r_true: f64) -> ([[f64; 1]; 64], [[f64; 1]; 64]) {
let proc_noise = lcg_noise(42, steps, q_true.sqrt());
let meas_noise = lcg_noise(99, steps, r_true.sqrt());
let mut y = [[0.0_f64; 1]; 64];
let u = [[0.0_f64; 1]; 64];
let mut x = 0.0_f64;
for k in 0..steps {
x += proc_noise[k]; y[k][0] = x + meas_noise[k]; }
(y, u)
}
fn build_estimator_1d() -> MlEstimator<f64, 1, 1, 64> {
MlEstimator::new(
Matrix::<f64, 1, 1>::identity(), Matrix::<f64, 1, 1>::zeros(), Matrix::<f64, 1, 1>::identity(), [0.0_f64; 1], Matrix::<f64, 1, 1> { data: [[10.0]] }, 1e-4, 1e-3, )
}
#[test]
fn ml_fit_produces_positive_diagonal_q_r() {
let (y, u) = make_data_1d(30, 0.1, 1.0);
let est = build_estimator_1d();
let (q_est, r_est) = est.fit(&y, &u, 30, 50).expect("fit");
assert!(q_est.data[0][0] > 0.0, "Q diagonal must be positive");
assert!(r_est.data[0][0] > 0.0, "R diagonal must be positive");
}
#[test]
fn ml_fit_recovers_order_of_magnitude_q_r() {
let q_true = 0.05_f64;
let r_true = 0.5_f64;
let (y, u) = make_data_1d(50, q_true, r_true);
let est = build_estimator_1d();
let (q_est, r_est) = est.fit(&y, &u, 50, 200).expect("fit");
let q_ratio = q_est.data[0][0] / q_true;
let r_ratio = r_est.data[0][0] / r_true;
assert!(
q_ratio > 0.01 && q_ratio < 200.0,
"Q estimate order of magnitude off: {q_ratio}"
);
assert!(
r_ratio > 0.01 && r_ratio < 200.0,
"R estimate order of magnitude off: {r_ratio}"
);
}
#[test]
fn batch_smoother_length_correct() {
let (y, u) = make_data_1d(20, 0.1, 1.0);
let est = build_estimator_1d();
let batch = BatchKalmanSmoother::new(est);
let out = batch
.fit_and_smooth(&y, &u, 20, 30)
.expect("fit_and_smooth");
assert_eq!(out.smoothed.len, 20);
}
#[test]
fn batch_smoother_covariance_non_negative() {
let (y, u) = make_data_1d(15, 0.1, 1.0);
let est = build_estimator_1d();
let batch = BatchKalmanSmoother::new(est);
let out = batch
.fit_and_smooth(&y, &u, 15, 30)
.expect("fit_and_smooth");
for k in 0..out.smoothed.len {
let p = out.smoothed.states[k].p.data[0][0];
assert!(
p >= 0.0,
"Smoothed covariance must be non-negative at k={k}: {p}"
);
}
}
#[test]
fn length_mismatch_returns_error() {
let (y, u) = make_data_1d(10, 0.1, 1.0);
let est = build_estimator_1d();
let res = est.fit(&y, &u, 0, 10);
assert!(matches!(res, Err(EstError::LengthMismatch)));
}
}