use crate::clock_state::{q_from_allan, ClockClass};
use crate::types::Seconds;
pub const C_LIGHT_M_PER_S: f64 = 299_792_458.0;
pub fn coast_phase_variance(q_wf: f64, q_rw: f64, q_drift: f64, t: Seconds) -> f64 {
let t = t.max(0.0);
let t2 = t * t;
let t3 = t2 * t;
let t5 = t3 * t2;
q_wf * t + q_rw * t3 / 3.0 + q_drift * t5 / 20.0
}
pub fn coast_phase_sigma(q_wf: f64, q_rw: f64, q_drift: f64, t: Seconds) -> Seconds {
coast_phase_variance(q_wf, q_rw, q_drift, t).sqrt()
}
pub fn holdover_seconds(q_wf: f64, q_rw: f64, q_drift: f64, threshold_s: Seconds) -> Seconds {
if !q_wf.is_finite() || !q_rw.is_finite() || !q_drift.is_finite() || !threshold_s.is_finite() {
return f64::NAN;
}
if q_wf < 0.0 || q_rw < 0.0 || q_drift < 0.0 {
return f64::NAN;
}
if threshold_s <= 0.0 {
return 0.0;
}
let target = threshold_s * threshold_s; let var = |t: f64| coast_phase_variance(q_wf, q_rw, q_drift, t);
if var(1.0) == 0.0 && q_wf == 0.0 && q_rw == 0.0 && q_drift == 0.0 {
return f64::INFINITY;
}
let mut hi = 1.0_f64;
let mut guard = 0;
while var(hi) < target {
hi *= 2.0;
guard += 1;
if guard > 200 {
return f64::INFINITY; }
}
let mut lo = 0.0_f64;
for _ in 0..100 {
let mid = 0.5 * (lo + hi);
if var(mid) < target {
lo = mid;
} else {
hi = mid;
}
}
0.5 * (lo + hi)
}
pub fn deterministic_tie(freq_offset: f64, drift: f64, t: Seconds) -> Seconds {
let t = t.max(0.0);
freq_offset * t + 0.5 * drift * t * t
}
pub fn phase_to_range_m(phase_s: Seconds) -> f64 {
C_LIGHT_M_PER_S * phase_s
}
pub fn holdover_for_class(class: ClockClass, threshold_s: Seconds) -> Seconds {
let (q_wf, q_rw, q_drift) = class.psds();
holdover_seconds(q_wf, q_rw, q_drift, threshold_s)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum QuantumClockClass {
OpticalLattice,
TrappedIon,
MercuryIon,
}
impl QuantumClockClass {
pub fn adev_1s(self) -> f64 {
match self {
QuantumClockClass::OpticalLattice => 5.0e-16,
QuantumClockClass::TrappedIon => 1.0e-15,
QuantumClockClass::MercuryIon => 1.0e-13,
}
}
pub fn psds(self) -> (f64, f64, f64) {
let a = self.adev_1s();
q_from_allan(a, a * 1.0e-2, a * 1.0e-4)
}
pub fn holdover_seconds(self, threshold_s: Seconds) -> Seconds {
let (q_wf, q_rw, q_drift) = self.psds();
holdover_seconds(q_wf, q_rw, q_drift, threshold_s)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clock_state::ClockState3;
#[test]
fn coast_variance_matches_multistep_kalman_recursion() {
let (q_wf, q_rw, q_drift) = (1e-20, 1e-24, 1e-30);
for &t in &[100.0, 3600.0] {
let mut cs = ClockState3::new(q_wf, q_rw, q_drift);
let steps = 500;
let dt = t / steps as f64;
for _ in 0..steps {
cs.predict(dt);
}
let theirs = cs.p[0][0]; let ours = coast_phase_variance(q_wf, q_rw, q_drift, t);
let rel = (ours - theirs).abs() / theirs.max(1e-300);
assert!(
rel < 1e-9,
"t={t}: closed {ours:.6e} vs 500-step recursion {theirs:.6e}"
);
}
}
#[test]
fn white_fm_holdover_is_exact_closed_form() {
let q_wf = 9e-21; let threshold = 1e-9; let t = holdover_seconds(q_wf, 0.0, 0.0, threshold);
let exact = threshold * threshold / q_wf; let rel = (t - exact).abs() / exact;
assert!(rel < 1e-9, "holdover {t:.6e} vs exact {exact:.6e}");
}
#[test]
fn holdover_round_trips_to_threshold() {
let (q_wf, q_rw, q_drift) = (1e-22, 1e-26, 1e-32);
let threshold = 1e-8;
let t = holdover_seconds(q_wf, q_rw, q_drift, threshold);
let sigma = coast_phase_sigma(q_wf, q_rw, q_drift, t);
let rel = (sigma - threshold).abs() / threshold;
assert!(
rel < 1e-6,
"σ(holdover)={sigma:.6e} vs threshold {threshold:.6e}"
);
}
#[test]
fn variance_is_monotone_in_time() {
let (q_wf, q_rw, q_drift) = (1e-20, 1e-24, 1e-30);
let mut prev = 0.0;
for k in 0..20 {
let t = (k as f64) * 100.0 + 1.0;
let v = coast_phase_variance(q_wf, q_rw, q_drift, t);
assert!(v > prev, "variance not increasing at t={t}");
prev = v;
}
}
#[test]
fn holdover_ordering_follows_the_shared_floor_recipe() {
let threshold = 1e-8; let csac = holdover_for_class(ClockClass::Csac, threshold);
let uso = holdover_for_class(ClockClass::Uso, threshold);
let dsac = holdover_for_class(ClockClass::Dsac, threshold);
let optical = QuantumClockClass::OpticalLattice.holdover_seconds(threshold);
assert!(uso > csac, "USO {uso:.2} should beat CSAC {csac:.2}");
assert!(dsac > uso, "DSAC {dsac:.2} should beat USO {uso:.2}");
assert!(
optical > dsac,
"optical {optical:.2} should beat DSAC {dsac:.2}"
);
}
#[test]
fn class_holdover_to_tight_threshold_is_floor_dominated() {
let threshold = 1e-9; let a = QuantumClockClass::OpticalLattice.adev_1s();
let (q_wf, _, _) = QuantumClockClass::OpticalLattice.psds();
let white_only = holdover_seconds(q_wf, 0.0, 0.0, threshold);
assert!(
white_only > 1.0e9,
"white-FM-only holdover {white_only:.2e} s is mission-irrelevant"
);
let with_floor = QuantumClockClass::OpticalLattice.holdover_seconds(threshold);
assert!(
with_floor < white_only / 1.0e3,
"the assumed floor must dominate the class holdover"
);
let (q_wf2, q_rw2, q_drift2) = q_from_allan(a, a * 1.0e-3, a * 1.0e-3);
let steeper = holdover_seconds(q_wf2, q_rw2, q_drift2, threshold);
assert!(
(steeper - with_floor).abs() / with_floor > 0.2,
"holdover should be sensitive to the assumed floor ({steeper:.2e} vs {with_floor:.2e})"
);
}
#[test]
fn nan_or_negative_psd_returns_nan() {
assert!(holdover_seconds(f64::NAN, 0.0, 0.0, 1e-9).is_nan());
assert!(holdover_seconds(-1e-20, 0.0, 0.0, 1e-9).is_nan());
assert!(holdover_seconds(1e-20, -1e-30, 0.0, 1e-9).is_nan());
}
#[test]
fn deterministic_tie_quadratic_in_drift() {
let drift = 1e-12; let t = 1000.0;
let tie = deterministic_tie(0.0, drift, t);
let expected = 0.5 * drift * t * t;
assert!((tie - expected).abs() < 1e-18, "tie {tie:.6e}");
let tie2 = deterministic_tie(1e-13, 0.0, t);
assert!((tie2 - 1e-13 * t).abs() < 1e-22);
}
#[test]
fn one_ns_is_about_thirty_cm() {
let r = phase_to_range_m(1e-9);
assert!((r - 0.299_792_458).abs() < 1e-9, "1 ns → {r} m");
}
#[test]
fn zero_threshold_and_noiseless_clock() {
assert_eq!(holdover_seconds(1e-20, 0.0, 0.0, 0.0), 0.0);
assert_eq!(holdover_seconds(0.0, 0.0, 0.0, 1e-9), f64::INFINITY);
}
}