Core concept

A simulation “explodes” when the numerical solution grows without bound while the physics it represents would not. In MuJoCo that almost always means one of three things: something in the model is stiffer than the integrator can follow at the chosen timestep; a force is computed explicitly on a body too light to take it; or the state starts somewhere the contact model must violently correct. A fourth failure looks different but has the same roots: objects passing through each other, because the contact was never seen or was too soft to stop them.

This lesson measures each one, shows how MuJoCo reports it (and how it hides it), and orders the fixes by what they cost. Earlier lessons supplied pieces: the explicit damping limit $h < 2I/c$ (Lesson 1.3), armature for light joints (Lesson 5.2), and the time-constant rule for contacts (Lesson 9.2).

[!established] How MuJoCo reports divergence When an acceleration becomes NaN, infinite or larger than a threshold, mj_step records an mjWARN_BADQACC warning in mjData.warning, resets the state to the model’s defaults, and continues, unless the autoreset flag is disabled. The warning counter (number) and the index of the offending degree of freedom (lastinfo) are the only traces.

Visual intuition

The lab fires a 1 cm ball at a 2 mm wall and reports whether the ball stopped, pushed through a contact that was detected, or tunnelled past without any contact being seen.

  1. Fire at 1 m/s: stopped. At 5 m/s: push-through. The contact was detected, but with a 20 ms time constant the constraint asks for a deceleration far too gentle to stop the ball within the wall’s thickness.
  2. Set the timestep to 0.5 ms and the time constant to 1 ms (the smallest refsafe allows at that step), and fire at 5, 20 and 100 m/s: stopped every time.
  3. Back at 2 ms with the default contact, fire at 100 m/s: tunnelled. The ball moves 20 cm per step and is never overlapping the wall at the end of any step.

