formal-mas / scripts /build_reference_set.py
NicolasT08's picture
Formal-MAS source code
dfe1826 verified
Raw History Blame Contribute Delete
5.54 kB
"""Build the retrieval reference set Q (Phase 1.2) from the pinned Mathlib environment.
1. Run scripts/extract_proof_consts.lean with `lake env lean` (offline data building,
CLAUDE.md §7) over the project-area modules → data/index/proof_consts.jsonl (~8 min).
2. Derive gold lemmas, select ~200 balanced queries and split them 2/3 dev, 1/3 test.
Outputs (data/retrieval_eval/): q_dev.jsonl, q_test.jsonl, infra_constants.json, meta.json.
Also writes data/index/corpus_eval.jsonl: the corpus without the Q theorems, from which the
evaluation index is built (the queries must not retrieve themselves).
Usage:
python scripts/build_reference_set.py [--size 200] [--seed 2026] [--skip-extract]
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
from formal_mas.retrieval.corpus import AREA_PREFIXES, SourceIndex, load_jsonl # noqa: E402
from formal_mas.retrieval.reference_set import ( # noqa: E402
PROJECT_AREAS,
build_candidates,
infrastructure,
select,
split,
summary,
)
EXTRACT_SCRIPT = ROOT / "scripts" / "extract_proof_consts.lean"
def run_extraction(lean_project: Path, out: Path) -> None:
"""Dump proof-term constants of the project-area modules. Raises if Lean fails."""
out.parent.mkdir(parents=True, exist_ok=True)
prefixes = sorted({p for area in PROJECT_AREAS for p in AREA_PREFIXES[area]})
env = {**os.environ, "FORMAL_MAS_CONSTS_OUT": str(out.resolve()),
"FORMAL_MAS_MODULE_PREFIXES": ",".join(prefixes)}
proc = subprocess.run(
["lake", "env", "lean", str(EXTRACT_SCRIPT)], cwd=lean_project, env=env,
capture_output=True, text=True, encoding="utf-8", errors="replace",
)
if proc.returncode != 0:
raise subprocess.CalledProcessError(proc.returncode, proc.args, proc.stdout, proc.stderr)
def write_jsonl(rows, path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8", newline="\n") as f:
for r in rows:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
def main(argv: list[str] | None = None) -> None:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--lean-project", type=Path, default=ROOT / "lean_project")
parser.add_argument("--index-dir", type=Path, default=ROOT / "data" / "index")
parser.add_argument("--out", type=Path, default=ROOT / "data" / "retrieval_eval")
parser.add_argument("--size", type=int, default=200)
parser.add_argument("--seed", type=int, default=2026)
parser.add_argument("--infra-pct", type=float, default=1.0)
parser.add_argument("--skip-extract", action="store_true", help="reuse proof_consts.jsonl")
args = parser.parse_args(argv)
consts_path = args.index_dir / "proof_consts.jsonl"
if not args.skip_extract:
run_extraction(args.lean_project, consts_path)
corpus_rows = load_jsonl(args.index_dir / "corpus.jsonl")
corpus = {r["name"]: r for r in corpus_rows}
entries = load_jsonl(consts_path)
# Population for infrastructure frequencies: hand-written theorems of the project areas.
population = [e for e in entries
if e["name"] in corpus and corpus[e["name"]]["origin"] == "manual"
and corpus[e["name"]]["area"] in PROJECT_AREAS]
infra = infrastructure(population, corpus, args.infra_pct)
sources = SourceIndex(args.lean_project / ".lake" / "packages" / "mathlib")
candidates = build_candidates(population, corpus, set(infra), sources)
items = split(select(candidates, args.size, args.seed), args.seed)
for part in ("dev", "test"):
write_jsonl([q.to_json() for q in items if q.split == part], args.out / f"q_{part}.jsonl")
(args.out / "infra_constants.json").write_text(json.dumps({
"threshold_pct": args.infra_pct,
"population": len(population),
"constants": [{"name": c, "pct": round(p, 3)} for c, p in infra.items()],
}, indent=2, ensure_ascii=False), encoding="utf-8")
excluded = {q.name for q in items}
write_jsonl((r for r in corpus_rows if r["name"] not in excluded), args.index_dir / "corpus_eval.jsonl")
cand_by = {a: {"concrete": sum(1 for q in candidates if q.area == a and q.concrete),
"abstract": sum(1 for q in candidates if q.area == a and not q.concrete)}
for a in PROJECT_AREAS}
stats = summary(items)
meta = {
"created_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"),
"mathlib_commit": json.loads((args.index_dir / "corpus_meta.json").read_text(encoding="utf-8")).get("mathlib_commit"),
"seed": args.seed,
"size": args.size,
"gold_method": "c: proof-term constants in corpus, minus Mathlib.Tactic and infrastructure, "
"whose last name component is written in the proof (gold_b = method b)",
"gold_range": [1, 10],
"infra_pct": args.infra_pct,
"population": len(population),
"candidates": cand_by,
"corpus_eval_size": len(corpus_rows) - len(excluded),
"q": stats,
}
(args.out / "meta.json").write_text(json.dumps(meta, indent=2, ensure_ascii=False), encoding="utf-8")
print(json.dumps({"candidates": cand_by, **stats}, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()