For one rigid body, everything MuJoCo computes reduces to two laws. Newton’s law for the centre of mass: the net force equals mass times the acceleration of the centre of mass. Euler’s law for rotation: the net torque about the centre of mass equals the rate of change of angular momentum. Together they are the Newton-Euler equations, and an articulated robot is many bodies obeying them, coupled by joints.
MuJoCo never solves the Newton-Euler equations body by body in world coordinates; it works in joint coordinates (Level 7). But for a single free body the two descriptions coincide, which makes it the perfect place to check the engine against the textbook, and to learn what conservation laws should and should not hold in a simulation.
Two balls approach each other in zero gravity and collide. The collision forces are equal and opposite, so total momentum cannot change. Total kinetic energy can, and how much is lost depends on the contact’s damping ratio. Change the damping ratio and the mass of the blue ball, and predict the outcome before each run.
```lab simlab {“dock”: true, “title”: “A collision in zero gravity”, “model”: “two_balls”, “key”: 0, “height”: 220, “camera”: {“azimuth”: -90, “elevation”: 5, “distance”: 1.5, “target”: [0, 0, 0.3]}, “toggles”: [“contacts”, “forces”], “overlays”: {“forces”: true}, “forceScale”: 0.005, “controls”: [ {“label”: “contact damping ratio (both balls)”, “apply”: “model.geom_solref[1] = value; model.geom_solref[3] = value”, “min”: 0.02, “max”: 1.0, “step”: 0.01, “value”: 1.0, “digits”: 2, “reset”: true}, {“label”: “mass of the blue ball”, “unit”: “kg”, “apply”: “model.body_mass[2] = value; model.body_inertia.set([0.4 * value * 0.0025, 0.4 * value * 0.0025, 0.4 * value * 0.0025], 6); mj.mj_setConst(model, data)”, “min”: 0.1, “max”: 5, “step”: 0.1, “value”: 1.0, “digits”: 1, “reset”: true} ], “readouts”: [ {“label”: “total momentum x (kg m/s)”, “expr”: “model.body_mass[1] * data.qvel[0] + model.body_mass[2] * data.qvel[6]”, “digits”: 6}, {“label”: “kinetic energy (J)”, “expr”: “0.5 * model.body_mass[1] * (data.qvel[0]2 + data.qvel[1]2 + data.qvel[2]2) + 0.5 * model.body_mass[2] * (data.qvel[6]2 + data.qvel[7]2 + data.qvel[8]2)”, “digits”: 4}, {“label”: “orange vx (m/s)”, “expr”: “data.qvel[0]”, “digits”: 4}, {“label”: “blue vx (m/s)”, “expr”: “data.qvel[6]”, “digits”: 4}], “plots”: [{“ylabel”: “velocity along x (m/s)”, “window”: 1.2, “traces”: [ {“label”: “orange (m/s)”, “expr”: “data.qvel[0]”}, {“label”: “blue (m/s)”, “expr”: “data.qvel[6]”}, {“label”: “centre of mass (m/s)”, “expr”: “(model.body_mass[1] * data.qvel[0] + model.body_mass[2] * data.qvel[6]) / (model.body_mass[1] + model.body_mass[2])”, “dash”: true}]}], “note”: “The dashed line is the velocity of the pair’s centre of mass. It must stay flat through the collision; check that it does for every setting.”}
## Mathematics
### Newton and Euler
For a body of mass $m$ with centre of mass $\mathbf c$, inertia $I$ about it, angular velocity $\boldsymbol\omega$, acted on by a net force $\mathbf f$ and a net torque $\boldsymbol\tau$ about the centre of mass:
$$m\,\ddot{\mathbf c} = \mathbf f, \qquad I\,\dot{\boldsymbol\omega} + \boldsymbol\omega \times I\boldsymbol\omega = \boldsymbol\tau .$$
The term $\boldsymbol\omega \times I\boldsymbol\omega$ is the **gyroscopic** term. It is why a spinning body resists changes of its spin axis, why the intermediate axis is unstable (Lesson 4.2), and why the explicit `Euler` integrator can pump energy into fast-spinning bodies. In the body's principal frame, $I$ is diagonal and the rotational equation is written component by component as Euler's equations.
> [!established] Where the equations live in MuJoCo
> For a free joint, MuJoCo's `qacc[0:3]` is the linear acceleration in the world frame and `qacc[3:6]` the angular acceleration in the body frame (the same split as `qvel`, Lesson 1.2). A wrench applied through `data.xfrc_applied[body]` is $[\mathbf f, \boldsymbol\tau]$ in world coordinates at the body's centre of mass, as MuJoCo's `mj_xfrcAccumulate` shows in `engine_support.c`.
### Momentum and energy
Summing Newton's law over a system of bodies, internal forces cancel in pairs, so the total linear momentum $\mathbf p = \sum_i m_i \dot{\mathbf c}_i$ changes only through external forces. The same holds for total angular momentum and external torques. Energy is different: internal forces can do work. A collision converts kinetic energy into whatever the contact model does with it. In a real collision that is heat, sound and permanent deformation; in MuJoCo it is the damping of the soft contact.
> [!derivation] Energy after a head-on collision
> For two bodies colliding along a line, with masses $m_a, m_b$, approach speed $u$ (relative velocity before) and coefficient of restitution $e$ (separation speed over approach speed), momentum conservation leaves the centre-of-mass motion unchanged, and the kinetic energy after is
> $$E' = \tfrac12 (m_a + m_b) v_{\text{cm}}^2 + \tfrac12 \mu\, e^2 u^2, \qquad \mu = \frac{m_a m_b}{m_a + m_b},$$
> so the collision removes $\tfrac12\mu(1 - e^2)u^2$. For the lab's default (two 1 kg balls, 1 m/s against -0.5 m/s): $v_{\text{cm}}$ = 0.25 m/s, $\mu$ = 0.5 kg, $u$ = 1.5 m/s, and $E' = 0.0625 + 0.5625\,e^2$ joules.
## Implementation
```io
INPUT: `tumbling_box.xml` and `two_balls.xml`
PROCESS: apply a known wrench to the box and compare MuJoCo's accelerations with Newton-Euler; collide the balls at four damping ratios
OUTPUT: accelerations side by side; momentum, energy and restitution per damping ratio
```python file=examples/l4_3_newton_euler.py “"”Lesson 4.3: Newton-Euler for one body, and conservation through a collision.
INPUT tumbling_box.xml (one free body), two_balls.xml (two free spheres) PROCESS (1) apply a known force and torque to the box, then compare MuJoCo’s accelerations with Newton’s and Euler’s equations computed by hand; (2) collide the two balls at four contact damping ratios and measure total linear momentum and kinetic energy before and after OUTPUT printed comparisons
Run: python examples/l4_3_newton_euler.py “””
import mujoco import numpy as np
from mjcourse import model_path
def newton_euler() -> None: model = mujoco.MjModel.from_xml_path(str(model_path(“tumbling_box”))) data = mujoco.MjData(model) data.qvel[3:6] = [0.4, -1.2, 2.0] # body-frame angular velocity (rad/s) force, torque = np.array([0.3, -0.2, 0.5]), np.array([0.01, 0.02, -0.015]) # world frame, at the COM data.xfrc_applied[1] = np.r_[force, torque] mujoco.mj_forward(model, data)
m, inertia = model.body_mass[1], np.diag(model.body_inertia[1]) # principal axes = body axes here
r = data.xmat[1].reshape(3, 3)
w = data.qvel[3:6]
lin = force / m # Newton: world-frame acceleration
ang = np.linalg.solve(inertia, r.T @ torque - np.cross(w, inertia @ w)) # Euler, in the body frame
print(f" linear qacc[0:3] MuJoCo {np.round(data.qacc[0:3], 9)} Newton {np.round(lin, 9)}")
print(f" angular qacc[3:6] MuJoCo {np.round(data.qacc[3:6], 9)} Euler {np.round(ang, 9)}")
def collision(dampratio: float) -> tuple[float, float, float, float, float]: “"”Return momentum before/after, kinetic energy before/after, and restitution.””” xml = model_path(“two_balls”).read_text().replace(‘solref=”0.01 1”’, f’solref=”0.01 {dampratio}”’) model = mujoco.MjModel.from_xml_string(xml) data = mujoco.MjData(model) mujoco.mj_resetDataKeyframe(model, data, 0) m = model.body_mass[1:3]
def momentum_energy():
v = np.array([data.qvel[0:3], data.qvel[6:9]])
return (m[:, None] * v).sum(axis=0)[0], 0.5 * float((m * (v ** 2).sum(axis=1)).sum())
p0, e0 = momentum_energy()
approach = data.qvel[6] - data.qvel[0] # relative velocity of b w.r.t. a
for _ in range(1000): # 1 s: the balls meet at about t = 0.33 s
mujoco.mj_step(model, data)
p1, e1 = momentum_energy()
restitution = -(data.qvel[6] - data.qvel[0]) / approach # e = -(separation speed) / (approach speed)
return p0, p1, e0, e1, restitution
if name == “main”: print(“Newton-Euler for the tumbling box under an applied wrench:”) newton_euler() print(“head-on collision, 1 kg at +1 m/s against 1 kg at -0.5 m/s:”) print(f” {‘dampratio’:>9} {‘momentum before’:>16} {‘after’:>10} {‘KE before (J)’:>14} {‘after (J)’:>10} {‘restitution’:>12}”) for z in (1.0, 0.5, 0.2, 0.05): p0, p1, e0, e1, rest = collision(z) print(f” {z:>9.2f} {p0:>16.6f} {p1:>10.6f} {e0:>14.4f} {e1:>10.4f} {rest:>12.3f}”)
Output:
```text
Newton-Euler for the tumbling box under an applied wrench:
linear qacc[0:3] MuJoCo [ 0.3 -0.2 0.5] Newton [ 0.3 -0.2 0.5]
angular qacc[3:6] MuJoCo [12.08275862 6.50769231 -3.312 ] Euler [12.08275862 6.50769231 -3.312 ]
head-on collision, 1 kg at +1 m/s against 1 kg at -0.5 m/s:
dampratio momentum before after KE before (J) after (J) restitution
1.00 0.500000 0.500000 0.6250 0.0722 0.132
0.50 0.500000 0.500000 0.6250 0.1099 0.290
0.20 0.500000 0.500000 0.6250 0.2380 0.559
0.05 0.500000 0.500000 0.6250 0.4459 0.826
The energies match the derivation: $0.0625 + 0.5625 \times 0.132^2 = 0.0723$ J and $0.0625 + 0.5625 \times 0.826^2 = 0.446$ J. Momentum is conserved to the printed six decimals in every case.
[!research] There is no restitution coefficient to set MuJoCo has no “coefficient of restitution” parameter. The bounce emerges from the contact’s virtual spring-damper (
solref: time constant and damping ratio). Even with the damping ratio at 1, the default and nominally critical, these balls separate with $e$ = 0.13, because the spring that pushed them apart releases some of its stored energy before the contact opens. The mapping from damping ratio to restitution also depends on the time constant, the masses and the timestep. If a task depends on bounce (throwing, juggling, dropping parts into bins), identifysolrefagainst measured bounces rather than reading a restitution value off a datasheet.
Momentum “is not conserved” in a check. Something external acted: gravity, a floor contact, joint limits, an actuator anchored to the world, or xfrc_applied left non-zero from a perturbation. Isolate the bodies in zero gravity first, as these models do.
Angular acceleration from qacc[3:6] disagrees with your Euler-equation code. Frames: MuJoCo’s free-joint angular quantities are in the body frame; torques in xfrc_applied are in the world frame. Rotate with $R^\top$, as the script does.
A collision in MuJoCo loses more energy than the real objects do. The default contact is close to critically damped. Lower the damping ratio, or use the negative-number form of solref (direct stiffness and damping), and validate against a measured bounce.
Change the collision so the blue ball is ten times heavier and repeat the table. Predict, before running, the velocity of the light ball after a collision with $e = 1$. Then measure what damping ratio is needed to get within 5% of that elastic prediction.
Make the two balls collide off-centre (offset by 6 cm in $y$, so they touch at a glancing angle) with friction enabled (condim="3"). Show that total linear momentum and total angular momentum about the system’s centre of mass are conserved, and that the balls leave with spin. Where does the spin come from?
Conservation laws are the cheapest correctness tests a simulation can run, and they transfer to learned models. A learned dynamics model (a world model, Level 21.4) that violates momentum conservation in a free-floating scenario is wrong in a way that is easy to measure, and physics-informed architectures build such conservation in. When you evaluate a learned simulator, report conservation errors next to prediction errors.
{"id": "4.3-check", "title": "Knowledge check", "questions": [
{"kind": "numeric", "q": "A 2 kg ball at 3 m/s hits a stationary 1 kg ball head-on and they stick together. What is their common velocity (m/s)?",
"answer": 2.0, "tol": 0.001, "unit": "m/s",
"explain": "<p>Momentum 6 kg m/s over 3 kg: 2 m/s. Kinetic energy falls from 9 J to 6 J.</p>"},
{"kind": "mcq", "q": "Which statement about MuJoCo collisions is true?",
"options": ["Each geom has a restitution coefficient", "Restitution emerges from the contact's solref spring-damper and depends on more than the damping ratio", "Contacts conserve kinetic energy by default", "Momentum is only approximately conserved because contacts are soft"],
"answer": 1,
"explain": "<p>There is no restitution parameter; bounce comes from <code>solref</code>. Momentum is conserved because contact forces are equal and opposite, soft or not.</p>"},
{"kind": "predict", "q": "In the lab, set the blue ball's mass to 5 kg and the damping ratio to 0.05. After the collision, which way does the orange (1 kg) ball move?",
"options": ["Still to the right, slower", "It stops", "To the left (it bounces back)", "It cannot be predicted without simulating"],
"answer": 2,
"explain": "<p>Total momentum is $1 \\times 1 + 5 \\times (-0.5) = -1.5$ kg m/s, so the centre of mass moves left at -0.25 m/s. A nearly elastic bounce reverses the light ball's velocity relative to the centre of mass: it leaves moving left, faster than the heavy ball.</p>",
"sim": {"model": "two_balls", "key": 0, "height": 200, "camera": {"azimuth": -90, "elevation": 5, "distance": 1.5, "target": [0, 0, 0.3]},
"setup": "model.geom_solref[1] = 0.05; model.geom_solref[3] = 0.05; model.body_mass[2] = 5; model.body_inertia.set([0.005, 0.005, 0.005], 6); mj.mj_setConst(model, data)",
"readouts": [{"label": "orange vx (m/s)", "expr": "data.qvel[0]", "digits": 3}, {"label": "blue vx (m/s)", "expr": "data.qvel[6]", "digits": 3}]}}
]}
Level 5 models robots: a two-link arm first, then the course’s 7-DOF arm, a gripper, a hand and a bimanual cell, each with the choices that make it simulate well.