Core concept

Lesson 5.3 asked whether a grasp holds an object at rest. Manipulation asks whether it holds while the object is moved, and the answer comes from the same friction budget, now spent on acceleration as well as weight. Two pads squeezing with normal force $N$ and friction coefficient $\mu$ can supply at most $2\mu N$ of tangential force; carrying a mass $m$ with acceleration $a$ needs $m\,|a - g|$ of it. Lifting adds to the demand, lowering subtracts from it, and moving sideways adds in quadrature.

Two practical facts make the budget harder to respect than the formula suggests. The acceleration the object actually feels is not the planned one but whatever the controller delivers, overshoot included. And slip is not all-or-nothing: a brief excess costs a fraction of a millimetre, a sustained one drops the object. This lesson measures both, then works through MuJoCo’s documented checklist for preventing slip.

[!established] MuJoCo’s checklist for slip, in its documented order First, the slip-preventing forces must lie inside the friction cone: enough normal force (maximum squeeze times $\mu$ well above the weight), a high enough $\mu$, and torsional friction (condim 4 or 6) when torques about the contact normal are needed. Second, the geometry must support the forces: enough contact points (keep multiccd and nativeccd enabled for convex meshes). Third, no high-frequency vibration: lower gains, add armature, use implicitfast or implicit against explicit damping. Last, slow slippage: elliptic cones with higher impratio, then NoSlip.

Visual intuition

Grasp and lift picks up the cube, raises it 3 cm, then lifts it a further 10 cm with a minimum-jerk profile of the duration you choose. The readouts compare the acceleration the carriage delivered with the grasp’s upward budget.

  1. With the default 1 s lift, the delivered peak is under 1 m/s² against a budget of about 13.7 m/s². The slip readout still creeps up to a couple of millimetres over the next seconds: that is creep (Lesson 9.3), not the lift.
  2. Shorten the lift to 0.2 s: the delivered peak rises above the budget for a moment, yet the slip ends up no larger than after the slow lift. A brief excess costs almost nothing.
  3. Shorten it to 0.15 s: the cube slides about a centimetre down between the pads. Raise the cube mass to 2 kg and repeat at 0.3 s: the budget shrinks with $m$ and the same lift now fails.

