Skip to content

Simple articulation USD import fails #517#527

Merged
vreutskyy merged 11 commits into
newton-physics:mainfrom
vreutskyy:517-simple-articulation-usd-import-fails
Aug 18, 2025
Merged

Simple articulation USD import fails #517#527
vreutskyy merged 11 commits into
newton-physics:mainfrom
vreutskyy:517-simple-articulation-usd-import-fails

Conversation

@vreutskyy

@vreutskyy vreutskyy commented Aug 12, 2025

Copy link
Copy Markdown
Member

Description

This PR fixes a bug in USD import where joints with missing body1 references would cause a KeyError: -1.

Problem

When importing USD files, if a joint only specifies body0 but not body1, the import would fail with a KeyError when trying to access body_shapes[-1].

Solution

The fix properly handles this case by following the standard physics engine convention:

  • Normal behavior: body0 is parent, body1 is child
  • When body1 is missing: body0 becomes child, world (-1) becomes parent

This is implemented with a simple check that swaps parent and child IDs when child_id == -1.

Testing

Added test_import_revolute_articulation which tests importing a USD file (revolute_articulation.usda) containing a FixedJoint with only body0 specified. The test verifies that the joint correctly connects to the world frame.

Closes #517

Newton Migration Guide

Please ensure the migration guide for warp.sim users is up-to-date with the changes made in this MR.

  • The migration guide in docs/migration.rst is up-to date
    • No migration guide changes needed as this is a bug fix that doesn't change the API

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
    • Added clear comments in the code explaining the behavior
  • Code passes formatting and linting checks with pre-commit run -a

Summary by CodeRabbit

  • New Features

    • Added a sample USD scene showcasing a physics-enabled revolute articulation with lighting and render settings.
  • Bug Fixes

    • Improved USD import to correctly handle joints with a missing counterpart by attaching to the world when appropriate.
    • Enhanced robustness of joint transform handling and added clearer verbose messages for world-connected joints.
  • Tests

    • Added tests validating import of revolute articulations, ensuring expected bodies and joint types are correctly recognized.

Signed-off-by: Viktor Reutskyy <vreutskyy@nvidia.com>
@coderabbitai

coderabbitai Bot commented Aug 12, 2025

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a new USD asset for a simple revolute articulation, introduces a unit test to import it, and updates parse_joint in import_usd.py to handle joints with unresolved endpoints by swapping to connect the known body to the world or skipping if both ends are unresolved.

Changes

Cohort / File(s) Summary
USD Test Asset
newton/tests/assets/revolute_articulation.usda
New USD 1.0 scene defining an articulated robot: Xform root with two rigid bodies (Sphere CenterPivot, Cube Arm), a FixedJoint on CenterPivot, a RevoluteJoint between Arm and CenterPivot, a PhysicsScene, World with DistantLight, and Render settings.
Tests
newton/tests/test_import_usd.py
Adds test_import_revolute_articulation: loads the new USD, asserts bodies and joints, checks FREE root joint, REVOLUTE between CenterPivot and Arm, and FIXED joint to world when one endpoint is missing.
USD Import Logic
newton/_src/utils/import_usd.py
Enhances parse_joint: precomputes child_tf; handles unresolved endpoints—skips when both unknown, swaps when child unknown so world becomes parent; updates transforms consistently; optional verbose logs.

Sequence Diagram(s)

sequenceDiagram
  participant Test as Test
  participant Parser as parse_usd
  participant J as parse_joint
  participant B as Builder

  Test->>Parser: parse_usd(revolute_articulation.usda)
  Parser->>J: parse_joint(joint_desc)
  alt both ends unresolved
    J-->>Parser: skip joint
  else child unresolved, parent resolved
    J->>J: swap IDs (parent=-1 world, child=known)
    J->>J: adjust parent/child transforms
    J->>B: add_joint_fixed/add_joint_* (world -> body)
  else both resolved
    J->>B: add_joint_* (parent -> child)
  end
  Parser-->>Test: import result (bodies, joints, mappings)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Assessment against linked issues

