Core concept

A rigid body floating in space has six degrees of freedom: three for where it is, three for how it is oriented. A body attached to its parent by a hinge has one. MuJoCo describes a whole mechanism by listing, joint by joint, only the freedoms its joints allow. These numbers are the generalized coordinates $q$, stored in qpos, and their rates of change are the generalized velocities $v$, stored in qvel.

The surprise is that qpos and qvel do not always have the same length. MuJoCo stores orientations of free and ball joints as unit quaternions, four numbers for three degrees of freedom, while their angular velocities are three numbers. So:

Joint type Degrees of freedom qpos entries qvel entries qpos contents qvel contents
free 6 7 6 position (world), quaternion (world) linear velocity (world), angular velocity (body frame)
ball 3 4 3 quaternion relative to parent angular velocity (local frame)
slide 1 1 1 displacement along the axis (m) rate (m/s)
hinge 1 1 1 angle about the axis (rad) rate (rad/s)

In the model’s sizes, nq counts qpos entries and nv counts qvel entries. nv is the number of degrees of freedom; nq is at least nv, larger by one for each free or ball joint.

[!established] Free-joint velocities are mixed-frame MuJoCo’s documentation states that the position and linear velocity of a free joint are in the global frame, the quaternion is global, and the angular velocity is in the local body frame, “the natural parameterization” because angular velocities live in the quaternion’s tangent space. If you build a free body’s angular velocity from a world-frame vector, rotate it into the body frame first.

Visual intuition

The lab shows one body per joint type, with gravity turned off. The buttons give each body a velocity; watch which numbers change, and watch that the free body’s quaternion stays a unit vector as it tumbles.

```lab simlab {“dock”: true, “title”: “One joint of each type”, “model”: “joint_zoo”, “height”: 250, “camera”: {“azimuth”: -90, “elevation”: 12, “distance”: 1.6, “target”: [0, 0, 0.45]}, “toggles”: [“frames”, “joints”], “overlays”: {“frames”: true, “joints”: true}, “controls”: [ {“type”: “button”, “label”: “Spin the free box (qvel[3:6] = 0.5, 1.0, 2.0 rad/s, body frame)”, “run”: “data.qvel[3] = 0.5; data.qvel[4] = 1.0; data.qvel[5] = 2.0”}, {“type”: “button”, “label”: “Swing the ball joint (qvel[6:9])”, “run”: “data.qvel[6] = 1.5; data.qvel[7] = 0.5”}, {“type”: “button”, “label”: “Push the slider (qvel[9] = 0.2 m/s)”, “run”: “data.qvel[9] = 0.2”}, {“type”: “button”, “label”: “Turn the hinge (qvel[10] = 2 rad/s)”, “run”: “data.qvel[10] = 2”} ], “readouts”: [ {“label”: “nq”, “expr”: “model.nq”, “digits”: 0}, {“label”: “nv”, “expr”: “model.nv”, “digits”: 0}, {“label”: “free quat w”, “expr”: “data.qpos[3]”, “digits”: 4}, {“label”: “|free quat|”, “expr”: “Math.hypot(data.qpos[3], data.qpos[4], data.qpos[5], data.qpos[6])”, “digits”: 12}, {“label”: “ball quat w”, “expr”: “data.qpos[7]”, “digits”: 4}, {“label”: “slide (m)”, “expr”: “data.qpos[11]”, “digits”: 4}, {“label”: “hinge (rad)”, “expr”: “data.qpos[12]”, “digits”: 4} ], “plots”: [{“ylabel”: “free-body quaternion”, “window”: 6, “ymin”: -1.05, “ymax”: 1.05, “traces”: [ {“label”: “qw”, “expr”: “data.qpos[3]”}, {“label”: “qx”, “expr”: “data.qpos[4]”}, {“label”: “qy”, “expr”: “data.qpos[5]”}, {“label”: “qz”, “expr”: “data.qpos[6]”}]}], “note”: “Press Play, then a button. The free box keeps tumbling (no gravity, no damping); its quaternion norm stays at 1 to the printed precision because MuJoCo integrates orientations on the unit sphere.”}


## Mathematics

### Why four numbers for three freedoms

A rotation can be written as a unit quaternion $\mathbf{q} = (w, x, y, z) = \left(\cos\tfrac{\theta}{2},\ \sin\tfrac{\theta}{2}\,\hat{\mathbf{n}}\right)$ for a rotation by angle $\theta$ about the unit axis $\hat{\mathbf{n}}$. MuJoCo puts $w$ first. Unit quaternions have no singularities, unlike any three-number parameterization such as Euler angles, which is why a simulator that must handle arbitrary tumbling uses them. Level 4.1 treats rotations properly; here we only need the consequence: **positions of free and ball joints live on a curved space, velocities live in a flat one.**

### Integrating positions

For a hinge, the position update is the obvious $q_{t+h} = q_t + h\,v_{t+h}$. For a quaternion it is not: adding a small vector to a unit quaternion leaves the unit sphere. The correct update rotates the quaternion by the angle the angular velocity sweeps in one step. For a body-frame angular velocity $\boldsymbol{\omega}$,

$$\mathbf{q}_{t+h} = \mathbf{q}_t \otimes \exp\!\left(\tfrac{h}{2}\,(0, \boldsymbol{\omega})\right), \qquad \exp\!\left(\tfrac12 (0,\boldsymbol\phi)\right) = \left(\cos\tfrac{|\boldsymbol\phi|}{2},\ \sin\tfrac{|\boldsymbol\phi|}{2}\,\tfrac{\boldsymbol\phi}{|\boldsymbol\phi|}\right),$$

where $\otimes$ is quaternion multiplication and $\boldsymbol\phi = h\boldsymbol\omega$ is the rotation vector swept in one step. Right-multiplication applies the rotation in the body frame, matching where MuJoCo keeps free-joint angular velocity.

> [!derivation] The inverse operation
> Given two configurations $q_1$ and $q_2$ a time $h$ apart, the velocity that carries one to the other is, for the quaternion part, $\boldsymbol\omega = \tfrac{1}{h}\,\log(\mathbf{q}_1^{-1} \otimes \mathbf{q}_2)$, a 3-vector. This is what you need for finite-difference velocities, for imitation-learning targets from recorded poses, and for numerical Jacobians. The plain difference $(\mathbf{q}_2 - \mathbf{q}_1)/h$ is a 4-vector that is not an angular velocity in any frame.

MuJoCo implements both directions for every joint type at once:

> [!established] mj_integratePos and mj_differentiatePos
> `mujoco.mj_integratePos(model, qpos, qvel, dt)` advances `qpos` in place by `qvel * dt`, handling quaternions correctly. `mujoco.mj_differentiatePos(model, qvel, dt, qpos1, qpos2)` writes into `qvel` the velocity that takes `qpos1` to `qpos2` in time `dt`. Both work on the full `nq` and `nv` vectors, so you never need to know where the quaternions are.

## Implementation

```io
INPUT: `joint_zoo.xml`, one joint of each type, gravity off
PROCESS: print the qpos and qvel layout; spin the free body and recover its velocity from two poses two ways; integrate a pose
OUTPUT: the layout table, the true velocity next to both estimates, and a quaternion norm check

