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/runsims.py
ADDED
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from gmxtopology import Topology
|
|
11
|
+
|
|
12
|
+
from ..domain.specs import ChargeConstraint, RandomParamsGenerator, Specs
|
|
13
|
+
from ..io.logs import Logger
|
|
14
|
+
from ..io.schedulers import Slurm
|
|
15
|
+
from ..io.utils import compress_results, load_yaml, save_yaml
|
|
16
|
+
from ..topology import TopologyModifier
|
|
17
|
+
from .configs import SimulateConfig, SimulationCampaignConfig, SimulationSystemConfig
|
|
18
|
+
from .md import modify_topology
|
|
19
|
+
|
|
20
|
+
PathLike = str | Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
SCHEDULER_CLASSES = {
|
|
24
|
+
"slurm": Slurm,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _relative_path(path: Path | None, base_dir: Path) -> str | None:
|
|
29
|
+
if path is None:
|
|
30
|
+
return None
|
|
31
|
+
return str(path.relative_to(base_dir))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _system_record(
|
|
35
|
+
system: SimulationSystemConfig,
|
|
36
|
+
trainset_dir: Path,
|
|
37
|
+
) -> dict[str, Any]:
|
|
38
|
+
"""Serialize one staged system into the campaign metadata file."""
|
|
39
|
+
return {
|
|
40
|
+
"system_id": system.system_id,
|
|
41
|
+
"topology": _relative_path(system.fn_topol, trainset_dir),
|
|
42
|
+
"coordinates": _relative_path(system.fn_coordinates, trainset_dir),
|
|
43
|
+
"mdp": {
|
|
44
|
+
"em": _relative_path(system.fn_mdp_em, trainset_dir),
|
|
45
|
+
"prod": _relative_path(system.fn_mdp_prod, trainset_dir),
|
|
46
|
+
},
|
|
47
|
+
"index": _relative_path(system.fn_ndx, trainset_dir),
|
|
48
|
+
"bias": _relative_path(system.bias.input_file, trainset_dir),
|
|
49
|
+
"n_steps": int(system.n_steps),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def build_specs(config: SimulateConfig) -> Path:
|
|
54
|
+
"""Create the force-field specification file for one sampled campaign."""
|
|
55
|
+
config.trainset_dir.resolve().mkdir(parents=True, exist_ok=True)
|
|
56
|
+
top_modifier = TopologyModifier(
|
|
57
|
+
config.systems[0].fn_topol,
|
|
58
|
+
config.mol_resname,
|
|
59
|
+
config.implicit_atoms,
|
|
60
|
+
)
|
|
61
|
+
bounds_resolved = top_modifier.resolve_parameter_names(config.bounds)
|
|
62
|
+
|
|
63
|
+
group: list[str] = []
|
|
64
|
+
for name in bounds_resolved:
|
|
65
|
+
param_name, *atoms = name.split()
|
|
66
|
+
if param_name == "charge":
|
|
67
|
+
group.extend(atoms)
|
|
68
|
+
group_charge = top_modifier.group_charge(group)
|
|
69
|
+
constraint_charge = (
|
|
70
|
+
config.total_charge - top_modifier.total_charge + group_charge
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
specs = Specs(
|
|
74
|
+
{
|
|
75
|
+
"mol_resname": config.mol_resname,
|
|
76
|
+
"implicit_atoms": [atom.name for atom in top_modifier.implicit_atoms],
|
|
77
|
+
"bounds": bounds_resolved,
|
|
78
|
+
"total_charge": config.total_charge,
|
|
79
|
+
"charge_target": constraint_charge,
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
fn_specs = config.trainset_dir / "specs.yaml"
|
|
83
|
+
specs.write(fn_specs)
|
|
84
|
+
return fn_specs
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def stage_systems(
|
|
88
|
+
systems: list[SimulationSystemConfig],
|
|
89
|
+
trainset_dir: Path,
|
|
90
|
+
) -> list[SimulationSystemConfig]:
|
|
91
|
+
"""Copy static simulation-system inputs into the campaign directory."""
|
|
92
|
+
staged_systems: list[SimulationSystemConfig] = []
|
|
93
|
+
for index, system in enumerate(systems):
|
|
94
|
+
fn_topol = trainset_dir / f"window-{index:03d}.top"
|
|
95
|
+
Topology(system.fn_topol).write(fn_topol, overwrite=True)
|
|
96
|
+
|
|
97
|
+
fn_coordinates = trainset_dir / f"window-{index:03d}.gro"
|
|
98
|
+
shutil.copy2(system.fn_coordinates, fn_coordinates)
|
|
99
|
+
|
|
100
|
+
fn_mdp_em = None
|
|
101
|
+
if system.fn_mdp_em is not None:
|
|
102
|
+
fn_mdp_em = trainset_dir / f"window-{index:03d}.em.mdp"
|
|
103
|
+
shutil.copy2(system.fn_mdp_em, fn_mdp_em)
|
|
104
|
+
|
|
105
|
+
fn_mdp_prod = trainset_dir / f"window-{index:03d}.mdp"
|
|
106
|
+
shutil.copy2(system.fn_mdp_prod, fn_mdp_prod)
|
|
107
|
+
|
|
108
|
+
fn_ndx = trainset_dir / f"window-{index:03d}.ndx"
|
|
109
|
+
shutil.copy2(system.fn_ndx, fn_ndx)
|
|
110
|
+
|
|
111
|
+
bias = system.bias
|
|
112
|
+
if bias.input_file is not None and bias.input_filename is not None:
|
|
113
|
+
fn_bias = trainset_dir / f"window-{index:03d}.{bias.input_filename}"
|
|
114
|
+
shutil.copy2(bias.input_file, fn_bias)
|
|
115
|
+
bias = type(bias).load(fn_bias)
|
|
116
|
+
|
|
117
|
+
staged_systems.append(
|
|
118
|
+
SimulationSystemConfig(
|
|
119
|
+
system_id=system.system_id,
|
|
120
|
+
fn_topol=fn_topol,
|
|
121
|
+
fn_coordinates=fn_coordinates,
|
|
122
|
+
fn_mdp_em=fn_mdp_em,
|
|
123
|
+
fn_mdp_prod=fn_mdp_prod,
|
|
124
|
+
fn_ndx=fn_ndx,
|
|
125
|
+
bias=bias,
|
|
126
|
+
n_steps=system.n_steps,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
return staged_systems
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def stage_campaign(
|
|
133
|
+
config: SimulationCampaignConfig,
|
|
134
|
+
*,
|
|
135
|
+
fn_specs: Path | None = None,
|
|
136
|
+
) -> tuple[Path | None, list[SimulationSystemConfig]]:
|
|
137
|
+
"""Prepare the trainset directory and write static campaign inputs."""
|
|
138
|
+
trainset_dir = config.trainset_dir.resolve()
|
|
139
|
+
trainset_dir.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
|
|
141
|
+
systems = stage_systems(config.systems, trainset_dir)
|
|
142
|
+
if fn_specs is not None:
|
|
143
|
+
resolved_specs = fn_specs.resolve()
|
|
144
|
+
else:
|
|
145
|
+
resolved_specs = None
|
|
146
|
+
|
|
147
|
+
save_yaml(
|
|
148
|
+
{
|
|
149
|
+
"systems": [_system_record(system, trainset_dir) for system in systems],
|
|
150
|
+
"samples": {},
|
|
151
|
+
},
|
|
152
|
+
trainset_dir / "samples.yaml",
|
|
153
|
+
)
|
|
154
|
+
return resolved_specs, systems
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def write_sample_job_config(
|
|
158
|
+
*,
|
|
159
|
+
sample_id: str,
|
|
160
|
+
sample: np.ndarray,
|
|
161
|
+
trainset_dir: Path,
|
|
162
|
+
fn_specs: Path,
|
|
163
|
+
gmx_cmd: str,
|
|
164
|
+
job_scheduler: str,
|
|
165
|
+
store: tuple[str, ...],
|
|
166
|
+
systems: list[SimulationSystemConfig],
|
|
167
|
+
) -> Path:
|
|
168
|
+
"""Write one MD job configuration for a sampled force-field vector."""
|
|
169
|
+
config_md = {
|
|
170
|
+
"sample_id": sample_id,
|
|
171
|
+
"params": np.asarray(sample, dtype=float).tolist(),
|
|
172
|
+
"trainset_dir": str(trainset_dir),
|
|
173
|
+
"fn_specs": str(fn_specs.resolve()),
|
|
174
|
+
"gmx_cmd": gmx_cmd,
|
|
175
|
+
"job_scheduler": job_scheduler,
|
|
176
|
+
"store": list(store),
|
|
177
|
+
"systems": [system.to_dict() for system in systems],
|
|
178
|
+
}
|
|
179
|
+
fn_config_md = trainset_dir / f"config-{sample_id}.yaml"
|
|
180
|
+
save_yaml(config_md, fn_config_md)
|
|
181
|
+
return fn_config_md
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def stage_sample_topologies(
|
|
185
|
+
*,
|
|
186
|
+
sample_id: str,
|
|
187
|
+
sample: np.ndarray,
|
|
188
|
+
trainset_dir: Path,
|
|
189
|
+
fn_specs: Path,
|
|
190
|
+
systems: list[SimulationSystemConfig],
|
|
191
|
+
) -> None:
|
|
192
|
+
"""Write modified per-sample topology files into the campaign directory."""
|
|
193
|
+
for i, system in enumerate(systems):
|
|
194
|
+
fn_topol = trainset_dir / f"md-{sample_id}-{i:03d}.top"
|
|
195
|
+
modify_topology(system.fn_topol, fn_specs, sample, True, fn_topol)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def build_submission_script(
|
|
199
|
+
*,
|
|
200
|
+
sample_id: str,
|
|
201
|
+
fn_config_md: Path,
|
|
202
|
+
config: SimulationCampaignConfig,
|
|
203
|
+
) -> Slurm | None:
|
|
204
|
+
"""Build the Slurm submission script for one staged sample."""
|
|
205
|
+
if config.job_scheduler != "slurm":
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
trainset_dir = config.trainset_dir.resolve()
|
|
209
|
+
repo_root = Path(__file__).resolve().parents[2]
|
|
210
|
+
cmd_run = [sys.executable, "-m", "bff.cli", "md", str(fn_config_md)]
|
|
211
|
+
|
|
212
|
+
submit_cls = SCHEDULER_CLASSES[config.job_scheduler]
|
|
213
|
+
assert config.slurm is not None
|
|
214
|
+
fn_stdout = trainset_dir / f"run-{sample_id}.out"
|
|
215
|
+
submit_specs = dict(config.slurm.sbatch or {}) | {"output": fn_stdout}
|
|
216
|
+
submit_script = submit_cls(**submit_specs)
|
|
217
|
+
|
|
218
|
+
for cmd in config.slurm.setup:
|
|
219
|
+
submit_script.add_command(cmd)
|
|
220
|
+
pythonpath_prefix = (
|
|
221
|
+
f"PYTHONPATH={shlex.quote(str(repo_root))}"
|
|
222
|
+
'${PYTHONPATH:+":$PYTHONPATH"}'
|
|
223
|
+
)
|
|
224
|
+
submit_script.add_command(f"{pythonpath_prefix} {shlex.join(cmd_run)}")
|
|
225
|
+
for cmd in config.slurm.teardown:
|
|
226
|
+
submit_script.add_command(cmd)
|
|
227
|
+
|
|
228
|
+
return submit_script
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def write_submission_script(
|
|
232
|
+
*,
|
|
233
|
+
sample_id: str,
|
|
234
|
+
fn_config_md: Path,
|
|
235
|
+
config: SimulationCampaignConfig,
|
|
236
|
+
) -> Path | None:
|
|
237
|
+
"""Write the Slurm submission script for one staged sample."""
|
|
238
|
+
submit_script = build_submission_script(
|
|
239
|
+
sample_id=sample_id,
|
|
240
|
+
fn_config_md=fn_config_md,
|
|
241
|
+
config=config,
|
|
242
|
+
)
|
|
243
|
+
if submit_script is None:
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
fn_submit = config.trainset_dir.resolve() / f"run-{sample_id}.sh"
|
|
247
|
+
submit_script.save(fn_submit)
|
|
248
|
+
return fn_submit
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def dispatch_simulation_job(
|
|
252
|
+
*,
|
|
253
|
+
sample_id: str,
|
|
254
|
+
config: SimulationCampaignConfig,
|
|
255
|
+
) -> int | None:
|
|
256
|
+
"""Run or submit one MD job for a force-field parameter vector."""
|
|
257
|
+
trainset_dir = config.trainset_dir.resolve()
|
|
258
|
+
fn_config_md = trainset_dir / f"config-{sample_id}.yaml"
|
|
259
|
+
cmd_run = [sys.executable, "-m", "bff.cli", "md", str(fn_config_md)]
|
|
260
|
+
if config.job_scheduler == "local":
|
|
261
|
+
subprocess.run(cmd_run, cwd=str(trainset_dir), check=True)
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
submit_script = build_submission_script(
|
|
265
|
+
sample_id=sample_id,
|
|
266
|
+
fn_config_md=fn_config_md,
|
|
267
|
+
config=config,
|
|
268
|
+
)
|
|
269
|
+
assert submit_script is not None
|
|
270
|
+
fn_submit = trainset_dir / f"run-{sample_id}.sh"
|
|
271
|
+
return submit_script.submit(fn_submit)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def get_active_jobs(ids: list[int], scheduler: str, chunk_size: int = 1000) -> int:
|
|
275
|
+
"""Count active jobs for the supported scheduler."""
|
|
276
|
+
if scheduler != "slurm":
|
|
277
|
+
raise NotImplementedError
|
|
278
|
+
|
|
279
|
+
def chunks(values: list[int], n: int):
|
|
280
|
+
for i in range(0, len(values), n):
|
|
281
|
+
yield values[i:i+n]
|
|
282
|
+
|
|
283
|
+
n_active = 0
|
|
284
|
+
for chunk in chunks(ids, chunk_size):
|
|
285
|
+
ids_str = ",".join(map(str, chunk))
|
|
286
|
+
res = subprocess.run(
|
|
287
|
+
["squeue", "-j", ids_str, "--noheader", "--format", "%i,%t"],
|
|
288
|
+
capture_output=True,
|
|
289
|
+
text=True,
|
|
290
|
+
check=False,
|
|
291
|
+
)
|
|
292
|
+
n_active += res.stdout.strip().count("\n")
|
|
293
|
+
if res.stdout.strip() and not res.stdout.endswith("\n"):
|
|
294
|
+
n_active += 1
|
|
295
|
+
|
|
296
|
+
return n_active
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def wait_for_scheduler_slot(
|
|
300
|
+
*,
|
|
301
|
+
job_ids: list[int],
|
|
302
|
+
scheduler: str,
|
|
303
|
+
max_parallel_jobs: float,
|
|
304
|
+
) -> None:
|
|
305
|
+
"""Wait until the scheduler has capacity for one more submitted job."""
|
|
306
|
+
while get_active_jobs(job_ids, scheduler) >= max_parallel_jobs:
|
|
307
|
+
time.sleep(5)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def control_jobs(job_ids: list[int], scheduler: str) -> None:
|
|
311
|
+
"""Block until all submitted jobs finish."""
|
|
312
|
+
while True:
|
|
313
|
+
if get_active_jobs(job_ids, scheduler) == 0:
|
|
314
|
+
break
|
|
315
|
+
time.sleep(5)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _result_record(sample_id: str, trainset_dir: Path) -> dict[str, Any]:
|
|
319
|
+
fn_result = trainset_dir / f"result-{sample_id}.yaml"
|
|
320
|
+
if fn_result.exists():
|
|
321
|
+
return load_yaml(fn_result)
|
|
322
|
+
return {}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def collect_campaign_metadata(
|
|
326
|
+
*,
|
|
327
|
+
samples: dict[str, dict[str, Any]],
|
|
328
|
+
systems: list[SimulationSystemConfig],
|
|
329
|
+
trainset_dir: Path,
|
|
330
|
+
compress: bool = False,
|
|
331
|
+
remove: bool = False,
|
|
332
|
+
) -> None:
|
|
333
|
+
"""Merge per-sample result files into the final campaign metadata file."""
|
|
334
|
+
sample_records: dict[str, Any] = {}
|
|
335
|
+
for sample_id, sample_data in samples.items():
|
|
336
|
+
result = _result_record(sample_id, trainset_dir)
|
|
337
|
+
sample_records[sample_id] = {
|
|
338
|
+
"params": sample_data["params"],
|
|
339
|
+
"job_id": sample_data.get("job_id"),
|
|
340
|
+
"status": result.get("status", sample_data.get("status", "failed")),
|
|
341
|
+
"outputs": result.get("outputs", sample_data.get("outputs", [])),
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
save_yaml(
|
|
345
|
+
{
|
|
346
|
+
"systems": [_system_record(system, trainset_dir) for system in systems],
|
|
347
|
+
"samples": sample_records,
|
|
348
|
+
},
|
|
349
|
+
trainset_dir / "samples.yaml",
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
for fn_result in trainset_dir.glob("result-*.yaml"):
|
|
353
|
+
fn_result.unlink(missing_ok=True)
|
|
354
|
+
|
|
355
|
+
if compress:
|
|
356
|
+
compress_results(trainset_dir)
|
|
357
|
+
if remove:
|
|
358
|
+
shutil.rmtree(trainset_dir)
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
if remove:
|
|
362
|
+
patterns = ["run-*.sh", "run-*.out", "config-*.yaml"]
|
|
363
|
+
for pattern in patterns:
|
|
364
|
+
for file in trainset_dir.glob(pattern):
|
|
365
|
+
file.unlink(missing_ok=True)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def print_simulate_summary(
|
|
369
|
+
config: SimulateConfig,
|
|
370
|
+
fn_specs: PathLike,
|
|
371
|
+
logger: Logger,
|
|
372
|
+
) -> None:
|
|
373
|
+
"""Print a concise summary of the sampled simulation campaign."""
|
|
374
|
+
specs = Specs(fn_specs)
|
|
375
|
+
logger.info("", level=0)
|
|
376
|
+
logger.info("=== Running simulation campaign ===\n", level=0)
|
|
377
|
+
logger.info(f"molecule name: {specs.mol_resname}", level=1)
|
|
378
|
+
logger.info(f"trainset dir: {config.trainset_dir.resolve()}", level=1)
|
|
379
|
+
logger.info(
|
|
380
|
+
f"systems: {len(config.systems)} | samples: {config.n_samples} "
|
|
381
|
+
f"| scheduler: {config.job_scheduler}",
|
|
382
|
+
level=1,
|
|
383
|
+
)
|
|
384
|
+
logger.info(
|
|
385
|
+
f"dispatch: {'yes' if config.dispatch else 'no (stage only)'}",
|
|
386
|
+
level=1,
|
|
387
|
+
)
|
|
388
|
+
logger.info(
|
|
389
|
+
f"stored outputs: {', '.join(config.store) if config.store else 'none'}",
|
|
390
|
+
level=1,
|
|
391
|
+
)
|
|
392
|
+
logger.info("parameters:", level=1)
|
|
393
|
+
for name, bounds in specs.bounds.by_name.items():
|
|
394
|
+
label = f"{name}: {bounds}"
|
|
395
|
+
if name == specs.implicit_param:
|
|
396
|
+
label += " (implicit)"
|
|
397
|
+
logger.info(label, level=2)
|
|
398
|
+
logger.info(f"total charge: {specs.total_charge}\n", level=1)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def print_validate_summary(
|
|
402
|
+
config: SimulationCampaignConfig,
|
|
403
|
+
fn_specs: PathLike,
|
|
404
|
+
n_samples: int,
|
|
405
|
+
logger: Logger,
|
|
406
|
+
) -> None:
|
|
407
|
+
"""Print a concise summary of the validation campaign."""
|
|
408
|
+
specs = Specs(fn_specs)
|
|
409
|
+
logger.info("", level=0)
|
|
410
|
+
logger.info("=== Running validation campaign ===\n", level=0)
|
|
411
|
+
logger.info(f"molecule name: {specs.mol_resname}", level=1)
|
|
412
|
+
logger.info(f"trainset dir: {config.trainset_dir.resolve()}", level=1)
|
|
413
|
+
logger.info(
|
|
414
|
+
f"systems: {len(config.systems)} | samples: {n_samples} "
|
|
415
|
+
f"| scheduler: {config.job_scheduler}",
|
|
416
|
+
level=1,
|
|
417
|
+
)
|
|
418
|
+
logger.info(
|
|
419
|
+
f"dispatch: {'yes' if config.dispatch else 'no (stage only)'}",
|
|
420
|
+
level=1,
|
|
421
|
+
)
|
|
422
|
+
logger.info(
|
|
423
|
+
f"stored outputs: {', '.join(config.store) if config.store else 'none'}\n",
|
|
424
|
+
level=1,
|
|
425
|
+
)
|
|
426
|
+
parameter_source = getattr(config, "parameters", None)
|
|
427
|
+
if parameter_source is not None:
|
|
428
|
+
logger.info(
|
|
429
|
+
f"parameter source: {Path(parameter_source).resolve()}\n",
|
|
430
|
+
level=1,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def run_campaign(
|
|
435
|
+
*,
|
|
436
|
+
config: SimulationCampaignConfig,
|
|
437
|
+
fn_specs: Path,
|
|
438
|
+
systems: list[SimulationSystemConfig],
|
|
439
|
+
parameter_samples: np.ndarray,
|
|
440
|
+
logger: Logger,
|
|
441
|
+
) -> None:
|
|
442
|
+
"""Run a local or Slurm-backed simulation campaign."""
|
|
443
|
+
n_total = len(parameter_samples)
|
|
444
|
+
samples: dict[str, dict[str, Any]] = {}
|
|
445
|
+
job_ids: list[int] = []
|
|
446
|
+
pad = len(str(max(n_total, 1)))
|
|
447
|
+
trainset_dir = config.trainset_dir.resolve()
|
|
448
|
+
job_scheduler = config.job_scheduler
|
|
449
|
+
max_parallel_jobs = None
|
|
450
|
+
action = "Running MD" if config.dispatch else "Staging jobs"
|
|
451
|
+
campaign_finished = False
|
|
452
|
+
|
|
453
|
+
if config.dispatch and job_scheduler not in {"local", *SCHEDULER_CLASSES}:
|
|
454
|
+
raise NotImplementedError(
|
|
455
|
+
f"Unsupported scheduler '{job_scheduler}'. "
|
|
456
|
+
f"Supported: {['local', *SCHEDULER_CLASSES]}"
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
for idx, sample in enumerate(parameter_samples):
|
|
461
|
+
sample_id = f"{idx:0{pad}d}"
|
|
462
|
+
logger.info(
|
|
463
|
+
f"{action}: {idx + 1}/{n_total} "
|
|
464
|
+
f"({((idx + 1) / n_total * 100):.0f}%)",
|
|
465
|
+
level=1,
|
|
466
|
+
overwrite=True,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
sample = np.asarray(sample, dtype=float).reshape(-1)
|
|
470
|
+
fn_config_md = write_sample_job_config(
|
|
471
|
+
sample_id=sample_id,
|
|
472
|
+
sample=sample,
|
|
473
|
+
trainset_dir=trainset_dir,
|
|
474
|
+
fn_specs=fn_specs,
|
|
475
|
+
gmx_cmd=config.gmx_cmd,
|
|
476
|
+
job_scheduler=config.job_scheduler,
|
|
477
|
+
store=config.store,
|
|
478
|
+
systems=systems,
|
|
479
|
+
)
|
|
480
|
+
samples[sample_id] = {
|
|
481
|
+
"params": sample.tolist(),
|
|
482
|
+
"job_id": None,
|
|
483
|
+
"status": "staged" if not config.dispatch else "failed",
|
|
484
|
+
"outputs": [],
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if not config.dispatch:
|
|
488
|
+
stage_sample_topologies(
|
|
489
|
+
sample_id=sample_id,
|
|
490
|
+
sample=sample,
|
|
491
|
+
trainset_dir=trainset_dir,
|
|
492
|
+
fn_specs=fn_specs,
|
|
493
|
+
systems=systems,
|
|
494
|
+
)
|
|
495
|
+
write_submission_script(
|
|
496
|
+
sample_id=sample_id,
|
|
497
|
+
fn_config_md=fn_config_md,
|
|
498
|
+
config=config,
|
|
499
|
+
)
|
|
500
|
+
continue
|
|
501
|
+
|
|
502
|
+
if job_scheduler == "local":
|
|
503
|
+
dispatch_simulation_job(
|
|
504
|
+
sample_id=sample_id,
|
|
505
|
+
config=config,
|
|
506
|
+
)
|
|
507
|
+
continue
|
|
508
|
+
|
|
509
|
+
assert config.slurm is not None
|
|
510
|
+
max_parallel_jobs = config.slurm.max_parallel_jobs
|
|
511
|
+
max_parallel_jobs = np.inf if max_parallel_jobs == -1 else max_parallel_jobs
|
|
512
|
+
if max_parallel_jobs > 0:
|
|
513
|
+
wait_for_scheduler_slot(
|
|
514
|
+
job_ids=job_ids,
|
|
515
|
+
scheduler=job_scheduler,
|
|
516
|
+
max_parallel_jobs=max_parallel_jobs,
|
|
517
|
+
)
|
|
518
|
+
job_id = dispatch_simulation_job(
|
|
519
|
+
sample_id=sample_id,
|
|
520
|
+
config=config,
|
|
521
|
+
)
|
|
522
|
+
if job_id is not None:
|
|
523
|
+
job_ids.append(job_id)
|
|
524
|
+
samples[sample_id]["job_id"] = job_id
|
|
525
|
+
|
|
526
|
+
if (
|
|
527
|
+
config.dispatch
|
|
528
|
+
and job_scheduler != "local"
|
|
529
|
+
and max_parallel_jobs is not None
|
|
530
|
+
and max_parallel_jobs > 0
|
|
531
|
+
):
|
|
532
|
+
control_jobs(job_ids, job_scheduler)
|
|
533
|
+
|
|
534
|
+
campaign_finished = True
|
|
535
|
+
logger.info(f"{action}: {n_total}/{n_total} (100%) | Done.", level=1)
|
|
536
|
+
finally:
|
|
537
|
+
collect_campaign_metadata(
|
|
538
|
+
samples=samples,
|
|
539
|
+
systems=systems,
|
|
540
|
+
trainset_dir=trainset_dir,
|
|
541
|
+
compress=(
|
|
542
|
+
config.compress
|
|
543
|
+
if config.dispatch and campaign_finished
|
|
544
|
+
else False
|
|
545
|
+
),
|
|
546
|
+
remove=(
|
|
547
|
+
config.cleanup
|
|
548
|
+
if config.dispatch and campaign_finished
|
|
549
|
+
else False
|
|
550
|
+
),
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def build_parameter_samples(config: SimulateConfig) -> tuple[Path, np.ndarray]:
|
|
555
|
+
"""Build ``specs.yaml`` and sample explicit parameters for one campaign."""
|
|
556
|
+
fn_specs = build_specs(config)
|
|
557
|
+
constraint = ChargeConstraint(fn_specs)
|
|
558
|
+
sampler = RandomParamsGenerator(constraint.explicit_bounds, constraint)
|
|
559
|
+
parameter_samples = np.asarray(
|
|
560
|
+
[sampler(1).squeeze(0) for _ in range(config.n_samples)],
|
|
561
|
+
dtype=float,
|
|
562
|
+
)
|
|
563
|
+
return fn_specs, parameter_samples
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _load_yaml_parameter_samples(
|
|
567
|
+
data: dict[str, Any],
|
|
568
|
+
*,
|
|
569
|
+
specs: Specs,
|
|
570
|
+
) -> np.ndarray:
|
|
571
|
+
explicit_names = list(specs.parameter_names(explicit_only=True))
|
|
572
|
+
|
|
573
|
+
if all(name in data for name in explicit_names):
|
|
574
|
+
lengths = {len(data[name]) for name in explicit_names}
|
|
575
|
+
if len(lengths) != 1:
|
|
576
|
+
raise ValueError(
|
|
577
|
+
"Column-oriented YAML sample lists must all have the same length."
|
|
578
|
+
)
|
|
579
|
+
return np.column_stack(
|
|
580
|
+
[np.asarray(data[name], dtype=float) for name in explicit_names]
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
raise ValueError(
|
|
584
|
+
"Unsupported YAML parameter-sample format. Expected a column-oriented "
|
|
585
|
+
"mapping keyed by explicit parameter names."
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def load_parameter_samples(
|
|
590
|
+
fn_samples: PathLike,
|
|
591
|
+
fn_specs: PathLike,
|
|
592
|
+
) -> np.ndarray:
|
|
593
|
+
"""Load validation parameter samples from a column-oriented ``.yaml`` file."""
|
|
594
|
+
fn_samples = Path(fn_samples).resolve()
|
|
595
|
+
specs = Specs(fn_specs)
|
|
596
|
+
|
|
597
|
+
if fn_samples.suffix != ".yaml":
|
|
598
|
+
raise ValueError(
|
|
599
|
+
f"Unsupported sample file {fn_samples}. Expected a .yaml file."
|
|
600
|
+
)
|
|
601
|
+
raw = load_yaml(fn_samples)
|
|
602
|
+
if not isinstance(raw, dict):
|
|
603
|
+
raise ValueError(
|
|
604
|
+
f"YAML parameter sample file {fn_samples} must contain a mapping."
|
|
605
|
+
)
|
|
606
|
+
samples = _load_yaml_parameter_samples(raw, specs=specs)
|
|
607
|
+
|
|
608
|
+
if samples.ndim != 2:
|
|
609
|
+
raise ValueError(
|
|
610
|
+
f"Parameter samples must form a 2D array, got shape {samples.shape}."
|
|
611
|
+
)
|
|
612
|
+
expected = len(specs.parameter_names(explicit_only=True))
|
|
613
|
+
if samples.shape[1] != expected:
|
|
614
|
+
raise ValueError(
|
|
615
|
+
f"Expected {expected} explicit parameter values per sample, got "
|
|
616
|
+
f"{samples.shape[1]}."
|
|
617
|
+
)
|
|
618
|
+
if samples.shape[0] == 0:
|
|
619
|
+
raise ValueError("No validation parameter samples were found.")
|
|
620
|
+
return samples
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Workflow entry point for sampled training-set simulations."""
|
|
2
|
+
|
|
3
|
+
from ..io.logs import Logger
|
|
4
|
+
from .configs import SimulateConfig
|
|
5
|
+
from .runsims import (
|
|
6
|
+
build_parameter_samples,
|
|
7
|
+
print_simulate_summary,
|
|
8
|
+
run_campaign,
|
|
9
|
+
stage_campaign,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main(fn_config: str) -> None:
|
|
14
|
+
"""Run a sampled simulation campaign for training-set generation."""
|
|
15
|
+
config = SimulateConfig.load(fn_config)
|
|
16
|
+
fn_specs, parameter_samples = build_parameter_samples(config)
|
|
17
|
+
resolved_specs, systems = stage_campaign(config, fn_specs=fn_specs)
|
|
18
|
+
assert resolved_specs is not None
|
|
19
|
+
|
|
20
|
+
logger = Logger("simulate")
|
|
21
|
+
print_simulate_summary(config, resolved_specs, logger)
|
|
22
|
+
run_campaign(
|
|
23
|
+
config=config,
|
|
24
|
+
fn_specs=resolved_specs,
|
|
25
|
+
systems=systems,
|
|
26
|
+
parameter_samples=parameter_samples,
|
|
27
|
+
logger=logger,
|
|
28
|
+
)
|