use num_complex::Complex64;
use std::f64::consts::PI;
const C_LIGHT: f64 = 2.997_924_58e8;
pub struct FewModeFiber {
pub n_modes: usize,
pub core_radius_um: f64,
pub n_core: f64,
pub n_clad: f64,
pub length_km: f64,
pub loss_db_per_km: Vec<f64>,
pub beta_coeffs: Vec<Vec<f64>>,
pub wavelength: f64,
}
impl FewModeFiber {
pub fn new_2mode(length_km: f64, wavelength: f64) -> Self {
let n_modes = 2;
let k0 = 2.0 * PI / wavelength;
let n_core = 1.455_f64;
let n_clad = 1.444_f64;
let b0_01 = n_core * k0;
let b0_11 = (n_core - 0.002) * k0;
let b1_01 = 1.4682_f64 / C_LIGHT;
let b1_11 = b1_01 + 2.0e-10_f64;
let d_01 = 18.0e-6_f64; let d_11 = 17.0e-6_f64;
let b2_01 = -d_01 * wavelength * wavelength / (2.0 * PI * C_LIGHT);
let b2_11 = -d_11 * wavelength * wavelength / (2.0 * PI * C_LIGHT);
let b3 = 0.08e-39_f64;
Self {
n_modes,
core_radius_um: 9.0,
n_core,
n_clad,
length_km,
loss_db_per_km: vec![0.20, 0.21],
beta_coeffs: vec![vec![b0_01, b1_01, b2_01, b3], vec![b0_11, b1_11, b2_11, b3]],
wavelength,
}
}
pub fn new_6mode(length_km: f64, wavelength: f64) -> Self {
let n_modes = 6;
let k0 = 2.0 * PI / wavelength;
let n_core = 1.457_f64;
let n_clad = 1.440_f64;
let b2_base = -2.0e-26_f64; let b3 = 0.08e-39_f64;
let delta_b1: [f64; 6] = [0.0, 1.0e-10, 1.0e-10, 3.0e-10, 3.0e-10, 3.5e-10];
let b1_base = 1.4700_f64 / C_LIGHT;
let loss = [0.20, 0.21, 0.21, 0.22, 0.22, 0.22_f64];
let coeffs: Vec<Vec<f64>> = (0..n_modes)
.map(|i| {
vec![
n_core * k0 - i as f64 * 50.0, b1_base + delta_b1[i],
b2_base,
b3,
]
})
.collect();
Self {
n_modes,
core_radius_um: 13.0,
n_core,
n_clad,
length_km,
loss_db_per_km: loss.to_vec(),
beta_coeffs: coeffs,
wavelength,
}
}
pub fn new_10mode(length_km: f64, wavelength: f64) -> Self {
let n_modes = 10;
let k0 = 2.0 * PI / wavelength;
let n_core = 1.460_f64;
let n_clad = 1.440_f64;
let b2_base = -2.0e-26_f64;
let b3 = 0.08e-39_f64;
let b1_base = 1.472_f64 / C_LIGHT;
let delta_b1_ps_km: [f64; 10] = [
0.0, 80.0, 80.0, 200.0, 200.0, 220.0, 400.0, 400.0, 500.0, 500.0,
];
let coeffs: Vec<Vec<f64>> = (0..n_modes)
.map(|i| {
let db1 = delta_b1_ps_km[i] * 1.0e-12 / 1.0e3; vec![n_core * k0 - i as f64 * 30.0, b1_base + db1, b2_base, b3]
})
.collect();
let loss: Vec<f64> = (0..n_modes).map(|i| 0.20 + i as f64 * 0.005).collect();
Self {
n_modes,
core_radius_um: 17.0,
n_core,
n_clad,
length_km,
loss_db_per_km: loss,
beta_coeffs: coeffs,
wavelength,
}
}
pub fn dgd_ps(&self, mode_a: usize, mode_b: usize) -> f64 {
if mode_a >= self.n_modes || mode_b >= self.n_modes {
return 0.0;
}
let b1_a = self.beta_coeffs[mode_a].get(1).copied().unwrap_or(0.0);
let b1_b = self.beta_coeffs[mode_b].get(1).copied().unwrap_or(0.0);
(b1_a - b1_b).abs() * self.length_km * 1.0e3 * 1.0e12 }
pub fn group_delay_spread_ps(&self) -> f64 {
let b1s: Vec<f64> = self
.beta_coeffs
.iter()
.map(|c| c.get(1).copied().unwrap_or(0.0))
.collect();
let b1_max = b1s.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let b1_min = b1s.iter().cloned().fold(f64::INFINITY, f64::min);
(b1_max - b1_min) * self.length_km * 1.0e3 * 1.0e12
}
pub fn dispersion_ps_per_nm(&self, mode: usize) -> f64 {
if mode >= self.n_modes {
return 0.0;
}
let b2 = self.beta_coeffs[mode].get(2).copied().unwrap_or(0.0);
let d_si = -2.0 * PI * C_LIGHT / (self.wavelength * self.wavelength) * b2;
let d_ps_nm_km = d_si * 1.0e6;
d_ps_nm_km * self.length_km }
pub fn mimo_capacity_tbps(&self, launch_power_dbm: f64, snr_db: f64, baud_gbaud: f64) -> f64 {
let snr_linear = 10.0_f64.powf(snr_db / 10.0);
let bits_per_symbol = (1.0 + snr_linear).log2();
let avg_loss = self.average_loss_db_per_km() * self.length_km;
let power_at_rx_dbm = launch_power_dbm - avg_loss;
let power_penalty = if power_at_rx_dbm < -30.0 { 0.1 } else { 1.0 };
let bw_hz = baud_gbaud * 1.0e9;
self.n_modes as f64 * bw_hz * bits_per_symbol * power_penalty * 1.0e-12
}
pub fn average_loss_db_per_km(&self) -> f64 {
if self.loss_db_per_km.is_empty() {
return 0.0;
}
self.loss_db_per_km.iter().sum::<f64>() / self.loss_db_per_km.len() as f64
}
pub fn propagation_matrix(&self, mode: usize, freq_offset_rad: f64) -> Complex64 {
if mode >= self.n_modes {
return Complex64::new(0.0, 0.0);
}
let coeffs = &self.beta_coeffs[mode];
let b1 = coeffs.get(1).copied().unwrap_or(0.0);
let b2 = coeffs.get(2).copied().unwrap_or(0.0);
let b3 = coeffs.get(3).copied().unwrap_or(0.0);
let dw = freq_offset_rad;
let l_m = self.length_km * 1.0e3;
let phase = (b1 * dw + 0.5 * b2 * dw * dw + b3 / 6.0 * dw * dw * dw) * l_m;
let alpha_lin =
self.loss_db_per_km[mode] * self.length_km / (10.0 / std::f64::consts::LN_10);
let amplitude = (-alpha_lin / 2.0).exp();
Complex64::new(0.0, phase).exp() * amplitude
}
}
#[derive(Debug, Clone)]
pub enum CoreLayout {
Linear,
Hexagonal { rings: usize },
Square { side: usize },
Ring { n: usize, radius_um: f64 },
}
pub struct MulticoreFiber {
pub n_cores: usize,
pub core_pitch_um: f64,
pub core_radius_um: f64,
pub n_core: f64,
pub n_clad: f64,
pub length_km: f64,
pub layout: CoreLayout,
}
impl MulticoreFiber {
pub fn new_7core(length_km: f64, _wavelength: f64) -> Self {
Self {
n_cores: 7,
core_pitch_um: 40.0,
core_radius_um: 4.5,
n_core: 1.4550,
n_clad: 1.4440,
length_km,
layout: CoreLayout::Hexagonal { rings: 1 },
}
}
pub fn new_19core(length_km: f64, _wavelength: f64) -> Self {
Self {
n_cores: 19,
core_pitch_um: 35.0,
core_radius_um: 4.2,
n_core: 1.4550,
n_clad: 1.4440,
length_km,
layout: CoreLayout::Hexagonal { rings: 2 },
}
}
pub fn core_positions(&self) -> Vec<[f64; 2]> {
match &self.layout {
CoreLayout::Linear => {
let d = self.core_pitch_um;
let offset = (self.n_cores as f64 - 1.0) / 2.0 * d;
(0..self.n_cores)
.map(|i| [i as f64 * d - offset, 0.0])
.collect()
}
CoreLayout::Hexagonal { rings } => {
let d = self.core_pitch_um;
let mut pos = vec![[0.0_f64; 2]];
for ring in 1..=*rings {
let r = ring as f64;
let dirs: [(f64, f64); 6] = [
(1.0, 0.0),
(0.5, -(3.0_f64).sqrt() / 2.0),
(-0.5, -(3.0_f64).sqrt() / 2.0),
(-1.0, 0.0),
(-0.5, (3.0_f64).sqrt() / 2.0),
(0.5, (3.0_f64).sqrt() / 2.0),
];
let mut x = r * d;
let mut y = 0.0_f64;
for (dx, dy) in dirs {
for _ in 0..ring {
if pos.len() < self.n_cores {
pos.push([x, y]);
}
x += dx * d;
y += dy * d;
}
}
}
pos.truncate(self.n_cores);
pos
}
CoreLayout::Square { side } => {
let d = self.core_pitch_um;
let offset = (*side as f64 - 1.0) / 2.0 * d;
let mut pos = Vec::new();
'outer: for row in 0..*side {
for col in 0..*side {
if pos.len() >= self.n_cores {
break 'outer;
}
pos.push([col as f64 * d - offset, row as f64 * d - offset]);
}
}
pos
}
CoreLayout::Ring { n, radius_um } => (0..*n)
.map(|i| {
let theta = 2.0 * PI * i as f64 / *n as f64;
[radius_um * theta.cos(), radius_um * theta.sin()]
})
.collect(),
}
}
pub fn coupling_coefficient(&self, wavelength: f64) -> f64 {
let a = self.core_radius_um * 1.0e-6;
let d = self.core_pitch_um * 1.0e-6;
let k0 = 2.0 * PI / wavelength;
let na2 = self.n_core * self.n_core - self.n_clad * self.n_clad;
let na = if na2 > 0.0 { na2.sqrt() } else { 0.01 };
let v = k0 * a * na;
let u = if v > 2.0 { 2.0 } else { v * 0.8 };
let w = (v * v - u * u).max(0.0).sqrt();
let alpha_eff = w / a; let gap = d - 2.0 * a;
let k0_prefactor = (u * u / (v * v * a)).abs();
k0_prefactor * (-alpha_eff * gap.max(0.0)).exp()
}
pub fn inter_core_xt_db(&self, wavelength: f64) -> f64 {
let kappa = self.coupling_coefficient(wavelength);
let k0 = 2.0 * PI / wavelength;
let beta = k0 * self.n_core; let l_m = self.length_km * 1.0e3;
let h = kappa * kappa / beta;
let xt = 2.0 * h * l_m;
if xt <= 0.0 {
return f64::NEG_INFINITY;
}
10.0 * xt.log10()
}
pub fn crosstalk_between(&self, core1: usize, core2: usize, wavelength: f64) -> f64 {
let positions = self.core_positions();
if core1 >= positions.len() || core2 >= positions.len() {
return f64::NEG_INFINITY;
}
let p1 = positions[core1];
let p2 = positions[core2];
let dist_um = ((p1[0] - p2[0]).powi(2) + (p1[1] - p2[1]).powi(2)).sqrt();
let pitch = self.core_pitch_um;
let kappa_adj = self.coupling_coefficient(wavelength);
let gap_adj = pitch - 2.0 * self.core_radius_um; let gap_pair = dist_um - 2.0 * self.core_radius_um;
let a = self.core_radius_um * 1.0e-6;
let k0 = 2.0 * PI / wavelength;
let na = (self.n_core * self.n_core - self.n_clad * self.n_clad)
.max(0.0)
.sqrt();
let v = k0 * a * na;
let u = if v > 2.0 { 2.0 } else { v * 0.8 };
let w = (v * v - u * u).max(0.0).sqrt();
let alpha_eff = w / a; let gap_adj_m = gap_adj.max(0.0) * 1.0e-6;
let gap_pair_m = gap_pair.max(0.0) * 1.0e-6;
let kappa = kappa_adj * ((-alpha_eff * (gap_pair_m - gap_adj_m)).exp());
let beta = k0 * self.n_core;
let l_m = self.length_km * 1.0e3;
let h = kappa * kappa / beta;
let xt = 2.0 * h * l_m;
if xt <= 0.0 {
return f64::NEG_INFINITY;
}
10.0 * xt.log10()
}
pub fn total_capacity_pbps(&self, se_bps_per_hz: f64, bandwidth_thz: f64) -> f64 {
self.n_cores as f64 * se_bps_per_hz * bandwidth_thz * 1.0e12 * 1.0e-15
}
pub fn cladding_diameter_um(&self) -> f64 {
let positions = self.core_positions();
let r_max = positions
.iter()
.map(|p| (p[0] * p[0] + p[1] * p[1]).sqrt())
.fold(0.0_f64, f64::max);
(r_max + self.core_radius_um + 35.0) * 2.0
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_2mode_dgd() {
let fmf = FewModeFiber::new_2mode(100.0, 1.55e-6);
let dgd = fmf.dgd_ps(0, 1);
assert!(dgd > 0.0, "DGD should be positive");
assert!(dgd < 1.0e8, "DGD should be physically plausible");
}
#[test]
fn test_group_delay_spread_monotone() {
let fmf6 = FewModeFiber::new_6mode(100.0, 1.55e-6);
let fmf2 = FewModeFiber::new_2mode(100.0, 1.55e-6);
let spread6 = fmf6.group_delay_spread_ps();
let spread2 = fmf2.group_delay_spread_ps();
assert!(spread6 >= spread2, "6-mode should have >= spread vs 2-mode");
}
#[test]
fn test_propagation_matrix_unit_amplitude_zero_offset() {
let fmf = FewModeFiber::new_2mode(0.0, 1.55e-6); let h = fmf.propagation_matrix(0, 0.0);
assert_abs_diff_eq!(h.norm(), 1.0, epsilon = 0.1);
}
#[test]
fn test_7core_positions_count() {
let mcf = MulticoreFiber::new_7core(100.0, 1.55e-6);
let pos = mcf.core_positions();
assert_eq!(pos.len(), 7, "7-core MCF should have 7 positions");
}
#[test]
fn test_19core_positions_count() {
let mcf = MulticoreFiber::new_19core(100.0, 1.55e-6);
let pos = mcf.core_positions();
assert_eq!(pos.len(), 19, "19-core MCF should have 19 positions");
}
#[test]
fn test_average_loss_positive() {
let fmf = FewModeFiber::new_6mode(100.0, 1.55e-6);
let loss = fmf.average_loss_db_per_km();
assert!(
loss > 0.0 && loss < 1.0,
"Loss should be 0.2–0.3 dB/km, got {}",
loss
);
}
#[test]
fn test_mimo_capacity_positive() {
let fmf = FewModeFiber::new_6mode(100.0, 1.55e-6);
let cap = fmf.mimo_capacity_tbps(0.0, 20.0, 32.0);
assert!(cap > 0.0, "Capacity should be positive");
}
#[test]
fn test_cladding_diameter_7core() {
let mcf = MulticoreFiber::new_7core(100.0, 1.55e-6);
let diam = mcf.cladding_diameter_um();
assert!(
diam > 80.0 && diam < 400.0,
"Cladding diameter out of range: {} µm",
diam
);
}
#[test]
fn test_capacity_scales_with_cores() {
let mcf7 = MulticoreFiber::new_7core(100.0, 1.55e-6);
let mcf19 = MulticoreFiber::new_19core(100.0, 1.55e-6);
let c7 = mcf7.total_capacity_pbps(8.0, 4.0);
let c19 = mcf19.total_capacity_pbps(8.0, 4.0);
assert!(c19 > c7, "19-core should have higher capacity than 7-core");
}
}