use core::f32::consts::{PI, TAU};
use nalgebra::{ComplexField, Matrix3, Quaternion, SMatrix, UnitQuaternion, Vector3};
use crate::state::{CovarianceMatrix, ErrorState, Offset, STATES};
use crate::units::Seconds;
const MAX_INFLATION: f32 = 1.0e6;
pub(crate) fn correlation_inflation(interval: Option<Seconds>, tau: Option<Seconds>) -> f32 {
let (Some(dt), Some(tau)) = (interval, tau) else {
return 1.0;
};
let (dt, tau) = (dt.as_secs(), tau.as_secs());
if !(dt >= 0.0 && tau > 0.0) {
return 1.0;
}
let x = -ComplexField::exp_m1(-dt / tau);
((2.0 - x) / x).min(MAX_INFLATION)
}
#[rustfmt::skip]
pub(crate) fn skew(u: Vector3<f32>) -> Matrix3<f32> {
Matrix3::new(
0.0, -u.z, u.y,
u.z, 0.0, -u.x,
-u.y, u.x, 0.0,
)
}
pub(crate) fn exp_quat(phi: Vector3<f32>) -> UnitQuaternion<f32> {
let angle = phi.norm();
let half = 0.5 * angle;
let scale = if angle < SMALL_ANGLE {
0.5 - angle * angle / 48.0
} else {
ComplexField::sin(half) / angle
};
UnitQuaternion::new_unchecked(Quaternion::new(
ComplexField::cos(half),
scale * phi.x,
scale * phi.y,
scale * phi.z,
))
}
const SMALL_ANGLE: f32 = 1.0e-3;
pub(crate) fn wrap_pi(angle: f32) -> f32 {
let remainder = angle % TAU; if remainder > PI {
remainder - TAU
} else if remainder <= -PI {
remainder + TAU
} else {
remainder
}
}
pub(crate) fn enforce_symmetry<const N: usize>(p: &mut SMatrix<f32, N, N>) {
for i in 0..N {
for j in (i + 1)..N {
let mean = 0.5 * (p[(i, j)] + p[(j, i)]);
p[(i, j)] = mean;
p[(j, i)] = mean;
}
}
}
#[rustfmt::skip]
const FLOOR: [f32; STATES] = [
1e-6, 1e-6, 1e-6, 1e-6, 1e-6, 1e-6, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, ];
pub(crate) fn floor_offset(offset: &mut Offset) -> bool {
let floor = FLOOR[ErrorState::PositionDown.index()];
let raised = offset.variance < floor;
if raised {
offset.variance = floor;
}
raised
}
pub(crate) fn below_floor(p: &CovarianceMatrix) -> bool {
FLOOR
.iter()
.enumerate()
.any(|(i, floor)| p[(i, i)] < *floor)
}
pub(crate) fn floor_diagonal(p: &mut CovarianceMatrix) -> u32 {
let mut raised = 0;
for (i, floor) in FLOOR.iter().enumerate() {
if p[(i, i)] < *floor {
p[(i, i)] = *floor;
raised += 1;
}
}
raised
}
#[cfg(test)]
mod tests {
use super::*;
use nalgebra::SVector;
fn inflation(dt: f32, tau: f32) -> f32 {
correlation_inflation(Some(Seconds::from_secs(dt)), Some(Seconds::from_secs(tau)))
}
#[test]
fn the_inflation_of_24_prime_is_its_definition() {
let rho = (-1.0f32).exp();
assert!((inflation(0.5, 0.5) - (1.0 + rho) / (1.0 - rho)).abs() < 1e-5);
}
#[test]
fn the_inflation_limits_are_white_and_two_tau_over_the_interval() {
assert_eq!(inflation(1e3, 1.0), 1.0);
let short = inflation(1e-4, 14.0);
assert!((short / (2.0 * 14.0 / 1e-4) - 1.0).abs() < 1e-3, "{short}");
}
#[test]
fn the_same_error_twice_is_worth_nothing_and_stays_finite() {
for (dt, tau) in [(0.0, 4.2), (0.2, f32::INFINITY), (0.2, f32::MAX)] {
assert_eq!(inflation(dt, tau), MAX_INFLATION, "Δt {dt}, τ {tau}");
}
}
#[test]
fn with_nothing_to_measure_or_nothing_configured_the_measurement_is_white() {
let dt = Some(Seconds::from_secs(0.2));
for (interval, tau) in [
(None, Some(Seconds::from_secs(4.2))),
(dt, None),
(dt, Some(Seconds::ZERO)),
(dt, Some(Seconds::from_secs(-1.0))),
(dt, Some(Seconds::from_secs(f32::NAN))),
] {
assert_eq!(correlation_inflation(interval, tau), 1.0);
}
}
use crate::state::ErrorState;
const VECTORS: [Vector3<f32>; 5] = [
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(0.3, -1.7, 2.5),
Vector3::new(-4.0, 0.25, 0.0),
Vector3::new(-0.01, -0.02, -0.03),
];
#[test]
fn the_skew_matrix_is_the_cross_product() {
for u in VECTORS {
for v in VECTORS {
assert_eq!(skew(u) * v, u.cross(&v), "u = {u:?}, v = {v:?}");
}
}
}
#[test]
fn the_skew_matrix_is_skew_symmetric() {
for u in VECTORS {
assert_eq!(skew(u).transpose(), -skew(u), "u = {u:?}");
}
}
#[test]
fn exp_agrees_with_the_library_for_ordinary_angles() {
let rotations = [
Vector3::new(0.0, 0.0, core::f32::consts::FRAC_PI_2),
Vector3::new(0.1, -0.2, 0.3),
Vector3::new(PI, 0.0, 0.0),
Vector3::new(-1.5, 2.5, -0.5),
Vector3::new(0.0, 1.3e-3, 0.0),
];
for phi in rotations {
let ours = exp_quat(phi).into_inner();
let theirs = UnitQuaternion::from_scaled_axis(phi).into_inner();
assert!(
(ours - theirs).norm() < 1e-6,
"phi = {phi:?}: {ours:?} vs {theirs:?}"
);
}
}
#[test]
fn exp_stays_finite_and_unit_where_the_scale_factor_is_zero_over_zero() {
for phi in [
Vector3::zeros(),
Vector3::new(1.0e-9, 0.0, 0.0),
Vector3::new(-1.0e-20, 1.0e-20, 0.0),
Vector3::new(5.0e-4, -5.0e-4, 5.0e-4),
] {
let q = exp_quat(phi).into_inner();
assert!(q.norm().is_finite(), "phi = {phi:?}: {q:?}");
assert!((q.norm() - 1.0).abs() < 1e-6, "phi = {phi:?}: {q:?}");
}
}
#[test]
fn exp_keeps_the_first_order_term_the_library_drops() {
let phi = Vector3::new(1.0e-9, 0.0, 0.0);
let ours = exp_quat(phi).into_inner();
assert_eq!(ours.i, 5.0e-10);
assert_eq!(ours.w, 1.0);
assert_eq!(
UnitQuaternion::from_scaled_axis(phi),
UnitQuaternion::identity()
);
}
#[test]
fn exp_of_zero_is_the_identity() {
assert_eq!(exp_quat(Vector3::zeros()), UnitQuaternion::identity());
}
#[test]
fn wrap_closes_the_interval_at_plus_pi() {
assert_eq!(wrap_pi(PI), PI);
assert_eq!(wrap_pi(-PI), PI);
}
#[test]
fn wrap_moves_an_angle_by_whole_turns_only() {
let cases = [
(PI - 0.01, PI - 0.01),
(PI + 0.01, -PI + 0.01),
(-PI + 0.01, -PI + 0.01),
(-PI - 0.01, PI - 0.01),
(TAU + 0.5, 0.5),
(-TAU - 0.5, -0.5),
(3.0 * PI, PI),
(-3.0 * PI, -PI),
(0.0, 0.0),
];
for (angle, expected) in cases {
let wrapped = wrap_pi(angle);
assert!(
(wrapped - expected).abs() < 1e-5,
"wrap_pi({angle}) = {wrapped}, expected {expected}"
);
assert!(
wrapped > -PI && wrapped <= PI,
"wrap_pi({angle}) = {wrapped} is outside (-pi, pi]"
);
}
}
#[test]
fn wrap_lands_inside_the_interval_at_every_magnitude() {
for exponent in 0..255u32 {
for mantissa in [0, 1, 0x2a_aaaa, 0x7f_ffff] {
let magnitude = f32::from_bits((exponent << 23) | mantissa);
for angle in [magnitude, -magnitude] {
let wrapped = wrap_pi(angle);
assert!(
wrapped > -PI && wrapped <= PI,
"wrap_pi({angle:e}) = {wrapped} is outside (-pi, pi]"
);
}
}
}
}
#[test]
fn wrap_carries_a_non_finite_angle_through_rather_than_inventing_one() {
assert!(wrap_pi(f32::NAN).is_nan());
assert!(wrap_pi(f32::INFINITY).is_nan());
assert!(wrap_pi(f32::NEG_INFINITY).is_nan());
}
#[test]
fn wrap_leaves_an_angle_already_in_range_alone() {
let mut angle = -PI + 0.001;
while angle < PI {
assert_eq!(wrap_pi(angle), angle);
angle += 0.1;
}
}
fn asymmetric() -> CovarianceMatrix {
CovarianceMatrix::from_fn(|i, j| (i * 15 + j) as f32 * 0.125)
}
#[test]
fn symmetry_enforcement_is_idempotent() {
let mut p = asymmetric();
enforce_symmetry(&mut p);
let once = p;
enforce_symmetry(&mut p);
assert_eq!(p, once);
assert_eq!(p, p.transpose());
}
#[test]
fn symmetry_enforcement_leaves_a_symmetric_matrix_bit_identical() {
let mut p = CovarianceMatrix::from_fn(|i, j| ((i + 1) * (j + 1)) as f32 * 0.3);
let before = p;
enforce_symmetry(&mut p);
assert_eq!(p, before);
}
#[test]
fn symmetry_enforcement_averages_the_two_halves() {
let mut p = asymmetric();
let before = p;
enforce_symmetry(&mut p);
assert_eq!(p[(2, 7)], 0.5 * (before[(2, 7)] + before[(7, 2)]));
assert_eq!(p.diagonal(), before.diagonal());
}
#[test]
fn the_floor_raises_a_collapsed_diagonal_to_its_own_floor_and_counts_every_entry() {
let mut p = CovarianceMatrix::zeros();
assert_eq!(floor_diagonal(&mut p), STATES as u32);
for i in 0..STATES {
assert_eq!(p[(i, i)], FLOOR[i], "state {i}");
}
}
#[test]
fn the_floor_leaves_the_variances_the_filter_actually_reaches_alone() {
#[rustfmt::skip]
let smallest = [
5.1e-3, 5.1e-3, 5.1e-3,
2.6e-3, 2.6e-3, 2.6e-3,
2.6e-4, 2.6e-4, 2.6e-4,
3.5e-3, 3.5e-3, 3.5e-3,
1.6e-5, 1.6e-5, 1.6e-5,
];
let mut p = CovarianceMatrix::from_diagonal(&SVector::from(smallest));
let before = p;
assert_eq!(floor_diagonal(&mut p), 0);
assert_eq!(p, before);
}
#[test]
fn each_floor_sits_against_the_state_it_was_measured_for() {
assert_eq!(FLOOR[ErrorState::PositionNorth.index()], 1e-6);
assert_eq!(FLOOR[ErrorState::VelocityNorth.index()], 1e-6);
assert_eq!(FLOOR[ErrorState::AttitudeX.index()], 1e-9);
assert_eq!(FLOOR[ErrorState::AccelBiasX.index()], 1e-9);
assert_eq!(FLOOR[ErrorState::GyroBiasX.index()], 1e-9);
}
#[test]
fn the_below_floor_test_agrees_with_the_floor_it_names() {
for i in 0..STATES {
let mut p = CovarianceMatrix::from_diagonal_element(1.0);
p[(i, i)] = FLOOR[i] * 0.5;
assert!(below_floor(&p), "state {i} below its floor");
p[(i, i)] = FLOOR[i];
assert!(!below_floor(&p), "state {i} exactly at its floor");
}
assert!(!below_floor(&CovarianceMatrix::from_diagonal_element(1.0)));
assert!(below_floor(&CovarianceMatrix::zeros()));
let mut negative = CovarianceMatrix::from_diagonal_element(1.0);
negative[(4, 4)] = -1.0;
assert!(below_floor(&negative));
}
#[test]
fn the_floor_is_per_group_rather_than_one_number_for_the_matrix() {
let mut p = CovarianceMatrix::from_diagonal_element(1e-7);
assert_eq!(floor_diagonal(&mut p), 6);
assert_eq!(p[(0, 0)], 1e-6);
assert_eq!(p[(5, 5)], 1e-6);
assert_eq!(p[(6, 6)], 1e-7);
assert_eq!(p[(14, 14)], 1e-7);
}
#[test]
fn the_floor_touches_nothing_off_the_diagonal() {
let mut p = CovarianceMatrix::from_fn(|i, j| if i == j { 0.0 } else { 0.25 });
let before = p;
assert_eq!(floor_diagonal(&mut p), STATES as u32);
for i in 0..STATES {
for j in 0..STATES {
if i != j {
assert_eq!(p[(i, j)], before[(i, j)], "({i}, {j})");
}
}
}
assert_eq!(p, p.transpose());
}
#[test]
fn the_floor_leaves_a_nan_variance_for_the_finiteness_check_to_refuse() {
let mut p = CovarianceMatrix::zeros();
p[(3, 3)] = f32::NAN;
assert_eq!(floor_diagonal(&mut p), (STATES - 1) as u32);
assert!(p[(3, 3)].is_nan());
}
}