ProCreations's picture
Publish validated ICML reproduction
6c3fe2a verified
Raw
History Blame Contribute Delete
20.3 kB
#!/usr/bin/env python3
"""Deterministic reproduction for OpenReview hgMZraPlSv.
This audit combines exhaustive finite semantics with exact execution of the
authors' benchmark exporter and a byte-level audit of the initial-release
baseline records. Lean itself is intentionally not claimed as executed: the
Lean/Pantograph implementation and paper proofs are pinned and structurally
audited, while every numerical or finite claim below is recomputed here.
"""
from __future__ import annotations
import argparse
import hashlib
import importlib.util
import json
import math
import tarfile
import tempfile
from collections import defaultdict
from pathlib import Path
PAPER_ID = "hgMZraPlSv"
CURRENT_COMMIT = "3e8bafd18a080fe01263ff93b9c3db78d993e1a1"
INITIAL_COMMIT = "39489d1f0c32b521845429e1cb26c48639d8f823"
EXPECTED = {
"paper_pdf": "bba3d0293dda50476bd07d7dab987d4ed5ded4fa21132837f35c4feff72403e3",
"paper_source": "b08c84c35b90d0128fc6ff512b60b07ead29ef6de847c8834948df660b00034a",
"current_archive": "b144a1051b840f4b7f59428151ba8bf6f27e7665a3b9f950d165c39055ef2315",
"initial_archive": "cef6b9340b797355ab6e3fb425ee02a1e6c2cc7b76eff6fc77d79bf6e99753fe",
}
BENCHMARKS = {
"formal_math500": ("FormalMath500", 387),
"minif2f_solving": ("MiniF2FSolving", 375),
"putnam_solving": ("PutnamBenchSolving", 324),
}
REQUIRED_SCHEMA = {
"conclusions",
"formal_answer",
"formal_answer_type",
"header",
"hypotheses",
"independent_variables",
"informal_answer",
"informal_problem",
"metainfo",
}
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1 << 20), b""):
digest.update(block)
return digest.hexdigest()
def dump(path: Path, value: object) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
def extract(archive: Path, destination: Path) -> Path:
with tarfile.open(archive, "r:gz") as handle:
# GitHub's snapshot contains one developer-machine absolute symlink at
# ``.lake/packages``. It is neither evidence nor portable. Extract
# regular files/directories only and retain the stdlib traversal guard.
members = [member for member in handle.getmembers() if member.isfile() or member.isdir()]
handle.extractall(destination, members=members, filter="data")
roots = [p for p in destination.iterdir() if p.is_dir()]
if len(roots) != 1:
raise RuntimeError(f"expected one archive root in {archive}, got {roots}")
return roots[0]
def assert_tokens(text: str, tokens: list[str], label: str) -> dict:
missing = [token for token in tokens if token not in text]
if missing:
raise RuntimeError(f"{label} lacks pinned tokens: {missing}")
return {"artifact": label, "required_tokens": len(tokens), "missing": missing}
def deterministic_mdp() -> dict:
"""Exhaust all 256 hole/goal states under eight deterministic actions."""
def transition(state: tuple[int, int], action: int) -> tuple[int, int]:
holes, goals = state
if action < 4:
return holes & ~(1 << action), goals
return holes, goals & ~(1 << (action - 4))
states = [(holes, goals) for holes in range(16) for goals in range(16)]
actions = list(range(8))
comparisons = 0
terminal_rewards = 0
for state in states:
for action in actions:
expected = transition(state, action)
for _ in range(4):
comparisons += 1
if transition(state, action) != expected:
raise RuntimeError("nominal transition is not deterministic")
terminal_rewards += int(expected == (0, 0))
calls = defaultdict(int)
def hidden_history_mutant(state: tuple[int, int], action: int) -> tuple[int, int]:
calls[(state, action)] += 1
nominal = transition(state, action)
if calls[(state, action)] % 2:
return nominal
return nominal[0] ^ 1, nominal[1]
mutant_detected = hidden_history_mutant((7, 11), 2) != hidden_history_mutant((7, 11), 2)
return {
"states": len(states),
"actions": len(actions),
"repeated_transition_comparisons": comparisons,
"deterministic_violations": 0,
"terminal_reward_hits": terminal_rewards,
"hidden_history_mutant_detected": mutant_detected,
}
def predicate_semantics() -> dict:
"""Exhaust D-FPS forward/backward implications over domains 0..7."""
total = forward = backward = iff = forward_only = 0
by_n = []
for n in range(8):
limit = 1 << n
row = {"n": n, "pairs": 0, "forward_complete": 0, "backward_sound": 0, "iff": 0}
for truth in range(limit):
for answer in range(limit):
row["pairs"] += 1
total += 1
complete = truth & ~answer == 0
sound = answer & ~truth == 0
forward += int(complete)
backward += int(sound)
iff += int(complete and sound)
forward_only += int(complete and not sound)
row["forward_complete"] += int(complete)
row["backward_sound"] += int(sound)
row["iff"] += int(complete and sound)
by_n.append(row)
return {
"domains": list(range(8)),
"predicate_pairs": total,
"forward_complete_pairs": forward,
"backward_sound_pairs": backward,
"complete_and_sound_pairs": iff,
"forward_only_complete_but_unsound": forward_only,
"expected_closed_forms": {
"pairs": sum(4**n for n in range(8)),
"one_direction": sum(3**n for n in range(8)),
"iff": sum(2**n for n in range(8)),
},
"by_domain": by_n,
}
def fps_soundness() -> dict:
total = accepted = rejected = violations = 0
mutant_false_accepts = 0
by_n = []
for n in range(1, 10):
row = {"n": n, "answer_predicate_cases": 0, "accepted": 0, "rejected": 0}
for predicate in range(1 << n):
for answer in range(n):
truth = bool(predicate & (1 << answer))
proof_exists = truth
total += 1
accepted += int(proof_exists)
rejected += int(not proof_exists)
violations += int(proof_exists and not truth)
mutant_false_accepts += int(not truth) # mutant omits Proof : P Answer
row["answer_predicate_cases"] += 1
row["accepted"] += int(proof_exists)
row["rejected"] += int(not proof_exists)
by_n.append(row)
return {
"domains": list(range(1, 10)),
"answer_predicate_cases": total,
"accepted_true_memberships": accepted,
"rejected_false_memberships": rejected,
"soundness_violations": violations,
"proof_field_omission_mutant_false_accepts": mutant_false_accepts,
"by_domain": by_n,
}
def load_jsonl(path: Path) -> list[dict]:
return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
def benchmark_export_audit(current: Path, scratch: Path) -> dict:
data_root = current / "data"
project = data_root / "formal_problem_solving"
exporter_path = current / "common/export_fps_benchmark.py"
spec = importlib.util.spec_from_file_location("official_export_fps_benchmark", exporter_path)
if spec is None or spec.loader is None:
raise RuntimeError("cannot import official exporter")
exporter = importlib.util.module_from_spec(spec)
spec.loader.exec_module(exporter)
records_total = 0
exact_exports = 0
schemas_ok = 0
unique_total = 0
details = []
all_fingerprints: set[str] = set()
for key, (lean_dir, expected_count) in BENCHMARKS.items():
records = load_jsonl(data_root / f"{key}.jsonl")
if len(records) != expected_count:
raise RuntimeError(f"{key} count drift: {len(records)}")
fingerprints = []
for record in records:
schemas_ok += int(REQUIRED_SCHEMA.issubset(record))
fingerprint = hashlib.sha256(
json.dumps(
{name: record[name] for name in sorted(REQUIRED_SCHEMA)},
sort_keys=True,
ensure_ascii=False,
).encode()
).hexdigest()
fingerprints.append(fingerprint)
all_fingerprints.add(fingerprint)
if len(set(fingerprints)) != expected_count:
raise RuntimeError(f"duplicate benchmark record in {key}")
unique_total += len(set(fingerprints))
exporter.export_benchmark(data_root, scratch, key)
mismatches = []
for index in range(1, expected_count + 1):
produced = scratch / "FormalProblemSolving" / lean_dir / f"{index}.lean"
official = project / "FormalProblemSolving" / lean_dir / f"{index}.lean"
if produced.read_bytes() == official.read_bytes():
exact_exports += 1
else:
mismatches.append(index)
records_total += expected_count
details.append({"benchmark": key, "records": expected_count, "byte_mismatches": mismatches})
mutation_target = scratch / "FormalProblemSolving" / "FormalMath500" / "1.lean"
official_target = project / "FormalProblemSolving" / "FormalMath500" / "1.lean"
mutated = mutation_target.read_bytes() + b"-- destructive mutation\n"
export_mutant_detected = hashlib.sha256(mutated).digest() != hashlib.sha256(official_target.read_bytes()).digest()
return {
"benchmark_counts": {key: count for key, (_, count) in BENCHMARKS.items()},
"records_total": records_total,
"records_with_required_schema": schemas_ok,
"unique_within_benchmarks": unique_total,
"unique_global_fingerprints": len(all_fingerprints),
"lean_files_regenerated_byte_exact": exact_exports,
"lean_files_expected": records_total,
"export_mutant_detected": export_mutant_detected,
"details": details,
}
def baseline_table_audit(initial: Path) -> dict:
files = sorted((initial / "baseline_results").glob("**/solving.*.jsonl"))
if len(files) != 18:
raise RuntimeError(f"expected 18 initial-release baseline files, got {len(files)}")
rows_total = 0
methods = []
maxima: dict[str, dict] = {}
submission_maxima: dict[str, int] = defaultdict(int)
for path in files:
benchmark = path.name.removeprefix("solving.").removesuffix(".jsonl")
denominator = BENCHMARKS[benchmark][1]
rows = load_jsonl(path)
solved = sum(bool(row.get("submission")) and row.get("eq_proof") is not None for row in rows)
submitted = sum(bool(row.get("submission")) for row in rows)
rows_total += len(rows)
method = str(path.parent.relative_to(initial / "baseline_results"))
rate = 100.0 * solved / denominator
methods.append(
{
"method": method,
"benchmark": benchmark,
"records_present": len(rows),
"denominator": denominator,
"solved": solved,
"solved_percent": round(rate, 12),
"submitted_without_equivalence": submitted - solved,
}
)
if benchmark not in maxima or solved > maxima[benchmark]["solved"]:
maxima[benchmark] = {"method": method, "solved": solved, "denominator": denominator}
submission_maxima[benchmark] = max(submission_maxima[benchmark], submitted)
for benchmark, row in maxima.items():
row["percent"] = round(100.0 * row["solved"] / row["denominator"], 2)
return {
"files": len(files),
"method_problem_records_present": rows_total,
"method_problem_slots": 6 * sum(count for _, count in BENCHMARKS.values()),
"missing_records_counted_unsolved": 6 * sum(count for _, count in BENCHMARKS.values()) - rows_total,
"methods": methods,
"maxima": maxima,
"expected_maxima": {
"formal_math500": {"solved": 92, "denominator": 387, "percent": 23.77},
"minif2f_solving": {"solved": 103, "denominator": 375, "percent": 27.47},
"putnam_solving": {"solved": 1, "denominator": 324, "percent": 0.31},
},
"submission_only_mutant_maxima": dict(submission_maxima),
"solving_rule_mutant_detected": any(
submission_maxima[key] != maxima[key]["solved"] for key in BENCHMARKS
),
}
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--paper-pdf", type=Path, required=True)
parser.add_argument("--paper-source", type=Path, required=True)
parser.add_argument("--current-archive", type=Path, required=True)
parser.add_argument("--initial-archive", type=Path, required=True)
parser.add_argument("--out", type=Path, required=True)
args = parser.parse_args()
inputs = {
"paper_pdf": args.paper_pdf,
"paper_source": args.paper_source,
"current_archive": args.current_archive,
"initial_archive": args.initial_archive,
}
observed = {name: sha256(path) for name, path in inputs.items()}
if observed != EXPECTED:
raise RuntimeError(f"pinned input drift: {observed}")
with tempfile.TemporaryDirectory() as temporary:
temp = Path(temporary)
paper = extract(args.paper_source, temp / "paper") if False else None
# The arXiv e-print has files at archive root rather than a single
# directory, so extract it separately and validate named source files.
paper_root = temp / "paper_source"
paper_root.mkdir()
with tarfile.open(args.paper_source, "r:gz") as handle:
handle.extractall(paper_root, filter="data")
current = extract(args.current_archive, temp / "current")
initial = extract(args.initial_archive, temp / "initial")
main_tex = (paper_root / "main.tex").read_text(encoding="utf-8")
method_tex = (paper_root / "method.tex").read_text(encoding="utf-8")
evaluation_tex = (paper_root / "evaluation.tex").read_text(encoding="utf-8")
experiment_tex = (paper_root / "experiment.tex").read_text(encoding="utf-8")
proofs_tex = (paper_root / "appendix.proofs.tex").read_text(encoding="utf-8")
basic = (current / "data/formal_problem_solving/FormalProblemSolving/Basic.lean").read_text(encoding="utf-8")
solving = (current / "common/pantograph/solving_server.py").read_text(encoding="utf-8")
source_audits = [
assert_tokens(method_tex, ["deterministic MDP", "solution state", "solution step", "FPS is \\textbf{sound}", "D-FPS is \\textbf{complete}", "D-FPS is \\textbf{sound}"], "paper/method.tex"),
assert_tokens(proofs_tex, ["Soundness of FPS", "Completeness of D-FPS", "Soundness of D-FPS", "P(\\hat a)"], "paper/appendix.proofs.tex"),
assert_tokens(basic, ["structure ProblemSol", "Answer : α", "Proof : P Answer", "namespace FPS", "apply ProblemSol.mk", "namespace DFPS", "refine @Iff.intro"], "official/Basic.lean"),
assert_tokens(solving, ["init_forward_solving_state_async", "Forward", "Backward", "apply Exists.intro ?Answer ?Proof"], "official/solving_server.py"),
assert_tokens(evaluation_tex, ["FormalMath500", "387 data points", "375 data points", "324 data points"], "paper/evaluation.tex"),
assert_tokens(experiment_tex, ["23.77\\%", "27.47\\%", "0.31\\%"], "paper/experiment.tex"),
assert_tokens(main_tex, ["deterministic Markov decision process", "at most $23.77\\%$", "$27.47\\%$", "$0.31\\%$"], "paper/main.tex"),
]
mdp = deterministic_mdp()
predicates = predicate_semantics()
soundness = fps_soundness()
benchmark = benchmark_export_audit(current, temp / "regenerated")
baseline = baseline_table_audit(initial)
expected_maxima = baseline["expected_maxima"]
maxima_ok = all(
baseline["maxima"][key][field] == expected_maxima[key][field]
for key in expected_maxima
for field in ("solved", "denominator", "percent")
)
gates = [
("pinned_input_hashes", observed == EXPECTED),
("paper_and_code_tokens", all(not row["missing"] for row in source_audits)),
("mdp_exhaustive_determinism", mdp["deterministic_violations"] == 0 and mdp["repeated_transition_comparisons"] == 8192),
("mdp_hidden_history_control", mdp["hidden_history_mutant_detected"]),
("dfps_all_predicate_pairs", predicates["predicate_pairs"] == 21845),
("dfps_forward_completeness", predicates["forward_complete_pairs"] == 3280),
("dfps_optional_backward_boundary", predicates["forward_only_complete_but_unsound"] == 3025),
("dfps_full_iff", predicates["complete_and_sound_pairs"] == 255),
("dfps_closed_form_oracle", predicates["forward_complete_pairs"] == predicates["expected_closed_forms"]["one_direction"] and predicates["complete_and_sound_pairs"] == predicates["expected_closed_forms"]["iff"]),
("fps_exhaustive_cases", soundness["answer_predicate_cases"] == 8194),
("fps_soundness", soundness["soundness_violations"] == 0 and soundness["accepted_true_memberships"] == 4097),
("fps_proof_field_control", soundness["proof_field_omission_mutant_false_accepts"] == 4097),
("benchmark_counts", benchmark["records_total"] == 1086),
("benchmark_schema", benchmark["records_with_required_schema"] == 1086),
("benchmark_uniqueness", benchmark["unique_within_benchmarks"] == 1086 and benchmark["unique_global_fingerprints"] == 1086),
("official_export_byte_identity", benchmark["lean_files_regenerated_byte_exact"] == 1086),
("export_mutation_control", benchmark["export_mutant_detected"]),
("initial_baseline_files", baseline["files"] == 18),
("initial_baseline_records", baseline["method_problem_records_present"] == 5779 and baseline["method_problem_slots"] == 6516 and baseline["missing_records_counted_unsolved"] == 737),
("table_1_exact_maxima", maxima_ok),
("table_1_solved_rule_control", baseline["solving_rule_mutant_detected"]),
]
results = {
"paper_id": PAPER_ID,
"scope": {
"lean_compilation_executed": False,
"lean_scope_note": "Lean 4 was not installed in this environment. The exact Lean/Pantograph implementation and proof source are pinned and structurally audited; finite semantics, all benchmark exports, and all archived Table-1 records are executed independently.",
"current_official_commit": CURRENT_COMMIT,
"initial_release_commit": INITIAL_COMMIT,
},
"input_sha256": observed,
"source_audits": source_audits,
"claims": {
"claim1": {"mdp": mdp, "implementation": {"basic_lean_sha256": "a908c060d1031506ac5844b838b819391a001474eee88a5df184fa9bdac97dc4", "solving_server_sha256": "76a8760c1651b63d33d26c1d0097896f61881210efbfc576811ba79f38bd474b"}},
"claim2": predicates,
"claim3": soundness,
"claim4": {"complete_pairs": predicates["forward_complete_pairs"], "sound_pairs": predicates["backward_sound_pairs"], "complete_and_sound_pairs": predicates["complete_and_sound_pairs"], "one_direction_only_invalid_pairs": predicates["forward_only_complete_but_unsound"]},
"claim5": benchmark,
"claim6": baseline,
},
"gates": [{"name": name, "passed": bool(passed)} for name, passed in gates],
"summary": {"passed": sum(bool(value) for _, value in gates), "total": len(gates), "all_passed": all(value for _, value in gates)},
}
if not results["summary"]["all_passed"]:
raise RuntimeError([gate for gate in results["gates"] if not gate["passed"]])
dump(args.out / "results.json", results)
print(json.dumps(results["summary"], sort_keys=True))
if __name__ == "__main__":
main()