Skip to content

MuJoCo Warp Generates Nans with sliding friction=0.001 #1517

Description

@gyeomannvidia

Reproduction script here:

"""
Reproduce NaN in mujoco_warp: rigid box-box contact with zero sliding friction
and condim=3.

Two free boxes stacked on a ground plane. The top box-bottom box contact has
sliding friction = max(box_mu, box_mu) = box_mu, condim=3. When box_mu is too
small the friction cone is degenerate and qpos goes NaN.

Usage:
uv run python repro_nan_friction.py
"""

import mujoco
import numpy as np

import mujoco_warp as mjw

---------------------------------------------------------------------------

Scene

---------------------------------------------------------------------------

MJCF_TEMPLATE = """

<!-- Box B (bottom): 0.4 m cube, sits on ground, bottom face at z=0 -->
<body name="boxB" pos="0 0 0.2">
  <freejoint/>
  <inertial mass="1.0" pos="0 0 0" diaginertia="1.0 1.0 1.0"/>
  <geom name="geomB" type="box" size="0.2 0.2 0.2"
        friction="{mu:.10g} 0.005 0.0001" condim="3"/>
</body>

<!-- Box A (top): 0.2 m cube, sits on box B, bottom face at z=0.4 -->
<body name="boxA" pos="0 0 0.5">
  <freejoint/>
  <inertial mass="1.0" pos="0 0 0" diaginertia="1.0 1.0 1.0"/>
  <geom name="geomA" type="box" size="0.1 0.1 0.1"
        friction="{mu:.10g} 0.005 0.0001" condim="3"/>
</body>
"""

DT = 1.0 / 240.0
N_STEPS = 30

---------------------------------------------------------------------------

Helper

---------------------------------------------------------------------------

def run_scene(mu: float, n_steps: int = N_STEPS, verbose: bool = True) -> dict:
"""
Load the scene with the given sliding friction mu, step n_steps times, and
return a result dict with keys:
nan_step - first step that produced a NaN (-1 if none)
nan_body - name of the body (or 'none')
qpos_final - final qpos array (may contain NaN)
"""
xml = MJCF_TEMPLATE.format(timestep=DT, mu=mu)

mjm = mujoco.MjModel.from_xml_string(xml)
mjd = mujoco.MjData(mjm)
mujoco.mj_forward(mjm, mjd)          # establish initial valid state

m = mjw.put_model(mjm)
d = mjw.put_data(mjm, mjd)

nan_step = -1
nan_qpos = None

for step_idx in range(n_steps):
    mjw.step(m, d)

    qpos = d.qpos.numpy()[0]          # shape (nq,) for world 0

    if np.any(np.isnan(qpos)):
        nan_step = step_idx
        nan_qpos = qpos.copy()
        if verbose:
            print(f"  -> NaN detected at step {step_idx}: qpos = {qpos}")
        break

if nan_step == -1:
    nan_qpos = d.qpos.numpy()[0]

return {
    "nan_step": nan_step,
    "nan_qpos": nan_qpos,
}

---------------------------------------------------------------------------

Friction sweep

---------------------------------------------------------------------------

MU_SWEEP = [1e-4, 1e-3, 0.1, 1.0]

results = [(mu, run_scene(mu=mu, verbose=False)) for mu in MU_SWEEP]

COL_MU = 24 # len("Sliding mu (friction[0])")
COL_STEP = 14 # len("First NaN step")
COL_NAN = 8 # len("Got NaN?")

print()
print("=" * 56)
print("Friction sweep — box sliding friction vs NaN")
print("=" * 56)
print(f" {'Sliding mu (friction[0])':>{COL_MU}} {'First NaN step':>{COL_STEP}} {'Got NaN?':>{COL_NAN}}")
print(f" {'-'*COL_MU} {'-'*COL_STEP} {'-'*COL_NAN}")

for mu, r in results:
nan_step = r["nan_step"]
got_nan = "YES" if nan_step >= 0 else "no"
step_str = str(nan_step) if nan_step >= 0 else "-"
print(f" {mu:>{COL_MU}.2g} {step_str:>{COL_STEP}} {got_nan:>{COL_NAN}}")

print()
print("MJ_MINMU = 1e-5: NaN persists two orders of magnitude above it (up to ~1e-4).")
print("Done.")

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions