dynamics/lib.rs
1#![allow(non_snake_case)]
2
3//! This module contains a traditional molecular dynamics approach
4//!
5//! [Good article](https://www.owlposting.com/p/a-primer-on-molecular-dynamics)
6//! [A summary paper](https://arxiv.org/pdf/1401.1181)
7//!
8//! [Amber Force Fields reference](https://ambermd.org/AmberModels.php)
9//! [Small molucules using GAFF2](https://ambermd.org/downloads/amber_geostd.tar.bz2)
10//! [Amber RM 2025](https://ambermd.org/doc12/Amber25.pdf)
11//!
12//! To download .dat files (GAFF2), download Amber source (Option 2) [here](https://ambermd.org/GetAmber.php#ambertools).
13//! Files are in dat -> leap -> parm
14//!
15//! Base units: Å, ps (10^-12), Dalton (AMU), native charge units (derive from other base units;
16//! not a traditional named unit).
17//!
18//! We are using f64, and CPU-only for now, unless we confirm f32 will work.
19//! Maybe a mixed approach: Coordinates, velocities, and forces in 32-bit; sensitive global
20//! reductions (energy, virial, integration) in 64-bit.
21//!
22//! We use Verlet integration. todo: Velocity verlet? Other techniques that improve and build upon it?
23//!
24//! Amber: ff19SB for proteins, gaff2 for ligands. (Based on recs from https://ambermd.org/AmberModels.php).
25//!
26//! We use the term "Non-bonded" interactions to refer to Coulomb, and Lennard Interactions, the latter
27//! of which is an approximation for Van der Waals force.
28//!
29//! ## A broad list of components of this simulation:
30//! - Atoms are divided into three categories:
31//! -- Dynamic: Atoms that move
32//! -- Static: Atoms that don't move, but have mutual non-bonded interactions with dynamic atoms and water
33//! -- Water: Rigid OPC water molecules that have mutual non-bonded interactions with dynamic atoms and water
34//!
35//! - Thermostat/barostat, with a way to specify temp, pressure, water density
36//! - OPC water model
37//! - Cell wrapping
38//! - Verlet integration (Water and non-water)
39//! - Amber parameters for mass, partial charge, VdW (via LJ), dihedral/improper, angle, bond len
40//! - Optimizations for Coulomb: Ewald/PME/SPME?
41//! - Optimizations for LJ: Dist cutoff for now.
42//! - Amber 1-2, 1-3 exclusions, and 1-4 scaling of covalently-bonded atoms.
43//! - Rayon parallelization of non-bonded forces
44//! - WIP SIMD and CUDA parallelization of non-bonded forces, depending on hardware availability. todo
45//! - A thermostat+barostat for the whole system. (Is water and dyn separate here?) todo
46//! - An energy-measuring system.
47//!
48//! --------
49//! A timing test, using bond-stretching forces between two atoms only. Measure the period
50//! of oscillation for these atom combinations, e.g. using custom Mol2 files.
51//! c6-c6: 35fs (correct). os-os: 47fs nc-nc: 34fs hw-hw: 9fs
52//! Our measurements, 2025-08-04
53//! c6-c6: 35fs os-os: 31fs nc-nc: 34fs (Correct) hw-hw: 6fs
54//!
55//! --------
56//!
57//! We use traditional MD non-bonded terms to maintain geometry: Bond length, valence angle between
58//! 3 bonded atoms, dihedral angle between 4 bonded atoms (linear), and improper dihedral angle between
59//! each hub and 3 spokes. (E.g. at ring intersections). We also apply Coulomb force between atom-centered
60//! partial charges, and Lennard Jones potentials to simulate Van der Waals forces. These use spring-like
61//! forces to retain most geometry, while allowing for flexibility.
62//!
63//! We use the OPC water model. (See `water_opc.rs`). For both maintaining the geometry of each water
64//! molecule, and for maintaining Hydrogen atom positions, we do not apply typical non-bonded interactions:
65//! We use SHAKE + RATTLE algorithms for these. In the case of water, it's required for OPC compliance.
66//! For H, it allows us to maintain integrator stability with a greater timestep, e.g. 2fs instead of 1fs.
67//!
68//! On f32 vs f64 floating point precision: f32 may be good enough for most things, and typical MD packages
69//! use mixed precision. Long-range electrostatics are a good candidate for using f64. Or, very long
70//! runs.
71//!
72//! Note on performance: It appears that non-bonded forces dominate computation time. This is my observation,
73//! and it's confirmed by an LLM. Both LJ and Coulomb take up most of the time; bonded forces
74//! are comparatively insignificant. Building neighbor lists are also significant. These are the areas
75//! we focus on for parallel computation (Thread pools, SIMD, CUDA)
76
77// todo: You should keep more data on the GPU betwween time steps, instead of passing back and
78// todo forth each time. If practical.
79
80extern crate core;
81
82mod add_hydrogens;
83mod ambient;
84mod bonded;
85mod bonded_forces;
86mod forces;
87pub mod integrate;
88mod neighbors;
89mod non_bonded;
90pub mod params;
91mod prep;
92pub mod snapshot;
93mod util;
94mod water_init;
95mod water_opc;
96mod water_settle;
97
98#[cfg(feature = "cuda")]
99use std::sync::Arc;
100use std::{
101 collections::HashSet,
102 fmt,
103 fmt::{Display, Formatter},
104 path::Path,
105 time::Instant,
106};
107
108pub use add_hydrogens::{add_hydrogens_2::Dihedral, populate_hydrogens_dihedrals};
109use ambient::SimBox;
110#[cfg(feature = "encode")]
111use bincode::{Decode, Encode};
112use bio_files::{AtomGeneric, BondGeneric, md_params::ForceFieldParams, mmcif::MmCif, mol2::Mol2};
113#[cfg(feature = "cuda")]
114use cudarc::driver::{CudaModule, CudaStream};
115use ewald::{PmeRecip, ewald_comp_force};
116pub use integrate::Integrator;
117#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
118use lin_alg::f64::{Vec3x4, f64x4};
119use lin_alg::{f32::Vec3 as Vec3F32, f64::Vec3};
120use na_seq::Element;
121use neighbors::NeighborsNb;
122pub use params::{ProtFFTypeChargeMap, ProtFfMap};
123pub use prep::{HydrogenConstraint, merge_params};
124use rand::Rng;
125use rand_distr::StandardNormal;
126pub use util::{load_snapshots, save_snapshots};
127pub use water_opc::ForcesOnWaterMol;
128
129use crate::{
130 ambient::BerendsenBarostat,
131 non_bonded::{
132 CHARGE_UNIT_SCALER, EWALD_ALPHA, LjTableIndices, LjTables, SCALE_COUL_14, SPME_N,
133 },
134 params::{FfParamSet, ForceFieldParamsIndexed},
135 snapshot::{FILE_SAVE_INTERVAL, SaveType, Snapshot, SnapshotHandler, append_dcd},
136 util::build_adjacency_list,
137 water_init::make_water_mols,
138 water_opc::WaterMol,
139};
140
141// Note: If you haven't generated this file yet when compiling (e.g. from a freshly-cloned repo),
142// make an edit to one of the CUDA files (e.g. add a newline), then run, to create this file.
143#[cfg(feature = "cuda")]
144pub const PTX: &str = include_str!("../dynamics.ptx");
145
146/// Convert convert kcal mol⁻¹ Å⁻¹ (Values in the Amber parameter files) to amu Å ps⁻². Multiply all bonded
147/// accelerations by this. TODO: we are currently multiplying *all* accelerations by this.
148const ACCEL_CONVERSION: f64 = 418.4;
149const ACCEL_CONVERSION_F32: f32 = 418.4;
150const ACCEL_CONVERSION_INV: f64 = 1. / ACCEL_CONVERSION;
151const ACCEL_CONVERSION_INV_F32: f32 = 1. / ACCEL_CONVERSION_F32;
152
153// For assigning velocities from temperature, and other thermostat/barostat use.
154const KB: f32 = 0.001_987_204_1; // kcal mol⁻¹ K⁻¹ (Amber-style units)
155
156// Boltzmann constant in (amu · Å^2 / ps^2) K⁻¹
157const KB_A2_PS2_PER_K_PER_AMU: f32 = 0.831_446_26;
158
159// SHAKE tolerances for fixed hydrogens. These SHAKE constraints are for fixed hydrogens.
160// The tolerance controls how close we get
161// to the target value; lower values are more precise, but require more iterations. `SHAKE_MAX_ITER`
162// constrains the number of iterations.
163const SHAKE_TOL: f32 = 1.0e-4; // Å
164const SHAKE_MAX_IT: usize = 100;
165
166// Every this many steps, re-
167const CENTER_SIMBOX_RATIO: usize = 30;
168
169#[derive(Debug, Clone, Default)]
170pub enum ComputationDevice {
171 #[default]
172 Cpu,
173 #[cfg(feature = "cuda")]
174 Gpu((Arc<CudaStream>, Arc<CudaModule>)),
175}
176
177/// Represents problems loading parameters. For example, if an atom is missing a force field type
178/// or partial charge, or has a force field type that hasn't been loaded.
179#[derive(Clone, Debug)]
180pub struct ParamError {
181 pub descrip: String,
182}
183
184impl ParamError {
185 pub fn new(descrip: &str) -> Self {
186 Self {
187 descrip: descrip.to_owned(),
188 }
189 }
190}
191
192/// This is used to assign the correct force field parameters to a molecule.
193#[derive(Clone, Copy, PartialEq, Debug)]
194pub enum FfMolType {
195 /// Protein or other construct of amino acids
196 Peptide,
197 /// E.g. a ligand.
198 SmallOrganic,
199 Dna,
200 Rna,
201 Lipid,
202 Carbohydrate,
203}
204
205/// Packages information required to perform dynamics on a Molecule. This is used to initialize
206/// the simulation with atoms and related; one or more of these is passed at init.
207#[derive(Clone, Debug)]
208pub struct MolDynamics<'a> {
209 pub ff_mol_type: FfMolType,
210 /// These must hold force field type and partial charge.
211 pub atoms: &'a [AtomGeneric],
212 /// Separate from `atoms`; this may be more convenient than mutating the atoms
213 /// as they may move! If None, we use the positions stored in the atoms.
214 pub atom_posits: Option<&'a [Vec3]>,
215 /// Not required if static.
216 pub bonds: &'a [BondGeneric],
217 /// If None, will be generated automatically from atoms and bonds. Use this
218 /// if you wish to cache.
219 pub adjacency_list: Option<&'a [Vec<usize>]>,
220 /// If true, the atoms in the molecule don't move, but exert LJ and Coulomb forces
221 /// on other atoms in the system.
222 pub static_: bool,
223 /// If present, any values here override molecule-type general parameters.
224 pub mol_specific_params: Option<&'a ForceFieldParams>,
225}
226
227// impl MolDynamics<'_> {
228// /// Load an Amber Geostd molecule from an online database.
229// /// todo: Wonky due to use of refs.
230// pub fn from_amber_geostd(ident: &str) -> io::Result<Self> {
231// let data = bio_apis::amber_geostd::load_mol_files("CPB").map_err(|e| io::Error::new(io::ErrorKind::Other, "Error loading data"))?;
232// let mol = Mol2::new(&data.mol2)?;
233// let params = ForceFieldParams::from_frcmod(&data.frcmod.unwrap())?;
234//
235// Ok(Self {
236// ff_mol_type: FfMolType::SmallOrganic,
237// atoms: &mol.atoms,
238// atom_posits: None,
239// bonds: &mol.bonds,
240// adjacency_list: None,
241// static_: false,
242// mol_specific_params: Some(¶ms)
243// })
244// }
245// }
246
247/// A trimmed-down atom for use with molecular dynamics. Contains parameters for single-atom,
248/// but we use ParametersIndex for multi-atom parameters.
249#[derive(Clone, Debug, Default)]
250pub struct AtomDynamics {
251 pub serial_number: u32,
252 /// Sources that affect atoms in the system, but are not themselves affected by it. E.g.
253 /// in docking, this might be a rigid receptor. These are for *non-bonded* interactions (e.g. Coulomb
254 /// and VDW) only.
255 pub static_: bool,
256 pub force_field_type: String,
257 pub element: Element,
258 // pub name: String,
259 pub posit: Vec3F32,
260 /// Å / ps
261 pub vel: Vec3F32,
262 /// Å / ps²
263 pub accel: Vec3F32,
264 /// Daltons
265 /// todo: Move these 4 out of this to save memory; use from the params struct directly.
266 pub mass: f32,
267 /// Amber charge units. This is not the elementary charge units found in amino19.lib and gaff2.dat;
268 /// it's scaled by a constant.
269 pub partial_charge: f32,
270 /// Å
271 pub lj_sigma: f32,
272 /// kcal/mol
273 pub lj_eps: f32,
274}
275
276impl Display for AtomDynamics {
277 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
278 write!(
279 f,
280 "Atom {}: {}, {}. ff: {}, q: {}",
281 self.serial_number,
282 self.element.to_letter(),
283 self.posit,
284 self.force_field_type,
285 self.partial_charge,
286 )?;
287
288 if self.static_ {
289 write!(f, ", Static")?;
290 }
291
292 Ok(())
293 }
294}
295
296impl AtomDynamics {
297 pub fn new(
298 atom: &AtomGeneric,
299 atom_posits: &[Vec3F32],
300 i: usize,
301 static_: bool,
302 ) -> Result<Self, ParamError> {
303 let ff_type = match &atom.force_field_type {
304 Some(ff_type) => ff_type.clone(),
305 None => {
306 return Err(ParamError::new(&format!(
307 "Atom missing FF type; can't run dynamics: {:?}",
308 atom
309 )));
310 }
311 };
312
313 Ok(Self {
314 serial_number: atom.serial_number,
315 static_,
316 element: atom.element,
317 // name: atom.type_in_res.clone().unwrap_or_default(),
318 posit: atom_posits[i],
319 force_field_type: ff_type,
320 ..Default::default()
321 })
322 }
323
324 /// Populate atom-specific parameters.
325 /// E.g. we use this workflow if creating the atoms prior to the indexed FF.
326 pub(crate) fn assign_data_from_params(
327 &mut self,
328 ff_params: &ForceFieldParamsIndexed,
329 // ff_params: &ForceFieldParams,
330 i: usize,
331 ) {
332 // mass: ff_params.mass.get(&i).unwrap().mass as f64,
333 self.mass = ff_params.mass[&i].mass;
334 // We get partial charge for ligands from (e.g. Amber-provided) Mol files, so we load it from the atom, vice
335 // the loaded FF params. They are not in the dat or frcmod files that angle, bond-length etc params are from.
336 self.partial_charge = CHARGE_UNIT_SCALER * self.partial_charge;
337 // lj_sigma: ff_params.lennard_jones.get(&i).unwrap().sigma as f64,
338 self.lj_sigma = ff_params.lennard_jones[&i].sigma;
339 // lj_eps: ff_params.lennard_jones.get(&i).unwrap().eps as f64,
340 self.lj_eps = ff_params.lennard_jones[&i].eps;
341 }
342}
343
344#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
345#[derive(Clone, Debug)]
346pub(crate) struct AtomDynamicsx4 {
347 // pub posit: Vec3x8,
348 // pub vel: Vec3x8,
349 // pub accel: Vec3x8,
350 // pub mass: f32x8,
351 pub posit: Vec3x4,
352 pub vel: Vec3x4,
353 pub accel: Vec3x4,
354 pub mass: f64x4,
355 pub element: [Element; 4],
356}
357
358// #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
359// impl AtomDynamicsx4 {
360// pub fn from_array(bodies: [AtomDynamics; 4]) -> Self {
361// let mut posits = [Vec3F32::new_zero(); 4];
362// let mut vels = [Vec3F32::new_zero(); 4];
363// let mut accels = [Vec3F32::new_zero(); 4];
364// let mut masses = [0.0; 4];
365// // Replace `Element::H` (for example) with some valid default for your `Element` type:
366// let mut elements = [Element::Hydrogen; 4];
367//
368// for (i, body) in bodies.into_iter().enumerate() {
369// posits[i] = body.posit;
370// vels[i] = body.vel;
371// accels[i] = body.accel;
372// masses[i] = body.mass;
373// elements[i] = body.element;
374// }
375//
376// Self {
377// posit: Vec3x4::from_array(posits),
378// vel: Vec3x4::from_array(vels),
379// accel: Vec3x4::from_array(accels),
380// mass: f64x4::from_array(masses),
381// element: elements,
382// }
383// }
384// }
385
386// todo: FIgure out how to apply this to python.
387/// Note: The shortest edge should be > 2(r_cutoff + r_skin), to prevent atoms
388/// from interacting with their own image in the real-space component.
389#[cfg_attr(feature = "encode", derive(Encode, Decode))]
390#[derive(Debug, Clone)]
391pub enum SimBoxInit {
392 /// Distance in Å from the edge to the molecule, at init.
393 Pad(f32),
394 /// Coordinate boundaries, at opposite corners
395 Fixed((Vec3F32, Vec3F32)),
396}
397
398impl Default for SimBoxInit {
399 fn default() -> Self {
400 Self::Pad(10.)
401 }
402}
403
404#[cfg_attr(feature = "encode", derive(Encode, Decode))]
405#[derive(Debug, Clone)]
406pub struct MdConfig {
407 /// Defaults to Velocity Verlet.
408 pub integrator: Integrator,
409 /// If enabled, zero the drift in center of mass of the system.
410 /// todo: Implement
411 pub zero_com_drift: bool,
412 /// Kelvin. Defaults to 310 K.
413 pub temp_target: f32,
414 /// Bar (Pa/100). Defaults to 1 bar.
415 pub pressure_target: f32,
416 /// Allows constraining Hydrogens to be rigid with their bonded atom, using SHAKE and RATTLE
417 /// algorithms. This allows for higher time steps.
418 pub hydrogen_constraint: HydrogenConstraint,
419 pub snapshot_handlers: Vec<SnapshotHandler>,
420 pub sim_box: SimBoxInit,
421}
422
423impl Default for MdConfig {
424 fn default() -> Self {
425 Self {
426 integrator: Default::default(),
427 zero_com_drift: false, // todo: True?
428 temp_target: 310.,
429 pressure_target: 1.,
430 hydrogen_constraint: Default::default(),
431 // snapshot_ratio_memory: 1,
432 // snapshot_ratio_file: 2,
433 // snapshot_path: None,
434 snapshot_handlers: vec![SnapshotHandler {
435 save_type: SaveType::Memory,
436 ratio: 1,
437 }],
438 sim_box: Default::default(),
439 }
440 }
441}
442
443#[derive(Default)]
444pub struct MdState {
445 // todo: Update how we handle mode A/R.
446 // todo: You need to rework this state in light of arbitrary mol count.
447 pub cfg: MdConfig,
448 pub atoms: Vec<AtomDynamics>,
449 pub adjacency_list: Vec<Vec<usize>>,
450 // h_constraints: Vec<HydrogenConstraintInner>,
451 // /// Sources that affect atoms in the system, but are not themselves affected by it. E.g.
452 // /// in docking, this might be a rigid receptor. These are for *non-bonded* interactions (e.g. Coulomb
453 // /// and VDW) only.
454 // pub atoms_static: Vec<AtomDynamics>,
455 // todo: Make this a vec. For each dynamic atom.
456 // todo: We don't need it for static, as they use partial charge and LJ data, which
457 // todo are assigned to each atom.
458 pub force_field_params: ForceFieldParamsIndexed,
459 // /// `lj_lut`, `lj_sigma`, and `lj_eps` are Lennard Jones parameters. Flat here, with outer loop receptor.
460 // /// Flattened. Separate single-value array facilitate use in CUDA and SIMD, vice a tuple.
461 // pub lj_sigma: Vec<f64>,
462 // pub lj_eps: Vec<f64>,
463 // todo: Implment these SIMD variants A/R, bearing in mind the caveat about our built-in ones vs
464 // todo ones loaded from [e.g. Amber] files.
465 // #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
466 // pub lj_sigma_x8: Vec<f64x4>,
467 // #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
468 // pub lj_eps_x8: Vec<f64x4>,
469 /// Current simulation time, in picoseconds.
470 pub time: f64,
471 pub step_count: usize, // increments.
472 /// These are the snapshots we keep in memory, accumulating.
473 pub snapshots: Vec<Snapshot>,
474 pub cell: SimBox,
475 pub neighbors_nb: NeighborsNb,
476 // max_disp_sq: f64, // track atom displacements²
477 /// K
478 barostat: BerendsenBarostat,
479 /// Exclusions of non-bonded forces for atoms connected by 1, or 2 covalent bonds.
480 /// I can't find this in the RM, but ChatGPT is confident of it, and references an Amber file
481 /// called 'prmtop', which I can't find. Fishy, but we're going with it.
482 pairs_excluded_12_13: HashSet<(usize, usize)>,
483 /// See Amber RM, sectcion 15, "1-4 Non-Bonded Interaction Scaling"
484 /// These are indices of atoms separated by three consecutive bonds
485 pairs_14_scaled: HashSet<(usize, usize)>,
486 water: Vec<WaterMol>,
487 lj_tables: LjTables,
488 // todo: Hmm... Is this DRY with forces_on_water? Investigate.
489 pub water_pme_sites_forces: Vec<[Vec3; 3]>, // todo: A/R
490 pme_recip: Option<PmeRecip>,
491 /// kcal/mol
492 pub kinetic_energy: f64,
493 pub potential_energy: f64,
494 /// Every so many snapshots, write these to file, then clear from memory.
495 snapshot_queue_for_file: Vec<Snapshot>,
496}
497
498impl fmt::Display for MdState {
499 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
500 write!(
501 f,
502 "MdState. # Snapshots: {}. # steps: {} Current time: {}. # of dynamic atoms: {}. # Water mols: {}",
503 self.snapshots.len(),
504 self.step_count,
505 self.time,
506 self.atoms.len(),
507 self.water.len()
508 )
509 }
510}
511
512impl MdState {
513 pub fn new(
514 cfg: &MdConfig,
515 mols: &[MolDynamics],
516 param_set: &FfParamSet,
517 ) -> Result<Self, ParamError> {
518 // todo: QC how you handle hydrogen_md_type.
519
520 if mols.iter().filter(|m| !m.static_).count() != 1 {
521 return Err(ParamError::new(
522 &"We currently only support exactly 1 dynamic molecule. Sorry!",
523 ));
524 }
525
526 // We create a flattened atom list, which simplifies our workflow, and is conducive to
527 // parallel operations.
528 // These Vecs all share indices, and all include all molecules.
529 let mut atoms_md = Vec::new();
530 let mut adjacency_list = Vec::new();
531
532 // let mut atoms_md_static: Vec<AtomDynamics> = Vec::new();
533
534 // todo: Sort this out. One set for all molecules now. Assumes unique FF names between
535 // todo differenet set types.
536 // let mut ff_params: ForceFieldParamsIndexed = Default::default();
537
538 // We combine all molecule general and specific params into this set, then
539 // create Indexed params from it.
540 let mut params = ForceFieldParams::default();
541
542 // todo: Make sure you don't use atom SN anywhere in the MD pipeline.
543 // let mut last_mol_sn = 0;
544
545 // Used for updating indices for tracking purposes.
546 let mut total_atom_count = 0;
547
548 for (i, mol) in mols.iter().enumerate() {
549 // Filter out hetero atoms in proteins. These are often example ligands that we do
550 // not wish to model.
551 // We must perform this filter prior to most of the other steps in this function.
552 let atoms: Vec<AtomGeneric> = match mol.ff_mol_type {
553 FfMolType::Peptide => mol
554 .atoms
555 .into_iter()
556 .filter(|a| !a.hetero)
557 .map(|a| a.clone())
558 .collect(),
559 _ => mol.atoms.to_vec(),
560 };
561
562 // If the atoms list isn't already filtered by Hetero, and a manual
563 // adjacency list or atom posits is passed, this will get screwed up.
564 if mol.ff_mol_type == FfMolType::Peptide
565 && atoms.len() != mol.atoms.len()
566 && (mol.adjacency_list.is_some() || mol.atom_posits.is_some())
567 {
568 return Err(ParamError::new(
569 "Unable to perform MD on this peptide: If passing atom positions or an adjacency list,\
570 you must already have filtered out hetero atoms. We found one or more hetero atoms in the input.",
571 ));
572 }
573
574 {
575 let params_general = match mol.ff_mol_type {
576 FfMolType::Peptide => ¶m_set.peptide,
577 FfMolType::SmallOrganic => ¶m_set.small_mol,
578 FfMolType::Dna => ¶m_set.dna,
579 FfMolType::Rna => ¶m_set.rna,
580 FfMolType::Lipid => ¶m_set.lipids,
581 FfMolType::Carbohydrate => ¶m_set.carbohydrates,
582 };
583
584 let Some(params_general) = params_general else {
585 return Err(ParamError::new(&format!(
586 "Missing general parameters for {:?}",
587 mol.ff_mol_type
588 )));
589 };
590
591 // todo: If there are multiple molecules of a given type, this is unnecessary.
592 // todo: Make sure overrides from one individual molecule don't affect others, todo,
593 // todo and don't affect general params.
594 params = merge_params(¶ms, ¶ms_general);
595
596 if let Some(p) = mol.mol_specific_params {
597 params = merge_params(¶ms, &p);
598 }
599 }
600
601 let mut p: Vec<Vec3F32> = Vec::new(); // to store the ref.
602 let atom_posits = match mol.atom_posits {
603 Some(a) => {
604 p = a.iter().map(|p| (*p).into()).collect();
605 &p
606 }
607 None => {
608 p = mol.atoms.iter().map(|a| a.posit.into()).collect();
609 &p
610 }
611 };
612
613 for (i, atom) in atoms.iter().enumerate() {
614 // atom.serial_number += last_mol_sn;
615 atoms_md.push(AtomDynamics::new(
616 &atom,
617 atom_posits,
618 // ¶ms,
619 i,
620 mol.static_,
621 )?);
622 }
623
624 // Use the included adjacency list if available. If not, construct it.
625 let adjacency_list_ = match mol.adjacency_list {
626 Some(a) => a,
627 None => &build_adjacency_list(&atoms, mol.bonds)?,
628 };
629
630 for aj in adjacency_list_ {
631 let mut updated = aj.clone();
632 for neighbor in &mut updated {
633 *neighbor += total_atom_count;
634 }
635
636 adjacency_list.push(updated);
637 }
638
639 // for constraint in h_constraints_ {
640 //
641 // }
642
643 // atoms_md.extend(atoms_md);
644
645 // if mol.static_ {
646 // // atoms_md_static.extend(atoms_md);
647 // } else {
648 // // Set up Indexed params. Merges general with atom-specific if available.
649 // // Not required for static atoms. (Only applies to bonded forces.)
650 // // ff_params = params;
651 // adjacency_list = adjacency_list_.to_vec();
652 // }
653
654 // h_constraints.push(h_constraints_);
655
656 total_atom_count += atoms.len();
657 }
658
659 let force_field_params = ForceFieldParamsIndexed::new(
660 ¶ms,
661 // mol.mol_specific_params,
662 &atoms_md,
663 // mol.bonds,
664 &adjacency_list,
665 // &mut h_constraints,
666 cfg.hydrogen_constraint,
667 )?;
668
669 // Assign mass, LJ params, etc.
670 for (i, atom) in atoms_md.iter_mut().enumerate() {
671 atom.assign_data_from_params(&force_field_params, i);
672 }
673
674 // let cell = SimBox::new_padded(&atoms_dy);
675 let cell = SimBox::new(&atoms_md, &cfg.sim_box);
676
677 let mut result = Self {
678 cfg: cfg.clone(),
679 atoms: atoms_md,
680 adjacency_list: adjacency_list.to_vec(),
681 // h_constraints,
682 // atoms_static: atoms_md_static,
683 cell,
684 pairs_excluded_12_13: HashSet::new(),
685 pairs_14_scaled: HashSet::new(),
686 force_field_params,
687 ..Default::default()
688 };
689
690 result.barostat.pressure_target = cfg.pressure_target as f64;
691
692 result.water = make_water_mols(
693 &result.cell,
694 cfg.temp_target,
695 &result.atoms,
696 // &result.atoms_static,
697 );
698 result.water_pme_sites_forces = vec![[Vec3::new_zero(); 3]; result.water.len()];
699
700 result.setup_nonbonded_exclusion_scale_flags();
701 result.init_neighbors();
702 // Initializes the FFT planner[s], among other things.
703 result.regen_pme();
704
705 // Set up our LJ cache.
706 result.lj_tables = LjTables::new(&result.atoms, &result.water);
707
708 Ok(result)
709 }
710
711 /// Reset acceleration and virial pair. Do this each step after the first half-step and drift, and
712 /// shaking the fixed hydrogens.
713 /// We must reset the virial pair prior to accumulating it, which we do when calculating non-bonded
714 /// forces. Also reset forces on water.
715 fn reset_accels(&mut self) {
716 for a in &mut self.atoms {
717 a.accel = Vec3F32::new_zero();
718 }
719 for mol in &mut self.water {
720 mol.o.accel = Vec3F32::new_zero();
721 mol.m.accel = Vec3F32::new_zero();
722 mol.h0.accel = Vec3F32::new_zero();
723 mol.h1.accel = Vec3F32::new_zero();
724 }
725
726 self.barostat.virial_pair_kcal = 0.0;
727 self.potential_energy = 0.;
728 }
729
730 fn apply_all_forces(&mut self, dev: &ComputationDevice) {
731 // Bonded forces
732 let mut start = Instant::now();
733 self.apply_bond_stretching_forces();
734
735 if self.step_count == 0 {
736 let elapsed = start.elapsed();
737 println!("Bond stretching time: {:?} μs", elapsed.as_micros());
738 }
739
740 if self.step_count == 0 {
741 start = Instant::now();
742 }
743 self.apply_angle_bending_forces();
744
745 if self.step_count == 0 {
746 let elapsed = start.elapsed();
747 println!("Angle bending time: {:?} μs", elapsed.as_micros());
748 }
749
750 if self.step_count == 0 {
751 start = Instant::now();
752 }
753
754 self.apply_dihedral_forces(false);
755 if self.step_count == 0 {
756 let elapsed = start.elapsed();
757 println!("Dihedral: {:?} μs", elapsed.as_micros());
758 }
759
760 if self.step_count == 0 {
761 start = Instant::now();
762 }
763
764 self.apply_dihedral_forces(true);
765 if self.step_count == 0 {
766 let elapsed = start.elapsed();
767 println!("Improper time: {:?} μs", elapsed.as_micros());
768 }
769
770 if self.step_count == 0 {
771 start = Instant::now();
772 }
773
774 // Note: Non-bonded takes the vast majority of time.
775 self.apply_nonbonded_forces(dev);
776 if self.step_count == 0 {
777 let elapsed = start.elapsed();
778 println!("Non-bonded time: {:?} μs", elapsed.as_micros());
779 }
780 }
781
782 /// Relaxes the molecules. Use this at the start of the simulation to control kinetic energy that
783 /// arrises from differences between atom positions, and bonded parameters.
784 fn minimize_energy(&mut self, dev: &ComputationDevice) {}
785
786 // todo: For calling by user at the end (temp), don't force it to append the path.
787 //todo: DRY with in the main step path (Doesn't call this) to avoid a dbl borrow.
788 pub fn save_snapshots_to_file(&mut self, path: &Path) {
789 if self.step_count % FILE_SAVE_INTERVAL == 0 {
790 if let Err(e) = append_dcd(&self.snapshot_queue_for_file, &path) {
791 eprintln!("Error saving snapshot as DCD: {e:?}");
792 }
793 self.snapshot_queue_for_file = Vec::new();
794 }
795 }
796
797 /// Note: This is currently only for the dynamic atoms; does not take water kinetic energy into account.
798 fn current_kinetic_energy(&self) -> f64 {
799 self.atoms
800 .iter()
801 .map(|a| 0.5 * (a.mass * a.vel.magnitude_squared()) as f64)
802 .sum()
803 }
804
805 fn take_snapshot(&self) -> Snapshot {
806 let mut water_o_posits = Vec::with_capacity(self.water.len());
807 let mut water_h0_posits = Vec::with_capacity(self.water.len());
808 let mut water_h1_posits = Vec::with_capacity(self.water.len());
809 let mut water_velocities = Vec::with_capacity(self.water.len());
810
811 for water in &self.water {
812 water_o_posits.push(water.o.posit);
813 water_h0_posits.push(water.h0.posit);
814 water_h1_posits.push(water.h1.posit);
815 water_velocities.push(water.o.vel); // Can be from any atom; they should be the same.
816 }
817
818 Snapshot {
819 time: self.time,
820 atom_posits: self.atoms.iter().map(|a| a.posit).collect(),
821 atom_velocities: self.atoms.iter().map(|a| a.vel).collect(),
822 water_o_posits,
823 water_h0_posits,
824 water_h1_posits,
825 water_velocities,
826 energy_kinetic: self.kinetic_energy as f32,
827 energy_potential: self.potential_energy as f32,
828 }
829 }
830}
831
832#[inline]
833/// Mutable aliasing helpers.
834pub(crate) fn split2_mut<T>(v: &mut [T], i: usize, j: usize) -> (&mut T, &mut T) {
835 assert!(i != j);
836
837 let (low, high) = if i < j { (i, j) } else { (j, i) };
838 let (left, right) = v.split_at_mut(high);
839 (&mut left[low], &mut right[0])
840}
841
842#[inline]
843fn split3_mut<T>(v: &mut [T], i: usize, j: usize, k: usize) -> (&mut T, &mut T, &mut T) {
844 let len = v.len();
845 assert!(i < len && j < len && k < len, "index out of bounds");
846 assert!(i != j && i != k && j != k, "indices must be distinct");
847
848 // SAFETY: we just asserted that 0 <= i,j,k < v.len() and that they're all different.
849 let ptr = v.as_mut_ptr();
850 unsafe {
851 let a = &mut *ptr.add(i);
852 let b = &mut *ptr.add(j);
853 let c = &mut *ptr.add(k);
854 (a, b, c)
855 }
856}
857
858#[inline]
859pub(crate) fn split4_mut<T>(
860 slice: &mut [T],
861 i0: usize,
862 i1: usize,
863 i2: usize,
864 i3: usize,
865) -> (&mut T, &mut T, &mut T, &mut T) {
866 // Safety gates
867 let len = slice.len();
868 assert!(
869 i0 < len && i1 < len && i2 < len && i3 < len,
870 "index out of bounds"
871 );
872 assert!(
873 i0 != i1 && i0 != i2 && i0 != i3 && i1 != i2 && i1 != i3 && i2 != i3,
874 "indices must be pair-wise distinct"
875 );
876
877 unsafe {
878 let base = slice.as_mut_ptr();
879 (
880 &mut *base.add(i0),
881 &mut *base.add(i1),
882 &mut *base.add(i2),
883 &mut *base.add(i3),
884 )
885 }
886}
887
888// todo: Move this somewhere apt
889#[derive(Clone, Copy, Debug)]
890pub enum PMEIndex {
891 // Dynamic atoms (protein, ligand, ions, etc.)
892 Dyn(usize),
893
894 // Water sites (by molecule index)
895 WatO(usize),
896 WatM(usize),
897 WatH0(usize),
898 WatH1(usize),
899
900 // Static atoms (included in the field, but you won't update their accel)
901 Static(usize),
902}