mujoco physics sim-to-real

Configuring Inertial Tensors and Friction Coefficients for Sim-to-Real Consistency in MuJoCo

A technical guide to specifying mass, inertia, and contact parameters in MJCF: why auto-computed inertia breaks sim-to-real, how to calibrate solref and solimp, and a Python diagnostic suite that validates an asset before you train on it.

Rigyd Team
·

Sim-to-real transfer for robotic manipulation depends on aligning the physical properties of the simulation with the physical properties of the hardware. When you deploy a reinforcement learning policy or a model-based controller, such as a torque-controlled impedance controller or model predictive control, small discrepancies in mass distribution or contact dynamics degrade controller stability, cause tracking drift, or saturate actuators. This guide covers the rigid-body dynamics behind MuJoCo assets, gives production-ready MJCF, shows how to calibrate the constraint solver, and provides a Python diagnostic suite that validates an asset before you train on it.

It assumes you are producing physics-enabled simulation assets and care about whether their behavior matches a real robot. For the broader picture of why physics accuracy drives transfer, see sim-to-real transfer and physics accuracy.

1. The mathematics of inertia and volumetric integration

The equations of motion for a multi-joint system, written in joint space, are:

M(q) q̈ + c(q, q̇) + g(q) = τ − J(q)ᵀ F_c

where:

  • M(q) is the symmetric, positive-definite joint-space inertia matrix (n x n).
  • c(q, q̇) is the vector of Coriolis and centrifugal forces.
  • g(q) is the vector of gravitational forces.
  • τ is the vector of actuator joint torques.
  • J(q)ᵀ F_c maps external contact forces into joint space.

The inertia matrix M(q) is built directly from the link inertia tensors expressed in their local frames, centered at each body’s center of mass (CoM). For a rigid body with continuous density ρ(r) over its volume V:

mass:    m       = ∫_V ρ(r) dV
CoM:     r_com   = (1/m) ∫_V ρ(r) r dV
inertia: I       = ∫_V ρ(r) ( (r·r) E − r rᵀ ) dV

where E is the 3x3 identity matrix and r = [x, y, z]ᵀ is measured relative to the CoM. The inertia tensor is symmetric:

      | Ixx  −Ixy  −Ixz |
I  =  | −Ixy  Iyy  −Iyz |
      | −Ixz  −Iyz  Izz |

To conform to the solver’s requirements, rotate the local frame to align with the principal axes of inertia, which diagonalizes the tensor:

I_diag = diag(I1, I2, I3)

In MuJoCo, those three diagonal elements are the diaginertia attribute, and the orientation of the principal frame relative to the body frame is set by the quat or euler parameter of the <inertial> element.

Failure modes of auto-computation (inertiafromgeom=“true”)

MuJoCo can compute mass, CoM, and inertia from the collision geometry under a uniform-density assumption (ρ(r) = ρ0). That shortcut introduces three failure modes in real pipelines:

  • Hollow or shell geometries. High-poly assets and CAD-exported STL or OBJ files describe an outer skin. Parsed with inertiafromgeom="true", MuJoCo treats them as solid homogeneous bodies, which overestimates both the mass m and the moments Ii, and drives unphysically high torque requirements in simulation.
  • Composite, non-uniform assemblies. A real gripper mixes steel gears, aluminum linkages, brushless motors, and a fiberglass enclosure. A uniform-density assumption shifts the computed r_com away from the heavy actuator cores, misaligning the gravity-torque profile and producing tracking errors during dynamic maneuvers.
  • Open or defective meshes. If a CAD export has open boundaries or non-manifold geometry, the volume integral has no closed volume to integrate over, yielding NaN values, negative masses, or a non-positive-definite tensor that crashes the solver.

Automatically estimating these physics properties at asset-preparation time is what makes manual override unnecessary at scale, but when you do author by hand, override auto-computation and declare the values analytically.

2. Structured MJCF implementation

Set inertiafromgeom="false" and specify mass properties manually. The template below structures a composite tool with explicit inertials and multi-hull convex collision geometry. The hulls come from convex decomposition of the visual mesh, kept separate from the high-poly visual mesh.

