A physics simulator is a function that takes the state of a system at one instant and returns its state a short time later. Everything else you will see in this course, the 3D view, the sensor readings, the camera images, the rewards a learning agent receives, is computed from that state. If you hold on to one idea from Level 0, hold on to this one: the state is the simulation.
For a rigid-body simulator such as MuJoCo the state is made of positions and velocities. Positions say where every part of the system is; velocities say how fast those positions are changing. Given the state, the simulator works out the forces (gravity, motors, contact between bodies, joint friction) and from the forces the accelerations. Then it takes a small step forward in time.
That step forward is called integration, and the length of the step is the timestep, written $h$. MuJoCo’s default timestep is 2 ms, so one simulated second takes 500 steps. Integration is where the first approximation enters: the real world does not move in 2 ms jumps, and the simulator’s answer depends on $h$.
[!established] MuJoCo’s default timestep and integrator In MuJoCo 3.14.0 the
timestepoption defaults to 0.002 s and theintegratoroption defaults toEuler, which the documentation describes as the semi-implicit Euler method. The documentation calls the timestep “the single most important parameter affecting the speed-accuracy trade-off”. Both are set in the<option>element of a model.
The lab beside this text (below it on a narrow screen) drops two balls from the same height. The orange one has a mass of 2 kg, the blue one 0.1 kg. Before you press Play, decide which one reaches the floor first.
```lab simlab {“dock”: true, “title”: “Two balls, one gravity”, “model”: “ball_drop”, “height”: 280, “camera”: {“azimuth”: -90, “elevation”: 6, “distance”: 2.3, “target”: [0, 0, 0.5]}, “controls”: [ {“label”: “gravity, z component”, “unit”: “m/s^2”, “set”: “model.opt.gravity[2]”, “min”: -20, “max”: 0, “step”: 0.01, “value”: -9.81, “digits”: 2}, {“label”: “mass of the blue ball”, “unit”: “kg”, “apply”: “model.body_mass[2] = value; mj.mj_setConst(model, data)”, “min”: 0.01, “max”: 5, “step”: 0.01, “value”: 0.1, “digits”: 2}, {“label”: “timestep h”, “unit”: “s”, “set”: “model.opt.timestep”, “min”: 0.0005, “max”: 0.02, “step”: 0.0005, “value”: 0.002, “digits”: 4} ], “plots”: [{“ylabel”: “centre height (m)”, “window”: 1.5, “ymin”: 0, “ymax”: 1.05, “traces”: [ {“label”: “orange, 2 kg”, “expr”: “data.qpos[2]”}, {“label”: “blue”, “expr”: “data.qpos[9]”, “dash”: true}]}], “readouts”: [ {“label”: “time (s)”, “expr”: “data.time”}, {“label”: “orange z (m)”, “expr”: “data.qpos[2]”, “digits”: 4}, {“label”: “blue z (m)”, “expr”: “data.qpos[9]”, “digits”: 4}, {“label”: “contacts”, “expr”: “data.ncon”, “digits”: 0}], “note”: “Reset, change one slider, play again. The plot shows the height of each ball’s centre; the balls have a radius of 0.05 m, so they touch the floor when the centre reaches 0.05 m.”}
Change the blue ball's mass from 0.1 kg to 5 kg and drop them again. Then halve gravity. Then set the timestep to 20 ms. Each change answers a question you could also answer on paper, which is the habit this course is trying to build: **predict, then simulate, then explain the difference**.
```quiz
{"id": "0.1-predict", "title": "Predict before you run", "questions": [
{"kind": "predict", "q": "Both balls start at rest with their centres at 1.0 m. The orange ball is 20 times heavier. Which one touches the floor first in MuJoCo?",
"options": ["The heavy ball, clearly earlier", "The light ball, because it has less inertia", "Both at the same time", "It depends on the timestep"],
"answer": 2,
"explain": "<p>With gravity as the only force, the acceleration of each ball is $g$ regardless of its mass: the force $mg$ is divided by the same $m$ in Newton's second law. MuJoCo adds no air drag unless the model sets a fluid <code>density</code> or <code>viscosity</code> in <code><option></code>, and both default to zero. The timestep changes when contact is <em>detected</em>, but it changes it equally for both balls.</p>",
"sim": {"model": "ball_drop", "height": 220, "camera": {"azimuth": -90, "elevation": 6, "distance": 2.3, "target": [0, 0, 0.5]},
"plots": [{"ylabel": "centre height (m)", "window": 1.2, "traces": [{"label": "heavy", "expr": "data.qpos[2]"}, {"label": "light", "expr": "data.qpos[9]", "dash": true}]}]}},
{"kind": "predict", "q": "Now the timestep goes from 2 ms to 20 ms, ten times larger. What happens?",
"options": ["Nothing measurable changes", "Touchdown is detected later, the ball sinks deeper into the floor at rest, but it does not fall through", "The ball tunnels through the floor", "MuJoCo refuses to run"],
"answer": 1,
"explain": "<p>In this model, contact first appears in the step ending at 0.442 s with $h$ = 2 ms and at 0.460 s with $h$ = 20 ms, because contact is only checked once per step. At rest the ball sits 0.37 mm into the floor at 2 ms and 0.83 mm at 20 ms. The second number grows because MuJoCo, by default, will not let a contact's time constant be smaller than twice the timestep (the <code>refsafe</code> flag), so a larger step means a softer contact. The floor is a plane, an infinite half-space, so there is nothing to tunnel through; a thin box would be a different story (Level 9.4).</p>",
"sim": {"model": "ball_drop", "height": 220, "camera": {"azimuth": -90, "elevation": 6, "distance": 2.3, "target": [0, 0, 0.5]}, "setup": "model.opt.timestep = 0.02",
"readouts": [{"label": "time (s)", "expr": "data.time"}, {"label": "centre z (m)", "expr": "data.qpos[2]", "digits": 5}]}}
]}
Take one ball and ignore the floor. Write $z$ for the height of its centre and $v = \dot z$ for its vertical velocity. Newton’s second law with gravity as the only force is
\[m \ddot z = -m g \quad\Longrightarrow\quad \ddot z = -g,\]where $m$ is the mass in kg and $g$ = 9.81 m/s² is the magnitude of gravitational acceleration. The mass cancels, which is the whole answer to the first quiz question. Integrating twice from rest at height $z_0$ gives the exact solution
\[z(t) = z_0 - \tfrac{1}{2} g t^2, \qquad v(t) = -g t.\]The lowest point of a ball of radius $r$ = 0.05 m released with its centre at $z_0$ = 1.0 m falls $d = z_0 - r = 0.95$ m before touching the floor, which takes $t^\ast = \sqrt{2d/g} = 0.4401$ s and ends at speed $\sqrt{2gd} = 4.317$ m/s.
A simulator does not have the exact solution in general, so it approximates it step by step. MuJoCo’s default integrator updates the velocity first and then the position with the new velocity:
\[v_{k+1} = v_k + h\, a_k, \qquad z_{k+1} = z_k + h\, v_{k+1}.\][!derivation] The error of semi-implicit Euler for a falling body With constant acceleration $a_k = -g$ and $v_0 = 0$, the velocity after $k$ steps is $v_k = -g h k$, which is exact. The position is $z_k = z_0 - g h^2 \sum_{j=1}^{k} j = z_0 - \tfrac12 g h^2 k(k+1)$. Writing $t_k = kh$, \(z_k = \underbrace{z_0 - \tfrac12 g t_k^2}_{\text{exact}} \; - \; \tfrac12 g h\, t_k .\) The simulated ball is always below the exact one, by $\tfrac12 g h t$: the error grows linearly with time and linearly with the timestep. Halving $h$ halves the error. That is what “first-order accurate” means.
The prediction is checkable, and it should be checked rather than believed. The script below steps a ball with no floor (free_fall.xml) to $t$ = 0.3 s at five timesteps and prints the measured error next to $-\tfrac12 g h t$.
INPUT: `free_fall.xml` (one free ball, gravity only) and five timesteps
PROCESS: step to t = 0.3 s with `mujoco.mj_step`, compare `qpos[2]` with the exact solution
OUTPUT: measured and predicted position error, in millimetres
```python file=examples/l0_1_free_fall.py “"”Lesson 0.1: measure MuJoCo’s integration error for a falling ball.
INPUT free_fall.xml (one ball, gravity only) and a list of timesteps PROCESS step each model to t = 0.3 s and compare z with z0 - g t^2 / 2 OUTPUT a table of position error per timestep, next to the error predicted for the semi-implicit Euler integrator: -g h t / 2
Run: python examples/l0_1_free_fall.py “””
import mujoco
from mjcourse import model_path
G, Z0, T_END = 9.81, 1.0, 0.3
def error_at(timestep: float) -> tuple[float, float]: model = mujoco.MjModel.from_xml_path(str(model_path(“free_fall”))) model.opt.timestep = timestep data = mujoco.MjData(model) for _ in range(round(T_END / timestep)): mujoco.mj_step(model, data) exact = Z0 - 0.5 * G * data.time**2 measured = data.qpos[2] - exact # qpos = [x, y, z, qw, qx, qy, qz] predicted = -0.5 * G * timestep * data.time return measured, predicted
if name == “main”: print(f”{‘timestep (s)’:>12} {‘error (mm)’:>11} {‘-g h t / 2 (mm)’:>16}”) for h in (0.0005, 0.001, 0.002, 0.005, 0.01): measured, predicted = error_at(h) print(f”{h:>12.4f} {1e3 * measured:>11.4f} {1e3 * predicted:>16.4f}”)
Output with MuJoCo 3.14.0:
| timestep (s) | error (mm) | $-\tfrac12 g h t$ (mm) |
|---:|---:|---:|
| 0.0005 | -0.7357 | -0.7358 |
| 0.0010 | -1.4715 | -1.4715 |
| 0.0020 | -2.9430 | -2.9430 |
| 0.0050 | -7.3575 | -7.3575 |
| 0.0100 | -14.7150 | -14.7150 |
The match is exact to the printed precision, which tells you two things at once: the derivation is right, and MuJoCo's `Euler` integrator really is the semi-implicit scheme written above. Watch the same error grow live in the lab below: it plots simulated height minus exact height while the ball falls.
```lab simlab
{"title": "Integration error, live", "model": "free_fall", "height": 200, "autoplay": false, "stopAt": 0.6,
"camera": {"azimuth": -90, "elevation": 0, "distance": 2.6, "target": [0, 0, 0]},
"controls": [{"label": "timestep h", "unit": "s", "set": "model.opt.timestep", "min": 0.0005, "max": 0.02, "step": 0.0005, "value": 0.002, "digits": 4, "reset": true}],
"plots": [{"ylabel": "z simulated - z exact (mm)", "window": 0.6, "traces": [
{"label": "measured error (mm)", "expr": "1000 * (data.qpos[2] - (1 - 0.5 * 9.81 * data.time * data.time))"},
{"label": "-g h t / 2 (mm)", "expr": "-1000 * 0.5 * 9.81 * model.opt.timestep * data.time", "dash": true}]}],
"readouts": [{"label": "time (s)", "expr": "data.time"}, {"label": "error (mm)", "expr": "1000 * (data.qpos[2] - (1 - 0.5 * 9.81 * data.time * data.time))", "digits": 4}],
"note": "The ball has no floor to hit, so the lab pauses itself at t = 0.6 s. Changing the timestep restarts the fall."}
Here is the whole MuJoCo program for the two-ball experiment. It loads the model, steps until each ball first touches the floor, and then lets both settle so you can read how far they sink.
INPUT: `ball_drop.xml`: a 2.0 kg and a 0.1 kg ball, centres released at z = 1.0 m
PROCESS: `mj_step` until a contact with the floor appears for each ball, then 3 s more
OUTPUT: touchdown time and speed per ball, the analytic values, and the resting penetration
```python file=examples/l0_1_ball_drop.py “"”Lesson 0.1: drop two balls of different mass and time their fall.
INPUT ball_drop.xml: a 2.0 kg and a 0.1 kg ball, centres released at z = 1.0 m PROCESS step until each ball’s first contact with the floor, then let them settle OUTPUT touchdown time and speed per ball, the analytic values for comparison, and the resting penetration depth of each ball into the floor
Run: python examples/l0_1_ball_drop.py “””
import math
import mujoco
from mjcourse import model_path
G, DROP = 9.81, 0.95 # the ball’s lowest point falls 1.0 - 0.05 m before touching
def main() -> None: model = mujoco.MjModel.from_xml_path(str(model_path(“ball_drop”))) data = mujoco.MjData(model) floor = model.geom(“floor”).id touchdown = {} while len(touchdown) < 2: speeds = {name: -data.joint(f”{name}_free”).qvel[2] for name in (“heavy”, “light”)} mujoco.mj_step(model, data) for c in data.contact[: data.ncon]: if floor in (c.geom1, c.geom2): other = c.geom2 if c.geom1 == floor else c.geom1 name = model.geom(other).name.removesuffix(“_geom”) # Report the time and speed at the start of the step in which contact appeared. touchdown.setdefault(name, (data.time - model.opt.timestep, speeds[name])) t_exact, v_exact = math.sqrt(2 * DROP / G), math.sqrt(2 * G * DROP) print(f”analytic: t = {t_exact:.4f} s, v = {v_exact:.3f} m/s”) for name, (t, v) in touchdown.items(): print(f”{name:>5}: t = {t:.4f} s, v = {v:.3f} m/s”)
for _ in range(1500): # 3 s more: let both balls come to rest
mujoco.mj_step(model, data)
for name in ("heavy", "light"):
z = data.body(name).xpos[2]
print(f"{name:>5} at rest: centre z = {z:.5f} m, penetration = {1e3 * (0.05 - z):.3f} mm")
if name == “main”: main()
Output:
```text
analytic: t = 0.4401 s, v = 4.317 m/s
heavy: t = 0.4400 s, v = 4.316 m/s
light: t = 0.4400 s, v = 4.316 m/s
heavy at rest: centre z = 0.04963 m, penetration = 0.367 mm
light at rest: centre z = 0.04963 m, penetration = 0.367 mm
Three details of this program recur in every MuJoCo script you will write.
State lives in data, structure lives in model. model holds what does not change while the simulation runs (masses, sizes, the timestep). data holds the state (qpos, qvel, time) and everything MuJoCo computes from it, such as body positions (xpos) and the list of active contacts (contact, ncon). Level 1.1 takes this apart properly.
A free body has seven position numbers and six velocity numbers. Each ball’s qpos block is [x, y, z, qw, qx, qy, qz]: a 3-D position and a unit quaternion for orientation. Its qvel block is [vx, vy, vz, wx, wy, wz]. That is why the blue ball’s height is qpos[9] (the second block starts at index 7) and why nq and nv differ, a fact Level 1.2 explains.
Named access beats hand-computed indices. data.joint("light_free").qvel and data.body("light").xpos find the right slice by name. The lab above uses raw indices only so you can see the layout; your own code should use names.
[!established] Both balls rest at the same depth The heavy and the light ball both settle 0.367 mm into the floor. That is not a coincidence of this model. MuJoCo’s soft-contact parameters (
solref,solimp) define the contact’s stiffness relative to the effective mass it acts on, so with default parameters the resting penetration does not depend on the mass of the object. Level 9.2 derives this. If you assumed a fixed spring stiffness in newtons per metre, as many simulators expose, your intuition about contact in MuJoCo will be wrong in a predictable direction.
[!derivation] Why the impact sinks much deeper than the resting depth At the moment of impact the ball moves at 4.3 m/s, and the default contact behaves like a critically damped spring-damper whose time constant is $\tau$ = 0.02 s. For a critically damped system started with velocity $v_0$ and natural frequency $\omega \approx 1/\tau$, the peak displacement is about $v_0 / (\omega e) = 4.3 / (50 \times 2.718)$ = 32 mm. MuJoCo measures 29 mm for this model at $h$ = 2 ms. For a short instant the ball is more than half its radius inside the floor. You do not see it in the 3D view because it lasts tens of milliseconds, but a contact-rich manipulation task feels it in every collision.
The program above is short, and it still contains three traps that cost beginners hours.
Reading derived quantities that have not been computed. If you write data.qpos[2] = 0.5 and immediately read data.body("heavy").xpos, you get the old position. xpos is computed from qpos by forward kinematics, which runs inside mj_step and mj_forward. After you set state by hand, call mujoco.mj_forward(model, data) before reading anything derived from it.
Off-by-one-step timing. After mj_step returns, data.time has already advanced by $h$. A contact reported in data.contact was found at the start of that step, from the positions before integration. The script subtracts one timestep to report the time at which contact appeared, which is why it prints 0.4400 s, within one step of the analytic 0.4401 s.
Expecting a bounce. The balls do not bounce. MuJoCo’s default contact is critically damped (solref = “0.02 1”, damping ratio 1), so a rigid sphere stops dead. Restitution is a modelling choice you make explicitly, and contact parameters of the two geoms involved are combined (Level 9.2), so changing only one geom’s solref does not set the contact’s value.
Write a script that drops the heavy ball alone from five heights between 0.2 m and 2.0 m and plots the touchdown time against $\sqrt{2(z_0 - r)/g}$. All points should lie on the diagonal. Then repeat at $h$ = 10 ms and explain, using the derivation above, which way the points move and by how much.
[!try] Make the ball bounce Add
solref="0.02 0.2"to both the floor geom and the ball geom (an underdamped contact: damping ratio 0.2) and measure the rebound height. With the defaultsolmix, the contact uses a blend of the two geoms’ parameters, so setting both is the clean way to know which value the contact gets. Then read thesolrefdocumentation for the negative-number form, which sets stiffness and damping directly.
Find, by experiment, the largest timestep at which the heavy ball, dropped from 1 m onto the floor, still comes to rest with its centre above 0.049 m. Report the timestep, the resting penetration, and the explanation in terms of the refsafe rule (the contact time constant is never smaller than $2h$). Then predict, before measuring, what happens to that largest timestep if you change the floor’s solref time constant from 0.02 s to 0.005 s.
Every number a simulation produces is conditional on choices that are easy to leave unreported: the timestep, the integrator, the contact parameters, the MuJoCo version. The same policy evaluated at 2 ms and at 10 ms is evaluated in two different simulators. When you read a robot-learning paper that reports a success rate in a MuJoCo environment, ask whether you could reproduce the simulator, not only the policy. When you write one, report the MuJoCo version, the timestep and the integrator, and say whether any contact parameters differ from the defaults. Level 21 turns this into a checklist.
[!research] Where approximation starts to matter For a falling ball the integration error is millimetres and nobody cares. For a grasp, a 29 mm transient penetration during impact or a 0.4 mm resting penetration changes friction forces and therefore whether an object slips. The errors that matter in robotics research are rarely in free flight; they are in contact, which is why Levels 9 and 10 are the longest in this course.
{"id": "0.1-check", "title": "Knowledge check", "questions": [
{"kind": "mcq", "q": "Which of these is <em>state</em> in MuJoCo, rather than something computed from state?",
"options": ["The world position of a body, <code>data.xpos</code>", "The joint positions and velocities, <code>data.qpos</code> and <code>data.qvel</code>", "The list of contacts, <code>data.contact</code>", "The camera image"],
"answer": 1,
"explain": "<p>MuJoCo's documentation defines the <em>physics state</em> as <code>qpos</code>, <code>qvel</code>, <code>act</code> (activations of actuators with internal dynamics) and <code>history</code> (buffers for delayed controls and sensors). The simulation time joins them in what it calls the <em>full</em> physics state. Body positions, contacts and images are all derived from the state. Level 1.1 goes through the components.</p>"},
{"kind": "numeric", "q": "With semi-implicit Euler at $h$ = 5 ms, how far below the exact solution (in mm) is a freely falling body after 0.5 s? Use $g$ = 9.81 m/s².",
"answer": 12.2625, "tol": 0.05, "unit": "mm",
"explain": "<p>The error is $\\tfrac12 g h t = 0.5 \\times 9.81 \\times 0.005 \\times 0.5$ = 0.01226 m = 12.26 mm, below the exact solution.</p>"},
{"kind": "mcq", "q": "You set <code>data.qpos[2] = 0.5</code> and then read <code>data.body('heavy').xpos[2]</code> without calling anything. What do you get?",
"options": ["0.5", "The previous height, because xpos is only updated by forward kinematics", "An error", "Zero"],
"answer": 1,
"explain": "<p><code>xpos</code> is an output of forward kinematics. It is recomputed by <code>mj_forward</code> and inside <code>mj_step</code>, not when you write to <code>qpos</code>.</p>"},
{"kind": "open", "q": "A colleague reports that their grasping policy succeeds 92% of the time in MuJoCo and asks you to reproduce it. List the simulator settings you would ask for before running anything, and say why each matters.",
"reference": "<p>At least: the MuJoCo version (contact and solver behaviour changes between releases; this course pins 3.14.0); the timestep and integrator (they set integration error and, through <code>refsafe</code>, the effective contact softness); the friction cone type and <code>impratio</code> (they decide slip); any non-default <code>solref</code>/<code>solimp</code>/<code>friction</code>/<code>condim</code> on the gripper pads and objects; the solver, iterations and tolerance; and whether <code>noslip_iterations</code> is used. Then the non-simulator items: initial-state distribution, number of episodes and seeds, and the success definition.</p>"}
]}
Lesson 0.2 installs MuJoCo 3.14.0 on your machine and shows which parts of this course run in the browser and which need Python. If you already have a working install, skim it for the headless-rendering section and continue to 0.3, which is about what MuJoCo does not model.