#[derive(Clone, Copy, Debug)]
pub struct TrajectoryPredictor {
times: [u64; 3],
values: [f32; 3],
count: usize,
damping: f32,
max_horizon_ms: u64,
}
impl TrajectoryPredictor {
pub fn new(damping: f32, max_horizon_ms: u64) -> Self {
Self {
times: [0; 3],
values: [0.0; 3],
count: 0,
damping: damping.clamp(0.0, 1.0),
max_horizon_ms,
}
}
pub fn observe(&mut self, time_ms: u64, value: f32) {
if self.count > 0 && time_ms <= self.times[self.count - 1] {
return;
}
if self.count == 3 {
self.times.rotate_left(1);
self.values.rotate_left(1);
self.times[2] = time_ms;
self.values[2] = value;
} else {
self.times[self.count] = time_ms;
self.values[self.count] = value;
self.count += 1;
}
}
pub fn predict(&self, time_ms: u64) -> Option<f32> {
if self.count == 0 {
return None;
}
let newest = self.times[self.count - 1];
let base = self.values[self.count - 1];
let target = time_ms.min(newest.saturating_add(self.max_horizon_ms));
let dt = if target >= newest {
(target - newest) as f32 / 1000.0
} else {
-((newest - target) as f32 / 1000.0)
};
let v = self.velocity().unwrap_or(0.0);
let a = self.acceleration().unwrap_or(0.0) * self.damping;
Some(base + v * dt + 0.5 * a * dt * dt)
}
pub fn velocity(&self) -> Option<f32> {
if self.count < 2 {
return None;
}
let (i, j) = (self.count - 2, self.count - 1);
let dt = (self.times[j] - self.times[i]) as f32 / 1000.0;
(dt > 0.0).then(|| (self.values[j] - self.values[i]) / dt)
}
pub fn acceleration(&self) -> Option<f32> {
if self.count < 3 {
return None;
}
let dt_old = (self.times[1] - self.times[0]) as f32 / 1000.0;
let dt_new = (self.times[2] - self.times[1]) as f32 / 1000.0;
if dt_old <= 0.0 || dt_new <= 0.0 {
return None;
}
let v_old = (self.values[1] - self.values[0]) / dt_old;
let v_new = (self.values[2] - self.values[1]) / dt_new;
let span = (dt_old + dt_new) * 0.5;
Some((v_new - v_old) / span)
}
pub fn newest_time(&self) -> Option<u64> {
(self.count > 0).then(|| self.times[self.count - 1])
}
pub fn samples(&self) -> usize {
self.count
}
pub fn reset(&mut self) {
self.count = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32, tol: f32) -> bool {
(a - b).abs() <= tol
}
#[test]
fn it_answers_from_the_first_sample_and_sharpens_as_they_arrive() {
let mut p = TrajectoryPredictor::new(1.0, 1000);
assert_eq!(p.predict(100), None);
p.observe(0, 5.0);
assert_eq!(p.predict(500), Some(5.0), "one sample holds");
p.observe(100, 6.0);
assert!(approx(p.predict(200).unwrap(), 7.0, 0.001), "two samples is a straight line");
p.observe(200, 8.0);
assert!(p.predict(300).unwrap() > 10.0, "three samples curve: {:?}", p.predict(300));
}
#[test]
fn a_straight_line_stays_straight() {
let mut p = TrajectoryPredictor::new(1.0, 5000);
for (t, v) in [(0u64, 0.0f32), (50, 5.0), (100, 10.0)] {
p.observe(t, v);
}
assert!(approx(p.acceleration().unwrap(), 0.0, 0.001));
assert!(approx(p.predict(1000).unwrap(), 100.0, 0.01), "got {:?}", p.predict(1000));
}
#[test]
fn damping_sits_between_first_and_second_order() {
let samples = [(0u64, 0.0f32), (100, 1.0), (200, 4.0)];
let mut none = TrajectoryPredictor::new(0.0, 5000);
let mut half = TrajectoryPredictor::new(0.5, 5000);
let mut full = TrajectoryPredictor::new(1.0, 5000);
for (t, v) in samples {
none.observe(t, v);
half.observe(t, v);
full.observe(t, v);
}
let (n, h, f) = (none.predict(400).unwrap(), half.predict(400).unwrap(), full.predict(400).unwrap());
assert!(n < h && h < f, "damping orders the predictions: {n} {h} {f}");
assert!(approx(n, 4.0 + 30.0 * 0.2, 0.01), "zero damping is plain constant velocity: {n}");
}
#[test]
fn the_horizon_holds_instead_of_running_away() {
let mut p = TrajectoryPredictor::new(1.0, 200);
for (t, v) in [(0u64, 0.0f32), (100, 1.0), (200, 4.0)] {
p.observe(t, v);
}
let at_horizon = p.predict(400).unwrap();
assert!(approx(p.predict(10_000).unwrap(), at_horizon, 0.001), "held at the horizon rather than projected to it");
assert!(at_horizon.is_finite() && at_horizon < 20.0);
}
#[test]
fn a_reordered_straggler_is_ignored() {
let mut p = TrajectoryPredictor::new(1.0, 1000);
for (t, v) in [(0u64, 0.0f32), (100, 10.0), (200, 20.0)] {
p.observe(t, v);
}
let before = p.predict(300).unwrap();
p.observe(150, 999.0);
assert_eq!(p.samples(), 3);
assert!(approx(p.predict(300).unwrap(), before, 0.001), "the straggler changed nothing");
}
#[test]
fn a_turn_is_tracked_far_better_than_a_tangent() {
let sample = |t_ms: u64| {
let t = t_ms as f32 / 1000.0;
(t * 2.0).sin() * 100.0
};
let mut first = TrajectoryPredictor::new(0.0, 1000);
let mut second = TrajectoryPredictor::new(1.0, 1000);
for t in [400u64, 500, 600] {
first.observe(t, sample(t));
second.observe(t, sample(t));
}
let truth = sample(700);
let e_first = (first.predict(700).unwrap() - truth).abs();
let e_second = (second.predict(700).unwrap() - truth).abs();
assert!(e_second < e_first * 0.6, "second order should cut it substantially: {e_second:.2} against {e_first:.2}");
}
#[test]
fn it_interpolates_between_its_own_samples() {
let mut p = TrajectoryPredictor::new(1.0, 1000);
for (t, v) in [(0u64, 0.0f32), (100, 10.0), (200, 20.0)] {
p.observe(t, v);
}
assert!(approx(p.predict(150).unwrap(), 15.0, 0.01), "got {:?}", p.predict(150));
}
}