Skip to content

Fix MJCF importer to have correct body_q based on pos/quat xml attributes#565

Merged
eric-heiden merged 17 commits into
newton-physics:mainfrom
adenzler-nvidia:dev/adenzler/fix-mjcf-importer
Aug 18, 2025
Merged

Fix MJCF importer to have correct body_q based on pos/quat xml attributes#565
eric-heiden merged 17 commits into
newton-physics:mainfrom
adenzler-nvidia:dev/adenzler/fix-mjcf-importer

Conversation

@adenzler-nvidia

@adenzler-nvidia adenzler-nvidia commented Aug 18, 2025

Copy link
Copy Markdown
Member

This change fixes the MJCF importer to initialize the right world transforms based on combining the pos/quat tags with the parent xform.

It also makes sure that the joint_q values for free joint are initialized correctly with the world transforms.

Before your PR is "Ready for review"

  • All commits are signed-off to indicate that your contribution adheres to the Developer Certificate of Origin requirements
  • Necessary tests have been added and new examples are tested (see newton/tests/test_examples.py)
  • Documentation is up-to-date
  • Code passes formatting and linting checks with pre-commit run -a

Summary by CodeRabbit

  • New Features

    • More accurate MJCF importing with consistent world-space transforms for bodies and joints.
  • Bug Fixes

    • Fixed misaligned geometries and incorrect joint placement by composing world transforms through hierarchies.
    • Improved free/base joint behavior by deriving joint poses from joint-space references.
  • Refactor

    • Simplified recursive transform propagation; removed debug output.
  • Tests

    • Added extensive importer tests covering hierarchies, floating bases, scales, and quaternion conventions.
  • Chores

    • No public API changes; fixes are internal.

Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
@coderabbitai

coderabbitai Bot commented Aug 18, 2025

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Parses MJCF bodies with hierarchical transforms: computes local_xform from body pos/orientation (and scale), composes world_xform with parent_world_xform, threads world_xform through recursive parse_body calls, and uses world-space transforms for body and joint placement. Tests added covering transform composition and joint/world pose consistency.

Changes

Cohort / File(s) Summary of changes
MJCF importer: world transform propagation and joint pose updates
newton/_src/utils/import_mjcf.py
Added local_xform and world_xform computation; introduced parent_world_xform param in recursive parse_body; compose world transforms and thread them into recursive calls; use world_xform for builder.add_body; compute joint-space pose using scaled body position/orientation; removed debug print; public API unchanged.
Importer tests: extensive transform and joint validation
newton/tests/test_import_mjcf.py
Added comprehensive tests validating single body, root xform composition, hierarchical body composition, floating/free joints, chained rotations, scale propagation, and branching trees; verify world/body/joint poses and quaternion conventions using Warp math and parse_mjcf options.

Sequence Diagram(s)

sequenceDiagram
  participant parse_mjcf
  participant parse_body
  participant builder

  parse_mjcf->>parse_body: parse_body(root_body, parent_world_xform=None)
  activate parse_body
  Note right of parse_body: local_xform = T(body_pos*scale, body_ori)
  alt parent_world_xform provided
    parse_body->>parse_body: world_xform = parent_world_xform * local_xform
  else
    parse_body->>parse_body: world_xform = local_xform
  end
  parse_body->>builder: add_body(xform=world_xform)
  parse_body->>builder: add_joint(using body_pos_for_joints/body_ori_for_joints)
  loop for each child
    parse_body->>parse_body: parse_body(child, parent_world_xform=world_xform)
  end
  deactivate parse_body
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • eric-heiden
  • camevor

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@adenzler-nvidia adenzler-nvidia marked this pull request as draft August 18, 2025 09:31
@adenzler-nvidia

Copy link
Copy Markdown
Member Author

converting to draft, will add tests.

@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

🔭 Outside diff range comments (3)
newton/_src/utils/import_mjcf.py (3)

198-203: *Bug: xform is already a Transform; re-wrapping with wp.transform(xform) will fail

Earlier (Lines 89-93), xform is normalized to a wp.transform. Here you re-wrap it using wp.transform(*xform), but a Transform is not an iterable tuple. This will likely raise at runtime. Compose with axis_xform directly.

Apply this diff:

-    if xform is None:
-        xform = axis_xform
-    else:
-        xform = wp.transform(*xform) * axis_xform
+    xform = xform * axis_xform

Optional cleanup: You can also remove the earlier normalization block (Lines 89-93) and handle normalization once here, but the above minimal fix is sufficient.


290-312: Double application of incoming_xform for world geoms (link == -1)

incoming_xform is applied to tf twice when link == -1: once at Lines 291-295 and again at 310-312. This doubles the transform for worldbody geoms. Plane geoms depend on geom_pos/geom_rot (used to compute plane equation), so we should apply incoming_xform once to tf and refresh geom_pos/geom_rot only once for planes.

Apply this diff:

