use std::f64::consts::PI;
pub const RB87_D2_WAVELENGTH_M: f64 = 780.241_209e-9;
pub fn effective_wavevector(wavelength_m: f64) -> f64 {
4.0 * PI / wavelength_m
}
pub fn mach_zehnder_phase(k_eff: f64, accel: f64, pulse_sep_t: f64) -> f64 {
k_eff * accel * pulse_sep_t * pulse_sep_t
}
pub fn projection_noise_rad(contrast: f64, atom_number: f64) -> f64 {
let c = contrast.clamp(1e-9, 1.0);
let n = atom_number.max(1.0);
1.0 / (c * n.sqrt())
}
pub fn accel_sensitivity_per_shot(sigma_phi: f64, k_eff: f64, pulse_sep_t: f64) -> f64 {
let scale = k_eff * pulse_sep_t * pulse_sep_t;
if scale == 0.0 {
return f64::INFINITY;
}
sigma_phi / scale
}
pub fn accel_transfer_function(omega_rad_s: f64, pulse_sep_t: f64) -> f64 {
let w = omega_rad_s.abs();
if w * pulse_sep_t < 1e-4 {
return pulse_sep_t * pulse_sep_t;
}
let s = (w * pulse_sep_t / 2.0).sin();
4.0 * s * s / (w * w)
}
pub fn vibration_phase_variance_white(k_eff: f64, pulse_sep_t: f64, accel_psd: f64) -> f64 {
k_eff * k_eff * accel_psd * pulse_sep_t.powi(3) / 3.0
}
pub fn vibration_phase_variance_band(
k_eff: f64,
pulse_sep_t: f64,
accel_psd: f64,
f_lo: f64,
f_hi: f64,
n_steps: usize,
) -> f64 {
let n = n_steps.max(1);
let df = (f_hi - f_lo) / n as f64;
let two_pi = 2.0 * PI;
let mut sum = 0.0;
for i in 0..=n {
let f = f_lo + df * i as f64;
let h = accel_transfer_function(two_pi * f, pulse_sep_t);
let w = if i == 0 || i == n { 0.5 } else { 1.0 };
sum += w * h * h;
}
k_eff * k_eff * accel_psd * sum * df
}
pub fn beam_axis_projection(beam_unit: [f64; 3], accel: [f64; 3]) -> f64 {
let norm = (beam_unit[0].powi(2) + beam_unit[1].powi(2) + beam_unit[2].powi(2)).sqrt();
if norm == 0.0 {
return 0.0;
}
(beam_unit[0] * accel[0] + beam_unit[1] * accel[1] + beam_unit[2] * accel[2]) / norm
}
#[derive(Clone, Copy, Debug)]
pub struct CaiAccelerometer {
pub wavelength_m: f64,
pub pulse_sep_t: f64,
pub atom_number: f64,
pub contrast: f64,
pub cycle_time_s: f64,
}
impl CaiAccelerometer {
pub fn k_eff(&self) -> f64 {
effective_wavevector(self.wavelength_m)
}
pub fn scale_factor(&self) -> f64 {
self.k_eff() * self.pulse_sep_t * self.pulse_sep_t
}
pub fn projection_noise_phase(&self) -> f64 {
projection_noise_rad(self.contrast, self.atom_number)
}
pub fn accel_sensitivity_per_shot(&self) -> f64 {
accel_sensitivity_per_shot(
self.projection_noise_phase(),
self.k_eff(),
self.pulse_sep_t,
)
}
pub fn accel_asd(&self) -> f64 {
self.accel_sensitivity_per_shot() * self.cycle_time_s.max(0.0).sqrt()
}
pub fn q_va(&self) -> f64 {
let n = self.accel_asd();
n * n
}
pub fn contrast_at(&self, t_s: f64, tau_contrast_s: f64) -> f64 {
if tau_contrast_s <= 0.0 {
return self.contrast;
}
self.contrast * (-t_s / tau_contrast_s).exp()
}
pub fn vibration_phase_noise(&self, accel_psd: f64) -> f64 {
vibration_phase_variance_white(self.k_eff(), self.pulse_sep_t, accel_psd).sqrt()
}
pub fn vibration_limited_accel(&self, accel_psd: f64) -> f64 {
let scale = self.scale_factor();
if scale == 0.0 {
return f64::INFINITY;
}
self.vibration_phase_noise(accel_psd) / scale
}
}
pub fn coriolis_phase(k_eff: f64, v_perp: f64, omega_rot: f64, pulse_sep_t: f64) -> f64 {
2.0 * k_eff * v_perp * omega_rot * pulse_sep_t * pulse_sep_t
}
pub fn coriolis_accel_bias(v_perp: f64, omega_rot: f64) -> f64 {
2.0 * v_perp * omega_rot
}
pub fn ac_stark_phase(delta_ls_first: f64, delta_ls_third: f64, rabi_eff: f64) -> f64 {
if rabi_eff == 0.0 {
return 0.0;
}
(delta_ls_first - delta_ls_third) / rabi_eff
}
#[derive(Clone, Copy, Debug)]
pub struct DriftSweepPoint {
pub cycle_time_s: f64,
pub q_va: f64,
pub pos_drift_m: f64,
}
pub fn cai_drift_sweep(
base: CaiAccelerometer,
cycle_times_s: &[f64],
nav_duration_s: f64,
) -> Vec<DriftSweepPoint> {
cycle_times_s
.iter()
.map(|&tc| {
let cai = CaiAccelerometer {
cycle_time_s: tc,
..base
};
let q = cai.q_va();
let drift = (q * nav_duration_s.powi(3) / 3.0).sqrt();
DriftSweepPoint {
cycle_time_s: tc,
q_va: q,
pos_drift_m: drift,
}
})
.collect()
}
#[derive(Clone, Copy, Debug)]
pub struct QuantumNavBudget {
pub cai: CaiAccelerometer,
pub bias_m_s2: f64,
pub scale_factor_ppm: f64,
pub ref_accel_m_s2: f64,
pub tau_stability_s: f64,
}
impl QuantumNavBudget {
pub fn bias_drift_m(&self, t: f64) -> f64 {
0.5 * self.bias_m_s2 * t * t
}
pub fn scale_factor_drift_m(&self, t: f64) -> f64 {
0.5 * (self.scale_factor_ppm * 1.0e-6) * self.ref_accel_m_s2 * t * t
}
pub fn vrw_position_variance(&self, t: f64) -> f64 {
let q0 = self.cai.q_va();
if self.tau_stability_s <= 0.0 || t <= 0.0 {
return q0 * t.max(0.0).powi(3) / 3.0;
}
let n = 2000usize;
let h = t / n as f64;
let f = |u: f64| (t - u) * (t - u) * q0 * (2.0 * u / self.tau_stability_s).exp();
let mut s = f(0.0) + f(t);
for i in 1..n {
let u = i as f64 * h;
s += if i % 2 == 1 { 4.0 } else { 2.0 } * f(u);
}
s * h / 3.0
}
pub fn vrw_drift_m(&self, t: f64) -> f64 {
self.vrw_position_variance(t).sqrt()
}
pub fn position_drift_1sigma(&self, t: f64) -> f64 {
let b = self.bias_drift_m(t);
let sf = self.scale_factor_drift_m(t);
let v2 = self.vrw_position_variance(t);
(b * b + sf * sf + v2).sqrt()
}
pub fn holdover_seconds(&self, threshold_m: f64) -> f64 {
if threshold_m <= 0.0 {
return 0.0;
}
let drift = |t: f64| self.position_drift_1sigma(t);
if drift(1.0) == 0.0 {
return f64::INFINITY;
}
let mut hi = 1.0_f64;
let mut guard = 0;
while drift(hi) < threshold_m {
hi *= 2.0;
guard += 1;
if guard > 200 {
return f64::INFINITY;
}
}
let mut lo = 0.0_f64;
for _ in 0..100 {
let mid = 0.5 * (lo + hi);
if drift(mid) < threshold_m {
lo = mid;
} else {
hi = mid;
}
}
0.5 * (lo + hi)
}
}
#[cfg(test)]
mod budget_tests {
use super::*;
use crate::inertial::AccelModel;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
fn ref_cai() -> CaiAccelerometer {
CaiAccelerometer {
wavelength_m: RB87_D2_WAVELENGTH_M,
pulse_sep_t: 0.05,
atom_number: 1.0e6,
contrast: 0.5,
cycle_time_s: 0.5,
}
}
fn ref_budget(bias: f64, ppm: f64, a_ref: f64, tau_s: f64) -> QuantumNavBudget {
QuantumNavBudget {
cai: ref_cai(),
bias_m_s2: bias,
scale_factor_ppm: ppm,
ref_accel_m_s2: a_ref,
tau_stability_s: tau_s,
}
}
#[test]
fn bias_drift_matches_accel_model_double_integration() {
let b = 1.0e-5; let budget = ref_budget(b, 0.0, 0.0, 0.0);
let mut am = AccelModel::new("t", "test", b, 0.0);
let mut rng = ChaCha8Rng::seed_from_u64(1);
let t_total = 600.0;
let dt = 0.01;
let steps = (t_total / dt) as usize;
for _ in 0..steps {
am.step(dt, &mut rng);
}
let closed = budget.bias_drift_m(t_total); let stepped = am.pos();
let rel = (closed - stepped).abs() / closed;
assert!(
rel < 1e-3,
"closed {closed:.4} vs AccelModel {stepped:.4}, rel {rel:.2e}"
);
}
#[test]
fn vrw_matches_closed_form_constant_q_va() {
let budget = ref_budget(0.0, 0.0, 0.0, 0.0);
let t = 300.0;
let var = budget.vrw_position_variance(t);
let closed = budget.cai.q_va() * t.powi(3) / 3.0;
let rel = (var - closed).abs() / closed;
assert!(rel < 1e-12, "vrw var {var:.6e} vs q_va t³/3 {closed:.6e}");
}
#[test]
fn vrw_stability_decay_matches_analytic_integral() {
let budget = ref_budget(0.0, 0.0, 0.0, 200.0);
let t = 100.0;
let q0 = budget.cai.q_va();
let a: f64 = 2.0 / 200.0; let analytic = q0
* ((a * t).exp() * 2.0 / a.powi(3) - (t * t / a + 2.0 * t / (a * a) + 2.0 / a.powi(3)));
let numeric = budget.vrw_position_variance(t);
let rel = (numeric - analytic).abs() / analytic;
assert!(
rel < 1e-6,
"Simpson {numeric:.6e} vs analytic {analytic:.6e}, rel {rel:.2e}"
);
}
#[test]
fn stability_decay_increases_drift_and_limits_correctly() {
let no_decay = ref_budget(0.0, 0.0, 0.0, 0.0);
let decay = QuantumNavBudget {
tau_stability_s: 60.0,
..no_decay
};
let t = 300.0;
assert!(
decay.vrw_drift_m(t) > no_decay.vrw_drift_m(t),
"stability decay should inflate drift"
);
let huge = QuantumNavBudget {
tau_stability_s: 1.0e12,
..no_decay
};
let rel = (huge.vrw_position_variance(t) - no_decay.vrw_position_variance(t)).abs()
/ no_decay.vrw_position_variance(t);
assert!(
rel < 1e-3,
"τ_s→∞ should recover constant-q_va form, rel {rel:.2e}"
);
}
#[test]
fn scale_factor_term_quadratic_and_zero_in_free_fall() {
let budget = ref_budget(0.0, 100.0, 9.806_65, 0.0); let s100 = budget.scale_factor_drift_m(100.0);
let s200 = budget.scale_factor_drift_m(200.0);
assert!((s200 / s100 - 4.0).abs() < 1e-9, "should be quadratic in t");
let closed = 0.5 * 100e-6 * 9.806_65 * 100.0 * 100.0;
assert!((s100 - closed).abs() / closed < 1e-12);
let free_fall = QuantumNavBudget {
ref_accel_m_s2: 0.0,
..budget
};
assert_eq!(free_fall.scale_factor_drift_m(100.0), 0.0);
}
#[test]
fn total_combines_terms_against_hand_values() {
let budget = ref_budget(1.0e-5, 100.0, 2.0, 0.0);
let t = 200.0;
let bias_hand: f64 = 0.5 * 1.0e-5 * 200.0 * 200.0; let sf_hand: f64 = 0.5 * 100.0e-6 * 2.0 * 200.0 * 200.0; assert!((bias_hand - 0.2).abs() < 1e-12, "bias hand value");
assert!((sf_hand - 4.0).abs() < 1e-12, "sf hand value");
let v2 = budget.vrw_position_variance(t); let expected = (bias_hand * bias_hand + sf_hand * sf_hand + v2).sqrt();
let got = budget.position_drift_1sigma(t);
assert!(
(got - expected).abs() / expected < 1e-12,
"total {got} vs hand-composed {expected}"
);
}
#[test]
fn inertial_holdover_round_trips() {
let budget = ref_budget(1.0e-5, 0.0, 0.0, 0.0);
let threshold = 50.0; let t = budget.holdover_seconds(threshold);
let drift = budget.position_drift_1sigma(t);
assert!(
(drift - threshold).abs() / threshold < 1e-6,
"drift {drift} vs {threshold}"
);
let expect = (2.0 * threshold / 1.0e-5).sqrt();
assert!(
t < expect && t > 0.95 * expect,
"t {t:.1} should be just under √(2thr/b) {expect:.1}"
);
}
#[test]
fn free_falling_low_noise_budget_holds_over_effectively_forever() {
let budget = QuantumNavBudget {
cai: CaiAccelerometer {
atom_number: 1.0e30, ..ref_cai()
},
bias_m_s2: 0.0,
scale_factor_ppm: 1000.0,
ref_accel_m_s2: 0.0, tau_stability_s: 0.0,
};
let t = budget.holdover_seconds(10.0);
assert!(
t > 1.0e9,
"near-noiseless free-falling holdover {t:.2e} s should exceed any mission timescale"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn effective_wavevector_for_rubidium() {
let k = effective_wavevector(RB87_D2_WAVELENGTH_M);
assert!((k - 1.610_6e7).abs() / 1.610_6e7 < 1e-3, "k_eff = {k}");
}
#[test]
fn mach_zehnder_phase_is_k_a_t_squared() {
let phi = mach_zehnder_phase(1.610_6e7, 9.81, 0.01);
assert!((phi - 1.580e4).abs() / 1.580e4 < 1e-3, "Φ = {phi}");
let phi2 = mach_zehnder_phase(1.610_6e7, 9.81, 0.02);
assert!(
(phi2 / phi - 4.0).abs() < 1e-9,
"T² scaling broken: {}",
phi2 / phi
);
}
#[test]
fn projection_noise_is_inverse_contrast_root_n() {
let s = projection_noise_rad(0.5, 1e6);
assert!((s - 2e-3).abs() < 1e-12, "σ_Φ = {s}");
let s2 = projection_noise_rad(0.5, 1e8);
assert!(
(s / s2 - 10.0).abs() < 1e-9,
"1/√N scaling broken: {}",
s / s2
);
}
#[test]
fn per_shot_sensitivity_and_q_va_match_hand_computation() {
let cai = CaiAccelerometer {
wavelength_m: RB87_D2_WAVELENGTH_M,
pulse_sep_t: 0.01,
atom_number: 1e6,
contrast: 0.5,
cycle_time_s: 0.5,
};
let sigma_a = cai.accel_sensitivity_per_shot();
assert!(
(sigma_a - 1.2418e-6).abs() / 1.2418e-6 < 2e-3,
"σ_a = {sigma_a}"
);
let n_a = cai.accel_asd();
assert!((n_a - 8.78e-7).abs() / 8.78e-7 < 3e-3, "n_a = {n_a}");
assert!((cai.q_va() - n_a * n_a).abs() < 1e-30);
assert!(n_a < 1e-6, "shot-noise floor should be sub-µg/√Hz: {n_a}");
}
#[test]
fn longer_interrogation_and_more_atoms_improve_sensitivity() {
let base = CaiAccelerometer {
wavelength_m: RB87_D2_WAVELENGTH_M,
pulse_sep_t: 0.01,
atom_number: 1e6,
contrast: 0.5,
cycle_time_s: 0.5,
};
let mut long_t = base;
long_t.pulse_sep_t = 0.1;
assert!(
(base.accel_sensitivity_per_shot() / long_t.accel_sensitivity_per_shot() - 100.0).abs()
< 1e-6
);
let mut more_n = base;
more_n.atom_number = 1e8;
assert!(
(base.accel_sensitivity_per_shot() / more_n.accel_sensitivity_per_shot() - 10.0).abs()
< 1e-6
);
}
#[test]
fn contrast_decays_exponentially() {
let cai = CaiAccelerometer {
wavelength_m: RB87_D2_WAVELENGTH_M,
pulse_sep_t: 0.01,
atom_number: 1e6,
contrast: 0.8,
cycle_time_s: 0.5,
};
assert!((cai.contrast_at(0.0, 1.0) - 0.8).abs() < 1e-12);
assert!((cai.contrast_at(1.0, 1.0) - 0.8 / std::f64::consts::E).abs() < 1e-12);
assert!((cai.contrast_at(5.0, 0.0) - 0.8).abs() < 1e-12);
}
#[test]
fn transfer_function_reduces_to_t_squared_at_dc() {
let h0 = accel_transfer_function(1e-3, 0.01);
assert!((h0 - 1e-4).abs() < 1e-15, "H(0) should be T² = 1e-4: {h0}");
let w = PI / 0.01;
let h = accel_transfer_function(w, 0.01);
assert!((h - 4.0528e-5).abs() / 4.0528e-5 < 1e-3, "|H(π/T)| = {h}");
}
#[test]
fn white_vibration_variance_matches_closed_form() {
let k = effective_wavevector(RB87_D2_WAVELENGTH_M);
let var = vibration_phase_variance_white(k, 0.01, 1e-10);
assert!((var - 8.6465e-3).abs() / 8.6465e-3 < 1e-3, "σ_Φ² = {var}");
let cai = CaiAccelerometer {
wavelength_m: RB87_D2_WAVELENGTH_M,
pulse_sep_t: 0.01,
atom_number: 1e6,
contrast: 0.5,
cycle_time_s: 0.5,
};
let sigma = cai.vibration_phase_noise(1e-10);
assert!((sigma - 0.092987).abs() / 0.092987 < 1e-3, "σ_Φ = {sigma}");
let var2 = vibration_phase_variance_white(k, 0.02, 1e-10);
assert!(
(var2 / var - 8.0).abs() < 1e-9,
"T³ scaling broken: {}",
var2 / var
);
}
#[test]
fn band_integral_converges_to_closed_form() {
let k = effective_wavevector(RB87_D2_WAVELENGTH_M);
let closed = vibration_phase_variance_white(k, 0.01, 1e-10);
let numeric = vibration_phase_variance_band(k, 0.01, 1e-10, 0.0, 5000.0, 20_000);
assert!(
(numeric - closed).abs() / closed < 0.02,
"numeric {numeric} vs closed {closed}"
);
}
#[test]
fn vibration_floor_is_wavelength_independent_and_dominates_shot_noise() {
let rb = CaiAccelerometer {
wavelength_m: RB87_D2_WAVELENGTH_M,
pulse_sep_t: 0.01,
atom_number: 1e6,
contrast: 0.5,
cycle_time_s: 0.5,
};
let a_vib = rb.vibration_limited_accel(1e-10);
assert!(
(a_vib - 5.7735e-5).abs() / 5.7735e-5 < 1e-3,
"σ_a,vib = {a_vib}"
);
let cs = CaiAccelerometer {
wavelength_m: 852.0e-9,
..rb
};
assert!(
(rb.vibration_limited_accel(1e-10) - cs.vibration_limited_accel(1e-10)).abs() / a_vib
< 1e-12,
"vibration floor must be wavelength-independent"
);
assert!(
a_vib > 10.0 * rb.accel_sensitivity_per_shot(),
"vibration ({a_vib}) should dominate shot noise ({})",
rb.accel_sensitivity_per_shot()
);
}
#[test]
fn coriolis_phase_is_two_k_v_omega_t_squared_and_maps_to_2vomega() {
let k = effective_wavevector(RB87_D2_WAVELENGTH_M);
let phi = coriolis_phase(k, 3.0, 1e-3, 0.05);
assert!((phi - 241.59).abs() / 241.59 < 1e-3, "Φ_cor = {phi}");
assert!((coriolis_phase(k, 3.0, 1e-3, 0.10) / phi - 4.0).abs() < 1e-9);
assert!((coriolis_phase(k, 6.0, 1e-3, 0.05) / phi - 2.0).abs() < 1e-9);
let a_bias = coriolis_accel_bias(3.0, 1e-3);
assert!(
(a_bias - 2.0 * 3.0 * 1e-3).abs() < 1e-15,
"a_cor = {a_bias}"
);
assert!((phi / (k * 0.05 * 0.05) - a_bias).abs() / a_bias < 1e-9);
let earth = coriolis_accel_bias(0.1, 7.292e-5);
assert!(
(earth - 1.4584e-5).abs() / 1.4584e-5 < 1e-3,
"Earth-rate bias = {earth}"
);
}
#[test]
fn ac_stark_phase_cancels_when_symmetric() {
let rabi = 2.0 * PI * 10_000.0; assert!(ac_stark_phase(2.0 * PI * 150.0, 2.0 * PI * 150.0, rabi).abs() < 1e-15);
let phi = ac_stark_phase(2.0 * PI * 200.0, 2.0 * PI * 150.0, rabi);
assert!((phi - 5e-3).abs() < 1e-9, "Φ_LS = {phi}");
assert_eq!(ac_stark_phase(1.0, 0.0, 0.0), 0.0);
}
#[test]
fn cai_drift_grows_with_cycle_time_and_duration() {
let base = CaiAccelerometer {
wavelength_m: RB87_D2_WAVELENGTH_M,
pulse_sep_t: 0.01,
atom_number: 1e6,
contrast: 0.5,
cycle_time_s: 0.5,
};
let sweep = cai_drift_sweep(base, &[0.1, 1.0, 10.0], 600.0);
assert_eq!(sweep.len(), 3);
assert!(sweep[0].pos_drift_m < sweep[1].pos_drift_m);
assert!(sweep[1].pos_drift_m < sweep[2].pos_drift_m);
let short = cai_drift_sweep(base, &[1.0], 600.0)[0].pos_drift_m;
let long = cai_drift_sweep(base, &[1.0], 2400.0)[0].pos_drift_m;
assert!(
(long / short - 8.0).abs() < 1e-6,
"t^1.5 scaling: {}",
long / short
);
assert!(sweep.iter().all(|p| p.pos_drift_m > 0.0 && p.q_va > 0.0));
}
#[test]
fn freier_2016_mobile_gravimeter_quantum_floor_is_below_achieved() {
let gain = CaiAccelerometer {
wavelength_m: RB87_D2_WAVELENGTH_M,
pulse_sep_t: 0.26,
atom_number: 1e6,
contrast: 0.6,
cycle_time_s: 1.0,
};
let achieved = 96e-9; let sql = gain.accel_asd(); assert!(
sql < achieved,
"SQL floor {sql} must be below achieved {achieved}"
);
assert!(
achieved < 200.0 * sql,
"achieved {achieved} implausibly far above the SQL floor {sql}"
);
}
#[test]
fn beam_axis_projection_takes_the_along_axis_component() {
assert!((beam_axis_projection([1.0, 0.0, 0.0], [9.81, 1.0, 2.0]) - 9.81).abs() < 1e-12);
assert!((beam_axis_projection([0.0, 0.0, 2.0], [1.0, 2.0, 3.0]) - 3.0).abs() < 1e-12);
assert!(beam_axis_projection([1.0, 0.0, 0.0], [0.0, 5.0, 0.0]).abs() < 1e-12);
}
}