use std::f64::consts::PI;
const H_PLANCK: f64 = 6.626_070_15e-34;
const C_LIGHT: f64 = 2.997_924_58e8;
const Q_ELECTRON: f64 = 1.602_176_634e-19;
#[derive(Debug, Clone)]
pub struct Soa {
pub active_length_mm: f64,
pub active_width_um: f64,
pub active_height_um: f64,
pub injection_current_ma: f64,
pub internal_loss_per_cm: f64,
pub confinement_factor: f64,
pub differential_gain: f64,
pub transparency_density: f64,
pub carrier_lifetime_ns: f64,
pub wavelength: f64,
}
impl Soa {
pub fn new_ingaasp_1550nm(current_ma: f64, length_mm: f64) -> Self {
Self {
active_length_mm: length_mm,
active_width_um: 2.0,
active_height_um: 0.2,
injection_current_ma: current_ma,
internal_loss_per_cm: 20.0,
confinement_factor: 0.4,
differential_gain: 3.0e-20,
transparency_density: 1.0e24, carrier_lifetime_ns: 0.3, wavelength: 1550e-9,
}
}
pub fn new_bulk_1310nm(current_ma: f64, length_mm: f64) -> Self {
Self {
active_length_mm: length_mm,
active_width_um: 2.5,
active_height_um: 0.25,
injection_current_ma: current_ma,
internal_loss_per_cm: 25.0,
confinement_factor: 0.4,
differential_gain: 3.5e-20,
transparency_density: 1.0e24,
carrier_lifetime_ns: 0.4, wavelength: 1310e-9,
}
}
pub fn active_volume_m3(&self) -> f64 {
let l = self.active_length_mm * 1e-3;
let w = self.active_width_um * 1e-6;
let d = self.active_height_um * 1e-6;
l * w * d
}
pub fn current_density_a_per_m2(&self) -> f64 {
let i_a = self.injection_current_ma * 1e-3;
let w = self.active_width_um * 1e-6;
let l = self.active_length_mm * 1e-3;
i_a / (w * l)
}
pub fn carrier_density(&self) -> f64 {
let j = self.current_density_a_per_m2();
let d = self.active_height_um * 1e-6;
let tau_s = self.carrier_lifetime_ns * 1e-9;
j * tau_s / (Q_ELECTRON * d)
}
pub fn material_gain(&self) -> f64 {
let n = self.carrier_density();
self.differential_gain * (n - self.transparency_density)
}
pub fn modal_gain_per_cm(&self) -> f64 {
let g_mat_per_cm = self.material_gain() * 1e-2; self.confinement_factor * g_mat_per_cm - self.internal_loss_per_cm
}
pub fn single_pass_gain_db(&self) -> f64 {
let g_m_per_m = self.modal_gain_per_cm() * 100.0; let l = self.active_length_mm * 1e-3;
let gain_linear = (g_m_per_m * l).exp();
10.0 * gain_linear.log10()
}
pub fn saturation_power_dbm(&self) -> f64 {
let nu = C_LIGHT / self.wavelength;
let w = self.active_width_um * 1e-6;
let d = self.active_height_um * 1e-6;
let tau_s = self.carrier_lifetime_ns * 1e-9;
let a_eff = w * d / self.confinement_factor;
let p_sat_w =
H_PLANCK * nu * a_eff / (self.differential_gain * self.confinement_factor * tau_s);
10.0 * (p_sat_w * 1e3).log10()
}
pub fn gain_db(&self, input_power_dbm: f64) -> f64 {
let p_in_mw = 10.0_f64.powf(input_power_dbm / 10.0);
let p_sat_mw = 10.0_f64.powf(self.saturation_power_dbm() / 10.0);
let g_ss = 10.0_f64.powf(self.single_pass_gain_db() / 10.0);
let g = g_ss / (1.0 + p_in_mw * g_ss / p_sat_mw);
10.0 * g.log10()
}
fn nsp(&self) -> f64 {
let n = self.carrier_density();
if n <= self.transparency_density {
return 1e6;
}
n / (n - self.transparency_density)
}
pub fn noise_figure_db(&self) -> f64 {
let nsp = self.nsp();
let g_ss = 10.0_f64.powf(self.single_pass_gain_db() / 10.0);
if g_ss <= 1.0 {
return 3.0_f64.max(-10.0 * g_ss.log10());
}
let nf_linear = 2.0 * nsp * (1.0 - 1.0 / g_ss) + 1.0 / g_ss;
if nf_linear <= 0.0 {
return 3.0;
}
10.0 * nf_linear.log10()
}
pub fn alpha_factor(&self) -> f64 {
4.0
}
pub fn xgm_bandwidth_ghz(&self) -> f64 {
let tau_s = self.carrier_lifetime_ns * 1e-9;
1.0 / (2.0 * PI * tau_s) * 1e-9 }
pub fn polarization_sensitivity_db(&self) -> f64 {
let aspect = self.active_width_um / self.active_height_um;
(aspect.log10() * 1.5).clamp(0.0, 3.0)
}
pub fn gain_at_temperature(&self, temp_c: f64) -> f64 {
let t0_k = 60.0; let delta_t = temp_c - 25.0; let g_ref_db = self.single_pass_gain_db();
let scale_db = -delta_t / t0_k * 10.0 * f64::ln(10.0).recip() * 4.343;
g_ref_db + scale_db
}
}
#[derive(Debug, Clone)]
pub struct SoaXgm {
pub soa: Soa,
pub pump_wavelength: f64,
pub probe_wavelength: f64,
}
impl SoaXgm {
pub fn new(soa: Soa, pump_wl: f64, probe_wl: f64) -> Self {
Self {
soa,
pump_wavelength: pump_wl,
probe_wavelength: probe_wl,
}
}
pub fn conversion_efficiency_db(&self, pump_power_dbm: f64) -> f64 {
let probe_gain = self.soa.gain_db(pump_power_dbm);
let pump_gain = self.soa.gain_db(pump_power_dbm);
probe_gain - pump_gain + 3.0 }
pub fn extinction_ratio_db(&self, input_er_db: f64) -> f64 {
let g_max_lin = 10.0_f64.powf(self.soa.single_pass_gain_db() / 10.0);
let p_sat = 10.0_f64.powf(self.soa.saturation_power_dbm() / 10.0); let p_mark_dbm = 3.0; let g_mark_lin = 10.0_f64.powf(self.soa.gain_db(p_mark_dbm) / 10.0);
let g_space_lin = g_max_lin;
let er_in_lin = 10.0_f64.powf(-input_er_db / 10.0);
if g_mark_lin <= 0.0 || p_sat <= 0.0 {
return 0.0;
}
let er_out_lin = (g_space_lin * er_in_lin) / g_mark_lin;
-10.0 * er_out_lin.log10()
}
pub fn bandwidth_ghz(&self) -> f64 {
self.soa.xgm_bandwidth_ghz()
}
}
pub fn modal_gain_to_linear(modal_gain_per_cm: f64, length_mm: f64) -> f64 {
let g_per_m = modal_gain_per_cm * 100.0;
let l_m = length_mm * 1e-3;
(g_per_m * l_m).exp()
}
pub fn linear_gain_to_db(gain_linear: f64) -> f64 {
if gain_linear <= 0.0 {
return f64::NEG_INFINITY;
}
10.0 * gain_linear.log10()
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_carrier_density_increases_with_current() {
let soa_low = Soa::new_ingaasp_1550nm(50.0, 0.5);
let soa_high = Soa::new_ingaasp_1550nm(200.0, 0.5);
assert!(
soa_high.carrier_density() > soa_low.carrier_density(),
"Higher current must produce higher carrier density"
);
}
#[test]
fn test_modal_gain_positive_above_transparency() {
let soa = Soa::new_ingaasp_1550nm(300.0, 1.0);
let gm = soa.modal_gain_per_cm();
assert!(
gm > 0.0,
"Modal gain must be positive at high injection; got {gm}"
);
}
#[test]
fn test_single_pass_gain_increases_with_length() {
let soa_short = Soa::new_ingaasp_1550nm(200.0, 0.5);
let soa_long = Soa::new_ingaasp_1550nm(800.0, 2.0); let g_short = soa_short.single_pass_gain_db();
let g_long = soa_long.single_pass_gain_db();
assert!(
g_long > g_short,
"Longer SOA (at constant J) must have higher gain; short={g_short} dB, long={g_long} dB"
);
}
#[test]
fn test_noise_figure_above_3db() {
let soa = Soa::new_ingaasp_1550nm(200.0, 1.0);
let nf = soa.noise_figure_db();
assert!(nf >= 3.0, "SOA NF must be ≥ 3 dB; got {nf}");
}
#[test]
fn test_gain_compression_under_saturation() {
let soa = Soa::new_ingaasp_1550nm(200.0, 1.0);
let g_small = soa.gain_db(-30.0);
let g_large = soa.gain_db(5.0);
assert!(
g_small >= g_large,
"Gain must decrease with input power; g(-30 dBm)={g_small}, g(5 dBm)={g_large}"
);
}
#[test]
fn test_xgm_bandwidth_finite_positive() {
let soa = Soa::new_ingaasp_1550nm(200.0, 1.0);
let bw = soa.xgm_bandwidth_ghz();
assert!(
bw > 0.0 && bw.is_finite(),
"XGM bandwidth must be finite and positive; got {bw}"
);
}
#[test]
fn test_saturation_power_positive() {
let soa = Soa::new_ingaasp_1550nm(200.0, 1.0);
let p_sat = soa.saturation_power_dbm();
assert!(
p_sat.is_finite(),
"Saturation power must be finite; got {p_sat}"
);
}
#[test]
fn test_gain_decreases_with_temperature() {
let soa = Soa::new_ingaasp_1550nm(200.0, 1.0);
let g_25 = soa.gain_at_temperature(25.0);
let g_60 = soa.gain_at_temperature(60.0);
assert!(
g_25 > g_60,
"Gain must decrease with temperature; g(25°C)={g_25}, g(60°C)={g_60}"
);
}
#[test]
fn test_active_volume_calculation() {
let soa = Soa::new_ingaasp_1550nm(200.0, 1.0);
let v = soa.active_volume_m3();
assert_abs_diff_eq!(v, 1e-3 * 2e-6 * 0.2e-6, epsilon = 1e-25);
}
#[test]
fn test_modal_gain_to_linear_utility() {
let g = modal_gain_to_linear(0.0, 1.0);
assert_abs_diff_eq!(g, 1.0, epsilon = 1e-12);
}
}