Fix MJCF importer to have correct body_q based on pos/quat xml attributes#565
Conversation
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
📝 WalkthroughWalkthroughParses MJCF bodies with hierarchical transforms: computes local_xform from body pos/orientation (and scale), composes world_xform with parent_world_xform, threads world_xform through recursive parse_body calls, and uses world-space transforms for body and joint placement. Tests added covering transform composition and joint/world pose consistency. Changes
Sequence Diagram(s)sequenceDiagram
participant parse_mjcf
participant parse_body
participant builder
parse_mjcf->>parse_body: parse_body(root_body, parent_world_xform=None)
activate parse_body
Note right of parse_body: local_xform = T(body_pos*scale, body_ori)
alt parent_world_xform provided
parse_body->>parse_body: world_xform = parent_world_xform * local_xform
else
parse_body->>parse_body: world_xform = local_xform
end
parse_body->>builder: add_body(xform=world_xform)
parse_body->>builder: add_joint(using body_pos_for_joints/body_ori_for_joints)
loop for each child
parse_body->>parse_body: parse_body(child, parent_world_xform=world_xform)
end
deactivate parse_body
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 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
|
|
converting to draft, will add tests. |
There was a problem hiding this comment.
Actionable comments posted: 1
🔭 Outside diff range comments (3)
newton/_src/utils/import_mjcf.py (3)
198-203: *Bug: xform is already a Transform; re-wrapping with wp.transform(xform) will failEarlier (Lines 89-93), xform is normalized to a wp.transform. Here you re-wrap it using wp.transform(*xform), but a Transform is not an iterable tuple. This will likely raise at runtime. Compose with axis_xform directly.
Apply this diff:
- if xform is None: - xform = axis_xform - else: - xform = wp.transform(*xform) * axis_xform + xform = xform * axis_xformOptional cleanup: You can also remove the earlier normalization block (Lines 89-93) and handle normalization once here, but the above minimal fix is sufficient.
290-312: Double application of incoming_xform for world geoms (link == -1)incoming_xform is applied to tf twice when link == -1: once at Lines 291-295 and again at 310-312. This doubles the transform for worldbody geoms. Plane geoms depend on geom_pos/geom_rot (used to compute plane equation), so we should apply incoming_xform once to tf and refresh geom_pos/geom_rot only once for planes.
Apply this diff:
- tf = wp.transform(geom_pos, geom_rot) - if link == -1 and incoming_xform is not None: - tf = incoming_xform * tf - geom_pos = tf.p - geom_rot = tf.q + tf = wp.transform(geom_pos, geom_rot) @@ - if incoming_xform is not None: - tf = incoming_xform * tf + if incoming_xform is not None: + tf = incoming_xform * tf + if link == -1: + # keep geom_pos/geom_rot in sync for types (e.g., plane) that use them directly + geom_pos = tf.p + geom_rot = tf.q
577-584: Bug: base_joint dict case uses child=root (XML root), not linkWhen base_joint is a dict, you set base_joint["child"] = root, which is the XML root element, not the body index. This will break joint creation; it must be the current link.
Apply this diff:
- base_joint["parent"] = -1 - base_joint["child"] = root + base_joint["parent"] = -1 + base_joint["child"] = link
🧹 Nitpick comments (2)
newton/_src/utils/import_mjcf.py (2)
434-440: Use consistent type annotations: prefer Transform over wp.transformThe rest of the file (e.g., parse_mjcf’s xform param) uses the alias Transform. Using wp.transform in annotations is inconsistent and not a valid typing construct; prefer Transform for readability and type checking.
Apply this diff:
- parent_world_xform: wp.transform | None = None, + parent_world_xform: Transform | None = None,
254-432: General: world_xform should not be passed to per-body geoms (correct), but do validate worldbody geoms with testsYour per-body shapes are correctly authored in the body-local frame (leveraging the body’s world transform from add_body). Worldbody geoms (link == -1) get incoming_xform applied, which is correct. Please add tests that validate:
- Nested bodies with pos/quat compose to the expected world_xform.
- Free joints’ joint_q reflect the composed world_xform.
- Worldbody plane, box, and mesh geoms are transformed by xform exactly once.
I can open a test PR or generate pytest cases for a small 2-body chain (root + child), including a free-root variant. Do you want me to draft them?
📜 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_mjcf.py(9 hunks)
🔇 Additional comments (7)
newton/_src/utils/import_mjcf.py (7)
459-472: World transform propagation is correct (root and children).The local_xform and world_xform composition matches the prior multiplication order used elsewhere (tf = incoming_xform * tf), so parent->child composition is consistent. This fixes the long-standing issue with pos/quat being treated as world transforms.
591-602: Initializing free-base dofs from world_xform looks goodSetting joint_q to the composed world transform addresses the original bug where free joints didn’t reflect world-space initialization.
613-614: Free joint parent_xform orientation: identity vs body orientationYou set parent_xform=Transform(0, body_ori). For a free joint (6-DOF), parent_xform often being identity is simpler to reason about (axes unrotated). Using body orientation moves that rotation into the parent frame. If intentional, all good; otherwise consider identity here.
If you want to switch to identity:
- parent_xform=wp.transform(wp.vec3(0.0, 0.0, 0.0), body_ori_for_joints), + parent_xform=wp.transform(wp.vec3(0.0, 0.0, 0.0), wp.quat_identity()),
634-636: Parent/child xforms for non-free joints remain consistentUsing parent_xform as body_pos_for_joints + joint_pos and child_xform at joint_pos with identity rotation keeps joint frames aligned with MJCF semantics. This is consistent with previous behavior and the new world-space logic.
792-793: Passing world_xform down the hierarchy is correctThreading parent_world_xform to children ensures proper composition of world-space transforms across the body tree.
912-914: Top-level call updated to seed the hierarchySeeding with parent_world_xform=None at world-body level is appropriate. Root bodies will correctly use the top-level xform when composing.
598-602: No action needed: quaternion ordering is consistent (xyzw)
All code paths—from MJCF/URDF imports through ModelBuilder to FK/IK kernels and solvers—use wp.quat(x, y, z, w). The builder writes world_xform.q[0]..q[3] into joint_q[start+3..6], and every consumer constructs quaternions via wp.quat(qx, qy, qz, qw). No index swap is required.
There was a problem hiding this comment.
Actionable comments posted: 0
🔭 Outside diff range comments (3)
newton/_src/utils/import_mjcf.py (3)
198-203: Bug: incorrect re-derivation of xform with axis_xform; will TypeError when xform is already a TransformAt this point, xform has already been normalized to a wp.transform at Lines 89-93. Re-applying wp.transform(*xform) assumes xform is iterable (tuple of (p, q)), which will raise when xform is a Transform. Also, the None-guard is now unreachable since xform cannot be None anymore, making the branch misleading.
Replace with a simple post-multiplication by axis_xform.
Apply this diff:
- axis_xform = wp.transform(wp.vec3(0.0), quat_between_axes(up_axis, builder.up_axis)) - if xform is None: - xform = axis_xform - else: - xform = wp.transform(*xform) * axis_xform + axis_xform = wp.transform(wp.vec3(0.0), quat_between_axes(up_axis, builder.up_axis)) + xform = xform * axis_xform
578-584: Bug: dict-based base_joint uses child=root instead of child=linkWhen base_joint is a dict, you set base_joint["child"] = root, but root here is the parsed XML root element, not the body index. This will cause incorrect behavior or errors. It should be the newly created body index (link).
Apply this diff:
- base_joint["parent"] = -1 - base_joint["child"] = root + base_joint["parent"] = -1 + base_joint["child"] = link base_joint["parent_xform"] = base_parent_xform base_joint["child_xform"] = base_child_xform base_joint["key"] = "base_joint" builder.add_joint(**base_joint)
291-313: Double application of incoming_xform for world geoms (link == -1)tf is multiplied by incoming_xform at Line 291 for world geoms, then again at Line 310 unconditionally when incoming_xform is set. This doubles the transform for world-level shapes and leads to incorrect placement, especially noticeable for planes where geom_pos/rot are also altered.
To avoid double transforms while retaining the plane normal computation, condition the second multiplication on link != -1:
- if incoming_xform is not None: - tf = incoming_xform * tf + if incoming_xform is not None and link != -1: + tf = incoming_xform * tfAlternatively, remove the early multiplication and compute plane normal and p using tf after a single application at this site.
🧹 Nitpick comments (1)
newton/_src/utils/import_mjcf.py (1)
549-606: Edge case: root body with and floating/base_joint flagsThe control flow here is nuanced. For root with :
- base_joint string: OK (D6 setup with position-only parent frame; rotation applied in child frame)
- floating True: OK (free joint added and joint_q set from world_xform)
- else: fixed base with _xform
Looks good overall; just consider explicitly documenting precedence when both base_joint and floating are provided (base_joint currently takes precedence).
📜 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_mjcf.py(9 hunks)
🔇 Additional comments (10)
newton/_src/utils/import_mjcf.py (10)
434-440: Signature update looks good; threading parent_world_xform is the right approachAdding parent_world_xform to parse_body enables proper hierarchical world pose propagation without changing the public API. No concerns here.
459-476: World transform composition is correct (root = xform * local, child = parent_world * local)
- Scaling applied to positions only is appropriate.
- Fallback branch is fine as a defensive default.
534-537: Using world_xform for builder.add_body is the correct fixBodies now enter the builder with their world pose, aligning joint initialization later. Good change.
549-557: Initialize base joint transform from scaled body/joint pose (OK), but verify dict-based base_joint child assignment separatelyThe shift to scaled body_pos_for_joints (+ joint_pos) for _xform is correct. See separate comment below about dict-based base_joint child assignment.
591-602: Free base initialization from world_xform is correct; confirm quaternion storage orderSetting joint_q from world_xform ensures the floating base starts at the proper world pose. Please confirm builder.joint_q expects quaternion as (x, y, z, w), which matches wp.quat’s layout.
If helpful, I can add a small unit test that loads a trivial MJCF with a rotated/scaled root and asserts the builder.joint_q matches the expected wp.transform. Want me to draft it?
614-614: Verify parent_xform semantics for non-root free jointsPassing parent_xform=wp.transform(0, body_ori_for_joints) assumes the joint frame on the parent side carries the body’s orientation. Depending on ModelBuilder semantics, a free joint typically uses identity frames and encodes the initial relative pose solely in joint_q. If non-root freejoints are possible in your pipeline, please confirm this matches expectations.
I can cross-check ModelBuilder’s add_joint_free contract in this repo and propose an adjustment if needed.
616-624: Setting non-root free joint_q from world_xform may need to be relative to parentFor free joints that are not at the root, joint_q generally represents the child’s pose relative to the parent frame, not world. If non-root is possible in MJCF (and supported), joint_q here might need to be computed as relative(parent_world_xform, child_world_xform).
Would you like me to add a helper to compute relative transforms and wire it here only when parent != -1?
634-636: Using scaled parent_xform for non-free joints is consistent with the rest of the fixparent_xform based on body_pos_for_joints + joint_pos with body_ori_for_joints looks correct.
792-793: World transform propagation into recursion is correctPassing parent_world_xform=world_xform ensures consistent world poses for all descendants.
913-914: Root invocation updated correctlyInitializing recursion with parent_world_xform=None is aligned with the new logic.
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (11)
newton/tests/test_import_mjcf.py (11)
227-227: Make importer up-axis explicit to avoid hidden axis-correctionThe importer may apply an axis correction if its default up_axis differs from the builder’s up_axis. To make the test stable across defaults, pass up_axis="Z" explicitly.
- newton.utils.parse_mjcf(mjcf_content, builder) + newton.utils.parse_mjcf(mjcf_content, builder, up_axis="Z")If you prefer, apply the same tweak to all parse_mjcf calls in this file to future-proof these tests.
236-239: Compare quaternions modulo sign to reduce brittlenessNumerically equivalent rotations can flip sign (q ≡ -q). Use a helper assertion that accepts both, to reduce intermittent failures across platforms and refactors.
Add this helper to TestImportMjcf (outside changed lines):
def assert_quat_allclose(self, q_actual, q_expected, atol=1e-6, rtol=1e-6): qa = np.asarray(q_actual) qe = np.asarray(q_expected) if np.allclose(qa, qe, rtol=rtol, atol=atol): return if np.allclose(qa, -qe, rtol=rtol, atol=atol): return self.fail(f"Quaternions not equal up to sign: {qa} vs {qe}")Then replace usages like:
np.testing.assert_allclose(body_quat, expected_quat, atol=1e-6)with:
self.assert_quat_allclose(body_quat, expected_quat, atol=1e-6)This applies to all quaternion equality checks below.
241-280: Root body + custom xform composition is correct; minor robustness tweaksThe transform composition expected_xform = custom_xform * body_xform aligns with the importer’s semantics. Consider two small refinements:
- newton.utils.parse_mjcf(mjcf_content, builder, xform=custom_xform) + newton.utils.parse_mjcf(mjcf_content, builder, xform=custom_xform, up_axis="Z")And ensure dtype compatibility in asserts (Warp types generally work, but np.asarray is safer):
- np.testing.assert_allclose(body_pos, expected_pos, atol=1e-6) - np.testing.assert_allclose(body_quat, expected_quat, atol=1e-6) + np.testing.assert_allclose(body_pos, np.asarray(expected_pos), atol=1e-6) + self.assert_quat_allclose(body_quat, np.asarray(expected_quat), atol=1e-6)
301-301: Stabilize hierarchy test against up-axis defaultsTo avoid implicit axis-correction affecting expectations, make up-axis explicit.
- newton.utils.parse_mjcf(mjcf_content, builder) + newton.utils.parse_mjcf(mjcf_content, builder, up_axis="Z")
320-324: Remove unused placeholder initializationThe temporary quat_child placeholder is assigned then immediately overwritten. Drop it to reduce noise.
- quat_child = np.array([0, 0, 0, 1]) # placeholder, will be overwritten - quat_child_mjcf = np.array([0.7071068, 0, 0, 0.7071068]) - # MJCF: [w, x, y, z] → warp: [x, y, z, w] - quat_child = np.array([quat_child_mjcf[1], quat_child_mjcf[2], quat_child_mjcf[3], quat_child_mjcf[0]]) + quat_child_mjcf = np.array([0.7071068, 0, 0, 0.7071068]) # [w, x, y, z] + # MJCF [w, x, y, z] → Warp [x, y, z, w] + quat_child = np.array([quat_child_mjcf[1], quat_child_mjcf[2], quat_child_mjcf[3], quat_child_mjcf[0]])
351-351: Avoid accidental axis-correction in floating-base testMake up-axis explicit for stability across environment defaults.
- newton.utils.parse_mjcf(mjcf_content, builder, floating=True) + newton.utils.parse_mjcf(mjcf_content, builder, floating=True, up_axis="Z")
402-402: Make up-axis explicit in hierarchy + freejoint testSame rationale as above; avoids hidden rotations from axis correction.
- newton.utils.parse_mjcf(mjcf_content, builder) + newton.utils.parse_mjcf(mjcf_content, builder, up_axis="Z")
441-445: Remove debug prints from testsThese prints will spam test output and aren’t needed for assertions. Prefer logging behind a verbosity flag or drop entirely.
- # Debug output to see what's happening - print(f"Root transform: pos={root_xform.p}, quat={root_xform.q}") - print(f"Child local transform: pos={child_local_xform.p}, quat={child_local_xform.q}") - print(f"Expected composed: pos={expected_pos}, quat={expected_quat}") - print(f"Actual joint_q: pos={joint_pos}, quat={joint_quat}")
469-469: Explicit up-axis makes chain test resilient- newton.utils.parse_mjcf(mjcf_content, builder) + newton.utils.parse_mjcf(mjcf_content, builder, up_axis="Z")
526-526: Explicit up-axis in scale test- newton.utils.parse_mjcf(mjcf_content, builder, scale=2.0) + newton.utils.parse_mjcf(mjcf_content, builder, scale=2.0, up_axis="Z")
570-570: Explicit up-axis in branching tree test- newton.utils.parse_mjcf(mjcf_content, builder) + newton.utils.parse_mjcf(mjcf_content, builder, up_axis="Z")
📜 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_mjcf.py(8 hunks)newton/tests/test_import_mjcf.py(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- newton/_src/utils/import_mjcf.py
🧰 Additional context used
🧬 Code Graph Analysis (1)
newton/tests/test_import_mjcf.py (2)
newton/_src/utils/import_mjcf.py (1)
parse_mjcf(32-927)newton/tests/test_cloth.py (1)
finalize(773-815)
⏰ 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). (2)
- GitHub Check: Run GPU Unit Tests on AWS EC2 (Pull Request)
- GitHub Check: Run GPU Benchmarks (Pull Request)
🔇 Additional comments (1)
newton/tests/test_import_mjcf.py (1)
215-240: Solid baseline check for single-body world transform (pos + quat) — LGTMThis test correctly validates MJCF WXYZ-to-Warp XYZW conversion and translation passthrough for a single root body. Expectations match a 90° Z rotation.
There was a problem hiding this comment.
Actionable comments posted: 0
🔭 Outside diff range comments (1)
newton/_src/utils/import_mjcf.py (1)
578-583: Bug: base_joint dict path sets child to the XML root instead of the linkSetting base_joint["child"] = root will pass the XML root element to add_joint, which should be an integer link index. This will mis-wire the joint or raise at runtime.
Apply:
- base_joint["child"] = root + base_joint["child"] = link
♻️ Duplicate comments (1)
newton/_src/utils/import_mjcf.py (1)
594-595: Use wp.vec3 default here too (remaining tuple default)This site still defaults joint_pos to a Python tuple, which can trigger Warp deprecation warnings and potential type mismatch.
Apply:
- joint_pos = joint_pos[0] if len(joint_pos) > 0 else (0.0, 0.0, 0.0) + joint_pos = joint_pos[0] if len(joint_pos) > 0 else wp.vec3(0.0, 0.0, 0.0)
🧹 Nitpick comments (4)
newton/_src/utils/import_mjcf.py (2)
459-471: Simplify world transform composition to one branchYou can collapse the three-branch logic into a single expression to reduce cognitive load and avoid edge-case drift.
Apply:
- # Compose transforms properly through the hierarchy - if parent == -1: - # Root body: apply the root transform - world_xform = xform * local_xform - elif parent_world_xform is not None: - # Child body: compose with parent's world transform - world_xform = parent_world_xform * local_xform - else: - # Fallback (shouldn't happen in normal cases) - world_xform = local_xform + # Compose with either the passed parent world transform or the import root xform + world_xform = (parent_world_xform or xform) * local_xformAnd call parse_body for roots with xform as the initial parent transform:
- for body in world.findall("body"): - parse_body(body, -1, world_defaults, parent_world_xform=None) + for body in world.findall("body"): + parse_body(body, -1, world_defaults, parent_world_xform=xform)
598-603: Verify: parent_xform for free joints is necessary?ModelBuilder.add_joint_free typically initializes joint_q from the child body's xform; passing a non-identity parent_xform can rotate the joint frame relative to the parent unnecessarily and risks double-applying orientation.
Please confirm this is intended. If not needed, drop parent_xform for clarity and to align with builder semantics.
Proposed change:
- builder.add_joint_free( - link, - key="_".join(joint_name), - parent_xform=wp.transform(wp.vec3(0.0, 0.0, 0.0), body_ori_for_joints), - ) + builder.add_joint_free( + link, + key="_".join(joint_name), + )newton/tests/test_import_mjcf.py (2)
113-119: Make mesh maxhullvert test robust to shape orderingIterating range(3) assumes the first three shapes are meshes. Filter across all shapes for those with maxhullvert instead.
Apply:
- # Check that meshes have correct maxhullvert values - # Note: This assumes meshes are added in order they appear in MJCF - meshes = [model.shape_source[i] for i in range(3) if hasattr(model.shape_source[i], "maxhullvert")] - - if len(meshes) >= 3: - self.assertEqual(meshes[0].maxhullvert, 32) - self.assertEqual(meshes[1].maxhullvert, 128) - self.assertEqual(meshes[2].maxhullvert, 64) # Default value + # Check that meshes have correct maxhullvert values + meshes = [src for src in model.shape_source if hasattr(src, "maxhullvert")] + self.assertEqual(len(meshes), 3) + self.assertEqual(meshes[0].maxhullvert, 32) + self.assertEqual(meshes[1].maxhullvert, 128) + self.assertEqual(meshes[2].maxhullvert, 64) # Default value
441-445: Remove debug prints from testsThese prints add noise to CI logs; assertions already capture mismatches.
Apply:
- # Debug output to see what's happening - print(f"Root transform: pos={root_xform.p}, quat={root_xform.q}") - print(f"Child local transform: pos={child_local_xform.p}, quat={child_local_xform.q}") - print(f"Expected composed: pos={expected_pos}, quat={expected_quat}") - print(f"Actual joint_q: pos={joint_pos}, quat={joint_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 (2)
newton/_src/utils/import_mjcf.py(8 hunks)newton/tests/test_import_mjcf.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). (2)
- GitHub Check: Run GPU Unit Tests on AWS EC2 (Pull Request)
- GitHub Check: Run GPU Benchmarks (Pull Request)
🔇 Additional comments (5)
newton/_src/utils/import_mjcf.py (4)
434-440: Good: threading parent world transform through the recursionAdding parent_world_xform to parse_body is the right move to compose transforms hierarchically without re-deriving state.
535-537: Correct: bodies now get composed world transformsPassing xform=world_xform to add_body aligns body_q with the compounded XML pos/quat and importer xform.
549-551: Nice: default joint pos uses wp.vec3 to avoid Warp deprecationThis prevents tuple + wp.vec3 coercion and future breakage. Good catch.
771-772: Good: pass composed world transform into recursionPropagating world_xform ensures all descendants compose relative to their true parents.
newton/tests/test_import_mjcf.py (1)
215-639: Strong, targeted coverage for transform composition and free-joint initializationThis suite exercises root/child composition, importer xform, scaling, branching, and joint_q/body_q consistency. Nicely done.
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
|
@eric-heiden - I might have been overeager. Does the newton free joint follow MuJoCo in the sense the joint_q is always in world frame, or does it respect the transform hierarchy? |
@adenzler-nvidia The free joint will follow the transform hierarchy so if there were a parent body the free joint's coordinates would be relative to that parent. But this is not possible in MJCF so we can assume in this case the free joint will always be in world coordinates. |
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🔭 Outside diff range comments (2)
newton/_src/utils/import_mjcf.py (2)
577-584: base_joint dict branch sets child to XML root instead of linkPassing child=root (the XML element) into builder.add_joint is a bug. It should be the body index link.
Apply this diff:
- base_joint["child"] = root + base_joint["child"] = link
606-614: Rotate joint_pos into the parent frame when building parent_xformparent_xform should be T_parent->joint = T_parent->child ∘ T_child->joint. Given joint_pos is specified in the child frame, you need to rotate it by body_ori_for_joints before adding to body_pos_for_joints. Without this, anchors are incorrect whenever the child has a rotation.
Apply this diff:
- parent_xform=wp.transform(body_pos_for_joints + joint_pos, body_ori_for_joints), + parent_xform=wp.transform( + body_pos_for_joints + wp.quat_rotate(body_ori_for_joints, joint_pos), + body_ori_for_joints, + ),If helpful, I can add a minimal test where a child body has a nontrivial orientation and a joint_pos offset, verifying the computed anchor matches T_parent->child ∘ T_child->joint.
♻️ Duplicate comments (1)
newton/_src/utils/import_mjcf.py (1)
593-595: Good: default joint_pos is now a wp.vec3This resolves the Warp deprecation warning and prevents type-mismatch at runtime. Thanks for addressing the earlier feedback consistently in both branches.
🧹 Nitpick comments (2)
newton/_src/utils/import_mjcf.py (2)
459-476: World transform composition is correct; minor naming/consistency nitlocal_xform and world_xform are composed correctly, and body_pos_for_joints/body_ori_for_joints are cleanly separated for joint construction. No issues here.
If you want to avoid confusion later, consider renaming body_pos_for_joints to child_pos_in_parent and body_ori_for_joints to child_ori_in_parent to make the frame explicit.
89-93: Deduplicate and harden xform normalization with up-axis alignmentMinor cleanup: xform is normalized at Lines 89–93 and then re-normalized/checked again at Lines 198–203. Because xform is set to a non-None value at Line 90, the “if xform is None” branch at Line 199 is effectively dead. Also, re-applying wp.transform(*xform) when xform is already a wp.transform relies on unpacking semantics that may not be guaranteed.
Consider normalizing exactly once near the up-axis alignment and handling both tuple and wp.transform inputs explicitly.
Apply this diff to simplify:
@@ - if xform is None: - xform = wp.transform() - else: - xform = wp.transform(*xform) + # Defer normalization until after up-axis alignment is known @@ - axis_xform = wp.transform(wp.vec3(0.0), quat_between_axes(up_axis, builder.up_axis)) - if xform is None: - xform = axis_xform - else: - xform = wp.transform(*xform) * axis_xform + axis_xform = wp.transform(wp.vec3(0.0), quat_between_axes(up_axis, builder.up_axis)) + # Normalize xform into a wp.transform and compose up-axis adjustment exactly once + if xform is None: + xform = axis_xform + elif isinstance(xform, wp.transform): + xform = xform * axis_xform + else: + xform = wp.transform(*xform) * axis_xformAlso applies to: 198-203
📜 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_mjcf.py(8 hunks)newton/tests/test_import_mjcf.py(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- newton/tests/test_import_mjcf.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 (4)
newton/_src/utils/import_mjcf.py (4)
434-440: Good API: threading parent world transform into parse_bodyPassing parent_world_xform down the recursion is the right call to ensure correct hierarchical composition.
534-536: Using world_xform for body creation is correctThis ensures the body’s initial pose fully reflects the parent chain, root xform, and up-axis conversion.
763-771: Propagating world_xform to children is correctForwarding parent_world_xform=world_xform ensures consistent hierarchical poses throughout parsing.
889-892: Root invocation with parent_world_xform=None is appropriateClear separation between root-body composition and child recursion. Looks good.
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Signed-off-by: Alain Denzler <adenzler@nvidia.com>
There was a problem hiding this comment.
Actionable comments posted: 0
🔭 Outside diff range comments (2)
newton/_src/utils/import_mjcf.py (2)
545-576: base_joint dict: child set to XML root instead of the body indexWhen base_joint is a dict, you assign child=root (the XML element), which will break joint creation. This should be the newly created body index (link).
Apply this diff:
- elif isinstance(base_joint, dict): - base_joint["parent"] = -1 - base_joint["child"] = root - base_joint["parent_xform"] = base_parent_xform - base_joint["child_xform"] = base_child_xform - base_joint["key"] = "base_joint" - builder.add_joint(**base_joint) + elif isinstance(base_joint, dict): + base_joint["parent"] = -1 + base_joint["child"] = link + base_joint["parent_xform"] = base_parent_xform + base_joint["child_xform"] = base_child_xform + base_joint["key"] = "base_joint" + builder.add_joint(**base_joint)
596-606: Parent joint frame ignores child's rotation for joint_pos (misplaced anchors when child is rotated)In the non-root joint path, parent_xform uses body_pos_for_joints + joint_pos without rotating joint_pos by the child body’s orientation. This yields incorrect parent-side joint anchor when the child has a local rotation.
Apply this diff:
- parent_xform=wp.transform(body_pos_for_joints + joint_pos, body_ori_for_joints), + parent_xform=wp.transform( + body_pos_for_joints + wp.quat_rotate(body_ori_for_joints, joint_pos), + body_ori_for_joints + ),Note: The special handling in the root base_joint path to not rotate the base joint’s axis is orthogonal and doesn’t apply here; regular joints should rotate their local offsets into the parent frame.
🧹 Nitpick comments (3)
newton/_src/utils/import_mjcf.py (1)
581-583: Avoid hardcoded joint name for floating-base free jointHardcoding "floating_base" is brittle. Prefer the MJCF-provided freejoint name if present (or default to "_freejoint") to keep naming consistent with the non-base path.
Apply this diff:
- elif floating is not None and floating: - builder.add_joint_free(link, key="floating_base") + elif floating is not None and floating: + builder.add_joint_free(link, key=freejoint_tags[0].attrib.get("name", f"{body_name}_freejoint"))newton/tests/test_import_mjcf.py (2)
332-379: Make joint lookup resilient to name changesRelying on the exact name "floating_body_freejoint" can be brittle. Prefer a fallback: if that name is missing, find a free joint whose name ends with "_freejoint" and contains "floating_body" (or use a model API mapping if available).
Apply this diff:
- joint_idx = model.joint_key.index("floating_body_freejoint") + joint_names = model.joint_key + if "floating_body_freejoint" in joint_names: + joint_idx = joint_names.index("floating_body_freejoint") + else: + joint_idx = next( + i for i, n in enumerate(joint_names) + if n.endswith("_freejoint") and "floating_body" in n + )
230-240: Optional: make quaternion asserts sign-agnosticQuaternions q and -q represent the same rotation; numerical operations can occasionally flip signs. Current asserts may fail in such cases.
You can add a small helper and use it where you compare quats:
def assert_quat_allclose(testcase, actual, expected, **kwargs): actual = np.asarray(actual) expected = np.asarray(expected) if np.dot(actual, expected) < 0.0: expected = -expected np.testing.assert_allclose(actual, expected, **kwargs)Then replace np.testing.assert_allclose(body_quat, expected, ...) with assert_quat_allclose(self, body_quat, expected, ...).
Also applies to: 278-280, 311-313, 329-331, 426-428, 437-439, 516-519, 531-533, 543-545, 555-557, 567-569
📜 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_mjcf.py(8 hunks)newton/tests/test_import_mjcf.py(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
newton/tests/test_import_mjcf.py (2)
newton/_src/utils/import_mjcf.py (1)
parse_mjcf(32-918)newton/tests/test_cloth.py (1)
finalize(773-815)
⏰ 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). (2)
- GitHub Check: Run GPU Benchmarks (Pull Request)
- GitHub Check: Run GPU Unit Tests on AWS EC2 (Pull Request)
🔇 Additional comments (9)
newton/_src/utils/import_mjcf.py (3)
459-467: World transform composition and recursion threading look correctLocal → world composition via (parent_world_xform or xform) * local_xform and passing parent_world_xform through recursion is the right fix. This aligns with the tests and ensures body_q is truly world-space.
Also applies to: 762-763, 883-884
541-543: Good fix: default joint_pos now uses wp.vec3Using wp.vec3 instead of a tuple prevents Warp deprecation warnings and potential type errors when adding to a wp.vec3.
527-529: Bodies now receive world_xform at creationPassing xform=world_xform into add_body is consistent with the new composition logic and matches the new tests’ expectations for body_q.
newton/tests/test_import_mjcf.py (6)
215-240: LGTM: single-body world transform validation is correctSolid sanity check for position and quaternion convention mapping (MJCF [w,x,y,z] → Warp [x,y,z,w]).
241-280: LGTM: custom xform composition test is accurateComposing expected_xform = custom_xform * body_xform mirrors importer logic.
281-331: LGTM: parent-child hierarchy composition test reads correctlyValidates both translation and quaternion composition for child relative to rotated parent.
380-439: LGTM: rotation chain test covers quaternion composition wellGood coverage for cumulative rotations across a chain.
440-473: LGTM: scale propagation test is correctVerifies that child positions are scaled in world as expected.
474-569: LGTM: branching hierarchy test is thoroughCovers multiple quaternion encodings and independent branches. Nice cross-check of orientation conversions.
eric-heiden
left a comment
There was a problem hiding this comment.
LGTM, feel free to merge! Thanks for adding the tests also.
…utes (newton-physics#565) Signed-off-by: Alain Denzler <adenzler@nvidia.com> Signed-off-by: Miguel Zamora <mazamoramora@nvidia.com>
…utes (newton-physics#565) Signed-off-by: Alain Denzler <adenzler@nvidia.com>
Fixes deprecated scipy imports in the devices module for interfacing with SE3 gamepad, keyboard and spacemouse. - Bug fix (non-breaking change which fixes an issue) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there
…#565) Turn off async rendering for replicator to avoid race condition for external camera rendering # Description <!-- Thank you for your interest in sending a pull request. Please make sure to check the contribution guidelines. Link: https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html --> Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. Fixes # (issue) <!-- As a practice, it is recommended to open an issue to have discussions on the proposed pull request. This makes it easier for the community to keep track of what is being developed or added, and if a given feature is demanded by more than one party. --> ## Type of change <!-- As you go through the list, delete the ones that are not applicable. --> - Bug fix (non-breaking change which fixes an issue) - New feature (non-breaking change which adds functionality) - Breaking change (fix or feature that would cause existing functionality to not work as expected) - This change requires a documentation update ## Screenshots Please attach before and after screenshots of the change if applicable. <!-- Example: | Before | After | | ------ | ----- | | _gif/png before_ | _gif/png after_ | To upload images to a PR -- simply drag and drop an image while in edit mode and it should upload the image directly. You can then paste that source into the above before/after sections. --> ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there <!-- As you go through the checklist above, you can mark something as done by putting an x character in it For example, - [x] I have done this task - [ ] I have not done this task -->
…utes (newton-physics#565) Signed-off-by: Alain Denzler <adenzler@nvidia.com>
This change fixes the MJCF importer to initialize the right world transforms based on combining the pos/quat tags with the parent xform.
It also makes sure that the joint_q values for free joint are initialized correctly with the world transforms.
Before your PR is "Ready for review"
newton/tests/test_examples.py)pre-commit run -aSummary by CodeRabbit
New Features
Bug Fixes
Refactor
Tests
Chores