use num_complex::Complex64;
use std::f64::consts::PI;
const C0: f64 = 2.997_924_58e8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BowtieMaterial {
Gold,
Silver,
Aluminum,
}
impl BowtieMaterial {
fn omega_p(&self) -> f64 {
match self {
BowtieMaterial::Gold => 1.37e16,
BowtieMaterial::Silver => 1.37e16,
BowtieMaterial::Aluminum => 2.24e16,
}
}
fn gamma(&self) -> f64 {
match self {
BowtieMaterial::Gold => 1.22e14,
BowtieMaterial::Silver => 2.73e13,
BowtieMaterial::Aluminum => 1.22e14,
}
}
fn eps_inf(&self) -> f64 {
match self {
BowtieMaterial::Gold => 9.5,
BowtieMaterial::Silver => 3.7,
BowtieMaterial::Aluminum => 1.0,
}
}
pub fn permittivity(&self, omega: f64) -> Complex64 {
let wp = self.omega_p();
let gam = self.gamma();
let ei = self.eps_inf();
let denom = Complex64::new(omega * omega, gam * omega);
Complex64::new(ei, 0.0) - Complex64::new(wp * wp, 0.0) / denom
}
fn intrinsic_q(&self, omega: f64) -> f64 {
omega / self.gamma()
}
}
fn purcell_from_q_v(q: f64, mode_volume_m3: f64, wavelength_m: f64, n: f64) -> f64 {
if mode_volume_m3 < 1.0e-50 {
return 0.0;
}
let lambda_n = wavelength_m / n;
let prefactor = 3.0 / (4.0 * PI * PI);
prefactor * (lambda_n * lambda_n * lambda_n) * q / mode_volume_m3
}
#[derive(Debug, Clone)]
pub struct BowtiAntenna {
pub arm_length: f64,
pub gap_nm: f64,
pub material: BowtieMaterial,
pub wavelength: f64,
pub substrate_index: f64,
}
impl BowtiAntenna {
pub fn new_gold(arm_length_nm: f64, gap_nm: f64, wavelength: f64) -> Self {
Self {
arm_length: arm_length_nm * 1.0e-9,
gap_nm,
material: BowtieMaterial::Gold,
wavelength,
substrate_index: 1.5, }
}
pub fn field_enhancement(&self) -> f64 {
let omega = 2.0 * PI * C0 / self.wavelength;
let eps_m = self.material.permittivity(omega);
let eps_d = self.substrate_index * self.substrate_index;
let lf = (eps_m.re.abs() / eps_d).sqrt();
let gap_m = self.gap_nm * 1.0e-9;
let geom = (self.arm_length / gap_m).sqrt();
(lf * geom).min(2000.0)
}
pub fn mode_volume(&self) -> f64 {
let gap_m = self.gap_nm * 1.0e-9;
gap_m * gap_m * gap_m
}
pub fn q_factor(&self) -> f64 {
let omega = 2.0 * PI * C0 / self.wavelength;
let q_ohm = self.material.intrinsic_q(omega) / 2.0;
let q_rad = 10.0_f64; 1.0 / (1.0 / q_rad + 1.0 / q_ohm)
}
pub fn purcell_factor(&self) -> f64 {
let n = self.substrate_index;
purcell_from_q_v(self.q_factor(), self.mode_volume(), self.wavelength, n)
}
pub fn sers_enhancement(&self) -> f64 {
let fe = self.field_enhancement();
fe * fe * fe * fe
}
pub fn resonance_wavelength(&self) -> f64 {
let omega_est = 2.0 * PI * C0 / self.wavelength;
let eps_m = self.material.permittivity(omega_est);
let eps_d = self.substrate_index * self.substrate_index;
let n_eff = ((eps_m.re.abs() * eps_d) / (eps_m.re.abs() + eps_d))
.sqrt()
.max(1.0);
let total_length = 2.0 * self.arm_length + self.gap_nm * 1.0e-9;
2.0 * n_eff * total_length
}
}
#[derive(Debug, Clone)]
pub struct NanoparticleOnMirror {
pub particle_radius_nm: f64,
pub gap_nm: f64,
pub particle_material: BowtieMaterial,
pub mirror_material: BowtieMaterial,
pub spacer_index: f64,
}
impl NanoparticleOnMirror {
pub fn new_gold(radius_nm: f64, gap_nm: f64) -> Self {
Self {
particle_radius_nm: radius_nm,
gap_nm,
particle_material: BowtieMaterial::Gold,
mirror_material: BowtieMaterial::Gold,
spacer_index: 1.46, }
}
pub fn mode_volume(&self) -> f64 {
let gap = self.gap_nm * 1.0e-9;
let r = self.particle_radius_nm * 1.0e-9;
PI * r * gap * gap
}
pub fn q_factor(&self) -> f64 {
let lambda = self.resonance_wavelength();
let omega = 2.0 * PI * C0 / lambda;
let q_ohm = self.particle_material.intrinsic_q(omega) / 2.0;
let q_rad = 15.0_f64;
1.0 / (1.0 / q_rad + 1.0 / q_ohm)
}
pub fn field_enhancement(&self) -> f64 {
let r = self.particle_radius_nm;
let gap = self.gap_nm;
let scale = (r / gap).powf(1.0 / 3.0);
let q = self.q_factor();
(scale * q).min(3000.0)
}
pub fn purcell_factor(&self) -> f64 {
let n = self.spacer_index;
let lambda = self.resonance_wavelength();
purcell_from_q_v(self.q_factor(), self.mode_volume(), lambda, n)
}
pub fn resonance_wavelength(&self) -> f64 {
let eps_d = self.spacer_index * self.spacer_index;
let omega_p = self.particle_material.omega_p();
let eps_inf = self.particle_material.eps_inf();
let omega_lspr = (omega_p * omega_p / (eps_inf + 2.0 * eps_d)).sqrt();
let gap = self.gap_nm;
let r = self.particle_radius_nm;
let coupling_shift = 1.0 - 0.2 * (gap / r).sqrt();
let omega_res = omega_lspr * coupling_shift.max(0.5);
2.0 * PI * C0 / omega_res
}
pub fn facet_modes(&self) -> Vec<f64> {
let lambda0 = self.resonance_wavelength();
(1..=5_usize)
.map(|l| lambda0 / (1.0 + 0.05 * (l as f64 - 1.0)))
.collect()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PhCCavityType {
L3,
H1,
H3,
Heterostructure,
}
#[derive(Debug, Clone)]
pub struct PhCNanocavity {
pub lattice_constant_nm: f64,
pub hole_radius_nm: f64,
pub slab_thickness_nm: f64,
pub n_slab: f64,
pub cavity_type: PhCCavityType,
}
impl PhCNanocavity {
pub fn new_l3_gaas(a: f64, r: f64) -> Self {
Self {
lattice_constant_nm: a,
hole_radius_nm: r,
slab_thickness_nm: a * 0.6,
n_slab: 3.4,
cavity_type: PhCCavityType::L3,
}
}
pub fn new_l3_sin(a: f64, r: f64) -> Self {
Self {
lattice_constant_nm: a,
hole_radius_nm: r,
slab_thickness_nm: a * 0.5,
n_slab: 2.0,
cavity_type: PhCCavityType::L3,
}
}
pub fn quality_factor(&self) -> f64 {
match self.cavity_type {
PhCCavityType::L3 => 1.0e6,
PhCCavityType::H1 => 1.0e4,
PhCCavityType::H3 => 1.0e5,
PhCCavityType::Heterostructure => 1.0e8,
}
}
pub fn mode_volume_cubic_lambda(&self) -> f64 {
match self.cavity_type {
PhCCavityType::L3 => 0.7,
PhCCavityType::H1 => 0.3,
PhCCavityType::H3 => 0.4,
PhCCavityType::Heterostructure => 0.8,
}
}
pub fn mode_volume_m3(&self) -> f64 {
let lambda_n = self.resonance_wavelength() / self.n_slab;
let v_norm = self.mode_volume_cubic_lambda();
v_norm * lambda_n * lambda_n * lambda_n
}
pub fn purcell_factor(&self) -> f64 {
let lambda = self.resonance_wavelength();
let n = self.n_slab;
purcell_from_q_v(self.quality_factor(), self.mode_volume_m3(), lambda, n)
}
pub fn resonance_wavelength(&self) -> f64 {
let a_m = self.lattice_constant_nm * 1.0e-9;
let r_over_a = self.hole_radius_nm / self.lattice_constant_nm;
let a_over_lambda = 0.26 + 0.15 * (r_over_a - 0.30).clamp(-0.1, 0.1);
a_m / a_over_lambda
}
pub fn coupling_to_waveguide(&self, waveguide_gap_holes: usize) -> f64 {
let q_total = self.quality_factor();
let barrier = waveguide_gap_holes as f64;
q_total * (barrier * 0.8_f64).exp()
}
pub fn zero_point_field_v_per_m(&self, wavelength: f64) -> f64 {
const HBAR: f64 = 1.054_571_817e-34;
const EPS0: f64 = 8.854_187_817e-12;
let omega = 2.0 * PI * C0 / wavelength;
let n = self.n_slab;
let v = self.mode_volume_m3();
if v < 1.0e-50 {
return 0.0;
}
(HBAR * omega / (2.0 * EPS0 * n * n * v)).sqrt()
}
}
#[derive(Debug, Clone)]
pub struct MimNanocavity {
pub length_nm: f64,
pub width_nm: f64,
pub gap_nm: f64,
pub metal: BowtieMaterial,
pub dielectric_index: f64,
}
impl MimNanocavity {
pub fn new_gold_air(length_nm: f64, width_nm: f64, gap_nm: f64) -> Self {
Self {
length_nm,
width_nm,
gap_nm,
metal: BowtieMaterial::Gold,
dielectric_index: 1.0,
}
}
pub fn effective_index(&self, wavelength: f64) -> Complex64 {
let omega = 2.0 * PI * C0 / wavelength;
let eps_m = self.metal.permittivity(omega);
let eps_d = Complex64::new(self.dielectric_index * self.dielectric_index, 0.0);
let d = self.gap_nm * 1.0e-9;
let k0 = omega / C0;
let neff_sq_0 = -eps_m;
let n_eff_0 = neff_sq_0.sqrt();
let n0 = if n_eff_0.re < 0.0 { -n_eff_0 } else { n_eff_0 };
let k0d = k0 * d;
let delta_n = eps_d * Complex64::new(k0d, 0.0) / (Complex64::new(2.0, 0.0) * n0);
let n_eff = n0 + delta_n;
if n_eff.re < 0.0 {
-n_eff
} else {
n_eff
}
}
pub fn resonance_wavelength(&self) -> f64 {
let l = self.length_nm * 1.0e-9;
let mut lambda = 2.0 * l * self.dielectric_index;
for _ in 0..20 {
let n_eff = self.effective_index(lambda);
lambda = 2.0 * n_eff.re * l;
if lambda < 200.0e-9 {
lambda = 200.0e-9;
break;
}
}
lambda
}
pub fn mode_volume(&self) -> f64 {
let d = self.gap_nm * 1.0e-9;
let w = self.width_nm * 1.0e-9;
let l = self.length_nm * 1.0e-9;
let l_eff = l + 20.0e-9;
d * w * l_eff
}
pub fn q_factor(&self) -> f64 {
let lambda = self.resonance_wavelength();
let n_eff = self.effective_index(lambda);
if n_eff.im.abs() < 1.0e-10 || n_eff.re < 1.0e-10 {
return 10.0; }
PI * n_eff.re / n_eff.im.abs()
}
pub fn purcell_factor(&self) -> f64 {
let n = self.dielectric_index;
let lambda = self.resonance_wavelength();
purcell_from_q_v(self.q_factor(), self.mode_volume(), lambda, n)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_bowtie_material_gold_permittivity_negative_re() {
let omega = 2.0 * PI * C0 / 532.0e-9;
let eps = BowtieMaterial::Gold.permittivity(omega);
assert!(
eps.re < 0.0,
"Gold Re(ε) at 532 nm should be negative: {}",
eps.re
);
}
#[test]
fn test_bowtie_material_silver_lower_loss_than_gold() {
let omega = 2.0 * PI * C0 / 632.0e-9;
let q_ag = BowtieMaterial::Silver.intrinsic_q(omega);
let q_au = BowtieMaterial::Gold.intrinsic_q(omega);
assert!(
q_ag > q_au,
"Silver should have higher intrinsic Q (lower loss) than gold: {q_ag} vs {q_au}"
);
}
#[test]
fn test_bowtie_field_enhancement_positive() {
let bowtie = BowtiAntenna::new_gold(75.0, 5.0, 800.0e-9);
let fe = bowtie.field_enhancement();
assert!(fe > 0.0, "Field enhancement must be positive: {fe}");
}
#[test]
fn test_bowtie_sers_enhancement_is_fe4() {
let bowtie = BowtiAntenna::new_gold(75.0, 5.0, 800.0e-9);
let fe = bowtie.field_enhancement();
let sers = bowtie.sers_enhancement();
assert_abs_diff_eq!(sers, fe * fe * fe * fe, epsilon = 1.0);
}
#[test]
fn test_bowtie_mode_volume_scales_with_gap() {
let bowtie1 = BowtiAntenna::new_gold(75.0, 5.0, 800.0e-9);
let bowtie2 = BowtiAntenna::new_gold(75.0, 10.0, 800.0e-9);
let v1 = bowtie1.mode_volume();
let v2 = bowtie2.mode_volume();
assert_abs_diff_eq!(v2 / v1, 8.0, epsilon = 1.0e-10);
}
#[test]
fn test_bowtie_purcell_factor_positive() {
let bowtie = BowtiAntenna::new_gold(75.0, 5.0, 800.0e-9);
let fp = bowtie.purcell_factor();
assert!(fp > 0.0, "Purcell factor must be positive: {fp}");
}
#[test]
fn test_bowtie_resonance_wavelength_reasonable() {
let bowtie = BowtiAntenna::new_gold(75.0, 5.0, 800.0e-9);
let lam = bowtie.resonance_wavelength();
assert!(
lam > 300.0e-9 && lam < 3.0e-6,
"Resonance wavelength should be 300 nm–3 µm: {} nm",
lam * 1.0e9
);
}
#[test]
fn test_npom_mode_volume_sub_lambda3() {
let npom = NanoparticleOnMirror::new_gold(40.0, 1.0);
let v = npom.mode_volume();
let lambda = npom.resonance_wavelength();
let lambda3 = lambda * lambda * lambda;
assert!(
v < lambda3,
"NPoM mode volume should be sub-diffraction: V={v:.3e} λ³={lambda3:.3e}"
);
}
#[test]
fn test_npom_facet_modes_monotone_decreasing() {
let npom = NanoparticleOnMirror::new_gold(40.0, 1.0);
let modes = npom.facet_modes();
assert_eq!(modes.len(), 5);
for i in 0..modes.len() - 1 {
assert!(
modes[i] > modes[i + 1],
"Facet modes should be monotone decreasing in wavelength"
);
}
}
#[test]
fn test_npom_field_enhancement_increases_with_smaller_gap() {
let npom1 = NanoparticleOnMirror::new_gold(40.0, 2.0);
let npom2 = NanoparticleOnMirror::new_gold(40.0, 1.0);
let fe1 = npom1.field_enhancement();
let fe2 = npom2.field_enhancement();
assert!(
fe2 > fe1,
"Smaller gap should give higher enhancement: {fe2} vs {fe1}"
);
}
#[test]
fn test_phc_l3_quality_factor() {
let cav = PhCNanocavity::new_l3_gaas(260.0, 78.0);
let q = cav.quality_factor();
assert_abs_diff_eq!(q, 1.0e6, epsilon = 1.0);
}
#[test]
fn test_phc_resonance_wavelength_in_nir() {
let cav = PhCNanocavity::new_l3_gaas(260.0, 78.0);
let lam = cav.resonance_wavelength();
assert!(
lam > 600.0e-9 && lam < 2.0e-6,
"PhC resonance should be in NIR: {} nm",
lam * 1.0e9
);
}
#[test]
fn test_phc_purcell_factor_large() {
let cav = PhCNanocavity::new_l3_gaas(260.0, 78.0);
let fp = cav.purcell_factor();
assert!(fp > 1000.0, "L3 Purcell factor should be > 1000: {fp}");
}
#[test]
fn test_phc_zero_point_field_positive() {
let cav = PhCNanocavity::new_l3_gaas(260.0, 78.0);
let lambda = cav.resonance_wavelength();
let ezpf = cav.zero_point_field_v_per_m(lambda);
assert!(ezpf > 0.0, "Zero-point field must be positive: {ezpf}");
}
#[test]
fn test_phc_heterostructure_q_exceeds_l3() {
let l3 = PhCNanocavity::new_l3_gaas(260.0, 78.0);
let hs = PhCNanocavity {
lattice_constant_nm: 260.0,
hole_radius_nm: 78.0,
slab_thickness_nm: 156.0,
n_slab: 3.4,
cavity_type: PhCCavityType::Heterostructure,
};
assert!(hs.quality_factor() > l3.quality_factor());
}
#[test]
fn test_mim_effective_index_large_real_part() {
let mim = MimNanocavity::new_gold_air(200.0, 50.0, 5.0);
let lambda = 800.0e-9;
let n_eff = mim.effective_index(lambda);
assert!(
n_eff.re > 1.0,
"MIM n_eff should exceed free-space: {}",
n_eff.re
);
}
#[test]
fn test_mim_mode_volume_positive() {
let mim = MimNanocavity::new_gold_air(200.0, 50.0, 5.0);
let v = mim.mode_volume();
assert!(v > 0.0, "Mode volume must be positive: {v}");
}
#[test]
fn test_mim_purcell_factor_positive() {
let mim = MimNanocavity::new_gold_air(200.0, 50.0, 5.0);
let fp = mim.purcell_factor();
assert!(fp > 0.0, "MIM Purcell factor must be positive: {fp}");
}
}