bfflearn 0.1.0.dev1__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.
- bff/__init__.py +30 -0
- bff/bayes/__init__.py +11 -0
- bff/bayes/gaussian_process.py +285 -0
- bff/bayes/kernels.py +71 -0
- bff/bayes/learning.py +484 -0
- bff/bayes/likelihoods.py +142 -0
- bff/bayes/posterior.py +62 -0
- bff/bayes/priors.py +179 -0
- bff/bayes/results.py +327 -0
- bff/bayes/utils.py +333 -0
- bff/cli.py +254 -0
- bff/data.py +93 -0
- bff/domain/__init__.py +12 -0
- bff/domain/bias.py +149 -0
- bff/domain/specs.py +347 -0
- bff/domain/trainset.py +201 -0
- bff/io/__init__.py +12 -0
- bff/io/colvars.py +186 -0
- bff/io/commands.py +20 -0
- bff/io/cp2k.py +330 -0
- bff/io/logs.py +213 -0
- bff/io/mdp.py +98 -0
- bff/io/plumed.py +184 -0
- bff/io/progress.py +74 -0
- bff/io/schedulers.py +118 -0
- bff/io/utils.py +208 -0
- bff/mcmc/__init__.py +3 -0
- bff/mcmc/convergence.py +168 -0
- bff/mcmc/proposal.py +254 -0
- bff/mcmc/sampler.py +481 -0
- bff/plotting.py +362 -0
- bff/qoi/__init__.py +9 -0
- bff/qoi/analysis.py +209 -0
- bff/qoi/data.py +169 -0
- bff/qoi/hbonds.py +255 -0
- bff/qoi/rdf.py +108 -0
- bff/qoi/routines.py +272 -0
- bff/tools.py +168 -0
- bff/topology.py +581 -0
- bff/workflows/__init__.py +0 -0
- bff/workflows/configs.py +1147 -0
- bff/workflows/learn.py +77 -0
- bff/workflows/md.py +326 -0
- bff/workflows/prepare.py +788 -0
- bff/workflows/qoi.py +260 -0
- bff/workflows/runsims.py +620 -0
- bff/workflows/simulate.py +28 -0
- bff/workflows/train.py +69 -0
- bff/workflows/validate.py +29 -0
- bfflearn-0.1.0.dev1.dist-info/METADATA +913 -0
- bfflearn-0.1.0.dev1.dist-info/RECORD +55 -0
- bfflearn-0.1.0.dev1.dist-info/WHEEL +5 -0
- bfflearn-0.1.0.dev1.dist-info/entry_points.txt +2 -0
- bfflearn-0.1.0.dev1.dist-info/licenses/LICENSE +674 -0
- bfflearn-0.1.0.dev1.dist-info/top_level.txt +1 -0
bff/workflows/prepare.py
ADDED
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import warnings
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Optional, Sequence, Union
|
|
9
|
+
|
|
10
|
+
import MDAnalysis as mda
|
|
11
|
+
import numpy as np
|
|
12
|
+
from gmxtopology import Topology
|
|
13
|
+
from MDAnalysis import transformations as trans
|
|
14
|
+
from MDAnalysis.analysis.distances import distance_array
|
|
15
|
+
from MDAnalysis.selections.gromacs import SelectionWriter
|
|
16
|
+
|
|
17
|
+
from ..domain.bias import BiasSpec
|
|
18
|
+
from ..io.colvars import (
|
|
19
|
+
resolve_distance_bias_metadata as resolve_colvars_distance_bias_metadata,
|
|
20
|
+
)
|
|
21
|
+
from ..io.colvars import (
|
|
22
|
+
write_mdp_with_colvars,
|
|
23
|
+
)
|
|
24
|
+
from ..io.commands import build_command
|
|
25
|
+
from ..io.cp2k import (
|
|
26
|
+
make_cp2k_input,
|
|
27
|
+
make_cp2k_single_point_input,
|
|
28
|
+
write_cp2k_md_slurm_script,
|
|
29
|
+
write_cp2k_single_point_slurm_script,
|
|
30
|
+
)
|
|
31
|
+
from ..io.logs import Logger
|
|
32
|
+
from ..io.plumed import (
|
|
33
|
+
ensure_plumed_kernel,
|
|
34
|
+
)
|
|
35
|
+
from ..io.plumed import (
|
|
36
|
+
resolve_distance_bias_metadata as resolve_plumed_distance_bias_metadata,
|
|
37
|
+
)
|
|
38
|
+
from ..topology import create_box
|
|
39
|
+
from .configs import PrepareConfig
|
|
40
|
+
|
|
41
|
+
PathLike = Union[str, Path]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(slots=True)
|
|
45
|
+
class EquilibratedTopology:
|
|
46
|
+
"""Shared topology state reused across simulation windows."""
|
|
47
|
+
|
|
48
|
+
fn_topol_processed: Path
|
|
49
|
+
fn_coord: Path
|
|
50
|
+
universe: mda.Universe
|
|
51
|
+
box: np.ndarray
|
|
52
|
+
maxwarn: int
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def check_gmx_available(gmx_cmd: str = 'gmx') -> None:
|
|
56
|
+
"""Check if the 'gmx' command is available in the system PATH."""
|
|
57
|
+
try:
|
|
58
|
+
subprocess.run(
|
|
59
|
+
build_command(gmx_cmd, '--version'),
|
|
60
|
+
stdout=subprocess.DEVNULL,
|
|
61
|
+
stderr=subprocess.DEVNULL,
|
|
62
|
+
check=True
|
|
63
|
+
)
|
|
64
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
65
|
+
raise RuntimeError(
|
|
66
|
+
"GROMACS is not available.\n"
|
|
67
|
+
"Please make sure that your Gromacs executable is available "
|
|
68
|
+
"from the command line via e.g. gmx command."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def setup_directories(main_dir: PathLike = Path('./')) -> tuple[Path, Path, Path, Path]:
|
|
73
|
+
"""Create required directories for the preparation workflow."""
|
|
74
|
+
main_dir = Path(main_dir).resolve()
|
|
75
|
+
equilibration_dir = main_dir / "equilibration"
|
|
76
|
+
training_dir = main_dir / "training"
|
|
77
|
+
reference_dir = main_dir / "reference"
|
|
78
|
+
reference_md_dir = reference_dir / "md"
|
|
79
|
+
reference_sp_dir = reference_dir / "single-points"
|
|
80
|
+
|
|
81
|
+
for directory in [
|
|
82
|
+
equilibration_dir,
|
|
83
|
+
training_dir,
|
|
84
|
+
reference_dir,
|
|
85
|
+
reference_md_dir,
|
|
86
|
+
reference_sp_dir,
|
|
87
|
+
]:
|
|
88
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
|
|
90
|
+
return equilibration_dir, training_dir, reference_md_dir, reference_sp_dir
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def make_ndx(universe: mda.Universe, selections: List[str], fn_out: str) -> None:
|
|
94
|
+
with SelectionWriter(fn_out, mode='w') as ndx:
|
|
95
|
+
ndx.write(universe.atoms, name='System')
|
|
96
|
+
if selections is not None:
|
|
97
|
+
for sel in selections:
|
|
98
|
+
for atom in sel.split():
|
|
99
|
+
group = universe.select_atoms('name ' + atom)
|
|
100
|
+
ndx.write(group, name=atom)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def determine_maxwarn(topol: Topology) -> int:
|
|
104
|
+
"""Determine whether charged systems need relaxed ``grompp`` warnings."""
|
|
105
|
+
total_charge = sum(atom.charge for atom in topol.atoms)
|
|
106
|
+
return 1 if not np.isclose(total_charge, 0, atol=1e-4) else 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def run_md(
|
|
110
|
+
name: str,
|
|
111
|
+
fn_mdp: PathLike,
|
|
112
|
+
fn_topol: PathLike,
|
|
113
|
+
fn_coord: PathLike,
|
|
114
|
+
fn_ndx: Optional[PathLike],
|
|
115
|
+
bias: BiasSpec | None = None,
|
|
116
|
+
gmx_cmd: str = "gmx",
|
|
117
|
+
n_steps: int = -2,
|
|
118
|
+
maxwarn: int = 0,
|
|
119
|
+
fn_log: PathLike = 'gmx.log'
|
|
120
|
+
) -> Path:
|
|
121
|
+
|
|
122
|
+
fn_mdp_path = Path(fn_mdp).resolve()
|
|
123
|
+
fn_topol = str(Path(fn_topol).resolve())
|
|
124
|
+
fn_coord = str(Path(fn_coord).resolve())
|
|
125
|
+
fn_ndx = str(Path(fn_ndx).resolve()) if fn_ndx else None
|
|
126
|
+
fn_tpr = str(name) + '.tpr'
|
|
127
|
+
fn_mdp_run = fn_mdp_path
|
|
128
|
+
run_cwd = fn_mdp_path.parent
|
|
129
|
+
run_env = None
|
|
130
|
+
mdrun_cmd = [
|
|
131
|
+
*build_command(
|
|
132
|
+
gmx_cmd,
|
|
133
|
+
'mdrun',
|
|
134
|
+
'-deffnm',
|
|
135
|
+
name,
|
|
136
|
+
'-nsteps',
|
|
137
|
+
str(n_steps),
|
|
138
|
+
),
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
if bias is not None and bias.kind == "colvars" and bias.input_file is not None:
|
|
142
|
+
fn_bias_local = Path(fn_mdp_path.parent) / Path(bias.input_file).name
|
|
143
|
+
if Path(bias.input_file).resolve() != fn_bias_local.resolve():
|
|
144
|
+
shutil.copy2(bias.input_file, fn_bias_local)
|
|
145
|
+
fn_mdp_run = Path(f"{name}-colvars.mdp").resolve()
|
|
146
|
+
run_cwd = fn_mdp_run.parent
|
|
147
|
+
write_mdp_with_colvars(fn_mdp_path, fn_bias_local, fn_mdp_run)
|
|
148
|
+
elif bias is not None and bias.kind == "plumed" and bias.input_file is not None:
|
|
149
|
+
kernel = ensure_plumed_kernel()
|
|
150
|
+
run_env = dict(os.environ)
|
|
151
|
+
run_env.setdefault("PLUMED_KERNEL", str(kernel))
|
|
152
|
+
mdrun_cmd.extend(["-plumed", str(bias.input_file)])
|
|
153
|
+
|
|
154
|
+
maxwarn = str(maxwarn)
|
|
155
|
+
|
|
156
|
+
grompp_cmd = [
|
|
157
|
+
*build_command(
|
|
158
|
+
gmx_cmd,
|
|
159
|
+
'grompp',
|
|
160
|
+
'-f',
|
|
161
|
+
str(fn_mdp_run),
|
|
162
|
+
'-c',
|
|
163
|
+
fn_coord,
|
|
164
|
+
'-p',
|
|
165
|
+
fn_topol,
|
|
166
|
+
'-o',
|
|
167
|
+
fn_tpr,
|
|
168
|
+
'-maxwarn',
|
|
169
|
+
maxwarn,
|
|
170
|
+
),
|
|
171
|
+
]
|
|
172
|
+
if fn_ndx:
|
|
173
|
+
grompp_cmd.extend(['-n', fn_ndx])
|
|
174
|
+
|
|
175
|
+
with open(fn_log, "a") as f:
|
|
176
|
+
subprocess.run(grompp_cmd, stdout=f, stderr=f, check=True, cwd=run_cwd)
|
|
177
|
+
subprocess.run(
|
|
178
|
+
mdrun_cmd,
|
|
179
|
+
stdout=f,
|
|
180
|
+
stderr=f,
|
|
181
|
+
check=True,
|
|
182
|
+
env=run_env,
|
|
183
|
+
cwd=run_cwd,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return fn_mdp_run
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def get_average_box(
|
|
190
|
+
universe: mda.Universe,
|
|
191
|
+
start: int = 0,
|
|
192
|
+
stop: int = None,
|
|
193
|
+
step: int = 1
|
|
194
|
+
) -> np.ndarray:
|
|
195
|
+
"""Compute the average box size over a trajectory."""
|
|
196
|
+
sl = slice(start, stop or universe.trajectory.n_frames, step)
|
|
197
|
+
|
|
198
|
+
box = np.zeros((len(universe.trajectory[sl]), 6))
|
|
199
|
+
for i, ts in enumerate(universe.trajectory[sl]):
|
|
200
|
+
box[i] = ts.dimensions
|
|
201
|
+
box_avg = np.round(np.mean(box, axis=0), 4)
|
|
202
|
+
|
|
203
|
+
return box_avg
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def create_bias_window(
|
|
207
|
+
universe: mda.Universe,
|
|
208
|
+
pair_labels: List[str],
|
|
209
|
+
target_distances: List[float],
|
|
210
|
+
box: np.ndarray,
|
|
211
|
+
fn_out: PathLike,
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Write the frame closest to the requested bias-window distances.
|
|
214
|
+
|
|
215
|
+
Parameters
|
|
216
|
+
----------
|
|
217
|
+
universe
|
|
218
|
+
Equilibrated universe from which the best-matching frame is selected.
|
|
219
|
+
pair_labels
|
|
220
|
+
Atom-name pairs whose distances define the bias window.
|
|
221
|
+
target_distances
|
|
222
|
+
Target distances in nm corresponding to ``pair_labels``.
|
|
223
|
+
box
|
|
224
|
+
Unit-cell dimensions assigned to the output frame.
|
|
225
|
+
fn_out
|
|
226
|
+
Output coordinate file.
|
|
227
|
+
"""
|
|
228
|
+
target_distances = np.array(target_distances, dtype=float)
|
|
229
|
+
ags = [[universe.select_atoms('name ' + x)
|
|
230
|
+
for x in a.split()] for a in pair_labels]
|
|
231
|
+
distances = np.array([
|
|
232
|
+
[distance_array(ag_pair[0], ag_pair[1], box=ts.dimensions)[0, 0]
|
|
233
|
+
for ag_pair in ags]
|
|
234
|
+
for ts in universe.trajectory
|
|
235
|
+
])
|
|
236
|
+
|
|
237
|
+
target_distances_scaled = np.array(target_distances) * 10
|
|
238
|
+
idx = int(np.argmin(np.sum(np.abs(distances - target_distances_scaled), axis=1)))
|
|
239
|
+
with mda.Writer(str(fn_out), 'w') as w:
|
|
240
|
+
ts = universe.trajectory[idx]
|
|
241
|
+
ts.dimensions = box
|
|
242
|
+
w.write(universe.atoms)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def strip_topol(
|
|
246
|
+
fn_topol: PathLike,
|
|
247
|
+
fn_coords: PathLike,
|
|
248
|
+
fn_out_topol: PathLike,
|
|
249
|
+
*fn_out_coords: PathLike
|
|
250
|
+
) -> None:
|
|
251
|
+
top = Topology(fn_topol)
|
|
252
|
+
u = mda.Universe(fn_topol, fn_coords, topology_format='ITP')
|
|
253
|
+
|
|
254
|
+
# Remove virtual sites from topology
|
|
255
|
+
for mol, _ in top.molecules.values():
|
|
256
|
+
mol.remove_vsites()
|
|
257
|
+
top.write(fn_out_topol, overwrite=True)
|
|
258
|
+
|
|
259
|
+
# Remove virtual sites from coordinates
|
|
260
|
+
atoms = u.select_atoms('not mass -1 to 0.5')
|
|
261
|
+
for fn_out in fn_out_coords:
|
|
262
|
+
atoms.write(fn_out, frames=u.trajectory[[-1]])
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def prepare_bias_file(
|
|
266
|
+
bias: BiasSpec,
|
|
267
|
+
fn_out: PathLike,
|
|
268
|
+
) -> Path | None:
|
|
269
|
+
"""Copy a user-provided bias file for one simulation window.
|
|
270
|
+
|
|
271
|
+
Parameters
|
|
272
|
+
----------
|
|
273
|
+
bias
|
|
274
|
+
Bias specification for the current window.
|
|
275
|
+
fn_out
|
|
276
|
+
Output bias file path.
|
|
277
|
+
|
|
278
|
+
Returns
|
|
279
|
+
-------
|
|
280
|
+
pathlib.Path or None
|
|
281
|
+
Path to the prepared bias file, or ``None`` for unbiased systems.
|
|
282
|
+
"""
|
|
283
|
+
if not bias.is_biased or bias.input_file is None:
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
fn_out = Path(fn_out).resolve()
|
|
287
|
+
shutil.copy2(bias.input_file, fn_out)
|
|
288
|
+
return fn_out
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def localize_bias_spec(
|
|
292
|
+
bias: BiasSpec,
|
|
293
|
+
fn_input: Path | None,
|
|
294
|
+
) -> BiasSpec:
|
|
295
|
+
"""Return a bias spec that points to a prepared local bias input file."""
|
|
296
|
+
if fn_input is None or not bias.is_biased:
|
|
297
|
+
return bias
|
|
298
|
+
if bias.kind == "colvars":
|
|
299
|
+
return BiasSpec(kind="colvars", colvars_file=fn_input)
|
|
300
|
+
if bias.kind == "plumed":
|
|
301
|
+
return BiasSpec(kind="plumed", plumed_file=fn_input)
|
|
302
|
+
return bias
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def resolve_bias_window_definition(
|
|
306
|
+
bias: BiasSpec,
|
|
307
|
+
fn_system: PathLike,
|
|
308
|
+
) -> tuple[tuple[str, ...], tuple[float, ...]]:
|
|
309
|
+
"""Resolve distance-based window metadata from a simple bias file.
|
|
310
|
+
|
|
311
|
+
Returns empty tuples for unbiased systems or for bias files that do not
|
|
312
|
+
expose simple distance centers.
|
|
313
|
+
"""
|
|
314
|
+
if not bias.is_biased or bias.input_file is None:
|
|
315
|
+
return (), ()
|
|
316
|
+
|
|
317
|
+
if bias.kind == "colvars":
|
|
318
|
+
return resolve_colvars_distance_bias_metadata(bias.input_file, fn_system)
|
|
319
|
+
if bias.kind == "plumed":
|
|
320
|
+
return resolve_plumed_distance_bias_metadata(bias.input_file, fn_system)
|
|
321
|
+
return (), ()
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def window_name(window_index: int) -> str:
|
|
325
|
+
"""Return the canonical zero-padded window name."""
|
|
326
|
+
return f"window-{window_index:03d}"
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def topology_name(topology_index: int) -> str:
|
|
330
|
+
"""Return the canonical zero-padded topology name."""
|
|
331
|
+
return f"topology-{topology_index:03d}"
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def sample_snapshot_indices(n_frames: int, n_snapshots: int) -> np.ndarray:
|
|
335
|
+
"""Choose evenly spaced snapshot indices from a trajectory."""
|
|
336
|
+
if n_frames <= 0:
|
|
337
|
+
raise ValueError("Cannot sample snapshots from an empty trajectory.")
|
|
338
|
+
count = min(int(n_snapshots), int(n_frames))
|
|
339
|
+
return np.unique(np.linspace(0, n_frames - 1, num=count, dtype=int))
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def write_snapshot_xyz_files(
|
|
343
|
+
universe: mda.Universe,
|
|
344
|
+
*,
|
|
345
|
+
snapshots_dir: Path,
|
|
346
|
+
n_snapshots: int,
|
|
347
|
+
) -> list[Path]:
|
|
348
|
+
"""Write evenly spaced XYZ snapshots from a trajectory."""
|
|
349
|
+
snapshots_dir.mkdir(parents=True, exist_ok=True)
|
|
350
|
+
indices = sample_snapshot_indices(universe.trajectory.n_frames, n_snapshots)
|
|
351
|
+
atoms = universe.select_atoms("not mass -1 to 0.5")
|
|
352
|
+
written: list[Path] = []
|
|
353
|
+
|
|
354
|
+
for output_index, frame_index in enumerate(indices):
|
|
355
|
+
fn_snapshot = snapshots_dir / f"snapshot-{output_index:04d}.xyz"
|
|
356
|
+
with warnings.catch_warnings():
|
|
357
|
+
warnings.filterwarnings(
|
|
358
|
+
"ignore",
|
|
359
|
+
message=r".*Reader has no dt information, set to 1.0 ps.*",
|
|
360
|
+
category=UserWarning,
|
|
361
|
+
)
|
|
362
|
+
atoms.write(fn_snapshot, frames=universe.trajectory[[frame_index]])
|
|
363
|
+
written.append(fn_snapshot)
|
|
364
|
+
|
|
365
|
+
return written
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def write_window_coordinates(
|
|
369
|
+
universe: mda.Universe,
|
|
370
|
+
*,
|
|
371
|
+
box: np.ndarray,
|
|
372
|
+
fn_out: PathLike,
|
|
373
|
+
pair_labels: Sequence[str],
|
|
374
|
+
target_distances: Sequence[float],
|
|
375
|
+
) -> None:
|
|
376
|
+
"""Write coordinates for one NVT window."""
|
|
377
|
+
if pair_labels and target_distances:
|
|
378
|
+
create_bias_window(
|
|
379
|
+
universe,
|
|
380
|
+
list(pair_labels),
|
|
381
|
+
list(target_distances),
|
|
382
|
+
box,
|
|
383
|
+
fn_out,
|
|
384
|
+
)
|
|
385
|
+
return
|
|
386
|
+
|
|
387
|
+
with mda.Writer(fn_out, "w") as writer:
|
|
388
|
+
ts = universe.trajectory[-1]
|
|
389
|
+
ts.dimensions = box
|
|
390
|
+
writer.write(universe.atoms)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def window_run_name(window_index: int) -> str:
|
|
394
|
+
"""Return the canonical filename stem for the prepared production run."""
|
|
395
|
+
return f"{window_name(window_index)}-prod"
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def prepare_equilibrated_topology(
|
|
399
|
+
*,
|
|
400
|
+
topol_index: int,
|
|
401
|
+
fn_topol: Path,
|
|
402
|
+
templates: dict[str, Path],
|
|
403
|
+
box: list[float] | None,
|
|
404
|
+
fn_mdp_em: Path,
|
|
405
|
+
fn_mdp_npt: Path,
|
|
406
|
+
nsteps_npt: int,
|
|
407
|
+
equilibration_dir: Path,
|
|
408
|
+
gmx_cmd: str,
|
|
409
|
+
fn_gmx_log: Path,
|
|
410
|
+
logger: Logger,
|
|
411
|
+
) -> EquilibratedTopology:
|
|
412
|
+
"""Create, minimize, and optionally NpT-equilibrate one unique topology."""
|
|
413
|
+
topology_label = topology_name(topol_index)
|
|
414
|
+
fn_coord_box = equilibration_dir / f"{topology_label}-box.gro"
|
|
415
|
+
|
|
416
|
+
logger.info("Creating box: in progress...", overwrite=True, level=2)
|
|
417
|
+
universe, topol = create_box(
|
|
418
|
+
fn_topol,
|
|
419
|
+
templates,
|
|
420
|
+
fn_out=fn_coord_box,
|
|
421
|
+
box=box,
|
|
422
|
+
)
|
|
423
|
+
logger.info("Creating box: Done.", level=2)
|
|
424
|
+
|
|
425
|
+
fn_topol_processed = fn_coord_box.with_suffix(".top")
|
|
426
|
+
topol.write(fn_topol_processed, overwrite=True)
|
|
427
|
+
maxwarn = determine_maxwarn(topol)
|
|
428
|
+
|
|
429
|
+
deffnm_em = equilibration_dir / f"{topology_label}-em"
|
|
430
|
+
logger.info("Energy minimization: in progress...", overwrite=True, level=2)
|
|
431
|
+
run_md(
|
|
432
|
+
deffnm_em,
|
|
433
|
+
fn_mdp_em,
|
|
434
|
+
fn_topol_processed,
|
|
435
|
+
fn_coord_box,
|
|
436
|
+
fn_ndx=None,
|
|
437
|
+
gmx_cmd=gmx_cmd,
|
|
438
|
+
n_steps=-2,
|
|
439
|
+
maxwarn=maxwarn,
|
|
440
|
+
fn_log=fn_gmx_log,
|
|
441
|
+
)
|
|
442
|
+
logger.info("Energy minimization: Done.", level=2)
|
|
443
|
+
|
|
444
|
+
if nsteps_npt <= 0:
|
|
445
|
+
logger.info("NpT equilibration: skipped (box defined)", level=2)
|
|
446
|
+
return EquilibratedTopology(
|
|
447
|
+
fn_topol_processed=fn_topol_processed,
|
|
448
|
+
fn_coord=deffnm_em.with_suffix(".gro"),
|
|
449
|
+
universe=mda.Universe(
|
|
450
|
+
deffnm_em.with_suffix(".gro"),
|
|
451
|
+
to_guess=("elements", "masses"),
|
|
452
|
+
),
|
|
453
|
+
box=np.asarray(universe.dimensions, dtype=float),
|
|
454
|
+
maxwarn=maxwarn,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
deffnm_npt = equilibration_dir / f"{topology_label}-npt"
|
|
458
|
+
logger.info("NpT equilibration: in progress...", overwrite=True, level=2)
|
|
459
|
+
run_md(
|
|
460
|
+
deffnm_npt,
|
|
461
|
+
fn_mdp_npt,
|
|
462
|
+
fn_topol_processed,
|
|
463
|
+
deffnm_em.with_suffix(".gro"),
|
|
464
|
+
fn_ndx=None,
|
|
465
|
+
gmx_cmd=gmx_cmd,
|
|
466
|
+
n_steps=nsteps_npt,
|
|
467
|
+
maxwarn=maxwarn,
|
|
468
|
+
fn_log=fn_gmx_log,
|
|
469
|
+
)
|
|
470
|
+
universe = mda.Universe(
|
|
471
|
+
deffnm_npt.with_suffix(".gro"),
|
|
472
|
+
deffnm_npt.with_suffix(".xtc"),
|
|
473
|
+
to_guess=("elements", "masses"),
|
|
474
|
+
)
|
|
475
|
+
universe.trajectory.add_transformations(trans.unwrap(universe.atoms))
|
|
476
|
+
discard = int(universe.trajectory.n_frames * 0.2)
|
|
477
|
+
box_avg = get_average_box(universe, start=discard)
|
|
478
|
+
logger.info("NpT equilibration: Done.", level=2)
|
|
479
|
+
|
|
480
|
+
return EquilibratedTopology(
|
|
481
|
+
fn_topol_processed=fn_topol_processed,
|
|
482
|
+
fn_coord=deffnm_npt.with_suffix(".gro"),
|
|
483
|
+
universe=universe,
|
|
484
|
+
box=box_avg,
|
|
485
|
+
maxwarn=maxwarn,
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def save_training_artifacts(
|
|
490
|
+
*,
|
|
491
|
+
window_index: int,
|
|
492
|
+
training_dir: Path,
|
|
493
|
+
fn_mdp_em: Path,
|
|
494
|
+
fn_mdp_npt: Path,
|
|
495
|
+
fn_mdp_prod: Path,
|
|
496
|
+
fn_coord: Path,
|
|
497
|
+
fn_ndx: Path,
|
|
498
|
+
fn_topol: Path,
|
|
499
|
+
fn_bias_input: Path | None,
|
|
500
|
+
bias: BiasSpec,
|
|
501
|
+
) -> None:
|
|
502
|
+
"""Save all training artifacts produced for one window."""
|
|
503
|
+
window_label = window_name(window_index)
|
|
504
|
+
asset_dir = training_dir / window_label
|
|
505
|
+
asset_dir.mkdir(parents=True, exist_ok=True)
|
|
506
|
+
for src, dst in [
|
|
507
|
+
(fn_mdp_em, asset_dir / f"{window_label}.em.mdp"),
|
|
508
|
+
(fn_mdp_npt, asset_dir / f"{window_label}.npt.mdp"),
|
|
509
|
+
(fn_mdp_prod, asset_dir / f"{window_label}.mdp"),
|
|
510
|
+
(fn_coord, asset_dir / f"{window_label}.gro"),
|
|
511
|
+
(fn_ndx, asset_dir / f"{window_label}.ndx"),
|
|
512
|
+
(fn_topol, asset_dir / f"{window_label}.top"),
|
|
513
|
+
]:
|
|
514
|
+
shutil.copy2(src, dst)
|
|
515
|
+
|
|
516
|
+
for suffix in ("bias.colvars.dat", "bias.plumed.dat"):
|
|
517
|
+
stale_file = asset_dir / f"{window_label}.{suffix}"
|
|
518
|
+
if stale_file.exists():
|
|
519
|
+
stale_file.unlink()
|
|
520
|
+
|
|
521
|
+
if fn_bias_input is not None and bias.input_filename is not None:
|
|
522
|
+
fn_bias_train = asset_dir / f"{window_label}.{bias.input_filename}"
|
|
523
|
+
shutil.copy2(fn_bias_input, fn_bias_train)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def write_reference_md_assets(
|
|
527
|
+
*,
|
|
528
|
+
reference_md_dir: Path,
|
|
529
|
+
window_index: int,
|
|
530
|
+
project_name: str,
|
|
531
|
+
charge: int,
|
|
532
|
+
mult: int,
|
|
533
|
+
box: np.ndarray,
|
|
534
|
+
fn_topol_processed: Path,
|
|
535
|
+
fn_coord_nvt: Path,
|
|
536
|
+
bias: BiasSpec,
|
|
537
|
+
logger: Logger,
|
|
538
|
+
) -> None:
|
|
539
|
+
"""Write CP2K MD assets for one simulation window."""
|
|
540
|
+
logger.info("Reference MD assets: in progress...", overwrite=True, level=2)
|
|
541
|
+
window_dir = reference_md_dir / window_name(window_index)
|
|
542
|
+
window_dir.mkdir(parents=True, exist_ok=True)
|
|
543
|
+
|
|
544
|
+
strip_topol(
|
|
545
|
+
fn_topol_processed,
|
|
546
|
+
fn_coord_nvt,
|
|
547
|
+
window_dir / "system.top",
|
|
548
|
+
window_dir / "system.gro",
|
|
549
|
+
window_dir / "system.xyz",
|
|
550
|
+
)
|
|
551
|
+
fn_plumed = None
|
|
552
|
+
stale_plumed = window_dir / "plumed.dat"
|
|
553
|
+
if stale_plumed.exists():
|
|
554
|
+
stale_plumed.unlink()
|
|
555
|
+
if bias.kind == "plumed" and bias.input_file is not None:
|
|
556
|
+
fn_plumed = window_dir / "plumed.dat"
|
|
557
|
+
shutil.copy2(bias.input_file, fn_plumed)
|
|
558
|
+
|
|
559
|
+
for cp2k_specs in [
|
|
560
|
+
{"eq": True, "restart": False, "fn": "md-eq-start.inp"},
|
|
561
|
+
{"eq": True, "restart": True, "fn": "md-eq-restart.inp"},
|
|
562
|
+
{"eq": False, "restart": True, "fn": "md-prod.inp"},
|
|
563
|
+
]:
|
|
564
|
+
make_cp2k_input(
|
|
565
|
+
project_name,
|
|
566
|
+
charge,
|
|
567
|
+
mult,
|
|
568
|
+
box[:3].astype(float).tolist(),
|
|
569
|
+
window_dir / "system.xyz",
|
|
570
|
+
cp2k_specs["eq"],
|
|
571
|
+
cp2k_specs["restart"],
|
|
572
|
+
window_dir / cp2k_specs["fn"],
|
|
573
|
+
plumed_input_file=fn_plumed,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
fn_submit = window_dir / "submit-slurm.sh"
|
|
577
|
+
write_cp2k_md_slurm_script(fn_submit, uses_plumed=fn_plumed is not None)
|
|
578
|
+
fn_submit.chmod(0o755)
|
|
579
|
+
logger.info("Reference MD assets: Done.", level=2)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def write_reference_single_point_assets(
|
|
583
|
+
*,
|
|
584
|
+
reference_sp_dir: Path,
|
|
585
|
+
window_index: int,
|
|
586
|
+
project_name: str,
|
|
587
|
+
charge: int,
|
|
588
|
+
mult: int,
|
|
589
|
+
box: np.ndarray,
|
|
590
|
+
fn_topol_processed: Path,
|
|
591
|
+
fn_coord_nvt: Path,
|
|
592
|
+
fn_trj_nvt: Path,
|
|
593
|
+
n_snapshots: int,
|
|
594
|
+
logger: Logger,
|
|
595
|
+
) -> None:
|
|
596
|
+
"""Write CP2K single-point assets for one simulation window."""
|
|
597
|
+
logger.info(
|
|
598
|
+
"Reference single-point assets: in progress...",
|
|
599
|
+
overwrite=True,
|
|
600
|
+
level=2,
|
|
601
|
+
)
|
|
602
|
+
window_dir = reference_sp_dir / window_name(window_index)
|
|
603
|
+
snapshots_dir = window_dir / "snapshots"
|
|
604
|
+
runs_dir = window_dir / "runs"
|
|
605
|
+
window_dir.mkdir(parents=True, exist_ok=True)
|
|
606
|
+
runs_dir.mkdir(exist_ok=True)
|
|
607
|
+
|
|
608
|
+
with warnings.catch_warnings():
|
|
609
|
+
warnings.filterwarnings(
|
|
610
|
+
"ignore",
|
|
611
|
+
message=r".*Reader has no dt information, set to 1.0 ps.*",
|
|
612
|
+
category=UserWarning,
|
|
613
|
+
)
|
|
614
|
+
universe = mda.Universe(
|
|
615
|
+
fn_topol_processed,
|
|
616
|
+
fn_coord_nvt,
|
|
617
|
+
topology_format="ITP",
|
|
618
|
+
)
|
|
619
|
+
universe.load_new(fn_trj_nvt, dt=1)
|
|
620
|
+
snapshot_files = write_snapshot_xyz_files(
|
|
621
|
+
universe,
|
|
622
|
+
snapshots_dir=snapshots_dir,
|
|
623
|
+
n_snapshots=n_snapshots,
|
|
624
|
+
)
|
|
625
|
+
fn_template_xyz = window_dir / "snapshot.xyz"
|
|
626
|
+
shutil.copy2(snapshot_files[0], fn_template_xyz)
|
|
627
|
+
make_cp2k_single_point_input(
|
|
628
|
+
project_name,
|
|
629
|
+
charge,
|
|
630
|
+
mult,
|
|
631
|
+
box[:3].astype(float).tolist(),
|
|
632
|
+
fn_template_xyz,
|
|
633
|
+
window_dir / "single-point.inp",
|
|
634
|
+
)
|
|
635
|
+
fn_submit = window_dir / "submit-slurm.sh"
|
|
636
|
+
write_cp2k_single_point_slurm_script(fn_submit)
|
|
637
|
+
fn_submit.chmod(0o755)
|
|
638
|
+
logger.info("Reference single-point assets: Done.", level=2)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def main(fn_config: PathLike) -> None:
|
|
642
|
+
config = PrepareConfig.load(fn_config)
|
|
643
|
+
check_gmx_available(config.gmx_cmd)
|
|
644
|
+
if any(system.bias.kind == "plumed" for system in config.systems):
|
|
645
|
+
ensure_plumed_kernel()
|
|
646
|
+
|
|
647
|
+
fn_gmx_log = config.project_dir / "gmx.log"
|
|
648
|
+
fn_gmx_log.parent.mkdir(parents=True, exist_ok=True)
|
|
649
|
+
|
|
650
|
+
logger = Logger("prepare", str(config.fn_log) if config.fn_log else None)
|
|
651
|
+
logger.info("", level=0)
|
|
652
|
+
logger.info(f"=== Preparing project: {config.project_dir.name} ===\n", level=0)
|
|
653
|
+
|
|
654
|
+
(
|
|
655
|
+
equilibration_dir,
|
|
656
|
+
training_dir,
|
|
657
|
+
reference_md_dir,
|
|
658
|
+
reference_sp_dir,
|
|
659
|
+
) = setup_directories(config.project_dir)
|
|
660
|
+
|
|
661
|
+
equilibrated_topologies: Dict[str, EquilibratedTopology] = {}
|
|
662
|
+
|
|
663
|
+
n_total = len(config.systems)
|
|
664
|
+
for i, system in enumerate(config.systems):
|
|
665
|
+
logger.info(f"System: {i + 1}/{n_total}", level=1)
|
|
666
|
+
|
|
667
|
+
topology_key = str(system.fn_topol)
|
|
668
|
+
if topology_key not in equilibrated_topologies:
|
|
669
|
+
topol_index = next(
|
|
670
|
+
j
|
|
671
|
+
for j, candidate in enumerate(config.systems)
|
|
672
|
+
if candidate.fn_topol == system.fn_topol
|
|
673
|
+
)
|
|
674
|
+
equilibrated_topologies[topology_key] = prepare_equilibrated_topology(
|
|
675
|
+
topol_index=topol_index,
|
|
676
|
+
fn_topol=system.fn_topol,
|
|
677
|
+
templates=system.templates,
|
|
678
|
+
box=system.box,
|
|
679
|
+
fn_mdp_em=system.fn_mdp_em,
|
|
680
|
+
fn_mdp_npt=system.fn_mdp_npt,
|
|
681
|
+
nsteps_npt=system.nsteps_npt,
|
|
682
|
+
equilibration_dir=equilibration_dir,
|
|
683
|
+
gmx_cmd=config.gmx_cmd,
|
|
684
|
+
fn_gmx_log=fn_gmx_log,
|
|
685
|
+
logger=logger,
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
topology_state = equilibrated_topologies[topology_key]
|
|
689
|
+
|
|
690
|
+
window_label = window_name(i)
|
|
691
|
+
fn_mdp_prod = system.fn_mdp_prod
|
|
692
|
+
fn_prod_local = equilibration_dir / f"{window_label}.mdp"
|
|
693
|
+
fn_ndx = equilibration_dir / f"{window_label}.ndx"
|
|
694
|
+
fn_coord = equilibration_dir / f"{window_label}.gro"
|
|
695
|
+
fn_bias_local = (
|
|
696
|
+
None
|
|
697
|
+
if system.bias.input_filename is None
|
|
698
|
+
else equilibration_dir / f"{window_label}.{system.bias.input_filename}"
|
|
699
|
+
)
|
|
700
|
+
fn_topol_local = equilibration_dir / f"{window_label}.top"
|
|
701
|
+
fn_topol_local.write_text(topology_state.fn_topol_processed.read_text())
|
|
702
|
+
fn_bias_input = None if fn_bias_local is None else prepare_bias_file(
|
|
703
|
+
system.bias,
|
|
704
|
+
fn_bias_local,
|
|
705
|
+
)
|
|
706
|
+
bias_run = localize_bias_spec(system.bias, fn_bias_input)
|
|
707
|
+
pair_labels, target_distances = resolve_bias_window_definition(
|
|
708
|
+
system.bias,
|
|
709
|
+
topology_state.fn_coord,
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
write_window_coordinates(
|
|
713
|
+
topology_state.universe,
|
|
714
|
+
box=topology_state.box,
|
|
715
|
+
fn_out=fn_coord,
|
|
716
|
+
pair_labels=pair_labels,
|
|
717
|
+
target_distances=target_distances,
|
|
718
|
+
)
|
|
719
|
+
make_ndx(topology_state.universe, None, fn_out=fn_ndx)
|
|
720
|
+
shutil.copy2(fn_mdp_prod, fn_prod_local)
|
|
721
|
+
|
|
722
|
+
deffnm_prod = equilibration_dir / window_run_name(i)
|
|
723
|
+
logger.info(
|
|
724
|
+
"Production preparation run: in progress...",
|
|
725
|
+
overwrite=True,
|
|
726
|
+
level=2,
|
|
727
|
+
)
|
|
728
|
+
run_md(
|
|
729
|
+
deffnm_prod,
|
|
730
|
+
fn_prod_local,
|
|
731
|
+
fn_topol_local,
|
|
732
|
+
fn_coord,
|
|
733
|
+
fn_ndx,
|
|
734
|
+
bias=bias_run,
|
|
735
|
+
gmx_cmd=config.gmx_cmd,
|
|
736
|
+
n_steps=system.nsteps_prod,
|
|
737
|
+
maxwarn=topology_state.maxwarn,
|
|
738
|
+
fn_log=fn_gmx_log,
|
|
739
|
+
)
|
|
740
|
+
logger.info("Production preparation run: Done.", level=2)
|
|
741
|
+
|
|
742
|
+
logger.info("Training MD assets: in progress...", overwrite=True, level=2)
|
|
743
|
+
save_training_artifacts(
|
|
744
|
+
window_index=i,
|
|
745
|
+
training_dir=training_dir,
|
|
746
|
+
fn_mdp_em=system.fn_mdp_em,
|
|
747
|
+
fn_mdp_npt=system.fn_mdp_npt,
|
|
748
|
+
fn_mdp_prod=system.fn_mdp_prod,
|
|
749
|
+
fn_coord=fn_coord,
|
|
750
|
+
fn_ndx=fn_ndx,
|
|
751
|
+
fn_topol=fn_topol_local,
|
|
752
|
+
fn_bias_input=fn_bias_input,
|
|
753
|
+
bias=system.bias,
|
|
754
|
+
)
|
|
755
|
+
logger.info("Training MD assets: Done.", level=2)
|
|
756
|
+
write_reference_md_assets(
|
|
757
|
+
reference_md_dir=reference_md_dir,
|
|
758
|
+
window_index=i,
|
|
759
|
+
project_name=config.project_dir.name,
|
|
760
|
+
charge=system.charge,
|
|
761
|
+
mult=system.mult,
|
|
762
|
+
box=topology_state.box,
|
|
763
|
+
fn_topol_processed=topology_state.fn_topol_processed,
|
|
764
|
+
fn_coord_nvt=deffnm_prod.with_suffix(".gro"),
|
|
765
|
+
bias=system.bias,
|
|
766
|
+
logger=logger,
|
|
767
|
+
)
|
|
768
|
+
write_reference_single_point_assets(
|
|
769
|
+
reference_sp_dir=reference_sp_dir,
|
|
770
|
+
window_index=i,
|
|
771
|
+
project_name=config.project_dir.name,
|
|
772
|
+
charge=system.charge,
|
|
773
|
+
mult=system.mult,
|
|
774
|
+
box=topology_state.box,
|
|
775
|
+
fn_topol_processed=topology_state.fn_topol_processed,
|
|
776
|
+
fn_coord_nvt=deffnm_prod.with_suffix(".gro"),
|
|
777
|
+
fn_trj_nvt=deffnm_prod.with_suffix(".xtc"),
|
|
778
|
+
n_snapshots=config.n_single_point_snapshots,
|
|
779
|
+
logger=logger,
|
|
780
|
+
)
|
|
781
|
+
logger.info("", level=0)
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
if __name__ == "__main__":
|
|
785
|
+
parser = argparse.ArgumentParser(description="Prepare simulation assets.")
|
|
786
|
+
parser.add_argument('fn_config', help='Path to the config file [YAML].')
|
|
787
|
+
args = parser.parse_args()
|
|
788
|
+
main(args.fn_config)
|