File size: 5,337 Bytes
9b2853a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#!/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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")


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 = ["""<!doctype html><meta charset="utf-8">
<style>
 body{font:15px/1.5 system-ui,Arial;max-width:1120px;margin:24px auto;padding:0 16px;color:#111}
 h1{font-size:26px} h2{font-size:19px;margin:28px 0 6px;border-top:2px solid #ddd;padding-top:14px}
 video{width:260px;border:1px solid #ccc;border-radius:6px;display:block}
 .row{display:flex;gap:28px;align-items:flex-start;margin:10px 0 4px;flex-wrap:wrap}
 .q{font-size:13.5px;max-width:720px}
 .q li{margin:5px 0}
 .t{display:inline-block;background:#eef;border-radius:4px;padding:0 6px;font-size:11px;margin-right:6px}
 .c{background:#efe}
 table{border-collapse:collapse;font-size:13.5px;margin:8px 0}
 td,th{border:1px solid #ccc;padding:4px 10px} th{background:#f4f4f4}
 code{background:#f5f5f5;padding:1px 5px;border-radius:4px}
</style>
<h1>Pendulum 3K · CLEVRER-style QA dataset — preview</h1>
<p>3,000 videos (160 frames @12 fps, 196×196, H.264) · frame-aligned CSV labels ·
~24k questions across the four CLEVRER types. Validation is <b>out-of-distribution
physics</b> 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).</p>"""]

    n_q = sum(len(v) for v in qj.values())
    parts.append(f"<h2>Dataset stats</h2><table><tr><th></th><th>videos</th><th>questions</th></tr>"
                 f"<tr><td>train (in-distribution)</td><td>2400</td><td>—</td></tr>"
                 f"<tr><td>val (6 OOD regimes × 100)</td><td>600</td><td>—</td></tr>"
                 f"<tr><td><b>total</b></td><td><b>3000</b></td><td><b>{n_q}</b></td></tr></table>")
    parts.append("<table><tr><th>question type</th><th>count</th></tr>"
                 + "".join(f"<tr><td>{k}</td><td>{v}</td></tr>" for k, v in stats_type.most_common())
                 + "</table>")
    parts.append("<details><summary><b>causal-concept histogram</b></summary><table>"
                 "<tr><th>concept</th><th>count</th></tr>"
                 + "".join(f"<tr><td>{esc(k)}</td><td>{v}</td></tr>" for k, v in stats_concept.most_common())
                 + "</table></details>")

    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"<h2>{esc(REGIME_TITLES[regime])}</h2>")
        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"<p><code>{vid}.mp4</code> · {esc(params)}</p>")
        parts.append('<div class="row">')
        parts.append(f'<video controls loop muted playsinline '
                     f'src="data:video/mp4;base64,{b64(mp4)}"></video>')
        parts.append('<div class="q"><ul>')
        for q in qj[vid]:
            parts.append(f'<li><span class="t">{q["question_type"]}</span>{esc(q["question"])}<br>')
            if "choices" in q:
                for i, c in enumerate(q["choices"]):
                    cls = ' class="c"' if i == q["answer"] else ""
                    parts.append(f'<span{cls}>{"abcd"[i]}) {esc(c)}</span> · ')
            else:
                parts.append(f'<span class="c">answer: {esc(q["answer"])}</span>')
            parts.append("</li>")
        parts.append("</ul></div></div>")

    Path(__file__, ).parent.joinpath("preview.html").write_text("".join(parts))
    print("wrote preview.html")


if __name__ == "__main__":
    main()