```lab simlab {“dock”: true, “title”: “Lifting fast”, “model”: “gantry_gripper”, “key”: 0, “height”: 250, “camera”: {“azimuth”: -55, “elevation”: 12, “distance”: 0.6, “target”: [0.12, 0, 0.2]}, “toggles”: [“contacts”, “forces”], “overlays”: {“contacts”: 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; ctx.peak = 0; ctx.off = null”}, {“label”: “lift duration”, “unit”: “s”, “param”: “T”, “min”: 0.1, “max”: 1.0, “step”: 0.01, “value”: 1.0, “digits”: 2}, {“label”: “cube mass”, “unit”: “kg”, “apply”: “model.body_mass[lib.bodyId(‘cube’)] = value; mj.mj_setConst(model, data); ctx.m = value”, “min”: 0.1, “max”: 2.4, “step”: 0.1, “value”: 1.0, “digits”: 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); }; let z = -0.33 * s(t); if (t > 1.9) z = -0.33 + 0.03 * s((t - 1.9) / 1.0); if (t > 3.2) z = -0.30 + 0.1 * s((t - 3.2) / ctx.T); data.ctrl[2] = z; data.ctrl[3] = t < 1.3 ? 0.04 : 0; const gap = lib.sitePos(‘gripper/tcp’)[2] - lib.bodyPos(‘cube’)[2]; if (t > 3.15 && ctx.off == null) ctx.off = gap; if (t > 3.2 && t < 3.5 + ctx.T) ctx.peak = Math.max(ctx.peak, Math.abs(data.qacc[2])); ctx.slip = ctx.off == null ? 0 : 1000 * (gap - ctx.off); }”, “readouts”: [ {“label”: “upward budget 2μN/m − g (m/s²)”, “expr”: “2 * 1.5 * lib.normalForce(‘gripper/pad_left’) / (ctx.m || 1) - 9.81”, “digits”: 2}, {“label”: “delivered peak acceleration (m/s²)”, “expr”: “ctx.peak || 0”, “digits”: 1}, {“label”: “slip since the lift began (mm)”, “expr”: “ctx.slip || 0”, “digits”: 2}, {“label”: “cube height (m)”, “expr”: “lib.bodyPos(‘cube’)[2]”, “digits”: 3}], “plots”: [{“ylabel”: “slip (mm)”, “window”: 6, “traces”: [{“label”: “cube slip relative to the fingertips (mm)”, “expr”: “ctx.slip || 0”}]}], “note”: “The budget uses the left pad’s normal force and the pads’ friction 1.5; it reads correctly only while the cube is grasped. The delivered peak is the carriage’s largest vertical acceleration during the lift, overshoot included.”}


## Mathematics

### The budget under acceleration

In the frame of the moving gripper, the object feels gravity plus an inertial force $-ma$. Both lie in the pads' tangent plane when the grasp axis is horizontal, as here, so the grasp holds while

$$m\,\|a - g\| \le 2\mu N .$$

For pure vertical motion this is $m(g + a_\uparrow) \le 2\mu N$, giving the largest upward acceleration $a_\uparrow = 2\mu N/m - g$. For pure horizontal motion the two add in quadrature: $a_\rightarrow = \sqrt{(2\mu N/m)^2 - g^2}$. With this gripper's 7.83 N per pad, $\mu$ = 1.5 and a 1 kg cube: 13.7 m/s² up, 21.3 m/s² sideways (output 1).

### From a planned motion to a delivered acceleration

A minimum-jerk move of distance $d$ in time $T$ has a peak acceleration of $5.77\,d/T^2$, so a budget becomes a shortest allowed duration: $T_\text{min} = \sqrt{5.77\,d/a_\text{max}}$, or 0.21 s for a 10 cm lift here. But the gantry's servo tracks the plan imperfectly, and its overshoot raised the delivered peak 25 to 70% above the planned one (outputs 2 and 3). Budget against the delivered acceleration, or leave a margin of at least that size.

### Brief excess versus sustained excess

Above the budget, the cube accelerates relative to the pads by the excess, and slip is that excess integrated twice over the time it lasts. A 0.2 s lift exceeded the budget by about 30% for a moment and slipped 0.45 mm, no more than creep. A 0.15 s lift exceeded it threefold and slipped 11 mm; a 0.1 s lift, 37 mm. The same holds sideways, where a 0.15 s carry dropped the cube.

> [!recommendation] Plan with the budget, then verify on the delivered motion
> Compute $a_\text{max}$ for the heaviest object and lowest $\mu$ you expect, choose durations with a margin for controller overshoot, and log the slip during evaluation as Lesson 10.2 does. If faster motion is needed, raise $N$ (stiffer servo or force-limited clamp, Lesson 5.3), raise $\mu$, or reorient the grasp so gravity is carried by normal force rather than friction (a gripper underneath, or a cage grasp).

### The slip audit

Output (4) evaluates the documented checklist for this grasp and passes every item: the friction available (11.7 N per pad) exceeds the weight per pad (4.9 N); pad priority fixes $\mu$ at 1.5; `condim` 4 provides torsional friction; four contact points per pad; `implicitfast`; elliptic cones with `impratio` 10. The audit is static: it says the grasp can hold the cube at rest, which is necessary and, as outputs (2) and (3) show, not sufficient for fast motion.

## Implementation

```io
INPUT: gantry_gripper.xml with a 1 kg cube
PROCESS: compute the friction budget; lift and carry with minimum-jerk motions of decreasing duration, recording the delivered acceleration and slip; evaluate the documented slip checklist
OUTPUT: printed tables

