use crate::timegeo::C_M_PER_S;
pub const PLANCK_J_S: f64 = 6.626_070_15e-34;
pub const WAVELENGTH_1550_NM_M: f64 = 1.55e-6;
pub fn photon_energy_j(wavelength_m: f64) -> f64 {
PLANCK_J_S * C_M_PER_S / wavelength_m
}
pub fn diffraction_divergence_rad(wavelength_m: f64, aperture_diameter_m: f64) -> f64 {
wavelength_m / aperture_diameter_m
}
pub fn diffraction_footprint_m(wavelength_m: f64, aperture_diameter_m: f64, range_m: f64) -> f64 {
diffraction_divergence_rad(wavelength_m, aperture_diameter_m) * range_m
}
pub fn airy_footprint_m(wavelength_m: f64, aperture_diameter_m: f64, range_m: f64) -> f64 {
1.22 * diffraction_footprint_m(wavelength_m, aperture_diameter_m, range_m)
}
pub fn photon_limited_toa_crlb_s(pulse_rms_width_s: f64, detected_photons: f64) -> f64 {
if detected_photons <= 0.0 {
return f64::INFINITY;
}
pulse_rms_width_s / detected_photons.sqrt()
}
pub fn photon_limited_range_crlb_m(
pulse_rms_width_s: f64,
detected_photons: f64,
two_way: bool,
) -> f64 {
let toa = photon_limited_toa_crlb_s(pulse_rms_width_s, detected_photons);
let scale = if two_way { 0.5 } else { 1.0 };
scale * C_M_PER_S * toa
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OpticalLinkParams {
pub wavelength_m: f64,
pub tx_power_w: f64,
pub tx_aperture_m: f64,
pub rx_aperture_m: f64,
pub range_m: f64,
pub optics_efficiency: f64,
pub detector_efficiency: f64,
pub atmospheric_loss_db: f64,
pub pointing_loss_db: f64,
}
impl Default for OpticalLinkParams {
fn default() -> Self {
OpticalLinkParams {
wavelength_m: WAVELENGTH_1550_NM_M,
tx_power_w: 1.0e-3, tx_aperture_m: 0.85,
rx_aperture_m: 0.85,
range_m: 3.84e8, optics_efficiency: 0.5,
detector_efficiency: 0.7,
atmospheric_loss_db: 3.0,
pointing_loss_db: 3.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OpticalLinkResult {
pub divergence_rad: f64,
pub footprint_m: f64,
pub geometric_loss_db: f64,
pub total_loss_db: f64,
pub received_power_w: f64,
pub photon_energy_j: f64,
pub photon_rate_hz: f64,
}
pub fn optical_link_budget(p: &OpticalLinkParams) -> OpticalLinkResult {
let divergence_rad = diffraction_divergence_rad(p.wavelength_m, p.tx_aperture_m);
let footprint_m = divergence_rad * p.range_m;
let capture = ((p.rx_aperture_m / footprint_m.max(f64::MIN_POSITIVE)).powi(2)).min(1.0);
let geometric_loss_db = -10.0 * capture.log10();
let total_loss_db = geometric_loss_db + p.atmospheric_loss_db + p.pointing_loss_db;
let transmission = 10f64.powf(-total_loss_db / 10.0);
let received_power_w = p.tx_power_w * transmission * p.optics_efficiency;
let e_photon = photon_energy_j(p.wavelength_m);
let photon_rate_hz = received_power_w / e_photon * p.detector_efficiency;
OpticalLinkResult {
divergence_rad,
footprint_m,
geometric_loss_db,
total_loss_db,
received_power_w,
photon_energy_j: e_photon,
photon_rate_hz,
}
}
pub fn detected_photons(photon_rate_hz: f64, integration_s: f64) -> f64 {
(photon_rate_hz * integration_s.max(0.0)).max(0.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::quantum_devices::EntanglementTimeLink;
#[test]
fn photon_energy_matches_hc_over_lambda() {
let lambda = WAVELENGTH_1550_NM_M;
let hand = 6.626_070_15e-34 * 299_792_458.0 / lambda;
let got = photon_energy_j(lambda);
assert!((got - hand).abs() < 1e-30, "E = {got} J vs hand {hand} J");
assert!(
(1.2e-19..1.35e-19).contains(&got),
"1550 nm photon energy {got} J not ≈ 1.28e-19 J"
);
let ev = got / 1.602_176_634e-19;
assert!((0.75..0.85).contains(&ev), "photon {ev} eV not ≈ 0.8 eV");
}
#[test]
fn diffraction_footprint_is_lambda_over_d_times_range() {
let lambda = WAVELENGTH_1550_NM_M;
let d = 0.85;
let range = 3.84e8; let hand = lambda / d * range;
let got = diffraction_footprint_m(lambda, d, range);
assert!(
(got - hand).abs() < 1e-9,
"footprint {got} m vs hand {hand} m"
);
assert!(
(650.0..750.0).contains(&got),
"lunar footprint {got} m not ≈ 0.7 km"
);
assert!((airy_footprint_m(lambda, d, range) - 1.22 * got).abs() < 1e-9);
}
#[test]
fn ranging_crlb_matches_the_analytic_photon_limited_bound() {
let sigma_tau = 50e-12_f64; let n = 10_000.0_f64;
let toa_hand = sigma_tau / n.sqrt();
assert!((photon_limited_toa_crlb_s(sigma_tau, n) - toa_hand).abs() < 1e-24);
let c = 299_792_458.0;
let r1_hand = c * toa_hand;
assert!((photon_limited_range_crlb_m(sigma_tau, n, false) - r1_hand).abs() < 1e-12);
let r2 = photon_limited_range_crlb_m(sigma_tau, n, true);
assert!((r2 - 0.5 * r1_hand).abs() < 1e-12);
assert!((photon_limited_range_crlb_m(sigma_tau, n, false) / r2 - 2.0).abs() < 1e-12);
let r_100x = photon_limited_range_crlb_m(sigma_tau, 100.0 * n, false);
assert!((r1_hand / r_100x - 10.0).abs() < 1e-9);
assert!(photon_limited_toa_crlb_s(sigma_tau, 0.0).is_infinite());
}
#[test]
fn shot_bound_agrees_with_quantum_devices_link() {
let jitter = 50e-12;
let integration = 1.0;
let link = EntanglementTimeLink {
single_photon_jitter_s: jitter,
dark_rate_hz: 0.0,
systematic_floor_s: 0.0,
..Default::default()
};
let n = link.detected_coincidence_rate_hz() * integration;
let device = link.timing_precision_s(integration);
let optical = photon_limited_toa_crlb_s(jitter, n);
assert!(
(device - optical).abs() < 1e-24,
"quantum-devices {device} s vs optical CRLB {optical} s"
);
}
#[test]
fn link_budget_is_deterministic_and_ordered() {
let p = OpticalLinkParams::default();
let a = optical_link_budget(&p);
let b = optical_link_budget(&p);
assert_eq!(a, b, "link budget must be deterministic");
assert!(a.footprint_m.is_finite() && a.photon_rate_hz.is_finite());
assert!(
(650.0..750.0).contains(&a.footprint_m),
"footprint {} m",
a.footprint_m
);
let wide = OpticalLinkParams {
tx_aperture_m: 0.4,
..p
};
let w = optical_link_budget(&wide);
assert!(w.footprint_m > a.footprint_m);
assert!(w.geometric_loss_db > a.geometric_loss_db);
assert!(w.photon_rate_hz < a.photon_rate_hz);
let n1 = detected_photons(a.photon_rate_hz, 1.0);
let n2 = detected_photons(a.photon_rate_hz, 2.0);
assert!((n2 - 2.0 * n1).abs() < 1e-3 * n1.max(1.0));
assert!(detected_photons(a.photon_rate_hz, -5.0) == 0.0);
}
}