Fix quaternion inversion in USD importer#559
Conversation
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
📝 WalkthroughWalkthroughReplaced 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
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)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~15 minutes 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/_src/utils/import_usd.py (1)
175-185: Normalize parsed USD quaternions to ensure a proper rotation; avoid duplication with from_gfquatThe 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 defaultAlternatively, 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 behaviorNormalizing 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.
📒 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 quaternionsShort 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>
There was a problem hiding this comment.
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 inputsThe 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 opportunitySince 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.
📒 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)
|
@shi-eric tagging you for review because you seemed to be involved in the change that flipped the default 2 weeks ago. |
Signed-off-by: Alain Denzler <adenzler@nvidia.com> Signed-off-by: Miguel Zamora <mazamoramora@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com> Signed-off-by: Miguel Zamora <mazamoramora@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
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"
newton/tests/test_examples.py)pre-commit run -aSummary by CodeRabbit
Refactor
Tests