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,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])