use std::collections::VecDeque;
pub const ENVELOPE_SLOT_NS: u64 = 50_000_000;
pub const ENVELOPE_BIN_NS: u64 = 2_000_000_000;
pub const ENVELOPE_SIGMA_BINS: u32 = 3;
pub const ENVELOPE_LOCK_SPAN_NS: u64 = 40_000_000_000;
pub const ENVELOPE_SLOPE_SPAN_NS: u64 = 2_000_000_000;
#[derive(Debug, Clone)]
pub struct EnvelopeFit {
window_ns: u64,
pts: VecDeque<(u64, i128)>,
hull: Vec<(u64, i128)>,
model: Option<Line>,
}
#[derive(Debug, Clone, Copy)]
struct Line {
x0: u64,
y0: i128,
s: f64,
sigma_ns: i64,
}
impl Line {
fn rel(&self, host_ns: u64) -> f64 {
self.s * (host_ns as i128 - self.x0 as i128) as f64
}
}
impl EnvelopeFit {
pub fn new(window_ns: u64) -> Self {
EnvelopeFit {
window_ns,
pts: VecDeque::new(),
hull: Vec::new(),
model: None,
}
}
pub fn sixty_seconds() -> Self {
Self::new(60_000_000_000)
}
pub fn push(&mut self, host_ns: u64, source_ns: i64) {
let y = source_ns as i128 - host_ns as i128;
let mut at = self.pts.len();
while at > 0 && self.pts[at - 1].0 > host_ns {
at -= 1;
}
let slot = host_ns / ENVELOPE_SLOT_NS;
let same = [at.checked_sub(1), Some(at)]
.into_iter()
.flatten()
.find(|&i| {
self.pts
.get(i)
.is_some_and(|p| p.0 / ENVELOPE_SLOT_NS == slot)
});
if let Some(i) = same {
if self.pts[i].1 >= y {
return;
}
self.pts.remove(i);
if i < at {
at -= 1;
}
}
self.pts.insert(at, (host_ns, y));
let newest = self.pts.back().map_or(host_ns, |p| p.0);
while let Some(&(h, _)) = self.pts.front() {
if newest.saturating_sub(h) > self.window_ns {
self.pts.pop_front();
} else {
break;
}
}
self.refit();
}
pub fn reset(&mut self) {
self.pts.clear();
self.model = None;
}
pub fn len(&self) -> usize {
self.pts.len()
}
pub fn is_empty(&self) -> bool {
self.pts.is_empty()
}
pub fn span_ns(&self) -> u64 {
match (self.pts.front(), self.pts.back()) {
(Some(a), Some(b)) => b.0 - a.0,
_ => 0,
}
}
fn refit(&mut self) {
let (Some(&(oldest, _)), Some(&(newest, _))) = (self.pts.front(), self.pts.back()) else {
self.model = None;
return;
};
if newest - oldest < ENVELOPE_SLOPE_SPAN_NS {
let (x0, y0) = self
.pts
.iter()
.copied()
.max_by_key(|p| p.1)
.unwrap_or((oldest, 0));
let mut line = Line {
x0,
y0,
s: 0.0,
sigma_ns: -1,
};
line.sigma_ns = self.sigma(&line);
self.model = Some(line);
return;
}
self.hull.clear();
for &(x, y) in &self.pts {
if let Some(&(lx, ly)) = self.hull.last() {
if lx == x {
if y <= ly {
continue;
}
self.hull.pop();
}
}
while let [.., (ax, ay), (bx, by)] = self.hull[..] {
let cross = (bx - ax) as i128 * (y - ay) - (by - ay) * (x - ax) as i128;
if cross >= 0 {
self.hull.pop();
} else {
break;
}
}
self.hull.push((x, y));
}
let Some(&first) = self.hull.first() else {
self.model = None;
return;
};
let n = self.pts.len() as u128;
let sum: u128 = self.pts.iter().map(|&(x, _)| (x - first.0) as u128).sum();
let mean_x = first.0 + (sum / n) as u64;
let (a, b) = self
.hull
.windows(2)
.map(|w| (w[0], w[1]))
.find(|&(_, b)| b.0 >= mean_x)
.unwrap_or((first, first));
let s = if b.0 > a.0 {
(b.1 - a.1) as f64 / (b.0 - a.0) as f64
} else {
0.0
};
let mut line = Line {
x0: a.0,
y0: a.1,
s,
sigma_ns: -1,
};
line.sigma_ns = self.sigma(&line);
self.model = Some(line);
}
fn sigma(&self, line: &Line) -> i64 {
let (Some(&(oldest, _)), Some(&(newest, _))) = (self.pts.front(), self.pts.back()) else {
return -1;
};
let (first_bin, open_bin) = (oldest / ENVELOPE_BIN_NS, newest / ENVELOPE_BIN_NS);
let mut bin = u64::MAX;
let mut min = f64::INFINITY;
let (mut sum_sq, mut bins) = (0.0f64, 0u32);
for &(x, y) in &self.pts {
let b = x / ENVELOPE_BIN_NS;
if b == first_bin {
continue;
}
if b == open_bin {
break;
}
if b != bin {
if min.is_finite() {
sum_sq += min * min;
bins += 1;
}
bin = b;
min = f64::INFINITY;
}
let late = ((line.y0 - y) as f64 + line.rel(x)).max(0.0);
min = min.min(late);
}
if min.is_finite() {
sum_sq += min * min;
bins += 1;
}
if bins < ENVELOPE_SIGMA_BINS {
return -1;
}
(sum_sq / f64::from(bins)).sqrt().round() as i64
}
pub fn offset_i64(&self, host_ns: u64) -> Option<i64> {
let m = self.model?;
Some((m.y0 + m.rel(host_ns).round() as i128) as i64)
}
pub fn to_source(&self, host_ns: u64) -> Option<i64> {
self.offset_i64(host_ns)
.map(|o| (host_ns as i128 + o as i128) as i64)
}
pub fn skew_ppm(&self) -> f64 {
self.model.map_or(0.0, |m| m.s * 1e6)
}
pub fn sigma_ns(&self) -> i64 {
self.model.map_or(-1, |m| m.sigma_ns)
}
}
#[cfg(test)]
mod tests {
use super::*;
const MS: u64 = 1_000_000;
#[test]
fn a_noiseless_line_is_recovered_exactly() {
let mut f = EnvelopeFit::sixty_seconds();
for i in 0..100u64 {
let h = 1_000 * MS + i * 100 * MS;
f.push(h, (h as f64 * (1.0 + 40e-6)) as i64 - 37 * MS as i64);
}
let h = 1_000 * MS + 99 * 100 * MS;
let want = (h as f64 * (1.0 + 40e-6)) as i64 - 37 * MS as i64;
assert!(f.to_source(h).unwrap().abs_diff(want) <= 2);
assert!((f.skew_ppm() - 40.0).abs() < 0.01);
assert!(f.sigma_ns() <= 2, "sigma {}", f.sigma_ns());
}
#[test]
fn late_pairs_do_not_move_the_line() {
let mut f = EnvelopeFit::sixty_seconds();
for i in 0..50u64 {
let h = i * 100 * MS;
f.push(h, h as i64);
}
let before = f.offset_i64(49 * 100 * MS);
f.push(4_950 * MS, (4_750 * MS) as i64);
f.push(4_920 * MS, (4_720 * MS) as i64);
assert_eq!(f.offset_i64(49 * 100 * MS), before);
assert_eq!(f.len(), 51);
}
#[test]
fn each_slot_keeps_its_least_late_pair() {
let mut f = EnvelopeFit::sixty_seconds();
for k in 0..70 * 30u64 {
for cam in 0..4u64 {
let h = k * 33_333_333 + cam * 7 * MS;
f.push(h, h as i64 - (cam * 3 * MS) as i64);
}
}
assert!(f.len() <= (60_000 / 50) as usize + 1, "{} pairs", f.len());
assert!(f.span_ns() >= 59_900 * MS, "span {}", f.span_ns());
assert_eq!(f.offset_i64(69 * 1_000 * MS), Some(0));
let h = 70_000 * MS + 10 * MS;
f.push(h, h as i64 - 20 * MS as i64);
f.push(h + 5 * MS, (h + 5 * MS) as i64);
assert_eq!(f.pts.back(), Some(&(h + 5 * MS, 0)));
}
#[test]
fn a_young_fit_is_flat_through_the_least_late_pair() {
let mut f = EnvelopeFit::sixty_seconds();
f.push(1_000 * MS, (800 * MS) as i64);
f.push(1_050 * MS, (1_050 * MS) as i64);
assert_eq!(f.skew_ppm(), 0.0);
assert_eq!(f.offset_i64(3_000 * MS), Some(0));
}
#[test]
fn sigma_waits_for_three_whole_bins() {
let mut f = EnvelopeFit::sixty_seconds();
let mut h = 0;
while h < 4 * ENVELOPE_BIN_NS {
f.push(h, h as i64);
h += 100 * MS;
}
assert_eq!(
f.sigma_ns(),
-1,
"the oldest bin is left out, the fourth is open"
);
f.push(h, h as i64);
assert_eq!(f.sigma_ns(), 0);
}
}