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(&params)
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 => &param_set.peptide,
577                    FfMolType::SmallOrganic => &param_set.small_mol,
578                    FfMolType::Dna => &param_set.dna,
579                    FfMolType::Rna => &param_set.rna,
580                    FfMolType::Lipid => &param_set.lipids,
581                    FfMolType::Carbohydrate => &param_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(&params, &params_general);
595
596                if let Some(p) = mol.mol_specific_params {
597                    params = merge_params(&params, &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                    // &params,
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            &params,
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}