const KB: f64 = 1.380649e-23; const PI: f64 = std::f64::consts::PI;
pub fn stokes_drag(radius_m: f64, viscosity_pa_s: f64) -> f64 {
6.0 * PI * viscosity_pa_s * radius_m
}
pub fn diffusion_coefficient(temperature_k: f64, drag: f64) -> f64 {
if drag <= 0.0 {
return 0.0;
}
KB * temperature_k / drag
}
pub fn faxen_drag_correction(radius_m: f64, wall_distance_m: f64) -> f64 {
if wall_distance_m <= radius_m {
return 100.0;
}
let rh = radius_m / wall_distance_m;
let denom = 1.0 - (9.0 / 16.0) * rh + (1.0 / 8.0) * rh.powi(3)
- (45.0 / 256.0) * rh.powi(4)
- (1.0 / 16.0) * rh.powi(5);
if denom <= 0.0 {
return 100.0;
}
1.0 / denom
}
#[derive(Debug, Clone)]
pub struct LangevinSimulator {
pub temperature_k: f64,
pub drag_coeff: f64,
pub trap_stiffness: [f64; 3],
pub dt_s: f64,
pub position: [f64; 3],
rng_state: u64,
}
impl LangevinSimulator {
pub fn new(
temperature_k: f64,
radius_m: f64,
viscosity_pa_s: f64,
stiffness: [f64; 3],
dt_s: f64,
) -> Self {
let drag_coeff = stokes_drag(radius_m, viscosity_pa_s);
Self {
temperature_k,
drag_coeff,
trap_stiffness: stiffness,
dt_s,
position: [0.0; 3],
rng_state: 0xdeadbeef_cafebabe_u64,
}
}
pub fn with_drag(temperature_k: f64, drag_coeff: f64, stiffness: [f64; 3], dt_s: f64) -> Self {
Self {
temperature_k,
drag_coeff,
trap_stiffness: stiffness,
dt_s,
position: [0.0; 3],
rng_state: 0xfeedface_deadc0de_u64,
}
}
pub fn seed(&mut self, seed: u64) {
self.rng_state = seed;
}
fn lcg_uniform(&mut self) -> f64 {
self.rng_state = self
.rng_state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(self.rng_state >> 33) as f64 / u32::MAX as f64
}
fn gaussian_sample(&mut self) -> f64 {
let u1 = self.lcg_uniform().max(1e-15); let u2 = self.lcg_uniform();
(-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos()
}
pub fn step(&mut self) -> [f64; 3] {
let dt = self.dt_s;
let gamma = self.drag_coeff;
let sigma_x = (2.0 * KB * self.temperature_k * dt / gamma).sqrt();
let mut new_pos = [0.0f64; 3];
for (i, pos) in new_pos.iter_mut().enumerate() {
let drift = -self.trap_stiffness[i] * self.position[i] / gamma * dt;
let noise = sigma_x * self.gaussian_sample();
*pos = self.position[i] + drift + noise;
}
self.position = new_pos;
new_pos
}
pub fn run(&mut self, n_steps: usize) -> Vec<[f64; 3]> {
let mut trajectory = Vec::with_capacity(n_steps);
for _ in 0..n_steps {
trajectory.push(self.step());
}
trajectory
}
pub fn mean_square_displacement(trajectory: &[[f64; 3]], max_lag: usize) -> Vec<f64> {
let n = trajectory.len();
if n == 0 {
return Vec::new();
}
let max_lag = max_lag.min(n - 1);
let mut msd = vec![0.0f64; max_lag];
for lag in 1..=max_lag {
let count = n - lag;
if count == 0 {
continue;
}
let mut sum = 0.0;
for t in 0..count {
let r0 = trajectory[t];
let r1 = trajectory[t + lag];
let dr2 =
(r1[0] - r0[0]).powi(2) + (r1[1] - r0[1]).powi(2) + (r1[2] - r0[2]).powi(2);
sum += dr2;
}
msd[lag - 1] = sum / count as f64;
}
msd
}
pub fn power_spectral_density(trajectory: &[f64], dt: f64) -> Vec<(f64, f64)> {
let n = trajectory.len();
if n < 2 {
return Vec::new();
}
let windowed: Vec<f64> = trajectory
.iter()
.enumerate()
.map(|(i, &x)| {
let w = 0.5 * (1.0 - (2.0 * PI * i as f64 / (n as f64 - 1.0)).cos());
x * w
})
.collect();
let win_norm: f64 = windowed
.iter()
.map(|&_w| {
let i_approx = 0; let _ = i_approx;
1.0
})
.count() as f64;
let window_power: f64 = (0..n)
.map(|i| {
let w = 0.5 * (1.0 - (2.0 * PI * i as f64 / (n as f64 - 1.0)).cos());
w * w
})
.sum::<f64>()
/ n as f64;
let _ = win_norm;
let n_freq = n / 2 + 1;
let mut psd = Vec::with_capacity(n_freq);
for k in 0..n_freq {
let freq = k as f64 / (n as f64 * dt);
let mut re = 0.0f64;
let mut im = 0.0f64;
for (j, &w) in windowed.iter().enumerate().take(n) {
let angle = -2.0 * PI * k as f64 * j as f64 / n as f64;
re += w * angle.cos();
im += w * angle.sin();
}
let power = (re * re + im * im) / (n as f64 * window_power * n as f64 * dt);
let factor = if k == 0 || k == n / 2 { 1.0 } else { 2.0 };
psd.push((freq, power * factor));
}
psd
}
pub fn estimate_diffusion_from_msd(msd: &[f64]) -> f64 {
if msd.is_empty() {
return 0.0;
}
let start = msd.len() * 9 / 10;
let tail = &msd[start..];
if tail.is_empty() {
return 0.0;
}
let mean_plateau = tail.iter().sum::<f64>() / tail.len() as f64;
mean_plateau / 2.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stokes_drag_water() {
let gamma = stokes_drag(1e-6, 1e-3);
let expected = 6.0 * PI * 1e-3 * 1e-6; assert!(
(gamma - expected).abs() / expected < 0.01,
"stokes_drag={} expected~{}",
gamma,
expected
);
}
#[test]
fn diffusion_coefficient_einstein() {
let gamma = stokes_drag(1e-6, 1e-3);
let d = diffusion_coefficient(300.0, gamma);
assert!(
d > 1e-14 && d < 1e-11,
"diffusion coefficient D={} out of expected range",
d
);
}
#[test]
fn faxen_correction_no_wall() {
let correction = faxen_drag_correction(1e-6, 1e-3); assert!(
(correction - 1.0).abs() < 0.01,
"Faxén correction {} should be ~1 far from wall",
correction
);
}
#[test]
fn faxen_correction_near_wall_increases() {
let r = 1e-6;
let far = faxen_drag_correction(r, 100e-6);
let near = faxen_drag_correction(r, 5e-6);
assert!(
near > far,
"Faxén correction should increase near wall: far={} near={}",
far,
near
);
}
#[test]
fn langevin_free_diffusion_msd_linear() {
let mut sim = LangevinSimulator::new(300.0, 1e-6, 1e-3, [0.0; 3], 1e-5);
sim.seed(42);
let traj = sim.run(2000);
let msd = LangevinSimulator::mean_square_displacement(&traj, 100);
let grows = msd.windows(2).take(20).all(|w| w[1] >= w[0]);
assert!(grows, "MSD should grow for free diffusion (first 20 lags)");
}
#[test]
fn langevin_trapped_msd_plateau() {
let k = 1e-4; let gamma = stokes_drag(1e-6, 1e-3);
let mut sim = LangevinSimulator::with_drag(300.0, gamma, [k; 3], 1e-5);
sim.seed(123);
let traj = sim.run(5000);
let msd = LangevinSimulator::mean_square_displacement(&traj, 500);
let plateau = msd.last().copied().unwrap_or(0.0);
let expected_plateau = 6.0 * KB * 300.0 / k;
assert!(
plateau < 3.0 * expected_plateau && plateau > 0.0,
"Trapped MSD plateau {} >> expected {}",
plateau,
expected_plateau
);
}
#[test]
fn psd_returns_correct_length() {
let mut sim = LangevinSimulator::new(300.0, 1e-6, 1e-3, [0.0; 3], 1e-4);
sim.seed(99);
let traj = sim.run(128);
let x_traj: Vec<f64> = traj.iter().map(|p| p[0]).collect();
let psd = LangevinSimulator::power_spectral_density(&x_traj, 1e-4);
assert_eq!(psd.len(), 128 / 2 + 1, "PSD length mismatch");
}
#[test]
fn gaussian_sample_zero_mean() {
let mut sim = LangevinSimulator::new(300.0, 1e-6, 1e-3, [0.0; 3], 1e-5);
sim.seed(777);
let n = 10000;
let sum: f64 = (0..n).map(|_| sim.gaussian_sample()).sum();
let mean = sum / n as f64;
assert!(mean.abs() < 0.05, "Gaussian mean {} too far from 0", mean);
}
}