use std::fmt::Display;
use lin_alg::f32::Vec3;
use rand::prelude::ThreadRng;
use rand_distr::{Distribution, StandardNormal};
use crate::{AtomDynamics, KCAL_TO_NATIVE, MdState, NATIVE_TO_KCAL, SimBoxInit, solvent::WaterMol};
pub(crate) const BAR_PER_KCAL_MOL_PER_ANSTROM_CUBED: f64 = 69476.95457055373;
const KB_BAR_A3_PER_K: f64 = 138.064_9;
#[derive(Clone, Copy, Default, PartialEq, Debug)]
pub struct SimBox {
pub bounds_low: Vec3,
pub bounds_high: Vec3,
pub extent: Vec3,
}
impl SimBox {
pub fn new(atoms: &[AtomDynamics], box_type: &SimBoxInit) -> Self {
match box_type {
SimBoxInit::Pad(pad) => {
let (mut min, mut max) =
(Vec3::splat(f32::INFINITY), Vec3::splat(f32::NEG_INFINITY));
for a in atoms {
min = min.min(a.posit);
max = max.max(a.posit);
}
if atoms.is_empty() {
min = Vec3::new_zero();
max = Vec3::new_zero();
}
let bounds_low = min - Vec3::splat(*pad);
let bounds_high = max + Vec3::splat(*pad);
Self {
bounds_low,
bounds_high,
extent: bounds_high - bounds_low,
}
}
SimBoxInit::Fixed((bounds_low, bounds_high)) => {
let bounds_low: Vec3 = *bounds_low;
let bounds_high: Vec3 = *bounds_high;
Self {
bounds_low,
bounds_high,
extent: bounds_high - bounds_low,
}
}
}
}
pub fn recenter(&mut self, atoms: &[AtomDynamics]) {
if atoms.is_empty() {
return;
}
let half_ext = self.extent / 2.;
let mut center = Vec3::new_zero();
for atom in atoms {
center += atom.posit;
}
center /= atoms.len() as f32;
self.bounds_low = center - half_ext;
self.bounds_high = center + half_ext;
}
pub fn wrap(&self, p: Vec3) -> Vec3 {
let ext = &self.extent;
assert!(
ext.x > 0.0 && ext.y > 0.0 && ext.z > 0.0,
"SimBox edges must be > 0 (lo={:?}, hi={:?})",
self.bounds_low,
self.bounds_high
);
Vec3::new(
(p.x - self.bounds_low.x).rem_euclid(ext.x) + self.bounds_low.x,
(p.y - self.bounds_low.y).rem_euclid(ext.y) + self.bounds_low.y,
(p.z - self.bounds_low.z).rem_euclid(ext.z) + self.bounds_low.z,
)
}
pub fn min_image(&self, dv: Vec3) -> Vec3 {
let ext = &self.extent;
Vec3::new(
dv.x - (dv.x / ext.x).round() * ext.x,
dv.y - (dv.y / ext.y).round() * ext.y,
dv.z - (dv.z / ext.z).round() * ext.z,
)
}
pub fn volume(&self) -> f32 {
(self.bounds_high.x - self.bounds_low.x).abs()
* (self.bounds_high.y - self.bounds_low.y).abs()
* (self.bounds_high.z - self.bounds_low.z).abs()
}
pub fn center(&self) -> Vec3 {
(self.bounds_low + self.bounds_high) * 0.5
}
pub fn scale_isotropic(&mut self, lambda: f32) {
let lam = if lambda.is_finite() && lambda.abs() > 1.0e-12 {
lambda
} else {
1.0
};
let c = self.center();
let lo = c + (self.bounds_low - c) * lam;
let hi = c + (self.bounds_high - c) * lam;
self.bounds_low = Vec3::new(lo.x.min(hi.x), lo.y.min(hi.y), lo.z.min(hi.z));
self.bounds_high = Vec3::new(lo.x.max(hi.x), lo.y.max(hi.y), lo.z.max(hi.z));
self.extent = self.bounds_high - self.bounds_low;
debug_assert!({
let ext = &self.extent;
ext.x > 0.0 && ext.y > 0.0 && ext.z > 0.0
});
}
pub fn contains(&self, posit: Vec3) -> bool {
!(posit.x < self.bounds_low.x
|| posit.y < self.bounds_low.y
|| posit.z < self.bounds_low.z
|| posit.x > self.bounds_high.x
|| posit.y > self.bounds_high.y
|| posit.z > self.bounds_high.z)
}
}
#[derive(Debug, Default)]
pub struct VirialKcalMol {
pub bonded: f64,
pub nonbonded_short_range: f64,
pub nonbonded_long_range: f64,
pub constraints: f64,
}
impl VirialKcalMol {
pub(crate) fn total(&self) -> f64 {
self.bonded + self.nonbonded_short_range + self.nonbonded_long_range + self.constraints
}
}
impl Display for VirialKcalMol {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Virial, kcal/mol. W_bonded: {:.3} kcal/mol W Short range={:.3} W long range: {:.3} W_constraint: {:.3}",
self.bonded, self.nonbonded_short_range, self.nonbonded_long_range, self.constraints,
)
}
}
#[derive(Debug, Default)]
pub struct Virial {
pub bonded: f64,
pub nonbonded_short_range: f64,
pub nonbonded_long_range: f64,
pub constraints: f64,
}
impl Virial {
pub(crate) fn to_kcal_mol(&self) -> VirialKcalMol {
VirialKcalMol {
bonded: self.bonded,
nonbonded_short_range: self.nonbonded_short_range,
nonbonded_long_range: self.nonbonded_long_range,
constraints: self.constraints * NATIVE_TO_KCAL as f64,
}
}
}
impl Display for Virial {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Virial, native. W_bonded: {:.3} kcal/mol W Short range={:.3} W long range: {:.3} W_constraint: {:.3}",
self.bonded, self.nonbonded_short_range, self.nonbonded_long_range, self.constraints,
)
}
}
pub struct Barostat {
pub pressure_target: f64,
pub kappa_t: f64,
pub virial: Virial,
pub rng: ThreadRng,
}
impl Default for Barostat {
fn default() -> Self {
Self {
pressure_target: 1.,
kappa_t: 4.5e-5,
virial: Default::default(),
rng: rand::rng(),
}
}
}
impl Barostat {
pub fn scale_factor(
&mut self,
p_inst: f64,
dt: f64,
temp_k: f64,
vol_a3: f64,
tau_pressure: f64,
) -> f64 {
let dlnv_det = (self.kappa_t / tau_pressure) * (p_inst - self.pressure_target) * dt;
let sigma_lnv =
(2.0 * self.kappa_t * KB_BAR_A3_PER_K * temp_k * dt / (tau_pressure * vol_a3)).sqrt();
let xi: f64 = StandardNormal.sample(&mut self.rng);
const MAX_DLNV: f64 = 0.10;
let dlnv = (dlnv_det + sigma_lnv * xi).clamp(-MAX_DLNV, MAX_DLNV);
(dlnv / 3.0).exp()
}
pub(crate) fn apply_isotropic(
&mut self,
dt_ps: f64,
p_inst_bar: f64,
temp_k: f64,
tau_pressure: f64,
simbox: &mut SimBox,
atoms_dyn: &mut [AtomDynamics],
waters: &mut [WaterMol],
) {
return;
let vol_a3 = simbox.volume() as f64;
let lam = self.scale_factor(p_inst_bar, dt_ps, temp_k, vol_a3, tau_pressure);
if !(lam.is_finite() && lam > 0.0) || (lam - 1.0).abs() < 1e-12 {
return; }
simbox.scale_isotropic(lam as f32);
let c = simbox.center();
let lc = lam as f32;
fn scale_pos(p: &mut Vec3, c: Vec3, s: f32) {
*p = c + (*p - c) * s;
}
for a in atoms_dyn.iter_mut() {
if !a.static_ {
scale_pos(&mut a.posit, c, lc);
}
}
for w in waters.iter_mut() {
scale_pos(&mut w.o.posit, c, lc);
scale_pos(&mut w.h0.posit, c, lc);
scale_pos(&mut w.h1.posit, c, lc);
}
for a in atoms_dyn.iter_mut() {
if !a.static_ {
a.vel *= lc;
}
}
for w in waters.iter_mut() {
w.o.vel *= lc;
w.h0.vel *= lc;
w.h1.vel *= lc;
w.update_virtual_site();
}
}
}
pub(crate) fn measure_pressure(
kinetic_energy: f64, simbox: &SimBox,
virial: &VirialKcalMol,
) -> f64 {
let vol = simbox.volume() as f64;
let result = (2.0 * kinetic_energy + virial.total()) / (3.0 * vol);
result * BAR_PER_KCAL_MOL_PER_ANSTROM_CUBED
}
impl MdState {
#[allow(unused)]
pub(crate) fn print_ambient_data(&self, pressure: f64) {
println!(
"\n\n------Ambient stats at step {}--------",
self.step_count
);
let cell_vol = self.cell.volume() as f64;
let atom_count = &self.atoms.iter().filter(|a| !a.static_).count();
println!(
"Cell vol: {cell_vol:.1} Å^3 num dynamic atoms: {atom_count} num solvent mols: {}",
self.water.len()
);
{
let p_kin_bar = (2.0 * self.measure_kinetic_energy_translational()) / (3.0 * cell_vol)
* BAR_PER_KCAL_MOL_PER_ANSTROM_CUBED;
let vir_total_kcal = self.barostat.virial.to_kcal_mol().total();
let p_vir_bar = vir_total_kcal / (3.0 * cell_vol) * BAR_PER_KCAL_MOL_PER_ANSTROM_CUBED;
println!("P_kin: {p_kin_bar:.3} bar P_vir: {p_vir_bar:.3} bar");
}
let temp = self.measure_temperature();
println!("\nTemperature: {temp:.2} K");
let mut water_v = 0.;
for mol in &self.water {
water_v += mol.o.vel.magnitude();
}
let mut atom_v = 0.;
for atom in self.atoms.iter().filter(|a| !a.static_) {
atom_v += atom.vel.magnitude();
}
println!(
"Ke: {:.2} kcal/mol = {:.2} (Å/ps)² DOF: {}",
self.kinetic_energy,
self.kinetic_energy * KCAL_TO_NATIVE as f64,
self.thermo_dof
);
println!(
"Water O avg vel: {:.3} Å/ps | Atom (non-static) avg vel: {:.3} Å/ps",
water_v / self.water.len() as f32,
atom_v / *atom_count as f32
);
println!("\nPressure: {pressure:.3} bar");
println!("Virial: {}", self.barostat.virial.to_kcal_mol());
println!("------------------------");
}
}