MuJoCo basics
Let go of a ball. It falls faster, hits the floor, and bounces back up, a little lower each time.
How do we ask Python to produce that motion?
MuJoCo is a physics simulator: we describe the objects and their surroundings, give them a starting situation, and let it calculate what happens next.
Overview
Our ball starts one metre above a floor, at rest. After one tiny step it is a little lower and moving downward; after many steps it reaches the floor and bounces.
We supply the scene and its starting numbers. MuJoCo calculates the motion using physical rules.
Unlike the PyTorch lesson, no rule is being learned here. We are advancing a physical simulation.
The useful mental model is one world description, one changing state.
This is also the exact ball that the bouncing-ball workshop later teaches a network to predict.
World
First describe what exists:
- the ball: radius
0.05 m, mass0.1 kg; - the floor: a flat surface at height zero;
- gravity, pulling downward.
We restrict the ball to vertical movement, so there is only one position and one velocity to follow.
The ball touches the floor when its centre is lower than its radius, 0.05 m.
MuJoCo describes a scene in an XML format called MJCF. Here is ours, from the example’s make_model():
CONTACT = 'solref="-100000 -20" solimp="0.99 0.99 0.001"' # a stiff, lightly damped spring
xml = f"""
<mujoco model="one bouncing ball">
<option timestep="{TIME_STEP}" gravity="0 0 -{gravity}"/>
<default>
<geom {contact}/>
</default>
<worldbody>
<geom name="floor" type="plane" size="2 2 0.1"/>
<body name="ball">
<joint name="up_down" type="slide" axis="0 0 1"/>
<geom type="sphere" size="{RADIUS}" mass="0.1"/>
</body>
</worldbody>
</mujoco>
"""
model = mujoco.MjModel.from_xml_string(xml)Read it from the outside in:
- A
bodyis the physical object. - Its
geomgives it a shape, used for collision and drawing. - The
jointsays how the body can move:type="slide"withaxis="0 0 1"allows movement along the vertical axis only. - The floor is a
geomwith no body, so nothing moves it. - In
option, the negative gravity component points down, andtimestep=0.001advances the clock by one millisecond per step. - The
defaultblock gives every geom our contact settings,CONTACT. The Contact section below explains them.
from_xml_string(xml) compiles that description into an MjModel: the object MuJoCo uses for the world’s shapes, joints and settings.
Why XML rather than Python? MuJoCo’s engine is written in C, and an XML file is a world description that any language can load and anyone can share. (Python can also build one in code with mujoco.MjSpec.)
Model and data
The same floor and gravity can support many different drops. Starting the ball at one metre or at sixty centimetres changes the starting situation, not the room.
MuJoCo keeps that situation in a separate object, MjData.
MjData(model) give two independent states for the same world.Stepping one does not advance the other.
In code, one state for our drop:
data = mujoco.MjData(model)
data.qpos[0] = 1.0 # centre height, in metres
data.qvel[0] = 0.0 # vertical velocity: at rest
mujoco.mj_forward(model, data) # prepare this state; time stays 0data.qpos stores joint positions and data.qvel stores joint velocities. Our one vertical joint has one entry in each, so qpos[0] is the centre height and qvel[0] is its vertical velocity: negative means moving down, positive means moving up.
mj_forward(model, data) calculates what follows from that state, such as where the shapes are and whether they touch. It prepares the state without advancing time.
One step
Advancing time is one call. Then we read the new state:
mujoco.mj_step(model, data)
print(f"{data.time:.3f} {data.qpos[0]:.8f} {data.qvel[0]:.5f}") # 0.001 0.99999019 -0.00981mj_step uses the world and the current state to calculate the motion, advances time by 0.001 s, and updates the same data object. It returns nothing: you read the result from data.
Gravity changes velocity by \(-9.81 \times 0.001 = -0.00981\ \text{m/s}\).
This example’s default integrator uses that new velocity to move the ball by \(-0.00981 \times 0.001 = -0.00000981\ \text{m}\). That is why the height becomes 0.99999019 m.
The first move is tiny. Repeated steps build up downward velocity, and larger moves.
The floor turns the motion round in a sharp corner, and the ball climbs back almost as high.
Calling mj_step repeatedly is the simulation loop.
Every call adds the same −0.00981 m/s to the velocity. After 100 calls (0.1 s) it is −0.981 m/s, a hundred times the first change, and the height is about 0.9505 m.
In the player below you will see both:
- velocity grows by the same amount each step, so it is a straight line;
- height changes a little more each step, so its curve bends.
Contact
If gravity were the only influence, the ball would fall straight through the floor.
MuJoCo checks which shapes touch (data.ncon counts the contacts it found) and pushes them apart.
That push works like a very stiff spring: the deeper the ball sinks into the floor, the harder the floor pushes back. A damper takes away a little speed.
solrefsets that spring. InCONTACT, negative numbers mean “use these exact values”: stiffness100000and damping20.solimpmakes the spring work at full strength from the first millimetre of overlap.
Here is the first bounce close up, one dot per 1 ms step:
The bounce is not an instant flip: over 10 steps the spring compresses, stops the fall, and throws the ball back up.
The first bounce, step by step:
- The ball touches the floor at
0.440 s, falling at4.32 m/s. - Over the next 10 steps (10 ms) the spring compresses
12.8 mm, stops the fall, and pushes the ball back out. - It leaves at
3.90 m/s: 90% of the speed it arrived with.
The damper took the other 10%. That is why the ball climbs back to 0.82 m, not 1 m, and why every bounce is lower than the last.
Without our setting, MuJoCo uses its default contact: a soft, heavily damped spring, built to stop things that touch rather than bounce them. The same ball just stops on the floor; try it in the player below.
The timestep matters too. The bounce lasts 10 ms, so the spring needs several steps inside it.
With 10 ms steps the whole bounce would fit in one step, and a spring this stiff would then add energy instead of removing it.
Try a simulation
This player shows saved output from the actual MuJoCo example. The ball, plots and readouts all follow the same recorded step.
Press One step to check the worked calculation, or play the drop: it slows to one step per frame through each bounce.
Swipe a wide diagram sideways to read all its labels.
Predict: will velocity be positive or negative after one step?
Change the drop
The dotted reference is the original drop: gravity 9.81 m/s², starting height 1 m, our spring. MuJoCo’s default contact is recorded for that drop only.
Simulation loop
The shape of every MuJoCo program:
- the world is created once;
- the starting state is set once;
- the loop repeatedly advances that state and reads the resulting numbers.
Here is the core of run_drop():
model = make_model(gravity, contact)
data = mujoco.MjData(model)
data.qpos[0] = start_height
data.qvel[0] = 0.0
mujoco.mj_forward(model, data)
rows = [(data.time, data.qpos[0], data.qvel[0], data.ncon > 0)]
for _ in range(STEPS):
mujoco.mj_step(model, data)
mujoco.mj_forward(model, data) # refresh contacts for the new position
rows.append((data.time, data.qpos[0], data.qvel[0], data.ncon > 0))
return rowsFull file · mujoco_first_steps.py · 55 lines
"""A tiny MuJoCo world: one bouncing ball constrained to vertical motion.
Run: python3 examples/mujoco_first_steps.py
The bouncing-ball workshop uses this same ball, contact, and timestep.
"""
import mujoco
TIME_STEP = 0.001 # seconds per simulation step
STEPS = 1500 # 1.5 s of simulated time
RADIUS = 0.05 # metres
CONTACT = 'solref="-100000 -20" solimp="0.99 0.99 0.001"' # a stiff, lightly damped spring
def make_model(gravity=9.81, contact=CONTACT):
xml = f"""
<mujoco model="one bouncing ball">
<option timestep="{TIME_STEP}" gravity="0 0 -{gravity}"/>
<default>
<geom {contact}/>
</default>
<worldbody>
<geom name="floor" type="plane" size="2 2 0.1"/>
<body name="ball">
<joint name="up_down" type="slide" axis="0 0 1"/>
<geom type="sphere" size="{RADIUS}" mass="0.1"/>
</body>
</worldbody>
</mujoco>
"""
return mujoco.MjModel.from_xml_string(xml)
def run_drop(gravity=9.81, start_height=1.0, contact=CONTACT):
model = make_model(gravity, contact) # The world's rules: gravity, shapes, contact, timestep.
data = mujoco.MjData(model) # One changing state of that world.
data.qpos[0] = start_height
data.qvel[0] = 0.0
mujoco.mj_forward(model, data) # Recalculate from this start; time does not advance.
rows = [(data.time, data.qpos[0], data.qvel[0], data.ncon > 0)]
for _ in range(STEPS):
mujoco.mj_step(model, data) # Advance time and update data in place.
mujoco.mj_forward(model, data) # Refresh contacts for the new position.
rows.append((data.time, data.qpos[0], data.qvel[0], data.ncon > 0))
return rows
if __name__ == "__main__":
rows = run_drop()
for step in (0, 1, 100, 400, 430, 445, 460, 1500):
time, height, velocity, contact = rows[step]
print(f"step {step:4d}: time {time:.3f} s height {height:.5f} m "
f"velocity {velocity:+.4f} m/s contact {contact}")
The first row is the starting state, and each step adds one more: 1,501 rows, the start plus 1,500 steps.
The extra mj_forward matters for the contact flag. mj_step finds contacts before it moves the ball, so without it ncon would describe the previous position, and contact would be reported one step late.
- The timestep lives in
TIME_STEP = 0.001and is passed into the XML’soption. STEPS = 1500controls how many updates we take: \(1500 \times 0.001\ \text{s} = 1.5\ \text{s}\) of simulated time.- A viewer only draws the state; it is not needed to calculate these numbers.
Recap
make_model(gravity, contact)describes the ball, floor, joint, gravity, contact spring and timestep in XML, then compiles them into a model.data = mujoco.MjData(model)creates the state storage for one run in that world.data.qpos[0] = start_heightsupplies the starting height.data.qvel[0] = 0.0starts the ball at rest.mujoco.mj_forward(model, data)prepares derived quantities for the assigned state without advancing time.rows.append(...)copies the current time, position, velocity, and contact flag into the recording, so we can inspect earlier states later.mujoco.mj_step(model, data)uses the physical rules, including the contact spring, to advance time by 1 ms and update the state. The loop repeats this call 1,500 times.mujoco.mj_forward(model, data)after each step refreshes contacts for the new position, so the recorded contact flag matches the recorded height.data.time,data.qpos, anddata.qvelgive the current clock, joint positions, and joint velocities.data.ncongives the computed contact count.
Describe the world, supply the start, then step and observe. That short sequence produces the entire drop, including the bounce.
Key concepts
- MJCF
- MuJoCo’s XML format for describing a physical scene.
- Body / geom
- The physical object / its shape for collision and drawing.
- Joint
- An allowed movement of a body. This sphere has one vertical slide joint.
- MjModel
- The compiled world description, including shapes, joints, gravity, and simulation settings.
- MjData
- One run’s changing state and calculated quantities.
- qpos / qvel
- Joint positions / joint velocities. Here their only entries are height and vertical velocity.
- Timestep
- The physical time advanced by one simulation step; 0.001 seconds in this example.
- mj_forward
- Calculate derived quantities from the assigned state without advancing time.
- mj_step
- Advance the simulation by one timestep and update data in place.
- Contact
- Two shapes touching. MuJoCo pushes them apart like a stiff spring with a damper;
solrefsets the spring.nconis the computed count.
All plotted states are recorded by the local MuJoCo example. References: model and data, simulation calls, and MJCF scene description. The lesson works offline; Colab and reference links need an internet connection.