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.
Files changed (55) hide show
  1. bff/__init__.py +30 -0
  2. bff/bayes/__init__.py +11 -0
  3. bff/bayes/gaussian_process.py +285 -0
  4. bff/bayes/kernels.py +71 -0
  5. bff/bayes/learning.py +484 -0
  6. bff/bayes/likelihoods.py +142 -0
  7. bff/bayes/posterior.py +62 -0
  8. bff/bayes/priors.py +179 -0
  9. bff/bayes/results.py +327 -0
  10. bff/bayes/utils.py +333 -0
  11. bff/cli.py +254 -0
  12. bff/data.py +93 -0
  13. bff/domain/__init__.py +12 -0
  14. bff/domain/bias.py +149 -0
  15. bff/domain/specs.py +347 -0
  16. bff/domain/trainset.py +201 -0
  17. bff/io/__init__.py +12 -0
  18. bff/io/colvars.py +186 -0
  19. bff/io/commands.py +20 -0
  20. bff/io/cp2k.py +330 -0
  21. bff/io/logs.py +213 -0
  22. bff/io/mdp.py +98 -0
  23. bff/io/plumed.py +184 -0
  24. bff/io/progress.py +74 -0
  25. bff/io/schedulers.py +118 -0
  26. bff/io/utils.py +208 -0
  27. bff/mcmc/__init__.py +3 -0
  28. bff/mcmc/convergence.py +168 -0
  29. bff/mcmc/proposal.py +254 -0
  30. bff/mcmc/sampler.py +481 -0
  31. bff/plotting.py +362 -0
  32. bff/qoi/__init__.py +9 -0
  33. bff/qoi/analysis.py +209 -0
  34. bff/qoi/data.py +169 -0
  35. bff/qoi/hbonds.py +255 -0
  36. bff/qoi/rdf.py +108 -0
  37. bff/qoi/routines.py +272 -0
  38. bff/tools.py +168 -0
  39. bff/topology.py +581 -0
  40. bff/workflows/__init__.py +0 -0
  41. bff/workflows/configs.py +1147 -0
  42. bff/workflows/learn.py +77 -0
  43. bff/workflows/md.py +326 -0
  44. bff/workflows/prepare.py +788 -0
  45. bff/workflows/qoi.py +260 -0
  46. bff/workflows/runsims.py +620 -0
  47. bff/workflows/simulate.py +28 -0
  48. bff/workflows/train.py +69 -0
  49. bff/workflows/validate.py +29 -0
  50. bfflearn-0.1.0.dev1.dist-info/METADATA +913 -0
  51. bfflearn-0.1.0.dev1.dist-info/RECORD +55 -0
  52. bfflearn-0.1.0.dev1.dist-info/WHEEL +5 -0
  53. bfflearn-0.1.0.dev1.dist-info/entry_points.txt +2 -0
  54. bfflearn-0.1.0.dev1.dist-info/licenses/LICENSE +674 -0
  55. 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
@@ -0,0 +1,9 @@
1
+ from . import analysis, data, hbonds, rdf, routines
2
+
3
+ __all__ = [
4
+ "data",
5
+ "hbonds",
6
+ "rdf",
7
+ "routines",
8
+ "analysis",
9
+ ]
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