anytimemonitor 0.9.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.
- anytimemonitor/__init__.py +21 -0
- anytimemonitor/avci.py +387 -0
- anytimemonitor/controller.py +1604 -0
- anytimemonitor/demo.py +93 -0
- anytimemonitor/ui/__init__.py +6 -0
- anytimemonitor/ui/app.py +1388 -0
- anytimemonitor/ui/plotting.py +299 -0
- anytimemonitor/ui/state.py +552 -0
- anytimemonitor/variance.py +316 -0
- anytimemonitor-0.9.0.dist-info/METADATA +269 -0
- anytimemonitor-0.9.0.dist-info/RECORD +14 -0
- anytimemonitor-0.9.0.dist-info/WHEEL +5 -0
- anytimemonitor-0.9.0.dist-info/licenses/LICENSE +21 -0
- anytimemonitor-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""anytimemonitor — adaptive controller for TTS-MOS listening tests.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
AnytimeMonitor — stateful controller built on an anytime-valid
|
|
5
|
+
confidence sequence (per-item paired diffs).
|
|
6
|
+
Add ratings, query for per-pair verdicts and
|
|
7
|
+
two-lever (+items / +listeners) advice at any
|
|
8
|
+
data-dependent time.
|
|
9
|
+
Decision — dataclass returned by AnytimeMonitor.decision().
|
|
10
|
+
|
|
11
|
+
See README.md for use case and architecture.
|
|
12
|
+
"""
|
|
13
|
+
from .controller import Decision, AnytimeMonitor
|
|
14
|
+
from .variance import VarianceEstimate, estimate_components
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AnytimeMonitor",
|
|
18
|
+
"Decision",
|
|
19
|
+
"VarianceEstimate",
|
|
20
|
+
"estimate_components",
|
|
21
|
+
]
|
anytimemonitor/avci.py
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""AnytimeCI — confidence sequence on the mean of a per-item paired diff stream.
|
|
2
|
+
|
|
3
|
+
V2 per-item revision — supersedes the per-listener version preserved at
|
|
4
|
+
``git tag avci-per-listener-v1.1``. See ``Architecture.md`` for the design
|
|
5
|
+
rationale.
|
|
6
|
+
|
|
7
|
+
The observation is the per-item paired difference
|
|
8
|
+
``d_j = mean_ℓ [Y_{ℓ,A,j} − Y_{ℓ,B,j}]`` over the listeners who rated
|
|
9
|
+
item ``j`` for both systems A and B. Under the §4.5 random-effects
|
|
10
|
+
model, conditional on the listener pool fixed at ``L_pool`` distinct
|
|
11
|
+
listeners, the d_j are i.i.d. with
|
|
12
|
+
|
|
13
|
+
Var(d_j | pool, L_j) = Σ²_int_pair + (Σ²_γαβ + Σ²_eval) / L_j
|
|
14
|
+
|
|
15
|
+
The pool itself contributes a *constant offset* C across all d_j (the
|
|
16
|
+
realised mean of (γα)_ℓA − (γα)_ℓB over the pool). Marginalising over
|
|
17
|
+
the pool adds an irreducible variance term
|
|
18
|
+
|
|
19
|
+
2 σ²_(γα) / L_pool
|
|
20
|
+
|
|
21
|
+
that does not shrink with N_items. To preserve coverage of the
|
|
22
|
+
marginal effect δ = E[Y_A − Y_B] (and not just the pool-conditional
|
|
23
|
+
effect), the AVCI boundary combines the Howard 2021 sub-Gaussian
|
|
24
|
+
half-width and a deterministic floor in quadrature:
|
|
25
|
+
|
|
26
|
+
Ψ_eff(t) = √( Ψ_Howard(σ̂²_t, t)² + plancher² )
|
|
27
|
+
plancher = √( 2 · c · σ̂²_t / L_pool )
|
|
28
|
+
|
|
29
|
+
where ``c`` (``gamma_alpha_factor``) is a multiplier expressing the
|
|
30
|
+
upper bound assumed for σ²_(γα) as a fraction of σ²_int (default 1.0,
|
|
31
|
+
i.e. σ²_(γα) ≤ σ²_int — see ``Architecture.md`` for the prior
|
|
32
|
+
discussion). σ̂²_t (Welford or Hoeffding fallback) is used as a
|
|
33
|
+
self-contained proxy for σ²_int: this slightly inflates the floor
|
|
34
|
+
(σ̂²_d_j also absorbs listener-noise / k_ℓ terms) but keeps the
|
|
35
|
+
boundary auto-contained — no MoM coupling required.
|
|
36
|
+
|
|
37
|
+
The Howard 2021 line-crossing boundary itself is unchanged
|
|
38
|
+
(eq.14 of arXiv 1810.08240): two proxy variance options remain:
|
|
39
|
+
|
|
40
|
+
- ``hoeffding`` : σ²_proxy = ((b−a)/2)² fixed (worst-case bound).
|
|
41
|
+
- ``ebernstein`` : σ²_proxy = σ̂²_t (Welford running estimator), with
|
|
42
|
+
Hoeffding fallback while t < t_min_ebernstein.
|
|
43
|
+
"""
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
from dataclasses import dataclass, field
|
|
47
|
+
from typing import Literal, Union
|
|
48
|
+
|
|
49
|
+
import numpy as np
|
|
50
|
+
from numpy.typing import ArrayLike
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
PROXY = Literal["hoeffding", "ebernstein"]
|
|
54
|
+
VERDICT = Literal["STOP_SEP", "STOP_EQUIV", "CONTINUE"]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def eta_from_n_planned(n_planned: int) -> float:
|
|
58
|
+
"""Howard's a-priori tightness rule: η = 1/√(N_planned).
|
|
59
|
+
|
|
60
|
+
``N_planned`` is the PLANNED item budget; the boundary is tightest near
|
|
61
|
+
t ≈ 1/η² = N_planned items. η is fixed a priori from the plan and is
|
|
62
|
+
never tuned to the observed data (it sets efficiency, not validity).
|
|
63
|
+
Single source of truth for this formula — used by the controller and
|
|
64
|
+
the UI so neither re-implements it.
|
|
65
|
+
"""
|
|
66
|
+
if n_planned < 1:
|
|
67
|
+
raise ValueError("n_planned must be >= 1")
|
|
68
|
+
return 1.0 / float(np.sqrt(n_planned))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class AnytimeCI:
|
|
73
|
+
"""Sub-Gaussian anytime-valid confidence sequence on E[X].
|
|
74
|
+
|
|
75
|
+
Streaming API: feed per-item paired differences through ``update``,
|
|
76
|
+
query the current confidence interval and verdict at any time.
|
|
77
|
+
|
|
78
|
+
The boundary is widened by a deterministic floor accounting for the
|
|
79
|
+
listener-pool contribution that does not shrink with t. See module
|
|
80
|
+
docstring for the model rationale.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
alpha: float = 0.05
|
|
84
|
+
proxy: PROXY = "ebernstein"
|
|
85
|
+
# Range of the *observation* d_j (per-item paired diff). For MOS
|
|
86
|
+
# ∈ [1, 5] this is (-4, 4). Override for other rating scales.
|
|
87
|
+
d_bounds: tuple[float, float] = (-4.0, 4.0)
|
|
88
|
+
# Howard 2021 line-crossing shape parameter. Boundary is tightest near
|
|
89
|
+
# t ≈ 1/η²; set it a priori from the PLANNED item budget as
|
|
90
|
+
# η = 1/√(N_planned) (never tune it to the data — efficiency, not
|
|
91
|
+
# validity). Default 0.07 ≈ 1/√200 from the §7 survey median budget
|
|
92
|
+
# (~400 ratings/system at K=2 ⇒ N_planned ≈ 200); PROVISIONAL until
|
|
93
|
+
# that survey table is validated.
|
|
94
|
+
eta: float = 0.07
|
|
95
|
+
# Below this t, ``ebernstein`` proxy falls back to Hoeffding to avoid
|
|
96
|
+
# exploding bounds when σ̂² is undefined / very noisy.
|
|
97
|
+
t_min_ebernstein: int = 10
|
|
98
|
+
# Relative floor on the empirical variance estimate, expressed as a
|
|
99
|
+
# fraction of the Hoeffding proxy ((b-a)/2)². Prevents anti-conservative
|
|
100
|
+
# bounds when the running σ̂² collapses on near-constant streams.
|
|
101
|
+
ebernstein_floor_frac: float = 0.01
|
|
102
|
+
# Listener-pool size — the number of distinct listeners contributing
|
|
103
|
+
# to the d_j stream. Used in the marginal-coverage floor only ; the
|
|
104
|
+
# Howard 2021 boundary still drives the bulk of Ψ at small/medium t.
|
|
105
|
+
# Set to ``None`` for the rare regime where the listener pool is
|
|
106
|
+
# already effectively infinite (e.g. fresh-listener-per-item
|
|
107
|
+
# crowdsourcing) — the floor then collapses to zero.
|
|
108
|
+
L_pool: int | None = None
|
|
109
|
+
# Upper-bound multiplier for σ²_(γα) as a fraction of σ²_int.
|
|
110
|
+
# Default 1.0: assume σ²_(γα) ≤ σ²_int (cf. Architecture.md ;
|
|
111
|
+
# 1.0 is the conservative baseline, 2.0 the worst-case Blizzard
|
|
112
|
+
# envelope). This is the fallback upper bound used ONLY when no
|
|
113
|
+
# estimate is supplied; the controller's own cold-start instead plugs
|
|
114
|
+
# in a large BTTF-anchored prior (≈2.7·σ²_int) — see
|
|
115
|
+
# `controller.cold_start_ga_ratio`.
|
|
116
|
+
# **Only used when `sigma2_ga_estimate` is None** (no data-driven
|
|
117
|
+
# estimate available, e.g. warm-up).
|
|
118
|
+
gamma_alpha_factor: float = 1.0
|
|
119
|
+
# Optional data-driven estimate of σ²_(γα). When set (typically by
|
|
120
|
+
# the controller from the per-(listener, item) d_ℓj matrix via
|
|
121
|
+
# MS_across − MS_within/k_ℓ_mean), the plancher uses it directly:
|
|
122
|
+
# plancher = √(2·σ̂²_(γα)/L_pool). Avoids the surcoût conservatif of
|
|
123
|
+
# c·σ̂²_proxy on homogeneous palettes (where σ²_(γα) ≪ σ²_int).
|
|
124
|
+
sigma2_ga_estimate: Optional[float] = None
|
|
125
|
+
# Optional override on the ITEM-side (iid) standard error of the
|
|
126
|
+
# mean. When set, half_width() uses
|
|
127
|
+
# ``Ψ_eff = √((se_iid_override · multiplier)² + plancher²)``
|
|
128
|
+
# bypassing the Welford σ̂²_proxy → Ψ_Howard path. The multiplier
|
|
129
|
+
# is the AVCI peeking-penalty factor (cf. _avci_multiplier).
|
|
130
|
+
# Typically set by the controller from a bootstrap-by-items
|
|
131
|
+
# resample, which estimates Var(δ̂ | pool) — the iid component
|
|
132
|
+
# that DOES decay with t and IS subject to peeking penalty.
|
|
133
|
+
se_iid_override: Optional[float] = None
|
|
134
|
+
# Optional override on the listener-pool plancher (the constant-
|
|
135
|
+
# in-N component of the SE). When set, half_width() uses
|
|
136
|
+
# ``plancher_override`` instead of computing it from
|
|
137
|
+
# ``sigma2_ga_estimate`` (or the conservative fallback). Typically
|
|
138
|
+
# set by the controller from a bootstrap-by-listeners resample,
|
|
139
|
+
# which estimates Var(C(pool)) directly and bypasses the MoM
|
|
140
|
+
# σ̂²_(γα) estimation. NOT subject to the peeking penalty (it is
|
|
141
|
+
# a constant-in-t variance component).
|
|
142
|
+
plancher_override: Optional[float] = None
|
|
143
|
+
|
|
144
|
+
_n: int = field(default=0, init=False, repr=False)
|
|
145
|
+
_mean: float = field(default=0.0, init=False, repr=False)
|
|
146
|
+
_M2: float = field(default=0.0, init=False, repr=False)
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def n(self) -> int:
|
|
150
|
+
return self._n
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def mean(self) -> float:
|
|
154
|
+
return self._mean
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def variance(self) -> float:
|
|
158
|
+
"""Welford sample variance (unbiased, divisor n−1). 0 if n < 2."""
|
|
159
|
+
if self._n < 2:
|
|
160
|
+
return 0.0
|
|
161
|
+
return self._M2 / (self._n - 1)
|
|
162
|
+
|
|
163
|
+
def update(self, new_obs: Union[ArrayLike, float]) -> None:
|
|
164
|
+
"""Ingest one or more per-item paired diffs (Welford streaming update)."""
|
|
165
|
+
arr = np.atleast_1d(np.asarray(new_obs, dtype=float)).ravel()
|
|
166
|
+
for x in arr:
|
|
167
|
+
self._n += 1
|
|
168
|
+
delta = x - self._mean
|
|
169
|
+
self._mean += delta / self._n
|
|
170
|
+
delta2 = x - self._mean
|
|
171
|
+
self._M2 += delta * delta2
|
|
172
|
+
|
|
173
|
+
def reset(self) -> None:
|
|
174
|
+
self._n = 0
|
|
175
|
+
self._mean = 0.0
|
|
176
|
+
self._M2 = 0.0
|
|
177
|
+
|
|
178
|
+
def _sigma2_proxy(self) -> float:
|
|
179
|
+
a, b = self.d_bounds
|
|
180
|
+
sigma2_hoeffding = ((b - a) / 2.0) ** 2
|
|
181
|
+
if self.proxy == "hoeffding":
|
|
182
|
+
return sigma2_hoeffding
|
|
183
|
+
if self.proxy == "ebernstein":
|
|
184
|
+
if self._n < self.t_min_ebernstein:
|
|
185
|
+
return sigma2_hoeffding
|
|
186
|
+
floor = self.ebernstein_floor_frac * sigma2_hoeffding
|
|
187
|
+
return max(self.variance, floor, 1e-12)
|
|
188
|
+
raise ValueError(f"unknown proxy: {self.proxy!r}")
|
|
189
|
+
|
|
190
|
+
def _psi_howard(self, sigma2: float) -> float:
|
|
191
|
+
"""Howard et al. 2021 line-crossing half-width.
|
|
192
|
+
|
|
193
|
+
Ψ = σ · √( 2 (t·η² + 1) / (t²·η²) · log(√(t·η² + 1) / α) )
|
|
194
|
+
|
|
195
|
+
Pulled out as a separate method so :meth:`half_width` can compose
|
|
196
|
+
it with the listener-pool floor in quadrature.
|
|
197
|
+
"""
|
|
198
|
+
if self._n < 1:
|
|
199
|
+
return float("inf")
|
|
200
|
+
t = self._n
|
|
201
|
+
eta2_t = (self.eta ** 2) * t
|
|
202
|
+
log_term = float(np.log(np.sqrt(eta2_t + 1.0) / self.alpha))
|
|
203
|
+
if log_term <= 0:
|
|
204
|
+
return float("inf")
|
|
205
|
+
rad = 2.0 * (eta2_t + 1.0) / (t * eta2_t) * log_term
|
|
206
|
+
return float(np.sqrt(sigma2) * np.sqrt(rad))
|
|
207
|
+
|
|
208
|
+
def plancher(self) -> float:
|
|
209
|
+
"""Listener-pool floor √(2·σ²_(γα)/L_pool).
|
|
210
|
+
|
|
211
|
+
Two paths:
|
|
212
|
+
- If ``sigma2_ga_estimate`` is set (data-driven, from the
|
|
213
|
+
controller's MS_across−MS_within decomposition), use it
|
|
214
|
+
directly: plancher = √(2·σ̂²_(γα)/L_pool).
|
|
215
|
+
- Otherwise fall back to the conservative upper bound
|
|
216
|
+
``c · σ̂²_proxy`` for σ²_(γα): plancher =
|
|
217
|
+
√(2·c·σ̂²_proxy/L_pool). This is the original V2 design;
|
|
218
|
+
used during warm-up or when the per-listener decomposition
|
|
219
|
+
isn't reliable.
|
|
220
|
+
|
|
221
|
+
Returns 0.0 when ``L_pool`` is None (assumed infinite pool).
|
|
222
|
+
"""
|
|
223
|
+
if self.L_pool is None or self.L_pool <= 0:
|
|
224
|
+
return 0.0
|
|
225
|
+
if self.sigma2_ga_estimate is not None:
|
|
226
|
+
sigma2_ga = max(0.0, float(self.sigma2_ga_estimate))
|
|
227
|
+
else:
|
|
228
|
+
sigma2_ga = self.gamma_alpha_factor * self._sigma2_proxy()
|
|
229
|
+
return float(np.sqrt(2.0 * sigma2_ga / self.L_pool))
|
|
230
|
+
|
|
231
|
+
def _avci_multiplier(self) -> float:
|
|
232
|
+
"""AVCI peeking-penalty multiplier such that
|
|
233
|
+
``Ψ_Howard(σ², t) = (σ/√t) · _avci_multiplier(t)``.
|
|
234
|
+
|
|
235
|
+
Used by the ``se_override`` path: given an SE that already
|
|
236
|
+
captures the variance structure (bootstrap, lmer, etc.), the
|
|
237
|
+
AVCI band is ``Ψ = SE · _avci_multiplier(t)``. Asymptotically
|
|
238
|
+
~ √(2·log(log t / α)) (loi du logarithme itéré).
|
|
239
|
+
"""
|
|
240
|
+
if self._n < 1:
|
|
241
|
+
return float("inf")
|
|
242
|
+
t = self._n
|
|
243
|
+
eta2_t = (self.eta ** 2) * t
|
|
244
|
+
log_term = float(np.log(np.sqrt(eta2_t + 1.0) / self.alpha))
|
|
245
|
+
if log_term <= 0:
|
|
246
|
+
return float("inf")
|
|
247
|
+
return float(np.sqrt(2.0 * (eta2_t + 1.0) / eta2_t * log_term))
|
|
248
|
+
|
|
249
|
+
def half_width(self) -> float:
|
|
250
|
+
"""Effective half-width.
|
|
251
|
+
|
|
252
|
+
Ψ_eff = √( Ψ_iid² + plancher² )
|
|
253
|
+
|
|
254
|
+
where the iid term gets the AVCI peeking-penalty multiplier and
|
|
255
|
+
the plancher does not (it is constant in t). Each component can
|
|
256
|
+
come from either MoM-Welford or a bootstrap override:
|
|
257
|
+
|
|
258
|
+
- ``se_iid_override`` set ⇒ ``Ψ_iid = se_iid · multiplier(t)``.
|
|
259
|
+
Otherwise ``Ψ_iid = Ψ_Howard(σ̂²_proxy_Welford, t)``.
|
|
260
|
+
- ``plancher_override`` set ⇒ ``plancher = plancher_override``.
|
|
261
|
+
Otherwise ``plancher = plancher()`` (MoM σ̂²_(γα) or fallback).
|
|
262
|
+
"""
|
|
263
|
+
# iid component
|
|
264
|
+
if self.se_iid_override is not None:
|
|
265
|
+
multiplier = self._avci_multiplier()
|
|
266
|
+
if not np.isfinite(multiplier):
|
|
267
|
+
return multiplier
|
|
268
|
+
psi_iid = float(self.se_iid_override) * multiplier
|
|
269
|
+
else:
|
|
270
|
+
sigma2_proxy = self._sigma2_proxy()
|
|
271
|
+
psi_iid = self._psi_howard(sigma2_proxy)
|
|
272
|
+
if not np.isfinite(psi_iid):
|
|
273
|
+
return psi_iid
|
|
274
|
+
|
|
275
|
+
# plancher component
|
|
276
|
+
if self.plancher_override is not None:
|
|
277
|
+
plancher = float(self.plancher_override)
|
|
278
|
+
else:
|
|
279
|
+
plancher = self.plancher()
|
|
280
|
+
|
|
281
|
+
return float(np.sqrt(psi_iid * psi_iid + plancher * plancher))
|
|
282
|
+
|
|
283
|
+
def interval(self) -> tuple[float, float]:
|
|
284
|
+
psi = self.half_width()
|
|
285
|
+
return (self._mean - psi, self._mean + psi)
|
|
286
|
+
|
|
287
|
+
def verdict(self, delta_sep: float = 0.0,
|
|
288
|
+
delta_equiv: float | None = None,
|
|
289
|
+
effect_size_guard: bool = False) -> VERDICT:
|
|
290
|
+
"""STOP / CONTINUE verdict on the AVCI confidence sequence.
|
|
291
|
+
|
|
292
|
+
Parameters
|
|
293
|
+
----------
|
|
294
|
+
delta_sep : float, default 0.0
|
|
295
|
+
Used as the default value of ``delta_equiv`` when the latter
|
|
296
|
+
is None (single-threshold TOST behaviour). It is **NOT** a
|
|
297
|
+
STOP_SEP gate by default — that would be a minimum-effect
|
|
298
|
+
(TOST-sup) test, stricter than the classical anytime-valid
|
|
299
|
+
rejection. Use ``effect_size_guard=True`` to activate the
|
|
300
|
+
guard explicitly.
|
|
301
|
+
delta_equiv : float or None, default None
|
|
302
|
+
Equivalence margin for STOP_EQUIV. If None, falls back to
|
|
303
|
+
``delta_sep`` (single-threshold default).
|
|
304
|
+
effect_size_guard : bool, default False
|
|
305
|
+
If True, STOP_SEP additionally requires
|
|
306
|
+
``|mean| − Ψ_eff ≥ delta_sep`` (TOST-sup). Default off —
|
|
307
|
+
matches the classical anytime-valid rejection semantics
|
|
308
|
+
(0 ∉ CI_t).
|
|
309
|
+
|
|
310
|
+
Verdicts:
|
|
311
|
+
|
|
312
|
+
- STOP_SEP : 0 ∉ CI_t. With ``effect_size_guard=True``, also
|
|
313
|
+
requires ``|mean| − Ψ_eff ≥ delta_sep``.
|
|
314
|
+
- STOP_EQUIV : CI ⊂ (−delta_equiv, +delta_equiv) AND
|
|
315
|
+
Ψ_eff ≤ delta_equiv/2
|
|
316
|
+
(pair indistinguishable within tolerance)
|
|
317
|
+
- CONTINUE : otherwise.
|
|
318
|
+
"""
|
|
319
|
+
if delta_equiv is None:
|
|
320
|
+
delta_equiv = delta_sep
|
|
321
|
+
if self._n < 1:
|
|
322
|
+
return "CONTINUE"
|
|
323
|
+
psi = self.half_width()
|
|
324
|
+
lo, hi = self._mean - psi, self._mean + psi
|
|
325
|
+
# STOP_SEP: classical AVCI rejection — 0 ∉ CI_t.
|
|
326
|
+
# The effect-size guard is opt-in (TOST-sup semantics).
|
|
327
|
+
if lo > 0 or hi < 0:
|
|
328
|
+
if not effect_size_guard or abs(self._mean) - psi >= delta_sep:
|
|
329
|
+
return "STOP_SEP"
|
|
330
|
+
if (-delta_equiv < lo and hi < delta_equiv
|
|
331
|
+
and psi <= delta_equiv / 2.0):
|
|
332
|
+
return "STOP_EQUIV"
|
|
333
|
+
return "CONTINUE"
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
if __name__ == "__main__":
|
|
337
|
+
rng = np.random.default_rng(42)
|
|
338
|
+
|
|
339
|
+
print("Sanity check — per-item AVCI on simulated d_j streams\n")
|
|
340
|
+
|
|
341
|
+
# H1: true δ = 0.3, σ²_d_j = 1 (MOS-ish scale, mid-effect).
|
|
342
|
+
# No pool floor (L_pool=None) — recovers the pure Howard 2021 boundary.
|
|
343
|
+
print("[H1, no floor] δ_true = 0.30, σ²_d = 1.00, MDE_threshold = 0.20, α = 0.05")
|
|
344
|
+
print(f"{'t':>5} {'mean':>8} {'Ψ':>8} {'CI_lo':>8} {'CI_hi':>8} {'verdict':>10}")
|
|
345
|
+
ci = AnytimeCI(alpha=0.05, proxy="ebernstein", eta=0.1, L_pool=None)
|
|
346
|
+
delta_true, sigma_true, mde_threshold = 0.30, 1.0, 0.20
|
|
347
|
+
for t in range(1, 301):
|
|
348
|
+
ci.update(rng.normal(delta_true, sigma_true))
|
|
349
|
+
if t in (1, 5, 10, 20, 50, 100, 200, 300):
|
|
350
|
+
psi = ci.half_width()
|
|
351
|
+
lo, hi = ci.interval()
|
|
352
|
+
v = ci.verdict(mde_threshold)
|
|
353
|
+
print(f"{t:>5d} {ci.mean:>8.3f} {psi:>8.3f} {lo:>8.3f} {hi:>8.3f} {v:>10}")
|
|
354
|
+
|
|
355
|
+
# H1: same setup but with L_pool=20 — floor should plateau Ψ at √(2·1·σ̂²/20)
|
|
356
|
+
print("\n[H1, L_pool=20, c=1.0] same setup but with listener-pool floor")
|
|
357
|
+
print(f"{'t':>5} {'mean':>8} {'Ψ':>8} {'plancher':>10} {'verdict':>10}")
|
|
358
|
+
ci = AnytimeCI(alpha=0.05, proxy="ebernstein", eta=0.1,
|
|
359
|
+
L_pool=20, gamma_alpha_factor=1.0)
|
|
360
|
+
for t in range(1, 1001):
|
|
361
|
+
ci.update(rng.normal(delta_true, sigma_true))
|
|
362
|
+
if t in (1, 10, 50, 100, 300, 500, 1000):
|
|
363
|
+
psi = ci.half_width()
|
|
364
|
+
plancher = ci.plancher()
|
|
365
|
+
v = ci.verdict(mde_threshold)
|
|
366
|
+
print(f"{t:>5d} {ci.mean:>8.3f} {psi:>8.3f} "
|
|
367
|
+
f"{plancher:>10.3f} {v:>10}")
|
|
368
|
+
|
|
369
|
+
# H0 coverage check
|
|
370
|
+
print("\n[H0 coverage, no floor] δ_true = 0, R = 200 replicates, T = 200")
|
|
371
|
+
print(" Verify: AVCI brackets 0 at every t in ≥ 95 % of replicates.")
|
|
372
|
+
R, T = 200, 200
|
|
373
|
+
n_falses = 0
|
|
374
|
+
for r in range(R):
|
|
375
|
+
ci = AnytimeCI(alpha=0.05, proxy="ebernstein", eta=0.1, L_pool=None)
|
|
376
|
+
crossed = False
|
|
377
|
+
for _ in range(T):
|
|
378
|
+
ci.update(rng.normal(0.0, 1.0))
|
|
379
|
+
lo, hi = ci.interval()
|
|
380
|
+
if lo > 0 or hi < 0:
|
|
381
|
+
crossed = True
|
|
382
|
+
break
|
|
383
|
+
if crossed:
|
|
384
|
+
n_falses += 1
|
|
385
|
+
coverage = 1.0 - n_falses / R
|
|
386
|
+
print(f" Empirical coverage = {coverage:.3f} "
|
|
387
|
+
f"(target ≥ 0.95) — {n_falses} crossings out of {R}")
|