const C_M_PER_S: f64 = 2.997_924_58e8;
#[derive(Debug, Clone, PartialEq)]
pub enum ItuGrid {
Fixed100Ghz,
Fixed50Ghz,
Fixed25Ghz,
Flex {
min_spacing_ghz: f64,
},
}
impl ItuGrid {
pub fn spacing_ghz(&self) -> f64 {
match self {
ItuGrid::Fixed100Ghz => 100.0,
ItuGrid::Fixed50Ghz => 50.0,
ItuGrid::Fixed25Ghz => 25.0,
ItuGrid::Flex { min_spacing_ghz } => *min_spacing_ghz,
}
}
pub fn is_flex(&self) -> bool {
matches!(self, ItuGrid::Flex { .. })
}
}
#[derive(Debug, Clone)]
pub struct ItuChannelPlan {
pub grid_type: ItuGrid,
pub center_frequency_thz: f64,
pub n_channels: usize,
}
impl ItuChannelPlan {
pub fn new_c_band_100ghz() -> Self {
Self {
grid_type: ItuGrid::Fixed100Ghz,
center_frequency_thz: 191.3,
n_channels: 32,
}
}
pub fn new_c_band_50ghz() -> Self {
Self {
grid_type: ItuGrid::Fixed50Ghz,
center_frequency_thz: 191.35,
n_channels: 64,
}
}
pub fn new_c_plus_l_100ghz() -> Self {
Self {
grid_type: ItuGrid::Fixed100Ghz,
center_frequency_thz: 184.5,
n_channels: 80,
}
}
pub fn new_flex(start_freq_thz: f64, min_spacing_ghz: f64, n_channels: usize) -> Self {
Self {
grid_type: ItuGrid::Flex { min_spacing_ghz },
center_frequency_thz: start_freq_thz,
n_channels,
}
}
pub fn spacing_ghz(&self) -> f64 {
self.grid_type.spacing_ghz()
}
pub fn channel_frequency_thz(&self, channel: usize) -> f64 {
let df_thz = self.spacing_ghz() * 1e-3; self.center_frequency_thz + channel as f64 * df_thz
}
pub fn channel_wavelength_nm(&self, channel: usize) -> f64 {
let f_hz = self.channel_frequency_thz(channel) * 1e12;
C_M_PER_S / f_hz * 1e9
}
pub fn is_on_grid(&self, freq_thz: f64) -> bool {
let df_thz = self.spacing_ghz() * 1e-3;
let offset = freq_thz - self.center_frequency_thz;
let remainder = offset.rem_euclid(df_thz);
let tol_thz = 1e-6; remainder < tol_thz || (df_thz - remainder) < tol_thz
}
pub fn nearest_channel(&self, freq_thz: f64) -> usize {
if self.n_channels == 0 {
return 0;
}
let df_thz = self.spacing_ghz() * 1e-3;
let raw_idx = ((freq_thz - self.center_frequency_thz) / df_thz).round();
raw_idx.max(0.0).min((self.n_channels - 1) as f64) as usize
}
pub fn total_capacity_tbps(&self, bits_per_symbol: u32, baud_gbaud: f64) -> f64 {
let per_channel_gbps = bits_per_symbol as f64 * baud_gbaud;
self.n_channels as f64 * per_channel_gbps / 1e3
}
pub fn all_frequencies_thz(&self) -> Vec<f64> {
(0..self.n_channels)
.map(|k| self.channel_frequency_thz(k))
.collect()
}
pub fn all_wavelengths_nm(&self) -> Vec<f64> {
(0..self.n_channels)
.map(|k| self.channel_wavelength_nm(k))
.collect()
}
pub fn occupied_bandwidth_thz(&self) -> f64 {
if self.n_channels == 0 {
return 0.0;
}
self.spacing_ghz() * 1e-3 * self.n_channels as f64
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum WdmModFormat {
OokNrz,
DpQpsk,
Dp16Qam,
Dp64Qam,
Dp256Qam,
Custom {
bits_per_symbol: u32,
required_osnr_db: f64,
},
}
impl WdmModFormat {
pub fn bits_per_symbol(&self) -> u32 {
match self {
WdmModFormat::OokNrz => 1,
WdmModFormat::DpQpsk => 4,
WdmModFormat::Dp16Qam => 8,
WdmModFormat::Dp64Qam => 12,
WdmModFormat::Dp256Qam => 16,
WdmModFormat::Custom {
bits_per_symbol, ..
} => *bits_per_symbol,
}
}
pub fn required_osnr_db(&self) -> f64 {
match self {
WdmModFormat::OokNrz => 14.0,
WdmModFormat::DpQpsk => 10.5,
WdmModFormat::Dp16Qam => 16.0,
WdmModFormat::Dp64Qam => 22.0,
WdmModFormat::Dp256Qam => 28.0,
WdmModFormat::Custom {
required_osnr_db, ..
} => *required_osnr_db,
}
}
pub fn spectral_efficiency_bps_per_hz(&self) -> f64 {
self.bits_per_symbol() as f64
}
pub fn modulation_gain_db(&self) -> f64 {
self.required_osnr_db() - WdmModFormat::OokNrz.required_osnr_db()
}
}
#[derive(Debug, Clone)]
pub struct WdmLineSystem {
pub channel_plan: ItuChannelPlan,
pub launch_power_dbm_per_channel: f64,
pub modulation_format: WdmModFormat,
pub baud_rate_gbaud: f64,
pub fec_overhead: f64,
}
impl WdmLineSystem {
pub fn new(plan: ItuChannelPlan, launch_dbm: f64, format: WdmModFormat, baud: f64) -> Self {
Self {
channel_plan: plan,
launch_power_dbm_per_channel: launch_dbm,
modulation_format: format,
baud_rate_gbaud: baud,
fec_overhead: 0.20,
}
}
pub fn with_fec_overhead(mut self, overhead: f64) -> Self {
self.fec_overhead = overhead;
self
}
pub fn gross_bit_rate_gbps(&self) -> f64 {
self.modulation_format.bits_per_symbol() as f64 * self.baud_rate_gbaud
}
pub fn net_bit_rate_gbps(&self) -> f64 {
self.gross_bit_rate_gbps() / (1.0 + self.fec_overhead)
}
pub fn total_capacity_tbps(&self) -> f64 {
self.net_bit_rate_gbps() * self.channel_plan.n_channels as f64 / 1e3
}
pub fn channel_bandwidth_ghz(&self) -> f64 {
1.1 * self.baud_rate_gbaud
}
pub fn spectral_efficiency(&self) -> f64 {
let channel_spacing_hz = self.channel_plan.spacing_ghz() * 1e9;
self.net_bit_rate_gbps() * 1e9 / channel_spacing_hz
}
pub fn osnr_margin_db(&self, actual_osnr_db: f64) -> f64 {
actual_osnr_db - self.modulation_format.required_osnr_db()
}
pub fn is_viable(&self, actual_osnr_db: f64) -> bool {
self.osnr_margin_db(actual_osnr_db) >= 0.0
}
pub fn n_channels(&self) -> usize {
self.channel_plan.n_channels
}
pub fn launch_power_mw(&self) -> f64 {
10.0_f64.powf(self.launch_power_dbm_per_channel / 10.0)
}
pub fn total_launch_power_dbm(&self) -> f64 {
self.launch_power_dbm_per_channel + 10.0 * (self.channel_plan.n_channels as f64).log10()
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn itu_c_band_100ghz_channel_count() {
let plan = ItuChannelPlan::new_c_band_100ghz();
assert_eq!(plan.n_channels, 32);
}
#[test]
fn itu_channel_frequency_spacing() {
let plan = ItuChannelPlan::new_c_band_100ghz();
let f0 = plan.channel_frequency_thz(0);
let f1 = plan.channel_frequency_thz(1);
assert_abs_diff_eq!(f1 - f0, 0.1, epsilon = 1e-9); }
#[test]
fn itu_channel_wavelength_in_c_band() {
let plan = ItuChannelPlan::new_c_band_100ghz();
for k in 0..plan.n_channels {
let wl = plan.channel_wavelength_nm(k);
assert!(wl > 1520.0 && wl < 1580.0, "ch{k}: λ={wl:.2} nm");
}
}
#[test]
fn itu_nearest_channel_roundtrip() {
let plan = ItuChannelPlan::new_c_band_50ghz();
for k in [0, 10, 30, 63] {
let f = plan.channel_frequency_thz(k);
assert_eq!(plan.nearest_channel(f), k);
}
}
#[test]
fn itu_is_on_grid() {
let plan = ItuChannelPlan::new_c_band_100ghz();
let f_on = plan.channel_frequency_thz(5);
assert!(plan.is_on_grid(f_on));
assert!(!plan.is_on_grid(f_on + 37e-6));
}
#[test]
fn wdm_mod_format_bits_per_symbol() {
assert_eq!(WdmModFormat::DpQpsk.bits_per_symbol(), 4);
assert_eq!(WdmModFormat::Dp16Qam.bits_per_symbol(), 8);
assert_eq!(WdmModFormat::Dp64Qam.bits_per_symbol(), 12);
assert_eq!(WdmModFormat::Dp256Qam.bits_per_symbol(), 16);
}
#[test]
fn wdm_mod_format_required_osnr_increases_with_order() {
assert!(WdmModFormat::DpQpsk.required_osnr_db() < WdmModFormat::Dp16Qam.required_osnr_db());
assert!(
WdmModFormat::Dp16Qam.required_osnr_db() < WdmModFormat::Dp64Qam.required_osnr_db()
);
}
#[test]
fn wdm_line_system_net_bit_rate() {
let plan = ItuChannelPlan::new_c_band_100ghz();
let sys = WdmLineSystem::new(plan, 0.0, WdmModFormat::DpQpsk, 32.0);
let expected = 128.0 / 1.20;
assert_abs_diff_eq!(sys.net_bit_rate_gbps(), expected, epsilon = 0.01);
}
#[test]
fn wdm_line_system_capacity_scales() {
let plan = ItuChannelPlan::new_c_band_100ghz();
let sys = WdmLineSystem::new(plan, 0.0, WdmModFormat::Dp16Qam, 32.0);
let capacity = sys.total_capacity_tbps();
assert!(capacity > 0.0);
assert!(capacity > 5.0 && capacity < 10.0);
}
#[test]
fn wdm_line_system_osnr_margin() {
let plan = ItuChannelPlan::new_c_band_100ghz();
let sys = WdmLineSystem::new(plan, 0.0, WdmModFormat::DpQpsk, 32.0);
assert_abs_diff_eq!(sys.osnr_margin_db(15.0), 4.5, epsilon = 1e-9);
assert!(sys.is_viable(15.0));
assert!(!sys.is_viable(9.0));
}
#[test]
fn wdm_line_system_spectral_efficiency_positive() {
let plan = ItuChannelPlan::new_c_band_50ghz();
let sys = WdmLineSystem::new(plan, 0.0, WdmModFormat::Dp16Qam, 32.0);
let se = sys.spectral_efficiency();
assert!(se > 0.0 && se < 20.0, "SE = {se}");
}
#[test]
fn itu_total_capacity_formula() {
let plan = ItuChannelPlan::new_c_band_100ghz();
let cap = plan.total_capacity_tbps(4, 32.0);
assert_abs_diff_eq!(cap, 32.0 * 4.0 * 32.0 / 1000.0, epsilon = 1e-9);
}
}