use num_complex::Complex64;
#[cfg(test)]
use crate::error::OxiPhotonError;
const EPS0: f64 = 8.854_187_817e-12; const C0: f64 = 2.997_924_58e8; const MU0: f64 = 1.256_637_061e-6;
#[derive(Debug, Clone)]
pub struct ThzMaterial {
pub name: String,
pub n_real: f64,
pub alpha_cm: f64,
}
impl ThzMaterial {
pub fn new(name: impl Into<String>, n_real: f64, alpha_cm: f64) -> Self {
Self {
name: name.into(),
n_real,
alpha_cm,
}
}
pub fn silicon() -> Self {
Self::new("Silicon (HR)", 3.42, 0.02)
}
pub fn gaas() -> Self {
Self::new("GaAs (SI)", 3.56, 0.5)
}
pub fn water() -> Self {
Self::new("Water (liquid)", 2.1, 200.0)
}
pub fn ptfe() -> Self {
Self::new("PTFE (Teflon)", 1.43, 0.2)
}
pub fn polyethylene() -> Self {
Self::new("HDPE", 1.54, 0.2)
}
pub fn quartz_fused() -> Self {
Self::new("Fused Silica", 1.95, 0.1)
}
pub fn znte() -> Self {
Self::new("ZnTe", 2.85, 1.2)
}
pub fn refractive_index(&self) -> Complex64 {
let freq_thz = 1.0;
let omega = 2.0 * std::f64::consts::PI * freq_thz * 1e12;
let k = self.alpha_cm * 1e2 * C0 / (2.0 * omega); Complex64::new(self.n_real, k)
}
pub fn absorption_coefficient_cm(&self) -> f64 {
self.alpha_cm
}
pub fn transmission(length_mm: f64, alpha_cm: f64) -> f64 {
let l_cm = length_mm / 10.0;
(-alpha_cm * l_cm).exp()
}
}
#[derive(Debug, Clone)]
pub struct DrudeTHz {
pub plasma_freq_thz: f64,
pub scattering_rate_thz: f64,
pub eps_inf: f64,
}
impl DrudeTHz {
pub fn new(plasma_freq_thz: f64, scattering_rate_thz: f64, eps_inf: f64) -> Self {
Self {
plasma_freq_thz,
scattering_rate_thz,
eps_inf,
}
}
pub fn gold_thz() -> Self {
Self::new(4300.0, 6.5, 1.0)
}
pub fn copper_thz() -> Self {
Self::new(3700.0, 8.3, 1.0)
}
pub fn doped_silicon(n_carriers: f64) -> Self {
let m_e = 9.109_383_7e-31; let m_star = 0.26 * m_e;
let e = 1.602_176_634e-19; let eps_r_si = 11.7;
let omega_p_sq = n_carriers * e * e / (EPS0 * eps_r_si * m_star);
let omega_p_thz = omega_p_sq.sqrt() / (2.0 * std::f64::consts::PI * 1e12);
let mu_si = 1400.0 * 1e-4; let gamma_rad_per_s = e / (m_star * mu_si);
let gamma_thz = gamma_rad_per_s / (2.0 * std::f64::consts::PI * 1e12);
Self::new(omega_p_thz, gamma_thz, eps_r_si)
}
pub fn permittivity(&self, freq_thz: f64) -> Complex64 {
let omega = 2.0 * std::f64::consts::PI * freq_thz * 1e12;
let omega_p = 2.0 * std::f64::consts::PI * self.plasma_freq_thz * 1e12;
let gamma = 2.0 * std::f64::consts::PI * self.scattering_rate_thz * 1e12;
let denom = Complex64::new(omega * omega - 0.0, gamma * omega); let drude_term = Complex64::new(omega_p * omega_p, 0.0) / denom;
Complex64::new(self.eps_inf, 0.0) - drude_term
}
pub fn refractive_index(&self, freq_thz: f64) -> Complex64 {
let eps = self.permittivity(freq_thz);
complex_sqrt(eps)
}
pub fn skin_depth_um(&self, freq_thz: f64) -> f64 {
let n = self.refractive_index(freq_thz);
let omega = 2.0 * std::f64::consts::PI * freq_thz * 1e12;
let kappa = n.im.abs(); if kappa < 1e-30 {
return f64::INFINITY;
}
let delta_m = C0 / (omega * kappa); delta_m * 1e6 }
pub fn surface_resistance_ohm(&self, freq_thz: f64) -> f64 {
let eps = self.permittivity(freq_thz);
let omega = 2.0 * std::f64::consts::PI * freq_thz * 1e12;
let freq_hz = freq_thz * 1e12;
let sigma = EPS0 * omega * eps.im.abs();
if sigma < 1e-20 {
return f64::INFINITY;
}
(std::f64::consts::PI * freq_hz * MU0 / sigma).sqrt()
}
}
#[derive(Debug, Clone)]
pub struct AtmosphericAbsorption {
pub humidity_percent: f64,
pub temperature_c: f64,
pub pressure_atm: f64,
}
const H2O_LINES: &[(f64, f64)] = &[
(0.557, 1.000), (0.752, 0.080),
(0.988, 0.040),
(1.097, 0.180),
(1.113, 0.040),
(1.163, 0.070),
(1.207, 0.130),
(1.229, 0.025),
(1.411, 0.020),
(1.602, 0.010),
(1.661, 0.015),
(1.717, 0.025),
(1.763, 0.010),
(1.797, 0.008),
(1.919, 0.012),
(2.074, 0.018),
(2.164, 0.008),
(2.221, 0.015),
(2.264, 0.010),
(2.344, 0.025),
(2.640, 0.035),
(2.774, 0.020),
(2.968, 0.018),
];
const ALPHA_PEAK_STD: f64 = 0.35;
const LINEWIDTH_THZ: f64 = 0.005;
impl AtmosphericAbsorption {
pub fn new(humidity_percent: f64, temperature_c: f64, pressure_atm: f64) -> Self {
Self {
humidity_percent,
temperature_c,
pressure_atm,
}
}
pub fn standard_conditions() -> Self {
Self::new(50.0, 20.0, 1.0)
}
fn saturation_pressure_hpa(&self) -> f64 {
let t = self.temperature_c;
6.112 * ((17.67 * t) / (t + 243.5)).exp()
}
fn water_vapour_pressure_hpa(&self) -> f64 {
self.saturation_pressure_hpa() * self.humidity_percent / 100.0
}
fn relative_humidity_factor(&self) -> f64 {
let p_w = self.water_vapour_pressure_hpa();
let p_std = 6.112_f64 * ((17.67_f64 * 20.0_f64) / (20.0_f64 + 243.5_f64)).exp() * 0.5;
let pressure_factor = self.pressure_atm; (p_w / p_std) * pressure_factor
}
fn linewidth_at_pressure(&self) -> f64 {
LINEWIDTH_THZ * self.pressure_atm
}
pub fn absorption_coefficient_cm(&self, freq_thz: f64) -> f64 {
let rh_factor = self.relative_humidity_factor();
let gamma = self.linewidth_at_pressure();
let mut alpha = 0.0_f64;
for &(f0, strength) in H2O_LINES {
let df = freq_thz - f0;
let lorentz = (gamma / std::f64::consts::PI) / (df * df + gamma * gamma);
let peak = ALPHA_PEAK_STD * strength * rh_factor;
alpha += peak * std::f64::consts::PI * gamma * lorentz;
}
alpha
}
pub fn transmission(&self, freq_thz: f64, path_length_m: f64) -> f64 {
let alpha = self.absorption_coefficient_cm(freq_thz);
let l_cm = path_length_m * 100.0;
(-alpha * l_cm).exp()
}
pub fn transmission_spectrum(&self, f_min: f64, f_max: f64, n_pts: usize) -> Vec<(f64, f64)> {
if n_pts == 0 {
return Vec::new();
}
let path_m = 1.0; let df = if n_pts > 1 {
(f_max - f_min) / (n_pts - 1) as f64
} else {
0.0
};
(0..n_pts)
.map(|i| {
let f = f_min + i as f64 * df;
let t = self.transmission(f, path_m);
(f, t)
})
.collect()
}
pub fn transmission_windows(&self) -> Vec<(f64, f64)> {
let threshold = 0.5; let spectrum = self.transmission_spectrum(0.05, 3.0, 600);
let mut windows: Vec<(f64, f64)> = Vec::new();
let mut in_window = false;
let mut win_start = 0.0_f64;
for &(f, t) in &spectrum {
if t >= threshold && !in_window {
in_window = true;
win_start = f;
} else if t < threshold && in_window {
in_window = false;
windows.push((win_start, f));
}
}
if in_window {
if let Some(&(f_last, _)) = spectrum.last() {
windows.push((win_start, f_last));
}
}
windows
}
pub fn max_range_m(&self, freq_thz: f64, max_loss_db: f64) -> f64 {
let alpha_cm = self.absorption_coefficient_cm(freq_thz);
if alpha_cm < 1e-30 {
return f64::INFINITY;
}
let l_cm = max_loss_db / (alpha_cm * 4.342_944_8);
l_cm / 100.0 }
}
fn complex_sqrt(z: Complex64) -> Complex64 {
let r = z.norm().sqrt();
let theta = z.arg() / 2.0;
Complex64::new(r * theta.cos(), r * theta.sin())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_silicon_low_absorption() {
let si = ThzMaterial::silicon();
assert!(
si.alpha_cm < 0.1,
"Si absorption should be <0.1 cm⁻¹, got {}",
si.alpha_cm
);
}
#[test]
fn test_water_high_absorption() {
let h2o = ThzMaterial::water();
assert!(
h2o.alpha_cm > 100.0,
"Water absorption should be >100 cm⁻¹, got {}",
h2o.alpha_cm
);
}
#[test]
fn test_drude_gold_negative_real_eps() {
let gold = DrudeTHz::gold_thz();
let eps = gold.permittivity(1.0); assert!(
eps.re < 0.0,
"Gold ε' should be negative at THz, got Re(ε)={}",
eps.re
);
}
#[test]
fn test_skin_depth_positive() {
let gold = DrudeTHz::gold_thz();
let delta = gold.skin_depth_um(1.0);
assert!(delta > 0.0, "Skin depth must be positive, got {delta}");
}
#[test]
fn test_atmospheric_transmission_decreases_with_distance() {
let atm = AtmosphericAbsorption::standard_conditions();
let t1 = atm.transmission(0.557, 1.0);
let t2 = atm.transmission(0.557, 10.0);
assert!(
t2 < t1,
"Transmission should decrease with distance: T(1m)={t1}, T(10m)={t2}"
);
}
#[test]
fn test_transmission_windows_exist() {
let atm = AtmosphericAbsorption::standard_conditions();
let windows = atm.transmission_windows();
assert!(
!windows.is_empty(),
"At least one THz transmission window must exist"
);
}
#[test]
fn test_surface_resistance_positive() {
let gold = DrudeTHz::gold_thz();
let rs = gold.surface_resistance_ohm(1.0);
assert!(rs > 0.0, "Surface resistance must be positive, got {rs}");
}
fn _assert_error_import(_: OxiPhotonError) {}
}