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/io/__init__.py
ADDED
bff/io/colvars.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import MDAnalysis as mda
|
|
6
|
+
|
|
7
|
+
from .mdp import patch_mdp
|
|
8
|
+
|
|
9
|
+
PathLike = str | Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class ColvarsDistanceBias:
|
|
14
|
+
"""Parsed distance bias from a COLVARS file.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
name
|
|
19
|
+
Colvar name.
|
|
20
|
+
atom_indices
|
|
21
|
+
One-based atom indices of the restrained pair.
|
|
22
|
+
center
|
|
23
|
+
Optional harmonic center in nm.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
name: str
|
|
27
|
+
atom_indices: tuple[int, int]
|
|
28
|
+
center: float | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _extract_named_blocks(text: str, name: str) -> list[str]:
|
|
32
|
+
"""Extract balanced top-level blocks of one COLVARS section type."""
|
|
33
|
+
pattern = re.compile(rf"\b{name}\s*\{{", re.MULTILINE)
|
|
34
|
+
blocks: list[str] = []
|
|
35
|
+
start = 0
|
|
36
|
+
while True:
|
|
37
|
+
match = pattern.search(text, start)
|
|
38
|
+
if match is None:
|
|
39
|
+
return blocks
|
|
40
|
+
|
|
41
|
+
i = match.end() - 1
|
|
42
|
+
depth = 0
|
|
43
|
+
end = i
|
|
44
|
+
while end < len(text):
|
|
45
|
+
if text[end] == "{":
|
|
46
|
+
depth += 1
|
|
47
|
+
elif text[end] == "}":
|
|
48
|
+
depth -= 1
|
|
49
|
+
if depth == 0:
|
|
50
|
+
blocks.append(text[i + 1:end])
|
|
51
|
+
start = end + 1
|
|
52
|
+
break
|
|
53
|
+
end += 1
|
|
54
|
+
else:
|
|
55
|
+
raise ValueError(f"Unbalanced '{name}' block in COLVARS file.")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _extract_atom_numbers(block: str, group_name: str) -> tuple[int, ...]:
|
|
59
|
+
"""Extract atom numbers from one COLVARS group block."""
|
|
60
|
+
groups = _extract_named_blocks(block, group_name)
|
|
61
|
+
if not groups:
|
|
62
|
+
return ()
|
|
63
|
+
match = re.search(r"\batomNumbers\s+([0-9\s]+)", groups[0])
|
|
64
|
+
if match is None:
|
|
65
|
+
return ()
|
|
66
|
+
return tuple(int(value) for value in match.group(1).split())
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parse_distance_biases(fn_colvars: PathLike) -> list[ColvarsDistanceBias]:
|
|
70
|
+
"""Parse simple distance/harmonic restraints from a COLVARS file.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
fn_colvars
|
|
75
|
+
Path to the COLVARS input file.
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
list of ColvarsDistanceBias
|
|
80
|
+
Parsed single-pair distance colvars. Only colvars with one atom in
|
|
81
|
+
``group1`` and one atom in ``group2`` are returned.
|
|
82
|
+
"""
|
|
83
|
+
text = Path(fn_colvars).read_text()
|
|
84
|
+
|
|
85
|
+
colvars: dict[str, tuple[int, int]] = {}
|
|
86
|
+
for block in _extract_named_blocks(text, "colvar"):
|
|
87
|
+
name_match = re.search(r"^\s*name\s+(\S+)", block, re.MULTILINE)
|
|
88
|
+
if name_match is None:
|
|
89
|
+
continue
|
|
90
|
+
distance_blocks = _extract_named_blocks(block, "distance")
|
|
91
|
+
if not distance_blocks:
|
|
92
|
+
continue
|
|
93
|
+
group_1 = _extract_atom_numbers(distance_blocks[0], "group1")
|
|
94
|
+
group_2 = _extract_atom_numbers(distance_blocks[0], "group2")
|
|
95
|
+
if len(group_1) != 1 or len(group_2) != 1:
|
|
96
|
+
continue
|
|
97
|
+
colvars[name_match.group(1)] = (group_1[0], group_2[0])
|
|
98
|
+
|
|
99
|
+
centers: dict[str, float] = {}
|
|
100
|
+
for block in _extract_named_blocks(text, "harmonic"):
|
|
101
|
+
name_match = re.search(r"^\s*colvars\s+(\S+)", block, re.MULTILINE)
|
|
102
|
+
center_match = re.search(r"^\s*centers\s+([-+0-9.eE]+)", block, re.MULTILINE)
|
|
103
|
+
if name_match is None or center_match is None:
|
|
104
|
+
continue
|
|
105
|
+
centers[name_match.group(1)] = float(center_match.group(1))
|
|
106
|
+
|
|
107
|
+
return [
|
|
108
|
+
ColvarsDistanceBias(
|
|
109
|
+
name=name,
|
|
110
|
+
atom_indices=indices,
|
|
111
|
+
center=centers.get(name),
|
|
112
|
+
)
|
|
113
|
+
for name, indices in colvars.items()
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def resolve_distance_bias_metadata(
|
|
118
|
+
fn_colvars: PathLike,
|
|
119
|
+
fn_system: PathLike,
|
|
120
|
+
) -> tuple[tuple[str, ...], tuple[float, ...]]:
|
|
121
|
+
"""Resolve atom-name pairs and centers from a COLVARS file.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
fn_colvars
|
|
126
|
+
Path to the COLVARS input file.
|
|
127
|
+
fn_system
|
|
128
|
+
Structure file used to map atom indices to atom names.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
tuple
|
|
133
|
+
Tuple of ``(pair_labels, centers)`` where labels are atom-name pairs
|
|
134
|
+
such as ``"C2 CAL"`` and centers are harmonic centers in nm.
|
|
135
|
+
"""
|
|
136
|
+
universe = mda.Universe(str(fn_system))
|
|
137
|
+
distances = parse_distance_biases(fn_colvars)
|
|
138
|
+
|
|
139
|
+
labels: list[str] = []
|
|
140
|
+
centers: list[float] = []
|
|
141
|
+
for distance in distances:
|
|
142
|
+
index_1, index_2 = distance.atom_indices
|
|
143
|
+
atom_1 = universe.atoms[index_1 - 1].name
|
|
144
|
+
atom_2 = universe.atoms[index_2 - 1].name
|
|
145
|
+
labels.append(f"{atom_1} {atom_2}")
|
|
146
|
+
if distance.center is not None:
|
|
147
|
+
centers.append(float(distance.center))
|
|
148
|
+
|
|
149
|
+
return tuple(labels), tuple(centers)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def write_mdp_with_colvars(
|
|
153
|
+
fn_mdp: PathLike,
|
|
154
|
+
fn_colvars: PathLike,
|
|
155
|
+
fn_out: PathLike,
|
|
156
|
+
*,
|
|
157
|
+
seed: int | None = None,
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Write an MDP file with GROMACS Colvars enabled.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
fn_mdp
|
|
164
|
+
Input MDP file.
|
|
165
|
+
fn_colvars
|
|
166
|
+
Colvars configuration file referenced from the generated MDP.
|
|
167
|
+
fn_out
|
|
168
|
+
Output MDP file with Colvars options injected.
|
|
169
|
+
seed
|
|
170
|
+
Optional seed passed to the Colvars module.
|
|
171
|
+
"""
|
|
172
|
+
updates = {
|
|
173
|
+
"colvars-active": "yes",
|
|
174
|
+
"colvars-configfile": f"./{Path(fn_colvars).name}",
|
|
175
|
+
}
|
|
176
|
+
if seed is not None:
|
|
177
|
+
updates["colvars-seed"] = str(int(seed))
|
|
178
|
+
patch_mdp(fn_mdp, updates, fn_out)
|
|
179
|
+
with open(fn_out, "r+") as handle:
|
|
180
|
+
content = handle.read()
|
|
181
|
+
block = "\n; colvars\n"
|
|
182
|
+
if "colvars-active" in content:
|
|
183
|
+
content = content.replace("colvars-active", block + "colvars-active", 1)
|
|
184
|
+
handle.seek(0)
|
|
185
|
+
handle.write(content)
|
|
186
|
+
handle.truncate()
|
bff/io/commands.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def split_command(command: str | Sequence[str]) -> list[str]:
|
|
7
|
+
"""Normalize a shell-style command into a subprocess argument vector."""
|
|
8
|
+
if isinstance(command, str):
|
|
9
|
+
parts = shlex.split(command)
|
|
10
|
+
else:
|
|
11
|
+
parts = [str(part) for part in command]
|
|
12
|
+
|
|
13
|
+
if not parts:
|
|
14
|
+
raise ValueError("Command must not be empty.")
|
|
15
|
+
return parts
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_command(command: str | Sequence[str], *args: str | Path) -> list[str]:
|
|
19
|
+
"""Append stringified arguments to a normalized command vector."""
|
|
20
|
+
return [*split_command(command), *(str(arg) for arg in args)]
|
bff/io/cp2k.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import MDAnalysis as mda
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from ..data import CATIONS, CP2K_INPUT_TEMPLATE, CP2K_KIND_DEFAULTS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def format_cp2k_input(data: dict[str, Any], indent: int = 0) -> list[str]:
|
|
12
|
+
"""Format a CP2K input tree as text.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
data
|
|
17
|
+
Nested mapping representing the CP2K input tree.
|
|
18
|
+
indent
|
|
19
|
+
Current indentation level.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
list of str
|
|
24
|
+
Formatted CP2K input lines.
|
|
25
|
+
"""
|
|
26
|
+
lines: list[str] = []
|
|
27
|
+
prefix = " " * indent
|
|
28
|
+
|
|
29
|
+
for key, value in data.items():
|
|
30
|
+
section_name = key.upper()
|
|
31
|
+
section_end = key.split()[0].upper()
|
|
32
|
+
|
|
33
|
+
if isinstance(value, dict):
|
|
34
|
+
lines.append(f"{prefix}&{section_name}")
|
|
35
|
+
lines.extend(format_cp2k_input(value, indent + 2))
|
|
36
|
+
lines.append(f"{prefix}&END {section_end}")
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
if isinstance(value, list):
|
|
40
|
+
for item in value:
|
|
41
|
+
if isinstance(item, dict):
|
|
42
|
+
lines.extend(format_cp2k_input({key: item}, indent))
|
|
43
|
+
elif isinstance(item, list):
|
|
44
|
+
lines.append(f"{prefix}{section_name} {' '.join(map(str, item))}")
|
|
45
|
+
else:
|
|
46
|
+
lines.append(f"{prefix}{section_name} {item}")
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
if isinstance(value, bool):
|
|
50
|
+
value = ".TRUE." if value else ".FALSE."
|
|
51
|
+
|
|
52
|
+
lines.append(f"{prefix}{section_name} {value}")
|
|
53
|
+
|
|
54
|
+
return lines
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_cp2k_elements(fn_pos: str | Path) -> np.ndarray:
|
|
58
|
+
"""Return element symbols in coordinate-file order without duplicates."""
|
|
59
|
+
universe = mda.Universe(fn_pos)
|
|
60
|
+
elements, index = np.unique(universe.atoms.elements, return_index=True)
|
|
61
|
+
return elements[np.argsort(index)]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def build_cp2k_kinds(elements: np.ndarray) -> dict[str, dict[str, str]]:
|
|
65
|
+
"""Build CP2K KIND sections for the elements present in a system."""
|
|
66
|
+
return {
|
|
67
|
+
f"kind {element.capitalize()}": CP2K_KIND_DEFAULTS[element.lower()]
|
|
68
|
+
for element in elements
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def build_cp2k_d3_exclusions(elements: np.ndarray) -> list[list[int]]:
|
|
73
|
+
"""Build D3 kind-pair exclusions for cation-containing systems."""
|
|
74
|
+
return [
|
|
75
|
+
[i + 1, j + 1]
|
|
76
|
+
for i, element in enumerate(elements)
|
|
77
|
+
if element.lower() in CATIONS
|
|
78
|
+
for j in range(len(elements))
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def configure_cp2k_restart(
|
|
83
|
+
tree: dict[str, Any],
|
|
84
|
+
*,
|
|
85
|
+
project: str,
|
|
86
|
+
restart: bool,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Configure CP2K SCF and restart sections."""
|
|
89
|
+
if restart:
|
|
90
|
+
tree["ext_restart"] = {"restart_file_name": f"{project}-1.restart"}
|
|
91
|
+
tree["force_eval"]["dft"]["scf"]["scf_guess"] = "RESTART"
|
|
92
|
+
tree["force_eval"]["dft"]["wfn_restart_file_name"] = (
|
|
93
|
+
f"{project}-RESTART.wfn"
|
|
94
|
+
)
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
tree["force_eval"]["dft"]["scf"]["scf_guess"] = "ATOMIC"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def configure_cp2k_ensemble(
|
|
101
|
+
tree: dict[str, Any],
|
|
102
|
+
*,
|
|
103
|
+
equilibration: bool,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Configure the MD ensemble for equilibration or production."""
|
|
106
|
+
tree["motion"]["md"]["ensemble"] = "LANGEVIN" if equilibration else "NVT"
|
|
107
|
+
if equilibration:
|
|
108
|
+
tree["motion"]["md"]["langevin"] = {"gamma": 0.02}
|
|
109
|
+
tree["motion"]["md"].pop("thermostat", None)
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
tree["motion"]["md"]["thermostat"] = {
|
|
113
|
+
"region": "GLOBAL",
|
|
114
|
+
"type": "CSVR",
|
|
115
|
+
"csvr": {"timecon": "[fs] 100"},
|
|
116
|
+
}
|
|
117
|
+
tree["motion"]["md"].pop("langevin", None)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def configure_cp2k_plumed(
|
|
121
|
+
tree: dict[str, Any],
|
|
122
|
+
*,
|
|
123
|
+
plumed_input_file: str | Path,
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Configure CP2K to use a PLUMED input file for biased MD.
|
|
126
|
+
|
|
127
|
+
Parameters
|
|
128
|
+
----------
|
|
129
|
+
tree
|
|
130
|
+
CP2K input tree to update.
|
|
131
|
+
plumed_input_file
|
|
132
|
+
PLUMED input filename referenced from the CP2K input.
|
|
133
|
+
"""
|
|
134
|
+
tree["motion"]["free_energy"] = {
|
|
135
|
+
"metadyn": {
|
|
136
|
+
"use_plumed": True,
|
|
137
|
+
"plumed_input_file": Path(plumed_input_file).name,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def configure_cp2k_single_point(tree: dict[str, Any]) -> None:
|
|
143
|
+
"""Configure a CP2K input tree for single-point energy/forces."""
|
|
144
|
+
tree["global"]["run_type"] = "ENERGY_FORCE"
|
|
145
|
+
tree.pop("motion", None)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def build_cp2k_tree(
|
|
149
|
+
*,
|
|
150
|
+
project: str,
|
|
151
|
+
charge: int,
|
|
152
|
+
multiplicity: int,
|
|
153
|
+
unitcell: list[float],
|
|
154
|
+
fn_pos: str | Path,
|
|
155
|
+
) -> dict[str, Any]:
|
|
156
|
+
"""Build the common CP2K input tree for one structure."""
|
|
157
|
+
fn_pos = Path(fn_pos)
|
|
158
|
+
elements = get_cp2k_elements(fn_pos)
|
|
159
|
+
tree = deepcopy(CP2K_INPUT_TEMPLATE)
|
|
160
|
+
|
|
161
|
+
tree["global"]["project_name"] = project
|
|
162
|
+
tree["force_eval"]["subsys"]["topology"] = {
|
|
163
|
+
"coord_file_name": fn_pos.name,
|
|
164
|
+
"coord_file_format": "XYZ",
|
|
165
|
+
}
|
|
166
|
+
tree["force_eval"]["subsys"]["cell"] = {
|
|
167
|
+
"ABC": f"[angstrom] {unitcell[0]} {unitcell[1]} {unitcell[2]}",
|
|
168
|
+
"periodic": "XYZ",
|
|
169
|
+
}
|
|
170
|
+
tree["force_eval"]["subsys"].update(build_cp2k_kinds(elements))
|
|
171
|
+
|
|
172
|
+
tree["force_eval"]["dft"]["charge"] = charge
|
|
173
|
+
tree["force_eval"]["dft"]["multiplicity"] = multiplicity
|
|
174
|
+
tree["force_eval"]["dft"]["uks"] = multiplicity != 1
|
|
175
|
+
|
|
176
|
+
if any(element.lower() in CATIONS for element in elements):
|
|
177
|
+
tree["force_eval"]["dft"]["xc"]["vdw_potential"]["pair_potential"][
|
|
178
|
+
"d3_exclude_kind_pair"
|
|
179
|
+
] = build_cp2k_d3_exclusions(elements)
|
|
180
|
+
|
|
181
|
+
return tree
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def make_cp2k_input(
|
|
185
|
+
project: str,
|
|
186
|
+
charge: int,
|
|
187
|
+
multiplicity: int,
|
|
188
|
+
unitcell: list[float],
|
|
189
|
+
fn_pos: str | Path,
|
|
190
|
+
equilibration: bool,
|
|
191
|
+
restart: bool,
|
|
192
|
+
fn_out: str | Path,
|
|
193
|
+
plumed_input_file: str | Path | None = None,
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Generate a CP2K input file for one simulation window.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
project
|
|
200
|
+
Base project name.
|
|
201
|
+
charge
|
|
202
|
+
Total charge of the system.
|
|
203
|
+
multiplicity
|
|
204
|
+
Spin multiplicity.
|
|
205
|
+
unitcell
|
|
206
|
+
Unit-cell dimensions in Angstrom.
|
|
207
|
+
fn_pos
|
|
208
|
+
Path to the XYZ coordinate file.
|
|
209
|
+
equilibration
|
|
210
|
+
Whether to generate an equilibration input.
|
|
211
|
+
restart
|
|
212
|
+
Whether the CP2K run restarts from a previous step.
|
|
213
|
+
fn_out
|
|
214
|
+
Output CP2K input file.
|
|
215
|
+
plumed_input_file
|
|
216
|
+
Optional PLUMED input file used for biased CP2K MD.
|
|
217
|
+
"""
|
|
218
|
+
tree = build_cp2k_tree(
|
|
219
|
+
project=project,
|
|
220
|
+
charge=charge,
|
|
221
|
+
multiplicity=multiplicity,
|
|
222
|
+
unitcell=unitcell,
|
|
223
|
+
fn_pos=fn_pos,
|
|
224
|
+
)
|
|
225
|
+
configure_cp2k_restart(tree, project=project, restart=restart)
|
|
226
|
+
configure_cp2k_ensemble(tree, equilibration=equilibration)
|
|
227
|
+
if plumed_input_file is not None:
|
|
228
|
+
configure_cp2k_plumed(tree, plumed_input_file=plumed_input_file)
|
|
229
|
+
|
|
230
|
+
with open(fn_out, "w") as handle:
|
|
231
|
+
handle.write("\n".join(format_cp2k_input(tree)) + "\n")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def make_cp2k_single_point_input(
|
|
235
|
+
project: str,
|
|
236
|
+
charge: int,
|
|
237
|
+
multiplicity: int,
|
|
238
|
+
unitcell: list[float],
|
|
239
|
+
fn_pos: str | Path,
|
|
240
|
+
fn_out: str | Path,
|
|
241
|
+
) -> None:
|
|
242
|
+
"""Generate a CP2K single-point input file."""
|
|
243
|
+
tree = build_cp2k_tree(
|
|
244
|
+
project=project,
|
|
245
|
+
charge=charge,
|
|
246
|
+
multiplicity=multiplicity,
|
|
247
|
+
unitcell=unitcell,
|
|
248
|
+
fn_pos=fn_pos,
|
|
249
|
+
)
|
|
250
|
+
configure_cp2k_single_point(tree)
|
|
251
|
+
tree["global"]["print_level"] = "MEDIUM"
|
|
252
|
+
|
|
253
|
+
with open(fn_out, "w") as handle:
|
|
254
|
+
handle.write("\n".join(format_cp2k_input(tree)) + "\n")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def write_cp2k_md_slurm_script(
|
|
258
|
+
fn_out: str | Path,
|
|
259
|
+
*,
|
|
260
|
+
uses_plumed: bool = False,
|
|
261
|
+
) -> None:
|
|
262
|
+
"""Write a restart-aware Slurm script for CP2K MD."""
|
|
263
|
+
plumed_block = ""
|
|
264
|
+
if uses_plumed:
|
|
265
|
+
plumed_block = """
|
|
266
|
+
if [ -f plumed.dat ]; then
|
|
267
|
+
echo "Using PLUMED input: plumed.dat"
|
|
268
|
+
fi
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
script = f"""#!/bin/bash
|
|
272
|
+
#SBATCH --job-name=cp2k-md
|
|
273
|
+
#SBATCH --nodes=1
|
|
274
|
+
#SBATCH --ntasks-per-node=8
|
|
275
|
+
#SBATCH --time=24:00:00
|
|
276
|
+
#SBATCH --output=slurm-%j.out
|
|
277
|
+
|
|
278
|
+
set -euo pipefail
|
|
279
|
+
|
|
280
|
+
CP2K_CMD="${{CP2K_CMD:-cp2k.psmp}}"
|
|
281
|
+
{plumed_block}
|
|
282
|
+
|
|
283
|
+
if [ ! -f .md-eq-start.done ]; then
|
|
284
|
+
${{CP2K_CMD}} -i md-eq-start.inp -o md-eq-start.out
|
|
285
|
+
touch .md-eq-start.done
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
if [ ! -f .md-eq-restart.done ]; then
|
|
289
|
+
${{CP2K_CMD}} -i md-eq-restart.inp -o md-eq-restart.out
|
|
290
|
+
touch .md-eq-restart.done
|
|
291
|
+
fi
|
|
292
|
+
|
|
293
|
+
${{CP2K_CMD}} -i md-prod.inp -o md-prod.out
|
|
294
|
+
"""
|
|
295
|
+
Path(fn_out).write_text(script)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def write_cp2k_single_point_slurm_script(
|
|
299
|
+
fn_out: str | Path,
|
|
300
|
+
*,
|
|
301
|
+
snapshots_dirname: str = "snapshots",
|
|
302
|
+
) -> None:
|
|
303
|
+
"""Write a Slurm script for batched CP2K single-point evaluations."""
|
|
304
|
+
script = f"""#!/bin/bash
|
|
305
|
+
#SBATCH --job-name=cp2k-sp
|
|
306
|
+
#SBATCH --nodes=1
|
|
307
|
+
#SBATCH --ntasks-per-node=8
|
|
308
|
+
#SBATCH --time=24:00:00
|
|
309
|
+
#SBATCH --output=slurm-%j.out
|
|
310
|
+
|
|
311
|
+
set -euo pipefail
|
|
312
|
+
|
|
313
|
+
CP2K_CMD="${{CP2K_CMD:-cp2k.psmp}}"
|
|
314
|
+
SNAPSHOT_DIR="{snapshots_dirname}"
|
|
315
|
+
INPUT_TEMPLATE="single-point.inp"
|
|
316
|
+
|
|
317
|
+
for xyz in "${{SNAPSHOT_DIR}}"/snapshot-*.xyz; do
|
|
318
|
+
[ -e "${{xyz}}" ] || continue
|
|
319
|
+
stem=$(basename "${{xyz}}" .xyz)
|
|
320
|
+
run_dir="runs/${{stem}}"
|
|
321
|
+
mkdir -p "${{run_dir}}"
|
|
322
|
+
cp "${{INPUT_TEMPLATE}}" "${{run_dir}}/single-point.inp"
|
|
323
|
+
cp "${{xyz}}" "${{run_dir}}/snapshot.xyz"
|
|
324
|
+
(
|
|
325
|
+
cd "${{run_dir}}"
|
|
326
|
+
${{CP2K_CMD}} -i single-point.inp -o single-point.out
|
|
327
|
+
)
|
|
328
|
+
done
|
|
329
|
+
"""
|
|
330
|
+
Path(fn_out).write_text(script)
|