Skip to content

03 · Contact: soft constraints, solref and solimp

Part1 · Simulation
Run typeCPU · Mac or Colab CPU
Prerequisites02
Learn firstNumerical Stability
Time~40 min
Codeexperiments/03-contact/experiment.py
Status✅ done

Act on this lesson

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

MuJoCo contact is a stiff, damped virtual spring: you choose how soft it is, and the timestep decides whether it stays stable.

In lesson 02 a cube went through a 1 cm tabletop even at dt = 1 ms: the contact was detected on the right side, but it let the cube sink about 3 cm. Every MuJoCo contact interpenetrates a little. The Menagerie SO-101 sets its own gripper contact parameters (solref="0.01 1", condim="6"), values its README describes as working well for manipulation (so101.xml, README). This lesson measures what those two numbers do: how deep a resting cube sinks, how high a dropped cube bounces, and what happens when the contact is stiffer than the timestep can integrate.

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

1. You double the solref timeconst of a resting contact. How much deeper does the body sink?

  • Twice as deep, because the spring is twice as soft.
  • About four times as deep, because the resting penetration grows with timeconst².
  • Not at all; timeconst only changes how fast it settles.

Why: r=(1−d) au (dw/d)2 timeconst2 dampratio2r = (1-d)\,a_u\,(d_w/d)^2\,\text{timeconst}^2\,\text{dampratio}^2. In Sweep 1 the sphere goes from 1.2262 mm at 0.05 s to 4.9050 mm at 0.1 s, both at dd = 0.95.

2. A dropped cube bounces and you want it to stop bouncing. Which number do you change?

  • Raise dampratio to 1 or more.
  • Lower timeconst well below 2·dt.
  • Raise the solimp width.

Why: dampratio is MuJoCo’s restitution knob. In Sweep 3 the rebound falls from 56.504 mm at 0.1 to 0.000 mm at 1.0; a timeconst below 2·dt is replaced by refsafe anyway.

Real objects cannot overlap. Forbidding every overlap exactly would mean a hard, jumpy problem at every step, so MuJoCo does something softer: when two shapes overlap, it puts an invisible spring with a shock absorber between them. The deeper the overlap, the harder it pushes back. Objects always sink in a little, and you choose how much.

Two knobs describe that spring. solref says how fast it undoes an overlap (its time constant: small = stiff) and how strongly it is damped (its damping ratio: below 1 it bounces). solimp says how much of the spring’s push the solver may apply (the impedance, between 0 and 1, rising with depth).

The catch is time. A spring that undoes an overlap in less than about two timesteps overshoots on every step and gains energy. That is why MuJoCo quietly refuses time constants shorter than two timesteps.

flowchart TD
    O["overlap r and its speed v"] --> R["solref: reference spring + damper<br/>wants acceleration −b·v − k·r"]
    O --> I["solimp: impedance d(r)<br/>how much of it may be applied"]
    R --> F["contact force"]
    I --> F
    F --> S["integrator step of length h"]
    S -- "new overlap" --> O
def solref_to_spring(timeconst, dampratio, d, d_w):
b = 2 / (d_w * timeconst) # damping of the reference spring (1/s)
k = d / (d_w**2 * timeconst**2 * dampratio**2) # stiffness of the reference spring (1/s²)
return b, k
# What the solver aims for on one contact: a1 + d * (b * v + k * r) == (1 - d) * a_u
# At rest a1 = v = 0: the resting penetration (Build it, Step 3 solves it exactly)
timeconst_used = max(timeconst, 2 * h) # the refsafe guard (h = timestep)
QuantityFormula
soft constraint (one contact)a1+d (b v+k r)=(1−d) aua_1 + d\,(b\,v + k\,r) = (1 - d)\,a_u
reference spring from solrefb=2dw timeconst,k=d(r)dw2 timeconst2 dampratio2b = \dfrac{2}{d_w\,\text{timeconst}}, \quad k = \dfrac{d(r)}{d_w^2\,\text{timeconst}^2\,\text{dampratio}^2}
resting penetrationr=(1−d) au (dwd)2 timeconst2 dampratio2r = (1-d)\,a_u\,\Big(\dfrac{d_w}{d}\Big)^2\,\text{timeconst}^2\,\text{dampratio}^2
spring frequencyω=1/(timeconst⋅dampratio)\omega = 1/(\text{timeconst}\cdot\text{dampratio})
stability guard (refsafe)timeconst used =max⁡(timeconst,2h)= \max(\text{timeconst}, 2h)

