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,475 @@
|
|
|
1
|
+
"""Tests for :mod:`polyphys.probe.pairs`.
|
|
2
|
+
|
|
3
|
+
The inputs are tier A: a chain of ten beads of which three, the foci, sit
|
|
4
|
+
at hand-picked positions, written as a data file and a dump by the
|
|
5
|
+
``make_chain_universe`` fixture. Expected values are analytic: the sides
|
|
6
|
+
of a 3-4-5 triangle, the contacts, bonds, and clusters they give at a
|
|
7
|
+
cutoff of 3.5 by hand, the minimum-image distance across a periodic
|
|
8
|
+
boundary by hand, and the backbone indices of the foci from their places
|
|
9
|
+
in the chain.
|
|
10
|
+
|
|
11
|
+
The real-frame tests run the TransFoci studies' probe specs on the tier B
|
|
12
|
+
polymer dumps of their wholes (``polyphys/tests/probe/conftest.py``, "Real
|
|
13
|
+
frames"). Their expected values are the foci's places along the ring in the
|
|
14
|
+
data file, read with :func:`polyphys.simulate.data.read_data`; the
|
|
15
|
+
minimum-image distances of :func:`MDAnalysis.lib.distances.distance_array`;
|
|
16
|
+
and the conservation of the foci among the bonds and the clusters.
|
|
17
|
+
"""
|
|
18
|
+
import numpy as np
|
|
19
|
+
import pytest
|
|
20
|
+
from numpy.testing import (
|
|
21
|
+
assert_allclose,
|
|
22
|
+
assert_array_equal,
|
|
23
|
+
assert_equal,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from polyphys.tests.synthetic import RingCyl
|
|
27
|
+
from polyphys.spec import ProbeSpec, ProjectSpec
|
|
28
|
+
from polyphys.simulate.data import read_data
|
|
29
|
+
|
|
30
|
+
pytest.importorskip("MDAnalysis")
|
|
31
|
+
|
|
32
|
+
from polyphys.probe import PairDistances # noqa: E402
|
|
33
|
+
|
|
34
|
+
# The dump reader warns that the frames carry no time step; the tests
|
|
35
|
+
# measure per frame and give no dt.
|
|
36
|
+
pytestmark = pytest.mark.filterwarnings(
|
|
37
|
+
"ignore:Reader has no dt information:UserWarning")
|
|
38
|
+
|
|
39
|
+
# Tolerances: MDAnalysis holds positions as float32, so a distance carries a
|
|
40
|
+
# relative round-off of about 1e-7; 1e-6 leaves room for a few operations.
|
|
41
|
+
RTOL = 1e-6
|
|
42
|
+
ATOL = 1e-6
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.fixture
|
|
46
|
+
def types() -> list[int]:
|
|
47
|
+
"""The atom type of each of ten beads: the foci, type 2, are beads 3,
|
|
48
|
+
6, and 9 (LAMMPS IDs), at backbone indices 2, 5, and 8."""
|
|
49
|
+
return [1, 1, 2, 1, 1, 2, 1, 1, 2, 1]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.fixture
|
|
53
|
+
def triangle(types) -> np.ndarray:
|
|
54
|
+
"""Two frames of ten beads about (10, 10, 10): the foci at the corners
|
|
55
|
+
of a 3-4-5 triangle, (0, 0, 0), (3, 0, 0), and (3, 4, 0) from the
|
|
56
|
+
center, the other beads at the center; in frame 1 the third focus moves
|
|
57
|
+
to (3, 3, 0), 3 from the second and sqrt(18) from the first."""
|
|
58
|
+
first = np.full((10, 3), 10.0)
|
|
59
|
+
first[2] += [0.0, 0.0, 0.0]
|
|
60
|
+
first[5] += [3.0, 0.0, 0.0]
|
|
61
|
+
first[8] += [3.0, 4.0, 0.0]
|
|
62
|
+
second = first.copy()
|
|
63
|
+
second[8] = [13.0, 13.0, 10.0]
|
|
64
|
+
return np.stack([first, second])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.fixture
|
|
68
|
+
def make_analysis(make_chain_universe, triangle, types):
|
|
69
|
+
"""Factory: the analysis of the three foci of the triangle frames, with
|
|
70
|
+
any argument overridden; `positions` replaces the frames."""
|
|
71
|
+
|
|
72
|
+
def _make(positions=None, **overrides):
|
|
73
|
+
universe = make_chain_universe(
|
|
74
|
+
triangle if positions is None else positions,
|
|
75
|
+
atom_types=types, masses=(1.0, 1.0))
|
|
76
|
+
kwargs = {'cutoff': 3.5, 'genomic_positions': [2, 5, 8]}
|
|
77
|
+
kwargs.update(overrides)
|
|
78
|
+
return PairDistances(universe.select_atoms('type 2'), **kwargs)
|
|
79
|
+
|
|
80
|
+
return _make
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Hand values
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_distance_matrix_holds_the_upper_triangle_and_the_genomic_diagonal(
|
|
87
|
+
make_analysis):
|
|
88
|
+
"""Frame 0 gives the triangle's sides 3, 5, and 4 above the diagonal,
|
|
89
|
+
the backbone indices 2, 5, 8 on it, and zeros below."""
|
|
90
|
+
analysis = make_analysis().run()
|
|
91
|
+
|
|
92
|
+
matrix = analysis.results['distMatTFoci']
|
|
93
|
+
|
|
94
|
+
assert matrix.shape == (2, 3, 3)
|
|
95
|
+
assert matrix.dtype == np.float64
|
|
96
|
+
assert_allclose(matrix[0], [[2.0, 3.0, 5.0],
|
|
97
|
+
[0.0, 5.0, 4.0],
|
|
98
|
+
[0.0, 0.0, 8.0]], rtol=RTOL, atol=ATOL)
|
|
99
|
+
assert_allclose(matrix[1], [[2.0, 3.0, np.sqrt(18.0)],
|
|
100
|
+
[0.0, 5.0, 3.0],
|
|
101
|
+
[0.0, 0.0, 8.0]], rtol=RTOL, atol=ATOL)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_contacts_bonds_and_clusters_at_the_cutoff(make_analysis):
|
|
105
|
+
"""At a cutoff of 3.5, foci 1 and 2 (3 apart) touch and focus 3 is
|
|
106
|
+
alone in frame 0: one focus with no contact and two with one each,
|
|
107
|
+
bonds [1, 2, 0], and one cluster of size 1 and one of size 2."""
|
|
108
|
+
analysis = make_analysis().run()
|
|
109
|
+
|
|
110
|
+
contacts = analysis.results['directContactsMatTFoci'][0]
|
|
111
|
+
|
|
112
|
+
assert contacts.dtype == np.bool_
|
|
113
|
+
assert_array_equal(contacts, [[True, True, False],
|
|
114
|
+
[True, True, False],
|
|
115
|
+
[False, False, True]])
|
|
116
|
+
assert_array_equal(analysis.results['bondsHistTFoci'][0], [1, 2, 0])
|
|
117
|
+
assert_array_equal(analysis.results['clustersHistTFoci'][0],
|
|
118
|
+
[0, 1, 1, 0])
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_frame_1_forms_a_chain_of_three(make_analysis):
|
|
122
|
+
"""In frame 1, focus 3 touches focus 2 (3 apart) but not focus 1
|
|
123
|
+
(sqrt(18) apart): the bonds are [0, 2, 1], one cluster of size 3."""
|
|
124
|
+
analysis = make_analysis().run()
|
|
125
|
+
|
|
126
|
+
assert_array_equal(analysis.results['bondsHistTFoci'][1], [0, 2, 1])
|
|
127
|
+
assert_array_equal(analysis.results['clustersHistTFoci'][1],
|
|
128
|
+
[0, 0, 0, 1])
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@pytest.mark.parametrize('inclusive, bonds', [
|
|
132
|
+
pytest.param(True, [1, 2, 0], id='at-the-cutoff-touches'),
|
|
133
|
+
pytest.param(False, [3, 0, 0], id='at-the-cutoff-does-not'),
|
|
134
|
+
])
|
|
135
|
+
def test_inclusive_decides_a_pair_at_the_cutoff(make_analysis, inclusive,
|
|
136
|
+
bonds):
|
|
137
|
+
"""A pair exactly at the cutoff, 3 apart with cutoff 3, is in contact
|
|
138
|
+
only when the cutoff is inclusive."""
|
|
139
|
+
analysis = make_analysis(cutoff=3.0, inclusive=inclusive).run()
|
|
140
|
+
|
|
141
|
+
assert_array_equal(analysis.results['bondsHistTFoci'][0], bonds)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Minimum image
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@pytest.mark.parametrize('periodic, distance, contact', [
|
|
148
|
+
pytest.param((False, False, True), 1.0, True, id='z-periodic'),
|
|
149
|
+
pytest.param(None, 19.0, False, id='no-minimum-image'),
|
|
150
|
+
])
|
|
151
|
+
def test_minimum_image_along_the_periodic_axis(make_chain_universe, types,
|
|
152
|
+
periodic, distance, contact):
|
|
153
|
+
"""Two foci at z = 0.5 and 19.5 in a box of length 20 are 1 apart with
|
|
154
|
+
the minimum image along z and 19 without."""
|
|
155
|
+
positions = np.full((1, 10, 3), 10.0)
|
|
156
|
+
positions[0, 2] = [10.0, 10.0, 0.5]
|
|
157
|
+
positions[0, 5] = [10.0, 10.0, 19.5]
|
|
158
|
+
positions[0, 8] = [15.0, 10.0, 10.0]
|
|
159
|
+
universe = make_chain_universe(positions, atom_types=types,
|
|
160
|
+
masses=(1.0, 1.0))
|
|
161
|
+
|
|
162
|
+
analysis = PairDistances(universe.select_atoms('type 2'), 1.5,
|
|
163
|
+
[2, 5, 8], periodic=periodic).run()
|
|
164
|
+
|
|
165
|
+
assert analysis.results['distMatTFoci'][0, 0, 1] == pytest.approx(
|
|
166
|
+
distance, rel=RTOL)
|
|
167
|
+
assert analysis.results['directContactsMatTFoci'][0, 0, 1] is np.bool_(
|
|
168
|
+
contact)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_numpy_boolean_periodic_flags_act_as_booleans(make_analysis):
|
|
172
|
+
"""Periodic flags given as numpy booleans give the run of the same
|
|
173
|
+
flags given as Python booleans."""
|
|
174
|
+
expected = make_analysis(periodic=(False, False, True)).run()
|
|
175
|
+
|
|
176
|
+
analysis = make_analysis(
|
|
177
|
+
periodic=(np.bool_(False), np.bool_(False), np.bool_(True))).run()
|
|
178
|
+
|
|
179
|
+
assert analysis.periodic == (False, False, True)
|
|
180
|
+
assert_equal(dict(analysis.results), dict(expected.results))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_periodic_direction_needs_a_box():
|
|
184
|
+
"""A frame without box dimensions cannot take the minimum image, and
|
|
185
|
+
the run raises ValueError, as the other probes do."""
|
|
186
|
+
import MDAnalysis as mda
|
|
187
|
+
|
|
188
|
+
universe = mda.Universe.empty(2, trajectory=True)
|
|
189
|
+
universe.atoms.positions = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]])
|
|
190
|
+
analysis = PairDistances(universe.atoms, 1.5, [0, 1],
|
|
191
|
+
periodic=(False, False, True))
|
|
192
|
+
|
|
193
|
+
with pytest.raises(ValueError, match="carries no box"):
|
|
194
|
+
analysis.run()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# Parallel
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_parallel_run_matches_serial(make_analysis):
|
|
201
|
+
"""Two workers over two parts give the same four arrays as one."""
|
|
202
|
+
serial = make_analysis().run()
|
|
203
|
+
parallel = make_analysis().run(backend='multiprocessing', n_workers=2,
|
|
204
|
+
n_parts=2)
|
|
205
|
+
|
|
206
|
+
for key in serial.results:
|
|
207
|
+
assert_array_equal(parallel.results[key], serial.results[key])
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_serial_parts_match_the_multiprocessing_run(make_analysis):
|
|
211
|
+
"""The serial backend split into two parts, one frame each, gives the
|
|
212
|
+
frames, times, and arrays of the multiprocessing backend over the same
|
|
213
|
+
parts: the result does not depend on the backend."""
|
|
214
|
+
parallel = make_analysis().run(backend='multiprocessing', n_workers=2,
|
|
215
|
+
n_parts=2)
|
|
216
|
+
|
|
217
|
+
serial = make_analysis().run(backend='serial', n_parts=2)
|
|
218
|
+
|
|
219
|
+
assert_array_equal(serial.frames, parallel.frames)
|
|
220
|
+
assert_array_equal(serial.times, parallel.times)
|
|
221
|
+
assert_equal(dict(serial.results), dict(parallel.results))
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# from_spec
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@pytest.fixture
|
|
228
|
+
def foci_spec():
|
|
229
|
+
"""A RingCyl probe spec whose pair species is the foci, with the
|
|
230
|
+
research code's cutoff, the large-monomer and crowder diameters summed."""
|
|
231
|
+
project = ProjectSpec(
|
|
232
|
+
parser=RingCyl, group='bug', divisor=0.025,
|
|
233
|
+
crowder_attr='phi_bulk_c',
|
|
234
|
+
physical_attrs=('space', 'nmon_small', 'nmon_large', 'dcyl'),
|
|
235
|
+
species_sizes={'Mon': 'dmon_small', 'Foci': 'dmon_large',
|
|
236
|
+
'Crd': 'dcrowd'},
|
|
237
|
+
tseries_properties=('gyrTMon',))
|
|
238
|
+
|
|
239
|
+
def cutoff(info):
|
|
240
|
+
return info.dmon_large + info.dcrowd
|
|
241
|
+
|
|
242
|
+
return ProbeSpec(
|
|
243
|
+
project, {'Mon': 'resid 1', 'Foci': 'type 2'}, 'Mon', ('gyrT',),
|
|
244
|
+
unwrap=False, pair_species='Foci', contact_cutoff=cutoff)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@pytest.fixture
|
|
248
|
+
def foci_whole():
|
|
249
|
+
"""A RingCyl whole with dmon_large 2 and dcrowd 1.5, so the cutoff
|
|
250
|
+
is 3.5."""
|
|
251
|
+
return RingCyl(
|
|
252
|
+
'r5al2nl3ml8ns7ac1.5nc0lz10dt0.005bdump1adump1ens1',
|
|
253
|
+
'whole', 'bug')
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_from_spec_takes_backbone_indices_cutoff_and_periodicity(
|
|
257
|
+
make_chain_universe, triangle, types, foci_spec, foci_whole):
|
|
258
|
+
"""from_spec selects the foci, places them at their 0-based positions
|
|
259
|
+
among the chain's atoms, resolves the cutoff for the whole, and uses
|
|
260
|
+
the minimum image along the cylinder axis."""
|
|
261
|
+
universe = make_chain_universe(triangle, atom_types=types,
|
|
262
|
+
masses=(1.0, 1.0))
|
|
263
|
+
|
|
264
|
+
analysis = PairDistances.from_spec(universe, foci_spec, foci_whole)
|
|
265
|
+
|
|
266
|
+
assert analysis.atomgroup.n_atoms == 3
|
|
267
|
+
assert_array_equal(analysis.genomic_positions, [2, 5, 8])
|
|
268
|
+
assert (analysis.cutoff, analysis.species, analysis.periodic,
|
|
269
|
+
analysis.inclusive) == (3.5, 'Foci', (False, False, True), True)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_from_spec_without_minimum_image(make_chain_universe, triangle, types,
|
|
273
|
+
foci_spec, foci_whole):
|
|
274
|
+
"""A spec with minimum_image False gives no periodic direction."""
|
|
275
|
+
spec = ProbeSpec(
|
|
276
|
+
foci_spec.project, dict(foci_spec.selections), 'Mon', ('gyrT',),
|
|
277
|
+
unwrap=False, pair_species='Foci',
|
|
278
|
+
contact_cutoff=foci_spec.contact_cutoff, minimum_image=False)
|
|
279
|
+
universe = make_chain_universe(triangle, atom_types=types,
|
|
280
|
+
masses=(1.0, 1.0))
|
|
281
|
+
|
|
282
|
+
analysis = PairDistances.from_spec(universe, spec, foci_whole)
|
|
283
|
+
|
|
284
|
+
assert analysis.periodic is None
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_from_spec_needs_a_pair_species(make_chain_universe, triangle, types,
|
|
288
|
+
foci_spec, foci_whole):
|
|
289
|
+
"""A spec without a pair species cannot build the analysis."""
|
|
290
|
+
spec = ProbeSpec(foci_spec.project, dict(foci_spec.selections), 'Mon',
|
|
291
|
+
('gyrT',), unwrap=False)
|
|
292
|
+
universe = make_chain_universe(triangle, atom_types=types,
|
|
293
|
+
masses=(1.0, 1.0))
|
|
294
|
+
|
|
295
|
+
with pytest.raises(ValueError, match="names no pair species"):
|
|
296
|
+
PairDistances.from_spec(universe, spec, foci_whole)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def test_from_spec_rejects_foci_outside_the_chain(make_chain_universe,
|
|
300
|
+
triangle, types, foci_spec,
|
|
301
|
+
foci_whole):
|
|
302
|
+
"""A pair species whose atoms are not among the chain's raises, naming
|
|
303
|
+
their IDs."""
|
|
304
|
+
spec = ProbeSpec(
|
|
305
|
+
foci_spec.project, {'Mon': 'type 1', 'Foci': 'type 2'}, 'Mon',
|
|
306
|
+
('gyrT',), unwrap=False, pair_species='Foci',
|
|
307
|
+
contact_cutoff=foci_spec.contact_cutoff)
|
|
308
|
+
universe = make_chain_universe(triangle, atom_types=types,
|
|
309
|
+
masses=(1.0, 1.0))
|
|
310
|
+
|
|
311
|
+
with pytest.raises(ValueError, match=r"IDs \[3, 6, 9\] are not among"):
|
|
312
|
+
PairDistances.from_spec(universe, spec, foci_whole)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# Validation
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@pytest.mark.parametrize('overrides, match', [
|
|
319
|
+
pytest.param({'cutoff': 0.0}, "'cutoff' must be a finite positive",
|
|
320
|
+
id='zero-cutoff'),
|
|
321
|
+
pytest.param({'cutoff': float('inf')}, "'cutoff' must be a finite",
|
|
322
|
+
id='infinite-cutoff'),
|
|
323
|
+
pytest.param({'genomic_positions': [2, 5]},
|
|
324
|
+
"one non-negative integer per particle, 3 in all",
|
|
325
|
+
id='too-few-positions'),
|
|
326
|
+
pytest.param({'genomic_positions': [2, -5, 8]},
|
|
327
|
+
"one non-negative integer per particle",
|
|
328
|
+
id='negative-position'),
|
|
329
|
+
pytest.param({'genomic_positions': [2.0, 5.0, 8.0]},
|
|
330
|
+
"one non-negative integer per particle",
|
|
331
|
+
id='float-positions'),
|
|
332
|
+
pytest.param({'genomic_positions': [2, 2, 8]}, "repeats an index",
|
|
333
|
+
id='repeated-position'),
|
|
334
|
+
pytest.param({'species': ''}, "'species' must be a non-empty",
|
|
335
|
+
id='empty-species'),
|
|
336
|
+
pytest.param({'periodic': (True, False)}, "'periodic' must be None",
|
|
337
|
+
id='short-periodic'),
|
|
338
|
+
pytest.param({'periodic': (1, 0, 1)}, "'periodic' must be None",
|
|
339
|
+
id='non-boolean-periodic'),
|
|
340
|
+
])
|
|
341
|
+
def test_invalid_arguments_raise(make_analysis, overrides, match):
|
|
342
|
+
"""A bad cutoff, backbone index list, species, or periodicity raises at
|
|
343
|
+
construction."""
|
|
344
|
+
with pytest.raises(ValueError, match=match):
|
|
345
|
+
make_analysis(**overrides)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def test_fewer_than_two_particles_raise(make_chain_universe, triangle):
|
|
349
|
+
"""One particle has no pair."""
|
|
350
|
+
universe = make_chain_universe(triangle)
|
|
351
|
+
|
|
352
|
+
with pytest.raises(ValueError, match="at least two particles"):
|
|
353
|
+
PairDistances(universe.atoms[:1], 3.5, [0])
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# Saving
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def test_save_writes_the_research_names(make_analysis, tmp_path):
|
|
360
|
+
"""save writes the four arrays as the research code named them, with
|
|
361
|
+
the species label, and each file holds its array."""
|
|
362
|
+
analysis = make_analysis().run()
|
|
363
|
+
folder = tmp_path / 'probes'
|
|
364
|
+
folder.mkdir()
|
|
365
|
+
|
|
366
|
+
paths = analysis.save(folder, 'ns7nl3ens1-bug')
|
|
367
|
+
|
|
368
|
+
assert [path.name for path in paths] == [
|
|
369
|
+
'ns7nl3ens1-bug-distMatTFoci.npy',
|
|
370
|
+
'ns7nl3ens1-bug-directContactsMatTFoci.npy',
|
|
371
|
+
'ns7nl3ens1-bug-bondsHistTFoci.npy',
|
|
372
|
+
'ns7nl3ens1-bug-clustersHistTFoci.npy']
|
|
373
|
+
assert_array_equal(np.load(paths[0]), analysis.results['distMatTFoci'])
|
|
374
|
+
assert_array_equal(np.load(paths[3]),
|
|
375
|
+
analysis.results['clustersHistTFoci'])
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def test_save_before_run_raises(make_analysis, tmp_path):
|
|
379
|
+
"""save needs the results of run."""
|
|
380
|
+
analysis = make_analysis()
|
|
381
|
+
|
|
382
|
+
with pytest.raises(RuntimeError, match="Call run"):
|
|
383
|
+
analysis.save(tmp_path, 'ns7nl3ens1-bug')
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# Real frames
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@pytest.fixture
|
|
390
|
+
def real_pairs(real_files, real_universe, study_spec):
|
|
391
|
+
"""Factory: the study's :meth:`PairDistances.from_spec` run on a real
|
|
392
|
+
TransFoci polymer dump, with the universe it read."""
|
|
393
|
+
|
|
394
|
+
def _run(whole):
|
|
395
|
+
module = study_spec(whole.study)
|
|
396
|
+
files = real_files(whole)
|
|
397
|
+
universe = real_universe(files.topology, files.polymer)
|
|
398
|
+
parser = module.SPEC.parser(f"{whole.whole}.ring", 'whole',
|
|
399
|
+
whole.group)
|
|
400
|
+
return (PairDistances.from_spec(universe, module.PROBE,
|
|
401
|
+
parser).run(), universe)
|
|
402
|
+
|
|
403
|
+
return _run
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def test_real_foci_diagonal_holds_their_places_along_the_ring(
|
|
407
|
+
real_foci_whole, real_files, real_pairs):
|
|
408
|
+
"""Every frame's diagonal holds the 0-based places of the large
|
|
409
|
+
monomers along the ring, as the data file orders its atoms."""
|
|
410
|
+
data = read_data(real_files(real_foci_whole).topology)
|
|
411
|
+
expected = np.flatnonzero(data.types[data.mol_ids == 1] == 2)
|
|
412
|
+
|
|
413
|
+
analysis, _ = real_pairs(real_foci_whole)
|
|
414
|
+
|
|
415
|
+
assert_array_equal(
|
|
416
|
+
np.diagonal(analysis.results['distMatTFoci'], axis1=1, axis2=2),
|
|
417
|
+
np.tile(expected, (50, 1)))
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def test_real_foci_distances_are_the_minimum_image_distances(
|
|
421
|
+
real_foci_whole, real_pairs, study_spec, trajectory_arrays,
|
|
422
|
+
reference_distances):
|
|
423
|
+
"""Above the diagonal, every frame holds the foci's minimum-image
|
|
424
|
+
distances along the geometry's periodic directions."""
|
|
425
|
+
probe = study_spec(real_foci_whole.study).PROBE
|
|
426
|
+
|
|
427
|
+
analysis, universe = real_pairs(real_foci_whole)
|
|
428
|
+
|
|
429
|
+
arrays = trajectory_arrays(universe)
|
|
430
|
+
foci = arrays['positions'][:, analysis.atomgroup.indices]
|
|
431
|
+
expected = reference_distances(foci, foci, arrays['boxes'],
|
|
432
|
+
probe.geometry)
|
|
433
|
+
upper = np.triu_indices(analysis.atomgroup.n_atoms, k=1)
|
|
434
|
+
# float32 positions of at most about 165 carry a round-off of about
|
|
435
|
+
# 1e-5 in a separation.
|
|
436
|
+
assert_allclose(analysis.results['distMatTFoci'][:, upper[0], upper[1]],
|
|
437
|
+
expected[:, upper[0], upper[1]], rtol=0, atol=5e-5)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def test_real_foci_bonds_and_clusters_conserve_the_foci(real_foci_whole,
|
|
441
|
+
real_pairs):
|
|
442
|
+
"""In every frame, the bonds histogram counts each large monomer once
|
|
443
|
+
and the clusters histogram, weighted by cluster size, does too."""
|
|
444
|
+
analysis, _ = real_pairs(real_foci_whole)
|
|
445
|
+
clusters = analysis.results['clustersHistTFoci']
|
|
446
|
+
sizes = np.arange(clusters.shape[1])
|
|
447
|
+
|
|
448
|
+
assert_array_equal(analysis.results['bondsHistTFoci'].sum(axis=1),
|
|
449
|
+
np.full(50, real_foci_whole.n_foci))
|
|
450
|
+
assert_array_equal(clusters @ sizes, np.full(50, real_foci_whole.n_foci))
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# Against the research code's probe (tier C)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@pytest.mark.private_data # pragma: no cover - needs data
|
|
457
|
+
@pytest.mark.parametrize('histogram, sizes', [
|
|
458
|
+
pytest.param('bondsHistTFoci', slice(None), id='bonds'),
|
|
459
|
+
pytest.param('clustersHistTFoci', slice(1, None), id='clusters'),
|
|
460
|
+
])
|
|
461
|
+
def test_real_foci_histograms_match_the_research_code(
|
|
462
|
+
real_transfocicub_whole, real_pairs, research_probe_file, histogram,
|
|
463
|
+
sizes):
|
|
464
|
+
"""On the 50 frames of a TransFociCub polymer dump, the foci bonds
|
|
465
|
+
histogram and the clusters histogram from size 1 up equal the research
|
|
466
|
+
code's probe of the same frames (SciOps ``trans_foci_cub_bug``), whose
|
|
467
|
+
contacts took no minimum image: the ring is whole in these dumps, so
|
|
468
|
+
the image changes no contact. The research clusters histogram counted
|
|
469
|
+
the zero eigenvalues of the contact matrix in its size-0 column, which
|
|
470
|
+
the analysis table drops; this probe writes no cluster of size 0."""
|
|
471
|
+
analysis, _ = real_pairs(real_transfocicub_whole)
|
|
472
|
+
expected = np.load(research_probe_file(real_transfocicub_whole, histogram))
|
|
473
|
+
|
|
474
|
+
assert_array_equal(analysis.results[histogram][:, sizes],
|
|
475
|
+
expected[:, sizes])
|