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,1222 @@
|
|
|
1
|
+
"""Tests for :mod:`polyphys.manage.parser`.
|
|
2
|
+
|
|
3
|
+
The parsers here are the synthetic ones of :mod:`polyphys.tests.synthetic`;
|
|
4
|
+
each published study tests its own parser in ``projects/<study>/``.
|
|
5
|
+
"""
|
|
6
|
+
import importlib
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from collections import OrderedDict
|
|
10
|
+
from operator import attrgetter
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, NamedTuple
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
from hypothesis import given, settings
|
|
16
|
+
from hypothesis import strategies as st
|
|
17
|
+
|
|
18
|
+
from polyphys.manage.parser import KeywordSpec, ParserBase, Species
|
|
19
|
+
from polyphys.manage.utils import (
|
|
20
|
+
number_density_cube,
|
|
21
|
+
number_density_cylinder,
|
|
22
|
+
volume_fraction_cube,
|
|
23
|
+
volume_fraction_cylinder,
|
|
24
|
+
)
|
|
25
|
+
from polyphys.tests import synthetic
|
|
26
|
+
from polyphys.tests.strategies import lineage_names, stored_values
|
|
27
|
+
from polyphys.tests.synthetic import (
|
|
28
|
+
ChainCyl,
|
|
29
|
+
RingCub,
|
|
30
|
+
RingCubBinders,
|
|
31
|
+
RingCubTwoCrowders,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ParseCase(NamedTuple):
|
|
36
|
+
"""A hand-written artifact name and what its parser must give."""
|
|
37
|
+
parser_class: type[ParserBase]
|
|
38
|
+
lineage: Any
|
|
39
|
+
group: Any
|
|
40
|
+
artifact: str
|
|
41
|
+
name: str
|
|
42
|
+
values: dict[str, Any]
|
|
43
|
+
physical: list[str]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ValidCase(NamedTuple):
|
|
47
|
+
"""A valid ``ensemble_long`` artifact name for one parser class."""
|
|
48
|
+
parser_class: type[ParserBase]
|
|
49
|
+
artifact: str
|
|
50
|
+
lineage: Any
|
|
51
|
+
group: Any
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.fixture(params=[
|
|
55
|
+
pytest.param(
|
|
56
|
+
ParseCase(
|
|
57
|
+
ChainCyl, 'segment', 'bug',
|
|
58
|
+
artifact='N2000r10.5lz300.0ac1.0nc117000dt0.005bdump5000'
|
|
59
|
+
'adump10000ens2.j02.bug',
|
|
60
|
+
name='N2000r10.5lz300.0ac1.0nc117000dt0.005bdump5000adump10000'
|
|
61
|
+
'ens2.j02',
|
|
62
|
+
values=dict(nmon=2000, dcyl=20, lcyl=600.0, dcrowd=1.0,
|
|
63
|
+
ncrowd=117000, dt=0.005, bdump=5000, adump=10000,
|
|
64
|
+
ensemble_id=2, segment_id=2),
|
|
65
|
+
physical=['dmon', 'phi_bulk_m', 'rho_bulk_m', 'phi_bulk_c',
|
|
66
|
+
'rho_bulk_c']),
|
|
67
|
+
id='ChainCyl-segment'),
|
|
68
|
+
pytest.param(
|
|
69
|
+
ParseCase(
|
|
70
|
+
ChainCyl, 'whole', 'bug',
|
|
71
|
+
artifact='N2000r10.5lz300.0ac1.0nc117000dt0.005bdump5000'
|
|
72
|
+
'adump10000ens2.bug',
|
|
73
|
+
name='N2000r10.5lz300.0ac1.0nc117000dt0.005bdump5000adump10000'
|
|
74
|
+
'ens2',
|
|
75
|
+
values=dict(nmon=2000, dcyl=20, lcyl=600.0, dcrowd=1.0,
|
|
76
|
+
ncrowd=117000, dt=0.005, bdump=5000, adump=10000,
|
|
77
|
+
ensemble_id=2),
|
|
78
|
+
physical=['dmon', 'phi_bulk_m', 'rho_bulk_m', 'phi_bulk_c',
|
|
79
|
+
'rho_bulk_c']),
|
|
80
|
+
id='ChainCyl-whole'),
|
|
81
|
+
pytest.param(
|
|
82
|
+
ParseCase(
|
|
83
|
+
ChainCyl, 'ensemble_long', 'bug',
|
|
84
|
+
artifact='N2000r10.5lz300.0ac1.0nc117000dt0.005bdump5000'
|
|
85
|
+
'adump10000',
|
|
86
|
+
name='N2000r10.5lz300.0ac1.0nc117000dt0.005bdump5000adump10000',
|
|
87
|
+
values=dict(nmon=2000, dcyl=20, lcyl=600.0, dcrowd=1.0,
|
|
88
|
+
ncrowd=117000, dt=0.005, bdump=5000, adump=10000),
|
|
89
|
+
physical=['dmon', 'phi_bulk_m', 'rho_bulk_m', 'phi_bulk_c',
|
|
90
|
+
'rho_bulk_c']),
|
|
91
|
+
id='ChainCyl-ensemble_long'),
|
|
92
|
+
pytest.param(
|
|
93
|
+
ParseCase(
|
|
94
|
+
ChainCyl, 'ensemble', 'bug',
|
|
95
|
+
artifact='N2000D20.0ac1.0nc117000',
|
|
96
|
+
name='N2000D20.0ac1.0nc117000',
|
|
97
|
+
values=dict(dmon=1.0, nmon=2000, dcrowd=1.0, ncrowd=117000,
|
|
98
|
+
dcyl=20),
|
|
99
|
+
physical=['dmon']),
|
|
100
|
+
id='ChainCyl-ensemble'),
|
|
101
|
+
pytest.param(
|
|
102
|
+
ParseCase(
|
|
103
|
+
ChainCyl, 'space', 'bug',
|
|
104
|
+
artifact='N2000D20.0ac1.0',
|
|
105
|
+
name='N2000D20.0ac1.0',
|
|
106
|
+
values=dict(dmon=1.0, nmon=2000, dcrowd=1.0, dcyl=20),
|
|
107
|
+
physical=['dmon']),
|
|
108
|
+
id='ChainCyl-space'),
|
|
109
|
+
pytest.param(
|
|
110
|
+
ParseCase(
|
|
111
|
+
RingCub, 'segment', 'all',
|
|
112
|
+
artifact='al5nl5ml125ns400ac3nc7735l45dt0.005bdump2000adump5000'
|
|
113
|
+
'ens4.j10.ring.all',
|
|
114
|
+
name='al5nl5ml125ns400ac3nc7735l45dt0.005bdump2000adump5000ens4.'
|
|
115
|
+
'j10.ring',
|
|
116
|
+
values=dict(dmon_large=5.0, nmon_large=5, mmon_large=125.0,
|
|
117
|
+
nmon_small=400, dcrowd=3.0, ncrowd=7735, lcube=90.0,
|
|
118
|
+
dt=0.005, bdump=2000, adump=5000, ensemble_id=4,
|
|
119
|
+
segment_id=10),
|
|
120
|
+
physical=['dmon_small', 'mmon_small', 'mcrowd', 'phi_bulk_m_small',
|
|
121
|
+
'rho_bulk_m_small', 'phi_bulk_m_large',
|
|
122
|
+
'rho_bulk_m_large', 'phi_bulk_m', 'rho_bulk_m',
|
|
123
|
+
'phi_bulk_c', 'rho_bulk_c']),
|
|
124
|
+
id='RingCub-segment'),
|
|
125
|
+
pytest.param(
|
|
126
|
+
ParseCase(
|
|
127
|
+
RingCub, 'whole', 'all',
|
|
128
|
+
artifact='al5nl5ml125ns400ac3nc7735l45dt0.005bdump2000adump5000'
|
|
129
|
+
'ens4.ring.all',
|
|
130
|
+
name='al5nl5ml125ns400ac3nc7735l45dt0.005bdump2000adump5000ens4.'
|
|
131
|
+
'ring',
|
|
132
|
+
values=dict(dmon_large=5.0, nmon_large=5, mmon_large=125.0,
|
|
133
|
+
nmon_small=400, dcrowd=3.0, ncrowd=7735, lcube=90.0,
|
|
134
|
+
dt=0.005, bdump=2000, adump=5000, ensemble_id=4),
|
|
135
|
+
physical=['dmon_small', 'mmon_small', 'mcrowd', 'phi_bulk_m_small',
|
|
136
|
+
'rho_bulk_m_small', 'phi_bulk_m_large',
|
|
137
|
+
'rho_bulk_m_large', 'phi_bulk_m', 'rho_bulk_m',
|
|
138
|
+
'phi_bulk_c', 'rho_bulk_c']),
|
|
139
|
+
id='RingCub-whole'),
|
|
140
|
+
pytest.param(
|
|
141
|
+
ParseCase(
|
|
142
|
+
RingCub, 'ensemble_long', 'all',
|
|
143
|
+
artifact='al5nl5ml125ns400ac3nc7735l45dt0.005bdump2000adump5000',
|
|
144
|
+
name='al5nl5ml125ns400ac3nc7735l45dt0.005bdump2000adump5000',
|
|
145
|
+
values=dict(dmon_large=5.0, nmon_large=5, mmon_large=125.0,
|
|
146
|
+
nmon_small=400, dcrowd=3.0, ncrowd=7735, lcube=90.0,
|
|
147
|
+
dt=0.005, bdump=2000, adump=5000),
|
|
148
|
+
physical=['dmon_small', 'mmon_small', 'mcrowd', 'phi_bulk_m_small',
|
|
149
|
+
'rho_bulk_m_small', 'phi_bulk_m_large',
|
|
150
|
+
'rho_bulk_m_large', 'phi_bulk_m', 'rho_bulk_m',
|
|
151
|
+
'phi_bulk_c', 'rho_bulk_c']),
|
|
152
|
+
id='RingCub-ensemble_long'),
|
|
153
|
+
pytest.param(
|
|
154
|
+
ParseCase(
|
|
155
|
+
RingCub, 'ensemble', 'all',
|
|
156
|
+
artifact='ns400nl5al5ac3nc7735',
|
|
157
|
+
name='ns400nl5al5ac3nc7735',
|
|
158
|
+
values=dict(dmon_large=5.0, nmon_large=5, nmon_small=400,
|
|
159
|
+
dcrowd=3.0, ncrowd=7735),
|
|
160
|
+
physical=['dmon_small', 'mmon_small', 'mcrowd']),
|
|
161
|
+
id='RingCub-ensemble'),
|
|
162
|
+
pytest.param(
|
|
163
|
+
ParseCase(
|
|
164
|
+
RingCub, 'space', 'all',
|
|
165
|
+
artifact='ns400nl5al5ac3',
|
|
166
|
+
name='ns400nl5al5ac3',
|
|
167
|
+
values=dict(dmon_large=5.0, nmon_large=5, nmon_small=400,
|
|
168
|
+
dcrowd=3.0),
|
|
169
|
+
physical=['dmon_small', 'mmon_small', 'mcrowd']),
|
|
170
|
+
id='RingCub-space'),
|
|
171
|
+
])
|
|
172
|
+
def parse_case(request: pytest.FixtureRequest) -> ParseCase:
|
|
173
|
+
"""
|
|
174
|
+
One hand-written name per parser class and lineage, with the values its
|
|
175
|
+
parser must give: the digits in the name, converted as the class
|
|
176
|
+
docstring states (``lcube = 2 l``, ``lcyl = 2 lz``, ``dcyl = 2 r - 1``).
|
|
177
|
+
"""
|
|
178
|
+
case: ParseCase = request.param
|
|
179
|
+
return case
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@pytest.fixture
|
|
183
|
+
def parsed(parse_case: ParseCase) -> ParserBase:
|
|
184
|
+
"""The parser built from the name of `parse_case`."""
|
|
185
|
+
return parse_case.parser_class(parse_case.artifact, parse_case.lineage,
|
|
186
|
+
parse_case.group)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@pytest.fixture(params=[
|
|
190
|
+
pytest.param(
|
|
191
|
+
ValidCase(ChainCyl,
|
|
192
|
+
'N2000r10.5lz300.0ac1.0nc117000dt0.005bdump5000adump10000',
|
|
193
|
+
'ensemble_long', 'bug'),
|
|
194
|
+
id='ChainCyl'),
|
|
195
|
+
pytest.param(
|
|
196
|
+
ValidCase(RingCub,
|
|
197
|
+
'al5nl5ml125ns400ac3nc7735l45dt0.005bdump2000adump5000',
|
|
198
|
+
'ensemble_long', 'bug'),
|
|
199
|
+
id='RingCub'),
|
|
200
|
+
])
|
|
201
|
+
def valid_case(request: pytest.FixtureRequest) -> ValidCase:
|
|
202
|
+
"""A valid ``ensemble_long`` name for each parser class."""
|
|
203
|
+
case: ValidCase = request.param
|
|
204
|
+
return case
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@pytest.fixture(params=[
|
|
208
|
+
pytest.param((parser_class, lineage),
|
|
209
|
+
id=f"{parser_class.__name__}-{lineage}")
|
|
210
|
+
for parser_class in (ChainCyl, RingCub, RingCubTwoCrowders,
|
|
211
|
+
RingCubBinders)
|
|
212
|
+
for lineage in ('segment', 'whole', 'ensemble_long', 'ensemble', 'space')
|
|
213
|
+
])
|
|
214
|
+
def declared_case(
|
|
215
|
+
request: pytest.FixtureRequest
|
|
216
|
+
) -> tuple[type[ParserBase], Any, str, dict[str, Any]]:
|
|
217
|
+
"""
|
|
218
|
+
A name built from a class's declared pattern for one lineage, with 10.5
|
|
219
|
+
as the value of every float keyword and 2 as the value of every integer
|
|
220
|
+
keyword, and the values the parser must store for them. With these
|
|
221
|
+
values every volume fraction stays below 1 in both geometries.
|
|
222
|
+
"""
|
|
223
|
+
parser_class, lineage = request.param
|
|
224
|
+
specs = [(keyword, parser_class._keywords[keyword])
|
|
225
|
+
for keyword in parser_class._patterns[lineage]]
|
|
226
|
+
name = ''.join(f"{keyword}{'10.5' if spec.kind is float else '2'}"
|
|
227
|
+
for keyword, spec in specs)
|
|
228
|
+
expected = {spec.attr: (spec.scale * 10.5 + spec.offset
|
|
229
|
+
if spec.kind is float else 2)
|
|
230
|
+
for _, spec in specs}
|
|
231
|
+
return parser_class, lineage, name, expected
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@pytest.fixture
|
|
235
|
+
def chain_declaration() -> dict[str, Any]:
|
|
236
|
+
"""The class attributes of a valid parser for a chain in a cube."""
|
|
237
|
+
return {
|
|
238
|
+
'_geometry': 'cubic',
|
|
239
|
+
'_topology': 'linear',
|
|
240
|
+
'_groups': ['bug', 'all'],
|
|
241
|
+
'_dump_keywords': {'bug': 'bdump', 'all': 'adump'},
|
|
242
|
+
'_keywords': {
|
|
243
|
+
'N': KeywordSpec('nmon', int),
|
|
244
|
+
'ac': KeywordSpec('dcrowd', float),
|
|
245
|
+
'nc': KeywordSpec('ncrowd', int),
|
|
246
|
+
'l': KeywordSpec('lcube', float, scale=2.0),
|
|
247
|
+
'ens': KeywordSpec('ensemble_id', int),
|
|
248
|
+
'j': KeywordSpec('segment_id', int),
|
|
249
|
+
},
|
|
250
|
+
'_patterns': {
|
|
251
|
+
'segment': ('N', 'ac', 'nc', 'l', 'ens', 'j'),
|
|
252
|
+
'whole': ('N', 'ac', 'nc', 'l', 'ens'),
|
|
253
|
+
'ensemble_long': ('N', 'ac', 'nc', 'l'),
|
|
254
|
+
'ensemble': ('N', 'ac', 'nc'),
|
|
255
|
+
'space': ('N', 'ac'),
|
|
256
|
+
},
|
|
257
|
+
'_defaults': {'dmon': 1.0, 'bdump': 1000, 'adump': 5000},
|
|
258
|
+
'_species': (Species('m', 'nmon', 'dmon'),
|
|
259
|
+
Species('c', 'ncrowd', 'dcrowd')),
|
|
260
|
+
'_project_attributes': {
|
|
261
|
+
'segment': ['dmon'],
|
|
262
|
+
'whole': ['dmon'],
|
|
263
|
+
'ensemble_long': ['dmon'],
|
|
264
|
+
'ensemble': ['dmon'],
|
|
265
|
+
'space': ['dmon'],
|
|
266
|
+
},
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# Parsing hand-written names
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_parser_sets_values_from_name(parse_case, parsed):
|
|
274
|
+
"""Each lineage's name parses to its digits, converted as documented."""
|
|
275
|
+
actual = attrgetter(*parse_case.values)(parsed)
|
|
276
|
+
# Values are parsed from decimal text; equal to float64 round-off.
|
|
277
|
+
assert actual == pytest.approx(tuple(parse_case.values.values()),
|
|
278
|
+
rel=1e-12, abs=1e-12)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def test_parser_reduces_artifact_to_lineage_name(parse_case, parsed):
|
|
282
|
+
"""The name drops the group and topology suffixes after the lineage."""
|
|
283
|
+
assert parsed.name == parse_case.name
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def test_parser_lists_physical_attributes(parse_case, parsed):
|
|
287
|
+
"""Each lineage lists the fixed and computed attributes it sets."""
|
|
288
|
+
assert parsed.physical_attributes == parse_case.physical
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_parser_names_its_project(parse_case, parsed):
|
|
292
|
+
"""The project name is the parser class's name."""
|
|
293
|
+
assert parsed.project_name == parse_case.parser_class.__name__
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def test_parser_without_path_reports_no_path(parse_case, parsed):
|
|
297
|
+
"""A parser built from a name keeps it and has no filepath or ext."""
|
|
298
|
+
assert (parsed.filename, parsed.filepath, parsed.ext, parsed.ispath,
|
|
299
|
+
parsed.lineage, parsed.group) == (
|
|
300
|
+
parse_case.artifact, None, None, False, parse_case.lineage,
|
|
301
|
+
parse_case.group)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def test_from_name_reports_no_path():
|
|
305
|
+
"""A parser built with from_name has no filepath or ext."""
|
|
306
|
+
parser = ChainCyl.from_name('N200D10.0ac2.0', 'space', 'bug')
|
|
307
|
+
|
|
308
|
+
assert (parser.filepath, parser.ext, parser.ispath) == (None, None, False)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_parser_reads_name_from_file_path(parse_case, make_temp_file):
|
|
312
|
+
"""A parser built from a file path parses the file's stem."""
|
|
313
|
+
full_path = make_temp_file(parse_case.artifact + '.txt')
|
|
314
|
+
|
|
315
|
+
parser = parse_case.parser_class(full_path, parse_case.lineage,
|
|
316
|
+
parse_case.group, ispath=True)
|
|
317
|
+
|
|
318
|
+
assert (parser.filename, parser.filepath, parser.ext, parser.ispath,
|
|
319
|
+
parser.name) == (
|
|
320
|
+
parse_case.artifact + '.txt', Path(os.path.dirname(full_path)),
|
|
321
|
+
'.txt', True, parse_case.name)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def test_parser_reports_empty_ext_for_file_without_suffix(make_temp_file):
|
|
325
|
+
"""A file path with no suffix gives an empty ext, not None."""
|
|
326
|
+
full_path = make_temp_file('N200D10ac2')
|
|
327
|
+
|
|
328
|
+
parser = ChainCyl(full_path, 'space', 'bug', ispath=True)
|
|
329
|
+
|
|
330
|
+
assert (parser.ext, parser.filepath, parser.name) == (
|
|
331
|
+
'', Path(os.path.dirname(full_path)), 'N200D10ac2')
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@pytest.mark.parametrize('filename, name, ext, dcrowd', [
|
|
335
|
+
pytest.param('N20D10ac1.5', 'N20D10ac1.5', '', 1.5, id='trailing-float'),
|
|
336
|
+
pytest.param('N20D10ac1.5.csv', 'N20D10ac1.5', '.csv', 1.5,
|
|
337
|
+
id='trailing-float-with-extension'),
|
|
338
|
+
pytest.param('N20D10ac1.', 'N20D10ac1.', '', 1.0, id='trailing-dot'),
|
|
339
|
+
])
|
|
340
|
+
def test_parser_strips_only_an_extension_with_a_letter(
|
|
341
|
+
make_temp_file, filename, name, ext, dcrowd):
|
|
342
|
+
"""With ispath, a suffix made of digits, or a lone dot, stays part of the
|
|
343
|
+
name instead of being dropped as an extension."""
|
|
344
|
+
full_path = make_temp_file(filename)
|
|
345
|
+
|
|
346
|
+
parser = ChainCyl(full_path, 'space', 'bug', ispath=True)
|
|
347
|
+
|
|
348
|
+
assert (parser.name, parser.ext, parser.dcrowd) == (name, ext, dcrowd)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@pytest.mark.parametrize('parser_class, lineage, group, artifact, expected', [
|
|
352
|
+
pytest.param(ChainCyl, 'ensemble', 'bug', 'N2000D20.0ac1.0nc117000',
|
|
353
|
+
['nmon', 'dcyl', 'dcrowd', 'ncrowd'],
|
|
354
|
+
id='ChainCyl-ensemble'),
|
|
355
|
+
])
|
|
356
|
+
def test_parser_lists_lineage_attributes_in_pattern_order(
|
|
357
|
+
parser_class, lineage, group, artifact, expected):
|
|
358
|
+
"""The parsed attributes are listed in the order the name gives them."""
|
|
359
|
+
parser = parser_class(artifact, lineage, group)
|
|
360
|
+
|
|
361
|
+
assert parser.lineage_attributes == expected
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def test_name_through_unknown_keyword_raises_value_error():
|
|
365
|
+
"""Cutting a name at a keyword it lacks raises ValueError naming it."""
|
|
366
|
+
parser = ChainCyl('N2000D20.0ac1.0', 'space', 'bug')
|
|
367
|
+
|
|
368
|
+
with pytest.raises(ValueError,
|
|
369
|
+
match="'zz' keyword not found in 'N2000D20.0ac1.0'"):
|
|
370
|
+
parser._name_through('zz')
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def test_parser_lists_each_attribute_once(parsed):
|
|
374
|
+
"""No attribute appears twice in `attributes`."""
|
|
375
|
+
assert len(parsed.attributes) == len(set(parsed.attributes))
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def test_parser_sets_every_listed_attribute(parsed):
|
|
379
|
+
"""Every attribute in `attributes` is set on the instance."""
|
|
380
|
+
assert set(parsed.attributes) <= vars(parsed).keys()
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@pytest.mark.parametrize('lineage, artifact, expected', [
|
|
384
|
+
pytest.param('segment',
|
|
385
|
+
'N2000r10.5lz300.0ac1.0nc117000dt0.005bdump5000adump10000'
|
|
386
|
+
'ens2.j02.bug',
|
|
387
|
+
['segment', 'whole', 'ensemble_long', 'ensemble', 'space'],
|
|
388
|
+
id='segment'),
|
|
389
|
+
pytest.param('whole',
|
|
390
|
+
'N2000r10.5lz300.0ac1.0nc117000dt0.005bdump5000adump10000'
|
|
391
|
+
'ens2.bug',
|
|
392
|
+
['whole', 'ensemble_long', 'ensemble', 'space'],
|
|
393
|
+
id='whole'),
|
|
394
|
+
pytest.param('ensemble_long',
|
|
395
|
+
'N2000r10.5lz300.0ac1.0nc117000dt0.005bdump5000adump10000',
|
|
396
|
+
['ensemble_long', 'ensemble', 'space'], id='ensemble_long'),
|
|
397
|
+
pytest.param('ensemble', 'N2000D20.0ac1.0nc117000',
|
|
398
|
+
['ensemble', 'space'], id='ensemble'),
|
|
399
|
+
pytest.param('space', 'N2000D20.0ac1.0', ['space'], id='space'),
|
|
400
|
+
])
|
|
401
|
+
def test_lineage_genealogy_lists_lineage_and_parents(lineage, artifact,
|
|
402
|
+
expected):
|
|
403
|
+
"""A lineage's genealogy is itself followed by its parent lineages."""
|
|
404
|
+
parser = ChainCyl(artifact, lineage, 'bug')
|
|
405
|
+
|
|
406
|
+
assert parser.lineage_genealogy == expected
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@pytest.mark.parametrize(
|
|
410
|
+
'parser_class, artifact, group, geometry, topology, groups', [
|
|
411
|
+
pytest.param(ChainCyl, 'N2000D20.0ac1.0', 'bug', 'cylindrical',
|
|
412
|
+
'linear', ['bug', 'all'], id='ChainCyl'),
|
|
413
|
+
pytest.param(RingCub, 'ns400nl5al5ac3', 'all', 'cubic', 'ring',
|
|
414
|
+
['bug', 'all'], id='RingCub'),
|
|
415
|
+
])
|
|
416
|
+
def test_parser_reports_geometry_topology_and_groups(
|
|
417
|
+
parser_class, artifact, group, geometry, topology, groups):
|
|
418
|
+
"""Each parser reports its project's geometry, topology, and groups."""
|
|
419
|
+
parser = parser_class(artifact, 'space', group)
|
|
420
|
+
|
|
421
|
+
assert (parser.geometry, parser.topology, parser.groups) == (
|
|
422
|
+
geometry, topology, groups)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@pytest.mark.parametrize('parser_class, artifact, group, dump_keywords', [
|
|
426
|
+
pytest.param(ChainCyl, 'N2000D20.0ac1.0', 'bug',
|
|
427
|
+
{'bug': 'bdump', 'all': 'adump'}, id='ChainCyl'),
|
|
428
|
+
pytest.param(RingCub, 'ns400nl5al5ac3', 'all',
|
|
429
|
+
{'bug': 'bdump', 'all': 'adump'}, id='RingCub'),
|
|
430
|
+
])
|
|
431
|
+
def test_parser_maps_each_group_to_its_dump_keyword(
|
|
432
|
+
parser_class, artifact, group, dump_keywords):
|
|
433
|
+
"""Each group's dump keyword names the period of its own trajectory."""
|
|
434
|
+
parser = parser_class(artifact, 'space', group)
|
|
435
|
+
|
|
436
|
+
assert parser.dump_keywords == dump_keywords
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@pytest.mark.parametrize('parser_class, artifact, group, keyword, period', [
|
|
440
|
+
pytest.param(ChainCyl,
|
|
441
|
+
'N200r10.5lz25.0ac2.0nc1000dt0.005bdump2000adump5000ens1',
|
|
442
|
+
'bug', 'bdump', 2000, id='ChainCyl-bug'),
|
|
443
|
+
pytest.param(ChainCyl,
|
|
444
|
+
'N200r10.5lz25.0ac2.0nc1000dt0.005bdump2000adump5000ens1',
|
|
445
|
+
'all', 'adump', 5000, id='ChainCyl-all'),
|
|
446
|
+
])
|
|
447
|
+
def test_dump_keyword_reads_the_group_dump_period(
|
|
448
|
+
parser_class, artifact, group, keyword, period):
|
|
449
|
+
"""dump_keyword names the attribute holding the group's dump period."""
|
|
450
|
+
parser = parser_class(artifact, 'whole', group)
|
|
451
|
+
|
|
452
|
+
assert (parser.dump_keyword, getattr(parser, parser.dump_keyword)) == (
|
|
453
|
+
keyword, period)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@pytest.mark.parametrize('parser_class, artifact, group, attrs, expected', [
|
|
457
|
+
pytest.param(RingCub,
|
|
458
|
+
'al5nl5ml125.ns400ac3nc7735l45dt0.005bdump2000adump5000',
|
|
459
|
+
'bug', ('mmon_large', 'nmon_small'), (125.0, 400),
|
|
460
|
+
id='RingCub'),
|
|
461
|
+
])
|
|
462
|
+
def test_parser_reads_value_with_trailing_dot(parser_class, artifact, group,
|
|
463
|
+
attrs, expected):
|
|
464
|
+
"""A value written with a trailing dot, such as ``1.``, parses."""
|
|
465
|
+
parser = parser_class(artifact, 'ensemble_long', group)
|
|
466
|
+
|
|
467
|
+
# float('1.') is exact.
|
|
468
|
+
assert attrgetter(*attrs)(parser) == pytest.approx(expected, rel=1e-12)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def test_declared_pattern_round_trips(declared_case):
|
|
472
|
+
"""
|
|
473
|
+
A name built from a declared pattern parses back to the declared types
|
|
474
|
+
and conversions, so float keywords keep non-integer values (property).
|
|
475
|
+
"""
|
|
476
|
+
parser_class, lineage, name, expected = declared_case
|
|
477
|
+
|
|
478
|
+
actual = attrgetter(*expected)(
|
|
479
|
+
parser_class(name, lineage, parser_class._groups[0]))
|
|
480
|
+
|
|
481
|
+
# One multiplication and one addition in float64.
|
|
482
|
+
assert actual == pytest.approx(tuple(expected.values()), rel=1e-12)
|
|
483
|
+
assert tuple(map(type, actual)) == tuple(map(type, expected.values()))
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
# Error paths of parsing
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
@pytest.mark.parametrize('parser_class, artifact, group, keyword, attr', [
|
|
490
|
+
pytest.param(ChainCyl,
|
|
491
|
+
'N2000r10.5lz300.0ac1.0nc117000XX0.005bdump5000adump10000',
|
|
492
|
+
'bug', 'dt', 'dt', id='ChainCyl-replaced'),
|
|
493
|
+
pytest.param(RingCub,
|
|
494
|
+
'al5nl5ml125ns400ac3XX7735l45dt0.005bdump2000adump5000',
|
|
495
|
+
'bug', 'nc', 'ncrowd', id='RingCub-replaced'),
|
|
496
|
+
pytest.param(ChainCyl,
|
|
497
|
+
'2000r10.5lz300.0ac1.0nc117000dt0.005bdump5000adump10000',
|
|
498
|
+
'bug', 'N', 'nmon', id='ChainCyl-heading-digit'),
|
|
499
|
+
pytest.param(RingCub,
|
|
500
|
+
'5nl5ml125ns400ac3nc7735l45dt0.005bdump2000adump5000',
|
|
501
|
+
'bug', 'al', 'dmon_large', id='RingCub-heading-digit'),
|
|
502
|
+
pytest.param(RingCubTwoCrowders,
|
|
503
|
+
'al4nl5ns400ac1ab5nc139229XX1671l45dt0.005bdump2000'
|
|
504
|
+
'adump5000',
|
|
505
|
+
'bug', 'nb', 'ncrowd_two', id='RingCubTwoCrowders-replaced'),
|
|
506
|
+
])
|
|
507
|
+
def test_missing_keyword_raises_value_error(parser_class, artifact, group,
|
|
508
|
+
keyword, attr):
|
|
509
|
+
"""A name without a pattern keyword raises ValueError naming it."""
|
|
510
|
+
message = (f"keyword '{keyword}' for attribute '{attr}' not found in "
|
|
511
|
+
f"'{artifact}' (lineage 'ensemble_long')")
|
|
512
|
+
|
|
513
|
+
with pytest.raises(ValueError, match=re.escape(message)):
|
|
514
|
+
parser_class(artifact, 'ensemble_long', group)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
@pytest.mark.parametrize('parser_class, artifact, group, keyword', [
|
|
518
|
+
pytest.param(ChainCyl,
|
|
519
|
+
'N.r10.5lz300.0ac1.0nc117000dt0.005bdump5000adump10000',
|
|
520
|
+
'bug', 'N', id='ChainCyl'),
|
|
521
|
+
pytest.param(RingCub,
|
|
522
|
+
'al.nl5ml125ns400ac3nc7735l45dt0.005bdump2000adump5000',
|
|
523
|
+
'bug', 'al', id='RingCub'),
|
|
524
|
+
])
|
|
525
|
+
def test_non_numeric_value_raises_value_error(parser_class, artifact, group,
|
|
526
|
+
keyword):
|
|
527
|
+
"""A keyword whose value is not a number raises ValueError naming it."""
|
|
528
|
+
message = (f"value of keyword '{keyword}' in '{artifact}' "
|
|
529
|
+
"(lineage 'ensemble_long'): '.' is not a number")
|
|
530
|
+
|
|
531
|
+
with pytest.raises(ValueError, match=re.escape(message)):
|
|
532
|
+
parser_class(artifact, 'ensemble_long', group)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
@pytest.mark.parametrize('parser_class, artifact, group, keyword, token', [
|
|
536
|
+
pytest.param(ChainCyl,
|
|
537
|
+
'N2.5r10.5lz300.0ac1.0nc117000dt0.005bdump5000adump10000',
|
|
538
|
+
'bug', 'N', '2.5', id='ChainCyl-N'),
|
|
539
|
+
])
|
|
540
|
+
def test_non_integral_integer_raises_value_error(parser_class, artifact,
|
|
541
|
+
group, keyword, token):
|
|
542
|
+
"""A non-integral value of an integer keyword raises ValueError."""
|
|
543
|
+
message = (f"value of keyword '{keyword}' in '{artifact}' "
|
|
544
|
+
f"(lineage 'ensemble_long'): '{token}' is not an integer")
|
|
545
|
+
|
|
546
|
+
with pytest.raises(ValueError, match=re.escape(message)):
|
|
547
|
+
parser_class(artifact, 'ensemble_long', group)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def test_empty_artifact_raises_value_error(valid_case):
|
|
551
|
+
"""An empty artifact name raises ValueError."""
|
|
552
|
+
with pytest.raises(ValueError,
|
|
553
|
+
match="'artifact' cannot be an empty string."):
|
|
554
|
+
valid_case.parser_class('', valid_case.lineage, valid_case.group)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def test_invalid_group_raises_value_error(valid_case):
|
|
558
|
+
"""A group the project does not have raises ValueError."""
|
|
559
|
+
with pytest.raises(ValueError, match='is an invalid option.'):
|
|
560
|
+
valid_case.parser_class(valid_case.artifact, valid_case.lineage,
|
|
561
|
+
'invalid')
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def test_invalid_lineage_raises_value_error(valid_case):
|
|
565
|
+
"""A lineage outside the five lineages raises ValueError."""
|
|
566
|
+
with pytest.raises(ValueError, match='is an invalid option.'):
|
|
567
|
+
valid_case.parser_class(valid_case.artifact, 'invalid',
|
|
568
|
+
valid_case.group)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
# Text representations
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def test_str_summarizes_artifact(valid_case):
|
|
575
|
+
"""`str` lists the name, geometry, group, lineage, topology, project."""
|
|
576
|
+
parser = valid_case.parser_class(valid_case.artifact, valid_case.lineage,
|
|
577
|
+
valid_case.group)
|
|
578
|
+
|
|
579
|
+
assert str(parser) == (
|
|
580
|
+
f"Artifact:\n"
|
|
581
|
+
f" Name: '{valid_case.artifact}',\n"
|
|
582
|
+
f" Geometry: '{parser.geometry}',\n"
|
|
583
|
+
f" Group: '{valid_case.group}',\n"
|
|
584
|
+
f" Lineage: '{valid_case.lineage}',\n"
|
|
585
|
+
f" Topology: '{parser.topology}',\n"
|
|
586
|
+
f" Project: '{valid_case.parser_class.__name__}'")
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def test_repr_names_artifact(valid_case):
|
|
590
|
+
"""`repr` names the file, geometry, group, lineage, topology, project."""
|
|
591
|
+
parser = valid_case.parser_class(valid_case.artifact, valid_case.lineage,
|
|
592
|
+
valid_case.group)
|
|
593
|
+
|
|
594
|
+
assert repr(parser) == (
|
|
595
|
+
f"Artifact(filename='{valid_case.artifact}', "
|
|
596
|
+
f"geometry='{parser.geometry}', group='{valid_case.group}', "
|
|
597
|
+
f"lineage='{valid_case.lineage}', topology='{parser.topology}', "
|
|
598
|
+
f"project='{valid_case.parser_class.__name__}')")
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
# Bulk values
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
@pytest.mark.parametrize('parser_class, artifact, suffix, number, size', [
|
|
605
|
+
pytest.param(RingCub,
|
|
606
|
+
'al5nl5ml125ns400ac3nc7735l45dt0.005bdump2000adump5000',
|
|
607
|
+
'm_small', 'nmon_small', 'dmon_small', id='RingCub-m_small'),
|
|
608
|
+
pytest.param(RingCub,
|
|
609
|
+
'al5nl5ml125ns400ac3nc7735l45dt0.005bdump2000adump5000',
|
|
610
|
+
'm_large', 'nmon_large', 'dmon_large', id='RingCub-m_large'),
|
|
611
|
+
pytest.param(RingCub,
|
|
612
|
+
'al5nl5ml125ns400ac3nc7735l45dt0.005bdump2000adump5000',
|
|
613
|
+
'c', 'ncrowd', 'dcrowd', id='RingCub-c'),
|
|
614
|
+
pytest.param(RingCubTwoCrowders,
|
|
615
|
+
'al4nl5ns400ac1ab5nc139229nb1671l45dt0.005bdump2000'
|
|
616
|
+
'adump5000',
|
|
617
|
+
'm_small', 'nmon_small', 'dmon_small',
|
|
618
|
+
id='RingCubTwoCrowders-m_small'),
|
|
619
|
+
pytest.param(RingCubTwoCrowders,
|
|
620
|
+
'al4nl5ns400ac1ab5nc139229nb1671l45dt0.005bdump2000'
|
|
621
|
+
'adump5000',
|
|
622
|
+
'm_large', 'nmon_large', 'dmon_large',
|
|
623
|
+
id='RingCubTwoCrowders-m_large'),
|
|
624
|
+
pytest.param(RingCubTwoCrowders,
|
|
625
|
+
'al4nl5ns400ac1ab5nc139229nb1671l45dt0.005bdump2000'
|
|
626
|
+
'adump5000',
|
|
627
|
+
'c_one', 'ncrowd_one', 'dcrowd_one',
|
|
628
|
+
id='RingCubTwoCrowders-c_one'),
|
|
629
|
+
pytest.param(RingCubTwoCrowders,
|
|
630
|
+
'al4nl5ns400ac1ab5nc139229nb1671l45dt0.005bdump2000'
|
|
631
|
+
'adump5000',
|
|
632
|
+
'c_two', 'ncrowd_two', 'dcrowd_two',
|
|
633
|
+
id='RingCubTwoCrowders-c_two'),
|
|
634
|
+
])
|
|
635
|
+
def test_cube_bulk_values_match_utils(parser_class, artifact, suffix, number,
|
|
636
|
+
size):
|
|
637
|
+
"""Cubic bulk values are the cube functions of the parsed values."""
|
|
638
|
+
parser = parser_class(artifact, 'ensemble_long', parser_class._groups[0])
|
|
639
|
+
n, d, lcube = attrgetter(number, size, 'lcube')(parser)
|
|
640
|
+
|
|
641
|
+
actual = attrgetter(f'rho_bulk_{suffix}', f'phi_bulk_{suffix}')(parser)
|
|
642
|
+
|
|
643
|
+
# The same functions on the same inputs; float64 round-off only.
|
|
644
|
+
assert actual == pytest.approx(
|
|
645
|
+
(number_density_cube(n, d, lcube, pbc=True),
|
|
646
|
+
volume_fraction_cube(n, d, lcube, pbc=True)), rel=1e-12)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
@pytest.mark.parametrize('parser_class, artifact, suffix, number, size', [
|
|
650
|
+
pytest.param(ChainCyl,
|
|
651
|
+
'N2000r10.5lz300.0ac1.0nc117000dt0.005bdump5000adump10000',
|
|
652
|
+
'm', 'nmon', 'dmon', id='ChainCyl-m'),
|
|
653
|
+
pytest.param(ChainCyl,
|
|
654
|
+
'N2000r10.5lz300.0ac1.0nc117000dt0.005bdump5000adump10000',
|
|
655
|
+
'c', 'ncrowd', 'dcrowd', id='ChainCyl-c'),
|
|
656
|
+
])
|
|
657
|
+
def test_cylinder_bulk_values_match_utils(parser_class, artifact, suffix,
|
|
658
|
+
number, size):
|
|
659
|
+
"""Cylindrical bulk values are the cylinder functions of the values."""
|
|
660
|
+
parser = parser_class(artifact, 'ensemble_long', parser_class._groups[0])
|
|
661
|
+
n, d, lcyl, dcyl = attrgetter(number, size, 'lcyl', 'dcyl')(parser)
|
|
662
|
+
|
|
663
|
+
actual = attrgetter(f'rho_bulk_{suffix}', f'phi_bulk_{suffix}')(parser)
|
|
664
|
+
|
|
665
|
+
# The same functions on the same inputs; float64 round-off only.
|
|
666
|
+
assert actual == pytest.approx(
|
|
667
|
+
(number_density_cylinder(n, d, lcyl, dcyl, pbc=True),
|
|
668
|
+
volume_fraction_cylinder(n, d, lcyl, dcyl, pbc=True)), rel=1e-12)
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
@pytest.mark.parametrize('parser_class, artifact', [
|
|
672
|
+
pytest.param(RingCub,
|
|
673
|
+
'al5nl5ml125ns400ac3nc7735l45dt0.005bdump2000adump5000',
|
|
674
|
+
id='RingCub'),
|
|
675
|
+
pytest.param(RingCubTwoCrowders,
|
|
676
|
+
'al4nl5ns400ac1ab5nc139229nb1671l45dt0.005bdump2000'
|
|
677
|
+
'adump5000',
|
|
678
|
+
id='RingCubTwoCrowders'),
|
|
679
|
+
])
|
|
680
|
+
def test_monomer_bulk_values_are_sums_over_sizes(parser_class, artifact):
|
|
681
|
+
"""All-monomer bulk values are the sums of the small and large ones."""
|
|
682
|
+
parser = parser_class(artifact, 'ensemble_long', 'bug')
|
|
683
|
+
small = attrgetter('rho_bulk_m_small', 'phi_bulk_m_small')(parser)
|
|
684
|
+
large = attrgetter('rho_bulk_m_large', 'phi_bulk_m_large')(parser)
|
|
685
|
+
|
|
686
|
+
actual = attrgetter('rho_bulk_m', 'phi_bulk_m')(parser)
|
|
687
|
+
|
|
688
|
+
# One float64 addition each.
|
|
689
|
+
assert actual == pytest.approx(
|
|
690
|
+
(small[0] + large[0], small[1] + large[1]), rel=1e-12)
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def test_crowder_bulk_values_are_sums_over_species():
|
|
694
|
+
"""All-crowder bulk values are the sums of the two species' values."""
|
|
695
|
+
parser = RingCubTwoCrowders(
|
|
696
|
+
'al4nl5ns400ac1ab5nc139229nb1671l45dt0.005bdump2000adump5000',
|
|
697
|
+
'ensemble_long', 'bug')
|
|
698
|
+
one = attrgetter('rho_bulk_c_one', 'phi_bulk_c_one')(parser)
|
|
699
|
+
two = attrgetter('rho_bulk_c_two', 'phi_bulk_c_two')(parser)
|
|
700
|
+
|
|
701
|
+
actual = attrgetter('rho_bulk_c', 'phi_bulk_c')(parser)
|
|
702
|
+
|
|
703
|
+
# One float64 addition each.
|
|
704
|
+
assert actual == pytest.approx((one[0] + two[0], one[1] + two[1]),
|
|
705
|
+
rel=1e-12)
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
@pytest.mark.parametrize('parser_class, artifact', [
|
|
709
|
+
pytest.param(RingCub,
|
|
710
|
+
'al5nl5ml125ns400ac3nc7735l45dt0.005bdump2000adump5000',
|
|
711
|
+
id='RingCub'),
|
|
712
|
+
])
|
|
713
|
+
def test_crowder_mass_is_size_cubed(parser_class, artifact):
|
|
714
|
+
"""At a long lineage the crowder mass is its size cubed, m = d^3."""
|
|
715
|
+
parser = parser_class(artifact, 'ensemble_long', 'bug')
|
|
716
|
+
|
|
717
|
+
# One float64 power.
|
|
718
|
+
assert parser.mcrowd == pytest.approx(parser.dcrowd ** 3, rel=1e-12)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
@pytest.mark.parametrize('parser_class, artifact', [
|
|
722
|
+
pytest.param(RingCub, 'ns400nl5al5ac3nc7735', id='RingCub'),
|
|
723
|
+
])
|
|
724
|
+
def test_crowder_mass_is_placeholder_at_ensemble(parser_class, artifact):
|
|
725
|
+
"""At the ensemble lineage the crowder mass keeps the placeholder -1."""
|
|
726
|
+
parser = parser_class(artifact, 'ensemble', 'bug')
|
|
727
|
+
|
|
728
|
+
assert parser.mcrowd == -1
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
# Lineage-name rules
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
@pytest.mark.parametrize(
|
|
735
|
+
'parser_class, group, lineage, artifact, ensemble, space',
|
|
736
|
+
[
|
|
737
|
+
pytest.param(ChainCyl, 'all', 'segment',
|
|
738
|
+
'N200r10.5lz25.0ac2.0nc1000dt0.005bdump2000adump5000'
|
|
739
|
+
'ens1.j03',
|
|
740
|
+
'N200D20ac2nc1000', 'N200D20ac2',
|
|
741
|
+
id='ChainCyl-converted-dcyl'),
|
|
742
|
+
],
|
|
743
|
+
)
|
|
744
|
+
def test_short_name_prints_whole_number_float_as_integer(
|
|
745
|
+
parser_class, group, lineage, artifact, ensemble, space):
|
|
746
|
+
"""A whole-number float, zero included, prints as an integer."""
|
|
747
|
+
parser = parser_class(artifact, lineage, group)
|
|
748
|
+
|
|
749
|
+
assert (parser.ensemble, parser.space) == (ensemble, space)
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
@pytest.mark.parametrize(
|
|
753
|
+
'parser_class, group, lineage, artifact, ensemble, space',
|
|
754
|
+
[
|
|
755
|
+
pytest.param(ChainCyl, 'all', 'segment',
|
|
756
|
+
'N200r10.75lz25.0ac1.000001nc1000dt0.005bdump2000'
|
|
757
|
+
'adump5000ens1.j03',
|
|
758
|
+
'N200D20.5ac1.000001nc1000', 'N200D20.5ac1.000001',
|
|
759
|
+
id='ChainCyl-converted-dcyl'),
|
|
760
|
+
],
|
|
761
|
+
)
|
|
762
|
+
def test_short_name_prints_fractional_float_as_repr(
|
|
763
|
+
parser_class, group, lineage, artifact, ensemble, space):
|
|
764
|
+
"""A fractional float prints in full, as repr, in short names."""
|
|
765
|
+
parser = parser_class(artifact, lineage, group)
|
|
766
|
+
|
|
767
|
+
assert (parser.ensemble, parser.space) == (ensemble, space)
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
@pytest.mark.parametrize('token, expected', [
|
|
771
|
+
pytest.param('4.1', 7.2, id='r4.1'),
|
|
772
|
+
pytest.param('3.3', 5.6, id='r3.3'),
|
|
773
|
+
pytest.param('5.5', 10.0, id='r5.5'),
|
|
774
|
+
])
|
|
775
|
+
def test_keyword_spec_converts_decimal_token_exactly(token, expected):
|
|
776
|
+
"""The conversion 2r - 1 of a decimal token gives the float nearest the
|
|
777
|
+
exact decimal result, not one carrying binary round-off."""
|
|
778
|
+
spec = KeywordSpec('dcyl', float, scale=2.0, offset=-1.0)
|
|
779
|
+
|
|
780
|
+
value = spec.parse(token)
|
|
781
|
+
|
|
782
|
+
assert value == expected
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
@pytest.mark.parametrize('parser_class, group, artifact, ensemble', [
|
|
786
|
+
pytest.param(ChainCyl, 'bug',
|
|
787
|
+
'N20r4.1lz10ac1nc100dt0.005bdump1000adump5000',
|
|
788
|
+
'N20D7.2ac1nc100', id='ChainCyl-r4.1'),
|
|
789
|
+
pytest.param(ChainCyl, 'bug',
|
|
790
|
+
'N20r5.5lz10ac1nc100dt0.005bdump1000adump5000',
|
|
791
|
+
'N20D10ac1nc100', id='ChainCyl-r5.5'),
|
|
792
|
+
])
|
|
793
|
+
def test_short_name_prints_cylinder_size_from_r_as_decimal(
|
|
794
|
+
parser_class, group, artifact, ensemble):
|
|
795
|
+
"""The size D = 2r - 1 converted from a decimal r prints as the decimal
|
|
796
|
+
result in the ensemble name, so it matches the files named with it."""
|
|
797
|
+
parser = parser_class(artifact, 'ensemble_long', group)
|
|
798
|
+
|
|
799
|
+
assert parser.ensemble == ensemble
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
@pytest.mark.parametrize('short_lineage', [
|
|
803
|
+
pytest.param('ensemble', id='ensemble'),
|
|
804
|
+
pytest.param('space', id='space'),
|
|
805
|
+
])
|
|
806
|
+
def test_short_name_reparses_to_itself(valid_case, short_lineage):
|
|
807
|
+
"""A short name parsed at its own lineage rebuilds the same name."""
|
|
808
|
+
parser = valid_case.parser_class(valid_case.artifact, valid_case.lineage,
|
|
809
|
+
valid_case.group)
|
|
810
|
+
name = getattr(parser, short_lineage)
|
|
811
|
+
|
|
812
|
+
reparsed = valid_case.parser_class(name, short_lineage, valid_case.group)
|
|
813
|
+
|
|
814
|
+
assert getattr(reparsed, short_lineage) == name
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
@pytest.mark.parametrize('artifact, lineage, group, expected', [
|
|
818
|
+
pytest.param('runs/al4nl5ns400ac1ab5nc139229nb1671l45dt0.005bdump2000'
|
|
819
|
+
'adump5000ens1.j20.ring.all.lammpstrj',
|
|
820
|
+
'segment', 'all',
|
|
821
|
+
'al4nl5ns400ac1ab5nc139229nb1671l45dt0.005bdump2000'
|
|
822
|
+
'adump5000ens1.j20.ring', id='segment-trajectory'),
|
|
823
|
+
pytest.param('runs/al4nl5ns400ac1ab5nc139229nb1671l45dt0.005bdump2000'
|
|
824
|
+
'adump5000ens1.ring.bug.data',
|
|
825
|
+
'whole', 'bug',
|
|
826
|
+
'al4nl5ns400ac1ab5nc139229nb1671l45dt0.005bdump2000'
|
|
827
|
+
'adump5000ens1.ring', id='whole-data'),
|
|
828
|
+
pytest.param('runs/al4nl5ns400ac1ab5nc139229nb1671l45dt0.005bdump2000'
|
|
829
|
+
'adump5000ens1.ring.bug.restart.lammpstrj',
|
|
830
|
+
'whole', 'bug',
|
|
831
|
+
'al4nl5ns400ac1ab5nc139229nb1671l45dt0.005bdump2000'
|
|
832
|
+
'adump5000ens1.ring', id='whole-restart'),
|
|
833
|
+
])
|
|
834
|
+
def test_parser_reduces_file_path_to_lineage_name(artifact, lineage, group,
|
|
835
|
+
expected):
|
|
836
|
+
"""A project file path reduces to its name before the group keyword."""
|
|
837
|
+
parser = RingCubTwoCrowders(artifact, lineage, group, ispath=True)
|
|
838
|
+
|
|
839
|
+
assert parser.name == expected
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def test_segment_names_its_parents():
|
|
843
|
+
"""A segment gives every parent name, short ones without a trailing .0."""
|
|
844
|
+
parser = RingCubTwoCrowders(
|
|
845
|
+
'al4nl5ns400ac1ab5nc139229nb1671l45dt0.005bdump2000adump5000'
|
|
846
|
+
'ens1.j20.ring.all', 'segment', 'all')
|
|
847
|
+
|
|
848
|
+
assert (parser.segment, parser.whole, parser.ensemble_long,
|
|
849
|
+
parser.ensemble, parser.space) == (
|
|
850
|
+
'al4nl5ns400ac1ab5nc139229nb1671l45dt0.005bdump2000adump5000ens1.j20',
|
|
851
|
+
'al4nl5ns400ac1ab5nc139229nb1671l45dt0.005bdump2000adump5000ens1',
|
|
852
|
+
'al4nl5ns400ac1ab5nc139229nb1671l45dt0.005bdump2000adump5000',
|
|
853
|
+
'al4nl5ns400ac1ab5nc139229nb1671',
|
|
854
|
+
'al4nl5ns400ac1ab5')
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
@pytest.mark.parametrize(
|
|
858
|
+
'parser_class, group, child_lineage, child_name, parent_lineage',
|
|
859
|
+
[
|
|
860
|
+
pytest.param(ChainCyl, 'all', 'segment',
|
|
861
|
+
'N200r10.5lz25.0ac2.0nc1000dt0.005bdump2000adump5000'
|
|
862
|
+
'ens1.j01', 'whole',
|
|
863
|
+
id='ChainCyl-segment-whole'),
|
|
864
|
+
pytest.param(ChainCyl, 'all', 'whole',
|
|
865
|
+
'N200r10.5lz25.0ac2.0nc1000dt0.005bdump2000adump5000'
|
|
866
|
+
'ens1', 'ensemble_long',
|
|
867
|
+
id='ChainCyl-whole-ensemble_long'),
|
|
868
|
+
pytest.param(RingCub, 'all', 'whole',
|
|
869
|
+
'al5nl5ml125ns400ac3nc7735l45dt0.005bdump2000adump5000'
|
|
870
|
+
'ens4.ring.all', 'ensemble_long',
|
|
871
|
+
id='RingCub-whole-ensemble_long'),
|
|
872
|
+
pytest.param(RingCubTwoCrowders, 'all', 'segment',
|
|
873
|
+
'al4nl5ns400ac1ab5nc139229nb1671l45dt0.005bdump2000'
|
|
874
|
+
'adump5000ens1.j20.ring.all', 'whole',
|
|
875
|
+
id='RingCubTwoCrowders-segment-whole'),
|
|
876
|
+
pytest.param(RingCubTwoCrowders, 'bug', 'whole',
|
|
877
|
+
'al4nl5ns400ac1ab5nc139229nb1671l45dt0.005bdump2000'
|
|
878
|
+
'adump5000ens1.ring.bug', 'ensemble_long',
|
|
879
|
+
id='RingCubTwoCrowders-whole-ensemble_long'),
|
|
880
|
+
],
|
|
881
|
+
)
|
|
882
|
+
def test_parser_rebuilt_parent_name_keeps_attributes(
|
|
883
|
+
parser_class, group, child_lineage, child_name, parent_lineage):
|
|
884
|
+
"""A rebuilt parent name parses to its child's attribute values."""
|
|
885
|
+
child = parser_class(child_name, child_lineage, group)
|
|
886
|
+
parent = parser_class(getattr(child, parent_lineage), parent_lineage,
|
|
887
|
+
group)
|
|
888
|
+
values = attrgetter(*parser_class._genealogy_attributes[parent_lineage])
|
|
889
|
+
|
|
890
|
+
# Both sides parse the same digits, so they agree to float64 round-off.
|
|
891
|
+
assert values(parent) == pytest.approx(values(child), rel=1e-12,
|
|
892
|
+
abs=1e-12)
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
@pytest.mark.parametrize(
|
|
896
|
+
'parser_class, group, lineage, artifact, parent_lineage, expected',
|
|
897
|
+
[
|
|
898
|
+
pytest.param(ChainCyl, 'all', 'segment',
|
|
899
|
+
'N200r10.5lz25.0ac2.0nc1000dt0.005bdump2000adump5000'
|
|
900
|
+
'ens1.j01', 'segment',
|
|
901
|
+
'N200r10.5lz25.0ac2.0nc1000dt0.005bdump2000adump5000'
|
|
902
|
+
'ens1.j01', id='ChainCyl-segment'),
|
|
903
|
+
pytest.param(ChainCyl, 'all', 'segment',
|
|
904
|
+
'N200r10.5lz25.0ac2.0nc1000dt0.005bdump2000adump5000'
|
|
905
|
+
'ens1.j01', 'whole',
|
|
906
|
+
'N200r10.5lz25.0ac2.0nc1000dt0.005bdump2000adump5000'
|
|
907
|
+
'ens1', id='ChainCyl-whole'),
|
|
908
|
+
pytest.param(ChainCyl, 'all', 'segment',
|
|
909
|
+
'N200r10.5lz25.0ac2.0nc1000dt0.005bdump2000adump5000'
|
|
910
|
+
'ens1.j01', 'ensemble_long',
|
|
911
|
+
'N200r10.5lz25.0ac2.0nc1000dt0.005bdump2000adump5000',
|
|
912
|
+
id='ChainCyl-ensemble_long'),
|
|
913
|
+
],
|
|
914
|
+
)
|
|
915
|
+
def test_parser_rebuilt_parent_name_is_name_prefix(
|
|
916
|
+
parser_class, group, lineage, artifact, parent_lineage, expected):
|
|
917
|
+
"""A long-form parent name is the artifact name cut after its last key."""
|
|
918
|
+
parser = parser_class(artifact, lineage, group)
|
|
919
|
+
|
|
920
|
+
assert getattr(parser, parent_lineage) == expected
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
# Declarations
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
@pytest.mark.parametrize('parser_class, lineage, expected', [
|
|
927
|
+
pytest.param(ChainCyl, 'whole',
|
|
928
|
+
OrderedDict([('nmon', 'N'), ('dcyl', 'r'), ('lcyl', 'lz'),
|
|
929
|
+
('dcrowd', 'ac'), ('ncrowd', 'nc'),
|
|
930
|
+
('dt', 'dt'), ('bdump', 'bdump'),
|
|
931
|
+
('adump', 'adump'), ('ensemble_id', 'ens')]),
|
|
932
|
+
id='ChainCyl-whole'),
|
|
933
|
+
pytest.param(ChainCyl, 'ensemble',
|
|
934
|
+
OrderedDict([('nmon', 'N'), ('dcyl', 'D'),
|
|
935
|
+
('dcrowd', 'ac'), ('ncrowd', 'nc')]),
|
|
936
|
+
id='ChainCyl-ensemble'),
|
|
937
|
+
])
|
|
938
|
+
def test_genealogy_attributes_follow_pattern_order(parser_class, lineage,
|
|
939
|
+
expected):
|
|
940
|
+
"""Each lineage maps attributes to keywords in the pattern's order."""
|
|
941
|
+
table = parser_class._genealogy_attributes[lineage]
|
|
942
|
+
|
|
943
|
+
assert (type(table), list(table.items())) == (OrderedDict,
|
|
944
|
+
list(expected.items()))
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
@pytest.mark.parametrize('parser_class', [
|
|
948
|
+
pytest.param(ParserBase, id='ParserBase'),
|
|
949
|
+
pytest.param(type('PatternlessParser', (ParserBase,), {}),
|
|
950
|
+
id='subclass-without-patterns'),
|
|
951
|
+
])
|
|
952
|
+
def test_parser_without_patterns_cannot_be_instantiated(parser_class):
|
|
953
|
+
"""A parser class that declares no patterns raises TypeError."""
|
|
954
|
+
with pytest.raises(TypeError, match='declares no lineage patterns'):
|
|
955
|
+
parser_class('N200', 'space', 'bug')
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
# Class-level reads of the declaration
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
def test_pattern_returns_declared_keywords():
|
|
962
|
+
"""pattern gives a lineage's keywords in the order its name writes them."""
|
|
963
|
+
keywords = ChainCyl.pattern('space')
|
|
964
|
+
|
|
965
|
+
# The ChainCyl declaration: 'space': ('N', 'D', 'ac').
|
|
966
|
+
assert keywords == ('N', 'D', 'ac')
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
@pytest.mark.parametrize('lineage, expected', [
|
|
970
|
+
pytest.param(
|
|
971
|
+
'ensemble_long',
|
|
972
|
+
{'nmon', 'dcyl', 'lcyl', 'dcrowd', 'ncrowd', 'dt', 'bdump', 'adump',
|
|
973
|
+
'dmon', 'segment', 'whole', 'ensemble_long', 'ensemble', 'space',
|
|
974
|
+
'rho_bulk_m', 'phi_bulk_m', 'rho_bulk_c', 'phi_bulk_c'},
|
|
975
|
+
id='ensemble_long-with-bulk-values'),
|
|
976
|
+
pytest.param(
|
|
977
|
+
'space',
|
|
978
|
+
{'nmon', 'dcyl', 'dcrowd', 'dmon', 'segment', 'whole',
|
|
979
|
+
'ensemble_long', 'ensemble', 'space'},
|
|
980
|
+
id='space-without-bulk-values'),
|
|
981
|
+
])
|
|
982
|
+
def test_attributes_at_lists_declared_attributes(lineage, expected):
|
|
983
|
+
"""attributes_at gives the parsed, default, lineage, and bulk names."""
|
|
984
|
+
names = ChainCyl.attributes_at(lineage)
|
|
985
|
+
|
|
986
|
+
# From the ChainCyl declaration: pattern attributes, 'dmon' in
|
|
987
|
+
# _defaults, the five lineage names, and at ensemble_long the bulk
|
|
988
|
+
# values of the species 'm' and 'c'.
|
|
989
|
+
assert names == frozenset(expected)
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def test_attributes_at_adds_unlisted_bulk_values(chain_declaration):
|
|
993
|
+
"""attributes_at adds bulk values that _project_attributes omits."""
|
|
994
|
+
chain_cub = type('ChainCub', (ParserBase,), chain_declaration)
|
|
995
|
+
|
|
996
|
+
names = chain_cub.attributes_at('ensemble_long')
|
|
997
|
+
|
|
998
|
+
# The declaration lists only 'dmon' as a project attribute; its species
|
|
999
|
+
# 'm' and 'c' still get bulk values at ensemble_long.
|
|
1000
|
+
assert {'rho_bulk_m', 'phi_bulk_m', 'rho_bulk_c', 'phi_bulk_c'} <= names
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def test_attributes_at_matches_parsed_instance(parse_case, parsed):
|
|
1004
|
+
"""attributes_at names exactly the public attributes a parser sets."""
|
|
1005
|
+
public = {name for name in vars(parsed) if not name.startswith('_')}
|
|
1006
|
+
|
|
1007
|
+
names = parse_case.parser_class.attributes_at(parse_case.lineage)
|
|
1008
|
+
|
|
1009
|
+
assert names == public
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
@pytest.mark.parametrize('method', [
|
|
1013
|
+
pytest.param('pattern', id='pattern'),
|
|
1014
|
+
pytest.param('attributes_at', id='attributes_at'),
|
|
1015
|
+
])
|
|
1016
|
+
def test_class_reads_need_patterns(method):
|
|
1017
|
+
"""pattern and attributes_at raise TypeError on a patternless class."""
|
|
1018
|
+
with pytest.raises(TypeError, match='declares no lineage patterns'):
|
|
1019
|
+
getattr(ParserBase, method)('space')
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
@pytest.mark.parametrize('method', [
|
|
1023
|
+
pytest.param('pattern', id='pattern'),
|
|
1024
|
+
pytest.param('attributes_at', id='attributes_at'),
|
|
1025
|
+
])
|
|
1026
|
+
def test_class_reads_reject_unknown_lineage(method):
|
|
1027
|
+
"""pattern and attributes_at raise ValueError on an unknown lineage."""
|
|
1028
|
+
with pytest.raises(ValueError, match='invalid option'):
|
|
1029
|
+
getattr(ChainCyl, method)('galaxy')
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
def test_declared_parser_parses_its_names(chain_declaration):
|
|
1033
|
+
"""A class built from a valid declaration parses names of its grammar."""
|
|
1034
|
+
chain_cub = type('ChainCub', (ParserBase,), chain_declaration)
|
|
1035
|
+
|
|
1036
|
+
parser = chain_cub('N100ac2.0nc50l10.0ens1.bug', 'whole', 'bug')
|
|
1037
|
+
|
|
1038
|
+
assert (parser.nmon, parser.dcrowd, parser.ncrowd, parser.lcube,
|
|
1039
|
+
parser.ensemble_long, parser.ensemble, parser.space) == (
|
|
1040
|
+
100, 2.0, 50, 20.0, 'N100ac2.0nc50l10.0', 'N100ac2nc50',
|
|
1041
|
+
'N100ac2')
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
@pytest.mark.parametrize('override, match', [
|
|
1045
|
+
pytest.param({'_geometry': None}, 'must declare', id='no-geometry'),
|
|
1046
|
+
pytest.param({'_dump_keywords': None}, "'_dump_keywords'",
|
|
1047
|
+
id='no-dump-keywords'),
|
|
1048
|
+
pytest.param({'_dump_keywords': {'bug': 'bdump'}},
|
|
1049
|
+
"_dump_keywords must have one entry per group",
|
|
1050
|
+
id='group-no-dump-keyword'),
|
|
1051
|
+
pytest.param(
|
|
1052
|
+
{'_dump_keywords': {'bug': 'bdump', 'all': 'adump',
|
|
1053
|
+
'nucleoid': 'ndump'}},
|
|
1054
|
+
"_dump_keywords must have one entry per group",
|
|
1055
|
+
id='dump-keyword-of-undeclared-group'),
|
|
1056
|
+
pytest.param(
|
|
1057
|
+
{'_dump_keywords': {'bug': 'tdump', 'all': 'adump'}},
|
|
1058
|
+
"the dump keyword 'tdump' of group 'bug' is not set by the "
|
|
1059
|
+
"'segment' pattern or '_defaults'",
|
|
1060
|
+
id='dump-keyword-not-set'),
|
|
1061
|
+
pytest.param(
|
|
1062
|
+
{'_patterns': {'segment': ('N', 'ac', 'nc', 'l', 'ens', 'j'),
|
|
1063
|
+
'whole': ('N', 'ac', 'nc', 'l', 'ens'),
|
|
1064
|
+
'ensemble_long': ('N', 'ac', 'nc', 'l'),
|
|
1065
|
+
'ensemble': ('N', 'ac', 'nc')}},
|
|
1066
|
+
"_patterns must have one entry per lineage", id='lineage-no-pattern'),
|
|
1067
|
+
pytest.param(
|
|
1068
|
+
{'_project_attributes': {'segment': [], 'whole': []}},
|
|
1069
|
+
"_project_attributes must have one entry per lineage",
|
|
1070
|
+
id='lineage-no-project-attributes'),
|
|
1071
|
+
pytest.param(
|
|
1072
|
+
{'_patterns': {'segment': ('N', 'ac', 'nc', 'l', 'ens', 'j'),
|
|
1073
|
+
'whole': ('N', 'ac', 'nc', 'l', 'ens'),
|
|
1074
|
+
'ensemble_long': ('N', 'ac', 'nc', 'l'),
|
|
1075
|
+
'ensemble': ('N', 'ac', 'nc'),
|
|
1076
|
+
'space': ('N', 'xx')}},
|
|
1077
|
+
"keyword 'xx' in the 'space' pattern has no KeywordSpec",
|
|
1078
|
+
id='undeclared-keyword'),
|
|
1079
|
+
pytest.param(
|
|
1080
|
+
{'_patterns': {'segment': ('N', 'ac', 'nc', 'l', 'ens', 'j'),
|
|
1081
|
+
'whole': ('N', 'ac', 'nc', 'l', 'ens'),
|
|
1082
|
+
'ensemble_long': ('N', 'ac', 'nc', 'l'),
|
|
1083
|
+
'ensemble': ('N', 'ac', 'nc'),
|
|
1084
|
+
'space': ('N', 'ac', 'N')}},
|
|
1085
|
+
"attribute 'nmon' appears more than once in the 'space' pattern",
|
|
1086
|
+
id='duplicate-attribute'),
|
|
1087
|
+
pytest.param(
|
|
1088
|
+
{'_totals': {'all': ('m', 'x')}},
|
|
1089
|
+
"total 'all' sums 'x', which is not a declared species",
|
|
1090
|
+
id='total-of-undeclared-species'),
|
|
1091
|
+
pytest.param(
|
|
1092
|
+
{'_patterns': {'segment': ('N', 'ac', 'nc', 'l', 'ens', 'j'),
|
|
1093
|
+
'whole': ('N', 'ac', 'nc', 'l', 'ens'),
|
|
1094
|
+
'ensemble_long': ('N', 'ac', 'nc'),
|
|
1095
|
+
'ensemble': ('N', 'ac', 'nc'),
|
|
1096
|
+
'space': ('N', 'ac')}},
|
|
1097
|
+
"the bulk values need 'lcube', which the 'ensemble_long' pattern",
|
|
1098
|
+
id='bulk-input-not-parsed'),
|
|
1099
|
+
])
|
|
1100
|
+
def test_invalid_declaration_raises_type_error_at_class_creation(
|
|
1101
|
+
chain_declaration, override, match):
|
|
1102
|
+
"""An incomplete or inconsistent declaration fails when it is defined."""
|
|
1103
|
+
with pytest.raises(TypeError, match=re.escape(match)):
|
|
1104
|
+
type('BadParser', (ParserBase,), {**chain_declaration, **override})
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
# KeywordSpec
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
@pytest.mark.parametrize('spec, token, expected', [
|
|
1111
|
+
pytest.param(KeywordSpec('nmon', int), '200', 200, id='int'),
|
|
1112
|
+
pytest.param(KeywordSpec('ncrowd', int), '1000.', 1000,
|
|
1113
|
+
id='int-trailing-dot'),
|
|
1114
|
+
pytest.param(KeywordSpec('dcrowd', float), '2.5', 2.5, id='float'),
|
|
1115
|
+
pytest.param(KeywordSpec('lcube', float, scale=2.0), '25.0', 50.0,
|
|
1116
|
+
id='half-side'),
|
|
1117
|
+
pytest.param(KeywordSpec('dcyl', float, scale=2.0, offset=-1.0), '10.5',
|
|
1118
|
+
20.0, id='wall-radius'),
|
|
1119
|
+
])
|
|
1120
|
+
def test_keyword_spec_converts_token(spec, token, expected):
|
|
1121
|
+
"""A token converts to `scale * value + offset`, typed as declared."""
|
|
1122
|
+
actual = spec.parse(token)
|
|
1123
|
+
|
|
1124
|
+
# The conversions are exact in float64 for these tokens.
|
|
1125
|
+
assert (actual, type(actual)) == (pytest.approx(expected, rel=1e-12),
|
|
1126
|
+
type(expected))
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
@pytest.mark.parametrize('spec, token, match', [
|
|
1130
|
+
pytest.param(KeywordSpec('dcrowd', float), '.', "'.' is not a number",
|
|
1131
|
+
id='dot'),
|
|
1132
|
+
pytest.param(KeywordSpec('dcrowd', float), '', "'' is not a number",
|
|
1133
|
+
id='empty'),
|
|
1134
|
+
pytest.param(KeywordSpec('nmon', int), '1.5', "'1.5' is not an integer",
|
|
1135
|
+
id='non-integral'),
|
|
1136
|
+
])
|
|
1137
|
+
def test_keyword_spec_rejects_bad_token(spec, token, match):
|
|
1138
|
+
"""A token that is not a number, or not whole for int, is rejected."""
|
|
1139
|
+
with pytest.raises(ValueError, match=re.escape(match)):
|
|
1140
|
+
spec.parse(token)
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
@pytest.mark.parametrize('kwargs, match', [
|
|
1144
|
+
pytest.param({'attr': 'name', 'kind': str}, "must be int or float",
|
|
1145
|
+
id='str-kind'),
|
|
1146
|
+
pytest.param({'attr': 'nmon', 'kind': int, 'offset': 1.0},
|
|
1147
|
+
"only a float keyword", id='int-with-offset'),
|
|
1148
|
+
])
|
|
1149
|
+
def test_keyword_spec_rejects_bad_declaration(kwargs, match):
|
|
1150
|
+
"""A KeywordSpec of another type, or an int with a conversion, fails."""
|
|
1151
|
+
with pytest.raises(TypeError, match=match):
|
|
1152
|
+
KeywordSpec(**kwargs)
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
# Study parsers that left the module
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
@pytest.mark.parametrize('name, message', [
|
|
1159
|
+
pytest.param('HnsCub', 'projects/hns_cub/parser.py', id='HnsCub'),
|
|
1160
|
+
pytest.param('HnsCyl', 'projects/hns_cyl/parser.py', id='HnsCyl'),
|
|
1161
|
+
pytest.param('TransFociCub', 'projects/trans_foci_cub/parser.py',
|
|
1162
|
+
id='TransFociCub'),
|
|
1163
|
+
pytest.param('TransFociCyl', 'projects/trans_foci_cyl/parser.py',
|
|
1164
|
+
id='TransFociCyl'),
|
|
1165
|
+
pytest.param('SumRuleCyl', 'removed from PolyPhys in 1.0.0',
|
|
1166
|
+
id='SumRuleCyl'),
|
|
1167
|
+
pytest.param('SumRuleCubHeteroRing', 'removed from PolyPhys in 1.0.0',
|
|
1168
|
+
id='SumRuleCubHeteroRing'),
|
|
1169
|
+
pytest.param('SumRuleCubHeteroLinear', 'removed from PolyPhys in 1.0.0',
|
|
1170
|
+
id='SumRuleCubHeteroLinear'),
|
|
1171
|
+
pytest.param('SumRuleCubHeteroRingBiDisperse',
|
|
1172
|
+
'removed from PolyPhys in 1.0.0',
|
|
1173
|
+
id='SumRuleCubHeteroRingBiDisperse'),
|
|
1174
|
+
pytest.param('TwoMonDepCub', 'removed from PolyPhys in 1.0.0',
|
|
1175
|
+
id='TwoMonDepCub'),
|
|
1176
|
+
])
|
|
1177
|
+
def test_moved_study_parser_import_raises_import_error(name, message):
|
|
1178
|
+
"""Importing a study parser that left the module says where it went."""
|
|
1179
|
+
module = importlib.import_module('polyphys.manage.parser')
|
|
1180
|
+
|
|
1181
|
+
with pytest.raises(ImportError, match=re.escape(message)):
|
|
1182
|
+
getattr(module, name)
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
def test_unknown_name_raises_attribute_error():
|
|
1186
|
+
"""A name the module never defined raises AttributeError, as usual."""
|
|
1187
|
+
module = importlib.import_module('polyphys.manage.parser')
|
|
1188
|
+
|
|
1189
|
+
with pytest.raises(AttributeError, match="has no attribute 'NoSuchName'"):
|
|
1190
|
+
getattr(module, 'NoSuchName')
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
# Generated names
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
@pytest.mark.parametrize('lineage', [
|
|
1197
|
+
pytest.param('segment', id='segment'),
|
|
1198
|
+
pytest.param('whole', id='whole'),
|
|
1199
|
+
pytest.param('ensemble_long', id='ensemble_long'),
|
|
1200
|
+
pytest.param('ensemble', id='ensemble'),
|
|
1201
|
+
pytest.param('space', id='space'),
|
|
1202
|
+
])
|
|
1203
|
+
@pytest.mark.parametrize('parser_class', [
|
|
1204
|
+
pytest.param(getattr(synthetic, name), id=name)
|
|
1205
|
+
for name in synthetic.__all__
|
|
1206
|
+
])
|
|
1207
|
+
@settings(max_examples=50, deadline=None)
|
|
1208
|
+
@given(data=st.data())
|
|
1209
|
+
def test_generated_name_parses_to_its_values_and_back(parser_class, lineage,
|
|
1210
|
+
data):
|
|
1211
|
+
"""A name built from a parser's own pattern parses to its values and
|
|
1212
|
+
rebuilds to itself: a round-trip property, with no reference value."""
|
|
1213
|
+
name, values = data.draw(lineage_names(parser_class, lineage))
|
|
1214
|
+
|
|
1215
|
+
parsed = parser_class(name, lineage, parser_class._groups[0])
|
|
1216
|
+
|
|
1217
|
+
assert getattr(parsed, lineage) == name
|
|
1218
|
+
# The parser converts in exact decimal arithmetic, the expectation in
|
|
1219
|
+
# float64: a few ulp apart; atol covers a value converted to zero.
|
|
1220
|
+
assert {attr: getattr(parsed, attr)
|
|
1221
|
+
for attr in stored_values(parser_class, values)} == pytest.approx(
|
|
1222
|
+
stored_values(parser_class, values), rel=1e-12, abs=1e-12)
|