google-deepmind / google-deepmind/mujoco

URDF inertial origin rotation is lost during compilation, changing rotational dynamics

Open
#3,559 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
C++
Stars
15.2k
Forks
1.8k
Avg merge
10d 16h
Merged PRs (30d)
25

Description

### Intro

Hi! I use MuJoCo for robot simulation and found this while checking the inertial properties of imported URDF models.

### My setup

- MuJoCo **3.12.0**, official Python package; also reproduced on **3.3.7**.
- Python 3.11.15, NumPy 2.2.6.
- Ubuntu 24.04.4 LTS, x86_64.
- Native Python APIs: `MjModel.from_xml_string` and `MjSpec.from_string(...).compile()`.

### What's happening? What did you expect?

A URDF body's `` rotation is lost during compilation. This changes the reconstructed inertia tensor in the link frame and the resulting rotational dynamics.

The minimal example has one floating body, with its center of mass at the link origin. It specifies inertia `diag(1, 2, 2.5)` in an inertial frame rotated 90 degrees about Z. Therefore its inertia in the link frame should be `diag(2, 1, 2.5)`.

Both URDF import paths instead produce `diag(1, 2, 2.5)`. For an angular acceleration of 1 rad/s² about link X, inverse dynamics consequently returns **1 Nm**, rather than the expected **2 Nm**. An equivalent MJCF with the inertia orientation expressed as a quaternion produces the expected tensor and 2 Nm torque.

| Input / loader | Reconstructed link-frame inertia | X torque for 1 rad/s² |
|---|---|---|
| URDF / `MjModel` | `diag(1, 2, 2.5)` | 1 Nm |
| URDF / `MjSpec` | `diag(1, 2, 2.5)` | 1 Nm |
| Equivalent MJCF | `diag(2, 1, 2.5)` | 2 Nm |

The comparison reconstructs the full tensor from `body_iquat` and `body_inertia`, so it accounts for principal-axis/eigenvalue reordering. `fusestatic` and `alignfree` are explicitly disabled, and the model has no contacts or external assets.

### Steps for reproduction

Install `mujoco==3.12.0` and `numpy==2.2.6`, then run the self-contained script below. It includes the complete URDF, an equivalent MJCF control case, and both tensor and inverse-dynamics checks. The same script produces the same discrepancy on 3.3.7.

### Minimal models and code for reproduction

```python
import platform
import sys

import mujoco
import numpy as np

urdf = """













"""

mjcf = """







"""

print("MuJoCo:", mujoco.__version__)
print("Python:", sys.version.split()[0], "NumPy:", np.__version__)
print("Platform:", platform.system(), platform.release(), platform.machine())
print("Expected inertia in link frame: diag(2, 1, 2.5)")
print("Expected torque for 1 rad/s^2 around link X: 2 Nm")
for label, model in (
("URDF / MjModel", mujoco.MjModel.from_xml_string(urdf)),
("URDF / MjSpec", mujoco.MjSpec.from_string(urdf).compile()),
("Equivalent MJCF", mujoco.MjModel.from_xml_string(mjcf)),
):
body = model.body("body")
rotation = np.empty(9)
mujoco.mju_quat2Mat(rotation, body.iquat)
rotation = rotation.reshape(3, 3)
tensor = rotation @ np.diag(body.inertia) @ rotation.T
data = mujoco.MjData(model)
data.qacc[3] = 1.0
mujoco.mj_inverse(model, data)
print(label)
print(" inertia:", np.round(tensor, 10).tolist())
print(" X torque:", round(float(data.qfrc_inverse[3]), 10))
print(" tensor correct:", np.allclose(tensor, np.diag([2, 1, 2.5])))
```

### Possible cause

In 3.12.0, the [URDF reader](https://github.com/google-deepmind/mujoco/blob/3.12.0/src/xml/xml_urdf.cc#L241-L284) reads the inertial-origin orientation into `iquat` and composes it with the principal-axis orientation. However, the full tensor remains expressed in the original inertial frame.

The later [body compilation step](https://github.com/google-deepmind/mujoco/blob/3.12.0/src/user/user_objects.cc#L2712-L2718) calls `mjuu_fullInertia(iquat, inertia, this->fullinertia)` again. This appears to overwrite the previously composed `iquat` using that unchanged tensor, losing the URDF origin rotation.

Expressing the URDF tensor in link axes first (`I_link = R @ I_inertial @ R.T`) and setting the inertial-origin rotation to zero is a workaround.

### Checks before reporting

- Reviewed the current URDF import and inertial-frame documentation.
- Searched existing issues, PRs, and discussions for URDF inertia rotation / inertial-origin handling.
- Checked #1851 (principal-axis reordering) and #2982 (inertia accumulation with fixed-body fusion); this reproducer checks the reconstructed tensor and uses a single moving body with fusion disabled.

Contributor guide

Open the contributing guide

Research direction

Run the provided Python reproducer with both URDF loading paths and the equivalent MJCF control case. Inspect the URDF reader in src/xml/xml_urdf.cc around lines 241-284 and the body compilation step in src/user/user_objects.cc around lines 2712-2718. Done means the URDF cases reconstruct diag(2, 1, 2.5) and report 2 Nm for the stated acceleration, matching MJCF.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, python
Domain
compilers, robotics
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.