Skip to content

04 · Friction and friction cones

Part1 · Simulation
Run typeCPU · Mac or Colab CPU
Prerequisites03
Learn firstVectors, Matrices & Operations
Time~40 min
Codeexperiments/04-friction/experiment.py · notebook
Status✅ done

Act on this lesson

Tick each step when you have done it. Saved in this browser only.

Slip is decided by a ratio, not a weight: tan θ = μ, and the simulated cone has to approximate it.

Unitree’s open base training config (the one its Go2 config inherits) randomizes floor friction but not mass: each of num_envs = 4096 parallel copies draws its friction from friction_range = [0.5, 1.25], with randomize_base_mass = False (legged_robot_config.py). For slip, friction is the right knob: whether a foot or a grasped object slips depends on the friction coefficient and on the direction of the contact force, not on how heavy the object is. This lesson measures the slip angle of a cube on a ramp in MuJoCo, checks it against Coulomb’s law, and finds the two places where the simulation deviates from it: creep below the threshold, and a pyramidal friction cone that depends on direction.

Before you read · 2 questions. Guess first; the lesson tells you why.

1. On the same ramp (μ = 0.3), does a 10 kg cube slip at a larger angle than a 0.1 kg cube?

  • Yes, a heavier cube presses harder into the ramp and gets a bigger friction budget.
  • No, the downhill pull and the friction budget both scale with mm, so the slip angle depends only on μ.
  • No, the lighter cube holds longer because it has less momentum.

Why: tan⁡θ>μ\tan\theta > \mu has no mass in it. In the Experiment table all three masses slip at 16.75° for μ = 0.3 (theory 16.70°).

2. Two cubes, 0.1 kg and 10 kg, slide down the same ramp above the slip angle. Which accelerates faster?

  • The 10 kg cube, because gravity pulls it harder.
  • The 0.1 kg cube, because friction holds it less.
  • Neither; both accelerate at g(sin⁡θ−μcos⁡θ)g(\sin\theta - \mu\cos\theta), in which the mass cancels.

Why: Pull and friction budget both scale with mgm g (In plain words); Build it, Step 2 checks the formula against MuJoCo within 1.2 %, and the Experiment’s speed traces of 0.1 kg and 10 kg cubes differ by at most 2.1·10⁻¹² cm/s.

Friction is a budget. A surface can push sideways on an object, but only up to a limit, and that limit is proportional to how hard the object presses on the surface. The proportionality factor is the friction coefficient μ.

Put a phone on a book and tilt the book slowly. Tilting does two things at once: less of the phone’s weight presses into the book (smaller budget) and more of it pulls the phone downhill (bigger demand). At some angle the demand exceeds the budget and the phone slides. A heavy phone presses harder, but it is also pulled harder by exactly the same factor, so weight cancels: the slip angle depends only on μ.

A simulator has to represent that budget. The exact rule says the sideways force may point in any direction, up to the same limit, so the allowed forces form a round cone. MuJoCo’s default approximates the round cone with a pyramid, whose flat faces make the limit depend on direction. And because MuJoCo’s contact is soft (lesson 03), friction is soft too: below the limit an object can still creep slowly.

flowchart TD
    W["weight m·g"] --> N["pressing into the ramp<br/>m·g·cos θ"]
    W --> P["pulling downhill<br/>m·g·sin θ"]
    N --> B["friction budget<br/>μ · m·g·cos θ"]
    B --> C{"pull larger<br/>than budget?"}
    P --> C
    C -- "no" --> H["cube holds"]
    C -- "yes" --> S["cube slips"]