```lab simlab {“dock”: true, “title”: “Stopped, pushed through, or tunnelled”, “height”: 220, “xml”: “<body name='ball' pos='0 0 0'></body>”, “camera”: {“azimuth”: -90, “elevation”: 35, “distance”: 0.7, “target”: [0.2, 0, 0]}, “setup”: “ctx.touched = false”, “controls”: [ {“label”: “speed”, “unit”: “m/s”, “param”: “v”, “min”: 0.5, “max”: 100, “step”: 0.5, “value”: 5, “digits”: 1}, {“type”: “button”, “label”: “Fire”, “run”: “mj.mj_resetData(model, data); data.qvel[0] = ctx.v; ctx.touched = false; sim.play()”}, {“label”: “contact time constant”, “unit”: “s”, “apply”: “model.geom_solref[0] = value; model.geom_solref[2] = value”, “min”: 0.001, “max”: 0.05, “step”: 0.001, “value”: 0.02, “digits”: 3}, {“type”: “select”, “label”: “timestep”, “set”: “model.opt.timestep”, “options”: [[“2 ms”, 0.002], [“0.5 ms”, 0.0005]], “value”: 0.002}], “controller”: “ctx.touched = ctx.touched || data.ncon > 0”, “readouts”: [ {“label”: “outcome”, “expr”: “data.qpos[0] < 0.25 ? (ctx.touched ? ‘stopped’ : (data.qvel[0] > 0 ? ‘on its way’ : ‘-‘)) : (ctx.touched ? ‘push-through’ : ‘tunnelled’)”}, {“label”: “contact seen”, “expr”: “ctx.touched ? ‘yes’ : ‘no’”}, {“label”: “ball x (m)”, “expr”: “data.qpos[0]”, “digits”: 3}, {“label”: “time (s)”, “expr”: “data.time”, “digits”: 3}], “stopAt”: 0.5, “note”: “The wall is at x = 0.25 m and 2 mm thick; the ball starts at x = 0 with radius 1 cm. refsafe raises time constants below twice the timestep to 2h, so 1 ms only takes effect at the 0.5 ms step.”}


## Mathematics

### Stiffness and the stability region

For a mass $m$ on a spring of stiffness $k$, $\omega = \sqrt{k/m}$. One step of each integrator multiplies the oscillation by a factor whose magnitude decides stability:

- **Semi-implicit Euler** (`Euler`), stable for $\omega h < 2$ and energy-preserving inside it. Near the limit the oscillation's position amplitude grows to $1/\sqrt{1 - (\omega h/2)^2}$ times its true value (3.2 at $\omega h$ = 1.9 in output 1) without diverging. `implicitfast` and `implicit` treat only velocity-dependent forces implicitly, so a spring has exactly the same limit under them.
- **RK4**, stable up to $\omega h = 2\sqrt2 \approx 2.83$, and numerically damped inside: at $\omega h$ = 1.41 each step multiplies the amplitude by 0.958, so the oscillation is gone within a second.
- **discrete**, implicit in position as well, never diverges, but its factor $1/\sqrt{1 + (\omega h)^2}$ damps every oscillation; even at $\omega h$ = 0.2 it removes 2% per step. Stability bought this way changes the physics.

Contacts, joint limits and servos are springs too: a contact's time constant sets its effective $\omega$, which is why `refsafe` keeps it at least $2h$.

### Push-through and tunnelling

A body approaching a contact at speed $v$ is decelerated by the constraint's reference dynamics, which act on a time scale of the time constant: it needs a penetration of the order of $v \cdot \text{timeconst}$ to stop (an estimate). If that exceeds the obstacle's thickness, it **pushes through**: the contact is detected, but its normal flips once the ball's centre passes the wall's mid-plane, and the contact then helps it out of the far side. If instead $v\,h$ exceeds the thickness plus the body's size, no step ends with the two overlapping and the contact is never seen: **tunnelling**. MuJoCo detects collisions at discrete instants; there is no sweep between them. Output (2) shows both, and that a stiffer contact at a smaller step stops even 100 m/s.

### Starting inside something

A body that starts overlapping by $\delta$ is pushed out by the constraint's reference acceleration $-k\,r$, with $k \propto 1/\text{timeconst}^2$. With the default 20 ms time constant a 1 kg box that starts 20 mm deep rises gently (0.38 m/s, no overshoot); with 4 ms it is ejected at 2.6 m/s and flies 33 cm. Settle initial states in simulation, or generate them without overlap, before you start a stiff scene.

### Divergence is reset, not reported

Output (4) shows MuJoCo's behaviour on a diverging step: one warning, the state reset to `qpos0`, the clock restarted, and the simulation continuing as if nothing had happened. Here the reset state is a spring at rest, which never diverges again, so a run of 1000 steps finishes "successfully" with one warning. Disable `autoreset` while developing to make divergence visible (the state becomes NaN), and check the warning counters after every evaluation run.

### Fixes, in order of cost

The cheapest fixes change the model, not the simulation: realistic masses and inertias, armature on geared joints (Lesson 5.2), damping in the model rather than in the controller (Lessons 5.2 and 8.1), initial states without overlap, thicker collision geoms. Next, the integrator: `implicitfast` costs about the same as `Euler` and removes the damping limit for modelled damping and actuator `kv`; `implicit` costs about 30% more here; `discrete` was even cheaper per step on this scene (we have not isolated why; it used fewer solver iterations) but damps oscillations. Then the contact time constant, free but floored at $2h$. Last, the timestep itself: halving it doubles the cost of every simulated second, and RK4 costs nearly three times `Euler` per step while remaining explicit.

## Implementation

```io
INPUT: small scenes in the script; pick_place.xml
PROCESS: map the spring stability limit for five integrators; fire a ball at a wall for four settings and four speeds; start a box inside the floor; force a divergence and watch the reset; time pick_place under each integrator
OUTPUT: printed tables

