File size: 12,780 Bytes
13b9d08
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
"""Cairn's compute layer for the browser build (Pyodide, no server, no gradio).

Why this exists instead of Gradio-lite: gradio-lite imports ``gradio`` *before*
it installs the page's requirements, and gradio 5.x currently cannot be resolved
against ``huggingface-hub`` 1.x inside Pyodide -- the capped builds fail to
install, the uncapped ones import and then die on a missing ``httpcore``. Since
the boot order is not ours to change there, we skip the framework and drive
Pyodide directly. The page ends up lighter too: no pandas, pydantic or orjson,
just numpy, scipy, pillow and the ``cairn`` wheel.

Every function here returns plain JSON-able data (HTML fragments and base64 PNG
data URIs) which ``index.html`` drops into the DOM. All of the science is
imported unchanged from the ``cairn`` package; nothing is reimplemented for the
browser.

Claim: R/E -- lets anyone check the two headline claims themselves, for free,
with no GPU, no install and no account.
"""

from __future__ import annotations

import base64
import io
from typing import Any, Dict, List, Tuple

import numpy as np
from PIL import Image

from cairn.runner import CONDITION_NAMES, RunConfig, run_condition
from cairn.world import (
    make_departure_return_trajectory,
    make_scene,
    render,
    schedule_edit,
    usable_targets,
)

GAP = 6
BASELINES = {"A": "Vanilla", "B": "Context-window", "C": "Compressed-memory"}


# --------------------------------------------------------------------------
# rendering helpers
# --------------------------------------------------------------------------


def _png(arr: np.ndarray) -> str:
    """``(H, W, 3)`` float image -> base64 PNG data URI for an ``<img>`` tag."""
    a = (np.clip(np.asarray(arr), 0.0, 1.0) * 255).astype(np.uint8)
    buf = io.BytesIO()
    Image.fromarray(a).save(buf, format="PNG", optimize=True)
    return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode("ascii")


def _filmstrip(frames: np.ndarray, idx: List[int]) -> str:
    """Selected frames side by side, separated by pale gutters.

    Pyodide has no ffmpeg, so there is no video. A strip is arguably the better
    medium for this claim anyway: "before leaving" and "after returning" sit in
    one glance instead of several seconds apart in a loop.

    Claim: R -- the experiment, in one picture.
    """
    idx = [i for i in idx if 0 <= i < len(frames)]
    if not idx:
        return _png(np.zeros((8, 8, 3)))
    h = frames[0].shape[0]
    gutter = np.full((h, GAP, 3), 0.93, dtype=np.float32)
    panels: List[np.ndarray] = []
    for k, i in enumerate(idx):
        if k:
            panels.append(gutter)
        panels.append(frames[i])
    return _png(np.concatenate(panels, axis=1))


def _windows(traj) -> List[int]:
    return [
        0,
        max(0, traj.observe_frames[1] - 1),
        (traj.departure_frame + traj.return_frame) // 2,
        traj.return_frame + 1,
        min(len(traj) - 1, traj.return_frame + 7),
    ]


PANEL_CAPTION = (
    "opening shot &middot; last frame before leaving &middot; "
    "<b>looking away</b> &middot; just back &middot; settled after return"
)


def _ledger_html(res) -> str:
    rows = []
    for e in res.ledger.entries(include_absent=True):
        p = e.pose.position
        state = "present" if e.present else '<b style="color:#b3261e">REMOVED</b>'
        rows.append(
            f"<tr><td>{e.object_id}</td><td>{p[0]:.2f}</td><td>{p[2]:.2f}</td>"
            f"<td>{e.pose.yaw:+.2f}</td>"
            f"<td><span class='sw' style='background:rgb("
            f"{int(e.appearance[0]*255)},{int(e.appearance[1]*255)},{int(e.appearance[2]*255)})'></span>"
            f"{e.appearance[0]:.2f}, {e.appearance[1]:.2f}, {e.appearance[2]:.2f}</td>"
            f"<td>{e.n_observations}</td><td>{e.confidence:.2f}</td><td>{state}</td></tr>"
        )
    return (
        "<table><thead><tr><th>id</th><th>x</th><th>z</th><th>yaw</th><th>rgb</th>"
        "<th>seen</th><th>conf</th><th>state</th></tr></thead><tbody>"
        + "".join(rows)
        + "</tbody></table>"
    )


def _cfg(cond: str, seed: int) -> RunConfig:
    # reference_video=False skips rendering a second full clip that only the
    # FVD-proxy consumes, and this page never shows that number. Roughly halves
    # the work per click in the browser.
    return RunConfig(condition=cond, seed=1000 + int(seed), reference_video=False)


def _resolve_target(scene, requested: int, seed: int) -> Tuple[int, str]:
    """Snap the slider to an object that can actually host an episode.

    Claim: R -- the demo shows the same well-posed episodes the benchmark scores.
    """
    ok = usable_targets(scene, seed=int(seed))
    if not ok:
        raise ValueError("No object in this room can be left and returned to — try another seed.")
    if int(requested) in ok:
        return int(requested), ""
    chosen = min(ok, key=lambda o: abs(o - int(requested)))
    return chosen, (
        f"<p class='note'>Object #{int(requested)} is permanently hidden behind another object "
        f"in this room, so it cannot host a leave-and-return episode. Showing object "
        f"#{chosen} instead.</p>"
    )


# --------------------------------------------------------------------------
# public entry points (called from JavaScript)
# --------------------------------------------------------------------------


def scene_preview(seed: int, n_objects: int) -> str:
    """Four views of the room, so you can see what you are about to test."""
    from cairn.types import CameraPose

    scene = make_scene(int(n_objects), seed=int(seed))
    cams = [
        CameraPose(np.array([5.0, 1.55, 5.0]), a)
        for a in np.linspace(-np.pi, np.pi, 4, endpoint=False)
    ]
    frames = np.stack([render(scene.states(), c, scene.settings).rgb for c in cams])
    return _filmstrip(frames, list(range(4)))


def _verdict_html(res, label: str) -> str:
    m = res.metrics
    drawn = m["return_observed"] > 0.5
    ok = m["return_success"] > 0.5
    err = "not drawn at all" if not drawn else f"{m['return_self_trans']:.2f} m"
    badge = (
        "<span class='ok'>consistent</span>" if ok else "<span class='bad'>inconsistent</span>"
    )
    return (
        f"<div class='verdict'><h4>{label} &nbsp;{badge}</h4><ul>"
        f"<li>moved on return: <b>{err}</b></li>"
        f"<li>identity preserved: {'yes' if m['return_identity_preserved'] else '<b>no</b>'}</li>"
        f"<li>yaw error {m['return_self_yaw']:.2f} rad &middot; "
        f"appearance error {m['return_self_appearance']:.3f}</li>"
        f"<li>integrated consistency debt {m['debt_area']:.1f} (peak {m['debt_peak']:.2f})</li>"
        f"</ul></div>"
    )


def compare(seed: int, n_objects: int, absence: int, target: int, baseline: str) -> Dict[str, Any]:
    """Same scene, same trajectory, same generator seed — only the memory differs.

    Claim: R -- the interactive form of the headline experiment.
    """
    seed, n_objects, absence = int(seed), int(n_objects), int(absence)
    scene = make_scene(n_objects, seed=seed)
    target, note = _resolve_target(scene, int(np.clip(target, 0, n_objects - 1)), seed)
    traj = make_departure_return_trajectory(scene, target, absence, seed=seed)

    off = run_condition(scene, traj, _cfg(baseline, seed))
    on = run_condition(scene, traj, _cfg("D", seed))
    idx = _windows(traj)

    header = (
        f"<p>Camera left object <b>#{target}</b> at frame {traj.departure_frame} and came back "
        f"at frame {traj.return_frame} — <b>{traj.absence_frames} frames away</b>. "
        f"The return viewpoint is deliberately <i>not</i> the departure viewpoint, so neither "
        f"method can win by replaying its last frame.</p>"
        f"<p class='caption'>Panels: {PANEL_CAPTION}</p>{note}"
    )
    return {
        "header": header,
        "off_img": _filmstrip(off.frames, idx),
        "on_img": _filmstrip(on.frames, idx),
        "off_label": f"Cairn OFF — ({baseline}) {CONDITION_NAMES[baseline]}",
        "on_label": "Cairn ON — (D) explicit world ledger",
        "off_verdict": _verdict_html(off, f"Cairn OFF — ({baseline}) {BASELINES[baseline]}"),
        "on_verdict": _verdict_html(on, "Cairn ON — (D) explicit world ledger"),
        "ledger": _ledger_html(on),
    }