pull = m * g * sin(theta) # gravity component along the ramp (N)
budget = mu * m * g * cos(theta) # largest friction force the surface can supply (N)
slips = pull > budget # m and g cancel: tan(theta) > mu (Build it, Step 1)
def in_elliptic_cone(f_n, f_t1, f_t2, mu): # MuJoCo's round cone, condim 3
return f_n >= 0 and f_t1**2 + f_t2**2 <= (mu * f_n)**2
QuantityFormula
force pulling the cube downhillmgsin⁡θm g \sin\theta
friction limitμ mgcos⁡θ\mu\, m g \cos\theta
slip conditiontan⁡θ>μ\tan\theta > \mu
acceleration while slidinga=g(sin⁡θ−μcos⁡θ)a = g(\sin\theta - \mu\cos\theta)
elliptic conef1≥0, f12≥∑ifi2/μi−12f_1 \ge 0,\ f_1^2 \ge \sum_i f_i^2/\mu_{i-1}^2

mm is the cube’s mass, gg gravity, θ\theta the ramp angle and μ\mu the sliding friction coefficient; f1f_1 is the contact force along the surface normal and f2,f3f_2, f_3 (more with higher condim) the tangential components, each limited by its own coefficient μi−1\mu_{i-1}.

Math details: Coulomb's law on a ramp, and why the simulated cone deviates

Slip condition. Static friction can supply at most μN\mu N, where N=mgcos⁡θN = m g\cos\theta is the normal force on a ramp. The cube holds while mgsin⁡θ≤μ mgcos⁡θm g\sin\theta \le \mu\, m g\cos\theta. Dividing by mgcos⁡θm g\cos\theta gives tan⁡θ≤μ\tan\theta \le \mu: mass and gravity cancel (OpenStax University Physics 1, §6.2). Above the threshold the net downhill force is mg(sin⁡θ−μcos⁡θ)m g(\sin\theta - \mu\cos\theta), hence the acceleration in the table.

Cones. A contact force ff is admissible if it lies in the friction cone (Computation · Contact). The elliptic cone is the inequality in the table. The pyramidal cone is built from 2(n−1)2(n-1) non-negative edge forces, where nn is condim; for condim=3 that is 4 edges. A pyramid has flat faces, so its cross-section is a polygon, not a circle, and the friction limit it allows depends on the direction of sliding relative to the contact frame. The docs call elliptic cones the better model of physical reality, with pyramidal cones sometimes faster and more robust (option/cone).

Creep. The friction constraint is solved softly, like the normal one, so a small steady sliding speed below the threshold is allowed. The docs call this slow slippage and list two mitigations: elliptic cones with impratio > 1, or the noslip solver (Modeling · Slow slippage).

One μ. geom/friction holds three numbers (sliding, torsional, rolling; default 1 0.005 0.0001). There is one sliding coefficient, so no separate static and kinetic friction. For a contact pair, the element-wise maximum is used unless one geom has higher priority (geom/friction, Contact parameters).

Go deeper: Vectors, Matrices & Operations (AIEFS: splitting a force into components); OpenStax University Physics 1, §6.2; MuJoCo Computation · Contact.

option/coneDefinitionRows per condim=3 contact
pyramidal (default)2(n−1)2(n-1) non-negative edge forces of a pyramid4
ellipticf1≥0, f12≥∑fi2/μi−12f_1 \ge 0,\ f_1^2 \ge \sum f_i^2/\mu_{i-1}^23

A row is one scalar unknown the constraint solver has to find for that contact.

You will now write the Coulomb slip test, the sliding acceleration and MuJoCo’s pyramidal cone in about 40 lines of NumPy, and check them against MuJoCo’s motion, its constraint rows and a SciPy linear program. Type the steps into your own file and run it after each step. The finished functions are in physai/sim.py; uv run python experiments/04-friction/build.py runs all four steps with the checks (output in build_output.txt).

def ramp_slip(theta, mu, g=9.81):
"""Does a block on a ramp at angle theta slip (tan theta > mu), and its downhill acceleration if so."""
slips = np.tan(theta) > mu
return slips, g * (np.sin(theta) - mu * np.cos(theta)) if slips else 0.0

