Skip to content

Honor MJCF geom inertia selection#3596

Open
eric-heiden wants to merge 6 commits into
newton-physics:mainfrom
eric-heiden:eric-heiden/fix-mjcf-geom-inertia-selection
Open

Honor MJCF geom inertia selection#3596
eric-heiden wants to merge 6 commits into
newton-physics:mainfrom
eric-heiden:eric-heiden/fix-mjcf-geom-inertia-selection

Conversation

@eric-heiden

@eric-heiden eric-heiden commented Jul 21, 2026

Copy link
Copy Markdown
Member

Description

Newton's MJCF importer did not follow MuJoCo's compiler rules for choosing body inertia.

This change adds support for:

  • inertiafromgeom="auto", "true", and "false"
  • inertiagrouprange
  • inertia from visual geoms even when parse_visuals=False

Loaded shapes still use the normal mass path. Visual geoms that are intentionally not loaded are evaluated in a temporary builder, so they affect body mass and inertia without appearing in the model.

Closes #3593.

Checklist

  • New or existing tests cover the change.
  • Existing documentation remains accurate.
  • The changelog includes the user-facing fix.

Test plan

  • uv run --extra dev -m newton.tests -k test_visual_geom_mass_without_parsing_visuals
  • uv run --extra dev -m newton.tests -k test_compiler_inertiagrouprange
  • uv run --extra dev -m newton.tests -k test_compiler_inertiafromgeom_modes
  • uv run --extra dev -m newton.tests -p test_import_mjcf.py (234 tests)
  • uvx pre-commit run -a

Bug fix

Before this change, parse_visuals=False could change body mass, excluded geom groups still contributed inertia, and inertiafromgeom="true" did not override explicit inertial data. These cases now follow MuJoCo's compiler semantics.

Summary by CodeRabbit

  • Bug Fixes
    • Updated the MJCF importer’s inertia/mass inference to properly honor <compiler inertiafromgeom> modes and <compiler inertiagrouprange> filtering.
    • Ensures inferred body mass/inertia remains consistent regardless of whether visuals are parsed as shapes.
    • Correctly applies explicit mass for small mesh geoms without spurious warnings.
    • Enforces expected behavior when inference is disallowed but <inertial> data is missing.
  • Tests
    • Added regression tests for mesh explicit-mass handling and compiler-driven inertia/mass semantics.
  • Documentation
    • Refreshed the unreleased changelog notes describing the fix.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

ModelBuilder.add_mjcf() now honors inertiafromgeom and inertiagrouprange, computes inferred inertia independently of visual parsing, handles explicit mesh mass, propagates the policy through nested frames, and adds regression tests.

Changes

MJCF inertia inference

