use num_complex::Complex64;
use std::f64::consts::PI;
const C_LIGHT: f64 = 2.997_924_58e8;
const H_PLANCK: f64 = 6.626_070_15e-34;
#[derive(Debug, Clone)]
pub struct RinSpectrum {
pub beta_factor: f64,
pub relaxation_oscillation_ghz: f64,
pub damping_rate_ghz: f64,
pub laser_power_mw: f64,
}
impl RinSpectrum {
pub fn new(beta: f64, ro_ghz: f64, gamma_ghz: f64, power_mw: f64) -> Self {
Self {
beta_factor: beta.clamp(1e-8, 1.0),
relaxation_oscillation_ghz: ro_ghz.max(1e-3),
damping_rate_ghz: gamma_ghz.max(1e-3),
laser_power_mw: power_mw.max(1e-6),
}
}
fn denom_sq(&self, freq_ghz: f64) -> f64 {
let f_ro = self.relaxation_oscillation_ghz;
let gamma = self.damping_rate_ghz;
let d_re = f_ro * f_ro - freq_ghz * freq_ghz;
let d_im = freq_ghz * gamma;
d_re * d_re + d_im * d_im
}
pub fn rin_db_per_hz(&self, freq_ghz: f64) -> f64 {
let nu_hz = C_LIGHT / 1550e-9;
let p_w = self.laser_power_mw * 1e-3;
let f_ro = self.relaxation_oscillation_ghz;
let gamma = self.damping_rate_ghz;
let denom_sq = self.denom_sq(freq_ghz);
let denom_sq = if denom_sq < 1e-30 { 1e-30 } else { denom_sq };
let rin_0_per_ghz = 2.0 * self.beta_factor * (gamma / (f_ro * f_ro));
let rin_shaped = rin_0_per_ghz * f_ro.powi(4) / denom_sq;
let rin_noise = rin_shaped * 1e-9;
let shot_noise = 2.0 * H_PLANCK * nu_hz / p_w;
let rin_total = rin_noise + shot_noise;
if rin_total <= 0.0 {
return -200.0;
}
10.0 * rin_total.log10()
}
pub fn integrated_rin_db(&self, f_low_ghz: f64, f_high_ghz: f64) -> f64 {
let n = 64_usize;
let df = (f_high_ghz - f_low_ghz) / n as f64;
let mut integral_lin = 0.0_f64;
for k in 0..n {
let f = f_low_ghz + (k as f64 + 0.5) * df;
let rin_db = self.rin_db_per_hz(f);
let rin_lin = 10.0_f64.powf(rin_db / 10.0);
integral_lin += rin_lin * df * 1e9; }
if integral_lin <= 0.0 {
return -200.0;
}
10.0 * integral_lin.log10()
}
pub fn peak_rin_db_per_hz(&self) -> f64 {
self.rin_db_per_hz(self.relaxation_oscillation_ghz)
}
pub fn rin_floor_db_per_hz(&self) -> f64 {
self.rin_db_per_hz(self.relaxation_oscillation_ghz * 10.0)
}
}
#[derive(Debug, Clone)]
pub struct LaserFrequencyNoise {
pub linewidth_hz: f64,
pub flicker_noise_hz2: f64,
pub white_noise_hz: f64,
}
impl LaserFrequencyNoise {
fn from_linewidth(linewidth_hz: f64, flicker_hz2: f64) -> Self {
Self {
linewidth_hz,
flicker_noise_hz2: flicker_hz2,
white_noise_hz: linewidth_hz / PI,
}
}
pub fn new_diode(linewidth_mhz: f64) -> Self {
let lw_hz = linewidth_mhz * 1e6;
Self::from_linewidth(lw_hz, lw_hz * 1e6) }
pub fn new_fiber_laser(linewidth_khz: f64) -> Self {
let lw_hz = linewidth_khz * 1e3;
Self::from_linewidth(lw_hz, lw_hz * 100.0)
}
pub fn new_ecl(linewidth_khz: f64) -> Self {
let lw_hz = linewidth_khz * 1e3;
Self::from_linewidth(lw_hz, lw_hz * 10.0) }
pub fn freq_noise_hz2_per_hz(&self, freq_hz: f64) -> f64 {
let f = freq_hz.max(1.0); self.white_noise_hz + self.flicker_noise_hz2 / f
}
pub fn phase_noise_dbc_hz(&self, freq_hz: f64) -> f64 {
let f = freq_hz.max(1.0);
let s_phi = self.freq_noise_hz2_per_hz(f) / (f * f);
if s_phi <= 0.0 {
return -200.0;
}
10.0 * s_phi.log10()
}
pub fn linewidth_from_noise_hz(&self) -> f64 {
PI * self.white_noise_hz
}
pub fn coherence_length_m(&self) -> f64 {
let lw = self.linewidth_from_noise_hz().max(1.0);
C_LIGHT / (PI * lw)
}
pub fn phase_error_rad(&self, symbol_rate_gbaud: f64) -> f64 {
let r_s = symbol_rate_gbaud.max(1e-6) * 1e9;
let lw = self.linewidth_hz.max(1.0);
(2.0 * PI * lw / r_s).sqrt()
}
}
#[derive(Debug, Clone)]
pub struct PartitionNoise {
pub n_modes: usize,
pub mode_partition_parameter: f64,
pub fiber_dispersion_ps_per_nm: f64,
}
impl PartitionNoise {
pub fn new(n_modes: usize, k: f64, disp_ps_per_nm: f64) -> Self {
Self {
n_modes: n_modes.max(1),
mode_partition_parameter: k.clamp(0.0, 1.0),
fiber_dispersion_ps_per_nm: disp_ps_per_nm,
}
}
pub fn power_penalty_db(&self, q_factor: f64, source_spectral_width_nm: f64) -> f64 {
let k = self.mode_partition_parameter;
let d = self.fiber_dispersion_ps_per_nm; let l_km = 1.0_f64;
let sigma_t_ps = d * l_km * source_spectral_width_nm; let sigma_t_s = sigma_t_ps * 1e-12;
let t_b = 1e-9_f64;
let sigma_norm_sq = (q_factor * k * sigma_t_s / t_b).powi(2) / 2.0;
let arg = 1.0 - sigma_norm_sq;
if arg <= 0.0 {
return 20.0;
} -5.0 * arg.log10()
}
pub fn max_length_km(&self, max_penalty_db: f64, q_factor: f64, spectral_width_nm: f64) -> f64 {
let k = self.mode_partition_parameter;
let d = self.fiber_dispersion_ps_per_nm;
let threshold_lin = 10.0_f64.powf(-max_penalty_db / 5.0);
let sigma_norm_sq_max = 1.0 - threshold_lin;
if sigma_norm_sq_max <= 0.0 {
return 0.0;
}
let t_b = 1e-9_f64;
let qk = q_factor * k;
if qk.abs() < 1e-30 {
return f64::INFINITY;
}
let d_l_sigma_s = (sigma_norm_sq_max * 2.0).sqrt() * t_b / qk;
let d_sigma_km_s = d * spectral_width_nm * 1e-12; if d_sigma_km_s.abs() < 1e-30 {
return f64::INFINITY;
}
(d_l_sigma_s / d_sigma_km_s.abs()).abs()
}
}
pub fn laser_noise_transfer(omega_rad_s: f64, omega_ro_rad_s: f64, gamma_rad_s: f64) -> Complex64 {
let omega_r_sq = omega_ro_rad_s * omega_ro_rad_s;
let denom = Complex64::new(
omega_r_sq - omega_rad_s * omega_rad_s,
gamma_rad_s * omega_rad_s,
);
if denom.norm() < 1e-30 {
Complex64::new(1e30, 0.0)
} else {
Complex64::new(omega_r_sq, 0.0) / denom
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_rin_floor_lower_than_peak() {
let rin = RinSpectrum::new(1e-4, 5.0, 1.0, 1.0);
let peak = rin.peak_rin_db_per_hz();
let floor = rin.rin_floor_db_per_hz();
assert!(
floor < peak,
"RIN floor should be lower than peak: floor={} peak={}",
floor,
peak
);
}
#[test]
fn test_rin_integrated_finite() {
let rin = RinSpectrum::new(1e-4, 5.0, 1.0, 1.0);
let rin_int = rin.integrated_rin_db(0.1, 10.0);
assert!(
rin_int.is_finite(),
"Integrated RIN should be finite: {}",
rin_int
);
}
#[test]
fn test_frequency_noise_diode_vs_fiber() {
let diode = LaserFrequencyNoise::new_diode(10.0); let fiber = LaserFrequencyNoise::new_fiber_laser(1.0); let s_diode = diode.freq_noise_hz2_per_hz(1e3);
let s_fiber = fiber.freq_noise_hz2_per_hz(1e3);
assert!(
s_diode > s_fiber,
"Diode should be noisier than fiber laser at 1 kHz"
);
}
#[test]
fn test_coherence_length_narrow_linewidth() {
let ecl = LaserFrequencyNoise::new_ecl(0.1); let lc = ecl.coherence_length_m();
assert!(lc > 1e4, "ECL coherence length should be many km: {} m", lc);
}
#[test]
fn test_partition_noise_penalty_zero_at_short_reach() {
let pn = PartitionNoise::new(5, 0.5, 17.0);
let penalty = pn.power_penalty_db(7.0, 0.1);
assert!(
penalty >= 0.0,
"Power penalty must be non-negative: {}",
penalty
);
}
#[test]
fn test_partition_noise_max_length_decreases_with_dispersion() {
let pn_low = PartitionNoise::new(5, 0.5, 3.5); let pn_high = PartitionNoise::new(5, 0.5, 17.0); let l_low = pn_low.max_length_km(1.0, 7.0, 1.0);
let l_high = pn_high.max_length_km(1.0, 7.0, 1.0);
assert!(l_low > l_high, "Lower dispersion should allow longer reach");
}
#[test]
fn test_phase_error_increases_with_linewidth() {
let narrow = LaserFrequencyNoise::new_ecl(0.1);
let wide = LaserFrequencyNoise::new_diode(1.0);
let err_narrow = narrow.phase_error_rad(28.0);
let err_wide = wide.phase_error_rad(28.0);
assert!(
err_wide > err_narrow,
"Wider linewidth → larger phase error"
);
}
#[test]
fn test_laser_noise_transfer_dc() {
let omega_ro = 2.0 * PI * 5e9; let gamma = 2.0 * PI * 1e9; let h = laser_noise_transfer(1.0, omega_ro, gamma); assert_abs_diff_eq!(h.re, 1.0, epsilon = 0.01);
assert_abs_diff_eq!(h.im, 0.0, epsilon = 0.01);
}
}