mod health;
mod history;
mod motion;
pub mod pure;
#[cfg(test)]
mod tests;
use core::time::Duration;
use crate::error::{ensure_range, Result};
use crate::estimation::{Observation, ProcessModel};
use crate::event::{
EventList, NavigationEvent, NavigationIntegrity, RejectionReason, SensorHealth, SensorId,
};
use crate::geodesy::{Ellipsoid, GeodeticPoint, Height};
use crate::local::LocalFrame;
use crate::math;
use crate::snapshot::NavigationSnapshot;
use crate::state::{NavigationState, StateComponent};
use crate::time::{Instant, Utc};
use crate::units::{Distance, METRES_PER_NAUTICAL_MILE};
use health::{IntegrityLimits, Sensors};
use history::History;
pub use health::MAX_SENSORS;
pub use history::MAX_HISTORY;
pub use motion::SteadyMotion;
pub use pure::UpdateReport;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum LatePolicy {
Reject,
Smooth {
max_lag: Duration,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(try_from = "StoredEstimatorConfig", into = "StoredEstimatorConfig")
)]
pub struct EstimatorConfig {
reanchor_after: Distance,
max_age: Duration,
late: LatePolicy,
alert_limit: Distance,
max_unaided: Duration,
suspect_after: u8,
}
impl EstimatorConfig {
#[must_use]
pub const fn standard() -> Self {
Self {
reanchor_after: Distance::from_nautical_miles_unchecked(
50_000.0 / METRES_PER_NAUTICAL_MILE,
),
max_age: Duration::from_secs(10),
late: LatePolicy::Smooth {
max_lag: Duration::from_secs(2),
},
alert_limit: Distance::from_nautical_miles_unchecked(100.0 / METRES_PER_NAUTICAL_MILE),
max_unaided: Duration::from_secs(30),
suspect_after: 5,
}
}
pub fn with_reanchor_after(mut self, distance: Distance) -> Result<Self> {
ensure_range(
"reanchor distance",
distance.metres(),
f64::MIN_POSITIVE,
f64::MAX,
)?;
self.reanchor_after = distance;
Ok(self)
}
#[must_use]
pub const fn with_max_age(mut self, max_age: Duration) -> Self {
self.max_age = max_age;
self
}
#[must_use]
pub const fn with_late(mut self, late: LatePolicy) -> Self {
self.late = late;
self
}
pub fn with_alert_limit(mut self, limit: Distance) -> Result<Self> {
ensure_range("alert limit", limit.metres(), f64::MIN_POSITIVE, f64::MAX)?;
self.alert_limit = limit;
Ok(self)
}
#[must_use]
pub const fn with_max_unaided(mut self, max_unaided: Duration) -> Self {
self.max_unaided = max_unaided;
self
}
#[must_use]
pub const fn with_suspect_after(mut self, rejections: u8) -> Self {
self.suspect_after = rejections;
self
}
#[must_use]
pub const fn reanchor_after(&self) -> Distance {
self.reanchor_after
}
#[must_use]
pub const fn max_age(&self) -> Duration {
self.max_age
}
#[must_use]
pub const fn late(&self) -> LatePolicy {
self.late
}
#[must_use]
pub const fn alert_limit(&self) -> Distance {
self.alert_limit
}
#[must_use]
pub const fn max_unaided(&self) -> Duration {
self.max_unaided
}
#[must_use]
pub const fn suspect_after(&self) -> u8 {
self.suspect_after
}
const fn integrity_limits(&self) -> IntegrityLimits {
IntegrityLimits {
alert_limit: self.alert_limit,
max_unaided: self.max_unaided,
}
}
}
#[cfg(feature = "serde")]
#[derive(serde::Serialize, serde::Deserialize)]
struct StoredEstimatorConfig {
reanchor_after: Distance,
max_age: Duration,
late: LatePolicy,
alert_limit: Distance,
max_unaided: Duration,
suspect_after: u8,
}
#[cfg(feature = "serde")]
impl TryFrom<StoredEstimatorConfig> for EstimatorConfig {
type Error = crate::error::NavigationError;
fn try_from(stored: StoredEstimatorConfig) -> Result<Self> {
Self::standard()
.with_reanchor_after(stored.reanchor_after)?
.with_alert_limit(stored.alert_limit)
.map(|config| {
config
.with_max_age(stored.max_age)
.with_late(stored.late)
.with_max_unaided(stored.max_unaided)
.with_suspect_after(stored.suspect_after)
})
}
}
#[cfg(feature = "serde")]
impl From<EstimatorConfig> for StoredEstimatorConfig {
fn from(config: EstimatorConfig) -> Self {
Self {
reanchor_after: config.reanchor_after,
max_age: config.max_age,
late: config.late,
alert_limit: config.alert_limit,
max_unaided: config.max_unaided,
suspect_after: config.suspect_after,
}
}
}
#[must_use = "the events say what happened; an unread outcome is a lost event"]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Outcome {
events: EventList,
report: Option<UpdateReport>,
}
impl Outcome {
pub const fn events(&self) -> &EventList {
&self.events
}
#[must_use]
pub const fn report(&self) -> Option<&UpdateReport> {
self.report.as_ref()
}
}
#[derive(Debug, Clone)]
pub struct Estimator<P: ProcessModel> {
history: History,
process: P,
config: EstimatorConfig,
sensors: Sensors,
last_fix: Option<Instant<Utc>>,
integrity: NavigationIntegrity,
}
impl<P: ProcessModel> Estimator<P> {
pub fn new(initial: NavigationState, process: P, config: EstimatorConfig) -> Self {
let integrity = health::assess(
&initial,
None,
initial.valid_at(),
&config.integrity_limits(),
);
Self {
history: History::starting_with(&initial),
process,
config,
sensors: Sensors::new(),
last_fix: None,
integrity,
}
}
#[must_use]
pub fn state(&self) -> &NavigationState {
self.history.current()
}
#[must_use]
pub const fn config(&self) -> &EstimatorConfig {
&self.config
}
#[must_use]
pub const fn process(&self) -> &P {
&self.process
}
#[must_use]
pub const fn integrity(&self) -> NavigationIntegrity {
self.integrity
}
#[must_use]
pub fn sensor_health(&self, sensor: SensorId) -> Option<SensorHealth> {
self.sensors.health(sensor)
}
pub fn predicted_at(&self, when: Instant<Utc>) -> Result<NavigationState> {
let state = self.state();
let over = when.duration_since(state.valid_at())?;
if over.is_zero() {
return Ok(*state);
}
pure::predict(state, &self.process, over)
}
pub fn advance_to(&mut self, when: Instant<Utc>) -> Result<Outcome> {
let mut events = EventList::new();
self.history.record(&self.predicted_at(when)?);
self.reanchor_if_far()?;
self.judge(&mut events);
Ok(Outcome {
events,
report: None,
})
}
pub fn ingest(&mut self, observation: &dyn Observation) -> Result<Outcome> {
let mut events = EventList::new();
let when = observation.taken_at();
let stepped = if when < self.state().valid_at() {
self.fold_in_late(observation).transpose()?
} else {
self.history.record(&self.predicted_at(when)?);
Some(pure::update(self.state(), observation)?)
};
let Some((updated, report)) = stepped else {
events.push(NavigationEvent::ObservationRejected {
reason: RejectionReason::OutOfOrder,
at: when,
});
return Ok(Outcome {
events,
report: None,
});
};
if report.accepted() {
self.history.record(&updated);
if report.fixes_position() {
self.last_fix = Some(when.max(self.last_fix.unwrap_or(when)));
}
} else {
events.push(NavigationEvent::ObservationRejected {
reason: RejectionReason::Improbable {
normalised_innovation_squared: report.normalised_innovation_squared(),
},
at: when,
});
}
self.sensors.note(
observation.sensor(),
report.accepted(),
self.config.suspect_after,
when,
&mut events,
)?;
self.reanchor_if_far()?;
self.judge(&mut events);
Ok(Outcome {
events,
report: Some(report),
})
}
#[must_use]
pub fn snapshot_at(&self, now: Instant<Utc>) -> NavigationSnapshot {
let state = self.state();
let age = now.checked_duration_since(state.valid_at());
let integrity = health::assess(state, self.last_fix, now, &self.config.integrity_limits());
state
.project()
.with_integrity(integrity)
.with_age(age, age.is_some_and(|age| age > self.config.max_age))
}
fn fold_in_late(
&self,
observation: &dyn Observation,
) -> Option<Result<(NavigationState, UpdateReport)>> {
let LatePolicy::Smooth { max_lag } = self.config.late else {
return None;
};
let lag = self
.state()
.valid_at()
.checked_duration_since(observation.taken_at())?;
if lag > max_lag {
return None;
}
let history = self.history.reaching(observation.taken_at())?;
Some(pure::update_late(history, &self.process, observation))
}
fn judge(&mut self, events: &mut EventList) {
let state = self.state();
let now = health::assess(
state,
self.last_fix,
state.valid_at(),
&self.config.integrity_limits(),
);
if now != self.integrity {
events.push(NavigationEvent::IntegrityChanged {
from: self.integrity,
to: now,
at: state.valid_at(),
});
self.integrity = now;
}
}
fn reanchor_if_far(&mut self) -> Result<()> {
let state = self.state();
let vector = state.vector();
let north = vector.element(StateComponent::North.index()).unwrap_or(0.0);
let east = vector.element(StateComponent::East.index()).unwrap_or(0.0);
if math::hypot(north, east) <= self.config.reanchor_after.metres() {
return Ok(());
}
let anchor = GeodeticPoint::new(state.position(), Height::above_ellipsoid(Distance::ZERO));
let frame = LocalFrame::at(anchor, &Ellipsoid::WGS84)?;
let mut moved = *vector;
moved.set(StateComponent::North.index(), 0, 0.0);
moved.set(StateComponent::East.index(), 0, 0.0);
let moved =
NavigationState::from_parts(state.valid_at(), frame, moved, *state.covariance())?;
self.history.restart_with(&moved);
Ok(())
}
}