use std::f64::consts::PI;
use num_complex::Complex64;
#[allow(unused_imports)]
use crate::error::{OxiPhotonError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Polarization {
TE,
TM,
Average,
}
#[derive(Debug, Clone)]
pub struct Layer {
pub n: Complex64,
pub thickness_nm: f64,
pub name: String,
}
impl Layer {
pub fn new(n: impl Into<Complex64>, thickness_nm: f64, name: impl Into<String>) -> Self {
Self {
n: n.into(),
thickness_nm,
name: name.into(),
}
}
pub fn lossless(n_real: f64, thickness_nm: f64, name: impl Into<String>) -> Self {
Self::new(Complex64::new(n_real, 0.0), thickness_nm, name)
}
pub fn quarter_wave(n_real: f64, lambda_nm: f64, name: impl Into<String>) -> Self {
let d = lambda_nm / (4.0 * n_real);
Self::lossless(n_real, d, name)
}
pub fn half_wave(n_real: f64, lambda_nm: f64, name: impl Into<String>) -> Self {
let d = lambda_nm / (2.0 * n_real);
Self::lossless(n_real, d, name)
}
pub fn optical_thickness_nm(&self) -> f64 {
self.n.re * self.thickness_nm
}
}
pub fn snell_cos(n1: Complex64, theta1_rad: f64, n2: Complex64) -> Complex64 {
let sin_t1 = Complex64::new(theta1_rad.sin(), 0.0);
let sin_t2 = n1 / n2 * sin_t1;
let cos_t2 = (Complex64::new(1.0, 0.0) - sin_t2 * sin_t2).sqrt();
if cos_t2.re < 0.0 {
-cos_t2
} else {
cos_t2
}
}
pub fn snell_law(n1: Complex64, theta1_rad: f64, n2: Complex64) -> Complex64 {
snell_cos(n1, theta1_rad, n2)
}
fn admittance(n: Complex64, cos_theta: Complex64, pol: Polarization) -> Complex64 {
match pol {
Polarization::TE | Polarization::Average => n * cos_theta,
Polarization::TM => n / cos_theta,
}
}
pub fn fresnel_r_s(n1: Complex64, n2: Complex64, theta1: f64) -> Complex64 {
let cos_t1 = Complex64::new(theta1.cos(), 0.0);
let cos_t2 = snell_cos(n1, theta1, n2);
let eta1 = n1 * cos_t1;
let eta2 = n2 * cos_t2;
(eta1 - eta2) / (eta1 + eta2)
}
pub fn fresnel_r_p(n1: Complex64, n2: Complex64, theta1: f64) -> Complex64 {
let cos_t1 = Complex64::new(theta1.cos(), 0.0);
let cos_t2 = snell_cos(n1, theta1, n2);
let eta1 = n1 / cos_t1;
let eta2 = n2 / cos_t2;
(eta1 - eta2) / (eta1 + eta2)
}
pub fn fresnel_t_s(n1: Complex64, n2: Complex64, theta1: f64) -> Complex64 {
let cos_t1 = Complex64::new(theta1.cos(), 0.0);
let cos_t2 = snell_cos(n1, theta1, n2);
let eta1 = n1 * cos_t1;
let eta2 = n2 * cos_t2;
(Complex64::new(2.0, 0.0) * eta1) / (eta1 + eta2)
}
pub struct MultilayerStack {
pub layers: Vec<Layer>,
pub n_incident: Complex64,
pub n_substrate: Complex64,
}
impl MultilayerStack {
pub fn new(n_incident: impl Into<Complex64>, n_substrate: impl Into<Complex64>) -> Self {
Self {
layers: Vec::new(),
n_incident: n_incident.into(),
n_substrate: n_substrate.into(),
}
}
pub fn add_layer(&mut self, layer: Layer) {
self.layers.push(layer);
}
pub fn add_layers(&mut self, layers: Vec<Layer>) {
self.layers.extend(layers);
}
pub fn layer_matrix(
&self,
layer: &Layer,
lambda_nm: f64,
theta_rad: f64,
polarization: Polarization,
) -> [[Complex64; 2]; 2] {
let pol = match polarization {
Polarization::Average => Polarization::TE,
p => p,
};
let cos_t = snell_cos(self.n_incident, theta_rad, layer.n);
let delta =
Complex64::new(2.0 * PI / lambda_nm, 0.0) * layer.n * cos_t * layer.thickness_nm;
let cos_delta = delta.cos();
let sin_delta = delta.sin();
let eta = admittance(layer.n, cos_t, pol);
let i = Complex64::new(0.0, 1.0);
[
[cos_delta, -i * sin_delta / eta],
[-i * eta * sin_delta, cos_delta],
]
}
fn mat_mul(a: &[[Complex64; 2]; 2], b: &[[Complex64; 2]; 2]) -> [[Complex64; 2]; 2] {
let zero = Complex64::new(0.0, 0.0);
let mut c = [[zero; 2]; 2];
for i in 0..2 {
for j in 0..2 {
for k in 0..2 {
c[i][j] += a[i][k] * b[k][j];
}
}
}
c
}
fn mat_identity() -> [[Complex64; 2]; 2] {
let one = Complex64::new(1.0, 0.0);
let zero = Complex64::new(0.0, 0.0);
[[one, zero], [zero, one]]
}
fn total_matrix_single_pol(
&self,
lambda_nm: f64,
theta_rad: f64,
pol: Polarization,
) -> [[Complex64; 2]; 2] {
let mut m = Self::mat_identity();
for layer in &self.layers {
let mi = self.layer_matrix(layer, lambda_nm, theta_rad, pol);
m = Self::mat_mul(&m, &mi);
}
m
}
pub fn total_matrix(
&self,
lambda_nm: f64,
theta_rad: f64,
polarization: Polarization,
) -> [[Complex64; 2]; 2] {
let pol = match polarization {
Polarization::Average => Polarization::TE,
p => p,
};
self.total_matrix_single_pol(lambda_nm, theta_rad, pol)
}
fn medium_admittances(&self, theta_rad: f64, pol: Polarization) -> (Complex64, Complex64) {
let cos_inc = Complex64::new(theta_rad.cos(), 0.0);
let eta_inc = admittance(self.n_incident, cos_inc, pol);
let cos_sub = snell_cos(self.n_incident, theta_rad, self.n_substrate);
let eta_sub = admittance(self.n_substrate, cos_sub, pol);
(eta_inc, eta_sub)
}
fn b_c(m: &[[Complex64; 2]; 2], eta_sub: Complex64) -> (Complex64, Complex64) {
let b = m[0][0] + m[0][1] * eta_sub;
let c = m[1][0] + m[1][1] * eta_sub;
(b, c)
}
fn reflection_amplitude_single(
&self,
lambda_nm: f64,
theta_rad: f64,
pol: Polarization,
) -> Complex64 {
let m = self.total_matrix_single_pol(lambda_nm, theta_rad, pol);
let (eta_inc, eta_sub) = self.medium_admittances(theta_rad, pol);
let (b, c) = Self::b_c(&m, eta_sub);
(eta_inc * b - c) / (eta_inc * b + c)
}
fn reflectance_single(&self, lambda_nm: f64, theta_rad: f64, pol: Polarization) -> f64 {
let r = self.reflection_amplitude_single(lambda_nm, theta_rad, pol);
r.norm_sqr()
}
fn transmittance_single(&self, lambda_nm: f64, theta_rad: f64, pol: Polarization) -> f64 {
let m = self.total_matrix_single_pol(lambda_nm, theta_rad, pol);
let (eta_inc, eta_sub) = self.medium_admittances(theta_rad, pol);
let (b, c) = Self::b_c(&m, eta_sub);
let denom = (eta_inc * b + c).norm_sqr();
if denom < f64::EPSILON {
return 0.0;
}
4.0 * eta_inc.re * eta_sub.re / denom
}
pub fn reflection_amplitude(
&self,
lambda_nm: f64,
theta_rad: f64,
polarization: Polarization,
) -> Complex64 {
match polarization {
Polarization::Average => {
let r_te = self.reflection_amplitude_single(lambda_nm, theta_rad, Polarization::TE);
let r_tm = self.reflection_amplitude_single(lambda_nm, theta_rad, Polarization::TM);
(r_te + r_tm) * Complex64::new(0.5, 0.0)
}
p => self.reflection_amplitude_single(lambda_nm, theta_rad, p),
}
}
pub fn reflectance(&self, lambda_nm: f64, theta_rad: f64, polarization: Polarization) -> f64 {
match polarization {
Polarization::Average => {
0.5 * (self.reflectance_single(lambda_nm, theta_rad, Polarization::TE)
+ self.reflectance_single(lambda_nm, theta_rad, Polarization::TM))
}
p => self.reflectance_single(lambda_nm, theta_rad, p),
}
}
pub fn transmittance(&self, lambda_nm: f64, theta_rad: f64, polarization: Polarization) -> f64 {
match polarization {
Polarization::Average => {
0.5 * (self.transmittance_single(lambda_nm, theta_rad, Polarization::TE)
+ self.transmittance_single(lambda_nm, theta_rad, Polarization::TM))
}
p => self.transmittance_single(lambda_nm, theta_rad, p),
}
}
pub fn absorptance(&self, lambda_nm: f64, theta_rad: f64, polarization: Polarization) -> f64 {
let r = self.reflectance(lambda_nm, theta_rad, polarization);
let t = self.transmittance(lambda_nm, theta_rad, polarization);
(1.0 - r - t).clamp(0.0, 1.0)
}
pub fn reflection_phase_rad(
&self,
lambda_nm: f64,
theta_rad: f64,
polarization: Polarization,
) -> f64 {
self.reflection_amplitude(lambda_nm, theta_rad, polarization)
.arg()
}
pub fn group_delay_fs(
&self,
lambda_nm: f64,
theta_rad: f64,
polarization: Polarization,
) -> f64 {
let dl = 0.1_f64; let l1 = (lambda_nm - dl).max(1.0);
let l2 = lambda_nm + dl;
let c_nm_per_fs = 299.792_458_f64;
let omega1 = 2.0 * PI * c_nm_per_fs / l2;
let omega2 = 2.0 * PI * c_nm_per_fs / l1;
let d_omega = omega2 - omega1;
let phi1 = self.reflection_phase_rad(l2, theta_rad, polarization);
let phi2 = self.reflection_phase_rad(l1, theta_rad, polarization);
let mut dphi = phi2 - phi1;
while dphi > PI {
dphi -= 2.0 * PI;
}
while dphi < -PI {
dphi += 2.0 * PI;
}
-dphi / d_omega }
pub fn group_delay_dispersion_fs2(
&self,
lambda_nm: f64,
theta_rad: f64,
polarization: Polarization,
) -> f64 {
let dl = 0.5_f64; let l1 = (lambda_nm - dl).max(1.0);
let l2 = lambda_nm + dl;
let c_nm_per_fs = 299.792_458_f64;
let omega1 = 2.0 * PI * c_nm_per_fs / l2;
let omega2 = 2.0 * PI * c_nm_per_fs / l1;
let d_omega = (omega2 - omega1) / 2.0;
let gd_lo = self.group_delay_fs(l2, theta_rad, polarization);
let gd_hi = self.group_delay_fs(l1, theta_rad, polarization);
(gd_hi - gd_lo) / (2.0 * d_omega) }
pub fn spectrum(
&self,
lambda_min_nm: f64,
lambda_max_nm: f64,
n_pts: usize,
theta_rad: f64,
polarization: Polarization,
) -> Vec<(f64, f64, f64, f64)> {
if n_pts == 0 {
return Vec::new();
}
let denom = (n_pts.saturating_sub(1)).max(1) as f64;
(0..n_pts)
.map(|i| {
let t = i as f64 / denom;
let lambda = lambda_min_nm + t * (lambda_max_nm - lambda_min_nm);
let r = self.reflectance(lambda, theta_rad, polarization);
let tr = self.transmittance(lambda, theta_rad, polarization);
let a = (1.0 - r - tr).clamp(0.0, 1.0);
(lambda, r, tr, a)
})
.collect()
}
pub fn field_distribution(
&self,
lambda_nm: f64,
theta_rad: f64,
polarization: Polarization,
n_pts_per_layer: usize,
) -> Vec<(f64, f64)> {
let pol = match polarization {
Polarization::Average => Polarization::TE,
p => p,
};
let n_pts = n_pts_per_layer.max(2);
let mut result = Vec::with_capacity(self.layers.len() * n_pts);
let r = self.reflection_amplitude_single(lambda_nm, theta_rad, pol);
let (eta_inc, _) = self.medium_admittances(theta_rad, pol);
let b0 = Complex64::new(1.0, 0.0) + r;
let c0 = eta_inc * (Complex64::new(1.0, 0.0) - r);
let mut z_nm = 0.0_f64;
let mut b_cur = b0;
let mut c_cur = c0;
for layer in &self.layers {
let cos_t = snell_cos(self.n_incident, theta_rad, layer.n);
let eta_layer = admittance(layer.n, cos_t, pol);
let k_z = Complex64::new(2.0 * PI / lambda_nm, 0.0) * layer.n * cos_t;
for p in 0..n_pts {
let frac = p as f64 / (n_pts - 1) as f64;
let dz = frac * layer.thickness_nm;
let z_total = z_nm + dz;
let delta = k_z * dz;
let cos_d = delta.cos();
let sin_d = delta.sin();
let i = Complex64::new(0.0, 1.0);
let b_z = cos_d * b_cur - i * sin_d / eta_layer * c_cur;
result.push((z_total, b_z.norm_sqr()));
}
let delta_full = k_z * layer.thickness_nm;
let cos_d = delta_full.cos();
let sin_d = delta_full.sin();
let i = Complex64::new(0.0, 1.0);
let b_next = cos_d * b_cur - i * sin_d / eta_layer * c_cur;
let c_next = -i * eta_layer * sin_d * b_cur + cos_d * c_cur;
b_cur = b_next;
c_cur = c_next;
z_nm += layer.thickness_nm;
}
result
}
pub fn total_optical_thickness_nm(&self) -> f64 {
self.layers.iter().map(|l| l.optical_thickness_nm()).sum()
}
pub fn n_layers(&self) -> usize {
self.layers.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
fn bare_glass_r(n_sub: f64) -> f64 {
let r = (1.0 - n_sub) / (1.0 + n_sub);
r * r
}
#[test]
fn test_empty_stack_r_equals_fresnel() {
let n_glass = 1.52_f64;
let stack = MultilayerStack::new(1.0_f64, n_glass);
let r_stack = stack.reflectance(550.0, 0.0, Polarization::TE);
let r_expected = bare_glass_r(n_glass);
assert_abs_diff_eq!(r_stack, r_expected, epsilon = 1e-10);
}
#[test]
fn test_qw_single_layer_r_at_design() {
let n_mgf2 = 1.38_f64;
let n_glass = 1.52_f64;
let lambda_nm = 550.0_f64;
let mut stack = MultilayerStack::new(1.0_f64, n_glass);
stack.add_layer(Layer::quarter_wave(n_mgf2, lambda_nm, "MgF2"));
let r_coated = stack.reflectance(lambda_nm, 0.0, Polarization::TE);
let r_bare = bare_glass_r(n_glass);
assert!(
r_coated < r_bare,
"QW AR coating should reduce R: coated={r_coated:.6} bare={r_bare:.6}"
);
}
#[test]
fn test_energy_conservation() {
let n_tio2 = 2.35_f64;
let n_sio2 = 1.46_f64;
let n_glass = 1.52_f64;
let lambda_nm = 600.0_f64;
let mut stack = MultilayerStack::new(1.0_f64, n_glass);
for _ in 0..4 {
stack.add_layer(Layer::quarter_wave(n_tio2, lambda_nm, "TiO2"));
stack.add_layer(Layer::quarter_wave(n_sio2, lambda_nm, "SiO2"));
}
for lambda in [400.0_f64, 550.0, 600.0, 700.0, 900.0] {
for theta in [0.0_f64, 0.1, 0.3, 0.5] {
for &pol in &[Polarization::TE, Polarization::TM] {
let r = stack.reflectance(lambda, theta, pol);
let t = stack.transmittance(lambda, theta, pol);
let total = r + t;
assert!(
(total - 1.0).abs() < 1e-8,
"R+T={total:.10} ≠ 1 at λ={lambda} θ={theta} {pol:?}"
);
}
}
}
}
#[test]
fn test_normal_incidence_te_tm_equal() {
let n_glass = 1.52_f64;
let mut stack = MultilayerStack::new(1.0_f64, n_glass);
stack.add_layer(Layer::lossless(1.38, 100.0, "AR"));
let r_te = stack.reflectance(550.0, 0.0, Polarization::TE);
let r_tm = stack.reflectance(550.0, 0.0, Polarization::TM);
assert_abs_diff_eq!(r_te, r_tm, epsilon = 1e-10);
}
#[test]
fn test_reflection_phase_180_for_metal() {
let n_dense = 3.5_f64;
let stack = MultilayerStack::new(1.0_f64, n_dense);
let phase = stack.reflection_phase_rad(800.0, 0.0, Polarization::TE);
assert!(
(phase.abs() - PI).abs() < 1e-10,
"Expected |φ| ≈ π, got φ={phase:.6}"
);
}
#[test]
fn test_field_distribution_length() {
let n_glass = 1.52_f64;
let mut stack = MultilayerStack::new(1.0_f64, n_glass);
let n_layers = 3_usize;
let pts = 10_usize;
for i in 0..n_layers {
stack.add_layer(Layer::lossless(
1.5 + 0.1 * i as f64,
100.0,
format!("L{i}"),
));
}
let fd = stack.field_distribution(550.0, 0.0, Polarization::TE, pts);
assert_eq!(fd.len(), n_layers * pts);
}
#[test]
fn test_optical_thickness() {
let mut stack = MultilayerStack::new(1.0_f64, 1.5_f64);
stack.add_layer(Layer::lossless(1.46, 100.0, "SiO2"));
stack.add_layer(Layer::lossless(2.35, 50.0, "TiO2"));
let expected = 1.46 * 100.0 + 2.35 * 50.0;
assert_abs_diff_eq!(
stack.total_optical_thickness_nm(),
expected,
epsilon = 1e-10
);
}
#[test]
fn test_fresnel_single_interface() {
let n1 = Complex64::new(1.0, 0.0);
let n2 = Complex64::new(1.52, 0.0);
let theta = 0.4_f64;
let r_s = fresnel_r_s(n1, n2, theta);
let r_p = fresnel_r_p(n1, n2, theta);
let stack = MultilayerStack::new(n1, n2);
let r_te = stack.reflection_amplitude_single(550.0, theta, Polarization::TE);
let r_tm = stack.reflection_amplitude_single(550.0, theta, Polarization::TM);
assert_abs_diff_eq!(r_s.re, r_te.re, epsilon = 1e-10);
assert_abs_diff_eq!(r_s.im, r_te.im, epsilon = 1e-10);
assert_abs_diff_eq!(r_p.re, r_tm.re, epsilon = 1e-10);
assert_abs_diff_eq!(r_p.im, r_tm.im, epsilon = 1e-10);
}
}