| |
| """ |
| MCR / MAC for **baseline-export UPPAAL XMLs** (e.g. ``artifacts/baselines/claude/1.xml``). |
| |
| MCR (same spirit as ``aggregate_batch_metrics.py``): |
| fraction of specs S01..S20 where the baseline ``{spec_id}.xml`` exists, is non-empty, |
| and verifyta runs ``A[] not deadlock`` without engine/syntax errors. |
| |
| MAC: |
| For each smoke-OK spec, run **reference-adapted** gold queries from |
| ``datasets/query_translation_eval.csv`` (see ``adapt_csv_gold_to_reference_xml``) |
| on the **baseline** XML. Compare satisfy / not_satisfy to column ``answer`` (T/F). |
| MAC = mean of per-spec accuracies (correct / 5), matching batch convention. |
| |
| Sanity (``--check-gold-model``): |
| On ``datasets/gold_models/S{{id}}/model.xml``, run the **raw** ``ground_query`` |
| from the CSV (identifiers match gold). If verdict ≠ ``answer``, the eval row or gold |
| model is inconsistent — reported as warnings. |
| |
| Usage |
| ----- |
| python scripts/eval_baseline_models_mcr_mac.py --baseline claude |
| python scripts/eval_baseline_models_mcr_mac.py --baseline claude --check-gold-model |
| """ |
| from __future__ import annotations |
|
|
| import argparse |
| import csv |
| import json |
| import re |
| import sys |
| from pathlib import Path |
| from typing import Any |
|
|
| ROOT = Path(__file__).resolve().parents[1] |
| sys.path.insert(0, str(ROOT / "src")) |
|
|
| from frame.evaluation.reference_xml_gold_adapt import adapt_csv_gold_to_reference_xml |
| from frame.pipeline.model_checking_pipeline import ModelCheckingPipeline |
| from frame.pipeline.nl_uppaal_query import parse_verifyta_text_verdict |
|
|
| EVAL_CSV = ROOT / "datasets" / "query_translation_eval.csv" |
| GOLD_MODELS = ROOT / "datasets" / "gold_models" |
| BASELINES = ROOT / "artifacts" / "baselines" |
| N_SPECS = 20 |
|
|
|
|
| def _norm_ws(s: str) -> str: |
| return re.sub(r"\s+", " ", (s or "").strip()) |
|
|
|
|
| def _verdict_tf(v: str | None) -> str: |
| if v == "satisfied": |
| return "T" |
| if v == "not_satisfied": |
| return "F" |
| return "?" |
|
|
|
|
| def _smoke_ok(checker: ModelCheckingPipeline) -> bool: |
| _res, _tr, errs = checker.verify("A[] not deadlock") |
| errors = [e for e in (errs or []) if e and str(e).strip()] |
| return not errors |
|
|
|
|
| def load_rows(path: Path) -> list[dict[str, str]]: |
| with path.open(encoding="utf-8", newline="") as f: |
| return list(csv.DictReader(f)) |
|
|
|
|
| def main() -> int: |
| ap = argparse.ArgumentParser(description=__doc__) |
| ap.add_argument("--baseline", required=True, help="Subfolder under artifacts/baselines (e.g. claude)") |
| ap.add_argument("--csv", type=Path, default=EVAL_CSV) |
| ap.add_argument( |
| "--out-json", |
| type=Path, |
| default=None, |
| help="Optional path to write detailed JSON (per-spec + per-row).", |
| ) |
| ap.add_argument( |
| "--check-gold-model", |
| action="store_true", |
| help="Verify raw ground_query on datasets/gold_models (sanity).", |
| ) |
| args = ap.parse_args() |
|
|
| base_dir = (BASELINES / args.baseline).resolve() |
| if not base_dir.is_dir(): |
| print(f"Not a directory: {base_dir}", file=sys.stderr) |
| return 1 |
|
|
| rows = load_rows(args.csv.resolve()) |
| by_spec: dict[int, list[dict[str, str]]] = {} |
| for r in rows: |
| by_spec.setdefault(int(r["spec_id"]), []).append(r) |
|
|
| gold_checkers: dict[int, ModelCheckingPipeline] = {} |
| gold_mismatch = 0 |
| if args.check_gold_model: |
| for sid in range(1, N_SPECS + 1): |
| gp = GOLD_MODELS / f"S{sid:02d}" / "model.xml" |
| if not gp.is_file(): |
| print(f"[gold-model] missing {gp}", file=sys.stderr) |
| continue |
| gold_checkers[sid] = ModelCheckingPipeline(str(gp)) |
|
|
| for r in rows: |
| sid = int(r["spec_id"]) |
| if sid not in gold_checkers: |
| continue |
| gq = _norm_ws(r.get("ground_query") or "") |
| exp = (r.get("answer") or "").strip().upper() |
| res, _t, errs = gold_checkers[sid].verify(gq) |
| v = parse_verifyta_text_verdict(res, errors=errs) |
| got = _verdict_tf(v) |
| if got != exp: |
| gold_mismatch += 1 |
| print( |
| f"[gold-model MISMATCH] S{sid:02d} expected {exp} got {got} " |
| f"(query …{gq[:60]}…)", |
| file=sys.stderr, |
| ) |
|
|
| n_compile = 0 |
| per_spec_scores: list[tuple[int, str, float, int, int]] = [] |
| detail_specs: list[dict[str, Any]] = [] |
|
|
| for sid in range(1, N_SPECS + 1): |
| xm = base_dir / f"{sid}.xml" |
| spec_rows = by_spec.get(sid, []) |
| spec_name = f"S{sid:02d}" |
|
|
| if not xm.is_file() or xm.stat().st_size == 0: |
| detail_specs.append( |
| { |
| "spec_id": sid, |
| "xml": str(xm), |
| "smoke_ok": False, |
| "reason": "missing or empty XML", |
| "queries": [], |
| } |
| ) |
| continue |
|
|
| bl_ch = ModelCheckingPipeline(str(xm)) |
| ok = _smoke_ok(bl_ch) |
| if not ok: |
| detail_specs.append( |
| { |
| "spec_id": sid, |
| "xml": str(xm), |
| "smoke_ok": False, |
| "reason": "smoke verifyta error", |
| "queries": [], |
| } |
| ) |
| continue |
|
|
| n_compile += 1 |
| q_correct = 0 |
| q_total = 0 |
| q_rows_out: list[dict[str, Any]] = [] |
|
|
| for r in spec_rows: |
| raw_gold = _norm_ws(r.get("ground_query") or "") |
| adapted = adapt_csv_gold_to_reference_xml(raw_gold) |
| exp = (r.get("answer") or "").strip().upper() |
| res, _t, errs = bl_ch.verify(adapted) |
| v = parse_verifyta_text_verdict(res, errors=errs) |
| got = _verdict_tf(v) |
| errors = [e for e in (errs or []) if e and str(e).strip()] |
| ok_row = got == exp and got in ("T", "F") |
| if ok_row: |
| q_correct += 1 |
| q_total += 1 |
| q_rows_out.append( |
| { |
| "nl_query": (r.get("nl_query") or "")[:200], |
| "ground_query_adapted": adapted, |
| "expected": exp, |
| "verdict": got, |
| "correct": ok_row, |
| "verifyta_errors": errors[:3], |
| } |
| ) |
|
|
| acc = q_correct / q_total if q_total else 0.0 |
| per_spec_scores.append((sid, spec_name, acc, q_correct, q_total)) |
| detail_specs.append( |
| { |
| "spec_id": sid, |
| "xml": str(xm), |
| "smoke_ok": True, |
| "q_correct": q_correct, |
| "q_total": q_total, |
| "accuracy": acc, |
| "queries": q_rows_out, |
| } |
| ) |
|
|
| mcr = n_compile / N_SPECS |
| mac = ( |
| sum(s[2] for s in per_spec_scores) / len(per_spec_scores) |
| if per_spec_scores |
| else 0.0 |
| ) |
|
|
| print(f"Baseline folder: {base_dir}") |
| print(f"Eval CSV: {args.csv.resolve()}") |
| print(f"MCR = {n_compile}/{N_SPECS} = {mcr:.4f} ({mcr*100:.1f}%)") |
| print( |
| f"MAC = mean(per-spec accuracy) over {len(per_spec_scores)} smoke-OK spec(s) " |
| f"= {mac:.4f} ({mac * 100:.1f}%)" |
| if per_spec_scores |
| else "MAC = n/a (no smoke-OK specs)" |
| ) |
| print() |
| for sid, name, acc, qc, qt in per_spec_scores: |
| print(f" S{sid:02d} {qc}/{qt} ({acc:.2f})") |
| if args.check_gold_model: |
| print() |
| print(f"Gold-model row checks: {gold_mismatch} mismatch(es) (expected 0)") |
|
|
| if args.out_json: |
| args.out_json.parent.mkdir(parents=True, exist_ok=True) |
| payload = { |
| "baseline": args.baseline, |
| "mcr": mcr, |
| "mac": mac, |
| "n_compile": n_compile, |
| "per_spec": [ |
| {"spec_id": a, "name": b, "accuracy": c, "q_correct": d, "q_total": e} |
| for a, b, c, d, e in per_spec_scores |
| ], |
| "specs": detail_specs, |
| } |
| args.out_json.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8") |
| print(f"\nWrote {args.out_json}") |
|
|
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|