use crate::error::OxiPhotonError;
const H_PLANCK: f64 = 6.626_070_15e-34; const E_CHARGE: f64 = 1.602_176_634e-19; const C0: f64 = 2.997_924_58e8; const KB: f64 = 1.380_649e-23;
#[derive(Debug, Clone)]
pub struct PinPhotodiode {
pub responsivity_a_per_w: f64,
pub peak_wavelength_nm: f64,
pub bandwidth_ghz: f64,
pub dark_current_na: f64,
pub capacitance_ff: f64,
pub series_resistance_ohm: f64,
pub active_area_um2: f64,
}
impl PinPhotodiode {
pub fn new(
responsivity_a_per_w: f64,
peak_wavelength_nm: f64,
bandwidth_ghz: f64,
dark_current_na: f64,
capacitance_ff: f64,
series_resistance_ohm: f64,
active_area_um2: f64,
) -> Result<Self, OxiPhotonError> {
if !responsivity_a_per_w.is_finite() || responsivity_a_per_w <= 0.0 {
return Err(OxiPhotonError::NumericalError(
"responsivity must be positive and finite".into(),
));
}
if !peak_wavelength_nm.is_finite() || peak_wavelength_nm <= 0.0 {
return Err(OxiPhotonError::NumericalError(
"peak_wavelength_nm must be positive and finite".into(),
));
}
if !bandwidth_ghz.is_finite() || bandwidth_ghz <= 0.0 {
return Err(OxiPhotonError::NumericalError(
"bandwidth_ghz must be positive and finite".into(),
));
}
Ok(Self {
responsivity_a_per_w,
peak_wavelength_nm,
bandwidth_ghz,
dark_current_na,
capacitance_ff,
series_resistance_ohm,
active_area_um2,
})
}
pub fn ingaas_1550() -> Self {
Self {
responsivity_a_per_w: 0.9,
peak_wavelength_nm: 1550.0,
bandwidth_ghz: 10.0,
dark_current_na: 1.0,
capacitance_ff: 200.0,
series_resistance_ohm: 10.0,
active_area_um2: 50.0,
}
}
pub fn si_900nm() -> Self {
Self {
responsivity_a_per_w: 0.55,
peak_wavelength_nm: 900.0,
bandwidth_ghz: 1.0,
dark_current_na: 5.0,
capacitance_ff: 500.0,
series_resistance_ohm: 20.0,
active_area_um2: 200.0,
}
}
pub fn quantum_efficiency(&self) -> f64 {
let lambda_m = self.peak_wavelength_nm * 1e-9;
self.responsivity_a_per_w * H_PLANCK * C0 / (lambda_m * E_CHARGE)
}
pub fn quantum_efficiency_at(&self, lambda_nm: f64) -> f64 {
let eta_peak = self.quantum_efficiency();
eta_peak * (lambda_nm / self.peak_wavelength_nm)
}
pub fn photocurrent_a(&self, power_w: f64) -> f64 {
self.responsivity_a_per_w * power_w
}
pub fn frequency_response(&self, freq_ghz: f64) -> f64 {
let ratio = freq_ghz / self.bandwidth_ghz;
1.0 / (1.0 + ratio * ratio).sqrt()
}
pub fn shot_noise_a_per_sqrt_hz(&self, signal_power_w: f64) -> f64 {
let i_sig = self.photocurrent_a(signal_power_w);
let i_dark = self.dark_current_na * 1e-9;
(2.0 * E_CHARGE * (i_sig + i_dark)).sqrt()
}
pub fn thermal_noise_a_per_sqrt_hz(&self, temperature_k: f64, load_resistance_ohm: f64) -> f64 {
(4.0 * KB * temperature_k / load_resistance_ohm).sqrt()
}
pub fn total_noise_a_per_sqrt_hz(&self, power_w: f64, temp_k: f64, r_load: f64) -> f64 {
let i_shot = self.shot_noise_a_per_sqrt_hz(power_w);
let i_th = self.thermal_noise_a_per_sqrt_hz(temp_k, r_load);
(i_shot * i_shot + i_th * i_th).sqrt()
}
pub fn nep_w_per_sqrt_hz(&self, temp_k: f64, r_load: f64) -> f64 {
let i_noise = self.total_noise_a_per_sqrt_hz(0.0, temp_k, r_load);
i_noise / self.responsivity_a_per_w
}
pub fn d_star_cm_sqrt_hz_per_w(&self, temp_k: f64, r_load: f64) -> f64 {
let nep = self.nep_w_per_sqrt_hz(temp_k, r_load);
let area_cm2 = self.active_area_um2 * 1e-8;
area_cm2.sqrt() / nep
}
pub fn saturation_power_dbm(&self) -> f64 {
let i_max_a = 10e-3; let p_sat_w = i_max_a / self.responsivity_a_per_w;
10.0 * (p_sat_w / 1e-3).log10()
}
pub fn rise_time_ps(&self) -> f64 {
0.35 / (self.bandwidth_ghz * 1e9) * 1e12 }
pub fn snr_db(&self, power_w: f64, bandwidth_ghz: f64, temp_k: f64, r_load: f64) -> f64 {
let i_sig = self.photocurrent_a(power_w);
let noise_density = self.total_noise_a_per_sqrt_hz(power_w, temp_k, r_load);
let bandwidth_hz = bandwidth_ghz * 1e9;
let i_noise = noise_density * bandwidth_hz.sqrt();
if i_noise == 0.0 {
return f64::INFINITY;
}
20.0 * (i_sig / i_noise).log10()
}
pub fn mdp_dbm(&self, bandwidth_ghz: f64, temp_k: f64, r_load: f64) -> f64 {
let nep = self.nep_w_per_sqrt_hz(temp_k, r_load);
let bandwidth_hz = bandwidth_ghz * 1e9;
let mdp_w = nep * bandwidth_hz.sqrt();
10.0 * (mdp_w / 1e-3).log10()
}
}
#[derive(Debug, Clone)]
pub struct AvalanchePhotodiode {
pub pin: PinPhotodiode,
pub gain_m: f64,
pub excess_noise_factor_x: f64,
pub gain_bandwidth_product_ghz: f64,
}
impl AvalanchePhotodiode {
pub fn new(
pin: PinPhotodiode,
gain_m: f64,
excess_noise_factor_x: f64,
gain_bandwidth_product_ghz: f64,
) -> Result<Self, OxiPhotonError> {
if gain_m < 1.0 {
return Err(OxiPhotonError::NumericalError(
"APD gain must be >= 1".into(),
));
}
if excess_noise_factor_x <= 0.0 || excess_noise_factor_x > 2.0 {
return Err(OxiPhotonError::NumericalError(
"excess_noise_factor_x must be in (0, 2]".into(),
));
}
Ok(Self {
pin,
gain_m,
excess_noise_factor_x,
gain_bandwidth_product_ghz,
})
}
pub fn ingaas_apd() -> Self {
Self {
pin: PinPhotodiode::ingaas_1550(),
gain_m: 10.0,
excess_noise_factor_x: 0.7,
gain_bandwidth_product_ghz: 150.0,
}
}
pub fn effective_responsivity(&self) -> f64 {
self.pin.responsivity_a_per_w * self.gain_m
}
pub fn excess_noise_factor(&self) -> f64 {
self.gain_m.powf(self.excess_noise_factor_x)
}
pub fn effective_bandwidth_ghz(&self) -> f64 {
self.gain_bandwidth_product_ghz / self.gain_m
}
pub fn apd_noise_a_per_sqrt_hz(&self, power_w: f64) -> f64 {
let i_primary = self.pin.photocurrent_a(power_w); let i_dark = self.pin.dark_current_na * 1e-9;
let f_m = self.excess_noise_factor();
let m2 = self.gain_m * self.gain_m;
let noise_sq = 2.0 * E_CHARGE * (i_primary + i_dark) * f_m * m2;
noise_sq.sqrt()
}
pub fn optimum_gain(&self, signal_power_w: f64, temp_k: f64, r_load: f64) -> f64 {
let i_th_sq = 4.0 * KB * temp_k / r_load;
let r = self.pin.responsivity_a_per_w;
let i_sig = r * signal_power_w;
let x = self.excess_noise_factor_x;
let mut best_m = 1.0_f64;
let mut best_snr = f64::NEG_INFINITY;
let steps = 500usize;
for k in 1..=steps {
let m = 1.0 + (k as f64 / steps as f64) * 999.0;
let f_m = m.powf(x);
let i_dark = self.pin.dark_current_na * 1e-9;
let i_shot_sq = 2.0 * E_CHARGE * (i_sig + i_dark) * f_m * m * m;
let i_signal_sq = (r * signal_power_w * m).powi(2);
let snr = i_signal_sq / (i_shot_sq + i_th_sq);
if snr > best_snr {
best_snr = snr;
best_m = m;
}
}
best_m
}
pub fn snr_db(&self, power_w: f64, bw_ghz: f64, temp_k: f64, r_load: f64) -> f64 {
let i_sig = self.effective_responsivity() * power_w;
let i_shot_density = self.apd_noise_a_per_sqrt_hz(power_w);
let i_th_density = self.pin.thermal_noise_a_per_sqrt_hz(temp_k, r_load);
let bw_hz = bw_ghz * 1e9;
let i_noise =
((i_shot_density * i_shot_density + i_th_density * i_th_density) * bw_hz).sqrt();
if i_noise == 0.0 {
return f64::INFINITY;
}
20.0 * (i_sig / i_noise).log10()
}
pub fn nep_w_per_sqrt_hz(&self, temp_k: f64, r_load: f64) -> f64 {
let i_noise = {
let i_shot = self.apd_noise_a_per_sqrt_hz(0.0);
let i_th = self.pin.thermal_noise_a_per_sqrt_hz(temp_k, r_load);
(i_shot * i_shot + i_th * i_th).sqrt()
};
i_noise / self.effective_responsivity()
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn ingaas() -> PinPhotodiode {
PinPhotodiode::ingaas_1550()
}
#[test]
fn test_ingaas_qe_reasonable() {
let det = ingaas();
let qe = det.quantum_efficiency();
assert!(qe > 0.60 && qe < 0.95, "QE = {qe:.3} outside [0.60, 0.95]");
}
#[test]
fn test_photocurrent_linear() {
let det = ingaas();
let p = 1e-3; let i = det.photocurrent_a(p);
assert_relative_eq!(i, det.responsivity_a_per_w * p, epsilon = 1e-15);
let i2 = det.photocurrent_a(2.0 * p);
assert_relative_eq!(i2, 2.0 * i, epsilon = 1e-15);
}
#[test]
fn test_shot_noise_scaling() {
let det = ingaas();
let i1 = det.shot_noise_a_per_sqrt_hz(1e-3);
let i4 = det.shot_noise_a_per_sqrt_hz(4e-3);
let ratio = i4 / i1;
assert!(
(ratio - 2.0).abs() < 0.05,
"ratio = {ratio:.4}, expected ≈ 2.0"
);
}
#[test]
fn test_nep_positive() {
let det = ingaas();
let nep = det.nep_w_per_sqrt_hz(300.0, 50.0);
assert!(nep > 0.0, "NEP must be positive, got {nep}");
assert!(nep.is_finite(), "NEP must be finite");
}
#[test]
fn test_rise_time_formula() {
let det = ingaas(); let tr = det.rise_time_ps();
assert_relative_eq!(tr, 35.0, epsilon = 1e-6);
}
#[test]
fn test_apd_gain_multiplies_current() {
let apd = AvalanchePhotodiode::ingaas_apd();
let r_eff = apd.effective_responsivity();
assert_relative_eq!(
r_eff,
apd.pin.responsivity_a_per_w * apd.gain_m,
epsilon = 1e-12
);
}
#[test]
fn test_apd_excess_noise() {
let apd = AvalanchePhotodiode::ingaas_apd();
let f_m = apd.excess_noise_factor();
assert!(f_m > 1.0, "F(M) = {f_m:.3} should exceed 1");
}
#[test]
fn test_d_star_positive() {
let det = ingaas();
let d_star = det.d_star_cm_sqrt_hz_per_w(300.0, 50.0);
assert!(d_star > 0.0, "D* must be positive");
assert!(d_star.is_finite(), "D* must be finite");
}
}