use std::f64::consts::PI;
const BOLTZMANN: f64 = 1.380_649e-23; const SPEED_OF_LIGHT: f64 = 2.997_924_58e8; const ELECTRON_CHARGE: f64 = 1.602_176_634e-19; const TEMPERATURE_K: f64 = 290.0;
#[derive(Debug, Clone)]
pub enum EoModulatorType {
Mzm {
vpi: f64,
insertion_loss_db: f64,
bias_point: MzmBias,
},
PhaseModulator {
vpi: f64,
},
Eam {
extinction_ratio_db: f64,
insertion_loss_db: f64,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MzmBias {
QuadraturePush,
QuadraturePull,
MinimumTransmission,
MaximumTransmission,
Custom(f64),
}
impl MzmBias {
pub fn radians(self) -> f64 {
match self {
MzmBias::QuadraturePush => PI / 2.0,
MzmBias::QuadraturePull => -PI / 2.0,
MzmBias::MinimumTransmission => PI,
MzmBias::MaximumTransmission => 0.0,
MzmBias::Custom(phi) => phi,
}
}
pub fn transmission(self) -> f64 {
let phi = self.radians();
0.5 * (1.0 + phi.cos())
}
}
#[derive(Debug, Clone)]
pub struct PhotodetectorParams {
pub responsivity: f64,
pub bandwidth_hz: f64,
pub dark_current_a: f64,
pub load_resistance: f64,
}
impl PhotodetectorParams {
pub fn typical_ingaas() -> Self {
PhotodetectorParams {
responsivity: 0.85,
bandwidth_hz: 20.0e9,
dark_current_a: 5.0e-9,
load_resistance: 50.0,
}
}
}
#[derive(Debug, Clone)]
pub struct AnalogPhotonicLink {
pub wavelength: f64,
pub optical_power_dbm: f64,
pub modulator: EoModulatorType,
pub fiber_length_km: f64,
pub fiber_loss_db_per_km: f64,
pub pd: PhotodetectorParams,
}
impl AnalogPhotonicLink {
pub fn new_intensity_modulated(wl: f64, power_dbm: f64, vpi: f64, length_km: f64) -> Self {
AnalogPhotonicLink {
wavelength: wl,
optical_power_dbm: power_dbm,
modulator: EoModulatorType::Mzm {
vpi,
insertion_loss_db: 3.0,
bias_point: MzmBias::QuadraturePush,
},
fiber_length_km: length_km,
fiber_loss_db_per_km: 0.2,
pd: PhotodetectorParams::typical_ingaas(),
}
}
pub fn dc_photocurrent_a(&self) -> f64 {
let power_w = dbm_to_watts(self.optical_power_dbm);
let transmission = self.modulator_bias_transmission();
power_w * transmission * self.pd.responsivity
}
fn modulator_bias_transmission(&self) -> f64 {
match &self.modulator {
EoModulatorType::Mzm { bias_point, .. } => bias_point.transmission(),
EoModulatorType::PhaseModulator { .. } => 1.0,
EoModulatorType::Eam {
extinction_ratio_db,
..
} => {
let er_linear = db_to_linear(*extinction_ratio_db);
(1.0 + 1.0 / er_linear) / 2.0
}
}
}
fn vpi(&self) -> f64 {
match &self.modulator {
EoModulatorType::Mzm { vpi, .. } => *vpi,
EoModulatorType::PhaseModulator { vpi } => *vpi,
EoModulatorType::Eam { .. } => {
1.0
}
}
}
pub fn rf_gain_db(&self) -> f64 {
let i_dc = self.dc_photocurrent_a();
let gain_linear = match &self.modulator {
EoModulatorType::Mzm {
vpi, bias_point, ..
} => {
let phi0 = bias_point.radians();
let slope = (-phi0.sin()) * PI / (2.0 * vpi);
let g = (PI * self.pd.responsivity * self.pd.load_resistance / (2.0 * vpi)).powi(2)
* (phi0.cos().powi(2))
+ (slope * self.pd.load_resistance * i_dc / self.pd.responsivity).powi(2) * 0.0;
let _ = g;
let numerator = PI * self.pd.load_resistance * i_dc;
let denominator = 2.0 * vpi;
(numerator / denominator).powi(2)
}
EoModulatorType::PhaseModulator { vpi } => {
let numerator = PI * self.pd.load_resistance * i_dc;
let denominator = 2.0 * vpi;
(numerator / denominator).powi(2)
}
EoModulatorType::Eam {
extinction_ratio_db,
..
} => {
let er = db_to_linear(*extinction_ratio_db);
let v_swing = 1.0;
let slope = (er - 1.0) / (er + 1.0) * i_dc / v_swing;
(slope * self.pd.load_resistance).powi(2)
}
};
10.0 * gain_linear.log10()
}
pub fn noise_figure_db(&self) -> f64 {
let i_dc = self.dc_photocurrent_a();
let g_linear = db_to_linear(self.rf_gain_db());
let rl = self.pd.load_resistance;
let s_shot = 2.0 * ELECTRON_CHARGE * i_dc;
let s_thermal = 4.0 * BOLTZMANN * TEMPERATURE_K / rl;
let s_dark = 2.0 * ELECTRON_CHARGE * self.pd.dark_current_a;
let s_out = (s_shot + s_thermal + s_dark) * rl.powi(2);
let s_in = BOLTZMANN * TEMPERATURE_K;
let nf_linear = s_out / (g_linear * s_in * rl);
if nf_linear > 0.0 {
10.0 * nf_linear.log10()
} else {
f64::INFINITY
}
}
pub fn sfdr_db_hz(&self) -> f64 {
let oip3_dbm = self.oip3_dbm();
let nf_db = self.noise_figure_db();
let noise_floor_dbm_per_hz = -174.0 + nf_db; (2.0 / 3.0) * (oip3_dbm - noise_floor_dbm_per_hz)
}
pub fn oip2_dbm(&self) -> f64 {
let i_dc = self.dc_photocurrent_a();
let rl = self.pd.load_resistance;
let vpi = self.vpi();
let oip2_w = (4.0 * vpi * i_dc * rl) / PI;
watts_to_dbm(oip2_w)
}
pub fn oip3_dbm(&self) -> f64 {
let i_dc = self.dc_photocurrent_a();
let rl = self.pd.load_resistance;
let vpi = self.vpi();
match &self.modulator {
EoModulatorType::Mzm { bias_point, .. } => {
let phi0 = bias_point.radians();
let factor = phi0.sin().abs();
let oip3_w = (2.0 / 3.0) * (2.0 * vpi / PI).powi(2) * i_dc * rl * factor;
watts_to_dbm(oip3_w)
}
EoModulatorType::PhaseModulator { vpi } => {
let oip3_w = (2.0 / 3.0) * (2.0 * vpi / PI).powi(2) * i_dc * rl * 0.5;
watts_to_dbm(oip3_w)
}
EoModulatorType::Eam {
extinction_ratio_db,
..
} => {
let er = db_to_linear(*extinction_ratio_db);
let oip3_w = (2.0 / 3.0) * i_dc.powi(2) * rl * er / (er + 1.0);
watts_to_dbm(oip3_w)
}
}
}
pub fn p1db_dbm(&self) -> f64 {
self.oip3_dbm() - 9.6
}
pub fn bandwidth_hz(&self) -> f64 {
let bw_pd = self.pd.bandwidth_hz;
let d_ps_per_nm_km: f64 = match self.wavelength {
wl if wl < 1.35e-9 => 0.0_f64,
wl if wl < 1.40e-9 => -50.0_f64, wl if wl < 1.60e-9 => 17.0_f64, _ => 20.0_f64,
};
let delta_lambda_nm: f64 = 0.1; let bw_dispersion = if d_ps_per_nm_km.abs() > 1e-10 {
let d_s_per_m_km = d_ps_per_nm_km * 1e-12 / 1e-9; 1.0 / (PI * d_s_per_m_km.abs() * self.fiber_length_km * delta_lambda_nm * 1e-9)
} else {
f64::INFINITY
};
bw_pd.min(bw_dispersion)
}
pub fn cnr_db(&self, rf_power_dbm: f64, rf_freq_hz: f64) -> f64 {
let rf_power_w = dbm_to_watts(rf_power_dbm);
let g_linear = db_to_linear(self.rf_gain_db());
let signal_out = rf_power_w * g_linear;
let nu = SPEED_OF_LIGHT / self.wavelength;
let i_dc = self.dc_photocurrent_a();
let rl = self.pd.load_resistance;
let shot_psd = 2.0 * ELECTRON_CHARGE * i_dc * rl.powi(2); let thermal_psd = 4.0 * BOLTZMANN * TEMPERATURE_K * rl; let rin_per_hz = 1e-15_f64; let rin_psd = rin_per_hz * i_dc.powi(2) * rl.powi(2);
let _ = (nu, rf_freq_hz); let noise_1hz = shot_psd + thermal_psd + rin_psd;
let cnr = signal_out / noise_1hz;
10.0 * cnr.log10()
}
pub fn link_budget(&self) -> LinkBudget {
LinkBudget {
rf_gain_db: self.rf_gain_db(),
noise_figure_db: self.noise_figure_db(),
sfdr_db_hz: self.sfdr_db_hz(),
bandwidth_hz: self.bandwidth_hz(),
oip3_dbm: self.oip3_dbm(),
}
}
}
#[derive(Debug, Clone)]
pub struct LinkBudget {
pub rf_gain_db: f64,
pub noise_figure_db: f64,
pub sfdr_db_hz: f64,
pub bandwidth_hz: f64,
pub oip3_dbm: f64,
}
impl std::fmt::Display for LinkBudget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "=== Analog Photonic Link Budget ===")?;
writeln!(f, " RF Gain : {:+.2} dB", self.rf_gain_db)?;
writeln!(f, " Noise Figure : {:.2} dB", self.noise_figure_db)?;
writeln!(f, " SFDR : {:.2} dB·Hz^(2/3)", self.sfdr_db_hz)?;
writeln!(f, " Bandwidth : {:.3} GHz", self.bandwidth_hz * 1e-9)?;
write!(f, " OIP3 : {:+.2} dBm", self.oip3_dbm)
}
}
fn dbm_to_watts(dbm: f64) -> f64 {
1e-3 * 10.0_f64.powf(dbm / 10.0)
}
fn watts_to_dbm(w: f64) -> f64 {
if w > 0.0 {
10.0 * (w * 1e3).log10()
} else {
f64::NEG_INFINITY
}
}
fn db_to_linear(db: f64) -> f64 {
10.0_f64.powf(db / 10.0)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
fn default_link() -> AnalogPhotonicLink {
AnalogPhotonicLink::new_intensity_modulated(1550e-9, 0.0, 5.0, 10.0)
}
#[test]
fn test_dc_photocurrent() {
let link = default_link();
let i_dc = link.dc_photocurrent_a();
assert_abs_diff_eq!(i_dc, 0.425e-3, epsilon = 1e-6);
}
#[test]
fn test_rf_gain_negative() {
let link = default_link();
let gain = link.rf_gain_db();
assert!(gain < 0.0, "RF gain should be negative at 0 dBm: {}", gain);
}
#[test]
fn test_rf_gain_increases_with_optical_power() {
let link_low = AnalogPhotonicLink::new_intensity_modulated(1550e-9, 0.0, 5.0, 10.0);
let link_high = AnalogPhotonicLink::new_intensity_modulated(1550e-9, 10.0, 5.0, 10.0);
assert!(
link_high.rf_gain_db() > link_low.rf_gain_db(),
"Higher optical power should give higher RF gain"
);
}
#[test]
fn test_oip3_increases_with_optical_power() {
let link_low = AnalogPhotonicLink::new_intensity_modulated(1550e-9, 0.0, 5.0, 10.0);
let link_high = AnalogPhotonicLink::new_intensity_modulated(1550e-9, 10.0, 5.0, 10.0);
assert!(link_high.oip3_dbm() > link_low.oip3_dbm());
}
#[test]
fn test_p1db_below_oip3() {
let link = default_link();
let p1db = link.p1db_dbm();
let oip3 = link.oip3_dbm();
assert_abs_diff_eq!(oip3 - p1db, 9.6, epsilon = 0.01);
}
#[test]
fn test_link_budget_fields() {
let link = default_link();
let budget = link.link_budget();
assert!(budget.rf_gain_db.is_finite());
assert!(budget.noise_figure_db.is_finite());
assert!(budget.sfdr_db_hz.is_finite());
assert!(budget.bandwidth_hz > 0.0);
assert!(budget.oip3_dbm.is_finite());
}
#[test]
fn test_mzm_bias_quadrature_transmission() {
let bias = MzmBias::QuadraturePush;
assert_abs_diff_eq!(bias.transmission(), 0.5, epsilon = 1e-10);
}
#[test]
fn test_mzm_bias_max_transmission() {
let bias = MzmBias::MaximumTransmission;
assert_abs_diff_eq!(bias.transmission(), 1.0, epsilon = 1e-10);
}
#[test]
fn test_mzm_bias_min_transmission() {
let bias = MzmBias::MinimumTransmission;
assert_abs_diff_eq!(bias.transmission(), 0.0, epsilon = 1e-10);
}
#[test]
fn test_bandwidth_limited_by_pd() {
let link = default_link();
assert!(link.bandwidth_hz() <= link.pd.bandwidth_hz + 1.0);
}
#[test]
fn test_sfdr_reasonable_range() {
let link = default_link();
let sfdr = link.sfdr_db_hz();
assert!(sfdr > 50.0 && sfdr < 180.0, "SFDR={:.1} dB·Hz^(2/3)", sfdr);
}
#[test]
fn test_dbm_watts_roundtrip() {
for dbm in [-30.0_f64, -10.0, 0.0, 10.0, 20.0] {
let w = dbm_to_watts(dbm);
let back = watts_to_dbm(w);
assert_abs_diff_eq!(back, dbm, epsilon = 1e-9);
}
}
}