-            tf = wp.transform(geom_pos, geom_rot)
-            if link == -1 and incoming_xform is not None:
-                tf = incoming_xform * tf
-                geom_pos = tf.p
-                geom_rot = tf.q
+            tf = wp.transform(geom_pos, geom_rot)
@@
-            if incoming_xform is not None:
-                tf = incoming_xform * tf
+            if incoming_xform is not None:
+                tf = incoming_xform * tf
+                if link == -1:
+                    # keep geom_pos/geom_rot in sync for types (e.g., plane) that use them directly
+                    geom_pos = tf.p
+                    geom_rot = tf.q

577-584: Bug: base_joint dict case uses child=root (XML root), not link

When base_joint is a dict, you set base_joint["child"] = root, which is the XML root element, not the body index. This will break joint creation; it must be the current link.

Apply this diff:

-                    base_joint["parent"] = -1
-                    base_joint["child"] = root
+                    base_joint["parent"] = -1
+                    base_joint["child"] = link
🧹 Nitpick comments (2)
newton/_src/utils/import_mjcf.py (2)

434-440: Use consistent type annotations: prefer Transform over wp.transform

The rest of the file (e.g., parse_mjcf’s xform param) uses the alias Transform. Using wp.transform in annotations is inconsistent and not a valid typing construct; prefer Transform for readability and type checking.

Apply this diff:

-        parent_world_xform: wp.transform | None = None,
+        parent_world_xform: Transform | None = None,

254-432: General: world_xform should not be passed to per-body geoms (correct), but do validate worldbody geoms with tests

Your per-body shapes are correctly authored in the body-local frame (leveraging the body’s world transform from add_body). Worldbody geoms (link == -1) get incoming_xform applied, which is correct. Please add tests that validate:

  • Nested bodies with pos/quat compose to the expected world_xform.
  • Free joints’ joint_q reflect the composed world_xform.
  • Worldbody plane, box, and mesh geoms are transformed by xform exactly once.

I can open a test PR or generate pytest cases for a small 2-body chain (root + child), including a free-root variant. Do you want me to draft them?

📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 11a7403 and 3415e38.

📒 Files selected for processing (1)
  • newton/_src/utils/import_mjcf.py (9 hunks)
🔇 Additional comments (7)
newton/_src/utils/import_mjcf.py (7)

459-472: World transform propagation is correct (root and children).

The local_xform and world_xform composition matches the prior multiplication order used elsewhere (tf = incoming_xform * tf), so parent->child composition is consistent. This fixes the long-standing issue with pos/quat being treated as world transforms.


591-602: Initializing free-base dofs from world_xform looks good

Setting joint_q to the composed world transform addresses the original bug where free joints didn’t reflect world-space initialization.


613-614: Free joint parent_xform orientation: identity vs body orientation

You set parent_xform=Transform(0, body_ori). For a free joint (6-DOF), parent_xform often being identity is simpler to reason about (axes unrotated). Using body orientation moves that rotation into the parent frame. If intentional, all good; otherwise consider identity here.

If you want to switch to identity:

-                    parent_xform=wp.transform(wp.vec3(0.0, 0.0, 0.0), body_ori_for_joints),
+                    parent_xform=wp.transform(wp.vec3(0.0, 0.0, 0.0), wp.quat_identity()),

634-636: Parent/child xforms for non-free joints remain consistent

Using parent_xform as body_pos_for_joints + joint_pos and child_xform at joint_pos with identity rotation keeps joint frames aligned with MJCF semantics. This is consistent with previous behavior and the new world-space logic.


792-793: Passing world_xform down the hierarchy is correct

Threading parent_world_xform to children ensures proper composition of world-space transforms across the body tree.


912-914: Top-level call updated to seed the hierarchy

Seeding with parent_world_xform=None at world-body level is appropriate. Root bodies will correctly use the top-level xform when composing.


598-602: No action needed: quaternion ordering is consistent (xyzw)
All code paths—from MJCF/URDF imports through ModelBuilder to FK/I​K kernels and solvers—use wp.quat(x, y, z, w). The builder writes world_xform.q[0]..q[3] into joint_q[start+3..6], and every consumer constructs quaternions via wp.quat(qx, qy, qz, qw). No index swap is required.

Comment thread newton/_src/utils/import_mjcf.py

@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: 0

🔭 Outside diff range comments (3)
newton/_src/utils/import_mjcf.py (3)

198-203: Bug: incorrect re-derivation of xform with axis_xform; will TypeError when xform is already a Transform

At this point, xform has already been normalized to a wp.transform at Lines 89-93. Re-applying wp.transform(*xform) assumes xform is iterable (tuple of (p, q)), which will raise when xform is a Transform. Also, the None-guard is now unreachable since xform cannot be None anymore, making the branch misleading.

Replace with a simple post-multiplication by axis_xform.

Apply this diff:

-    axis_xform = wp.transform(wp.vec3(0.0), quat_between_axes(up_axis, builder.up_axis))
-    if xform is None:
-        xform = axis_xform
-    else:
-        xform = wp.transform(*xform) * axis_xform
+    axis_xform = wp.transform(wp.vec3(0.0), quat_between_axes(up_axis, builder.up_axis))
+    xform = xform * axis_xform