<mujoco model="rigyd_simready_manipulator_tool">
  <compiler meshdir="assets/" balanceinertia="false" coordinate="local"/>

  <default>
    <!-- Contact-active collision geometry -->
    <default class="collision_geom">
      <geom type="mesh" contype="1" conaffinity="1" group="0"
            rgba="0.4 0.4 0.4 1" condim="6"
            solimp="0.95 0.99 0.001 0.5 2" solref="0.015 1.0"/>
    </default>
    <!-- Visual-only geometry (no collision) -->
    <default class="visual_geom">
      <geom type="mesh" contype="0" conaffinity="0" group="1"
            rgba="0.8 0.8 0.8 1"/>
    </default>
  </default>

  <asset>
    <mesh name="tool_visual" file="tool_visual_highpoly.stl"/>
    <!-- Optimized V-HACD collision hulls -->
    <mesh name="tool_hull_0" file="hulls/tool_hull_0.stl"/>
    <mesh name="tool_hull_1" file="hulls/tool_hull_1.stl"/>
    <mesh name="tool_hull_2" file="hulls/tool_hull_2.stl"/>
  </asset>

  <worldbody>
    <body name="ee_tool" pos="0.0 0.0 1.2" quat="1.0 0.0 0.0 0.0">
      <!--
        Explicit inertial, overriding volumetric auto-computation.
        - mass:        real mass (2.385 kg) from CAD assembly density profiles
        - pos:         exact CoM position in the body local frame
        - diaginertia: principal moments (I1, I2, I3) in kg*m^2
        - quat:        principal-axis rotation relative to the body frame
      -->
      <inertial pos="0.0034 -0.0125 0.0872" mass="2.385"
                diaginertia="0.00412 0.00567 0.00289"
                quat="0.99619 0.08715 0.0 0.0"/>

      <joint name="wrist_joint_3" type="hinge" axis="0 0 1" pos="0 0 0"
             limited="true" range="-180 180"/>

      <!-- Visual only, excluded from physics -->
      <geom name="ee_visual_geom" class="visual_geom" mesh="tool_visual"/>

      <!--
        Collision hulls.
        - condim="6" enables sliding, torsional, and rolling friction
        - friction = [sliding, torsional, rolling]
        - solimp / solref calibrated for high contact stiffness
      -->
      <geom name="ee_hull_geom_0" class="collision_geom" mesh="tool_hull_0"
            friction="0.85 0.008 0.0002"/>
      <geom name="ee_hull_geom_1" class="collision_geom" mesh="tool_hull_1"
            friction="0.85 0.008 0.0002"/>
      <!-- Softer, higher-friction grasping pad -->
      <geom name="ee_hull_geom_2" class="collision_geom" mesh="tool_hull_2"
            friction="0.95 0.015 0.0005"
            solimp="0.90 0.95 0.002 0.5 2" solref="0.020 1.0"/>
    </body>
  </worldbody>
</mujoco>

For a step-by-step walkthrough of choosing mass, friction, and joint values, see how to set up mass, friction, and joint properties for robot training.

3. Calibration of contact mechanics

MuJoCo does not model contacts and limits as rigid impulses. It resolves them as soft constraints with a spring-damper formulation, which stabilizes the numerical integration of multi-body systems. Two parameters govern this: solver reference (solref) and solver impedance (solimp).

Solver reference (solref)

solref is a 2-element vector [d, ζ]:

  • Time constant d (seconds): the relaxation time of the constraint, which sets how quickly the solver corrects penetration. The natural frequency of the virtual spring is approximately ωn ≈ 2 / d.
  • Damping ratio ζ: the damping profile of the correction.
    • ζ = 1.0: critically damped, the correct choice for rigid bodies. Resolves penetration without bounce.
    • ζ < 1.0: underdamped. Causes high-frequency contact vibration and bouncing.
    • ζ > 1.0: overdamped. Produces sluggish, spongy contact.

Solver impedance (solimp)

solimp is a 5-element vector [d_min, d_max, w, s, p] that sets constraint stiffness as a function of penetration depth r:

  • d_min: minimum impedance (typically 0.90).
  • d_max: maximum impedance (typically 0.95 to 0.99).
  • w: transition width (meters), the penetration threshold over which impedance moves from d_min to d_max.
  • s: interpolation spline power (typically 0.5).
  • p: polynomial scaling order (typically 2.0).

Impedance d(r) varies smoothly from d_min to d_max as penetration deepens:

  d(r)
 d_max |          .-------------------
       |        /
       |      /
 d_min |----+
       |    |
       +----+-------------------------> penetration r
       0    w

Calibration protocols

Eliminating high-frequency rest chatter. Micro-vibration while a static asset sits flat on a surface comes from a very low solref time constant combined with high solimp values, which create constraint forces beyond the integrator’s stability limit. Keep the time constant d at least three times the timestep dt (for dt = 0.002 s, set d >= 0.006 s). If vibration persists, lower d_min (for example 0.90 to 0.80) to soften initial engagement.

