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;
const AVOGADRO: f64 = 6.02214076e23;
#[derive(Debug, Clone)]
pub struct FcsSetup {
pub beam_waist_xy_m: f64,
pub beam_waist_z_m: f64,
pub wavelength_m: f64,
}
impl FcsSetup {
pub fn new(beam_waist_xy_m: f64, beam_waist_z_m: f64, wavelength_m: f64) -> Self {
Self {
beam_waist_xy_m,
beam_waist_z_m,
wavelength_m,
}
}
pub fn kappa(&self) -> f64 {
self.beam_waist_z_m / self.beam_waist_xy_m.max(f64::EPSILON)
}
pub fn diffusion_time(&self, diffusion_coeff_m2_s: f64) -> f64 {
self.beam_waist_xy_m * self.beam_waist_xy_m / (4.0 * diffusion_coeff_m2_s.max(f64::EPSILON))
}
pub fn autocorrelation(&self, tau_s: f64, n_molecules: f64, diffusion_coeff: f64) -> f64 {
let tau_d = self.diffusion_time(diffusion_coeff);
let kappa = self.kappa();
let lat = 1.0 + tau_s / tau_d;
let ax = 1.0 + tau_s / (kappa * kappa * tau_d);
(1.0 / n_molecules.max(f64::EPSILON)) * (1.0 / lat) * (1.0 / ax.sqrt())
}
pub fn g_zero(&self, n_molecules: f64) -> f64 {
1.0 / n_molecules.max(f64::EPSILON)
}
pub fn focal_volume_m3(&self) -> f64 {
PI.powf(1.5) * self.beam_waist_xy_m * self.beam_waist_xy_m * self.beam_waist_z_m
}
pub fn concentration_from_g0(&self, g0: f64) -> f64 {
let n = 1.0 / g0.max(f64::EPSILON);
let v_eff = self.focal_volume_m3();
n / (v_eff * AVOGADRO * 1000.0)
}
pub fn autocorrelation_with_triplet(
&self,
tau_s: f64,
n_molecules: f64,
diffusion_coeff: f64,
triplet_fraction: f64,
triplet_time_s: f64,
) -> f64 {
let g_diff = self.autocorrelation(tau_s, n_molecules, diffusion_coeff);
let t = triplet_fraction.clamp(0.0, 0.9999);
let triplet_factor =
(1.0 - t + t * (-tau_s / triplet_time_s.max(f64::EPSILON)).exp()) / (1.0 - t);
g_diff * triplet_factor
}
pub fn molecular_brightness(&self, avg_intensity_counts_s: f64, n_molecules: f64) -> f64 {
avg_intensity_counts_s / n_molecules.max(f64::EPSILON)
}
}
#[derive(Debug, Clone)]
pub struct FccsMeasurement {
pub fcs_ch1: FcsSetup,
pub fcs_ch2: FcsSetup,
pub overlap: f64,
}
impl FccsMeasurement {
pub fn new(fcs_ch1: FcsSetup, fcs_ch2: FcsSetup, overlap: f64) -> Self {
Self {
fcs_ch1,
fcs_ch2,
overlap: overlap.clamp(0.0, 1.0),
}
}
pub fn cross_correlation_amplitude(&self, n_ch1: f64, n_ch2: f64, n_double: f64) -> f64 {
let denom = n_ch1 * n_ch2;
if denom < f64::EPSILON {
return 0.0;
}
n_double / denom * self.overlap
}
pub fn bound_fraction(&self, g_cross: f64, n_total: f64) -> f64 {
(g_cross * n_total).clamp(0.0, 1.0)
}
}
#[derive(Debug, Clone)]
pub struct FcsFitter {
pub n_molecules_init: f64,
pub diffusion_coeff_init: f64,
}
impl FcsFitter {
pub fn new(n_molecules_init: f64, diffusion_coeff_init: f64) -> Self {
Self {
n_molecules_init,
diffusion_coeff_init,
}
}
pub fn fit(&self, setup: &FcsSetup, tau_data: &[f64], g_data: &[f64]) -> (f64, f64, f64) {
if tau_data.is_empty() || g_data.is_empty() || tau_data.len() != g_data.len() {
return (self.n_molecules_init, self.diffusion_coeff_init, f64::NAN);
}
let mut n_fit = self.n_molecules_init.max(f64::EPSILON);
let mut d_fit = self.diffusion_coeff_init.max(f64::EPSILON);
let n_iter = 200_usize;
let step_init = 0.1_f64; let mut step = step_init;
let chi2 = |n: f64, d: f64| -> f64 {
tau_data
.iter()
.zip(g_data.iter())
.map(|(&tau, &g_meas)| {
let g_model = setup.autocorrelation(tau, n, d);
let residual = g_meas - g_model;
residual * residual
})
.sum()
};
let mut best_chi2 = chi2(n_fit, d_fit);
for _iter in 0..n_iter {
let candidates = [
(n_fit * (1.0 + step), d_fit),
(n_fit * (1.0 - step), d_fit),
(n_fit, d_fit * (1.0 + step)),
(n_fit, d_fit * (1.0 - step)),
(n_fit * (1.0 + step), d_fit * (1.0 + step)),
(n_fit * (1.0 - step), d_fit * (1.0 - step)),
];
let mut improved = false;
for &(n_try, d_try) in &candidates {
if n_try <= 0.0 || d_try <= 0.0 {
continue;
}
let c = chi2(n_try, d_try);
if c < best_chi2 {
best_chi2 = c;
n_fit = n_try;
d_fit = d_try;
improved = true;
}
}
if !improved {
step *= 0.5;
}
if step < 1e-10 {
break;
}
}
(n_fit, d_fit, best_chi2)
}
pub fn hydrodynamic_radius(
&self,
diffusion_coeff: f64,
viscosity_pa_s: f64,
temperature_k: f64,
) -> f64 {
KB * temperature_k / (6.0 * PI * viscosity_pa_s * diffusion_coeff.max(f64::EPSILON))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn gfp_setup() -> FcsSetup {
FcsSetup::new(200e-9, 1000e-9, 488e-9)
}
#[test]
fn test_focal_volume_femtolitre_range() {
let setup = gfp_setup();
let v = setup.focal_volume_m3();
assert!(
v > 1e-20 && v < 1e-17,
"Focal volume {} m³ outside 0.01–10 fL",
v
);
}
#[test]
fn test_g_zero_equals_one_over_n() {
let setup = gfp_setup();
let n = 5.0;
let g0 = setup.g_zero(n);
assert!(
(g0 - 1.0 / n).abs() < 1e-15,
"G(0) should equal 1/N, got {}",
g0
);
}
#[test]
fn test_autocorrelation_decreases_with_lag() {
let setup = gfp_setup();
let d = 87e-12;
let n = 3.0;
let g0 = setup.autocorrelation(0.0, n, d);
let g_long = setup.autocorrelation(1.0, n, d); assert!(
g0 > g_long,
"G(τ) should decrease from G(0) to 0 at long lags"
);
}
#[test]
fn test_concentration_from_g0_n_m_range() {
let setup = gfp_setup();
let g0 = setup.g_zero(3.0);
let c_mol_per_l = setup.concentration_from_g0(g0);
assert!(
c_mol_per_l > 1e-9 && c_mol_per_l < 1e-5,
"Concentration {} mol/L outside nM–μM range",
c_mol_per_l
);
}
#[test]
fn test_triplet_correction_raises_g() {
let setup = gfp_setup();
let d = 87e-12;
let n = 3.0;
let tau = 1e-6_f64; let g_no_triplet = setup.autocorrelation(tau, n, d);
let g_triplet = setup.autocorrelation_with_triplet(tau, n, d, 0.2, 5e-6);
assert!(
g_triplet > g_no_triplet,
"Triplet correction should increase G(τ) at short lags"
);
}
#[test]
fn test_fitter_recovers_diffusion_coefficient() {
let setup = gfp_setup();
let d_true = 87e-12;
let n_true = 4.0;
let tau_data: Vec<f64> = (0..20)
.map(|i| 1e-6 * 10.0_f64.powf(i as f64 * 0.2))
.collect();
let g_data: Vec<f64> = tau_data
.iter()
.map(|&tau| setup.autocorrelation(tau, n_true, d_true))
.collect();
let fitter = FcsFitter::new(2.0, 50e-12);
let (n_fit, d_fit, _chi2) = fitter.fit(&setup, &tau_data, &g_data);
assert!(
(n_fit - n_true).abs() / n_true < 0.05,
"N_fit = {} vs N_true = {}",
n_fit,
n_true
);
assert!(
(d_fit - d_true).abs() / d_true < 0.10,
"D_fit = {} vs D_true = {}",
d_fit,
d_true
);
}
#[test]
fn test_hydrodynamic_radius_gfp() {
let fitter = FcsFitter::new(1.0, 87e-12);
let r_h = fitter.hydrodynamic_radius(87e-12, 8.9e-4, 298.0);
assert!(
r_h > 1e-9 && r_h < 10e-9,
"R_H = {} m outside 1–10 nm range for GFP",
r_h
);
}
#[test]
fn test_fccs_cross_correlation_zero_without_double() {
let ch1 = gfp_setup();
let ch2 = FcsSetup::new(200e-9, 1000e-9, 561e-9);
let fccs = FccsMeasurement::new(ch1, ch2, 0.9);
let g_cross = fccs.cross_correlation_amplitude(5.0, 5.0, 0.0);
assert!(g_cross.abs() < 1e-15, "No double-labelled → G_cross = 0");
}
#[test]
fn test_fccs_bound_fraction_clamped() {
let ch1 = gfp_setup();
let ch2 = FcsSetup::new(200e-9, 1000e-9, 561e-9);
let fccs = FccsMeasurement::new(ch1, ch2, 0.9);
let g_cross = fccs.cross_correlation_amplitude(5.0, 5.0, 3.0);
let f_bound = fccs.bound_fraction(g_cross, 10.0);
assert!(
(0.0..=1.0).contains(&f_bound),
"Bound fraction {} out of [0,1]",
f_bound
);
}
}