use crate::error::OxiPhotonError;
use std::f64::consts::PI;
#[allow(dead_code)]
const C0: f64 = 2.99792458e8;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GratingType {
Transmission,
Reflection,
Echelle,
Volume,
}
#[derive(Debug, Clone)]
pub struct DiffractionGrating {
pub period_um: f64,
pub n_ambient: f64,
pub n_substrate: f64,
pub blaze_angle_deg: f64,
pub grating_type: GratingType,
pub groove_depth_nm: f64,
}
impl DiffractionGrating {
pub fn new(
period_um: f64,
n_medium: f64,
blaze_deg: f64,
grating_type: GratingType,
groove_depth_nm: f64,
) -> Result<Self, OxiPhotonError> {
if period_um <= 0.0 {
return Err(OxiPhotonError::InvalidLayer(format!(
"grating period must be > 0, got {period_um} μm"
)));
}
if n_medium <= 0.0 {
return Err(OxiPhotonError::InvalidRefractiveIndex {
n: n_medium,
k: 0.0,
});
}
if groove_depth_nm < 0.0 {
return Err(OxiPhotonError::InvalidLayer(format!(
"groove depth must be ≥ 0, got {groove_depth_nm} nm"
)));
}
Ok(Self {
period_um,
n_ambient: n_medium,
n_substrate: n_medium,
blaze_angle_deg: blaze_deg,
grating_type,
groove_depth_nm,
})
}
pub fn with_substrate_index(mut self, n_sub: f64) -> Self {
self.n_substrate = n_sub;
self
}
pub fn diffraction_angle_rad(
&self,
lambda_nm: f64,
order: i32,
incident_angle_rad: f64,
) -> Result<f64, OxiPhotonError> {
if lambda_nm <= 0.0 {
return Err(OxiPhotonError::InvalidWavelength(lambda_nm * 1e-9));
}
let lambda_um = lambda_nm * 1e-3;
let n_in = self.n_ambient;
let n_out = match self.grating_type {
GratingType::Transmission => self.n_substrate,
_ => self.n_ambient, };
let sin_theta_m =
(n_in * incident_angle_rad.sin() + order as f64 * lambda_um / self.period_um) / n_out;
if sin_theta_m.abs() > 1.0 {
return Err(OxiPhotonError::NumericalError(format!(
"order m={order} is evanescent at λ={lambda_nm} nm (sin θ_m = {sin_theta_m:.4})"
)));
}
Ok(sin_theta_m.asin())
}
pub fn propagating_orders(&self, lambda_nm: f64, incident_angle_rad: f64) -> Vec<i32> {
let lambda_um = lambda_nm * 1e-3;
let max_m = ((self.period_um * (self.n_ambient + 1.0) / lambda_um).ceil() as i32) + 1;
let mut orders = Vec::new();
for m in -max_m..=max_m {
if self
.diffraction_angle_rad(lambda_nm, m, incident_angle_rad)
.is_ok()
{
orders.push(m);
}
}
orders
}
pub fn blaze_efficiency(&self, lambda_nm: f64, order: i32, incident_angle_rad: f64) -> f64 {
let blaze_rad = self.blaze_angle_deg.to_radians();
let lambda_um = lambda_nm * 1e-3;
let sin_theta_d =
self.n_ambient * incident_angle_rad.sin() + order as f64 * lambda_um / self.period_um;
let blaze_lambda_um = self.period_um * (incident_angle_rad.sin() + blaze_rad.sin());
let x = order as f64 - blaze_lambda_um / lambda_um;
let eta = sinc_sq(x);
if self.blaze_angle_deg.abs() < 1e-9 && order == 0 {
return 1.0;
}
let shadow = match self.grating_type {
GratingType::Reflection | GratingType::Echelle => {
let cos_b = blaze_rad.cos().max(1e-9);
let cos_d = (sin_theta_d.clamp(-1.0, 1.0)).asin().cos().abs();
(cos_d / cos_b).clamp(0.0, 1.0)
}
_ => 1.0,
};
eta * shadow
}
pub fn blaze_wavelength_nm(&self, incident_angle_rad: f64) -> f64 {
let blaze_rad = self.blaze_angle_deg.to_radians();
let lambda_um = self.period_um * (incident_angle_rad.sin() + blaze_rad.sin());
lambda_um * 1e3 }
pub fn littrow_angle_rad(&self, lambda_nm: f64, order: i32) -> f64 {
let lambda_um = lambda_nm * 1e-3;
let arg = order as f64 * lambda_um / (2.0 * self.period_um * self.n_ambient);
if arg.abs() > 1.0 {
return f64::NAN;
}
-arg.asin()
}
pub fn littrow_angle_magnitude_rad(&self, lambda_nm: f64, order: i32) -> f64 {
self.littrow_angle_rad(lambda_nm, order).abs()
}
pub fn angular_dispersion_rad_per_nm(
&self,
lambda_nm: f64,
order: i32,
incident_angle_rad: f64,
) -> f64 {
match self.diffraction_angle_rad(lambda_nm, order, incident_angle_rad) {
Err(_) => 0.0,
Ok(theta_m) => {
let _lambda_um = lambda_nm * 1e-3;
let n_out = match self.grating_type {
GratingType::Transmission => self.n_substrate,
_ => self.n_ambient,
};
let cos_theta = theta_m.cos().abs().max(1e-12);
order as f64 / (self.period_um * 1e3 * cos_theta * n_out)
}
}
}
pub fn linear_dispersion_mm_per_nm(
&self,
lambda_nm: f64,
order: i32,
focal_length_mm: f64,
incident_angle_rad: f64,
) -> f64 {
self.angular_dispersion_rad_per_nm(lambda_nm, order, incident_angle_rad) * focal_length_mm
}
pub fn resolving_power(&self, order: i32, n_grooves: usize) -> f64 {
(order.abs() as f64) * n_grooves as f64
}
pub fn resolution_nm(&self, lambda_nm: f64, order: i32, n_grooves: usize) -> f64 {
let r = self.resolving_power(order, n_grooves);
if r < 1e-30 {
return f64::INFINITY;
}
lambda_nm / r
}
pub fn free_spectral_range_nm(&self, lambda_nm: f64, order: i32) -> f64 {
if order == 0 {
return f64::INFINITY;
}
lambda_nm / (order.abs() as f64)
}
pub fn useful_bandwidth_nm(&self, lambda_center_nm: f64, order: i32) -> f64 {
self.free_spectral_range_nm(lambda_center_nm, order)
}
pub fn woods_anomaly_wavelength_nm(&self, incident_angle_rad: f64) -> Vec<f64> {
let sin_i = incident_angle_rad.sin();
let mut anomalies = Vec::new();
for m in [-3i32, -2, -1, 1, 2, 3] {
for sign in [1.0_f64, -1.0] {
let lambda_um = self.period_um * self.n_ambient * (sign - sin_i) / (m as f64);
if lambda_um > 0.0 {
anomalies.push(lambda_um * 1e3); }
}
}
anomalies.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
anomalies
}
}
#[derive(Debug, Clone)]
pub struct HolographicGrating {
pub period_um: f64,
pub modulation_depth: f64,
pub n_medium: f64,
}
impl HolographicGrating {
pub fn new(period_um: f64, modulation: f64, n_medium: f64) -> Self {
Self {
period_um,
modulation_depth: modulation,
n_medium,
}
}
pub fn first_order_efficiency_thin(&self, lambda_nm: f64) -> f64 {
let lambda_um = lambda_nm * 1e-3;
let phi = PI * self.modulation_depth * self.period_um / lambda_um;
bessel_j1_sq(phi)
}
pub fn raman_nath_parameter(&self, lambda_nm: f64, thickness_um: f64) -> f64 {
let lambda_um = lambda_nm * 1e-3;
2.0 * PI * lambda_um * thickness_um / (self.n_medium * self.period_um * self.period_um)
}
pub fn bragg_condition_met(&self, lambda_nm: f64, angle_rad: f64) -> bool {
let lambda_um = lambda_nm * 1e-3;
let bragg_lambda = 2.0 * self.n_medium * self.period_um * angle_rad.sin();
(bragg_lambda - lambda_um).abs() < 0.05 * lambda_um }
pub fn diffraction_efficiency_thick(&self, lambda_nm: f64, thickness_um: f64) -> f64 {
let lambda_um = lambda_nm * 1e-3;
let bragg_angle = (lambda_um / (2.0 * self.n_medium * self.period_um))
.clamp(-1.0, 1.0)
.asin();
let cos_theta = bragg_angle.cos().max(1e-12);
let nu = PI * self.modulation_depth * thickness_um / (lambda_um * cos_theta);
nu.sin().powi(2)
}
}
#[derive(Debug, Clone)]
pub struct GratingSpectrometer {
pub grating: DiffractionGrating,
pub focal_length_mm: f64,
pub detector_width_mm: f64,
pub n_pixels: usize,
pub order: i32,
pub center_wavelength_nm: f64,
pub incident_angle_rad: f64,
}
impl GratingSpectrometer {
pub fn new(
grating: DiffractionGrating,
focal_mm: f64,
det_width_mm: f64,
n_pixels: usize,
order: i32,
center_nm: f64,
angle_rad: f64,
) -> Self {
Self {
grating,
focal_length_mm: focal_mm,
detector_width_mm: det_width_mm,
n_pixels,
order,
center_wavelength_nm: center_nm,
incident_angle_rad: angle_rad,
}
}
pub fn wavelength_range_nm(&self) -> (f64, f64) {
let disp = self.grating.linear_dispersion_mm_per_nm(
self.center_wavelength_nm,
self.order,
self.focal_length_mm,
self.incident_angle_rad,
);
if disp.abs() < 1e-30 {
return (self.center_wavelength_nm, self.center_wavelength_nm);
}
let half_range = self.detector_width_mm / (2.0 * disp.abs());
(
self.center_wavelength_nm - half_range,
self.center_wavelength_nm + half_range,
)
}
pub fn nm_per_pixel(&self) -> f64 {
if self.n_pixels == 0 {
return 0.0;
}
let (lmin, lmax) = self.wavelength_range_nm();
(lmax - lmin) / self.n_pixels as f64
}
pub fn pixel_limited_resolution_nm(&self) -> f64 {
self.nm_per_pixel()
}
pub fn etendue_mm2_sr(&self, beam_width_mm: f64) -> f64 {
let omega = (beam_width_mm / self.focal_length_mm).powi(2);
beam_width_mm * beam_width_mm * omega
}
pub fn wavelength_axis_nm(&self) -> Vec<f64> {
if self.n_pixels == 0 {
return Vec::new();
}
let (lmin, lmax) = self.wavelength_range_nm();
let dlambda = (lmax - lmin) / self.n_pixels as f64;
(0..self.n_pixels)
.map(|i| lmin + (i as f64 + 0.5) * dlambda)
.collect()
}
pub fn stray_light_db(&self) -> f64 {
match self.grating.grating_type {
GratingType::Transmission => -45.0,
GratingType::Volume => -55.0,
_ => -38.0,
}
}
}
#[derive(Debug, Clone)]
pub struct VolumeBraggGrating {
pub bragg_wavelength_nm: f64,
pub refractive_index: f64,
pub delta_n: f64,
pub thickness_mm: f64,
pub beam_diameter_mm: f64,
}
impl VolumeBraggGrating {
pub fn new(
lambda_b_nm: f64,
n_avg: f64,
delta_n: f64,
thickness_mm: f64,
beam_mm: f64,
) -> Self {
Self {
bragg_wavelength_nm: lambda_b_nm,
refractive_index: n_avg,
delta_n,
thickness_mm,
beam_diameter_mm: beam_mm,
}
}
pub fn peak_reflectivity(&self) -> f64 {
let lambda_um = self.bragg_wavelength_nm * 1e-3;
let thickness_um = self.thickness_mm * 1e3;
let kappa_d = PI * self.delta_n * thickness_um / lambda_um;
kappa_d.tanh().powi(2)
}
pub fn bandwidth_nm(&self) -> f64 {
let thickness_um = self.thickness_mm * 1e3;
let lambda_sq = self.bragg_wavelength_nm * self.bragg_wavelength_nm;
lambda_sq / (self.delta_n * thickness_um * 1e3)
}
pub fn angular_bandwidth_mrad(&self) -> f64 {
let lambda_um = self.bragg_wavelength_nm * 1e-3;
let thickness_um = self.thickness_mm * 1e3;
let period_um = lambda_um / (2.0 * self.refractive_index);
let bragg_angle = (lambda_um / (2.0 * self.refractive_index * period_um))
.clamp(-1.0, 1.0)
.asin();
let tan_b = bragg_angle.tan().abs().max(1e-12);
let delta_theta_rad = lambda_um / (self.refractive_index * thickness_um * tan_b);
delta_theta_rad * 1e3 }
pub fn reflection_spectrum(
&self,
lambda_min_nm: f64,
lambda_max_nm: f64,
n: usize,
) -> Vec<(f64, f64)> {
if n == 0 {
return Vec::new();
}
let r_peak = self.peak_reflectivity();
let bw_nm = self.bandwidth_nm();
let dlambda = if n > 1 {
(lambda_max_nm - lambda_min_nm) / (n - 1) as f64
} else {
0.0
};
(0..n)
.map(|i| {
let lam = lambda_min_nm + i as f64 * dlambda;
let delta_lam = lam - self.bragg_wavelength_nm;
let x = delta_lam / bw_nm;
let reflectivity = r_peak * sinc_sq(x);
(lam, reflectivity.clamp(0.0, 1.0))
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct DammannGrating {
pub period_um: f64,
pub n_spots: usize,
pub wavelength_nm: f64,
optimised_transitions: Option<Vec<f64>>,
optimised_n_orders: usize,
}
impl DammannGrating {
pub fn new(period_um: f64, n_spots: usize, lambda_nm: f64) -> Self {
Self {
period_um,
n_spots: n_spots.max(1),
wavelength_nm: lambda_nm,
optimised_transitions: None,
optimised_n_orders: 0,
}
}
pub fn transition_points(&self) -> Vec<f64> {
if let Some(ref t) = self.optimised_transitions {
return t.clone();
}
self.table_transition_points()
}
fn table_transition_points(&self) -> Vec<f64> {
match self.n_spots {
1 => vec![],
2 => vec![0.5],
3 => vec![1.0 / 6.0, 1.0 / 2.0, 5.0 / 6.0],
4 => vec![0.125, 0.375, 0.625, 0.875],
5 => {
vec![0.1, 0.35, 0.5, 0.65, 0.9]
}
n => {
(1..n).map(|k| k as f64 / n as f64).collect()
}
}
}
pub fn get_optimised_transitions(&self) -> Option<&[f64]> {
self.optimised_transitions.as_deref()
}
fn fourier_coefficients_static(
transitions: &[f64],
m_max: usize,
) -> Vec<num_complex::Complex64> {
let boundaries: Vec<f64> = {
let mut b = Vec::with_capacity(transitions.len() + 2);
b.push(0.0_f64);
b.extend_from_slice(transitions);
b.push(1.0_f64);
b
};
let n_strips = boundaries.len() - 1;
let mut coeffs = Vec::with_capacity(m_max + 1);
let c0_real: f64 = (0..n_strips)
.map(|i| {
let sign = if i % 2 == 0 { 1.0_f64 } else { -1.0_f64 };
sign * (boundaries[i + 1] - boundaries[i])
})
.sum();
coeffs.push(num_complex::Complex64::new(c0_real, 0.0));
for m in 1..=m_max {
let mf = m as f64;
let two_pi_m = 2.0 * PI * mf;
let mut c_re = 0.0_f64;
let mut c_im = 0.0_f64;
for i in 0..n_strips {
let sign = if i % 2 == 0 { 1.0_f64 } else { -1.0_f64 };
let angle_a = two_pi_m * boundaries[i];
let angle_b = two_pi_m * boundaries[i + 1];
let num_re = angle_a.cos() - angle_b.cos();
let num_im = -(angle_a.sin() - angle_b.sin());
c_re += sign * (num_im / two_pi_m);
c_im += sign * (-num_re / two_pi_m);
}
coeffs.push(num_complex::Complex64::new(c_re, c_im));
}
coeffs
}
fn half_to_full_transitions(half: &[f64]) -> Vec<f64> {
let mut full = Vec::with_capacity(2 * half.len());
for &t in half {
full.push(t);
}
for &t in half {
full.push(t + 0.5);
}
full.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
full
}
pub fn fourier_coefficients(&self, m_max: usize) -> Vec<num_complex::Complex64> {
let tp = self.transition_points();
Self::fourier_coefficients_static(&tp, m_max)
}
fn cost_and_grad_half(half: &[f64], n_orders: usize) -> (f64, Vec<f64>) {
let m_max_odd = 2 * n_orders - 1;
let cost = |h_pts: &[f64]| -> f64 {
let full = Self::half_to_full_transitions(h_pts);
let coeffs = Self::fourier_coefficients_static(&full, m_max_odd);
let powers: Vec<f64> = (0..n_orders)
.map(|k| coeffs[2 * k + 1].norm_sqr())
.collect();
let target = powers.iter().sum::<f64>() / n_orders as f64;
let variance =
powers.iter().map(|&p| (p - target).powi(2)).sum::<f64>() / n_orders as f64;
let mono_pen: f64 = h_pts
.windows(2)
.map(|w| (w[0] - w[1]).max(0.0).powi(2))
.sum();
variance + 1e3 * mono_pen
};
let j0 = cost(half);
let h = 1e-6_f64;
let mut grad = vec![0.0_f64; half.len()];
let mut h_pert = half.to_vec();
for i in 0..half.len() {
h_pert[i] = half[i] + 2.0 * h;
let jp2 = cost(&h_pert);
h_pert[i] = half[i] + h;
let jp1 = cost(&h_pert);
h_pert[i] = half[i] - h;
let jm1 = cost(&h_pert);
h_pert[i] = half[i] - 2.0 * h;
let jm2 = cost(&h_pert);
h_pert[i] = half[i]; grad[i] = (-jp2 + 8.0 * jp1 - 8.0 * jm1 + jm2) / (12.0 * h);
}
(j0, grad)
}
pub fn optimize_transitions(
&mut self,
n_orders: usize,
) -> Result<f64, crate::error::OxiPhotonError> {
if n_orders == 0 {
return Err(crate::error::OxiPhotonError::Convergence(
"n_orders must be ≥ 1".to_owned(),
));
}
let k = n_orders; let default_seed: Vec<f64> = (1..=k).map(|i| i as f64 / (2 * k + 2) as f64).collect();
let mut best_half = default_seed.clone();
let mut best_j = f64::INFINITY;
for restart in 0..5_usize {
let mut x: Vec<f64> = if restart == 0 {
default_seed.clone()
} else {
let seed = restart as f64 * 0.09;
(1..=k)
.map(|i| {
let base = i as f64 / (2 * k + 2) as f64;
(base + seed * (i as f64 * 1.618).sin()).clamp(0.001, 0.498)
})
.collect()
};
x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mut mu = 1e-2_f64;
for _iter in 0..300_usize {
let (j, grad) = Self::cost_and_grad_half(&x, n_orders);
let gnorm = grad.iter().map(|g| g.abs()).fold(0.0_f64, f64::max);
if gnorm < 1e-12 {
break;
}
let x_new: Vec<f64> = x
.iter()
.zip(grad.iter())
.map(|(&xi, &gi)| (xi - gi / (1.0 + mu)).clamp(0.001, 0.498))
.collect();
let (j_new, _) = Self::cost_and_grad_half(&x_new, n_orders);
if j_new < j {
x = x_new;
x.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
mu *= 0.5_f64.max(1e-12);
} else {
mu *= 2.0;
if mu > 1e12 {
break;
}
}
}
let (j_final, _) = Self::cost_and_grad_half(&x, n_orders);
if j_final < best_j {
best_j = j_final;
best_half = x;
}
}
let full = Self::half_to_full_transitions(&best_half);
self.optimised_transitions = Some(full);
self.optimised_n_orders = n_orders;
Ok(best_j)
}
pub fn efficiency(&self) -> f64 {
let tp = self.transition_points();
let n = if self.optimised_transitions.is_some() {
self.optimised_n_orders.max(1)
} else {
self.n_spots
};
let m_max = if self.optimised_transitions.is_some() {
2 * n - 1 } else {
n
};
let coeffs = Self::fourier_coefficients_static(&tp, m_max);
2.0 * coeffs[1..].iter().map(|c| c.norm_sqr()).sum::<f64>()
}
pub fn uniformity(&self) -> f64 {
let tp = self.transition_points();
let n = if self.optimised_transitions.is_some() {
self.optimised_n_orders.max(1)
} else {
self.n_spots
};
let m_max = if self.optimised_transitions.is_some() {
2 * n - 1
} else {
n
};
let coeffs = Self::fourier_coefficients_static(&tp, m_max);
let powers: Vec<f64> = if self.optimised_transitions.is_some() {
(0..n).map(|k| coeffs[2 * k + 1].norm_sqr()).collect()
} else {
coeffs[1..].iter().map(|c| c.norm_sqr()).collect()
};
if powers.is_empty() {
return 1.0;
}
let min = powers.iter().cloned().fold(f64::INFINITY, f64::min);
let max = powers.iter().cloned().fold(0.0_f64, f64::max);
if max < 1e-30 {
0.0
} else {
min / max
}
}
pub fn nonuniformity_metric(&self, _n_orders: usize) -> f64 {
1.0 - self.uniformity()
}
pub fn spot_angle_rad(&self, spot_idx: i32) -> f64 {
let lambda_um = self.wavelength_nm * 1e-3;
let arg = spot_idx as f64 * lambda_um / self.period_um;
if arg.abs() > 1.0 {
return f64::NAN;
}
arg.asin()
}
}
#[derive(Debug, Clone)]
pub struct VolumeGrating {
pub delta_n: f64,
pub thickness_m: f64,
pub bragg_wavelength_m: f64,
pub period_m: f64,
pub bragg_angle_rad: f64,
}
impl VolumeGrating {
pub fn new(
delta_n: f64,
thickness_m: f64,
bragg_wavelength_m: f64,
period_m: f64,
bragg_angle_rad: f64,
) -> Result<Self, OxiPhotonError> {
if thickness_m <= 0.0 {
return Err(OxiPhotonError::InvalidLayer(format!(
"VolumeGrating thickness must be > 0, got {thickness_m} m"
)));
}
if period_m <= 0.0 {
return Err(OxiPhotonError::InvalidLayer(format!(
"VolumeGrating period must be > 0, got {period_m} m"
)));
}
if bragg_wavelength_m <= 0.0 {
return Err(OxiPhotonError::InvalidWavelength(bragg_wavelength_m));
}
if delta_n < 0.0 {
return Err(OxiPhotonError::InvalidRefractiveIndex { n: delta_n, k: 0.0 });
}
Ok(Self {
delta_n,
thickness_m,
bragg_wavelength_m,
period_m,
bragg_angle_rad,
})
}
pub fn raman_nath_modulation(&self, wavelength_m: f64) -> f64 {
2.0 * PI * self.delta_n * self.thickness_m / wavelength_m
}
pub fn diffraction_orders(&self, m_max: usize, wavelength_m: f64) -> Vec<(i32, f64)> {
let nu = self.raman_nath_modulation(wavelength_m);
let j_vals = bessel_j_integer(m_max, nu);
let mut orders = Vec::with_capacity(2 * m_max + 1);
for m in 0..=m_max as i32 {
let eta = j_vals[m as usize].powi(2);
if m == 0 {
orders.push((0, eta));
} else {
orders.push((m, eta));
orders.push((-m, eta));
}
}
orders
}
pub fn first_order_efficiency_thin(&self, wavelength_m: f64) -> f64 {
let nu = self.raman_nath_modulation(wavelength_m);
let j_vals = bessel_j_integer(1, nu);
j_vals[1].powi(2)
}
fn coupling_constant(&self) -> f64 {
PI * self.delta_n / self.bragg_wavelength_m
}
fn detuning(&self, wavelength_m: f64) -> f64 {
PI * (self.bragg_wavelength_m - wavelength_m) / (self.bragg_wavelength_m * self.period_m)
}
pub fn reflection_spectrum(&self, wavelength_m: f64) -> f64 {
let kappa = self.coupling_constant();
let d = self.thickness_m;
let delta = self.detuning(wavelength_m);
let nu = kappa * d; let xi = delta * d;
if nu.abs() < 1e-30 {
return 0.0;
}
let nu2 = nu * nu;
let xi2 = xi * xi;
let ratio2 = xi2 / nu2;
if nu2 >= xi2 {
let gamma = (nu2 - xi2).sqrt();
if gamma < 1e-15 {
return nu.tanh().powi(2);
}
let sh = gamma.sinh();
let ch = gamma.cosh();
let numerator = sh * sh;
let denominator = ch * ch - ratio2;
if denominator < 1e-15 {
return 1.0_f64.min(numerator / 1e-15);
}
(numerator / denominator).clamp(0.0, 1.0)
} else {
let phi = (xi2 - nu2).sqrt();
let sn = phi.sin();
let numerator = sn * sn;
let denominator = numerator + ratio2 - 1.0;
if denominator.abs() < 1e-15 {
return 0.0;
}
if denominator <= 0.0 {
return 0.0;
}
(numerator / denominator).clamp(0.0, 1.0)
}
}
}
fn bessel_j_integer(m_max: usize, x: f64) -> Vec<f64> {
if x.abs() < 1e-15 {
let mut result = vec![0.0f64; m_max + 1];
result[0] = 1.0;
return result;
}
let m_extra = (x.abs() as usize).saturating_mul(3).max(30) + 20;
let m_start = m_max + m_extra;
let mut f = vec![0.0f64; m_start + 2];
f[m_start - 1] = 1.0;
for m in (1..=m_start - 1).rev() {
f[m - 1] = 2.0 * m as f64 / x * f[m] - f[m + 1];
if f[m - 1].abs() > 1e150 {
let scale = 1.0 / f[m - 1].abs();
for fi in f.iter_mut() {
*fi *= scale;
}
}
}
let sum_sq: f64 = f[0].powi(2) + 2.0 * f[1..=m_start].iter().map(|v| v.powi(2)).sum::<f64>();
let norm = sum_sq.sqrt();
if norm < 1e-300 {
let mut result = vec![0.0f64; m_max + 1];
result[0] = 1.0;
return result;
}
f[..=m_max].iter().map(|v| v / norm).collect()
}
fn sinc_sq(x: f64) -> f64 {
if x.abs() < 1e-12 {
return 1.0;
}
let pix = PI * x;
(pix.sin() / pix).powi(2)
}
fn bessel_j1_sq(x: f64) -> f64 {
let j1 = bessel_j1(x);
j1 * j1
}
fn bessel_j1(x: f64) -> f64 {
let ax = x.abs();
let sign = if x < 0.0 { -1.0 } else { 1.0 };
if ax < 1e-12 {
return 0.0;
}
if ax <= 3.0 {
let t = x / 3.0;
let t2 = t * t;
sign * (0.5 - 0.0625 * t2 + 0.002604_167 * t2 * t2 - 6.510_417e-5 * t2.powi(3)
+ 1.0286_458e-6 * t2.powi(4)
- 1.0942_96e-8 * t2.powi(5))
* x
} else {
(2.0 / (PI * ax)).sqrt() * (ax - 3.0 * PI / 4.0).cos() * sign
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_grating(period_um: f64) -> DiffractionGrating {
DiffractionGrating::new(period_um, 1.0, 0.0, GratingType::Reflection, 0.0).unwrap()
}
#[test]
fn test_grating_equation_zero_order() {
let g = make_grating(1.0); let theta_in = 0.3_f64.to_radians(); let theta_out = g.diffraction_angle_rad(500.0, 0, theta_in).unwrap();
let diff = (theta_out - theta_in).abs();
assert!(diff < 1e-10, "m=0 should give θ_out = θ_in, diff={diff}");
}
#[test]
fn test_grating_equation_first_order() {
let g = make_grating(1.0);
let theta_out = g.diffraction_angle_rad(500.0, 1, 0.0).unwrap();
let expected = (30.0_f64).to_radians();
assert!(
(theta_out - expected).abs() < 1e-8,
"θ_1 should be 30°, got {:.4}°",
theta_out.to_degrees()
);
}
#[test]
fn test_littrow_symmetry() {
let g = make_grating(1.6); let lambda_nm = 800.0;
let order = 1;
let theta_i = g.littrow_angle_rad(lambda_nm, order); assert!(theta_i.is_finite(), "Littrow angle should be finite");
let theta_d = g.diffraction_angle_rad(lambda_nm, order, theta_i).unwrap();
assert!(
(theta_d + theta_i).abs() < 1e-8,
"Littrow retroreflection not satisfied: θ_i={theta_i:.6}, θ_d={theta_d:.6}, expected θ_d=-θ_i"
);
let expected_mag = (order.abs() as f64 * lambda_nm * 1e-3 / (2.0 * 1.6)).asin();
assert!(
(theta_i.abs() - expected_mag).abs() < 1e-8,
"|θ_L| mismatch: got {:.6}, expected {expected_mag:.6}",
theta_i.abs()
);
}
#[test]
fn test_angular_dispersion_positive() {
let g = make_grating(1.0);
let disp = g.angular_dispersion_rad_per_nm(500.0, 1, 0.0);
assert!(disp > 0.0, "dθ/dλ should be positive for m=+1, got {disp}");
}
#[test]
fn test_resolving_power() {
let g = make_grating(1.0);
let r = g.resolving_power(2, 1000);
assert_eq!(r, 2000.0, "R = |m|·N = 2·1000 = 2000");
let r2 = g.resolving_power(-3, 500);
assert_eq!(r2, 1500.0, "R = |m|·N = 3·500 = 1500");
}
#[test]
fn test_fsr_formula() {
let g = make_grating(1.0);
let fsr = g.free_spectral_range_nm(500.0, 2);
let expected = 500.0 / 2.0;
assert!(
(fsr - expected).abs() < 1e-10,
"FSR={fsr}, expected {expected}"
);
let fsr_inf = g.free_spectral_range_nm(500.0, 0);
assert!(fsr_inf.is_infinite(), "FSR for m=0 must be infinite");
}
#[test]
fn test_blaze_efficiency_at_design_wavelength() {
let g = DiffractionGrating::new(
1.0,
1.0,
30.0, GratingType::Reflection,
200.0,
)
.unwrap();
let lambda_blaze = g.blaze_wavelength_nm(0.0);
let eta_blaze = g.blaze_efficiency(lambda_blaze, 1, 0.0);
assert!(
eta_blaze > 0.8,
"Blaze efficiency at design wavelength should be high, got {eta_blaze:.4}"
);
}
#[test]
fn test_woods_anomaly_exists() {
let g = make_grating(1.0);
let anomalies = g.woods_anomaly_wavelength_nm(0.0);
assert!(
!anomalies.is_empty(),
"Should have Wood's anomaly wavelengths"
);
for &lam in &anomalies {
assert!(lam > 0.0, "Anomaly wavelength must be positive, got {lam}");
}
}
#[test]
fn test_vbg_peak_reflectivity_range() {
let vbg = VolumeBraggGrating::new(1064.0, 1.487, 1e-3, 5.0, 2.0);
let r = vbg.peak_reflectivity();
assert!(
(0.0..=1.0).contains(&r),
"Reflectivity must be in [0,1], got {r}"
);
}
#[test]
fn test_vbg_bandwidth_positive() {
let vbg = VolumeBraggGrating::new(1064.0, 1.487, 1e-3, 5.0, 2.0);
let bw = vbg.bandwidth_nm();
assert!(bw > 0.0, "VBG bandwidth must be positive, got {bw}");
}
#[test]
fn test_spectrometer_wavelength_range() {
let g = DiffractionGrating::new(
1.0,
1.0,
26.74, GratingType::Reflection,
150.0,
)
.unwrap();
let spec = GratingSpectrometer::new(g, 100.0, 25.0, 2048, 1, 532.0, 0.0);
let (lmin, lmax) = spec.wavelength_range_nm();
assert!(lmin < lmax, "λ_min < λ_max required");
assert!(lmax - lmin > 1.0, "Wavelength range should be > 1 nm");
}
}