[!TIP]
Related skills:
- mujoco-spec-editing — Procedural model editing and composing scenes with
MjSpec.- mujoco-rendering — Offscreen image rendering, cameras, depth, and segmentation.
- mujoco-accelerated — GPU/TPU physics simulation with MJX (JAX) and MJWarp (CUDA).
- mujoco-studio — Interactive viewers, Studio viewer apps, and plugin architecture.
[!CAUTION]
- NEVER use
mj_name2id,model.name2id, or raw integer indexing (e.g.,data.qpos[0],data.xpos[body_id]) to look up elements by name. Always use named access (data.body('name').xpos) orbind()instead.- Do not use dm_control / PyMJCF lookup patterns.
model.find('body', 'name'),.find_body(), andphysics.nameddo not exist in MuJoCo 3.x. Use direct accessors:.body('name'),.geom('name')etc. onMjSpec,MjModel, andMjData.
1. Compilation Lifecycle: MjSpec → MjModel → MjData
MuJoCo models follow a three-stage lifecycle:
MjSpec ──spec.compile()──▶ MjModel ──MjData(model)──▶ MjData
│ │ │
│ (mutable blueprint) │ (compiled, mostly frozen) │ (simulation state)
│ │ │
└── spec.recompile(m, d) ─────┴────────────────────────────┘
MjSpec: Mutable data structure defining the simulation elements and hierarchy.MjModel: Compiled, optimized physics model containing constant and precomputed parameters. Structural changes require recompilation.MjData: Dynamic simulation state (positions, velocities, forces, contacts, diagnostic buffers).
Python Lifecycle
import mujoco
# 1. Build or load spec
spec = mujoco.MjSpec()
body = spec.worldbody.add_body(name='pendulum', pos=[0, 0, 1])
body.add_geom(type=mujoco.mjtGeom.mjGEOM_SPHERE, size=[0.1])
body.add_joint(name='hinge', type=mujoco.mjtJoint.mjJNT_HINGE, axis=[0, 1, 0])
# 2. Compile to model and instantiate data
model = spec.compile()
data = mujoco.MjData(model)
mujoco.mj_forward(model, data)
# 3. Dynamic recompilation (preserves existing state)
body2 = spec.worldbody.add_body(name='ball2', pos=[1, 0, 1])
body2.add_geom(size=[0.1])
body2.add_freejoint()
model, data = spec.recompile(model, data) # Always reassign return values
C++ Lifecycle
#include <mujoco/mujoco.h>
// 1. Create or load spec (or: mj_parseXML(filename, nullptr, error, sizeof(error)))
mjSpec* spec = mj_makeSpec();
mjsBody* world = mjs_findBody(spec, "world");
mjsBody* body = mjs_addBody(world, nullptr);
mjsGeom* geom = mjs_addGeom(body, nullptr);
geom->type = mjGEOM_SPHERE;
geom->size[0] = 0.1;
// 2. Compile to model and create data
mjModel* model = mj_compile(spec, nullptr);
if (!model) {
printf("%s\n", mjs_getError(spec));
}
mjData* data = mj_makeData(model);
// 3. Step or evaluate
mj_forward(model, data);
// 4. Clean up allocated resources
mj_deleteData(data);
mj_deleteModel(model);
mj_deleteSpec(spec);
Loading and Serializing
# Direct model loading from file or string
model = mujoco.MjModel.from_xml_path('model.xml')
model = mujoco.MjModel.from_xml_string(xml_str)
model = mujoco.MjModel.from_binary_path('model.mjb') # Precompiled binary format
# Loading mutable spec from file or string
spec = mujoco.MjSpec.from_file('model.xml')
spec = mujoco.MjSpec.from_string(xml_str)
# Serialization
xml_string = spec.to_xml()
spec.encode('model.xml', model)
[!CAUTION]
mujoco.load_model_from_pathdoes not exist in MuJoCo 3.x. Usemujoco.MjModel.from_xml_path(path)ormujoco.MjSpec.from_file(path).
2. Accessing State: Named Access & bind()
1. Named Access (on MjModel and MjData)
In Python, access elements by calling the element-type accessor with the element name:
# Reading state
torso_pos = data.body('torso').xpos # NumPy view (3,)
knee_angle = data.joint('knee').qpos # Shape depends on joint type
ball_size = model.geom('ball').size # NumPy view (3,)
# Writing controls and state
data.actuator('motor').ctrl = 1.0 # Writable view
data.joint('knee').qpos = 0.5
Supported element accessors: body, geom, joint (alias jnt), site,
camera (alias cam), light, mesh, skin, texture (alias tex),
material (alias mat), pair, equality (alias eq), tendon (alias
ten), actuator, sensor, numeric, text, tuple, keyframe (alias
key).
[!WARNING]
Named access returns views, not copies. In a simulation loop, modifying the simulation changes the array in-place. If logging or storing history, always call
.copy():# ❌ WRONG — all history items will point to the same final state history.append(data.body('torso').xpos) # ✅ RIGHT — records distinct coordinates history.append(data.body('torso').xpos.copy())
2. bind() (Bridges MjSpec elements → MjModel / MjData)
When building models programmatically with MjSpec, use bind() to map spec
element handles directly to compiled data:
geom = spec.worldbody.add_geom(name='ball', size=[0.1])
joint = spec.worldbody.add_joint(name='hinge', type=mujoco.mjtJoint.mjJNT_HINGE)
model = spec.compile()
data = mujoco.MjData(model)
# Read through bind
radius = model.bind(geom).size[0]
ball_pos = data.bind(geom).xpos
# Write through bind — always use .set()
data.bind(joint).set('qpos', 0.5)
# Bind lists of spec elements in batch
joints = [spec.joint('j1'), spec.joint('j2')]
joint_angles = data.bind(joints).qpos
3. Simulation Stepping & Execution Pipeline
Stepping Pipeline (mj_step vs mj_step1 / mj_step2)
mj_step(model, data): Advances the simulation by one timestep (model.opt.timestep). It computes kinematics, forces, collision constraints, accelerations, and numerical integration in one full pass.- Split Stepping (
mj_step1+mj_step2): Used when custom controller forces or external computations depend on the current kinematic state:
# 1. Advance position kinematics and sensor computations
mujoco.mj_step1(model, data)
# 2. Apply control torques or external forces based on current kinematics
data.ctrl[:] = compute_feedback_control(data.qpos, data.qvel)
data.qfrc_applied[:] = compute_external_forces(data)
# 3. Complete dynamic acceleration and integration
mujoco.mj_step2(model, data)
Sensor and Kinematic Initialization (mj_forward)
Before reading sensors or positions at time zero, call mj_forward() instead of
mj_step(). mj_forward evaluates kinematics and sensor outputs from the
current state without advancing simulation time:
# ✅ RIGHT — initialize sensors and kinematics at t=0
mujoco.mj_forward(model, data)
initial_obs = data.sensordata.copy()
# Advance physics
for _ in range(num_steps):
mujoco.mj_step(model, data)
Keyframe Resets & Restoring State
Use keyframes to reset configurations reliably:
# Python: reset to named keyframe
key_id = model.key('home').id
mujoco.mj_resetDataKeyframe(model, data, key_id)
mujoco.mj_forward(model, data) # Recompute kinematics and sensors
In C++:
int key_id = mj_name2id(m, mjOBJ_KEY, "home");
if (key_id >= 0) {
mj_resetDataKeyframe(m, d, key_id);
mj_forward(m, d);
}
Integrators & Numerical Stability
Configured via model.opt.integrator:
mjINT_EULER(default, kept for compatibility): Semi-implicit Euler; only joint damping is integrated implicitly.mjINT_IMPLICITFAST(recommended): Implicit in velocity, i.e. one Newton step linearized in velocity using analytical derivatives of velocity-dependent forces (joint/tendon/actuator damping includingkv, fluid drag, gyroscopic forces of free bodies). Skips Coriolis/centripetal derivatives and symmetrizes the derivative so it can use Cholesky; cost is similar to Euler.mjINT_IMPLICIT: Likeimplicitfastbut also includes Coriolis/centripetal derivatives (LU factorization). Helps rapidly spinning multi-link chains.mjINT_RK4: 4th-order Runge-Kutta. Most accurate for smooth, energy-conserving systems; costs four force evaluations per step.mjINT_DISCRETE(experimental): The only integrator that is also implicit in position, so stiff springs and position servos are treated implicitly, including their interaction with contacts.
[!NOTE]
implicit/implicitfastare not full backward Euler: they are not implicit in position (stiffness) and do not differentiate constraint/contact forces. Their stability benefit is for large velocity-dependent forces.
4. Contact & Force Sensing: Declarative Sensors
Why data.contact Should NOT Be Used for Observations
data.contact is a variable-length buffer that changes size and ordering every
timestep based on active collision pairs:
# ❌ WRONG — variable size and non-deterministic order break ML policies
for c in data.contact:
if c.geom1 == target_id:
force = ...
Declarative Contact Sensors
<contact> sensors declaratively filter contacts, reduce them to a fixed
number of slots, and write deterministic vectors directly into data.sensordata:
<sensor>
<!-- Detect contacts between gripper and object: fixed 3 slots -->
<contact name="grip_contact"
body1="gripper" body2="object"
num="3" data="found force normal"
reduce="maxforce"/>
<!-- Net wrench on an entire body -->
<contact name="object_net"
body1="object"
data="force torque"
reduce="netforce"/>
</sensor>
Reading contact sensors in Python:
contact_data = data.sensor('grip_contact').data # Always fixed length
Touch Sensors for Scalar Normal Force
For scalar surface contact forces (e.g., fingertips), use touch sensors
attached to a site. Make sure the site is large enough to encompass the geom:
site = body.add_site(name='fingertip', pos=[0, 0, 0.05], size=[0.02])
spec.add_sensor(
name='touch_sensor',
type=mujoco.mjtSensor.mjSENS_TOUCH,
objname=site.name,
objtype=mujoco.mjtObj.mjOBJ_SITE,
)
5. Spatial Mathematics (mju_ Library)
MuJoCo provides an optimized spatial mathematics library under the mju_
namespace in both C++ and Python.
[!IMPORTANT]
MuJoCo quaternions are scalar-first:
[w, x, y, z]. SciPy and ROS use scalar-last:[x, y, z, w]. Swizzle components when converting:# MuJoCo -> SciPy/ROS scipy_quat = mj_quat[[1, 2, 3, 0]] # SciPy/ROS -> MuJoCo mj_quat = scipy_quat[[3, 0, 1, 2]]
Quaternion Operations
res = np.zeros(3)
mujoco.mju_rotVecQuat(res, vec, quat) # Rotate vector by quaternion
quat = np.zeros(4)
mujoco.mju_mat2Quat(quat, mat9) # Flat (9,) rotation matrix → quaternion
mujoco.mju_quat2Mat(mat9, quat) # Quaternion → flat (9,) matrix
mujoco.mju_axisAngle2Quat(quat, axis, angle) # Axis-angle → quaternion
mujoco.mju_euler2Quat(quat, euler, 'xyz') # Euler angles → quaternion
mujoco.mju_mulQuat(res_q, q1, q2) # Compose quaternions (q1 * q2)
mujoco.mju_negQuat(res_q, quat) # Quaternion conjugate (inverse)
mujoco.mju_quatZ2Vec(quat, vec) # Quaternion rotating Z-axis to vector
Pose Operations
# Compose poses (pos1/quat1 then pos2/quat2)
mujoco.mju_mulPose(pos_res, quat_res, pos1, quat1, pos2, quat2)
# Invert pose
mujoco.mju_negPose(pos_res, quat_res, pos, quat)
# Transform 3D point by pose
mujoco.mju_trnVecPose(res, pos, quat, vec)
6. Energy & Diagnostics
MuJoCo can continuously compute total mechanical energy to diagnose simulation stability:
# Enable energy monitoring
model.opt.enableflags |= mujoco.mjtEnableBit.mjENBL_ENERGY
mujoco.mj_step(model, data)
potential_energy = data.energy[0]
kinetic_energy = data.energy[1]
total_energy = potential_energy + kinetic_energy
In passive, unforced, undamped systems, total_energy must remain constant.
Growing energy indicates numerical integration instability (requiring smaller
timesteps, or implicitfast if large damping or other velocity-dependent forces
are the cause).
7. Compute Backend Selection
| Feature | C++ Engine (Default) | MJX (JAX) | MJWarp (CUDA) |
|---|---|---|---|
| Import | import mujoco |
from mujoco import mjx |
import mujoco_warp as mjw |
| Primary Use | Real-time control, interactive tools, full solver support | Differentiable physics, large batch RL | High-throughput batch rollouts |
| Hardware | CPU | GPU / TPU | NVIDIA GPU |
| Precision | float64 | float32 | float32 |
| Solvers | Newton, CG, PGS, noslip | Newton, CG | Newton, CG |
| Plugins | All plugins | Not supported | SDF only |
(For detailed GPU and TPU execution patterns, refer to mujoco-accelerated.)
8. Common Gotchas & Best Practices
- Computed fields are read-only: Fields like
data.xpos,data.xmat, anddata.xquatare output buffers. Never assign to them directly; change inputs (data.qpos,data.ctrl) and callmj_forward()ormj_step(). - Duplicate names are forbidden: Spec element names must be unique within each element type.
- Orientation keywords are mutually exclusive: When adding geoms, bodies,
or sites, provide only one of
quat,euler,axisangle,xyaxes, orzaxis. - Euler sequences: Lowercase characters denote intrinsic rotations
(
'xyz'), uppercase denote extrinsic rotations ('XYZ'). - Coordinate convention: +Z is up, gravity is
[0, 0, -9.81]. - Repeating controls with
nstep: Callingmj_step(model, data, nstep=N)repeats the current control acrossNsteps. For dynamic feedback control, step one timestep at a time.