Objective Addressed Explanation
Fix import failure when a simple articulation USD has a joint endpoint unresolved, avoiding KeyError and correctly modeling world connection (#517)
Add regression coverage for the failing articulation import path (#517)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Extensive Render and RTX settings embedded in test asset (newton/tests/assets/revolute_articulation.usda) Not required to validate USD import of articulations; included render pipelines are unrelated to the import logic objectives. Unclear if necessary for tests but functionally out of scope.

Possibly related PRs

Suggested reviewers

  • eric-heiden
  • adenzler-nvidia
  • Milad-Rakhsha-NV

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.

@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/utils/import_usd.py (1)

518-527: Swap also the joint frames when swapping parent/child IDs to keep anchors consistent

You correctly swap IDs when body1 is missing, but parent_tf/child_tf remain tied to localPose0/localPose1 of the original roles. This misassigns anchor frames (parent frame should come from the world side when world becomes parent). Fix by swapping the frames too and guarding against the degenerate case where both bodies resolve to world.

Apply this diff:

-        child_id = path_body_map.get(child_path, -1)
-        # If child_id is -1, swap parent and child
-        if child_id == -1:
-            parent_id, child_id = child_id, parent_id
-            if verbose:
-                print(f"Joint {joint_path} connects {parent_path} to world")
-        parent_tf = wp.transform(joint_desc.localPose0Position, from_gfquat(joint_desc.localPose0Orientation))
-        if incoming_xform is not None:
-            parent_tf = wp.mul(incoming_xform, parent_tf)
-        child_tf = wp.transform(joint_desc.localPose1Position, from_gfquat(joint_desc.localPose1Orientation))
+        child_id = path_body_map.get(child_path, -1)
+
+        # If body1 is missing (world), make world the parent and body0 the child.
+        swapped = False
+        if child_id == -1 and getattr(joint_desc, "body1", None) in (None, Sdf.Path.emptyPath):
+            if parent_id == -1:
+                if verbose:
+                    print(f"Skipping joint {joint_path}: both bodies resolve to world (-1)")
+                return
+            parent_id, child_id = -1, parent_id
+            swapped = True
+            if verbose:
+                print(f"Joint {joint_path} connects {parent_path} to world")
+
+        # Compute joint frames; if swapped, also swap the frames so they stay consistent
+        # with the parent/child bodies.
+        tf0 = wp.transform(joint_desc.localPose0Position, from_gfquat(joint_desc.localPose0Orientation))
+        tf1 = wp.transform(joint_desc.localPose1Position, from_gfquat(joint_desc.localPose1Orientation))
+        parent_tf = tf1 if swapped else tf0
+        child_tf = tf0 if swapped else tf1
+        if incoming_xform is not None:
+            parent_tf = wp.mul(incoming_xform, parent_tf)
🧹 Nitpick comments (2)
newton/tests/assets/revolute_articulation.usda (1)

1-40: Consider trimming render/camera metadata to keep the test asset minimal

The asset contains extensive render settings, camera presets, and kit-specific metadata unrelated to the importer logic under test. A smaller, hand-authored USDA focused on physics prims (Articulation, two RigidBodies, the two Joints, PhysicsScene) will be easier to maintain and review.

If helpful, I can provide a pared-down USDA with the same semantics (world Z-up, metersPerUnit=1) that only includes the necessary physics prims.

newton/tests/test_import_usd.py (1)

101-147: Make assertions robust to joint ordering by resolving indices via joint keys

The test assumes a specific joint order ([FREE, REVOLUTE, FIXED]); that can change with topology sorting or future importer tweaks. Resolve indices from builder.joint_key to avoid flakiness.

Apply this diff:

-        # Verify joint types
-        self.assertEqual(builder.joint_type[0], newton.JOINT_FREE)
-        self.assertEqual(builder.joint_type[1], newton.JOINT_REVOLUTE)
-        self.assertEqual(builder.joint_type[2], newton.JOINT_FIXED)
+        # Verify joint types by key to avoid relying on order
+        idx_by_key = {k: i for i, k in enumerate(builder.joint_key)}
+        self.assertIn(newton.JOINT_FREE, builder.joint_type)  # root free joint present
+        self.assertEqual(
+            builder.joint_type[idx_by_key["/Articulation/Arm/RevoluteJoint"]],
+            newton.JOINT_REVOLUTE,
+        )
+        self.assertEqual(
+            builder.joint_type[idx_by_key["/Articulation/CenterPivot/FixedJoint"]],
+            newton.JOINT_FIXED,
+        )
 
-        # The key test: verify the FixedJoint connects CenterPivot to world
-        # because body1 was missing in the USD file
-        fixed_joint_idx = 2
+        # The key test: verify the FixedJoint connects CenterPivot to world
+        # because body1 was missing in the USD file
+        fixed_joint_idx = idx_by_key["/Articulation/CenterPivot/FixedJoint"]
         self.assertEqual(builder.joint_parent[fixed_joint_idx], -1)  # Parent is world (-1)
         # Child should be CenterPivot (which was body0 in the USD)
         center_pivot_idx = builder.body_key.index("/Articulation/CenterPivot")
         self.assertEqual(builder.joint_child[fixed_joint_idx], center_pivot_idx)
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between d898d06 and adb5146.

📒 Files selected for processing (3)
  • newton/tests/assets/revolute_articulation.usda (1 hunks)
  • newton/tests/test_import_usd.py (1 hunks)
  • newton/utils/import_usd.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
newton/tests/test_import_usd.py (2)
newton/sim/builder.py (2)
  • ModelBuilder (80-3923)
  • collapse_fixed_joints (1581-1855)
newton/utils/import_usd.py (1)
  • parse_usd (34-1274)
⏰ 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 Benchmarks (Pull Request)
  • GitHub Check: Run GPU Unit Tests on AWS EC2 (Pull Request)
  • GitHub Check: run-newton-tests / newton-unittests (windows-latest)
  • GitHub Check: run-newton-tests / newton-unittests (ubuntu-latest)
🔇 Additional comments (2)
newton/utils/import_usd.py (1)

518-523: Narrow the swap condition to “missing body1” to avoid misinterpreting disabled/ignored bodies

Using child_id == -1 alone will also trigger for bodies filtered out (disabled joints/bodies, ignored paths, etc.). Prefer checking that body1 is actually unauthored/empty instead of just missing from path_body_map (see the diff above using Sdf.Path.emptyPath). This reduces false positives.

Please confirm whether there are scenarios in your stages where body1 can be authored but filtered out (e.g., via only_load_enabled_rigid_bodies, ignore_paths). If so, the narrowed condition is necessary; otherwise current behavior could silently change joint topology. If you want, I can add a targeted unit test that simulates a filtered-out body1.

newton/tests/assets/revolute_articulation.usda (1)

86-106: Asset correctly exercises “missing body1” path

Defining PhysicsFixedJoint with only physics:body0 authored is a good, minimal trigger for the import edge case this PR fixes. No issues here.

Signed-off-by: Viktor Reutskyy <vreutskyy@nvidia.com>
Signed-off-by: Viktor Reutskyy <vreutskyy@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

🧹 Nitpick comments (2)
newton/utils/import_usd.py (2)

517-524: Add a defensive guard when both bodies resolve to -1; narrow the swap condition

Edge case: if both parent_id and child_id are -1 (e.g., unresolved paths or both missing), attempting to add the joint would still be invalid. Suggest short-circuiting in that case and narrowing the swap to the intended scenario (child is -1, parent is valid).

Consider this diff:

         child_tf = wp.transform(joint_desc.localPose1Position, from_gfquat(joint_desc.localPose1Orientation))
-        # If child_id is -1, swap parent and child
-        if child_id == -1:
+        # Defensive handling: both bodies unresolved -> skip
+        if parent_id == -1 and child_id == -1:
+            if verbose:
+                print(f"Skipping joint {joint_path}: both bodies unresolved (-1)")
+            return
+        # If child_id is -1 but parent is valid, swap so world is the parent
+        if child_id == -1 and parent_id != -1:
             parent_id, child_id = child_id, parent_id
             parent_tf, child_tf = child_tf, parent_tf
             if verbose:
                 print(f"Joint {joint_path} connects {parent_path} to world")

Additionally, please verify revolute/prismatic joints that connect to world via a missing body (fixed asset covers FixedJoint only). Axis conventions may depend on body0/body1 frames in USD; the swap should preserve semantics, but a quick unit test for a world-anchored Revolute/Prismatic would de-risk this.


527-528: Remove commented-out debug code

These commented prints add noise. If needed, prefer a verbose-gated print with context or delete them.

Apply:

-        # if parent_id == -1:
-        #     print("joint connected to world")
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between adb5146 and c1677cd.

📒 Files selected for processing (2)
  • newton/tests/test_import_usd.py (1 hunks)
  • newton/utils/import_usd.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • newton/tests/test_import_usd.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 (1)
newton/utils/import_usd.py (1)

517-524: Fix: Swapping when child == -1 correctly treats world as parent and prevents KeyError

This change aligns with engine conventions (world as parent) and avoids attempting to index builder/body_shapes[-1] on world-connected joints. Computing child_tf early and swapping the xforms is the right move. The verbose print is gated correctly.

@vreutskyy

Copy link
Copy Markdown
Member Author

@adenzler-nvidia it seems to stuck saying it has some conflict, but I don't see any conflicts locally. there's nothing to fix

@vreutskyy

Copy link
Copy Markdown
Member Author

@adenzler-nvidia it seems to stuck saying it has some conflict, but I don't see any conflicts locally. there's nothing to fix

solved it, I guess. should have pulled main from upstream, not from origin

Signed-off-by: Viktor Reutskyy <vreutskyy@nvidia.com>
@vreutskyy vreutskyy enabled auto-merge (squash) August 15, 2025 16:02
@adenzler-nvidia

Copy link
Copy Markdown
Member

@vreutskyy - I think this needs an update to a version where the public API refactor is in, otherwise looks good. Let's get this merged.

Signed-off-by: Viktor Reutskyy <vreutskyy@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

🧹 Nitpick comments (3)
newton/tests/test_import_usd.py (2)

131-136: Make free-joint detection more robust (avoid relying on key names).

Current logic infers the free joint by exclusion of specific path keys, which is brittle if additional joints are ever authored. Prefer detecting by joint type and asserting uniqueness.

Apply this diff:

-        # The free joint typically has a generic key like "joint_1"
-        free_joint_idx = next(
-            i
-            for i, key in enumerate(builder.joint_key)
-            if key not in ["/Articulation/CenterPivot/FixedJoint", "/Articulation/Arm/RevoluteJoint"]
-        )
+        # The free joint is identified by its type
+        self.assertEqual(builder.joint_type.count(newton.JointType.FREE), 1)
+        free_joint_idx = builder.joint_type.index(newton.JointType.FREE)

149-152: Relax shape-map count assertion to reduce brittleness.

Assets often accrete visuals/colliders over time. Hard-coding exactly 1 shape path makes this test flaky for unrelated changes. Prefer asserting a minimum.

Apply this diff:

-        self.assertEqual(len(results["path_shape_map"]), 1)
+        self.assertGreaterEqual(len(results["path_shape_map"]), 1)

Would you like me to extend the test to also assert parent/child indices for the revolute joint (CenterPivot → Arm) to lock-in that relationship?

newton/_src/utils/import_usd.py (1)

518-526: Narrow the swap to truly “omitted body1” (avoid misconnecting filtered/disabled bodies to world).

child_id == -1 can also arise when the referenced child body exists in USD but wasn’t loaded (ignored path, disabled body, etc.). In those cases, silently connecting to world could be surprising. Tighten the condition to only swap when USD explicitly omits body1 (Sdf.Path.emptyPath). Otherwise, skip the joint (or gate on a verbose warning).

Proposed change:

-        # If child_id is -1, swap parent and child
-        if child_id == -1:
+        # If child_id is -1, only swap when USD actually omitted body1.
+        # Otherwise (unresolved due to filtering/disabled), skip to avoid accidental world-connections.
+        if child_id == -1:
+            if joint_desc.body1 != Sdf.Path.emptyPath:
+                if verbose:
+                    print(f"Skipping joint {joint_path}: child '{child_path}' unresolved (possibly filtered/disabled)")
+                return
             if parent_id == -1:
                 if verbose:
                     print(f"Skipping joint {joint_path}: both bodies unresolved")
                 return
             parent_id, child_id = child_id, parent_id
             parent_tf, child_tf = child_tf, parent_tf
             if verbose:
                 print(f"Joint {joint_path} connects {parent_path} to world")

If you want, I can scan the repository for other places where we treat -1 as “world” to ensure this change doesn’t regress any intended “world-parent” connections that come from authored body0 == world.

📜 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 5ca1ce1 and d9792dd.

📒 Files selected for processing (2)
  • newton/_src/utils/import_usd.py (1 hunks)
  • newton/tests/test_import_usd.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). (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 (ubuntu-latest)
  • GitHub Check: run-newton-tests / newton-unittests (windows-latest)
🔇 Additional comments (3)
newton/tests/test_import_usd.py (1)

97-152: Solid regression test for “missing body1” semantics.

This test accurately captures the intended convention: when body1 is omitted, world is parent (-1) and the authored body0 becomes the child. The assertions on joint count, types, and the fixed joint parent/child mapping exercise the fix well.

newton/_src/utils/import_usd.py (2)

516-526: Correctly handle “missing body1” by swapping to world-parent.

Pre-computing child_tf and swapping parent/child when child_id == -1 resolves the KeyError: -1 and aligns with the stated convention (body0 becomes child, world is parent). The transform swap keeps frames consistent. Good fix.


527-529: Confirm transform application semantics (parent-only incoming_xform).

Applying incoming_xform only to parent_tf (and post-swap, that’s the world side) matches common root-joint semantics. Just double-check this doesn’t break authored offsets when the articulation root joint itself had the missing body1 case; if needed, we can extend the tests to cover a root joint with omitted body1.

@vreutskyy vreutskyy merged commit 11a7403 into newton-physics:main Aug 18, 2025
11 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Aug 18, 2025
5 tasks
mzamoramora-nvidia pushed a commit to MiguelZamoraM/newton that referenced this pull request Aug 20, 2025
…cs#527)

Signed-off-by: Viktor Reutskyy <vreutskyy@nvidia.com>
Signed-off-by: Miguel Zamora <mazamoramora@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.

Simple articulation USD import fails

3 participants