Skip to content

Fix quaternion inversion in USD importer#559

Merged
adenzler-nvidia merged 3 commits into
newton-physics:mainfrom
adenzler-nvidia:dev/adenzler/fix-quat-inversion
Aug 15, 2025
Merged

Fix quaternion inversion in USD importer#559
adenzler-nvidia merged 3 commits into
newton-physics:mainfrom
adenzler-nvidia:dev/adenzler/fix-quat-inversion

Conversation

@adenzler-nvidia

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

Copy link
Copy Markdown
Member

Partially replacing #552

I think this is safe to submit given that we only use that function in one place. We do however need to figure out why this was there in the first place.

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

  • Refactor

    • Standardized quaternion parsing for USD imports to produce normalized quaternions, improving determinism and consistency in orientation handling.
  • Tests

    • Added coverage for non-symmetric inertia specified in principal axes misaligned with the body frame, verifying correct transformation into the body-frame inertia tensor.

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

coderabbitai Bot commented Aug 15, 2025

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Replaced parse_quat's previous invert_rotations-based branch with a new from_gfquat(gfquat) helper that builds and normalizes a quaternion from gfquat.real/imaginary. Added a unit test test_non_symmetric_inertia validating principal-axis inertia rotated into the body frame from USD.

Changes

Cohort / File(s) Summary
USD import quaternion handling
newton/_src/utils/import_usd.py
Added from_gfquat(gfquat) and changed parse_quat to call it (constructs quaternion from gfquat.imaginary + gfquat.real and normalizes). Removed the previous sign-based/invert_rotations branch in parse_quat. parse_xform still uses invert_rotations. No public signatures changed.
Tests: non-symmetric inertia import
newton/tests/test_import_usd.py
Added test_non_symmetric_inertia (skipped unless USD available). Creates an in-memory USD stage with MassAPI principal inertias and principal-axes quaternion, parses via parse_usd, and asserts the parsed body-frame inertia equals R·diag([2,4,6])·Rᵀ within tolerance.

Sequence Diagram(s)

sequenceDiagram
  participant USD as USD Stage
  participant Parser as import_usd.parse_usd
  participant PQ as parse_quat / from_gfquat
  participant Builder as ModelBuilder

  USD->>Parser: Read prims (MassAPI, RigidBodyAPI, CollisionAPI)
  Parser->>PQ: Request principal-axes quaternion (Gf.Quatf)
  PQ-->>Parser: wp.quat(...) normalized (from_gfquat)
  Parser->>Builder: Build body, mass, compute inertia tensor (use quaternion -> R -> I_body)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~15 minutes

Possibly related PRs

Suggested reviewers

  • eric-heiden
  • shi-eric

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

175-185: Normalize parsed USD quaternions to ensure a proper rotation; avoid duplication with from_gfquat

The component order is correct (x, y, z, w), but the quaternion is not normalized before use. Downstream we call wp.quat_to_matrix(i_rot) for inertia, which assumes unit quaternions. A non-unit quaternion would skew the rotation. Also, you already have from_gfquat() that performs normalization; reusing it here improves consistency.

Apply this minimal fix to normalize and keep the existing guard:

-        quat = wp.quat(*val.imaginary, val.real)
-        l = wp.length(quat)
-        if np.isfinite(l) and l > 0.0:
-            return quat
+        q = wp.quat(*val.imaginary, val.real)
+        l = wp.length(q)
+        if np.isfinite(l) and l > 0.0:
+            return wp.normalize(q)
         return default

Alternatively, reuse the helper for consistency:

-        quat = wp.quat(*val.imaginary, val.real)
-        l = wp.length(quat)
-        if np.isfinite(l) and l > 0.0:
-            return quat
+        # reuse helper which normalizes
+        quat = from_gfquat(val)
+        l = wp.length(quat)
+        if np.isfinite(l) and l > 0.0:
+            return quat
         return default
🧹 Nitpick comments (1)
newton/tests/test_import_usd.py (1)

349-351: Normalize the test quaternion to avoid subtle drift and align with importer behavior

Normalizing ensures R is a true rotation matrix and makes the test robust if the importer normalizes quaternions (recommended).

-        q = wp.quat(0.1830127, 0.1830127, 0.6830127, 0.6830127)
+        q = wp.normalize(wp.quat(0.1830127, 0.1830127, 0.6830127, 0.6830127))
         R = np.array(wp.quat_to_matrix(q)).reshape(3, 3)
📜 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 bb73e84 and b9b73da.

📒 Files selected for processing (2)
  • newton/_src/utils/import_usd.py (1 hunks)
  • newton/tests/test_import_usd.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
newton/tests/test_import_usd.py (1)
newton/_src/utils/import_usd.py (1)
  • parse_usd (34-1266)
⏰ 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 (1)
newton/_src/utils/import_usd.py (1)

201-208: Confirm intent: invert_rotations only affects matrix-based xforms, not quaternions

