Robot scenes take 1.25 ms physics steps unless you ask for a coarser one¶
What is wrong¶
A scene with a robot steps its rigid bodies at the fixed step: 1/600 s snapped down to the grid dt / 2**j, which is
1.25 ms at dt=0.01 and at dt=0.005. So one control period costs 8 physics steps at dt=0.01 and 4 at
dt=0.005, also for a robot whose servos and contacts would be stable at a larger step. Scenes of free rigid bodies
only already pick the largest stable step by themselves (docs/guide/worlds.md); robot scenes do not.
The larger step is opt-in. Set it before building:
scene.rigid_step = 0.0025 # pin a step in seconds (snapped down to the grid dt / 2**j)
scene.rigid_step = "auto" # or: the largest step every cap below allows, never under the fixed step
sim.routes() and alloy.estimate(scene, worlds, seconds)["rigid_step"] name the step and what bound it. A scene
that never sets rigid_step builds and steps exactly as before.
The caps a robot adds (the free bodies beside it keep their own):
- servo: for every position servo, omega dt <= max(1, zeta), with omega = sqrt(kp / I), zeta = (kv + joint
damping) / (2 sqrt(kp I)) and I the joint's inertia at the start pose, armature included. The cap reads each joint alone at the start pose: a folded arm has a smaller inertia, and the servos together have a stiffer mode than any one joint (1.73x on the LEAP hand, 1.34x on the Panda, 1.16x on the Go2, 1.00x on FR3, UR10e and G1, the worst of 200 poses). The cap sits a factor 2 or more below the stability limit, which covers both. Past it a step
response is off by more than about a tenth of the step; the servo is unstable at 2 zeta + 2 sqrt(zeta^2 + 1).
- damping: joint damping and a servo's kv weaken a contact's push on the joint by 1 / (1 + dt b / I),
b = kv + joint damping. 1 + dt b / I may be at most 1.25 times its value at 1.25 ms, so a link's contact sinks
at most 25 % deeper than at the fixed step. This is the cap that holds stiff, heavily damped arms.
- solref: dt <= tau / 2 for the time constant of every contact shape, joint limit and equality row the robot file
writes. MuJoCo raises a smaller tau to 2 dt, so those rows get softer by (tau / 2 dt)^2.
- link_gap: v dt <= the robot's contact gap (5 mm, or add_robot(..., contact_gap=)): a link travels at most
its contact gap per step, so a contact starts at most that deep; v is the speed of the farthest collision point
below one joint at 2 rad/s (0.5 m/s for a slide, or the file's joint velocity limit when that is lower). Several
joints moving together give a faster tip.
- loose_body: a loose free body in the robot file (a box or a mug beside an arm) must not get a softer contact than
at 1.25 ms. Alloy stiffens a light loose body's contact as far as the step allows, so a light one holds 1.25 ms.
"auto" takes the largest grid step that passes all of them. A pinned step is taken as asked; the build then warns once and names each cap it breaks, the joint or shape, and the cost (for example "contacts 0.25x as stiff as at 1.25 ms").
Number¶
- Physics steps per control period at the fixed step: 8 at
dt=0.01, 4 atdt=0.005; withrigid_step = 0.005atdt=0.005: 1. - The shipped Go2 (
emb_e8_go2): "auto" gives 2.5 ms, half the physics steps. The damping cap binds: at 5 ms a calf's contact would sink 1.48x deeper than at 1.25 ms. link_gap allows 5 ms (its feet reach 0.32 m from the hip axis: 7.8 ms), its 20 ms default time constants 10 ms and its servos 18 ms. - The shipped Panda (
emb_e2_panda): "auto" stays at 1.25 ms. Two caps hold it: damping (its servos'kvof 450 on a joint inertia of 0.22 kg m2: contacts on its links would sink 1.72x deeper at 2.5 ms, 3.15x at 5 ms) and loose_body (the box, cylinder and mug in its file: 0.25x as stiff at 2.5 ms, 0.06x at 5 ms). link_gap gives 3.2 ms (it reaches 0.79 m from joint 2), the finger coupling's 5 ms time constant 2.5 ms, its servos 5 ms.
Rows (dt=0.005; the step pinned with scene.rigid_step; speed against the same robot at 1.25 ms):
| scene | 2.5 ms (2 steps per period) | 5 ms (1 step per period) |
|---|---|---|
| speed, 1,024 worlds on one L4: Panda, FR3, UR10e, LEAP | 1.85-1.90x | 3.3-4.2x |
| speed, 1,024 worlds on one L4: Go2 | 1.66x | 2.45x |
| speed, 4 worlds on the CPU, six robots | 1.75-1.97x | 2.8-4.0x |
| arms moving freely: joints against the 1.25 ms run | within 0.009 rad | within 0.027 rad |
| loose box, cylinder, mug beside an arm: end position | within 0.05 mm | within 0.12 mm |
| LEAP hand: cube stays in the hand (1,024 worlds) | all, 0.2 mm off | all, 0.6 mm off |
| Go2 standing (1,024 worlds) | all stand, base 0.02 mm off | all stand |
| G1: joints against the 1.25 ms run, standing / moving | 0.04 / 0.26 rad | 0.08 / 0.44 rad |
| gripper holding a thin plate: slip (0.57 mm at 1.25 ms) | 1.28 mm | 4.22 mm |
| gripped chain: pad penetration (0.10 mm at 1.25 ms) | 0.34 mm | 0.72 mm |
| pick and place, 2,560 episodes: episodes with a penetration glitch (0 at 1.25 ms) | 12 (0.47 %) | 360 (14 %) |
No world raised a health flag in the free-motion and standing rows. Reading: a robot that moves, reaches or stands is as good at 2.5 ms and close at 5 ms; a robot that grips is not, and the build's warning says so (damping, solref). Under "auto" only the Go2 leaves 1.25 ms (2.5 ms) among the shipped robots: the damping cap holds the arms, the hands and both humanoids. G1 at 1,024 worlds and every robot at 4,096 worlds ran out of memory on a 24 GB L4 at every step, so they have no GPU rows.
A pinned step that breaks the damping or solref cap is not for a robot that grips: at 2.5 ms 0.47 % of pick-and-place episodes had a penetration glitch and at 5 ms 14 % (0 at 1.25 ms).
Why¶
The fixed step was chosen once for all robots: stiff position servos, thin fingers and contacts written for 1.25 ms. Whether a given robot is stable and as accurate at a larger step depends on its servo gains, its link inertias and the contact time constants in its file, which the caps above now read. Making the larger step the default would change every robot scene's motion, so it stays a choice.
Workaround¶
- Set
scene.rigid_step = "auto", or pin a step, and readsim.routes()for the step and the cap that bound it. After a pinned step, read the build's warning before trusting grasps and contact depths. - A robot file that writes 2.5 ms time constants on its own shapes, limits or equality rows stays at 1.25 ms under "auto". To step it coarser without softer contacts than the file intends, write time constants of at least twice the step you want.
- Light loose objects in the robot file hold "auto" at 1.25 ms. Add them with
scene.add_boxand the like instead: scene objects have their own caps, and a light one still holds the fixed step. - Check the run as always:
sim.check()["ok"]andsim.health().
Planned fix¶
Not fixed yet:
- the default: a robot scene that never sets rigid_step keeps 1.25 ms;
- arms held at 1.25 ms by their servos' damping, by 2.5 ms contact time constants in their files and by the light
loose objects in them;
- a robot beside cloth, liquid, soft bodies, sand or paste, a kinematic object, a precision fit or an attachment
keeps the fixed step, and so do the precision and debug profiles under "auto"; rigid_step = "fast" is not offered
for robots;
- a servo on a tendon and joint limits written as stiffness and damping (URDF, USD) are not checked by the caps,
and a loose body in the robot file is checked for its contact stiffness only, not for how far it moves in a step.
Making "auto" the default for robots is proposed (it changes only the Go2 among the shipped robots) and waits on a ruling.