use super::*;
use crate::matrix::vector::Vector;
use crate::Matrix;
fn approx_eq(a: f64, b: f64, tol: f64) {
assert!(
(a - b).abs() < tol,
"expected {} ≈ {} (diff = {}, tol = {})",
a,
b,
(a - b).abs(),
tol
);
}
#[test]
fn ekf_linear_predict() {
let dt = 0.1;
let x0 = Vector::from_array([0.0_f64, 1.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let q = Matrix::new([[0.001, 0.0], [0.0, 0.001]]);
let mut ekf = Ekf::<f64, 2, 1>::new(x0, p0);
ekf.predict(
|x| Vector::from_array([x[0] + dt * x[1], x[1]]),
|_x| Matrix::new([[1.0, dt], [0.0, 1.0]]),
Some(&q),
);
approx_eq(ekf.x[0], 0.1, 1e-12);
approx_eq(ekf.x[1], 1.0, 1e-12);
}
#[test]
fn ekf_linear_update() {
let x0 = Vector::from_array([0.0_f64, 1.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let r = Matrix::new([[0.1]]);
let mut ekf = Ekf::<f64, 2, 1>::new(x0, p0);
ekf.update(
&Vector::from_array([0.5]),
|x| Vector::from_array([x[0]]),
|_x| Matrix::new([[1.0, 0.0]]),
&r,
)
.unwrap();
assert!(ekf.x[0] > 0.0);
assert!(ekf.x[0] < 0.5);
assert!(ekf.p[(0, 0)] < 1.0);
}
#[test]
fn ekf_converges_to_true_state() {
let dt = 0.1;
let x0 = Vector::from_array([0.0_f64, 0.0]);
let p0 = Matrix::new([[10.0, 0.0], [0.0, 10.0]]);
let q = Matrix::new([[0.01, 0.0], [0.0, 0.01]]);
let r = Matrix::new([[0.1]]);
let mut ekf = Ekf::<f64, 2, 1>::new(x0, p0);
let mut true_pos = 5.0;
let true_vel = 1.0;
for _ in 0..100 {
ekf.predict(
|x| Vector::from_array([x[0] + dt * x[1], x[1]]),
|_x| Matrix::new([[1.0, dt], [0.0, 1.0]]),
Some(&q),
);
true_pos += dt * true_vel;
ekf.update(
&Vector::from_array([true_pos]),
|x| Vector::from_array([x[0]]),
|_x| Matrix::new([[1.0, 0.0]]),
&r,
)
.unwrap();
}
approx_eq(ekf.x[0], true_pos, 0.5);
approx_eq(ekf.x[1], true_vel, 0.5);
}
#[test]
fn ekf_predict_fd_matches_explicit() {
let dt = 0.1;
let x0 = Vector::from_array([1.0_f64, 2.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let q = Matrix::new([[0.01, 0.0], [0.0, 0.01]]);
let f = |x: &Vector<f64, 2>| {
Vector::from_array([x[0] + dt * x[1], x[1]])
};
let fj = |_x: &Vector<f64, 2>| Matrix::new([[1.0, dt], [0.0, 1.0]]);
let mut ekf_explicit = Ekf::<f64, 2, 1>::new(x0, p0);
let mut ekf_fd = Ekf::<f64, 2, 1>::new(x0, p0);
ekf_explicit.predict(f, fj, Some(&q));
ekf_fd.predict_fd(f, Some(&q));
for i in 0..2 {
approx_eq(ekf_explicit.x[i], ekf_fd.x[i], 1e-10);
for j in 0..2 {
approx_eq(ekf_explicit.p[(i, j)], ekf_fd.p[(i, j)], 1e-6);
}
}
}
#[test]
fn ekf_update_fd_matches_explicit() {
let x0 = Vector::from_array([1.0_f64, 2.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let r = Matrix::new([[0.1]]);
let z = Vector::from_array([1.5]);
let h = |x: &Vector<f64, 2>| Vector::from_array([x[0]]);
let hj = |_x: &Vector<f64, 2>| Matrix::new([[1.0, 0.0]]);
let mut ekf_explicit = Ekf::<f64, 2, 1>::new(x0, p0);
let mut ekf_fd = Ekf::<f64, 2, 1>::new(x0, p0);
ekf_explicit.update(&z, h, hj, &r).unwrap();
ekf_fd.update_fd(&z, h, &r).unwrap();
for i in 0..2 {
approx_eq(ekf_explicit.x[i], ekf_fd.x[i], 1e-6);
for j in 0..2 {
approx_eq(ekf_explicit.p[(i, j)], ekf_fd.p[(i, j)], 1e-4);
}
}
}
#[test]
fn ekf_nonlinear_measurement() {
let x0 = Vector::from_array([3.0_f64, 4.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let r = Matrix::new([[0.01]]);
let mut ekf = Ekf::<f64, 2, 1>::new(x0, p0);
let true_range = 5.0; ekf.update(
&Vector::from_array([true_range]),
|x| Vector::from_array([(x[0] * x[0] + x[1] * x[1]).sqrt()]),
|x| {
let r = (x[0] * x[0] + x[1] * x[1]).sqrt();
Matrix::new([[x[0] / r, x[1] / r]])
},
&r,
)
.unwrap();
approx_eq(ekf.x[0], 3.0, 0.1);
approx_eq(ekf.x[1], 4.0, 0.1);
}
#[test]
fn ekf_zero_process_noise() {
let x0 = Vector::from_array([1.0_f64, 2.0]);
let p0 = Matrix::new([[0.5, 0.0], [0.0, 0.5]]);
let mut ekf = Ekf::<f64, 2, 1>::new(x0, p0);
ekf.predict(|x| *x, |_x| Matrix::eye(), None);
approx_eq(ekf.x[0], 1.0, 1e-14);
approx_eq(ekf.x[1], 2.0, 1e-14);
approx_eq(ekf.p[(0, 0)], 0.5, 1e-14);
approx_eq(ekf.p[(1, 1)], 0.5, 1e-14);
}
#[test]
fn ekf_joseph_form_symmetry() {
let x0 = Vector::from_array([0.0_f64, 0.0, 0.0]);
let p0 = Matrix::new([[10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]]);
let q = Matrix::new([[0.1, 0.0, 0.0], [0.0, 0.1, 0.0], [0.0, 0.0, 0.1]]);
let r = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let mut ekf = Ekf::<f64, 3, 2>::new(x0, p0);
for k in 0..50 {
let t = k as f64 * 0.1;
ekf.predict(
|x| {
Vector::from_array([
x[0] + 0.1 * x[1],
x[1] + 0.1 * x[2],
x[2],
])
},
|_x| {
Matrix::new([
[1.0, 0.1, 0.0],
[0.0, 1.0, 0.1],
[0.0, 0.0, 1.0],
])
},
Some(&q),
);
let z = Vector::from_array([t.sin(), t.cos()]);
ekf.update(
&z,
|x| Vector::from_array([x[0], x[1]]),
|_x| {
Matrix::new([
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
])
},
&r,
)
.unwrap();
}
for i in 0..3 {
for j in 0..3 {
approx_eq(ekf.p[(i, j)], ekf.p[(j, i)], 1e-10);
}
}
}
#[test]
fn ekf_f32() {
let x0 = Vector::from_array([0.0_f32, 1.0]);
let p0 = Matrix::new([[1.0_f32, 0.0], [0.0, 1.0]]);
let q = Matrix::new([[0.01_f32, 0.0], [0.0, 0.01]]);
let r = Matrix::new([[0.5_f32]]);
let mut ekf = Ekf::<f32, 2, 1>::new(x0, p0);
let dt = 0.1_f32;
ekf.predict(
|x| Vector::from_array([x[0] + dt * x[1], x[1]]),
|_x| Matrix::new([[1.0, dt], [0.0, 1.0]]),
Some(&q),
);
ekf.update(
&Vector::from_array([0.12_f32]),
|x| Vector::from_array([x[0]]),
|_x| Matrix::new([[1.0_f32, 0.0]]),
&r,
)
.unwrap();
assert!(ekf.x[0].is_finite());
assert!(ekf.p[(0, 0)] > 0.0);
}
#[cfg(feature = "alloc")]
mod ukf_tests {
use super::*;
#[test]
fn ukf_linear_predict() {
let dt = 0.1;
let x0 = Vector::from_array([0.0_f64, 1.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let q = Matrix::new([[0.001, 0.0], [0.0, 0.001]]);
let mut ukf = Ukf::<f64, 2, 1>::new(x0, p0);
ukf.predict(
|x| Vector::from_array([x[0] + dt * x[1], x[1]]),
Some(&q),
)
.unwrap();
approx_eq(ukf.x[0], 0.1, 1e-6);
approx_eq(ukf.x[1], 1.0, 1e-6);
}
#[test]
fn ukf_linear_update() {
let x0 = Vector::from_array([0.0_f64, 1.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let r = Matrix::new([[0.1]]);
let mut ukf = Ukf::<f64, 2, 1>::new(x0, p0);
ukf.update(
&Vector::from_array([0.5]),
|x| Vector::from_array([x[0]]),
&r,
)
.unwrap();
assert!(ukf.x[0] > 0.0);
assert!(ukf.x[0] < 0.5);
assert!(ukf.p[(0, 0)] < 1.0);
}
#[test]
fn ukf_converges_to_true_state() {
let dt = 0.1;
let x0 = Vector::from_array([0.0_f64, 0.0]);
let p0 = Matrix::new([[10.0, 0.0], [0.0, 10.0]]);
let q = Matrix::new([[0.01, 0.0], [0.0, 0.01]]);
let r = Matrix::new([[0.1]]);
let mut ukf = Ukf::<f64, 2, 1>::new(x0, p0);
let mut true_pos = 5.0;
let true_vel = 1.0;
for _ in 0..100 {
ukf.predict(
|x| Vector::from_array([x[0] + dt * x[1], x[1]]),
Some(&q),
)
.unwrap();
true_pos += dt * true_vel;
ukf.update(
&Vector::from_array([true_pos]),
|x| Vector::from_array([x[0]]),
&r,
)
.unwrap();
}
approx_eq(ukf.x[0], true_pos, 0.5);
approx_eq(ukf.x[1], true_vel, 0.5);
}
#[test]
fn ukf_nonlinear_measurement() {
let x0 = Vector::from_array([3.0_f64, 4.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let r = Matrix::new([[0.01]]);
let mut ukf = Ukf::<f64, 2, 1>::new(x0, p0);
let true_range = 5.0;
ukf.update(
&Vector::from_array([true_range]),
|x| {
Vector::from_array([
(x[0] * x[0] + x[1] * x[1]).sqrt(),
])
},
&r,
)
.unwrap();
approx_eq(ukf.x[0], 3.0, 0.2);
approx_eq(ukf.x[1], 4.0, 0.2);
}
#[test]
fn ukf_custom_params() {
let x0 = Vector::from_array([0.0_f64, 1.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let q = Matrix::new([[0.01, 0.0], [0.0, 0.01]]);
let mut ukf = Ukf::<f64, 2, 1>::with_params(x0, p0, 0.5, 2.0, 0.0);
ukf.predict(
|x| Vector::from_array([x[0] + 0.1 * x[1], x[1]]),
Some(&q),
)
.unwrap();
approx_eq(ukf.x[0], 0.1, 1e-6);
approx_eq(ukf.x[1], 1.0, 1e-6);
}
#[test]
fn ukf_covariance_stays_spd() {
let x0 = Vector::from_array([0.0_f64, 0.0, 0.0]);
let p0 = Matrix::new([[10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]]);
let q = Matrix::new([[0.1, 0.0, 0.0], [0.0, 0.1, 0.0], [0.0, 0.0, 0.1]]);
let r = Matrix::new([[1.0]]);
let mut ukf = Ukf::<f64, 3, 1>::new(x0, p0);
for k in 0..30 {
ukf.predict(
|x| {
Vector::from_array([
x[0] + 0.1 * x[1],
x[1] + 0.1 * x[2],
x[2],
])
},
Some(&q),
)
.unwrap();
let z_val = (k as f64 * 0.1).sin();
ukf.update(
&Vector::from_array([z_val]),
|x| Vector::from_array([x[0]]),
&r,
)
.unwrap();
}
for i in 0..3 {
for j in 0..3 {
approx_eq(ukf.p[(i, j)], ukf.p[(j, i)], 1e-10);
}
}
for i in 0..3 {
assert!(ukf.p[(i, i)] > 0.0, "P[{},{}] = {} not positive", i, i, ukf.p[(i, i)]);
}
assert!(ukf.p.cholesky().is_ok());
}
#[test]
fn ukf_f32() {
let x0 = Vector::from_array([0.0_f32, 1.0]);
let p0 = Matrix::new([[1.0_f32, 0.0], [0.0, 1.0]]);
let q = Matrix::new([[0.01_f32, 0.0], [0.0, 0.01]]);
let r = Matrix::new([[0.5_f32]]);
let mut ukf = Ukf::<f32, 2, 1>::new(x0, p0);
ukf.predict(
|x| Vector::from_array([x[0] + 0.1 * x[1], x[1]]),
Some(&q),
)
.unwrap();
ukf.update(
&Vector::from_array([0.12_f32]),
|x| Vector::from_array([x[0]]),
&r,
)
.unwrap();
assert!(ukf.x[0].is_finite());
assert!(ukf.p[(0, 0)] > 0.0);
}
#[test]
fn ukf_covariance_not_pd_error() {
let x0 = Vector::from_array([0.0_f64, 0.0]);
let p0 = Matrix::new([[-1.0, 0.0], [0.0, -1.0]]);
let mut ukf = Ukf::<f64, 2, 1>::new(x0, p0);
let result = ukf.predict(|x| *x, None);
assert_eq!(result, Err(EstimateError::CovarianceNotPD));
}
#[test]
fn ukf_predict_no_process_noise() {
let x0 = Vector::from_array([1.0_f64, 2.0]);
let p0 = Matrix::new([[0.5, 0.0], [0.0, 0.5]]);
let mut ukf = Ukf::<f64, 2, 1>::new(x0, p0);
ukf.predict(|x| *x, None).unwrap();
approx_eq(ukf.x[0], 1.0, 1e-6);
approx_eq(ukf.x[1], 2.0, 1e-6);
}
#[test]
fn ekf_ukf_agree_linear() {
let dt = 0.1;
let x0 = Vector::from_array([0.0_f64, 1.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let q = Matrix::new([[0.01, 0.0], [0.0, 0.01]]);
let r = Matrix::new([[0.5]]);
let mut ekf = Ekf::<f64, 2, 1>::new(x0, p0);
let mut ukf = Ukf::<f64, 2, 1>::with_params(x0, p0, 1.0, 2.0, 0.0);
let f = |x: &Vector<f64, 2>| {
Vector::from_array([x[0] + dt * x[1], x[1]])
};
let fj = |_x: &Vector<f64, 2>| Matrix::new([[1.0, dt], [0.0, 1.0]]);
let h = |x: &Vector<f64, 2>| Vector::from_array([x[0]]);
let hj = |_x: &Vector<f64, 2>| Matrix::new([[1.0, 0.0]]);
let measurements = [0.1, 0.22, 0.31, 0.45, 0.53, 0.68, 0.79, 0.9, 1.02, 1.11];
for &z_val in &measurements {
ekf.predict(f, fj, Some(&q));
ukf.predict(f, Some(&q)).unwrap();
let z = Vector::from_array([z_val]);
ekf.update(&z, h, hj, &r).unwrap();
ukf.update(&z, h, &r).unwrap();
}
for i in 0..2 {
approx_eq(ekf.x[i], ukf.x[i], 0.05);
}
}
#[test]
fn ekf_ukf_both_converge_nonlinear() {
let x0 = Vector::from_array([5.0_f64, 0.3]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 0.1]]);
let q = Matrix::new([[0.01, 0.0], [0.0, 0.001]]);
let r = Matrix::new([[0.1]]);
let mut ekf = Ekf::<f64, 2, 1>::new(x0, p0);
let mut ukf = Ukf::<f64, 2, 1>::new(x0, p0);
let true_x = 5.0 * 0.3_f64.cos();
let h = |x: &Vector<f64, 2>| Vector::from_array([x[0] * x[1].cos()]);
let hj = |x: &Vector<f64, 2>| {
Matrix::new([[x[1].cos(), -x[0] * x[1].sin()]])
};
for _ in 0..20 {
ekf.predict(|x| *x, |_x| Matrix::eye(), Some(&q));
ukf.predict(|x| *x, Some(&q)).unwrap();
let z = Vector::from_array([true_x]);
ekf.update(&z, h, hj, &r).unwrap();
ukf.update(&z, h, &r).unwrap();
}
let ekf_x_pred = ekf.x[0] * ekf.x[1].cos();
let ukf_x_pred = ukf.x[0] * ukf.x[1].cos();
approx_eq(ekf_x_pred, true_x, 0.5);
approx_eq(ukf_x_pred, true_x, 0.5);
}
}
#[cfg(feature = "alloc")]
mod cholupdate_tests {
use super::*;
use super::super::cholupdate::cholupdate;
use crate::linalg::CholeskyDecomposition;
#[test]
fn cholupdate_rank1_update_roundtrip() {
let p = Matrix::new([[4.0, 2.0], [2.0, 3.0_f64]]);
let chol = CholeskyDecomposition::new(&p).unwrap();
let mut l = chol.l_full();
let v_orig = Vector::from_array([1.0, 0.5_f64]);
let mut v = v_orig;
cholupdate(&mut l, &mut v, 1.0).unwrap();
let p_new = l * l.transpose();
let p_expected = p + v_orig * v_orig.transpose();
for i in 0..2 {
for j in 0..2 {
approx_eq(p_new[(i, j)], p_expected[(i, j)], 1e-10);
}
}
}
#[test]
fn cholupdate_rank1_downdate_roundtrip() {
let p_base = Matrix::new([[4.0, 2.0], [2.0, 3.0_f64]]);
let v = Vector::from_array([0.5, 0.3_f64]);
let p_aug = p_base + v * v.transpose();
let chol = CholeskyDecomposition::new(&p_aug).unwrap();
let mut l = chol.l_full();
let mut v_work = v;
cholupdate(&mut l, &mut v_work, -1.0).unwrap();
let p_recovered = l * l.transpose();
for i in 0..2 {
for j in 0..2 {
approx_eq(p_recovered[(i, j)], p_base[(i, j)], 1e-10);
}
}
}
#[test]
fn cholupdate_downdate_fails_non_pd() {
let mut l = Matrix::<f64, 2, 2>::eye();
let mut v = Vector::from_array([1.5, 0.0]);
let result = cholupdate(&mut l, &mut v, -1.0);
assert_eq!(result, Err(EstimateError::CholdowndateFailed));
}
#[test]
fn cholupdate_large_matrix() {
let p = Matrix::new([
[5.0, 1.0, 0.5, 0.1],
[1.0, 4.0, 0.3, 0.2],
[0.5, 0.3, 3.0, 0.1],
[0.1, 0.2, 0.1, 2.0_f64],
]);
let chol = CholeskyDecomposition::new(&p).unwrap();
let mut l = chol.l_full();
let v_orig = Vector::from_array([0.3, 0.7, 0.1, 0.5]);
let mut v = v_orig;
cholupdate(&mut l, &mut v, 1.0).unwrap();
let p_new = l * l.transpose();
let p_expected = p + v_orig * v_orig.transpose();
for i in 0..4 {
for j in 0..4 {
approx_eq(p_new[(i, j)], p_expected[(i, j)], 1e-10);
}
}
}
}
#[cfg(feature = "alloc")]
mod srukf_tests {
use super::*;
#[test]
fn srukf_linear_cv_converges() {
let dt = 0.1;
let x0 = Vector::from_array([0.0_f64, 0.0]);
let p0 = Matrix::new([[10.0, 0.0], [0.0, 10.0]]);
let q = Matrix::new([[0.01, 0.0], [0.0, 0.01]]);
let r = Matrix::new([[0.1]]);
let mut srukf = SrUkf::<f64, 2, 1>::from_covariance(x0, p0).unwrap();
let mut true_pos = 5.0;
let true_vel = 1.0;
for _ in 0..100 {
srukf
.predict(
|x| Vector::from_array([x[0] + dt * x[1], x[1]]),
Some(&q),
)
.unwrap();
true_pos += dt * true_vel;
srukf
.update(
&Vector::from_array([true_pos]),
|x| Vector::from_array([x[0]]),
&r,
)
.unwrap();
}
approx_eq(srukf.x[0], true_pos, 0.5);
approx_eq(srukf.x[1], true_vel, 0.5);
}
#[test]
fn srukf_agrees_with_ukf_linear() {
let dt = 0.1;
let x0 = Vector::from_array([0.0_f64, 1.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let q = Matrix::new([[0.01, 0.0], [0.0, 0.01]]);
let r = Matrix::new([[0.5]]);
let mut ukf = Ukf::<f64, 2, 1>::with_params(x0, p0, 0.5, 2.0, 0.0);
let mut srukf =
SrUkf::<f64, 2, 1>::from_covariance_with_params(x0, p0, 0.5, 2.0, 0.0).unwrap();
let f = |x: &Vector<f64, 2>| {
Vector::from_array([x[0] + dt * x[1], x[1]])
};
let h = |x: &Vector<f64, 2>| Vector::from_array([x[0]]);
let measurements = [0.1, 0.22, 0.31, 0.45, 0.53];
for &z_val in &measurements {
ukf.predict(f, Some(&q)).unwrap();
srukf.predict(f, Some(&q)).unwrap();
let z = Vector::from_array([z_val]);
ukf.update(&z, h, &r).unwrap();
srukf.update(&z, h, &r).unwrap();
}
for i in 0..2 {
approx_eq(ukf.x[i], srukf.x[i], 0.1);
}
let srukf_p = srukf.covariance();
for i in 0..2 {
for j in 0..2 {
approx_eq(ukf.p[(i, j)], srukf_p[(i, j)], 0.1);
}
}
}
#[test]
fn srukf_covariance_stays_pd() {
let x0 = Vector::from_array([0.0_f64, 0.0, 0.0]);
let p0 = Matrix::new([[10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]]);
let q = Matrix::new([[0.1, 0.0, 0.0], [0.0, 0.1, 0.0], [0.0, 0.0, 0.1]]);
let r = Matrix::new([[1.0]]);
let mut srukf = SrUkf::<f64, 3, 1>::from_covariance(x0, p0).unwrap();
for k in 0..30 {
srukf
.predict(
|x| {
Vector::from_array([
x[0] + 0.1 * x[1],
x[1] + 0.1 * x[2],
x[2],
])
},
Some(&q),
)
.unwrap();
let z_val = (k as f64 * 0.1).sin();
srukf
.update(
&Vector::from_array([z_val]),
|x| Vector::from_array([x[0]]),
&r,
)
.unwrap();
}
let p = srukf.covariance();
assert!(p.cholesky().is_ok());
}
#[test]
fn srukf_predict_only() {
let x0 = Vector::from_array([1.0_f64, 2.0]);
let p0 = Matrix::new([[0.5, 0.0], [0.0, 0.5]]);
let mut srukf = SrUkf::<f64, 2, 1>::from_covariance(x0, p0).unwrap();
srukf.predict(|x| *x, None).unwrap();
approx_eq(srukf.x[0], 1.0, 1e-6);
approx_eq(srukf.x[1], 2.0, 1e-6);
}
#[test]
fn srukf_custom_params() {
let x0 = Vector::from_array([0.0_f64, 1.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let q = Matrix::new([[0.01, 0.0], [0.0, 0.01]]);
let mut srukf =
SrUkf::<f64, 2, 1>::from_covariance_with_params(x0, p0, 0.5, 2.0, 0.0).unwrap();
srukf
.predict(
|x| Vector::from_array([x[0] + 0.1 * x[1], x[1]]),
Some(&q),
)
.unwrap();
approx_eq(srukf.x[0], 0.1, 1e-6);
approx_eq(srukf.x[1], 1.0, 1e-6);
}
#[test]
fn srukf_nonlinear_measurement() {
let x0 = Vector::from_array([3.0_f64, 4.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let r = Matrix::new([[0.01]]);
let mut srukf = SrUkf::<f64, 2, 1>::from_covariance(x0, p0).unwrap();
let true_range = 5.0;
srukf
.update(
&Vector::from_array([true_range]),
|x| {
Vector::from_array([
(x[0] * x[0] + x[1] * x[1]).sqrt(),
])
},
&r,
)
.unwrap();
approx_eq(srukf.x[0], 3.0, 0.3);
approx_eq(srukf.x[1], 4.0, 0.3);
}
#[test]
fn srukf_f32() {
let x0 = Vector::from_array([0.0_f32, 1.0]);
let p0 = Matrix::new([[1.0_f32, 0.0], [0.0, 1.0]]);
let q = Matrix::new([[0.01_f32, 0.0], [0.0, 0.01]]);
let r = Matrix::new([[0.5_f32]]);
let mut srukf = SrUkf::<f32, 2, 1>::from_covariance(x0, p0).unwrap();
srukf
.predict(
|x| Vector::from_array([x[0] + 0.1 * x[1], x[1]]),
Some(&q),
)
.unwrap();
srukf
.update(
&Vector::from_array([0.12_f32]),
|x| Vector::from_array([x[0]]),
&r,
)
.unwrap();
assert!(srukf.x[0].is_finite());
let p = srukf.covariance();
assert!(p[(0, 0)] > 0.0);
}
#[test]
fn srukf_covariance_not_pd_error() {
let x0 = Vector::from_array([0.0_f64, 0.0]);
let p0 = Matrix::new([[-1.0, 0.0], [0.0, -1.0]]);
let result = SrUkf::<f64, 2, 1>::from_covariance(x0, p0);
match result {
Err(EstimateError::CovarianceNotPD) => {}
_ => panic!("expected CovarianceNotPD error"),
}
}
}
#[cfg(feature = "alloc")]
mod ckf_tests {
use super::*;
#[test]
fn ckf_linear_cv_converges() {
let dt = 0.1;
let x0 = Vector::from_array([0.0_f64, 0.0]);
let p0 = Matrix::new([[10.0, 0.0], [0.0, 10.0]]);
let q = Matrix::new([[0.01, 0.0], [0.0, 0.01]]);
let r = Matrix::new([[0.1]]);
let mut ckf = Ckf::<f64, 2, 1>::new(x0, p0);
let mut true_pos = 5.0;
let true_vel = 1.0;
for _ in 0..100 {
ckf.predict(
|x| Vector::from_array([x[0] + dt * x[1], x[1]]),
Some(&q),
)
.unwrap();
true_pos += dt * true_vel;
ckf.update(
&Vector::from_array([true_pos]),
|x| Vector::from_array([x[0]]),
&r,
)
.unwrap();
}
approx_eq(ckf.x[0], true_pos, 0.5);
approx_eq(ckf.x[1], true_vel, 0.5);
}
#[test]
fn ckf_agrees_with_ekf_linear() {
let dt = 0.1;
let x0 = Vector::from_array([0.0_f64, 1.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let q = Matrix::new([[0.01, 0.0], [0.0, 0.01]]);
let r = Matrix::new([[0.5]]);
let mut ekf = Ekf::<f64, 2, 1>::new(x0, p0);
let mut ckf = Ckf::<f64, 2, 1>::new(x0, p0);
let f = |x: &Vector<f64, 2>| {
Vector::from_array([x[0] + dt * x[1], x[1]])
};
let fj = |_x: &Vector<f64, 2>| Matrix::new([[1.0, dt], [0.0, 1.0]]);
let h = |x: &Vector<f64, 2>| Vector::from_array([x[0]]);
let hj = |_x: &Vector<f64, 2>| Matrix::new([[1.0, 0.0]]);
let measurements = [0.1, 0.22, 0.31, 0.45, 0.53, 0.68, 0.79, 0.9, 1.02, 1.11];
for &z_val in &measurements {
ekf.predict(f, fj, Some(&q));
ckf.predict(f, Some(&q)).unwrap();
let z = Vector::from_array([z_val]);
ekf.update(&z, h, hj, &r).unwrap();
ckf.update(&z, h, &r).unwrap();
}
for i in 0..2 {
approx_eq(ekf.x[i], ckf.x[i], 0.1);
}
}
#[test]
fn ckf_nonlinear_dynamics() {
let x0 = Vector::from_array([3.0_f64, 4.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let r = Matrix::new([[0.01]]);
let mut ckf = Ckf::<f64, 2, 1>::new(x0, p0);
let true_range = 5.0;
ckf.update(
&Vector::from_array([true_range]),
|x| {
Vector::from_array([
(x[0] * x[0] + x[1] * x[1]).sqrt(),
])
},
&r,
)
.unwrap();
approx_eq(ckf.x[0], 3.0, 0.3);
approx_eq(ckf.x[1], 4.0, 0.3);
}
#[test]
fn ckf_predict_only() {
let x0 = Vector::from_array([1.0_f64, 2.0]);
let p0 = Matrix::new([[0.5, 0.0], [0.0, 0.5]]);
let mut ckf = Ckf::<f64, 2, 1>::new(x0, p0);
ckf.predict(|x| *x, None).unwrap();
approx_eq(ckf.x[0], 1.0, 1e-6);
approx_eq(ckf.x[1], 2.0, 1e-6);
}
#[test]
fn ckf_predict_no_process_noise() {
let x0 = Vector::from_array([1.0_f64, 2.0]);
let p0 = Matrix::new([[0.5, 0.0], [0.0, 0.5]]);
let mut ckf = Ckf::<f64, 2, 1>::new(x0, p0);
ckf.predict(|x| *x, None).unwrap();
approx_eq(ckf.p[(0, 0)], 0.5, 1e-6);
approx_eq(ckf.p[(1, 1)], 0.5, 1e-6);
}
#[test]
fn ckf_covariance_symmetry_and_pd() {
let x0 = Vector::from_array([0.0_f64, 0.0, 0.0]);
let p0 = Matrix::new([[10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]]);
let q = Matrix::new([[0.1, 0.0, 0.0], [0.0, 0.1, 0.0], [0.0, 0.0, 0.1]]);
let r = Matrix::new([[1.0]]);
let mut ckf = Ckf::<f64, 3, 1>::new(x0, p0);
for k in 0..30 {
ckf.predict(
|x| {
Vector::from_array([
x[0] + 0.1 * x[1],
x[1] + 0.1 * x[2],
x[2],
])
},
Some(&q),
)
.unwrap();
let z_val = (k as f64 * 0.1).sin();
ckf.update(
&Vector::from_array([z_val]),
|x| Vector::from_array([x[0]]),
&r,
)
.unwrap();
}
for i in 0..3 {
for j in 0..3 {
approx_eq(ckf.p[(i, j)], ckf.p[(j, i)], 1e-10);
}
}
assert!(ckf.p.cholesky().is_ok());
}
#[test]
fn ckf_multiple_update_cycles() {
let dt = 0.1;
let x0 = Vector::from_array([0.0_f64, 1.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let q = Matrix::new([[0.01, 0.0], [0.0, 0.01]]);
let r = Matrix::new([[0.1]]);
let mut ckf = Ckf::<f64, 2, 1>::new(x0, p0);
for k in 0..20 {
ckf.predict(
|x| Vector::from_array([x[0] + dt * x[1], x[1]]),
Some(&q),
)
.unwrap();
let z_val = (k + 1) as f64 * dt;
ckf.update(
&Vector::from_array([z_val]),
|x| Vector::from_array([x[0]]),
&r,
)
.unwrap();
}
assert!(ckf.x[0].is_finite());
assert!(ckf.p[(0, 0)] > 0.0);
}
#[test]
fn ckf_f32() {
let x0 = Vector::from_array([0.0_f32, 1.0]);
let p0 = Matrix::new([[1.0_f32, 0.0], [0.0, 1.0]]);
let q = Matrix::new([[0.01_f32, 0.0], [0.0, 0.01]]);
let r = Matrix::new([[0.5_f32]]);
let mut ckf = Ckf::<f32, 2, 1>::new(x0, p0);
ckf.predict(
|x| Vector::from_array([x[0] + 0.1 * x[1], x[1]]),
Some(&q),
)
.unwrap();
ckf.update(
&Vector::from_array([0.12_f32]),
|x| Vector::from_array([x[0]]),
&r,
)
.unwrap();
assert!(ckf.x[0].is_finite());
assert!(ckf.p[(0, 0)] > 0.0);
}
}
#[cfg(feature = "alloc")]
mod rts_tests {
use super::*;
extern crate alloc;
use alloc::vec::Vec;
fn run_forward_ekf(measurements: &[f64]) -> (Ekf<f64, 2, 1>, Vec<EkfStep<f64, 2>>) {
let dt = 0.1;
let x0 = Vector::from_array([0.0_f64, 0.0]);
let p0 = Matrix::new([[10.0, 0.0], [0.0, 10.0]]);
let q = Matrix::new([[0.01, 0.0], [0.0, 0.01]]);
let r = Matrix::new([[0.5]]);
let f_jac = Matrix::new([[1.0, dt], [0.0, 1.0]]);
let mut ekf = Ekf::<f64, 2, 1>::new(x0, p0);
let mut steps = Vec::new();
for &z_val in measurements {
ekf.predict(
|x| Vector::from_array([x[0] + dt * x[1], x[1]]),
|_| f_jac,
Some(&q),
);
let x_predicted = ekf.x;
let p_predicted = ekf.p;
ekf.update(
&Vector::from_array([z_val]),
|x| Vector::from_array([x[0]]),
|_| Matrix::new([[1.0, 0.0]]),
&r,
)
.unwrap();
steps.push(EkfStep {
x_predicted,
p_predicted,
x_updated: ekf.x,
p_updated: ekf.p,
f_jacobian: f_jac,
});
}
(ekf, steps)
}
#[test]
fn rts_improves_estimates() {
let measurements: Vec<f64> = (1..=20).map(|i| i as f64 * 0.1).collect();
let (_, steps) = run_forward_ekf(&measurements);
let smoothed = rts_smooth(&steps).unwrap();
assert_eq!(smoothed.len(), steps.len());
for (k, (_, p_smooth)) in smoothed.iter().enumerate() {
let trace_smooth = p_smooth[(0, 0)] + p_smooth[(1, 1)];
let trace_filter = steps[k].p_updated[(0, 0)] + steps[k].p_updated[(1, 1)];
assert!(
trace_smooth <= trace_filter + 1e-10,
"step {}: smooth trace {} > filter trace {}",
k,
trace_smooth,
trace_filter
);
}
}
#[test]
fn rts_smoothed_covariance_leq_filtered() {
let measurements: Vec<f64> = (1..=10).map(|i| i as f64 * 0.1).collect();
let (_, steps) = run_forward_ekf(&measurements);
let smoothed = rts_smooth(&steps).unwrap();
for (k, (_, p_smooth)) in smoothed.iter().enumerate() {
for d in 0..2 {
assert!(
p_smooth[(d, d)] <= steps[k].p_updated[(d, d)] + 1e-10,
"step {} dim {}: smooth P {} > filter P {}",
k,
d,
p_smooth[(d, d)],
steps[k].p_updated[(d, d)]
);
}
}
}
#[test]
fn rts_empty_input() {
let result = rts_smooth::<f64, 2>(&[]).unwrap();
assert!(result.is_empty());
}
#[test]
fn rts_single_step() {
let measurements = [1.0];
let (_, steps) = run_forward_ekf(&measurements);
let smoothed = rts_smooth(&steps).unwrap();
assert_eq!(smoothed.len(), 1);
for i in 0..2 {
approx_eq(smoothed[0].0[i], steps[0].x_updated[i], 1e-14);
for j in 0..2 {
approx_eq(smoothed[0].1[(i, j)], steps[0].p_updated[(i, j)], 1e-14);
}
}
}
#[test]
fn rts_last_step_equals_filtered() {
let measurements: Vec<f64> = (1..=5).map(|i| i as f64 * 0.1).collect();
let (_, steps) = run_forward_ekf(&measurements);
let smoothed = rts_smooth(&steps).unwrap();
let last = smoothed.len() - 1;
for i in 0..2 {
approx_eq(
smoothed[last].0[i],
steps[last].x_updated[i],
1e-14,
);
}
}
#[test]
fn rts_backward_consistency() {
let measurements: Vec<f64> = (1..=20).map(|i| i as f64 * 0.1).collect();
let (_, steps) = run_forward_ekf(&measurements);
let smoothed = rts_smooth(&steps).unwrap();
let improvement_first = {
let tf = steps[0].p_updated[(0, 0)] + steps[0].p_updated[(1, 1)];
let ts = smoothed[0].1[(0, 0)] + smoothed[0].1[(1, 1)];
tf - ts
};
let improvement_last = {
let n = steps.len() - 1;
let tf = steps[n].p_updated[(0, 0)] + steps[n].p_updated[(1, 1)];
let ts = smoothed[n].1[(0, 0)] + smoothed[n].1[(1, 1)];
tf - ts
};
assert!(
improvement_first > improvement_last - 1e-10,
"first improvement {} not > last improvement {}",
improvement_first,
improvement_last
);
}
#[test]
fn rts_symmetry_preserved() {
let measurements: Vec<f64> = (1..=10).map(|i| i as f64 * 0.1).collect();
let (_, steps) = run_forward_ekf(&measurements);
let smoothed = rts_smooth(&steps).unwrap();
for (_, (_, p)) in smoothed.iter().enumerate() {
for i in 0..2 {
for j in 0..2 {
approx_eq(
p[(i, j)],
p[(j, i)],
1e-10,
);
}
}
}
}
#[test]
fn rts_f32() {
let dt = 0.1_f32;
let x0 = Vector::from_array([0.0_f32, 0.0]);
let p0 = Matrix::new([[10.0_f32, 0.0], [0.0, 10.0]]);
let q = Matrix::new([[0.01_f32, 0.0], [0.0, 0.01]]);
let r = Matrix::new([[0.5_f32]]);
let f_jac = Matrix::new([[1.0_f32, dt], [0.0, 1.0]]);
let mut ekf = Ekf::<f32, 2, 1>::new(x0, p0);
let mut steps = Vec::new();
for k in 1..=5 {
ekf.predict(
|x| Vector::from_array([x[0] + dt * x[1], x[1]]),
|_| f_jac,
Some(&q),
);
let x_predicted = ekf.x;
let p_predicted = ekf.p;
ekf.update(
&Vector::from_array([k as f32 * dt]),
|x| Vector::from_array([x[0]]),
|_| Matrix::new([[1.0_f32, 0.0]]),
&r,
)
.unwrap();
steps.push(EkfStep {
x_predicted,
p_predicted,
x_updated: ekf.x,
p_updated: ekf.p,
f_jacobian: f_jac,
});
}
let smoothed = rts_smooth(&steps).unwrap();
assert_eq!(smoothed.len(), 5);
assert!(smoothed[0].0[0].is_finite());
}
}
mod batch_tests {
use super::*;
#[test]
fn batch_single_observation() {
let mut lsq = BatchLsq::<f64, 1>::new();
let h = Matrix::new([[1.0_f64]]);
let r = Matrix::new([[0.1]]);
let z = Vector::from_array([3.0]);
lsq.add_observation(&z, &h, &r).unwrap();
let (x, p) = lsq.solve().unwrap();
approx_eq(x[0], 3.0, 1e-12);
approx_eq(p[(0, 0)], 0.1, 1e-12);
}
#[test]
fn batch_multiple_observations_converge() {
let mut lsq = BatchLsq::<f64, 2>::new();
let r = Matrix::new([[0.1]]);
let h1 = Matrix::new([[1.0, 0.0_f64]]);
lsq.add_observation(&Vector::from_array([1.05]), &h1, &r)
.unwrap();
lsq.add_observation(&Vector::from_array([0.95]), &h1, &r)
.unwrap();
lsq.add_observation(&Vector::from_array([1.02]), &h1, &r)
.unwrap();
let h2 = Matrix::new([[0.0, 1.0_f64]]);
lsq.add_observation(&Vector::from_array([2.1]), &h2, &r)
.unwrap();
lsq.add_observation(&Vector::from_array([1.9]), &h2, &r)
.unwrap();
lsq.add_observation(&Vector::from_array([2.03]), &h2, &r)
.unwrap();
let (x, _p) = lsq.solve().unwrap();
approx_eq(x[0], 1.0067, 0.01);
approx_eq(x[1], 2.01, 0.01);
}
#[test]
fn batch_with_prior() {
let x0 = Vector::from_array([0.0_f64, 0.0]);
let p0 = Matrix::new([[100.0, 0.0], [0.0, 100.0]]); let mut lsq = BatchLsq::<f64, 2>::with_prior(&x0, &p0).unwrap();
let h = Matrix::new([[1.0, 0.0_f64]]);
let r = Matrix::new([[0.1]]);
lsq.add_observation(&Vector::from_array([5.0]), &h, &r)
.unwrap();
let (x, _p) = lsq.solve().unwrap();
approx_eq(x[0], 5.0, 0.1);
}
#[test]
fn batch_overdetermined_system() {
let mut lsq = BatchLsq::<f64, 1>::new();
let h = Matrix::new([[1.0_f64]]);
let r = Matrix::new([[1.0]]);
let obs = [10.1, 9.8, 10.3, 9.9];
for &z in &obs {
lsq.add_observation(&Vector::from_array([z]), &h, &r)
.unwrap();
}
let (x, p) = lsq.solve().unwrap();
let mean = obs.iter().sum::<f64>() / obs.len() as f64;
approx_eq(x[0], mean, 1e-10);
approx_eq(p[(0, 0)], 0.25, 1e-10);
}
#[test]
fn batch_reset_works() {
let mut lsq = BatchLsq::<f64, 2>::new();
let h = Matrix::new([[1.0, 0.0_f64]]);
let r = Matrix::new([[0.1]]);
lsq.add_observation(&Vector::from_array([1.0]), &h, &r)
.unwrap();
lsq.reset();
assert_eq!(lsq.solve(), Err(EstimateError::SingularInnovation));
}
#[test]
fn batch_singular_r_error() {
let mut lsq = BatchLsq::<f64, 1>::new();
let h = Matrix::new([[1.0_f64]]);
let r = Matrix::new([[0.0]]);
let result =
lsq.add_observation(&Vector::from_array([1.0]), &h, &r);
assert_eq!(result, Err(EstimateError::SingularInnovation));
}
#[test]
fn batch_singular_info_error() {
let lsq = BatchLsq::<f64, 2>::new();
assert_eq!(lsq.solve(), Err(EstimateError::SingularInnovation));
}
#[test]
fn batch_mixed_measurement_dimensions() {
let mut lsq = BatchLsq::<f64, 2>::new();
let h1 = Matrix::new([[1.0, 0.0_f64]]);
let r1 = Matrix::new([[0.1]]);
lsq.add_observation(&Vector::from_array([3.0]), &h1, &r1)
.unwrap();
let h2 = Matrix::new([[1.0, 0.0], [0.0, 1.0_f64]]);
let r2 = Matrix::new([[0.1, 0.0], [0.0, 0.1]]);
lsq.add_observation(
&Vector::from_array([3.1, 7.0]),
&h2,
&r2,
)
.unwrap();
let (x, _p) = lsq.solve().unwrap();
approx_eq(x[0], 3.05, 0.05);
approx_eq(x[1], 7.0, 1e-10);
}
#[test]
fn batch_f32() {
let mut lsq = BatchLsq::<f32, 1>::new();
let h = Matrix::new([[1.0_f32]]);
let r = Matrix::new([[0.1_f32]]);
lsq.add_observation(&Vector::from_array([2.0_f32]), &h, &r)
.unwrap();
let (x, p) = lsq.solve().unwrap();
assert!((x[0] - 2.0).abs() < 1e-5);
assert!(p[(0, 0)] > 0.0);
}
}
#[cfg(feature = "alloc")]
mod cross_filter_tests {
use super::*;
#[test]
fn all_filters_agree_linear() {
let dt = 0.1;
let x0 = Vector::from_array([0.0_f64, 1.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let q = Matrix::new([[0.01, 0.0], [0.0, 0.01]]);
let r = Matrix::new([[0.5]]);
let mut ekf = Ekf::<f64, 2, 1>::new(x0, p0);
let mut ukf = Ukf::<f64, 2, 1>::with_params(x0, p0, 1.0, 2.0, 0.0);
let mut ckf = Ckf::<f64, 2, 1>::new(x0, p0);
let mut srukf =
SrUkf::<f64, 2, 1>::from_covariance_with_params(x0, p0, 1.0, 2.0, 0.0).unwrap();
let f = |x: &Vector<f64, 2>| {
Vector::from_array([x[0] + dt * x[1], x[1]])
};
let fj = |_x: &Vector<f64, 2>| Matrix::new([[1.0, dt], [0.0, 1.0]]);
let h = |x: &Vector<f64, 2>| Vector::from_array([x[0]]);
let hj = |_x: &Vector<f64, 2>| Matrix::new([[1.0, 0.0]]);
let measurements = [0.1, 0.22, 0.31, 0.45, 0.53, 0.68, 0.79, 0.9, 1.02, 1.11];
for &z_val in &measurements {
ekf.predict(f, fj, Some(&q));
ukf.predict(f, Some(&q)).unwrap();
ckf.predict(f, Some(&q)).unwrap();
srukf.predict(f, Some(&q)).unwrap();
let z = Vector::from_array([z_val]);
ekf.update(&z, h, hj, &r).unwrap();
ukf.update(&z, h, &r).unwrap();
ckf.update(&z, h, &r).unwrap();
srukf.update(&z, h, &r).unwrap();
}
for i in 0..2 {
let ekf_val = ekf.x[i];
approx_eq(ukf.x[i], ekf_val, 0.1);
approx_eq(ckf.x[i], ekf_val, 0.1);
approx_eq(srukf.x[i], ekf_val, 0.1);
}
}
#[test]
fn ckf_vs_ukf_nonlinear() {
let x0 = Vector::from_array([3.0_f64, 4.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let q = Matrix::new([[0.01, 0.0], [0.0, 0.01]]);
let r = Matrix::new([[0.1]]);
let mut ukf = Ukf::<f64, 2, 1>::new(x0, p0);
let mut ckf = Ckf::<f64, 2, 1>::new(x0, p0);
let h = |x: &Vector<f64, 2>| {
Vector::from_array([
(x[0] * x[0] + x[1] * x[1]).sqrt(),
])
};
let true_range = 5.0;
for _ in 0..10 {
ukf.predict(|x| *x, Some(&q)).unwrap();
ckf.predict(|x| *x, Some(&q)).unwrap();
let z = Vector::from_array([true_range]);
ukf.update(&z, h, &r).unwrap();
ckf.update(&z, h, &r).unwrap();
}
let ukf_range =
(ukf.x[0] * ukf.x[0] + ukf.x[1] * ukf.x[1]).sqrt();
let ckf_range =
(ckf.x[0] * ckf.x[0] + ckf.x[1] * ckf.x[1]).sqrt();
approx_eq(ukf_range, 5.0, 0.5);
approx_eq(ckf_range, 5.0, 0.5);
}
#[test]
fn srukf_vs_ukf_covariance_equivalence() {
let dt = 0.1;
let x0 = Vector::from_array([0.0_f64, 1.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let q = Matrix::new([[0.01, 0.0], [0.0, 0.01]]);
let r = Matrix::new([[0.5]]);
let mut ukf = Ukf::<f64, 2, 1>::with_params(x0, p0, 0.5, 2.0, 0.0);
let mut srukf =
SrUkf::<f64, 2, 1>::from_covariance_with_params(x0, p0, 0.5, 2.0, 0.0).unwrap();
let f = |x: &Vector<f64, 2>| {
Vector::from_array([x[0] + dt * x[1], x[1]])
};
let h = |x: &Vector<f64, 2>| Vector::from_array([x[0]]);
for k in 0..10 {
ukf.predict(f, Some(&q)).unwrap();
srukf.predict(f, Some(&q)).unwrap();
let z = Vector::from_array([(k + 1) as f64 * dt]);
ukf.update(&z, h, &r).unwrap();
srukf.update(&z, h, &r).unwrap();
}
let srukf_p = srukf.covariance();
for i in 0..2 {
for j in 0..2 {
approx_eq(ukf.p[(i, j)], srukf_p[(i, j)], 0.1);
}
}
}
#[test]
fn batch_vs_ekf_linear() {
let r_val = 0.5;
let r = Matrix::new([[r_val]]);
let measurements = [0.1, 0.22, 0.31, 0.45, 0.53];
let mut lsq = BatchLsq::<f64, 1>::new();
let h = Matrix::new([[1.0_f64]]);
for &z_val in &measurements {
lsq.add_observation(&Vector::from_array([z_val]), &h, &r)
.unwrap();
}
let (x_batch, _) = lsq.solve().unwrap();
let mean = measurements.iter().sum::<f64>() / measurements.len() as f64;
approx_eq(x_batch[0], mean, 1e-10);
}
}
#[test]
fn fd_jacobian_linear() {
let x = Vector::from_array([1.0_f64, 1.0]);
let jac = fd_jacobian(
&|x: &Vector<f64, 2>| {
Vector::from_array([x[0] + 2.0 * x[1], 3.0 * x[0] + 4.0 * x[1]])
},
&x,
);
approx_eq(jac[(0, 0)], 1.0, 1e-6);
approx_eq(jac[(0, 1)], 2.0, 1e-6);
approx_eq(jac[(1, 0)], 3.0, 1e-6);
approx_eq(jac[(1, 1)], 4.0, 1e-6);
}
#[test]
fn fd_jacobian_nonlinear() {
let x = Vector::from_array([3.0_f64, 4.0]);
let jac = fd_jacobian(
&|x: &Vector<f64, 2>| {
Vector::from_array([x[0] * x[0], x[0] * x[1]])
},
&x,
);
approx_eq(jac[(0, 0)], 6.0, 1e-5);
approx_eq(jac[(0, 1)], 0.0, 1e-5);
approx_eq(jac[(1, 0)], 4.0, 1e-5);
approx_eq(jac[(1, 1)], 3.0, 1e-5);
}
#[test]
fn fd_jacobian_rectangular() {
let x = Vector::from_array([1.0_f64, 2.0]);
let jac = fd_jacobian(
&|x: &Vector<f64, 2>| {
Vector::from_array([x[0], x[1], x[0] + x[1]])
},
&x,
);
assert_eq!(jac.nrows(), 3);
assert_eq!(jac.ncols(), 2);
approx_eq(jac[(0, 0)], 1.0, 1e-6);
approx_eq(jac[(0, 1)], 0.0, 1e-6);
approx_eq(jac[(1, 0)], 0.0, 1e-6);
approx_eq(jac[(1, 1)], 1.0, 1e-6);
approx_eq(jac[(2, 0)], 1.0, 1e-6);
approx_eq(jac[(2, 1)], 1.0, 1e-6);
}
#[test]
fn ekf_singular_innovation() {
let x0 = Vector::from_array([0.0_f64]);
let p0 = Matrix::new([[0.0]]); let r = Matrix::new([[0.0]]); let mut ekf = Ekf::<f64, 1, 1>::new(x0, p0);
let result = ekf.update(
&Vector::from_array([1.0]),
|x| *x,
|_x| Matrix::new([[1.0]]),
&r,
);
assert!(result.is_ok(), "expected Ok with jitter recovery, got {result:?}");
let nis = result.unwrap();
assert!(nis > 1e6, "NIS should be huge for inconsistent measurement, got {nis}");
approx_eq(ekf.x[0], 0.0, 1e-14);
}
#[test]
fn estimate_error_display() {
let e = EstimateError::CovarianceNotPD;
assert_eq!(
alloc::format!("{}", e),
"covariance matrix is not positive definite"
);
let e = EstimateError::SingularInnovation;
assert_eq!(
alloc::format!("{}", e),
"innovation covariance is singular"
);
let e = EstimateError::CholdowndateFailed;
assert_eq!(
alloc::format!("{}", e),
"Cholesky downdate failed: result not positive definite"
);
}
#[test]
fn cholesky_jitter_retries_near_singular() {
use super::cholesky_with_jitter;
use crate::linalg::CholeskyDecomposition;
let eps = 1e-10_f64;
let p = Matrix::new([[1.0 + eps, 1.0], [1.0, 1.0 + eps]]);
let singular = Matrix::new([[1.0_f64, 1.0], [1.0, 1.0]]);
assert!(CholeskyDecomposition::new(&singular).is_err());
let result = cholesky_with_jitter(&p);
assert!(result.is_ok());
let l = result.unwrap().l_full();
let p_reconstructed = l * l.transpose();
for i in 0..2 {
for j in 0..2 {
approx_eq(p_reconstructed[(i, j)], p[(i, j)], 1e-8);
}
}
}
#[cfg(feature = "alloc")]
#[test]
fn ukf_kskt_covariance_stays_pd_after_many_updates() {
use crate::linalg::CholeskyDecomposition;
let x0 = Vector::from_array([0.0_f64, 1.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let r = Matrix::new([[0.1]]);
let mut ukf = Ukf::<f64, 2, 1>::new(x0, p0);
for k in 0..200 {
let z_val = 0.1 * k as f64;
ukf.update(
&Vector::from_array([z_val]),
|x| Vector::from_array([x[0]]),
&r,
)
.unwrap();
assert!(CholeskyDecomposition::new(&ukf.p).is_ok(), "P non-PD at step {k}");
}
}
#[cfg(feature = "alloc")]
#[test]
fn ckf_kskt_covariance_stays_pd_after_many_updates() {
use crate::linalg::CholeskyDecomposition;
let x0 = Vector::from_array([0.0_f64, 1.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let r = Matrix::new([[0.1]]);
let mut ckf = Ckf::<f64, 2, 1>::new(x0, p0);
for k in 0..200 {
let z_val = 0.1 * k as f64;
ckf.update(
&Vector::from_array([z_val]),
|x| Vector::from_array([x[0]]),
&r,
)
.unwrap();
assert!(CholeskyDecomposition::new(&ckf.p).is_ok(), "P non-PD at step {k}");
}
}
#[cfg(feature = "alloc")]
#[test]
fn ukf_default_alpha_one_converges() {
let dt = 0.1;
let x0 = Vector::from_array([0.0_f64, 0.0]);
let p0 = Matrix::new([[10.0, 0.0], [0.0, 10.0]]);
let q = Matrix::new([[0.01, 0.0], [0.0, 0.01]]);
let r = Matrix::new([[0.1]]);
let mut ukf = Ukf::<f64, 2, 1>::new(x0, p0);
let mut true_pos = 5.0;
let true_vel = 1.0;
for _ in 0..100 {
ukf.predict(
|x| Vector::from_array([x[0] + dt * x[1], x[1]]),
Some(&q),
)
.unwrap();
true_pos += dt * true_vel;
ukf.update(
&Vector::from_array([true_pos]),
|x| Vector::from_array([x[0]]),
&r,
)
.unwrap();
}
approx_eq(ukf.x[0], true_pos, 0.5);
approx_eq(ukf.x[1], true_vel, 0.5);
}
#[test]
fn ekf_min_variance_floor_applied() {
let x0 = Vector::from_array([0.0_f64]);
let p0 = Matrix::new([[1.0_f64]]);
let r = Matrix::new([[1e-6_f64]]); let floor = 1e-4_f64;
let mut ekf = Ekf::<f64, 1, 1>::new(x0, p0).with_min_variance(floor);
for _ in 0..200 {
ekf.update(
&Vector::from_array([0.0]),
|x| *x,
|_x| Matrix::new([[1.0]]),
&r,
)
.unwrap();
assert!(ekf.p[(0, 0)] >= floor, "P fell below floor: {}", ekf.p[(0, 0)]);
}
}
#[cfg(feature = "alloc")]
#[test]
fn ukf_min_variance_floor_applied() {
let x0 = Vector::from_array([0.0_f64]);
let p0 = Matrix::new([[1.0_f64]]);
let r = Matrix::new([[1e-6_f64]]);
let floor = 1e-4_f64;
let mut ukf = Ukf::<f64, 1, 1>::new(x0, p0).with_min_variance(floor);
for _ in 0..200 {
ukf.update(
&Vector::from_array([0.0]),
|x| *x,
&r,
)
.unwrap();
assert!(ukf.p[(0, 0)] >= floor, "P fell below floor: {}", ukf.p[(0, 0)]);
}
}
#[test]
fn ekf_gate_rejects_large_innovation() {
let x0 = Vector::from_array([0.0_f64, 0.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let r = Matrix::new([[1.0]]);
let mut ekf = Ekf::<f64, 2, 1>::new(x0, p0);
let result = ekf
.update_gated(
&Vector::from_array([1000.0]),
|x| Vector::from_array([x[0]]),
|_x| Matrix::new([[1.0, 0.0]]),
&r,
10.0,
)
.unwrap();
assert!(result.is_none(), "large innovation should be gated out");
approx_eq(ekf.x[0], 0.0, 1e-14);
let result = ekf
.update_gated(
&Vector::from_array([0.5]),
|x| Vector::from_array([x[0]]),
|_x| Matrix::new([[1.0, 0.0]]),
&r,
10.0,
)
.unwrap();
assert!(result.is_some(), "consistent innovation should pass gate");
assert!(ekf.x[0] > 0.0, "state should move toward measurement");
}
#[cfg(feature = "alloc")]
#[test]
fn ukf_gate_rejects_large_innovation() {
let x0 = Vector::from_array([0.0_f64, 0.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let r = Matrix::new([[1.0]]);
let mut ukf = Ukf::<f64, 2, 1>::new(x0, p0);
let result = ukf
.update_gated(
&Vector::from_array([1000.0]),
|x| Vector::from_array([x[0]]),
&r,
10.0,
)
.unwrap();
assert!(result.is_none());
approx_eq(ukf.x[0], 0.0, 1e-14);
}
#[cfg(feature = "alloc")]
#[test]
fn ckf_gate_rejects_large_innovation() {
let x0 = Vector::from_array([0.0_f64, 0.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let r = Matrix::new([[1.0]]);
let mut ckf = Ckf::<f64, 2, 1>::new(x0, p0);
let result = ckf
.update_gated(
&Vector::from_array([1000.0]),
|x| Vector::from_array([x[0]]),
&r,
10.0,
)
.unwrap();
assert!(result.is_none());
approx_eq(ckf.x[0], 0.0, 1e-14);
}
#[cfg(feature = "alloc")]
#[test]
fn srukf_gate_rejects_large_innovation() {
let x0 = Vector::from_array([0.0_f64, 0.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let r = Matrix::new([[1.0]]);
let mut srukf = SrUkf::<f64, 2, 1>::from_covariance(x0, p0).unwrap();
let result = srukf
.update_gated(
&Vector::from_array([1000.0]),
|x| Vector::from_array([x[0]]),
&r,
10.0,
)
.unwrap();
assert!(result.is_none());
approx_eq(srukf.x[0], 0.0, 1e-14);
}
#[test]
fn iekf_nonlinear_measurement_more_accurate_than_ekf() {
let x0 = Vector::from_array([4.0_f64, 3.0]); let p0 = Matrix::new([[0.5, 0.0], [0.0, 0.5]]);
let r = Matrix::new([[0.001]]);
let true_x = Vector::from_array([3.0_f64, 4.0]);
let true_range = (3.0_f64 * 3.0 + 4.0 * 4.0).sqrt();
let h = |x: &Vector<f64, 2>| {
Vector::from_array([(x[0] * x[0] + x[1] * x[1]).sqrt()])
};
let hj = |x: &Vector<f64, 2>| {
let r = (x[0] * x[0] + x[1] * x[1]).sqrt();
Matrix::new([[x[0] / r, x[1] / r]])
};
let mut ekf = Ekf::<f64, 2, 1>::new(x0, p0);
let mut iekf = Ekf::<f64, 2, 1>::new(x0, p0);
let z = Vector::from_array([true_range]);
ekf.update(&z, h, hj, &r).unwrap();
iekf.update_iterated(&z, h, hj, &r, 20, 1e-8).unwrap();
let ekf_err = ((ekf.x[0] - true_x[0]).powi(2)
+ (ekf.x[1] - true_x[1]).powi(2))
.sqrt();
let iekf_err = ((iekf.x[0] - true_x[0]).powi(2)
+ (iekf.x[1] - true_x[1]).powi(2))
.sqrt();
assert!(
iekf_err <= ekf_err + 1e-8,
"IEKF err {iekf_err:.6} should be ≤ EKF err {ekf_err:.6}"
);
}
#[test]
fn iekf_linear_matches_ekf() {
let x0 = Vector::from_array([1.0_f64, 2.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let r = Matrix::new([[0.5]]);
let z = Vector::from_array([1.3]);
let h = |x: &Vector<f64, 2>| Vector::from_array([x[0]]);
let hj = |_x: &Vector<f64, 2>| Matrix::new([[1.0, 0.0]]);
let mut ekf = Ekf::<f64, 2, 1>::new(x0, p0);
let mut iekf = Ekf::<f64, 2, 1>::new(x0, p0);
ekf.update(&z, h, hj, &r).unwrap();
iekf.update_iterated(&z, h, hj, &r, 10, 1e-10).unwrap();
for i in 0..2 {
approx_eq(ekf.x[i], iekf.x[i], 1e-10);
for j in 0..2 {
approx_eq(ekf.p[(i, j)], iekf.p[(i, j)], 1e-10);
}
}
}
#[test]
fn iekf_fd_matches_explicit() {
let x0 = Vector::from_array([4.0_f64, 3.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let r = Matrix::new([[0.1]]);
let z = Vector::from_array([5.0]);
let h = |x: &Vector<f64, 2>| {
Vector::from_array([(x[0] * x[0] + x[1] * x[1]).sqrt()])
};
let hj = |x: &Vector<f64, 2>| {
let rng = (x[0] * x[0] + x[1] * x[1]).sqrt();
Matrix::new([[x[0] / rng, x[1] / rng]])
};
let mut iekf_explicit = Ekf::<f64, 2, 1>::new(x0, p0);
let mut iekf_fd = Ekf::<f64, 2, 1>::new(x0, p0);
iekf_explicit.update_iterated(&z, h, hj, &r, 20, 1e-10).unwrap();
iekf_fd.update_fd_iterated(&z, h, &r, 20, 1e-10).unwrap();
for i in 0..2 {
approx_eq(iekf_explicit.x[i], iekf_fd.x[i], 1e-5);
}
}
#[test]
fn ekf_fading_memory_inflates_covariance() {
let x0 = Vector::from_array([0.0_f64, 0.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let mut ekf_std = Ekf::<f64, 2, 1>::new(x0, p0);
let mut ekf_fade = Ekf::<f64, 2, 1>::new(x0, p0).with_fading_memory(1.05);
let f = |x: &Vector<f64, 2>| {
Vector::from_array([x[0] + 0.1 * x[1], x[1]])
};
let fj = |_x: &Vector<f64, 2>| Matrix::new([[1.0, 0.1], [0.0, 1.0]]);
ekf_std.predict(f, fj, None);
ekf_fade.predict(f, fj, None);
assert!(
ekf_fade.p[(0, 0)] > ekf_std.p[(0, 0)],
"fading covariance should exceed standard: {} vs {}",
ekf_fade.p[(0, 0)],
ekf_std.p[(0, 0)]
);
}
#[cfg(feature = "alloc")]
#[test]
fn ukf_fading_memory_inflates_covariance() {
let x0 = Vector::from_array([0.0_f64, 0.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let mut ukf_std = Ukf::<f64, 2, 1>::new(x0, p0);
let mut ukf_fade = Ukf::<f64, 2, 1>::new(x0, p0).with_fading_memory(1.05);
let f = |x: &Vector<f64, 2>| {
Vector::from_array([x[0] + 0.1 * x[1], x[1]])
};
ukf_std.predict(f, None).unwrap();
ukf_fade.predict(f, None).unwrap();
assert!(
ukf_fade.p[(0, 0)] > ukf_std.p[(0, 0)],
"fading covariance should exceed standard"
);
}
#[cfg(feature = "alloc")]
#[test]
fn ckf_fading_memory_inflates_covariance() {
let x0 = Vector::from_array([0.0_f64, 0.0]);
let p0 = Matrix::new([[1.0, 0.0], [0.0, 1.0]]);
let mut ckf_std = Ckf::<f64, 2, 1>::new(x0, p0);
let mut ckf_fade = Ckf::<f64, 2, 1>::new(x0, p0).with_fading_memory(1.05);
let f = |x: &Vector<f64, 2>| {
Vector::from_array([x[0] + 0.1 * x[1], x[1]])
};
ckf_std.predict(f, None).unwrap();
ckf_fade.predict(f, None).unwrap();
assert!(ckf_fade.p[(0, 0)] > ckf_std.p[(0, 0)]);
}