use std::f64::consts::PI;
const C0: f64 = 2.997_924_58e8;
const KB: f64 = 1.380_649e-23;
const EPS0: f64 = 8.854_187_817e-12;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrapAxis {
Lateral,
Axial,
}
#[derive(Debug, Clone)]
pub struct OpticalTweezers {
pub beam_power_w: f64,
pub wavelength_nm: f64,
pub numerical_aperture: f64,
pub medium_index: f64,
}
impl OpticalTweezers {
pub fn new(power_w: f64, lambda_nm: f64, na: f64, n_medium: f64) -> Self {
Self {
beam_power_w: power_w,
wavelength_nm: lambda_nm,
numerical_aperture: na,
medium_index: n_medium,
}
}
pub fn beam_waist_nm(&self) -> f64 {
self.wavelength_nm / (PI * self.numerical_aperture)
}
pub fn peak_intensity_w_per_m2(&self) -> f64 {
let w0_m = self.beam_waist_nm() * 1.0e-9;
2.0 * self.beam_power_w / (PI * w0_m * w0_m)
}
pub fn rayleigh_range_nm(&self) -> f64 {
let w0_nm = self.beam_waist_nm();
PI * w0_nm * w0_nm * self.medium_index / self.wavelength_nm
}
fn clausius_mossotti(m_relative: f64) -> f64 {
let m2 = m_relative * m_relative;
(m2 - 1.0) / (m2 + 2.0)
}
fn scattering_cross_section_m2(radius_m: f64, lambda_m: f64, m_relative: f64) -> f64 {
let k_cm = Self::clausius_mossotti(m_relative);
let r6 = radius_m.powi(6);
let lambda4 = lambda_m.powi(4);
128.0 * PI.powi(5) * r6 / (3.0 * lambda4) * k_cm * k_cm
}
fn intensity_at_position(&self, pos_nm: [f64; 3]) -> f64 {
let w0_nm = self.beam_waist_nm();
let z_r = self.rayleigh_range_nm();
let x = pos_nm[0];
let y = pos_nm[1];
let z = pos_nm[2];
let r_sq = x * x + y * y;
let wz_sq = w0_nm * w0_nm * (1.0 + (z / z_r).powi(2));
let i0 = self.peak_intensity_w_per_m2();
i0 * (w0_nm * w0_nm / wz_sq) * (-2.0 * r_sq / wz_sq).exp()
}
fn intensity_gradient_si(&self, pos_nm: [f64; 3]) -> [f64; 3] {
let delta_nm = self.beam_waist_nm() * 1.0e-4; let delta_m = delta_nm * 1.0e-9;
let mut grad = [0.0f64; 3];
for axis in 0..3 {
let mut pos_plus = pos_nm;
let mut pos_minus = pos_nm;
pos_plus[axis] += delta_nm;
pos_minus[axis] -= delta_nm;
let i_plus = self.intensity_at_position(pos_plus);
let i_minus = self.intensity_at_position(pos_minus);
grad[axis] = (i_plus - i_minus) / (2.0 * delta_m);
}
grad
}
pub fn gradient_force_n(
&self,
particle_radius_nm: f64,
particle_index: f64,
position_nm: [f64; 3],
) -> [f64; 3] {
let r_m = particle_radius_nm * 1.0e-9;
let m = particle_index / self.medium_index;
let k_cm = Self::clausius_mossotti(m);
let prefactor = 2.0 * PI * self.medium_index * r_m.powi(3) * k_cm / C0;
let grad_i = self.intensity_gradient_si(position_nm);
[
prefactor * grad_i[0],
prefactor * grad_i[1],
prefactor * grad_i[2],
]
}
pub fn scattering_force_n(
&self,
particle_radius_nm: f64,
particle_index: f64,
intensity: f64,
) -> f64 {
let r_m = particle_radius_nm * 1.0e-9;
let lambda_m = self.wavelength_nm * 1.0e-9 / self.medium_index;
let m = particle_index / self.medium_index;
let sigma = Self::scattering_cross_section_m2(r_m, lambda_m, m);
self.medium_index * sigma * intensity / C0
}
pub fn total_force_n(
&self,
particle_radius_nm: f64,
particle_index: f64,
position_nm: [f64; 3],
) -> [f64; 3] {
let f_grad = self.gradient_force_n(particle_radius_nm, particle_index, position_nm);
let intensity = self.intensity_at_position(position_nm);
let f_scat = self.scattering_force_n(particle_radius_nm, particle_index, intensity);
[f_grad[0], f_grad[1], f_grad[2] + f_scat]
}
pub fn trap_stiffness_n_per_m(
&self,
particle_radius_nm: f64,
particle_index: f64,
axis: TrapAxis,
) -> f64 {
let delta_nm = self.beam_waist_nm() * 0.001; let delta_m = delta_nm * 1.0e-9;
match axis {
TrapAxis::Lateral => {
let pos_plus = [delta_nm, 0.0, 0.0];
let pos_minus = [-delta_nm, 0.0, 0.0];
let f_plus = self.gradient_force_n(particle_radius_nm, particle_index, pos_plus);
let f_minus = self.gradient_force_n(particle_radius_nm, particle_index, pos_minus);
-(f_plus[0] - f_minus[0]) / (2.0 * delta_m)
}
TrapAxis::Axial => {
let pos_plus = [0.0, 0.0, delta_nm];
let pos_minus = [0.0, 0.0, -delta_nm];
let f_plus = self.total_force_n(particle_radius_nm, particle_index, pos_plus);
let f_minus = self.total_force_n(particle_radius_nm, particle_index, pos_minus);
-(f_plus[2] - f_minus[2]) / (2.0 * delta_m)
}
}
}
pub fn trap_depth_kt(&self, particle_radius_nm: f64, particle_index: f64) -> f64 {
let r_m = particle_radius_nm * 1.0e-9;
let m = particle_index / self.medium_index;
let k_cm = Self::clausius_mossotti(m);
let i0 = self.peak_intensity_w_per_m2();
let prefactor = 2.0 * PI * self.medium_index * r_m.powi(3) * k_cm / C0;
let u_joules = prefactor.abs() * i0;
let kt = KB * 300.0;
u_joules / kt
}
pub fn escape_force_n(&self, particle_radius_nm: f64, particle_index: f64) -> f64 {
let z_r = self.rayleigh_range_nm();
let z_test = -z_r / 2.0_f64.sqrt();
let f = self.gradient_force_n(particle_radius_nm, particle_index, [0.0, 0.0, z_test]);
f[2].abs()
}
pub fn thermal_fluctuation_nm(&self, trap_stiffness: f64, temperature_k: f64) -> f64 {
let x_rms_m = (KB * temperature_k / trap_stiffness.abs()).sqrt();
x_rms_m * 1.0e9
}
pub fn equilibrium_position_nm(&self, particle_radius_nm: f64, particle_index: f64) -> f64 {
let z_r = self.rayleigh_range_nm();
let mut z_low = 0.0_f64;
let mut z_high = 2.0 * z_r;
for _ in 0..64 {
let z_mid = (z_low + z_high) / 2.0;
let f = self.total_force_n(particle_radius_nm, particle_index, [0.0, 0.0, z_mid]);
let fz = f[2];
if fz > 0.0 {
z_low = z_mid;
} else {
z_high = z_mid;
}
}
(z_low + z_high) / 2.0
}
}
#[derive(Debug, Clone)]
pub struct OpticalBinding {
pub tweezers: OpticalTweezers,
}
impl OpticalBinding {
pub fn new(tweezers: OpticalTweezers) -> Self {
Self { tweezers }
}
pub fn binding_force_n(
&self,
particle_radius_nm: f64,
particle_index: f64,
separation_nm: f64,
) -> f64 {
let r_m = particle_radius_nm * 1.0e-9;
let m = particle_index / self.tweezers.medium_index;
let k_cm = OpticalTweezers::clausius_mossotti(m);
let alpha = 4.0 * PI * EPS0 * self.tweezers.medium_index.powi(2) * r_m.powi(3) * k_cm;
let lambda_m = self.tweezers.wavelength_nm * 1.0e-9;
let k_med = 2.0 * PI * self.tweezers.medium_index / lambda_m;
let d_m = separation_nm * 1.0e-9;
let i0 = self.tweezers.peak_intensity_w_per_m2();
let e0_sq = 2.0 * i0 / (C0 * EPS0 * self.tweezers.medium_index);
let f_scale =
alpha.powi(2) * e0_sq * k_med.powi(3) / (4.0 * PI * EPS0 * self.tweezers.medium_index);
let kd = k_med * d_m;
if kd < 1.0e-10 {
return 0.0;
}
f_scale * (kd).sin() / (kd * kd)
}
pub fn preferred_separation_nm(&self) -> f64 {
self.tweezers.wavelength_nm / (2.0 * self.tweezers.medium_index)
}
}
#[derive(Debug, Clone)]
pub struct DualBeamTrap {
pub beam1: OpticalTweezers,
pub beam2: OpticalTweezers,
}
impl DualBeamTrap {
pub fn symmetric(power_w: f64, lambda_nm: f64, na: f64, n_medium: f64) -> Self {
let beam1 = OpticalTweezers::new(power_w, lambda_nm, na, n_medium);
let beam2 = OpticalTweezers::new(power_w, lambda_nm, na, n_medium);
Self { beam1, beam2 }
}
pub fn axial_force_n(&self, particle_radius_nm: f64, particle_index: f64, z_nm: f64) -> f64 {
let pos1 = [0.0, 0.0, z_nm];
let f1 = self
.beam1
.total_force_n(particle_radius_nm, particle_index, pos1);
let pos2 = [0.0, 0.0, -z_nm];
let f2 = self
.beam2
.total_force_n(particle_radius_nm, particle_index, pos2);
f1[2] - f2[2]
}
pub fn equilibrium_z_nm(&self, particle_radius_nm: f64, particle_index: f64) -> f64 {
let z_r = self.beam1.rayleigh_range_nm();
let mut z_low = -z_r;
let mut z_high = z_r;
for _ in 0..64 {
let z_mid = (z_low + z_high) / 2.0;
let fz = self.axial_force_n(particle_radius_nm, particle_index, z_mid);
if fz > 0.0 {
z_low = z_mid;
} else {
z_high = z_mid;
}
}
(z_low + z_high) / 2.0
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
fn standard_tweezers() -> OpticalTweezers {
OpticalTweezers::new(0.1, 1064.0, 1.2, 1.33)
}
#[test]
fn test_beam_waist() {
let tw = standard_tweezers();
let w0 = tw.beam_waist_nm();
let expected = 1064.0 / (PI * 1.2);
assert_abs_diff_eq!(w0, expected, epsilon = 1.0e-10);
assert!((w0 - 282.0).abs() < 1.0);
}
#[test]
fn test_peak_intensity() {
let tw = standard_tweezers();
let w0_m = tw.beam_waist_nm() * 1.0e-9;
let i0 = tw.peak_intensity_w_per_m2();
let expected = 2.0 * tw.beam_power_w / (PI * w0_m * w0_m);
assert_abs_diff_eq!(i0, expected, epsilon = 1.0);
}
#[test]
fn test_gradient_force_at_center_zero() {
let tw = standard_tweezers();
let f = tw.gradient_force_n(100.0, 1.5, [0.0, 0.0, 0.0]);
assert!(f[0].abs() < 1.0e-25, "Fx at center should be zero");
assert!(f[1].abs() < 1.0e-25, "Fy at center should be zero");
assert!(f[2].abs() < 1.0e-25, "Fz at center should be zero");
}
#[test]
fn test_trap_stiffness_positive() {
let tw = standard_tweezers();
let k_lat = tw.trap_stiffness_n_per_m(100.0, 1.5, TrapAxis::Lateral);
assert!(
k_lat > 0.0,
"Lateral stiffness must be positive, got {}",
k_lat
);
}
#[test]
fn test_trap_depth_in_kt() {
let tw = standard_tweezers();
let depth = tw.trap_depth_kt(100.0, 1.59);
assert!(
depth > 1.0,
"Trap depth should exceed 1 kT, got {} kT",
depth
);
}
#[test]
fn test_thermal_fluctuation_decreases_with_stiffness() {
let tw = standard_tweezers();
let x1 = tw.thermal_fluctuation_nm(1.0e-5, 300.0);
let x2 = tw.thermal_fluctuation_nm(1.0e-4, 300.0);
let x3 = tw.thermal_fluctuation_nm(1.0e-3, 300.0);
assert!(x1 > x2, "Higher stiffness should reduce fluctuations");
assert!(x2 > x3, "Higher stiffness should reduce fluctuations");
}
#[test]
fn test_dual_beam_equilibrium_at_center() {
let trap = DualBeamTrap::symmetric(0.1, 1064.0, 1.2, 1.33);
let z_eq = trap.equilibrium_z_nm(100.0, 1.59);
assert!(
z_eq.abs() < 5.0,
"Symmetric dual-beam equilibrium should be near z=0 nm, got {} nm",
z_eq
);
}
#[test]
fn test_rayleigh_range_positive() {
let tw = standard_tweezers();
let z_r = tw.rayleigh_range_nm();
assert!(z_r > 0.0, "Rayleigh range must be positive");
}
#[test]
fn test_clausius_mossotti_sign() {
let k_positive = OpticalTweezers::clausius_mossotti(1.2); let k_negative = OpticalTweezers::clausius_mossotti(0.8); assert!(k_positive > 0.0, "K(m>1) should be positive");
assert!(k_negative < 0.0, "K(m<1) should be negative");
}
}