use tracing::trace;
use super::single_slit_intensity;
#[must_use]
#[inline]
pub fn fraunhofer_rect(
wavelength: f64,
width: f64,
height: f64,
angle_x: f64,
angle_y: f64,
i0: f64,
) -> f64 {
single_slit_intensity(wavelength, width, angle_x, 1.0)
* single_slit_intensity(wavelength, height, angle_y, 1.0)
* i0
}
#[must_use]
#[inline]
pub fn fraunhofer_1d(aperture: &[(f64, f64)], wavelength: f64, angle: f64) -> f64 {
let k_sin = std::f64::consts::TAU * angle.sin() / wavelength;
let mut real = 0.0;
let mut imag = 0.0;
for &(x, amplitude) in aperture {
let (sin_p, cos_p) = (k_sin * x).sin_cos();
real += amplitude * cos_p;
imag += amplitude * sin_p;
}
real * real + imag * imag
}
#[must_use]
#[inline]
pub fn fresnel_number(wavelength: f64, aperture_radius: f64, distance: f64) -> f64 {
aperture_radius * aperture_radius / (wavelength * distance)
}
#[must_use]
#[inline]
pub fn fresnel_integral_c(x: f64) -> f64 {
let ax = x.abs();
let result = if ax < 1.0 {
let x2 = ax * ax;
let t = std::f64::consts::FRAC_PI_2 * x2;
let t2 = t * t;
ax * (1.0 - t2 / 20.0 + t2 * t2 / 1680.0)
} else {
let pi_x2 = std::f64::consts::FRAC_PI_2 * ax * ax;
let (f, g) = fresnel_fg(ax);
0.5 + f * pi_x2.sin() - g * pi_x2.cos()
};
if x < 0.0 { -result } else { result }
}
#[must_use]
#[inline]
pub fn fresnel_integral_s(x: f64) -> f64 {
let ax = x.abs();
let result = if ax < 1.0 {
let x2 = ax * ax;
let t = std::f64::consts::FRAC_PI_2 * x2;
let t2 = t * t;
ax * x2 * (std::f64::consts::FRAC_PI_2 / 3.0) * (1.0 - t2 / 42.0 + t2 * t2 / 3960.0)
} else {
let pi_x2 = std::f64::consts::FRAC_PI_2 * ax * ax;
let (f, g) = fresnel_fg(ax);
0.5 - f * pi_x2.cos() - g * pi_x2.sin()
};
if x < 0.0 { -result } else { result }
}
#[inline]
fn fresnel_fg(x: f64) -> (f64, f64) {
let x2 = x * x;
let x3 = x2 * x;
let x4 = x2 * x2;
let f = (1.0 + 0.926 * x2) / (2.0 + 1.792 * x2 + 3.104 * x4) / x;
let g = 1.0 / (2.0 + 4.142 * x2 + 3.492 * x4 + 6.670 * x2 * x4) / x3;
(f, g)
}
#[must_use]
#[inline]
pub fn fresnel_integral_cs(x: f64) -> (f64, f64) {
let ax = x.abs();
let (c, s) = if ax < 1.0 {
let x2 = ax * ax;
let t = std::f64::consts::FRAC_PI_2 * x2;
let t2 = t * t;
let c_val = ax * (1.0 - t2 / 20.0 + t2 * t2 / 1680.0);
let s_val =
ax * x2 * (std::f64::consts::FRAC_PI_2 / 3.0) * (1.0 - t2 / 42.0 + t2 * t2 / 3960.0);
(c_val, s_val)
} else {
let pi_x2 = std::f64::consts::FRAC_PI_2 * ax * ax;
let (f, g) = fresnel_fg(ax);
let (sin_px2, cos_px2) = pi_x2.sin_cos();
let c_val = 0.5 + f * sin_px2 - g * cos_px2;
let s_val = 0.5 - f * cos_px2 - g * sin_px2;
(c_val, s_val)
};
if x < 0.0 { (-c, -s) } else { (c, s) }
}
#[must_use]
#[inline]
pub fn fresnel_edge_intensity(u: f64) -> f64 {
let (fc, fs) = fresnel_integral_cs(u);
let c = fc + 0.5;
let s = fs + 0.5;
(c * c + s * s) / 2.0
}
#[must_use]
#[inline]
pub fn fresnel_parameter(wavelength: f64, x: f64, distance: f64) -> f64 {
x * (2.0 / (wavelength * distance)).sqrt()
}
#[must_use]
#[inline]
pub fn huygens_fresnel_1d(aperture: &[(f64, f64)], wavelength: f64, z: f64, x_obs: f64) -> f64 {
let k = std::f64::consts::TAU / wavelength;
let mut real = 0.0;
let mut imag = 0.0;
for &(x_ap, amplitude) in aperture {
let dx = x_obs - x_ap;
let r = z.hypot(dx);
let (sin_p, cos_p) = (k * r).sin_cos();
let weight = amplitude * z / (r * r);
real += weight * cos_p;
imag += weight * sin_p;
}
real * real + imag * imag
}
#[must_use]
#[inline]
pub fn ar_ideal_index(n1: f64, n2: f64) -> f64 {
(n1 * n2).sqrt()
}
#[must_use]
#[inline]
pub fn ar_quarter_wave_thickness(wavelength: f64, n_coating: f64) -> f64 {
wavelength / (4.0 * n_coating)
}
#[must_use]
#[inline]
pub fn coating_reflectance(
wavelength: f64,
n_incident: f64,
n_coating: f64,
n_substrate: f64,
thickness: f64,
) -> f64 {
let delta = std::f64::consts::TAU * n_coating * thickness / wavelength;
let (sin_d, cos_d) = delta.sin_cos();
let a = n_incident * n_substrate - n_coating * n_coating;
let b = n_incident * n_substrate + n_coating * n_coating;
let c = n_coating * (n_incident + n_substrate);
(a * a * sin_d * sin_d) / (b * b * sin_d * sin_d + c * c * cos_d * cos_d)
}
#[must_use]
#[inline]
pub fn vcoat_reflectance(n1: f64, n2: f64, n3: f64) -> f64 {
let num = n1 * n3 - n2 * n2;
let den = n1 * n3 + n2 * n2;
(num / den) * (num / den)
}
#[must_use]
pub fn multilayer_reflectance(
wavelength: f64,
n_incident: f64,
n_substrate: f64,
layers: &[(f64, f64)],
) -> f64 {
trace!(
num_layers = layers.len(),
wavelength, "multilayer_reflectance"
);
let mut m11_r = 1.0;
let mut m12_i = 0.0; let mut m21_i = 0.0; let mut m22_r = 1.0;
for &(n, d) in layers {
let delta = std::f64::consts::TAU * n * d / wavelength;
let (sin_d, cos_d) = delta.sin_cos();
let new_m11_r = m11_r * cos_d - m12_i * n * sin_d;
let new_m12_i = -m11_r * sin_d / n + m12_i * cos_d;
let new_m21_i = m21_i * cos_d - m22_r * n * sin_d;
let new_m22_r = -m21_i * sin_d / n + m22_r * cos_d;
m11_r = new_m11_r;
m12_i = new_m12_i;
m21_i = new_m21_i;
m22_r = new_m22_r;
}
let ni = n_incident;
let ns = n_substrate;
let num_r = ni * m11_r - ns * m22_r;
let num_i = ni * ns * m12_i - m21_i;
let den_r = ni * m11_r + ns * m22_r;
let den_i = ni * ns * m12_i + m21_i;
(num_r * num_r + num_i * num_i) / (den_r * den_r + den_i * den_i)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ThinFilmResult {
pub r_s: f64,
pub r_p: f64,
pub t_s: f64,
pub t_p: f64,
pub r_avg: f64,
pub t_avg: f64,
}
#[must_use]
pub fn multilayer_rt(
wavelength: f64,
angle: f64,
n_incident: f64,
n_substrate: f64,
layers: &[(f64, f64)],
) -> ThinFilmResult {
trace!(
num_layers = layers.len(),
wavelength, angle, "multilayer_rt"
);
let cos_i = angle.cos();
let sin_i = angle.sin();
let ni_sin_i = n_incident * sin_i;
let cos_in_medium = |n: f64| -> f64 {
let sin_t = ni_sin_i / n;
if sin_t.abs() > 1.0 {
0.0 } else {
(1.0 - sin_t * sin_t).sqrt()
}
};
let cos_sub = cos_in_medium(n_substrate);
let tmm = |eta_fn: &dyn Fn(f64, f64) -> f64| -> (f64, f64) {
let mut m11_r = 1.0;
let mut m12_i = 0.0;
let mut m21_i = 0.0;
let mut m22_r = 1.0;
for &(n, d) in layers {
let cos_l = cos_in_medium(n);
let delta = std::f64::consts::TAU * n * d * cos_l / wavelength;
let eta = eta_fn(n, cos_l);
let (sin_d, cos_d) = delta.sin_cos();
let new_m11_r = m11_r * cos_d - m12_i * eta * sin_d;
let new_m12_i = -m11_r * sin_d / eta + m12_i * cos_d;
let new_m21_i = m21_i * cos_d - m22_r * eta * sin_d;
let new_m22_r = -m21_i * sin_d / eta + m22_r * cos_d;
m11_r = new_m11_r;
m12_i = new_m12_i;
m21_i = new_m21_i;
m22_r = new_m22_r;
}
let eta_i = eta_fn(n_incident, cos_i);
let eta_s = eta_fn(n_substrate, cos_sub);
let num_r = eta_i * m11_r - eta_s * m22_r;
let num_i = eta_i * eta_s * m12_i - m21_i;
let den_r = eta_i * m11_r + eta_s * m22_r;
let den_i = eta_i * eta_s * m12_i + m21_i;
let r = (num_r * num_r + num_i * num_i) / (den_r * den_r + den_i * den_i);
let t = 1.0 - r; (r, t)
};
let (r_s, t_s) = tmm(&|n, cos_t| n * cos_t); let (r_p, t_p) = tmm(&|n, cos_t| {
if cos_t.abs() < 1e-15 {
n * 1e15
} else {
n / cos_t
}
});
ThinFilmResult {
r_s,
r_p,
t_s,
t_p,
r_avg: 0.5 * (r_s + r_p),
t_avg: 0.5 * (t_s + t_p),
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-6;
#[test]
fn test_fraunhofer_rect_central_max() {
let i = fraunhofer_rect(550e-9, 1e-3, 1e-3, 0.0, 0.0, 1.0);
assert!((i - 1.0).abs() < EPS);
}
#[test]
fn test_fraunhofer_rect_decreases_off_axis() {
let i_center = fraunhofer_rect(550e-9, 1e-3, 1e-3, 0.0, 0.0, 1.0);
let i_off = fraunhofer_rect(550e-9, 1e-3, 1e-3, 0.001, 0.0, 1.0);
assert!(i_off < i_center);
}
#[test]
fn test_fraunhofer_rect_separable() {
let ix = single_slit_intensity(550e-9, 1e-3, 0.001, 1.0);
let iy = single_slit_intensity(550e-9, 0.5e-3, 0.002, 1.0);
let i_rect = fraunhofer_rect(550e-9, 1e-3, 0.5e-3, 0.001, 0.002, 1.0);
assert!((i_rect - ix * iy).abs() < EPS);
}
#[test]
fn test_fraunhofer_1d_uniform_aperture() {
let n = 100;
let width = 1e-3;
let aperture: Vec<(f64, f64)> = (0..n)
.map(|i| {
let x = -width / 2.0 + width * (i as f64) / (n as f64 - 1.0);
(x, 1.0)
})
.collect();
let i_center = fraunhofer_1d(&aperture, 550e-9, 0.0);
let i_off = fraunhofer_1d(&aperture, 550e-9, 0.001);
assert!(i_center > i_off, "Central max should be brightest");
assert!(i_center > 0.0);
}
#[test]
fn test_fraunhofer_1d_non_negative() {
let aperture: Vec<(f64, f64)> = (0..50).map(|i| ((i as f64 - 25.0) * 1e-5, 1.0)).collect();
for angle_mrad in 0..20 {
let angle = angle_mrad as f64 * 0.001;
let i = fraunhofer_1d(&aperture, 550e-9, angle);
assert!(i >= 0.0, "Negative intensity at angle {angle}");
}
}
#[test]
fn test_fresnel_number_far_field() {
let nf = fresnel_number(550e-9, 0.1e-3, 1.0);
assert!(nf < 1.0, "Should be Fraunhofer regime, N_F={nf}");
}
#[test]
fn test_fresnel_number_near_field() {
let nf = fresnel_number(550e-9, 5e-3, 0.01);
assert!(nf > 1.0, "Should be Fresnel regime, N_F={nf}");
}
#[test]
fn test_fresnel_c_at_zero() {
assert!(fresnel_integral_c(0.0).abs() < EPS);
}
#[test]
fn test_fresnel_s_at_zero() {
assert!(fresnel_integral_s(0.0).abs() < EPS);
}
#[test]
fn test_fresnel_c_converges_to_half() {
let c = fresnel_integral_c(10.0);
assert!((c - 0.5).abs() < 0.05, "C(10) should be ≈0.5, got {c}");
}
#[test]
fn test_fresnel_s_converges_to_half() {
let s = fresnel_integral_s(10.0);
assert!((s - 0.5).abs() < 0.05, "S(10) should be ≈0.5, got {s}");
}
#[test]
fn test_fresnel_c_odd_function() {
assert!((fresnel_integral_c(-2.0) + fresnel_integral_c(2.0)).abs() < 0.01);
}
#[test]
fn test_fresnel_s_odd_function() {
assert!((fresnel_integral_s(-2.0) + fresnel_integral_s(2.0)).abs() < 0.01);
}
#[test]
fn test_fresnel_c_known_value() {
let c = fresnel_integral_c(1.0);
assert!((c - 0.7799).abs() < 0.01, "C(1) ≈ 0.7799, got {c}");
}
#[test]
fn test_fresnel_s_known_value() {
let s = fresnel_integral_s(1.0);
assert!((s - 0.4383).abs() < 0.01, "S(1) ≈ 0.4383, got {s}");
}
#[test]
fn test_fresnel_edge_shadow_boundary() {
let i = fresnel_edge_intensity(0.0);
assert!((i - 0.25).abs() < 0.01, "At edge: I/I₀ ≈ 0.25, got {i}");
}
#[test]
fn test_fresnel_edge_deep_shadow() {
let i = fresnel_edge_intensity(-5.0);
assert!(i < 0.01, "Deep shadow should be ~0, got {i}");
}
#[test]
fn test_fresnel_edge_illuminated() {
let i = fresnel_edge_intensity(5.0);
assert!((i - 1.0).abs() < 0.05, "Illuminated region ≈ 1.0, got {i}");
}
#[test]
fn test_fresnel_parameter() {
let u = fresnel_parameter(550e-9, 1e-3, 1.0);
assert!(u > 0.0);
assert!(u.is_finite());
}
#[test]
fn test_huygens_fresnel_1d_positive() {
let aperture: Vec<(f64, f64)> = (0..50).map(|i| ((i as f64 - 25.0) * 1e-5, 1.0)).collect();
let i = huygens_fresnel_1d(&aperture, 550e-9, 0.1, 0.0);
assert!(i > 0.0);
}
#[test]
fn test_huygens_fresnel_1d_peak_on_axis() {
let aperture: Vec<(f64, f64)> = (0..100).map(|i| ((i as f64 - 50.0) * 1e-5, 1.0)).collect();
let i_center = huygens_fresnel_1d(&aperture, 550e-9, 0.1, 0.0);
let i_off = huygens_fresnel_1d(&aperture, 550e-9, 0.1, 1e-3);
assert!(i_center > i_off, "On-axis should be brightest");
}
#[test]
fn test_ar_ideal_index() {
let n = ar_ideal_index(1.0, 1.52);
assert!((n - 1.233).abs() < 0.001);
}
#[test]
fn test_ar_quarter_wave_thickness() {
let d = ar_quarter_wave_thickness(550.0, 1.38);
assert!((d - 550.0 / (4.0 * 1.38)).abs() < EPS);
}
#[test]
fn test_vcoat_ideal_zero_reflectance() {
let n1: f64 = 1.0;
let n3: f64 = 1.52;
let n2 = (n1 * n3).sqrt();
let r = vcoat_reflectance(n1, n2, n3);
assert!(r < EPS, "Ideal V-coat should have ~0 reflectance, got {r}");
}
#[test]
fn test_vcoat_mgf2_on_glass() {
let r = vcoat_reflectance(1.0, 1.38, 1.52);
assert!(
r > 0.0 && r < 0.02,
"MgF₂ V-coat ≈ 1.3% reflectance, got {r}"
);
}
#[test]
fn test_coating_reflectance_at_design_wavelength() {
let n1 = 1.0;
let n2 = 1.38;
let n3 = 1.52;
let wl = 550.0;
let d = ar_quarter_wave_thickness(wl, n2);
let r_coating = coating_reflectance(wl, n1, n2, n3, d);
let r_vcoat = vcoat_reflectance(n1, n2, n3);
assert!(
(r_coating - r_vcoat).abs() < 0.001,
"Coating at design λ should match V-coat: {r_coating} vs {r_vcoat}"
);
}
#[test]
fn test_coating_reflectance_varies_with_wavelength() {
let n2 = 1.38;
let n3 = 1.52;
let d = ar_quarter_wave_thickness(550.0, n2);
let r_design = coating_reflectance(550.0, 1.0, n2, n3, d);
let r_off = coating_reflectance(450.0, 1.0, n2, n3, d);
assert!(
(r_design - r_off).abs() > 0.001,
"Reflectance should vary with wavelength"
);
}
#[test]
fn test_coating_reflectance_range() {
let n2 = 1.38;
let n3 = 1.52;
let d = ar_quarter_wave_thickness(550.0, n2);
for wl_nm in (400..=700).step_by(10) {
let r = coating_reflectance(wl_nm as f64, 1.0, n2, n3, d);
assert!(
(0.0..=1.0).contains(&r),
"Reflectance out of range at {wl_nm}nm: {r}"
);
}
}
#[test]
fn test_multilayer_single_layer_matches_coating() {
let n1 = 1.0;
let n2 = 1.38;
let n3 = 1.52;
let wl = 550.0;
let d = ar_quarter_wave_thickness(wl, n2);
let r_single = coating_reflectance(wl, n1, n2, n3, d);
let r_multi = multilayer_reflectance(wl, n1, n3, &[(n2, d)]);
assert!(
(r_single - r_multi).abs() < 0.001,
"Single layer should match: coating={r_single}, multi={r_multi}"
);
}
#[test]
fn test_multilayer_no_layers() {
let r = multilayer_reflectance(550.0, 1.0, 1.52, &[]);
let r_bare = ((1.0_f64 - 1.52) / (1.0 + 1.52)).powi(2);
assert!(
(r - r_bare).abs() < 0.001,
"No layers = bare surface: {r} vs {r_bare}"
);
}
#[test]
fn test_multilayer_two_layer_valid() {
let wl = 550.0;
let n_mgf2 = 1.38;
let n_zro2 = 2.1;
let d1 = ar_quarter_wave_thickness(wl, n_zro2);
let d2 = ar_quarter_wave_thickness(wl, n_mgf2);
let r = multilayer_reflectance(wl, 1.0, 1.52, &[(n_mgf2, d2), (n_zro2, d1)]);
assert!(
(0.0..=1.0).contains(&r),
"Two-layer reflectance should be in [0,1], got {r}"
);
}
#[test]
fn test_multilayer_reflectance_range() {
let wl = 550.0;
let d = ar_quarter_wave_thickness(wl, 1.38);
let layers = [(1.38, d), (2.1, ar_quarter_wave_thickness(wl, 2.1))];
for wl_nm in (400..=700).step_by(10) {
let r = multilayer_reflectance(wl_nm as f64, 1.0, 1.52, &layers);
assert!(
(0.0..=1.0 + EPS).contains(&r),
"Reflectance out of range at {wl_nm}nm: {r}"
);
}
}
#[test]
fn test_tmm_normal_matches_simple() {
let wl = 550.0;
let n2 = 1.38;
let d = ar_quarter_wave_thickness(wl, n2);
let r_simple = multilayer_reflectance(wl, 1.0, 1.52, &[(n2, d)]);
let result = multilayer_rt(wl, 0.0, 1.0, 1.52, &[(n2, d)]);
assert!(
(result.r_avg - r_simple).abs() < 0.01,
"TMM at normal should match simple: {:.4} vs {:.4}",
result.r_avg,
r_simple
);
}
#[test]
fn test_tmm_sp_equal_at_normal() {
let result = multilayer_rt(550.0, 0.0, 1.0, 1.52, &[(1.38, 99.6)]);
assert!(
(result.r_s - result.r_p).abs() < 0.01,
"s={:.4}, p={:.4} should match at normal",
result.r_s,
result.r_p
);
}
#[test]
fn test_tmm_sp_differ_at_oblique() {
let angle = std::f64::consts::FRAC_PI_4;
let result = multilayer_rt(550.0, angle, 1.0, 1.52, &[(1.38, 99.6)]);
assert!(
(result.r_s - result.r_p).abs() > 0.001,
"s={:.4}, p={:.4} should differ at 45°",
result.r_s,
result.r_p
);
}
#[test]
fn test_tmm_energy_conservation() {
let result = multilayer_rt(550.0, 0.3, 1.0, 1.52, &[(1.38, 99.6), (2.1, 65.5)]);
assert!(
(result.r_s + result.t_s - 1.0).abs() < 0.01,
"R_s + T_s should ≈ 1: {:.4} + {:.4}",
result.r_s,
result.t_s
);
assert!(
(result.r_p + result.t_p - 1.0).abs() < 0.01,
"R_p + T_p should ≈ 1: {:.4} + {:.4}",
result.r_p,
result.t_p
);
}
#[test]
fn test_tmm_bare_surface_at_angle() {
let angle = 0.5; let result = multilayer_rt(550.0, angle, 1.0, 1.52, &[]);
let _r_bare = ((1.0_f64 - 1.52) / (1.0 + 1.52)).powi(2);
assert!(
(0.0..=1.0).contains(&result.r_s),
"R_s out of range: {}",
result.r_s
);
assert!(
(0.0..=1.0).contains(&result.r_p),
"R_p out of range: {}",
result.r_p
);
assert!(result.r_s >= result.r_p - 0.01);
}
#[test]
fn test_tmm_reflectance_range() {
for angle_deg in (0..=80).step_by(10) {
let angle = (angle_deg as f64).to_radians();
let result = multilayer_rt(550.0, angle, 1.0, 1.52, &[(1.38, 99.6)]);
assert!(
(0.0..=1.0 + 0.01).contains(&result.r_s),
"R_s={:.4} out of range at {angle_deg}°",
result.r_s
);
assert!(
(0.0..=1.0 + 0.01).contains(&result.r_p),
"R_p={:.4} out of range at {angle_deg}°",
result.r_p
);
}
}
}