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
}
}
#[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 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);
}
}