Core concept

A parallel-jaw grasp holds when four things are right at once: the two fingers move as one, they squeeze hard enough, the friction at the pads can carry the object’s weight plus whatever the motion adds, and the simulator’s friction model holds the object instead of letting it creep. A gripper model makes a claim about each, and each has a MuJoCo setting that decides it:

What must hold Where it lives in gripper.xml What breaks it
The fingers stay centred a fixed tendon averaging the two finger joints, driven by one actuator, plus a joint equality forcing the right finger to mirror the left one finger touching first pushes the object sideways
The squeeze a position servo on the tendon, kp, kv, and its forcerange a soft servo commanded to a target just inside the object squeezes gently
The friction budget pad friction, priority, condim low or wrongly combined friction, missing torsional friction
Friction that holds option/cone, impratio, noslip_iterations soft friction lets a held object creep

The lesson’s measurements come from gantry_gripper.xml: the same gripper on a three-axis gantry, so that every failure is a contact or actuation failure and never an inverse-kinematics one.

[!established] The pattern in gripper.xml Each finger has its own slide joint (0 closed, 0.04 m open). The tendon grip has length $L = \tfrac12 q_\text{left} + \tfrac12 q_\text{right}$, the actuator drives $L$, and the equality mirror constrains $q_\text{left} = q_\text{right}$. Drive the average, constrain the difference: one scalar command opens and closes the jaws symmetrically. The pads have sliding friction 1.5, condim="4" (sliding plus torsional) and priority="1".

Visual intuition

Grasp and lift runs the routine from Research mode: descend, close, then lift the cube 0.1 m with a smooth profile and hold it. The readouts compare the squeeze with the friction it buys. Try:

  1. Run it as loaded. The fingertip-to-cube gap in the plot rises slowly while the cube is held: the cube is creeping down between the pads, about 0.1 mm/s for this light cube. (The plot starts once the lift has finished.)
  2. Raise the cube mass towards the “friction capacity” readout (about 2.36 kg) and run again. The creep gets faster; past the capacity the cube drops.
  3. Raise the force limit to 40 N. Nothing changes: this servo never reaches 30 N on a 40 mm cube. Now raise kp to 3000 N/m: the squeeze jumps to the force limit, and the force limit now decides everything.
  4. Set impratio to 1 and run with a 1.2 kg cube: it slides out. Set it back to 10 and add noslip iterations: the creep nearly stops.

