use nalgebra::{UnitQuaternion, Vector3};
use crate::config::LATENCY_HORIZON;
use crate::frames::Body;
use crate::math::exp_quat;
use crate::state::State;
use crate::units::{AngularRate, Attitude, Position, Timestamp, Velocity};
const CAPACITY: usize = 32;
const SPACING: u64 = (LATENCY_HORIZON.as_secs() * 1.0e6) as u64 / (CAPACITY as u64 - 2);
#[derive(Clone, Copy, Debug, PartialEq)]
struct Entry {
time: Timestamp,
position: Vector3<f32>,
velocity: Vector3<f32>,
attitude: UnitQuaternion<f32>,
}
impl Entry {
fn of(time: Timestamp, state: &State) -> Self {
Self {
time,
position: state.position.vector(),
velocity: state.velocity.vector(),
attitude: state.attitude.quaternion(),
}
}
}
impl Default for Entry {
fn default() -> Self {
Self {
time: Timestamp::ZERO,
position: Vector3::zeros(),
velocity: Vector3::zeros(),
attitude: UnitQuaternion::identity(),
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct History {
entries: [Entry; CAPACITY],
len: usize,
next: usize,
}
impl Default for History {
fn default() -> Self {
Self {
entries: [Entry::default(); CAPACITY],
len: 0,
next: 0,
}
}
}
impl History {
pub(crate) fn clear(&mut self) {
self.len = 0;
self.next = 0;
}
pub(crate) fn record(&mut self, time: Timestamp, state: &State) {
if let Some(newest) = self.newest()
&& time.as_micros().saturating_sub(newest.time.as_micros()) < SPACING
{
return;
}
if let Some(slot) = self.entries.get_mut(self.next) {
*slot = Entry::of(time, state);
}
self.next = (self.next + 1) % CAPACITY;
self.len = (self.len + 1).min(CAPACITY);
}
pub(crate) fn shift(&mut self, before: &State, after: &State) {
let dp = after.position.vector() - before.position.vector();
let dv = after.velocity.vector() - before.velocity.vector();
let dq = after.attitude.quaternion() * before.attitude.quaternion().inverse();
for entry in self.entries.iter_mut().take(self.len) {
entry.position += dp;
entry.velocity += dv;
entry.attitude = dq * entry.attitude;
}
}
pub(crate) fn at(
&self,
time: Timestamp,
now: Timestamp,
current: &State,
omega: AngularRate<Body>,
) -> (State, Timestamp) {
let lead = time.since(now).as_secs();
if lead > 0.0 {
let ahead = State {
position: Position::from_vector(
current.position.vector() + current.velocity.vector() * lead,
),
attitude: Attitude::from_quaternion(
current.attitude.quaternion() * exp_quat(omega.vector() * lead),
),
..*current
};
return (ahead, time);
}
let mut later = Entry::of(now, current);
for k in 0..self.len {
let Some(earlier) = self.newest_but(k) else {
break;
};
if earlier.time <= time {
return (with(current, interpolate(earlier, later, time)), time);
}
later = earlier;
}
(with(current, later), later.time)
}
fn newest(&self) -> Option<Entry> {
self.newest_but(0)
}
fn newest_but(&self, k: usize) -> Option<Entry> {
if k >= self.len {
return None;
}
self.entries
.get((self.next + CAPACITY - 1 - k) % CAPACITY)
.copied()
}
}
fn interpolate(earlier: Entry, later: Entry, time: Timestamp) -> Entry {
let span = later.time.since(earlier.time).as_secs();
if span <= 0.0 {
return later;
}
let s = (time.since(earlier.time).as_secs() / span).clamp(0.0, 1.0);
Entry {
time,
position: earlier.position.lerp(&later.position, s),
velocity: earlier.velocity.lerp(&later.velocity, s),
attitude: earlier
.attitude
.try_slerp(&later.attitude, s, 1.0e-6)
.unwrap_or(later.attitude),
}
}
fn with(current: &State, entry: Entry) -> State {
State {
position: Position::from_vector(entry.position),
velocity: Velocity::from_vector(entry.velocity),
attitude: Attitude::from_quaternion(entry.attitude),
..*current
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::units::Seconds;
const DT: Seconds = Seconds::from_secs(0.0025);
fn still() -> AngularRate<Body> {
AngularRate::body(0.0, 0.0, 0.0)
}
fn moving(north: f32) -> State {
State {
position: Position::ned(north, 0.0, 0.0),
velocity: Velocity::ned(20.0, 0.0, 0.0),
..State::default()
}
}
fn flown() -> (History, Timestamp, State) {
let mut history = History::default();
let mut time = Timestamp::ZERO;
let mut state = moving(0.0);
for _ in 0..160 {
history.record(time, &state);
time = time.after(DT);
state = moving(state.position.x() + 20.0 * DT.as_secs());
}
(history, time, state)
}
#[test]
fn the_past_of_a_straight_line_is_on_it() {
let (history, now, state) = flown();
for age in [0.0, 0.001, 0.0137, 0.15, 0.29] {
let (past, _) = history.at(now.before(Seconds::from_secs(age)), now, &state, still());
let expected = state.position.x() - 20.0 * age;
assert!(
(past.position.x() - expected).abs() < 1e-3,
"{age} s: {} against {expected}",
past.position.x()
);
}
}
#[test]
fn the_ring_spans_the_horizon_at_imu_rate() {
let (history, now, state) = flown();
let (oldest, placed) = history.at(Timestamp::ZERO, now, &state, still());
assert!(
placed > Timestamp::ZERO,
"placed at the oldest entry, not before it"
);
let reached = (state.position.x() - oldest.position.x()) / 20.0;
assert!(reached >= LATENCY_HORIZON.as_secs(), "{reached} s");
}
#[test]
fn a_turn_about_down_reaches_the_past_about_down() {
let mut history = History::default();
let rolled = |angle: f32| State {
attitude: Attitude::from_quaternion(UnitQuaternion::from_euler_angles(angle, 0.0, 0.0)),
..State::default()
};
history.record(Timestamp::ZERO, &rolled(0.0));
let now = Timestamp::from_micros(100_000);
let before = rolled(core::f32::consts::FRAC_PI_2);
let turned =
UnitQuaternion::from_euler_angles(0.0, 0.0, 1.0) * before.attitude.quaternion();
let after = State {
attitude: Attitude::from_quaternion(turned),
..before
};
history.shift(&before, &after);
let (past, _) = history.at(Timestamp::ZERO, now, &after, still());
let (roll, pitch, yaw) = past.attitude.euler_angles();
assert!(roll.abs() < 1e-5 && pitch.abs() < 1e-5, "{roll} {pitch}");
assert!((yaw - 1.0).abs() < 1e-5, "{yaw}");
}
#[test]
fn a_time_ahead_of_the_present_is_carried_forward() {
let (history, now, state) = flown();
let turning = AngularRate::body(0.0, 0.0, core::f32::consts::FRAC_PI_2);
let lead = Seconds::from_secs(0.1);
let (ahead, placed) = history.at(now.after(lead), now, &state, turning);
assert_eq!(placed, now.after(lead));
assert!((ahead.position.x() - (state.position.x() + 2.0)).abs() < 1e-4);
let (_, _, yaw) = ahead.attitude.euler_angles();
assert!((yaw - 0.05 * core::f32::consts::PI).abs() < 1e-5, "{yaw}");
}
#[test]
fn a_correction_reaches_the_past() {
let (mut history, now, state) = flown();
let corrected = State {
position: Position::ned(state.position.x() + 3.0, 0.5, 0.0),
..state
};
history.shift(&state, &corrected);
let (past, _) = history.at(
now.before(Seconds::from_secs(0.1)),
now,
&corrected,
still(),
);
assert!((past.position.x() - (state.position.x() - 2.0 + 3.0)).abs() < 1e-3);
assert!((past.position.y() - 0.5).abs() < 1e-6);
}
}