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.
Files changed (69) hide show
  1. polyphys/__init__.py +7 -0
  2. polyphys/__version__.py +22 -0
  3. polyphys/analyze/__init__.py +51 -0
  4. polyphys/analyze/clusters.py +1738 -0
  5. polyphys/analyze/correlations.py +995 -0
  6. polyphys/analyze/density.py +1138 -0
  7. polyphys/analyze/measurer.py +1338 -0
  8. polyphys/analyze/pipeline.py +1868 -0
  9. polyphys/analyze/stats.py +810 -0
  10. polyphys/analyze/sumrule.py +1159 -0
  11. polyphys/manage/__init__.py +11 -0
  12. polyphys/manage/organizer.py +2188 -0
  13. polyphys/manage/parser.py +1264 -0
  14. polyphys/manage/types.py +171 -0
  15. polyphys/manage/utils.py +777 -0
  16. polyphys/probe/__init__.py +25 -0
  17. polyphys/probe/binders.py +711 -0
  18. polyphys/probe/chain_size.py +583 -0
  19. polyphys/probe/core.py +437 -0
  20. polyphys/probe/histograms.py +863 -0
  21. polyphys/probe/pairs.py +555 -0
  22. polyphys/simulate/__init__.py +48 -0
  23. polyphys/simulate/build.py +1231 -0
  24. polyphys/simulate/core.py +237 -0
  25. polyphys/simulate/data.py +977 -0
  26. polyphys/simulate/dump.py +752 -0
  27. polyphys/simulate/log.py +788 -0
  28. polyphys/spec.py +1359 -0
  29. polyphys/tests/analyze/test_clusters.py +1058 -0
  30. polyphys/tests/analyze/test_correlations.py +708 -0
  31. polyphys/tests/analyze/test_density.py +813 -0
  32. polyphys/tests/analyze/test_measurer.py +785 -0
  33. polyphys/tests/analyze/test_pipeline.py +1881 -0
  34. polyphys/tests/analyze/test_stats.py +732 -0
  35. polyphys/tests/analyze/test_sumrule.py +917 -0
  36. polyphys/tests/conftest.py +753 -0
  37. polyphys/tests/data/lammps_build/make_starts.py +70 -0
  38. polyphys/tests/manage/test_organizer.py +1495 -0
  39. polyphys/tests/manage/test_parser.py +1222 -0
  40. polyphys/tests/manage/test_utils.py +498 -0
  41. polyphys/tests/probe/conftest.py +407 -0
  42. polyphys/tests/probe/test_binders.py +686 -0
  43. polyphys/tests/probe/test_chain_size.py +557 -0
  44. polyphys/tests/probe/test_core.py +391 -0
  45. polyphys/tests/probe/test_histograms.py +779 -0
  46. polyphys/tests/probe/test_pairs.py +475 -0
  47. polyphys/tests/simulate/test_build.py +1042 -0
  48. polyphys/tests/simulate/test_core.py +106 -0
  49. polyphys/tests/simulate/test_data.py +858 -0
  50. polyphys/tests/simulate/test_dump.py +487 -0
  51. polyphys/tests/simulate/test_log.py +667 -0
  52. polyphys/tests/strategies.py +120 -0
  53. polyphys/tests/synthetic.py +410 -0
  54. polyphys/tests/test_conftest.py +414 -0
  55. polyphys/tests/test_docs.py +82 -0
  56. polyphys/tests/test_packaging.py +97 -0
  57. polyphys/tests/test_spec.py +772 -0
  58. polyphys/tests/viz/conftest.py +32 -0
  59. polyphys/tests/viz/test_colors.py +113 -0
  60. polyphys/tests/viz/test_helpers.py +292 -0
  61. polyphys/viz/__init__.py +15 -0
  62. polyphys/viz/colors.py +204 -0
  63. polyphys/viz/helpers.py +549 -0
  64. polyphys-1.0.0.dist-info/METADATA +339 -0
  65. polyphys-1.0.0.dist-info/RECORD +69 -0
  66. polyphys-1.0.0.dist-info/WHEEL +5 -0
  67. polyphys-1.0.0.dist-info/licenses/AUTHORS.rst +13 -0
  68. polyphys-1.0.0.dist-info/licenses/LICENSE +22 -0
  69. 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)