From c96549576ad5f8c4ba1a3cb51136c4af12f7333f Mon Sep 17 00:00:00 2001 From: TATP-233 Date: Thu, 24 Sep 2026 15:44:43 +0800 Subject: [PATCH 1/4] Enable viser rendering for the motrix backend unisim-core 1.7.6 (unilabsim/unisim#302) implements the physics-state playback contract on the Motrix adapter for both portable entity scenes and audited whole-MJCF scenes, so the viser viewer no longer fail-closes on --sim motrix: eval routes to play_viser.py and train forwards training.play_render_mode=viser, and the browser-based viewer no longer requires mxpython on macOS. genesis/isaacgym/isaacsim keep their fail-closed rejection, now covered by dedicated tests. --- .../source/en/5-reference/5-support_matrix.md | 4 +- .../zh_CN/5-reference/5-support_matrix.md | 2 +- pyproject.rocm.toml | 4 +- pyproject.toml | 6 +- src/unilab/cli.py | 8 ++- tests/test_cli.py | 70 +++++++++++++++++-- uv.lock | 10 +-- 7 files changed, 84 insertions(+), 20 deletions(-) diff --git a/docs/sphinx/source/en/5-reference/5-support_matrix.md b/docs/sphinx/source/en/5-reference/5-support_matrix.md index a9307537e..8fe789240 100644 --- a/docs/sphinx/source/en/5-reference/5-support_matrix.md +++ b/docs/sphinx/source/en/5-reference/5-support_matrix.md @@ -24,7 +24,9 @@ in English. Do not infer support beyond the evidence grade shown below. - `mujoco`: `--render-mode auto` exports `play_video.mp4`; `--render-mode viser` serves the rollout in a browser-based viser viewer. - `motrix`: `--render-mode auto` opens an interactive renderer window; it does - not record a video and is not bound by `play_steps`. + not record a video and is not bound by `play_steps`. `--render-mode viser` + routes to the browser-based viser viewer with per-env MuJoCo playback + models driven by physics-state snapshots. - `mjwarp`: supports explicit, finite-step `record` by default, rendered offline through the task owner's MuJoCo visual model; `--render-mode interactive` routes to the MuJoCo interactive viewer (mjwarp runs the physics while diff --git a/docs/sphinx/source/zh_CN/5-reference/5-support_matrix.md b/docs/sphinx/source/zh_CN/5-reference/5-support_matrix.md index ffd1018ad..a00cc9ac8 100644 --- a/docs/sphinx/source/zh_CN/5-reference/5-support_matrix.md +++ b/docs/sphinx/source/zh_CN/5-reference/5-support_matrix.md @@ -20,7 +20,7 @@ - `mujoco`: `--render-mode auto` 会导出 `play_video.mp4`;`--render-mode viser` 通过基于浏览器的 viser viewer 展示回放 -- `motrix`: `--render-mode auto` 会打开交互式 renderer 窗口,不录制视频,不受 `play_steps` 限制 +- `motrix`: `--render-mode auto` 会打开交互式 renderer 窗口,不录制视频,不受 `play_steps` 限制;`--render-mode viser` 路由到浏览器 viser viewer(物理快照驱动按 env 的 MuJoCo playback model) - `mjwarp`: 默认仅支持显式、有限步数的 `record`,通过 task owner 的 MuJoCo visual model 离线录制;`--render-mode interactive` 路由到 MuJoCo 交互 viewer(mjwarp 跑物理、MuJoCo 渲染 env[0],强制单 env);`--render-mode viser` 路由到浏览器 viser viewer(按 env 使用 MuJoCo playback model);不支持 `auto` 或 native renderer - `isaacsim`: `auto` 在有 display 时选择 Kit viewer,否则选择 headless RGB camera;当前真实主机仍有 RTX renderer 初始化 blocker,支持等级保持 `Configured` - `--render-mode record`: MuJoCo、mjwarp、Motrix 和 IsaacSim 都只录制视频 diff --git a/pyproject.rocm.toml b/pyproject.rocm.toml index 0128cabcb..f6478474b 100644 --- a/pyproject.rocm.toml +++ b/pyproject.rocm.toml @@ -23,7 +23,7 @@ classifiers = [ requires-python = ">=3.10,<3.14" dependencies = [ "numpy", - "unisim-core>=1.7.3", + "unisim-core>=1.7.6", "torch==2.11.0", "triton-rocm==3.6.0 ; sys_platform == 'linux' and platform_machine == 'x86_64'", "gymnasium", @@ -69,7 +69,7 @@ mujoco = [ "mujoco~=3.11.0", "mjbatch-uni~=0.2.1", ] -motrix = ["unisim-core[motrix]>=1.7.3"] +motrix = ["unisim-core[motrix]>=1.7.6"] uni_rl = ["unilab-rl==1.3.2"] [dependency-groups] diff --git a/pyproject.toml b/pyproject.toml index f05644878..9e488060d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ classifiers = [ requires-python = ">=3.10,<3.14" dependencies = [ "numpy", - "unisim-core>=1.7.5", + "unisim-core>=1.7.6", "numba>=0.67", "prettytable>=3.10", # A range (not an exact pin) lets ROCm users substitute a ROCm torch @@ -108,13 +108,13 @@ newton = [ "pyglet>=2.1.6,<3", "imgui-bundle>=1.92.0", ] -motrix = ["unisim-core[motrix]>=1.7.5"] +motrix = ["unisim-core[motrix]>=1.7.6"] genesis = ["genesis-world==1.3.3"] uni_rl = ["unilab-rl==1.3.2"] # Wheels are CPython 3.12/3.13 Linux x86_64 only; the marker keeps uv lock # resolvable for the other required-environments. superdex = [ - "unisim-core[superdex]>=1.7.5 ; python_version >= '3.12' and sys_platform == 'linux' and platform_machine == 'x86_64'", + "unisim-core[superdex]>=1.7.6 ; python_version >= '3.12' and sys_platform == 'linux' and platform_machine == 'x86_64'", ] [dependency-groups] diff --git a/src/unilab/cli.py b/src/unilab/cli.py index a7d156e53..abf9b1ec3 100644 --- a/src/unilab/cli.py +++ b/src/unilab/cli.py @@ -40,7 +40,7 @@ # playback contract the viser viewer renders from (they expose native # renderers instead). Supporting viser there requires upstream unisim # capability work, so these sims fail closed with an actionable message. -VISER_UNSUPPORTED_SIMS = frozenset({"motrix", "genesis", "isaacgym", "isaacsim"}) +VISER_UNSUPPORTED_SIMS = frozenset({"genesis", "isaacgym", "isaacsim"}) RESERVED_OVERRIDE_KEYS = { "algo", "task", @@ -229,7 +229,11 @@ def _needs_motrix_renderer(mode: str, sim: str, overrides: Sequence[str]) -> boo if sim != "motrix": return False play_render_mode = _override_value(overrides, "training.play_render_mode") - if play_render_mode is not None and play_render_mode.strip().lower() in {"none", "record"}: + if play_render_mode is not None and play_render_mode.strip().lower() in { + "none", + "record", + "viser", + }: return False if mode == "eval": return True diff --git a/tests/test_cli.py b/tests/test_cli.py index b87389149..473b7c5aa 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -785,18 +785,74 @@ def test_macos_mujoco_viser_eval_uses_current_python( assert command[0] == sys.executable -def test_eval_viser_render_mode_rejected_for_motrix( +def test_eval_viser_render_mode_routes_to_play_viser_for_motrix( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: _make_minimal_checkout(tmp_path) + (tmp_path / "scripts" / "play_viser.py").write_text("", encoding="utf-8") _pretend_motrix_is_installed(monkeypatch) + # The viser viewer renders MuJoCo playback models, so the CLI also + # requires the mujoco package regardless of the physics backend. + monkeypatch.setattr( + cli, + "find_spec", + lambda name: ModuleSpec(name, loader=None) if name in {"motrixsim", "mujoco"} else None, + ) + + command = cli.build_command( + mode="eval", + algo="ppo", + task="go2_joystick_flat", + sim="motrix", + overrides=[], + load_run="-1", + render_mode="viser", + root=tmp_path, + ) + + assert command[1] == str(tmp_path / "scripts" / "play_viser.py") + assert "--sim" in command and "motrix" in command + + +def test_train_viser_render_mode_routes_to_train_script_for_motrix( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _make_minimal_checkout(tmp_path) + _pretend_motrix_is_installed(monkeypatch) + monkeypatch.setattr( + cli, + "find_spec", + lambda name: ModuleSpec(name, loader=None) if name in {"motrixsim", "mujoco"} else None, + ) + + command = cli.build_command( + mode="train", + algo="ppo", + task="go2_joystick_flat", + sim="motrix", + overrides=[], + render_mode="viser", + root=tmp_path, + ) + + assert command[1] == str(tmp_path / "scripts" / "train_rsl_rl.py") + assert "training.play_render_mode=viser" in command + + +def test_eval_viser_render_mode_rejected_for_genesis( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + owner_dir = tmp_path / "conf" / "ppo" / "task" / "go2_joystick_flat" + owner_dir.mkdir(parents=True) + (owner_dir / "genesis.yaml").write_text("training:\n sim_backend: genesis\n", encoding="utf-8") + _pretend_genesis_runtime(monkeypatch, available=True) with pytest.raises(SystemExit, match="viser"): cli.build_command( mode="eval", algo="ppo", task="go2_joystick_flat", - sim="motrix", + sim="genesis", overrides=[], load_run="-1", render_mode="viser", @@ -804,18 +860,20 @@ def test_eval_viser_render_mode_rejected_for_motrix( ) -def test_train_viser_render_mode_rejected_for_motrix( +def test_train_viser_render_mode_rejected_for_genesis( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - _make_minimal_checkout(tmp_path) - _pretend_motrix_is_installed(monkeypatch) + owner_dir = tmp_path / "conf" / "ppo" / "task" / "go2_joystick_flat" + owner_dir.mkdir(parents=True) + (owner_dir / "genesis.yaml").write_text("training:\n sim_backend: genesis\n", encoding="utf-8") + _pretend_genesis_runtime(monkeypatch, available=True) with pytest.raises(SystemExit, match="viser"): cli.build_command( mode="train", algo="ppo", task="go2_joystick_flat", - sim="motrix", + sim="genesis", overrides=[], render_mode="viser", root=tmp_path, diff --git a/uv.lock b/uv.lock index abee3ce21..384cbb37d 100644 --- a/uv.lock +++ b/uv.lock @@ -5238,9 +5238,9 @@ requires-dist = [ { name = "trimesh", specifier = ">=3.21.7" }, { name = "typing-extensions" }, { name = "unilab-rl", marker = "extra == 'uni-rl'", specifier = "==1.3.2" }, - { name = "unisim-core", specifier = ">=1.7.5" }, - { name = "unisim-core", extras = ["motrix"], marker = "extra == 'motrix'", specifier = ">=1.7.5" }, - { name = "unisim-core", extras = ["superdex"], marker = "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'superdex'", specifier = ">=1.7.5" }, + { name = "unisim-core", specifier = ">=1.7.6" }, + { name = "unisim-core", extras = ["motrix"], marker = "extra == 'motrix'", specifier = ">=1.7.6" }, + { name = "unisim-core", extras = ["superdex"], marker = "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'superdex'", specifier = ">=1.7.6" }, { name = "viser", specifier = ">=1.0.26" }, { name = "wandb" }, { name = "warp-lang", marker = "extra == 'mjwarp'", specifier = "==1.16.0" }, @@ -5284,13 +5284,13 @@ wheels = [ [[package]] name = "unisim-core" -version = "1.7.5" +version = "1.7.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/2c/8255a60c0260a4dc3b41aa60d9910b417a9ae11a1ef3de2b682252680390/unisim_core-1.7.5.tar.gz", hash = "sha256:7613702bf757a6253691bbcd5ab7ae38bf2ce2b8466cf5e6b0947c59466fbc11", size = 526190, upload-time = "2026-09-23T12:31:03.655Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/d4/2be8690d01447b83e05420d786713832d634af5c91c123d82221f590ce7c/unisim_core-1.7.6.tar.gz", hash = "sha256:8f2a906607dda8e743434f6b4a2e57f46ca84379d583a40ff2e3e667d8f29e2e", size = 530146, upload-time = "2026-09-24T07:04:46.064Z" } [package.optional-dependencies] motrix = [ From 5e31688e18e7062408b373906dc939fea118cfa0 Mon Sep 17 00:00:00 2001 From: TATP-233 Date: Thu, 24 Sep 2026 16:17:21 +0800 Subject: [PATCH 2/4] Adapt motrix test fixtures to unisim-core 1.7.6 construction validation The whole-MJCF joint-order validation added in unilabsim/unisim#302 runs at MotrixBackend construction: fake-model tests stub it out (its own coverage lives upstream), the pre-step-control fake gains the new _time_view/_sim_dt playback-clock fields, and the stewart scene names its twelve passive leg hinges (physics unchanged) because the validation requires named MJCF joints. --- src/unilab/assets/robots/stewart/scene.xml | 24 ++++++++++----------- tests/base/test_backend_pre_step_control.py | 2 ++ tests/base/test_motrix_backend_options.py | 10 +++++++++ 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/unilab/assets/robots/stewart/scene.xml b/src/unilab/assets/robots/stewart/scene.xml index a3a8b7f41..9e5b1e699 100644 --- a/src/unilab/assets/robots/stewart/scene.xml +++ b/src/unilab/assets/robots/stewart/scene.xml @@ -62,8 +62,8 @@ - - + + @@ -82,8 +82,8 @@ - - + + @@ -102,8 +102,8 @@ - - + + @@ -122,8 +122,8 @@ - - + + @@ -142,8 +142,8 @@ - - + + @@ -162,8 +162,8 @@ - - + + diff --git a/tests/base/test_backend_pre_step_control.py b/tests/base/test_backend_pre_step_control.py index c53ea20bb..e7637ca70 100644 --- a/tests/base/test_backend_pre_step_control.py +++ b/tests/base/test_backend_pre_step_control.py @@ -299,6 +299,8 @@ def _fake_motrix_backend(pre_step_control_fn=None): actuator_ctrls=np.zeros((1, 2), dtype=np.float32), sensor_value=0.0, ) + backend._sim_dt = 0.002 + backend._time_view = np.zeros((1,), dtype=np.float32) backend._refresh_link_pose_cache = lambda: None return backend diff --git a/tests/base/test_motrix_backend_options.py b/tests/base/test_motrix_backend_options.py index 61111aafa..934e7db06 100644 --- a/tests/base/test_motrix_backend_options.py +++ b/tests/base/test_motrix_backend_options.py @@ -247,6 +247,16 @@ def _install_fake_motrix(monkeypatch, tmp_path): "_materialize_motrix_scene_with_sensor_names", lambda **kwargs: (fake_model, ()), ) + # These tests exercise override caches and terrain plumbing with a fake + # native model; unisim-core 1.7.6 validates the whole-MJCF joint order + # against the real source file at construction (covered upstream), which + # has no meaning for the fake model and only reads "source.xml" from disk. + monkeypatch.setattr( + mod.MotrixBackend, + "_validate_non_portable_joint_order", + lambda self, model_file: None, + raising=False, + ) return mod, fake_model From 1b6fd66e4b0d0a32fcc7730e4f8967116b326357 Mon Sep 17 00:00:00 2001 From: TATP-233 Date: Thu, 24 Sep 2026 18:05:05 +0800 Subject: [PATCH 3/4] Require unisim-core 1.7.8 for the motrix viser playback fix unisim-core 1.7.8 merges every section (including the ones inlined by ) in extract_mjcf_joint_layout, matching MuJoCo. Without it, scenes composed from includes that each carry a worldbody (for example the Allegro in-hand scene) fail motrix playback validation at construction with a generalized-state dimension mismatch. --- pyproject.rocm.toml | 4 ++-- pyproject.toml | 6 +++--- uv.lock | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pyproject.rocm.toml b/pyproject.rocm.toml index f6478474b..9eaffbf0f 100644 --- a/pyproject.rocm.toml +++ b/pyproject.rocm.toml @@ -23,7 +23,7 @@ classifiers = [ requires-python = ">=3.10,<3.14" dependencies = [ "numpy", - "unisim-core>=1.7.6", + "unisim-core>=1.7.8", "torch==2.11.0", "triton-rocm==3.6.0 ; sys_platform == 'linux' and platform_machine == 'x86_64'", "gymnasium", @@ -69,7 +69,7 @@ mujoco = [ "mujoco~=3.11.0", "mjbatch-uni~=0.2.1", ] -motrix = ["unisim-core[motrix]>=1.7.6"] +motrix = ["unisim-core[motrix]>=1.7.8"] uni_rl = ["unilab-rl==1.3.2"] [dependency-groups] diff --git a/pyproject.toml b/pyproject.toml index 9e488060d..45e59e5ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ classifiers = [ requires-python = ">=3.10,<3.14" dependencies = [ "numpy", - "unisim-core>=1.7.6", + "unisim-core>=1.7.8", "numba>=0.67", "prettytable>=3.10", # A range (not an exact pin) lets ROCm users substitute a ROCm torch @@ -108,13 +108,13 @@ newton = [ "pyglet>=2.1.6,<3", "imgui-bundle>=1.92.0", ] -motrix = ["unisim-core[motrix]>=1.7.6"] +motrix = ["unisim-core[motrix]>=1.7.8"] genesis = ["genesis-world==1.3.3"] uni_rl = ["unilab-rl==1.3.2"] # Wheels are CPython 3.12/3.13 Linux x86_64 only; the marker keeps uv lock # resolvable for the other required-environments. superdex = [ - "unisim-core[superdex]>=1.7.6 ; python_version >= '3.12' and sys_platform == 'linux' and platform_machine == 'x86_64'", + "unisim-core[superdex]>=1.7.8 ; python_version >= '3.12' and sys_platform == 'linux' and platform_machine == 'x86_64'", ] [dependency-groups] diff --git a/uv.lock b/uv.lock index 384cbb37d..276a7ed37 100644 --- a/uv.lock +++ b/uv.lock @@ -5238,9 +5238,9 @@ requires-dist = [ { name = "trimesh", specifier = ">=3.21.7" }, { name = "typing-extensions" }, { name = "unilab-rl", marker = "extra == 'uni-rl'", specifier = "==1.3.2" }, - { name = "unisim-core", specifier = ">=1.7.6" }, - { name = "unisim-core", extras = ["motrix"], marker = "extra == 'motrix'", specifier = ">=1.7.6" }, - { name = "unisim-core", extras = ["superdex"], marker = "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'superdex'", specifier = ">=1.7.6" }, + { name = "unisim-core", specifier = ">=1.7.8" }, + { name = "unisim-core", extras = ["motrix"], marker = "extra == 'motrix'", specifier = ">=1.7.8" }, + { name = "unisim-core", extras = ["superdex"], marker = "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'superdex'", specifier = ">=1.7.8" }, { name = "viser", specifier = ">=1.0.26" }, { name = "wandb" }, { name = "warp-lang", marker = "extra == 'mjwarp'", specifier = "==1.16.0" }, @@ -5284,13 +5284,13 @@ wheels = [ [[package]] name = "unisim-core" -version = "1.7.6" +version = "1.7.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/d4/2be8690d01447b83e05420d786713832d634af5c91c123d82221f590ce7c/unisim_core-1.7.6.tar.gz", hash = "sha256:8f2a906607dda8e743434f6b4a2e57f46ca84379d583a40ff2e3e667d8f29e2e", size = 530146, upload-time = "2026-09-24T07:04:46.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/7f/9b13d12cf57250b704b9e43f0caba5646db0479124aa880393cff1c54273/unisim_core-1.7.8.tar.gz", hash = "sha256:d550e7052c85762778aa7e365fac748099b8b749a12fc6216947fd4ba98a3add", size = 532853, upload-time = "2026-09-24T10:02:44.662Z" } [package.optional-dependencies] motrix = [ From b6af915f67c2d972f9bb4b14aa12fc78c211340f Mon Sep 17 00:00:00 2001 From: TATP-233 Date: Thu, 24 Sep 2026 18:36:08 +0800 Subject: [PATCH 4/4] ci: keep git-tracked robot assets at the checked-out commit The test job restores a long-lived actions/cache snapshot of src/unilab/assets/robots to reuse HF-downloaded meshes, but the snapshot also contains git-tracked scene XMLs and silently rolls them back to the cached revision. The stewart scene rename in this branch (named passive hinges, required by the unisim-core 1.7.6+ motrix joint-order validation) never reached the parser: CI kept reading the cached old XML and failed with 'a hinge joint on body leg00 is unnamed' while the checked-out commit carried the fix. Restore tracked files from the index after the cache extraction; untracked mesh downloads are unaffected. --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c654214e7..625f0e1f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -215,6 +215,12 @@ jobs: with: path: src/unilab/assets/robots key: ${{ runner.os }}-robot-assets-v1-${{ hashFiles('src/unilab/assets/hub.py') }} + - name: Keep git-tracked assets at the checked-out commit + # The robots cache persists HF-downloaded meshes across runs, but it + # also snapshots git-tracked scene XMLs and would silently roll them + # back to the cached revision. Restore tracked files from the index; + # untracked downloads stay untouched. + run: git restore --worktree -- src/unilab/assets/robots - name: Pull robot assets run: uv run --no-sync unilab-pull-assets --robot all - name: Test with coverage