From e3d03b8c6aa4b58493c8d542c8f809f8448d1467 Mon Sep 17 00:00:00 2001 From: rsasaki0109 Date: Thu, 4 Jun 2026 22:44:08 +0900 Subject: [PATCH] Add Pyodide Phase-0 PoC: run the real pick_and_retry loop in the browser Implements Phase 0 from docs/pyodide_playground_strategy.md. - scripts/build_pyodide_bundle.py: zip the pure-Python pir package + the pick_and_retry example (~24 KB) for in-browser loading. - docs/pyodide/poc.html: loads Pyodide, loads numpy, unpacks the bundle, and runs the UNMODIFIED run(seed, render=False), printing Trace.summary() with per-stage timings. Blocks matplotlib so the headless path can never silently pull it. Uses zip + unpackArchive (not micropip) to avoid resolving the declared matplotlib dependency and keep the download tiny. - docs/pyodide/pir_bundle.zip: the committed bundle (also rebuilt on Pages deploy). - tests/test_pyodide_bundle.py: pins the zip contents byte-for-byte to the source tree (drift guard) and asserts the bundled loop runs headless with matplotlib blocked. - pages.yml: rebuild the bundle from source before deploy; trigger Pages on pir/** and the builder too. - pyodide_playground_strategy.md: mark Phase 0 built and record the packaging decision + verification. Python path verified locally by simulating Pyodide's unpack-into-cwd with the exact HTML driver: {"steps":4,"success":true,"failure_counts":{"grasp_miss":2}, "total_reward":0.69}, identical to the --no-render CLI. Browser-side JS glue (loadPyodide/unpackArchive/fetch + first-load time) still needs a real browser. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/pages.yml | 10 ++ docs/pyodide/pir_bundle.zip | Bin 0 -> 24057 bytes docs/pyodide/poc.html | 150 ++++++++++++++++++++++++++++ docs/pyodide_playground_strategy.md | 24 ++++- scripts/build_pyodide_bundle.py | 50 ++++++++++ tests/test_pyodide_bundle.py | 75 ++++++++++++++ 6 files changed, 305 insertions(+), 4 deletions(-) create mode 100644 docs/pyodide/pir_bundle.zip create mode 100644 docs/pyodide/poc.html create mode 100644 scripts/build_pyodide_bundle.py create mode 100644 tests/test_pyodide_bundle.py diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index e836939..6e4d612 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -5,6 +5,8 @@ on: branches: ["main"] paths: - "docs/**" + - "pir/**" + - "scripts/build_pyodide_bundle.py" - ".github/workflows/pages.yml" workflow_dispatch: @@ -29,6 +31,14 @@ jobs: - name: Check out repository uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Rebuild Pyodide bundle from source + run: python3 scripts/build_pyodide_bundle.py + - name: Configure GitHub Pages uses: actions/configure-pages@v5 diff --git a/docs/pyodide/pir_bundle.zip b/docs/pyodide/pir_bundle.zip new file mode 100644 index 0000000000000000000000000000000000000000..72db0f82a9ba8ef124b5be352600eebb9baaeea9 GIT binary patch literal 24057 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 + + + + +Pyodide PoC — real pick_and_retry loop in the browser + + + +

Proof of concept: the real Python loop, in your browser

+

+ This page loads Pyodide (CPython + NumPy in + WebAssembly), unpacks the actual pir package, and runs the + unmodified + examples/manipulation/01_pick_and_retry.py loop headless — no + install, no server, no matplotlib. It prints the resulting + Trace.summary(). This is Phase 0 from + docs/pyodide_playground_strategy.md: confirm the real loop runs in + the browser and measure load time before wiring it into the playground UI. +

+ + + + +
Click “Run the real loop”. First run downloads Pyodide (~a few MB); later runs are cached.
+ + + + + diff --git a/docs/pyodide_playground_strategy.md b/docs/pyodide_playground_strategy.md index 6cda93e..600ac48 100644 --- a/docs/pyodide_playground_strategy.md +++ b/docs/pyodide_playground_strategy.md @@ -80,10 +80,26 @@ Two render families cover all five: **grid** (already drawn today) and ## Phased plan -**Phase 0 — proof of concept (½ day).** A standalone `docs/pyodide_poc.html` -that loads Pyodide, installs numpy, fetches `pir` + `pick_and_retry.py`, runs -`run(seed=0, render=False)`, and `console.log`s `trace.summary()`. Goal: confirm -load time and that the real loop runs unmodified. Decide packaging (1) vs (2). +**Phase 0 — proof of concept. ✅ built (Python path verified; needs a browser +check).** [`docs/pyodide/poc.html`](pyodide/poc.html) loads Pyodide, loads numpy, +unpacks [`docs/pyodide/pir_bundle.zip`](pyodide/) (built by +[`scripts/build_pyodide_bundle.py`](../scripts/build_pyodide_bundle.py) — the +pure-Python `pir` package + `01_pick_and_retry.py`, ~24 KB), runs the +**unmodified** `run(seed, render=False)`, and prints `Trace.summary()` with +timings. It blocks `matplotlib` so the headless path can never silently pull it. + +Packaging decision: went with a **zip + `unpackArchive`** (option close to (1)), +not micropip — it avoids resolving the declared matplotlib dependency and keeps +the download tiny. `tests/test_pyodide_bundle.py` pins the zip to the source so +it cannot drift, and `pages.yml` rebuilds it on deploy. + +Verified locally (CPython simulating Pyodide's unpack-into-cwd, exact driver from +the HTML): `{"steps": 4, "success": true, "failure_counts": {"grasp_miss": 2}, +"total_reward": 0.69}` — identical to `python3 .../01_pick_and_retry.py +--no-render`. **Still to confirm in a real browser:** Pyodide first-load time and +that `loadPyodide`/`unpackArchive`/`fetch` behave as expected. Open +`docs/pyodide/poc.html` via a local server (e.g. `python3 -m http.server` from +`docs/`) or on GitHub Pages and click “Run the real loop”. **Phase 1 — one real loop on the page (1–2 days).** Add a "Run real Python" toggle to the existing playground for `clarifying_question` (its renderer already diff --git a/scripts/build_pyodide_bundle.py b/scripts/build_pyodide_bundle.py new file mode 100644 index 0000000..723f72a --- /dev/null +++ b/scripts/build_pyodide_bundle.py @@ -0,0 +1,50 @@ +"""Bundle the pure-Python `pir` package + flagship examples into a zip. + +The zip is loaded directly in the browser with `pyodide.unpackArchive`, so the +Pyodide playground runs the *real* example code with no build step and without +pulling matplotlib (the headless loop path is numpy-only). Re-run this whenever +`pir/` or a bundled example changes; the output is committed so GitHub Pages can +serve it. + +Usage: + python3 scripts/build_pyodide_bundle.py +""" + +from __future__ import annotations + +import zipfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +OUT = ROOT / "docs" / "pyodide" / "pir_bundle.zip" + +# Examples exposed to the browser PoC. Add to this list as more flagship loops +# get a JS renderer (see docs/pyodide_playground_strategy.md). +BUNDLED_EXAMPLES = [ + "examples/manipulation/01_pick_and_retry.py", +] + + +def iter_pir_sources(): + for path in sorted((ROOT / "pir").rglob("*.py")): + if "__pycache__" in path.parts: + continue + yield path + + +def build() -> Path: + OUT.parent.mkdir(parents=True, exist_ok=True) + files = list(iter_pir_sources()) + [ROOT / rel for rel in BUNDLED_EXAMPLES] + + with zipfile.ZipFile(OUT, "w", compression=zipfile.ZIP_DEFLATED) as zf: + for path in files: + arcname = path.relative_to(ROOT).as_posix() + zf.write(path, arcname) + + size_kb = OUT.stat().st_size / 1024 + print(f"wrote {OUT.relative_to(ROOT)} ({len(files)} files, {size_kb:.1f} KB)") + return OUT + + +if __name__ == "__main__": + build() diff --git a/tests/test_pyodide_bundle.py b/tests/test_pyodide_bundle.py new file mode 100644 index 0000000..b7929b7 --- /dev/null +++ b/tests/test_pyodide_bundle.py @@ -0,0 +1,75 @@ +"""Guard the committed Pyodide bundle against drift from the source tree. + +The browser PoC (docs/pyodide/poc.html) runs the real `pir` code by unpacking +docs/pyodide/pir_bundle.zip. If `pir/` or a bundled example changes but the zip +is not rebuilt, the browser would silently run stale code. These tests fail in +that case and tell you to re-run scripts/build_pyodide_bundle.py. +""" + +from __future__ import annotations + +import importlib.util +import zipfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +BUNDLE = ROOT / "docs" / "pyodide" / "pir_bundle.zip" + + +def _load_builder(): + spec = importlib.util.spec_from_file_location( + "build_pyodide_bundle", ROOT / "scripts" / "build_pyodide_bundle.py" + ) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_bundle_exists() -> None: + assert BUNDLE.exists(), "run: python3 scripts/build_pyodide_bundle.py" + + +def test_bundle_contents_match_source() -> None: + builder = _load_builder() + expected = {p.relative_to(ROOT).as_posix() for p in builder.iter_pir_sources()} + expected |= set(builder.BUNDLED_EXAMPLES) + + with zipfile.ZipFile(BUNDLE) as zf: + archived = {info.filename: zf.read(info.filename) for info in zf.infolist()} + + assert set(archived) == expected, ( + "bundle file list is stale; re-run python3 scripts/build_pyodide_bundle.py" + ) + + for arcname, data in archived.items(): + on_disk = (ROOT / arcname).read_bytes() + assert data == on_disk, ( + f"{arcname} differs from source; re-run python3 scripts/build_pyodide_bundle.py" + ) + + +def test_bundled_example_runs_headless_without_matplotlib() -> None: + """The browser path imports numpy only; prove the loop never needs matplotlib.""" + import sys + + class _Block: + def find_spec(self, name, path=None, target=None): + if name == "matplotlib" or name.startswith("matplotlib."): + raise ImportError("blocked for test") + return None + + blocker = _Block() + sys.meta_path.insert(0, blocker) + try: + example = ROOT / "examples" / "manipulation" / "01_pick_and_retry.py" + spec = importlib.util.spec_from_file_location("pick_and_retry_pyodide", example) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + trace = module.run(seed=3, render=False) + summary = trace.summary() + assert summary.success + assert summary.failure_counts.get("grasp_miss", 0) >= 1 + finally: + sys.meta_path.remove(blocker)