Author fixed-root-link world joint with pure USD#6536
Conversation
Replace the omni.physx.scripts.utils.createJoint call in modify_articulation_root_properties with a pure-USD equivalent so the fixed-root-link spawn path no longer depends on omni.physx. This fixes a ModuleNotFoundError when spawning fixed-base articulations on kitless backends (e.g. Newton), where omni.physx is unavailable. Add regression tests on an in-memory USD stage covering the world-anchored joint attributes and the instanceable-root fallback, plus a changelog fragment.
| """Authoring must materialize a ``UsdPhysics.FixedJoint`` anchored to the world. | ||
|
|
||
| A world-attached fixed joint targets only ``body1`` (the root link), leaving ``body0`` | ||
| empty, and anchors ``localPos0`` at the root link's world translation. | ||
| """ | ||
| stage = Usd.Stage.CreateInMemory() | ||
| prim = _make_root_prim(stage, "/World/Robot", translation=(1.0, 2.0, 3.0)) | ||
|
|
||
| _create_world_fixed_joint(prim, stage) | ||
|
|
||
| joint = _find_fixed_joint(stage) | ||
| assert joint is not None, "no UsdPhysics.FixedJoint was authored" | ||
| # world-attached joint: body0 empty, body1 -> root link | ||
| assert joint.GetBody0Rel().GetTargets() == [] | ||
| assert joint.GetBody1Rel().GetTargets() == [prim.GetPath()] | ||
| # localPos0 anchors the joint at the root link's world pose | ||
| local_pos0 = joint.GetLocalPos0Attr().Get() | ||
| assert math.isclose(local_pos0[0], 1.0) and math.isclose(local_pos0[1], 2.0) and math.isclose(local_pos0[2], 3.0) | ||
| # the joint must be effectively unbreakable | ||
| assert joint.GetBreakForceAttr().Get() > 1e37 | ||
| assert joint.GetBreakTorqueAttr().Get() > 1e37 |
There was a problem hiding this comment.
localRot0 code path is untested
Both test functions use a prim with a zero or identity rotation, so joint.GetLocalRot0Attr().Get() is always the identity quaternion and the Gf.Quatf(world_pose.ExtractRotationQuat()) branch is never exercised. If the rotation-extraction logic regresses (e.g. wrong matrix decomposition order, inadvertent RemoveScaleShear stripping the rotation for scaled parents), no test would catch it. Adding a third test that translates and rotates the prim, then asserts the authored localRot0 matches the expected quaternion, would close this gap.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| def test_create_world_fixed_joint_skips_instanceable_root(): | ||
| """The joint must be authored on a writable ancestor when the root prim is instanceable. | ||
|
|
||
| Instanceable/instance-proxy/prototype prims are not authorable, so the joint prim must | ||
| be created under the first writable ancestor rather than under the instanceable root. | ||
| """ | ||
| stage = Usd.Stage.CreateInMemory() | ||
| UsdGeom.Xform.Define(stage, "/World") | ||
| prim = _make_root_prim(stage, "/World/Robot", translation=(0.0, 0.0, 0.0)) | ||
| prim.SetInstanceable(True) | ||
| assert prim.IsInstanceable() | ||
|
|
||
| _create_world_fixed_joint(prim, stage) | ||
|
|
||
| joint = _find_fixed_joint(stage) | ||
| assert joint is not None, "no UsdPhysics.FixedJoint was authored" | ||
| # the joint prim must not be parented under the instanceable root | ||
| joint_path = joint.GetPrim().GetPath().pathString | ||
| assert not joint_path.startswith("/World/Robot/"), f"joint authored under instanceable root: {joint_path}" | ||
| # it still targets the root link | ||
| assert joint.GetBody1Rel().GetTargets() == [prim.GetPath()] |
There was a problem hiding this comment.
The name-deduplication loop inside _create_world_fixed_joint (FixedJoint → FixedJoint0 → …) is never exercised. A test that calls _create_world_fixed_joint twice on the same parent prim and asserts that two distinct joint prims exist with unique paths would cover this branch and protect against off-by-one regressions in the uniquifier counter.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
…d_joint fix_articulation_root reimplemented the same joint authoring logic that _create_world_fixed_joint provides, without the instanceable-prim walk-up or unbreakable break forces. Delegate to _create_world_fixed_joint so both code paths share one implementation.
The local import inside fix_articulation_root was cargo-culted from cfg-only modules that must defer Kit imports. PhysicsManager is not imported before Kit, so a top-level import is correct and preferred.
Two issues fixed: - Importing a `_`-prefixed symbol across module boundaries is bad design. Rename to `create_world_fixed_joint` (no underscore) so callers can import it without bypassing the privacy convention. - The top-level import added in the previous commit was wrong: physics_manager is pulled in before the simulation app starts (env cfg -> manager_base -> PhysicsManager), so top-level imports of USD/omni modules break config loading. Restore the local import pattern and update the comment to document this constraint explicitly.
The local-import pattern with # noqa: PLC0415 was cargo-cult: cfg files (ManagerBasedRLEnvCfg etc.) do not import physics_manager.py — they reach manager_term_cfg.py via lazy_export, which never touches manager_base.py. There is no pre-Kit import constraint here. Move pxr, create_world_fixed_joint, and find_global_fixed_joint_prim to the module-level import block and remove the now-incorrect comments.
Description
modify_articulation_root_propertiescreated the fixed-root-link world joint throughomni.physx.scripts.utils.createJoint, so spawning a fixed-base articulation (fix_root_link=True) raisedModuleNotFoundError: omni.physxon kitless backends (e.g. Newton) where Kit and PhysX extensions are not loaded.This PR replaces that call with a pure-USD
_create_world_fixed_jointhelper that reproduces the behavior ofomni.physx.createJointfor the single-body (world-attached) case: it targets onlybody1(the root link), anchorslocalPos0/localRot0at the root link's world pose with scale/shear removed, sets effectively unbreakable break force/torque, walks up to the first writable ancestor when the root link lives inside an instanceable/prototype subtree, and uniquifies the joint name on collision. The same code path now runs on every backend, so Kit/PhysX behavior is unchanged.Regression tests run on an in-memory USD stage without launching Isaac Sim / Kit, covering the world-anchored joint attributes and the instanceable-root fallback. Since this helper is the single fixed-root-link authoring path for all backends, passing without Kit also proves the kitless case.
Type of change
Checklist
pre-commitchecks with./isaaclab.sh --formatsource/<pkg>/changelog.d/for every touched package (do not editCHANGELOG.rstor bumpextension.toml— CI handles that)CONTRIBUTORS.mdor my name already exists there