Intro
Hi!
I am an engineer at Leap Automation, where we use MuJoCo to build digital twins of vision-guided pick-and-place machines for food packing.
My setup
MuJoCo 3.10.0 and 3.11.0 (both reproduce), Python bindings, macOS arm64 (Apple Silicon). Default solver options; margin/gap are 0. Reproduces with and without nativeccd and multiccd (box-box pairs take the analytic primitive collider in all combinations).
What's happening? What did you expect?
The box-box collider returns a phantom deep contact for two identical boxes resting face-to-face at near-zero penetration: alongside the correct contact (dist = -0.056 mm at a corner), the manifold contains a second point with dist = -71.056 — the full box width along the flush axis plus the real depth — with the same +y normal. The true configuration is two boxes whose faces kiss with ~0.06 mm overlap at one corner; a 71 mm overlap along the contact normal is geometrically impossible for these poses.
Consequences in a full simulation: the constraint solver resolves the phantom penetration by ejecting the two resting bodies at ±17.9 m/s in a single 0.5 ms step. In our digital twin this detonated a settled 3×3 stack of packed products inside a tray (products at rest, zero velocity, zero applied force for seconds beforehand) — the stacked rows had drifted into flush face-to-face contact, which is a resident configuration for packed goods.
The degenerate branch appears to be a point singularity: perturbing either box by ±10 µm in x, y, or z, or ±0.1° of yaw — in any direction — yields correct contacts (either none, or shallow ~-0.06 depths). Deeper real penetration (-0.01 mm and beyond) is also handled correctly. Resting stacks still find the branch because a settled body's qpos is frozen (qvel exactly zero) while its quaternion creeps at ~1e-13/substep, sliding the pair along the singular manifold until a step lands on it; in our sim the same pair hit it three times in 7 seconds.
The phantom depth is exactly real_depth - 2*size[1] (the flush axis' full extent), which smells like the face-clipping code measuring depth against the wrong (far) face for one manifold point when the boxes are near-identical in pose (relative yaw here is ~0.5°, bottom faces coplanar, side edges flush to within 0.07 mm).
I'm aware from #1800 / #2816 that a replacement box-box collider is written and pending; per the request in that thread, offering this as an additional regression case — it's a fabricated deep contact rather than a missed one, so it may exercise different code than the failure-to-detect cases.
Steps for reproduction
Run the script below (poses are 17-significant-digit reprs, which round-trip float64 exactly, captured from the live sim at the detonating step). Expected output on 3.10.0/3.11.0:
contact depths (mm): [-71.056, -0.056]
Expected correct behavior: only shallow contacts (~-0.056 at the touching corner).
Minimal model for reproduction
import mujoco
# Model is in mm/g units (box 150 x 71 x 45 mm); the phantom is
# unit-independent — depths below are in model units.
XML = """
<mujoco>
<option timestep="0.0005"/>
<worldbody>
<body name="A"><freejoint/>
<geom name="gA" type="box" size="75 35.5 22.5" mass="0.25"/>
</body>
<body name="B"><freejoint/>
<geom name="gB" type="box" size="75 35.5 22.5" mass="0.25"/>
</body>
</worldbody>
</mujoco>
"""
# Both boxes yawed ~90 deg (relative yaw ~0.5 deg), faces kissing
# with a real ~0.056 corner overlap; bottom faces coplanar.
POSE_A = [862.4672361088551, 444.5974448882937, -87.50001574938747,
0.70870472649706, 2.2005940043163968e-13,
-1.8342950101702237e-13, 0.7055052165935622]
POSE_B = [862.4009654665109, 594.9219226097865, -87.50002095390447,
0.7054546906050059, -7.947440490083151e-08,
-3.301086291725321e-08, 0.7087550207958939]
model = mujoco.MjModel.from_xml_string(XML)
data = mujoco.MjData(model)
data.qpos[0:7] = POSE_A
data.qpos[7:14] = POSE_B
mujoco.mj_forward(model, data)
print("mujoco", mujoco.__version__)
print("contact depths (mm):",
[round(float(data.contact[i].dist), 3) for i in range(data.ncon)])
# 3.10.0 / 3.11.0: [-71.056, -0.056] <- -71.056 is the phantom
# (= full 71 box width + the real 0.056 corner depth, normal +y).
# Perturb either pose by 1e-2 (10 um) on any coordinate, or add
# 0.1 deg of yaw, and the phantom disappears.
Intro
Hi!
I am an engineer at Leap Automation, where we use MuJoCo to build digital twins of vision-guided pick-and-place machines for food packing.
My setup
MuJoCo 3.10.0 and 3.11.0 (both reproduce), Python bindings, macOS arm64 (Apple Silicon). Default solver options; margin/gap are 0. Reproduces with and without
nativeccdandmulticcd(box-box pairs take the analytic primitive collider in all combinations).What's happening? What did you expect?
The box-box collider returns a phantom deep contact for two identical boxes resting face-to-face at near-zero penetration: alongside the correct contact (
dist = -0.056mm at a corner), the manifold contains a second point withdist = -71.056— the full box width along the flush axis plus the real depth — with the same +y normal. The true configuration is two boxes whose faces kiss with ~0.06 mm overlap at one corner; a 71 mm overlap along the contact normal is geometrically impossible for these poses.Consequences in a full simulation: the constraint solver resolves the phantom penetration by ejecting the two resting bodies at ±17.9 m/s in a single 0.5 ms step. In our digital twin this detonated a settled 3×3 stack of packed products inside a tray (products at rest, zero velocity, zero applied force for seconds beforehand) — the stacked rows had drifted into flush face-to-face contact, which is a resident configuration for packed goods.
The degenerate branch appears to be a point singularity: perturbing either box by ±10 µm in x, y, or z, or ±0.1° of yaw — in any direction — yields correct contacts (either none, or shallow ~-0.06 depths). Deeper real penetration (-0.01 mm and beyond) is also handled correctly. Resting stacks still find the branch because a settled body's qpos is frozen (qvel exactly zero) while its quaternion creeps at ~1e-13/substep, sliding the pair along the singular manifold until a step lands on it; in our sim the same pair hit it three times in 7 seconds.
The phantom depth is exactly
real_depth - 2*size[1](the flush axis' full extent), which smells like the face-clipping code measuring depth against the wrong (far) face for one manifold point when the boxes are near-identical in pose (relative yaw here is ~0.5°, bottom faces coplanar, side edges flush to within 0.07 mm).I'm aware from #1800 / #2816 that a replacement box-box collider is written and pending; per the request in that thread, offering this as an additional regression case — it's a fabricated deep contact rather than a missed one, so it may exercise different code than the failure-to-detect cases.
Steps for reproduction
Run the script below (poses are 17-significant-digit reprs, which round-trip float64 exactly, captured from the live sim at the detonating step). Expected output on 3.10.0/3.11.0:
Expected correct behavior: only shallow contacts (~-0.056 at the touching corner).
Minimal model for reproduction