AsianPlayer/TriDrive / code /eval /report_gaze_chain.py
AsianPlayer's picture
download
raw
14.6 kB
"""cwm3/eval/report_gaze_chain.py — results/tables/gaze_chain.md from results/paired_compare.csv (tags gaze_* / gazeon_*) and results/gaze_chain/gaze_gold.csv
(tags gazegold_*); gaze/robustness agent 2026-09-12. Verdicts = cwm2/eval/kill_checks.beats over the per-seed paired rows (BEATS / WORSE / NOT_WORSE /
INCONCLUSIVE; 3 seeds). Sections: (a) K4-style driver cells of the +chain / +chain+face U runs vs the survivor on identical rows (+ levels vs k0 / persistence);
(b) gaze_off_on / gaze_off_off @3s vs the current-state rule and persistence (+ face effect); (c) gold clean-test rows: model vs DMS att@3s (comparator) on our
gaze_off_on label; (d) the hand-off columns for the warning head. python eval/report_gaze_chain.py [--seeds "0 1 2"] [--out results/tables/gaze_chain.md]
"""
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.parent / "cwm2" / "eval"))
import kill_checks as KC # noqa: E402
DRV = ("hand_leave", "hand_return", "head_turn_onset", "head_return")
GZ = ("gaze_off_on", "gaze_off_off")
def beats(g):
pr = pd.DataFrame({"seed": g.seed, "delta": g.delta.astype(float), "delta_ci_lo": g.delta_ci_lo.astype(float), "delta_ci_hi": g.delta_ci_hi.astype(float), "status": "secondary"})
return KC.beats(pr, pd.DataFrame({"value": g.value_a.astype(float)}), pd.DataFrame({"value": g.value_b.astype(float)}), min_seeds=3)["verdict"]
def ms(v):
v = np.asarray([x for x in v if x is not None and np.isfinite(x)], float)
return "—" if not len(v) else (f"{v.mean():.3f} ± {v.std(ddof=1):.3f}" if len(v) > 1 else f"{v[0]:.3f}")
def dcell(g, seeds):
by = {int(s): r for s, r in zip(g.seed, g.itertuples())}
return [f"{by[s].delta:+.3f} [{by[s].delta_ci_lo:+.3f},{by[s].delta_ci_hi:+.3f}]" if s in by else "—" for s in seeds]
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--seeds", default="0 1 2")
ap.add_argument("--out", default=str(CWM3 / "results" / "tables" / "gaze_chain.md"))
a = ap.parse_args()
seeds = [int(s) for s in a.seeds.replace(",", " ").split()]
d = pd.read_csv(CWM3 / "results" / "paired_compare.csv", low_memory=False)
d = d[d.tag.str.startswith(("gaze_", "gazeon_"))].copy()
d["seed"] = d.tag.str.extract(r"_s(\d+)_")[0].astype(int)
d = d.sort_values("run_stamp").groupby(["tag", "regime", "cell", "metric"], as_index=False).tail(1)
for c in ("value_a", "value_b", "delta", "delta_ci_lo", "delta_ci_hi", "n", "n_pos"):
d[c] = pd.to_numeric(d[c], errors="coerce")
gold_p = CWM3 / "results" / "gaze_chain" / "gaze_gold.csv"
gold = pd.read_csv(gold_p) if gold_p.exists() else pd.DataFrame()
if len(gold):
gold = gold.sort_values("run_stamp").groupby(["tag", "regime", "column", "comparator", "metric"], as_index=False).tail(1)
stats = json.loads((CWM3 / "results" / "targets_gaze_causal_routes_v2" / "build_stats_latest.json").read_text())
thr = stats["thresholds"]
L = [f"# gaze_off as a driver target chain — U0 `+chain` (`_gz`) and `+chain+face` (`_gzfc`) vs the survivor recipe (seeds {seeds}, test, v2 causal targets; rendered {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())})", "",
f"State: `gaze_off` = hysteresis on |lm_yaw_dev| of OUR RTMW HALPE-136 face landmarks (causal per-route reference; on / off = TRAIN moving-frame P90 / P75 = {thr['gaze_off_on']:.4f} / {thr['gaze_off_off']:.4f}), "
f"re-derived on the 10 Hz route grid by `data/build_gaze_targets.py` (0 mismatches vs the face pack on {sum(v['n_checked_vs_face_F0'] for v in stats['splits'].values()):,} anchors) -> "
f"`results/targets_gaze_causal_routes_v2/gaze_<regime>_<split>.npz` (S_g / S0_g / E_g / EM_g; s12 M censoring; uid/t asserted vs `kin_causal_*`). "
f"Base rates (3 s, inactive anchors): manual test gaze_off_on {stats['splits']['manual_test']['events']['gaze_off_on']['base_rate@3s']:.3f} ({stats['splits']['manual_test']['events']['gaze_off_on']['pos@3s']:,}/{stats['splits']['manual_test']['events']['gaze_off_on']['n@3s']:,}), "
f"engaged test {stats['splits']['engaged_test']['events']['gaze_off_on']['base_rate@3s']:.3f} ({stats['splits']['engaged_test']['events']['gaze_off_on']['pos@3s']:,}/{stats['splits']['engaged_test']['events']['gaze_off_on']['n@3s']:,}).",
"Runs: `U0_kin_can_noK_s<k>_kcv2_clip8_ln_gz` (recipe `u_gaze_chain`: DriverBranch `gaze_head` -> a third 2-state chain, NLL `chain_gaze_off` in the U loss (lambda G = 1) and in the selection objective) and "
"`..._gzfc` (+ `u_face`: the 9 causal `lm_*` channels + valid bit appended to every GRU input step); otherwise the frozen U recipe (causal v2 inputs, stats v2, kclip 8, input LN, chains only). "
"Every row: identical rows of A and B, device-cluster bootstrap (n_boot 1000, seed 0), Δ = A − B; verdict = kill_checks.beats over the 3 seeds.", ""]
# ---- (a) driver cells vs survivor
L += ["## (a) K4-style driver cells (onset PR-AUC@3s, v2 causal targets) — A = gaze variant, B = survivor `U0_kin_can_noK_s<k>_kcv2_clip8_ln` (identical rows)", "",
"Requirement: hand_leave / hand_return / head_turn_onset NOT WORSE (verdict not WORSE).", ""]
for arm, label in (("gz", "+chain"), ("gzfc", "+chain+face")):
L += [f"### {label} (`_{arm}`) vs survivor", "", "| regime | cell | metric | A (mean ± sd) | B (mean ± sd) | " + " | ".join(f"Δ s{s} [CI]" for s in seeds) + " | mean Δ | verdict |", "|---|---|---|---|---|" + "---|" * len(seeds) + "---|---|"]
x = d[d.tag.str.match(rf"gaze_s\d+_{arm}_vs_clip8ln$")]
for reg in ("manual", "engaged"):
for ev in DRV:
for metric in ("pr_auc", "nll"):
cell = f"drv_onset|{ev}|3s" if metric == "pr_auc" else f"hazard_nll|{ev}"
g = x[(x.regime == reg) & (x.cell == cell) & (x.metric == metric)].sort_values("seed")
if not len(g):
continue
v = beats(g) if metric == "pr_auc" else beats(g.assign(delta=-g.delta, delta_ci_lo=-g.delta_ci_hi, delta_ci_hi=-g.delta_ci_lo))
if metric == "nll":
v = {"BEATS": "BEATS (lower NLL)", "WORSE": "WORSE (higher NLL)"}.get(v, v)
L.append(f"| {reg} | {cell} | {metric} | {ms(g.value_a)} | {ms(g.value_b)} | " + " | ".join(dcell(g, seeds)) + f" | {g.delta.mean():+.4f} | {v} |")
L.append("")
x = d[d.tag.str.match(r"gaze_s\d+_gzfc_vs_gz$")]
if len(x):
L += ["### face-input effect on the driver cells: A = `_gzfc`, B = `_gz`", "", "| regime | cell | A | B | " + " | ".join(f"Δ s{s} [CI]" for s in seeds) + " | verdict |", "|---|---|---|---|" + "---|" * len(seeds) + "---|"]
for reg in ("manual", "engaged"):
for ev in DRV:
g = x[(x.regime == reg) & (x.cell == f"drv_onset|{ev}|3s") & (x.metric == "pr_auc")].sort_values("seed")
if len(g):
L.append(f"| {reg} | drv_onset/{ev}/3s | {ms(g.value_a)} | {ms(g.value_b)} | " + " | ".join(dcell(g, seeds)) + f" | {beats(g)} |")
L.append("")
# levels vs k0 / persistence
L += ["### Levels vs the label-free references (k0 heuristic from the v2 causal K0; persistence = base rate)", "", "| regime | event | survivor (k4_v2_summary) | +chain | +chain+face | k0 | persistence | +chain vs k0 | +chain+face vs k0 |", "|---|---|---|---|---|---|---|---|---|"]
k4 = pd.read_csv(CWM3 / "results" / "paired_compare.csv", low_memory=False)
k4 = k4[k4.tag.str.match(r"K4v2c_s\d+_clip8ln_vs_k0$")].copy()
k4["seed"] = k4.tag.str.extract(r"_s(\d+)_")[0].astype(int)
k4 = k4.sort_values("run_stamp").groupby(["tag", "regime", "cell", "metric"], as_index=False).tail(1)
for reg in ("manual", "engaged"):
for ev in ("hand_leave", "hand_return", "head_turn_onset"):
cell = f"drv_onset|{ev}|3s"
gz_ = d[d.tag.str.match(r"gaze_s\d+_gz_vs_k0$") & (d.regime == reg) & (d.cell == cell) & (d.metric == "pr_auc")].sort_values("seed")
fc_ = d[d.tag.str.match(r"gaze_s\d+_gzfc_vs_k0$") & (d.regime == reg) & (d.cell == cell) & (d.metric == "pr_auc")].sort_values("seed")
sv_ = k4[(k4.regime == reg) & (k4.cell == cell) & (k4.metric == "pr_auc")]
pr_ = d[d.tag.str.match(r"gaze_s\d+_gz_vs_k0$") & (d.regime == reg) & (d.cell == cell) & (d.metric == "base_rate")]
if not len(gz_):
continue
L.append(f"| {reg} | {ev} | {ms(pd.to_numeric(sv_.value_a, errors='coerce'))} | {ms(gz_.value_a)} | {ms(fc_.value_a)} | {ms(gz_.value_b)} | {ms(pd.to_numeric(pr_.value_a, errors='coerce'))} | {beats(gz_) if len(gz_) >= 3 else '—'} | {beats(fc_) if len(fc_) >= 3 else '—'} |")
L.append("")
# ---- (b) gaze onset vs rule / persistence
L += ["## (b) The new chain: gaze_off_on@3s (and gaze_off_off@3s) — model vs the current-state rule |lm_yaw_dev|(t) and vs persistence (test grid, first-passage populations, identical rows)", "",
"Rule = the continuous quantity behind the hysteresis at the anchor (higher = closer to the on threshold; sign-flipped for the off event) — the gaze analogue of the k0 heuristic. Persistence = constant score (PR-AUC = base rate).", "",
"| regime | event | arm | metric | model (mean ± sd) | rule | persistence (base rate) | " + " | ".join(f"Δ model−rule s{s} [CI]" for s in seeds) + " | verdict vs rule |", "|---|---|---|---|---|---|---|" + "---|" * len(seeds) + "---|"]
for reg in ("manual", "engaged"):
for ev in GZ:
cell = f"gaze_onset|{ev}|3s"
pr_ = d[d.tag.str.match(r"gazeon_s\d+_gz_persist$") & (d.regime == reg) & (d.cell == cell) & (d.metric == "base_rate")]
for arm in ("gz", "gzfc"):
for metric in ("pr_auc", "auroc"):
g = d[d.tag.str.match(rf"gazeon_s\d+_{arm}_vs_rule$") & (d.regime == reg) & (d.cell == cell) & (d.metric == metric)].sort_values("seed")
if not len(g):
continue
L.append(f"| {reg} | {ev} | {arm} | {metric} | {ms(g.value_a)} | {ms(g.value_b)} | {ms(pd.to_numeric(pr_.value_a, errors='coerce')) if metric == 'pr_auc' else '0.5'} | " + " | ".join(dcell(g, seeds)) + f" | {beats(g) if len(g) >= 3 else '—'} |")
L.append("")
x = d[d.tag.str.match(r"gazeon_s\d+_gzfc_vs_gz$")]
if len(x):
L += ["### face-input effect on the gaze chain: A = `_gzfc`, B = `_gz`", "", "| regime | event | metric | A | B | " + " | ".join(f"Δ s{s} [CI]" for s in seeds) + " | verdict |", "|---|---|---|---|---|" + "---|" * len(seeds) + "---|"]
for reg in ("manual", "engaged"):
for ev in GZ:
for metric in ("pr_auc", "auroc"):
g = x[(x.regime == reg) & (x.cell == f"gaze_onset|{ev}|3s") & (x.metric == metric)].sort_values("seed")
if len(g):
L.append(f"| {reg} | {ev} | {metric} | {ms(g.value_a)} | {ms(g.value_b)} | " + " | ".join(dcell(g, seeds)) + f" | {beats(g) if len(g) >= 3 else '—'} |")
L.append("")
# ---- (c) gold rows vs DMS
L += ["## (c) Gold clean-test rows (cwm2/eval/warning_gold.py anchors, legacy_first, seed-matched legacy CSV): LABEL = our gaze_off_on onset within 3 s (anchor inactive); model vs the DMS `att@3s` column (COMPARATOR only) vs the rule vs persistence", ""]
if len(gold):
n_note = gold.groupby(["seed", "regime"]).agg(n=("n", "first"), n_pos=("n_pos", "first"), n_dev=("n_dev", "first")).reset_index()
L += ["Rows used per seed / stratum (rows whose anchor is inactive & valid for the gaze chain and carry every score): " + "; ".join(f"s{int(r.seed)} {r.regime} n={int(r.n)} pos={int(r.n_pos)} dev={int(r.n_dev)}" for r in n_note.itertuples()), "",
"| stratum | arm | column | comparator | metric | value (mean ± sd over seeds) | comparator value | " + " | ".join(f"Δ s{s} [CI]" for s in seeds) + " | verdict |", "|---|---|---|---|---|---|---|" + "---|" * len(seeds) + "---|"]
gold["arm"] = gold.tag.str.extract(r"gazegold_s\d+_(\w+)$")[0]
for strat in ("pooled", "manual", "engaged"):
for arm in ("gz", "gzfc"):
for colkey, coln in (("model", "model_gaze_off_onset_p@3s"), ("dms", "dms_att@3s"), ("rule", "rule_absyawdev_t")):
for comp in ("dms_att@3s", "rule_absyawdev_t", "persistence"):
for metric in ("auroc", "pr_auc"):
g = gold[(gold.regime == strat) & (gold.arm == arm) & (gold.comparator == comp) & (gold.metric == metric) & (gold["column"].str.endswith(coln))].sort_values("seed")
if not len(g) or (colkey != "model" and arm != "gz"):
continue
gg = g.rename(columns={"value": "value_a"})
v = beats(gg) if len(gg) >= 3 else "—"
L.append(f"| {strat} | {arm if colkey == 'model' else '—'} | {coln} | {comp} | {metric} | {ms(g.value)} | {ms(g.value_b)} | " + " | ".join(dcell(gg, seeds)) + f" | {v} |")
L.append("")
else:
L += ["(no gold rows yet)", ""]
# ---- (d) hand-off
L += ["## (d) Hand-off to the warning head", "",
"Every `U0_..._gz*` layout (`pred_layout_<regime>_<split>.npz`, test + val) carries EXTRA keys beside the unchanged 14-column contract: `gaze_off_onset_p[N,4]` "
"(P(first 0->1 within tau | z0), taus (5, 10, 15, 25) in `gaze_off_taus`), `gaze_off_pi[N,25]` (forward marginal P(gaze_off = 1 at t+k)), `gaze_off_h_on` / `gaze_off_h_off[N,25]`, "
"`gaze_off_z0[N]` (state at t) and `gaze_off_z0valid[N]`; NaN where the branch did not predict (no causal driver box) or the anchor state is undefined. "
"Meta key `gaze_block` documents the layout; `roi_streams` gains `Sg` (+ `Fh` for `_gzfc`). A warning head reads them exactly like the `p_*` road-pressure keys "
"(train/train_warning_head.py `--extra-features`); the joint model (`models/cwm3.py`) is untouched, so the columns come from the U0 base runs, not from `I1_*_final`. "
"The warning head was NOT retrained here.", ""]
Path(a.out).write_text("\n".join(L) + "\n")
print(f"written {a.out} ({len(d)} paired rows, {len(gold)} gold rows)")
if __name__ == "__main__":
main()

Xet Storage Details

Size:
14.6 kB
·
Xet hash:
805ab94178716f5ce820196ecdca2e0ed5bc61bab03ca8961423debe48f888f6

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