AsianPlayer/TriDrive / code /eval /paired_compare_joint.py
AsianPlayer's picture
download
raw
20.9 kB
"""cwm3/eval/paired_compare_joint.py — paired comparison of two pred_layout files on the rows BOTH scored, for the joint-model stages.
Extends eval/paired_compare.py (imported: boot_pair, COLS, OUT — the same append-only results/paired_compare.csv and column set) with the
cells the joint model adds: cross-leg endpoints C1 / C1_given_k / C2_<f> / C2_hand_<f> / C2_head_<f> / C2_given_k_<f> (populations.P_C1 /
P_C2 rows, PR-AUC and AUROC), hazard-NLL of the driver / vehicle / demand chains (mean over the first-passage-contract steps, as eval_grid),
veh_any_on onset, cooccur Brier, plus everything paired_compare does (drv / veh / d onset PR-AUC@3s, y_mae / y_crps / f_cos means,
nr_brier). Every cell: intersection of finite rows of A and B, shared-resample device-cluster bootstrap (n_boot 1000, seed 0), Δ = A − B.
python paired_compare_joint.py --regime manual --a results/runs/I1_all_s0 --b results/runs/I1_indep_s0 [--split test] [--tag I1_s0_all_vs_indep]
K4: --kin-targets causal_routes (driver populations from results/targets_kin_causal_routes[_v2], --kin-version v1|v2) and --mask-routes <suspects json> (protocol b).
CPU only; reads the targets / kin / demand files and the two layouts; no openpilot DMS field.
"""
from __future__ import annotations
import argparse
import csv
import sys
import time
from pathlib import Path
import numpy as np
CWM3 = Path(__file__).resolve().parents[1]
EXPLORE = CWM3.parent
CWM2 = EXPLORE / "cwm2"
for p in (CWM2 / "eval", CWM2 / "data", EXPLORE / "s12_wm", CWM3 / "common", CWM3 / "eval"):
sys.path.insert(0, str(p))
import populations as P # noqa: E402
from pred_layout import load_pred, col, TAU_NAME, FAMILIES # noqa: E402
import importlib.util as _ilu # noqa: E402
_spec = _ilu.spec_from_file_location("cwm3_runmeta", CWM3 / "common" / "runmeta.py") # 2026-09-11: cwm2/eval/populations imports cwm2/data/runmeta.py (unfixed stamp-collision
_rm = _ilu.module_from_spec(_spec); _spec.loader.exec_module(_rm) # assert) into sys.modules["runmeta"] first; load cwm3's fixed writer by path
write_run_json = _rm.write_run_json
from paired_compare import boot_pair, COLS, OUT, layout_guard, guard_line # noqa: E402
from pagecache import drop_all as _drop_cache # noqa: E402
def auroc_weighted(y, s, w):
order = np.argsort(s, kind="mergesort")
ys, ws = y[order], w[order]
# weighted rank-sum AUROC (ties broken by order; scores are continuous)
cum_neg = np.cumsum(ws * ~ys)
pos_w = ws * ys
n_pos, n_neg = pos_w.sum(), (ws * ~ys).sum()
if n_pos <= 0 or n_neg <= 0:
return np.nan
return float((pos_w * cum_neg).sum() / (n_pos * n_neg))
def boot_pair_auroc(y, sa, sb, dongle, n_boot, seed):
uniq, dev = np.unique(dongle, return_inverse=True)
rng = np.random.default_rng(seed)
ones = np.ones(len(sa))
pa, pb = auroc_weighted(y, sa, ones), auroc_weighted(y, sb, ones)
da, db = np.full(n_boot, np.nan), np.full(n_boot, np.nan)
for i in range(n_boot):
cnt = np.bincount(rng.integers(0, len(uniq), len(uniq)), minlength=len(uniq)).astype(np.float64)
w = cnt[dev]
if (w * y).sum() <= 0 or (w * y).sum() >= w.sum():
continue
da[i], db[i] = auroc_weighted(y, sa, w), auroc_weighted(y, sb, w)
d = da - db
ok = np.isfinite(d)
pct = lambda v: (float(np.nanpercentile(v, 2.5)), float(np.nanpercentile(v, 97.5))) # noqa: E731
return {"value_a": pa, "value_b": pb, "ci_a": pct(da), "ci_b": pct(db), "delta": pa - pb, "ci_d": pct(d),
"frac_gt0": float((d[ok] > 0).mean()) if ok.any() else np.nan, "n_dev": int(len(uniq))}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--regime", required=True, choices=["manual", "engaged"])
ap.add_argument("--a", required=True)
ap.add_argument("--b", required=True)
ap.add_argument("--split", default="test")
ap.add_argument("--tag", default="")
ap.add_argument("--n-boot", type=int, default=1000)
ap.add_argument("--taus", default="3s")
ap.add_argument("--min-rows", type=int, default=1000)
ap.add_argument("--min-pos", type=int, default=20)
ap.add_argument("--kin-targets", default="cwm2", choices=["cwm2", "causal_routes"], help="K4: build the driver populations from the causal route-level kin targets")
ap.add_argument("--kin-version", default="v1", choices=["v1", "v2"], help="causal kin pack build (v2 = results/targets_kin_causal_routes_v2, population driver-side prior)")
ap.add_argument("--mask-routes", default="", help="json with `flagged` [{uid}]: rows on these routes are removed from every population (protocol b)")
ap.add_argument("--b-k0", action="store_true", help="B = the current-state k0 heuristic (eval_grid.k0_score form: -dist / dist / |head_turn_dev| from the kin K0 of the chosen "
"target set) instead of a second layout; only the driver onset cells are paired (claim B1)")
ap.add_argument("--only-onset", action="store_true", help="opt-in (2026-09-12 robustness table): pair the onset cells (drv / veh / d + veh_any_on) only — no cross-leg, hazard-NLL, "
"per-row-mean, nr / cooccur cells (identical numbers for the cells kept; default off = every cell as before)")
a = ap.parse_args()
t0 = time.time()
stamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())
guards = [layout_guard(a.a, a.regime, a.split)] + ([] if a.b_k0 else [layout_guard(a.b, a.regime, a.split)]) # abort on stale / unfinished layouts
for g in guards:
print(guard_line(g), flush=True)
if a.kin_targets == "causal_routes":
KINC = CWM3 / "results" / ("targets_kin_causal_routes_v2" if a.kin_version == "v2" else "targets_kin_causal_routes")
def load_kin_causal(regime, split, tg):
d = np.load(KINC / f"kin_causal_{regime}_{split}.npz", allow_pickle=True)
assert np.array_equal(d["uid"], tg["uid"]) and np.allclose(d["t"], tg["t"]), "causal kin rows != targets rows"
ok = d["row_ok"].astype(bool)
S = d["S_c"].astype(np.float32); S[~ok] = np.nan
S0 = d["S0_c"].astype(np.float32); S0[~ok] = np.nan
EM = d["EM_c"].astype(bool) & tg["M"][:, :, None] & ok[:, None, None]
return {"S": S, "S0": S0, "E": d["E_c"] * EM, "EM": EM, "E0": d["E0_c"], "K0": d["K0_c"], "Kvalid": d["Kvalid"] & ok[:, None]}
P.load_kin = load_kin_causal
data = P.load_all(a.regime, a.split)
if a.mask_routes:
import json as _json
flagged = {r["uid"] for r in _json.loads(Path(a.mask_routes).read_text())["flagged"]}
drop = np.isin(data["targets"]["uid"], list(flagged))
# remove the flagged rows from every population by censoring their targets (M False, kin EM False) -> they never enter sel
data["targets"]["M"][drop] = False
data["kin"]["EM"][drop] = False
print(f"mask-routes: {int(drop.sum()):,} rows on {len(flagged)} flagged routes censored out of the populations", flush=True)
tg, kin, dm = data["targets"], data["kin"], data["demand"]
N = len(tg["t"])
pa = load_pred(Path(a.a) / f"pred_layout_{a.regime}_{a.split}.npz", tg)
if a.b_k0:
K0 = kin["K0"].astype(np.float64)
dist = np.where(np.isfinite(K0[:, 10]), K0[:, 10], np.nanmean(K0[:, 10]))
hdev = np.nan_to_num(np.abs(K0[:, 7]), nan=0.0)
k0s = {"hand_return": -dist, "hand_leave": dist, "head_turn_onset": hdev}
# a pseudo-layout with the k0 scores broadcast over the onset taus for the three driver events (other events NaN)
pb = {"event_names": pa["event_names"], "onset_taus": pa["onset_taus"], "onset_p": np.full(pa["onset_p"].shape, np.nan, np.float32), "meta": {"arm": "k0_heuristic"}}
for ev, sc in k0s.items():
e = col([str(x) for x in pa["event_names"]], ev)
if e is not None:
pb["onset_p"][:, :, e] = sc[:, None]
nb = "k0_heuristic" + (("_causal" + ("_v2" if a.kin_version == "v2" else "")) if a.kin_targets == "causal_routes" else "")
else:
pb = load_pred(Path(a.b) / f"pred_layout_{a.regime}_{a.split}.npz", tg)
nb = Path(a.b).name
na = Path(a.a).name
_drop_cache(list(data.get("files", [])) + [Path(a.a) / f"pred_layout_{a.regime}_{a.split}.npz", Path(a.b) / f"pred_layout_{a.regime}_{a.split}.npz"]
+ ([KINC / f"kin_causal_{a.regime}_{a.split}.npz"] if a.kin_targets == "causal_routes" else [])) # loaded -> evict cached pages (free-memory guard)
pops = P.build_all(data, tuple(a.taus.split(",")))
rows = []
def add(cell, metric, res, n, n_pos):
rows.append({"run_stamp": stamp, "tag": a.tag, "regime": a.regime, "split": a.split, "cell": cell, "metric": metric, "a": na, "b": nb,
"value_a": res["value_a"], "ci_a_lo": res["ci_a"][0], "ci_a_hi": res["ci_a"][1], "value_b": res["value_b"],
"ci_b_lo": res["ci_b"][0], "ci_b_hi": res["ci_b"][1], "delta": res["delta"], "delta_ci_lo": res["ci_d"][0],
"delta_ci_hi": res["ci_d"][1], "frac_boot_d_gt0": res["frac_gt0"], "n": n, "n_pos": n_pos, "n_dev": res["n_dev"], "n_boot": a.n_boot})
print(f" {cell:30s} {metric:7s} A={res['value_a']:.4f} B={res['value_b']:.4f} Δ={res['delta']:+.4f} [{res['ci_d'][0]:+.4f},{res['ci_d'][1]:+.4f}] "
f"P(Δ>0)={res['frac_gt0']:.2f} n={n} pos={n_pos}", flush=True)
def pair_scores(cell, pop, sa, sb, metrics=("pr_auc",)):
keep = np.isfinite(sa) & np.isfinite(sb)
if keep.sum() < a.min_rows or pop.y[keep].sum() < a.min_pos or pop.y[keep].sum() >= keep.sum():
return
for m in metrics:
if m == "pr_auc":
res = boot_pair(pop.y[keep], sa[keep], sb[keep], pop.dongle[keep], a.n_boot, 0, "ap")
else:
res = boot_pair_auroc(pop.y[keep], sa[keep], sb[keep], pop.dongle[keep], a.n_boot, 0)
add(cell, m, res, int(keep.sum()), int(pop.y[keep].sum()))
# ---- onset cells (drv / veh / d) + veh_any_on
ev_a, ev_b = [str(x) for x in pa["event_names"]], [str(x) for x in pb["event_names"]]
for name, pop in pops.items():
parts = name.split("|")
if len(parts) != 3 or parts[0] not in ("drv_onset", "veh_onset", "d_onset"):
continue
kind, ev, tau = parts
tau = int(tau)
evn = f"D_{ev}" if kind == "d_onset" else ev
e_a, e_b = col(ev_a, evn), col(ev_b, evn)
if e_a is None or e_b is None:
continue
ti = list(map(int, pa["onset_taus"])).index(tau)
sa = pa["onset_p"][pop.sel, ti, e_a].astype(np.float64)
sb = pb["onset_p"][pop.sel, ti, e_b].astype(np.float64)
pair_scores(f"{kind}|{ev}|{TAU_NAME.get(tau, tau)}", pop, sa, sb)
if a.b_k0:
# persistence (constant score) = base rate of each driver population: recorded as a row with metric "base_rate"
for name, pop in pops.items():
parts = name.split("|")
if len(parts) == 3 and parts[0] == "drv_onset" and int(parts[2]) == 15:
rows.append({"run_stamp": stamp, "tag": a.tag, "regime": a.regime, "split": a.split, "cell": f"drv_onset|{parts[1]}|3s", "metric": "base_rate",
"a": na, "b": "persistence", "value_a": float(pop.y.mean()), "ci_a_lo": "", "ci_a_hi": "", "value_b": float(pop.y.mean()), "ci_b_lo": "", "ci_b_hi": "",
"delta": "", "delta_ci_lo": "", "delta_ci_hi": "", "frac_boot_d_gt0": "", "n": int(pop.n), "n_pos": int(pop.n_pos), "n_dev": int(pop.n_dev), "n_boot": 0})
new = not OUT.exists()
with open(OUT, "a", newline="") as f:
w = csv.DictWriter(f, fieldnames=COLS)
if new:
w.writeheader()
for r in rows:
w.writerow(r)
print(f"appended {len(rows)} rows (k0 pairing) to {OUT} ({time.time() - t0:.0f}s)")
return
if "veh_onset|veh_any_on|15" not in pops:
pop = P.P_veh(data, "veh_any_on", 15)
e_a, e_b = col(ev_a, "veh_any_on"), col(ev_b, "veh_any_on")
if e_a is not None and e_b is not None:
ti = list(map(int, pa["onset_taus"])).index(15)
pair_scores("veh_onset|veh_any_on|3s", pop, pa["onset_p"][pop.sel, ti, e_a].astype(np.float64), pb["onset_p"][pop.sel, ti, e_b].astype(np.float64))
if a.only_onset:
new = not OUT.exists()
with open(OUT, "a", newline="") as f:
w = csv.DictWriter(f, fieldnames=COLS)
if new:
w.writeheader()
for r in rows:
w.writerow(r)
print(f"appended {len(rows)} rows (onset cells only) to {OUT} ({time.time() - t0:.0f}s)")
write_run_json(CWM3 / "results", "paired_compare_joint", [Path(__file__), CWM3 / "eval" / "paired_compare.py", CWM2 / "eval" / "populations.py"],
[Path(a.a) / f"pred_layout_{a.regime}_{a.split}.npz", Path(a.b) / f"pred_layout_{a.regime}_{a.split}.npz"], seed=0, t_start=t0,
input_listing=False, extra={"run_stamp": stamp, "tag": a.tag, "regime": a.regime, "a": a.a, "b": a.b, "n_rows": len(rows), "dms_fields_used": [],
"kin_targets": a.kin_targets, "kin_version": a.kin_version, "mask_routes": a.mask_routes, "only_onset": True, "layout_guard": guards})
return
# ---- cross-leg endpoints
epn_a, epn_b = [str(x) for x in pa.get("ep_names", [])], [str(x) for x in pb.get("ep_names", [])]
def ep(cell, pop, name):
ja, jb = col(epn_a, name), col(epn_b, name)
if ja is None or jb is None or "ep_p" not in pa or "ep_p" not in pb:
return
sa = pa["ep_p"][pop.sel, ja].astype(np.float64)
sb = pb["ep_p"][pop.sel, jb].astype(np.float64)
if "ep_mask" in pa:
sa = np.where(pa["ep_mask"][pop.sel, ja], sa, np.nan)
if "ep_mask" in pb:
sb = np.where(pb["ep_mask"][pop.sel, jb], sb, np.nan)
pair_scores(cell, pop, sa, sb, ("pr_auc", "auroc"))
ep("C1", pops["C1"], "C1")
ep("C1_given_k", pops["C1"], "C1_given_k")
for ch in ("gasPressed", "brakePressed", "steer_phase"):
ep(f"C1_{ch}", pops[f"C1_{ch}"], "C1")
for f in FAMILIES:
ep(f"C2|{f}", pops[f"C2|{f}"], f"C2_{f}")
ep(f"C2_given_k|{f}", pops[f"C2|{f}"], f"C2_given_k_{f}")
ep(f"C2_hand|{f}", pops[f"C2_hand|{f}"], f"C2_hand_{f}")
ep(f"C2_head|{f}", pops[f"C2_head|{f}"], f"C2_head_{f}")
pooled = pops["C2|pooled_warnable"]
parts_ = [pops[f"C2|{f}"] for f in ("ttc", "ttc_drop", "lane", "badas")]
if all(col(epn_a, f"C2_{f}") is not None and col(epn_b, f"C2_{f}") is not None for f in ("ttc", "ttc_drop", "lane", "badas")):
sa = np.concatenate([pa["ep_p"][p_.sel, col(epn_a, f"C2_{f}")].astype(np.float64) for p_, f in zip(parts_, ("ttc", "ttc_drop", "lane", "badas"))])
sb = np.concatenate([pb["ep_p"][p_.sel, col(epn_b, f"C2_{f}")].astype(np.float64) for p_, f in zip(parts_, ("ttc", "ttc_drop", "lane", "badas"))])
pair_scores("C2|pooled_warnable", pooled, sa, sb, ("pr_auc", "auroc"))
# ---- hazard NLL (first-passage contract: inactive steps up to and including the first onset) per driver / vehicle / demand event
dong = tg["dongle"]
M = tg["M"].astype(bool)
def nll_cell(ev, zp, z, em, e_a, e_b):
m_tr = em & (np.nan_to_num(zp, nan=1.0) < 0.5)
onset = (np.nan_to_num(z, nan=0.0) > 0.5) & m_tr
first = np.where(onset.any(1), onset.argmax(1), 10 ** 6)
m_tr = m_tr & (np.arange(m_tr.shape[1])[None, :] <= first[:, None])
onset = onset & m_tr
rows_tr = np.flatnonzero(m_tr.any(1))
out = []
for pr, e in ((pa, e_a), (pb, e_b)):
h = np.clip(pr["h_on"][rows_tr, :, e].astype(np.float64), 1e-6, 1 - 1e-6)
nll = -(np.where(onset[rows_tr], np.log(h), 0) + np.where(m_tr[rows_tr] & ~onset[rows_tr], np.log(1 - h), 0))
out.append(np.where(np.isfinite(h).all(1), nll.sum(1) / m_tr[rows_tr].sum(1), np.nan))
va, vb = out
keep = np.isfinite(va) & np.isfinite(vb)
if keep.sum() < a.min_rows:
return
res = boot_pair(None, va[keep], vb[keep], dong[rows_tr][keep], a.n_boot, 0, "mean")
add(f"hazard_nll|{ev}", "nll", res, int(keep.sum()), int(onset[rows_tr][keep].any(1).sum()))
for j, ev in enumerate(P.DRV_EVENTS):
e_a, e_b = col(ev_a, ev), col(ev_b, ev)
if e_a is None or e_b is None:
continue
z0, z, em = P.driver_chains(kin)[j]
nll_cell(ev, np.concatenate([z0[:, None], z[:, :-1]], 1), z, em, e_a, e_b)
chains = P.vehicle_chains(tg)
for ev, ch in P.VEH_CH.items():
e_a, e_b = col(ev_a, ev), col(ev_b, ev)
if e_a is None or e_b is None:
continue
zp, z = chains[ch]
nll_cell(ev, zp.astype(np.float32), z.astype(np.float32), M, e_a, e_b)
fam_names = list(map(str, dm["families"]))
for f in FAMILIES:
e_a, e_b = col(ev_a, f"D_{f}"), col(ev_b, f"D_{f}")
if e_a is None or e_b is None:
continue
fi = fam_names.index(f)
z = dm["D"][:, :, fi].astype(np.float32)
zp = np.concatenate([dm["D0"][:, fi].astype(np.float32)[:, None], z[:, :-1]], 1)
valid = dm["Dvalid"][:, :, fi] & dm["Moving"] & M & np.concatenate([dm["D0valid"][:, fi][:, None], dm["Dvalid"][:, :-1, fi]], 1)
nll_cell(f"D_{f}", zp, z, valid, e_a, e_b)
# ---- per-row means (as paired_compare)
for key, names in (("y_mae", ("steeringAngleDeg", "vEgo", "aEgo")), ("y_crps", ("steeringAngleDeg", "vEgo", "aEgo")), ("f_cos", tuple(f"+{i}s" for i in range(1, 6)))):
if key in pa and key in pb:
for j, nm in enumerate(names):
va, vb = pa[key][:, j].astype(np.float64), pb[key][:, j].astype(np.float64)
keep = np.isfinite(va) & np.isfinite(vb)
if keep.sum() < a.min_rows:
continue
res = boot_pair(None, va[keep], vb[keep], dong[keep], a.n_boot, 0, "mean")
add(f"{key}|{nm}", "mean", res, int(keep.sum()), "")
if "nr_p" in pa and "nr_p" in pb:
nr = dm["NoResp"].astype(np.float64)
valid = dm["NoRespValid"] & (np.arange(25)[None, :] + 15 <= 24)
va, vb = pa["nr_p"].astype(np.float64), pb["nr_p"].astype(np.float64)
ok = valid & np.isfinite(va) & np.isfinite(vb)
rows_ok = ok.any(1)
if rows_ok.sum() >= a.min_rows:
ba = np.where(ok, (va - nr) ** 2, 0).sum(1)[rows_ok] / ok.sum(1)[rows_ok]
bb = np.where(ok, (vb - nr) ** 2, 0).sum(1)[rows_ok] / ok.sum(1)[rows_ok]
res = boot_pair(None, ba, bb, dong[rows_ok], a.n_boot, 0, "mean")
add("nr_brier|NoResp", "brier", res, int(rows_ok.sum()), "")
if "cooccur_p" in pa and "cooccur_p" in pb:
EM = kin["EM"]
valid = M[:, :15].all(1) & EM[:, :15, :].all((1, 2))
drv_any = np.zeros(N, bool)
for z0, z, em in P.driver_chains(kin):
inact0, onset, first = P._first_onset(z0, z, em)
drv_any |= inact0 & (first >= 0) & (first < 15)
von, _ = P.vehicle_onsets(tg, M)
lab = (drv_any & von["any"][:, :15].any(1)).astype(np.float64)
va, vb = pa["cooccur_p"].astype(np.float64), pb["cooccur_p"].astype(np.float64)
keep = valid & np.isfinite(va) & np.isfinite(vb)
if keep.sum() >= a.min_rows:
res = boot_pair(None, (va[keep] - lab[keep]) ** 2, (vb[keep] - lab[keep]) ** 2, dong[keep], a.n_boot, 0, "mean")
add("cooccur_brier|drv_any&veh_any", "brier", res, int(keep.sum()), int(lab[keep].sum()))
new = not OUT.exists()
with open(OUT, "a", newline="") as f:
w = csv.DictWriter(f, fieldnames=COLS)
if new:
w.writeheader()
for r in rows:
w.writerow(r)
print(f"appended {len(rows)} rows to {OUT} ({time.time() - t0:.0f}s)")
write_run_json(CWM3 / "results", "paired_compare_joint", [Path(__file__), CWM3 / "eval" / "paired_compare.py", CWM2 / "eval" / "populations.py"],
[Path(a.a) / f"pred_layout_{a.regime}_{a.split}.npz", Path(a.b) / f"pred_layout_{a.regime}_{a.split}.npz"], seed=0, t_start=t0,
input_listing=False, extra={"run_stamp": stamp, "tag": a.tag, "regime": a.regime, "a": a.a, "b": a.b, "n_rows": len(rows), "dms_fields_used": [],
"kin_targets": a.kin_targets, "kin_version": a.kin_version, "mask_routes": a.mask_routes, "layout_guard": guards})
if __name__ == "__main__":
main()

Xet Storage Details

Size:
20.9 kB
·
Xet hash:
b4b44d0ae5d0ec402bc007dab282ef2c916a27c75813d2b67675ea67ebdf0977

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