```python file=examples/l1_2_generalized_coordinates.py “"”Lesson 1.2: generalized coordinates, and why positions are not velocities.

INPUT joint_zoo.xml: one free, one ball, one slide and one hinge joint PROCESS (1) print where each joint lives in qpos and in qvel; (2) spin the free body, then recover its velocity from two poses by naive differencing and by mj_differentiatePos; (3) integrate a pose with mj_integratePos and check the quaternion norm OUTPUT the layout table and the two velocity estimates side by side

Run: python examples/l1_2_generalized_coordinates.py “””

import mujoco import numpy as np

from mjcourse import model_path

NAMES = {0: “free”, 1: “ball”, 2: “slide”, 3: “hinge”}

def layout(model: mujoco.MjModel) -> None: print(f”nq = {model.nq}, nv = {model.nv}”) for j in range(model.njnt): qadr, vadr = model.jnt_qposadr[j], model.jnt_dofadr[j] qend = model.jnt_qposadr[j + 1] if j + 1 < model.njnt else model.nq vend = model.jnt_dofadr[j + 1] if j + 1 < model.njnt else model.nv print(f” {model.joint(j).name:<6} ({NAMES[model.jnt_type[j]]:<5}) “ f”qpos[{qadr}:{qend}] size {qend - qadr} qvel[{vadr}:{vend}] size {vend - vadr}”)

def recover_velocity(model: mujoco.MjModel) -> None: data = mujoco.MjData(model) free = model.joint(“free”) q0 = data.qpos.copy() # Spin the free body: 2 rad/s about its local z axis, plus 0.1 m/s along world x. data.qvel[free.dofadr[0]: free.dofadr[0] + 6] = [0.1, 0, 0, 0, 0, 2.0] true_v = data.qvel.copy() dt = 0.05 for _ in range(round(dt / model.opt.timestep)): mujoco.mj_step(model, data) q1 = data.qpos.copy()

naive = (q1 - q0) / dt                    # length nq: not a velocity at all for free and ball joints
proper = np.zeros(model.nv)
mujoco.mj_differentiatePos(model, proper, dt, q0, q1)
a, b = free.qposadr[0], free.dofadr[0]
print("free joint, true qvel           :", np.round(true_v[b:b + 6], 4))
print("mj_differentiatePos(dt, q0, q1)  :", np.round(proper[b:b + 6], 4))
print("naive (q1 - q0) / dt, 7 numbers  :", np.round(naive[a:a + 7], 4))

def integrate_pose(model: mujoco.MjModel) -> None: q = mujoco.MjData(model).qpos.copy() v = np.zeros(model.nv) free = model.joint(“free”) v[free.dofadr[0] + 3: free.dofadr[0] + 6] = [1.0, 2.0, 3.0] # angular velocity, rad/s mujoco.mj_integratePos(model, q, v, 0.5) # advance 0.5 s quat = q[free.qposadr[0] + 3: free.qposadr[0] + 7] print(“after mj_integratePos: quaternion =”, np.round(quat, 4), “ norm =”, round(float(np.linalg.norm(quat)), 12))

if name == “main”: model = mujoco.MjModel.from_xml_path(str(model_path(“joint_zoo”))) layout(model) recover_velocity(model) integrate_pose(model)

Output:

```text
nq = 13, nv = 11
  free   (free ) qpos[0:7] size 7   qvel[0:6] size 6
  ball   (ball ) qpos[7:11] size 4   qvel[6:9] size 3
  slide  (slide) qpos[11:12] size 1   qvel[9:10] size 1
  hinge  (hinge) qpos[12:13] size 1   qvel[10:11] size 1
