use std::f64::consts::PI;
pub const C_LIGHT: f64 = 2.99792458e8;
pub const H_PLANCK: f64 = 6.62607015e-34;
pub const HBAR: f64 = 1.054571817e-34;
pub const KB: f64 = 1.380649e-23;
#[derive(Debug, Clone)]
pub struct StedBeam {
pub power_w: f64,
pub wavelength_m: f64,
pub na: f64,
pub n_medium: f64,
}
impl StedBeam {
pub fn new(power_w: f64, wavelength_m: f64, na: f64, n_medium: f64) -> Self {
Self {
power_w,
wavelength_m,
na,
n_medium,
}
}
pub fn beam_waist_m(&self) -> f64 {
self.wavelength_m / (PI * self.na / self.n_medium)
}
pub fn peak_intensity_w_m2(&self) -> f64 {
let w0 = self.beam_waist_m();
let area = PI * w0 * w0;
2.0 * self.power_w / area * 0.54
}
pub fn donut_intensity(&self, r: f64, z: f64) -> f64 {
let w0 = self.beam_waist_m();
let z_r = PI * self.n_medium * w0 * w0 / self.wavelength_m;
let i_peak = self.peak_intensity_w_m2();
let radial = (PI * r / w0).sin().powi(2);
let axial = (-2.0 * z * z / (z_r * z_r)).exp();
i_peak * radial * radial * axial
}
pub fn effective_resolution(&self, confocal_res_m: f64, saturation_intensity: f64) -> f64 {
let i_peak = self.peak_intensity_w_m2();
let zeta = i_peak / saturation_intensity.max(f64::EPSILON);
confocal_res_m / (1.0 + zeta).sqrt()
}
pub fn saturation_factor(&self, saturation_intensity: f64) -> f64 {
self.peak_intensity_w_m2() / saturation_intensity.max(f64::EPSILON)
}
pub fn effective_psf_fwhm(&self, fluorophore: &Fluorophore, confocal_fwhm: f64) -> f64 {
let i_sat = fluorophore.saturation_intensity(self.wavelength_m);
self.effective_resolution(confocal_fwhm, i_sat)
}
}
#[derive(Debug, Clone)]
pub struct Fluorophore {
pub absorption_cross_section_m2: f64,
pub emission_wavelength_m: f64,
pub stimulated_emission_cross_section_m2: f64,
pub lifetime_ns: f64,
pub quantum_yield: f64,
}
impl Fluorophore {
pub fn new(
absorption_cross_section_m2: f64,
emission_wavelength_m: f64,
stimulated_emission_cross_section_m2: f64,
lifetime_ns: f64,
quantum_yield: f64,
) -> Self {
Self {
absorption_cross_section_m2,
emission_wavelength_m,
stimulated_emission_cross_section_m2,
lifetime_ns,
quantum_yield,
}
}
pub fn saturation_intensity(&self, sted_wavelength_m: f64) -> f64 {
let h_nu = H_PLANCK * C_LIGHT / sted_wavelength_m;
let tau_s = self.lifetime_ns * 1e-9;
let sigma_sted = self.stimulated_emission_cross_section_m2.max(f64::EPSILON);
h_nu / (sigma_sted * tau_s)
}
pub fn sted_depletion(&self, sted_intensity: f64) -> f64 {
let h_nu = H_PLANCK * C_LIGHT / self.emission_wavelength_m;
let tau_s = self.lifetime_ns * 1e-9;
let exponent = -self.stimulated_emission_cross_section_m2 * sted_intensity * tau_s / h_nu;
exponent.exp()
}
pub fn effective_brightness(&self, excitation_intensity: f64, sted_intensity: f64) -> f64 {
let h_nu_exc = H_PLANCK * C_LIGHT / self.emission_wavelength_m; let rate_abs = self.absorption_cross_section_m2 * excitation_intensity / h_nu_exc;
let depletion_factor = 1.0 - self.sted_depletion(sted_intensity);
self.quantum_yield * rate_abs * (1.0 - depletion_factor)
}
}
#[derive(Debug, Clone)]
pub struct Sted3d {
pub xy_sted: StedBeam,
pub z_sted: StedBeam,
}
impl Sted3d {
pub fn new(xy_sted: StedBeam, z_sted: StedBeam) -> Self {
Self { xy_sted, z_sted }
}
pub fn effective_3d_resolution(&self, fluorophore: &Fluorophore) -> (f64, f64, f64) {
let lambda_exc = self.xy_sted.wavelength_m * 0.85;
let na = self.xy_sted.na;
let n = self.xy_sted.n_medium;
let dr_conf = 0.51 * lambda_exc / na;
let dz_conf = 1.77 * lambda_exc * n / (na * na);
let i_sat_xy = fluorophore.saturation_intensity(self.xy_sted.wavelength_m);
let i_sat_z = fluorophore.saturation_intensity(self.z_sted.wavelength_m);
let dr_eff = self.xy_sted.effective_resolution(dr_conf, i_sat_xy);
let dz_eff = self.z_sted.effective_resolution(dz_conf, i_sat_z);
(dr_eff, dr_eff, dz_eff)
}
pub fn required_sted_power(&self, target_resolution_m: f64, fluorophore: &Fluorophore) -> f64 {
let lambda_exc = self.xy_sted.wavelength_m * 0.85;
let dr_conf = 0.51 * lambda_exc / self.xy_sted.na;
let i_sat = fluorophore.saturation_intensity(self.xy_sted.wavelength_m);
let ratio_sq = (dr_conf / target_resolution_m.max(f64::EPSILON)).powi(2);
let i_required = i_sat * (ratio_sq - 1.0).max(0.0);
let w0 = self.xy_sted.beam_waist_m();
let area = PI * w0 * w0;
i_required * area / (2.0 * 0.54)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn atto647n() -> Fluorophore {
Fluorophore::new(3.4e-20, 660e-9, 1.0e-20, 3.5, 0.65)
}
fn standard_sted() -> StedBeam {
StedBeam::new(0.200, 775e-9, 1.4, 1.515)
}
#[test]
fn test_saturation_intensity_physical_range() {
let fl = atto647n();
let i_sat = fl.saturation_intensity(775e-9);
assert!(
i_sat > 1e5 && i_sat < 1e11,
"I_sat = {} W/m² outside physical range",
i_sat
);
}
#[test]
fn test_sted_resolution_improves_with_power() {
let sted_low = StedBeam::new(0.010, 775e-9, 1.4, 1.515);
let sted_high = StedBeam::new(0.500, 775e-9, 1.4, 1.515);
let fl = atto647n();
let confocal_fwhm = 200e-9;
let res_low = sted_low.effective_psf_fwhm(&fl, confocal_fwhm);
let res_high = sted_high.effective_psf_fwhm(&fl, confocal_fwhm);
assert!(
res_high < res_low,
"Higher STED power should give finer resolution"
);
assert!(
res_low <= confocal_fwhm,
"STED resolution must be ≤ confocal"
);
}
#[test]
fn test_donut_zero_on_axis() {
let beam = standard_sted();
let i_axis = beam.donut_intensity(0.0, 0.0);
assert!(i_axis.abs() < 1e-30, "Donut must have zero on-axis (r=0)");
}
#[test]
fn test_donut_intensity_positive_off_axis() {
let beam = standard_sted();
let w0 = beam.beam_waist_m();
let i_ring = beam.donut_intensity(w0 * 0.5, 0.0);
assert!(i_ring > 0.0, "Donut intensity should be positive off-axis");
}
#[test]
fn test_depletion_increases_with_intensity() {
let fl = atto647n();
let eta_low = fl.sted_depletion(1e9);
let eta_high = fl.sted_depletion(1e11);
assert!(
eta_high < eta_low,
"Depletion should increase with STED intensity"
);
}
#[test]
fn test_3d_sted_axial_finer_than_confocal() {
let xy = StedBeam::new(0.100, 775e-9, 1.4, 1.515);
let z = StedBeam::new(0.150, 775e-9, 1.4, 1.515);
let scope = Sted3d::new(xy, z);
let fl = atto647n();
let (dx, _dy, dz) = scope.effective_3d_resolution(&fl);
let lambda_exc = 775e-9 * 0.85;
let dz_conf = 1.77 * lambda_exc * 1.515 / (1.4_f64.powi(2));
let dr_conf = 0.51 * lambda_exc / 1.4;
assert!(dx < dr_conf, "STED Δx should be < confocal");
assert!(dz < dz_conf, "STED Δz should be < confocal");
}
#[test]
fn test_required_power_monotone_with_resolution() {
let xy = StedBeam::new(0.1, 775e-9, 1.4, 1.515);
let z = StedBeam::new(0.1, 775e-9, 1.4, 1.515);
let scope = Sted3d::new(xy, z);
let fl = atto647n();
let p_50nm = scope.required_sted_power(50e-9, &fl);
let p_30nm = scope.required_sted_power(30e-9, &fl);
assert!(p_30nm > p_50nm, "Finer resolution requires more STED power");
}
#[test]
fn test_saturation_factor_dimensionless() {
let beam = standard_sted();
let fl = atto647n();
let zeta = beam.saturation_factor(fl.saturation_intensity(beam.wavelength_m));
assert!(
zeta.is_finite() && zeta >= 0.0,
"ζ must be finite and non-negative"
);
}
}