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.
Files changed (55) hide show
  1. bff/__init__.py +30 -0
  2. bff/bayes/__init__.py +11 -0
  3. bff/bayes/gaussian_process.py +285 -0
  4. bff/bayes/kernels.py +71 -0
  5. bff/bayes/learning.py +484 -0
  6. bff/bayes/likelihoods.py +142 -0
  7. bff/bayes/posterior.py +62 -0
  8. bff/bayes/priors.py +179 -0
  9. bff/bayes/results.py +327 -0
  10. bff/bayes/utils.py +333 -0
  11. bff/cli.py +254 -0
  12. bff/data.py +93 -0
  13. bff/domain/__init__.py +12 -0
  14. bff/domain/bias.py +149 -0
  15. bff/domain/specs.py +347 -0
  16. bff/domain/trainset.py +201 -0
  17. bff/io/__init__.py +12 -0
  18. bff/io/colvars.py +186 -0
  19. bff/io/commands.py +20 -0
  20. bff/io/cp2k.py +330 -0
  21. bff/io/logs.py +213 -0
  22. bff/io/mdp.py +98 -0
  23. bff/io/plumed.py +184 -0
  24. bff/io/progress.py +74 -0
  25. bff/io/schedulers.py +118 -0
  26. bff/io/utils.py +208 -0
  27. bff/mcmc/__init__.py +3 -0
  28. bff/mcmc/convergence.py +168 -0
  29. bff/mcmc/proposal.py +254 -0
  30. bff/mcmc/sampler.py +481 -0
  31. bff/plotting.py +362 -0
  32. bff/qoi/__init__.py +9 -0
  33. bff/qoi/analysis.py +209 -0
  34. bff/qoi/data.py +169 -0
  35. bff/qoi/hbonds.py +255 -0
  36. bff/qoi/rdf.py +108 -0
  37. bff/qoi/routines.py +272 -0
  38. bff/tools.py +168 -0
  39. bff/topology.py +581 -0
  40. bff/workflows/__init__.py +0 -0
  41. bff/workflows/configs.py +1147 -0
  42. bff/workflows/learn.py +77 -0
  43. bff/workflows/md.py +326 -0
  44. bff/workflows/prepare.py +788 -0
  45. bff/workflows/qoi.py +260 -0
  46. bff/workflows/runsims.py +620 -0
  47. bff/workflows/simulate.py +28 -0
  48. bff/workflows/train.py +69 -0
  49. bff/workflows/validate.py +29 -0
  50. bfflearn-0.1.0.dev1.dist-info/METADATA +913 -0
  51. bfflearn-0.1.0.dev1.dist-info/RECORD +55 -0
  52. bfflearn-0.1.0.dev1.dist-info/WHEEL +5 -0
  53. bfflearn-0.1.0.dev1.dist-info/entry_points.txt +2 -0
  54. bfflearn-0.1.0.dev1.dist-info/licenses/LICENSE +674 -0
  55. 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)