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/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Bayesic Force Fields public package namespace."""
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.0.dev1"
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"bayes",
|
|
9
|
+
"data",
|
|
10
|
+
"domain",
|
|
11
|
+
"io",
|
|
12
|
+
"plotting",
|
|
13
|
+
"qoi",
|
|
14
|
+
"tools",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def __getattr__(name: str):
|
|
19
|
+
if name in {
|
|
20
|
+
"bayes",
|
|
21
|
+
"data",
|
|
22
|
+
"domain",
|
|
23
|
+
"io",
|
|
24
|
+
"plotting",
|
|
25
|
+
"qoi",
|
|
26
|
+
"tools",
|
|
27
|
+
}:
|
|
28
|
+
return import_module(f".{name}", __name__)
|
|
29
|
+
|
|
30
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
bff/bayes/__init__.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Self, Union
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import torch
|
|
6
|
+
|
|
7
|
+
from .kernels import gaussian_kernel
|
|
8
|
+
from .utils import check_tensor, nearest_positive_definite, smape
|
|
9
|
+
|
|
10
|
+
PathLike = Union[str, Path]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LocalGaussianProcess:
|
|
14
|
+
"""
|
|
15
|
+
Local Gaussian Process Regression model.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
X_train : torch.Tensor
|
|
20
|
+
Training input features.
|
|
21
|
+
y_train : torch.Tensor
|
|
22
|
+
Training output values.
|
|
23
|
+
y_mean : torch.Tensor
|
|
24
|
+
Mean of the output values.
|
|
25
|
+
lengths : torch.Tensor
|
|
26
|
+
Length scales for the Gaussian kernel.
|
|
27
|
+
width : float
|
|
28
|
+
Width (amplitude) of the Gaussian kernel.
|
|
29
|
+
sigma : float
|
|
30
|
+
Observation noise standard deviation.
|
|
31
|
+
device : str
|
|
32
|
+
Device on which tensors are stored (e.g., "cpu" or "cuda").
|
|
33
|
+
|
|
34
|
+
Attributes
|
|
35
|
+
----------
|
|
36
|
+
Kdd_inv : torch.Tensor
|
|
37
|
+
Inverse of the training covariance matrix.
|
|
38
|
+
|
|
39
|
+
Methods
|
|
40
|
+
-------
|
|
41
|
+
predict(Xi: torch.Tensor) -> torch.Tensor
|
|
42
|
+
Predict outputs for given input points.
|
|
43
|
+
|
|
44
|
+
Properties
|
|
45
|
+
----------
|
|
46
|
+
n_params : int
|
|
47
|
+
Number of input parameters (features).
|
|
48
|
+
y_size : int
|
|
49
|
+
Dimensionality of the output values.
|
|
50
|
+
hyperparameters : Dict[str, Union[np.ndarray, float]]
|
|
51
|
+
Dictionary of hyperparameters used in the model.
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
X_train: torch.Tensor, y_train: torch.Tensor, y_mean: torch.Tensor,
|
|
58
|
+
lengths: torch.Tensor, width: float, sigma: float,
|
|
59
|
+
device: str
|
|
60
|
+
) -> None:
|
|
61
|
+
|
|
62
|
+
self.X_train = check_tensor(X_train, device=device)
|
|
63
|
+
self.y_train = check_tensor(y_train, device=device)
|
|
64
|
+
self.y_mean = check_tensor(y_mean, device=device)
|
|
65
|
+
self.lengths = check_tensor(lengths, device=device)
|
|
66
|
+
self.width = check_tensor(width, device=device)
|
|
67
|
+
self.sigma = check_tensor(sigma, device=device)
|
|
68
|
+
self.device = device
|
|
69
|
+
|
|
70
|
+
n_samples = len(self.X_train)
|
|
71
|
+
|
|
72
|
+
noise = torch.eye(n_samples, device=device) * self.sigma
|
|
73
|
+
Kdd = gaussian_kernel(self.X_train, self.X_train, self.lengths, width) + noise
|
|
74
|
+
Kdd = nearest_positive_definite(Kdd)
|
|
75
|
+
L = torch.linalg.cholesky(Kdd)
|
|
76
|
+
Kdd_inv = torch.cholesky_inverse(L)
|
|
77
|
+
self.Kdd_inv = check_tensor(Kdd_inv, device=device)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def n_params(self) -> int:
|
|
81
|
+
return self.X_train.shape[1]
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def y_size(self) -> int:
|
|
85
|
+
return self.y_train.shape[1]
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def hyperparameters(self) -> dict[str, Union[np.ndarray, float]]:
|
|
89
|
+
return {
|
|
90
|
+
'lengths': self.lengths.cpu().numpy(),
|
|
91
|
+
'width': self.width.cpu().numpy().item(),
|
|
92
|
+
'sigma': self.sigma.cpu().numpy().item()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@torch.no_grad()
|
|
96
|
+
def predict(self, Xi: torch.Tensor) -> torch.Tensor:
|
|
97
|
+
"""
|
|
98
|
+
Predict outputs for given input points.
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
Xi : torch.Tensor
|
|
103
|
+
Input tensor of shape (n_samples, n_features).
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
torch.Tensor
|
|
108
|
+
Predicted outputs of shape (n_samples, output_dim).
|
|
109
|
+
"""
|
|
110
|
+
Xi = check_tensor(Xi, device=self.device)
|
|
111
|
+
Kid = gaussian_kernel(Xi, self.X_train, self.lengths, self.width)
|
|
112
|
+
mean = self.y_mean + (Kid @ self.Kdd_inv) @ (self.y_train - self.y_mean)
|
|
113
|
+
return mean
|
|
114
|
+
|
|
115
|
+
def state_dict(self) -> dict:
|
|
116
|
+
return {
|
|
117
|
+
"X_train": self.X_train,
|
|
118
|
+
"y_train": self.y_train,
|
|
119
|
+
"y_mean": self.y_mean,
|
|
120
|
+
"lengths": self.lengths,
|
|
121
|
+
"width": self.width,
|
|
122
|
+
"sigma": self.sigma,
|
|
123
|
+
"device": self.device,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
def __repr__(self) -> str:
|
|
127
|
+
hp = self.hyperparameters
|
|
128
|
+
return (
|
|
129
|
+
f"{self.__class__.__name__}(\n"
|
|
130
|
+
f" n_train={self.X_train.shape[0]},\n"
|
|
131
|
+
f" n_params={self.n_params},\n"
|
|
132
|
+
f" lengths={hp['lengths'].tolist()},\n"
|
|
133
|
+
f" width={hp['width']:.4f},\n"
|
|
134
|
+
f" noise={hp['sigma']:.4f},\n"
|
|
135
|
+
f" device='{self.device}'\n"
|
|
136
|
+
f")"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class LGPCommittee:
|
|
141
|
+
"""
|
|
142
|
+
Committee of Local Gaussian Process (LGP) models.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
lgps : list of LocalGaussianProcess
|
|
147
|
+
List of LGP models.
|
|
148
|
+
n_observations : int
|
|
149
|
+
Effective number of observations used in the likelihood term.
|
|
150
|
+
reference_values : np.ndarray
|
|
151
|
+
Reference observation vector matched by the surrogate outputs.
|
|
152
|
+
nuisance : float, optional
|
|
153
|
+
Nuisance parameter for model selection (default is None).
|
|
154
|
+
stochastic : bool, optional
|
|
155
|
+
If True, randomly select one model for prediction instead of averaging
|
|
156
|
+
(default is False).
|
|
157
|
+
|
|
158
|
+
Properties
|
|
159
|
+
----------
|
|
160
|
+
size : int
|
|
161
|
+
Number of LGP models in the committee.
|
|
162
|
+
n_params : int
|
|
163
|
+
Number of input parameters (features) used by the LGP models.
|
|
164
|
+
|
|
165
|
+
Methods
|
|
166
|
+
-------
|
|
167
|
+
predict(X: torch.Tensor) -> torch.Tensor
|
|
168
|
+
Predict outputs by averaging predictions from all LGP models
|
|
169
|
+
or randomly selecting one if stochastic is True.
|
|
170
|
+
validate(X_test: torch.Tensor, y_test: torch.Tensor) -> float
|
|
171
|
+
Validate the committee by computing the
|
|
172
|
+
mean squared error of predictions against test data.
|
|
173
|
+
"""
|
|
174
|
+
def __init__(
|
|
175
|
+
self,
|
|
176
|
+
lgps: list[LocalGaussianProcess],
|
|
177
|
+
n_observations: int,
|
|
178
|
+
reference_values: np.ndarray,
|
|
179
|
+
nuisance: float | None = None,
|
|
180
|
+
stochastic: bool = False
|
|
181
|
+
) -> None:
|
|
182
|
+
self.lgps = lgps
|
|
183
|
+
self.error: float | None = None
|
|
184
|
+
self.n_observations = int(n_observations)
|
|
185
|
+
self.reference_values = np.asarray(reference_values, dtype=float).reshape(-1)
|
|
186
|
+
self.nuisance = nuisance
|
|
187
|
+
self.stochastic = stochastic
|
|
188
|
+
|
|
189
|
+
if self.reference_values.size != self.lgps[0].y_size:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
"Reference observation size does not match surrogate output size."
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def size(self) -> int:
|
|
196
|
+
return len(self.lgps)
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def n_params(self) -> int:
|
|
200
|
+
return self.lgps[0].n_params
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def y_size(self) -> int:
|
|
204
|
+
return self.lgps[0].y_size
|
|
205
|
+
|
|
206
|
+
def predict(self, X: torch.Tensor) -> torch.Tensor:
|
|
207
|
+
"""
|
|
208
|
+
Predict outputs by averaging predictions from all LGP models.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
X : torch.Tensor
|
|
213
|
+
Input tensor of shape (n_samples, n_features).
|
|
214
|
+
|
|
215
|
+
Returns
|
|
216
|
+
-------
|
|
217
|
+
torch.Tensor
|
|
218
|
+
Averaged predicted outputs of shape (n_samples, output_dim).
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
if self.size > 1 and self.stochastic:
|
|
222
|
+
# Select a random model and return its prediction
|
|
223
|
+
idx = torch.randint(self.size, (1,)).item()
|
|
224
|
+
return self.lgps[idx].predict(X)
|
|
225
|
+
|
|
226
|
+
# Otherwise, return the mean prediction across all models
|
|
227
|
+
predictions = torch.stack([lgp.predict(X) for lgp in self.lgps])
|
|
228
|
+
return predictions.mean(dim=0).squeeze(0)
|
|
229
|
+
|
|
230
|
+
def validate(self, X_test: torch.Tensor, y_test: torch.Tensor) -> float:
|
|
231
|
+
"""
|
|
232
|
+
Validate the committee by computing the mean squared error.
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
X_test : torch.Tensor
|
|
237
|
+
Test input tensor of shape (n_samples, n_features).
|
|
238
|
+
y_test : torch.Tensor
|
|
239
|
+
Test output tensor of shape (n_samples, output_dim).
|
|
240
|
+
|
|
241
|
+
Returns
|
|
242
|
+
-------
|
|
243
|
+
float
|
|
244
|
+
Mean squared error of the predictions.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
y_pred = self.predict(X_test)
|
|
248
|
+
self.error = 100.0 * smape(y_test, y_pred)
|
|
249
|
+
return self.error
|
|
250
|
+
|
|
251
|
+
@classmethod
|
|
252
|
+
def load(cls, fn: PathLike) -> Self:
|
|
253
|
+
state = torch.load(fn, weights_only=False)
|
|
254
|
+
lgps = [LocalGaussianProcess(**lgp_state) for lgp_state in state["lgps"]]
|
|
255
|
+
committee = cls(
|
|
256
|
+
lgps=lgps,
|
|
257
|
+
n_observations=state["n_observations"],
|
|
258
|
+
reference_values=np.asarray(state["reference_values"], dtype=float),
|
|
259
|
+
nuisance=state["nuisance"],
|
|
260
|
+
stochastic=state["stochastic"],
|
|
261
|
+
)
|
|
262
|
+
committee.error = state["error"]
|
|
263
|
+
return committee
|
|
264
|
+
|
|
265
|
+
def write(self, fn_out: PathLike) -> None:
|
|
266
|
+
|
|
267
|
+
state = {
|
|
268
|
+
"n_observations": self.n_observations,
|
|
269
|
+
"reference_values": self.reference_values.tolist(),
|
|
270
|
+
"nuisance": self.nuisance,
|
|
271
|
+
"stochastic": self.stochastic,
|
|
272
|
+
"error": self.error,
|
|
273
|
+
"lgps": [lgp.state_dict() for lgp in self.lgps],
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
torch.save(state, fn_out)
|
|
277
|
+
|
|
278
|
+
def __repr__(self) -> str:
|
|
279
|
+
return (
|
|
280
|
+
f"{self.__class__.__name__}(\n"
|
|
281
|
+
f" committee_size={self.size},\n"
|
|
282
|
+
f" n_params={self.n_params},\n"
|
|
283
|
+
f" testset error={self.error},\n"
|
|
284
|
+
f")"
|
|
285
|
+
)
|
bff/bayes/kernels.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
|
|
5
|
+
from .utils import check_tensor, with_manual_sqdist_flag
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@with_manual_sqdist_flag
|
|
9
|
+
def gaussian_kernel(
|
|
10
|
+
x1: torch.Tensor, x2: torch.Tensor,
|
|
11
|
+
lengths: Union[float, torch.Tensor],
|
|
12
|
+
width: Union[float, torch.Tensor],
|
|
13
|
+
manual_sqdist: bool = False
|
|
14
|
+
) -> torch.Tensor:
|
|
15
|
+
"""Gaussian (Squared Exponential) kernel.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
x1 : torch.Tensor
|
|
20
|
+
First input tensor of shape (n_samples, n_features) or (n_features,).
|
|
21
|
+
x2 : torch.Tensor
|
|
22
|
+
Second input tensor of shape (n_samples, n_features) or (n_features,).
|
|
23
|
+
lengths : Union[float, torch.Tensor]
|
|
24
|
+
Length scale(s) for the kernel.
|
|
25
|
+
Can be a single float or a tensor of shape (n_features,).
|
|
26
|
+
width : Union[float, torch.Tensor]
|
|
27
|
+
Width scale(s) for the kernel.
|
|
28
|
+
Can be a single float or a tensor of shape (n_features,).
|
|
29
|
+
manual_sqdist : bool, optional
|
|
30
|
+
If True, computes the squared distance manually.
|
|
31
|
+
If False, uses `torch.cdist` for efficiency.
|
|
32
|
+
manual_sqdist is necessary for evaluation of second derivatives.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
torch.Tensor
|
|
37
|
+
Kernel matrix of shape (n_samples_x1, n_samples_x2).
|
|
38
|
+
If x1 and x2 are 2D tensors, the output will be of shape
|
|
39
|
+
(n_samples, n_samples_x1, n_samples_x2).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
assert x1.device == x2.device, "x1 and x2 must be on the same device"
|
|
43
|
+
device = x1.device
|
|
44
|
+
|
|
45
|
+
if x1.dim() == 2:
|
|
46
|
+
x1 = x1[None, :, :].to(device)
|
|
47
|
+
if x2.dim() == 2:
|
|
48
|
+
x2 = x2[None, :, :].to(device)
|
|
49
|
+
|
|
50
|
+
lengths = check_tensor(lengths, device=device)
|
|
51
|
+
if lengths.dim() == 1:
|
|
52
|
+
lengths = lengths[None, :].to(device)
|
|
53
|
+
|
|
54
|
+
width = check_tensor(width, device=device)
|
|
55
|
+
if width.dim() == 1:
|
|
56
|
+
width = width[None, :].to(device)
|
|
57
|
+
|
|
58
|
+
x1_scaled = x1 / lengths
|
|
59
|
+
x2_scaled = x2 / lengths
|
|
60
|
+
|
|
61
|
+
if manual_sqdist:
|
|
62
|
+
x1_sq = (x1_scaled**2).sum(dim=-1, keepdim=True) # (B, N1, 1)
|
|
63
|
+
x2_sq = (x2_scaled**2).sum(dim=-1, keepdim=True).transpose(1, 2) # (B, 1, N2)
|
|
64
|
+
sqdist = x1_sq + x2_sq - 2 * (x1_scaled @ x2_scaled.transpose(1, 2))
|
|
65
|
+
sqdist = torch.clamp(sqdist, min=0.0)
|
|
66
|
+
else:
|
|
67
|
+
sqdist = torch.cdist(x1_scaled, x2_scaled, p=2).pow(2)
|
|
68
|
+
|
|
69
|
+
kernel = width.pow(2).mean() * torch.exp(-0.5 * sqdist)
|
|
70
|
+
|
|
71
|
+
return kernel.squeeze(0)
|