578-584: Bug: dict-based base_joint uses child=root instead of child=link

When base_joint is a dict, you set base_joint["child"] = root, but root here is the parsed XML root element, not the body index. This will cause incorrect behavior or errors. It should be the newly created body index (link).

Apply this diff:

-                    base_joint["parent"] = -1
-                    base_joint["child"] = root
+                    base_joint["parent"] = -1
+                    base_joint["child"] = link
                     base_joint["parent_xform"] = base_parent_xform
                     base_joint["child_xform"] = base_child_xform
                     base_joint["key"] = "base_joint"
                     builder.add_joint(**base_joint)

291-313: Double application of incoming_xform for world geoms (link == -1)

tf is multiplied by incoming_xform at Line 291 for world geoms, then again at Line 310 unconditionally when incoming_xform is set. This doubles the transform for world-level shapes and leads to incorrect placement, especially noticeable for planes where geom_pos/rot are also altered.

To avoid double transforms while retaining the plane normal computation, condition the second multiplication on link != -1:

-            if incoming_xform is not None:
-                tf = incoming_xform * tf
+            if incoming_xform is not None and link != -1:
+                tf = incoming_xform * tf

Alternatively, remove the early multiplication and compute plane normal and p using tf after a single application at this site.

🧹 Nitpick comments (1)
newton/_src/utils/import_mjcf.py (1)

549-606: Edge case: root body with and floating/base_joint flags

The control flow here is nuanced. For root with :

  • base_joint string: OK (D6 setup with position-only parent frame; rotation applied in child frame)
  • floating True: OK (free joint added and joint_q set from world_xform)
  • else: fixed base with _xform

Looks good overall; just consider explicitly documenting precedence when both base_joint and floating are provided (base_joint currently takes precedence).

📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 11a7403 and 3415e38.

📒 Files selected for processing (1)
  • newton/_src/utils/import_mjcf.py (9 hunks)
🔇 Additional comments (10)
newton/_src/utils/import_mjcf.py (10)

434-440: Signature update looks good; threading parent_world_xform is the right approach

Adding parent_world_xform to parse_body enables proper hierarchical world pose propagation without changing the public API. No concerns here.


459-476: World transform composition is correct (root = xform * local, child = parent_world * local)

  • Scaling applied to positions only is appropriate.
  • Fallback branch is fine as a defensive default.

534-537: Using world_xform for builder.add_body is the correct fix

Bodies now enter the builder with their world pose, aligning joint initialization later. Good change.


549-557: Initialize base joint transform from scaled body/joint pose (OK), but verify dict-based base_joint child assignment separately

The shift to scaled body_pos_for_joints (+ joint_pos) for _xform is correct. See separate comment below about dict-based base_joint child assignment.


591-602: Free base initialization from world_xform is correct; confirm quaternion storage order

Setting joint_q from world_xform ensures the floating base starts at the proper world pose. Please confirm builder.joint_q expects quaternion as (x, y, z, w), which matches wp.quat’s layout.

If helpful, I can add a small unit test that loads a trivial MJCF with a rotated/scaled root and asserts the builder.joint_q matches the expected wp.transform. Want me to draft it?


614-614: Verify parent_xform semantics for non-root free joints

Passing parent_xform=wp.transform(0, body_ori_for_joints) assumes the joint frame on the parent side carries the body’s orientation. Depending on ModelBuilder semantics, a free joint typically uses identity frames and encodes the initial relative pose solely in joint_q. If non-root freejoints are possible in your pipeline, please confirm this matches expectations.

I can cross-check ModelBuilder’s add_joint_free contract in this repo and propose an adjustment if needed.


616-624: Setting non-root free joint_q from world_xform may need to be relative to parent

For free joints that are not at the root, joint_q generally represents the child’s pose relative to the parent frame, not world. If non-root is possible in MJCF (and supported), joint_q here might need to be computed as relative(parent_world_xform, child_world_xform).

Would you like me to add a helper to compute relative transforms and wire it here only when parent != -1?


634-636: Using scaled parent_xform for non-free joints is consistent with the rest of the fix

parent_xform based on body_pos_for_joints + joint_pos with body_ori_for_joints looks correct.


792-793: World transform propagation into recursion is correct

Passing parent_world_xform=world_xform ensures consistent world poses for all descendants.


913-914: Root invocation updated correctly

Initializing recursion with parent_world_xform=None is aligned with the new logic.

Comment thread newton/_src/utils/import_mjcf.py Outdated
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
@adenzler-nvidia adenzler-nvidia marked this pull request as ready for review August 18, 2025 11:49

@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

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

227-227: Make importer up-axis explicit to avoid hidden axis-correction

The importer may apply an axis correction if its default up_axis differs from the builder’s up_axis. To make the test stable across defaults, pass up_axis="Z" explicitly.

-        newton.utils.parse_mjcf(mjcf_content, builder)
+        newton.utils.parse_mjcf(mjcf_content, builder, up_axis="Z")

