use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Polarization {
TE,
TM,
}
#[derive(Debug, Clone)]
pub struct SlabMode {
pub n_eff: f64,
pub order: usize,
pub polarization: Polarization,
}
pub struct SlabWaveguide {
pub n_core: f64,
pub n_clad: f64,
pub thickness: f64,
}
impl SlabWaveguide {
pub fn new(n_core: f64, n_clad: f64, thickness: f64) -> Self {
assert!(n_core > n_clad, "n_core must be > n_clad for guidance");
Self {
n_core,
n_clad,
thickness,
}
}
pub fn solve_te(&self, wavelength: f64) -> Vec<SlabMode> {
self.solve_modes(wavelength, Polarization::TE)
}
pub fn solve_tm(&self, wavelength: f64) -> Vec<SlabMode> {
self.solve_modes(wavelength, Polarization::TM)
}
fn solve_modes(&self, wavelength: f64, pol: Polarization) -> Vec<SlabMode> {
let k0 = 2.0 * PI / wavelength;
let h = self.thickness;
let dn = (self.n_core * self.n_core - self.n_clad * self.n_clad).sqrt();
let big_v = k0 * h / 2.0 * dn;
let pol_factor = match pol {
Polarization::TE => 1.0,
Polarization::TM => (self.n_clad / self.n_core).powi(2),
};
let w = |u: f64| -> f64 { (big_v * big_v - u * u).sqrt() };
let mut u_roots: Vec<f64> = Vec::new();
let n_scan = 2000;
let du = big_v / n_scan as f64;
let f = |u: f64, is_even: bool| -> f64 {
if u <= 0.0 || u >= big_v {
return f64::NAN;
}
let wu = w(u);
let tan_u = u.tan();
if is_even {
u * tan_u - pol_factor * wu
} else {
-u / tan_u - pol_factor * wu
}
};
for mode_order in 0..20 {
let is_even = mode_order % 2 == 0;
let half_ord = mode_order / 2;
let u_start = if is_even {
half_ord as f64 * PI + 1e-10
} else {
half_ord as f64 * PI + PI / 2.0 + 1e-10
};
let u_end = if is_even {
half_ord as f64 * PI + PI / 2.0 - 1e-10
} else {
(half_ord + 1) as f64 * PI - 1e-10
};
if u_start >= big_v {
break; }
let u_end = u_end.min(big_v - 1e-12);
if u_end <= u_start {
continue;
}
let n_sub = (((u_end - u_start) / du).ceil() as usize).max(10);
let sub_step = (u_end - u_start) / n_sub as f64;
let mut prev = f(u_start, is_even);
for i in 1..=n_sub {
let u = u_start + i as f64 * sub_step;
let val = f(u, is_even);
if !prev.is_nan() && !val.is_nan() && prev * val < 0.0 && (val - prev).abs() < 1e15
{
if let Some(u_root) = bisect(|uu| f(uu, is_even), u - sub_step, u, 1e-14) {
u_roots.push(u_root);
}
break;
}
prev = val;
}
}
let mut modes: Vec<SlabMode> = u_roots
.iter()
.enumerate()
.map(|(i, &u)| {
let kappa = 2.0 * u / h;
let beta = (self.n_core * self.n_core * k0 * k0 - kappa * kappa).sqrt();
SlabMode {
n_eff: beta / k0,
order: i,
polarization: pol,
}
})
.collect();
modes.sort_by(|a, b| {
b.n_eff
.partial_cmp(&a.n_eff)
.unwrap_or(std::cmp::Ordering::Equal)
});
for (i, m) in modes.iter_mut().enumerate() {
m.order = i;
}
modes
}
}
pub struct AsymmetricSlab {
pub n_left: f64,
pub n_core: f64,
pub n_right: f64,
pub thickness: f64,
}
impl AsymmetricSlab {
pub fn new(n_left: f64, n_core: f64, n_right: f64, thickness: f64) -> Self {
assert!(
n_core > n_left.max(n_right),
"n_core must exceed both cladding indices"
);
Self {
n_left,
n_core,
n_right,
thickness,
}
}
pub fn solve_te(&self, wavelength: f64) -> Vec<SlabMode> {
self.solve_modes(wavelength, Polarization::TE)
}
pub fn solve_tm(&self, wavelength: f64) -> Vec<SlabMode> {
self.solve_modes(wavelength, Polarization::TM)
}
fn solve_modes(&self, wavelength: f64, pol: Polarization) -> Vec<SlabMode> {
let k0 = 2.0 * PI / wavelength;
let h = self.thickness;
let n1 = self.n_left;
let n2 = self.n_core;
let n3 = self.n_right;
let n_max_clad = n1.max(n3);
let beta_min = n_max_clad * k0 + 1e-10;
let beta_max = n2 * k0 - 1e-10;
if beta_min >= beta_max {
return Vec::new();
}
let (r1, r3) = match pol {
Polarization::TE => (1.0, 1.0),
Polarization::TM => (n2 * n2 / (n1 * n1), n2 * n2 / (n3 * n3)),
};
let char_eq = |beta: f64, m: usize| -> f64 {
let kappa2 = n2 * n2 * k0 * k0 - beta * beta;
let g1_2 = beta * beta - n1 * n1 * k0 * k0;
let g3_2 = beta * beta - n3 * n3 * k0 * k0;
if kappa2 <= 0.0 {
return f64::NAN;
}
let kappa = kappa2.sqrt();
let g1 = if g1_2 > 0.0 { g1_2.sqrt() } else { 0.0 };
let g3 = if g3_2 > 0.0 { g3_2.sqrt() } else { 0.0 };
kappa * h - (r1 * g1 / kappa).atan() - (r3 * g3 / kappa).atan() - m as f64 * PI
};
let max_modes = ((k0 * h * (n2 * n2 - n_max_clad * n_max_clad).sqrt() / PI) as usize) + 2;
let n_scan = 1000;
let step = (beta_max - beta_min) / n_scan as f64;
let mut modes = Vec::new();
for m in 0..max_modes {
let mut prev = char_eq(beta_min, m);
for i in 1..=n_scan {
let beta = beta_min + i as f64 * step;
let val = char_eq(beta, m);
if !prev.is_nan() && !val.is_nan() && prev * val < 0.0 {
if let Some(beta_root) = bisect(|b| char_eq(b, m), beta - step, beta, 1e-14) {
modes.push(SlabMode {
n_eff: beta_root / k0,
order: 0,
polarization: pol,
});
}
break;
}
prev = val;
}
}
modes.sort_by(|a, b| {
b.n_eff
.partial_cmp(&a.n_eff)
.unwrap_or(std::cmp::Ordering::Equal)
});
for (i, m) in modes.iter_mut().enumerate() {
m.order = i;
}
modes
}
}
pub fn strip_waveguide_eim(
n_core: f64,
n_clad: f64,
width: f64,
height: f64,
wavelength: f64,
pol: Polarization,
) -> Option<f64> {
let vert = SlabWaveguide::new(n_core, n_clad, height);
let vert_modes = vert.solve_modes(wavelength, pol);
let n_eff_v = vert_modes.first()?.n_eff;
if n_eff_v <= n_clad {
return None;
}
let horiz = SlabWaveguide::new(n_eff_v, n_clad, width);
let horiz_modes = horiz.solve_modes(wavelength, pol);
Some(horiz_modes.first()?.n_eff)
}
fn bisect<F: Fn(f64) -> f64>(f: F, mut lo: f64, mut hi: f64, tol: f64) -> Option<f64> {
let mut f_lo = f(lo);
let f_hi = f(hi);
if f_lo.is_nan() || f_hi.is_nan() {
return None;
}
if f_lo * f_hi > 0.0 {
return None;
}
for _ in 0..100 {
let mid = (lo + hi) / 2.0;
if (hi - lo) < tol {
return Some(mid);
}
let f_mid = f(mid);
if f_mid.is_nan() {
return None;
}
if f_lo * f_mid <= 0.0 {
hi = mid;
} else {
lo = mid;
f_lo = f_mid;
}
}
Some((lo + hi) / 2.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn symmetric_slab_te0_guided() {
let slab = SlabWaveguide::new(3.476, 1.444, 1000e-9);
let modes = slab.solve_te(1550e-9);
assert!(!modes.is_empty(), "Should find at least TE0");
let n_eff = modes[0].n_eff;
assert!(n_eff > 1.444 && n_eff < 3.476, "n_eff={n_eff} out of range");
}
#[test]
fn symmetric_slab_500nm_si_te0() {
let slab = SlabWaveguide::new(3.476, 1.444, 500e-9);
let modes = slab.solve_te(1550e-9);
assert!(!modes.is_empty());
let n0 = modes[0].n_eff;
assert!(n0 > 2.5 && n0 < 3.476, "TE0 n_eff={n0:.4} unexpected");
}
#[test]
fn asymmetric_slab_te_guided() {
let slab = AsymmetricSlab::new(1.444, 3.476, 1.0, 220e-9);
let modes = slab.solve_te(1550e-9);
assert!(!modes.is_empty(), "Should find TE0 mode");
let n_eff = modes[0].n_eff;
assert!(n_eff > 1.444 && n_eff < 3.476, "n_eff={n_eff}");
}
#[test]
fn eim_strip_waveguide_si_1550() {
let n_eff = strip_waveguide_eim(3.476, 1.444, 500e-9, 220e-9, 1550e-9, Polarization::TE);
let n_eff = n_eff.expect("EIM should find a mode");
assert!(n_eff > 1.8 && n_eff < 3.0, "EIM n_eff={n_eff:.4}");
}
#[test]
fn mode_ordering_by_neff() {
let slab = SlabWaveguide::new(3.476, 1.444, 2000e-9);
let modes = slab.solve_te(1550e-9);
assert!(modes.len() >= 2, "Thick slab should have multiple modes");
assert!(
modes[0].n_eff > modes[1].n_eff,
"Modes must be in descending n_eff order"
);
}
}