#![allow(non_snake_case)]
#![allow(confusable_idents)]
mod add_hydrogens;
mod barostat;
mod bonded;
mod bonded_forces;
mod config;
mod forces;
pub mod integrate;
mod neighbors;
mod non_bonded;
pub mod params;
pub mod partial_charge_inference;
mod prep;
#[cfg(target_arch = "x86_64")]
mod simd;
pub mod snapshot;
mod solvent;
mod thermostat;
mod util;
mod com_zero;
#[cfg(feature = "cuda")]
mod gpu_interface;
pub mod minimize_energy;
pub mod alchemical;
pub mod param_inference;
mod sa_surface;
#[cfg(test)]
mod tests;
#[cfg(feature = "cuda")]
use std::sync::Arc;
use std::{
collections::HashSet,
fmt,
fmt::{Display, Formatter},
io,
time::Instant,
};
pub use add_hydrogens::{
add_hydrogens_2::Dihedral,
bond_vecs::{find_planar_posit, find_tetra_posit_final, find_tetra_posits},
populate_hydrogens_dihedrals,
};
use barostat::SimBox;
#[cfg(feature = "encode")]
use bincode::{Decode, Encode};
use bio_files::{
AtomGeneric, BondGeneric, Sdf,
md_params::{ForceFieldParams, ForceFieldParamsIndexed, LjParams, MassParams},
mol2::Mol2,
};
pub use bonded::{LINCS_ITER_DEFAULT, LINCS_ORDER_DEFAULT, SHAKE_TOL_DEFAULT};
pub use config::MdConfig;
#[cfg(feature = "cuda")]
use cudarc::{
driver::{CudaContext, CudaFunction, CudaStream},
nvrtc::Ptx,
};
use ewald::PmeRecip;
pub use integrate::Integrator;
#[allow(unused)]
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use lin_alg::f32::{Vec3x8, Vec3x16, f32x8, f32x16};
use lin_alg::{f32::Vec3, f64::Vec3 as Vec3F64};
use na_seq::Element;
use neighbors::NeighborsNb;
pub use prep::{HydrogenConstraint, merge_params};
pub use solvent::{
ForcesOnWaterMol, Solvent,
init::{WATER_TEMPLATE_60A, WaterInitTemplate},
};
#[cfg(feature = "cuda")]
use crate::gpu_interface::{ForcesPositsGpu, PerNeighborGpu};
use crate::{
barostat::Barostat,
non_bonded::{CHARGE_UNIT_SCALER, LjTables, NonBondedPair},
param_inference::update_small_mol_params,
params::FfParamSet,
snapshot::Snapshot,
solvent::{
WaterMol, WaterMolx8, WaterMolx16,
init::{make_water_mols, pack_custom_solvent},
},
util::{ComputationTime, ComputationTimeSums, build_adjacency_list},
};
pub use crate::{
barostat::{BarostatCfg, PRESSURE_DEFAULT, TAU_PRESSURE_DEFAULT},
thermostat::{LANGEVIN_GAMMA_DEFAULT, TAU_TEMP_DEFAULT},
};
#[cfg(feature = "cuda")]
const PTX: &str = include_str!("../dynamics.ptx");
const KCAL_TO_NATIVE: f32 = 418.4;
const NATIVE_TO_KCAL: f32 = 1. / KCAL_TO_NATIVE;
const CENTER_SIMBOX_RATIO: usize = 30;
const SPME_RATIO: usize = 2;
const COMPUTATION_TIME_RATIO: usize = 20;
#[derive(Debug, Clone, Default)]
pub enum ComputationDevice {
#[default]
Cpu,
#[cfg(feature = "cuda")]
Gpu(Arc<CudaStream>),
}
#[derive(Clone, Debug)]
pub struct ParamError {
pub descrip: String,
}
impl ParamError {
pub fn new(descrip: &str) -> Self {
Self {
descrip: descrip.to_owned(),
}
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum FfMolType {
Peptide,
SmallOrganic,
Dna,
Rna,
Lipid,
Carbohydrate,
}
#[derive(Clone, Debug)]
pub struct MolDynamics {
pub ff_mol_type: FfMolType,
pub atoms: Vec<AtomGeneric>,
pub atom_posits: Option<Vec<Vec3F64>>,
pub atom_init_velocities: Option<Vec<Vec3>>,
pub bonds: Vec<BondGeneric>,
pub adjacency_list: Option<Vec<Vec<usize>>>,
pub static_: bool,
pub mol_specific_params: Option<ForceFieldParams>,
pub bonded_only: bool,
}
impl Default for MolDynamics {
fn default() -> Self {
Self {
ff_mol_type: FfMolType::SmallOrganic,
atoms: Vec::new(),
atom_posits: None,
atom_init_velocities: None,
bonds: Vec::new(),
adjacency_list: None,
static_: false,
mol_specific_params: None,
bonded_only: false,
}
}
}
impl MolDynamics {
pub fn from_mol2(mol: &Mol2, mol_specific_params: Option<ForceFieldParams>) -> Self {
Self {
ff_mol_type: FfMolType::SmallOrganic,
atoms: mol.atoms.clone(),
atom_posits: None,
atom_init_velocities: None,
bonds: mol.bonds.clone(),
adjacency_list: None,
static_: false,
mol_specific_params,
bonded_only: false,
}
}
pub fn from_sdf(mol: &Sdf, mol_specific_params: Option<ForceFieldParams>) -> Self {
Self {
ff_mol_type: FfMolType::SmallOrganic,
atoms: mol.atoms.clone(),
atom_posits: None,
atom_init_velocities: None,
bonds: mol.bonds.clone(),
adjacency_list: None,
static_: false,
mol_specific_params,
bonded_only: false,
}
}
pub fn from_amber_geostd(ident: &str) -> io::Result<Self> {
let data = bio_apis::amber_geostd::load_mol_files(ident)
.map_err(|e| io::Error::other(format!("Error loading data: {e:?}")))?;
let mol = Mol2::new(&data.mol2)?;
let params = ForceFieldParams::from_frcmod(&data.frcmod.unwrap())?;
Ok(Self {
ff_mol_type: FfMolType::SmallOrganic,
atoms: mol.atoms,
atom_posits: None,
atom_init_velocities: None,
bonds: mol.bonds,
adjacency_list: None,
static_: false,
mol_specific_params: Some(params),
bonded_only: false,
})
}
}
#[derive(Clone, Debug, Default)]
pub struct AtomDynamics {
pub serial_number: u32,
pub static_: bool,
pub bonded_only: bool,
pub force_field_type: String,
pub element: Element,
pub posit: Vec3,
pub vel: Vec3,
pub accel: Vec3,
pub force: Vec3,
pub mass: f32,
pub partial_charge: f32,
pub lj_sigma: f32,
pub lj_eps: f32,
}
impl Display for AtomDynamics {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"Atom {}: {}, {}. ff: {}, q: {}",
self.serial_number,
self.element.to_letter(),
self.posit,
self.force_field_type,
self.partial_charge,
)?;
if self.static_ {
write!(f, ", Static")?;
}
Ok(())
}
}
impl AtomDynamics {
pub fn new(
atom: &AtomGeneric,
atom_posits: &[Vec3],
i: usize,
static_: bool,
bonded_only: bool,
) -> Result<Self, ParamError> {
let ff_type = match &atom.force_field_type {
Some(ff_type) => ff_type.clone(),
None => {
return Err(ParamError::new(&format!(
"Atom missing FF type; can't run dynamics: {:?}",
atom
)));
}
};
let partial_charge = match atom.partial_charge {
Some(p) => p * CHARGE_UNIT_SCALER,
None => return Err(ParamError::new("Missing partial charge on atom {i}")),
};
Ok(Self {
serial_number: atom.serial_number,
static_,
bonded_only,
element: atom.element,
posit: atom_posits[i],
force_field_type: ff_type,
partial_charge,
..Default::default()
})
}
pub(crate) fn assign_data_from_params(
&mut self,
ff_params: &ForceFieldParamsIndexed,
i: usize,
) {
self.mass = ff_params.mass[&i].mass;
self.lj_sigma = ff_params.lennard_jones[&i].sigma;
self.lj_eps = ff_params.lennard_jones[&i].eps;
}
}
impl bio_files::md_params::AtomFfSource for AtomDynamics {
fn ff_type(&self) -> Option<&str> {
Some(&self.force_field_type)
}
fn element(&self) -> na_seq::Element {
self.element
}
fn serial_number(&self) -> u32 {
self.serial_number
}
}
#[allow(unused)]
#[cfg(target_arch = "x86_64")]
#[derive(Clone, Debug)]
pub(crate) struct AtomDynamicsx8 {
pub serial_number: [u32; 8],
pub static_: [bool; 8],
pub bonded_only: [bool; 8],
pub force_field_type: [String; 8],
pub element: [Element; 8],
pub posit: Vec3x8,
pub vel: Vec3x8,
pub accel: Vec3x8,
pub mass: f32x8,
pub partial_charge: f32x8,
pub lj_sigma: f32x8,
pub lj_eps: f32x8,
}
#[allow(unused)]
#[cfg(target_arch = "x86_64")]
#[derive(Clone, Debug)]
pub(crate) struct AtomDynamicsx16 {
pub serial_number: [u32; 16],
pub static_: [bool; 16],
pub bonded_only: [bool; 16],
pub force_field_type: [String; 16],
pub element: [Element; 16],
pub posit: Vec3x16,
pub vel: Vec3x16,
pub accel: Vec3x16,
pub mass: f32x16,
pub partial_charge: f32x16,
pub lj_sigma: f32x16,
pub lj_eps: f32x16,
}
#[cfg_attr(feature = "encode", derive(Encode, Decode))]
#[derive(Debug, Clone, PartialEq)]
pub enum SimBoxInit {
Pad(f32),
Fixed((Vec3, Vec3)),
}
impl SimBoxInit {
pub fn new_cube(side_len: f32) -> Self {
let l = side_len / 2.;
Self::Fixed((Vec3::new(-l, -l, -l), Vec3::new(l, l, l)))
}
}
impl Default for SimBoxInit {
fn default() -> Self {
Self::Pad(12.)
}
}
#[derive(Clone, Default, Debug, PartialEq)]
#[cfg_attr(feature = "encode", derive(Encode, Decode))]
pub struct MdOverrides {
pub skip_water_relaxation: bool,
pub bonded_disabled: bool,
pub coulomb_disabled: bool,
pub lj_disabled: bool,
pub long_range_recip_disabled: bool,
pub snapshots_during_equilibration: bool,
pub snapshots_during_energy_min: bool,
}
#[derive(Default)]
pub struct MdState {
pub cfg: MdConfig,
pub atoms: Vec<AtomDynamics>,
#[allow(unused)]
#[cfg(target_arch = "x86_64")]
pub(crate) atoms_x8: Vec<AtomDynamicsx8>,
#[allow(unused)]
#[cfg(target_arch = "x86_64")]
pub(crate) atoms_x16: Vec<AtomDynamicsx16>,
pub water: Vec<WaterMol>,
#[allow(unused)]
#[cfg(target_arch = "x86_64")]
pub(crate) water_x8: Vec<WaterMolx8>,
#[allow(unused)]
#[cfg(target_arch = "x86_64")]
pub(crate) water_x16: Vec<WaterMolx16>,
pub adjacency_list: Vec<Vec<usize>>,
pub(crate) force_field_params: ForceFieldParamsIndexed,
pub time: f64,
pub step_count: usize, pub snapshots: Vec<Snapshot>,
pub cell: SimBox,
pub neighbors_nb: NeighborsNb,
nb_pairs: Vec<NonBondedPair>,
barostat: Barostat,
pairs_excluded_12_13: HashSet<(usize, usize)>,
pairs_14_scaled: HashSet<(usize, usize)>,
lj_tables: LjTables,
pub water_pme_sites_forces: Vec<[Vec3F64; 3]>, pme_recip: Option<PmeRecip>,
pub kinetic_energy: f64,
pub potential_energy: f64,
pub potential_energy_nonbonded: f64,
pub potential_energy_bonded: f64,
pub potential_energy_between_mols: Vec<f64>,
snapshot_queue_for_dcd: Vec<Snapshot>,
snapshot_queue_for_trr: Vec<Snapshot>,
snapshot_queue_for_xtc: Vec<Snapshot>,
#[cfg(feature = "cuda")]
gpu_kernel: Option<CudaFunction>, #[cfg(feature = "cuda")]
gpu_kernel_zero_f32: Option<CudaFunction>,
#[cfg(feature = "cuda")]
gpu_kernel_zero_f64: Option<CudaFunction>,
#[cfg(feature = "cuda")]
forces_posits_gpu: Option<ForcesPositsGpu>,
#[cfg(feature = "cuda")]
per_neighbor_gpu: Option<PerNeighborGpu>,
pub neighbor_rebuild_count: usize,
mass_accel_factor: Vec<f32>,
pub computation_time: ComputationTimeSums,
spme_force_prev: Option<(Vec<Vec3>, f64, f64)>,
_num_static_atoms: usize,
thermo_dof: usize,
pub mol_start_indices: Vec<usize>,
solvent_only_sim_at_init: bool,
pub alch_mol_idx: Option<usize>,
pub lambda: f64,
pub run_index: Option<usize>,
}
impl MdState {
pub fn new(
dev: &ComputationDevice,
cfg: &MdConfig,
mols: &[MolDynamics],
param_set: &FfParamSet,
) -> Result<Self, ParamError> {
let mut atoms_md = Vec::new();
let mut adjacency_list = Vec::new();
let mut params = ForceFieldParams::default();
let mut atom_ct_prior_to_this_mol = 0;
let mut mol_start_indices = Vec::new();
let custom_packed: Vec<MolDynamics> = match (&cfg.solvent, &cfg.sim_box) {
(Solvent::Custom((mols_solvent, _)), SimBoxInit::Fixed((low, high))) => {
let existing: Vec<Vec3F64> = mols
.iter()
.flat_map(|m| -> Vec<Vec3F64> {
if let Some(ap) = &m.atom_posits {
ap.clone()
} else {
m.atoms.iter().map(|a| a.posit).collect()
}
})
.collect();
pack_custom_solvent(*low, *high, &existing, mols_solvent)
}
(Solvent::Custom(_), SimBoxInit::Pad(_)) => {
return Err(ParamError {
descrip: "Custom solvent with a Pad sim box is not yet supported; \
skipping custom solvent packing."
.to_string(),
});
}
_ => Vec::new(),
};
let combined_mols: Vec<MolDynamics>;
let all_mols: &[MolDynamics] = if custom_packed.is_empty() {
mols
} else {
combined_mols = mols.iter().cloned().chain(custom_packed).collect();
&combined_mols
};
for mol in all_mols {
if !mol.atoms.is_empty() {
mol_start_indices.push(atoms_md.len());
}
let mut atoms: Vec<AtomGeneric> = match mol.ff_mol_type {
FfMolType::Peptide => mol.atoms.iter().filter(|a| !a.hetero).cloned().collect(),
_ => mol.atoms.to_vec(),
};
let mut mol_specific_params = mol.mol_specific_params.clone();
if mol.ff_mol_type == FfMolType::SmallOrganic {
let mut needs_ff_type_or_q = false;
for atom in &atoms {
if atom.force_field_type.is_none() || atom.partial_charge.is_none() {
needs_ff_type_or_q = true;
break;
}
}
if needs_ff_type_or_q {
mol_specific_params = Some(
update_small_mol_params(
&mut atoms,
&mol.bonds,
Some(&adjacency_list),
param_set.small_mol.as_ref().unwrap(),
)
.map_err(|_| ParamError {
descrip: "Problem inferring params".to_string(),
})?,
);
}
}
if mol.ff_mol_type == FfMolType::Peptide
&& atoms.len() != mol.atoms.len()
&& (mol.adjacency_list.is_some() || mol.atom_posits.is_some())
{
return Err(ParamError::new(
"Unable to perform MD on this peptide: If passing atom positions or an adjacency list,\
you must already have filtered out hetero atoms. We found one or more hetero atoms in the input.",
));
}
{
let params_general = match mol.ff_mol_type {
FfMolType::Peptide => ¶m_set.peptide,
FfMolType::SmallOrganic => ¶m_set.small_mol,
FfMolType::Dna => ¶m_set.dna,
FfMolType::Rna => ¶m_set.rna,
FfMolType::Lipid => ¶m_set.lipids,
FfMolType::Carbohydrate => ¶m_set.carbohydrates,
};
let Some(params_general) = params_general else {
return Err(ParamError::new(&format!(
"Missing general parameters for {:?}",
mol.ff_mol_type
)));
};
params = merge_params(¶ms, params_general);
if let Some(p) = &mol_specific_params {
params = merge_params(¶ms, p);
}
}
let atom_posits: Vec<Vec3> = match &mol.atom_posits {
Some(a) => a.iter().map(|p| (*p).into()).collect(),
None => mol.atoms.iter().map(|a| a.posit.into()).collect(),
};
for (i, atom) in atoms.iter().enumerate() {
let mut atom =
AtomDynamics::new(atom, &atom_posits, i, mol.static_, mol.bonded_only)?;
if let Some(vel) = &mol.atom_init_velocities {
if i >= vel.len() {
return Err(ParamError::new(
"Initial velocities passed, but don't match atom len.",
));
}
atom.vel = vel[i];
}
atoms_md.push(atom);
}
let adjacency_list_ = match &mol.adjacency_list {
Some(a) => a,
None => &build_adjacency_list(&atoms, &mol.bonds)?,
};
for aj in adjacency_list_ {
let mut updated = aj.clone();
for neighbor in &mut updated {
*neighbor += atom_ct_prior_to_this_mol;
}
adjacency_list.push(updated);
}
atom_ct_prior_to_this_mol += atoms.len();
}
let net_q_e: f32 =
atoms_md.iter().map(|a| a.partial_charge).sum::<f32>() / CHARGE_UNIT_SCALER;
let n_ions = net_q_e.abs().round() as usize;
let h_constrained = !matches!(cfg.hydrogen_constraint, HydrogenConstraint::Flexible);
let force_field_params =
ForceFieldParamsIndexed::new(¶ms, &atoms_md, &adjacency_list, h_constrained)
.map_err(|e| ParamError::new(&e.to_string()))?;
let mut mass_accel_factor = Vec::with_capacity(atoms_md.len());
for (i, atom) in atoms_md.iter_mut().enumerate() {
atom.assign_data_from_params(&force_field_params, i);
mass_accel_factor.push(KCAL_TO_NATIVE / atom.mass);
}
let cell = SimBox::new(&atoms_md, &cfg.sim_box);
let num_static_atoms = atoms_md.iter().filter(|a| !a.static_).count();
let potential_energy_between_mols = vec![0.; mol_start_indices.len().pow(2)];
let mut result = Self {
cfg: cfg.clone(),
atoms: atoms_md,
adjacency_list: adjacency_list.to_vec(),
cell,
pairs_excluded_12_13: HashSet::new(),
pairs_14_scaled: HashSet::new(),
force_field_params,
mass_accel_factor,
_num_static_atoms: num_static_atoms,
mol_start_indices,
potential_energy_between_mols,
..Default::default()
};
result.check_for_overlaps_oob()?;
result.cell.recenter(&result.atoms);
result.lj_tables = LjTables::new(&result.atoms);
result.neighbors_nb = NeighborsNb::new(result.cfg.neighbor_skin, result.cfg.coulomb_cutoff);
result.water = {
let count = match &cfg.solvent {
Solvent::None | Solvent::WaterOpc => None,
Solvent::WaterOpcSpecifyMolCount(c) => Some(*c),
Solvent::Custom((_, c)) => Some(*c),
};
let water_template_override: Option<WaterInitTemplate> = cfg
.water_template_path
.as_deref()
.and_then(|path_str| {
match WaterInitTemplate::load(std::path::Path::new(path_str)) {
Ok(t) => Some(t),
Err(e) => {
eprintln!(
"Warning: could not load water template from {path_str:?}: {e}. Using default."
);
None
}
}
});
make_water_mols(
&result.cell,
&result.atoms,
count,
water_template_override.as_ref(),
cfg.skip_water_pbc_filter,
)
};
add_ions(&mut result, net_q_e, n_ions);
if n_ions > 0 && !result.water.is_empty() {
result.lj_tables = LjTables::new(&result.atoms);
}
result.thermo_dof = result.dof_for_thermo();
result.water_pme_sites_forces = vec![[Vec3F64::new_zero(); 3]; result.water.len()];
result.setup_nonbonded_exclusion_scale_flags();
result.build_all_neighbors(dev);
result.regen_pme(dev);
#[cfg(feature = "cuda")]
if let ComputationDevice::Gpu(stream) = dev {
let ctx = CudaContext::new(0).unwrap();
let module = ctx.load_module(Ptx::from_src(PTX)).unwrap();
result.gpu_kernel = Some(module.load_function("nonbonded_force_kernel").unwrap());
result.gpu_kernel_zero_f32 = Some(module.load_function("zero_f32").unwrap());
result.gpu_kernel_zero_f64 = Some(module.load_function("zero_f64").unwrap());
result.forces_posits_gpu = Some(ForcesPositsGpu::new(
stream,
result.atoms.len(),
result.water.len(),
result.cfg.coulomb_cutoff,
result.cfg.spme_alpha,
));
result.per_neighbor_gpu = Some(PerNeighborGpu::new(
stream,
&result.nb_pairs,
&result.atoms,
&result.water,
&result.lj_tables,
));
}
#[cfg(target_arch = "x86_64")]
result.pack_atoms();
if !result.cfg.overrides.skip_water_relaxation {
result.md_on_water_only(dev);
}
if let Some(max_iters) = result.cfg.max_init_relaxation_iters {
result.minimize_energy(dev, max_iters, None);
}
result.computation_time = Default::default();
Ok(result)
}
fn check_for_overlaps_oob(&mut self) -> Result<(), ParamError> {
const MIN_DIST_FROM_EDGE: f32 = 0.5; const MIN_INTER_MOL_DIST: f32 = 0.5;
let lo = self.cell.bounds_low;
let hi = self.cell.bounds_high;
for (i, atom) in self.atoms.iter().enumerate() {
let p = atom.posit;
if p.x < lo.x || p.y < lo.y || p.z < lo.z || p.x > hi.x || p.y > hi.y || p.z > hi.z {
return Err(ParamError::new(&format!(
"Atom index {i} is outside the sim box. \
Pos ({:.3}, {:.3}, {:.3}), box [{:.3}..{:.3}, {:.3}..{:.3}, {:.3}..{:.3}]",
p.x, p.y, p.z, lo.x, hi.x, lo.y, hi.y, lo.z, hi.z,
)));
}
let dist_to_edge = (p.x - lo.x)
.min(hi.x - p.x)
.min(p.y - lo.y)
.min(hi.y - p.y)
.min(p.z - lo.z)
.min(hi.z - p.z);
if dist_to_edge < MIN_DIST_FROM_EDGE {
return Err(ParamError::new(&format!(
"Atom index {i} is too close to the sim box edge ({dist_to_edge:.3} Å). \
Pos ({:.3}, {:.3}, {:.3})",
p.x, p.y, p.z,
)));
}
}
if self.mol_start_indices.len() > 1 {
let mut atom_mol = vec![0usize; self.atoms.len()];
for (mol_i, &start) in self.mol_start_indices.iter().enumerate() {
let end = self
.mol_start_indices
.get(mol_i + 1)
.copied()
.unwrap_or(self.atoms.len());
for idx in start..end {
atom_mol[idx] = mol_i;
}
}
for i in 0..self.atoms.len() {
for j in (i + 1)..self.atoms.len() {
if atom_mol[i] == atom_mol[j] {
continue;
}
let diff = self
.cell
.min_image(self.atoms[i].posit - self.atoms[j].posit);
let dist = diff.magnitude();
if dist < MIN_INTER_MOL_DIST {
return Err(ParamError::new(&format!(
"Atoms from different molecules (indices {i} and {j}) are too \
close in minimum-image distance ({dist:.3} Å). This would cause \
immediate simulation malfunction.",
)));
}
}
}
}
Ok(())
}
pub fn computation_time(&self) -> io::Result<ComputationTime> {
self.computation_time.time_per_step(self.step_count)
}
pub(crate) fn reset_f_acc_pe_virial(&mut self) {
for a in &mut self.atoms {
a.accel = Vec3::new_zero();
a.force = Vec3::new_zero();
}
for mol in &mut self.water {
mol.o.accel = Vec3::new_zero();
mol.m.accel = Vec3::new_zero();
mol.h0.accel = Vec3::new_zero();
mol.h1.accel = Vec3::new_zero();
mol.o.force = Vec3::new_zero();
mol.m.force = Vec3::new_zero();
mol.h0.force = Vec3::new_zero();
mol.h1.force = Vec3::new_zero();
}
self.barostat.virial = Default::default();
self.potential_energy = 0.;
self.potential_energy_nonbonded = 0.;
self.potential_energy_bonded = 0.;
self.potential_energy_between_mols = vec![0.; self.mol_start_indices.len().pow(2)]
}
pub(crate) fn apply_all_forces(
&mut self,
dev: &ComputationDevice,
external_force: &Option<Vec<Vec3>>,
) {
let mut start = Instant::now();
let log_time = self.step_count.is_multiple_of(COMPUTATION_TIME_RATIO);
if log_time {
start = Instant::now();
}
if !self.cfg.overrides.bonded_disabled {
self.apply_bonded_forces();
}
if log_time {
let elapsed = start.elapsed().as_micros() as u64;
self.computation_time.bonded_sum += elapsed;
start = Instant::now();
}
self.apply_nonbonded_forces(dev);
if log_time {
let elapsed = start.elapsed().as_micros() as u64;
self.computation_time.non_bonded_short_range_sum += elapsed;
}
if !self.cfg.overrides.long_range_recip_disabled {
if self.step_count.is_multiple_of(SPME_RATIO) {
if log_time {
start = Instant::now();
}
let data = self.handle_spme_recip(dev);
if SPME_RATIO != 1 {
self.spme_force_prev = Some(data);
}
if log_time {
let elapsed = start.elapsed().as_micros() as u64;
self.computation_time.ewald_long_range_sum += elapsed;
}
} else {
match &self.spme_force_prev {
Some((forces, potential_e, virial_e)) => {
let water_start = self.atoms.len();
for (i, f) in forces.iter().enumerate() {
if i < water_start {
self.atoms[i].force += *f;
} else {
let i_wat = i - water_start;
let i_wat_mol = i_wat / 3;
match i_wat % 3 {
0 => self.water[i_wat_mol].m.force += *f,
1 => self.water[i_wat_mol].h0.force += *f,
_ => self.water[i_wat_mol].h1.force += *f,
}
}
}
self.potential_energy += potential_e;
self.potential_energy_nonbonded += potential_e;
self.barostat.virial.nonbonded_long_range += virial_e;
}
None => {
eprintln!(
"Error! Attempting to use cached previous SPME forces, but it's not set"
);
}
}
}
}
if let Some(f_ext) = external_force {
for (i, f) in f_ext.iter().enumerate() {
self.atoms[i].force += *f;
}
}
}
pub fn compute_dh_dl(&self) -> f64 {
let m = match self.alch_mol_idx {
Some(m) => m,
None => return 0.0,
};
let n = self.mol_start_indices.len();
let u_interact: f64 = (0..n)
.filter(|&j| j != m)
.map(|j| self.potential_energy_between_mols[m * n + j])
.sum();
-u_interact
}
}
impl Display for MdState {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"MdState. # Snapshots: {}. # steps: {} Current time: {}. # of dynamic atoms: {}. # Water mols: {}",
self.snapshots.len(),
self.step_count,
self.time,
self.atoms.len(),
self.water.len()
)
}
}
pub(crate) fn split2_mut<T>(v: &mut [T], i: usize, j: usize) -> (&mut T, &mut T) {
assert!(i != j);
let (low, high) = if i < j { (i, j) } else { (j, i) };
let (left, right) = v.split_at_mut(high);
(&mut left[low], &mut right[0])
}
fn split3_mut<T>(v: &mut [T], i: usize, j: usize, k: usize) -> (&mut T, &mut T, &mut T) {
let len = v.len();
assert!(i < len && j < len && k < len, "index out of bounds");
assert!(i != j && i != k && j != k, "indices must be distinct");
let ptr = v.as_mut_ptr();
unsafe {
let a = &mut *ptr.add(i);
let b = &mut *ptr.add(j);
let c = &mut *ptr.add(k);
(a, b, c)
}
}
pub(crate) fn split4_mut<T>(
slice: &mut [T],
i0: usize,
i1: usize,
i2: usize,
i3: usize,
) -> (&mut T, &mut T, &mut T, &mut T) {
let len = slice.len();
assert!(
i0 < len && i1 < len && i2 < len && i3 < len,
"index out of bounds"
);
assert!(
i0 != i1 && i0 != i2 && i0 != i3 && i1 != i2 && i1 != i3 && i2 != i3,
"indices must be pair-wise distinct"
);
unsafe {
let base = slice.as_mut_ptr();
(
&mut *base.add(i0),
&mut *base.add(i1),
&mut *base.add(i2),
&mut *base.add(i3),
)
}
}
pub fn compute_energy_snapshot(
dev: &ComputationDevice,
mols: &[MolDynamics],
param_set: &FfParamSet,
) -> Result<Snapshot, ParamError> {
let cfg = MdConfig {
integrator: Integrator::VerletVelocity { thermostat: None },
hydrogen_constraint: HydrogenConstraint::Flexible,
max_init_relaxation_iters: None,
solvent: Solvent::None,
..Default::default()
};
let mut md_state = MdState::new(dev, &cfg, mols, param_set)?;
let dt = 0.001;
md_state.step(dev, dt, None);
if md_state.snapshots.is_empty() {
return Err(ParamError {
descrip: String::from("Snapshots empty on energy compuptation"),
});
}
Ok(md_state.snapshots[0].clone())
}
fn add_ions(state: &mut MdState, net_q_e: f32, n_ions: usize) {
if n_ions > 0 && !state.water.is_empty() {
let (ff_type, elem, mass, q_scaled, sigma, eps): (&str, Element, f32, f32, f32, f32) =
if net_q_e > 0.0 {
(
"Cl-",
Element::Chlorine,
35.45,
-CHARGE_UNIT_SCALER,
4.478,
0.0073,
)
} else {
(
"Na+",
Element::Sodium,
22.99,
CHARGE_UNIT_SCALER,
2.439,
0.1065,
)
};
let stride = (state.water.len() / n_ions).max(1);
let mut water_to_remove: Vec<usize> = (0..n_ions)
.map(|i| (i * stride).min(state.water.len() - 1))
.collect();
for (k, &w_idx) in water_to_remove.iter().enumerate() {
let atom_idx = state.atoms.len() + k;
let posit = state.water[w_idx].o.posit;
state.atoms.push(AtomDynamics {
serial_number: atom_idx as u32,
force_field_type: ff_type.to_string(),
element: elem,
posit,
mass,
partial_charge: q_scaled,
lj_sigma: sigma,
lj_eps: eps,
..Default::default()
});
state.force_field_params.mass.insert(
atom_idx,
MassParams {
atom_type: ff_type.to_string(),
mass,
comment: None,
},
);
state.force_field_params.lennard_jones.insert(
atom_idx,
LjParams {
atom_type: ff_type.to_string(),
sigma,
eps,
},
);
state.adjacency_list.push(Vec::new());
state.mass_accel_factor.push(KCAL_TO_NATIVE / mass);
state.mol_start_indices.push(atom_idx);
}
let n_mols = state.mol_start_indices.len();
state
.potential_energy_between_mols
.resize(n_mols.pow(2), 0.0);
water_to_remove.sort_unstable_by(|a, b| b.cmp(a));
water_to_remove.dedup();
for idx in water_to_remove {
state.water.remove(idx);
}
eprintln!(
"Added {n_ions} {} ion(s) to neutralize net charge ({net_q_e:+.3}e).",
ff_type
);
} else if n_ions > 0 {
eprintln!(
"Warning: net charge {net_q_e:+.3}e detected but no solvent available to \
displace; skipping ion insertion."
);
}
}