Simple articulation USD import fails #517#527
Conversation
Signed-off-by: Viktor Reutskyy <vreutskyy@nvidia.com>
📝 WalkthroughWalkthroughAdds 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
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)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested reviewers
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 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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
There was a problem hiding this comment.
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 consistentYou correctly swap IDs when
body1is missing, butparent_tf/child_tfremain tied tolocalPose0/localPose1of 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 minimalThe 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 keysThe test assumes a specific joint order ([FREE, REVOLUTE, FIXED]); that can change with topology sorting or future importer tweaks. Resolve indices from
builder.joint_keyto 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
📒 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 bodiesUsing
child_id == -1alone will also trigger for bodies filtered out (disabled joints/bodies, ignored paths, etc.). Prefer checking thatbody1is actually unauthored/empty instead of just missing frompath_body_map(see the diff above usingSdf.Path.emptyPath). This reduces false positives.Please confirm whether there are scenarios in your stages where
body1can be authored but filtered out (e.g., viaonly_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-outbody1.newton/tests/assets/revolute_articulation.usda (1)
86-106: Asset correctly exercises “missing body1” pathDefining
PhysicsFixedJointwith onlyphysics:body0authored 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>
There was a problem hiding this comment.
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 conditionEdge case: if both
parent_idandchild_idare-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 codeThese 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
📒 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 KeyErrorThis 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.
Signed-off-by: Viktor Reutskyy <vreutskyy@nvidia.com>
…thub.com/vreutskyy/newton into 517-simple-articulation-usd-import-fails
Signed-off-by: Viktor Reutskyy <vreutskyy@nvidia.com>
|
@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 - I think this needs an update to a version where the public API refactor is in, otherwise looks good. Let's get this merged. |
There was a problem hiding this comment.
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.
📒 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.
…cs#527) Signed-off-by: Viktor Reutskyy <vreutskyy@nvidia.com> Signed-off-by: Miguel Zamora <mazamoramora@nvidia.com>
…cs#527) Signed-off-by: Viktor Reutskyy <vreutskyy@nvidia.com>
…cs#527) Signed-off-by: Viktor Reutskyy <vreutskyy@nvidia.com>
Description
This PR fixes a bug in USD import where joints with missing
body1references would cause aKeyError: -1.Problem
When importing USD files, if a joint only specifies
body0but notbody1, the import would fail with a KeyError when trying to accessbody_shapes[-1].Solution
The fix properly handles this case by following the standard physics engine convention:
body0is parent,body1is childbody1is missing:body0becomes child, world (-1) becomes parentThis is implemented with a simple check that swaps parent and child IDs when
child_id == -1.Testing
Added
test_import_revolute_articulationwhich tests importing a USD file (revolute_articulation.usda) containing a FixedJoint with onlybody0specified. 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.
docs/migration.rstis up-to dateBefore your PR is "Ready for review"
newton/tests/test_examples.py)pre-commit run -aSummary by CodeRabbit
New Features
Bug Fixes
Tests