use num_complex::Complex64;
const C0: f64 = 2.997_924_58e8;
#[derive(Debug, Clone)]
pub struct FirstOrderCoherence {
pub coherence_time_s: f64,
pub center_wavelength_nm: f64,
}
impl FirstOrderCoherence {
pub fn new(coherence_time_s: f64, center_wavelength_nm: f64) -> Self {
Self {
coherence_time_s,
center_wavelength_nm,
}
}
#[inline]
fn omega0(&self) -> f64 {
2.0 * std::f64::consts::PI * C0 / (self.center_wavelength_nm * 1e-9)
}
pub fn g1_single_mode(&self, tau_s: f64) -> Complex64 {
let phase = -self.omega0() * tau_s;
let decay = -tau_s.abs() / self.coherence_time_s;
Complex64::from_polar(decay.exp(), phase)
}
pub fn g1_thermal(&self, tau_s: f64) -> Complex64 {
let phase = -self.omega0() * tau_s;
let decay = -(tau_s * tau_s) / (2.0 * self.coherence_time_s * self.coherence_time_s);
Complex64::from_polar(decay.exp(), phase)
}
#[inline]
pub fn coherence_length_m(&self) -> f64 {
C0 * self.coherence_time_s
}
#[inline]
pub fn linewidth_hz(&self) -> f64 {
1.0 / (std::f64::consts::PI * self.coherence_time_s)
}
pub fn visibility(&self, tau_s: f64) -> f64 {
(-tau_s.abs() / self.coherence_time_s).exp()
}
#[inline]
pub fn degree_of_coherence(&self) -> f64 {
1.0
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum LightSource {
Laser,
Thermal,
SinglePhoton,
Squeezed {
squeezing_r: f64,
},
}
#[derive(Debug, Clone)]
pub struct SecondOrderCoherence {
pub source_type: LightSource,
pub coherence_time_s: f64,
}
impl SecondOrderCoherence {
pub fn new(source_type: LightSource, coherence_time_s: f64) -> Self {
Self {
source_type,
coherence_time_s,
}
}
pub fn g2_at_zero(&self) -> f64 {
match &self.source_type {
LightSource::Laser => 1.0,
LightSource::Thermal => 2.0,
LightSource::SinglePhoton => 0.0,
LightSource::Squeezed { squeezing_r } => {
let r = squeezing_r;
(-2.0 * r).exp()
}
}
}
pub fn g2_at_tau(&self, tau_s: f64) -> f64 {
match &self.source_type {
LightSource::Laser => 1.0,
LightSource::Thermal => 1.0 + (-2.0 * tau_s.abs() / self.coherence_time_s).exp(),
LightSource::SinglePhoton => 1.0 - (-tau_s.abs() / self.coherence_time_s).exp(),
LightSource::Squeezed { .. } => {
let g2_0 = self.g2_at_zero();
1.0 + (g2_0 - 1.0) * (-tau_s.abs() / self.coherence_time_s).exp()
}
}
}
pub fn is_antibunching(&self) -> bool {
self.g2_at_zero() < 1.0
}
pub fn is_bunching(&self) -> bool {
self.g2_at_zero() > 1.0
}
pub fn hanbury_brown_twiss_visibility(&self) -> f64 {
self.g2_at_zero() - 1.0
}
}
#[derive(Debug, Clone)]
pub struct SpatialCoherence {
pub wavelength_nm: f64,
pub source_size_mm: f64,
pub propagation_distance_m: f64,
}
impl SpatialCoherence {
pub fn new(wavelength_nm: f64, source_size_mm: f64, propagation_distance_m: f64) -> Self {
Self {
wavelength_nm,
source_size_mm,
propagation_distance_m,
}
}
pub fn coherence_radius_mm(&self) -> f64 {
let lambda_m = self.wavelength_nm * 1e-9;
let d_m = self.source_size_mm * 1e-3;
let r_c_m = lambda_m * self.propagation_distance_m / (std::f64::consts::PI * d_m);
r_c_m * 1e3
}
pub fn mutual_coherence(&self, separation_mm: f64) -> f64 {
let lambda_m = self.wavelength_nm * 1e-9;
let d_m = self.source_size_mm * 1e-3;
let delta_r_m = separation_mm * 1e-3;
let x = std::f64::consts::PI * d_m * delta_r_m / (lambda_m * self.propagation_distance_m);
if x.abs() < 1e-12 {
1.0
} else {
x.sin() / x
}
}
#[inline]
pub fn speckle_size_mm(&self) -> f64 {
self.coherence_radius_mm()
}
pub fn coherence_cells(&self, detector_diameter_mm: f64) -> f64 {
let r_c = self.coherence_radius_mm();
if r_c <= 0.0 {
return f64::INFINITY;
}
(detector_diameter_mm / r_c).powi(2)
}
pub fn coherence_volume_mm3(&self, coherence_time_s: f64) -> f64 {
let r_c = self.coherence_radius_mm();
let area_mm2 = std::f64::consts::PI * r_c * r_c;
let lc_m = C0 * coherence_time_s;
let lc_mm = lc_m * 1e3;
area_mm2 * lc_mm
}
}
#[derive(Debug, Clone)]
pub struct HBTExperiment {
pub source: SecondOrderCoherence,
pub detector_jitter_ps: f64,
pub dead_time_ns: f64,
pub dark_count_rate: f64,
}
impl HBTExperiment {
pub fn new(
source: SecondOrderCoherence,
jitter_ps: f64,
dead_time_ns: f64,
dark_count_rate: f64,
) -> Self {
Self {
source,
detector_jitter_ps: jitter_ps,
dead_time_ns,
dark_count_rate,
}
}
pub fn measured_g2(&self, tau_s: f64, signal_rate: f64) -> f64 {
let total_rate = signal_rate + self.dark_count_rate;
let rho = if total_rate > 0.0 {
signal_rate / total_rate
} else {
1.0
};
let jitter_s = self.detector_jitter_ps * 1e-12;
let tau_eff = (tau_s.abs() + jitter_s).max(0.0);
let g2_true = self.source.g2_at_tau(tau_eff);
rho * rho * g2_true + 2.0 * rho * (1.0 - rho) + (1.0 - rho) * (1.0 - rho)
}
pub fn snr(&self, signal_rate: f64, measurement_time_s: f64) -> f64 {
let tau_c = self.source.coherence_time_s;
signal_rate.powf(1.5) * measurement_time_s.sqrt() * tau_c
}
pub fn required_measurement_time(&self, signal_rate: f64) -> f64 {
let tau_c = self.source.coherence_time_s;
9.0 / (signal_rate.powi(3) * tau_c * tau_c)
}
pub fn accidental_rate(&self, signal_rate: f64, window_ns: f64) -> f64 {
let total = signal_rate + self.dark_count_rate;
let window_s = window_ns * 1e-9;
2.0 * total * total * window_s
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_g1_at_zero() {
let coh = FirstOrderCoherence::new(1e-12, 800.0);
let g1_0 = coh.g1_single_mode(0.0);
assert_relative_eq!(g1_0.norm(), 1.0, epsilon = 1e-12);
}
#[test]
fn test_g1_thermal_at_zero() {
let coh = FirstOrderCoherence::new(1e-12, 800.0);
let g1_0 = coh.g1_thermal(0.0);
assert_relative_eq!(g1_0.norm(), 1.0, epsilon = 1e-12);
}
#[test]
fn test_coherence_length() {
let tau_c = 1e-12; let coh = FirstOrderCoherence::new(tau_c, 800.0);
let lc = coh.coherence_length_m();
assert_relative_eq!(lc, C0 * tau_c, epsilon = 1e-5);
}
#[test]
fn test_linewidth_from_coherence() {
let tau_c = 1e-9; let coh = FirstOrderCoherence::new(tau_c, 800.0);
let dnu = coh.linewidth_hz();
assert_relative_eq!(dnu, 1.0 / (std::f64::consts::PI * tau_c), epsilon = 1e-5);
}
#[test]
fn test_g2_zero_laser() {
let src = SecondOrderCoherence::new(LightSource::Laser, 1e-12);
assert_relative_eq!(src.g2_at_zero(), 1.0, epsilon = 1e-15);
assert!(!src.is_antibunching());
assert!(!src.is_bunching());
}
#[test]
fn test_g2_zero_thermal() {
let src = SecondOrderCoherence::new(LightSource::Thermal, 1e-12);
assert_relative_eq!(src.g2_at_zero(), 2.0, epsilon = 1e-15);
assert!(src.is_bunching());
}
#[test]
fn test_g2_zero_single_photon() {
let src = SecondOrderCoherence::new(LightSource::SinglePhoton, 1e-9);
assert_relative_eq!(src.g2_at_zero(), 0.0, epsilon = 1e-15);
assert!(src.is_antibunching());
}
#[test]
fn test_g2_thermal_large_tau() {
let tau_c = 1e-12;
let src = SecondOrderCoherence::new(LightSource::Thermal, tau_c);
let g2_large = src.g2_at_tau(1e6 * tau_c);
assert_relative_eq!(g2_large, 1.0, epsilon = 1e-6);
}
#[test]
fn test_coherence_radius_vczernike() {
let sc = SpatialCoherence::new(633.0, 1.0, 1.0);
let r_c = sc.coherence_radius_mm();
let expected_m = 633e-9 * 1.0 / (std::f64::consts::PI * 1e-3);
assert_relative_eq!(r_c, expected_m * 1e3, epsilon = 1e-6);
}
#[test]
fn test_mutual_coherence_at_zero_separation() {
let sc = SpatialCoherence::new(800.0, 2.0, 0.5);
assert_relative_eq!(sc.mutual_coherence(0.0), 1.0, epsilon = 1e-10);
}
#[test]
fn test_hbt_accidental_rate() {
let src = SecondOrderCoherence::new(LightSource::SinglePhoton, 1e-9);
let hbt = HBTExperiment::new(src, 50.0, 10.0, 100.0);
let rate = hbt.accidental_rate(1e6, 1.0); let total = 1e6_f64 + 100.0;
let expected = 2.0 * total * total * 1e-9;
assert_relative_eq!(rate, expected, epsilon = 1e-3);
}
#[test]
fn test_hbt_g2_ideal_single_photon_at_zero() {
let src = SecondOrderCoherence::new(LightSource::SinglePhoton, 1e-9);
let hbt = HBTExperiment::new(src, 0.0, 0.0, 0.0); let g2 = hbt.measured_g2(0.0, 1e6);
assert!(
g2 < 0.1,
"Expected near-zero g²(0) for single photon, got {g2}"
);
}
#[test]
fn test_hbt_snr_increases_with_time() {
let src = SecondOrderCoherence::new(LightSource::Thermal, 1e-9);
let hbt = HBTExperiment::new(src, 50.0, 10.0, 100.0);
let snr_1s = hbt.snr(1e6, 1.0);
let snr_100s = hbt.snr(1e6, 100.0);
assert!(snr_100s > snr_1s);
}
}