use super::comb::{FrequencyComb, C0};
use crate::error::OxiPhotonError;
const G_EARTH: f64 = 9.80665;
const CS_FREQ_HZ: f64 = 9_192_631_770.0;
#[derive(Debug, Clone)]
pub struct F2fInterferometer {
pub octave_spanning: bool,
pub ppln_length_mm: f64,
pub ceo_detection_snr_db: f64,
}
impl F2fInterferometer {
pub fn new(ppln_length_mm: f64) -> Self {
Self {
octave_spanning: false, ppln_length_mm,
ceo_detection_snr_db: 30.0,
}
}
pub fn new_octave_spanning(ppln_length_mm: f64) -> Self {
Self {
octave_spanning: true,
ppln_length_mm,
ceo_detection_snr_db: 35.0,
}
}
pub fn beat_frequency(&self, comb: &FrequencyComb) -> f64 {
comb.f_ceo
}
pub fn broadening_factor_required(&self) -> f64 {
2.0
}
pub fn beat_snr_db(&self, optical_power_mw: f64, detection_bw_hz: f64) -> f64 {
let power_w = optical_power_mw * 1e-3;
let h_planck = 6.626_070_15e-34;
let nu = C0 / 800e-9;
let eta = 0.8; let snr_linear = eta * power_w / (h_planck * nu * detection_bw_hz.max(1.0));
if snr_linear <= 0.0 {
return f64::NEG_INFINITY;
}
10.0 * snr_linear.log10()
}
pub fn required_temperature_range_k(&self, shg_bandwidth_nm: f64) -> f64 {
0.5 * shg_bandwidth_nm
}
}
#[derive(Debug, Clone)]
pub struct CombPll {
pub loop_bandwidth_hz: f64,
pub phase_noise_floor_dbc_hz: f64,
pub lock_range_hz: f64,
}
impl CombPll {
pub fn new(bandwidth_hz: f64) -> Self {
Self {
loop_bandwidth_hz: bandwidth_hz,
phase_noise_floor_dbc_hz: -140.0,
lock_range_hz: bandwidth_hz * 10.0,
}
}
pub fn phase_noise_dbc_hz(&self, offset_freq_hz: f64) -> f64 {
if offset_freq_hz <= 0.0 {
return self.phase_noise_floor_dbc_hz; }
if offset_freq_hz >= self.loop_bandwidth_hz {
self.phase_noise_floor_dbc_hz
} else {
self.phase_noise_floor_dbc_hz + 20.0 * (offset_freq_hz / self.loop_bandwidth_hz).log10()
}
}
pub fn timing_jitter_fs(&self, f_low: f64, f_high: f64) -> f64 {
if f_low <= 0.0 || f_high <= f_low {
return 0.0;
}
let n_pts = 1000_usize;
let log_fl = f_low.log10();
let log_fh = f_high.log10();
let df_log = (log_fh - log_fl) / n_pts as f64;
let mut integral_dbc = 0.0_f64;
for i in 0..n_pts {
let f1 = 10_f64.powf(log_fl + i as f64 * df_log);
let f2 = 10_f64.powf(log_fl + (i + 1) as f64 * df_log);
let df = f2 - f1;
let s1 = 10_f64.powf(self.phase_noise_dbc_hz(f1) / 10.0); let s2 = 10_f64.powf(self.phase_noise_dbc_hz(f2) / 10.0);
integral_dbc += 0.5 * (s1 + s2) * df; }
let rms_phase_rad = (2.0 * integral_dbc).sqrt(); let f_rep_approx = f_low.max(1.0); let sigma_t_s = rms_phase_rad / (2.0 * std::f64::consts::PI * f_rep_approx);
sigma_t_s * 1e15 }
pub fn lock_acquisition_time_us(&self) -> f64 {
1.0 / self.loop_bandwidth_hz * 1e6 }
pub fn residual_freq_error_hz(&self) -> f64 {
let phase_noise_lin = 10_f64.powf(self.phase_noise_floor_dbc_hz / 20.0);
self.loop_bandwidth_hz * (phase_noise_lin * self.loop_bandwidth_hz).sqrt()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AtomType {
SrLattice,
YbLattice,
AlIon,
CaIon,
HgIon,
}
impl AtomType {
pub fn transition_frequency_hz(&self) -> f64 {
match self {
AtomType::SrLattice => 429_228_066_418_012.0, AtomType::YbLattice => 518_295_836_590_863.0, AtomType::AlIon => 1_121_015_393_207_857.0, AtomType::CaIon => 411_042_129_776_395.0, AtomType::HgIon => 1_064_721_609_899_145.0, }
}
pub fn q_factor(&self) -> f64 {
match self {
AtomType::SrLattice => 4.3e17,
AtomType::YbLattice => 1.0e18,
AtomType::AlIon => 1.7e18,
AtomType::CaIon => 6.6e15,
AtomType::HgIon => 1.4e18,
}
}
pub fn systematic_uncertainty_hz(&self) -> f64 {
match self {
AtomType::SrLattice => 0.2e-18 * self.transition_frequency_hz(),
AtomType::YbLattice => 1.4e-18 * self.transition_frequency_hz(),
AtomType::AlIon => 9.4e-19 * self.transition_frequency_hz(),
AtomType::CaIon => 5.0e-16 * self.transition_frequency_hz(),
AtomType::HgIon => 1.9e-17 * self.transition_frequency_hz(),
}
}
}
#[derive(Debug, Clone)]
pub struct OpticalClock {
pub comb: FrequencyComb,
pub atomic_transition_hz: f64,
pub q_factor: f64,
pub atom_type: AtomType,
}
impl OpticalClock {
pub fn new(comb: FrequencyComb, atom: AtomType) -> Self {
Self {
atomic_transition_hz: atom.transition_frequency_hz(),
q_factor: atom.q_factor(),
atom_type: atom,
comb,
}
}
pub fn allan_deviation_1s(&self, snr: f64) -> f64 {
if snr <= 0.0 || self.q_factor <= 0.0 {
return f64::INFINITY;
}
1.0 / (self.q_factor * snr)
}
pub fn frequency_accuracy_hz(&self) -> f64 {
self.atom_type.systematic_uncertainty_hz()
}
pub fn ratio_to_cs_standard(&self) -> f64 {
self.atomic_transition_hz / CS_FREQ_HZ
}
pub fn gravitational_redshift(&self, height_m: f64) -> f64 {
G_EARTH * height_m / (C0 * C0)
}
pub fn gravitational_frequency_shift_hz(&self, height_m: f64) -> f64 {
self.gravitational_redshift(height_m) * self.atomic_transition_hz
}
pub fn nearest_tooth_number(&self) -> Result<i64, OxiPhotonError> {
let n_approx = (self.atomic_transition_hz - self.comb.f_ceo) / self.comb.f_rep;
if n_approx < 0.0 {
return Err(OxiPhotonError::NumericalError(
"Atomic transition below comb CEO frequency".into(),
));
}
Ok(n_approx.round() as i64)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_f2f_beat_equals_fceo() {
let comb = FrequencyComb::new_ti_sapphire(100e6, 25e6);
let ifo = F2fInterferometer::new(2.0);
assert_abs_diff_eq!(ifo.beat_frequency(&comb), 25e6, epsilon = 1.0);
}
#[test]
fn test_pll_phase_noise_in_loop_lower() {
let pll = CombPll::new(100e3);
let in_loop = pll.phase_noise_dbc_hz(1e3); let out_loop = pll.phase_noise_dbc_hz(1e6); assert!(
in_loop < out_loop,
"in-loop: {in_loop}, out-of-loop: {out_loop}"
);
}
#[test]
fn test_pll_lock_time_reasonable() {
let pll = CombPll::new(100e3); let t_acq = pll.lock_acquisition_time_us();
assert_abs_diff_eq!(t_acq, 10.0, epsilon = 0.001);
}
#[test]
fn test_atom_type_frequencies_positive() {
for atom in [
AtomType::SrLattice,
AtomType::YbLattice,
AtomType::AlIon,
AtomType::CaIon,
AtomType::HgIon,
] {
let f = atom.transition_frequency_hz();
assert!(f > 0.0, "{atom:?} frequency must be positive: {f}");
let q = atom.q_factor();
assert!(q > 0.0, "{atom:?} Q must be positive: {q}");
}
}
#[test]
fn test_optical_clock_allan_deviation() {
let comb = FrequencyComb::new_erbium_fiber(250e6, 20e6);
let clock = OpticalClock::new(comb, AtomType::SrLattice);
let sigma = clock.allan_deviation_1s(10.0); let expected = 1.0 / (AtomType::SrLattice.q_factor() * 10.0);
assert_abs_diff_eq!(sigma, expected, epsilon = 1e-22);
}
#[test]
fn test_gravitational_redshift_sign() {
let comb = FrequencyComb::new_ti_sapphire(100e6, 0.0);
let clock = OpticalClock::new(comb, AtomType::SrLattice);
let shift = clock.gravitational_redshift(1000.0); assert!(
shift > 0.0,
"upward gravitational redshift must be positive: {shift}"
);
let expected = G_EARTH * 1000.0 / (C0 * C0);
assert_abs_diff_eq!(shift, expected, epsilon = 1e-16);
}
#[test]
fn test_ratio_to_cs_reasonable() {
let comb = FrequencyComb::new_ti_sapphire(100e6, 0.0);
let clock = OpticalClock::new(comb, AtomType::SrLattice);
let ratio = clock.ratio_to_cs_standard();
assert!(
ratio > 4e4 && ratio < 5e4,
"ratio {ratio} out of expected range"
);
}
#[test]
fn test_beat_snr_db_positive_power() {
let ifo = F2fInterferometer::new(3.0);
let snr = ifo.beat_snr_db(1.0, 100e3); assert!(snr > 0.0, "SNR should be positive for 1 mW: {snr} dB");
}
}