use core::time::Duration;
use super::{predict, UpdateReport};
use crate::angle::wrap180;
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 fn update_late(
history: &[NavigationState],
model: &impl ProcessModel,
observation: &dyn Observation,
) -> Result<(NavigationState, UpdateReport)> {
let when = observation.taken_at();
let (Some(first), Some(present)) = (history.first(), history.last()) else {
return Err(NavigationError::Kernel(KernelError::Missing {
what: "a late update with no history",
}));
};
if first.valid_at() > when || present.valid_at() <= when {
return Err(NavigationError::Kernel(KernelError::Missing {
what: "a late update outside its history",
}));
}
let at_moment = if first.valid_at() == when {
*first
} else {
predict(first, model, when.duration_since(first.valid_at())?)?
};
let mut smoothed = Smoothed {
vector: *present.vector(),
covariance: *present.covariance(),
gain_chain: Matrix::identity(),
};
let mut next = *present;
let between = history.len().saturating_sub(2);
for node in history.iter().rev().skip(1).take(between) {
smoothed.step_back(node, &next, model)?;
next = *node;
}
smoothed.step_back(&at_moment, &next, model)?;
let then =
NavigationState::from_parts(when, *present.frame(), smoothed.vector, smoothed.covariance)?;
let predicted = observation.predict(&then)?;
let jacobian = observation.jacobian(&then)?;
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 = LateStep {
present,
then: &then,
gain_chain: &smoothed.gain_chain,
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 Smoothed {
vector: Vector<STATE_DIM>,
covariance: Matrix<STATE_DIM, STATE_DIM>,
gain_chain: Matrix<STATE_DIM, STATE_DIM>,
}
impl Smoothed {
fn step_back(
&mut self,
node: &NavigationState,
next: &NavigationState,
model: &impl ProcessModel,
) -> Result<()> {
let over: Duration = next.valid_at().duration_since(node.valid_at())?;
let predicted = predict(node, model, over)?;
let transition = *model.jacobian(node, over)?.matrix();
let factor = predicted
.covariance()
.cholesky()
.ok_or(NavigationError::Kernel(KernelError::SingularSystem {
context: "predicted covariance",
}))?;
let gain = factor
.solve(&(transition * *node.covariance()))
.ok_or(NavigationError::Kernel(KernelError::SingularSystem {
context: "predicted covariance",
}))?
.transpose();
let residual = difference(&self.vector, predicted.vector());
self.vector = *node.vector() + gain * residual;
self.covariance = (*node.covariance()
+ gain * (self.covariance - *predicted.covariance()) * gain.transpose())
.symmetrised();
self.gain_chain = gain * self.gain_chain;
Ok(())
}
}
fn difference(a: &Vector<STATE_DIM>, b: &Vector<STATE_DIM>) -> Vector<STATE_DIM> {
let mut result = *a - *b;
let heading = StateComponent::Heading.index();
if let Some(turn) = result.element(heading) {
let wrapped = math::to_radians(wrap180(math::to_degrees(turn)));
result.set(heading, 0, wrapped);
}
result
}
struct LateStep<'a> {
present: &'a NavigationState,
then: &'a NavigationState,
gain_chain: &'a Matrix<STATE_DIM, STATE_DIM>,
jacobian: &'a ObservationJacobian,
noise: &'a ObservationNoise,
innovation: &'a ObservationVector,
gate: Option<f64>,
sensor: SensorId,
}
impl LateStep<'_> {
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_then = *self.then.covariance();
let p_now = *self.present.covariance();
let s = (h * p_then * 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: super::fixes_position(self.jacobian),
};
if self
.gate
.is_some_and(|threshold| normalised_innovation_squared > threshold)
{
report.accepted = false;
return Ok((*self.present, report));
}
let cross = p_now * self.gain_chain.transpose() * h.transpose();
let gain = factor
.solve(&cross.transpose())
.ok_or(NavigationError::Kernel(KernelError::SingularSystem {
context: "innovation covariance",
}))?
.transpose();
let corrected = *self.present.vector() + gain * nu;
let covariance = (p_now - gain * s * gain.transpose()).symmetrised();
let state = NavigationState::from_parts(
self.present.valid_at(),
*self.present.frame(),
corrected,
covariance,
)?;
Ok((state, report))
}
}