def edit(seed: int, n_objects: int, absence: int, target: int, kind: str) -> Dict[str, Any]:
    """Issue an edit while the object is off screen, then score what came back.

    Claim: E -- the operation conditions A–C cannot express at all.
    """
    seed, n_objects, absence = int(seed), int(n_objects), int(absence)
    scene = make_scene(n_objects, seed=seed)
    target, note = _resolve_target(scene, int(np.clip(target, 0, n_objects - 1)), seed)
    traj = make_departure_return_trajectory(scene, target, absence, seed=seed)
    ev = schedule_edit(traj, scene, kind, seed=seed)
    traj.edits = [ev]

    on = run_condition(scene, traj, _cfg("D", seed))
    off = run_condition(scene, traj, _cfg("A", seed))
    idx = _windows(traj)

    if kind == "move":
        d = float(np.linalg.norm(np.asarray(ev.payload["position"]) - scene.get(target).pose.position))
        what = f"move object #{target} {d:.1f} m and turn it"
    elif kind == "remove":
        what = f"delete object #{target} from the world"
    else:
        v = np.asarray(ev.payload["value"], dtype=float)
        what = (
            f"recolour object #{target} to "
            f"<span class='sw' style='background:rgb({int(v[0]*255)},{int(v[1]*255)},{int(v[2]*255)})'></span>"
            f"RGB {np.round(v, 2).tolist()}"
        )

    rows = []
    for res, cond in ((off, "A"), (on, "D")):
        for s in res.edit_scores:
            expressible = (
                "yes" if cond == "D" else "<b>no</b> — no addressable world state"
            )
            if s.complied:
                v = "<span class='ok'>yes</span>"
            elif s.ledger_correct:
                v = "<span class='warn'>written, not confirmable</span>"
            else:
                v = "<span class='bad'>no</span>"
            rows.append(
                f"<tr><td>({cond}) {CONDITION_NAMES[cond]}</td><td>{expressible}</td>"
                f"<td>{v}</td><td><code>{s.detail}</code></td></tr>"
            )

    header = (
        f"<p><b>Command:</b> {what}</p>"
        f"<p>Issued at frame <b>{ev.frame}</b>, while the object is off screen "
        f"(frames {traj.departure_frame}{traj.return_frame}).</p>"
        f"<p class='caption'>Panels: {PANEL_CAPTION}</p>{note}"
    )
    table = (
        "<table><thead><tr><th>condition</th><th>can express it?</th><th>obeyed?</th>"
        "<th>evidence</th></tr></thead><tbody>" + "".join(rows) + "</tbody></table>"
        f"<p class='note'>Conditions A–C hold the world implicitly, in activations. There is no "
        f'row named "object #{target}" to write to, so <code>move</code>/<code>remove</code>/'
        f"<code>set_attr</code> are not merely hard for them — they are undefined.</p>"
    )
    return {
        "header": header,
        "off_img": _filmstrip(off.frames, idx),
        "on_img": _filmstrip(on.frames, idx),
        "table": table,
        "ledger": _ledger_html(on),
    }


def ledger_view(seed: int, n_objects: int, absence: int, target: int, rewind_to: int) -> Dict[str, Any]:
    """Show the ledger, its transaction log, and the effect of a rewind.

    Claim: E -- the world is a table with an audit trail; rewinding is one call.
    """
    seed, n_objects, absence = int(seed), int(n_objects), int(absence)
    scene = make_scene(n_objects, seed=seed)
    tgt, _ = _resolve_target(scene, int(np.clip(target, 0, n_objects - 1)), seed)
    traj = make_departure_return_trajectory(scene, tgt, absence, seed=seed)
    res = run_condition(scene, traj, _cfg("D", seed))
    led = res.ledger

    before = _ledger_html(res)
    log_rows = "".join(
        f"<tr><td>{t.index}</td><td>{t.t}</td><td><code>{t.op}</code></td>"
        f"<td>{t.object_id}</td><td>{t.source}</td><td>{t.note or ''}</td></tr>"
        for t in led.log[-40:]
    )
    log = (
        "<table><thead><tr><th>#</th><th>frame</th><th>op</th><th>object</th>"
        "<th>source</th><th>note</th></tr></thead><tbody>" + log_rows + "</tbody></table>"
    )
    v0 = led.version
    undone = led.rollback_to_time(int(rewind_to))
    note = (
        f"<p><b>Rewound to frame {int(rewind_to)}</b>: undid {undone} of {v0} transactions. "
        f"The ledger is now exactly as it stood at that instant, object for object. No learned "
        f"memory offers this operation — its state is entangled across every object and every "
        f"timestep at once.</p>"
    )
    return {"before": before, "log": log, "after": _ledger_html(res), "note": note}