#![cfg_attr(not(feature = "std"), no_std)]
#![allow(non_snake_case)]
#[cfg(feature = "std")]
use log::trace;
use nalgebra as na;
use nalgebra::{
allocator::Allocator, base::storage::Owned, dimension::DimMin, DefaultAllocator, Dim, DimName,
Matrix, RealField, Vector, U1,
};
use num_traits::identities::One;
#[cfg(not(feature = "std"))]
macro_rules! trace {
($e:expr) => {{}};
($e:expr, $($es:expr),+) => {{}};
}
macro_rules! debug_assert_symmetric {
($mat:expr) => {
#[cfg(debug_assertions)]
{
if approx::relative_ne!($mat, &$mat.transpose(), max_relative = na::convert(1e-5)) {
return Err(Error::CovarianceNotPositiveSemiDefinite);
}
}
};
}
#[cfg(feature = "std")]
macro_rules! pretty_print {
($arr:expr) => {{
let indent = 4;
let prefix = String::from_utf8(vec![b' '; indent]).unwrap();
let mut result_els = vec!["".to_string()];
for i in 0..$arr.nrows() {
let mut row_els = vec![];
for j in 0..$arr.ncols() {
row_els.push(format!("{:12.3}", $arr[(i, j)]));
}
let row_str = row_els.into_iter().collect::<Vec<_>>().join(" ");
let row_str = format!("{}{}", prefix, row_str);
result_els.push(row_str);
}
result_els.into_iter().collect::<Vec<_>>().join("\n")
}};
}
mod error;
pub use error::Error;
mod state_and_covariance;
pub use state_and_covariance::StateAndCovariance;
pub trait TransitionModelLinearNoControl<R, SS>
where
R: RealField,
SS: Dim,
DefaultAllocator: Allocator<SS, SS> + Allocator<SS>,
{
fn F(&self) -> &Matrix<R, SS, SS, Owned<R, SS, SS>>;
fn FT(&self) -> &Matrix<R, SS, SS, Owned<R, SS, SS>>;
fn Q(&self) -> &Matrix<R, SS, SS, Owned<R, SS, SS>>;
fn predict(&self, previous_estimate: &StateAndCovariance<R, SS>) -> StateAndCovariance<R, SS> {
let P = previous_estimate.state();
let F = self.F();
let mut state = P.clone(); F.mul_to(P, &mut state);
let covariance: Matrix<R, SS, SS, _> =
((F * previous_estimate.covariance()) * self.FT()) + self.Q();
StateAndCovariance::new(state, covariance)
}
}
pub trait ObservationModel<R, SS, OS>
where
R: RealField,
SS: Dim,
OS: Dim + DimMin<OS, Output = OS>,
DefaultAllocator: Allocator<SS, SS>
+ Allocator<SS>
+ Allocator<OS, SS>
+ Allocator<SS, OS>
+ Allocator<OS, OS>
+ Allocator<OS>,
Matrix<R, SS, SS, Owned<R, SS, SS>>: One,
{
fn predict_observation(
&self,
state: &Vector<R, SS, Owned<R, SS>>,
) -> Vector<R, OS, Owned<R, OS>> {
self.H() * state
}
fn H(&self) -> &Matrix<R, OS, SS, Owned<R, OS, SS>>;
fn HT(&self) -> &Matrix<R, SS, OS, Owned<R, SS, OS>>;
fn R(&self) -> &Matrix<R, OS, OS, Owned<R, OS, OS>>;
fn update(
&self,
prior: &StateAndCovariance<R, SS>,
observation: &Vector<R, OS, Owned<R, OS>>,
covariance_method: CovarianceUpdateMethod,
) -> Result<StateAndCovariance<R, SS>, Error> {
let h = self.H();
trace!("h {}", pretty_print!(h));
let p = prior.covariance();
trace!("p {}", pretty_print!(p));
debug_assert_symmetric!(p);
let ht = self.HT();
trace!("ht {}", pretty_print!(ht));
let r = self.R();
trace!("r {}", pretty_print!(r));
let s = (h * p * ht) + r;
trace!("s {}", pretty_print!(s));
let s_chol = match na::linalg::Cholesky::new(s) {
Some(v) => v,
None => {
return Err(Error::CovarianceNotPositiveSemiDefinite);
}
};
let s_inv: Matrix<R, OS, OS, _> = s_chol.inverse();
trace!("s_inv {}", pretty_print!(s_inv));
let k_gain: Matrix<R, SS, OS, _> = p * ht * s_inv;
trace!("k_gain {}", pretty_print!(k_gain));
let predicted: Vector<R, OS, _> = self.predict_observation(prior.state());
trace!("predicted {}", pretty_print!(predicted));
trace!("observation {}", pretty_print!(observation));
let innovation: Vector<R, OS, _> = observation - predicted;
trace!("innovation {}", pretty_print!(innovation));
let state: Vector<R, SS, _> = prior.state() + &k_gain * innovation;
trace!("state {}", pretty_print!(state));
trace!("self.observation_matrix() {}", pretty_print!(self.H()));
let kh: Matrix<R, SS, SS, _> = &k_gain * self.H();
trace!("kh {}", pretty_print!(kh));
let one_minus_kh = Matrix::<R, SS, SS, Owned<R, SS, SS>>::one() - kh;
trace!("one_minus_kh {}", pretty_print!(one_minus_kh));
let covariance: Matrix<R, SS, SS, _> = match covariance_method {
CovarianceUpdateMethod::JosephForm => {
let left = &one_minus_kh * prior.covariance() * one_minus_kh.transpose();
let right = &k_gain * r * &k_gain.transpose();
left + right
}
CovarianceUpdateMethod::OptimalKalman => &one_minus_kh * prior.covariance(),
CovarianceUpdateMethod::OptimalKalmanForcedSymmetric => {
let covariance1 = &one_minus_kh * prior.covariance();
trace!("covariance1 {}", pretty_print!(covariance1));
covariance1.symmetric_part()
}
};
trace!("covariance {}", pretty_print!(covariance));
debug_assert_symmetric!(covariance);
Ok(StateAndCovariance::new(state, covariance))
}
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum CovarianceUpdateMethod {
OptimalKalman,
OptimalKalmanForcedSymmetric,
JosephForm,
}
pub struct KalmanFilterNoControl<'a, R, SS, OS>
where
R: RealField,
SS: Dim,
OS: Dim,
{
transition_model: &'a dyn TransitionModelLinearNoControl<R, SS>,
observation_matrix: &'a dyn ObservationModel<R, SS, OS>,
}
impl<'a, R, SS, OS> KalmanFilterNoControl<'a, R, SS, OS>
where
R: RealField,
SS: DimName,
OS: Dim + DimMin<OS, Output = OS>,
DefaultAllocator: Allocator<SS, SS>
+ Allocator<SS>
+ Allocator<OS, SS>
+ Allocator<SS, OS>
+ Allocator<OS, OS>
+ Allocator<OS>,
{
pub fn new(
transition_model: &'a dyn TransitionModelLinearNoControl<R, SS>,
observation_matrix: &'a dyn ObservationModel<R, SS, OS>,
) -> Self {
Self {
transition_model,
observation_matrix,
}
}
pub fn step(
&self,
previous_estimate: &StateAndCovariance<R, SS>,
observation: &Vector<R, OS, Owned<R, OS>>,
) -> Result<StateAndCovariance<R, SS>, Error> {
self.step_with_options(
previous_estimate,
observation,
CovarianceUpdateMethod::JosephForm,
)
}
pub fn step_with_options(
&self,
previous_estimate: &StateAndCovariance<R, SS>,
observation: &Vector<R, OS, Owned<R, OS>>,
covariance_update_method: CovarianceUpdateMethod,
) -> Result<StateAndCovariance<R, SS>, Error> {
let prior = self.transition_model.predict(previous_estimate);
if observation.iter().any(|x| is_nan(x.clone())) {
Ok(prior)
} else {
self.observation_matrix
.update(&prior, observation, covariance_update_method)
}
}
pub fn filter_inplace(
&self,
initial_estimate: &StateAndCovariance<R, SS>,
observations: &[Vector<R, OS, Owned<R, OS>>],
state_estimates: &mut [StateAndCovariance<R, SS>],
) -> Result<(), Error> {
let mut previous_estimate = initial_estimate.clone();
assert!(state_estimates.len() >= observations.len());
for (this_observation, state_estimate) in
observations.iter().zip(state_estimates.iter_mut())
{
let this_observation: &Matrix<R, OS, U1, Owned<R, OS>> = this_observation;
let this_estimate: StateAndCovariance<R, SS> =
self.step(&previous_estimate, this_observation)?;
*state_estimate = this_estimate.clone();
previous_estimate = this_estimate;
}
Ok(())
}
#[cfg(feature = "std")]
pub fn filter(
&self,
initial_estimate: &StateAndCovariance<R, SS>,
observations: &[Vector<R, OS, Owned<R, OS>>],
) -> Result<Vec<StateAndCovariance<R, SS>>, Error> {
use nalgebra::OMatrix;
let mut state_estimates = Vec::with_capacity(observations.len());
let empty: StateAndCovariance<R, SS> = StateAndCovariance::new(na::zero(), OMatrix::one());
for _ in 0..observations.len() {
state_estimates.push(empty.clone());
}
self.filter_inplace(initial_estimate, observations, &mut state_estimates)?;
Ok(state_estimates)
}
#[cfg(feature = "std")]
pub fn smooth(
&self,
initial_estimate: &StateAndCovariance<R, SS>,
observations: &[Vector<R, OS, Owned<R, OS>>],
) -> Result<Vec<StateAndCovariance<R, SS>>, Error> {
let forward_results = self.filter(initial_estimate, observations)?;
self.smooth_from_filtered(forward_results)
}
#[cfg(feature = "std")]
pub fn smooth_from_filtered(
&self,
mut forward_results: Vec<StateAndCovariance<R, SS>>,
) -> Result<Vec<StateAndCovariance<R, SS>>, Error> {
forward_results.reverse();
let mut smoothed_backwards = Vec::with_capacity(forward_results.len());
let mut smooth_future = forward_results[0].clone();
smoothed_backwards.push(smooth_future.clone());
for filt in forward_results.iter().skip(1) {
smooth_future = self.smooth_step(&smooth_future, filt)?;
smoothed_backwards.push(smooth_future.clone());
}
smoothed_backwards.reverse();
Ok(smoothed_backwards)
}
#[cfg(feature = "std")]
fn smooth_step(
&self,
smooth_future: &StateAndCovariance<R, SS>,
filt: &StateAndCovariance<R, SS>,
) -> Result<StateAndCovariance<R, SS>, Error> {
let prior = self.transition_model.predict(filt);
let v_chol = match na::linalg::Cholesky::new(prior.covariance().clone()) {
Some(v) => v,
None => {
return Err(Error::CovarianceNotPositiveSemiDefinite);
}
};
let inv_prior_covariance: Matrix<R, SS, SS, _> = v_chol.inverse();
trace!(
"inv_prior_covariance {}",
pretty_print!(inv_prior_covariance)
);
let j = filt.covariance() * (self.transition_model.FT() * inv_prior_covariance);
let residuals = smooth_future.state() - prior.state();
let state = filt.state() + &j * residuals;
let covar_residuals = smooth_future.covariance() - prior.covariance();
let covariance = filt.covariance() + &j * (covar_residuals * j.transpose());
Ok(StateAndCovariance::new(state, covariance))
}
}
#[inline]
fn is_nan<R: RealField>(x: R) -> bool {
x.partial_cmp(&R::zero()).is_none()
}
#[test]
fn test_is_nan() {
assert!(!is_nan::<f64>(-1.0));
assert!(!is_nan::<f64>(0.0));
assert!(!is_nan::<f64>(1.0));
assert!(!is_nan::<f64>(1.0 / 0.0));
assert!(!is_nan::<f64>(-1.0 / 0.0));
assert!(is_nan::<f64>(std::f64::NAN));
assert!(!is_nan::<f32>(-1.0));
assert!(!is_nan::<f32>(0.0));
assert!(!is_nan::<f32>(1.0));
assert!(!is_nan::<f32>(1.0 / 0.0));
assert!(!is_nan::<f32>(-1.0 / 0.0));
assert!(is_nan::<f32>(std::f32::NAN));
}