use crate::allan::{overlapping_adev, time_deviation, PowerLawNoise};
use crate::timegeo::{sagnac_correction, C_M_PER_S, OMEGA_EARTH};
use crate::types::Seconds;
use rand::{RngCore, SeedableRng};
use rand_chacha::ChaCha8Rng;
use rand_distr::{Distribution, Normal};
use std::f64::consts::PI;
type Vec3 = [f64; 3];
fn equatorial_triangle_area(a: Vec3, b: Vec3, c: Vec3) -> f64 {
0.5 * ((b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]))
}
pub fn twstft_sagnac(r_a: Vec3, r_s: Vec3, r_b: Vec3) -> f64 {
sagnac_correction(r_a, r_s) + sagnac_correction(r_s, r_b) + sagnac_correction(r_b, r_a)
}
pub fn twstft_sagnac_bipm(r_a: Vec3, r_s: Vec3, r_b: Vec3) -> f64 {
let area = equatorial_triangle_area(r_a, r_s, r_b);
2.0 * area * OMEGA_EARTH / (C_M_PER_S * C_M_PER_S)
}
#[derive(Clone, Debug)]
pub struct TwstftScenario {
pub r_a: Vec3,
pub r_s: Vec3,
pub r_b: Vec3,
pub true_offset_s: f64,
pub transponder_delay_s: f64,
pub sigma_j: f64,
pub step_s: Seconds,
pub n_steps: usize,
pub seed: u64,
}
#[derive(Clone, Debug)]
pub struct TwstftResult {
pub sagnac_s: f64,
pub bipm_sagnac_s: f64,
pub offset_est_s: f64,
pub tdev: Vec<(f64, f64)>,
}
pub fn run_twstft(scn: &TwstftScenario) -> TwstftResult {
let sagnac = twstft_sagnac(scn.r_a, scn.r_s, scn.r_b);
let bipm = twstft_sagnac_bipm(scn.r_a, scn.r_s, scn.r_b);
let nrm = Normal::new(0.0, scn.sigma_j).unwrap();
let mut rng = ChaCha8Rng::seed_from_u64(scn.seed);
let mut corrected = Vec::with_capacity(scn.n_steps);
for _ in 0..scn.n_steps {
let raw = scn.true_offset_s - sagnac + nrm.sample(&mut rng);
corrected.push(raw + sagnac);
}
let mean = corrected.iter().sum::<f64>() / corrected.len() as f64;
let mut tdev = Vec::new();
let mut m = 1usize;
while corrected.len() >= 3 * m {
let td = time_deviation(&corrected, scn.step_s, m);
if td.is_finite() {
tdev.push(((m as f64) * scn.step_s, td));
}
m *= 2;
}
TwstftResult {
sagnac_s: sagnac,
bipm_sagnac_s: bipm,
offset_est_s: mean,
tdev,
}
}
#[derive(Clone, Copy, Debug)]
pub struct CvEpoch {
pub pr_a: f64,
pub range_a: f64,
pub pr_b: f64,
pub range_b: f64,
}
pub fn gnss_common_view_series(epochs: &[CvEpoch]) -> Vec<f64> {
epochs
.iter()
.map(|e| crate::timegeo::common_view_offset(e.pr_a, e.range_a, e.pr_b, e.range_b))
.collect()
}
pub fn offset_tdev(series: &[f64], step_s: Seconds) -> Vec<(f64, f64)> {
let mut out = Vec::new();
let mut m = 1usize;
while series.len() >= 3 * m {
let td = time_deviation(series, step_s, m);
if td.is_finite() {
out.push(((m as f64) * step_s, td));
}
m *= 2;
}
out
}
pub const F_L1: f64 = 1_575.42e6;
pub const F_L2: f64 = 1_227.60e6;
pub fn iono_free_combination(p1: f64, p2: f64) -> f64 {
let f1 = F_L1 * F_L1;
let f2 = F_L2 * F_L2;
(f1 * p1 - f2 * p2) / (f1 - f2)
}
pub fn ppp_receiver_clock(p_if: f64, geom_range: f64, sat_clock_s: f64) -> f64 {
(p_if - geom_range) / C_M_PER_S + sat_clock_s
}
pub fn iono_delay_m(tec_tecu: f64, f_hz: f64) -> f64 {
40.3 * 1.0e16 * tec_tecu / (f_hz * f_hz)
}
pub fn rytov_variance(cn2: f64, wavelength_m: f64, path_len_m: f64) -> f64 {
let k = 2.0 * PI / wavelength_m;
1.23 * cn2 * k.powf(7.0 / 6.0) * path_len_m.powf(11.0 / 6.0)
}
pub fn fried_parameter(cn2: f64, wavelength_m: f64, path_len_m: f64) -> f64 {
let k = 2.0 * PI / wavelength_m;
(0.423 * k * k * cn2 * path_len_m).powf(-3.0 / 5.0)
}
pub fn lognormal_fading(sigma_r2: f64, rng: &mut dyn RngCore) -> f64 {
let sig_chi2 = sigma_r2 / 4.0;
let nrm = Normal::new(-sig_chi2, sig_chi2.sqrt()).unwrap();
(2.0 * nrm.sample(rng)).exp()
}
#[derive(Clone, Debug)]
pub struct PowerLawFit {
pub h2: f64,
pub h1: f64,
pub h0: f64,
pub hm1: f64,
pub hm2: f64,
pub dominant_per_decade: Vec<(f64, PowerLawNoise)>,
}
fn avar_basis(tau: f64, f_h: f64) -> [f64; 5] {
let fourpi2 = 4.0 * PI * PI;
[
3.0 * f_h / (fourpi2 * tau * tau), (1.038 + 3.0 * (2.0 * PI * f_h * tau).ln()) / (fourpi2 * tau * tau), 1.0 / (2.0 * tau), 2.0 * (2.0_f64).ln(), 2.0 * PI * PI * tau / 3.0, ]
}
fn solve_lin(mut a: Vec<Vec<f64>>, mut b: Vec<f64>) -> Option<Vec<f64>> {
let n = b.len();
for col in 0..n {
let mut piv = col;
for r in (col + 1)..n {
if a[r][col].abs() > a[piv][col].abs() {
piv = r;
}
}
if a[piv][col].abs() < 1e-300 {
return None;
}
a.swap(col, piv);
b.swap(col, piv);
let pivot_row = a[col].clone();
let b_col = b[col];
let akk = pivot_row[col];
for r in (col + 1)..n {
let f = a[r][col] / akk;
for (slot, &pv) in a[r][col..n].iter_mut().zip(pivot_row[col..n].iter()) {
*slot -= f * pv;
}
b[r] -= f * b_col;
}
}
let mut x = vec![0.0; n];
for i in (0..n).rev() {
let mut s = b[i];
for c in (i + 1)..n {
s -= a[i][c] * x[c];
}
x[i] = s / a[i][i];
}
Some(x)
}
pub fn fit_power_law_psd(curve: &[(f64, f64)], f_h: f64) -> Option<PowerLawFit> {
if curve.len() < 5 {
return None;
}
let mut ata = vec![vec![0.0; 5]; 5];
let mut aty = vec![0.0; 5];
for &(tau, adev) in curve {
let row = avar_basis(tau, f_h);
let y = adev * adev;
for i in 0..5 {
aty[i] += row[i] * y;
for j in 0..5 {
ata[i][j] += row[i] * row[j];
}
}
}
let h = solve_lin(ata, aty)?;
let types = [
PowerLawNoise::WhitePm,
PowerLawNoise::FlickerPm,
PowerLawNoise::WhiteFm,
PowerLawNoise::FlickerFm,
PowerLawNoise::RandomWalkFm,
];
let tau_min = curve.iter().map(|p| p.0).fold(f64::INFINITY, f64::min);
let tau_max = curve.iter().map(|p| p.0).fold(0.0_f64, f64::max);
let dec_lo = tau_min.log10().floor() as i32;
let dec_hi = tau_max.log10().floor() as i32;
let mut dominant = Vec::new();
for d in dec_lo..=dec_hi {
let tau_c = 10f64.powi(d) * (10f64).sqrt(); let basis = avar_basis(tau_c, f_h);
let mut best = 0usize;
let mut best_val = f64::NEG_INFINITY;
for i in 0..5 {
let contrib = (h[i].max(0.0)) * basis[i];
if contrib > best_val {
best_val = contrib;
best = i;
}
}
dominant.push((tau_c, types[best]));
}
Some(PowerLawFit {
h2: h[0],
h1: h[1],
h0: h[2],
hm1: h[3],
hm2: h[4],
dominant_per_decade: dominant,
})
}
pub fn ensemble_timescale(clocks: &[Vec<f64>], weights: &[f64]) -> Vec<f64> {
assert_eq!(clocks.len(), weights.len());
assert!(!clocks.is_empty());
let len = clocks[0].len();
let wsum: f64 = weights.iter().sum();
(0..len)
.map(|k| {
clocks
.iter()
.zip(weights)
.map(|(c, &w)| w * c[k])
.sum::<f64>()
/ wsum
})
.collect()
}
pub fn adev_tau0(phase: &[f64], tau0: Seconds) -> f64 {
overlapping_adev(phase, tau0, 1)
}
#[cfg(test)]
mod tests {
use super::*;
const RE: f64 = 6_378_137.0;
const GEO: f64 = 4.2164e7;
#[test]
fn twstft_sagnac_equals_the_bipm_2a_omega_over_c2_form() {
let a = [RE, 0.0, 0.0];
let s = [GEO * 0.5, GEO * 0.866_025_403_8, 0.0]; let b = [RE * 0.5, RE * 0.866_025_403_8, 0.0]; let loop_sum = twstft_sagnac(a, s, b);
let bipm = twstft_sagnac_bipm(a, s, b);
assert!(
(loop_sum - bipm).abs() < 1e-18,
"loop {loop_sum} vs BIPM {bipm}"
);
}
#[test]
fn twstft_sagnac_within_5pct_for_a_continental_baseline() {
let a = [RE * 0.7, RE * 0.7, 0.0];
let s = [GEO * 0.2, GEO * 0.97, 0.0];
let b = [RE * 0.4, RE * 0.9, 0.0];
let loop_sum = twstft_sagnac(a, s, b);
let bipm = twstft_sagnac_bipm(a, s, b);
assert!(bipm.abs() > 1e-9, "Sagnac should be tens of ns: {bipm}");
assert!((loop_sum - bipm).abs() / bipm.abs() < 0.05);
}
#[test]
fn twstft_sagnac_is_zero_for_a_degenerate_radial_loop() {
let z = twstft_sagnac([RE, 0.0, 0.0], [2.0 * RE, 0.0, 0.0], [3.0 * RE, 0.0, 0.0]);
assert!(z.abs() < 1e-20, "z = {z}");
}
#[test]
fn twstft_campaign_recovers_the_offset_after_sagnac_removal() {
let scn = TwstftScenario {
r_a: [RE, 0.0, 0.0],
r_s: [GEO * 0.5, GEO * 0.866, 0.0],
r_b: [RE * 0.5, RE * 0.866, 0.0],
true_offset_s: 2.5e-8, transponder_delay_s: 1.2e-7,
sigma_j: 2e-10, step_s: 60.0,
n_steps: 1440, seed: 7,
};
let r = run_twstft(&scn);
assert!(
(r.offset_est_s - scn.true_offset_s).abs() < 5.0 * scn.sigma_j / (1440.0_f64).sqrt(),
"recovered {} vs true {}",
r.offset_est_s,
scn.true_offset_s
);
assert!((r.sagnac_s - r.bipm_sagnac_s).abs() < 1e-18);
}
#[test]
fn twstft_campaign_emits_a_finite_tdev_curve() {
let scn = TwstftScenario {
r_a: [RE, 0.0, 0.0],
r_s: [GEO * 0.5, GEO * 0.866, 0.0],
r_b: [RE * 0.5, RE * 0.866, 0.0],
true_offset_s: 0.0,
transponder_delay_s: 0.0,
sigma_j: 1e-10,
step_s: 60.0,
n_steps: 1440,
seed: 3,
};
let r = run_twstft(&scn);
assert!(r.tdev.len() >= 3, "expected a multi-point TDEV curve");
assert!(r
.tdev
.iter()
.all(|&(t, d)| t.is_finite() && d.is_finite() && d > 0.0));
assert!(r.tdev.first().unwrap().1 > r.tdev.last().unwrap().1);
}
fn cv_epoch(dt_a: f64, dt_b: f64, sv: f64, ra: f64, rb: f64) -> CvEpoch {
CvEpoch {
pr_a: ra + C_M_PER_S * (dt_a - sv),
range_a: ra,
pr_b: rb + C_M_PER_S * (dt_b - sv),
range_b: rb,
}
}
#[test]
fn common_view_recovers_offset_and_cancels_sat_clock() {
let (dt_a, dt_b) = (1.0e-7, -3.0e-8);
for &sv in &[0.0, 1e-6, -5e-7] {
let e = cv_epoch(dt_a, dt_b, sv, 2.10e7, 2.13e7);
let est = gnss_common_view_series(&[e])[0];
assert!((est - (dt_a - dt_b)).abs() < 1e-15, "sv {sv}: {est}");
}
}
#[test]
fn common_view_series_is_constant_for_a_constant_offset() {
let (dt_a, dt_b) = (5e-8, 5e-9);
let epochs: Vec<CvEpoch> = (0..50)
.map(|k| {
cv_epoch(
dt_a,
dt_b,
1e-6 * (k as f64).sin(),
2.0e7 + 1e3 * k as f64,
2.05e7,
)
})
.collect();
let s = gnss_common_view_series(&epochs);
assert!(s.iter().all(|v| (v - (dt_a - dt_b)).abs() < 1e-15));
}
#[test]
fn common_view_emits_a_tdev_curve() {
let epochs: Vec<CvEpoch> = (0..100)
.map(|k| cv_epoch(1e-7, 0.0, 1e-6 * (k as f64), 2.0e7, 2.0e7))
.collect();
let s = gnss_common_view_series(&epochs);
let td = offset_tdev(&s, 30.0);
assert!(td.len() >= 3 && td.iter().all(|&(t, d)| t.is_finite() && d.is_finite()));
}
#[test]
fn common_view_handles_differing_ranges() {
let e = cv_epoch(2e-8, -2e-8, 7e-7, 2.0e7, 2.4e7);
let est = gnss_common_view_series(&[e])[0];
assert!((est - 4e-8).abs() < 1e-15);
}
#[test]
fn common_view_white_noise_averages_down() {
let nrm = Normal::new(0.0, 1.0).unwrap(); let mut rng = ChaCha8Rng::seed_from_u64(11);
let n = 4000;
let mut series = Vec::new();
for _ in 0..n {
let na = nrm.sample(&mut rng);
let nb = nrm.sample(&mut rng);
let e = CvEpoch {
pr_a: 2.0e7 + C_M_PER_S * 1e-7 + na,
range_a: 2.0e7,
pr_b: 2.0e7 + nb,
range_b: 2.0e7,
};
series.push(gnss_common_view_series(&[e])[0]);
}
let mean = series.iter().sum::<f64>() / n as f64;
assert!((mean - 1e-7).abs() < 5.0 * 2f64.sqrt() / C_M_PER_S / (n as f64).sqrt());
}
#[test]
fn iono_free_cancels_first_order_ionosphere_exactly() {
let (rho, dt_rx, dt_sat, tec) = (2.15e7, 3e-7, 1e-9, 25.0);
let common = rho + C_M_PER_S * (dt_rx - dt_sat);
let p1 = common + iono_delay_m(tec, F_L1);
let p2 = common + iono_delay_m(tec, F_L2);
let p_if = iono_free_combination(p1, p2);
assert!((p_if - common).abs() < 1e-5, "p_if {p_if} vs {common}");
}
#[test]
fn ppp_recovers_receiver_clock_exactly_when_noiseless() {
let (rho, dt_rx, dt_sat, tec) = (2.0e7, -4.2e-7, 5e-10, 40.0);
let common = rho + C_M_PER_S * (dt_rx - dt_sat);
let p1 = common + iono_delay_m(tec, F_L1);
let p2 = common + iono_delay_m(tec, F_L2);
let p_if = iono_free_combination(p1, p2);
let est = ppp_receiver_clock(p_if, rho, dt_sat);
assert!((est - dt_rx).abs() < 1e-13, "est {est} vs {dt_rx}");
}
#[test]
fn iono_free_combination_coefficients_sum_to_unity_on_geometry() {
let f1 = F_L1 * F_L1;
let f2 = F_L2 * F_L2;
let c1 = f1 / (f1 - f2);
let c2 = -f2 / (f1 - f2);
assert!((c1 + c2 - 1.0).abs() < 1e-12);
assert!(c1 > 2.5 && c1 < 2.6); }
#[test]
fn iono_delay_is_larger_on_l2_than_l1() {
let d1 = iono_delay_m(30.0, F_L1);
let d2 = iono_delay_m(30.0, F_L2);
assert!(d2 > d1 && d1 > 0.0);
assert!(((d2 / d1) - (F_L1 / F_L2).powi(2)).abs() < 1e-9);
}
#[test]
fn ppp_noisy_clock_series_emits_tdev_and_stays_unbiased() {
let nrm = Normal::new(0.0, 0.3).unwrap(); let mut rng = ChaCha8Rng::seed_from_u64(21);
let (rho, dt_rx, dt_sat, tec) = (2.1e7, 6e-8, 0.0, 15.0);
let mut series = Vec::new();
for _ in 0..600 {
let common = rho + C_M_PER_S * (dt_rx - dt_sat);
let p1 = common + iono_delay_m(tec, F_L1) + nrm.sample(&mut rng);
let p2 = common + iono_delay_m(tec, F_L2) + nrm.sample(&mut rng);
let p_if = iono_free_combination(p1, p2);
series.push(ppp_receiver_clock(p_if, rho, dt_sat));
}
let mean = series.iter().sum::<f64>() / series.len() as f64;
assert!((mean - dt_rx).abs() < 1e-9, "mean {mean} vs {dt_rx}");
let td = offset_tdev(&series, 30.0);
assert!(td.len() >= 3 && td.iter().all(|&(_, d)| d.is_finite()));
}
#[test]
fn rytov_variance_matches_hand_calculation() {
let s = rytov_variance(1e-16, 1.064e-6, 1.0e4);
assert!((s - 0.210).abs() < 0.006, "σ_R² = {s}");
}
#[test]
fn fried_parameter_matches_hand_calculation() {
let r0 = fried_parameter(1e-16, 1.064e-6, 1.0e4);
assert!((r0 - 0.199).abs() < 0.006, "r₀ = {r0}");
}
#[test]
fn lognormal_fading_has_unit_mean() {
let sigma_r2 = 0.2;
let mut rng = ChaCha8Rng::seed_from_u64(99);
let n = 200_000;
let mean: f64 = (0..n)
.map(|_| lognormal_fading(sigma_r2, &mut rng))
.sum::<f64>()
/ n as f64;
assert!((mean - 1.0).abs() < 0.02, "mean = {mean}");
}
#[test]
fn lognormal_fading_variance_tracks_rytov() {
let sigma_r2 = 0.2;
let mut rng = ChaCha8Rng::seed_from_u64(100);
let n = 200_000;
let xs: Vec<f64> = (0..n)
.map(|_| lognormal_fading(sigma_r2, &mut rng))
.collect();
let mean = xs.iter().sum::<f64>() / n as f64;
let var = xs.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n as f64;
let expected = (sigma_r2).exp() - 1.0;
assert!(
(var - expected).abs() / expected < 0.08,
"var {var} vs {expected}"
);
}
#[test]
fn stronger_turbulence_increases_rytov_and_shrinks_fried() {
let weak = rytov_variance(1e-17, 1.55e-6, 1.0e4);
let strong = rytov_variance(1e-15, 1.55e-6, 1.0e4);
assert!(strong > weak);
let r0_weak = fried_parameter(1e-17, 1.55e-6, 1.0e4);
let r0_strong = fried_parameter(1e-15, 1.55e-6, 1.0e4);
assert!(r0_strong < r0_weak);
}
fn synthetic_curve(h2: f64, h1: f64, h0: f64, hm1: f64, hm2: f64, f_h: f64) -> Vec<(f64, f64)> {
(0..10)
.map(|i| {
let tau = (1u64 << i) as f64; let b = avar_basis(tau, f_h);
let avar = h2 * b[0] + h1 * b[1] + h0 * b[2] + hm1 * b[3] + hm2 * b[4];
(tau, avar.sqrt())
})
.collect()
}
#[test]
fn fit_recovers_pure_white_fm() {
let h0 = 1e-22;
let curve = synthetic_curve(0.0, 0.0, h0, 0.0, 0.0, 0.5);
let f = fit_power_law_psd(&curve, 0.5).unwrap();
assert!((f.h0 - h0).abs() / h0 < 1e-6, "h0 {} vs {h0}", f.h0);
assert!(f.hm2.abs() < 1e-28 && f.hm1.abs() < 1e-26);
}
#[test]
fn fit_recovers_pure_random_walk_fm() {
let hm2 = 3e-30;
let curve = synthetic_curve(0.0, 0.0, 0.0, 0.0, hm2, 0.5);
let f = fit_power_law_psd(&curve, 0.5).unwrap();
assert!((f.hm2 - hm2).abs() / hm2 < 1e-6, "hm2 {} vs {hm2}", f.hm2);
}
#[test]
fn fit_recovers_pure_flicker_fm() {
let hm1 = 2e-24;
let curve = synthetic_curve(0.0, 0.0, 0.0, hm1, 0.0, 0.5);
let f = fit_power_law_psd(&curve, 0.5).unwrap();
assert!((f.hm1 - hm1).abs() / hm1 < 1e-6, "hm1 {} vs {hm1}", f.hm1);
}
#[test]
fn fit_recovers_a_three_process_mix() {
let (h0, hm1, hm2) = (1e-22, 5e-25, 2e-30);
let curve = synthetic_curve(0.0, 0.0, h0, hm1, hm2, 0.5);
let f = fit_power_law_psd(&curve, 0.5).unwrap();
assert!((f.h0 - h0).abs() / h0 < 1e-5);
assert!((f.hm1 - hm1).abs() / hm1 < 1e-5);
assert!((f.hm2 - hm2).abs() / hm2 < 1e-5);
}
#[test]
fn fit_recovers_white_pm_with_cutoff() {
let h2 = 4e-26;
let f_h = 10.0;
let curve = synthetic_curve(h2, 0.0, 0.0, 0.0, 0.0, f_h);
let f = fit_power_law_psd(&curve, f_h).unwrap();
assert!((f.h2 - h2).abs() / h2 < 1e-4, "h2 {} vs {h2}", f.h2);
}
#[test]
fn fit_reports_dominant_process_per_decade() {
let curve = synthetic_curve(0.0, 0.0, 1e-22, 0.0, 1e-26, 0.5);
let f = fit_power_law_psd(&curve, 0.5).unwrap();
assert!(!f.dominant_per_decade.is_empty());
let first = f.dominant_per_decade.first().unwrap().1;
let last = f.dominant_per_decade.last().unwrap().1;
assert_eq!(first, PowerLawNoise::WhiteFm);
assert_eq!(last, PowerLawNoise::RandomWalkFm);
}
fn white_fm_clock(sigma_y: f64, tau0: f64, n: usize, seed: u64) -> Vec<f64> {
let nrm = Normal::new(0.0, sigma_y).unwrap();
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut phase = 0.0;
let mut out = Vec::with_capacity(n);
for _ in 0..n {
out.push(phase);
phase += nrm.sample(&mut rng) * tau0;
}
out
}
#[test]
fn ensemble_of_three_equal_clocks_beats_each_single() {
let tau0 = 1.0;
let n = 2048;
let c1 = white_fm_clock(1e-11, tau0, n, 1);
let c2 = white_fm_clock(1e-11, tau0, n, 2);
let c3 = white_fm_clock(1e-11, tau0, n, 3);
let ens = ensemble_timescale(&[c1.clone(), c2.clone(), c3.clone()], &[1.0, 1.0, 1.0]);
let a1 = adev_tau0(&c1, tau0);
let a2 = adev_tau0(&c2, tau0);
let a3 = adev_tau0(&c3, tau0);
let ae = adev_tau0(&ens, tau0);
let min_single = a1.min(a2).min(a3);
assert!(
ae < min_single,
"ensemble {ae} not below best single {min_single}"
);
let mean_single = (a1 + a2 + a3) / 3.0;
assert!(
ae < 0.75 * mean_single && ae > 0.40 * mean_single,
"ae {ae} mean {mean_single}"
);
}
#[test]
fn inverse_variance_weighting_beats_the_best_clock() {
let tau0 = 1.0;
let n = 2048;
let (s1, s2, s3) = (1e-11, 2e-11, 4e-11);
let c1 = white_fm_clock(s1, tau0, n, 10);
let c2 = white_fm_clock(s2, tau0, n, 20);
let c3 = white_fm_clock(s3, tau0, n, 30);
let w = [1.0 / (s1 * s1), 1.0 / (s2 * s2), 1.0 / (s3 * s3)];
let ens = ensemble_timescale(&[c1.clone(), c2.clone(), c3.clone()], &w);
let ae = adev_tau0(&ens, tau0);
let best = adev_tau0(&c1, tau0); assert!(ae < best, "ensemble {ae} not below best clock {best}");
}
#[test]
fn ensemble_is_reproducible() {
let c1 = white_fm_clock(1e-11, 1.0, 512, 7);
let c2 = white_fm_clock(1e-11, 1.0, 512, 8);
let a = ensemble_timescale(&[c1.clone(), c2.clone()], &[1.0, 1.0]);
let b = ensemble_timescale(&[c1, c2], &[1.0, 1.0]);
assert_eq!(a, b);
}
#[test]
fn ensemble_weights_are_a_convex_combination() {
let c = vec![1.0, 2.0, 3.0, 4.0];
let ens = ensemble_timescale(&[c.clone(), c.clone(), c.clone()], &[0.2, 0.5, 1.3]);
assert_eq!(ens, c);
}
#[test]
fn single_clock_ensemble_is_the_identity() {
let c = white_fm_clock(1e-11, 1.0, 256, 5);
let ens = ensemble_timescale(std::slice::from_ref(&c), &[3.7]);
assert!(ens
.iter()
.zip(&c)
.all(|(e, x)| (e - x).abs() <= 1e-24 + 1e-12 * x.abs()));
}
}