Correcting sponginess and excessive penetration. Heavy assets that visibly sink into a surface indicate d_max is too low or the transition width w is too wide, delaying peak stiffness. Raise d_max toward 0.99 for near-rigid contact, and reduce w to sub-millimeter scale (0.001 m or 0.0005 m) so peak restoring force triggers almost immediately.

Preventing simulation blow-ups. Assets that accelerate violently outward on first contact indicate a time constant smaller than the timestep (d < dt), which makes the integrator unstable and grows restorative velocity exponentially in one step. Set ζ = 1.0 and never set d below dt.

4. Programmatic validation and diagnostics

The script below uses the official mujoco Python bindings to run two checks: an inertial definiteness and triangle-inequality verification, and a gravity-compensation acceleration test that isolates mass and CoM under a closed-loop gravity-compensation torque. If the analytical model is correct, applying the computed bias torque leaves the system static.

"""
MuJoCo MJCF inertial and dynamic calibration diagnostic suite.
Verifies rigid-body physical properties, triangle-inequality constraints,
and joint-torque profiles under closed-loop gravity compensation.
"""
import os
import numpy as np
import mujoco


def run_physical_diagnostics(mjcf_path: str):
    print("=" * 70)
    print(f"PHYSICAL DIAGNOSTICS FOR: {os.path.basename(mjcf_path)}")
    print("=" * 70)

    # 1. Load the model
    try:
        model = mujoco.MjModel.from_xml_path(mjcf_path)
        data = mujoco.MjData(model)
        print("[OK] MJCF parsed. Compiling dynamic variables...\n")
    except Exception as e:
        print(f"[CRITICAL] Failed to load MJCF model: {e}")
        return

    validation_passed = True

    # 2. Validate inertial properties (body 0 is the static worldbody)
    for body_id in range(1, model.nbody):
        name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_BODY, body_id)
        mass = model.body_mass[body_id]
        com = model.body_ipos[body_id]
        diag = model.body_inertia[body_id]

        print(f"Body '{name}' (id {body_id})")
        print(f"  mass: {mass:.4f} kg   CoM: {com.tolist()}")
        print(f"  principal inertia: {diag.tolist()}")

        if mass <= 0:
            print(f"  [ERROR] non-positive mass on '{name}'")
            validation_passed = False

        if np.any(diag <= 0):
            print(f"  [ERROR] non-positive principal inertia on '{name}'")
            validation_passed = False

        # Triangle inequality: each moment <= sum of the other two
        ixx, iyy, izz = diag
        t = -1e-7  # numerical tolerance
        if (ixx + iyy - izz < t or ixx + izz - iyy < t or iyy + izz - ixx < t):
            print(f"  [ERROR] '{name}' violates the inertia triangle inequality")
            validation_passed = False
        else:
            print("  [PASS] mass, positivity, and triangle inequality verified")
        print("-" * 70)

    # 3. Gravity-compensation torque test
    print("\n[TEST] gravity-compensation diagnostic...")
    model.opt.disableflags |= mujoco.mjtDisableBit.mjDSBL_CONTACT
    model.opt.disableflags |= mujoco.mjtDisableBit.mjDSBL_CONSTRAINT

    mujoco.mj_resetData(model, data)
    if model.nq > 0:
        data.qpos[0] = 0.523  # ~30 deg, a gravity-sensitive pose

    # qfrc_bias = c(q, q̇) + g(q); with q̇ = 0 this is the exact gravity torque
    mujoco.mj_forward(model, data)
    gravity_torque = np.copy(data.qfrc_bias)

    data.qfrc_applied[:] = gravity_torque
    mujoco.mj_step(model, data)

    qacc = np.copy(data.qacc)
    max_acc = np.max(np.abs(qacc))
    print(f"  compensating torque: {gravity_torque.tolist()} Nm")
    print(f"  residual acceleration: {qacc.tolist()} rad/s^2")
    print(f"  peak residual: {max_acc:.2e} rad/s^2")

    if max_acc > 1e-5:
        print(f"  [ERROR] gravity compensation failed ({max_acc:.2e} > 1e-5)")
        print("  Likely: joint frames misaligned with CoM, or inconsistent inertia")
        validation_passed = False
    else:
        print("  [PASS] static stability maintained under manual torque control")

    print("\n" + "=" * 70)
    print("SUMMARY: PASSED" if validation_passed else "SUMMARY: FAILED")
    print("=" * 70)


if __name__ == "__main__":
    path = "rigyd_simready_manipulator_tool.xml"
    if os.path.exists(path):
        run_physical_diagnostics(path)
    else:
        print(f"Save the MJCF sample to '{path}' and re-run this script.")

