use crate::errors::{ChronosError, Result};
use crate::linalg;
use ndarray::{Array1, Array2, Axis};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct StateSpaceModel {
#[serde(with = "crate::utils::serde_array2")]
pub transition_matrix: Array2<f64>, #[serde(with = "crate::utils::serde_array2")]
pub selection_matrix: Array2<f64>, #[serde(with = "crate::utils::serde_array1")]
pub design_matrix: Array1<f64>, #[serde(with = "crate::utils::serde_array2")]
pub state_cov: Array2<f64>, pub obs_cov: f64, }
#[derive(Serialize, Deserialize)]
pub struct FilterStepResult {
#[serde(with = "crate::utils::serde_array1")]
pub a_prior: Array1<f64>, #[serde(with = "crate::utils::serde_array2")]
pub p_prior: Array2<f64>, #[serde(with = "crate::utils::serde_array1")]
pub a_post: Array1<f64>, #[serde(with = "crate::utils::serde_array2")]
pub p_post: Array2<f64>, pub v: f64, pub f: f64, #[serde(with = "crate::utils::serde_array1")]
pub k: Array1<f64>, }
pub struct KalmanFilterResult {
pub steps: Vec<FilterStepResult>,
pub log_likelihood: f64,
}
pub struct SmootherResult {
pub smoothed_states: Vec<Array1<f64>>, pub smoothed_covs: Vec<Array2<f64>>, }
pub struct KalmanFilter<'a> {
pub model: &'a StateSpaceModel,
}
impl StateSpaceModel {
pub fn local_linear_trend(sigma_obs: f64, sigma_level: f64, sigma_trend: f64) -> Self {
use ndarray::{array, Array2};
let design_matrix = array![1.0, 0.0];
let transition_matrix = array![[1.0, 1.0], [0.0, 1.0]];
let selection_matrix = Array2::eye(2);
let state_cov = array![[sigma_level.powi(2), 0.0], [0.0, sigma_trend.powi(2)]];
let obs_cov = sigma_obs.powi(2);
Self {
transition_matrix,
selection_matrix,
design_matrix,
state_cov,
obs_cov,
}
}
}
impl<'a> KalmanFilter<'a> {
pub fn new(model: &'a StateSpaceModel) -> Self {
Self { model }
}
pub fn filter(
&self,
observations: &Array1<f64>,
a0: Option<Array1<f64>>,
p0: Option<Array2<f64>>,
) -> Result<KalmanFilterResult> {
let n = observations.len();
let m = self.model.design_matrix.len();
let mut a = a0.unwrap_or_else(|| Array1::zeros(m));
let mut p = p0.unwrap_or_else(|| {
let kappa = 1e6; Array2::eye(m) * kappa
});
let mut steps = Vec::with_capacity(n);
let mut log_like = 0.0;
let t = &self.model.transition_matrix;
let r = &self.model.selection_matrix;
let z = &self.model.design_matrix;
let q = &self.model.state_cov;
let h = self.model.obs_cov;
let rqr = r.dot(q).dot(&r.t());
for t_idx in 0..n {
let y_t = observations[t_idx];
let a_prior = a.clone();
let p_prior = p.clone();
let y_hat = z.dot(&a_prior);
let v_t = y_t - y_hat;
let f_t = z.dot(&p_prior.dot(z)) + h;
if f_t <= 0.0 {
return Err(ChronosError::ConvergenceFailure(
"Non-positive innovation variance encountered during filter pass".to_string(),
));
}
let k_t = p_prior.dot(z) / f_t;
let a_post = &a_prior + &(&k_t * v_t);
let z_row = z.clone().insert_axis(Axis(0));
let k_col = k_t.clone().insert_axis(Axis(1));
let p_post = &p_prior - &k_col.dot(&z_row.dot(&p_prior));
let ln_2_pi = std::f64::consts::TAU.ln(); log_like -= 0.5 * (ln_2_pi + f_t.ln() + (v_t * v_t) / f_t);
steps.push(FilterStepResult {
a_prior: a_prior.clone(),
p_prior: p_prior.clone(),
a_post: a_post.clone(),
p_post: p_post.clone(),
v: v_t,
f: f_t,
k: k_t,
});
a = t.dot(&a_post);
p = t.dot(&p_post).dot(&t.t()) + &rqr;
}
Ok(KalmanFilterResult {
steps,
log_likelihood: log_like,
})
}
pub fn smooth(&self, filter_result: &KalmanFilterResult) -> Result<SmootherResult> {
let n = filter_result.steps.len();
if n == 0 {
return Err(ChronosError::InsufficientData {
required: 1,
found: 0,
});
}
let m = self.model.design_matrix.len();
let t = &self.model.transition_matrix;
let mut smoothed_states = vec![Array1::zeros(m); n];
let mut smoothed_covs = vec![Array2::zeros((m, m)); n];
smoothed_states[n - 1] = filter_result.steps[n - 1].a_post.clone();
smoothed_covs[n - 1] = filter_result.steps[n - 1].p_post.clone();
for t_idx in (0..n - 1).rev() {
let step_curr = &filter_result.steps[t_idx];
let step_next = &filter_result.steps[t_idx + 1];
let p_next_pred = &step_next.p_prior;
let p_next_inv = linalg::inv(p_next_pred).map_err(ChronosError::LinalgError)?;
let c_t = step_curr.p_post.dot(&t.t()).dot(&p_next_inv);
let a_diff = &smoothed_states[t_idx + 1] - &step_next.a_prior;
smoothed_states[t_idx] = &step_curr.a_post + &c_t.dot(&a_diff);
let p_diff = &smoothed_covs[t_idx + 1] - p_next_pred;
smoothed_covs[t_idx] = &step_curr.p_post + &c_t.dot(&p_diff).dot(&c_t.t());
}
Ok(SmootherResult {
smoothed_states,
smoothed_covs,
})
}
}