amkkk's picture
download
raw
8.3 kB
"""Unit tests for src/sequential_test.py.
Validates primitives against closed-form cases (i.i.d. chain, identical P=Q),
cross-checks the parametric-family information rate against the value printed
by the authors' own G3 notebook (I = 2.0251949227282857 for m=64, seed 123),
and runs Algorithm 1 end-to-end on a tiny instance.
"""
import json
import sys
import numpy as np
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
import sequential_test as st
def _near(a, b, tol=1e-9):
return abs(a - b) <= tol * max(1.0, abs(b))
def test_stationary_dist():
rng = np.random.default_rng(0)
P = rng.random((6, 6))
P = P / P.sum(axis=1, keepdims=True)
pi = st.stationary_dist(P)
assert _near(pi.sum(), 1.0)
assert np.allclose(pi @ P, pi, atol=1e-10), pi @ P - pi
assert np.all(pi > 0)
print(" stationary_dist OK: pi =", np.round(pi, 4))
def test_pseudo_spectral_gap_iid():
# i.i.d. chain: rows all equal to pi => gamma_ps == 1, C_P == 2.
pi = np.array([0.1, 0.2, 0.3, 0.4])
P = np.tile(pi, (4, 1))
g = st.pseudo_spectral_gap(P)
assert _near(g, 1.0, 1e-9), g
C = st.C_constant(P)
assert _near(C, 2.0, 1e-9), C
print(f" iid chain: gamma_ps={g:.6f} C_P={C:.6f} (expected 1.0 / 2.0)")
def test_pseudo_spectral_gap_nontrivial():
rng = np.random.default_rng(7)
P = rng.random((5, 5))
P = P / P.sum(axis=1, keepdims=True)
g = st.pseudo_spectral_gap(P)
assert 0.0 < g < 1.0, g
C = st.C_constant(P)
assert C > 2.0, (g, C) # for gamma_ps < 1, C_P > 2 (mixing correction)
print(f" nontrivial chain: gamma_ps={g:.4f} C_P={C:.4f} (> 2 as expected)")
def test_D_M_zero_for_identical():
rng = np.random.default_rng(1)
Q = rng.random((5, 5)); Q = Q / Q.sum(axis=1, keepdims=True)
dm, f = st.D_M(Q, Q)
assert _near(dm, 0.0, 1e-9), dm
assert np.allclose(f, 0.0, atol=1e-9)
print(" D_M(Q,Q)=0 OK")
def test_poisson_solution_iid_constant():
# i.i.d. Q (rows = pi) with f = constant c => PE rhs = c - c = 0 => omega = 0.
pi = np.array([0.2, 0.3, 0.5])
Q = np.tile(pi, (3, 1))
f = np.array([1.7, 1.7, 1.7])
w = st.poisson_solution(Q, f)
assert np.allclose(w, 0.0, atol=1e-9), w
print(" Poisson solution for iid+constant f = 0 OK")
def test_build_P_theta_stochastic():
rng = np.random.default_rng(123)
m = 5
P0 = rng.random((m, m)); P0 = P0 / P0.sum(axis=1, keepdims=True)
f = np.array([1.0, 1.0, 0.0, -1.0, -1.0])
for theta in [-0.6, -0.4, 0.4, 0.6, 0.8]:
Pt, rho, v = st.build_P_theta(theta, P0, f)
assert np.allclose(Pt.sum(axis=1), 1.0, atol=1e-9), Pt.sum(axis=1)
assert np.all(Pt >= 0)
# cross-check matrices match the authors' build_P_theta to machine precision
import official_reference as off
for theta in [-0.6, -0.4, 0.4, 0.6, 0.8]:
Pt_mine, _, _ = st.build_P_theta(theta, P0, f)
Pt_off, _, _ = off.build_P_theta(theta, P0, f)
assert np.allclose(Pt_mine, Pt_off, atol=1e-10), (theta, np.abs(Pt_mine - Pt_off).max())
print(" build_P_theta row-stochastic + matches official function OK")
def test_D_M_inf_matches_official_functions():
"""Cross-check my build_P_theta / D_M_inf against the authors' own G3
functions (src/official_reference.py, pasted verbatim) on IDENTICAL inputs.
The notebook's printed I=2.0251949227282857 used an unseeded legacy
`np.random.randn(m)` for the feature f, so it is not bit-reproducible.
Instead we verify functional agreement: my functions == official functions
to machine precision on a shared (P0, f, Q).
"""
import official_reference as off
rng = np.random.default_rng(123)
m = 64
P0 = rng.random((m, m)); P0 = P0 / P0.sum(axis=1, keepdims=True)
f = rng.standard_normal(m)
# build_P_theta: compare matrices
for theta in [-0.6, -0.4, 0.4, 0.6, 0.8]:
P_mine, _, _ = st.build_P_theta(theta, P0, f)
P_off, _, _ = off.build_P_theta(theta, P0, f)
assert np.allclose(P_mine, P_off, atol=1e-10), (theta, np.abs(P_mine-P_off).max())
Q, _, _ = st.build_P_theta(-0.6, P0, f)
# D_M: compare per-P values
for theta in [0.4, 0.6, 0.8]:
Pth, _, _ = st.build_P_theta(theta, P0, f)
d_mine, _ = st.D_M(Q, Pth)
d_off = off.D_M_official(Q, Pth)
assert _near(d_mine, d_off, 1e-9), (theta, d_mine, d_off)
# D_M^inf: compare optimized value
D_mine, _, _ = st.make_parametric_null((0.4, 0.8), P0, f)["D_M_inf"](Q)
D_off = off.compute_information_rate(Q, (0.4, 0.8), P0, f, off.build_P_theta)
rel = abs(D_mine - D_off) / abs(D_off)
print(f" D_M^inf mine={D_mine:.10f} official_fn={D_off:.10f} rel_err={rel:.2e}")
# eps (1e-15 vs 1e-12 in KL clipping) + optimizer xatol => agree to ~1e-5
assert rel < 1e-3, rel
# brute-force grid sanity (coarse) — independent of any optimizer
grid = np.linspace(0.4, 0.8, 2001)
pi_Q = st.stationary_dist(Q)
vals = [float(pi_Q @ st.f_P_vector(Q, st.build_P_theta(th, P0, f)[0])) for th in grid]
D_grid = min(vals)
assert abs(D_grid - D_mine) < 5e-3, (D_grid, D_mine)
print(f" brute-force grid min = {D_grid:.6f} (consistent with optimizer)")
def test_algorithm1_null_rarely_stops():
"""Under the null (data from P_theta with theta in Theta_P) the false-reject
rate should be <= alpha (up to MC noise)."""
rng = np.random.default_rng(202)
m = 5
P0 = rng.random((m, m)); P0 = P0 / P0.sum(axis=1, keepdims=True)
f = np.array([1.0, 1.0, 0.0, -1.0, -1.0])
theta_null = 0.6 # in Theta_P = (0.4, 0.8)
P_null, _, _ = st.build_P_theta(theta_null, P0, f)
alpha = 0.05
n_trials = 40
T_max = 2000
stops = 0
for _ in range(n_trials):
gen = st.MarkovGenerator(P_null, rng=np.random.default_rng())
test = st.SequentialMarkovChainTest(m, alpha, (0.4, 0.8), P0, f)
tau, _, _, _ = st.run_trial(test, gen, T_max)
if tau < T_max:
stops += 1
rate = stops / n_trials
print(f" null false-reject rate = {rate:.3f} (alpha={alpha}, {n_trials} trials) "
f"-> {'OK' if rate <= 3*alpha else 'CHECK'}")
# Type-I control with a 3-sigma style slack (sequential test, conservative boundary)
assert rate <= 3 * alpha, rate
def test_algorithm1_stops_under_alternative():
"""Under the alternative (theta_Q = -0.6) the test stops well before T_max."""
rng = np.random.default_rng(321)
m = 5
P0 = rng.random((m, m)); P0 = P0 / P0.sum(axis=1, keepdims=True)
f = np.array([1.0, 1.0, 0.0, -1.0, -1.0])
Q, _, _ = st.build_P_theta(-0.6, P0, f)
alpha = 0.01
T_max = 20000
taus = []
for _ in range(10):
gen = st.MarkovGenerator(Q, rng=np.random.default_rng())
test = st.SequentialMarkovChainTest(m, alpha, (0.4, 0.8), P0, f)
tau, _, _, _ = st.run_trial(test, gen, T_max)
taus.append(tau)
print(f" alt stopping times (m=5,alpha=0.01): mean={np.mean(taus):.1f} "
f"min={min(taus)} max={max(taus)} (T_max={T_max})")
assert np.mean(taus) < T_max, taus
if __name__ == "__main__":
tests = [
("stationary_dist", test_stationary_dist),
("pseudo_spectral_gap_iid", test_pseudo_spectral_gap_iid),
("pseudo_spectral_gap_nontrivial", test_pseudo_spectral_gap_nontrivial),
("D_M_zero_for_identical", test_D_M_zero_for_identical),
("poisson_solution_iid_constant", test_poisson_solution_iid_constant),
("build_P_theta_stochastic", test_build_P_theta_stochastic),
("D_M_inf_matches_official_functions", test_D_M_inf_matches_official_functions),
("algorithm1_null_rarely_stops", test_algorithm1_null_rarely_stops),
("algorithm1_stops_under_alternative", test_algorithm1_stops_under_alternative),
]
print(f"Running {len(tests)} unit tests\n")
failed = 0
for name, fn in tests:
print(f"[{name}]")
try:
fn()
except AssertionError as e:
print(f" FAIL: {e}")
failed += 1
except Exception as e:
print(f" ERROR: {type(e).__name__}: {e}")
failed += 1
print(f"\n{len(tests)-failed}/{len(tests)} passed")
sys.exit(1 if failed else 0)

Xet Storage Details

Size:
8.3 kB
·
Xet hash:
e98d97964d6740ca20c95e4c818eb52e332b3ab4f85e62cb4ade08703943799d

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.