mujoco · diff
v1.0.2 to git:20260907.863840e
40 added, 247 removed. Audit A to A.
---
name: mujoco
- version: 1.0.2
- description: >
- MuJoCo for lightweight, contact-rich robot manipulation simulation on
- macOS/Linux, especially single-arm grasping without ROS: headless offscreen
- rendering, MJCF models, mujoco_menagerie assets, damped-least-squares inverse
- kinematics, and empirical grasp calibration. Use when: 'MuJoCo', 'MJCF',
- 'mjpython', 'MUJOCO_GL', 'menagerie', 'SO-101' / SO-ARM100 arm, 'offscreen
- render', 'inverse kinematics' / 'IK', 'grasp' / 'pick and place' in sim,
- hand-building a manipulation env, or headless mujoco.Renderer determinism.
- Load after architect or simulation route a non-ROS single-arm manipulation
- task here (Gazebo and Isaac Sim are the ROS / GPU-photoreal alternatives).
- Defers to MuJoCo's own docs and mujoco_menagerie for the physics/model API;
- embeds the macOS-arm64 platform and grasp/IK gotchas hard-won from a real
- SO-101 build. Not for: ROS-integrated sim (gazebo), GPU-parallel RL or
- photoreal synthetic data (isaac-sim / isaac-lab), or LeRobot policy
- training/eval mechanics (lerobot).
+ description: Build and debug lightweight robot manipulation simulations with MuJoCo.
---
- # mujoco
-
- MuJoCo 3.x for robium's lightweight, non-ROS manipulation side: a fast
- contact-rich physics engine with an offscreen renderer, well suited to
- hand-building a single-arm grasping/pick-and-place environment when the full
- ROS + Gazebo stack (or a GPU-gated Isaac stack) is more than the task needs.
- This skill stays deliberately thin on the MuJoCo API itself: the model format
- (MJCF), the Python bindings, and the robot assets in mujoco_menagerie are all
- documented upstream and move release to release. What it embeds instead is the
- set of hard-won gotchas from the vla-trial SO-101 build (macOS arm64, MuJoCo
- 3.x, observed 2026-07-13/14): the macOS headless-render backend, version-pin
- traps, offscreen-render determinism, and the IK / grasp-calibration failure
- modes that cost the most time and give no error message when they bite.
-
- ## When to use this skill
-
- - Simulating a low-DOF arm (grasping, pick-and-place, contact-rich
- manipulation) where ROS is not required and a GPU-photoreal simulator is
- overkill; MuJoCo runs natively and fast on Apple Silicon.
- - Rendering camera observations headless for a policy/dataset pipeline, and
- needing that rendering to be deterministic/reproducible.
- - Writing an inverse-kinematics reach + grasp routine by hand (damped
- least-squares), and debugging why the arm won't reach or knocks the object
- over.
- - Loading a robot from mujoco_menagerie (e.g. the SO-101 / SO-ARM100 arm) and
- adapting its scene into a manipulation task.
- - The trigger phrases in the description: 'MuJoCo', 'MJCF', 'mjpython',
- 'MUJOCO_GL', 'menagerie', 'SO-101', 'offscreen render', 'IK', 'grasp'.
- - Cross-references: go to the sibling skill instead when the question is:
- - A ROS 2-integrated simulation (ros_gz bridge, SDF worlds) → the `gazebo`
- skill.
- - Photoreal rendering, synthetic data at scale, or GPU-parallel RL → the
- `isaac-sim` / `isaac-lab` skills (GPU-gated).
- - Which simulator to pick at all → the `simulation` umbrella (routes here for
- lightweight non-ROS manipulation).
- - LeRobot dataset/policy training/eval mechanics → the `lerobot` skill;
- MuJoCo here is only the sim that produces observations/actions.
- - The whole-stack decision this feeds into → the `architect` skill.
-
- ## Key directives
-
- - **Delegation posture: embed + links.** The robium-specific,
- hard-won gotchas (macOS render backend, version pins, offscreen-render
- determinism, IK/grasp failure modes) live here because they are not written
- down anywhere upstream. For the physics/model API itself (MJCF syntax, the
- `mujoco` Python bindings, `mj_step`/`mj_forward`, the `Renderer` class, and
- robot assets) point upstream to MuJoCo's docs and the mujoco_menagerie repo
- (see References); do not restate the API from memory, it changes across 3.x
- releases.
- - **On macOS, `MUJOCO_GL=cgl` is the only headless render backend.** <!-- id: macos-gl-cgl-only --> osmesa and
- egl are Linux-only (mujoco#2164). Set it before importing `mujoco`.
- - **Watch the version-pin trap.** <!-- id: mujoco-version-pin-trap --> Menagerie's SO-101 needs MuJoCo ≥3.1.3, but
- common gym wrappers pin it *down* (gym-hil pins `mujoco<3.9`, gym-xarm pins
- `<3.0`); depending on one of those can silently downgrade MuJoCo below the
- model's requirement. Pin MuJoCo explicitly and verify the installed version.
- - **Hand-written IK fails silently.** <!-- id: ik-fails-silently --> A damped-least-squares solver does not
- raise on an unreachable target or on a bad grasp orientation; it returns a
- locally-optimal pose short of the goal. Probe reachability and calibrate
- grasps empirically (see Usage patterns); never assume a returned qpos means
- success.
- - **Verify the asset matches the robot's intended work envelope** <!-- id: verify-asset-work-envelope --> before
- blaming IK. Menagerie scenes are generic and can put the floor, object spawn,
- or camera in the wrong place for the arm (see Platform gotchas).
-
- ## Quick start
-
- **1. Install (pin MuJoCo explicitly, above the menagerie floor):** <!-- id: pin-mujoco-install -->
-
- ```bash
- uv add "mujoco>=3.1.3" # current 3.10 as of 2026-07; SO-101 needs >=3.1.3
- ```
-
- Do not let a gym wrapper (gym-hil, gym-xarm) choose the version transitively;
- it can pin MuJoCo below what the model needs.
-
- **2. Get the robot asset from mujoco_menagerie, not upstream MJCF.** <!-- id: use-menagerie-not-upstream-mjcf --> Clone
- [mujoco_menagerie](https://github.com/google-deepmind/mujoco_menagerie) and use
- its SO-101 model (the robotstudio_so101 directory, which ships a pick-and-place
- scene, manipulation-tuned collision geometry, and a camera mount). Do NOT use
- the upstream TheRobotStudio/SO-ARM100 MJCF: its own README admits the gripper
- linear-joint mapping is not reflected in the model.
-
- **3. Headless offscreen render (macOS):**
-
- ```python
- import os
- os.environ["MUJOCO_GL"] = "cgl" # macOS headless backend; must precede import
- import mujoco
-
- model = mujoco.MjModel.from_xml_path("scene.xml")
- data = mujoco.MjData(model)
- renderer = mujoco.Renderer(model, height=256, width=256)
- # warm-up: one throwaway full reset+render (see determinism gotcha below)
- mujoco.mj_resetData(model, data)
- mujoco.mj_forward(model, data)
- renderer.update_scene(data, camera="fixed_cam")
- _ = renderer.render()
- ```
-
- **4. Interactive viewer needs `mjpython`.** `mujoco.viewer.launch_passive`
- requires launching with `mjpython`, not `python` (and is buggy under `uv`,
- mujoco#1923). Offscreen rendering (step 3) has no such requirement.
-
- For MJCF syntax, the bindings, and API signatures, go to MuJoCo's own docs
- (References) before hand-writing model or physics code.
-
- ## Usage patterns
-
- **Headless render is not the bottleneck on Apple Silicon.** <!-- id: render-not-bottleneck --> Measured on the
- vla-trial build: ~84 fps at 256x256 via `MUJOCO_GL=cgl`, against a 60 fps
- floor; `mj_step` ~0.012 ms vs ~11.8 ms per render. This kills the common
- upstream worry that "offscreen render is extremely slow": for a low-DOF arm
- scene it is comfortably real-time. Render, don't skip frames to save time.
-
- **Deterministic offscreen rendering** (needed for reproducible datasets/evals):
-
- - **Use a static xyaxes look-at camera, not `mode="targetbody"`.** <!-- id: static-camera-not-targetbody --> A
- target-tracking camera re-runs its smoothing filter on every
- `update_scene()`, making the rendered pose call-history-dependent (observed
- up to 85 LSB drift) even when the physics is bit-identical.
- - **Warm up the renderer with one throwaway full reset + render in `__init__`.** <!-- id: renderer-warmup-reset -->
- A fresh `mujoco.Renderer`'s first reset→`mj_forward`→render cycle is not
- repeatable against later renders (~6 LSB). A raw pre-`mj_forward` warm-up does
- NOT consume the cold cycle; it must be a full reset+render.
- - Ruled out as a cause: MSAA offsamples (a red herring here).
-
- **Reachability probing (IK).** <!-- id: reachability-probing-ik --> Because the DLS solver returns silently on an
- unreachable target, probe the full task footprint × every lift height
- *independently* before trusting the env. Menagerie's default box spawn
- (0.5, 0, 0.03) is unreachable by the SO-101 mount, for example.
-
- **Empirical grasp calibration.** <!-- id: empirical-grasp-calibration --> The end-effector site is not the grasp point.
- Calibrate the gripper-local grasp offset by experiment: teleport the object to
- a grid, close, lift, and keep the offsets that actually lift. Key facts:
-
- - Calibration is a function of **pose**, not a gripper property; recalibrate <!-- id: grasp-calibration-is-pose-function -->
- after any wrist-orientation change.
- - It is millimeter-sensitive and non-monotonic: a 3 mm offset change cost 6/10 <!-- id: grasp-offset-millimeter-sensitive -->
- success in one sweep. Sweep the grasp offset *and* the pedestal height
- end-to-end rather than tuning one in isolation.
-
- **Wrist-roll orientation is the silent half of position-only IK.** <!-- id: wrist-roll-position-only-ik --> On a 5-DOF
- arm, position-only IK leaves wrist roll free. At roll≈0 the pinch axis was 91%
- vertical, so a 4.2 cm aperture could not span a 6 cm cube (closed on air,
- 0/10). Diagnostic: print the world-frame pinch axis and check it is
- perpendicular to the grasped dimension. Fix: solve wrist roll as a 1-D root
- find (`pinch_z(roll)` is a smooth sinusoid), then re-solve position with roll
- pinned via jnt_range; adding roll to the DLS objective *diverges*. This took
- the oracle grasp from 0/10 to 8/10. (Position-only IK also gives no signal
- about the orientation it chose; a level-finger constraint may be genuinely
- infeasible on a 5-DOF arm.)
-
- **Debugging "arm won't reach / knocked the object over":**
-
- - **Bisect IK vs obstruction** <!-- id: bisect-ik-vs-obstruction --> by deleting/banishing the object and re-running
- the same pose. Cube present: arm_err=0.18, saturated_joints=[1 2]; cube
- banished: arm_err=0.0006, saturated=[]. Instantly tells you whether IK or a
- collision is the problem.
- - **Check `qfrc_actuator` vs `forcerange`** <!-- id: check-qfrc-actuator-forcerange -->: saturated joints mean BLOCKED,
- not slow. Raising settle steps (e.g. 12→60) is a dead end when joints are
- saturated.
- - **The first waypoint can sweep the arm through the object** <!-- id: first-waypoint-sweep-collision -->: a position
- servo takes an arbitrary joint path to the target. Raising the initial
- approach waypoint (0.16→0.20 m) fixed a 2/10 → 10/10 grasp.
-
- **Resolve gripper geoms by body, not name.** <!-- id: gripper-geoms-by-body --> Menagerie leaves mesh geoms
- UNNAMED, so a name-based gripper-geom list silently misses the fixed-jaw
- collision mesh, a false-success risk in contact checks. Collect gripper geoms
- by `model.geom_bodyid` membership instead.
-
- **Determine gripper polarity empirically.** <!-- id: gripper-polarity-empirical --> Drive the gripper joint to each end
- of its `actuator_ctrlrange` and measure the fingertip gap. SO-101 is
- LOW=closed / HIGH=open; the upstream SO-ARM100 docs claim the opposite and even
- admit their own MJCF disagrees, so measure, don't trust the docs.
-
- ## Platform gotchas
-
- - **macOS headless render backend is `MUJOCO_GL=cgl` only** (osmesa/egl are
- Linux-only, mujoco#2164). Set it before `import mujoco`.
- - **The macOS CGL renderer is thread-affine: a cross-thread first render is a
- silent deadlock.** <!-- id: cgl-thread-affine-deadlock --> A `mujoco.Renderer` created on one thread hangs forever
- (no error, no timeout, no exception) the first time a *different* thread
- renders on it; the stack sits in cgl `make_current`. This bites frameworks
- that run handlers on worker threads (e.g. Gradio): a first `env.reset()`
- called from a worker freezes the stream. Fix: construct the env/renderer in
- the same thread that runs the episode (a per-run env in the handler), not once
- at import on the main thread. (observed 2026-07-15, vla-trial)
- - **Interactive viewer needs `mjpython`**, <!-- id: mjpython-viewer-required --> not `python`, and is buggy under
- `uv` (mujoco#1923). Offscreen rendering does not need `mjpython`.
- - **No off-the-shelf SO-101 Gymnasium env exists.** <!-- id: no-so101-gym-env --> gym-hil is Franka-only;
- LIBERO is Linux-only. A language-conditioned SO-101 sim env must be
- hand-built; budget for it rather than expecting to import one.
- - **Menagerie assets can be geometrically wrong for the robot.** <!-- id: menagerie-assets-geometrically-wrong --> The SO-101
- scene put the floor at the arm's base level; a real rig has base + objects on
- a raised surface. A 6 cm pedestal restored the intended work envelope (fixing
- a forced ~32° down-pitch). Check the asset world matches the robot's intended
- envelope before blaming the IK solver.
+ # MuJoCo
- ## Customization
+ A plausible render proves little by itself. Follow the physical chain from
+ model through kinematics, actuation, contact, and observation.
- - **Different arm from menagerie:** the failure modes above (silent IK,
- unnamed geoms, empirical gripper polarity, static-camera determinism) are
- general MuJoCo/menagerie traps, not SO-101-specific. Re-run the empirical
- calibration (grasp offset grid, gripper-polarity sweep, reachability probe)
- for the new model rather than porting the SO-101 numbers.
- - **Different task envelope:** re-check the scene geometry (floor height,
- object spawn, camera pose) against the robot's reach before writing task
- logic; a pedestal or a re-placed spawn is often the fix, not the IK.
- - **Feeding a LeRobot pipeline:** MuJoCo here produces the observations and
- executes actions; the dataset format, recording, and policy training/eval
- belong to the `lerobot` skill; keep the sim env and the dataset layer
- separate.
+ ## Start from the model
- ## References
+ - Read the MJCF and the pinned asset revision before adding control code. Check
+ joint ranges, actuator limits, collision geometry, sites, masses, and the
+ intended work surface.
+ - Prefer a maintained model from
+ [MuJoCo Menagerie](https://github.com/google-deepmind/mujoco_menagerie), but
+ verify it against the real robot and task envelope.
+ - Confirm gripper polarity, fingertip gap, and contact geometry empirically.
+ Names and documentation can disagree with the model that actually runs.
+ - Use the current [MuJoCo documentation](https://mujoco.readthedocs.io/) for
+ MJCF and Python APIs rather than carrying signatures forward from memory.
- - Upstream (primary sources; check before writing model or physics code):
- [MuJoCo documentation](https://mujoco.readthedocs.io/) (MJCF, Python
- bindings, the `Renderer` API), [google-deepmind/mujoco GitHub
- repo](https://github.com/google-deepmind/mujoco) (releases and the issues
- cited above: #2164 render backends, #1923 viewer-under-uv),
- [mujoco_menagerie](https://github.com/google-deepmind/mujoco_menagerie)
- (robot assets, incl. the SO-101 model; prefer this over upstream
- TheRobotStudio/SO-ARM100 MJCF). All facts above observed on MuJoCo 3.x,
- macOS arm64, 2026-07-13/14 (vla-trial SO-101 build); re-verify version-tied
- claims against current releases.
- - Sibling skills: the `simulation` umbrella (routes here for lightweight
- non-ROS manipulation; owns the Gazebo-vs-Isaac selection), `gazebo` (ROS 2
- simulation), `isaac-sim` / `isaac-lab` (GPU-gated photoreal / RL sim),
- `lerobot` (policy training/eval that consumes this sim's observations),
- `architect` (routes the whole stack decision here).
+ ## Follow the physical chain
- ## Changelog
+ - **Kinematics:** solve only for reachable targets and check the residual;
+ damped least-squares can return a poor local solution without raising.
+ - **Actuation:** compare commanded position or torque with joint state,
+ actuator force, range limits, and saturation.
+ - **Contact:** inspect which geoms belong to the gripper and object. Unnamed
+ mesh geoms make name-only contact filters unsafe.
+ - **Grasp:** calibrate the grasp point, approach path, wrist orientation, and
+ lift together. The end-effector site is not automatically the physical pinch
+ point.
+ - **Observation:** make cameras and renderer lifecycle deterministic before
+ using frames as training or regression data.
- <!-- One dated line per battle-tested change, added by skill-author hardening sessions. -->
+ ## Go deeper only when needed
- - 1.0.2 (2026-08-03): style pass; removed em dashes throughout (no content changes).
+ - For reachability, collision, grasp, saturation, and rendering symptoms, read
+ [FAILURES.md](FAILURES.md).
+ - For the measured SO-arm and macOS evidence from Robium's manipulation trial,
+ read [SO-ARM-MACOS.md](SO-ARM-MACOS.md). Preserve its numbers only with the
+ stated model, scene, hardware, and renderer conditions.
+ - Use LeRobot guidance when the boundary reaches datasets, policies, or
+ evaluation; use simulator-selection guidance when MuJoCo itself has not yet
+ been chosen.
- - 1.0.1 (2026-08-01): anchor IDs added to claim-bearing items (learning-engine Phase 1); no content changes.
+ ## Done
- - 1.0.0 (2026-07-31): created: captured from the vla-trial SO-101 manipulation build (2026-07-13/14).
+ - The intended workspace is reachable, commands produce the expected joint and
+ contact state, grasps survive a lift across representative poses, and seeded
+ resets produce acceptably stable observations.