#![allow(dead_code)]
#![allow(clippy::too_many_arguments)]
use std::f64::consts::PI;
const SPEED_OF_LIGHT: f64 = 2.997_924_58e8;
const MU_0: f64 = 1.256_637_061_4e-6;
const EPSILON_0: f64 = 8.854_187_817e-12;
const PLANCK_H: f64 = 6.626_070_15e-34;
const ELEM_CHARGE: f64 = 1.602_176_634e-19;
#[derive(Debug, Clone, Copy)]
pub struct OpticalMaterial {
pub n_real: f64,
pub n_imag: f64,
pub wavelength_nm: f64,
}
impl OpticalMaterial {
pub fn new(n: f64, k: f64, wl_nm: f64) -> Self {
Self {
n_real: n,
n_imag: k,
wavelength_nm: wl_nm,
}
}
pub fn absorption_coefficient(&self) -> f64 {
4.0 * PI * self.n_imag / (self.wavelength_nm * 1e-9)
}
pub fn reflectance_normal(&self) -> f64 {
let n = self.n_real;
let k = self.n_imag;
let num = (n - 1.0) * (n - 1.0) + k * k;
let den = (n + 1.0) * (n + 1.0) + k * k;
num / den
}
pub fn penetration_depth(&self) -> f64 {
if self.n_imag == 0.0 {
f64::INFINITY
} else {
self.wavelength_nm * 1e-9 / (4.0 * PI * self.n_imag)
}
}
pub fn optical_impedance(&self) -> f64 {
376.730_313_412 / self.n_real
}
}
pub fn fresnel_rs(n1: f64, n2: f64, theta_i: f64) -> f64 {
let cos_i = theta_i.cos();
let sin_i = theta_i.sin();
let sin_t_sq = (n1 / n2 * sin_i) * (n1 / n2 * sin_i);
if sin_t_sq > 1.0 {
return -1.0;
}
let cos_t = (1.0 - sin_t_sq).sqrt();
(n1 * cos_i - n2 * cos_t) / (n1 * cos_i + n2 * cos_t)
}
pub fn fresnel_rp(n1: f64, n2: f64, theta_i: f64) -> f64 {
let cos_i = theta_i.cos();
let sin_i = theta_i.sin();
let sin_t_sq = (n1 / n2 * sin_i) * (n1 / n2 * sin_i);
if sin_t_sq > 1.0 {
return -1.0;
}
let cos_t = (1.0 - sin_t_sq).sqrt();
(n2 * cos_i - n1 * cos_t) / (n2 * cos_i + n1 * cos_t)
}
pub fn fresnel_power(n1: f64, n2: f64, theta_i: f64) -> (f64, f64, f64, f64) {
let rs = fresnel_rs(n1, n2, theta_i);
let rp = fresnel_rp(n1, n2, theta_i);
let r_s = rs * rs;
let r_p = rp * rp;
let sin_i = theta_i.sin();
let sin_t_sq = (n1 / n2 * sin_i) * (n1 / n2 * sin_i);
if sin_t_sq > 1.0 {
return (1.0, 0.0, 1.0, 0.0);
}
let cos_i = theta_i.cos();
let cos_t = (1.0 - sin_t_sq).sqrt();
let factor = (n2 * cos_t) / (n1 * cos_i);
let ts_amp = 2.0 * n1 * cos_i / (n1 * cos_i + n2 * cos_t);
let tp_amp = 2.0 * n1 * cos_i / (n2 * cos_i + n1 * cos_t);
let t_s = factor * ts_amp * ts_amp;
let t_p = factor * tp_amp * tp_amp;
(r_s, t_s, r_p, t_p)
}
pub fn brewster_angle(n1: f64, n2: f64) -> f64 {
(n2 / n1).atan()
}
pub fn critical_angle(n1: f64, n2: f64) -> Option<f64> {
if n1 <= n2 {
None
} else {
Some((n2 / n1).asin())
}
}
pub fn sellmeier_equation(wavelength_um: f64, b: &[f64; 3], c: &[f64; 3]) -> f64 {
let lam2 = wavelength_um * wavelength_um;
let mut n2 = 1.0;
for i in 0..3 {
n2 += b[i] * lam2 / (lam2 - c[i]);
}
n2.sqrt()
}
pub fn cauchy_equation(wavelength_um: f64, a: f64, b: f64, c: f64) -> f64 {
let lam2 = wavelength_um * wavelength_um;
a + b / lam2 + c / (lam2 * lam2)
}
pub fn abbe_number(n_d: f64, n_f: f64, n_c: f64) -> f64 {
(n_d - 1.0) / (n_f - n_c)
}
pub fn glass_type_from_abbe(v: f64) -> &'static str {
if v > 55.0 {
"crown"
} else if v >= 35.0 {
"intermediate"
} else {
"flint"
}
}
pub fn group_refractive_index(n_wl: impl Fn(f64) -> f64, wl: f64, dwl: f64) -> f64 {
let dn_dlam = (n_wl(wl + dwl) - n_wl(wl - dwl)) / (2.0 * dwl);
n_wl(wl) - wl * dn_dlam
}
pub fn chromatic_dispersion(n_wl: impl Fn(f64) -> f64, wl: f64, dwl: f64) -> f64 {
let d2n = (n_wl(wl + dwl) - 2.0 * n_wl(wl) + n_wl(wl - dwl)) / (dwl * dwl);
-wl / SPEED_OF_LIGHT * d2n
}
pub fn beer_lambert(i0: f64, alpha: f64, length: f64) -> f64 {
i0 * (-alpha * length).exp()
}
pub fn absorbance(alpha: f64, length: f64) -> f64 {
alpha * length / std::f64::consts::LN_10
}
#[derive(Debug, Clone, Copy)]
pub struct BirefringentMaterial {
pub n_ordinary: f64,
pub n_extraordinary: f64,
}
impl BirefringentMaterial {
pub fn new(n_ordinary: f64, n_extraordinary: f64) -> Self {
Self {
n_ordinary,
n_extraordinary,
}
}
pub fn birefringence(&self) -> f64 {
self.n_extraordinary - self.n_ordinary
}
pub fn effective_index(&self, theta: f64) -> f64 {
let no = self.n_ordinary;
let ne = self.n_extraordinary;
let cos_t = theta.cos();
let sin_t = theta.sin();
1.0 / ((cos_t * cos_t / (no * no) + sin_t * sin_t / (ne * ne)).sqrt())
}
pub fn phase_retardation(&self, d_m: f64, wl_nm: f64) -> f64 {
2.0 * PI * self.birefringence().abs() * d_m / (wl_nm * 1e-9)
}
pub fn quarter_wave_thickness(&self, wl_nm: f64) -> f64 {
wl_nm * 1e-9 / (4.0 * self.birefringence().abs())
}
}
#[derive(Debug, Clone, Copy)]
pub struct ChiralMedium {
pub n_left: f64,
pub n_right: f64,
}
impl ChiralMedium {
pub fn new(n_left: f64, n_right: f64) -> Self {
Self { n_left, n_right }
}
pub fn specific_rotation(&self, wl_nm: f64) -> f64 {
PI * (self.n_left - self.n_right) / (wl_nm * 1e-9)
}
pub fn rotation_angle(&self, l_m: f64, wl_nm: f64) -> f64 {
self.specific_rotation(wl_nm) * l_m
}
pub fn mean_index(&self) -> f64 {
(self.n_left + self.n_right) / 2.0
}
}
pub fn photoelastic_birefringence(stress_optic_coeff: f64, sigma1: f64, sigma2: f64) -> f64 {
stress_optic_coeff * (sigma1 - sigma2)
}
pub fn photoelastic_fringe_order(delta_n: f64, d_m: f64, wl_nm: f64) -> f64 {
delta_n * d_m / (wl_nm * 1e-9)
}
pub fn pockels_index_change(n0: f64, r_eo: f64, e_field: f64) -> f64 {
-0.5 * n0 * n0 * n0 * r_eo * e_field
}
pub fn kerr_index_change(n0: f64, kerr_const: f64, wl_m: f64, e_field: f64) -> f64 {
0.5 * n0 * n0 * n0 * kerr_const * wl_m * e_field * e_field
}
pub fn pockels_half_wave_voltage(
wl_m: f64,
n0: f64,
r_eo: f64,
length_m: f64,
electrode_gap_m: f64,
) -> f64 {
wl_m * electrode_gap_m / (n0 * n0 * n0 * r_eo * length_m)
}
pub fn shg_efficiency(
d_eff: f64,
length_m: f64,
pump_intensity: f64,
n1: f64,
n2: f64,
wl_m: f64,
) -> f64 {
let omega = 2.0 * PI * SPEED_OF_LIGHT / wl_m;
let num = 2.0 * omega * omega * d_eff * d_eff * length_m * length_m * pump_intensity;
let den = n1 * n1 * n2 * SPEED_OF_LIGHT * SPEED_OF_LIGHT * SPEED_OF_LIGHT * EPSILON_0;
if den.abs() < 1e-60 { 0.0 } else { num / den }
}
pub fn shg_phase_matching_bandwidth(wl_m: f64, length_m: f64, n_group1: f64, n_group2: f64) -> f64 {
let delta_ng = (n_group1 - n_group2).abs();
if delta_ng < 1e-30 {
f64::INFINITY
} else {
wl_m * wl_m / (2.0 * length_m * delta_ng)
}
}
pub fn spm_phase_shift(
n2_nonlinear: f64,
wl_m: f64,
aeff_m2: f64,
peak_power_w: f64,
length_m: f64,
) -> f64 {
let gamma = 2.0 * PI * n2_nonlinear / (wl_m * aeff_m2);
gamma * peak_power_w * length_m
}
pub fn xpm_phase_shift(
n2_nonlinear: f64,
wl_m: f64,
aeff_m2: f64,
pump_power_w: f64,
length_m: f64,
) -> f64 {
2.0 * spm_phase_shift(n2_nonlinear, wl_m, aeff_m2, pump_power_w, length_m)
}
pub fn plqy(k_radiative: f64, k_nonradiative: f64) -> f64 {
let total = k_radiative + k_nonradiative;
if total == 0.0 {
0.0
} else {
k_radiative / total
}
}
pub fn fluorescence_lifetime(k_radiative: f64, k_nonradiative: f64) -> f64 {
1.0 / (k_radiative + k_nonradiative)
}
pub fn tauc_ordinate(alpha: f64, photon_energy_ev: f64, r: f64) -> f64 {
let alpha_hv = alpha * photon_energy_ev;
alpha_hv.powf(1.0 / r)
}
pub fn tauc_bandgap(slope: f64, intercept: f64) -> f64 {
if slope.abs() < 1e-30 {
f64::NAN
} else {
-intercept / slope
}
}
pub fn drude_dielectric(
epsilon_inf: f64,
omega_p: f64,
gamma_drude: f64,
omega: f64,
) -> (f64, f64) {
let denom = omega * omega + gamma_drude * gamma_drude;
let re = epsilon_inf - omega_p * omega_p / denom;
let im = omega_p * omega_p * gamma_drude / (omega * denom);
(re, im)
}
pub fn drude_plasma_frequency(carrier_density: f64, eff_mass: f64) -> f64 {
(carrier_density * ELEM_CHARGE * ELEM_CHARGE / (EPSILON_0 * eff_mass)).sqrt()
}
pub fn drude_refractive_index(epsilon_real: f64, epsilon_imag: f64) -> (f64, f64) {
let magnitude = (epsilon_real * epsilon_real + epsilon_imag * epsilon_imag).sqrt();
let n = ((magnitude + epsilon_real) / 2.0).sqrt();
let k = ((magnitude - epsilon_real) / 2.0).sqrt();
(n, k)
}
pub fn maxwell_garnett(eps_host: f64, eps_incl: f64, fill_fraction: f64) -> f64 {
let f = fill_fraction;
let num = eps_incl * (1.0 + 2.0 * f) + eps_host * 2.0 * (1.0 - f);
let den = eps_incl * (1.0 - f) + eps_host * (2.0 + f);
if den.abs() < 1e-30 {
eps_host
} else {
eps_host * num / den
}
}
pub fn bruggeman_effective_medium(eps1: f64, eps2: f64, f1: f64) -> f64 {
let f2 = 1.0 - f1;
let b = (f1 * (2.0 * eps1 - eps2) + f2 * (2.0 * eps2 - eps1)) / 3.0;
let disc = b * b + eps1 * eps2 / 9.0 + (f1 * eps1 + f2 * eps2) / 3.0;
let mut eps_eff = f1 * eps1 + f2 * eps2;
for _ in 0..100 {
let t1 = f1 * (eps1 - eps_eff) / (eps1 + 2.0 * eps_eff);
let t2 = f2 * (eps2 - eps_eff) / (eps2 + 2.0 * eps_eff);
let _b_unused = b;
let _disc_unused = disc;
let residual = t1 + t2;
let dt1 = f1 * (-3.0 * eps1) / ((eps1 + 2.0 * eps_eff) * (eps1 + 2.0 * eps_eff));
let dt2 = f2 * (-3.0 * eps2) / ((eps2 + 2.0 * eps_eff) * (eps2 + 2.0 * eps_eff));
let drv = dt1 + dt2;
if drv.abs() < 1e-30 {
break;
}
let step = residual / drv;
eps_eff -= step;
if step.abs() < 1e-12 {
break;
}
}
eps_eff
}
pub fn photonic_crystal_1d_bandgap(n1: f64, n2: f64, d1: f64, d2: f64) -> (f64, f64) {
let lambda_center = 2.0 * (n1 * d1 + n2 * d2);
let fractional_gap = (4.0 / PI) * ((n1 - n2).abs() / (n1 + n2)).asin();
(lambda_center, fractional_gap)
}
pub fn transfer_matrix_layer(n: f64, delta: f64) -> [f64; 4] {
let cos_d = delta.cos();
let sin_d = delta.sin();
[cos_d, -sin_d / n, -n * sin_d, cos_d]
}
pub fn transfer_matrix_multiply(a: [f64; 4], b: [f64; 4]) -> [f64; 4] {
[
a[0] * b[0] + a[1] * b[2],
a[0] * b[1] + a[1] * b[3],
a[2] * b[0] + a[3] * b[2],
a[2] * b[1] + a[3] * b[3],
]
}
pub fn bragg_mirror_reflectance(
n0: f64,
ns: f64,
n1: f64,
n2: f64,
d1: f64,
d2: f64,
wl_m: f64,
n_periods: usize,
) -> f64 {
let delta1 = 2.0 * PI * n1 * d1 / wl_m;
let delta2 = 2.0 * PI * n2 * d2 / wl_m;
let m1 = transfer_matrix_layer(n1, delta1);
let m2 = transfer_matrix_layer(n2, delta2);
let bilayer = transfer_matrix_multiply(m1, m2);
let mut result = [1.0_f64, 0.0, 0.0, 1.0]; let mut base = bilayer;
let mut exp = n_periods;
while exp > 0 {
if exp % 2 == 1 {
result = transfer_matrix_multiply(result, base);
}
base = transfer_matrix_multiply(base, base);
exp /= 2;
}
let num = n0 * result[0] + n0 * ns * result[1] - result[2] - ns * result[3];
let den = n0 * result[0] + n0 * ns * result[1] + result[2] + ns * result[3];
if den.abs() < 1e-30 {
1.0
} else {
(num / den) * (num / den)
}
}
#[derive(Debug, Clone, Copy)]
pub struct AntiReflectionCoating {
pub n_substrate: f64,
pub n_coating: f64,
pub thickness_nm: f64,
pub wavelength_nm: f64,
}
impl AntiReflectionCoating {
pub fn new(n_substrate: f64, n_coating: f64, thickness_nm: f64, wavelength_nm: f64) -> Self {
Self {
n_substrate,
n_coating,
thickness_nm,
wavelength_nm,
}
}
pub fn optimal_n(&self) -> f64 {
self.n_substrate.sqrt()
}
pub fn reflectance(&self, wl_nm: f64) -> f64 {
let r1 = (1.0 - self.n_coating) / (1.0 + self.n_coating);
let r2 = (self.n_coating - self.n_substrate) / (self.n_coating + self.n_substrate);
let delta = 4.0 * PI * self.n_coating * self.thickness_nm / wl_nm;
let cos_d = delta.cos();
let num = r1 * r1 + r2 * r2 + 2.0 * r1 * r2 * cos_d;
let den = 1.0 + r1 * r1 * r2 * r2 + 2.0 * r1 * r2 * cos_d;
if den.abs() < 1e-30 {
0.0
} else {
(num / den).abs()
}
}
}
pub fn etalon_transmission(r: f64, delta: f64) -> f64 {
let f_coeff = 4.0 * r / ((1.0 - r) * (1.0 - r));
let half_sin = (delta / 2.0).sin();
1.0 / (1.0 + f_coeff * half_sin * half_sin)
}
pub fn finesse(r: f64) -> f64 {
PI * r.sqrt() / (1.0 - r)
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-9;
#[test]
fn test_reflectance_normal_range() {
let mat = OpticalMaterial::new(1.5, 0.0, 500.0);
let r = mat.reflectance_normal();
assert!(
(0.0..=1.0).contains(&r),
"Reflectance must be in [0,1], got {r}"
);
}
#[test]
fn test_reflectance_glass() {
let mat = OpticalMaterial::new(1.5, 0.0, 550.0);
let r = mat.reflectance_normal();
let expected = (0.5 / 2.5) * (0.5 / 2.5);
assert!(
(r - expected).abs() < EPS,
"Glass reflectance: {r} vs {expected}"
);
}
#[test]
fn test_absorption_zero_for_transparent() {
let mat = OpticalMaterial::new(1.5, 0.0, 500.0);
assert_eq!(mat.absorption_coefficient(), 0.0, "No absorption when k=0");
}
#[test]
fn test_absorption_positive_for_absorbing() {
let mat = OpticalMaterial::new(2.0, 0.5, 500.0);
let alpha = mat.absorption_coefficient();
assert!(
alpha > 0.0,
"Absorption coefficient must be positive for k>0, got {alpha}"
);
}
#[test]
fn test_penetration_depth_transparent() {
let mat = OpticalMaterial::new(1.5, 0.0, 500.0);
assert!(
mat.penetration_depth().is_infinite(),
"Non-absorbing material: infinite penetration depth"
);
}
#[test]
fn test_penetration_depth_decreases_with_k() {
let mat1 = OpticalMaterial::new(2.0, 0.1, 500.0);
let mat2 = OpticalMaterial::new(2.0, 1.0, 500.0);
assert!(
mat2.penetration_depth() < mat1.penetration_depth(),
"Larger k → smaller penetration depth"
);
}
#[test]
fn test_optical_impedance_positive() {
let mat = OpticalMaterial::new(1.5, 0.0, 500.0);
let z = mat.optical_impedance();
assert!(z > 0.0, "Optical impedance must be positive, got {z}");
}
#[test]
fn test_optical_impedance_vacuum() {
let mat = OpticalMaterial::new(1.0, 0.0, 500.0);
let z = mat.optical_impedance();
assert!(
(z - 376.730_313_412).abs() < 1e-6,
"Vacuum impedance mismatch: {z}"
);
}
#[test]
fn test_brewster_angle_positive() {
let theta_b = brewster_angle(1.0, 1.5);
assert!(
theta_b > 0.0,
"Brewster angle must be positive, got {theta_b}"
);
}
#[test]
fn test_fresnel_rp_zero_at_brewster() {
let n1 = 1.0_f64;
let n2 = 1.5_f64;
let theta_b = brewster_angle(n1, n2);
let rp = fresnel_rp(n1, n2, theta_b);
assert!(
rp.abs() < 1e-10,
"r_p should be zero at Brewster angle, got {rp}"
);
}
#[test]
fn test_fresnel_rs_normal_incidence() {
let n1 = 1.0_f64;
let n2 = 1.5_f64;
let rs = fresnel_rs(n1, n2, 0.0);
let expected = (n1 - n2) / (n1 + n2);
assert!(
(rs - expected).abs() < EPS,
"r_s at normal incidence: {rs} vs {expected}"
);
}
#[test]
fn test_critical_angle_none_for_low_n1() {
assert!(critical_angle(1.0, 1.5).is_none(), "No TIR when n1 < n2");
}
#[test]
fn test_critical_angle_some_for_high_n1() {
let theta_c = critical_angle(1.5, 1.0);
assert!(theta_c.is_some(), "TIR exists when n1 > n2");
}
#[test]
fn test_critical_angle_formula() {
let n1 = 1.5_f64;
let n2 = 1.0_f64;
let theta_c = critical_angle(n1, n2).unwrap();
let expected = (n2 / n1).asin();
assert!(
(theta_c - expected).abs() < EPS,
"Critical angle: {theta_c} vs {expected}"
);
}
#[test]
fn test_sellmeier_bk7_visible() {
let b = [1.03961212, 0.231792344, 1.01046945];
let c = [0.00600069867, 0.0200179144, 103.560653];
let n = sellmeier_equation(0.589, &b, &c);
assert!(n > 1.0, "Sellmeier should give n > 1, got {n}");
assert!(
(n - 1.5168).abs() < 0.001,
"BK7 at 589 nm: expected ~1.517, got {n}"
);
}
#[test]
fn test_abbe_number_crown() {
let v = abbe_number(1.5168, 1.5224, 1.5143);
assert!(
v > 55.0,
"BK7 Abbe number should be crown glass (>55), got {v:.2}"
);
assert_eq!(glass_type_from_abbe(v), "crown");
}
#[test]
fn test_abbe_number_flint() {
let v = abbe_number(1.7, 1.73, 1.68);
assert!(v < 35.0, "Flint glass Abbe number < 35, got {v:.2}");
assert_eq!(glass_type_from_abbe(v), "flint");
}
#[test]
fn test_beer_lambert_decay() {
let i1 = beer_lambert(1.0, 100.0, 0.01);
let i2 = beer_lambert(1.0, 100.0, 0.02);
assert!(
i2 < i1,
"Beer-Lambert: intensity decreases with path length"
);
assert!(
i1 > 0.0 && i2 > 0.0,
"Transmitted intensity must be positive"
);
}
#[test]
fn test_beer_lambert_zero_length() {
let i = beer_lambert(5.0, 100.0, 0.0);
assert!(
(i - 5.0).abs() < EPS,
"Beer-Lambert at zero path: {i} vs 5.0"
);
}
#[test]
fn test_birefringence_delta_n() {
let mat = BirefringentMaterial::new(1.658, 1.486); let delta = mat.birefringence();
let expected = 1.486 - 1.658;
assert!(
(delta - expected).abs() < EPS,
"Birefringence mismatch: {delta}"
);
}
#[test]
fn test_birefringence_effective_index_at_zero() {
let mat = BirefringentMaterial::new(1.658, 1.486);
let n_eff = mat.effective_index(0.0);
assert!(
(n_eff - mat.n_ordinary).abs() < 1e-6,
"At θ=0, n_eff should equal n_o: {n_eff} vs {}",
mat.n_ordinary
);
}
#[test]
fn test_phase_retardation_zero_thickness() {
let mat = BirefringentMaterial::new(1.5, 1.6);
let gamma = mat.phase_retardation(0.0, 500.0);
assert!(gamma.abs() < EPS, "Zero thickness → zero retardation");
}
#[test]
fn test_optical_rotation_scales_with_length() {
let chiral = ChiralMedium::new(1.500, 1.501);
let phi1 = chiral.rotation_angle(0.01, 500.0);
let phi2 = chiral.rotation_angle(0.02, 500.0);
assert!(
(phi2 - 2.0 * phi1).abs() < 1e-10,
"Rotation scales linearly with length"
);
}
#[test]
fn test_pockels_sign() {
let dn_pos = pockels_index_change(2.0, 3e-12, 1e7);
let dn_neg = pockels_index_change(2.0, 3e-12, -1e7);
assert!(
dn_pos < 0.0,
"Pockels: positive field → negative Δn for positive r"
);
assert!(
(dn_pos + dn_neg).abs() < EPS,
"Pockels: Δn is antisymmetric in E"
);
}
#[test]
fn test_kerr_quadratic() {
let dn1 = kerr_index_change(1.5, 1e-12, 500e-9, 1e6);
let dn2 = kerr_index_change(1.5, 1e-12, 500e-9, -1e6);
assert!((dn1 - dn2).abs() < EPS, "Kerr effect is symmetric in E");
let dn_double = kerr_index_change(1.5, 1e-12, 500e-9, 2e6);
assert!(
(dn_double - 4.0 * dn1).abs() < 1e-30,
"Kerr scales as E²: dn(2E)=4*dn(E)"
);
}
#[test]
fn test_plqy_range() {
let q = plqy(1e8, 1e7);
assert!((0.0..=1.0).contains(&q), "PLQY must be in [0,1], got {q}");
}
#[test]
fn test_plqy_unity_no_nonradiative() {
let q = plqy(1e9, 0.0);
assert!(
(q - 1.0).abs() < EPS,
"PLQY = 1 with no non-radiative decay, got {q}"
);
}
#[test]
fn test_drude_below_plasma_freq() {
let omega_p = 1e15_f64;
let (re, _im) = drude_dielectric(1.0, omega_p, 1e13, omega_p * 0.5);
assert!(
re < 0.0,
"Drude ε_real < 0 below plasma frequency, got {re}"
);
}
#[test]
fn test_drude_imag_positive() {
let (_re, im) = drude_dielectric(1.0, 1e15, 1e13, 5e14);
assert!(im > 0.0, "Drude ε_imag must be positive, got {im}");
}
#[test]
fn test_maxwell_garnett_zero_fill() {
let eps_eff = maxwell_garnett(2.25, 10.0, 0.0);
assert!(
(eps_eff - 2.25).abs() < EPS,
"Maxwell Garnett at f=0 should give ε_host: {eps_eff}"
);
}
#[test]
fn test_maxwell_garnett_full_fill() {
let eps_eff = maxwell_garnett(2.25, 10.0, 1.0);
assert!(
(eps_eff - 10.0).abs() < EPS,
"Maxwell Garnett at f=1 should give ε_incl: {eps_eff}"
);
}
#[test]
fn test_photonic_crystal_bandgap_scaling() {
let (lambda1, _) = photonic_crystal_1d_bandgap(2.0, 1.5, 100e-9, 100e-9);
let (lambda2, _) = photonic_crystal_1d_bandgap(2.0, 1.5, 200e-9, 200e-9);
assert!(
(lambda2 - 2.0 * lambda1).abs() < 1e-20,
"Bandgap centre scales with period: {lambda1} vs {lambda2}"
);
}
#[test]
fn test_bragg_mirror_periods() {
let r5 = bragg_mirror_reflectance(1.0, 1.0, 2.3, 1.5, 60e-9, 90e-9, 600e-9, 5);
let r10 = bragg_mirror_reflectance(1.0, 1.0, 2.3, 1.5, 60e-9, 90e-9, 600e-9, 10);
assert!(
r10 >= r5,
"More periods → higher Bragg reflectance: R5={r5:.4}, R10={r10:.4}"
);
}
#[test]
fn test_etalon_transmission_range() {
let t = etalon_transmission(0.9, 1.2);
assert!(
(0.0..=1.0).contains(&t),
"Etalon T must be in [0,1], got {t}"
);
}
#[test]
fn test_etalon_transmission_at_resonance() {
let t = etalon_transmission(0.9, 0.0);
assert!((t - 1.0).abs() < EPS, "Etalon T = 1 at resonance, got {t}");
}
#[test]
fn test_finesse_increases_with_r() {
let f1 = finesse(0.5);
let f2 = finesse(0.9);
assert!(
f2 > f1,
"Finesse should increase with reflectance: f1={f1}, f2={f2}"
);
}
#[test]
fn test_ar_coating_optimal_n() {
let coating = AntiReflectionCoating::new(2.25, 1.5, 125.0, 500.0);
let n_opt = coating.optimal_n();
let expected = 2.25_f64.sqrt();
assert!(
(n_opt - expected).abs() < EPS,
"Optimal coating n: {n_opt} vs {expected}"
);
}
#[test]
fn test_fresnel_power_conservation() {
let (rs, ts, rp, tp) = fresnel_power(1.0, 1.5, 0.3);
assert!(
(rs + ts - 1.0).abs() < 1e-10,
"Energy conservation for s-pol: Rs+Ts={}",
rs + ts
);
assert!(
(rp + tp - 1.0).abs() < 1e-10,
"Energy conservation for p-pol: Rp+Tp={}",
rp + tp
);
}
#[test]
fn test_shg_efficiency_scales_with_l2() {
let e1 = shg_efficiency(1e-12, 1e-3, 1e12, 1.7, 1.7, 1064e-9);
let e2 = shg_efficiency(1e-12, 2e-3, 1e12, 1.7, 1.7, 1064e-9);
let ratio = e2 / e1;
assert!(
(ratio - 4.0).abs() < 1e-6,
"SHG efficiency scales as L²: ratio={ratio:.6}"
);
}
#[test]
fn test_spm_phase_linear_power() {
let phi1 = spm_phase_shift(2.6e-20, 1550e-9, 80e-12, 1e3, 1.0);
let phi2 = spm_phase_shift(2.6e-20, 1550e-9, 80e-12, 2e3, 1.0);
assert!(
(phi2 - 2.0 * phi1).abs() < 1e-20,
"SPM phase shift scales with power: phi1={phi1:.6}, phi2={phi2:.6}"
);
}
#[test]
fn test_xpm_is_twice_spm() {
let phi_spm = spm_phase_shift(2.6e-20, 1550e-9, 80e-12, 1e3, 1.0);
let phi_xpm = xpm_phase_shift(2.6e-20, 1550e-9, 80e-12, 1e3, 1.0);
assert!(
(phi_xpm - 2.0 * phi_spm).abs() < 1e-20,
"XPM = 2×SPM: spm={phi_spm:.6}, xpm={phi_xpm:.6}"
);
}
#[test]
fn test_tauc_zero_alpha() {
let ord = tauc_ordinate(0.0, 2.5, 0.5);
assert!(
ord.abs() < EPS,
"Tauc ordinate should be zero for alpha=0, got {ord}"
);
}
#[test]
fn test_cauchy_returns_n_greater_one() {
let n = cauchy_equation(0.55, 1.458, 0.00354, 0.0);
assert!(n > 1.0, "Cauchy must return n > 1, got {n}");
}
#[test]
fn test_drude_plasma_frequency_positive() {
let wp = drude_plasma_frequency(8.5e28, 9.1e-31);
assert!(wp > 0.0, "Plasma frequency must be positive, got {wp:.3e}");
}
#[test]
fn test_photoelastic_zero_for_equal_stress() {
let dn = photoelastic_birefringence(1e-12, 1e6, 1e6);
assert!(
dn.abs() < EPS,
"No birefringence for hydrostatic stress, got {dn}"
);
}
#[test]
fn test_bruggeman_between_components() {
let e1 = 2.25;
let e2 = 12.0;
let eps_eff = bruggeman_effective_medium(e1, e2, 0.3);
assert!(
eps_eff >= e1 && eps_eff <= e2,
"Bruggeman eps_eff should be between components: {eps_eff}"
);
}
}