Core concept

A tool pose has six numbers, three for position and three for orientation, so six joints are enough to reach a pose. A seventh joint adds a self-motion: a way to move the elbow while the tool stays put. Redundant arms use it to stay away from joint limits, obstacles and singular poses, and the course’s arm, arm7.xml, has it for that reason.

Its joints alternate between roll (about the link’s own axis, $z$) and pitch (about $y$): j1 shoulder yaw, j2 shoulder pitch, j3 upper-arm roll, j4 elbow, j5 forearm roll, j6 wrist pitch, j7 flange roll. Three groups do three jobs: the shoulder (j1–j3) orients the upper arm, the elbow (j4) sets the reach, and the wrist (j5–j7) orients the tool. Several commercial 7-DOF arms use a layout of this kind; arm7 is written for the course and is not a model of any of them.

Lesson 5.1’s checks still apply: forward kinematics, gravity torque, motor limits. A 7-DOF arm adds three questions that a two-link arm never raises. At which poses does the arm lose the ability to move its tool in some direction? Which poses make the arm hit itself? And why does the model carry an armature term on every joint, when no link in the drawing looks like a rotor?

[!established] What MuJoCo does with these model choices armature adds a constant to the diagonal of the joint-space inertia matrix for that degree of freedom. Contacts between a body and its parent are skipped by default (the filterparent flag); every other pair of geoms whose contype and conaffinity bitmasks match is tested for contact, including two links of the same arm. Joint damping is a passive force $-b\,\dot q$, and under the Euler and implicitfast integrators MuJoCo integrates it implicitly (for Euler, unless the eulerdamp flag is disabled).

Visual intuition

The lab holds the arm at the joint angles on the sliders with a PD controller plus gravity compensation, computed in the page every step: $\tau = \text{qfrc_bias} + k_p(q^\star - q) - k_d\,\dot q$ with $k_p$ = 100 N m/rad. Try four things.

  1. Find a singular pose. Drag j2, j4 and j6 to 0: the arm stands straight up and the smallest singular value of the tool Jacobian drops to 0. Three of the six tool directions are lost at once (Mathematics explains which).
  2. Find a self-collision. From the home pose, drag j4 (elbow) past about 2.6 rad. The forearm folds into the upper arm and the contact readout names link2 / link4, the most common self-contact across all poses; at the 2.9 rad limit seven pairs touch.
  3. Remove the armature. Set armature to 0, then nudge any joint slider. The flange roll j7 starts to buzz at about 15 rad/s, held in check only by its 10 N m torque limit. (Without the nudge nothing happens: at perfect rest, even an unstable controller holds still.) The controller’s damping gain is now far beyond what the bare wrist can take at a 2 ms step.
  4. Lower the controller damping with armature still at 0. Below about $k_d$ = 0.62 N m s/rad the buzz stops. The script below finds the threshold to 0.02 and predicts it from one line of algebra.