```python file=examples/l9_4_stability.py “"”Lesson 9.4: why simulations explode, how to tell, and what the fixes cost.

INPUT small scenes written below; pick_place.xml for the cost of fixes PROCESS (1) a 1 kg mass on a spring: largest stable omega * h for five integrators, and how much each damps the oscillation; (2) a small ball fired at a thin wall: tunnelling (no contact ever detected) versus soft push-through (detected but too soft to stop it), and the fixes; (3) a box that starts inside the floor: ejection speed for two contact stiffnesses; (4) what MuJoCo does when a simulation diverges: the warning, the automatic reset; (5) the cost of the fixes: microseconds per step of pick_place.xml for each integrator and timestep OUTPUT printed tables

Run: python examples/l9_4_stability.py “””

import math import os import time

import mujoco import numpy as np

from mjcourse import model_path

SPRING = “”“

</worldbody></mujoco>””” WALL = “”“

</worldbody></mujoco>””” OVERLAP = “”“

</worldbody></mujoco>”””

def spring_stability() -> None: stiffness = (1e4, 5e5, 9e5, 1.1e6, 2e6, 4e6) print(“ “ + “ “ * 13 + ““.join(f”{f’wh {math.sqrt(k) * 0.002:.2f}’:>12}” for k in stiffness)) for integ in (“Euler”, “implicitfast”, “implicit”, “RK4”, “discrete”): cells = [] for k in stiffness: model = mujoco.MjModel.from_xml_string(SPRING.format(integ=integ, k=k)) data = mujoco.MjData(model) data.qpos[0] = 0.01 peak = 0.0 for i in range(2000): # 4 s mujoco.mj_step(model, data) if i >= 1750: # amplitude over the last 0.5 s peak = max(peak, abs(data.qpos[0])) diverged = data.warning[mujoco.mjtWarning.mjWARN_BADQACC].number > 0 cells.append(“diverged” if diverged else f”{peak / 0.01:.3f}”) print(f” {integ:<13}” + ““.join(f”{c:>12}” for c in cells)) print(“ (cells: amplitude after 4 s divided by the initial 10 mm; 1.000 means no numerical damping)”)

def wall(v: float, half: float, h: float, tc: float) -> str: model = mujoco.MjModel.from_xml_string(WALL.format(h=h, t=half, tc=tc)) data = mujoco.MjData(model) data.qvel[0] = v touched = False for _ in range(round(0.5 / h)): mujoco.mj_step(model, data) touched |= data.ncon > 0 if data.qpos[0] < 0.25: return “stopped” if touched else “never arrived” return “push-through” if touched else “tunnelled”

def wall_table() -> None: speeds = (1, 5, 20, 100) cases = [(“2 mm wall, h 2 ms, timeconst 0.02”, 0.001, 0.002, 0.02), (“2 mm wall, h 0.5 ms, timeconst 0.02”, 0.001, 0.0005, 0.02), (“2 mm wall, h 0.5 ms, timeconst 0.001”, 0.001, 0.0005, 0.001), (“20 mm wall, h 0.5 ms, timeconst 0.001”, 0.01, 0.0005, 0.001)] print(“ “ + “ “ * 40 + ““.join(f”{f’{v} m/s’:>14}” for v in speeds)) for label, half, h, tc in cases: print(f” {label:<40}” + ““.join(f”{wall(v, half, h, tc):>14}” for v in speeds))

def overlap() -> None: for tc in (0.02, 0.004): cells = [] for depth in (0.001, 0.005, 0.02): model = mujoco.MjModel.from_xml_string(OVERLAP.format(tc=tc, z=0.05 - depth)) data = mujoco.MjData(model) vmax = zmax = 0.0 for _ in range(500): mujoco.mj_step(model, data) vmax, zmax = max(vmax, data.qvel[2]), max(zmax, data.qpos[2]) cells.append(f”{1000 * depth:g} mm: {vmax:.2f} m/s, {max(0.0, 1000 * (zmax - 0.05)):.0f} mm up”) print(f” timeconst {tc:<6} “ + “ “.join(cells))

def divergence() -> None: model = mujoco.MjModel.from_xml_string(SPRING.format(integ=”Euler”, k=4e6)) data = mujoco.MjData(model) data.qpos[0] = 0.01 first = None for i in range(1000): mujoco.mj_step(model, data) w = data.warning[mujoco.mjtWarning.mjWARN_BADQACC] if w.number and first is None: first = (i + 1, w.lastinfo, data.time, data.qpos[0]) w = data.warning[mujoco.mjtWarning.mjWARN_BADQACC] print(f” first bad-acceleration warning during step {first[0]} (lastinfo = dof {first[1]}); MuJoCo reset the state “ f”to qpos0 and continued: time {first[2]:.3f} s, qpos {first[3]:.3g} (it started at 0.01)”) print(f” after 1000 steps: {w.number} warnings in total, time {data.time:.3f} s (the clock restarts at every reset)”) model.opt.disableflags |= mujoco.mjtDisableBit.mjDSBL_AUTORESET data = mujoco.MjData(model) data.qpos[0] = 0.01 for _ in range(1000): mujoco.mj_step(model, data) print(f” with autoreset disabled: time {data.time:.3f} s, qpos {data.qpos[0]:.3g}, finite {bool(np.isfinite(data.qpos[0]))}”)

def cost() -> None: reps = 300 if os.environ.get(“MJC_FAST”) == “1” else 3000 base = None for integ in (“mjINT_EULER”, “mjINT_IMPLICITFAST”, “mjINT_IMPLICIT”, “mjINT_RK4”, “mjINT_DISCRETE”): model = mujoco.MjModel.from_xml_path(str(model_path(“pick_place”))) model.opt.integrator = getattr(mujoco.mjtIntegrator, integ) data = mujoco.MjData(model) mujoco.mj_resetDataKeyframe(model, data, 1 if model.nkey > 1 else 0) for _ in range(200): mujoco.mj_step(model, data) iters = 0 t0 = time.perf_counter() for _ in range(reps): mujoco.mj_step(model, data) iters += data.solver_niter[0] us = 1e6 * (time.perf_counter() - t0) / reps base = base or us print(f” {integ[6:].lower():<13} {us:7.1f} us per step ({us / base:.2f} x Euler), {iters / reps:.2f} solver iterations “ f”per step; per simulated second {500 * us / 1000:.1f} ms at h = 2 ms, {1000 * us / 1000:.1f} ms at h = 1 ms”) print(“ (timings on this machine)”)

if name == “main”: print(“(1) a 1 kg mass on a spring, h = 2 ms: amplitude ratio after 4 s, by integrator and omega h”) spring_stability() print(“(2) a 1 cm ball (10 g) fired at a wall 0.25 m away, gravity off: what happens within 0.5 s”) wall_table() print(“(3) a 1 kg box starting inside the floor: peak upward speed and how far it flies”) overlap() print(“(4) what happens when a step diverges (the stiff spring of (1) under Euler, wh = 4)”) divergence() print(“(5) the cost of the fixes: pick_place.xml”) cost()

Output:

```text
(1) a 1 kg mass on a spring, h = 2 ms: amplitude ratio after 4 s, by integrator and omega h
                    wh 0.20     wh 1.41     wh 1.90     wh 2.10     wh 2.83     wh 4.00
  Euler               1.005       1.000       3.162    diverged    diverged    diverged
  implicitfast        1.005       1.000       3.162    diverged    diverged    diverged
  implicit            1.005       1.000       3.162    diverged    diverged    diverged
  RK4                 0.999       0.000       0.000       0.000       1.000    diverged
  discrete            0.000       0.000       0.000       0.000       0.000       0.000
  (cells: amplitude after 4 s divided by the initial 10 mm; 1.000 means no numerical damping)
