use nalgebra::{Matrix3, SMatrix, SVector, Vector3};
use crate::frames::{Body, Ned};
use crate::health::{Status, Validity};
use crate::math::enforce_symmetry;
use crate::units::{Acceleration, AngularRate, Attitude, Position, Velocity};
pub const STATES: usize = 15;
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct State {
pub attitude: Attitude,
pub position: Position<Ned>,
pub velocity: Velocity<Ned>,
pub accel_bias: Acceleration<Body>,
pub gyro_bias: AngularRate<Body>,
pub status: Status,
pub validity: Validity,
}
impl State {
pub(crate) fn is_finite(&self) -> bool {
let q = self.attitude.quaternion();
[q.w, q.i, q.j, q.k].iter().all(|v| v.is_finite())
&& self.position.is_finite()
&& self.velocity.is_finite()
&& self.accel_bias.is_finite()
&& self.gyro_bias.is_finite()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[repr(usize)]
pub enum ErrorState {
PositionNorth = 0,
PositionEast = 1,
PositionDown = 2,
VelocityNorth = 3,
VelocityEast = 4,
VelocityDown = 5,
AttitudeX = 6,
AttitudeY = 7,
AttitudeZ = 8,
AccelBiasX = 9,
AccelBiasY = 10,
AccelBiasZ = 11,
GyroBiasX = 12,
GyroBiasY = 13,
GyroBiasZ = 14,
}
impl ErrorState {
pub const fn index(self) -> usize {
self as usize
}
}
pub(crate) type CovarianceMatrix = SMatrix<f32, STATES, STATES>;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Covariance(CovarianceMatrix);
impl Covariance {
pub fn zero() -> Self {
Self(CovarianceMatrix::zeros())
}
pub fn from_sigmas(sigmas: [f32; STATES]) -> Self {
let variances = SVector::<f32, STATES>::from_iterator(sigmas.iter().map(|s| s * s));
Self(CovarianceMatrix::from_diagonal(&variances))
}
pub fn from_rows(p: [[f32; STATES]; STATES]) -> Self {
Self(CovarianceMatrix::from_fn(|row, column| p[row][column]))
}
pub(crate) const fn from_matrix(p: CovarianceMatrix) -> Self {
Self(p)
}
pub fn get(&self, row: ErrorState, column: ErrorState) -> f32 {
self.0[(row.index(), column.index())]
}
pub fn variance(&self, state: ErrorState) -> f32 {
self.get(state, state)
}
pub(crate) fn reset_state(&mut self, state: ErrorState, variance: f32) {
let i = state.index();
for k in 0..STATES {
self.0[(i, k)] = 0.0;
self.0[(k, i)] = 0.0;
}
self.0[(i, i)] = variance;
}
pub(crate) fn set_attitude_block(&mut self, block: Matrix3<f32>) {
let theta = ErrorState::AttitudeX.index();
self.0
.fixed_view_mut::<3, 3>(theta, theta)
.copy_from(&block);
}
pub(crate) fn set_attitude_accel_bias_block(&mut self, block: Matrix3<f32>) {
let (theta, beta) = (
ErrorState::AttitudeX.index(),
ErrorState::AccelBiasX.index(),
);
self.0.fixed_view_mut::<3, 3>(theta, beta).copy_from(&block);
self.0
.fixed_view_mut::<3, 3>(beta, theta)
.copy_from(&block.transpose());
}
pub(crate) fn reset_attitude_direction(&mut self, u: Vector3<f32>, variance: f32) {
let theta = ErrorState::AttitudeX.index();
let m = Matrix3::identity() - u * u.transpose();
let rows = m * self.0.fixed_rows::<3>(theta);
self.0.fixed_rows_mut::<3>(theta).copy_from(&rows);
let columns = self.0.fixed_columns::<3>(theta) * m;
self.0.fixed_columns_mut::<3>(theta).copy_from(&columns);
let block = self.0.fixed_view::<3, 3>(theta, theta) + u * u.transpose() * variance;
self.0
.fixed_view_mut::<3, 3>(theta, theta)
.copy_from(&block);
enforce_symmetry(&mut self.0);
}
pub(crate) fn reset_block<const N: usize>(
&mut self,
states: [ErrorState; N],
variances: [f32; N],
) {
for (state, variance) in states.iter().zip(variances) {
self.reset_state(*state, variance);
}
}
pub fn to_rows(&self) -> [[f32; STATES]; STATES] {
core::array::from_fn(|row| core::array::from_fn(|column| self.0[(row, column)]))
}
pub(crate) const fn as_matrix(&self) -> &CovarianceMatrix {
&self.0
}
pub(crate) fn is_finite(&self) -> bool {
self.0.iter().all(|entry| entry.is_finite())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct AttitudeVariance {
pub tilt_north: f32,
pub tilt_east: f32,
pub heading: f32,
}
impl AttitudeVariance {
pub(crate) fn of(attitude: &Attitude, covariance: &Covariance) -> Self {
let r = attitude.quaternion().to_rotation_matrix().into_inner();
let theta = ErrorState::AttitudeX.index();
let p_theta = covariance.as_matrix().fixed_view::<3, 3>(theta, theta);
let ned = r * p_theta * r.transpose();
Self {
tilt_north: ned[(0, 0)],
tilt_east: ned[(1, 1)],
heading: ned[(2, 2)],
}
}
pub fn to_enu(self) -> [f32; 3] {
[self.tilt_east, self.tilt_north, self.heading]
}
pub(crate) fn in_body(self, attitude: &Attitude) -> Matrix3<f32> {
let r = attitude.quaternion().to_rotation_matrix().into_inner();
let ned =
Matrix3::from_diagonal(&Vector3::new(self.tilt_north, self.tilt_east, self.heading));
let mut body = r.transpose() * ned * r;
enforce_symmetry(&mut body);
body
}
}
impl Default for Covariance {
fn default() -> Self {
Self::zero()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub(crate) struct Offset {
pub(crate) cross: SVector<f32, STATES>,
pub(crate) variance: f32,
}
impl Offset {
pub(crate) fn independent(variance: f32) -> Self {
Self {
cross: SVector::zeros(),
variance,
}
}
pub(crate) fn from_estimate(covariance: &Covariance, noise_variance: f32) -> Self {
let down = ErrorState::PositionDown;
Self {
cross: -covariance.as_matrix().column(down.index()),
variance: covariance.variance(down) + noise_variance,
}
}
pub(crate) fn decorrelate(&mut self, state: ErrorState) {
self.cross[state.index()] = 0.0;
}
pub(crate) fn decorrelate_attitude_direction(&mut self, u: Vector3<f32>) {
let theta = ErrorState::AttitudeX.index();
let m = Matrix3::identity() - u * u.transpose();
let cross = m * self.cross.fixed_rows::<3>(theta);
self.cross.fixed_rows_mut::<3>(theta).copy_from(&cross);
}
pub(crate) fn is_finite(&self) -> bool {
self.variance.is_finite() && self.cross.iter().all(|entry| entry.is_finite())
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::f32::consts::FRAC_PI_2;
use nalgebra::UnitQuaternion;
fn attitude_of(roll: f32, pitch: f32, yaw: f32) -> Attitude {
Attitude::from_quaternion(UnitQuaternion::from_euler_angles(roll, pitch, yaw))
}
#[test]
fn the_array_is_rows_in_and_out() {
let mut rows = [[0.0; STATES]; STATES];
rows[ErrorState::PositionNorth.index()][ErrorState::GyroBiasZ.index()] = 1.0;
let covariance = Covariance::from_rows(rows);
assert_eq!(
covariance.get(ErrorState::PositionNorth, ErrorState::GyroBiasZ),
1.0
);
assert_eq!(
covariance.get(ErrorState::GyroBiasZ, ErrorState::PositionNorth),
0.0
);
assert_eq!(covariance.to_rows(), rows);
}
#[test]
fn enu_order_swaps_the_tilts_and_keeps_heading_last() {
let variance = AttitudeVariance {
tilt_north: 1.0,
tilt_east: 2.0,
heading: 3.0,
};
assert_eq!(variance.to_enu(), [2.0, 1.0, 3.0]);
}
#[test]
fn the_attitude_blocks_a_start_and_a_heading_reset_write_are_exactly_symmetric() {
let variance = AttitudeVariance {
tilt_north: 4.1e-4,
tilt_east: 3.7e-4,
heading: 0.29,
};
for (roll, pitch, yaw) in [(0.3, -0.2, 1.1), (0.01, 0.02, -2.9), (-0.7, 0.4, 0.5)] {
let attitude = attitude_of(roll, pitch, yaw);
let block = variance.in_body(&attitude);
assert_eq!(
block,
block.transpose(),
"in_body at {roll}, {pitch}, {yaw}"
);
let mut p = with_attitude_block(block);
let down = attitude
.quaternion()
.inverse_transform_vector(&Vector3::z());
p.reset_attitude_direction(down, 0.05);
let p = p.as_matrix();
assert_eq!(*p, p.transpose(), "reset at {roll}, {pitch}, {yaw}");
}
}
fn with_attitude_block(block: Matrix3<f32>) -> Covariance {
let mut p = CovarianceMatrix::identity();
let theta = ErrorState::AttitudeX.index();
p.fixed_view_mut::<3, 3>(theta, theta).copy_from(&block);
Covariance::from_matrix(p)
}
#[test]
fn level_and_facing_north_the_body_diagonal_is_tilt_and_heading() {
let p = with_attitude_block(Matrix3::from_diagonal(&Vector3::new(1.0, 2.0, 3.0)));
let variance = AttitudeVariance::of(&Attitude::default(), &p);
assert_eq!((variance.tilt_north, variance.tilt_east), (1.0, 2.0));
assert_eq!(variance.heading, 3.0);
}
#[test]
fn on_its_tail_body_x_is_the_heading_axis() {
let p = with_attitude_block(Matrix3::from_diagonal(&Vector3::new(1.0, 2.0, 3.0)));
let variance = AttitudeVariance::of(&attitude_of(0.0, FRAC_PI_2, 0.0), &p);
assert!((variance.heading - 1.0).abs() < 1e-6, "{variance:?}");
assert!((variance.tilt_east - 2.0).abs() < 1e-6, "{variance:?}");
assert!((variance.tilt_north - 3.0).abs() < 1e-6, "{variance:?}");
}
#[test]
fn in_body_is_the_inverse_of_of() {
let attitude = attitude_of(0.4, 1.2, -2.0);
let ned = AttitudeVariance {
tilt_north: 1e-4,
tilt_east: 4e-4,
heading: 0.1,
};
let p = with_attitude_block(ned.in_body(&attitude));
let back = AttitudeVariance::of(&attitude, &p);
assert!((back.tilt_north - ned.tilt_north).abs() < 1e-7, "{back:?}");
assert!((back.tilt_east - ned.tilt_east).abs() < 1e-7, "{back:?}");
assert!((back.heading - ned.heading).abs() < 1e-6, "{back:?}");
}
#[test]
fn a_reset_along_a_body_axis_is_reset_state() {
let mut p = CovarianceMatrix::from_fn(|i, j| 0.01 * (1 + i.min(j)) as f32);
p.fill_diagonal(1.0);
let mut along = Covariance::from_matrix(p);
along.reset_attitude_direction(Vector3::z(), 0.25);
let mut axis = Covariance::from_matrix(p);
axis.reset_state(ErrorState::AttitudeZ, 0.25);
assert_eq!(along, axis);
}
#[test]
fn a_reset_along_a_direction_leaves_the_orthogonal_components_alone() {
let u = Vector3::new(0.0, 0.6, 0.8);
let w = Vector3::new(0.0, 0.8, -0.6);
let mut p = CovarianceMatrix::from_fn(|i, j| 0.01 * (1 + i.min(j)) as f32);
p.fill_diagonal(1.0);
let theta = ErrorState::AttitudeX.index();
let before = p.fixed_view::<3, 3>(theta, theta).into_owned();
let mut covariance = Covariance::from_matrix(p);
covariance.reset_attitude_direction(u, 0.25);
let q = covariance.as_matrix();
let block = q.fixed_view::<3, 3>(theta, theta).into_owned();
assert!(((u.transpose() * block * u)[0] - 0.25).abs() < 1e-6);
assert!((u.transpose() * block * w)[0].abs() < 1e-6);
assert!(((w.transpose() * block * w)[0] - (w.transpose() * before * w)[0]).abs() < 1e-6);
for k in (0..STATES).filter(|k| !(theta..theta + 3).contains(k)) {
let cross = u.dot(&q.fixed_view::<3, 1>(theta, k).into_owned());
assert!(
cross.abs() < 1e-6,
"the reset component correlates with {k}: {cross}"
);
}
}
}