```lab simlab {“dock”: true, “title”: “Posing a 7-DOF arm”, “model”: “arm7”, “key”: 1, “height”: 300, “autoplay”: true, “camera”: {“azimuth”: -60, “elevation”: 15, “distance”: 2.2, “target”: [0.12, 0, 0.62]}, “toggles”: [“frames”, “joints”, “contacts”], “overlays”: {“contacts”: true}, “trail”: “ee”, “setup”: “ctx.kp = 100”, “controls”: [ {“label”: “j1 shoulder yaw”, “unit”: “rad”, “param”: “q0”, “min”: -2.9, “max”: 2.9, “step”: 0.01, “value”: 0, “digits”: 2}, {“label”: “j2 shoulder pitch”, “unit”: “rad”, “param”: “q1”, “min”: -1.8, “max”: 1.8, “step”: 0.01, “value”: 0.5, “digits”: 2}, {“label”: “j3 upper-arm roll”, “unit”: “rad”, “param”: “q2”, “min”: -2.9, “max”: 2.9, “step”: 0.01, “value”: 0, “digits”: 2}, {“label”: “j4 elbow”, “unit”: “rad”, “param”: “q3”, “min”: -0.1, “max”: 2.9, “step”: 0.01, “value”: 1.6, “digits”: 2}, {“label”: “j5 forearm roll”, “unit”: “rad”, “param”: “q4”, “min”: -2.9, “max”: 2.9, “step”: 0.01, “value”: 0, “digits”: 2}, {“label”: “j6 wrist pitch”, “unit”: “rad”, “param”: “q5”, “min”: -2.2, “max”: 2.2, “step”: 0.01, “value”: 1.0416, “digits”: 2}, {“label”: “j7 flange roll”, “unit”: “rad”, “param”: “q6”, “min”: -2.9, “max”: 2.9, “step”: 0.01, “value”: 0, “digits”: 2}, {“label”: “controller damping kd”, “unit”: “N m s/rad”, “param”: “kd”, “min”: 0, “max”: 10, “step”: 0.02, “value”: 5, “digits”: 2}, {“type”: “select”, “label”: “armature, every joint”, “apply”: “model.dof_armature.fill(value)”, “options”: [[“0.05 kg m² (as modelled)”, 0.05], [“0 (bare links)”, 0]], “value”: 0.05}], “controller”: “mj.mj_forward(model, data); for (let i = 0; i < 7; i++) data.ctrl[i] = data.qfrc_bias[i] + ctx.kp * (ctx[‘q’ + i] - data.qpos[i]) - ctx.kd * data.qvel[i]”, “readouts”: [ {“label”: “smallest singular value of J”, “expr”: “lib.jacobianSV(‘ee’)[5]”, “digits”: 4}, {“label”: “manipulability”, “expr”: “lib.jacobianSV(‘ee’).reduce((a, b) => a * b, 1)”, “digits”: 4}, {“label”: “self-contacts”, “expr”: “lib.contactPairs().join(‘, ‘) || ‘none’”}, {“label”: “j7 speed (rad/s)”, “expr”: “data.qvel[6]”, “digits”: 3}], “plots”: [ {“ylabel”: “j7 speed (rad/s)”, “window”: 4, “traces”: [{“label”: “j7 (rad/s)”, “expr”: “data.qvel[6]”}]}, {“ylabel”: “σ_min of J”, “window”: 8, “traces”: [{“label”: “smallest singular value”, “expr”: “lib.jacobianSV(‘ee’)[5]”}]}], “note”: “The Jacobian is the 6 x 7 matrix of the ee site: three rows of linear velocity (m/s) and three of angular velocity (rad/s) per unit joint speed. Its singular values mix those units; Mathematics explains why that matters.”}


## Mathematics

### Redundancy and the null space

The site Jacobian $J(q) \in \mathbb R^{6 \times 7}$ maps joint speeds to the tool's linear and angular velocity, $[v;\ \omega] = J\dot q$. With seven columns and at most six independent rows, $J$ always has a null space of dimension at least one: joint motions $\dot q_0$ with $J \dot q_0 = 0$ that move the arm without moving the tool. Level 6 uses it for posture control; Level 8 for a null-space controller that keeps the elbow up.

### Singular poses

Write $J = U \Sigma V^\top$. The singular value $\sigma_i$ is the tool speed produced per unit joint speed along the $i$-th input direction, and the columns of $U$ are the tool directions. When $\sigma_{\min} \to 0$ the arm needs unbounded joint speed to move its tool along $u_{\min}$; at $\sigma_{\min} = 0$ it cannot at all. Yoshikawa's manipulability measure, $w = \sqrt{\det(JJ^\top)} = \prod_i \sigma_i$ (T. Yoshikawa, *Manipulability of Robotic Mechanisms*, IJRR 4(2), 1985, [doi:10.1177/027836498500400201](https://doi.org/10.1177/027836498500400201)), summarizes this in one number.

> [!derivation] Why the straight-up pose has rank 3
> At $q = 0$ every roll axis (`j1`, `j3`, `j5`, `j7`) lies on the vertical line through the arm and every pitch axis is parallel to $y$. A roll joint then only spins the tool about $z$: its Jacobian column is $[0;\ \hat z]$, the same for all four. A pitch joint at height $h_k$ below the tool moves it along $x$ at speed $h_k$ and turns it about $y$: column $[h_k \hat x;\ \hat y]$. Three distinct heights give two independent directions, $x$-translation and $y$-rotation. Together with $z$-rotation the rank is 3. Translation along $y$ and $z$ and rotation about $x$ are lost: the arm cannot move its tool sideways, cannot extend further, and cannot tilt the tool sideways.

> [!warning] Singular values mix units
> The top three rows of $J$ are in m/rad and the bottom three in rad/rad, so $\sigma_{\min}$ and $w$ change if you measure lengths in millimetres. Compare singular values only within one model and one unit system, or analyse the position block $J_p$ and the orientation block $J_r$ separately, or scale the position rows by a characteristic length of the task.

### Armature is a rotor seen through a gearbox

A motor rotor with inertia $J_r$ drives a joint through a gear of ratio $N$. The rotor turns $N$ times faster than the joint, so its kinetic energy is $\tfrac12 J_r (N\dot q)^2$, and the joint feels an extra inertia

$$I_{\text{reflected}} = N^2 J_r .$$

For illustration (not a datasheet), $J_r = 5 \times 10^{-6}$ kg m² with $N = 100$ gives 0.05 kg m², the value in `arm7.xml`. This term does not move with the arm, so MuJoCo adds it to the diagonal of $M(q)$. On a geared robot it is often larger than the link's own inertia about distal joints: for `j7` the bare link contributes $2.4 \times 10^{-4}$ kg m², and armature is 99.5% of what the joint feels.

### Why armature decides stability under a torque controller

A controller that computes $\tau = -k_d \dot q$ applies damping MuJoCo cannot see in advance: it is a number written to `ctrl`, integrated explicitly whatever the integrator. The joint's own damping $b$ is different: `Euler` integrates it implicitly. For one joint with inertia $I$, one step of semi-implicit Euler gives

$$\dot q_{n+1} = \dot q_n\,\frac{I - h k_d}{I + h b}, \qquad \text{stable when } |I - h k_d| < I + h b \iff h < \frac{2I}{k_d - b}\ \ (k_d > b).$$

For the bare `j7` ($I = 2.4 \times 10^{-4}$, $b = 0.5$, $h$ = 2 ms) the bound allows $k_d$ up to $b + 2I/h = 0.74$ N m s/rad. With the armature, $I$ = 0.0502 and the limit is 50.7: any reasonable gain is stable. The script measures both.

> [!recommendation] Three ways to make a stiff controller stable
> Give distal joints realistic armature (the physical fix, when the real joint is geared); move damping from the controller into the model, where the integrator can treat it implicitly (joint `damping`, or a position actuator's `kv` under `implicitfast`); or reduce the timestep. Switching the integrator alone does not help a controller that computes its own damping, as output (5) shows.

## Implementation

```io
INPUT: arm7.xml
PROCESS: tabulate joints with and without armature; Jacobian singular values at two poses; gravity torque and self-collision over 10 000 random poses; hold the home pose under five settings; sweep the controller damping on a bare wrist
OUTPUT: printed tables

