use std::f64::consts::PI;
#[allow(dead_code)]
const C_LIGHT: f64 = 2.998_924_58e8;
#[derive(Debug, Clone, PartialEq)]
pub struct BeamQuality {
pub wavelength_m: f64,
pub m2: f64,
pub beam_waist_m: f64,
pub waist_position_m: f64,
}
impl BeamQuality {
pub fn new(wavelength_m: f64, m2: f64, beam_waist_m: f64, waist_position_m: f64) -> Self {
Self {
wavelength_m,
m2: m2.max(1.0),
beam_waist_m,
waist_position_m,
}
}
pub fn rayleigh_range_m(&self) -> f64 {
PI * self.beam_waist_m * self.beam_waist_m / (self.m2 * self.wavelength_m)
}
pub fn beam_radius_at(&self, z_m: f64) -> f64 {
let zr = self.rayleigh_range_m();
let dz = z_m - self.waist_position_m;
let ratio = dz / zr;
self.beam_waist_m * (1.0 + ratio * ratio).sqrt()
}
pub fn divergence_half_angle_rad(&self) -> f64 {
self.m2 * self.wavelength_m / (PI * self.beam_waist_m)
}
pub fn beam_parameter_product(&self) -> f64 {
self.beam_waist_m * self.divergence_half_angle_rad()
}
pub fn strehl_ratio(&self) -> f64 {
let m2sq = self.m2 * self.m2;
1.0 / (m2sq * m2sq)
}
pub fn focus_spot_through_lens(&self, focal_length_m: f64, input_beam_radius_m: f64) -> f64 {
self.m2 * self.wavelength_m * focal_length_m / (PI * input_beam_radius_m)
}
pub fn propagate_through_abcd(&self, a: f64, b: f64, c: f64, d: f64) -> BeamQuality {
let zr = self.rayleigh_range_m();
let qr = 0.0_f64; let qi = zr;
let nr = a * qr + b;
let ni = a * qi;
let dr = c * qr + d;
let di = c * qi;
let denom_sq = dr * dr + di * di;
let qp_r = if denom_sq < 1e-300 {
0.0
} else {
(nr * dr + ni * di) / denom_sq
};
let qp_i = if denom_sq < 1e-300 {
zr
} else {
(ni * dr - nr * di) / denom_sq
};
let zr_new = qp_i.abs();
let z0_new = self.waist_position_m + qp_r;
let w0_new = ((zr_new * self.m2 * self.wavelength_m) / PI).sqrt();
BeamQuality {
wavelength_m: self.wavelength_m,
m2: self.m2,
beam_waist_m: w0_new,
waist_position_m: z0_new,
}
}
pub fn n_transverse_modes(&self) -> f64 {
self.m2
}
pub fn encircled_energy(&self, z_m: f64, r_m: f64) -> f64 {
let w = self.beam_radius_at(z_m);
1.0 - (-2.0 * r_m * r_m / (w * w)).exp()
}
}
pub fn laguerre_gaussian_m2(p: u32, l: i32) -> f64 {
(2 * p + l.unsigned_abs() + 1) as f64
}
pub fn hermite_gaussian_m2(m: u32, n: u32) -> (f64, f64) {
((2 * m + 1) as f64, (2 * n + 1) as f64)
}
pub fn brightness_w_per_m2_sr(power_w: f64, m2: f64, wavelength_m: f64) -> f64 {
let bpp = m2 * wavelength_m / PI;
power_w / (PI * bpp * bpp)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ideal_gaussian_bpp() {
let lambda = 1064e-9;
let bq = BeamQuality::new(lambda, 1.0, 1e-3, 0.0);
let bpp = bq.beam_parameter_product();
let expected = lambda / PI;
assert!(
(bpp - expected).abs() / expected < 1e-10,
"BPP mismatch: got {bpp}, expected {expected}"
);
}
#[test]
fn rayleigh_range_ideal() {
let lambda = 1064e-9;
let w0 = 1e-3;
let bq = BeamQuality::new(lambda, 1.0, w0, 0.0);
let zr = bq.rayleigh_range_m();
let expected = PI * w0 * w0 / lambda;
assert!(
(zr - expected).abs() / expected < 1e-10,
"zR mismatch: got {zr}, expected {expected}"
);
}
#[test]
fn radius_at_waist() {
let bq = BeamQuality::new(1064e-9, 1.5, 0.5e-3, 0.1);
let w = bq.beam_radius_at(0.1);
assert!(
(w - bq.beam_waist_m).abs() < 1e-15,
"Radius at waist should equal w₀, got {w}"
);
}
#[test]
fn radius_at_rayleigh_range() {
let bq = BeamQuality::new(1064e-9, 1.0, 1e-3, 0.0);
let zr = bq.rayleigh_range_m();
let w = bq.beam_radius_at(zr);
let expected = bq.beam_waist_m * 2.0_f64.sqrt();
assert!(
(w - expected).abs() / expected < 1e-10,
"Radius at zR should be √2 w₀: got {w}, expected {expected}"
);
}
#[test]
fn lg_mode_m2() {
assert_eq!(laguerre_gaussian_m2(0, 0), 1.0); assert_eq!(laguerre_gaussian_m2(0, 1), 2.0); assert_eq!(laguerre_gaussian_m2(1, 0), 3.0);
assert_eq!(laguerre_gaussian_m2(1, 1), 4.0);
}
#[test]
fn hg_mode_m2() {
assert_eq!(hermite_gaussian_m2(0, 0), (1.0, 1.0)); assert_eq!(hermite_gaussian_m2(1, 0), (3.0, 1.0));
assert_eq!(hermite_gaussian_m2(2, 3), (5.0, 7.0));
}
#[test]
fn encircled_energy_one_waist() {
let bq = BeamQuality::new(1064e-9, 1.0, 1e-3, 0.0);
let ee = bq.encircled_energy(0.0, bq.beam_waist_m);
let expected = 1.0 - (-2.0_f64).exp();
assert!(
(ee - expected).abs() < 1e-10,
"Encircled energy at w: got {ee}, expected {expected}"
);
}
#[test]
fn abcd_free_space_propagation() {
let bq = BeamQuality::new(1064e-9, 1.2, 0.5e-3, 0.0);
let propagated = bq.propagate_through_abcd(1.0, 0.5, 0.0, 1.0);
assert!(
(propagated.m2 - bq.m2).abs() < 1e-12,
"M² must be invariant under free-space propagation"
);
}
#[test]
fn brightness_scales_with_m2() {
let lambda = 1064e-9;
let power = 1.0;
let b1 = brightness_w_per_m2_sr(power, 1.0, lambda);
let b2 = brightness_w_per_m2_sr(power, 2.0, lambda);
let ratio = b1 / b2;
assert!(
(ratio - 4.0).abs() < 1e-10,
"Brightness ratio M²=1 vs M²=2 should be 4, got {ratio}"
);
}
}