use oxiblas::prelude::{Mat, SymmetricEvd};
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct FdMode {
pub n_eff: f64,
pub field: Vec<f64>,
pub order: usize,
}
pub struct FdModeSolver1d {
pub n_profile: Vec<f64>,
pub dx: f64,
pub n_min: f64,
}
impl FdModeSolver1d {
pub fn new(n_profile: Vec<f64>, dx: f64, n_min: f64) -> Self {
Self {
n_profile,
dx,
n_min,
}
}
pub fn solve(&self, wavelength: f64) -> Vec<FdMode> {
let k0 = 2.0 * PI / wavelength;
let n = self.n_profile.len();
let dx2 = self.dx * self.dx;
let mut mat = Mat::<f64>::zeros(n, n);
for i in 0..n {
mat[(i, i)] = -2.0 / dx2 + k0 * k0 * self.n_profile[i] * self.n_profile[i];
if i + 1 < n {
mat[(i, i + 1)] = 1.0 / dx2;
mat[(i + 1, i)] = 1.0 / dx2;
}
}
let evd = match SymmetricEvd::compute(mat.as_ref()) {
Ok(e) => e,
Err(_) => return Vec::new(),
};
let eigenvalues = evd.eigenvalues();
let eigenvectors = evd.eigenvectors();
let beta_min_sq = (self.n_min * k0) * (self.n_min * k0);
let mut modes: Vec<FdMode> = eigenvalues
.iter()
.enumerate()
.filter(|(_, &lambda)| lambda > beta_min_sq)
.map(|(idx, &lambda)| {
let n_eff = (lambda / (k0 * k0)).sqrt();
let field: Vec<f64> = (0..n).map(|i| eigenvectors[(i, idx)]).collect();
FdMode {
n_eff,
field,
order: 0,
}
})
.collect();
modes.sort_by(|a, b| {
b.n_eff
.partial_cmp(&a.n_eff)
.unwrap_or(std::cmp::Ordering::Equal)
});
for (i, mode) in modes.iter_mut().enumerate() {
mode.order = i;
}
modes
}
pub fn slab_profile(
n_core: f64,
n_clad: f64,
thickness: f64,
n_pts: usize,
dx: f64,
) -> Vec<f64> {
let total = n_pts as f64 * dx;
let center = total / 2.0;
(0..n_pts)
.map(|i| {
let x = i as f64 * dx;
let dist = (x - center).abs();
if dist <= thickness / 2.0 {
n_core
} else {
n_clad
}
})
.collect()
}
}
pub struct FdModeSolver2d {
pub n_profile: Vec<f64>,
pub nx: usize,
pub ny: usize,
pub dx: f64,
pub dy: f64,
pub n_min: f64,
}
impl FdModeSolver2d {
pub fn new(n_profile: Vec<f64>, nx: usize, ny: usize, dx: f64, dy: f64, n_min: f64) -> Self {
assert_eq!(n_profile.len(), nx * ny);
Self {
n_profile,
nx,
ny,
dx,
dy,
n_min,
}
}
#[allow(clippy::too_many_arguments)]
pub fn strip_profile(
n_core: f64,
n_clad: f64,
width: f64,
height: f64,
nx: usize,
ny: usize,
dx: f64,
dy: f64,
) -> Vec<f64> {
let cx = nx as f64 * dx / 2.0;
let cy = ny as f64 * dy / 2.0;
let mut n = vec![n_clad; nx * ny];
for j in 0..ny {
for i in 0..nx {
let x = i as f64 * dx;
let y = j as f64 * dy;
if (x - cx).abs() <= width / 2.0 && (y - cy).abs() <= height / 2.0 {
n[j * nx + i] = n_core;
}
}
}
n
}
pub fn solve(&self, wavelength: f64) -> Vec<FdMode> {
let k0 = 2.0 * PI / wavelength;
let nxy = self.nx * self.ny;
let dx2 = self.dx * self.dx;
let dy2 = self.dy * self.dy;
let mut mat = Mat::<f64>::zeros(nxy, nxy);
for j in 0..self.ny {
for i in 0..self.nx {
let k = j * self.nx + i;
let n_k = self.n_profile[k];
mat[(k, k)] = -2.0 / dx2 - 2.0 / dy2 + k0 * k0 * n_k * n_k;
if i + 1 < self.nx {
let kr = k + 1;
mat[(k, kr)] = 1.0 / dx2;
mat[(kr, k)] = 1.0 / dx2;
}
if j + 1 < self.ny {
let ku = k + self.nx;
mat[(k, ku)] = 1.0 / dy2;
mat[(ku, k)] = 1.0 / dy2;
}
}
}
let evd = match SymmetricEvd::compute(mat.as_ref()) {
Ok(e) => e,
Err(_) => return Vec::new(),
};
let eigenvalues = evd.eigenvalues();
let eigenvectors = evd.eigenvectors();
let beta_min_sq = (self.n_min * k0) * (self.n_min * k0);
let mut modes: Vec<FdMode> = eigenvalues
.iter()
.enumerate()
.filter(|(_, &lam)| lam > beta_min_sq)
.map(|(idx, &lam)| {
let n_eff = (lam / (k0 * k0)).sqrt();
let field = (0..nxy).map(|i| eigenvectors[(i, idx)]).collect();
FdMode {
n_eff,
field,
order: 0,
}
})
.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 FdTmSolver1d {
pub n_profile: Vec<f64>,
pub dx: f64,
pub n_min: f64,
}
impl FdTmSolver1d {
pub fn new(n_profile: Vec<f64>, dx: f64, n_min: f64) -> Self {
Self {
n_profile,
dx,
n_min,
}
}
pub fn solve(&self, wavelength: f64) -> Vec<FdMode> {
let k0 = 2.0 * PI / wavelength;
let n = self.n_profile.len();
let dx2 = self.dx * self.dx;
let eps: Vec<f64> = self.n_profile.iter().map(|&ni| ni * ni).collect();
let sqrt_eps: Vec<f64> = eps.iter().map(|&e| e.sqrt()).collect();
let mut mat = Mat::<f64>::zeros(n, n);
for i in 0..n {
let inv_eps_left = if i > 0 {
2.0 / (eps[i - 1] + eps[i])
} else {
0.0
};
let inv_eps_right = if i + 1 < n {
2.0 / (eps[i] + eps[i + 1])
} else {
0.0
};
mat[(i, i)] = k0 * k0 * eps[i] - eps[i] * (inv_eps_left + inv_eps_right) / dx2;
if i + 1 < n {
let coupling = sqrt_eps[i] * sqrt_eps[i + 1] * 2.0 / (eps[i] + eps[i + 1]) / dx2;
mat[(i, i + 1)] = coupling;
mat[(i + 1, i)] = coupling;
}
}
let evd = match SymmetricEvd::compute(mat.as_ref()) {
Ok(e) => e,
Err(_) => return Vec::new(),
};
let eigenvalues = evd.eigenvalues();
let eigenvectors = evd.eigenvectors();
let beta_min_sq = (self.n_min * k0) * (self.n_min * k0);
let mut modes: Vec<FdMode> = eigenvalues
.iter()
.enumerate()
.filter(|(_, &lambda)| lambda > beta_min_sq)
.map(|(idx, &lambda)| {
let n_eff = (lambda / (k0 * k0)).sqrt();
let field: Vec<f64> = (0..n)
.map(|i| sqrt_eps[i] * eigenvectors[(i, idx)])
.collect();
FdMode {
n_eff,
field,
order: 0,
}
})
.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 fn group_velocity(lambda1: f64, n_eff1: f64, lambda2: f64, n_eff2: f64) -> f64 {
use std::f64::consts::PI;
let c = 2.998e8;
let omega1 = 2.0 * PI * c / lambda1;
let omega2 = 2.0 * PI * c / lambda2;
let beta1 = n_eff1 * 2.0 * PI / lambda1;
let beta2 = n_eff2 * 2.0 * PI / lambda2;
let d_omega = omega2 - omega1;
let d_beta = beta2 - beta1;
if d_beta.abs() < 1e-30 {
c
} else {
d_omega / d_beta
}
}
pub fn group_index(lambda1: f64, n_eff1: f64, lambda2: f64, n_eff2: f64) -> f64 {
2.998e8 / group_velocity(lambda1, n_eff1, lambda2, n_eff2).max(1e-30)
}
pub fn dispersion_parameter_d(
lambda: f64,
n_m: f64, n_0: f64, n_p: f64, dl: f64, ) -> f64 {
let c = 2.998e8;
let d2n_dl2 = (n_p - 2.0 * n_0 + n_m) / (dl * dl);
let d_s_m2 = -lambda / c * d2n_dl2;
d_s_m2 * 1e3 }
pub fn si_strip_neff_1550nm() -> f64 {
let n_si = 3.476_f64;
let n_sio2 = 1.444_f64;
let width = 500e-9_f64;
let height = 220e-9_f64;
let wavelength = 1550e-9_f64;
let nx = 20_usize;
let ny = 15_usize;
let dx = 2000e-9 / nx as f64;
let dy = 1500e-9 / ny as f64;
let n_profile = FdModeSolver2d::strip_profile(n_si, n_sio2, width, height, nx, ny, dx, dy);
let solver = FdModeSolver2d::new(n_profile, nx, ny, dx, dy, n_sio2);
let modes = solver.solve(wavelength);
modes.first().map(|m| m.n_eff).unwrap_or(0.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fd1d_slab_finds_guided_modes() {
let n_pts = 100;
let dx = 50e-9; let n_profile = FdModeSolver1d::slab_profile(3.476, 1.444, 1000e-9, n_pts, dx);
let solver = FdModeSolver1d::new(n_profile, dx, 1.444);
let modes = solver.solve(1550e-9);
assert!(!modes.is_empty(), "Should find guided modes");
assert!(
modes[0].n_eff > 1.444 && modes[0].n_eff < 3.476,
"n_eff={} out of guidance range",
modes[0].n_eff
);
}
#[test]
fn fd1d_matches_eim_symmetric_slab() {
use crate::mode::effective_index::SlabWaveguide;
let n_core = 3.476;
let n_clad = 1.444;
let thickness = 500e-9;
let wavelength = 1550e-9;
let slab = SlabWaveguide::new(n_core, n_clad, thickness);
let eim_modes = slab.solve_te(wavelength);
let eim_neff = eim_modes[0].n_eff;
let n_pts = 150;
let dx = 20e-9;
let n_profile = FdModeSolver1d::slab_profile(n_core, n_clad, thickness, n_pts, dx);
let solver = FdModeSolver1d::new(n_profile, dx, n_clad);
let fd_modes = solver.solve(wavelength);
let fd_neff = fd_modes[0].n_eff;
let rel_err = (fd_neff - eim_neff).abs() / eim_neff;
assert!(
rel_err < 0.005,
"FD={fd_neff:.4} EIM={eim_neff:.4} rel_err={rel_err:.4}"
);
}
#[test]
fn si_strip_waveguide_neff_range() {
let n_eff = si_strip_neff_1550nm();
assert!(
n_eff > 2.0 && n_eff < 3.0,
"Si strip n_eff={n_eff:.4} outside expected range 2.0–3.0"
);
}
#[test]
fn tm_solver_finds_modes() {
let n_pts = 80;
let dx = 30e-9;
let n_profile = FdModeSolver1d::slab_profile(3.476, 1.444, 800e-9, n_pts, dx);
let solver = FdTmSolver1d::new(n_profile, dx, 1.444);
let modes = solver.solve(1550e-9);
assert!(!modes.is_empty(), "TM solver should find modes");
assert!(modes[0].n_eff > 1.444, "TM n_eff should be guided");
}
#[test]
fn group_velocity_less_than_c() {
let c = 2.998e8;
let dl = 1e-9;
let n1 = 1.444 + 0.002; let n2 = 1.444;
let vg = group_velocity(1550e-9 - dl / 2.0, n1, 1550e-9 + dl / 2.0, n2);
assert!(vg > 0.0);
assert!(vg < c * 1.1, "v_g={vg:.3e} unexpectedly > c");
}
#[test]
fn group_index_greater_than_phase_index() {
let dl = 5e-9;
let n_eff = 1.5;
let n1 = n_eff + 0.001; let n2 = n_eff - 0.001; let ng = group_index(1550e-9 - dl, n1, 1550e-9 + dl, n2);
assert!(ng > 0.0);
}
#[test]
fn dispersion_parameter_d_sign() {
let n_m = 1.4495;
let n_0 = 1.4500;
let n_p = 1.4505;
let dl = 5e-9;
let d = dispersion_parameter_d(1300e-9, n_m, n_0, n_p, dl);
assert!(d.is_finite());
}
}