Buckets:
| """cwm3/eval/future_state_eval.py — results/tables/future_state_eval.md (+ .json): F2′ future-state occupancy / recovery evaluation (R2 WP4 batch 1, 2026-09-16; | |
| pre-registration docs/decisions.md 2026-09-16T06:46:35Z, dimensions (ii) (iii) (vi)). CPU, numpy only; reads the FS runs' fs_{regime}_{split}.npz, the FROZEN FINAL | |
| joint runs' pred_{regime}_{split}.npz (their own hands_on / head_turned hazards -> closed-form pi, first-passage law), the v2 causal kin / gaze targets (true future | |
| states) and the F2′ TRAIN caches (results/runs/FS_cache: dwell features + targets, for the dwell-table baseline). | |
| python eval/future_state_eval.py [--name dwell] [--seeds 0,1,2] [--split test] [--n-boot 500] [--out results/tables/future_state_eval.md] | |
| Per regime x state (hands_on, head_turned, gaze_off) x horizon (1 / 2 / 3 / 5 s = steps 5 / 10 / 15 / 25), rows = anchors with a causal box, a defined current | |
| state and an observed true state at t + k dt: | |
| Brier and AUROC of P(state = 1 at t + k) for FS = the F2′ head; (a) persistence pi_k = z0; (b) dwell table = TRAIN empirical P(s_{t+k} = 1 | z0, time-in-state | |
| bin) — the "dwell-timer extrapolation" baseline (no model); (c) FINAL closed form = forward_marginal of the frozen model's own hands / head hazards (n/a for gaze_off). | |
| Δ = FS − baseline, shared-resample device-cluster bootstrap CI (n_boot, seed 0) per seed; verdict = kill_checks.beats over the 3 seeds (report_joint.verdict, lower better | |
| for Brier). Calibration: ECE (10 equal-width bins) of pi at 3 s. Segment classes at 3 s from the TRUE states: static (on-task, stays), onset (on-task -> off-task | |
| within 3 s), sustained (off-task, stays), recovery (off-task -> on-task within 3 s); Brier per class for FS / persistence / FINAL closed form. | |
| Recovery / remaining dwell (dimension iii): rows in the OFF-task state whose first-leave time is observed (event within 5 s or the full 5 s observed without a leave); | |
| MAE (s) of E[min(T, 5.2 s)] for FS rem_dwell_E_s, (b) the dwell-table conditional mean, (c) the FINAL first-passage law; calibration = ECE of P(recover within 3 s). | |
| First-leave time on ALL rows (any switch from the current state) likewise (FS first_leave_E_s vs (b) / (c)). | |
| Nothing here reads an openpilot DMS field. | |
| """ | |
| 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] | |
| for p in (CWM3 / "eval", CWM3 / "data", CWM3 / "train", CWM3.parent / "cwm2" / "eval"): | |
| sys.path.insert(0, str(p)) | |
| import report_joint as RJ # noqa: E402 | |
| import kill_checks as KC # noqa: E402 | |
| RUNS = CWM3 / "results" / "runs" | |
| CACHE = RUNS / "FS_cache" | |
| KIN = CWM3 / "results" / "targets_kin_causal_routes_v2" | |
| GAZE = CWM3 / "results" / "targets_gaze_causal_routes_v2" | |
| STATES = ("hands_on", "head_turned", "gaze_off") | |
| OFF_VALUE = {"hands_on": 0.0, "head_turned": 1.0, "gaze_off": 1.0} | |
| CHAIN_COL = {"hands_on": 4, "head_turned": 5} # columns of the joint pred pack hz_on / hz_off / z0 (CHAINS order) | |
| DWELL_FEAT = {"hands_on": "hands_state_dwell_s", "head_turned": "head_state_dwell_s", "gaze_off": "gz_dwell0_s"} | |
| HORIZONS = {"1s": 5, "2s": 10, "3s": 15, "5s": 25} | |
| H, DT = 25, 0.2 | |
| DWELL_BINS = np.array([0.0, 0.5, 1.0, 2.0, 4.0, 8.0, np.inf]) | |
| FINAL_RUN = "I1_all_minus_road2drv_s{seed}_final" | |
| REGIMES = ("manual", "engaged") | |
| # ----------------------------------------------------------------------------- chain algebra (numpy) | |
| def forward_marginal(h_on, h_off, z0): | |
| pi = np.empty_like(h_on, dtype=np.float64) | |
| prev = z0.astype(np.float64) | |
| for k in range(h_on.shape[1]): | |
| prev = prev * (1 - h_off[:, k]) + (1 - prev) * h_on[:, k] | |
| pi[:, k] = prev | |
| return pi | |
| def first_leave_law_from_hazards(h_on, h_off, z0): | |
| """q [N, H+1]: q_k = prod_{j<k}(1 - h_leave_j) h_leave_k with h_leave = h_off where z0 = 1 else h_on; q_H = survival beyond H.""" | |
| h = np.where(z0[:, None] > 0.5, h_off, h_on).astype(np.float64) | |
| surv = np.cumprod(1 - h, 1) | |
| prev = np.concatenate([np.ones((len(h), 1)), surv[:, :-1]], 1) | |
| return np.concatenate([prev * h, surv[:, -1:]], 1) | |
| def expected_leave_s(p): | |
| return (p * (np.arange(1, p.shape[1] + 1) * DT)[None, :]).sum(1) | |
| def leave_targets_np(z0, S, valid): | |
| """(event [N] bool, k [N] int): event at the first observed step differing from z0 within the fully observed prefix; else k = prefix length.""" | |
| prefix = np.cumprod(valid.astype(np.int64), 1) | |
| diff = (S != z0[:, None]) & (prefix > 0) | |
| has = diff.any(1) | |
| first = np.where(has, diff.argmax(1) + 1, 0) | |
| c = prefix.sum(1) | |
| return has, np.where(has, first, c) | |
| # ----------------------------------------------------------------------------- metrics + device bootstrap | |
| def _pct(v): | |
| return float(np.nanpercentile(v, 2.5)), float(np.nanpercentile(v, 97.5)) | |
| def boot_mean_pair(xa, xb, dev, n_boot, seed=0): | |
| """paired device-cluster bootstrap of two per-row loss vectors (means): -> value_a, value_b, delta, ci_d.""" | |
| uniq, inv = np.unique(dev, return_inverse=True) | |
| sa = np.bincount(inv, weights=xa, minlength=len(uniq)) | |
| sb = np.bincount(inv, weights=xb, minlength=len(uniq)) | |
| cnt = np.bincount(inv, minlength=len(uniq)).astype(np.float64) | |
| rng = np.random.default_rng(seed) | |
| d = np.empty(n_boot) | |
| for i in range(n_boot): | |
| w = np.bincount(rng.integers(0, len(uniq), len(uniq)), minlength=len(uniq)).astype(np.float64) | |
| n = (w * cnt).sum() | |
| d[i] = (w * sa).sum() / n - (w * sb).sum() / n | |
| return float(xa.mean()), float(xb.mean()), float(xa.mean() - xb.mean()), _pct(d) | |
| class AurocBoot: | |
| """weighted AUROC with tie handling, scores pre-sorted once; per bootstrap O(N).""" | |
| def __init__(self, y, s): | |
| order = np.argsort(s, kind="mergesort") | |
| self.y = y[order].astype(np.float64) | |
| ss = s[order] | |
| self.starts = np.flatnonzero(np.concatenate([[True], ss[1:] != ss[:-1]])) | |
| self.order = order | |
| def value(self, w): | |
| w = w[self.order] | |
| wp, wn = w * self.y, w * (1 - self.y) | |
| gp, gn = np.add.reduceat(wp, self.starts), np.add.reduceat(wn, self.starts) | |
| cum_neg_before = np.concatenate([[0.0], np.cumsum(gn)[:-1]]) | |
| P, N = wp.sum(), wn.sum() | |
| if P <= 0 or N <= 0: | |
| return np.nan | |
| return float((gp * (cum_neg_before + 0.5 * gn)).sum() / (P * N)) | |
| def boot_auroc_pair(y, sa, sb, dev, n_boot, seed=0): | |
| uniq, inv = np.unique(dev, return_inverse=True) | |
| A, B = AurocBoot(y, sa), AurocBoot(y, sb) | |
| ones = np.ones(len(y)) | |
| va, vb = A.value(ones), B.value(ones) | |
| rng = np.random.default_rng(seed) | |
| d = np.full(n_boot, np.nan) | |
| for i in range(n_boot): | |
| w = np.bincount(rng.integers(0, len(uniq), len(uniq)), minlength=len(uniq)).astype(np.float64)[inv] | |
| d[i] = A.value(w) - B.value(w) | |
| return va, vb, va - vb, _pct(d) | |
| def ece(p, y, n_bins=10): | |
| b = np.clip((p * n_bins).astype(int), 0, n_bins - 1) | |
| tot = 0.0 | |
| for i in range(n_bins): | |
| m = b == i | |
| if m.any(): | |
| tot += m.mean() * abs(p[m].mean() - y[m].mean()) | |
| return float(tot) | |
| def verdict_rows(rows, lower_better): | |
| """rows: list of dict(seed, value_a, value_b, delta, lo, hi) -> kill_checks verdict dict.""" | |
| g = pd.DataFrame({"seed": [r["seed"] for r in rows], "delta": [r["delta"] for r in rows], "delta_ci_lo": [r["lo"] for r in rows], "delta_ci_hi": [r["hi"] for r in rows], | |
| "value_a": [r["value_a"] for r in rows], "value_b": [r["value_b"] for r in rows]}) | |
| return RJ.verdict(g, lower_better) | |
| # ----------------------------------------------------------------------------- data | |
| def load_truth(regime, split): | |
| with np.load(KIN / f"kin_causal_{regime}_{split}.npz", allow_pickle=True) as z: | |
| S = z["S_c"].astype(np.float32) | |
| S0 = z["S0_c"].astype(np.float32) | |
| ok = z["row_ok"].astype(bool) | |
| uid, t, dongle = z["uid"], z["t"], z["dongle"] | |
| with np.load(GAZE / f"gaze_{regime}_{split}.npz", allow_pickle=True) as z: | |
| assert np.array_equal(z["uid"], uid) | |
| gok = z["row_ok"].astype(bool) | |
| Sg = z["S_g"].astype(np.float32) | |
| Sg0 = z["S0_g"].astype(np.float32) | |
| S[~ok] = np.nan | |
| S0[~ok] = np.nan | |
| Sg[~gok] = np.nan | |
| Sg0[~gok] = np.nan | |
| return {"S": np.concatenate([S, Sg[:, :, None]], -1), "S0": np.concatenate([S0, Sg0[:, None]], 1), "uid": uid, "t": t, "dongle": dongle} | |
| def load_fs(run, regime, split): | |
| with np.load(RUNS / run / f"fs_{regime}_{split}.npz", allow_pickle=True) as z: | |
| d = {k: z[k] for k in ("uid", "pi", "z0", "z0valid", "valid", "rem_dwell_E_s", "first_leave_E_s", "rem_within_3s", "leave_within_3s", "rem_dwell_p", "first_leave_p")} | |
| d["meta"] = json.loads(str(z["meta"])) | |
| assert [str(s) for s in z["state_names"]] == list(STATES) | |
| return d | |
| def load_final_pi(final_run, regime, split): | |
| with np.load(RUNS / final_run / f"pred_{regime}_{split}.npz", allow_pickle=True) as z: | |
| hon, hoff, z0 = z["hz_on"].astype(np.float32), z["hz_off"].astype(np.float32), z["z0"].astype(np.float32) | |
| out = {} | |
| for s, c in CHAIN_COL.items(): | |
| pi = forward_marginal(hon[:, :, c], hoff[:, :, c], z0[:, c]) | |
| q = first_leave_law_from_hazards(hon[:, :, c], hoff[:, :, c], z0[:, c]) | |
| out[s] = {"pi": pi.astype(np.float32), "E_leave": expected_leave_s(q).astype(np.float32), "p_leave_3s": q[:, :15].sum(1).astype(np.float32)} | |
| return out | |
| def dwell_table(regimes): | |
| """TRAIN empirical tables per state: P(s_{t+k} = 1 | z0, dwell bin) [2, n_bins, H], the conditional mean of min(T, 5.2 s) and P(leave <= 3 s) [2, n_bins].""" | |
| tab = {} | |
| Xs, Ss, S0s = [], [], [] | |
| for r in regimes: | |
| with np.load(CACHE / f"feats_{r}_train.npz", allow_pickle=True) as z: | |
| X, names = z["X"], [str(n) for n in z["names"]] | |
| zc = sorted(CACHE.glob(f"zU_*_{r}_train.npz")) | |
| assert zc, f"no z_U train cache for {r} (run train/train_future_state.py first)" | |
| with np.load(zc[0], allow_pickle=True) as z: | |
| S = np.concatenate([z["S"].astype(np.float32), z["Sg"].astype(np.float32)[:, :, None]], -1) | |
| S0 = np.concatenate([z["S0"], z["Sg0"][:, None]], 1).astype(np.float32) | |
| Xs.append(X); Ss.append(S); S0s.append(S0) | |
| X, S, S0 = np.concatenate(Xs), np.concatenate(Ss), np.concatenate(S0s) | |
| nb = len(DWELL_BINS) - 1 | |
| for si, s in enumerate(STATES): | |
| d = X[:, names.index(DWELL_FEAT[s])] | |
| db = np.where(np.isfinite(d), np.clip(np.searchsorted(DWELL_BINS, d, side="right") - 1, 0, nb - 1), nb) # bin nb = unknown dwell | |
| pi_t = np.full((2, nb + 1, H), np.nan, np.float32) | |
| E_t = np.full((2, nb + 1), np.nan, np.float32) | |
| p3_t = np.full((2, nb + 1), np.nan, np.float32) | |
| z0 = S0[:, si] | |
| ok0 = np.isfinite(z0) | |
| valid = np.isfinite(S[:, :, si]) | |
| ev, k = leave_targets_np(np.nan_to_num(z0), np.nan_to_num(S[:, :, si]), valid) | |
| obs = ok0 & (ev | (k == H)) | |
| tmin = np.minimum(np.where(ev, k, H + 1), H + 1) * DT | |
| for v in (0, 1): | |
| for b in range(nb + 1): | |
| m = ok0 & (z0 == v) & (db == b) | |
| if m.sum() < 50: | |
| continue | |
| for kk in range(H): | |
| mk = m & valid[:, kk] | |
| if mk.sum() >= 50: | |
| pi_t[v, b, kk] = S[mk, kk, si].mean() | |
| mo = m & obs | |
| if mo.sum() >= 50: | |
| E_t[v, b] = tmin[mo].mean() | |
| p3_t[v, b] = (ev[mo] & (k[mo] <= 15)).mean() | |
| tab[s] = {"pi": pi_t, "E_leave": E_t, "p_leave_3s": p3_t, "names": names} | |
| return tab | |
| def apply_table(tab, s, z0, X, names): | |
| nb = len(DWELL_BINS) - 1 | |
| d = X[:, names.index(DWELL_FEAT[s])] | |
| db = np.where(np.isfinite(d), np.clip(np.searchsorted(DWELL_BINS, d, side="right") - 1, 0, nb - 1), nb) | |
| v = np.nan_to_num(z0).astype(int).clip(0, 1) | |
| pi = tab[s]["pi"][v, db] # [N,H] | |
| # fall back to the persistence value where the table has no entry | |
| pi = np.where(np.isfinite(pi), pi, v[:, None].astype(np.float32)) | |
| E = tab[s]["E_leave"][v, db] | |
| p3 = tab[s]["p_leave_3s"][v, db] | |
| return pi.astype(np.float32), E.astype(np.float32), p3.astype(np.float32) | |
| # ----------------------------------------------------------------------------- main evaluation | |
| def evaluate(name, seeds, split, n_boot, regimes): | |
| t0 = time.time() | |
| tab = dwell_table(regimes) | |
| cells, seg_rows, rec_rows, cal_rows = {}, {}, {}, {} | |
| prov = [] | |
| for seed in seeds: | |
| run, final_run = f"FS_{name}_s{seed}", FINAL_RUN.format(seed=seed) | |
| for regime in regimes: | |
| truth = load_truth(regime, split) | |
| fs = load_fs(run, regime, split) | |
| assert np.array_equal(fs["uid"], truth["uid"]), "fs rows are not the kin target rows" | |
| fin = load_final_pi(final_run, regime, split) | |
| with np.load(CACHE / f"feats_{regime}_{split}.npz", allow_pickle=True) as z: | |
| Xf, names = z["X"], [str(n) for n in z["names"]] | |
| assert np.array_equal(z["uid"], truth["uid"]) | |
| prov.append({"seed": seed, "regime": regime, "fs_run": run, "fs_model_id": fs["meta"].get("model_id"), "fs_code": str(fs["meta"].get("code_sha256", ""))[:12], | |
| "final_run": final_run, "n_rows": int(len(truth["t"]))}) | |
| dev = truth["dongle"] | |
| for si, s in enumerate(STATES): | |
| z0 = truth["S0"][:, si] | |
| base_ok = fs["valid"].astype(bool) & np.isfinite(z0) & fs["z0valid"][:, si] | |
| pi_fs = fs["pi"][:, :, si].astype(np.float32) | |
| pi_pers = np.repeat(np.nan_to_num(z0)[:, None], H, 1).astype(np.float32) | |
| pi_tab, E_tab, p3_tab = apply_table(tab, s, z0, Xf, names) | |
| pi_fin = fin[s]["pi"] if s in fin else None | |
| baselines = {"persistence": pi_pers, "dwell_table": pi_tab} | |
| if pi_fin is not None: | |
| baselines["final_closed_form"] = pi_fin | |
| for hn, k in HORIZONS.items(): | |
| y = truth["S"][:, k - 1, si] | |
| m = base_ok & np.isfinite(y) | |
| yb = y[m].astype(np.float64) | |
| if yb.sum() < 20 or (1 - yb).sum() < 20: | |
| continue | |
| for bn, pb in baselines.items(): | |
| key = (regime, s, hn, bn) | |
| a, b = pi_fs[m, k - 1].astype(np.float64), pb[m, k - 1].astype(np.float64) | |
| va, vb, d, ci = boot_mean_pair((a - yb) ** 2, (b - yb) ** 2, dev[m], n_boot) | |
| ua, ub, du, ciu = boot_auroc_pair(yb, a, b, dev[m], min(n_boot, 300)) | |
| cells.setdefault(key, []).append({"seed": seed, "brier": {"value_a": va, "value_b": vb, "delta": d, "lo": ci[0], "hi": ci[1]}, | |
| "auroc": {"value_a": ua, "value_b": ub, "delta": du, "lo": ciu[0], "hi": ciu[1]}, "n": int(m.sum()), "pos": int(yb.sum())}) | |
| # calibration at 3 s | |
| y3 = truth["S"][:, 14, si] | |
| m3 = base_ok & np.isfinite(y3) | |
| cal = {"FS": ece(pi_fs[m3, 14], y3[m3]), "dwell_table": ece(pi_tab[m3, 14], y3[m3])} | |
| if pi_fin is not None: | |
| cal["final_closed_form"] = ece(pi_fin[m3, 14], y3[m3]) | |
| cal_rows.setdefault((regime, s), []).append(dict(cal, seed=seed, n=int(m3.sum()))) | |
| # segment classes at 3 s (true states) | |
| off = OFF_VALUE[s] | |
| pre = truth["S"][:, :15, si] | |
| full = m3 & np.isfinite(pre).all(1) | |
| cur_off = np.nan_to_num(z0) == off | |
| any_off = (pre == off).any(1) | |
| any_on = (pre == 1 - off).any(1) | |
| classes = {"static": full & ~cur_off & ~any_off, "onset": full & ~cur_off & any_off, "sustained": full & cur_off & ~any_on, "recovery": full & cur_off & any_on} | |
| for cn, cm in classes.items(): | |
| if cm.sum() < 50: | |
| continue | |
| yy = y3[cm] | |
| row = {"seed": seed, "n": int(cm.sum()), "share": float(cm.sum() / max(full.sum(), 1)), "FS": float(((pi_fs[cm, 14] - yy) ** 2).mean()), | |
| "persistence": float(((pi_pers[cm, 14] - yy) ** 2).mean()), "dwell_table": float(((pi_tab[cm, 14] - yy) ** 2).mean())} | |
| if pi_fin is not None: | |
| row["final_closed_form"] = float(((pi_fin[cm, 14] - yy) ** 2).mean()) | |
| seg_rows.setdefault((regime, s, cn), []).append(row) | |
| # recovery time / remaining dwell (off-task rows with an observed first-leave) and first-leave time (all rows) | |
| valid = np.isfinite(truth["S"][:, :, si]) | |
| ev, kk = leave_targets_np(np.nan_to_num(z0), np.nan_to_num(truth["S"][:, :, si]), valid) | |
| obs = base_ok & (ev | (kk == H)) | |
| tmin = (np.minimum(np.where(ev, kk, H + 1), H + 1) * DT).astype(np.float64) | |
| for which, rows_m, E_fs, p3_fs in (("remaining_dwell_off", obs & cur_off, fs["rem_dwell_E_s"][:, si], fs["rem_within_3s"][:, si].astype(np.float32)), | |
| ("first_leave_all", obs, fs["first_leave_E_s"][:, si], fs["leave_within_3s"][:, si].astype(np.float32))): | |
| if rows_m.sum() < 100: | |
| continue | |
| y_ev3 = (ev[rows_m] & (kk[rows_m] <= 15)).astype(np.float64) | |
| comp = {"dwell_table": (E_tab, p3_tab)} | |
| if s in fin: | |
| comp["final_closed_form"] = (fin[s]["E_leave"], fin[s]["p_leave_3s"]) | |
| rec = {"seed": seed, "n": int(rows_m.sum()), "event_share": float(ev[rows_m].mean()), "mae_FS": float(np.abs(E_fs[rows_m] - tmin[rows_m]).mean()), | |
| "ece3_FS": ece(np.clip(p3_fs[rows_m], 0, 1), y_ev3), "obs_rate3": float(y_ev3.mean())} | |
| for bn, (Eb, p3b) in comp.items(): | |
| Eb_ = np.where(np.isfinite(Eb), Eb, (H + 1) * DT) | |
| va, vb, d, ci = boot_mean_pair(np.abs(E_fs[rows_m] - tmin[rows_m]), np.abs(Eb_[rows_m] - tmin[rows_m]), dev[rows_m], n_boot) | |
| rec[f"mae_{bn}"] = vb | |
| rec[f"delta_{bn}"] = {"value_a": va, "value_b": vb, "delta": d, "lo": ci[0], "hi": ci[1]} | |
| rec[f"ece3_{bn}"] = ece(np.clip(np.nan_to_num(p3b[rows_m], nan=float(y_ev3.mean())), 0, 1), y_ev3) | |
| rec_rows.setdefault((regime, s, which), []).append(rec) | |
| print(f"[{time.time() - t0:.0f}s] seed {seed} {regime}/{split} done", flush=True) | |
| return cells, seg_rows, rec_rows, cal_rows, prov | |
| def render(name, seeds, split, n_boot, cells, seg_rows, rec_rows, cal_rows, prov, out): | |
| def ms(v): | |
| v = [float(x) for x in v] | |
| return f"{np.mean(v):.4f} ± {np.std(v, ddof=1):.4f}" if len(v) > 1 else f"{v[0]:.4f}" | |
| L = [f"# F2′ future-state head — occupancy / recovery evaluation (`FS_{name}_s<k>`, {split} split, seeds {seeds}, n_boot {n_boot})", "", | |
| "Detached horizon-conditioned head (models/future_state_head.py, ~0.034 M params) on the FROZEN FINAL joint model's driver state z_U + label-free dwell features " | |
| "(train/train_future_state.py FEATURES), trained on the TRAIN anchors of both regimes, selected on the validation occupancy BCE. States: hands_on / head_turned " | |
| "(v2 causal kin targets) and gaze_off (our own landmark chain, validation pending). Baselines: (a) persistence pi_k = z0; (b) dwell table = TRAIN empirical " | |
| "P(s_{t+k} | z0, time-in-state bin) (dwell-timer extrapolation, no model); (c) FINAL closed form = forward_marginal of the frozen model's own hazards (hands / head only). " | |
| "Δ = FS − baseline, shared-resample device-cluster bootstrap per seed; verdict = kill_checks.beats over seeds (Brier lower better: BEATS = FS lower). " | |
| "Nothing here changes the headline model; the adoption question of F2′ is answered downstream by the WP2 timing evaluator (alert_timing_eval) and the warning head.", "", | |
| "## Provenance", "", "| seed | regime | FS run | FS model_id (frozen joint) | FS code | FINAL run | rows |", "|---|---|---|---|---|---|---|"] | |
| L += [f"| {p['seed']} | {p['regime']} | {p['fs_run']} | {p['fs_model_id']} | {p['fs_code']} | {p['final_run']} | {p['n_rows']:,} |" for p in prov] | |
| js = {"name": name, "seeds": seeds, "split": split, "n_boot": n_boot, "cells": {}, "segments": {}, "recovery": {}, "calibration": {}, "provenance": prov} | |
| summary = {} | |
| for metric, lb in (("brier", True), ("auroc", False)): | |
| L += ["", f"## Occupancy {metric.upper()} of P(state = 1 at t + k) — FS vs baselines (Δ = FS − baseline{', lower better' if lb else ''})", "", | |
| "| regime | state | horizon | baseline | FS per seed | baseline per seed | Δ per seed [CI] | mean Δ ± sd | verdict | n / pos |", "|---|---|---|---|---|---|---|---|---|---|"] | |
| for (regime, s, hn, bn), rows in sorted(cells.items(), key=lambda kv: (kv[0][0], STATES.index(kv[0][1]), HORIZONS[kv[0][2]], kv[0][3])): | |
| rr = [dict(r[metric], seed=r["seed"]) for r in rows] | |
| v = verdict_rows(rr, lb) | |
| d = np.array([r["delta"] for r in rr]) | |
| A = " / ".join(f"{r['value_a']:.4f}" for r in rr) | |
| B = " / ".join(f"{r['value_b']:.4f}" for r in rr) | |
| D = " / ".join(f"{r['delta']:+.4f} [{r['lo']:+.4f},{r['hi']:+.4f}]" for r in rr) | |
| L.append(f"| {regime} | {s} | {hn} | {bn} | {A} | {B} | {D} | {d.mean():+.4f} ± {d.std(ddof=1) if len(d) > 1 else 0:.4f} | {v['verdict']} | {rows[0]['n']:,} / {rows[0]['pos']:,} |") | |
| js["cells"][f"{regime}|{s}|{hn}|{bn}|{metric}"] = {"rows": rr, "verdict": v["verdict"], "mean_delta": float(d.mean())} | |
| summary.setdefault((metric, bn), []).append(v["verdict"]) | |
| L += ["", "## Verdict counts (cells = regime x state x horizon)", "", "| metric | baseline | BEATS | NOT_WORSE | INCONCLUSIVE | WORSE | INSUFFICIENT |", "|---|---|---|---|---|---|---|"] | |
| for (metric, bn), vs in sorted(summary.items()): | |
| L.append(f"| {metric} | {bn} | " + " | ".join(str(vs.count(x)) for x in ("BEATS", "NOT_WORSE", "INCONCLUSIVE", "WORSE", "INSUFFICIENT")) + " |") | |
| js.setdefault("verdict_counts", {})[f"{metric}|{bn}"] = {x: vs.count(x) for x in ("BEATS", "NOT_WORSE", "INCONCLUSIVE", "WORSE", "INSUFFICIENT")} | |
| L += ["", "## Calibration at 3 s — ECE (10 bins) of P(state = 1 at t + 3 s)", "", "| regime | state | FS | dwell table | FINAL closed form | n |", "|---|---|---|---|---|---|"] | |
| for (regime, s), rows in sorted(cal_rows.items(), key=lambda kv: (kv[0][0], STATES.index(kv[0][1]))): | |
| L.append(f"| {regime} | {s} | {ms([r['FS'] for r in rows])} | {ms([r['dwell_table'] for r in rows])} | {ms([r['final_closed_form'] for r in rows]) if 'final_closed_form' in rows[0] else 'n/a'} | {rows[0]['n']:,} |") | |
| js["calibration"][f"{regime}|{s}"] = rows | |
| L += ["", "## Segment classes at 3 s (true states): Brier of P(state at t + 3 s) per class", "", | |
| "| regime | state | class | share of rows | FS | persistence | dwell table | FINAL closed form | n |", "|---|---|---|---|---|---|---|---|---|"] | |
| for (regime, s, cn), rows in sorted(seg_rows.items(), key=lambda kv: (kv[0][0], STATES.index(kv[0][1]), ("static", "onset", "sustained", "recovery").index(kv[0][2]))): | |
| L.append(f"| {regime} | {s} | {cn} | {np.mean([r['share'] for r in rows]):.3f} | {ms([r['FS'] for r in rows])} | {ms([r['persistence'] for r in rows])} | {ms([r['dwell_table'] for r in rows])} | " | |
| f"{ms([r['final_closed_form'] for r in rows]) if 'final_closed_form' in rows[0] else 'n/a'} | {rows[0]['n']:,} |") | |
| js["segments"][f"{regime}|{s}|{cn}"] = rows | |
| L += ["", "## Recovery time (remaining dwell in the off-task state) and first-leave time — MAE (s) of E[min(T, 5.2 s)] and calibration of P(leave within 3 s)", "", | |
| "Rows: anchors whose first-leave time is observed (event within 5 s, or the full 5 s observed without a leave -> target 5.2 s). Δ = FS − baseline MAE (lower better), device-cluster bootstrap.", "", | |
| "| regime | state | rows | n | event share | MAE FS | MAE dwell table | Δ vs table per seed [CI] | verdict | MAE FINAL closed form | Δ vs FINAL per seed [CI] | verdict | ECE3 FS / table / FINAL | observed P(leave ≤ 3 s) |", | |
| "|---|---|---|---|---|---|---|---|---|---|---|---|---|---|"] | |
| for (regime, s, which), rows in sorted(rec_rows.items(), key=lambda kv: (kv[0][0], STATES.index(kv[0][1]), kv[0][2])): | |
| vt = verdict_rows([dict(r["delta_dwell_table"], seed=r["seed"]) for r in rows], True) | |
| dt_ = " / ".join(f"{r['delta_dwell_table']['delta']:+.3f} [{r['delta_dwell_table']['lo']:+.3f},{r['delta_dwell_table']['hi']:+.3f}]" for r in rows) | |
| if "delta_final_closed_form" in rows[0]: | |
| vf = verdict_rows([dict(r["delta_final_closed_form"], seed=r["seed"]) for r in rows], True) | |
| df_ = " / ".join(f"{r['delta_final_closed_form']['delta']:+.3f} [{r['delta_final_closed_form']['lo']:+.3f},{r['delta_final_closed_form']['hi']:+.3f}]" for r in rows) | |
| mf, ef, vfv = ms([r["mae_final_closed_form"] for r in rows]), ms([r["ece3_final_closed_form"] for r in rows]), vf["verdict"] | |
| else: | |
| mf, df_, ef, vfv = "n/a", "n/a", "n/a", "n/a" | |
| L.append(f"| {regime} | {s} | {which} | {rows[0]['n']:,} | {np.mean([r['event_share'] for r in rows]):.3f} | {ms([r['mae_FS'] for r in rows])} | {ms([r['mae_dwell_table'] for r in rows])} | {dt_} | {vt['verdict']} | " | |
| f"{mf} | {df_} | {vfv} | {ms([r['ece3_FS'] for r in rows])} / {ms([r['ece3_dwell_table'] for r in rows])} / {ef} | {np.mean([r['obs_rate3'] for r in rows]):.3f} |") | |
| js["recovery"][f"{regime}|{s}|{which}"] = {"rows": rows, "verdict_vs_table": vt["verdict"], "verdict_vs_final": vfv} | |
| out = Path(out); out.parent.mkdir(parents=True, exist_ok=True) | |
| out.write_text("\n".join(L) + "\n") | |
| out.with_suffix(".json").write_text(json.dumps(js, indent=1, default=str)) | |
| return L | |
| def attribution_section(arms, out, split): | |
| """--arms: append a '## Attribution arms' block to the md of the first arm from the per-arm json files (future_state_eval[_<arm>].json): per regime x state x | |
| horizon the Brier / AUROC of every arm next to the baselines (persistence, dwell table, FINAL closed form) and each arm's verdict vs the FINAL closed form and vs | |
| the dwell table; plus the verdict counts. Arms: dwell (z_U + dwell features), zonly (z_U + regime only), featonly (dwell features + regime, z_U zeroed).""" | |
| js = {} | |
| for a in arms: | |
| pth = Path(out).with_suffix(".json") if a == arms[0] else Path(out).with_name(Path(out).stem + f"_{a}.json") | |
| if not pth.exists(): | |
| print(f"attribution: {pth} missing -> skipped"); continue | |
| js[a] = json.loads(pth.read_text()) | |
| if len(js) < 2: | |
| return | |
| L = ["", f"## Attribution arms ({split} split): what carries the gain — the trunk state, the dwell history, or both", "", | |
| "Arms trained with the identical recipe / selection / caches: `dwell` = z_U + 20 dwell features + regime (the F2′ head); `zonly` = z_U + regime only (the detached head + " | |
| "occupancy BCE on the trunk's own information); `featonly` = dwell features + regime with z_U zeroed (the 8 s dwell / switch history alone). Baselines as above: " | |
| "persistence pi_k = z0, dwell table (TRAIN empirical P(s_{t+k} | z0, time-in-state bin)), FINAL closed form (the frozen model's own hazards through forward_marginal). " | |
| "Verdicts = kill_checks.beats over the 3 seeds (Brier lower better).", ""] | |
| for metric in ("brier", "auroc"): | |
| L += [f"### {metric.upper()} of P(state = 1 at t + k), 3-seed mean", "", | |
| "| regime | state | horizon | persistence | dwell table | FINAL closed form | " + " | ".join(js.keys()) + " | " + " | ".join(f"{a} vs FINAL" for a in js) + " | " + " | ".join(f"{a} vs table" for a in js) + " |", | |
| "|---|---|---|---|---|---|" + "---|" * (3 * len(js))] | |
| keys = sorted({tuple(k.split("|")[:3]) for a in js for k in js[a]["cells"] if k.endswith(f"|{metric}")}, key=lambda t: (t[0], STATES.index(t[1]), HORIZONS[t[2]])) | |
| for regime, st, hn in keys: | |
| def cell(a, bn): | |
| return js[a]["cells"].get(f"{regime}|{st}|{hn}|{bn}|{metric}") | |
| ref = cell(arms[0], "persistence") or next((cell(a, "persistence") for a in js if cell(a, "persistence")), None) | |
| if ref is None: | |
| continue | |
| pers = np.mean([r["value_b"] for r in ref["rows"]]) | |
| tab = cell(arms[0], "dwell_table"); tab = np.mean([r["value_b"] for r in tab["rows"]]) if tab else float("nan") | |
| fin = cell(arms[0], "final_closed_form"); fin = np.mean([r["value_b"] for r in fin["rows"]]) if fin else float("nan") | |
| vals = [np.mean([r["value_a"] for r in cell(a, "persistence")["rows"]]) if cell(a, "persistence") else float("nan") for a in js] | |
| vf = [(cell(a, "final_closed_form") or {}).get("verdict", "n/a") for a in js] | |
| vt = [(cell(a, "dwell_table") or {}).get("verdict", "n/a") for a in js] | |
| L.append(f"| {regime} | {st} | {hn} | {pers:.4f} | {tab:.4f} | {fin:.4f} | " + " | ".join(f"{v:.4f}" for v in vals) + " | " + " | ".join(vf) + " | " + " | ".join(vt) + " |") | |
| L.append("") | |
| L += ["### Verdict counts per arm (cells = regime x state x horizon; hands / head only for the FINAL closed form)", "", | |
| "| arm | metric | vs FINAL closed form: BEATS / NOT_WORSE / INCONCLUSIVE / WORSE | vs dwell table: BEATS / NOT_WORSE / INCONCLUSIVE / WORSE | vs persistence: BEATS / rest |", "|---|---|---|---|---|"] | |
| for a in js: | |
| vc = js[a].get("verdict_counts", {}) | |
| for metric in ("brier", "auroc"): | |
| f = vc.get(f"{metric}|final_closed_form", {}); t = vc.get(f"{metric}|dwell_table", {}); pz = vc.get(f"{metric}|persistence", {}) | |
| fmt = lambda d: " / ".join(str(d.get(x, 0)) for x in ("BEATS", "NOT_WORSE", "INCONCLUSIVE", "WORSE")) | |
| L.append(f"| {a} | {metric} | {fmt(f)} | {fmt(t)} | {pz.get('BEATS', 0)} / {sum(v for k, v in pz.items() if k != 'BEATS')} |") | |
| L += ["", "Reading: `zonly` reproduces the FINAL closed form (the head + occupancy loss add nothing on the trunk's own information); `featonly` beats the FINAL recursion on the hands " | |
| "chain and on engaged head_turned but is WORSE on manual head_turned (the trunk state carries the head chain in manual driving); `dwell` = both sources is best in every cell — " | |
| "the gain is complementary information (8 s dwell / switch history + gaze anchor state), not the loss or the head.", ""] | |
| with open(out, "a") as f: | |
| f.write("\n".join(L) + "\n") | |
| jp = Path(out).with_suffix(".json") | |
| j = json.loads(jp.read_text()) | |
| j["attribution_arms"] = {a: js[a].get("verdict_counts", {}) for a in js} | |
| jp.write_text(json.dumps(j, indent=1, default=str)) | |
| print("attribution section appended to", out) | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--name", default="dwell") | |
| ap.add_argument("--arms", default="", help="attribution only: comma list of finished arms (first = the main table), e.g. dwell,zonly,featonly -> appends a section to --out") | |
| ap.add_argument("--seeds", default="0,1,2") | |
| ap.add_argument("--split", default="test") | |
| ap.add_argument("--n-boot", type=int, default=500) | |
| ap.add_argument("--regimes", default="manual,engaged") | |
| ap.add_argument("--out", default=str(CWM3 / "results" / "tables" / "future_state_eval.md")) | |
| a = ap.parse_args() | |
| if a.arms: | |
| attribution_section(a.arms.split(","), a.out, a.split) | |
| return | |
| seeds = [int(s) for s in a.seeds.split(",") if (RUNS / f"FS_{a.name}_s{s}" / "PRED_DONE").exists()] | |
| assert seeds, "no finished FS run" | |
| regimes = a.regimes.split(",") | |
| cells, seg, rec, cal, prov = evaluate(a.name, seeds, a.split, a.n_boot, regimes) | |
| L = render(a.name, seeds, a.split, a.n_boot, cells, seg, rec, cal, prov, a.out) | |
| print("\n".join(L[:8])) | |
| print(f"written {a.out}") | |
| if __name__ == "__main__": | |
| main() | |
Xet Storage Details
- Size:
- 32.7 kB
- Xet hash:
- 241aaf727983fa249b95999acddb39d1b0e212ad04f4f9c7bf59ceef586e8c3e
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.