```python file=examples/l5_2_seven_dof.py “"”Lesson 5.2: checking a 7-DOF arm built from primitives.

INPUT arm7.xml PROCESS (1) tabulate each joint: axis, range, armature, damping, torque limit, its diagonal entry of M at the home pose with and without armature, and the largest timestep at which a controller’s damping gain kd stays stable; (2) singular values of the 6x7 tool Jacobian at the zero and home poses; (3) gravity torque over random poses against the motor limits; (4) self-collision over random poses within the joint ranges; (5) hold the home pose at h = 2 ms with and without armature, under gravity compensation alone and under a joint PD controller; (6) find the controller damping at which the bare wrist (no armature) goes unstable, and compare with the bound h < 2 I / (kd - b) OUTPUT printed tables

Run: python examples/l5_2_seven_dof.py “””

import os

import mujoco import numpy as np

from mjcourse import model_path

N_POSES = 2000 if os.environ.get(“MJC_FAST”) == “1” else 10000

def load() -> tuple[mujoco.MjModel, mujoco.MjData]: model = mujoco.MjModel.from_xml_path(str(model_path(“arm7”))) return model, mujoco.MjData(model)

def mass_matrix(model, data) -> np.ndarray: mass = np.zeros((model.nv, model.nv)) mujoco.mj_fullM(model, data, mass) return mass

KP, KD = 100.0, 5.0 # joint PD gains used in (1) and (5): N m/rad, N m s/rad

def h_max(inertia: float, b: float, kd: float = KD) -> str: “"”Largest stable step for explicit damping kd on a joint whose damping b is implicit (Euler).””” return “any” if kd <= b else f”{1000 * 2 * inertia / (kd - b):.2f} ms”

def joint_table(model, data) -> None: mujoco.mj_resetDataKeyframe(model, data, model.key(“home”).id) mujoco.mj_forward(model, data) with_arm = np.diag(mass_matrix(model, data)).copy() bare = with_arm - model.dof_armature # armature adds to the diagonal only print(f” {‘joint’:<5}{‘axis’:>5}{‘range (rad)’:>15}{‘armature’:>10}{‘damping’:>9}{‘limit’:>7}” f”{‘M_ii’:>9}{‘M_ii bare’:>11}{‘h_max’:>10}{‘bare’:>11}”) for j in range(model.njnt): axis = “xyz”[int(np.argmax(np.abs(model.jnt_axis[j])))] print(f” {model.joint(j).name:<5}{axis:>5}{np.array2string(model.jnt_range[j], precision=1):>15}” f”{model.dof_armature[j]:>10.3f}{model.dof_damping[j]:>9.2f}{model.actuator_ctrlrange[j, 1]:>7.0f}” f”{with_arm[j]:>9.4f}{bare[j]:>11.5f}{h_max(with_arm[j], model.dof_damping[j]):>10}{h_max(bare[j], model.dof_damping[j]):>11}”) print(f” units: limit N m, M_ii kg m^2 at the home pose; h_max = 2 M_ii / (kd - b) for a controller”) print(f” damping kd = {KD} N m s/rad on top of the joint’s own damping b, which Euler treats implicitly”)

def jacobian_svd(model, data) -> None: site = model.site(“ee”).id jacp, jacr = np.zeros((3, model.nv)), np.zeros((3, model.nv)) for key in (“zero”, “home”): mujoco.mj_resetDataKeyframe(model, data, model.key(key).id) mujoco.mj_forward(model, data) mujoco.mj_jacSite(model, data, jacp, jacr, site) sv = np.linalg.svd(np.vstack([jacp, jacr]), compute_uv=False) rank = int(np.sum(sv > 1e-9 * sv[0])) print(f” {key:<5} singular values {np.round(sv, 4)} rank {rank} “ f”manipulability sqrt(det(J J^T)) = {np.prod(sv):.2e}”)

def gravity_budget(model, data) -> None: rng = np.random.default_rng(0) lo, hi = model.jnt_range[:, 0], model.jnt_range[:, 1] peak = np.zeros(model.nv) over = 0 for _ in range(N_POSES): data.qpos[:] = rng.uniform(lo, hi) data.qvel[:] = 0 mujoco.mj_forward(model, data) tau = np.abs(data.qfrc_bias) peak = np.maximum(peak, tau) over += bool(np.any(tau > model.actuator_ctrlrange[:, 1])) limit = model.actuator_ctrlrange[:, 1] ratio = [”-“ if p < 1e-9 else f”{lim / p:.2f}” for lim, p in zip(limit, peak)] print(f” {N_POSES} random poses within the ranges: largest gravity torque per joint (N m) {np.round(peak, 2)}”) print(f” motor limit / largest gravity torque: {‘ ‘.join(ratio)} (- : vertical axis, gravity never loads it)”) print(f” poses where some joint’s gravity torque exceeds its motor limit: {over}”)

def self_collision(model, data) -> None: rng = np.random.default_rng(1) lo, hi = model.jnt_range[:, 0], model.jnt_range[:, 1] hits, pairs = 0, {} for _ in range(N_POSES): data.qpos[:] = rng.uniform(lo, hi) mujoco.mj_forward(model, data) if data.ncon: hits += 1 for c in data.contact[:data.ncon]: key = “ / “.join(sorted((model.geom(c.geom1).name, model.geom(c.geom2).name))) pairs[key] = pairs.get(key, 0) + 1 print(f” {N_POSES} random poses: {100 * hits / N_POSES:.1f}% have at least one self-contact”) for key, n in sorted(pairs.items(), key=lambda kv: -kv[1])[:4]: print(f” {key:<24} {n} poses”) for key in (“zero”, “home”): mujoco.mj_resetDataKeyframe(model, data, model.key(key).id) mujoco.mj_forward(model, data) print(f” contacts at the {key} pose: {data.ncon}”)

def wrist_peak(kd: float, kp: float, seconds: float = 4.0) -> float: “"”Largest |qvel| of j7 over the last quarter of a hold with no armature anywhere.””” model, data = load() model.dof_armature[:] = 0 key = model.key(“home”).id mujoco.mj_resetDataKeyframe(model, data, key) target = model.key_qpos[key].copy() data.qvel[:] = 0.01 n = round(seconds / model.opt.timestep) peak = 0.0 for i in range(n): mujoco.mj_forward(model, data) data.ctrl[:] = data.qfrc_bias + kp * (target - data.qpos) - kd * data.qvel mujoco.mj_step(model, data) if i > 0.75 * n: peak = max(peak, abs(data.qvel[6])) return peak

def wrist_threshold() -> None: model, data = load() mujoco.mj_resetDataKeyframe(model, data, model.key(“home”).id) mujoco.mj_forward(model, data) inertia = mass_matrix(model, data)[6, 6] - model.dof_armature[6] b, h = model.dof_damping[6], model.opt.timestep print(f” bare j7: I = {inertia:.6f} kg m^2, b = {b} N m s/rad, h = {h} s -> bound predicts kd < b + 2 I / h = {b + 2 * inertia / h:.3f}”) for kp in (0.0, KP): stable = [kd for kd in np.arange(0.50, 0.81, 0.02) if wrist_peak(kd, kp) < 1e-3] print(f” kp {kp:>5g}: stable for kd up to {max(stable):.2f}, unstable from {max(stable) + 0.02:.2f} N m s/rad”)

def hold_home(armature: float, integrator: str, pd: bool, eulerdamp: bool = True, seconds: float = 2.0) -> str: model, data = load() model.dof_armature[:] = armature model.opt.integrator = getattr(mujoco.mjtIntegrator, integrator) if not eulerdamp: model.opt.disableflags |= mujoco.mjtDisableBit.mjDSBL_EULERDAMP key = model.key(“home”).id mujoco.mj_resetDataKeyframe(model, data, key) target = model.key_qpos[key].copy() data.qvel[:] = 0.01 # a small disturbance peak = np.zeros(model.nv) for _ in range(round(seconds / model.opt.timestep)): mujoco.mj_forward(model, data) data.ctrl[:] = data.qfrc_bias # gravity compensation (clamped to ctrlrange) if pd: data.ctrl[:] += KP * (target - data.qpos) - KD * data.qvel mujoco.mj_step(model, data) peak = np.maximum(peak, np.abs(data.qvel)) warn = data.warning[mujoco.mjtWarning.mjWARN_BADQACC].number j = int(np.argmax(peak)) return f”max |qvel| {peak[j]:9.3g} rad/s at {model.joint(j).name}, bad-acceleration warnings {warn}”

if name == “main”: model, data = load() print(“(1) joints, inertia and the explicit-damping timestep bound:”) joint_table(model, data) print(“(2) singular values of the 6x7 Jacobian of the ee site:”) jacobian_svd(model, data) print(“(3) gravity torque against motor limits:”) gravity_budget(model, data) print(“(4) self-collision:”) self_collision(model, data) print(f”(5) hold the home pose for 2 s at h = 2 ms after a 0.01 rad/s nudge (PD: kp {KP:g}, kd {KD:g}):”) cases = [ (“gravity compensation only”, 0.0, “mjINT_EULER”, False, True), (“ same, eulerdamp disabled”, 0.0, “mjINT_EULER”, False, False), (“joint PD”, 0.05, “mjINT_EULER”, True, True), (“joint PD”, 0.0, “mjINT_EULER”, True, True), (“joint PD”, 0.0, “mjINT_IMPLICITFAST”, True, True), ] for label, arm, integ, pd, eulerdamp in cases: name = {“mjINT_EULER”: “Euler”, “mjINT_IMPLICITFAST”: “implicitfast”}[integ] print(f” {label:<28} armature {arm:<5} {name:<13} {hold_home(arm, integ, pd, eulerdamp)}”) print(“(6) the bare wrist’s stability threshold in kd (no armature, Euler, h = 2 ms):”) wrist_threshold()

Output:

```text
(1) joints, inertia and the explicit-damping timestep bound:
  joint axis    range (rad)  armature  damping  limit     M_ii  M_ii bare     h_max       bare
  j1       z    [-2.9  2.9]     0.050     0.50     80   1.0659    1.01592 473.74 ms  451.52 ms
  j2       y    [-1.8  1.8]     0.050     0.50     80   1.6672    1.61723 740.99 ms  718.77 ms
  j3       z    [-2.9  2.9]     0.050     0.50     60   0.4674    0.41735 207.71 ms  185.49 ms
  j4       y    [-0.1  2.9]     0.050     0.50     60   0.4700    0.42002 208.90 ms  186.68 ms
  j5       z    [-2.9  2.9]     0.050     0.50     20   0.0546    0.00461  24.27 ms    2.05 ms
  j6       y    [-2.2  2.2]     0.050     0.50     20   0.0549    0.00487  24.39 ms    2.17 ms
  j7       z    [-2.9  2.9]     0.050     0.50     10   0.0502    0.00024  22.33 ms    0.11 ms
  units: limit N m, M_ii kg m^2 at the home pose; h_max = 2 M_ii / (kd - b) for a controller
  damping kd = 5.0 N m s/rad on top of the joint's own damping b, which Euler treats implicitly
