File size: 56,137 Bytes
63f41a6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 | #!/usr/bin/env python
"""P2 evaluation report: pass@k, Wilson / bootstrap CIs, paired McNemar, correction lift.
`--config sota` runs the separately registered SOTA analysis instead (docs/PREREG_SOTA.md): co-primary class A and
class B on miniF2F-test, unbiased pass@32 from n = 64, paired sign-flip test, Holm over the two, secondaries, exploratory
outputs, protocol-tuple tags, and the rewritten-statement count. Every report that touches a test split is appended to
an append-only, hash-chained look log. The P2 primary below is untouched.
.venv/bin/python scripts/p2/eval_report.py \\
--problems minif2f.jsonl mobench.jsonl \\
--run base:gens_base.jsonl:ver_base.jsonl --run sft:gens_sft.jsonl:ver_sft.jsonl \\
--baseline base --candidate sft --out-dir REPORT_DIR
`--run NAME:GEN[,GEN..]:VER[,VER..]`. Writes `report.json` and `report.md`. Every table row names
its problem set, its token cap and the verifier budget; nothing is pooled across caps.
Pre-registered endpoints (docs/P2.md, written before any data): PRIMARY = miniF2F solved@8 at cap
16384, baseline vs post-SFT, paired exact McNemar, alpha = 0.05. Everything else in the report is
labelled SECONDARY and is NOT multiplicity-corrected.
What this script refuses to do (each is a number that would be about a different experiment):
* score a run with a missing verification, a verification with no generation, a stale verdict
(`code_sha256` mismatch), or a problem with a different number of round-0 samples than the
rest (unless --allow-ragged-n): a missing sample is not a failure of the model;
* report pass@k for k > n;
* compare two runs whose verifier pins / config / budget differ (--allow-verifier-mismatch
to override; the mismatch is then printed in the report header);
* compare two runs that do not cover exactly the same problems;
* compare two runs generated with different sampling settings or prompted differently (--allow-design-mismatch);
* label a comparison PRIMARY unless it is the registered test: miniF2F at cap 16384, k = 8, ALL 244 problems, baseline = the
registered model revision, temperature 1.0 / top_p 0.95 / no top_k (anything else is printed as secondary, with the reason).
"""
from __future__ import annotations
import argparse
import getpass
import hashlib
import importlib.util
import json
import os
import re
import socket
import sys
from collections import Counter, defaultdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
REPO = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(REPO))
def _load_common():
spec = importlib.util.spec_from_file_location("p2_common", Path(__file__).with_name("p2_common.py"))
mod = importlib.util.module_from_spec(spec)
sys.modules["p2_common"] = mod
spec.loader.exec_module(mod)
return mod
C = sys.modules.get("p2_common") or _load_common()
from align.eval import paired as PD # noqa: E402
from align.eval import proof_stats as S # noqa: E402
#: What docs/P2.md section 1 registers. `n_problems`, `baseline_model_rev` and the sampling settings are part of the
#: registration: a comparison that differs in any of them is a DIFFERENT experiment and must not carry the PRIMARY label
#: (a 64-problem subset run labelled PRIMARY would print a confirmatory-looking p-value for a test nobody registered).
PRIMARY = {"source": "minif2f", "cap": 16384, "k": 8, "n_problems": 244,
"baseline_model_rev": "dfd02e6271a58375dfbf3ece0175277cf6b6a89a",
"sampling": {"temperature": 1.0, "top_p": 0.95, "top_k": [0, -1]}}
DEFAULT_KS = (1, 2, 4, 8, 16, 32)
DOC = REPO / "docs" / "P2.md"
_PREREG_BLOCK = re.compile(r"<!-- PREREG:BEGIN -->\n(.*?)<!-- PREREG:END -->", re.S)
_PREREG_RECORDED = re.compile(r"PREREG_SHA256:\s*`([0-9a-f]{64})`")
def prereg_status(doc: Path = DOC) -> dict:
"""Recompute the hash of the pre-registered block of docs/P2.md and compare it with the
value recorded next to it. A report whose analysis plan was edited after registration says so
in its header instead of silently presenting the new plan as the registered one."""
if not Path(doc).exists():
return {"status": "missing", "doc": str(doc)}
text = Path(doc).read_text()
m = _PREREG_BLOCK.search(text)
rec = _PREREG_RECORDED.search(text)
if not m:
return {"status": "no_block", "doc": str(doc)}
h = hashlib.sha256(m.group(1).encode()).hexdigest()
if not rec:
return {"status": "unrecorded", "computed": h}
return {"status": "verified" if rec.group(1) == h else "MODIFIED_AFTER_REGISTRATION", "recorded": rec.group(1), "computed": h}
class Sample:
__slots__ = ("accepted", "reasons", "finish", "n_tok", "parent", "budget", "verify_s", "eff_max", "sampling", "model_rev", "prompt_sha",
"fidelity")
def __init__(self, accepted, reasons, finish, n_tok, parent, budget, verify_s, eff_max, sampling=None, model_rev=None, prompt_sha=None,
fidelity=None):
self.accepted, self.reasons, self.finish = accepted, reasons, finish
self.n_tok, self.parent, self.budget, self.verify_s = n_tok, parent, budget, verify_s
self.eff_max = eff_max # tokens the sampler was really allowed (== cap unless the context window clipped it)
self.sampling = sampling # (temperature, top_p, top_k) as recorded; None = not recorded
self.model_rev, self.prompt_sha = model_rev, prompt_sha
#: SOTA configuration only: for an ACCEPTED sample, "faithful" | "rewritten" | "unreadable" (the displayed theorem
#: compared with the reference statement); None = not checked (rejected sample, or no statements were supplied).
self.fidelity = fidelity
def _sampling_of(rec: dict) -> tuple:
"""(temperature, top_p, top_k) exactly as recorded; a missing key stays None (unknown is not 'default')."""
sp = rec.get("sampling") or {}
return (sp.get("temperature"), sp.get("top_p"), sp.get("top_k"))
def verifier_signature(v: dict) -> str:
cfg = dict(v.get("config", {}))
return hashlib.sha256(json.dumps({"pins": v.get("pins"), "config": cfg}, sort_keys=True).encode()).hexdigest()[:16]
# =========================================================================== loading
def _fidelity(statements: Optional[dict], pid: str, completion_text: str) -> Optional[str]:
if statements is None or pid not in statements:
return None
shown = C.displayed_signature(completion_text)
if shown is None:
return "unreadable"
return "faithful" if shown == C.statement_key(statements[pid])[2] else "rewritten"
def load_run(gen_paths, ver_paths, statements: Optional[dict] = None) -> tuple[dict[tuple, Sample], dict]:
"""`statements` ({problem_id: formal_statement}) switches on the statement-fidelity check of accepted samples (SOTA configuration)."""
ver: dict[tuple, dict] = {}
sigs: Counter = Counter()
for p in ver_paths:
for rec in C.iter_jsonl(p):
key = C.gen_key(rec)
if key in ver:
raise C.ContractError(f"{p}: duplicate verification key {key}")
ver[key] = rec
sigs[verifier_signature(rec["verifier"])] += 1
samples: dict[tuple, Sample] = {}
for p in gen_paths:
with open(p, encoding="utf-8") as f:
for lineno, line in enumerate(f, 1):
if not line.strip():
continue
rec = json.loads(line)
C.validate_generation(rec)
key = C.gen_key(rec)
if key in samples:
raise C.ContractError(f"{p}:{lineno}: duplicate generation key {key}")
v = ver.get(key)
if v is None:
raise C.ContractError(f"{p}:{lineno}: generation {key} has no verification record")
if v["code_sha256"] != C.code_sha(rec["lean_code"]):
raise C.ContractError(f"{p}:{lineno}: stale verification for {key} (code_sha256 mismatch)")
par = rec["parent"]
samples[key] = Sample(bool(v["accepted"]), list(v["reasons"]), rec["finish_reason"], len(rec["completion_ids"]),
None if par is None else (par["problem_id"], int(par["sample_idx"]), int(par["round"]), key[3]),
v["budget_s"], v["verify_s"], C.effective_max_tokens(rec),
_sampling_of(rec), rec["model_rev"], hashlib.sha256(json.dumps(rec["prompt_ids"]).encode()).hexdigest()[:16],
_fidelity(statements, rec["problem_id"], rec["completion_text"]) if v["accepted"] else None)
orphan = set(ver) - set(samples)
if orphan:
raise C.ContractError(f"{len(orphan)} verification records have no generation (first: {sorted(orphan)[0]})")
meta = {"verifier_signatures": dict(sigs), "n_samples": len(samples),
"budgets": sorted({s.budget for s in samples.values()}),
"verifier": next(iter(ver.values()))["verifier"] if ver else None}
return samples, meta
# =========================================================================== metrics
def round0_by_problem(samples, problem_ids, cap: int, allow_ragged: bool) -> dict[str, list[bool]]:
"""problem_id -> accepted flags of round-0 samples ordered by sample_idx (contiguous 0..n-1)."""
per: dict[str, dict[int, bool]] = defaultdict(dict)
for (pid, si, rd, c), s in samples.items():
if rd == 0 and c == cap and pid in problem_ids:
per[pid][si] = s.accepted
missing = sorted(set(problem_ids) - set(per))
if missing:
raise C.ContractError(f"incomplete run at cap {cap}: {len(missing)} problems have no round-0 sample (first: {missing[0]})")
out = {}
for pid, d in per.items():
if sorted(d) != list(range(len(d))):
raise C.ContractError(f"{pid}: round-0 sample_idx not contiguous from 0 ({sorted(d)[:12]}...)")
out[pid] = [d[i] for i in range(len(d))]
ns = {len(v) for v in out.values()}
if len(ns) > 1 and not allow_ragged:
raise C.ContractError(f"problems have different numbers of round-0 samples at cap {cap}: {sorted(ns)} (--allow-ragged-n to override)")
return out
def run_metrics(samples, problem_ids, cap: int, ks, seed: int, n_boot: int, allow_ragged: bool = False) -> dict:
flags = round0_by_problem(samples, set(problem_ids), cap, allow_ragged)
pids = sorted(flags)
n_min = min(len(v) for v in flags.values())
counts = [(len(flags[p]), sum(flags[p])) for p in pids]
out: dict = {"cap_tokens": cap, "n_problems": len(pids), "n_per_problem_min": n_min,
"n_per_problem_max": max(len(v) for v in flags.values()), "n_samples_round0": sum(n for n, _ in counts)}
tot_s, tot_c = out["n_samples_round0"], sum(c for _, c in counts)
per_problem_p1 = [c / n for n, c in counts]
lo, hi = S.bootstrap_mean_ci(per_problem_p1, seed, n_boot)
nlo, nhi = S.wilson_interval(tot_c, tot_s)
out["pass@1"] = {"value": tot_c / tot_s, "ci95_cluster_bootstrap": [lo, hi],
"ci95_wilson_pooled_samples_ANTICONSERVATIVE": [nlo, nhi]}
out["pass@k"] = {}
out["solved@k"] = {}
for k in ks:
if k > n_min:
out["pass@k"][str(k)] = None # explicit: not computable, not zero
out["solved@k"][str(k)] = None
continue
vals = [S.pass_at_k(n, c, k) for n, c in counts]
blo, bhi = S.bootstrap_mean_ci(vals, seed + k, n_boot)
solved = sum(1 for p in pids if S.solved_at_k(flags[p], k))
wlo, whi = S.wilson_interval(solved, len(pids))
out["pass@k"][str(k)] = {"value": sum(vals) / len(vals), "ci95_cluster_bootstrap": [blo, bhi]}
out["solved@k"][str(k)] = {"solved": solved, "n": len(pids), "fraction": solved / len(pids), "ci95_wilson": [wlo, whi]}
r0 = [s for (pid, si, rd, c), s in samples.items() if rd == 0 and c == cap and pid in set(pids)]
out["completion"] = {
"length_capped_fraction": sum(1 for s in r0 if s.finish == "length") / len(r0),
"mean_completion_tokens": sum(s.n_tok for s in r0) / len(r0),
"no_code_fraction": sum(1 for s in r0 if s.reasons == ["no_code"]) / len(r0),
"reason_histogram": dict(Counter(r for s in r0 if not s.accepted for r in s.reasons[:1]).most_common(12)),
"verify_s_mean": sum(s.verify_s for s in r0) / len(r0),
"context_clipped_fraction": sum(1 for s in r0 if s.eff_max < cap) / len(r0),
"mean_effective_max_tokens": sum(s.eff_max for s in r0) / len(r0),
}
out["design"] = {"model_revs": sorted({s.model_rev for s in r0}), "sampling": sorted({tuple(s.sampling or ()) for s in r0}, key=str)}
out["_flags"] = flags # stripped before JSON
return out
def primary_blockers(src: str, cap: int, k: int, base_metrics: dict) -> list[str]:
"""Why a comparison of this shape may NOT carry the PRIMARY label ([] = it is the registered test)."""
if (src, cap, k) != (PRIMARY["source"], PRIMARY["cap"], PRIMARY["k"]):
return ["not the registered (set, cap, k)"]
why = []
if base_metrics["n_problems"] != PRIMARY["n_problems"]:
why.append(f"{base_metrics['n_problems']} problems scored, the registered set has {PRIMARY['n_problems']}")
revs = base_metrics["design"]["model_revs"]
if revs != [PRIMARY["baseline_model_rev"]]:
why.append(f"baseline model_rev {revs} is not the registered {PRIMARY['baseline_model_rev']}")
want = PRIMARY["sampling"]
for t, p, kk in base_metrics["design"]["sampling"] or [()]:
if (t, p) != (want["temperature"], want["top_p"]) or kk not in want["top_k"]:
why.append(f"baseline sampling (temperature, top_p, top_k) = {(t, p, kk)} is not the registered {(want['temperature'], want['top_p'], want['top_k'])}")
return why
PRIMARY_ALPHA = 0.05
def registered_decision(m: dict) -> str:
"""The pre-registered decision rule, applied mechanically so the wording is not chosen after the data:
success = p < alpha AND the candidate solves more problems; p < alpha with the candidate lower is a
significant degradation; anything else is "no detectable improvement" (never "no effect")."""
sig = m["p_value_exact_two_sided"] < PRIMARY_ALPHA
if sig and m["solved_b"] > m["solved_a"]:
return "SUCCESS: significant improvement"
if sig and m["solved_b"] < m["solved_a"]:
return "significant DEGRADATION"
return "no detectable improvement"
def design_mismatches(base: dict[tuple, Sample], cand: dict[tuple, Sample]) -> list[str]:
"""Differences between the two systems' GENERATION that no verifier signature can show: sampling
settings and the round-0 prompts. A paired test of two runs that were not asked the same question
with the same sampler measures the harness, not the model."""
out = []
sb = {tuple(s.sampling or ()) for s in base.values()}
sc = {tuple(s.sampling or ()) for s in cand.values()}
if len(sb) != 1 or len(sc) != 1 or sb != sc:
out.append(f"sampling settings differ or are mixed: baseline {sorted(sb, key=str)} vs candidate {sorted(sc, key=str)}")
common = sorted(k for k in set(base) & set(cand) if k[2] == 0)
bad = sorted({k[0] for k in common if base[k].prompt_sha != cand[k].prompt_sha})
if bad:
out.append(f"{len(bad)} problems were prompted differently in the two runs at the same sample keys (first: {bad[0]})")
return out
def compare(base: dict, cand: dict, k: int, seed: int, n_boot: int) -> dict:
fb, fc = base["_flags"], cand["_flags"]
if set(fb) != set(fc):
raise C.ContractError("the two runs cover different problem sets")
if min(base["n_per_problem_min"], cand["n_per_problem_min"]) < k:
return {"k": k, "available": False, "why": f"needs >= {k} samples per problem in both runs"}
sa = {p: S.solved_at_k(fb[p], k) for p in fb}
sb = {p: S.solved_at_k(fc[p], k) for p in fb}
m = S.mcnemar_exact(sa, sb)
pids = sorted(fb)
va = [S.pass_at_k(len(fb[p]), sum(fb[p]), k) for p in pids]
vb = [S.pass_at_k(len(fc[p]), sum(fc[p]), k) for p in pids]
dlo, dhi = S.paired_bootstrap_diff_ci(va, vb, seed + 7 * k, n_boot)
return {"k": k, "available": True, "mcnemar": m.to_json(), "delta_solved": m.solved_b - m.solved_a,
"delta_solved_fraction": (m.solved_b - m.solved_a) / m.n_problems,
"delta_pass@k": sum(vb) / len(vb) - sum(va) / len(va), "delta_pass@k_ci95_paired_bootstrap": [dlo, dhi]}
def correction_lift(samples, problem_ids, cap: int) -> Optional[dict]:
"""Per-round table: how many unsolved problems did correction rounds convert, at what cost.
A problem is solved through round r when ANY sample of rounds <= r was accepted. The lift is
confounded with compute (corrections are extra samples): the table reports samples and
completion tokens spent per round so the comparison against `pass@(n0 + m)` can be made."""
pids = set(problem_ids)
rounds = sorted({rd for (pid, si, rd, c) in samples if c == cap and pid in pids})
if len(rounds) < 2:
return None
acc_by_round: dict[int, set[str]] = defaultdict(set)
gen_by_round: Counter = Counter()
tok_by_round: Counter = Counter()
acc_samples: Counter = Counter()
for (pid, si, rd, c), s in samples.items():
if c != cap or pid not in pids:
continue
gen_by_round[rd] += 1
tok_by_round[rd] += s.n_tok
if s.accepted:
acc_by_round[rd].add(pid)
acc_samples[rd] += 1
if rd > 0:
par = s.parent
if par is None or par not in samples:
raise C.ContractError(f"correction sample {(pid, si, rd, c)} has no parent among the generations")
if samples[par].accepted:
raise C.ContractError(f"correction sample {(pid, si, rd, c)} corrects an ACCEPTED parent {par}")
n = len(pids)
solved = set(acc_by_round[0])
rows = [{"round": 0, "samples": gen_by_round[0], "completion_tokens": tok_by_round[0], "accepted_samples": acc_samples[0],
"newly_solved_problems": len(solved), "solved_through_round": len(solved), "solved_fraction": len(solved) / n}]
unsolved_after0 = n - len(solved)
for r in rounds[1:]:
new = acc_by_round[r] - solved
solved |= acc_by_round[r]
lo, hi = S.wilson_interval(len(solved) - rows[0]["solved_through_round"], unsolved_after0) if unsolved_after0 else (0.0, 0.0)
rows.append({"round": r, "samples": gen_by_round[r], "completion_tokens": tok_by_round[r], "accepted_samples": acc_samples[r],
"sample_success_rate": acc_samples[r] / gen_by_round[r] if gen_by_round[r] else None,
"newly_solved_problems": len(new), "solved_through_round": len(solved), "solved_fraction": len(solved) / n,
"conversion_of_round0_unsolved": (len(solved) - rows[0]["solved_through_round"]) / unsolved_after0 if unsolved_after0 else None,
"conversion_ci95_wilson": [lo, hi]})
return {"cap_tokens": cap, "n_problems": n, "rows": rows,
"note": "lift is extra-sample compute, not free: compare solved_through_round with pass@(n0 + corrections per problem) of round 0 when n0 allows"}
# =========================================================================== report
def build_report(problems_by_source: dict[str, dict], runs: dict[str, tuple], baseline: Optional[str], candidate: Optional[str],
ks, seed: int, n_boot: int, allow_ragged: bool, allow_verifier_mismatch: bool, allow_design_mismatch: bool = False) -> dict:
rep: dict = {"preregistered_primary": dict(PRIMARY), "prereg": prereg_status(), "runs": {}, "comparisons": [], "correction_lift": {}}
metrics: dict[str, dict] = {}
for name, (samples, meta) in runs.items():
caps = sorted({c for (_, _, _, c) in samples})
rep["runs"][name] = {"meta": meta, "tables": {}}
metrics[name] = {}
for src, probs in problems_by_source.items():
for cap in caps:
covered = {pid for (pid, _, _, c) in samples if c == cap and pid in probs}
if not covered:
continue
m = run_metrics(samples, set(probs), cap, ks, seed, n_boot, allow_ragged)
metrics[name][(src, cap)] = m
rep["runs"][name]["tables"][f"{src}@{cap}"] = {k: v for k, v in m.items() if not k.startswith("_")}
lift = correction_lift(samples, set(probs), cap)
if lift:
rep["correction_lift"][f"{name}/{src}@{cap}"] = lift
if baseline and candidate:
sig_b = {verifier_signature(runs[baseline][1]["verifier"])} if runs[baseline][1]["verifier"] else set()
sig_c = {verifier_signature(runs[candidate][1]["verifier"])} if runs[candidate][1]["verifier"] else set()
if sig_b != sig_c or runs[baseline][1]["budgets"] != runs[candidate][1]["budgets"]:
msg = f"verifier differs: baseline {sig_b} budgets {runs[baseline][1]['budgets']} vs candidate {sig_c} budgets {runs[candidate][1]['budgets']}"
if not allow_verifier_mismatch:
raise C.ContractError(msg + " (--allow-verifier-mismatch to compare anyway)")
rep["verifier_mismatch"] = msg
design = design_mismatches(runs[baseline][0], runs[candidate][0])
if design:
if not allow_design_mismatch:
raise C.ContractError("; ".join(design) + " (--allow-design-mismatch to compare anyway)")
rep["design_mismatch"] = design
for (src, cap), mb in sorted(metrics[baseline].items()):
mc = metrics[candidate].get((src, cap))
if mc is None:
continue
for k in ks:
cmpd = compare(mb, mc, k, seed, n_boot)
blockers = primary_blockers(src, cap, k, mb)
cmpd.update({"source": src, "cap_tokens": cap, "baseline": baseline, "candidate": candidate,
"endpoint": "PRIMARY" if not blockers else "secondary"})
if not blockers and cmpd["available"]:
cmpd["decision"] = registered_decision(cmpd["mcnemar"])
if blockers and blockers != ["not the registered (set, cap, k)"]:
cmpd["not_primary_because"] = blockers # primary-shaped but not the registered test
rep["comparisons"].append(cmpd)
if not any(c["endpoint"] == "PRIMARY" and c["available"] for c in rep["comparisons"]):
rep["primary_missing"] = "the pre-registered primary comparison (miniF2F, cap 16384, k=8) could not be computed from these runs"
near = [c for c in rep["comparisons"] if c.get("not_primary_because")]
if near:
rep["primary_missing"] += "; the primary-shaped comparison below is NOT the registered test: " + "; ".join(near[0]["not_primary_because"])
return rep
def render_md(rep: dict) -> str:
L = ["# P2 evaluation report", "",
f"Pre-registered primary endpoint: {PRIMARY['source']} solved@{PRIMARY['k']} at cap {PRIMARY['cap']}, paired exact McNemar, alpha 0.05. All other rows are secondary and not multiplicity-corrected.", ""]
pr = rep.get("prereg", {})
L += [f"Pre-registration (docs/P2.md): {pr.get('status')}" + (f" (sha256 {pr.get('computed')})" if pr.get("computed") else ""), ""]
if pr.get("status") != "verified":
L += ["WARNING: the registered analysis plan could not be verified; treat every p-value below as exploratory.", ""]
if rep.get("verifier_mismatch"):
L += [f"WARNING: {rep['verifier_mismatch']}", ""]
if rep.get("design_mismatch"):
L += [f"WARNING: generation design differs between the runs: {'; '.join(rep['design_mismatch'])}", ""]
if rep.get("primary_missing"):
L += [f"WARNING: {rep['primary_missing']}", ""]
pct = lambda x: "n/a" if x is None else f"{100 * x:.1f}%"
for name, r in rep["runs"].items():
L += [f"## Run `{name}`", "", f"verifier budgets (s): {r['meta']['budgets']}; samples: {r['meta']['n_samples']}", "",
"| set @ cap | problems | n/prob | pass@1 [95% cluster-bootstrap] | " + " | ".join(f"pass@{k}" for k in DEFAULT_KS) + " | length-capped | mean tok | mean effective max_tokens |",
"|" + "---|" * (7 + len(DEFAULT_KS))]
for tab, t in r["tables"].items():
p1 = t["pass@1"]
cells = []
for k in DEFAULT_KS:
v = t["pass@k"].get(str(k))
cells.append("n/a" if v is None else f"{pct(v['value'])} ({t['solved@k'][str(k)]['solved']}/{t['n_problems']})")
L.append(f"| {tab} | {t['n_problems']} | {t['n_per_problem_min']} | {pct(p1['value'])} [{pct(p1['ci95_cluster_bootstrap'][0])}, {pct(p1['ci95_cluster_bootstrap'][1])}] | "
+ " | ".join(cells) + f" | {pct(t['completion']['length_capped_fraction'])} | {t['completion']['mean_completion_tokens']:.0f} | {t['completion']['mean_effective_max_tokens']:.0f} |")
L.append("")
if rep["comparisons"]:
L += ["## Paired comparison (McNemar exact on solved@k)", "",
"| endpoint | set @ cap | k | solved base | solved cand | only base | only cand | discordant | p (exact, 2-sided) | delta pass@k [95% paired bootstrap] |", "|" + "---|" * 10]
for c in rep["comparisons"]:
if not c["available"]:
L.append(f"| {c['endpoint']} | {c['source']} @ {c['cap_tokens']} | {c['k']} | n/a | n/a | | | | | {c['why']} |")
continue
m = c["mcnemar"]
L.append(f"| {c['endpoint']} | {c['source']} @ {c['cap_tokens']} | {c['k']} | {m['solved_a']} | {m['solved_b']} | {m['only_a']} | {m['only_b']} | "
f"{m['n_discordant']} | {m['p_value_exact_two_sided']:.4f} | {100 * c['delta_pass@k']:+.1f} pp [{100 * c['delta_pass@k_ci95_paired_bootstrap'][0]:+.1f}, {100 * c['delta_pass@k_ci95_paired_bootstrap'][1]:+.1f}] |")
L.append("")
for c in rep["comparisons"]:
if c.get("decision"):
L += [f"**Pre-registered decision (primary, alpha {PRIMARY_ALPHA}): {c['decision']}.** A null result means no gain of roughly 6 pp or more was detectable, not that there is no effect (docs/P2.md section 1).", ""]
if rep["correction_lift"]:
L += ["## Correction-round lift", "", "| run / set @ cap | round | samples | completion tokens | accepted samples | newly solved problems | solved through round | solved % | conversion of round-0 unsolved [95% Wilson] |", "|" + "---|" * 9]
for name, t in rep["correction_lift"].items():
for r in t["rows"]:
conv = r.get("conversion_of_round0_unsolved")
ci = r.get("conversion_ci95_wilson")
L.append(f"| {name} | {r['round']} | {r['samples']} | {r['completion_tokens']} | {r['accepted_samples']} | {r['newly_solved_problems']} | "
f"{r['solved_through_round']} | {pct(r['solved_fraction'])} | " + ("" if conv is None else f"{pct(conv)} [{pct(ci[0])}, {pct(ci[1])}]") + " |")
L += ["", "Lift costs extra samples: compare against round-0 pass@k at the same total samples per problem before calling it a gain.", ""]
return "\n".join(L)
# =========================================================================== SOTA configuration (docs/PREREG_SOTA.md)
#
# A SEPARATE registration from the P2 primary above (which is left exactly as it was). Co-primary endpoints, both on
# miniF2F-test: A = class A (single pass, cap 40k), B = class B (attempt + up to 2 Lean-feedback repairs, 40k total
# completion tokens). Estimand: mean over problems of the unbiased pass@32 from n = 64 samples. Test: paired sign-flip
# permutation over problems, two-sided alpha = 0.05, Holm over {A, B}. Secondaries: exact McNemar on solved@32 of the
# first 32 samples, mean pass@1, ProofNet-test class A. Everything else is exploratory.
SOTA = {
"name": "sota-v1",
"alpha": 0.05,
"coprimary": {"source": "minif2f", "split": "test", "n_problems": 244, "cap": 40960, "n": 64, "k": 32},
"class_b": {"max_repairs": 2, "ledger_tokens": 40960},
"secondary": {"mcnemar_first_n": 32, "proofnet": {"source": "proofnet", "split": "test", "k": 32}},
"verifier_budget_s": 300.0,
"n_flips": 100_000,
"exploratory_ks": (1, 8, 16, 32),
}
LOOK_LOG_DEFAULT = REPO / "docs" / "evidence" / "test_split_looks.jsonl"
def _prereg_hash_module():
spec = importlib.util.spec_from_file_location("p2_prereg_hash", Path(__file__).with_name("prereg_hash.py"))
mod = importlib.util.module_from_spec(spec)
sys.modules["p2_prereg_hash"] = mod
spec.loader.exec_module(mod)
return mod
def prereg_overall_hash() -> Optional[str]:
"""Current sha256 of (registration text + analysis code), as printed by scripts/p2/prereg_hash.py; None if an input is missing."""
try:
return _prereg_hash_module().compute()["sha256"]
except FileNotFoundError:
return None
def verifier_hash(v: Optional[dict]) -> Optional[str]:
"""Full sha256 of the verifier identity (pins + config, canonical JSON; `workers` is deliberately excluded, as in
`verifier_signature`). This is the value the frozen-verifier rule compares."""
if not v:
return None
return hashlib.sha256(json.dumps({"pins": v.get("pins"), "config": dict(v.get("config", {}))}, sort_keys=True).encode()).hexdigest()
def class_a_counts(samples, pids, cap: int) -> tuple[dict, dict]:
"""-> ({pid: (n, c)}, {pid: flags}) from round-0 samples at `cap` (ragged n is refused)."""
flags = round0_by_problem(samples, set(pids), cap, allow_ragged=False)
return {p: (len(f), sum(f)) for p, f in flags.items()}, flags
def class_b_counts(samples, pids, cap: int, max_repairs: int, ledger: int, allow_unrepaired: bool = False) -> Optional[dict]:
"""Class B: {pid: (n_trajectories, c_solved)}; None if the run has no repair rounds at all.
A trajectory is the chain round-0 sample -> its repair -> its repair (<= `max_repairs`). It is solved iff some element
is accepted AND the cumulative completion tokens up to and including that element are <= `ledger` (the 40k TOTAL).
A failed sample with ledger left but no repair record is a missing record, not a failure: refused unless `allow_unrepaired`."""
pset = set(pids)
if not any(rd > 0 and c == cap and pid in pset for (pid, _, rd, c) in samples):
return None
child: dict[tuple, tuple] = {}
for key, s in samples.items():
if key[2] > 0 and key[3] == cap and key[0] in pset:
if s.parent in child:
raise C.ContractError(f"two repairs claim the same parent {s.parent}")
child[s.parent] = key
out = {}
for pid in sorted(pset):
n = solved = 0
for key in sorted(k for k in samples if k[0] == pid and k[2] == 0 and k[3] == cap):
n += 1
cum, cur, ok = 0, key, False
for _ in range(max_repairs + 1):
s = samples[cur]
cum += s.n_tok
if s.accepted:
ok = cum <= ledger
break
if cum >= ledger or cur[2] >= max_repairs:
break
nxt = child.get(cur)
if nxt is None:
if not allow_unrepaired:
raise C.ContractError(f"{cur} failed with ledger left but has no repair record (--allow-unrepaired to count it as a failure)")
break
cur = nxt
solved += ok
out[pid] = (n, solved)
return out
def sota_contrast(cb: dict, cc: dict, k: int, seed: int, n_boot: int, n_flips: int) -> dict:
"""Paired contrast of the unbiased pass@k estimand; `cb`/`cc` are {pid: (n, c)}. Sign-flip p + problem bootstrap CI."""
if set(cb) != set(cc):
raise C.ContractError("the two runs cover different problem sets")
pids = sorted(cb)
if min(min(n for n, _ in cb.values()), min(n for n, _ in cc.values())) < k:
return {"available": False, "k": k, "why": f"needs >= {k} samples per problem in both runs"}
va = PD.pass_at_k_array([cb[p][0] for p in pids], [cb[p][1] for p in pids], k)
vb = PD.pass_at_k_array([cc[p][0] for p in pids], [cc[p][1] for p in pids], k)
d = vb - va
t = PD.sign_flip_test(d, n_flips=n_flips, seed=seed)
lo, hi = PD.paired_bootstrap_ci(d, seed + 11 * k, n_boot)
return {"available": True, "k": k, "n_problems": len(pids), "n_per_problem": sorted({cb[p][0] for p in pids} | {cc[p][0] for p in pids}),
"mean_baseline": float(va.mean()), "mean_candidate": float(vb.mean()), "delta": float(d.mean()),
"delta_ci95_paired_bootstrap": [lo, hi], "p_sign_flip": t["p"], "sign_flip_method": t["method"], "sign_flip_n": t["n_flips"],
"n_changed_problems": t["n_nonzero"], "null_quantiles_of_delta": t["null_quantiles"]}
def _fidelity_summary(samples, pids, cap: int) -> dict:
pset = set(pids)
acc = [(key, s) for key, s in samples.items() if key[3] == cap and key[0] in pset and s.accepted]
cnt = Counter(s.fidelity or "unchecked" for _, s in acc)
bad = [(k, s) for k, s in acc if s.fidelity in ("rewritten", "unreadable")]
return {"accepted_samples": len(acc), "faithful": cnt["faithful"], "rewritten": cnt["rewritten"], "unreadable": cnt["unreadable"],
"unchecked": cnt["unchecked"],
"rewritten_statement_count": cnt["rewritten"] + cnt["unreadable"],
"problems_with_rewritten_accept": len({k[0] for k, _ in bad}),
"in_repair_rounds": sum(1 for k, _ in bad if k[2] > 0)}
def _strict_class_a(samples, pids, cap: int) -> dict:
"""Class-A (n, c) with rewritten/unreadable accepts counted as failures (exploratory fidelity sensitivity)."""
per: dict[str, dict[int, bool]] = defaultdict(dict)
for (pid, si, rd, c), s in samples.items():
if rd == 0 and c == cap and pid in pids:
per[pid][si] = s.accepted and s.fidelity == "faithful"
return {p: (len(d), sum(d.values())) for p, d in per.items()}
def append_look_log(path, entry: dict) -> dict:
"""Append ONE hash-chained line to the test-split look log (append-only: O_APPEND, flock, fsync; each line carries the
sha256 of the previous line's content, so a deletion or edit is detectable by `verify_look_log`)."""
import fcntl
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "a+b") as f:
fcntl.flock(f, fcntl.LOCK_EX)
f.seek(0)
lines = [l for l in f.read().splitlines() if l.strip()]
prev = json.loads(lines[-1])["entry_sha256"] if lines else "0" * 64
rec = dict(entry, prev_sha256=prev)
rec["entry_sha256"] = hashlib.sha256(json.dumps(rec, sort_keys=True).encode()).hexdigest()
f.write((json.dumps(rec, sort_keys=True) + "\n").encode())
f.flush()
os.fsync(f.fileno())
return rec
def verify_look_log(path) -> dict:
"""Recompute the chain. -> {ok, n_entries, error}."""
prev, n = "0" * 64, 0
for i, line in enumerate(Path(path).read_text().splitlines(), 1):
if not line.strip():
continue
rec = json.loads(line)
got = rec.pop("entry_sha256", None)
if rec.get("prev_sha256") != prev:
return {"ok": False, "n_entries": n, "error": f"line {i}: chain broken (prev_sha256 does not match the previous entry)"}
if hashlib.sha256(json.dumps(rec, sort_keys=True).encode()).hexdigest() != got:
return {"ok": False, "n_entries": n, "error": f"line {i}: entry content does not match its hash"}
prev, n = got, n + 1
return {"ok": True, "n_entries": n, "error": None}
def look_entry(config: str, splits: list, report_json: Path, report_md: Path, inputs: dict) -> dict:
try:
who = getpass.getuser()
except Exception: # noqa: BLE001 (no passwd entry in some containers)
who = os.environ.get("USER", "unknown")
return {"who": {"user": who, "host": socket.gethostname(), "pid": os.getpid(), "argv": list(sys.argv)},
"when_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
"what": {"config": config, "splits": sorted(splits), "report_json": str(report_json), "report_md": str(report_md),
"report_json_sha256": C.sha256_file(report_json), "report_md_sha256": C.sha256_file(report_md)},
"inputs": inputs}
def build_sota_report(problems_by_source: dict, runs: dict, baseline: str, candidate: str, seed: int, n_boot: int,
frozen_verifier_hash: Optional[str] = None, registered_hash: Optional[str] = None,
allow_verifier_mismatch: bool = False, allow_design_mismatch: bool = False, allow_unrepaired: bool = False,
n_flips: Optional[int] = None) -> dict:
cfg, alpha = SOTA, SOTA["alpha"]
n_flips = n_flips or cfg["n_flips"]
cp, cb_cfg = cfg["coprimary"], cfg["class_b"]
sb, sc = runs[baseline][0], runs[candidate][0]
mb_, mc_ = runs[baseline][1], runs[candidate][1]
sig_b = {verifier_signature(mb_["verifier"])} if mb_["verifier"] else set()
sig_c = {verifier_signature(mc_["verifier"])} if mc_["verifier"] else set()
rep: dict = {"configuration": cfg["name"], "registered": {k: v for k, v in cfg.items()}, "baseline": baseline, "candidate": candidate,
"seed": seed}
if sig_b != sig_c or mb_["budgets"] != mc_["budgets"]:
msg = f"verifier differs: baseline {sig_b} budgets {mb_['budgets']} vs candidate {sig_c} budgets {mc_['budgets']}"
if not allow_verifier_mismatch:
raise C.ContractError(msg + " (--allow-verifier-mismatch to compare anyway)")
rep["verifier_mismatch"] = msg
design = design_mismatches(sb, sc)
if design:
if not allow_design_mismatch:
raise C.ContractError("; ".join(design) + " (--allow-design-mismatch to compare anyway)")
rep["design_mismatch"] = design
cur = prereg_overall_hash()
rep["prereg"] = {"current_overall_sha256": cur, "registered_overall_sha256": registered_hash,
"status": "unregistered" if not registered_hash else ("verified" if registered_hash == cur else "MODIFIED_AFTER_REGISTRATION")}
def subset(src, split):
return {pid: r for pid, r in problems_by_source.get(src, {}).items() if r.get("split") == split}
test = subset(cp["source"], cp["split"])
pids = set(test)
if not pids:
raise C.ContractError(f"no {cp['source']}/{cp['split']} problems supplied: the co-primary set is empty")
prob_set_hash = hashlib.sha256(json.dumps(sorted((p, test[p]["statement_sha"]) for p in test)).encode()).hexdigest()
# ---- blockers: what stops a number from carrying the CONFIRMATORY label (never an exception: the numbers are still shown)
blockers = []
if len(pids) != cp["n_problems"]:
blockers.append(f"{len(pids)} {cp['source']}/{cp['split']} problems scored, the registered set has {cp['n_problems']}")
for name, (_, meta) in runs.items():
if name not in (baseline, candidate):
continue
if len(meta["verifier_signatures"]) != 1:
blockers.append(f"run {name}: records were verified under {len(meta['verifier_signatures'])} different verifier configurations")
if meta["budgets"] != [cfg["verifier_budget_s"]]:
blockers.append(f"run {name}: verifier budgets {meta['budgets']} are not the registered [{cfg['verifier_budget_s']}]")
vh = verifier_hash(meta["verifier"])
if frozen_verifier_hash is None:
blockers.append("no frozen verifier hash supplied (--frozen-verifier-hash): the frozen-verifier rule cannot be checked")
elif vh != frozen_verifier_hash:
blockers.append(f"run {name}: verifier hash {vh} is not the frozen {frozen_verifier_hash}")
blockers = list(dict.fromkeys(blockers))
if rep["prereg"]["status"] != "verified":
blockers.append(f"pre-registration hash status: {rep['prereg']['status']}")
if rep.get("verifier_mismatch"):
blockers.append("verifier mismatch overridden")
if rep.get("design_mismatch"):
blockers.append("generation design mismatch overridden")
if allow_unrepaired:
blockers.append("--allow-unrepaired: failed samples without a repair record were counted as failures")
fid = {name: _fidelity_summary(runs[name][0], pids, cp["cap"]) for name in (baseline, candidate)}
if any(f["unchecked"] for f in fid.values()):
blockers.append("statement fidelity (rewritten-statement count) was not checked for every accepted sample")
tok = {name: sum(s.n_tok for (pid, _, _, c), s in runs[name][0].items() if pid in pids and c == cp["cap"]) for name in (baseline, candidate)}
def tag(klass: str, source: str, split: str, n_prob: int, n: int, k: int, ledger=None) -> dict:
return {"class": klass, "benchmark": f"{source}/{split}", "n_problems": n_prob, "problem_set_sha256": prob_set_hash if source == cp["source"] else None,
"n": n, "k": k, "cap_tokens": cp["cap"], "ledger_tokens": ledger,
"lean_mathlib_pins": (mb_["verifier"] or {}).get("pins"), "verifier_hash": verifier_hash(mb_["verifier"]),
"verifier_budget_s": mb_["budgets"], "verifier_config": (mb_["verifier"] or {}).get("config"),
"rewritten_statement_count": {"baseline": fid[baseline]["rewritten_statement_count"], "candidate": fid[candidate]["rewritten_statement_count"]},
"completion_tokens_total": {"baseline": tok[baseline], "candidate": tok[candidate]}, "statement_version": "as supplied in the problems file (see problem_set_sha256)"}
# ---- co-primary A
co: dict = {}
ca_b, fl_b = class_a_counts(sb, pids, cp["cap"])
ca_c, fl_c = class_a_counts(sc, pids, cp["cap"])
for nm, cn in (("baseline", ca_b), ("candidate", ca_c)):
ns = {n for n, _ in cn.values()}
if ns != {cp["n"]}:
blockers.append(f"{nm}: n per problem is {sorted(ns)}, the registered n is {cp['n']}")
A = sota_contrast(ca_b, ca_c, cp["k"], seed, n_boot, n_flips)
A["protocol"] = tag("A", cp["source"], cp["split"], len(pids), cp["n"], cp["k"])
co["A"] = A
# ---- co-primary B
bb = class_b_counts(sb, pids, cp["cap"], cb_cfg["max_repairs"], cb_cfg["ledger_tokens"], allow_unrepaired)
bc = class_b_counts(sc, pids, cp["cap"], cb_cfg["max_repairs"], cb_cfg["ledger_tokens"], allow_unrepaired)
if bb is None or bc is None:
B = {"available": False, "k": cp["k"], "why": "no Lean-feedback repair rounds in the " + ("baseline" if bb is None else "candidate") + " run"}
else:
B = sota_contrast(bb, bc, cp["k"], seed + 1, n_boot, n_flips)
if B["available"] and B["n_per_problem"] != [cp["n"]]:
blockers.append(f"class B: trajectories per problem {B['n_per_problem']}, registered {cp['n']}")
B["protocol"] = tag("B", cp["source"], cp["split"], len(pids), cp["n"], cp["k"], cb_cfg["ledger_tokens"])
co["B"] = B
pA = A["p_sign_flip"] if A["available"] else 1.0
pB = B["p_sign_flip"] if B["available"] else 1.0 # an uncomputed co-primary counts as p = 1: Holm is over the registered two
h = PD.holm([pA, pB], alpha)
for key, nm, adj, rej in zip(("A", "B"), ("A", "B"), h["adjusted"], h["reject"]):
e = co[key]
e["p_holm"], e["reject_holm"] = adj, bool(rej)
if e["available"]:
e["decision"] = ("SUCCESS: significant improvement" if rej and e["delta"] > 0 else
"significant DEGRADATION" if rej and e["delta"] < 0 else "no detectable improvement")
co["holm"] = {"alpha": alpha, "m": 2, "p_in": [pA, pB], **{k: v for k, v in h.items() if k != "alpha"}}
rep["coprimary"] = co
rep["confirmatory_blockers"] = blockers
rep["label"] = "CONFIRMATORY" if not blockers and A["available"] and B["available"] else "NOT CONFIRMATORY"
rep["fidelity"] = fid
# ---- secondary (not multiplicity-corrected, stated as such)
sec: dict = {}
n1 = cfg["secondary"]["mcnemar_first_n"]
if min(len(f) for f in fl_b.values()) >= n1 and min(len(f) for f in fl_c.values()) >= n1:
m = S.mcnemar_exact({p: S.solved_at_k(fl_b[p], n1) for p in fl_b}, {p: S.solved_at_k(fl_c[p], n1) for p in fl_c})
sec["mcnemar_first_32"] = dict(m.to_json(), definition=f"solved@{n1} = any of the FIRST {n1} samples (by sample_idx), class A")
else:
sec["mcnemar_first_32"] = {"available": False, "why": f"needs >= {n1} samples per problem"}
sec["mean_pass@1"] = sota_contrast(ca_b, ca_c, 1, seed + 2, n_boot, n_flips)
pn = cfg["secondary"]["proofnet"]
pset = set(subset(pn["source"], pn["split"]))
if pset and all(any(k[0] == p and k[2] == 0 and k[3] == cp["cap"] for k in sb) for p in pset) and all(any(k[0] == p and k[2] == 0 and k[3] == cp["cap"] for k in sc) for p in pset):
pb_, _ = class_a_counts(sb, pset, cp["cap"])
pc_, _ = class_a_counts(sc, pset, cp["cap"])
sec["proofnet_class_A"] = sota_contrast(pb_, pc_, pn["k"], seed + 3, n_boot, n_flips)
sec["proofnet_class_A"]["protocol"] = tag("A", pn["source"], pn["split"], len(pset), min(n for n, _ in pb_.values()), pn["k"])
else:
sec["proofnet_class_A"] = {"available": False, "why": "no complete proofnet/test class-A samples in both runs"}
rep["secondary"] = sec
# ---- exploratory (no multiplicity control, no decision attached)
exp: dict = {"fidelity_strict_class_A": None, "other_sets": {}, "class_A_other_k": {}}
sa_b, sa_c = _strict_class_a(sb, pids, cp["cap"]), _strict_class_a(sc, pids, cp["cap"])
exp["fidelity_strict_class_A"] = dict(sota_contrast(sa_b, sa_c, cp["k"], seed + 4, n_boot, n_flips),
definition="class A with rewritten/unreadable accepted samples counted as failures")
for k in cfg["exploratory_ks"]:
if k != cp["k"]:
exp["class_A_other_k"][str(k)] = sota_contrast(ca_b, ca_c, k, seed + 5 + k, n_boot, n_flips)
for src, probs in sorted(problems_by_source.items()):
for split in sorted({r.get("split") for r in probs.values()}):
if (src, split) in ((cp["source"], cp["split"]), (pn["source"], pn["split"])):
continue
ids = set(subset(src, split))
try:
xb, _ = class_a_counts(sb, ids, cp["cap"])
xc, _ = class_a_counts(sc, ids, cp["cap"])
except C.ContractError as e:
exp["other_sets"][f"{src}/{split}"] = {"available": False, "why": str(e)}
continue
exp["other_sets"][f"{src}/{split}"] = {str(k): sota_contrast(xb, xc, k, seed + 9 + k, n_boot, n_flips) for k in cfg["exploratory_ks"]}
rep["exploratory"] = exp
return rep
def render_sota_md(rep: dict) -> str:
pct = lambda x: "n/a" if x is None else f"{100 * x:.2f}"
L = [f"# SOTA evaluation report ({rep['configuration']})", "",
f"**{rep['label']}.** Baseline `{rep['baseline']}`, candidate `{rep['candidate']}`. Registered in docs/PREREG_SOTA.md: co-primary A and B on miniF2F-test, "
"unbiased pass@32 from n = 64, paired sign-flip test, two-sided alpha 0.05, Holm over {A, B}.", ""]
pr = rep["prereg"]
L += [f"Pre-registration hash: {pr['status']} (current {pr['current_overall_sha256']}, registered {pr['registered_overall_sha256']}).", ""]
if rep["confirmatory_blockers"]:
L += ["WARNING: not confirmatory because:"] + [f"* {b}" for b in rep["confirmatory_blockers"]] + [""]
for w in ("verifier_mismatch", "design_mismatch"):
if rep.get(w):
L += [f"WARNING: {w}: {rep[w]}", ""]
L += ["## Co-primary (Holm over A and B)", "",
"| class | k | n/prob | pass@k base % | pass@k cand % | delta pp [95% problem bootstrap] | p (sign-flip) | p (Holm) | decision |", "|" + "---|" * 9]
for key in ("A", "B"):
e = rep["coprimary"][key]
if not e["available"]:
L.append(f"| {key} | {e['k']} | | | | | | | UNAVAILABLE: {e['why']} |")
continue
L.append(f"| {key} | {e['k']} | {e['n_per_problem']} | {pct(e['mean_baseline'])} | {pct(e['mean_candidate'])} | {pct(e['delta']).rjust(1)} "
f"[{pct(e['delta_ci95_paired_bootstrap'][0])}, {pct(e['delta_ci95_paired_bootstrap'][1])}] | {e['p_sign_flip']:.5f} ({e['sign_flip_method']}) | "
f"{e['p_holm']:.5f} | {e['decision']} |")
L += ["", "Rewritten-statement count (accepted samples whose displayed theorem differs from the reference statement; unreadable included):", ""]
for name, f in rep["fidelity"].items():
L.append(f"* `{name}`: {f['rewritten_statement_count']} of {f['accepted_samples']} accepted samples "
f"(rewritten {f['rewritten']}, unreadable {f['unreadable']}, unchecked {f['unchecked']}; {f['problems_with_rewritten_accept']} problems; {f['in_repair_rounds']} in repair rounds)")
L += ["", "Protocol tuple (class A):", "", "```json", json.dumps(rep["coprimary"]["A"]["protocol"], indent=1, default=str), "```", "",
"## Secondary (not multiplicity-corrected)", ""]
m = rep["secondary"]["mcnemar_first_32"]
if m.get("n_problems"):
L.append(f"* McNemar exact, solved@32 of the first 32 samples, class A: solved {m['solved_a']} vs {m['solved_b']}, only-base {m['only_a']}, only-cand {m['only_b']}, p = {m['p_value_exact_two_sided']:.4f}")
else:
L.append(f"* McNemar first 32: unavailable ({m.get('why')})")
for key, nm in (("mean_pass@1", "mean pass@1 (class A)"), ("proofnet_class_A", "ProofNet-test class A pass@32")):
e = rep["secondary"][key]
L.append(f"* {nm}: " + (f"{pct(e['mean_baseline'])} -> {pct(e['mean_candidate'])}, delta {pct(e['delta'])} pp [{pct(e['delta_ci95_paired_bootstrap'][0])}, {pct(e['delta_ci95_paired_bootstrap'][1])}], p = {e['p_sign_flip']:.4f}"
if e.get("available") else f"unavailable ({e.get('why')})"))
L += ["", "## Exploratory (descriptive; no decision attached)", ""]
e = rep["exploratory"]["fidelity_strict_class_A"]
if e.get("available"):
L.append(f"* class A pass@32 with rewritten accepts counted as failures: {pct(e['mean_baseline'])} -> {pct(e['mean_candidate'])}, delta {pct(e['delta'])} pp, p = {e['p_sign_flip']:.4f}")
for k, e in rep["exploratory"]["class_A_other_k"].items():
if e.get("available"):
L.append(f"* miniF2F-test class A pass@{k}: delta {pct(e['delta'])} pp [{pct(e['delta_ci95_paired_bootstrap'][0])}, {pct(e['delta_ci95_paired_bootstrap'][1])}], p = {e['p_sign_flip']:.4f}")
for name, ks in rep["exploratory"]["other_sets"].items():
for k, e in (ks.items() if "available" not in ks else []):
if e.get("available"):
L.append(f"* {name} class A pass@{k}: delta {pct(e['delta'])} pp, p = {e['p_sign_flip']:.4f}")
if "available" in ks:
L.append(f"* {name}: unavailable ({ks['why']})")
return "\n".join(L) + "\n"
def main(argv=None) -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--config", choices=("p2", "sota"), default="p2",
help="p2 = the registered P2 primary (default, unchanged); sota = the separately registered SOTA analysis (docs/PREREG_SOTA.md)")
ap.add_argument("--verify-prereg", action="store_true", help="print the pre-registration hash status and exit")
ap.add_argument("--problems", nargs="+")
ap.add_argument("--run", action="append", help="NAME:GEN[,GEN..]:VER[,VER..]")
ap.add_argument("--baseline")
ap.add_argument("--candidate")
ap.add_argument("--out-dir")
ap.add_argument("--ks", type=int, nargs="+", default=list(DEFAULT_KS))
ap.add_argument("--seed", type=int, default=0)
ap.add_argument("--bootstrap", type=int, default=10_000)
ap.add_argument("--allow-ragged-n", action="store_true")
ap.add_argument("--allow-verifier-mismatch", action="store_true")
ap.add_argument("--allow-design-mismatch", action="store_true", help="compare runs whose sampling settings or round-0 prompts differ (recorded in the report)")
ap.add_argument("--frozen-verifier-hash", help="sota: sha256 of the frozen verifier (pins + config); a run verified under anything else is not confirmatory")
ap.add_argument("--registered-hash", help="sota: the overall hash from scripts/p2/prereg_hash.py that was time-stamped")
ap.add_argument("--n-flips", type=int, default=SOTA["n_flips"], help="sota: Monte Carlo sign flips when more than 20 problems differ")
ap.add_argument("--allow-unrepaired", action="store_true", help="sota: count failed samples with no repair record as class-B failures (recorded)")
ap.add_argument("--look-log", default=os.environ.get("P2_LOOK_LOG", str(LOOK_LOG_DEFAULT)),
help="append-only log of every test-split report (default docs/evidence/test_split_looks.jsonl, or $P2_LOOK_LOG)")
a = ap.parse_args(argv)
if a.verify_prereg:
st = prereg_status()
print(json.dumps(st))
return 0 if st["status"] == "verified" else 1
if not (a.problems and a.run and a.out_dir):
ap.error("--problems, --run and --out-dir are required")
problems_by_source: dict[str, dict] = defaultdict(dict)
for p in a.problems:
for pid, rec in C.load_problems(p).items():
problems_by_source[rec["source"]][pid] = rec
statements = {pid: rec["formal_statement"] for probs in problems_by_source.values() for pid, rec in probs.items()} if a.config == "sota" else None
runs = {}
for spec in a.run:
name, gens, vers = spec.split(":")
if name in runs:
raise SystemExit(f"duplicate run name {name}")
runs[name] = load_run(gens.split(","), vers.split(","), statements)
if bool(a.baseline) != bool(a.candidate):
raise SystemExit("--baseline and --candidate go together")
for n in (a.baseline, a.candidate):
if n and n not in runs:
raise SystemExit(f"unknown run {n}")
if a.config == "sota":
if not (a.baseline and a.candidate):
raise SystemExit("--config sota needs --baseline and --candidate")
rep = build_sota_report(dict(problems_by_source), runs, a.baseline, a.candidate, a.seed, a.bootstrap, a.frozen_verifier_hash, a.registered_hash,
a.allow_verifier_mismatch, a.allow_design_mismatch, a.allow_unrepaired, a.n_flips)
md = render_sota_md(rep)
else:
rep = build_report(dict(problems_by_source), runs, a.baseline, a.candidate, tuple(a.ks), a.seed, a.bootstrap, a.allow_ragged_n, a.allow_verifier_mismatch, a.allow_design_mismatch)
md = render_md(rep)
out = Path(a.out_dir)
out.mkdir(parents=True, exist_ok=True)
(out / "report.json").write_text(json.dumps(rep, indent=1, default=str))
(out / "report.md").write_text(md)
splits = sorted({f"{src}/{r.get('split')}" for src, probs in problems_by_source.items() for r in probs.values() if r.get("split") == "test"})
if splits: # a test-split look: recorded BEFORE the numbers are printed
inputs = {"problems": {p: C.sha256_file(p) for p in a.problems},
"runs": {n: {"gen": {g: C.sha256_file(g) for g in spec.split(":")[1].split(",")}, "ver": {v: C.sha256_file(v) for v in spec.split(":")[2].split(",")}}
for n, spec in ((sp.split(":")[0], sp) for sp in a.run)}}
append_look_log(a.look_log, look_entry(a.config, splits, out / "report.json", out / "report.md", inputs))
print(md)
return 0
if __name__ == "__main__":
raise SystemExit(main())
|