If you prefer, apply the same tweak to all parse_mjcf calls in this file to future-proof these tests.


236-239: Compare quaternions modulo sign to reduce brittleness

Numerically equivalent rotations can flip sign (q ≡ -q). Use a helper assertion that accepts both, to reduce intermittent failures across platforms and refactors.

Add this helper to TestImportMjcf (outside changed lines):

def assert_quat_allclose(self, q_actual, q_expected, atol=1e-6, rtol=1e-6):
    qa = np.asarray(q_actual)
    qe = np.asarray(q_expected)
    if np.allclose(qa, qe, rtol=rtol, atol=atol):
        return
    if np.allclose(qa, -qe, rtol=rtol, atol=atol):
        return
    self.fail(f"Quaternions not equal up to sign: {qa} vs {qe}")

Then replace usages like:

np.testing.assert_allclose(body_quat, expected_quat, atol=1e-6)

with:

self.assert_quat_allclose(body_quat, expected_quat, atol=1e-6)

This applies to all quaternion equality checks below.


241-280: Root body + custom xform composition is correct; minor robustness tweaks

The transform composition expected_xform = custom_xform * body_xform aligns with the importer’s semantics. Consider two small refinements:

-        newton.utils.parse_mjcf(mjcf_content, builder, xform=custom_xform)
+        newton.utils.parse_mjcf(mjcf_content, builder, xform=custom_xform, up_axis="Z")

And ensure dtype compatibility in asserts (Warp types generally work, but np.asarray is safer):

-        np.testing.assert_allclose(body_pos, expected_pos, atol=1e-6)
-        np.testing.assert_allclose(body_quat, expected_quat, atol=1e-6)
+        np.testing.assert_allclose(body_pos, np.asarray(expected_pos), atol=1e-6)
+        self.assert_quat_allclose(body_quat, np.asarray(expected_quat), atol=1e-6)

301-301: Stabilize hierarchy test against up-axis defaults

To avoid implicit axis-correction affecting expectations, make up-axis explicit.

-        newton.utils.parse_mjcf(mjcf_content, builder)
+        newton.utils.parse_mjcf(mjcf_content, builder, up_axis="Z")

320-324: Remove unused placeholder initialization

The temporary quat_child placeholder is assigned then immediately overwritten. Drop it to reduce noise.

-        quat_child = np.array([0, 0, 0, 1])  # placeholder, will be overwritten
-        quat_child_mjcf = np.array([0.7071068, 0, 0, 0.7071068])
-        # MJCF: [w, x, y, z] → warp: [x, y, z, w]
-        quat_child = np.array([quat_child_mjcf[1], quat_child_mjcf[2], quat_child_mjcf[3], quat_child_mjcf[0]])
+        quat_child_mjcf = np.array([0.7071068, 0, 0, 0.7071068])  # [w, x, y, z]
+        # MJCF [w, x, y, z] → Warp [x, y, z, w]
+        quat_child = np.array([quat_child_mjcf[1], quat_child_mjcf[2], quat_child_mjcf[3], quat_child_mjcf[0]])

351-351: Avoid accidental axis-correction in floating-base test

Make up-axis explicit for stability across environment defaults.

-        newton.utils.parse_mjcf(mjcf_content, builder, floating=True)
+        newton.utils.parse_mjcf(mjcf_content, builder, floating=True, up_axis="Z")

402-402: Make up-axis explicit in hierarchy + freejoint test

Same rationale as above; avoids hidden rotations from axis correction.

-        newton.utils.parse_mjcf(mjcf_content, builder)
+        newton.utils.parse_mjcf(mjcf_content, builder, up_axis="Z")

441-445: Remove debug prints from tests

These prints will spam test output and aren’t needed for assertions. Prefer logging behind a verbosity flag or drop entirely.

-        # Debug output to see what's happening
-        print(f"Root transform: pos={root_xform.p}, quat={root_xform.q}")
-        print(f"Child local transform: pos={child_local_xform.p}, quat={child_local_xform.q}")
-        print(f"Expected composed: pos={expected_pos}, quat={expected_quat}")
-        print(f"Actual joint_q: pos={joint_pos}, quat={joint_quat}")

469-469: Explicit up-axis makes chain test resilient

-        newton.utils.parse_mjcf(mjcf_content, builder)
+        newton.utils.parse_mjcf(mjcf_content, builder, up_axis="Z")

526-526: Explicit up-axis in scale test

-        newton.utils.parse_mjcf(mjcf_content, builder, scale=2.0)
+        newton.utils.parse_mjcf(mjcf_content, builder, scale=2.0, up_axis="Z")

570-570: Explicit up-axis in branching tree test

-        newton.utils.parse_mjcf(mjcf_content, builder)
+        newton.utils.parse_mjcf(mjcf_content, builder, up_axis="Z")
📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3415e38 and 0e9d906.

📒 Files selected for processing (2)
  • newton/_src/utils/import_mjcf.py (8 hunks)
  • newton/tests/test_import_mjcf.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • newton/_src/utils/import_mjcf.py
