use crate::scenario::GnssState;
use crate::types::Seconds;
pub struct HoldoverEstimator {
f_s: f64,
drift_est: f64,
last_sync_t: Seconds,
last_sync_phase: Seconds,
synced: bool,
}
impl HoldoverEstimator {
pub fn new() -> Self {
Self {
f_s: 0.0,
drift_est: 0.0,
last_sync_t: 0.0,
last_sync_phase: 0.0,
synced: false,
}
}
pub fn timing_error(
&mut self,
t: Seconds,
true_phase: Seconds,
det_freq: f64,
drift: f64,
gnss: GnssState,
) -> Seconds {
match gnss {
GnssState::Nominal => {
self.f_s = det_freq;
self.drift_est = drift;
self.last_sync_t = t;
self.last_sync_phase = true_phase;
self.synced = true;
0.0
}
_ => {
if !self.synced {
return 0.0;
}
let dt = t - self.last_sync_t;
let predicted =
self.last_sync_phase + self.f_s * dt + 0.5 * self.drift_est * dt * dt;
true_phase - predicted
}
}
}
}
impl Default for HoldoverEstimator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scenario::GnssState::*;
#[test]
fn synced_then_perfect_prediction_is_zero_error() {
let mut e = HoldoverEstimator::new();
assert_eq!(e.timing_error(0.0, 0.0, 1e-9, 0.0, Nominal), 0.0);
let true_phase = 1e-9 * 10.0;
assert!((e.timing_error(10.0, true_phase, 1e-9, 0.0, Denied)).abs() < 1e-18);
}
#[test]
fn quadratic_aging_is_removed() {
let mut e = HoldoverEstimator::new();
e.timing_error(0.0, 0.0, 1e-9, 2e-13, Nominal);
let true_phase = 1e-9 * 10.0 + 2e-13 * 10.0 * 10.0 / 2.0;
let err = e.timing_error(10.0, true_phase, 1.002e-9, 2e-13, Denied);
assert!(err.abs() < 1e-22, "err={err}");
}
#[test]
fn residual_drift_during_outage_is_reported() {
let mut e = HoldoverEstimator::new();
e.timing_error(0.0, 0.0, 0.0, 0.0, Nominal);
assert!((e.timing_error(5.0, 2e-7, 0.0, 0.0, Denied) - 2e-7).abs() < 1e-18);
}
#[test]
fn never_synced_returns_zero() {
let mut e = HoldoverEstimator::new();
assert_eq!(e.timing_error(3.0, 1.0, 0.0, 0.0, Denied), 0.0);
}
}