#[cfg(test)]
use crate::error::OxiPhotonError;
const C0: f64 = 2.997_924_58e8; const EPS0: f64 = 8.854_187_817e-12; #[allow(dead_code)]
const H_PLANCK: f64 = 6.626_070_15e-34; const E_CHARGE: f64 = 1.602_176_634e-19;
#[derive(Debug, Clone)]
pub struct OpticalRectification {
pub crystal_name: String,
pub d_eff_pm_per_v: f64,
pub crystal_length_mm: f64,
pub pump_wavelength_nm: f64,
pub pump_pulse_duration_fs: f64,
pub pump_energy_uj: f64,
pub refractive_index_optical: f64,
pub refractive_index_thz: f64,
}
impl OpticalRectification {
#[allow(clippy::too_many_arguments)]
pub fn new(
crystal_name: impl Into<String>,
d_eff_pm_per_v: f64,
crystal_length_mm: f64,
pump_wavelength_nm: f64,
pump_pulse_duration_fs: f64,
pump_energy_uj: f64,
refractive_index_optical: f64,
refractive_index_thz: f64,
) -> Self {
Self {
crystal_name: crystal_name.into(),
d_eff_pm_per_v,
crystal_length_mm,
pump_wavelength_nm,
pump_pulse_duration_fs,
pump_energy_uj,
refractive_index_optical,
refractive_index_thz,
}
}
pub fn znte() -> Self {
Self::new(
"ZnTe", 68.0, 2.0, 800.0, 100.0, 1.0, 2.85, 3.17,
)
}
pub fn linbo3_thz() -> Self {
Self::new(
"LiNbO3", 168.0, 5.0, 1030.0, 300.0, 100.0, 2.26, 4.96,
)
}
pub fn phase_mismatch(&self, freq_thz: f64) -> f64 {
let omega_thz = 2.0 * std::f64::consts::PI * freq_thz * 1e12; omega_thz / C0 * (self.refractive_index_optical - self.refractive_index_thz).abs()
}
pub fn coherence_length_mm(&self, freq_thz: f64) -> f64 {
let delta_k = self.phase_mismatch(freq_thz);
if delta_k.abs() < 1e-30 {
return f64::MAX;
}
let lc_m = std::f64::consts::PI / delta_k;
lc_m * 1e3 }
fn sinc_factor(&self, freq_thz: f64) -> f64 {
let delta_k = self.phase_mismatch(freq_thz);
let l_m = self.crystal_length_mm * 1e-3;
let arg = delta_k * l_m / 2.0;
if arg.abs() < 1e-15 {
1.0
} else {
(arg.sin() / arg).powi(2)
}
}
pub fn thz_field_amplitude_v_per_m(&self, freq_thz: f64) -> f64 {
let d_eff_si = self.d_eff_pm_per_v * 1e-12; let l_m = self.crystal_length_mm * 1e-3;
let tau_s = self.pump_pulse_duration_fs * 1e-15;
let area_m2 = 1e-6; let energy_j = self.pump_energy_uj * 1e-6;
let intensity = energy_j / (tau_s * area_m2);
let e_pump = (2.0 * intensity / (C0 * EPS0 * self.refractive_index_optical)).sqrt();
let omega_thz = 2.0 * std::f64::consts::PI * freq_thz * 1e12;
let e_thz = omega_thz * d_eff_si * l_m * e_pump.powi(2) / (self.refractive_index_thz * C0);
e_thz * self.sinc_factor(freq_thz).sqrt()
}
pub fn thz_pulse_energy_nj(&self) -> f64 {
let eta = self.conversion_efficiency();
self.pump_energy_uj * eta * 1e3 }
pub fn conversion_efficiency(&self) -> f64 {
let freq_thz = 1.0; let d_eff_si = self.d_eff_pm_per_v * 1e-12;
let l_m = self.crystal_length_mm * 1e-3;
let omega_thz = 2.0 * std::f64::consts::PI * freq_thz * 1e12;
let tau_s = self.pump_pulse_duration_fs * 1e-15;
let energy_j = self.pump_energy_uj * 1e-6;
let area_m2 = 1e-6;
let intensity = energy_j / (tau_s * area_m2);
let numerator = (d_eff_si * omega_thz * l_m).powi(2) * intensity;
let denominator =
self.refractive_index_thz.powi(2) * self.refractive_index_optical * C0.powi(3) * EPS0;
let eta_raw = numerator / denominator;
eta_raw * self.sinc_factor(freq_thz)
}
pub fn optimal_length_mm(&self, freq_thz: f64) -> f64 {
self.coherence_length_mm(freq_thz)
}
pub fn thz_bandwidth_thz(&self) -> f64 {
let tau_ps = self.pump_pulse_duration_fs * 1e-3; 0.44 / tau_ps
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PcaSubstrate {
LowTemperatureGaAs,
ErAsGaAs,
InGaAs,
Si,
}
impl PcaSubstrate {
pub fn default_carrier_lifetime_ps(self) -> f64 {
match self {
Self::LowTemperatureGaAs => 0.3,
Self::ErAsGaAs => 0.15,
Self::InGaAs => 1.0,
Self::Si => 10.0,
}
}
pub fn default_mobility_cm2_per_vs(self) -> f64 {
match self {
Self::LowTemperatureGaAs => 200.0,
Self::ErAsGaAs => 100.0,
Self::InGaAs => 3000.0,
Self::Si => 1400.0,
}
}
pub fn refractive_index_thz(self) -> f64 {
match self {
Self::LowTemperatureGaAs | Self::ErAsGaAs => 3.56,
Self::InGaAs => 3.60,
Self::Si => 3.42,
}
}
}
#[derive(Debug, Clone)]
pub struct PhotoconductiveAntenna {
pub substrate: PcaSubstrate,
pub gap_um: f64,
pub antenna_length_um: f64,
pub bias_voltage_v: f64,
pub carrier_lifetime_ps: f64,
pub mobility_cm2_per_vs: f64,
}
impl PhotoconductiveAntenna {
#[allow(clippy::too_many_arguments)]
pub fn new(
substrate: PcaSubstrate,
gap_um: f64,
antenna_length_um: f64,
bias_voltage_v: f64,
carrier_lifetime_ps: f64,
mobility_cm2_per_vs: f64,
) -> Self {
Self {
substrate,
gap_um,
antenna_length_um,
bias_voltage_v,
carrier_lifetime_ps,
mobility_cm2_per_vs,
}
}
pub fn lt_gaas_standard() -> Self {
let sub = PcaSubstrate::LowTemperatureGaAs;
Self::new(
sub,
5.0, 30.0, 30.0, sub.default_carrier_lifetime_ps(),
sub.default_mobility_cm2_per_vs(),
)
}
pub fn radiation_impedance_ohm(&self) -> f64 {
73.0 }
pub fn photocurrent_density_a_per_m2(&self, carrier_density: f64) -> f64 {
let mu_si = self.mobility_cm2_per_vs * 1e-4; let e_bias = self.bias_voltage_v / (self.gap_um * 1e-6); E_CHARGE * mu_si * carrier_density * e_bias
}
pub fn thz_field_waveform(&self, optical_pulse: &[f64], dt_ps: f64) -> Vec<f64> {
let n = optical_pulse.len();
if n == 0 {
return Vec::new();
}
let e_bias = self.bias_voltage_v / (self.gap_um * 1e-6);
let mu_si = self.mobility_cm2_per_vs * 1e-4; let tau_ps = self.carrier_lifetime_ps;
let dt = dt_ps;
let mut j_current = vec![0.0_f64; n];
for (i, j_val) in j_current.iter_mut().enumerate().take(n) {
let mut acc = 0.0_f64;
for (k, &pulse_k) in optical_pulse.iter().enumerate().take(i + 1) {
let t_delay = (i - k) as f64 * dt;
let decay = (-t_delay / tau_ps).exp();
acc += pulse_k * decay * dt;
}
*j_val = E_CHARGE * mu_si * e_bias * acc;
}
let mut e_thz = vec![0.0_f64; n];
for i in 0..n {
let dj = if i == 0 {
j_current[1] - j_current[0]
} else if i == n - 1 {
j_current[n - 1] - j_current[n - 2]
} else {
(j_current[i + 1] - j_current[i - 1]) / 2.0
};
e_thz[i] = dj / (dt * 1e-12); }
e_thz
}
pub fn radiated_power_mw(&self, optical_power_mw: f64) -> f64 {
let responsivity_a_per_w = 0.01; let i_photo = responsivity_a_per_w * optical_power_mw * 1e-3; let z_rad = self.radiation_impedance_ohm();
let power_w = 0.5 * i_photo.powi(2) * z_rad;
power_w * 1e3 }
pub fn resonance_frequency_thz(&self, n_substrate: f64) -> f64 {
let l_m = self.antenna_length_um * 1e-6;
C0 / (2.0 * l_m * n_substrate) * 1e-12 }
pub fn bandwidth_thz(&self) -> f64 {
let tau_s = self.carrier_lifetime_ps * 1e-12;
1.0 / (std::f64::consts::PI * tau_s) * 1e-12 }
}
#[derive(Debug, Clone)]
pub struct FelThzSource {
pub frequency_thz: f64,
pub power_w: f64,
pub bandwidth_mhz: f64,
pub pulse_duration_ps: f64,
}
impl FelThzSource {
pub fn new(
frequency_thz: f64,
power_w: f64,
bandwidth_mhz: f64,
pulse_duration_ps: f64,
) -> Self {
Self {
frequency_thz,
power_w,
bandwidth_mhz,
pulse_duration_ps,
}
}
pub fn wavelength_mm(&self) -> f64 {
C0 / (self.frequency_thz * 1e12) * 1e3 }
pub fn coherence_length_mm(&self) -> f64 {
let delta_nu_hz = self.bandwidth_mhz * 1e6;
C0 / delta_nu_hz * 1e3 }
pub fn is_coherent(&self) -> bool {
let coherence_time_ps = self.coherence_length_mm() / (C0 * 1e3) * 1e12;
coherence_time_ps > self.pulse_duration_ps
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_or_coherence_length_at_phase_match() {
let crystal =
OpticalRectification::new("PerfectMatch", 68.0, 2.0, 800.0, 100.0, 1.0, 3.17, 3.17);
let lc = crystal.coherence_length_mm(1.0);
assert!(
lc > 1e10,
"coherence length should diverge when Δk = 0, got {lc}"
);
}
#[test]
fn test_or_bandwidth() {
let crystal = OpticalRectification::znte();
let bw = crystal.thz_bandwidth_thz();
let expected = 0.44 / (100e-3_f64); assert!(
(bw - expected).abs() < 1e-6,
"bandwidth = {bw}, expected {expected}"
);
}
#[test]
fn test_or_conversion_efficiency_small() {
let crystal = OpticalRectification::znte();
let eta = crystal.conversion_efficiency();
assert!(eta < 0.01, "efficiency should be <1%, got {eta}");
assert!(eta > 0.0, "efficiency must be positive, got {eta}");
}
#[test]
fn test_pca_bandwidth() {
let pca = PhotoconductiveAntenna::lt_gaas_standard();
let bw = pca.bandwidth_thz();
let expected = 1.0 / (std::f64::consts::PI * 0.3e-12) * 1e-12;
assert!(
(bw - expected).abs() / expected < 1e-9,
"PCA bandwidth = {bw} THz, expected ≈{expected} THz"
);
}
#[test]
fn test_pca_resonance_frequency_positive() {
let pca = PhotoconductiveAntenna::lt_gaas_standard();
let n_sub = PcaSubstrate::LowTemperatureGaAs.refractive_index_thz();
let f_res = pca.resonance_frequency_thz(n_sub);
assert!(
f_res > 0.0,
"resonance frequency must be positive, got {f_res}"
);
}
#[test]
fn test_pca_waveform_length() {
let pca = PhotoconductiveAntenna::lt_gaas_standard();
let pulse: Vec<f64> = (0..256)
.map(|i| {
let t = (i as f64 - 128.0) * 0.05;
(-t * t).exp()
})
.collect();
let waveform = pca.thz_field_waveform(&pulse, 0.05);
assert_eq!(
waveform.len(),
pulse.len(),
"waveform length must match input"
);
}
#[test]
fn test_fel_wavelength() {
let fel = FelThzSource::new(1.0, 100.0, 10.0, 10.0);
let lambda_mm = fel.wavelength_mm();
let expected_mm = C0 / 1e12 * 1e3;
assert!(
(lambda_mm - expected_mm).abs() < 1e-6,
"λ = {lambda_mm} mm, expected {expected_mm} mm"
);
}
fn _assert_error_type(_: OxiPhotonError) {}
}