← Foundation lessonsFoundations · lesson 4

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.

Describe the worldA ball, floor, gravity, contact.Set the startHeight 1 m, speed zero.Step and observeRead the new height and speed.

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, mass 0.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.

Ball constrained to a vertical joint above a floor. qpos is the centre height measured from the floor; gravity points down. Heightqpos[0] Gravity pulls down Ball · radius 0.05 m Vertical slide joint Floor · height 0
The position is measured at the ball’s centre.

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():

The scene inside 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 body is the physical object.
  • Its geom gives it a shape, used for collision and drawing.
  • The joint says how the body can move: type="slide" with axis="0 0 1" allows movement along the vertical axis only.
  • The floor is a geom with no body, so nothing moves it.
  • In option, the negative gravity component points down, and timestep=0.001 advances the clock by one millisecond per step.
  • The default block 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.

One model can be shared by two independent data objects, starting at heights 1 metre and 0.6 metres. MjModel · world descriptionBall, floor, contactGravity: 9.81 m/s² downwardTimestep: 0.001 s MjData · drop AHeight 1 m · speed 0 m/sTime 0 s MjData · drop BHeight 0.6 m · speed 0 m/sTime 0 s
Two calls to MjData(model) give two independent states for the same world.

Stepping one does not advance the other.

In code, one state for our drop:

Creating and starting one state
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 0

data.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:

One step, then read the state
mujoco.mj_step(model, data)
print(f"{data.time:.3f} {data.qpos[0]:.8f} {data.qvel[0]:.5f}")   # 0.001 0.99999019 -0.00981

mj_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.

Beforetime 0.000 s · height 1.00000000 m · velocity 0.00000 m/s
After one steptime 0.001 s · height 0.99999019 m · velocity −0.00981 m/s
World settingsgravity, contact and timestep stay the same

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.

Before contact the curve gets steeper: the ball travels farther during each step.

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.

  • solref sets that spring. In CONTACT, negative numbers mean “use these exact values”: stiffness 100000 and damping 20.
  • solimp makes 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:

Orange dots are steps in contact.

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 at 4.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.

Ball and state
Height over time
Velocity over time
Time0.000 s
qpos · height1.0000 m
qvel · velocity0.000 m/s
Contact present?No

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():

examples/mujoco_first_steps.py · setup and repeated stepsOpen source ↗
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 rows
Full 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.001 and is passed into the XML’s option.
  • STEPS = 1500 controls 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_height supplies the starting height. data.qvel[0] = 0.0 starts 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, and data.qvel give the current clock, joint positions, and joint velocities. data.ncon gives 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; solref sets the spring. ncon is 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.