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,779 @@
|
|
|
1
|
+
"""Tests for :mod:`polyphys.probe.histograms`.
|
|
2
|
+
|
|
3
|
+
The inputs are tier A: two species of hand-placed beads in a box of side 20
|
|
4
|
+
about (10, 10, 10), written as a data file and a dump by the
|
|
5
|
+
``make_chain_universe`` fixture, binned with hand edges of unit or
|
|
6
|
+
half-unit width. Expected values are analytic: the bin of each bead by
|
|
7
|
+
hand from its recentered coordinates; the conservation of each species'
|
|
8
|
+
particle number per frame; the per-block means from the block sums
|
|
9
|
+
divided by the frame counts; and the edges of ``histogram_edges`` from the
|
|
10
|
+
research code's bin sizes over the ranges the maintainer set on
|
|
11
|
+
2026-11-04 (SciOps ``sciops/probe/prober.py``, ``sum_rule_cyl_all`` and
|
|
12
|
+
``trans_foci_cub_all``, extended to the wall surface and the cube's
|
|
13
|
+
corner).
|
|
14
|
+
|
|
15
|
+
The real-frame tests bin the tier B and tier C frames of the studies'
|
|
16
|
+
wholes (``polyphys/tests/probe/conftest.py``, "Real frames") with each
|
|
17
|
+
study's own probe spec and parser. Their expected values are conservation:
|
|
18
|
+
every position of a real frame falls inside the edges, so each frame's
|
|
19
|
+
counts sum to the species' particle number in the whole's name, and the run's
|
|
20
|
+
counts to the number of frames times it.
|
|
21
|
+
"""
|
|
22
|
+
import numpy as np
|
|
23
|
+
import pytest
|
|
24
|
+
from numpy.testing import (
|
|
25
|
+
assert_allclose,
|
|
26
|
+
assert_array_equal,
|
|
27
|
+
assert_equal,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from polyphys.tests.synthetic import ChainCyl, RingCub
|
|
31
|
+
from polyphys.spec import ProbeSpec, ProjectSpec
|
|
32
|
+
|
|
33
|
+
pytest.importorskip("MDAnalysis")
|
|
34
|
+
|
|
35
|
+
from polyphys.probe import SpatialHistograms, histogram_edges # noqa: E402
|
|
36
|
+
|
|
37
|
+
# The dump reader warns that the frames carry no time step; the tests
|
|
38
|
+
# measure per frame and give no dt.
|
|
39
|
+
pytestmark = pytest.mark.filterwarnings(
|
|
40
|
+
"ignore:Reader has no dt information:UserWarning")
|
|
41
|
+
|
|
42
|
+
# Tolerance for the per-block means and the edges, both a few float64
|
|
43
|
+
# operations on exact values.
|
|
44
|
+
RTOL = 1e-12
|
|
45
|
+
ATOL = 1e-12
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.fixture
|
|
49
|
+
def center() -> float:
|
|
50
|
+
"""The box center along each axis, for a box from 0 to 20."""
|
|
51
|
+
return 10.0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.fixture
|
|
55
|
+
def two_species(center) -> np.ndarray:
|
|
56
|
+
"""Five frames of six beads: beads 1-3 (type 1, ``Mon``) and 4-6 (type 2,
|
|
57
|
+
``Crd``), the first of each species moved 0.5 along z per frame.
|
|
58
|
+
|
|
59
|
+
About the center ``c``: bead 1 at ``(c + 2.5, c, c)``, bead 2 at
|
|
60
|
+
``(c, c + 1, c + 3.2)``, bead 3 at ``(c - 1.5, c - 1.5, c - 2)``, bead 4
|
|
61
|
+
at ``(c + 0.5, c + 0.5, c + 0.5)``, bead 5 at ``(c - 3, c, c - 7)``, and
|
|
62
|
+
bead 6 at ``(c, c - 2.5, c + 6)``.
|
|
63
|
+
"""
|
|
64
|
+
first = np.array([
|
|
65
|
+
[2.5, 0.0, 0.0], [0.0, 1.0, 3.2], [-1.5, -1.5, -2.0],
|
|
66
|
+
[0.5, 0.5, 0.5], [-3.0, 0.0, -7.0], [0.0, -2.5, 6.0]]) + center
|
|
67
|
+
frames = np.stack([first.copy() for _ in range(5)])
|
|
68
|
+
frames[:, 0, 2] += 0.5 * np.arange(5)
|
|
69
|
+
frames[:, 3, 2] += 0.5 * np.arange(5)
|
|
70
|
+
return frames
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@pytest.fixture
|
|
74
|
+
def types() -> list[int]:
|
|
75
|
+
"""The atom type of each of the six beads."""
|
|
76
|
+
return [1, 1, 1, 2, 2, 2]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.fixture
|
|
80
|
+
def cylinder_edges() -> dict[str, np.ndarray]:
|
|
81
|
+
"""Hand edges of a cylinder of radius 4 and length 20: unit bins along
|
|
82
|
+
r, x, and y, half-unit bins along z, 72 bins along theta."""
|
|
83
|
+
return {
|
|
84
|
+
'r': np.arange(0.0, 4.5, 1.0),
|
|
85
|
+
'z': np.linspace(-10.0, 10.0, 41),
|
|
86
|
+
'theta': np.linspace(-np.pi, np.pi, 73),
|
|
87
|
+
'x': np.arange(-4.0, 4.5, 1.0),
|
|
88
|
+
'y': np.arange(-4.0, 4.5, 1.0),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest.fixture
|
|
93
|
+
def cube_edges() -> dict[str, np.ndarray]:
|
|
94
|
+
"""Hand edges of a cube of side 20: unit bins along r to the corner and
|
|
95
|
+
along x, y, and z."""
|
|
96
|
+
return {
|
|
97
|
+
'r': np.arange(0.0, 18.5, 1.0),
|
|
98
|
+
'x': np.arange(-10.0, 10.5, 1.0),
|
|
99
|
+
'y': np.arange(-10.0, 10.5, 1.0),
|
|
100
|
+
'z': np.arange(-10.0, 10.5, 1.0),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@pytest.fixture
|
|
105
|
+
def make_analysis(make_chain_universe, two_species, types, cylinder_edges):
|
|
106
|
+
"""Factory: the cylinder analysis of the two species, with any argument
|
|
107
|
+
overridden; `positions` replaces the frames."""
|
|
108
|
+
|
|
109
|
+
def _make(positions=None, **overrides):
|
|
110
|
+
universe = make_chain_universe(
|
|
111
|
+
two_species if positions is None else positions,
|
|
112
|
+
atom_types=types, masses=(1.0, 1.0))
|
|
113
|
+
groups = {'Mon': universe.select_atoms('type 1'),
|
|
114
|
+
'Crd': universe.select_atoms('type 2')}
|
|
115
|
+
kwargs = {'geometry': 'cylindrical', 'block_frames': 2,
|
|
116
|
+
'recenter': 'half'}
|
|
117
|
+
kwargs.update(overrides)
|
|
118
|
+
return SpatialHistograms(groups, cylinder_edges, **kwargs)
|
|
119
|
+
|
|
120
|
+
return _make
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# Hand bins
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@pytest.mark.parametrize('key, filled', [
|
|
127
|
+
pytest.param('r', {1: 1, 2: 2}, id='r-1-2.5-and-2.12'),
|
|
128
|
+
pytest.param('theta', {36: 1, 54: 1, 9: 1},
|
|
129
|
+
id='theta-0-pi-over-2-and-minus-3pi-over-4'),
|
|
130
|
+
pytest.param('z', {20: 1, 26: 1, 16: 1}, id='z-0-3.2-and-minus-2'),
|
|
131
|
+
])
|
|
132
|
+
def test_histograms_bin_each_bead_by_its_recentered_coordinate(
|
|
133
|
+
make_analysis, key, filled):
|
|
134
|
+
"""In frame 0 the three Mon beads, at (2.5, 0, 0), (0, 1, 3.2), and
|
|
135
|
+
(-1.5, -1.5, -2) from the center, fill the r, theta, and z bins
|
|
136
|
+
computed by hand from the edges and nothing else."""
|
|
137
|
+
analysis = make_analysis().run()
|
|
138
|
+
|
|
139
|
+
row = analysis.results[f'{key}HistTMon'][0]
|
|
140
|
+
|
|
141
|
+
expected = np.zeros(row.size, dtype=np.int64)
|
|
142
|
+
for index, count in filled.items():
|
|
143
|
+
expected[index] = count
|
|
144
|
+
assert_array_equal(row, expected)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_histogram_t_tracks_a_moving_bead(make_analysis):
|
|
148
|
+
"""A bead moved 0.5 along z per frame moves one z bin per frame, so the
|
|
149
|
+
per-frame rows differ as the bead does."""
|
|
150
|
+
analysis = make_analysis().run()
|
|
151
|
+
|
|
152
|
+
rows = analysis.results['zHistTMon']
|
|
153
|
+
|
|
154
|
+
assert_array_equal(rows[:, 20:25].diagonal(), np.ones(5, dtype=int))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@pytest.mark.parametrize('plane, cell, mean', [
|
|
158
|
+
pytest.param('xy', (6, 4), 1.0, id='xy-of-(2.5,0)'),
|
|
159
|
+
pytest.param('xz', (6, 20), 0.5, id='xz-of-(2.5,0)'),
|
|
160
|
+
pytest.param('yz', (4, 20), 0.5, id='yz-of-(2.5,0)'),
|
|
161
|
+
])
|
|
162
|
+
def test_maps_put_a_bead_in_its_cell_with_rows_along_the_first_axis(
|
|
163
|
+
make_analysis, plane, cell, mean):
|
|
164
|
+
"""Bead 1 at (2.5, 0, 0) falls in the cell indexed by its first-axis
|
|
165
|
+
coordinate then its second, in the first block's mean map: in both
|
|
166
|
+
frames of the block in the xy plane, in frame 0 only in the planes with
|
|
167
|
+
z, where it moves from the half-unit bin [0, 0.5) to the next."""
|
|
168
|
+
analysis = make_analysis().run()
|
|
169
|
+
|
|
170
|
+
block = analysis.results[f'{plane}HistBlockMon'][0]
|
|
171
|
+
|
|
172
|
+
assert block[cell] == pytest.approx(mean, abs=ATOL)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# Conservation and consistency
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@pytest.mark.parametrize('key', ['r', 'z', 'theta'])
|
|
179
|
+
def test_each_frame_conserves_the_particle_number(make_analysis, key):
|
|
180
|
+
"""Every frame's counts along a direction sum to the species' three
|
|
181
|
+
beads, for both species."""
|
|
182
|
+
analysis = make_analysis().run()
|
|
183
|
+
|
|
184
|
+
assert_array_equal(analysis.results[f'{key}HistTMon'].sum(axis=1),
|
|
185
|
+
np.full(5, 3))
|
|
186
|
+
assert_array_equal(analysis.results[f'{key}HistTCrd'].sum(axis=1),
|
|
187
|
+
np.full(5, 3))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@pytest.mark.parametrize('key', ['r', 'z', 'theta'])
|
|
191
|
+
def test_summed_histogram_is_the_column_sum_of_the_frames(make_analysis, key):
|
|
192
|
+
"""The frame-summed histogram equals the per-frame rows summed down the
|
|
193
|
+
columns, int64."""
|
|
194
|
+
analysis = make_analysis().run()
|
|
195
|
+
|
|
196
|
+
summed = analysis.results[f'{key}HistCrd']
|
|
197
|
+
|
|
198
|
+
assert summed.dtype == np.int64
|
|
199
|
+
assert_array_equal(summed, analysis.results[f'{key}HistTCrd'].sum(axis=0))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@pytest.mark.parametrize('plane', ['xy', 'xz', 'yz'])
|
|
203
|
+
def test_block_means_times_block_frames_give_the_summed_map(make_analysis,
|
|
204
|
+
plane):
|
|
205
|
+
"""Each block's mean map times its frame count, summed over the blocks,
|
|
206
|
+
is the frame-summed map, whose total is the beads times the frames."""
|
|
207
|
+
analysis = make_analysis().run()
|
|
208
|
+
|
|
209
|
+
means = analysis.results[f'{plane}HistBlockMon']
|
|
210
|
+
counts = analysis.results['blockFrames']
|
|
211
|
+
|
|
212
|
+
assert_array_equal(counts, [2, 2, 1])
|
|
213
|
+
assert_allclose((means * counts[:, None, None]).sum(axis=0),
|
|
214
|
+
analysis.results[f'{plane}HistMon'], rtol=RTOL, atol=ATOL)
|
|
215
|
+
assert analysis.results[f'{plane}HistMon'].sum() == pytest.approx(
|
|
216
|
+
15.0, abs=ATOL)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_blocks_follow_the_frames_of_the_run(make_analysis):
|
|
220
|
+
"""A run over four of the five frames with blocks of two gives two full
|
|
221
|
+
blocks, numbered over the frames visited."""
|
|
222
|
+
analysis = make_analysis().run(start=1, stop=5)
|
|
223
|
+
|
|
224
|
+
assert_array_equal(analysis.results['blockFrames'], [2, 2])
|
|
225
|
+
assert analysis.results['xyHistBlockMon'].shape == (2, 8, 8)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_parallel_run_matches_serial(make_analysis):
|
|
229
|
+
"""Two workers over two parts, the split falling inside a block, give
|
|
230
|
+
the same per-frame rows, block means, and frame counts as one worker."""
|
|
231
|
+
serial = make_analysis().run()
|
|
232
|
+
parallel = make_analysis().run(backend='multiprocessing', n_workers=2,
|
|
233
|
+
n_parts=2)
|
|
234
|
+
|
|
235
|
+
for key in serial.results:
|
|
236
|
+
assert_allclose(parallel.results[key], serial.results[key],
|
|
237
|
+
rtol=RTOL, atol=ATOL)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@pytest.mark.parametrize('n_parts', [
|
|
241
|
+
pytest.param(2, id='two-parts'),
|
|
242
|
+
pytest.param(3, id='three-parts'),
|
|
243
|
+
])
|
|
244
|
+
def test_serial_parts_match_the_multiprocessing_run(make_analysis, n_parts):
|
|
245
|
+
"""The serial backend split into parts gives the frames, times,
|
|
246
|
+
per-frame rows, block means, and frame counts of the multiprocessing
|
|
247
|
+
backend over the same parts: the result does not depend on the
|
|
248
|
+
backend."""
|
|
249
|
+
parallel = make_analysis().run(backend='multiprocessing', n_workers=2,
|
|
250
|
+
n_parts=n_parts)
|
|
251
|
+
|
|
252
|
+
serial = make_analysis().run(backend='serial', n_parts=n_parts)
|
|
253
|
+
|
|
254
|
+
assert_array_equal(serial.frames, parallel.frames)
|
|
255
|
+
assert_array_equal(serial.times, parallel.times)
|
|
256
|
+
assert_equal(dict(serial.results), dict(parallel.results))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_empty_group_gives_zero_histograms(make_chain_universe, two_species,
|
|
260
|
+
types, cylinder_edges):
|
|
261
|
+
"""A species a whole lacks yields all-zero rows of the right shape."""
|
|
262
|
+
universe = make_chain_universe(two_species, atom_types=types,
|
|
263
|
+
masses=(1.0, 1.0))
|
|
264
|
+
groups = {'Mon': universe.select_atoms('type 1'),
|
|
265
|
+
'Hns': universe.select_atoms('type 3')}
|
|
266
|
+
|
|
267
|
+
analysis = SpatialHistograms(groups, cylinder_edges,
|
|
268
|
+
geometry='cylindrical', block_frames=2).run()
|
|
269
|
+
|
|
270
|
+
assert_array_equal(analysis.results['rHistTHns'], np.zeros((5, 4), int))
|
|
271
|
+
assert_array_equal(analysis.results['rHistTMon'].sum(axis=1),
|
|
272
|
+
np.full(5, 3))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# Recentering
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@pytest.mark.parametrize('recenter', [
|
|
279
|
+
pytest.param('half', id='half-the-box'),
|
|
280
|
+
pytest.param((10.0, 10.0, 10.0), id='the-same-triple'),
|
|
281
|
+
])
|
|
282
|
+
def test_recenter_half_and_its_triple_agree(make_analysis, recenter):
|
|
283
|
+
"""Half of a box of side 20 and the triple (10, 10, 10) give the same
|
|
284
|
+
bins: the Mon r row [0, 1, 2, 0]."""
|
|
285
|
+
analysis = make_analysis(recenter=recenter).run()
|
|
286
|
+
|
|
287
|
+
assert_array_equal(analysis.results['rHistTMon'][0], [0, 1, 2, 0])
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_recenter_half_undoes_the_readers_translation(
|
|
291
|
+
make_chain_universe, cylinder_edges):
|
|
292
|
+
"""For a box from -10 to 10, which the dump reader moves to the origin,
|
|
293
|
+
'half' restores the written coordinates: a bead written at x = 2.5 is
|
|
294
|
+
in r bin 2 and x bin 6."""
|
|
295
|
+
positions = np.array([[[2.5, 0.0, 0.0], [0.0, 1.0, 0.0]]])
|
|
296
|
+
universe = make_chain_universe(positions, box=((-10.0, 10.0),) * 3)
|
|
297
|
+
|
|
298
|
+
analysis = SpatialHistograms(
|
|
299
|
+
{'Mon': universe.atoms}, cylinder_edges, geometry='cylindrical',
|
|
300
|
+
block_frames=1, recenter='half').run()
|
|
301
|
+
|
|
302
|
+
assert analysis.results['rHistTMon'][0, 2] == 1
|
|
303
|
+
assert analysis.results['xyHistBlockMon'][0, 6, 4] == pytest.approx(
|
|
304
|
+
1.0, abs=ATOL)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_recenter_none_keeps_the_readers_frame(make_chain_universe,
|
|
308
|
+
cube_edges):
|
|
309
|
+
"""With 'none', positions stay in the reader's frame, from 0 to 20, so
|
|
310
|
+
a bead written at (12.5, 10, 10) lies beyond cube edges centered on the
|
|
311
|
+
origin and the run raises instead of binning it."""
|
|
312
|
+
positions = np.array([[[12.5, 10.0, 10.0], [10.0, 10.0, 10.0]]])
|
|
313
|
+
universe = make_chain_universe(positions)
|
|
314
|
+
|
|
315
|
+
analysis = SpatialHistograms(
|
|
316
|
+
{'Mon': universe.atoms}, cube_edges, geometry='cubic',
|
|
317
|
+
block_frames=1, recenter='none')
|
|
318
|
+
|
|
319
|
+
with pytest.raises(ValueError, match="positions fall outside"):
|
|
320
|
+
analysis.run()
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# Out-of-range positions
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def test_position_outside_a_direction_raises(make_analysis, two_species):
|
|
327
|
+
"""A bead beyond the last r edge aborts the run, naming the count."""
|
|
328
|
+
positions = two_species.copy()
|
|
329
|
+
positions[0, 0, 0] = 10.0 + 4.5 # r = 4.5 > 4
|
|
330
|
+
|
|
331
|
+
with pytest.raises(ValueError, match="1 of 3 positions fall outside"):
|
|
332
|
+
make_analysis(positions=positions).run()
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def test_position_outside_the_cartesian_edges_raises(
|
|
336
|
+
make_chain_universe, two_species, types, cylinder_edges):
|
|
337
|
+
"""A bead inside every one-dimensional range but beyond a Cartesian
|
|
338
|
+
edge of the maps aborts the run, naming the axis and the frame."""
|
|
339
|
+
edges = dict(cylinder_edges)
|
|
340
|
+
edges['x'] = np.arange(-2.0, 2.5, 1.0) # bead 1 at x = 2.5 is beyond
|
|
341
|
+
universe = make_chain_universe(two_species, atom_types=types,
|
|
342
|
+
masses=(1.0, 1.0))
|
|
343
|
+
|
|
344
|
+
analysis = SpatialHistograms(
|
|
345
|
+
{'Mon': universe.select_atoms('type 1')}, edges,
|
|
346
|
+
geometry='cylindrical', block_frames=2)
|
|
347
|
+
|
|
348
|
+
with pytest.raises(ValueError,
|
|
349
|
+
match="1 of 3 Mon positions fall outside the x edges"):
|
|
350
|
+
analysis.run()
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@pytest.mark.parametrize('position, axis, expected_bin', [
|
|
354
|
+
pytest.param([20.03, 10.0, 10.0], 'x', 0, id='beyond-hi-x'),
|
|
355
|
+
pytest.param([10.0, -0.02, 10.0], 'y', 19, id='below-lo-y'),
|
|
356
|
+
pytest.param([10.0, 10.0, 20.04], 'z', 0, id='beyond-hi-z'),
|
|
357
|
+
])
|
|
358
|
+
def test_cube_bins_a_position_past_the_box_as_its_periodic_image(
|
|
359
|
+
make_chain_universe, cube_edges, position, axis, expected_bin):
|
|
360
|
+
"""A position a little past a periodic face, as a LAMMPS dump writes
|
|
361
|
+
between reneighborings, is binned as its periodic image."""
|
|
362
|
+
positions = np.array([[position, [10.0, 10.0, 10.0]]])
|
|
363
|
+
universe = make_chain_universe(positions, boundary='pp pp pp')
|
|
364
|
+
|
|
365
|
+
analysis = SpatialHistograms(
|
|
366
|
+
{'Mon': universe.atoms}, cube_edges, geometry='cubic',
|
|
367
|
+
block_frames=1).run()
|
|
368
|
+
|
|
369
|
+
# One bead at the center and one at the periodic image: x = 0.03 and
|
|
370
|
+
# z = 0.04 fall in the first unit bin, y = 19.98 in the last.
|
|
371
|
+
plane = {'x': 'xyHistMon', 'y': 'xyHistMon', 'z': 'xzHistMon'}[axis]
|
|
372
|
+
marginal = analysis.results[plane].sum(axis={'x': 1, 'y': 0,
|
|
373
|
+
'z': 0}[axis])
|
|
374
|
+
assert marginal[expected_bin] == 1
|
|
375
|
+
assert int(analysis.results['rHistTMon'].sum()) == 2
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def test_frame_without_a_box_raises(cube_edges):
|
|
379
|
+
"""A frame that carries no box cannot be wrapped, and the run says
|
|
380
|
+
so."""
|
|
381
|
+
import MDAnalysis as mda
|
|
382
|
+
|
|
383
|
+
universe = mda.Universe.empty(1, trajectory=True)
|
|
384
|
+
universe.atoms.positions = np.array([[1.0, 2.0, 3.0]])
|
|
385
|
+
analysis = SpatialHistograms({'Mon': universe.atoms}, cube_edges,
|
|
386
|
+
geometry='cubic', block_frames=1,
|
|
387
|
+
recenter='none')
|
|
388
|
+
|
|
389
|
+
with pytest.raises(ValueError, match="carries no box"):
|
|
390
|
+
analysis.run()
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def test_cylinder_bins_a_position_past_the_axis_end_as_its_periodic_image(
|
|
394
|
+
make_analysis, two_species):
|
|
395
|
+
"""A position a little past the end of the periodic cylinder axis is
|
|
396
|
+
binned as its periodic image, while the walls still bound x and y."""
|
|
397
|
+
positions = two_species.copy()
|
|
398
|
+
positions[0, 1, 2] = 20.03 # z past the box [0, 20]: image at 0.03
|
|
399
|
+
|
|
400
|
+
analysis = make_analysis(positions=positions).run()
|
|
401
|
+
|
|
402
|
+
# Recentered by half the box, the image is at z = -9.97, in the first
|
|
403
|
+
# half-unit bin of [-10, 10].
|
|
404
|
+
assert analysis.results['zHistTMon'][0, 0] == 1
|
|
405
|
+
assert int(analysis.results['zHistTMon'][0].sum()) == 3
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# Cube geometry
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def test_cube_bins_r_from_the_box_center(make_chain_universe, cube_edges):
|
|
412
|
+
"""In a cube, r is the distance from the box center: a bead at
|
|
413
|
+
(c + 3, c + 4, c) is in r bin 5, and only r is binned."""
|
|
414
|
+
positions = np.array([[[13.0, 14.0, 10.0], [10.0, 10.0, 10.0]]])
|
|
415
|
+
universe = make_chain_universe(positions)
|
|
416
|
+
|
|
417
|
+
analysis = SpatialHistograms(
|
|
418
|
+
{'Mon': universe.atoms}, cube_edges, geometry='cubic',
|
|
419
|
+
block_frames=1).run()
|
|
420
|
+
|
|
421
|
+
assert_array_equal(analysis.results['rHistTMon'][0, [0, 5]], [1, 1])
|
|
422
|
+
assert 'zHistTMon' not in analysis.results
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
# Validation
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
@pytest.mark.parametrize('overrides, match', [
|
|
429
|
+
pytest.param({'geometry': 'spherical'}, "'geometry' must be one of",
|
|
430
|
+
id='unknown-geometry'),
|
|
431
|
+
pytest.param({'block_frames': 0}, "'block_frames' must be a positive",
|
|
432
|
+
id='zero-block-frames'),
|
|
433
|
+
pytest.param({'block_frames': 2.0}, "'block_frames' must be a positive",
|
|
434
|
+
id='float-block-frames'),
|
|
435
|
+
pytest.param({'recenter': 'center'}, "'recenter' must be",
|
|
436
|
+
id='unknown-recenter'),
|
|
437
|
+
pytest.param({'recenter': (1.0, 2.0)}, "'recenter' must be",
|
|
438
|
+
id='short-recenter'),
|
|
439
|
+
])
|
|
440
|
+
def test_invalid_arguments_raise(make_analysis, overrides, match):
|
|
441
|
+
"""A bad geometry, block length, or recentering raises at construction."""
|
|
442
|
+
with pytest.raises(ValueError, match=match):
|
|
443
|
+
make_analysis(**overrides)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
@pytest.mark.parametrize('edit, match', [
|
|
447
|
+
pytest.param(lambda edges: edges.pop('theta'), r"lacks \['theta'\]",
|
|
448
|
+
id='missing-direction'),
|
|
449
|
+
pytest.param(lambda edges: edges.pop('x'), r"lacks \['x'\]",
|
|
450
|
+
id='missing-cartesian-axis'),
|
|
451
|
+
pytest.param(lambda edges: edges.update(r=np.array([0.0, 2.0, 1.0])),
|
|
452
|
+
"edges along 'r' must be", id='decreasing-edges'),
|
|
453
|
+
pytest.param(lambda edges: edges.update(r=np.array([0.0])),
|
|
454
|
+
"edges along 'r' must be", id='single-edge'),
|
|
455
|
+
])
|
|
456
|
+
def test_invalid_edges_raise(make_chain_universe, two_species, cylinder_edges,
|
|
457
|
+
edit, match):
|
|
458
|
+
"""Edges that lack a key the geometry needs, or are not increasing with
|
|
459
|
+
at least two values, raise at construction."""
|
|
460
|
+
edges = dict(cylinder_edges)
|
|
461
|
+
edit(edges)
|
|
462
|
+
universe = make_chain_universe(two_species)
|
|
463
|
+
|
|
464
|
+
with pytest.raises(ValueError, match=match):
|
|
465
|
+
SpatialHistograms({'Mon': universe.atoms}, edges,
|
|
466
|
+
geometry='cylindrical', block_frames=2)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def test_empty_groups_raise(make_chain_universe, two_species, cylinder_edges):
|
|
470
|
+
"""No species at all raises."""
|
|
471
|
+
make_chain_universe(two_species)
|
|
472
|
+
|
|
473
|
+
with pytest.raises(ValueError, match="'groups' must map"):
|
|
474
|
+
SpatialHistograms({}, cylinder_edges, geometry='cylindrical',
|
|
475
|
+
block_frames=2)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def test_groups_from_two_universes_raise(make_chain_universe, two_species,
|
|
479
|
+
cylinder_edges):
|
|
480
|
+
"""Species from different universes cannot share one trajectory."""
|
|
481
|
+
first = make_chain_universe(two_species, name='first')
|
|
482
|
+
second = make_chain_universe(two_species, name='second')
|
|
483
|
+
|
|
484
|
+
with pytest.raises(ValueError, match="one universe"):
|
|
485
|
+
SpatialHistograms({'Mon': first.atoms, 'Crd': second.atoms},
|
|
486
|
+
cylinder_edges, geometry='cylindrical',
|
|
487
|
+
block_frames=2)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
# histogram_edges and from_spec
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@pytest.fixture
|
|
494
|
+
def cylinder_spec():
|
|
495
|
+
"""A ChainCyl probe spec with the research code's bin sizes: 0.1 of
|
|
496
|
+
the smaller diameter along r, x, and y, half of it along z, pi/36 along
|
|
497
|
+
theta."""
|
|
498
|
+
project = ProjectSpec(
|
|
499
|
+
parser=ChainCyl, group='bug', divisor=0.025,
|
|
500
|
+
crowder_attr='phi_bulk_c',
|
|
501
|
+
physical_attrs=('space', 'nmon', 'dcyl', 'dcrowd'),
|
|
502
|
+
species_sizes={'Mon': 'dmon', 'Crd': 'dcrowd'},
|
|
503
|
+
tseries_properties=('gyrTMon',))
|
|
504
|
+
|
|
505
|
+
def tenth(info):
|
|
506
|
+
return 0.1 * min(info.dmon, info.dcrowd)
|
|
507
|
+
|
|
508
|
+
def half(info):
|
|
509
|
+
return 0.5 * min(info.dmon, info.dcrowd)
|
|
510
|
+
|
|
511
|
+
return ProbeSpec(
|
|
512
|
+
project, {'Mon': 'type 1', 'Crd': 'type 2'}, 'Mon', ('gyrT',),
|
|
513
|
+
unwrap=False, histogram_species=('Mon', 'Crd'), block_frames=2,
|
|
514
|
+
bin_sizes={'r': tenth, 'z': half, 'theta': np.pi / 36, 'x': tenth,
|
|
515
|
+
'y': tenth})
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def test_histogram_edges_of_a_cylinder(cylinder_spec):
|
|
519
|
+
"""For dcyl = 4 and lcyl = 20 with unit beads: r has 25 bins of 0.1 to
|
|
520
|
+
the wall surface 2.5, z 40 bins of 0.5 over [-10, 10], theta 72 bins
|
|
521
|
+
over [-pi, pi], and x and y 50 bins of 0.1 over [-2.5, 2.5]."""
|
|
522
|
+
whole = ChainCyl(
|
|
523
|
+
'N10r2.5lz10ac1nc0dt0.005bdump1adump1ens1', 'whole', 'all')
|
|
524
|
+
|
|
525
|
+
edges = histogram_edges(cylinder_spec, whole)
|
|
526
|
+
|
|
527
|
+
assert list(edges) == ['r', 'z', 'theta', 'x', 'y']
|
|
528
|
+
assert_allclose(edges['r'], np.linspace(0.0, 2.5, 26), rtol=RTOL,
|
|
529
|
+
atol=ATOL)
|
|
530
|
+
assert_allclose(edges['z'], np.linspace(-10.0, 10.0, 41), rtol=RTOL,
|
|
531
|
+
atol=ATOL)
|
|
532
|
+
assert_allclose(edges['theta'], np.linspace(-np.pi, np.pi, 73),
|
|
533
|
+
rtol=RTOL, atol=ATOL)
|
|
534
|
+
assert_allclose(edges['x'], np.linspace(-2.5, 2.5, 51), rtol=RTOL,
|
|
535
|
+
atol=ATOL)
|
|
536
|
+
assert_array_equal(edges['y'], edges['x'])
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def test_histogram_edges_scale_with_the_whole(cylinder_spec):
|
|
540
|
+
"""A crowder of diameter 0.5 halves the r bin size, so the same cylinder
|
|
541
|
+
gets 50 r bins."""
|
|
542
|
+
whole = ChainCyl(
|
|
543
|
+
'N10r2.5lz10ac0.5nc4dt0.005bdump1adump1ens1', 'whole',
|
|
544
|
+
'all')
|
|
545
|
+
|
|
546
|
+
edges = histogram_edges(cylinder_spec, whole)
|
|
547
|
+
|
|
548
|
+
assert edges['r'].size == 51
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def test_histogram_edges_of_a_cube():
|
|
552
|
+
"""For lcube = 90: r runs to the corner, sqrt(3) * 45, in bins of 0.3,
|
|
553
|
+
extended upward to a whole bin; x, y, and z have 60 bins of 1.5 over
|
|
554
|
+
[-45, 45]."""
|
|
555
|
+
project = ProjectSpec(
|
|
556
|
+
parser=RingCub, group='bug', divisor=0.025,
|
|
557
|
+
crowder_attr='phi_bulk_c',
|
|
558
|
+
physical_attrs=('space', 'nmon_small', 'lcube'),
|
|
559
|
+
species_sizes={'Mon': 'dmon_small', 'Foci': 'dmon_large',
|
|
560
|
+
'Crd': 'dcrowd'},
|
|
561
|
+
tseries_properties=('gyrTMon',))
|
|
562
|
+
spec = ProbeSpec(
|
|
563
|
+
project, {'Mon': 'resid 1', 'Crd': 'resid 0'}, 'Mon', ('gyrT',),
|
|
564
|
+
unwrap=True, histogram_species=('Mon',), block_frames=2,
|
|
565
|
+
bin_sizes={'r': 0.3, 'x': 1.5, 'y': 1.5, 'z': 1.5})
|
|
566
|
+
whole = RingCub(
|
|
567
|
+
'al5nl5ml125ns400ac3nc7735l45dt0.005bdump2000adump5000ens4', 'whole',
|
|
568
|
+
'all')
|
|
569
|
+
|
|
570
|
+
edges = histogram_edges(spec, whole)
|
|
571
|
+
|
|
572
|
+
corner = np.sqrt(3.0) * 45.0
|
|
573
|
+
assert list(edges) == ['r', 'x', 'y', 'z']
|
|
574
|
+
assert edges['r'][0] == 0.0
|
|
575
|
+
assert edges['r'].size == int(np.ceil(corner / 0.3)) + 1
|
|
576
|
+
assert edges['r'][-1] == pytest.approx(0.3 * np.ceil(corner / 0.3),
|
|
577
|
+
abs=1e-9)
|
|
578
|
+
assert_allclose(edges['z'], np.linspace(-45.0, 45.0, 61), rtol=RTOL,
|
|
579
|
+
atol=ATOL)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def test_histogram_edges_need_every_key(cylinder_spec):
|
|
583
|
+
"""A spec lacking a bin size the geometry needs raises, naming it."""
|
|
584
|
+
spec = ProbeSpec(
|
|
585
|
+
cylinder_spec.project, dict(cylinder_spec.selections), 'Mon',
|
|
586
|
+
('gyrT',), unwrap=False, histogram_species=('Mon',), block_frames=2,
|
|
587
|
+
bin_sizes={'r': 0.1, 'z': 0.5, 'theta': np.pi / 36})
|
|
588
|
+
whole = ChainCyl(
|
|
589
|
+
'N10r2.5lz10ac1nc0dt0.005bdump1adump1ens1', 'whole', 'all')
|
|
590
|
+
|
|
591
|
+
with pytest.raises(ValueError, match=r"lacks \['x', 'y'\]"):
|
|
592
|
+
histogram_edges(spec, whole)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def test_from_spec_selects_the_species_and_carries_the_spec(
|
|
596
|
+
make_chain_universe, two_species, types, cylinder_spec):
|
|
597
|
+
"""from_spec selects each histogram species, builds the whole's edges,
|
|
598
|
+
and takes the geometry, block length, and recentering from the spec."""
|
|
599
|
+
universe = make_chain_universe(two_species, atom_types=types,
|
|
600
|
+
masses=(1.0, 1.0))
|
|
601
|
+
whole = ChainCyl(
|
|
602
|
+
'N3r2.5lz10ac1nc3dt0.005bdump1adump1ens1', 'whole', 'all')
|
|
603
|
+
|
|
604
|
+
analysis = SpatialHistograms.from_spec(universe, cylinder_spec, whole)
|
|
605
|
+
|
|
606
|
+
assert [group.n_atoms for group in analysis.groups.values()] == [3, 3]
|
|
607
|
+
assert list(analysis.groups) == ['Mon', 'Crd']
|
|
608
|
+
assert (analysis.geometry, analysis.block_frames, analysis.recenter) == (
|
|
609
|
+
'cylindrical', 2, 'half')
|
|
610
|
+
assert analysis.edges['r'].size == 26
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def test_from_spec_needs_histogram_species(make_chain_universe, two_species,
|
|
614
|
+
cylinder_spec):
|
|
615
|
+
"""A spec without histogram species cannot build the analysis."""
|
|
616
|
+
spec = ProbeSpec(cylinder_spec.project, dict(cylinder_spec.selections),
|
|
617
|
+
'Mon', ('gyrT',), unwrap=False)
|
|
618
|
+
universe = make_chain_universe(two_species)
|
|
619
|
+
whole = ChainCyl(
|
|
620
|
+
'N3r2.5lz10ac1nc3dt0.005bdump1adump1ens1', 'whole', 'all')
|
|
621
|
+
|
|
622
|
+
with pytest.raises(ValueError, match="names no histogram species"):
|
|
623
|
+
SpatialHistograms.from_spec(universe, spec, whole)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
# Saving
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def test_save_writes_the_research_names(make_analysis, tmp_path):
|
|
630
|
+
"""save writes the edges, summed and per-frame histograms per species
|
|
631
|
+
and direction, the Cartesian edges without a species, the summed and
|
|
632
|
+
per-block maps, and the block frame counts, under the research names."""
|
|
633
|
+
analysis = make_analysis().run()
|
|
634
|
+
folder = tmp_path / 'probes'
|
|
635
|
+
folder.mkdir()
|
|
636
|
+
|
|
637
|
+
paths = analysis.save(folder, 'N6ens1-all')
|
|
638
|
+
|
|
639
|
+
names = [path.name for path in paths]
|
|
640
|
+
assert names[:9] == [
|
|
641
|
+
'N6ens1-all-rEdgeMon.npy', 'N6ens1-all-rHistMon.npy',
|
|
642
|
+
'N6ens1-all-rHistTMon.npy', 'N6ens1-all-zEdgeMon.npy',
|
|
643
|
+
'N6ens1-all-zHistMon.npy', 'N6ens1-all-zHistTMon.npy',
|
|
644
|
+
'N6ens1-all-thetaEdgeMon.npy', 'N6ens1-all-thetaHistMon.npy',
|
|
645
|
+
'N6ens1-all-thetaHistTMon.npy']
|
|
646
|
+
assert names[18:21] == ['N6ens1-all-xEdge.npy', 'N6ens1-all-yEdge.npy',
|
|
647
|
+
'N6ens1-all-zEdge.npy']
|
|
648
|
+
assert names[21:23] == ['N6ens1-all-xyHistMon.npy',
|
|
649
|
+
'N6ens1-all-xyHistBlockMon.npy']
|
|
650
|
+
assert names[-1] == 'N6ens1-all-blockFrames.npy'
|
|
651
|
+
assert len(names) == 34
|
|
652
|
+
assert_array_equal(np.load(paths[2]), analysis.results['rHistTMon'])
|
|
653
|
+
assert_array_equal(np.load(paths[-1]), [2, 2, 1])
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def test_save_before_run_raises(make_analysis, tmp_path):
|
|
657
|
+
"""save needs the results of run."""
|
|
658
|
+
analysis = make_analysis()
|
|
659
|
+
|
|
660
|
+
with pytest.raises(RuntimeError, match="Call run"):
|
|
661
|
+
analysis.save(tmp_path, 'N6ens1-all')
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
# Real frames
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
@pytest.fixture
|
|
668
|
+
def real_histograms(study_spec, real_universe):
|
|
669
|
+
"""Factory: :meth:`SpatialHistograms.from_spec` run on a real dump with
|
|
670
|
+
its topology, with the edges of the whole's parser in `group`."""
|
|
671
|
+
|
|
672
|
+
def _run(whole, topology, trajectory, group):
|
|
673
|
+
module = study_spec(whole.study)
|
|
674
|
+
parser = module.SPEC.parser(f"{whole.whole}.ring", 'whole', group)
|
|
675
|
+
universe = real_universe(topology, trajectory)
|
|
676
|
+
return SpatialHistograms.from_spec(universe, module.PROBE,
|
|
677
|
+
parser).run()
|
|
678
|
+
|
|
679
|
+
return _run
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
@pytest.fixture
|
|
683
|
+
def directions():
|
|
684
|
+
"""The one-dimensional histograms of each geometry."""
|
|
685
|
+
return {'cubic': ('r',), 'cylindrical': ('r', 'z', 'theta')}
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def _counted(analysis, species, directions):
|
|
689
|
+
"""Per species and direction: the distinct per-frame sums, and the sum
|
|
690
|
+
over the run."""
|
|
691
|
+
return {
|
|
692
|
+
f"{d}{s}": (np.unique(analysis.results[f"{d}HistT{s}"].sum(axis=1))
|
|
693
|
+
.tolist(), int(analysis.results[f"{d}Hist{s}"].sum()))
|
|
694
|
+
for s in species for d in directions}
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def test_real_window_histograms_conserve_every_species(
|
|
698
|
+
real_window_whole, real_files, real_histograms, study_spec,
|
|
699
|
+
directions):
|
|
700
|
+
"""Every position of a real ``all`` window falls inside the edges: each
|
|
701
|
+
frame counts every particle of each species once, and the run counts it
|
|
702
|
+
once per frame."""
|
|
703
|
+
probe = study_spec(real_window_whole.study).PROBE
|
|
704
|
+
files = real_files(real_window_whole)
|
|
705
|
+
counts = real_window_whole.window_counts()
|
|
706
|
+
|
|
707
|
+
analysis = real_histograms(real_window_whole, files.data, files.window,
|
|
708
|
+
'all')
|
|
709
|
+
|
|
710
|
+
assert _counted(analysis, probe.histogram_species,
|
|
711
|
+
directions[probe.geometry]) == {
|
|
712
|
+
f"{d}{s}": ([counts[s]], counts[s] * real_window_whole.n_window_frames)
|
|
713
|
+
for s in probe.histogram_species
|
|
714
|
+
for d in directions[probe.geometry]}
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def test_real_polymer_histograms_conserve_every_species(
|
|
718
|
+
real_polymer_whole, real_files, real_histograms, study_spec,
|
|
719
|
+
directions):
|
|
720
|
+
"""Every position of a real polymer dump falls inside the edges of its
|
|
721
|
+
whole: each frame counts every particle of each species once, and the
|
|
722
|
+
crowders, absent from the dump, not at all."""
|
|
723
|
+
probe = study_spec(real_polymer_whole.study).PROBE
|
|
724
|
+
files = real_files(real_polymer_whole)
|
|
725
|
+
counts = real_polymer_whole.polymer_counts()
|
|
726
|
+
|
|
727
|
+
analysis = real_histograms(real_polymer_whole, files.topology,
|
|
728
|
+
files.polymer, real_polymer_whole.group)
|
|
729
|
+
|
|
730
|
+
assert _counted(analysis, probe.histogram_species,
|
|
731
|
+
directions[probe.geometry]) == {
|
|
732
|
+
f"{d}{s}": ([counts[s]], counts[s] * 50)
|
|
733
|
+
for s in probe.histogram_species
|
|
734
|
+
for d in directions[probe.geometry]}
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def test_real_window_histograms_have_one_int64_row_per_frame_and_bin(
|
|
738
|
+
real_window_whole, real_files, real_histograms, study_spec,
|
|
739
|
+
directions):
|
|
740
|
+
"""Each per-frame histogram of a real window is int64 with one row per
|
|
741
|
+
frame and one column per bin of the whole's edges."""
|
|
742
|
+
module = study_spec(real_window_whole.study)
|
|
743
|
+
probe = module.PROBE
|
|
744
|
+
files = real_files(real_window_whole)
|
|
745
|
+
edges = histogram_edges(probe, module.SPEC.parser(
|
|
746
|
+
f"{real_window_whole.whole}.ring", 'whole', 'all'))
|
|
747
|
+
|
|
748
|
+
analysis = real_histograms(real_window_whole, files.data, files.window,
|
|
749
|
+
'all')
|
|
750
|
+
|
|
751
|
+
assert {f"{d}{s}": (analysis.results[f"{d}HistT{s}"].dtype,
|
|
752
|
+
analysis.results[f"{d}HistT{s}"].shape)
|
|
753
|
+
for s in probe.histogram_species
|
|
754
|
+
for d in directions[probe.geometry]} == {
|
|
755
|
+
f"{d}{s}": (np.dtype(np.int64),
|
|
756
|
+
(real_window_whole.n_window_frames, edges[d].size - 1))
|
|
757
|
+
for s in probe.histogram_species
|
|
758
|
+
for d in directions[probe.geometry]}
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
@pytest.mark.private_data # pragma: no cover - needs data
|
|
762
|
+
def test_crowded_window_histograms_conserve_every_species(
|
|
763
|
+
real_crowded_whole, private_window_files, real_histograms, study_spec,
|
|
764
|
+
directions):
|
|
765
|
+
"""Every position of a crowded ``all`` window, crowders at the wall and
|
|
766
|
+
in the corners included, falls inside the edges: each frame counts every
|
|
767
|
+
particle of each species once."""
|
|
768
|
+
probe = study_spec(real_crowded_whole.study).PROBE
|
|
769
|
+
window, data = private_window_files(real_crowded_whole)
|
|
770
|
+
counts = real_crowded_whole.window_counts()
|
|
771
|
+
|
|
772
|
+
analysis = real_histograms(real_crowded_whole, data, window, 'all')
|
|
773
|
+
|
|
774
|
+
assert _counted(analysis, probe.histogram_species,
|
|
775
|
+
directions[probe.geometry]) == {
|
|
776
|
+
f"{d}{s}": ([counts[s]],
|
|
777
|
+
counts[s] * real_crowded_whole.n_window_frames)
|
|
778
|
+
for s in probe.histogram_species
|
|
779
|
+
for d in directions[probe.geometry]}
|