Falsification core (src/neurotwin/falsification.py)#
Shared, lane-neutral harness for the synthetic v-lane falsification benchmarks (v2, v3, and future EM/v2.5). Extracted in Sprint 1B.5 so each lane stops hand-copying the same Outcome type, finite check, gate assembly, and report/write boilerplate.
Lane neutrality is a hard rule. This module imports only stdlib,
numpy,neurotwin.gates, andneurotwin.repro. It must never import a lane (models/dual_field,transition_gym,em,models/ktm). Depending on the core therefore never couples lanes to each other — they each depend on a neutral base.
API#
Outcome(name: str, passed: bool, detail: dict, reason: str = "")#
One diagnostic result. detail holds numeric scores (may nest dicts). reason is a
human-readable failure message (empty when passed). Lanes alias it for local readability
(DiagnosticOutcome = Outcome, V3Outcome = Outcome).
outcomes_finite(outcomes) -> bool#
True iff every numeric value in every outcome detail — including nested dicts/lists — is
finite. Bools and strings are ignored. (Replaces two divergent per-lane walkers; the old v2 one
silently skipped nested dicts.)
assemble_gate(*, branch, dataset, claim_scope, outcomes, required, split_audit_passed=True, baseline_table_present=True, extra_finite=True) -> dict#
Builds the unified evidence gate. The narrow claim is calibrated by the required diagnostics:
calibration_checked = all(required pass), and each failed required outcome becomes a gate
failure reason. finite_metrics = outcomes_finite(outcomes) and extra_finite (pass
extra_finite to fold in finiteness of inputs outside the outcomes, e.g. a baseline leaderboard).
Delegates to neurotwin.gates.evaluate_gate, so the output is the exact dossier schema and the
claim is blocked unless claim_scope is in the narrow allowlist.
build_report(*, schema, branch, claim_scope, seed, config, outcomes, gate, extra=None) -> dict#
Standard report dict: schema/branch/claim_status/claim_scope/seed/config/diagnostics +
falsification_passed/scientific_claim_allowed/failure_reasons/evidence_gate derived from
gate. extra merges lane-specific keys (e.g. v3’s baseline_leaderboard, ranking,
ktm_beats_baselines).
write_report(out_dir, *, report, gate, prefix) -> {"report": Path, "evidence_gate": Path}#
Writes {prefix}_benchmark_report.json + {prefix}_evidence_gate.json.
How a lane wires a falsifier#
from neurotwin.falsification import Outcome, assemble_gate, build_report, write_report
def my_diagnostic(data) -> Outcome:
score = ... # compute on a leakage-safe split
ok = score >= THRESHOLD
return Outcome("my_diagnostic", ok, {"score": float(score)},
"" if ok else f"score {score:.3f} below {THRESHOLD}")
def run_my_benchmark(config, *, seed=None):
outcomes = [my_diagnostic(d) for d in ...]
gate = assemble_gate(
branch="em", dataset="em_stage0", claim_scope="em_artifact_audit_no_human",
outcomes=outcomes, required=[o.name for o in outcomes], # or a gate-critical subset
)
return Result(outcomes=outcomes, gate=gate,
passed=bool(gate["scientific_claim_allowed"]),
failure_reasons=list(gate["failure_reasons"]))
def benchmark_report(result):
return build_report(schema="kahlus.em_stage0.v1", branch="em",
claim_scope="em_artifact_audit_no_human", seed=result.seed,
config=result.config.__dict__, outcomes=result.outcomes, gate=result.gate)
Add the new claim_scope to NARROW_CLAIM_SCOPES in src/neurotwin/gates/unified_gate.py, or
the gate blocks it as “too broad”.
Canonical baselines#
Baseline models live in src/neurotwin/baseline_runner.py (run_baselines, the
_run_single_model dispatch, regression_metrics, retrieval_knn_predict). A leaderboard should
be a single run_baselines(...) call — register new baselines in the dispatch rather than scoring
them inline in a benchmark.
Reference implementations#
v2:
src/neurotwin/models/dual_field/benchmark.py+diagnostics.pyv3:
src/neurotwin/transition_gym/benchmark.py+operator_recovery.pycore tests:
tests/test_falsification.py