```lab simlab {“dock”: true, “title”: “A grasp and its friction budget”, “model”: “gantry_gripper”, “key”: 0, “height”: 260, “camera”: {“azimuth”: -55, “elevation”: 12, “distance”: 0.55, “target”: [0.12, 0, 0.17]}, “toggles”: [“contacts”, “forces”], “overlays”: {“contacts”: true, “forces”: true}, “forceScale”: 0.004, “setup”: “ctx.t0 = null”, “controls”: [ {“type”: “button”, “label”: “Grasp and lift”, “run”: “mj.mj_resetDataKeyframe(model, data, 0); ctx.t0 = data.time”}, {“label”: “cube mass”, “unit”: “kg”, “apply”: “model.body_mass[lib.bodyId(‘cube’)] = value; mj.mj_setConst(model, data)”, “min”: 0.1, “max”: 4, “step”: 0.05, “value”: 0.1, “digits”: 2}, {“label”: “servo stiffness kp”, “unit”: “N/m”, “apply”: “model.actuator_gainprm[30] = value; model.actuator_biasprm[31] = -value”, “min”: 200, “max”: 5000, “step”: 50, “value”: 800, “digits”: 0}, {“label”: “force limit”, “unit”: “N”, “apply”: “model.actuator_forcerange[6] = -value; model.actuator_forcerange[7] = value”, “min”: 5, “max”: 40, “step”: 1, “value”: 30, “digits”: 0}, {“type”: “select”, “label”: “impratio”, “set”: “model.opt.impratio”, “options”: [[“10 (as modelled)”, 10], [“1”, 1]], “value”: 10}, {“type”: “select”, “label”: “noslip iterations”, “set”: “model.opt.noslip_iterations”, “options”: [[“0 (as modelled)”, 0], [“10”, 10]], “value”: 0}], “controller”: “if (ctx.t0 != null) { const t = data.time - ctx.t0; const s = (x) => { x = Math.min(Math.max(x, 0), 1); return x * x * x * (10 - 15 * x + 6 * x * x); }; data.ctrl[2] = t < 2.2 ? -0.33 * s(t) : -0.33 + 0.1 * s((t - 2.2) / 2); data.ctrl[3] = t < 1.3 ? 0.04 : 0; }”, “readouts”: [ {“label”: “squeeze, actuator (N)”, “expr”: “-data.actuator_force[3]”, “digits”: 2}, {“label”: “normal force, left pad (N)”, “expr”: “lib.normalForce(‘gripper/pad_left’)”, “digits”: 2}, {“label”: “friction capacity 2μN/g (kg)”, “expr”: “2 * 1.5 * lib.normalForce(‘gripper/pad_left’) / 9.81”, “digits”: 2}, {“label”: “cube height (m)”, “expr”: “lib.bodyPos(‘cube’)[2]”, “digits”: 3}], “plots”: [{“ylabel”: “tip-to-cube gap (mm)”, “window”: 8, “traces”: [ {“label”: “fingertip minus cube centre, height (mm)”, “expr”: “ctx.t0 != null && data.time - ctx.t0 > 4.4 ? 1000 * (lib.sitePos(‘gripper/tcp’)[2] - lib.bodyPos(‘cube’)[2]) : NaN”}]}], “note”: “The capacity uses μ = 1.5, the pads’ friction, which the contact uses because the pads have higher priority. It is the static limit: during the lift the cube also needs force to accelerate. Press Reset to stop the routine and return to the open pose.”}


## Mathematics

### One command, two fingers

The tendon length is $L = \tfrac12(q_l + q_r)$, so its Jacobian with respect to $(q_l, q_r)$ is $(\tfrac12, \tfrac12)$, and an actuator force $F$ on the tendon becomes a generalized force of $\tfrac12 F$ on each finger. The equality adds whatever force keeps $q_l = q_r$. If one finger meets the object first, the equality pulls the other one in by the same amount and the object is not pushed sideways. Like every MuJoCo constraint, the equality is soft: output (1) shows a 0.2 mm difference between the fingers under a 5 N push on one of them.

### What sets the squeeze

A position servo produces $F = k_p(u - L) - k_v \dot L$, clipped to `forcerange`. Commanding $u = 0$ (fully closed) on an object that stops the fingers at opening $L_c$ gives, once the fingers are still,

$$F = -\min\left(k_p L_c,\ F_{\max}\right), \qquad N = \tfrac12 |F| \ \text{per pad}.$$

Two regimes follow. A **soft servo** ($k_p L_c < F_{\max}$) squeezes in proportion to the object's width: a wider object is held harder. A **stiff servo** saturates, and the squeeze is $F_{\max}/2$ per pad whatever the width: a force-limited clamp. With $k_p$ = 800 N/m and the 40 mm cube, the course's gripper is the first kind: $800 \times 0.0193$ = 15.4 N, below its 30 N limit (output 2). The limit only takes effect when the servo is pushed past it.

### The friction budget

Two pads, each able to resist a tangential force up to $\mu N$, can hold an object of mass $m$ accelerating upward at $a$ if

$$2\mu N \ \ge\ m(g + a) \qquad\Longrightarrow\qquad m^\star = \frac{2\mu N}{g} \ \text{at rest.}$$

With $\mu$ = 1.5 and $N$ = 7.72 N, $m^\star$ = 2.36 kg. Output (3) confirms the threshold: held at 0.95 $m^\star$, dropped at 1.05 $m^\star$, for both servo settings.

### Which friction coefficient a contact uses

Each contact gets one set of coefficients from its two geoms: the geom with the higher `priority` wins outright; at equal priority MuJoCo takes the element-wise maximum. The pads' `priority="1"` makes the grasp independent of the object's friction, which is the property you want when objects are randomized: output (4) shows the contact using 1.5 against a 0.3 or a 2.0 cube, while at equal priority the 2.0 cube would make the grasp easier than the gripper alone allows.

`condim` picks which friction terms exist: 1 frictionless, 3 sliding in two directions, 4 adds torsional friction about the contact normal (coefficient `friction[1]`, in metres: the maximum torque is `friction[1]` times $N$), 6 adds rolling. For a grasp, torsional friction resists the object turning about the line between the pads. Box pads already touch the cube at several points, whose sliding friction resists twisting too, so condim 4 adds 20% here (output 5); on a single-point contact, such as a fingertip on a sphere, it is the only resistance.

### Soft friction creeps

> [!derivation] Why a held object slides slowly
> MuJoCo solves friction as a soft constraint (Level 9). A friction row has no position error to push against, so its reference acceleration depends on the slip velocity only. In a steady hold, the solver produces the friction force the load requires only with a small, steady slip velocity: the larger the load, the faster the creep (output 3: from 0.4 mm/s at a quarter of $m^\star$ to 1.3 mm/s near it). `impratio` makes friction rows stiffer relative to normal ones; `noslip_iterations` runs a post-processing solver that removes most of the remaining slip. Neither is free: noslip adds solver work every step.

> [!recommendation] Configure grasping scenes this way
> Elliptic cones with `impratio` around 10, as `gantry_gripper.xml` does and as MuJoCo's modeling guide advises for slip-sensitive tasks; pads with `priority` above any object they touch and `condim="4"`; a squeeze margin of at least 2 over the heaviest object ($m^\star \ge 2m$); and, if your task needs long holds, `noslip_iterations` or an explicit check that creep over the episode stays below your tolerance. Report all of these with any grasping result.

## Implementation

```io
INPUT: gantry_gripper.xml
PROCESS: test the finger coupling; measure squeeze for three servo settings; hold cubes at fractions of the friction limit under five contact settings; check friction mixing; twist a held cube with condim 3 and 4
OUTPUT: printed tables