Apply it on the Experiment’s 0.05° grid, then apply the Experiment’s own criterion (fast and accelerating after 2 s) to this ideal block, which starts from rest with constant acceleration, so its mean speeds over 0.5–1.0 s and 1.5–2.0 s are 0.75a0.75a and 1.75a1.75a:

== Step 1. Slip angle: tan(theta) > mu, then the Experiment's criterion on an ideal Coulomb block ==
mu arctan mu first grid angle that slips ideal block + criterion
0.3 16.70° 16.70° 16.80°
0.5 26.57° 26.60° 26.65°
0.8 38.66° 38.70° 38.75°
1.0 45.00° 45.05° 45.05°

The Experiment measured 16.75°, 26.60°, 38.70° and 44.90°: one grid step before the ideal block passes the criterion (three at μ = 1). Just above arctan⁡μ\arctan\mu the ideal block accelerates so slowly that it is still below 2 cm/s after 2 s; the MuJoCo cube adds its creep speed (up to 1.42 cm/s, Observe) on top.

Above the threshold the block accelerates at g(sin⁡θ−μcos⁡θ)g(\sin\theta - \mu\cos\theta). The Experiment’s 80 cm ramp is too short to measure that for long, so tilt gravity instead: a cube on a flat floor with gravity (gsin⁡θ,0,−gcos⁡θ)(g\sin\theta, 0, -g\cos\theta) is the same physics with no edge to slide off. Fit x(t)x(t) with a parabola:

== Step 2. Sliding acceleration (mu = 0.5): g (sin - mu cos) vs MuJoCo, fit over 0.2-1.0 s ==
theta ours (m/s^2) elliptic pyramidal
28.0° 0.2747 0.2744 0.2745
30.0° 0.6571 0.6571 0.6567
35.0° 1.6088 1.6207 1.6117
45.0° 3.4684 3.4640 3.5092

Within 1.2 % at every angle, for both cones. The residue is not noise in the formula: a sliding box tips onto its leading edge and hops (at times it has no contact at all), so the fit averages a bumpy slide.

For condim=3 the pyramidal cone replaces each contact by four edge forces fk≥0f_k \ge 0 along n±μt1n \pm \mu t_1 and n±μt2n \pm \mu t_2. In constraint form each edge is a row Jn±μJtiJ_n \pm \mu J_{t_i}, and the contact force is their sum:

def pyramid_rows(j_normal, j_tangent, mu):
"""Constraint rows of MuJoCo's pyramidal cone: J_n + mu J_t and J_n - mu J_t for each tangent direction."""
return np.array([j_normal + s * m * jt for m, jt in zip(mu, j_tangent) for s in (1, -1)])
def pyramid_to_force(edge_forces, mu):
"""Contact-frame force (normal, tangential...) from the non-negative pyramid edge forces."""
f = np.asarray(edge_forces, float).reshape(-1, 2)
return np.r_[f.sum(), [m * (fp - fm) for m, (fp, fm) in zip(mu, f)]]

Build JnJ_n, JtJ_t from the contact frame (d.contact[i].frame, normal first) and the cube’s point Jacobian at the contact (mj_jac), then compare with MuJoCo’s efc_J and mj_contactForce:

== Step 3. Pyramid rows and edge forces vs MuJoCo (cube holding at 20°, mu = 0.5, pyramidal) ==
4 contacts x 4 rows = 16 rows · max |our rows - efc_J| = 0.0e+00
max |force from our edge sum - mj_contactForce| = 0.0e+00 N

Exact: that is how MuJoCo assembles a pyramidal contact.

Step 4 · How much friction, in which direction

Section titled “Step 4 · How much friction, in which direction”

Non-negative edge forces with unit normal force reach any tangential force with ∣f1∣+∣f2∣≤μ|f_1| + |f_2| \le \mu: a diamond, not a circle. Along a direction at angle ϕ\phi to the first tangent the limit is therefore μ/(∣cos⁡ϕ∣+∣sin⁡ϕ∣)\mu / (|\cos\phi| + |\sin\phi|):

