#[derive(Debug, Clone)]
pub struct LinkBudget {
pub tx_power_dbm: f64,
pub fiber_loss_db_per_km: f64,
pub link_length_km: f64,
pub connector_loss_db: f64,
pub rx_sensitivity_dbm: f64,
}
impl LinkBudget {
pub fn new(
tx_dbm: f64,
loss_per_km: f64,
length_km: f64,
connector_db: f64,
rx_sens_dbm: f64,
) -> Self {
Self {
tx_power_dbm: tx_dbm,
fiber_loss_db_per_km: loss_per_km,
link_length_km: length_km,
connector_loss_db: connector_db,
rx_sensitivity_dbm: rx_sens_dbm,
}
}
pub fn fiber_loss_total_db(&self) -> f64 {
self.fiber_loss_db_per_km * self.link_length_km
}
pub fn received_power_dbm(&self) -> f64 {
self.tx_power_dbm - self.fiber_loss_total_db() - self.connector_loss_db
}
pub fn power_margin_db(&self) -> f64 {
self.received_power_dbm() - self.rx_sensitivity_dbm
}
pub fn max_length_km(&self) -> f64 {
if self.fiber_loss_db_per_km <= 0.0 {
return f64::INFINITY;
}
let available_loss = self.tx_power_dbm - self.rx_sensitivity_dbm - self.connector_loss_db;
(available_loss / self.fiber_loss_db_per_km).max(0.0)
}
pub fn is_feasible(&self) -> bool {
self.power_margin_db() > 0.0
}
}
#[derive(Debug, Clone)]
pub struct FiberLink {
pub length_km: f64,
pub loss_db_per_km: f64,
pub dispersion_ps_per_nm_km: f64,
pub effective_area_um2: f64,
pub nonlinear_index_n2_m2_per_w: f64,
}
impl FiberLink {
pub fn smf28() -> Self {
Self {
length_km: 80.0,
loss_db_per_km: 0.2,
dispersion_ps_per_nm_km: 17.0,
effective_area_um2: 80.0,
nonlinear_index_n2_m2_per_w: 2.6e-20,
}
}
pub fn nonlinear_coefficient_per_w_km(&self, lambda_nm: f64) -> f64 {
let lambda_m = lambda_nm * 1e-9;
let a_eff_m2 = self.effective_area_um2 * 1e-12;
2.0 * std::f64::consts::PI * self.nonlinear_index_n2_m2_per_w / (lambda_m * a_eff_m2)
* 1e-3
}
pub fn effective_length_km(&self) -> f64 {
let alpha_per_km = self.loss_db_per_km / (10.0 * std::f64::consts::LOG10_E);
let al = alpha_per_km * self.length_km;
(1.0 - (-al).exp()) / alpha_per_km.max(1e-15)
}
}
#[derive(Debug, Clone)]
pub struct AmplifierSpec {
pub gain_db: f64,
pub noise_figure_db: f64,
pub n_spans: usize,
pub span_length_km: f64,
}
impl AmplifierSpec {
pub fn edfa_standard() -> Self {
Self {
gain_db: 20.0,
noise_figure_db: 5.0,
n_spans: 10,
span_length_km: 80.0,
}
}
}
#[derive(Debug, Clone)]
pub struct DwdmLinkBudget {
pub n_channels: usize,
pub channel_spacing_ghz: f64,
pub per_channel_power_dbm: f64,
pub fiber: FiberLink,
pub amplifiers: AmplifierSpec,
}
impl DwdmLinkBudget {
pub fn new(
n_channels: usize,
channel_spacing_ghz: f64,
per_channel_power_dbm: f64,
fiber: FiberLink,
amplifiers: AmplifierSpec,
) -> Self {
Self {
n_channels,
channel_spacing_ghz,
per_channel_power_dbm,
fiber,
amplifiers,
}
}
pub fn total_launch_power_dbm(&self) -> f64 {
if self.n_channels == 0 {
return f64::NEG_INFINITY;
}
self.per_channel_power_dbm + 10.0 * (self.n_channels as f64).log10()
}
pub fn nonlinear_threshold_dbm(&self) -> f64 {
let lambda_nm = 1550.0_f64;
let gamma = self.fiber.nonlinear_coefficient_per_w_km(lambda_nm); let l_eff = self.fiber.effective_length_km(); let alpha_per_km = self.fiber.loss_db_per_km / (10.0 * std::f64::consts::LOG10_E);
let a_eff_km2 = self.fiber.effective_area_um2 * 1e-18; let n = self.amplifiers.n_spans as f64;
let num = alpha_per_km * a_eff_km2;
let den = gamma * n * l_eff;
if den <= 0.0 {
return f64::INFINITY;
}
let p_nlt_w_km = (num / den).sqrt(); let p_nlt_w = p_nlt_w_km / l_eff.max(1e-10);
10.0 * (p_nlt_w * 1e3).max(1e-40).log10() }
pub fn is_linear_regime(&self) -> bool {
self.per_channel_power_dbm < self.nonlinear_threshold_dbm()
}
pub fn required_spans(&self, target_osnr_db: f64, lambda_nm: f64) -> usize {
for n in 1..=10_000_usize {
let chain = crate::comms::modulation::AmplifierChain::new(
n,
self.amplifiers.gain_db,
self.amplifiers.noise_figure_db,
self.amplifiers.span_length_km * self.fiber.loss_db_per_km,
);
let osnr = chain.output_osnr_db(self.per_channel_power_dbm, lambda_nm, 0.1);
if osnr < target_osnr_db {
return n;
}
}
10_000
}
pub fn system_osnr_db(&self, lambda_nm: f64) -> f64 {
let chain = crate::comms::modulation::AmplifierChain::new(
self.amplifiers.n_spans,
self.amplifiers.gain_db,
self.amplifiers.noise_figure_db,
self.amplifiers.span_length_km * self.fiber.loss_db_per_km,
);
chain.output_osnr_db(self.per_channel_power_dbm, lambda_nm, 0.1)
}
pub fn shannon_capacity_tbps(&self) -> f64 {
let osnr_db = self.system_osnr_db(1550.0);
let osnr_lin = 10.0_f64.powf(osnr_db / 10.0);
let bw_hz = self.channel_spacing_ghz * 1e9;
let capacity_per_channel = bw_hz * (1.0 + osnr_lin).log2();
let total_bps = self.n_channels as f64 * capacity_per_channel;
total_bps / 1e12 }
}
#[derive(Debug, Clone)]
pub struct FecAnalysis {
pub overhead_percent: f64,
pub coding_gain_db: f64,
pub input_ber_threshold: f64,
pub output_ber: f64,
}
impl FecAnalysis {
pub fn g709_hard_fec() -> Self {
Self {
overhead_percent: 7.0,
coding_gain_db: 8.6,
input_ber_threshold: 3.8e-3,
output_ber: 1e-15,
}
}
pub fn soft_decision_fec() -> Self {
Self {
overhead_percent: 20.0,
coding_gain_db: 11.0,
input_ber_threshold: 2.0e-2,
output_ber: 1e-15,
}
}
pub fn new(overhead_pct: f64, coding_gain_db: f64, input_ber: f64, output_ber: f64) -> Self {
Self {
overhead_percent: overhead_pct,
coding_gain_db,
input_ber_threshold: input_ber,
output_ber,
}
}
pub fn effective_data_rate_gbps(&self, line_rate_gbps: f64) -> f64 {
line_rate_gbps / (1.0 + self.overhead_percent / 100.0)
}
pub fn required_pre_fec_osnr_db(&self, base_osnr_db: f64) -> f64 {
base_osnr_db - self.coding_gain_db
}
pub fn is_decodable(&self, pre_fec_ber: f64) -> bool {
pre_fec_ber <= self.input_ber_threshold
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_received_power_decreases_with_distance() {
let short = LinkBudget::new(0.0, 0.2, 50.0, 1.0, -28.0);
let long = LinkBudget::new(0.0, 0.2, 100.0, 1.0, -28.0);
let p_short = short.received_power_dbm();
let p_long = long.received_power_dbm();
assert!(
p_long < p_short,
"longer link → lower received power: {p_long} vs {p_short}"
);
}
#[test]
fn test_power_margin_positive() {
let lb = LinkBudget::new(0.0, 0.2, 10.0, 0.5, -28.0);
let margin = lb.power_margin_db();
assert!(
margin > 0.0,
"power margin should be positive, got {margin}"
);
}
#[test]
fn test_max_length_formula() {
let tx = 0.0_f64;
let alpha = 0.2_f64;
let connector = 1.0_f64;
let sens = -28.0_f64;
let lb = LinkBudget::new(tx, alpha, 0.0, connector, sens);
let l_max = lb.max_length_km();
let expected = (tx - sens - connector) / alpha;
assert!(
(l_max - expected).abs() < 1e-10,
"L_max should be {expected:.2} km, got {l_max:.2}"
);
}
#[test]
fn test_total_launch_power_increases_with_channels() {
let fiber = FiberLink::smf28();
let amps = AmplifierSpec::edfa_standard();
let sys8 = DwdmLinkBudget::new(8, 100.0, 0.0, fiber.clone(), amps.clone());
let sys32 = DwdmLinkBudget::new(32, 100.0, 0.0, fiber, amps);
let p8 = sys8.total_launch_power_dbm();
let p32 = sys32.total_launch_power_dbm();
assert!(
p32 > p8,
"more channels → higher total launch power: {p32} vs {p8}"
);
}
#[test]
fn test_shannon_capacity_positive() {
let fiber = FiberLink::smf28();
let amps = AmplifierSpec::edfa_standard();
let sys = DwdmLinkBudget::new(40, 100.0, 0.0, fiber, amps);
let cap = sys.shannon_capacity_tbps();
assert!(
cap > 0.0 && cap.is_finite(),
"Shannon capacity = {cap} Tbit/s"
);
}
#[test]
fn test_fec_g709_effective_rate() {
let fec = FecAnalysis::g709_hard_fec();
let line_rate = 107.0_f64; let data_rate = fec.effective_data_rate_gbps(line_rate);
let expected = line_rate / 1.07;
assert!(
(data_rate - expected).abs() < 1e-6,
"G.709 effective rate should be {expected:.4}, got {data_rate:.4}"
);
}
#[test]
fn test_fec_is_decodable() {
let fec = FecAnalysis::g709_hard_fec();
assert!(fec.is_decodable(1e-4), "BER=1e-4 should be decodable");
assert!(
!fec.is_decodable(1e-2),
"BER=1e-2 should NOT be decodable by G.709"
);
}
#[test]
fn test_soft_fec_higher_overhead() {
let hard = FecAnalysis::g709_hard_fec();
let soft = FecAnalysis::soft_decision_fec();
assert!(
soft.overhead_percent > hard.overhead_percent,
"soft-decision FEC overhead ({}) > hard ({}):",
soft.overhead_percent,
hard.overhead_percent
);
}
#[test]
fn test_nonlinear_threshold_finite() {
let fiber = FiberLink::smf28();
let amps = AmplifierSpec::edfa_standard();
let sys = DwdmLinkBudget::new(80, 100.0, 0.0, fiber, amps);
let nlt = sys.nonlinear_threshold_dbm();
assert!(
nlt.is_finite() && nlt > -100.0 && nlt < 30.0,
"NLT should be in physical range −100..+30 dBm, got {nlt} dBm"
);
}
#[test]
fn test_fec_reduces_required_osnr() {
let fec = FecAnalysis::soft_decision_fec();
let base_osnr = 15.0_f64; let pre_fec = fec.required_pre_fec_osnr_db(base_osnr);
assert!(
pre_fec < base_osnr,
"pre-FEC OSNR {pre_fec} should be less than base {base_osnr}"
);
}
#[test]
fn test_link_budget_feasibility() {
let feasible = LinkBudget::new(0.0, 0.2, 10.0, 0.5, -28.0);
let infeasible = LinkBudget::new(0.0, 0.2, 200.0, 0.5, -28.0);
assert!(feasible.is_feasible(), "short link should be feasible");
assert!(
!infeasible.is_feasible(),
"200 km link should be infeasible at 0.2 dB/km"
);
}
}