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
@@ -0,0 +1,555 @@
1
+ """
2
+ ==========================================================
3
+ :mod:`polyphys.probe.pairs`
4
+ ==========================================================
5
+
6
+ The :mod:`polyphys.probe.pairs` module measures, in every frame of a
7
+ trajectory, the distances between the particles of one species, the pairs
8
+ in direct contact, and the bonds and clusters those contacts form: the
9
+ foci of the *TransFoci* studies, the large monomers of a ring among small
10
+ ones. :class:`PairDistances` is an MDAnalysis
11
+ :class:`~MDAnalysis.analysis.base.AnalysisBase`, so it runs over a slice of
12
+ frames in serial or, split across workers, in parallel, and stores one
13
+ array per quantity in its ``results``.
14
+
15
+ The research code measured the same four quantities in one loop over the
16
+ frames of each *TransFoci* study's probe function, without the minimum
17
+ image, and wrote the LAMMPS atom IDs of the foci on the diagonal of the
18
+ distance matrix. Here the diagonal carries the foci's 0-based backbone
19
+ indices (see :class:`PairDistances`, "Notes"), and the minimum image along
20
+ the periodic directions is optional.
21
+
22
+ Contents
23
+ ========
24
+
25
+ - :class:`PairDistances`, the per-frame pair analysis.
26
+
27
+ Dependencies
28
+ ============
29
+ - Python standard library: `math`, `pathlib`, `typing`.
30
+ - `NumPy <https://numpy.org/>`_.
31
+ - `MDAnalysis <https://docs.mdanalysis.org/>`_ 2.8 or later, through the
32
+ ``mda`` extra: :class:`~MDAnalysis.analysis.base.AnalysisBase` and
33
+ :class:`~MDAnalysis.analysis.results.ResultsGroup`.
34
+ - :mod:`polyphys.analyze.clusters`:
35
+ :func:`~polyphys.analyze.clusters.pair_distances`,
36
+ :func:`~polyphys.analyze.clusters.find_direct_contacts`,
37
+ :func:`~polyphys.analyze.clusters.count_bonds`, and
38
+ :func:`~polyphys.analyze.clusters.count_clusters`.
39
+ - :mod:`polyphys.spec`:
40
+ :class:`~polyphys.spec.ProbeSpec`.
41
+ - :mod:`polyphys.manage.parser`:
42
+ :class:`~polyphys.manage.parser.ParserBase`.
43
+ - :mod:`polyphys.probe.core`: ``_ProbeAnalysis``, the base whose ``run``
44
+ closes the pool of the ``multiprocessing`` backend, and
45
+ ``_compute_part``, which computes each part of a split run on its own
46
+ copy of the analysis.
47
+
48
+ Notation
49
+ ========
50
+ :math:`n`, :math:`F`
51
+ The number of particles of the species and the frames of a run.
52
+ :math:`d_{ij}`, :math:`r_c`
53
+ The distance between particles :math:`i < j` in a frame, in the dump's
54
+ length unit, and the contact cutoff: :math:`i` and :math:`j` are in
55
+ direct contact when :math:`d_{ij} \\le r_c`.
56
+ :math:`g_i`
57
+ The backbone index of particle :math:`i`, 0-based, from which the
58
+ pipeline's :class:`~polyphys.analyze.pipeline.FociPairDistances` takes
59
+ the genomic distance of each pair.
60
+
61
+ Examples
62
+ ========
63
+ Three foci at the corners of a 3-4-5 triangle, one frame, with a cutoff of
64
+ 3.5: the first two are in contact, the third is alone.
65
+
66
+ >>> import MDAnalysis as mda
67
+ >>> import numpy as np
68
+ >>> universe = mda.Universe.empty(3, trajectory=True)
69
+ >>> universe.atoms.positions = np.array(
70
+ ... [[0.0, 0.0, 0.0], [3.0, 0.0, 0.0], [3.0, 4.0, 0.0]])
71
+ >>> analysis = PairDistances(universe.atoms, 3.5, [2, 5, 8]).run()
72
+ >>> analysis.results['distMatTFoci'][0]
73
+ array([[2., 3., 5.],
74
+ [0., 5., 4.],
75
+ [0., 0., 8.]])
76
+ >>> analysis.results['bondsHistTFoci'], analysis.results['clustersHistTFoci']
77
+ (array([[1, 2, 0]]), array([[0, 1, 1, 0]]))
78
+
79
+ .. versionadded:: 0.7.0
80
+ """
81
+ import math
82
+ from pathlib import Path
83
+ from typing import Any, Self, Sequence
84
+
85
+ import numpy as np
86
+ from MDAnalysis.analysis.results import ResultsGroup
87
+ from MDAnalysis.core.groups import AtomGroup
88
+ from MDAnalysis.core.universe import Universe
89
+
90
+ from ..analyze import clusters
91
+ from ..manage.parser import ParserBase
92
+ from ..spec import ProbeSpec
93
+ from ..manage.types import EntityT, GeometryT, Pathish
94
+ from .core import _ProbeAnalysis, _compute_part
95
+
96
+ __all__ = [
97
+ 'PairDistances',
98
+ ]
99
+
100
+ # The periodic directions of each geometry, for the minimum image: a
101
+ # cylinder is periodic along its axis, z, a cube in every direction.
102
+ _PERIODIC: dict[GeometryT, tuple[bool, bool, bool]] = {
103
+ 'cubic': (True, True, True),
104
+ 'cylindrical': (False, False, True),
105
+ }
106
+
107
+ # The quantities, in the order `results` and `save` list them, as the
108
+ # research code named them before the species label.
109
+ _QUANTITIES = ('distMatT', 'directContactsMatT', 'bondsHistT',
110
+ 'clustersHistT')
111
+
112
+ _BACKENDS = ('serial', 'multiprocessing', 'dask')
113
+
114
+
115
+ class PairDistances(_ProbeAnalysis):
116
+ """
117
+ The pair distances, direct contacts, bonds, and clusters of one
118
+ species, measured in every frame.
119
+
120
+ .. versionadded:: 0.7.0
121
+
122
+ Parameters
123
+ ----------
124
+ atomgroup : :class:`~MDAnalysis.core.groups.AtomGroup`
125
+ The particles, at least two, in atom-ID order, which
126
+ ``Universe.select_atoms`` gives; they index the matrices.
127
+ cutoff : float
128
+ The contact cutoff :math:`r_c`, finite and positive, in the dump's
129
+ length unit.
130
+ genomic_positions : sequence of int or numpy.ndarray
131
+ The backbone index :math:`g_i` of each particle, 0-based,
132
+ non-negative, and distinct, one per particle in the group's order;
133
+ written on the diagonal of every frame's distance matrix.
134
+ species : :py:obj:`~.EntityT`, default 'Foci'
135
+ The species label, which names ``results`` and the files
136
+ :meth:`save` writes, ``<name>-<quantity><species>.npy``.
137
+ periodic : tuple of bool or None, default None
138
+ The directions along which the distances use the minimum image,
139
+ one flag each for ``x``, ``y``, and ``z``, with the frame's box
140
+ lengths; ``None`` for none, as the research
141
+ code measured.
142
+ inclusive : bool, default True
143
+ Whether a pair at exactly the cutoff is in contact,
144
+ :math:`d_{ij} \\le r_c`, as the research code counted, rather than
145
+ :math:`d_{ij} < r_c`.
146
+ verbose : bool, default False
147
+ Whether to show a progress bar during
148
+ :meth:`~MDAnalysis.analysis.base.AnalysisBase.run`.
149
+
150
+ Attributes
151
+ ----------
152
+ atomgroup : :class:`~MDAnalysis.core.groups.AtomGroup`
153
+ The particles.
154
+ cutoff : float
155
+ The contact cutoff.
156
+ genomic_positions : numpy.ndarray
157
+ The backbone indices, int64, shape ``(n,)``.
158
+ species : :py:obj:`~.EntityT`
159
+ The species label.
160
+ periodic : tuple of bool or None
161
+ The minimum-image directions.
162
+ inclusive : bool
163
+ Whether the cutoff itself counts as contact.
164
+ results : :class:`~MDAnalysis.analysis.results.Results`
165
+ After :meth:`~MDAnalysis.analysis.base.AnalysisBase.run`, with ``S``
166
+ the species label: ``distMatTS``, float64 ``(n_frames, n, n)``, each
167
+ frame's :math:`d_{ij}` for :math:`i < j` above the diagonal,
168
+ :math:`g_i` on it, and zeros below; ``directContactsMatTS``, bool
169
+ ``(n_frames, n, n)``, the symmetric contact matrix with ``True`` on the
170
+ diagonal, as :func:`~polyphys.analyze.clusters.find_direct_contacts`
171
+ returns it; ``bondsHistTS``, int64 ``(n_frames, n)``, ``[t, d]`` the
172
+ number of particles with ``d`` contacts in frame ``t``;
173
+ ``clustersHistTS``, int64 ``(n_frames, n + 1)``, ``[t, s]`` the number
174
+ of clusters of size ``s`` in frame ``t``, with ``[t, 0]`` always 0.
175
+ n_frames : int
176
+ After :meth:`~MDAnalysis.analysis.base.AnalysisBase.run`, the number of
177
+ frames analyzed, from :class:`~MDAnalysis.analysis.base.AnalysisBase`.
178
+ frames : numpy.ndarray
179
+ After :meth:`~MDAnalysis.analysis.base.AnalysisBase.run`, the indices
180
+ of the frames analyzed, shape ``(n_frames,)``.
181
+ times : numpy.ndarray
182
+ After :meth:`~MDAnalysis.analysis.base.AnalysisBase.run`, the times of
183
+ the frames analyzed, shape ``(n_frames,)``, in the trajectory's time
184
+ unit.
185
+
186
+ Raises
187
+ ------
188
+ ValueError
189
+ If `atomgroup` has fewer than two particles; if `cutoff` is not a
190
+ finite positive number; if `genomic_positions` has not one non-negative
191
+ integer per particle or repeats one; if `species` is empty; or if
192
+ `periodic` is neither ``None`` nor three booleans, Python or NumPy.
193
+ During :meth:`~MDAnalysis.analysis.base.AnalysisBase.run`, if a
194
+ periodic direction is asked for and the frame carries no box.
195
+
196
+ Notes
197
+ -----
198
+ Per frame, :func:`~polyphys.analyze.clusters.pair_distances` gives the
199
+ full distance matrix, with the minimum image along each direction of
200
+ `periodic` using that frame's box length;
201
+ :func:`~polyphys.analyze.clusters.find_direct_contacts` the contacts
202
+ at the cutoff; :func:`~polyphys.analyze.clusters.count_bonds` the
203
+ bonds histogram; and :func:`~polyphys.analyze.clusters.count_clusters`
204
+ the clusters histogram, from the connected components of the contact
205
+ matrix. The distance matrix is stored as the research code wrote it:
206
+ its upper triangle, with the diagonal replaced.
207
+
208
+ **Indexing.** LAMMPS numbers atoms from 1 (``read_data``: "a unique
209
+ value from 1 to Natoms for each atom"), and MDAnalysis keeps those
210
+ numbers as ``AtomGroup.ids`` while numbering its own atoms from 0 as
211
+ ``AtomGroup.indices``. The research code wrote ``foci.atoms.ids`` on
212
+ the diagonal, so the pair tags of its analysis, ``loc<i>loc<j>``,
213
+ carry 1-based IDs. Here the diagonal carries the 0-based position of
214
+ each particle along its chain, which
215
+ :func:`~polyphys.analyze.clusters.genomic_distance` requires and
216
+ :meth:`from_spec` computes; a tag of this package is therefore one
217
+ less than the research tag of the same pair. The genomic distance
218
+ itself is the same under either convention.
219
+
220
+ The MDAnalysis contract: ``_prepare`` allocates one array per
221
+ quantity for the frames of the run, ``_single_frame`` writes the
222
+ current frame's entry, and ``_conclude`` does nothing. The class is
223
+ marked parallelizable, with ``serial``, ``multiprocessing``, and
224
+ ``dask`` backends; each worker fills the arrays of its own frames,
225
+ which ``_get_aggregator`` concatenates along the frame axis
226
+ (``ResultsGroup.ndarray_vstack``).
227
+
228
+ Cost per frame: :math:`O(n^2)` time and space for the distance and
229
+ contact matrices and the connected components. Over a run,
230
+ :math:`8 F n^2` bytes for the distance matrices, :math:`F n^2` for the
231
+ contact matrices, and :math:`8 F (2 n + 1)` for the two histograms:
232
+ for the five foci of the *TransFoci* studies,
233
+ :math:`8 \\cdot 25 + 25 + 8 \\cdot 11 = 313` bytes per frame. Positions
234
+ come from MDAnalysis as float32, so each distance carries a relative
235
+ round-off of about :math:`10^{-7}`.
236
+
237
+ Examples
238
+ --------
239
+ Two particles across the periodic boundary of a box of length 20
240
+ along z, at z = 0.5 and 19.5, are 1 apart with the minimum image and
241
+ 19 without:
242
+
243
+ >>> import MDAnalysis as mda
244
+ >>> import numpy as np
245
+ >>> universe = mda.Universe.empty(2, trajectory=True)
246
+ >>> universe.load_new(
247
+ ... np.array([[[0.0, 0.0, 0.5], [0.0, 0.0, 19.5]]]), format='MEMORY',
248
+ ... dimensions=np.array([[20.0, 20.0, 20.0, 90.0, 90.0, 90.0]]))
249
+ <Universe with 2 atoms>
250
+ >>> wrapped = PairDistances(universe.atoms, 1.5, [0, 9],
251
+ ... periodic=(False, False, True)).run()
252
+ >>> wrapped.results['distMatTFoci'][0, 0, 1]
253
+ np.float64(1.0)
254
+ >>> wrapped.results['directContactsMatTFoci'][0]
255
+ array([[ True, True],
256
+ [ True, True]])
257
+ >>> PairDistances(universe.atoms, 1.5, [0, 9]).run().results[
258
+ ... 'distMatTFoci'][0, 0, 1]
259
+ np.float64(19.0)
260
+ """
261
+
262
+ _analysis_algorithm_is_parallelizable = True
263
+
264
+ def __init__(
265
+ self,
266
+ atomgroup: AtomGroup,
267
+ cutoff: float,
268
+ genomic_positions: Sequence[int] | np.ndarray,
269
+ *,
270
+ species: EntityT = 'Foci',
271
+ periodic: tuple[bool, bool, bool] | None = None,
272
+ inclusive: bool = True,
273
+ verbose: bool = False,
274
+ ) -> None:
275
+ super().__init__(atomgroup.universe.trajectory, verbose=verbose)
276
+ n_atoms = atomgroup.n_atoms
277
+ if n_atoms < 2:
278
+ raise ValueError(
279
+ f"'atomgroup' must hold at least two particles, not "
280
+ f"{n_atoms}.")
281
+ if not (isinstance(cutoff, (int, float))
282
+ and not isinstance(cutoff, bool)
283
+ and math.isfinite(cutoff) and cutoff > 0):
284
+ raise ValueError(
285
+ f"'cutoff' must be a finite positive number, not "
286
+ f"{cutoff!r}.")
287
+ positions = np.asarray(genomic_positions)
288
+ if (positions.shape != (n_atoms,)
289
+ or not np.issubdtype(positions.dtype, np.integer)
290
+ or np.any(positions < 0)):
291
+ raise ValueError(
292
+ f"'genomic_positions' must hold one non-negative integer "
293
+ f"per particle, {n_atoms} in all, not "
294
+ f"{list(np.ravel(genomic_positions))!r}.")
295
+ if np.unique(positions).size != n_atoms:
296
+ raise ValueError(
297
+ f"'genomic_positions' repeats an index: "
298
+ f"{positions.tolist()}.")
299
+ if not (isinstance(species, str) and species):
300
+ raise ValueError(
301
+ f"'species' must be a non-empty label, not {species!r}.")
302
+ if periodic is not None and not (
303
+ isinstance(periodic, (tuple, list)) and len(periodic) == 3
304
+ and all(isinstance(flag, (bool, np.bool_))
305
+ for flag in periodic)):
306
+ raise ValueError(
307
+ f"'periodic' must be None or three booleans, not "
308
+ f"{periodic!r}.")
309
+ self.atomgroup = atomgroup
310
+ self.cutoff = float(cutoff)
311
+ self.genomic_positions = positions.astype(np.int64)
312
+ self.species = species
313
+ self.periodic: tuple[bool, bool, bool] | None = (
314
+ None if periodic is None
315
+ else (bool(periodic[0]), bool(periodic[1]), bool(periodic[2])))
316
+ self.inclusive = inclusive
317
+ self._upper = np.triu_indices(n_atoms, 1)
318
+ self._diagonal = np.diag_indices(n_atoms)
319
+
320
+ @classmethod
321
+ def from_spec(
322
+ cls,
323
+ universe: Universe,
324
+ spec: ProbeSpec,
325
+ parser: ParserBase,
326
+ *,
327
+ verbose: bool = False,
328
+ ) -> Self:
329
+ """
330
+ Build the analysis of a whole's pair species from its study's probe
331
+ spec.
332
+
333
+ The particles are ``spec.pair_species`` selected with its string in
334
+ ``spec.selections``; the cutoff is ``spec.contact_cutoff`` resolved
335
+ for this whole; the backbone indices are the particles' positions
336
+ among the atoms of ``spec.chain``, in atom-ID order; and the
337
+ minimum image follows the geometry when ``spec.minimum_image`` is
338
+ set.
339
+
340
+ .. versionadded:: 0.7.0
341
+
342
+ Parameters
343
+ ----------
344
+ universe : :class:`~MDAnalysis.core.universe.Universe`
345
+ The trajectory, as :func:`~polyphys.probe.core.make_universe`
346
+ opens it.
347
+ spec : :class:`~polyphys.spec.ProbeSpec`
348
+ The study's probe spec, with ``pair_species`` and
349
+ ``contact_cutoff`` set.
350
+ parser : :class:`~polyphys.manage.parser.ParserBase`
351
+ The whole's parser, for the cutoff.
352
+ verbose : bool, default False
353
+ Whether to show a progress bar during
354
+ :meth:`~MDAnalysis.analysis.base.AnalysisBase.run`.
355
+
356
+ Returns
357
+ -------
358
+ PairDistances
359
+ The analysis, not yet run.
360
+
361
+ Raises
362
+ ------
363
+ ValueError
364
+ If the spec names no pair species, or if a selected particle is
365
+ not among the chain's atoms.
366
+
367
+ Examples
368
+ --------
369
+ A ring of six monomers whose atoms 2, 4, and 6 (LAMMPS IDs) are the
370
+ large ones: their backbone indices are 1, 3, and 5.
371
+
372
+ >>> import MDAnalysis as mda
373
+ >>> import numpy as np
374
+ >>> from polyphys.tests.synthetic import RingCub
375
+ >>> from polyphys.spec import ProbeSpec, ProjectSpec
376
+ >>> project = ProjectSpec(
377
+ ... parser=RingCub, group='bug', divisor=0.025,
378
+ ... crowder_attr='phi_bulk_c',
379
+ ... physical_attrs=('space', 'nmon_small', 'nmon_large'),
380
+ ... species_sizes={'Mon': 'dmon_small', 'Foci': 'dmon_large',
381
+ ... 'Crd': 'dcrowd'},
382
+ ... tseries_properties=('gyrTMon',))
383
+ >>> spec = ProbeSpec(
384
+ ... project, {'Mon': 'resid 1', 'Foci': 'type 2'}, 'Mon',
385
+ ... ('gyrT',), unwrap=False, pair_species='Foci',
386
+ ... contact_cutoff=lambda info: info.dmon_large + info.dcrowd)
387
+ >>> whole = RingCub(
388
+ ... 'al2nl3ml8ns3ac1nc0l5dt0.005bdump1adump1ens1',
389
+ ... 'whole', 'bug')
390
+ >>> universe = mda.Universe.empty(
391
+ ... 6, n_residues=1, atom_resindex=[0] * 6, trajectory=True)
392
+ >>> universe.add_TopologyAttr('resid', [1])
393
+ >>> universe.add_TopologyAttr('ids', [1, 2, 3, 4, 5, 6])
394
+ >>> universe.add_TopologyAttr('types', ['1', '2'] * 3)
395
+ >>> universe.load_new(np.array([[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0],
396
+ ... [2.0, 0.0, 0.0], [3.0, 0.0, 0.0], [3.0, 1.0, 0.0],
397
+ ... [3.0, 2.0, 0.0]]]), format='MEMORY', dimensions=np.array(
398
+ ... [[10.0, 10.0, 20.0, 90.0, 90.0, 90.0]]))
399
+ <Universe with 6 atoms>
400
+ >>> analysis = PairDistances.from_spec(universe, spec, whole)
401
+ >>> analysis.genomic_positions, analysis.cutoff, analysis.periodic
402
+ (array([1, 3, 5]), 3.0, (True, True, True))
403
+ >>> analysis.run().results['distMatTFoci'][0]
404
+ array([[1. , 2. , 2.82842712],
405
+ [0. , 3. , 2. ],
406
+ [0. , 0. , 5. ]])
407
+ """
408
+ if spec.pair_species is None or spec.contact_cutoff is None:
409
+ raise ValueError(
410
+ f"{spec.name}: the probe spec names no pair species.")
411
+ group = universe.select_atoms(spec.selections[spec.pair_species])
412
+ chain = universe.select_atoms(spec.selections[spec.chain])
413
+ in_chain = np.isin(group.ids, chain.ids)
414
+ if not np.all(in_chain):
415
+ raise ValueError(
416
+ f"{spec.name}: the {spec.pair_species} atoms with IDs "
417
+ f"{group.ids[~in_chain].tolist()} are not among the "
418
+ f"{spec.chain} atoms.")
419
+ genomic = np.flatnonzero(np.isin(chain.ids, group.ids))
420
+ periodic = _PERIODIC[spec.geometry] if spec.minimum_image else None
421
+ return cls(group, ProbeSpec.resolve(spec.contact_cutoff, parser),
422
+ genomic, species=spec.pair_species, periodic=periodic,
423
+ verbose=verbose)
424
+
425
+ @classmethod
426
+ def get_supported_backends(cls) -> tuple[str, ...]:
427
+ """
428
+ The backends :meth:`~MDAnalysis.analysis.base.AnalysisBase.run`
429
+ accepts: ``'serial'``, ``'multiprocessing'``, and ``'dask'``.
430
+
431
+ Examples
432
+ --------
433
+ >>> PairDistances.get_supported_backends()
434
+ ('serial', 'multiprocessing', 'dask')
435
+ """
436
+ return _BACKENDS
437
+
438
+ def _key(self, quantity: str) -> str:
439
+ return f"{quantity}{self.species}"
440
+
441
+ def _compute(
442
+ self, indexed_frames: np.ndarray, verbose: Any = None, *,
443
+ progressbar_kwargs: Any = None,
444
+ ) -> Any:
445
+ """
446
+ Compute one part of the run on a copy of this analysis, so that
447
+ serial parts do not overwrite each other.
448
+ """
449
+ return _compute_part(self, indexed_frames, verbose,
450
+ progressbar_kwargs=progressbar_kwargs)
451
+
452
+ def _get_aggregator(self) -> ResultsGroup:
453
+ """
454
+ Concatenate each worker's arrays along the frame axis.
455
+ """
456
+ return ResultsGroup(lookup={
457
+ self._key(quantity): ResultsGroup.ndarray_vstack
458
+ for quantity in _QUANTITIES})
459
+
460
+ def _prepare(self) -> None:
461
+ """
462
+ Allocate one array per quantity for the frames of this run or
463
+ worker.
464
+ """
465
+ n_atoms = self.atomgroup.n_atoms
466
+ self.results[self._key('distMatT')] = np.zeros(
467
+ (self.n_frames, n_atoms, n_atoms), dtype=np.float64)
468
+ self.results[self._key('directContactsMatT')] = np.zeros(
469
+ (self.n_frames, n_atoms, n_atoms), dtype=np.bool_)
470
+ self.results[self._key('bondsHistT')] = np.zeros(
471
+ (self.n_frames, n_atoms), dtype=np.int64)
472
+ self.results[self._key('clustersHistT')] = np.zeros(
473
+ (self.n_frames, n_atoms + 1), dtype=np.int64)
474
+
475
+ def _box(self) -> Any:
476
+ """The box lengths of the periodic directions, 0 elsewhere."""
477
+ if self.periodic is None:
478
+ return None
479
+ dimensions = self._ts.dimensions
480
+ if dimensions is None:
481
+ raise ValueError(
482
+ f"frame {self._ts.frame} carries no box, which the "
483
+ f"periodic directions {self.periodic} need.")
484
+ lengths = np.asarray(dimensions[:3], dtype=np.float64)
485
+ return np.where(self.periodic, lengths, 0.0)
486
+
487
+ def _single_frame(self) -> None:
488
+ """
489
+ Measure the four quantities on the current frame and store them in
490
+ this frame's entries.
491
+ """
492
+ distances = clusters.pair_distances(
493
+ self.atomgroup.positions.astype(np.float64), self._box())
494
+ contacts = clusters.find_direct_contacts(
495
+ distances, self.cutoff, inclusive=self.inclusive)
496
+ matrix = np.zeros_like(distances)
497
+ matrix[self._upper] = distances[self._upper]
498
+ matrix[self._diagonal] = self.genomic_positions
499
+ index = self._frame_index
500
+ self.results[self._key('distMatT')][index] = matrix
501
+ self.results[self._key('directContactsMatT')][index] = contacts
502
+ self.results[self._key('bondsHistT')][index] = (
503
+ clusters.count_bonds(contacts))
504
+ self.results[self._key('clustersHistT')][index] = (
505
+ clusters.count_clusters(contacts))
506
+
507
+ def save(self, save_to: Pathish, name: str) -> list[Path]:
508
+ """
509
+ Save each quantity's array as ``<name>-<quantity><species>.npy``.
510
+
511
+ .. versionadded:: 0.7.0
512
+
513
+ Parameters
514
+ ----------
515
+ save_to : str or pathlib.Path
516
+ An existing directory.
517
+ name : str
518
+ The artifact's name, ``<parser.name>-<parser.group>`` in the
519
+ studies, such as ``'epss5...ens1-bug'``.
520
+
521
+ Returns
522
+ -------
523
+ list of pathlib.Path
524
+ The files written: the distance matrices, the contact matrices,
525
+ the bonds histograms, and the clusters histograms.
526
+
527
+ Raises
528
+ ------
529
+ RuntimeError
530
+ If :meth:`~MDAnalysis.analysis.base.AnalysisBase.run` has not been
531
+ called.
532
+
533
+ Examples
534
+ --------
535
+ >>> import tempfile
536
+ >>> import MDAnalysis as mda
537
+ >>> import numpy as np
538
+ >>> universe = mda.Universe.empty(2, trajectory=True)
539
+ >>> universe.atoms.positions = np.array([[0.0, 0.0, 0.0],
540
+ ... [0.0, 0.0, 3.0]])
541
+ >>> analysis = PairDistances(universe.atoms, 1.0, [0, 4]).run()
542
+ >>> with tempfile.TemporaryDirectory() as tmp:
543
+ ... paths = analysis.save(tmp, 'N2ens1-bug')
544
+ ... len(paths), paths[0].name, np.load(paths[2]).tolist()
545
+ (4, 'N2ens1-bug-distMatTFoci.npy', [[2, 0]])
546
+ """
547
+ if any(self._key(quantity) not in self.results
548
+ for quantity in _QUANTITIES):
549
+ raise RuntimeError("Call run() before save().")
550
+ paths = []
551
+ for quantity in _QUANTITIES:
552
+ path = Path(save_to) / f"{name}-{self._key(quantity)}.npy"
553
+ np.save(path, self.results[self._key(quantity)])
554
+ paths.append(path)
555
+ return paths
@@ -0,0 +1,48 @@
1
+ """
2
+ The `simulate` subpackage works with the files of the simulation engine,
3
+ LAMMPS: it reads logs and dumps, reads and writes data files, and builds the
4
+ start configurations a run begins from, sharing the box and the error types
5
+ of :mod:`polyphys.simulate.core`.
6
+ """
7
+ from polyphys.simulate import core
8
+ from polyphys.simulate import log
9
+ from polyphys.simulate import dump
10
+ from polyphys.simulate import data
11
+ from polyphys.simulate import build
12
+ from polyphys.simulate.build import (
13
+ ConformationError,
14
+ ConformationReport,
15
+ chain_angles,
16
+ chain_bonds,
17
+ chain_data,
18
+ check_conformation,
19
+ helix,
20
+ plectoneme_ring,
21
+ resample_curve,
22
+ )
23
+ from polyphys.simulate.core import Box, LammpsFormatError
24
+ from polyphys.simulate.data import (
25
+ BrokenDataError,
26
+ DataFile,
27
+ read_data,
28
+ with_frame,
29
+ write_data,
30
+ )
31
+ from polyphys.simulate.dump import (
32
+ BrokenDumpError,
33
+ DumpFrame,
34
+ iter_dump,
35
+ read_dump_frame,
36
+ )
37
+ from polyphys.simulate.log import BrokenLogError, LogFile, parse_log, read_log
38
+
39
+ __all__ = [
40
+ "core", "log", "dump", "data", "build",
41
+ "Box", "LammpsFormatError",
42
+ "BrokenLogError", "LogFile", "parse_log", "read_log",
43
+ "BrokenDumpError", "DumpFrame", "iter_dump", "read_dump_frame",
44
+ "BrokenDataError", "DataFile", "read_data", "with_frame", "write_data",
45
+ "ConformationError", "ConformationReport", "chain_angles", "chain_bonds",
46
+ "chain_data", "check_conformation", "helix", "plectoneme_ring",
47
+ "resample_curve",
48
+ ]