#[derive(Clone, Debug)]
pub struct KalmanClock {
x: [f64; 2], p: [[f64; 2]; 2], q_wf: f64, q_rw: f64, r: f64, }
impl KalmanClock {
pub fn new(q_wf: f64, q_rw: f64, r: f64) -> Self {
Self {
x: [0.0, 0.0],
p: [[0.0, 0.0], [0.0, 0.0]],
q_wf,
q_rw,
r,
}
}
pub fn with_initial_cov(mut self, phase_var: f64, freq_var: f64) -> Self {
self.p = [[phase_var, 0.0], [0.0, freq_var]];
self
}
pub fn predict(&mut self, dt: f64) {
if dt <= 0.0 {
return;
}
self.x[0] += dt * self.x[1];
let p = self.p;
let fp = [
[p[0][0] + dt * p[1][0], p[0][1] + dt * p[1][1]],
[p[1][0], p[1][1]],
];
let mut np = [
[fp[0][0] + dt * fp[0][1], fp[0][1]],
[fp[1][0] + dt * fp[1][1], fp[1][1]],
];
let (dt2, dt3) = (dt * dt, dt * dt * dt);
np[0][0] += self.q_wf * dt + self.q_rw * dt3 / 3.0;
np[0][1] += self.q_rw * dt2 / 2.0;
np[1][0] += self.q_rw * dt2 / 2.0;
np[1][1] += self.q_rw * dt;
self.p = np;
}
pub fn update(&mut self, z: f64) {
self.update_with_r(z, self.r);
}
pub fn update_with_r(&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]; let innov = z - self.x[0];
self.x[0] += k[0] * innov;
self.x[1] += k[1] * innov;
let p = self.p;
self.p = [
[(1.0 - k[0]) * p[0][0], (1.0 - k[0]) * p[0][1]],
[p[1][0] - k[1] * p[0][0], p[1][1] - k[1] * p[0][1]],
];
}
pub fn phase_est(&self) -> f64 {
self.x[0]
}
pub fn freq_est(&self) -> f64 {
self.x[1]
}
pub fn phase_var(&self) -> f64 {
self.p[0][0]
}
pub fn phase_sigma(&self) -> f64 {
self.p[0][0].max(0.0).sqrt()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn coasting_covariance_matches_analytic_holdover() {
let (q_wf, q_rw, r) = (1e-24, 1e-30, 1e-20);
let dt = 1.0;
let n = 100usize;
let t = n as f64 * dt;
let mut kf = KalmanClock::new(q_wf, q_rw, r);
for _ in 0..n {
kf.predict(dt);
}
let expected_phase_var = q_wf * t + q_rw * t.powi(3) / 3.0;
let rel = (kf.phase_var() - expected_phase_var).abs() / expected_phase_var;
assert!(
rel < 1e-9,
"phase_var={} expected={expected_phase_var}",
kf.phase_var()
);
}
#[test]
fn pure_random_walk_fm_coast_is_q_rw_t_cubed_over_three() {
let q_rw = 2e-31;
let dt = 0.5;
let n = 200usize;
let t = n as f64 * dt;
let mut kf = KalmanClock::new(0.0, q_rw, 1e-20);
for _ in 0..n {
kf.predict(dt);
}
let expected = q_rw * t.powi(3) / 3.0;
let rel = (kf.phase_var() - expected).abs() / expected;
assert!(
rel < 1e-9,
"phase_var={} expected={expected}",
kf.phase_var()
);
}
#[test]
fn measurement_pulls_estimate_and_shrinks_covariance() {
let mut kf = KalmanClock::new(1e-24, 1e-30, 1e-26);
for _ in 0..50 {
kf.predict(1.0);
}
let var_before = kf.phase_var();
kf.update(3e-12);
assert!(kf.phase_var() < var_before, "covariance did not shrink");
assert!(
(kf.phase_est() - 3e-12).abs() < 3e-13,
"phase_est={}",
kf.phase_est()
);
}
#[test]
fn perfect_repeated_measurements_drive_variance_down() {
let mut kf = KalmanClock::new(1e-26, 1e-32, 1e-24).with_initial_cov(1e-18, 1e-24);
for _ in 0..200 {
kf.predict(1.0);
kf.update(0.0);
}
assert!(kf.phase_var() < 1e-22, "phase_var={}", kf.phase_var());
assert!(kf.phase_est().abs() < 1e-9);
}
#[test]
fn predict_update_sequence_is_deterministic() {
let run = || {
let mut kf = KalmanClock::new(1e-24, 1e-30, 1e-22);
for i in 0..100 {
kf.predict(1.0);
if i % 5 == 0 {
kf.update(1e-13 * i as f64);
}
}
(kf.phase_est(), kf.phase_var())
};
assert_eq!(run(), run());
}
}