🧰 Additional context used
🧬 Code Graph Analysis (1)
newton/tests/test_import_mjcf.py (2)
newton/_src/utils/import_mjcf.py (1)
  • parse_mjcf (32-927)
newton/tests/test_cloth.py (1)
  • finalize (773-815)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run GPU Unit Tests on AWS EC2 (Pull Request)
  • GitHub Check: Run GPU Benchmarks (Pull Request)
🔇 Additional comments (1)
newton/tests/test_import_mjcf.py (1)

215-240: Solid baseline check for single-body world transform (pos + quat) — LGTM

This test correctly validates MJCF WXYZ-to-Warp XYZW conversion and translation passthrough for a single root body. Expectations match a 90° Z rotation.

Comment thread newton/tests/test_import_mjcf.py

@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: 0

🔭 Outside diff range comments (1)
newton/_src/utils/import_mjcf.py (1)

578-583: Bug: base_joint dict path sets child to the XML root instead of the link

Setting base_joint["child"] = root will pass the XML root element to add_joint, which should be an integer link index. This will mis-wire the joint or raise at runtime.

Apply:

-                    base_joint["child"] = root
+                    base_joint["child"] = link
♻️ Duplicate comments (1)
newton/_src/utils/import_mjcf.py (1)

594-595: Use wp.vec3 default here too (remaining tuple default)

This site still defaults joint_pos to a Python tuple, which can trigger Warp deprecation warnings and potential type mismatch.

Apply:

-            joint_pos = joint_pos[0] if len(joint_pos) > 0 else (0.0, 0.0, 0.0)
+            joint_pos = joint_pos[0] if len(joint_pos) > 0 else wp.vec3(0.0, 0.0, 0.0)
🧹 Nitpick comments (4)
newton/_src/utils/import_mjcf.py (2)

459-471: Simplify world transform composition to one branch

You can collapse the three-branch logic into a single expression to reduce cognitive load and avoid edge-case drift.

Apply:

-        # Compose transforms properly through the hierarchy
-        if parent == -1:
-            # Root body: apply the root transform
-            world_xform = xform * local_xform
-        elif parent_world_xform is not None:
-            # Child body: compose with parent's world transform
-            world_xform = parent_world_xform * local_xform
-        else:
-            # Fallback (shouldn't happen in normal cases)
-            world_xform = local_xform
+        # Compose with either the passed parent world transform or the import root xform
+        world_xform = (parent_world_xform or xform) * local_xform

And call parse_body for roots with xform as the initial parent transform:

-    for body in world.findall("body"):
-        parse_body(body, -1, world_defaults, parent_world_xform=None)
+    for body in world.findall("body"):
+        parse_body(body, -1, world_defaults, parent_world_xform=xform)

598-603: Verify: parent_xform for free joints is necessary?

ModelBuilder.add_joint_free typically initializes joint_q from the child body's xform; passing a non-identity parent_xform can rotate the joint frame relative to the parent unnecessarily and risks double-applying orientation.

Please confirm this is intended. If not needed, drop parent_xform for clarity and to align with builder semantics.

Proposed change:

-                builder.add_joint_free(
-                    link,
-                    key="_".join(joint_name),
-                    parent_xform=wp.transform(wp.vec3(0.0, 0.0, 0.0), body_ori_for_joints),
-                )
+                builder.add_joint_free(
+                    link,
+                    key="_".join(joint_name),
+                )
newton/tests/test_import_mjcf.py (2)

113-119: Make mesh maxhullvert test robust to shape ordering

Iterating range(3) assumes the first three shapes are meshes. Filter across all shapes for those with maxhullvert instead.

Apply:

-            # Check that meshes have correct maxhullvert values
-            # Note: This assumes meshes are added in order they appear in MJCF
-            meshes = [model.shape_source[i] for i in range(3) if hasattr(model.shape_source[i], "maxhullvert")]
-
-            if len(meshes) >= 3:
-                self.assertEqual(meshes[0].maxhullvert, 32)
-                self.assertEqual(meshes[1].maxhullvert, 128)
-                self.assertEqual(meshes[2].maxhullvert, 64)  # Default value
+            # Check that meshes have correct maxhullvert values
+            meshes = [src for src in model.shape_source if hasattr(src, "maxhullvert")]
+            self.assertEqual(len(meshes), 3)
+            self.assertEqual(meshes[0].maxhullvert, 32)
+            self.assertEqual(meshes[1].maxhullvert, 128)
+            self.assertEqual(meshes[2].maxhullvert, 64)  # Default value

441-445: Remove debug prints from tests

These prints add noise to CI logs; assertions already capture mismatches.

Apply:

-        # Debug output to see what's happening
-        print(f"Root transform: pos={root_xform.p}, quat={root_xform.q}")
-        print(f"Child local transform: pos={child_local_xform.p}, quat={child_local_xform.q}")
-        print(f"Expected composed: pos={expected_pos}, quat={expected_quat}")
-        print(f"Actual joint_q: pos={joint_pos}, quat={joint_quat}")
📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3415e38 and 0e9d906.

