use nalgebra::RealField;
use num_traits::Float;
use crate::types::spaces::{StateCovariance, StateVector};
use crate::types::transforms::TransitionMatrix;
pub trait TransitionModel<T: RealField, const N: usize> {
fn transition_matrix(&self, dt: T) -> TransitionMatrix<T, N>;
fn process_noise(&self, dt: T) -> StateCovariance<T, N>;
fn survival_probability(&self, state: &StateVector<T, N>) -> T;
}
pub trait NonlinearTransitionModel<T: RealField, const N: usize>: TransitionModel<T, N> {
fn predict_nonlinear(&self, state: &StateVector<T, N>, dt: T) -> StateVector<T, N>;
fn jacobian_at(&self, state: &StateVector<T, N>, dt: T) -> TransitionMatrix<T, N>;
}
#[derive(Debug, Clone)]
pub struct ConstantVelocity2D<T: RealField> {
pub noise_diff_coeff: T,
pub p_survival: T,
}
impl<T: RealField + Float + Copy> ConstantVelocity2D<T> {
pub fn new(noise_diff_coeff: T, p_survival: T) -> Self {
assert!(
noise_diff_coeff >= T::zero(),
"Process noise noise_diff_coeff must be non-negative"
);
assert!(
p_survival >= T::zero() && p_survival <= T::one(),
"Survival probability must be in [0, 1]"
);
Self {
noise_diff_coeff,
p_survival,
}
}
}
impl<T: RealField + Float + Copy> TransitionModel<T, 4> for ConstantVelocity2D<T> {
fn transition_matrix(&self, dt: T) -> TransitionMatrix<T, 4> {
assert!(dt >= T::zero(), "Time step dt must be non-negative");
let one = T::one();
let zero = T::zero();
TransitionMatrix::from_matrix(nalgebra::matrix![
one, zero, dt, zero;
zero, one, zero, dt;
zero, zero, one, zero;
zero, zero, zero, one
])
}
fn process_noise(&self, dt: T) -> StateCovariance<T, 4> {
assert!(dt >= T::zero(), "Time step dt must be non-negative");
let dt2 = dt * dt;
let dt3 = dt2 * dt;
let two = T::from_f64(2.0).unwrap();
let three = T::from_f64(3.0).unwrap();
let q = self.noise_diff_coeff;
let q_pp = dt3 / three * q; let q_pv = dt2 / two * q; let q_vv = dt * q;
let zero = T::zero();
StateCovariance::from_matrix(nalgebra::matrix![
q_pp, zero, q_pv, zero;
zero, q_pp, zero, q_pv;
q_pv, zero, q_vv, zero;
zero, q_pv, zero, q_vv
])
}
fn survival_probability(&self, _state: &StateVector<T, 4>) -> T {
self.p_survival
}
}
#[derive(Debug, Clone)]
pub struct ConstantVelocity3D<T: RealField> {
pub noise_diff_coeff: T,
pub p_survival: T,
}
impl<T: RealField + Float + Copy> ConstantVelocity3D<T> {
pub fn new(noise_diff_coeff: T, p_survival: T) -> Self {
assert!(
noise_diff_coeff >= T::zero(),
"Process noise noise_diff_coeff must be non-negative"
);
assert!(
p_survival >= T::zero() && p_survival <= T::one(),
"Survival probability must be in [0, 1]"
);
Self {
noise_diff_coeff,
p_survival,
}
}
}
impl<T: RealField + Float + Copy> TransitionModel<T, 6> for ConstantVelocity3D<T> {
fn transition_matrix(&self, dt: T) -> TransitionMatrix<T, 6> {
assert!(dt >= T::zero(), "Time step dt must be non-negative");
let one = T::one();
let zero = T::zero();
TransitionMatrix::from_matrix(nalgebra::matrix![
one, zero, zero, dt, zero, zero;
zero, one, zero, zero, dt, zero;
zero, zero, one, zero, zero, dt;
zero, zero, zero, one, zero, zero;
zero, zero, zero, zero, one, zero;
zero, zero, zero, zero, zero, one
])
}
fn process_noise(&self, dt: T) -> StateCovariance<T, 6> {
assert!(dt >= T::zero(), "Time step dt must be non-negative");
let dt2 = dt * dt;
let dt3 = dt2 * dt;
let two = T::from_f64(2.0).unwrap();
let three = T::from_f64(3.0).unwrap();
let q = self.noise_diff_coeff;
let q_pp = dt3 / three * q; let q_pv = dt2 / two * q; let q_vv = dt * q;
let zero = T::zero();
StateCovariance::from_matrix(nalgebra::matrix![
q_pp, zero, zero, q_pv, zero, zero;
zero, q_pp, zero, zero, q_pv, zero;
zero, zero, q_pp, zero, zero, q_pv;
q_pv, zero, zero, q_vv, zero, zero;
zero, q_pv, zero, zero, q_vv, zero;
zero, zero, q_pv, zero, zero, q_vv
])
}
fn survival_probability(&self, _state: &StateVector<T, 6>) -> T {
self.p_survival
}
}
#[derive(Debug, Clone)]
pub struct CoordinatedTurn2D<T: RealField> {
pub noise_diff_coeff: T,
pub turn_noise_diff_coeff: T,
pub p_survival: T,
}
impl<T: RealField + Float + Copy> CoordinatedTurn2D<T> {
pub fn new(noise_diff_coeff: T, turn_noise_diff_coeff: T, p_survival: T) -> Self {
assert!(
noise_diff_coeff >= T::zero(),
"Process noise noise_diff_coeff must be non-negative"
);
assert!(
turn_noise_diff_coeff >= T::zero(),
"Process noise turn_noise_diff_coeff must be non-negative"
);
assert!(
p_survival >= T::zero() && p_survival <= T::one(),
"Survival probability must be in [0, 1]"
);
Self {
noise_diff_coeff,
turn_noise_diff_coeff,
p_survival,
}
}
pub fn predict_nonlinear(&self, state: &StateVector<T, 5>, dt: T) -> StateVector<T, 5> {
assert!(dt >= T::zero(), "Time step dt must be non-negative");
let x = *state.index(0);
let y = *state.index(1);
let vx = *state.index(2);
let vy = *state.index(3);
let omega = *state.index(4);
let omega_dt = omega * dt;
let eps = T::from_f64(1e-5).unwrap();
if num_traits::Float::abs(omega) < eps {
StateVector::from_array([x + vx * dt, y + vy * dt, vx, vy, omega])
} else {
let sin_omega_dt = num_traits::Float::sin(omega_dt);
let cos_omega_dt = num_traits::Float::cos(omega_dt);
let one_minus_cos = T::one() - cos_omega_dt;
let x_new = x + (vx * sin_omega_dt + vy * one_minus_cos) / omega;
let y_new = y + (vx * one_minus_cos + vy * sin_omega_dt) / omega;
let vx_new = vx * cos_omega_dt - vy * sin_omega_dt;
let vy_new = vx * sin_omega_dt + vy * cos_omega_dt;
StateVector::from_array([x_new, y_new, vx_new, vy_new, omega])
}
}
pub fn transition_matrix_at(&self, state: &StateVector<T, 5>, dt: T) -> TransitionMatrix<T, 5> {
assert!(dt >= T::zero(), "Time step dt must be non-negative");
let vx = *state.index(2);
let vy = *state.index(3);
let omega = *state.index(4);
let omega_dt = omega * dt;
let eps = T::from_f64(1e-5).unwrap();
let one = T::one();
let zero = T::zero();
if num_traits::Float::abs(omega) < eps {
TransitionMatrix::from_matrix(nalgebra::matrix![
one, zero, dt, zero, zero;
zero, one, zero, dt, zero;
zero, zero, one, zero, zero;
zero, zero, zero, one, zero;
zero, zero, zero, zero, one
])
} else {
let sin_omega_dt = num_traits::Float::sin(omega_dt);
let cos_omega_dt = num_traits::Float::cos(omega_dt);
let one_minus_cos = one - cos_omega_dt;
let omega_sq = omega * omega;
let dx_dvx = sin_omega_dt / omega;
let dx_dvy = one_minus_cos / omega;
let dy_dvx = one_minus_cos / omega;
let dy_dvy = sin_omega_dt / omega;
let dx_domega = (vx * (omega_dt * cos_omega_dt - sin_omega_dt)
+ vy * (omega_dt * sin_omega_dt - one_minus_cos))
/ omega_sq;
let dy_domega = (vx * (omega_dt * sin_omega_dt - one_minus_cos)
+ vy * (omega_dt * cos_omega_dt - sin_omega_dt))
/ omega_sq;
let dvx_domega = -vx * dt * sin_omega_dt - vy * dt * cos_omega_dt;
let dvy_domega = vx * dt * cos_omega_dt - vy * dt * sin_omega_dt;
TransitionMatrix::from_matrix(nalgebra::matrix![
one, zero, dx_dvx, dx_dvy, dx_domega;
zero, one, dy_dvx, dy_dvy, dy_domega;
zero, zero, cos_omega_dt, -sin_omega_dt, dvx_domega;
zero, zero, sin_omega_dt, cos_omega_dt, dvy_domega;
zero, zero, zero, zero, one
])
}
}
}
impl<T: RealField + Float + Copy> TransitionModel<T, 5> for CoordinatedTurn2D<T> {
fn transition_matrix(&self, dt: T) -> TransitionMatrix<T, 5> {
assert!(dt >= T::zero(), "Time step dt must be non-negative");
let one = T::one();
let zero = T::zero();
TransitionMatrix::from_matrix(nalgebra::matrix![
one, zero, dt, zero, zero;
zero, one, zero, dt, zero;
zero, zero, one, zero, zero;
zero, zero, zero, one, zero;
zero, zero, zero, zero, one
])
}
fn process_noise(&self, dt: T) -> StateCovariance<T, 5> {
assert!(dt >= T::zero(), "Time step dt must be non-negative");
let dt2 = dt * dt;
let dt3 = dt2 * dt;
let two = T::from_f64(2.0).unwrap();
let three = T::from_f64(3.0).unwrap();
let q = self.noise_diff_coeff;
let q_omega = self.turn_noise_diff_coeff;
let zero = T::zero();
let q_pp = dt3 / three * q; let q_pv = dt2 / two * q; let q_vv = dt * q;
let q_omega_omega = dt * q_omega;
StateCovariance::from_matrix(nalgebra::matrix![
q_pp, zero, q_pv, zero, zero;
zero, q_pp, zero, q_pv, zero;
q_pv, zero, q_vv, zero, zero;
zero, q_pv, zero, q_vv, zero;
zero, zero, zero, zero, q_omega_omega
])
}
fn survival_probability(&self, _state: &StateVector<T, 5>) -> T {
self.p_survival
}
}
impl<T: RealField + Float + Copy> NonlinearTransitionModel<T, 5> for CoordinatedTurn2D<T> {
fn predict_nonlinear(&self, state: &StateVector<T, 5>, dt: T) -> StateVector<T, 5> {
CoordinatedTurn2D::predict_nonlinear(self, state, dt)
}
fn jacobian_at(&self, state: &StateVector<T, 5>, dt: T) -> TransitionMatrix<T, 5> {
CoordinatedTurn2D::transition_matrix_at(self, state, dt)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_constant_velocity_2d() {
let model = ConstantVelocity2D::new(1.0_f64, 0.99);
let dt = 1.0;
let f = model.transition_matrix(dt);
let state = StateVector::from_array([0.0, 0.0, 1.0, 2.0]);
let next_state = f.apply_state(&state);
assert!((next_state.index(0) - 1.0).abs() < 1e-10);
assert!((next_state.index(1) - 2.0).abs() < 1e-10);
}
#[test]
fn test_survival_probability() {
let model = ConstantVelocity2D::new(1.0_f64, 0.95);
let state = StateVector::from_array([0.0, 0.0, 1.0, 2.0]);
assert!((model.survival_probability(&state) - 0.95).abs() < 1e-10);
}
#[test]
fn test_coordinated_turn_straight() {
let model = CoordinatedTurn2D::new(1.0_f64, 0.1, 0.99);
let state = StateVector::from_array([0.0, 0.0, 10.0, 0.0, 0.0]);
let dt = 1.0;
let predicted = model.predict_nonlinear(&state, dt);
assert!((predicted.index(0) - 10.0).abs() < 1e-10);
assert!((predicted.index(1) - 0.0).abs() < 1e-10);
assert!((predicted.index(2) - 10.0).abs() < 1e-10);
assert!((predicted.index(3) - 0.0).abs() < 1e-10);
}
#[test]
fn test_coordinated_turn_90_degrees() {
use std::f64::consts::FRAC_PI_2;
let model = CoordinatedTurn2D::new(1.0_f64, 0.1, 0.99);
let state = StateVector::from_array([0.0, 0.0, 10.0, 0.0, FRAC_PI_2]);
let dt = 1.0;
let predicted = model.predict_nonlinear(&state, dt);
let r = 10.0 / FRAC_PI_2;
assert!(
(predicted.index(0) - r).abs() < 1e-6,
"x: {} vs {}",
predicted.index(0),
r
);
assert!(
(predicted.index(1) - r).abs() < 1e-6,
"y: {} vs {}",
predicted.index(1),
r
);
assert!(
(predicted.index(2) - 0.0).abs() < 1e-6,
"vx: {} should be ~0",
predicted.index(2)
);
assert!(
(predicted.index(3) - 10.0).abs() < 1e-6,
"vy: {} should be ~10",
predicted.index(3)
);
}
#[test]
fn test_coordinated_turn_jacobian_vs_numerical() {
use std::f64::consts::FRAC_PI_4;
let model = CoordinatedTurn2D::new(1.0_f64, 0.1, 0.99);
let state = StateVector::from_array([5.0, 3.0, 8.0, 4.0, FRAC_PI_4]);
let dt = 0.5;
let jacobian = model.transition_matrix_at(&state, dt);
let eps = 1e-6;
let state_plus = StateVector::from_array([5.0, 3.0, 8.0 + eps, 4.0, FRAC_PI_4]);
let state_minus = StateVector::from_array([5.0, 3.0, 8.0 - eps, 4.0, FRAC_PI_4]);
let f_plus = model.predict_nonlinear(&state_plus, dt);
let f_minus = model.predict_nonlinear(&state_minus, dt);
let numerical_dx_dvx = (f_plus.index(0) - f_minus.index(0)) / (2.0 * eps);
let analytical_dx_dvx = jacobian.as_matrix()[(0, 2)];
assert!(
(numerical_dx_dvx - analytical_dx_dvx).abs() < 1e-4,
"dx/dvx: numerical {} vs analytical {}",
numerical_dx_dvx,
analytical_dx_dvx
);
}
}