use std::f64::consts::PI;
const C: f64 = 2.997_924_58e8;
#[derive(Debug, Clone, PartialEq)]
pub enum SpatialCoherenceError {
InvalidParameter(String),
LengthMismatch { expected: usize, got: usize },
}
impl std::fmt::Display for SpatialCoherenceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidParameter(msg) => write!(f, "Invalid parameter: {msg}"),
Self::LengthMismatch { expected, got } => {
write!(f, "Length mismatch: expected {expected}, got {got}")
}
}
}
}
impl std::error::Error for SpatialCoherenceError {}
fn bessel_j1(x: f64) -> f64 {
if x.abs() < f64::EPSILON {
return 0.0;
}
let mut term = x / 2.0;
let mut sum = term;
let mut m = 1_u32;
loop {
let x2 = x * x;
term *= -x2 / (4.0 * m as f64 * (m as f64 + 1.0));
let prev = sum;
sum += term;
if (sum - prev).abs() < f64::EPSILON * sum.abs().max(1e-300) {
break;
}
m += 1;
if m > 200 {
break;
}
}
sum
}
fn jinc(x: f64) -> f64 {
if x.abs() < 1e-14 {
return 1.0;
}
let pix = PI * x;
2.0 * bessel_j1(pix) / pix
}
pub struct SpatialCoherence;
impl SpatialCoherence {
pub fn young_visibility(
source_diameter: f64,
slit_separation: f64,
wavelength: f64,
distance: f64,
) -> Result<f64, SpatialCoherenceError> {
if source_diameter <= 0.0 {
return Err(SpatialCoherenceError::InvalidParameter(
"source_diameter must be positive".into(),
));
}
if wavelength <= 0.0 {
return Err(SpatialCoherenceError::InvalidParameter(
"wavelength must be positive".into(),
));
}
if distance <= 0.0 {
return Err(SpatialCoherenceError::InvalidParameter(
"distance must be positive".into(),
));
}
let u = slit_separation * source_diameter / (wavelength * distance);
Ok(jinc(u).abs())
}
pub fn coherence_radius(
source_size: f64,
wavelength: f64,
distance: f64,
) -> Result<f64, SpatialCoherenceError> {
if source_size <= 0.0 {
return Err(SpatialCoherenceError::InvalidParameter(
"source_size must be positive".into(),
));
}
if wavelength <= 0.0 {
return Err(SpatialCoherenceError::InvalidParameter(
"wavelength must be positive".into(),
));
}
if distance <= 0.0 {
return Err(SpatialCoherenceError::InvalidParameter(
"distance must be positive".into(),
));
}
Ok(wavelength * distance / source_size)
}
pub fn transverse_coherence_length(
divergence_angle: f64,
wavelength: f64,
) -> Result<f64, SpatialCoherenceError> {
if divergence_angle <= 0.0 {
return Err(SpatialCoherenceError::InvalidParameter(
"divergence_angle must be positive".into(),
));
}
if wavelength <= 0.0 {
return Err(SpatialCoherenceError::InvalidParameter(
"wavelength must be positive".into(),
));
}
Ok(wavelength / (2.0 * PI * divergence_angle))
}
}
#[derive(Debug, Clone)]
pub struct PropagatingCoherence {
coherence_radius: f64,
wavelength: f64,
pub position: f64,
}
impl PropagatingCoherence {
pub fn new(
initial_coherence_radius: f64,
wavelength: f64,
) -> Result<Self, SpatialCoherenceError> {
if initial_coherence_radius <= 0.0 {
return Err(SpatialCoherenceError::InvalidParameter(
"initial_coherence_radius must be positive".into(),
));
}
if wavelength <= 0.0 {
return Err(SpatialCoherenceError::InvalidParameter(
"wavelength must be positive".into(),
));
}
Ok(Self {
coherence_radius: initial_coherence_radius,
wavelength,
position: 0.0,
})
}
pub fn propagate_free_space(&mut self, z: f64) {
let r0 = self.coherence_radius;
let z_c = PI * r0 * r0 / self.wavelength;
self.coherence_radius = r0 * (1.0 + (z / z_c).powi(2)).sqrt();
self.position += z;
}
pub fn through_lens(&mut self, focal_length: f64) {
let z_c_in = PI * self.coherence_radius * self.coherence_radius / self.wavelength;
let inv_z_c_out = 1.0 / z_c_in - 1.0 / focal_length;
if inv_z_c_out.abs() > f64::EPSILON {
let z_c_out = 1.0 / inv_z_c_out;
let r_new = (self.wavelength * z_c_out.abs() / PI).sqrt();
self.coherence_radius = r_new;
}
}
pub fn coherence_radius(&self) -> f64 {
self.coherence_radius
}
pub fn coherence_rayleigh_range(&self) -> f64 {
PI * self.coherence_radius * self.coherence_radius / self.wavelength
}
}
#[derive(Debug, Clone)]
pub struct PartiallyCoherentBeam {
pub spatial_coherence_radius: f64,
pub temporal_coherence_length: f64,
pub beam_radius: f64,
pub wavelength: f64,
}
impl PartiallyCoherentBeam {
pub fn new(
spatial_coherence_radius: f64,
temporal_coherence_length: f64,
beam_radius: f64,
wavelength: f64,
) -> Result<Self, SpatialCoherenceError> {
if spatial_coherence_radius <= 0.0
|| temporal_coherence_length <= 0.0
|| beam_radius <= 0.0
|| wavelength <= 0.0
{
return Err(SpatialCoherenceError::InvalidParameter(
"all parameters must be positive".into(),
));
}
Ok(Self {
spatial_coherence_radius,
temporal_coherence_length,
beam_radius,
wavelength,
})
}
pub fn degree_of_coherence(&self, delta_r: f64, delta_path: f64) -> f64 {
let mu_spatial = (-delta_r * delta_r
/ (2.0 * self.spatial_coherence_radius * self.spatial_coherence_radius))
.exp();
let mu_temporal = (-delta_path * delta_path
/ (2.0 * self.temporal_coherence_length * self.temporal_coherence_length))
.exp();
mu_spatial * mu_temporal
}
pub fn etendue(&self) -> f64 {
let divergence = self.wavelength / (PI * self.spatial_coherence_radius);
PI * PI * self.beam_radius * self.beam_radius * divergence * divergence
}
pub fn num_transverse_coherence_cells(&self) -> f64 {
(self.beam_radius / self.spatial_coherence_radius).powi(2)
}
pub fn temporal_coherence_time(&self) -> f64 {
self.temporal_coherence_length / C
}
}
#[derive(Debug, Clone)]
pub struct LateralCoherenceFunction {
pub separations: Vec<f64>,
pub values: Vec<f64>,
}
impl LateralCoherenceFunction {
pub fn from_table(
separations: Vec<f64>,
values: Vec<f64>,
) -> Result<Self, SpatialCoherenceError> {
if separations.len() != values.len() {
return Err(SpatialCoherenceError::LengthMismatch {
expected: separations.len(),
got: values.len(),
});
}
for i in 1..separations.len() {
if separations[i] <= separations[i - 1] {
return Err(SpatialCoherenceError::InvalidParameter(
"separations must be strictly increasing".into(),
));
}
}
Ok(Self {
separations,
values,
})
}
pub fn gaussian(coherence_radius: f64, n_points: usize) -> Result<Self, SpatialCoherenceError> {
if coherence_radius <= 0.0 {
return Err(SpatialCoherenceError::InvalidParameter(
"coherence_radius must be positive".into(),
));
}
let separations: Vec<f64> = (0..n_points)
.map(|i| 4.0 * coherence_radius * i as f64 / (n_points as f64 - 1.0))
.collect();
let values: Vec<f64> = separations
.iter()
.map(|&dr| (-dr * dr / (2.0 * coherence_radius * coherence_radius)).exp())
.collect();
Ok(Self {
separations,
values,
})
}
pub fn jinc_shaped(
coherence_radius: f64,
n_points: usize,
) -> Result<Self, SpatialCoherenceError> {
if coherence_radius <= 0.0 {
return Err(SpatialCoherenceError::InvalidParameter(
"coherence_radius must be positive".into(),
));
}
let separations: Vec<f64> = (0..n_points)
.map(|i| 4.0 * coherence_radius * i as f64 / (n_points as f64 - 1.0))
.collect();
let values: Vec<f64> = separations
.iter()
.map(|&dr| jinc(dr / coherence_radius).abs())
.collect();
Ok(Self {
separations,
values,
})
}
pub fn evaluate(&self, delta_r: f64) -> f64 {
let n = self.separations.len();
if n == 0 || delta_r < self.separations[0] || delta_r > self.separations[n - 1] {
return 0.0;
}
let idx = self
.separations
.partition_point(|&s| s <= delta_r)
.saturating_sub(1)
.min(n - 2);
let s0 = self.separations[idx];
let s1 = self.separations[idx + 1];
let v0 = self.values[idx];
let v1 = self.values[idx + 1];
let t = (delta_r - s0) / (s1 - s0);
v0 + t * (v1 - v0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn young_visibility_incoherent_point_source_is_unity() {
let v = SpatialCoherence::young_visibility(
1e-9, 1e-3, 633e-9, 1.0, )
.expect("valid parameters");
assert_abs_diff_eq!(v, 1.0, epsilon = 1e-3);
}
#[test]
fn coherence_radius_scales_linearly_with_distance() {
let r1 = SpatialCoherence::coherence_radius(1e-3, 633e-9, 1.0).expect("ok");
let r2 = SpatialCoherence::coherence_radius(1e-3, 633e-9, 2.0).expect("ok");
assert_abs_diff_eq!(r2, 2.0 * r1, epsilon = 1e-20);
}
#[test]
fn propagating_coherence_increases_with_free_space() {
let mut pc = PropagatingCoherence::new(0.5e-3, 633e-9).expect("valid");
let r0 = pc.coherence_radius();
pc.propagate_free_space(10.0);
assert!(pc.coherence_radius() > r0, "coherence radius must grow");
}
#[test]
fn partially_coherent_beam_self_coherence_is_unity() {
let beam = PartiallyCoherentBeam::new(0.5e-3, 1e-3, 1e-3, 633e-9).expect("valid");
let mu = beam.degree_of_coherence(0.0, 0.0);
assert_abs_diff_eq!(mu, 1.0, epsilon = 1e-12);
}
#[test]
fn lateral_coherence_gaussian_at_zero_is_unity() {
let lcf = LateralCoherenceFunction::gaussian(0.5e-3, 100).expect("valid");
let v = lcf.evaluate(0.0);
assert_abs_diff_eq!(v, 1.0, epsilon = 1e-10);
}
#[test]
fn lateral_coherence_interpolation_is_monotone() {
let rc = 0.5e-3_f64;
let lcf = LateralCoherenceFunction::gaussian(rc, 200).expect("valid");
let v1 = lcf.evaluate(rc * 0.5);
let v2 = lcf.evaluate(rc * 1.5);
assert!(v1 > v2, "coherence must decrease with separation");
}
#[test]
fn jinc_at_zero_is_unity() {
assert_abs_diff_eq!(super::jinc(0.0), 1.0, epsilon = 1e-12);
}
#[test]
fn jinc_first_zero_near_1_22() {
let u = 1.2197_f64;
assert!(
super::jinc(u).abs() < 0.02,
"jinc should be near zero at u ≈ 1.22"
);
}
#[test]
fn transverse_coherence_length_inversely_proportional_to_divergence() {
let lc1 = SpatialCoherence::transverse_coherence_length(1e-3, 633e-9).expect("ok");
let lc2 = SpatialCoherence::transverse_coherence_length(2e-3, 633e-9).expect("ok");
assert_abs_diff_eq!(lc1, 2.0 * lc2, epsilon = 1e-20);
}
}