use std::f64::consts::PI;
use super::m2_factor::BeamQuality;
#[derive(Debug, Clone, PartialEq)]
pub struct BeamMeasurement {
pub z_m: f64,
pub radius_x_m: f64,
pub radius_y_m: f64,
}
#[derive(Debug, Clone)]
pub struct BeamCaustic {
pub measurements: Vec<BeamMeasurement>,
pub wavelength_m: f64,
}
impl BeamCaustic {
pub fn new(wavelength_m: f64) -> Self {
Self {
measurements: Vec::new(),
wavelength_m,
}
}
pub fn add_measurement(&mut self, z_m: f64, radius_x_m: f64, radius_y_m: f64) {
self.measurements.push(BeamMeasurement {
z_m,
radius_x_m,
radius_y_m,
});
}
pub fn fit_parabola_x(&self) -> Option<(f64, f64, f64)> {
let z: Vec<f64> = self.measurements.iter().map(|m| m.z_m).collect();
let w2: Vec<f64> = self
.measurements
.iter()
.map(|m| m.radius_x_m * m.radius_x_m)
.collect();
fit_parabola(&z, &w2)
}
pub fn fit_parabola_y(&self) -> Option<(f64, f64, f64)> {
let z: Vec<f64> = self.measurements.iter().map(|m| m.z_m).collect();
let w2: Vec<f64> = self
.measurements
.iter()
.map(|m| m.radius_y_m * m.radius_y_m)
.collect();
fit_parabola(&z, &w2)
}
pub fn extract_beam_quality_x(&self) -> Option<BeamQuality> {
let (a, b, c) = self.fit_parabola_x()?;
beam_quality_from_parabola(a, b, c, self.wavelength_m)
}
pub fn extract_beam_quality_y(&self) -> Option<BeamQuality> {
let (a, b, c) = self.fit_parabola_y()?;
beam_quality_from_parabola(a, b, c, self.wavelength_m)
}
pub fn fit_residual_x(&self) -> f64 {
match self.fit_parabola_x() {
None => 0.0,
Some((a, b, c)) => {
let n = self.measurements.len();
if n == 0 {
return 0.0;
}
let sum_sq: f64 = self
.measurements
.iter()
.map(|m| {
let z = m.z_m;
let w2_meas = m.radius_x_m * m.radius_x_m;
let w2_fit = a + b * z + c * z * z;
let diff = w2_meas - w2_fit;
diff * diff
})
.sum();
(sum_sq / n as f64).sqrt()
}
}
}
pub fn fit_residual_y(&self) -> f64 {
match self.fit_parabola_y() {
None => 0.0,
Some((a, b, c)) => {
let n = self.measurements.len();
if n == 0 {
return 0.0;
}
let sum_sq: f64 = self
.measurements
.iter()
.map(|m| {
let z = m.z_m;
let w2_meas = m.radius_y_m * m.radius_y_m;
let w2_fit = a + b * z + c * z * z;
let diff = w2_meas - w2_fit;
diff * diff
})
.sum();
(sum_sq / n as f64).sqrt()
}
}
}
pub fn to_vec(&self) -> &Vec<BeamMeasurement> {
&self.measurements
}
}
pub(crate) fn fit_parabola(z: &[f64], w2: &[f64]) -> Option<(f64, f64, f64)> {
let n = z.len();
if n < 3 || n != w2.len() {
return None;
}
let nf = n as f64;
let sz1: f64 = z.iter().sum();
let sz2: f64 = z.iter().map(|&zi| zi * zi).sum();
let sz3: f64 = z.iter().map(|&zi| zi * zi * zi).sum();
let sz4: f64 = z.iter().map(|&zi| zi * zi * zi * zi).sum();
let sw: f64 = w2.iter().sum();
let szw: f64 = z.iter().zip(w2.iter()).map(|(&zi, &wi)| zi * wi).sum();
let sz2w: f64 = z.iter().zip(w2.iter()).map(|(&zi, &wi)| zi * zi * wi).sum();
let mut mat = [
[nf, sz1, sz2, sw],
[sz1, sz2, sz3, szw],
[sz2, sz3, sz4, sz2w],
];
for col in 0..3usize {
let mut max_val = mat[col][col].abs();
let mut max_row = col;
for (row, r_data) in mat.iter().enumerate().skip(col + 1) {
if r_data[col].abs() > max_val {
max_val = r_data[col].abs();
max_row = row;
}
}
if max_val < 1e-300 {
return None; }
mat.swap(col, max_row);
let pivot = mat[col][col];
for row in col + 1..3 {
let factor = mat[row][col] / pivot;
let pivot_row: Vec<f64> = mat[col][col..4].to_vec();
for (k_off, &pv) in pivot_row.iter().enumerate() {
mat[row][col + k_off] -= factor * pv;
}
}
}
let c = mat[2][3] / mat[2][2];
let b = (mat[1][3] - mat[1][2] * c) / mat[1][1];
let a = (mat[0][3] - mat[0][2] * c - mat[0][1] * b) / mat[0][0];
Some((a, b, c))
}
fn beam_quality_from_parabola(a: f64, b: f64, c: f64, wavelength_m: f64) -> Option<BeamQuality> {
if c <= 0.0 {
return None; }
let z0 = -b / (2.0 * c);
let w0_sq = a - b * b / (4.0 * c);
if w0_sq <= 0.0 {
return None; }
let w0 = w0_sq.sqrt();
let m2 = (PI * w0 * c.sqrt() / wavelength_m).max(1.0);
Some(BeamQuality::new(wavelength_m, m2, w0, z0))
}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic_caustic(lambda: f64, m2: f64, w0: f64, z0: f64) -> BeamCaustic {
let bq = BeamQuality::new(lambda, m2, w0, z0);
let mut caustic = BeamCaustic::new(lambda);
let zr = bq.rayleigh_range_m();
let n = 10usize;
for i in 0..n {
let z = z0 - 3.0 * zr + 6.0 * zr * i as f64 / (n - 1) as f64;
let w = bq.beam_radius_at(z);
caustic.add_measurement(z, w, w);
}
caustic
}
#[test]
fn fit_ideal_gaussian_recovers_parameters() {
let lambda = 1064e-9;
let w0 = 1e-3;
let z0 = 0.05;
let caustic = synthetic_caustic(lambda, 1.0, w0, z0);
let bq = caustic
.extract_beam_quality_x()
.expect("fit should succeed");
assert!(
(bq.beam_waist_m - w0).abs() / w0 < 1e-6,
"waist: got {}, expected {}",
bq.beam_waist_m,
w0
);
assert!(
(bq.waist_position_m - z0).abs() < 1e-9,
"z0: got {}, expected {}",
bq.waist_position_m,
z0
);
assert!(
(bq.m2 - 1.0).abs() < 1e-4,
"M²: got {}, expected 1.0",
bq.m2
);
}
#[test]
fn fit_m2_2_beam() {
let lambda = 1064e-9;
let m2 = 2.0;
let w0 = 0.5e-3;
let z0 = 0.0;
let caustic = synthetic_caustic(lambda, m2, w0, z0);
let bq = caustic
.extract_beam_quality_x()
.expect("fit should succeed");
assert!(
(bq.m2 - m2).abs() / m2 < 0.01,
"M²: got {}, expected {}",
bq.m2,
m2
);
}
#[test]
fn fit_parabola_exact_three_points() {
let z = vec![-1.0, 0.0, 1.0];
let w2: Vec<f64> = z.iter().map(|&zi| 1.0 + 2.0 * zi + 3.0 * zi * zi).collect();
let (a, b, c) = fit_parabola(&z, &w2).expect("fit should succeed");
assert!((a - 1.0).abs() < 1e-10, "a={}", a);
assert!((b - 2.0).abs() < 1e-10, "b={}", b);
assert!((c - 3.0).abs() < 1e-10, "c={}", c);
}
#[test]
fn fit_fails_with_too_few_points() {
let result = fit_parabola(&[0.0, 1.0], &[1.0, 2.0]);
assert!(result.is_none(), "Should fail with only 2 points");
}
#[test]
fn residual_exact_data_is_zero() {
let lambda = 1064e-9;
let caustic = synthetic_caustic(lambda, 1.0, 1e-3, 0.0);
let res = caustic.fit_residual_x();
assert!(
res < 1e-18,
"Residual for exact data should be near zero, got {}",
res
);
}
#[test]
fn add_measurement_and_retrieve() {
let mut c = BeamCaustic::new(1550e-9);
c.add_measurement(0.0, 1e-3, 1.1e-3);
c.add_measurement(0.1, 1.2e-3, 1.3e-3);
assert_eq!(c.to_vec().len(), 2);
assert!((c.to_vec()[0].z_m - 0.0).abs() < 1e-15);
}
#[test]
fn negative_z0_caustic() {
let lambda = 532e-9;
let caustic = synthetic_caustic(lambda, 1.5, 0.3e-3, -0.2);
let bq = caustic
.extract_beam_quality_x()
.expect("fit should succeed");
assert!(
(bq.waist_position_m - (-0.2)).abs() < 1e-6,
"z0: got {}",
bq.waist_position_m
);
}
}