#![allow(non_snake_case)]
use nalgebra::{allocator::Allocator, Cholesky, DefaultAllocator, Dim, OMatrix, OVector, RealField, U1};
use alloc::vec::Vec;
use crate::models::{Estimator, FunctionalObserver, FunctionalPredictor, KalmanEstimator, KalmanState};
use crate::noise::CorrelatedNoise;
use num_traits::FromPrimitive;
pub struct UnscentedState<N: RealField, D: Dim>
where
DefaultAllocator: Allocator<D, D> + Allocator<D>,
{
pub kalman: KalmanState<N, D>,
pub w: UnscentedWeights<N>,
}
pub struct UnscentedWeights<N: RealField> {
pub nu: N,
pub Wm0: N,
pub Wc0: N,
pub W_: N
}
impl<N: Copy + RealField> UnscentedWeights<N> {
pub fn new_weights(dd: usize, alpha: N, beta: N) -> Self {
let d = N::from_usize(dd).unwrap();
let lamda = d * alpha * alpha - d;
let nu = (d + lamda).sqrt();
let Wm0 = lamda / (d + lamda);
let Wc0 = Wm0 + (N::one() - alpha * alpha + beta);
let two = N::from_usize(2).unwrap();
let W_ = (two * (d + lamda)).recip();
UnscentedWeights { nu, Wm0, Wc0, W_ }
}
}
impl<N: Copy + RealField> UnscentedWeights<N>
{
pub fn mean<PD: Dim>(&self, points: &[OVector<N, PD>]) -> OVector<N, PD>
where DefaultAllocator: Allocator<PD> {
let mut pi = points.iter();
let mut mean = pi.next().unwrap() * self.Wm0;
pi.for_each(|point| mean += point * self.W_);
mean
}
pub fn cross_covariance<AD: Dim, BD: Dim>(
&self,
a: &[OVector<N, AD>],
b: &[OVector<N, BD>],
) -> OMatrix<N, AD, BD> where DefaultAllocator: Allocator<AD> + Allocator<BD> + Allocator<U1, BD> + Allocator<AD, BD> {
let mut zipi = a.iter().zip(b);
let point0 = zipi.next().unwrap();
let mut cc = point0.0 * point0.1.transpose() * self.Wc0;
zipi.for_each(|point| cc += point.0 * point.1.transpose() * self.W_);
cc
}
}
impl<N: Copy + FromPrimitive + RealField, D: Dim> Estimator<N, D>
for UnscentedState<N, D>
where
DefaultAllocator: Allocator<D, D> + Allocator<D> + Allocator<U1, D>,
{
fn state<'e>(&self) -> Result<OVector<N, D>, &'e str> {
Ok(self.kalman.x.clone())
}
}
impl<N: Copy + FromPrimitive + RealField, D: Dim> KalmanEstimator<N, D>
for UnscentedState<N, D>
where
DefaultAllocator: Allocator<D, D> + Allocator<D> + Allocator<U1, D>,
{
fn kalman_state<'e>(&self) -> Result<KalmanState<N, D>, &'e str> {
Ok(self.kalman.clone())
}
}
impl<N: Copy + RealField, D: Dim> UnscentedState<N, D>
where
DefaultAllocator: Allocator<D, D> + Allocator<D>,
{
pub fn from_kalman_state(state: KalmanState<N, D>, alpha: N, beta: N) -> Result<Self, &'static str> {
let d = state.x.nrows();
Ok(UnscentedState {
kalman: state,
w: UnscentedWeights::new_weights(d, alpha, beta)
})
}
fn sigma_points(&self) -> Result<Vec<OVector<N, D>>, &'static str> {
let d = self.kalman.x.nrows();
let mut sigma_points = Vec::with_capacity(2 * d + 1);
let sigma = self.kalman.X.clone()
.cholesky()
.ok_or("sigma_point, X not PSD")?
.l();
sigma_points.push(self.kalman.x.clone());
for c in 0..d {
let sigmaCol = sigma.column(c) * self.w.nu;
sigma_points.push(&self.kalman.x + &sigmaCol);
sigma_points.push(&self.kalman.x - &sigmaCol);
}
Ok(sigma_points)
}
}
impl<N: Copy + FromPrimitive + RealField, D: Dim> FunctionalPredictor<N, D>
for UnscentedState<N, D>
where
DefaultAllocator: Allocator<D, D> + Allocator<D> + Allocator<U1, D>,
{
fn predict(
&mut self,
f: impl Fn(&OVector<N, D>) -> OVector<N, D>,
noise: &CorrelatedNoise<N, D>,
) -> Result<(), &'static str> {
let sigma_points = self.sigma_points()?;
let predict_points: Vec<OVector<N, D>> = sigma_points.iter()
.map(|point| f(point))
.collect();
self.kalman.x = self.w.mean(&predict_points);
let point_diff: Vec<OVector<N, D>> = predict_points.iter()
.map(|point| point - &self.kalman.x)
.collect();
self.kalman.X = self.w.cross_covariance(&point_diff, &point_diff) + &noise.Q;
Ok(())
}
}
impl<N: Copy + FromPrimitive + RealField, D: Dim, ZD: Dim> FunctionalObserver<N, D, ZD>
for UnscentedState<N, D>
where
DefaultAllocator: Allocator<D, D>
+ Allocator<D>
+ Allocator<ZD>
+ Allocator<U1, ZD>
+ Allocator<ZD, D>
+ Allocator<D, ZD>
+ Allocator<ZD, ZD>
+ Allocator<U1, D>,
{
fn observe(
&mut self,
z: &OVector<N, ZD>,
h: impl Fn(&OVector<N, D>) -> OVector<N, ZD>,
noise: &CorrelatedNoise<N, ZD>,
) -> Result<(), &'static str> {
let sigma_points = self.sigma_points()?;
let predicted_obs: Vec<OVector<N, ZD>> = sigma_points.iter()
.map(|point| h(point))
.collect();
let predicted_obs_mean = self.w.mean(&predicted_obs);
let obs_diff: Vec<OVector<N, ZD>> = predicted_obs.iter()
.map(|point| point - &predicted_obs_mean)
.collect();
let state_differences: Vec<OVector<N, D>> = sigma_points.iter()
.map(|point| point - &self.kalman.x)
.collect();
let S = self.w.cross_covariance(&obs_diff, &obs_diff) + &noise.Q;
let SI = S.clone().cholesky().ok_or("S not PD in observe")?.inverse();
let W = self.w.cross_covariance(&state_differences, &obs_diff) * &SI;
self.kalman.x += &W * (z - predicted_obs_mean);
self.kalman.X.quadform_tr(N::one().neg(), &W, &S, N::one());
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct ObserveInnovation<N: RealField, ZD: Dim>
where
DefaultAllocator: Allocator<ZD> + Allocator<ZD, ZD> + Allocator<U1, ZD>,
{
pub s: OVector<N, ZD>,
pub S: OMatrix<N, ZD, ZD>,
pub S_chol: Cholesky<N, ZD>,
pub SI: OMatrix<N, ZD, ZD>,
}
impl<N: Copy + FromPrimitive + RealField, D: Dim> UnscentedState<N, D>
where
DefaultAllocator: Allocator<D, D>
+ Allocator<D>
+ Allocator<U1, D>,
{
pub fn observe_with_innovation<ZD: Dim>(
&mut self,
z: &OVector<N, ZD>,
h: impl Fn(&OVector<N, D>) -> OVector<N, ZD>,
noise: &CorrelatedNoise<N, ZD>,
) -> Result<ObserveInnovation<N, ZD>, &'static str>
where
DefaultAllocator: Allocator<ZD>
+ Allocator<U1, ZD>
+ Allocator<ZD, D>
+ Allocator<D, ZD>
+ Allocator<ZD, ZD>
+ Allocator<U1, D>,
{
let sigma_points = self.sigma_points()?;
let predicted_obs: Vec<OVector<N, ZD>> = sigma_points.iter().map(|point| h(point)).collect();
let predicted_obs_mean = self.w.mean(&predicted_obs);
let obs_diff: Vec<OVector<N, ZD>> = predicted_obs.iter().map(|point| point - &predicted_obs_mean).collect();
let state_differences: Vec<OVector<N, D>> = sigma_points.iter().map(|point| point - &self.kalman.x).collect();
let S = self.w.cross_covariance(&obs_diff, &obs_diff) + &noise.Q;
let S_chol = S.clone().cholesky().ok_or("S not PD in observe")?;
let SI = S_chol.inverse();
let s = z - &predicted_obs_mean;
let W = self.w.cross_covariance(&state_differences, &obs_diff) * &SI;
self.kalman.x += &W * &s;
self.kalman.X.quadform_tr(N::one().neg(), &W, &S, N::one());
Ok(ObserveInnovation { s, S, S_chol, SI })
}
}