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,1264 @@
|
|
|
1
|
+
"""\
|
|
2
|
+
==========================================================
|
|
3
|
+
:mod:`polyphys.manage.parser`
|
|
4
|
+
==========================================================
|
|
5
|
+
|
|
6
|
+
This :mod:`~polyphys.manage.parser` module turns the name of a simulation
|
|
7
|
+
artifact into typed attributes. A name such as
|
|
8
|
+
``N200r10.5lz25.0ac2.0nc1000dt0.005bdump2000adump5000ens1`` is a
|
|
9
|
+
run of keyword-value pairs; each project has its own keywords, and each level
|
|
10
|
+
of the lineage ``segment``, ``whole``, ``ensemble_long``, ``ensemble``, and
|
|
11
|
+
``space`` uses its own subset of them, in its own order.
|
|
12
|
+
|
|
13
|
+
A parser class is a declaration. It states the project's geometry, polymer
|
|
14
|
+
topology, and particle groups; one :class:`KeywordSpec` per filename keyword;
|
|
15
|
+
the keyword pattern of each lineage; fixed values; and the particle species
|
|
16
|
+
whose bulk number density and volume fraction the parser computes.
|
|
17
|
+
:class:`ParserBase` does the parsing, builds the parent lineage names, and
|
|
18
|
+
computes the bulk values.
|
|
19
|
+
|
|
20
|
+
Contents
|
|
21
|
+
========
|
|
22
|
+
|
|
23
|
+
- :class:`KeywordSpec` and :class:`Species`, the two building blocks of a
|
|
24
|
+
declaration.
|
|
25
|
+
- :class:`ParserBase`, the base class every parser derives from.
|
|
26
|
+
|
|
27
|
+
The module ships no study's parser: a study declares its own, as the
|
|
28
|
+
"Studies" page shows.
|
|
29
|
+
|
|
30
|
+
.. versionchanged:: 1.0.0
|
|
31
|
+
The parsers of nine studies left this module. ``TransFociCyl``,
|
|
32
|
+
``TransFociCub``, ``HnsCub``, and ``HnsCyl`` are in the ``projects/``
|
|
33
|
+
folder of the PolyPhys repository, which is not installed;
|
|
34
|
+
``SumRuleCyl``, ``SumRuleCubHeteroRing``, ``SumRuleCubHeteroLinear``,
|
|
35
|
+
``SumRuleCubHeteroRingBiDisperse``, and ``TwoMonDepCub`` were removed.
|
|
36
|
+
Importing any of the nine from here raises :exc:`ImportError` that says
|
|
37
|
+
where it went.
|
|
38
|
+
|
|
39
|
+
Dependencies
|
|
40
|
+
============
|
|
41
|
+
- Python standard library: `os`, `re`, `collections`, `dataclasses`,
|
|
42
|
+
`decimal`, `pathlib`, `typing`.
|
|
43
|
+
- :mod:`polyphys.manage.utils`: :func:`~polyphys.manage.utils.invalid_keyword`
|
|
44
|
+
and the number-density and volume-fraction functions.
|
|
45
|
+
- :mod:`polyphys.manage.types`: domain type aliases such as
|
|
46
|
+
:py:obj:`~.LineageT`.
|
|
47
|
+
|
|
48
|
+
Examples
|
|
49
|
+
========
|
|
50
|
+
>>> from polyphys.tests.synthetic import ChainCyl
|
|
51
|
+
>>> artifact = ChainCyl('N200D10.0ac2.0nc0', 'ensemble', 'all')
|
|
52
|
+
>>> print(artifact.dcrowd)
|
|
53
|
+
2.0
|
|
54
|
+
|
|
55
|
+
>>> import polyphys.manage.parser as parser
|
|
56
|
+
>>> parser.HnsCub # doctest: +ELLIPSIS
|
|
57
|
+
Traceback (most recent call last):
|
|
58
|
+
...
|
|
59
|
+
ImportError: HnsCub moved out of PolyPhys in 1.0.0, to projects/hns_cub/...
|
|
60
|
+
|
|
61
|
+
Notes
|
|
62
|
+
=====
|
|
63
|
+
The parsed and computed attributes are set on the instance at construction
|
|
64
|
+
with :func:`setattr`, so their names come from the declaration rather than
|
|
65
|
+
from the class body; :attr:`ParserBase.attributes` lists the ones a given
|
|
66
|
+
lineage sets.
|
|
67
|
+
"""
|
|
68
|
+
import os
|
|
69
|
+
import re
|
|
70
|
+
from collections import OrderedDict
|
|
71
|
+
from dataclasses import dataclass
|
|
72
|
+
from decimal import Context, Decimal
|
|
73
|
+
from pathlib import Path
|
|
74
|
+
from typing import ClassVar, Optional, Self
|
|
75
|
+
from .utils import (
|
|
76
|
+
invalid_keyword,
|
|
77
|
+
number_density_cube,
|
|
78
|
+
number_density_cylinder,
|
|
79
|
+
volume_fraction_cube,
|
|
80
|
+
volume_fraction_cylinder
|
|
81
|
+
)
|
|
82
|
+
from .types import LineageT, TopologyT, GroupT, GeometryT, Pathish
|
|
83
|
+
|
|
84
|
+
__all__ = [
|
|
85
|
+
'KeywordSpec',
|
|
86
|
+
'ParserBase',
|
|
87
|
+
'Species',
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
# The study parsers that left this module in 1.0.0: each published one,
|
|
91
|
+
# with its folder under projects/ in the PolyPhys repository, and each
|
|
92
|
+
# unpublished one, with None.
|
|
93
|
+
_MOVED_STUDIES: dict[str, str | None] = {
|
|
94
|
+
'HnsCub': 'hns_cub',
|
|
95
|
+
'HnsCyl': 'hns_cyl',
|
|
96
|
+
'TransFociCub': 'trans_foci_cub',
|
|
97
|
+
'TransFociCyl': 'trans_foci_cyl',
|
|
98
|
+
'SumRuleCubHeteroLinear': None,
|
|
99
|
+
'SumRuleCubHeteroRing': None,
|
|
100
|
+
'SumRuleCubHeteroRingBiDisperse': None,
|
|
101
|
+
'SumRuleCyl': None,
|
|
102
|
+
'TwoMonDepCub': None,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# A keyword is a run of letters (or hyphens); its value is the text up to the
|
|
106
|
+
# next keyword.
|
|
107
|
+
_TOKEN_RE = re.compile(r"([a-zA-Z\-]+)")
|
|
108
|
+
|
|
109
|
+
# A file extension, as ``ispath=True`` strips it: a dot and a run of letters
|
|
110
|
+
# and digits with at least one letter, such as ``.csv`` or ``.h5``, so that the
|
|
111
|
+
# fractional part of a trailing value, such as ``.5`` in ``ac1.5``, stays in
|
|
112
|
+
# the name.
|
|
113
|
+
_EXTENSION_RE = re.compile(r"\.[a-zA-Z0-9]*[a-zA-Z][a-zA-Z0-9]*")
|
|
114
|
+
|
|
115
|
+
# The exact decimal arithmetic of a keyword conversion; 50 digits hold the
|
|
116
|
+
# product of any filename value with a scale and an offset without rounding.
|
|
117
|
+
_DECIMAL_CONTEXT = Context(prec=50)
|
|
118
|
+
|
|
119
|
+
# The lineages whose names carry the box size, and so the bulk values.
|
|
120
|
+
_BULK_LINEAGES: tuple[LineageT, ...] = ('segment', 'whole', 'ensemble_long')
|
|
121
|
+
|
|
122
|
+
# The attributes that size the simulation box in each geometry.
|
|
123
|
+
_BOX_ATTRIBUTES: dict[GeometryT, tuple[str, ...]] = {
|
|
124
|
+
'cubic': ('lcube',),
|
|
125
|
+
'cylindrical': ('lcyl', 'dcyl'),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _format_value(value: int | float) -> str:
|
|
130
|
+
"""
|
|
131
|
+
Return the text of a keyword value in a short lineage name.
|
|
132
|
+
|
|
133
|
+
A float that is a whole number prints as an integer; any other float
|
|
134
|
+
prints as :func:`repr`, Python's shortest form that reads back as the
|
|
135
|
+
same float, so two different values never print alike. An `int` prints
|
|
136
|
+
unchanged. The cost is :math:`O(1)` per value.
|
|
137
|
+
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
value : int or float
|
|
141
|
+
A parsed keyword value.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
str
|
|
146
|
+
The value as it appears after its keyword in an ``ensemble`` or
|
|
147
|
+
``space`` name.
|
|
148
|
+
|
|
149
|
+
Examples
|
|
150
|
+
--------
|
|
151
|
+
>>> _format_value(2.0), _format_value(0.0), _format_value(40.0)
|
|
152
|
+
('2', '0', '40')
|
|
153
|
+
>>> _format_value(1.5), _format_value(1.000001), _format_value(200)
|
|
154
|
+
('1.5', '1.000001', '200')
|
|
155
|
+
"""
|
|
156
|
+
if isinstance(value, float) and value.is_integer():
|
|
157
|
+
return str(int(value))
|
|
158
|
+
return repr(value)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass(frozen=True)
|
|
162
|
+
class KeywordSpec:
|
|
163
|
+
"""
|
|
164
|
+
How a parser reads the value of one filename keyword.
|
|
165
|
+
|
|
166
|
+
The value :math:`x` that follows the keyword in a name is stored in the
|
|
167
|
+
attribute `attr` as :math:`y = s x + c`, with scale :math:`s` and offset
|
|
168
|
+
:math:`c`; the default :math:`s = 1`, :math:`c = 0` stores the value as
|
|
169
|
+
written.
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
attr : str
|
|
174
|
+
Name of the parser attribute that stores the value.
|
|
175
|
+
kind : {int, float}
|
|
176
|
+
Type of the stored value.
|
|
177
|
+
scale : float, default 1.0
|
|
178
|
+
Scale :math:`s` of the conversion. Only a `float` keyword may set it.
|
|
179
|
+
offset : float, default 0.0
|
|
180
|
+
Offset :math:`c` of the conversion. Only a `float` keyword may set
|
|
181
|
+
it.
|
|
182
|
+
|
|
183
|
+
Raises
|
|
184
|
+
------
|
|
185
|
+
TypeError
|
|
186
|
+
If `kind` is neither `int` nor `float`, or if an `int` keyword sets
|
|
187
|
+
`scale` or `offset`.
|
|
188
|
+
|
|
189
|
+
Examples
|
|
190
|
+
--------
|
|
191
|
+
The keyword ``N`` is a number of monomers:
|
|
192
|
+
|
|
193
|
+
>>> KeywordSpec('nmon', int).parse('200')
|
|
194
|
+
200
|
|
195
|
+
|
|
196
|
+
The keyword ``r`` is the radius of the cylinder on which the centers of
|
|
197
|
+
the wall beads of size 1 lie, so the cylinder size is :math:`D = 2r - 1`:
|
|
198
|
+
|
|
199
|
+
>>> spec = KeywordSpec('dcyl', float, scale=2.0, offset=-1.0)
|
|
200
|
+
>>> spec.parse('10.5')
|
|
201
|
+
20.0
|
|
202
|
+
"""
|
|
203
|
+
attr: str
|
|
204
|
+
kind: type[int] | type[float]
|
|
205
|
+
scale: float = 1.0
|
|
206
|
+
offset: float = 0.0
|
|
207
|
+
|
|
208
|
+
def __post_init__(self) -> None:
|
|
209
|
+
if self.kind not in (int, float):
|
|
210
|
+
raise TypeError(
|
|
211
|
+
f"KeywordSpec {self.attr!r}: 'kind' must be int or float, "
|
|
212
|
+
f"not {self.kind!r}.")
|
|
213
|
+
if self.kind is int and not self.is_identity:
|
|
214
|
+
raise TypeError(
|
|
215
|
+
f"KeywordSpec {self.attr!r}: only a float keyword can have a "
|
|
216
|
+
"'scale' or an 'offset'.")
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def is_identity(self) -> bool:
|
|
220
|
+
"""
|
|
221
|
+
Whether the conversion stores the value as written.
|
|
222
|
+
"""
|
|
223
|
+
return self.scale == 1.0 and self.offset == 0.0
|
|
224
|
+
|
|
225
|
+
def parse(self, token: str) -> int | float:
|
|
226
|
+
"""
|
|
227
|
+
Convert the text that follows a keyword into the stored value.
|
|
228
|
+
|
|
229
|
+
Parameters
|
|
230
|
+
----------
|
|
231
|
+
token : str
|
|
232
|
+
The text between the keyword and the next keyword, such as
|
|
233
|
+
``'10.5'`` or ``'1.'``.
|
|
234
|
+
|
|
235
|
+
Returns
|
|
236
|
+
-------
|
|
237
|
+
int or float
|
|
238
|
+
The value as an `int` for an `int` keyword, else
|
|
239
|
+
:math:`s x + c`. A conversion that is not the identity is done in
|
|
240
|
+
exact decimal arithmetic on the text of `token` and of
|
|
241
|
+
:func:`repr` of :math:`s` and :math:`c`, then rounded once to the
|
|
242
|
+
nearest float, so ``'4.1'`` with :math:`s = 2`, :math:`c = -1`
|
|
243
|
+
gives ``7.2`` rather than the binary result
|
|
244
|
+
``7.199999999999999``. The cost is :math:`O(1)` per value.
|
|
245
|
+
|
|
246
|
+
Raises
|
|
247
|
+
------
|
|
248
|
+
ValueError
|
|
249
|
+
If `token` is not a number, or if the keyword is an `int` keyword
|
|
250
|
+
and `token` is not a whole number, such as ``'1.5'``.
|
|
251
|
+
|
|
252
|
+
Examples
|
|
253
|
+
--------
|
|
254
|
+
>>> KeywordSpec('ncrowd', int).parse('1000.')
|
|
255
|
+
1000
|
|
256
|
+
>>> KeywordSpec('lcube', float, scale=2.0).parse('25.0')
|
|
257
|
+
50.0
|
|
258
|
+
>>> KeywordSpec('dcyl', float, scale=2.0, offset=-1.0).parse('4.1')
|
|
259
|
+
7.2
|
|
260
|
+
"""
|
|
261
|
+
try:
|
|
262
|
+
value = float(token)
|
|
263
|
+
except ValueError:
|
|
264
|
+
raise ValueError(f"{token!r} is not a number") from None
|
|
265
|
+
if self.kind is int:
|
|
266
|
+
if not value.is_integer():
|
|
267
|
+
raise ValueError(f"{token!r} is not an integer")
|
|
268
|
+
return int(value)
|
|
269
|
+
if self.is_identity:
|
|
270
|
+
return value
|
|
271
|
+
context = _DECIMAL_CONTEXT
|
|
272
|
+
exact = context.add(
|
|
273
|
+
context.multiply(Decimal(token), Decimal(repr(self.scale))),
|
|
274
|
+
Decimal(repr(self.offset)))
|
|
275
|
+
return float(exact)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@dataclass(frozen=True)
|
|
279
|
+
class Species:
|
|
280
|
+
"""
|
|
281
|
+
A particle species whose bulk number density and volume fraction a parser
|
|
282
|
+
computes.
|
|
283
|
+
|
|
284
|
+
At the ``segment``, ``whole``, and ``ensemble_long`` lineages, a parser
|
|
285
|
+
sets ``rho_bulk_<suffix>``, the number of particles of the species per
|
|
286
|
+
unit volume of the box, and ``phi_bulk_<suffix>``, the fraction of the
|
|
287
|
+
box volume they fill.
|
|
288
|
+
|
|
289
|
+
Parameters
|
|
290
|
+
----------
|
|
291
|
+
suffix : str
|
|
292
|
+
Suffix of the two attribute names, such as ``'m'`` for monomers or
|
|
293
|
+
``'c'`` for crowders.
|
|
294
|
+
number : str
|
|
295
|
+
Name of the attribute that holds the number of particles.
|
|
296
|
+
size : str
|
|
297
|
+
Name of the attribute that holds the particle diameter.
|
|
298
|
+
|
|
299
|
+
Examples
|
|
300
|
+
--------
|
|
301
|
+
>>> crowders = Species('c', 'ncrowd', 'dcrowd')
|
|
302
|
+
>>> crowders.suffix
|
|
303
|
+
'c'
|
|
304
|
+
"""
|
|
305
|
+
suffix: str
|
|
306
|
+
number: str
|
|
307
|
+
size: str
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class ParserBase:
|
|
311
|
+
"""
|
|
312
|
+
Base parser class for extracting information from filenames or file paths
|
|
313
|
+
in a structured project.
|
|
314
|
+
|
|
315
|
+
A subclass declares a project's filename grammar and physics through the
|
|
316
|
+
class attributes listed under "Declaration"; this class parses the name,
|
|
317
|
+
builds the names of the parent lineages, and computes the bulk number
|
|
318
|
+
densities and volume fractions. A declaration is checked when the
|
|
319
|
+
subclass is created; a class whose `_patterns` is ``None`` is abstract and
|
|
320
|
+
cannot be instantiated. :meth:`from_name` constructs a parser from a name
|
|
321
|
+
token instead of a path, with ``filepath`` and ``ext`` set to ``None``.
|
|
322
|
+
|
|
323
|
+
Parameters
|
|
324
|
+
----------
|
|
325
|
+
artifact : Pathish
|
|
326
|
+
The artifact to be parsed: a name, or, with ``ispath=True``, a
|
|
327
|
+
relative or absolute file path (string or Path).
|
|
328
|
+
lineage : :py:obj:`~.LineageT`
|
|
329
|
+
The lineage of the artifact, specifying the hierarchical level within
|
|
330
|
+
the project. Must be one of: ``'segment'``, ``'whole'``,
|
|
331
|
+
``'ensemble_long'``, ``'ensemble'``, or ``'space'``.
|
|
332
|
+
group : :py:obj:`~.GroupT`
|
|
333
|
+
The particle group the artifact describes; one of the subclass's
|
|
334
|
+
`groups`.
|
|
335
|
+
ispath : bool, default False
|
|
336
|
+
Whether `artifact` is a file path. A path is expanded (``~`` and
|
|
337
|
+
environment variables) and resolved, and its stem is parsed.
|
|
338
|
+
|
|
339
|
+
Attributes
|
|
340
|
+
----------
|
|
341
|
+
filepath : pathlib.Path or None
|
|
342
|
+
The resolved absolute directory that holds `artifact` if it is a
|
|
343
|
+
file path, or ``None`` if it is a name.
|
|
344
|
+
filename : str
|
|
345
|
+
The filename extracted from `artifact`, or `artifact` itself.
|
|
346
|
+
ext : str or None
|
|
347
|
+
The file extension, including the leading dot, if `artifact` is a
|
|
348
|
+
file path: :attr:`pathlib.PurePath.suffix`, which is ``''`` for a
|
|
349
|
+
file with no extension. ``None`` if `artifact` is a name.
|
|
350
|
+
name : str
|
|
351
|
+
The lineage name: `filename` without its extension, cut before
|
|
352
|
+
``.<group>`` (``segment`` and ``whole``) and before the first
|
|
353
|
+
hyphen.
|
|
354
|
+
group : :py:obj:`~.GroupT`
|
|
355
|
+
Particle group type in the project.
|
|
356
|
+
dump_keyword : str
|
|
357
|
+
The attribute that holds the dump period, in time steps, of the
|
|
358
|
+
`group` trajectory: ``dump_keywords[group]``.
|
|
359
|
+
lineage : :py:obj:`~.LineageT`
|
|
360
|
+
The lineage of the artifact.
|
|
361
|
+
project_name : str
|
|
362
|
+
The name of the parser subclass.
|
|
363
|
+
lineage_genealogy : list of :py:obj:`~.LineageT`
|
|
364
|
+
The artifact's `lineage` and its parent lineages.
|
|
365
|
+
lineage_attributes : list of str
|
|
366
|
+
The attributes parsed from `name`, in pattern order.
|
|
367
|
+
physical_attributes : list of str
|
|
368
|
+
The fixed and computed attributes listed for the artifact's
|
|
369
|
+
`lineage`.
|
|
370
|
+
attributes : list of str
|
|
371
|
+
`lineage_attributes` followed by `physical_attributes`.
|
|
372
|
+
segment : str
|
|
373
|
+
The artifact's name at the ``segment`` lineage if it is in
|
|
374
|
+
`lineage_genealogy`, else ``'N/A'``; cut from `name`, with no
|
|
375
|
+
topology suffix.
|
|
376
|
+
whole : str
|
|
377
|
+
As `segment`, at the ``whole`` lineage.
|
|
378
|
+
ensemble_long : str
|
|
379
|
+
As `segment`, at the ``ensemble_long`` lineage.
|
|
380
|
+
ensemble : str
|
|
381
|
+
The artifact's name at the ``ensemble`` lineage if it is in
|
|
382
|
+
`lineage_genealogy`, else ``'N/A'``; built from the parsed values,
|
|
383
|
+
so a whole-number float prints without ``.0``.
|
|
384
|
+
space : str
|
|
385
|
+
As `ensemble`, at the ``space`` lineage.
|
|
386
|
+
|
|
387
|
+
Class Attributes
|
|
388
|
+
----------------
|
|
389
|
+
lineages : list of :py:obj:`~.LineageT`
|
|
390
|
+
List of valid lineage types.
|
|
391
|
+
genealogy : dict of :py:obj:`~.LineageT` to list of :py:obj:`~.LineageT`
|
|
392
|
+
Each lineage mapped to itself and its parent lineages.
|
|
393
|
+
geometry : :py:obj:`~.GeometryT`
|
|
394
|
+
Specifies the geometry of the system.
|
|
395
|
+
topology : :py:obj:`~.TopologyT`
|
|
396
|
+
Specifies how particles are connected (or not) in the system.
|
|
397
|
+
groups : list of :py:obj:`~.GroupT`
|
|
398
|
+
Allowed particle groups in the system.
|
|
399
|
+
dump_keywords : dict of :py:obj:`~.GroupT` to str
|
|
400
|
+
Each group mapped to the attribute that holds the dump period, in
|
|
401
|
+
time steps, of its trajectory.
|
|
402
|
+
genealogy_attributes : dict of :py:obj:`~.LineageT` to OrderedDict
|
|
403
|
+
Each lineage mapped to an OrderedDict of attribute names and their
|
|
404
|
+
filename keywords, in pattern order; built from `_keywords` and
|
|
405
|
+
`_patterns`.
|
|
406
|
+
project_attributes : dict of :py:obj:`~.LineageT` to list of str
|
|
407
|
+
Each lineage mapped to the attributes that are fixed or computed
|
|
408
|
+
rather than parsed.
|
|
409
|
+
|
|
410
|
+
Declaration
|
|
411
|
+
-----------
|
|
412
|
+
_geometry : :py:obj:`~.GeometryT`
|
|
413
|
+
``'cubic'`` or ``'cylindrical'``. A cubic project with species
|
|
414
|
+
declares the attribute ``lcube``; a cylindrical one ``lcyl`` and
|
|
415
|
+
``dcyl``.
|
|
416
|
+
_topology : :py:obj:`~.TopologyT`
|
|
417
|
+
The polymer topology.
|
|
418
|
+
_groups : list of :py:obj:`~.GroupT`
|
|
419
|
+
The particle groups.
|
|
420
|
+
_dump_keywords : dict of :py:obj:`~.GroupT` to str
|
|
421
|
+
As `dump_keywords` above, one entry per group. Each attribute is set
|
|
422
|
+
at ``segment``, ``whole``, and ``ensemble_long``, by the pattern or
|
|
423
|
+
by `_defaults`.
|
|
424
|
+
_keywords : dict of str to :class:`KeywordSpec`
|
|
425
|
+
Each filename keyword mapped to how its value is read.
|
|
426
|
+
_patterns : dict of :py:obj:`~.LineageT` to tuple of str
|
|
427
|
+
Each lineage mapped to its keywords in name order.
|
|
428
|
+
_project_attributes : dict of :py:obj:`~.LineageT` to list of str
|
|
429
|
+
As `project_attributes` above.
|
|
430
|
+
_defaults : dict of str to float, optional
|
|
431
|
+
Attributes set at every lineage before parsing, such as a fixed
|
|
432
|
+
monomer size.
|
|
433
|
+
_species : tuple of :class:`Species`, optional
|
|
434
|
+
The species whose bulk values are computed.
|
|
435
|
+
_totals : dict of str to tuple of str, optional
|
|
436
|
+
A suffix mapped to species suffixes; ``rho_bulk_<suffix>`` and
|
|
437
|
+
``phi_bulk_<suffix>`` are the sums of theirs.
|
|
438
|
+
_masses : dict of str to str, optional
|
|
439
|
+
A mass attribute mapped to a size attribute; the mass is the size
|
|
440
|
+
cubed, :math:`m = d^3`, for a uniform mass density.
|
|
441
|
+
|
|
442
|
+
Raises
|
|
443
|
+
------
|
|
444
|
+
TypeError
|
|
445
|
+
On instantiation of a class with no `_patterns`, such as
|
|
446
|
+
`ParserBase` itself; on creation of a subclass whose declaration is
|
|
447
|
+
incomplete or inconsistent.
|
|
448
|
+
ValueError
|
|
449
|
+
If `artifact` is empty; if `lineage` or `group` is not valid; if
|
|
450
|
+
`name` lacks a keyword of the lineage's pattern; or if a keyword's
|
|
451
|
+
value is not a number, or not a whole number for an `int` keyword.
|
|
452
|
+
|
|
453
|
+
Notes
|
|
454
|
+
-----
|
|
455
|
+
Parsing splits `name` once into keyword-value pairs and keeps the first
|
|
456
|
+
value of each keyword, so a name may carry keywords the pattern does not
|
|
457
|
+
use. For a name of length :math:`L` and a pattern of :math:`k` keywords,
|
|
458
|
+
parsing costs :math:`O(L + k)` time and :math:`O(L)` memory.
|
|
459
|
+
|
|
460
|
+
The bulk values are computed only at the ``segment``, ``whole``, and
|
|
461
|
+
``ensemble_long`` lineages, whose names carry the box size.
|
|
462
|
+
|
|
463
|
+
A value that is a function of parsed values but fits none of the tables
|
|
464
|
+
above, such as a complementary percentage, is set by overriding
|
|
465
|
+
``_derived_attributes``, which runs at every lineage after the bulk
|
|
466
|
+
values.
|
|
467
|
+
|
|
468
|
+
Examples
|
|
469
|
+
--------
|
|
470
|
+
A parser for a hypothetical study of a linear chain among crowders in a
|
|
471
|
+
periodic cube, whose ``bug`` and ``all`` trajectories are dumped every
|
|
472
|
+
1000 and 5000 time steps:
|
|
473
|
+
|
|
474
|
+
>>> class ChainCub(ParserBase):
|
|
475
|
+
... _geometry = 'cubic'
|
|
476
|
+
... _topology = 'linear'
|
|
477
|
+
... _groups = ['bug', 'all']
|
|
478
|
+
... _dump_keywords = {'bug': 'bdump', 'all': 'adump'}
|
|
479
|
+
... _keywords = {
|
|
480
|
+
... 'N': KeywordSpec('nmon', int),
|
|
481
|
+
... 'ac': KeywordSpec('dcrowd', float),
|
|
482
|
+
... 'nc': KeywordSpec('ncrowd', int),
|
|
483
|
+
... 'l': KeywordSpec('lcube', float, scale=2.0),
|
|
484
|
+
... 'ens': KeywordSpec('ensemble_id', int),
|
|
485
|
+
... 'j': KeywordSpec('segment_id', int),
|
|
486
|
+
... }
|
|
487
|
+
... _patterns = {
|
|
488
|
+
... 'segment': ('N', 'ac', 'nc', 'l', 'ens', 'j'),
|
|
489
|
+
... 'whole': ('N', 'ac', 'nc', 'l', 'ens'),
|
|
490
|
+
... 'ensemble_long': ('N', 'ac', 'nc', 'l'),
|
|
491
|
+
... 'ensemble': ('N', 'ac', 'nc'),
|
|
492
|
+
... 'space': ('N', 'ac'),
|
|
493
|
+
... }
|
|
494
|
+
... _defaults = {'dmon': 1.0, 'bdump': 1000, 'adump': 5000}
|
|
495
|
+
... _species = (Species('m', 'nmon', 'dmon'),
|
|
496
|
+
... Species('c', 'ncrowd', 'dcrowd'))
|
|
497
|
+
... _project_attributes = {
|
|
498
|
+
... 'segment': ['dmon', 'phi_bulk_m', 'rho_bulk_m',
|
|
499
|
+
... 'phi_bulk_c', 'rho_bulk_c'],
|
|
500
|
+
... 'whole': ['dmon', 'phi_bulk_m', 'rho_bulk_m',
|
|
501
|
+
... 'phi_bulk_c', 'rho_bulk_c'],
|
|
502
|
+
... 'ensemble_long': ['dmon', 'phi_bulk_m', 'rho_bulk_m',
|
|
503
|
+
... 'phi_bulk_c', 'rho_bulk_c'],
|
|
504
|
+
... 'ensemble': ['dmon'],
|
|
505
|
+
... 'space': ['dmon'],
|
|
506
|
+
... }
|
|
507
|
+
>>> artifact = ChainCub('N100ac2.0nc50l10.0ens1.bug', 'whole', 'bug')
|
|
508
|
+
>>> artifact.lcube
|
|
509
|
+
20.0
|
|
510
|
+
>>> artifact.ensemble_long, artifact.ensemble, artifact.space
|
|
511
|
+
('N100ac2.0nc50l10.0', 'N100ac2nc50', 'N100ac2')
|
|
512
|
+
>>> artifact.filepath is None, artifact.ext is None
|
|
513
|
+
(True, True)
|
|
514
|
+
>>> artifact.dump_keyword, getattr(artifact, artifact.dump_keyword)
|
|
515
|
+
('bdump', 1000)
|
|
516
|
+
"""
|
|
517
|
+
_lineages: ClassVar[list[LineageT]] = \
|
|
518
|
+
['segment', 'whole', 'ensemble_long', 'ensemble', 'space']
|
|
519
|
+
_genealogy: ClassVar[dict[LineageT, list[LineageT]]] = {
|
|
520
|
+
'segment': ['segment', 'whole', 'ensemble_long', 'ensemble', 'space'],
|
|
521
|
+
'whole': ['whole', 'ensemble_long', 'ensemble', 'space'],
|
|
522
|
+
'ensemble_long': ['ensemble_long', 'ensemble', 'space'],
|
|
523
|
+
'ensemble': ['ensemble', 'space'],
|
|
524
|
+
'space': ['space'],
|
|
525
|
+
}
|
|
526
|
+
# Every concrete subclass sets these; __init_subclass__ checks them.
|
|
527
|
+
_geometry: ClassVar[GeometryT]
|
|
528
|
+
_topology: ClassVar[TopologyT]
|
|
529
|
+
_groups: ClassVar[list[GroupT]]
|
|
530
|
+
_dump_keywords: ClassVar[dict[GroupT, str]]
|
|
531
|
+
_keywords: ClassVar[Optional[dict[str, KeywordSpec]]] = None
|
|
532
|
+
_patterns: ClassVar[Optional[dict[LineageT, tuple[str, ...]]]] = None
|
|
533
|
+
_defaults: ClassVar[dict[str, float]] = {}
|
|
534
|
+
_species: ClassVar[tuple[Species, ...]] = ()
|
|
535
|
+
_totals: ClassVar[dict[str, tuple[str, ...]]] = {}
|
|
536
|
+
_masses: ClassVar[dict[str, str]] = {}
|
|
537
|
+
_genealogy_attributes: ClassVar[dict[LineageT, OrderedDict[str, str]]]
|
|
538
|
+
_project_attributes: ClassVar[dict[LineageT, list[str]]]
|
|
539
|
+
|
|
540
|
+
def __init_subclass__(cls) -> None:
|
|
541
|
+
super().__init_subclass__()
|
|
542
|
+
if cls._patterns is None:
|
|
543
|
+
return
|
|
544
|
+
keywords, patterns = cls._check_declaration()
|
|
545
|
+
cls._genealogy_attributes = {
|
|
546
|
+
lineage: OrderedDict(
|
|
547
|
+
(keywords[keyword].attr, keyword)
|
|
548
|
+
for keyword in patterns[lineage])
|
|
549
|
+
for lineage in cls._lineages
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
@classmethod
|
|
553
|
+
def _check_declaration(
|
|
554
|
+
cls
|
|
555
|
+
) -> tuple[dict[str, KeywordSpec], dict[LineageT, tuple[str, ...]]]:
|
|
556
|
+
"""
|
|
557
|
+
Check a subclass's declaration and return its keyword and pattern
|
|
558
|
+
tables.
|
|
559
|
+
|
|
560
|
+
Returns
|
|
561
|
+
-------
|
|
562
|
+
tuple of (dict, dict)
|
|
563
|
+
`_keywords` and `_patterns`.
|
|
564
|
+
|
|
565
|
+
Raises
|
|
566
|
+
------
|
|
567
|
+
TypeError
|
|
568
|
+
If a required class attribute is unset; if a lineage has no
|
|
569
|
+
pattern or no project attributes; if a group has no dump
|
|
570
|
+
keyword, or a dump keyword names no group; if a pattern keyword
|
|
571
|
+
has no :class:`KeywordSpec`; if an attribute appears twice in
|
|
572
|
+
one pattern; if the bulk recipe or a dump keyword reads an
|
|
573
|
+
attribute a ``segment``, ``whole``, or ``ensemble_long`` name
|
|
574
|
+
does not set; or if a total names an undeclared species.
|
|
575
|
+
"""
|
|
576
|
+
name = cls.__name__
|
|
577
|
+
keywords = cls._keywords
|
|
578
|
+
patterns = cls._patterns
|
|
579
|
+
geometry: Optional[GeometryT] = getattr(cls, '_geometry', None)
|
|
580
|
+
if (geometry is None or getattr(cls, '_topology', None) is None
|
|
581
|
+
or getattr(cls, '_groups', None) is None
|
|
582
|
+
or getattr(cls, '_dump_keywords', None) is None
|
|
583
|
+
or keywords is None or patterns is None
|
|
584
|
+
or getattr(cls, '_project_attributes', None) is None):
|
|
585
|
+
raise TypeError(
|
|
586
|
+
f"{name} must declare '_geometry', '_topology', '_groups', "
|
|
587
|
+
"'_dump_keywords', '_keywords', '_patterns', and "
|
|
588
|
+
"'_project_attributes'.")
|
|
589
|
+
for table in ('_patterns', '_project_attributes'):
|
|
590
|
+
if set(getattr(cls, table)) != set(cls._lineages):
|
|
591
|
+
raise TypeError(
|
|
592
|
+
f"{name}.{table} must have one entry per lineage: "
|
|
593
|
+
f"{cls._lineages}.")
|
|
594
|
+
if set(cls._dump_keywords) != set(cls._groups):
|
|
595
|
+
raise TypeError(
|
|
596
|
+
f"{name}._dump_keywords must have one entry per group: "
|
|
597
|
+
f"{cls._groups}.")
|
|
598
|
+
for lineage, pattern in patterns.items():
|
|
599
|
+
for keyword in pattern:
|
|
600
|
+
if keyword not in keywords:
|
|
601
|
+
raise TypeError(
|
|
602
|
+
f"{name}: keyword {keyword!r} in the {lineage!r} "
|
|
603
|
+
"pattern has no KeywordSpec.")
|
|
604
|
+
attrs = [keywords[keyword].attr for keyword in pattern]
|
|
605
|
+
for attr in attrs:
|
|
606
|
+
if attrs.count(attr) > 1:
|
|
607
|
+
raise TypeError(
|
|
608
|
+
f"{name}: attribute {attr!r} appears more than once "
|
|
609
|
+
f"in the {lineage!r} pattern.")
|
|
610
|
+
suffixes = {species.suffix for species in cls._species}
|
|
611
|
+
for total, parts in cls._totals.items():
|
|
612
|
+
for part in parts:
|
|
613
|
+
if part not in suffixes:
|
|
614
|
+
raise TypeError(
|
|
615
|
+
f"{name}: total {total!r} sums {part!r}, which is "
|
|
616
|
+
"not a declared species.")
|
|
617
|
+
needed: list[str] = list(cls._masses.values())
|
|
618
|
+
if cls._species:
|
|
619
|
+
needed.extend(_BOX_ATTRIBUTES[geometry])
|
|
620
|
+
for species in cls._species:
|
|
621
|
+
needed.extend((species.number, species.size))
|
|
622
|
+
for lineage in _BULK_LINEAGES:
|
|
623
|
+
available = set(cls._defaults).union(
|
|
624
|
+
keywords[keyword].attr for keyword in patterns[lineage])
|
|
625
|
+
for attr in needed:
|
|
626
|
+
if attr not in available:
|
|
627
|
+
raise TypeError(
|
|
628
|
+
f"{name}: the bulk values need {attr!r}, which the "
|
|
629
|
+
f"{lineage!r} pattern does not set.")
|
|
630
|
+
for group, attr in cls._dump_keywords.items():
|
|
631
|
+
if attr not in available:
|
|
632
|
+
raise TypeError(
|
|
633
|
+
f"{name}: the dump keyword {attr!r} of group "
|
|
634
|
+
f"{group!r} is not set by the {lineage!r} pattern "
|
|
635
|
+
"or '_defaults'.")
|
|
636
|
+
return keywords, patterns
|
|
637
|
+
|
|
638
|
+
def __init__(
|
|
639
|
+
self,
|
|
640
|
+
artifact: Pathish,
|
|
641
|
+
lineage: LineageT,
|
|
642
|
+
group: GroupT,
|
|
643
|
+
ispath: bool = False
|
|
644
|
+
) -> None:
|
|
645
|
+
if self._patterns is None:
|
|
646
|
+
raise TypeError(
|
|
647
|
+
f"{type(self).__name__} declares no lineage patterns; "
|
|
648
|
+
"instantiate a subclass that declares a study.")
|
|
649
|
+
artifact_str = os.fspath(artifact)
|
|
650
|
+
if artifact_str == '':
|
|
651
|
+
raise ValueError("'artifact' cannot be an empty string.")
|
|
652
|
+
|
|
653
|
+
invalid_keyword(lineage, self.lineages)
|
|
654
|
+
invalid_keyword(group, self.groups)
|
|
655
|
+
|
|
656
|
+
self._ispath = ispath
|
|
657
|
+
if ispath:
|
|
658
|
+
# Normalize: expand env/~ and convert to absolute path
|
|
659
|
+
p = Path(
|
|
660
|
+
os.path.expandvars(os.path.expanduser(artifact_str))
|
|
661
|
+
).resolve()
|
|
662
|
+
self._filepath: Optional[Path] = p.parent
|
|
663
|
+
self._filename: str = p.name
|
|
664
|
+
ext = p.suffix if _EXTENSION_RE.fullmatch(p.suffix) else ''
|
|
665
|
+
self._name: str = p.name[:len(p.name) - len(ext)]
|
|
666
|
+
self._ext: Optional[str] = ext
|
|
667
|
+
else:
|
|
668
|
+
self._filepath = None
|
|
669
|
+
self._filename = artifact_str
|
|
670
|
+
self._name = artifact_str
|
|
671
|
+
self._ext = None
|
|
672
|
+
|
|
673
|
+
self._project_name = self.__class__.__name__
|
|
674
|
+
self._lineage: LineageT = lineage
|
|
675
|
+
self._group: GroupT = group
|
|
676
|
+
|
|
677
|
+
# Update the self._name by dropping 'group' and/or any other extra
|
|
678
|
+
# beyond the template defined above.
|
|
679
|
+
self._find_name()
|
|
680
|
+
self._lineage_genealogy: list[LineageT] = self._genealogy[lineage]
|
|
681
|
+
self._lineage_attributes = \
|
|
682
|
+
list(self.genealogy_attributes[lineage].keys())
|
|
683
|
+
self._physical_attributes = self.project_attributes[lineage]
|
|
684
|
+
self._attributes = \
|
|
685
|
+
self._lineage_attributes + self._physical_attributes
|
|
686
|
+
|
|
687
|
+
self._set_defaults()
|
|
688
|
+
self._parse_name()
|
|
689
|
+
self._set_parents()
|
|
690
|
+
if self.lineage in _BULK_LINEAGES:
|
|
691
|
+
self._dependent_attributes()
|
|
692
|
+
self._derived_attributes()
|
|
693
|
+
|
|
694
|
+
def __str__(self) -> str:
|
|
695
|
+
"""
|
|
696
|
+
Provide a formatted summary of the parser instance.
|
|
697
|
+
"""
|
|
698
|
+
observation = (
|
|
699
|
+
f"Artifact:\n"
|
|
700
|
+
f" Name: '{self.filename}',\n"
|
|
701
|
+
f" Geometry: '{self.geometry}',\n"
|
|
702
|
+
f" Group: '{self.group}',\n"
|
|
703
|
+
f" Lineage: '{self.lineage}',\n"
|
|
704
|
+
f" Topology: '{self.topology}',\n"
|
|
705
|
+
f" Project: '{self.project_name}'"
|
|
706
|
+
)
|
|
707
|
+
return observation
|
|
708
|
+
|
|
709
|
+
def __repr__(self) -> str:
|
|
710
|
+
return (
|
|
711
|
+
f"Artifact(filename='{self.filename}', "
|
|
712
|
+
f"geometry='{self.geometry}', "
|
|
713
|
+
f"group='{self.group}', lineage='{self.lineage}', "
|
|
714
|
+
f"topology='{self.topology}', project='{self.project_name}')"
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
@classmethod
|
|
718
|
+
def from_name(
|
|
719
|
+
cls,
|
|
720
|
+
name: str,
|
|
721
|
+
lineage: LineageT,
|
|
722
|
+
group: GroupT
|
|
723
|
+
) -> Self:
|
|
724
|
+
"""
|
|
725
|
+
Construct a parser from a *name token* (not a path).
|
|
726
|
+
|
|
727
|
+
Sets `filepath` and `ext` to ``None`` and keeps the token as both
|
|
728
|
+
`filename` and `name` before `_find_name()` normalization.
|
|
729
|
+
|
|
730
|
+
Examples
|
|
731
|
+
--------
|
|
732
|
+
>>> from polyphys.tests.synthetic import ChainCyl
|
|
733
|
+
>>> parser = ChainCyl.from_name('N200D10.0ac2.0', 'space', 'bug')
|
|
734
|
+
>>> parser.filepath is None, parser.ext is None
|
|
735
|
+
(True, True)
|
|
736
|
+
"""
|
|
737
|
+
return cls(name, lineage, group, ispath=False)
|
|
738
|
+
|
|
739
|
+
@classmethod
|
|
740
|
+
def pattern(cls, lineage: LineageT) -> tuple[str, ...]:
|
|
741
|
+
"""
|
|
742
|
+
Return the filename keywords of `lineage`, in name order.
|
|
743
|
+
|
|
744
|
+
Parameters
|
|
745
|
+
----------
|
|
746
|
+
lineage : :py:obj:`~.LineageT`
|
|
747
|
+
One of ``'segment'``, ``'whole'``, ``'ensemble_long'``,
|
|
748
|
+
``'ensemble'``, or ``'space'``.
|
|
749
|
+
|
|
750
|
+
Returns
|
|
751
|
+
-------
|
|
752
|
+
tuple of str
|
|
753
|
+
The keywords, such as ``'N'`` or ``'ac'``, that a name of
|
|
754
|
+
`lineage` carries, in the order they appear in it.
|
|
755
|
+
|
|
756
|
+
Raises
|
|
757
|
+
------
|
|
758
|
+
TypeError
|
|
759
|
+
If the class declares no patterns, such as `ParserBase` itself.
|
|
760
|
+
ValueError
|
|
761
|
+
If `lineage` is not a valid lineage.
|
|
762
|
+
|
|
763
|
+
Examples
|
|
764
|
+
--------
|
|
765
|
+
>>> from polyphys.tests.synthetic import ChainCyl
|
|
766
|
+
>>> ChainCyl.pattern('space')
|
|
767
|
+
('N', 'D', 'ac')
|
|
768
|
+
"""
|
|
769
|
+
patterns = cls._declared_patterns()
|
|
770
|
+
invalid_keyword(lineage, cls._lineages)
|
|
771
|
+
return patterns[lineage]
|
|
772
|
+
|
|
773
|
+
@classmethod
|
|
774
|
+
def attributes_at(cls, lineage: LineageT) -> frozenset[str]:
|
|
775
|
+
"""
|
|
776
|
+
Return the names of the attributes that a parser of `lineage` sets.
|
|
777
|
+
|
|
778
|
+
The set holds the attributes parsed from the lineage's pattern, the
|
|
779
|
+
`_defaults`, the five lineage names (``segment`` to ``space``), and
|
|
780
|
+
the lineage's `_project_attributes`. At ``segment``, ``whole``, and
|
|
781
|
+
``ensemble_long`` it also holds the bulk number density
|
|
782
|
+
``rho_bulk_<suffix>`` and volume fraction ``phi_bulk_<suffix>`` of
|
|
783
|
+
each species and each total, and the declared masses. An attribute
|
|
784
|
+
that an override of ``_derived_attributes`` sets counts only if
|
|
785
|
+
`_project_attributes` lists it.
|
|
786
|
+
|
|
787
|
+
The set is built from the declaration, without parsing a name; the
|
|
788
|
+
cost is :math:`O(k + n)` for a pattern of :math:`k` keywords and
|
|
789
|
+
:math:`n` declared defaults, species, totals, masses, and project
|
|
790
|
+
attributes.
|
|
791
|
+
|
|
792
|
+
Parameters
|
|
793
|
+
----------
|
|
794
|
+
lineage : :py:obj:`~.LineageT`
|
|
795
|
+
One of ``'segment'``, ``'whole'``, ``'ensemble_long'``,
|
|
796
|
+
``'ensemble'``, or ``'space'``.
|
|
797
|
+
|
|
798
|
+
Returns
|
|
799
|
+
-------
|
|
800
|
+
frozenset of str
|
|
801
|
+
The attribute names.
|
|
802
|
+
|
|
803
|
+
Raises
|
|
804
|
+
------
|
|
805
|
+
TypeError
|
|
806
|
+
If the class declares no patterns, such as `ParserBase` itself.
|
|
807
|
+
ValueError
|
|
808
|
+
If `lineage` is not a valid lineage.
|
|
809
|
+
|
|
810
|
+
Examples
|
|
811
|
+
--------
|
|
812
|
+
The bulk values exist at ``ensemble_long``, not at ``space``:
|
|
813
|
+
|
|
814
|
+
>>> from polyphys.tests.synthetic import ChainCyl
|
|
815
|
+
>>> 'phi_bulk_c' in ChainCyl.attributes_at('ensemble_long')
|
|
816
|
+
True
|
|
817
|
+
>>> 'phi_bulk_c' in ChainCyl.attributes_at('space')
|
|
818
|
+
False
|
|
819
|
+
"""
|
|
820
|
+
cls._declared_patterns()
|
|
821
|
+
invalid_keyword(lineage, cls._lineages)
|
|
822
|
+
names = set(cls._genealogy_attributes[lineage])
|
|
823
|
+
names.update(cls._defaults, cls._lineages,
|
|
824
|
+
cls._project_attributes[lineage])
|
|
825
|
+
if lineage in _BULK_LINEAGES:
|
|
826
|
+
suffixes = [species.suffix for species in cls._species]
|
|
827
|
+
suffixes.extend(cls._totals)
|
|
828
|
+
for suffix in suffixes:
|
|
829
|
+
names.update((f"rho_bulk_{suffix}", f"phi_bulk_{suffix}"))
|
|
830
|
+
names.update(cls._masses)
|
|
831
|
+
return frozenset(names)
|
|
832
|
+
|
|
833
|
+
@classmethod
|
|
834
|
+
def _declared_patterns(cls) -> dict[LineageT, tuple[str, ...]]:
|
|
835
|
+
"""
|
|
836
|
+
Return `_patterns`, or raise `TypeError` if the class declares none.
|
|
837
|
+
"""
|
|
838
|
+
if cls._patterns is None:
|
|
839
|
+
raise TypeError(
|
|
840
|
+
f"{cls.__name__} declares no lineage patterns; use a "
|
|
841
|
+
"subclass that declares a study.")
|
|
842
|
+
return cls._patterns
|
|
843
|
+
|
|
844
|
+
@property
|
|
845
|
+
def lineages(self) -> list[LineageT]:
|
|
846
|
+
"""
|
|
847
|
+
List of all the acceptable lineages.
|
|
848
|
+
"""
|
|
849
|
+
return self._lineages
|
|
850
|
+
|
|
851
|
+
@property
|
|
852
|
+
def genealogy(self) -> dict[LineageT, list[LineageT]]:
|
|
853
|
+
"""
|
|
854
|
+
Dictionary of lineages and their parent lineages.
|
|
855
|
+
"""
|
|
856
|
+
return self._genealogy
|
|
857
|
+
|
|
858
|
+
@property
|
|
859
|
+
def geometry(self) -> GeometryT:
|
|
860
|
+
"""
|
|
861
|
+
System geometry in a molecular dynamics system.
|
|
862
|
+
"""
|
|
863
|
+
return self._geometry
|
|
864
|
+
|
|
865
|
+
@property
|
|
866
|
+
def groups(self) -> list[GroupT]:
|
|
867
|
+
"""
|
|
868
|
+
List of valid group names for the subclass.
|
|
869
|
+
"""
|
|
870
|
+
return self._groups
|
|
871
|
+
|
|
872
|
+
@property
|
|
873
|
+
def dump_keywords(self) -> dict[GroupT, str]:
|
|
874
|
+
"""
|
|
875
|
+
Each group mapped to the attribute that holds the dump period, in
|
|
876
|
+
time steps, of its trajectory.
|
|
877
|
+
"""
|
|
878
|
+
return self._dump_keywords
|
|
879
|
+
|
|
880
|
+
@property
|
|
881
|
+
def topology(self) -> TopologyT:
|
|
882
|
+
"""
|
|
883
|
+
Define the polymer topology for the parser subclass.
|
|
884
|
+
"""
|
|
885
|
+
return self._topology
|
|
886
|
+
|
|
887
|
+
@property
|
|
888
|
+
def genealogy_attributes(self) -> dict[LineageT, OrderedDict[str, str]]:
|
|
889
|
+
"""
|
|
890
|
+
Dictionary of lineage-specific attributes. Each key is a lineage type,
|
|
891
|
+
and each value is an OrderedDict mapping attribute names to their
|
|
892
|
+
filename keywords, in pattern order.
|
|
893
|
+
"""
|
|
894
|
+
return self._genealogy_attributes
|
|
895
|
+
|
|
896
|
+
@property
|
|
897
|
+
def project_attributes(self) -> dict[LineageT, list[str]]:
|
|
898
|
+
"""
|
|
899
|
+
Dictionary of project attributes. Each key is a lineage type, and
|
|
900
|
+
each value is the list of attributes that are fixed or computed
|
|
901
|
+
rather than parsed at that lineage.
|
|
902
|
+
"""
|
|
903
|
+
return self._project_attributes
|
|
904
|
+
|
|
905
|
+
@property
|
|
906
|
+
def filename(self) -> str:
|
|
907
|
+
"""
|
|
908
|
+
Return the filename, either extracted from the path or the name
|
|
909
|
+
itself.
|
|
910
|
+
"""
|
|
911
|
+
return self._filename
|
|
912
|
+
|
|
913
|
+
@property
|
|
914
|
+
def filepath(self) -> Optional[Path]:
|
|
915
|
+
"""
|
|
916
|
+
Return the resolved absolute directory that holds the artifact, or
|
|
917
|
+
``None`` if the parser was built from a name.
|
|
918
|
+
|
|
919
|
+
Examples
|
|
920
|
+
--------
|
|
921
|
+
>>> from polyphys.tests.synthetic import ChainCyl
|
|
922
|
+
>>> import tempfile
|
|
923
|
+
>>> with tempfile.TemporaryDirectory() as folder:
|
|
924
|
+
... path = Path(folder, 'N200D10.0ac2.0.csv')
|
|
925
|
+
... path.touch()
|
|
926
|
+
... parser = ChainCyl(path, 'space', 'bug', ispath=True)
|
|
927
|
+
... parser.filepath == Path(folder).resolve()
|
|
928
|
+
True
|
|
929
|
+
>>> ChainCyl('N200D10.0ac2.0', 'space', 'bug').filepath is None
|
|
930
|
+
True
|
|
931
|
+
"""
|
|
932
|
+
return self._filepath
|
|
933
|
+
|
|
934
|
+
@property
|
|
935
|
+
def ispath(self) -> bool:
|
|
936
|
+
"""
|
|
937
|
+
Return whether the artifact was parsed as a filesystem path.
|
|
938
|
+
"""
|
|
939
|
+
return self._ispath
|
|
940
|
+
|
|
941
|
+
@property
|
|
942
|
+
def group(self) -> GroupT:
|
|
943
|
+
"""
|
|
944
|
+
Return the current group.
|
|
945
|
+
"""
|
|
946
|
+
return self._group
|
|
947
|
+
|
|
948
|
+
@property
|
|
949
|
+
def dump_keyword(self) -> str:
|
|
950
|
+
"""
|
|
951
|
+
The attribute that holds the dump period, in time steps, of the
|
|
952
|
+
`group` trajectory.
|
|
953
|
+
"""
|
|
954
|
+
return self._dump_keywords[self._group]
|
|
955
|
+
|
|
956
|
+
@property
|
|
957
|
+
def ext(self) -> Optional[str]:
|
|
958
|
+
"""
|
|
959
|
+
Return the file extension, including the leading dot, or ``None`` if
|
|
960
|
+
the parser was built from a name.
|
|
961
|
+
|
|
962
|
+
The extension is :attr:`pathlib.PurePath.suffix`, the part from the
|
|
963
|
+
last dot, when that part holds a letter, such as ``.csv`` or ``.h5``;
|
|
964
|
+
otherwise it is ``''`` and the part stays in :attr:`name`. A name
|
|
965
|
+
that ends with a value, such as ``ac1.5``, or with a dot, such as
|
|
966
|
+
``ac1.``, so keeps its last value whatever the Python version.
|
|
967
|
+
|
|
968
|
+
Examples
|
|
969
|
+
--------
|
|
970
|
+
>>> from polyphys.tests.synthetic import ChainCyl
|
|
971
|
+
>>> import tempfile
|
|
972
|
+
>>> with tempfile.TemporaryDirectory() as folder:
|
|
973
|
+
... path = Path(folder, 'N200D10ac2')
|
|
974
|
+
... path.touch()
|
|
975
|
+
... ChainCyl(path, 'space', 'bug', ispath=True).ext
|
|
976
|
+
''
|
|
977
|
+
>>> with tempfile.TemporaryDirectory() as folder:
|
|
978
|
+
... path = Path(folder, 'N200D10ac1.5')
|
|
979
|
+
... path.touch()
|
|
980
|
+
... parser = ChainCyl(path, 'space', 'bug', ispath=True)
|
|
981
|
+
... parser.ext, parser.dcrowd
|
|
982
|
+
('', 1.5)
|
|
983
|
+
>>> ChainCyl('N200D10.0ac2.0', 'space', 'bug').ext is None
|
|
984
|
+
True
|
|
985
|
+
"""
|
|
986
|
+
return self._ext
|
|
987
|
+
|
|
988
|
+
@property
|
|
989
|
+
def lineage(self) -> LineageT:
|
|
990
|
+
"""
|
|
991
|
+
Return the current lineage.
|
|
992
|
+
"""
|
|
993
|
+
return self._lineage
|
|
994
|
+
|
|
995
|
+
@property
|
|
996
|
+
def name(self) -> str:
|
|
997
|
+
"""
|
|
998
|
+
Return the unique name parsed from the filename.
|
|
999
|
+
"""
|
|
1000
|
+
return self._name
|
|
1001
|
+
|
|
1002
|
+
@property
|
|
1003
|
+
def project_name(self) -> str:
|
|
1004
|
+
"""
|
|
1005
|
+
Return the project (parser class) name,
|
|
1006
|
+
"""
|
|
1007
|
+
return self._project_name
|
|
1008
|
+
|
|
1009
|
+
@property
|
|
1010
|
+
def attributes(self) -> list[str]:
|
|
1011
|
+
"""
|
|
1012
|
+
Return lineage-specific and project attributes for an artifact.
|
|
1013
|
+
"""
|
|
1014
|
+
return self._attributes
|
|
1015
|
+
|
|
1016
|
+
@property
|
|
1017
|
+
def lineage_genealogy(self) -> list[LineageT]:
|
|
1018
|
+
"""
|
|
1019
|
+
Return the parents of a given `lineage`.
|
|
1020
|
+
"""
|
|
1021
|
+
return self._lineage_genealogy
|
|
1022
|
+
|
|
1023
|
+
@property
|
|
1024
|
+
def lineage_attributes(self) -> list[str]:
|
|
1025
|
+
"""
|
|
1026
|
+
Return lineage-specific attributes for an artifact with a given
|
|
1027
|
+
`lineage`.
|
|
1028
|
+
"""
|
|
1029
|
+
return self._lineage_attributes
|
|
1030
|
+
|
|
1031
|
+
@property
|
|
1032
|
+
def physical_attributes(self) -> list[str]:
|
|
1033
|
+
"""
|
|
1034
|
+
Return project-level attributes for an artifact with a given
|
|
1035
|
+
`lineage`.
|
|
1036
|
+
"""
|
|
1037
|
+
return self._physical_attributes
|
|
1038
|
+
|
|
1039
|
+
def _find_name(self) -> None:
|
|
1040
|
+
"""
|
|
1041
|
+
parses the unique lineage_name (the first substring of filename
|
|
1042
|
+
and/or the segment keyword middle substring) of a filename.
|
|
1043
|
+
"""
|
|
1044
|
+
if self.lineage in ['segment', 'whole']:
|
|
1045
|
+
# a 'segment' lineage only used in 'probe' phase
|
|
1046
|
+
# a 'whole' lineage used in 'probe' or 'analyze' phases
|
|
1047
|
+
# so its lineage_name is either ended by 'group' keyword or '-'.
|
|
1048
|
+
# these two combined below:
|
|
1049
|
+
base = self.name.split(f".{self.group}")[0]
|
|
1050
|
+
self._name = base.split('-')[0]
|
|
1051
|
+
else: # 'ensemble' or 'space' lineages
|
|
1052
|
+
self._name = self.name.split('-')[0]
|
|
1053
|
+
|
|
1054
|
+
def _set_defaults(self) -> None:
|
|
1055
|
+
"""
|
|
1056
|
+
Set the attributes declared in `_defaults`.
|
|
1057
|
+
"""
|
|
1058
|
+
for attr, value in self._defaults.items():
|
|
1059
|
+
setattr(self, attr, value)
|
|
1060
|
+
|
|
1061
|
+
def _parse_name(self) -> None:
|
|
1062
|
+
"""
|
|
1063
|
+
Set the attributes of the lineage's pattern from the values in
|
|
1064
|
+
`name`.
|
|
1065
|
+
|
|
1066
|
+
Raises
|
|
1067
|
+
------
|
|
1068
|
+
ValueError
|
|
1069
|
+
If `name` lacks a keyword of the pattern, or a keyword's value is
|
|
1070
|
+
not a number, or not a whole number for an `int` keyword.
|
|
1071
|
+
"""
|
|
1072
|
+
keywords = self._keywords
|
|
1073
|
+
patterns = self._patterns
|
|
1074
|
+
assert keywords is not None and patterns is not None # __init__
|
|
1075
|
+
words = _TOKEN_RE.split(self.name)
|
|
1076
|
+
values: dict[str, str] = {}
|
|
1077
|
+
for keyword, token in zip(words[1::2], words[2::2]):
|
|
1078
|
+
values.setdefault(keyword, token)
|
|
1079
|
+
for keyword in patterns[self.lineage]:
|
|
1080
|
+
spec = keywords[keyword]
|
|
1081
|
+
if keyword not in values:
|
|
1082
|
+
raise ValueError(
|
|
1083
|
+
f"keyword {keyword!r} for attribute {spec.attr!r} not "
|
|
1084
|
+
f"found in {self.name!r} (lineage {self.lineage!r}).")
|
|
1085
|
+
try:
|
|
1086
|
+
value = spec.parse(values[keyword])
|
|
1087
|
+
except ValueError as err:
|
|
1088
|
+
raise ValueError(
|
|
1089
|
+
f"value of keyword {keyword!r} in {self.name!r} "
|
|
1090
|
+
f"(lineage {self.lineage!r}): {err}.") from err
|
|
1091
|
+
setattr(self, spec.attr, value)
|
|
1092
|
+
|
|
1093
|
+
def _set_parents(self) -> None:
|
|
1094
|
+
"""
|
|
1095
|
+
Set parent lineage names for each lineage type, following the
|
|
1096
|
+
hierarchy defined in `_genealogy`.
|
|
1097
|
+
|
|
1098
|
+
Notes
|
|
1099
|
+
-----
|
|
1100
|
+
The `self._genealogy` defines the following parent-child hierarchy:
|
|
1101
|
+
|
|
1102
|
+
- space -> ensemble -> ensemble_long -> whole -> segment
|
|
1103
|
+
|
|
1104
|
+
Each lineage on the left has all the lineages on its right.
|
|
1105
|
+
|
|
1106
|
+
The long-form names (``segment``, ``whole``, and ``ensemble_long``)
|
|
1107
|
+
are cut from `name` after the value of the parent's last keyword, so
|
|
1108
|
+
they keep the original tokens. `_parse_name` converts some values,
|
|
1109
|
+
such as ``r`` to `dcyl` and ``l`` to `lcube`; writing those
|
|
1110
|
+
attributes back under the original keywords would convert them again
|
|
1111
|
+
at every re-parse. Because the cut ends at a keyword's value, a
|
|
1112
|
+
``segment`` or ``whole`` name never carries the topology suffix:
|
|
1113
|
+
``...ens1.ring`` and ``...ens1.j07.ring`` give the ``whole``
|
|
1114
|
+
``...ens1``, so a simulation has one ``whole`` name whichever file it
|
|
1115
|
+
was parsed from.
|
|
1116
|
+
|
|
1117
|
+
The short-form names (``ensemble`` and ``space``) use their own
|
|
1118
|
+
keywords, such as ``D``, and are built from the attributes. A float
|
|
1119
|
+
value that is a whole number prints as an integer and any other
|
|
1120
|
+
float as :func:`repr` (``ac2``, ``epshc0``, ``epshc1.5``), whatever
|
|
1121
|
+
the name being parsed wrote, such as ``ac2.0`` or ``ac2``.
|
|
1122
|
+
|
|
1123
|
+
Every parent attribute is read once, so an unparsed keyword raises
|
|
1124
|
+
`AttributeError`. The cost is :math:`O(L)` in the length :math:`L`
|
|
1125
|
+
of `name`.
|
|
1126
|
+
"""
|
|
1127
|
+
for lineage_name in self.lineages:
|
|
1128
|
+
lineage_value = 'N/A'
|
|
1129
|
+
if lineage_name in self.genealogy[self.lineage]:
|
|
1130
|
+
lineage_attr = self.genealogy_attributes[lineage_name]
|
|
1131
|
+
lineage_value = ''.join(
|
|
1132
|
+
f"{attr_short}{_format_value(getattr(self, attr_long))}"
|
|
1133
|
+
for attr_long, attr_short in lineage_attr.items()
|
|
1134
|
+
)
|
|
1135
|
+
if lineage_name in ['segment', 'whole', 'ensemble_long']:
|
|
1136
|
+
last_keyword = next(reversed(lineage_attr.values()))
|
|
1137
|
+
lineage_value = self._name_through(last_keyword)
|
|
1138
|
+
setattr(self, lineage_name, lineage_value)
|
|
1139
|
+
|
|
1140
|
+
def _name_through(self, keyword: str) -> str:
|
|
1141
|
+
"""
|
|
1142
|
+
Return `name` up to and including the value of `keyword`, without a
|
|
1143
|
+
trailing dot.
|
|
1144
|
+
|
|
1145
|
+
Parameters
|
|
1146
|
+
----------
|
|
1147
|
+
keyword : str
|
|
1148
|
+
A short-form keyword, such as ``ens``, that appears in `name`.
|
|
1149
|
+
|
|
1150
|
+
Returns
|
|
1151
|
+
-------
|
|
1152
|
+
str
|
|
1153
|
+
The prefix of `name` that ends with the value of `keyword`.
|
|
1154
|
+
|
|
1155
|
+
Raises
|
|
1156
|
+
------
|
|
1157
|
+
ValueError
|
|
1158
|
+
If `keyword` is not a keyword of `name`.
|
|
1159
|
+
"""
|
|
1160
|
+
for match in re.finditer(r"([a-zA-Z]+)([^a-zA-Z]*)", self.name):
|
|
1161
|
+
if match.group(1) == keyword:
|
|
1162
|
+
return self.name[:match.end()].rstrip('.')
|
|
1163
|
+
raise ValueError(f"'{keyword}' keyword not found in '{self.name}'.")
|
|
1164
|
+
|
|
1165
|
+
def _bulk_values(self, number: float, size: float) -> tuple[float, float]:
|
|
1166
|
+
"""
|
|
1167
|
+
Return the bulk number density and volume fraction of `number`
|
|
1168
|
+
particles of diameter `size` in the periodic box.
|
|
1169
|
+
|
|
1170
|
+
Parameters
|
|
1171
|
+
----------
|
|
1172
|
+
number : float
|
|
1173
|
+
Number of particles.
|
|
1174
|
+
size : float
|
|
1175
|
+
Particle diameter.
|
|
1176
|
+
|
|
1177
|
+
Returns
|
|
1178
|
+
-------
|
|
1179
|
+
tuple of (float, float)
|
|
1180
|
+
The number density and the volume fraction, from
|
|
1181
|
+
:func:`~polyphys.manage.utils.number_density_cube` and
|
|
1182
|
+
:func:`~polyphys.manage.utils.volume_fraction_cube` with the side
|
|
1183
|
+
`lcube`, or their cylinder counterparts with `lcyl` and `dcyl`,
|
|
1184
|
+
all with ``pbc=True``.
|
|
1185
|
+
"""
|
|
1186
|
+
if self.geometry == 'cubic':
|
|
1187
|
+
lcube = getattr(self, 'lcube')
|
|
1188
|
+
return (number_density_cube(number, size, lcube, pbc=True),
|
|
1189
|
+
volume_fraction_cube(number, size, lcube, pbc=True))
|
|
1190
|
+
lcyl = getattr(self, 'lcyl')
|
|
1191
|
+
dcyl = getattr(self, 'dcyl')
|
|
1192
|
+
return (number_density_cylinder(number, size, lcyl, dcyl, pbc=True),
|
|
1193
|
+
volume_fraction_cylinder(number, size, lcyl, dcyl, pbc=True))
|
|
1194
|
+
|
|
1195
|
+
def _dependent_attributes(self) -> None:
|
|
1196
|
+
"""
|
|
1197
|
+
Compute the bulk values of each species, their declared totals, and
|
|
1198
|
+
the declared masses.
|
|
1199
|
+
|
|
1200
|
+
Raises
|
|
1201
|
+
------
|
|
1202
|
+
ValueError
|
|
1203
|
+
If a species leaves its centers no room in the box, such as a
|
|
1204
|
+
species as wide as the cylinder; the message names the species,
|
|
1205
|
+
`name`, and the lineage.
|
|
1206
|
+
"""
|
|
1207
|
+
for species in self._species:
|
|
1208
|
+
try:
|
|
1209
|
+
rho, phi = self._bulk_values(getattr(self, species.number),
|
|
1210
|
+
getattr(self, species.size))
|
|
1211
|
+
except ValueError as err:
|
|
1212
|
+
raise ValueError(
|
|
1213
|
+
f"species {species.suffix!r} of {self.name!r} "
|
|
1214
|
+
f"(lineage {self.lineage!r}): {err}") from err
|
|
1215
|
+
setattr(self, f"rho_bulk_{species.suffix}", rho)
|
|
1216
|
+
setattr(self, f"phi_bulk_{species.suffix}", phi)
|
|
1217
|
+
for total, parts in self._totals.items():
|
|
1218
|
+
for quantity in ('rho_bulk', 'phi_bulk'):
|
|
1219
|
+
setattr(self, f"{quantity}_{total}",
|
|
1220
|
+
sum(getattr(self, f"{quantity}_{part}")
|
|
1221
|
+
for part in parts))
|
|
1222
|
+
for mass, size in self._masses.items():
|
|
1223
|
+
setattr(self, mass, getattr(self, size) ** 3)
|
|
1224
|
+
|
|
1225
|
+
def _derived_attributes(self) -> None:
|
|
1226
|
+
"""
|
|
1227
|
+
Set the attributes that a subclass derives from parsed values outside
|
|
1228
|
+
the declaration tables; the base class sets none.
|
|
1229
|
+
|
|
1230
|
+
It runs at every lineage, after parsing, the parent names, and the
|
|
1231
|
+
bulk values, so an override reads only attributes that the lineage's
|
|
1232
|
+
pattern, `_defaults`, or the bulk recipe set. An override costs
|
|
1233
|
+
:math:`O(1)` per attribute it sets.
|
|
1234
|
+
"""
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def __getattr__(name: str) -> object:
|
|
1238
|
+
"""
|
|
1239
|
+
Say where a study parser that left this module in 1.0.0 went.
|
|
1240
|
+
|
|
1241
|
+
Python calls this (:pep:`562`) only for a name the module does not
|
|
1242
|
+
define. An :exc:`ImportError`, not an :exc:`AttributeError`, so that
|
|
1243
|
+
``from polyphys.manage.parser import HnsCub`` shows the message: Python
|
|
1244
|
+
replaces an :exc:`AttributeError` raised here with its own.
|
|
1245
|
+
|
|
1246
|
+
Raises
|
|
1247
|
+
------
|
|
1248
|
+
ImportError
|
|
1249
|
+
If `name` is a study parser that left the module.
|
|
1250
|
+
AttributeError
|
|
1251
|
+
For any other name.
|
|
1252
|
+
"""
|
|
1253
|
+
if name in _MOVED_STUDIES:
|
|
1254
|
+
folder = _MOVED_STUDIES[name]
|
|
1255
|
+
if folder is None:
|
|
1256
|
+
raise ImportError(
|
|
1257
|
+
f"{name} was removed from PolyPhys in 1.0.0; pin polyphys "
|
|
1258
|
+
f"0.9.0 to keep using it, or copy it into your study's code.")
|
|
1259
|
+
raise ImportError(
|
|
1260
|
+
f"{name} moved out of PolyPhys in 1.0.0, to "
|
|
1261
|
+
f"projects/{folder}/parser.py in the PolyPhys repository, which "
|
|
1262
|
+
f"is not installed; copy it into your study's code.")
|
|
1263
|
+
raise AttributeError(
|
|
1264
|
+
f"module 'polyphys.manage.parser' has no attribute {name!r}")
|