use crate::holdover::coast_phase_sigma;
use crate::security::min_detectable_offset_ns;
use serde::Serialize;
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
pub struct TplInputs {
pub q_wf: f64,
pub q_rw: f64,
pub q_drift: f64,
pub r: f64,
pub tau: f64,
pub samples: f64,
pub k: f64,
pub detection_latency_s: f64,
}
pub fn timing_protection_level_ns(inp: &TplInputs) -> f64 {
let floor = min_detectable_offset_ns(inp.q_wf, inp.q_rw, inp.r, inp.tau, inp.samples, inp.k);
let coast = coast_phase_sigma(inp.q_wf, inp.q_rw, inp.q_drift, inp.detection_latency_s) * 1e9;
floor + coast
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
pub struct TplBand {
pub low_ns: f64,
pub nominal_ns: f64,
pub high_ns: f64,
pub decades: f64,
}
pub fn tpl_band(inp: &TplInputs, decades: f64) -> TplBand {
let scale = 10.0_f64.powf(decades);
let lo = timing_protection_level_ns(&TplInputs {
q_rw: inp.q_rw / scale,
q_drift: inp.q_drift / scale,
..*inp
});
let hi = timing_protection_level_ns(&TplInputs {
q_rw: inp.q_rw * scale,
q_drift: inp.q_drift * scale,
..*inp
});
TplBand {
low_ns: lo,
nominal_ns: timing_protection_level_ns(inp),
high_ns: hi,
decades,
}
}
#[derive(Clone, Copy, Debug)]
pub struct Cusum {
pub kref: f64,
pub h: f64,
s: f64,
n: usize,
}
impl Cusum {
pub fn new(kref: f64, h: f64) -> Self {
Cusum {
kref,
h,
s: 0.0,
n: 0,
}
}
pub fn update(&mut self, z: f64) -> bool {
self.s = (self.s + z - self.kref).max(0.0);
self.n += 1;
self.s > self.h
}
pub fn statistic(&self) -> f64 {
self.s
}
pub fn samples(&self) -> usize {
self.n
}
}
pub fn cusum_latency_s(kref: f64, h: f64, z: f64, dt: f64) -> f64 {
let step = z - kref;
if step <= 0.0 {
return f64::INFINITY;
}
((h / step).floor() + 1.0) * dt
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clock_state::q_from_allan;
fn uso_inputs() -> TplInputs {
let (q_wf, q_rw, q_drift) = q_from_allan(1e-12, 1e-14, 1e-16);
TplInputs {
q_wf,
q_rw,
q_drift,
r: 1e-20,
tau: 600.0,
samples: 600.0,
k: 5.0,
detection_latency_s: 60.0,
}
}
#[test]
fn tpl_equals_hand_derived_sum_of_validated_terms() {
let inp = uso_inputs();
let floor =
min_detectable_offset_ns(inp.q_wf, inp.q_rw, inp.r, inp.tau, inp.samples, inp.k);
let coast =
coast_phase_sigma(inp.q_wf, inp.q_rw, inp.q_drift, inp.detection_latency_s) * 1e9;
assert!((timing_protection_level_ns(&inp) - (floor + coast)).abs() < 1e-12);
assert!(timing_protection_level_ns(&inp) >= floor);
}
#[test]
fn tpl_grows_with_detection_latency() {
let mut a = uso_inputs();
a.detection_latency_s = 10.0;
let mut b = uso_inputs();
b.detection_latency_s = 300.0;
assert!(timing_protection_level_ns(&b) > timing_protection_level_ns(&a));
}
#[test]
fn floor_band_is_material_for_a_stable_clock_at_long_latency() {
let mut inp = uso_inputs();
inp.detection_latency_s = 1000.0;
let band = tpl_band(&inp, 1.0);
assert!(band.high_ns > band.nominal_ns);
assert!(band.nominal_ns > band.low_ns);
assert!(
band.high_ns > 1.2 * band.low_ns,
"band not material: lo={} hi={}",
band.low_ns,
band.high_ns
);
}
#[test]
fn cusum_alarms_on_a_sustained_shift_at_the_hand_derived_sample() {
let mut c = Cusum::new(0.5, 5.0);
let mut alarm_at = 0;
for i in 1..=20 {
if c.update(1.5) {
alarm_at = i;
break;
}
}
assert_eq!(alarm_at, 6);
}
#[test]
fn cusum_does_not_alarm_below_reference() {
let mut c = Cusum::new(0.5, 5.0);
for _ in 0..10_000 {
assert!(!c.update(0.4));
}
assert_eq!(c.statistic(), 0.0);
}
#[test]
fn cusum_latency_closed_form_matches_the_detector() {
assert_eq!(cusum_latency_s(0.5, 5.0, 1.5, 1.0), 6.0);
assert!(cusum_latency_s(0.5, 5.0, 0.4, 1.0).is_infinite());
for &z in &[0.8_f64, 1.0, 2.0, 3.5] {
let mut c = Cusum::new(0.5, 5.0);
let mut n = 0;
for i in 1..=100 {
if c.update(z) {
n = i;
break;
}
}
assert_eq!(n as f64, cusum_latency_s(0.5, 5.0, z, 1.0));
}
}
#[test]
fn real_calibrated_tpl_is_far_below_the_observed_capture() {
let observed_pull_ns = 1_010_923.0;
let (q_wf, q_rw, q_drift) = q_from_allan(2.8e-9, 4.4e-10, 1.0e-11);
let inp = TplInputs {
q_wf,
q_rw,
q_drift,
r: (22.1e-9_f64).powi(2),
tau: 1.0,
samples: 1.0,
k: 5.0,
detection_latency_s: 60.0,
};
let tpl = timing_protection_level_ns(&inp);
assert!(
tpl < observed_pull_ns / 10.0,
"real-calibrated TPL {tpl} ns not far below observed pull {observed_pull_ns} ns"
);
assert!(tpl > 5.0 * 22.1);
}
}