use crate::core::matrix::{matmul, matvec, Matrix};
use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FixedIntervalError {
SingularMatrix,
BufferFull,
InsufficientData,
}
impl core::fmt::Display for FixedIntervalError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
FixedIntervalError::SingularMatrix => {
write!(f, "FixedIntervalSmoother: singular matrix")
}
FixedIntervalError::BufferFull => write!(f, "FixedIntervalSmoother: buffer full"),
FixedIntervalError::InsufficientData => {
write!(f, "FixedIntervalSmoother: insufficient data")
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct FisSlot<S: ControlScalar, const N: usize, const M: usize> {
pub x_post: [S; N],
pub p_post: Matrix<S, N, N>,
pub x_pred: [S; N],
pub p_pred: Matrix<S, N, N>,
pub z: [S; M],
}
impl<S: ControlScalar, const N: usize, const M: usize> FisSlot<S, N, M> {
pub fn new(
x_post: [S; N],
p_post: Matrix<S, N, N>,
x_pred: [S; N],
p_pred: Matrix<S, N, N>,
z: [S; M],
) -> Self {
Self {
x_post,
p_post,
x_pred,
p_pred,
z,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct FisSmoothed<S: ControlScalar, const N: usize> {
pub x: [S; N],
pub p: Matrix<S, N, N>,
}
#[derive(Debug, Clone, Copy)]
pub struct FisSmoothedData<S: ControlScalar, const N: usize, const T: usize> {
pub states: [FisSmoothed<S, N>; T],
pub len: usize,
}
#[derive(Debug, Clone, Copy)]
pub struct FixedIntervalSmoother<S: ControlScalar, const N: usize, const M: usize, const T: usize> {
buffer: [FisSlot<S, N, M>; T],
count: usize,
h: Matrix<S, M, N>,
q: Matrix<S, N, N>,
r: Matrix<S, M, M>,
}
impl<S: ControlScalar, const N: usize, const M: usize, const T: usize>
FixedIntervalSmoother<S, N, M, T>
{
pub fn new(h: Matrix<S, M, N>, q: Matrix<S, N, N>, r: Matrix<S, M, M>) -> Self {
let zero_slot = FisSlot {
x_post: [S::ZERO; N],
p_post: Matrix::zeros(),
x_pred: [S::ZERO; N],
p_pred: Matrix::zeros(),
z: [S::ZERO; M],
};
Self {
buffer: [zero_slot; T],
count: 0,
h,
q,
r,
}
}
pub fn reset(&mut self) {
self.count = 0;
}
pub fn store_slot(&mut self, slot: FisSlot<S, N, M>) -> Result<(), FixedIntervalError> {
if self.count >= T {
return Err(FixedIntervalError::BufferFull);
}
self.buffer[self.count] = slot;
self.count += 1;
Ok(())
}
pub fn len(&self) -> usize {
self.count
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
pub fn process_noise_cov(&self) -> &Matrix<S, N, N> {
&self.q
}
pub fn smooth(
&self,
a: &Matrix<S, N, N>,
) -> Result<FisSmoothedData<S, N, T>, FixedIntervalError> {
let n = self.count;
if n == 0 {
return Err(FixedIntervalError::InsufficientData);
}
let zero_s = FisSmoothed {
x: [S::ZERO; N],
p: Matrix::zeros(),
};
let mut out = FisSmoothedData {
states: [zero_s; T],
len: n,
};
let r_inv = self.r.inv().ok_or(FixedIntervalError::SingularMatrix)?;
let ht = self.h.transpose();
let ht_rinv = matmul(&ht, &r_inv);
let ht_rinv_h: Matrix<S, N, N> = matmul(&ht_rinv, &self.h);
let at = a.transpose();
let mut lambda_vec: [S; N] = {
let z_last = self.buffer[n - 1].z;
let rinv_z = matvec(&r_inv, &z_last);
matvec(&ht, &rinv_z)
};
out.states[n - 1] = FisSmoothed {
x: self.buffer[n - 1].x_post,
p: self.buffer[n - 1].p_post,
};
if n == 1 {
return Ok(out);
}
for k in (0..n - 1).rev() {
let p_pred_kp1 = &self.buffer[k].p_pred;
let m = p_pred_kp1.inv().ok_or(FixedIntervalError::SingularMatrix)?;
let at_m = matmul(&at, &m);
let at_m_a = matmul(&at_m, a);
let lambda_k = ht_rinv_h.add_mat(&at_m_a);
let z_k = self.buffer[k].z;
let rinv_zk = matvec(&r_inv, &z_k);
let xi_k = matvec(&ht, &rinv_zk);
let at_m_lam = matvec(&at_m, &lambda_vec);
let lambda_k_vec: [S; N] = core::array::from_fn(|i| xi_k[i] + at_m_lam[i]);
let p_post = &self.buffer[k].p_post;
let p_post_inv = p_post.inv().ok_or(FixedIntervalError::SingularMatrix)?;
let p_smooth_inv = p_post_inv.add_mat(&lambda_k);
let p_smooth = p_smooth_inv
.inv()
.ok_or(FixedIntervalError::SingularMatrix)?;
let p_post_inv_x = matvec(&p_post_inv, &self.buffer[k].x_post);
let rhs: [S; N] = core::array::from_fn(|i| p_post_inv_x[i] + lambda_k_vec[i]);
let x_smooth = matvec(&p_smooth, &rhs);
out.states[k] = FisSmoothed {
x: x_smooth,
p: p_smooth,
};
lambda_vec = lambda_k_vec;
}
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::matrix::matmul;
fn run_forward_1d(
steps: usize,
q_val: f64,
r_val: f64,
) -> FixedIntervalSmoother<f64, 1, 1, 32> {
let a = Matrix::<f64, 1, 1>::identity();
let h = Matrix::<f64, 1, 1>::identity();
let q = Matrix::<f64, 1, 1> { data: [[q_val]] };
let r = Matrix::<f64, 1, 1> { data: [[r_val]] };
let mut fis = FixedIntervalSmoother::<f64, 1, 1, 32>::new(h, q, r);
let mut x = [0.0_f64; 1];
let mut p = Matrix::<f64, 1, 1> { data: [[10.0]] };
let measurement = 5.0_f64;
for _ in 0..steps {
let x_pred = x;
let ap = matmul(&a, &p);
let at = a.transpose();
let apat = matmul(&ap, &at);
let p_pred = apat.add_mat(&q);
let hx = [x_pred[0]];
let innov = [measurement - hx[0]];
let hp = matmul(&h, &p_pred);
let ht = h.transpose();
let hpht = matmul(&hp, &ht);
let s_mat = hpht.add_mat(&r);
let s_inv = s_mat.inv().expect("S invertible");
let pht = matmul(&p_pred, &ht);
let k = matmul(&pht, &s_inv);
let kv = crate::core::matrix::matvec(&k, &innov);
let x_post: [f64; 1] = core::array::from_fn(|i| x_pred[i] + kv[i]);
let kh = matmul(&k, &h);
let eye = Matrix::<f64, 1, 1>::identity();
let i_kh = eye.sub_mat(&kh);
let p_post = matmul(&i_kh, &p_pred);
fis.store_slot(FisSlot::new(x_post, p_post, x_pred, p_pred, [measurement]))
.expect("store");
x = x_post;
p = p_post;
}
fis
}
#[test]
fn smoother_variance_less_than_filter_variance() {
let steps = 10_usize;
let fis = run_forward_1d(steps, 1e-4, 1.0);
let a = Matrix::<f64, 1, 1>::identity();
let smoothed = fis.smooth(&a).expect("smooth");
for k in 0..steps {
let p_filter = fis.buffer[k].p_post.trace();
let p_smooth = smoothed.states[k].p.trace();
assert!(
p_smooth <= p_filter + 1e-9,
"Smoothed variance must be ≤ filtered at k={k}: \
p_smooth={p_smooth}, p_filter={p_filter}"
);
}
}
#[test]
fn single_step_smoother_equals_filter() {
let fis = run_forward_1d(1, 1e-4, 1.0);
let a = Matrix::<f64, 1, 1>::identity();
let smoothed = fis.smooth(&a).expect("smooth");
assert_eq!(smoothed.len, 1);
let x_filter = fis.buffer[0].x_post[0];
let x_smooth = smoothed.states[0].x[0];
assert!(
(x_filter - x_smooth).abs() < 1e-9,
"Single step: smooth={x_smooth} must equal filter={x_filter}"
);
}
#[test]
fn empty_smoother_returns_error() {
let h = Matrix::<f64, 1, 1>::identity();
let q = Matrix::<f64, 1, 1> { data: [[1e-4]] };
let r = Matrix::<f64, 1, 1> { data: [[1.0]] };
let fis = FixedIntervalSmoother::<f64, 1, 1, 8>::new(h, q, r);
let a = Matrix::<f64, 1, 1>::identity();
assert!(matches!(
fis.smooth(&a),
Err(FixedIntervalError::InsufficientData)
));
}
#[test]
fn buffer_full_error() {
let h = Matrix::<f64, 1, 1>::identity();
let q = Matrix::<f64, 1, 1> { data: [[1e-4]] };
let r = Matrix::<f64, 1, 1> { data: [[1.0]] };
let mut fis = FixedIntervalSmoother::<f64, 1, 1, 2>::new(h, q, r);
let slot = FisSlot::new(
[0.0_f64; 1],
Matrix::identity(),
[0.0_f64; 1],
Matrix::identity(),
[0.0_f64; 1],
);
fis.store_slot(slot).expect("slot 1");
fis.store_slot(slot).expect("slot 2");
assert!(matches!(
fis.store_slot(slot),
Err(FixedIntervalError::BufferFull)
));
}
#[test]
fn smoothed_len_matches_stored_count() {
let fis = run_forward_1d(12, 1e-4, 0.5);
let a = Matrix::<f64, 1, 1>::identity();
let smoothed = fis.smooth(&a).expect("smooth");
assert_eq!(smoothed.len, 12);
}
}