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,732 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for :mod:`polyphys.analyze.stats`.
|
|
3
|
+
|
|
4
|
+
Expected values come from three analytic results, derived here so that no
|
|
5
|
+
expected value depends on the code under test.
|
|
6
|
+
|
|
7
|
+
R1. For a stationary series with autocorrelation function ``rho(t)``, the
|
|
8
|
+
statistical inefficiency is ``s = sum_{t=-inf}^{inf} rho(t)``, twice the
|
|
9
|
+
integrated autocorrelation time (Wolff, Comput. Phys. Commun. 156, 143
|
|
10
|
+
(2004), Eq. (23) and (25)). For independent frames ``s = 1`` at every
|
|
11
|
+
block size.
|
|
12
|
+
|
|
13
|
+
R2. AR(1), ``x_t = phi x_{t-1} + e_t`` with unit-variance innovations and a
|
|
14
|
+
stationary start, has ``rho(t) = phi**|t|`` and ``var(x) = 1/(1-phi^2)``.
|
|
15
|
+
The variance of the mean of ``B`` consecutive values is
|
|
16
|
+
``(var(x)/B) [1 + 2 sum_{t=1}^{B-1} (1 - t/B) phi**t]``, so the
|
|
17
|
+
inefficiency at block size ``B`` is the bracket, and its ``B -> inf``
|
|
18
|
+
limit is ``(1+phi)/(1-phi)`` by the geometric series in R1. Finite-``n``
|
|
19
|
+
corrections are of order ``s/n``, below 3e-4 for the sizes used here.
|
|
20
|
+
|
|
21
|
+
R3. A block-constant series, ``x_t = c_{t // L}`` with i.i.d. ``c_i`` and
|
|
22
|
+
``L`` dividing ``n``: for ``B <= L`` every block mean is one of the
|
|
23
|
+
``c_i`` and both ``ddof=1`` variances are sums over the same repeated
|
|
24
|
+
values, so exactly ``s(B) = B (n-1) / (n-B)``. For ``B >= L`` the block
|
|
25
|
+
means are means of ``B/L`` independent ``c_i``, so ``E[var_B] = var(c)
|
|
26
|
+
L/B`` and ``E[var_0] = var(c) (n-L)/(n-1)``, giving ``L (n-1)/(n-L)`` as
|
|
27
|
+
the ratio of expectations; the two forms agree at ``B = L``.
|
|
28
|
+
|
|
29
|
+
Statistical tolerances are five times the leading-order Gaussian error of a
|
|
30
|
+
variance estimated from ``m`` block means, ``sqrt(2/(m-1))`` relative (Lee et
|
|
31
|
+
al., Phys. Rev. E 83, 066706 (2011), Eq. (15)), checked only at levels with at
|
|
32
|
+
least 32 blocks, where that Gaussian approximation holds. With about one
|
|
33
|
+
hundred such comparisons in this file, the false-failure probability is below
|
|
34
|
+
1e-4, and every input is seeded, so a run is deterministic.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
import warnings
|
|
38
|
+
|
|
39
|
+
import numpy as np
|
|
40
|
+
import pytest
|
|
41
|
+
from hypothesis import assume, example, given, settings
|
|
42
|
+
from hypothesis import strategies as st
|
|
43
|
+
from hypothesis.extra.numpy import arrays
|
|
44
|
+
from numpy.testing import assert_allclose, assert_array_equal
|
|
45
|
+
|
|
46
|
+
from polyphys.analyze.measurer import sem
|
|
47
|
+
from polyphys.analyze.stats import (
|
|
48
|
+
BlockingResult,
|
|
49
|
+
block_average,
|
|
50
|
+
select_plateau,
|
|
51
|
+
sem_blocked,
|
|
52
|
+
statistical_inefficiency,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Relative tolerance for results that differ only by float64 round-off over
|
|
56
|
+
# at most ~2 n additions: far above the 2.2e-16 epsilon, far below any
|
|
57
|
+
# statistical effect.
|
|
58
|
+
FLOAT64_RTOL = 1e-12
|
|
59
|
+
# Gaussian-error multiple and the smallest block count at which it is used;
|
|
60
|
+
# see the module docstring.
|
|
61
|
+
SIGMA = 5.0
|
|
62
|
+
MIN_BLOCKS_FOR_GAUSSIAN = 32
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def ar1_inefficiency(phi: float, block_size: np.ndarray) -> np.ndarray:
|
|
66
|
+
"""Return the AR(1) inefficiency at each block size, from R2."""
|
|
67
|
+
out = np.empty(len(block_size), dtype=np.float64)
|
|
68
|
+
for i, size in enumerate(block_size):
|
|
69
|
+
t = np.arange(1, size)
|
|
70
|
+
out[i] = 1.0 + 2.0 * np.sum((1.0 - t / size) * phi ** t)
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def block_constant_inefficiency(
|
|
75
|
+
period: int, n_frames: int, block_size: np.ndarray
|
|
76
|
+
) -> np.ndarray:
|
|
77
|
+
"""Return the block-constant inefficiency at each block size, from R3."""
|
|
78
|
+
effective = np.minimum(block_size, period)
|
|
79
|
+
return effective * (n_frames - 1) / (n_frames - effective)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def relative_gaussian_error(n_blocks: np.ndarray) -> np.ndarray:
|
|
83
|
+
"""Return the relative error of a variance from ``n_blocks`` samples."""
|
|
84
|
+
return np.sqrt(2.0 / (n_blocks - 1.0))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@pytest.fixture
|
|
88
|
+
def make_ar1():
|
|
89
|
+
"""Return a factory for a stationary AR(1) series with unit innovations."""
|
|
90
|
+
|
|
91
|
+
def _make(phi: float, n_frames: int, seed: int) -> np.ndarray:
|
|
92
|
+
rng = np.random.default_rng(seed)
|
|
93
|
+
noise = rng.standard_normal(n_frames)
|
|
94
|
+
x = np.empty(n_frames, dtype=np.float64)
|
|
95
|
+
x[0] = noise[0] / np.sqrt(1.0 - phi ** 2) # stationary start
|
|
96
|
+
for t in range(1, n_frames):
|
|
97
|
+
x[t] = phi * x[t - 1] + noise[t]
|
|
98
|
+
return x
|
|
99
|
+
|
|
100
|
+
return _make
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@pytest.fixture
|
|
104
|
+
def make_block_constant():
|
|
105
|
+
"""Return a factory for a series constant over blocks of ``period``."""
|
|
106
|
+
|
|
107
|
+
def _make(period: int, n_frames: int, seed: int) -> np.ndarray:
|
|
108
|
+
rng = np.random.default_rng(seed)
|
|
109
|
+
return np.repeat(rng.standard_normal(n_frames // period), period)
|
|
110
|
+
|
|
111
|
+
return _make
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@pytest.fixture
|
|
115
|
+
def make_table():
|
|
116
|
+
"""Return a factory for a hand-built :class:`BlockingResult`.
|
|
117
|
+
|
|
118
|
+
Only ``sem`` (and optionally ``sem_err``) carry the test's values; the
|
|
119
|
+
other fields are filled consistently with the given ``n_frames``.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def _make(
|
|
123
|
+
sem_values, n_frames: int, sem_err_values=None
|
|
124
|
+
) -> BlockingResult:
|
|
125
|
+
sem_arr = np.asarray(sem_values, dtype=np.float64)
|
|
126
|
+
n_levels = len(sem_arr)
|
|
127
|
+
block_size = 2 ** np.arange(n_levels, dtype=np.int64)
|
|
128
|
+
n_blocks = n_frames // block_size
|
|
129
|
+
var = sem_arr ** 2 * n_blocks
|
|
130
|
+
if sem_err_values is None:
|
|
131
|
+
sem_err = sem_arr / np.sqrt(2.0 * (n_blocks - 1))
|
|
132
|
+
else:
|
|
133
|
+
sem_err = np.asarray(sem_err_values, dtype=np.float64)
|
|
134
|
+
inefficiency = block_size * var / var[0]
|
|
135
|
+
return BlockingResult(
|
|
136
|
+
n_frames=n_frames,
|
|
137
|
+
block_size=block_size,
|
|
138
|
+
n_blocks=n_blocks,
|
|
139
|
+
mean=np.zeros(n_levels),
|
|
140
|
+
var=var,
|
|
141
|
+
sem=sem_arr,
|
|
142
|
+
sem_err=sem_err,
|
|
143
|
+
statistical_inefficiency=inefficiency,
|
|
144
|
+
inefficiency_err=inefficiency * np.sqrt(2.0 / (n_blocks - 1)),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return _make
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# --- the table ---------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@pytest.mark.parametrize(
|
|
154
|
+
"n_frames,n_levels",
|
|
155
|
+
[
|
|
156
|
+
pytest.param(64, 5, id="power-of-two-64"),
|
|
157
|
+
pytest.param(100, 5, id="odd-levels-100"),
|
|
158
|
+
pytest.param(127, 5, id="just-below-128"),
|
|
159
|
+
pytest.param(128, 6, id="power-of-two-128"),
|
|
160
|
+
pytest.param(1000, 8, id="1000"),
|
|
161
|
+
],
|
|
162
|
+
)
|
|
163
|
+
def test_block_average_level_layout(n_frames, n_levels):
|
|
164
|
+
"""Levels have block sizes 1, 2, 4, ... and stop at min_blocks blocks."""
|
|
165
|
+
# n_levels is floor(log2(n_frames // 4)) + 1, worked by hand per case.
|
|
166
|
+
x = np.random.default_rng(0).standard_normal(n_frames)
|
|
167
|
+
|
|
168
|
+
table = block_average(x, min_blocks=4)
|
|
169
|
+
|
|
170
|
+
assert table.n_levels == n_levels
|
|
171
|
+
assert_array_equal(table.block_size, 2 ** np.arange(n_levels))
|
|
172
|
+
assert_array_equal(table.n_blocks, n_frames // table.block_size)
|
|
173
|
+
assert table.n_blocks[-1] >= 4
|
|
174
|
+
assert table.n_blocks[-1] // 2 < 4
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_block_average_level_zero_is_naive_estimate():
|
|
178
|
+
"""Level 0 reproduces the naive SEM and an inefficiency of exactly 1."""
|
|
179
|
+
x = np.random.default_rng(1).standard_normal(1000)
|
|
180
|
+
|
|
181
|
+
table = block_average(x)
|
|
182
|
+
|
|
183
|
+
assert table.sem[0] == pytest.approx(sem(x), rel=FLOAT64_RTOL)
|
|
184
|
+
# var / var is exactly 1 in IEEE arithmetic.
|
|
185
|
+
assert table.statistical_inefficiency[0] == pytest.approx(1.0, abs=0.0)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_block_average_preserves_mean_without_dropped_frames():
|
|
189
|
+
"""With a power-of-two frame count the mean is the same at every level."""
|
|
190
|
+
x = np.random.default_rng(2).standard_normal(2 ** 10)
|
|
191
|
+
|
|
192
|
+
table = block_average(x)
|
|
193
|
+
|
|
194
|
+
assert_allclose(
|
|
195
|
+
table.mean, np.full(table.n_levels, x.mean()), rtol=FLOAT64_RTOL
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@pytest.mark.parametrize(
|
|
200
|
+
"scale,offset",
|
|
201
|
+
[
|
|
202
|
+
pytest.param(-3.0, 7.0, id="negative-scale"),
|
|
203
|
+
pytest.param(0.5, 7.0, id="shrink"),
|
|
204
|
+
],
|
|
205
|
+
)
|
|
206
|
+
def test_block_average_is_affine_equivariant(scale, offset):
|
|
207
|
+
"""Scaling a series scales its SEMs; the inefficiency does not change."""
|
|
208
|
+
x = np.random.default_rng(3).standard_normal(2 ** 10)
|
|
209
|
+
|
|
210
|
+
base = block_average(x)
|
|
211
|
+
moved = block_average(scale * x + offset)
|
|
212
|
+
|
|
213
|
+
assert_allclose(moved.sem, abs(scale) * base.sem, rtol=FLOAT64_RTOL)
|
|
214
|
+
assert_allclose(
|
|
215
|
+
moved.statistical_inefficiency,
|
|
216
|
+
base.statistical_inefficiency,
|
|
217
|
+
rtol=FLOAT64_RTOL,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# --- known answers -----------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_block_average_iid_inefficiency_is_one_at_every_level():
|
|
225
|
+
"""Independent frames give an inefficiency of 1 at every level (R1)."""
|
|
226
|
+
n_frames = 2 ** 16
|
|
227
|
+
x = np.random.default_rng(4).standard_normal(n_frames)
|
|
228
|
+
|
|
229
|
+
table = block_average(x)
|
|
230
|
+
|
|
231
|
+
checked = table.n_blocks >= MIN_BLOCKS_FOR_GAUSSIAN
|
|
232
|
+
sigma = relative_gaussian_error(table.n_blocks[checked])
|
|
233
|
+
pulls = (table.statistical_inefficiency[checked] - 1.0) / sigma
|
|
234
|
+
assert_allclose(pulls, np.zeros_like(pulls), rtol=0.0, atol=SIGMA)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@pytest.mark.parametrize(
|
|
238
|
+
"phi,seed",
|
|
239
|
+
[
|
|
240
|
+
pytest.param(-0.5, 5, id="anticorrelated"),
|
|
241
|
+
pytest.param(0.5, 6, id="phi-0.5"),
|
|
242
|
+
pytest.param(0.9, 7, id="phi-0.9"),
|
|
243
|
+
],
|
|
244
|
+
)
|
|
245
|
+
def test_block_average_ar1_inefficiency_matches_analytic(
|
|
246
|
+
make_ar1, phi, seed
|
|
247
|
+
):
|
|
248
|
+
"""An AR(1) series gives the analytic inefficiency at each level (R2)."""
|
|
249
|
+
n_frames = 2 ** 16
|
|
250
|
+
x = make_ar1(phi, n_frames, seed)
|
|
251
|
+
|
|
252
|
+
table = block_average(x)
|
|
253
|
+
|
|
254
|
+
checked = table.n_blocks >= MIN_BLOCKS_FOR_GAUSSIAN
|
|
255
|
+
expected = ar1_inefficiency(phi, table.block_size[checked])
|
|
256
|
+
sigma = expected * relative_gaussian_error(table.n_blocks[checked])
|
|
257
|
+
pulls = (table.statistical_inefficiency[checked] - expected) / sigma
|
|
258
|
+
assert_allclose(pulls, np.zeros_like(pulls), rtol=0.0, atol=SIGMA)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@pytest.mark.parametrize(
|
|
262
|
+
"phi,seed,method",
|
|
263
|
+
[
|
|
264
|
+
pytest.param(0.5, 8, "lee", id="phi-0.5-lee"),
|
|
265
|
+
pytest.param(0.9, 9, "lee", id="phi-0.9-lee"),
|
|
266
|
+
pytest.param(0.5, 8, "sem_err", id="phi-0.5-sem_err"),
|
|
267
|
+
pytest.param(0.9, 9, "sem_err", id="phi-0.9-sem_err"),
|
|
268
|
+
],
|
|
269
|
+
)
|
|
270
|
+
def test_sem_blocked_ar1_matches_analytic_error(make_ar1, phi, seed, method):
|
|
271
|
+
"""The blocked SEM of an AR(1) series is the analytic error at the level
|
|
272
|
+
each rule selects, and a long series raises no warning (R2)."""
|
|
273
|
+
n_frames = 2 ** 16
|
|
274
|
+
x = make_ar1(phi, n_frames, seed)
|
|
275
|
+
variance = 1.0 / (1.0 - phi ** 2)
|
|
276
|
+
|
|
277
|
+
with warnings.catch_warnings():
|
|
278
|
+
warnings.simplefilter("error")
|
|
279
|
+
table = block_average(x)
|
|
280
|
+
level = select_plateau(table, method=method)
|
|
281
|
+
estimate = sem_blocked(x, method=method)
|
|
282
|
+
|
|
283
|
+
assert level >= 0
|
|
284
|
+
block_size = table.block_size[level]
|
|
285
|
+
expected = np.sqrt(
|
|
286
|
+
ar1_inefficiency(phi, np.array([block_size]))[0]
|
|
287
|
+
* variance / n_frames
|
|
288
|
+
)
|
|
289
|
+
tolerance = SIGMA * expected * relative_gaussian_error(
|
|
290
|
+
np.array([table.n_blocks[level]])
|
|
291
|
+
)[0] / 2.0 # the SEM is a square root, so half the relative error
|
|
292
|
+
assert estimate == pytest.approx(expected, abs=tolerance)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@pytest.mark.parametrize(
|
|
296
|
+
"period,seed",
|
|
297
|
+
[
|
|
298
|
+
pytest.param(8, 10, id="period-8"),
|
|
299
|
+
pytest.param(32, 11, id="period-32"),
|
|
300
|
+
],
|
|
301
|
+
)
|
|
302
|
+
def test_block_average_block_constant_below_period_is_exact(
|
|
303
|
+
make_block_constant, period, seed
|
|
304
|
+
):
|
|
305
|
+
"""Below the period a block-constant series has an exact inefficiency
|
|
306
|
+
B (n-1)/(n-B) (R3)."""
|
|
307
|
+
n_frames = 2 ** 14
|
|
308
|
+
x = make_block_constant(period, n_frames, seed)
|
|
309
|
+
|
|
310
|
+
table = block_average(x)
|
|
311
|
+
|
|
312
|
+
below = table.block_size <= period
|
|
313
|
+
expected = block_constant_inefficiency(
|
|
314
|
+
period, n_frames, table.block_size[below]
|
|
315
|
+
)
|
|
316
|
+
assert_allclose(
|
|
317
|
+
table.statistical_inefficiency[below], expected, rtol=FLOAT64_RTOL
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@pytest.mark.parametrize(
|
|
322
|
+
"period,seed",
|
|
323
|
+
[
|
|
324
|
+
pytest.param(8, 10, id="period-8"),
|
|
325
|
+
pytest.param(32, 11, id="period-32"),
|
|
326
|
+
],
|
|
327
|
+
)
|
|
328
|
+
def test_block_average_block_constant_plateaus_at_period(
|
|
329
|
+
make_block_constant, period, seed
|
|
330
|
+
):
|
|
331
|
+
"""At and above the period the inefficiency plateaus at L (n-1)/(n-L)
|
|
332
|
+
within statistical error (R3)."""
|
|
333
|
+
n_frames = 2 ** 14
|
|
334
|
+
x = make_block_constant(period, n_frames, seed)
|
|
335
|
+
|
|
336
|
+
table = block_average(x)
|
|
337
|
+
|
|
338
|
+
checked = (table.block_size >= period) & (
|
|
339
|
+
table.n_blocks >= MIN_BLOCKS_FOR_GAUSSIAN
|
|
340
|
+
)
|
|
341
|
+
expected = block_constant_inefficiency(
|
|
342
|
+
period, n_frames, table.block_size[checked]
|
|
343
|
+
)
|
|
344
|
+
sigma = expected * relative_gaussian_error(table.n_blocks[checked])
|
|
345
|
+
pulls = (table.statistical_inefficiency[checked] - expected) / sigma
|
|
346
|
+
assert_allclose(pulls, np.zeros_like(pulls), rtol=0.0, atol=SIGMA)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# --- contracts: shapes, dtypes, independence of observables ------------------
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def test_block_average_treats_trailing_axes_independently(make_ar1):
|
|
353
|
+
"""Each observable of an n-D input gets the table of its own 1-D series."""
|
|
354
|
+
first = make_ar1(0.3, 2 ** 10, 12)
|
|
355
|
+
second = make_ar1(0.8, 2 ** 10, 13)
|
|
356
|
+
stacked = np.column_stack([first, second]).reshape(2 ** 10, 2, 1)
|
|
357
|
+
|
|
358
|
+
table = block_average(stacked)
|
|
359
|
+
|
|
360
|
+
assert_allclose(
|
|
361
|
+
table.sem[:, 0, 0], block_average(first).sem, rtol=FLOAT64_RTOL
|
|
362
|
+
)
|
|
363
|
+
assert_allclose(
|
|
364
|
+
table.statistical_inefficiency[:, 1, 0],
|
|
365
|
+
block_average(second).statistical_inefficiency,
|
|
366
|
+
rtol=FLOAT64_RTOL,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@pytest.mark.parametrize(
|
|
371
|
+
"trailing_shape",
|
|
372
|
+
[
|
|
373
|
+
pytest.param((), id="scalar-series"),
|
|
374
|
+
pytest.param((3,), id="vector-series"),
|
|
375
|
+
pytest.param((2, 3), id="matrix-series"),
|
|
376
|
+
],
|
|
377
|
+
)
|
|
378
|
+
def test_block_average_shapes_follow_the_input(trailing_shape):
|
|
379
|
+
"""Per-level arrays are (n_levels, *trailing); counts are (n_levels,)."""
|
|
380
|
+
n_frames = 256
|
|
381
|
+
x = np.random.default_rng(14).standard_normal((n_frames, *trailing_shape))
|
|
382
|
+
|
|
383
|
+
table = block_average(x)
|
|
384
|
+
|
|
385
|
+
assert table.observable_shape == trailing_shape
|
|
386
|
+
assert table.block_size.shape == (table.n_levels,)
|
|
387
|
+
assert table.n_blocks.shape == (table.n_levels,)
|
|
388
|
+
assert table.mean.shape == (table.n_levels, *trailing_shape)
|
|
389
|
+
assert table.sem.shape == (table.n_levels, *trailing_shape)
|
|
390
|
+
assert table.sem_err.shape == (table.n_levels, *trailing_shape)
|
|
391
|
+
assert table.statistical_inefficiency.shape == (
|
|
392
|
+
table.n_levels, *trailing_shape
|
|
393
|
+
)
|
|
394
|
+
assert table.inefficiency_err.shape == (table.n_levels, *trailing_shape)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def test_select_plateau_returns_int_for_scalar_series():
|
|
398
|
+
"""A scalar series gives a plain int level and float wrapped estimates."""
|
|
399
|
+
x = np.random.default_rng(15).standard_normal(2 ** 12)
|
|
400
|
+
|
|
401
|
+
level = select_plateau(block_average(x), method="lee")
|
|
402
|
+
|
|
403
|
+
assert isinstance(level, int)
|
|
404
|
+
assert isinstance(sem_blocked(x, method="lee"), float)
|
|
405
|
+
assert isinstance(statistical_inefficiency(x, method="lee"), float)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def test_select_plateau_returns_array_per_observable():
|
|
409
|
+
"""An n-D series gives one level and one estimate per observable."""
|
|
410
|
+
x = np.random.default_rng(16).standard_normal((2 ** 12, 2, 3))
|
|
411
|
+
|
|
412
|
+
level = select_plateau(block_average(x), method="lee")
|
|
413
|
+
|
|
414
|
+
assert level.shape == (2, 3)
|
|
415
|
+
assert level.dtype == np.int64
|
|
416
|
+
assert sem_blocked(x, method="lee").shape == (2, 3)
|
|
417
|
+
assert statistical_inefficiency(x, method="lee").shape == (2, 3)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@pytest.mark.parametrize(
|
|
421
|
+
"dtype",
|
|
422
|
+
[
|
|
423
|
+
pytest.param(np.float32, id="float32"),
|
|
424
|
+
pytest.param(np.int64, id="int64"),
|
|
425
|
+
pytest.param(np.float64, id="float64"),
|
|
426
|
+
],
|
|
427
|
+
)
|
|
428
|
+
def test_block_average_output_dtypes(dtype):
|
|
429
|
+
"""Statistics are float64 and counts int64 whatever the input dtype."""
|
|
430
|
+
x = np.random.default_rng(17).integers(0, 100, 512).astype(dtype)
|
|
431
|
+
|
|
432
|
+
table = block_average(x)
|
|
433
|
+
|
|
434
|
+
assert table.block_size.dtype == np.int64
|
|
435
|
+
assert table.n_blocks.dtype == np.int64
|
|
436
|
+
assert table.mean.dtype == np.float64
|
|
437
|
+
assert table.var.dtype == np.float64
|
|
438
|
+
assert table.sem.dtype == np.float64
|
|
439
|
+
assert table.sem_err.dtype == np.float64
|
|
440
|
+
assert table.statistical_inefficiency.dtype == np.float64
|
|
441
|
+
assert table.inefficiency_err.dtype == np.float64
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def test_block_average_leaves_input_unchanged():
|
|
445
|
+
"""The input array is not modified by the in-place blocking buffer."""
|
|
446
|
+
x = np.random.default_rng(18).standard_normal((512, 2))
|
|
447
|
+
before = x.copy()
|
|
448
|
+
|
|
449
|
+
block_average(x)
|
|
450
|
+
|
|
451
|
+
assert_array_equal(x, before)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
# --- the selection rules on hand-built tables --------------------------------
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
@pytest.mark.parametrize(
|
|
458
|
+
"sem_ratios,n_frames,expected_level",
|
|
459
|
+
[
|
|
460
|
+
# n = 1024: a level qualifies when B**3 > 2048 r**4; with r = 1
|
|
461
|
+
# everywhere that is B >= 16, so the run from the top ends at k = 4.
|
|
462
|
+
pytest.param([1, 1, 1, 1, 1, 1, 1, 1], 1024, 4, id="flat-table"),
|
|
463
|
+
# n = 512, r = 0.5 for k <= 3: at k = 2, B**3 = 64 equals
|
|
464
|
+
# 2 * 512 * 0.5**4 = 64, so strict '>' stops the run at k = 3; a
|
|
465
|
+
# non-strict rule would continue to k = 2.
|
|
466
|
+
pytest.param([1, 0.5, 0.5, 0.5, 1, 1, 1], 512, 3, id="strict"),
|
|
467
|
+
# n = 4096: a level qualifies when B**3 > 8192 r**4, so with r = 1
|
|
468
|
+
# that is B >= 32. r = 10 at k = 5 breaks the run: k = 7, 6 qualify,
|
|
469
|
+
# k = 5 does not, so the rule stops at k = 6 although k = 5 would
|
|
470
|
+
# qualify with r = 1 and a smallest-anywhere rule would pick it.
|
|
471
|
+
pytest.param([1, 1, 1, 1, 1, 10, 1, 1], 4096, 6, id="broken-run"),
|
|
472
|
+
],
|
|
473
|
+
)
|
|
474
|
+
def test_select_plateau_lee_scans_down_from_largest_block(
|
|
475
|
+
make_table, sem_ratios, n_frames, expected_level
|
|
476
|
+
):
|
|
477
|
+
"""The Lee rule takes the smallest block of the qualifying run that
|
|
478
|
+
includes the largest block, with a strict inequality."""
|
|
479
|
+
table = make_table(sem_ratios, n_frames)
|
|
480
|
+
|
|
481
|
+
level = select_plateau(table, method="lee")
|
|
482
|
+
|
|
483
|
+
assert level == expected_level
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def test_select_plateau_lee_reports_no_plateau_when_top_level_fails(
|
|
487
|
+
make_table
|
|
488
|
+
):
|
|
489
|
+
"""A top level that fails the Lee inequality gives -1 with a warning."""
|
|
490
|
+
# n = 1024, k = 7: B**3 = 2**21 is not above 2048 * 10**4.
|
|
491
|
+
table = make_table([1, 1, 1, 1, 1, 1, 1, 10], 1024)
|
|
492
|
+
|
|
493
|
+
with pytest.warns(RuntimeWarning, match="No plateau found"):
|
|
494
|
+
level = select_plateau(table, method="lee")
|
|
495
|
+
|
|
496
|
+
assert level == -1
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def test_select_plateau_lee_warns_when_fewer_than_fifty_blocks_remain(
|
|
500
|
+
make_table
|
|
501
|
+
):
|
|
502
|
+
"""Selecting a block size at or above n/50 raises Lee's reliability
|
|
503
|
+
warning while still returning the level."""
|
|
504
|
+
# n = 1024, r = 10 for k <= 4: the run from the top stops at k = 5,
|
|
505
|
+
# B = 32, and 50 * 32 >= 1024.
|
|
506
|
+
table = make_table([1, 10, 10, 10, 10, 1, 1, 1], 1024)
|
|
507
|
+
|
|
508
|
+
with pytest.warns(RuntimeWarning, match="not below n_frames / 50"):
|
|
509
|
+
level = select_plateau(table, method="lee")
|
|
510
|
+
|
|
511
|
+
assert level == 5
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
@pytest.mark.parametrize(
|
|
515
|
+
"sem_values,expected_level",
|
|
516
|
+
[
|
|
517
|
+
# n = 1024, sem_err 0.05 everywhere: levels 1 to 3 are flat at 10,
|
|
518
|
+
# so sem_err picks k = 1, with 512 blocks. The Lee run from the top
|
|
519
|
+
# stops at k = 5, B = 32, as in the test above, and 50 * 32 >= 1024.
|
|
520
|
+
pytest.param(
|
|
521
|
+
[1, 10, 10, 10, 10, 1, 1, 1], 1, id="lee-block-too-large"
|
|
522
|
+
),
|
|
523
|
+
# n = 1024: sem_err picks k = 0, but the top level fails the Lee
|
|
524
|
+
# inequality, B**3 = 2**21 not above 2048 * 10**4, so the Lee rule
|
|
525
|
+
# finds no level at all.
|
|
526
|
+
pytest.param(
|
|
527
|
+
[1, 1, 1, 1, 1, 1, 1, 10], 0, id="lee-level-missing"
|
|
528
|
+
),
|
|
529
|
+
],
|
|
530
|
+
)
|
|
531
|
+
def test_select_plateau_sem_err_warns_when_the_lee_block_is_too_large(
|
|
532
|
+
make_table, sem_values, expected_level
|
|
533
|
+
):
|
|
534
|
+
"""Under sem_err, Lee's reliability warning follows the Lee block size,
|
|
535
|
+
not the selected one, and the selected level is still returned."""
|
|
536
|
+
table = make_table(sem_values, 1024, [0.05] * len(sem_values))
|
|
537
|
+
|
|
538
|
+
with pytest.warns(RuntimeWarning, match="not below n_frames / 50"):
|
|
539
|
+
level = select_plateau(table, method="sem_err")
|
|
540
|
+
|
|
541
|
+
assert level == expected_level
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def test_select_plateau_sem_err_warns_on_a_too_short_ar1_run(make_ar1):
|
|
545
|
+
"""AR(1) with phi = 0.99 over 2**14 frames, about 82 independent
|
|
546
|
+
samples, makes the sem_err rule warn that its plateau is unreliable."""
|
|
547
|
+
# s = 199 by R2, so n / s = 82. Lee's criterion n / B_opt > 50, with
|
|
548
|
+
# B_opt = (2 n s**2)**(1/3), needs n / s > 500.
|
|
549
|
+
table = block_average(make_ar1(0.99, 2 ** 14, 22))
|
|
550
|
+
|
|
551
|
+
with pytest.warns(RuntimeWarning, match="not below n_frames / 50"):
|
|
552
|
+
level = select_plateau(table, method="sem_err")
|
|
553
|
+
|
|
554
|
+
assert level >= 0
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
@pytest.mark.parametrize(
|
|
558
|
+
"sem_values,sem_err_values,expected_level",
|
|
559
|
+
[
|
|
560
|
+
# Level 3 is the first whose SEM equals the next two within 0.05.
|
|
561
|
+
pytest.param(
|
|
562
|
+
[1.0, 1.5, 1.9, 2.0, 2.0, 2.0, 2.0, 2.0],
|
|
563
|
+
[0.05] * 8,
|
|
564
|
+
3,
|
|
565
|
+
id="first-flat-level",
|
|
566
|
+
),
|
|
567
|
+
# Level 2 has a large error bar of its own, but the rule compares
|
|
568
|
+
# against the error bars of levels 3 and 4, which it fails.
|
|
569
|
+
pytest.param(
|
|
570
|
+
[1.0, 1.5, 1.9, 2.0, 2.0, 2.0, 2.0, 2.0],
|
|
571
|
+
[0.05, 0.05, 0.2, 0.05, 0.05, 0.05, 0.05, 0.05],
|
|
572
|
+
3,
|
|
573
|
+
id="next-levels-error-bars",
|
|
574
|
+
),
|
|
575
|
+
],
|
|
576
|
+
)
|
|
577
|
+
def test_select_plateau_sem_err_takes_first_level_within_next_two_errors(
|
|
578
|
+
make_table, sem_values, sem_err_values, expected_level
|
|
579
|
+
):
|
|
580
|
+
"""The sem_err rule picks the first level whose SEM lies within the
|
|
581
|
+
error bar of each of the next two levels."""
|
|
582
|
+
table = make_table(sem_values, 1024, sem_err_values)
|
|
583
|
+
|
|
584
|
+
# At 1024 frames the table's block sizes fail the Lee et al. (2011)
|
|
585
|
+
# Eq. (14) check, which warns; the sem_err rule still picks a level.
|
|
586
|
+
with pytest.warns(RuntimeWarning, match="not below n_frames / 50"):
|
|
587
|
+
level = select_plateau(table, method="sem_err")
|
|
588
|
+
|
|
589
|
+
assert level == expected_level
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
@pytest.mark.parametrize(
|
|
593
|
+
"sem_values",
|
|
594
|
+
[
|
|
595
|
+
pytest.param([1.0, 2.0, 4.0, 8.0, 16.0], id="still-rising"),
|
|
596
|
+
pytest.param([1.0, 1.0], id="two-levels-only"),
|
|
597
|
+
],
|
|
598
|
+
)
|
|
599
|
+
def test_select_plateau_sem_err_reports_no_plateau(make_table, sem_values):
|
|
600
|
+
"""A table that never flattens, or has fewer than three levels, gives
|
|
601
|
+
-1 with a warning."""
|
|
602
|
+
table = make_table(sem_values, 1024, [0.01] * len(sem_values))
|
|
603
|
+
|
|
604
|
+
with pytest.warns(RuntimeWarning, match="No plateau found"):
|
|
605
|
+
level = select_plateau(table, method="sem_err")
|
|
606
|
+
|
|
607
|
+
assert level == -1
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
@pytest.mark.parametrize("method", ["lee", "sem_err"])
|
|
611
|
+
def test_wrappers_return_nan_for_a_drifting_series(method):
|
|
612
|
+
"""A series with a linear drift has no plateau: the wrappers give NaN."""
|
|
613
|
+
x = np.arange(1024, dtype=np.float64)
|
|
614
|
+
|
|
615
|
+
with pytest.warns(RuntimeWarning, match="No plateau found"):
|
|
616
|
+
estimate = sem_blocked(x, method=method)
|
|
617
|
+
|
|
618
|
+
assert np.isnan(estimate)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
@pytest.mark.parametrize("method", ["lee", "sem_err"])
|
|
622
|
+
def test_wrappers_return_the_selected_row_of_the_table(make_ar1, method):
|
|
623
|
+
"""sem_blocked and statistical_inefficiency equal the table entries at
|
|
624
|
+
the level select_plateau chooses."""
|
|
625
|
+
x = make_ar1(0.7, 2 ** 14, 19)
|
|
626
|
+
table = block_average(x)
|
|
627
|
+
level = select_plateau(table, method=method)
|
|
628
|
+
|
|
629
|
+
blocked_sem = sem_blocked(x, method=method)
|
|
630
|
+
inefficiency = statistical_inefficiency(x, method=method)
|
|
631
|
+
|
|
632
|
+
assert blocked_sem == pytest.approx(table.sem[level], rel=FLOAT64_RTOL)
|
|
633
|
+
assert inefficiency == pytest.approx(
|
|
634
|
+
table.statistical_inefficiency[level], rel=FLOAT64_RTOL
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
# --- invalid input -----------------------------------------------------------
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
@pytest.mark.parametrize(
|
|
642
|
+
"x,min_blocks,match",
|
|
643
|
+
[
|
|
644
|
+
pytest.param(3.0, 4, "got a scalar", id="scalar"),
|
|
645
|
+
pytest.param([1.0, np.nan, 2.0, 3.0], 4, "finite", id="nan"),
|
|
646
|
+
pytest.param([1.0, np.inf, 2.0, 3.0], 4, "finite", id="inf"),
|
|
647
|
+
pytest.param([1.0, 2.0, 3.0], 4, "at least min_blocks", id="short"),
|
|
648
|
+
pytest.param([1.0, 2.0, 3.0, 4.0], 1, "at least 2", id="min-blocks"),
|
|
649
|
+
pytest.param(
|
|
650
|
+
np.column_stack([np.arange(8.0), np.ones(8)]),
|
|
651
|
+
4,
|
|
652
|
+
"constant series",
|
|
653
|
+
id="constant-observable",
|
|
654
|
+
),
|
|
655
|
+
],
|
|
656
|
+
)
|
|
657
|
+
def test_block_average_rejects_invalid_input(x, min_blocks, match):
|
|
658
|
+
"""Scalars, non-finite values, too few frames, a min_blocks below 2, and
|
|
659
|
+
a constant observable raise ValueError."""
|
|
660
|
+
with pytest.raises(ValueError, match=match):
|
|
661
|
+
block_average(x, min_blocks=min_blocks)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def test_select_plateau_rejects_unknown_method(make_table):
|
|
665
|
+
"""An unknown method name raises ValueError naming the choices."""
|
|
666
|
+
table = make_table([1, 1, 1, 1, 1], 256)
|
|
667
|
+
|
|
668
|
+
with pytest.raises(ValueError, match="choose 'lee' or 'sem_err'"):
|
|
669
|
+
select_plateau(table, method="bogus")
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def test_sem_blocked_rejects_unknown_method():
|
|
673
|
+
"""The wrappers pass an unknown method name through as ValueError."""
|
|
674
|
+
x = np.random.default_rng(20).standard_normal(256)
|
|
675
|
+
|
|
676
|
+
with pytest.raises(ValueError, match="Unknown plateau method"):
|
|
677
|
+
sem_blocked(x, method="bogus")
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
# Generated series
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
@settings(max_examples=50, deadline=None)
|
|
684
|
+
@given(
|
|
685
|
+
x=arrays(np.float64, st.integers(min_value=8, max_value=512),
|
|
686
|
+
elements=st.floats(min_value=-1.0, max_value=1.0)),
|
|
687
|
+
scale=st.one_of(st.floats(min_value=-10.0, max_value=-0.1),
|
|
688
|
+
st.floats(min_value=0.1, max_value=10.0)),
|
|
689
|
+
offset=st.floats(min_value=-10.0, max_value=10.0),
|
|
690
|
+
)
|
|
691
|
+
# Every level-1 block mean is exactly 0, so the plain variance there is
|
|
692
|
+
# exactly 0 while the mapped one is round-off of about 4e-33.
|
|
693
|
+
@example(
|
|
694
|
+
x=np.array([0.1, -0.1, 0.3, -0.3, 0.7, -0.7, 0.9, -0.9]),
|
|
695
|
+
scale=3.0,
|
|
696
|
+
offset=0.1,
|
|
697
|
+
)
|
|
698
|
+
def test_block_average_is_affine_equivariant_on_generated_series(
|
|
699
|
+
x, scale, offset):
|
|
700
|
+
"""For a x + b, every blocking level keeps its blocks, its mean becomes
|
|
701
|
+
a mean + b, its variance a**2 var, its standard error |a| sem, and its
|
|
702
|
+
inefficiency is unchanged: the linearity of the mean and the variance,
|
|
703
|
+
with no reference value."""
|
|
704
|
+
assume(np.ptp(x) > 0.1)
|
|
705
|
+
|
|
706
|
+
plain = block_average(x)
|
|
707
|
+
mapped = block_average(scale * x + offset)
|
|
708
|
+
|
|
709
|
+
assert_array_equal(mapped.block_size, plain.block_size)
|
|
710
|
+
assert_array_equal(mapped.n_blocks, plain.n_blocks)
|
|
711
|
+
# float64 round-off of a sum of at most 512 values of size about 20,
|
|
712
|
+
# whose spread is at least 0.01 times the offset: 1e-8 holds with room.
|
|
713
|
+
assert_allclose(mapped.mean, scale * plain.mean + offset,
|
|
714
|
+
rtol=1e-8, atol=1e-10)
|
|
715
|
+
# A level whose block means are all equal has a variance of exactly 0 in
|
|
716
|
+
# one series and of round-off in the other, which no rtol accepts. With
|
|
717
|
+
# eps = 2.2e-16 and |values| <= 20, a block mean carries at most one
|
|
718
|
+
# rounding per halving (at most 9 for 512 frames) and two from the map,
|
|
719
|
+
# so the error of each block mean, both series counted, is
|
|
720
|
+
# delta <= 1e-13. Over m blocks the variance then moves by at most
|
|
721
|
+
# 2 sqrt(2 var) delta + 2 delta**2 <= (rtol / 2) var + 4 delta**2 / rtol
|
|
722
|
+
# (AM-GM), with 4 delta**2 / rtol = 4e-18: atol 1e-17 for var.
|
|
723
|
+
# sem = sqrt(var / m) with m >= 4 moves by at most sqrt(1e-17 / 4),
|
|
724
|
+
# about 1.6e-9: atol 2e-9. The inefficiency B var / var_0 has B <= 256
|
|
725
|
+
# and, since the range exceeds 0.1 over at most 512 frames and
|
|
726
|
+
# |scale| >= 0.1, var_0 >= 0.1**2 / (2 * 511) * 0.1**2, about 1e-7, so
|
|
727
|
+
# it moves by at most 256 * 1e-17 / 9.8e-8, about 3e-8: atol 1e-7.
|
|
728
|
+
assert_allclose(mapped.var, scale**2 * plain.var, rtol=1e-8, atol=1e-17)
|
|
729
|
+
assert_allclose(mapped.sem, abs(scale) * plain.sem,
|
|
730
|
+
rtol=1e-8, atol=2e-9)
|
|
731
|
+
assert_allclose(mapped.statistical_inefficiency,
|
|
732
|
+
plain.statistical_inefficiency, rtol=1e-8, atol=1e-7)
|