use oxiblas::prelude::{Mat, SymmetricEvd};
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct FemMode1d {
pub beta: f64,
pub n_eff: f64,
pub field: Vec<f64>,
}
#[derive(Debug, Clone)]
pub struct FemDispersionPoint {
pub lambda: f64,
pub n_eff: f64,
pub n_g: f64,
pub gvd: f64,
}
#[derive(Debug, Clone)]
pub struct FemModeSolver1d {
pub n_nodes: usize,
pub half_width: f64,
pub n_profile: Vec<f64>,
pub wavelength: f64,
}
impl FemModeSolver1d {
pub fn new(n_profile: Vec<f64>, half_width: f64, wavelength: f64) -> Self {
let n_nodes = n_profile.len();
assert!(n_nodes >= 3, "need at least 3 nodes");
Self {
n_nodes,
half_width,
n_profile,
wavelength,
}
}
pub fn slab(
n_core: f64,
n_clad: f64,
core_width: f64,
wavelength: f64,
n_nodes: usize,
) -> Self {
let half_width = core_width * 3.0; let dx = 2.0 * half_width / (n_nodes - 1) as f64;
let n_profile: Vec<f64> = (0..n_nodes)
.map(|i| {
let x = -half_width + i as f64 * dx;
if x.abs() <= core_width / 2.0 {
n_core
} else {
n_clad
}
})
.collect();
Self::new(n_profile, half_width, wavelength)
}
pub fn solve(&self, n_clad: f64) -> Vec<FemMode1d> {
let n = self.n_nodes;
let dx = 2.0 * self.half_width / (n - 1) as f64;
let k0 = 2.0 * PI / self.wavelength;
let k0sq = k0 * k0;
let nint = n - 2;
let inv_dx2 = 1.0 / (dx * dx);
let mut a_mat = vec![0.0f64; nint * nint];
for i in 0..nint {
let ni = i + 1; let n_sq = self.n_profile[ni].powi(2);
a_mat[i * nint + i] = k0sq * n_sq - 2.0 * inv_dx2;
if i > 0 {
a_mat[i * nint + (i - 1)] = inv_dx2;
a_mat[(i - 1) * nint + i] = inv_dx2;
}
}
let mat = Mat::from_slice(nint, nint, &a_mat);
let evd = SymmetricEvd::compute(mat.as_ref()).expect("EVD failed");
let eigenvalues = evd.eigenvalues();
let eigenvectors = evd.eigenvectors();
let n_clad_sq = n_clad * n_clad;
let beta_sq_lo = n_clad_sq * k0sq;
let n_core = self.n_profile.iter().cloned().fold(0.0_f64, f64::max);
let beta_sq_hi = n_core * n_core * k0sq;
let mut modes: Vec<FemMode1d> = eigenvalues
.iter()
.enumerate()
.filter_map(|(idx, &beta_sq)| {
if beta_sq <= beta_sq_lo || beta_sq >= beta_sq_hi {
return None;
}
let n_eff = (beta_sq / k0sq).sqrt();
let beta = beta_sq.sqrt();
let ev_col: Vec<f64> = (0..nint).map(|i| eigenvectors[(i, idx)]).collect();
let mut field = vec![0.0f64; n];
field[1..n - 1].copy_from_slice(&ev_col);
let norm: f64 = field.iter().map(|&v| v * v).sum::<f64>() * dx;
if norm > 0.0 {
let inv_norm = 1.0 / norm.sqrt();
for v in &mut field {
*v *= inv_norm;
}
}
Some(FemMode1d { beta, n_eff, field })
})
.collect();
modes.sort_by(|a, b| {
b.n_eff
.partial_cmp(&a.n_eff)
.unwrap_or(std::cmp::Ordering::Equal)
});
modes
}
pub fn solve_vector(&self, n_modes: usize, n_clad: f64) -> Vec<FemMode1d> {
let mut modes = self.solve(n_clad);
modes.truncate(n_modes);
modes
}
pub fn compute_dispersion(
&self,
n_modes: usize,
n_lambda: usize,
lambda_range: (f64, f64),
n_clad: f64,
) -> Vec<Vec<f64>> {
let (lambda_min, lambda_max) = lambda_range;
let dl = (lambda_max - lambda_min) / (n_lambda - 1) as f64;
let mut dispersion: Vec<Vec<f64>> = vec![Vec::new(); n_modes];
for li in 0..n_lambda {
let lam = lambda_min + li as f64 * dl;
let solver = FemModeSolver1d::new(self.n_profile.clone(), self.half_width, lam);
let modes = solver.solve(n_clad);
for mi in 0..n_modes {
if mi < modes.len() {
dispersion[mi].push(modes[mi].n_eff);
}
}
}
dispersion
}
pub fn group_index(&self, mode_idx: usize, delta_lambda: f64, n_clad: f64) -> Option<f64> {
let lam = self.wavelength;
let solver_p =
FemModeSolver1d::new(self.n_profile.clone(), self.half_width, lam + delta_lambda);
let solver_m =
FemModeSolver1d::new(self.n_profile.clone(), self.half_width, lam - delta_lambda);
let modes_p = solver_p.solve(n_clad);
let modes_m = solver_m.solve(n_clad);
let modes_c = self.solve(n_clad);
let neff_c = modes_c.get(mode_idx)?.n_eff;
let neff_p = modes_p.get(mode_idx)?.n_eff;
let neff_m = modes_m.get(mode_idx)?.n_eff;
let dn_dl = (neff_p - neff_m) / (2.0 * delta_lambda);
let ng = neff_c - lam * dn_dl;
Some(ng)
}
pub fn gvd(&self, mode_idx: usize, delta_lambda: f64, n_clad: f64) -> Option<f64> {
const C: f64 = 299_792_458.0;
let lam = self.wavelength;
let solver_p =
FemModeSolver1d::new(self.n_profile.clone(), self.half_width, lam + delta_lambda);
let solver_m =
FemModeSolver1d::new(self.n_profile.clone(), self.half_width, lam - delta_lambda);
let modes_p = solver_p.solve(n_clad);
let modes_m = solver_m.solve(n_clad);
let modes_c = self.solve(n_clad);
let neff_c = modes_c.get(mode_idx)?.n_eff;
let neff_p = modes_p.get(mode_idx)?.n_eff;
let neff_m = modes_m.get(mode_idx)?.n_eff;
let d2n_dl2 = (neff_p - 2.0 * neff_c + neff_m) / (delta_lambda * delta_lambda);
let gvd = -lam / C * d2n_dl2;
Some(gvd)
}
pub fn confinement_factor(
&self,
mode_idx: usize,
region: (f64, f64),
n_clad: f64,
) -> Option<f64> {
let modes = self.solve(n_clad);
let mode = modes.get(mode_idx)?;
let x = self.x_coords();
Some(mode.confinement_factor(region.0, region.1, &x))
}
pub fn beta(&self, mode_idx: usize, n_clad: f64) -> Option<f64> {
let modes = self.solve(n_clad);
modes.get(mode_idx).map(|m| m.beta)
}
pub fn coupling_coefficient(
&self,
mode_a: usize,
mode_b: usize,
delta_n: f64,
n_clad: f64,
) -> Option<f64> {
let modes = self.solve(n_clad);
let ma = modes.get(mode_a)?;
let mb = modes.get(mode_b)?;
let x = self.x_coords();
let dx = 2.0 * self.half_width / (self.n_nodes - 1) as f64;
let n_core = self.n_profile.iter().cloned().fold(0.0_f64, f64::max);
let k0 = 2.0 * PI / self.wavelength;
let core_half = self.half_width / 3.0; let overlap: f64 = x
.iter()
.enumerate()
.filter(|(_, &xi)| xi.abs() <= core_half)
.map(|(i, _)| ma.field[i] * mb.field[i] * dx)
.sum();
let norm_a: f64 = (ma.field.iter().map(|&v| v * v).sum::<f64>() * dx).sqrt();
let norm_b: f64 = (mb.field.iter().map(|&v| v * v).sum::<f64>() * dx).sqrt();
if norm_a < 1e-30 || norm_b < 1e-30 {
return Some(0.0);
}
Some(k0 * n_core * delta_n * overlap / (norm_a * norm_b))
}
pub fn full_dispersion_analysis(&self, n_clad: f64) -> Vec<FemDispersionPoint> {
let modes = self.solve(n_clad);
let delta_lambda = self.wavelength / 100.0;
let lam = self.wavelength;
modes
.iter()
.enumerate()
.map(|(mi, m)| {
let ng = self
.group_index(mi, delta_lambda, n_clad)
.unwrap_or(m.n_eff);
let gvd = self.gvd(mi, delta_lambda, n_clad).unwrap_or(0.0);
FemDispersionPoint {
lambda: lam,
n_eff: m.n_eff,
n_g: ng,
gvd,
}
})
.collect()
}
pub fn x_coords(&self) -> Vec<f64> {
let dx = 2.0 * self.half_width / (self.n_nodes - 1) as f64;
(0..self.n_nodes)
.map(|i| -self.half_width + i as f64 * dx)
.collect()
}
}
impl FemMode1d {
pub fn intensity(&self) -> Vec<f64> {
self.field.iter().map(|&e| e * e).collect()
}
pub fn confinement_factor(&self, x_lo: f64, x_hi: f64, x_coords: &[f64]) -> f64 {
let n = self.field.len();
let total: f64 = self.field.iter().map(|&e| e * e).sum();
if total < 1e-30 {
return 0.0;
}
let core: f64 = (0..n)
.filter(|&i| x_coords[i] >= x_lo && x_coords[i] <= x_hi)
.map(|i| self.field[i] * self.field[i])
.sum();
core / total
}
pub fn effective_mode_area(&self, dx: f64) -> f64 {
let sum2: f64 = self.field.iter().map(|&v| v * v).sum::<f64>() * dx;
let sum4: f64 = self.field.iter().map(|&v| v.powi(4)).sum::<f64>() * dx;
if sum4 < 1e-60 {
return 0.0;
}
sum2 * sum2 / sum4
}
pub fn peak_intensity_index(&self) -> usize {
self.field
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| {
a.abs()
.partial_cmp(&b.abs())
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i)
.unwrap_or(0)
}
pub fn beta_at_wavelength(&self, new_lambda: f64, old_lambda: f64) -> f64 {
self.beta * (old_lambda / new_lambda)
}
}
fn power_iteration_dominant(
a_mat: &[f64],
n: usize,
beta_sq_lo: f64,
max_iter: usize,
tol: f64,
) -> Option<(f64, Vec<f64>)> {
use std::f64;
let shift = beta_sq_lo;
let mut v: Vec<f64> = (0..n).map(|i| ((i + 1) as f64).sin()).collect();
let norm0: f64 = v.iter().map(|&x| x * x).sum::<f64>().sqrt();
if norm0 < 1e-30 {
return None;
}
for vi in &mut v {
*vi /= norm0;
}
let mut lambda_old = f64::NEG_INFINITY;
let mut w = vec![0.0f64; n];
for _ in 0..max_iter {
for i in 0..n {
let diag = a_mat[i * n + i] - shift;
w[i] = diag * v[i];
if i > 0 {
w[i] += a_mat[i * n + (i - 1)] * v[i - 1];
}
if i + 1 < n {
w[i] += a_mat[i * n + (i + 1)] * v[i + 1];
}
}
let vw: f64 = v.iter().zip(w.iter()).map(|(&vi, &wi)| vi * wi).sum();
let vv: f64 = v.iter().map(|&vi| vi * vi).sum();
let lambda_new = vw / vv + shift;
let norm: f64 = w.iter().map(|&wi| wi * wi).sum::<f64>().sqrt();
if norm < 1e-30 {
break;
}
for (vi, wi) in v.iter_mut().zip(w.iter()) {
*vi = wi / norm;
}
if (lambda_new - lambda_old).abs() < tol {
return Some((lambda_new, v));
}
lambda_old = lambda_new;
}
Some((lambda_old, v))
}
pub fn solve_by_deflation(
a_mat: &[f64],
n: usize,
beta_sq_lo: f64,
beta_sq_hi: f64,
n_want: usize,
max_iter: usize,
tol: f64,
) -> Vec<(f64, Vec<f64>)> {
let mut results: Vec<(f64, Vec<f64>)> = Vec::new();
let mut a_def = a_mat.to_vec();
for _ in 0..n_want {
if let Some((bsq, ev)) = power_iteration_dominant(&a_def, n, beta_sq_lo, max_iter, tol) {
if bsq <= beta_sq_lo || bsq >= beta_sq_hi {
break;
}
let vv: f64 = ev.iter().map(|&x| x * x).sum();
if vv < 1e-30 {
break;
}
for i in 0..n {
for j in 0..n {
a_def[i * n + j] -= bsq * ev[i] * ev[j] / vv;
}
}
results.push((bsq, ev));
} else {
break;
}
}
results.sort_by(|(a, _), (b, _)| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
results
}
#[cfg(test)]
mod tests {
use super::*;
fn si_sio2_slab() -> FemModeSolver1d {
FemModeSolver1d::slab(3.48, 1.44, 220e-9, 1550e-9, 101)
}
#[test]
fn fem_finds_at_least_one_mode() {
let solver = si_sio2_slab();
let modes = solver.solve(1.44);
assert!(!modes.is_empty(), "No guided modes found");
}
#[test]
fn fundamental_mode_neff_in_range() {
let solver = si_sio2_slab();
let modes = solver.solve(1.44);
assert!(!modes.is_empty());
let n_eff = modes[0].n_eff;
assert!(n_eff > 1.44 && n_eff < 3.48, "n_eff={n_eff}");
}
#[test]
fn field_normalized() {
let solver = si_sio2_slab();
let modes = solver.solve(1.44);
if modes.is_empty() {
return;
}
let dx = 2.0 * solver.half_width / (solver.n_nodes - 1) as f64;
let norm: f64 = modes[0].field.iter().map(|&v| v * v).sum::<f64>() * dx;
assert!((norm - 1.0).abs() < 0.1, "norm={norm}");
}
#[test]
fn fundamental_mode_is_symmetric() {
let solver = si_sio2_slab();
let modes = solver.solve(1.44);
if modes.is_empty() {
return;
}
let f = &modes[0].field;
let n = f.len();
let mid = n / 2;
let asym: f64 = (0..mid)
.map(|i| (f[i].abs() - f[n - 1 - i].abs()).abs())
.sum::<f64>()
/ mid as f64;
assert!(asym < 0.1, "asymmetry={asym}");
}
#[test]
fn x_coords_correct_length() {
let solver = si_sio2_slab();
let x = solver.x_coords();
assert_eq!(x.len(), solver.n_nodes);
}
#[test]
fn x_coords_span_domain() {
let solver = si_sio2_slab();
let x = solver.x_coords();
assert!((x[0] + solver.half_width).abs() < 1e-20, "x[0]={}", x[0]);
assert!((x[x.len() - 1] - solver.half_width).abs() < 1e-20);
}
#[test]
fn confinement_factor_in_01() {
let solver = si_sio2_slab();
let modes = solver.solve(1.44);
if modes.is_empty() {
return;
}
let x = solver.x_coords();
let gamma = modes[0].confinement_factor(-110e-9, 110e-9, &x);
assert!((0.0..=1.0).contains(&gamma), "gamma={gamma}");
}
#[test]
fn intensity_is_field_squared() {
let mode = FemMode1d {
beta: 1e7,
n_eff: 2.0,
field: vec![1.0, -2.0, 0.5],
};
let intensity = mode.intensity();
assert!((intensity[0] - 1.0).abs() < 1e-12);
assert!((intensity[1] - 4.0).abs() < 1e-12);
assert!((intensity[2] - 0.25).abs() < 1e-12);
}
#[test]
fn solve_vector_returns_at_most_n_modes() {
let solver = si_sio2_slab();
let modes = solver.solve_vector(1, 1.44);
assert!(modes.len() <= 1, "Expected at most 1 mode");
}
#[test]
fn beta_method_returns_some() {
let solver = si_sio2_slab();
let beta = solver.beta(0, 1.44);
assert!(beta.is_some(), "beta(mode 0) should be Some");
let b = beta.expect("guaranteed Some");
assert!(b > 0.0, "beta should be positive");
}
#[test]
fn confinement_factor_method_returns_value() {
let solver = si_sio2_slab();
let cf = solver.confinement_factor(0, (-110e-9, 110e-9), 1.44);
if let Some(gamma) = cf {
assert!((0.0..=1.0).contains(&gamma), "gamma={gamma}");
}
}
#[test]
fn group_index_is_reasonable() {
let solver = si_sio2_slab();
let ng = solver.group_index(0, 1e-9, 1.44);
if let Some(ng_val) = ng {
assert!(ng_val > 1.0 && ng_val < 10.0, "n_g={ng_val:.3}");
}
}
#[test]
fn effective_mode_area_positive() {
let solver = si_sio2_slab();
let modes = solver.solve(1.44);
if modes.is_empty() {
return;
}
let dx = 2.0 * solver.half_width / (solver.n_nodes - 1) as f64;
let a_eff = modes[0].effective_mode_area(dx);
assert!(a_eff > 0.0, "A_eff={a_eff:.2e}");
}
#[test]
fn compute_dispersion_returns_data() {
let solver = si_sio2_slab();
let disp = solver.compute_dispersion(1, 5, (1400e-9, 1600e-9), 1.44);
assert!(!disp.is_empty(), "Dispersion data should not be empty");
assert!(!disp[0].is_empty(), "Mode 0 should have dispersion data");
}
#[test]
fn full_dispersion_analysis_returns_points() {
let solver = si_sio2_slab();
let pts = solver.full_dispersion_analysis(1.44);
assert!(!pts.is_empty(), "Should find at least one dispersion point");
for pt in &pts {
assert!(pt.n_eff > 1.0, "n_eff should be > 1");
assert!(pt.n_g > 0.5, "n_g should be > 0.5");
}
}
#[test]
fn peak_intensity_index_valid() {
let mode = FemMode1d {
beta: 1e7,
n_eff: 2.0,
field: vec![0.1, 3.0, 0.5, 1.0],
};
let idx = mode.peak_intensity_index();
assert_eq!(idx, 1, "Peak should be at index 1");
}
#[test]
fn dispersion_monotone_in_silicon() {
let solver = si_sio2_slab();
let disp = solver.compute_dispersion(1, 3, (1450e-9, 1650e-9), 1.44);
if disp.is_empty() || disp[0].len() < 2 {
return;
}
for &neff in &disp[0] {
assert!(neff > 1.44 && neff < 3.48, "n_eff={neff} out of range");
}
}
}