use super::wdm_system::WdmLineSystem;
const H_PLANCK: f64 = 6.626_070_15e-34;
const F_REF_HZ: f64 = 193.1e12;
#[allow(dead_code)]
const K_B: f64 = 1.380_649e-23;
#[derive(Debug, Clone, PartialEq)]
pub enum FiberType {
Smf28 {
effective_area_um2: f64,
n2: f64,
},
TrueWave {
effective_area_um2: f64,
},
UltraLowLoss {
loss_db_per_km: f64,
},
DispersionShifted,
}
impl FiberType {
pub fn nonlinear_coefficient_per_w_per_km(&self) -> f64 {
let lambda_m = 1.55e-6_f64;
match self {
FiberType::Smf28 {
effective_area_um2,
n2,
} => {
let a_eff_m2 = effective_area_um2 * 1e-12; 2.0 * std::f64::consts::PI * n2 / (lambda_m * a_eff_m2) * 1e3
}
FiberType::TrueWave { effective_area_um2 } => {
let a_eff_m2 = effective_area_um2 * 1e-12;
let n2 = 2.6e-20_f64;
2.0 * std::f64::consts::PI * n2 / (lambda_m * a_eff_m2) * 1e3
}
FiberType::UltraLowLoss { .. } => {
let a_eff_m2 = 130e-12_f64;
let n2 = 2.2e-20_f64;
2.0 * std::f64::consts::PI * n2 / (lambda_m * a_eff_m2) * 1e3
}
FiberType::DispersionShifted => {
let a_eff_m2 = 50e-12_f64;
let n2 = 2.6e-20_f64;
2.0 * std::f64::consts::PI * n2 / (lambda_m * a_eff_m2) * 1e3
}
}
}
pub fn effective_area_um2(&self) -> f64 {
match self {
FiberType::Smf28 {
effective_area_um2, ..
} => *effective_area_um2,
FiberType::TrueWave { effective_area_um2 } => *effective_area_um2,
FiberType::UltraLowLoss { .. } => 130.0,
FiberType::DispersionShifted => 50.0,
}
}
}
#[derive(Debug, Clone)]
pub struct OpticalSpan {
pub fiber_length_km: f64,
pub fiber_loss_db_per_km: f64,
pub fiber_dispersion_ps_per_nm_km: f64,
pub fiber_type: FiberType,
pub amplifier_gain_db: f64,
pub amplifier_nf_db: f64,
}
impl OpticalSpan {
pub fn new_smf28(length_km: f64, amp_gain_db: f64, amp_nf_db: f64) -> Self {
Self {
fiber_length_km: length_km,
fiber_loss_db_per_km: 0.2,
fiber_dispersion_ps_per_nm_km: 17.0,
fiber_type: FiberType::Smf28 {
effective_area_um2: 80.0,
n2: 2.6e-20,
},
amplifier_gain_db: amp_gain_db,
amplifier_nf_db: amp_nf_db,
}
}
pub fn new_ull(length_km: f64, amp_gain_db: f64, amp_nf_db: f64) -> Self {
Self {
fiber_length_km: length_km,
fiber_loss_db_per_km: 0.157,
fiber_dispersion_ps_per_nm_km: 20.0,
fiber_type: FiberType::UltraLowLoss {
loss_db_per_km: 0.157,
},
amplifier_gain_db: amp_gain_db,
amplifier_nf_db: amp_nf_db,
}
}
pub fn span_loss_db(&self) -> f64 {
self.fiber_loss_db_per_km * self.fiber_length_km
}
pub fn alpha_per_km(&self) -> f64 {
self.fiber_loss_db_per_km / (10.0 * std::f64::consts::LOG10_E)
}
pub fn effective_length_km(&self) -> f64 {
let alpha = self.alpha_per_km();
if alpha < 1e-12 {
return self.fiber_length_km;
}
(1.0 - (-alpha * self.fiber_length_km).exp()) / alpha
}
pub fn osnr_contribution_db(&self, launch_power_dbm: f64, bandwidth_nm: f64) -> f64 {
let p_launch_w = 1e-3 * 10.0_f64.powf(launch_power_dbm / 10.0);
let nf_linear = 10.0_f64.powf(self.amplifier_nf_db / 10.0);
let g_linear = 10.0_f64.powf(self.amplifier_gain_db / 10.0);
let lambda_m = 1.55e-6_f64;
let bw_hz = 2.998e8 * bandwidth_nm * 1e-9 / (lambda_m * lambda_m);
let p_ase = nf_linear * (g_linear - 1.0) * H_PLANCK * F_REF_HZ * bw_hz;
let osnr = p_launch_w / p_ase.max(1e-30);
10.0 * osnr.log10()
}
pub fn dispersion_ps_per_nm(&self) -> f64 {
self.fiber_dispersion_ps_per_nm_km * self.fiber_length_km
}
pub fn nonlinear_phase_shift_rad(&self, launch_power_dbm: f64) -> f64 {
let p0_w = 1e-3 * 10.0_f64.powf(launch_power_dbm / 10.0);
let gamma = self.fiber_type.nonlinear_coefficient_per_w_per_km();
gamma * p0_w * self.effective_length_km()
}
}
#[derive(Debug, Clone)]
pub struct WdmLink {
pub spans: Vec<OpticalSpan>,
pub system: WdmLineSystem,
}
impl WdmLink {
pub fn new(spans: Vec<OpticalSpan>, system: WdmLineSystem) -> Self {
Self { spans, system }
}
pub fn n_spans(&self) -> usize {
self.spans.len()
}
pub fn total_length_km(&self) -> f64 {
self.spans.iter().map(|s| s.fiber_length_km).sum()
}
pub fn end_of_link_osnr_db(&self) -> f64 {
if self.spans.is_empty() {
return f64::INFINITY;
}
let p_dbm = self.system.launch_power_dbm_per_channel;
let bw_nm = 0.1_f64; let noise_sum: f64 = self
.spans
.iter()
.map(|span| {
let osnr_db = span.osnr_contribution_db(p_dbm, bw_nm);
10.0_f64.powf(-osnr_db / 10.0)
})
.sum();
if noise_sum <= 0.0 {
return f64::INFINITY;
}
-10.0 * noise_sum.log10()
}
pub fn nonlinear_snr_db(&self) -> f64 {
if self.spans.is_empty() {
return f64::INFINITY;
}
let p_w = 1e-3 * 10.0_f64.powf(self.system.launch_power_dbm_per_channel / 10.0);
let n_sp = self.spans.len() as f64;
let gamma_avg: f64 = self
.spans
.iter()
.map(|s| s.fiber_type.nonlinear_coefficient_per_w_per_km())
.sum::<f64>()
/ n_sp;
let l_eff_avg_km: f64 = self
.spans
.iter()
.map(|s| s.effective_length_km())
.sum::<f64>()
/ n_sp;
let d_avg: f64 = self
.spans
.iter()
.map(|s| s.fiber_dispersion_ps_per_nm_km)
.sum::<f64>()
/ n_sp;
let beta2_s2_per_m = d_avg.abs() * 1.28e-26;
let bw_hz = self.system.channel_plan.n_channels as f64
* self.system.channel_plan.spacing_ghz()
* 1e9;
let alpha_per_m = self
.spans
.first()
.map(|s| s.alpha_per_km() / 1000.0)
.unwrap_or(4.6e-5_f64);
let arg =
std::f64::consts::PI * std::f64::consts::PI * beta2_s2_per_m.abs() * bw_hz * bw_hz
/ alpha_per_m.max(1e-30);
let kappa = (8.0 / 27.0) * std::f64::consts::PI * arg.max(1.0).ln();
let snr_nl_inv =
kappa * gamma_avg * gamma_avg * p_w * p_w * l_eff_avg_km * l_eff_avg_km * n_sp;
if snr_nl_inv <= 0.0 {
return f64::INFINITY;
}
-10.0 * snr_nl_inv.log10()
}
pub fn total_snr_db(&self) -> f64 {
let snr_ase = 10.0_f64.powf(self.end_of_link_osnr_db() / 10.0);
let snr_nl = 10.0_f64.powf(self.nonlinear_snr_db() / 10.0);
let inv_total = 1.0 / snr_ase.max(1e-30) + 1.0 / snr_nl.max(1e-30);
-10.0 * inv_total.log10()
}
pub fn optimal_launch_power_dbm(&self) -> f64 {
let mut lo = -10.0_f64;
let mut hi = 10.0_f64;
let gr = (5.0_f64.sqrt() - 1.0) / 2.0; let tol = 1e-4_f64;
let mut test_link = self.clone();
for _ in 0..100 {
let x1 = hi - gr * (hi - lo);
let x2 = lo + gr * (hi - lo);
test_link.system.launch_power_dbm_per_channel = x1;
let snr1 = test_link.total_snr_db();
test_link.system.launch_power_dbm_per_channel = x2;
let snr2 = test_link.total_snr_db();
if snr1 < snr2 {
lo = x1;
} else {
hi = x2;
}
if (hi - lo).abs() < tol {
break;
}
}
(lo + hi) / 2.0
}
pub fn link_margin_db(&self) -> f64 {
let total_snr = self.total_snr_db();
let required = self.system.modulation_format.required_osnr_db();
total_snr - required
}
pub fn max_reach_km(&self, target_osnr_db: f64) -> f64 {
if self.spans.is_empty() {
return 0.0;
}
let current_osnr_db = self.end_of_link_osnr_db();
let osnr_excess = current_osnr_db - target_osnr_db;
if osnr_excess <= 0.0 {
return 0.0;
}
let n_max = 10.0_f64.powf(osnr_excess / 10.0) * self.n_spans() as f64;
let avg_span_km = self.total_length_km() / self.n_spans() as f64;
n_max * avg_span_km
}
pub fn q_factor_db(&self) -> f64 {
self.total_snr_db() - 3.0
}
pub fn ber_estimate(&self) -> f64 {
let q_linear = 10.0_f64.powf(self.q_factor_db() / 20.0);
0.5 * erfc_approx(q_linear / std::f64::consts::SQRT_2)
}
pub fn total_dispersion_ps_per_nm(&self) -> f64 {
self.spans.iter().map(|s| s.dispersion_ps_per_nm()).sum()
}
}
fn erfc_approx(x: f64) -> f64 {
if x < 0.0 {
return 2.0 - erfc_approx(-x);
}
let t = 1.0 / (1.0 + 0.3275911 * x);
let poly = t
* (0.254_829_592
+ t * (-0.284_496_736
+ t * (1.421_413_741 + t * (-1.453_152_027 + t * 1.061_405_429))));
(-x * x).exp() * poly
}
#[cfg(test)]
mod tests {
use super::*;
use crate::optical_network::wdm_system::{ItuChannelPlan, WdmLineSystem, WdmModFormat};
use approx::assert_abs_diff_eq;
fn make_system() -> WdmLineSystem {
WdmLineSystem::new(
ItuChannelPlan::new_c_band_100ghz(),
0.0,
WdmModFormat::DpQpsk,
32.0,
)
}
fn make_link(n_spans: usize) -> WdmLink {
let spans: Vec<OpticalSpan> = (0..n_spans)
.map(|_| OpticalSpan::new_smf28(80.0, 16.0, 5.5))
.collect();
WdmLink::new(spans, make_system())
}
#[test]
fn fiber_type_nonlinear_coeff_smf28() {
let fiber = FiberType::Smf28 {
effective_area_um2: 80.0,
n2: 2.6e-20,
};
let gamma = fiber.nonlinear_coefficient_per_w_per_km();
assert!(gamma > 1.0 && gamma < 2.0, "γ = {gamma:.3}");
}
#[test]
fn optical_span_span_loss() {
let span = OpticalSpan::new_smf28(80.0, 16.0, 5.5);
assert_abs_diff_eq!(span.span_loss_db(), 16.0, epsilon = 1e-9);
}
#[test]
fn optical_span_effective_length() {
let span = OpticalSpan::new_smf28(80.0, 16.0, 5.5);
let l_eff = span.effective_length_km();
assert!(l_eff > 15.0 && l_eff < 25.0, "L_eff = {l_eff:.2} km");
}
#[test]
fn optical_span_nonlinear_phase_shift_positive() {
let span = OpticalSpan::new_smf28(80.0, 16.0, 5.5);
let phi = span.nonlinear_phase_shift_rad(0.0); assert!(phi > 0.0);
}
#[test]
fn optical_span_dispersion() {
let span = OpticalSpan::new_smf28(80.0, 16.0, 5.5);
assert_abs_diff_eq!(span.dispersion_ps_per_nm(), 1360.0, epsilon = 1e-6);
}
#[test]
fn wdm_link_total_length() {
let link = make_link(10);
assert_abs_diff_eq!(link.total_length_km(), 800.0, epsilon = 1e-9);
}
#[test]
fn wdm_link_osnr_decreases_with_more_spans() {
let link5 = make_link(5);
let link20 = make_link(20);
assert!(link5.end_of_link_osnr_db() > link20.end_of_link_osnr_db());
}
#[test]
fn wdm_link_total_snr_positive() {
let link = make_link(10);
let snr = link.total_snr_db();
assert!(snr > 0.0 && snr < 50.0, "SNR = {snr:.2} dB");
}
#[test]
fn wdm_link_optimal_launch_power_in_range() {
let link = make_link(8);
let p_opt = link.optimal_launch_power_dbm();
assert!(p_opt > -10.0 && p_opt < 10.0, "P_opt = {p_opt:.2} dBm");
}
#[test]
fn wdm_link_max_reach_positive() {
let link = make_link(5);
let reach = link.max_reach_km(20.0); assert!(reach > 0.0);
}
#[test]
fn wdm_link_ber_between_zero_and_one() {
let link = make_link(8);
let ber = link.ber_estimate();
assert!((0.0..=1.0).contains(&ber), "BER = {ber:.2e}");
}
#[test]
fn erfc_approx_at_zero_is_one() {
assert_abs_diff_eq!(erfc_approx(0.0), 1.0, epsilon = 1e-6);
}
#[test]
fn erfc_approx_large_x_near_zero() {
assert!(erfc_approx(5.0) < 1e-5);
}
#[test]
fn optical_span_ull_lower_loss() {
let smf = OpticalSpan::new_smf28(80.0, 16.0, 5.0);
let ull = OpticalSpan::new_ull(80.0, 16.0, 5.0);
assert!(ull.fiber_loss_db_per_km < smf.fiber_loss_db_per_km);
}
}