#!/usr/bin/env python3 """Build preview.html: embedded sample videos per regime + their questions + stats.""" import base64 import csv import json from collections import Counter from pathlib import Path ROOT = Path(__file__).parent / "dataset" REGIME_TITLES = { "train": "TRAIN (in-distribution, matches pendulum-cjepa-160f recipe)", "val_high_damp": "VAL OOD · high damping γ∈(0.55,0.90)", "val_low_damp": "VAL OOD · near-zero damping γ∈(0,0.08)", "val_large_angle": "VAL OOD · large release angle ±(45–80°)", "val_gravity": "VAL OOD · Moon / Mars / Jupiter gravity", "val_quad_damping": "VAL OOD · quadratic drag β·ω|ω| (new law)", "val_driven": "VAL OOD · periodically driven pendulum (new forcing)", } def b64(p): return base64.b64encode(open(p, "rb").read()).decode() def esc(s): return str(s).replace("&", "&").replace("<", "<").replace(">", ">") def main(): idx = list(csv.DictReader(open(ROOT / "index.csv"))) by_regime = {} for r in idx: by_regime.setdefault(r["regime"], []).append(r) qj = {} for split in ("train", "val"): for e in json.load(open(ROOT / "questions" / f"questions_{split}.json"))["scene_annotation"]: qj[e["video_id"]] = e["questions"] stats_type = Counter() stats_concept = Counter() for qs in qj.values(): for q in qs: stats_type[q["question_type"]] += 1 stats_concept[q.get("concept", "?")] += 1 parts = ["""

Pendulum 3K · CLEVRER-style QA dataset — preview

3,000 videos (160 frames @12 fps, 196×196, H.264) · frame-aligned CSV labels · ~24k questions across the four CLEVRER types. Validation is out-of-distribution physics on purpose (causal generalization), not just a held-out seed. All question answers are derived from the ground-truth RK4 simulation (videos show 13.33 s; a hidden 28 s continuation grounds the predictive questions).

"""] n_q = sum(len(v) for v in qj.values()) parts.append(f"

Dataset stats

" f"" f"" f"
videosquestions
train (in-distribution)2400—
val (6 OOD regimes × 100)600—
total3000{n_q}
") parts.append("" + "".join(f"" for k, v in stats_type.most_common()) + "
question typecount
{k}{v}
") parts.append("
causal-concept histogram" "" + "".join(f"" for k, v in stats_concept.most_common()) + "
conceptcount
{esc(k)}{v}
") for regime in ["train"] + [k for k in REGIME_TITLES if k != "train"]: rows = by_regime.get(regime) if not rows: continue r = rows[len(rows) // 3] vid = r["video_id"] mp4 = ROOT / r["video_path"] parts.append(f"

{esc(REGIME_TITLES[regime])}

") params = (f"θ₀={float(r['theta0_deg']):+.1f}° ω₀={float(r['omega0_rad_s']):+.3f} " f"γ={float(r['damping_s_inv']):.3f} β={float(r['quad_drag_beta']):.3f} " f"g={float(r['gravity_m_s2']):.2f} drive_A={float(r['drive_amp_rad_s2']):.2f} " f"f_d={float(r['drive_freq_hz']):.3f}Hz") parts.append(f"

{vid}.mp4 · {esc(params)}

") parts.append('
') parts.append(f'') parts.append('
") Path(__file__, ).parent.joinpath("preview.html").write_text("".join(parts)) print("wrote preview.html") if __name__ == "__main__": main()