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,1159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
==========================================================
|
|
3
|
+
:mod:`polyphys.analyze.sumrule`
|
|
4
|
+
==========================================================
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
The :mod:`polyphys.analyze.sumrule` module tests the density sum rule of
|
|
8
|
+
Jeon et al. (2016) on the ensemble-averaged density profiles of a space: it
|
|
9
|
+
scales each species' profile by its particle size, normalizes it by a
|
|
10
|
+
reference value, folds a longitudinal profile about the polymer's center,
|
|
11
|
+
and sums the species' scaled profiles state point by state point. The
|
|
12
|
+
profiles come from the ``ensAvg`` stage of the artifact lineage that
|
|
13
|
+
:mod:`polyphys.manage.organizer` builds; the per-frame densities behind them
|
|
14
|
+
are the caller's work.
|
|
15
|
+
|
|
16
|
+
The profiles must be the raw ensemble averages, in their physical units: a
|
|
17
|
+
volume fraction or a number density, not a profile already divided by its
|
|
18
|
+
sum, its integral, or its maximum. The sum rule relates the species to each
|
|
19
|
+
other through their physical scales, so a species normalized on its own
|
|
20
|
+
enters the sum :math:`\\sum_i \\phi_i / a_i` with an arbitrary weight and
|
|
21
|
+
``<prop>-scale`` loses its unit and its comparability with the nominal bulk
|
|
22
|
+
value. Only the ``<prop>-norm`` columns of such a profile survive, because
|
|
23
|
+
dividing by the profile's own ``'first'`` or ``'max'`` value is
|
|
24
|
+
scale-invariant. :func:`space_profiles` warns when a profile sums to one,
|
|
25
|
+
the signature of a profile normalized by its sum, but cannot detect the
|
|
26
|
+
other normalizations from one profile.
|
|
27
|
+
|
|
28
|
+
Notation
|
|
29
|
+
========
|
|
30
|
+
|
|
31
|
+
:math:`a_i`
|
|
32
|
+
The particle diameter of species :math:`i`, in the monomer diameter: the
|
|
33
|
+
parser attribute that the study's ``species_sizes`` names for it.
|
|
34
|
+
:math:`\\phi_i(x)`, :math:`\\rho_i(x)`
|
|
35
|
+
The local volume fraction (dimensionless) and local number density (per
|
|
36
|
+
cubic monomer diameter) of species :math:`i`, related by
|
|
37
|
+
:math:`\\phi_i = \\pi a_i^3 \\rho_i / 6`.
|
|
38
|
+
:math:`r`
|
|
39
|
+
The radial coordinate: the distance from the cylinder axis in a
|
|
40
|
+
cylinder and from the polymer's center of mass in a periodic cube. The
|
|
41
|
+
bins of an ``r`` profile run outward from :math:`r = 0`.
|
|
42
|
+
:math:`z`
|
|
43
|
+
The longitudinal coordinate: the axial distance from the polymer's
|
|
44
|
+
center of mass in a cylinder, with bins symmetric about :math:`z = 0`.
|
|
45
|
+
*scaled profile*
|
|
46
|
+
One term of the sum rule, the column ``<prop>-scale``:
|
|
47
|
+
:math:`\\phi_i / a_i` for a volume fraction and :math:`a_i^2 \\rho_i` for
|
|
48
|
+
a number density.
|
|
49
|
+
*normalizer*
|
|
50
|
+
The reference value a scaled profile, or the species sum, is divided by:
|
|
51
|
+
see :py:obj:`~.NormalizerT`.
|
|
52
|
+
:math:`D`, :math:`a_c`
|
|
53
|
+
The cylinder diameter and a crowder diameter; the ratio
|
|
54
|
+
:math:`\\kappa = (D - a_c) / a_c` sets how far the crowder layering on
|
|
55
|
+
the wall reaches inward.
|
|
56
|
+
|
|
57
|
+
Contents
|
|
58
|
+
========
|
|
59
|
+
|
|
60
|
+
- :func:`normalize_r` and :func:`normalize_z`: normalize one scaled profile,
|
|
61
|
+
folding a longitudinal one about :math:`z = 0`.
|
|
62
|
+
- :func:`space_profiles`: scale, normalize, and annotate every
|
|
63
|
+
ensemble-averaged profile of one species across a space.
|
|
64
|
+
- :func:`space_profiles_from_spec`: the same, with the study's choices read
|
|
65
|
+
from its :class:`~polyphys.spec.ProjectSpec`.
|
|
66
|
+
- :func:`sum_rule_profiles`: sum the species' scaled profiles and normalize
|
|
67
|
+
the sum.
|
|
68
|
+
- :data:`PROFILE_NORMALIZERS` and :data:`SUM_NORMALIZERS`: the default
|
|
69
|
+
normalizers by geometry and direction.
|
|
70
|
+
|
|
71
|
+
Dependencies
|
|
72
|
+
============
|
|
73
|
+
- `numpy` and `pandas`.
|
|
74
|
+
- :mod:`polyphys.manage.organizer`:
|
|
75
|
+
:func:`~polyphys.manage.organizer.make_database` and
|
|
76
|
+
:func:`~polyphys.manage.organizer.create_fullname`.
|
|
77
|
+
- :mod:`polyphys.manage.utils` and :mod:`polyphys.manage.types`.
|
|
78
|
+
- :class:`polyphys.spec.ProjectSpec`.
|
|
79
|
+
|
|
80
|
+
References
|
|
81
|
+
==========
|
|
82
|
+
- C. Jeon, C. Hyeon, Y. Jung, and B.-Y. Ha, "How are molecular crowding and
|
|
83
|
+
the spatial organization of a biopolymer interrelated", Soft Matter 12,
|
|
84
|
+
9786-9796 (2016). https://doi.org/10.1039/c6sm01924b
|
|
85
|
+
- J. Kim, C. Jeon, H. Jeong, Y. Jung, and B.-Y. Ha, "A polymer in a crowded
|
|
86
|
+
and confined space: effects of crowder size and poly-dispersity", Soft
|
|
87
|
+
Matter 11, 1877-1888 (2015). https://doi.org/10.1039/c4sm02198c
|
|
88
|
+
- A. H. Sadeghi Isfahani, PhD thesis, University of Waterloo (2024),
|
|
89
|
+
Chapter 3.
|
|
90
|
+
|
|
91
|
+
.. versionadded:: 0.6.0
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
import logging
|
|
95
|
+
import os
|
|
96
|
+
import warnings
|
|
97
|
+
from glob import glob
|
|
98
|
+
from typing import Any, Callable, Literal, Mapping, Sequence, TYPE_CHECKING
|
|
99
|
+
|
|
100
|
+
import numpy as np
|
|
101
|
+
import pandas as pd
|
|
102
|
+
|
|
103
|
+
from ..manage.organizer import create_fullname, make_database
|
|
104
|
+
from ..manage.types import (
|
|
105
|
+
DirectionT,
|
|
106
|
+
EntityT,
|
|
107
|
+
GeometryT,
|
|
108
|
+
GroupT,
|
|
109
|
+
NormalizerT,
|
|
110
|
+
ParserClassT,
|
|
111
|
+
Pathish,
|
|
112
|
+
)
|
|
113
|
+
from ..manage.utils import invalid_keyword, round_to_multiple, sort_filenames
|
|
114
|
+
from ..spec import ProjectSpec
|
|
115
|
+
|
|
116
|
+
if TYPE_CHECKING:
|
|
117
|
+
# Unused here, but sphinx_autodoc_typehints runs this block to resolve
|
|
118
|
+
# the "ParserBase" forward reference inside ParserClassT.
|
|
119
|
+
from ..manage.parser import ParserBase # noqa: F401
|
|
120
|
+
|
|
121
|
+
__all__ = [
|
|
122
|
+
'PROFILE_NORMALIZERS',
|
|
123
|
+
'SUM_NORMALIZERS',
|
|
124
|
+
'normalize_r',
|
|
125
|
+
'normalize_z',
|
|
126
|
+
'space_profiles',
|
|
127
|
+
'space_profiles_from_spec',
|
|
128
|
+
'sum_rule_profiles',
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
logger = logging.getLogger(__name__)
|
|
132
|
+
|
|
133
|
+
# Relative tolerance, in the largest |z|, for matching a bin center with its
|
|
134
|
+
# mirror when :func:`normalize_z` folds: bin centers built from a symmetric
|
|
135
|
+
# range carry float64 round-off of a few 1e-16 relative, and the smallest
|
|
136
|
+
# intended asymmetry is a fraction of a bin, far above 1e-9 of the range,
|
|
137
|
+
# as for ``_BIN_COUNT_RTOL`` in :mod:`polyphys.analyze.measurer`.
|
|
138
|
+
_FOLD_RTOL = 1e-9
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# The normalizer each density profile takes by default, keyed by (geometry,
|
|
142
|
+
# direction). In a cylinder, r runs from the axis to the wall, whose crowder
|
|
143
|
+
# layering makes the maximum the wrong reference, so the axis value is used;
|
|
144
|
+
# along z and in a periodic cube the maximum is the far-field value of a
|
|
145
|
+
# crowder profile and the center value of a monomer profile.
|
|
146
|
+
PROFILE_NORMALIZERS: dict[tuple[GeometryT, DirectionT], NormalizerT] = {
|
|
147
|
+
('cylindrical', 'r'): 'first',
|
|
148
|
+
('cylindrical', 'z'): 'max',
|
|
149
|
+
('cubic', 'r'): 'max',
|
|
150
|
+
}
|
|
151
|
+
"""The default normalizer of a density profile, by (geometry, direction):
|
|
152
|
+
``'first'`` for ``r`` in a cylinder, ``'max'`` otherwise.
|
|
153
|
+
|
|
154
|
+
.. versionadded:: 0.6.0
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
# The normalizer the species sum takes by default: its own value at the
|
|
158
|
+
# axis in a cylinder, and its far-field value along z and in a cube, where
|
|
159
|
+
# only crowders remain.
|
|
160
|
+
SUM_NORMALIZERS: dict[tuple[GeometryT, DirectionT], NormalizerT] = {
|
|
161
|
+
('cylindrical', 'r'): 'first',
|
|
162
|
+
('cylindrical', 'z'): 'last',
|
|
163
|
+
('cubic', 'r'): 'last',
|
|
164
|
+
}
|
|
165
|
+
"""The default normalizer of the species sum, by (geometry, direction):
|
|
166
|
+
``'first'`` for ``r`` in a cylinder, ``'last'`` otherwise.
|
|
167
|
+
|
|
168
|
+
.. versionadded:: 0.6.0
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _normalizer_value(
|
|
173
|
+
prop: str, profile: pd.DataFrame, normalizer: NormalizerT
|
|
174
|
+
) -> float:
|
|
175
|
+
"""
|
|
176
|
+
Return the value that `normalizer` picks from the ``<prop>-scale``
|
|
177
|
+
column of `profile`: its first or last value, its maximum, the given
|
|
178
|
+
number, or the result of the given function of `profile`.
|
|
179
|
+
|
|
180
|
+
Raises
|
|
181
|
+
------
|
|
182
|
+
ValueError
|
|
183
|
+
If `normalizer` is a string other than ``'first'``, ``'last'``, or
|
|
184
|
+
``'max'``, or neither a string, a number, nor callable.
|
|
185
|
+
"""
|
|
186
|
+
scale = profile[f"{prop}-scale"]
|
|
187
|
+
if isinstance(normalizer, str):
|
|
188
|
+
invalid_keyword(normalizer, ["first", "last", "max"])
|
|
189
|
+
if normalizer == "first":
|
|
190
|
+
return float(scale.iloc[0])
|
|
191
|
+
if normalizer == "last":
|
|
192
|
+
return float(scale.iloc[-1])
|
|
193
|
+
return float(scale.max())
|
|
194
|
+
if isinstance(normalizer, (int, float)) and not isinstance(
|
|
195
|
+
normalizer, bool
|
|
196
|
+
):
|
|
197
|
+
return float(normalizer)
|
|
198
|
+
if callable(normalizer):
|
|
199
|
+
return float(normalizer(profile))
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f"'normalizer' must be 'first', 'last', 'max', a number, or a "
|
|
202
|
+
f"function of the profile, not {normalizer!r}."
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _with_parser(
|
|
207
|
+
func: Callable[[pd.DataFrame, Any], float], info: Any
|
|
208
|
+
) -> Callable[[pd.DataFrame], float]:
|
|
209
|
+
"""
|
|
210
|
+
Bind the parser instance `info` as the second argument of `func`, a
|
|
211
|
+
two-argument normalizer of :func:`space_profiles`.
|
|
212
|
+
"""
|
|
213
|
+
def normalizer(profile: pd.DataFrame) -> float:
|
|
214
|
+
return func(profile, info)
|
|
215
|
+
return normalizer
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _normalize(
|
|
219
|
+
prop: str, profile: pd.DataFrame, normalizer: NormalizerT
|
|
220
|
+
) -> pd.DataFrame:
|
|
221
|
+
"""
|
|
222
|
+
Return a copy of `profile` with the ``<prop>-normalizer`` column set to
|
|
223
|
+
the value `normalizer` picks and ``<prop>-norm`` to ``<prop>-scale``
|
|
224
|
+
divided by it; a zero value warns and sets ``<prop>-norm`` to zero.
|
|
225
|
+
"""
|
|
226
|
+
out = profile.copy()
|
|
227
|
+
value = _normalizer_value(prop, out, normalizer)
|
|
228
|
+
out[f"{prop}-normalizer"] = value
|
|
229
|
+
if value != 0:
|
|
230
|
+
out[f"{prop}-norm"] = out[f"{prop}-scale"] / value
|
|
231
|
+
else:
|
|
232
|
+
warnings.warn(
|
|
233
|
+
f"The normalizer of '{prop}' is zero; its normalized values are"
|
|
234
|
+
" set to zero.",
|
|
235
|
+
UserWarning,
|
|
236
|
+
)
|
|
237
|
+
out[f"{prop}-norm"] = 0.0
|
|
238
|
+
return out
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def normalize_r(
|
|
242
|
+
prop: str,
|
|
243
|
+
ens_avg: pd.DataFrame,
|
|
244
|
+
normalizer: NormalizerT = "first",
|
|
245
|
+
) -> pd.DataFrame:
|
|
246
|
+
"""
|
|
247
|
+
Normalize the scaled radial profile ``<prop>-scale`` of an
|
|
248
|
+
ensemble-averaged distribution by a reference value.
|
|
249
|
+
|
|
250
|
+
The radial coordinate :math:`r` is the distance from the cylinder axis
|
|
251
|
+
in a cylinder and from the polymer's center of mass in a periodic cube,
|
|
252
|
+
in the monomer diameter; the profile's bins run outward from
|
|
253
|
+
:math:`r = 0`.
|
|
254
|
+
|
|
255
|
+
Parameters
|
|
256
|
+
----------
|
|
257
|
+
prop : str
|
|
258
|
+
The full property name, such as ``'rPhiCrd'``; the input has the
|
|
259
|
+
column ``<prop>-scale``.
|
|
260
|
+
ens_avg : pd.DataFrame
|
|
261
|
+
The ensemble-averaged distribution, one row per bin. It is not
|
|
262
|
+
modified.
|
|
263
|
+
normalizer : :py:obj:`~.NormalizerT`, \
|
|
264
|
+
default 'first'
|
|
265
|
+
The reference value: ``'first'`` is the value at :math:`r = 0`,
|
|
266
|
+
``'last'`` the value of the outermost bin, ``'max'`` the maximum;
|
|
267
|
+
a number is used as given; a function is called with `ens_avg` and
|
|
268
|
+
returns the value.
|
|
269
|
+
|
|
270
|
+
Returns
|
|
271
|
+
-------
|
|
272
|
+
pd.DataFrame
|
|
273
|
+
A copy of `ens_avg` with two more columns: ``<prop>-normalizer``,
|
|
274
|
+
the reference value, and ``<prop>-norm``, ``<prop>-scale`` divided
|
|
275
|
+
by it.
|
|
276
|
+
|
|
277
|
+
Raises
|
|
278
|
+
------
|
|
279
|
+
ValueError
|
|
280
|
+
If `normalizer` is none of the accepted forms.
|
|
281
|
+
|
|
282
|
+
Warns
|
|
283
|
+
-----
|
|
284
|
+
UserWarning
|
|
285
|
+
If the reference value is zero; ``<prop>-norm`` is then zero.
|
|
286
|
+
|
|
287
|
+
Examples
|
|
288
|
+
--------
|
|
289
|
+
>>> profile = pd.DataFrame({'bin_center': [0.5, 1.5, 2.5],
|
|
290
|
+
... 'rPhiCrd-scale': [2.0, 4.0, 1.0]})
|
|
291
|
+
>>> normalize_r('rPhiCrd', profile)['rPhiCrd-norm'].tolist()
|
|
292
|
+
[1.0, 2.0, 0.5]
|
|
293
|
+
>>> normalize_r('rPhiCrd', profile, 'max')['rPhiCrd-norm'].tolist()
|
|
294
|
+
[0.5, 1.0, 0.25]
|
|
295
|
+
>>> normalize_r('rPhiCrd', profile, 8.0)['rPhiCrd-norm'].tolist()
|
|
296
|
+
[0.25, 0.5, 0.125]
|
|
297
|
+
|
|
298
|
+
A function sees the whole frame, so it can average the inner bins:
|
|
299
|
+
|
|
300
|
+
>>> normalize_r('rPhiCrd', profile,
|
|
301
|
+
... lambda df: df['rPhiCrd-scale'].iloc[:2].mean()
|
|
302
|
+
... )['rPhiCrd-normalizer'].iloc[0]
|
|
303
|
+
np.float64(3.0)
|
|
304
|
+
|
|
305
|
+
Notes
|
|
306
|
+
-----
|
|
307
|
+
Time and space are :math:`O(n_\\text{bins} \\, n_\\text{cols})` for the
|
|
308
|
+
copy. The references of the sum rule are in :func:`space_profiles`.
|
|
309
|
+
|
|
310
|
+
.. versionadded:: 0.6.0
|
|
311
|
+
"""
|
|
312
|
+
return _normalize(prop, ens_avg, normalizer)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def normalize_z(
|
|
316
|
+
prop: str,
|
|
317
|
+
ens_avg: pd.DataFrame,
|
|
318
|
+
normalizer: NormalizerT = "max",
|
|
319
|
+
fold: bool = True,
|
|
320
|
+
) -> pd.DataFrame:
|
|
321
|
+
"""
|
|
322
|
+
Normalize the scaled longitudinal profile ``<prop>-scale`` of an
|
|
323
|
+
ensemble-averaged distribution by a reference value and, by default,
|
|
324
|
+
fold it about :math:`z = 0`.
|
|
325
|
+
|
|
326
|
+
The longitudinal coordinate :math:`z` is the axial distance from the
|
|
327
|
+
polymer's center of mass, in the monomer diameter. Folding needs bins
|
|
328
|
+
symmetric about :math:`z = 0`; it averages bin :math:`i` with its
|
|
329
|
+
mirror bin :math:`n - 1 - i` and keeps the center bin of an odd count
|
|
330
|
+
as it is; the folded ``bin_center`` is :math:`|z|`. The one exception
|
|
331
|
+
is ``<prop>-scale-normalized_curve``, a distribution over the bins,
|
|
332
|
+
whose mirror bins are summed, so the folded curve is the distribution
|
|
333
|
+
of :math:`|z|` and still sums to one.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
prop : str
|
|
338
|
+
The full property name, such as ``'zRhoMon'``; the input has the
|
|
339
|
+
column ``<prop>-scale``.
|
|
340
|
+
ens_avg : pd.DataFrame
|
|
341
|
+
The ensemble-averaged distribution, one row per bin, with a
|
|
342
|
+
``bin_center`` column when `fold` is True. It is not modified.
|
|
343
|
+
normalizer : :py:obj:`~.NormalizerT`, default 'max'
|
|
344
|
+
The reference value, as in :func:`normalize_r`; ``'max'`` is the
|
|
345
|
+
far-field value of a crowder profile and the center value of a
|
|
346
|
+
monomer profile when each is monotone in :math:`|z|`.
|
|
347
|
+
fold : bool, default True
|
|
348
|
+
Whether to fold the profile about :math:`z = 0`.
|
|
349
|
+
|
|
350
|
+
Returns
|
|
351
|
+
-------
|
|
352
|
+
pd.DataFrame
|
|
353
|
+
A copy of `ens_avg` with ``<prop>-normalizer`` and ``<prop>-norm``
|
|
354
|
+
added, as in :func:`normalize_r`. With `fold`, every numeric column
|
|
355
|
+
is averaged with its mirror bin, the other columns are those of the
|
|
356
|
+
:math:`z \\geq 0` half, and the result has ``ceil(n / 2)`` rows
|
|
357
|
+
ordered by :math:`|z|`.
|
|
358
|
+
|
|
359
|
+
Raises
|
|
360
|
+
------
|
|
361
|
+
ValueError
|
|
362
|
+
If `normalizer` is none of the accepted forms, or `fold` is True
|
|
363
|
+
and `ens_avg` has no ``bin_center`` column or its bin centers do
|
|
364
|
+
not increase symmetrically about :math:`z = 0`, to a tolerance of
|
|
365
|
+
``1e-9`` times the largest :math:`|z|`.
|
|
366
|
+
|
|
367
|
+
Warns
|
|
368
|
+
-----
|
|
369
|
+
UserWarning
|
|
370
|
+
If the reference value is zero; ``<prop>-norm`` is then zero.
|
|
371
|
+
|
|
372
|
+
Examples
|
|
373
|
+
--------
|
|
374
|
+
An even number of bins folds to half as many:
|
|
375
|
+
|
|
376
|
+
>>> profile = pd.DataFrame({'bin_center': [-1.5, -0.5, 0.5, 1.5],
|
|
377
|
+
... 'zRhoMon-scale': [1.0, 3.0, 3.0, 1.0]})
|
|
378
|
+
>>> normalize_z('zRhoMon', profile)
|
|
379
|
+
bin_center zRhoMon-scale zRhoMon-normalizer zRhoMon-norm
|
|
380
|
+
0 0.5 3.0 3.0 1.000000
|
|
381
|
+
1 1.5 1.0 3.0 0.333333
|
|
382
|
+
|
|
383
|
+
An odd number keeps its center bin:
|
|
384
|
+
|
|
385
|
+
>>> profile = pd.DataFrame({'bin_center': [-1.0, 0.0, 1.0],
|
|
386
|
+
... 'zRhoMon-scale': [2.0, 4.0, 2.0]})
|
|
387
|
+
>>> normalize_z('zRhoMon', profile)['zRhoMon-norm'].tolist()
|
|
388
|
+
[1.0, 0.5]
|
|
389
|
+
|
|
390
|
+
Notes
|
|
391
|
+
-----
|
|
392
|
+
The ``-var`` and ``-sem`` columns of the input are averaged like every
|
|
393
|
+
other numeric column, which is what the research code did; the result
|
|
394
|
+
is not the uncertainty of the folded mean, which would need the
|
|
395
|
+
per-replicate folded profiles. Time and space are
|
|
396
|
+
:math:`O(n_\\text{bins} \\, n_\\text{cols})`.
|
|
397
|
+
|
|
398
|
+
.. versionadded:: 0.6.0
|
|
399
|
+
.. versionchanged:: 0.9.0
|
|
400
|
+
Reject bins that are not symmetric about :math:`z = 0`, and fold
|
|
401
|
+
``<prop>-scale-normalized_curve`` by summing mirror bins.
|
|
402
|
+
"""
|
|
403
|
+
out = _normalize(prop, ens_avg, normalizer)
|
|
404
|
+
if not fold:
|
|
405
|
+
return out
|
|
406
|
+
if "bin_center" not in out.columns:
|
|
407
|
+
raise ValueError("Folding needs a 'bin_center' column.")
|
|
408
|
+
centers = out["bin_center"].to_numpy(dtype=np.float64)
|
|
409
|
+
# Mirror bins match to a tolerance relative to the largest |z|, far
|
|
410
|
+
# above float64 round-off in the bin centers and far below a bin width.
|
|
411
|
+
atol = _FOLD_RTOL * float(np.max(np.abs(centers), initial=0.0))
|
|
412
|
+
if np.any(np.diff(centers) <= 0) or not np.allclose(
|
|
413
|
+
centers, -centers[::-1], rtol=0.0, atol=atol
|
|
414
|
+
):
|
|
415
|
+
raise ValueError(
|
|
416
|
+
"Folding needs bin centers that increase and are symmetric"
|
|
417
|
+
f" about z = 0; got {centers.tolist()}."
|
|
418
|
+
)
|
|
419
|
+
n_bins = len(out)
|
|
420
|
+
half = n_bins // 2
|
|
421
|
+
# The z > 0 half in increasing |z|, and the z < 0 half reversed to match
|
|
422
|
+
# it bin for bin; the center bin of an odd count stays on its own.
|
|
423
|
+
pos = out.iloc[n_bins - half:].reset_index(drop=True)
|
|
424
|
+
neg = out.iloc[:half].iloc[::-1].reset_index(drop=True).copy()
|
|
425
|
+
neg["bin_center"] = -neg["bin_center"]
|
|
426
|
+
numeric = out.select_dtypes(include="number").columns
|
|
427
|
+
folded = pos.copy()
|
|
428
|
+
folded[numeric] = 0.5 * (pos[numeric] + neg[numeric])
|
|
429
|
+
# The normalized curve is a distribution over the bins: folding it onto
|
|
430
|
+
# |z| adds the two mirror probabilities, P(|z|) = P(z) + P(-z), so it
|
|
431
|
+
# still sums to one.
|
|
432
|
+
curve = f"{prop}-scale-normalized_curve"
|
|
433
|
+
if curve in folded.columns:
|
|
434
|
+
folded[curve] = pos[curve] + neg[curve]
|
|
435
|
+
if n_bins % 2:
|
|
436
|
+
center = out.iloc[[half]].copy()
|
|
437
|
+
center["bin_center"] = center["bin_center"].abs()
|
|
438
|
+
folded = pd.concat([center, folded], axis=0)
|
|
439
|
+
return folded.reset_index(drop=True)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def space_profiles(
|
|
443
|
+
input_database: str,
|
|
444
|
+
quantity: Literal["Rho", "Phi"],
|
|
445
|
+
direction: DirectionT,
|
|
446
|
+
species: EntityT,
|
|
447
|
+
parser: ParserClassT,
|
|
448
|
+
hierarchy: str,
|
|
449
|
+
physical_attrs: list[str],
|
|
450
|
+
group: GroupT,
|
|
451
|
+
species_sizes: Mapping[EntityT, str],
|
|
452
|
+
*,
|
|
453
|
+
crowders: Sequence[EntityT] = ("Crd",),
|
|
454
|
+
normalizer: (
|
|
455
|
+
Literal["first", "last", "max"] | float
|
|
456
|
+
| Callable[[pd.DataFrame, Any], float] | None
|
|
457
|
+
) = None,
|
|
458
|
+
divisor: float = 0.025,
|
|
459
|
+
round_to: int = 3,
|
|
460
|
+
is_save: bool = False,
|
|
461
|
+
crowder_attr: str = "phi_bulk_c",
|
|
462
|
+
) -> pd.DataFrame:
|
|
463
|
+
"""
|
|
464
|
+
Scale and normalize the ensemble-averaged density profiles of one
|
|
465
|
+
species across a space for the density sum rule, add the physical
|
|
466
|
+
attributes of each state point, and concatenate them into one
|
|
467
|
+
DataFrame.
|
|
468
|
+
|
|
469
|
+
The density sum rule of Jeon et al. (2016), eqns (5) to (7), states that
|
|
470
|
+
in a crowded polymer solution the volume fractions :math:`\\phi_i(x)`
|
|
471
|
+
of the species :math:`i`, each rescaled by its particle diameter
|
|
472
|
+
:math:`a_i`, sum to the far-field value of the crowder term:
|
|
473
|
+
|
|
474
|
+
.. math::
|
|
475
|
+
|
|
476
|
+
\\sum_i \\frac{\\phi_i(x)}{a_i} \\approx \\frac{\\phi_c(\\infty)}{a_c},
|
|
477
|
+
\\qquad
|
|
478
|
+
\\sum_i a_i^2 \\rho_i(x) \\approx a_c^2 \\rho_c(\\infty),
|
|
479
|
+
|
|
480
|
+
where the number density :math:`\\rho_i = 6 \\phi_i / (\\pi a_i^3)`
|
|
481
|
+
makes the two forms the same statement, and :math:`x` is the radial
|
|
482
|
+
coordinate :math:`r` (the distance from the cylinder axis in a cylinder
|
|
483
|
+
and from the polymer's center of mass in a periodic cube) or the
|
|
484
|
+
longitudinal coordinate :math:`z` (the axial distance from the
|
|
485
|
+
polymer's center of mass). This function builds one term of the sum:
|
|
486
|
+
for ``quantity='Phi'`` the scaled profile ``<prop>-scale`` is the
|
|
487
|
+
volume fraction divided by the species' diameter, and for
|
|
488
|
+
``quantity='Rho'`` the number density times the diameter squared.
|
|
489
|
+
:func:`sum_rule_profiles` builds the sum.
|
|
490
|
+
|
|
491
|
+
The input profiles must be the raw ensemble averages, in their physical
|
|
492
|
+
units. A profile normalized beforehand, by its sum, its integral, or its
|
|
493
|
+
maximum, enters the species sum with an arbitrary weight, and its
|
|
494
|
+
``<prop>-scale`` loses its unit; only its ``<prop>-norm`` survives,
|
|
495
|
+
since dividing by the profile's own reference value is scale-invariant.
|
|
496
|
+
A profile whose ``<prop>-mean`` sums to one, as one normalized by its
|
|
497
|
+
sum does, raises a :class:`UserWarning` naming the file; the other
|
|
498
|
+
normalizations cannot be told from one profile.
|
|
499
|
+
|
|
500
|
+
Parameters
|
|
501
|
+
----------
|
|
502
|
+
input_database : str
|
|
503
|
+
Path to the directory of the ensemble-averaged profiles, named
|
|
504
|
+
``<ensemble_long>-<group>-<direction><quantity><species>-ensAvg.csv``,
|
|
505
|
+
ending with a path separator. Each file has a ``bin_center`` column
|
|
506
|
+
and a ``<direction><quantity><species>-mean`` column holding the
|
|
507
|
+
raw ensemble-averaged volume fraction or number density.
|
|
508
|
+
quantity : {'Rho', 'Phi'}
|
|
509
|
+
The number density or the volume fraction.
|
|
510
|
+
direction : {'r', 'z'}
|
|
511
|
+
The coordinate of the profile; ``'z'`` exists in a cylinder only.
|
|
512
|
+
species : EntityT
|
|
513
|
+
The species label of the profile, such as ``'Mon'`` or ``'Crd'``; a
|
|
514
|
+
key of `species_sizes`.
|
|
515
|
+
parser : ParserClassT
|
|
516
|
+
The study's parser class; its geometry chooses the default
|
|
517
|
+
normalizer and the recentered columns.
|
|
518
|
+
hierarchy : str
|
|
519
|
+
Pattern prefix of the filenames, such as ``'N*'``.
|
|
520
|
+
physical_attrs : list of str
|
|
521
|
+
The parser attributes added as columns to the result.
|
|
522
|
+
group : GroupT
|
|
523
|
+
The particle group of the profiles, usually ``'all'``.
|
|
524
|
+
species_sizes : mapping of EntityT to str
|
|
525
|
+
Each species label mapped to the parser attribute holding its
|
|
526
|
+
particle diameter, in the monomer diameter: the ``species_sizes`` of
|
|
527
|
+
the study's :class:`~polyphys.spec.ProjectSpec`.
|
|
528
|
+
crowders : sequence of EntityT, default ('Crd',)
|
|
529
|
+
The crowder species, each a key of `species_sizes`; one set of
|
|
530
|
+
``bin_center`` columns in crowder units is added per crowder.
|
|
531
|
+
normalizer : NormalizerT or callable or None, default None
|
|
532
|
+
The reference value of the normalization. None takes the default of
|
|
533
|
+
:data:`PROFILE_NORMALIZERS` for the parser's geometry and
|
|
534
|
+
`direction`. Otherwise ``'first'``, ``'last'``, ``'max'``, or a
|
|
535
|
+
number, as in :func:`normalize_r`; or a function of two arguments,
|
|
536
|
+
the scaled profile and the parser instance of the file, returning
|
|
537
|
+
the value, so that ``lambda df, info: info.phi_bulk_c / info.dcrowd``
|
|
538
|
+
normalizes a ``'Phi'`` profile by the nominal bulk volume fraction
|
|
539
|
+
of crowders in scale units. An explicit value is logged at INFO
|
|
540
|
+
with the default it replaces.
|
|
541
|
+
divisor : float, default 0.025
|
|
542
|
+
Rounding step of the bulk volume fraction of crowders named by
|
|
543
|
+
`crowder_attr`.
|
|
544
|
+
round_to : int, default 3
|
|
545
|
+
Number of decimal places of that rounding.
|
|
546
|
+
is_save : bool, default False
|
|
547
|
+
If True, save the result as
|
|
548
|
+
``<space>-<group>-<direction><quantity><species>-normalizedRescaled-space.csv``
|
|
549
|
+
in the ``analysis`` phase's ``space`` directory, which
|
|
550
|
+
:func:`~polyphys.manage.organizer.make_database` creates next to
|
|
551
|
+
`input_database`, and log the path at INFO.
|
|
552
|
+
crowder_attr : str, default 'phi_bulk_c'
|
|
553
|
+
The parser attribute holding the bulk volume fraction of crowders,
|
|
554
|
+
added as a column of that name and rounded into
|
|
555
|
+
``phi_bulk_c_round``.
|
|
556
|
+
|
|
557
|
+
Returns
|
|
558
|
+
-------
|
|
559
|
+
pd.DataFrame
|
|
560
|
+
The profiles of every file, concatenated in filename order, with
|
|
561
|
+
``<prop> = <direction><quantity><species>`` and these columns added
|
|
562
|
+
to each file's own:
|
|
563
|
+
|
|
564
|
+
- ``<prop>-scaler``: the diameter :math:`a` for ``'Phi'`` and
|
|
565
|
+
:math:`a^2` for ``'Rho'``; ``<prop>-sumrule_constant`` repeats
|
|
566
|
+
it.
|
|
567
|
+
- ``<prop>-scale``: ``<prop>-mean`` divided by :math:`a` for
|
|
568
|
+
``'Phi'`` and times :math:`a^2` for ``'Rho'``.
|
|
569
|
+
- ``<prop>-scale-normalized_curve``: ``<prop>-scale`` divided by
|
|
570
|
+
its sum over the bins; for ``'z'``, folded by summing mirror
|
|
571
|
+
bins, so it is the distribution of :math:`|z|` and still sums
|
|
572
|
+
to one.
|
|
573
|
+
- ``<prop>-normalizer`` and ``<prop>-norm``: from
|
|
574
|
+
:func:`normalize_r` or, for ``'z'``, :func:`normalize_z` with
|
|
575
|
+
folding, so that a ``'z'`` profile has ``ceil(n / 2)`` rows.
|
|
576
|
+
- ``<prop>-norm-sem``, when the file has it and `normalizer` is
|
|
577
|
+
None: carried through from the file and, for ``'z'``, folded
|
|
578
|
+
like every other numeric column.
|
|
579
|
+
:mod:`polyphys.analyze.pipeline` writes it as the standard error
|
|
580
|
+
over the wholes of each whole's profile divided by its own value
|
|
581
|
+
in the bin the default normalizer names: the SEM across profiles
|
|
582
|
+
normalized separately for each whole. Normalization is
|
|
583
|
+
nonlinear, so it is not generally an uncertainty estimate for
|
|
584
|
+
``<prop>-norm``, the normalized ensemble mean. With an explicit
|
|
585
|
+
`normalizer` the column no longer matches ``<prop>-norm``'s
|
|
586
|
+
reference bin, so it is dropped with a warning.
|
|
587
|
+
- ``bin_center-norm``: ``bin_center`` divided by its maximum.
|
|
588
|
+
- ``ensemble_long``, the file's state-point name; one column per
|
|
589
|
+
entry of `physical_attrs`; `crowder_attr`; and
|
|
590
|
+
``phi_bulk_c_round``.
|
|
591
|
+
- For each crowder of `crowders` with size attribute ``c``:
|
|
592
|
+
``bin_center-<c>``, which is :math:`2 b / a_c` for the bin center
|
|
593
|
+
:math:`b`, the bin center in crowder radii. For ``'r'`` in a
|
|
594
|
+
cylinder of diameter :math:`D` also
|
|
595
|
+
``bin_center-<c>-recentered``, which is
|
|
596
|
+
:math:`2 b / a_c - (D \\bmod a_c) / a_c`, so that the wall sits at
|
|
597
|
+
an integer number of crowder diameters and the layers of
|
|
598
|
+
different state points overlay, and ``bin_center-recentered-norm``
|
|
599
|
+
(``bin_center-recentered-norm-<c>`` with several crowders), which
|
|
600
|
+
is :math:`b - (D \\bmod a_c)` divided by its maximum.
|
|
601
|
+
|
|
602
|
+
Raises
|
|
603
|
+
------
|
|
604
|
+
ValueError
|
|
605
|
+
If `quantity` is not ``'Rho'`` or ``'Phi'``; if the parser's
|
|
606
|
+
geometry and `direction` have no entry in
|
|
607
|
+
:data:`PROFILE_NORMALIZERS`, such as ``'z'`` in a cube; if no file
|
|
608
|
+
matches; or if `normalizer` is none of the accepted forms.
|
|
609
|
+
KeyError
|
|
610
|
+
If `species` or a crowder is not a key of `species_sizes`.
|
|
611
|
+
|
|
612
|
+
Warns
|
|
613
|
+
-----
|
|
614
|
+
UserWarning
|
|
615
|
+
If a file's ``<prop>-mean`` sums to one within ``1e-6``, which marks
|
|
616
|
+
a profile normalized by its sum; the file is processed regardless,
|
|
617
|
+
since a raw profile can sum to one by coincidence. If a file has a
|
|
618
|
+
``<prop>-norm-sem`` column and `normalizer` is not None; the column
|
|
619
|
+
is dropped. Also if a file's reference value is zero; its
|
|
620
|
+
``<prop>-norm`` is then zero.
|
|
621
|
+
|
|
622
|
+
Examples
|
|
623
|
+
--------
|
|
624
|
+
One ensemble-averaged radial volume fraction of crowders of diameter
|
|
625
|
+
``ac2.0`` in a cylinder, normalized by its axis value:
|
|
626
|
+
|
|
627
|
+
>>> import tempfile
|
|
628
|
+
>>> from pathlib import Path
|
|
629
|
+
>>> from polyphys.tests.synthetic import ChainCyl
|
|
630
|
+
>>> tmp = tempfile.TemporaryDirectory()
|
|
631
|
+
>>> database = Path(tmp.name) / 'analysis' / 'N20-all-ensAvg'
|
|
632
|
+
>>> database.mkdir(parents=True)
|
|
633
|
+
>>> state = 'N20r5.5lz25.0ac2.0nc100dt0.005bdump1000adump5000'
|
|
634
|
+
>>> pd.DataFrame({'bin_center': [0.5, 1.5, 2.5],
|
|
635
|
+
... 'rPhiCrd-mean': [0.2, 0.3, 0.1]}).to_csv(
|
|
636
|
+
... database / f'{state}-all-rPhiCrd-ensAvg.csv', index=False)
|
|
637
|
+
>>> space = space_profiles(str(database) + '/', 'Phi', 'r', 'Crd',
|
|
638
|
+
... ChainCyl, 'N*', ['dcyl'], 'all',
|
|
639
|
+
... {'Mon': 'dmon', 'Crd': 'dcrowd'})
|
|
640
|
+
>>> space[['bin_center', 'rPhiCrd-scale', 'rPhiCrd-norm',
|
|
641
|
+
... 'bin_center-dcrowd-recentered']]
|
|
642
|
+
bin_center rPhiCrd-scale rPhiCrd-norm bin_center-dcrowd-recentered
|
|
643
|
+
0 0.5 0.10 1.0 0.5
|
|
644
|
+
1 1.5 0.15 1.5 1.5
|
|
645
|
+
2 2.5 0.05 0.5 2.5
|
|
646
|
+
>>> space['dcyl'].tolist()
|
|
647
|
+
[10.0, 10.0, 10.0]
|
|
648
|
+
>>> tmp.cleanup()
|
|
649
|
+
|
|
650
|
+
Notes
|
|
651
|
+
-----
|
|
652
|
+
- Files are selected by globbing ``input_database + hierarchy +
|
|
653
|
+
'-<group>-<prop>-ensAvg.csv'`` and sorted alphanumerically by name.
|
|
654
|
+
- Let n_files be the number of matched files, n_bins the number of rows
|
|
655
|
+
of each, and n_cols the number of columns of the result. Time is
|
|
656
|
+
:math:`O(n_\\text{files} \\, n_\\text{bins} \\, n_\\text{cols} +
|
|
657
|
+
n_\\text{files} \\log n_\\text{files})`; one file is in memory at a
|
|
658
|
+
time while the finished frames accumulate, so peak memory is about
|
|
659
|
+
twice the result, :math:`O(n_\\text{files} \\, n_\\text{bins} \\,
|
|
660
|
+
n_\\text{cols})`.
|
|
661
|
+
- The default normalizers follow the research code: the value at
|
|
662
|
+
:math:`r = 0` in a cylinder, where the outermost bins are the crowder
|
|
663
|
+
layers on the wall and no far field exists, and the maximum
|
|
664
|
+
otherwise. The sum rule is a local relation and holds in the
|
|
665
|
+
moderate-compaction regime to about 15 percent (Jeon et al., 2016,
|
|
666
|
+
Section 3.1); under cylindrical confinement its radial form holds
|
|
667
|
+
only where the wall layering is weak, which the ratio
|
|
668
|
+
:math:`\\kappa = (D - a_c) / a_c` controls (Sadeghi Isfahani, 2024,
|
|
669
|
+
Chapter 3).
|
|
670
|
+
- ``<prop>-norm`` is the normalized ensemble average, the published
|
|
671
|
+
estimator (Jeon et al., 2016, p. 9788: a time average within each
|
|
672
|
+
run, then over runs, then normalized by the far-field value), not the
|
|
673
|
+
average of per-whole normalized profiles. The file's
|
|
674
|
+
``<prop>-norm-sem`` is the SEM across profiles normalized separately
|
|
675
|
+
for each whole at the same bin. It is not generally an uncertainty
|
|
676
|
+
estimate for the normalized ensemble mean.
|
|
677
|
+
|
|
678
|
+
References
|
|
679
|
+
----------
|
|
680
|
+
- C. Jeon, C. Hyeon, Y. Jung, and B.-Y. Ha, "How are molecular crowding
|
|
681
|
+
and the spatial organization of a biopolymer interrelated", Soft
|
|
682
|
+
Matter 12, 9786-9796 (2016). https://doi.org/10.1039/c6sm01924b
|
|
683
|
+
- J. Kim, C. Jeon, H. Jeong, Y. Jung, and B.-Y. Ha, "A polymer in a
|
|
684
|
+
crowded and confined space: effects of crowder size and
|
|
685
|
+
poly-dispersity", Soft Matter 11, 1877-1888 (2015).
|
|
686
|
+
https://doi.org/10.1039/c4sm02198c
|
|
687
|
+
- A. H. Sadeghi Isfahani, PhD thesis, University of Waterloo (2024),
|
|
688
|
+
Chapter 3.
|
|
689
|
+
|
|
690
|
+
.. versionadded:: 0.6.0
|
|
691
|
+
.. versionchanged:: 0.7.0
|
|
692
|
+
Carry ``<prop>-norm-sem`` through, and warn on a profile that sums
|
|
693
|
+
to one.
|
|
694
|
+
"""
|
|
695
|
+
invalid_keyword(quantity, ["Rho", "Phi"])
|
|
696
|
+
if species not in species_sizes:
|
|
697
|
+
raise KeyError(
|
|
698
|
+
f"Species {species!r} is not in 'species_sizes' "
|
|
699
|
+
f"{sorted(species_sizes)}."
|
|
700
|
+
)
|
|
701
|
+
missing = [label for label in crowders if label not in species_sizes]
|
|
702
|
+
if missing:
|
|
703
|
+
raise KeyError(
|
|
704
|
+
f"Crowders {missing} are not in 'species_sizes' "
|
|
705
|
+
f"{sorted(species_sizes)}."
|
|
706
|
+
)
|
|
707
|
+
geometry = parser._geometry
|
|
708
|
+
try:
|
|
709
|
+
default = PROFILE_NORMALIZERS[(geometry, direction)]
|
|
710
|
+
except KeyError:
|
|
711
|
+
raise ValueError(
|
|
712
|
+
f"No density profile along {direction!r} in a {geometry} "
|
|
713
|
+
f"geometry; the (geometry, direction) pairs are "
|
|
714
|
+
f"{sorted(PROFILE_NORMALIZERS)}."
|
|
715
|
+
) from None
|
|
716
|
+
if normalizer is not None:
|
|
717
|
+
logger.info(
|
|
718
|
+
"Normalizing %s%s%s profiles with %r instead of the %s %s "
|
|
719
|
+
"default %r.", direction, quantity, species, normalizer, geometry,
|
|
720
|
+
direction, default,
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
prop = f"{direction}{quantity}{species}"
|
|
724
|
+
prop_ext = f"-{group}-{prop}-ensAvg.csv"
|
|
725
|
+
ens_avg_csvs = sort_filenames(
|
|
726
|
+
glob(input_database + hierarchy + prop_ext), ["csv"]
|
|
727
|
+
)
|
|
728
|
+
if not ens_avg_csvs:
|
|
729
|
+
raise ValueError(
|
|
730
|
+
f"No '{hierarchy}{prop_ext}' file in '{input_database}'."
|
|
731
|
+
)
|
|
732
|
+
size_attr = species_sizes[species]
|
|
733
|
+
prop_csvs = []
|
|
734
|
+
|
|
735
|
+
for ens_avg_csv in ens_avg_csvs:
|
|
736
|
+
ens_avg = pd.read_csv(ens_avg_csv[0], header=0)
|
|
737
|
+
prop_info = parser(ens_avg_csv[0], "ensemble_long", group, ispath=True)
|
|
738
|
+
|
|
739
|
+
# A profile normalized by its sum adds up to one; the sum rule needs
|
|
740
|
+
# the raw profile, so warn, but go on: a raw one can sum to one too.
|
|
741
|
+
if abs(ens_avg[f"{prop}-mean"].sum() - 1.0) <= 1e-6:
|
|
742
|
+
warnings.warn(
|
|
743
|
+
f"The '{prop}-mean' column of '{ens_avg_csv[0]}' sums to one"
|
|
744
|
+
" within 1e-6, as a profile normalized by its sum does; the"
|
|
745
|
+
" sum rule needs the raw ensemble-averaged profile.",
|
|
746
|
+
UserWarning,
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
# The pipeline's per-whole error belongs to the default normalizer;
|
|
750
|
+
# with another, it would not describe <prop>-norm.
|
|
751
|
+
norm_sem = f"{prop}-norm-sem"
|
|
752
|
+
if norm_sem in ens_avg.columns and normalizer is not None:
|
|
753
|
+
warnings.warn(
|
|
754
|
+
f"Dropping '{norm_sem}' of '{ens_avg_csv[0]}': it is the"
|
|
755
|
+
f" error under the default normalizer {default!r}, not under"
|
|
756
|
+
f" {normalizer!r}.",
|
|
757
|
+
UserWarning,
|
|
758
|
+
)
|
|
759
|
+
ens_avg = ens_avg.drop(columns=norm_sem)
|
|
760
|
+
|
|
761
|
+
# One term of the sum rule: phi / a, or rho * a**2
|
|
762
|
+
size = getattr(prop_info, size_attr)
|
|
763
|
+
if quantity == "Phi":
|
|
764
|
+
scaler = size
|
|
765
|
+
ens_avg[f"{prop}-scale"] = ens_avg[f"{prop}-mean"] / scaler
|
|
766
|
+
else:
|
|
767
|
+
scaler = size ** 2
|
|
768
|
+
ens_avg[f"{prop}-scale"] = ens_avg[f"{prop}-mean"] * scaler
|
|
769
|
+
ens_avg[f"{prop}-scaler"] = scaler
|
|
770
|
+
ens_avg[f"{prop}-scale-normalized_curve"] = (
|
|
771
|
+
ens_avg[f"{prop}-scale"] / ens_avg[f"{prop}-scale"].sum()
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
# Normalize by the reference value; fold a z profile about z = 0
|
|
775
|
+
file_normalizer: NormalizerT
|
|
776
|
+
if normalizer is None:
|
|
777
|
+
file_normalizer = default
|
|
778
|
+
elif callable(normalizer):
|
|
779
|
+
file_normalizer = _with_parser(normalizer, prop_info)
|
|
780
|
+
else:
|
|
781
|
+
file_normalizer = normalizer
|
|
782
|
+
if direction == "z":
|
|
783
|
+
ens_avg = normalize_z(prop, ens_avg, file_normalizer)
|
|
784
|
+
else:
|
|
785
|
+
ens_avg = normalize_r(prop, ens_avg, file_normalizer)
|
|
786
|
+
ens_avg[f"{prop}-sumrule_constant"] = scaler
|
|
787
|
+
ens_avg["bin_center-norm"] = (
|
|
788
|
+
ens_avg["bin_center"] / ens_avg["bin_center"].max()
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
# Add the state point's name and physical attributes
|
|
792
|
+
ens_avg["ensemble_long"] = getattr(prop_info, "ensemble_long")
|
|
793
|
+
for attr_name in physical_attrs:
|
|
794
|
+
ens_avg[attr_name] = getattr(prop_info, attr_name)
|
|
795
|
+
ens_avg[crowder_attr] = getattr(prop_info, crowder_attr)
|
|
796
|
+
ens_avg["phi_bulk_c_round"] = ens_avg[crowder_attr].apply(
|
|
797
|
+
lambda x: round_to_multiple(x, divisor, round_to)
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
# Bin centers in crowder units, recentered on the wall in a cylinder
|
|
801
|
+
for label in crowders:
|
|
802
|
+
crowder_size_attr = species_sizes[label]
|
|
803
|
+
crowder_size = getattr(prop_info, crowder_size_attr)
|
|
804
|
+
ens_avg[f"bin_center-{crowder_size_attr}"] = (
|
|
805
|
+
2 * ens_avg["bin_center"] / crowder_size
|
|
806
|
+
)
|
|
807
|
+
if geometry == "cylindrical" and direction == "r":
|
|
808
|
+
leftover = getattr(prop_info, "dcyl") % crowder_size
|
|
809
|
+
ens_avg[f"bin_center-{crowder_size_attr}-recentered"] = (
|
|
810
|
+
ens_avg[f"bin_center-{crowder_size_attr}"]
|
|
811
|
+
- leftover / crowder_size
|
|
812
|
+
)
|
|
813
|
+
recentered = ens_avg["bin_center"] - leftover
|
|
814
|
+
suffix = f"-{crowder_size_attr}" if len(crowders) > 1 else ""
|
|
815
|
+
ens_avg[f"bin_center-recentered-norm{suffix}"] = (
|
|
816
|
+
recentered / recentered.max()
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
prop_csvs.append(ens_avg)
|
|
820
|
+
|
|
821
|
+
prop_db = pd.concat(prop_csvs, axis=0)
|
|
822
|
+
prop_db = prop_db.reset_index(drop=True)
|
|
823
|
+
|
|
824
|
+
if is_save:
|
|
825
|
+
save_to_space = make_database(
|
|
826
|
+
input_database, "analysis", stage="space", group=group
|
|
827
|
+
)
|
|
828
|
+
space = save_to_space.split("/")[-2].split("-")[0]
|
|
829
|
+
filepath = (
|
|
830
|
+
save_to_space
|
|
831
|
+
+ f"{create_fullname(space, group, prop)}"
|
|
832
|
+
+ "-normalizedRescaled-space.csv"
|
|
833
|
+
)
|
|
834
|
+
prop_db.to_csv(filepath, index=False)
|
|
835
|
+
logger.info("Saved %s.", filepath)
|
|
836
|
+
|
|
837
|
+
return prop_db
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def space_profiles_from_spec(
|
|
841
|
+
input_database: Pathish,
|
|
842
|
+
spec: ProjectSpec,
|
|
843
|
+
quantity: Literal["Rho", "Phi"],
|
|
844
|
+
direction: DirectionT,
|
|
845
|
+
species: EntityT,
|
|
846
|
+
*,
|
|
847
|
+
group: GroupT = "all",
|
|
848
|
+
crowders: Sequence[EntityT] = ("Crd",),
|
|
849
|
+
normalizer: (
|
|
850
|
+
Literal["first", "last", "max"] | float
|
|
851
|
+
| Callable[[pd.DataFrame, Any], float] | None
|
|
852
|
+
) = None,
|
|
853
|
+
round_to: int = 3,
|
|
854
|
+
save: bool = False,
|
|
855
|
+
) -> pd.DataFrame:
|
|
856
|
+
"""
|
|
857
|
+
Scale and normalize the ensemble-averaged density profiles of one
|
|
858
|
+
species across a space for the density sum rule, with the columns, the
|
|
859
|
+
species sizes, and the rounding step the study's spec declares.
|
|
860
|
+
|
|
861
|
+
The spec supplies every study choice of :func:`space_profiles`: the
|
|
862
|
+
parser, the ``ensemble_long`` glob prefix (`spec.hierarchy`), the
|
|
863
|
+
attributes that become columns (`spec.physical_attrs`), the parser
|
|
864
|
+
attribute holding the particle diameter :math:`a_i` of each species
|
|
865
|
+
(`spec.species_sizes`), the rounding step :math:`\\Delta` of the bulk
|
|
866
|
+
volume fraction of crowders :math:`\\phi_c` (`spec.divisor`), and the
|
|
867
|
+
parser attribute that holds :math:`\\phi_c` (`spec.crowder_attr`). No
|
|
868
|
+
study default is taken from the library.
|
|
869
|
+
|
|
870
|
+
Parameters
|
|
871
|
+
----------
|
|
872
|
+
input_database : path-like
|
|
873
|
+
The ``ensAvg`` directory of the space, which holds the files
|
|
874
|
+
``<ensemble_long>-<group>-<direction><quantity><species>-ensAvg.csv``
|
|
875
|
+
of the raw ensemble-averaged profiles, as :func:`space_profiles`
|
|
876
|
+
requires.
|
|
877
|
+
spec : ~polyphys.spec.ProjectSpec
|
|
878
|
+
The study's spec.
|
|
879
|
+
quantity : {'Rho', 'Phi'}
|
|
880
|
+
The number density or the volume fraction.
|
|
881
|
+
direction : {'r', 'z'}
|
|
882
|
+
The coordinate of the profile; ``'z'`` exists in a cylinder only.
|
|
883
|
+
species : EntityT
|
|
884
|
+
The species label of the profile, such as ``'Mon'`` or ``'Crd'``; a
|
|
885
|
+
key of `spec.species_sizes`.
|
|
886
|
+
group : GroupT, default 'all'
|
|
887
|
+
The particle group of the profiles. A density profile covers every
|
|
888
|
+
particle of its species, so it is in the ``'all'`` group, not the
|
|
889
|
+
polymer group `spec.group`.
|
|
890
|
+
crowders : sequence of EntityT, default ('Crd',)
|
|
891
|
+
The crowder species, each a key of `spec.species_sizes`, as in
|
|
892
|
+
:func:`space_profiles`.
|
|
893
|
+
normalizer : NormalizerT or callable or None, default None
|
|
894
|
+
The reference value of the normalization, as in
|
|
895
|
+
:func:`space_profiles`; None takes the default of
|
|
896
|
+
:data:`PROFILE_NORMALIZERS` for the spec's geometry and `direction`.
|
|
897
|
+
round_to : int, default 3
|
|
898
|
+
The number of decimal places of the rounded :math:`\\phi_c`.
|
|
899
|
+
save : bool, default False
|
|
900
|
+
Also write the table to
|
|
901
|
+
``<space>-<group>-<direction><quantity><species>-normalizedRescaled-space.csv``
|
|
902
|
+
in the ``analysis`` phase's ``space`` directory next to
|
|
903
|
+
`input_database`.
|
|
904
|
+
|
|
905
|
+
Returns
|
|
906
|
+
-------
|
|
907
|
+
pd.DataFrame
|
|
908
|
+
The table of :func:`space_profiles`, with a `spec.crowder_attr`
|
|
909
|
+
column and ``phi_bulk_c_round``, that attribute rounded to a
|
|
910
|
+
multiple of `spec.divisor`.
|
|
911
|
+
|
|
912
|
+
Raises
|
|
913
|
+
------
|
|
914
|
+
ValueError, KeyError
|
|
915
|
+
As :func:`space_profiles` raises them.
|
|
916
|
+
|
|
917
|
+
Warns
|
|
918
|
+
-----
|
|
919
|
+
UserWarning
|
|
920
|
+
As :func:`space_profiles` warns.
|
|
921
|
+
|
|
922
|
+
Notes
|
|
923
|
+
-----
|
|
924
|
+
The cost is that of :func:`space_profiles`:
|
|
925
|
+
:math:`O(n_\\text{files} \\, n_\\text{bins} \\, n_\\text{cols} +
|
|
926
|
+
n_\\text{files} \\log n_\\text{files})` time and
|
|
927
|
+
:math:`O(n_\\text{files} \\, n_\\text{bins} \\, n_\\text{cols})` memory
|
|
928
|
+
for n_files matched files of n_bins rows and n_cols columns of the
|
|
929
|
+
result.
|
|
930
|
+
|
|
931
|
+
Examples
|
|
932
|
+
--------
|
|
933
|
+
In a cylinder of diameter :math:`D = 10` and length :math:`Z = 50`
|
|
934
|
+
with :math:`n_c = 100` crowders of diameter :math:`a_c = 2`,
|
|
935
|
+
:math:`\\phi_c = 2 n_c a_c^3 / (3 Z (D - a_c)^2) = 1/6`, which the
|
|
936
|
+
spec's step of 0.1 rounds to 0.2:
|
|
937
|
+
|
|
938
|
+
>>> import tempfile
|
|
939
|
+
>>> from pathlib import Path
|
|
940
|
+
>>> from polyphys.tests.synthetic import ChainCyl
|
|
941
|
+
>>> from polyphys.spec import ProjectSpec
|
|
942
|
+
>>> spec = ProjectSpec(
|
|
943
|
+
... parser=ChainCyl, group='bug', divisor=0.1,
|
|
944
|
+
... crowder_attr='phi_bulk_c', physical_attrs=('dcyl',),
|
|
945
|
+
... species_sizes={'Mon': 'dmon', 'Crd': 'dcrowd'},
|
|
946
|
+
... tseries_properties=('gyrTMon',))
|
|
947
|
+
>>> tmp = tempfile.TemporaryDirectory()
|
|
948
|
+
>>> database = Path(tmp.name) / 'analysis' / 'N20-all-ensAvg'
|
|
949
|
+
>>> database.mkdir(parents=True)
|
|
950
|
+
>>> state = 'N20r5.5lz25.0ac2.0nc100dt0.005bdump1000adump5000'
|
|
951
|
+
>>> pd.DataFrame({'bin_center': [0.5, 1.5, 2.5],
|
|
952
|
+
... 'rPhiCrd-mean': [0.2, 0.3, 0.1]}).to_csv(
|
|
953
|
+
... database / f'{state}-all-rPhiCrd-ensAvg.csv', index=False)
|
|
954
|
+
>>> space = space_profiles_from_spec(database, spec, 'Phi', 'r', 'Crd')
|
|
955
|
+
>>> space[['bin_center', 'rPhiCrd-norm', 'dcyl', 'phi_bulk_c_round']]
|
|
956
|
+
bin_center rPhiCrd-norm dcyl phi_bulk_c_round
|
|
957
|
+
0 0.5 1.0 10.0 0.2
|
|
958
|
+
1 1.5 1.5 10.0 0.2
|
|
959
|
+
2 2.5 0.5 10.0 0.2
|
|
960
|
+
>>> tmp.cleanup()
|
|
961
|
+
|
|
962
|
+
.. versionadded:: 0.9.0
|
|
963
|
+
"""
|
|
964
|
+
return space_profiles(
|
|
965
|
+
os.path.join(os.fspath(input_database), ""), quantity, direction,
|
|
966
|
+
species, spec.parser, spec.hierarchy, list(spec.physical_attrs),
|
|
967
|
+
group, spec.species_sizes, crowders=crowders, normalizer=normalizer,
|
|
968
|
+
divisor=spec.divisor, round_to=round_to, is_save=save,
|
|
969
|
+
crowder_attr=spec.crowder_attr)
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def sum_rule_profiles(
|
|
973
|
+
profiles: Mapping[EntityT, pd.DataFrame],
|
|
974
|
+
prop: str,
|
|
975
|
+
geometry: GeometryT,
|
|
976
|
+
direction: DirectionT,
|
|
977
|
+
*,
|
|
978
|
+
by: Sequence[str] = ("ensemble_long",),
|
|
979
|
+
normalizer: NormalizerT | None = None,
|
|
980
|
+
) -> pd.DataFrame:
|
|
981
|
+
"""
|
|
982
|
+
Sum the scaled density profiles of several species, state point by
|
|
983
|
+
state point, and normalize the sum by its own reference value: the
|
|
984
|
+
left side of the density sum rule.
|
|
985
|
+
|
|
986
|
+
With the species' scaled profiles :math:`\\phi_i(x) / a_i` from
|
|
987
|
+
:func:`space_profiles`, the sum :math:`\\sum_i \\phi_i(x) / a_i` is
|
|
988
|
+
constant in :math:`x` where the sum rule holds (Jeon et al., 2016,
|
|
989
|
+
eqn (5); eqn (7) for several crowders). Normalized by its own value at
|
|
990
|
+
the reference, the sum is one there and its departure from one
|
|
991
|
+
elsewhere measures the rule's failure.
|
|
992
|
+
|
|
993
|
+
The species' profiles must descend from the raw ensemble averages, as
|
|
994
|
+
:func:`space_profiles` requires: the sum weights each species by its
|
|
995
|
+
physical scale, and a species normalized on its own, by its sum, its
|
|
996
|
+
integral, or its maximum, enters with an arbitrary weight. Nothing here
|
|
997
|
+
can detect that, so it is the caller's contract.
|
|
998
|
+
|
|
999
|
+
Parameters
|
|
1000
|
+
----------
|
|
1001
|
+
profiles : mapping of EntityT to pd.DataFrame
|
|
1002
|
+
Each species label mapped to its :func:`space_profiles` result for
|
|
1003
|
+
the same `prop`; each has the columns of `by`, ``bin_center``, and
|
|
1004
|
+
``<prop><species>-scale``, in the same row order, with ``-scale``
|
|
1005
|
+
built from the raw ensemble-averaged profile.
|
|
1006
|
+
prop : str
|
|
1007
|
+
The direction and quantity, such as ``'rPhi'`` or ``'zRho'``.
|
|
1008
|
+
geometry : {'cubic', 'cylindrical'}
|
|
1009
|
+
The geometry of the study, which with `direction` chooses the
|
|
1010
|
+
default normalizer.
|
|
1011
|
+
direction : {'r', 'z'}
|
|
1012
|
+
The coordinate of the profiles.
|
|
1013
|
+
by : sequence of str, default ('ensemble_long',)
|
|
1014
|
+
The columns that identify a state point; the sum is normalized
|
|
1015
|
+
within each. An empty sequence treats the whole input as one.
|
|
1016
|
+
normalizer : :py:obj:`~.NormalizerT` or None, \
|
|
1017
|
+
default None
|
|
1018
|
+
The reference value of each state point's sum. None takes the
|
|
1019
|
+
default of :data:`SUM_NORMALIZERS`: the value at :math:`r = 0` for
|
|
1020
|
+
``'r'`` in a cylinder, the far-field (last-bin) value otherwise,
|
|
1021
|
+
where the monomer term vanishes and the sum is the crowder term
|
|
1022
|
+
:math:`\\phi_c(\\infty) / a_c`. A function is called with the state
|
|
1023
|
+
point's frame of the columns below. An explicit value is logged at
|
|
1024
|
+
INFO with the default it replaces.
|
|
1025
|
+
|
|
1026
|
+
Returns
|
|
1027
|
+
-------
|
|
1028
|
+
pd.DataFrame
|
|
1029
|
+
One row per bin and state point, in the input order: the `by`
|
|
1030
|
+
columns, ``bin_center``, ``<prop>Sum-scale`` (the sum over species),
|
|
1031
|
+
``<prop>Sum-normalizer`` (the reference value of the state point),
|
|
1032
|
+
and ``<prop>Sum-norm``. When every species carries
|
|
1033
|
+
``<prop><species>-norm-sem``, also ``<prop>Sum-norm-sem``, the
|
|
1034
|
+
species' ``-norm-sem`` values :math:`s_i` added in quadrature,
|
|
1035
|
+
:math:`\\sqrt{\\sum_i s_i^2}`, which assumes that the species'
|
|
1036
|
+
errors are independent. Each :math:`s_i` is the SEM across profiles
|
|
1037
|
+
normalized separately for each whole, not generally an uncertainty
|
|
1038
|
+
estimate for a normalized ensemble mean, so neither is the sum. It
|
|
1039
|
+
refers to the sum of the separately normalized species,
|
|
1040
|
+
:math:`\\sum_i \\phi_i / (a_i N_i)` for each species' own normalizer
|
|
1041
|
+
:math:`N_i`, and to ``<prop>Sum-norm``, :math:`\\sum_i \\phi_i /
|
|
1042
|
+
a_i` over the sum's normalizer, only when every species shares one
|
|
1043
|
+
normalizer value.
|
|
1044
|
+
|
|
1045
|
+
Raises
|
|
1046
|
+
------
|
|
1047
|
+
ValueError
|
|
1048
|
+
If `profiles` is empty; if the species disagree on the `by` columns
|
|
1049
|
+
or on ``bin_center`` in any row; if a row has NaN in a `by` column;
|
|
1050
|
+
if (`geometry`, `direction`) has no entry in
|
|
1051
|
+
:data:`SUM_NORMALIZERS`; or if `normalizer` is none of the accepted
|
|
1052
|
+
forms.
|
|
1053
|
+
|
|
1054
|
+
Warns
|
|
1055
|
+
-----
|
|
1056
|
+
UserWarning
|
|
1057
|
+
If a state point's reference value is zero; its ``<prop>Sum-norm``
|
|
1058
|
+
is then zero.
|
|
1059
|
+
|
|
1060
|
+
Examples
|
|
1061
|
+
--------
|
|
1062
|
+
Two species over three radial bins of one state point in a cube: the
|
|
1063
|
+
monomer term falls to zero where the crowder term reaches its far-field
|
|
1064
|
+
value, and the sum is one where the rule holds:
|
|
1065
|
+
|
|
1066
|
+
>>> keys = {'ensemble_long': ['s1'] * 3, 'bin_center': [0.5, 1.5, 2.5]}
|
|
1067
|
+
>>> monomers = pd.DataFrame({**keys, 'rPhiMon-scale': [0.8, 0.4, 0.0]})
|
|
1068
|
+
>>> crowders = pd.DataFrame({**keys, 'rPhiCrd-scale': [0.2, 0.6, 1.0]})
|
|
1069
|
+
>>> total = sum_rule_profiles({'Mon': monomers, 'Crd': crowders},
|
|
1070
|
+
... 'rPhi', 'cubic', 'r')
|
|
1071
|
+
>>> total[['bin_center', 'rPhiSum-scale', 'rPhiSum-norm']]
|
|
1072
|
+
bin_center rPhiSum-scale rPhiSum-norm
|
|
1073
|
+
0 0.5 1.0 1.0
|
|
1074
|
+
1 1.5 1.0 1.0
|
|
1075
|
+
2 2.5 1.0 1.0
|
|
1076
|
+
>>> total['rPhiSum-normalizer'].tolist()
|
|
1077
|
+
[1.0, 1.0, 1.0]
|
|
1078
|
+
|
|
1079
|
+
Notes
|
|
1080
|
+
-----
|
|
1081
|
+
Time and space are :math:`O(n_\\text{species} \\, n_\\text{rows})` for
|
|
1082
|
+
the sum, the checks, and the error, plus :math:`O(n_\\text{rows})` per
|
|
1083
|
+
state point for the normalization. See :func:`space_profiles` for the
|
|
1084
|
+
references. The species' errors are correlated within a whole by
|
|
1085
|
+
excluded volume, which the quadrature sum ignores; the per-bin
|
|
1086
|
+
estimator is an open question.
|
|
1087
|
+
|
|
1088
|
+
.. versionadded:: 0.6.0
|
|
1089
|
+
.. versionchanged:: 0.7.0
|
|
1090
|
+
Added ``<prop>Sum-norm-sem``.
|
|
1091
|
+
"""
|
|
1092
|
+
if not profiles:
|
|
1093
|
+
raise ValueError("'profiles' is empty.")
|
|
1094
|
+
try:
|
|
1095
|
+
default = SUM_NORMALIZERS[(geometry, direction)]
|
|
1096
|
+
except KeyError:
|
|
1097
|
+
raise ValueError(
|
|
1098
|
+
f"No density profile along {direction!r} in a {geometry} "
|
|
1099
|
+
f"geometry; the (geometry, direction) pairs are "
|
|
1100
|
+
f"{sorted(SUM_NORMALIZERS)}."
|
|
1101
|
+
) from None
|
|
1102
|
+
if normalizer is None:
|
|
1103
|
+
normalizer = default
|
|
1104
|
+
else:
|
|
1105
|
+
logger.info(
|
|
1106
|
+
"Normalizing the %s sum with %r instead of the %s %s default "
|
|
1107
|
+
"%r.", prop, normalizer, geometry, direction, default,
|
|
1108
|
+
)
|
|
1109
|
+
keys = [*by, "bin_center"]
|
|
1110
|
+
sum_prop = f"{prop}Sum"
|
|
1111
|
+
|
|
1112
|
+
# The key columns of the first species fix the row order; every other
|
|
1113
|
+
# species must match them row for row.
|
|
1114
|
+
labels = list(profiles)
|
|
1115
|
+
first = profiles[labels[0]]
|
|
1116
|
+
total = first[keys].reset_index(drop=True)
|
|
1117
|
+
total[f"{sum_prop}-scale"] = 0.0
|
|
1118
|
+
# The species' normalized errors add in quadrature when every species
|
|
1119
|
+
# has one; the column is left out otherwise.
|
|
1120
|
+
with_error = all(
|
|
1121
|
+
f"{prop}{label}-norm-sem" in profiles[label].columns
|
|
1122
|
+
for label in labels
|
|
1123
|
+
)
|
|
1124
|
+
if with_error:
|
|
1125
|
+
total[f"{sum_prop}-norm-sem"] = 0.0
|
|
1126
|
+
for label in labels:
|
|
1127
|
+
frame = profiles[label]
|
|
1128
|
+
if len(frame) != len(first) or not (
|
|
1129
|
+
frame[list(by)].to_numpy().tolist()
|
|
1130
|
+
== first[list(by)].to_numpy().tolist()
|
|
1131
|
+
and np.allclose(frame["bin_center"], first["bin_center"])
|
|
1132
|
+
):
|
|
1133
|
+
raise ValueError(
|
|
1134
|
+
f"The {label!r} profiles do not match the {labels[0]!r} "
|
|
1135
|
+
f"profiles on {keys}."
|
|
1136
|
+
)
|
|
1137
|
+
total[f"{sum_prop}-scale"] += frame[f"{prop}{label}-scale"].to_numpy()
|
|
1138
|
+
if with_error:
|
|
1139
|
+
total[f"{sum_prop}-norm-sem"] += (
|
|
1140
|
+
frame[f"{prop}{label}-norm-sem"].to_numpy() ** 2
|
|
1141
|
+
)
|
|
1142
|
+
if with_error:
|
|
1143
|
+
total[f"{sum_prop}-norm-sem"] = np.sqrt(total[f"{sum_prop}-norm-sem"])
|
|
1144
|
+
|
|
1145
|
+
# Normalize each state point's sum by its own reference value; a row
|
|
1146
|
+
# with no state point would be dropped by the group-by, and the groups
|
|
1147
|
+
# are put back in the input row order.
|
|
1148
|
+
if by:
|
|
1149
|
+
missing = int(total[list(by)].isna().any(axis=1).sum())
|
|
1150
|
+
if missing:
|
|
1151
|
+
rows = "row has" if missing == 1 else "rows have"
|
|
1152
|
+
raise ValueError(
|
|
1153
|
+
f"{missing} {rows} no value in the 'by' columns "
|
|
1154
|
+
f"{list(by)}; the group-by would drop them.")
|
|
1155
|
+
groups = [group for _, group in total.groupby(list(by), sort=False)]
|
|
1156
|
+
else:
|
|
1157
|
+
groups = [total]
|
|
1158
|
+
normalized = [_normalize(sum_prop, group, normalizer) for group in groups]
|
|
1159
|
+
return pd.concat(normalized, axis=0).sort_index().reset_index(drop=True)
|