(2) a 1 cm ball (10 g) fired at a wall 0.25 m away, gravity off: what happens within 0.5 s
                                                   1 m/s         5 m/s        20 m/s       100 m/s
  2 mm wall, h 2 ms, timeconst 0.02              stopped  push-through  push-through     tunnelled
  2 mm wall, h 0.5 ms, timeconst 0.02            stopped  push-through  push-through  push-through
  2 mm wall, h 0.5 ms, timeconst 0.001           stopped       stopped       stopped       stopped
  20 mm wall, h 0.5 ms, timeconst 0.001          stopped       stopped       stopped       stopped
(3) a 1 kg box starting inside the floor: peak upward speed and how far it flies
  timeconst 0.02   1 mm: 0.02 m/s, 0 mm up   5 mm: 0.09 m/s, 0 mm up   20 mm: 0.38 m/s, 0 mm up
  timeconst 0.004  1 mm: 0.13 m/s, 0 mm up   5 mm: 0.65 m/s, 17 mm up   20 mm: 2.60 m/s, 326 mm up
(4) what happens when a step diverges (the stiff spring of (1) under Euler, wh = 4)
  first bad-acceleration warning during step 6 (lastinfo = dof 0); MuJoCo reset the state to qpos0 and continued: time 0.002 s, qpos 0 (it started at 0.01)
  after 1000 steps: 1 warnings in total, time 1.990 s (the clock restarts at every reset)
  with autoreset disabled: time 2.000 s, qpos nan, finite False
(5) the cost of the fixes: pick_place.xml
  euler            36.6 us per step (1.00 x Euler), 1.25 solver iterations per step; per simulated second 18.3 ms at h = 2 ms, 36.6 ms at h = 1 ms
  implicitfast     37.6 us per step (1.03 x Euler), 1.26 solver iterations per step; per simulated second 18.8 ms at h = 2 ms, 37.6 ms at h = 1 ms
  implicit         47.4 us per step (1.30 x Euler), 1.28 solver iterations per step; per simulated second 23.7 ms at h = 2 ms, 47.4 ms at h = 1 ms
  rk4              80.3 us per step (2.20 x Euler), 1.19 solver iterations per step; per simulated second 40.2 ms at h = 2 ms, 80.3 ms at h = 1 ms
  discrete         21.5 us per step (0.59 x Euler), 1.07 solver iterations per step; per simulated second 10.7 ms at h = 2 ms, 21.5 ms at h = 1 ms
  (timings on this machine)