(2) singular values of the 6x7 Jacobian of the ee site:
  zero  singular values [2.0666 2.     0.498  0.     0.     0.    ]  rank 3  manipulability sqrt(det(J J^T)) = 0.00e+00
  home  singular values [1.85   1.8247 1.0681 0.4078 0.35   0.2332]  rank 6  manipulability sqrt(det(J J^T)) = 1.20e-01
(3) gravity torque against motor limits:
  10000 random poses within the ranges: largest gravity torque per joint (N m) [ 0.   42.47 11.63 11.78  0.48  0.49  0.  ]
  motor limit / largest gravity torque: - 1.88 5.16 5.09 41.26 41.19 -  (- : vertical axis, gravity never loads it)
  poses where some joint's gravity torque exceeds its motor limit: 0
(4) self-collision:
  10000 random poses: 15.5% have at least one self-contact
    link2 / link4            1550 poses
    flange / link2           282 poses
    flange / link1           250 poses
    link2 / link5            185 poses
  contacts at the zero pose: 0
  contacts at the home pose: 0
(5) hold the home pose for 2 s at h = 2 ms after a 0.01 rad/s nudge (PD: kp 100, kd 5):
  gravity compensation only    armature 0.0   Euler         max |qvel|    0.0108 rad/s at j1, bad-acceleration warnings 0
    same, eulerdamp disabled   armature 0.0   Euler         max |qvel|  1.59e+06 rad/s at j7, bad-acceleration warnings 1
  joint PD                     armature 0.05  Euler         max |qvel|    0.0104 rad/s at j1, bad-acceleration warnings 0
  joint PD                     armature 0.0   Euler         max |qvel|      15.2 rad/s at j7, bad-acceleration warnings 0
  joint PD                     armature 0.0   implicitfast  max |qvel|      15.2 rad/s at j7, bad-acceleration warnings 0
