use crate::portable_math::PortableFloat;
use std::f64::consts::PI;
pub const SPEED_OF_LIGHT_M_S: f64 = 299_792_458.0;
pub const BOLTZMANN_J_PER_K: f64 = 1.380_649e-23;
pub const RB87_MASS_KG: f64 = 1.443_160_648e-25;
pub const DEFAULT_PI2_PULSE_S: f64 = 10.0e-6;
pub fn rabi_frequency_for_pi2_pulse(tau_r_s: f64) -> f64 {
PI / (2.0 * tau_r_s)
}
pub fn raman_phase_transfer_function(omega_rad_s: f64, pulse_sep_t: f64, tau_r_s: f64) -> f64 {
let w = omega_rad_s.abs();
if tau_r_s <= 0.0 {
let s = (w * pulse_sep_t / 2.0).psin();
return 4.0 * s * s;
}
let om = rabi_frequency_for_pi2_pulse(tau_r_s);
let w = if (w - om).abs() < 1e-7 * om {
om * (1.0 + 1e-6)
} else {
w
};
let a = w * (pulse_sep_t + 2.0 * tau_r_s) / 2.0;
let h = 4.0 * om * a.psin() * (w * a.pcos() + om * (w * pulse_sep_t / 2.0).psin())
/ (w * w - om * om);
h.abs()
}
pub fn raman_phase_variance_white(s_phi0_rad2_per_hz: f64, tau_r_s: f64) -> f64 {
if tau_r_s <= 0.0 {
return 0.0;
}
(PI / 2.0).ppowi(2) * s_phi0_rad2_per_hz / tau_r_s
}
pub fn raman_phase_variance_numeric(
pulse_sep_t: f64,
tau_r_s: f64,
psd: &dyn Fn(f64) -> f64,
f_hi: f64,
n_steps: usize,
) -> f64 {
let n = n_steps.max(1);
let df = f_hi / n as f64;
let mut sum = 0.0;
for i in 0..=n {
let f = df * i as f64;
let h = raman_phase_transfer_function(2.0 * PI * f, pulse_sep_t, tau_r_s);
let w = if i == 0 || i == n { 0.5 } else { 1.0 };
sum += w * h * h * psd(f);
}
sum * df
}
pub fn short_pulse_white_band_variance(s_phi0_rad2_per_hz: f64, pulse_sep_t: f64, f_c: f64) -> f64 {
let t = pulse_sep_t;
let x = 2.0 * PI * f_c * t;
s_phi0_rad2_per_hz * (6.0 * f_c - 4.0 * x.psin() / (PI * t) + (2.0 * x).psin() / (2.0 * PI * t))
}
pub fn retro_delay_s(retro_distance_m: f64) -> f64 {
2.0 * retro_distance_m / SPEED_OF_LIGHT_M_S
}
pub fn delay_filter(f_hz: f64, delay_s: f64) -> f64 {
let s = (PI * f_hz * delay_s).psin();
4.0 * s * s
}
pub fn delay_frequency_noise_variance_white(
s_nu0_hz2_per_hz: f64,
delay_s: f64,
tau_r_s: f64,
) -> f64 {
if tau_r_s <= 0.0 {
return 0.0;
}
(2.0 * PI * delay_s).ppowi(2) * s_nu0_hz2_per_hz * PI * PI / (4.0 * tau_r_s)
}
pub fn delay_frequency_noise_variance_numeric(
pulse_sep_t: f64,
tau_r_s: f64,
delay_s: f64,
s_nu: &dyn Fn(f64) -> f64,
f_hi: f64,
n_steps: usize,
) -> f64 {
let n = n_steps.max(1);
let df = f_hi / n as f64;
let mut sum = 0.0;
for i in 0..=n {
let f = df * i as f64;
let h = raman_phase_transfer_function(2.0 * PI * f, pulse_sep_t, tau_r_s);
let filt = if f == 0.0 {
(2.0 * PI * delay_s).ppowi(2)
} else {
delay_filter(f, delay_s) / (f * f)
};
let w = if i == 0 || i == n { 0.5 } else { 1.0 };
sum += w * h * h * filt * s_nu(f);
}
sum * df
}
pub fn thermal_velocity_variance(temperature_k: f64, mass_kg: f64) -> f64 {
BOLTZMANN_J_PER_K * temperature_k.max(0.0) / mass_kg
}
pub fn wavefront_phase_shift(
beam_phase: &dyn Fn(f64, f64) -> f64,
r0: [f64; 2],
v: [f64; 2],
t_first_pulse_s: f64,
pulse_sep_t: f64,
) -> f64 {
let at = |t: f64| beam_phase(r0[0] + v[0] * t, r0[1] + v[1] * t);
let t1 = t_first_pulse_s;
at(t1) - 2.0 * at(t1 + pulse_sep_t) + at(t1 + 2.0 * pulse_sep_t)
}
pub fn defocus_beam_phase(k_eff: f64, radius_m: f64) -> impl Fn(f64, f64) -> f64 {
move |x: f64, y: f64| 0.5 * k_eff * (x * x + y * y) / (2.0 * radius_m)
}
pub fn curvature_bias_accel(sigma_v2: f64, radius_m: f64) -> f64 {
if radius_m == 0.0 || !radius_m.is_finite() {
return 0.0;
}
sigma_v2 / radius_m
}
pub fn detected_transverse_v2(
sigma_v2: f64,
cloud_radius_m: f64,
detection_radius_m: f64,
detection_time_s: f64,
) -> f64 {
let full = 2.0 * sigma_v2;
if !detection_radius_m.is_finite() || detection_radius_m <= 0.0 {
return full;
}
let sv_t2 = sigma_v2 * detection_time_s * detection_time_s;
let sd2 = cloud_radius_m * cloud_radius_m + sv_t2;
if sd2 <= 0.0 {
return full;
}
let rho2 = sv_t2 / sd2;
let u = detection_radius_m * detection_radius_m / (2.0 * sd2);
let h = if u > 700.0 {
1.0
} else {
1.0 - u / u.pexp_m1()
};
full * (1.0 - rho2 * (1.0 - h))
}
pub fn curvature_bias_with_detection(
sigma_v2: f64,
cloud_radius_m: f64,
detection_radius_m: f64,
detection_time_s: f64,
radius_m: f64,
) -> f64 {
if radius_m == 0.0 || !radius_m.is_finite() {
return 0.0;
}
detected_transverse_v2(
sigma_v2,
cloud_radius_m,
detection_radius_m,
detection_time_s,
) / (2.0 * radius_m)
}
pub fn zernike_defocus_to_radius(c_m: f64, pupil_radius_m: f64) -> f64 {
if c_m == 0.0 {
return f64::INFINITY;
}
pupil_radius_m * pupil_radius_m / (4.0 * 3f64.sqrt() * c_m)
}
#[derive(Clone, Copy, Debug)]
pub struct CloudGeometry {
pub sigma_v2: f64,
pub cloud_radius_m: f64,
pub t_first_pulse_s: f64,
pub pulse_sep_t: f64,
pub detection_time_s: f64,
pub detection_radius_m: f64,
}
pub fn wavefront_bias_monte_carlo(
beam_phase: &dyn Fn(f64, f64) -> f64,
k_eff: f64,
cloud: &CloudGeometry,
n_atoms: usize,
seed: u64,
) -> (f64, f64) {
use crate::portable_distr::{Distribution, Normal};
use rand::SeedableRng;
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(seed);
let pos = Normal::new(0.0, cloud.cloud_radius_m.max(0.0)).expect("finite cloud radius");
let vel = Normal::new(0.0, cloud.sigma_v2.max(0.0).sqrt()).expect("finite sigma_v");
let r_det2 = cloud.detection_radius_m * cloud.detection_radius_m;
let mut sum = 0.0;
let mut kept = 0usize;
for _ in 0..n_atoms {
let r0 = [pos.sample(&mut rng), pos.sample(&mut rng)];
let v = [vel.sample(&mut rng), vel.sample(&mut rng)];
let xd = r0[0] + v[0] * cloud.detection_time_s;
let yd = r0[1] + v[1] * cloud.detection_time_s;
if xd * xd + yd * yd > r_det2 {
continue;
}
kept += 1;
sum += wavefront_phase_shift(beam_phase, r0, v, cloud.t_first_pulse_s, cloud.pulse_sep_t);
}
if kept == 0 {
return (0.0, 0.0);
}
let scale = k_eff * cloud.pulse_sep_t * cloud.pulse_sep_t;
(
sum / kept as f64 / scale,
kept as f64 / n_atoms.max(1) as f64,
)
}
pub fn wrong_index_probability(s_rad: f64) -> f64 {
if s_rad <= 0.0 {
return 0.0;
}
libm::erfc(PI / (std::f64::consts::SQRT_2 * s_rad))
}
pub fn ambiguity_failure_probability(
k_eff: f64,
pulse_sep_t: f64,
sigma_classical_m_s2: f64,
sigma_phi_rad: f64,
) -> f64 {
let pred = k_eff * pulse_sep_t * pulse_sep_t * sigma_classical_m_s2;
wrong_index_probability(pred.phypot(sigma_phi_rad))
}
pub fn resolve_with_classical(
k_eff: f64,
pulse_sep_t: f64,
a_classical_m_s2: f64,
phi_wrapped: f64,
) -> (i64, f64) {
let scale = k_eff * pulse_sep_t * pulse_sep_t;
let n = ((scale * a_classical_m_s2 - phi_wrapped) / (2.0 * PI)).round();
(n as i64, (phi_wrapped + 2.0 * PI * n) / scale)
}
pub fn max_classical_sigma_for_failure(
k_eff: f64,
pulse_sep_t: f64,
sigma_phi_rad: f64,
p_target: f64,
) -> f64 {
if p_target <= 0.0 {
return 0.0;
}
let (mut lo, mut hi) = (0.0_f64, 100.0_f64);
for _ in 0..200 {
let mid = 0.5 * (lo + hi);
if wrong_index_probability(mid) > p_target {
hi = mid;
} else {
lo = mid;
}
}
let s = lo;
if s <= sigma_phi_rad {
return 0.0;
}
(s * s - sigma_phi_rad * sigma_phi_rad).sqrt() / (k_eff * pulse_sep_t * pulse_sep_t)
}
pub fn multi_t_failure_probability(
t_short: f64,
t_long: f64,
sigma_phi_short: f64,
sigma_phi_long: f64,
) -> f64 {
let ratio = (t_long / t_short).ppowi(2);
wrong_index_probability((ratio * sigma_phi_short).phypot(sigma_phi_long))
}
pub fn resolve_multi_t(
k_eff: f64,
t_short: f64,
t_long: f64,
phi_short_wrapped: f64,
phi_long_wrapped: f64,
) -> (i64, f64) {
let ratio = (t_long / t_short).ppowi(2);
let n = ((ratio * phi_short_wrapped - phi_long_wrapped) / (2.0 * PI)).round();
(
n as i64,
(phi_long_wrapped + 2.0 * PI * n) / (k_eff * t_long * t_long),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::inertial::quantum_imu::{
accel_transfer_function, effective_wavevector, mach_zehnder_phase, max_unambiguous_accel,
wrap_phase, RB87_D2_WAVELENGTH_M,
};
use crate::portable_distr::{Distribution, Normal};
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
#[test]
fn raman_transfer_short_pulse_limit_matches_the_acceleration_transfer_function() {
let t = 0.01;
for f in [3.0, 17.0, 55.5, 120.0] {
let w = 2.0 * PI * f;
let hp = raman_phase_transfer_function(w, t, 0.0);
let ha = accel_transfer_function(w, t);
assert!((hp - w * w * ha).abs() < 1e-9 * hp.max(1e-12), "f={f}");
let tau_r = 1e-6;
let finite = raman_phase_transfer_function(w, t, tau_r);
let cot = 1.0 / (w * t / 2.0).ptan();
assert!(
(finite - hp).abs() < 4.0 * w * tau_r * (1.0 + cot.abs()) * hp + 1e-9,
"f={f}: {finite} vs {hp}"
);
}
let tau = 20e-6;
let fk = 3.0 / (t + 2.0 * tau);
assert!(raman_phase_transfer_function(2.0 * PI * fk, t, tau) < 1e-9);
let om = rabi_frequency_for_pi2_pulse(tau);
assert!(raman_phase_transfer_function(om, t, tau).is_finite());
assert_eq!(raman_phase_transfer_function(0.0, t, tau), 0.0);
}
#[test]
fn white_raman_phase_closed_form_matches_quadrature() {
let (t, tau, s0) = (1e-3, 20e-6, 1e-11);
let f_hi = 2.0e6;
let numeric = raman_phase_variance_numeric(t, tau, &|_| s0, f_hi, 2_000_000);
let om = rabi_frequency_for_pi2_pulse(tau);
let tail = s0 * om * om / (2.0 * PI * PI * f_hi);
let closed = raman_phase_variance_white(s0, tau);
let rel = ((numeric + tail) - closed).abs() / closed;
assert!(
rel < 2e-3,
"quadrature {numeric:.4e}+{tail:.2e} vs closed {closed:.4e}"
);
let numeric2 = raman_phase_variance_numeric(2e-3, tau, &|_| s0, f_hi, 2_000_000);
assert!(((numeric2 + tail) - closed).abs() / closed < 2e-3);
}
#[test]
fn short_pulse_band_limited_closed_form_matches_quadrature() {
let (t, s0, fc) = (1e-3, 1e-12, 10_000.0);
let numeric = raman_phase_variance_numeric(t, 0.0, &|_| s0, fc, 200_000);
let closed = short_pulse_white_band_variance(s0, t, fc);
assert!(
(numeric - closed).abs() / closed < 1e-6,
"{numeric} vs {closed}"
);
assert!((closed / (6.0 * s0 * fc) - 1.0).abs() < 1e-2);
}
#[test]
fn cheinet_2008_white_phase_noise_requirement_is_reproduced() {
let s0 = 10f64.ppowf(-111.0 / 10.0);
let sigma = raman_phase_variance_white(s0, 20e-6).sqrt();
assert!((sigma - 1e-3).abs() / 1e-3 < 0.02, "σ_Φ = {sigma:.4e} rad");
}
#[test]
fn delay_term_closed_form_matches_exact_filter_quadrature() {
let (t, tau) = (1e-3, 20e-6);
let td = retro_delay_s(0.465); assert!((td - 3.102e-9).abs() < 1e-11);
let h0 = 1.0e3; let f_hi = 2.0e6;
let numeric = delay_frequency_noise_variance_numeric(t, tau, td, &|_| h0, f_hi, 2_000_000);
let om = rabi_frequency_for_pi2_pulse(tau);
let tail = (2.0 * PI * td).ppowi(2) * h0 * om * om / (2.0 * PI * PI * f_hi);
let closed = delay_frequency_noise_variance_white(h0, td, tau);
let rel = ((numeric + tail) - closed).abs() / closed;
assert!(rel < 3e-3, "{numeric:.4e} vs {closed:.4e}");
assert_eq!(delay_frequency_noise_variance_white(h0, 0.0, tau), 0.0);
assert_eq!(delay_filter(0.0, td), 0.0);
let c2 = delay_frequency_noise_variance_white(h0, retro_delay_s(0.93), tau);
assert!((c2 / closed - 4.0).abs() < 1e-9);
}
fn cloud(temp_k: f64, r_cloud: f64, r_det: f64) -> CloudGeometry {
CloudGeometry {
sigma_v2: thermal_velocity_variance(temp_k, RB87_MASS_KG),
cloud_radius_m: r_cloud,
t_first_pulse_s: 0.13,
pulse_sep_t: 0.26,
detection_time_s: 0.13 + 2.0 * 0.26 + 0.128,
detection_radius_m: r_det,
}
}
#[test]
fn defocus_phase_is_position_independent_and_tilt_cancels() {
let k = effective_wavevector(RB87_D2_WAVELENGTH_M);
let r = 1000.0;
let phase = defocus_beam_phase(k, r);
let v = [0.012, -0.007];
let expect = k * (v[0] * v[0] + v[1] * v[1]) * 0.26 * 0.26 / (2.0 * r);
for r0 in [[0.0, 0.0], [0.003, -0.002], [-0.01, 0.004]] {
let dphi = wavefront_phase_shift(&phase, r0, v, 0.13, 0.26);
assert!(
(dphi - expect).abs() < 1e-9 * expect.abs(),
"{dphi} vs {expect}"
);
}
let tilt = |x: f64, y: f64| 3.0e4 * x - 1.0e4 * y;
assert!(wavefront_phase_shift(&tilt, [0.002, 0.001], v, 0.13, 0.26).abs() < 1e-9);
}
#[test]
fn curvature_bias_monte_carlo_matches_closed_form() {
let k = effective_wavevector(RB87_D2_WAVELENGTH_M);
let r = 500.0;
let phase = defocus_beam_phase(k, r);
let c_all = cloud(3e-6, 3e-3, f64::INFINITY);
let (mc, frac) = wavefront_bias_monte_carlo(&phase, k, &c_all, 200_000, 7);
let closed = curvature_bias_accel(c_all.sigma_v2, r);
assert_eq!(frac, 1.0);
assert!(
(mc - closed).abs() / closed < 0.01,
"MC {mc:.4e} vs σ_v²/R {closed:.4e}"
);
let c_det = cloud(3e-6, 3e-3, 5e-3);
let (mc_d, frac_d) = wavefront_bias_monte_carlo(&phase, k, &c_det, 400_000, 11);
let closed_d =
curvature_bias_with_detection(c_det.sigma_v2, 3e-3, 5e-3, c_det.detection_time_s, r);
assert!(frac_d > 0.01 && frac_d < 0.9, "detected fraction {frac_d}");
assert!(
closed_d < closed,
"detection selection must reduce the bias"
);
assert!(
(mc_d - closed_d).abs() / closed_d < 0.02,
"MC {mc_d:.4e} vs {closed_d:.4e}"
);
}
#[test]
fn curvature_bias_limiting_cases() {
let sv2 = thermal_velocity_variance(2e-6, RB87_MASS_KG);
assert!((sv2.sqrt() - 0.013_83).abs() < 5e-5, "σ_v = {}", sv2.sqrt());
let b = curvature_bias_accel(sv2, 1000.0);
assert!((curvature_bias_accel(2.0 * sv2, 1000.0) / b - 2.0).abs() < 1e-12);
assert!((curvature_bias_accel(sv2, 2000.0) / b - 0.5).abs() < 1e-12);
assert_eq!(curvature_bias_accel(sv2, f64::INFINITY), 0.0);
assert_eq!(curvature_bias_accel(0.0, 1000.0), 0.0);
let t_det = 0.79;
assert_eq!(
curvature_bias_with_detection(sv2, 3e-3, f64::INFINITY, t_det, 1000.0),
b
);
let big = curvature_bias_with_detection(sv2, 10.0, 5e-3, t_det, 1000.0);
let rho2 = sv2 * t_det * t_det / (100.0 + sv2 * t_det * t_det);
assert!(
(big / b - 1.0).abs() <= rho2,
"{} vs ρ² {rho2}",
big / b - 1.0
);
let tiny = curvature_bias_with_detection(sv2, 0.0, 1e-9, t_det, 1000.0);
assert!(tiny.abs() < 1e-9 * b);
let small_c = curvature_bias_with_detection(sv2, 1e-3, 5e-3, t_det, 1000.0);
let large_c = curvature_bias_with_detection(sv2, 4e-3, 5e-3, t_det, 1000.0);
assert!(small_c < large_c && large_c < b);
let rz = zernike_defocus_to_radius(78e-9, 0.01);
assert!((rz - 0.01 * 0.01 / (4.0 * 3f64.sqrt() * 78e-9)).abs() < 1e-9 * rz);
assert!(zernike_defocus_to_radius(0.0, 0.01).is_infinite());
}
#[test]
fn classical_aided_resolution_monte_carlo_matches_closed_form() {
let k = effective_wavevector(RB87_D2_WAVELENGTH_M);
let t = 0.01;
let a_max = max_unambiguous_accel(k, t);
let sigma_phi = 0.05;
let sigma_c = 1.9 / (k * t * t);
let p = ambiguity_failure_probability(k, t, sigma_c, sigma_phi);
assert!(p > 0.05 && p < 0.2, "p = {p}");
let mut rng = ChaCha8Rng::seed_from_u64(42);
let nc = Normal::new(0.0, sigma_c).unwrap();
let np = Normal::new(0.0, sigma_phi).unwrap();
let n = 200_000;
let mut wrong = 0usize;
for _ in 0..n {
let a = rng.gen_range(-50.0 * a_max..50.0 * a_max);
let phi_w = wrap_phase(mach_zehnder_phase(k, a, t) + np.sample(&mut rng));
let (_, a_hat) = resolve_with_classical(k, t, a + nc.sample(&mut rng), phi_w);
if (a_hat - a).abs() > a_max {
wrong += 1;
}
}
let mc = wrong as f64 / n as f64;
let sd = (p * (1.0 - p) / n as f64).sqrt();
assert!(
(mc - p).abs() < 4.0 * sd,
"MC {mc:.4} vs closed {p:.4} (±{sd:.4})"
);
}
#[test]
fn multi_t_resolution_monte_carlo_matches_closed_form() {
let k = effective_wavevector(RB87_D2_WAVELENGTH_M);
let (t1, t2) = (0.002, 0.01); let (s1, s2) = (0.07, 0.02);
let p = multi_t_failure_probability(t1, t2, s1, s2);
assert!(p > 0.01 && p < 0.3, "p = {p}");
let a_max1 = max_unambiguous_accel(k, t1);
let a_max2 = max_unambiguous_accel(k, t2);
let mut rng = ChaCha8Rng::seed_from_u64(9);
let n1 = Normal::new(0.0, s1).unwrap();
let n2 = Normal::new(0.0, s2).unwrap();
let n = 200_000;
let mut wrong = 0usize;
for _ in 0..n {
let a = rng.gen_range(-0.8 * a_max1..0.8 * a_max1);
let p1 = wrap_phase(mach_zehnder_phase(k, a, t1) + n1.sample(&mut rng));
let p2 = wrap_phase(mach_zehnder_phase(k, a, t2) + n2.sample(&mut rng));
let (_, a_hat) = resolve_multi_t(k, t1, t2, p1, p2);
if (a_hat - a).abs() > a_max2 {
wrong += 1;
}
}
let mc = wrong as f64 / n as f64;
let sd = (p * (1.0 - p) / n as f64).sqrt();
assert!(
(mc - p).abs() < 4.0 * sd + 1e-3,
"MC {mc:.4} vs closed {p:.4}"
);
assert!((a_max1 / a_max2 - 25.0).abs() < 1e-9);
}
#[test]
fn ambiguity_limits_and_inverse() {
let k = effective_wavevector(RB87_D2_WAVELENGTH_M);
let t = 0.05;
assert_eq!(ambiguity_failure_probability(k, t, 0.0, 0.0), 0.0);
let sc = 2e-5;
let direct = 1.0 - libm::erf(PI / (k * t * t * sc * std::f64::consts::SQRT_2));
assert!((ambiguity_failure_probability(k, t, sc, 0.0) - direct).abs() < 1e-12);
assert!(
ambiguity_failure_probability(k, t, 2e-5, 0.01)
> ambiguity_failure_probability(k, t, 1e-5, 0.01)
);
assert!(ambiguity_failure_probability(k, t, 1.0, 0.0) > 0.99);
let s_max = max_classical_sigma_for_failure(k, t, 0.01, 1e-6);
let back = ambiguity_failure_probability(k, t, s_max, 0.01);
assert!((back - 1e-6).abs() < 1e-9, "{back:.3e}");
assert!(s_max < max_unambiguous_accel(k, t) / 4.0);
assert_eq!(max_classical_sigma_for_failure(k, t, 1.0, 1e-6), 0.0);
let same = multi_t_failure_probability(0.01, 0.01, 0.3, 0.4);
assert!((same - wrong_index_probability(0.5)).abs() < 1e-15);
}
}