use num_complex::Complex64;
use std::f64::consts::PI;
const SPEED_OF_LIGHT: f64 = 2.997_924_58e8;
#[derive(Debug, Clone)]
pub struct DielectricPillar {
pub radius: f64,
pub height: f64,
pub period: f64,
pub n_pillar: f64,
pub n_substrate: f64,
pub wavelength: f64,
}
impl DielectricPillar {
pub fn new_tio2(radius: f64, height: f64, period: f64, wavelength: f64) -> Self {
Self {
radius,
height,
period,
n_pillar: 2.4,
n_substrate: 1.46,
wavelength,
}
}
pub fn new_silicon(radius: f64, height: f64, period: f64, wavelength: f64) -> Self {
Self {
radius,
height,
period,
n_pillar: 3.5,
n_substrate: 1.46,
wavelength,
}
}
pub fn fill_fraction(&self) -> f64 {
PI * self.radius * self.radius / (self.period * self.period)
}
fn n_eff(&self) -> f64 {
let ff = self.fill_fraction().clamp(0.0, 1.0);
let n_air = 1.0_f64;
(ff * self.n_pillar * self.n_pillar + (1.0 - ff) * n_air * n_air).sqrt()
}
pub fn magnetic_dipole_resonance(&self) -> f64 {
2.4 * self.n_pillar * self.height
}
pub fn electric_dipole_resonance(&self) -> f64 {
1.7 * self.n_pillar * self.height
}
fn propagation_phase(&self) -> f64 {
2.0 * PI * self.n_eff() * self.height / self.wavelength
}
fn md_lorentzian_factor(&self) -> Complex64 {
let omega = 2.0 * PI * SPEED_OF_LIGHT / self.wavelength;
let lambda_md = self.magnetic_dipole_resonance();
let omega_md = 2.0 * PI * SPEED_OF_LIGHT / lambda_md;
let q = (self.n_pillar / (2.0 * self.fill_fraction().max(0.01))).clamp(5.0, 200.0);
let gamma = omega_md / q;
Complex64::new(0.0, -gamma) / Complex64::new(omega - omega_md, gamma)
}
pub fn transmission(&self) -> Complex64 {
let phi = self.propagation_phase();
let prop = Complex64::from_polar(1.0, phi);
let t_fresnel = 2.0 * self.n_eff() / (self.n_eff() + self.n_substrate);
let resonance_strength = 0.3;
let res = self.md_lorentzian_factor();
let modulation = Complex64::new(1.0, 0.0) + resonance_strength * res;
prop * modulation * t_fresnel
}
pub fn phase_at_radius(&self, radius: f64) -> f64 {
let tmp = Self {
radius,
height: self.height,
period: self.period,
n_pillar: self.n_pillar,
n_substrate: self.n_substrate,
wavelength: self.wavelength,
};
let t = tmp.transmission();
t.arg().rem_euclid(2.0 * PI)
}
pub fn amplitude_at_radius(&self, radius: f64) -> f64 {
let tmp = Self {
radius,
height: self.height,
period: self.period,
n_pillar: self.n_pillar,
n_substrate: self.n_substrate,
wavelength: self.wavelength,
};
tmp.transmission().norm().clamp(0.0, 1.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlasmonicMetal {
Gold,
Silver,
Aluminum,
}
impl PlasmonicMetal {
pub fn plasma_frequency_rad_s(&self) -> f64 {
match self {
PlasmonicMetal::Gold => 1.37e16,
PlasmonicMetal::Silver => 1.37e16,
PlasmonicMetal::Aluminum => 2.24e16,
}
}
pub fn damping_rate_rad_s(&self) -> f64 {
match self {
PlasmonicMetal::Gold => 4.08e13,
PlasmonicMetal::Silver => 2.73e13,
PlasmonicMetal::Aluminum => 1.22e14,
}
}
fn epsilon_inf(&self) -> f64 {
match self {
PlasmonicMetal::Gold => 9.5,
PlasmonicMetal::Silver => 3.7,
PlasmonicMetal::Aluminum => 1.0,
}
}
pub fn permittivity(&self, omega: f64) -> Complex64 {
let wp = self.plasma_frequency_rad_s();
let gamma = self.damping_rate_rad_s();
let eps_inf = self.epsilon_inf();
let denominator = Complex64::new(omega * omega, gamma * omega);
Complex64::new(eps_inf, 0.0) - Complex64::new(wp * wp, 0.0) / denominator
}
}
#[derive(Debug, Clone)]
pub struct PlasmonicAntenna {
pub length: f64,
pub width: f64,
pub height: f64,
pub period_x: f64,
pub period_y: f64,
pub metal: PlasmonicMetal,
pub wavelength: f64,
}
impl PlasmonicAntenna {
pub fn new_gold_rod(
length: f64,
width: f64,
height: f64,
period: f64,
wavelength: f64,
) -> Self {
Self {
length,
width,
height,
period_x: period,
period_y: period,
metal: PlasmonicMetal::Gold,
wavelength,
}
}
fn n_eff_medium(&self) -> f64 {
let n_sub = 1.5_f64;
let n_air = 1.0_f64;
((n_sub + n_air) / 2.0).sqrt()
}
pub fn resonance_wavelength(&self) -> f64 {
2.0 * self.n_eff_medium() * self.length
}
fn omega(&self) -> f64 {
2.0 * PI * SPEED_OF_LIGHT / self.wavelength
}
fn omega_res(&self) -> f64 {
2.0 * PI * SPEED_OF_LIGHT / self.resonance_wavelength()
}
fn q_factor(&self) -> f64 {
let omega = self.omega();
let eps = self.metal.permittivity(omega);
let q_raw = eps.re.abs() / eps.im.abs();
q_raw.clamp(3.0, 30.0)
}
pub fn phase_shift(&self) -> f64 {
let omega = self.omega();
let omega_r = self.omega_res();
let gamma = self.metal.damping_rate_rad_s() / self.q_factor();
(gamma / (omega_r - omega + 1e-30)).atan() + PI / 2.0
}
pub fn amplitude(&self) -> f64 {
let omega = self.omega();
let omega_r = self.omega_res();
let gamma = self.metal.damping_rate_rad_s() / self.q_factor();
let detuning_sq = (omega - omega_r).powi(2);
let gamma_sq = gamma * gamma;
let peak_absorption = 0.5_f64; (1.0 - peak_absorption * gamma_sq / (detuning_sq + gamma_sq)).clamp(0.0, 1.0)
}
pub fn cross_polarization_efficiency(&self) -> f64 {
let t_p = self.amplitude();
let t_s = 1.0_f64;
0.25 * (t_s - t_p).powi(2)
}
}
#[derive(Debug, Clone)]
pub struct VAntenna {
pub arm_length: f64,
pub gap: f64,
pub opening_angle_deg: f64,
pub orientation_deg: f64,
pub metal: PlasmonicMetal,
pub wavelength: f64,
}
impl VAntenna {
pub fn new(arm: f64, gap: f64, angle_deg: f64, orientation_deg: f64, wavelength: f64) -> Self {
Self {
arm_length: arm,
gap,
opening_angle_deg: angle_deg,
orientation_deg,
metal: PlasmonicMetal::Gold,
wavelength,
}
}
pub fn geometric_phase(&self) -> f64 {
2.0 * self.orientation_deg.to_radians()
}
fn lambda_sym(&self) -> f64 {
let n_eff = 1.22_f64; let half_angle = (self.opening_angle_deg / 2.0).to_radians();
2.0 * n_eff * self.arm_length * half_angle.cos()
}
fn lambda_asym(&self) -> f64 {
let n_eff = 1.22_f64;
let half_angle = (self.opening_angle_deg / 2.0).to_radians();
2.0 * n_eff * self.arm_length * half_angle.sin()
}
fn lorentzian_amplitude(&self, lambda_res: f64) -> f64 {
let omega = 2.0 * PI * SPEED_OF_LIGHT / self.wavelength;
let omega_res = 2.0 * PI * SPEED_OF_LIGHT / lambda_res;
let gamma = self.metal.damping_rate_rad_s();
let detuning_sq = (omega - omega_res).powi(2);
gamma / (detuning_sq + gamma * gamma).sqrt()
}
pub fn cross_pol_amplitude(&self) -> f64 {
let a_sym = self.lorentzian_amplitude(self.lambda_sym());
let a_asym = self.lorentzian_amplitude(self.lambda_asym());
(a_sym * a_asym).clamp(0.0, 1.0)
}
pub fn jones_matrix(&self) -> [[Complex64; 2]; 2] {
let theta = self.orientation_deg.to_radians();
let cos_t = theta.cos();
let sin_t = theta.sin();
let omega = 2.0 * PI * SPEED_OF_LIGHT / self.wavelength;
let lorentz = |lambda_res: f64| -> Complex64 {
let omega_res = 2.0 * PI * SPEED_OF_LIGHT / lambda_res;
let gamma = self.metal.damping_rate_rad_s();
Complex64::new(0.0, gamma) / Complex64::new(omega_res - omega, -gamma)
};
let t_s = lorentz(self.lambda_sym());
let t_a = lorentz(self.lambda_asym());
let j00 = t_s * cos_t * cos_t + t_a * sin_t * sin_t;
let j01 = (t_a - t_s) * Complex64::new(sin_t * cos_t, 0.0);
let j10 = j01;
let j11 = t_s * sin_t * sin_t + t_a * cos_t * cos_t;
[[j00, j01], [j10, j11]]
}
}
#[derive(Debug, Clone)]
pub struct HuygensMetasurface {
pub electric_polarizability: Complex64,
pub magnetic_polarizability: Complex64,
pub period: f64,
pub wavelength: f64,
}
impl HuygensMetasurface {
pub fn new(alpha_e: Complex64, alpha_m: Complex64, period: f64, wavelength: f64) -> Self {
Self {
electric_polarizability: alpha_e,
magnetic_polarizability: alpha_m,
period,
wavelength,
}
}
fn k0(&self) -> f64 {
2.0 * PI / self.wavelength
}
fn chi_ee(&self) -> Complex64 {
let eps0 = 8.854_187_817e-12_f64;
let a2 = self.period * self.period;
self.electric_polarizability / Complex64::new(eps0 * a2, 0.0)
}
fn chi_mm(&self) -> Complex64 {
let mu0 = 1.256_637_061_4e-6_f64;
let a2 = self.period * self.period;
self.magnetic_polarizability / Complex64::new(mu0 * a2, 0.0)
}
pub fn transmission(&self) -> Complex64 {
let k = self.k0();
let factor = Complex64::new(0.0, k / 2.0);
Complex64::new(1.0, 0.0) + factor * (self.chi_ee() + self.chi_mm())
}
pub fn reflection(&self) -> Complex64 {
let k = self.k0();
let factor = Complex64::new(0.0, k / 2.0);
factor * (self.chi_ee() - self.chi_mm())
}
pub fn kerker_condition_met(&self) -> bool {
let diff = (self.electric_polarizability - self.magnetic_polarizability).norm();
let sum = (self.electric_polarizability + self.magnetic_polarizability).norm();
if sum < 1e-30 {
return true;
}
diff / sum < 0.05
}
pub fn phase_coverage(&self) -> f64 {
if self.kerker_condition_met() {
2.0 * PI
} else {
let t = self.transmission();
(2.0 * t.arg().abs()).clamp(0.0, 2.0 * PI)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn tio2_pillar_fill_fraction() {
let p = DielectricPillar::new_tio2(80e-9, 600e-9, 400e-9, 532e-9);
let ff = p.fill_fraction();
assert_abs_diff_eq!(
ff,
PI * 80.0_f64.powi(2) / 400.0_f64.powi(2),
epsilon = 1e-6
);
}
#[test]
fn tio2_pillar_mie_resonances() {
let p = DielectricPillar::new_tio2(80e-9, 600e-9, 400e-9, 532e-9);
assert_abs_diff_eq!(
p.magnetic_dipole_resonance(),
2.4 * 2.4 * 600e-9,
epsilon = 1e-12
);
assert_abs_diff_eq!(
p.electric_dipole_resonance(),
1.7 * 2.4 * 600e-9,
epsilon = 1e-12
);
}
#[test]
fn si_pillar_transmission_is_unit_amplitude_bounded() {
let p = DielectricPillar::new_silicon(100e-9, 500e-9, 350e-9, 700e-9);
let amp = p.transmission().norm();
assert!(amp > 0.0 && amp < 2.0, "amplitude={amp}");
}
#[test]
fn plasmonic_gold_drude_permittivity_negative_real() {
let metal = PlasmonicMetal::Gold;
let omega = 2.0 * PI * SPEED_OF_LIGHT / 700e-9; let eps = metal.permittivity(omega);
assert!(eps.re < 0.0, "ε_r(Au, 700nm)={}", eps.re);
}
#[test]
fn v_antenna_geometric_phase_twice_orientation() {
let ant = VAntenna::new(150e-9, 30e-9, 60.0, 45.0, 800e-9);
assert_abs_diff_eq!(
ant.geometric_phase(),
2.0 * 45.0_f64.to_radians(),
epsilon = 1e-12
);
}
#[test]
fn huygens_kerker_condition_transmission_high() {
let alpha = Complex64::new(1e-33, 0.0);
let hs = HuygensMetasurface::new(alpha, alpha, 300e-9, 500e-9);
assert!(hs.kerker_condition_met(), "Kerker not detected");
let r = hs.reflection().norm();
assert!(r < 0.05, "|r|={r} (should be ~0 at Kerker condition)");
}
#[test]
fn huygens_phase_coverage_two_pi_at_kerker() {
let alpha = Complex64::new(1e-33, 0.0);
let hs = HuygensMetasurface::new(alpha, alpha, 300e-9, 500e-9);
assert_abs_diff_eq!(hs.phase_coverage(), 2.0 * PI, epsilon = 1e-10);
}
#[test]
fn jones_matrix_symmetry() {
let ant = VAntenna::new(150e-9, 30e-9, 60.0, 30.0, 800e-9);
let j = ant.jones_matrix();
let diff = (j[0][1] - j[1][0]).norm();
assert!(diff < 1e-20, "Jones matrix not symmetric: diff={diff}");
}
}