mjlab integration (MuJoCo backend)#
This page documents the integration of mjlab’s registered task suite into RoboVerse’s MetaSim/MuJoCo backend.
mjlab pairs the Isaac-Lab manager-based RL API with MuJoCo-Warp’s GPU
simulator. RoboVerse’s MetaSim already speaks MuJoCo (CPU), so the goal
of this integration is physics-step parity: for a given task MJCF and
identical control inputs, the trajectory MetaSim produces should match
what mjlab’s underlying mj_step produces. Once physics parity holds,
reward functions and observation manager ports become straightforward
because they read from identical state.
A live report with embedded videos and parity tables is at http://localhost:8000/#roboverse/mjlab_integration.
Coverage#
All 12 mjlab-registered task IDs are wrapped under
roboverse_pack.tasks.mjlab and run end-to-end on MetaSim. Final
parity (200 control steps, raw mujoco vs MetaSim/MuJoCo):
Task |
nq |
qpos_max|Δ| |
qvel_max|Δ| |
verdict |
|---|---|---|---|---|
|
2 |
0.00e+00 |
0.00e+00 |
bitwise |
|
2 |
0.00e+00 |
0.00e+00 |
bitwise |
|
36 |
1.59e-13 |
1.46e-13 |
machine epsilon |
|
36 |
1.59e-13 |
1.46e-13 |
machine epsilon |
|
36 |
1.59e-13 |
1.46e-13 |
machine epsilon |
|
36 |
1.59e-13 |
1.46e-13 |
machine epsilon |
|
19 |
1.58e-01 |
1.71e-01 |
contact residual |
|
19 |
1.58e-01 |
1.71e-01 |
contact residual |
|
8 |
1.61e+00 |
1.20e+01 |
contact residual |
|
8 |
1.61e+00 |
1.20e+01 |
contact residual |
|
8 |
1.61e+00 |
1.20e+01 |
contact residual |
|
8 |
1.61e+00 |
1.20e+01 |
contact residual |
The cartpole reward (mjlab’s cartpole_smooth_reward) is also bitwise
identical (Σ reward = 114.32 exact on balance, 0.084 exact on
swingup).
Asset layout#
MJCF assets resolve in two ways (see roboverse_pack/tasks/mjlab/_locator.py):
Local mjlab clone —
~/projects/mjlab, or setMJLAB_REPO=/path/to/mjlab. Used for development so the canonical mjlab bytes drive parity checks.HuggingFace fallback — with no clone present, the referenced XML and its meshes are downloaded on demand from
RoboVerseOrg/roboverse_dataunderrobots/mjlab/…. This is what lets a fresh checkout run without cloning mjlab.
MJCFs we currently consume:
tasks/cartpole/cartpole.xml— the cartpole scene (own floor, rails, cameras, plus cart+pole).asset_zoo/robots/unitree_g1/xmls/g1.xml— Unitree G1 humanoid (29 DoF, internal<freejoint name="floating_base_joint"/>).asset_zoo/robots/unitree_go1/xmls/go1.xml— Unitree Go1 quadruped (12 DoF, same internal freejoint pattern).asset_zoo/robots/i2rt_yam/xmls/yam.xml— i2rt YAM 7-DoF arm.
Loading model: scene vs robot#
mjlab assets fall into two shapes that need different MetaSim load paths:
Complete scene MJCF (e.g. cartpole.xml). The MJCF declares its own floor, rails, lights, cameras, plus the moving body. Loading it as
RobotCfg(mjcf_path=…)triggers namespace-wrapping that re-parents the worldbody decorations into a wrapper body, breaking the authored world-frame placement. Instead, pass it as the scene:ScenarioCfg( scene=SceneCfg(mjcf_path=mjlab_asset("tasks/cartpole/cartpole.xml")), robots=[], simulator="mujoco", add_default_ground=False, # cartpole.xml already declares a floor )
Bare robot MJCF (g1, go1, yam). The MJCF only declares the robot; no world floor, no cameras. Load as
RobotCfg:ScenarioCfg( robots=[RobotCfg( name="g1", num_joints=29, fix_base_link=False, mjcf_path=mjlab_asset("asset_zoo/robots/unitree_g1/xmls/g1.xml"), enabled_self_collisions=False, )], simulator="mujoco", add_default_ground=False, # raw rollout doesn't add a ground either )
The add_default_ground=False is critical when comparing against raw
MjModel.from_xml_path — the raw load doesn’t auto-attach a ground
plane, so MetaSim mustn’t either.
Fixes that landed in MetaSim#
Five small, backward-compatible changes on branch
fix/mujoco-mjlab-task-parity:
ScenarioCfg.add_default_ground(defaultTrue). New opt-out field so callers can suppress MetaSim’s auto-floor when their scene MJCF already provides one. Default keeps the legacy behavior.Missing-inertial fallback in
_add_robots_to_model. Robots whose root body has no<inertial>(URDF norm, MuJoCo computes inertia from geom mass) no longer crash withAttributeError.MJCF root-offset composition in
_add_robots_to_model. When the re-rooting branch hoists a non-origin root body onto the attachment wrapper, the user-supplieddefault_positionnow composes with the MJCF offset instead of overwriting it. Backward-compatible for URDFs whose root body sits at origin.Inner-freejoint promotion in
_add_robots_to_model. Any author-declared<freejoint>inside the robot MJCF is removed before attach and re-added on the wrapper (which is top-level by construction), so mjlab’s go1 / g1 no longer trip MuJoCo’s “free joint can only be used on top level” compile-time check.BaseTaskEnvclass-attr scenario fallback. The docstring already promised “If None, it will use the class variable ‘scenario’”; the implementation now honours it. Required for class-based@register_task("mjlab.*")wrappers to instantiate viacls().
Regression tests live at
metasim/test/test_mujoco_scene_and_attach.py (mocked / minimal XML,
no heavy runtime).
Known divergence sources#
The two open residuals (go1 / yam) trace to contact filtering semantics:
raw mujoco default:
option.flag.filterparent = enable(parent-child contacts filtered); no<exclude>pairs.MetaSim with
enabled_self_collisions = False: adds<exclude>for all body pairs inside the robot. Over-restrictive.MetaSim with
enabled_self_collisions = True: setsfilterparent = disableglobally. Over-permissive.
Neither MetaSim option matches mujoco’s default. A follow-up MetaSim
PR will add a self_collisions = "mujoco_default" sentinel.
How to reproduce#
conda activate roboverse
cd "$ROBOVERSE" # repo root
PYTHONPATH="$ROBOVERSE:$METASIM" \
python -m tools.mjlab_integration.runner --no-render --n-steps 200 # loader-only
PYTHONPATH="$ROBOVERSE:$METASIM" \
MUJOCO_GL=egl \
python -m tools.mjlab_integration.full_runner --n-steps 200 # full pipeline
PYTHONPATH="$ROBOVERSE:$METASIM" \
MUJOCO_GL=egl \
python -m tools.mjlab_integration.render_sweep --frame-stride 3 # side-by-side mp4
PYTHONPATH="$ROBOVERSE:$METASIM" \
python -m tools.mjlab_integration.reward_sweep # reward parity
cd "$METASIM"
MUJOCO_GL=egl pytest metasim/test/test_mujoco_scene_and_attach.py -v # regression tests
Artefacts land at reports/mjlab_integration/
(summary_full.json, reward_summary.json, render_summary.json, plus
runs/<task>/qpos_compare_full.png / side_by_side_full.mp4 /
reward_compare.png).
Files added#
Harness —
RoboVerse/tools/mjlab_integration/inventory.py— task catalog (12 entries)raw_rollout.py— pure mujoco ground truth +RolloutResult(now carriesjnt_qposadr/jnt_dofadr/jnt_type)metasim_rollout.py— loader-only and full-pipeline driversfull_runner.py/runner.py— sweep orchestrationrender_sweep.py— side-by-side mp4 (side_by_side_videoinrender.py)reward_sweep.py+rewards.py— port of mjlab’s reward kernelsdiff.py/plot.py— l2 / max-abs / bitwise metrics + overlay plotsdiagnose_*.py— step-by-step root-cause scripts kept for posterity
Task wrappers —
RoboVerse/roboverse_pack/tasks/mjlab/cartpole.py(Balance / Swingup, scene-mode)floating_base.py(G1 / Go1 velocity flat+rough, G1 tracking ×2)lift_cube.py(Yam base + RGB/depth/seg variants)_locator.py—mjlab_asset(relpath)shared by all wrappers