```python file=examples/l10_1_grasping.py “"”Lesson 10.1: grasping under motion: the friction budget, fast lifts and transports, a slip audit.

INPUT gantry_gripper.xml (parallel gripper on a Cartesian gantry), a 1 kg cube PROCESS (1) the grasp’s friction budget: pad normal force, friction coefficient, the static capacity and the largest vertical and horizontal accelerations a 1 kg cube can take; (2) lift the cube 0.1 m with minimum-jerk profiles of decreasing duration: planned and delivered peak acceleration, slip, outcome; (3) carry it 0.2 m sideways the same way; (4) the slip audit: MuJoCo’s documented checklist evaluated for this grasp OUTPUT printed tables

Run: python examples/l10_1_grasping.py “””

import math

import mujoco import numpy as np

from mjcourse import model_path

G, MASS, GRIP = 9.81, 1.0, 3 # cube mass (kg), gripper actuator index

def scene() -> tuple[mujoco.MjModel, mujoco.MjData]: model = mujoco.MjModel.from_xml_path(str(model_path(“gantry_gripper”))) model.body_mass[model.body(“cube”).id] = MASS data = mujoco.MjData(model) mujoco.mj_setConst(model, data) return model, data

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

def min_jerk(s: float) -> float: s = min(max(s, 0.0), 1.0) return 10 * s3 - 15 * s4 + 6 * s**5

def grasp(model, data) -> None: “"”Settle, descend 0.33 m, close, then raise 3 cm slowly so the cube hangs free.””” mujoco.mj_resetDataKeyframe(model, data, 0) run(model, data, 0.3) for k in range(100): data.ctrl[2] = -0.33 * min_jerk((k + 1) / 100) run(model, data, 0.01) run(model, data, 0.3) data.ctrl[GRIP] = 0.0 run(model, data, 0.6) for k in range(100): data.ctrl[2] = -0.33 + 0.03 * min_jerk((k + 1) / 100) run(model, data, 0.01) run(model, data, 0.3)

def pad_normal(model, data) -> tuple[float, float, int]: pad, cube = model.geom(“gripper/pad_left”).id, model.geom(“cube”).id total, mu, n, f = 0.0, 0.0, 0, np.zeros(6) for i in range(data.ncon): c = data.contact[i] if {c.geom1, c.geom2} == {pad, cube}: mujoco.mj_contactForce(model, data, i, f) total, mu, n = total + f[0], c.friction[0], n + 1 return total, mu, n

def move(axis: int, distance: float, duration: float) -> tuple[float, float, str]: “"”Move the gantry along one axis with a minimum-jerk profile, hold 0.5 s; return (delivered peak acceleration, slip in mm, outcome).””” model, data = scene() grasp(model, data) cube, tcp = model.body(“cube”).id, model.site(“gripper/tcp”).id offset = data.site_xpos[tcp] - data.xpos[cube] start, n = data.ctrl[axis], max(1, round(duration / model.opt.timestep)) peak = 0.0 for k in range(n + 250): data.ctrl[axis] = start + distance * min_jerk((k + 1) / n) mujoco.mj_step(model, data) peak = max(peak, abs(data.qacc[axis])) # the carriage’s actual acceleration slip = 1000 * float(np.linalg.norm((data.site_xpos[tcp] - data.xpos[cube]) - offset)) outcome = “dropped” if data.xpos[cube][2] < 0.025 else (“slipped” if slip > 5 else “held”) return peak, slip, outcome

def budget() -> tuple[float, float]: model, data = scene() grasp(model, data) normal, mu, n = pad_normal(model, data) capacity = 2 * mu * normal a_up = capacity / MASS - G a_side = math.sqrt((capacity / MASS) ** 2 - G**2) print(f” pad normal force {normal:.2f} N ({n} contacts per pad), contact friction {mu}, friction budget 2 mu N = {capacity:.2f} N”) print(f” static capacity 2 mu N / g = {capacity / G:.2f} kg; for the {MASS} kg cube: largest upward acceleration “ f”2 mu N / m - g = {a_up:.2f} m/s^2, largest horizontal sqrt((2 mu N / m)^2 - g^2) = {a_side:.2f} m/s^2”) return a_up, a_side

def sweep(axis: int, distance: float, durations, limit: float, name: str) -> None: print(f” {‘duration (s)’:>12}{‘planned peak (m/s^2)’:>22}{‘delivered peak (m/s^2)’:>24}{‘slip (mm)’:>11}{‘outcome’:>10}”) for duration in durations: planned = 5.7735 * distance / duration**2 # peak of a minimum-jerk acceleration peak, slip, outcome = move(axis, distance, duration) flag = “ (over budget)” if peak > limit else “” print(f” {duration:>12.2f}{planned:>22.1f}{peak:>24.1f}{slip:>11.2f}{outcome:>10}{flag}”) print(f” {name} budget: {limit:.2f} m/s^2”)

def audit() -> None: model, data = scene() grasp(model, data) normal, mu, n = pad_normal(model, data) pad = model.geom(“gripper/pad_left”).id force_limit = model.actuator_forcerange[GRIP, 1] checks = [ (“normal force: squeeze x mu vs weight”, f”{mu * normal:.1f} N of friction per pad in use, {mu * force_limit / 2:.1f} N “ f”if the servo saturated, weight per pad {MASS * G / 2:.2f} N”, mu * normal > MASS * G / 2), (“friction coefficient”, f”{mu} (pad priority {model.geom_priority[pad]} fixes it at the pad’s value)”, mu >= 1.0), (“torsional friction”, f”condim {model.geom_condim[pad]}, torsional coefficient {model.geom_friction[pad][1]} m”, model.geom_condim[pad] >= 4), (“contact points”, f”{n} per pad (box-box collider)”, n >= 3), (“integrator (explicit damping causes vibration)”, mujoco.mjtIntegrator(model.opt.integrator).name, model.opt.integrator in (int(mujoco.mjtIntegrator.mjINT_IMPLICITFAST), int(mujoco.mjtIntegrator.mjINT_IMPLICIT))), (“slow slippage: cone and impratio”, f”{mujoco.mjtCone(model.opt.cone).name}, impratio {model.opt.impratio:g}, “ f”noslip {model.opt.noslip_iterations}”, model.opt.cone == int(mujoco.mjtCone.mjCONE_ELLIPTIC) and model.opt.impratio > 1), ] for name, value, ok in checks: print(f” [{‘ok’ if ok else ‘check’}] {name:<48} {value}”)

if name == “main”: print(f”(1) the friction budget of the gantry grasp, {MASS} kg cube”) a_up, a_side = budget() print(“(2) lift 0.1 m, minimum-jerk”) sweep(2, 0.1, (1.0, 0.5, 0.3, 0.2, 0.15, 0.1), a_up, “upward”) print(“(3) carry 0.2 m along x, minimum-jerk”) sweep(0, 0.2, (1.0, 0.5, 0.3, 0.25, 0.2, 0.15), a_side, “horizontal”) print(“(4) slip audit: MuJoCo’s documented checklist for preventing slip, applied to this grasp”) audit()

Output:

```text
(1) the friction budget of the gantry grasp, 1.0 kg cube
  pad normal force 7.83 N (4 contacts per pad), contact friction 1.5, friction budget 2 mu N = 23.49 N
  static capacity 2 mu N / g = 2.39 kg; for the 1.0 kg cube: largest upward acceleration 2 mu N / m - g = 13.68 m/s^2, largest horizontal sqrt((2 mu N / m)^2 - g^2) = 21.34 m/s^2
