use crate::comms::modulation::ModulationFormat;
const H_PLANCK: f64 = 6.626_070_15e-34; const C_LIGHT: f64 = 2.997_924_58e8; const SQRT2: f64 = std::f64::consts::SQRT_2;
#[derive(Debug, Clone)]
pub struct OsnrAnalysis {
pub signal_power_dbm: f64,
pub noise_power_dbm: f64,
pub reference_bandwidth_nm: f64,
pub signal_bandwidth_nm: f64,
}
impl OsnrAnalysis {
pub fn new(signal_dbm: f64, noise_dbm: f64, ref_bw_nm: f64, sig_bw_nm: f64) -> Self {
Self {
signal_power_dbm: signal_dbm,
noise_power_dbm: noise_dbm,
reference_bandwidth_nm: ref_bw_nm,
signal_bandwidth_nm: sig_bw_nm,
}
}
pub fn osnr_db(&self) -> f64 {
self.signal_power_dbm - self.noise_power_dbm
}
pub fn required_osnr_db(format: &ModulationFormat, ber_target: f64) -> f64 {
format.required_osnr_db(ber_target)
}
pub fn dispersion_penalty_db(
accumulated_dispersion_ps_per_nm: f64,
data_rate_gbps: f64,
) -> f64 {
let delta = accumulated_dispersion_ps_per_nm * data_rate_gbps * data_rate_gbps * 1e-3;
10.0 * (1.0 + delta * delta).log10()
}
pub fn ase_per_span_dbm(gain_db: f64, nf_db: f64, lambda_nm: f64, bandwidth_nm: f64) -> f64 {
let g = 10.0_f64.powf(gain_db / 10.0);
let nf = 10.0_f64.powf(nf_db / 10.0);
let lambda_m = lambda_nm * 1e-9;
let nu = C_LIGHT / lambda_m; let bw_hz = C_LIGHT / (lambda_m * lambda_m) * (bandwidth_nm * 1e-9);
let n_sp = nf * g / (2.0 * (g - 1.0).max(1e-12));
let p_ase_w = n_sp * (g - 1.0) * H_PLANCK * nu * bw_hz;
10.0 * (p_ase_w * 1e3).max(1e-40).log10()
}
pub fn accumulated_ase_dbm(n_spans: usize, per_span_ase_dbm: f64) -> f64 {
if n_spans == 0 {
return f64::NEG_INFINITY;
}
per_span_ase_dbm + 10.0 * (n_spans as f64).log10()
}
}
pub struct BerCalculator;
impl BerCalculator {
pub fn ook_direct_ber(q_factor: f64) -> f64 {
0.5 * Self::erfc(q_factor / SQRT2)
}
pub fn bpsk_ber(eb_n0_linear: f64) -> f64 {
0.5 * Self::erfc(eb_n0_linear.max(0.0).sqrt())
}
pub fn qpsk_ber(eb_n0_linear: f64) -> f64 {
Self::bpsk_ber(eb_n0_linear)
}
pub fn qam16_ber(eb_n0_linear: f64) -> f64 {
let arg = (2.0 * eb_n0_linear.max(0.0) / 5.0).sqrt();
(3.0 / 8.0) * Self::erfc(arg)
}
pub fn qam64_ber(eb_n0_linear: f64) -> f64 {
let arg = (eb_n0_linear.max(0.0) / 7.0).sqrt();
(7.0 / 24.0) * Self::erfc(arg)
}
pub fn q_from_ber(ber: f64) -> f64 {
let ber_clamped = ber.clamp(1e-20, 0.5 - 1e-10);
SQRT2 * Self::erfinv(1.0 - 2.0 * ber_clamped)
}
pub fn ber_from_q(q_factor: f64) -> f64 {
0.5 * Self::erfc(q_factor / SQRT2)
}
pub fn required_eb_n0_db(format: &ModulationFormat, ber_target: f64) -> f64 {
let ber_fn: Box<dyn Fn(f64) -> f64> = match format {
ModulationFormat::Ook => Box::new(|eb: f64| {
Self::ook_direct_ber((2.0 * eb).sqrt())
}),
ModulationFormat::Bpsk | ModulationFormat::Dpsk => {
Box::new(|eb: f64| Self::bpsk_ber(eb))
}
ModulationFormat::Qpsk | ModulationFormat::Dqpsk => {
Box::new(|eb: f64| Self::qpsk_ber(eb))
}
ModulationFormat::Qam16 => Box::new(|eb: f64| Self::qam16_ber(eb)),
ModulationFormat::Qam64 => Box::new(|eb: f64| Self::qam64_ber(eb)),
ModulationFormat::Qam256 => Box::new(|eb: f64| {
let arg = (eb.max(0.0) / 85.0).sqrt();
(15.0 / 64.0) * Self::erfc(arg)
}),
ModulationFormat::Pam4 => Box::new(|eb: f64| {
(3.0 / 4.0) * Self::erfc((eb.max(0.0) / 5.0).sqrt())
}),
};
let mut lo: f64 = 1e-2_f64;
let mut hi: f64 = 1e6_f64;
for _ in 0..80 {
let mid = (lo * hi).sqrt(); if ber_fn(mid) > ber_target {
lo = mid;
} else {
hi = mid;
}
}
let eb_n0_linear = (lo * hi).sqrt();
10.0 * eb_n0_linear.log10()
}
pub fn osnr_to_eb_n0(osnr_linear: f64, baud_rate: f64, ref_bandwidth_hz: f64) -> f64 {
if baud_rate <= 0.0 {
return 0.0;
}
osnr_linear * ref_bandwidth_hz / (2.0 * baud_rate)
}
pub fn erfc(x: f64) -> f64 {
if x < 0.0 {
return 2.0 - Self::erfc(-x);
}
if x > 8.0 {
let x2 = x * x;
return (-x2).exp() / (x * std::f64::consts::PI.sqrt())
* (1.0 - 1.0 / (2.0 * x2) + 3.0 / (4.0 * x2 * x2));
}
let t = 1.0 / (1.0 + 0.327_591_1 * 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))));
poly * (-x * x).exp()
}
pub fn erfinv(y: f64) -> f64 {
if y >= 1.0 {
return f64::INFINITY;
}
if y <= -1.0 {
return f64::NEG_INFINITY;
}
if y == 0.0 {
return 0.0;
}
let sign = y.signum();
let ya = y.abs();
const A: f64 = 0.147;
const TWO_OVER_PI_A: f64 = 2.0 / (std::f64::consts::PI * A);
let ln_term = ((1.0 - ya * ya).max(1e-300_f64)).ln() / 2.0;
let inner = TWO_OVER_PI_A + ln_term;
let discriminant = (inner * inner - ln_term / A).max(0.0);
let mut x = sign * (discriminant.sqrt() - inner).max(0.0).sqrt();
for _ in 0..6 {
let erf_x = if x >= 0.0 {
1.0 - Self::erfc(x)
} else {
Self::erfc(-x) - 1.0
};
let f = erf_x - y;
let fp = (2.0 / std::f64::consts::PI.sqrt()) * (-(x * x)).exp();
if fp.abs() < 1e-300 {
break;
}
let fpp = -2.0 * x * fp; let denom = fp - f * fpp / (2.0 * fp);
if denom.abs() < 1e-300 {
x -= f / fp; } else {
x -= f / denom;
}
}
x
}
}
#[derive(Debug, Clone)]
pub struct QFactor {
pub signal_high: f64,
pub signal_low: f64,
pub sigma_high: f64,
pub sigma_low: f64,
}
impl QFactor {
pub fn new(signal_high: f64, signal_low: f64, sigma_high: f64, sigma_low: f64) -> Self {
debug_assert!(
signal_high > signal_low,
"Q-factor: high level must exceed low level"
);
debug_assert!(
sigma_high > 0.0 && sigma_low > 0.0,
"Q-factor: sigmas must be positive"
);
Self {
signal_high,
signal_low,
sigma_high,
sigma_low,
}
}
pub fn q_factor(&self) -> f64 {
let denom = self.sigma_high + self.sigma_low;
if denom < 1e-300 {
return f64::INFINITY;
}
(self.signal_high - self.signal_low) / denom
}
pub fn optimal_threshold(&self) -> f64 {
let denom = self.sigma_high + self.sigma_low;
if denom < 1e-300 {
return (self.signal_high + self.signal_low) / 2.0;
}
(self.signal_low * self.sigma_high + self.signal_high * self.sigma_low) / denom
}
pub fn ber(&self) -> f64 {
BerCalculator::ber_from_q(self.q_factor())
}
pub fn eye_opening(&self) -> f64 {
self.signal_high - self.signal_low
}
pub fn eye_opening_penalty_db(&self, back_to_back_q: f64) -> f64 {
let q = self.q_factor();
if q <= 0.0 || back_to_back_q <= 0.0 {
return f64::INFINITY;
}
20.0 * (back_to_back_q / q).log10()
}
}
#[derive(Debug, Clone)]
pub struct ReceiverSensitivity {
pub responsivity: f64,
pub dark_current_na: f64,
pub thermal_noise_dbm_per_hz: f64,
pub excess_noise_factor: f64,
}
impl ReceiverSensitivity {
pub fn pin_receiver(responsivity: f64) -> Self {
Self {
responsivity,
dark_current_na: 5.0,
thermal_noise_dbm_per_hz: -160.0,
excess_noise_factor: 1.0,
}
}
pub fn apd_receiver(responsivity: f64, gain: f64, excess_noise: f64) -> Self {
Self {
responsivity: responsivity * gain,
dark_current_na: 10.0,
thermal_noise_dbm_per_hz: -160.0,
excess_noise_factor: excess_noise,
}
}
pub fn sensitivity_dbm(&self, ber_target: f64, bandwidth_ghz: f64) -> f64 {
const E_CHARGE: f64 = 1.602_176_634e-19; let q = BerCalculator::q_from_ber(ber_target);
let bw_hz = bandwidth_ghz * 1e9;
let nep_w_per_hz = 10.0_f64.powf(self.thermal_noise_dbm_per_hz / 10.0) * 1e-3;
let i_th_rms = self.responsivity * (nep_w_per_hz * bw_hz).sqrt();
let i_dark = self.dark_current_na * 1e-9;
let sigma_dark = (2.0 * E_CHARGE * i_dark * bw_hz).sqrt();
let sigma0 = (i_th_rms * i_th_rms + sigma_dark * sigma_dark).sqrt();
let mut p_w = 1e-6_f64; for _ in 0..60 {
let i_sig = self.responsivity * p_w;
let sigma1 =
(2.0 * E_CHARGE * self.excess_noise_factor * i_sig.max(0.0) * bw_hz).sqrt();
let required_i = q * (sigma0 + sigma1);
p_w = required_i / self.responsivity;
}
10.0 * (p_w * 1e3).max(1e-40).log10()
}
pub fn shot_noise_limited_dbm(&self, ber_target: f64, bandwidth_ghz: f64) -> f64 {
const E_CHARGE: f64 = 1.602_176_634e-19;
let q = BerCalculator::q_from_ber(ber_target);
let bw_hz = bandwidth_ghz * 1e9;
let p_min_w = q * q * E_CHARGE * bw_hz / self.responsivity.max(1e-10);
10.0 * (p_min_w * 1e3).max(1e-40).log10()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ber_from_q_factor() {
let ber = BerCalculator::ber_from_q(6.0);
assert!(
ber < 1e-8 && ber > 1e-11,
"BER for Q=6 should be ~1e-9, got {ber:.3e}"
);
}
#[test]
fn test_q_from_ber() {
let q = BerCalculator::q_from_ber(1e-9);
assert!(
(q - 6.0).abs() < 0.2,
"Q-factor for BER=1e-9 should be ≈6, got {q:.4}"
);
}
#[test]
fn test_bpsk_ber_at_10db_snr() {
let eb_n0_lin = 10.0_f64.powf(10.0 / 10.0); let ber = BerCalculator::bpsk_ber(eb_n0_lin);
assert!(
ber < 1e-3,
"BPSK BER at 10 dB Eb/N0 = {ber:.3e}, expected < 1e-3"
);
}
#[test]
fn test_erfc_at_zero() {
let val = BerCalculator::erfc(0.0);
assert!((val - 1.0).abs() < 1e-6, "erfc(0) = {val}, expected 1.0");
}
#[test]
fn test_erfc_large_x() {
let val = BerCalculator::erfc(5.0);
assert!(val < 1e-10, "erfc(5) = {val:.3e}, expected < 1e-10");
}
#[test]
fn test_osnr_db() {
let analysis = OsnrAnalysis::new(0.0, -20.0, 0.1, 0.3);
let osnr = analysis.osnr_db();
assert!(
(osnr - 20.0).abs() < 1e-10,
"OSNR should be 20 dB, got {osnr}"
);
}
#[test]
fn test_q_factor_formula() {
let q = QFactor::new(1.0, 0.0, 0.1, 0.1);
let expected = 1.0 / 0.2;
let got = q.q_factor();
assert!(
(got - expected).abs() < 1e-10,
"Q should be {expected}, got {got}"
);
}
#[test]
fn test_optimal_threshold() {
let q = QFactor::new(1.0, 0.0, 0.08, 0.12);
let thresh = q.optimal_threshold();
assert!(
thresh > 0.0 && thresh < 1.0,
"threshold {thresh} should be in (S0, S1) = (0, 1)"
);
}
#[test]
fn test_dispersion_penalty_zero_for_no_dispersion() {
let penalty = OsnrAnalysis::dispersion_penalty_db(0.0, 10.0);
assert!(
penalty.abs() < 1e-10,
"zero dispersion → zero penalty, got {penalty}"
);
}
#[test]
fn test_dispersion_penalty_increases_with_rate() {
let p10 = OsnrAnalysis::dispersion_penalty_db(1000.0, 10.0);
let p40 = OsnrAnalysis::dispersion_penalty_db(1000.0, 40.0);
assert!(
p40 > p10,
"higher data rate → larger dispersion penalty ({p40} vs {p10})"
);
}
#[test]
fn test_accumulated_ase_n_spans() {
let per_span = -10.0_f64; let ase5 = OsnrAnalysis::accumulated_ase_dbm(5, per_span);
let ase50 = OsnrAnalysis::accumulated_ase_dbm(50, per_span);
assert!(
(ase50 - ase5 - 10.0).abs() < 1e-9,
"10× more spans → +10 dB, got diff = {}",
ase50 - ase5
);
}
}