use super::edfa::PumpDirection;
const H_PLANCK: f64 = 6.626_070_15e-34;
const C_LIGHT: f64 = 2.997_924_58e8;
const RAMAN_PEAK_SHIFT_CM_INV: f64 = 440.0;
const RAMAN_LINEWIDTH_CM_INV: f64 = 60.0;
#[derive(Debug, Clone, PartialEq)]
pub enum RamanFiberType {
Smf28,
Dcf,
Hnlf,
Custom {
raman_gain_coeff: f64,
effective_area_um2: f64,
loss_db_per_km: f64,
},
}
impl RamanFiberType {
pub fn raman_gain_coefficient(&self) -> f64 {
let g_per_w_km = match self {
RamanFiberType::Smf28 => 0.4,
RamanFiberType::Dcf => 3.0,
RamanFiberType::Hnlf => 2.0,
RamanFiberType::Custom {
raman_gain_coeff, ..
} => *raman_gain_coeff,
};
g_per_w_km * 1e-3 }
pub fn effective_area_um2(&self) -> f64 {
match self {
RamanFiberType::Smf28 => 80.0,
RamanFiberType::Dcf => 20.0,
RamanFiberType::Hnlf => 12.0,
RamanFiberType::Custom {
effective_area_um2, ..
} => *effective_area_um2,
}
}
pub fn loss_db_per_km_at_1550(&self) -> f64 {
match self {
RamanFiberType::Smf28 => 0.2,
RamanFiberType::Dcf => 0.5,
RamanFiberType::Hnlf => 0.9,
RamanFiberType::Custom { loss_db_per_km, .. } => *loss_db_per_km,
}
}
fn loss_per_m(&self) -> f64 {
self.loss_db_per_km_at_1550() * 1e-3 * f64::ln(10.0) / 10.0
}
}
#[derive(Debug, Clone)]
pub struct RamanAmplifier {
pub fiber_length_km: f64,
pub pump_wavelengths: Vec<f64>,
pub pump_powers: Vec<f64>,
pub signal_wavelength: f64,
pub pump_direction: PumpDirection,
pub fiber_type: RamanFiberType,
}
impl RamanAmplifier {
pub fn new_single_pump(pump_wl: f64, pump_mw: f64, signal_wl: f64, length_km: f64) -> Self {
Self {
fiber_length_km: length_km,
pump_wavelengths: vec![pump_wl],
pump_powers: vec![pump_mw],
signal_wavelength: signal_wl,
pump_direction: PumpDirection::Counterpropagating,
fiber_type: RamanFiberType::Smf28,
}
}
pub fn new_dual_pump(
pump_wls: [f64; 2],
pump_mws: [f64; 2],
signal_wl: f64,
length_km: f64,
) -> Self {
Self {
fiber_length_km: length_km,
pump_wavelengths: pump_wls.to_vec(),
pump_powers: pump_mws.to_vec(),
signal_wavelength: signal_wl,
pump_direction: PumpDirection::Counterpropagating,
fiber_type: RamanFiberType::Smf28,
}
}
pub fn raman_shift_cm_inv(&self, pump_idx: usize) -> f64 {
if pump_idx >= self.pump_wavelengths.len() {
return 0.0;
}
let pump_wl = self.pump_wavelengths[pump_idx];
let nu_pump_cm = 1.0 / (pump_wl * 100.0); let nu_sig_cm = 1.0 / (self.signal_wavelength * 100.0);
nu_pump_cm - nu_sig_cm
}
fn raman_shape_at_shift(&self, shift_cm_inv: f64) -> f64 {
let gamma_half = RAMAN_LINEWIDTH_CM_INV / 2.0;
let delta = shift_cm_inv - RAMAN_PEAK_SHIFT_CM_INV;
(gamma_half * gamma_half) / (delta * delta + gamma_half * gamma_half)
}
pub fn raman_gain_coefficient_at_signal(&self) -> f64 {
if self.pump_wavelengths.is_empty() {
return 0.0;
}
let g_bulk = self.fiber_type.raman_gain_coefficient();
let total: f64 = self
.pump_wavelengths
.iter()
.zip(self.pump_powers.iter())
.enumerate()
.map(|(i, (&_pwl, &p_mw))| {
let shift = self.raman_shift_cm_inv(i);
let shape = self.raman_shape_at_shift(shift);
let p_w = p_mw * 1e-3;
g_bulk * p_w * shape
})
.sum();
total
}
pub fn effective_length_m(&self) -> f64 {
let alpha = self.fiber_type.loss_per_m();
let l_m = self.fiber_length_km * 1e3;
if alpha < 1e-12 {
l_m
} else {
(1.0 - (-alpha * l_m).exp()) / alpha
}
}
pub fn on_off_gain_db(&self) -> f64 {
let g_coeff = self.raman_gain_coefficient_at_signal(); let l_eff = self.effective_length_m();
let gain_linear = (g_coeff * l_eff).exp();
10.0 * gain_linear.log10()
}
pub fn net_gain_db(&self) -> f64 {
let alpha_db_km = self.fiber_type.loss_db_per_km_at_1550();
let signal_loss_db = alpha_db_km * self.fiber_length_km;
self.on_off_gain_db() - signal_loss_db
}
pub fn noise_figure_db(&self) -> f64 {
let alpha_db_km = self.fiber_type.loss_db_per_km_at_1550();
let loss_db = alpha_db_km * self.fiber_length_km;
let g_on_off = self.on_off_gain_db();
let nf = 2.0 * loss_db - g_on_off + 3.0;
nf.max(0.1)
}
pub fn gain_at_wavelength(&self, wavelength: f64) -> f64 {
if self.pump_wavelengths.is_empty() {
return 0.0;
}
let g_bulk_per_w_km = self.fiber_type.raman_gain_coefficient() * 1e3;
let total_gain_coeff_nep_per_km: f64 = self
.pump_wavelengths
.iter()
.zip(self.pump_powers.iter())
.map(|(&pump_wl, &p_mw)| {
let nu_pump_cm = 1.0 / (pump_wl * 100.0);
let nu_sig_cm = 1.0 / (wavelength * 100.0);
let shift = nu_pump_cm - nu_sig_cm;
let shape = self.raman_shape_at_shift(shift);
let p_w = p_mw * 1e-3;
g_bulk_per_w_km * p_w * shape
})
.sum();
total_gain_coeff_nep_per_km * 10.0 / f64::ln(10.0)
}
pub fn transparency_pump_power_mw(&self) -> f64 {
let alpha = self.fiber_type.loss_per_m();
let l_m = self.fiber_length_km * 1e3;
let l_eff = self.effective_length_m();
let g_bulk = self.fiber_type.raman_gain_coefficient();
let shape = if self.pump_wavelengths.is_empty() {
1.0
} else {
self.raman_shape_at_shift(self.raman_shift_cm_inv(0))
};
if g_bulk * shape * l_eff < 1e-20 {
return f64::INFINITY;
}
let p_w = alpha * l_m / (g_bulk * shape * l_eff);
p_w * 1e3 }
pub fn drb_penalty_db(&self) -> f64 {
let rayleigh_coeff = 1.5e-4_f64; let _l_m = self.fiber_length_km * 1e3;
let g_linear = 10.0_f64.powf(self.on_off_gain_db() / 10.0);
let l_eff = self.effective_length_m();
let drb_ratio = (rayleigh_coeff * l_eff).powi(2) * g_linear;
if drb_ratio <= 0.0 {
return 0.0;
}
-10.0 * drb_ratio.log10() }
}
#[derive(Debug, Clone)]
pub struct LumpedRamanAmplifier {
pub fiber_length_m: f64,
pub pump_power_w: f64,
pub signal_wavelength: f64,
pub fiber_type: RamanFiberType,
}
impl LumpedRamanAmplifier {
pub fn new_hnlf(length_m: f64, pump_w: f64, signal_wl: f64) -> Self {
Self {
fiber_length_m: length_m,
pump_power_w: pump_w,
signal_wavelength: signal_wl,
fiber_type: RamanFiberType::Hnlf,
}
}
pub fn effective_length_m(&self) -> f64 {
let alpha = self.fiber_type.loss_per_m();
if alpha < 1e-12 {
self.fiber_length_m
} else {
(1.0 - (-alpha * self.fiber_length_m).exp()) / alpha
}
}
pub fn gain_db(&self) -> f64 {
let g_bulk = self.fiber_type.raman_gain_coefficient();
let l_eff = self.effective_length_m();
let gain_linear = (g_bulk * self.pump_power_w * l_eff).exp();
10.0 * gain_linear.log10()
}
pub fn noise_figure_db(&self) -> f64 {
let loss_db = self.fiber_type.loss_db_per_km_at_1550() * self.fiber_length_m * 1e-3;
(2.0 * loss_db + 3.0).max(3.0)
}
pub fn threshold_power_w(&self) -> f64 {
let g_r = self.fiber_type.raman_gain_coefficient();
let a_eff_m2 = self.fiber_type.effective_area_um2() * 1e-12;
let l_eff = self.effective_length_m();
if g_r * l_eff < 1e-30 {
return f64::INFINITY;
}
16.0 * a_eff_m2 / (g_r * l_eff)
}
}
pub fn wavelength_to_raman_shift(signal_wl: f64, pump_wl: f64) -> f64 {
let nu_pump = 1.0 / (pump_wl * 100.0); let nu_sig = 1.0 / (signal_wl * 100.0); nu_pump - nu_sig
}
pub fn stokes_wavelength(pump_wl: f64, shift_cm_inv: f64) -> f64 {
let nu_pump_cm = 1.0 / (pump_wl * 100.0);
let nu_stokes_cm = nu_pump_cm - shift_cm_inv;
if nu_stokes_cm <= 0.0 {
return f64::INFINITY;
}
1.0 / (nu_stokes_cm * 100.0)
}
pub fn photon_energy_j(wavelength: f64) -> f64 {
H_PLANCK * C_LIGHT / wavelength
}
pub fn raman_gain_profile(shift_cm_inv: f64) -> f64 {
let gamma_half = RAMAN_LINEWIDTH_CM_INV / 2.0;
let delta = shift_cm_inv - RAMAN_PEAK_SHIFT_CM_INV;
(gamma_half * gamma_half) / (delta * delta + gamma_half * gamma_half)
}
pub fn raman_bandwidth_fwhm_cm_inv() -> f64 {
RAMAN_LINEWIDTH_CM_INV
}
fn _stokes_wavelength_nm(pump_nm: f64, shift_cm_inv: f64) -> f64 {
stokes_wavelength(pump_nm * 1e-9, shift_cm_inv) * 1e9
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_raman_gain_positive_for_adequate_pump() {
let dra = RamanAmplifier::new_single_pump(1455e-9, 1000.0, 1550e-9, 80.0);
let g = dra.on_off_gain_db();
assert!(g > 0.0, "On-off gain should be positive; got {g}");
}
#[test]
fn test_effective_length_less_than_physical_length() {
let dra = RamanAmplifier::new_single_pump(1455e-9, 500.0, 1550e-9, 80.0);
let l_eff = dra.effective_length_m();
let l_phys = 80.0 * 1e3;
assert!(
l_eff < l_phys,
"Effective length {l_eff} must be less than physical length {l_phys}"
);
}
#[test]
fn test_stokes_wavelength_relation() {
let stokes = stokes_wavelength(1455e-9, 440.0);
assert!(
stokes > 1540e-9 && stokes < 1580e-9,
"Stokes wavelength {:.1} nm out of expected range",
stokes * 1e9
);
}
#[test]
fn test_raman_profile_peak_at_440() {
let peak_val = raman_gain_profile(440.0);
let off_val = raman_gain_profile(0.0);
assert_abs_diff_eq!(peak_val, 1.0, epsilon = 1e-12);
assert!(
off_val < peak_val,
"Profile must be less than 1 away from peak"
);
}
#[test]
fn test_transparency_pump_power_finite() {
let dra = RamanAmplifier::new_single_pump(1455e-9, 100.0, 1550e-9, 80.0);
let p_trans = dra.transparency_pump_power_mw();
assert!(
p_trans.is_finite() && p_trans > 0.0,
"Transparency power must be finite and positive; got {p_trans}"
);
}
#[test]
fn test_lumped_raman_gain_increases_with_pump() {
let amp_low = LumpedRamanAmplifier::new_hnlf(500.0, 0.5, 1550e-9);
let amp_high = LumpedRamanAmplifier::new_hnlf(500.0, 2.0, 1550e-9);
assert!(
amp_high.gain_db() > amp_low.gain_db(),
"Higher pump power must produce more gain"
);
}
#[test]
fn test_dual_pump_raman_shift() {
let dra = RamanAmplifier::new_dual_pump([1430e-9, 1455e-9], [500.0, 500.0], 1550e-9, 80.0);
let shift0 = dra.raman_shift_cm_inv(0);
let shift1 = dra.raman_shift_cm_inv(1);
assert!(
shift0 > 0.0,
"Raman shift for pump 0 must be positive; got {shift0}"
);
assert!(
shift1 > 0.0,
"Raman shift for pump 1 must be positive; got {shift1}"
);
}
#[test]
fn test_fiber_type_loss_coefficients() {
let smf = RamanFiberType::Smf28;
let dcf = RamanFiberType::Dcf;
assert!(
dcf.loss_db_per_km_at_1550() > smf.loss_db_per_km_at_1550(),
"DCF must have higher loss than SMF-28"
);
}
#[test]
fn test_raman_gain_coefficient_units() {
let smf = RamanFiberType::Smf28;
assert_abs_diff_eq!(smf.raman_gain_coefficient(), 4e-4, epsilon = 1e-10);
}
#[test]
fn test_net_gain_less_than_on_off_gain() {
let dra = RamanAmplifier::new_single_pump(1455e-9, 1000.0, 1550e-9, 80.0);
let on_off = dra.on_off_gain_db();
let net = dra.net_gain_db();
assert!(
net < on_off,
"Net gain {net} must be less than on-off gain {on_off}"
);
}
}