📒 Files selected for processing (2)
  • newton/_src/utils/import_mjcf.py (8 hunks)
  • newton/tests/test_import_mjcf.py (1 hunks)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run GPU Unit Tests on AWS EC2 (Pull Request)
  • GitHub Check: Run GPU Benchmarks (Pull Request)
🔇 Additional comments (5)
newton/_src/utils/import_mjcf.py (4)

434-440: Good: threading parent world transform through the recursion

Adding parent_world_xform to parse_body is the right move to compose transforms hierarchically without re-deriving state.


535-537: Correct: bodies now get composed world transforms

Passing xform=world_xform to add_body aligns body_q with the compounded XML pos/quat and importer xform.


549-551: Nice: default joint pos uses wp.vec3 to avoid Warp deprecation

This prevents tuple + wp.vec3 coercion and future breakage. Good catch.


771-772: Good: pass composed world transform into recursion

Propagating world_xform ensures all descendants compose relative to their true parents.

newton/tests/test_import_mjcf.py (1)

215-639: Strong, targeted coverage for transform composition and free-joint initialization

This suite exercises root/child composition, importer xform, scaling, branching, and joint_q/body_q consistency. Nicely done.

Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
@adenzler-nvidia

Copy link
Copy Markdown
Member Author

@eric-heiden - I might have been overeager. Does the newton free joint follow MuJoCo in the sense the joint_q is always in world frame, or does it respect the transform hierarchy?

@eric-heiden

Copy link
Copy Markdown
Member

@eric-heiden - I might have been overeager. Does the newton free joint follow MuJoCo in the sense the joint_q is always in world frame, or does it respect the transform hierarchy?

@adenzler-nvidia The free joint will follow the transform hierarchy so if there were a parent body the free joint's coordinates would be relative to that parent. But this is not possible in MJCF so we can assume in this case the free joint will always be in world coordinates.

Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>

@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

🔭 Outside diff range comments (2)
newton/_src/utils/import_mjcf.py (2)

577-584: base_joint dict branch sets child to XML root instead of link

Passing child=root (the XML element) into builder.add_joint is a bug. It should be the body index link.

Apply this diff:

-                    base_joint["child"] = root
+                    base_joint["child"] = link

606-614: Rotate joint_pos into the parent frame when building parent_xform

parent_xform should be T_parent->joint = T_parent->child ∘ T_child->joint. Given joint_pos is specified in the child frame, you need to rotate it by body_ori_for_joints before adding to body_pos_for_joints. Without this, anchors are incorrect whenever the child has a rotation.

Apply this diff:

-                    parent_xform=wp.transform(body_pos_for_joints + joint_pos, body_ori_for_joints),
+                    parent_xform=wp.transform(
+                        body_pos_for_joints + wp.quat_rotate(body_ori_for_joints, joint_pos),
+                        body_ori_for_joints,
+                    ),

If helpful, I can add a minimal test where a child body has a nontrivial orientation and a joint_pos offset, verifying the computed anchor matches T_parent->child ∘ T_child->joint.

♻️ Duplicate comments (1)
newton/_src/utils/import_mjcf.py (1)

593-595: Good: default joint_pos is now a wp.vec3

This resolves the Warp deprecation warning and prevents type-mismatch at runtime. Thanks for addressing the earlier feedback consistently in both branches.

🧹 Nitpick comments (2)
newton/_src/utils/import_mjcf.py (2)

459-476: World transform composition is correct; minor naming/consistency nit

local_xform and world_xform are composed correctly, and body_pos_for_joints/body_ori_for_joints are cleanly separated for joint construction. No issues here.

If you want to avoid confusion later, consider renaming body_pos_for_joints to child_pos_in_parent and body_ori_for_joints to child_ori_in_parent to make the frame explicit.


89-93: Deduplicate and harden xform normalization with up-axis alignment

Minor cleanup: xform is normalized at Lines 89–93 and then re-normalized/checked again at Lines 198–203. Because xform is set to a non-None value at Line 90, the “if xform is None” branch at Line 199 is effectively dead. Also, re-applying wp.transform(*xform) when xform is already a wp.transform relies on unpacking semantics that may not be guaranteed.

Consider normalizing exactly once near the up-axis alignment and handling both tuple and wp.transform inputs explicitly.

Apply this diff to simplify:

@@
-    if xform is None:
-        xform = wp.transform()
-    else:
-        xform = wp.transform(*xform)
+    # Defer normalization until after up-axis alignment is known
@@
-    axis_xform = wp.transform(wp.vec3(0.0), quat_between_axes(up_axis, builder.up_axis))
-    if xform is None:
-        xform = axis_xform
-    else:
-        xform = wp.transform(*xform) * axis_xform
+    axis_xform = wp.transform(wp.vec3(0.0), quat_between_axes(up_axis, builder.up_axis))
+    # Normalize xform into a wp.transform and compose up-axis adjustment exactly once
+    if xform is None:
+        xform = axis_xform
+    elif isinstance(xform, wp.transform):
+        xform = xform * axis_xform
+    else:
+        xform = wp.transform(*xform) * axis_xform

