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.")
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 = """
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)
---------------------------------------------------------------------------
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.")