rr is the penetration (how far the shapes overlap), vv its rate of change, aua_u the acceleration the body would have without the contact (gravity, here gg), and a1a_1 the acceleration the solver produces along the contact normal. d∈(0,1)d \in (0,1) is the impedance from solimp: it rises from d0d_0 at r=0r = 0 to dwd_w at rr = width along a smooth S-curve. timeconst and dampratio are the two solref numbers, kk and bb the stiffness and damping they imply, ω\omega the spring’s natural frequency and hh the timestep.

Math details: resting penetration, the refsafe guard and its gap

Resting penetration. At rest a1=v=0a_1 = v = 0, so d k r=(1−d) aud\,k\,r = (1-d)\,a_u; substituting kk gives the table’s formula (Modeling · Solver parameters). With constant impedance (d=dwd = d_w) it becomes the docs’ r=au(1−d) timeconst2 dampratio2r = a_u (1-d)\,\text{timeconst}^2\,\text{dampratio}^2. Penetration grows with timeconst²: twice as soft sinks four times as deep. Because dd depends on rr, the exact value is where both relations agree (Build it, Step 3).

Why refsafe exists. The docs state that timeconst should be at least twice the timestep, otherwise the system becomes too stiff for the integrator and can go unstable; MuJoCo enforces this unless the refsafe flag is disabled (Reference, option/flag/refsafe). It is the step-size limit of lesson 02 applied to the contact spring; Build it, Step 4 computes where it sits.

The gap in the guard. “Time constant” here is the inverse of natural frequency times damping ratio, so ω=1/(timeconst⋅dampratio)\omega = 1/(\text{timeconst}\cdot\text{dampratio}). refsafe clamps timeconst, not the product. A small dampratio therefore raises ω\omega past the limit even with the guard on. (That holds for Euler, implicitfast, implicit and RK4; under discrete, refsafe instead replaces rows with timeconst·dampratio < hh, see option/flag/refsafe.)

Two geoms, one contact. When two geoms touch, their solref/solimp are averaged (weights solmix) unless one has higher priority; margins and gaps are summed (Contact parameters).

Go deeper: Numerical Stability (AIEFS: why explicit integration of a stiff spring blows up); MuJoCo Modeling · Solver parameters.

AttributeNumbers (default)Meaning
solreftimeconst dampratio (0.02 1)the reference spring above. Larger timeconst = softer. Negative values = direct -stiffness -damping
solimpd0 dw width midpoint power (0.9 0.95 0.001 0.5 2)d(r)d(r) rises from d0d_0 at r=0r = 0 to dwd_w at rr = width (1 mm) along a sigmoid
margin0contact becomes active (forces) at distance < margin
gap0contacts between margin and margin + gap are reported but produce no force
condim31 frictionless · 3 sliding friction · 4 + torsional · 6 + rolling

Sources: Reference (solref), Impedance (solimp), geom/condim, margin and gap. condim is the number of force directions a contact can use: 1 is a pure push along the normal, 3 adds sliding friction in two directions, 4 and 6 add twisting and rolling resistance.

You will now write MuJoCo’s contact law for one contact in about 50 lines of NumPy, drop a sphere with it, and check each step against MuJoCo’s constraint data. The body is the Experiment’s single-contact sphere (5 cm radius, 1 kg, dt = 2 ms) with condim="1", so one contact is exactly one constraint row. 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/03-contact/build.py runs all four steps with the checks (output in build_output.txt).

solimp becomes the impedance d(r)d(r): two power curves that meet at midpoint and climb from d0d_0 to dwd_w over width. solref becomes the reference spring’s stiffness KK and damping BB, with the refsafe guard built in:

def impedance(r, solimp):
"""MuJoCo's impedance d(r) for penetration r >= 0: d0 at r = 0 rising to dwidth at r = width."""
d0, dw, width, mid, power = solimp
x = min(r / width, 1.0)
if x >= 1.0:
return dw
if x <= mid: # two power curves joined at the midpoint
y = x**power / mid ** (power - 1)
else:
y = 1 - (1 - x) ** power / (1 - mid) ** (power - 1)
return d0 + y * (dw - d0)
def solref_kb(solref, dmax, h, refsafe=True):
"""Stiffness K and damping B of the reference spring, as MuJoCo stores them (efc_KBIP[:, :2])."""
timeconst, dampratio = solref
if refsafe:
timeconst = max(timeconst, 2 * h) # the guard: never stiffer than two timesteps
return 1 / (dmax**2 * timeconst**2 * dampratio**2), 2 / (dmax * timeconst)

