use crate::config::{Config, ConfigError};
use crate::frames::Body;
use crate::geodetic::LocalOrigin;
use crate::gsf::YawEstimator;
use crate::health::{Diagnostics, Status};
use crate::math::{floor_diagonal, floor_offset};
use crate::state::{Covariance, Offset, State};
use crate::units::{Altitude, AngularRate, Radians, Timestamp};
mod adopt;
mod estimate;
mod fuse;
mod heading;
mod hold;
mod predict;
mod site;
mod start;
mod validity;
mod yaw;
use estimate::Estimate;
#[derive(Clone, Debug)]
pub struct Eskf {
config: Config,
estimate: Estimate,
covariance: Covariance,
diagnostics: Diagnostics,
baro_reference: Option<Altitude>,
offset: Offset,
origin: Option<LocalOrigin>,
declination: Radians,
declination_set: bool,
magnetic_north: bool,
angular_rate: Option<AngularRate<Body>>,
earliest: Timestamp,
unestablished: Unestablished,
aligned: bool,
hold: hold::HoldState,
yaw_estimator: YawEstimator,
yaw_replaced: yaw::YawReplaced,
initialized: bool,
time: Timestamp,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct Unestablished {
position: bool,
velocity: bool,
heading: bool,
}
impl Default for Eskf {
fn default() -> Self {
Self::with(Config::default())
}
}
impl Eskf {
pub fn new(config: Config) -> Result<Self, ConfigError> {
config.validate()?;
Ok(Self::with(config))
}
fn with(config: Config) -> Self {
Self {
config,
estimate: Estimate::default(),
covariance: Covariance::zero(),
diagnostics: Diagnostics::default(),
baro_reference: None,
offset: Offset::default(),
origin: None,
declination: Radians::ZERO,
declination_set: false,
magnetic_north: false,
angular_rate: None,
earliest: Timestamp::ZERO,
unestablished: Unestablished::default(),
aligned: false,
hold: hold::HoldState::default(),
yaw_estimator: YawEstimator::default(),
yaw_replaced: yaw::YawReplaced::default(),
initialized: false,
time: Timestamp::ZERO,
}
}
pub const fn config(&self) -> &Config {
&self.config
}
pub const fn is_initialized(&self) -> bool {
self.initialized
}
pub const fn time(&self) -> Option<Timestamp> {
if self.initialized {
Some(self.time)
} else {
None
}
}
fn commit_offset(&mut self, mut offset: Offset) {
if self.baro_reference.is_none() {
self.offset = Offset::default();
return;
}
if floor_offset(&mut offset) {
self.diagnostics.floored = self.diagnostics.floored.saturating_add(1);
}
self.offset = offset;
}
fn commit_covariance(&mut self, covariance: Covariance, offset: Offset) {
let mut matrix = *covariance.as_matrix();
let raised = floor_diagonal(&mut matrix);
self.covariance = Covariance::from_matrix(matrix);
self.commit_offset(offset);
self.diagnostics.floored = self.diagnostics.floored.saturating_add(raised);
}
pub fn state(&self) -> State {
let validity = self.validity();
let mut state = *self.estimate.state();
state.status = self.derive_status();
state.validity = validity;
state
}
pub const fn angular_rate(&self) -> Option<AngularRate<Body>> {
self.angular_rate
}
pub const fn diagnostics(&self) -> &Diagnostics {
&self.diagnostics
}
pub const fn covariance(&self) -> &Covariance {
&self.covariance
}
fn derive_status(&self) -> Status {
let d = &self.diagnostics;
let horizontal = d
.horizontal()
.any(|source| source.accepted_within(self.config.timeouts.dead_reckoning_after));
if !horizontal {
Status::DeadReckoning
} else if !self.aligned {
Status::Aligning
} else if d
.aiding()
.filter(|source| source.has_been_used())
.all(|source| source.is_fresh(&self.config.timeouts))
{
Status::Healthy
} else {
Status::Degraded
}
}
}
#[cfg(test)]
mod adversarial;
#[cfg(test)]
mod fixtures;
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{Config, Recovery};
use crate::eskf::fixtures::*;
use crate::geodetic::Geodetic;
use crate::health::{Fusion, GnssFusion, Propagation, Refusal, Status};
use crate::init::tests::{spaced, still};
use crate::init::{Alignment, InitError, StaticSample, StaticWindow};
use crate::propagate::ImuSample;
use crate::state::{Covariance, Offset, State};
use crate::units::{
Acceleration, Altitude, AltitudeNoise, AngularRate, HeadingNoise, MagField, Position,
PositionNoise, Radians, Seconds, Timestamp, Velocity, VelocityNoise,
};
#[test]
fn a_config_outside_its_bounds_builds_no_filter() {
let config = Config {
recovery: Recovery {
gnss_height: Some(Seconds::from_secs(f32::NAN)),
..Recovery::default()
},
..Config::default()
};
let refused = Eskf::new(config).err();
assert_eq!(
refused.map(|e| (e.field, e.bound)),
Some(("recovery.gnss_height", crate::ConfigBound::Positive))
);
assert!(refused.is_some_and(|e| e.value.is_nan()));
assert_eq!(Eskf::default().config(), &Config::default());
}
#[test]
fn a_filter_with_no_barometric_reference_holds_no_offset() {
let mut filter = initialized();
assert_eq!(filter.baro_reference(), None);
for _ in 0..10 {
assert_eq!(filter.step(still().imu, DT), Propagation::Propagated);
}
assert_eq!(filter.offset, Offset::default());
}
#[test]
fn a_start_that_overflows_is_refused_and_commits_nothing() {
let mut filter = Eskf::default();
let imu = ImuSample::from_rates(
Timestamp::from_micros(1_000_000),
AngularRate::body(0.0, 0.0, 0.0),
Acceleration::body(0.0, 0.0, f32::MAX),
DT,
);
assert_eq!(filter.initialize_coarse(imu), Err(InitError::NotFinite));
assert!(!filter.is_initialized());
}
#[test]
fn a_still_window_whose_barometric_reference_overflows_is_refused_whole() {
let mut altitudes = [100.0, 100.2, 99.9, 100.1, 100.0, 99.8, 100.3, 100.0];
altitudes[3] = f32::MAX;
let window = StaticWindow::try_from(
spaced(&window_with_baro(altitudes), Seconds::from_secs(0.25)).as_slice(),
)
.expect("every sample is finite");
let mut filter = Eskf::default();
assert_eq!(filter.alignment_of(&window), Err(InitError::NotFinite));
assert_eq!(filter.initialize(&window), Err(InitError::NotFinite));
assert!(!filter.is_initialized());
assert_eq!(filter.baro_reference(), None);
}
#[cfg(feature = "magnetic-model")]
#[test]
fn a_geodetic_fix_with_no_origin_under_it_turns_no_heading() {
let mut filter = Eskf::default();
let _ = filter
.initialize_over(&window_with_mag(), Seconds::from_secs(0.25))
.expect("a still window");
let noise = PositionNoise::horizontal_vertical(1.0, 1.0);
assert!(filter.reset_position_to(Position::ned(3.0, 0.0, 0.0), noise));
let (attitude, declination) = (filter.state().attitude, filter.magnetic_declination());
let outcome = filter.fuse_gnss_geodetic(
filter.now(),
Geodetic::from_degrees(56.0, 44.0, 3.4e38),
PositionNoise::horizontal_vertical(1.0, 1.0),
Position::zero(),
);
assert_eq!(outcome.horizontal, Fusion::NoReference);
assert_eq!(filter.state().attitude, attitude);
assert_eq!(filter.magnetic_declination(), declination);
assert_eq!(filter.origin(), None);
}
#[test]
fn an_adoption_that_would_overflow_is_refused_and_places_nothing() {
let arm = Position::body(f32::MAX, f32::MAX, f32::MAX);
let noise = PositionNoise::horizontal_vertical(1.0, 1.0);
let mut filter = Eskf::default();
let tilted = ImuSample::from_rates(
Timestamp::from_micros(1_000_000),
AngularRate::body(0.0, 0.0, 0.0),
Acceleration::body(3.0, -2.0, -9.0),
DT,
);
assert!(filter.initialize_coarse(tilted).is_ok());
let (state, declination) = (*filter.estimate.state(), filter.magnetic_declination());
let outcome = filter.fuse_gnss_position(filter.now(), Position::zero(), noise, arm);
assert_eq!(outcome, GnssFusion::both(Fusion::NotFinite));
let fix = Geodetic::from_degrees(56.0, 44.0, 100.0);
let outcome = filter.fuse_gnss_geodetic(filter.now(), fix, noise, arm);
assert_eq!(outcome, GnssFusion::both(Fusion::NotFinite));
assert_eq!(*filter.estimate.state(), state);
assert_eq!(filter.origin(), None);
assert_eq!(filter.magnetic_declination(), declination);
assert!(filter.unestablished.position);
}
#[test]
fn a_seed_that_is_not_symmetric_is_committed_symmetric() {
let mut p = crate::state::CovarianceMatrix::from_diagonal_element(0.1);
p[(0, 1)] = 0.01;
p[(1, 0)] = 0.03;
let mut filter = Eskf::default();
assert!(
filter
.seed(State::default(), Covariance::from_matrix(p))
.is_ok()
);
let committed = filter.covariance().as_matrix();
assert_eq!(*committed, committed.transpose());
assert_eq!(committed[(0, 1)], 0.02);
}
#[test]
fn a_heading_adoption_whose_variance_overflows_is_refused() {
let mut filter = Eskf::default();
let spinning = ImuSample::from_rates(
Timestamp::from_micros(1_000_000),
AngularRate::body(0.0, 0.0, 1.0e20),
Acceleration::body(0.3, 0.0, -9.3),
DT,
);
assert!(filter.initialize_coarse(spinning).is_ok());
let (state, covariance) = (*filter.estimate.state(), *filter.covariance());
let field = MagField::body(0.22, 0.0, 0.44);
let outcome = filter.fuse_mag_heading(filter.now(), field, HeadingNoise::from_sigma(0.02));
assert_eq!(outcome, Fusion::NotFinite);
assert_eq!(
(*filter.estimate.state(), *filter.covariance()),
(state, covariance)
);
assert!(filter.unestablished.heading);
}
#[test]
fn an_origin_the_position_cannot_be_written_about_is_refused() {
let mut filter = initialized();
assert!(filter.set_origin(Geodetic::from_degrees(47.4, 8.5, 488.0)));
let held = (filter.origin(), filter.state().position);
assert!(!filter.set_origin(Geodetic::from_degrees(47.4, 8.5, -1.0e39)));
assert_eq!((filter.origin(), filter.state().position), held);
}
#[test]
fn an_ordinary_run_never_reaches_the_floor() {
let mut filter = aided();
for step in 0..400 {
assert_eq!(filter.step(still().imu, DT), Propagation::Propagated);
if step % 25 == 0 {
assert!(
filter
.fuse_gnss_position(
filter.now(),
Position::ned(0.0, 0.0, 0.0),
PositionNoise::horizontal_vertical(1.5, 1.5),
Position::zero(),
)
.is_accepted()
);
assert!(
filter
.fuse_baro_altitude(
filter.now(),
Altitude::from_meters(100.0),
AltitudeNoise::from_sigma(2.0),
)
.is_accepted()
);
}
}
assert_eq!(filter.diagnostics().floored, 0);
}
fn aligned() -> Eskf {
let window = window_at(0.0).map(|sample| StaticSample {
mag: Some(MagField::body(0.22, 0.0, 0.44)),
..sample
});
let mut filter = Eskf::default();
assert_eq!(
filter.initialize_over(&window, Seconds::from_secs(0.25)),
Ok(Alignment::Static)
);
assert!(filter.is_aligned());
filter
}
#[test]
fn a_fast_source_that_stops_degrades_the_status_on_its_own_schedule() {
let mut filter = aligned();
let baro = |filter: &mut Eskf| {
let outcome = filter.fuse_baro_altitude(
filter.now(),
Altitude::from_meters(0.0),
AltitudeNoise::from_sigma(2.0),
);
assert!(outcome.is_accepted(), "{outcome:?}");
};
let gnss = |filter: &mut Eskf| {
hold_velocity(filter, Velocity::ned(0.0, 0.0, 0.0));
};
hold(&mut filter, 10.0, 10, |filter| {
baro(filter);
if filter.diagnostics().baro_altitude.accepted % 10 == 0 {
gnss(filter);
}
});
assert_eq!(filter.state().status, Status::Healthy);
hold(&mut filter, 0.4, 100, |_| {});
assert_eq!(filter.state().status, Status::Degraded);
}
#[test]
fn a_status_reads_dead_reckoning_once_gnss_stops_whatever_else_arrives() {
let mut filter = aligned();
hold(&mut filter, 2.0, 10, |filter| {
hold_velocity(filter, Velocity::ned(0.0, 0.0, 0.0));
let _ = filter.fuse_baro_altitude(
filter.now(),
Altitude::from_meters(0.0),
AltitudeNoise::from_sigma(2.0),
);
});
assert_eq!(filter.state().status, Status::Healthy);
hold(&mut filter, 5.5, 10, |filter| {
let _ = filter.fuse_baro_altitude(
filter.now(),
Altitude::from_meters(0.0),
AltitudeNoise::from_sigma(2.0),
);
});
assert!(
filter
.diagnostics()
.baro_altitude
.is_fresh(&crate::Timeouts::default())
);
assert_eq!(filter.state().status, Status::DeadReckoning);
}
#[test]
fn a_course_refused_at_a_stop_does_not_degrade_the_status() {
let east = Velocity::ned(0.0, 15.0, 0.0);
let stopped = Velocity::ned(0.0, 0.0, 0.0);
let heading = Radians::from_radians(core::f32::consts::FRAC_PI_2);
let noise = HeadingNoise::from_sigma(0.05);
let mut with = cruising(east);
let mut without = cruising(east);
for filter in [&mut with, &mut without] {
assert!(
filter
.fuse_gnss_heading(filter.now(), heading, noise)
.is_reset()
);
}
assert!(with.fuse_course(with.now(), noise).is_accepted());
for filter in [&mut with, &mut without] {
assert!(filter.reset_velocity_to(stopped, VelocityNoise::from_speed_accuracy(0.3)));
hold(filter, 4.0, 10, |filter| {
hold_velocity(filter, stopped);
let _ = filter.fuse_gnss_heading(filter.now(), heading, noise);
let _ = filter.fuse_course(filter.now(), noise);
});
}
assert_eq!(
with.diagnostics().course.last_refusal,
Some(Refusal::Unobservable)
);
assert_eq!(without.state().status, Status::Healthy);
assert_eq!(with.state().status, without.state().status);
}
}