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/plotting.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any, Mapping, Optional, Sequence, Union
|
|
3
|
+
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import numpy as np
|
|
6
|
+
import torch
|
|
7
|
+
from matplotlib.colors import ListedColormap
|
|
8
|
+
from matplotlib.ticker import MaxNLocator
|
|
9
|
+
from scipy.stats import gaussian_kde
|
|
10
|
+
|
|
11
|
+
from .bayes.results import InferenceResults
|
|
12
|
+
from .domain.specs import Specs
|
|
13
|
+
|
|
14
|
+
PathLike = Union[str, Path]
|
|
15
|
+
ArrayLike = Union[np.ndarray, torch.Tensor]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _wrap_label(text: str, max_per_line: int = 4) -> str:
|
|
19
|
+
words = text.split()
|
|
20
|
+
return "\n".join(
|
|
21
|
+
" ".join(words[i:i + max_per_line])
|
|
22
|
+
for i in range(0, len(words), max_per_line)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _coerce_specs(specs: Specs | PathLike) -> Specs:
|
|
27
|
+
return specs if isinstance(specs, Specs) else Specs(specs)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _coerce_samples(
|
|
31
|
+
samples: InferenceResults | ArrayLike,
|
|
32
|
+
) -> np.ndarray:
|
|
33
|
+
if isinstance(samples, InferenceResults):
|
|
34
|
+
return np.asarray(samples.prepared_samples, dtype=float)
|
|
35
|
+
if isinstance(samples, torch.Tensor):
|
|
36
|
+
return samples.detach().cpu().numpy()
|
|
37
|
+
return np.asarray(samples, dtype=float)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _parameter_labels(
|
|
41
|
+
names: Sequence[str],
|
|
42
|
+
labels: Optional[Sequence[str] | Mapping[str, str]] = None,
|
|
43
|
+
) -> list[str]:
|
|
44
|
+
if labels is None:
|
|
45
|
+
return list(names)
|
|
46
|
+
if isinstance(labels, Mapping):
|
|
47
|
+
return [labels.get(name, name) for name in names]
|
|
48
|
+
if len(labels) != len(names):
|
|
49
|
+
raise ValueError(
|
|
50
|
+
"parameter_labels must match the number of plotted parameters.")
|
|
51
|
+
return list(labels)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _expand_short_labels(
|
|
55
|
+
labels: Sequence[str],
|
|
56
|
+
full_labels: Sequence[str],
|
|
57
|
+
) -> list[str]:
|
|
58
|
+
"""Expand shortened labels like ``C1`` back to ``charge C1`` when possible."""
|
|
59
|
+
if len(labels) != len(full_labels):
|
|
60
|
+
return list(labels)
|
|
61
|
+
|
|
62
|
+
lookup: dict[str, str] = {}
|
|
63
|
+
for full_label in full_labels:
|
|
64
|
+
if full_label.startswith("$"):
|
|
65
|
+
lookup.setdefault(full_label, full_label)
|
|
66
|
+
continue
|
|
67
|
+
lookup.setdefault(full_label, full_label)
|
|
68
|
+
lookup.setdefault(full_label.split(maxsplit=1)[-1], full_label)
|
|
69
|
+
|
|
70
|
+
expanded: list[str] = []
|
|
71
|
+
changed = False
|
|
72
|
+
for label in labels:
|
|
73
|
+
replacement = lookup.get(label, label)
|
|
74
|
+
expanded.append(replacement)
|
|
75
|
+
changed |= replacement != label
|
|
76
|
+
|
|
77
|
+
return expanded if changed else list(labels)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _axis_labels(kind: str) -> tuple[str, str]:
|
|
81
|
+
if kind == "charge":
|
|
82
|
+
return "Atom", "Charge [e]"
|
|
83
|
+
if kind == "sigma":
|
|
84
|
+
return "Atom type", "$\\sigma$ [nm]"
|
|
85
|
+
return kind.capitalize(), kind.capitalize()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def plot_marginals(
|
|
89
|
+
results: InferenceResults,
|
|
90
|
+
specs: Specs | PathLike,
|
|
91
|
+
*,
|
|
92
|
+
parameter_labels: Optional[Sequence[str] | Mapping[str, str]] = None,
|
|
93
|
+
color_prior: str = "gray",
|
|
94
|
+
color_posterior: str = "tab:red",
|
|
95
|
+
fn_out: Optional[PathLike] = None,
|
|
96
|
+
) -> None:
|
|
97
|
+
specs = _coerce_specs(specs)
|
|
98
|
+
posterior = specs.with_implicit_charge(results.prepared_samples)
|
|
99
|
+
param_names = specs.bounds.names.tolist()
|
|
100
|
+
tick_labels = _parameter_labels(param_names, parameter_labels)
|
|
101
|
+
|
|
102
|
+
param_groups: dict[str, list[int]] = {}
|
|
103
|
+
for idx, name in enumerate(param_names):
|
|
104
|
+
kind = name.split()[0]
|
|
105
|
+
param_groups.setdefault(kind, []).append(idx)
|
|
106
|
+
|
|
107
|
+
fig, axes = plt.subplots(
|
|
108
|
+
1,
|
|
109
|
+
len(param_groups),
|
|
110
|
+
figsize=(4 * len(param_groups), 3.2),
|
|
111
|
+
gridspec_kw={"wspace": 0.3},
|
|
112
|
+
)
|
|
113
|
+
axes = np.atleast_1d(axes)
|
|
114
|
+
|
|
115
|
+
explicit_names = specs.bounds.without(specs.implicit_param).names.tolist()
|
|
116
|
+
prior_index = {name: i for i, name in enumerate(explicit_names)}
|
|
117
|
+
show_prior = results.priors is not None
|
|
118
|
+
legend_used = {"prior": False, "posterior": False, "bounds": False}
|
|
119
|
+
|
|
120
|
+
for ax, (kind, indices) in zip(axes, param_groups.items()):
|
|
121
|
+
bounds_block = np.asarray(
|
|
122
|
+
[specs.bounds.by_name[param_names[i]] for i in indices],
|
|
123
|
+
dtype=float,
|
|
124
|
+
)
|
|
125
|
+
y_min = bounds_block[:, 0].min()
|
|
126
|
+
y_max = bounds_block[:, 1].max()
|
|
127
|
+
y_pad = max(0.05, 0.18 * (y_max - y_min))
|
|
128
|
+
label_y = y_min - 0.80 * y_pad
|
|
129
|
+
posterior_peaks: list[float] = []
|
|
130
|
+
prior_peaks: list[float] = []
|
|
131
|
+
curves: dict[
|
|
132
|
+
int, tuple[np.ndarray, np.ndarray, Optional[np.ndarray], float]
|
|
133
|
+
] = {}
|
|
134
|
+
|
|
135
|
+
for idx in indices:
|
|
136
|
+
name = param_names[idx]
|
|
137
|
+
lower, upper = specs.bounds.by_name[name]
|
|
138
|
+
y = np.linspace(lower - y_pad, upper + y_pad, 400)
|
|
139
|
+
|
|
140
|
+
prior_density = None
|
|
141
|
+
if show_prior and name in prior_index:
|
|
142
|
+
prior = results.priors.distributions[prior_index[name]]
|
|
143
|
+
prior_density = (
|
|
144
|
+
prior.log_prob(torch.as_tensor(y, dtype=torch.float32))
|
|
145
|
+
.exp()
|
|
146
|
+
.detach()
|
|
147
|
+
.cpu()
|
|
148
|
+
.numpy()
|
|
149
|
+
)
|
|
150
|
+
prior_peaks.append(float(np.max(prior_density)))
|
|
151
|
+
|
|
152
|
+
posterior_density = gaussian_kde(posterior[:, idx])(y)
|
|
153
|
+
posterior_peaks.append(float(np.max(posterior_density)))
|
|
154
|
+
mode = float(y[np.argmax(posterior_density)])
|
|
155
|
+
curves[idx] = (y, posterior_density, prior_density, mode)
|
|
156
|
+
|
|
157
|
+
max_posterior_peak = max(posterior_peaks, default=1.0)
|
|
158
|
+
max_prior_peak = max(prior_peaks, default=max_posterior_peak)
|
|
159
|
+
posterior_width = 1.2
|
|
160
|
+
prior_width = 0.7
|
|
161
|
+
posterior_scale = posterior_width / max(max_posterior_peak, 1e-12)
|
|
162
|
+
prior_scale = prior_width / max(max_prior_peak, 1e-12)
|
|
163
|
+
|
|
164
|
+
for xpos, idx in enumerate(indices):
|
|
165
|
+
name = param_names[idx]
|
|
166
|
+
lower, upper = specs.bounds.by_name[name]
|
|
167
|
+
y, posterior_density, prior_density, mode = curves[idx]
|
|
168
|
+
|
|
169
|
+
if prior_density is not None:
|
|
170
|
+
ax.fill_betweenx(
|
|
171
|
+
y,
|
|
172
|
+
xpos - prior_scale * prior_density,
|
|
173
|
+
xpos,
|
|
174
|
+
color=color_prior,
|
|
175
|
+
lw=0,
|
|
176
|
+
label="prior" if not legend_used["prior"] else None,
|
|
177
|
+
)
|
|
178
|
+
legend_used["prior"] = True
|
|
179
|
+
|
|
180
|
+
ax.fill_betweenx(
|
|
181
|
+
y,
|
|
182
|
+
xpos,
|
|
183
|
+
xpos + posterior_scale * posterior_density,
|
|
184
|
+
color=color_posterior,
|
|
185
|
+
lw=0,
|
|
186
|
+
label="posterior" if not legend_used["posterior"] else None,
|
|
187
|
+
)
|
|
188
|
+
legend_used["posterior"] = True
|
|
189
|
+
|
|
190
|
+
center = 0.5 * (lower + upper)
|
|
191
|
+
yerr = np.array([[center - lower], [upper - center]])
|
|
192
|
+
ax.errorbar(
|
|
193
|
+
[xpos],
|
|
194
|
+
[center],
|
|
195
|
+
yerr=yerr,
|
|
196
|
+
lw=2,
|
|
197
|
+
ls="",
|
|
198
|
+
capsize=4,
|
|
199
|
+
capthick=2,
|
|
200
|
+
markeredgewidth=2,
|
|
201
|
+
color="k",
|
|
202
|
+
label="bounds" if not legend_used["bounds"] else None,
|
|
203
|
+
)
|
|
204
|
+
legend_used["bounds"] = True
|
|
205
|
+
|
|
206
|
+
ax.text(
|
|
207
|
+
xpos,
|
|
208
|
+
label_y,
|
|
209
|
+
f"{mode:.3f}",
|
|
210
|
+
color=color_posterior,
|
|
211
|
+
fontweight="bold",
|
|
212
|
+
ha="center",
|
|
213
|
+
va="top",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
ax.set_xlim(-prior_width - 0.25, len(indices) - 1 + posterior_width + 0.25)
|
|
217
|
+
ax.set_ylim(label_y - 0.6 * y_pad, y_max + y_pad)
|
|
218
|
+
ax.set_xticks(range(len(indices)))
|
|
219
|
+
ax.set_xticklabels(
|
|
220
|
+
[_wrap_label(tick_labels[i]) for i in indices],
|
|
221
|
+
rotation=30,
|
|
222
|
+
ha="center",
|
|
223
|
+
)
|
|
224
|
+
xlabel, ylabel = _axis_labels(kind)
|
|
225
|
+
ax.set_xlabel(xlabel)
|
|
226
|
+
ax.set_ylabel(ylabel)
|
|
227
|
+
ax.tick_params(direction="in")
|
|
228
|
+
|
|
229
|
+
if len(axes):
|
|
230
|
+
axes[0].legend(
|
|
231
|
+
loc="upper center",
|
|
232
|
+
bbox_to_anchor=(0.5, 1.18),
|
|
233
|
+
ncol=3,
|
|
234
|
+
frameon=False,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if fn_out is not None:
|
|
238
|
+
plt.savefig(fn_out, bbox_inches="tight")
|
|
239
|
+
plt.close(fig)
|
|
240
|
+
else:
|
|
241
|
+
plt.show()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def plot_corner(
|
|
245
|
+
samples: InferenceResults | ArrayLike,
|
|
246
|
+
labels: Optional[Sequence[str]] = None,
|
|
247
|
+
*,
|
|
248
|
+
quantiles: Sequence[float] = (0.16, 0.5, 0.84),
|
|
249
|
+
figsize: float = 2.6,
|
|
250
|
+
cmap: Any = "Reds",
|
|
251
|
+
levels: int = 5,
|
|
252
|
+
scatter_alpha: float = 0.15,
|
|
253
|
+
fn_out: Optional[PathLike] = None,
|
|
254
|
+
) -> None:
|
|
255
|
+
sample_source = samples
|
|
256
|
+
samples = _coerce_samples(samples)
|
|
257
|
+
if samples.ndim != 2:
|
|
258
|
+
raise ValueError("plot_corner expects samples with shape (n_samples, n_dim).")
|
|
259
|
+
|
|
260
|
+
n_dim = samples.shape[1]
|
|
261
|
+
if labels is None:
|
|
262
|
+
if isinstance(sample_source, InferenceResults):
|
|
263
|
+
labels = list(sample_source.labels)
|
|
264
|
+
else:
|
|
265
|
+
labels = [f"theta_{i}" for i in range(n_dim)]
|
|
266
|
+
elif len(labels) != n_dim:
|
|
267
|
+
raise ValueError("labels must match the posterior sample dimension.")
|
|
268
|
+
elif isinstance(sample_source, InferenceResults):
|
|
269
|
+
labels = _expand_short_labels(labels, sample_source.labels)
|
|
270
|
+
|
|
271
|
+
labels = [_wrap_label(label) for label in labels]
|
|
272
|
+
base_cmap = plt.get_cmap(cmap)
|
|
273
|
+
colors = base_cmap(np.linspace(0, 1, max(levels, 2)))
|
|
274
|
+
colors[0] = np.array([1.0, 1.0, 1.0, 0.0])
|
|
275
|
+
contour_cmap = ListedColormap(colors)
|
|
276
|
+
fig, axes = plt.subplots(
|
|
277
|
+
n_dim,
|
|
278
|
+
n_dim,
|
|
279
|
+
figsize=(figsize * n_dim, figsize * n_dim),
|
|
280
|
+
gridspec_kw={"wspace": 0.05, "hspace": 0.05},
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
limits = [(samples[:, i].min(), samples[:, i].max()) for i in range(n_dim)]
|
|
284
|
+
|
|
285
|
+
for i in range(n_dim):
|
|
286
|
+
for j in range(n_dim):
|
|
287
|
+
ax = axes[i, j]
|
|
288
|
+
if i < j:
|
|
289
|
+
ax.axis("off")
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
if i == j:
|
|
293
|
+
x = np.linspace(*limits[i], 400)
|
|
294
|
+
kde = gaussian_kde(samples[:, i])
|
|
295
|
+
density = kde(x)
|
|
296
|
+
ax.plot(x, density, color="k", lw=2.5)
|
|
297
|
+
ax.fill_between(x, 0, density, color="0.75", alpha=0.7)
|
|
298
|
+
if quantiles:
|
|
299
|
+
q_values = np.quantile(samples[:, i], quantiles)
|
|
300
|
+
for q in q_values:
|
|
301
|
+
ax.axvline(q, color="k", ls="--", lw=1.3)
|
|
302
|
+
if len(q_values) == 3:
|
|
303
|
+
median = q_values[1]
|
|
304
|
+
lower = median - q_values[0]
|
|
305
|
+
upper = q_values[2] - median
|
|
306
|
+
ax.set_title(
|
|
307
|
+
(
|
|
308
|
+
f"{labels[i]}\n"
|
|
309
|
+
f"{median:.3f}\n"
|
|
310
|
+
f"(+{upper:.3f} / -{lower:.3f})"
|
|
311
|
+
),
|
|
312
|
+
fontsize=12,
|
|
313
|
+
)
|
|
314
|
+
ax.set_xlim(*limits[i])
|
|
315
|
+
ax.set_yticks([])
|
|
316
|
+
ax.tick_params(axis="y", left=False, labelleft=False)
|
|
317
|
+
else:
|
|
318
|
+
x = samples[::10, j]
|
|
319
|
+
y = samples[::10, i]
|
|
320
|
+
ax.scatter(
|
|
321
|
+
x, y, s=5, lw=0, alpha=scatter_alpha, color="k", rasterized=True
|
|
322
|
+
)
|
|
323
|
+
try:
|
|
324
|
+
kde = gaussian_kde(np.vstack([x, y]))
|
|
325
|
+
xi, yi = np.mgrid[
|
|
326
|
+
limits[j][0]:limits[j][1]:100j,
|
|
327
|
+
limits[i][0]:limits[i][1]:100j,
|
|
328
|
+
]
|
|
329
|
+
zi = kde(np.vstack([xi.ravel(), yi.ravel()])).reshape(xi.shape)
|
|
330
|
+
contour_levels = np.linspace(zi.min(), zi.max(), levels)
|
|
331
|
+
ax.contourf(
|
|
332
|
+
xi, yi, zi, levels=contour_levels, cmap=contour_cmap
|
|
333
|
+
)
|
|
334
|
+
ax.contour(
|
|
335
|
+
xi, yi, zi, levels=contour_levels, colors="k", linewidths=0.8
|
|
336
|
+
)
|
|
337
|
+
except np.linalg.LinAlgError:
|
|
338
|
+
pass
|
|
339
|
+
ax.set_xlim(*limits[j])
|
|
340
|
+
ax.set_ylim(*limits[i])
|
|
341
|
+
|
|
342
|
+
ax.xaxis.set_major_locator(MaxNLocator(nbins=4))
|
|
343
|
+
ax.yaxis.set_major_locator(MaxNLocator(nbins=4))
|
|
344
|
+
ax.tick_params(direction="in", top=True, right=True, labelsize=11)
|
|
345
|
+
|
|
346
|
+
if i == n_dim - 1:
|
|
347
|
+
ax.set_xlabel(labels[j], fontsize=12)
|
|
348
|
+
else:
|
|
349
|
+
ax.set_xticklabels([])
|
|
350
|
+
|
|
351
|
+
if j == 0 and i > 0:
|
|
352
|
+
ax.set_ylabel(labels[i], fontsize=12)
|
|
353
|
+
elif i != j:
|
|
354
|
+
ax.set_yticklabels([])
|
|
355
|
+
|
|
356
|
+
fig.align_labels()
|
|
357
|
+
|
|
358
|
+
if fn_out is not None:
|
|
359
|
+
plt.savefig(fn_out, bbox_inches="tight")
|
|
360
|
+
plt.close(fig)
|
|
361
|
+
else:
|
|
362
|
+
plt.show()
|
bff/qoi/__init__.py
ADDED
bff/qoi/analysis.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import gc
|
|
2
|
+
import multiprocessing as mp
|
|
3
|
+
import warnings
|
|
4
|
+
from functools import partial
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Iterable, Sequence
|
|
7
|
+
|
|
8
|
+
import MDAnalysis as mda
|
|
9
|
+
|
|
10
|
+
from ..domain.trainset import TrajectorySet
|
|
11
|
+
from ..io.logs import Logger
|
|
12
|
+
from ..io.progress import iter_progress
|
|
13
|
+
from ..tools import _normalized_dimensions
|
|
14
|
+
from ..topology import prepare_universe
|
|
15
|
+
from .data import QoI
|
|
16
|
+
from .routines import RuntimeRoutine, run_analysis_routines
|
|
17
|
+
|
|
18
|
+
PathLike = str | Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
warnings.filterwarnings(
|
|
22
|
+
"ignore", category=DeprecationWarning, module="MDAnalysis.coordinates.DCD"
|
|
23
|
+
)
|
|
24
|
+
warnings.filterwarnings(
|
|
25
|
+
"ignore", category=UserWarning, module="MDAnalysis.coordinates.XDR"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _cleanup_universe(universe: mda.Universe | None) -> None:
|
|
30
|
+
"""Release trajectory resources after one analysis task."""
|
|
31
|
+
if universe is not None:
|
|
32
|
+
trajectory = getattr(universe, "trajectory", None)
|
|
33
|
+
if trajectory is not None:
|
|
34
|
+
close = getattr(trajectory, "close", None)
|
|
35
|
+
if callable(close):
|
|
36
|
+
close()
|
|
37
|
+
raw_trajectory = getattr(universe, "_trajectory", None)
|
|
38
|
+
if raw_trajectory is not None and raw_trajectory is not trajectory:
|
|
39
|
+
close = getattr(raw_trajectory, "close", None)
|
|
40
|
+
if callable(close):
|
|
41
|
+
close()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _prepare_universe(
|
|
45
|
+
fn_topol: PathLike,
|
|
46
|
+
fn_coord: PathLike,
|
|
47
|
+
fn_trj: PathLike,
|
|
48
|
+
*,
|
|
49
|
+
start: int,
|
|
50
|
+
stop: int | None,
|
|
51
|
+
step: int,
|
|
52
|
+
in_memory: bool,
|
|
53
|
+
) -> mda.Universe:
|
|
54
|
+
"""Prepare one MDAnalysis universe for trajectory analysis."""
|
|
55
|
+
universe = prepare_universe(str(fn_topol), str(fn_coord), dt=1)
|
|
56
|
+
default_dimensions = _normalized_dimensions(universe.dimensions)
|
|
57
|
+
universe.load_new(str(fn_trj))
|
|
58
|
+
universe._bff_default_dimensions = default_dimensions
|
|
59
|
+
if in_memory:
|
|
60
|
+
universe.transfer_to_memory(start=start, stop=stop, step=step)
|
|
61
|
+
if default_dimensions is not None:
|
|
62
|
+
for ts in universe.trajectory:
|
|
63
|
+
if ts.dimensions is None:
|
|
64
|
+
ts.dimensions = default_dimensions
|
|
65
|
+
return universe
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def analyze_trajectory_set(
|
|
69
|
+
trajectory_set: TrajectorySet,
|
|
70
|
+
*,
|
|
71
|
+
routines_by_system: Sequence[tuple[RuntimeRoutine, ...]],
|
|
72
|
+
start: int = 0,
|
|
73
|
+
stop: int | None = None,
|
|
74
|
+
step: int = 1,
|
|
75
|
+
in_memory: bool = False,
|
|
76
|
+
) -> list[dict[str, QoI]]:
|
|
77
|
+
"""Analyze all trajectories that belong to one sample or reference set."""
|
|
78
|
+
if len(routines_by_system) != len(trajectory_set.fn_trj):
|
|
79
|
+
raise ValueError(
|
|
80
|
+
"Analysis routine count must match the number of trajectories in the set."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
results: list[dict[str, QoI]] = []
|
|
84
|
+
routine_start = 0 if in_memory else start
|
|
85
|
+
routine_stop = None if in_memory else stop
|
|
86
|
+
routine_step = 1 if in_memory else step
|
|
87
|
+
for fn_topol, fn_coord, fn_trj, routines in zip(
|
|
88
|
+
trajectory_set.fn_topol,
|
|
89
|
+
trajectory_set.fn_coord,
|
|
90
|
+
trajectory_set.fn_trj,
|
|
91
|
+
routines_by_system,
|
|
92
|
+
):
|
|
93
|
+
universe = None
|
|
94
|
+
try:
|
|
95
|
+
universe = _prepare_universe(
|
|
96
|
+
fn_topol,
|
|
97
|
+
fn_coord,
|
|
98
|
+
fn_trj,
|
|
99
|
+
start=start,
|
|
100
|
+
stop=stop,
|
|
101
|
+
step=step,
|
|
102
|
+
in_memory=in_memory,
|
|
103
|
+
)
|
|
104
|
+
result = run_analysis_routines(
|
|
105
|
+
routines,
|
|
106
|
+
universe=universe,
|
|
107
|
+
start=routine_start,
|
|
108
|
+
stop=routine_stop,
|
|
109
|
+
step=routine_step,
|
|
110
|
+
)
|
|
111
|
+
if not result:
|
|
112
|
+
raise ValueError("Analysis routine returned no QoI outputs.")
|
|
113
|
+
results.append(result)
|
|
114
|
+
finally:
|
|
115
|
+
_cleanup_universe(universe)
|
|
116
|
+
return results
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _maybe_collect_garbage(
|
|
120
|
+
completed: int,
|
|
121
|
+
total: int,
|
|
122
|
+
*,
|
|
123
|
+
gc_collect: bool,
|
|
124
|
+
progress_stride: int,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Run the garbage collector at coarse progress intervals when requested."""
|
|
127
|
+
if not gc_collect:
|
|
128
|
+
return
|
|
129
|
+
if (completed % progress_stride == 0) or (completed == total):
|
|
130
|
+
gc.collect()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _iter_analyzed_sets(
|
|
134
|
+
trajectory_sets: Sequence[TrajectorySet],
|
|
135
|
+
*,
|
|
136
|
+
analyze_one: Any,
|
|
137
|
+
workers: int,
|
|
138
|
+
maxtasksperchild: int,
|
|
139
|
+
) -> Iterable[list[dict[str, QoI]]]:
|
|
140
|
+
"""Yield analyzed trajectory sets from the serial or multiprocessing path."""
|
|
141
|
+
if workers <= 1:
|
|
142
|
+
yield from (analyze_one(trajectory_set) for trajectory_set in trajectory_sets)
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
context = mp.get_context()
|
|
146
|
+
with context.Pool(workers, maxtasksperchild=maxtasksperchild) as pool:
|
|
147
|
+
yield from pool.imap(
|
|
148
|
+
analyze_one,
|
|
149
|
+
trajectory_sets,
|
|
150
|
+
chunksize=1,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def analyze_trajectory_sets(
|
|
155
|
+
trajectory_sets: Sequence[TrajectorySet],
|
|
156
|
+
*,
|
|
157
|
+
routines_by_system: Sequence[tuple[RuntimeRoutine, ...]],
|
|
158
|
+
start: int = 0,
|
|
159
|
+
stop: int | None = None,
|
|
160
|
+
step: int = 1,
|
|
161
|
+
workers: int = 1,
|
|
162
|
+
progress_stride: int = 10,
|
|
163
|
+
progress_label: str = "Trajectory QoI",
|
|
164
|
+
logger: Logger | None = None,
|
|
165
|
+
in_memory: bool = False,
|
|
166
|
+
gc_collect: bool = False,
|
|
167
|
+
maxtasksperchild: int = 100,
|
|
168
|
+
) -> list[list[dict[str, QoI]]]:
|
|
169
|
+
"""Analyze multiple trajectory sets with one shared routine setup."""
|
|
170
|
+
logger = logger or Logger(progress_label)
|
|
171
|
+
n_sets = len(trajectory_sets)
|
|
172
|
+
if n_sets == 0:
|
|
173
|
+
return []
|
|
174
|
+
|
|
175
|
+
analyze_one = partial(
|
|
176
|
+
analyze_trajectory_set,
|
|
177
|
+
routines_by_system=routines_by_system,
|
|
178
|
+
start=start,
|
|
179
|
+
stop=stop,
|
|
180
|
+
step=step,
|
|
181
|
+
in_memory=in_memory,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
workers = mp.cpu_count() if workers == -1 else workers
|
|
185
|
+
completed_sets = _iter_analyzed_sets(
|
|
186
|
+
trajectory_sets,
|
|
187
|
+
analyze_one=analyze_one,
|
|
188
|
+
workers=workers,
|
|
189
|
+
maxtasksperchild=maxtasksperchild,
|
|
190
|
+
)
|
|
191
|
+
qoi = []
|
|
192
|
+
for completed, result in enumerate(
|
|
193
|
+
iter_progress(
|
|
194
|
+
completed_sets,
|
|
195
|
+
total=n_sets,
|
|
196
|
+
stride=progress_stride,
|
|
197
|
+
logger=logger,
|
|
198
|
+
label=progress_label,
|
|
199
|
+
),
|
|
200
|
+
start=1,
|
|
201
|
+
):
|
|
202
|
+
qoi.append(result)
|
|
203
|
+
_maybe_collect_garbage(
|
|
204
|
+
completed,
|
|
205
|
+
n_sets,
|
|
206
|
+
gc_collect=gc_collect,
|
|
207
|
+
progress_stride=progress_stride,
|
|
208
|
+
)
|
|
209
|
+
return qoi
|