AsianPlayer/TriDrive / code /eval /report_kp_residual_f1.py
AsianPlayer's picture
download
raw
18.1 kB
"""cwm3/eval/report_kp_residual_f1.py — results/tables/kp_residual_f1.md (+ .json): LANE F1 keypoint residual forecaster + distillation into the driver branch
(R2 WP4 batch 2; pre-registration docs/decisions.md 2026-09-17T19:33:12Z). Composes eval/report_joint.py + eval/report_final_face.py blocks (load / render /
verdict / margin table) over results/paired_compare.csv, the forecaster evaluation json (eval/kp_residual_eval.py) and, when given, the alert-timing run of
eval/alert_timing_eval.py. Nothing is recomputed here; every number is a script output.
python eval/report_kp_residual_f1.py [--seeds 0,1,2] [--forecaster results/tables/kp_residual_forecaster.json] [--timing-stamp <stamp>] [--out results/tables/kp_residual_f1.md]
Blocks: §1 forecaster (MPJPE per horizon / motion class vs zero- and constant-velocity; mb vs cwm at matched params; longer history vs downsampled observation;
future-state probabilities vs persistence); §2 distillation — (a-val) finalcv_s<k>_allminusecankpf_vs_allminusecan on the VALIDATION split = the ADOPTION decision,
(a) finalc_* test confirmation, finalcm_* masked protocol, finalc_*_vs_k0 (B1 on the variant), finalP_* P cells; §3 alert timing (kpf vs ecan driver_only_t15
scores, Recall_{>= δ s early} at 10 / 20 per h, paired); §4 params + GPU minutes.
PRE-REGISTERED READING: adoption candidate iff on VALIDATION no own onset cell (report_joint.OWN) is WORSE, no cell breaks the margins (onset −0.005 mean Δ, hazard NLL
+0.010 on the mean and every seed's CI) AND >= 1 of the four paper driver cells (manual hand_leave / hand_return / head_turn_onset, engaged head_turn_onset) BEATS.
Writes results/tables/kp_residual_f1_decision.json.
"""
from __future__ import annotations
import argparse
import json
import sys
import time
from pathlib import Path
import numpy as np
import pandas as pd
CWM3 = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(CWM3 / "eval"))
import report_joint as RJ # noqa: E402
import report_final_face as RF # noqa: E402
A, B = "allminusecankpf", "allminusecan"
PAPER_DRV = [("manual", "drv_onset|hand_leave|3s"), ("manual", "drv_onset|hand_return|3s"), ("manual", "drv_onset|head_turn_onset|3s"), ("engaged", "drv_onset|head_turn_onset|3s")]
VEH4 = ["veh_onset|gas_on|3s", "veh_onset|brake_on|3s", "veh_onset|steer_phase_on|3s", "veh_onset|veh_any_on|3s"]
# 2026-09-18 coverage fix: paired_compare_joint writes the vehicle hazard-NLL cells as hazard_nll|{gas_on,brake_on,steer_phase_on} (RF.NLL_VEH names them
# gas/brake/steer_phase -> "no rows" in the shared block); the pre-registered +0.010 margin is checked on these cells and veh_any_on is added to the onset margin cells.
NLL_VEH = ["hazard_nll|gas_on", "hazard_nll|brake_on", "hazard_nll|steer_phase_on"]
MARGIN_ONSET_CELLS = RF.DRV + VEH4
def forecaster_section(path):
if not path.exists():
return [f"### 1. Forecaster — no evaluation yet (`{path}` missing)", ""], {}
F = json.loads(path.read_text())
L = [f"### 1. Forecaster (eval/kp_residual_eval.py {F['stamp']}, split {F['split']}, n_boot {F['n_boot']}; MPJPE = frame-normalised units × 1000; motion classes with θ = {F['theta']:.5f} = {F['theta_rule']})", ""]
L += ["Params: " + ", ".join(f"`{k}` {v['total']:,} (backbone {v['backbone']:,})" for k, v in F["params"].items()), ""]
HS = ["0.5s", "1s", "2s", "3s"]
L += ["| regime | arm | " + " | ".join(f"model {h}" for h in HS) + " | " + " | ".join(f"ZV {h}" for h in HS) + " | " + " | ".join(f"CV {h}" for h in HS) + " |", "|---|---|" + "---|" * 12]
arms = sorted({k.rsplit("_s", 1)[0] for k in F["arms"]})
for r in ("manual", "engaged"):
for arm in arms:
recs = [v for k, v in F["arms"].items() if k.startswith(f"{arm}_s") and k.endswith(f"_{r}")]
if recs:
cells = []
for n in ("model", "zv", "cv"):
for h in HS:
v = np.array([x[f"mpjpe_{n}"][h] for x in recs]); cells.append(f"{v.mean():.2f} ± {v.std(ddof=1) if len(v) > 1 else 0:.2f}")
L.append(f"| {r} | {arm} | " + " | ".join(cells) + " |")
L += ["", "Paired Δ MPJPE (A − B, lower better; verdict over seeds = kill_checks.beats on −Δ, BEATS = A lower):", "",
"| pair | regime | " + " | ".join(f"Δ {h} (verdict)" for h in HS) + " |", "|---|---|" + "---|" * 4]
pairs = sorted({k.split("|")[0] for k in F["pairs"] if k != "class_delta"})
summary = {}
for tag in pairs:
for r in ("manual", "engaged"):
cells = []
for h in HS:
v = F["pairs"].get(f"{tag}|{r}|{h}")
if v:
d = np.mean([x["delta"] for x in v["seeds"]]); cells.append(f"{d:+.2f} ({v['verdict']['verdict']})"); summary[f"{tag}|{r}|{h}"] = {"mean_delta": float(d), "verdict": v["verdict"]["verdict"]}
else:
cells.append("—")
L.append(f"| {tag} | {r} | " + " | ".join(cells) + " |")
L += ["", "MPJPE by motion class (3-seed mean; model / ZV):", "", "| regime | arm | class | share | " + " | ".join(HS) + " |", "|---|---|---|---|" + "---|" * 4]
for r in ("manual", "engaged"):
for arm in arms:
recs = [v for k, v in F["arms"].items() if k.startswith(f"{arm}_s") and k.endswith(f"_{r}")]
if not recs:
continue
for c in ("still", "onset", "sustained", "recovery"):
L.append(f"| {r} | {arm} | {c} | {F['class_share'][r][c]:.3f} | " + " | ".join(f"{np.nanmean([x['mpjpe_model_by_class'][c][h] for x in recs]):.2f} / {np.nanmean([x['mpjpe_zv_by_class'][c][h] for x in recs]):.2f}" for h in HS) + " |")
L += ["", "Future-state probabilities of `mb_full` (3-seed mean) vs persistence (S0):", "", "| regime | state@h | Brier model / persistence | AUROC model / persistence | n |", "|---|---|---|---|---|"]
for r in ("manual", "engaged"):
recs = [v for k, v in F["arms"].items() if k.startswith("mb_full_s") and k.endswith(f"_{r}")]
for sh in (recs[0]["states"] if recs else []):
g = lambda f: float(np.nanmean([x["states"][sh][f] for x in recs]))
L.append(f"| {r} | {sh} | {g('brier_model'):.4f} / {g('brier_persistence'):.4f} | {g('auroc_model'):.4f} / {g('auroc_persistence'):.4f} | {recs[0]['states'][sh]['n']:,} |")
L.append("")
return L, {"params": F["params"], "pairs": summary, "runs": F["runs"], "theta": F["theta"], "class_share": F["class_share"]}
def timing_section(stamp):
if not stamp:
return ["### 3. Alert timing — not run for this lane yet", ""], {}
d = CWM3 / "results" / "alert_timing" / stamp
if not (d / "paired.csv").exists():
return [f"### 3. Alert timing — run {stamp} has no paired.csv", ""], {}
pr = pd.read_csv(d / "paired.csv"); me = pd.read_csv(d / "metrics.csv")
pr = pr[(pr.a == "kpf_drv") & (pr.b == "ecan_drv") & (pr.thr_fit == "val")]
L = [f"### 3. Alert timing (eval/alert_timing_eval.py {stamp}; scores = the joint layouts' `driver_only_t15` column: `kpf_drv` = I1_all_minus_road2drv_s<k>_final_ecan_kpf, `ecan_drv` = the frozen ECAN main; val-fitted thresholds; Δ = kpf − ecan on identical resamples)", "",
"| family | budget /h | regime | δ | Recall kpf per seed | Recall ecan per seed | Δ per seed [CI] | mean Δ | CI≠0 |", "|---|---|---|---|---|---|---|---|---|"]
out = {}
for (fam, bud, reg, dl), g in pr.groupby(["family", "budget", "regime", "delta_s"]):
if fam not in ("A", "B") or reg == "pooled" and False:
continue
g = g.sort_values("seed")
ex = int(((g.ci_lo > 0) | (g.ci_hi < 0)).sum())
L.append(f"| {fam} | {bud:g} | {reg} | {dl:g} | " + " / ".join(f"{x:.3f}" for x in g.value_a) + " | " + " / ".join(f"{x:.3f}" for x in g.value_b) + " | "
+ " / ".join(f"{x.diff:+.3f} [{x.ci_lo:+.3f},{x.ci_hi:+.3f}]" for x in g.itertuples()) + f" | {g['diff'].mean():+.3f} | {ex}/{len(g)} |")
out[f"{fam}|{bud:g}|{reg}|{dl:g}"] = {"deltas": g["diff"].tolist(), "ci": list(zip(g.ci_lo.tolist(), g.ci_hi.tolist())), "n_excl": ex, "n_seeds": int(len(g))}
ach = me[(me.score.isin(["kpf_drv", "ecan_drv"])) & (me.thr_fit == "val") & (me.family == "A")]
if len(ach):
L += ["", "Achieved test load (family A, val-fitted; alerts/h, mean over seeds): " + "; ".join(f"{s} {reg} @{b:g}/h: {g.alerts_per_h_test.mean():.1f}" for (s, reg, b), g in ach.groupby(["score", "regime", "budget"]))]
return L + [""], out
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--seeds", default="0,1,2")
ap.add_argument("--forecaster", default=str(CWM3 / "results" / "tables" / "kp_residual_forecaster.json"))
ap.add_argument("--timing-stamp", default="")
ap.add_argument("--out", default=str(CWM3 / "results" / "tables" / "kp_residual_f1.md"))
a = ap.parse_args()
seeds = [int(s) for s in a.seeds.split(",")]
stamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
L = [f"# LANE F1 — keypoint residual forecaster on BATON + distillation into the driver branch (pre-registration docs/decisions.md 2026-09-17T19:33:12Z; report {stamp})", "",
"Default configuration = CockpitWM (ECAN): E base `E0_pooled_P_can_s<k>_v2`; frozen main `I1_all_minus_road2drv_s<k>_final_ecan`; variant `I1_all_minus_road2drv_s<k>_final_ecan_kpf` = the same "
"command line + `--u-kpf results/runs/KPF_mb_full_s<k>` (seed-matched forecaster; detached 10-channel state / mode probabilities through a zero-init injector on h_u). Pairings: "
"eval/run_joint_evals_f1_kpf.sh (pc_joint_locked, n_boot 1000, identical rows, v2 causal kin targets); verdict = kill_checks.beats; margins onset −0.005 / NLL +0.010. Every number is a script output; the manuscript is not edited here.", ""]
dec = {"report": stamp, "seeds": seeds}
fl, dec["forecaster"] = forecaster_section(Path(a.forecaster))
L += fl
L += ["### 2. Distillation into the joint model", ""]
dfs = []
# (a-val) adoption decision
dfv, body, verd_v, cl_v = RF.block("finalcv", A, B, "B2", "(a-val) variant vs FROZEN ECAN main — VALIDATION split (adoption decision)", seeds)
dfs.append(dfv); L += body + [""]
if len(dfv):
mt, checks_v = RF.margin_table(dfv, verd_v, "(a-val) driver / vehicle cells and hazard-NLL costs with the acceptable-degradation margins (VALIDATION)", MARGIN_ONSET_CELLS, RF.NLL_DRV + NLL_VEH)
L += mt
worse, broke, _, ht, insuff = RF.own_status(verd_v, checks_v)
paper_beats = [f"{r}|{c}" for r, c in PAPER_DRV if verd_v.get((r, c, "pr_auc"), {}).get("verdict") == "BEATS"]
paper_worse = [f"{r}|{c}" for r, c in PAPER_DRV if verd_v.get((r, c, "pr_auc"), {}).get("verdict") == "WORSE"]
nll_broke = [k for k, c in checks_v.items() if k[2] == "nll" and not c["ok"]]
adopt = (not worse) and (not broke) and (not nll_broke) and len(paper_beats) >= 1 and not insuff
status = "INSUFFICIENT (< 3 seeds or missing cells)" if insuff else ("ADOPTION CANDIDATE (validation)" if adopt else "REJECTED (validation)")
L += [f"**(a-val) pre-registered reading: {status}.** own onset WORSE: {RF.fmt(worse)}; own onset cells beyond −0.005: {RF.fmt(broke)}; NLL cells beyond +0.010: {RF.fmt(nll_broke)}; "
f"paper driver cells BEATS: {paper_beats or 'none'}; paper driver cells WORSE: {paper_worse or 'none'}; head_turn verdicts {ht}.", ""]
dec["validation"] = {"status": status, "own_worse": RF.fmt(worse), "own_beyond_margin": RF.fmt(broke), "nll_beyond_margin": RF.fmt(nll_broke), "paper_beats": paper_beats, "paper_worse": paper_worse,
"cells": {f"{r}|{c}|{m}": v["verdict"] for (r, c, m), v in verd_v.items()}, "margin_checks": {f"{r}|{c}|{m}": c_ for (r, c, m), c_ in checks_v.items()}}
else:
dec["validation"] = {"status": "NO ROWS"}
# (a) test confirmation
dft, body, verd_t, cl_t = RF.block("finalc", A, B, "B2", "(a) variant vs FROZEN ECAN main — TEST split (confirmation)", seeds)
dfs.append(dft); L += body + [""]
if len(dft):
mt, checks_t = RF.margin_table(dft, verd_t, "(a) driver / vehicle cells and hazard-NLL costs with the margins (TEST)", MARGIN_ONSET_CELLS, RF.NLL_DRV + NLL_VEH)
L += mt
worse_t, broke_t, _, ht_t, _ = RF.own_status(verd_t, checks_t)
dec["test"] = {"own_worse": RF.fmt(worse_t), "own_beyond_margin": RF.fmt(broke_t), "head_turn": ht_t, "B2_style": cl_t[0],
"cells": {f"{r}|{c}|{m}": v["verdict"] for (r, c, m), v in verd_t.items()}, "paper_cells": {f"{r}|{c}": verd_t.get((r, c, "pr_auc"), {}).get("verdict", "NO ROWS") for r, c in PAPER_DRV},
"veh4": {f"{r}|{c}": verd_t.get((r, c, "pr_auc"), {}).get("verdict", "NO ROWS") for r in ("manual", "engaged") for c in VEH4},
"hazard_nll": {f"{r}|{c}": verd_t.get((r, c, "nll"), {}).get("verdict", "NO ROWS") for r in ("manual", "engaged") for c in RF.NLL_DRV + NLL_VEH}}
L += [f"**(a) TEST:** own onset WORSE: {RF.fmt(worse_t)}; beyond −0.005: {RF.fmt(broke_t)}; head_turn {ht_t}; B2-style claim (no own WORSE and ≥ 1 cross-leg BEATS): {cl_t[0]}.", ""]
else:
dec["test"] = {"status": "NO ROWS"}
dfm, body, verd_m, cl_m = RF.block("finalcm", A, B, "B2", "masked protocol (human-verified driver-box mask) — TEST", seeds)
dfs.append(dfm); L += body + [""]
dec["masked"] = {f"{r}|{c}|{m}": v["verdict"] for (r, c, m), v in verd_m.items()}
# B1 vs k0 on the variant
dfk = RJ.load("finalc", A, "k0", seeds); dfs.append(dfk)
if len(dfk):
L += ["### B1 on the variant — driver onset PR-AUC@3s vs the k0 heuristic (TEST)", "", "| regime | event | model per seed | k0 per seed | Δ per seed [CI] | verdict |", "|---|---|---|---|---|---|"]
vk = {}
for reg in ("manual", "engaged"):
for ev in RF.EV:
cell = f"drv_onset|{ev}|3s"
g = dfk[(dfk.regime == reg) & (dfk.cell == cell) & (dfk.metric == "pr_auc")].sort_values("seed")
if len(g):
res = RJ.verdict(g); vk[f"{reg}|{cell}"] = res["verdict"]
L.append(f"| {reg} | {ev} | {RF.ms(g.value_a)} | {RF.ms(g.value_b)} | " + " / ".join(f"{float(x.delta):+.3f} [{float(x.delta_ci_lo):+.3f},{float(x.delta_ci_hi):+.3f}]" for x in g.itertuples()) + f" | {res['verdict']} |")
L.append(""); dec["b1_vs_k0"] = vk
else:
L += ["### B1 on the variant — no rows yet (`finalc_s<k>_allminusecankpf_vs_k0`)", ""]
dfp, body, verd_p, _ = RF.block("finalP", A, B, "B2x", "P cells (eval/pressure_eval.py) — TEST", seeds)
dfs.append(dfp); L += body + [""]
dec["P"] = {f"{r}|{c}|{m}": v["verdict"] for (r, c, m), v in verd_p.items()}
tl, dec["timing"] = timing_section(a.timing_stamp)
L += tl
# params + GPU minutes
L += ["### 4. Parameters and GPU minutes", ""]
rows = []
for s in seeds:
rd = CWM3 / "results" / "runs" / f"I1_all_minus_road2drv_s{s}_final_ecan_kpf"
pj = rd / "params.json"
rj = sorted(rd.glob("run_train_I1_*_2*.json"))
if pj.exists():
P = json.loads(pj.read_text())
wall = json.loads(rj[-1].read_text()).get("wall_s") if rj else None
hist = [json.loads(x) for x in (rd / "history.jsonl").read_text().splitlines()] if (rd / "history.jsonl").exists() else []
rows.append({"seed": s, "total": P.get("total"), "new_total": P.get("new_total"), "kpf": P.get("kpf"), "interaction_total": P.get("interaction_total"), "wall_s": wall, "epochs": len(hist),
"best_val": json.loads((rd / "TRAIN_DONE").read_text()).get("best") if (rd / "TRAIN_DONE").exists() else None})
if rows:
L += ["| joint run | registered params (total) | new modules | of which kpf injector | interaction | epochs | wall s |", "|---|---|---|---|---|---|---|"]
for r in rows:
L.append(f"| I1_all_minus_road2drv_s{r['seed']}_final_ecan_kpf | {r['total']:,} | {r['new_total']:,} | {r['kpf']:,} | {r['interaction_total']:,} | {r['epochs']} | {r['wall_s']:.0f} |" if r["wall_s"] else
f"| I1_all_minus_road2drv_s{r['seed']}_final_ecan_kpf | {r['total']:,} | {r['new_total']:,} | {r['kpf']:,} | {r['interaction_total']:,} | {r['epochs']} | — |")
fp = dec.get("forecaster", {}).get("params", {})
fr = dec.get("forecaster", {}).get("runs", {})
kpf_min = sum(v.get("gpu_wall_s") or 0 for v in fr.values()) / 60
joint_min = sum(r["wall_s"] or 0 for r in rows) / 60
L += ["", f"Forecaster params: " + ", ".join(f"`{k}` {v['total']:,}" for k, v in fp.items()) + f". Total trainable of the F1 configuration = joint registered params (above) + forecaster `mb_full` {fp.get('mb_full', {}).get('total', 0):,} "
f"= {(rows[0]['total'] if rows else 0) + fp.get('mb_full', {}).get('total', 0):,} (budget ≤ 7 M). GPU minutes: forecasters {kpf_min:.1f} ({len(fr)} runs), joint {joint_min:.1f} ({len(rows)} runs), total {kpf_min + joint_min:.1f}.", ""]
dec["params"] = {"joint_runs": rows, "forecaster": fp, "gpu_min": {"forecasters": kpf_min, "joint": joint_min}}
prov = [d for d in dfs if len(d)]
if prov: # 2026-09-18 rendering fix: RJ.provenance returns (lines, stale) — was extended as a tuple (TypeError on join); numbers unchanged
prov_lines, stale = RJ.provenance(pd.concat(prov))
L += ["### Provenance (layout files of every A / B run in the rendered rows; STALE = rows older than the current layout)" + (f" — **VOID: stale rows for {stale}**" if stale else ""), ""] + prov_lines + [""]
dec["stale"] = stale
Path(a.out).write_text("\n".join(L) + "\n")
Path(a.out).with_name("kp_residual_f1_decision.json").write_text(json.dumps(dec, indent=1, default=str))
print(f"written {a.out}; VERDICT validation: {dec.get('validation', {}).get('status')}")
if __name__ == "__main__":
main()

Xet Storage Details

Size:
18.1 kB
·
Xet hash:
0a0b1b1b0320ab14afcd0f6c3fe07e4756fdbb1d0a8c807d87a87a273545ef6d

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.