const SPEED_OF_LIGHT_M: f64 = 2.998e8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WdmGrid {
Dwdm100Ghz,
Dwdm50Ghz,
Dwdm25Ghz,
Cwdm,
LanWdm,
}
impl WdmGrid {
pub fn spacing_hz(&self) -> f64 {
match self {
WdmGrid::Dwdm100Ghz => 100e9,
WdmGrid::Dwdm50Ghz => 50e9,
WdmGrid::Dwdm25Ghz => 25e9,
WdmGrid::Cwdm => SPEED_OF_LIGHT_M / (1550e-9 * 1550e-9) * 20e-9, WdmGrid::LanWdm => 100e9,
}
}
pub fn spacing_nm(&self) -> f64 {
let df = self.spacing_hz();
1550e-9 * 1550e-9 * df / SPEED_OF_LIGHT_M * 1e9
}
}
#[derive(Debug, Clone, Copy)]
pub struct WdmChannel {
pub index: usize,
pub frequency_hz: f64,
pub bandwidth_hz: f64,
pub launch_power_dbm: f64,
}
impl WdmChannel {
pub fn wavelength_m(&self) -> f64 {
SPEED_OF_LIGHT_M / self.frequency_hz
}
pub fn frequency_offset(&self, other: &WdmChannel) -> f64 {
(self.frequency_hz - other.frequency_hz).abs()
}
}
#[derive(Debug, Clone)]
pub struct WdmSystem {
pub channels: Vec<WdmChannel>,
pub grid: WdmGrid,
pub mux_loss_db: f64,
pub crosstalk_suppression_db: f64,
}
impl WdmSystem {
pub fn dwdm(n_channels: usize, grid: WdmGrid) -> Self {
let f0 = 193.1e12; let df = grid.spacing_hz();
let channels = (0..n_channels)
.map(|i| WdmChannel {
index: i,
frequency_hz: f0 + i as f64 * df,
bandwidth_hz: df * 0.5, launch_power_dbm: 0.0,
})
.collect();
Self {
channels,
grid,
mux_loss_db: 3.0,
crosstalk_suppression_db: 30.0,
}
}
pub fn c_band_8ch() -> Self {
Self::dwdm(8, WdmGrid::Dwdm100Ghz)
}
pub fn lan_wdm_4ch() -> Self {
Self::dwdm(4, WdmGrid::LanWdm)
}
pub fn n_channels(&self) -> usize {
self.channels.len()
}
pub fn aggregate_capacity_gbps(&self, per_channel_gbps: f64) -> f64 {
self.channels.len() as f64 * per_channel_gbps
}
pub fn spectral_efficiency(&self, per_channel_gbps: f64) -> f64 {
let df = self.grid.spacing_hz();
per_channel_gbps * 1e9 / df
}
pub fn occupied_bandwidth_thz(&self) -> f64 {
let df = self.grid.spacing_hz();
self.channels.len() as f64 * df / 1e12
}
pub fn wavelength_range_nm(&self) -> (f64, f64) {
let f_min = self
.channels
.iter()
.map(|c| c.frequency_hz)
.fold(f64::INFINITY, f64::min);
let f_max = self
.channels
.iter()
.map(|c| c.frequency_hz)
.fold(f64::NEG_INFINITY, f64::max);
(
SPEED_OF_LIGHT_M / f_max * 1e9,
SPEED_OF_LIGHT_M / f_min * 1e9,
)
}
pub fn nearest_neighbor_xt(&self) -> f64 {
10.0_f64.powf(-self.crosstalk_suppression_db / 10.0)
}
pub fn xt_osnr_penalty_db(&self) -> f64 {
let n = self.n_channels() as f64;
let xt = self.nearest_neighbor_xt();
let total_xt = (n - 1.0) * xt;
if total_xt >= 1.0 {
return 30.0;
}
-10.0 * (1.0 - total_xt).log10()
}
pub fn frequencies_hz(&self) -> Vec<f64> {
self.channels.iter().map(|c| c.frequency_hz).collect()
}
pub fn mux_isolation_db(&self, delta_f_hz: f64, bw_3db_hz: f64) -> f64 {
let x = delta_f_hz / bw_3db_hz;
let h_sq = (-4.0 * std::f64::consts::LN_2 * x * x).exp();
-10.0 * h_sq.max(1e-30).log10()
}
}
#[derive(Debug, Clone, Copy)]
pub struct RingWdmFilter {
pub fsr_hz: f64,
pub q_factor: f64,
pub insertion_loss_db: f64,
pub extinction_ratio_db: f64,
}
impl RingWdmFilter {
pub fn dwdm_100ghz() -> Self {
Self {
fsr_hz: 1.5e12, q_factor: 1e4,
insertion_loss_db: 0.5,
extinction_ratio_db: 20.0,
}
}
pub fn bandwidth_hz(&self, center_frequency_hz: f64) -> f64 {
center_frequency_hz / self.q_factor
}
pub fn adjacent_suppression_db(&self, delta_f_hz: f64, center_hz: f64) -> f64 {
let bw = self.bandwidth_hz(center_hz);
let x = 2.0 * delta_f_hz / bw;
10.0 * (1.0 + x * x).log10()
}
pub fn max_channels(&self, channel_spacing_hz: f64) -> usize {
(self.fsr_hz / channel_spacing_hz).floor() as usize
}
}
#[derive(Debug, Clone)]
pub struct DwdmChannelPlan {
pub center_wavelength_nm: f64,
pub channel_spacing_ghz: f64,
pub n_channels: usize,
}
impl DwdmChannelPlan {
pub fn new(center_wavelength_nm: f64, channel_spacing_ghz: f64, n_channels: usize) -> Self {
Self {
center_wavelength_nm,
channel_spacing_ghz,
n_channels,
}
}
fn center_freq_thz(&self) -> f64 {
SPEED_OF_LIGHT_M / (self.center_wavelength_nm * 1e-9) / 1e12
}
fn channel_freq_thz(&self, ch_idx: usize) -> f64 {
let df = self.channel_spacing_ghz * 1e-3; let half = (self.n_channels as f64 - 1.0) / 2.0;
self.center_freq_thz() + (ch_idx as f64 - half) * df
}
pub fn channel_wavelengths_nm(&self) -> Vec<f64> {
(0..self.n_channels)
.map(|i| {
let f_thz = self.channel_freq_thz(i);
SPEED_OF_LIGHT_M / (f_thz * 1e12) * 1e9
})
.collect()
}
pub fn channel_frequencies_thz(&self) -> Vec<f64> {
(0..self.n_channels)
.map(|i| self.channel_freq_thz(i))
.collect()
}
pub fn channel_index_for_wavelength(&self, wl_nm: f64) -> Option<usize> {
if self.n_channels == 0 {
return None;
}
let wls = self.channel_wavelengths_nm();
let idx = wls
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| {
(*a - wl_nm)
.abs()
.partial_cmp(&(*b - wl_nm).abs())
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i);
idx
}
}
#[derive(Debug, Clone)]
pub struct CrosstalkMatrix {
n: usize,
data: Vec<f64>,
}
impl CrosstalkMatrix {
pub fn new(n_channels: usize) -> Self {
let n = n_channels;
let mut data = vec![-60.0_f64; n * n];
for i in 0..n {
data[i * n + i] = 0.0;
}
Self { n, data }
}
pub fn set_crosstalk(&mut self, from: usize, to: usize, xtalk_db: f64) {
assert!(from < self.n && to < self.n, "channel index out of range");
self.data[from * self.n + to] = xtalk_db;
}
pub fn total_interference_db(&self, channel: usize) -> f64 {
assert!(channel < self.n, "channel index out of range");
let sum: f64 = (0..self.n)
.filter(|&k| k != channel)
.map(|k| 10.0_f64.powf(self.data[k * self.n + channel] / 10.0))
.sum();
if sum <= 0.0 {
return f64::NEG_INFINITY;
}
10.0 * sum.log10()
}
}
#[derive(Debug, Clone)]
pub struct OpticalAddDropMux {
channel_plan: DwdmChannelPlan,
bus: Vec<Option<f64>>,
insertion_loss_db: f64,
}
impl OpticalAddDropMux {
pub fn new(channel_plan: DwdmChannelPlan) -> Self {
let n = channel_plan.n_channels;
Self {
channel_plan,
bus: vec![None; n],
insertion_loss_db: 1.0,
}
}
pub fn add_channel(&mut self, ch_idx: usize, power_dbm: f64) {
assert!(
ch_idx < self.channel_plan.n_channels,
"channel index out of range"
);
self.bus[ch_idx] = Some(power_dbm);
}
pub fn drop_channel(&mut self, ch_idx: usize) -> Option<f64> {
assert!(
ch_idx < self.channel_plan.n_channels,
"channel index out of range"
);
self.bus[ch_idx].take()
}
pub fn express_channels(&self) -> Vec<(usize, f64)> {
self.bus
.iter()
.enumerate()
.filter_map(|(i, p)| p.map(|pw| (i, pw - self.insertion_loss_db)))
.collect()
}
pub fn insertion_loss_db(&self) -> f64 {
self.insertion_loss_db
}
pub fn channel_plan(&self) -> &DwdmChannelPlan {
&self.channel_plan
}
}
#[derive(Debug, Clone)]
pub struct PowerEqualizer {
pub n_channels: usize,
pub target_power_dbm: f64,
pub channel_gains: Vec<f64>,
}
impl PowerEqualizer {
pub fn new(n_channels: usize, target_power_dbm: f64) -> Self {
Self {
n_channels,
target_power_dbm,
channel_gains: vec![0.0; n_channels],
}
}
pub fn equalize(&mut self, input_powers_dbm: &[f64]) {
for (i, &p_in) in input_powers_dbm.iter().take(self.n_channels).enumerate() {
self.channel_gains[i] = self.target_power_dbm - p_in;
}
}
pub fn max_variation_db(&self) -> f64 {
if self.n_channels < 2 {
return 0.0;
}
let min_g = self
.channel_gains
.iter()
.cloned()
.fold(f64::INFINITY, f64::min);
let max_g = self
.channel_gains
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max);
max_g - min_g
}
pub fn total_gain_db(&self) -> f64 {
self.channel_gains.iter().sum()
}
}
#[derive(Debug, Clone)]
pub struct Roadm {
pub n_degree: usize,
pub n_channels: usize,
pub insertion_loss_db: f64,
pub isolation_db: f64,
pub switching_matrix: Vec<Vec<Option<usize>>>,
}
impl Roadm {
pub fn new(n_degree: usize, n_channels: usize) -> Self {
Self {
n_degree,
n_channels,
insertion_loss_db: 5.0,
isolation_db: 40.0,
switching_matrix: vec![vec![None; n_channels]; n_degree],
}
}
pub fn add_connection(
&mut self,
from_port: usize,
channel: usize,
to_port: usize,
) -> Result<(), crate::error::OxiPhotonError> {
if from_port >= self.n_degree || to_port >= self.n_degree {
return Err(crate::error::OxiPhotonError::InvalidLayer(format!(
"port index out of range: from={from_port}, to={to_port}, degree={}",
self.n_degree
)));
}
if channel >= self.n_channels {
return Err(crate::error::OxiPhotonError::InvalidLayer(format!(
"channel index {channel} out of range (n_channels={})",
self.n_channels
)));
}
self.switching_matrix[from_port][channel] = Some(to_port);
Ok(())
}
pub fn drop_connection(&mut self, from_port: usize, channel: usize) {
if from_port < self.n_degree && channel < self.n_channels {
self.switching_matrix[from_port][channel] = None;
}
}
pub fn route_channel(&self, channel: usize, input_port: usize) -> Option<usize> {
self.switching_matrix
.get(input_port)?
.get(channel)?
.as_ref()
.copied()
}
pub fn through_loss_db(&self) -> f64 {
self.insertion_loss_db
}
pub fn add_drop_loss_db(&self) -> f64 {
self.insertion_loss_db + 2.0
}
pub fn active_connections(&self) -> usize {
self.switching_matrix
.iter()
.flat_map(|row| row.iter())
.filter(|x| x.is_some())
.count()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AmplifierType {
Edfa,
Soa,
Raman,
}
#[derive(Debug, Clone)]
pub struct OpticalAmplifier {
pub gain_db: f64,
pub noise_figure_db: f64,
pub saturation_power_dbm: f64,
pub amplifier_type: AmplifierType,
}
impl OpticalAmplifier {
pub fn edfa_c_band() -> Self {
Self {
gain_db: 20.0,
noise_figure_db: 5.0,
saturation_power_dbm: 17.0,
amplifier_type: AmplifierType::Edfa,
}
}
pub fn soa_o_band() -> Self {
Self {
gain_db: 15.0,
noise_figure_db: 8.0,
saturation_power_dbm: 10.0,
amplifier_type: AmplifierType::Soa,
}
}
pub fn output_power_dbm(&self, input_power_dbm: f64) -> f64 {
let g0 = 10.0_f64.powf(self.gain_db / 10.0);
let p_in_mw = 10.0_f64.powf(input_power_dbm / 10.0);
let p_sat_mw = 10.0_f64.powf(self.saturation_power_dbm / 10.0);
let mut p_out = g0 * p_in_mw;
for _ in 0..50 {
p_out = g0 * p_in_mw / (1.0 + p_out / p_sat_mw);
}
10.0 * p_out.max(1e-30).log10()
}
pub fn output_osnr_db(&self, input_osnr_db: f64, _bw_nm: f64) -> f64 {
input_osnr_db - (self.noise_figure_db - 3.0).max(0.0)
}
pub fn noise_power_dbm(&self, bw_hz: f64) -> f64 {
const H_PLANCK: f64 = 6.626e-34; const NU_C_BAND: f64 = 193.1e12; let g = 10.0_f64.powf(self.gain_db / 10.0);
let nf = 10.0_f64.powf(self.noise_figure_db / 10.0);
let n_sp = nf * g / (2.0 * (g - 1.0).max(1e-10));
let p_ase_w = n_sp * (g - 1.0) * H_PLANCK * NU_C_BAND * bw_hz;
10.0 * (p_ase_w * 1e3).max(1e-30).log10() }
pub fn gain_at_saturation(&self, input_power_dbm: f64) -> f64 {
let p_out_dbm = self.output_power_dbm(input_power_dbm);
p_out_dbm - input_power_dbm
}
pub fn ase_psd(&self, _input_signal_power_dbm: f64) -> f64 {
const H_PLANCK: f64 = 6.626e-34;
const NU_C_BAND: f64 = 193.1e12;
let g = 10.0_f64.powf(self.gain_db / 10.0);
let nf = 10.0_f64.powf(self.noise_figure_db / 10.0);
let n_sp = nf * g / (2.0 * (g - 1.0).max(1e-10));
n_sp * (g - 1.0) * H_PLANCK * NU_C_BAND
}
}
#[derive(Debug, Clone)]
pub struct LinkComponent {
pub name: String,
pub gain_or_loss_db: f64,
}
#[derive(Debug, Clone)]
pub struct LinkBudget {
pub components: Vec<LinkComponent>,
}
impl LinkBudget {
pub fn new() -> Self {
Self {
components: Vec::new(),
}
}
pub fn add_component(&mut self, name: &str, gain_loss_db: f64) {
self.components.push(LinkComponent {
name: name.to_owned(),
gain_or_loss_db: gain_loss_db,
});
}
pub fn add_fiber(&mut self, length_km: f64, loss_db_per_km: f64) {
self.add_component(
&format!("Fiber {length_km:.1} km"),
-(length_km * loss_db_per_km),
);
}
pub fn add_amplifier(&mut self, gain_db: f64) {
self.add_component("Amplifier", gain_db);
}
pub fn total_gain_db(&self) -> f64 {
self.components
.iter()
.map(|c| c.gain_or_loss_db)
.filter(|&g| g > 0.0)
.sum()
}
pub fn total_loss_db(&self) -> f64 {
-self
.components
.iter()
.map(|c| c.gain_or_loss_db)
.filter(|&g| g < 0.0)
.sum::<f64>()
}
pub fn net_db(&self) -> f64 {
self.components.iter().map(|c| c.gain_or_loss_db).sum()
}
pub fn margin_db(&self, receiver_sensitivity_dbm: f64, launch_power_dbm: f64) -> f64 {
launch_power_dbm + self.net_db() - receiver_sensitivity_dbm
}
pub fn max_reach_km(
&self,
fiber_loss_db_per_km: f64,
launch_power_dbm: f64,
sensitivity_dbm: f64,
) -> f64 {
if fiber_loss_db_per_km <= 0.0 {
return f64::INFINITY;
}
let available_loss_db = launch_power_dbm - sensitivity_dbm + self.total_gain_db();
(available_loss_db / fiber_loss_db_per_km).max(0.0)
}
}
impl Default for LinkBudget {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wdm_c_band_8ch_n_channels() {
let wdm = WdmSystem::c_band_8ch();
assert_eq!(wdm.n_channels(), 8);
}
#[test]
fn wdm_capacity_scales_with_rate() {
let wdm = WdmSystem::c_band_8ch();
let c1 = wdm.aggregate_capacity_gbps(100.0);
let c2 = wdm.aggregate_capacity_gbps(200.0);
assert!((c2 - 2.0 * c1).abs() < 1e-6);
}
#[test]
fn wdm_spectral_efficiency_positive() {
let wdm = WdmSystem::c_band_8ch();
assert!(wdm.spectral_efficiency(100.0) > 0.0);
}
#[test]
fn wdm_wavelength_range_in_c_band() {
let wdm = WdmSystem::c_band_8ch();
let (lmin, lmax) = wdm.wavelength_range_nm();
assert!(lmin > 1530.0 && lmax < 1570.0, "λ=[{lmin:.1},{lmax:.1}]nm");
}
#[test]
fn wdm_grid_spacing_100ghz() {
assert!((WdmGrid::Dwdm100Ghz.spacing_hz() - 100e9).abs() < 1e6);
}
#[test]
fn ring_filter_max_channels_positive() {
let f = RingWdmFilter::dwdm_100ghz();
let n = f.max_channels(100e9);
assert!(n > 0);
}
#[test]
fn ring_filter_adjacent_suppression_positive() {
let f = RingWdmFilter::dwdm_100ghz();
let supp = f.adjacent_suppression_db(100e9, 193.1e12);
assert!(supp > 0.0);
}
#[test]
fn wdm_lan_4ch_count() {
let wdm = WdmSystem::lan_wdm_4ch();
assert_eq!(wdm.n_channels(), 4);
}
#[test]
fn wdm_occupied_bw_positive() {
let wdm = WdmSystem::c_band_8ch();
assert!(wdm.occupied_bandwidth_thz() > 0.0);
}
#[test]
fn dwdm_plan_channel_count() {
let plan = DwdmChannelPlan::new(1550.0, 100.0, 8);
assert_eq!(plan.channel_wavelengths_nm().len(), 8);
assert_eq!(plan.channel_frequencies_thz().len(), 8);
}
#[test]
fn dwdm_plan_center_wavelength_in_range() {
let plan = DwdmChannelPlan::new(1550.0, 100.0, 8);
let wls = plan.channel_wavelengths_nm();
let wl_min = wls.iter().cloned().fold(f64::INFINITY, f64::min);
let wl_max = wls.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let center = (wl_min + wl_max) / 2.0;
assert!((center - 1550.0).abs() < 1.0, "center={center:.3} nm");
}
#[test]
fn dwdm_plan_frequencies_increasing() {
let plan = DwdmChannelPlan::new(1550.0, 100.0, 4);
let freqs = plan.channel_frequencies_thz();
for w in freqs.windows(2) {
assert!(w[1] > w[0], "frequencies should increase with index");
}
}
#[test]
fn dwdm_plan_channel_spacing_correct() {
let spacing_ghz = 100.0_f64;
let plan = DwdmChannelPlan::new(1550.0, spacing_ghz, 4);
let freqs = plan.channel_frequencies_thz();
for w in freqs.windows(2) {
let df_ghz = (w[1] - w[0]) * 1e3; assert!((df_ghz - spacing_ghz).abs() < 0.01, "df={df_ghz} GHz");
}
}
#[test]
fn dwdm_plan_channel_index_for_wavelength() {
let plan = DwdmChannelPlan::new(1550.0, 100.0, 8);
let wls = plan.channel_wavelengths_nm();
let idx = plan.channel_index_for_wavelength(wls[3]).unwrap();
assert_eq!(idx, 3);
}
#[test]
fn dwdm_plan_channel_index_none_for_empty() {
let plan = DwdmChannelPlan::new(1550.0, 100.0, 0);
assert!(plan.channel_index_for_wavelength(1550.0).is_none());
}
#[test]
fn crosstalk_matrix_diagonal_zero() {
let m = CrosstalkMatrix::new(4);
for i in 0..4 {
assert!((m.data[i * 4 + i] - 0.0).abs() < 1e-12);
}
}
#[test]
fn crosstalk_matrix_set_and_interference() {
let mut m = CrosstalkMatrix::new(3);
m.set_crosstalk(0, 1, -20.0); m.set_crosstalk(2, 1, -20.0); let ti = m.total_interference_db(1);
let expected = 10.0 * (2.0 * 10.0_f64.powf(-2.0)).log10();
assert!((ti - expected).abs() < 1e-6, "ti={ti}, expected={expected}");
}
#[test]
fn crosstalk_matrix_default_interference_very_low() {
let m = CrosstalkMatrix::new(8);
let ti = m.total_interference_db(0);
assert!(
ti < -50.0,
"default off-diagonal −60 dB → low interference, got {ti}"
);
}
#[test]
fn oadm_add_then_express() {
let plan = DwdmChannelPlan::new(1550.0, 100.0, 4);
let mut oadm = OpticalAddDropMux::new(plan);
oadm.add_channel(0, 0.0);
oadm.add_channel(1, 3.0);
let express = oadm.express_channels();
assert_eq!(express.len(), 2);
let il = oadm.insertion_loss_db();
assert!((il - 1.0).abs() < 1e-12);
for (idx, pwr) in &express {
let original = if *idx == 0 { 0.0 } else { 3.0 };
assert!((pwr - (original - il)).abs() < 1e-10);
}
}
#[test]
fn oadm_drop_removes_from_bus() {
let plan = DwdmChannelPlan::new(1550.0, 100.0, 4);
let mut oadm = OpticalAddDropMux::new(plan);
oadm.add_channel(2, -3.0);
let dropped = oadm.drop_channel(2);
assert!((dropped.unwrap() - (-3.0)).abs() < 1e-12);
assert!(oadm.express_channels().iter().all(|(i, _)| *i != 2));
}
#[test]
fn oadm_drop_absent_returns_none() {
let plan = DwdmChannelPlan::new(1550.0, 100.0, 4);
let mut oadm = OpticalAddDropMux::new(plan);
assert!(oadm.drop_channel(0).is_none());
}
#[test]
fn oadm_express_empty_when_no_channels() {
let plan = DwdmChannelPlan::new(1550.0, 100.0, 4);
let oadm = OpticalAddDropMux::new(plan);
assert!(oadm.express_channels().is_empty());
}
#[test]
fn oadm_insertion_loss_default_one_db() {
let plan = DwdmChannelPlan::new(1550.0, 100.0, 4);
let oadm = OpticalAddDropMux::new(plan);
assert!((oadm.insertion_loss_db() - 1.0).abs() < 1e-12);
}
#[test]
fn power_equalizer_equalizes_to_target() {
let mut eq = PowerEqualizer::new(4, 0.0);
let input_powers = [-3.0_f64, -5.0, -1.0, -8.0];
eq.equalize(&input_powers);
for (i, &p_in) in input_powers.iter().enumerate() {
let p_out = p_in + eq.channel_gains[i];
assert!((p_out - 0.0).abs() < 1e-12, "ch{i}: p_out={p_out}");
}
}
#[test]
fn power_equalizer_max_variation_correct() {
let mut eq = PowerEqualizer::new(3, 0.0);
eq.equalize(&[-10.0, -5.0, -3.0]);
let var = eq.max_variation_db();
assert!((var - 7.0).abs() < 1e-10, "variation={var}");
}
#[test]
fn power_equalizer_total_gain_sum() {
let mut eq = PowerEqualizer::new(3, 0.0);
eq.equalize(&[-1.0, -2.0, -3.0]);
let total = eq.total_gain_db();
assert!((total - 6.0).abs() < 1e-10, "total_gain={total}");
}
#[test]
fn roadm_add_and_route_connection() {
let mut roadm = Roadm::new(4, 8);
roadm.add_connection(0, 3, 2).expect("valid connection");
let out = roadm.route_channel(3, 0);
assert_eq!(out, Some(2));
}
#[test]
fn roadm_drop_connection_removes_route() {
let mut roadm = Roadm::new(4, 8);
roadm.add_connection(1, 5, 3).expect("valid connection");
roadm.drop_connection(1, 5);
assert!(roadm.route_channel(5, 1).is_none());
}
#[test]
fn roadm_out_of_range_port_returns_error() {
let mut roadm = Roadm::new(4, 8);
let result = roadm.add_connection(10, 0, 0); assert!(result.is_err());
}
#[test]
fn roadm_through_loss_positive() {
let roadm = Roadm::new(4, 8);
assert!(roadm.through_loss_db() > 0.0);
}
#[test]
fn roadm_add_drop_loss_greater_than_through() {
let roadm = Roadm::new(4, 8);
assert!(roadm.add_drop_loss_db() > roadm.through_loss_db());
}
#[test]
fn roadm_active_connections_count() {
let mut roadm = Roadm::new(4, 8);
roadm.add_connection(0, 0, 1).expect("ok");
roadm.add_connection(0, 1, 2).expect("ok");
roadm.add_connection(1, 0, 3).expect("ok");
assert_eq!(roadm.active_connections(), 3);
}
#[test]
fn edfa_output_power_increases_with_gain() {
let amp = OpticalAmplifier::edfa_c_band();
let p_in = -20.0_f64; let p_out = amp.output_power_dbm(p_in);
assert!(p_out > p_in, "amplifier should increase power");
}
#[test]
fn edfa_gain_at_saturation_less_than_small_signal() {
let amp = OpticalAmplifier::edfa_c_band();
let p_in_high = 10.0_f64; let gain_sat = amp.gain_at_saturation(p_in_high);
assert!(
gain_sat < amp.gain_db,
"saturated gain={gain_sat} vs small-signal={}",
amp.gain_db
);
}
#[test]
fn soa_noise_figure_higher_than_edfa() {
let edfa = OpticalAmplifier::edfa_c_band();
let soa = OpticalAmplifier::soa_o_band();
assert!(soa.noise_figure_db > edfa.noise_figure_db);
}
#[test]
fn amplifier_ase_psd_positive() {
let amp = OpticalAmplifier::edfa_c_band();
let psd = amp.ase_psd(-30.0);
assert!(psd > 0.0, "ASE PSD should be positive, got {psd}");
}
#[test]
fn amplifier_noise_power_increases_with_bandwidth() {
let amp = OpticalAmplifier::edfa_c_band();
let p1 = amp.noise_power_dbm(10e9); let p2 = amp.noise_power_dbm(100e9); assert!(p2 > p1, "wider bandwidth → more noise: p1={p1}, p2={p2}");
}
#[test]
fn link_budget_net_fiber_only() {
let mut budget = LinkBudget::new();
budget.add_fiber(80.0, 0.2); let net = budget.net_db();
assert!((net + 16.0).abs() < 1e-10, "net={net}");
}
#[test]
fn link_budget_net_with_amplifier() {
let mut budget = LinkBudget::new();
budget.add_fiber(80.0, 0.2); budget.add_amplifier(16.0); let net = budget.net_db();
assert!(net.abs() < 1e-10, "net={net}");
}
#[test]
fn link_budget_margin_positive_when_feasible() {
let mut budget = LinkBudget::new();
budget.add_fiber(10.0, 0.2); let margin = budget.margin_db(-28.0, 0.0); assert!(margin > 0.0, "margin={margin}");
}
#[test]
fn link_budget_max_reach_positive() {
let budget = LinkBudget::new();
let reach = budget.max_reach_km(0.2, 0.0, -28.0);
assert!((reach - 140.0).abs() < 1e-6, "reach={reach}");
}
#[test]
fn link_budget_total_loss_positive() {
let mut budget = LinkBudget::new();
budget.add_fiber(100.0, 0.2);
budget.add_component("Connector", -0.5);
assert!(budget.total_loss_db() > 0.0);
}
#[test]
fn link_budget_total_gain_zero_for_loss_only() {
let mut budget = LinkBudget::new();
budget.add_fiber(50.0, 0.2);
assert!((budget.total_gain_db() - 0.0).abs() < 1e-12);
}
}