use std::f64::consts::PI;
#[derive(Debug, Clone, PartialEq)]
pub enum SpeckleError {
InvalidParameter(String),
LengthMismatch { expected: usize, got: usize },
}
impl std::fmt::Display for SpeckleError {
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 SpeckleError {}
#[derive(Debug, Clone)]
pub struct SpeckleStatistics {
pub mean_intensity: f64,
}
impl SpeckleStatistics {
pub fn new(mean_intensity: f64) -> Result<Self, SpeckleError> {
if mean_intensity <= 0.0 {
return Err(SpeckleError::InvalidParameter(
"mean_intensity must be positive".into(),
));
}
Ok(Self { mean_intensity })
}
pub fn pdf(&self, intensity: f64) -> f64 {
if intensity < 0.0 {
return 0.0;
}
let i_bar = self.mean_intensity;
(-intensity / i_bar).exp() / i_bar
}
pub fn cdf(&self, intensity: f64) -> f64 {
if intensity < 0.0 {
return 0.0;
}
1.0 - (-intensity / self.mean_intensity).exp()
}
pub fn contrast(&self) -> f64 {
1.0
}
pub fn autocorrelation(&self, delta_r: f64, coherence_area: f64) -> Result<f64, SpeckleError> {
if coherence_area <= 0.0 {
return Err(SpeckleError::InvalidParameter(
"coherence_area must be positive".into(),
));
}
let i_bar = self.mean_intensity;
let mu_sq = (-delta_r * delta_r / (coherence_area * coherence_area)).exp();
Ok(i_bar * i_bar * (1.0 + mu_sq))
}
pub fn variance(&self) -> f64 {
self.mean_intensity * self.mean_intensity
}
pub fn moment(&self, n: u32) -> f64 {
let n_fac: f64 = (1..=(n as u64)).map(|k| k as f64).product();
n_fac * self.mean_intensity.powi(n as i32)
}
}
pub struct SpeckleReduction;
impl SpeckleReduction {
pub fn spatial_averaging(n_patterns: usize) -> Result<f64, SpeckleError> {
if n_patterns == 0 {
return Err(SpeckleError::InvalidParameter(
"n_patterns must be at least 1".into(),
));
}
Ok(1.0 / (n_patterns as f64).sqrt())
}
pub fn polarization_diversity() -> f64 {
1.0 / 2.0_f64.sqrt()
}
pub fn frequency_diversity(
bandwidth: f64,
coherence_length: f64,
path_length: f64,
) -> Result<f64, SpeckleError> {
if bandwidth <= 0.0 {
return Err(SpeckleError::InvalidParameter(
"bandwidth must be positive".into(),
));
}
if coherence_length <= 0.0 {
return Err(SpeckleError::InvalidParameter(
"coherence_length must be positive".into(),
));
}
if path_length < 0.0 {
return Err(SpeckleError::InvalidParameter(
"path_length must be non-negative".into(),
));
}
let m = 1.0 + path_length / coherence_length;
Ok(1.0 / m.sqrt())
}
pub fn angular_diversity(n_angles: usize) -> Result<f64, SpeckleError> {
Self::spatial_averaging(n_angles)
}
pub fn combined(
n_spatial: usize,
m_frequency: f64,
n_polarization: f64,
) -> Result<f64, SpeckleError> {
if n_spatial == 0 {
return Err(SpeckleError::InvalidParameter(
"n_spatial must be at least 1".into(),
));
}
if m_frequency <= 0.0 {
return Err(SpeckleError::InvalidParameter(
"m_frequency must be positive".into(),
));
}
if n_polarization <= 0.0 {
return Err(SpeckleError::InvalidParameter(
"n_polarization must be positive".into(),
));
}
let total = n_spatial as f64 * m_frequency * n_polarization;
Ok(1.0 / total.sqrt())
}
}
pub struct ObjectiveSpeckleSize;
impl ObjectiveSpeckleSize {
pub fn airy_disk_radius(wavelength: f64, f_number: f64) -> Result<f64, SpeckleError> {
if wavelength <= 0.0 {
return Err(SpeckleError::InvalidParameter(
"wavelength must be positive".into(),
));
}
if f_number <= 0.0 {
return Err(SpeckleError::InvalidParameter(
"f_number must be positive".into(),
));
}
Ok(1.22 * wavelength * f_number)
}
pub fn speckle_size_subjective(
wavelength: f64,
distance: f64,
aperture: f64,
) -> Result<f64, SpeckleError> {
if wavelength <= 0.0 {
return Err(SpeckleError::InvalidParameter(
"wavelength must be positive".into(),
));
}
if distance <= 0.0 {
return Err(SpeckleError::InvalidParameter(
"distance must be positive".into(),
));
}
if aperture <= 0.0 {
return Err(SpeckleError::InvalidParameter(
"aperture must be positive".into(),
));
}
Ok(1.22 * wavelength * distance / aperture)
}
pub fn speckle_area(
wavelength: f64,
distance: f64,
aperture: f64,
) -> Result<f64, SpeckleError> {
let r = Self::speckle_size_subjective(wavelength, distance, aperture)?;
Ok(PI * r * r)
}
pub fn num_speckles_in_area(
wavelength: f64,
distance: f64,
aperture: f64,
observation_area: f64,
) -> Result<f64, SpeckleError> {
if observation_area <= 0.0 {
return Err(SpeckleError::InvalidParameter(
"observation_area must be positive".into(),
));
}
let a_speckle = Self::speckle_area(wavelength, distance, aperture)?;
Ok(observation_area / a_speckle)
}
}
#[derive(Debug, Clone)]
pub struct SpeckleCorrelation {
pub separations: Vec<f64>,
pub correlation: Vec<f64>,
}
impl SpeckleCorrelation {
pub fn compute(
pattern1: &[f64],
pattern2: &[f64],
grid_spacing: f64,
) -> Result<Self, SpeckleError> {
let n = pattern1.len();
if pattern2.len() != n {
return Err(SpeckleError::LengthMismatch {
expected: n,
got: pattern2.len(),
});
}
if n == 0 {
return Err(SpeckleError::InvalidParameter(
"patterns must not be empty".into(),
));
}
let mean1 = pattern1.iter().sum::<f64>() / n as f64;
let mean2 = pattern2.iter().sum::<f64>() / n as f64;
let var1 = pattern1.iter().map(|&x| (x - mean1).powi(2)).sum::<f64>() / n as f64;
let var2 = pattern2.iter().map(|&x| (x - mean2).powi(2)).sum::<f64>() / n as f64;
if var1 < f64::EPSILON || var2 < f64::EPSILON {
return Err(SpeckleError::InvalidParameter(
"patterns must have non-zero variance".into(),
));
}
let sigma1 = var1.sqrt();
let sigma2 = var2.sqrt();
let n_shifts = n / 2;
let mut separations = Vec::with_capacity(n_shifts);
let mut correlation = Vec::with_capacity(n_shifts);
for shift in 0..n_shifts {
let cov: f64 = (0..(n - shift))
.map(|i| (pattern1[i] - mean1) * (pattern2[i + shift] - mean2))
.sum::<f64>()
/ (n - shift) as f64;
separations.push(shift as f64 * grid_spacing);
correlation.push(cov / (sigma1 * sigma2));
}
Ok(Self {
separations,
correlation,
})
}
pub fn analytic_gaussian(
mutual_coherence: f64,
speckle_size: f64,
separations: Vec<f64>,
) -> Result<Self, SpeckleError> {
if !(0.0..=1.0).contains(&mutual_coherence) {
return Err(SpeckleError::InvalidParameter(
"mutual_coherence must be in [0, 1]".into(),
));
}
if speckle_size <= 0.0 {
return Err(SpeckleError::InvalidParameter(
"speckle_size must be positive".into(),
));
}
let c0 = mutual_coherence * mutual_coherence;
let correlation: Vec<f64> = separations
.iter()
.map(|&dr| c0 * (-dr * dr / (2.0 * speckle_size * speckle_size)).exp())
.collect();
Ok(Self {
separations,
correlation,
})
}
pub fn peak(&self) -> f64 {
self.correlation.first().copied().unwrap_or(0.0)
}
pub fn correlation_length(&self) -> Option<f64> {
let c0 = self.peak();
if c0 < f64::EPSILON {
return None;
}
let threshold = c0 / std::f64::consts::E;
for i in 1..self.separations.len() {
if self.correlation[i] <= threshold {
let s0 = self.separations[i - 1];
let s1 = self.separations[i];
let c_prev = self.correlation[i - 1];
let c_next = self.correlation[i];
let t = (threshold - c_prev) / (c_next - c_prev);
return Some(s0 + t * (s1 - s0));
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn speckle_pdf_integrates_to_unity() {
let stats = SpeckleStatistics::new(1.0).expect("valid");
let n = 10_000_usize;
let i_max = 30.0_f64;
let di = i_max / (n as f64 - 1.0);
let integral: f64 = (0..n)
.map(|k| {
let i = k as f64 * di;
let w = if k == 0 || k == n - 1 { 0.5 } else { 1.0 };
w * stats.pdf(i) * di
})
.sum();
assert_abs_diff_eq!(integral, 1.0, epsilon = 1e-4);
}
#[test]
fn speckle_contrast_is_unity() {
let stats = SpeckleStatistics::new(2.5).expect("valid");
assert_abs_diff_eq!(stats.contrast(), 1.0, epsilon = 1e-12);
}
#[test]
fn spatial_averaging_contrast_reduction() {
let c = SpeckleReduction::spatial_averaging(4).expect("valid");
assert_abs_diff_eq!(c, 0.5, epsilon = 1e-12);
}
#[test]
fn polarization_diversity_contrast() {
let c = SpeckleReduction::polarization_diversity();
assert_abs_diff_eq!(c, 1.0 / 2.0_f64.sqrt(), epsilon = 1e-12);
}
#[test]
fn frequency_diversity_no_scatter_gives_unity_contrast() {
let c = SpeckleReduction::frequency_diversity(1e12, 1e-3, 0.0).expect("valid");
assert_abs_diff_eq!(c, 1.0, epsilon = 1e-12);
}
#[test]
fn airy_disk_radius_scales_with_f_number() {
let r1 = ObjectiveSpeckleSize::airy_disk_radius(633e-9, 2.0).expect("ok");
let r2 = ObjectiveSpeckleSize::airy_disk_radius(633e-9, 4.0).expect("ok");
assert_abs_diff_eq!(r2, 2.0 * r1, epsilon = 1e-20);
}
#[test]
fn speckle_correlation_self_is_unity() {
let pattern: Vec<f64> = (0..64).map(|i| (i as f64 * 0.3).sin() + 2.0).collect();
let corr = SpeckleCorrelation::compute(&pattern, &pattern, 1e-6).expect("ok");
assert_abs_diff_eq!(corr.peak(), 1.0, epsilon = 1e-10);
}
#[test]
fn analytic_gaussian_correlation_peak_equals_mu_squared() {
let mu = 0.7_f64;
let seps: Vec<f64> = (0..50).map(|i| i as f64 * 1e-6).collect();
let corr = SpeckleCorrelation::analytic_gaussian(mu, 5e-6, seps).expect("ok");
assert_abs_diff_eq!(corr.peak(), mu * mu, epsilon = 1e-12);
}
#[test]
fn num_speckles_in_area_positive() {
let n = ObjectiveSpeckleSize::num_speckles_in_area(633e-9, 1.0, 1e-3, 1e-4).expect("ok");
assert!(n > 0.0);
}
#[test]
fn speckle_moment_n2_equals_two_i_bar_squared() {
let i_bar = 3.0_f64;
let stats = SpeckleStatistics::new(i_bar).expect("valid");
assert_abs_diff_eq!(stats.moment(2), 2.0 * i_bar * i_bar, epsilon = 1e-10);
}
}