MuJoCo stores KK, BB and dd of every active constraint row in d.efc_KBIP after mj_forward:

== Step 1. Impedance and reference spring vs efc_KBIP (sphere placed at penetration r) ==
timeconst r (mm) d ours d MuJoCo K ours K MuJoCo B ours B MuJoCo
0.001 0.5 0.9250 0.9250 69252.1 69252.1 526.32 526.32
0.004 0.5 0.9250 0.9250 69252.1 69252.1 526.32 526.32
0.020 0.2 0.9040 0.9040 2770.1 2770.1 105.26 105.26
0.020 0.5 0.9250 0.9250 2770.1 2770.1 105.26 105.26
0.020 2.0 0.9500 0.9500 2770.1 2770.1 105.26 105.26
0.100 0.5 0.9250 0.9250 110.8 110.8 21.05 21.05

KK is the table’s kk without its factor d(r)d(r); MuJoCo applies dd when it builds the reference acceleration. Rows 1 and 2 are identical: timeconst 0.001 s was raised to 2h2h = 0.004 s.

The reference acceleration is aref=−Bv−Kd (dist)a_\text{ref} = -B v - K d\,(\text{dist}), with dist < 0 while penetrating. The solver blends it with the free acceleration a0a_0 through the impedance and never lets the contact pull:

def contact_accel(dist, vel, a0, solref, solimp, h, refsafe=True):
"""Normal acceleration of one frictionless contact (dist < 0 = penetrating, a0 = acceleration without it)."""
if dist >= 0:
return a0
d = impedance(-dist, solimp)
k, b = solref_kb(solref, solimp[1], h, refsafe)
a_ref = -b * vel - k * d * dist # what the reference spring-damper asks for
return max(a0, (1 - d) * a0 + d * a_ref) # a contact can push, never pull

Plug it into lesson 02’s semi_implicit_euler as accel and drop the sphere from 10 cm:

== Step 2. One contact by hand vs MuJoCo ==
at r = 0.5 mm, v = -0.3 m/s: a_ref ours 32.860111 m/s^2 · efc_aref 32.860111 m/s^2
10 cm drop, 2 s: peak penetration (mm) rebound (mm) max |z_ours - z_mujoco| (m)
dampratio 1.0: 8.726 0.000 2.4e-13
dampratio 0.3: 4.859 14.519 1.4e-13
dampratio 0.1: 2.282 57.912 8.7e-12

Your 1-D sphere follows MuJoCo’s to 1e-11 m or better: for one frictionless contact, the constraint solver reduces to these lines. The Experiment’s cube sinks less (8.283 mm at dampratio 1): four corner contacts share its weight.

At rest a1=v=0a_1 = v = 0, so the constraint reads d k r=(1−d) aud\,k\,r = (1-d)\,a_u; because dd depends on rr, solve it by bisection:

def resting_penetration(a_u, solref, solimp, h):
"""Penetration where the contact holds a body still: d k r = (1 - d) a_u, solved for r by bisection."""
def excess(r): # > 0 once the spring pushes more than needed
d = impedance(r, solimp)
k, _ = solref_kb(solref, solimp[1], h)
return d * d * k * r - (1 - d) * a_u
lo, hi = 0.0, 1.0
for _ in range(200):
mid = (lo + hi) / 2
lo, hi = (lo, mid) if excess(mid) > 0 else (mid, hi)
return (lo + hi) / 2
== Step 3. Resting penetration: bisection on d k r = (1 - d) a_u vs the resting sphere (mm) ==
timeconst ours MuJoCo, 2 s at rest
0.001 0.0175 0.0175
0.002 0.0175 0.0175
0.004 0.0175 0.0175
0.005 0.0273 0.0273
0.010 0.1078 0.1078
0.020 0.3672 0.3672
0.050 1.2263 1.2262
0.100 4.9050 4.9050

The Experiment’s sphere column, every row. At 0.05 s the bisection gives 1.2263 mm against 1.2262 mm after 2 s: that softest-but-one contact is still settling by a tenth of a micrometre.

