1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
use std::{io, path::Path};
#[cfg(feature = "encode")]
use bincode::{Decode, Encode};
use bio_files::{
gromacs,
gromacs::mdp::{
Barostat, BarostatCfg, ConstraintAlgorithm, Constraints, CoulombType,
Integrator as MdpIntegrator, MdpParams, Pbc, PmeConfig, PressureCouplingType, Thermostat,
VdwModifier, VdwType,
},
};
use crate::{
MdOverrides, SimBoxInit, barostat,
barostat::PRESSURE_DEFAULT,
integrate::Integrator,
prep::HydrogenConstraint,
snapshot::SnapshotHandlers,
solvent::Solvent,
thermostat::{TAU_TEMP_DEFAULT, TEMP_DEFAULT},
};
/// This is the primary way of configurating an MD run. It's passed at init, along with the
/// molecule list and FF params.
#[cfg_attr(feature = "encode", derive(Encode, Decode))]
#[derive(Debug, Clone, PartialEq)]
pub struct MdConfig {
/// Defaults to Velocity Verlet.
pub integrator: Integrator,
/// If enabled, zero the drift in center of mass of the system.
pub zero_com_drift: bool,
/// Kelvin. Defaults to 310 K.
pub temp_target: f32,
/// None to disable it.
pub barostat_cfg: Option<barostat::BarostatCfg>,
/// Allows constraining Hydrogens to be rigid with their bonded atom, using SHAKE and RATTLE
/// algorithms. This allows for higher time steps.
pub hydrogen_constraint: HydrogenConstraint,
pub snapshot_handlers: SnapshotHandlers,
pub sim_box: SimBoxInit,
pub solvent: Solvent,
/// Prior to the first integrator step, we attempt to relax energy in the system.
/// Use no more than this many iterations to do so. Higher can produce better results,
/// but is slower. If None, don't relax.
pub max_init_relaxation_iters: Option<usize>,
/// Simliar to GROMACS' emtol. kcal mol⁻¹ Å⁻¹
pub energy_minimization_tolerance: f32,
/// Distance threshold, in Å, used to determine when we rebuild neighbor lists.
/// 2-4Å are common values. Higher values rebuild less often, and have more computationally-intense
/// rebuilds. Rebuild the list if an atom moved > skin/2.
pub neighbor_skin: f32,
/// Optional path to a pre-equilibrated water template file.
/// When set, this template is used instead of the built-in 60 Å template.
/// A box-specific template (e.g. 30 Å) is required for accurate initial pressures,
/// because the built-in template is cut from a larger equilibrated cell and lacks
/// the long-range Coulomb correlations appropriate for the target PBC cell size.
/// Generate one with the `generate_water_init_template` test (marked #[ignore]).
pub water_template_path: Option<String>,
/// Skip the PBC-boundary proximity check (2.8 Å cross-boundary O-O filter) when placing
/// water molecules. Use this only when generating a pre-equilibrated template at the correct
/// density: the ~88 boundary molecules that the filter would otherwise reject are needed to
/// reach 1 g/cm³, and the MD equilibration run will push them to their natural distances.
pub skip_water_pbc_filter: bool,
// /// We use the GROMACS struct unaltered.
// pub energy_minimization: EnergyMinimization,
/// SPME mesh spacing in **Å**. Smaller = higher-resolution reciprocal mesh, more accurate
/// but slower. 1.0 Å is a good default; equivalent to GROMACS `fourierspacing = 0.1 nm`.
pub spme_mesh_spacing: f32,
/// A bigger α means more damping, and a smaller real-space contribution. (Cheaper real), but larger
/// reciprocal load.
/// Common rule for α: erfc(α r_c) ≲ 10⁻⁴…10⁻⁵
/// Å^-1. 0.35 is good for cutoff of 10–12 Å.
pub spme_alpha: f32,
/// The distance at which we cut off short-range (Direct) Coulomb operations, and transtion
/// to SPME reciprical forces. Å
pub coulomb_cutoff: f32,
/// A hard distance cutoff for VDW forces. Å
pub lj_cutoff: f32,
pub overrides: MdOverrides,
}
impl Default for MdConfig {
fn default() -> Self {
Self {
integrator: Default::default(),
zero_com_drift: false, // todo: True?
temp_target: TEMP_DEFAULT, // GROMACS uses this.
barostat_cfg: Some(Default::default()),
hydrogen_constraint: Default::default(),
snapshot_handlers: Default::default(),
sim_box: Default::default(),
solvent: Default::default(),
max_init_relaxation_iters: Some(1_000), // todo: A/R
// GROMACS emtol = 1000, converted to our units. This is not the same as its default
// of 10: It's loose, for this initial energy minimization. We are converting from
// kJ mol-1 nm-1 to KCal Mol-1 Angstrom-1
energy_minimization_tolerance: 1_000. * 0.0239005,
neighbor_skin: 4.0,
water_template_path: None,
skip_water_pbc_filter: false,
spme_mesh_spacing: 1.0,
// Å⁻¹. Chosen so erfc(α × r_c) ≈ 1e-5 at r_c = 12 Å, matching GROMACS' default
// ewald-rtol. (0.35 Å⁻¹ gives ~2.9e-9 — accurate but pushes k-space costs up.)
spme_alpha: 0.26,
coulomb_cutoff: 10.,
lj_cutoff: 10.,
overrides: Default::default(),
}
}
}
impl MdConfig {
/// Creates a similar config for use with GROMACS. Attempts to replicate this library's
/// settings where we don't have an applicable MdConfig field.
///
/// Not `From` trait: This lib depends on `bio_files`, but not the other way around.
pub fn to_gromacs(&self, n_steps: usize, dt: f32) -> MdpParams {
let (integrator, tau_t, thermostat) = match self.integrator {
Integrator::LangevinMiddle { gamma } => {
// GROMACS `sd` (stochastic dynamics) is the correct counterpart for Langevin.
// `tcoupl` must be `no` for `sd`; friction is given via `tau-t` = 1/γ (ps).
let tau = if gamma > 0.0 { 1.0 / gamma } else { 1.0 };
(
gromacs::mdp::Integrator::Sd,
tau,
gromacs::mdp::Thermostat::No,
)
}
Integrator::VerletVelocity { thermostat } => {
let tau = if let Some(t) = thermostat {
t as f32
} else {
TAU_TEMP_DEFAULT as f32
};
(
gromacs::mdp::Integrator::MdVv,
tau,
gromacs::mdp::Thermostat::VRescale,
)
}
Integrator::Leapfrog { thermostat } => {
let tau = if let Some(t) = thermostat {
t as f32
} else {
TAU_TEMP_DEFAULT as f32
};
(
gromacs::mdp::Integrator::Md,
tau,
gromacs::mdp::Thermostat::VRescale,
)
}
};
let pcoupl = match &self.barostat_cfg {
Some(bc) => gromacs::mdp::Barostat::CRescale(BarostatCfg {
// Standard water compressibility
pcoupltype: PressureCouplingType::Isotropic {
ref_p: bc.pressure_target,
compressibility: bc.solvent_compressibility,
},
tau_p: bc.tau,
}),
None => gromacs::mdp::Barostat::No,
};
const ANGSTROM_TO_NM: f32 = 0.1;
// Many of these values are the MdpParams defaults, but we specify here
// to be explicit, and not miss any.
MdpParams {
integrator,
nsteps: n_steps as u64,
dt,
output_control: self.snapshot_handlers.gromacs.clone(),
coulombtype: CoulombType::Pme(PmeConfig {
fourierspacing: self.spme_mesh_spacing * ANGSTROM_TO_NM,
order: 4, // Hard-coded in `ewald`.
// Convert internal Å⁻¹ to GROMACS nm⁻¹ (1 Å⁻¹ = 10 nm⁻¹).
// PmeConfig::make_inp() derives ewald-rtol = erfc(alpha × rcoulomb).
alpha: self.spme_alpha / ANGSTROM_TO_NM,
..Default::default()
}),
rcoulomb: self.coulomb_cutoff * ANGSTROM_TO_NM,
vdwtype: VdwType::CutOff,
vdw_modifier: VdwModifier::default(),
rvdw: self.lj_cutoff * ANGSTROM_TO_NM,
thermostat,
// We only have one temperature-coupling group in this lib.
tau_t: vec![tau_t],
ref_t: vec![self.temp_target],
pcoupl,
pbc: Pbc::Xyz,
gen_vel: true,
gen_temp: self.temp_target,
gen_seed: None,
constraints: self.hydrogen_constraint.to_gromacs(),
// energy_minimization: Some(self.energy_minimization.clone()),
free_energy_calculations: Default::default(),
}
}
/// Convenience function to save in Gromacs' `.mdp` format.
pub fn save_mdp(&self, path: &Path, n_steps: usize, dt: f32) -> io::Result<()> {
self.to_gromacs(n_steps, dt).save(path)
}
pub fn load_from_mdp(path: &Path) -> io::Result<Self> {
Ok(MdpParams::load(path)?.into())
}
}
impl From<MdpParams> for MdConfig {
fn from(p: MdpParams) -> Self {
use crate::thermostat::LANGEVIN_GAMMA_DEFAULT;
// Mirror of to_gromacs(): Sd → LangevinMiddle, everything else → VerletVelocity.
let integrator = match p.integrator {
MdpIntegrator::Sd => {
let tau = p.tau_t.first().copied().unwrap_or(1.0);
let gamma = if tau > 0.0 {
1.0 / tau
} else {
LANGEVIN_GAMMA_DEFAULT
};
Integrator::LangevinMiddle { gamma }
}
_ => {
let thermostat = if p.thermostat != Thermostat::No {
Some(p.tau_t.first().copied().unwrap_or(TAU_TEMP_DEFAULT as f32) as f64)
} else {
None
};
Integrator::VerletVelocity { thermostat }
}
};
let hydrogen_constraint = match p.constraints {
Constraints::HBonds(ca) => match ca {
ConstraintAlgorithm::Lincs { order, iter } => {
HydrogenConstraint::Linear { order, iter }
}
ConstraintAlgorithm::Shake { tol } => HydrogenConstraint::Shake {
shake_tolerance: tol,
},
},
Constraints::None => HydrogenConstraint::Flexible,
_ => {
eprintln!("Can't use this GROMACS' bond constraint; reverting to a safe default");
Default::default()
}
};
let mut overrides = MdOverrides::default();
// Use nstlog as the snapshot cadence — it's the natural output ratio.
let nstlog = p.output_control.nstlog.unwrap_or(0);
let snapshot_handlers = SnapshotHandlers {
memory: None,
dcd: None,
gromacs: p.output_control.clone(),
};
const NM_TO_ANGSTROM: f32 = 10.0;
let (mesh_spacing, spme_alpha) = match &p.coulombtype {
CoulombType::Pme(pme) => {
let spacing = pme.fourierspacing * NM_TO_ANGSTROM;
// Convert nm⁻¹ back to Å⁻¹ (1 nm⁻¹ = 0.1 Å⁻¹).
let alpha = pme.alpha / NM_TO_ANGSTROM;
(spacing, alpha)
}
_ => (1.0, 0.35),
};
let barostat_cfg = match p.pcoupl {
Barostat::No => None,
Barostat::Berendsen(v)
| Barostat::CRescale(v)
| Barostat::ParrinelloRahman(v)
| Barostat::Mtkk(v) => match v.pcoupltype {
PressureCouplingType::Isotropic {
ref_p,
compressibility,
} => Some(barostat::BarostatCfg {
tau: v.tau_p,
pressure_target: ref_p,
solvent_compressibility: compressibility,
}),
_ => {
eprintln!("Unsupported GROMACS pressure coupling type; reverting to a default");
Some(barostat::BarostatCfg::default())
}
},
};
let def = Self::default();
Self {
integrator,
zero_com_drift: true,
temp_target: p.ref_t.first().copied().unwrap_or(300.0),
barostat_cfg,
hydrogen_constraint,
snapshot_handlers,
sim_box: Default::default(),
solvent: Default::default(),
max_init_relaxation_iters: def.max_init_relaxation_iters,
energy_minimization_tolerance: def.energy_minimization_tolerance,
neighbor_skin: def.neighbor_skin,
overrides,
water_template_path: None,
skip_water_pbc_filter: false,
// energy_minimization: p.energy_minimization.unwrap_or_default(),
spme_mesh_spacing: mesh_spacing,
spme_alpha,
coulomb_cutoff: p.rcoulomb * NM_TO_ANGSTROM,
lj_cutoff: p.rvdw * NM_TO_ANGSTROM,
}
}
}