(6) the bare wrist's stability threshold in kd (no armature, Euler, h = 2 ms):
  bare j7: I = 0.000240 kg m^2, b = 0.5 N m s/rad, h = 0.002 s -> bound predicts kd < b + 2 I / h = 0.740
  kp     0: stable for kd up to 0.72, unstable from 0.74 N m s/rad
  kp   100: stable for kd up to 0.62, unstable from 0.64 N m s/rad

MuJoCo also prints WARNING: Nan, Inf or huge value in QACC at DOF 1 (to the terminal and to MUJOCO_LOG.TXT) during the run with eulerdamp disabled; it reset that simulation, which is why output (5) shows one warning and an absurd peak speed.

Reading the output:

Debugging

The wrist buzzes while the controller holds still. Explicit controller damping on a joint with tiny inertia. Check M_ii for the distal joints (output 1) and compare $h$ with $2 M_{ii} / (k_d - b)$. Add armature if the real joint is geared, lower $k_d$, or let MuJoCo do the damping.

Inverse kinematics converges from everywhere except the zero pose. The zero pose is singular (rank 3): a Jacobian-based step from there cannot move the tool along three directions. Start from home.

A trajectory that is fine in joint space fails with a contact between link2 and link4. The elbow folded into the upper arm (output 4). Check every waypoint and the path between them for contacts, not only the endpoints.