Short answer: parse_quat does not use invert_rotations and, in this repo, is only used to read physics:principalAxes for inertia, so the asymmetry appears intentional.

Locations to note:

  • parse_quat definition: newton/_src/utils/import_usd.py:175 (returns normalized wp.quat from a GfQuat)
  • parse_xform (matrix path that honors invert_rotations): newton/_src/utils/import_usd.py (around lines 201–208)
  • sole usage of parse_quat: newton/_src/utils/import_usd.py:1154 — parsing "physics:principalAxes" for inertia

No other occurrences of parse_quat or physics:principalAxes were found in the repo during verification.

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

201-209: Action required: clarify invert_rotations policy — it's applied to matrix-derived Xform rotations only (fix required)

Short: parse_xform (matrix path) transposes the 3x3 when invert_rotations=True, while Gf.Quat → wp.quat (from_gfquat) and all quaternion-based paths are not inverted. The current docstring ("inverts any rotations") is therefore inaccurate and should be corrected or the behavior reconciled.

Files/locations to fix or review:

  • newton/_src/utils/import_usd.py
    • parse_usd signature / docstring (around lines ~41 and ~65–71): currently says "inverts any rotations defined in the shape transforms." — update to specify matrix-only behavior.
    • parse_xform (lines ~201–209): applies transpose when invert_rotations is True (matrix-derived rotation).
    • from_gfquat (definition around lines ~172–174) and its call sites (around lines 180, 479, 517, 986): quaternion-derived rotations are parsed without inversion.
  • tests: newton/tests/test_import_usd.py (calls parse_usd with invert_rotations=True at ~line 203) — ensure tests and expectations match the clarified policy.

Suggested docstring replacement (concise):
Replace
"invert_rotations (bool): If True, inverts any rotations defined in the shape transforms."
with
"invert_rotations (bool): If True, inverts rotations derived from UsdGeom.Xform local transformation matrices only. Quaternion attributes (e.g., principalAxes, localRot, joint localPose orientations and other Gf.Quat sources) are parsed directly from Gf.Quat and are not affected by this flag."

Options for maintainers:

  • If intended: update the docstring as above and add an inline comment near parse_xform to avoid future confusion.
  • If not intended (you want quaternion paths inverted too): implement optional inversion in from_gfquat or its callers and update tests accordingly.
🧹 Nitpick comments (2)
newton/_src/utils/import_usd.py (2)

172-174: Correct XYZW mapping from USD Gf.Quat to Warp; consider guarding zero-length/NaN inputs

The mapping (x, y, z, w) and normalization are correct for Warp. To harden this helper against malformed USD data (zero-length or non-finite quats) and to prevent NaNs from propagating to callsites that use from_gfquat directly (parse_body/parse_joint), add a light guard before normalization.

-def from_gfquat(gfquat):
-    return wp.normalize(wp.quat(*gfquat.imaginary, gfquat.real))
+def from_gfquat(gfquat):
+    # USD stores quats as (real=w, imaginary=(x,y,z)); Warp expects (x,y,z,w)
+    x, y, z = gfquat.imaginary
+    w = gfquat.real
+    v = np.array([x, y, z, w], dtype=np.float32)
+    # Guard against non-finite or zero-length quaternions to avoid NaNs
+    if not np.isfinite(v).all() or np.linalg.norm(v) == 0.0:
+        return wp.quat_identity()
+    return wp.normalize(wp.quat(x, y, z, w))

175-185: parse_quat now uses from_gfquat; minor simplification opportunity

Since from_gfquat already normalizes (and with the guard above, returns identity on invalid inputs), the extra length check can be dropped for a leaner path. If you keep the check, it’s fine as-is.

-        quat = from_gfquat(val)
-        l = wp.length(quat)
-        if np.isfinite(l) and l > 0.0:
-            return quat
-        return default
+        quat = from_gfquat(val)
+        return 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 b9b73da and da1afab.

📒 Files selected for processing (1)
  • newton/_src/utils/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 (windows-latest)
  • GitHub Check: run-newton-tests / newton-unittests (ubuntu-latest)

@adenzler-nvidia

Copy link
Copy Markdown
Member Author

@shi-eric tagging you for review because you seemed to be involved in the change that flipped the default 2 weeks ago.

@adenzler-nvidia adenzler-nvidia merged commit 7f6229e into newton-physics:main Aug 15, 2025
12 checks passed
mzamoramora-nvidia pushed a commit to MiguelZamoraM/newton that referenced this pull request Aug 20, 2025
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Miguel Zamora <mazamoramora@nvidia.com>
mzamoramora-nvidia pushed a commit to MiguelZamoraM/newton that referenced this pull request Aug 20, 2025
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Miguel Zamora <mazamoramora@nvidia.com>
@coderabbitai coderabbitai Bot mentioned this pull request Aug 22, 2025
5 tasks
eric-heiden pushed a commit to eric-heiden/newton that referenced this pull request Jan 28, 2026
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
mmacklin pushed a commit to mmacklin/newton that referenced this pull request Apr 7, 2026
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