```python file=examples/l5_3_grippers.py “"”Lesson 5.3: what decides whether a parallel-jaw grasp holds.

INPUT gantry_gripper.xml (gripper.xml on a three-axis gantry, one cube) PROCESS (1) the coupling: command openings, then push one finger and watch the equality constraint share the push; (2) the squeeze: actuator force and pad normal forces for three servo stiffnesses, against kp * opening and the forcerange cap; (3) the friction budget: lift cubes at fractions of m* = 2 mu N / g and measure whether they stay held and how fast they creep; then hold one cube under five friction-cone and solver settings; (4) which friction coefficient a pad contact uses, with and without priority; (5) twisting a held cube with condim 3 and condim 4 pads OUTPUT printed tables

Run: python examples/l5_3_grippers.py “””

import mujoco import numpy as np

from mjcourse import model_path

G = 9.81 GRIP = 3 # actuator index of gripper/grip; ctrl = half-opening (m)

def build(condim: int | None = None, pad_priority: int | None = None, cube_friction: float | None = None): spec = mujoco.MjSpec.from_file(str(model_path(“gantry_gripper”))) for name in (“gripper/pad_left”, “gripper/pad_right”): if condim is not None: spec.geom(name).condim = condim if pad_priority is not None: spec.geom(name).priority = pad_priority if cube_friction is not None: spec.geom(“cube”).friction = [cube_friction, 0.005, 0.0001] model = spec.compile() return model, mujoco.MjData(model)

def run(model, data, seconds: float) -> None: for _ in range(round(seconds / model.opt.timestep)): mujoco.mj_step(model, data)

def set_servo(model, kp: float, force_limit: float = 30.0) -> None: model.actuator_gainprm[GRIP, 0] = kp model.actuator_biasprm[GRIP, 1] = -kp model.actuator_forcerange[GRIP] = [-force_limit, force_limit]

def pad_forces(model, data) -> tuple[float, float, list[float], int]: “"”Normal force on each pad from the cube, the friction coefficients used, and the contact count.””” pads = [model.geom(“gripper/pad_left”).id, model.geom(“gripper/pad_right”).id] cube = model.geom(“cube”).id normal, mus, n = [0.0, 0.0], set(), 0 f = np.zeros(6) for i in range(data.ncon): c = data.contact[i] for k, pad in enumerate(pads): if {c.geom1, c.geom2} == {pad, cube}: mujoco.mj_contactForce(model, data, i, f) normal[k] += f[0] mus.add(round(float(c.friction[0]), 3)) n += 1 return normal[0], normal[1], sorted(mus), n

def close_on_cube(model, data, mass: float = 0.1) -> None: “"”The research-mode routine: settle, descend 0.33 m over 1 s, close.””” mujoco.mj_resetDataKeyframe(model, data, 0) model.body_mass[model.body(“cube”).id] = mass mujoco.mj_setConst(model, data) run(model, data, 0.3) for k in range(100): data.ctrl[2] = -0.33 * (k + 1) / 100 run(model, data, 0.01) run(model, data, 0.3) data.ctrl[GRIP] = 0.0 run(model, data, 0.6)

def coupling() -> None: model, data = build() mujoco.mj_resetDataKeyframe(model, data, 0) data.qpos[5:8] = [0.3, 0.3, 0.02] # move the cube out of the way for target in (0.03, 0.02, 0.01): data.ctrl[GRIP] = target run(model, data, 0.5) print(f” command {target:.3f} m: left {data.qpos[3]:.5f}, right {data.qpos[4]:.5f}, “ f”tendon {data.ten_length[0]:.5f} m, actuator force {data.actuator_force[GRIP]:+.3f} N”) finger = model.body(“gripper/finger_left”).id axis = data.xmat[finger].reshape(3, 3)[:, 1] # the left finger slides along its body y axis data.xfrc_applied[finger, :3] = 5.0 * axis # push the left finger open with 5 N run(model, data, 0.5) print(f” 5 N on the left finger only: left {data.qpos[3]:.5f}, right {data.qpos[4]:.5f}, “ f”difference {1e6 * (data.qpos[3] - data.qpos[4]):.1f} um, tendon {data.ten_length[0]:.5f} m”)

def squeeze() -> None: print(f” {‘kp (N/m)’:>9}{‘forcerange’:>12}{‘opening (m)’:>13}{‘kp * opening’:>14}{‘actuator force’:>16}” f”{‘N left’:>8}{‘N right’:>9}{‘contacts’:>10}”) for kp, cap in ((800, 30), (3000, 30), (3000, 10)): model, data = build() set_servo(model, kp, cap) close_on_cube(model, data) nl, nr, _, n = pad_forces(model, data) opening = data.ten_length[0] print(f” {kp:>9}{cap:>10} N{opening:>13.5f}{kp * opening:>12.2f} N{data.actuator_force[GRIP]:>14.2f} N” f”{nl:>8.2f}{nr:>9.2f}{n:>10}”)

def hold(mass: float, kp: float = 800, cap: float = 30, cone: str = “mjCONE_ELLIPTIC”, impratio: float = 10, noslip: int = 0) -> tuple[float, bool, float]: “"”Close on a cube, lift it 0.1 m smoothly over 2 s, hold 4 s.

Returns (normal force per pad at the grasp, whether the cube is still held,
creep: how fast the cube slides down relative to the fingertips during the hold, mm/s).
"""
model, data = build()
set_servo(model, kp, cap)
model.opt.cone = getattr(mujoco.mjtCone, cone)
model.opt.impratio = impratio
model.opt.noslip_iterations = noslip
close_on_cube(model, data, mass)
normal = 0.5 * sum(pad_forces(model, data)[:2])
for k in range(200):                                  # minimum-jerk lift: no acceleration spike
    s = (k + 1) / 200
    data.ctrl[2] = -0.33 + 0.1 * (10 * s**3 - 15 * s**4 + 6 * s**5)
    run(model, data, 0.01)
run(model, data, 0.5)
cube, tcp = model.body("cube").id, model.site("gripper/tcp").id
gap0, t0 = data.site_xpos[tcp][2] - data.xpos[cube][2], data.time
run(model, data, 4.0)
gap1 = data.site_xpos[tcp][2] - data.xpos[cube][2]
held = bool(data.xpos[cube][2] > 0.05)
return normal, held, 1000 * (gap1 - gap0) / (data.time - t0) if held else float("nan")

def friction_budget() -> None: mu = 1.5 for kp, cap in ((800, 30), (3000, 30)): normal = hold(0.1, kp, cap)[0] m_star = 2 * mu * normal / G print(f” kp {kp} N/m, forcerange {cap} N: N = {normal:.2f} N per pad -> m* = 2 mu N / g = {m_star:.2f} kg”) for frac in (0.25, 0.5, 0.75, 0.95, 1.05): _, held, creep = hold(frac * m_star, kp, cap) state = f”held, creeping {creep:.3f} mm/s” if held else “dropped” print(f” {frac:4.2f} m* = {frac * m_star:5.2f} kg: {state}”) m_star = 2 * mu * hold(0.1)[0] / G print(f” one cube at 0.5 m* = {0.5 * m_star:.2f} kg (kp 800), five contact settings:”) for cone, imp, noslip in ((“mjCONE_ELLIPTIC”, 10, 0), (“mjCONE_ELLIPTIC”, 10, 10), (“mjCONE_PYRAMIDAL”, 10, 0), (“mjCONE_ELLIPTIC”, 1, 0), (“mjCONE_PYRAMIDAL”, 1, 0)): _, held, creep = hold(0.5 * m_star, cone=cone, impratio=imp, noslip=noslip) state = f”held, creeping {creep:.3f} mm/s” if held else “dropped” label = f”{cone[7:].lower()}, impratio {imp}” + (f”, noslip {noslip}” if noslip else “”) print(f” {label:<34} {state}”)

def friction_mixing() -> None: for prio in (1, 0): for cube_mu in (0.3, 2.0): model, data = build(pad_priority=prio, cube_friction=cube_mu) close_on_cube(model, data) mus = pad_forces(model, data)[2] print(f” pad priority {prio}, pad friction 1.5, cube friction {cube_mu}: contact friction {mus}”)

def twist(condim: int) -> float: “"”Torque about the grasp axis (N m) at which a held, lifted cube turns by more than 2 degrees.””” model, data = build(condim=condim) close_on_cube(model, data) for k in range(50): # lift 0.1 m slowly data.ctrl[2] = -0.33 + 0.1 * (k + 1) / 50 run(model, data, 0.02) run(model, data, 0.3) cube = model.body(“cube”).id q0 = data.xquat[cube].copy() axis = data.xmat[model.body(“gripper/finger_left”).id].reshape(3, 3)[:, 1] # the line through both pads for k in range(400): # ramp 0 -> 1 N m over 4 s torque = 1.0 * k / 400 data.xfrc_applied[cube, 3:] = torque * axis run(model, data, 0.01) dq = np.zeros(3) mujoco.mju_subQuat(dq, data.xquat[cube], q0) if np.linalg.norm(dq) > np.radians(2): return torque return float(“nan”)

if name == “main”: print(“(1) coupling: one command, two fingers”) coupling() print(“(2) squeeze: what sets the grip force (cube 40 mm wide)”) squeeze() print(“(3) friction budget: cubes at fractions of m*, lifted 0.1 m and held 4 s”) friction_budget() print(“(4) which friction coefficient the pad contacts use”) friction_mixing() print(“(5) twisting a held cube about the line through both pads”) for condim in (4, 3): print(f” pads with condim {condim}: the cube turns 2 degrees at {twist(condim):.3f} N m”)

Output:

```text
(1) coupling: one command, two fingers
  command 0.030 m: left 0.03000, right 0.03000, tendon 0.03000 m, actuator force +0.000 N
  command 0.020 m: left 0.02000, right 0.02000, tendon 0.02000 m, actuator force +0.000 N
  command 0.010 m: left 0.01000, right 0.01000, tendon 0.01000 m, actuator force +0.000 N
  5 N on the left finger only: left 0.01635, right 0.01615, difference 196.1 um, tendon 0.01625 m
(2) squeeze: what sets the grip force (cube 40 mm wide)
   kp (N/m)  forcerange  opening (m)  kp * opening  actuator force  N left  N right  contacts
        800        30 N      0.01930       15.44 N        -15.44 N    7.72     7.72         8
       3000        30 N      0.01886       56.58 N        -30.00 N   15.00    15.00         8
       3000        10 N      0.01945       58.34 N        -10.00 N    5.00     5.00         8
(3) friction budget: cubes at fractions of m*, lifted 0.1 m and held 4 s
  kp 800 N/m, forcerange 30 N: N = 7.72 N per pad -> m* = 2 mu N / g = 2.36 kg
    0.25 m* =  0.59 kg: held, creeping 0.407 mm/s
    0.50 m* =  1.18 kg: held, creeping 0.745 mm/s
    0.75 m* =  1.77 kg: held, creeping 1.081 mm/s
    0.95 m* =  2.24 kg: held, creeping 1.349 mm/s
    1.05 m* =  2.48 kg: dropped
  kp 3000 N/m, forcerange 30 N: N = 15.00 N per pad -> m* = 2 mu N / g = 4.59 kg
    0.25 m* =  1.15 kg: held, creeping 0.563 mm/s
    0.50 m* =  2.29 kg: held, creeping 1.079 mm/s
    0.75 m* =  3.44 kg: held, creeping 1.595 mm/s
    0.95 m* =  4.36 kg: held, creeping 2.007 mm/s
    1.05 m* =  4.82 kg: dropped
  one cube at 0.5 m* = 1.18 kg (kp 800), five contact settings:
    elliptic, impratio 10              held, creeping 0.745 mm/s
    elliptic, impratio 10, noslip 10   held, creeping 0.034 mm/s
    pyramidal, impratio 10             held, creeping 4.346 mm/s
    elliptic, impratio 1               dropped
    pyramidal, impratio 1              dropped
(4) which friction coefficient the pad contacts use
  pad priority 1, pad friction 1.5, cube friction 0.3: contact friction [1.5]
  pad priority 1, pad friction 1.5, cube friction 2.0: contact friction [1.5]
  pad priority 0, pad friction 1.5, cube friction 0.3: contact friction [1.5]
  pad priority 0, pad friction 1.5, cube friction 2.0: contact friction [2.0]
(5) twisting a held cube about the line through both pads
  pads with condim 4: the cube turns 2 degrees at 0.297 N m
  pads with condim 3: the cube turns 2 degrees at 0.247 N m

Reading the output:

[!warning] “Success within 5 mm of slip” depends on how long you wait The research-mode experiment judged success 0.5 s after the lift. With creep, a longer test gives a lower success rate for the same grasp: at the 0.75 mm/s measured for a 1.2 kg cube, a 5 mm slip tolerance is used up in under 7 s of holding. Define success with its time window, and report it.

Debugging

The object slips out as soon as the arm lifts. Compare the squeeze with the load: print actuator_force for the gripper and the pad normal forces, and compute $2\mu N / g$ against the object’s mass. Then check which $\mu$ the contact actually uses (data.contact.friction), and the cone and impratio.

Raising forcerange does nothing. The servo is not saturating (output 2). Raise kp, or command a target further inside the object.

