use num_complex::Complex64;
use std::f64::consts::PI;
use crate::plasmonics::spp::DrudeMetal;
const C0: f64 = 2.997_924_58e8;
#[derive(Debug, Clone)]
pub struct PlasmonicNanoparticle {
pub radius_nm: f64,
pub metal: DrudeMetal,
pub eps_medium: f64,
}
impl PlasmonicNanoparticle {
pub fn new(radius_nm: f64, metal: DrudeMetal, eps_medium: f64) -> Self {
Self {
radius_nm,
metal,
eps_medium,
}
}
pub fn polarizability(&self, omega: f64) -> Complex64 {
let r_m = self.radius_nm * 1.0e-9;
let eps_m = self.metal.permittivity(omega);
let eps_d = Complex64::new(self.eps_medium, 0.0);
let vol = 4.0 * PI * r_m * r_m * r_m;
vol * (eps_m - eps_d) / (eps_m + 2.0 * eps_d)
}
fn lspr_omega(&self) -> f64 {
let eps_inf = self.metal.eps_inf;
let wp = self.metal.omega_p;
let gamma = self.metal.gamma;
let eps_d = self.eps_medium;
let arg = wp * wp / (eps_inf + 2.0 * eps_d) - gamma * gamma;
if arg > 0.0 {
arg.sqrt()
} else {
wp / (eps_inf + 2.0 * eps_d).sqrt()
}
}
pub fn lspr_wavelength_nm(&self) -> f64 {
let omega_lspr = self.lspr_omega();
2.0 * PI * C0 / omega_lspr * 1.0e9
}
pub fn lspr_quality_factor(&self) -> f64 {
self.lspr_omega() / self.metal.gamma
}
pub fn extinction_cross_section_nm2(&self, omega: f64) -> f64 {
let k = omega * self.eps_medium.sqrt() / C0; let alpha = self.polarizability(omega);
let sigma_m2 = k * alpha.im;
sigma_m2 * 1.0e18 }
pub fn scattering_cross_section_nm2(&self, omega: f64) -> f64 {
let k = omega * self.eps_medium.sqrt() / C0;
let alpha = self.polarizability(omega);
let k4 = k * k * k * k;
let alpha2 = alpha.norm_sqr();
let sigma_m2 = k4 * alpha2 / (6.0 * PI);
sigma_m2 * 1.0e18
}
pub fn absorption_cross_section_nm2(&self, omega: f64) -> f64 {
let ext = self.extinction_cross_section_nm2(omega);
let sca = self.scattering_cross_section_nm2(omega);
(ext - sca).max(0.0)
}
pub fn near_field_enhancement(&self, omega: f64) -> f64 {
let eps_m = self.metal.permittivity(omega);
let eps_d = Complex64::new(self.eps_medium, 0.0);
let f = (eps_m - eps_d) / (eps_m + 2.0 * eps_d);
f.norm_sqr()
}
pub fn sers_enhancement(&self, omega: f64) -> f64 {
let fe2 = self.near_field_enhancement(omega);
fe2 * fe2
}
pub fn sensitivity_nm_per_riu(&self) -> f64 {
let dn = 0.01_f64;
let n_d = self.eps_medium.sqrt();
let eps_d2 = (n_d + dn) * (n_d + dn);
let np2 = PlasmonicNanoparticle::new(self.radius_nm, self.metal.clone(), eps_d2);
let lam1 = self.lspr_wavelength_nm();
let lam2 = np2.lspr_wavelength_nm();
(lam2 - lam1) / dn
}
pub fn figure_of_merit_per_riu(&self) -> f64 {
let sensitivity = self.sensitivity_nm_per_riu();
let omega_lspr = self.lspr_omega();
let gamma = self.metal.gamma;
let fwhm_nm = 2.0 * PI * C0 * gamma / (omega_lspr * omega_lspr) * 1.0e9;
if fwhm_nm < f64::EPSILON {
return 0.0;
}
sensitivity / fwhm_nm
}
pub fn extinction_spectrum(
&self,
lambda_min_nm: f64,
lambda_max_nm: f64,
n_pts: usize,
) -> Vec<(f64, f64)> {
(0..n_pts)
.map(|i| {
let lam_nm = lambda_min_nm
+ (lambda_max_nm - lambda_min_nm) * i as f64 / (n_pts - 1).max(1) as f64;
let omega = 2.0 * PI * C0 / (lam_nm * 1.0e-9);
let sigma = self.extinction_cross_section_nm2(omega);
(lam_nm, sigma)
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct PlasmonicNanorod {
pub short_axis_nm: f64,
pub long_axis_nm: f64,
pub metal: DrudeMetal,
pub eps_medium: f64,
}
impl PlasmonicNanorod {
pub fn new(short_nm: f64, long_nm: f64, metal: DrudeMetal, eps_medium: f64) -> Self {
Self {
short_axis_nm: short_nm,
long_axis_nm: long_nm,
metal,
eps_medium,
}
}
pub fn aspect_ratio(&self) -> f64 {
self.long_axis_nm / self.short_axis_nm
}
pub fn depolarization_factor_long(&self) -> f64 {
let ar = self.aspect_ratio();
if (ar - 1.0).abs() < 1.0e-6 {
return 1.0 / 3.0; }
let e2 = 1.0 - 1.0 / (ar * ar);
let e = e2.sqrt();
let lz = (1.0 - e2) / (e2) * (-1.0 + (1.0 / (2.0 * e)) * ((1.0 + e) / (1.0 - e)).ln());
lz.abs()
}
pub fn depolarization_factor_trans(&self) -> f64 {
(1.0 - self.depolarization_factor_long()) / 2.0
}
fn lspr_omega_for_depol(&self, dep_l: f64) -> f64 {
let eps_inf = self.metal.eps_inf;
let wp = self.metal.omega_p;
let gamma = self.metal.gamma;
let eps_d = self.eps_medium;
let target = eps_inf + eps_d * (1.0 - dep_l) / dep_l;
if target <= 0.0 {
return wp / eps_inf.sqrt();
}
let omega2 = wp * wp / target - gamma * gamma;
if omega2 > 0.0 {
omega2.sqrt()
} else {
wp / target.sqrt()
}
}
pub fn longitudinal_lspr_nm(&self) -> f64 {
let omega = self.lspr_omega_for_depol(self.depolarization_factor_long());
2.0 * PI * C0 / omega * 1.0e9
}
pub fn transverse_lspr_nm(&self) -> f64 {
let omega = self.lspr_omega_for_depol(self.depolarization_factor_trans());
2.0 * PI * C0 / omega * 1.0e9
}
pub fn polarizability_longitudinal(&self, omega: f64) -> Complex64 {
let a_m = self.long_axis_nm * 1.0e-9;
let b_m = self.short_axis_nm * 1.0e-9;
let vol = 4.0 / 3.0 * PI * a_m * b_m * b_m;
let eps_m = self.metal.permittivity(omega);
let eps_d = Complex64::new(self.eps_medium, 0.0);
let l = Complex64::new(self.depolarization_factor_long(), 0.0);
let numerator = eps_d * (eps_m - eps_d);
let denominator = eps_d + l * (eps_m - eps_d);
vol * numerator / denominator
}
pub fn polarizability_transverse(&self, omega: f64) -> Complex64 {
let a_m = self.long_axis_nm * 1.0e-9;
let b_m = self.short_axis_nm * 1.0e-9;
let vol = 4.0 / 3.0 * PI * a_m * b_m * b_m;
let eps_m = self.metal.permittivity(omega);
let eps_d = Complex64::new(self.eps_medium, 0.0);
let l = Complex64::new(self.depolarization_factor_trans(), 0.0);
let numerator = eps_d * (eps_m - eps_d);
let denominator = eps_d + l * (eps_m - eps_d);
vol * numerator / denominator
}
}
#[derive(Debug, Clone)]
pub struct PlasmonicGap {
pub particle1: PlasmonicNanoparticle,
pub particle2: PlasmonicNanoparticle,
pub gap_nm: f64,
}
impl PlasmonicGap {
pub fn new(p1: PlasmonicNanoparticle, p2: PlasmonicNanoparticle, gap_nm: f64) -> Self {
Self {
particle1: p1,
particle2: p2,
gap_nm,
}
}
pub fn gap_enhancement(&self, omega: f64) -> f64 {
let ef1 = self.particle1.near_field_enhancement(omega);
let ef2 = self.particle2.near_field_enhancement(omega);
let ef_single = (ef1 * ef2).sqrt();
let r_eff = self.particle1.radius_nm.min(self.particle2.radius_nm);
let coupling = (r_eff / self.gap_nm).max(1.0);
ef_single * coupling
}
pub fn lspr_redshift_nm(&self) -> f64 {
let lam_sp = self.particle1.lspr_wavelength_nm();
let r = self.particle1.radius_nm;
let g = self.gap_nm;
let a = 0.18_f64;
let kappa = 0.23_f64;
lam_sp * a * (-g / (kappa * r)).exp()
}
}
#[derive(Debug, Clone)]
pub struct DipoleAntenna {
pub arm_length_nm: f64,
pub arm_width_nm: f64,
pub gap_nm: f64,
pub metal: DrudeMetal,
pub eps_substrate: f64,
}
impl DipoleAntenna {
pub fn new(
length_nm: f64,
width_nm: f64,
gap_nm: f64,
metal: DrudeMetal,
eps_sub: f64,
) -> Self {
Self {
arm_length_nm: length_nm,
arm_width_nm: width_nm,
gap_nm,
metal,
eps_substrate: eps_sub,
}
}
pub fn resonance_wavelength_nm(&self) -> f64 {
let eps_eff = (self.eps_substrate + 1.0) / 2.0;
let n_eff = eps_eff.sqrt();
let total_length_nm = 2.0 * self.arm_length_nm + self.gap_nm;
2.0 * n_eff * total_length_nm
}
pub fn radiation_efficiency(&self) -> f64 {
let lambda_res_m = self.resonance_wavelength_nm() * 1.0e-9;
let omega = 2.0 * PI * C0 / lambda_res_m;
let spp = crate::plasmonics::spp::SurfacePlasmonPolariton::new(self.metal.clone(), 1.0);
let l_sp_nm = spp.propagation_length_um(omega) * 1.0e3; let l_arm = self.arm_length_nm;
if l_sp_nm < f64::EPSILON {
return 0.0;
}
1.0 / (1.0 + l_arm / l_sp_nm)
}
pub fn directivity(&self) -> f64 {
1.5_f64
}
pub fn gap_enhancement(&self, omega: f64) -> f64 {
let eps_m = self.metal.permittivity(omega);
let eps_d = Complex64::new(1.0, 0.0); let f_lf = (eps_m - eps_d) / (eps_m + 2.0 * eps_d);
let local_fe = f_lf.norm_sqr();
let geometric = self.arm_length_nm / self.gap_nm.max(0.1);
local_fe * geometric
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
fn omega_from_nm(lam_nm: f64) -> f64 {
2.0 * PI * C0 / (lam_nm * 1.0e-9)
}
#[test]
fn test_lspr_wavelength_gold_in_water() {
let gold = DrudeMetal::gold();
let np = PlasmonicNanoparticle::new(10.0, gold, 1.77);
let lam = np.lspr_wavelength_nm();
assert!(
lam > 400.0 && lam < 700.0,
"Gold LSPR in water should be ~500-600 nm; got {lam:.1} nm"
);
}
#[test]
fn test_extinction_cross_section_at_resonance_peaks() {
let gold = DrudeMetal::gold();
let np = PlasmonicNanoparticle::new(10.0, gold, 1.77);
let spectrum = np.extinction_spectrum(400.0, 800.0, 200);
let (lam_peak, sigma_peak) =
spectrum
.iter()
.copied()
.fold(
(0.0_f64, 0.0_f64),
|(bl, bs), (l, s)| {
if s > bs {
(l, s)
} else {
(bl, bs)
}
},
);
assert!(sigma_peak > 0.0, "Extinction peak must be positive");
assert!(
lam_peak > 400.0 && lam_peak < 800.0,
"Peak must be in visible range; got {lam_peak:.1} nm"
);
}
#[test]
fn test_sers_enhancement_gt_scattering() {
let gold = DrudeMetal::gold();
let np = PlasmonicNanoparticle::new(10.0, gold, 1.77);
let lspr_nm = np.lspr_wavelength_nm();
let omega = omega_from_nm(lspr_nm);
let fe2 = np.near_field_enhancement(omega);
let sers = np.sers_enhancement(omega);
assert!(
sers >= fe2,
"SERS EF ({sers:.2}) should be >= near-field enhancement squared ({fe2:.2})"
);
}
#[test]
fn test_near_field_enhancement_at_resonance() {
let gold = DrudeMetal::gold();
let np = PlasmonicNanoparticle::new(10.0, gold, 1.77);
let lspr_nm = np.lspr_wavelength_nm();
let omega = omega_from_nm(lspr_nm);
let fe2 = np.near_field_enhancement(omega);
assert!(
fe2 > 1.0,
"Near-field enhancement at LSPR should exceed 1; got {fe2:.3}"
);
}
#[test]
fn test_sensitivity_nm_per_riu_positive() {
let gold = DrudeMetal::gold();
let np = PlasmonicNanoparticle::new(10.0, gold, 1.77);
let s = np.sensitivity_nm_per_riu();
assert!(
s > 0.0,
"LSPR sensitivity must be positive (red-shift with higher n); got {s}"
);
}
#[test]
fn test_nanorod_aspect_ratio() {
let gold = DrudeMetal::gold();
let rod = PlasmonicNanorod::new(10.0, 40.0, gold, 1.77);
let ar = rod.aspect_ratio();
let expected = 40.0 / 10.0;
assert!(
(ar - expected).abs() < 1.0e-10,
"Aspect ratio mismatch: got {ar}, expected {expected}"
);
}
#[test]
fn test_nanorod_longitudinal_redshifted() {
let gold = DrudeMetal::gold();
let rod = PlasmonicNanorod::new(10.0, 40.0, gold, 1.77);
let lam_long = rod.longitudinal_lspr_nm();
let lam_trans = rod.transverse_lspr_nm();
assert!(
lam_long > lam_trans,
"Longitudinal LSPR ({lam_long:.1} nm) must be red-shifted vs transverse ({lam_trans:.1} nm)"
);
}
#[test]
fn test_gap_enhancement_larger_than_single() {
let gold1 = DrudeMetal::gold();
let gold2 = DrudeMetal::gold();
let np1 = PlasmonicNanoparticle::new(10.0, gold1.clone(), 1.0);
let np2 = PlasmonicNanoparticle::new(10.0, gold2.clone(), 1.0);
let gap = PlasmonicGap::new(np1.clone(), np2.clone(), 2.0);
let omega = omega_from_nm(np1.lspr_wavelength_nm());
let fe_single = np1.near_field_enhancement(omega);
let fe_gap = gap.gap_enhancement(omega);
assert!(
fe_gap >= fe_single,
"Gap enhancement ({fe_gap:.2}) should exceed single-particle enhancement ({fe_single:.2})"
);
}
}