Repository navigation
Conversation
With fix_base=True, the Isaac Sim importers keep a fixed joint that already attaches the root link to the world, the root_joint that the URDF importer adds or the weld of an MJCF root body without joints, but leave the articulation root on the root link. UsdPhysics treats such an articulation as floating, so PhysX simulated these assets as floating articulations that the joint only held as an extra constraint: they reported a floating base and undid root-pose writes. Newton treated them as fixed-base. Root the articulation at that fixed world joint after the import, as the importers do for a fixed joint they create. Revolute, prismatic and D6 world joints keep a body root, since PhysX would weld the root of an articulation rooted at them. Let fix_root_link=True enable the fixed joint that roots an articulation, instead of raising NotImplementedError or adding a second world joint.
|
There was a problem hiding this comment.
Isaac Lab Review Bot
The fixed-base articulation-root relocation is appropriately scoped and covered across converter, PhysX, and Newton paths. Before merge, add the required return annotations to the new no-value helpers and strengthen the MJCF welded-root test to verify that the relocated root is actually a fixed joint rather than relying only on its generated name.
- Design and architecture: The post-import correction is centralized in the converter base and edits the shared physics layer or flat USD, allowing URDF and MJCF conversions and their physics variants to share the same relocation behavior. The backend schema handling and fixed-joint-root reuse paths were reviewed without an actionable architecture issue.
- API:
MjcfConverterCfg.fix_base,UrdfConverterCfg.fix_base, andArticulationRootBaseCfg.fix_root_linkretain their names, types, and defaults. The manager and schema-writer changes extendfix_root_link=Trueto reuse an articulation root that is itself a fixed joint instead of raising or creating a duplicate joint. - Compatibility and deprecation: Breaking changes: none identified. The observable topology and dynamics changes align
fix_base=Truewith its documented fixed-base contract; the changelog records changed matrix/Jacobian layouts, root-path implications, and the fact that cached or previously converted USD files remain unchanged until reconversion. - Implementation: The relocation path distinguishes enabled fixed world joints from disabled, excluded, or non-fixed joints, handles layered and flat converter output, and moves authored articulation-root schemas and attributes. Add
-> Noneto_root_articulations_at_world_jointsand_move_articulation_rootas required for functions that return no value. - Style consistency: The new private helpers follow the file’s structural ordering and keep
pxrimports deferred. The two no-value helper signatures omit the contribution guide’s required-> Nonereturn annotation and should be corrected. - Test quality: The changed tests cover layered and flat URDF output, MJCF free/welded/slide root paths, PhysX replication, and Newton duplicate-joint handling. The MJCF welded case currently identifies the relocated root only by the generated name
PhysicsFixedJoint; it should also assertIsA(UsdPhysics.FixedJoint)so the test verifies the fixed-joint topology it claims to protect.
Minor fixes needed. Posted 2 actionable findings inline.
Automated review; human maintainers own approval decisions.
fix_root_link=True treated any fixed joint that roots an articulation as the joint that attaches it to the world. A fixed joint between two bodies does not, so enable the joint only when one of its sides is the world, and keep raising NotImplementedError otherwise, as before.
UsdPhysics resolves a joint target that is a collider without a rigid body to a static body, and roots no articulation at that joint, so do not treat it as the world. Annotate the new helpers that return nothing and assert that the MJCF fixed-base roots are fixed joints.
|
Hi @NeoZng Thanks a lot for finding this! I feel like the issue is more on the converter than on IsaacLab, and that this PR is mostly working around the issue from IsaacSim's converter. Am I reading this correctly? If so, could the fix land on IsaacSim instead? |
|
Yes, that's right: the root cause is in the Isaac Sim importer, which I filed as isaac-sim/IsaacSim#859 with a patch (the file lives under |
The importer layout is to be fixed in the Isaac Sim importer (isaac-sim/IsaacSim#859), so drop the post-import correction from the converters and its tests. Keep the fix_root_link=True handling of an articulation rooted at its fixed world joint, which such assets need once the importer roots them there, and test it on PhysX and Newton with a hand-authored USD in that layout.
|
Done in 14020d9: the converter correction is dropped, and the PR now only contains the |
AntoineRichard
left a comment
There was a problem hiding this comment.
Thanks for the revision! Left a couple of comments
| def _is_world_fixed_joint(prim: Usd.Prim) -> bool: | ||
| """Return whether a prim is a fixed joint that attaches a body to the world. | ||
|
|
||
| UsdPhysics attaches a side of a joint to the world when its body target is empty, or when neither the target nor | ||
| an ancestor is a rigid body or a collider. A collider without a rigid-body ancestor is a static body. | ||
| """ | ||
| if not prim.IsA(UsdPhysics.FixedJoint): | ||
| return False | ||
| joint = UsdPhysics.FixedJoint(prim) | ||
| stage = prim.GetStage() | ||
| for rel in (joint.GetBody0Rel(), joint.GetBody1Rel()): | ||
| targets = rel.GetTargets() | ||
| body = stage.GetPrimAtPath(targets[0]) if targets else None | ||
| while body and not body.IsPseudoRoot(): | ||
| if body.HasAPI(UsdPhysics.RigidBodyAPI) or body.HasAPI(UsdPhysics.CollisionAPI): | ||
| break | ||
| body = body.GetParent() | ||
| if not body or body.IsPseudoRoot(): | ||
| return True | ||
| return False | ||
|
|
||
|
|
There was a problem hiding this comment.
I am not sure if this should be here. I think this is more of a utility.
There was a problem hiding this comment.
Moved it to isaaclab.sim.utils.queries in 762c3fe, next to find_global_fixed_joint_prim. I kept it private, since only the legacy writer and PhysicsManager.fix_articulation_root use it.
| # Base link with one revolute child, rooted at the fixed joint that attaches the base link to the asset prim, the | ||
| # fixed-base layout that UsdPhysics recommends. | ||
| _FIXED_JOINT_ROOT_USDA = """#usda 1.0 | ||
| ( | ||
| defaultPrim = "Robot" | ||
| metersPerUnit = 1 | ||
| upAxis = "Z" | ||
| ) | ||
|
|
||
| def Xform "Robot" | ||
| { | ||
| def Xform "base" ( | ||
| prepend apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI"] | ||
| ) | ||
| { | ||
| float physics:mass = 5 | ||
| float3 physics:diagonalInertia = (0.05, 0.05, 0.05) | ||
|
|
||
| def Cube "collision" ( | ||
| prepend apiSchemas = ["PhysicsCollisionAPI"] | ||
| ) |
There was a problem hiding this comment.
In some of your PRs these assets are authored in python in this one it's in plane text. Could you move that to python. We'll collect all these custom assets later to upload them to our nucleus.
There was a problem hiding this comment.
Done in 762c3fe: _author_fixed_joint_root_robot builds the asset with the USD API. I also reduced the test to fix_root_link=True and the is_fixed_base check.
| # Base link with one revolute child, rooted at the fixed joint that attaches the base link to the asset prim, the | ||
| # fixed-base layout that UsdPhysics recommends. | ||
| _FIXED_JOINT_ROOT_USDA = """#usda 1.0 | ||
| ( | ||
| defaultPrim = "Robot" | ||
| metersPerUnit = 1 | ||
| upAxis = "Z" | ||
| ) | ||
|
|
||
| def Xform "Robot" | ||
| { |
There was a problem hiding this comment.
Done in 762c3fe, with the same helper and the same reduction.
Move _is_world_fixed_joint to isaaclab.sim.utils.queries, next to find_global_fixed_joint_prim, and keep it private: only the legacy schema writer and PhysicsManager.fix_articulation_root use it. Author the test asset rooted at its fixed world joint with the USD API instead of a USDA string, and reduce the PhysX and Newton tests to fix_root_link=True on that asset. Write the changelog fragment in the towncrier format and empty the skip markers.
Replace the hand-written ancestor walk with the existing query. A dangling body target is still taken as the world, as UsdPhysics does; it is checked first because the query raises for a missing prim.
|
@AntoineRichard Thanks for the review. The updates for your comments:
|
|
@AntoineRichard I merged |
AntoineRichard
left a comment
There was a problem hiding this comment.
Thanks @NeoZng, the scope after dropping the converter part looks right, and the rejection tests cover what Greptile flagged. On your question: keep _is_world_fixed_joint private, no need to export it.
Two things I'd like addressed (inline): the both-sides-world case in _is_world_fixed_joint, and making fix_root_link=False explicit for joint roots. The other two are wording nits.
| targets[0], lambda p: p.HasAPI(UsdPhysics.RigidBodyAPI) or p.HasAPI(UsdPhysics.CollisionAPI), stage=stage | ||
| ) | ||
| if body is None: | ||
| return True |
There was a problem hiding this comment.
When both body0 and body1 point at non-body prims (e.g. both target /Robot), this returns True. fix_root_link=True then enables a joint that attaches nothing to the world, and returns as if it succeeded. Before this PR, that root raised NotImplementedError. Could you require one side to resolve to the world and the other to resolve to a rigid body?
There was a problem hiding this comment.
Done in 9b9c96f: one side must now be a rigid body and the other the world, so these roots raise NotImplementedError again.
|
|
||
| if fix_root_link is not None: | ||
| existing_fixed_joint_prim = find_global_fixed_joint_prim(prim_path, stage=stage) | ||
| if fix_root_link and _is_world_fixed_joint(articulation_prim): |
There was a problem hiding this comment.
The new docs paragraph says fix_root_link=False can't make an articulation rooted on a fixed joint floating. It's silently ignored when body0 targets the asset prim, and per the docs it breaks PhysX when body0 is empty. Could both writers (here and the fix_root_link is False branch of apply_articulation_root_properties) log a warning in that case? If you'd rather keep this PR narrow, a follow-up issue is fine.
There was a problem hiding this comment.
Done in 9b9c96f: both writers now warn in that case, for either body0 layout; the behavior is unchanged.
| @@ -0,0 +1,4 @@ | |||
| * Fixed ``fix_root_link=True`` raising ``NotImplementedError`` for an articulation rooted at the fixed joint that | |||
| attaches it to the world when the joint's ``body0`` targets the asset prim, the fixed-base layout that UsdPhysics | |||
There was a problem hiding this comment.
Nit: UsdPhysics allows this layout rather than recommending it. The ArticulationRootAPI docs list it as one option: "…or on the joint itself". Could you say "allows" here and in the PR description?
There was a problem hiding this comment.
Done in 9b9c96f and in the PR description.
| def _is_world_fixed_joint(prim: Usd.Prim) -> bool: | ||
| """Check whether a prim is a fixed joint that attaches a body to the simulation world. | ||
|
|
||
| UsdPhysics attaches a side of a joint to the world when its body target is empty, or when neither the target nor |
There was a problem hiding this comment.
Nit: the spec doesn't say that a missing body target attaches to the world. That's a reasonable choice, but it's Isaac Lab's choice, so the docstring shouldn't present it as a UsdPhysics rule. A short comment saying a plain UsdPhysics.Joint is excluded on purpose (it's a free D6 joint) would also help, since find_global_fixed_joint_prim accepts it.
There was a problem hiding this comment.
Done in 9b9c96f: the docstring now calls it Isaac Lab's rule, and a comment explains why a plain UsdPhysics.Joint is excluded.
A fixed joint between two prims that are not rigid bodies, such as one whose targets both name the asset prim, could count as a world joint, so fix_root_link=True enabled it instead of raising. Require one side to resolve to a rigid body and the other to the world. fix_root_link=False is not supported for an articulation rooted at its fixed world joint, so both writers now log a warning for it. Say that UsdPhysics allows this layout rather than recommends it, present the world rule as Isaac Lab's, and note why a plain UsdPhysics.Joint is excluded.
|
@AntoineRichard Thanks for the review. All four points are addressed in 9b9c96f (replies inline); |
| if _is_world_fixed_joint(articulation_prim): | ||
| joint = UsdPhysics.Joint(articulation_prim) | ||
| else: | ||
| joint = find_global_fixed_joint_prim(root_path, stage=stage) |
There was a problem hiding this comment.
I am wondering if it can be all handled in find_global_fixed_joint_prim function?
The three fix_root_link call sites each checked whether the articulation root is its own fixed world joint before calling find_global_fixed_joint_prim. The finder now does that check first, and the callers use it alone. fix_root_link=False on such a root warned that it is not supported and then still disabled the joint when its body0 was empty, because the subtree search also visits the root prim. Both writers now warn and leave the joint enabled.
|
run-ci |
fix_root_link=False now leaves an articulation's root world joint enabled, so the how-to no longer says that a root joint with an empty body0 is disabled. The changelog fragment drops "or adding a second world joint": for this layout develop always raised.
|
run-ci |
Description
fix_root_link=TrueraisesNotImplementedError: Cannot fix non-rigid articulation rootfor an articulation whoseArticulationRootAPIis on the fixed joint that attaches it to the world, when the joint'sbody0targets the assetprim. This is a fixed-base layout that UsdPhysics allows. It is also the layout that the Isaac Sim URDF and MJCF
importers will produce with
fix_base=Trueonce isaac-sim/IsaacSim#859 is fixed: they will root the articulation atthe fixed joint that already attaches the root link to the world, such as the URDF importer's
root_joint, whosebody0is the asset prim.fix_root_linkonly recognized a world joint with an empty body target under the rootprim.
find_global_fixed_joint_primnow returns the queried prim when it is such a joint, sofix_root_link=Trueenables itin both the schema-fragment and the legacy writer. One side of the joint must resolve to a rigid body, and the other
side counts as the world when its body target is empty, names no prim, or has no rigid-body or collider ancestor.
Joints below the queried prim still need an empty body target. A fixed joint root between two bodies, between a body
and a static collider, or between two prims that are not rigid bodies still raises.
fix_root_link=Falseis not supported for an articulation rooted at its world joint: both writers now log a warningand leave the joint enabled. Before, a root joint with an empty
body0, such as thefix_base_jointthat theimporters add to a free root body with
fix_base=True, was disabled: PhysX then could not create the articulation,while Newton simulated a floating base. Assets whose root link the importers find attached to the world keep the root
on the link until #859 is fixed and are not affected.
make_fixed_prim.rstdocuments this arrangement.Type of change
Release backport
developVerification
Isaac Sim 6.1:
test_schemas.py: 39 passed. Ondevelop,test_fix_root_link_enables_fixed_world_joint_root[fragments|legacy]fails with a
NotImplementedError.test_fix_root_link_rejects_non_world_fixed_joint_root[rigid_base|static_collider_base|no_rigid_body|world_static_collider]checks that both writers still raise, and
test_fix_root_link_false_warns_for_fixed_world_joint_root[asset_prim_body0-fragments|empty_body0-legacy]that bothwarn and leave the joint enabled for
fix_root_link=False.test_articulation.py::test_initialization_fixed_joint_rootpass on an asset in this layout thatthe test authors with the USD API. On
develop, it fails with the same error on both backends.pre-commitandtools/changelog/cli.py checkpass.Checklist
pre-commitchecks withuv run isaaclab --formatsource/<pkg>/changelog.d/for every touched package (do not editCHANGELOG.rstor bump package versions — CI handles that)CONTRIBUTORS.mdor my name already exists there