formal-mas / scripts /build_eval_set.py
NicolasT08's picture
Formal-MAS source code
dfe1826 verified
Raw History Blame Contribute Delete
6.82 kB
"""Build data/eval.jsonl from the pinned sources and data/curation/criteria.yaml.
bash scripts/fetch_eval_sources.sh # pinned miniF2F port and PutnamBench → data/raw/
python scripts/build_eval_set.py # draws, compiles with `sorry`, writes the set
For each cell the problems are taken in seeded order; a problem that does not compile, or that the
fidelity review discarded (criteria.yaml → discards), is logged in data/curation/discarded.json and
replaced by the next one. No LLM runs here, and only Lean's compile-with-sorry check is used.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
import yaml
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
from formal_mas import evalset as ev # noqa: E402
from formal_mas.lean.runner import LeanRunner, compiles # noqa: E402
from formal_mas.lean.statement import parse_theorem # noqa: E402
def load_pools(raw: Path, criteria: dict) -> dict[tuple[str, str], list[dict]]:
labels = ev.flatten_labels(criteria["labels"])
seen = set(criteria["seen_in_repo"])
pools: dict[tuple[str, str], list[dict]] = {}
for rec in ev.load_minif2f(raw / "minif2f.jsonl"):
area = ev.area_of(rec["id"], labels)
if area and rec["id"] not in seen:
pools.setdefault((ev.difficulty_of(rec["id"]), area), []).append(rec)
informal = json.loads((raw / "putnam" / "informal" / "putnam.json").read_text(encoding="utf-8"))
for entry in informal:
path = raw / "putnam" / "lean4" / "src" / f"{entry['problem_name']}.lean"
if not path.exists():
continue
rec = ev.putnam_record(entry["problem_name"], entry["tags"], entry["informal_statement"],
path.read_text(encoding="utf-8"))
if rec and ev.putnam_eligible(rec):
pools.setdefault(("hard", "combinatorics"), []).append(rec)
return pools
def finalize(rec: dict, criteria: dict) -> dict:
"""Apply the only allowed edit and add provenance. Nothing else changes the statement."""
src = criteria["sources"][rec["origin"]]
out = {"id": rec["id"], "source": src["name"], "source_commit": src["commit"], "source_id": rec["id"],
"source_split": rec["source_split"], "license": src["license"], "informal": rec["informal"],
"header": rec["header"]}
if rec.get("tags"):
out["source_tags"] = rec["tags"]
converted = ev.convert_big_operators(rec["formal_statement"])
out["formal_statement"] = converted
if converted != rec["formal_statement"]:
out["formal_statement_original"] = rec["formal_statement"]
out["converted"] = ev.CONVERSION_MARK
out["statement"] = rec["header"] + converted
return out
def check_compiles(runner: LeanRunner, rec: dict) -> str | None:
"""None if the statement parses and compiles with `sorry`, else the reason it does not."""
try:
thm = parse_theorem(rec["statement"], header=rec["header"])
except ValueError as exc:
return f"parse_theorem: {exc}"
result = runner.verify(f"{rec['header']}{rec['formal_statement'].rstrip()} sorry\n", thm.name)
if compiles(result):
return None
if result.status != "finished": # a slow machine must not change the set
raise SystemExit(f"{rec['id']}: Lean did not finish ({result.status}); nothing written")
errors = " | ".join(m.text for m in result.errors)
return f"does not compile with sorry ({result.status}): {errors[:300]}"
def reserved_minimum(criteria: dict, pools: dict) -> dict[tuple[str, str], int]:
reserved = set(criteria["reserved_dev"])
return {cell: n for cell, pool in pools.items() if (n := sum(r["id"] in reserved for r in pool))}
def build(raw: Path, criteria: dict, runner: LeanRunner) -> tuple[list[dict], list[dict]]:
seed = criteria["seed"]
pools = load_pools(raw, criteria)
manual = criteria["discards"] or {}
ev.check_discards(manual)
reserved = set(criteria["reserved_dev"])
dev_n = ev.dev_counts(criteria["grid"], criteria["total_dev"], seed, reserved_minimum(criteria, pools))
chosen: list[dict] = []
discarded: list[dict] = []
for difficulty, row in criteria["grid"].items():
for area, target in row.items():
pool = {r["id"]: r for r in pools.get((difficulty, area), [])}
if not target:
continue
order = [i for i in criteria["reserved_dev"] if i in pool]
order += ev.seeded_order([i for i in pool if i not in reserved], seed)
cell: list[dict] = []
for problem_id in order:
if len(cell) == target:
break
rec = finalize(pool[problem_id], criteria)
if problem_id in manual:
reason, stage = manual[problem_id], "fidelity_review"
else:
reason, stage = check_compiles(runner, rec), "compile_with_sorry"
if reason:
discarded.append({"id": problem_id, "difficulty": difficulty, "area": area,
"stage": stage, "reason": reason})
continue
cell.append({**rec, "area": area, "difficulty": difficulty})
if len(cell) < target:
raise SystemExit(f"{difficulty}/{area}: only {len(cell)} of {target} problems available")
splits = ev.assign_split([r["id"] for r in cell], dev_n[(difficulty, area)], reserved, seed)
chosen += [{**r, "split": splits[r["id"]], "seed": seed} for r in cell]
return chosen, discarded
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--raw", type=Path, default=ROOT / "data" / "raw")
parser.add_argument("--criteria", type=Path, default=ROOT / "data" / "curation" / "criteria.yaml")
parser.add_argument("--out", type=Path, default=ROOT / "data" / "eval.jsonl")
args = parser.parse_args()
criteria = yaml.safe_load(args.criteria.read_text(encoding="utf-8"))
with LeanRunner() as runner:
rows, discarded = build(args.raw, criteria, runner)
lean_info = runner.run_info()
args.out.write_text("".join(json.dumps(r, ensure_ascii=False) + "\n" for r in rows), encoding="utf-8")
log = args.criteria.parent / "discarded.json"
log.write_text(json.dumps(discarded, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
info = args.criteria.parent / "build_info.json"
info.write_text(json.dumps(lean_info, ensure_ascii=False, indent=1, default=str) + "\n", encoding="utf-8")
json.dump({"problems": len(rows), "discarded": len(discarded)}, sys.stdout)
return 0
if __name__ == "__main__":
raise SystemExit(main())