use num_complex::Complex64;
use std::f64::consts::PI;
const C0: f64 = 2.997_924_58e8;
#[derive(Debug, Clone, PartialEq)]
pub struct FiberDispersion {
pub beta2_ps2_per_km: f64,
pub beta3_ps3_per_km: f64,
pub beta4_ps4_per_km: f64,
pub center_wavelength_nm: f64,
}
impl FiberDispersion {
pub fn new(
beta2_ps2_per_km: f64,
beta3_ps3_per_km: f64,
beta4_ps4_per_km: f64,
center_wavelength_nm: f64,
) -> Self {
Self {
beta2_ps2_per_km,
beta3_ps3_per_km,
beta4_ps4_per_km,
center_wavelength_nm,
}
}
pub fn smf28() -> Self {
Self {
beta2_ps2_per_km: -21.7,
beta3_ps3_per_km: 0.12,
beta4_ps4_per_km: 0.0,
center_wavelength_nm: 1550.0,
}
}
pub fn dsf() -> Self {
Self {
beta2_ps2_per_km: 0.0,
beta3_ps3_per_km: 0.06,
beta4_ps4_per_km: 0.0,
center_wavelength_nm: 1550.0,
}
}
pub fn hnlf() -> Self {
Self {
beta2_ps2_per_km: -1.0,
beta3_ps3_per_km: 0.03,
beta4_ps4_per_km: 0.0,
center_wavelength_nm: 1550.0,
}
}
pub fn beta2_s2_per_m(&self) -> f64 {
self.beta2_ps2_per_km * 1.0e-27
}
pub fn beta3_s3_per_m(&self) -> f64 {
self.beta3_ps3_per_km * 1.0e-39
}
pub fn beta4_s4_per_m(&self) -> f64 {
self.beta4_ps4_per_km * 1.0e-51
}
pub fn dispersion_ps_per_nm_km(&self) -> f64 {
let lambda_m = self.center_wavelength_nm * 1.0e-9;
let beta2_si = self.beta2_s2_per_m();
let d_si = -(2.0 * PI * C0 / (lambda_m * lambda_m)) * beta2_si; d_si * 1.0e6
}
pub fn slope_ps_per_nm2_km(&self) -> f64 {
let lambda_m = self.center_wavelength_nm * 1.0e-9;
let beta2_si = self.beta2_s2_per_m();
let beta3_si = self.beta3_s3_per_m();
let s_si = (4.0 * PI * C0 / lambda_m.powi(3)) * beta2_si
+ (2.0 * PI * C0 / lambda_m.powi(2)).powi(2) * beta3_si;
s_si * 1.0e3
}
pub fn zdw_nm(&self) -> Option<f64> {
let lambda0_m = self.center_wavelength_nm * 1.0e-9;
let dbeta2_dlambda = -(2.0 * PI * C0 / (lambda0_m * lambda0_m)) * self.beta3_s3_per_m();
if dbeta2_dlambda.abs() < 1.0e-60 {
return None;
}
let delta_lambda_m = -self.beta2_s2_per_m() / dbeta2_dlambda;
let zdw_m = lambda0_m + delta_lambda_m;
if zdw_m > 0.0 && zdw_m.is_finite() {
Some(zdw_m * 1.0e9) } else {
None
}
}
pub fn dispersion_operator(&self, omega: &[f64], dz_m: f64) -> Vec<Complex64> {
let b2 = self.beta2_s2_per_m();
let b3 = self.beta3_s3_per_m();
let b4 = self.beta4_s4_per_m();
omega
.iter()
.map(|&w| {
let phase =
(b2 / 2.0 * w * w + b3 / 6.0 * w * w * w + b4 / 24.0 * w.powi(4)) * dz_m;
Complex64::new(0.0, phase).exp()
})
.collect()
}
pub fn gvd_length_m(&self, fwhm_ps: f64) -> f64 {
let b2_abs = self.beta2_s2_per_m().abs();
if b2_abs < 1.0e-60 {
return f64::INFINITY;
}
let t0_s = fwhm_ps * 1.0e-12 / (2.0 * 2.0_f64.ln().sqrt());
t0_s * t0_s / b2_abs
}
pub fn broadening_factor(&self, z_km: f64, fwhm_ps: f64) -> f64 {
let ld_m = self.gvd_length_m(fwhm_ps);
if ld_m.is_infinite() || ld_m < 1.0e-30 {
return 1.0;
}
let z_m = z_km * 1.0e3;
let ratio = z_m / ld_m;
(1.0 + ratio * ratio).sqrt()
}
}
#[derive(Debug, Clone, Default)]
pub struct DispersionMap {
pub spans: Vec<(FiberDispersion, f64)>,
}
impl DispersionMap {
pub fn new() -> Self {
Self { spans: Vec::new() }
}
pub fn add_span(&mut self, dispersion: FiberDispersion, length_km: f64) {
self.spans.push((dispersion, length_km));
}
pub fn net_dispersion_ps2(&self) -> f64 {
self.spans
.iter()
.map(|(disp, length)| disp.beta2_ps2_per_km * length)
.sum()
}
pub fn average_dispersion_ps2_per_km(&self) -> f64 {
let total_l = self.total_length_km();
if total_l < 1.0e-30 {
return 0.0;
}
self.net_dispersion_ps2() / total_l
}
pub fn total_length_km(&self) -> f64 {
self.spans.iter().map(|(_, l)| l).sum()
}
pub fn is_compensated(&self, threshold_ps2: f64) -> bool {
self.net_dispersion_ps2().abs() < threshold_ps2
}
pub fn accumulated_dispersion_profile(&self) -> Vec<(f64, f64)> {
let mut position = 0.0_f64;
let mut net = 0.0_f64;
let mut profile = vec![(0.0, 0.0)];
for (disp, length) in &self.spans {
position += length;
net += disp.beta2_ps2_per_km * length;
profile.push((position, net));
}
profile
}
pub fn peak_excursion_ps2(&self) -> f64 {
self.accumulated_dispersion_profile()
.iter()
.map(|(_, d)| d.abs())
.fold(0.0_f64, f64::max)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_smf28_dispersion() {
let fiber = FiberDispersion::smf28();
let d = fiber.dispersion_ps_per_nm_km();
assert!(
d > 0.0,
"SMF-28 anomalous dispersion should give D > 0, got {d}"
);
assert!(
d > 14.0 && d < 22.0,
"SMF-28 D expected ~17 ps/(nm·km), got {d:.3}"
);
}
#[test]
fn test_dispersion_operator_shape() {
let fiber = FiberDispersion::smf28();
let n = 256_usize;
let omega: Vec<f64> = (0..n)
.map(|i| (i as f64 - n as f64 / 2.0) * 1.0e12)
.collect();
let op = fiber.dispersion_operator(&omega, 100.0);
assert_eq!(
op.len(),
n,
"Dispersion operator output length must match input omega length"
);
for v in &op {
assert_relative_eq!(v.norm(), 1.0, max_relative = 1.0e-12);
}
}
#[test]
fn test_broadening_factor_increases_with_z() {
let fiber = FiberDispersion::smf28();
let fwhm_ps = 1.0;
let b1 = fiber.broadening_factor(1.0, fwhm_ps);
let b2 = fiber.broadening_factor(10.0, fwhm_ps);
let b3 = fiber.broadening_factor(100.0, fwhm_ps);
assert!(
b1 > 1.0 && b2 > b1 && b3 > b2,
"Broadening factor must increase monotonically: {b1:.3} < {b2:.3} < {b3:.3}"
);
}
#[test]
fn test_dispersion_map_total_length() {
let mut map = DispersionMap::new();
map.add_span(FiberDispersion::smf28(), 80.0);
map.add_span(FiberDispersion::new(350.0, 0.0, 0.0, 1550.0), 4.0); assert_relative_eq!(map.total_length_km(), 84.0, max_relative = 1.0e-10);
}
#[test]
fn test_dispersion_map_net_dispersion() {
let mut map = DispersionMap::new();
map.add_span(FiberDispersion::smf28(), 80.0);
map.add_span(FiberDispersion::new(21.7, 0.0, 0.0, 1550.0), 80.0);
let net = map.net_dispersion_ps2();
assert_relative_eq!(net, 0.0, epsilon = 1.0e-6);
assert!(map.is_compensated(1.0));
}
#[test]
fn test_zdw_nm_smf28() {
let fiber = FiberDispersion::smf28();
let zdw = fiber.zdw_nm();
assert!(zdw.is_some(), "SMF-28 should have a finite ZDW");
let zdw_val = zdw.expect("zdw should be Some");
assert!(
zdw_val > 100.0 && zdw_val < 5000.0,
"ZDW out of plausible range: {zdw_val}"
);
}
#[test]
fn test_gvd_length_scaling() {
let fiber = FiberDispersion::smf28();
let ld1 = fiber.gvd_length_m(1.0);
let ld2 = fiber.gvd_length_m(2.0);
assert_relative_eq!(ld2 / ld1, 4.0, max_relative = 1.0e-9);
}
#[test]
fn test_dsf_near_zero_dispersion() {
let fiber = FiberDispersion::dsf();
let d = fiber.dispersion_ps_per_nm_km().abs();
assert!(
d < 1.0,
"DSF dispersion should be near zero, got {d:.3} ps/(nm·km)"
);
}
#[test]
fn test_hnlf_small_anomalous() {
let fiber = FiberDispersion::hnlf();
assert!(
fiber.beta2_ps2_per_km < 0.0,
"HNLF should be anomalous (β₂ < 0)"
);
assert!(
fiber.beta2_ps2_per_km.abs() < 5.0,
"HNLF |β₂| should be small (< 5 ps²/km)"
);
}
}