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.
Files changed (69) hide show
  1. polyphys/__init__.py +7 -0
  2. polyphys/__version__.py +22 -0
  3. polyphys/analyze/__init__.py +51 -0
  4. polyphys/analyze/clusters.py +1738 -0
  5. polyphys/analyze/correlations.py +995 -0
  6. polyphys/analyze/density.py +1138 -0
  7. polyphys/analyze/measurer.py +1338 -0
  8. polyphys/analyze/pipeline.py +1868 -0
  9. polyphys/analyze/stats.py +810 -0
  10. polyphys/analyze/sumrule.py +1159 -0
  11. polyphys/manage/__init__.py +11 -0
  12. polyphys/manage/organizer.py +2188 -0
  13. polyphys/manage/parser.py +1264 -0
  14. polyphys/manage/types.py +171 -0
  15. polyphys/manage/utils.py +777 -0
  16. polyphys/probe/__init__.py +25 -0
  17. polyphys/probe/binders.py +711 -0
  18. polyphys/probe/chain_size.py +583 -0
  19. polyphys/probe/core.py +437 -0
  20. polyphys/probe/histograms.py +863 -0
  21. polyphys/probe/pairs.py +555 -0
  22. polyphys/simulate/__init__.py +48 -0
  23. polyphys/simulate/build.py +1231 -0
  24. polyphys/simulate/core.py +237 -0
  25. polyphys/simulate/data.py +977 -0
  26. polyphys/simulate/dump.py +752 -0
  27. polyphys/simulate/log.py +788 -0
  28. polyphys/spec.py +1359 -0
  29. polyphys/tests/analyze/test_clusters.py +1058 -0
  30. polyphys/tests/analyze/test_correlations.py +708 -0
  31. polyphys/tests/analyze/test_density.py +813 -0
  32. polyphys/tests/analyze/test_measurer.py +785 -0
  33. polyphys/tests/analyze/test_pipeline.py +1881 -0
  34. polyphys/tests/analyze/test_stats.py +732 -0
  35. polyphys/tests/analyze/test_sumrule.py +917 -0
  36. polyphys/tests/conftest.py +753 -0
  37. polyphys/tests/data/lammps_build/make_starts.py +70 -0
  38. polyphys/tests/manage/test_organizer.py +1495 -0
  39. polyphys/tests/manage/test_parser.py +1222 -0
  40. polyphys/tests/manage/test_utils.py +498 -0
  41. polyphys/tests/probe/conftest.py +407 -0
  42. polyphys/tests/probe/test_binders.py +686 -0
  43. polyphys/tests/probe/test_chain_size.py +557 -0
  44. polyphys/tests/probe/test_core.py +391 -0
  45. polyphys/tests/probe/test_histograms.py +779 -0
  46. polyphys/tests/probe/test_pairs.py +475 -0
  47. polyphys/tests/simulate/test_build.py +1042 -0
  48. polyphys/tests/simulate/test_core.py +106 -0
  49. polyphys/tests/simulate/test_data.py +858 -0
  50. polyphys/tests/simulate/test_dump.py +487 -0
  51. polyphys/tests/simulate/test_log.py +667 -0
  52. polyphys/tests/strategies.py +120 -0
  53. polyphys/tests/synthetic.py +410 -0
  54. polyphys/tests/test_conftest.py +414 -0
  55. polyphys/tests/test_docs.py +82 -0
  56. polyphys/tests/test_packaging.py +97 -0
  57. polyphys/tests/test_spec.py +772 -0
  58. polyphys/tests/viz/conftest.py +32 -0
  59. polyphys/tests/viz/test_colors.py +113 -0
  60. polyphys/tests/viz/test_helpers.py +292 -0
  61. polyphys/viz/__init__.py +15 -0
  62. polyphys/viz/colors.py +204 -0
  63. polyphys/viz/helpers.py +549 -0
  64. polyphys-1.0.0.dist-info/METADATA +339 -0
  65. polyphys-1.0.0.dist-info/RECORD +69 -0
  66. polyphys-1.0.0.dist-info/WHEEL +5 -0
  67. polyphys-1.0.0.dist-info/licenses/AUTHORS.rst +13 -0
  68. polyphys-1.0.0.dist-info/licenses/LICENSE +22 -0
  69. 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)