Skip to content

Conversation

@eric-heiden
Copy link
Member

@eric-heiden eric-heiden commented Jan 17, 2026

In some cases the physics:body0 relates to the joint child and physics:body1 related to the joint parent.

Allowing flipped joints (body0 being the topological child and body1 being the parent) would need runtime handling in several places: Joint targets would have to be transformed to keep the semantics of body0 → body1 and and body parent forces would also have to be inverted and reported at the parent link COM (alternatively, this could happen in the selection API, but this would introduce even more confusion).

Fixes #700.
Fixes #1377.

Summary by CodeRabbit

  • New Features

    • Added mesh utilities (two mesh types and three helpers), undirected topology ordering that reports reversed joints, and USD import now returns joint-index mappings.
  • Documentation

    • Clarified joint/anchor-frame wording across builder/import docs; expanded API docs; updated solver feature table and MAXVAL.
  • Tests

    • Added tests covering detection and handling of unsupported reversed joints.

✏️ Tip: You can customize this high-level summary in your review settings.

@eric-heiden eric-heiden requested a review from Copilot January 17, 2026 02:38
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 17, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Undirected topological sorting was added to the USD import pipeline; parse_usd now records per-joint reversal and propagates joint path maps through replication. Topology utilities were generalized and exported, tests for reversed joints were added, and docstrings/API docs were updated.

Changes

Cohort / File(s) Summary
USD Import Pipeline
newton/_src/utils/import_usd.py
Switch to topological_sort_undirected; add path_joint_map to parse_usd return dict; propagate joint-index updates during world cloning; record reversed_joints/reversed_joint_ids and validate/adjust axes for reversed joints; docstring/typing updates.
Topology Utilities
newton/_src/utils/topology.py, newton/_src/utils/__init__.py
Generalize topological_sort to generic NodeT; add _joint_key; add topological_sort_undirected returning (joint_order, reversed_joints); export topological_sort_undirected from package init.
Tests
newton/tests/test_import_usd.py
Add tests for reversed-joint handling: expect ValueError for unsupported reversed joints; allow reversed fixed root-to-world case.
Docstrings & Builder Clarifications
newton/_src/sim/articulation.py, newton/_src/sim/builder.py
Docstring updates to consistently reference joint parent anchor frames and clarify axis/frame semantics; no API signature or behavior changes.
API Docs & Utils Exports
docs/api/newton_utils.rst, newton/_src/utils/__init__.py
Document new public items in newton.utils (MeshAdjacency, MeshEdge, bourke_color_map, copy_rgb_frame_uint8, solidify_mesh); add topological_sort_undirected to exports.
Small Docs/Table Tweaks
docs/api/newton.rst, docs/api/newton_solvers.rst
Minor documentation formatting/value updates (MAXVAL numeric formatting; SolverVBD implicit feature flag).

Sequence Diagram(s)

sequenceDiagram
    participant USD as "USD File"
    participant Parser as "parse_usd()"
    participant Topo as "topological_sort_undirected()"
    participant JointProc as "Joint Processor"
    participant Builder as "ModelBuilder"

    USD->>Parser: load bodies, shapes, joints
    Parser->>Topo: provide joint edges (undirected)
    Topo->>Topo: traverse graph -> joint_order + reversed_joints
    Topo-->>Parser: return joint_order, reversed_joints
    Parser->>JointProc: process each joint in joint_order with reverse flag
    JointProc->>JointProc: if reversed -> validate type & adjust_axis (negate)
    JointProc-->>Parser: processed joint descriptors
    Parser->>Builder: add bodies/shapes/joints with corrected axes/order
    Parser-->>Parser: record path_body_map, path_shape_map, path_joint_map, reversed_joint_ids
    Parser->>Caller: return parsed data and maps
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • vreutskyy
  • AntoineRichard
  • camevor
🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 59.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive Most changes are in-scope for handling reversed joints. However, docs/api/newton.rst (MAXVAL formatting change) and docs/api/newton_solvers.rst (SolverVBD feature table) appear unrelated to reversed joint handling and should be verified as intentional. Clarify whether documentation changes to MAXVAL formatting and SolverVBD features are intentional or should be separated into a different PR.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Handle reversed joints in USD' accurately describes the main objective: supporting USD scenes where physics:body0 and physics:body1 roles are reversed.
Linked Issues check ✅ Passed The PR addresses both linked issues: #700 (removing artificial body0/body1 ordering assumptions) and #1377 (restoring single-joint articulation imports) through topology.py changes adding topological_sort_undirected, import_usd.py updates tracking reversed joints, and comprehensive test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

