1use crate::errors::{ChronosError, Result};
2use crate::linalg;
3use ndarray::{Array1, Array2, Axis};
4use serde::{Deserialize, Serialize};
5
6#[derive(Serialize, Deserialize)]
10pub struct StateSpaceModel {
11 #[serde(with = "crate::utils::serde_array2")]
12 pub transition_matrix: Array2<f64>, #[serde(with = "crate::utils::serde_array2")]
14 pub selection_matrix: Array2<f64>, #[serde(with = "crate::utils::serde_array1")]
16 pub design_matrix: Array1<f64>, #[serde(with = "crate::utils::serde_array2")]
18 pub state_cov: Array2<f64>, pub obs_cov: f64, }
21
22#[derive(Serialize, Deserialize)]
23pub struct FilterStepResult {
24 #[serde(with = "crate::utils::serde_array1")]
25 pub a_prior: Array1<f64>, #[serde(with = "crate::utils::serde_array2")]
27 pub p_prior: Array2<f64>, #[serde(with = "crate::utils::serde_array1")]
29 pub a_post: Array1<f64>, #[serde(with = "crate::utils::serde_array2")]
31 pub p_post: Array2<f64>, pub v: f64, pub f: f64, #[serde(with = "crate::utils::serde_array1")]
35 pub k: Array1<f64>, }
37
38pub struct KalmanFilterResult {
39 pub steps: Vec<FilterStepResult>,
40 pub log_likelihood: f64,
41}
42
43pub struct SmootherResult {
44 pub smoothed_states: Vec<Array1<f64>>, pub smoothed_covs: Vec<Array2<f64>>, }
47
48pub struct KalmanFilter<'a> {
49 pub model: &'a StateSpaceModel,
50}
51
52impl StateSpaceModel {
53 pub fn local_linear_trend(sigma_obs: f64, sigma_level: f64, sigma_trend: f64) -> Self {
54 use ndarray::{array, Array2};
55
56 let design_matrix = array![1.0, 0.0];
58
59 let transition_matrix = array![[1.0, 1.0], [0.0, 1.0]];
61
62 let selection_matrix = Array2::eye(2);
64
65 let state_cov = array![[sigma_level.powi(2), 0.0], [0.0, sigma_trend.powi(2)]];
67
68 let obs_cov = sigma_obs.powi(2);
70
71 Self {
72 transition_matrix,
73 selection_matrix,
74 design_matrix,
75 state_cov,
76 obs_cov,
77 }
78 }
79}
80
81impl<'a> KalmanFilter<'a> {
82 pub fn new(model: &'a StateSpaceModel) -> Self {
83 Self { model }
84 }
85
86 pub fn filter(
88 &self,
89 observations: &Array1<f64>,
90 a0: Option<Array1<f64>>,
91 p0: Option<Array2<f64>>,
92 ) -> Result<KalmanFilterResult> {
93 let n = observations.len();
94 let m = self.model.design_matrix.len();
95
96 let mut a = a0.unwrap_or_else(|| Array1::zeros(m));
98 let mut p = p0.unwrap_or_else(|| {
99 let kappa = 1e6; Array2::eye(m) * kappa
101 });
102
103 let mut steps = Vec::with_capacity(n);
104 let mut log_like = 0.0;
105
106 let t = &self.model.transition_matrix;
107 let r = &self.model.selection_matrix;
108 let z = &self.model.design_matrix;
109 let q = &self.model.state_cov;
110 let h = self.model.obs_cov;
111
112 let rqr = r.dot(q).dot(&r.t());
113
114 for t_idx in 0..n {
115 let y_t = observations[t_idx];
116
117 let a_prior = a.clone();
118 let p_prior = p.clone();
119
120 let y_hat = z.dot(&a_prior);
122 let v_t = y_t - y_hat;
123
124 let f_t = z.dot(&p_prior.dot(z)) + h;
126
127 if f_t <= 0.0 {
128 return Err(ChronosError::ConvergenceFailure(
129 "Non-positive innovation variance encountered during filter pass".to_string(),
130 ));
131 }
132
133 let k_t = p_prior.dot(z) / f_t;
135
136 let a_post = &a_prior + &(&k_t * v_t);
138
139 let z_row = z.clone().insert_axis(Axis(0));
141 let k_col = k_t.clone().insert_axis(Axis(1));
142 let p_post = &p_prior - &k_col.dot(&z_row.dot(&p_prior));
143
144 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);
147
148 steps.push(FilterStepResult {
149 a_prior: a_prior.clone(),
150 p_prior: p_prior.clone(),
151 a_post: a_post.clone(),
152 p_post: p_post.clone(),
153 v: v_t,
154 f: f_t,
155 k: k_t,
156 });
157
158 a = t.dot(&a_post);
160 p = t.dot(&p_post).dot(&t.t()) + &rqr;
161 }
162
163 Ok(KalmanFilterResult {
164 steps,
165 log_likelihood: log_like,
166 })
167 }
168
169 pub fn smooth(&self, filter_result: &KalmanFilterResult) -> Result<SmootherResult> {
172 let n = filter_result.steps.len();
173 if n == 0 {
174 return Err(ChronosError::InsufficientData {
175 required: 1,
176 found: 0,
177 });
178 }
179
180 let m = self.model.design_matrix.len();
181 let t = &self.model.transition_matrix;
182
183 let mut smoothed_states = vec![Array1::zeros(m); n];
184 let mut smoothed_covs = vec![Array2::zeros((m, m)); n];
185
186 smoothed_states[n - 1] = filter_result.steps[n - 1].a_post.clone();
188 smoothed_covs[n - 1] = filter_result.steps[n - 1].p_post.clone();
189
190 for t_idx in (0..n - 1).rev() {
192 let step_curr = &filter_result.steps[t_idx];
193 let step_next = &filter_result.steps[t_idx + 1];
194
195 let p_next_pred = &step_next.p_prior;
197
198 let p_next_inv = linalg::inv(p_next_pred).map_err(ChronosError::LinalgError)?;
200 let c_t = step_curr.p_post.dot(&t.t()).dot(&p_next_inv);
201
202 let a_diff = &smoothed_states[t_idx + 1] - &step_next.a_prior;
204 smoothed_states[t_idx] = &step_curr.a_post + &c_t.dot(&a_diff);
205
206 let p_diff = &smoothed_covs[t_idx + 1] - p_next_pred;
208 smoothed_covs[t_idx] = &step_curr.p_post + &c_t.dot(&p_diff).dot(&c_t.t());
209 }
210
211 Ok(SmootherResult {
212 smoothed_states,
213 smoothed_covs,
214 })
215 }
216}