use super::comb::C0;
use crate::error::OxiPhotonError;
const G: f64 = 9.80665;
#[derive(Debug, Clone)]
pub struct AllanDeviation {
pub tau_values: Vec<f64>,
pub oadev: Vec<f64>,
pub mdev: Vec<f64>,
}
impl AllanDeviation {
pub fn from_fractional_freq(y: &[f64], tau0: f64) -> Self {
let n = y.len();
if n < 2 {
return Self {
tau_values: Vec::new(),
oadev: Vec::new(),
mdev: Vec::new(),
};
}
let mut x = Vec::with_capacity(n + 1);
x.push(0.0_f64);
for &yi in y {
let last = *x.last().expect("x always has at least one element");
x.push(last + yi * tau0);
}
Self::from_phase_internal(&x, tau0)
}
pub fn from_phase_data(x: &[f64], tau0: f64) -> Self {
Self::from_phase_internal(x, tau0)
}
fn from_phase_internal(x: &[f64], tau0: f64) -> Self {
let n = x.len();
if n < 3 {
return Self {
tau_values: Vec::new(),
oadev: Vec::new(),
mdev: Vec::new(),
};
}
let max_m = (n / 4).max(1);
let mut m_values: Vec<usize> = Vec::new();
let mut m = 1_usize;
while m <= max_m {
m_values.push(m);
m = if m == 1 { 2 } else { m * 2 };
}
let mut tau_values = Vec::with_capacity(m_values.len());
let mut oadev = Vec::with_capacity(m_values.len());
let mut mdev_vals = Vec::with_capacity(m_values.len());
for &m in &m_values {
let tau = m as f64 * tau0;
let oadev_val = Self::compute_oadev(x, m);
let mdev_val = Self::compute_mdev(x, m);
tau_values.push(tau);
oadev.push(oadev_val);
mdev_vals.push(mdev_val);
}
Self {
tau_values,
oadev,
mdev: mdev_vals,
}
}
fn compute_oadev(x: &[f64], m: usize) -> f64 {
let n = x.len();
if n <= 2 * m {
return 0.0;
}
let count = n - 2 * m;
let tau0 = 1.0_f64; let tau_m_sq = (m as f64 * tau0).powi(2);
let sum_sq: f64 = (0..count)
.map(|j| {
let diff = x[j + 2 * m] - 2.0 * x[j + m] + x[j];
diff * diff
})
.sum();
let variance = sum_sq / (2.0 * tau_m_sq * count as f64);
variance.sqrt()
}
fn compute_mdev(x: &[f64], m: usize) -> f64 {
let n = x.len();
if n <= 3 * m {
return 0.0;
}
let tau0 = 1.0_f64;
let denom_factor = 2.0 * (m as f64).powi(4) * tau0 * tau0;
let outer_count = n - 3 * m + 1;
if outer_count == 0 {
return 0.0;
}
let sum_sq: f64 = (0..outer_count)
.map(|j| {
let inner: f64 = (j..(j + m))
.map(|i| {
if i + 2 * m < n {
x[i + 2 * m] - 2.0 * x[i + m] + x[i]
} else {
0.0
}
})
.sum();
inner * inner
})
.sum();
let variance = sum_sq / (denom_factor * outer_count as f64);
variance.sqrt()
}
pub fn white_freq_noise(&self) -> Option<f64> {
if self.tau_values.len() < 2 {
return None;
}
let tau1 = self.tau_values[0];
let tau2 = self.tau_values[1];
let s1 = self.oadev[0];
let s2 = self.oadev[1];
if tau1 <= 0.0 || tau2 <= 0.0 || s1 <= 0.0 || s2 <= 0.0 {
return None;
}
let slope = (s2 / s1).log10() / (tau2 / tau1).log10();
if (slope + 0.5).abs() < 0.3 {
Some(2.0 * tau1 * s1 * s1)
} else {
None
}
}
pub fn flicker_floor(&self) -> Option<f64> {
if self.oadev.len() < 3 {
return None;
}
let start = self.oadev.len() / 3;
let end = 2 * self.oadev.len() / 3;
if end <= start {
return None;
}
let segment = &self.oadev[start..end];
let mean: f64 = segment.iter().sum::<f64>() / segment.len() as f64;
let var: f64 =
segment.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / segment.len() as f64;
let rms_variation = var.sqrt() / mean;
if rms_variation < 0.20 {
Some(mean)
} else {
None
}
}
pub fn stability_at(&self, tau: f64) -> Option<f64> {
if self.tau_values.is_empty() || tau <= 0.0 {
return None;
}
for (i, &t) in self.tau_values.iter().enumerate() {
if (t - tau).abs() < tau * 1e-6 {
return Some(self.oadev[i]);
}
}
for i in 0..self.tau_values.len().saturating_sub(1) {
let t1 = self.tau_values[i];
let t2 = self.tau_values[i + 1];
if tau >= t1 && tau <= t2 {
let alpha = (tau.log10() - t1.log10()) / (t2.log10() - t1.log10());
let log_s1 = self.oadev[i].log10();
let log_s2 = self.oadev[i + 1].log10();
let log_s = log_s1 + alpha * (log_s2 - log_s1);
return Some(10_f64.powf(log_s));
}
}
None
}
}
#[derive(Debug, Clone)]
pub struct FiberFrequencyTransfer {
pub fiber_length_km: f64,
pub noise_cancellation: bool,
pub group_velocity_dispersion: f64,
}
impl FiberFrequencyTransfer {
pub fn new(length_km: f64, noise_cancel: bool) -> Self {
Self {
fiber_length_km: length_km,
noise_cancellation: noise_cancel,
group_velocity_dispersion: -21.7e-27, }
}
pub fn fiber_phase_noise_dbc_hz(&self, offset_freq_hz: f64) -> f64 {
if offset_freq_hz <= 0.0 {
return 0.0;
}
let length_m = self.fiber_length_km * 1e3;
let lambda = 1550e-9; let geo_factor = (2.0 * std::f64::consts::PI * length_m / lambda).powi(2);
let s0_m2_hz = 1e-17_f64; let s_dl = if offset_freq_hz < 1e3 {
s0_m2_hz * 1e3 / offset_freq_hz } else {
s0_m2_hz
};
let s_phi_linear = geo_factor * s_dl;
if s_phi_linear <= 0.0 {
return f64::NEG_INFINITY;
}
10.0 * s_phi_linear.log10()
}
pub fn transfer_instability(&self, tau_s: f64) -> f64 {
if tau_s <= 0.0 {
return f64::INFINITY;
}
let _length_m = self.fiber_length_km * 1e3;
let nu0 = C0 / 1550e-9; let f_corner = 1e3_f64;
let s_phi_at_fc = 10_f64.powf(self.fiber_phase_noise_dbc_hz(f_corner) / 10.0);
let rms_phase = (s_phi_at_fc * f_corner).sqrt();
let sigma = rms_phase / (2.0 * std::f64::consts::PI * nu0 * tau_s);
if self.noise_cancellation {
sigma / 1000.0
} else {
sigma
}
}
pub fn noise_floor_after_cancellation(&self, offset_freq_hz: f64) -> f64 {
let raw_db = self.fiber_phase_noise_dbc_hz(offset_freq_hz);
if offset_freq_hz < 1e4 {
raw_db - 60.0 } else {
raw_db
}
}
pub fn max_distance_km(&self) -> f64 {
if self.noise_cancellation {
1000.0 } else {
10.0 }
}
pub fn dispersion_spread_ps(&self, bandwidth_nm: f64) -> f64 {
let length_m = self.fiber_length_km * 1e3;
let lambda0 = 1550e-9; let d_omega = 2.0 * std::f64::consts::PI * C0 / (lambda0 * lambda0) * bandwidth_nm * 1e-9;
let dt_s = self.group_velocity_dispersion.abs() * length_m * d_omega;
dt_s * 1e12 }
}
#[derive(Debug, Clone)]
pub struct ClockComparison {
pub clock_a_freq: f64,
pub clock_b_freq: f64,
pub height_difference_m: f64,
pub fractional_uncertainty: f64,
}
impl ClockComparison {
pub fn new(fa: f64, fb: f64, dh: f64) -> Self {
Self {
clock_a_freq: fa,
clock_b_freq: fb,
height_difference_m: dh,
fractional_uncertainty: 1e-18,
}
}
pub fn gravitational_frequency_difference(&self) -> f64 {
G * self.height_difference_m / (C0 * C0)
}
pub fn height_sensitivity_cm_per_1e18(&self) -> f64 {
let delta_h_m = 1e-18 * C0 * C0 / G;
delta_h_m * 100.0 }
pub fn required_stability_for_geodesy(&self, resolution_cm: f64) -> f64 {
let delta_h_m = resolution_cm * 1e-2; G * delta_h_m / (C0 * C0)
}
pub fn ratio_uncertainty(&self) -> f64 {
if self.clock_b_freq <= 0.0 {
return f64::INFINITY;
}
let ratio = self.clock_a_freq / self.clock_b_freq;
ratio * self.fractional_uncertainty
}
pub fn measured_fractional_difference(&self) -> Result<f64, OxiPhotonError> {
if self.clock_b_freq <= 0.0 {
return Err(OxiPhotonError::NumericalError(
"Clock B frequency must be positive".into(),
));
}
Ok((self.clock_a_freq - self.clock_b_freq) / self.clock_b_freq)
}
pub fn inferred_height_difference_m(&self) -> Result<f64, OxiPhotonError> {
let delta_f_over_f = self.measured_fractional_difference()?;
Ok(delta_f_over_f * C0 * C0 / G)
}
pub fn can_resolve_1cm(&self) -> bool {
self.fractional_uncertainty < self.required_stability_for_geodesy(1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_allan_dev_white_freq_noise_scaling() {
let y = vec![0.0_f64; 64];
let oadev = AllanDeviation::from_fractional_freq(&y, 1.0);
assert!(
!oadev.tau_values.is_empty(),
"should have at least one tau value"
);
for &s in &oadev.oadev {
assert_abs_diff_eq!(s, 0.0, epsilon = 1e-15);
}
}
#[test]
fn test_allan_dev_stability_at_interpolation() {
let y: Vec<f64> = vec![1e-13; 128];
let oadev = AllanDeviation::from_fractional_freq(&y, 1.0);
if oadev.tau_values.len() >= 2 {
let tau0 = oadev.tau_values[0];
let s0 = oadev.oadev[0];
let interp = oadev.stability_at(tau0);
assert!(
interp.is_some(),
"stability_at should return Some at a known tau"
);
assert_abs_diff_eq!(interp.expect("checked above"), s0, epsilon = s0 * 1e-4);
}
}
#[test]
fn test_allan_dev_from_phase_data() {
let x = vec![1e-9_f64; 64]; let oadev = AllanDeviation::from_phase_data(&x, 1.0);
for &s in &oadev.oadev {
assert_abs_diff_eq!(s, 0.0, epsilon = 1e-15);
}
}
#[test]
fn test_allan_dev_tau_values_increasing() {
let y: Vec<f64> = (0..128).map(|i| ((i as f64) * 0.1).sin() * 1e-13).collect();
let oadev = AllanDeviation::from_fractional_freq(&y, 1.0);
for i in 1..oadev.tau_values.len() {
assert!(
oadev.tau_values[i] > oadev.tau_values[i - 1],
"tau values must be strictly increasing"
);
}
}
#[test]
fn test_fiber_phase_noise_decreases_with_frequency() {
let link = FiberFrequencyTransfer::new(100.0, false);
let noise_low = link.fiber_phase_noise_dbc_hz(100.0); let noise_high = link.fiber_phase_noise_dbc_hz(10e3); assert!(
noise_low > noise_high,
"low-f noise {noise_low} dBc/Hz should exceed high-f noise {noise_high} dBc/Hz"
);
}
#[test]
fn test_fiber_anc_improves_instability() {
let link_raw = FiberFrequencyTransfer::new(1000.0, false);
let link_anc = FiberFrequencyTransfer::new(1000.0, true);
let sigma_raw = link_raw.transfer_instability(1.0);
let sigma_anc = link_anc.transfer_instability(1.0);
assert!(
sigma_anc < sigma_raw,
"ANC link σ_y={sigma_anc} must be smaller than raw σ_y={sigma_raw}"
);
}
#[test]
fn test_fiber_noise_cancellation_reduces_noise() {
let link = FiberFrequencyTransfer::new(100.0, true);
let raw_noise = link.fiber_phase_noise_dbc_hz(1e3);
let cancelled = link.noise_floor_after_cancellation(1e3);
assert!(
cancelled < raw_noise,
"cancelled noise {cancelled} dBc/Hz must be lower than raw {raw_noise} dBc/Hz"
);
}
#[test]
fn test_gravitational_frequency_difference_sign() {
let cmp = ClockComparison::new(429e12, 429e12, 100.0);
let delta = cmp.gravitational_frequency_difference();
assert!(
delta > 0.0,
"upward gravitational redshift must be positive: {delta}"
);
}
#[test]
fn test_height_sensitivity_about_1cm_per_1e18() {
let cmp = ClockComparison::new(429e12, 429e12, 0.0);
let h_cm = cmp.height_sensitivity_cm_per_1e18();
assert_abs_diff_eq!(h_cm, 1e-18 * C0 * C0 / G * 100.0, epsilon = 1e-5);
}
#[test]
fn test_required_stability_1cm_geodesy() {
let cmp = ClockComparison::new(429e12, 429e12, 0.0);
let req = cmp.required_stability_for_geodesy(1.0); assert!(
req > 1e-19 && req < 1e-17,
"required stability {req} out of expected range"
);
}
#[test]
fn test_inferred_height_consistent_with_input() {
let delta_f_over_f = G * 100.0 / (C0 * C0);
let fb = 429_228_066_418_012.0_f64;
let fa = fb * (1.0 + delta_f_over_f);
let cmp = ClockComparison::new(fa, fb, 100.0);
let inferred = cmp.inferred_height_difference_m();
assert!(inferred.is_ok(), "inferred height should not error");
let h = inferred.expect("checked above");
assert_abs_diff_eq!(h, 100.0, epsilon = 0.5); }
}