use std::f64::consts::PI;
const C_LIGHT: f64 = 2.997_924_58e8; const EPS0: f64 = 8.854_187_817e-12; const HBAR: f64 = 1.054_571_817e-34; const ETA0: f64 = 376.730_313_668;
#[derive(Debug, Clone)]
pub struct HertzianDipole {
pub wavelength_m: f64,
pub dipole_moment_cm: f64,
pub n_medium: f64,
}
impl HertzianDipole {
pub fn omega(&self) -> f64 {
2.0 * PI * C_LIGHT / self.wavelength_m
}
pub fn radiated_power_w(&self, omega: f64) -> f64 {
let p = self.dipole_moment_cm;
let n = self.n_medium;
p * p * omega.powi(4) * n / (12.0 * PI * EPS0 * C_LIGHT.powi(3))
}
pub fn directivity(&self) -> f64 {
1.5
}
pub fn radiation_resistance_ohm(&self, length_m: f64) -> f64 {
let lambda_eff = self.wavelength_m / self.n_medium;
let eta = ETA0 / self.n_medium;
(2.0 * PI / 3.0) * eta * (length_m / lambda_eff).powi(2)
}
pub fn near_field_intensity(&self, r_m: f64) -> f64 {
if r_m < 1.0e-30 {
return f64::MAX;
}
let p = self.dipole_moment_cm;
p * p / r_m.powi(6)
}
pub fn far_field_pattern(&self, theta_rad: f64) -> f64 {
theta_rad.sin().powi(2)
}
pub fn spontaneous_emission_rate(&self) -> f64 {
let omega = self.omega();
let p = self.dipole_moment_cm;
let n = self.n_medium;
p * p * omega.powi(3) * n / (3.0 * PI * EPS0 * HBAR * C_LIGHT.powi(3))
}
}
#[derive(Debug, Clone)]
pub struct NanorodAntenna {
pub length_m: f64,
pub radius_m: f64,
pub eps_metal: (f64, f64),
pub wavelength_m: f64,
pub n_medium: f64,
}
impl NanorodAntenna {
pub fn effective_wavelength_m(&self) -> f64 {
let (eps_re, eps_im) = self.eps_metal;
let n2 = self.n_medium * self.n_medium;
let denom_re = eps_re + n2;
let denom_im = eps_im;
let denom_sq = denom_re * denom_re + denom_im * denom_im;
if denom_sq < f64::MIN_POSITIVE {
return self.wavelength_m / self.n_medium;
}
let num_re = eps_re * n2;
let num_im = eps_im * n2;
let ratio_re = (num_re * denom_re + num_im * denom_im) / denom_sq;
let ratio_im = (num_im * denom_re - num_re * denom_im) / denom_sq;
let r_mag = (ratio_re * ratio_re + ratio_im * ratio_im).sqrt();
let theta = ratio_im.atan2(ratio_re) / 2.0;
let n_eff_re = r_mag.sqrt() * theta.cos();
let n_eff = if n_eff_re > self.n_medium {
n_eff_re
} else {
self.n_medium * 1.5
};
self.wavelength_m / n_eff
}
pub fn is_resonant(&self) -> bool {
let lam_eff = self.effective_wavelength_m();
(self.length_m - lam_eff / 2.0).abs() / lam_eff < 0.1
}
pub fn field_enhancement(&self) -> f64 {
let (_eps_re, eps_im) = self.eps_metal;
let lam_eff = self.effective_wavelength_m();
if eps_im.abs() < 1.0e-10 || self.radius_m < 1.0e-12 {
return 1.0;
}
let ratio = lam_eff / self.radius_m;
let denom = 3.0 * eps_im.abs();
let enhancement_sq = ratio * ratio / (denom * denom);
enhancement_sq.sqrt().clamp(1.0, 200.0)
}
pub fn absorption_cross_section_m2(&self) -> f64 {
let (_eps_re, eps_im) = self.eps_metal;
let k = 2.0 * PI * self.n_medium / self.wavelength_m;
let vol = PI * self.radius_m * self.radius_m * self.length_m;
(2.0 * vol * k * eps_im.abs() / (self.n_medium * self.n_medium)).max(0.0)
}
pub fn scattering_cross_section_m2(&self) -> f64 {
let (eps_re, eps_im) = self.eps_metal;
let k = 2.0 * PI * self.n_medium / self.wavelength_m;
let vol = PI * self.radius_m * self.radius_m * self.length_m;
let n2 = self.n_medium * self.n_medium;
let delta_eps2 = (eps_re - n2) * (eps_re - n2) + eps_im * eps_im;
(k.powi(4) * vol * vol * delta_eps2 / (6.0 * PI)).max(0.0)
}
pub fn sers_enhancement(&self) -> f64 {
self.field_enhancement().powi(4)
}
pub fn radiation_efficiency(&self) -> f64 {
let sigma_sca = self.scattering_cross_section_m2();
let sigma_abs = self.absorption_cross_section_m2();
let sigma_ext = sigma_sca + sigma_abs;
if sigma_ext < 1.0e-60 {
return 0.0;
}
(sigma_sca / sigma_ext).clamp(0.0, 1.0)
}
}
#[derive(Debug, Clone)]
pub struct YagiUdaAntenna {
pub feed: NanorodAntenna,
pub n_directors: usize,
pub director_spacing_m: f64,
pub reflector_length_scale: f64,
pub wavelength_m: f64,
pub n_medium: f64,
}
impl YagiUdaAntenna {
pub fn front_to_back_ratio_db(&self) -> f64 {
(5.0 + 2.0 * self.n_directors as f64).min(15.0)
}
pub fn directivity(&self) -> f64 {
1.5 * (1.0 + self.n_directors as f64)
}
pub fn hpbw_deg(&self) -> f64 {
let d = self.directivity();
if d < 1.0e-10 {
return 180.0;
}
60.0 / d.sqrt()
}
pub fn array_factor_magnitude(&self, theta_rad: f64) -> f64 {
let n_total = self.n_directors + 1; let k = 2.0 * PI * self.n_medium / self.wavelength_m;
let d = self.director_spacing_m;
let delta_phi = -k * d;
let psi = k * d * theta_rad.cos() + delta_phi;
if psi.abs() < 1.0e-12 {
return 1.0;
}
let n = n_total as f64;
let af = (n * psi / 2.0).sin() / (n * (psi / 2.0).sin());
af.abs()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn gold_nanorod(length_nm: f64, radius_nm: f64, wavelength_nm: f64) -> NanorodAntenna {
NanorodAntenna {
length_m: length_nm * 1.0e-9,
radius_m: radius_nm * 1.0e-9,
eps_metal: (-25.0, 1.5),
wavelength_m: wavelength_nm * 1.0e-9,
n_medium: 1.0,
}
}
#[test]
fn hertzian_dipole_directivity() {
let d = HertzianDipole {
wavelength_m: 1064e-9,
dipole_moment_cm: 1e-29,
n_medium: 1.0,
};
assert!(
(d.directivity() - 1.5).abs() < 1.0e-10,
"Dipole directivity must be exactly 1.5, got {}",
d.directivity()
);
}
#[test]
fn hertzian_dipole_radiated_power_positive() {
let d = HertzianDipole {
wavelength_m: 800e-9,
dipole_moment_cm: 1e-29,
n_medium: 1.5,
};
let omega = d.omega();
let p = d.radiated_power_w(omega);
assert!(p > 0.0, "Radiated power must be positive: {p}");
}
#[test]
fn hertzian_dipole_far_field_sin_squared() {
let d = HertzianDipole {
wavelength_m: 1e-6,
dipole_moment_cm: 1e-29,
n_medium: 1.0,
};
assert!((d.far_field_pattern(PI / 2.0) - 1.0).abs() < 1.0e-12);
assert!(d.far_field_pattern(0.0).abs() < 1.0e-12);
}
#[test]
fn hertzian_dipole_near_field_r6_scaling() {
let d = HertzianDipole {
wavelength_m: 1e-6,
dipole_moment_cm: 1e-29,
n_medium: 1.0,
};
let i1 = d.near_field_intensity(10.0e-9);
let i2 = d.near_field_intensity(20.0e-9);
let ratio = i1 / i2;
assert!(
(ratio - 64.0).abs() < 1.0e-6,
"Near-field r^-6 scaling: ratio={ratio}"
);
}
#[test]
fn nanorod_effective_wavelength_shorter_than_freespace() {
let rod = gold_nanorod(100.0, 10.0, 800.0);
let lam_eff = rod.effective_wavelength_m();
assert!(
lam_eff < rod.wavelength_m,
"λ_eff={:.1}nm must be shorter than λ={:.1}nm",
lam_eff * 1e9,
rod.wavelength_m * 1e9
);
}
#[test]
fn nanorod_cross_sections_positive() {
let rod = gold_nanorod(120.0, 12.0, 800.0);
assert!(rod.absorption_cross_section_m2() > 0.0);
assert!(rod.scattering_cross_section_m2() > 0.0);
}
#[test]
fn nanorod_radiation_efficiency_in_range() {
let rod = gold_nanorod(120.0, 12.0, 800.0);
let eta = rod.radiation_efficiency();
assert!(
(0.0..=1.0).contains(&eta),
"Efficiency must be in [0,1]: {eta}"
);
}
#[test]
fn yagi_uda_directivity_increases_with_directors() {
let feed = gold_nanorod(100.0, 10.0, 800.0);
let yagi3 = YagiUdaAntenna {
feed: feed.clone(),
n_directors: 3,
director_spacing_m: 240.0e-9,
reflector_length_scale: 1.1,
wavelength_m: 800.0e-9,
n_medium: 1.0,
};
let yagi1 = YagiUdaAntenna {
feed,
n_directors: 1,
director_spacing_m: 240.0e-9,
reflector_length_scale: 1.1,
wavelength_m: 800.0e-9,
n_medium: 1.0,
};
assert!(
yagi3.directivity() > yagi1.directivity(),
"More directors → higher directivity"
);
}
}