MuJoCo splits a simulation into two objects, and almost every bug a beginner writes comes from confusing them.
mjModel describes the system: how many bodies and joints there are and how they connect, every mass, size, friction coefficient and actuator gain, the timestep and solver options. It is built once by compiling a model description, and the simulation never writes to it.
mjData holds the state of the system at one instant, the inputs you apply to it, and everything MuJoCo computes from those: body poses, contacts, forces, accelerations, sensor readings. Every call to mj_step reads mjModel and rewrites mjData.
The road from your XML file to a moving simulation has four stops:
| Stage | Object | What it is | Who changes it |
|---|---|---|---|
| MJCF text | a .xml file |
the model as you wrote it | you |
| parsed model | mjSpec |
an editable tree of elements (bodies, geoms, actuators) with defaults still unresolved | you, through code (Lesson 3.3) |
| compiled model | mjModel |
flat arrays of numbers, every default resolved, every derived constant computed | nobody, during simulation |
| simulation data | mjData |
state, inputs and computed quantities | mj_step, mj_forward, and you |
You can have several mjData for one mjModel, which is how parallel rollouts work: one description, many independent worlds. The reverse is never true: an mjData is allocated for one specific model and is meaningless with another.
The inspector in the side panel runs the cart-pole and shows its mjData live. Use the buttons to run one call at a time; every cell the call wrote turns amber.
```lab inspector {“dock”: true, “model”: “cartpole”, “key”: 0, “height”: 220, “camera”: {“azimuth”: -90, “elevation”: 8, “distance”: 2.6, “target”: [0, 0, 0.6]}, “tabs”: [“state”, “bodies”, “ctrl”, “sensors”, “sizes”]}
Try this sequence and watch the **Bodies** tab, which shows world positions (`xpos`):
1. Press **qpos[0] += 0.1 (no forward)**. The cart's `qpos` changes. Its world position in `xpos` does not, and neither does the 3D view.
2. Press **mj_forward**. Now `xpos` updates, and so does the picture.
3. Press **mj_step** a few times. `time` advances, `qpos` and `qvel` change, and so does everything computed from them.
4. Open **Model sizes**. Nothing there ever turns amber: those are `mjModel` fields.
The first two steps are the most important ten seconds of this lesson. Writing to the state does not recompute anything. The derived quantities in `mjData` are only as fresh as the last call that computed them.
## What is in each object
> [!established] mjModel: description and constants
> Sizes (`nq`, `nv`, `nu`, `nbody`, `ngeom`, ...), options (`opt.timestep`, `opt.gravity`, `opt.integrator`, ...), structure (`body_parentid`, `jnt_type`, `jnt_qposadr`, `geom_bodyid`, ...), physical parameters (`body_mass`, `body_inertia`, `geom_size`, `geom_friction`, `dof_damping`, `actuator_gainprm`, ...), the reference configuration `qpos0` and the keyframes (`key_qpos`, ...). MuJoCo also stores here constants it derived at compile time, such as `body_subtreemass`.
> [!established] mjData: state, inputs, and everything computed
> MuJoCo's documentation defines the **physics state** as `qpos`, `qvel`, `act` (activations of actuators with internal dynamics) and `history` (buffers for delayed controls and sensors); with `time` and plugin state it is the **full physics state**. The **user inputs** are `ctrl`, `qfrc_applied`, `xfrc_applied`, `mocap_pos`, `mocap_quat`, `eq_active` and `userdata`. Everything else is computed: `xpos`, `xquat`, `xmat` (body poses), `geom_xpos`, `site_xpos`, `contact` and `ncon`, `qacc`, `qfrc_bias`, `actuator_force`, `sensordata`, `energy`, and the constraint solver's internals (`efc_*`).
Here is the same separation shown from Python, plus the operation that most often goes wrong, saving and restoring state.
```io
INPUT: `cartpole.xml` and `cube_table.xml`
PROCESS: (1) step and check which object changed; (2) write qpos and read a derived position before and after `mj_forward`; (3) save state mid-contact with two specifications and replay
OUTPUT: printed checks, and the replay error for each kind of saved state
```python file=examples/l1_1_model_and_data.py “"”Lesson 1.1: what lives in mjModel, what lives in mjData, and how to save state.
INPUT cartpole.xml and cube_table.xml PROCESS (1) print model sizes and show that mj_step writes data, never model; (2) show that derived quantities are stale until mj_forward; (3) save state at t = 0.5 s with two state specifications, restore each into a fresh MjData, and compare the replay with the original run OUTPUT printed checks; the replay comparison shows why the integration state, not just qpos and qvel, is needed for bit-exact reproduction
Run: python examples/l1_1_model_and_data.py “””
import mujoco import numpy as np
from mjcourse import model_path
def sizes_and_ownership() -> None: model = mujoco.MjModel.from_xml_path(str(model_path(“cartpole”))) data = mujoco.MjData(model) print(f”cartpole: nq={model.nq} nv={model.nv} nu={model.nu} nbody={model.nbody} “ f”timestep={model.opt.timestep} s”) mass_before = model.body_mass.copy() qpos_before = data.qpos.copy() data.qpos[1] = 0.2 # tilt the pole for _ in range(100): mujoco.mj_step(model, data) print(“mj_step changed model.body_mass:”, not np.array_equal(mass_before, model.body_mass)) print(“mj_step changed data.qpos: “, not np.array_equal(qpos_before, data.qpos))
def stale_until_forward() -> None: model = mujoco.MjModel.from_xml_path(str(model_path(“cartpole”))) data = mujoco.MjData(model) mujoco.mj_forward(model, data) tip = model.site(“pole_tip”).id before = data.site_xpos[tip].copy() data.qpos[0] = 0.5 # move the cart by hand print(“tip x after writing qpos, no forward: “, round(data.site_xpos[tip][0], 4), “(stale)”) mujoco.mj_forward(model, data) print(“tip x after mj_forward: “, round(data.site_xpos[tip][0], 4), f”(moved {data.site_xpos[tip][0] - before[0]:.4f} m)”)
def save_and_replay() -> None: model = mujoco.MjModel.from_xml_path(str(model_path(“cube_table”))) data = mujoco.MjData(model) for _ in range(250): # 0.5 s: the dropped cube is in contact by now mujoco.mj_step(model, data) print(f”state saved at t = {data.time:.3f} s with {data.ncon} active contacts”) specs = {“physics (qpos, qvel, act, history)”: mujoco.mjtState.mjSTATE_PHYSICS, “integration (all forward-dynamics inputs)”: mujoco.mjtState.mjSTATE_INTEGRATION} saved = {} for name, spec in specs.items(): buf = np.empty(mujoco.mj_stateSize(model, spec)) mujoco.mj_getState(model, data, buf, spec) saved[name] = (spec, buf) reference = [] for _ in range(500): mujoco.mj_step(model, data) reference.append(data.qpos.copy()) for name, (spec, buf) in saved.items(): replay = mujoco.MjData(model) mujoco.mj_setState(model, replay, buf, spec) worst = 0.0 for k in range(500): mujoco.mj_step(model, replay) worst = max(worst, float(np.max(np.abs(replay.qpos - reference[k])))) print(f”replay from {name:<42s} max |qpos difference| over 1 s = {worst:.3e}”)
if name == “main”: sizes_and_ownership() stale_until_forward() save_and_replay()
Output with MuJoCo 3.14.0:
```text
cartpole: nq=2 nv=2 nu=1 nbody=3 timestep=0.002 s
mj_step changed model.body_mass: False
mj_step changed data.qpos: True
tip x after writing qpos, no forward: 0.0 (stale)
tip x after mj_forward: 0.5 (moved 0.5000 m)
state saved at t = 0.500 s with 8 active contacts
replay from physics (qpos, qvel, act, history) max |qpos difference| over 1 s = 7.281e-14
replay from integration (all forward-dynamics inputs) max |qpos difference| over 1 s = 0.000e+00
The last two lines deserve a second look. Restoring only qpos and qvel gives a replay that is almost identical, off by $10^{-13}$. Restoring the integration state gives an identical one. The difference is the constraint solver’s warm start (qacc_warmstart), which the integration state includes: the solver starts from the previous step’s answer, and a different starting point converges to the same solution only up to rounding.
[!established] Which state to save MuJoCo’s
mjtStateenum names the groups.mjSTATE_PHYSICSis what changes over time;mjSTATE_FULLPHYSICSaddstimeand plugin state;mjSTATE_INTEGRATIONis the union of everything that is an input to forward dynamics, including controls, applied forces, mocap poses and warm starts. The documentation states that twomjDatawith the same integration state produce identical pipeline outputs. Usemj_stateSize,mj_getStateandmj_setStatewith the group you need.
[!research] Why $10^{-13}$ matters In a chaotic system (a stack of blocks, a hand juggling an object, a double pendulum), a $10^{-13}$ difference grows exponentially and two “identical” rollouts separate within seconds. If your evaluation restores episodes from saved states, save the integration state, or accept that replays are statistical, not exact.
Seen from far away, MuJoCo computes one function. Write $x$ for the state, $u$ for the inputs and $h$ for the timestep. The physics model is defined in continuous time,
\[\dot x = f(t, x, u),\]and mj_forward evaluates the expensive part of $f$: given positions and velocities it computes accelerations. mj_step then advances the state by one timestep with the chosen integrator, $x_{t+h} = \Phi_h(x_t, u_t)$. Lesson 1.3 opens up $\Phi_h$; Level 7 opens up $f$.
The split between mjModel and mjData maps onto this: the parameters of $f$ and $\Phi_h$ live in mjModel, the arguments $x$ and $u$ and the intermediate values of the computation live in mjData.
“The simulation never writes mjModel” does not mean you cannot. Domain randomization, system identification and many labs in this course change model parameters between or during rollouts. The documentation gives the rule.
[!established] What is safe to change Real-valued parameters are generally safe to change (friction, damping, gains, gravity, timestep); structural integers are not (joint types, address arrays, counts), because they decide sizes and indexing. Some real-valued fields feed constants MuJoCo precomputes at compile time and are “safe with
mj_setConst”: among thembody_mass,body_inertia,body_ipos,body_iquat,body_pos,body_quatanddof_armature. Callmujoco.mj_setConst(model, data)after changing them. Changingbody_posorbody_quatof a static body is unsafe even withmj_setConst, because it invalidates precomputed collision structures; move static things as mocap bodies instead.
That last rule shaped this course’s incline lab: the ramp is a mocap body, because tilting an ordinary static body by rewriting model.body_quat is documented as unsafe.
Two mjData, one mjModel. mujoco.MjData(model) allocates a fresh, independent instance. Ten of them share one model’s memory for the description and each has its own state. This is the basis of parallel rollouts (Level 20.3).
Named access returns views. data.body("cart").xpos is a NumPy view into mjData memory. It changes when the simulation steps. If you append it to a list for logging, you append the same memory every time and your log contains the last value repeated. Call .copy(). Lesson 3.1 is about this trap.
mjData memory is preallocated. MuJoCo allocates mjData once and does not touch the heap during simulation. Contacts, constraint arrays and scratch (“stack”) arrays share one arena whose size you can set in MJCF (<size memory="16M"/>; the default lets the compiler guess). The documentation spells out what happens when it runs out: extra contacts are dropped for that step with a warning, a shortage of constraint memory disables the constraint solver for that step with a warning, and a shortage of stack memory is a hard error. A crowded scene that “sometimes ignores contacts” may simply need more memory.
“I set the robot to a new pose but the camera image and the end-effector position did not change.” You wrote qpos and read derived quantities without mj_forward. Call it after any manual state change.
“My domain randomization changed the mass but the dynamics did not change as expected.” If you changed body_mass (or inertia, or armature) without mj_setConst, some precomputed constants still describe the old mass. Call mj_setConst after the change.
“Two runs from the same saved state diverge after a few seconds.” You restored the physics state, not the integration state. The warm start differs, the solver’s answer differs in the last bits, and a contact-rich system amplifies it.
Load pick_place.xml, reset to keyframe 1 (home), and write a function snapshot(model, data) -> dict that returns copies of time, qpos, qvel, the world position of every body and the number of contacts. Call it before and after mj_step, and before and after mj_forward on an unchanged state. Which fields does mj_forward change on an unchanged state, and why?
Implement “time travel” for the cube-table scene: record the integration state every 0.1 s for 3 s, then restore the state at a time the user picks and step forward again. Verify with an assertion that the re-simulated trajectory is bit-identical to the original. Then deliberately record only the physics state and measure how many seconds it takes for the two trajectories to differ by more than 1 mm.
Simulation-based evaluation often restores episodes from logged states (to replay failures, to branch off counterfactual actions, to evaluate world models against ground truth from the same state). All of these need to know what “the same state” means. The precise answer is the integration state; a looser answer is acceptable only if you show the results do not depend on it.
{"id": "1.1-check", "title": "Knowledge check", "questions": [
{"kind": "mcq", "q": "Which of these does <code>mj_step</code> write?",
"options": ["<code>model.opt.timestep</code>", "<code>model.body_mass</code>", "<code>data.qacc</code>", "<code>model.nq</code>"],
"answer": 2,
"explain": "<p>Only <code>mjData</code> fields change during simulation. <code>qacc</code> is computed by forward dynamics in every step.</p>"},
{"kind": "predict", "q": "In the cart-pole, you set <code>data.qpos[1] = 0.3</code> (tilt the pole) and immediately read <code>data.sensordata</code>, whose second entry is the pole-angle sensor. What do you read?",
"options": ["0.3", "The previous pole angle", "Zero", "An error, because the sensor needs a step first"],
"answer": 1,
"explain": "<p><code>sensordata</code> is computed by the pipeline (position sensors in the position stage). It keeps its old value until <code>mj_forward</code> or <code>mj_step</code> runs. Check it in the inspector: change qpos without forward and look at the Sensors tab.</p>"},
{"kind": "mcq", "q": "You change <code>model.body_mass</code> of a link for domain randomization. What else should you do?",
"options": ["Nothing; masses are read fresh every step", "Recompile the XML", "Call <code>mujoco.mj_setConst(model, data)</code>", "Call <code>mj_resetData</code>"],
"answer": 2,
"explain": "<p>The documentation lists <code>body_mass</code> as safe to change <em>with</em> <code>mj_setConst</code>, which recomputes the constants derived from it. Recompiling also works but is much slower and throws away state.</p>"},
{"kind": "open", "q": "Design a test that would catch a logging bug where every logged body position is identical to the last one.",
"reference": "<p>Log positions over a rollout in which the body is known to move (for example a falling ball), then assert that the logged array is not constant: <code>assert np.ptp(log[:, 2]) > 0.1</code>. A sharper test also checks that the logged values match a recomputation: restore the saved state at a few logged times, call <code>mj_forward</code>, and compare. The bug comes from appending views; the fix is <code>.copy()</code>.</p>"}
]}
Lesson 1.2 explains the layout of qpos and qvel: why a free body has seven position numbers and six velocity numbers, and why subtracting two qpos vectors does not give you a velocity.