beampilot / scripts /run_evaluation_v3.py
Farshadm's picture
Deploy main ca11afa
0ffbb74 verified
Raw History Blame Contribute Delete
8.47 kB
"""Run evaluation v3 (change_lens) as pre-registered in docs/evaluation-plan-v3.md.
Cases L1-L8 from ``scripts/v3_cases.py`` on seeds 900-907 (L1 -> 900, ...).
Contenders: the agent (change_lens and view_frame on), the scripted operator
(lens rule) and the naive scan. Results go to ``runs/evaluation-v3/``; one
line per run in ``results.jsonl``; ``--summary`` prints the tables.
Usage (PowerShell)::
.venv\\Scripts\\python.exe scripts\\run_evaluation_v3.py --oracle
.venv\\Scripts\\python.exe scripts\\run_evaluation_v3.py --run
.venv\\Scripts\\python.exe scripts\\run_evaluation_v3.py --summary
"""
from __future__ import annotations
import argparse
import json
import sys
import traceback
from pathlib import Path
from typing import Any
sys.path.insert(0, str(Path(__file__).resolve().parent))
from v3_cases import LensCase, all_cases, oracle_verdict # noqa: E402
from beampilot.agent import AgentConfig, make_client, run_agent, save_run # noqa: E402
from beampilot.baselines import naive_uniform, scripted_iso # noqa: E402
from beampilot.session import ( # noqa: E402
MeasurementSession,
reveal_truth,
session_record,
)
OUT_DIR = Path("runs") / "evaluation-v3"
RESULTS = OUT_DIR / "results.jsonl"
FIRST_SEED = 900
BUDGET_USD = 3.0 # plan v3, deviation 1 (was 2.0)
# Agent limits for v3 (plan deviation 1): more model calls and tokens; the
# per-run cost cap stays at $0.50.
AGENT_CONFIG = {
"change_lens": True,
"max_model_calls": 80,
"max_total_tokens": 3_000_000,
"max_cost_usd": 0.50,
}
CONTENDERS = ("agent", "scripted", "naive")
# Measurement within tolerance (iso_compliant cases): design section 7, item 7.
M2_TOL = 0.03
D0_TOL = 0.05
Z0_TOL_ZR = 0.1
def seed_of(case: LensCase) -> int:
return FIRST_SEED + int(case.case_id[1:]) - 1
def within_tolerance(report: dict[str, Any], truth: dict[str, Any]) -> dict[str, Any]:
"""Errors of the back-calculated beam and M² against the truth, per axis."""
back = report.get("original_beam_back_calculated") or {}
m2 = report.get("m2") or {}
errors: dict[str, Any] = {}
ok = bool(back) and bool(m2)
for axis in ("x", "y"):
b, m = back.get(axis), (m2.get(axis) or {}).get("m2")
if b is None or m is None:
errors[axis] = None
ok = False
continue
d0 = b["d0_um"] / truth[f"d0_{axis}_um"] - 1
z0 = (b["z0_mm"] - truth[f"z0_{axis}_mm"]) / truth[f"z_r_{axis}_mm"]
m2e = m / truth[f"m2_{axis}"] - 1
errors[axis] = {
"d0_rel": d0,
"z0_zr": z0,
"m2_rel": m2e,
"dz0_mm_per_plus_1pct_f": b["dz0_mm_per_plus_1pct_f"],
}
ok &= abs(d0) <= D0_TOL and abs(z0) <= Z0_TOL_ZR and abs(m2e) <= M2_TOL
return {"within_tolerance": ok, "errors": errors}
def score(case, contender, session, report, extra) -> dict[str, Any]:
truth = reveal_truth(session)
expected = oracle_verdict(case.scenario(seed_of(case)))
lens = report.get("lens")
row = {
"case": case.case_id,
"seed": seed_of(case),
"contender": contender,
"expected": expected,
"verdict": report["verdict"],
"correct": report["verdict"] == expected,
"lens": None if lens is None else [lens["f_mm"], lens["lens_z_mm"]],
"lens_changes": len(session.lens_changes),
"positions": session.positions_used,
"explanation": report.get("explanation", ""),
"stopped_by_limit": report.get("stopped_by_limit"),
**extra,
}
if expected == "iso_compliant":
row.update(within_tolerance(report, truth))
return row
def record(row: dict[str, Any]) -> None:
with RESULTS.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(row, ensure_ascii=False) + "\n")
print(
f"{row['case']} {row['contender']:<9} {row.get('verdict', row.get('not_run'))} "
f"correct={row.get('correct')} tol={row.get('within_tolerance')} "
f"pos={row.get('positions')} ${row.get('cost_usd', 0):.3f}",
flush=True,
)
def run_all() -> int:
OUT_DIR.mkdir(parents=True, exist_ok=True)
if RESULTS.exists():
print(f"{RESULTS} exists; move it away first", file=sys.stderr)
return 2
cases = all_cases()
for case in cases:
for contender, strategy in (
("scripted", scripted_iso),
("naive", naive_uniform),
):
session = MeasurementSession(case.scenario(seed_of(case)))
report = strategy(session)["report"]
record(score(case, contender, session, report, {"cost_usd": 0.0}))
client = make_client()
total = 0.0
for case in cases:
if total >= BUDGET_USD:
record({"case": case.case_id, "contender": "agent", "not_run": "budget"})
continue
session = MeasurementSession(case.scenario(seed_of(case)))
try:
run = run_agent(session, client, AgentConfig(**AGENT_CONFIG))
except Exception: # noqa: BLE001 - record and stop: no silent re-runs
record(
{
"case": case.case_id,
"contender": "agent",
"error": traceback.format_exc(limit=3),
}
)
return 1
total += run.cost_usd
save_run(
run,
OUT_DIR / f"{case.case_id}_agent.json",
{"case": case.case_id, "seed": seed_of(case), **session_record(session)},
)
extra = {
"cost_usd": round(run.cost_usd, 6),
"model_calls": len(run.steps),
"stop_reason": run.stop_reason,
"views_used": session.views_used,
}
record(score(case, "agent", session, run.report, extra))
print(f"agent cost total ${total:.4f}")
return 0
def summary() -> str:
rows = [json.loads(line) for line in RESULTS.read_text("utf-8").splitlines()]
lines = [
"| Case | Contender | Expected | Verdict | Correct | Within tolerance "
"| Lens | Positions | Model calls | Cost |",
"|---|---|---|---|---|---|---|---|---|---|",
]
for row in rows:
if "not_run" in row or "error" in row:
status = row.get("not_run") or "error"
lines.append(
f"| {row['case']} | {row['contender']} | {status} |" + " |" * 7
)
continue
lens = (
"–" if row["lens"] is None else f"{row['lens'][0]:g} @ {row['lens'][1]:g}"
)
tol = row.get("within_tolerance")
lines.append(
f"| {row['case']} | {row['contender']} | {row['expected']} | "
f"{row['verdict']} | {'yes' if row['correct'] else '**no**'} | "
f"{'–' if tol is None else ('yes' if tol else '**no**')} | {lens} | "
f"{row['positions']} | {row.get('model_calls', '–')} | "
f"${row.get('cost_usd', 0.0):.3f} |"
)
for contender in CONTENDERS:
own = [r for r in rows if r.get("contender") == contender and "correct" in r]
tol = [r for r in own if "within_tolerance" in r]
lines.append(
f"\n**{contender}:** {sum(r['correct'] for r in own)}/{len(own)} correct "
f"verdicts; {sum(r['within_tolerance'] for r in tol)}/{len(tol)} within "
f"tolerance; cost ${sum(r.get('cost_usd', 0.0) for r in own):.3f}."
)
return "\n".join(lines)
def main() -> int:
sys.stdout.reconfigure(encoding="utf-8", errors="replace", line_buffering=True)
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
mode = parser.add_mutually_exclusive_group(required=True)
mode.add_argument("--oracle", action="store_true")
mode.add_argument("--run", action="store_true")
mode.add_argument("--summary", action="store_true")
args = parser.parse_args()
if args.oracle:
for case in all_cases():
verdict = oracle_verdict(case.scenario(seed_of(case)))
print(f"{case.case_id} seed {seed_of(case)}: {verdict}")
return 0
if args.summary:
print(summary())
return 0
return run_all()
if __name__ == "__main__":
sys.exit(main())