The object is pushed sideways when the gripper closes. One finger reached it first and nothing coupled the other. Check that the equality exists and is active, and that both fingers move under one command.

A grasp that worked for 1 s fails in a 30 s hold. Creep. Measure the slip rate (output 3), then add noslip_iterations or raise the squeeze.

The closed gripper reports contact forces with nothing in it. The pads touch each other when fully closed. gripper.xml excludes that pair with <exclude body1="finger_left" body2="finger_right"/>; a model without it wastes solver effort and confuses touch sensing.

Exercise

Choose kp so that commanding $u = 0$ on the 40 mm cube produces 10 N on each pad with the 30 N limit left in place. Compute it from the formula above, set it in the lab, and check the readouts.

Challenge

Make the squeeze independent of object size. Test a soft and a saturating servo on cubes 20, 40 and 60 mm wide: tabulate the pad normal force and $m^\star$ for each, then verify $m^\star$ by holding cubes at 0.95 and 1.05 of it. Explain which design a learned policy would find easier to use, and why.

Research connection

[!research] Grasp success is partly a contact-model setting The measurements above show that the same gripper, cube and motion succeed or fail depending on cone, impratio and noslip_iterations, and that success at a fixed slip tolerance depends on the length of the hold. Manipulation results in simulation therefore depend on these settings as much as on the policy (inference from this lesson’s measurements), yet papers rarely report them. Level 21 treats them as part of a benchmark’s specification, and Level 18 identifies friction from data instead of guessing it.

