+
{html.escape(title)}
+Closed-loop WebXR MuJoCo sim, {len(TASKS)} training scenes. Cards in each row compare checkpoints on the same seeded reset. Each video shows the ego view on the left and the chest view on the right. Scenes are sorted best to worst; ★ marks scenes with gpt_fleet_depth data.
+
run {html.escape(a.run)}seeds shown {", ".join(map(str, a.seeds))}chunk 32 steps, executed fullyflow steps 10control 20 Hz
+
Stage rates over all evaluated seeds
+| checkpoint | rollouts | reach | grasp | move | all three | object fell | IK converged |
{"".join(rows)}
+These are sim signals only. Reach means a hand came within 3 cm of an object. Grasp means 0.5 s or more of contact while the object was lifted or carried. Move means an object moved 5 cm or more, or a joint turned 20° or more. Task completion waits on the VLM judge.
+{"".join(secs)}
+
"""
+ (a.out / "index.html").write_text(page)
+ json.dump(files, open(a.out / "files.json", "w"))
+ print(f"wrote {a.out / 'index.html'}: {len(files)} videos, {sum((a.out / f).stat().st_size for f in files) / 1e6:.1f} MB")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/benchmarks/sharpa_webxr/calibrate.py b/benchmarks/sharpa_webxr/calibrate.py
new file mode 100644
index 00000000..0824c89d
--- /dev/null
+++ b/benchmarks/sharpa_webxr/calibrate.py
@@ -0,0 +1,118 @@
+"""Verifier vs human labels on data/webxr_episodes (success / recover = completed, failure = not completed).
+
+usage: calibrate.py [--root data/webxr_episodes] [--tsv out.tsv] [--show-errors N]
+Per scene: n by label, recall on completed (success_any true), specificity on failures, per-stage achieved rate
+split by label, and the worst-offending stage. Scenes whose chain ends in a judge stage are scored on sim_prefix_ok.
+"""
+from __future__ import annotations
+
+import argparse
+import json
+from collections import defaultdict
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[2]
+TASKS = json.loads((Path(__file__).resolve().parent / "tasks.json").read_text())
+
+
+def load(root: Path):
+ rows = []
+ for f in root.glob("*/*/verifier.json"):
+ v = json.loads(f.read_text())
+ if "error" in v:
+ rows.append({"scene_id": v["scene_id"], "dir": str(f.parent), "error": v["error"]})
+ continue
+ meta = json.loads((f.parent / "rollout.json").read_text())
+ v["label"] = meta["outcome"]
+ v["completed"] = meta["outcome"] != "failure"
+ v["dir"] = str(f.parent)
+ rows.append(v)
+ return rows
+
+
+def rate(xs):
+ return round(sum(xs) / len(xs), 3) if xs else None
+
+
+def main():
+ p = argparse.ArgumentParser()
+ p.add_argument("--root", type=Path, default=ROOT / "data/webxr_episodes")
+ p.add_argument("--tsv", type=Path)
+ p.add_argument("--show-errors", type=int, default=0, help="print N false negatives / positives per scene")
+ p.add_argument("--scenes", nargs="*")
+ a = p.parse_args()
+ rows = load(a.root)
+ by = defaultdict(list)
+ for r in rows:
+ if not a.scenes or any(r["scene_id"].startswith(s) for s in a.scenes):
+ by[r["scene_id"]].append(r)
+ out = []
+ print(f"{'scene':8s} {'category':24s} {'n_s/r/f':>9s} {'recall':>7s} {'spec':>6s} {'rec_s':>6s} {'rec_r':>6s} stage any/final rate: completed|failure")
+ tot = {"tp": 0, "fn": 0, "tn": 0, "fp": 0}
+ tot_sim = {"tp": 0, "fn": 0, "tn": 0, "fp": 0} # scenes whose chain is fully sim-verifiable
+ for sid in sorted(by):
+ rs = [r for r in by[sid] if "error" not in r]
+ errs = [r for r in by[sid] if "error" in r]
+ if not rs:
+ print(sid[:8], "all errors:", errs[0]["error"][:80] if errs else "")
+ continue
+ pos = [r for r in rs if r["completed"]]
+ neg = [r for r in rs if not r["completed"]]
+ key = "sim_prefix_ok"
+ tp = sum(r[key] for r in pos); fn = len(pos) - tp
+ fp = sum(r[key] for r in neg); tn = len(neg) - fp
+ for k, v in zip(("tp", "fn", "tn", "fp"), (tp, fn, tn, fp)):
+ tot[k] += v
+ if not rs[0]["needs_judge"]:
+ tot_sim[k] += v
+ stages = [s["name"] for s in rs[0]["stages"]]
+ st = []
+ for i, name in enumerate(stages):
+ if rs[0]["stages"][i]["check"] == "judge":
+ st.append(f"{name}:J")
+ continue
+ ap = rate([r["stages"][i]["achieved"] for r in pos]); an = rate([r["stages"][i]["achieved"] for r in neg])
+ fp_ = rate([bool(r["stages"][i]["final"]) for r in pos]); fn_ = rate([bool(r["stages"][i]["final"]) for r in neg])
+ st.append(f"{name}:{ap}/{fp_}|{an}/{fn_}")
+ n_s = sum(r["label"] == "success" for r in rs); n_r = sum(r["label"] == "recover" for r in rs)
+ rec = {"scene": sid[:8], "category": rs[0].get("category"), "n_success": n_s, "n_recover": n_r, "n_failure": len(neg),
+ "recall": rate([r[key] for r in pos]), "specificity": rate([not r[key] for r in neg]),
+ "recall_success": rate([r[key] for r in pos if r["label"] == "success"]),
+ "recall_recover": rate([r[key] for r in pos if r["label"] == "recover"]),
+ "fallen_rate": rate([any(r["fallen"].values()) for r in rs]), "n_errors": len(errs), "stages": " ".join(st)}
+ out.append(rec)
+ print(f"{rec['scene']:8s} {str(rec['category'])[:24]:24s} {n_s:3d}/{n_r:2d}/{len(neg):2d} {str(rec['recall']):>7s} {str(rec['specificity']):>6s} "
+ f"{str(rec['recall_success']):>6s} {str(rec['recall_recover']):>6s} {rec['stages']}" + (f" ERR={len(errs)}" if errs else ""))
+ if a.show_errors:
+ for r in [r for r in pos if not r[key]][: a.show_errors]:
+ print(" FN", r["dir"].split("/")[-1], [(s["name"], s["achieved"]) for s in r["stages"]], {s["name"]: s["metrics"] for s in r["stages"] if not s["achieved"]})
+ for r in [r for r in neg if r[key]][: a.show_errors]:
+ print(" FP", r["dir"].split("/")[-1], [(s["name"], round(s["t_first_s"], 1) if s["t_first_s"] is not None else None) for s in r["stages"]])
+ # same totals under pure any-time and pure end-state semantics (sim-only scenes)
+ def any_time(r):
+ return all(s["any_time"] for s in r["stages"]) and not any(r["fallen"].values())
+
+ def end_state(r): # earlier stages any time, the goal stage still true at the end of the episode
+ return all(s["any_time"] for s in r["stages"][:-1]) and r["stages"][-1]["end_state"] and not any(r["fallen"].values())
+
+ for fn_, label in ((any_time, "SIM-ONLY, any-time semantics"), (end_state, "SIM-ONLY, end-state semantics")):
+ rs = [r for sid in by for r in by[sid] if "error" not in r and not r["needs_judge"] and "any_time" in r["stages"][0]]
+ if rs:
+ tp = sum(fn_(r) for r in rs if r["completed"]); fn = sum(not fn_(r) for r in rs if r["completed"])
+ fp = sum(fn_(r) for r in rs if not r["completed"]); tn = sum(not fn_(r) for r in rs if not r["completed"])
+ print(f"{label}: recall={tp / max(1, tp + fn):.3f} specificity={tn / max(1, tn + fp):.3f} accuracy={(tp + tn) / len(rs):.3f}")
+ for name, t in (("ALL (judge scenes scored on sim prefix)", tot), ("SIM-ONLY scenes", tot_sim)):
+ n = sum(t.values())
+ if n:
+ print(f"\n{name}: n={n} recall={t['tp'] / max(1, t['tp'] + t['fn']):.3f} specificity={t['tn'] / max(1, t['tn'] + t['fp']):.3f} "
+ f"accuracy={(t['tp'] + t['tn']) / n:.3f} (tp={t['tp']} fn={t['fn']} tn={t['tn']} fp={t['fp']})")
+ if a.tsv:
+ cols = list(out[0].keys())
+ with open(a.tsv, "w") as f:
+ f.write("\t".join(cols) + "\n")
+ for r in out:
+ f.write("\t".join(str(r[c]) for c in cols) + "\n")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/benchmarks/sharpa_webxr/check_policy_parity.py b/benchmarks/sharpa_webxr/check_policy_parity.py
new file mode 100644
index 00000000..8a9b5595
--- /dev/null
+++ b/benchmarks/sharpa_webxr/check_policy_parity.py
@@ -0,0 +1,77 @@
+"""G3: eval policy path == training path on val windows.
+
+(1) images/state/prompt from GWPSharpaPolicy.preprocess (raw frames) are bit-identical to the training dataset's
+ __getitem__; (2) training flow-matching action loss of the loaded weights on val windows (compare to the
+ run's val/action_loss); (3) sampled chunk (eval sampler, 10 and 20 steps) vs ground truth: normalized MSE and
+ decoded absolute wrist position / rotation / hand-joint errors.
+"""
+import argparse
+import json
+import sys
+
+import numpy as np
+import torch
+
+sys.path.insert(0, "benchmarks/sharpa_webxr")
+from policy_server import GWPSharpaPolicy # noqa: E402
+
+p = argparse.ArgumentParser()
+p.add_argument("--ckpt", required=True)
+p.add_argument("--n", type=int, default=24)
+p.add_argument("--out", required=True)
+a = p.parse_args()
+
+pol = GWPSharpaPolicy(a.ckpt, num_steps=10)
+T = pol.T
+T.SPLIT_FILE = "assets/sharpa_full_meta/split_v1.json"
+dirs = ["assets/sharpa_full_meta/gpt_fleet_depth", "assets/sharpa_full_meta/simteleop_1004_fleet"]
+norm = {"observation.state": {"q01": pol.s_lo, "q99": pol.s_hi}, "action": {"q01": pol.a_lo, "q99": pol.a_hi}}
+ds = T.SharpaGigaEEFDataset("val", None, norm, pol.prompt_embeds, False, dirs)
+rng = np.random.default_rng(0)
+pool = rng.choice(ds.pool_size(), size=a.n, replace=False)
+ds.indices = pool
+
+from giga_train import ModuleDict # noqa: E402
+loss_ns = T.build_loss_ns(ModuleDict({"transformer": pol.model}), pol.vae, pol.device)
+res = {"ckpt": a.ckpt, "n": a.n, "identical_inputs": [], "flow_action_loss": [], "s10": [], "s20": []}
+A = ds.A
+for i, w in enumerate(pool):
+ item = ds[i]
+ s = ds.sample(int(w))
+ # (1) preprocess parity
+ imgs = {c.split(".")[-1]: np.asarray(s["video"][c][0]) for c in T.EEF_CAMERAS}
+ state = ds.codec.state_encode(s["state"])[0]
+ raw = pol.preprocess(imgs, state, s["prompt"])
+ same = (torch.equal(raw["images"][0, 0], item["images"][0]) and torch.allclose(raw["state"][0], item["state"])
+ and torch.equal(raw["prompt_embeds"][0], item["prompt_embeds"]))
+ res["identical_inputs"].append(bool(same))
+ # (2) training loss (train mode = training contract), fixed noise
+ pol.model.train()
+ with torch.no_grad(), torch.random.fork_rng(devices=[pol.device]):
+ torch.manual_seed(1234 + i)
+ batch = T.to_batch({k: v[None] for k, v in item.items()}, pol.device)
+ res["flow_action_loss"].append(float(loss_ns.forward_step(batch)["action_loss"]))
+ pol.model.eval()
+ # (3) sampled chunk vs GT
+ gt = (item["action"].numpy() + 1) / 2 * np.maximum(pol.a_hi - pol.a_lo, 1e-8) + pol.a_lo
+ gt_abs = ds.codec.decode(gt, **A.decode_kwargs(s, ds.codec))
+ for key, steps in (("s10", 10), ("s20", 20)):
+ pred = pol.predict(imgs, state, s["prompt"], seed=i, num_steps=steps)
+ pn = (pred - pol.a_lo) / np.maximum(pol.a_hi - pol.a_lo, 1e-8) * 2 - 1
+ pa = ds.codec.decode(pred, **A.decode_kwargs(s, ds.codec))
+ dR = np.einsum("tkji,tkjl->tkil", pa.rot, gt_abs.rot)
+ ang = np.degrees(np.arccos(np.clip((np.trace(dR, axis1=-2, axis2=-1) - 1) / 2, -1, 1)))
+ res[key].append({"mse_norm": float(((pn - item["action"].numpy()) ** 2).mean()),
+ "pos_err_mm": float(np.linalg.norm(pa.pos - gt_abs.pos, axis=-1).mean() * 1e3),
+ "pos_err_mm_last": float(np.linalg.norm(pa.pos[-1] - gt_abs.pos[-1], axis=-1).mean() * 1e3),
+ "rot_err_deg": float(ang.mean()),
+ "hand_err_deg": float(np.degrees(np.abs(pa.hand - gt_abs.hand)).mean())})
+ print(i, res["identical_inputs"][-1], round(res["flow_action_loss"][-1], 4), res["s10"][-1], flush=True)
+
+summary = {"identical_inputs": int(sum(res["identical_inputs"])), "n": a.n,
+ "flow_action_loss_mean": float(np.mean(res["flow_action_loss"]))}
+for key in ("s10", "s20"):
+ summary[key] = {m: float(np.mean([r[m] for r in res[key]])) for m in res[key][0]}
+res["summary"] = summary
+json.dump(res, open(a.out, "w"), indent=1)
+print(json.dumps(summary, indent=1))
diff --git a/benchmarks/sharpa_webxr/check_server.py b/benchmarks/sharpa_webxr/check_server.py
new file mode 100644
index 00000000..d9fd35c6
--- /dev/null
+++ b/benchmarks/sharpa_webxr/check_server.py
@@ -0,0 +1,108 @@
+"""Conformance check for a policy server (policies/common.py contract). Runs in .venv_webxr, like rollout.py.
+
+Calls "meta" and validates it, then sends real observations from one scene (reset + one follow-up request, at
+every row in meta obs_offsets) and validates each response: shapes, finiteness, rotations, frame, distance from
+the current pose, latency. A server must pass this before it is evaluated.
+
+usage: check_server.py --port 11600 [--scene