free joint, true qvel           : [0.1 0.  0.  0.  0.  2. ]
mj_differentiatePos(dt, q0, q1)  : [0.1 0.  0.  0.  0.  2. ]
naive (q1 - q0) / dt, 7 numbers  : [ 0.1     0.      0.     -0.025   0.      0.      0.9996]
after mj_integratePos: quaternion = [0.5935 0.2151 0.4302 0.6453]  norm = 1.0

mj_differentiatePos recovers the true velocity exactly. The naive difference gets the linear part right (it is a flat space) and returns nonsense for the rotation: a 4-vector whose largest entry, 0.9996, is the rate of change of the quaternion’s $z$ component, which is about half the angular speed because of the half-angle in the quaternion. Feed that into a controller or a loss function and the bug is silent.

Never hard-code indices into qpos. Use model.jnt_qposadr[j] and model.jnt_dofadr[j] (or named access, data.joint("free").qpos), because adding one free object to a scene shifts every index after it by 7 in qpos and by 6 in qvel.

Debugging

A free body’s orientation goes wrong after you “integrate” it yourself. You added a velocity to a quaternion. Use mj_integratePos, or MuJoCo’s quaternion helpers mju_quatIntegrate and mju_mulQuat.

A learned policy’s action space has 7 numbers for a 6-DOF floating base. It was built from qpos instead of qvel. Action and velocity spaces have dimension nv.

Angular velocity of a free body “rotates” when you change the body’s orientation. It is expressed in the body frame. To get the world-frame angular velocity, rotate it by the body’s orientation, or read data.cvel (world-oriented, about the centre of mass of the subtree) or a frameangvel sensor.

Exercise

Add a second free box to joint_zoo.xml after the first one and predict the new nq, nv and the qpos addresses of every joint before compiling. Then check your prediction with code.

Challenge

Write finite_difference_velocity(model, qpos_traj, dt) that turns a recorded trajectory of qpos vectors into velocities using mj_differentiatePos, and test it on a tumbling free body against the simulated qvel. Report the error for $h$ = 2 ms and 20 ms and explain why it is not zero. Then implement the free-joint case by hand with quaternion algebra and confirm you match MuJoCo to $10^{-12}$.

Research connection

Learned models of robot motion (behaviour cloning targets, world models, diffusion policies over trajectories) must choose how to represent orientation. Quaternions have a double cover ($\mathbf q$ and $-\mathbf q$ are the same rotation), Euler angles have singularities, and rotation matrices have six redundant numbers. The choice changes what a regression loss measures. Level 4.1 discusses the continuous 6-D representation used in pose-estimation and policy-learning work, and why the loss on rotations should usually be a geodesic angle rather than a vector norm.

{"id": "1.2-check", "title": "Knowledge check", "questions": [
  {"kind": "numeric", "q": "A scene has a 7-DOF arm with hinge joints, a two-finger gripper with two slide joints, and three free objects. What is <code>nq</code>?",
   "answer": 30, "tol": 0, "unit": "",
   "explain": "<p>7 + 2 = 9 for the robot, plus 3 x 7 = 21 for the objects: 30. And <code>nv</code> = 9 + 3 x 6 = 27, which matches <code>pick_place.xml</code> in this course.</p>"},
  {"kind": "mcq", "q": "In which frame is the angular part of a free joint's <code>qvel</code> expressed?",
   "options": ["World frame", "The body's local frame", "The parent body's frame", "The frame of the joint's anchor"],
   "answer": 1,
   "explain": "<p>Linear velocity is in the world frame and angular velocity in the body frame. The documentation explains this as the natural parameterization of the quaternion's tangent space.</p>"},
  {"kind": "mcq", "q": "You need the velocity that moves configuration <code>q1</code> to <code>q2</code> in 0.01 s, for a model with a ball joint. What do you call?",
   "options": ["<code>(q2 - q1) / 0.01</code>", "<code>mujoco.mj_differentiatePos(model, v, 0.01, q1, q2)</code>", "<code>mujoco.mj_integratePos(model, q1, v, 0.01)</code>", "<code>mujoco.mj_forward</code>"],
   "answer": 1,
   "explain": "<p><code>mj_differentiatePos</code> handles the quaternion part; the plain difference has the wrong length and the wrong meaning.</p>"}
]}

Next

Lesson 1.3 opens mj_step: the stages of the pipeline, and how the five integrators trade accuracy, stability and cost.