use std::f64::consts::PI;
use super::caustic::{fit_parabola, BeamCaustic, BeamMeasurement};
use super::m2_factor::BeamQuality;
#[derive(Debug, Clone)]
pub struct Iso11146Measurement {
pub wavelength_m: f64,
pub measurements: Vec<BeamMeasurement>,
}
impl Iso11146Measurement {
pub fn new(wavelength_m: f64) -> Self {
Self {
wavelength_m,
measurements: Vec::new(),
}
}
pub fn add_point(&mut self, z_m: f64, d4sigma_x_m: f64, d4sigma_y_m: f64) {
self.measurements.push(BeamMeasurement {
z_m,
radius_x_m: d4sigma_x_m / 2.0,
radius_y_m: d4sigma_y_m / 2.0,
});
}
pub fn check_sampling(&self) -> bool {
if self.measurements.len() < 10 {
return false;
}
let caustic = self.to_caustic();
let bq = match caustic.extract_beam_quality_x() {
Some(b) => b,
None => return false,
};
let z0 = bq.waist_position_m;
let zr = bq.rayleigh_range_m();
let near: usize = self
.measurements
.iter()
.filter(|m| (m.z_m - z0).abs() <= zr)
.count();
let far: usize = self
.measurements
.iter()
.filter(|m| (m.z_m - z0).abs() >= 2.0 * zr)
.count();
near >= 5 && far >= 5
}
pub fn analyze(&self) -> Option<Iso11146Result> {
let caustic = self.to_caustic();
let bq_x = caustic.extract_beam_quality_x()?;
let bq_y = caustic.extract_beam_quality_y()?;
let astigmatic_difference_m = (bq_x.waist_position_m - bq_y.waist_position_m).abs();
Some(Iso11146Result {
m2_x: bq_x.m2,
m2_y: bq_y.m2,
waist_x_m: bq_x.beam_waist_m,
waist_y_m: bq_y.beam_waist_m,
waist_z_m: 0.5 * (bq_x.waist_position_m + bq_y.waist_position_m),
divergence_x_rad: bq_x.divergence_half_angle_rad(),
divergence_y_rad: bq_y.divergence_half_angle_rad(),
astigmatic_difference_m,
beam_parameter_product_x: bq_x.beam_parameter_product(),
beam_parameter_product_y: bq_y.beam_parameter_product(),
})
}
fn to_caustic(&self) -> BeamCaustic {
let mut caustic = BeamCaustic::new(self.wavelength_m);
for m in &self.measurements {
caustic.add_measurement(m.z_m, m.radius_x_m, m.radius_y_m);
}
caustic
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Iso11146Result {
pub m2_x: f64,
pub m2_y: f64,
pub waist_x_m: f64,
pub waist_y_m: f64,
pub waist_z_m: f64,
pub divergence_x_rad: f64,
pub divergence_y_rad: f64,
pub astigmatic_difference_m: f64,
pub beam_parameter_product_x: f64,
pub beam_parameter_product_y: f64,
}
pub fn synthetic_gaussian_caustic(
wavelength_m: f64,
m2: f64,
w0_m: f64,
z0_m: f64,
z_min: f64,
z_max: f64,
n_points: usize,
) -> Vec<(f64, f64)> {
let bq = BeamQuality::new(wavelength_m, m2, w0_m, z0_m);
(0..n_points)
.map(|i| {
let t = if n_points > 1 {
i as f64 / (n_points - 1) as f64
} else {
0.5
};
let z = z_min + t * (z_max - z_min);
let w = bq.beam_radius_at(z);
(z, w)
})
.collect()
}
pub fn count_near_far_samples(z: &[f64], w: &[f64], wavelength_m: f64) -> (usize, usize) {
let w2: Vec<f64> = w.iter().map(|&wi| wi * wi).collect();
let (a, b, c) = match fit_parabola(z, &w2) {
Some(coeffs) => coeffs,
None => return (0, 0),
};
if c <= 0.0 {
return (0, 0);
}
let z0 = -b / (2.0 * c);
let w0_sq = a - b * b / (4.0 * c);
if w0_sq <= 0.0 {
return (0, 0);
}
let w0 = w0_sq.sqrt();
let m2 = (PI * w0 * c.sqrt() / wavelength_m).max(1.0);
let zr = PI * w0 * w0 / (m2 * wavelength_m);
let near = z.iter().filter(|&&zi| (zi - z0).abs() <= zr).count();
let far = z.iter().filter(|&&zi| (zi - z0).abs() >= 2.0 * zr).count();
(near, far)
}
#[cfg(test)]
mod tests {
use super::*;
fn build_measurement(lambda: f64, m2: f64, w0: f64, z0: f64) -> Iso11146Measurement {
let bq = BeamQuality::new(lambda, m2, w0, z0);
let zr = bq.rayleigh_range_m();
let mut meas = Iso11146Measurement::new(lambda);
for i in 0..6usize {
let z = z0 - zr + 2.0 * zr * i as f64 / 5.0;
let w = bq.beam_radius_at(z);
meas.add_point(z, 2.0 * w, 2.0 * w);
}
for i in 0..6usize {
let z = z0 + 2.5 * zr + 2.0 * zr * i as f64 / 5.0;
let w = bq.beam_radius_at(z);
meas.add_point(z, 2.0 * w, 2.0 * w);
}
meas
}
#[test]
fn iso_analysis_recovers_m2() {
let lambda = 1064e-9;
let m2 = 1.5;
let w0 = 0.5e-3;
let meas = build_measurement(lambda, m2, w0, 0.0);
let result = meas.analyze().expect("ISO analysis should succeed");
assert!(
(result.m2_x - m2).abs() / m2 < 0.01,
"M²_x: got {}, expected {}",
result.m2_x,
m2
);
}
#[test]
fn iso_analysis_recovers_waist() {
let lambda = 1064e-9;
let w0 = 0.8e-3;
let meas = build_measurement(lambda, 1.0, w0, 0.05);
let result = meas.analyze().expect("ISO analysis should succeed");
assert!(
(result.waist_x_m - w0).abs() / w0 < 0.01,
"waist_x: got {}, expected {}",
result.waist_x_m,
w0
);
}
#[test]
fn bpp_satisfies_relation() {
let lambda = 532e-9;
let m2 = 2.0;
let meas = build_measurement(lambda, m2, 0.3e-3, 0.0);
let result = meas.analyze().expect("ISO analysis should succeed");
let expected_bpp = m2 * lambda / PI;
assert!(
(result.beam_parameter_product_x - expected_bpp).abs() / expected_bpp < 0.02,
"BPP: got {}, expected {}",
result.beam_parameter_product_x,
expected_bpp
);
}
#[test]
fn synthetic_gaussian_has_correct_waist() {
let lambda = 1064e-9;
let w0 = 1e-3;
let points = synthetic_gaussian_caustic(lambda, 1.0, w0, 0.0, -0.5, 0.5, 20);
let min_w = points.iter().map(|(_, w)| *w).fold(f64::INFINITY, f64::min);
assert!(
(min_w - w0).abs() / w0 < 0.05,
"Min radius: got {min_w}, expected {w0}"
);
}
#[test]
fn d4sigma_radius_conversion() {
let lambda = 1064e-9;
let mut meas = Iso11146Measurement::new(lambda);
meas.add_point(0.0, 2.0e-3, 3.0e-3);
assert!((meas.measurements[0].radius_x_m - 1.0e-3).abs() < 1e-15);
assert!((meas.measurements[0].radius_y_m - 1.5e-3).abs() < 1e-15);
}
#[test]
fn astigmatism_zero_for_symmetric_beam() {
let lambda = 1064e-9;
let meas = build_measurement(lambda, 1.2, 0.4e-3, 0.0);
let result = meas.analyze().expect("ISO analysis should succeed");
assert!(
result.astigmatic_difference_m < 1e-9,
"Astigmatism should be ~0 for symmetric beam, got {}",
result.astigmatic_difference_m
);
}
#[test]
fn count_near_far_helper() {
let lambda = 1064e-9;
let w0 = 1e-3;
let points = synthetic_gaussian_caustic(lambda, 1.0, w0, 0.0, -10.0, 10.0, 40);
let z: Vec<f64> = points.iter().map(|(z, _)| *z).collect();
let w: Vec<f64> = points.iter().map(|(_, w)| *w).collect();
let (near, far) = count_near_far_samples(&z, &w, lambda);
assert!(near > 0, "Should have near-waist samples, got {near}");
assert!(far > 0, "Should have far-field samples, got {far}");
}
}