use std::f64::consts::PI;
pub const H_PLANCK: f64 = 6.62607015e-34;
pub const C_LIGHT: f64 = 2.99792458e8;
pub const KB: f64 = 1.380649e-23;
const Q_ELECTRON: f64 = 1.602176634e-19;
#[derive(Clone, Debug)]
pub struct ShotNoise {
pub responsivity_a_per_w: f64,
pub dark_current_a: f64,
pub bandwidth_hz: f64,
}
impl ShotNoise {
pub fn typical_si() -> Self {
Self {
responsivity_a_per_w: 0.8,
dark_current_a: 1e-9,
bandwidth_hz: 10e9,
}
}
pub fn noise_current_rms_a(&self, optical_power_w: f64) -> f64 {
let i_ph = self.responsivity_a_per_w * optical_power_w;
(2.0 * Q_ELECTRON * (i_ph + self.dark_current_a) * self.bandwidth_hz).sqrt()
}
pub fn snr_shot_limited(&self, signal_power_w: f64) -> f64 {
let i_signal = self.responsivity_a_per_w * signal_power_w;
let i_noise = self.noise_current_rms_a(signal_power_w);
if i_noise <= 0.0 {
return f64::INFINITY;
}
(i_signal / i_noise).powi(2)
}
pub fn noise_equivalent_power_w(&self) -> f64 {
let i_dark_noise = (2.0 * Q_ELECTRON * self.dark_current_a * self.bandwidth_hz).sqrt();
if self.responsivity_a_per_w <= 0.0 {
return f64::INFINITY;
}
i_dark_noise / self.responsivity_a_per_w
}
pub fn minimum_detectable_power_w(&self, snr_required: f64) -> f64 {
if self.responsivity_a_per_w <= 0.0 {
return f64::INFINITY;
}
2.0 * Q_ELECTRON * snr_required * self.bandwidth_hz / self.responsivity_a_per_w
}
pub fn snr_db(&self, signal_power_w: f64) -> f64 {
10.0 * self.snr_shot_limited(signal_power_w).log10()
}
pub fn photon_flux(&self, power_w: f64, wavelength_m: f64) -> f64 {
let photon_energy = H_PLANCK * C_LIGHT / wavelength_m;
power_w / photon_energy
}
}
#[derive(Clone, Debug)]
pub struct RinNoise {
pub rin_db_per_hz: f64,
pub bandwidth_hz: f64,
pub power_w: f64,
}
impl RinNoise {
pub fn typical_dfb(power_w: f64, bandwidth_hz: f64) -> Self {
Self {
rin_db_per_hz: -150.0,
bandwidth_hz,
power_w,
}
}
pub fn rin_linear(&self) -> f64 {
10.0_f64.powf(self.rin_db_per_hz / 10.0)
}
pub fn power_noise_rms_w(&self) -> f64 {
self.power_w * (self.rin_linear() * self.bandwidth_hz).sqrt()
}
pub fn snr_rin_limited(&self, signal_power_fraction: f64) -> f64 {
let p_noise = self.power_noise_rms_w();
if p_noise <= 0.0 {
return f64::INFINITY;
}
let p_signal = self.power_w * signal_power_fraction;
(p_signal / p_noise).powi(2)
}
pub fn dynamic_range_db(&self) -> f64 {
10.0 * (1.0 / (self.rin_linear() * self.bandwidth_hz)).log10()
}
}
#[derive(Clone, Debug)]
pub struct PolarizationDependentLoss {
pub pdl_db: f64,
}
impl PolarizationDependentLoss {
pub fn new(pdl_db: f64) -> Self {
Self {
pdl_db: pdl_db.abs(),
}
}
pub fn transmission_ratio(&self) -> f64 {
10.0_f64.powf(self.pdl_db / 10.0)
}
pub fn worst_case_insertion_loss_db(&self) -> f64 {
self.pdl_db / 2.0
}
pub fn snr_penalty_db(&self) -> f64 {
0.5 * self.pdl_db
}
pub fn jones_eigenvalues(&self) -> (f64, f64) {
let t_max = self.transmission_ratio().sqrt();
let t_min = 1.0 / t_max;
(t_max, t_min)
}
pub fn average_insertion_loss_db(&self) -> f64 {
let ratio = self.transmission_ratio();
let t_avg = (ratio + 1.0 / ratio) / 2.0;
if t_avg <= 0.0 {
return f64::INFINITY;
}
-10.0 * t_avg.log10()
}
pub fn normalised_pdl(&self) -> f64 {
let r = self.transmission_ratio();
(r - 1.0) / (r + 1.0)
}
}
#[derive(Clone, Debug)]
pub struct ThermalNoise {
pub temperature_k: f64,
pub resistance_ohm: f64,
pub bandwidth_hz: f64,
}
impl ThermalNoise {
pub fn room_temperature(resistance_ohm: f64, bandwidth_hz: f64) -> Self {
Self {
temperature_k: 300.0,
resistance_ohm,
bandwidth_hz,
}
}
pub fn voltage_noise_rms_v(&self) -> f64 {
(4.0 * KB * self.temperature_k * self.resistance_ohm * self.bandwidth_hz).sqrt()
}
pub fn current_noise_rms_a(&self) -> f64 {
if self.resistance_ohm <= 0.0 {
return 0.0;
}
self.voltage_noise_rms_v() / self.resistance_ohm
}
pub fn voltage_psd_v2_per_hz(&self) -> f64 {
4.0 * KB * self.temperature_k * self.resistance_ohm
}
pub fn available_noise_power_w(&self) -> f64 {
KB * self.temperature_k * self.bandwidth_hz
}
pub fn noise_figure_db(&self, reference_temperature_k: f64) -> f64 {
let t_ratio = self.temperature_k / reference_temperature_k;
10.0 * (1.0 + t_ratio).log10()
}
}
pub struct OsnrModel;
impl OsnrModel {
pub fn osnr_to_electrical_snr(
osnr_db: f64,
bandwidth_optical_hz: f64,
bandwidth_electrical_hz: f64,
) -> f64 {
let osnr_linear = 10.0_f64.powf(osnr_db / 10.0);
osnr_linear * bandwidth_electrical_hz / (2.0 * bandwidth_optical_hz)
}
pub fn required_osnr_bpsk_db(ber_target: f64, bandwidth_ratio: f64) -> f64 {
let q2 = Self::q_squared_from_ber(ber_target);
10.0 * (q2 * bandwidth_ratio).log10()
}
fn q_squared_from_ber(ber: f64) -> f64 {
let ber_clamped = ber.clamp(1e-20, 0.5);
let q = (-2.0 * (2.0 * ber_clamped).ln()).sqrt();
q * q
}
pub fn photons_per_bit(osnr_linear: f64, symbol_rate_hz: f64, noise_bandwidth_hz: f64) -> f64 {
osnr_linear * noise_bandwidth_hz / symbol_rate_hz
}
}
#[derive(Clone, Debug)]
pub struct PhaseNoise {
pub linewidth_hz: f64,
pub integration_time_s: f64,
}
impl PhaseNoise {
pub fn rms_phase_noise_rad(&self) -> f64 {
(2.0 * PI * self.linewidth_hz * self.integration_time_s).sqrt()
}
pub fn phase_variance_rad2(&self) -> f64 {
2.0 * PI * self.linewidth_hz * self.integration_time_s
}
pub fn coherence_time_s(&self) -> f64 {
1.0 / (PI * self.linewidth_hz)
}
pub fn coherence_length_m(&self) -> f64 {
C_LIGHT / (PI * self.linewidth_hz)
}
pub fn dpsk_snr_penalty_db(&self) -> f64 {
let sigma2 = self.phase_variance_rad2();
10.0 * (1.0 + std::f64::consts::PI * std::f64::consts::PI * sigma2 / 3.0).log10()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shot_noise_increases_with_power() {
let sn = ShotNoise::typical_si();
let n1 = sn.noise_current_rms_a(1e-3);
let n2 = sn.noise_current_rms_a(10e-3);
assert!(n2 > n1, "noise should grow with power");
}
#[test]
fn shot_noise_nep_positive() {
let sn = ShotNoise::typical_si();
let nep = sn.noise_equivalent_power_w();
assert!(nep > 0.0 && nep < 1e-6, "NEP={}", nep);
}
#[test]
fn rin_noise_dfb_typical_range() {
let rin = RinNoise::typical_dfb(1e-3, 10e9);
let noise = rin.power_noise_rms_w();
assert!(noise > 0.0 && noise < 1e-3, "RIN noise={:.3e}", noise);
}
#[test]
fn pdl_eigenvalues_product_unity() {
let pdl = PolarizationDependentLoss::new(3.0);
let (t_max, t_min) = pdl.jones_eigenvalues();
assert!(
(t_max * t_min - 1.0).abs() < 1e-10,
"product={}",
t_max * t_min
);
}
#[test]
fn pdl_worst_case_il() {
let pdl = PolarizationDependentLoss::new(2.0);
assert!((pdl.worst_case_insertion_loss_db() - 1.0).abs() < 1e-10);
}
#[test]
fn thermal_noise_room_temperature() {
let tn = ThermalNoise::room_temperature(50.0, 1e9);
let v = tn.voltage_noise_rms_v();
assert!(v > 1e-6 && v < 1e-3, "V_rms={:.3e}", v);
}
#[test]
fn thermal_noise_current_ohms_law() {
let tn = ThermalNoise::room_temperature(1000.0, 1e6);
let v = tn.voltage_noise_rms_v();
let i = tn.current_noise_rms_a();
assert!((v / 1000.0 - i).abs() < 1e-20, "Ohm's law violation");
}
#[test]
fn phase_noise_coherence_time() {
let pn = PhaseNoise {
linewidth_hz: 100e3,
integration_time_s: 1e-9,
};
let tc = pn.coherence_time_s();
assert!((tc - 1.0 / (PI * 100e3)).abs() < 1e-12, "τ_c={:.3e}", tc);
}
#[test]
fn shot_noise_min_detectable_power_consistent() {
let sn = ShotNoise::typical_si();
let snr_req = 10.0; let p_min = sn.minimum_detectable_power_w(snr_req);
let snr_actual = sn.snr_shot_limited(p_min);
assert!(
(snr_actual / snr_req - 1.0).abs() < 0.5,
"SNR={}",
snr_actual
);
}
}