use std::f64::consts::PI;
const C0: f64 = 2.997_924_58e8;
#[derive(Debug, Clone)]
pub struct SersSubstrate {
pub hotspot_field_enhancement: f64,
pub hotspot_density: f64,
pub molecule_cross_section: f64,
pub laser_wavelength: f64,
pub raman_wavelength: f64,
}
impl SersSubstrate {
pub fn new_bowtie(enhancement: f64, density: f64, laser_wl: f64) -> Self {
let shift_cm1 = 1000.0_f64;
let k_laser = 1.0 / laser_wl * 1.0e-2; let k_raman = k_laser - shift_cm1;
let raman_wl = if k_raman > 0.0 {
1.0 / (k_raman * 1.0e2)
} else {
laser_wl * 1.05
};
Self {
hotspot_field_enhancement: enhancement,
hotspot_density: density,
molecule_cross_section: 1.0e-30, laser_wavelength: laser_wl,
raman_wavelength: raman_wl,
}
}
pub fn electromagnetic_ef(&self) -> f64 {
let g = self.hotspot_field_enhancement;
g * g * g * g
}
pub fn sers_cross_section(&self) -> f64 {
self.electromagnetic_ef() * self.molecule_cross_section
}
pub fn single_molecule_signal(
&self,
laser_intensity_w_per_m2: f64,
collection_efficiency: f64,
) -> f64 {
const HBAR: f64 = 1.054_571_817e-34;
let omega_laser = 2.0 * PI * C0 / self.laser_wavelength;
let photon_flux = laser_intensity_w_per_m2 / (HBAR * omega_laser);
self.sers_cross_section() * photon_flux * 4.0 * PI * collection_efficiency
}
pub fn detection_limit(
&self,
laser_intensity: f64,
collection_eff: f64,
snr: f64,
integration_time: f64,
) -> f64 {
let signal_per_molecule = self.single_molecule_signal(laser_intensity, collection_eff);
if signal_per_molecule < f64::EPSILON || integration_time < f64::EPSILON {
return f64::INFINITY;
}
let n_min_molecules = snr * snr / (signal_per_molecule * integration_time);
n_min_molecules / (self.hotspot_density.max(1.0))
}
pub fn chemical_ef() -> f64 {
30.0
}
pub fn total_ef(&self) -> f64 {
self.electromagnetic_ef() * Self::chemical_ef()
}
}
#[derive(Debug, Clone)]
pub struct TersSetup {
pub tip_radius_nm: f64,
pub tip_material: String,
pub gap_nm: f64,
pub laser_wavelength: f64,
pub excitation_power_uw: f64,
}
impl TersSetup {
pub fn new_gold_tip(radius_nm: f64, gap_nm: f64, wavelength: f64) -> Self {
Self {
tip_radius_nm: radius_nm,
tip_material: String::from("gold"),
gap_nm,
laser_wavelength: wavelength,
excitation_power_uw: 100.0,
}
}
fn tip_q(&self) -> f64 {
let omega_p = 1.37e16_f64;
let gamma = 1.22e14_f64;
let omega = 2.0 * PI * C0 / self.laser_wavelength;
let eps_re = -(omega_p * omega_p) / (omega * omega + gamma * gamma);
let eps_im = gamma * omega_p * omega_p / (omega * (omega * omega + gamma * gamma));
if eps_im.abs() < f64::EPSILON {
return 10.0;
}
(eps_re.abs() / eps_im).clamp(1.0, 50.0)
}
pub fn field_enhancement(&self) -> f64 {
let q = self.tip_q();
let lambda = self.laser_wavelength;
let r = self.tip_radius_nm * 1.0e-9;
let gap_factor = (self.tip_radius_nm / self.gap_nm).powf(1.0 / 3.0);
let base_fe = q * (lambda / (4.0 * PI * r)).sqrt();
(base_fe * gap_factor).min(1.0e4) }
pub fn spatial_resolution_nm(&self) -> f64 {
self.tip_radius_nm
}
pub fn ters_enhancement_factor(&self) -> f64 {
let fe = self.field_enhancement();
fe * fe * fe * fe
}
pub fn near_field_to_far_field_ratio_db(&self) -> f64 {
10.0 * self.ters_enhancement_factor().log10()
}
pub fn signal_contrast(&self) -> f64 {
let a_tip = PI * (self.tip_radius_nm * 1.0e-9).powi(2);
let a_spot = PI * (self.laser_wavelength / 2.0).powi(2);
if a_spot < f64::EPSILON {
return 1.0;
}
self.ters_enhancement_factor() * (a_tip / a_spot)
}
}
#[derive(Debug, Clone)]
pub enum NanotagType {
AuNanosphere { diameter_nm: f64 },
AuNanostar { core_nm: f64, arm_length_nm: f64 },
AuAggregates { n_particles: usize },
}
#[derive(Debug, Clone)]
pub struct SersNanotag {
pub particle_type: NanotagType,
pub molecule: String,
pub ef: f64,
pub cross_section_pm2: f64,
}
impl SersNanotag {
pub fn new_gold_nanosphere(diameter_nm: f64, molecule: &str) -> Self {
let ef_base = 1.0e6_f64; let size_factor = (30.0_f64 / diameter_nm).powi(2).clamp(0.1, 10.0);
let ef = ef_base * size_factor;
let sigma_pm2 = ef * 1.0e-30 * 4.0 * PI * 1.0e24;
Self {
particle_type: NanotagType::AuNanosphere { diameter_nm },
molecule: molecule.to_string(),
ef,
cross_section_pm2: sigma_pm2,
}
}
pub fn new_gold_nanostar(core_nm: f64, arm_nm: f64, molecule: &str) -> Self {
let n_arms = 6_usize; let ef_per_arm = 1.0e8_f64 * (arm_nm / 40.0).powi(2).clamp(0.1, 100.0);
let ef = ef_per_arm * n_arms as f64;
let sigma_pm2 = ef * 1.0e-30 * 4.0 * PI * 1.0e24;
Self {
particle_type: NanotagType::AuNanostar {
core_nm,
arm_length_nm: arm_nm,
},
molecule: molecule.to_string(),
ef,
cross_section_pm2: sigma_pm2,
}
}
pub fn detection_sensitivity_pm(&self) -> f64 {
if self.ef < f64::EPSILON {
return f64::INFINITY;
}
let ef_norm = self.ef / 1.0e6;
1.0 / ef_norm.sqrt()
}
pub fn multiplexing_capacity(&self) -> usize {
match &self.particle_type {
NanotagType::AuNanosphere { .. } => 30,
NanotagType::AuNanostar { .. } => 25, NanotagType::AuAggregates { n_particles } => {
(*n_particles).min(10)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_electromagnetic_ef_is_g4() {
let sub = SersSubstrate::new_bowtie(100.0, 1.0e12, 532.0e-9);
let ef = sub.electromagnetic_ef();
assert_abs_diff_eq!(ef, 100.0_f64.powi(4), epsilon = 1.0);
}
#[test]
fn test_sers_cross_section_gt_normal() {
let sub = SersSubstrate::new_bowtie(100.0, 1.0e12, 532.0e-9);
let sigma_sers = sub.sers_cross_section();
assert!(
sigma_sers > sub.molecule_cross_section,
"SERS cross-section must exceed normal Raman: {sigma_sers:.3e} vs {:.3e}",
sub.molecule_cross_section
);
}
#[test]
fn test_total_ef_includes_chemical() {
let sub = SersSubstrate::new_bowtie(100.0, 1.0e12, 532.0e-9);
let ef_em = sub.electromagnetic_ef();
let ef_total = sub.total_ef();
let ef_chem = SersSubstrate::chemical_ef();
assert_abs_diff_eq!(ef_total, ef_em * ef_chem, epsilon = 1.0);
}
#[test]
fn test_single_molecule_signal_positive() {
let sub = SersSubstrate::new_bowtie(200.0, 1.0e12, 532.0e-9);
let signal = sub.single_molecule_signal(1.0e9, 0.05);
assert!(
signal > 0.0,
"Single-molecule signal must be positive: {signal}"
);
}
#[test]
fn test_detection_limit_decreases_with_higher_ef() {
let sub_low = SersSubstrate::new_bowtie(100.0, 1.0e12, 532.0e-9);
let sub_high = SersSubstrate::new_bowtie(1000.0, 1.0e12, 532.0e-9);
let lod_low = sub_low.detection_limit(1.0e9, 0.05, 3.0, 1.0);
let lod_high = sub_high.detection_limit(1.0e9, 0.05, 3.0, 1.0);
assert!(
lod_high < lod_low,
"Higher EF should give lower detection limit: {lod_high:.3e} vs {lod_low:.3e}"
);
}
#[test]
fn test_chemical_ef_constant() {
let ef_chem = SersSubstrate::chemical_ef();
assert!(
(10.0..=1000.0).contains(&ef_chem),
"Chemical EF should be 10–1000: {ef_chem}"
);
}
#[test]
fn test_ters_field_enhancement_positive() {
let ters = TersSetup::new_gold_tip(20.0, 1.0, 633.0e-9);
let fe = ters.field_enhancement();
assert!(fe > 1.0, "TERS field enhancement must exceed 1: {fe}");
}
#[test]
fn test_ters_enhancement_factor_is_fe4() {
let ters = TersSetup::new_gold_tip(20.0, 1.0, 633.0e-9);
let fe = ters.field_enhancement();
let ef = ters.ters_enhancement_factor();
assert_abs_diff_eq!(ef, fe.powi(4), epsilon = 1.0);
}
#[test]
fn test_ters_spatial_resolution_equals_tip_radius() {
let ters = TersSetup::new_gold_tip(15.0, 1.0, 633.0e-9);
assert_abs_diff_eq!(ters.spatial_resolution_nm(), 15.0, epsilon = 1.0e-10);
}
#[test]
fn test_ters_near_field_db_positive_for_enhancement() {
let ters = TersSetup::new_gold_tip(20.0, 1.0, 633.0e-9);
let db = ters.near_field_to_far_field_ratio_db();
assert!(
db > 0.0,
"Near-field to far-field ratio in dB should be positive: {db}"
);
}
#[test]
fn test_ters_smaller_tip_gives_better_resolution() {
let ters1 = TersSetup::new_gold_tip(10.0, 1.0, 633.0e-9);
let ters2 = TersSetup::new_gold_tip(30.0, 1.0, 633.0e-9);
assert!(
ters1.spatial_resolution_nm() < ters2.spatial_resolution_nm(),
"Smaller tip → better resolution"
);
}
#[test]
fn test_gold_nanosphere_nanotag_ef_positive() {
let tag = SersNanotag::new_gold_nanosphere(40.0, "4-MBA");
assert!(tag.ef > 0.0, "Nanotag EF must be positive: {}", tag.ef);
}
#[test]
fn test_gold_nanostar_nanotag_ef_gt_sphere() {
let sphere = SersNanotag::new_gold_nanosphere(40.0, "4-MBA");
let star = SersNanotag::new_gold_nanostar(40.0, 50.0, "4-MBA");
assert!(
star.ef > sphere.ef,
"Nanostar EF ({:.3e}) should exceed sphere EF ({:.3e})",
star.ef,
sphere.ef
);
}
#[test]
fn test_detection_sensitivity_decreases_with_ef() {
let sphere = SersNanotag::new_gold_nanosphere(40.0, "4-MBA");
let star = SersNanotag::new_gold_nanostar(40.0, 50.0, "4-MBA");
let lod_sphere = sphere.detection_sensitivity_pm();
let lod_star = star.detection_sensitivity_pm();
assert!(
lod_star < lod_sphere,
"Nanostar should have lower LOD: {lod_star:.3} vs {lod_sphere:.3} pM"
);
}
#[test]
fn test_multiplexing_capacity_nanosphere() {
let tag = SersNanotag::new_gold_nanosphere(40.0, "4-MBA");
let cap = tag.multiplexing_capacity();
assert!(cap > 0 && cap <= 100, "Multiplexing capacity: {cap}");
}
#[test]
fn test_multiplexing_capacity_aggregate_limited_by_n() {
let agg = SersNanotag {
particle_type: NanotagType::AuAggregates { n_particles: 5 },
molecule: String::from("DTNB"),
ef: 1.0e10,
cross_section_pm2: 1.0e4,
};
assert_eq!(agg.multiplexing_capacity(), 5);
}
}