Also applies to: 198-203

📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0e9d906 and f941a55.

📒 Files selected for processing (2)
  • newton/_src/utils/import_mjcf.py (8 hunks)
  • newton/tests/test_import_mjcf.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • newton/tests/test_import_mjcf.py
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run GPU Unit Tests on AWS EC2 (Pull Request)
  • GitHub Check: Run GPU Benchmarks (Pull Request)
  • GitHub Check: run-newton-tests / newton-unittests (windows-latest)
  • GitHub Check: run-newton-tests / newton-unittests (ubuntu-latest)
🔇 Additional comments (4)
newton/_src/utils/import_mjcf.py (4)

434-440: Good API: threading parent world transform into parse_body

Passing parent_world_xform down the recursion is the right call to ensure correct hierarchical composition.


534-536: Using world_xform for body creation is correct

This ensures the body’s initial pose fully reflects the parent chain, root xform, and up-axis conversion.


763-771: Propagating world_xform to children is correct

Forwarding parent_world_xform=world_xform ensures consistent hierarchical poses throughout parsing.


889-892: Root invocation with parent_world_xform=None is appropriate

Clear separation between root-body composition and child recursion. Looks good.

Comment thread newton/_src/utils/import_mjcf.py
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>

@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: 0

🔭 Outside diff range comments (2)
newton/_src/utils/import_mjcf.py (2)

545-576: base_joint dict: child set to XML root instead of the body index

When base_joint is a dict, you assign child=root (the XML element), which will break joint creation. This should be the newly created body index (link).

Apply this diff:

-                elif isinstance(base_joint, dict):
-                    base_joint["parent"] = -1
-                    base_joint["child"] = root
-                    base_joint["parent_xform"] = base_parent_xform
-                    base_joint["child_xform"] = base_child_xform
-                    base_joint["key"] = "base_joint"
-                    builder.add_joint(**base_joint)
+                elif isinstance(base_joint, dict):
+                    base_joint["parent"] = -1
+                    base_joint["child"] = link
+                    base_joint["parent_xform"] = base_parent_xform
+                    base_joint["child_xform"] = base_child_xform
+                    base_joint["key"] = "base_joint"
+                    builder.add_joint(**base_joint)

596-606: Parent joint frame ignores child's rotation for joint_pos (misplaced anchors when child is rotated)

In the non-root joint path, parent_xform uses body_pos_for_joints + joint_pos without rotating joint_pos by the child body’s orientation. This yields incorrect parent-side joint anchor when the child has a local rotation.

Apply this diff:

-                    parent_xform=wp.transform(body_pos_for_joints + joint_pos, body_ori_for_joints),
+                    parent_xform=wp.transform(
+                        body_pos_for_joints + wp.quat_rotate(body_ori_for_joints, joint_pos),
+                        body_ori_for_joints
+                    ),

Note: The special handling in the root base_joint path to not rotate the base joint’s axis is orthogonal and doesn’t apply here; regular joints should rotate their local offsets into the parent frame.

🧹 Nitpick comments (3)
newton/_src/utils/import_mjcf.py (1)

581-583: Avoid hardcoded joint name for floating-base free joint

Hardcoding "floating_base" is brittle. Prefer the MJCF-provided freejoint name if present (or default to "_freejoint") to keep naming consistent with the non-base path.

Apply this diff:

-            elif floating is not None and floating:
-                builder.add_joint_free(link, key="floating_base")
+            elif floating is not None and floating:
+                builder.add_joint_free(link, key=freejoint_tags[0].attrib.get("name", f"{body_name}_freejoint"))
newton/tests/test_import_mjcf.py (2)

332-379: Make joint lookup resilient to name changes

Relying on the exact name "floating_body_freejoint" can be brittle. Prefer a fallback: if that name is missing, find a free joint whose name ends with "_freejoint" and contains "floating_body" (or use a model API mapping if available).

Apply this diff:

-        joint_idx = model.joint_key.index("floating_body_freejoint")
+        joint_names = model.joint_key
+        if "floating_body_freejoint" in joint_names:
+            joint_idx = joint_names.index("floating_body_freejoint")
+        else:
+            joint_idx = next(
+                i for i, n in enumerate(joint_names)
+                if n.endswith("_freejoint") and "floating_body" in n
+            )

230-240: Optional: make quaternion asserts sign-agnostic

Quaternions q and -q represent the same rotation; numerical operations can occasionally flip signs. Current asserts may fail in such cases.

You can add a small helper and use it where you compare quats:

def assert_quat_allclose(testcase, actual, expected, **kwargs):
    actual = np.asarray(actual)
    expected = np.asarray(expected)
    if np.dot(actual, expected) < 0.0:
        expected = -expected
    np.testing.assert_allclose(actual, expected, **kwargs)

