polyphys 1.0.0__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.
- polyphys/__init__.py +7 -0
- polyphys/__version__.py +22 -0
- polyphys/analyze/__init__.py +51 -0
- polyphys/analyze/clusters.py +1738 -0
- polyphys/analyze/correlations.py +995 -0
- polyphys/analyze/density.py +1138 -0
- polyphys/analyze/measurer.py +1338 -0
- polyphys/analyze/pipeline.py +1868 -0
- polyphys/analyze/stats.py +810 -0
- polyphys/analyze/sumrule.py +1159 -0
- polyphys/manage/__init__.py +11 -0
- polyphys/manage/organizer.py +2188 -0
- polyphys/manage/parser.py +1264 -0
- polyphys/manage/types.py +171 -0
- polyphys/manage/utils.py +777 -0
- polyphys/probe/__init__.py +25 -0
- polyphys/probe/binders.py +711 -0
- polyphys/probe/chain_size.py +583 -0
- polyphys/probe/core.py +437 -0
- polyphys/probe/histograms.py +863 -0
- polyphys/probe/pairs.py +555 -0
- polyphys/simulate/__init__.py +48 -0
- polyphys/simulate/build.py +1231 -0
- polyphys/simulate/core.py +237 -0
- polyphys/simulate/data.py +977 -0
- polyphys/simulate/dump.py +752 -0
- polyphys/simulate/log.py +788 -0
- polyphys/spec.py +1359 -0
- polyphys/tests/analyze/test_clusters.py +1058 -0
- polyphys/tests/analyze/test_correlations.py +708 -0
- polyphys/tests/analyze/test_density.py +813 -0
- polyphys/tests/analyze/test_measurer.py +785 -0
- polyphys/tests/analyze/test_pipeline.py +1881 -0
- polyphys/tests/analyze/test_stats.py +732 -0
- polyphys/tests/analyze/test_sumrule.py +917 -0
- polyphys/tests/conftest.py +753 -0
- polyphys/tests/data/lammps_build/make_starts.py +70 -0
- polyphys/tests/manage/test_organizer.py +1495 -0
- polyphys/tests/manage/test_parser.py +1222 -0
- polyphys/tests/manage/test_utils.py +498 -0
- polyphys/tests/probe/conftest.py +407 -0
- polyphys/tests/probe/test_binders.py +686 -0
- polyphys/tests/probe/test_chain_size.py +557 -0
- polyphys/tests/probe/test_core.py +391 -0
- polyphys/tests/probe/test_histograms.py +779 -0
- polyphys/tests/probe/test_pairs.py +475 -0
- polyphys/tests/simulate/test_build.py +1042 -0
- polyphys/tests/simulate/test_core.py +106 -0
- polyphys/tests/simulate/test_data.py +858 -0
- polyphys/tests/simulate/test_dump.py +487 -0
- polyphys/tests/simulate/test_log.py +667 -0
- polyphys/tests/strategies.py +120 -0
- polyphys/tests/synthetic.py +410 -0
- polyphys/tests/test_conftest.py +414 -0
- polyphys/tests/test_docs.py +82 -0
- polyphys/tests/test_packaging.py +97 -0
- polyphys/tests/test_spec.py +772 -0
- polyphys/tests/viz/conftest.py +32 -0
- polyphys/tests/viz/test_colors.py +113 -0
- polyphys/tests/viz/test_helpers.py +292 -0
- polyphys/viz/__init__.py +15 -0
- polyphys/viz/colors.py +204 -0
- polyphys/viz/helpers.py +549 -0
- polyphys-1.0.0.dist-info/METADATA +339 -0
- polyphys-1.0.0.dist-info/RECORD +69 -0
- polyphys-1.0.0.dist-info/WHEEL +5 -0
- polyphys-1.0.0.dist-info/licenses/AUTHORS.rst +13 -0
- polyphys-1.0.0.dist-info/licenses/LICENSE +22 -0
- polyphys-1.0.0.dist-info/top_level.txt +1 -0
polyphys/probe/core.py
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"""
|
|
2
|
+
==========================================================
|
|
3
|
+
:mod:`polyphys.probe.core`
|
|
4
|
+
==========================================================
|
|
5
|
+
|
|
6
|
+
The :mod:`polyphys.probe.core` module holds what every probe shares: how a
|
|
7
|
+
study's LAMMPS data file and dump are opened as one MDAnalysis
|
|
8
|
+
:class:`~MDAnalysis.core.universe.Universe`, and the *stamps* file, the
|
|
9
|
+
one-row table of a probed artifact's lineage names, parser attributes, and
|
|
10
|
+
frame count that the organizer later combines across a space.
|
|
11
|
+
|
|
12
|
+
The research code opened every universe with the same four arguments and
|
|
13
|
+
wrote the stamps with the same columns in each of its probe functions;
|
|
14
|
+
:func:`make_universe` and :func:`write_stamps` are those two pieces, written
|
|
15
|
+
once.
|
|
16
|
+
|
|
17
|
+
Contents
|
|
18
|
+
========
|
|
19
|
+
|
|
20
|
+
- :func:`make_universe`, a universe of a LAMMPS data file and dump in the
|
|
21
|
+
convention of the studies.
|
|
22
|
+
- :func:`write_stamps`, the stamps CSV of one probed artifact.
|
|
23
|
+
- :data:`UNIVERSE_KWARGS`, the fixed arguments of :func:`make_universe`.
|
|
24
|
+
|
|
25
|
+
Dependencies
|
|
26
|
+
============
|
|
27
|
+
- Python standard library: `copy`, `multiprocessing`, `os`, `pathlib`,
|
|
28
|
+
`types`, `typing`.
|
|
29
|
+
- `MDAnalysis <https://docs.mdanalysis.org/>`_ 2.8 or later, through the
|
|
30
|
+
``mda`` extra.
|
|
31
|
+
- `pandas <https://pandas.pydata.org/>`_.
|
|
32
|
+
- :mod:`polyphys.manage.parser`: :class:`~polyphys.manage.parser.ParserBase`.
|
|
33
|
+
|
|
34
|
+
Every probe analysis computes each part of a split run on its own shallow
|
|
35
|
+
copy of itself (``_compute_part``), so that the ``serial`` backend, which
|
|
36
|
+
hands every part the same analysis object, keeps each part's ``results``,
|
|
37
|
+
``frames``, and ``times`` apart, as the ``multiprocessing`` and ``dask``
|
|
38
|
+
backends do by pickling.
|
|
39
|
+
|
|
40
|
+
Every probe analysis also takes its ``run`` from ``_ProbeAnalysis``, which
|
|
41
|
+
closes and joins the pool of the ``multiprocessing`` backend instead of
|
|
42
|
+
terminating it, so the workers exit normally and release the semaphores
|
|
43
|
+
they made.
|
|
44
|
+
|
|
45
|
+
Examples
|
|
46
|
+
========
|
|
47
|
+
>>> import tempfile
|
|
48
|
+
>>> from polyphys.tests.synthetic import ChainCyl
|
|
49
|
+
>>> parser = ChainCyl('N4r2.5lz10ac1nc0dt0.005bdump1adump1ens1',
|
|
50
|
+
... 'whole', 'bug')
|
|
51
|
+
>>> with tempfile.TemporaryDirectory() as tmp:
|
|
52
|
+
... path = write_stamps(parser, n_frames=5, save_to=tmp)
|
|
53
|
+
... path.name
|
|
54
|
+
'N4r2.5lz10ac1nc0dt0.005bdump1adump1ens1-bug-stamps.csv'
|
|
55
|
+
|
|
56
|
+
.. versionadded:: 0.7.0
|
|
57
|
+
"""
|
|
58
|
+
# Annotations stay strings, so the docs build, which mocks MDAnalysis,
|
|
59
|
+
# never evaluates a union such as ``str | BackendBase``.
|
|
60
|
+
from __future__ import annotations
|
|
61
|
+
|
|
62
|
+
import copy
|
|
63
|
+
import os
|
|
64
|
+
from multiprocessing import Pool
|
|
65
|
+
from pathlib import Path
|
|
66
|
+
from types import MappingProxyType
|
|
67
|
+
from typing import Any, Callable, Mapping
|
|
68
|
+
|
|
69
|
+
import MDAnalysis as mda
|
|
70
|
+
import numpy as np
|
|
71
|
+
import pandas as pd
|
|
72
|
+
from MDAnalysis.analysis.backends import BackendBase, BackendMultiprocessing
|
|
73
|
+
from MDAnalysis.analysis.base import AnalysisBase
|
|
74
|
+
from MDAnalysis.analysis.results import Results
|
|
75
|
+
|
|
76
|
+
from ..manage.parser import ParserBase
|
|
77
|
+
from ..manage.types import Pathish
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
'UNIVERSE_KWARGS',
|
|
81
|
+
'make_universe',
|
|
82
|
+
'write_stamps',
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
#: The fixed arguments of :func:`make_universe`: a LAMMPS data file as the
|
|
86
|
+
#: topology, a LAMMPS dump as the trajectory with its coordinates read as the
|
|
87
|
+
#: wrapped, unscaled ``x y z`` columns, and the ``Atoms`` lines of the data
|
|
88
|
+
#: file read as ``id resid type x y z``, the ``bond``, ``angle``, and
|
|
89
|
+
#: ``molecular`` atom styles whose molecule ID becomes the residue ID that
|
|
90
|
+
#: a selection such as ``'resid 1'`` picks.
|
|
91
|
+
#:
|
|
92
|
+
#: .. versionadded:: 0.7.0
|
|
93
|
+
UNIVERSE_KWARGS: Mapping[str, str] = MappingProxyType({
|
|
94
|
+
'topology_format': 'DATA',
|
|
95
|
+
'format': 'LAMMPSDUMP',
|
|
96
|
+
'lammps_coordinate_convention': 'unscaled',
|
|
97
|
+
'atom_style': 'id resid type x y z',
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def make_universe(
|
|
102
|
+
topology: Pathish,
|
|
103
|
+
trajectory: Pathish,
|
|
104
|
+
**kwargs: Any,
|
|
105
|
+
) -> mda.Universe:
|
|
106
|
+
"""
|
|
107
|
+
Open a LAMMPS data file and dump as one MDAnalysis universe, as the
|
|
108
|
+
studies read them.
|
|
109
|
+
|
|
110
|
+
.. versionadded:: 0.7.0
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
topology : str or pathlib.Path
|
|
115
|
+
The LAMMPS data file, whose ``Atoms`` lines are ``atom-ID
|
|
116
|
+
molecule-ID atom-type x y z`` and whose ``Masses`` section gives
|
|
117
|
+
every atom type a mass. The masses weight the radius of gyration
|
|
118
|
+
and the gyration tensor; without them MDAnalysis cannot form the
|
|
119
|
+
tensor.
|
|
120
|
+
trajectory : str or pathlib.Path
|
|
121
|
+
The LAMMPS dump, in the ``atom`` style with unscaled, wrapped
|
|
122
|
+
coordinates (``ITEM: ATOMS id type x y z``). MDAnalysis sorts the
|
|
123
|
+
atoms of each frame by ``id``, so ``positions`` are in atom-ID
|
|
124
|
+
order, the order along the chain.
|
|
125
|
+
**kwargs
|
|
126
|
+
Further arguments of :class:`~MDAnalysis.core.universe.Universe`,
|
|
127
|
+
such as ``dt``, the time between frames, or ``in_memory``. The
|
|
128
|
+
keys of :data:`UNIVERSE_KWARGS` are fixed and may not be given.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
MDAnalysis.core.universe.Universe
|
|
133
|
+
The universe, with the trajectory at its first frame.
|
|
134
|
+
|
|
135
|
+
Raises
|
|
136
|
+
------
|
|
137
|
+
ValueError
|
|
138
|
+
If `kwargs` names a key of :data:`UNIVERSE_KWARGS`.
|
|
139
|
+
|
|
140
|
+
Warns
|
|
141
|
+
-----
|
|
142
|
+
UserWarning
|
|
143
|
+
From the dump reader, ``Reader has no dt information``, when `dt` is
|
|
144
|
+
not given: the frame times are then the dump's time steps.
|
|
145
|
+
|
|
146
|
+
Notes
|
|
147
|
+
-----
|
|
148
|
+
The dump's box is read per frame; the data file's box is not used after
|
|
149
|
+
the topology is built. Opening a universe reads the whole data file and
|
|
150
|
+
indexes the dump, :math:`O(N + F)` time for :math:`N` atoms and
|
|
151
|
+
:math:`F` frames, and holds the topology, :math:`O(N)` space; frames
|
|
152
|
+
are read one at a time unless `in_memory` is set.
|
|
153
|
+
|
|
154
|
+
Examples
|
|
155
|
+
--------
|
|
156
|
+
A two-bead chain in a one-frame dump:
|
|
157
|
+
|
|
158
|
+
>>> import tempfile
|
|
159
|
+
>>> import numpy as np
|
|
160
|
+
>>> from polyphys.simulate.core import Box
|
|
161
|
+
>>> from polyphys.simulate.data import DataFile, write_data
|
|
162
|
+
>>> pair = DataFile(
|
|
163
|
+
... atom_style='bond', box=Box(lo=np.zeros(3), hi=np.full(3, 5.0)),
|
|
164
|
+
... ids=np.array([1, 2]), types=np.array([1, 1]),
|
|
165
|
+
... positions=np.array([[2.0, 2.5, 2.5], [3.0, 2.5, 2.5]]),
|
|
166
|
+
... n_atom_types=1, masses=np.array([1.0]), mol_ids=np.array([1, 1]),
|
|
167
|
+
... bonds=np.array([[1, 1, 2]]), n_bond_types=1)
|
|
168
|
+
>>> dump = '''ITEM: TIMESTEP
|
|
169
|
+
... 0
|
|
170
|
+
... ITEM: NUMBER OF ATOMS
|
|
171
|
+
... 2
|
|
172
|
+
... ITEM: BOX BOUNDS pp pp pp
|
|
173
|
+
... 0 5
|
|
174
|
+
... 0 5
|
|
175
|
+
... 0 5
|
|
176
|
+
... ITEM: ATOMS id type x y z
|
|
177
|
+
... 2 1 3.0 2.5 2.5
|
|
178
|
+
... 1 1 2.0 2.5 2.5
|
|
179
|
+
... '''
|
|
180
|
+
>>> with tempfile.TemporaryDirectory() as tmp:
|
|
181
|
+
... data = write_data(pair, Path(tmp) / 'pair.data')
|
|
182
|
+
... _ = (Path(tmp) / 'pair.lammpstrj').write_text(dump)
|
|
183
|
+
... universe = make_universe(data, Path(tmp) / 'pair.lammpstrj',
|
|
184
|
+
... dt=1.0)
|
|
185
|
+
... chain = universe.select_atoms('resid 1')
|
|
186
|
+
... chain.n_atoms, universe.trajectory.n_frames, chain.ids.tolist()
|
|
187
|
+
(2, 1, [1, 2])
|
|
188
|
+
"""
|
|
189
|
+
fixed = sorted(set(kwargs) & set(UNIVERSE_KWARGS))
|
|
190
|
+
if fixed:
|
|
191
|
+
raise ValueError(
|
|
192
|
+
f"{fixed} are fixed by make_universe and cannot be given.")
|
|
193
|
+
return mda.Universe(os.fspath(topology), os.fspath(trajectory),
|
|
194
|
+
**UNIVERSE_KWARGS, **kwargs)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def write_stamps(
|
|
198
|
+
parser: ParserBase,
|
|
199
|
+
n_frames: int,
|
|
200
|
+
save_to: Pathish,
|
|
201
|
+
) -> Path:
|
|
202
|
+
"""
|
|
203
|
+
Write the stamps of one probed artifact: a one-row CSV of its lineage
|
|
204
|
+
names, its parser attributes, and its number of frames.
|
|
205
|
+
|
|
206
|
+
.. versionadded:: 0.7.0
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
parser : :class:`~polyphys.manage.parser.ParserBase`
|
|
211
|
+
The artifact's parser, at the ``segment`` or ``whole`` lineage,
|
|
212
|
+
built from the trajectory's name or path.
|
|
213
|
+
n_frames : int
|
|
214
|
+
The number of frames probed, zero or more.
|
|
215
|
+
save_to : str or pathlib.Path
|
|
216
|
+
An existing directory; the file is
|
|
217
|
+
``<name>-<group>-stamps.csv`` in it, with the parser's `name` and
|
|
218
|
+
`group`.
|
|
219
|
+
|
|
220
|
+
Returns
|
|
221
|
+
-------
|
|
222
|
+
pathlib.Path
|
|
223
|
+
The file written.
|
|
224
|
+
|
|
225
|
+
Raises
|
|
226
|
+
------
|
|
227
|
+
ValueError
|
|
228
|
+
If `n_frames` is not a non-negative integer.
|
|
229
|
+
|
|
230
|
+
Notes
|
|
231
|
+
-----
|
|
232
|
+
The columns are, in order, the lineage names of
|
|
233
|
+
:attr:`~polyphys.manage.parser.ParserBase.lineage_genealogy`, the
|
|
234
|
+
``group``, every name in
|
|
235
|
+
:attr:`~polyphys.manage.parser.ParserBase.attributes`, and
|
|
236
|
+
``n_frames``; the row holds the parser's values. The research code's
|
|
237
|
+
``stamps_report`` wrote the same columns without ``group``, which
|
|
238
|
+
:func:`~polyphys.manage.organizer.children_stamps` then takes from the
|
|
239
|
+
file name. Time and space are :math:`O(a)` for :math:`a` attributes.
|
|
240
|
+
|
|
241
|
+
Examples
|
|
242
|
+
--------
|
|
243
|
+
>>> import tempfile
|
|
244
|
+
>>> from polyphys.tests.synthetic import ChainCyl
|
|
245
|
+
>>> parser = ChainCyl(
|
|
246
|
+
... 'N4r2.5lz10ac1nc0dt0.005bdump1adump1ens1.j01', 'segment',
|
|
247
|
+
... 'bug')
|
|
248
|
+
>>> with tempfile.TemporaryDirectory() as tmp:
|
|
249
|
+
... stamps = pd.read_csv(write_stamps(parser, 3, tmp))
|
|
250
|
+
>>> stamps.columns[:3].tolist(), stamps.columns[-2:].tolist()
|
|
251
|
+
(['segment', 'whole', 'ensemble_long'], ['rho_bulk_c', 'n_frames'])
|
|
252
|
+
>>> int(stamps.loc[0, 'nmon']), int(stamps.loc[0, 'n_frames'])
|
|
253
|
+
(4, 3)
|
|
254
|
+
"""
|
|
255
|
+
if isinstance(n_frames, bool) or not isinstance(n_frames, int) \
|
|
256
|
+
or n_frames < 0:
|
|
257
|
+
raise ValueError(
|
|
258
|
+
f"'n_frames' must be a non-negative integer, not {n_frames!r}.")
|
|
259
|
+
columns = [*parser.lineage_genealogy, 'group', *parser.attributes,
|
|
260
|
+
'n_frames']
|
|
261
|
+
values = [
|
|
262
|
+
*(getattr(parser, lineage) for lineage in parser.lineage_genealogy),
|
|
263
|
+
parser.group,
|
|
264
|
+
*(getattr(parser, attr) for attr in parser.attributes),
|
|
265
|
+
n_frames,
|
|
266
|
+
]
|
|
267
|
+
stamps = pd.DataFrame([values], columns=columns)
|
|
268
|
+
path = Path(save_to) / f"{parser.name}-{parser.group}-stamps.csv"
|
|
269
|
+
stamps.to_csv(path, index=False)
|
|
270
|
+
return path
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _compute_part(
|
|
274
|
+
analysis: AnalysisBase,
|
|
275
|
+
indexed_frames: np.ndarray,
|
|
276
|
+
verbose: bool | None = None,
|
|
277
|
+
*,
|
|
278
|
+
progressbar_kwargs: dict[str, Any] | None = None,
|
|
279
|
+
) -> AnalysisBase:
|
|
280
|
+
"""
|
|
281
|
+
Compute one part of a run on a shallow copy of `analysis` with empty
|
|
282
|
+
results, and return the copy.
|
|
283
|
+
|
|
284
|
+
MDAnalysis 2.8 and later split a run's frames into parts and call
|
|
285
|
+
``_compute`` once per part, then merge the returned objects' ``results``,
|
|
286
|
+
``frames``, and ``times`` with the analysis's ``_get_aggregator``. The
|
|
287
|
+
``multiprocessing`` and ``dask`` backends pickle the analysis for each
|
|
288
|
+
part; the ``serial`` backend passes the analysis itself to every part,
|
|
289
|
+
so without a copy each part overwrites the one before it and the merge
|
|
290
|
+
repeats the last part ``n_parts`` times.
|
|
291
|
+
|
|
292
|
+
Parameters
|
|
293
|
+
----------
|
|
294
|
+
analysis : MDAnalysis.analysis.base.AnalysisBase
|
|
295
|
+
The analysis whose ``_compute`` calls this, with its frames set up
|
|
296
|
+
by ``run``.
|
|
297
|
+
indexed_frames : numpy.ndarray
|
|
298
|
+
The part's frames, shape ``(n, 2)``: the run's iteration index and
|
|
299
|
+
the trajectory frame of each.
|
|
300
|
+
verbose : bool or None, default None
|
|
301
|
+
Whether to show a progress bar; ``None`` keeps the analysis's own
|
|
302
|
+
setting.
|
|
303
|
+
progressbar_kwargs : dict or None, default None
|
|
304
|
+
Arguments of :class:`MDAnalysis.lib.log.ProgressBar`.
|
|
305
|
+
|
|
306
|
+
Returns
|
|
307
|
+
-------
|
|
308
|
+
MDAnalysis.analysis.base.AnalysisBase
|
|
309
|
+
The copy, holding this part's ``results``, ``frames``, and
|
|
310
|
+
``times``; `analysis` is unchanged.
|
|
311
|
+
|
|
312
|
+
Notes
|
|
313
|
+
-----
|
|
314
|
+
The copy shares the analysis's atom groups, trajectory, and settings;
|
|
315
|
+
only ``results`` is new, and ``_compute`` rebinds the part's frames,
|
|
316
|
+
times, and sliced trajectory on the copy. The copy costs :math:`O(a)`
|
|
317
|
+
time and space for :math:`a` attributes, independent of the frames and
|
|
318
|
+
atoms.
|
|
319
|
+
|
|
320
|
+
Examples
|
|
321
|
+
--------
|
|
322
|
+
Two parts of a four-frame run, computed serially, stay apart:
|
|
323
|
+
|
|
324
|
+
>>> import MDAnalysis as mda
|
|
325
|
+
>>> from polyphys.probe.chain_size import ChainSize
|
|
326
|
+
>>> frames = np.zeros((4, 2, 3))
|
|
327
|
+
>>> frames[:, 1, 2] = [1.0, 2.0, 3.0, 4.0]
|
|
328
|
+
>>> universe = mda.Universe.empty(2, trajectory=True)
|
|
329
|
+
>>> universe.add_TopologyAttr('masses', np.ones(2))
|
|
330
|
+
>>> universe.load_new(frames, format='MEMORY')
|
|
331
|
+
<Universe with 2 atoms>
|
|
332
|
+
>>> analysis = ChainSize(universe.atoms, ('rfloryT',))
|
|
333
|
+
>>> analysis.run(backend='serial', n_parts=2).results['rfloryT']
|
|
334
|
+
array([1., 2., 3., 4.])
|
|
335
|
+
"""
|
|
336
|
+
part = copy.copy(analysis)
|
|
337
|
+
part.results = Results()
|
|
338
|
+
return AnalysisBase._compute(part, indexed_frames, verbose,
|
|
339
|
+
progressbar_kwargs=progressbar_kwargs)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class _ClosingMultiprocessing(BackendMultiprocessing):
|
|
343
|
+
"""
|
|
344
|
+
The ``multiprocessing`` backend of MDAnalysis, with a pool that is
|
|
345
|
+
closed and joined instead of terminated.
|
|
346
|
+
|
|
347
|
+
:class:`MDAnalysis.analysis.backends.BackendMultiprocessing` maps the
|
|
348
|
+
parts in ``with multiprocessing.Pool(...)``, whose exit calls
|
|
349
|
+
:meth:`multiprocessing.pool.Pool.terminate`. That kills the workers
|
|
350
|
+
before their finalizers run, so a semaphore a worker made is never
|
|
351
|
+
released. Each worker makes one: the progress bar of
|
|
352
|
+
:meth:`MDAnalysis.analysis.base.AnalysisBase._compute` builds the lock
|
|
353
|
+
of tqdm, a :func:`multiprocessing.RLock`, even when the bar is hidden.
|
|
354
|
+
At shutdown the resource tracker then reports the semaphores as leaked.
|
|
355
|
+
Closing and joining the pool lets each worker exit normally and release
|
|
356
|
+
its semaphore.
|
|
357
|
+
|
|
358
|
+
Notes
|
|
359
|
+
-----
|
|
360
|
+
The parts, their order, and their results are those of the MDAnalysis
|
|
361
|
+
backend: both call :meth:`multiprocessing.pool.Pool.map`. If the map
|
|
362
|
+
raises, the pool is terminated, as the MDAnalysis backend does, and the
|
|
363
|
+
error propagates.
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
def apply(self, func: Callable[[Any], Any],
|
|
367
|
+
computations: list[Any]) -> list[Any]:
|
|
368
|
+
"""
|
|
369
|
+
Apply `func` to each of `computations` on a pool of
|
|
370
|
+
``n_workers`` processes, and return the results in order.
|
|
371
|
+
"""
|
|
372
|
+
pool = Pool(processes=self.n_workers)
|
|
373
|
+
try:
|
|
374
|
+
results = pool.map(func, computations)
|
|
375
|
+
pool.close()
|
|
376
|
+
except BaseException:
|
|
377
|
+
pool.terminate()
|
|
378
|
+
raise
|
|
379
|
+
finally:
|
|
380
|
+
pool.join()
|
|
381
|
+
return results
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class _ProbeAnalysis(AnalysisBase):
|
|
385
|
+
"""
|
|
386
|
+
The base of the probe analyses: an MDAnalysis analysis whose
|
|
387
|
+
``'multiprocessing'`` backend is :class:`_ClosingMultiprocessing`.
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
def run(
|
|
391
|
+
self,
|
|
392
|
+
start: int | None = None,
|
|
393
|
+
stop: int | None = None,
|
|
394
|
+
step: int | None = None,
|
|
395
|
+
frames: Any = None,
|
|
396
|
+
verbose: bool | None = None,
|
|
397
|
+
n_workers: int | None = None,
|
|
398
|
+
n_parts: int | None = None,
|
|
399
|
+
backend: str | BackendBase | None = None,
|
|
400
|
+
*,
|
|
401
|
+
unsupported_backend: bool = False,
|
|
402
|
+
progressbar_kwargs: dict[str, Any] | None = None,
|
|
403
|
+
) -> Any:
|
|
404
|
+
"""
|
|
405
|
+
Run the analysis, as :meth:`MDAnalysis.analysis.base.AnalysisBase.run`
|
|
406
|
+
does.
|
|
407
|
+
|
|
408
|
+
The arguments and the result are those of
|
|
409
|
+
:meth:`MDAnalysis.analysis.base.AnalysisBase.run`. The one difference
|
|
410
|
+
is the ``'multiprocessing'`` backend: its pool of `n_workers`
|
|
411
|
+
processes, one if `n_workers` is ``None``, is closed and joined at
|
|
412
|
+
the end of the run instead of terminated, so the workers exit
|
|
413
|
+
normally and release the semaphores they made.
|
|
414
|
+
|
|
415
|
+
Examples
|
|
416
|
+
--------
|
|
417
|
+
>>> import MDAnalysis as mda
|
|
418
|
+
>>> from polyphys.probe.chain_size import ChainSize
|
|
419
|
+
>>> frames = np.zeros((4, 2, 3))
|
|
420
|
+
>>> frames[:, 1, 2] = [1.0, 2.0, 3.0, 4.0]
|
|
421
|
+
>>> universe = mda.Universe.empty(2, trajectory=True)
|
|
422
|
+
>>> universe.add_TopologyAttr('masses', np.ones(2))
|
|
423
|
+
>>> universe.load_new(frames, format='MEMORY')
|
|
424
|
+
<Universe with 2 atoms>
|
|
425
|
+
>>> analysis = ChainSize(universe.atoms, ('rfloryT',))
|
|
426
|
+
>>> analysis.run(backend='multiprocessing', n_workers=2,
|
|
427
|
+
... n_parts=2).results['rfloryT']
|
|
428
|
+
array([1., 2., 3., 4.])
|
|
429
|
+
"""
|
|
430
|
+
if isinstance(backend, str) and backend == 'multiprocessing':
|
|
431
|
+
backend = _ClosingMultiprocessing(
|
|
432
|
+
n_workers=1 if n_workers is None else n_workers)
|
|
433
|
+
unsupported_backend = True
|
|
434
|
+
return super().run(
|
|
435
|
+
start, stop, step, frames, verbose, n_workers, n_parts, backend,
|
|
436
|
+
unsupported_backend=unsupported_backend,
|
|
437
|
+
progressbar_kwargs=progressbar_kwargs)
|