Reading the output:

  • (1) Euler, implicitfast and implicit are identical for a spring: bounded up to $\omega h$ = 1.9, diverged from 2.1. RK4 survives to 2.83 but damps everything in between; discrete never diverges and damps everything.
  • (2) Four outcomes from one scene: stopped, push-through (the contact was seen and failed), tunnelled (never seen), and stopped again once the contact is stiff enough for the speed.
  • (3) The stiff contact ejects the overlapping box at 2.6 m/s; the soft one lifts it gently.
  • (4) One warning, a silent reset, a clock that restarts, and with autoreset disabled, NaN.
  • (5) The integrators’ costs per step on a contact-rich scene, on this machine. The ratio, not the microseconds, is what transfers to yours.

Debugging

A short procedure for any simulation that blows up:

  1. Find out that it happened. Check data.warning[mjtWarning.mjWARN_BADQACC].number after every run, or disable autoreset during development.
  2. Find where. lastinfo names the degree of freedom; print qacc, qfrc_* and the active contacts at the step before.
  3. Classify. A stiff spring, servo or contact (compare $\omega h$ with 2); explicit damping on a light joint (compare $h$ with $2I/c$); an initial overlap (check contacts at time zero); or a model error such as a tiny or zero inertia (print body_mass, body_inertia).
  4. Fix in order of cost (Mathematics), and re-run the check.

The Debugging clinic’s cases 2 (an arm that explodes) and 7 (a ball through a shelf) are practice material for exactly this procedure.

Exercise

For the spring of output (1) at $\omega h$ = 1.9, compute the predicted amplitude distortion $1/\sqrt{1-(\omega h/2)^2}$ and compare it with the measured 3.162. Then find, by bisection on the stiffness, the exact $\omega h$ at which Euler first diverges for this setup, and explain any difference from 2.

Challenge

Build a stack of five boxes on a table and find the largest timestep at which it stays stable for 30 s with Euler, implicitfast and discrete, with default contacts. Then find the smallest time constant each tolerates at a 2 ms step. Report what limits the stack in each case, and which configuration simulates 30 s fastest while keeping the boxes within 1 mm of their resting heights.

Research connection

[!research] Silent failures in large-scale training Reinforcement learning runs simulate millions of episodes, and a divergence that autoreset turns into a quiet reset becomes a strange transition in the replay data rather than an error (inference). Benchmark code that does not check warning counters can therefore train and evaluate on episodes that physically never happened. Level 12 builds environments that count warnings per episode and terminate on them; Level 21 treats the count as a metric to report.

{"id": "9.4-check", "title": "Knowledge check", "questions": [
  {"kind": "numeric", "q": "A 0.5 kg mass hangs on a spring of stiffness 2 × 10⁵ N/m. What is the largest timestep (ms) at which semi-implicit Euler stays stable?",
   "answer": 3.162, "tol": 0.005, "unit": "ms",
   "explain": "<p>$\\omega = \\sqrt{k/m} = \\sqrt{4 \\times 10^5}$ = 632.5 rad/s; $h < 2/\\omega$ = 3.162 ms. <code>implicitfast</code> and <code>implicit</code> do not change this limit for a spring.</p>"},
  {"kind": "mcq", "q": "A fast ball passes through a thin wall, and contacts were detected during the pass. What happened?",
   "options": ["Tunnelling", "Push-through: the contact was too soft to stop the ball within the wall's thickness", "A collision-filter error", "A solver failure"],
   "answer": 1,
   "explain": "<p>Tunnelling means no step saw an overlap. With contacts detected, the contact's time constant was too long for the speed and thickness (output 2). Stiffen the contact with a smaller timestep, or thicken the wall.</p>"},
  {"kind": "mcq", "q": "Your RL evaluation runs 1000 episodes with no errors, but some episodes contain jumps back to the initial pose. What is the most likely cause?",
   "options": ["The policy resets itself", "Divergent steps triggered MuJoCo's automatic reset", "Sensor noise", "Floating-point rounding"],
   "answer": 1,
   "explain": "<p>On a bad acceleration, <code>mj_step</code> resets the state to <code>qpos0</code> and continues, counting the event in <code>data.warning</code> (output 4).</p>"},
  {"kind": "mcq", "q": "Which fix for a stiff spring that diverges under Euler also adds artificial damping to every oscillation?",
   "options": ["Switching to implicitfast", "Switching to the discrete integrator", "Adding armature", "Disabling autoreset"],
   "answer": 1,
   "explain": "<p>The discrete integrator is implicit in position: unconditionally stable for springs, but each step multiplies an oscillation's amplitude by $1/\\sqrt{1 + (\\omega h)^2}$. implicitfast does not stabilize springs at all.</p>"}
]}

Next

Level 10 puts contact to work: grasping and manipulation, from a single pick to contact-rich tasks such as insertion.