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/learn.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Workflow entry point for posterior learning from trained surrogates."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Mapping, Union
|
|
6
|
+
|
|
7
|
+
from ..domain.specs import ChargeConstraint
|
|
8
|
+
from ..io.logs import Logger
|
|
9
|
+
from .configs import LearnConfig
|
|
10
|
+
|
|
11
|
+
PathLike = Union[str, Path]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _import_learning_stack():
|
|
15
|
+
try:
|
|
16
|
+
from ..bayes.gaussian_process import LGPCommittee
|
|
17
|
+
from ..bayes.learning import InferenceProblem
|
|
18
|
+
except ModuleNotFoundError as exc:
|
|
19
|
+
if exc.name == "torch":
|
|
20
|
+
raise RuntimeError(
|
|
21
|
+
"PyTorch is required for 'bff learn'. Install a CPU or CUDA "
|
|
22
|
+
"build of PyTorch first."
|
|
23
|
+
) from exc
|
|
24
|
+
raise
|
|
25
|
+
return LGPCommittee, InferenceProblem
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_models(model_paths: Mapping[str, PathLike]) -> dict[str, object]:
|
|
29
|
+
"""Load trained surrogate models from QoI-name to file-path mapping."""
|
|
30
|
+
lgp_committee_type, _ = _import_learning_stack()
|
|
31
|
+
return {
|
|
32
|
+
name: lgp_committee_type.load(path)
|
|
33
|
+
for name, path in model_paths.items()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def build_problem(
|
|
38
|
+
*,
|
|
39
|
+
specs: PathLike,
|
|
40
|
+
model_paths: Mapping[str, PathLike],
|
|
41
|
+
):
|
|
42
|
+
"""Build the inference problem from specs and trained model files."""
|
|
43
|
+
_, inference_problem_type = _import_learning_stack()
|
|
44
|
+
constraint = ChargeConstraint(specs)
|
|
45
|
+
models = load_models(model_paths)
|
|
46
|
+
return inference_problem_type.from_models(models, constraint=constraint)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main(fn_config: PathLike) -> None:
|
|
50
|
+
config = LearnConfig.load(fn_config)
|
|
51
|
+
logger = Logger("BFF", str(config.log), mode="w")
|
|
52
|
+
problem = build_problem(
|
|
53
|
+
specs=config.specs,
|
|
54
|
+
model_paths=config.models,
|
|
55
|
+
)
|
|
56
|
+
problem.infer(
|
|
57
|
+
priors_disttype=config.mcmc.priors_disttype,
|
|
58
|
+
total_steps=config.mcmc.total_steps,
|
|
59
|
+
warmup=config.mcmc.warmup,
|
|
60
|
+
thin=config.mcmc.thin,
|
|
61
|
+
progress_stride=config.mcmc.progress_stride,
|
|
62
|
+
n_walkers=config.mcmc.n_walkers,
|
|
63
|
+
fn_posterior=config.mcmc.posterior,
|
|
64
|
+
fn_checkpoint=config.mcmc.checkpoint,
|
|
65
|
+
fn_priors=config.mcmc.priors,
|
|
66
|
+
restart=config.mcmc.restart,
|
|
67
|
+
device=config.mcmc.device,
|
|
68
|
+
logger=logger,
|
|
69
|
+
rhat_tol=config.mcmc.rhat_tol,
|
|
70
|
+
ess_min=config.mcmc.ess_min,
|
|
71
|
+
include_implicit_charge=config.mcmc.include_implicit_charge,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
fn_config = sys.argv[1]
|
|
77
|
+
main(fn_config)
|
bff/workflows/md.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from ..domain.specs import Specs
|
|
11
|
+
from ..io.colvars import write_mdp_with_colvars
|
|
12
|
+
from ..io.commands import build_command
|
|
13
|
+
from ..io.mdp import get_n_frames_target
|
|
14
|
+
from ..io.plumed import ensure_plumed_kernel
|
|
15
|
+
from ..io.utils import save_yaml
|
|
16
|
+
from ..topology import TopologyModifier
|
|
17
|
+
from .configs import MDJobConfig
|
|
18
|
+
|
|
19
|
+
PathLike = Union[str, Path]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _sample_output_paths(
|
|
23
|
+
run_dir: Path,
|
|
24
|
+
sample_id: str,
|
|
25
|
+
system_index: int,
|
|
26
|
+
) -> list[Path]:
|
|
27
|
+
"""Return all files created for one sample/system pair."""
|
|
28
|
+
files: set[Path] = set()
|
|
29
|
+
for pattern in (
|
|
30
|
+
f"md-{sample_id}-{system_index}*",
|
|
31
|
+
f"md-{sample_id}-{system_index:03d}*",
|
|
32
|
+
):
|
|
33
|
+
files.update(path for path in run_dir.glob(pattern) if path.is_file())
|
|
34
|
+
return sorted(files)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _prune_unstored_outputs(
|
|
38
|
+
files: list[Path],
|
|
39
|
+
store: tuple[str, ...],
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Remove generated files whose suffix is not requested in ``store``."""
|
|
42
|
+
keep_suffixes = {"." + ext.lstrip(".") for ext in store}
|
|
43
|
+
for file in files:
|
|
44
|
+
if file.suffix in keep_suffixes:
|
|
45
|
+
continue
|
|
46
|
+
file.unlink(missing_ok=True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _prune_auxiliary_outputs(run_dir: Path, store: tuple[str, ...]) -> None:
|
|
50
|
+
"""Remove shared GROMACS or PLUMED aux files that are not requested."""
|
|
51
|
+
keep_suffixes = {"." + ext.lstrip(".") for ext in store}
|
|
52
|
+
for pattern in ("mdout.mdp", "PLUMED.OUT", "bck*.PLUMED.OUT"):
|
|
53
|
+
for file in run_dir.glob(pattern):
|
|
54
|
+
if file.suffix in keep_suffixes:
|
|
55
|
+
continue
|
|
56
|
+
file.unlink(missing_ok=True)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def check_success(
|
|
60
|
+
fn_trj: str,
|
|
61
|
+
fn_mdp: str,
|
|
62
|
+
n_target: int,
|
|
63
|
+
gmx_cmd: str = "gmx",
|
|
64
|
+
) -> bool:
|
|
65
|
+
"""Check if the trajectory has reached the target number of frames."""
|
|
66
|
+
output = subprocess.run(
|
|
67
|
+
build_command(gmx_cmd, 'check', '-f', fn_trj),
|
|
68
|
+
capture_output=True,
|
|
69
|
+
text=True,
|
|
70
|
+
check=True,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
n = None
|
|
74
|
+
for line in output.stderr.splitlines():
|
|
75
|
+
if line.startswith("Step"):
|
|
76
|
+
n = int(line.split()[1])
|
|
77
|
+
break
|
|
78
|
+
if n is None:
|
|
79
|
+
raise RuntimeError(f"Could not determine trajectory length from {fn_trj}.")
|
|
80
|
+
|
|
81
|
+
n_target_mdp, stride = get_n_frames_target(fn_mdp)
|
|
82
|
+
n_target = n_target_mdp if n_target == -2 else (n_target // stride)
|
|
83
|
+
|
|
84
|
+
return n >= n_target
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def modify_topology(
|
|
88
|
+
fn_topol: str,
|
|
89
|
+
specs: Union[str, dict, Specs],
|
|
90
|
+
params: Union[list, np.ndarray],
|
|
91
|
+
implicit: bool,
|
|
92
|
+
fn_out: str
|
|
93
|
+
) -> None:
|
|
94
|
+
|
|
95
|
+
if isinstance(specs, (str, Path, dict)):
|
|
96
|
+
specs = Specs(specs)
|
|
97
|
+
|
|
98
|
+
constraint_charge = specs.constraint_charge if implicit else None
|
|
99
|
+
params_dict = specs.parameter_dict(params, explicit_only=implicit)
|
|
100
|
+
|
|
101
|
+
top_modifier = TopologyModifier(
|
|
102
|
+
fn_topol,
|
|
103
|
+
specs.mol_resname,
|
|
104
|
+
specs.implicit_atoms,
|
|
105
|
+
)
|
|
106
|
+
top_modifier.apply_parameters(params_dict, constraint_charge=constraint_charge)
|
|
107
|
+
|
|
108
|
+
if fn_out:
|
|
109
|
+
top_modifier.write(fn_out)
|
|
110
|
+
return top_modifier
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main(fn_config: PathLike) -> None:
|
|
114
|
+
config = MDJobConfig.load(fn_config)
|
|
115
|
+
if config.fn_specs is None:
|
|
116
|
+
raise ValueError("MD job configuration requires 'fn_specs'.")
|
|
117
|
+
sample_id = config.sample_id
|
|
118
|
+
params = config.params
|
|
119
|
+
trainset_dir = config.trainset_dir
|
|
120
|
+
specs = Specs(config.fn_specs)
|
|
121
|
+
implicit = True
|
|
122
|
+
gmx_cmd = config.gmx_cmd
|
|
123
|
+
run = config.run
|
|
124
|
+
if any(
|
|
125
|
+
system.bias.kind == "plumed"
|
|
126
|
+
for system in config.systems
|
|
127
|
+
):
|
|
128
|
+
ensure_plumed_kernel()
|
|
129
|
+
|
|
130
|
+
# Determine the run directory
|
|
131
|
+
job_scheduler = config.job_scheduler
|
|
132
|
+
run_dir = trainset_dir if job_scheduler == "local" else Path("./").resolve()
|
|
133
|
+
|
|
134
|
+
fn_log = trainset_dir / "gmx.log"
|
|
135
|
+
success = []
|
|
136
|
+
outputs: list[dict[str, str | None]] = []
|
|
137
|
+
with open(fn_log, 'a+') as log:
|
|
138
|
+
try:
|
|
139
|
+
for i, system in enumerate(config.systems):
|
|
140
|
+
em = system.fn_mdp_em
|
|
141
|
+
prod = system.fn_mdp_prod
|
|
142
|
+
coord = system.fn_coordinates
|
|
143
|
+
top = system.fn_topol
|
|
144
|
+
ndx = system.fn_ndx
|
|
145
|
+
steps = system.n_steps
|
|
146
|
+
bias = system.bias
|
|
147
|
+
|
|
148
|
+
# Define the output file names
|
|
149
|
+
deffnm = run_dir / f"md-{sample_id}-{i}"
|
|
150
|
+
fn_tpr = run_dir / f"{deffnm}.tpr"
|
|
151
|
+
fn_coord_prod = coord
|
|
152
|
+
|
|
153
|
+
# Create topology with new parameters
|
|
154
|
+
fn_top_new = run_dir / f"md-{sample_id}-{i:03d}.top"
|
|
155
|
+
_ = modify_topology(top, specs, params, implicit, fn_top_new)
|
|
156
|
+
fn_prod_mdp = prod
|
|
157
|
+
mdrun_extra_args: list[str] = []
|
|
158
|
+
run_env = None
|
|
159
|
+
if bias.kind == "colvars" and bias.input_file is not None:
|
|
160
|
+
fn_bias_local = run_dir / Path(bias.input_file).name
|
|
161
|
+
if Path(bias.input_file).resolve() != fn_bias_local.resolve():
|
|
162
|
+
shutil.copy2(bias.input_file, fn_bias_local)
|
|
163
|
+
fn_prod_mdp = run_dir / f"md-{sample_id}-{i:03d}-colvars.mdp"
|
|
164
|
+
write_mdp_with_colvars(prod, fn_bias_local, fn_prod_mdp)
|
|
165
|
+
elif bias.kind == "plumed" and bias.input_file is not None:
|
|
166
|
+
kernel = ensure_plumed_kernel()
|
|
167
|
+
run_env = dict(os.environ)
|
|
168
|
+
run_env.setdefault("PLUMED_KERNEL", str(kernel))
|
|
169
|
+
mdrun_extra_args.extend(["-plumed", str(bias.input_file)])
|
|
170
|
+
|
|
171
|
+
# Skip running the simulation if specified
|
|
172
|
+
if not run:
|
|
173
|
+
success.append(True)
|
|
174
|
+
outputs.append(
|
|
175
|
+
{
|
|
176
|
+
"system_id": system.system_id,
|
|
177
|
+
"trajectory": None,
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
# Minimize energy
|
|
183
|
+
if em:
|
|
184
|
+
deffnm_em = run_dir / f"md-{sample_id}-{i}-em"
|
|
185
|
+
fn_tpr_em = run_dir / f"{deffnm_em}.tpr"
|
|
186
|
+
subprocess.run(
|
|
187
|
+
build_command(
|
|
188
|
+
gmx_cmd,
|
|
189
|
+
'grompp',
|
|
190
|
+
'-f',
|
|
191
|
+
em,
|
|
192
|
+
'-c',
|
|
193
|
+
coord,
|
|
194
|
+
'-p',
|
|
195
|
+
fn_top_new,
|
|
196
|
+
'-n',
|
|
197
|
+
ndx,
|
|
198
|
+
'-o',
|
|
199
|
+
fn_tpr_em,
|
|
200
|
+
'-maxwarn',
|
|
201
|
+
'2',
|
|
202
|
+
),
|
|
203
|
+
cwd=run_dir, stdout=log, stderr=log, check=True
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
subprocess.run(
|
|
207
|
+
build_command(
|
|
208
|
+
gmx_cmd,
|
|
209
|
+
'mdrun',
|
|
210
|
+
'-s',
|
|
211
|
+
fn_tpr_em,
|
|
212
|
+
'-deffnm',
|
|
213
|
+
deffnm_em,
|
|
214
|
+
),
|
|
215
|
+
cwd=run_dir,
|
|
216
|
+
stdout=log,
|
|
217
|
+
stderr=log,
|
|
218
|
+
check=True,
|
|
219
|
+
)
|
|
220
|
+
fn_coord_prod = deffnm_em.with_suffix(".gro")
|
|
221
|
+
else:
|
|
222
|
+
deffnm.with_suffix('.gro').write_text(coord.read_text())
|
|
223
|
+
fn_coord_prod = deffnm.with_suffix(".gro")
|
|
224
|
+
|
|
225
|
+
# Run production MD
|
|
226
|
+
subprocess.run(
|
|
227
|
+
build_command(
|
|
228
|
+
gmx_cmd,
|
|
229
|
+
'grompp',
|
|
230
|
+
'-f',
|
|
231
|
+
fn_prod_mdp,
|
|
232
|
+
'-c',
|
|
233
|
+
fn_coord_prod,
|
|
234
|
+
'-p',
|
|
235
|
+
fn_top_new,
|
|
236
|
+
'-n',
|
|
237
|
+
ndx,
|
|
238
|
+
'-o',
|
|
239
|
+
fn_tpr,
|
|
240
|
+
'-maxwarn',
|
|
241
|
+
'2',
|
|
242
|
+
),
|
|
243
|
+
cwd=run_dir, stdout=log, stderr=log, check=True
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
subprocess.run(
|
|
247
|
+
build_command(
|
|
248
|
+
gmx_cmd,
|
|
249
|
+
'mdrun',
|
|
250
|
+
'-deffnm',
|
|
251
|
+
deffnm,
|
|
252
|
+
'-nsteps',
|
|
253
|
+
str(steps),
|
|
254
|
+
'-dlb',
|
|
255
|
+
'yes',
|
|
256
|
+
*mdrun_extra_args,
|
|
257
|
+
),
|
|
258
|
+
cwd=run_dir, stdout=log, stderr=log, check=True, env=run_env
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Check if the simulation finished aka has the expected number of frames
|
|
262
|
+
success.append(
|
|
263
|
+
check_success(f'{deffnm}.xtc', fn_prod_mdp, steps, gmx_cmd)
|
|
264
|
+
)
|
|
265
|
+
generated_files = _sample_output_paths(run_dir, sample_id, i)
|
|
266
|
+
if job_scheduler == "local":
|
|
267
|
+
_prune_unstored_outputs(generated_files, config.store)
|
|
268
|
+
trajectory_name = (
|
|
269
|
+
f"{deffnm.name}.xtc" if "xtc" in config.store else None
|
|
270
|
+
)
|
|
271
|
+
outputs.append(
|
|
272
|
+
{
|
|
273
|
+
"system_id": system.system_id,
|
|
274
|
+
"trajectory": trajectory_name,
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
except Exception:
|
|
278
|
+
for system_index in range(len(config.systems)):
|
|
279
|
+
for file in _sample_output_paths(run_dir, sample_id, system_index):
|
|
280
|
+
file.unlink(missing_ok=True)
|
|
281
|
+
save_yaml(
|
|
282
|
+
{
|
|
283
|
+
"sample_id": sample_id,
|
|
284
|
+
"status": "failed",
|
|
285
|
+
"outputs": outputs,
|
|
286
|
+
},
|
|
287
|
+
trainset_dir / f"result-{sample_id}.yaml",
|
|
288
|
+
)
|
|
289
|
+
raise
|
|
290
|
+
|
|
291
|
+
if np.all(success):
|
|
292
|
+
if job_scheduler != 'local':
|
|
293
|
+
patterns = ["*." + ext for ext in config.store]
|
|
294
|
+
files = list(sum((list(run_dir.glob(p)) for p in patterns), []))
|
|
295
|
+
for file in files:
|
|
296
|
+
shutil.copy(file, trainset_dir / file.name)
|
|
297
|
+
else:
|
|
298
|
+
_prune_auxiliary_outputs(run_dir, config.store)
|
|
299
|
+
save_yaml(
|
|
300
|
+
{
|
|
301
|
+
"sample_id": sample_id,
|
|
302
|
+
"status": "completed",
|
|
303
|
+
"outputs": outputs,
|
|
304
|
+
},
|
|
305
|
+
trainset_dir / f"result-{sample_id}.yaml",
|
|
306
|
+
)
|
|
307
|
+
else:
|
|
308
|
+
for system_index in range(len(config.systems)):
|
|
309
|
+
for file in _sample_output_paths(run_dir, sample_id, system_index):
|
|
310
|
+
file.unlink(missing_ok=True)
|
|
311
|
+
save_yaml(
|
|
312
|
+
{
|
|
313
|
+
"sample_id": sample_id,
|
|
314
|
+
"status": "failed",
|
|
315
|
+
"outputs": outputs,
|
|
316
|
+
},
|
|
317
|
+
trainset_dir / f"result-{sample_id}.yaml",
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
if __name__ == "__main__":
|
|
322
|
+
parser = argparse.ArgumentParser(description="Run Gromacs simulations.")
|
|
323
|
+
parser.add_argument("-f", "--fn_config", type=str,
|
|
324
|
+
help="Path to the configuration file.")
|
|
325
|
+
args = parser.parse_args()
|
|
326
|
+
main(args.fn_config)
|