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/bayes/utils.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Callable, Sequence, Tuple, Union
|
|
3
|
+
|
|
4
|
+
import torch
|
|
5
|
+
from torch.autograd.functional import hessian
|
|
6
|
+
|
|
7
|
+
PathLike = Union[str, Path]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def smape(y_true: torch.Tensor, y_pred: torch.Tensor) -> float:
|
|
11
|
+
"""Compute the symmetric mean absolute percentage error."""
|
|
12
|
+
|
|
13
|
+
device = y_true.device if isinstance(y_true, torch.Tensor) else 'cpu'
|
|
14
|
+
|
|
15
|
+
y_true = check_tensor(y_true, device=device)
|
|
16
|
+
y_pred = check_tensor(y_pred, device=device)
|
|
17
|
+
|
|
18
|
+
abs_diff = torch.sum(torch.abs(y_true - y_pred), dim=1)
|
|
19
|
+
y_true_abs = torch.sum(torch.abs(y_true), dim=1)
|
|
20
|
+
y_pred_abs = torch.sum(torch.abs(y_pred), dim=1)
|
|
21
|
+
norm = y_true_abs + y_pred_abs
|
|
22
|
+
|
|
23
|
+
return float(torch.mean(abs_diff / norm).item())
|
|
24
|
+
|
|
25
|
+
def initialize_walkers(
|
|
26
|
+
priors,
|
|
27
|
+
n_walkers: int,
|
|
28
|
+
constraint: Callable = None,
|
|
29
|
+
max_attempts: int | None = None,
|
|
30
|
+
) -> torch.Tensor:
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
Initialize walkers for the MCMC sampler.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
priors : dict
|
|
38
|
+
Dict of prior distributions for each parameter.
|
|
39
|
+
n_walkers : int
|
|
40
|
+
Number of walkers to initialize.
|
|
41
|
+
specs : object, optional
|
|
42
|
+
Specifications object containing bounds and constraints.
|
|
43
|
+
If provided, walkers will be initialized within the bounds.
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
torch.Tensor
|
|
48
|
+
A tensor of shape (n_walkers, n_params) containing the initial positions
|
|
49
|
+
of the walkers, sampled from the prior distributions.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
if constraint is None:
|
|
53
|
+
means = torch.tensor([p.mean for p in priors], dtype=torch.float32)
|
|
54
|
+
stds = torch.tensor([p.scale for p in priors], dtype=torch.float32)
|
|
55
|
+
p0 = torch.normal(means.expand(n_walkers, -1), stds.expand(n_walkers, -1))
|
|
56
|
+
else:
|
|
57
|
+
n_params = constraint.n_params
|
|
58
|
+
n_dim = len(priors)
|
|
59
|
+
max_attempts = max_attempts or (1000 * n_walkers)
|
|
60
|
+
|
|
61
|
+
p0 = torch.empty((n_walkers, n_dim))
|
|
62
|
+
count = 0
|
|
63
|
+
attempts = 0
|
|
64
|
+
while count < n_walkers and attempts < max_attempts:
|
|
65
|
+
p0_trial = torch.tensor([p.sample().item() for p in priors])
|
|
66
|
+
if constraint(p0_trial[:n_params]):
|
|
67
|
+
p0[count] = p0_trial
|
|
68
|
+
count += 1
|
|
69
|
+
attempts += 1
|
|
70
|
+
if count < n_walkers:
|
|
71
|
+
raise RuntimeError(
|
|
72
|
+
"Failed to initialize constrained walkers from the priors. "
|
|
73
|
+
f"Accepted {count}/{n_walkers} samples after {attempts} attempts."
|
|
74
|
+
)
|
|
75
|
+
return p0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def check_tensor(
|
|
79
|
+
x: Union[torch.Tensor, float, int, list, tuple],
|
|
80
|
+
device: str,
|
|
81
|
+
dtype: torch.dtype = torch.float32
|
|
82
|
+
) -> torch.Tensor:
|
|
83
|
+
"""Convert input to a torch tensor on the specified device."""
|
|
84
|
+
if not isinstance(x, torch.Tensor):
|
|
85
|
+
return torch.as_tensor(x, device=device, dtype=dtype)
|
|
86
|
+
return x.to(device, dtype=dtype)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def check_device(device: str) -> None:
|
|
90
|
+
"""Check if the specified device is available."""
|
|
91
|
+
if device.startswith("cuda"):
|
|
92
|
+
if not torch.cuda.is_available():
|
|
93
|
+
raise RuntimeError("CUDA is not available.")
|
|
94
|
+
elif device.startswith("mps"):
|
|
95
|
+
if not torch.mps.is_available():
|
|
96
|
+
raise RuntimeError("MPS is not available.")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@torch.no_grad()
|
|
100
|
+
def nearest_positive_definite(A: torch.Tensor) -> torch.Tensor:
|
|
101
|
+
"""Find the nearest positive definite matrix to A."""
|
|
102
|
+
# Symmetrize
|
|
103
|
+
A_sym = (A + A.T) / 2
|
|
104
|
+
|
|
105
|
+
# Check if already PD
|
|
106
|
+
if torch.all(torch.linalg.eigvalsh(A_sym) > 0):
|
|
107
|
+
return A_sym
|
|
108
|
+
|
|
109
|
+
# Eigen-decomposition
|
|
110
|
+
eigenvalues, eigenvectors = torch.linalg.eigh(A_sym)
|
|
111
|
+
|
|
112
|
+
# Shift eigenvalues minimally
|
|
113
|
+
min_eig = eigenvalues.min()
|
|
114
|
+
eps = 1e-8 # small positive shift
|
|
115
|
+
|
|
116
|
+
if min_eig < eps:
|
|
117
|
+
eigenvalues = eigenvalues - min_eig + eps
|
|
118
|
+
|
|
119
|
+
A_pd = (eigenvectors @ torch.diag(eigenvalues)) @ eigenvectors.T
|
|
120
|
+
return A_pd
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def train_test_split(
|
|
124
|
+
X: torch.Tensor, y: torch.Tensor, test_fraction: float = 0.2
|
|
125
|
+
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
126
|
+
"""Split the dataset into training and testing sets."""
|
|
127
|
+
|
|
128
|
+
n = len(X)
|
|
129
|
+
if n != len(y):
|
|
130
|
+
raise ValueError("X and y must have the same length.")
|
|
131
|
+
if not (0 < test_fraction < 1):
|
|
132
|
+
raise ValueError("test_fraction must be between 0 and 1.")
|
|
133
|
+
if n < 2:
|
|
134
|
+
raise ValueError("X and y must have at least 2 samples.")
|
|
135
|
+
|
|
136
|
+
indices = torch.randperm(n)
|
|
137
|
+
test_size = int(n * test_fraction)
|
|
138
|
+
idx_train = indices[test_size:]
|
|
139
|
+
idx_test = indices[:test_size]
|
|
140
|
+
return X[idx_train], X[idx_test], y[idx_train], y[idx_test]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# Global toggle for manual squared distance
|
|
144
|
+
_MANUAL_MODE = False
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class enable_manual_dist:
|
|
148
|
+
"""Enable Hessian-safe manual pairwise distances within a context."""
|
|
149
|
+
def __enter__(self) -> None:
|
|
150
|
+
global _MANUAL_MODE
|
|
151
|
+
self._prev = _MANUAL_MODE
|
|
152
|
+
_MANUAL_MODE = True
|
|
153
|
+
|
|
154
|
+
def __exit__(self, *args) -> None:
|
|
155
|
+
global _MANUAL_MODE
|
|
156
|
+
_MANUAL_MODE = self._prev
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def with_manual_sqdist_flag(fn: Callable) -> Callable:
|
|
160
|
+
"""Inject the global manual-distance flag into kernel call sites."""
|
|
161
|
+
def wrapper(*args, manual_sqdist=False, **kwargs):
|
|
162
|
+
return fn(*args, manual_sqdist=manual_sqdist or _MANUAL_MODE, **kwargs)
|
|
163
|
+
return wrapper
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def find_max_stable_lr(
|
|
167
|
+
fn: Callable,
|
|
168
|
+
p0: torch.Tensor,
|
|
169
|
+
learning_rates: (
|
|
170
|
+
Sequence[Union[float, torch.Tensor]] | float | torch.Tensor | None
|
|
171
|
+
) = None,
|
|
172
|
+
max_iter: int = 100,
|
|
173
|
+
param_bounds: Tuple[float, float] = (-7, 7),
|
|
174
|
+
) -> Union[float, None]:
|
|
175
|
+
"""Find the largest stable learning rate for gradient-based optimization.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
fn : Callable
|
|
180
|
+
Objective function returning a scalar tensor.
|
|
181
|
+
p0 : torch.Tensor
|
|
182
|
+
Initial parameter vector.
|
|
183
|
+
learning_rates : iterable of float, optional
|
|
184
|
+
Learning rates to test. Defaults to log-spaced values.
|
|
185
|
+
max_iter : int
|
|
186
|
+
Number of steps to test for each learning rate.
|
|
187
|
+
param_bounds : tuple of float
|
|
188
|
+
Bounds beyond which parameters are considered unstable.
|
|
189
|
+
|
|
190
|
+
Returns
|
|
191
|
+
-------
|
|
192
|
+
float or None
|
|
193
|
+
The largest stable learning rate found, or None if none were stable.
|
|
194
|
+
"""
|
|
195
|
+
if learning_rates is None:
|
|
196
|
+
learning_rates = 10 ** torch.linspace(-1, -6, 6)
|
|
197
|
+
elif isinstance(learning_rates, torch.Tensor) and learning_rates.ndim == 0:
|
|
198
|
+
learning_rates = [float(learning_rates.item())]
|
|
199
|
+
elif isinstance(learning_rates, (int, float)):
|
|
200
|
+
learning_rates = [float(learning_rates)]
|
|
201
|
+
else:
|
|
202
|
+
learning_rates = list(learning_rates)
|
|
203
|
+
lower, upper = param_bounds
|
|
204
|
+
for lr in learning_rates:
|
|
205
|
+
x = p0.clone().detach().requires_grad_(True)
|
|
206
|
+
opt = torch.optim.SGD([x], lr=lr)
|
|
207
|
+
|
|
208
|
+
for i in range(max_iter):
|
|
209
|
+
if torch.any(x < lower) or torch.any(x > upper):
|
|
210
|
+
break
|
|
211
|
+
opt.zero_grad()
|
|
212
|
+
loss = -fn(x)
|
|
213
|
+
loss.backward()
|
|
214
|
+
opt.step()
|
|
215
|
+
else:
|
|
216
|
+
# Only gets executed if inner loop did not break (i.e., stable)
|
|
217
|
+
return lr
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def find_map(
|
|
221
|
+
fn: Callable,
|
|
222
|
+
x0: torch.Tensor,
|
|
223
|
+
lr: Union[float, torch.Tensor] = None,
|
|
224
|
+
max_iter: int = 10000,
|
|
225
|
+
tol_grad: float = 1e-2,
|
|
226
|
+
device: str = 'cpu',
|
|
227
|
+
logger: Callable = None
|
|
228
|
+
) -> torch.Tensor:
|
|
229
|
+
|
|
230
|
+
"""Find the maximum a posteriori (MAP) estimate
|
|
231
|
+
using gradient-based optimization.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
fn : Callable
|
|
236
|
+
Objective function to maximize (log-posterior).
|
|
237
|
+
x0 : torch.Tensor
|
|
238
|
+
Initial parameter vector.
|
|
239
|
+
lr : float or torch.Tensor, optional
|
|
240
|
+
Learning rate for optimization. If None, it will be determined by a search.
|
|
241
|
+
max_iter : int
|
|
242
|
+
Maximum number of optimization iterations.
|
|
243
|
+
tol_grad : float
|
|
244
|
+
Gradient norm threshold for convergence.
|
|
245
|
+
device : str
|
|
246
|
+
Device to perform computations on.
|
|
247
|
+
logger : Callable, optional
|
|
248
|
+
Logger for progress updates. Should accept a string and a level argument.
|
|
249
|
+
|
|
250
|
+
Returns
|
|
251
|
+
-------
|
|
252
|
+
torch.Tensor
|
|
253
|
+
The MAP estimate found by optimization.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
if logger is not None:
|
|
257
|
+
logger.info("learning rate search: in progress...", level=2, overwrite=True)
|
|
258
|
+
|
|
259
|
+
lr_opt = find_max_stable_lr(fn, x0, learning_rates=lr)
|
|
260
|
+
if lr_opt is not None:
|
|
261
|
+
lr_opt = 0.5 * lr_opt
|
|
262
|
+
if logger is not None:
|
|
263
|
+
logger.info(f"learning rate search: Done. | lr = {lr_opt:.1e}", level=2)
|
|
264
|
+
else:
|
|
265
|
+
raise ValueError("No stable learning rate found.")
|
|
266
|
+
|
|
267
|
+
x0 = x0.clone().detach().to(device).requires_grad_(True)
|
|
268
|
+
optimizer = torch.optim.SGD([x0], lr=lr_opt)
|
|
269
|
+
|
|
270
|
+
for i in range(max_iter):
|
|
271
|
+
optimizer.zero_grad()
|
|
272
|
+
loss = -fn(x0)
|
|
273
|
+
loss.backward()
|
|
274
|
+
grad_norm = x0.grad.norm().item()
|
|
275
|
+
if i % 100 == 0 and logger is not None:
|
|
276
|
+
logger.info(
|
|
277
|
+
(
|
|
278
|
+
f"MAP search: it. {i}/{max_iter} | "
|
|
279
|
+
f"loss: {loss.item():.3f} | grad: {grad_norm:.3f}/{tol_grad}"
|
|
280
|
+
),
|
|
281
|
+
level=2,
|
|
282
|
+
overwrite=True
|
|
283
|
+
)
|
|
284
|
+
optimizer.step()
|
|
285
|
+
if grad_norm < tol_grad:
|
|
286
|
+
if logger is not None:
|
|
287
|
+
logger.info("MAP search: Done.", level=2)
|
|
288
|
+
break
|
|
289
|
+
|
|
290
|
+
else:
|
|
291
|
+
if logger is not None:
|
|
292
|
+
logger.info(
|
|
293
|
+
"MAP search: Fail. | Max iterations reached without convergence.",
|
|
294
|
+
level=2,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
return x0.detach()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def laplace_approximation(
|
|
301
|
+
fn: Callable,
|
|
302
|
+
map_theta: torch.Tensor,
|
|
303
|
+
device: str = 'cpu'
|
|
304
|
+
) -> torch.Tensor:
|
|
305
|
+
"""
|
|
306
|
+
Perform Laplace approximation around MAP estimate.
|
|
307
|
+
|
|
308
|
+
Parameters
|
|
309
|
+
----------
|
|
310
|
+
fn : Callable
|
|
311
|
+
Objective function to approximate (log-posterior).
|
|
312
|
+
map_theta : torch.Tensor
|
|
313
|
+
MAP estimate around which to perform the approximation.
|
|
314
|
+
device : str
|
|
315
|
+
Device to perform computations on.
|
|
316
|
+
|
|
317
|
+
Returns
|
|
318
|
+
-------
|
|
319
|
+
cov : torch.Tensor
|
|
320
|
+
The approximate posterior covariance (inverse Hessian).
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
map_theta = check_tensor(map_theta, device=device)
|
|
324
|
+
with enable_manual_dist():
|
|
325
|
+
H = -hessian(lambda th: fn(th).sum(), map_theta)
|
|
326
|
+
|
|
327
|
+
reg_eye = 1e-6 * torch.eye(H.shape[0], device=H.device)
|
|
328
|
+
cov = torch.linalg.inv(H + reg_eye)
|
|
329
|
+
|
|
330
|
+
# symmerize covariance matrix
|
|
331
|
+
cov = (cov + cov.T) / 2
|
|
332
|
+
|
|
333
|
+
return cov
|
bff/cli.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Command-line entry points for Bayesic Force Fields."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(
|
|
10
|
+
help="BayesicForceFields: Bayesian optimization of molecular force fields.",
|
|
11
|
+
no_args_is_help=True,
|
|
12
|
+
add_completion=False,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
WorkflowMain = Callable[[Path], None]
|
|
16
|
+
WORKFLOW_COMMANDS = (
|
|
17
|
+
"prepare",
|
|
18
|
+
"simulate",
|
|
19
|
+
"qoi",
|
|
20
|
+
"train",
|
|
21
|
+
"learn",
|
|
22
|
+
"validate",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_COMPLETION_ACTIVATE = f"""# Managed automatically by Bayesic Force Fields.
|
|
26
|
+
if [ -n "${{BASH_VERSION-}}" ]; then
|
|
27
|
+
_bff_completion() {{
|
|
28
|
+
local cur prev words cword
|
|
29
|
+
if declare -F _init_completion >/dev/null 2>&1; then
|
|
30
|
+
_init_completion || return
|
|
31
|
+
else
|
|
32
|
+
COMPREPLY=()
|
|
33
|
+
cur="${{COMP_WORDS[COMP_CWORD]}}"
|
|
34
|
+
prev="${{COMP_WORDS[COMP_CWORD-1]}}"
|
|
35
|
+
words=("${{COMP_WORDS[@]}}")
|
|
36
|
+
cword=$COMP_CWORD
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
local commands="{" ".join(WORKFLOW_COMMANDS)}"
|
|
40
|
+
if [[ $COMP_CWORD -eq 1 ]]; then
|
|
41
|
+
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
|
|
42
|
+
return 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
case "${{COMP_WORDS[1]}}" in
|
|
46
|
+
prepare|simulate|qoi|train|learn|validate)
|
|
47
|
+
COMPREPLY=($(compgen -f -- "$cur"))
|
|
48
|
+
;;
|
|
49
|
+
*)
|
|
50
|
+
COMPREPLY=()
|
|
51
|
+
;;
|
|
52
|
+
esac
|
|
53
|
+
}}
|
|
54
|
+
|
|
55
|
+
complete -o nosort -F _bff_completion bff
|
|
56
|
+
elif [ -n "${{ZSH_VERSION-}}" ]; then
|
|
57
|
+
autoload -Uz compinit 2>/dev/null || true
|
|
58
|
+
compinit -i 2>/dev/null || true
|
|
59
|
+
|
|
60
|
+
_bff_completion() {{
|
|
61
|
+
local -a commands
|
|
62
|
+
commands=({" ".join(WORKFLOW_COMMANDS)})
|
|
63
|
+
|
|
64
|
+
if (( CURRENT == 2 )); then
|
|
65
|
+
compadd -- "${{commands[@]}}"
|
|
66
|
+
return 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
case "${{words[2]}}" in
|
|
70
|
+
prepare|simulate|qoi|train|learn|validate)
|
|
71
|
+
_files
|
|
72
|
+
;;
|
|
73
|
+
esac
|
|
74
|
+
}}
|
|
75
|
+
|
|
76
|
+
compdef _bff_completion bff
|
|
77
|
+
fi
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
_COMPLETION_DEACTIVATE = """# Managed automatically by Bayesic Force Fields.
|
|
81
|
+
if [ -n "${BASH_VERSION-}" ]; then
|
|
82
|
+
complete -r bff 2>/dev/null || true
|
|
83
|
+
elif [ -n "${ZSH_VERSION-}" ]; then
|
|
84
|
+
unfunction _bff_completion 2>/dev/null || true
|
|
85
|
+
compdef -d bff 2>/dev/null || true
|
|
86
|
+
fi
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _conda_completion_hook_paths() -> tuple[Path, Path] | None:
|
|
91
|
+
conda_prefix = os.environ.get("CONDA_PREFIX")
|
|
92
|
+
if not conda_prefix:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
conda_dir = Path(conda_prefix) / "etc" / "conda"
|
|
96
|
+
return (
|
|
97
|
+
conda_dir / "activate.d" / "bff-completion.sh",
|
|
98
|
+
conda_dir / "deactivate.d" / "bff-completion.sh",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _write_if_changed(path: Path, content: str) -> None:
|
|
103
|
+
if path.exists() and path.read_text() == content:
|
|
104
|
+
return
|
|
105
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
path.write_text(content)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _ensure_completion_hooks() -> None:
|
|
110
|
+
hook_paths = _conda_completion_hook_paths()
|
|
111
|
+
if hook_paths is None:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
activate_hook, deactivate_hook = hook_paths
|
|
115
|
+
try:
|
|
116
|
+
_write_if_changed(activate_hook, _COMPLETION_ACTIVATE)
|
|
117
|
+
_write_if_changed(deactivate_hook, _COMPLETION_DEACTIVATE)
|
|
118
|
+
except OSError:
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def run_workflow(
|
|
123
|
+
fn_config: Path,
|
|
124
|
+
workflow_main: WorkflowMain,
|
|
125
|
+
workflow_name: str,
|
|
126
|
+
) -> None:
|
|
127
|
+
"""
|
|
128
|
+
Run a workflow main function with a config path.
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
workflow_main(fn_config)
|
|
132
|
+
except FileNotFoundError as exc:
|
|
133
|
+
raise typer.BadParameter(str(exc), param_hint="fn_config") from exc
|
|
134
|
+
except ValueError as exc:
|
|
135
|
+
raise typer.BadParameter(str(exc), param_hint=workflow_name) from exc
|
|
136
|
+
except RuntimeError as exc:
|
|
137
|
+
typer.echo(str(exc), err=True)
|
|
138
|
+
raise typer.Exit(code=1) from exc
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def config_argument() -> Path:
|
|
142
|
+
return typer.Argument(
|
|
143
|
+
...,
|
|
144
|
+
exists=True,
|
|
145
|
+
dir_okay=False,
|
|
146
|
+
help="Path to the configuration file.",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@app.callback()
|
|
151
|
+
def init_cli() -> None:
|
|
152
|
+
"""
|
|
153
|
+
Initialize the CLI environment.
|
|
154
|
+
"""
|
|
155
|
+
_ensure_completion_hooks()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@app.command()
|
|
159
|
+
def version() -> None:
|
|
160
|
+
"""
|
|
161
|
+
Print package version.
|
|
162
|
+
"""
|
|
163
|
+
from importlib import metadata
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
version_str = metadata.version("bfflearn")
|
|
167
|
+
except metadata.PackageNotFoundError:
|
|
168
|
+
from . import __version__
|
|
169
|
+
|
|
170
|
+
version_str = __version__
|
|
171
|
+
|
|
172
|
+
typer.echo(f"BayesicForceFields version: {version_str}")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@app.command()
|
|
176
|
+
def prepare(fn_config: Path = config_argument()) -> None:
|
|
177
|
+
"""
|
|
178
|
+
Prepare equilibration, training, and reference assets.
|
|
179
|
+
"""
|
|
180
|
+
from bff.workflows.prepare import main as prepare_main
|
|
181
|
+
|
|
182
|
+
run_workflow(fn_config, prepare_main, "prepare")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@app.command(name="simulate")
|
|
186
|
+
def simulate(fn_config: Path = config_argument()) -> None:
|
|
187
|
+
"""
|
|
188
|
+
Run a sampled molecular-dynamics campaign for training-set generation.
|
|
189
|
+
"""
|
|
190
|
+
from bff.workflows.simulate import main as simulate_main
|
|
191
|
+
|
|
192
|
+
run_workflow(fn_config, simulate_main, "simulate")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@app.command()
|
|
196
|
+
def validate(fn_config: Path = config_argument()) -> None:
|
|
197
|
+
"""
|
|
198
|
+
Run a validation campaign for provided parameter samples.
|
|
199
|
+
"""
|
|
200
|
+
from bff.workflows.validate import main as validate_main
|
|
201
|
+
|
|
202
|
+
run_workflow(fn_config, validate_main, "validate")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@app.command(hidden=True)
|
|
206
|
+
def md(fn_config: Path = config_argument()) -> None:
|
|
207
|
+
"""
|
|
208
|
+
Run molecular dynamics from a configuration file.
|
|
209
|
+
"""
|
|
210
|
+
from bff.workflows.md import main as md_main
|
|
211
|
+
|
|
212
|
+
run_workflow(fn_config, md_main, "md")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@app.command(name="qoi")
|
|
216
|
+
def qoi(fn_config: Path = config_argument()) -> None:
|
|
217
|
+
"""
|
|
218
|
+
Analyze quantities of interest from simulation data.
|
|
219
|
+
"""
|
|
220
|
+
from bff.workflows.qoi import main as qoi_main
|
|
221
|
+
|
|
222
|
+
run_workflow(fn_config, qoi_main, "qoi")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@app.command(name="analyze", hidden=True)
|
|
226
|
+
def analyze_alias(fn_config: Path = config_argument()) -> None:
|
|
227
|
+
"""
|
|
228
|
+
Hidden compatibility alias for the QoI workflow.
|
|
229
|
+
"""
|
|
230
|
+
qoi(fn_config)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@app.command()
|
|
234
|
+
def train(fn_config: Path = config_argument()) -> None:
|
|
235
|
+
"""
|
|
236
|
+
Train surrogate models from analyzed QoI datasets.
|
|
237
|
+
"""
|
|
238
|
+
from bff.workflows.train import main as train_main
|
|
239
|
+
|
|
240
|
+
run_workflow(fn_config, train_main, "train")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@app.command()
|
|
244
|
+
def learn(fn_config: Path = config_argument()) -> None:
|
|
245
|
+
"""
|
|
246
|
+
Run posterior learning from trained surrogate models.
|
|
247
|
+
"""
|
|
248
|
+
from bff.workflows.learn import main as learn_main
|
|
249
|
+
|
|
250
|
+
run_workflow(fn_config, learn_main, "learn")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
if __name__ == "__main__":
|
|
254
|
+
app()
|
bff/data.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
WATER_4SITE = np.array(
|
|
4
|
+
[[0.02909557, 0.03692916, -0.04588176],
|
|
5
|
+
[0.43909732, -0.54306981, 0.60411833],
|
|
6
|
+
[-0.90090283, -0.04307076, 0.12411808],
|
|
7
|
+
[-0.0309039, -0.04307076, 0.05411814]]
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
WATER_3SITE = np.array(
|
|
11
|
+
[[0.02909557, 0.03692916, -0.04588176],
|
|
12
|
+
[0.43909732, -0.54306981, 0.60411833],
|
|
13
|
+
[-0.90090283, -0.04307076, 0.12411808]]
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
IONS = ['li', 'na', 'k', 'ca', 'cal', 'mg', 'f', 'cl', 'br', 'i']
|
|
17
|
+
CATIONS = {'li', 'na', 'k', 'rb', 'ca', 'mg'}
|
|
18
|
+
ANIONS = {'f', 'cl', 'br', 'i'}
|
|
19
|
+
|
|
20
|
+
WATERS = ['SOL', 'water', 'HOH']
|
|
21
|
+
|
|
22
|
+
CP2K_KIND_DEFAULTS = {
|
|
23
|
+
'h': {'basis_set': 'TZV2P-GTH-q1', 'potential': 'GTH-PBE-q1'},
|
|
24
|
+
'o': {'basis_set': 'TZV2P-GTH-q6', 'potential': 'GTH-PBE-q6'},
|
|
25
|
+
'c': {'basis_set': 'TZV2P-GTH-q4', 'potential': 'GTH-PBE-q4'},
|
|
26
|
+
'p': {'basis_set': 'TZV2P-GTH-q5', 'potential': 'GTH-PBE-q5'},
|
|
27
|
+
'n': {'basis_set': 'TZV2P-GTH-q5', 'potential': 'GTH-PBE-q5'},
|
|
28
|
+
'na': {'basis_set': 'TZV2P-GTH-q9', 'potential': 'GTH-PBE-q9'},
|
|
29
|
+
'ca': {'basis_set': 'TZV2P-MOLOPT-PBE-GTH-q10', 'potential': 'GTH-PBE-q10'},
|
|
30
|
+
'cl': {'basis_set': 'TZV2P-GTH-q7', 'potential': 'GTH-PBE-q7'},
|
|
31
|
+
's': {'basis_set': 'TZV2P-GTH-q6', 'potential': 'GTH-PBE-q6'},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
CP2K_INPUT_TEMPLATE = {
|
|
35
|
+
'global': {
|
|
36
|
+
'project_name': 'project',
|
|
37
|
+
'print_level': 'LOW',
|
|
38
|
+
'run_type': 'MD',
|
|
39
|
+
'walltime': '03:50:00'
|
|
40
|
+
},
|
|
41
|
+
'motion': {
|
|
42
|
+
'md': {'temperature': 300.0, 'timestep': '[fs] 0.5', 'steps': 500},
|
|
43
|
+
'print': {
|
|
44
|
+
'forces': {
|
|
45
|
+
'format': 'DCD',
|
|
46
|
+
'each': {'md': 1}
|
|
47
|
+
},
|
|
48
|
+
'trajectory': {
|
|
49
|
+
'format': 'DCD',
|
|
50
|
+
'each': {'md': 1}
|
|
51
|
+
},
|
|
52
|
+
'restart': {
|
|
53
|
+
'backup_copies': 1,
|
|
54
|
+
'each': {'md': 1}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
'force_eval': {
|
|
59
|
+
'method': 'Quickstep',
|
|
60
|
+
'subsys': {},
|
|
61
|
+
'dft': {
|
|
62
|
+
'basis_set_file_name': ['GTH_BASIS_SETS', 'BASIS_MOLOPT_UZH'],
|
|
63
|
+
'potential_file_name': 'GTH_POTENTIALS',
|
|
64
|
+
'scf': {
|
|
65
|
+
'max_scf': 20, 'eps_scf': 5e-07,
|
|
66
|
+
'outer_scf': {'max_scf': 20, 'eps_scf': 5e-07},
|
|
67
|
+
'ot': {
|
|
68
|
+
'preconditioner': 'FULL_ALL',
|
|
69
|
+
'energy_gap': -1.0,
|
|
70
|
+
'minimizer': 'DIIS'}
|
|
71
|
+
},
|
|
72
|
+
'mgrid': {'cutoff': '[Ry] 400.0'},
|
|
73
|
+
'qs': {
|
|
74
|
+
'method': 'GPW',
|
|
75
|
+
'eps_default': 1e-12,
|
|
76
|
+
'extrapolation_order': 4,
|
|
77
|
+
'extrapolation': 'ASPC'
|
|
78
|
+
},
|
|
79
|
+
'xc': {
|
|
80
|
+
'xc_functional': {'pbe': {'parametrization': 'REVPBE'}},
|
|
81
|
+
'xc_grid': {'xc_deriv': 'NN50_SMOOTH'},
|
|
82
|
+
'vdw_potential': {
|
|
83
|
+
'potential_type': 'PAIR_POTENTIAL',
|
|
84
|
+
'pair_potential': {
|
|
85
|
+
'type': 'DFTD3', 'r_cutoff': '[angstrom] 16.0',
|
|
86
|
+
'reference_functional': 'revPBE',
|
|
87
|
+
'parameter_file_name': 'dftd3.dat'
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
bff/domain/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .specs import Bounds, ChargeConstraint, RandomParamsGenerator, Specs
|
|
2
|
+
from .trainset import SimulationSystem, TrainSetInfo, TrajectorySet
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"Bounds",
|
|
6
|
+
"ChargeConstraint",
|
|
7
|
+
"RandomParamsGenerator",
|
|
8
|
+
"SimulationSystem",
|
|
9
|
+
"Specs",
|
|
10
|
+
"TrainSetInfo",
|
|
11
|
+
"TrajectorySet",
|
|
12
|
+
]
|