const Q_E: f64 = 1.602_176_634e-19; const K_B: f64 = 1.380_649e-23;
#[derive(Debug, Clone)]
pub struct AvalanchePhotodetector {
pub responsivity: f64,
pub ionization_ratio: f64,
pub gain: f64,
pub bandwidth_hz: f64,
pub dark_current_na: f64,
pub capacitance_ff: f64,
pub load_resistance: f64,
}
impl AvalanchePhotodetector {
pub fn ingaas_1550() -> Self {
Self {
responsivity: 0.85, ionization_ratio: 0.45, gain: 10.0, bandwidth_hz: 10e9, dark_current_na: 10.0, capacitance_ff: 150.0, load_resistance: 50.0, }
}
pub fn si_800() -> Self {
Self {
responsivity: 0.5, ionization_ratio: 0.02, gain: 100.0, bandwidth_hz: 50e9, dark_current_na: 0.5, capacitance_ff: 80.0,
load_resistance: 50.0,
}
}
pub fn effective_responsivity(&self) -> f64 {
self.gain * self.responsivity
}
pub fn excess_noise_factor(&self) -> f64 {
let k = self.ionization_ratio;
let m = self.gain;
if m <= 0.0 {
return 1.0;
}
k * m + (1.0 - k) * (2.0 - 1.0 / m)
}
pub fn snr_db(&self, power_w: f64, bandwidth_hz: f64) -> f64 {
let t = 300.0; let i_signal = self.effective_responsivity() * power_w;
let i_ph = self.responsivity * power_w;
let f_excess = self.excess_noise_factor();
let i_shot_sq = 2.0 * Q_E * self.gain * self.gain * f_excess * i_ph * bandwidth_hz;
let i_dark_a = self.dark_current_na * 1e-9;
let i_dark_sq = 2.0 * Q_E * self.gain * self.gain * f_excess * i_dark_a * bandwidth_hz;
let i_thermal_sq = 4.0 * K_B * t * bandwidth_hz / self.load_resistance;
let noise_total = i_shot_sq + i_dark_sq + i_thermal_sq;
if noise_total <= 0.0 {
return f64::INFINITY;
}
10.0 * (i_signal * i_signal / noise_total).log10()
}
pub fn nep_w_per_sqrthz(&self, bandwidth_hz: f64) -> f64 {
let t = 300.0;
let f_excess = self.excess_noise_factor();
let i_dark_a = self.dark_current_na * 1e-9;
let shot_density = (2.0 * Q_E * self.gain * self.gain * f_excess * i_dark_a).sqrt();
let thermal_density = (4.0 * K_B * t / self.load_resistance).sqrt();
let total_noise_density = (shot_density * shot_density
+ thermal_density * thermal_density
+ 2.0
* Q_E
* self.gain
* self.gain
* f_excess
* self.responsivity
* bandwidth_hz
* 1e-3)
.sqrt();
let noise_density =
(shot_density * shot_density + thermal_density * thermal_density).sqrt();
let r_eff = self.effective_responsivity();
if r_eff <= 0.0 {
return f64::INFINITY;
}
let _ = total_noise_density; noise_density / r_eff
}
pub fn shot_noise_density(&self, current_a: f64) -> f64 {
let f_excess = self.excess_noise_factor();
(2.0 * Q_E * current_a * self.gain * self.gain * f_excess).sqrt()
}
pub fn thermal_noise_density(&self, temperature_k: f64) -> f64 {
(4.0 * K_B * temperature_k / self.load_resistance).sqrt()
}
pub fn optimal_gain(&self, optical_power_w: f64, temperature_k: f64) -> f64 {
let i_thermal_sq = 4.0 * K_B * temperature_k / self.load_resistance;
let i_ph = self.responsivity * optical_power_w;
let i_dark_a = self.dark_current_na * 1e-9;
let k = self.ionization_ratio;
let base_noise_current = 2.0 * Q_E * (2.0 - k) * (i_ph + i_dark_a);
if base_noise_current <= 0.0 {
return 1.0;
}
let m_opt = (i_thermal_sq / base_noise_current).powf(1.0 / 3.0);
m_opt.clamp(1.0, 1000.0)
}
pub fn bandwidth_at_gain(&self, gain: f64) -> f64 {
if gain <= 0.0 {
return self.bandwidth_hz;
}
self.bandwidth_hz / gain
}
pub fn photocurrent(&self, power_w: f64) -> f64 {
self.effective_responsivity() * power_w
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn ingaas_responsivity_reasonable() {
let apd = AvalanchePhotodetector::ingaas_1550();
assert_relative_eq!(apd.effective_responsivity(), 8.5, epsilon = 0.1);
}
#[test]
fn si_apd_high_gain() {
let apd = AvalanchePhotodetector::si_800();
assert!(apd.gain > 50.0);
}
#[test]
fn excess_noise_factor_bounded_below() {
let apd = AvalanchePhotodetector::ingaas_1550();
let f = apd.excess_noise_factor();
assert!(f >= 1.0, "F = {f}");
}
#[test]
fn excess_noise_factor_k0_approaches_two() {
let mut apd = AvalanchePhotodetector::si_800();
apd.gain = 100.0;
apd.ionization_ratio = 0.0;
let f = apd.excess_noise_factor();
assert!((f - 2.0).abs() < 0.02, "F = {f}");
}
#[test]
fn excess_noise_factor_k1_equals_m() {
let mut apd = AvalanchePhotodetector::ingaas_1550();
apd.ionization_ratio = 1.0;
apd.gain = 10.0;
assert_relative_eq!(apd.excess_noise_factor(), apd.gain, epsilon = 1e-10);
}
#[test]
fn snr_increases_with_power() {
let apd = AvalanchePhotodetector::ingaas_1550();
let snr_low = apd.snr_db(1e-6, 1e9); let snr_high = apd.snr_db(1e-3, 1e9); assert!(
snr_high > snr_low,
"SNR(low)={snr_low:.1}, SNR(high)={snr_high:.1}"
);
}
#[test]
fn nep_positive() {
let apd = AvalanchePhotodetector::ingaas_1550();
let nep = apd.nep_w_per_sqrthz(1e9);
assert!(nep > 0.0 && nep.is_finite(), "NEP = {nep}");
}
#[test]
fn shot_noise_scales_sqrt_current() {
let apd = AvalanchePhotodetector::ingaas_1550();
let s1 = apd.shot_noise_density(1e-3);
let s4 = apd.shot_noise_density(4e-3);
assert_relative_eq!(s4 / s1, 2.0, epsilon = 1e-10);
}
#[test]
fn thermal_noise_decreases_with_lower_temperature() {
let apd = AvalanchePhotodetector::ingaas_1550();
let th_room = apd.thermal_noise_density(300.0);
let th_cold = apd.thermal_noise_density(77.0);
assert!(th_cold < th_room);
}
#[test]
fn optimal_gain_in_reasonable_range() {
let apd = AvalanchePhotodetector::ingaas_1550();
let m_opt = apd.optimal_gain(1e-6, 300.0);
assert!((1.0..=1000.0).contains(&m_opt), "M_opt = {m_opt}");
}
#[test]
fn bandwidth_decreases_with_gain() {
let apd = AvalanchePhotodetector::ingaas_1550();
let bw10 = apd.bandwidth_at_gain(10.0);
let bw100 = apd.bandwidth_at_gain(100.0);
assert!(bw10 > bw100, "BW(10)={bw10:.1e}, BW(100)={bw100:.1e}");
}
#[test]
fn photocurrent_proportional_to_power() {
let apd = AvalanchePhotodetector::ingaas_1550();
let i1 = apd.photocurrent(1e-3);
let i2 = apd.photocurrent(2e-3);
assert_relative_eq!(i2, 2.0 * i1, epsilon = 1e-10);
}
}