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
armatureadds 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 (thefilterparentflag); every other pair of geoms whosecontypeandconaffinitybitmasks match is tested for contact, including two links of the same arm. Jointdampingis a passive force $-b\,\dot q$, and under theEulerandimplicitfastintegrators MuJoCo integrates it implicitly (forEuler, unless theeulerdampflag is disabled).
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.
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).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.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.```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:
j5, j6 and j7 together weigh almost nothing about their own axes ($5 \times 10^{-3}$ kg m² or less); the joints the controller sees are dominated by armature. Without it, the stable step for $k_d$ = 5 falls to 2 ms at j5 and j6 and to 0.11 ms at j7.j2 carries the largest gravity load, 42.47 N m in the worst sampled pose, against an 80 N m motor: a ratio of 1.88, smaller than the two-link arm’s 3.15. j1’s axis is always vertical, so gravity never loads it; j7 carries only the flange, whose centre of mass lies on the j7 axis, so gravity has no lever arm about it. A gripper with an off-axis load would change that.eulerdamp on, unstable with it off. The next three show the controller’s damping is the problem once armature is gone, and that implicitfast behaves exactly like Euler, to the printed digit.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.
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.
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] 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>"}
]}
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.