use std::f64::consts::PI;
const H_PLANCK: f64 = 6.626_070_15e-34;
const C_LIGHT: f64 = 2.997_924_58e8;
const _K_B: f64 = 1.380_649e-23;
const TAU_SP: f64 = 10e-3;
const PEAK_EMISSION_WL: f64 = 1530e-9;
const EMISSION_BW: f64 = 20e-9;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PumpDirection {
Copropagating,
Counterpropagating,
Bidirectional { forward_fraction: f64 },
}
#[derive(Debug, Clone)]
pub struct Edfa {
pub fiber_length_m: f64,
pub er_doping_concentration: f64,
pub mode_field_diameter_um: f64,
pub pump_wavelength: f64,
pub pump_power_mw: f64,
pub signal_wavelength: f64,
pub pump_direction: PumpDirection,
}
impl Edfa {
pub fn new_c_band_980nm(pump_power_mw: f64, length_m: f64) -> Self {
Self {
fiber_length_m: length_m,
er_doping_concentration: 3.0e24,
mode_field_diameter_um: 3.0,
pump_wavelength: 980e-9,
pump_power_mw,
signal_wavelength: 1550e-9,
pump_direction: PumpDirection::Copropagating,
}
}
pub fn new_c_band_1480nm(pump_power_mw: f64, length_m: f64) -> Self {
Self {
fiber_length_m: length_m,
er_doping_concentration: 2.0e24,
mode_field_diameter_um: 4.0,
pump_wavelength: 1480e-9,
pump_power_mw,
signal_wavelength: 1550e-9,
pump_direction: PumpDirection::Counterpropagating,
}
}
pub fn effective_area_m2(&self) -> f64 {
let r = self.mode_field_diameter_um * 1e-6 / 2.0;
PI * r * r
}
pub fn emission_cross_section(&self, wavelength: f64) -> f64 {
let sigma_e0 = 5.0e-25_f64; let delta = wavelength - PEAK_EMISSION_WL;
sigma_e0 * (-delta * delta / (2.0 * EMISSION_BW * EMISSION_BW)).exp()
}
pub fn absorption_cross_section(&self, wavelength: f64) -> f64 {
if (wavelength - 980e-9).abs() < 10e-9 {
2.5e-25
} else if (wavelength - 1480e-9).abs() < 15e-9 {
3.5e-25
} else {
let sigma_e = self.emission_cross_section(wavelength);
0.7 * sigma_e
}
}
fn pump_absorption_cross_section(&self) -> f64 {
self.absorption_cross_section(self.pump_wavelength)
}
fn pump_rate(&self) -> f64 {
let a_eff = self.effective_area_m2();
let nu_pump = C_LIGHT / self.pump_wavelength;
let phi_pump = (self.pump_power_mw * 1e-3) / (a_eff * H_PLANCK * nu_pump);
self.pump_absorption_cross_section() * phi_pump
}
pub fn inversion_parameter(&self) -> f64 {
let r_p = self.pump_rate();
let a21 = 1.0 / TAU_SP;
let n2 = r_p / (r_p + a21);
n2.max(0.501)
}
fn n2_fraction(&self) -> f64 {
let r_p = self.pump_rate();
let a21 = 1.0 / TAU_SP;
r_p / (r_p + a21)
}
fn confinement_factor(&self) -> f64 {
let mfd = self.mode_field_diameter_um;
(1.0 - (-2.0_f64 * (2.0 / mfd) * (2.0 / mfd)).exp()).clamp(0.3, 0.95)
}
pub fn small_signal_gain_db_per_m(&self) -> f64 {
let n2 = self.n2_fraction();
let n1 = 1.0 - n2;
let sigma_e = self.emission_cross_section(self.signal_wavelength);
let sigma_a = self.absorption_cross_section(self.signal_wavelength);
let gamma = self.confinement_factor();
let g0_per_m = gamma * self.er_doping_concentration * (sigma_e * n2 - sigma_a * n1);
g0_per_m * 10.0 / 2.0_f64.ln() }
pub fn small_signal_gain_db(&self) -> f64 {
self.small_signal_gain_db_per_m() * self.fiber_length_m
}
pub fn saturation_power_mw(&self) -> f64 {
let nu_s = C_LIGHT / self.signal_wavelength;
let a_eff = self.effective_area_m2();
let sigma_e = self.emission_cross_section(self.signal_wavelength);
let sigma_a = self.absorption_cross_section(self.signal_wavelength);
let gamma = self.confinement_factor();
let p_sat_w = H_PLANCK * nu_s * a_eff / ((sigma_e + sigma_a) * TAU_SP * gamma);
p_sat_w * 1e3
}
pub fn gain_db(&self, signal_power_dbm: f64) -> f64 {
let p_in_mw = 10.0_f64.powf(signal_power_dbm / 10.0);
let p_sat = self.saturation_power_mw();
let g_ss_linear = 10.0_f64.powf(self.small_signal_gain_db() / 10.0);
let g_linear = g_ss_linear / (1.0 + p_in_mw / p_sat);
10.0 * g_linear.log10()
}
pub fn output_power_dbm(&self, input_dbm: f64) -> f64 {
input_dbm + self.gain_db(input_dbm)
}
pub fn nsp(&self) -> f64 {
let n2 = self.n2_fraction();
let n1 = 1.0 - n2;
if n2 <= n1 {
return 1e6; }
n2 / (n2 - n1)
}
pub fn noise_figure_db(&self) -> f64 {
let nsp = self.nsp();
10.0 * (2.0 * nsp).log10()
}
pub fn ase_power_dbm(&self, bandwidth_nm: f64) -> f64 {
let nu = C_LIGHT / self.signal_wavelength;
let bw_hz =
C_LIGHT / (self.signal_wavelength * self.signal_wavelength) * (bandwidth_nm * 1e-9);
let g_ss = 10.0_f64.powf(self.small_signal_gain_db() / 10.0);
let nsp = self.nsp();
let p_ase_w = 2.0 * nsp * H_PLANCK * nu * (g_ss - 1.0) * bw_hz;
10.0 * (p_ase_w * 1e3).log10()
}
pub fn output_osnr_db(&self, input_power_dbm: f64) -> f64 {
let p_out_dbm = self.output_power_dbm(input_power_dbm);
let p_ase_dbm = self.ase_power_dbm(0.1); p_out_dbm - p_ase_dbm
}
pub fn gain_spectrum(&self, wavelength_nm: f64) -> f64 {
let lambda = wavelength_nm * 1e-9;
let peak1 = (-((lambda - 1530e-9).powi(2)) / (2.0 * (5e-9_f64).powi(2))).exp();
let peak2 = 0.7 * (-((lambda - 1550e-9).powi(2)) / (2.0 * (8e-9_f64).powi(2))).exp();
(peak1 + peak2).min(1.0)
}
pub fn optimal_length_m(&self) -> f64 {
let sigma_ap = self.pump_absorption_cross_section();
let gamma = self.confinement_factor();
let alpha_p = gamma * self.er_doping_concentration * sigma_ap; if alpha_p > 0.0 {
1.0 / alpha_p
} else {
self.fiber_length_m
}
}
pub fn gain_clamped_output(&self, input_dbm: f64, clamp_gain_db: f64) -> f64 {
input_dbm + clamp_gain_db
}
}
#[derive(Debug, Clone)]
pub struct WdmChannel {
pub wavelength_nm: f64,
pub power_dbm: f64,
}
impl WdmChannel {
pub fn new(wavelength_nm: f64, power_dbm: f64) -> Self {
Self {
wavelength_nm,
power_dbm,
}
}
}
#[derive(Debug, Clone)]
pub struct WdmEdfa {
pub edfa: Edfa,
pub channels: Vec<WdmChannel>,
}
impl WdmEdfa {
pub fn new(edfa: Edfa, channels: Vec<WdmChannel>) -> Self {
Self { edfa, channels }
}
pub fn channel_gains_db(&self) -> Vec<f64> {
let ref_gain = self.edfa.small_signal_gain_db();
let ref_spectrum = self.edfa.gain_spectrum(self.edfa.signal_wavelength / 1e-9);
self.channels
.iter()
.map(|ch| {
let s = self.edfa.gain_spectrum(ch.wavelength_nm);
let scale = if ref_spectrum > 1e-12 {
s / ref_spectrum
} else {
1.0
};
ref_gain * scale
})
.collect()
}
pub fn gain_tilt_db(&self) -> f64 {
if self.channels.len() < 2 {
return 0.0;
}
let gains = self.channel_gains_db();
let max_g = gains.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let min_g = gains.iter().cloned().fold(f64::INFINITY, f64::min);
max_g - min_g
}
pub fn total_output_power_dbm(&self) -> f64 {
let gains = self.channel_gains_db();
let total_mw: f64 = self
.channels
.iter()
.zip(gains.iter())
.map(|(ch, &g)| {
let p_out_dbm = ch.power_dbm + g;
10.0_f64.powf(p_out_dbm / 10.0)
})
.sum();
if total_mw <= 0.0 {
return f64::NEG_INFINITY;
}
10.0 * total_mw.log10()
}
pub fn gain_equalization_needed_db(&self) -> Vec<f64> {
let gains = self.channel_gains_db();
let min_g = gains.iter().cloned().fold(f64::INFINITY, f64::min);
gains.iter().map(|&g| -(g - min_g)).collect()
}
}
#[derive(Debug, Clone)]
pub struct EdfaCascade {
pub amplifiers: Vec<Edfa>,
pub span_losses_db: Vec<f64>,
}
impl EdfaCascade {
pub fn new_uniform(n_spans: usize, span_loss_db: f64, edfa: Edfa) -> Self {
let amplifiers = (0..n_spans).map(|_| edfa.clone()).collect();
let span_losses_db = vec![span_loss_db; n_spans];
Self {
amplifiers,
span_losses_db,
}
}
pub fn accumulated_ase_dbm(&self, bw_nm: f64) -> f64 {
let n = self.amplifiers.len() as f64;
if n == 0.0 {
return f64::NEG_INFINITY;
}
let p_ase_single = self.amplifiers[0].ase_power_dbm(bw_nm);
let p_ase_total_mw = n * 10.0_f64.powf(p_ase_single / 10.0);
10.0 * p_ase_total_mw.log10()
}
pub fn total_osnr_db(&self, launch_power_dbm: f64) -> f64 {
let n = self.amplifiers.len();
if n == 0 {
return f64::INFINITY;
}
let sum_inv_osnr: f64 = self
.amplifiers
.iter()
.map(|amp| {
let osnr_db = amp.output_osnr_db(launch_power_dbm);
10.0_f64.powf(-osnr_db / 10.0)
})
.sum();
if sum_inv_osnr <= 0.0 {
return f64::INFINITY;
}
-10.0 * sum_inv_osnr.log10()
}
pub fn max_spans(&self, target_osnr_db: f64, launch_power_dbm: f64) -> usize {
if self.amplifiers.is_empty() {
return 0;
}
let osnr_single_db = self.amplifiers[0].output_osnr_db(launch_power_dbm);
let ratio = 10.0_f64.powf((osnr_single_db - target_osnr_db) / 10.0);
ratio.floor().max(0.0) as usize
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_edfa_980nm_positive_gain() {
let edfa = Edfa::new_c_band_980nm(100.0, 10.0);
let gain = edfa.small_signal_gain_db();
assert!(gain > 0.0, "EDFA should have positive gain: got {gain}");
}
#[test]
fn test_edfa_noise_figure_above_3db() {
let edfa = Edfa::new_c_band_980nm(100.0, 10.0);
let nf = edfa.noise_figure_db();
assert!(nf >= 3.0, "NF must be ≥ 3 dB; got {nf}");
}
#[test]
fn test_edfa_saturation_power_positive() {
let edfa = Edfa::new_c_band_1480nm(200.0, 20.0);
let p_sat = edfa.saturation_power_mw();
assert!(
p_sat > 0.0,
"Saturation power must be positive; got {p_sat}"
);
}
#[test]
fn test_output_power_greater_than_input() {
let edfa = Edfa::new_c_band_980nm(150.0, 15.0);
let p_out = edfa.output_power_dbm(-20.0);
assert!(p_out > -20.0, "Output power must exceed input; got {p_out}");
}
#[test]
fn test_gain_compression_under_saturation() {
let edfa = Edfa::new_c_band_980nm(50.0, 10.0);
let g_small = edfa.gain_db(-30.0);
let g_large = edfa.gain_db(0.0);
assert!(
g_small >= g_large,
"Gain must decrease with input power; g(-30 dBm)={g_small}, g(0 dBm)={g_large}"
);
}
#[test]
fn test_ase_power_increases_with_bandwidth() {
let edfa = Edfa::new_c_band_980nm(100.0, 10.0);
let ase_01 = edfa.ase_power_dbm(0.1);
let ase_1 = edfa.ase_power_dbm(1.0);
assert!(ase_1 > ase_01, "ASE must increase with bandwidth");
}
#[test]
fn test_gain_spectrum_normalised() {
let edfa = Edfa::new_c_band_980nm(100.0, 10.0);
for wl_nm in [1530.0, 1540.0, 1550.0, 1565.0] {
let s = edfa.gain_spectrum(wl_nm);
assert!(
(0.0..=1.0).contains(&s),
"Spectrum must be in [0,1] at {wl_nm} nm; got {s}"
);
}
}
#[test]
fn test_wdm_edfa_gain_tilt() {
let edfa = Edfa::new_c_band_980nm(100.0, 10.0);
let channels = vec![
WdmChannel::new(1530.0, -20.0),
WdmChannel::new(1550.0, -20.0),
WdmChannel::new(1565.0, -20.0),
];
let wdm = WdmEdfa::new(edfa, channels);
let tilt = wdm.gain_tilt_db();
assert!(tilt >= 0.0, "Gain tilt must be non-negative; got {tilt}");
}
#[test]
fn test_cascade_ase_accumulation() {
let edfa = Edfa::new_c_band_980nm(100.0, 10.0);
let cascade_1 = EdfaCascade::new_uniform(1, 10.0, edfa.clone());
let cascade_4 = EdfaCascade::new_uniform(4, 10.0, edfa);
let ase_1 = cascade_1.accumulated_ase_dbm(0.1);
let ase_4 = cascade_4.accumulated_ase_dbm(0.1);
assert!(
ase_4 > ase_1,
"Accumulated ASE must increase with number of spans; 1-span={ase_1}, 4-span={ase_4}"
);
}
#[test]
fn test_effective_area() {
let edfa = Edfa::new_c_band_980nm(100.0, 10.0);
let area = edfa.effective_area_m2();
assert_abs_diff_eq!(area, PI * (1.5e-6_f64).powi(2), epsilon = 1e-15);
}
}