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
polyphys/spec.py
ADDED
|
@@ -0,0 +1,1359 @@
|
|
|
1
|
+
"""\
|
|
2
|
+
==========================================================
|
|
3
|
+
:mod:`polyphys.spec`
|
|
4
|
+
==========================================================
|
|
5
|
+
|
|
6
|
+
This :mod:`~polyphys.spec` module holds the records that describe one study
|
|
7
|
+
as data, every one of them in a module that imports neither MDAnalysis nor
|
|
8
|
+
:mod:`polyphys.probe`, so a study's spec builds without the ``mda`` extra:
|
|
9
|
+
|
|
10
|
+
- :class:`ProjectSpec`, the per-project facts that the parser class does not
|
|
11
|
+
hold. A parser class declares how a study's filenames read; a
|
|
12
|
+
:class:`ProjectSpec` declares how the study's analysis reads its
|
|
13
|
+
artifacts: which particle group is the polymer, which parser attributes
|
|
14
|
+
become columns, how the bulk volume fraction of crowders is rounded, which
|
|
15
|
+
time series exist, and which size attribute scales each species' density
|
|
16
|
+
profile.
|
|
17
|
+
- :class:`ProbeSpec`, how :mod:`polyphys.probe` reads the study's
|
|
18
|
+
trajectories: which MDAnalysis selection is each species, which
|
|
19
|
+
chain-size observables are measured, how the chain is unwrapped, the bin
|
|
20
|
+
sizes of the histograms, and, for a study with binders, a
|
|
21
|
+
:class:`BinderSpec` of how their binding is counted.
|
|
22
|
+
- :class:`AnalysisTable`, the analysis of one particle group, which lists a
|
|
23
|
+
:class:`PropertySpec` per probe artifact for
|
|
24
|
+
:func:`~polyphys.analyze.pipeline.reduce_space`.
|
|
25
|
+
|
|
26
|
+
The facts the parser already holds, such as the geometry, the topology, and
|
|
27
|
+
the glob patterns of the ``space`` and ``ensemble_long`` names, are read from
|
|
28
|
+
the parser rather than stored twice.
|
|
29
|
+
|
|
30
|
+
PolyPhys holds no table of studies: a study builds its own spec where its
|
|
31
|
+
analysis lives. The specs of the maintainer's studies are in the repository's
|
|
32
|
+
top-level ``projects/`` folder, which is not part of the installed package.
|
|
33
|
+
|
|
34
|
+
.. versionadded:: 0.9.0
|
|
35
|
+
The records of ``polyphys.manage.projects`` and the table records and
|
|
36
|
+
builders of :mod:`polyphys.analyze.pipeline` moved here; their old import
|
|
37
|
+
paths warned with :exc:`DeprecationWarning`.
|
|
38
|
+
|
|
39
|
+
.. versionchanged:: 1.0.0
|
|
40
|
+
The old import paths are removed: ``polyphys.manage.projects`` no
|
|
41
|
+
longer exists, and :mod:`polyphys.analyze.pipeline` no longer serves the
|
|
42
|
+
table records and builders.
|
|
43
|
+
|
|
44
|
+
Contents
|
|
45
|
+
========
|
|
46
|
+
|
|
47
|
+
Project and probe records:
|
|
48
|
+
|
|
49
|
+
- :class:`ProjectSpec`, a frozen record of one study.
|
|
50
|
+
- :class:`ProbeSpec`, a frozen record of how one study's trajectories are
|
|
51
|
+
probed, built beside its :class:`ProjectSpec`.
|
|
52
|
+
- :data:`CHAIN_SIZE_OBSERVABLES`, the names a :class:`ProbeSpec` may list as
|
|
53
|
+
chain-size observables.
|
|
54
|
+
- :class:`BinderSpec`, a frozen record of how the binding of a study's
|
|
55
|
+
two-patch binders to its chain is counted, held by the :class:`ProbeSpec`
|
|
56
|
+
of a study with binders.
|
|
57
|
+
- :data:`BINDER_SERIES`, the eight binding counts and the stems of their
|
|
58
|
+
time series.
|
|
59
|
+
- :data:`PerWhole`, a number or a function of a whole's parser, and
|
|
60
|
+
:data:`RecenterT`, how a probe moves a frame's positions.
|
|
61
|
+
|
|
62
|
+
Analysis records:
|
|
63
|
+
|
|
64
|
+
- :class:`PropertySpec` describes one probe artifact; :func:`tseries`,
|
|
65
|
+
:func:`histogram`, :func:`histogram_2d`, :func:`edge`, and
|
|
66
|
+
:func:`matrix_tseries` build the kinds the studies use.
|
|
67
|
+
- :class:`AnalysisTable` lists the properties of one group.
|
|
68
|
+
- :data:`PostHook`, the type of a post-hook; the hooks themselves are in
|
|
69
|
+
:mod:`polyphys.analyze.pipeline`.
|
|
70
|
+
|
|
71
|
+
Dependencies
|
|
72
|
+
============
|
|
73
|
+
- Python standard library: `dataclasses`, `math`, `numbers`, `types`,
|
|
74
|
+
`typing`.
|
|
75
|
+
- :mod:`polyphys.manage.parser`: :class:`~polyphys.manage.parser.ParserBase`.
|
|
76
|
+
- :mod:`polyphys.manage.types`: domain type aliases such as
|
|
77
|
+
:py:obj:`~.GroupT`.
|
|
78
|
+
|
|
79
|
+
Notation
|
|
80
|
+
========
|
|
81
|
+
:math:`\\phi_c`
|
|
82
|
+
The bulk volume fraction of crowders, the parser attribute
|
|
83
|
+
``phi_bulk_c``: the fraction of the volume available to crowder centers
|
|
84
|
+
that the crowders fill (dimensionless).
|
|
85
|
+
:math:`\\Delta`
|
|
86
|
+
The rounding step, `divisor`, of :math:`\\phi_c` in the
|
|
87
|
+
``phi_bulk_c_round`` column (dimensionless).
|
|
88
|
+
|
|
89
|
+
The *property*, *relation*, *whole type*, *post-hook*, and *lineage of a
|
|
90
|
+
table* of the analysis records are as :mod:`polyphys.analyze.pipeline`,
|
|
91
|
+
"Notation", defines them.
|
|
92
|
+
|
|
93
|
+
Examples
|
|
94
|
+
========
|
|
95
|
+
>>> from polyphys.tests.synthetic import ChainCyl
|
|
96
|
+
>>> spec = ProjectSpec(
|
|
97
|
+
... parser=ChainCyl, group='bug', divisor=0.025,
|
|
98
|
+
... crowder_attr='phi_bulk_c',
|
|
99
|
+
... physical_attrs=('space', 'nmon', 'dcyl', 'dcrowd'),
|
|
100
|
+
... species_sizes={'Mon': 'dmon', 'Crd': 'dcrowd'},
|
|
101
|
+
... tseries_properties=('gyrTMon',))
|
|
102
|
+
>>> spec.geometry, spec.space_pattern, spec.hierarchy
|
|
103
|
+
('cylindrical', 'N*D*ac*', 'N*')
|
|
104
|
+
>>> table = AnalysisTable('bug', 'whole', False, (tseries('gyrT', 'Mon'),))
|
|
105
|
+
>>> [prop.prop for prop in table.properties]
|
|
106
|
+
['gyrTMon']
|
|
107
|
+
"""
|
|
108
|
+
import math
|
|
109
|
+
import numbers
|
|
110
|
+
from dataclasses import dataclass, field
|
|
111
|
+
from types import MappingProxyType
|
|
112
|
+
from typing import (
|
|
113
|
+
TYPE_CHECKING, Callable, Literal, Mapping, TypeGuard, get_args)
|
|
114
|
+
|
|
115
|
+
from polyphys.manage.parser import ParserBase
|
|
116
|
+
from polyphys.manage.types import (
|
|
117
|
+
DirectionT,
|
|
118
|
+
EntityT,
|
|
119
|
+
GeometryT,
|
|
120
|
+
GroupT,
|
|
121
|
+
PrimitiveLineageT,
|
|
122
|
+
PropertyT,
|
|
123
|
+
TopologyT,
|
|
124
|
+
WholeRelationT,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if TYPE_CHECKING: # pragma: no cover - for the type checker only
|
|
128
|
+
from polyphys.analyze.pipeline import ArrayWholes, Derived, HookContext
|
|
129
|
+
|
|
130
|
+
__all__ = [
|
|
131
|
+
'BINDER_SERIES',
|
|
132
|
+
'CHAIN_SIZE_OBSERVABLES',
|
|
133
|
+
'AnalysisTable',
|
|
134
|
+
'BinderSpec',
|
|
135
|
+
'PerWhole',
|
|
136
|
+
'PostHook',
|
|
137
|
+
'ProbeSpec',
|
|
138
|
+
'ProjectSpec',
|
|
139
|
+
'PropertySpec',
|
|
140
|
+
'RecenterT',
|
|
141
|
+
'edge',
|
|
142
|
+
'histogram',
|
|
143
|
+
'histogram_2d',
|
|
144
|
+
'matrix_tseries',
|
|
145
|
+
'tseries',
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
# Project and probe records
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
#: A quantity of a probe that is either the same number for every whole of a
|
|
153
|
+
#: study or a function of the whole's parser, such as a bin size that scales
|
|
154
|
+
#: with the crowder diameter. :meth:`ProbeSpec.resolve` turns either into the
|
|
155
|
+
#: number for one whole.
|
|
156
|
+
#:
|
|
157
|
+
#: .. versionadded:: 0.7.0
|
|
158
|
+
PerWhole = float | Callable[[ParserBase], float]
|
|
159
|
+
|
|
160
|
+
#: How a probe moves the positions of a frame before binning them. The
|
|
161
|
+
#: LAMMPS dump reader of MDAnalysis 2.4 and later translates every frame so
|
|
162
|
+
#: that the lower box bounds sit at the origin, which undoes the ``fix
|
|
163
|
+
#: recenter`` of the studies' LAMMPS scripts. ``'half'`` subtracts half the
|
|
164
|
+
#: box lengths of the frame, which restores the LAMMPS frame exactly;
|
|
165
|
+
#: ``'none'`` keeps the reader's frame; a triple is subtracted as given, so
|
|
166
|
+
#: the research code's shift by half the parser's ``(dcyl, dcyl, lcyl)`` or
|
|
167
|
+
#: ``lcube`` is reproducible.
|
|
168
|
+
#:
|
|
169
|
+
#: .. versionadded:: 0.7.0
|
|
170
|
+
RecenterT = Literal['none', 'half'] | tuple[float, float, float]
|
|
171
|
+
|
|
172
|
+
# The directions of the density profiles in each geometry.
|
|
173
|
+
_DIRECTIONS: dict[GeometryT, tuple[DirectionT, ...]] = {
|
|
174
|
+
'cubic': ('r',),
|
|
175
|
+
'cylindrical': ('r', 'z'),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#: The chain-size observables that
|
|
179
|
+
#: :class:`~polyphys.probe.chain_size.ChainSize` measures per frame, in the
|
|
180
|
+
#: research code's order: each is the name of a time series without its
|
|
181
|
+
#: species label, so ``'gyrT'`` with the chain label ``'Mon'`` is the
|
|
182
|
+
#: ``gyrTMon`` artifact. ``'principalT'`` is a per-frame
|
|
183
|
+
#: :math:`3 \\times 3` matrix; the others are scalars.
|
|
184
|
+
#:
|
|
185
|
+
#: .. versionadded:: 0.7.0
|
|
186
|
+
CHAIN_SIZE_OBSERVABLES: tuple[PropertyT, ...] = (
|
|
187
|
+
'transSizeT', 'fsdT', 'gyrT', 'rfloryT', 'asphericityT', 'shapeT',
|
|
188
|
+
'principalT')
|
|
189
|
+
|
|
190
|
+
# The chain-size observables that are per-frame matrices, which a project's
|
|
191
|
+
# `tseries_properties` does not list.
|
|
192
|
+
_MATRIX_OBSERVABLES: frozenset[PropertyT] = frozenset({'principalT'})
|
|
193
|
+
|
|
194
|
+
#: The eight per-frame binding counts that
|
|
195
|
+
#: :class:`~polyphys.probe.binders.BinderStatistics` measures, in the
|
|
196
|
+
#: research code's order: each attribute of
|
|
197
|
+
#: :class:`~polyphys.analyze.clusters.FrameBinding` mapped to the stem of its
|
|
198
|
+
#: time series and to which label completes the name, ``'patches'`` for the
|
|
199
|
+
#: three patch counts and ``'binders'`` for the five binder counts. With the
|
|
200
|
+
#: labels ``HnsPatch`` and ``HnsCore``, ``n_binders_bridging`` is the
|
|
201
|
+
#: ``nBridgeTHnsCore`` artifact.
|
|
202
|
+
#:
|
|
203
|
+
#: .. versionadded:: 0.7.0
|
|
204
|
+
BINDER_SERIES: Mapping[str, tuple[PropertyT, str]] = MappingProxyType({
|
|
205
|
+
'n_patch_monomer_contacts': ('nBoundT', 'patches'),
|
|
206
|
+
'n_patches_free': ('nFreeT', 'patches'),
|
|
207
|
+
'n_patches_engaged': ('nEngagedT', 'patches'),
|
|
208
|
+
'n_binders_free': ('nFreeT', 'binders'),
|
|
209
|
+
'n_binders_bridging': ('nBridgeT', 'binders'),
|
|
210
|
+
'n_binders_dangling': ('nDangleT', 'binders'),
|
|
211
|
+
'n_pairs_cis': ('nCisT', 'binders'),
|
|
212
|
+
'n_pairs_trans': ('nTransT', 'binders'),
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
# The number of patches per binder that binding_statistics counts.
|
|
216
|
+
_TWO_PATCHES = 2
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@dataclass(frozen=True, slots=True)
|
|
220
|
+
class BinderSpec:
|
|
221
|
+
"""
|
|
222
|
+
How the binding of a study's two-patch binders to its chain is counted: the
|
|
223
|
+
facts :class:`~polyphys.probe.binders.BinderStatistics` needs beyond the
|
|
224
|
+
selections of the :class:`ProbeSpec` that holds this record.
|
|
225
|
+
|
|
226
|
+
.. versionadded:: 0.7.0
|
|
227
|
+
.. versionchanged:: 1.0.0
|
|
228
|
+
`cis_threshold` is keyword-only and has no default.
|
|
229
|
+
|
|
230
|
+
Parameters
|
|
231
|
+
----------
|
|
232
|
+
patches : :py:obj:`~.EntityT`
|
|
233
|
+
The label in the probe spec's `selections` of the binders' patches,
|
|
234
|
+
such as ``'HnsPatch'``, which names the three patch series
|
|
235
|
+
``nBoundT``, ``nFreeT``, and ``nEngagedT``. The patches of binder
|
|
236
|
+
:math:`k` are the ones at positions :math:`2k` and :math:`2k + 1`
|
|
237
|
+
of the selection, in atom-ID order.
|
|
238
|
+
binders : :py:obj:`~.EntityT`
|
|
239
|
+
The label of the binders, such as ``'HnsCore'``, which names the
|
|
240
|
+
five binder series ``nFreeT``, ``nBridgeT``, ``nDangleT``,
|
|
241
|
+
``nCisT``, and ``nTransT``. A label only: the binder cores are not
|
|
242
|
+
selected.
|
|
243
|
+
cutoff : float
|
|
244
|
+
The binding threshold :math:`r_\\mathrm{thres}`, in units of the
|
|
245
|
+
monomer diameter: a patch and a monomer closer than it are in
|
|
246
|
+
contact. Finite and positive. A study constant that the spec
|
|
247
|
+
states, never computed from the particle sizes; see the probe
|
|
248
|
+
guide, "Choosing the cutoff".
|
|
249
|
+
inclusive : bool, default True
|
|
250
|
+
Whether a pair at exactly `cutoff` is a contact
|
|
251
|
+
(:math:`r \\le r_\\mathrm{thres}`, the thesis criterion) or not
|
|
252
|
+
(:math:`r < r_\\mathrm{thres}`, the research probe's).
|
|
253
|
+
cis_threshold : int
|
|
254
|
+
Keyword-only, with no default. The largest genomic distance, in
|
|
255
|
+
bonds, of a cis bridge; non-negative. A study choice: the H-NS
|
|
256
|
+
studies state 3, a bridge with at most two monomers between its
|
|
257
|
+
sites, where the research probe used 4; the probe guide,
|
|
258
|
+
"Choosing the cis threshold", gives both values and why.
|
|
259
|
+
patches_per_binder : int, default 2
|
|
260
|
+
The number of patches per binder. Only ``2`` is accepted, which
|
|
261
|
+
:func:`~polyphys.analyze.clusters.binding_statistics` counts; the
|
|
262
|
+
field is kept so that binders of another valence can be declared
|
|
263
|
+
once a kernel counts them.
|
|
264
|
+
store_distances : bool, default False
|
|
265
|
+
Whether the probe also keeps and saves the monomer-to-patch
|
|
266
|
+
distance matrix of every frame, the input of the threshold sweep
|
|
267
|
+
and of the binder-cluster post-step of the research code.
|
|
268
|
+
|
|
269
|
+
Attributes
|
|
270
|
+
----------
|
|
271
|
+
series : mapping of str to :py:obj:`~.PropertyT`
|
|
272
|
+
Each attribute name of
|
|
273
|
+
:class:`~polyphys.analyze.clusters.FrameBinding` in
|
|
274
|
+
:data:`BINDER_SERIES` mapped to the full name of its time series,
|
|
275
|
+
``<stem><patches>`` or ``<stem><binders>``, in the research code's
|
|
276
|
+
order.
|
|
277
|
+
|
|
278
|
+
Raises
|
|
279
|
+
------
|
|
280
|
+
ValueError
|
|
281
|
+
If `patches` or `binders` is not a non-empty string or the two are
|
|
282
|
+
equal; if `cutoff` is not a finite positive number; if
|
|
283
|
+
`cis_threshold` is a negative integer or not an integer; or if
|
|
284
|
+
`patches_per_binder` is not ``2``.
|
|
285
|
+
|
|
286
|
+
Notes
|
|
287
|
+
-----
|
|
288
|
+
Construction and :attr:`series` are :math:`O(1)`.
|
|
289
|
+
|
|
290
|
+
Examples
|
|
291
|
+
--------
|
|
292
|
+
>>> spec = BinderSpec(patches='HnsPatch', binders='HnsCore',
|
|
293
|
+
... cutoff=0.685, cis_threshold=3)
|
|
294
|
+
>>> spec.series['n_binders_bridging'], spec.series['n_patches_free']
|
|
295
|
+
('nBridgeTHnsCore', 'nFreeTHnsPatch')
|
|
296
|
+
>>> BinderSpec('HnsPatch', 'HnsCore', cutoff=0.685, cis_threshold=3,
|
|
297
|
+
... patches_per_binder=3)
|
|
298
|
+
Traceback (most recent call last):
|
|
299
|
+
...
|
|
300
|
+
ValueError: 'patches_per_binder' must be 2, not 3: ...
|
|
301
|
+
"""
|
|
302
|
+
patches: EntityT
|
|
303
|
+
binders: EntityT
|
|
304
|
+
cutoff: float
|
|
305
|
+
inclusive: bool = True
|
|
306
|
+
patches_per_binder: int = _TWO_PATCHES
|
|
307
|
+
store_distances: bool = False
|
|
308
|
+
cis_threshold: int = field(kw_only=True)
|
|
309
|
+
|
|
310
|
+
def __post_init__(self) -> None:
|
|
311
|
+
for name, label in (('patches', self.patches),
|
|
312
|
+
('binders', self.binders)):
|
|
313
|
+
if not (isinstance(label, str) and label):
|
|
314
|
+
raise ValueError(
|
|
315
|
+
f"'{name}' must be a non-empty species label, not "
|
|
316
|
+
f"{label!r}.")
|
|
317
|
+
if self.patches == self.binders:
|
|
318
|
+
raise ValueError(
|
|
319
|
+
f"'patches' and 'binders' must be different labels, not "
|
|
320
|
+
f"both {self.patches!r}.")
|
|
321
|
+
if not (isinstance(self.cutoff, (int, float))
|
|
322
|
+
and not isinstance(self.cutoff, bool)
|
|
323
|
+
and math.isfinite(self.cutoff) and self.cutoff > 0):
|
|
324
|
+
raise ValueError(
|
|
325
|
+
f"'cutoff' must be a finite positive number, not "
|
|
326
|
+
f"{self.cutoff!r}.")
|
|
327
|
+
if (isinstance(self.cis_threshold, bool)
|
|
328
|
+
or not isinstance(self.cis_threshold, int)
|
|
329
|
+
or self.cis_threshold < 0):
|
|
330
|
+
raise ValueError(
|
|
331
|
+
f"'cis_threshold' must be a non-negative integer, not "
|
|
332
|
+
f"{self.cis_threshold!r}.")
|
|
333
|
+
if (isinstance(self.patches_per_binder, bool)
|
|
334
|
+
or self.patches_per_binder != _TWO_PATCHES):
|
|
335
|
+
raise ValueError(
|
|
336
|
+
f"'patches_per_binder' must be {_TWO_PATCHES}, not "
|
|
337
|
+
f"{self.patches_per_binder!r}: binding_statistics counts "
|
|
338
|
+
"two-patch binders only.")
|
|
339
|
+
|
|
340
|
+
@property
|
|
341
|
+
def series(self) -> Mapping[str, PropertyT]:
|
|
342
|
+
"""
|
|
343
|
+
Each binding count's attribute name mapped to the full name of its
|
|
344
|
+
time series, in the research code's order.
|
|
345
|
+
"""
|
|
346
|
+
labels = {'patches': self.patches, 'binders': self.binders}
|
|
347
|
+
return MappingProxyType({
|
|
348
|
+
attr: f"{stem}{labels[which]}"
|
|
349
|
+
for attr, (stem, which) in BINDER_SERIES.items()})
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _is_positive_number(value: object) -> TypeGuard[numbers.Real]:
|
|
353
|
+
"""Whether `value` is a finite positive real number: a Python or NumPy
|
|
354
|
+
integer or float, never a bool."""
|
|
355
|
+
return (isinstance(value, numbers.Real) and not isinstance(value, bool)
|
|
356
|
+
and math.isfinite(value) and float(value) > 0)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _is_per_whole(value: object) -> bool:
|
|
360
|
+
"""Whether `value` is a finite positive number or a callable."""
|
|
361
|
+
return callable(value) or _is_positive_number(value)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _check_resolved(result: object, subject: str) -> float:
|
|
365
|
+
"""
|
|
366
|
+
Return `result`, a :py:data:`PerWhole` constant or what a callable
|
|
367
|
+
gave, as a float.
|
|
368
|
+
|
|
369
|
+
Raises
|
|
370
|
+
------
|
|
371
|
+
ValueError
|
|
372
|
+
If `result` is not a finite positive number; the message begins
|
|
373
|
+
with `subject`, which names the quantity or the whole.
|
|
374
|
+
"""
|
|
375
|
+
if _is_positive_number(result):
|
|
376
|
+
return float(result)
|
|
377
|
+
raise ValueError(
|
|
378
|
+
f"{subject} must resolve to a finite positive number, not "
|
|
379
|
+
f"{result!r}.")
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@dataclass(frozen=True, slots=True)
|
|
383
|
+
class ProjectSpec:
|
|
384
|
+
"""
|
|
385
|
+
The per-project facts that the analysis of a study needs and its parser
|
|
386
|
+
class does not hold.
|
|
387
|
+
|
|
388
|
+
Every attribute a spec names is checked at construction against the
|
|
389
|
+
attributes its parser sets at the ``ensemble_long`` lineage, the lineage
|
|
390
|
+
whose names the organizer parses for these values.
|
|
391
|
+
|
|
392
|
+
Parameters
|
|
393
|
+
----------
|
|
394
|
+
parser : type of :class:`~polyphys.manage.parser.ParserBase`
|
|
395
|
+
The study's concrete parser class.
|
|
396
|
+
group : :py:obj:`~.GroupT`
|
|
397
|
+
The particle group of the polymer, such as ``'bug'`` or
|
|
398
|
+
``'nucleoid'``; one of the parser's groups, and not ``'all'``.
|
|
399
|
+
divisor : float
|
|
400
|
+
The step :math:`\\Delta > 0` to which the bulk volume fraction of
|
|
401
|
+
crowders :math:`\\phi_c` (`crowder_attr`, dimensionless) is rounded
|
|
402
|
+
in the ``phi_bulk_c_round`` column of the ``space`` tables.
|
|
403
|
+
physical_attrs : tuple of str
|
|
404
|
+
The parser attributes added as columns to the ``space`` tables, in
|
|
405
|
+
column order.
|
|
406
|
+
species_sizes : mapping of :py:obj:`~.EntityT` to str
|
|
407
|
+
Each species label of a density profile, such as ``'Mon'`` or
|
|
408
|
+
``'Crd'``, mapped to the parser attribute that holds its particle
|
|
409
|
+
diameter, in units of the monomer diameter. Stored read-only, in the
|
|
410
|
+
given order.
|
|
411
|
+
tseries_properties : tuple of :py:obj:`~.PropertyT`
|
|
412
|
+
The per-frame time series of the study, named
|
|
413
|
+
``<property>T<Species>``, such as ``'gyrTMon'``.
|
|
414
|
+
crowder_attr : str
|
|
415
|
+
Keyword-only. The parser attribute that holds :math:`\\phi_c`, such
|
|
416
|
+
as ``'phi_bulk_c'``, which the ``space`` tables add as a column and
|
|
417
|
+
round into ``phi_bulk_c_round``. A study choice, so it has no
|
|
418
|
+
default.
|
|
419
|
+
|
|
420
|
+
.. versionadded:: 0.9.0
|
|
421
|
+
|
|
422
|
+
Attributes
|
|
423
|
+
----------
|
|
424
|
+
name : str
|
|
425
|
+
The parser class name, which is the project name.
|
|
426
|
+
geometry : :py:obj:`~.GeometryT`
|
|
427
|
+
The parser's geometry.
|
|
428
|
+
topology : :py:obj:`~.TopologyT`
|
|
429
|
+
The parser's polymer topology.
|
|
430
|
+
directions : tuple of :py:obj:`~.DirectionT`
|
|
431
|
+
The directions of the density profiles: ``('r',)`` in a cube and
|
|
432
|
+
``('r', 'z')`` in a cylinder.
|
|
433
|
+
space_pattern : str
|
|
434
|
+
A glob pattern that matches the study's ``space`` names: each
|
|
435
|
+
keyword of the parser's ``space`` pattern followed by ``*``.
|
|
436
|
+
space_hierarchy : str
|
|
437
|
+
A glob prefix of the ``space`` names: their first keyword and ``*``.
|
|
438
|
+
hierarchy : str
|
|
439
|
+
A glob prefix of the ``ensemble_long`` names: their first keyword and
|
|
440
|
+
``*``.
|
|
441
|
+
|
|
442
|
+
Raises
|
|
443
|
+
------
|
|
444
|
+
TypeError
|
|
445
|
+
If `parser` is not a concrete subclass of
|
|
446
|
+
:class:`~polyphys.manage.parser.ParserBase`.
|
|
447
|
+
ValueError
|
|
448
|
+
If `group` is ``'all'`` or not one of the parser's groups; if
|
|
449
|
+
`divisor` is not a finite positive number; if `crowder_attr` is not
|
|
450
|
+
a non-empty string; or if `physical_attrs`, `species_sizes`, or
|
|
451
|
+
`crowder_attr` names an attribute the parser does not set at
|
|
452
|
+
``ensemble_long``.
|
|
453
|
+
|
|
454
|
+
Examples
|
|
455
|
+
--------
|
|
456
|
+
>>> from polyphys.tests.synthetic import ChainCyl, RingCubBinders
|
|
457
|
+
>>> spec = ProjectSpec(
|
|
458
|
+
... parser=RingCubBinders, group='nucleoid', divisor=0.01,
|
|
459
|
+
... crowder_attr='phi_bulk_c',
|
|
460
|
+
... physical_attrs=('space', 'nmon', 'nhns', 'dcrowd'),
|
|
461
|
+
... species_sizes={'Mon': 'dmon', 'Crd': 'dcrowd', 'Hns': 'dhns'},
|
|
462
|
+
... tseries_properties=('gyrTMon',))
|
|
463
|
+
>>> spec.name, spec.geometry, spec.directions
|
|
464
|
+
('RingCubBinders', 'cubic', ('r',))
|
|
465
|
+
>>> spec.space_pattern, spec.space_hierarchy, spec.hierarchy
|
|
466
|
+
('N*nh*ac*', 'N*', 'N*')
|
|
467
|
+
>>> spec.species_sizes['Hns']
|
|
468
|
+
'dhns'
|
|
469
|
+
|
|
470
|
+
A spec that names an attribute the parser does not set fails at once:
|
|
471
|
+
|
|
472
|
+
>>> ProjectSpec(ChainCyl, 'bug', 0.025, ('nhns',), {}, (),
|
|
473
|
+
... crowder_attr='phi_bulk_c')
|
|
474
|
+
Traceback (most recent call last):
|
|
475
|
+
...
|
|
476
|
+
ValueError: ChainCyl does not set ['nhns'] at 'ensemble_long'.
|
|
477
|
+
|
|
478
|
+
Notes
|
|
479
|
+
-----
|
|
480
|
+
Construction costs :math:`O(a + s + n)` for :math:`a` physical
|
|
481
|
+
attributes, :math:`s` species, and the :math:`n` names that
|
|
482
|
+
:meth:`~polyphys.manage.parser.ParserBase.attributes_at` collects; every
|
|
483
|
+
derived attribute is :math:`O(1)`.
|
|
484
|
+
"""
|
|
485
|
+
parser: type[ParserBase]
|
|
486
|
+
group: GroupT
|
|
487
|
+
divisor: float
|
|
488
|
+
physical_attrs: tuple[str, ...]
|
|
489
|
+
species_sizes: Mapping[EntityT, str] = field(hash=False)
|
|
490
|
+
tseries_properties: tuple[PropertyT, ...]
|
|
491
|
+
crowder_attr: str = field(kw_only=True)
|
|
492
|
+
|
|
493
|
+
def __post_init__(self) -> None:
|
|
494
|
+
# Copy the containers so that a caller's list or dict cannot change
|
|
495
|
+
# the spec afterwards.
|
|
496
|
+
object.__setattr__(self, 'physical_attrs', tuple(self.physical_attrs))
|
|
497
|
+
object.__setattr__(self, 'tseries_properties',
|
|
498
|
+
tuple(self.tseries_properties))
|
|
499
|
+
object.__setattr__(self, 'species_sizes',
|
|
500
|
+
MappingProxyType(dict(self.species_sizes)))
|
|
501
|
+
parser = self.parser
|
|
502
|
+
if not (isinstance(parser, type) and issubclass(parser, ParserBase)
|
|
503
|
+
and parser._patterns is not None):
|
|
504
|
+
raise TypeError(
|
|
505
|
+
f"'parser' must be a concrete ParserBase subclass, not "
|
|
506
|
+
f"{parser!r}.")
|
|
507
|
+
name = parser.__name__
|
|
508
|
+
if self.group == 'all' or self.group not in parser._groups:
|
|
509
|
+
raise ValueError(
|
|
510
|
+
f"{name}: 'group' must be the polymer group, one of "
|
|
511
|
+
f"{[g for g in parser._groups if g != 'all']}, not "
|
|
512
|
+
f"{self.group!r}.")
|
|
513
|
+
if not (math.isfinite(self.divisor) and self.divisor > 0):
|
|
514
|
+
raise ValueError(
|
|
515
|
+
f"{name}: 'divisor' must be a finite positive number, not "
|
|
516
|
+
f"{self.divisor!r}.")
|
|
517
|
+
if not (isinstance(self.crowder_attr, str) and self.crowder_attr):
|
|
518
|
+
raise ValueError(
|
|
519
|
+
f"{name}: 'crowder_attr' must be a non-empty attribute name, "
|
|
520
|
+
f"not {self.crowder_attr!r}.")
|
|
521
|
+
available = parser.attributes_at('ensemble_long')
|
|
522
|
+
named = [*self.physical_attrs, *self.species_sizes.values(),
|
|
523
|
+
self.crowder_attr]
|
|
524
|
+
missing = sorted({attr for attr in named if attr not in available})
|
|
525
|
+
if missing:
|
|
526
|
+
raise ValueError(
|
|
527
|
+
f"{name} does not set {missing} at 'ensemble_long'.")
|
|
528
|
+
|
|
529
|
+
@property
|
|
530
|
+
def name(self) -> str:
|
|
531
|
+
"""
|
|
532
|
+
The parser class name, which is the project name.
|
|
533
|
+
"""
|
|
534
|
+
return self.parser.__name__
|
|
535
|
+
|
|
536
|
+
@property
|
|
537
|
+
def geometry(self) -> GeometryT:
|
|
538
|
+
"""
|
|
539
|
+
The parser's geometry.
|
|
540
|
+
"""
|
|
541
|
+
return self.parser._geometry
|
|
542
|
+
|
|
543
|
+
@property
|
|
544
|
+
def topology(self) -> TopologyT:
|
|
545
|
+
"""
|
|
546
|
+
The parser's polymer topology.
|
|
547
|
+
"""
|
|
548
|
+
return self.parser._topology
|
|
549
|
+
|
|
550
|
+
@property
|
|
551
|
+
def directions(self) -> tuple[DirectionT, ...]:
|
|
552
|
+
"""
|
|
553
|
+
The directions of the density profiles in the parser's geometry.
|
|
554
|
+
"""
|
|
555
|
+
return _DIRECTIONS[self.geometry]
|
|
556
|
+
|
|
557
|
+
@property
|
|
558
|
+
def space_pattern(self) -> str:
|
|
559
|
+
"""
|
|
560
|
+
A glob pattern that matches the study's ``space`` names.
|
|
561
|
+
"""
|
|
562
|
+
return ''.join(f"{keyword}*"
|
|
563
|
+
for keyword in self.parser.pattern('space'))
|
|
564
|
+
|
|
565
|
+
@property
|
|
566
|
+
def space_hierarchy(self) -> str:
|
|
567
|
+
"""
|
|
568
|
+
A glob prefix of the ``space`` names: their first keyword and ``*``.
|
|
569
|
+
"""
|
|
570
|
+
return f"{self.parser.pattern('space')[0]}*"
|
|
571
|
+
|
|
572
|
+
@property
|
|
573
|
+
def hierarchy(self) -> str:
|
|
574
|
+
"""
|
|
575
|
+
A glob prefix of the ``ensemble_long`` names: their first keyword
|
|
576
|
+
and ``*``.
|
|
577
|
+
"""
|
|
578
|
+
return f"{self.parser.pattern('ensemble_long')[0]}*"
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
@dataclass(frozen=True, slots=True)
|
|
582
|
+
class ProbeSpec:
|
|
583
|
+
"""
|
|
584
|
+
How the trajectories of one study are probed: the facts that
|
|
585
|
+
:mod:`polyphys.probe` needs beyond the study's :class:`ProjectSpec`.
|
|
586
|
+
|
|
587
|
+
A probe spec is built beside the project spec, in the study's own
|
|
588
|
+
folder, and reads the parser class and the geometry from it rather than
|
|
589
|
+
storing them twice. Every observable it names is checked at construction
|
|
590
|
+
against :data:`CHAIN_SIZE_OBSERVABLES` and, for the scalar ones, against
|
|
591
|
+
the project's ``tseries_properties``.
|
|
592
|
+
|
|
593
|
+
.. versionadded:: 0.7.0
|
|
594
|
+
|
|
595
|
+
Parameters
|
|
596
|
+
----------
|
|
597
|
+
project : ProjectSpec
|
|
598
|
+
The study's project spec.
|
|
599
|
+
selections : mapping of :py:obj:`~.EntityT` to str
|
|
600
|
+
Each species label, such as ``'Mon'`` or ``'Foci'``, mapped to the
|
|
601
|
+
MDAnalysis selection string that picks its atoms, such as
|
|
602
|
+
``'resid 1'`` or ``'type 2'``. Stored read-only, in the given order.
|
|
603
|
+
chain : :py:obj:`~.EntityT`
|
|
604
|
+
The label in `selections` of the polymer chain, whose chain-size
|
|
605
|
+
time series are named ``<observable><chain>``.
|
|
606
|
+
observables : tuple of :py:obj:`~.PropertyT`
|
|
607
|
+
The chain-size observables measured per frame, each a name in
|
|
608
|
+
:data:`CHAIN_SIZE_OBSERVABLES`, in the order the probe stores them.
|
|
609
|
+
unwrap : bool
|
|
610
|
+
Whether the chain is unwrapped across the periodic boundaries before
|
|
611
|
+
its gyration tensor is formed for ``asphericityT``, the `unwrap`
|
|
612
|
+
argument of ``AtomGroup.asphericity``: ``False`` for a cylinder,
|
|
613
|
+
which is periodic along its axis only, and ``True`` for a periodic
|
|
614
|
+
cube. Every other observable, ``shapeT`` included, reads the
|
|
615
|
+
positions as the dump gives them; see
|
|
616
|
+
:class:`polyphys.probe.chain_size.ChainSize`.
|
|
617
|
+
bin_sizes : mapping of :py:obj:`~.DirectionT` to :py:data:`PerWhole`, \
|
|
618
|
+
default {}
|
|
619
|
+
The bin size of the histogram along each direction: in units of the
|
|
620
|
+
monomer diameter for a spatial direction, such as ``'r'`` or
|
|
621
|
+
``'z'``, and in radians for the angle ``'theta'``. Each is a finite
|
|
622
|
+
positive number, or a function of a whole's parser returning one,
|
|
623
|
+
for a size that scales with a whole's particle diameters. Stored
|
|
624
|
+
read-only. Left empty by a study whose histograms are not probed.
|
|
625
|
+
histogram_species : tuple of :py:obj:`~.EntityT`, default ()
|
|
626
|
+
The labels in `selections` of the species whose spatial histograms
|
|
627
|
+
:class:`~polyphys.probe.histograms.SpatialHistograms` collects, each
|
|
628
|
+
once. Left empty by a study whose histograms are not probed; otherwise
|
|
629
|
+
`bin_sizes` and `block_frames` are required.
|
|
630
|
+
block_frames : int or None, default None
|
|
631
|
+
The block length :math:`b` of the two-dimensional maps: the number of
|
|
632
|
+
consecutive frames whose maps are averaged into one stored map, a
|
|
633
|
+
positive integer. Required with `histogram_species`.
|
|
634
|
+
recenter : :py:data:`RecenterT`, default 'half'
|
|
635
|
+
How the positions of a frame are moved before they are binned:
|
|
636
|
+
``'half'`` subtracts half the frame's box lengths, which restores the
|
|
637
|
+
LAMMPS frame the dump reader translated; ``'none'`` keeps the reader's
|
|
638
|
+
frame; a triple ``(dx, dy, dz)`` is subtracted as given. Stored as a
|
|
639
|
+
tuple of floats when given as a triple.
|
|
640
|
+
pair_species : :py:obj:`~.EntityT` or None, default None
|
|
641
|
+
The label in `selections` of the species whose pair distances and
|
|
642
|
+
contacts :class:`~polyphys.probe.pairs.PairDistances` collects, such as
|
|
643
|
+
``'Foci'``; ``None`` when the study has no such species. Requires
|
|
644
|
+
`contact_cutoff`.
|
|
645
|
+
contact_cutoff : :py:data:`PerWhole` or None, default None
|
|
646
|
+
The center-to-center distance, in units of the monomer diameter, at
|
|
647
|
+
or below which two particles of `pair_species` are in direct
|
|
648
|
+
contact: a finite positive number or a function of a whole's parser,
|
|
649
|
+
such as the sum of the large-monomer and crowder diameters.
|
|
650
|
+
minimum_image : bool, default True
|
|
651
|
+
Whether the pair distances use the minimum-image convention along
|
|
652
|
+
the periodic directions of the geometry: the axis of a cylinder, all
|
|
653
|
+
three directions of a cube.
|
|
654
|
+
binders : BinderSpec, optional
|
|
655
|
+
How the binding of the study's two-patch binders is counted, for a
|
|
656
|
+
study with binders such as the H-NS ones; ``None`` for a study
|
|
657
|
+
without. Its `patches` must be a label in `selections` other than
|
|
658
|
+
`chain`, and each of its eight time series must be one of the
|
|
659
|
+
project's ``tseries_properties``.
|
|
660
|
+
|
|
661
|
+
Attributes
|
|
662
|
+
----------
|
|
663
|
+
name : str
|
|
664
|
+
The project name, from the project spec.
|
|
665
|
+
parser : type of :class:`~polyphys.manage.parser.ParserBase`
|
|
666
|
+
The study's parser class, from the project spec.
|
|
667
|
+
geometry : :py:obj:`~.GeometryT`
|
|
668
|
+
The study's geometry, from the project spec.
|
|
669
|
+
|
|
670
|
+
Raises
|
|
671
|
+
------
|
|
672
|
+
TypeError
|
|
673
|
+
If `project` is not a :class:`ProjectSpec`, or if `binders` is
|
|
674
|
+
neither ``None`` nor a :class:`BinderSpec`.
|
|
675
|
+
ValueError
|
|
676
|
+
If `selections` is empty or has a label or a selection that is not
|
|
677
|
+
a non-empty string; if `chain` is not a label in `selections`; if
|
|
678
|
+
`observables` is empty, repeats a name, or names an observable that
|
|
679
|
+
is not in :data:`CHAIN_SIZE_OBSERVABLES`; if a scalar observable
|
|
680
|
+
with the chain label is not one of the project's
|
|
681
|
+
``tseries_properties``; if a bin size is keyed by an unknown
|
|
682
|
+
direction or is neither a finite positive number nor callable; if a
|
|
683
|
+
histogram species is not a label in `selections` or repeats, or is
|
|
684
|
+
given without `bin_sizes` or `block_frames`; if `block_frames` is
|
|
685
|
+
not a positive integer; if `recenter` is neither ``'none'``,
|
|
686
|
+
``'half'``, nor a triple of finite numbers; if `pair_species` is not
|
|
687
|
+
a label in `selections` or is given without `contact_cutoff`; if
|
|
688
|
+
`contact_cutoff` is neither a finite positive number nor callable;
|
|
689
|
+
or if the patches of `binders` are not a label in `selections`
|
|
690
|
+
other than `chain`, or one of its time series is not among the
|
|
691
|
+
project's ``tseries_properties``.
|
|
692
|
+
|
|
693
|
+
Examples
|
|
694
|
+
--------
|
|
695
|
+
>>> from polyphys.tests.synthetic import ChainCyl
|
|
696
|
+
>>> project = ProjectSpec(
|
|
697
|
+
... parser=ChainCyl, group='bug', divisor=0.025,
|
|
698
|
+
... crowder_attr='phi_bulk_c',
|
|
699
|
+
... physical_attrs=('space', 'nmon', 'dcyl', 'dcrowd'),
|
|
700
|
+
... species_sizes={'Mon': 'dmon', 'Crd': 'dcrowd'},
|
|
701
|
+
... tseries_properties=('fsdTMon', 'gyrTMon', 'rfloryTMon'))
|
|
702
|
+
>>> probe = ProbeSpec(
|
|
703
|
+
... project=project, selections={'Mon': 'resid 1'}, chain='Mon',
|
|
704
|
+
... observables=('rfloryT', 'fsdT', 'gyrT'), unwrap=False)
|
|
705
|
+
>>> probe.name, probe.geometry, probe.selections['Mon']
|
|
706
|
+
('ChainCyl', 'cylindrical', 'resid 1')
|
|
707
|
+
|
|
708
|
+
An observable the project's time series do not list fails at once:
|
|
709
|
+
|
|
710
|
+
>>> ProbeSpec(project, {'Mon': 'resid 1'}, 'Mon', ('shapeT',), False)
|
|
711
|
+
Traceback (most recent call last):
|
|
712
|
+
...
|
|
713
|
+
ValueError: ChainCyl: 'observables' names ['shapeTMon'], which ...
|
|
714
|
+
|
|
715
|
+
A study whose histograms are probed names the species, the bin sizes,
|
|
716
|
+
which may depend on the whole, and the block length; the bin sizes are
|
|
717
|
+
resolved for one whole from its parser:
|
|
718
|
+
|
|
719
|
+
>>> probe = ProbeSpec(
|
|
720
|
+
... project=project, selections={'Mon': 'resid 1', 'Crd': 'resid 0'},
|
|
721
|
+
... chain='Mon', observables=('gyrT',), unwrap=False,
|
|
722
|
+
... bin_sizes={'r': lambda info: 0.1 * min(info.dmon, info.dcrowd),
|
|
723
|
+
... 'z': 0.5},
|
|
724
|
+
... histogram_species=('Mon', 'Crd'), block_frames=100)
|
|
725
|
+
>>> whole = ChainCyl(
|
|
726
|
+
... 'N200r10.5lz50ac0.5nc100dt0.005bdump1adump1ens1',
|
|
727
|
+
... 'whole', 'all')
|
|
728
|
+
>>> probe.resolve(probe.bin_sizes['r'], whole), probe.recenter
|
|
729
|
+
(0.05, 'half')
|
|
730
|
+
|
|
731
|
+
A study with binders adds a :class:`BinderSpec`:
|
|
732
|
+
|
|
733
|
+
>>> hns = ProjectSpec(
|
|
734
|
+
... parser=ChainCyl, group='bug', divisor=0.025,
|
|
735
|
+
... crowder_attr='phi_bulk_c',
|
|
736
|
+
... physical_attrs=('space', 'nmon', 'dcyl', 'dcrowd'),
|
|
737
|
+
... species_sizes={'Mon': 'dmon', 'Crd': 'dcrowd'},
|
|
738
|
+
... tseries_properties=(
|
|
739
|
+
... 'gyrTMon', 'nBoundTHnsPatch', 'nFreeTHnsPatch',
|
|
740
|
+
... 'nEngagedTHnsPatch', 'nFreeTHnsCore', 'nBridgeTHnsCore',
|
|
741
|
+
... 'nDangleTHnsCore', 'nCisTHnsCore', 'nTransTHnsCore'))
|
|
742
|
+
>>> probe = ProbeSpec(
|
|
743
|
+
... project=hns, selections={'Mon': 'resid 1', 'HnsPatch': 'type 2'},
|
|
744
|
+
... chain='Mon', observables=('gyrT',), unwrap=False,
|
|
745
|
+
... binders=BinderSpec('HnsPatch', 'HnsCore', cutoff=0.685,
|
|
746
|
+
... cis_threshold=3))
|
|
747
|
+
>>> probe.binders.series['n_pairs_cis']
|
|
748
|
+
'nCisTHnsCore'
|
|
749
|
+
|
|
750
|
+
Notes
|
|
751
|
+
-----
|
|
752
|
+
Construction costs :math:`O(s + o + b + h)` for :math:`s` selections,
|
|
753
|
+
:math:`o` observables, :math:`b` bin sizes, and :math:`h` histogram
|
|
754
|
+
species; every derived attribute is :math:`O(1)`.
|
|
755
|
+
"""
|
|
756
|
+
project: ProjectSpec
|
|
757
|
+
selections: Mapping[EntityT, str] = field(hash=False)
|
|
758
|
+
chain: EntityT
|
|
759
|
+
observables: tuple[PropertyT, ...]
|
|
760
|
+
unwrap: bool
|
|
761
|
+
bin_sizes: Mapping[DirectionT, PerWhole] = field(
|
|
762
|
+
default_factory=dict, hash=False)
|
|
763
|
+
histogram_species: tuple[EntityT, ...] = ()
|
|
764
|
+
block_frames: int | None = None
|
|
765
|
+
recenter: RecenterT = 'half'
|
|
766
|
+
pair_species: EntityT | None = None
|
|
767
|
+
contact_cutoff: PerWhole | None = None
|
|
768
|
+
minimum_image: bool = True
|
|
769
|
+
binders: BinderSpec | None = None
|
|
770
|
+
|
|
771
|
+
def __post_init__(self) -> None:
|
|
772
|
+
if not isinstance(self.project, ProjectSpec):
|
|
773
|
+
raise TypeError(
|
|
774
|
+
f"'project' must be a ProjectSpec, not {self.project!r}.")
|
|
775
|
+
if self.binders is not None \
|
|
776
|
+
and not isinstance(self.binders, BinderSpec):
|
|
777
|
+
raise TypeError(
|
|
778
|
+
f"'binders' must be a BinderSpec or None, not "
|
|
779
|
+
f"{self.binders!r}.")
|
|
780
|
+
name = self.project.name
|
|
781
|
+
# Copy the containers so that a caller's dict or list cannot change
|
|
782
|
+
# the spec afterwards.
|
|
783
|
+
object.__setattr__(self, 'selections',
|
|
784
|
+
MappingProxyType(dict(self.selections)))
|
|
785
|
+
object.__setattr__(self, 'observables', tuple(self.observables))
|
|
786
|
+
object.__setattr__(self, 'bin_sizes',
|
|
787
|
+
MappingProxyType(dict(self.bin_sizes)))
|
|
788
|
+
if not self.selections or any(
|
|
789
|
+
not (isinstance(key, str) and key
|
|
790
|
+
and isinstance(value, str) and value)
|
|
791
|
+
for key, value in self.selections.items()):
|
|
792
|
+
raise ValueError(
|
|
793
|
+
f"{name}: 'selections' must map each species label to a "
|
|
794
|
+
f"non-empty selection string, not {dict(self.selections)!r}.")
|
|
795
|
+
if self.chain not in self.selections:
|
|
796
|
+
raise ValueError(
|
|
797
|
+
f"{name}: 'chain' must be a label in 'selections', one of "
|
|
798
|
+
f"{list(self.selections)}, not {self.chain!r}.")
|
|
799
|
+
if not self.observables:
|
|
800
|
+
raise ValueError(f"{name}: 'observables' must not be empty.")
|
|
801
|
+
unknown = [obs for obs in self.observables
|
|
802
|
+
if obs not in CHAIN_SIZE_OBSERVABLES]
|
|
803
|
+
if unknown:
|
|
804
|
+
raise ValueError(
|
|
805
|
+
f"{name}: 'observables' names {unknown}, which are not in "
|
|
806
|
+
f"{list(CHAIN_SIZE_OBSERVABLES)}.")
|
|
807
|
+
if len(set(self.observables)) != len(self.observables):
|
|
808
|
+
raise ValueError(
|
|
809
|
+
f"{name}: 'observables' repeats a name: "
|
|
810
|
+
f"{list(self.observables)}.")
|
|
811
|
+
unlisted = [f"{obs}{self.chain}" for obs in self.observables
|
|
812
|
+
if obs not in _MATRIX_OBSERVABLES
|
|
813
|
+
and f"{obs}{self.chain}"
|
|
814
|
+
not in self.project.tseries_properties]
|
|
815
|
+
if unlisted:
|
|
816
|
+
raise ValueError(
|
|
817
|
+
f"{name}: 'observables' names {unlisted}, which are not "
|
|
818
|
+
f"among the project's 'tseries_properties' "
|
|
819
|
+
f"{list(self.project.tseries_properties)}.")
|
|
820
|
+
directions = get_args(DirectionT)
|
|
821
|
+
for direction, size in self.bin_sizes.items():
|
|
822
|
+
if direction not in directions:
|
|
823
|
+
raise ValueError(
|
|
824
|
+
f"{name}: 'bin_sizes' has the direction {direction!r}; "
|
|
825
|
+
f"each must be one of {list(directions)}.")
|
|
826
|
+
if not _is_per_whole(size):
|
|
827
|
+
raise ValueError(
|
|
828
|
+
f"{name}: the bin size along {direction!r} must be a "
|
|
829
|
+
f"finite positive number or a callable, not {size!r}.")
|
|
830
|
+
object.__setattr__(self, 'histogram_species',
|
|
831
|
+
tuple(self.histogram_species))
|
|
832
|
+
unselected = [species for species in self.histogram_species
|
|
833
|
+
if species not in self.selections]
|
|
834
|
+
if unselected:
|
|
835
|
+
raise ValueError(
|
|
836
|
+
f"{name}: 'histogram_species' names {unselected}, which are "
|
|
837
|
+
f"not labels in 'selections' {list(self.selections)}.")
|
|
838
|
+
if len(set(self.histogram_species)) != len(self.histogram_species):
|
|
839
|
+
raise ValueError(
|
|
840
|
+
f"{name}: 'histogram_species' repeats a label: "
|
|
841
|
+
f"{list(self.histogram_species)}.")
|
|
842
|
+
if self.histogram_species and not self.bin_sizes:
|
|
843
|
+
raise ValueError(
|
|
844
|
+
f"{name}: 'histogram_species' needs 'bin_sizes'.")
|
|
845
|
+
if self.histogram_species and self.block_frames is None:
|
|
846
|
+
raise ValueError(
|
|
847
|
+
f"{name}: 'histogram_species' needs 'block_frames'.")
|
|
848
|
+
if self.block_frames is not None and not (
|
|
849
|
+
isinstance(self.block_frames, int)
|
|
850
|
+
and not isinstance(self.block_frames, bool)
|
|
851
|
+
and self.block_frames > 0):
|
|
852
|
+
raise ValueError(
|
|
853
|
+
f"{name}: 'block_frames' must be a positive integer, not "
|
|
854
|
+
f"{self.block_frames!r}.")
|
|
855
|
+
if self.recenter not in ('none', 'half'):
|
|
856
|
+
shift = self.recenter
|
|
857
|
+
if not (isinstance(shift, (tuple, list)) and len(shift) == 3
|
|
858
|
+
and all(isinstance(value, (int, float))
|
|
859
|
+
and not isinstance(value, bool)
|
|
860
|
+
and math.isfinite(value) for value in shift)):
|
|
861
|
+
raise ValueError(
|
|
862
|
+
f"{name}: 'recenter' must be 'none', 'half', or a triple "
|
|
863
|
+
f"of finite numbers, not {shift!r}.")
|
|
864
|
+
object.__setattr__(self, 'recenter',
|
|
865
|
+
tuple(float(value) for value in shift))
|
|
866
|
+
if self.pair_species is not None:
|
|
867
|
+
if self.pair_species not in self.selections:
|
|
868
|
+
raise ValueError(
|
|
869
|
+
f"{name}: 'pair_species' must be a label in "
|
|
870
|
+
f"'selections', one of {list(self.selections)}, not "
|
|
871
|
+
f"{self.pair_species!r}.")
|
|
872
|
+
if self.contact_cutoff is None:
|
|
873
|
+
raise ValueError(
|
|
874
|
+
f"{name}: 'pair_species' needs 'contact_cutoff'.")
|
|
875
|
+
if self.contact_cutoff is not None and not _is_per_whole(
|
|
876
|
+
self.contact_cutoff):
|
|
877
|
+
raise ValueError(
|
|
878
|
+
f"{name}: 'contact_cutoff' must be a finite positive number "
|
|
879
|
+
f"or a callable, not {self.contact_cutoff!r}.")
|
|
880
|
+
|
|
881
|
+
if self.binders is not None:
|
|
882
|
+
if self.binders.patches not in self.selections \
|
|
883
|
+
or self.binders.patches == self.chain:
|
|
884
|
+
raise ValueError(
|
|
885
|
+
f"{name}: the patches of 'binders' must be a label in "
|
|
886
|
+
f"'selections' other than the chain {self.chain!r}, "
|
|
887
|
+
f"not {self.binders.patches!r}.")
|
|
888
|
+
unlisted = [
|
|
889
|
+
series for series in self.binders.series.values()
|
|
890
|
+
if series not in self.project.tseries_properties]
|
|
891
|
+
if unlisted:
|
|
892
|
+
raise ValueError(
|
|
893
|
+
f"{name}: 'binders' names the series {unlisted}, which "
|
|
894
|
+
f"are not among the project's 'tseries_properties' "
|
|
895
|
+
f"{list(self.project.tseries_properties)}.")
|
|
896
|
+
|
|
897
|
+
@staticmethod
|
|
898
|
+
def resolve(value: PerWhole, parser: ParserBase) -> float:
|
|
899
|
+
"""
|
|
900
|
+
The number a :py:data:`PerWhole` quantity takes for one whole.
|
|
901
|
+
|
|
902
|
+
Parameters
|
|
903
|
+
----------
|
|
904
|
+
value : :py:data:`PerWhole`
|
|
905
|
+
A number, returned as a float, or a function of `parser`.
|
|
906
|
+
parser : ParserBase
|
|
907
|
+
The whole's parser, passed to a callable `value`.
|
|
908
|
+
|
|
909
|
+
Returns
|
|
910
|
+
-------
|
|
911
|
+
float
|
|
912
|
+
The number for this whole.
|
|
913
|
+
|
|
914
|
+
Raises
|
|
915
|
+
------
|
|
916
|
+
ValueError
|
|
917
|
+
If `value`, or what a callable `value` returns, is anything but
|
|
918
|
+
a finite positive number: a Python or NumPy integer or float,
|
|
919
|
+
not a bool.
|
|
920
|
+
|
|
921
|
+
Examples
|
|
922
|
+
--------
|
|
923
|
+
>>> from polyphys.tests.synthetic import ChainCyl
|
|
924
|
+
>>> whole = ChainCyl(
|
|
925
|
+
... 'N200r10.5lz50ac0.5nc100dt0.005bdump1adump1ens1',
|
|
926
|
+
... 'whole', 'all')
|
|
927
|
+
>>> ProbeSpec.resolve(0.5, whole)
|
|
928
|
+
0.5
|
|
929
|
+
>>> ProbeSpec.resolve(lambda info: info.dmon + info.dcrowd, whole)
|
|
930
|
+
1.5
|
|
931
|
+
>>> ProbeSpec.resolve(-1.0, whole)
|
|
932
|
+
Traceback (most recent call last):
|
|
933
|
+
...
|
|
934
|
+
ValueError: A per-whole quantity of N200r10.5lz50ac0.5nc100dt0.005b...
|
|
935
|
+
|
|
936
|
+
Notes
|
|
937
|
+
-----
|
|
938
|
+
:math:`O(1)` beyond the cost of the callable.
|
|
939
|
+
|
|
940
|
+
.. versionadded:: 0.7.0
|
|
941
|
+
|
|
942
|
+
.. versionchanged:: 1.0.0
|
|
943
|
+
A callable may return a NumPy integer or float, such as
|
|
944
|
+
``numpy.float32``, where only a Python number or
|
|
945
|
+
``numpy.float64`` was accepted. A constant `value` is checked
|
|
946
|
+
with the same rule, where it was returned as ``float(value)``.
|
|
947
|
+
"""
|
|
948
|
+
result = value(parser) if callable(value) else value
|
|
949
|
+
return _check_resolved(
|
|
950
|
+
result, f"A per-whole quantity of {parser.name}")
|
|
951
|
+
|
|
952
|
+
@property
|
|
953
|
+
def name(self) -> str:
|
|
954
|
+
"""
|
|
955
|
+
The project name, from the project spec.
|
|
956
|
+
"""
|
|
957
|
+
return self.project.name
|
|
958
|
+
|
|
959
|
+
@property
|
|
960
|
+
def parser(self) -> type[ParserBase]:
|
|
961
|
+
"""
|
|
962
|
+
The study's parser class, from the project spec.
|
|
963
|
+
"""
|
|
964
|
+
return self.project.parser
|
|
965
|
+
|
|
966
|
+
@property
|
|
967
|
+
def geometry(self) -> GeometryT:
|
|
968
|
+
"""
|
|
969
|
+
The study's geometry, from the project spec.
|
|
970
|
+
"""
|
|
971
|
+
return self.project.geometry
|
|
972
|
+
|
|
973
|
+
# ---------------------------------------------------------------------------
|
|
974
|
+
# Analysis records
|
|
975
|
+
# ---------------------------------------------------------------------------
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
#: A post-hook ``(wholes, context) -> tuple[Derived, ...]``: see
|
|
979
|
+
#: :mod:`polyphys.analyze.pipeline`, "Notation", and its hooks. A hook may
|
|
980
|
+
#: also declare the names of the artifacts it returns with a method
|
|
981
|
+
#: ``outputs(prop) -> tuple[str, ...]``, which :class:`AnalysisTable` reads
|
|
982
|
+
#: to reject a repeated name when the table is built; every hook of
|
|
983
|
+
#: :mod:`polyphys.analyze.pipeline` declares them.
|
|
984
|
+
#:
|
|
985
|
+
#: .. versionadded:: 0.6.0
|
|
986
|
+
#:
|
|
987
|
+
#: .. versionchanged:: 0.9.0
|
|
988
|
+
#: The optional ``outputs`` method.
|
|
989
|
+
PostHook = Callable[['ArrayWholes', 'HookContext'], tuple['Derived', ...]]
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
@dataclass(frozen=True)
|
|
993
|
+
class PropertySpec:
|
|
994
|
+
"""
|
|
995
|
+
One probe artifact of a particle group, and how it reduces.
|
|
996
|
+
|
|
997
|
+
Attributes
|
|
998
|
+
----------
|
|
999
|
+
name : str
|
|
1000
|
+
The property's name without its species, such as ``'gyrT'`` or
|
|
1001
|
+
``'rHist'``.
|
|
1002
|
+
species : str
|
|
1003
|
+
The species the property is measured on, such as ``'Mon'``; the
|
|
1004
|
+
empty string for a property without one, such as an edge file.
|
|
1005
|
+
relation : {'tseries', 'histogram', 'bin_edge'}
|
|
1006
|
+
How the segments of a whole combine; see
|
|
1007
|
+
:mod:`polyphys.analyze.pipeline`, "Notation".
|
|
1008
|
+
whole_type : {'vector', 'matrix', 'bin_edge'}
|
|
1009
|
+
The shape of one whole as loaded; see
|
|
1010
|
+
:mod:`polyphys.analyze.pipeline`, "Notation".
|
|
1011
|
+
edge_prop : str, optional
|
|
1012
|
+
The property of the file that holds this histogram's bin edges, such
|
|
1013
|
+
as ``'rEdgeMon'`` for ``'rHistMon'``. Only for a ``'vector'``
|
|
1014
|
+
``'histogram'``.
|
|
1015
|
+
post_hook : callable, optional
|
|
1016
|
+
A post-hook ``(wholes, context) -> tuple[Derived, ...]``; see
|
|
1017
|
+
:mod:`polyphys.analyze.pipeline`, "Notation". ``None`` passes the
|
|
1018
|
+
wholes through under `prop`.
|
|
1019
|
+
|
|
1020
|
+
Raises
|
|
1021
|
+
------
|
|
1022
|
+
ValueError
|
|
1023
|
+
If `name` is empty; if `relation` or `whole_type` is unknown; if
|
|
1024
|
+
exactly one of them is ``'bin_edge'``; or if `edge_prop` is given
|
|
1025
|
+
for anything but a ``'vector'`` ``'histogram'``.
|
|
1026
|
+
|
|
1027
|
+
Notes
|
|
1028
|
+
-----
|
|
1029
|
+
The named constructors :func:`tseries`, :func:`histogram`,
|
|
1030
|
+
:func:`histogram_2d`, :func:`edge`, and :func:`matrix_tseries` build the
|
|
1031
|
+
kinds the studies use, so a table reads as data.
|
|
1032
|
+
|
|
1033
|
+
Examples
|
|
1034
|
+
--------
|
|
1035
|
+
>>> spec = PropertySpec('rHist', 'Mon', 'histogram', 'vector',
|
|
1036
|
+
... edge_prop='rEdgeMon')
|
|
1037
|
+
>>> spec.prop, spec.has_edges
|
|
1038
|
+
('rHistMon', True)
|
|
1039
|
+
|
|
1040
|
+
An edge file is a ``'bin_edge'`` in both its relation and its shape:
|
|
1041
|
+
|
|
1042
|
+
>>> PropertySpec('xEdge', '', 'bin_edge', 'vector')
|
|
1043
|
+
Traceback (most recent call last):
|
|
1044
|
+
...
|
|
1045
|
+
ValueError: 'xEdge': relation 'bin_edge' and whole_type 'vector' ...
|
|
1046
|
+
|
|
1047
|
+
.. versionadded:: 0.6.0
|
|
1048
|
+
"""
|
|
1049
|
+
|
|
1050
|
+
name: str
|
|
1051
|
+
species: EntityT
|
|
1052
|
+
relation: WholeRelationT
|
|
1053
|
+
whole_type: Literal['vector', 'matrix', 'bin_edge']
|
|
1054
|
+
edge_prop: PropertyT | None = None
|
|
1055
|
+
post_hook: 'PostHook | None' = None
|
|
1056
|
+
|
|
1057
|
+
def __post_init__(self) -> None:
|
|
1058
|
+
if not self.name:
|
|
1059
|
+
raise ValueError("A property's name must not be empty.")
|
|
1060
|
+
if self.relation not in ('tseries', 'histogram', 'bin_edge'):
|
|
1061
|
+
raise ValueError(
|
|
1062
|
+
f"'{self.prop}': relation must be 'tseries', 'histogram', or "
|
|
1063
|
+
f"'bin_edge', not '{self.relation}'."
|
|
1064
|
+
)
|
|
1065
|
+
if self.whole_type not in ('vector', 'matrix', 'bin_edge'):
|
|
1066
|
+
raise ValueError(
|
|
1067
|
+
f"'{self.prop}': whole_type must be 'vector', 'matrix', or "
|
|
1068
|
+
f"'bin_edge', not '{self.whole_type}'."
|
|
1069
|
+
)
|
|
1070
|
+
if (self.relation == 'bin_edge') != (self.whole_type == 'bin_edge'):
|
|
1071
|
+
raise ValueError(
|
|
1072
|
+
f"'{self.prop}': relation '{self.relation}' and whole_type "
|
|
1073
|
+
f"'{self.whole_type}' do not match; an edge file is "
|
|
1074
|
+
"'bin_edge' in both."
|
|
1075
|
+
)
|
|
1076
|
+
if self.edge_prop is not None and not (
|
|
1077
|
+
self.relation == 'histogram' and self.whole_type == 'vector'
|
|
1078
|
+
):
|
|
1079
|
+
raise ValueError(
|
|
1080
|
+
f"'{self.prop}': edge_prop is only valid for a 'vector' "
|
|
1081
|
+
f"'histogram', not a '{self.whole_type}' '{self.relation}'."
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
@property
|
|
1085
|
+
def prop(self) -> PropertyT:
|
|
1086
|
+
"""
|
|
1087
|
+
The property as it appears in file names, `name` then `species`.
|
|
1088
|
+
"""
|
|
1089
|
+
return self.name + self.species
|
|
1090
|
+
|
|
1091
|
+
@property
|
|
1092
|
+
def has_edges(self) -> bool:
|
|
1093
|
+
"""
|
|
1094
|
+
Whether the property has a bin-edge file.
|
|
1095
|
+
"""
|
|
1096
|
+
return self.edge_prop is not None
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
@dataclass(frozen=True)
|
|
1100
|
+
class AnalysisTable:
|
|
1101
|
+
"""
|
|
1102
|
+
The analysis of one particle group of a study, as data.
|
|
1103
|
+
|
|
1104
|
+
Attributes
|
|
1105
|
+
----------
|
|
1106
|
+
group : str
|
|
1107
|
+
The particle group whose artifacts the table reduces.
|
|
1108
|
+
lineage : {'segment', 'whole'}
|
|
1109
|
+
The lineage of the group's probe artifacts; see
|
|
1110
|
+
:mod:`polyphys.analyze.pipeline`, "Notation".
|
|
1111
|
+
stamps : bool
|
|
1112
|
+
Whether the group's stamp files are combined by
|
|
1113
|
+
:func:`~polyphys.analyze.pipeline.reduce_stamps`.
|
|
1114
|
+
properties : tuple of PropertySpec
|
|
1115
|
+
The artifacts to reduce, each listed once.
|
|
1116
|
+
|
|
1117
|
+
Raises
|
|
1118
|
+
------
|
|
1119
|
+
ValueError
|
|
1120
|
+
If `lineage` is unknown; if two properties name the same artifact;
|
|
1121
|
+
or if two outputs share a name, among the property of each entry
|
|
1122
|
+
without a hook and the names each hook declares with its
|
|
1123
|
+
``outputs`` method, so that no output overwrites another when
|
|
1124
|
+
:func:`~polyphys.analyze.pipeline.reduce_space` runs. A hook that
|
|
1125
|
+
declares nothing is checked when it runs. As a hook's
|
|
1126
|
+
``outputs`` raises, such as for a hook given a property it cannot
|
|
1127
|
+
reduce.
|
|
1128
|
+
|
|
1129
|
+
Examples
|
|
1130
|
+
--------
|
|
1131
|
+
>>> table = AnalysisTable('bug', 'whole', True, (tseries('gyrT', 'Mon'),))
|
|
1132
|
+
>>> table.lineage, len(table.properties)
|
|
1133
|
+
('whole', 1)
|
|
1134
|
+
|
|
1135
|
+
Listing an artifact twice is an error, so a table cannot reduce one file
|
|
1136
|
+
two ways by accident:
|
|
1137
|
+
|
|
1138
|
+
>>> AnalysisTable('bug', 'whole', True,
|
|
1139
|
+
... (tseries('gyrT', 'Mon'), tseries('gyrT', 'Mon')))
|
|
1140
|
+
Traceback (most recent call last):
|
|
1141
|
+
...
|
|
1142
|
+
ValueError: Properties listed more than once: ['gyrTMon'].
|
|
1143
|
+
|
|
1144
|
+
So is a hook output that another entry also produces:
|
|
1145
|
+
|
|
1146
|
+
>>> from polyphys.analyze.pipeline import TimeAverage
|
|
1147
|
+
>>> AnalysisTable('bug', 'whole', True, (
|
|
1148
|
+
... tseries('gyrT', 'Mon', post_hook=TimeAverage('fsdT')),
|
|
1149
|
+
... tseries('fsdT', 'Mon')))
|
|
1150
|
+
Traceback (most recent call last):
|
|
1151
|
+
...
|
|
1152
|
+
ValueError: Outputs named more than once: ['fsdTMon'].
|
|
1153
|
+
|
|
1154
|
+
Notes
|
|
1155
|
+
-----
|
|
1156
|
+
Construction is :math:`O(p + o)` for :math:`p` properties and the
|
|
1157
|
+
:math:`o` outputs their hooks declare.
|
|
1158
|
+
|
|
1159
|
+
.. versionadded:: 0.6.0
|
|
1160
|
+
|
|
1161
|
+
.. versionchanged:: 0.9.0
|
|
1162
|
+
Repeated output names are rejected.
|
|
1163
|
+
"""
|
|
1164
|
+
|
|
1165
|
+
group: GroupT
|
|
1166
|
+
lineage: PrimitiveLineageT
|
|
1167
|
+
stamps: bool
|
|
1168
|
+
properties: tuple[PropertySpec, ...]
|
|
1169
|
+
|
|
1170
|
+
def __post_init__(self) -> None:
|
|
1171
|
+
if self.lineage not in ('segment', 'whole'):
|
|
1172
|
+
raise ValueError(
|
|
1173
|
+
f"lineage must be 'segment' or 'whole', not "
|
|
1174
|
+
f"'{self.lineage}'."
|
|
1175
|
+
)
|
|
1176
|
+
object.__setattr__(self, 'properties', tuple(self.properties))
|
|
1177
|
+
seen: set[str] = set()
|
|
1178
|
+
repeated: list[str] = []
|
|
1179
|
+
for prop in self.properties:
|
|
1180
|
+
if prop.prop in seen and prop.prop not in repeated:
|
|
1181
|
+
repeated.append(prop.prop)
|
|
1182
|
+
seen.add(prop.prop)
|
|
1183
|
+
if repeated:
|
|
1184
|
+
raise ValueError(f"Properties listed more than once: {repeated}.")
|
|
1185
|
+
outputs: set[PropertyT] = set()
|
|
1186
|
+
repeated_outputs: list[PropertyT] = []
|
|
1187
|
+
for prop in self.properties:
|
|
1188
|
+
for name in _declared_outputs(prop):
|
|
1189
|
+
if name in outputs and name not in repeated_outputs:
|
|
1190
|
+
repeated_outputs.append(name)
|
|
1191
|
+
outputs.add(name)
|
|
1192
|
+
if repeated_outputs:
|
|
1193
|
+
raise ValueError(
|
|
1194
|
+
f"Outputs named more than once: {repeated_outputs}.")
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def _declared_outputs(prop: PropertySpec) -> tuple[PropertyT, ...]:
|
|
1198
|
+
"""
|
|
1199
|
+
The names of the artifacts that `prop` yields: its own without a hook,
|
|
1200
|
+
the hook's declared ``outputs``, or none for a hook that declares none.
|
|
1201
|
+
"""
|
|
1202
|
+
if prop.post_hook is None:
|
|
1203
|
+
return (prop.prop,)
|
|
1204
|
+
declare = getattr(prop.post_hook, 'outputs', None)
|
|
1205
|
+
if declare is None:
|
|
1206
|
+
return ()
|
|
1207
|
+
return tuple(declare(prop))
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
def tseries(
|
|
1211
|
+
name: str, species: EntityT, post_hook: 'PostHook | None' = None
|
|
1212
|
+
) -> PropertySpec:
|
|
1213
|
+
"""
|
|
1214
|
+
A scalar time series: one value per frame, concatenated over segments.
|
|
1215
|
+
|
|
1216
|
+
Parameters
|
|
1217
|
+
----------
|
|
1218
|
+
name : str
|
|
1219
|
+
The property's name, such as ``'gyrT'``.
|
|
1220
|
+
species : str
|
|
1221
|
+
The species, such as ``'Mon'``.
|
|
1222
|
+
post_hook : callable, optional
|
|
1223
|
+
A post-hook; see :class:`PropertySpec`.
|
|
1224
|
+
|
|
1225
|
+
Returns
|
|
1226
|
+
-------
|
|
1227
|
+
PropertySpec
|
|
1228
|
+
A ``'tseries'`` ``'vector'`` property.
|
|
1229
|
+
|
|
1230
|
+
Examples
|
|
1231
|
+
--------
|
|
1232
|
+
>>> tseries('gyrT', 'Mon').prop
|
|
1233
|
+
'gyrTMon'
|
|
1234
|
+
|
|
1235
|
+
.. versionadded:: 0.6.0
|
|
1236
|
+
"""
|
|
1237
|
+
return PropertySpec(name, species, 'tseries', 'vector',
|
|
1238
|
+
post_hook=post_hook)
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
def histogram(
|
|
1242
|
+
direction: DirectionT, species: EntityT,
|
|
1243
|
+
post_hook: 'PostHook | None' = None,
|
|
1244
|
+
) -> PropertySpec:
|
|
1245
|
+
"""
|
|
1246
|
+
A one-dimensional histogram with its bin-edge file.
|
|
1247
|
+
|
|
1248
|
+
Parameters
|
|
1249
|
+
----------
|
|
1250
|
+
direction : str
|
|
1251
|
+
The direction, such as ``'r'``, ``'z'``, or ``'theta'``: the property
|
|
1252
|
+
is ``<direction>Hist<species>`` and its edges
|
|
1253
|
+
``<direction>Edge<species>``.
|
|
1254
|
+
species : str
|
|
1255
|
+
The species, such as ``'Crd'``.
|
|
1256
|
+
post_hook : callable, optional
|
|
1257
|
+
A post-hook; see :class:`PropertySpec`.
|
|
1258
|
+
|
|
1259
|
+
Returns
|
|
1260
|
+
-------
|
|
1261
|
+
PropertySpec
|
|
1262
|
+
A ``'histogram'`` ``'vector'`` property with `edge_prop` set.
|
|
1263
|
+
|
|
1264
|
+
Examples
|
|
1265
|
+
--------
|
|
1266
|
+
>>> spec = histogram('r', 'Crd')
|
|
1267
|
+
>>> spec.prop, spec.edge_prop
|
|
1268
|
+
('rHistCrd', 'rEdgeCrd')
|
|
1269
|
+
|
|
1270
|
+
.. versionadded:: 0.6.0
|
|
1271
|
+
"""
|
|
1272
|
+
return PropertySpec(f'{direction}Hist', species, 'histogram', 'vector',
|
|
1273
|
+
edge_prop=f'{direction}Edge{species}',
|
|
1274
|
+
post_hook=post_hook)
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
def histogram_2d(plane: str, species: EntityT) -> PropertySpec:
|
|
1278
|
+
"""
|
|
1279
|
+
A two-dimensional histogram, summed over segments and stacked over wholes.
|
|
1280
|
+
|
|
1281
|
+
Parameters
|
|
1282
|
+
----------
|
|
1283
|
+
plane : str
|
|
1284
|
+
The plane as the probe phase names it, such as ``'xy'`` or
|
|
1285
|
+
``'xz'``: the property is ``<plane>Hist<species>``. Its edges are
|
|
1286
|
+
the :func:`edge` properties of the plane's two directions.
|
|
1287
|
+
species : str
|
|
1288
|
+
The species, such as ``'Crd'``.
|
|
1289
|
+
|
|
1290
|
+
Returns
|
|
1291
|
+
-------
|
|
1292
|
+
PropertySpec
|
|
1293
|
+
A ``'histogram'`` ``'matrix'`` property.
|
|
1294
|
+
|
|
1295
|
+
Examples
|
|
1296
|
+
--------
|
|
1297
|
+
>>> histogram_2d('xy', 'Crd').prop
|
|
1298
|
+
'xyHistCrd'
|
|
1299
|
+
|
|
1300
|
+
.. versionadded:: 0.6.0
|
|
1301
|
+
"""
|
|
1302
|
+
return PropertySpec(f'{plane}Hist', species, 'histogram', 'matrix')
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
def edge(direction: DirectionT) -> PropertySpec:
|
|
1306
|
+
"""
|
|
1307
|
+
A bin-edge file shared by the two-dimensional histograms of a direction.
|
|
1308
|
+
|
|
1309
|
+
Parameters
|
|
1310
|
+
----------
|
|
1311
|
+
direction : str
|
|
1312
|
+
The direction, such as ``'x'``: the property is
|
|
1313
|
+
``<direction>Edge``, with no species.
|
|
1314
|
+
|
|
1315
|
+
Returns
|
|
1316
|
+
-------
|
|
1317
|
+
PropertySpec
|
|
1318
|
+
A ``'bin_edge'`` property.
|
|
1319
|
+
|
|
1320
|
+
Examples
|
|
1321
|
+
--------
|
|
1322
|
+
>>> edge('x').prop
|
|
1323
|
+
'xEdge'
|
|
1324
|
+
|
|
1325
|
+
.. versionadded:: 0.6.0
|
|
1326
|
+
"""
|
|
1327
|
+
return PropertySpec(f'{direction}Edge', '', 'bin_edge', 'bin_edge')
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
def matrix_tseries(
|
|
1331
|
+
name: str, species: EntityT, post_hook: 'PostHook | None' = None
|
|
1332
|
+
) -> PropertySpec:
|
|
1333
|
+
"""
|
|
1334
|
+
A time series of arrays: one array per frame, concatenated over segments.
|
|
1335
|
+
|
|
1336
|
+
Parameters
|
|
1337
|
+
----------
|
|
1338
|
+
name : str
|
|
1339
|
+
The property's name, such as ``'principalT'`` (one matrix per frame)
|
|
1340
|
+
or ``'clustersHistT'`` (one histogram per frame).
|
|
1341
|
+
species : str
|
|
1342
|
+
The species, such as ``'Mon'``.
|
|
1343
|
+
post_hook : callable, optional
|
|
1344
|
+
A post-hook; see :class:`PropertySpec`.
|
|
1345
|
+
|
|
1346
|
+
Returns
|
|
1347
|
+
-------
|
|
1348
|
+
PropertySpec
|
|
1349
|
+
A ``'tseries'`` ``'matrix'`` property.
|
|
1350
|
+
|
|
1351
|
+
Examples
|
|
1352
|
+
--------
|
|
1353
|
+
>>> matrix_tseries('principalT', 'Mon').whole_type
|
|
1354
|
+
'matrix'
|
|
1355
|
+
|
|
1356
|
+
.. versionadded:: 0.6.0
|
|
1357
|
+
"""
|
|
1358
|
+
return PropertySpec(name, species, 'tseries', 'matrix',
|
|
1359
|
+
post_hook=post_hook)
|