The arm drifts down slowly under gravity compensation. Either the compensation torque was computed for a stale state (call mj_forward before reading qfrc_bias, as the script does), or a joint saturated its torque limit at that pose. Compare qfrc_bias with ctrlrange.

Exercise

At the home pose, sweep j6 from $-2.2$ to $2.2$ rad and plot $\sigma_{\min}$ of the full Jacobian and of its orientation block alone. Find the wrist singularity, name the two joint axes that align there, and explain why the position block stays well conditioned through it.

Challenge

Compute a null-space direction $\dot q_0$ of $J$ at the home pose and move the arm along it in small steps (re-computing $J$ each step) for 1 rad of total joint motion. Plot the tool’s position and orientation drift and the elbow’s height. Then use the same self-motion to move the arm out of a link2 / link4 contact without moving the tool by more than 1 mm.

Research connection

[!research] Action spaces inherit the arm’s geometry Learned policies for 7-DOF arms usually act in joint space or in end-effector space (Level 12). An end-effector action is converted to joint motion through $J$, so near the singular poses found here a small action demands large joint speeds, and the null space is resolved by whatever the controller does, not by the policy. A joint-space action avoids that but makes the policy learn the kinematics. Neither choice is neutral, and a paper that compares policies across action spaces is also comparing how each handles singularities and redundancy (inference; Level 21 asks you to test it).

