mdprep 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mdprep/__init__.py +7 -0
- mdprep/_selftest_data/basic_protein.yaml +33 -0
- mdprep/_selftest_data/ligand_sub.mol2 +11 -0
- mdprep/ambertools/__init__.py +2 -0
- mdprep/ambertools/antechamber.py +110 -0
- mdprep/ambertools/commands.py +33 -0
- mdprep/ambertools/mol2.py +415 -0
- mdprep/ambertools/parmchk2.py +89 -0
- mdprep/ambertools/resp.py +570 -0
- mdprep/charges/__init__.py +1 -0
- mdprep/charges/esp_grid.py +190 -0
- mdprep/cli.py +451 -0
- mdprep/config/__init__.py +9 -0
- mdprep/config/loader.py +146 -0
- mdprep/config/models.py +1100 -0
- mdprep/external/__init__.py +14 -0
- mdprep/external/discovery.py +14 -0
- mdprep/external/runner.py +55 -0
- mdprep/leap/__init__.py +2 -0
- mdprep/leap/builder.py +476 -0
- mdprep/leap/forcefields.py +93 -0
- mdprep/leap/ions.py +112 -0
- mdprep/leap/log_parser.py +170 -0
- mdprep/leap/report.py +99 -0
- mdprep/leap/residues.py +653 -0
- mdprep/leap/runner.py +65 -0
- mdprep/ligands/__init__.py +2 -0
- mdprep/ligands/extract.py +355 -0
- mdprep/ligands/identity.py +39 -0
- mdprep/ligands/pyscf_charges.py +242 -0
- mdprep/ligands/report.py +146 -0
- mdprep/ligands/workflow.py +838 -0
- mdprep/md/__init__.py +5 -0
- mdprep/md/roe_brooks.py +650 -0
- mdprep/metals/__init__.py +2 -0
- mdprep/metals/c4.py +157 -0
- mdprep/metals/coordination.py +313 -0
- mdprep/metals/gxtb_hessian.py +467 -0
- mdprep/metals/hydrogenate.py +344 -0
- mdprep/metals/mace_polar1_hessian.py +357 -0
- mdprep/metals/mace_polar1_worker.py +295 -0
- mdprep/metals/mcpb.py +949 -0
- mdprep/metals/mcpb_compat.py +125 -0
- mdprep/metals/nonbonded.py +294 -0
- mdprep/metals/parameter_comparison.py +563 -0
- mdprep/metals/pyscf_mcpb.py +1388 -0
- mdprep/metals/report.py +119 -0
- mdprep/metals/workflow.py +320 -0
- mdprep/protonation/__init__.py +15 -0
- mdprep/protonation/apply.py +968 -0
- mdprep/protonation/disulfide_states.py +141 -0
- mdprep/protonation/histidine_geometry.py +950 -0
- mdprep/protonation/histidine_xtb.py +390 -0
- mdprep/protonation/overrides.py +64 -0
- mdprep/protonation/pka_rules.py +165 -0
- mdprep/protonation/propka.py +174 -0
- mdprep/protonation/propka_parser.py +140 -0
- mdprep/protonation/report.py +187 -0
- mdprep/protonation/states.py +52 -0
- mdprep/protonation/temporary_hydrogenation.py +238 -0
- mdprep/protonation/xtb_parser.py +79 -0
- mdprep/protonation/xtb_runner.py +108 -0
- mdprep/qm/__init__.py +2 -0
- mdprep/qm/point_charges.py +233 -0
- mdprep/qm/pyscf_esp.py +70 -0
- mdprep/qm/pyscf_runner.py +570 -0
- mdprep/refinement/__init__.py +5 -0
- mdprep/refinement/ash.py +541 -0
- mdprep/refinement/coordinates.py +210 -0
- mdprep/refinement/mapping.py +163 -0
- mdprep/refinement/report.py +66 -0
- mdprep/refinement/selection.py +436 -0
- mdprep/refinement/workflow.py +188 -0
- mdprep/reports/__init__.py +8 -0
- mdprep/reports/structure_report.py +92 -0
- mdprep/structure/__init__.py +14 -0
- mdprep/structure/classify.py +92 -0
- mdprep/structure/disulfides.py +51 -0
- mdprep/structure/inspect.py +105 -0
- mdprep/structure/models.py +86 -0
- mdprep/structure/normalize.py +363 -0
- mdprep/structure/pdb.py +511 -0
- mdprep/structure/repair.py +566 -0
- mdprep/structure/selectors.py +204 -0
- mdprep/structure/writer.py +84 -0
- mdprep/validation/__init__.py +2 -0
- mdprep/validation/openmm_check.py +86 -0
- mdprep/validation/parmed_check.py +43 -0
- mdprep/validation/topology.py +239 -0
- mdprep/workflows/__init__.py +6 -0
- mdprep/workflows/init.py +242 -0
- mdprep/workflows/inspect.py +21 -0
- mdprep/workflows/prepare.py +763 -0
- mdprep/workflows/selftest.py +220 -0
- mdprep/workflows/validate.py +30 -0
- mdprep-0.2.0.dist-info/METADATA +264 -0
- mdprep-0.2.0.dist-info/RECORD +100 -0
- mdprep-0.2.0.dist-info/WHEEL +4 -0
- mdprep-0.2.0.dist-info/entry_points.txt +2 -0
- mdprep-0.2.0.dist-info/licenses/LICENSE +21 -0
mdprep/md/__init__.py
ADDED
mdprep/md/roe_brooks.py
ADDED
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
"""Manifest-driven Roe and Brooks equilibration protocol for OpenMM.
|
|
2
|
+
|
|
3
|
+
The implementation follows the staged protocol used by mdprep's development
|
|
4
|
+
system while making platform selection, convergence limits, random seeds, and
|
|
5
|
+
production reporting explicit and reproducible.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import csv
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from time import perf_counter
|
|
16
|
+
from typing import Any, Sequence
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
from mdprep.config.models import MolecularDynamicsConfig
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RoeBrooksError(ValueError):
|
|
24
|
+
"""Raised when the OpenMM MD protocol cannot complete safely."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class RoeBrooksResult:
|
|
29
|
+
output_dir: Path
|
|
30
|
+
platform: str
|
|
31
|
+
step10_elapsed_ps: float
|
|
32
|
+
production_steps: int
|
|
33
|
+
final_pdb: Path
|
|
34
|
+
final_checkpoint: Path
|
|
35
|
+
trajectory: Path
|
|
36
|
+
state_log: Path
|
|
37
|
+
report_path: Path
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> dict[str, object]:
|
|
40
|
+
return {
|
|
41
|
+
"protocol": "roe_brooks_2020",
|
|
42
|
+
"output_dir": str(self.output_dir),
|
|
43
|
+
"platform": self.platform,
|
|
44
|
+
"step10_elapsed_ps": self.step10_elapsed_ps,
|
|
45
|
+
"production_steps": self.production_steps,
|
|
46
|
+
"final_pdb": str(self.final_pdb),
|
|
47
|
+
"final_checkpoint": str(self.final_checkpoint),
|
|
48
|
+
"trajectory": str(self.trajectory),
|
|
49
|
+
"state_log": str(self.state_log),
|
|
50
|
+
"report_path": str(self.report_path),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
BACKBONE_NAMES = {
|
|
55
|
+
"N", "CA", "C", "O", "OXT", "P", "OP1", "OP2", "O5'", "C5'",
|
|
56
|
+
"C4'", "O4'", "C3'", "O3'", "C2'", "C1'", "O2'",
|
|
57
|
+
}
|
|
58
|
+
WATER_RESNAMES = {"HOH", "WAT", "TIP3", "TP3", "SPC", "SPCE", "OPC"}
|
|
59
|
+
ION_RESNAMES = {
|
|
60
|
+
"NA", "CL", "K", "RB", "CS", "LI", "MG", "CA", "ZN", "MN", "FE",
|
|
61
|
+
"CU", "CO", "NI",
|
|
62
|
+
}
|
|
63
|
+
AUTO_PLATFORM_ORDER = ("CUDA", "HIP", "Metal", "OpenCL", "CPU", "Reference")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def steps_from_time(time_q: Any, timestep_q: Any) -> int:
|
|
67
|
+
return int(round(time_q / timestep_q))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def density_plateau_check(
|
|
71
|
+
times_ps: Sequence[float],
|
|
72
|
+
densities_g_ml: Sequence[float],
|
|
73
|
+
*,
|
|
74
|
+
window_ps: float,
|
|
75
|
+
report_interval_ps: float,
|
|
76
|
+
slope_threshold: float,
|
|
77
|
+
mean_difference_threshold: float,
|
|
78
|
+
minimum_points: int,
|
|
79
|
+
) -> tuple[bool, dict[str, object]]:
|
|
80
|
+
"""Evaluate the final point-based density window."""
|
|
81
|
+
|
|
82
|
+
times = np.asarray(times_ps, dtype=float)
|
|
83
|
+
densities = np.asarray(densities_g_ml, dtype=float)
|
|
84
|
+
if len(densities) < minimum_points:
|
|
85
|
+
return False, {"reason": f"need at least {minimum_points} points"}
|
|
86
|
+
window_points = max(
|
|
87
|
+
minimum_points,
|
|
88
|
+
int(round(window_ps / report_interval_ps)) + 1,
|
|
89
|
+
)
|
|
90
|
+
if len(densities) < window_points:
|
|
91
|
+
return False, {"reason": f"need at least {window_points} window points"}
|
|
92
|
+
window_times = times[-window_points:]
|
|
93
|
+
window_densities = densities[-window_points:]
|
|
94
|
+
slope, _ = np.polyfit(window_times, window_densities, 1)
|
|
95
|
+
midpoint = len(window_densities) // 2
|
|
96
|
+
mean_first = float(np.mean(window_densities[:midpoint]))
|
|
97
|
+
mean_second = float(np.mean(window_densities[midpoint:]))
|
|
98
|
+
mean_difference = abs(mean_second - mean_first)
|
|
99
|
+
info: dict[str, object] = {
|
|
100
|
+
"points": window_points,
|
|
101
|
+
"window_ps": float(window_times[-1] - window_times[0]),
|
|
102
|
+
"slope_g_ml_ps": float(slope),
|
|
103
|
+
"slope_threshold_g_ml_ps": slope_threshold,
|
|
104
|
+
"mean_difference_g_ml": mean_difference,
|
|
105
|
+
"mean_difference_threshold_g_ml": mean_difference_threshold,
|
|
106
|
+
"standard_deviation_g_ml": float(np.std(window_densities)),
|
|
107
|
+
"mean_first_half_g_ml": mean_first,
|
|
108
|
+
"mean_second_half_g_ml": mean_second,
|
|
109
|
+
}
|
|
110
|
+
converged = (
|
|
111
|
+
abs(float(slope)) < slope_threshold
|
|
112
|
+
and mean_difference < mean_difference_threshold
|
|
113
|
+
)
|
|
114
|
+
return converged, info
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def platform_candidates(requested: str, available: Sequence[str]) -> list[str]:
|
|
118
|
+
"""Return deterministic accelerator-first candidates for an OpenMM run."""
|
|
119
|
+
|
|
120
|
+
available_set = set(available)
|
|
121
|
+
if requested != "auto":
|
|
122
|
+
if requested not in available_set:
|
|
123
|
+
raise RoeBrooksError(
|
|
124
|
+
f"Requested OpenMM platform {requested!r} is unavailable; "
|
|
125
|
+
f"available platforms: {', '.join(available)}"
|
|
126
|
+
)
|
|
127
|
+
return [requested]
|
|
128
|
+
candidates = [name for name in AUTO_PLATFORM_ORDER if name in available_set]
|
|
129
|
+
if not candidates:
|
|
130
|
+
raise RoeBrooksError("OpenMM reports no usable computational platforms")
|
|
131
|
+
return candidates
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _platform_properties(platform: Any, config: MolecularDynamicsConfig) -> dict[str, str]:
|
|
135
|
+
names = set(platform.getPropertyNames())
|
|
136
|
+
properties: dict[str, str] = {}
|
|
137
|
+
if config.device_index is not None and "DeviceIndex" in names:
|
|
138
|
+
properties["DeviceIndex"] = str(config.device_index)
|
|
139
|
+
if "Precision" in names:
|
|
140
|
+
properties["Precision"] = config.precision
|
|
141
|
+
if config.cpu_threads is not None and "Threads" in names:
|
|
142
|
+
properties["Threads"] = str(config.cpu_threads)
|
|
143
|
+
return properties
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def run_roe_brooks(
|
|
147
|
+
*,
|
|
148
|
+
prmtop_path: str | Path,
|
|
149
|
+
inpcrd_path: str | Path,
|
|
150
|
+
output_dir: str | Path,
|
|
151
|
+
config: MolecularDynamicsConfig,
|
|
152
|
+
) -> RoeBrooksResult:
|
|
153
|
+
"""Run equilibration and production from validated Amber inputs."""
|
|
154
|
+
|
|
155
|
+
if not config.enabled or config.production is None:
|
|
156
|
+
raise RoeBrooksError("The molecular_dynamics stage is not enabled")
|
|
157
|
+
try:
|
|
158
|
+
import openmm as mm
|
|
159
|
+
from openmm import Platform, unit
|
|
160
|
+
from openmm.app import (
|
|
161
|
+
AmberInpcrdFile,
|
|
162
|
+
AmberPrmtopFile,
|
|
163
|
+
CheckpointReporter,
|
|
164
|
+
DCDReporter,
|
|
165
|
+
HBonds,
|
|
166
|
+
PDBFile,
|
|
167
|
+
PME,
|
|
168
|
+
Simulation,
|
|
169
|
+
StateDataReporter,
|
|
170
|
+
)
|
|
171
|
+
except ImportError as exc:
|
|
172
|
+
raise RoeBrooksError(
|
|
173
|
+
"OpenMM is required for molecular_dynamics.enabled: true; install the "
|
|
174
|
+
"mdprep MD optional dependencies"
|
|
175
|
+
) from exc
|
|
176
|
+
|
|
177
|
+
output = Path(output_dir)
|
|
178
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
prmtop = AmberPrmtopFile(str(prmtop_path))
|
|
180
|
+
inpcrd = AmberInpcrdFile(str(inpcrd_path))
|
|
181
|
+
topology = prmtop.topology
|
|
182
|
+
positions = inpcrd.positions
|
|
183
|
+
box_vectors = inpcrd.boxVectors
|
|
184
|
+
if topology.getNumAtoms() != len(positions):
|
|
185
|
+
raise RoeBrooksError("prmtop and inpcrd atom counts differ")
|
|
186
|
+
if box_vectors is None:
|
|
187
|
+
raise RoeBrooksError("Roe--Brooks PME/NPT requires periodic box vectors")
|
|
188
|
+
|
|
189
|
+
temperature = config.temperature_kelvin * unit.kelvin
|
|
190
|
+
pressure = config.pressure_atmosphere * unit.atmosphere
|
|
191
|
+
started = perf_counter()
|
|
192
|
+
|
|
193
|
+
def build_system(*, constraints: Any, rigid_water: bool, barostat: bool) -> Any:
|
|
194
|
+
system = prmtop.createSystem(
|
|
195
|
+
nonbondedMethod=PME,
|
|
196
|
+
nonbondedCutoff=0.8 * unit.nanometer,
|
|
197
|
+
constraints=constraints,
|
|
198
|
+
rigidWater=rigid_water,
|
|
199
|
+
ewaldErrorTolerance=1.0e-4,
|
|
200
|
+
)
|
|
201
|
+
for force_index in range(system.getNumForces()):
|
|
202
|
+
force = system.getForce(force_index)
|
|
203
|
+
if isinstance(force, mm.NonbondedForce):
|
|
204
|
+
force.setUseDispersionCorrection(True)
|
|
205
|
+
if barostat:
|
|
206
|
+
barostat_force = mm.MonteCarloBarostat(pressure, temperature, 100)
|
|
207
|
+
barostat_force.setRandomNumberSeed(config.random_seed)
|
|
208
|
+
system.addForce(barostat_force)
|
|
209
|
+
return system
|
|
210
|
+
|
|
211
|
+
def add_restraints(
|
|
212
|
+
system: Any,
|
|
213
|
+
reference_positions: Any,
|
|
214
|
+
force_constant: float,
|
|
215
|
+
selection: str,
|
|
216
|
+
) -> None:
|
|
217
|
+
k = (
|
|
218
|
+
force_constant
|
|
219
|
+
* unit.kilocalories_per_mole
|
|
220
|
+
/ unit.angstrom**2
|
|
221
|
+
).in_units_of(unit.kilojoules_per_mole / unit.nanometer**2)
|
|
222
|
+
force = mm.CustomExternalForce("0.5*k*((x-x0)^2+(y-y0)^2+(z-z0)^2)")
|
|
223
|
+
force.addGlobalParameter("k", k)
|
|
224
|
+
for parameter in ("x0", "y0", "z0"):
|
|
225
|
+
force.addPerParticleParameter(parameter)
|
|
226
|
+
reference_nm = reference_positions.value_in_unit(unit.nanometer)
|
|
227
|
+
for atom in topology.atoms():
|
|
228
|
+
name = atom.name.strip()
|
|
229
|
+
is_hydrogen = (
|
|
230
|
+
atom.element is not None and atom.element.symbol == "H"
|
|
231
|
+
) or name.startswith("H")
|
|
232
|
+
residue_name = atom.residue.name.strip().upper()
|
|
233
|
+
if is_hydrogen or residue_name in WATER_RESNAMES | ION_RESNAMES:
|
|
234
|
+
continue
|
|
235
|
+
if selection == "backbone" and name not in BACKBONE_NAMES:
|
|
236
|
+
continue
|
|
237
|
+
if selection not in {"heavy", "backbone"}:
|
|
238
|
+
raise RoeBrooksError(f"Unknown restraint selection {selection!r}")
|
|
239
|
+
point = reference_nm[atom.index]
|
|
240
|
+
force.addParticle(atom.index, [point.x, point.y, point.z])
|
|
241
|
+
system.addForce(force)
|
|
242
|
+
|
|
243
|
+
available = [
|
|
244
|
+
Platform.getPlatform(index).getName()
|
|
245
|
+
for index in range(Platform.getNumPlatforms())
|
|
246
|
+
]
|
|
247
|
+
trial_system = build_system(constraints=HBonds, rigid_water=True, barostat=False)
|
|
248
|
+
selected_platform = None
|
|
249
|
+
selected_properties: dict[str, str] = {}
|
|
250
|
+
platform_failures: dict[str, str] = {}
|
|
251
|
+
for candidate in platform_candidates(config.platform, available):
|
|
252
|
+
candidate_platform = Platform.getPlatformByName(candidate)
|
|
253
|
+
candidate_properties = _platform_properties(candidate_platform, config)
|
|
254
|
+
trial_integrator = mm.VerletIntegrator(1.0 * unit.femtoseconds)
|
|
255
|
+
try:
|
|
256
|
+
trial_simulation = Simulation(
|
|
257
|
+
topology,
|
|
258
|
+
trial_system,
|
|
259
|
+
trial_integrator,
|
|
260
|
+
candidate_platform,
|
|
261
|
+
candidate_properties,
|
|
262
|
+
)
|
|
263
|
+
trial_simulation.context.setPositions(positions)
|
|
264
|
+
trial_simulation.context.setPeriodicBoxVectors(*box_vectors)
|
|
265
|
+
trial_state = trial_simulation.context.getState(getEnergy=True)
|
|
266
|
+
energy = trial_state.getPotentialEnergy().value_in_unit(
|
|
267
|
+
unit.kilojoules_per_mole
|
|
268
|
+
)
|
|
269
|
+
if not math.isfinite(energy):
|
|
270
|
+
raise RoeBrooksError("initial energy is not finite")
|
|
271
|
+
except Exception as exc: # OpenMM plugins raise several exception types
|
|
272
|
+
platform_failures[candidate] = str(exc)
|
|
273
|
+
if config.platform != "auto":
|
|
274
|
+
raise RoeBrooksError(
|
|
275
|
+
f"Requested OpenMM platform {candidate!r} failed: {exc}"
|
|
276
|
+
) from exc
|
|
277
|
+
continue
|
|
278
|
+
selected_platform = candidate_platform
|
|
279
|
+
selected_properties = candidate_properties
|
|
280
|
+
del trial_simulation, trial_integrator
|
|
281
|
+
break
|
|
282
|
+
if selected_platform is None:
|
|
283
|
+
raise RoeBrooksError(
|
|
284
|
+
"No OpenMM platform could initialize: "
|
|
285
|
+
+ "; ".join(f"{key}: {value}" for key, value in platform_failures.items())
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
stage_records: list[dict[str, object]] = []
|
|
289
|
+
|
|
290
|
+
def make_simulation(
|
|
291
|
+
system: Any,
|
|
292
|
+
integrator: Any,
|
|
293
|
+
stage_positions: Any,
|
|
294
|
+
stage_box: Any,
|
|
295
|
+
*,
|
|
296
|
+
velocities: Any | None = None,
|
|
297
|
+
initialize_velocities: bool = False,
|
|
298
|
+
) -> Any:
|
|
299
|
+
simulation = Simulation(
|
|
300
|
+
topology,
|
|
301
|
+
system,
|
|
302
|
+
integrator,
|
|
303
|
+
selected_platform,
|
|
304
|
+
selected_properties,
|
|
305
|
+
)
|
|
306
|
+
simulation.context.setPositions(stage_positions)
|
|
307
|
+
simulation.context.setPeriodicBoxVectors(*stage_box)
|
|
308
|
+
if velocities is not None:
|
|
309
|
+
simulation.context.setVelocities(velocities)
|
|
310
|
+
elif initialize_velocities:
|
|
311
|
+
simulation.context.setVelocitiesToTemperature(
|
|
312
|
+
temperature, config.random_seed
|
|
313
|
+
)
|
|
314
|
+
return simulation
|
|
315
|
+
|
|
316
|
+
def write_pdb(simulation: Any, name: str) -> Path:
|
|
317
|
+
path = output / name
|
|
318
|
+
state = simulation.context.getState(getPositions=True, enforcePeriodicBox=True)
|
|
319
|
+
with path.open("w", encoding="utf-8") as handle:
|
|
320
|
+
PDBFile.writeFile(topology, state.getPositions(), handle, keepIds=True)
|
|
321
|
+
return path
|
|
322
|
+
|
|
323
|
+
def record_stage(simulation: Any, label: str) -> Any:
|
|
324
|
+
state = simulation.context.getState(
|
|
325
|
+
getPositions=True,
|
|
326
|
+
getVelocities=True,
|
|
327
|
+
getEnergy=True,
|
|
328
|
+
enforcePeriodicBox=True,
|
|
329
|
+
)
|
|
330
|
+
potential = state.getPotentialEnergy().value_in_unit(unit.kilojoules_per_mole)
|
|
331
|
+
kinetic = state.getKineticEnergy().value_in_unit(unit.kilojoules_per_mole)
|
|
332
|
+
volume = state.getPeriodicBoxVolume().value_in_unit(unit.nanometer**3)
|
|
333
|
+
if not all(math.isfinite(value) for value in (potential, kinetic, volume)):
|
|
334
|
+
raise RoeBrooksError(f"{label} produced a non-finite state")
|
|
335
|
+
stage_records.append(
|
|
336
|
+
{
|
|
337
|
+
"stage": label,
|
|
338
|
+
"potential_energy_kj_mol": potential,
|
|
339
|
+
"kinetic_energy_kj_mol": kinetic,
|
|
340
|
+
"box_volume_nm3": volume,
|
|
341
|
+
}
|
|
342
|
+
)
|
|
343
|
+
return state
|
|
344
|
+
|
|
345
|
+
def save_restart(simulation: Any, stem: str) -> Path:
|
|
346
|
+
checkpoint = output / f"{stem}.chk"
|
|
347
|
+
state_path = output / f"{stem}.xml"
|
|
348
|
+
simulation.saveCheckpoint(str(checkpoint))
|
|
349
|
+
state = simulation.context.getState(
|
|
350
|
+
getPositions=True, getVelocities=True, getEnergy=True,
|
|
351
|
+
enforcePeriodicBox=True,
|
|
352
|
+
)
|
|
353
|
+
state_path.write_text(mm.XmlSerializer.serialize(state), encoding="utf-8")
|
|
354
|
+
return checkpoint
|
|
355
|
+
|
|
356
|
+
dt_min = 1.0 * unit.femtoseconds
|
|
357
|
+
dt_1fs = 1.0 * unit.femtoseconds
|
|
358
|
+
dt_2fs = 2.0 * unit.femtoseconds
|
|
359
|
+
|
|
360
|
+
# Steps 1--5. Positional forces are added before Context creation.
|
|
361
|
+
reference_positions = positions
|
|
362
|
+
current_positions = positions
|
|
363
|
+
current_box = box_vectors
|
|
364
|
+
system = build_system(constraints=None, rigid_water=False, barostat=False)
|
|
365
|
+
add_restraints(system, reference_positions, 5.0, "heavy")
|
|
366
|
+
simulation = make_simulation(
|
|
367
|
+
system, mm.VerletIntegrator(dt_min), current_positions, current_box
|
|
368
|
+
)
|
|
369
|
+
mm.LocalEnergyMinimizer.minimize(simulation.context, maxIterations=1000)
|
|
370
|
+
write_pdb(simulation, "step01.pdb")
|
|
371
|
+
state = record_stage(simulation, "step01")
|
|
372
|
+
current_positions = state.getPositions()
|
|
373
|
+
current_box = state.getPeriodicBoxVectors()
|
|
374
|
+
|
|
375
|
+
# Make the unconstrained minimum compatible with the SHAKE/rigid-water
|
|
376
|
+
# manifold before heating. Some accelerator and CPU constraint solvers
|
|
377
|
+
# otherwise fail on the first integration step for strained input water.
|
|
378
|
+
system = build_system(constraints=HBonds, rigid_water=True, barostat=False)
|
|
379
|
+
add_restraints(system, reference_positions, 5.0, "heavy")
|
|
380
|
+
simulation = make_simulation(
|
|
381
|
+
system, mm.VerletIntegrator(dt_min), current_positions, current_box
|
|
382
|
+
)
|
|
383
|
+
mm.LocalEnergyMinimizer.minimize(simulation.context, maxIterations=500)
|
|
384
|
+
write_pdb(simulation, "step01b.pdb")
|
|
385
|
+
state = record_stage(simulation, "step01b")
|
|
386
|
+
current_positions = state.getPositions()
|
|
387
|
+
current_box = state.getPeriodicBoxVectors()
|
|
388
|
+
|
|
389
|
+
system = build_system(constraints=HBonds, rigid_water=True, barostat=False)
|
|
390
|
+
add_restraints(system, reference_positions, 5.0, "heavy")
|
|
391
|
+
integrator = mm.LangevinMiddleIntegrator(temperature, 5.0 / unit.picosecond, dt_1fs)
|
|
392
|
+
integrator.setConstraintTolerance(1.0e-6)
|
|
393
|
+
integrator.setRandomNumberSeed(config.random_seed + 2)
|
|
394
|
+
simulation = make_simulation(
|
|
395
|
+
system, integrator, current_positions, current_box, initialize_velocities=True
|
|
396
|
+
)
|
|
397
|
+
simulation.step(steps_from_time(15.0 * unit.picoseconds, dt_1fs))
|
|
398
|
+
write_pdb(simulation, "step02.pdb")
|
|
399
|
+
save_restart(simulation, "after_step02")
|
|
400
|
+
state = record_stage(simulation, "step02")
|
|
401
|
+
current_positions, current_velocities = state.getPositions(), state.getVelocities()
|
|
402
|
+
current_box = state.getPeriodicBoxVectors()
|
|
403
|
+
|
|
404
|
+
for label, restraint_k in (("step03", 2.0), ("step04", 0.1)):
|
|
405
|
+
system = build_system(constraints=None, rigid_water=False, barostat=False)
|
|
406
|
+
add_restraints(system, reference_positions, restraint_k, "heavy")
|
|
407
|
+
simulation = make_simulation(
|
|
408
|
+
system, mm.VerletIntegrator(dt_min), current_positions, current_box
|
|
409
|
+
)
|
|
410
|
+
mm.LocalEnergyMinimizer.minimize(simulation.context, maxIterations=1000)
|
|
411
|
+
write_pdb(simulation, f"{label}.pdb")
|
|
412
|
+
state = record_stage(simulation, label)
|
|
413
|
+
current_positions, current_box = state.getPositions(), state.getPeriodicBoxVectors()
|
|
414
|
+
|
|
415
|
+
system = build_system(constraints=None, rigid_water=False, barostat=False)
|
|
416
|
+
simulation = make_simulation(
|
|
417
|
+
system, mm.VerletIntegrator(dt_min), current_positions, current_box
|
|
418
|
+
)
|
|
419
|
+
mm.LocalEnergyMinimizer.minimize(simulation.context, maxIterations=1000)
|
|
420
|
+
write_pdb(simulation, "step05.pdb")
|
|
421
|
+
state = record_stage(simulation, "step05")
|
|
422
|
+
current_positions, current_box = state.getPositions(), state.getPeriodicBoxVectors()
|
|
423
|
+
|
|
424
|
+
system = build_system(constraints=HBonds, rigid_water=True, barostat=False)
|
|
425
|
+
simulation = make_simulation(
|
|
426
|
+
system, mm.VerletIntegrator(dt_min), current_positions, current_box
|
|
427
|
+
)
|
|
428
|
+
mm.LocalEnergyMinimizer.minimize(simulation.context, maxIterations=500)
|
|
429
|
+
write_pdb(simulation, "step05b.pdb")
|
|
430
|
+
state = record_stage(simulation, "step05b")
|
|
431
|
+
current_positions, current_box = state.getPositions(), state.getPeriodicBoxVectors()
|
|
432
|
+
step5_reference = current_positions
|
|
433
|
+
|
|
434
|
+
system = build_system(constraints=HBonds, rigid_water=True, barostat=False)
|
|
435
|
+
add_restraints(system, step5_reference, 1.0, "heavy")
|
|
436
|
+
settle_dt = 0.25 * unit.femtoseconds
|
|
437
|
+
integrator = mm.LangevinMiddleIntegrator(
|
|
438
|
+
temperature, 20.0 / unit.picosecond, settle_dt
|
|
439
|
+
)
|
|
440
|
+
integrator.setConstraintTolerance(1.0e-6)
|
|
441
|
+
integrator.setRandomNumberSeed(config.random_seed + 5)
|
|
442
|
+
simulation = make_simulation(
|
|
443
|
+
system, integrator, current_positions, current_box, initialize_velocities=True
|
|
444
|
+
)
|
|
445
|
+
simulation.step(steps_from_time(0.2 * unit.picoseconds, settle_dt))
|
|
446
|
+
write_pdb(simulation, "step05c.pdb")
|
|
447
|
+
state = record_stage(simulation, "step05c")
|
|
448
|
+
current_positions, current_velocities = state.getPositions(), state.getVelocities()
|
|
449
|
+
current_box = state.getPeriodicBoxVectors()
|
|
450
|
+
|
|
451
|
+
def run_md_stage(
|
|
452
|
+
label: str,
|
|
453
|
+
*,
|
|
454
|
+
duration_ps: float,
|
|
455
|
+
timestep: Any,
|
|
456
|
+
restraint_k: float | None,
|
|
457
|
+
restraint_selection: str | None,
|
|
458
|
+
barostat: bool,
|
|
459
|
+
seed_offset: int,
|
|
460
|
+
) -> None:
|
|
461
|
+
nonlocal current_positions, current_velocities, current_box
|
|
462
|
+
system = build_system(constraints=HBonds, rigid_water=True, barostat=barostat)
|
|
463
|
+
if restraint_k is not None and restraint_selection is not None:
|
|
464
|
+
add_restraints(system, step5_reference, restraint_k, restraint_selection)
|
|
465
|
+
integrator = mm.LangevinMiddleIntegrator(
|
|
466
|
+
temperature, 5.0 / unit.picosecond, timestep
|
|
467
|
+
)
|
|
468
|
+
integrator.setConstraintTolerance(1.0e-6)
|
|
469
|
+
integrator.setRandomNumberSeed(config.random_seed + seed_offset)
|
|
470
|
+
simulation = make_simulation(
|
|
471
|
+
system,
|
|
472
|
+
integrator,
|
|
473
|
+
current_positions,
|
|
474
|
+
current_box,
|
|
475
|
+
velocities=current_velocities,
|
|
476
|
+
)
|
|
477
|
+
simulation.step(steps_from_time(duration_ps * unit.picoseconds, timestep))
|
|
478
|
+
write_pdb(simulation, f"{label}.pdb")
|
|
479
|
+
state = record_stage(simulation, label)
|
|
480
|
+
current_positions, current_velocities = state.getPositions(), state.getVelocities()
|
|
481
|
+
current_box = state.getPeriodicBoxVectors()
|
|
482
|
+
|
|
483
|
+
run_md_stage(
|
|
484
|
+
"step06", duration_ps=5.0, timestep=dt_1fs, restraint_k=1.0,
|
|
485
|
+
restraint_selection="heavy", barostat=False, seed_offset=6,
|
|
486
|
+
)
|
|
487
|
+
run_md_stage(
|
|
488
|
+
"step07", duration_ps=5.0, timestep=dt_1fs, restraint_k=0.5,
|
|
489
|
+
restraint_selection="heavy", barostat=True, seed_offset=7,
|
|
490
|
+
)
|
|
491
|
+
run_md_stage(
|
|
492
|
+
"step08", duration_ps=10.0, timestep=dt_1fs, restraint_k=0.5,
|
|
493
|
+
restraint_selection="backbone",
|
|
494
|
+
barostat=config.step08_ensemble == "NPT", seed_offset=8,
|
|
495
|
+
)
|
|
496
|
+
run_md_stage(
|
|
497
|
+
"step09", duration_ps=10.0, timestep=dt_2fs, restraint_k=None,
|
|
498
|
+
restraint_selection=None, barostat=True, seed_offset=9,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# Step 10: unrestrained NPT until the configured density window converges.
|
|
502
|
+
system = build_system(constraints=HBonds, rigid_water=True, barostat=True)
|
|
503
|
+
integrator = mm.LangevinMiddleIntegrator(
|
|
504
|
+
temperature, 5.0 / unit.picosecond, dt_2fs
|
|
505
|
+
)
|
|
506
|
+
integrator.setConstraintTolerance(1.0e-6)
|
|
507
|
+
integrator.setRandomNumberSeed(config.random_seed + 10)
|
|
508
|
+
simulation = make_simulation(
|
|
509
|
+
system, integrator, current_positions, current_box,
|
|
510
|
+
velocities=current_velocities,
|
|
511
|
+
)
|
|
512
|
+
total_mass_da = sum(
|
|
513
|
+
system.getParticleMass(index).value_in_unit(unit.dalton)
|
|
514
|
+
for index in range(system.getNumParticles())
|
|
515
|
+
)
|
|
516
|
+
density_path = output / "density_step10.csv"
|
|
517
|
+
with density_path.open("w", newline="", encoding="utf-8") as handle:
|
|
518
|
+
csv.writer(handle).writerow(["time_ps", "density_g_ml", "box_volume_nm3"])
|
|
519
|
+
report_steps = max(
|
|
520
|
+
1,
|
|
521
|
+
steps_from_time(config.density.report_interval_ps * unit.picoseconds, dt_2fs),
|
|
522
|
+
)
|
|
523
|
+
chunk_steps = max(
|
|
524
|
+
1,
|
|
525
|
+
steps_from_time(config.density.increment_ns * unit.nanoseconds, dt_2fs),
|
|
526
|
+
)
|
|
527
|
+
maximum_steps = steps_from_time(
|
|
528
|
+
config.density.maximum_duration_ns * unit.nanoseconds, dt_2fs
|
|
529
|
+
)
|
|
530
|
+
elapsed_steps = 0
|
|
531
|
+
times: list[float] = []
|
|
532
|
+
densities: list[float] = []
|
|
533
|
+
convergence: dict[str, object] = {}
|
|
534
|
+
converged = False
|
|
535
|
+
while elapsed_steps < maximum_steps and not converged:
|
|
536
|
+
steps_left = min(chunk_steps, maximum_steps - elapsed_steps)
|
|
537
|
+
while steps_left > 0:
|
|
538
|
+
advance = min(report_steps, steps_left)
|
|
539
|
+
simulation.step(advance)
|
|
540
|
+
elapsed_steps += advance
|
|
541
|
+
steps_left -= advance
|
|
542
|
+
state = simulation.context.getState(enforcePeriodicBox=True)
|
|
543
|
+
volume = state.getPeriodicBoxVolume().value_in_unit(unit.nanometer**3)
|
|
544
|
+
density = (total_mass_da / 6.02214076e23) / (volume * 1.0e-21)
|
|
545
|
+
elapsed_ps = (elapsed_steps * dt_2fs).value_in_unit(unit.picoseconds)
|
|
546
|
+
if not all(math.isfinite(value) for value in (volume, density)):
|
|
547
|
+
raise RoeBrooksError("Step 10 produced a non-finite density or box volume")
|
|
548
|
+
times.append(elapsed_ps)
|
|
549
|
+
densities.append(density)
|
|
550
|
+
with density_path.open("a", newline="", encoding="utf-8") as handle:
|
|
551
|
+
csv.writer(handle).writerow([elapsed_ps, density, volume])
|
|
552
|
+
converged, convergence = density_plateau_check(
|
|
553
|
+
times,
|
|
554
|
+
densities,
|
|
555
|
+
window_ps=config.density.window_ps,
|
|
556
|
+
report_interval_ps=config.density.report_interval_ps,
|
|
557
|
+
slope_threshold=config.density.slope_threshold_g_ml_ps,
|
|
558
|
+
mean_difference_threshold=(
|
|
559
|
+
config.density.mean_difference_threshold_g_ml
|
|
560
|
+
),
|
|
561
|
+
minimum_points=config.density.minimum_points,
|
|
562
|
+
)
|
|
563
|
+
if converged:
|
|
564
|
+
break
|
|
565
|
+
if not converged:
|
|
566
|
+
raise RoeBrooksError(
|
|
567
|
+
"Roe--Brooks step 10 did not reach the configured density plateau within "
|
|
568
|
+
f"{config.density.maximum_duration_ns} ns; last check: {convergence}"
|
|
569
|
+
)
|
|
570
|
+
step10_elapsed_ps = times[-1]
|
|
571
|
+
write_pdb(simulation, "step10.pdb")
|
|
572
|
+
save_restart(simulation, "after_step10")
|
|
573
|
+
record_stage(simulation, "step10")
|
|
574
|
+
|
|
575
|
+
production = config.production
|
|
576
|
+
trajectory = output / "production.dcd"
|
|
577
|
+
state_log = output / "production.log"
|
|
578
|
+
periodic_checkpoint = output / "production_periodic.chk"
|
|
579
|
+
simulation.reporters.append(
|
|
580
|
+
DCDReporter(str(trajectory), production.trajectory_interval_steps)
|
|
581
|
+
)
|
|
582
|
+
simulation.reporters.append(
|
|
583
|
+
StateDataReporter(
|
|
584
|
+
str(state_log),
|
|
585
|
+
production.state_interval_steps,
|
|
586
|
+
step=True,
|
|
587
|
+
time=True,
|
|
588
|
+
potentialEnergy=True,
|
|
589
|
+
kineticEnergy=True,
|
|
590
|
+
temperature=True,
|
|
591
|
+
density=True,
|
|
592
|
+
speed=True,
|
|
593
|
+
progress=True,
|
|
594
|
+
remainingTime=True,
|
|
595
|
+
totalSteps=simulation.currentStep + production.steps,
|
|
596
|
+
separator="\t",
|
|
597
|
+
)
|
|
598
|
+
)
|
|
599
|
+
simulation.reporters.append(
|
|
600
|
+
CheckpointReporter(str(periodic_checkpoint), production.checkpoint_interval_steps)
|
|
601
|
+
)
|
|
602
|
+
simulation.step(production.steps)
|
|
603
|
+
final_pdb = write_pdb(simulation, "production_last.pdb")
|
|
604
|
+
final_checkpoint = save_restart(simulation, "after_production")
|
|
605
|
+
final_state = record_stage(simulation, "production")
|
|
606
|
+
final_positions = final_state.getPositions(asNumpy=True).value_in_unit(unit.nanometer)
|
|
607
|
+
if not np.isfinite(final_positions).all():
|
|
608
|
+
raise RoeBrooksError("Production finished with non-finite coordinates")
|
|
609
|
+
|
|
610
|
+
report_path = output / "md_report.json"
|
|
611
|
+
report = {
|
|
612
|
+
"protocol": "roe_brooks_2020",
|
|
613
|
+
"prmtop": str(prmtop_path),
|
|
614
|
+
"inpcrd": str(inpcrd_path),
|
|
615
|
+
"atom_count": topology.getNumAtoms(),
|
|
616
|
+
"platform_requested": config.platform,
|
|
617
|
+
"platform_selected": selected_platform.getName(),
|
|
618
|
+
"platform_properties": selected_properties,
|
|
619
|
+
"platform_initialization_failures": platform_failures,
|
|
620
|
+
"random_seed": config.random_seed,
|
|
621
|
+
"temperature_kelvin": config.temperature_kelvin,
|
|
622
|
+
"pressure_atmosphere": config.pressure_atmosphere,
|
|
623
|
+
"step08_ensemble": config.step08_ensemble,
|
|
624
|
+
"step10_elapsed_ps": step10_elapsed_ps,
|
|
625
|
+
"step10_convergence": convergence,
|
|
626
|
+
"production": production.model_dump(mode="json"),
|
|
627
|
+
"stages": stage_records,
|
|
628
|
+
"runtime_seconds": perf_counter() - started,
|
|
629
|
+
"outputs": {
|
|
630
|
+
"trajectory": str(trajectory),
|
|
631
|
+
"state_log": str(state_log),
|
|
632
|
+
"periodic_checkpoint": str(periodic_checkpoint),
|
|
633
|
+
"final_checkpoint": str(final_checkpoint),
|
|
634
|
+
"final_pdb": str(final_pdb),
|
|
635
|
+
},
|
|
636
|
+
}
|
|
637
|
+
report_path.write_text(
|
|
638
|
+
json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
|
639
|
+
)
|
|
640
|
+
return RoeBrooksResult(
|
|
641
|
+
output_dir=output,
|
|
642
|
+
platform=selected_platform.getName(),
|
|
643
|
+
step10_elapsed_ps=step10_elapsed_ps,
|
|
644
|
+
production_steps=production.steps,
|
|
645
|
+
final_pdb=final_pdb,
|
|
646
|
+
final_checkpoint=final_checkpoint,
|
|
647
|
+
trajectory=trajectory,
|
|
648
|
+
state_log=state_log,
|
|
649
|
+
report_path=report_path,
|
|
650
|
+
)
|