mod late;
use core::time::Duration;
use crate::error::{KernelError, NavigationError, Result};
use crate::estimation::{
Observation, ObservationJacobian, ObservationNoise, ObservationVector, ProcessModel,
};
use crate::event::SensorId;
use crate::math;
use crate::matrix::{Matrix, Vector};
use crate::state::{NavigationState, StateComponent, STATE_DIM};
pub use late::update_late;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct UpdateReport {
sensor: SensorId,
accepted: bool,
normalised_innovation_squared: f64,
degrees_of_freedom: usize,
fixes_position: bool,
}
impl UpdateReport {
#[must_use]
pub const fn sensor(&self) -> SensorId {
self.sensor
}
#[must_use]
pub const fn accepted(&self) -> bool {
self.accepted
}
#[must_use]
pub const fn normalised_innovation_squared(&self) -> f64 {
self.normalised_innovation_squared
}
#[must_use]
pub const fn degrees_of_freedom(&self) -> usize {
self.degrees_of_freedom
}
#[must_use]
pub const fn fixes_position(&self) -> bool {
self.fixes_position
}
}
fn fixes_position(jacobian: &ObservationJacobian) -> bool {
jacobian.rows().iter().any(|row| {
math::abs(row.derivative(StateComponent::North)) > 0.0
|| math::abs(row.derivative(StateComponent::East)) > 0.0
})
}
pub fn predict(
state: &NavigationState,
model: &impl ProcessModel,
over: Duration,
) -> Result<NavigationState> {
let moved = model.propagate(state, over)?;
let transition = *model.jacobian(state, over)?.matrix();
let noise = *model.noise(state, over).matrix();
let covariance =
(transition * *state.covariance() * transition.transpose() + noise).symmetrised();
Ok(NavigationState::from_parts(
moved.valid_at(),
*moved.frame(),
*moved.vector(),
covariance,
)?)
}
pub fn update(
state: &NavigationState,
observation: &dyn Observation,
) -> Result<(NavigationState, UpdateReport)> {
let predicted = observation.predict(state)?;
let jacobian = observation.jacobian(state)?;
let noise = observation.noise();
let innovation = observation.innovation(&predicted)?;
let dimension = innovation.len();
if jacobian.len() != dimension || noise.len() != dimension || dimension == 0 {
return Err(NavigationError::Kernel(KernelError::BufferTooSmall {
needed: dimension,
found: jacobian.len().min(noise.len()),
}));
}
let step = Step {
state,
jacobian: &jacobian,
noise: &noise,
innovation: &innovation,
gate: observation.gate().threshold(),
sensor: observation.sensor(),
};
match dimension {
1 => step.run::<1>(),
2 => step.run::<2>(),
3 => step.run::<3>(),
4 => step.run::<4>(),
_ => Err(NavigationError::Kernel(KernelError::CapacityExceeded {
context: "an observation",
needed: dimension,
capacity: 4,
})),
}
}
struct Step<'a> {
state: &'a NavigationState,
jacobian: &'a ObservationJacobian,
noise: &'a ObservationNoise,
innovation: &'a ObservationVector,
gate: Option<f64>,
sensor: SensorId,
}
impl Step<'_> {
fn run<const M: usize>(&self) -> Result<(NavigationState, UpdateReport)> {
let h = Matrix::<M, STATE_DIM>::from_fn(|row, column| {
self.jacobian
.rows()
.get(row)
.and_then(|r| r.entries().get(column))
.copied()
.unwrap_or(0.0)
});
let r = Matrix::<M, M>::from_fn(|row, column| self.noise.get(row, column).unwrap_or(0.0));
let nu = Vector::<M>::from_fn(|row, _| self.innovation.get(row).unwrap_or(0.0));
let p = *self.state.covariance();
let s = (h * p * h.transpose() + r).symmetrised();
let factor = s
.cholesky()
.ok_or(NavigationError::Kernel(KernelError::SingularSystem {
context: "innovation covariance",
}))?;
let weighted =
factor
.solve(&nu)
.ok_or(NavigationError::Kernel(KernelError::SingularSystem {
context: "innovation covariance",
}))?;
let normalised_innovation_squared = nu.dot(&weighted);
let mut report = UpdateReport {
sensor: self.sensor,
accepted: true,
normalised_innovation_squared,
degrees_of_freedom: M,
fixes_position: fixes_position(self.jacobian),
};
if self
.gate
.is_some_and(|threshold| normalised_innovation_squared > threshold)
{
report.accepted = false;
return Ok((*self.state, report));
}
let gain = factor
.solve(&(h * p))
.ok_or(NavigationError::Kernel(KernelError::SingularSystem {
context: "innovation covariance",
}))?
.transpose();
let corrected = *self.state.vector() + gain * nu;
let shrink = Matrix::<STATE_DIM, STATE_DIM>::identity() - gain * h;
let covariance =
(shrink * p * shrink.transpose() + gain * r * gain.transpose()).symmetrised();
let state = NavigationState::from_parts(
self.state.valid_at(),
*self.state.frame(),
corrected,
covariance,
)?;
Ok((state, report))
}
}