use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct G2Function {
pub time_grid: Vec<f64>,
pub g2_values: Vec<f64>,
pub integration_time_s: f64,
pub count_rate_cps: f64,
}
impl G2Function {
pub fn new_single_photon_emitter(lifetime_ns: f64, n_time_points: usize) -> Self {
let n = n_time_points.max(2);
let tau_s = lifetime_ns * 1e-9;
let t_max = 5.0 * tau_s;
let dt = 2.0 * t_max / (n as f64 - 1.0);
let mut time_grid = Vec::with_capacity(n);
let mut g2_values = Vec::with_capacity(n);
for i in 0..n {
let t = -t_max + i as f64 * dt;
time_grid.push(t);
let g2 = Self::compute_g2(lifetime_ns, 0.0, t * 1e9); g2_values.push(g2);
}
Self {
time_grid,
g2_values,
integration_time_s: 3600.0,
count_rate_cps: 1e6,
}
}
pub fn compute_g2(lifetime_ns: f64, pure_dephasing_ns: f64, delay_ns: f64) -> f64 {
let tau_abs = delay_ns.abs();
if lifetime_ns <= 0.0 {
return 1.0;
}
let decay_rad = (-tau_abs / lifetime_ns).exp();
let decay_pure = if pure_dephasing_ns > 0.0 {
(-tau_abs / pure_dephasing_ns).exp()
} else {
1.0
};
1.0 - decay_rad * decay_pure
}
pub fn g2_zero(&self) -> f64 {
if self.time_grid.is_empty() || self.g2_values.is_empty() {
return 0.0;
}
let zero_idx = self
.time_grid
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| {
a.abs()
.partial_cmp(&b.abs())
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i)
.unwrap_or(0);
self.g2_values[zero_idx]
}
pub fn single_photon_purity(&self) -> f64 {
(1.0 - self.g2_zero()).clamp(0.0, 1.0)
}
pub fn g2_zero_from_areas(center_area: f64, lateral_area: f64) -> f64 {
if lateral_area <= 0.0 {
return 0.0;
}
(center_area / lateral_area).clamp(0.0, 2.0)
}
pub fn has_bunching(&self) -> bool {
self.time_grid
.iter()
.zip(self.g2_values.iter())
.any(|(t, g2)| t.abs() > 1e-15 && *g2 > 1.0 + 1e-9)
}
pub fn interference_visibility(&self) -> f64 {
(1.0 - self.g2_zero() / 2.0).clamp(0.0, 1.0)
}
}
#[derive(Debug, Clone)]
pub struct HbtSetup {
pub detector1_efficiency: f64,
pub detector2_efficiency: f64,
pub time_resolution_ps: f64,
pub coincidence_window_ns: f64,
pub dark_count_rate_cps: f64,
}
impl HbtSetup {
pub fn new(efficiency: f64, timing_resolution_ps: f64) -> Self {
Self {
detector1_efficiency: efficiency.clamp(0.0, 1.0),
detector2_efficiency: efficiency.clamp(0.0, 1.0),
time_resolution_ps: timing_resolution_ps.max(1.0),
coincidence_window_ns: 1.0,
dark_count_rate_cps: 100.0,
}
}
pub fn coincidence_rate(&self, signal_rate_cps: f64, g2_zero: f64) -> f64 {
let r1 = signal_rate_cps * self.detector1_efficiency * 0.5 + self.dark_count_rate_cps;
let r2 = signal_rate_cps * self.detector2_efficiency * 0.5 + self.dark_count_rate_cps;
let dt = self.coincidence_window_ns * 1e-9;
r1 * r2 * dt * g2_zero.max(0.0)
}
pub fn accidental_rate(&self, rate_cps: f64) -> f64 {
let r1 = rate_cps * self.detector1_efficiency * 0.5 + self.dark_count_rate_cps;
let r2 = rate_cps * self.detector2_efficiency * 0.5 + self.dark_count_rate_cps;
let dt = self.coincidence_window_ns * 1e-9;
r1 * r2 * dt
}
pub fn g2_zero_corrected(&self, raw_coincidences: f64, lateral_coincidences: f64) -> f64 {
if lateral_coincidences <= 0.0 {
return 0.0;
}
((raw_coincidences - lateral_coincidences) / lateral_coincidences + 1.0).clamp(0.0, 2.0)
}
pub fn required_integration_time_s(&self, signal_rate: f64, target_snr: f64) -> f64 {
let r_acc = self.accidental_rate(signal_rate);
let r1 = signal_rate * self.detector1_efficiency * 0.5 + self.dark_count_rate_cps;
let r2 = signal_rate * self.detector2_efficiency * 0.5 + self.dark_count_rate_cps;
let dt = self.coincidence_window_ns * 1e-9;
let r_signal = r1 * r2 * dt; if r_signal <= 0.0 || r_acc <= 0.0 {
return f64::INFINITY;
}
target_snr * target_snr * r_acc / r_signal
}
}
pub struct PhotonNumberDistribution;
impl PhotonNumberDistribution {
pub fn poisson(n: usize, mean: f64) -> f64 {
if mean < 0.0 {
return 0.0;
}
let log_p = -(mean) + n as f64 * mean.ln().max(f64::NEG_INFINITY) - log_factorial(n);
log_p.exp()
}
pub fn thermal(n: usize, mean: f64) -> f64 {
if mean < 0.0 {
return 0.0;
}
let denom = 1.0 + mean;
mean.powi(n as i32) / denom.powi((n + 1) as i32)
}
pub fn fock_one(n: usize) -> f64 {
if n == 1 {
1.0
} else {
0.0
}
}
pub fn mandel_q(distribution: &[f64]) -> f64 {
let mean = distribution
.iter()
.enumerate()
.map(|(n, p)| n as f64 * p)
.sum::<f64>();
if mean < 1e-30 {
return 0.0;
}
let mean_sq = distribution
.iter()
.enumerate()
.map(|(n, p)| (n as f64).powi(2) * p)
.sum::<f64>();
let variance = mean_sq - mean * mean;
variance / mean - 1.0
}
pub fn fano_factor(distribution: &[f64]) -> f64 {
Self::mandel_q(distribution) + 1.0
}
}
fn log_factorial(n: usize) -> f64 {
const LN_FACTORIALS: [f64; 21] = [
0.0, 0.0, std::f64::consts::LN_2, 1.791_759_469, 3.178_053_830, 4.787_491_743, 6.579_251_212, 8.525_161_361, 10.604_602_902, 12.801_827_480, 15.104_412_573, 17.502_307_846, 19.987_214_496, 22.552_163_853, 25.191_221_181, 27.899_271_384, 30.671_860_106, 33.505_073_451, 36.395_445_208, 39.339_884_187, 42.335_616_461, ];
if n <= 20 {
LN_FACTORIALS[n]
} else {
let nf = n as f64;
nf * nf.ln() - nf + 0.5 * (2.0 * PI * nf).ln()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_g2_zero_at_zero_delay() {
let g2_at_0 = G2Function::compute_g2(1.0, 0.0, 0.0);
assert_eq!(g2_at_0, 0.0, "g²(0) should be exactly 0");
}
#[test]
fn test_g2_approaches_unity_at_long_delay() {
let g2_inf = G2Function::compute_g2(1.0, 0.0, 50.0); assert!(
(g2_inf - 1.0).abs() < 1e-10,
"g²(∞) should approach 1; got {g2_inf}"
);
}
#[test]
fn test_g2_histogram_g2_zero() {
let hist = G2Function::new_single_photon_emitter(1.0, 101);
let g2_0 = hist.g2_zero();
assert!(
g2_0 < 0.1,
"g²(0) from histogram should be < 0.1 for SPE; got {g2_0:.4}"
);
}
#[test]
fn test_single_photon_purity_near_unity() {
let hist = G2Function::new_single_photon_emitter(1.0, 101);
let purity = hist.single_photon_purity();
assert!(
purity > 0.9,
"Purity should be > 0.9 for ideal SPE; got {purity:.4}"
);
}
#[test]
fn test_g2_zero_from_areas() {
let g2 = G2Function::g2_zero_from_areas(0.0, 100.0);
assert_eq!(g2, 0.0);
let g2_coh = G2Function::g2_zero_from_areas(100.0, 100.0);
assert!((g2_coh - 1.0).abs() < 1e-10);
}
#[test]
fn test_interference_visibility_is_bounded() {
let hist = G2Function::new_single_photon_emitter(1.0, 51);
let v = hist.interference_visibility();
assert!(
(0.0..=1.0).contains(&v),
"Visibility must be in [0,1]; got {v}"
);
}
#[test]
fn test_hbt_accidentals_positive() {
let hbt = HbtSetup::new(0.5, 50.0);
let acc = hbt.accidental_rate(1e6);
assert!(acc > 0.0, "Accidental rate must be positive; got {acc}");
}
#[test]
fn test_hbt_coincidences_zero_for_ideal_spe() {
let hbt = HbtSetup::new(0.5, 50.0);
let r_c = hbt.coincidence_rate(1e6, 0.0);
assert!(r_c < 1.0, "Coincidences should be near zero for ideal SPE");
}
#[test]
fn test_hbt_corrected_g2_sensible() {
let hbt = HbtSetup::new(0.5, 50.0);
let g2_c = hbt.g2_zero_corrected(100.0, 100.0);
assert!(
(g2_c - 1.0).abs() < 0.01,
"Corrected g²(0) should be 1 for equal peaks"
);
}
#[test]
fn test_poisson_normalises_to_one() {
let mean = 2.5_f64;
let total: f64 = (0..50)
.map(|n| PhotonNumberDistribution::poisson(n, mean))
.sum();
assert!(
(total - 1.0).abs() < 1e-6,
"Poisson must sum to 1; got {total}"
);
}
#[test]
fn test_thermal_normalises_to_one() {
let mean = 1.0_f64;
let total: f64 = (0..200)
.map(|n| PhotonNumberDistribution::thermal(n, mean))
.sum();
assert!(
(total - 1.0).abs() < 1e-4,
"Thermal distribution must sum to ~1; got {total}"
);
}
#[test]
fn test_fock_one_distribution() {
assert_eq!(PhotonNumberDistribution::fock_one(1), 1.0);
assert_eq!(PhotonNumberDistribution::fock_one(0), 0.0);
assert_eq!(PhotonNumberDistribution::fock_one(2), 0.0);
}
#[test]
fn test_mandel_q_coherent_is_zero() {
let mean = 3.0_f64;
let dist: Vec<f64> = (0..50)
.map(|n| PhotonNumberDistribution::poisson(n, mean))
.collect();
let q = PhotonNumberDistribution::mandel_q(&dist);
assert!(
q.abs() < 1e-4,
"Mandel Q for coherent state should be ~0; got {q}"
);
}
#[test]
fn test_mandel_q_thermal_positive() {
let mean = 2.0_f64;
let dist: Vec<f64> = (0..200)
.map(|n| PhotonNumberDistribution::thermal(n, mean))
.collect();
let q = PhotonNumberDistribution::mandel_q(&dist);
assert!(
q > 0.0,
"Thermal state should have Q > 0 (super-Poissonian); got {q}"
);
}
#[test]
fn test_fano_factor_fock_one_sub_poissonian() {
let dist: Vec<f64> = (0..5).map(PhotonNumberDistribution::fock_one).collect();
let f = PhotonNumberDistribution::fano_factor(&dist);
assert!(
f < 1.0,
"Fano factor for Fock state should be < 1 (sub-Poissonian); got {f}"
);
}
}