Tight-Fit Assembly in Newton: Solvers & Tunnelling
Bring-up and solver investigation for two contact-rich insertion tasks — a
gear-on-shaft assembly and a DisplayPort cable insertion — on a Flexiv
Rizon 4s + Grav gripper in Isaac Lab / Newton. The goal is to run both tasks at a much larger time
step than the current 2 kHz soft-contact regime by moving to Newton's constraint-based solvers
(SolverVBD, SolverKamino), while quantifying throughput and detecting tunnelling
before committing to a two-day training run.
Generated 2026-07-28 · Newton 1.4.0.dev0 (ships SolverVBD +
SolverKamino) · Isaac Lab worktree newton/gear-dp-combined · NVIDIA RTX PRO 6000
Blackwell (MIG 1g.24 GB) · runs are Kit-free (no Isaac Sim).
default preset runs use_mujoco_contacts=True, and MuJoCo "compiles every mesh
geom through its convex-hull path" — so the teeth, and the shafts, are hulled away before the solver ever
runs. This is the wobbling, the non-meshing and the gear sliding off the fixture, and no solver setting
could have fixed it. Full study below.
Where things stand
- ✓ Combined the gear-assembly and DisplayPort-insertion branches into one worktree (both tasks + LEAPP export).
- ✓ Rebuilt a Kit-free environment (Newton needs no Isaac Sim); both tasks build and step; asset resolution works over S3/CloudFront.
- ✓ Verified video rollouts render through Newton's
ViewerGL.get_frame()into an mp4, fully headless. - ✓ Wrote the missing VBD solver manager and added
newton_vbd/newton_kaminopresets; dt / substep / solver sweeps run from the CLI with no code edits. - ✓ Collision filtering via per-body Newton collision groups — and then fixed two bugs in it that were silently disabling the contacts it selected.
- ✓ Forced-insertion proxy — a scripted Jacobian push that presses the plug into the socket (worst-case contact moment); doubles as the close-up video driver.
- ✓ Tunnelling detector and a direct vertex-vs-SDF overlap measurement, both calibrated against the task's own geometry.
- ✓ Passive-stability and gear-train checks — the gear seats on its shaft to within 0.03 mm and stays.
- ✓ Fixed maximal-coordinate read-back — VBD/XPBD/Kamino were never surfacing their state, so a diverging scene scored as perfectly stable.
- ✓ Isolated gear-train study — the meshes are fine, the solvers are fine; the collider representation was the problem. XPBD and MuJoCo-with-Newton-contacts both drive the train at the exact ratios, from 1000 Hz down to 100 Hz.
- ✓ Made VBD finite on this scene — its rigid path needs
rigid_avbd_beta > 0; with the default fixed-penalty setting it goes non-finite within 25 ms. It is still too dissipative to drive the train. - ◻ Move the task off MuJoCo's convex-hull contact path, then re-run the dt sweep (1000→200→100 Hz) on the real scene.
Rollouts
All videos are rendered fully headless through Newton's ViewerGL.get_frame() into mp4 (no
Isaac Sim / Kit). The close-ups use the forced-insertion proxy — a scripted Jacobian push that presses
the grasped part straight down into the socket / onto the shaft.
DisplayPort insertion — close-up. The gripper presses the plug (dark connector) down toward the socket. Shown for scale only: DisplayPort contact is still broken (the plug passes through the socket), so nothing physical should be read from this clip.
Gear assembly — the gear threaded onto its shaft and resting on the plate, 0.03 mm from its designed rest height. Earlier versions of this clip showed the gear sliding past the fixture; they were wrong and have been replaced rather than kept.
Wide overview (4 environments) of the DisplayPort task — the Flexiv Rizon 4s arms holding the plug at the grasp pose, for scale and context.
The two tasks (as shipped)
| Setting | Gear assembly | DisplayPort insertion |
|---|---|---|
| Physics dt / decimation | 1 ms · 33 | 1 ms · 33 |
| Effective solver rate | 1 kHz × 2 substeps = 2 kHz, control at 30.3 Hz | |
| Default solver | Newton + MuJoCo internal contacts (soft/penalty), implicit-fast, elliptic cone | |
| Envs · episode | 4096 · 6.66 s | |
| Held part | gear (small/medium/large) | DisplayPort plug (30 g), ~0.27 mm blade clearance |
The soft-contact model is what forces the tiny time step: stiff penalty contacts need ~2 kHz to stay
stable and avoid tunnelling. Newton's SolverVBD and SolverKamino instead solve
contacts as constraints, so they should tolerate 100–200 Hz collision handling — the premise of this
investigation.
Key finding: contact count is the bottleneck
With collisions left unfiltered, the DisplayPort scene generates a huge number of contacts per body — the plug against the socket's full SDF cavity, plus robot links. Running VBD surfaces this immediately as per-body contact-buffer overflow; Kamino cannot even build.
Per-body rigid contact counts reported during a VBD rollout of the DisplayPort task (n = 578 overflow events, 4 envs). Median 681, max 820 — all far above the 256-contact per-body VBD buffer (dashed line). Every overflow silently drops contacts, which is exactly how tunnelling creeps in.
| Solver preset | Builds? | Steps? | Observation |
|---|---|---|---|
default (MuJoCo) | yes | yes | Baseline; soft contacts absorb the large contact set. |
newton_vbd | yes | yes | Runs, but floods contact buffer overflowed 820 > 256 → dropped contacts. |
newton_kamino | no | — | Crashes at construction: contact-sizing integer overflow (array shape −101886780). |
Both failure modes share one root cause and one fix: collision filtering. Restricting
collisions to the pairs that matter — plug ↔ socket and fingers ↔ plug, with robot
self-collision, robot-link↔socket, and cross-environment contacts switched off — should cut the contact
count by an order of magnitude, stop VBD dropping contacts, and let Kamino build. Only then are the dt sweep
and tunnelling evaluation meaningful.
Per-step throughput at 4 environments after collision filtering (higher is better), with the pre-filter number in parentheses. VBD jumps ~10× once it stops dropping contacts; Kamino now runs at all (it previously crashed at construction). Absolute numbers are small at 4 envs — the real comparison happens at scale.
Collision filtering: the fix
The fix is a small, general mechanism: a NewtonCollisionFilterCfg that assigns Newton
collision groups per body before the model is finalized. Newton's group rule — a body in a negative
"hub" group collides with any positive group, but positive groups don't collide with each other — expresses
the desired whitelist exactly: the plug is the hub (group −1), the socket
(group +1) and the gripper fingers (group +2) each collide with the plug but not with
one another, and everything else (robot arm, links, ground) is disabled (group 0). It is
O(number of shapes) and never pairs shapes across environments.
| Solver preset | Before filter | After filter (plug↔socket + plug↔finger) |
|---|---|---|
newton_vbd | runs, but 578 buffer-overflow events/rollout (drops contacts); ~2 env-steps/s | 0 overflow, ~21 env-steps/s (~10×) |
newton_kamino | build crash — contact-sizing integer overflow | builds & steps (~1 env-step/s; P-ADMM is heavy) |
gear_orientation_exceeded
termination, which measures the gear's absolute orientation relative to the link7 wrist frame.
Under Newton the gripper settles the grasped gear at ~23° relative roll, so it crossed the tight 30°
roll/pitch threshold one step after every reset — in both the play and the training
environment (i.e. it would have quietly broken a real training run). Widening the threshold to 60° clears
the stable grasp orientation and drops the gear to the same sporadic, workable reset rate as DisplayPort.
The scripted demo also disables episode terminations, so the rollout videos never reset mid-push.
The gear task gets the same collision treatment (gears are the hub; they collide with the
base / shafts and finger tips, not with one another). The gear teeth are intrinsically contact-dense —
~500 contacts per environment even after filtering, across the three shaft-pinned gears — which initially
overflowed Newton's auto-sized global contact buffer. For sub-mm-precision assembly the answer is to
keep every contact, not thin them out, so we over-provision the buffer instead: a new
rigid_contact_max_scale multiplier on Newton's auto-estimate (which itself scales with the
environment count). At 3× the gear runs VBD and Kamino with zero dropped contacts — full
contact fidelity, and still env-count-scalable.
Forced insertion: measuring tunnelling (retracted)
Tunnelling — the grasped part passing through the socket instead of seating against it — is the
failure that forces these tasks down to a 1 ms time step. To measure it rather than argue about it, the
probe tracks the signed insertion depth: the distance from the seat plane to the part's tip
along the insertion axis. A correct insertion drives that depth toward 0 and stops —
solid parts cannot go further. Two signals are flagged: through-penetration (depth ends
>2 mm below the seat) and a fly-through jump (>5 mm of depth change in one
control step, ending below the seat).
The depth zero-point is verified against the task's own mate helpers: the authored mated pose
gives depth 0.0000 m, the authored reset pose (68 mm clearance) gives
+0.0680 m. To keep the measurement independent of grasp quality, the part is
fired at the shaft from a known 30 mm gap at a set impact speed — the same approach as
Newton's RJ45 demo, which drives its plug directly rather than through a gripper. Gear task, 4 environments,
dt = 1 ms.
Environments that tunnelled, by solver and impact speed. MuJoCo shows a clean onset — it holds the gear at 0.5 m/s, loses half the environments at 2 m/s, and all of them at 8 m/s. The hydroelastic preset fails at every speed. VBD's 0% is hatched because it is not a pass: see the travel column below.
Insertion depth of the deepest environment during the 2 m/s impact. Zero is the seat plane; below it (shaded) is inside solid material. MuJoCo descends, chatters around the seat and dips ~9 mm into it; hydroelastic sails straight through and keeps going (off-scale, reaching −5.3 m); VBD never moves at all.
Full results
| Solver | Impact speed | Tunnelled | Min depth | Travel | Verdict |
|---|---|---|---|---|---|
| Hydroelastic (SDF) | 0.5 m/s | 4/4 | -4012.0 mm | 4020.1 mm | no contact at all |
| MuJoCo (default) | 0.5 m/s | 0/4 | 17.2 mm | 7.7 mm | contact held the gear |
| VBD | 0.5 m/s | 0/4 | 30.0 mm | 0.0 mm | invalid — part never moved |
| Hydroelastic (SDF) | 2.0 m/s | 4/4 | -5299.0 mm | 5257.6 mm | no contact at all |
| MuJoCo (default) | 2.0 m/s | 2/4 | -8.9 mm | 12.6 mm | half the envs pushed ~9 mm into the shaft |
| VBD | 2.0 m/s | 0/4 | 30.0 mm | 0.0 mm | invalid — part never moved |
| Kamino | 2.0 m/s | 4/4 | -7.5 mm | 10.6 mm | held the gear, but 7.5 mm inside the shaft |
| Hydroelastic (SDF) | 8.0 m/s | 4/4 | -10446.8 mm | 10207.4 mm | no contact at all |
| MuJoCo (default) | 8.0 m/s | 4/4 | -7062.2 mm | 6936.6 mm | blasted straight through |
| VBD | 8.0 m/s | 0/4 | 30.0 mm | 0.0 mm | invalid — part never moved |
Travel is how far the part actually moved. It is the honesty check that separates "contact stopped the part" from "the solver never moved it" — without it, a frozen body looks like perfect tunnelling immunity.
1. VBD state handling was wrong — fixed — but VBD still diverges. Under
newton_vbd the whole scene read back frozen at its initial pose. The cause was an
architectural mismatch: VBD, XPBD and Kamino are maximal-coordinate solvers — they integrate
body_q/body_qd directly and never touch joint_q — yet forward
kinematics was re-run from those stale joint coordinates on every state refresh, overwriting the solver's
own result. The fix is to treat body state as authoritative: seed it from joint coordinates once,
then re-run FK only for articulations whose joints were explicitly rewritten (a reset), and read
observations and rewards from body coordinates. (The second state buffer was also left uninitialized for
double-buffered solvers; it is seeded now too.) Generalized-coordinate solvers are bit-identical after the
change. VBD now integrates — but the articulated scene diverges (bodies reach kilometre
scale within a few steps), so VBD numbers are still not comparable; solver-parameter work is next.
2. The hydroelastic preset generates no contact here, letting the gear free-fall through the base at every speed.
collide=True in the right groups), and it happens
with the collision filter both on and off. The plug also is never really gripped: the task closes the hand
to hand_close_width = -0.1, and the fingers settle at exactly that target with zero deviation —
nothing between them. So the DisplayPort task cannot currently produce a meaningful insertion, which is why
the quantitative comparison above runs on the gear.
Getting the gear scene physically right
The geometry, measured rather than assumed
Reading the collider meshes directly settles what the scene is supposed to do:
| Feature | Measured |
|---|---|
| Base plate | z ∈ [−18.8, −11.0] mm → 7.8 mm thick, top face at −11.0 mm |
| Shafts | z ∈ [−11.3, +18.8] mm, radius 7.1 mm (30 mm tall) |
| Gear bore | radius 7.9 mm → 0.8 mm radial clearance on the shaft |
| Gear teeth | small 20T · medium 40T · large 60T, circular pitch ≈ 4.9–5.2 mm (module ≈ 1.6 mm) |
| Centre distances | large↔medium 75.7 mm, medium↔small 45.8 mm → ≈ 1 mm backlash |
So a correctly seated gear has its centre at +7.5 mm (bottom face resting on the plate),
exactly the task's own seated_gear_z_offset, and the three gears are a matched, meshable
set. Any failure to mesh is therefore a simulation problem, not a modelling one.
Why the gear went through the floor
- My drop height was above the shaft. The shaft tips end at +18.8 mm and a seated gear centre sits at +7.5 mm, so "drop it 15 mm above the seat" put the bore above the shaft tip entirely. The gear was never threaded, and simply fell past the fixture. Released at its seated pose, or dropped ≤5 mm, it threads on and stays: tilt 0.00°, resting 0.03 mm from the seat, 0.05 mm/s.
- The plate is penetrable under modest impact. Dropped 8 mm the gear ends 9.5 mm below its seat; dropped 11 mm, 22.6 mm below — it passes clean through a 7.8 mm plate. Raising the hydroelastic SDF resolution helps only marginally (−22.6 → −21.9 → −17.4 mm for 64 → 128 → 256), and 512 exhausts the GPU. This is a genuine contact-fidelity limit of the current setup, not a placement mistake.
Correctly seated. The gear descends over the shaft — the shaft tip is visible in the bore — and rests flat on the plate, where it stays.
Gear-train drive test. The large gear is spun at 3 rad/s while its neighbours are left free. They barely move — see below.
Does the assembly actually work?
Settling is far too weak a check: a gear lying under the fixture is also at rest. The real question is
whether the thing functions as a gear train, so gear_mesh_test.py seats all three gears, spins
one, and measures whether the others turn — checking direction and the ratio their radii imply,
not merely that something moved. It also fails when the driven gear itself does not turn, so a dead drive
cannot quietly pass.
| Gear | Measured ω | Expected ω | Stayed on its shaft |
|---|---|---|---|
| large (driven, 60T) | +2.14 rad/s | — (input) | within 1.5 mm |
| medium (40T) | +0.13 rad/s | −3.2 rad/s | within 1.5 mm |
| small (20T) | −0.04 rad/s | +6.4 rad/s | within 2.2 mm |
Where the meshing actually breaks
It would be easy to blame SDF resolution and stop. Measuring each link in the chain separately shows that is not it:
| Question | Measurement | Verdict |
|---|---|---|
| Are the gears a meshable set? | 20/40/60 teeth, circular pitch 4.9–5.2 mm, centre distances give ~1 mm backlash | yes |
| Does the SDF represent the teeth? | sampling the built SDF around the rim gives a clean 60 cycles/rev, 0.49 mm amplitude (1.53 mm peak-to-peak) | yes |
| Do the teeth actually touch? | driven-vs-neighbour overlap peaks at 0.97 mm, in contact 68 of 80 steps | yes |
| Is tangential load transmitted? | neighbour reaches 4 % of the meshing rate, in the wrong direction | no |
A caution on that SDF check: sampled at the gear's mid-height it looks featureless, because that height is the hub. The toothed rim sits at z ≈ −17 mm, and sampled there the teeth are unmistakable. An earlier reading of this as "the SDF has no teeth" was simply the probe in the wrong place.
Every lever on the contact stack was tried:
| Change | Neighbour ω (expect −3.2) | Effect |
|---|---|---|
| SDF resolution 64 → 128 → 256 | 0.21 → 0.41 → 0.64 rad/s | improves, then stalls; 512 exhausts the GPU |
normal_matching=False | +0.02 | overlap rises to 2.6 mm, still no drive |
reduce_contacts=False | +0.04 | gears explode (53 m) |
| Plain SDF contacts (no hydroelastic) | +0.01 | unstable; gears leave the fixture |
| Applied torque instead of prescribed spin | 0.00 | nothing moves at all |
The practical consequence for training: a policy here can seat a gear on its shaft, and that part is now sound, but nothing in the physics rewards or penalises whether the teeth engage. Insertion is simulated; meshing is not. Newton's threaded nut-and-bolt example is the closest working reference and runs plain SDF contacts under XPBD rather than MuJoCo, which is the next configuration to try — the option to build mesh SDFs without hydroelastic is now in place for it.
XPBD and VBD — and a bug that hid everything
Trying XPBD and VBD on this scene turned up something more serious than the meshing question: maximal-coordinate solvers were never being read back at all.
They advance body_q/body_qd directly. But Isaac Lab reads a floating base's
pose out of joint_q — literally
get_root_transforms() → joint_q[0:7] — which those solvers never write. So every
observation, reward and sensor reported the pose the scene started in, regardless of what the solver did.
Measured side by side, Newton's own body_q moved ~40 mm per step while the read-back sat
frozen to five decimal places:
| Step | Newton body_q z | Isaac Lab read-back z |
|---|---|---|
| 0 | +0.07065 | +0.06250 |
| 2 | +0.07738 | +0.06250 |
| 5 | +0.07340 | +0.06250 |
Recovering the joint coordinates from the body state after each step (inverse kinematics, the companion
to the forward pass) closes the gap exactly — body_q, the bound array and the read-back now
agree to every digit. MuJoCo is untouched and still bit-identical.
What the two solvers actually do
| Solver | Passive seating | Gear train |
|---|---|---|
| MuJoCo (hydroelastic SDF) | seats, 0.00° tilt, 0.03 mm from designed height | teeth touch but transmit ~4 % |
| XPBD + plain SDF contacts | diverges — gear leaves the fixture (74 m) | gears fly apart; no contact registered |
| VBD + plain SDF contacts | diverges hard — 380 m/s, 2660 °/s | gears fly apart |
| Kamino | too slow to evaluate here (~500× MuJoCo) | |
So the honest position on the original question — can a constraint-based solver run this task at a larger time step? — is that neither XPBD nor VBD is usable on this scene yet. Their configuration is now the open problem rather than their plumbing: both mirror Newton's threaded nut-and-bolt example, which is stable in Newton's own harness, so the difference is in this scene's mass ratios, contact parameters and the articulated arm sharing the solve. MuJoCo remains the only configuration that seats a gear correctly, and it still does not turn one.
Measuring the overlap
Overlap is measured directly: the moving mesh's vertices are transformed into the static shape's signed-distance field and sampled, so the most negative sample is the penetration depth. The SDF is built on demand and disk-cached, and one lookup per vertex keeps the query at ~0.04 ms per step — cheap enough to drive a reward penalty or a reset, both exposed as MDP terms.
Peak gear-in-plate overlap against impact speed, on the corrected scene with the gear properly threaded on its shaft. Hatched bars are runs where the gear did not stay seated.
| Impact | Outcome | Final tilt | Resting offset from seat | Peak overlap |
|---|---|---|---|---|
| 0 m/s (released) | seats correctly | 0.00° | −0.03 mm | 1.81 mm |
| 0.2 m/s | seats correctly | 0.01° | −0.03 mm | 3.32 mm |
| 0.5 m/s | punches through the plate | 0.54° | −30.0 mm | 1.10 mm |
| 1.0 m/s | knocked off entirely | 46° | fell away | 1.13 mm |
The isolated gear-train study
Everything above was measured on the full task: an articulated arm, a gripper, resets, an observation
stack and three gears sharing one solve. Any of those can cause the wobble, and none of them can be ruled
out from inside. So the scene was rebuilt from nothing —
scripts/benchmarks/gear_solver_study.py — as plain Newton: a static base carrying the three
shafts, the three gears seated on them, and nothing else. Same USD collision meshes the task
ships.
The 60-tooth gear is driven by a velocity servo rather than a bare torque. That matters: the train is friction-limited, so a fixed torque lands anywhere between stuck and 300 rad/s depending on stiction, and no two configurations measure the same thing. Holding the input speed fixed makes slip — how far the followers fall short of the ratio their tooth counts demand — directly comparable across every run.
The geometry, measured from the meshes
Slicing the collision meshes with a plane (not sampling their vertices — a cylinder tessellated only at its ends has no vertices at mid-height, so a vertex probe cannot tell a toothed profile from a smooth one) gives the numbers every threshold below is built on:
| Quantity | Value | Consequence |
|---|---|---|
| Module / tooth counts | 1.5 mm · 20 / 40 / 60 T | tip radii 16.50 / 31.50 / 46.50 mm |
| Tooth depth (tip − root) | 3.37 mm, 60 cycles/rev confirmed by FFT | the teeth are really there, at every height of the 13.5 mm toothed band |
| Centre distances | 75.75 / 45.75 mm vs 75 / 45 theoretical | 0.75 mm loose → ≈0.55 mm backlash |
| Bore vs shaft radius | 7.875 mm vs 7.125 mm | 0.75 mm radial slop; ~3° of tilt is physically real, not numerical |
| Seat | plate top −11.25 mm, gear bottom face −18.75 mm | seated gear centre at +7.5 mm |
Outer radius against angle for the 60-tooth gear, taken from a plane slice of its collision mesh at mid-tooth height. Green is the shipped mesh; the dashed line is its convex hull — the representation MuJoCo's own contact path uses.
With the real meshes, the train works
XPBD at 1 kHz with a single substep, 10 iterations, a 256³ SDF and a 0.5 mm contact gap drives all three gears at the ratios the tooth counts demand:
| Gear | Measured ω | Ratio demanded by tooth counts | Slip | Peak-to-peak tilt |
|---|---|---|---|---|
| large 60T (driven) | +5.24 rad/s | — (input) | — | 0.19° |
| medium 40T | −7.85 rad/s | −7.86 rad/s | 0.1 % | 0.36° |
| small 20T | +15.71 rad/s | +15.72 rad/s | 0.1 % | 0.43° |
MuJoCo reaches the same answer when Newton's collision pipeline supplies the contacts
(use_mujoco_contacts=False): 0.3 % and 1.5 % slip, 0.27–1.68° of tilt. The sign
alternates correctly along the chain, and every gear stays within 0.6 mm of its shaft.
Exact meshes + SDF contacts, XPBD. The teeth mesh and the train turns.
Identical run, gears convex-hulled. The driven gear spins; its neighbours never move.
The one change that breaks it
Same rig, same solver, same time step — only the collider representation changes:
| Colliders | XPBD | MuJoCo (Newton contacts) |
|---|---|---|
| Exact meshes + SDF | 0.1 % slip, 0.2–0.5° tilt | 0.3–1.5 % slip, 0.3–1.7° tilt |
| Gears convex-hulled, base exact | diverges | 100 % slip — followers dead still |
| Everything convex-hulled | diverges (4529 rad/s) | 100 % slip, 94–167° tumbling |
A convex hull of a spur gear is a plain disc of the tip radius with the bore filled in. Two such discs seated at the design centre distance overlap permanently by 2.25 mm — because that overlap is the tooth engagement, and with the teeth gone there is nothing to interlock, only two cylinders pressing on each other. The hull of the base is worse: it is a tent stretched over the three shafts, so the gears have no bore to thread and slide down its slope. That is precisely the "right gear slides down to the floor below the fixture" seen earlier.
default preset — the one sized for the
two-day training run — sets use_mujoco_contacts=True. Newton's MuJoCo exporter says it
plainly in solver_mujoco.py: "MuJoCo compiles every mesh geom through its convex-hull
path." So under default the gear teeth and the shafts do not exist as far as contact is
concerned. The 2 kHz time step was paying for a scene that is permanently 2.25 mm interpenetrated.
What the task scene actually hands to the solver
Dumping the finalized Newton model per preset (gear_scene_audit.py) turns up three more
problems that no amount of solver tuning would have reached:
| Preset | Gear collider Newton receives | Contact gap | SDF |
|---|---|---|---|
default | the gears' visual meshes — the authored collisions/ prims are absent from the model entirely | 100 mm | none |
newton_xpbd | correct meshes, but the medium gear gets two coincident colliders (its visual mesh and its collision mesh) | 10 mm | yes |
newton_hydroelastic | one collision mesh per gear, hydroelastic flagged | 5 mm | yes |
- 100 mm contact gap. Newton's
ModelBuilder.rigid_gapdefaults to 0.1 m, and any shape that does not go through Isaac Lab'sNewtonShapeCfg(which sets 0.01 m) inherits it. Against 0.55 mm of backlash, a 100 mm speculative envelope means every tooth "sees" every other tooth on the fixture. Even 10 mm is 18× the backlash. - A duplicated collider on the medium gear doubles its contact response against both neighbours and against the shaft.
- Friction is inconsistent: the same gear set carries µ = 1.0 on some shapes and µ = 3.0 on others.
Encouragingly, the isolated rig is insensitive to the gap on its own — it still transmits with a 20 mm gap — so the gap is a cost and a contact-count problem (it is where the ~680 contacts/body came from) rather than the cause of the wobble. The cause is the hull.
How slow can the loop run?
With the real colliders, the original question finally has an answer. Sweeping the outer step from 1 kHz to 100 Hz, 22 of 24 configurations transmit within 5 % of the exact ratio:
Worst-case slip against outer step rate. XPBD holds the ratio down to 100 Hz with a single substep; MuJoCo needs 4 substeps below 200 Hz. Only two configurations miss: both at 100 Hz with a single substep.
The rest of the sweep is almost boring, which is the point — with the geometry represented correctly the problem is not delicate: 9/9 SDF-resolution × gap combinations transmit (128–512³, 0.2–2 mm gap), 8/8 iteration counts (5–40), and 9/9 friction × stiffness combinations (µ 0.05–1.0, ke 10⁵–10⁷).
Wobble is flat across SDF resolution — 128³ is already enough for a 3.37 mm tooth on a 93 mm gear. Resolution was never the limiting factor.
VBD: one parameter away from finite
VBD failed differently and more interestingly. On this scene it goes non-finite about 25 ms after the gears touch — not by exploding, but by producing NaN while every velocity is still under 2 cm/s. It is not a contact-buffer overflow (51–91 contacts per body against a 4096 buffer), and it survives none of the usual levers: frictionless, hard or soft contacts, 8 substeps, 5× smaller dt, stiffness from 10⁵ to 10⁷, density up to 10⁵.
The one setting that changes the outcome is the AVBD penalty ramp. With the default
rigid_avbd_beta = 0 ("fixed-k") the solve goes NaN at step 25 in every one of the sixteen
configurations swept; with rigid_avbd_beta = 1000 it runs indefinitely and the gears settle
within 0.2 mm of their designed seat.
Finite is not the same as correct. Driven by the same velocity servo that takes XPBD to 5.2 rad/s, VBD's input gear reaches only 0.5–0.7 rad/s — its contact is heavily dissipative here — and the followers slip 34–56 %. Raising the drive torque to overcome the damping makes it unstable again (86–180° of tumbling). So VBD is not usable on this scene yet, but the failure is now specific enough to report upstream with a minimal repro: three meshes, one static, no articulation.
rigid_avbd_beta | Outcome | Input ω reached | Follower slip |
|---|---|---|---|
| 0 (default) | non-finite at ~25 ms | — | — |
| 10 – 100 | finite but tumbles (14–86°) | 4.8–6.0 rad/s | 64–100 % |
| 1000 | stable, seated to 0.2 mm | 0.5–0.7 rad/s | 34–56 % |
| 10000 | tumbles (162°) | 5.7 rad/s | ~100 % |
- "The gear train does not transmit, and it is the contact model rather than the geometry" — half right. It is not the geometry, but nor is it hydroelastic averaging: it is the collider representation upstream of any contact model. With exact meshes the same hydroelastic and SDF paths transmit to 0.1 %.
- "Neither XPBD nor VBD is usable on this scene" — XPBD is not just usable, it is the best configuration measured, and works at 100 Hz. The earlier divergence was the task scene's collider set, not XPBD.
- "SDF resolution improves, then stalls" — resolution was never the constraint; 128³ already resolves the teeth.
- An intermediate claim made during this study — that the shipped gear meshes are toothless cylinders — was wrong, and is recorded here because it nearly became the headline. It came from reading the mesh's vertex rings instead of slicing its faces. Plane-slicing shows 60 teeth, 3.37 mm deep, at every height. The audit script now slices.
What's next
The scene is physical now, which resets the order of work: the solver comparison has to be re-run before any of it means anything.
- Get the task off the convex-hull contact path. This is the one change that matters.
defaultmust stop being the training preset, or must stop usinguse_mujoco_contacts=True;newton_xpbdis the candidate, since XPBD with exact SDF colliders is the configuration that actually works in isolation. - Fix the asset pipeline problems the scene audit found — the missing gear colliders
under
default, the duplicated medium-gear collider undernewton_xpbd, the 100 mm / 10 mm contact gaps against 0.55 mm of backlash, and the mixed µ = 1.0 / 3.0 friction. - Find out why the task cannot hold a gear on its shaft. Running the same gear-train
test inside the task still fails under every preset, and a plainer probe shows why: seat all
three gears, disable nothing, step with zero actions, and the one gear the task leaves free is
47–96 mm off its seat within a second (60 mm under
default, 82 mm undernewton_xpbd, 47 mm undernewton_hydroelastic) while the pinned gears do not move at all. The isolated rig proves the physics is reachable; it does not prove the task reaches it, and the next suspect is the reset/grasp path rather than the contact model. - Take VBD from finite to correct.
rigid_avbd_beta > 0stops the NaN; the remaining 9–27 % transmission is the open question, and it is now small enough to report upstream with a minimal repro. - Fix DisplayPort contact. The plug still falls through its socket, so that task cannot train at all yet — it needs the same treatment the gear just had.
- Then the dt sweep (1000→200→100 Hz), with overlap and seating outcome as the stability axes and throughput as the cost axis.