def tangential_limit(phi, mu, cone):
"""Largest friction force per unit normal force when sliding at angle phi to the first tangent axis."""
if cone == "elliptic":
return mu
return mu / (abs(np.cos(phi)) + abs(np.sin(phi))) # pyramid: |f1| + |f2| <= mu f_n, a diamond
== Step 4. Largest friction force per unit normal force, by sliding direction (mu = 0.5) ==
phi elliptic pyramid (ours) pyramid (linprog)
0° 0.5000 0.5000 0.5000
15° 0.5000 0.4082 0.4082
30° 0.5000 0.3660 0.3660
45° 0.5000 0.3536 0.3536
MuJoCo: friction impulse / normal impulse on the cube over 0.2-1.0 s (world frame)
holding at 20° (tan 20° = 0.3640) elliptic 0.3640
holding at 20° (tan 20° = 0.3640) pyramidal 0.3640
sliding at 35° (mu = 0.5) elliptic 0.5000
sliding at 35° (mu = 0.5) pyramidal 0.5000

SciPy’s linear program over the four edge forces agrees at every angle. Along a diagonal the pyramid allows only μ/2\mu/\sqrt2, 0.3536 instead of 0.5. MuJoCo’s own forces obey the law: a holding cube uses exactly the friction it needs, tan⁡20°\tan 20° = 0.3640; a sliding cube sits on the cone’s boundary, ratio μ. Both cubes here slide along a tangent axis (ϕ=0\phi = 0), where the two cones agree. Whether the rotated cube of Break it slides at a different ϕ\phi was not isolated.

physai.sim now has ramp_slip, pyramid_rows, pyramid_to_force and tangential_limit. Lesson 16 needs the friction limit when the gripper squeezes an object. tests/test_04_friction.py pins them to MuJoCo; run uv run pytest tests/test_04_friction.py after you change anything.

In MuJoCo, friction is three numbers per geom plus two global options:

<option cone="elliptic" impratio="10"/> # round cone; impratio > 1 reduces creep (Math details)
<geom ... friction="1 0.005 0.0001" condim="3"/> # sliding, torsional, rolling; condim picks which act
f = np.zeros(6)
mujoco.mj_contactForce(m, d, i, f) # contact i: f[0] normal, f[1:3] friction, in its frame
f_world = d.contact[i].frame.reshape(3, 3).T @ f[:3]

Traps:

  • Contact frame, not world. mj_contactForce returns the force in the contact frame, normal first; rotate with frame.T before adding contacts or comparing with gravity (Step 4).
  • One μ for both bodies. MuJoCo takes the element-wise maximum of the two geoms’ friction unless priority decides (Math details): a slippery object on a μ = 1 gripper pad gets μ = 1.
  • Rows, not contacts. d.nefc counts rows: four per pyramidal condim=3 contact, three per elliptic one (Step 3 found 16 rows for 4 contacts).

Check after building · 2 questions.

1. In Build it, Step 4, the pyramidal cone with μ = 0.5 allows a friction force of only 0.3536 per unit normal force along the diagonal (ϕ\phi = 45°). Why?

  • The edge forces n±μt1n \pm \mu t_1, n±μt2n \pm \mu t_2 reach only ∣f1∣+∣f2∣≤μ|f_1| + |f_2| \le \mu, a diamond whose diagonal limit is μ/2\mu/\sqrt2.
  • MuJoCo halves μ for diagonal sliding to save computation.
  • The linear program did not converge at 45°.

Why: tangential_limit gives μ/(∣cos⁡ϕ∣+∣sin⁡ϕ∣)\mu/(|\cos\phi| + |\sin\phi|) and SciPy’s linprog over the four edge forces gives the same 0.5000, 0.4082, 0.3660, 0.3536 at 0°, 15°, 30°, 45°. The elliptic cone allows 0.5000 in every direction.

