Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion docs/sphinx/source/en/5-reference/5-support_matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/sphinx/source/zh_CN/5-reference/5-support_matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 都只录制视频
Expand Down
4 changes: 2 additions & 2 deletions pyproject.rocm.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ classifiers = [
requires-python = ">=3.10,<3.14"
dependencies = [
"numpy",
"unisim-core>=1.7.3",
"unisim-core>=1.7.8",
"torch==2.11.0",
"triton-rocm==3.6.0 ; sys_platform == 'linux' and platform_machine == 'x86_64'",
"gymnasium",
Expand Down Expand Up @@ -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.8"]
uni_rl = ["unilab-rl==1.3.2"]

[dependency-groups]
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ classifiers = [
requires-python = ">=3.10,<3.14"
dependencies = [
"numpy",
"unisim-core>=1.7.5",
"unisim-core>=1.7.8",
"numba>=0.67",
"prettytable>=3.10",
# A range (not an exact pin) lets ROCm users substitute a ROCm torch
Expand Down Expand Up @@ -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.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.5 ; 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]
Expand Down
24 changes: 12 additions & 12 deletions src/unilab/assets/robots/stewart/scene.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@
<geom type="box" pos="0.02 0.0 0.03" size="0.005 0.01 0.03"/>
<geom type="cylinder" pos="0 0 0.05" size="0.005 0.02" axisangle="0 1 0 90"/>
<body name="leg00" pos="0 0 0.05" euler="-30 20 0">
<joint type="hinge" axis="0 1 0"/>
<joint type="hinge" axis="1 0 0"/>
<joint name="leg00_hinge_y" type="hinge" axis="0 1 0"/>
<joint name="leg00_hinge_x" type="hinge" axis="1 0 0"/>
<geom type="cylinder" size="0.005 0.03" axisangle="1 0 0 90"/>
<geom type="box" pos="0 -0.03 0.03" size="0.02 0.005 0.04"/>
<geom type="box" pos="0 0.03 0.03" size="0.02 0.005 0.04"/>
Expand All @@ -82,8 +82,8 @@
<geom type="box" pos="0.02 0.0 0.03" size="0.005 0.01 0.03"/>
<geom type="cylinder" pos="0 0 0.05" size="0.005 0.02" axisangle="0 1 0 90"/>
<body name="leg10" pos="0 0 0.05" euler="-30 -20 0">
<joint type="hinge" axis="0 1 0"/>
<joint type="hinge" axis="1 0 0"/>
<joint name="leg10_hinge_y" type="hinge" axis="0 1 0"/>
<joint name="leg10_hinge_x" type="hinge" axis="1 0 0"/>
<geom type="cylinder" size="0.005 0.03" axisangle="1 0 0 90"/>
<geom type="box" pos="0 -0.03 0.03" size="0.02 0.005 0.04"/>
<geom type="box" pos="0 0.03 0.03" size="0.02 0.005 0.04"/>
Expand All @@ -102,8 +102,8 @@
<geom type="box" pos="0.02 0.0 0.03" size="0.005 0.01 0.03"/>
<geom type="cylinder" pos="0 0 0.05" size="0.005 0.02" axisangle="0 1 0 90"/>
<body name="leg01" pos="0 0 0.05" euler="-30 20 0">
<joint type="hinge" axis="0 1 0"/>
<joint type="hinge" axis="1 0 0"/>
<joint name="leg01_hinge_y" type="hinge" axis="0 1 0"/>
<joint name="leg01_hinge_x" type="hinge" axis="1 0 0"/>
<geom type="cylinder" size="0.005 0.03" axisangle="1 0 0 90"/>
<geom type="box" pos="0 -0.03 0.03" size="0.02 0.005 0.04"/>
<geom type="box" pos="0 0.03 0.03" size="0.02 0.005 0.04"/>
Expand All @@ -122,8 +122,8 @@
<geom type="box" pos="0.02 0.0 0.03" size="0.005 0.01 0.03"/>
<geom type="cylinder" pos="0 0 0.05" size="0.005 0.02" axisangle="0 1 0 90"/>
<body name="leg11" pos="0 0 0.05" euler="-30 -20 0">
<joint type="hinge" axis="0 1 0"/>
<joint type="hinge" axis="1 0 0"/>
<joint name="leg11_hinge_y" type="hinge" axis="0 1 0"/>
<joint name="leg11_hinge_x" type="hinge" axis="1 0 0"/>
<geom type="cylinder" size="0.005 0.03" axisangle="1 0 0 90"/>
<geom type="box" pos="0 -0.03 0.03" size="0.02 0.005 0.04"/>
<geom type="box" pos="0 0.03 0.03" size="0.02 0.005 0.04"/>
Expand All @@ -142,8 +142,8 @@
<geom type="box" pos="0.02 0.0 0.03" size="0.005 0.01 0.03"/>
<geom type="cylinder" pos="0 0 0.05" size="0.005 0.02" axisangle="0 1 0 90"/>
<body name="leg02" pos="0 0 0.05" euler="-30 20 0">
<joint type="hinge" axis="0 1 0"/>
<joint type="hinge" axis="1 0 0"/>
<joint name="leg02_hinge_y" type="hinge" axis="0 1 0"/>
<joint name="leg02_hinge_x" type="hinge" axis="1 0 0"/>
<geom type="cylinder" size="0.005 0.03" axisangle="1 0 0 90"/>
<geom type="box" pos="0 -0.03 0.03" size="0.02 0.005 0.04"/>
<geom type="box" pos="0 0.03 0.03" size="0.02 0.005 0.04"/>
Expand All @@ -162,8 +162,8 @@
<geom type="box" pos="0.02 0.0 0.03" size="0.005 0.01 0.03"/>
<geom type="cylinder" pos="0 0 0.05" size="0.005 0.02" axisangle="0 1 0 90"/>
<body name="leg12" pos="0 0 0.05" euler="-30 -20 0">
<joint type="hinge" axis="0 1 0"/>
<joint type="hinge" axis="1 0 0"/>
<joint name="leg12_hinge_y" type="hinge" axis="0 1 0"/>
<joint name="leg12_hinge_x" type="hinge" axis="1 0 0"/>
<geom type="cylinder" size="0.005 0.03" axisangle="1 0 0 90"/>
<geom type="box" pos="0 -0.03 0.03" size="0.02 0.005 0.04"/>
<geom type="box" pos="0 0.03 0.03" size="0.02 0.005 0.04"/>
Expand Down
8 changes: 6 additions & 2 deletions src/unilab/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions tests/base/test_backend_pre_step_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions tests/base/test_motrix_backend_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
70 changes: 64 additions & 6 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -785,37 +785,95 @@ 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",
root=tmp_path,
)


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,
Expand Down
10 changes: 5 additions & 5 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading