MuJoCo¶
The MuJoCo backend: install, offscreen rendering, physics queries, scene export and the MJCF editing surface.
By the end of this page you can run the default backend headless on a laptop, read physics, save and restore states, and know the MuJoCo-only calls.
pip install 'strands-robots[sim-mujoco]' # mujoco, robot_descriptions, imageio(-ffmpeg), mink, qpsolvers[daqp]
export MUJOCO_GL=cgl # macOS; headless Linux: egl or osmesa
What it is¶
MuJoCoSimEngine (strands_robots/simulation/mujoco/) is CPU physics at 500 Hz by default plus offscreen rendering, from mixins: physics.py (queries and state), scene_ops.py and spec_builder.py (MjSpec editing), rendering.py (cameras, observations), manipulation.py (attach, actuate), randomization.py, motion_primitives.py, recording.py. Robots come from robot_descriptions and bundled menagerie assets.
from strands_robots.simulation import create_simulation
sim = create_simulation("mujoco")
sim.create_world()
sim.add_robot("so101")
sim.add_object(name="cube", shape="box", size=[0.03, 0.03, 0.03], position=[0.25, 0.0, 0.15])
sim.step(200)
print(sim.get_body_state("cube")["content"][0]["text"].splitlines()[1])
sim.save_state("before")
sim.apply_force("cube", force=[0.0, 0.0, 5.0])
sim.step(50)
sim.load_state("before")
print(sim.export_xml()["status"], sim.get_total_mass()["status"], sim.get_energy()["status"])
sim.cleanup()
You should see the cube's pos: line with z near 0.015 (fallen and settled), then three success values.
Rendering¶
render(camera_name="default", width=None, height=None) returns a PNG in the content list; get_observation(robot) returns an (H, W, 3) array per camera plus a scalar per joint and its .vel companion. skip_images=True skips rendering, the 10x throughput win a non-VLA policy gets with requires_images = False. open_viewer() opens the interactive viewer when a display exists; on macOS run the script with mjpython (from the mujoco wheel) or the call is refused naming it.
Physics surface¶
| call | returns |
|---|---|
get_body_state(body) |
position, wxyz quaternion, linear and angular velocity |
get_contacts(), get_contact_forces() |
contact pairs, normal forces |
raycast(origin, direction), multi_raycast(...) |
first hit and distance |
get_jacobian(body), get_mass_matrix(), inverse_dynamics(), get_energy(), get_total_mass() |
dynamics quantities |
forward_kinematics(body) |
world pose after a forward pass |
get_sensor_data(name) |
any <sensor> in the model |
set_joint_positions(...), set_joint_velocities(...) |
write state directly |
set_body_properties(...), set_geom_properties(...) |
mass, friction, colour, live |
save_state(name), load_state(name) |
named snapshots of qpos, qvel, ctrl |
set_gravity(...), set_timestep(...) |
world parameters after creation |
get_ground_height(x, y) |
terrain height under a point |
mj_model and mj_data are the compiled model and data.
Scene editing¶
The scene is an MjSpec recompiled after every structural change, so add_robot, add_object and add_camera work on a live world. patch_scene_mjcf, replace_scene_mjcf, load_scene and export_xml edit, swap, load and write the model; details and sizes: worlds and objects.
Time¶
create_world(timestep=0.002) sets physics at 500 Hz. run_policy(control_frequency=50.0) steps 1 / (50 * timestep) physics substeps per action; control_substeps pins it. step(n) advances n physics steps; physics_timestep() reads the live value.
If the physics diverges MuJoCo resets the world; step, send_action and rollouts return status="error" with {"diverged": true}; reset() recovers.
Limits¶
- CPU only, one world per process; batched environments use
newtonorisaac. - Offscreen rendering needs a GL backend; headless Linux without EGL sets
MUJOCO_GL=osmesaand accepts slow frames. mjxaliases the same CPU engine; there is no JAX path.