#[derive(Clone, Debug)]
pub struct ClockState3 {
pub x: [f64; 3],
pub p: [[f64; 3]; 3],
q_wf: f64, q_rw: f64, q_drift: f64, }
impl ClockState3 {
pub fn new(q_wf: f64, q_rw: f64, q_drift: f64) -> Self {
Self {
x: [0.0; 3],
p: [[0.0; 3]; 3],
q_wf,
q_rw,
q_drift,
}
}
pub fn with_initial_cov(mut self, phase_var: f64, freq_var: f64, drift_var: f64) -> Self {
self.p = [
[phase_var, 0.0, 0.0],
[0.0, freq_var, 0.0],
[0.0, 0.0, drift_var],
];
self
}
pub fn predict(&mut self, dt: f64) {
if dt <= 0.0 {
return;
}
let (dt2, dt3, dt4, dt5) = {
let d2 = dt * dt;
(d2, d2 * dt, d2 * d2, d2 * d2 * dt)
};
let half_dt2 = 0.5 * dt2;
self.x[0] += dt * self.x[1] + half_dt2 * self.x[2];
self.x[1] += dt * self.x[2];
let p = self.p;
let fp = [
[
p[0][0] + dt * p[1][0] + half_dt2 * p[2][0],
p[0][1] + dt * p[1][1] + half_dt2 * p[2][1],
p[0][2] + dt * p[1][2] + half_dt2 * p[2][2],
],
[
p[1][0] + dt * p[2][0],
p[1][1] + dt * p[2][1],
p[1][2] + dt * p[2][2],
],
[p[2][0], p[2][1], p[2][2]],
];
let mut np = [[0.0f64; 3]; 3];
for i in 0..3 {
np[i][0] = fp[i][0] + dt * fp[i][1] + half_dt2 * fp[i][2];
np[i][1] = fp[i][1] + dt * fp[i][2];
np[i][2] = fp[i][2];
}
let (qwf, qrw, qd) = (self.q_wf, self.q_rw, self.q_drift);
np[0][0] += qwf * dt + qrw * dt3 / 3.0 + qd * dt5 / 20.0;
let q01 = qrw * dt2 / 2.0 + qd * dt4 / 8.0;
np[0][1] += q01;
np[1][0] += q01;
let q02 = qd * dt3 / 6.0;
np[0][2] += q02;
np[2][0] += q02;
np[1][1] += qrw * dt + qd * dt3 / 3.0;
let q12 = qd * dt2 / 2.0;
np[1][2] += q12;
np[2][1] += q12;
np[2][2] += qd * dt;
self.p = np;
}
pub fn update_phase(&mut self, z: f64, r: f64) {
let s = self.p[0][0] + r;
if s <= 0.0 {
return;
}
let k = [self.p[0][0] / s, self.p[1][0] / s, self.p[2][0] / s];
let innov = z - self.x[0];
for (xi, &ki) in self.x.iter_mut().zip(k.iter()) {
*xi += ki * innov;
}
let a = [[1.0 - k[0], 0.0, 0.0], [-k[1], 1.0, 0.0], [-k[2], 0.0, 1.0]];
let p = self.p;
let mut ap = [[0.0f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
ap[i][j] = a[i][0] * p[0][j] + a[i][1] * p[1][j] + a[i][2] * p[2][j];
}
}
let mut np = [[0.0f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
np[i][j] = ap[i][0] * a[j][0] + ap[i][1] * a[j][1] + ap[i][2] * a[j][2];
}
}
for i in 0..3 {
for j in 0..3 {
np[i][j] += r * k[i] * k[j];
}
}
self.p = np;
}
pub fn covariance(&self) -> [[f64; 3]; 3] {
self.p
}
pub fn phase_est(&self) -> f64 {
self.x[0]
}
pub fn freq_est(&self) -> f64 {
self.x[1]
}
pub fn drift_est(&self) -> f64 {
self.x[2]
}
pub fn phase_sigma(&self) -> f64 {
self.p[0][0].max(0.0).sqrt()
}
pub fn is_psd(&self) -> bool {
let p = self.p;
let scale = p[0][0]
.abs()
.max(p[1][1].abs())
.max(p[2][2].abs())
.max(1e-300);
let tol = -1e-9 * scale;
let sym_tol = 1e-9 * scale + 1e-300;
if (p[0][1] - p[1][0]).abs() > sym_tol
|| (p[0][2] - p[2][0]).abs() > sym_tol
|| (p[1][2] - p[2][1]).abs() > sym_tol
{
return false;
}
let pivot = |sum: f64| -> Option<f64> {
if sum < tol {
None
} else {
Some(sum.max(0.0).sqrt())
}
};
let off = |sum: f64, diag: f64| -> Option<f64> {
if diag > 0.0 {
Some(sum / diag)
} else if sum.abs() > 1e-6 * scale + 1e-300 {
None } else {
Some(0.0)
}
};
let Some(l00) = pivot(p[0][0]) else {
return false;
};
let Some(l10) = off(p[1][0], l00) else {
return false;
};
let Some(l11) = pivot(p[1][1] - l10 * l10) else {
return false;
};
let Some(l20) = off(p[2][0], l00) else {
return false;
};
let Some(l21) = off(p[2][1] - l20 * l10, l11) else {
return false;
};
pivot(p[2][2] - l20 * l20 - l21 * l21).is_some()
}
}
pub fn q_from_allan(white_fm_adev_1s: f64, rw_fm_level: f64, drift_level: f64) -> (f64, f64, f64) {
let q_wf = white_fm_adev_1s * white_fm_adev_1s;
let q_rw = 3.0 * rw_fm_level * rw_fm_level;
let q_drift = 20.0 * drift_level * drift_level;
(q_wf, q_rw, q_drift)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ClockClass {
Csac,
Uso,
Dsac,
}
impl ClockClass {
pub fn adev_1s(self) -> f64 {
match self {
ClockClass::Csac => 3.0e-10,
ClockClass::Uso => 1.0e-12,
ClockClass::Dsac => 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 doppler_floor_1s(self) -> f64 {
const C: f64 = 299_792_458.0;
C * self.adev_1s()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::allan::overlapping_adev;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
use rand_distr::{Distribution, Normal};
#[test]
fn van_loan_q_3state_matches_analytic() {
let (q_wf, q_rw, q_drift) = (1e-24, 1e-30, 1e-36);
let dt = 0.7;
let mut kf = ClockState3::new(q_wf, q_rw, q_drift);
kf.predict(dt);
let p = kf.covariance();
let (dt2, dt3, dt4, dt5) = (dt * dt, dt.powi(3), dt.powi(4), dt.powi(5));
let q00 = q_wf * dt + q_rw * dt3 / 3.0 + q_drift * dt5 / 20.0;
let q01 = q_rw * dt2 / 2.0 + q_drift * dt4 / 8.0;
let q02 = q_drift * dt3 / 6.0;
let q11 = q_rw * dt + q_drift * dt3 / 3.0;
let q12 = q_drift * dt2 / 2.0;
let q22 = q_drift * dt;
let want = [[q00, q01, q02], [q01, q11, q12], [q02, q12, q22]];
for i in 0..3 {
for j in 0..3 {
let scale = want[i][j].abs().max(1e-300);
assert!(
(p[i][j] - want[i][j]).abs() / scale < 1e-12,
"Q[{i}][{j}] = {} vs analytic {} (rel {})",
p[i][j],
want[i][j],
(p[i][j] - want[i][j]).abs() / scale
);
}
}
}
#[test]
fn van_loan_q_reduces_to_two_state_when_drift_off() {
let (q_wf, q_rw) = (2e-24, 3e-30);
let dt = 1.3;
let mut kf = ClockState3::new(q_wf, q_rw, 0.0);
kf.predict(dt);
let p = kf.covariance();
let (dt2, dt3) = (dt * dt, dt.powi(3));
assert!((p[0][0] - (q_wf * dt + q_rw * dt3 / 3.0)).abs() / p[0][0] < 1e-12);
assert!((p[0][1] - q_rw * dt2 / 2.0).abs() / p[0][1].abs() < 1e-12);
assert!((p[1][1] - q_rw * dt).abs() / p[1][1] < 1e-12);
assert_eq!(p[0][2], 0.0);
assert_eq!(p[1][2], 0.0);
assert_eq!(p[2][2], 0.0);
}
#[test]
fn coasting_phase_variance_matches_analytic_holdover() {
let (q_wf, q_rw, q_drift) = (1e-24, 1e-30, 1e-38);
let dt = 1.0;
let n = 100usize;
let t = n as f64 * dt;
let mut kf = ClockState3::new(q_wf, q_rw, q_drift);
for _ in 0..n {
kf.predict(dt);
}
let expected = q_wf * t + q_rw * t.powi(3) / 3.0 + q_drift * t.powi(5) / 20.0;
let rel = (kf.covariance()[0][0] - expected).abs() / expected;
assert!(
rel < 1e-9,
"P00 = {} expected {expected}",
kf.covariance()[0][0]
);
}
#[test]
fn drift_state_tracks_a_pure_aging_ramp() {
let true_drift = 5.0e-13; let dt = 1.0;
let (q_wf, q_rw, q_drift) = q_from_allan(1e-13, 1e-15, 1e-16);
let mut kf = ClockState3::new(q_wf, q_rw, q_drift).with_initial_cov(1e-18, 1e-20, 1e-24);
let r = 1e-24; for i in 0..400 {
kf.predict(dt);
let t = (i + 1) as f64 * dt;
let truth_phase = 0.5 * true_drift * t * t; kf.update_phase(truth_phase, r);
}
let rel = (kf.drift_est() - true_drift).abs() / true_drift;
assert!(
rel < 0.05,
"recovered drift {} vs true {true_drift} (rel {rel})",
kf.drift_est()
);
}
#[test]
fn clock_state_matches_uso_allan() {
let adev_1s = 1.0e-12; let rw_level = 1.0e-14; let (q_wf, q_rw, _q_d) = q_from_allan(adev_1s, rw_level, 0.0);
let dt = 1.0;
let n = 1 << 14;
let mut rng = ChaCha8Rng::seed_from_u64(20260614);
let rw = Normal::new(0.0, (q_rw * dt).sqrt()).unwrap();
let wf = Normal::new(0.0, (q_wf * dt).sqrt()).unwrap();
let mut freq = 0.0f64;
let mut phase = 0.0f64;
let mut truth = Vec::with_capacity(n);
truth.push(0.0);
for _ in 1..n {
freq += rw.sample(&mut rng);
phase += freq * dt + wf.sample(&mut rng);
truth.push(phase);
}
let recovered_1s = overlapping_adev(&truth, dt, 1);
let rel = (recovered_1s - adev_1s).abs() / adev_1s;
assert!(
rel < 0.25,
"recovered ADEV(1 s) = {recovered_1s} vs injected {adev_1s} (rel {rel})"
);
let r = (5.0e-13_f64).powi(2); let meas = Normal::new(0.0, r.sqrt()).unwrap();
let mut kf = ClockState3::new(q_wf, q_rw, 0.0).with_initial_cov(1e-20, 1e-24, 1e-30);
let mut inside = 0usize;
let mut total = 0usize;
for &tp in truth.iter().skip(1) {
kf.predict(dt);
kf.update_phase(tp + meas.sample(&mut rng), r);
let err = (kf.phase_est() - tp).abs();
let sigma = kf.phase_sigma();
if err <= 3.0 * sigma {
inside += 1;
}
total += 1;
assert!(kf.is_psd(), "covariance lost PSD-ness mid-run");
}
let frac = inside as f64 / total as f64;
assert!(
frac > 0.95,
"estimator inconsistent: only {:.1}% of steps inside 3-sigma",
100.0 * frac
);
}
#[test]
fn covariance_stays_psd() {
let (q_wf, q_rw, q_drift) = (1e-30, 1e-34, 1e-40);
let r = 1e-26;
let mut kf = ClockState3::new(q_wf, q_rw, q_drift).with_initial_cov(1e-18, 1e-24, 1e-30);
for i in 0..500 {
kf.predict(1.0);
kf.update_phase(1e-13 * (i as f64).sin(), r);
assert!(
kf.is_psd(),
"covariance lost PSD-ness at step {i}: P = {:?}",
kf.covariance()
);
let p = kf.covariance();
for a in 0..3 {
assert!(
p[a][a] >= 0.0,
"negative variance P[{a}][{a}] = {}",
p[a][a]
);
for b in (a + 1)..3 {
let scale = p[a][a].abs().max(p[b][b].abs()).max(1e-300);
assert!(
(p[a][b] - p[b][a]).abs() <= 1e-9 * scale,
"P not symmetric at ({a},{b})"
);
}
}
}
}
#[test]
fn measurement_pulls_estimate_and_shrinks_phase_variance() {
let mut kf = ClockState3::new(1e-24, 1e-30, 1e-36).with_initial_cov(1e-18, 1e-22, 1e-28);
for _ in 0..50 {
kf.predict(1.0);
}
let var_before = kf.covariance()[0][0];
kf.update_phase(3e-12, 1e-26);
assert!(
kf.covariance()[0][0] < var_before,
"phase variance did not shrink"
);
assert!(
(kf.phase_est() - 3e-12).abs() < 3e-13,
"phase_est = {}",
kf.phase_est()
);
}
#[test]
fn predict_update_sequence_is_deterministic() {
let run = || {
let mut kf = ClockState3::new(1e-24, 1e-30, 1e-36);
for i in 0..100 {
kf.predict(1.0);
if i % 5 == 0 {
kf.update_phase(1e-13 * i as f64, 1e-22);
}
}
(kf.phase_est(), kf.freq_est(), kf.drift_est())
};
assert_eq!(run(), run());
}
#[test]
fn clock_classes_carry_their_cited_figures() {
assert_eq!(ClockClass::Csac.adev_1s(), 3.0e-10);
assert_eq!(ClockClass::Uso.adev_1s(), 1.0e-12);
assert_eq!(ClockClass::Dsac.adev_1s(), 1.0e-13);
assert!(ClockClass::Csac.adev_1s() > ClockClass::Uso.adev_1s());
assert!(ClockClass::Uso.adev_1s() > ClockClass::Dsac.adev_1s());
let uso = ClockClass::Uso.doppler_floor_1s();
assert!((2e-4..4e-4).contains(&uso), "USO Doppler floor {uso} m/s");
let dsac = ClockClass::Dsac.doppler_floor_1s();
assert!(
(2e-5..4e-5).contains(&dsac),
"DSAC Doppler floor {dsac} m/s"
);
let (q_wf, q_rw, q_drift) = q_from_allan(2.0e-12, 1.0e-14, 1.0e-16);
assert!((q_wf - 4.0e-24).abs() / q_wf < 1e-12); assert!((q_rw - 3.0e-28).abs() / q_rw < 1e-12); assert!((q_drift - 2.0e-31).abs() / q_drift < 1e-12); }
}