use std::f64::consts::PI;
use num_complex::Complex64;
const C: f64 = 2.997_924_58e8;
#[derive(Debug, Clone, PartialEq)]
pub enum TemporalCoherenceError {
LengthMismatch { expected: usize, got: usize },
InvalidParameter(String),
ZeroSpectrum,
}
impl std::fmt::Display for TemporalCoherenceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::LengthMismatch { expected, got } => {
write!(f, "Length mismatch: expected {expected}, got {got}")
}
Self::InvalidParameter(msg) => write!(f, "Invalid parameter: {msg}"),
Self::ZeroSpectrum => write!(f, "Spectrum is identically zero"),
}
}
}
impl std::error::Error for TemporalCoherenceError {}
#[derive(Debug, Clone)]
pub struct TemporalCoherence {
pub taus: Vec<f64>,
pub gamma: Vec<Complex64>,
}
impl TemporalCoherence {
pub fn from_spectrum(freqs: &[f64], spectrum: &[f64]) -> Result<Self, TemporalCoherenceError> {
let n = freqs.len();
if spectrum.len() != n {
return Err(TemporalCoherenceError::LengthMismatch {
expected: n,
got: spectrum.len(),
});
}
if n < 2 {
return Err(TemporalCoherenceError::InvalidParameter(
"at least 2 frequency samples are required".into(),
));
}
let d_omega = (freqs[n - 1] - freqs[0]) / (n as f64 - 1.0);
if d_omega <= 0.0 {
return Err(TemporalCoherenceError::InvalidParameter(
"frequency grid must be strictly increasing".into(),
));
}
let total_power: f64 = spectrum.iter().sum::<f64>() * d_omega;
if total_power <= 0.0 {
return Err(TemporalCoherenceError::ZeroSpectrum);
}
let tau_max = PI / d_omega;
let taus: Vec<f64> = (0..n)
.map(|k| -tau_max + 2.0 * tau_max * k as f64 / (n as f64 - 1.0))
.collect();
let omega_center = freqs[n / 2];
let mut gamma: Vec<Complex64> = Vec::with_capacity(n);
for &tau in &taus {
let mut acc = Complex64::new(0.0, 0.0);
for k in 0..n {
let phase = (freqs[k] - omega_center) * tau;
acc += Complex64::new(0.0, phase).exp() * spectrum[k];
}
gamma.push(acc * d_omega / (2.0 * PI));
}
Ok(Self { taus, gamma })
}
pub fn coherence_time(&self) -> Option<f64> {
let n = self.taus.len();
if n < 2 {
return None;
}
let gamma0 = self.gamma[n / 2].norm(); if gamma0 < f64::EPSILON {
return None;
}
let mut integral = 0.0_f64;
for i in 0..(n - 1) {
let g1 = (self.gamma[i].norm() / gamma0).powi(2);
let g2 = (self.gamma[i + 1].norm() / gamma0).powi(2);
let d_tau = self.taus[i + 1] - self.taus[i];
integral += 0.5 * (g1 + g2) * d_tau;
}
Some(integral)
}
pub fn coherence_length(&self) -> Option<f64> {
self.coherence_time().map(|tc| C * tc)
}
pub fn degree_of_temporal_coherence(&self, tau: f64) -> f64 {
if self.taus.is_empty() {
return 0.0;
}
let n = self.taus.len();
let gamma0_norm = self.gamma[n / 2].norm();
if gamma0_norm < f64::EPSILON {
return 0.0;
}
let idx = self
.taus
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| {
((*a - tau).abs())
.partial_cmp(&((*b - tau).abs()))
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i)
.unwrap_or(n / 2);
(self.gamma[idx].norm() / gamma0_norm).min(1.0)
}
pub fn gamma_zero(&self) -> Complex64 {
let n = self.taus.len();
if n == 0 {
return Complex64::new(0.0, 0.0);
}
self.gamma[n / 2]
}
}
#[derive(Debug, Clone)]
pub enum SpectralShape {
Gaussian { center_freq: f64, bandwidth: f64 },
Lorentzian { center_freq: f64, linewidth: f64 },
FlatTop { f_low: f64, f_high: f64 },
}
#[derive(Debug, Clone)]
pub struct PowerSpectralDensity {
pub freqs: Vec<f64>,
pub values: Vec<f64>,
pub shape: SpectralShape,
}
impl PowerSpectralDensity {
pub fn gaussian(center_freq: f64, bandwidth: f64) -> Self {
let n = 2048_usize;
let omega_span = 8.0 * bandwidth;
let freqs: Vec<f64> = (0..n)
.map(|k| center_freq - omega_span / 2.0 + omega_span * k as f64 / (n as f64 - 1.0))
.collect();
let values: Vec<f64> = freqs
.iter()
.map(|&w| {
let x = (w - center_freq) / bandwidth;
(-x * x / 2.0).exp()
})
.collect();
Self {
freqs,
values,
shape: SpectralShape::Gaussian {
center_freq,
bandwidth,
},
}
}
pub fn lorentzian(center_freq: f64, linewidth: f64) -> Self {
let n = 2048_usize;
let gamma = linewidth / 2.0;
let omega_span = 40.0 * gamma;
let freqs: Vec<f64> = (0..n)
.map(|k| center_freq - omega_span / 2.0 + omega_span * k as f64 / (n as f64 - 1.0))
.collect();
let values: Vec<f64> = freqs
.iter()
.map(|&w| {
let dw = w - center_freq;
gamma * gamma / (dw * dw + gamma * gamma)
})
.collect();
Self {
freqs,
values,
shape: SpectralShape::Lorentzian {
center_freq,
linewidth,
},
}
}
pub fn flat_top(f_low: f64, f_high: f64) -> Self {
let n = 2048_usize;
let margin = (f_high - f_low) * 0.1;
let freqs: Vec<f64> = (0..n)
.map(|k| {
(f_low - margin) + (f_high - f_low + 2.0 * margin) * k as f64 / (n as f64 - 1.0)
})
.collect();
let values: Vec<f64> = freqs
.iter()
.map(|&w| if w >= f_low && w <= f_high { 1.0 } else { 0.0 })
.collect();
Self {
freqs,
values,
shape: SpectralShape::FlatTop { f_low, f_high },
}
}
pub fn coherence_time(&self) -> f64 {
match &self.shape {
SpectralShape::Gaussian { bandwidth, .. } => (2.0 * PI).sqrt() / bandwidth,
SpectralShape::Lorentzian { linewidth, .. } => {
2.0 / linewidth
}
SpectralShape::FlatTop { f_low, f_high } => {
2.0 * PI / (f_high - f_low)
}
}
}
pub fn coherence_length(&self) -> f64 {
C * self.coherence_time()
}
pub fn to_temporal_coherence(&self) -> Result<TemporalCoherence, TemporalCoherenceError> {
TemporalCoherence::from_spectrum(&self.freqs, &self.values)
}
}
pub struct MichelsonVisibility;
impl MichelsonVisibility {
pub fn compute(
gamma: &TemporalCoherence,
path_diff_m: f64,
wavelength: f64,
) -> Result<f64, TemporalCoherenceError> {
if wavelength <= 0.0 {
return Err(TemporalCoherenceError::InvalidParameter(
"wavelength must be positive".into(),
));
}
let tau = path_diff_m / C;
Ok(gamma.degree_of_temporal_coherence(tau))
}
pub fn compute_analytic(psd: &PowerSpectralDensity, path_diff_m: f64) -> f64 {
let lc = psd.coherence_length();
match &psd.shape {
SpectralShape::Gaussian { .. } => {
let x = path_diff_m / lc;
(-PI * PI * x * x / 2.0).exp()
}
SpectralShape::Lorentzian { .. } => (-path_diff_m.abs() / lc).exp(),
SpectralShape::FlatTop { f_low, f_high } => {
let delta_omega = f_high - f_low;
let tau = path_diff_m / C;
let x = delta_omega * tau / 2.0;
if x.abs() < f64::EPSILON {
1.0
} else {
(x.sin() / x).abs()
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
fn make_gaussian_psd(center: f64, bw: f64) -> PowerSpectralDensity {
PowerSpectralDensity::gaussian(center, bw)
}
#[test]
fn gaussian_psd_coherence_time_analytic() {
let bw = 1e12_f64; let psd = make_gaussian_psd(3e14, bw);
let tc_analytic = (2.0 * PI).sqrt() / bw;
assert_abs_diff_eq!(psd.coherence_time(), tc_analytic, epsilon = 1e-20);
}
#[test]
fn lorentzian_psd_coherence_time_analytic() {
let linewidth = 1e9_f64; let psd = PowerSpectralDensity::lorentzian(3e14, linewidth);
let tc_analytic = 2.0 / linewidth;
assert_abs_diff_eq!(psd.coherence_time(), tc_analytic, epsilon = 1e-20);
}
#[test]
fn flat_top_psd_coherence_time_analytic() {
let f_low = 2.9e14_f64;
let f_high = 3.1e14_f64;
let psd = PowerSpectralDensity::flat_top(f_low, f_high);
let tc_analytic = 2.0 * PI / (f_high - f_low);
assert_abs_diff_eq!(psd.coherence_time(), tc_analytic, epsilon = 1e-25);
}
#[test]
fn temporal_coherence_from_spectrum_gamma0_is_positive() {
let bw = 1e12_f64;
let psd = make_gaussian_psd(3e14, bw);
let tc = psd.to_temporal_coherence().expect("should succeed");
assert!(tc.gamma_zero().re > 0.0, "Γ(0) must be positive");
}
#[test]
fn degree_of_temporal_coherence_at_zero_is_unity() {
let psd = make_gaussian_psd(3e14, 1e12);
let tc = psd.to_temporal_coherence().expect("ok");
let mu = tc.degree_of_temporal_coherence(0.0);
assert_abs_diff_eq!(mu, 1.0, epsilon = 1e-6);
}
#[test]
fn michelson_visibility_zero_path_diff_is_unity() {
let psd = make_gaussian_psd(3e14, 1e12);
let tc = psd.to_temporal_coherence().expect("ok");
let v = MichelsonVisibility::compute(&tc, 0.0, 633e-9).expect("ok");
assert_abs_diff_eq!(v, 1.0, epsilon = 1e-6);
}
#[test]
fn michelson_visibility_analytic_gaussian_decays() {
let psd = make_gaussian_psd(3e14, 1e12);
let lc = psd.coherence_length();
let v0 = MichelsonVisibility::compute_analytic(&psd, 0.0);
let v1 = MichelsonVisibility::compute_analytic(&psd, lc);
assert_abs_diff_eq!(v0, 1.0, epsilon = 1e-12);
assert!(v1 < v0, "visibility must decay with path difference");
}
#[test]
fn michelson_visibility_analytic_lorentzian_at_lc() {
let linewidth = 1e9_f64;
let psd = PowerSpectralDensity::lorentzian(3e14, linewidth);
let lc = psd.coherence_length();
let v = MichelsonVisibility::compute_analytic(&psd, lc);
assert_abs_diff_eq!(v, (-1.0_f64).exp(), epsilon = 1e-10);
}
#[test]
fn coherence_length_is_positive() {
let psd = make_gaussian_psd(3e14, 1e12);
assert!(psd.coherence_length() > 0.0);
}
}