Skip to content

Run predicators envs in the browser via Pyodide + pybullet WASM#36

Open
yiyunliu wants to merge 1 commit into
masterfrom
web-squashed
Open

Run predicators envs in the browser via Pyodide + pybullet WASM#36
yiyunliu wants to merge 1 commit into
masterfrom
web-squashed

Conversation

@yiyunliu
Copy link
Copy Markdown

@yiyunliu yiyunliu commented Jun 2, 2026

This PR pulls out the mostly ML independent components from utils.py into a separate file utils_lite.py. The goal is to allow the predicators environments to run in a web browser.

The python side change should be quite minimal other than minor import changes and the utils_lite.py split. I'm not sure if the web frontend should be part of the repo or maintained separately. I prefer the former so we can ensure future changes don't break the web code (I have included CI workflow to check for web imports).

Split utils.py into a Pyodide-safe lite half and a slim CPython
wrapper, repoint env-load-path modules at the lite variant, and add
a proof-of-concept that boots predicators inside Pyodide and resets
each of the 18 envs in the dropdown.

Architecture:
- predicators/utils_lite.py is the bulk of the old utils.py minus
  imports of torch, scipy.stats.beta, imageio, and the
  pretrained-model SDKs. predicators/utils.py keeps those 11 helpers
  (DelayDistribution subclasses, save_video/save_images, the four
  beta_bernoulli_* functions, create_llm_by_name/create_vlm_by_name)
  and re-exports the lite module via `from utils_lite import *`, so
  CPython callers see no API change.
- structs.py imports utils_lite directly (not utils) to break the
  cycle that triggers when utils_lite is loaded first under Pyodide.
  torch and pretrained_model_interface are lazy imports inside the
  two make_*_process methods that need them (via a shared
  `_torch_and_delay` helper).
- 119 env-path modules (predicators/{envs, ground_truth_models,
  pybullet_helpers, perception, execution_monitoring}) flipped from
  `from predicators import utils` to `from predicators import
  utils_lite as utils` so they don't pull torch into Pyodide.
- envs/__init__.py and ground_truth_models/__init__.py call
  import_submodules(tolerate_import_errors=True) — catches plain
  ImportError too, not just ModuleNotFoundError — so optional-dep
  failures (torch / gym_sokoban / gymnasium-robotics) skip the
  submodule with a warning instead of aborting auto-discovery.
- base_env.py defers `from predicators.pretrained_model_interface
  import OpenAILLM` to inside the one method that uses it.
- setup.py: splits heavy ML/LLM deps into an `[ml]` extra; the base
  install is the env-runtime slim set that's installable under
  Pyodide via micropip. `[develop]` includes `[ml]` plus formatters.
  Strict version pins (numpy/matplotlib/pillow/pyyaml) are qualified
  with `sys_platform != 'emscripten'` so they don't conflict with
  Pyodide's pre-loaded packages.
- predicators/third_party/{__init__.py, fast_downward_translator/__init__.py}
  — empty package markers so `find_packages` actually ships the
  third_party tree in the built wheel (the env path actually imports
  `predicators.third_party.fast_downward_translator.translate`).
- Replace `assert not params` with `assert len(params) == 0` in 22
  ground-truth option policies; numpy ndarray params trigger
  "truth value of empty array is ambiguous" otherwise.

Browser POC (web/):
- web/app/{index.html, main.js} drive a Three.js renderer (via
  urdf-loader) over Pyodide+pybullet-wasm. JSON.stringify-quoted
  option names; mesh primitives with no mesh_url are skipped (they
  were rendering as 1m box stand-ins for in-memory vertex meshes
  like domino's top triangle).
- web/app/setup.py is the Pyodide-side bridge: per-env CFG
  overrides for envs that need legacy options, multi-link visual
  transforms, body/color diffs, begin_option/step_option protocol.
  begin_option wraps the NSRT grounding in try/except so a sampler
  raising KeyError or AssertionError returns {error: ...} instead
  of crashing across the Pyodide → JS boundary; step_option also
  catches the broad Exception after OptionExecutionFailure so a
  policy raising mid-rollout doesn't leave _active_option armed.
  Also guards `get_gt_options` with try/except in 3 call sites so
  envs without registered options (the domino fan/ramp/stairs
  variants) load cleanly with an empty option list.
- web/app/bundle.sh builds the predicators wheel, the gym 0.26 shim
  wheel, and packs predicators/envs/assets/ into a tarball
  (skipping the 32 MB tar rebuild when no asset is newer than the
  existing tarball). The pybullet WASM wheel is fetched from
  BasisResearch/pybullet-pyodide.
- All 18 envs in the dropdown reach `action_dim=...` when smoked
  headlessly in Chromium via web/app/browser_smoke.mjs:
  ants, balance, barrier, blocks, boil, circuit, coffee, cover,
  domino, domino_fan, domino_fan_ramp, domino_fan_ramp_stairs,
  fan, float, grow, laser, magic_bin, switch.

CI (.github/workflows/web.yml):
- One `import-check` job. Boots Pyodide in Node, installs the three
  wheels, asserts every env in web/app/index.html's dropdown
  registers as a non-abstract BaseEnv subclass. ~30 s wall. No
  Chromium, no env construction, no asset extraction.
- import_check.mjs wraps the Python import in try/except writing the
  traceback to stderr (which Pyodide's stderr handler captures), and
  Node-level unhandledRejection/uncaughtException handlers print
  the stack before exit — so a WASM abort surfaces a real message
  instead of just `pyodide.asm.js:8`.

CI (.github/workflows/predicators.yml):
- unit-tests / static-type-checking / lint steps install `[ml]` so
  they get the heavy deps slim utils.py imports at module top.
  yapf / isort / docformatter stay slim.

mypy.ini: add `web` to the top-level exclude so mypy doesn't bail
out on "Duplicate module named setup" (./setup.py vs
./web/app/setup.py).

.predicators_pylintrc: add `web` to ignore-paths — the browser
bridge monkey-patches pybullet, accesses env internals, and uses
in-function imports; those checks don't help the POC.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@yiyunliu
Copy link
Copy Markdown
Author

yiyunliu commented Jun 2, 2026

Btw I notice that the CI python version is really really old. It's using a code formatter that's not available for Python 3.13 anymore. Might open a separate issue to bump up the CI version.

state: State, objects: Sequence[Object],
params: Array) -> Tuple[Pose, Pose, str]:
assert not params
assert len(params) == 0
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we changing this and other similar assertions?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants