use num_complex::Complex64;
use std::f64::consts::PI;
const C0: f64 = 2.997_924_58e8; const EPS0: f64 = 8.854_187_817e-12; const K_B: f64 = 1.380_649e-23;
#[derive(Debug, Clone)]
pub struct NanoparticleForce {
pub radius: f64,
pub n_particle: Complex64,
pub n_medium: f64,
pub wavelength: f64,
pub power: f64,
}
impl NanoparticleForce {
pub fn new(radius: f64, n_particle: Complex64, n_medium: f64, wavelength: f64) -> Self {
Self {
radius,
n_particle,
n_medium,
wavelength,
power: 1.0e-3, }
}
fn relative_index(&self) -> Complex64 {
self.n_particle / Complex64::new(self.n_medium, 0.0)
}
fn polarizability_static(&self) -> Complex64 {
let a = self.radius;
let m = self.relative_index();
let m2 = m * m;
let cm = (m2 - Complex64::new(1.0, 0.0)) / (m2 + Complex64::new(2.0, 0.0));
Complex64::new(4.0 * PI * EPS0 * a * a * a, 0.0) * cm
}
pub fn polarizability(&self) -> Complex64 {
let k = 2.0 * PI * self.n_medium / self.wavelength;
let alpha0 = self.polarizability_static();
let k3 = k * k * k;
let rr = Complex64::new(0.0, -k3 / (6.0 * PI * EPS0));
let correction = Complex64::new(1.0, 0.0) - rr * alpha0;
if correction.norm() < f64::EPSILON {
return alpha0;
}
alpha0 / correction
}
pub fn gradient_force_per_intensity_gradient(&self) -> f64 {
let alpha = self.polarizability();
alpha.re / (2.0 * self.n_medium * C0 * EPS0)
}
pub fn scattering_cross_section(&self) -> f64 {
let k = 2.0 * PI * self.n_medium / self.wavelength;
let alpha = self.polarizability();
let k4 = k * k * k * k;
k4 * alpha.norm_sqr() / (6.0 * PI * EPS0 * EPS0)
}
pub fn absorption_cross_section(&self) -> f64 {
let k = 2.0 * PI * self.n_medium / self.wavelength;
let alpha = self.polarizability();
(k * alpha.im / EPS0).max(0.0)
}
pub fn extinction_cross_section(&self) -> f64 {
self.absorption_cross_section() + self.scattering_cross_section()
}
pub fn scattering_force(&self, intensity_w_per_m2: f64) -> f64 {
self.n_medium * self.extinction_cross_section() * intensity_w_per_m2 / C0
}
pub fn trap_stiffness_n_per_m(&self, beam_waist: f64, peak_intensity: f64) -> f64 {
let alpha = self.polarizability();
if beam_waist < f64::EPSILON {
return 0.0;
}
alpha.re * peak_intensity / (2.0 * self.n_medium * C0 * EPS0 * beam_waist * beam_waist)
}
pub fn escape_force_pn(&self, beam_waist: f64, peak_intensity: f64) -> f64 {
let k_trap = self.trap_stiffness_n_per_m(beam_waist, peak_intensity);
let f_esc_n = 0.43 * k_trap * beam_waist;
f_esc_n * 1.0e12 }
}
#[derive(Debug, Clone)]
pub struct NearFieldForce {
pub field_enhancement: f64,
pub gradient_length: f64,
pub wavelength: f64,
}
impl NearFieldForce {
pub fn new(enhancement: f64, gradient_length_nm: f64, wavelength: f64) -> Self {
Self {
field_enhancement: enhancement,
gradient_length: gradient_length_nm * 1.0e-9,
wavelength,
}
}
pub fn enhanced_gradient_force(&self, particle: &NanoparticleForce, power: f64) -> f64 {
let i_inc = power / (self.wavelength * self.wavelength);
let grad_i_local =
self.field_enhancement * self.field_enhancement * i_inc / self.gradient_length;
let coeff = particle.gradient_force_per_intensity_gradient();
coeff * grad_i_local
}
pub fn optical_binding_force(&self, separation: f64, particle_radius: f64) -> f64 {
if separation < f64::EPSILON {
return 0.0;
}
let k = 2.0 * PI / self.wavelength;
let kd = k * separation;
let r6 = (particle_radius * 2.0).powi(6);
let alpha0 = EPS0 * particle_radius.powi(3); let f0 = alpha0 * alpha0 * (2.0 * PI * C0 / self.wavelength) / (EPS0 * EPS0 * r6 * C0 * C0);
f0 * kd.sin() / kd
}
}
#[derive(Debug, Clone)]
pub struct NtaSimulator {
pub particle_radius_nm: f64,
pub medium_viscosity: f64,
pub temperature_k: f64,
}
impl NtaSimulator {
pub fn new(radius_nm: f64) -> Self {
Self {
particle_radius_nm: radius_nm,
medium_viscosity: 8.90e-4, temperature_k: 298.15, }
}
pub fn diffusion_coefficient_m2_per_s(&self) -> f64 {
let r = self.particle_radius_nm * 1.0e-9;
let eta = self.medium_viscosity;
let t = self.temperature_k;
K_B * t / (6.0 * PI * eta * r)
}
pub fn msd_at_time(&self, time_s: f64) -> f64 {
4.0 * self.diffusion_coefficient_m2_per_s() * time_s
}
pub fn radius_from_diffusion(d_m2_s: f64, viscosity: f64, temp_k: f64) -> f64 {
if d_m2_s < f64::EPSILON {
return 0.0;
}
K_B * temp_k / (6.0 * PI * viscosity * d_m2_s)
}
pub fn sedimentation_velocity_nm_per_s(
&self,
particle_density: f64,
medium_density: f64,
) -> f64 {
const G: f64 = 9.80665; let r = self.particle_radius_nm * 1.0e-9;
let eta = self.medium_viscosity;
let delta_rho = particle_density - medium_density;
let v_m_per_s = 2.0 * r * r * delta_rho * G / (9.0 * eta);
v_m_per_s * 1.0e9 }
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
fn gold_particle_532nm() -> NanoparticleForce {
NanoparticleForce::new(20.0e-9, Complex64::new(0.47, 2.40), 1.33, 532.0e-9)
}
fn silica_particle_1064nm() -> NanoparticleForce {
NanoparticleForce::new(100.0e-9, Complex64::new(1.45, 0.0), 1.33, 1064.0e-9)
}
#[test]
fn test_polarizability_nonzero() {
let p = gold_particle_532nm();
let alpha = p.polarizability();
assert!(
alpha.norm() > 0.0,
"Polarizability must be nonzero: {:?}",
alpha
);
}
#[test]
fn test_polarizability_static_scales_as_r3() {
let p1 = NanoparticleForce::new(10.0e-9, Complex64::new(1.45, 0.0), 1.0, 1064.0e-9);
let p2 = NanoparticleForce::new(20.0e-9, Complex64::new(1.45, 0.0), 1.0, 1064.0e-9);
let a1 = p1.polarizability_static().norm();
let a2 = p2.polarizability_static().norm();
assert_abs_diff_eq!(a2 / a1, 8.0, epsilon = 0.01);
}
#[test]
fn test_extinction_ge_absorption() {
let p = gold_particle_532nm();
let sigma_ext = p.extinction_cross_section();
let sigma_abs = p.absorption_cross_section();
assert!(
sigma_ext >= sigma_abs,
"σ_ext ({sigma_ext:.3e}) must be ≥ σ_abs ({sigma_abs:.3e})"
);
}
#[test]
fn test_scattering_cross_section_positive() {
let p = gold_particle_532nm();
let sigma_scat = p.scattering_cross_section();
assert!(sigma_scat > 0.0, "σ_scat must be positive: {sigma_scat}");
}
#[test]
fn test_absorption_cross_section_positive_for_lossy() {
let p = gold_particle_532nm();
let sigma_abs = p.absorption_cross_section();
assert!(
sigma_abs > 0.0,
"σ_abs must be positive for gold: {sigma_abs}"
);
}
#[test]
fn test_gradient_force_positive_for_dielectric() {
let p = silica_particle_1064nm();
let coeff = p.gradient_force_per_intensity_gradient();
assert!(
coeff > 0.0,
"Gradient force coeff should be positive for dielectric: {coeff}"
);
}
#[test]
fn test_scattering_force_positive() {
let p = gold_particle_532nm();
let f = p.scattering_force(1.0e9); assert!(f > 0.0, "Scattering force must be positive: {f}");
}
#[test]
fn test_trap_stiffness_positive_dielectric() {
let p = silica_particle_1064nm();
let k_trap = p.trap_stiffness_n_per_m(1.0e-6, 1.0e10);
assert!(k_trap > 0.0, "Trap stiffness must be positive: {k_trap}");
}
#[test]
fn test_escape_force_pn_positive() {
let p = silica_particle_1064nm();
let f_esc = p.escape_force_pn(1.0e-6, 1.0e10);
assert!(f_esc > 0.0, "Escape force must be positive: {f_esc}");
}
#[test]
fn test_near_field_force_enhanced_over_far_field() {
let near = NearFieldForce::new(100.0, 5.0, 800.0e-9);
let far = NearFieldForce::new(1.0, 5.0, 800.0e-9);
let p = silica_particle_1064nm();
let f_near = near.enhanced_gradient_force(&p, 1.0e-3);
let f_far = far.enhanced_gradient_force(&p, 1.0e-3);
assert!(
f_near > f_far * 100.0,
"Near-field force ({f_near:.3e}) should vastly exceed far-field ({f_far:.3e})"
);
}
#[test]
fn test_diffusion_coefficient_100nm() {
let nta = NtaSimulator::new(100.0);
let d = nta.diffusion_coefficient_m2_per_s();
assert!(
d > 1.0e-12 && d < 1.0e-10,
"D for 100 nm particle should be ~4e-12 m²/s: {d:.3e}"
);
}
#[test]
fn test_diffusion_scales_inversely_with_radius() {
let nta1 = NtaSimulator::new(50.0);
let nta2 = NtaSimulator::new(100.0);
let d1 = nta1.diffusion_coefficient_m2_per_s();
let d2 = nta2.diffusion_coefficient_m2_per_s();
assert_abs_diff_eq!(d1 / d2, 2.0, epsilon = 1.0e-10);
}
#[test]
fn test_msd_linear_in_time() {
let nta = NtaSimulator::new(100.0);
let d = nta.diffusion_coefficient_m2_per_s();
let msd_1s = nta.msd_at_time(1.0);
let msd_2s = nta.msd_at_time(2.0);
assert_abs_diff_eq!(msd_1s, 4.0 * d, epsilon = 1.0e-20);
assert_abs_diff_eq!(msd_2s, 8.0 * d, epsilon = 1.0e-20);
}
#[test]
fn test_radius_from_diffusion_roundtrip() {
let nta = NtaSimulator::new(75.0);
let d = nta.diffusion_coefficient_m2_per_s();
let r_recovered =
NtaSimulator::radius_from_diffusion(d, nta.medium_viscosity, nta.temperature_k);
assert_abs_diff_eq!(r_recovered * 1.0e9, 75.0, epsilon = 1.0e-6);
}
#[test]
fn test_sedimentation_positive_for_denser_particle() {
let nta = NtaSimulator::new(100.0);
let v = nta.sedimentation_velocity_nm_per_s(19300.0, 1000.0);
assert!(
v > 0.0,
"Sedimentation velocity should be positive for sinking particle: {v}"
);
}
#[test]
fn test_sedimentation_zero_for_matched_density() {
let nta = NtaSimulator::new(100.0);
let v = nta.sedimentation_velocity_nm_per_s(1000.0, 1000.0);
assert_abs_diff_eq!(v, 0.0, epsilon = 1.0e-30);
}
}