use num_complex::Complex64;
use std::f64::consts::PI;
use crate::error::OxiPhotonError;
const EPS0: f64 = 8.854_187_817e-12; const C0: f64 = 2.997_924_58e8; const HBAR: f64 = 1.054_571_8e-34; const E_CHARGE: f64 = 1.602_176_634e-19;
#[allow(dead_code)]
const _EPS0: f64 = EPS0;
#[allow(dead_code)]
const _HBAR: f64 = HBAR;
#[allow(dead_code)]
const _E_CHARGE: f64 = E_CHARGE;
#[derive(Debug, Clone)]
pub struct DrudeMetal {
pub eps_inf: f64,
pub omega_p: f64,
pub gamma: f64,
pub name: String,
}
impl DrudeMetal {
pub fn new(eps_inf: f64, omega_p: f64, gamma: f64, name: impl Into<String>) -> Self {
Self {
eps_inf,
omega_p,
gamma,
name: name.into(),
}
}
pub fn permittivity(&self, omega: f64) -> Complex64 {
let eps_inf = Complex64::new(self.eps_inf, 0.0);
let wp2 = self.omega_p * self.omega_p;
let denom = Complex64::new(omega * omega, self.gamma * omega);
eps_inf - wp2 / denom
}
pub fn refractive_index(&self, omega: f64) -> Complex64 {
self.permittivity(omega).sqrt()
}
pub fn skin_depth_nm(&self, omega: f64) -> f64 {
let n_tilde = self.refractive_index(omega);
let k_opt = n_tilde.im.abs();
if k_opt < f64::EPSILON {
return f64::INFINITY;
}
let delta_m = C0 / (omega * k_opt);
delta_m * 1.0e9 }
pub fn gold() -> Self {
Self::new(9.5, 13.7e15, 1.07e14, "Gold")
}
pub fn silver() -> Self {
Self::new(3.7, 13.7e15, 2.73e13, "Silver")
}
pub fn aluminum() -> Self {
Self::new(1.0, 22.7e15, 1.27e14, "Aluminum")
}
pub fn copper() -> Self {
Self::new(10.8, 13.4e15, 1.45e14, "Copper")
}
pub fn plasma_energy_ev(&self) -> f64 {
self.omega_p * HBAR / E_CHARGE
}
pub fn supports_spp(&self, omega: f64, eps_dielectric: f64) -> bool {
let eps_m = self.permittivity(omega);
eps_m.re < -eps_dielectric
}
}
pub struct SurfacePlasmonPolariton {
pub metal: DrudeMetal,
pub eps_dielectric: f64,
}
impl SurfacePlasmonPolariton {
pub fn new(metal: DrudeMetal, eps_dielectric: f64) -> Self {
Self {
metal,
eps_dielectric,
}
}
pub fn wavevector(&self, omega: f64) -> Complex64 {
let eps_m = self.metal.permittivity(omega);
let eps_d = Complex64::new(self.eps_dielectric, 0.0);
let k0 = omega / C0;
let ratio = eps_m * eps_d / (eps_m + eps_d);
k0 * ratio.sqrt()
}
pub fn effective_index(&self, omega: f64) -> Complex64 {
let eps_m = self.metal.permittivity(omega);
let eps_d = Complex64::new(self.eps_dielectric, 0.0);
(eps_m * eps_d / (eps_m + eps_d)).sqrt()
}
pub fn propagation_length_um(&self, omega: f64) -> f64 {
let k_sp = self.wavevector(omega);
let two_ki = 2.0 * k_sp.im.abs();
if two_ki < f64::EPSILON {
return f64::INFINITY;
}
(1.0 / two_ki) * 1.0e6 }
pub fn penetration_depth_metal_nm(&self, omega: f64) -> f64 {
let k_sp = self.wavevector(omega);
let k0 = omega / C0;
let eps_m = self.metal.permittivity(omega);
let kz_m = (k_sp * k_sp - k0 * k0 * eps_m).sqrt();
let im = kz_m.im.abs();
if im < f64::EPSILON {
return f64::INFINITY;
}
(1.0 / im) * 1.0e9
}
pub fn penetration_depth_dielectric_nm(&self, omega: f64) -> f64 {
let k_sp = self.wavevector(omega);
let k0 = omega / C0;
let eps_d = Complex64::new(self.eps_dielectric, 0.0);
let kz_d = (k_sp * k_sp - k0 * k0 * eps_d).sqrt();
let im = kz_d.im.abs();
if im < f64::EPSILON {
return f64::INFINITY;
}
(1.0 / im) * 1.0e9
}
pub fn group_velocity(&self, omega: f64, d_omega: f64) -> f64 {
let k1 = self.wavevector(omega - d_omega).re;
let k2 = self.wavevector(omega + d_omega).re;
2.0 * d_omega / (k2 - k1)
}
pub fn resonance_frequency(&self) -> f64 {
self.metal.omega_p / (self.metal.eps_inf + self.eps_dielectric).sqrt()
}
pub fn confinement_factor_metal(&self, omega: f64) -> f64 {
let delta_d = self.penetration_depth_dielectric_nm(omega);
let delta_m = self.penetration_depth_metal_nm(omega);
if !delta_d.is_finite() || !delta_m.is_finite() {
return 0.0;
}
delta_d / (delta_d + delta_m)
}
pub fn dispersion_curve(
&self,
omega_min: f64,
omega_max: f64,
n_pts: usize,
) -> Vec<(f64, f64)> {
(0..n_pts)
.map(|i| {
let omega =
omega_min + (omega_max - omega_min) * i as f64 / (n_pts - 1).max(1) as f64;
let k = self.wavevector(omega).re;
(omega, k)
})
.collect()
}
pub fn quality_factor(&self, omega: f64) -> f64 {
let k_sp = self.wavevector(omega);
if k_sp.im.abs() < f64::EPSILON {
return f64::INFINITY;
}
k_sp.re / (2.0 * k_sp.im.abs())
}
}
pub struct KretschmannConfig {
pub n_prism: f64,
pub metal: DrudeMetal,
pub metal_thickness_nm: f64,
pub eps_dielectric: f64,
}
impl KretschmannConfig {
pub fn new(n_prism: f64, metal: DrudeMetal, thickness_nm: f64, eps_d: f64) -> Self {
Self {
n_prism,
metal,
metal_thickness_nm: thickness_nm,
eps_dielectric: eps_d,
}
}
pub fn coupling_angle_rad(&self, omega: f64) -> f64 {
let eps_m = self.metal.permittivity(omega);
let eps_d = Complex64::new(self.eps_dielectric, 0.0);
let n_sp = (eps_m * eps_d / (eps_m + eps_d)).sqrt().re;
let sin_theta = n_sp / self.n_prism;
if sin_theta.abs() > 1.0 {
return f64::NAN;
}
sin_theta.asin()
}
pub fn reflectance(&self, omega: f64, theta_rad: f64) -> f64 {
let k0 = omega / C0;
let n_p = self.n_prism;
let eps_p = n_p * n_p;
let eps_m = self.metal.permittivity(omega);
let eps_d = Complex64::new(self.eps_dielectric, 0.0);
let kx = Complex64::new(k0 * n_p * theta_rad.sin(), 0.0);
let kz_p = (Complex64::new(eps_p, 0.0) * k0 * k0 - kx * kx).sqrt();
let kz_m = (eps_m * k0 * k0 - kx * kx).sqrt();
let kz_d = (eps_d * k0 * k0 - kx * kx).sqrt();
let kz_p_c = kz_p;
let r01 = (eps_m * kz_p_c - Complex64::new(eps_p, 0.0) * kz_m)
/ (eps_m * kz_p_c + Complex64::new(eps_p, 0.0) * kz_m);
let r12 = (eps_d * kz_m - eps_m * kz_d) / (eps_d * kz_m + eps_m * kz_d);
let d = self.metal_thickness_nm * 1.0e-9; let phase = Complex64::new(0.0, 2.0) * kz_m * d;
let exp_phase = phase.exp();
let r_total = (r01 + r12 * exp_phase) / (Complex64::new(1.0, 0.0) + r01 * r12 * exp_phase);
let r_norm = r_total.norm();
let reflectance = r_norm * r_norm;
reflectance.clamp(0.0, 1.0)
}
pub fn reflectance_vs_omega(
&self,
theta_rad: f64,
omega_min: f64,
omega_max: f64,
n_pts: usize,
) -> Vec<(f64, f64)> {
(0..n_pts)
.map(|i| {
let omega =
omega_min + (omega_max - omega_min) * i as f64 / (n_pts - 1).max(1) as f64;
(omega, self.reflectance(omega, theta_rad))
})
.collect()
}
pub fn reflectance_vs_angle(
&self,
omega: f64,
theta_min_rad: f64,
theta_max_rad: f64,
n_pts: usize,
) -> Vec<(f64, f64)> {
(0..n_pts)
.map(|i| {
let theta = theta_min_rad
+ (theta_max_rad - theta_min_rad) * i as f64 / (n_pts - 1).max(1) as f64;
(theta, self.reflectance(omega, theta))
})
.collect()
}
pub fn resonance_dip(&self, omega: f64) -> (f64, f64) {
let theta_c = (1.0_f64 / self.n_prism).asin(); let scan = self.reflectance_vs_angle(omega, theta_c, PI / 2.0 - 1e-6, 500);
scan.iter().copied().fold(
(f64::NAN, f64::INFINITY),
|(best_theta, best_r), (theta, r)| {
if r < best_r {
(theta, r)
} else {
(best_theta, best_r)
}
},
)
}
pub fn sensitivity_deg_per_riu(&self, omega: f64) -> f64 {
let dn = 0.001_f64;
let eps_d_orig = self.eps_dielectric;
let theta0 = self.coupling_angle_rad(omega);
let n_d = eps_d_orig.sqrt();
let eps_d2 = (n_d + dn) * (n_d + dn);
let cfg2 = KretschmannConfig {
n_prism: self.n_prism,
metal: self.metal.clone(),
metal_thickness_nm: self.metal_thickness_nm,
eps_dielectric: eps_d2,
};
let theta2 = cfg2.coupling_angle_rad(omega);
if theta0.is_nan() || theta2.is_nan() {
return 0.0;
}
(theta2 - theta0).to_degrees() / dn
}
}
pub struct MimWaveguide {
pub metal: DrudeMetal,
pub insulator_thickness_nm: f64,
pub eps_insulator: f64,
}
impl MimWaveguide {
pub fn new(metal: DrudeMetal, thickness_nm: f64, eps_ins: f64) -> Self {
Self {
metal,
insulator_thickness_nm: thickness_nm,
eps_insulator: eps_ins,
}
}
pub fn symmetric_mode_wavevector(&self, omega: f64) -> Result<Complex64, OxiPhotonError> {
let k0 = omega / C0;
let eps_m = self.metal.permittivity(omega);
let eps_i = Complex64::new(self.eps_insulator, 0.0);
let d = self.insulator_thickness_nm * 1.0e-9;
let branch = |z: Complex64| -> Complex64 {
let s = z.sqrt();
if s.im < -1.0e-30 {
-s
} else {
s
}
};
let char_fn = |beta: Complex64| -> Complex64 {
let kz_i = branch(beta * beta - eps_i * k0 * k0);
let kz_m = branch(beta * beta - eps_m * k0 * k0);
let arg = kz_i * d / 2.0;
let tanh_val = arg.tanh();
tanh_val + eps_i * kz_m / (eps_m * kz_i)
};
let n_ins = self.eps_insulator.sqrt();
let beta_lo = n_ins * k0 * 1.001; let beta_hi = n_ins * k0 * 80.0; const N_SCAN: usize = 2000;
let mut best_beta = Complex64::new(beta_lo, 0.0);
let mut best_residual = f64::INFINITY;
let mut prev_re = char_fn(Complex64::new(beta_lo, 0.0)).re;
let mut bracket: Option<(f64, f64)> = None;
for i in 1..N_SCAN {
let beta_r = beta_lo + (beta_hi - beta_lo) * i as f64 / N_SCAN as f64;
let beta_c = Complex64::new(beta_r, 0.0);
let f = char_fn(beta_c);
let r = f.norm();
if r < best_residual {
best_residual = r;
best_beta = beta_c;
}
if bracket.is_none() && f.re * prev_re < 0.0 {
let beta_prev = beta_lo + (beta_hi - beta_lo) * (i - 1) as f64 / N_SCAN as f64;
bracket = Some((beta_prev, beta_r));
}
prev_re = f.re;
}
if let Some((mut lo, mut hi)) = bracket {
for _ in 0..80 {
let mid = (lo + hi) / 2.0;
let f_lo = char_fn(Complex64::new(lo, 0.0)).re;
let f_mid = char_fn(Complex64::new(mid, 0.0)).re;
if f_lo * f_mid <= 0.0 {
hi = mid;
} else {
lo = mid;
}
if (hi - lo).abs() < 1.0e-4 * lo {
break;
}
}
let mid = (lo + hi) / 2.0;
best_beta = Complex64::new(mid, 0.0);
}
let mut z = best_beta;
const MAX_ITER: usize = 300;
const TOL: f64 = 1.0e-12;
let dh = beta_lo * 1.0e-7;
for _ in 0..MAX_ITER {
let f = char_fn(z);
if f.norm() < TOL {
break;
}
let df = (char_fn(z + dh) - char_fn(z - dh)) / (2.0 * dh);
if df.norm() < f64::EPSILON * 1.0e6 {
break;
}
let dz = f / df;
z -= dz;
if z.re < 0.0 {
z = Complex64::new(z.re.abs(), z.im);
}
if dz.norm() < TOL * z.norm().max(1.0) {
break;
}
}
let residual = char_fn(z).norm();
if residual < 1.0e-4 && z.re > 0.0 {
Ok(z)
} else {
Err(OxiPhotonError::NumericalError(format!(
"MIM mode solver did not converge for ω={omega:.4e} rad/s, \
d={:.1}nm (residual={:.3e}, β={:.4e}+{:.4e}i)",
self.insulator_thickness_nm, residual, z.re, z.im
)))
}
}
pub fn effective_index(&self, omega: f64) -> Result<f64, OxiPhotonError> {
let beta = self.symmetric_mode_wavevector(omega)?;
Ok(beta.re / (omega / C0))
}
pub fn propagation_length_um(&self, omega: f64) -> Result<f64, OxiPhotonError> {
let beta = self.symmetric_mode_wavevector(omega)?;
let two_ki = 2.0 * beta.im.abs();
if two_ki < f64::EPSILON {
return Ok(f64::INFINITY);
}
Ok((1.0 / two_ki) * 1.0e6)
}
pub fn confinement_factor(&self, omega: f64) -> f64 {
let d_nm = self.insulator_thickness_nm;
let delta_m = self.metal.skin_depth_nm(omega);
d_nm / (d_nm + 2.0 * delta_m.min(d_nm * 10.0))
}
pub fn cutoff_thickness_nm(&self, omega: f64) -> f64 {
let original_d = self.insulator_thickness_nm;
let d_values: Vec<f64> = (1..=500).map(|i| i as f64).collect();
for d_nm in d_values {
let test = MimWaveguide::new(self.metal.clone(), d_nm, self.eps_insulator);
if let Ok(beta) = test.symmetric_mode_wavevector(omega) {
let two_ki = 2.0 * beta.im.abs();
let l_nm = if two_ki > f64::EPSILON {
(1.0 / two_ki) * 1.0e9
} else {
f64::INFINITY
};
if l_nm > 100.0 {
return d_nm;
}
}
}
original_d
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
fn omega_633nm() -> f64 {
2.0 * PI * C0 / 633.0e-9
}
fn omega_800nm() -> f64 {
2.0 * PI * C0 / 800.0e-9
}
#[test]
fn test_drude_gold_negative_real_eps() {
let gold = DrudeMetal::gold();
let omega = omega_633nm();
let eps = gold.permittivity(omega);
assert!(
eps.re < 0.0,
"Gold Re(ε) must be negative at visible frequencies; got {:.3}",
eps.re
);
}
#[test]
fn test_drude_skin_depth_positive() {
let gold = DrudeMetal::gold();
let delta = gold.skin_depth_nm(omega_633nm());
assert!(
delta.is_finite() && delta > 0.0,
"Skin depth must be positive and finite; got {delta}"
);
}
#[test]
fn test_gold_supports_spp() {
let gold = DrudeMetal::gold();
let omega = omega_633nm();
assert!(
gold.supports_spp(omega, 2.25),
"Gold must support SPP on glass at 633 nm"
);
}
#[test]
fn test_spp_wavevector_larger_than_k0() {
let gold = DrudeMetal::gold();
let spp = SurfacePlasmonPolariton::new(gold, 1.0); let omega = omega_633nm();
let k_sp = spp.wavevector(omega);
let k0 = omega / C0;
assert!(
k_sp.re > k0,
"SPP k_sp.re ({:.3e}) must exceed k0 ({:.3e}) in air",
k_sp.re,
k0
);
}
#[test]
fn test_spp_propagation_length_finite() {
let gold = DrudeMetal::gold();
let spp = SurfacePlasmonPolariton::new(gold, 2.25); let l = spp.propagation_length_um(omega_633nm());
assert!(
l.is_finite() && l > 0.0,
"SPP propagation length must be positive and finite; got {l}"
);
}
#[test]
fn test_spp_resonance_frequency() {
let gold = DrudeMetal::gold();
let eps_d = 1.0_f64; let spp = SurfacePlasmonPolariton::new(gold.clone(), eps_d);
let omega_sp = spp.resonance_frequency();
let expected = gold.omega_p / (gold.eps_inf + eps_d).sqrt();
let rel_err = (omega_sp - expected).abs() / expected;
assert!(
rel_err < 1.0e-10,
"ω_sp mismatch: got {omega_sp:.4e}, expected {expected:.4e}"
);
}
#[test]
fn test_spp_quality_factor_positive() {
let gold = DrudeMetal::gold();
let spp = SurfacePlasmonPolariton::new(gold, 2.25);
let q = spp.quality_factor(omega_800nm());
assert!(q > 0.0, "Quality factor must be positive; got {q}");
}
#[test]
fn test_kretschmann_coupling_angle_greater_than_critical() {
let metal = DrudeMetal::gold();
let cfg = KretschmannConfig::new(1.78, metal, 50.0, 1.77); let omega = omega_633nm();
let theta_sp = cfg.coupling_angle_rad(omega);
let theta_c = (1.0_f64 / 1.78).asin();
assert!(
theta_sp > theta_c || theta_sp.is_nan(),
"SPP coupling angle ({:.4} rad) must exceed critical angle ({:.4} rad)",
theta_sp,
theta_c
);
}
#[test]
fn test_kretschmann_reflectance_range() {
let metal = DrudeMetal::gold();
let cfg = KretschmannConfig::new(1.5, metal, 50.0, 1.0);
let omega = omega_633nm();
for i in 0..100 {
let theta = 0.5 + i as f64 * (PI / 2.0 - 0.5) / 100.0;
let r = cfg.reflectance(omega, theta);
assert!(
(0.0..=1.0).contains(&r),
"Reflectance must be in [0,1]; got {r} at θ={theta:.4}"
);
}
}
#[test]
fn test_mim_effective_index_high() {
let metal = DrudeMetal::gold();
let mim = MimWaveguide::new(metal, 10.0, 2.25); let omega = omega_633nm();
match mim.effective_index(omega) {
Ok(n_eff) => {
assert!(
n_eff > 1.5,
"MIM n_eff should be significantly larger than 1 for thin gap; got {n_eff}"
);
}
Err(e) => {
eprintln!("MIM solver note: {e}");
}
}
}
}