Near rest, Step 2’s law is a linear spring-damper, a≈−dBv−d2Kra \approx -d B v - d^2 K r. Lesson 02’s step_growth measures its growth per step, and your 1-D drop shows what the sphere does:

== Step 4. Growth per step of the linearized contact spring (d = 0.95) vs the 10 cm drop ==
timeconst dampratio refsafe h*w growth highest bottom (m, from 0.100): ours / MuJoCo
0.0005 1.00 off 4.00 22.314 6.687 / 6.687
0.0010 1.00 off 2.00 6.464 13.753 / 13.753
0.0020 1.00 off 1.00 1.618 1.786 / 1.786
0.0030 1.00 off 0.67 0.699 0.100 / 0.100
0.0010 1.00 on 0.50 0.750 0.100 / 0.100
0.0200 0.10 on 1.00 0.894 0.100 / 0.100
0.0200 0.07 on 1.43 0.894 0.100 / 0.100
0.0200 0.05 on 2.00 1.740 0.345 / 0.345

Growth above 1 means every step amplifies the motion: exactly the rows that launch the sphere, in your code and in MuJoCo. The limit is lower than lesson 02’s hω<2h\omega < 2: the contact’s damping is integrated explicitly, which pulls the bound down to h2ω2<4−2hch^2\omega^2 < 4 - 2hc (lesson 05 derives it), about hω<0.83h\omega < 0.83 at dampratio 1. That is why 0.002 s (hωh\omega = 1.00) explodes and 0.003 s survives, and why refsafe’s 2h2h (hωh\omega = 0.50) is safe. Lowering dampratio raises ω\omega but not the damping: at 0.05 the growth is 1.740 with refsafe on. The Experiment’s cube also rebounds slightly above its drop height at 0.07 (0.106 m), which this one-contact sphere does not.

physai.sim now has impedance, solref_kb, contact_accel and resting_penetration, and you reused semi_implicit_euler and step_growth. Lesson 04 adds friction to the same contact. tests/test_03_contact.py pins them to MuJoCo; run uv run pytest tests/test_03_contact.py after you change anything.

In MuJoCo you set the two knobs per geom (or per default class, or per <pair>) and read the resulting constraint back:

<geom ... solref="0.01 1" solimp="0.9 0.95 0.001 0.5 2" condim="3" priority="1"/>
mujoco.mj_forward(m, d)
d.efc_pos[:d.nefc] # signed distance per row (negative = penetrating)
d.efc_KBIP[:d.nefc] # K, B, impedance d and its slope per row (Step 1)
d.efc_aref[:d.nefc] # reference acceleration (Step 2)
f = np.zeros(6)
mujoco.mj_contactForce(m, d, 0, f) # contact 0's force in its contact frame: normal first

Traps:

  • Rows are not contacts. With condim="3" and the default pyramidal cone one contact has four rows (lesson 04), and their dd, KK, BB are per row. Index efc_* through d.contact[i].efc_address.
  • Two geoms, one value. Mixed solref/solimp are averaged by solmix unless one geom has higher priority, as the SO-101 gripper does (Math details).
  • Negative solref switches to direct stiffness and damping, solref="-stiffness -damping", the form the docs recommend for system identification.

Check after building · 2 questions.

1. In Build it, Step 1, timeconst 0.001 s and 0.004 s give the same KK = 69252.1 and BB = 526.32 in efc_KBIP. Why?

  • The impedance dd saturates at 0.95 and hides the difference.
  • refsafe raises any timeconst below 2h2h = 0.004 s to 0.004 s before computing KK and BB.
  • MuJoCo stores KK and BB rounded to the timestep.

Why: solref_kb applies max(timeconst, 2 * h) and matches efc_KBIP in every row; the Experiment’s sweep 1 shows the same plateau, 0.0175 mm for 0.001, 0.002 and 0.004 s.

2. In Build it, Step 4, timeconst 0.002 s with refsafe off has hωh\omega = 1.00, below lesson 02’s limit of 2, yet its growth per step is 1.618 and the sphere is thrown to 1.786 m. Why?

  • The contact’s damping is integrated explicitly, which lowers the limit to h2ω2<4−2hch^2\omega^2 < 4 - 2hc, about hω<0.83h\omega < 0.83 at dampratio 1.
  • Lesson 02’s limit applies only to RK4.
  • The sphere hits the plane faster than 4.37 m/s.