The triangle-inequality check is the cheapest way to catch a physically impossible tensor: for any real 3D mass distribution, each principal moment must be no larger than the sum of the other two. The gravity-compensation test then confirms that mass and CoM are consistent enough that an analytical g(q) torque holds the arm static.

5. Parameter summary for common materials

Use these as analytical starting points when modeling structural composites by hand, rather than relying on default volumetric integration. Density is in kg/m^3; friction is [sliding, torsional, rolling].

MaterialDensityFrictionsolimpsolref
Machined aluminum (6061-T6)27000.30 0.001 0.00010.98 0.99 0.0005 0.5 20.005 1.0
High-density polyethylene (HDPE)9600.22 0.002 0.00010.95 0.98 0.001 0.5 20.010 1.0
Polyurethane rubber (grip pads)12000.95 0.025 0.00050.85 0.95 0.005 0.5 20.025 1.0
Hard carbon-fiber composite17500.35 0.002 0.00010.97 0.99 0.0005 0.5 20.006 1.0

These are guidelines, not measured constants. Treat them as a calibrated first pass, then verify against the diagnostic suite above and, where possible, against measurements from the real part. For the broader question of when estimated versus measured values are good enough, see sim-to-real transfer and physics accuracy.

Key takeaways

  1. Override inertiafromgeom for any hollow, composite, or CAD-exported asset. Uniform-density auto-computation misplaces mass and inertia in exactly the cases that matter for control.
  2. Declare inertia in diagonal (principal-axis) form with diaginertia plus a quat, and keep collision hulls separate from the visual mesh.
  3. Calibrate contacts with solref and solimp: critically damped (ζ = 1.0), a time constant at least 3x your timestep, and d_max near 0.99 with a sub-millimeter transition width for near-rigid contact.
  4. Validate every asset programmatically before training: positive mass, positive-definite and triangle-inequality-valid inertia, and a passing gravity-compensation test.
  5. Physical consistency at the asset level is what keeps a controller stable in sim and transferable to the real robot. It is cheaper to enforce at asset-preparation time than to debug in a training run.

Frequently asked questions

Why should I set inertiafromgeom to false in MuJoCo?

Auto-computed inertia assumes a uniform density over the collision geometry, which fails on three common asset types: hollow or shell meshes exported from CAD (treated as solid, so mass and inertia are overestimated), composite assemblies with dense motors or gears (the computed center of mass drifts away from the heavy cores), and open or non-manifold meshes (the volume integral produces NaN, negative mass, or a non-positive-definite tensor that crashes the solver). Setting inertiafromgeom to false lets you declare analytically derived mass, center of mass, and diagonal inertia instead.

What is diaginertia in an MJCF inertial element?

diaginertia is the three principal moments of inertia (the diagonal of the inertia tensor after it has been rotated to align with the body's principal axes), in kg*m^2. The orientation of that principal frame relative to the body frame is given by the quat or euler attribute on the same <inertial> element. Declaring the tensor in diagonal form is what MuJoCo's solver expects and avoids re-deriving off-diagonal products of inertia at load time.

What do solref and solimp control in a MuJoCo contact?

MuJoCo resolves contacts as soft spring-damper constraints, not rigid impulses. solref is a 2-vector [time_constant, damping_ratio] that sets how fast and how oscillation-free the constraint corrects penetration; damping_ratio = 1.0 is critically damped and correct for rigid bodies. solimp is a 5-vector [d_min, d_max, width, spline, power] that sets how constraint stiffness ramps from d_min to d_max as penetration deepens. Together they determine whether contact feels rigid, spongy, chattery, or explosive.

How do I stop a MuJoCo asset from vibrating while it rests on a surface?

Rest chatter comes from a solref time constant that is too small relative to the timestep combined with high solimp stiffness, which produces constraint forces beyond the integrator's stability limit. Keep the time constant at least three times the simulation timestep (for dt = 0.002 s, use a time constant >= 0.006 s), ensure the damping ratio is 1.0, and if vibration persists lower d_min in solimp (for example from 0.90 to 0.80) to soften the initial contact engagement.

What is the triangle inequality check for an inertia tensor?

For any physically valid 3D mass distribution, each principal moment of inertia must be no greater than the sum of the other two: Ixx + Iyy >= Izz, and the two cyclic permutations. A tensor that violates this cannot correspond to a real rigid body, even if all three moments are positive. Checking the triangle inequality (plus positive mass and positive-definite inertia) catches bad hand-entered or mis-exported values before they reach a controller.

Skip the manual physics work

Convert a 3D model, image, or text description into a SimReady OpenUSD asset in minutes. Mass, friction, collision meshes, all calibrated automatically.