2. In Build it, Step 1, an ideal Coulomb block judged by the Experiment’s criterion slips from 16.80° (μ = 0.3), but the MuJoCo cube slipped from 16.75°. Why does the simulated cube pass the criterion one grid step earlier?

  • MuJoCo uses a smaller μ than the file says.
  • Just above arctan⁡μ\arctan\mu the ideal block is still below 2 cm/s after 2 s; the simulated cube adds its creep speed on top.
  • The 0.05° grid rounds the simulated angle down.

Why: The criterion needs v2v_2 > 2 cm/s; the ideal block’s v2=1.75av_2 = 1.75a is too small that close to the threshold. Below the threshold the MuJoCo cube already creeps at up to 0.30 cm/s for μ = 0.3 (Observe).

An 8 cm cube placed at rest on an 80 × 30 × 2 cm ramp, aligned with it; both geoms get the same μ.

def xml(theta_deg, mu, mass, cone="pyramidal"):
return f"""
<mujoco>
<option cone="{cone}"/>
<worldbody>
<body name="ramp" euler="0 {-theta_deg} 0">
<geom type="box" size=".40 .15 .01" friction="{mu} 0.005 0.0001"/>
</body>
<body name="cube">
<freejoint/>
<geom type="box" size=".04 .04 .04" mass="{mass}" friction="{mu} 0.005 0.0001"/>
</body>
</worldbody>
</mujoco>"""

2 s are simulated; v1v_1 and v2v_2 are the mean downhill speeds over 0.5–1.0 s and 1.5–2.0 s. Because of creep (slow sliding below the threshold, see Math details), “slipping” needs two conditions:

def slips(v1, v2):
return v2 > 2.0 and (v2 - v1) > 0.5 # cm/s: fast AND accelerating

The slip angle is the smallest angle from which every larger angle slips, on a 0.05° grid from 3° below to 1° above arctan⁡μ\arctan\mu.

Predict. Does a 10 kg cube slip at a larger angle than a 0.1 kg one?

Run. experiment.py, MuJoCo 3.17.0, Apple M5, dt = 2 ms, deterministic; output in output.txt.

μarctan⁡μ\arctan\mu0.1 kg1 kg10 kg
0.316.70°16.75°16.75°16.75°
0.526.57°26.60°26.60°26.60°
0.838.66°38.70°38.70°38.70°

Observe.

  • Mass has no effect: across the whole sweep, the speed traces of the 0.1 kg and 10 kg cubes differ by at most 2.1·10⁻¹² cm/s (rounding).
  • The measured angle is within one grid step of theory (largest deviation +0.05°). For μ = 1 the script gives 44.90° against 45.00°.
  • Creep. At least 1° below the threshold the cube still moves at a constant speed: up to 0.30, 0.60 and 1.42 cm/s for μ = 0.3, 0.5, 0.8, while v2−v1v_2 - v_1 stays below 0.05 cm/s. With a speed-only criterion (v2v_2 > 2 cm/s) the “slip angle” drops to 16.35° (−0.35°) and 26.25° (−0.32°) for μ = 0.3 and 0.5. Creep, not acceleration, is what that criterion detects.
  • The elliptic cone gives the same angles for the aligned cube (16.75°, 26.60°, 38.70°).

Rotate the cube 45° about the ramp normal. Coulomb friction is isotropic, so nothing should change:

μconeslip anglevs theorymax steady speed ≥ 0.5° below threshold
0.5pyramidal26.30°−0.27°7.40 cm/s
0.5elliptic26.60°+0.03°0.18 cm/s
0.8pyramidal37.70°−0.96°27.24 cm/s
0.8elliptic38.60°−0.06°0.34 cm/s

With the default pyramidal cone the rotated cube slips almost 1° early at μ = 0.8 and slides at up to 27.24 cm/s where it should hold. The elliptic cone stays within one or two grid steps. A plausible mechanism is that the pyramid’s edges are tied to the contact frame, so the admissible tangential force depends on direction, but this was not isolated here (box–box contact point placement also changes with rotation). Treat it as a measured warning: if friction limits matter (grasping, foot slip), use cone="elliptic" or verify with a sweep.

