google-deepmind / google-deepmind/mujoco
URDF inertial origin rotation is lost during compilation, changing rotational dynamics
- 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
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