Then replace np.testing.assert_allclose(body_quat, expected, ...) with assert_quat_allclose(self, body_quat, expected, ...).

Also applies to: 278-280, 311-313, 329-331, 426-428, 437-439, 516-519, 531-533, 543-545, 555-557, 567-569

📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f941a55 and eed6048.

📒 Files selected for processing (2)
  • newton/_src/utils/import_mjcf.py (8 hunks)
  • newton/tests/test_import_mjcf.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
newton/tests/test_import_mjcf.py (2)
newton/_src/utils/import_mjcf.py (1)
  • parse_mjcf (32-918)
newton/tests/test_cloth.py (1)
  • finalize (773-815)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run GPU Benchmarks (Pull Request)
  • GitHub Check: Run GPU Unit Tests on AWS EC2 (Pull Request)
🔇 Additional comments (9)
newton/_src/utils/import_mjcf.py (3)

459-467: World transform composition and recursion threading look correct

Local → world composition via (parent_world_xform or xform) * local_xform and passing parent_world_xform through recursion is the right fix. This aligns with the tests and ensures body_q is truly world-space.

Also applies to: 762-763, 883-884


541-543: Good fix: default joint_pos now uses wp.vec3

Using wp.vec3 instead of a tuple prevents Warp deprecation warnings and potential type errors when adding to a wp.vec3.


527-529: Bodies now receive world_xform at creation

Passing xform=world_xform into add_body is consistent with the new composition logic and matches the new tests’ expectations for body_q.

newton/tests/test_import_mjcf.py (6)

215-240: LGTM: single-body world transform validation is correct

Solid sanity check for position and quaternion convention mapping (MJCF [w,x,y,z] → Warp [x,y,z,w]).


241-280: LGTM: custom xform composition test is accurate

Composing expected_xform = custom_xform * body_xform mirrors importer logic.


281-331: LGTM: parent-child hierarchy composition test reads correctly

Validates both translation and quaternion composition for child relative to rotated parent.


380-439: LGTM: rotation chain test covers quaternion composition well

Good coverage for cumulative rotations across a chain.


440-473: LGTM: scale propagation test is correct

Verifies that child positions are scaled in world as expected.


474-569: LGTM: branching hierarchy test is thorough

Covers multiple quaternion encodings and independent branches. Nice cross-check of orientation conversions.

@eric-heiden eric-heiden 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.

LGTM, feel free to merge! Thanks for adding the tests also.

@eric-heiden eric-heiden enabled auto-merge (squash) August 18, 2025 17:43
@eric-heiden eric-heiden merged commit b223e5d into newton-physics:main Aug 18, 2025
11 checks passed
mzamoramora-nvidia pushed a commit to MiguelZamoraM/newton that referenced this pull request Aug 20, 2025
…utes (newton-physics#565)

Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Miguel Zamora <mazamoramora@nvidia.com>
eric-heiden pushed a commit to eric-heiden/newton that referenced this pull request Jan 28, 2026
…utes (newton-physics#565)

Signed-off-by: Alain Denzler <adenzler@nvidia.com>
vidurv-nvidia pushed a commit to vidurv-nvidia/newton that referenced this pull request Mar 6, 2026
Fixes deprecated scipy imports in the devices module for interfacing
with SE3 gamepad, keyboard and spacemouse.

- Bug fix (non-breaking change which fixes an issue)

- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have updated the changelog and the corresponding version in the
extension's `config/extension.toml` file
- [x] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there
vidurv-nvidia pushed a commit to vidurv-nvidia/newton that referenced this pull request Mar 6, 2026
…#565)

Turn off async rendering for replicator to avoid race condition for
external camera rendering

# Description

<!--
Thank you for your interest in sending a pull request. Please make sure
to check the contribution guidelines.

Link:
https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html
-->

Please include a summary of the change and which issue is fixed. Please
also include relevant motivation and context.
List any dependencies that are required for this change.

Fixes # (issue)

<!-- As a practice, it is recommended to open an issue to have
discussions on the proposed pull request.
This makes it easier for the community to keep track of what is being
developed or added, and if a given feature
is demanded by more than one party. -->

## Type of change

<!-- As you go through the list, delete the ones that are not
applicable. -->

- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- This change requires a documentation update

## Screenshots

Please attach before and after screenshots of the change if applicable.

<!--
Example:

| Before | After |
| ------ | ----- |
| _gif/png before_ | _gif/png after_ |

To upload images to a PR -- simply drag and drop an image while in edit
mode and it should upload the image directly. You can then paste that
source into the above before/after sections.
-->

## Checklist

- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] I have updated the changelog and the corresponding version in the
extension's `config/extension.toml` file
- [x] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there

<!--
As you go through the checklist above, you can mark something as done by
putting an x character in it

For example,
- [x] I have done this task
- [ ] I have not done this task
-->
mmacklin pushed a commit to mmacklin/newton that referenced this pull request Apr 7, 2026
…utes (newton-physics#565)

Signed-off-by: Alain Denzler <adenzler@nvidia.com>
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.

2 participants