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,557 @@
|
|
|
1
|
+
"""Tests for :mod:`polyphys.probe.chain_size`.
|
|
2
|
+
|
|
3
|
+
The inputs are tier A: chains of ten beads at hand-picked positions, written
|
|
4
|
+
as a data file and a dump by the ``make_chain_universe`` fixture. Expected
|
|
5
|
+
values are analytic: the end-to-end distance and the span of a straight
|
|
6
|
+
chain by hand, the radius of gyration of equally spaced beads from the
|
|
7
|
+
variance of a discrete uniform distribution, and the asphericity and shape
|
|
8
|
+
parameter of a cube's vertices (0, an isotropic gyration tensor) and of
|
|
9
|
+
collinear beads (1 and 2, eigenvalues (l, 0, 0)) from the formulas of Dima
|
|
10
|
+
and Thirumalai, J. Phys. Chem. B 108, 6564 (2004), as MDAnalysis
|
|
11
|
+
implements them.
|
|
12
|
+
|
|
13
|
+
The real-frame tests read the tier B and tier C files of the studies'
|
|
14
|
+
wholes (``polyphys/tests/probe/conftest.py``, "Real frames"). Their expected
|
|
15
|
+
values are properties or other files of the same run: the chain measured in
|
|
16
|
+
the ``all`` window at a time step the polymer dump shares, which holds the
|
|
17
|
+
same configuration; the FENE bound on a bond, :math:`R_0 = 1.5 \\sigma`
|
|
18
|
+
(``docs/source/design.md``), far below half the box, so a wrapped chain
|
|
19
|
+
whose bonds are all shorter than half the box is whole and unwrapping it
|
|
20
|
+
changes nothing; and the first level of the blocking transform of
|
|
21
|
+
Flyvbjerg and Petersen, J. Chem. Phys. 91, 461 (1989), whose standard
|
|
22
|
+
error is the naive one of the uncorrelated estimate.
|
|
23
|
+
"""
|
|
24
|
+
import numpy as np
|
|
25
|
+
import pytest
|
|
26
|
+
from numpy.testing import (
|
|
27
|
+
assert_allclose,
|
|
28
|
+
assert_array_equal,
|
|
29
|
+
assert_equal,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from polyphys.analyze.stats import block_average
|
|
33
|
+
from polyphys.tests.synthetic import ChainCyl
|
|
34
|
+
from polyphys.spec import (
|
|
35
|
+
CHAIN_SIZE_OBSERVABLES,
|
|
36
|
+
ProbeSpec,
|
|
37
|
+
ProjectSpec,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
pytest.importorskip("MDAnalysis")
|
|
41
|
+
|
|
42
|
+
from polyphys.probe import ChainSize # noqa: E402
|
|
43
|
+
|
|
44
|
+
# The dump reader warns that the frames carry no time step; the tests
|
|
45
|
+
# measure per frame and give no dt.
|
|
46
|
+
pytestmark = pytest.mark.filterwarnings(
|
|
47
|
+
"ignore:Reader has no dt information:UserWarning")
|
|
48
|
+
|
|
49
|
+
# Tolerances: MDAnalysis holds positions as float32, so a value carries a
|
|
50
|
+
# relative round-off of about 1e-7; 1e-6 leaves room for a few operations.
|
|
51
|
+
RTOL = 1e-6
|
|
52
|
+
ATOL = 1e-6
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.fixture
|
|
56
|
+
def straight_chain() -> np.ndarray:
|
|
57
|
+
"""Five frames of ten beads one unit apart along x, from x = 5, moved
|
|
58
|
+
0.1 along z per frame."""
|
|
59
|
+
first = np.zeros((10, 3))
|
|
60
|
+
first[:, 0] = np.arange(10) + 5.0
|
|
61
|
+
first[:, 1:] = 10.0
|
|
62
|
+
return np.stack([first + [0.0, 0.0, 0.1 * k] for k in range(5)])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.fixture
|
|
66
|
+
def cube_vertices() -> np.ndarray:
|
|
67
|
+
"""One frame of the eight vertices of a cube of side 2 about (10, 10,
|
|
68
|
+
10), plus two beads at its center so the chain has ten beads."""
|
|
69
|
+
corners = np.array([[sx, sy, sz] for sx in (-1.0, 1.0)
|
|
70
|
+
for sy in (-1.0, 1.0) for sz in (-1.0, 1.0)])
|
|
71
|
+
return (np.vstack([corners, np.zeros((2, 3))]) + 10.0)[np.newaxis]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Hand values
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_end_to_end_of_a_straight_chain(make_chain_universe, straight_chain):
|
|
78
|
+
"""Ten beads one unit apart along x span 9 from first to last, in every
|
|
79
|
+
frame."""
|
|
80
|
+
universe = make_chain_universe(straight_chain)
|
|
81
|
+
|
|
82
|
+
analysis = ChainSize(universe.atoms, ('rfloryT',)).run()
|
|
83
|
+
|
|
84
|
+
assert_allclose(analysis.results['rfloryT'], np.full(5, 9.0), rtol=RTOL)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_fsd_is_the_span_along_the_axis(make_chain_universe):
|
|
88
|
+
"""Beads at z = 10, 10.5, ..., 14.5 span 4.5 along z, unchanged when
|
|
89
|
+
the chain moves."""
|
|
90
|
+
first = np.full((10, 3), 10.0)
|
|
91
|
+
first[:, 2] = 10.0 + 0.5 * np.arange(10)
|
|
92
|
+
frames = np.stack([first, first + [1.0, -1.0, 2.0]])
|
|
93
|
+
universe = make_chain_universe(frames)
|
|
94
|
+
|
|
95
|
+
analysis = ChainSize(universe.atoms, ('fsdT',), axis=2).run()
|
|
96
|
+
|
|
97
|
+
assert_allclose(analysis.results['fsdT'], [4.5, 4.5], rtol=RTOL)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_transverse_size_of_beads_on_a_circle(make_chain_universe):
|
|
101
|
+
"""Ten beads on a circle of radius 2 in the xy plane have transverse
|
|
102
|
+
size 4 across z: twice the mean distance from their center."""
|
|
103
|
+
angles = 2 * np.pi * np.arange(10) / 10
|
|
104
|
+
first = np.full((10, 3), 10.0)
|
|
105
|
+
first[:, 0] += 2.0 * np.cos(angles)
|
|
106
|
+
first[:, 1] += 2.0 * np.sin(angles)
|
|
107
|
+
universe = make_chain_universe(first[np.newaxis])
|
|
108
|
+
|
|
109
|
+
analysis = ChainSize(universe.atoms, ('transSizeT',), axis=2).run()
|
|
110
|
+
|
|
111
|
+
assert_allclose(analysis.results['transSizeT'], [4.0], rtol=RTOL)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_radius_of_gyration_of_equally_spaced_beads(make_chain_universe,
|
|
115
|
+
straight_chain):
|
|
116
|
+
"""N equal beads one unit apart on a line have Rg^2 = (N^2 - 1) / 12."""
|
|
117
|
+
universe = make_chain_universe(straight_chain)
|
|
118
|
+
|
|
119
|
+
analysis = ChainSize(universe.atoms, ('gyrT',)).run()
|
|
120
|
+
|
|
121
|
+
assert_allclose(analysis.results['gyrT'], np.full(5, np.sqrt(99 / 12)),
|
|
122
|
+
rtol=RTOL)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# Property tests of the gyration-tensor shapes
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_asphericity_is_zero_for_a_cube_and_one_for_a_line(
|
|
129
|
+
make_chain_universe, cube_vertices, straight_chain):
|
|
130
|
+
"""A cube's vertices have an isotropic gyration tensor, asphericity 0;
|
|
131
|
+
collinear beads have eigenvalues (l, 0, 0), asphericity 1."""
|
|
132
|
+
cube = make_chain_universe(cube_vertices, name='cube')
|
|
133
|
+
line = make_chain_universe(straight_chain, name='line')
|
|
134
|
+
|
|
135
|
+
on_cube = ChainSize(cube.atoms, ('asphericityT',)).run()
|
|
136
|
+
on_line = ChainSize(line.atoms, ('asphericityT',)).run()
|
|
137
|
+
|
|
138
|
+
assert_allclose(on_cube.results['asphericityT'], [0.0], atol=ATOL)
|
|
139
|
+
assert_allclose(on_line.results['asphericityT'], np.ones(5), rtol=RTOL)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_shape_parameter_is_zero_for_a_cube_and_two_for_a_line(
|
|
143
|
+
make_chain_universe, cube_vertices, straight_chain):
|
|
144
|
+
"""The shape parameter 27 prod(l_k - mean) / (sum l_k)^3 is 0 for an
|
|
145
|
+
isotropic tensor and 2 for eigenvalues (l, 0, 0)."""
|
|
146
|
+
cube = make_chain_universe(cube_vertices, name='cube')
|
|
147
|
+
line = make_chain_universe(straight_chain, name='line')
|
|
148
|
+
|
|
149
|
+
on_cube = ChainSize(cube.atoms, ('shapeT',)).run()
|
|
150
|
+
on_line = ChainSize(line.atoms, ('shapeT',)).run()
|
|
151
|
+
|
|
152
|
+
assert_allclose(on_cube.results['shapeT'], [0.0], atol=ATOL)
|
|
153
|
+
assert_allclose(on_line.results['shapeT'], np.full(5, 2.0), rtol=RTOL)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_principal_axes_are_orthonormal_with_the_line_last(
|
|
157
|
+
make_chain_universe, straight_chain):
|
|
158
|
+
"""A straight chain along x has a (3, 3) orthonormal frame per frame
|
|
159
|
+
whose last axis, the smallest moment of inertia, is along x."""
|
|
160
|
+
universe = make_chain_universe(straight_chain)
|
|
161
|
+
|
|
162
|
+
axes = ChainSize(universe.atoms, ('principalT',)).run().results[
|
|
163
|
+
'principalT']
|
|
164
|
+
|
|
165
|
+
assert axes.shape == (5, 3, 3)
|
|
166
|
+
assert_allclose(axes @ axes.transpose(0, 2, 1),
|
|
167
|
+
np.broadcast_to(np.eye(3), (5, 3, 3)), atol=ATOL)
|
|
168
|
+
assert_allclose(np.abs(axes[:, 2, 0]), np.ones(5), rtol=RTOL)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_unwrap_reaches_the_asphericity(make_chain_universe):
|
|
172
|
+
"""A straight diagonal chain wrapped across a periodic face is straight
|
|
173
|
+
again when unwrapped, asphericity 1, and the whole-chain check leaves
|
|
174
|
+
an unwrapped asphericity alone."""
|
|
175
|
+
first = np.full((10, 3), 10.0)
|
|
176
|
+
first[:, 0] = np.arange(16.0, 26.0) % 20.0
|
|
177
|
+
first[:, 1] = np.arange(5.0, 15.0)
|
|
178
|
+
universe = make_chain_universe(first[np.newaxis], boundary='pp pp pp')
|
|
179
|
+
|
|
180
|
+
unwrapped = ChainSize(universe.atoms, ('asphericityT',),
|
|
181
|
+
unwrap=True).run()
|
|
182
|
+
|
|
183
|
+
assert_allclose(unwrapped.results['asphericityT'], [1.0], rtol=RTOL)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@pytest.fixture
|
|
187
|
+
def split_chain() -> np.ndarray:
|
|
188
|
+
"""Two frames of a four-bead chain with unit bonds along x in a box of
|
|
189
|
+
10: whole at x = 1.5 to 4.5, then wrapped at x = 8.5, 9.5, 0.5, 1.5,
|
|
190
|
+
its third bond split by the boundary."""
|
|
191
|
+
frames = np.full((2, 4, 3), 5.0)
|
|
192
|
+
frames[0, :, 0] = [1.5, 2.5, 3.5, 4.5]
|
|
193
|
+
frames[1, :, 0] = [8.5, 9.5, 0.5, 1.5]
|
|
194
|
+
return frames
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@pytest.mark.parametrize('observables, unwrap', [
|
|
198
|
+
pytest.param(('gyrT',), True, id='gyration-with-unwrap'),
|
|
199
|
+
pytest.param(('asphericityT',), False, id='asphericity-without-unwrap'),
|
|
200
|
+
pytest.param(('asphericityT', 'shapeT'), True,
|
|
201
|
+
id='shape-beside-unwrapped-asphericity'),
|
|
202
|
+
])
|
|
203
|
+
def test_chain_split_by_the_boundary_raises(make_chain_universe,
|
|
204
|
+
split_chain, observables,
|
|
205
|
+
unwrap):
|
|
206
|
+
"""A frame whose chain bond spans half the box or more stops the run,
|
|
207
|
+
naming the frame, for every observable that reads the wrapped
|
|
208
|
+
positions."""
|
|
209
|
+
universe = make_chain_universe(split_chain, box=((0.0, 10.0),) * 3,
|
|
210
|
+
boundary='pp pp pp')
|
|
211
|
+
analysis = ChainSize(universe.atoms, observables, unwrap=unwrap)
|
|
212
|
+
|
|
213
|
+
with pytest.raises(ValueError,
|
|
214
|
+
match="frame 1: 1 of 3 chain bonds span half the "
|
|
215
|
+
"box or more along x"):
|
|
216
|
+
analysis.run()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_whole_chain_past_the_box_edge_gives_its_radius_of_gyration(
|
|
220
|
+
make_chain_universe):
|
|
221
|
+
"""A whole chain that reaches past the box edge passes the check and
|
|
222
|
+
gives the analytic radius of gyration of four unit-spaced beads,
|
|
223
|
+
sqrt(5) / 2."""
|
|
224
|
+
frames = np.full((1, 4, 3), 5.0)
|
|
225
|
+
frames[0, :, 0] = [8.5, 9.5, 10.5, 11.5]
|
|
226
|
+
universe = make_chain_universe(frames, box=((0.0, 10.0),) * 3,
|
|
227
|
+
boundary='pp pp pp')
|
|
228
|
+
|
|
229
|
+
analysis = ChainSize(universe.atoms, ('gyrT',)).run()
|
|
230
|
+
|
|
231
|
+
assert_allclose(analysis.results['gyrT'], [np.sqrt(5.0) / 2.0],
|
|
232
|
+
rtol=RTOL)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_chain_without_bonds_is_not_checked():
|
|
236
|
+
"""Without bonds in the topology the check cannot run, and the wrapped
|
|
237
|
+
end-to-end distance is measured as it stands, 7 for beads at
|
|
238
|
+
x = 8.5 and 1.5."""
|
|
239
|
+
import MDAnalysis as mda
|
|
240
|
+
|
|
241
|
+
universe = mda.Universe.empty(4, trajectory=True)
|
|
242
|
+
universe.add_TopologyAttr('masses', np.ones(4))
|
|
243
|
+
universe.dimensions = [10.0, 10.0, 10.0, 90.0, 90.0, 90.0]
|
|
244
|
+
universe.atoms.positions = np.array(
|
|
245
|
+
[[8.5, 5.0, 5.0], [9.5, 5.0, 5.0], [0.5, 5.0, 5.0], [1.5, 5.0, 5.0]])
|
|
246
|
+
|
|
247
|
+
analysis = ChainSize(universe.atoms, ('rfloryT',)).run()
|
|
248
|
+
|
|
249
|
+
assert_allclose(analysis.results['rfloryT'], [7.0], rtol=RTOL)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# The analysis contract
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_every_declared_observable_is_measured(make_chain_universe,
|
|
256
|
+
straight_chain):
|
|
257
|
+
"""Every name in CHAIN_SIZE_OBSERVABLES gives one array per frame."""
|
|
258
|
+
universe = make_chain_universe(straight_chain)
|
|
259
|
+
|
|
260
|
+
analysis = ChainSize(universe.atoms, CHAIN_SIZE_OBSERVABLES).run()
|
|
261
|
+
|
|
262
|
+
assert tuple(analysis.results) == CHAIN_SIZE_OBSERVABLES
|
|
263
|
+
assert {analysis.results[obs].shape[0]
|
|
264
|
+
for obs in CHAIN_SIZE_OBSERVABLES} == {5}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def test_run_slices_the_frames(make_chain_universe, straight_chain):
|
|
268
|
+
"""run(start, stop, step) measures only those frames, in order."""
|
|
269
|
+
universe = make_chain_universe(straight_chain)
|
|
270
|
+
|
|
271
|
+
analysis = ChainSize(universe.atoms, ('fsdT',)).run(
|
|
272
|
+
start=1, stop=5, step=2)
|
|
273
|
+
|
|
274
|
+
assert_array_equal(analysis.frames, [1, 3])
|
|
275
|
+
# The chain's z span is 0: its ten beads share one z in every frame.
|
|
276
|
+
assert_allclose(analysis.results['fsdT'], [0.0, 0.0], atol=ATOL)
|
|
277
|
+
assert analysis.n_frames == 2
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def test_parallel_run_matches_the_serial_run(make_chain_universe,
|
|
281
|
+
straight_chain):
|
|
282
|
+
"""Two workers over two parts give the serial arrays, scalar and
|
|
283
|
+
matrix series alike."""
|
|
284
|
+
universe = make_chain_universe(straight_chain)
|
|
285
|
+
observables = ('rfloryT', 'gyrT', 'principalT')
|
|
286
|
+
serial = ChainSize(universe.atoms, observables).run()
|
|
287
|
+
|
|
288
|
+
parallel = ChainSize(universe.atoms, observables).run(
|
|
289
|
+
backend='multiprocessing', n_workers=2, n_parts=2)
|
|
290
|
+
|
|
291
|
+
for obs in observables:
|
|
292
|
+
assert_array_equal(parallel.results[obs], serial.results[obs])
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@pytest.mark.parametrize('n_parts', [
|
|
296
|
+
pytest.param(2, id='two-parts'),
|
|
297
|
+
pytest.param(3, id='three-parts'),
|
|
298
|
+
])
|
|
299
|
+
def test_serial_parts_match_the_multiprocessing_run(
|
|
300
|
+
make_chain_universe, straight_chain, n_parts):
|
|
301
|
+
"""The serial backend split into parts gives the frames, times, and
|
|
302
|
+
arrays of the multiprocessing backend over the same parts: the result
|
|
303
|
+
does not depend on the backend."""
|
|
304
|
+
universe = make_chain_universe(straight_chain)
|
|
305
|
+
observables = ('rfloryT', 'gyrT', 'principalT')
|
|
306
|
+
parallel = ChainSize(universe.atoms, observables).run(
|
|
307
|
+
backend='multiprocessing', n_workers=2, n_parts=n_parts)
|
|
308
|
+
|
|
309
|
+
serial = ChainSize(universe.atoms, observables).run(
|
|
310
|
+
backend='serial', n_parts=n_parts)
|
|
311
|
+
|
|
312
|
+
assert_array_equal(serial.frames, parallel.frames)
|
|
313
|
+
assert_array_equal(serial.times, parallel.times)
|
|
314
|
+
assert_equal(dict(serial.results), dict(parallel.results))
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def test_from_spec_selects_the_chain_and_carries_the_spec(
|
|
318
|
+
make_chain_universe, straight_chain):
|
|
319
|
+
"""from_spec picks the spec's chain selection and its observables,
|
|
320
|
+
chain label, and unwrap."""
|
|
321
|
+
project = ProjectSpec(
|
|
322
|
+
parser=ChainCyl, group='bug', divisor=0.025,
|
|
323
|
+
crowder_attr='phi_bulk_c',
|
|
324
|
+
physical_attrs=('space', 'nmon'),
|
|
325
|
+
species_sizes={'Mon': 'dmon', 'Crd': 'dcrowd'},
|
|
326
|
+
tseries_properties=('fsdTMon', 'gyrTMon'))
|
|
327
|
+
spec = ProbeSpec(project, {'Mon': 'resid 1', 'Crd': 'resid 0'}, 'Mon',
|
|
328
|
+
('fsdT', 'gyrT'), unwrap=True)
|
|
329
|
+
universe = make_chain_universe(straight_chain)
|
|
330
|
+
|
|
331
|
+
analysis = ChainSize.from_spec(universe, spec)
|
|
332
|
+
|
|
333
|
+
assert (analysis.atomgroup.n_atoms, analysis.observables,
|
|
334
|
+
analysis.chain, analysis.unwrap) == (
|
|
335
|
+
10, ('fsdT', 'gyrT'), 'Mon', True)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@pytest.mark.parametrize('kwargs, match', [
|
|
339
|
+
pytest.param({'observables': ()}, "must not be empty",
|
|
340
|
+
id='no-observables'),
|
|
341
|
+
pytest.param({'observables': ('gyrT', 'radius')},
|
|
342
|
+
r"names \['radius'\], which are not in",
|
|
343
|
+
id='unknown-observable'),
|
|
344
|
+
pytest.param({'observables': ('gyrT', 'gyrT')}, "repeats a name",
|
|
345
|
+
id='repeated-observable'),
|
|
346
|
+
pytest.param({'chain': ''}, "'chain' must be a non-empty",
|
|
347
|
+
id='empty-chain'),
|
|
348
|
+
pytest.param({'axis': 3}, "'axis' must be 0, 1, or 2", id='bad-axis'),
|
|
349
|
+
])
|
|
350
|
+
def test_rejects_invalid_arguments(make_chain_universe, straight_chain,
|
|
351
|
+
kwargs, match):
|
|
352
|
+
"""An empty, unknown, or repeated observable, an empty chain label, or
|
|
353
|
+
an axis outside 0 to 2 raises at construction."""
|
|
354
|
+
universe = make_chain_universe(straight_chain)
|
|
355
|
+
fields = {'observables': ('gyrT',), **kwargs}
|
|
356
|
+
|
|
357
|
+
with pytest.raises(ValueError, match=match):
|
|
358
|
+
ChainSize(universe.atoms, **fields)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def test_rejects_a_chain_of_one_bead(make_chain_universe, straight_chain):
|
|
362
|
+
"""A chain needs at least two beads."""
|
|
363
|
+
universe = make_chain_universe(straight_chain)
|
|
364
|
+
|
|
365
|
+
with pytest.raises(ValueError, match="at least two beads"):
|
|
366
|
+
ChainSize(universe.select_atoms('id 1'), ('gyrT',))
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# save
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def test_save_writes_one_file_per_observable(make_chain_universe,
|
|
373
|
+
straight_chain, tmp_path):
|
|
374
|
+
"""save writes <name>-<observable><chain>.npy per observable, holding
|
|
375
|
+
the results."""
|
|
376
|
+
universe = make_chain_universe(straight_chain)
|
|
377
|
+
analysis = ChainSize(universe.atoms, ('rfloryT', 'principalT'),
|
|
378
|
+
chain='Mon').run()
|
|
379
|
+
|
|
380
|
+
paths = analysis.save(tmp_path, 'N10ens1-bug')
|
|
381
|
+
|
|
382
|
+
assert [path.name for path in paths] == [
|
|
383
|
+
'N10ens1-bug-rfloryTMon.npy', 'N10ens1-bug-principalTMon.npy']
|
|
384
|
+
assert_array_equal(np.load(paths[0]), analysis.results['rfloryT'])
|
|
385
|
+
assert_array_equal(np.load(paths[1]), analysis.results['principalT'])
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def test_save_before_run_raises(make_chain_universe, straight_chain,
|
|
389
|
+
tmp_path):
|
|
390
|
+
"""save needs the results of a run."""
|
|
391
|
+
universe = make_chain_universe(straight_chain)
|
|
392
|
+
analysis = ChainSize(universe.atoms, ('rfloryT',))
|
|
393
|
+
|
|
394
|
+
with pytest.raises(RuntimeError, match=r"Call run\(\) before save"):
|
|
395
|
+
analysis.save(tmp_path, 'N10ens1-bug')
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# Real frames
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _at_shared_steps(polymer, window, probe, trajectory_arrays):
|
|
402
|
+
"""The spec's observables of the chain in the polymer dump and in the
|
|
403
|
+
window, at the time steps both hold, each flattened and joined in the
|
|
404
|
+
spec's order; and the number of those steps."""
|
|
405
|
+
polymer_steps = trajectory_arrays(polymer)['steps']
|
|
406
|
+
window_steps = trajectory_arrays(window)['steps']
|
|
407
|
+
shared = np.intersect1d(polymer_steps, window_steps)
|
|
408
|
+
values = [
|
|
409
|
+
ChainSize.from_spec(universe, probe).run(
|
|
410
|
+
frames=np.flatnonzero(np.isin(steps, shared))).results
|
|
411
|
+
for universe, steps in ((polymer, polymer_steps),
|
|
412
|
+
(window, window_steps))]
|
|
413
|
+
return ([np.concatenate([result[obs].ravel()
|
|
414
|
+
for obs in probe.observables])
|
|
415
|
+
for result in values], shared.size)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def test_real_chain_size_agrees_between_the_polymer_dump_and_the_window(
|
|
419
|
+
real_window_whole, real_files, real_universe, study_spec,
|
|
420
|
+
trajectory_arrays):
|
|
421
|
+
"""At every time step the polymer dump and the ``all`` window share,
|
|
422
|
+
each chain-size observable of the spec is the same in both."""
|
|
423
|
+
probe = study_spec(real_window_whole.study).PROBE
|
|
424
|
+
files = real_files(real_window_whole)
|
|
425
|
+
polymer = real_universe(files.topology, files.polymer)
|
|
426
|
+
window = real_universe(files.data, files.window)
|
|
427
|
+
|
|
428
|
+
(from_polymer, from_window), n_shared = _at_shared_steps(
|
|
429
|
+
polymer, window, probe, trajectory_arrays)
|
|
430
|
+
|
|
431
|
+
assert n_shared == real_window_whole.n_shared_frames
|
|
432
|
+
assert_allclose(from_polymer, from_window, rtol=RTOL, atol=ATOL)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@pytest.mark.private_data # pragma: no cover - needs data
|
|
436
|
+
def test_real_chain_size_agrees_with_the_crowded_window(
|
|
437
|
+
real_crowded_whole, real_files, private_window_files, real_universe,
|
|
438
|
+
study_spec, trajectory_arrays):
|
|
439
|
+
"""In a crowded whole, the polymer dump opened with the borrowed
|
|
440
|
+
topology and the ``all`` window opened with the whole's own data file
|
|
441
|
+
give the same chain-size observables at every shared time step."""
|
|
442
|
+
probe = study_spec(real_crowded_whole.study).PROBE
|
|
443
|
+
files = real_files(real_crowded_whole)
|
|
444
|
+
window_path, data_path = private_window_files(real_crowded_whole)
|
|
445
|
+
polymer = real_universe(files.topology, files.polymer)
|
|
446
|
+
window = real_universe(data_path, window_path)
|
|
447
|
+
|
|
448
|
+
(from_polymer, from_window), n_shared = _at_shared_steps(
|
|
449
|
+
polymer, window, probe, trajectory_arrays)
|
|
450
|
+
|
|
451
|
+
assert n_shared == real_crowded_whole.n_shared_frames
|
|
452
|
+
assert_allclose(from_polymer, from_window, rtol=RTOL, atol=ATOL)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def test_real_chain_bonds_are_shorter_than_half_the_periodic_box(
|
|
456
|
+
real_polymer_whole, real_files, real_universe, study_spec,
|
|
457
|
+
trajectory_arrays):
|
|
458
|
+
"""In every frame of a polymer dump, every bond of the chain spans less
|
|
459
|
+
than half the box along each periodic direction: the wrapping splits no
|
|
460
|
+
chain. (Free H-NS proteins do cross the boundary of the periodic cube;
|
|
461
|
+
their core-patch bonds are not checked.)"""
|
|
462
|
+
probe = study_spec(real_polymer_whole.study).PROBE
|
|
463
|
+
files = real_files(real_polymer_whole)
|
|
464
|
+
universe = real_universe(files.topology, files.polymer)
|
|
465
|
+
arrays = trajectory_arrays(universe)
|
|
466
|
+
first, second = universe.select_atoms(
|
|
467
|
+
probe.selections[probe.chain]).bonds.indices.T
|
|
468
|
+
periodic = {'cubic': [0, 1, 2], 'cylindrical': [2]}[probe.geometry]
|
|
469
|
+
|
|
470
|
+
spans = np.abs(arrays['positions'][:, first] -
|
|
471
|
+
arrays['positions'][:, second])[..., periodic]
|
|
472
|
+
halves = 0.5 * arrays['boxes'][:, None, periodic]
|
|
473
|
+
|
|
474
|
+
assert np.count_nonzero(spans >= halves) == 0
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def test_real_asphericity_does_not_depend_on_unwrap(
|
|
478
|
+
real_polymer_whole, real_files, real_universe, study_spec):
|
|
479
|
+
"""On real chains that no boundary splits, the asphericity is the same
|
|
480
|
+
whether or not the chain is unwrapped first, in every study."""
|
|
481
|
+
probe = study_spec(real_polymer_whole.study).PROBE
|
|
482
|
+
files = real_files(real_polymer_whole)
|
|
483
|
+
chain = real_universe(files.topology, files.polymer).select_atoms(
|
|
484
|
+
probe.selections[probe.chain])
|
|
485
|
+
|
|
486
|
+
wrapped, unwrapped = (
|
|
487
|
+
ChainSize(chain, ('asphericityT',), unwrap=unwrap).run().results[
|
|
488
|
+
'asphericityT'] for unwrap in (False, True))
|
|
489
|
+
|
|
490
|
+
assert_allclose(unwrapped, wrapped, rtol=RTOL, atol=ATOL)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def test_block_average_of_a_real_gyration_series_starts_at_the_naive_error(
|
|
494
|
+
real_polymer_whole, real_files, real_universe, study_spec):
|
|
495
|
+
"""Block averaging runs on the 50-frame radius-of-gyration series of a
|
|
496
|
+
real chain, with finite errors at every level and the naive standard
|
|
497
|
+
error at the first."""
|
|
498
|
+
probe = study_spec(real_polymer_whole.study).PROBE
|
|
499
|
+
files = real_files(real_polymer_whole)
|
|
500
|
+
chain = real_universe(files.topology, files.polymer).select_atoms(
|
|
501
|
+
probe.selections[probe.chain])
|
|
502
|
+
series = ChainSize(chain, ('gyrT',)).run().results['gyrT']
|
|
503
|
+
|
|
504
|
+
result = block_average(series)
|
|
505
|
+
|
|
506
|
+
assert np.all(np.isfinite(result.sem))
|
|
507
|
+
# Both are float64 reductions of the same 50 values.
|
|
508
|
+
assert result.sem[0] == pytest.approx(
|
|
509
|
+
series.std(ddof=1) / np.sqrt(series.size), rel=1e-12)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
# Against the research code's probe (tier C)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
@pytest.mark.private_data # pragma: no cover - needs data
|
|
516
|
+
@pytest.mark.parametrize('observable', ['gyrT', 'shapeT', 'asphericityT'])
|
|
517
|
+
def test_real_chain_size_matches_the_research_code(
|
|
518
|
+
real_transfocicub_whole, real_files, real_universe, study_spec,
|
|
519
|
+
research_probe_file, observable):
|
|
520
|
+
"""On the 50 frames of a TransFociCub polymer dump, each scalar
|
|
521
|
+
chain-size observable of the spec equals the research code's probe of
|
|
522
|
+
the same frames (SciOps ``trans_foci_cub_bug``)."""
|
|
523
|
+
whole = real_transfocicub_whole
|
|
524
|
+
probe = study_spec(whole.study).PROBE
|
|
525
|
+
files = real_files(whole)
|
|
526
|
+
universe = real_universe(files.topology, files.polymer)
|
|
527
|
+
expected = np.load(research_probe_file(whole, f"{observable}Mon"))
|
|
528
|
+
|
|
529
|
+
results = ChainSize.from_spec(universe, probe).run().results
|
|
530
|
+
|
|
531
|
+
# The research code shifted the float32 positions by half the box
|
|
532
|
+
# before measuring, and this probe recenters the chain, so the two
|
|
533
|
+
# differ by the float32 round-off of coordinates of about 20: 1e-6.
|
|
534
|
+
assert_allclose(results[observable], expected, rtol=RTOL, atol=ATOL)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
@pytest.mark.private_data # pragma: no cover - needs data
|
|
538
|
+
def test_real_principal_axes_match_the_research_code_up_to_sign(
|
|
539
|
+
real_transfocicub_whole, real_files, real_universe, study_spec,
|
|
540
|
+
research_probe_file):
|
|
541
|
+
"""On the 50 frames of a TransFociCub polymer dump, each frame's
|
|
542
|
+
principal axes equal the research code's up to the sign of each axis,
|
|
543
|
+
which the eigenvector solver sets arbitrarily."""
|
|
544
|
+
whole = real_transfocicub_whole
|
|
545
|
+
probe = study_spec(whole.study).PROBE
|
|
546
|
+
files = real_files(whole)
|
|
547
|
+
universe = real_universe(files.topology, files.polymer)
|
|
548
|
+
expected = np.load(research_probe_file(whole, 'principalTMon'))
|
|
549
|
+
|
|
550
|
+
axes = ChainSize.from_spec(universe, probe).run().results['principalT']
|
|
551
|
+
|
|
552
|
+
# Per frame and axis, the smaller of |a - b| and |a + b|; the same
|
|
553
|
+
# float32 round-off as the scalar observables.
|
|
554
|
+
closest = np.minimum(np.linalg.norm(axes - expected, axis=-1),
|
|
555
|
+
np.linalg.norm(axes + expected, axis=-1))
|
|
556
|
+
assert closest.shape == (50, 3)
|
|
557
|
+
assert_allclose(closest, np.zeros_like(closest), rtol=0, atol=ATOL)
|