use num_complex::Complex64;
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct AsymmetricCoupler {
pub width_a: f64,
pub width_b: f64,
pub gap: f64,
pub length: f64,
pub n_core: f64,
pub n_clad: f64,
pub wavelength: f64,
}
impl AsymmetricCoupler {
pub fn new(
width_a: f64,
width_b: f64,
gap: f64,
length: f64,
n_core: f64,
n_clad: f64,
wavelength: f64,
) -> Self {
Self {
width_a,
width_b,
gap,
length,
n_core,
n_clad,
wavelength,
}
}
fn n_eff_for_width(&self, width: f64) -> f64 {
let k0 = 2.0 * PI / self.wavelength;
let lo = self.n_clad + 1e-10;
let hi = self.n_core - 1e-10;
if lo >= hi {
return (self.n_core + self.n_clad) / 2.0;
}
let n_scan = 500usize;
let char_fn = |n_eff: f64| -> f64 {
let kappa = (k0 * k0 * (self.n_core * self.n_core - n_eff * n_eff))
.max(0.0)
.sqrt();
let gamma = (k0 * k0 * (n_eff * n_eff - self.n_clad * self.n_clad))
.max(0.0)
.sqrt();
kappa * (kappa * width / 2.0).tan() - gamma
};
let step = (hi - lo) / n_scan as f64;
let mut f_prev = char_fn(hi);
let mut n_prev = hi;
for i in 1..=n_scan {
let n_curr = hi - i as f64 * step;
let f_curr = char_fn(n_curr);
if f_prev * f_curr < 0.0 {
if f_prev.abs() < 1e8 && f_curr.abs() < 1e8 {
let root = bisect(&char_fn, n_curr, n_prev, 60);
if let Some(r) = root {
return r;
}
}
}
f_prev = f_curr;
n_prev = n_curr;
}
(self.n_core * self.n_core * 0.8 + self.n_clad * self.n_clad * 0.2).sqrt()
}
pub fn effective_indices(&self) -> (f64, f64) {
(
self.n_eff_for_width(self.width_a),
self.n_eff_for_width(self.width_b),
)
}
pub fn phase_mismatch(&self) -> f64 {
let k0 = 2.0 * PI / self.wavelength;
let (na, nb) = self.effective_indices();
k0 * (na - nb)
}
pub fn coupling_coefficient(&self) -> f64 {
let k0 = 2.0 * PI / self.wavelength;
let (na, nb) = self.effective_indices();
let n_avg = (na + nb) / 2.0;
let gamma = (k0 * k0 * (n_avg * n_avg - self.n_clad * self.n_clad))
.max(0.0)
.sqrt();
let delta_n = n_avg - self.n_clad;
k0 * delta_n * (-gamma * self.gap).exp()
}
pub fn coupling_efficiency(&self) -> f64 {
let kappa = self.coupling_coefficient();
let delta_beta = self.phase_mismatch();
let discriminant = kappa * kappa + delta_beta * delta_beta / 4.0;
if discriminant <= 0.0 {
return 0.0;
}
let q = discriminant.sqrt();
(kappa * kappa / discriminant) * (q * self.length).sin().powi(2)
}
pub fn transfer_matrix(&self) -> [[Complex64; 2]; 2] {
let kappa = self.coupling_coefficient();
let delta_beta = self.phase_mismatch();
let discriminant = kappa * kappa + delta_beta * delta_beta / 4.0;
let q = discriminant.max(0.0).sqrt();
let l = self.length;
let i = Complex64::new(0.0, 1.0);
let phase_a = Complex64::new(0.0, -delta_beta * l / 2.0).exp();
let phase_b = Complex64::new(0.0, delta_beta * l / 2.0).exp();
let (s, c) = if q > 1e-30 {
((q * l).sin(), (q * l).cos())
} else {
(0.0, 1.0)
};
let m11 = phase_a * Complex64::new(c + delta_beta * s / (2.0 * q.max(1e-30)), 0.0);
let m12 = phase_a * (-i * kappa * s / q.max(1e-30));
let m21 = phase_b * (-i * kappa * s / q.max(1e-30));
let m22 = phase_b * Complex64::new(c - delta_beta * s / (2.0 * q.max(1e-30)), 0.0);
[[m11, m12], [m21, m22]]
}
pub fn phase_match_wavelength(&self) -> f64 {
let lambda_center = self.wavelength;
let span = lambda_center * 0.3;
let n_scan = 500usize;
let mut best_lambda = lambda_center;
let mut best_eta = 0.0_f64;
for i in 0..n_scan {
let lambda = lambda_center - span / 2.0 + i as f64 / (n_scan - 1) as f64 * span;
let coupler = AsymmetricCoupler::new(
self.width_a,
self.width_b,
self.gap,
self.length,
self.n_core,
self.n_clad,
lambda,
);
let eta = coupler.coupling_efficiency();
if eta > best_eta {
best_eta = eta;
best_lambda = lambda;
}
}
best_lambda
}
pub fn bandwidth_3db(&self) -> f64 {
let lambda_pm = self.phase_match_wavelength();
let span = lambda_pm * 0.3;
let n_scan = 500usize;
let lambda_max_eta = {
let c = AsymmetricCoupler::new(
self.width_a,
self.width_b,
self.gap,
self.length,
self.n_core,
self.n_clad,
lambda_pm,
);
c.coupling_efficiency()
};
let half_max = lambda_max_eta / 2.0;
let mut left = lambda_pm;
let mut right = lambda_pm;
let mut in_band = false;
for i in 0..n_scan {
let lambda = lambda_pm - span / 2.0 + i as f64 / (n_scan - 1) as f64 * span;
let coupler = AsymmetricCoupler::new(
self.width_a,
self.width_b,
self.gap,
self.length,
self.n_core,
self.n_clad,
lambda,
);
let eta = coupler.coupling_efficiency();
if eta >= half_max {
if !in_band {
left = lambda;
in_band = true;
}
right = lambda;
}
}
(right - left).abs()
}
pub fn through_spectrum(
&self,
lambda_min: f64,
lambda_max: f64,
n_points: usize,
) -> Vec<(f64, f64)> {
if n_points == 0 {
return Vec::new();
}
(0..n_points)
.map(|i| {
let lambda = lambda_min
+ i as f64 / (n_points - 1).max(1) as f64 * (lambda_max - lambda_min);
let coupler = AsymmetricCoupler::new(
self.width_a,
self.width_b,
self.gap,
self.length,
self.n_core,
self.n_clad,
lambda,
);
let eta_cross = coupler.coupling_efficiency();
(lambda, (1.0 - eta_cross).clamp(0.0, 1.0))
})
.collect()
}
pub fn cross_spectrum(
&self,
lambda_min: f64,
lambda_max: f64,
n_points: usize,
) -> Vec<(f64, f64)> {
if n_points == 0 {
return Vec::new();
}
(0..n_points)
.map(|i| {
let lambda = lambda_min
+ i as f64 / (n_points - 1).max(1) as f64 * (lambda_max - lambda_min);
let coupler = AsymmetricCoupler::new(
self.width_a,
self.width_b,
self.gap,
self.length,
self.n_core,
self.n_clad,
lambda,
);
let eta_cross = coupler.coupling_efficiency().clamp(0.0, 1.0);
(lambda, eta_cross)
})
.collect()
}
}
fn bisect<F: Fn(f64) -> f64>(f: &F, mut lo: f64, mut hi: f64, max_iter: usize) -> Option<f64> {
let f_lo = f(lo);
let f_hi = f(hi);
if f_lo * f_hi > 0.0 {
return None;
}
for _ in 0..max_iter {
let mid = (lo + hi) / 2.0;
let f_mid = f(mid);
if f_lo * f_mid < 0.0 {
hi = mid;
} else {
lo = mid;
}
if (hi - lo) < 1e-14 {
break;
}
}
Some((lo + hi) / 2.0)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn si_coupler() -> AsymmetricCoupler {
AsymmetricCoupler::new(0.45e-6, 0.55e-6, 0.2e-6, 50e-6, 3.476, 1.444, 1.55e-6)
}
#[test]
fn effective_indices_in_range() {
let c = si_coupler();
let (na, nb) = c.effective_indices();
assert!(na > c.n_clad && na < c.n_core, "na = {na}");
assert!(nb > c.n_clad && nb < c.n_core, "nb = {nb}");
}
#[test]
fn asymmetric_widths_give_different_n_effs() {
let c = si_coupler();
let (na, nb) = c.effective_indices();
assert!(nb > na, "na={na}, nb={nb}");
}
#[test]
fn phase_mismatch_nonzero_for_asymmetric() {
let c = si_coupler();
let db = c.phase_mismatch();
assert!(db.abs() > 0.0, "Δβ = {db}");
}
#[test]
fn coupling_coefficient_positive() {
let c = si_coupler();
assert!(c.coupling_coefficient() > 0.0);
}
#[test]
fn coupling_efficiency_between_zero_and_one() {
let c = si_coupler();
let eta = c.coupling_efficiency();
assert!((0.0..=1.0).contains(&eta), "eta = {eta}");
}
#[test]
fn through_plus_cross_approximately_conserved() {
let c = si_coupler();
let eta = c.coupling_efficiency();
let through = 1.0 - eta;
assert_relative_eq!(through + eta, 1.0, epsilon = 1e-12);
}
#[test]
fn transfer_matrix_is_2x2_and_finite() {
let c = si_coupler();
let tm = c.transfer_matrix();
for row in &tm {
for &elem in row {
assert!(elem.re.is_finite() && elem.im.is_finite());
}
}
}
#[test]
fn through_spectrum_correct_length() {
let c = si_coupler();
let spec = c.through_spectrum(1.4e-6, 1.7e-6, 100);
assert_eq!(spec.len(), 100);
}
#[test]
fn cross_spectrum_values_in_range() {
let c = si_coupler();
let spec = c.cross_spectrum(1.4e-6, 1.7e-6, 50);
for (_, t) in spec {
assert!((0.0..=1.0).contains(&t), "transmission = {t}");
}
}
#[test]
fn phase_match_wavelength_in_scan_range() {
let c = si_coupler();
let pm = c.phase_match_wavelength();
assert!(pm > 1.1e-6 && pm < 1.9e-6, "λ_pm = {pm:.3e}");
}
#[test]
fn bandwidth_3db_positive() {
let c = si_coupler();
let bw = c.bandwidth_3db();
assert!(bw >= 0.0, "BW = {bw}");
}
#[test]
fn coupling_increases_with_gap_decrease() {
let c_small_gap =
AsymmetricCoupler::new(0.45e-6, 0.55e-6, 0.1e-6, 50e-6, 3.476, 1.444, 1.55e-6);
let c_large_gap =
AsymmetricCoupler::new(0.45e-6, 0.55e-6, 0.3e-6, 50e-6, 3.476, 1.444, 1.55e-6);
assert!(
c_small_gap.coupling_coefficient() > c_large_gap.coupling_coefficient(),
"kappa(small)={:.3e}, kappa(large)={:.3e}",
c_small_gap.coupling_coefficient(),
c_large_gap.coupling_coefficient()
);
}
}