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.
Files changed (69) hide show
  1. polyphys/__init__.py +7 -0
  2. polyphys/__version__.py +22 -0
  3. polyphys/analyze/__init__.py +51 -0
  4. polyphys/analyze/clusters.py +1738 -0
  5. polyphys/analyze/correlations.py +995 -0
  6. polyphys/analyze/density.py +1138 -0
  7. polyphys/analyze/measurer.py +1338 -0
  8. polyphys/analyze/pipeline.py +1868 -0
  9. polyphys/analyze/stats.py +810 -0
  10. polyphys/analyze/sumrule.py +1159 -0
  11. polyphys/manage/__init__.py +11 -0
  12. polyphys/manage/organizer.py +2188 -0
  13. polyphys/manage/parser.py +1264 -0
  14. polyphys/manage/types.py +171 -0
  15. polyphys/manage/utils.py +777 -0
  16. polyphys/probe/__init__.py +25 -0
  17. polyphys/probe/binders.py +711 -0
  18. polyphys/probe/chain_size.py +583 -0
  19. polyphys/probe/core.py +437 -0
  20. polyphys/probe/histograms.py +863 -0
  21. polyphys/probe/pairs.py +555 -0
  22. polyphys/simulate/__init__.py +48 -0
  23. polyphys/simulate/build.py +1231 -0
  24. polyphys/simulate/core.py +237 -0
  25. polyphys/simulate/data.py +977 -0
  26. polyphys/simulate/dump.py +752 -0
  27. polyphys/simulate/log.py +788 -0
  28. polyphys/spec.py +1359 -0
  29. polyphys/tests/analyze/test_clusters.py +1058 -0
  30. polyphys/tests/analyze/test_correlations.py +708 -0
  31. polyphys/tests/analyze/test_density.py +813 -0
  32. polyphys/tests/analyze/test_measurer.py +785 -0
  33. polyphys/tests/analyze/test_pipeline.py +1881 -0
  34. polyphys/tests/analyze/test_stats.py +732 -0
  35. polyphys/tests/analyze/test_sumrule.py +917 -0
  36. polyphys/tests/conftest.py +753 -0
  37. polyphys/tests/data/lammps_build/make_starts.py +70 -0
  38. polyphys/tests/manage/test_organizer.py +1495 -0
  39. polyphys/tests/manage/test_parser.py +1222 -0
  40. polyphys/tests/manage/test_utils.py +498 -0
  41. polyphys/tests/probe/conftest.py +407 -0
  42. polyphys/tests/probe/test_binders.py +686 -0
  43. polyphys/tests/probe/test_chain_size.py +557 -0
  44. polyphys/tests/probe/test_core.py +391 -0
  45. polyphys/tests/probe/test_histograms.py +779 -0
  46. polyphys/tests/probe/test_pairs.py +475 -0
  47. polyphys/tests/simulate/test_build.py +1042 -0
  48. polyphys/tests/simulate/test_core.py +106 -0
  49. polyphys/tests/simulate/test_data.py +858 -0
  50. polyphys/tests/simulate/test_dump.py +487 -0
  51. polyphys/tests/simulate/test_log.py +667 -0
  52. polyphys/tests/strategies.py +120 -0
  53. polyphys/tests/synthetic.py +410 -0
  54. polyphys/tests/test_conftest.py +414 -0
  55. polyphys/tests/test_docs.py +82 -0
  56. polyphys/tests/test_packaging.py +97 -0
  57. polyphys/tests/test_spec.py +772 -0
  58. polyphys/tests/viz/conftest.py +32 -0
  59. polyphys/tests/viz/test_colors.py +113 -0
  60. polyphys/tests/viz/test_helpers.py +292 -0
  61. polyphys/viz/__init__.py +15 -0
  62. polyphys/viz/colors.py +204 -0
  63. polyphys/viz/helpers.py +549 -0
  64. polyphys-1.0.0.dist-info/METADATA +339 -0
  65. polyphys-1.0.0.dist-info/RECORD +69 -0
  66. polyphys-1.0.0.dist-info/WHEEL +5 -0
  67. polyphys-1.0.0.dist-info/licenses/AUTHORS.rst +13 -0
  68. polyphys-1.0.0.dist-info/licenses/LICENSE +22 -0
  69. 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)