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,407 @@
1
+ """
2
+ Fixtures shared by the probe tests.
3
+
4
+ Every test here needs MDAnalysis, which the ``mda`` extra installs; the test
5
+ modules call ``pytest.importorskip("MDAnalysis")``, and the fixtures import
6
+ the probe layer inside their factories so that collecting this folder
7
+ without MDAnalysis is harmless.
8
+
9
+ Fixtures
10
+ --------
11
+ make_chain_universe
12
+ The files of ``make_chain_files``, in ``polyphys/tests/conftest.py``,
13
+ opened with :func:`polyphys.probe.make_universe`.
14
+ study_spec
15
+ The spec module, ``projects/<study>/spec.py``, of a study.
16
+ real_polymer_whole, real_window_whole, real_crowded_whole, \
17
+ real_foci_whole, real_hns_whole, real_nh0_whole
18
+ The real wholes of the tier B and tier C tests, one test per whole (see
19
+ "Real frames" below).
20
+ real_files
21
+ The files of a real whole: its polymer dump and the topology it is
22
+ opened with, and its ``all`` window and data file when they are tier B.
23
+ private_window_files
24
+ The ``all`` window and data file of a crowded whole, tier C.
25
+ real_transfocicub_whole
26
+ The 4 TransFociCub wholes, whose research-code probe output is the
27
+ tier C reference of the pipeline tests.
28
+ research_probe_file
29
+ One property file of the research code's probe output of a whole,
30
+ tier C.
31
+ real_universe
32
+ A real dump opened with :func:`polyphys.probe.make_universe`.
33
+ trajectory_arrays
34
+ The positions, box lengths, and time steps of every frame of a
35
+ universe, in one pass.
36
+ reference_distances
37
+ Minimum-image distances between two sets of positions, frame by frame,
38
+ from :func:`MDAnalysis.lib.distances.distance_array`.
39
+
40
+ Real frames
41
+ -----------
42
+ The tier B files are in ``polyphys/tests/data/probe/<whole>/``: for each of
43
+ 22 wholes of the four live studies, the last 50 frames of the polymer group
44
+ dump (``<whole>.ring.<group>.last50.lammpstrj.gz``), and for the wholes
45
+ without crowders and the HnsCyl wholes at ``ac2 nc386``, the ``all`` frames
46
+ of the same time window (``<whole>.j<NN>.ring.all.window.lammpstrj.gz``)
47
+ and the whole's data file (``<whole>.ring.all.data.gz``). The maintainer
48
+ trimmed them from the studies' runs. The HnsCyl runs name their wholes
49
+ without ``epshc``; their copies here insert ``epshc1``, the value of their
50
+ H-NS core-crowder pair coefficients, so that the parser reads them.
51
+
52
+ The tier C files, the ``all`` windows and data files of the other crowded
53
+ wholes, stay in the maintainer's folder
54
+ ``<POLYPHYS_DATA_DIR>/<project>/trjs/`` under the runs' own names, with
55
+ ``<project>`` the study's project name, such as ``HnsCub``.
56
+
57
+ A polymer dump holds only the polymer group, so it cannot be opened with
58
+ its whole's full data file. It is opened with the data file of the whole
59
+ without crowders of the same family and replicate (:attr:`RealWhole.topology`),
60
+ whose atoms are the polymer group: the ring for TransFoci, the ring and the
61
+ H-NS proteins for Hns. The research code did the same. The polymer atoms
62
+ carry the IDs 1 to n in every system, with the crowders after them.
63
+ """
64
+ import importlib
65
+ import sys
66
+ import warnings
67
+ from dataclasses import dataclass
68
+ from pathlib import Path
69
+ from types import ModuleType
70
+ from typing import Any, Callable, Mapping, Optional
71
+
72
+ import numpy as np
73
+ import pytest
74
+
75
+
76
+ @pytest.fixture
77
+ def make_chain_universe(
78
+ make_chain_files: Callable[..., tuple[Path, Path]]
79
+ ) -> Callable[..., Any]:
80
+ """Factory: the universe of the files ``make_chain_files`` writes, with
81
+ the same arguments."""
82
+
83
+ def _make(positions: Any, **kwargs: Any) -> Any:
84
+ from polyphys.probe import make_universe
85
+
86
+ data_path, dump_path = make_chain_files(positions, **kwargs)
87
+ return make_universe(data_path, dump_path)
88
+
89
+ return _make
90
+
91
+
92
+ # Real frames
93
+
94
+
95
+ @dataclass(frozen=True)
96
+ class RealWhole:
97
+ """One real whole of the tier B and tier C tests.
98
+
99
+ Attributes
100
+ ----------
101
+ id : str
102
+ The test id, such as ``'HnsCub-nh30-nc10743-ens1'``.
103
+ study : str
104
+ Its study's folder in ``projects/``, such as ``'hns_cub'``.
105
+ whole : str
106
+ The whole's name without ``.ring``, as its parser reads it.
107
+ topology : str
108
+ The whole whose data file opens the polymer dump: the whole itself
109
+ when it has no crowders, else the whole without crowders of its
110
+ family and replicate.
111
+ group : str
112
+ The polymer group: ``'nucleoid'`` or ``'bug'``.
113
+ n_mon, n_foci, n_hns, n_crowd : int
114
+ The monomers, the large monomers among them, the H-NS proteins, and
115
+ the crowders, from the name (keywords ``N`` or ``ns`` plus ``nl``,
116
+ ``nl``, ``nh``, and ``nc``).
117
+ window_tier : str
118
+ ``'B'`` when the whole's ``all`` window is in the repository,
119
+ ``'C'`` when it is the maintainer's, ``''`` for none.
120
+ n_window_frames : int
121
+ The frames of the ``all`` window: 25 for Hns, 20 for TransFoci.
122
+ n_shared_frames : int
123
+ The window's frames that are also polymer frames, by time step:
124
+ all 25 for Hns, whose ``all`` dump period (10000) is twice the
125
+ ``nucleoid`` one (5000); 10 for TransFoci, whose periods are 5000
126
+ and 2000.
127
+ source : str
128
+ The whole's folder in the maintainer's fixtures folder: `whole`,
129
+ but without ``epshc1`` for HnsCyl.
130
+ """
131
+
132
+ id: str
133
+ study: str
134
+ whole: str
135
+ topology: str
136
+ group: str
137
+ n_mon: int
138
+ n_foci: int
139
+ n_hns: int
140
+ n_crowd: int
141
+ window_tier: str
142
+ n_window_frames: int
143
+ n_shared_frames: int
144
+ source: str
145
+
146
+ def polymer_counts(self) -> dict[str, int]:
147
+ """The atoms of each species of the study's spec in the polymer
148
+ dump: two patches per H-NS core (Jung, Sadeghi, and Ha, Sci. Rep.
149
+ 14, 139 (2024)), the small monomers and the foci of a TransFoci
150
+ ring, and no crowders."""
151
+ if self.group == 'nucleoid':
152
+ return {'Mon': self.n_mon, 'HnsPatch': 2 * self.n_hns,
153
+ 'Hns': self.n_hns, 'Crd': 0}
154
+ return {'Mon': self.n_mon, 'Foci': self.n_foci,
155
+ 'Dna': self.n_mon - self.n_foci, 'Crd': 0}
156
+
157
+ def window_counts(self) -> dict[str, int]:
158
+ """The atoms of each species of the study's spec in the ``all``
159
+ window: the polymer group plus the crowders."""
160
+ return {**self.polymer_counts(), 'Crd': self.n_crowd}
161
+
162
+
163
+ def _real_wholes() -> list[RealWhole]:
164
+ """The 22 real wholes: per study, two replicates without crowders and
165
+ two at a bulk crowder volume fraction of about 0.36 to 0.38, and for
166
+ the Hns studies the wholes without H-NS; HnsCyl adds crowders of size
167
+ 1."""
168
+ hns_cub = 'N200kbmm2nh{nh}ac2l25epshc1nc{nc}ens{e}'
169
+ hns_cyl = 'N200kbmm2r4.5nh{nh}ac{ac}lz75epshc1nc{nc}ens{e}'
170
+ foci_cub = ('al6nl5ml216ns400ac1nc{nc}l{l}dt0.005bdump2000adump5000'
171
+ 'ens{e}')
172
+ foci_cyl = ('epss5epsl5r10.5al6nl5ml216ns400ac1nc{nc}lz{l}dt0.005'
173
+ 'bdump2000adump5000ens{e}')
174
+ wholes = []
175
+ for e in (1, 2):
176
+ for nh, nc, tier in ((0, 0, 'B'), (30, 0, 'B'), (30, 10743, 'C')):
177
+ name = hns_cub.format(nh=nh, nc=nc, e=e)
178
+ wholes.append(RealWhole(
179
+ f"HnsCub-nh{nh}-nc{nc}-ens{e}", 'hns_cub', name,
180
+ hns_cub.format(nh=nh, nc=0, e=e), 'nucleoid', 200, 0, nh,
181
+ nc, tier, 25, 25, name))
182
+ for nh, ac, nc, tier in ((0, 2, 0, 'B'), (30, 2, 0, 'B'),
183
+ (30, 2, 386, 'B'), (30, 1, 4032, 'C')):
184
+ name = hns_cyl.format(nh=nh, ac=ac, nc=nc, e=e)
185
+ wholes.append(RealWhole(
186
+ f"HnsCyl-nh{nh}-ac{ac}-nc{nc}-ens{e}", 'hns_cyl', name,
187
+ hns_cyl.format(nh=nh, ac=2, nc=0, e=e), 'nucleoid', 200, 0,
188
+ nh, nc, tier, 25, 25, name.replace('epshc1', '')))
189
+ for template, study, label, uncrowded, crowded in (
190
+ (foci_cub, 'trans_foci_cub', 'TransFociCub',
191
+ (0, '46.5'), (112776, '27')),
192
+ (foci_cyl, 'trans_foci_cyl', 'TransFociCyl',
193
+ (0, '82.5'), (22230, '57'))):
194
+ topology = template.format(nc=uncrowded[0], l=uncrowded[1], e=e)
195
+ for (nc, length), tier in ((uncrowded, 'B'), (crowded, 'C')):
196
+ name = template.format(nc=nc, l=length, e=e)
197
+ wholes.append(RealWhole(
198
+ f"{label}-nc{nc}-ens{e}", study, name, topology, 'bug',
199
+ 405, 5, 0, nc, tier, 20, 10, name))
200
+ return sorted(wholes, key=lambda whole: whole.id)
201
+
202
+
203
+ def _params(keep: Callable[[RealWhole], bool]) -> list[Any]:
204
+ """The real wholes that `keep` accepts, as pytest params."""
205
+ return [pytest.param(whole, id=whole.id) for whole in _real_wholes()
206
+ if keep(whole)]
207
+
208
+
209
+ @pytest.fixture(params=_params(lambda whole: True))
210
+ def real_polymer_whole(request: pytest.FixtureRequest) -> RealWhole:
211
+ """Each of the 22 real wholes, whose polymer dumps are all tier B."""
212
+ return request.param
213
+
214
+
215
+ @pytest.fixture(params=_params(lambda whole: whole.window_tier == 'B'))
216
+ def real_window_whole(request: pytest.FixtureRequest) -> RealWhole:
217
+ """Each real whole whose ``all`` window and data file are tier B: the
218
+ 12 without crowders and the 2 HnsCyl wholes at ``ac2 nc386``."""
219
+ return request.param
220
+
221
+
222
+ @pytest.fixture(params=_params(lambda whole: whole.window_tier == 'C'))
223
+ def real_crowded_whole(request: pytest.FixtureRequest) -> RealWhole:
224
+ """Each of the 8 crowded wholes whose ``all`` window and data file are
225
+ tier C."""
226
+ return request.param # pragma: no cover - needs data
227
+
228
+
229
+ @pytest.fixture(params=_params(lambda whole: whole.n_foci > 0))
230
+ def real_foci_whole(request: pytest.FixtureRequest) -> RealWhole:
231
+ """Each of the 8 TransFoci wholes."""
232
+ return request.param
233
+
234
+
235
+ @pytest.fixture(params=_params(lambda whole: whole.n_hns > 0))
236
+ def real_hns_whole(request: pytest.FixtureRequest) -> RealWhole:
237
+ """Each of the 10 Hns wholes with H-NS proteins."""
238
+ return request.param
239
+
240
+
241
+ @pytest.fixture(params=_params(
242
+ lambda whole: whole.group == 'nucleoid' and whole.n_hns == 0))
243
+ def real_nh0_whole(request: pytest.FixtureRequest) -> RealWhole:
244
+ """Each of the 4 Hns wholes without H-NS proteins."""
245
+ return request.param
246
+
247
+
248
+ @pytest.fixture(scope='session')
249
+ def study_spec() -> Callable[[str], ModuleType]:
250
+ """Factory: the spec module ``projects/<study>/spec.py`` of a study,
251
+ with its ``SPEC`` and ``PROBE``. The study folders are not an installed
252
+ package, so the repository root is put on the import path, as the
253
+ pipeline's tier C test does."""
254
+ root = str(Path(__file__).resolve().parents[3])
255
+ if root not in sys.path:
256
+ sys.path.insert(0, root)
257
+
258
+ def _load(study: str) -> ModuleType:
259
+ return importlib.import_module(f"projects.{study}.spec")
260
+
261
+ return _load
262
+
263
+
264
+ @dataclass(frozen=True)
265
+ class RealFiles:
266
+ """The files of a real whole; `window` and `data` are None when they
267
+ are not in the repository."""
268
+
269
+ polymer: Path
270
+ topology: Path
271
+ window: Optional[Path]
272
+ data: Optional[Path]
273
+
274
+
275
+ @pytest.fixture
276
+ def real_files() -> Callable[[RealWhole], RealFiles]:
277
+ """Factory: the tier B files of a real whole."""
278
+ root = Path(__file__).resolve().parents[1] / 'data' / 'probe'
279
+
280
+ def _files(whole: RealWhole) -> RealFiles:
281
+ folder = root / whole.whole
282
+ windows = sorted(folder.glob(f"{whole.whole}.j*.ring.all.window."
283
+ "lammpstrj.gz"))
284
+ data = folder / f"{whole.whole}.ring.all.data.gz"
285
+ return RealFiles(
286
+ polymer=folder / (f"{whole.whole}.ring.{whole.group}.last50."
287
+ "lammpstrj.gz"),
288
+ topology=root / whole.topology / (f"{whole.topology}.ring.all."
289
+ "data.gz"),
290
+ window=windows[0] if windows else None,
291
+ data=data if data.exists() else None)
292
+
293
+ return _files
294
+
295
+
296
+ @pytest.fixture
297
+ def private_window_files(
298
+ private_data_dir: Path,
299
+ study_spec: Callable[[str], ModuleType],
300
+ ) -> Callable[[RealWhole], tuple[Path, Path]]:
301
+ """Factory: the tier C ``all`` window and data file of a crowded whole,
302
+ under ``<POLYPHYS_DATA_DIR>/<project>/trjs/<source>/``, with
303
+ ``<project>`` the study's project name (``SPEC.name``, such as
304
+ ``HnsCub``). Use it only in a test marked ``private_data``."""
305
+
306
+ def _files(whole: RealWhole) -> tuple[Path, Path]: # pragma: no cover
307
+ project = study_spec(whole.study).SPEC.name
308
+ folder = private_data_dir / project / 'trjs' / whole.source
309
+ (window,) = sorted(folder.glob(f"{whole.source}.j*.ring.all.window."
310
+ "lammpstrj.gz"))
311
+ return window, folder / f"{whole.source}.ring.all.data.gz"
312
+
313
+ return _files # pragma: no cover - needs data
314
+
315
+
316
+ @pytest.fixture
317
+ def real_universe() -> Callable[[Path, Path], Any]:
318
+ """Factory: a data file and dump opened with
319
+ :func:`polyphys.probe.make_universe`, without a time step: the tests
320
+ count frames, not time, so the reader's warning about the missing
321
+ ``dt`` is silenced."""
322
+
323
+ def _open(topology: Path, trajectory: Path) -> Any:
324
+ from polyphys.probe import make_universe
325
+
326
+ with warnings.catch_warnings():
327
+ warnings.filterwarnings(
328
+ 'ignore', 'Reader has no dt information', UserWarning)
329
+ return make_universe(topology, trajectory)
330
+
331
+ return _open
332
+
333
+
334
+ @pytest.fixture
335
+ def trajectory_arrays() -> Callable[[Any], Mapping[str, np.ndarray]]:
336
+ """Factory: every frame of a universe in one pass: ``'positions'``,
337
+ shape ``(n_frames, n_atoms, 3)``, in the reader's frame;
338
+ ``'boxes'``, shape ``(n_frames, 3)``, the box lengths; and
339
+ ``'steps'``, shape ``(n_frames,)``, the LAMMPS time steps."""
340
+
341
+ def _arrays(universe: Any) -> Mapping[str, np.ndarray]:
342
+ frames = [(ts.positions.copy(), ts.dimensions[:3].copy(),
343
+ ts.data['step']) for ts in universe.trajectory]
344
+ return {'positions': np.stack([frame[0] for frame in frames]),
345
+ 'boxes': np.stack([frame[1] for frame in frames]),
346
+ 'steps': np.array([frame[2] for frame in frames])}
347
+
348
+ return _arrays
349
+
350
+
351
+ @pytest.fixture
352
+ def reference_distances() -> Callable[..., np.ndarray]:
353
+ """Factory: the distances between the rows of `first` and `second`,
354
+ shapes ``(n_frames, n, 3)`` and ``(n_frames, m, 3)``, frame by frame
355
+ with :func:`MDAnalysis.lib.distances.distance_array` and the minimum
356
+ image along the geometry's periodic directions: all three in a cube;
357
+ only z in a cylinder, whose box lengths across the axis are replaced by
358
+ 1e4, far beyond any separation in it, so that only z is imaged.
359
+ Returns shape ``(n_frames, n, m)``."""
360
+
361
+ def _distances(first: np.ndarray, second: np.ndarray, boxes: np.ndarray,
362
+ geometry: str) -> np.ndarray:
363
+ from MDAnalysis.lib.distances import distance_array
364
+
365
+ lengths = np.array(boxes, dtype=np.float64)
366
+ if geometry == 'cylindrical':
367
+ lengths[:, :2] = 1e4
368
+ return np.stack([
369
+ distance_array(a, b, box=np.concatenate([box, [90.0] * 3]))
370
+ for a, b, box in zip(first, second, lengths)])
371
+
372
+ return _distances
373
+
374
+
375
+ @pytest.fixture(params=_params(lambda whole: whole.study == 'trans_foci_cub'))
376
+ def real_transfocicub_whole(request: pytest.FixtureRequest) -> RealWhole:
377
+ """Each of the 4 TransFociCub wholes: the research code probed the same
378
+ 50 frames of their polymer dumps, and that output, tier C, is the
379
+ reference of the pipeline tests."""
380
+ return request.param # pragma: no cover - needs data
381
+
382
+
383
+ @pytest.fixture
384
+ def research_probe_file(
385
+ private_data_dir: Path,
386
+ study_spec: Callable[[str], ModuleType],
387
+ ) -> Callable[..., Path]:
388
+ """Factory: the file of one property in the research code's probe output
389
+ of a real whole,
390
+ ``<POLYPHYS_DATA_DIR>/<project>/probes/<space>-<group>-probe/<whole>.ring/``
391
+ ``<whole>.ring-<group>-<property>.<ext>``, with ``<project>`` the
392
+ study's ``SPEC.name``; the research probe named a whole with its
393
+ topology suffix. The test is skipped when the study has no single such
394
+ probe folder. Use it only in a test marked ``private_data``."""
395
+
396
+ def _file(whole: RealWhole, prop: str, # pragma: no cover - needs data
397
+ ext: str = 'npy') -> Path:
398
+ project = study_spec(whole.study).SPEC.name
399
+ probes = private_data_dir / project / 'probes'
400
+ folders = sorted(probes.glob(f"*-{whole.group}-probe"))
401
+ if len(folders) != 1:
402
+ pytest.skip(f"no single research {whole.group} probe folder "
403
+ f"under {probes}")
404
+ return (folders[0] / f"{whole.whole}.ring"
405
+ / f"{whole.whole}.ring-{whole.group}-{prop}.{ext}")
406
+
407
+ return _file # pragma: no cover - needs data