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/pairs.py
ADDED
|
@@ -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
|
+
]
|