{"id": "5.3-check", "title": "Knowledge check", "questions": [
  {"kind": "numeric", "q": "A tendon-driven gripper servo has kp = 1000 N/m and forcerange ±30 N. It is commanded fully closed (u = 0) on an object that stops the fingers at a tendon length of 0.015 m. What normal force does each pad apply, in N?",
   "answer": 7.5, "tol": 0.01, "unit": "N",
   "explain": "<p>$k_p L_c$ = 15 N, below the 30 N limit, so $F$ = 15 N. The tendon averages the two fingers, so each finger receives half: 7.5 N per pad.</p>"},
  {"kind": "mcq", "q": "In the course's gripper (kp = 800 N/m) holding the 40 mm cube, you raise forcerange from 30 N to 60 N. What happens to the squeeze?",
   "options": ["It doubles", "It rises by the ratio 60/30 only during the lift", "Nothing: the servo produces kp times the opening, 15.4 N, below either limit", "It drops, because the servo becomes unstable"],
   "answer": 2,
   "explain": "<p>A force limit acts only when the servo reaches it. Output (2) shows 15.44 N at kp = 800 against a 30 N limit.</p>"},
  {"kind": "numeric", "q": "Two pads with friction coefficient 1.2 each press with 10 N. What is the heaviest object (kg) they can hold at rest? (g = 9.81 m/s²)",
   "answer": 2.446, "tol": 0.005, "unit": "kg",
   "explain": "<p>$m^\\star = 2\\mu N / g = 24/9.81$ = 2.446 kg.</p>"},
  {"kind": "mcq", "q": "Pads have friction 1.0 and priority 0; the object has friction 0.4 and priority 0. Which sliding friction does their contact use?",
   "options": ["0.4", "0.7", "1.0", "0.4 × 1.0 = 0.4"],
   "answer": 2,
   "explain": "<p>At equal priority MuJoCo takes the element-wise maximum. With the pads at a higher priority, their value would be used whatever the object's.</p>"},
  {"kind": "predict", "q": "The cube weighs 1.2 kg, about half the static friction limit. You set impratio from 10 to 1 and run Grasp and lift. What happens?",
   "options": ["Nothing changes: below the friction limit, the cube is held", "It creeps a little faster but stays", "It slides out of the grasp", "The gripper fails to close"],
   "answer": 2,
   "explain": "<p>Output (3): with impratio 1, the same cube is dropped under both cones, while impratio 10 holds it with 0.75 mm/s of creep. A Coulomb calculation alone says it should hold; the soft friction model decides otherwise.</p>",
   "sim": {"model": "gantry_gripper", "key": 0, "height": 200, "camera": {"azimuth": -55, "elevation": 12, "distance": 0.55, "target": [0.12, 0, 0.17]},
           "setup": "ctx.t0 = null; model.body_mass[lib.bodyId('cube')] = 1.2; mj.mj_setConst(model, data)",
           "controls": [
             {"type": "button", "label": "Grasp and lift", "run": "mj.mj_resetDataKeyframe(model, data, 0); ctx.t0 = data.time"},
             {"type": "select", "label": "impratio", "set": "model.opt.impratio", "options": [["1", 1], ["10", 10]], "value": 1}],
           "controller": "if (ctx.t0 != null) { const t = data.time - ctx.t0; const s = (x) => { x = Math.min(Math.max(x, 0), 1); return x * x * x * (10 - 15 * x + 6 * x * x); }; data.ctrl[2] = t < 2.2 ? -0.33 * s(t) : -0.33 + 0.1 * s((t - 2.2) / 2); data.ctrl[3] = t < 1.3 ? 0.04 : 0; }",
           "readouts": [{"label": "cube height (m)", "expr": "lib.bodyPos('cube')[2]", "digits": 3}, {"label": "time since start (s)", "expr": "ctx.t0 == null ? 0 : data.time - ctx.t0", "digits": 1}]}}
]}

Next

Lesson 5.4 reuses one finger definition three times to build a hand, composes two arms into a bimanual cell with MjSpec, and drives six finger joints from one number.