Why: Near rest the contact is a≈−dBv−d2Kra \approx -dBv - d^2Kr; with dampratio 1 the damping term is as large as the spring term, and an explicit damper shrinks the stable region. 0.003 s (hωh\omega = 0.67) survives, and refsafe’s 2h2h gives hωh\omega = 0.50.

A 10 cm, 1 kg cube (and, as a single-contact check, a 5 cm radius, 1 kg sphere) on a plane, dt = 2 ms, the same solref on both geoms:

<geom type="plane" size="1 1 .1" solref="{tc} {dr}"/>
<body pos="0 0 {z0}">
<freejoint/>
<geom type="box" size=".05 .05 .05" mass="1" solref="{tc} {dr}"/>
</body>

rest() places the body touching the plane and reads the contact distance after 2 s. drop() releases it 10 cm above the plane and records the deepest penetration and the rebound height.

Predict. If timeconst doubles, how much deeper does the cube sink?

Run. experiment.py, MuJoCo 3.17.0, NumPy 2.5.4, Apple M5, deterministic; output in output.txt.

Sweep 1 · timeconst → steady penetration (dampratio 1):

timeconst (s)cube (mm)sphere (mm)ddequation above (mm)
0.0010.00440.01750.90000.0175
0.0020.00440.01750.90000.0175
0.0040.00440.01750.90000.0175
0.0050.00680.02730.90010.0273
0.010.02730.10780.90120.1078
0.02 (default)0.10780.36720.91350.3672
0.050.49141.22620.95001.2263
0.11.22634.90500.95004.9050
  • The sphere (one contact) matches the steady-state equation to four digits in every row; the constant-dd form matches only once rr exceeds the 1 mm width (0.05 and 0.1 s), where d=dwd = d_w = 0.95.
  • The cube has four corner contacts, each carrying a quarter of the weight. Where both sit at the same dd (0.001–0.01 s and 0.1 s) it sinks a quarter as much as the sphere; at 0.02 and 0.05 s its shallower penetration sits lower on the solimp sigmoid (smaller dd), so it sinks relatively more.
  • 0.001, 0.002 and 0.004 s give identical results: below 2h2h = 0.004 s refsafe replaces the value.

Sweep 2 · impedance (solimp="d d 0.001", timeconst 0.02): penetration 0.4905 / 0.0981 / 0.0490 / 0.0098 mm for dd = 0.5 / 0.9 / 0.95 / 0.99, i.e. proportional to 1−d1-d.

Sweep 3 · dampratio → 10 cm drop (timeconst 0.02):

dampratiopeak penetration (mm)rebound (mm)
0.12.22056.504
0.23.50324.202
0.34.62913.766
0.56.2363.339
0.77.2880.055
1.08.2830.000
2.09.7230.000

Left: resting penetration against solref timeconst on log-log axes; sphere points lie on the steady-state equation, cube points sit lower, and everything below 0.004 s is flat because refsafe raises the value. Right: rebound height after a 10 cm drop against dampratio, falling from 56.504 mm at 0.1 to zero from 1.0 on.

dampratio is MuJoCo’s restitution knob (how bouncy an impact is): below 1 the cube bounces, from 1 on it does not. A softer timeconst at dampratio 1 sinks deeper on impact: 0.297, 3.295, 8.283, 23.248, 48.314 mm for 0.004, 0.01, 0.02, 0.05, 0.1 s. At its deepest, a 1 kg cube dropped 10 cm onto the default contact is 8.283 mm “into” the floor; at rest, 0.1078 mm.

1 · timeconst below 2·dt, refsafe off (dampratio 1, 10 cm drop, dt = 2 ms):

timeconstrefsafepeak pen. (mm)max height reachedjitter, last 0.5 s (mm)
0.0005–0.004on0.2970.100 m (start)0.000
0.0005off0.2977.834 m497.278
0.001off2.32713.809 m2084.273
0.002off6.3080.894 m267.969
0.003off0.5780.100 m (rebound 2.581 mm)0.000
0.004off0.2970.100 m0.000

Jitter is the standard deviation of the cube’s height over the last 0.5 s, when it should lie still. With the guard on, every timeconst below 0.004 s silently becomes 0.004 s. With it off, the contact injects energy: a 1 kg cube dropped from 10 cm is thrown up to 13.809 m. At 0.003 s (1.5·dt) it survives but rebounds 2.581 mm at dampratio 1, which should not bounce.

