use std::collections::VecDeque;
#[derive(Debug, Clone, Copy)]
struct Sample {
local: f64,
offset: f64,
}
#[derive(Debug, Clone)]
pub struct ClockSyncEstimator {
window: VecDeque<Sample>,
capacity: usize,
}
impl ClockSyncEstimator {
pub fn new(window: usize) -> Self {
if window < 2 {
panic!("ClockSyncEstimator window must be at least 2");
}
Self {
window: VecDeque::with_capacity(window),
capacity: window,
}
}
pub fn observe(&mut self, local: f64, offset: f64) {
if self.window.len() == self.capacity {
self.window.pop_front();
}
self.window.push_back(Sample { local, offset });
}
pub fn observe_exchange(&mut self, local_send: f64, remote_recv: f64, local_recv: f64) {
let offset = remote_recv - (local_send + local_recv) / 2.0;
self.observe(local_recv, offset);
}
pub fn is_ready(&self) -> bool {
self.window.len() >= 2
}
pub fn sample_count(&self) -> usize {
self.window.len()
}
fn fit(&self) -> Option<(f64, f64, f64)> {
let n = self.window.len();
if n < 2 {
return None;
}
let n_f = n as f64;
let mean_x = self.window.iter().map(|s| s.local).sum::<f64>() / n_f;
let mean_y = self.window.iter().map(|s| s.offset).sum::<f64>() / n_f;
let mut sxy = 0.0;
let mut sxx = 0.0;
for s in &self.window {
let dx = s.local - mean_x;
sxy += dx * (s.offset - mean_y);
sxx += dx * dx;
}
let skew = if sxx > 1e-9 { sxy / sxx } else { 0.0 };
Some((mean_x, mean_y, skew))
}
pub fn offset_at(&self, local: f64) -> Option<f64> {
match self.fit() {
Some((mean_x, mean_y, skew)) => Some(mean_y + skew * (local - mean_x)),
None => self.window.back().map(|s| s.offset),
}
}
pub fn server_time_at(&self, local: f64) -> Option<f64> {
self.offset_at(local).map(|off| local + off)
}
pub fn skew(&self) -> f64 {
self.fit().map(|(_, _, skew)| skew).unwrap_or(0.0)
}
pub fn clear(&mut self) {
self.window.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn a_window_below_two_panics() {
let _ = ClockSyncEstimator::new(1);
}
#[test]
fn with_no_skew_it_recovers_the_constant_offset() {
let mut est = ClockSyncEstimator::new(16);
for t in (0..2000).step_by(100) {
est.observe(t as f64, 500.0);
}
assert!(est.is_ready());
assert!((est.offset_at(5000.0).unwrap() - 500.0).abs() < 1e-6, "flat offset recovered");
assert!(est.skew().abs() < 1e-9, "no skew, got {}", est.skew());
assert!((est.server_time_at(1000.0).unwrap() - 1500.0).abs() < 1e-6);
}
#[test]
fn it_recovers_a_clock_drift_rate() {
let mut est = ClockSyncEstimator::new(32);
let skew = 0.001;
for t in (0..4000).step_by(100) {
let offset = 200.0 + skew * t as f64;
est.observe(t as f64, offset);
}
assert!((est.skew() - skew).abs() < 1e-6, "skew recovered, got {}", est.skew());
let expected = 200.0 + skew * 5000.0;
assert!((est.offset_at(5000.0).unwrap() - expected).abs() < 1e-3, "offset extrapolated along the drift");
}
#[test]
fn the_fit_averages_out_measurement_noise() {
let mut est = ClockSyncEstimator::new(64);
for (i, t) in (0..6400).step_by(100).enumerate() {
let noise = if i % 2 == 0 { 20.0 } else { -20.0 };
est.observe(t as f64, 300.0 + noise);
}
assert!((est.offset_at(3200.0).unwrap() - 300.0).abs() < 5.0, "noise averaged out, got {}", est.offset_at(3200.0).unwrap());
assert!(est.skew().abs() < 1e-3, "no meaningful skew from symmetric noise, got {}", est.skew());
}
#[test]
fn a_single_sample_reports_its_offset_and_no_skew() {
let mut est = ClockSyncEstimator::new(8);
est.observe(100.0, 42.0);
assert!(!est.is_ready());
assert_eq!(est.offset_at(999.0), Some(42.0), "one sample: flat at its offset");
assert_eq!(est.skew(), 0.0);
}
#[test]
fn the_window_slides_and_forgets_old_samples() {
let mut est = ClockSyncEstimator::new(4);
for t in 0..10 {
est.observe(t as f64, 0.0);
}
assert_eq!(est.sample_count(), 4, "window capped at capacity");
}
#[test]
fn observe_exchange_derives_a_symmetric_offset() {
let mut est = ClockSyncEstimator::new(8);
est.observe_exchange(1000.0, 1650.0, 1100.0);
assert_eq!(est.offset_at(1100.0), Some(600.0));
}
}