Download scripts/make_evaluation_summary.py from Farshadm/beampilot: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/spaces/Farshadm/beampilot/resolve/main/scripts/make_evaluation_summary.py
- Command line
-
hf download hf://spaces/Farshadm/beampilot/scripts/make_evaluation_summary.py
-
curl -L -o make_evaluation_summary.py https://huggingface.co/spaces/Farshadm/beampilot/resolve/main/scripts/make_evaluation_summary.py
10.4 kB
| """Write docs/evaluation-summary.md from the v1 and v2 result files. | |
| Every number in the summary is computed here from ``docs/results/*.jsonl`` | |
| (copies of ``runs/evaluation*/results.jsonl`` written by | |
| ``scripts/run_evaluation.py``); nothing is typed by hand. Run again after any | |
| change to the result files:: | |
| .venv\\Scripts\\python.exe scripts\\make_evaluation_summary.py | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import statistics | |
| import sys | |
| from collections.abc import Callable | |
| from pathlib import Path | |
| from typing import Any | |
| REPO = Path(__file__).resolve().parents[1] | |
| OUT = REPO / "docs" / "evaluation-summary.md" | |
| VERSIONS = ("v1", "v2") | |
| RESULT_FILES = { | |
| v: REPO / "docs" / "results" / f"evaluation-{v}.jsonl" for v in VERSIONS | |
| } | |
| CONTENDERS = ("agent", "agent-noview", "scripted", "naive") | |
| # Plans and the commits that fixed them before any run (see the results files). | |
| PLANS = { | |
| "v1": { | |
| "plan": "evaluation-plan.md", | |
| "results": "evaluation-results.md", | |
| "commits": [ | |
| ("d2f445e", "plan"), | |
| ("b3025e9", "deviations 1-7"), | |
| ("9ca92df", "deviation 8 and evaluation script"), | |
| ("cda0f73", "deviation 9"), | |
| ], | |
| "code": "03fb2c1", | |
| }, | |
| "v2": { | |
| "plan": "evaluation-plan-v2.md", | |
| "results": "evaluation-results-v2.md", | |
| "commits": [("75fe382", "plan"), ("3241612", "deviations 1-2")], | |
| "code": "3403a76", | |
| }, | |
| } | |
| GROUPS: dict[str, Callable[[dict[str, Any]], bool]] = { | |
| "A": lambda r: r["group"] == "A", | |
| "B": lambda r: r["group"] == "B", | |
| "C2-C5": lambda r: r["group"] == "C" and not r["seen"], | |
| "C1 (seen)": lambda r: r["group"] == "C" and r["seen"], | |
| } | |
| def load(path: Path) -> list[dict[str, Any]]: | |
| """Result rows of one evaluation (rows that were run and scored).""" | |
| rows = [json.loads(line) for line in path.read_text("utf-8").splitlines()] | |
| return [r for r in rows if "correct" in r] | |
| def _pct(value: float | None) -> str: | |
| return "–" if value is None else f"{value * 100:+.1f} %" | |
| def _tally(rows: list[dict[str, Any]]) -> str: | |
| return f"{sum(r['correct'] for r in rows)}/{len(rows)}" if rows else "–" | |
| def correct_table(data: dict[str, list[dict[str, Any]]]) -> list[str]: | |
| header = "| Contender | " + " | ".join(f"{g}: v1 · v2 · both" for g in GROUPS) | |
| lines = [header + " |", "|---|" + "---|" * len(GROUPS)] | |
| for contender in CONTENDERS: | |
| cells = [] | |
| for pick in GROUPS.values(): | |
| per = [ | |
| [r for r in data[v] if r["contender"] == contender and pick(r)] | |
| for v in VERSIONS | |
| ] | |
| both = per[0] + per[1] | |
| if not both: | |
| cells.append("–") | |
| continue | |
| cells.append(f"{_tally(per[0])} · {_tally(per[1])} · **{_tally(both)}**") | |
| lines.append(f"| {contender} | " + " | ".join(cells) + " |") | |
| return lines | |
| def effort_table(data: dict[str, list[dict[str, Any]]]) -> list[str]: | |
| lines = [ | |
| "| Contender | Version | Runs | Mean positions | Mean model calls | " | |
| "Stopped by a limit | Median / max \\|M² error\\| | Cost |", | |
| "|---|---|---|---|---|---|---|---|", | |
| ] | |
| for contender in CONTENDERS: | |
| for version in VERSIONS: | |
| rows = [r for r in data[version] if r["contender"] == contender] | |
| if not rows: | |
| continue | |
| calls = [r["model_calls"] for r in rows if "model_calls" in r] | |
| limited = [ | |
| r["case"] for r in rows if "limit" in (r.get("stop_reason") or "") | |
| ] | |
| errors = sorted( | |
| abs(r[k]) | |
| for r in rows | |
| for k in ("m2_error_x", "m2_error_y") | |
| if r.get(k) is not None | |
| ) | |
| error = ( | |
| f"{statistics.median(errors) * 100:.1f} % / {errors[-1] * 100:.1f} %" | |
| if errors | |
| else "–" | |
| ) | |
| mean_calls = f"{statistics.mean(calls):.1f}" if calls else "–" | |
| stopped = (", ".join(limited) or "none") if calls else "–" | |
| lines.append( | |
| f"| {contender} | {version} | {len(rows)} | " | |
| f"{statistics.mean(r['positions'] for r in rows):.1f} | " | |
| f"{mean_calls} | {stopped} | {error} | " | |
| f"${sum(r.get('cost_usd', 0.0) for r in rows):.2f} |" | |
| ) | |
| return lines | |
| def violations_table(data: dict[str, list[dict[str, Any]]]) -> list[str]: | |
| cases = sorted({r["case"] for v in VERSIONS for r in data[v] if r["group"] == "B"}) | |
| lines = [ | |
| "| Contender | " + " | ".join(f"{c} v1 · v2" for c in cases) + " | Total |", | |
| "|---|" + "---|" * (len(cases) + 1), | |
| ] | |
| for contender in ("agent", "scripted", "naive"): | |
| cells, total = [], 0 | |
| for case in cases: | |
| counts = [] | |
| for version in VERSIONS: | |
| row = next( | |
| r | |
| for r in data[version] | |
| if r["case"] == case and r["contender"] == contender | |
| ) | |
| counts.append(row["violations"]) | |
| total += row["violations"] | |
| cells.append(f"{counts[0]} · {counts[1]}") | |
| lines.append(f"| {contender} | " + " | ".join(cells) + f" | **{total}** |") | |
| return lines | |
| def failure_table(data: dict[str, list[dict[str, Any]]], case: str) -> list[str]: | |
| lines = [ | |
| "| Version | Contender | Verdict (expected) | Correct | M² err x / y | " | |
| "Views | Fault named |", | |
| "|---|---|---|---|---|---|---|", | |
| ] | |
| for version in VERSIONS: | |
| for contender in ("agent", "agent-noview"): | |
| row = next( | |
| r | |
| for r in data[version] | |
| if r["case"] == case and r["contender"] == contender | |
| ) | |
| named = "yes" if row["fault_noticed"] else "no" | |
| lines.append( | |
| f"| {version} | {contender} | {row['verdict']} ({row['expected']}) | " | |
| f"{'yes' if row['correct'] else '**no**'} | " | |
| f"{_pct(row['m2_error_x'])} / {_pct(row['m2_error_y'])} | " | |
| f"{row.get('views_used', '–')} | {named} |" | |
| ) | |
| return lines | |
| def summary(data: dict[str, list[dict[str, Any]]]) -> str: | |
| agent_runs = sum( | |
| 1 for v in VERSIONS for r in data[v] if r["contender"].startswith("agent") | |
| ) | |
| cost = sum(r.get("cost_usd", 0.0) for v in VERSIONS for r in data[v]) | |
| plan_lines = [] | |
| for version in VERSIONS: | |
| plan = PLANS[version] | |
| commits = ", ".join(f"`{h}` ({what})" for h, what in plan["commits"]) | |
| plan_lines.append( | |
| f"- **{version}:** [{plan['plan']}]({plan['plan']}), pre-registered in " | |
| f"{commits}; run on `main` at `{plan['code']}`. Full results: " | |
| f"[{plan['results']}]({plan['results']})." | |
| ) | |
| lines = [ | |
| "# BeamPilot evaluation summary (v1 + v2)", | |
| "", | |
| "Generated by `scripts/make_evaluation_summary.py` from " | |
| "`docs/results/evaluation-v1.jsonl` and `docs/results/evaluation-v2.jsonl`. " | |
| "No number on this page is typed by hand.", | |
| "", | |
| "## Plans", | |
| "", | |
| *plan_lines, | |
| "", | |
| "Between v1 and v2 only two things changed: the model-call limit (40 → 60) " | |
| "and a prompt asking for parallel tool calls. Seeds differ.", | |
| "", | |
| "## Read this first", | |
| "", | |
| "- **Small N.** Each case ran twice in total: once in v1 and once in v2, with " | |
| "different seeds. A difference of one or two cases is noise, not a measured " | |
| "effect.", | |
| '- **C1 is "seen".** The prompt was changed after the agent failed the same ' | |
| "fault type during development, so C1 is never counted as held out. C2-C5 " | |
| "were held out in v1 and seen in v2.", | |
| "- **The scripted baseline cannot read operator notes, by design.** In " | |
| "group B it runs without the note; its violations are counted below. Group B " | |
| "is therefore not a fair contest.", | |
| "- **The naive scan is a reference only** (10 fixed positions, 1 ms).", | |
| "", | |
| "## Correct verdicts", | |
| "", | |
| "Correct / run, per version and both together. Group C is run twice per " | |
| "version for the agent: with `view_frame` (`agent`) and without " | |
| "(`agent-noview`).", | |
| "", | |
| *correct_table(data), | |
| "", | |
| "## Effort, accuracy and cost", | |
| "", | |
| *effort_table(data), | |
| "", | |
| "## Constraint violations in group B", | |
| "", | |
| "Measurements that broke the operator note (B2: 1 if more than 12 positions " | |
| "were used; B3 has no constraint).", | |
| "", | |
| *violations_table(data), | |
| "", | |
| "## Main failure modes", | |
| "", | |
| "**C1, ghost reflection at every position (seen): saw the ghost but kept " | |
| "the points.** In v1 the agent with `view_frame` named the ghost and gave the " | |
| "right verdict. In v2 it looked at frames only after coverage was met, " | |
| "described the second spot, and still kept those points. Without " | |
| "`view_frame` the agent read the constant `axes_not_aligned` flag as a " | |
| "rotated beam. The wrong runs report a confident, very wrong M².", | |
| "", | |
| *failure_table(data, "C1"), | |
| "", | |
| "**C5, iris clipping the beam over 30-70 mm (no flag fires): never looked.** " | |
| "In no run did the agent look at a frame. It accepted far points inside the " | |
| "iris range and did not act on the rising fit residual, so the verdict was " | |
| "right but M² missed the 3 % tolerance.", | |
| "", | |
| *failure_table(data, "C5"), | |
| "", | |
| f"Total: {agent_runs} agent runs, ${cost:.2f} estimated API cost.", | |
| "", | |
| ] | |
| return "\n".join(lines) | |
| def main() -> int: | |
| data = {v: load(RESULT_FILES[v]) for v in VERSIONS} | |
| OUT.write_text(summary(data), encoding="utf-8", newline="\n") | |
| print(f"wrote {OUT.relative_to(REPO)}") | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |