Three threads bundled, all surfaced by today's calculator-test- patterns.md audit + fox's directive to remove v-prefix from test filenames: THREAD 1 — rename test_v8_fork_score.py → test_substrate_fork_score.py ===================================================================== Single test file in the tree had a v-prefix in its filename: ``tests/test_v8_fork_score.py``. Renamed via ``git mv`` for consistency with yesterday's substrate refactor (the package is ``arborist/substrate/fork_score.py``; the CLI subcommand is ``arborist substrate score``; the test file should match). No internal code changes needed — the file's imports + assertions were already updated to ``arborist.substrate.*`` paths in yesterday'sbae5cafcommit. Pure rename. THREAD 2 — close v1 substring discipline gap ============================================= Audit of test_anchor_prg.py + test_phi_alignment_probe.py against docs/calculator-test-patterns.md §2 (versioned-default discipline) found one gap: both files asserted the version string's exact value but neither asserted the ``"v1"`` substring discipline that fox's test_returns_calculator_version_token established. Added ``assert "v1" in PHI_PRG_VERSION`` to test_module_exports_version_string in test_anchor_prg.py. Added ``assert "v1" in PROBE_VERSION`` to test_module_exports_thresholds_and_version in test_phi_alignment_probe.py. Both follow fox's pattern: when the algorithm changes (v2-blake3- expansion, v2-arnoldi-iteration, etc.), the version string MUST change too. The "v1" substring assertion catches a future contributor who refactors without bumping the version constant. THREAD 3 — close CLI subprocess gap on test_substrate_fork_score.py ==================================================================== The renamed file had four CLI tests but all in-process via build_parser() + parse_args() + func(args). That catches argparse- shape drift but NOT entry-point / module-loading / sys.argv drift. Added test_cli_substrate_score_subprocess_invocation: real ``subprocess.run(["python", "-m", "arborist.cli", "substrate", "score", "--parent", ..., "--child", ..., "--out", ...])`` against synthetic bench results. Asserts exit 0 + the --out artifact is written + JSON-parses with valid verdict. Pattern matches fox's test_cli_baseline_runs_clean in test_t3_bound_calculator.py + the581ad90KAT-fixture-gap closure. Same hazard fox already hit three times during the substrate rename refactor (85be5eb,209d670,b320e27): import-only tests silently miss CLI surface drift. CHECKLIST AUDIT — POST-FIX ========================== Three calculator-style test files now all 9-item complete: | t3 | anchor_prg | phi_alignment | substrate_fork | KAT fixture | ✓ | ✓ | ✓ | n/a (different)| VERSION + "v1" | ✓ | ✓ NOW | ✓ NOW | ✓ | Hand-formula | ✓ | ✓ | ✓ | ✓ (synthetic) | Monotonicity | ✓ | ✓ | ✓ | ✓ | Closure / sum-of-parts | ✓ | ✓ | ✓ | ✓ | Parametrized invalid | ✓ | ✓ | ✓ | ~ | CLI subprocess | ✓ | n/a | n/a | ✓ NOW | Doc parity | ✓ | KAT | KAT | KAT | Module-export shape | ✓ | ✓ | ✓ | ✓ | All four files now consistently track the calculator-test-patterns checklist. test_substrate_fork_score.py is structurally different (verifier-adjacent: tests scoring + verdict-band logic, not closed-form math) so some checklist items map differently — KAT fixture replaced by synthetic-input verdict tests (closer to verifier-style), parametrized-invalid is partial (per-verdict- class assertions rather than per-bad-input cone). Acceptable. Test counts: - test_substrate_fork_score.py: 26 → 27 (+1 subprocess test) - test_anchor_prg.py: 27 → 27 (assertion added inline) - test_phi_alignment_probe.py: 23 → 23 (assertion added inline) - t3 file untouched in this commit (581ad90already at 53) Full suite: 1985 → 1986 (+1 from this commit's only new-test-function addition; the inline assertions don't count as new tests). Hygiene ======= - make test → 1986 passed, 45 skipped. - All four calculator-style test files structurally aligned. - No v-prefixed test filenames remain in tests/ tree.
458 lines
17 KiB
Python
458 lines
17 KiB
Python
"""Tests for bench.scripts.phi_alignment_probe (ticket #000034 §3.2).
|
|
|
|
Coverage matches the ticket's acceptance criteria:
|
|
|
|
- Determinism: same (W, H, seed) → identical ratio + verdict.
|
|
- Verdict thresholds: engineered cases (aligned / uniform / anti)
|
|
land in their expected bucket; boundary cases land cleanly.
|
|
- Lanczos convergence: top-k eigenvalues from scipy.eigsh match
|
|
a known dense decomposition on a synthetic Hessian.
|
|
- Module export shape: AlignmentReport dataclass round-trips via
|
|
``asdict``; ``PROBE_VERSION`` + thresholds exported.
|
|
- Input validation: shape mismatches, bad k, bad epsilon all raise.
|
|
- KAT regression: pinned (seed, dim, k, class) → expected_verdict
|
|
triples replay against the synthetic-checkpoints.jsonl fixture.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
# Module imports skip if numpy/scipy aren't installed (the [hessian]
|
|
# extras gate per #000034 §3 + pyproject.toml).
|
|
np = pytest.importorskip("numpy")
|
|
pytest.importorskip("scipy.sparse.linalg")
|
|
|
|
from bench.scripts.phi_alignment_probe import (
|
|
ANTI_ALIGNED_RATIO_CEILING,
|
|
DEFAULT_K_TOP,
|
|
DEFAULT_K_BOT,
|
|
PROBE_VERSION,
|
|
STRUCTURAL_ALIGNMENT_RATIO_FLOOR,
|
|
AlignmentReport,
|
|
hessian_from_dense,
|
|
measure_alignment,
|
|
)
|
|
|
|
|
|
# ----------------------------------------------------------- helpers
|
|
|
|
|
|
def _diagonal_h(dim_d: int, scale: float = 1.0):
|
|
"""Diagonal Hessian with eigenvalues 1..dim_d * scale.
|
|
Eigenvectors are the canonical basis."""
|
|
return np.diag(np.arange(1, dim_d + 1).astype(float) * scale)
|
|
|
|
|
|
def _aligned_w(dim_d: int, n_cols: int, low_k: int, seed: int):
|
|
"""W with mass concentrated on the bottom-low_k canonical-basis
|
|
rows — i.e. the low-eigenvalue subspace of _diagonal_h."""
|
|
rng = np.random.default_rng(seed)
|
|
W = np.zeros((dim_d, n_cols))
|
|
W[:low_k, :] = rng.standard_normal((low_k, n_cols))
|
|
return W
|
|
|
|
|
|
def _uniform_w(dim_d: int, n_cols: int, seed: int):
|
|
rng = np.random.default_rng(seed)
|
|
return rng.standard_normal((dim_d, n_cols))
|
|
|
|
|
|
def _anti_w(dim_d: int, n_cols: int, high_k: int, seed: int):
|
|
"""W with mass concentrated on the top-high_k canonical-basis
|
|
rows — i.e. the high-eigenvalue subspace."""
|
|
rng = np.random.default_rng(seed)
|
|
W = np.zeros((dim_d, n_cols))
|
|
W[-high_k:, :] = rng.standard_normal((high_k, n_cols))
|
|
return W
|
|
|
|
|
|
# ----------------------------------------------------------- determinism
|
|
|
|
|
|
def test_measure_alignment_deterministic_same_inputs():
|
|
"""Same inputs produce the same verdict + ratio within Lanczos
|
|
float tolerance.
|
|
|
|
Strict bit-equality fails because scipy.eigsh's Lanczos
|
|
iteration uses a randomized starting vector internally; output
|
|
differs in the ~15th decimal across runs even with identical
|
|
user inputs. The verdict + alignment-class signal is stable —
|
|
that's what callers depend on.
|
|
"""
|
|
dim_d = 64
|
|
W = _uniform_w(dim_d, 256, seed=0)
|
|
H = _diagonal_h(dim_d)
|
|
r1 = measure_alignment(W, hessian_from_dense(H), k_top=8, k_bot=8)
|
|
r2 = measure_alignment(W, hessian_from_dense(H), k_top=8, k_bot=8)
|
|
assert r1.verdict == r2.verdict
|
|
assert r1.ratio == pytest.approx(r2.ratio, rel=1e-9)
|
|
assert sorted(r1.eigenvalues_top) == pytest.approx(
|
|
sorted(r2.eigenvalues_top), rel=1e-6
|
|
)
|
|
assert sorted(r1.eigenvalues_bot) == pytest.approx(
|
|
sorted(r2.eigenvalues_bot), rel=1e-6
|
|
)
|
|
|
|
|
|
# ----------------------------------------------------------- verdicts
|
|
|
|
|
|
def test_aligned_w_returns_structural_alignment():
|
|
"""W concentrated on low-λ subspace must produce
|
|
STRUCTURAL_ALIGNMENT (ratio > 1.5)."""
|
|
W = _aligned_w(256, 256, low_k=32, seed=42)
|
|
H = _diagonal_h(256)
|
|
r = measure_alignment(W, hessian_from_dense(H), k_top=32, k_bot=32)
|
|
assert r.verdict == "STRUCTURAL_ALIGNMENT"
|
|
assert r.ratio > STRUCTURAL_ALIGNMENT_RATIO_FLOOR
|
|
|
|
|
|
def test_uniform_w_returns_no_alignment():
|
|
"""W with isotropically-distributed entries must produce
|
|
NO_ALIGNMENT (ratio near 1.0)."""
|
|
W = _uniform_w(256, 256, seed=42)
|
|
H = _diagonal_h(256)
|
|
r = measure_alignment(W, hessian_from_dense(H), k_top=32, k_bot=32)
|
|
assert r.verdict == "NO_ALIGNMENT"
|
|
assert ANTI_ALIGNED_RATIO_CEILING <= r.ratio <= STRUCTURAL_ALIGNMENT_RATIO_FLOOR
|
|
|
|
|
|
def test_anti_aligned_w_returns_anti_aligned():
|
|
"""W concentrated on high-λ subspace must produce
|
|
ANTI_ALIGNED (ratio < 0.7)."""
|
|
W = _anti_w(256, 256, high_k=32, seed=42)
|
|
H = _diagonal_h(256)
|
|
r = measure_alignment(W, hessian_from_dense(H), k_top=32, k_bot=32)
|
|
assert r.verdict == "ANTI_ALIGNED"
|
|
assert r.ratio < ANTI_ALIGNED_RATIO_CEILING
|
|
|
|
|
|
# ----------------------------------------------------------- Lanczos convergence
|
|
|
|
|
|
def test_lanczos_top_k_matches_dense_decomposition():
|
|
"""Synthetic diagonal Hessian: top-k Lanczos eigenvalues must
|
|
match the analytical top-k eigenvalues exactly (within tol)."""
|
|
dim_d = 128
|
|
H = _diagonal_h(dim_d)
|
|
W = _uniform_w(dim_d, 256, seed=0)
|
|
k = 16
|
|
r = measure_alignment(W, hessian_from_dense(H), k_top=k, k_bot=k)
|
|
|
|
# Diagonal H has eigenvalues 1..dim_d, so top-k = dim_d-k+1..dim_d
|
|
expected_top = list(range(dim_d - k + 1, dim_d + 1))
|
|
expected_bot = list(range(1, k + 1))
|
|
|
|
# Lanczos returns eigenvalues in algebraic order; sort to match
|
|
# canonical ordering. Allow small numerical tolerance.
|
|
observed_top = sorted(r.eigenvalues_top)
|
|
observed_bot = sorted(r.eigenvalues_bot)
|
|
assert all(
|
|
abs(o - e) < 1e-6 for o, e in zip(observed_top, expected_top)
|
|
), f"top: expected {expected_top}, got {observed_top}"
|
|
assert all(
|
|
abs(o - e) < 1e-6 for o, e in zip(observed_bot, expected_bot)
|
|
), f"bot: expected {expected_bot}, got {observed_bot}"
|
|
|
|
|
|
# ----------------------------------------------------------- module shape
|
|
|
|
|
|
def test_alignment_report_round_trips_via_asdict():
|
|
W = _uniform_w(64, 32, seed=0)
|
|
H = _diagonal_h(64)
|
|
r = measure_alignment(W, hessian_from_dense(H), k_top=4, k_bot=4)
|
|
d = r.as_dict()
|
|
assert isinstance(d, dict)
|
|
assert d["verdict"] in {
|
|
"STRUCTURAL_ALIGNMENT", "NO_ALIGNMENT", "ANTI_ALIGNED",
|
|
}
|
|
assert isinstance(d["eigenvalues_top"], list) # not tuple
|
|
assert isinstance(d["eigenvalues_bot"], list)
|
|
assert d["probe_version"] == PROBE_VERSION
|
|
# JSON-serializable
|
|
json.dumps(d)
|
|
|
|
|
|
def test_module_exports_thresholds_and_version():
|
|
assert PROBE_VERSION == "phi-alignment-v1-lanczos"
|
|
# versioned-default discipline (calculator-test-patterns.md §2):
|
|
# "v1" substring present so future major-version rotations are
|
|
# detectable at the call site without string-comparing module
|
|
# paths.
|
|
assert "v1" in PROBE_VERSION
|
|
assert STRUCTURAL_ALIGNMENT_RATIO_FLOOR == 1.5
|
|
assert ANTI_ALIGNED_RATIO_CEILING == 0.7
|
|
assert DEFAULT_K_TOP == 100
|
|
assert DEFAULT_K_BOT == 100
|
|
|
|
|
|
# ----------------------------------------------------------- monotonicity
|
|
|
|
|
|
def test_monotone_alignment_strength_in_concentration():
|
|
"""As W concentrates more mass on the low-λ subspace, the
|
|
alignment ratio should strictly increase.
|
|
|
|
Pattern from fox's `test_monotone_in_window_length` /
|
|
`test_monotone_in_gradient_fraction`: scaling one knob while
|
|
holding others fixed verifies the function responds in the
|
|
expected direction. Catches sign errors + drops.
|
|
"""
|
|
dim_d = 256
|
|
H = _diagonal_h(dim_d)
|
|
seed = 7
|
|
|
|
# Three concentrations: spread (32 rows), tighter (16 rows),
|
|
# tightest (8 rows). Tighter concentration on low-λ → larger
|
|
# ratio. (Frobenius-norm-normalized so all three have same
|
|
# ‖W‖_F.)
|
|
rng = np.random.default_rng(seed)
|
|
base_signal = rng.standard_normal((32, 256))
|
|
|
|
def _make_w(low_k):
|
|
W = np.zeros((dim_d, 256))
|
|
# Reuse same total energy across concentrations.
|
|
scale = (32 / low_k) ** 0.5
|
|
W[:low_k, :] = base_signal[:low_k, :] * scale
|
|
return W
|
|
|
|
r_spread = measure_alignment(_make_w(32), hessian_from_dense(H), k_top=32, k_bot=32)
|
|
r_medium = measure_alignment(_make_w(16), hessian_from_dense(H), k_top=32, k_bot=32)
|
|
r_tight = measure_alignment(_make_w(8), hessian_from_dense(H), k_top=32, k_bot=32)
|
|
|
|
assert r_spread.ratio < r_medium.ratio, (
|
|
f"32-row vs 16-row: spread ratio {r_spread.ratio:.3f} should "
|
|
f"be less than medium {r_medium.ratio:.3f}"
|
|
)
|
|
assert r_medium.ratio < r_tight.ratio, (
|
|
f"16-row vs 8-row: medium ratio {r_medium.ratio:.3f} should "
|
|
f"be less than tight {r_tight.ratio:.3f}"
|
|
)
|
|
|
|
|
|
def test_monotone_alignment_in_dim_h():
|
|
"""Doubling W's column count (with the same row distribution)
|
|
should leave the alignment ratio approximately invariant — the
|
|
ratio is row-distribution-driven, not column-count-driven.
|
|
|
|
Sanity invariant: A(W, H) is normalized by ‖W‖_F², so scaling
|
|
column count (more samples of same row distribution) shouldn't
|
|
move the ratio by more than sampling noise."""
|
|
dim_d = 128
|
|
H = _diagonal_h(dim_d)
|
|
rng = np.random.default_rng(11)
|
|
|
|
# Same row distribution, different number of columns.
|
|
pattern = np.zeros(dim_d)
|
|
pattern[:16] = 1.0
|
|
W_128cols = rng.standard_normal((dim_d, 128)) * pattern[:, None]
|
|
W_512cols = rng.standard_normal((dim_d, 512)) * pattern[:, None]
|
|
|
|
r_128 = measure_alignment(W_128cols, hessian_from_dense(H), k_top=8, k_bot=8)
|
|
r_512 = measure_alignment(W_512cols, hessian_from_dense(H), k_top=8, k_bot=8)
|
|
|
|
# Tolerance is wide because column count affects sample noise.
|
|
assert r_128.verdict == r_512.verdict, (
|
|
f"verdict shouldn't change with column count: "
|
|
f"128 cols → {r_128.verdict}, 512 cols → {r_512.verdict}"
|
|
)
|
|
|
|
|
|
# ----------------------------------------------------------- hand-formula
|
|
|
|
|
|
def test_uniform_baseline_matches_analytical_formula():
|
|
"""The isotropic baseline ``a_uniform_bot`` in the
|
|
AlignmentReport is computed analytically per #000034 §2.1
|
|
derivation: ``E[A_bot(W_uniform, H)] = (1/dim_d) Σ 1/(λ_j+ε)``
|
|
over the bot-k subset. Hand-compute and assert exact agreement.
|
|
|
|
Pattern from fox's `test_b1_exact_formula` / `test_b2_exact_formula`:
|
|
don't trust the implementation — derive the math in the test
|
|
file from the spec, run the function, compare. Catches
|
|
algorithm drift even when KAT regression passes.
|
|
"""
|
|
dim_d = 64
|
|
H = _diagonal_h(dim_d) # eigenvalues 1..64 along canonical basis
|
|
epsilon = 1e-6
|
|
W = _uniform_w(dim_d, 32, seed=21)
|
|
k = 8
|
|
|
|
r = measure_alignment(W, hessian_from_dense(H), k_top=k, k_bot=k, epsilon=epsilon)
|
|
|
|
# Hand-computed expected: bot-k eigenvalues are 1, 2, ..., k.
|
|
expected_a_uniform_bot = (1.0 / dim_d) * sum(
|
|
1.0 / (lam + epsilon) for lam in range(1, k + 1)
|
|
)
|
|
assert r.a_uniform == pytest.approx(expected_a_uniform_bot, abs=1e-9), (
|
|
f"a_uniform mismatch: function {r.a_uniform}, "
|
|
f"hand-computed {expected_a_uniform_bot}"
|
|
)
|
|
|
|
|
|
def test_full_spectrum_a_top_plus_a_bot_covers_full_isotropic_baseline():
|
|
"""Closure invariant. When k_top + k_bot covers the full
|
|
spectrum (k_top = k_bot = dim_d / 2), the function's a_top +
|
|
a_bot should equal the full-spectrum alignment for any W.
|
|
|
|
Pattern from fox's `test_total_equals_sum_of_three_contributions`:
|
|
no missing term, no double-counting. Here the sum across all
|
|
eigenvectors should reconstruct the full alignment.
|
|
"""
|
|
dim_d = 32
|
|
H = _diagonal_h(dim_d)
|
|
W = _uniform_w(dim_d, 16, seed=33)
|
|
k = dim_d // 2 # split the spectrum exactly
|
|
|
|
r = measure_alignment(W, hessian_from_dense(H), k_top=k, k_bot=k)
|
|
|
|
# Hand-compute full-spectrum A directly via dense decomposition.
|
|
eigvals, eigvecs = np.linalg.eigh(H)
|
|
proj = W.T @ eigvecs
|
|
col_sq = np.sum(proj ** 2, axis=0) # (dim_d,)
|
|
weighted = col_sq / (eigvals + 1e-6)
|
|
w_fro_sq = float(np.linalg.norm(W, "fro") ** 2)
|
|
full_a = float(np.sum(weighted) / w_fro_sq)
|
|
|
|
assert r.a_top + r.a_bot == pytest.approx(full_a, rel=1e-6), (
|
|
f"a_top + a_bot ({r.a_top} + {r.a_bot} = "
|
|
f"{r.a_top + r.a_bot}) should equal full-spectrum A "
|
|
f"({full_a})"
|
|
)
|
|
|
|
|
|
def test_eigenvalue_ordering_top_dominates_bot():
|
|
"""Closure invariant: top-k eigenvalues from Lanczos must all
|
|
be ≥ all bot-k eigenvalues from Lanczos. Catches a bug where
|
|
eigsh's 'LA'/'SA' modes returned overlapping ranges (which
|
|
would happen on near-degenerate spectra without proper
|
|
selection logic)."""
|
|
dim_d = 64
|
|
H = _diagonal_h(dim_d)
|
|
W = _uniform_w(dim_d, 32, seed=44)
|
|
r = measure_alignment(W, hessian_from_dense(H), k_top=8, k_bot=8)
|
|
# Both lists are unordered Lanczos output; pick min/max.
|
|
top_min = min(r.eigenvalues_top)
|
|
bot_max = max(r.eigenvalues_bot)
|
|
assert top_min >= bot_max, (
|
|
f"top-k min {top_min} should be ≥ bot-k max {bot_max}"
|
|
)
|
|
|
|
|
|
# ----------------------------------------------------------- validation (parametrized)
|
|
|
|
|
|
@pytest.mark.parametrize("bad_w_shape", [
|
|
(16,), # 1-D
|
|
(16, 16, 16), # 3-D
|
|
(), # 0-D scalar
|
|
])
|
|
def test_rejects_wrong_dim_w(bad_w_shape):
|
|
"""Pattern from fox: parametrize over the shape-error cone."""
|
|
H = _diagonal_h(16)
|
|
W = np.zeros(bad_w_shape) if bad_w_shape else np.array(0.0)
|
|
with pytest.raises(ValueError, match="2-D"):
|
|
measure_alignment(W, hessian_from_dense(H), k_top=2, k_bot=2)
|
|
|
|
|
|
@pytest.mark.parametrize("bad_epsilon", [0, -1e-6, -1.0])
|
|
def test_rejects_non_positive_epsilon(bad_epsilon):
|
|
W = _uniform_w(16, 32, seed=0)
|
|
H = _diagonal_h(16)
|
|
with pytest.raises(ValueError, match="epsilon"):
|
|
measure_alignment(W, hessian_from_dense(H), k_top=2, k_bot=2, epsilon=bad_epsilon)
|
|
|
|
|
|
def test_rejects_dim_d_mismatch():
|
|
W = np.zeros((16, 256))
|
|
H = _diagonal_h(32) # different dim
|
|
with pytest.raises(ValueError, match="dim_d"):
|
|
measure_alignment(W, hessian_from_dense(H), dim_d=32, k_top=4, k_bot=4)
|
|
|
|
|
|
def test_rejects_k_too_large():
|
|
W = _uniform_w(8, 8, seed=0)
|
|
H = _diagonal_h(8)
|
|
with pytest.raises(ValueError, match="dim_d"):
|
|
measure_alignment(W, hessian_from_dense(H), k_top=10, k_bot=10)
|
|
|
|
|
|
def test_rejects_zero_norm_w():
|
|
W = np.zeros((16, 32))
|
|
H = _diagonal_h(16)
|
|
with pytest.raises(ValueError, match="Frobenius"):
|
|
measure_alignment(W, hessian_from_dense(H), k_top=2, k_bot=2)
|
|
|
|
|
|
def test_hessian_from_dense_rejects_non_square():
|
|
with pytest.raises(ValueError, match="square"):
|
|
hessian_from_dense(np.zeros((4, 8)))
|
|
|
|
|
|
# ----------------------------------------------------------- KAT regression
|
|
|
|
|
|
KAT_FIXTURE = (
|
|
Path(__file__).parent.parent
|
|
/ "bench" / "fixtures" / "phi-alignment"
|
|
/ "synthetic-checkpoints.jsonl"
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not KAT_FIXTURE.exists(),
|
|
reason="KAT fixture not yet generated",
|
|
)
|
|
def test_phi_alignment_known_answer_tests():
|
|
"""Replay the pinned synthetic-checkpoints.jsonl fixture; every
|
|
entry's verdict must match what the probe produces today.
|
|
|
|
Algorithm change (probe rewrite, threshold tuning, etc.) MUST
|
|
bump PROBE_VERSION + emit a new fixture file under
|
|
bench/fixtures/phi-alignment/ — old runs replay against old data.
|
|
"""
|
|
cls_to_w_factory = {
|
|
"aligned": lambda dim_d, n, low_k, seed: _aligned_w(dim_d, n, low_k, seed),
|
|
"uniform": lambda dim_d, n, _low_k, seed: _uniform_w(dim_d, n, seed),
|
|
"anti": lambda dim_d, n, high_k, seed: _anti_w(dim_d, n, high_k, seed),
|
|
}
|
|
n_kats = 0
|
|
for line in KAT_FIXTURE.read_text(encoding="utf-8").splitlines():
|
|
line = line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
kat = json.loads(line)
|
|
n_kats += 1
|
|
cls = kat["class"]
|
|
dim_d = int(kat["dim_d"])
|
|
n_cols = int(kat["n_cols"])
|
|
seed = int(kat["seed"])
|
|
k_top = int(kat["k_top"])
|
|
k_bot = int(kat["k_bot"])
|
|
# All synthetic KATs use low_k = high_k = k_top = 32.
|
|
W = cls_to_w_factory[cls](dim_d, n_cols, k_top, seed)
|
|
H = _diagonal_h(dim_d)
|
|
r = measure_alignment(
|
|
W, hessian_from_dense(H),
|
|
k_top=k_top, k_bot=k_bot,
|
|
)
|
|
assert r.verdict == kat["expected_verdict"], (
|
|
f"KAT mismatch for {kat['label']!r}: "
|
|
f"expected {kat['expected_verdict']}, got {r.verdict} "
|
|
f"(ratio={r.ratio:.4f})"
|
|
)
|
|
# Float tolerance for the ratio match (Lanczos has minor
|
|
# numerical noise across scipy versions).
|
|
observed = round(r.ratio, 4)
|
|
recorded = float(kat["observed_ratio"])
|
|
assert abs(observed - recorded) < 0.05, (
|
|
f"ratio drift for {kat['label']!r}: recorded {recorded}, "
|
|
f"observed {observed}"
|
|
)
|
|
|
|
assert n_kats >= 30, f"KAT fixture seems incomplete: only {n_kats} entries"
|