2 · the gap in the guard: refsafe on, timeconst 0.02, small dampratio. With ω=1/(timeconst⋅dampratio)\omega = 1/(\text{timeconst}\cdot\text{dampratio}):

dampratiohωh\omegarebound (mm)max height
0.101.0056.5040.100 m
0.071.43105.8910.106 m
0.052.003146.9793.147 m

At 0.07 the cube rebounds higher than it was dropped from; at 0.05 (hωh\omega = 2) it is launched to 3.147 m. Outside discrete, refsafe checks only timeconst, so a bouncy contact can blow up with the guard on. Keep timeconst⋅dampratio\text{timeconst}\cdot\text{dampratio} well above dt.

  • Menagerie SO-101: arm collision geoms use condim="3" and default solref; gripper collision geoms (class collision_gripper) use condim="6", friction="1 5e-3 5e-4", solref="0.01 1" and priority="1", so their parameters win over the object’s (so101.xml). At the model’s timestep="0.005", 0.01 s is exactly 2h2h, the stiffest value refsafe allows.
  • The real gripper pads are 3D-printed plastic; no compliance or restitution value for them is published in the sources used here. Not yet measured on the real arm. Procedure: drop a known object onto the pad from fixed heights, film the rebound, fit dampratio; press it with a known force, measure deflection, fit timeconst (or the direct -stiffness -damping form, see Use it).

Quick check · 2 questions. Click an answer.

1. With refsafe on, timeconst 0.02 and dampratio 0.05, a 10 cm drop launches the cube to 3.147 m. Why did the guard not stop it?

  • A small dampratio means more damping, and damping adds energy.
  • timeconst 0.02 is already below 2·dt, so the spring itself is too stiff.
  • refsafe clamps only timeconst, but the spring frequency is ω=1/(timeconst⋅dampratio)\omega = 1/(\text{timeconst}\cdot\text{dampratio}), so hωh\omega reaches 2.00.

Why: Break it 2, the gap in the guard; outside discrete, keep timeconst·dampratio well above dt.

2. In sweep 1, at timeconst 0.001–0.01 s, the 10 cm cube sinks about a quarter as deep as the sphere (for example 0.0068 vs 0.0273 mm at 0.005 s). Why a quarter?

  • The cube is four times heavier per unit area.
  • solref is averaged over four geoms.
  • The cube rests on four corner contacts, each carrying a quarter of the weight at the same impedance dd, and penetration is proportional to the load aua_u.

Why: The resting penetration (1−d) au (dw/d)2 timeconst2 dampratio2(1-d)\,a_u\,(d_w/d)^2\,\text{timeconst}^2\,\text{dampratio}^2 is linear in the load. Where the cube’s shallower contacts sit lower on the solimp curve (0.02 and 0.05 s), dd differs and the ratio breaks.

  1. Easy. With default solimp, what timeconst puts a resting 1 kg sphere at 1 mm penetration? Use the equation, then check in the script.
  2. Medium. Set margin="0.001" on the plane. Where does the cube rest relative to the plane surface, and why?
  3. Hard. At dt = 5 ms (the SO-101 timestep), repeat Break it 1 and 2. Which timeconst and dampratio values become unsafe?
  1. Above 1 mm d=dwd = d_w = 0.95, so solve 9.81⋅0.05⋅timeconst2=0.0019.81 \cdot 0.05 \cdot \text{timeconst}^2 = 0.001; the table brackets it between 0.02 and 0.05 s.
  2. The impedance and reference now act on distance − margin, so the cube floats just under 1 mm above the plane at the old resting penetration, without touching it. Measure it.
  3. refsafe raises any timeconst below 0.01 s. For the rest, run Build it’s step_growth with hh = 0.005 on each setting before you simulate: growth above 1 is unsafe.
  • Contact is a soft constraint; steady penetration is (1−d) au (dw/d)2 timeconst2 dampratio2(1-d)\,a_u\,(d_w/d)^2\,\text{timeconst}^2\,\text{dampratio}^2, verified to four digits for a single contact.
  • dampratio sets bounce; timeconst sets softness. A 10 cm drop on the default contact penetrates 8.283 mm at the deepest point.
  • Keep timeconst ≥ 2·dt (refsafe enforces it) and timeconst·dampratio well above dt (only the discrete integrator’s refsafe enforces it).

Next: lesson 04 · Friction and friction cones.

Sources