{"id": "5.2-check", "title": "Knowledge check", "questions": [
  {"kind": "numeric", "q": "A joint is driven through a 50:1 gearbox by a motor whose rotor inertia is 2 × 10⁻⁵ kg m². What armature (kg m²) should the model carry for that joint?",
   "answer": 0.05, "tol": 0.0005, "unit": "kg m^2",
   "explain": "<p>$N^2 J_r = 2500 \\times 2 \\times 10^{-5}$ = 0.05 kg m².</p>"},
  {"kind": "numeric", "q": "A joint has inertia 0.001 kg m² and joint damping b = 0.5 N m s/rad. Your controller adds explicit damping kd = 1.5 N m s/rad. Under Euler, what is the largest stable timestep, in ms?",
   "answer": 2, "tol": 0.01, "unit": "ms",
   "explain": "<p>$h < 2I/(k_d - b) = 0.002/1.0$ s = 2 ms. The joint's own damping is implicit and offsets part of the controller's.</p>"},
  {"kind": "mcq", "q": "Your torque controller makes a light wrist joint chatter at h = 2 ms under Euler. You switch to implicitfast and nothing changes. Why?",
   "options": ["implicitfast is slower than Euler", "The damping is computed by your controller and written to ctrl, so every integrator treats it explicitly", "implicitfast ignores armature", "The chatter is caused by contacts, which no integrator changes"],
   "answer": 1,
   "explain": "<p>Implicit integrators can treat damping implicitly only when MuJoCo knows it: joint <code>damping</code> and actuator <code>kv</code>. A torque computed outside and written to <code>ctrl</code> is a constant for the step. Output (5) shows identical results under both integrators.</p>"},
  {"kind": "mcq", "q": "At the straight-up pose, which tool motion can the arm still make?",
   "options": ["Translation along y", "Translation along z (further up)", "Translation along x", "Rotation about x"],
   "answer": 2,
   "explain": "<p>The pitch joints (axes along y) at different heights move the tool along x and rotate it about y; the roll joints rotate it about z. Translation along y and z and rotation about x are lost.</p>"}
]}

Next

Lesson 5.3 mounts a gripper on this arm and asks the questions that decide whether a grasp holds: how two fingers are coupled, how hard they squeeze, and how friction is configured at the pads.