Layer / File(s) Summary
Compiler options and shape parsing
newton/_src/utils/import_mjcf.py
Parses compiler inertia options, gates geom contributions by group and policy, supports temporary-builder shape parsing, and realizes explicit mesh mass without double-counting inertia.
Geom inertia accumulation and propagation
newton/_src/utils/import_mjcf.py
Re-parses selected unloaded geoms into a temporary builder, updates target body mass properties, and propagates inference settings through body and frame processing.
Body policy and regression coverage
newton/_src/utils/import_mjcf.py, newton/tests/test_import_mjcf.py, CHANGELOG.md
Selects explicit or geom-derived inertia according to compiler settings and tests mesh mass, visual parsing independence, group ranges, and compiler modes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MJCF
  participant parse_body
  participant _process_body_geoms
  participant temporary_ModelBuilder
  participant target_body
  MJCF->>parse_body: provide compiler and body definitions
  parse_body->>_process_body_geoms: pass inertia inference policy
  _process_body_geoms->>temporary_ModelBuilder: re-parse selected unloaded geoms
  temporary_ModelBuilder-->>_process_body_geoms: return inferred mass and inertia
  _process_body_geoms->>target_body: update body mass, inertia, and center of mass
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: adenzler-nvidia

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: MJCF geom inertia selection semantics.
Linked Issues check ✅ Passed The importer changes match #3593 by honoring inertiafromgeom, inertiagrouprange, parse_visuals independence, and inertial override rules.
Out of Scope Changes check ✅ Passed The PR stays focused on MJCF inertia-selection fixes, with only matching tests and changelog updates.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@newton/_src/utils/import_mjcf.py`:
- Around line 1604-1607: Reject bodies missing <inertial> in the
inertia-selection logic around has_inertial_definition and
infer_body_inertia_from_geoms by raising ValueError when inertia_from_geom is
"false", ignore_inertial_definitions is false, and no inertial definition
exists. Add coverage in newton/tests/test_import_mjcf.py at lines 2833-2852
using a false-mode body without <inertial> and assert import raises.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 147d2025-5d1d-45a9-bb50-f2f0fd0679f7

📥 Commits

Reviewing files that changed from the base of the PR and between ce603b3 and ac04a40.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • newton/_src/utils/import_mjcf.py
  • newton/tests/test_import_mjcf.py

Comment thread newton/_src/utils/import_mjcf.py
@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.49123% with 2 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
newton/_src/utils/import_mjcf.py 96.49% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot mentioned this pull request Jul 21, 2026
3 tasks

Copy link
Copy Markdown
Member Author

CI failure analysis: every failing job reaches the same warning for explicit mass on the camera_mount_geom_0_visual mesh. This PR now evaluates visual geoms for inertia, so it exposes the existing mesh-mass bug fixed by #3595. I am keeping this PR focused instead of copying that fix here. Once #3595 lands, rebasing this branch onto main should clear these failures.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
newton/tests/test_import_mjcf.py (1)

2854-2865: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a prefix-first name for the MJCF fixture.

Rename missing_inertial_mjcf to mjcf_missing_inertial so MJCF fixtures group predictably in autocomplete.

Proposed fix
-        missing_inertial_mjcf = """<?xml version="1.0" ?>
+        mjcf_missing_inertial = """<?xml version="1.0" ?>
...
-            newton.ModelBuilder().add_mjcf(missing_inertial_mjcf)
+            newton.ModelBuilder().add_mjcf(mjcf_missing_inertial)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@newton/tests/test_import_mjcf.py` around lines 2854 - 2865, Rename the MJCF
fixture variable from missing_inertial_mjcf to mjcf_missing_inertial and update
its use in the ModelBuilder().add_mjcf call, leaving the test behavior
unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@newton/tests/test_import_mjcf.py`:
- Around line 2854-2865: Rename the MJCF fixture variable from
missing_inertial_mjcf to mjcf_missing_inertial and update its use in the
ModelBuilder().add_mjcf call, leaving the test behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 0973062e-148e-489f-99bf-f97249fabf2d

📥 Commits

Reviewing files that changed from the base of the PR and between ac04a40 and 5595d6a.

📒 Files selected for processing (2)
  • newton/_src/utils/import_mjcf.py
  • newton/tests/test_import_mjcf.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • newton/_src/utils/import_mjcf.py

@eric-heiden

Copy link
Copy Markdown
Member Author

Addressed the fixture naming nit in d719ab2. The change only renames the local test variable to use the MJCF prefix. Validation: the targeted test_compiler_inertiafromgeom_modes test passes, and uvx pre-commit run -a passes.

@eric-heiden

Copy link
Copy Markdown
Member Author

To unblock CI while #3595 awaits review, this branch now includes #3595 as an explicit temporary dependency in 634f4a9. The mesh-mass fix was not reimplemented here. After #3595 merges, I will update this branch from main and remove the temporary dependency so the final diff remains focused.

Local validation on the combined branch:

  • test_menagerie_mujoco.TestMenagerie_RobotstudioSo101: 3 tests passed
  • test_import_mjcf.py: 235 tests passed
  • uvx pre-commit run -a: passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
newton/tests/test_import_mjcf.py (1)

2892-2925: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert inertia and COM, not only mass, for inertiafromgeom modes.

This regression test can pass even if inertiafromgeom="true" overrides mass correctly but leaves the authored inertia or COM in place. Use distinct authored and geom-derived values, then assert body_inertia and body_com for all three modes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@newton/tests/test_import_mjcf.py` around lines 2892 - 2925, Extend
test_compiler_inertiafromgeom_modes with distinct authored and geom-derived
center-of-mass and inertia values, then define expected body_inertia and
body_com for each inertiafromgeom mode. Assert builder.body_inertia and
builder.body_com alongside body_mass for all modes, ensuring true uses
geom-derived values while auto and false preserve the authored inertial data.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@newton/tests/test_import_mjcf.py`:
- Around line 2892-2925: Extend test_compiler_inertiafromgeom_modes with
distinct authored and geom-derived center-of-mass and inertia values, then
define expected body_inertia and body_com for each inertiafromgeom mode. Assert
builder.body_inertia and builder.body_com alongside body_mass for all modes,
ensuring true uses geom-derived values while auto and false preserve the
authored inertial data.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 06655444-b1c1-4692-af74-6200ccc87d34

📥 Commits

Reviewing files that changed from the base of the PR and between 1e47fa1 and 634f4a9.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • newton/_src/utils/import_mjcf.py
  • newton/tests/test_import_mjcf.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • CHANGELOG.md
  • newton/_src/utils/import_mjcf.py

@eric-heiden

Copy link
Copy Markdown
Member Author

Addressed the inertia/COM coverage feedback in 23426d3. The test now gives the authored inertial and geom different COM and inertia values, then checks mass, COM, and the full inertia tensor for auto, false, and true modes.

Validation:

  • targeted test_compiler_inertiafromgeom_modes: passed
  • full test_import_mjcf.py: 235 tests passed
  • uvx pre-commit run -a: passed

Apply compiler inertiafromgeom and inertiagrouprange semantics. Compute inertia from visual geoms even when visual shapes are not retained by the importer.
Match MuJoCo when inertiafromgeom is false by rejecting bodies that omit the required inertial element.
@eric-heiden
eric-heiden force-pushed the eric-heiden/fix-mjcf-geom-inertia-selection branch from 23426d3 to c3826ef Compare July 22, 2026 16:18
@eric-heiden

Copy link
Copy Markdown
Member Author

PR #3595 has merged, so I rebuilt this branch on current upstream main and removed the temporary dependency merge. The final diff now contains only the inertiafromgeom / inertiagrouprange importer changes, their regression tests, and the #3596 changelog entry.

Validation after rebuilding:

  • uv run --extra dev -m newton.tests -p test_import_mjcf.py — 235 tests passed
  • uvx --from pre-commit==4.5.0 pre-commit run -a — all hooks passed

The explicit pre-commit version is only for local compatibility with Git 2.30; no project dependency changed.

@eric-heiden
eric-heiden marked this pull request as ready for review July 22, 2026 18:06
@eric-heiden
eric-heiden requested a review from vreutskyy July 22, 2026 18:08

@adenzler-nvidia adenzler-nvidia left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation looks good — the semantics match MuJoCo's compiler docs (inertiafromgeom modes, inclusive inertiagrouprange, compile error on false + missing <inertial>), the group-range zeroing happens before every consumer including the mesh path, and the scratch-builder transfer math checks out. Just a few nitpicks inline, mostly about hardening the tests around the new machinery.

Comment thread newton/tests/test_import_mjcf.py Outdated
msg="Visual geom with explicit mass should contribute non-zero inertia",
)

def test_visual_geom_mass_without_parsing_visuals(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three new tests put a single massive geom into an empty body, so new_com == p in _update_body_mass and the parallel-axis/rotation terms are identically zero — a sign or frame error in the scratch-builder transfer would pass every test here. Could you add a case with an off-origin, rotated visual plus a massive collider elsewhere (so the combined COM differs from both geom COMs), importing with parse_visuals=True and False and asserting body_mass, body_com, and the full body_inertia match between the two runs?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 710516f. test_geom_inertia_independent_of_visual_loading now combines an off-origin rotated visual box with a rotated collider at a different position. It verifies that the combined COM differs from both geom COMs and compares mass, COM, and the full inertia tensor between parse_visuals=True and parse_visuals=False.

unloaded_geoms = []
if parse_visuals_as_colliders:
loaded_geom_ids = {id(geom) for geom in visuals}
unloaded_geoms = [geom for geom in colliders if id(geom) not in loaded_geom_ids]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch is untested — nothing under newton/tests/ uses parse_visuals_as_colliders at all. It's the mirror image of the tested case (here the collision geoms go through the scratch builder), so it deserves the same mass/COM/inertia equality check against a parse_visuals=True reference import.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 710516f. The same regression now includes parse_visuals_as_colliders=True and compares its mass, COM, and full inertia tensor against the parse_visuals=True reference.

Comment thread newton/tests/test_import_mjcf.py Outdated
<body name="test">
<freejoint/>
<geom name="visual" class="visual" type="sphere" size="0.1"
contype="0" conaffinity="0" group="2" mass="0.012"/>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unloaded-geom path is only exercised with explicit mass=, but the mechanism behind #3593 is equally the density path (a visual geom without mass picking up the default density inside the scratch builder). Worth a density-only variant asserting equal mass/inertia for parse_visuals=True vs False.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 710516f. The regression now runs both explicit-mass and density-only cases across all three visual-loading modes and compares mass, COM, and full inertia.

Comment thread newton/_src/utils/import_mjcf.py Outdated
return shapes

def accumulate_inertia_from_unloaded_geoms(defaults, body_name, link, geoms, incoming_xform=None, label_prefix=""):
"""Accumulate selected geom inertia without retaining visual shapes."""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unloaded geoms aren't always visual: in the parse_visuals_as_colliders branch they are the displaced collision geoms. Maybe "Accumulate inertia from geoms not loaded as shapes, via a scratch builder."

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 710516f. The docstring now reads: Accumulate inertia from geoms not loaded as shapes, via a scratch builder.

Comment thread newton/tests/test_import_mjcf.py Outdated
)

def test_compiler_inertiafromgeom_modes(self):
"""Test compiler control over geom inference when inertial data exists."""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second half of the test asserts the missing-<inertial> error, exactly the case where inertial data does not exist. Maybe "Test inertiafromgeom modes with and without an element."

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 710516f. The test docstring now describes inertiafromgeom modes with and without an inertial element.

Comment thread newton/_src/utils/import_mjcf.py Outdated
@@ -735,6 +755,11 @@ def parse_shapes(
# We'll add the explicit mass to the body separately

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this promise is now conditional — the group-range check below can null geom_mass_explicit three lines later. The block comment above already covers the intent, so these two lines could go.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 710516f. I removed the two stale comments; the existing block comment remains as the accurate description of the explicit-mass path.

Comment thread newton/_src/utils/import_mjcf.py Outdated
@@ -1943,7 +2038,7 @@ def parse_body(
)

m = builder.body_mass[link]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this read is now dead — m is only used after being reassigned inside the branch below.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 710516f. I removed the dead builder.body_mass[link] read.

)
mass = inertia_builder.body_mass[inertia_link]
if mass > 0.0:
builder._update_body_mass(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: unlike the explicit-mass path in parse_shapes, this skips the body_lock_inertia check. It's redundant today (this helper only runs when inference is on, which also skips the <inertial> block that sets the lock), but a guard here would keep future call sites safe.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 710516f. The scratch-builder transfer now checks not builder.body_lock_inertia[link] before updating body mass properties.

@vreutskyy vreutskyy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One concern:

Comment thread newton/_src/utils/import_mjcf.py Outdated
Comment on lines +1624 to +1627
if inertia_from_geom == "false" and not ignore_inertial_definitions and not has_inertial_definition:
raise ValueError(
f"MJCF body '{body_name}' requires an <inertial> element when compiler inertiafromgeom=\"false\"."
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unconditional guard rejects every body without an <inertial> element when inertiafromgeom="false". MuJoCo only validates mass and inertia for bodies that have joints; fixed bodies are valid with zero mass. I reproduced this for both a body fixed to the world and a fixed child of a moving body: MuJoCo 3.10 accepts them, while this PR raises ValueError.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed with MuJoCo 3.10 and addressed in 710516f. The missing-inertial guard now applies only to bodies with an authored joint or freejoint. The regression covers a fixed body attached to the world and a fixed child under a moving body, while retaining the error for a moving body without <inertial>. The focused tests and all 235 MJCF importer tests pass locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] add_mjcf does not honor geom-based inertia selection semantics

3 participants