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_steprecords anmjWARN_BADQACCwarning inmjData.warning, resets the state to the model’s defaults, and continues, unless theautoresetflag is disabled. The warning counter (number) and the index of the offending degree of freedom (lastinfo) are the only traces.
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.
refsafe allows at that step), and fire at 5, 20 and 100 m/s: stopped every time.```lab simlab
{“dock”: true, “title”: “Stopped, pushed through, or tunnelled”, “height”: 220,
“xml”: “
## 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:
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.autoreset disabled, NaN.A short procedure for any simulation that blows up:
data.warning[mjtWarning.mjWARN_BADQACC].number after every run, or disable autoreset during development.lastinfo names the degree of freedom; print qacc, qfrc_* and the active contacts at the step before.body_mass, body_inertia).The Debugging clinic’s cases 2 (an arm that explodes) and 7 (a ball through a shelf) are practice material for exactly this procedure.
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.
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] Silent failures in large-scale training Reinforcement learning runs simulate millions of episodes, and a divergence that
autoresetturns 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>"}
]}
Level 10 puts contact to work: grasping and manipulation, from a single pick to contact-rich tasks such as insertion.