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,772 @@
|
|
|
1
|
+
"""Tests for :mod:`polyphys.spec`."""
|
|
2
|
+
import dataclasses
|
|
3
|
+
import importlib
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from polyphys.manage.parser import (
|
|
12
|
+
KeywordSpec,
|
|
13
|
+
ParserBase,
|
|
14
|
+
Species,
|
|
15
|
+
)
|
|
16
|
+
from polyphys.tests.synthetic import ChainCyl
|
|
17
|
+
from polyphys.analyze import pipeline
|
|
18
|
+
from polyphys.analyze.pipeline import (
|
|
19
|
+
Autocorrelation,
|
|
20
|
+
FociPairDistances,
|
|
21
|
+
LocalDensities,
|
|
22
|
+
TimeAverage,
|
|
23
|
+
)
|
|
24
|
+
from polyphys.spec import (
|
|
25
|
+
AnalysisTable,
|
|
26
|
+
BinderSpec,
|
|
27
|
+
PerWhole,
|
|
28
|
+
ProbeSpec,
|
|
29
|
+
ProjectSpec,
|
|
30
|
+
PropertySpec,
|
|
31
|
+
edge,
|
|
32
|
+
histogram,
|
|
33
|
+
histogram_2d,
|
|
34
|
+
matrix_tseries,
|
|
35
|
+
tseries,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def make_spec() -> Callable[..., ProjectSpec]:
|
|
41
|
+
"""Factory: a valid ChainCyl spec, with any field overridden."""
|
|
42
|
+
|
|
43
|
+
def _make(**overrides: Any) -> ProjectSpec:
|
|
44
|
+
fields: dict[str, Any] = {
|
|
45
|
+
'parser': ChainCyl,
|
|
46
|
+
'group': 'bug',
|
|
47
|
+
'divisor': 0.025,
|
|
48
|
+
'crowder_attr': 'phi_bulk_c',
|
|
49
|
+
'physical_attrs': ('space', 'nmon', 'dcyl'),
|
|
50
|
+
'species_sizes': {'Mon': 'dmon', 'Crd': 'dcrowd'},
|
|
51
|
+
'tseries_properties': ('gyrTMon',),
|
|
52
|
+
}
|
|
53
|
+
fields.update(overrides)
|
|
54
|
+
return ProjectSpec(**fields)
|
|
55
|
+
|
|
56
|
+
return _make
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.fixture
|
|
60
|
+
def chain_cub() -> type[ParserBase]:
|
|
61
|
+
"""A parser for a chain in a cube, declared outside PolyPhys."""
|
|
62
|
+
return type('ChainCub', (ParserBase,), {
|
|
63
|
+
'_geometry': 'cubic',
|
|
64
|
+
'_topology': 'linear',
|
|
65
|
+
'_groups': ['bug', 'all'],
|
|
66
|
+
'_dump_keywords': {'bug': 'bdump', 'all': 'adump'},
|
|
67
|
+
'_keywords': {
|
|
68
|
+
'N': KeywordSpec('nmon', int),
|
|
69
|
+
'ac': KeywordSpec('dcrowd', float),
|
|
70
|
+
'nc': KeywordSpec('ncrowd', int),
|
|
71
|
+
'l': KeywordSpec('lcube', float, scale=2.0),
|
|
72
|
+
'ens': KeywordSpec('ensemble_id', int),
|
|
73
|
+
'j': KeywordSpec('segment_id', int),
|
|
74
|
+
},
|
|
75
|
+
'_patterns': {
|
|
76
|
+
'segment': ('N', 'ac', 'nc', 'l', 'ens', 'j'),
|
|
77
|
+
'whole': ('N', 'ac', 'nc', 'l', 'ens'),
|
|
78
|
+
'ensemble_long': ('N', 'ac', 'nc', 'l'),
|
|
79
|
+
'ensemble': ('N', 'ac', 'nc'),
|
|
80
|
+
'space': ('N', 'ac'),
|
|
81
|
+
},
|
|
82
|
+
'_defaults': {'dmon': 1.0, 'bdump': 1000, 'adump': 5000},
|
|
83
|
+
'_species': (Species('m', 'nmon', 'dmon'),
|
|
84
|
+
Species('c', 'ncrowd', 'dcrowd')),
|
|
85
|
+
'_project_attributes': {
|
|
86
|
+
'segment': ['dmon'],
|
|
87
|
+
'whole': ['dmon'],
|
|
88
|
+
'ensemble_long': ['dmon'],
|
|
89
|
+
'ensemble': ['dmon'],
|
|
90
|
+
'space': ['dmon'],
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# The module
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_spec_imports_without_mdanalysis():
|
|
99
|
+
"""polyphys.spec imports, and builds a spec, where MDAnalysis cannot be
|
|
100
|
+
imported."""
|
|
101
|
+
script = (
|
|
102
|
+
"import sys\n"
|
|
103
|
+
"sys.modules['MDAnalysis'] = None\n"
|
|
104
|
+
"from polyphys.tests.synthetic import ChainCyl\n"
|
|
105
|
+
"from polyphys.spec import ProbeSpec, ProjectSpec\n"
|
|
106
|
+
"spec = ProjectSpec(ChainCyl, 'bug', 0.025, (), {}, ('gyrTMon',),\n"
|
|
107
|
+
" crowder_attr='phi_bulk_c')\n"
|
|
108
|
+
"ProbeSpec(spec, {'Mon': 'resid 1'}, 'Mon', ('gyrT',), False)\n"
|
|
109
|
+
"print('polyphys.probe' in sys.modules)\n")
|
|
110
|
+
|
|
111
|
+
result = subprocess.run([sys.executable, '-c', script],
|
|
112
|
+
capture_output=True, text=True, check=False)
|
|
113
|
+
|
|
114
|
+
assert (result.returncode, result.stdout.strip()) == (0, 'False'), \
|
|
115
|
+
result.stderr
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_per_whole_is_defined_once():
|
|
119
|
+
"""The pipeline's PerWhole is the spec module's alias, not a copy."""
|
|
120
|
+
assert pipeline.PerWhole is PerWhole
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_manage_projects_alias_is_removed():
|
|
124
|
+
"""The 0.9.0 alias module polyphys.manage.projects is gone in 1.0.0."""
|
|
125
|
+
with pytest.raises(ModuleNotFoundError):
|
|
126
|
+
importlib.import_module('polyphys.manage.projects')
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@pytest.mark.parametrize('name', [
|
|
130
|
+
'AnalysisTable', 'PropertySpec', 'PostHook', 'tseries', 'histogram',
|
|
131
|
+
'histogram_2d', 'edge', 'matrix_tseries',
|
|
132
|
+
])
|
|
133
|
+
def test_pipeline_no_longer_serves_names_moved_to_spec(name):
|
|
134
|
+
"""A table record or builder that moved to polyphys.spec in 0.9.0 is no
|
|
135
|
+
longer an attribute of polyphys.analyze.pipeline in 1.0.0."""
|
|
136
|
+
assert not hasattr(pipeline, name)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ProjectSpec
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_spec_is_frozen(make_spec):
|
|
143
|
+
"""A spec's fields cannot be reassigned."""
|
|
144
|
+
spec = make_spec()
|
|
145
|
+
|
|
146
|
+
with pytest.raises(dataclasses.FrozenInstanceError):
|
|
147
|
+
spec.divisor = 0.5 # type: ignore[misc]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_spec_copies_its_containers(make_spec):
|
|
151
|
+
"""Changing the caller's list or dict later leaves a spec unchanged."""
|
|
152
|
+
attrs = ['space', 'nmon']
|
|
153
|
+
sizes = {'Mon': 'dmon'}
|
|
154
|
+
spec = make_spec(physical_attrs=attrs, species_sizes=sizes)
|
|
155
|
+
|
|
156
|
+
attrs.append('dcyl')
|
|
157
|
+
sizes['Crd'] = 'dcrowd'
|
|
158
|
+
|
|
159
|
+
assert (spec.physical_attrs, dict(spec.species_sizes)) == (
|
|
160
|
+
('space', 'nmon'), {'Mon': 'dmon'})
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_spec_species_sizes_are_read_only(make_spec):
|
|
164
|
+
"""A spec's species-to-size map cannot be changed."""
|
|
165
|
+
spec = make_spec()
|
|
166
|
+
|
|
167
|
+
with pytest.raises(TypeError):
|
|
168
|
+
spec.species_sizes['Crd'] = 'dmon' # type: ignore[index]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@pytest.mark.parametrize('overrides, match', [
|
|
172
|
+
pytest.param({'physical_attrs': ('nmon', 'nhns')},
|
|
173
|
+
r"ChainCyl does not set \['nhns'\] at 'ensemble_long'",
|
|
174
|
+
id='unknown-physical-attribute'),
|
|
175
|
+
pytest.param({'species_sizes': {'Hns': 'dhns'}},
|
|
176
|
+
r"does not set \['dhns'\]", id='unknown-size-attribute'),
|
|
177
|
+
pytest.param({'group': 'nucleoid'}, "'group' must be the polymer group",
|
|
178
|
+
id='group-not-in-parser'),
|
|
179
|
+
pytest.param({'group': 'all'}, "'group' must be the polymer group",
|
|
180
|
+
id='group-all'),
|
|
181
|
+
pytest.param({'divisor': 0.0}, "'divisor' must be a finite positive",
|
|
182
|
+
id='zero-divisor'),
|
|
183
|
+
pytest.param({'divisor': -0.025}, "'divisor' must be a finite positive",
|
|
184
|
+
id='negative-divisor'),
|
|
185
|
+
pytest.param({'divisor': float('nan')},
|
|
186
|
+
"'divisor' must be a finite positive", id='nan-divisor'),
|
|
187
|
+
pytest.param({'crowder_attr': 'phi_c'}, r"does not set \['phi_c'\]",
|
|
188
|
+
id='unknown-crowder-attribute'),
|
|
189
|
+
pytest.param({'crowder_attr': ''}, "'crowder_attr' must be a non-empty",
|
|
190
|
+
id='empty-crowder-attribute'),
|
|
191
|
+
])
|
|
192
|
+
def test_spec_rejects_invalid_fields(make_spec, overrides, match):
|
|
193
|
+
"""A spec with an unknown attribute, group, crowder attribute, or bad
|
|
194
|
+
divisor raises."""
|
|
195
|
+
with pytest.raises(ValueError, match=match):
|
|
196
|
+
make_spec(**overrides)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_spec_requires_the_crowder_attribute():
|
|
200
|
+
"""A spec names its crowder attribute; the library supplies none."""
|
|
201
|
+
with pytest.raises(TypeError, match='crowder_attr'):
|
|
202
|
+
ProjectSpec(parser=ChainCyl, group='bug', divisor=0.025,
|
|
203
|
+
physical_attrs=(), species_sizes={},
|
|
204
|
+
tseries_properties=())
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@pytest.mark.parametrize('parser', [
|
|
208
|
+
pytest.param(ParserBase, id='abstract-parser'),
|
|
209
|
+
pytest.param(str, id='not-a-parser'),
|
|
210
|
+
])
|
|
211
|
+
def test_spec_rejects_non_concrete_parser(make_spec, parser):
|
|
212
|
+
"""A spec needs a concrete ParserBase subclass."""
|
|
213
|
+
with pytest.raises(TypeError, match='concrete ParserBase subclass'):
|
|
214
|
+
make_spec(parser=parser)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_spec_rejects_parser_without_crowder_fraction(make_spec, chain_cub):
|
|
218
|
+
"""A spec needs a parser that sets phi_bulk_c at ensemble_long."""
|
|
219
|
+
no_crowders = type('ChainOnly', (chain_cub,), {
|
|
220
|
+
'_species': (Species('m', 'nmon', 'dmon'),)})
|
|
221
|
+
|
|
222
|
+
with pytest.raises(ValueError, match=r"does not set \['phi_bulk_c'\]"):
|
|
223
|
+
make_spec(parser=no_crowders, physical_attrs=('nmon',),
|
|
224
|
+
species_sizes={'Mon': 'dmon'})
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_spec_for_parser_declared_outside_polyphys(chain_cub):
|
|
228
|
+
"""A study's own parser gets a valid spec with patterns from its names."""
|
|
229
|
+
spec = ProjectSpec(
|
|
230
|
+
parser=chain_cub, group='bug', divisor=0.025,
|
|
231
|
+
crowder_attr='phi_bulk_c',
|
|
232
|
+
physical_attrs=('space', 'ensemble_long', 'nmon', 'dcrowd'),
|
|
233
|
+
species_sizes={'Mon': 'dmon', 'Crd': 'dcrowd'},
|
|
234
|
+
tseries_properties=('gyrTMon',))
|
|
235
|
+
|
|
236
|
+
# From the declaration: cubic; 'space' is ('N', 'ac'); 'ensemble_long'
|
|
237
|
+
# starts with 'N'.
|
|
238
|
+
assert (spec.name, spec.geometry, spec.topology, spec.directions,
|
|
239
|
+
spec.space_pattern, spec.space_hierarchy, spec.hierarchy) == (
|
|
240
|
+
'ChainCub', 'cubic', 'linear', ('r',), 'N*ac*', 'N*', 'N*')
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ProbeSpec
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@pytest.fixture
|
|
247
|
+
def make_probe(make_spec) -> Callable[..., ProbeSpec]:
|
|
248
|
+
"""Factory: a valid probe spec of the ChainCyl spec, with any field
|
|
249
|
+
overridden; the project lists the chain-size time series."""
|
|
250
|
+
|
|
251
|
+
def _make(**overrides: Any) -> ProbeSpec:
|
|
252
|
+
fields: dict[str, Any] = {
|
|
253
|
+
'project': make_spec(tseries_properties=(
|
|
254
|
+
'fsdTMon', 'gyrTMon', 'rfloryTMon', 'asphericityTMon')),
|
|
255
|
+
'selections': {'Mon': 'resid 1', 'Crd': 'resid 0'},
|
|
256
|
+
'chain': 'Mon',
|
|
257
|
+
'observables': ('rfloryT', 'fsdT', 'gyrT', 'asphericityT'),
|
|
258
|
+
'unwrap': False,
|
|
259
|
+
}
|
|
260
|
+
fields.update(overrides)
|
|
261
|
+
return ProbeSpec(**fields)
|
|
262
|
+
|
|
263
|
+
return _make
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_probe_reads_parser_and_geometry_from_its_project(make_probe):
|
|
267
|
+
"""A probe spec's name, parser, and geometry are its project's."""
|
|
268
|
+
probe = make_probe()
|
|
269
|
+
|
|
270
|
+
assert (probe.name, probe.parser, probe.geometry) == (
|
|
271
|
+
'ChainCyl', ChainCyl, 'cylindrical')
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def test_probe_is_frozen(make_probe):
|
|
275
|
+
"""A probe spec's fields cannot be reassigned."""
|
|
276
|
+
probe = make_probe()
|
|
277
|
+
|
|
278
|
+
with pytest.raises(dataclasses.FrozenInstanceError):
|
|
279
|
+
probe.unwrap = True # type: ignore[misc]
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def test_probe_copies_its_containers(make_probe):
|
|
283
|
+
"""Changing the caller's dict or list later leaves a probe spec
|
|
284
|
+
unchanged, and its maps are read-only."""
|
|
285
|
+
selections = {'Mon': 'resid 1'}
|
|
286
|
+
observables = ['gyrT']
|
|
287
|
+
bin_sizes = {'r': 0.1}
|
|
288
|
+
probe = make_probe(selections=selections, observables=observables,
|
|
289
|
+
bin_sizes=bin_sizes)
|
|
290
|
+
|
|
291
|
+
selections['Crd'] = 'resid 0'
|
|
292
|
+
observables.append('fsdT')
|
|
293
|
+
bin_sizes['z'] = 0.5
|
|
294
|
+
|
|
295
|
+
assert (dict(probe.selections), probe.observables,
|
|
296
|
+
dict(probe.bin_sizes)) == (
|
|
297
|
+
{'Mon': 'resid 1'}, ('gyrT',), {'r': 0.1})
|
|
298
|
+
with pytest.raises(TypeError):
|
|
299
|
+
probe.selections['Crd'] = 'resid 0' # type: ignore[index]
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def test_probe_allows_the_matrix_observable_without_a_time_series(
|
|
303
|
+
make_probe):
|
|
304
|
+
"""principalT, a per-frame matrix, needs no entry in the project's
|
|
305
|
+
tseries_properties."""
|
|
306
|
+
probe = make_probe(observables=('gyrT', 'principalT'))
|
|
307
|
+
|
|
308
|
+
assert probe.observables == ('gyrT', 'principalT')
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@pytest.mark.parametrize('overrides, match', [
|
|
312
|
+
pytest.param({'selections': {}}, "'selections' must map",
|
|
313
|
+
id='no-selections'),
|
|
314
|
+
pytest.param({'selections': {'Mon': ''}}, "'selections' must map",
|
|
315
|
+
id='empty-selection'),
|
|
316
|
+
pytest.param({'selections': {'': 'resid 1'}}, "'selections' must map",
|
|
317
|
+
id='empty-label'),
|
|
318
|
+
pytest.param({'chain': 'Foci'}, "'chain' must be a label in",
|
|
319
|
+
id='chain-not-selected'),
|
|
320
|
+
pytest.param({'observables': ()}, "'observables' must not be empty",
|
|
321
|
+
id='no-observables'),
|
|
322
|
+
pytest.param({'observables': ('gyrT', 'radius')},
|
|
323
|
+
r"names \['radius'\], which are not in",
|
|
324
|
+
id='unknown-observable'),
|
|
325
|
+
pytest.param({'observables': ('gyrT', 'gyrT')}, "repeats a name",
|
|
326
|
+
id='repeated-observable'),
|
|
327
|
+
pytest.param({'observables': ('gyrT', 'shapeT')},
|
|
328
|
+
r"names \['shapeTMon'\], which are not among",
|
|
329
|
+
id='observable-not-a-time-series'),
|
|
330
|
+
pytest.param({'bin_sizes': {'phi': 0.1}}, "has the direction 'phi'",
|
|
331
|
+
id='unknown-direction'),
|
|
332
|
+
pytest.param({'bin_sizes': {'r': 0.0}}, "finite positive number",
|
|
333
|
+
id='zero-bin-size'),
|
|
334
|
+
pytest.param({'bin_sizes': {'r': float('inf')}},
|
|
335
|
+
"finite positive number", id='infinite-bin-size'),
|
|
336
|
+
pytest.param({'bin_sizes': {'r': '0.1'}}, "finite positive number",
|
|
337
|
+
id='string-bin-size'),
|
|
338
|
+
pytest.param({'bin_sizes': {'r': True}}, "finite positive number",
|
|
339
|
+
id='bool-bin-size'),
|
|
340
|
+
pytest.param({'histogram_species': ('Foci',), 'bin_sizes': {'r': 0.1},
|
|
341
|
+
'block_frames': 100},
|
|
342
|
+
r"'histogram_species' names \['Foci'\]",
|
|
343
|
+
id='histogram-species-not-selected'),
|
|
344
|
+
pytest.param({'histogram_species': ('Mon', 'Mon'),
|
|
345
|
+
'bin_sizes': {'r': 0.1}, 'block_frames': 100},
|
|
346
|
+
"'histogram_species' repeats a label",
|
|
347
|
+
id='repeated-histogram-species'),
|
|
348
|
+
pytest.param({'histogram_species': ('Mon',), 'block_frames': 100},
|
|
349
|
+
"'histogram_species' needs 'bin_sizes'",
|
|
350
|
+
id='histogram-species-without-bin-sizes'),
|
|
351
|
+
pytest.param({'histogram_species': ('Mon',), 'bin_sizes': {'r': 0.1}},
|
|
352
|
+
"'histogram_species' needs 'block_frames'",
|
|
353
|
+
id='histogram-species-without-block-frames'),
|
|
354
|
+
pytest.param({'block_frames': 0}, "'block_frames' must be a positive",
|
|
355
|
+
id='zero-block-frames'),
|
|
356
|
+
pytest.param({'block_frames': 2.0}, "'block_frames' must be a positive",
|
|
357
|
+
id='float-block-frames'),
|
|
358
|
+
pytest.param({'recenter': 'center'}, "'recenter' must be",
|
|
359
|
+
id='unknown-recenter'),
|
|
360
|
+
pytest.param({'recenter': (1.0, 2.0)}, "'recenter' must be",
|
|
361
|
+
id='short-recenter'),
|
|
362
|
+
pytest.param({'recenter': (1.0, 2.0, float('nan'))}, "'recenter' must be",
|
|
363
|
+
id='nan-recenter'),
|
|
364
|
+
pytest.param({'pair_species': 'Foci', 'contact_cutoff': 1.5},
|
|
365
|
+
"'pair_species' must be a label in",
|
|
366
|
+
id='pair-species-not-selected'),
|
|
367
|
+
pytest.param({'pair_species': 'Mon'},
|
|
368
|
+
"'pair_species' needs 'contact_cutoff'",
|
|
369
|
+
id='pair-species-without-cutoff'),
|
|
370
|
+
pytest.param({'contact_cutoff': 0.0}, "'contact_cutoff' must be",
|
|
371
|
+
id='zero-contact-cutoff'),
|
|
372
|
+
])
|
|
373
|
+
def test_probe_rejects_invalid_fields(make_probe, overrides, match):
|
|
374
|
+
"""A probe spec with a bad selection, chain, observable, bin size,
|
|
375
|
+
histogram species, block length, recentering, pair species, or contact
|
|
376
|
+
cutoff raises, naming the project."""
|
|
377
|
+
with pytest.raises(ValueError, match=match):
|
|
378
|
+
make_probe(**overrides)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def test_probe_rejects_a_non_project(make_probe):
|
|
382
|
+
"""A probe spec needs a ProjectSpec."""
|
|
383
|
+
with pytest.raises(TypeError, match="'project' must be a ProjectSpec"):
|
|
384
|
+
make_probe(project=ChainCyl)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def test_probe_accepts_positive_bin_sizes(make_probe):
|
|
388
|
+
"""Finite positive bin sizes along known directions are kept in order."""
|
|
389
|
+
probe = make_probe(bin_sizes={'r': 0.1, 'z': 0.5, 'theta': 0.0873})
|
|
390
|
+
|
|
391
|
+
assert list(probe.bin_sizes.items()) == [
|
|
392
|
+
('r', 0.1), ('z', 0.5), ('theta', 0.0873)]
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def test_probe_accepts_numpy_scalar_bin_sizes(make_probe):
|
|
396
|
+
"""A NumPy integer or float bin size is a finite positive number."""
|
|
397
|
+
probe = make_probe(bin_sizes={'r': np.float32(0.5), 'z': np.int64(2)})
|
|
398
|
+
|
|
399
|
+
assert list(probe.bin_sizes.items()) == [('r', 0.5), ('z', 2)]
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def test_probe_defaults_leave_histograms_and_pairs_unprobed(make_probe):
|
|
403
|
+
"""Without the histogram and pair fields, a probe spec recenters by half
|
|
404
|
+
the box, uses the minimum image, and names no species for them."""
|
|
405
|
+
probe = make_probe()
|
|
406
|
+
|
|
407
|
+
assert (probe.histogram_species, probe.block_frames, probe.recenter,
|
|
408
|
+
probe.pair_species, probe.contact_cutoff,
|
|
409
|
+
probe.minimum_image) == ((), None, 'half', None, None, True)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def test_probe_keeps_histogram_and_pair_fields(make_probe, whole):
|
|
413
|
+
"""The histogram species, block length, pair species, contact cutoff,
|
|
414
|
+
and minimum-image flag are stored as given, the species as a tuple, and
|
|
415
|
+
the cutoff resolves for a whole."""
|
|
416
|
+
def cutoff(info: ParserBase) -> float:
|
|
417
|
+
return 1.5
|
|
418
|
+
|
|
419
|
+
probe = make_probe(bin_sizes={'r': 0.1}, histogram_species=['Crd', 'Mon'],
|
|
420
|
+
block_frames=100, pair_species='Mon',
|
|
421
|
+
contact_cutoff=cutoff, minimum_image=False)
|
|
422
|
+
|
|
423
|
+
assert (probe.histogram_species, probe.block_frames, probe.pair_species,
|
|
424
|
+
probe.contact_cutoff, probe.minimum_image) == (
|
|
425
|
+
('Crd', 'Mon'), 100, 'Mon', cutoff, False)
|
|
426
|
+
assert ProbeSpec.resolve(probe.contact_cutoff, whole) == 1.5
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
@pytest.mark.parametrize('given, stored', [
|
|
430
|
+
pytest.param('none', 'none', id='none'),
|
|
431
|
+
pytest.param('half', 'half', id='half'),
|
|
432
|
+
pytest.param([1, 2.5, -3], (1.0, 2.5, -3.0), id='triple-as-floats'),
|
|
433
|
+
])
|
|
434
|
+
def test_probe_stores_recenter(make_probe, given, stored):
|
|
435
|
+
"""A recentering keyword is kept and a triple becomes a tuple of floats."""
|
|
436
|
+
assert make_probe(recenter=given).recenter == stored
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def test_probe_accepts_callable_bin_sizes(make_probe, whole):
|
|
440
|
+
"""A bin size may be a function of the whole's parser, kept as given and
|
|
441
|
+
resolved for a whole: a tenth of its crowder diameter 0.5."""
|
|
442
|
+
def bin_size(info: ParserBase) -> float:
|
|
443
|
+
return 0.1 * float(getattr(info, 'dcrowd'))
|
|
444
|
+
|
|
445
|
+
probe = make_probe(bin_sizes={'r': bin_size, 'z': 0.5})
|
|
446
|
+
|
|
447
|
+
assert probe.bin_sizes['r'] is bin_size
|
|
448
|
+
assert ProbeSpec.resolve(probe.bin_sizes['r'], whole) == 0.05
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@pytest.fixture
|
|
452
|
+
def whole() -> ParserBase:
|
|
453
|
+
"""A ChainCyl whole with dmon 1 and dcrowd 0.5."""
|
|
454
|
+
return ChainCyl(
|
|
455
|
+
'N200r10.5lz50ac0.5nc100dt0.005bdump1adump1ens1',
|
|
456
|
+
'whole', 'all')
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
@pytest.mark.parametrize('value, expected', [
|
|
460
|
+
pytest.param(2, 2.0, id='number-as-float'),
|
|
461
|
+
pytest.param(lambda info: info.dmon + info.dcrowd, 1.5,
|
|
462
|
+
id='callable-of-the-parser'),
|
|
463
|
+
pytest.param(lambda info: np.float32(0.5), 0.5, id='numpy-float32'),
|
|
464
|
+
pytest.param(lambda info: np.int64(2), 2.0, id='numpy-int64'),
|
|
465
|
+
pytest.param(np.float32(0.5), 0.5, id='numpy-float32-constant'),
|
|
466
|
+
pytest.param(np.int64(2), 2.0, id='numpy-int64-constant'),
|
|
467
|
+
])
|
|
468
|
+
def test_probe_resolve_returns_the_number_for_one_whole(whole, value,
|
|
469
|
+
expected):
|
|
470
|
+
"""resolve returns a number as a float and calls a callable with the
|
|
471
|
+
parser."""
|
|
472
|
+
assert ProbeSpec.resolve(value, whole) == expected
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
@pytest.mark.parametrize('bad', [
|
|
476
|
+
pytest.param(lambda info: 0.0, id='zero'),
|
|
477
|
+
pytest.param(lambda info: -1.0, id='negative'),
|
|
478
|
+
pytest.param(lambda info: float('nan'), id='nan'),
|
|
479
|
+
pytest.param(lambda info: '1', id='string'),
|
|
480
|
+
pytest.param(lambda info: True, id='bool'),
|
|
481
|
+
pytest.param(lambda info: np.bool_(True), id='numpy-bool'),
|
|
482
|
+
])
|
|
483
|
+
def test_probe_resolve_rejects_a_bad_callable_result(whole, bad):
|
|
484
|
+
"""resolve raises, naming the whole, when a callable does not return a
|
|
485
|
+
finite positive number."""
|
|
486
|
+
with pytest.raises(ValueError, match=f"{whole.name} must resolve to"):
|
|
487
|
+
ProbeSpec.resolve(bad, whole)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
@pytest.mark.parametrize('bad', [
|
|
491
|
+
pytest.param(0.0, id='zero'),
|
|
492
|
+
pytest.param(-1.0, id='negative'),
|
|
493
|
+
pytest.param(float('nan'), id='nan'),
|
|
494
|
+
pytest.param(float('inf'), id='inf'),
|
|
495
|
+
pytest.param('1.0', id='string'),
|
|
496
|
+
pytest.param(True, id='bool'),
|
|
497
|
+
])
|
|
498
|
+
def test_probe_resolve_rejects_a_bad_constant(whole, bad):
|
|
499
|
+
"""resolve raises, naming the whole, when a constant is not a finite
|
|
500
|
+
positive number."""
|
|
501
|
+
with pytest.raises(ValueError, match=f"{whole.name} must resolve to"):
|
|
502
|
+
ProbeSpec.resolve(bad, whole)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
# BinderSpec
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
@pytest.fixture
|
|
509
|
+
def make_binders() -> Callable[..., BinderSpec]:
|
|
510
|
+
"""Factory: a valid binder spec with the H-NS labels, the published
|
|
511
|
+
cutoff, and the studies' cis threshold, with any field overridden."""
|
|
512
|
+
|
|
513
|
+
def _make(**overrides: Any) -> BinderSpec:
|
|
514
|
+
fields: dict[str, Any] = {
|
|
515
|
+
'patches': 'HnsPatch',
|
|
516
|
+
'binders': 'HnsCore',
|
|
517
|
+
'cutoff': 0.685,
|
|
518
|
+
'cis_threshold': 3,
|
|
519
|
+
}
|
|
520
|
+
fields.update(overrides)
|
|
521
|
+
return BinderSpec(**fields)
|
|
522
|
+
|
|
523
|
+
return _make
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
@pytest.fixture
|
|
527
|
+
def make_hns_probe(make_spec, make_binders) -> Callable[..., ProbeSpec]:
|
|
528
|
+
"""Factory: a probe spec whose project lists the eight binding series
|
|
529
|
+
and whose selections include the patches, with any field overridden."""
|
|
530
|
+
|
|
531
|
+
def _make(**overrides: Any) -> ProbeSpec:
|
|
532
|
+
fields: dict[str, Any] = {
|
|
533
|
+
'project': make_spec(tseries_properties=(
|
|
534
|
+
'gyrTMon', 'nBoundTHnsPatch', 'nFreeTHnsPatch',
|
|
535
|
+
'nEngagedTHnsPatch', 'nFreeTHnsCore', 'nBridgeTHnsCore',
|
|
536
|
+
'nDangleTHnsCore', 'nCisTHnsCore', 'nTransTHnsCore')),
|
|
537
|
+
'selections': {'Mon': 'resid 1', 'HnsPatch': 'type 2'},
|
|
538
|
+
'chain': 'Mon',
|
|
539
|
+
'observables': ('gyrT',),
|
|
540
|
+
'unwrap': False,
|
|
541
|
+
'binders': make_binders(),
|
|
542
|
+
}
|
|
543
|
+
fields.update(overrides)
|
|
544
|
+
return ProbeSpec(**fields)
|
|
545
|
+
|
|
546
|
+
return _make
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def test_binders_series_take_the_patch_and_binder_labels(make_binders):
|
|
550
|
+
"""The three patch counts are named by the patch label and the five
|
|
551
|
+
binder counts by the binder label, in the research code's order."""
|
|
552
|
+
binders = make_binders(patches='Site', binders='Prot')
|
|
553
|
+
|
|
554
|
+
assert list(binders.series.items()) == [
|
|
555
|
+
('n_patch_monomer_contacts', 'nBoundTSite'),
|
|
556
|
+
('n_patches_free', 'nFreeTSite'),
|
|
557
|
+
('n_patches_engaged', 'nEngagedTSite'),
|
|
558
|
+
('n_binders_free', 'nFreeTProt'),
|
|
559
|
+
('n_binders_bridging', 'nBridgeTProt'),
|
|
560
|
+
('n_binders_dangling', 'nDangleTProt'),
|
|
561
|
+
('n_pairs_cis', 'nCisTProt'),
|
|
562
|
+
('n_pairs_trans', 'nTransTProt')]
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def test_binders_defaults(make_binders):
|
|
566
|
+
"""A binder spec defaults to an inclusive comparison, two patches per
|
|
567
|
+
binder, and no stored distances."""
|
|
568
|
+
binders = make_binders()
|
|
569
|
+
|
|
570
|
+
assert (binders.inclusive, binders.patches_per_binder,
|
|
571
|
+
binders.store_distances) == (True, 2, False)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def test_binders_requires_cis_threshold():
|
|
575
|
+
"""A binder spec has no default cis threshold: a study must state it."""
|
|
576
|
+
with pytest.raises(TypeError, match="cis_threshold"):
|
|
577
|
+
BinderSpec('HnsPatch', 'HnsCore', cutoff=0.685)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def test_binders_cis_threshold_is_keyword_only():
|
|
581
|
+
"""A cis threshold given by position is not accepted as one."""
|
|
582
|
+
with pytest.raises(TypeError, match="positional"):
|
|
583
|
+
BinderSpec('HnsPatch', 'HnsCore', 0.685, True, 2, False, 3)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def test_binders_is_frozen(make_binders):
|
|
587
|
+
"""A binder spec's fields cannot be reassigned."""
|
|
588
|
+
binders = make_binders()
|
|
589
|
+
|
|
590
|
+
with pytest.raises(dataclasses.FrozenInstanceError):
|
|
591
|
+
binders.cutoff = 0.661 # type: ignore[misc]
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
@pytest.mark.parametrize('overrides, match', [
|
|
595
|
+
pytest.param({'patches': ''}, "'patches' must be a non-empty",
|
|
596
|
+
id='empty-patches'),
|
|
597
|
+
pytest.param({'binders': ''}, "'binders' must be a non-empty",
|
|
598
|
+
id='empty-binders'),
|
|
599
|
+
pytest.param({'binders': 'HnsPatch'}, "must be different labels",
|
|
600
|
+
id='same-labels'),
|
|
601
|
+
pytest.param({'cutoff': 0.0}, "'cutoff' must be a finite positive",
|
|
602
|
+
id='zero-cutoff'),
|
|
603
|
+
pytest.param({'cutoff': -0.685}, "'cutoff' must be a finite positive",
|
|
604
|
+
id='negative-cutoff'),
|
|
605
|
+
pytest.param({'cutoff': float('inf')},
|
|
606
|
+
"'cutoff' must be a finite positive", id='infinite-cutoff'),
|
|
607
|
+
pytest.param({'cis_threshold': -1}, "'cis_threshold' must be",
|
|
608
|
+
id='negative-cis'),
|
|
609
|
+
pytest.param({'cis_threshold': 2.5}, "'cis_threshold' must be",
|
|
610
|
+
id='fractional-cis'),
|
|
611
|
+
pytest.param({'patches_per_binder': 3}, "'patches_per_binder' must be 2",
|
|
612
|
+
id='three-patches'),
|
|
613
|
+
])
|
|
614
|
+
def test_binders_rejects_invalid_fields(make_binders, overrides, match):
|
|
615
|
+
"""An empty or repeated label, a non-positive or infinite cutoff, a bad
|
|
616
|
+
cis threshold, or a valence other than two raises."""
|
|
617
|
+
with pytest.raises(ValueError, match=match):
|
|
618
|
+
make_binders(**overrides)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def test_probe_has_no_binders_by_default(make_probe):
|
|
622
|
+
"""A probe spec without a binder spec has binders None."""
|
|
623
|
+
assert make_probe().binders is None
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def test_probe_carries_its_binder_spec(make_hns_probe, make_binders):
|
|
627
|
+
"""A probe spec keeps the binder spec whose patches it selects and
|
|
628
|
+
whose series its project lists."""
|
|
629
|
+
binders = make_binders(store_distances=True)
|
|
630
|
+
|
|
631
|
+
probe = make_hns_probe(binders=binders)
|
|
632
|
+
|
|
633
|
+
assert probe.binders == binders
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
@pytest.mark.parametrize('overrides, match', [
|
|
637
|
+
pytest.param({'selections': {'Mon': 'resid 1'}},
|
|
638
|
+
"patches of 'binders' must be a label in",
|
|
639
|
+
id='patches-not-selected'),
|
|
640
|
+
pytest.param({'binders': BinderSpec('Mon', 'HnsCore', cutoff=0.685,
|
|
641
|
+
cis_threshold=3)},
|
|
642
|
+
"patches of 'binders' must be a label in",
|
|
643
|
+
id='patches-are-the-chain'),
|
|
644
|
+
])
|
|
645
|
+
def test_probe_rejects_binders_outside_its_selections(make_hns_probe,
|
|
646
|
+
overrides, match):
|
|
647
|
+
"""The patches of the binder spec must be a selected label other than
|
|
648
|
+
the chain."""
|
|
649
|
+
with pytest.raises(ValueError, match=match):
|
|
650
|
+
make_hns_probe(**overrides)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def test_probe_rejects_binder_series_the_project_lacks(make_hns_probe,
|
|
654
|
+
make_spec):
|
|
655
|
+
"""Each of the eight binding series must be one of the project's
|
|
656
|
+
tseries_properties."""
|
|
657
|
+
project = make_spec(tseries_properties=(
|
|
658
|
+
'gyrTMon', 'nBoundTHnsPatch', 'nFreeTHnsPatch', 'nEngagedTHnsPatch',
|
|
659
|
+
'nFreeTHnsCore', 'nBridgeTHnsCore', 'nDangleTHnsCore'))
|
|
660
|
+
|
|
661
|
+
match = r"names the series \['nCisTHnsCore', 'nTransTHnsCore'\]"
|
|
662
|
+
with pytest.raises(ValueError, match=match):
|
|
663
|
+
make_hns_probe(project=project)
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def test_probe_rejects_a_non_binder_spec(make_hns_probe):
|
|
667
|
+
"""binders must be a BinderSpec or None."""
|
|
668
|
+
with pytest.raises(TypeError, match="'binders' must be a BinderSpec"):
|
|
669
|
+
make_hns_probe(binders={'patches': 'HnsPatch'})
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
# PropertySpec and AnalysisTable
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
@pytest.mark.parametrize("kwargs, match", [
|
|
676
|
+
pytest.param(dict(name="", species="Mon", relation="tseries",
|
|
677
|
+
whole_type="vector"), "must not be empty",
|
|
678
|
+
id="empty-name"),
|
|
679
|
+
pytest.param(dict(name="gyrT", species="Mon", relation="series",
|
|
680
|
+
whole_type="vector"), "relation must be",
|
|
681
|
+
id="unknown-relation"),
|
|
682
|
+
pytest.param(dict(name="gyrT", species="Mon", relation="tseries",
|
|
683
|
+
whole_type="frame"), "whole_type must be",
|
|
684
|
+
id="unknown-whole-type"),
|
|
685
|
+
pytest.param(dict(name="xEdge", species="", relation="bin_edge",
|
|
686
|
+
whole_type="vector"), "do not match",
|
|
687
|
+
id="edge-relation-vector-whole"),
|
|
688
|
+
pytest.param(dict(name="xEdge", species="", relation="tseries",
|
|
689
|
+
whole_type="bin_edge"), "do not match",
|
|
690
|
+
id="tseries-relation-edge-whole"),
|
|
691
|
+
pytest.param(dict(name="gyrT", species="Mon", relation="tseries",
|
|
692
|
+
whole_type="vector", edge_prop="gyrEdgeMon"),
|
|
693
|
+
"edge_prop is only valid", id="edges-on-tseries"),
|
|
694
|
+
pytest.param(dict(name="xyHist", species="Crd", relation="histogram",
|
|
695
|
+
whole_type="matrix", edge_prop="xyEdgeCrd"),
|
|
696
|
+
"edge_prop is only valid", id="edges-on-matrix"),
|
|
697
|
+
])
|
|
698
|
+
def test_property_spec_rejects_inconsistent_kinds(kwargs, match):
|
|
699
|
+
"""A property whose relation, shape, and edges disagree is rejected."""
|
|
700
|
+
with pytest.raises(ValueError, match=match):
|
|
701
|
+
PropertySpec(**kwargs)
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def test_constructors_build_the_kinds_the_studies_use():
|
|
705
|
+
"""Each named constructor sets the property, relation, shape, and edges
|
|
706
|
+
of its kind."""
|
|
707
|
+
assert (tseries("gyrT", "Mon").prop, tseries("gyrT", "Mon").relation,
|
|
708
|
+
tseries("gyrT", "Mon").whole_type) == ("gyrTMon", "tseries",
|
|
709
|
+
"vector")
|
|
710
|
+
hist = histogram("z", "Crd")
|
|
711
|
+
assert (hist.prop, hist.relation, hist.edge_prop) == (
|
|
712
|
+
"zHistCrd", "histogram", "zEdgeCrd")
|
|
713
|
+
assert (histogram_2d("xz", "Dna").prop,
|
|
714
|
+
histogram_2d("xz", "Dna").whole_type) == ("xzHistDna", "matrix")
|
|
715
|
+
assert (edge("y").prop, edge("y").relation, edge("y").has_edges) == (
|
|
716
|
+
"yEdge", "bin_edge", False)
|
|
717
|
+
assert matrix_tseries("principalT", "Mon").whole_type == "matrix"
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def test_analysis_table_rejects_bad_lineage_and_repeated_properties():
|
|
721
|
+
"""A table needs a known lineage and lists each artifact once."""
|
|
722
|
+
with pytest.raises(ValueError, match="lineage must be"):
|
|
723
|
+
AnalysisTable("bug", "ensemble", False, ())
|
|
724
|
+
with pytest.raises(ValueError, match=r"more than once: \['gyrTMon'\]"):
|
|
725
|
+
AnalysisTable("bug", "whole", False,
|
|
726
|
+
(tseries("gyrT", "Mon"), tseries("gyrT", "Mon")))
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
@pytest.mark.parametrize("properties, repeated", [
|
|
730
|
+
pytest.param(
|
|
731
|
+
(tseries("gyrT", "Mon", post_hook=TimeAverage("size")),
|
|
732
|
+
tseries("fsdT", "Mon", post_hook=TimeAverage("size"))),
|
|
733
|
+
"sizeMon", id="two-hooks-one-output"),
|
|
734
|
+
pytest.param(
|
|
735
|
+
(tseries("gyrT", "Mon", post_hook=TimeAverage("fsdT")),
|
|
736
|
+
tseries("fsdT", "Mon")),
|
|
737
|
+
"fsdTMon", id="hook-output-is-a-listed-property"),
|
|
738
|
+
pytest.param(
|
|
739
|
+
(histogram("r", "Crd", post_hook=LocalDensities()),
|
|
740
|
+
tseries("rRho", "Crd")),
|
|
741
|
+
"rRhoCrd", id="density-output-is-a-listed-property"),
|
|
742
|
+
pytest.param(
|
|
743
|
+
(matrix_tseries("distMatT", "Foci",
|
|
744
|
+
post_hook=FociPairDistances(1.0, 3.0, 10)),
|
|
745
|
+
matrix_tseries("pairDistT", "Foci")),
|
|
746
|
+
"pairDistTFoci", id="foci-output-is-a-listed-property"),
|
|
747
|
+
])
|
|
748
|
+
def test_analysis_table_rejects_repeated_output_names(properties, repeated):
|
|
749
|
+
"""A table whose declared outputs name one artifact twice is rejected,
|
|
750
|
+
so no output overwrites another at run time."""
|
|
751
|
+
match = rf"Outputs named more than once: \['{repeated}'\]"
|
|
752
|
+
with pytest.raises(ValueError, match=match):
|
|
753
|
+
AnalysisTable("bug", "whole", False, properties)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def test_analysis_table_accepts_hooks_that_pass_their_property_through():
|
|
757
|
+
"""A hook that passes its own property through under its name, beside
|
|
758
|
+
its derived outputs, is not a repeat."""
|
|
759
|
+
table = AnalysisTable("all", "segment", False, (
|
|
760
|
+
tseries("gyrT", "Mon", post_hook=Autocorrelation(nlags=2)),
|
|
761
|
+
histogram("r", "Crd", post_hook=LocalDensities()),
|
|
762
|
+
))
|
|
763
|
+
|
|
764
|
+
assert [prop.prop for prop in table.properties] == ["gyrTMon", "rHistCrd"]
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def test_analysis_table_rejects_local_densities_on_a_non_histogram():
|
|
768
|
+
"""LocalDensities on a property that is not a histogram fails when the
|
|
769
|
+
table is built, not when it runs."""
|
|
770
|
+
with pytest.raises(ValueError, match="needs a histogram"):
|
|
771
|
+
AnalysisTable("bug", "whole", False,
|
|
772
|
+
(tseries("gyrT", "Mon", post_hook=LocalDensities()),))
|