use num_complex::Complex64;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PulseShape {
Gaussian,
Sech2,
Lorentzian,
Rectangle,
}
impl PulseShape {
pub fn deconvolution_factor(self) -> f64 {
match self {
PulseShape::Gaussian => 1.0_f64 / 2.0_f64.sqrt(),
PulseShape::Sech2 => 0.6482,
PulseShape::Lorentzian => 0.5,
PulseShape::Rectangle => 1.0,
}
}
}
fn signal_fwhm_samples(signal: &[f64]) -> f64 {
let peak = signal.iter().cloned().fold(0.0_f64, f64::max);
if peak < 1e-30 {
return 0.0;
}
let half = peak * 0.5;
let n = signal.len();
let mut i_rise = 0usize;
for (i, &val) in signal.iter().enumerate().take(n) {
if val >= half {
i_rise = i;
break;
}
}
let mut i_fall = n.saturating_sub(1);
for i in (0..n).rev() {
if signal[i] >= half {
i_fall = i;
break;
}
}
(i_fall.saturating_sub(i_rise)) as f64
}
#[derive(Debug, Clone)]
pub struct IntensityAutocorrelation {
pub delay_grid: Vec<f64>,
pub signal: Vec<f64>,
}
impl IntensityAutocorrelation {
pub fn compute(intensity: &[f64], dt: f64) -> Self {
let n = intensity.len();
let n_out = 2 * n - 1;
let delay_grid: Vec<f64> = (0..n_out)
.map(|k| (k as f64 - (n - 1) as f64) * dt)
.collect();
let mut signal = vec![0.0_f64; n_out];
for (tau, sig) in signal.iter_mut().enumerate().take(n_out) {
let shift = tau as isize - (n as isize - 1);
let mut acc = 0.0_f64;
for (t, &int_t) in intensity.iter().enumerate().take(n) {
let t2 = t as isize - shift;
if t2 >= 0 && (t2 as usize) < n {
acc += int_t * intensity[t2 as usize];
}
}
*sig = acc * dt;
}
Self { delay_grid, signal }
}
pub fn fwhm_fs(&self) -> f64 {
if self.delay_grid.len() < 2 {
return 0.0;
}
let dt = (self.delay_grid[1] - self.delay_grid[0]).abs();
signal_fwhm_samples(&self.signal) * dt * 1e15
}
pub fn pulse_fwhm_fs(&self) -> f64 {
self.pulse_fwhm_for_shape(PulseShape::Gaussian)
}
pub fn pulse_fwhm_for_shape(&self, shape: PulseShape) -> f64 {
self.fwhm_fs() * shape.deconvolution_factor()
}
pub fn peak_to_background_ratio(&self) -> f64 {
let n = self.signal.len();
if n < 3 {
return 1.0;
}
let peak = self.signal.iter().cloned().fold(0.0_f64, f64::max);
let bg_len = (n / 10).max(1);
let bg_left: f64 = self.signal[..bg_len].iter().sum::<f64>() / bg_len as f64;
let bg_right: f64 = self.signal[n - bg_len..].iter().sum::<f64>() / bg_len as f64;
let bg = (bg_left + bg_right) * 0.5;
if bg.abs() < 1e-30 {
return f64::INFINITY;
}
peak / bg
}
pub fn deconvolution_factor(pulse_shape: PulseShape) -> f64 {
pulse_shape.deconvolution_factor()
}
}
#[derive(Debug, Clone)]
pub struct InterferometricAutocorrelation {
pub delay_grid: Vec<f64>,
pub signal: Vec<f64>,
}
impl InterferometricAutocorrelation {
pub fn compute(field: &[Complex64], dt: f64) -> Self {
let n = field.len();
let n_out = 2 * n - 1;
let delay_grid: Vec<f64> = (0..n_out)
.map(|k| (k as f64 - (n - 1) as f64) * dt)
.collect();
let mut signal = vec![0.0_f64; n_out];
for (tau, sig) in signal.iter_mut().enumerate().take(n_out) {
let shift = tau as isize - (n as isize - 1);
let mut acc = 0.0_f64;
for (t, &field_t) in field.iter().enumerate().take(n) {
let t2 = t as isize - shift;
let e2 = if t2 >= 0 && (t2 as usize) < n {
field[t2 as usize]
} else {
Complex64::new(0.0, 0.0)
};
let e_sum = field_t + e2;
let val = e_sum.norm_sqr(); acc += val * val; }
*sig = acc * dt;
}
Self { delay_grid, signal }
}
pub fn fwhm_fs(&self) -> f64 {
if self.delay_grid.len() < 2 {
return 0.0;
}
let dt = (self.delay_grid[1] - self.delay_grid[0]).abs();
signal_fwhm_samples(&self.signal) * dt * 1e15
}
pub fn fringe_visibility(&self) -> f64 {
let n = self.signal.len();
if n < 3 {
return 0.0;
}
let peak = self.signal.iter().cloned().fold(0.0_f64, f64::max);
let centre = n / 2;
let window = (n / 20).max(1);
let local_min = self.signal[centre.saturating_sub(window)..=(centre + window).min(n - 1)]
.iter()
.cloned()
.fold(f64::INFINITY, f64::min);
let denom = peak + local_min;
if denom.abs() < 1e-30 {
return 0.0;
}
(peak - local_min) / denom
}
pub fn peak_to_background(&self) -> f64 {
let n = self.signal.len();
if n < 3 {
return 1.0;
}
let peak = self.signal.iter().cloned().fold(0.0_f64, f64::max);
let bg_len = (n / 10).max(1);
let bg_left: f64 = self.signal[..bg_len].iter().sum::<f64>() / bg_len as f64;
let bg_right: f64 = self.signal[n - bg_len..].iter().sum::<f64>() / bg_len as f64;
let bg = (bg_left + bg_right) * 0.5;
if bg.abs() < 1e-30 {
return f64::INFINITY;
}
peak / bg
}
pub fn estimated_chirp_fs2(&self) -> f64 {
let n = self.signal.len();
if n < 4 {
return 0.0;
}
let half = n / 2;
let left: f64 = self.signal[..half].iter().sum();
let right: f64 = self.signal[half..].iter().sum();
let total = left + right;
if total.abs() < 1e-30 {
return 0.0;
}
let asymmetry = (right - left) / total;
asymmetry * 1000.0
}
}
#[derive(Debug, Clone)]
pub struct CrossCorrelation {
pub delay_grid: Vec<f64>,
pub signal: Vec<f64>,
}
impl CrossCorrelation {
pub fn compute(field1: &[Complex64], field2: &[Complex64], dt: f64) -> Self {
let n1 = field1.len();
let n2 = field2.len();
let n = n1.min(n2); let n_out = 2 * n - 1;
let delay_grid: Vec<f64> = (0..n_out)
.map(|k| (k as f64 - (n - 1) as f64) * dt)
.collect();
let mut signal = vec![0.0_f64; n_out];
for (tau, sig) in signal.iter_mut().enumerate().take(n_out) {
let shift = tau as isize - (n as isize - 1);
let mut acc = Complex64::new(0.0, 0.0);
for (t, &f1t) in field1.iter().enumerate().take(n) {
let t2 = t as isize - shift;
if t2 >= 0 && (t2 as usize) < n {
acc += f1t * field2[t2 as usize].conj();
}
}
*sig = acc.norm_sqr() * dt * dt;
}
Self { delay_grid, signal }
}
pub fn fwhm_fs(&self) -> f64 {
if self.delay_grid.len() < 2 {
return 0.0;
}
let dt = (self.delay_grid[1] - self.delay_grid[0]).abs();
signal_fwhm_samples(&self.signal) * dt * 1e15
}
pub fn probe_duration_estimate_fs(&self, reference_duration_fs: f64) -> f64 {
let cc_fwhm = self.fwhm_fs();
let sq = cc_fwhm * cc_fwhm - reference_duration_fs * reference_duration_fs;
if sq <= 0.0 {
0.0
} else {
sq.sqrt()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
fn gaussian_intensity(n: usize, fwhm_samples: f64) -> Vec<f64> {
let sigma = fwhm_samples / (2.0 * (2.0_f64.ln()).sqrt());
(0..n)
.map(|i| {
let x = (i as f64) - (n as f64 / 2.0);
(-x * x / (sigma * sigma)).exp()
})
.collect()
}
fn gaussian_field(n: usize, fwhm_samples: f64) -> Vec<Complex64> {
let sigma = fwhm_samples / (2.0 * (2.0_f64.ln()).sqrt());
(0..n)
.map(|i| {
let x = (i as f64) - (n as f64 / 2.0);
Complex64::new((-0.5 * x * x / (sigma * sigma)).exp(), 0.0)
})
.collect()
}
#[test]
fn test_intensity_ac_peak_at_zero_delay() {
let n = 64;
let intensity = gaussian_intensity(n, 8.0);
let iac = IntensityAutocorrelation::compute(&intensity, 1e-15);
let centre = iac.signal.len() / 2;
let peak_idx = iac
.signal
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0);
assert!(
(peak_idx as isize - centre as isize).abs() <= 2,
"Autocorrelation peak should be at zero delay"
);
}
#[test]
fn test_intensity_ac_non_negative() {
let n = 32;
let intensity = gaussian_intensity(n, 6.0);
let iac = IntensityAutocorrelation::compute(&intensity, 1e-15);
for &val in &iac.signal {
assert!(val >= 0.0, "Autocorrelation must be non-negative");
}
}
#[test]
fn test_deconvolution_factors() {
assert_abs_diff_eq!(
IntensityAutocorrelation::deconvolution_factor(PulseShape::Gaussian),
1.0_f64 / 2.0_f64.sqrt(),
epsilon = 1e-10
);
assert_abs_diff_eq!(
IntensityAutocorrelation::deconvolution_factor(PulseShape::Sech2),
0.6482,
epsilon = 1e-10
);
assert_abs_diff_eq!(
IntensityAutocorrelation::deconvolution_factor(PulseShape::Rectangle),
1.0,
epsilon = 1e-10
);
}
#[test]
fn test_interferometric_ac_peak_to_background_near_eight() {
let n = 128;
let field = gaussian_field(n, 12.0);
let iac = InterferometricAutocorrelation::compute(&field, 1e-15);
let ptb = iac.peak_to_background();
assert!(
(5.0..=20.0).contains(&ptb),
"IAC peak-to-background should be near 8:1, got {ptb:.2}"
);
}
#[test]
fn test_cross_correlation_symmetric_identical_pulses() {
let n = 32;
let field = gaussian_field(n, 6.0);
let cc = CrossCorrelation::compute(&field, &field, 1e-15);
let m = cc.signal.len();
for i in 0..(m / 2) {
assert_abs_diff_eq!(cc.signal[i], cc.signal[m - 1 - i], epsilon = 1e-10);
}
}
#[test]
fn test_cross_correlation_fwhm_positive() {
let n = 64;
let f1 = gaussian_field(n, 8.0);
let f2 = gaussian_field(n, 8.0);
let cc = CrossCorrelation::compute(&f1, &f2, 1e-15);
let fwhm = cc.fwhm_fs();
assert!(fwhm > 0.0, "Cross-correlation FWHM should be positive");
}
#[test]
fn test_probe_duration_estimate() {
let n = 64;
let field = gaussian_field(n, 10.0);
let dt = 1e-15_f64;
let cc = CrossCorrelation::compute(&field, &field, dt);
let cc_fwhm = cc.fwhm_fs();
let probe = cc.probe_duration_estimate_fs(cc_fwhm);
assert_abs_diff_eq!(probe, 0.0, epsilon = 1e-8);
}
}