use num_complex::Complex64;
use std::f64::consts::PI;
use crate::error::OxiPhotonError;
use crate::fdtd::monitor::flux::FluxNormal;
#[derive(Debug, Clone)]
pub struct EigenModeProfile {
pub n_eff: f64,
pub field_ex: Vec<f64>,
pub field_ey: Vec<f64>,
pub field_hx: Vec<f64>,
pub field_hy: Vec<f64>,
pub power: f64,
pub mode_number: usize,
pub ni: usize,
pub nj: usize,
}
impl EigenModeProfile {
pub fn new(n_eff: f64, ni: usize, nj: usize) -> Self {
let ncells = ni * nj;
Self {
n_eff,
field_ex: vec![0.0; ncells],
field_ey: vec![0.0; ncells],
field_hx: vec![0.0; ncells],
field_hy: vec![0.0; ncells],
power: 0.0,
mode_number: 0,
ni,
nj,
}
}
pub fn overlap(&self, other: &EigenModeProfile) -> Complex64 {
let re: f64 = self
.field_ex
.iter()
.zip(other.field_ex.iter())
.map(|(a, b)| a * b)
.sum::<f64>()
+ self
.field_ey
.iter()
.zip(other.field_ey.iter())
.map(|(a, b)| a * b)
.sum::<f64>();
Complex64::new(re, 0.0)
}
pub fn self_overlap(&self) -> f64 {
self.field_ex.iter().map(|v| v * v).sum::<f64>()
+ self.field_ey.iter().map(|v| v * v).sum::<f64>()
}
pub fn normalize(&mut self) -> Result<(), OxiPhotonError> {
let norm2 = self.self_overlap();
if norm2 < 1e-30 {
return Err(OxiPhotonError::NumericalError(
"Cannot normalise eigenmode with zero energy".to_string(),
));
}
let norm = norm2.sqrt();
for v in self.field_ex.iter_mut() {
*v /= norm;
}
for v in self.field_ey.iter_mut() {
*v /= norm;
}
for v in self.field_hx.iter_mut() {
*v /= norm;
}
for v in self.field_hy.iter_mut() {
*v /= norm;
}
self.power = 1.0;
Ok(())
}
pub fn gaussian(n_eff: f64, ni: usize, nj: usize, sigma_i: f64, sigma_j: f64) -> Self {
let mut profile = Self::new(n_eff, ni, nj);
let ci = (ni as f64 - 1.0) / 2.0;
let cj = (nj as f64 - 1.0) / 2.0;
let sig_i2 = if sigma_i > 0.0 {
2.0 * sigma_i * sigma_i
} else {
1.0
};
let sig_j2 = if sigma_j > 0.0 {
2.0 * sigma_j * sigma_j
} else {
1.0
};
for i in 0..ni {
for j in 0..nj {
let di = (i as f64 - ci) / ni.max(1) as f64 * ni as f64;
let dj = (j as f64 - cj) / nj.max(1) as f64 * nj as f64;
let val = (-(di * di) / sig_i2 - (dj * dj) / sig_j2).exp();
let idx = i * nj + j;
profile.field_ex[idx] = val;
profile.field_hy[idx] = n_eff * val / 376.73;
}
}
profile.power = profile.self_overlap();
profile
}
}
pub struct EigenModeMonitor {
pub normal: FluxNormal,
pub index: usize,
pub i_range: (usize, usize),
pub j_range: (usize, usize),
pub modes: Vec<EigenModeProfile>,
pub frequencies: Vec<f64>,
pub t_coeffs: Vec<Vec<Complex64>>,
pub r_coeffs: Vec<Vec<Complex64>>,
pub dt: f64,
dft_ex: Vec<Vec<Complex64>>,
dft_ey: Vec<Vec<Complex64>>,
dft_hx: Vec<Vec<Complex64>>,
dft_hy: Vec<Vec<Complex64>>,
n_samples: usize,
}
impl EigenModeMonitor {
pub fn new(
normal: FluxNormal,
index: usize,
i_range: (usize, usize),
j_range: (usize, usize),
frequencies: Vec<f64>,
dt: f64,
) -> Self {
let nf = frequencies.len();
let ni = i_range.1.saturating_sub(i_range.0);
let nj = j_range.1.saturating_sub(j_range.0);
let ncells = ni * nj;
Self {
normal,
index,
i_range,
j_range,
modes: Vec::new(),
frequencies,
t_coeffs: vec![Vec::<Complex64>::new(); nf],
r_coeffs: vec![Vec::<Complex64>::new(); nf],
dt,
dft_ex: vec![vec![Complex64::new(0.0, 0.0); ncells]; nf],
dft_ey: vec![vec![Complex64::new(0.0, 0.0); ncells]; nf],
dft_hx: vec![vec![Complex64::new(0.0, 0.0); ncells]; nf],
dft_hy: vec![vec![Complex64::new(0.0, 0.0); ncells]; nf],
n_samples: 0,
}
}
pub fn add_mode(&mut self, mode: EigenModeProfile) {
let n_modes = self.modes.len() + 1;
let nf = self.frequencies.len();
for fi in 0..nf {
self.t_coeffs[fi].resize(n_modes, Complex64::new(0.0, 0.0));
self.r_coeffs[fi].resize(n_modes, Complex64::new(0.0, 0.0));
}
self.modes.push(mode);
}
#[allow(clippy::too_many_arguments)]
pub fn accumulate(
&mut self,
time_step: usize,
ex: &[f64],
ey: &[f64],
hx: &[f64],
hy: &[f64],
nx: usize,
ny: usize,
nz: usize,
) {
let t = time_step as f64 * self.dt;
let phasors: Vec<Complex64> = self
.frequencies
.iter()
.map(|&f| {
let phase = -2.0 * PI * f * t;
Complex64::new(phase.cos(), phase.sin()) * self.dt
})
.collect();
let ni_start = self.i_range.0;
let ni_end = self.i_range.1;
let nj_start = self.j_range.0;
let nj_end = self.j_range.1;
let n_cols = nj_end.saturating_sub(nj_start);
match self.normal {
FluxNormal::Z => {
let k = self.index;
if k >= nz {
return;
}
for i in ni_start..ni_end.min(nx) {
for j in nj_start..nj_end.min(ny) {
let field_idx = i * ny * nz + j * nz + k;
let local_idx = (i - ni_start) * n_cols + (j - nj_start);
let ex_v = ex.get(field_idx).copied().unwrap_or(0.0);
let ey_v = ey.get(field_idx).copied().unwrap_or(0.0);
let hx_v = hx.get(field_idx).copied().unwrap_or(0.0);
let hy_v = hy.get(field_idx).copied().unwrap_or(0.0);
for (fi, &phasor) in phasors.iter().enumerate() {
self.dft_ex[fi][local_idx] += ex_v * phasor;
self.dft_ey[fi][local_idx] += ey_v * phasor;
self.dft_hx[fi][local_idx] += hx_v * phasor;
self.dft_hy[fi][local_idx] += hy_v * phasor;
}
}
}
}
FluxNormal::X => {
let i = self.index;
if i >= nx {
return;
}
for j in ni_start..ni_end.min(ny) {
for k in nj_start..nj_end.min(nz) {
let field_idx = i * ny * nz + j * nz + k;
let local_idx = (j - ni_start) * n_cols + (k - nj_start);
let ex_v = ex.get(field_idx).copied().unwrap_or(0.0);
let ey_v = ey.get(field_idx).copied().unwrap_or(0.0);
let hx_v = hx.get(field_idx).copied().unwrap_or(0.0);
let hy_v = hy.get(field_idx).copied().unwrap_or(0.0);
for (fi, &phasor) in phasors.iter().enumerate() {
self.dft_ex[fi][local_idx] += ex_v * phasor;
self.dft_ey[fi][local_idx] += ey_v * phasor;
self.dft_hx[fi][local_idx] += hx_v * phasor;
self.dft_hy[fi][local_idx] += hy_v * phasor;
}
}
}
}
FluxNormal::Y => {
let j = self.index;
if j >= ny {
return;
}
for i in ni_start..ni_end.min(nx) {
for k in nj_start..nj_end.min(nz) {
let field_idx = i * ny * nz + j * nz + k;
let local_idx = (i - ni_start) * n_cols + (k - nj_start);
let ex_v = ex.get(field_idx).copied().unwrap_or(0.0);
let ey_v = ey.get(field_idx).copied().unwrap_or(0.0);
let hx_v = hx.get(field_idx).copied().unwrap_or(0.0);
let hy_v = hy.get(field_idx).copied().unwrap_or(0.0);
for (fi, &phasor) in phasors.iter().enumerate() {
self.dft_ex[fi][local_idx] += ex_v * phasor;
self.dft_ey[fi][local_idx] += ey_v * phasor;
self.dft_hx[fi][local_idx] += hx_v * phasor;
self.dft_hy[fi][local_idx] += hy_v * phasor;
}
}
}
}
}
self.n_samples += 1;
}
pub fn compute_coefficients(&mut self) {
let nf = self.frequencies.len();
let n_modes = self.modes.len();
let mode_data: Vec<(Vec<f64>, Vec<f64>, f64)> = self
.modes
.iter()
.map(|m| {
let norm2 = m.self_overlap().max(1e-60);
(m.field_ex.clone(), m.field_ey.clone(), norm2.sqrt())
})
.collect();
for fi in 0..nf {
for (mi, (ex_m, ey_m, norm)) in mode_data.iter().enumerate().take(n_modes) {
let norm = *norm;
let overlap: Complex64 = ex_m
.iter()
.zip(self.dft_ex[fi].iter())
.map(|(m_v, d_v)| *m_v * d_v)
.sum::<Complex64>()
+ ey_m
.iter()
.zip(self.dft_ey[fi].iter())
.map(|(m_v, d_v)| *m_v * d_v)
.sum::<Complex64>();
let coeff = overlap / norm;
if mi < self.t_coeffs[fi].len() {
self.t_coeffs[fi][mi] = coeff;
}
let r_overlap: Complex64 = ex_m
.iter()
.zip(self.dft_ex[fi].iter())
.map(|(m_v, d_v)| *m_v * d_v.conj())
.sum::<Complex64>()
+ ey_m
.iter()
.zip(self.dft_ey[fi].iter())
.map(|(m_v, d_v)| *m_v * d_v.conj())
.sum::<Complex64>();
if mi < self.r_coeffs[fi].len() {
self.r_coeffs[fi][mi] = r_overlap / norm;
}
}
}
}
pub fn transmission(&self, mode_idx: usize) -> Vec<f64> {
self.t_coeffs
.iter()
.map(|row| row.get(mode_idx).map(|c| c.norm_sqr()).unwrap_or(0.0))
.collect()
}
pub fn reflection(&self, mode_idx: usize) -> Vec<f64> {
self.r_coeffs
.iter()
.map(|row| row.get(mode_idx).map(|c| c.norm_sqr()).unwrap_or(0.0))
.collect()
}
pub fn s_parameter(&self, in_mode: usize, out_mode: usize) -> Vec<Complex64> {
self.t_coeffs
.iter()
.map(|row| {
let a_in = row
.get(in_mode)
.copied()
.unwrap_or(Complex64::new(0.0, 0.0));
let a_out = row
.get(out_mode)
.copied()
.unwrap_or(Complex64::new(0.0, 0.0));
let denom = a_in.norm();
if denom < 1e-60 {
Complex64::new(0.0, 0.0)
} else {
a_out / a_in.norm()
}
})
.collect()
}
pub fn total_transmission(&self) -> Vec<f64> {
let n_modes = self.modes.len();
(0..self.frequencies.len())
.map(|fi| {
(0..n_modes)
.map(|mi| {
self.t_coeffs[fi]
.get(mi)
.map(|c| c.norm_sqr())
.unwrap_or(0.0)
})
.sum::<f64>()
})
.collect()
}
pub fn insertion_loss_db(&self, mode_idx: usize) -> Vec<f64> {
self.transmission(mode_idx)
.into_iter()
.map(|t| {
if t > 0.0 {
-10.0 * t.log10()
} else {
f64::INFINITY
}
})
.collect()
}
#[allow(dead_code)]
fn get_dft_at(
&self,
fi: usize,
i: usize,
j: usize,
) -> (Complex64, Complex64, Complex64, Complex64) {
let n_cols = self.j_range.1.saturating_sub(self.j_range.0);
let idx = i * n_cols + j;
let ex = self
.dft_ex
.get(fi)
.and_then(|v| v.get(idx))
.copied()
.unwrap_or_default();
let ey = self
.dft_ey
.get(fi)
.and_then(|v| v.get(idx))
.copied()
.unwrap_or_default();
let hx = self
.dft_hx
.get(fi)
.and_then(|v| v.get(idx))
.copied()
.unwrap_or_default();
let hy = self
.dft_hy
.get(fi)
.and_then(|v| v.get(idx))
.copied()
.unwrap_or_default();
(ex, ey, hx, hy)
}
pub fn n_samples(&self) -> usize {
self.n_samples
}
pub fn frequencies(&self) -> &[f64] {
&self.frequencies
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_monitor(n_freq: usize) -> EigenModeMonitor {
let freqs: Vec<f64> = (0..n_freq).map(|i| (i + 1) as f64 * 100e12).collect();
EigenModeMonitor::new(FluxNormal::Z, 4, (0, 4), (0, 4), freqs, 1e-17)
}
#[test]
fn test_eigenmode_profile_creation() {
let profile = EigenModeProfile::new(1.5, 8, 6);
assert_eq!(
profile.field_ex.len(),
48,
"field_ex should have ni*nj elements"
);
assert_eq!(profile.field_ey.len(), 48);
assert_eq!(profile.field_hx.len(), 48);
assert_eq!(profile.field_hy.len(), 48);
assert_eq!(profile.ni, 8);
assert_eq!(profile.nj, 6);
assert!((profile.n_eff - 1.5).abs() < 1e-12);
}
#[test]
fn test_gaussian_profile() {
let profile = EigenModeProfile::gaussian(1.5, 16, 16, 3.0, 3.0);
assert_eq!(profile.field_ex.len(), 256);
assert!(
profile.power > 0.0,
"Gaussian mode power must be positive: {}",
profile.power
);
let ci = 7usize;
let cj = 7usize;
let centre_val = profile.field_ex[ci * 16 + cj];
assert!(
centre_val > 0.5,
"centre value should be near 1: {centre_val}"
);
}
#[test]
fn test_mode_normalize() {
let mut profile = EigenModeProfile::gaussian(1.5, 10, 10, 2.0, 2.0);
profile
.normalize()
.expect("normalize should succeed for non-zero mode");
let so = profile.self_overlap();
assert!(
(so - 1.0).abs() < 1e-10,
"self_overlap after normalise = {so}"
);
assert!((profile.power - 1.0).abs() < 1e-10);
}
#[test]
fn test_overlap_orthogonal() {
let mut a = EigenModeProfile::new(1.5, 4, 4);
let mut b = EigenModeProfile::new(1.5, 4, 4);
for i in 0..4 {
for j in 0..2 {
a.field_ex[i * 4 + j] = 1.0;
}
}
for i in 0..4 {
for j in 2..4 {
b.field_ex[i * 4 + j] = 1.0;
}
}
let ov = a.overlap(&b);
assert!(
ov.re.abs() < 1e-12,
"disjoint modes should have zero overlap: {}",
ov.re
);
}
#[test]
fn test_monitor_creation() {
let freqs = vec![100e12, 150e12, 200e12];
let mon = EigenModeMonitor::new(FluxNormal::Z, 4, (0, 8), (0, 8), freqs.clone(), 1e-17);
assert_eq!(mon.frequencies.len(), 3);
assert_eq!(mon.dft_ex.len(), 3, "DFT arrays should have n_freq rows");
assert_eq!(
mon.dft_ex[0].len(),
64,
"DFT arrays should have ni*nj cells"
);
assert_eq!(mon.n_samples(), 0);
}
#[test]
fn test_monitor_accumulate() {
let mut mon = make_monitor(1);
let nx = 8;
let ny = 8;
let nz = 8;
let n = nx * ny * nz;
let ex = vec![1.0_f64; n];
let ey = vec![0.0_f64; n];
let hx = vec![0.0_f64; n];
let hy = vec![0.0_f64; n];
for step in 0..100usize {
mon.accumulate(step, &ex, &ey, &hx, &hy, nx, ny, nz);
}
assert_eq!(mon.n_samples(), 100);
let (dft_ex, _, _, _) = mon.get_dft_at(0, 0, 0);
assert!(dft_ex.re.is_finite(), "DFT real part should be finite");
assert!(dft_ex.im.is_finite(), "DFT imag part should be finite");
}
#[test]
fn test_insertion_loss_calculation() {
let freqs = vec![100e12];
let mut mon = EigenModeMonitor::new(FluxNormal::Z, 4, (0, 2), (0, 2), freqs, 1e-17);
let mut mode = EigenModeProfile::new(1.5, 2, 2);
mode.field_ex = vec![1.0, 0.0, 0.0, 0.0];
mode.field_ey = vec![0.0, 0.0, 0.0, 0.0];
mode.power = 1.0;
mon.add_mode(mode);
let amp = 0.5_f64.sqrt();
mon.t_coeffs[0][0] = Complex64::new(amp, 0.0);
let il = mon.insertion_loss_db(0);
assert_eq!(il.len(), 1);
let il_val = il[0];
assert!(
(il_val - 3.0103).abs() < 0.001,
"Expected IL ≈ 3.01 dB for T=0.5, got {il_val}"
);
}
}