#![allow(non_snake_case)]
use nalgebra::{allocator::Allocator, stack, DefaultAllocator, Dim, Dyn, Matrix, OMatrix, OVector, RealField, StorageMut, Vector, U1};
use alloc::vec::Vec;
use crate::models::{Estimator, FunctionalObserver, FunctionalPredictor, KalmanEstimator, KalmanState};
use crate::noise::{CorrelatedNoise};
use crate::estimators::unscented::UnscentedWeights;
use num_traits::FromPrimitive;
pub struct UnscentedRootState<N: RealField, D: Dim>
where
DefaultAllocator: Allocator<D, D> + Allocator<D>,
{
pub x: OVector<N, D>,
pub S: OMatrix<N, D, D>,
pub w : UnscentedWeights<N>
}
impl<N: Copy + FromPrimitive + RealField, D: Dim> Estimator<N, D> for UnscentedRootState<N, D>
where
DefaultAllocator: Allocator<D, D> + Allocator<D> + Allocator<U1, D>,
{
fn state<'e>(&self) -> Result<OVector<N, D>, &'e str> {
Ok(self.x.clone())
}
}
impl<N: Copy + FromPrimitive + RealField, D: Dim> KalmanEstimator<N, D> for UnscentedRootState<N, D>
where
DefaultAllocator: Allocator<D, D> + Allocator<D> + Allocator<U1, D>,
{
fn kalman_state<'e>(&self) -> Result<KalmanState<N, D>, &'e str> {
Ok(KalmanState {
x : self.x.clone(),
X : &self.S * &self.S.transpose()
})
}
}
impl<N: Copy + RealField, D: Dim> UnscentedRootState<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 xx = state.x.nrows();
let mut state_chol = state.X.clone_owned();
cholesky_psd(&mut state_chol).ok_or("state covariance not PSD") ?;
Ok(UnscentedRootState {
x: state.x,
S: state_chol,
w: UnscentedWeights::new_weights(xx, alpha, beta)
})
}
fn sigma_points(&self) -> Vec<OVector<N, D>> {
let d = self.x.nrows();
let mut sigma_points = Vec::with_capacity(2 * d + 1);
sigma_points.push(self.x.clone());
for c in 0..d {
let sigmaCol = self.S.column(c) * self.w.nu;
sigma_points.push(&self.x + &sigmaCol);
sigma_points.push(&self.x - &sigmaCol);
}
sigma_points
}
}
impl<N: Copy + FromPrimitive + RealField, D: Dim> UnscentedRootState<N, D>
where
DefaultAllocator: Allocator<D, D> + Allocator<D> + Allocator<U1, D> + Allocator<D, Dyn>
{
fn predict<QD: Dim>(
&mut self,
f: impl Fn(&OVector<N, D>) -> OVector<N, D>,
noise_chol: &OMatrix<N, D, D>,
) -> Result<(), &'static str>
where
DefaultAllocator: Allocator<QD> + Allocator<D, QD>
{
let sigma_points = self.sigma_points();
let predict_points: Vec<OVector<N, D>> = sigma_points.iter()
.map(|point| f(point))
.collect();
self.x = self.w.mean(&predict_points);
let point_diff: Vec<OVector<N, D>> = predict_points.iter()
.map(|point| point - &self.x)
.collect();
let augmented_point_matrix = stack![ OMatrix::<N, D, Dyn>::from_columns(&point_diff[1..]) * self.w.W_.sqrt(), noise_chol ];
let R = augmented_point_matrix.transpose().qr().unpack_r();
let Sx: Matrix<N, D, D, _> = R.rows_generic(0, self.x.shape_generic().0).transpose();
self.S.copy_from(&Sx);
cholesky_rank_one_update(&mut self.S, &mut point_diff[0].clone_owned(), self.w.Wc0);
Ok(())
}
}
impl<N: Copy + FromPrimitive + RealField, D: Dim> FunctionalPredictor<N, D>
for UnscentedRootState<N, D>
where
DefaultAllocator: Allocator<D, D> + Allocator<D> + Allocator<U1, D> + Allocator<D, Dyn>
{
fn predict(
&mut self,
f: impl Fn(&OVector<N, D>) -> OVector<N, D>,
noise: &CorrelatedNoise<N, D>,
) -> Result<(), &'static str> {
let mut noise_chol = noise.Q.clone_owned();
cholesky_psd(&mut noise_chol).ok_or("predict noise not PSD") ?;
self.predict(f, &noise_chol)
}
}
impl<N: Copy + FromPrimitive + RealField, D: Dim> UnscentedRootState<N, D>
where
DefaultAllocator: Allocator<D, D>
+ Allocator<D>
+ Allocator<U1, D>,
{
fn observe<ZD: Dim>(
&mut self,
z: &OVector<N, ZD>,
h: impl Fn(&OVector<N, D>) -> OVector<N, ZD>,
noise_chol: &OMatrix<N, ZD, ZD>,
) -> Result<(), &'static str>
where
DefaultAllocator: Allocator<ZD> + Allocator<U1, ZD>
+ Allocator<ZD, D>
+ Allocator<ZD, Dyn>
+ Allocator<D, Dyn>
+ Allocator<D, ZD>
+ Allocator<ZD, ZD>
{
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.x)
.collect();
let Pxy = self.w.cross_covariance(&state_differences, &obs_diff);
let augmented_obs_matrix = stack![ OMatrix::<N, ZD, Dyn>::from_columns(&obs_diff[1..]) * self.w.W_.sqrt(), noise_chol ];
let R = augmented_obs_matrix.transpose().qr().unpack_r();
let mut Sy = R.rows_generic(0, z.shape_generic().0).transpose();
cholesky_rank_one_update(&mut Sy, &mut obs_diff[0].clone_owned(), self.w.Wc0);
let PxySyI = Sy.solve_lower_triangular(&Pxy.transpose()).unwrap();
let K = Sy.transpose().solve_upper_triangular(&PxySyI).unwrap().transpose();
self.x += &K * (z - predicted_obs_mean);
(K * Sy).column_iter_mut().for_each(|mut ki| {
cholesky_rank_one_update(&mut self.S, &mut ki, N::one().neg())
});
Ok(())
}
}
impl<N: Copy + FromPrimitive + RealField, D: Dim, ZD: Dim> FunctionalObserver<N, D, ZD>
for UnscentedRootState<N, D>
where
DefaultAllocator: Allocator<D, D>
+ Allocator<D>
+ Allocator<ZD>
+ Allocator<U1, ZD>
+ Allocator<ZD, D>
+ Allocator<ZD, Dyn>
+ Allocator<D, Dyn>
+ 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 mut noise_chol = noise.Q.clone_owned();
cholesky_psd(&mut noise_chol).ok_or("observe noise not PSD") ?;
self.observe(z, h, &noise_chol)
}
}
fn cholesky_psd<N: Copy + FromPrimitive + RealField, D: Dim>(matrix: &mut OMatrix<N, D, D>) -> Option<N>
where DefaultAllocator: Allocator<D, D> {
assert!(matrix.is_square(), "The input matrix must be square.");
let n = matrix.nrows();
for j in 0..n {
for k in 0..j {
let factor = unsafe { -matrix.get_unchecked((j, k)).clone() };
let (mut col_j, col_k) = matrix.columns_range_pair_mut(j, k);
let mut col_j = col_j.rows_range_mut(j..);
let col_k = col_k.rows_range(j..);
col_j.axpy(factor.conjugate(), &col_k, N::one());
}
let diag = unsafe { matrix.get_unchecked((j, j)).clone() };
if let Some(denom) = diag.try_sqrt()
{
unsafe {
*matrix.get_unchecked_mut((j, j)) = denom.clone();
}
let mut col = matrix.view_range_mut(j + 1.., j);
if denom.is_zero() {
if col.iter().any(|e| !e.is_zero()) {
return None
}
}
else {
col /= denom;
}
continue;
}
return None;
}
matrix.fill_upper_triangle(N::zero(), 1);
Some(N::one())
}
fn cholesky_rank_one_update<T: Copy + FromPrimitive + RealField, Dm, Sm, Rx, Sx>(
chol: &mut Matrix<T, Dm, Dm, Sm>,
x: &mut Vector<T, Rx, Sx>,
sigma: T::RealField,
) where
Dm: Dim,
Rx: Dim,
Sm: StorageMut<T, Dm, Dm>,
Sx: StorageMut<T, Rx, U1>,
{
let n = x.nrows();
assert_eq!(
n,
chol.nrows(),
"The input vector must be of the same size as the factorized matrix."
);
let mut beta = T::one();
for j in 0..n {
let diag = T::real(unsafe { chol.get_unchecked((j, j)).clone() });
let diag2 = diag.clone() * diag.clone();
let xj = unsafe { x.get_unchecked(j).clone() };
let sigma_xj2 = sigma.clone() * T::modulus_squared(xj.clone());
let gamma = diag2.clone() * beta.clone() + sigma_xj2.clone();
let new_diag = (diag2.clone() + sigma_xj2.clone() / beta.clone()).sqrt();
unsafe { *chol.get_unchecked_mut((j, j)) = T::from_real(new_diag.clone()) };
beta += sigma_xj2 / diag2;
let mut xjplus = x.rows_range_mut(j + 1..);
let mut col_j = chol.view_range_mut(j + 1.., j);
xjplus.axpy(-xj.clone() / T::from_real(diag.clone()), &col_j, T::one());
if !gamma.is_zero() {
col_j.axpy(
T::from_real(new_diag.clone() * sigma.clone() / gamma) * T::conjugate(xj),
&xjplus,
T::from_real(new_diag / diag),
);
}
}
}