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
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
"""
|
|
2
|
+
==========================================================
|
|
3
|
+
:mod:`polyphys.probe.chain_size`
|
|
4
|
+
==========================================================
|
|
5
|
+
|
|
6
|
+
The :mod:`polyphys.probe.chain_size` module measures the size and shape of
|
|
7
|
+
one polymer chain in every frame of a trajectory: its end-to-end distance,
|
|
8
|
+
its span along the cylinder axis, its transverse size, its radius of
|
|
9
|
+
gyration, and the asphericity, shape parameter, and principal axes of its
|
|
10
|
+
gyration tensor. :class:`ChainSize` 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 observable in its ``results``.
|
|
14
|
+
|
|
15
|
+
The research code measured the same seven quantities in one loop over the
|
|
16
|
+
frames of each study's probe function, after shifting every position by
|
|
17
|
+
half the box. Every quantity here is invariant under such a shift, because
|
|
18
|
+
each one first recenters the chain on its center of geometry or of mass,
|
|
19
|
+
so the shift is dropped.
|
|
20
|
+
|
|
21
|
+
Contents
|
|
22
|
+
========
|
|
23
|
+
|
|
24
|
+
- :class:`ChainSize`, the per-frame chain-size analysis.
|
|
25
|
+
|
|
26
|
+
Dependencies
|
|
27
|
+
============
|
|
28
|
+
- Python standard library: `pathlib`, `typing`.
|
|
29
|
+
- `NumPy <https://numpy.org/>`_.
|
|
30
|
+
- `MDAnalysis <https://docs.mdanalysis.org/>`_ 2.8 or later, through the
|
|
31
|
+
``mda`` extra: :class:`~MDAnalysis.analysis.base.AnalysisBase`,
|
|
32
|
+
:class:`~MDAnalysis.analysis.results.ResultsGroup`, and the shape methods
|
|
33
|
+
of :class:`~MDAnalysis.core.groups.AtomGroup`.
|
|
34
|
+
- :mod:`polyphys.analyze.measurer`: :func:`~polyphys.analyze.measurer.fsd`,
|
|
35
|
+
:func:`~polyphys.analyze.measurer.end_to_end`, and
|
|
36
|
+
:func:`~polyphys.analyze.measurer.transverse_size`.
|
|
37
|
+
- :mod:`polyphys.spec`:
|
|
38
|
+
:data:`~polyphys.spec.CHAIN_SIZE_OBSERVABLES` and
|
|
39
|
+
:class:`~polyphys.spec.ProbeSpec`.
|
|
40
|
+
- :mod:`polyphys.probe.core`: ``_ProbeAnalysis``, the base whose ``run``
|
|
41
|
+
closes the pool of the ``multiprocessing`` backend, and
|
|
42
|
+
``_compute_part``, which computes each part of a split run on its own
|
|
43
|
+
copy of the analysis.
|
|
44
|
+
|
|
45
|
+
Notation
|
|
46
|
+
========
|
|
47
|
+
:math:`N`
|
|
48
|
+
The number of beads in the chain, ``atomgroup.n_atoms``.
|
|
49
|
+
:math:`\\mathbf{r}_i`, :math:`m_i`
|
|
50
|
+
The position and mass of bead :math:`i`, in the dump's length and mass
|
|
51
|
+
units; the studies use the monomer diameter and mass.
|
|
52
|
+
:math:`\\mathbf{r}_\\mathrm{cg}`, :math:`\\mathbf{r}_\\mathrm{cm}`
|
|
53
|
+
The center of geometry :math:`\\sum_i \\mathbf{r}_i / N` and the center
|
|
54
|
+
of mass :math:`\\sum_i m_i \\mathbf{r}_i / \\sum_i m_i`.
|
|
55
|
+
:math:`\\mathbf{S}`, :math:`\\lambda_1 \\ge \\lambda_2 \\ge \\lambda_3`
|
|
56
|
+
The gyration tensor
|
|
57
|
+
:math:`S_{ab} = \\sum_i m_i (r_{i,a} - r_{\\mathrm{cm},a})
|
|
58
|
+
(r_{i,b} - r_{\\mathrm{cm},b}) / \\sum_i m_i`, and its eigenvalues,
|
|
59
|
+
which MDAnalysis's ``gyration_moments`` returns.
|
|
60
|
+
|
|
61
|
+
Examples
|
|
62
|
+
========
|
|
63
|
+
A straight chain of four beads one unit apart, in a one-frame universe:
|
|
64
|
+
|
|
65
|
+
>>> import MDAnalysis as mda
|
|
66
|
+
>>> import numpy as np
|
|
67
|
+
>>> universe = mda.Universe.empty(4, trajectory=True)
|
|
68
|
+
>>> universe.add_TopologyAttr('masses', np.ones(4))
|
|
69
|
+
>>> universe.atoms.positions = np.array(
|
|
70
|
+
... [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0], [3.0, 0.0, 0.0]])
|
|
71
|
+
>>> analysis = ChainSize(universe.atoms, ('rfloryT', 'asphericityT')).run()
|
|
72
|
+
>>> analysis.results['rfloryT'], analysis.results['asphericityT']
|
|
73
|
+
(array([3.]), array([1.]))
|
|
74
|
+
|
|
75
|
+
.. versionadded:: 0.7.0
|
|
76
|
+
"""
|
|
77
|
+
from pathlib import Path
|
|
78
|
+
from typing import Any, Callable, Mapping, Self, Sequence
|
|
79
|
+
|
|
80
|
+
import numpy as np
|
|
81
|
+
from MDAnalysis.analysis.results import ResultsGroup
|
|
82
|
+
from MDAnalysis.core.groups import AtomGroup
|
|
83
|
+
from MDAnalysis.core.universe import Universe
|
|
84
|
+
|
|
85
|
+
from ..analyze import measurer
|
|
86
|
+
from ..spec import CHAIN_SIZE_OBSERVABLES, ProbeSpec
|
|
87
|
+
from ..manage.types import AxisT, EntityT, Pathish, PropertyT
|
|
88
|
+
from .core import _ProbeAnalysis, _compute_part
|
|
89
|
+
|
|
90
|
+
__all__ = [
|
|
91
|
+
'ChainSize',
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
# One frame's value of an observable: a function of the chain, the cylinder
|
|
95
|
+
# axis, and whether to unwrap the chain.
|
|
96
|
+
_Measure = Callable[[AtomGroup, AxisT, bool], Any]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _trans_size(group: AtomGroup, axis: AxisT, unwrap: bool) -> Any:
|
|
100
|
+
return measurer.transverse_size(group.positions, axis)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _fsd(group: AtomGroup, axis: AxisT, unwrap: bool) -> Any:
|
|
104
|
+
return measurer.fsd(group.positions, axis)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _gyr(group: AtomGroup, axis: AxisT, unwrap: bool) -> Any:
|
|
108
|
+
return group.radius_of_gyration()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _rflory(group: AtomGroup, axis: AxisT, unwrap: bool) -> Any:
|
|
112
|
+
return measurer.end_to_end(group.positions)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _asphericity(group: AtomGroup, axis: AxisT, unwrap: bool) -> Any:
|
|
116
|
+
return group.asphericity(wrap=False, unwrap=unwrap)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _shape(group: AtomGroup, axis: AxisT, unwrap: bool) -> Any:
|
|
120
|
+
return group.shape_parameter(wrap=False)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _principal(group: AtomGroup, axis: AxisT, unwrap: bool) -> Any:
|
|
124
|
+
return group.principal_axes(wrap=False)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# The measure of each observable, keyed as CHAIN_SIZE_OBSERVABLES names them.
|
|
128
|
+
_MEASURES: Mapping[PropertyT, _Measure] = {
|
|
129
|
+
'transSizeT': _trans_size,
|
|
130
|
+
'fsdT': _fsd,
|
|
131
|
+
'gyrT': _gyr,
|
|
132
|
+
'rfloryT': _rflory,
|
|
133
|
+
'asphericityT': _asphericity,
|
|
134
|
+
'shapeT': _shape,
|
|
135
|
+
'principalT': _principal,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# The shape of one frame's value, after the frame axis; a scalar otherwise.
|
|
139
|
+
_FRAME_SHAPES: Mapping[PropertyT, tuple[int, ...]] = {'principalT': (3, 3)}
|
|
140
|
+
|
|
141
|
+
_BACKENDS = ('serial', 'multiprocessing', 'dask')
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _chain_bonds(atomgroup: AtomGroup) -> np.ndarray:
|
|
145
|
+
"""
|
|
146
|
+
The bonds within `atomgroup`, as pairs of positions in the group, shape
|
|
147
|
+
``(n_bonds, 2)``; empty when the topology has no bonds. Time
|
|
148
|
+
:math:`O(N + B \\log N)` and space :math:`O(N + B)` for :math:`N` beads
|
|
149
|
+
and :math:`B` bonds.
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
bonds = atomgroup.intra_bonds.indices
|
|
153
|
+
except AttributeError:
|
|
154
|
+
return np.empty((0, 2), dtype=np.intp)
|
|
155
|
+
order = np.argsort(atomgroup.indices)
|
|
156
|
+
local: np.ndarray = order[
|
|
157
|
+
np.searchsorted(atomgroup.indices, bonds, sorter=order)]
|
|
158
|
+
return local
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class ChainSize(_ProbeAnalysis):
|
|
162
|
+
"""
|
|
163
|
+
The size and shape of one polymer chain, measured in every frame.
|
|
164
|
+
|
|
165
|
+
.. versionadded:: 0.7.0
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
atomgroup : :class:`~MDAnalysis.core.groups.AtomGroup`
|
|
170
|
+
The chain, at least two beads, in atom-ID order: the order along
|
|
171
|
+
the backbone, which ``Universe.select_atoms`` gives.
|
|
172
|
+
observables : sequence of :py:obj:`~.PropertyT`
|
|
173
|
+
The observables to measure, each a name in
|
|
174
|
+
:data:`~polyphys.spec.CHAIN_SIZE_OBSERVABLES`, in the
|
|
175
|
+
order ``results`` and :meth:`save` list them.
|
|
176
|
+
chain : :py:obj:`~.EntityT`, default 'Mon'
|
|
177
|
+
The species label of the chain, which names the files
|
|
178
|
+
:meth:`save` writes, ``<name>-<observable><chain>.npy``.
|
|
179
|
+
unwrap : bool, default False
|
|
180
|
+
Whether the chain is unwrapped across the periodic boundaries
|
|
181
|
+
before its gyration tensor is formed, for ``asphericityT``. The
|
|
182
|
+
research code unwrapped in a periodic cube and not in a cylinder.
|
|
183
|
+
Every other observable reads the positions as the dump gives them,
|
|
184
|
+
so the chain must be whole; see Notes.
|
|
185
|
+
axis : :py:obj:`~.AxisT`, default 2
|
|
186
|
+
The cylinder axis, ``0``, ``1``, or ``2`` for ``x``, ``y``, or
|
|
187
|
+
``z``, along which ``fsdT`` spans and across which ``transSizeT``
|
|
188
|
+
spreads.
|
|
189
|
+
verbose : bool, default False
|
|
190
|
+
Whether to show a progress bar during
|
|
191
|
+
:meth:`~MDAnalysis.analysis.base.AnalysisBase.run`.
|
|
192
|
+
|
|
193
|
+
Attributes
|
|
194
|
+
----------
|
|
195
|
+
atomgroup : :class:`~MDAnalysis.core.groups.AtomGroup`
|
|
196
|
+
The chain.
|
|
197
|
+
observables : tuple of :py:obj:`~.PropertyT`
|
|
198
|
+
The observables, in order.
|
|
199
|
+
chain : :py:obj:`~.EntityT`
|
|
200
|
+
The species label of the chain.
|
|
201
|
+
unwrap : bool
|
|
202
|
+
Whether the chain is unwrapped for ``asphericityT``.
|
|
203
|
+
axis : :py:obj:`~.AxisT`
|
|
204
|
+
The cylinder axis.
|
|
205
|
+
results : :class:`~MDAnalysis.analysis.results.Results`
|
|
206
|
+
After :meth:`~MDAnalysis.analysis.base.AnalysisBase.run`, one float64
|
|
207
|
+
array per observable, keyed by its name: shape ``(n_frames,)`` for
|
|
208
|
+
every scalar, ``(n_frames, 3, 3)`` for ``principalT``.
|
|
209
|
+
n_frames : int
|
|
210
|
+
After :meth:`~MDAnalysis.analysis.base.AnalysisBase.run`, the number of
|
|
211
|
+
frames analyzed, from :class:`~MDAnalysis.analysis.base.AnalysisBase`.
|
|
212
|
+
frames : numpy.ndarray
|
|
213
|
+
After :meth:`~MDAnalysis.analysis.base.AnalysisBase.run`, the indices
|
|
214
|
+
of the frames analyzed, shape ``(n_frames,)``.
|
|
215
|
+
times : numpy.ndarray
|
|
216
|
+
After :meth:`~MDAnalysis.analysis.base.AnalysisBase.run`, the times of
|
|
217
|
+
the frames analyzed, shape ``(n_frames,)``, in the trajectory's time
|
|
218
|
+
unit.
|
|
219
|
+
|
|
220
|
+
Raises
|
|
221
|
+
------
|
|
222
|
+
ValueError
|
|
223
|
+
If `observables` is empty, repeats a name, or names an observable that
|
|
224
|
+
is not in :data:`~polyphys.spec.CHAIN_SIZE_OBSERVABLES`; if `chain` is
|
|
225
|
+
empty; if `axis` is not ``0``, ``1``, or ``2``; or if `atomgroup` has
|
|
226
|
+
fewer than two beads. During
|
|
227
|
+
:meth:`~MDAnalysis.analysis.base.AnalysisBase.run`, if a chain bond
|
|
228
|
+
spans half the frame's box or more along a direction, which means the
|
|
229
|
+
dump splits the chain across a periodic boundary; the message names the
|
|
230
|
+
frame.
|
|
231
|
+
|
|
232
|
+
Notes
|
|
233
|
+
-----
|
|
234
|
+
The observables, with the species label ``Mon`` as the studies name
|
|
235
|
+
them:
|
|
236
|
+
|
|
237
|
+
``rfloryTMon``
|
|
238
|
+
:math:`|\\mathbf{r}_N - \\mathbf{r}_1|`, the end-to-end distance
|
|
239
|
+
(:func:`~polyphys.analyze.measurer.end_to_end`).
|
|
240
|
+
``fsdTMon``
|
|
241
|
+
:math:`\\max_i z_i - \\min_i z_i` along `axis`, the span or Feret
|
|
242
|
+
diameter (:func:`~polyphys.analyze.measurer.fsd`).
|
|
243
|
+
``transSizeTMon``
|
|
244
|
+
:math:`2 \\langle |\\mathbf{r}_{i,\\perp} -
|
|
245
|
+
\\mathbf{r}_{\\mathrm{cg},\\perp}| \\rangle_i`, twice the mean
|
|
246
|
+
distance from the center of geometry in the plane across `axis`
|
|
247
|
+
(:func:`~polyphys.analyze.measurer.transverse_size`).
|
|
248
|
+
``gyrTMon``
|
|
249
|
+
:math:`R_g = \\sqrt{\\lambda_1 + \\lambda_2 + \\lambda_3}`, the
|
|
250
|
+
mass-weighted radius of gyration
|
|
251
|
+
(``AtomGroup.radius_of_gyration``).
|
|
252
|
+
``asphericityTMon``
|
|
253
|
+
:math:`\\frac{3}{2} \\sum_k (\\lambda_k - \\bar\\lambda)^2 /
|
|
254
|
+
(\\sum_k \\lambda_k)^2`, from 0 for a spherically symmetric chain
|
|
255
|
+
to 1 for a straight one
|
|
256
|
+
(``AtomGroup.asphericity(wrap=False, unwrap=unwrap)``).
|
|
257
|
+
``shapeTMon``
|
|
258
|
+
:math:`27 \\prod_k (\\lambda_k - \\bar\\lambda) /
|
|
259
|
+
(\\sum_k \\lambda_k)^3`, from :math:`-1/4` for a flat chain with two
|
|
260
|
+
equal eigenvalues and a zero one, such as a planar ring, through 0
|
|
261
|
+
for a spherically symmetric one, to 2 for a straight one
|
|
262
|
+
(``AtomGroup.shape_parameter(wrap=False)``).
|
|
263
|
+
``principalTMon``
|
|
264
|
+
The :math:`3 \\times 3` matrix of principal axes of the moment of
|
|
265
|
+
inertia, one eigenvector per row, by decreasing eigenvalue
|
|
266
|
+
(``AtomGroup.principal_axes(wrap=False)``).
|
|
267
|
+
|
|
268
|
+
The asphericity and shape parameter follow Dima and Thirumalai [1]_,
|
|
269
|
+
as MDAnalysis implements them. ``shapeT`` and ``principalT`` read the
|
|
270
|
+
positions as the dump wraps them, whatever `unwrap` is, as the research
|
|
271
|
+
code did; `unwrap` reaches ``asphericityT`` only.
|
|
272
|
+
|
|
273
|
+
Whole molecules: every observable but an unwrapped ``asphericityT`` reads
|
|
274
|
+
the positions as the dump gives them, so the dump must hold the chain
|
|
275
|
+
whole, as the studies' dumps do. In each frame,
|
|
276
|
+
:meth:`~MDAnalysis.analysis.base.AnalysisBase.run` checks that no bond
|
|
277
|
+
:math:`(i, j)` of the chain spans half the box or more along any direction
|
|
278
|
+
:math:`a`, :math:`|r_{i,a} - r_{j,a}| \\ge L_a / 2` for the frame's box
|
|
279
|
+
length :math:`L_a`, and raises on the first frame that fails. A bond is far
|
|
280
|
+
shorter than half the box (the studies' FENE bound :math:`R_0 = 1.5`,
|
|
281
|
+
``docs/source/design.md``), so a bond that long has been split by the
|
|
282
|
+
boundary. The check is skipped when the frame carries no box or the
|
|
283
|
+
topology has no bonds, and when ``asphericityT`` with `unwrap` is the only
|
|
284
|
+
observable, since unwrapping makes that one whole itself.
|
|
285
|
+
|
|
286
|
+
No per-frame shift of the positions is applied: each observable
|
|
287
|
+
recenters the chain itself, so the research code's shift of every
|
|
288
|
+
position by half the box changed none of these values.
|
|
289
|
+
|
|
290
|
+
The MDAnalysis contract: ``_prepare`` allocates one float64 array
|
|
291
|
+
per observable for the frames of the run, ``_single_frame`` writes
|
|
292
|
+
the current frame's row, and ``_conclude`` does nothing. The class
|
|
293
|
+
is marked parallelizable, with ``serial``, ``multiprocessing``, and
|
|
294
|
+
``dask`` backends; each worker fills the arrays of its own frames,
|
|
295
|
+
which ``_get_aggregator`` concatenates along the frame axis
|
|
296
|
+
(``ResultsGroup.ndarray_hstack`` for a scalar series,
|
|
297
|
+
``ResultsGroup.ndarray_vstack`` for ``principalT``).
|
|
298
|
+
|
|
299
|
+
Cost per frame: :math:`O(N)` time for every observable, since each is a
|
|
300
|
+
sum over the beads followed by at most a :math:`3 \\times 3`
|
|
301
|
+
eigenproblem, plus the :math:`O(N + N_\\mathrm{bonds})` unwrap of the
|
|
302
|
+
chain when `unwrap` is set, and the :math:`O(N_\\mathrm{bonds})` check
|
|
303
|
+
that the chain is whole; :math:`O(N + N_\\mathrm{bonds})` extra space
|
|
304
|
+
for the recentered positions and the bond spans. Over a run,
|
|
305
|
+
:math:`O(F)` space per scalar series and :math:`O(9 F)` for
|
|
306
|
+
``principalT``, for :math:`F` frames. Positions come
|
|
307
|
+
from MDAnalysis as float32, so each value carries a relative round-off
|
|
308
|
+
of about :math:`10^{-7}`.
|
|
309
|
+
|
|
310
|
+
References
|
|
311
|
+
----------
|
|
312
|
+
.. [1] R. I. Dima and D. Thirumalai, "Asymmetry in the Shapes of Folded
|
|
313
|
+
and Denatured States of Proteins", J. Phys. Chem. B 108, 6564-6570
|
|
314
|
+
(2004). https://doi.org/10.1021/jp037128y
|
|
315
|
+
|
|
316
|
+
Examples
|
|
317
|
+
--------
|
|
318
|
+
Two frames of a three-bead chain, the second frame stretched along z:
|
|
319
|
+
|
|
320
|
+
>>> import MDAnalysis as mda
|
|
321
|
+
>>> import numpy as np
|
|
322
|
+
>>> frames = np.array([
|
|
323
|
+
... [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
|
|
324
|
+
... [[0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, 2.0]]])
|
|
325
|
+
>>> universe = mda.Universe.empty(3, trajectory=True)
|
|
326
|
+
>>> universe.add_TopologyAttr('masses', np.ones(3))
|
|
327
|
+
>>> universe.load_new(frames, format='MEMORY')
|
|
328
|
+
<Universe with 3 atoms>
|
|
329
|
+
>>> analysis = ChainSize(universe.atoms, ('rfloryT', 'fsdT', 'gyrT'),
|
|
330
|
+
... axis=2)
|
|
331
|
+
>>> analysis = analysis.run()
|
|
332
|
+
>>> np.round(analysis.results['rfloryT'], 6)
|
|
333
|
+
array([1.414214, 2. ])
|
|
334
|
+
>>> analysis.results['fsdT']
|
|
335
|
+
array([0., 2.])
|
|
336
|
+
|
|
337
|
+
The frames to analyze are chosen as in any MDAnalysis analysis:
|
|
338
|
+
|
|
339
|
+
>>> ChainSize(universe.atoms, ('fsdT',)).run(start=1).results['fsdT']
|
|
340
|
+
array([2.])
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
_analysis_algorithm_is_parallelizable = True
|
|
344
|
+
|
|
345
|
+
def __init__(
|
|
346
|
+
self,
|
|
347
|
+
atomgroup: AtomGroup,
|
|
348
|
+
observables: Sequence[PropertyT],
|
|
349
|
+
*,
|
|
350
|
+
chain: EntityT = 'Mon',
|
|
351
|
+
unwrap: bool = False,
|
|
352
|
+
axis: AxisT = 2,
|
|
353
|
+
verbose: bool = False,
|
|
354
|
+
) -> None:
|
|
355
|
+
super().__init__(atomgroup.universe.trajectory, verbose=verbose)
|
|
356
|
+
observables = tuple(observables)
|
|
357
|
+
if not observables:
|
|
358
|
+
raise ValueError("'observables' must not be empty.")
|
|
359
|
+
unknown = [obs for obs in observables if obs not in _MEASURES]
|
|
360
|
+
if unknown:
|
|
361
|
+
raise ValueError(
|
|
362
|
+
f"'observables' names {unknown}, which are not in "
|
|
363
|
+
f"{list(CHAIN_SIZE_OBSERVABLES)}.")
|
|
364
|
+
if len(set(observables)) != len(observables):
|
|
365
|
+
raise ValueError(
|
|
366
|
+
f"'observables' repeats a name: {list(observables)}.")
|
|
367
|
+
if not (isinstance(chain, str) and chain):
|
|
368
|
+
raise ValueError(
|
|
369
|
+
f"'chain' must be a non-empty species label, not {chain!r}.")
|
|
370
|
+
if axis not in (0, 1, 2):
|
|
371
|
+
raise ValueError(f"'axis' must be 0, 1, or 2, not {axis!r}.")
|
|
372
|
+
if atomgroup.n_atoms < 2:
|
|
373
|
+
raise ValueError(
|
|
374
|
+
f"'atomgroup' must hold at least two beads, not "
|
|
375
|
+
f"{atomgroup.n_atoms}.")
|
|
376
|
+
self.atomgroup = atomgroup
|
|
377
|
+
self.observables = observables
|
|
378
|
+
self.chain = chain
|
|
379
|
+
self.unwrap = unwrap
|
|
380
|
+
self.axis = axis
|
|
381
|
+
# Every observable but an unwrapped asphericity reads the positions
|
|
382
|
+
# as the dump wraps them, so the chain must be whole in each frame.
|
|
383
|
+
self._check_whole = unwrap is False or any(
|
|
384
|
+
obs != 'asphericityT' for obs in observables)
|
|
385
|
+
self._bonds = _chain_bonds(atomgroup)
|
|
386
|
+
|
|
387
|
+
@classmethod
|
|
388
|
+
def from_spec(
|
|
389
|
+
cls,
|
|
390
|
+
universe: Universe,
|
|
391
|
+
spec: ProbeSpec,
|
|
392
|
+
*,
|
|
393
|
+
axis: AxisT = 2,
|
|
394
|
+
verbose: bool = False,
|
|
395
|
+
) -> Self:
|
|
396
|
+
"""
|
|
397
|
+
Build the analysis of a study's chain from its probe spec.
|
|
398
|
+
|
|
399
|
+
The chain is ``universe.select_atoms(spec.selections[spec.chain])``;
|
|
400
|
+
the observables, the chain label, and `unwrap` are the spec's.
|
|
401
|
+
|
|
402
|
+
.. versionadded:: 0.7.0
|
|
403
|
+
|
|
404
|
+
Parameters
|
|
405
|
+
----------
|
|
406
|
+
universe : :class:`~MDAnalysis.core.universe.Universe`
|
|
407
|
+
The trajectory, as :func:`~polyphys.probe.core.make_universe`
|
|
408
|
+
opens it.
|
|
409
|
+
spec : :class:`~polyphys.spec.ProbeSpec`
|
|
410
|
+
The study's probe spec.
|
|
411
|
+
axis : :py:obj:`~.AxisT`, default 2
|
|
412
|
+
The cylinder axis.
|
|
413
|
+
verbose : bool, default False
|
|
414
|
+
Whether to show a progress bar during
|
|
415
|
+
:meth:`~MDAnalysis.analysis.base.AnalysisBase.run`.
|
|
416
|
+
|
|
417
|
+
Returns
|
|
418
|
+
-------
|
|
419
|
+
ChainSize
|
|
420
|
+
The analysis, not yet run.
|
|
421
|
+
|
|
422
|
+
Raises
|
|
423
|
+
------
|
|
424
|
+
ValueError
|
|
425
|
+
If the selection picks fewer than two beads.
|
|
426
|
+
|
|
427
|
+
Examples
|
|
428
|
+
--------
|
|
429
|
+
>>> import MDAnalysis as mda
|
|
430
|
+
>>> import numpy as np
|
|
431
|
+
>>> from polyphys.tests.synthetic import ChainCyl
|
|
432
|
+
>>> from polyphys.spec import ProbeSpec, ProjectSpec
|
|
433
|
+
>>> project = ProjectSpec(
|
|
434
|
+
... parser=ChainCyl, group='bug', divisor=0.025,
|
|
435
|
+
... crowder_attr='phi_bulk_c',
|
|
436
|
+
... physical_attrs=('space', 'nmon'),
|
|
437
|
+
... species_sizes={'Mon': 'dmon', 'Crd': 'dcrowd'},
|
|
438
|
+
... tseries_properties=('gyrTMon', 'rfloryTMon'))
|
|
439
|
+
>>> spec = ProbeSpec(project, {'Mon': 'resid 1'}, 'Mon',
|
|
440
|
+
... ('rfloryT',), unwrap=False)
|
|
441
|
+
>>> universe = mda.Universe.empty(3, n_residues=1,
|
|
442
|
+
... atom_resindex=[0, 0, 0],
|
|
443
|
+
... trajectory=True)
|
|
444
|
+
>>> universe.add_TopologyAttr('resid', [1])
|
|
445
|
+
>>> universe.add_TopologyAttr('masses', np.ones(3))
|
|
446
|
+
>>> universe.atoms.positions = np.array(
|
|
447
|
+
... [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]])
|
|
448
|
+
>>> analysis = ChainSize.from_spec(universe, spec)
|
|
449
|
+
>>> analysis.chain, analysis.atomgroup.n_atoms
|
|
450
|
+
('Mon', 3)
|
|
451
|
+
>>> analysis.run().results['rfloryT']
|
|
452
|
+
array([2.])
|
|
453
|
+
"""
|
|
454
|
+
chain = universe.select_atoms(spec.selections[spec.chain])
|
|
455
|
+
return cls(chain, spec.observables, chain=spec.chain,
|
|
456
|
+
unwrap=spec.unwrap, axis=axis, verbose=verbose)
|
|
457
|
+
|
|
458
|
+
@classmethod
|
|
459
|
+
def get_supported_backends(cls) -> tuple[str, ...]:
|
|
460
|
+
"""
|
|
461
|
+
The backends :meth:`~MDAnalysis.analysis.base.AnalysisBase.run`
|
|
462
|
+
accepts: ``'serial'``, ``'multiprocessing'``, and ``'dask'``.
|
|
463
|
+
|
|
464
|
+
Examples
|
|
465
|
+
--------
|
|
466
|
+
>>> ChainSize.get_supported_backends()
|
|
467
|
+
('serial', 'multiprocessing', 'dask')
|
|
468
|
+
"""
|
|
469
|
+
return _BACKENDS
|
|
470
|
+
|
|
471
|
+
def _compute(
|
|
472
|
+
self, indexed_frames: np.ndarray, verbose: Any = None, *,
|
|
473
|
+
progressbar_kwargs: Any = None,
|
|
474
|
+
) -> Any:
|
|
475
|
+
"""
|
|
476
|
+
Compute one part of the run on a copy of this analysis, so that
|
|
477
|
+
serial parts do not overwrite each other.
|
|
478
|
+
"""
|
|
479
|
+
return _compute_part(self, indexed_frames, verbose,
|
|
480
|
+
progressbar_kwargs=progressbar_kwargs)
|
|
481
|
+
|
|
482
|
+
def _get_aggregator(self) -> ResultsGroup:
|
|
483
|
+
"""
|
|
484
|
+
Concatenate each worker's arrays along the frame axis.
|
|
485
|
+
"""
|
|
486
|
+
return ResultsGroup(lookup={
|
|
487
|
+
obs: (ResultsGroup.ndarray_vstack if obs in _FRAME_SHAPES
|
|
488
|
+
else ResultsGroup.ndarray_hstack)
|
|
489
|
+
for obs in self.observables})
|
|
490
|
+
|
|
491
|
+
def _prepare(self) -> None:
|
|
492
|
+
"""
|
|
493
|
+
Allocate one float64 array per observable for the frames of this
|
|
494
|
+
run or worker.
|
|
495
|
+
"""
|
|
496
|
+
for obs in self.observables:
|
|
497
|
+
self.results[obs] = np.empty(
|
|
498
|
+
(self.n_frames, *_FRAME_SHAPES.get(obs, ())),
|
|
499
|
+
dtype=np.float64)
|
|
500
|
+
|
|
501
|
+
def _single_frame(self) -> None:
|
|
502
|
+
"""
|
|
503
|
+
Check that the chain is whole, then measure every observable on the
|
|
504
|
+
current frame and store it in its array's row for this frame.
|
|
505
|
+
"""
|
|
506
|
+
if self._check_whole:
|
|
507
|
+
self._raise_if_split()
|
|
508
|
+
for obs in self.observables:
|
|
509
|
+
self.results[obs][self._frame_index] = _MEASURES[obs](
|
|
510
|
+
self.atomgroup, self.axis, self.unwrap)
|
|
511
|
+
|
|
512
|
+
def _raise_if_split(self) -> None:
|
|
513
|
+
"""
|
|
514
|
+
Raise if a chain bond spans half the frame's box or more along a
|
|
515
|
+
direction: the dump has split the chain across a periodic boundary.
|
|
516
|
+
"""
|
|
517
|
+
dimensions = self._ts.dimensions
|
|
518
|
+
if dimensions is None or not len(self._bonds):
|
|
519
|
+
return
|
|
520
|
+
lengths = np.asarray(dimensions[:3], dtype=np.float64)
|
|
521
|
+
positions = self.atomgroup.positions.astype(np.float64)
|
|
522
|
+
spans = np.abs(positions[self._bonds[:, 0]]
|
|
523
|
+
- positions[self._bonds[:, 1]])
|
|
524
|
+
split = (lengths > 0) & (spans >= 0.5 * lengths)
|
|
525
|
+
if split.any():
|
|
526
|
+
directions = ', '.join(
|
|
527
|
+
'xyz'[axis] for axis in np.flatnonzero(split.any(axis=0)))
|
|
528
|
+
raise ValueError(
|
|
529
|
+
f"frame {self._ts.frame}: "
|
|
530
|
+
f"{int(np.count_nonzero(split.any(axis=1)))} of "
|
|
531
|
+
f"{len(self._bonds)} chain bonds span half the box or more "
|
|
532
|
+
f"along {directions}, so the dump splits the chain across a "
|
|
533
|
+
f"periodic boundary; the chain-size observables need whole "
|
|
534
|
+
f"molecules.")
|
|
535
|
+
|
|
536
|
+
def save(self, save_to: Pathish, name: str) -> list[Path]:
|
|
537
|
+
"""
|
|
538
|
+
Save each observable's array as ``<name>-<observable><chain>.npy``.
|
|
539
|
+
|
|
540
|
+
.. versionadded:: 0.7.0
|
|
541
|
+
|
|
542
|
+
Parameters
|
|
543
|
+
----------
|
|
544
|
+
save_to : str or pathlib.Path
|
|
545
|
+
An existing directory.
|
|
546
|
+
name : str
|
|
547
|
+
The artifact's name, ``<parser.name>-<parser.group>`` in the
|
|
548
|
+
studies, such as ``'N200...ens1-bug'``.
|
|
549
|
+
|
|
550
|
+
Returns
|
|
551
|
+
-------
|
|
552
|
+
list of pathlib.Path
|
|
553
|
+
The files written, one per observable, in order.
|
|
554
|
+
|
|
555
|
+
Raises
|
|
556
|
+
------
|
|
557
|
+
RuntimeError
|
|
558
|
+
If :meth:`~MDAnalysis.analysis.base.AnalysisBase.run` has not been
|
|
559
|
+
called.
|
|
560
|
+
|
|
561
|
+
Examples
|
|
562
|
+
--------
|
|
563
|
+
>>> import tempfile
|
|
564
|
+
>>> import MDAnalysis as mda
|
|
565
|
+
>>> import numpy as np
|
|
566
|
+
>>> universe = mda.Universe.empty(2, trajectory=True)
|
|
567
|
+
>>> universe.add_TopologyAttr('masses', np.ones(2))
|
|
568
|
+
>>> universe.atoms.positions = np.array([[0.0, 0.0, 0.0],
|
|
569
|
+
... [0.0, 0.0, 3.0]])
|
|
570
|
+
>>> analysis = ChainSize(universe.atoms, ('rfloryT', 'fsdT')).run()
|
|
571
|
+
>>> with tempfile.TemporaryDirectory() as tmp:
|
|
572
|
+
... paths = analysis.save(tmp, 'N2ens1-bug')
|
|
573
|
+
... [path.name for path in paths], np.load(paths[1]).tolist()
|
|
574
|
+
(['N2ens1-bug-rfloryTMon.npy', 'N2ens1-bug-fsdTMon.npy'], [3.0])
|
|
575
|
+
"""
|
|
576
|
+
if any(obs not in self.results for obs in self.observables):
|
|
577
|
+
raise RuntimeError("Call run() before save().")
|
|
578
|
+
paths = []
|
|
579
|
+
for obs in self.observables:
|
|
580
|
+
path = Path(save_to) / f"{name}-{obs}{self.chain}.npy"
|
|
581
|
+
np.save(path, self.results[obs])
|
|
582
|
+
paths.append(path)
|
|
583
|
+
return paths
|