❤️ Share

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

Signed-off-by: Eric Heiden <[email protected]>
Signed-off-by: Eric Heiden <[email protected]>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request implements support for handling reversed joints in USD import, where physics:body0 relates to the joint child and physics:body1 relates to the joint parent. The PR addresses this by detecting reversed joints during topological sorting and appropriately swapping parent/child transforms and negating joint axes for supported joint types (revolute, prismatic, and fixed). For unsupported joint types (D6, spherical), the code raises an error.

Changes:

  • Added a new topological_sort_undirected function that treats the joint graph as undirected and detects which joints need to be reversed during traversal
  • Modified USD import logic to handle reversed joints by swapping transforms and negating axes for supported joint types
  • Updated documentation to clarify joint axis and transform frame conventions
  • Added tests to verify correct handling of reversed joints and appropriate error raising for unsupported types

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
newton/_src/utils/topology.py Added new topological_sort_undirected function with type annotations for handling undirected joint graphs and tracking reversed edges
newton/_src/utils/import_usd.py Modified parse_joint to handle reversed joints, added axis adjustment logic, updated documentation for return values including reversed_joint_ids
newton/_src/sim/builder.py Updated documentation to clarify joint transforms and axes are defined in joint parent anchor frame
newton/_src/sim/articulation.py Updated docstring for reconstruct_angular_q_qd to clarify frame conventions
newton/_src/utils/__init__.py Exported new topological_sort_undirected function
newton/tests/test_import_usd.py Added tests for reversed joint handling, updated existing tests to account for new joint ordering behavior, modified mass calculation test to sort before comparing
docs/api/newton_utils.rst Added documentation entries for new utilities

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@codecov
Copy link

codecov bot commented Jan 17, 2026

Codecov Report

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