The speed curves behind these numbers, for μ = 0.8, show both deviations at once (plot.py re-runs the same sweep(); the per-angle speeds are not in output.txt):

Steady downhill speed against ramp angle for μ = 0.8 on a log axis. The aligned cube with the pyramidal cone creeps at a steady speed below the threshold (up to 1.42 cm/s) and accelerates from 38.70°; the 45°-rotated cube with the pyramidal cone already slides at several cm/s far below the threshold; the rotated cube with the elliptic cone stays slowest and slips from 38.60°, apart from one isolated angle just below.

  • The Menagerie SO-101 already uses cone="elliptic" and impratio="10", the slow-slippage mitigation above. Its gripper collision geoms use friction="1 5e-3 5e-4", condim="6" and priority="1", so their μ = 1 overrides the object’s (so101.xml). Whether μ = 1 matches the printed pads against a given object is not yet measured on the real arm.
  • Procedure (same as this experiment): put the object on a pad-material ramp, tilt slowly, record the slip angle 5 times, μ = tan θ. Real surfaces have a static coefficient larger than the kinetic one; MuJoCo’s single μ cannot represent that difference.

Quick check · 2 questions. Click an answer.

1. Below the threshold the simulated cube still moves at a small constant speed. What goes wrong if “slipping” just means “faster than 2 cm/s”?

  • Nothing; the creep is a rounding error far below 2 cm/s at every angle.
  • The measured slip angle comes out too large, because creep slows the cube down.
  • Creep is counted as slip, so the measured slip angle comes out too small.

Why: Soft friction creeps. Under Observe, the speed-only criterion gives 16.35° (−0.35°) for μ = 0.3 and 26.25° (−0.32°) for μ = 0.5; requiring acceleration fixes it.

2. The cube is rotated 45° about the ramp normal. Coulomb friction is isotropic, so nothing should change. What did the run show at μ = 0.8?

  • The default pyramidal cone slipped almost 1° early (37.70°); the elliptic cone stayed close to theory (38.60°).
  • Both cones gave the same angle; the cone only affects solver speed.
  • The elliptic cone slipped early and the pyramidal cone stayed exact.

Why: Break it table, pyramidal −0.96° with up to 27.24 cm/s of sliding where it should hold. If friction limits matter, use cone="elliptic" or check with a sweep.

  1. Easy. Set mass to 1000 kg. Predict the slip angle at μ = 0.5, then run --quick after editing.
  2. Medium. Give the ramp μ = 0.3 and the cube μ = 0.8. At which angle does it slip? Then add priority="1" to the ramp geom.
  3. Hard. For the policy trained on μ ∈ [0.5, 1.25], compute the slip-angle range on a ramp. Then measure, with the rotated cube and both cones, how far the pyramidal result can be off at μ = 1.25. Note it for lesson 39 · Domain randomization.
  1. Unchanged, 26.60° in this setup: mass cancels.
  2. Without priority MuJoCo uses the larger μ, 0.8, so expect about 38.7°; with priority on the ramp, μ = 0.3, about 16.75°. Run to confirm.
  3. In theory from arctan⁡0.5\arctan 0.5 (26.57°, printed by the script) to arctan⁡1.25\arctan 1.25; the rotated-cube deviation at μ = 1.25 must be measured, not extrapolated.
  • tan θ = μ, independent of mass; MuJoCo reproduces it within 0.05° for μ = 0.3–0.8 (44.90° vs 45.00° at μ = 1) when the slip criterion requires acceleration.
  • Soft contact creeps below the threshold (up to 1.42 cm/s here); a speed-only criterion mistakes creep for slip.
  • The default pyramidal cone is direction-dependent in this test (−0.96° at 45°); elliptic is not.

Next: lesson 05 · Joints and actuators.

Sources