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,810 @@
|
|
|
1
|
+
"""
|
|
2
|
+
==========================================================
|
|
3
|
+
:mod:`polyphys.analyze.stats`
|
|
4
|
+
==========================================================
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
The :mod:`polyphys.analyze.stats` module estimates the uncertainty of a
|
|
8
|
+
time average over correlated molecular-dynamics frames with the blocking
|
|
9
|
+
method of Flyvbjerg and Petersen.
|
|
10
|
+
|
|
11
|
+
Consecutive frames of a trajectory are not independent. The textbook
|
|
12
|
+
standard error of the mean, :math:`\\sigma / \\sqrt{n}`, where :math:`n` is
|
|
13
|
+
the number of frames and :math:`\\sigma` the standard deviation of the
|
|
14
|
+
observable over them, counts every frame as a fresh observation and so
|
|
15
|
+
understates the true error, often by a large factor. Blocking averages
|
|
16
|
+
neighboring frames in pairs, repeatedly, and recomputes the standard error at
|
|
17
|
+
each level. Once the blocks are longer than the correlation time, the lag over
|
|
18
|
+
which the observable's autocorrelation decays, the block means are effectively
|
|
19
|
+
independent and the estimate stops growing: that plateau is the error bar to
|
|
20
|
+
report. The ratio of the plateau variance of the mean to the naive one, the
|
|
21
|
+
*statistical inefficiency* :math:`s`, says how many frames one independent
|
|
22
|
+
sample is worth; the true standard error is :math:`\\sigma \\sqrt{s / n}`.
|
|
23
|
+
|
|
24
|
+
The module distinguishes two kinds of average that look alike in a table:
|
|
25
|
+
|
|
26
|
+
- a *time average* over the frames of one run (one ``whole``), whose frames
|
|
27
|
+
are correlated and whose error needs this module;
|
|
28
|
+
- an *ensemble average* over independent replicates (one ``ensemble``), whose
|
|
29
|
+
members are independent runs and whose error is the naive standard error of
|
|
30
|
+
the replicate means, as :func:`polyphys.analyze.measurer.sem` computes it.
|
|
31
|
+
|
|
32
|
+
Notation
|
|
33
|
+
========
|
|
34
|
+
|
|
35
|
+
:math:`x_t`
|
|
36
|
+
The value of one observable at frame :math:`t = 1, \\dots, n`.
|
|
37
|
+
:math:`n`
|
|
38
|
+
The number of frames, ``n_frames``.
|
|
39
|
+
:math:`\\sigma^2`
|
|
40
|
+
The variance of :math:`x_t` over the frames, with ``ddof=1``.
|
|
41
|
+
:math:`k`
|
|
42
|
+
The blocking level, :math:`k = 0, 1, 2, \\dots`.
|
|
43
|
+
:math:`B`
|
|
44
|
+
The block size in frames at level :math:`k`, :math:`B = 2^k`.
|
|
45
|
+
:math:`m`
|
|
46
|
+
The number of blocks at that level, :math:`\\lfloor n / B \\rfloor`.
|
|
47
|
+
:math:`\\mathrm{SE}(B)`
|
|
48
|
+
The standard error of the mean estimated from the :math:`m` block means;
|
|
49
|
+
:math:`\\mathrm{SE}(1) = \\sigma / \\sqrt{n}` is the naive estimate.
|
|
50
|
+
:math:`s(B)`
|
|
51
|
+
The statistical inefficiency estimated at block size :math:`B`: the
|
|
52
|
+
variance of the block means times :math:`B`, divided by :math:`\\sigma^2`.
|
|
53
|
+
When :math:`n` is a power of two, :math:`s(B) = (\\mathrm{SE}(B) /
|
|
54
|
+
\\mathrm{SE}(1))^2`.
|
|
55
|
+
:math:`s`
|
|
56
|
+
The statistical inefficiency, the plateau of :math:`s(B)` at block sizes
|
|
57
|
+
beyond the correlation time.
|
|
58
|
+
:math:`\\rho(t)`
|
|
59
|
+
The normalized autocorrelation function of :math:`x_t` at a lag of
|
|
60
|
+
:math:`t` frames, with :math:`\\rho(0) = 1`; for a stationary series
|
|
61
|
+
:math:`s = \\sum_{t=-\\infty}^{\\infty} \\rho(t)`.
|
|
62
|
+
:math:`\\tau_\\mathrm{int}`
|
|
63
|
+
The integrated autocorrelation time in Wolff's (2004) convention,
|
|
64
|
+
:math:`\\tau_\\mathrm{int} = s / 2`, in frames.
|
|
65
|
+
|
|
66
|
+
Contents
|
|
67
|
+
========
|
|
68
|
+
|
|
69
|
+
The full blocking table:
|
|
70
|
+
:func:`block_average` returns a :class:`BlockingResult`, one row per blocking
|
|
71
|
+
level, so the caller can read the plateau directly, as Flyvbjerg and Petersen
|
|
72
|
+
do.
|
|
73
|
+
|
|
74
|
+
Choosing the plateau:
|
|
75
|
+
:func:`select_plateau` applies one of two rules to a table, chosen with the
|
|
76
|
+
``method`` keyword; there is no default, because the rule is an estimator
|
|
77
|
+
choice.
|
|
78
|
+
|
|
79
|
+
``'lee'``
|
|
80
|
+
The block size satisfies :math:`B^3 > 2 n (\\mathrm{SE}(B) /
|
|
81
|
+
\\mathrm{SE}(1))^4`, scanning from the largest block downward, as Lee et
|
|
82
|
+
al. (2011), Eq. (14), and the ``pyblock`` package implement it. Wolff
|
|
83
|
+
(2004), Eq. (47), derives the same balance between the systematic and
|
|
84
|
+
the statistical error of the error estimate, with a constant four times
|
|
85
|
+
smaller in :math:`B^3`, because his correlation time is
|
|
86
|
+
:math:`\\tau_\\mathrm{int} = s / 2` where Lee et al. use :math:`s`.
|
|
87
|
+
``'sem_err'``
|
|
88
|
+
The first level whose standard error lies within one error bar of each of
|
|
89
|
+
the next two levels. A convenience heuristic without a published source;
|
|
90
|
+
it tends to pick the start of the plateau and so to err on the low side.
|
|
91
|
+
|
|
92
|
+
Under either rule, :func:`select_plateau` warns when the series fails the
|
|
93
|
+
acceptance criterion of Lee et al. (2011): the block size the ``'lee'`` rule
|
|
94
|
+
selects must be below :math:`n / 50`, which amounts to :math:`n / s > 500`,
|
|
95
|
+
more than about 500 independent samples.
|
|
96
|
+
|
|
97
|
+
Convenience wrappers:
|
|
98
|
+
:func:`sem_blocked` and :func:`statistical_inefficiency` run both steps and
|
|
99
|
+
return the selected row's value.
|
|
100
|
+
|
|
101
|
+
Conventions
|
|
102
|
+
===========
|
|
103
|
+
|
|
104
|
+
- Frames run along axis 0 of the input, shape ``(n_frames, ...)``. Every
|
|
105
|
+
trailing axis is an independent observable: a per-frame histogram
|
|
106
|
+
``(n_frames, n_bins)`` gives one error bar per bin.
|
|
107
|
+
- Block sizes are powers of two, ``1, 2, 4, ...``. Level ``k`` has block size
|
|
108
|
+
``2**k``; when a level has an odd number of blocks, the last one is dropped
|
|
109
|
+
before the next pairing, as Allen and Tildesley's example code does.
|
|
110
|
+
- Variances use ``ddof=1``. The error of a standard error,
|
|
111
|
+
:math:`\\mathrm{SE} / \\sqrt{2 (m - 1)}` for :math:`m` blocks, and the error
|
|
112
|
+
of the inefficiency, :math:`s \\sqrt{2 / (m - 1)}`, are the leading-order
|
|
113
|
+
Gaussian estimates (Lee et al. 2011, Eq. (15)).
|
|
114
|
+
- Results are in the units of the input; the statistical inefficiency is
|
|
115
|
+
dimensionless, in frames per independent sample.
|
|
116
|
+
- The input must be stationary: discard the equilibration transient before
|
|
117
|
+
calling. A drift or a rare switch between metastable states shows up as a
|
|
118
|
+
standard error that keeps rising with block size, and the selectors then
|
|
119
|
+
report that no plateau was found rather than pick a level.
|
|
120
|
+
|
|
121
|
+
References
|
|
122
|
+
==========
|
|
123
|
+
|
|
124
|
+
- H. Flyvbjerg and H. G. Petersen, "Error estimates on averages of correlated
|
|
125
|
+
data", J. Chem. Phys. 91, 461-466 (1989).
|
|
126
|
+
https://doi.org/10.1063/1.457480
|
|
127
|
+
- U. Wolff (ALPHA Collaboration), "Monte Carlo errors with less errors",
|
|
128
|
+
Comput. Phys. Commun. 156, 143-153 (2004); erratum 176, 383 (2007).
|
|
129
|
+
https://doi.org/10.1016/S0010-4655(03)00467-3
|
|
130
|
+
- R. M. Lee, G. J. Conduit, N. Nemec, P. López Ríos, and N. D. Drummond,
|
|
131
|
+
"Strategies for improving the efficiency of quantum Monte Carlo
|
|
132
|
+
calculations", Phys. Rev. E 83, 066706 (2011); erratum 85, 029908 (2012).
|
|
133
|
+
https://doi.org/10.1103/PhysRevE.83.066706
|
|
134
|
+
- M. P. Allen and D. J. Tildesley, Computer Simulation of Liquids, 2nd ed.
|
|
135
|
+
(Oxford University Press, 2017); example code ``error_calc.py`` at
|
|
136
|
+
https://github.com/Allen-Tildesley/examples/blob/master/python_examples/error_calc.py
|
|
137
|
+
- J. S. Spencer, ``pyblock``: ``pyblock.blocking.find_optimal_block``.
|
|
138
|
+
https://github.com/jsspencer/pyblock
|
|
139
|
+
|
|
140
|
+
.. versionadded:: 0.5.0
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
import warnings
|
|
144
|
+
from dataclasses import dataclass
|
|
145
|
+
from typing import Literal
|
|
146
|
+
|
|
147
|
+
import numpy as np
|
|
148
|
+
import numpy.typing as npt
|
|
149
|
+
|
|
150
|
+
__all__ = [
|
|
151
|
+
'BlockingResult',
|
|
152
|
+
'PlateauMethod',
|
|
153
|
+
'block_average',
|
|
154
|
+
'select_plateau',
|
|
155
|
+
'sem_blocked',
|
|
156
|
+
'statistical_inefficiency',
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
PlateauMethod = Literal['lee', 'sem_err']
|
|
160
|
+
"""The plateau-selection rules :func:`select_plateau` accepts."""
|
|
161
|
+
|
|
162
|
+
#: Lee et al. (2011) accept a blocking analysis only when their optimal block
|
|
163
|
+
#: size is below ``n_frames / LEE_MIN_BLOCK_RATIO``, that is, when the ratio
|
|
164
|
+
#: of the frames to that block size exceeds this value.
|
|
165
|
+
#: :func:`select_plateau` applies the check to the level the ``'lee'`` rule
|
|
166
|
+
#: selects, whichever rule is asked for.
|
|
167
|
+
LEE_MIN_BLOCK_RATIO = 50
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass(frozen=True)
|
|
171
|
+
class BlockingResult:
|
|
172
|
+
"""
|
|
173
|
+
The blocking table of one time series, one row per blocking level.
|
|
174
|
+
|
|
175
|
+
Level ``k`` averages the frames in contiguous blocks of ``2**k`` frames
|
|
176
|
+
and treats the block means as the sample. Every per-level array has the
|
|
177
|
+
level along axis 0; the trailing axes are those of one frame of the input,
|
|
178
|
+
one entry per observable.
|
|
179
|
+
|
|
180
|
+
Attributes
|
|
181
|
+
----------
|
|
182
|
+
n_frames : int
|
|
183
|
+
Number of frames in the input, the sample size at level 0.
|
|
184
|
+
block_size : numpy.ndarray
|
|
185
|
+
Frames per block at each level, ``2**k``; shape ``(n_levels,)``,
|
|
186
|
+
``int64``.
|
|
187
|
+
n_blocks : numpy.ndarray
|
|
188
|
+
Blocks at each level, ``n_frames // block_size``; shape
|
|
189
|
+
``(n_levels,)``, ``int64``.
|
|
190
|
+
mean : numpy.ndarray
|
|
191
|
+
Mean of the block means, in the units of the input; shape
|
|
192
|
+
``(n_levels, *observable_shape)``, ``float64``. It changes across
|
|
193
|
+
levels only when a trailing frame is dropped at an odd level.
|
|
194
|
+
var : numpy.ndarray
|
|
195
|
+
Variance of the block means with ``ddof=1``, in squared input units.
|
|
196
|
+
sem : numpy.ndarray
|
|
197
|
+
Standard error of the mean at each level, ``sqrt(var / n_blocks)``,
|
|
198
|
+
in the units of the input. Level 0 is the naive estimate.
|
|
199
|
+
sem_err : numpy.ndarray
|
|
200
|
+
Leading-order error of ``sem``, ``sem / sqrt(2 (n_blocks - 1))``.
|
|
201
|
+
statistical_inefficiency : numpy.ndarray
|
|
202
|
+
``block_size * var / var[0]``, dimensionless: frames per independent
|
|
203
|
+
sample, as estimated at that level. Level 0 is exactly 1.
|
|
204
|
+
inefficiency_err : numpy.ndarray
|
|
205
|
+
Leading-order error of the inefficiency,
|
|
206
|
+
``statistical_inefficiency * sqrt(2 / (n_blocks - 1))``. It neglects
|
|
207
|
+
the error of ``var[0]``.
|
|
208
|
+
|
|
209
|
+
Examples
|
|
210
|
+
--------
|
|
211
|
+
>>> import numpy as np
|
|
212
|
+
>>> from polyphys.analyze.stats import block_average
|
|
213
|
+
>>> table = block_average(np.repeat([1.0, 3.0, 2.0, 4.0], 4))
|
|
214
|
+
>>> table.n_levels, table.observable_shape
|
|
215
|
+
(3, ())
|
|
216
|
+
>>> table.block_size
|
|
217
|
+
array([1, 2, 4])
|
|
218
|
+
|
|
219
|
+
.. versionadded:: 0.5.0
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
n_frames: int
|
|
223
|
+
block_size: npt.NDArray[np.int64]
|
|
224
|
+
n_blocks: npt.NDArray[np.int64]
|
|
225
|
+
mean: npt.NDArray[np.float64]
|
|
226
|
+
var: npt.NDArray[np.float64]
|
|
227
|
+
sem: npt.NDArray[np.float64]
|
|
228
|
+
sem_err: npt.NDArray[np.float64]
|
|
229
|
+
statistical_inefficiency: npt.NDArray[np.float64]
|
|
230
|
+
inefficiency_err: npt.NDArray[np.float64]
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def n_levels(self) -> int:
|
|
234
|
+
"""Number of blocking levels, the length of axis 0."""
|
|
235
|
+
return int(self.block_size.shape[0])
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def observable_shape(self) -> tuple[int, ...]:
|
|
239
|
+
"""Shape of one frame of the input, ``()`` for a scalar series."""
|
|
240
|
+
return tuple(self.mean.shape[1:])
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _as_series(x: npt.ArrayLike) -> npt.NDArray[np.float64]:
|
|
244
|
+
"""
|
|
245
|
+
Copy `x` to a finite ``float64`` array with at least one axis.
|
|
246
|
+
|
|
247
|
+
Raises
|
|
248
|
+
------
|
|
249
|
+
ValueError
|
|
250
|
+
If `x` is a scalar or holds a NaN or an infinity.
|
|
251
|
+
"""
|
|
252
|
+
data = np.array(x, dtype=np.float64)
|
|
253
|
+
if data.ndim == 0:
|
|
254
|
+
raise ValueError(
|
|
255
|
+
'Input must have frames along axis 0; got a scalar.'
|
|
256
|
+
)
|
|
257
|
+
if not np.all(np.isfinite(data)):
|
|
258
|
+
raise ValueError('Input must be finite; it holds NaN or inf.')
|
|
259
|
+
return data
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def block_average(x: npt.ArrayLike, min_blocks: int = 4) -> BlockingResult:
|
|
263
|
+
"""
|
|
264
|
+
Compute the Flyvbjerg-Petersen blocking table of a time series.
|
|
265
|
+
|
|
266
|
+
Starting from the frames themselves (block size 1), the series is
|
|
267
|
+
replaced by the means of consecutive pairs, and the mean, variance,
|
|
268
|
+
standard error, and statistical inefficiency are recorded at each level,
|
|
269
|
+
until the next halving would leave fewer than `min_blocks` blocks. The
|
|
270
|
+
full table is option (a) of the plateau choice: the caller reads where
|
|
271
|
+
the standard error stops growing.
|
|
272
|
+
|
|
273
|
+
Parameters
|
|
274
|
+
----------
|
|
275
|
+
x : array_like
|
|
276
|
+
Time series with frames along axis 0, shape ``(n_frames, ...)``. Every
|
|
277
|
+
trailing axis is an independent observable. Any numeric dtype; the
|
|
278
|
+
computation is in ``float64`` and `x` is not modified.
|
|
279
|
+
min_blocks : int, default 4
|
|
280
|
+
Smallest number of blocks kept; at least 2. The last level has at
|
|
281
|
+
least this many blocks. With 4 blocks the error of the standard error
|
|
282
|
+
is already 41% of its value, so levels this deep are mostly a guard
|
|
283
|
+
against reading noise as a plateau.
|
|
284
|
+
|
|
285
|
+
Returns
|
|
286
|
+
-------
|
|
287
|
+
BlockingResult
|
|
288
|
+
The table; see its attributes for shapes and units.
|
|
289
|
+
|
|
290
|
+
Raises
|
|
291
|
+
------
|
|
292
|
+
ValueError
|
|
293
|
+
If `x` is a scalar, holds NaN or inf, has fewer than `min_blocks`
|
|
294
|
+
frames, or has an observable that is constant across frames (its
|
|
295
|
+
inefficiency would be 0/0); or if `min_blocks` is below 2.
|
|
296
|
+
|
|
297
|
+
Notes
|
|
298
|
+
-----
|
|
299
|
+
For a series :math:`x_t` of :math:`n` frames, level :math:`k = 0, 1,
|
|
300
|
+
\\dots` has block size :math:`B_k = 2^k` frames, :math:`m_k = \\lfloor n
|
|
301
|
+
/ B_k \\rfloor` blocks, and block means :math:`\\bar{x}_j^{(k)}`, the
|
|
302
|
+
average of :math:`x_t` over block :math:`j = 1, \\dots, m_k`. With
|
|
303
|
+
:math:`\\langle \\bar{x}^{(k)} \\rangle` the mean of the block means, the
|
|
304
|
+
table records
|
|
305
|
+
|
|
306
|
+
.. math::
|
|
307
|
+
|
|
308
|
+
\\mathrm{var}_k = \\frac{1}{m_k - 1} \\sum_{j=1}^{m_k}
|
|
309
|
+
\\left(\\bar{x}_j^{(k)}
|
|
310
|
+
- \\langle \\bar{x}^{(k)} \\rangle\\right)^2,
|
|
311
|
+
\\quad
|
|
312
|
+
\\mathrm{SE}_k = \\sqrt{\\mathrm{var}_k / m_k},
|
|
313
|
+
\\quad
|
|
314
|
+
s_k = B_k \\, \\mathrm{var}_k / \\mathrm{var}_0 ,
|
|
315
|
+
|
|
316
|
+
the variance of the block means, the standard error of the mean, and the
|
|
317
|
+
statistical inefficiency at level :math:`k`; :math:`\\mathrm{var}_0` is
|
|
318
|
+
the variance of the frames themselves. As :math:`B_k` grows past the
|
|
319
|
+
correlation time, :math:`\\mathrm{SE}_k` reaches a plateau at the true
|
|
320
|
+
standard error of the time average and :math:`s_k` at the statistical
|
|
321
|
+
inefficiency :math:`s = \\sum_{t=-\\infty}^{\\infty} \\rho(t)`, where
|
|
322
|
+
:math:`\\rho(t)` is the normalized autocorrelation function of
|
|
323
|
+
:math:`x_t` at a lag of :math:`t` frames. This is twice the integrated
|
|
324
|
+
autocorrelation time :math:`\\tau_\\mathrm{int}` of Wolff (2004),
|
|
325
|
+
Eq. (23) and (25).
|
|
326
|
+
|
|
327
|
+
When a level has an odd number of blocks, the trailing block is left out
|
|
328
|
+
of the next pairing, so the frames used shrink by at most one block per
|
|
329
|
+
level and ``mean`` can drift slightly between levels.
|
|
330
|
+
|
|
331
|
+
With :math:`n_\\mathrm{obs}` the number of observables, the product of
|
|
332
|
+
the trailing dimensions of `x` (1 for a 1-D series), time complexity is
|
|
333
|
+
:math:`O(n \\, n_\\mathrm{obs})`, since the levels touch :math:`n + n/2
|
|
334
|
+
+ n/4 + \\dots < 2n` frames in all. Space is one ``float64`` copy of the
|
|
335
|
+
input, halved in place, plus the table of :math:`O(\\log_2 n \\,
|
|
336
|
+
n_\\mathrm{obs})` entries.
|
|
337
|
+
|
|
338
|
+
References
|
|
339
|
+
----------
|
|
340
|
+
H. Flyvbjerg and H. G. Petersen, J. Chem. Phys. 91, 461 (1989),
|
|
341
|
+
https://doi.org/10.1063/1.457480. Implementation after M. P. Allen and
|
|
342
|
+
D. J. Tildesley, Computer Simulation of Liquids, 2nd ed. (2017), example
|
|
343
|
+
code ``error_calc.py``.
|
|
344
|
+
|
|
345
|
+
Examples
|
|
346
|
+
--------
|
|
347
|
+
A signal of :math:`n = 16` frames that holds each value for :math:`L = 4`
|
|
348
|
+
frames has an exact table: for every block size :math:`B \\le L` the block
|
|
349
|
+
means are the held values themselves, so the inefficiency is
|
|
350
|
+
:math:`s(B) = B (n - 1) / (n - B)`, here 1, 30/14 and 5.
|
|
351
|
+
|
|
352
|
+
>>> import numpy as np
|
|
353
|
+
>>> from polyphys.analyze.stats import block_average
|
|
354
|
+
>>> x = np.repeat([1.0, 3.0, 2.0, 4.0], 4)
|
|
355
|
+
>>> table = block_average(x, min_blocks=4)
|
|
356
|
+
>>> table.block_size, table.n_blocks
|
|
357
|
+
(array([1, 2, 4]), array([16, 8, 4]))
|
|
358
|
+
>>> table.statistical_inefficiency.round(4)
|
|
359
|
+
array([1. , 2.1429, 5. ])
|
|
360
|
+
>>> table.sem.round(4)
|
|
361
|
+
array([0.2887, 0.4226, 0.6455])
|
|
362
|
+
|
|
363
|
+
A two-column input gives one table column per observable:
|
|
364
|
+
|
|
365
|
+
>>> table = block_average(np.column_stack([x, 2.0 * x]))
|
|
366
|
+
>>> table.observable_shape, table.sem.shape
|
|
367
|
+
((2,), (3, 2))
|
|
368
|
+
>>> np.allclose(table.sem[:, 1], 2.0 * table.sem[:, 0])
|
|
369
|
+
True
|
|
370
|
+
|
|
371
|
+
.. versionadded:: 0.5.0
|
|
372
|
+
"""
|
|
373
|
+
if min_blocks < 2:
|
|
374
|
+
raise ValueError(
|
|
375
|
+
f'min_blocks must be at least 2; got {min_blocks}.'
|
|
376
|
+
)
|
|
377
|
+
data = _as_series(x)
|
|
378
|
+
n_frames = int(data.shape[0])
|
|
379
|
+
if n_frames < min_blocks:
|
|
380
|
+
raise ValueError(
|
|
381
|
+
f'Input has {n_frames} frames but min_blocks is {min_blocks}; '
|
|
382
|
+
'at least min_blocks frames are needed.'
|
|
383
|
+
)
|
|
384
|
+
var_naive = data.var(axis=0, ddof=1)
|
|
385
|
+
if np.any(var_naive == 0.0):
|
|
386
|
+
raise ValueError(
|
|
387
|
+
'Every observable must vary across frames; a constant series has '
|
|
388
|
+
'an undefined statistical inefficiency.'
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
block_sizes: list[int] = []
|
|
392
|
+
block_counts: list[int] = []
|
|
393
|
+
means: list[npt.NDArray[np.float64]] = []
|
|
394
|
+
variances: list[npt.NDArray[np.float64]] = []
|
|
395
|
+
n_blocks = n_frames
|
|
396
|
+
block_size = 1
|
|
397
|
+
while True:
|
|
398
|
+
blocks = data[:n_blocks]
|
|
399
|
+
block_sizes.append(block_size)
|
|
400
|
+
block_counts.append(n_blocks)
|
|
401
|
+
means.append(blocks.mean(axis=0))
|
|
402
|
+
variances.append(blocks.var(axis=0, ddof=1))
|
|
403
|
+
if n_blocks // 2 < min_blocks:
|
|
404
|
+
break
|
|
405
|
+
n_blocks //= 2
|
|
406
|
+
# The blocking transformation: pair consecutive blocks, dropping a
|
|
407
|
+
# trailing unpaired one, and overwrite the front of the buffer.
|
|
408
|
+
data[:n_blocks] = 0.5 * (
|
|
409
|
+
data[0:2 * n_blocks:2] + data[1:2 * n_blocks:2]
|
|
410
|
+
)
|
|
411
|
+
block_size *= 2
|
|
412
|
+
|
|
413
|
+
size_arr = np.asarray(block_sizes, dtype=np.int64)
|
|
414
|
+
count_arr = np.asarray(block_counts, dtype=np.int64)
|
|
415
|
+
mean = np.stack(means)
|
|
416
|
+
var = np.stack(variances)
|
|
417
|
+
# Broadcast the per-level counts against the trailing observable axes.
|
|
418
|
+
level_shape = (-1,) + (1,) * (var.ndim - 1)
|
|
419
|
+
m = count_arr.reshape(level_shape).astype(np.float64)
|
|
420
|
+
b = size_arr.reshape(level_shape).astype(np.float64)
|
|
421
|
+
sem = np.sqrt(var / m)
|
|
422
|
+
sem_err = sem / np.sqrt(2.0 * (m - 1.0))
|
|
423
|
+
inefficiency = b * var / var_naive
|
|
424
|
+
inefficiency_err = inefficiency * np.sqrt(2.0 / (m - 1.0))
|
|
425
|
+
return BlockingResult(
|
|
426
|
+
n_frames=n_frames,
|
|
427
|
+
block_size=size_arr,
|
|
428
|
+
n_blocks=count_arr,
|
|
429
|
+
mean=mean,
|
|
430
|
+
var=var,
|
|
431
|
+
sem=sem,
|
|
432
|
+
sem_err=sem_err,
|
|
433
|
+
statistical_inefficiency=inefficiency,
|
|
434
|
+
inefficiency_err=inefficiency_err,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _select_lee(result: BlockingResult) -> npt.NDArray[np.int64]:
|
|
439
|
+
"""
|
|
440
|
+
Apply the rule of Lee et al. (2011), Eq. (14), to a blocking table.
|
|
441
|
+
|
|
442
|
+
Level ``k`` qualifies when ``B_k**3 > 2 n (SE_k / SE_0)**4``. Scanning
|
|
443
|
+
from the largest block downward, the selected level is the last
|
|
444
|
+
qualifying one before the first failure; ``-1`` marks an observable whose
|
|
445
|
+
largest level does not qualify. :func:`select_plateau` checks the result
|
|
446
|
+
against the acceptance criterion of the same paper.
|
|
447
|
+
"""
|
|
448
|
+
level_shape = (-1,) + (1,) * (result.sem.ndim - 1)
|
|
449
|
+
b = result.block_size.reshape(level_shape).astype(np.float64)
|
|
450
|
+
ratio = result.sem / result.sem[0]
|
|
451
|
+
qualifies = b ** 3 > 2.0 * result.n_frames * ratio ** 4
|
|
452
|
+
# Length of the run of qualifying levels that starts at the top level.
|
|
453
|
+
run_from_top = np.cumprod(qualifies[::-1], axis=0)
|
|
454
|
+
run_length = run_from_top.sum(axis=0)
|
|
455
|
+
return np.where(
|
|
456
|
+
run_length > 0, result.n_levels - run_length, -1
|
|
457
|
+
).astype(np.int64)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _select_sem_err(result: BlockingResult) -> npt.NDArray[np.int64]:
|
|
461
|
+
"""
|
|
462
|
+
Select the first level whose SEM agrees with the next two within error.
|
|
463
|
+
|
|
464
|
+
Level ``k`` qualifies when ``|SE_{k+j} - SE_k| <= sem_err_{k+j}`` for
|
|
465
|
+
``j = 1, 2``; ``-1`` marks an observable with no such level.
|
|
466
|
+
"""
|
|
467
|
+
sem = result.sem
|
|
468
|
+
err = result.sem_err
|
|
469
|
+
if result.n_levels < 3:
|
|
470
|
+
return np.full(result.observable_shape, -1, dtype=np.int64)
|
|
471
|
+
near_next = np.abs(sem[1:-1] - sem[:-2]) <= err[1:-1]
|
|
472
|
+
near_second = np.abs(sem[2:] - sem[:-2]) <= err[2:]
|
|
473
|
+
qualifies = near_next & near_second
|
|
474
|
+
found = qualifies.any(axis=0)
|
|
475
|
+
first = qualifies.argmax(axis=0)
|
|
476
|
+
return np.where(found, first, -1).astype(np.int64)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def select_plateau(
|
|
480
|
+
result: BlockingResult,
|
|
481
|
+
*,
|
|
482
|
+
method: PlateauMethod,
|
|
483
|
+
) -> int | npt.NDArray[np.int64]:
|
|
484
|
+
"""
|
|
485
|
+
Pick the blocking level that estimates the plateau, by a named rule.
|
|
486
|
+
|
|
487
|
+
Parameters
|
|
488
|
+
----------
|
|
489
|
+
result : BlockingResult
|
|
490
|
+
A table from :func:`block_average`.
|
|
491
|
+
method : {'lee', 'sem_err'}
|
|
492
|
+
The selection rule; required, because it is an estimator choice.
|
|
493
|
+
|
|
494
|
+
``'lee'``
|
|
495
|
+
The largest-block-downward scan of Lee et al. (2011): a level
|
|
496
|
+
qualifies when its block size :math:`B`, in frames, satisfies
|
|
497
|
+
:math:`B^3 > 2 n (\\mathrm{SE}(B) / \\mathrm{SE}(1))^4`, where
|
|
498
|
+
:math:`n` is the number of frames, :math:`\\mathrm{SE}(B)` the
|
|
499
|
+
level's standard error, and :math:`\\mathrm{SE}(1)` the naive
|
|
500
|
+
one at :math:`B = 1`. The selected level is the smallest block
|
|
501
|
+
size of the qualifying run that includes the largest block. The
|
|
502
|
+
rule balances the systematic underestimate of the error at small
|
|
503
|
+
blocks against its growing statistical error at large ones.
|
|
504
|
+
``'sem_err'``
|
|
505
|
+
The first level whose standard error is within one ``sem_err`` of
|
|
506
|
+
each of the next two levels, that is, where the table stops
|
|
507
|
+
rising. A convenience heuristic, not a published rule.
|
|
508
|
+
|
|
509
|
+
Returns
|
|
510
|
+
-------
|
|
511
|
+
int or numpy.ndarray
|
|
512
|
+
The selected level index into the table's axis 0: an ``int`` for a
|
|
513
|
+
scalar series, else an ``int64`` array of shape
|
|
514
|
+
``result.observable_shape``. ``-1`` marks an observable for which no
|
|
515
|
+
level qualifies.
|
|
516
|
+
|
|
517
|
+
Raises
|
|
518
|
+
------
|
|
519
|
+
ValueError
|
|
520
|
+
If `method` is not one of the rules above.
|
|
521
|
+
|
|
522
|
+
Warns
|
|
523
|
+
-----
|
|
524
|
+
RuntimeWarning
|
|
525
|
+
When no level qualifies for some observable, which means the series
|
|
526
|
+
is too short for its correlation time or is not stationary. Also,
|
|
527
|
+
under either `method`, when an observable has a selected level but
|
|
528
|
+
the block size the ``'lee'`` rule selects is not below
|
|
529
|
+
``n_frames / 50``, or that rule finds no level: Lee et al. (2011)
|
|
530
|
+
then consider the estimate unreliable. The selected level is still
|
|
531
|
+
returned.
|
|
532
|
+
|
|
533
|
+
Notes
|
|
534
|
+
-----
|
|
535
|
+
With :math:`n_\\mathrm{levels}` the number of rows of the table and
|
|
536
|
+
:math:`n_\\mathrm{obs}` the number of observables, both rules are
|
|
537
|
+
:math:`O(n_\\mathrm{levels} \\, n_\\mathrm{obs})` in time and space.
|
|
538
|
+
|
|
539
|
+
Lee et al. write their Eq. (14) as :math:`B_\\mathrm{opt} = (2 n
|
|
540
|
+
n_\\mathrm{corr}^2)^{1/3}`, the optimal block size for :math:`n` frames,
|
|
541
|
+
with :math:`n_\\mathrm{corr}` the statistical inefficiency :math:`s`, the
|
|
542
|
+
plateau of :math:`(\\mathrm{SE}(B) / \\mathrm{SE}(1))^2`; hence the
|
|
543
|
+
fourth power in the rule above. They cite Wolff (2004), whose Eq. (47)
|
|
544
|
+
reads :math:`B = \\tau (2N/\\tau)^{1/3}` for :math:`N` frames, with
|
|
545
|
+
:math:`\\tau` the integrated autocorrelation time, which is
|
|
546
|
+
:math:`s / 2`. The two prescriptions therefore differ by a factor of 4 in
|
|
547
|
+
:math:`B^3`; neither constant is exact, as Wolff's derivation is an
|
|
548
|
+
order-of-magnitude balance. This function implements Lee's form, as
|
|
549
|
+
``pyblock`` does.
|
|
550
|
+
|
|
551
|
+
Lee et al. accept the analysis only when :math:`n / B_\\mathrm{opt} >
|
|
552
|
+
50`, which with Eq. (14) reads :math:`n / s > 500`: the series must hold
|
|
553
|
+
more than about 500 independent samples. That is a property of the
|
|
554
|
+
series, not of the rule, so this function tests it at the ``'lee'``
|
|
555
|
+
level under either `method`. The ``'sem_err'`` rule can stop at a level
|
|
556
|
+
with more than 50 blocks on a series that fails the test, where the
|
|
557
|
+
table levels off before the blocks outgrow the correlation time.
|
|
558
|
+
|
|
559
|
+
References
|
|
560
|
+
----------
|
|
561
|
+
R. M. Lee et al., Phys. Rev. E 83, 066706 (2011), Sec. IV, Eq. (13)-(15).
|
|
562
|
+
U. Wolff, Comput. Phys. Commun. 156, 143 (2004), Eq. (46)-(48).
|
|
563
|
+
|
|
564
|
+
Examples
|
|
565
|
+
--------
|
|
566
|
+
A signal that holds each value for 8 frames has a statistical
|
|
567
|
+
inefficiency :math:`s \\approx 8`, so on the plateau
|
|
568
|
+
:math:`(\\mathrm{SE}(B) / \\mathrm{SE}(1))^4 = s^2 \\approx 8^2`. With
|
|
569
|
+
:math:`n = 16384` frames the Lee rule then needs :math:`B^3 > 2 \\cdot
|
|
570
|
+
16384 \\cdot 8^2`, that is :math:`B > 128`, and it selects the first
|
|
571
|
+
power of two above 128:
|
|
572
|
+
|
|
573
|
+
>>> import numpy as np
|
|
574
|
+
>>> from polyphys.analyze.stats import block_average, select_plateau
|
|
575
|
+
>>> rng = np.random.default_rng(7)
|
|
576
|
+
>>> x = np.repeat(rng.standard_normal(2048), 8)
|
|
577
|
+
>>> table = block_average(x)
|
|
578
|
+
>>> level = select_plateau(table, method='lee')
|
|
579
|
+
>>> int(table.block_size[level])
|
|
580
|
+
256
|
|
581
|
+
|
|
582
|
+
The ``'sem_err'`` rule stops where the standard error first flattens,
|
|
583
|
+
which for this signal is at or just past the plateau length:
|
|
584
|
+
|
|
585
|
+
>>> level = select_plateau(table, method='sem_err')
|
|
586
|
+
>>> bool(table.block_size[level] >= 8)
|
|
587
|
+
True
|
|
588
|
+
|
|
589
|
+
Holding each of 64 values for 256 frames gives :math:`s \\approx 256`,
|
|
590
|
+
so the series holds only 64 independent samples. The ``'sem_err'`` rule
|
|
591
|
+
stops at the period, where 64 blocks remain, but the Lee block size
|
|
592
|
+
leaves fewer than 50, so both rules warn:
|
|
593
|
+
|
|
594
|
+
>>> import warnings
|
|
595
|
+
>>> rng = np.random.default_rng(7)
|
|
596
|
+
>>> x = np.repeat(rng.standard_normal(64), 256)
|
|
597
|
+
>>> table = block_average(x)
|
|
598
|
+
>>> with warnings.catch_warnings(record=True) as caught:
|
|
599
|
+
... warnings.simplefilter('always')
|
|
600
|
+
... level = select_plateau(table, method='sem_err')
|
|
601
|
+
>>> int(table.block_size[level]), int(table.n_blocks[level])
|
|
602
|
+
(256, 64)
|
|
603
|
+
>>> [type(w.message).__name__ for w in caught]
|
|
604
|
+
['RuntimeWarning']
|
|
605
|
+
|
|
606
|
+
.. versionadded:: 0.5.0
|
|
607
|
+
"""
|
|
608
|
+
if method not in ('lee', 'sem_err'):
|
|
609
|
+
raise ValueError(
|
|
610
|
+
f"Unknown plateau method {method!r}; choose 'lee' or 'sem_err'."
|
|
611
|
+
)
|
|
612
|
+
lee_level = _select_lee(result)
|
|
613
|
+
level = lee_level if method == 'lee' else _select_sem_err(result)
|
|
614
|
+
|
|
615
|
+
# Lee's acceptance criterion judges the series, so it uses the Lee block
|
|
616
|
+
# size whichever rule selected the level. An observable without a
|
|
617
|
+
# selected level gets the no-plateau warning below instead.
|
|
618
|
+
lee_size = result.block_size[np.maximum(lee_level, 0)]
|
|
619
|
+
too_short = (level >= 0) & (
|
|
620
|
+
(lee_level < 0)
|
|
621
|
+
| (LEE_MIN_BLOCK_RATIO * lee_size >= result.n_frames)
|
|
622
|
+
)
|
|
623
|
+
n_short = int(np.count_nonzero(too_short))
|
|
624
|
+
if n_short:
|
|
625
|
+
warnings.warn(
|
|
626
|
+
f'For {n_short} observable(s) the block size of Lee et al. '
|
|
627
|
+
f'(2011), Eq. (14), is not below n_frames / '
|
|
628
|
+
f'{LEE_MIN_BLOCK_RATIO}, or no block size satisfies it: the '
|
|
629
|
+
'series is too short for its correlation time, and Lee et al. '
|
|
630
|
+
'consider the error estimate unreliable. Collect more frames.',
|
|
631
|
+
RuntimeWarning,
|
|
632
|
+
stacklevel=2,
|
|
633
|
+
)
|
|
634
|
+
n_missing = int(np.count_nonzero(level < 0))
|
|
635
|
+
if n_missing:
|
|
636
|
+
warnings.warn(
|
|
637
|
+
f'No plateau found for {n_missing} observable(s): the series is '
|
|
638
|
+
'too short for its correlation time, or not stationary. The '
|
|
639
|
+
'selected level is -1 and the wrapped estimate NaN.',
|
|
640
|
+
RuntimeWarning,
|
|
641
|
+
stacklevel=2,
|
|
642
|
+
)
|
|
643
|
+
if level.ndim == 0:
|
|
644
|
+
return int(level)
|
|
645
|
+
return level
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _pick_level(
|
|
649
|
+
table: npt.NDArray[np.float64],
|
|
650
|
+
level: int | npt.NDArray[np.int64],
|
|
651
|
+
) -> float | npt.NDArray[np.float64]:
|
|
652
|
+
"""
|
|
653
|
+
Take one row of a per-level array per observable; NaN where ``level < 0``.
|
|
654
|
+
"""
|
|
655
|
+
index = np.asarray(level, dtype=np.int64)
|
|
656
|
+
picked = np.take_along_axis(
|
|
657
|
+
table, np.expand_dims(np.maximum(index, 0), 0), axis=0
|
|
658
|
+
)[0]
|
|
659
|
+
picked = np.where(index < 0, np.nan, picked)
|
|
660
|
+
if picked.ndim == 0:
|
|
661
|
+
return float(picked)
|
|
662
|
+
return picked
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def sem_blocked(
|
|
666
|
+
x: npt.ArrayLike,
|
|
667
|
+
*,
|
|
668
|
+
method: PlateauMethod,
|
|
669
|
+
min_blocks: int = 4,
|
|
670
|
+
) -> float | npt.NDArray[np.float64]:
|
|
671
|
+
"""
|
|
672
|
+
Standard error of a time average over correlated frames.
|
|
673
|
+
|
|
674
|
+
Runs :func:`block_average` and :func:`select_plateau` and returns the
|
|
675
|
+
standard error at the selected level. This is the error bar to report
|
|
676
|
+
for the mean of `x` over its frames.
|
|
677
|
+
|
|
678
|
+
Parameters
|
|
679
|
+
----------
|
|
680
|
+
x : array_like
|
|
681
|
+
Time series with frames along axis 0, shape ``(n_frames, ...)``.
|
|
682
|
+
method : {'lee', 'sem_err'}
|
|
683
|
+
The plateau rule; see :func:`select_plateau`.
|
|
684
|
+
min_blocks : int, default 4
|
|
685
|
+
Passed to :func:`block_average`.
|
|
686
|
+
|
|
687
|
+
Returns
|
|
688
|
+
-------
|
|
689
|
+
float or numpy.ndarray
|
|
690
|
+
The blocked standard error, in the units of `x`: a ``float`` for a
|
|
691
|
+
scalar series, else a ``float64`` array of shape ``x.shape[1:]``. NaN
|
|
692
|
+
where no plateau was found.
|
|
693
|
+
|
|
694
|
+
Raises
|
|
695
|
+
------
|
|
696
|
+
ValueError
|
|
697
|
+
As :func:`block_average` and :func:`select_plateau`.
|
|
698
|
+
|
|
699
|
+
Warns
|
|
700
|
+
-----
|
|
701
|
+
RuntimeWarning
|
|
702
|
+
As :func:`select_plateau`.
|
|
703
|
+
|
|
704
|
+
Notes
|
|
705
|
+
-----
|
|
706
|
+
For independent frames the result agrees with
|
|
707
|
+
:func:`polyphys.analyze.measurer.sem` within its own error bar; for
|
|
708
|
+
correlated frames it is larger by :math:`\\sqrt{s}`, where :math:`s` is
|
|
709
|
+
the statistical inefficiency, the number of frames that make one
|
|
710
|
+
independent sample. With :math:`n` the number of frames and
|
|
711
|
+
:math:`n_\\mathrm{obs}` the number of observables (the product of the
|
|
712
|
+
trailing dimensions of `x`), time is :math:`O(n \\, n_\\mathrm{obs})` and
|
|
713
|
+
space one ``float64`` copy of `x`.
|
|
714
|
+
|
|
715
|
+
Examples
|
|
716
|
+
--------
|
|
717
|
+
Each value of this signal is held for 8 frames, so :math:`s \\approx 8`
|
|
718
|
+
and the blocked error is about :math:`\\sqrt{8} \\approx 2.8` times the
|
|
719
|
+
naive one:
|
|
720
|
+
|
|
721
|
+
>>> import numpy as np
|
|
722
|
+
>>> from polyphys.analyze.stats import sem_blocked
|
|
723
|
+
>>> from polyphys.analyze.measurer import sem
|
|
724
|
+
>>> rng = np.random.default_rng(7)
|
|
725
|
+
>>> x = np.repeat(rng.standard_normal(2048), 8)
|
|
726
|
+
>>> ratio = sem_blocked(x, method='lee') / sem(x)
|
|
727
|
+
>>> bool(2.3 < ratio < 3.4)
|
|
728
|
+
True
|
|
729
|
+
|
|
730
|
+
.. versionadded:: 0.5.0
|
|
731
|
+
"""
|
|
732
|
+
result = block_average(x, min_blocks=min_blocks)
|
|
733
|
+
level = select_plateau(result, method=method)
|
|
734
|
+
return _pick_level(result.sem, level)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def statistical_inefficiency(
|
|
738
|
+
x: npt.ArrayLike,
|
|
739
|
+
*,
|
|
740
|
+
method: PlateauMethod,
|
|
741
|
+
min_blocks: int = 4,
|
|
742
|
+
) -> float | npt.NDArray[np.float64]:
|
|
743
|
+
"""
|
|
744
|
+
Estimate the frames per independent sample of a time series, its
|
|
745
|
+
statistical inefficiency, from its blocking table.
|
|
746
|
+
|
|
747
|
+
Runs :func:`block_average` and :func:`select_plateau` and returns the
|
|
748
|
+
statistical inefficiency at the selected level: the ratio of the plateau
|
|
749
|
+
variance of the mean to the naive one, so ``n_frames / s`` is the
|
|
750
|
+
effective number of independent frames.
|
|
751
|
+
|
|
752
|
+
Parameters
|
|
753
|
+
----------
|
|
754
|
+
x : array_like
|
|
755
|
+
Time series with frames along axis 0, shape ``(n_frames, ...)``.
|
|
756
|
+
method : {'lee', 'sem_err'}
|
|
757
|
+
The plateau rule; see :func:`select_plateau`.
|
|
758
|
+
min_blocks : int, default 4
|
|
759
|
+
Passed to :func:`block_average`.
|
|
760
|
+
|
|
761
|
+
Returns
|
|
762
|
+
-------
|
|
763
|
+
float or numpy.ndarray
|
|
764
|
+
Dimensionless, in frames; 1 for independent frames, below 1 for
|
|
765
|
+
anticorrelated ones. A ``float`` for a scalar series, else a
|
|
766
|
+
``float64`` array of shape ``x.shape[1:]``. NaN where no plateau was
|
|
767
|
+
found.
|
|
768
|
+
|
|
769
|
+
Raises
|
|
770
|
+
------
|
|
771
|
+
ValueError
|
|
772
|
+
As :func:`block_average` and :func:`select_plateau`.
|
|
773
|
+
|
|
774
|
+
Warns
|
|
775
|
+
-----
|
|
776
|
+
RuntimeWarning
|
|
777
|
+
As :func:`select_plateau`.
|
|
778
|
+
|
|
779
|
+
Notes
|
|
780
|
+
-----
|
|
781
|
+
For a stationary series :math:`x_t` whose normalized autocorrelation
|
|
782
|
+
function at a lag of :math:`t` frames is :math:`\\rho(t)`, with
|
|
783
|
+
:math:`\\rho(0) = 1`, the inefficiency is :math:`s =
|
|
784
|
+
\\sum_{t=-\\infty}^{\\infty} \\rho(t) = 2 \\tau_\\mathrm{int}`, where
|
|
785
|
+
:math:`\\tau_\\mathrm{int}` is the integrated autocorrelation time (Wolff
|
|
786
|
+
2004, Eq. (23) and (25)). For an AR(1) process, :math:`x_t = \\phi
|
|
787
|
+
x_{t-1} + \\epsilon_t` with coefficient :math:`|\\phi| < 1` and
|
|
788
|
+
independent noise :math:`\\epsilon_t`, :math:`\\rho(t) = \\phi^{|t|}` and
|
|
789
|
+
:math:`s = (1 + \\phi) / (1 - \\phi)`. With :math:`n` the number of
|
|
790
|
+
frames and :math:`n_\\mathrm{obs}` the number of observables (the
|
|
791
|
+
product of the trailing dimensions of `x`), time is :math:`O(n \\,
|
|
792
|
+
n_\\mathrm{obs})` and space one ``float64`` copy of `x`.
|
|
793
|
+
|
|
794
|
+
Examples
|
|
795
|
+
--------
|
|
796
|
+
Each value of this signal is held for 8 frames, so :math:`s \\approx 8`:
|
|
797
|
+
|
|
798
|
+
>>> import numpy as np
|
|
799
|
+
>>> from polyphys.analyze.stats import statistical_inefficiency
|
|
800
|
+
>>> rng = np.random.default_rng(7)
|
|
801
|
+
>>> x = np.repeat(rng.standard_normal(2048), 8)
|
|
802
|
+
>>> s = statistical_inefficiency(x, method='lee')
|
|
803
|
+
>>> bool(5.0 < s < 11.0)
|
|
804
|
+
True
|
|
805
|
+
|
|
806
|
+
.. versionadded:: 0.5.0
|
|
807
|
+
"""
|
|
808
|
+
result = block_average(x, min_blocks=min_blocks)
|
|
809
|
+
level = select_plateau(result, method=method)
|
|
810
|
+
return _pick_level(result.statistical_inefficiency, level)
|