Files with missing lines Patch % Lines
newton/_src/utils/topology.py 68.60% 27 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@newton/_src/sim/builder.py`:
- Around line 307-308: Update the documentation to use consistent frame wording:
change the field comment for ModelBuilder.joint_axis to match the new inline
docstring that says the axis is expressed in the joint parent anchor frame (or
alternatively update the inline docstring if you intend the axis to be in the
child joint frame). Locate the stored field comment for ModelBuilder.joint_axis
and the assignment using self.axis = wp.normalize(axis_to_vec3(axis)) and ensure
both describe the same frame (explicitly mention "joint parent anchor frame" if
keeping the new docstring), so the docstring and field comment are reconciled.
♻️ Duplicate comments (1)
newton/_src/utils/import_usd.py (1)

1271-1338: reversed_joint_ids should store global joint indices (not local joint_edges indices).
Right now reversed_joint_ids is extended with indices into joint_edges. The return contract and downstream checks expect global builder joint indices. This can silently mislabel joints, especially once a free joint is inserted or when articulations are stacked. Populate reversed_joint_ids at the point where parse_joint returns the actual builder joint index (and consider offsetting during cloned_world replication as well).

🐛 Proposed fix (record global joint indices)
-                        reversed_joint_ids.extend(reversed_joint_list)
                         reversed_joints_set = set(reversed_joint_list)
@@
                     if joint is not None:
                         articulation_joint_indices.append(joint)
+                        if i in reversed_joints_set:
+                            reversed_joint_ids.append(joint)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@docs/api/newton_utils.rst`:
- Around line 12-15: The autosummary in docs/api/newton_utils.rst lists internal
symbols that are not exported from the public newton.utils API (MeshAdjacency,
MeshEdge, bourke_color_map, copy_rgb_frame_uint8, solidify_mesh); either remove
those names from the autosummary or add public re-exports in the newton.utils
package (e.g., import and expose these symbols from their internal modules in
newton.utils.__init__ or the package's public module) so that importing
newton.utils.<symbol> succeeds; update the docs or the public re-export code
accordingly to ensure the documented symbols match the actual public API.

In `@newton/_src/utils/__init__.py`:
- Around line 23-24: Add topological_sort_undirected to the public API surface:
in newton/utils.py, update the import from newton._src.utils to include
topological_sort_undirected alongside topological_sort, and add
"topological_sort_undirected" to the module's __all__ list so it is re-exported
for external users; ensure the name matches the function in
newton._src.utils.topology.

In `@newton/_src/utils/import_usd.py`:
- Line 153: Remove the unused noqa suppression on the import of
topological_sort_undirected in import_usd.py: replace "from .topology import
topological_sort_undirected  # noqa: PLC0415 (circular import)" with a normal
import and, if desired, a plain comment like "# circular import" but without the
"noqa: PLC0415" tag so Ruff no longer reports an unused suppression; ensure the
import remains exactly "from .topology import topological_sort_undirected" (or
equivalent) to preserve behavior.
♻️ Duplicate comments (1)
newton/_src/utils/import_usd.py (1)

1243-1281: Populate reversed_joint_ids with actual builder joint indices.

reversed_joint_ids.extend(reversed_joint_list) records local indices into joint_edges, which won’t match ModelBuilder joint indices and can include skipped joints. Append the created joint index when the joint was reversed instead.

🛠️ Proposed fix
-                        reversed_joint_ids.extend(reversed_joint_list)
                         reversed_joints_set = set(reversed_joint_list)
@@
-                    if joint is not None:
-                        articulation_joint_indices.append(joint)
+                    if joint is not None:
+                        articulation_joint_indices.append(joint)
+                        if i in reversed_joints_set:
+                            reversed_joint_ids.append(joint)

Also applies to: 1322-1338, 1755-1774

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

269-321: Well-structured test for reversed joint axis negation.

The test correctly verifies that when a USD joint has its body0/body1 reversed relative to the articulation tree, the joint axis is negated appropriately.

Minor: The noqa: PLC0415 directive at line 272 appears to be unused per static analysis. Consider removing it if the linter doesn't require it.


1848-1891: Verify the parent/child validation logic for edge cases.

The logic handles normal and reversed joints, but I want to highlight the control flow for clarity:

  1. Normal match (lines 1872-1873): Passes silently when actual matches expected.
  2. Swapped match (lines 1874-1881): Allows swapping only for FIXED, REVOLUTE, and PRISMATIC joints; fails for other types.
  3. Mismatch with reversed flag (lines 1882-1886): Fails if the joint was expected to be reversed but indices don't match.
  4. General mismatch (lines 1887-1891): Fails for any other case.

One consideration: On line 1882, when j in reversed_joint_ids, the assertion failure message references "expected reversed parent/child indices" but the actual comparison being made is against (expected_child, expected_parent). This seems inverted from what the message suggests—the check is that actual values should equal (expected_child, expected_parent), but this condition is only reached when they don't match the swapped expectation (since line 1874 already passed through).

This logic appears correct but is complex. Consider adding a brief inline comment explaining the control flow.

@eric-heiden eric-heiden marked this pull request as ready for review January 19, 2026 19:27
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3b608cd9a1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@newton/_src/utils/import_usd.py`:
- Around line 1338-1340: reversed_joint_ids is computed against original joint
indices and goes stale after index remaps from collapse_fixed_joints() and
cloned-world replication; change the logic to record reversed_joint_paths (store
each joint's unique scene/path when you append to articulation_joint_indices and
reversed_joint_ids), then after any index-remapping step (e.g., inside
collapse_fixed_joints and the clone/replication routine) rebuild
reversed_joint_ids by resolving those stored paths to the new indices and
applying any clone offset to the resolved index; update code references where
reversed_joint_ids is created (the block appending to articulation_joint_indices
and reversed_joint_ids) and where remaps occur so the rebuild runs immediately
after remapping.
♻️ Duplicate comments (1)
newton/_src/utils/import_usd.py (1)

153-153: Remove unused noqa suppression.

Ruff already flags this as an unused suppression.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
newton/_src/utils/import_usd.py (1)

1699-1754: reversed_joint_ids not propagated to cloned worlds.

When cloned_world is used, path_joint_map is correctly updated with offset indices for replicated worlds (lines 1728-1730), but reversed_joint_ids remains unchanged. This means the list only contains joint indices from the first world, making it incorrect for replicated worlds.

Consider offsetting reversed_joint_ids in the clone loop:

🔧 Suggested fix
             for path, joint_id in path_joint_map.items():
                 new_path = path.replace(cloned_world, world_path)
                 path_joint_map_updates[new_path] = joint_id + joint_count

+            # Add these lines after the path_joint_map loop:
+            reversed_joint_ids_updates = []
+            for jid in reversed_joint_ids:
+                reversed_joint_ids_updates.append(jid + joint_count)

And after the loop:

             path_joint_map = path_joint_map_updates
+            reversed_joint_ids.extend(reversed_joint_ids_updates)
♻️ Duplicate comments (1)
newton/_src/utils/import_usd.py (1)

153-153: Remove unused noqa directive.

The # noqa: PLC0415 suppression is unused as indicated by static analysis. Keep a plain comment if documenting the circular import is desired.

🧹 Suggested fix
-    from .topology import topological_sort_undirected  # noqa: PLC0415
+    from .topology import topological_sort_undirected  # circular import
🧹 Nitpick comments (1)
newton/_src/utils/import_usd.py (1)

828-843: Minor inconsistency: adjust_axis applied only to angular axes in D6 joints.

The adjust_axis helper is applied to angular axes but not to linear axes (line 779). This is currently harmless since D6 joints are excluded from reversed joint support (raising an error at line 559). However, if D6 support is added in the future, both linear and angular axes would need consistent handling.

@eric-heiden eric-heiden self-assigned this Jan 20, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@newton/_src/utils/import_usd.py`:
- Around line 1243-1251: The code currently raises a ValueError when any
reversed joint is detected in topological_sort_undirected (reversed_joint_list),
which blocks supported types; instead, collect the reversed joints into a set
(e.g., reversed_joint_set populated from reversed_joint_list using joint_names
or joint ids) and remove the ValueError block so import continues; then pass
that reversed_joint_set into parse_joint(...) so parse_joint can swap
parent/child ids and transforms, negate axes for revolute/prismatic as needed,
and only raise for truly unsupported types (D6/spherical/distance). Ensure
references to topological_sort_undirected,
reversed_joint_list/reversed_joint_paths/reversed_joint_names, and parse_joint
are updated accordingly.

In `@newton/tests/test_import_usd.py`:
- Around line 269-320: The test test_reversed_joints_in_articulation_raise is
asserting a ValueError for reversed revolute joints but the PR intends to
support reversed body0/body1 for revolute/prismatic/fixed joints; update the
test to call ModelBuilder.add_usd() expecting success (no exception), then
inspect the resulting model to verify the joint at "/World/Articulation/Joint1"
was imported with body0/body1 swapped as needed (e.g., child/parent relationship
adjusted) and that the joint axis/sign was corrected when required; reference
the ModelBuilder.add_usd method and the joint named "/World/Articulation/Joint1"
when implementing the assertions.
♻️ Duplicate comments (1)
newton/_src/utils/import_usd.py (1)

148-148: Remove unused noqa on local import.
Ruff flags this as an unused suppression.

🧹 Proposed tidy-up
-from .topology import topological_sort_undirected  # noqa: PLC0415
+from .topology import topological_sort_undirected

@eric-heiden eric-heiden requested a review from camevor January 20, 2026 19:12
camevor
camevor previously approved these changes Jan 20, 2026
Copy link
Member

@camevor camevor left a comment

Choose a reason for hiding this comment

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

Thanks, looks good to me! I added one comment about an edge case.

if reversed_joint_list:
reversed_joint_paths = [joint_names[joint_id] for joint_id in reversed_joint_list]
reversed_joint_names = ", ".join(reversed_joint_paths)
raise ValueError(
Copy link
Member

Choose a reason for hiding this comment

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

One special case arises for fixed joints to world that are reversed, which are swapped silently in resolve_joint_parent_child at line 515, which would prevent the raise here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for flagging this! I've added another test (test_reversed_fixed_root_joint_to_world_is_allowed) to double check this and we do still support these silently reversed root joints. This is because the joint_edges list we use for determining the reversed joints via topological_sort_undirected has already the correct [parent, child] ordering (thanks to calling resolve_joint_parent_child to fill in the joint edges) for these root joints, so there is no further reversal necessary.

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.

Single joint articulation import issues from USD USD importer imposes artificial ordering on joints

2 participants