(2) lift 0.1 m, minimum-jerk
  duration (s)  planned peak (m/s^2)  delivered peak (m/s^2)  slip (mm)   outcome
          1.00                   0.6                     0.8       0.96      held
          0.50                   2.3                     3.0       0.64      held
          0.30                   6.4                     8.3       0.51      held
          0.20                  14.4                    17.9       0.45      held (over budget)
          0.15                  25.7                    44.3      11.03   slipped (over budget)
          0.10                  57.7                    69.7      36.92   slipped (over budget)
  upward budget: 13.68 m/s^2
(3) carry 0.2 m along x, minimum-jerk
  duration (s)  planned peak (m/s^2)  delivered peak (m/s^2)  slip (mm)   outcome
          1.00                   1.2                     1.5       0.96      held
          0.50                   4.6                     6.1       0.64      held
          0.30                  12.8                    16.5       0.52      held
          0.25                  18.5                    23.5       0.48      held (over budget)
          0.20                  28.9                    50.4       5.36   slipped (over budget)
          0.15                  51.3                    79.5      38.23   dropped (over budget)
  horizontal budget: 21.34 m/s^2
(4) slip audit: MuJoCo's documented checklist for preventing slip, applied to this grasp
  [ok] normal force: squeeze x mu vs weight             11.7 N of friction per pad in use, 22.5 N if the servo saturated, weight per pad 4.91 N
  [ok] friction coefficient                             1.5 (pad priority 1 fixes it at the pad's value)
  [ok] torsional friction                               condim 4, torsional coefficient 0.02 m
  [ok] contact points                                   4 per pad (box-box collider)
  [ok] integrator (explicit damping causes vibration)   mjINT_IMPLICITFAST
  [ok] slow slippage: cone and impratio                 mjCONE_ELLIPTIC, impratio 10, noslip 0

Debugging

A grasp holds when lifted slowly and drops on fast moves. Compare the delivered acceleration with $2\mu N/m - g$ (and its horizontal counterpart). Slow the motion, raise $N$, or change the grasp.

Slip grows steadily during long holds or slow motions. Creep, not acceleration (Lesson 9.3): it scales with hold time, not with speed. Raise impratio or enable NoSlip.

The object rotates in the grasp during a sideways move. The inertial force does not pass through the line between the pads, so it makes a torque about the grasp axis. Add torsional friction (condim 4), grasp closer to the centre of mass, or slow down.

The grasp fails only on the real robot. The simulated $N$, $\mu$ and controller overshoot differ from the robot’s; identify them (Level 18) before tuning motion speed in simulation.

Exercise

For a 0.5 kg cube, compute the upward and horizontal budgets and the shortest minimum-jerk durations for a 0.1 m lift and a 0.2 m carry, allowing 50% controller overshoot. Check them with the lesson’s script by changing MASS.

Challenge

Design the fastest pick-and-carry of the 1 kg cube over 0.3 m that never exceeds 80% of the budget in delivered acceleration, by shaping the motion profile (for example a trapezoidal acceleration limit instead of minimum jerk, or a feedforward term on the gantry servo that reduces its overshoot). Report the time saved against a minimum-jerk profile of the same safety margin.

Research connection

[!research] Speed limits learned policies should know about Learned pick-and-place policies are often evaluated at fixed slow speeds and then run faster on hardware. The budget above makes the limit explicit: it depends on mass, squeeze, friction and the controller’s overshoot, all of which vary between objects and robots. A policy that learned fast motions on light simulated objects can drop heavy real ones with no change in its inputs (inference). Randomizing mass and friction (Level 17) or giving the policy access to the grasp’s force state are two ways to make that limit visible to it.

{"id": "10.1-check", "title": "Knowledge check", "questions": [
  {"kind": "numeric", "q": "Two pads with μ = 1.2 each press with 10 N on a 1.5 kg object. What is the largest upward acceleration (m/s²) the grasp can sustain? (g = 9.81)",
   "answer": 6.19, "tol": 0.02, "unit": "m/s^2",
   "explain": "<p>$2\\mu N/m - g = 24/1.5 - 9.81$ = 6.19 m/s².</p>"},
  {"kind": "numeric", "q": "Same grasp, moving horizontally. What is the largest horizontal acceleration (m/s²)?",
   "answer": 12.65, "tol": 0.03, "unit": "m/s^2",
   "explain": "<p>$\\sqrt{(2\\mu N/m)^2 - g^2} = \\sqrt{16^2 - 9.81^2}$ = 12.64 m/s².</p>"},
  {"kind": "mcq", "q": "A minimum-jerk lift is planned with a peak acceleration 20% under the budget, yet the object slips. What is the most likely reason?",
   "options": ["The friction coefficient changed", "The controller's overshoot delivered a higher acceleration than planned", "Gravity is larger in simulation", "The integrator is RK4"],
   "answer": 1,
   "explain": "<p>Outputs (2) and (3): delivered peaks ran 25 to 70% above planned ones.</p>"},
  {"kind": "mcq", "q": "The slip audit passes every item, but the object still drops during a fast carry. Why is that not a contradiction?",
   "options": ["The audit is wrong", "The audit checks the grasp at rest; motion adds inertial load to the same friction budget", "The audit ignores friction", "Fast motions disable contacts"],
   "answer": 1,
   "explain": "<p>The documented checks make holding possible in principle; the acceleration still has to fit in the budget.</p>"}
]}

Next

Lesson 10.2 builds a whole pick-and-place on the 7-DOF arm: a state machine over task-space targets, success judged from contacts rather than the plan, and a success rate measured over randomized initial states.