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,391 @@
|
|
|
1
|
+
"""Tests for :mod:`polyphys.probe.core`.
|
|
2
|
+
|
|
3
|
+
The inputs are tier A: a data file and a dump of one linear chain written
|
|
4
|
+
by the ``make_chain_files`` fixture, and parsers built from hand-written
|
|
5
|
+
names. Expected values are the positions the fixture wrote and the
|
|
6
|
+
attributes the parser sets.
|
|
7
|
+
|
|
8
|
+
The real-frame tests read the tier B and tier C files of the studies'
|
|
9
|
+
wholes (``polyphys/tests/probe/conftest.py``, "Real frames"). Their
|
|
10
|
+
expected values are the dump headers and coordinates as the package's own
|
|
11
|
+
LAMMPS readers, :func:`polyphys.simulate.dump.iter_dump` and
|
|
12
|
+
:func:`polyphys.simulate.data.read_data`, read them, and the particle
|
|
13
|
+
counts in the wholes' names.
|
|
14
|
+
"""
|
|
15
|
+
import os
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import textwrap
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
import pandas as pd
|
|
23
|
+
import pytest
|
|
24
|
+
from numpy.testing import assert_allclose, assert_array_equal
|
|
25
|
+
|
|
26
|
+
import polyphys
|
|
27
|
+
from polyphys.manage.organizer import children_stamps
|
|
28
|
+
from polyphys.tests.synthetic import ChainCyl
|
|
29
|
+
from polyphys.simulate.data import read_data
|
|
30
|
+
from polyphys.simulate.dump import iter_dump
|
|
31
|
+
|
|
32
|
+
pytest.importorskip("MDAnalysis")
|
|
33
|
+
|
|
34
|
+
from polyphys.probe import make_universe, write_stamps # noqa: E402
|
|
35
|
+
from polyphys.probe.binders import BinderStatistics # noqa: E402
|
|
36
|
+
from polyphys.probe.chain_size import ChainSize # noqa: E402
|
|
37
|
+
from polyphys.probe.core import ( # noqa: E402
|
|
38
|
+
_ClosingMultiprocessing, _ProbeAnalysis)
|
|
39
|
+
from polyphys.probe.histograms import SpatialHistograms # noqa: E402
|
|
40
|
+
from polyphys.probe.pairs import PairDistances # noqa: E402
|
|
41
|
+
|
|
42
|
+
# The dump reader warns that the frames carry no time step; the tests that
|
|
43
|
+
# give no dt measure per frame.
|
|
44
|
+
pytestmark = pytest.mark.filterwarnings(
|
|
45
|
+
"ignore:Reader has no dt information:UserWarning")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.fixture
|
|
49
|
+
def chain_frames() -> np.ndarray:
|
|
50
|
+
"""Five frames of a 10-bead chain along x, moved 0.1 along z per frame."""
|
|
51
|
+
first = np.zeros((10, 3))
|
|
52
|
+
first[:, 0] = np.arange(10) + 5.0
|
|
53
|
+
first[:, 1:] = 10.0
|
|
54
|
+
return np.stack([first + [0.0, 0.0, 0.1 * k] for k in range(5)])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.fixture
|
|
58
|
+
def whole_parser() -> ChainCyl:
|
|
59
|
+
"""A ChainCyl parser of a whole, from a hand-written name."""
|
|
60
|
+
return ChainCyl(
|
|
61
|
+
'N10r2.5lz10ac1nc0dt0.005bdump1adump1ens1', 'whole', 'bug')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# make_universe
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_make_universe_reads_the_chain_and_its_frames(
|
|
68
|
+
make_chain_files, chain_frames):
|
|
69
|
+
"""The universe has the data file's atoms, all in residue 1, and the
|
|
70
|
+
dump's frames."""
|
|
71
|
+
data_path, dump_path = make_chain_files(chain_frames)
|
|
72
|
+
|
|
73
|
+
universe = make_universe(data_path, dump_path)
|
|
74
|
+
|
|
75
|
+
assert (universe.atoms.n_atoms, universe.trajectory.n_frames) == (10, 5)
|
|
76
|
+
assert_array_equal(universe.select_atoms('resid 1').ids, np.arange(1, 11))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_make_universe_positions_follow_the_dump_frame_by_frame(
|
|
80
|
+
make_chain_files, chain_frames):
|
|
81
|
+
"""Frame k of the trajectory holds the dump's frame k, in atom-ID
|
|
82
|
+
order."""
|
|
83
|
+
data_path, dump_path = make_chain_files(chain_frames)
|
|
84
|
+
universe = make_universe(data_path, dump_path)
|
|
85
|
+
|
|
86
|
+
read = np.stack([universe.atoms.positions.copy()
|
|
87
|
+
for _ in universe.trajectory])
|
|
88
|
+
|
|
89
|
+
# 1e-5: the dump holds six decimals and MDAnalysis reads float32.
|
|
90
|
+
assert_allclose(read, chain_frames, atol=1e-5)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_make_universe_passes_extra_keywords_through(make_chain_files,
|
|
94
|
+
chain_frames):
|
|
95
|
+
"""A keyword such as dt reaches the MDAnalysis universe."""
|
|
96
|
+
data_path, dump_path = make_chain_files(chain_frames)
|
|
97
|
+
|
|
98
|
+
universe = make_universe(data_path, dump_path, dt=2.0)
|
|
99
|
+
|
|
100
|
+
assert universe.trajectory.dt == 2.0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_make_universe_rejects_a_fixed_keyword(make_chain_files,
|
|
104
|
+
chain_frames):
|
|
105
|
+
"""A keyword that make_universe fixes cannot be overridden."""
|
|
106
|
+
data_path, dump_path = make_chain_files(chain_frames)
|
|
107
|
+
|
|
108
|
+
with pytest.raises(ValueError, match=r"\['format'\] are fixed"):
|
|
109
|
+
make_universe(data_path, dump_path, format='DCD')
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# write_stamps
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_write_stamps_columns_are_genealogy_group_attributes_and_frames(
|
|
116
|
+
whole_parser, tmp_path):
|
|
117
|
+
"""The stamps have the lineage names, group, exactly the parser's
|
|
118
|
+
attributes in order, then n_frames, with the parser's values."""
|
|
119
|
+
path = write_stamps(whole_parser, 7, tmp_path)
|
|
120
|
+
|
|
121
|
+
stamps = pd.read_csv(path)
|
|
122
|
+
|
|
123
|
+
assert path.name == f'{whole_parser.name}-bug-stamps.csv'
|
|
124
|
+
assert stamps.columns.tolist() == (
|
|
125
|
+
['whole', 'ensemble_long', 'ensemble', 'space', 'group']
|
|
126
|
+
+ whole_parser.attributes + ['n_frames'])
|
|
127
|
+
assert stamps['whole'].tolist() == [whole_parser.name]
|
|
128
|
+
assert stamps['group'].tolist() == ['bug']
|
|
129
|
+
assert stamps['n_frames'].tolist() == [7]
|
|
130
|
+
for attr in whole_parser.attributes:
|
|
131
|
+
assert stamps.loc[0, attr] == getattr(whole_parser, attr), attr
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_write_stamps_reads_back_through_children_stamps(whole_parser,
|
|
135
|
+
tmp_path):
|
|
136
|
+
"""The organizer combines two written stamps into one row per whole."""
|
|
137
|
+
second = ChainCyl(
|
|
138
|
+
'N10r2.5lz10ac1nc0dt0.005bdump1adump1ens2', 'whole', 'bug')
|
|
139
|
+
paths = [(str(write_stamps(parser, n, tmp_path)),)
|
|
140
|
+
for parser, n in ((whole_parser, 3), (second, 4))]
|
|
141
|
+
|
|
142
|
+
combined = children_stamps(paths, 'whole')
|
|
143
|
+
|
|
144
|
+
assert combined['ensemble_id'].tolist() == [1, 2]
|
|
145
|
+
assert combined['n_frames'].tolist() == [3, 4]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@pytest.mark.parametrize('n_frames', [
|
|
149
|
+
pytest.param(-1, id='negative'),
|
|
150
|
+
pytest.param(2.5, id='float'),
|
|
151
|
+
pytest.param(True, id='bool'),
|
|
152
|
+
])
|
|
153
|
+
def test_write_stamps_rejects_a_bad_frame_count(whole_parser, tmp_path,
|
|
154
|
+
n_frames):
|
|
155
|
+
"""n_frames must be a non-negative integer."""
|
|
156
|
+
with pytest.raises(ValueError, match="non-negative integer"):
|
|
157
|
+
write_stamps(whole_parser, n_frames, tmp_path)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# The multiprocessing backend
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_multiprocessing_run_leaves_no_leaked_semaphore():
|
|
164
|
+
"""A run on the multiprocessing backend lets its workers exit, so the
|
|
165
|
+
resource tracker finds no leaked semaphore at shutdown."""
|
|
166
|
+
script = textwrap.dedent("""\
|
|
167
|
+
import numpy as np
|
|
168
|
+
import MDAnalysis as mda
|
|
169
|
+
from polyphys.probe.chain_size import ChainSize
|
|
170
|
+
|
|
171
|
+
if __name__ == '__main__':
|
|
172
|
+
frames = np.zeros((4, 2, 3))
|
|
173
|
+
frames[:, 1, 2] = [1.0, 2.0, 3.0, 4.0]
|
|
174
|
+
universe = mda.Universe.empty(2, trajectory=True)
|
|
175
|
+
universe.add_TopologyAttr('masses', np.ones(2))
|
|
176
|
+
universe.load_new(frames, format='MEMORY')
|
|
177
|
+
ChainSize(universe.atoms, ('rfloryT',)).run(
|
|
178
|
+
backend='multiprocessing', n_workers=2, n_parts=2)
|
|
179
|
+
""")
|
|
180
|
+
# The child and its workers import this checkout's package.
|
|
181
|
+
env = dict(os.environ,
|
|
182
|
+
PYTHONPATH=str(Path(polyphys.__file__).parents[1]))
|
|
183
|
+
|
|
184
|
+
completed = subprocess.run(
|
|
185
|
+
[sys.executable, '-W', 'default', '-c', script], env=env,
|
|
186
|
+
capture_output=True, text=True, timeout=120, check=False)
|
|
187
|
+
|
|
188
|
+
assert completed.returncode == 0, completed.stderr
|
|
189
|
+
assert 'leaked semaphore' not in completed.stderr
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_closing_multiprocessing_apply_passes_a_worker_error_on():
|
|
193
|
+
"""An error raised in a worker reaches the caller, after the pool is
|
|
194
|
+
terminated and joined."""
|
|
195
|
+
backend = _ClosingMultiprocessing(n_workers=1)
|
|
196
|
+
|
|
197
|
+
# int of a non-numeric string raises ValueError in the worker.
|
|
198
|
+
with pytest.raises(ValueError, match="invalid literal for int"):
|
|
199
|
+
backend.apply(int, ['1', 'x'])
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@pytest.mark.parametrize('analysis_class', [
|
|
203
|
+
pytest.param(BinderStatistics, id='BinderStatistics'),
|
|
204
|
+
pytest.param(ChainSize, id='ChainSize'),
|
|
205
|
+
pytest.param(PairDistances, id='PairDistances'),
|
|
206
|
+
pytest.param(SpatialHistograms, id='SpatialHistograms'),
|
|
207
|
+
])
|
|
208
|
+
def test_every_probe_analysis_runs_on_the_closing_pool(analysis_class):
|
|
209
|
+
"""Every probe analysis takes its run, and so its multiprocessing
|
|
210
|
+
backend, from the shared probe base."""
|
|
211
|
+
assert analysis_class.run is _ProbeAnalysis.run
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# Real frames
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_make_universe_opens_every_real_polymer_dump(
|
|
218
|
+
real_polymer_whole, real_files, real_universe):
|
|
219
|
+
"""A polymer dump opens with its topology, with the dump's atoms and
|
|
220
|
+
all 50 of its frames."""
|
|
221
|
+
files = real_files(real_polymer_whole)
|
|
222
|
+
n_atoms = next(iter_dump(files.polymer)).n_atoms
|
|
223
|
+
|
|
224
|
+
universe = real_universe(files.topology, files.polymer)
|
|
225
|
+
|
|
226
|
+
assert (universe.atoms.n_atoms, universe.trajectory.n_frames) == (
|
|
227
|
+
n_atoms, 50)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_make_universe_opens_every_real_window(real_window_whole, real_files,
|
|
231
|
+
real_universe):
|
|
232
|
+
"""An ``all`` window opens with its whole's data file, with the dump's
|
|
233
|
+
atoms and every frame of the window."""
|
|
234
|
+
files = real_files(real_window_whole)
|
|
235
|
+
n_atoms = next(iter_dump(files.window)).n_atoms
|
|
236
|
+
|
|
237
|
+
universe = real_universe(files.data, files.window)
|
|
238
|
+
|
|
239
|
+
assert (universe.atoms.n_atoms, universe.trajectory.n_frames) == (
|
|
240
|
+
n_atoms, real_window_whole.n_window_frames)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_make_universe_positions_are_the_dump_coordinates_from_the_origin(
|
|
244
|
+
real_polymer_whole, real_files, real_universe, trajectory_arrays):
|
|
245
|
+
"""Every frame's positions are the dump's coordinates, sorted by atom
|
|
246
|
+
ID, less the box's lower corner."""
|
|
247
|
+
files = real_files(real_polymer_whole)
|
|
248
|
+
expected = np.stack([frame.positions() - frame.box.lo
|
|
249
|
+
for frame in iter_dump(files.polymer)])
|
|
250
|
+
|
|
251
|
+
positions = trajectory_arrays(
|
|
252
|
+
real_universe(files.topology, files.polymer))['positions']
|
|
253
|
+
|
|
254
|
+
# float32 positions of at most about 165 carry an absolute round-off
|
|
255
|
+
# of about 1e-5.
|
|
256
|
+
assert_allclose(positions, expected, rtol=0, atol=2e-5)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_spec_selections_pick_the_species_of_a_real_polymer_dump(
|
|
260
|
+
real_polymer_whole, real_files, real_universe, study_spec):
|
|
261
|
+
"""Each selection of the study's probe spec picks, in the polymer dump,
|
|
262
|
+
the atoms the whole's name gives its species."""
|
|
263
|
+
probe = study_spec(real_polymer_whole.study).PROBE
|
|
264
|
+
files = real_files(real_polymer_whole)
|
|
265
|
+
universe = real_universe(files.topology, files.polymer)
|
|
266
|
+
|
|
267
|
+
counts = {species: universe.select_atoms(selection).n_atoms
|
|
268
|
+
for species, selection in probe.selections.items()}
|
|
269
|
+
|
|
270
|
+
assert counts == real_polymer_whole.polymer_counts()
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_spec_selections_pick_the_species_of_a_real_window(
|
|
274
|
+
real_window_whole, real_files, real_universe, study_spec):
|
|
275
|
+
"""Each selection of the study's probe spec picks, in the ``all``
|
|
276
|
+
window, the atoms the whole's name gives its species, crowders
|
|
277
|
+
included."""
|
|
278
|
+
probe = study_spec(real_window_whole.study).PROBE
|
|
279
|
+
files = real_files(real_window_whole)
|
|
280
|
+
universe = real_universe(files.data, files.window)
|
|
281
|
+
|
|
282
|
+
counts = {species: universe.select_atoms(selection).n_atoms
|
|
283
|
+
for species, selection in probe.selections.items()}
|
|
284
|
+
|
|
285
|
+
assert counts == real_window_whole.window_counts()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@pytest.mark.parametrize('study, whole, expected', [
|
|
289
|
+
pytest.param('hns_cub', 'N200kbmm2nh30ac2l25epshc1nc10743ens1',
|
|
290
|
+
{'nmon': 200, 'nhns': 30, 'dcrowd': 2.0, 'lcube': 50.0,
|
|
291
|
+
'eps_hc': 1.0, 'ncrowd': 10743, 'ensemble_id': 1},
|
|
292
|
+
id='HnsCub'),
|
|
293
|
+
pytest.param('hns_cyl', 'N200kbmm2r4.5nh30ac2lz75epshc1nc386ens2',
|
|
294
|
+
{'nmon': 200, 'nhns': 30, 'dcrowd': 2.0, 'dcyl': 8.0,
|
|
295
|
+
'lcyl': 150.0, 'ncrowd': 386, 'ensemble_id': 2},
|
|
296
|
+
id='HnsCyl'),
|
|
297
|
+
pytest.param('trans_foci_cub',
|
|
298
|
+
'al6nl5ml216ns400ac1nc112776l27dt0.005bdump2000adump5000'
|
|
299
|
+
'ens1',
|
|
300
|
+
{'dmon_large': 6.0, 'nmon_large': 5, 'mmon_large': 216.0,
|
|
301
|
+
'nmon_small': 400, 'ncrowd': 112776, 'lcube': 54.0},
|
|
302
|
+
id='TransFociCub'),
|
|
303
|
+
pytest.param('trans_foci_cyl',
|
|
304
|
+
'epss5epsl5r10.5al6nl5ml216ns400ac1nc0lz82.5dt0.005'
|
|
305
|
+
'bdump2000adump5000ens2',
|
|
306
|
+
{'dmon_large': 6.0, 'nmon_small': 400, 'ncrowd': 0,
|
|
307
|
+
'dcyl': 20.0, 'lcyl': 165.0, 'ensemble_id': 2},
|
|
308
|
+
id='TransFociCyl'),
|
|
309
|
+
])
|
|
310
|
+
def test_write_stamps_of_a_real_whole_carry_its_name(
|
|
311
|
+
study_spec, tmp_path, study, whole, expected):
|
|
312
|
+
"""The stamps of a real whole hold the values its name encodes and the
|
|
313
|
+
50 frames of its polymer dump."""
|
|
314
|
+
spec = study_spec(study).SPEC
|
|
315
|
+
parser = spec.parser(f"{whole}.ring", 'whole', spec.group)
|
|
316
|
+
|
|
317
|
+
stamps = pd.read_csv(write_stamps(parser, 50, tmp_path))
|
|
318
|
+
|
|
319
|
+
assert {key: stamps.loc[0, key] for key in expected} == expected
|
|
320
|
+
# A whole's lineage name never carries the topology suffix ``.ring``
|
|
321
|
+
# (ParserBase._set_parents).
|
|
322
|
+
assert (stamps.loc[0, 'whole'], int(stamps.loc[0, 'n_frames'])) == (
|
|
323
|
+
whole, 50)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@pytest.mark.private_data # pragma: no cover - needs data
|
|
327
|
+
def test_borrowed_topology_is_the_crowded_whole_own_polymer_topology(
|
|
328
|
+
real_crowded_whole, real_files, private_window_files):
|
|
329
|
+
"""The polymer atoms of a crowded whole's own data file have the IDs,
|
|
330
|
+
types, molecule IDs, masses, and bonds of the data file its polymer
|
|
331
|
+
dump borrows."""
|
|
332
|
+
_, own_path = private_window_files(real_crowded_whole)
|
|
333
|
+
borrowed = read_data(real_files(real_crowded_whole).topology)
|
|
334
|
+
own = read_data(own_path)
|
|
335
|
+
n_polymer = borrowed.n_atoms
|
|
336
|
+
in_polymer = np.all(own.bonds[:, 1:] <= n_polymer, axis=1)
|
|
337
|
+
|
|
338
|
+
def as_set(bonds):
|
|
339
|
+
# Each run's write_data numbers its bonds in its own order, so the
|
|
340
|
+
# bonds are compared as a set: sorted by type and atoms.
|
|
341
|
+
return bonds[np.lexsort((bonds[:, 2], bonds[:, 1], bonds[:, 0]))]
|
|
342
|
+
|
|
343
|
+
assert_array_equal(own.ids[:n_polymer], borrowed.ids)
|
|
344
|
+
assert_array_equal(own.types[:n_polymer], borrowed.types)
|
|
345
|
+
assert_array_equal(own.mol_ids[:n_polymer], borrowed.mol_ids)
|
|
346
|
+
assert_array_equal(own.masses[borrowed.types - 1],
|
|
347
|
+
borrowed.masses[borrowed.types - 1])
|
|
348
|
+
assert_array_equal(as_set(own.bonds[in_polymer]),
|
|
349
|
+
as_set(borrowed.bonds))
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@pytest.mark.private_data # pragma: no cover - needs data
|
|
353
|
+
def test_write_stamps_of_a_real_whole_match_the_research_code(
|
|
354
|
+
real_transfocicub_whole, study_spec, research_probe_file, tmp_path):
|
|
355
|
+
"""The stamps of a TransFociCub whole hold the research code's lineage
|
|
356
|
+
names under the documented mapping (Usage, "Lineage names"), the same
|
|
357
|
+
attribute values under the spec's renaming, every research attribute
|
|
358
|
+
but ``eps_others``, and the 50 frames the research probe counted."""
|
|
359
|
+
whole = real_transfocicub_whole
|
|
360
|
+
spec = study_spec(whole.study).SPEC
|
|
361
|
+
parser = spec.parser(f"{whole.whole}.ring", 'whole', spec.group)
|
|
362
|
+
theirs = pd.read_csv(research_probe_file(whole, 'stamps', ext='csv'))
|
|
363
|
+
# The research names of the bulk fractions and densities and their
|
|
364
|
+
# PolyPhys names (projects/trans_foci_cub/spec.py, module docstring).
|
|
365
|
+
renamed = {'phi_c_bulk': 'phi_bulk_c', 'rho_c_bulk': 'rho_bulk_c',
|
|
366
|
+
'phi_m_bulk': 'phi_bulk_m', 'rho_m_bulk': 'rho_bulk_m'}
|
|
367
|
+
names = ('lineage_name', 'whole', 'ensemble_long', 'ensemble', 'space')
|
|
368
|
+
|
|
369
|
+
ours = pd.read_csv(write_stamps(parser, 50, tmp_path))
|
|
370
|
+
|
|
371
|
+
# A research whole name keeps ``.ring``; a short name parsed at its
|
|
372
|
+
# lineage and rebuilt gives the current form.
|
|
373
|
+
assert ours.loc[0, 'whole'] == theirs.loc[0, 'whole'].removesuffix(
|
|
374
|
+
'.ring')
|
|
375
|
+
assert ours.loc[0, 'ensemble_long'] == theirs.loc[0, 'ensemble_long']
|
|
376
|
+
assert {lineage: ours.loc[0, lineage] for lineage in ('ensemble', 'space')
|
|
377
|
+
} == {lineage: getattr(spec.parser(theirs.loc[0, lineage],
|
|
378
|
+
lineage, spec.group), lineage)
|
|
379
|
+
for lineage in ('ensemble', 'space')}
|
|
380
|
+
attributes = [column for column in theirs.columns
|
|
381
|
+
if column not in names and column != 'n_frames']
|
|
382
|
+
unmatched = [column for column in attributes
|
|
383
|
+
if renamed.get(column, column) not in ours.columns]
|
|
384
|
+
assert unmatched == ['eps_others']
|
|
385
|
+
matched = [column for column in attributes if column not in unmatched]
|
|
386
|
+
# Both codes compute the fractions in float64 from the same integers.
|
|
387
|
+
assert_allclose(
|
|
388
|
+
ours.loc[0, [renamed.get(column, column) for column in matched]]
|
|
389
|
+
.to_numpy(dtype=float),
|
|
390
|
+
theirs.loc[0, matched].to_numpy(dtype=float), rtol=1e-12, atol=0)
|
|
391
|
+
assert int(theirs.loc[0, 'n_frames']) == int(ours.loc[0, 'n_frames']) == 50
|