Skip to content

Enable fix_root_link for articulations rooted at their fixed world joint - #8164

Open
NeoZng wants to merge 12 commits into
isaac-sim:mainfrom
NeoZng:fix/fixed-base-articulation-root
Open

NeoZng wants to merge 12 commits into
isaac-sim:mainfrom
NeoZng:fix/fixed-base-articulation-root

Conversation

@NeoZng

@NeoZng NeoZng commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Description

fix_root_link=True raises NotImplementedError: Cannot fix non-rigid articulation root for an articulation whose
ArticulationRootAPI is on the fixed joint that attaches it to the world, when the joint's body0 targets the asset
prim. 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=True once isaac-sim/IsaacSim#859 is fixed: they will root the articulation at
the fixed joint that already attaches the root link to the world, such as the URDF importer's root_joint, whose
body0 is the asset prim. fix_root_link only recognized a world joint with an empty body target under the root
prim.

find_global_fixed_joint_prim now returns the queried prim when it is such a joint, so fix_root_link=True enables it
in 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=False is not supported for an articulation rooted at its world joint: both writers now log a warning
and leave the joint enabled. Before, a root joint with an empty body0, such as the fix_base_joint that the
importers 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.rst documents this arrangement.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Release backport

  • Backport this pull request to the active release branch after it merges into develop

Verification

Isaac Sim 6.1:

  • test_schemas.py: 39 passed. On develop, 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 both
    warn and leave the joint enabled for fix_root_link=False.
  • PhysX and Newton test_articulation.py::test_initialization_fixed_joint_root pass on an asset in this layout that
    the test authors with the USD API. On develop, it fails with the same error on both backends.
  • pre-commit and tools/changelog/cli.py check pass.

Checklist

  • I have read and understood the contribution guidelines
  • I have run the pre-commit checks with uv run isaaclab --format
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have validated the changed behavior with focused coverage and applied the test-audit criteria to any test changes
  • I have added a changelog fragment under source/<pkg>/changelog.d/ for every touched package (do not edit CHANGELOG.rst or bump package versions — CI handles that)
  • I have added my name to the CONTRIBUTORS.md or my name already exists there

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.
@NeoZng
NeoZng requested a review from a team September 29, 2026 16:27
@github-actions github-actions Bot added bug Something isn't working documentation Improvements or additions to documentation isaac-lab Related to Isaac Lab team labels Sep 29, 2026
@greptile-apps

greptile-apps Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

[High risk] Moves articulation root position in physics simulation.

The PR should not merge until fix_root_link=True verifies that a fixed-joint articulation root is world-attached.

Findings

  1. P1 Internal joint mistaken for world joint ▶
  2. P2 Prepended root schemas are missed ▶

Summary

The PR moves fixed-base URDF and welded MJCF articulation roots from rigid bodies to their world fixed joints, then updates fix_root_link handling and adds converter and backend coverage.

  • The new joint-root shortcut needs to distinguish a world joint from an internal fixed joint.
  • Schema relocation should account for prepended USD API schemas.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Convert URDF or MJCF with fix_base] --> B[Find enabled fixed world joint]
  B --> C[Move articulation-root schemas and attributes]
  C --> D[Spawn articulation]
  D --> E{fix_root_link true?}
  E -- Yes --> F[Validate and enable world joint]
  E -- No --> G[Use converted root]
Loading

Reviews (1) · Last reviewed commit: "Fix floating PhysX articulations of conv..."

Comment thread source/isaaclab/isaaclab/physics/physics_manager.py Outdated
Comment thread source/isaaclab/isaaclab/sim/converters/asset_converter_base.py Outdated

@isaaclab-review-bot isaaclab-review-bot 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.

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, and ArticulationRootBaseCfg.fix_root_link retain their names, types, and defaults. The manager and schema-writer changes extend fix_root_link=True to 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=True with 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 -> None to _root_articulations_at_world_joints and _move_articulation_root as required for functions that return no value.
  • Style consistency: The new private helpers follow the file’s structural ordering and keep pxr imports deferred. The two no-value helper signatures omit the contribution guide’s required -> None return 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 assert IsA(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.

Comment thread source/isaaclab/isaaclab/sim/converters/asset_converter_base.py Outdated
Comment thread source/isaaclab/test/sim/test_mjcf_converter.py Outdated
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.
@AntoineRichard

Copy link
Copy Markdown
Collaborator

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?

@NeoZng

NeoZng commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

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 source/libraries, outside the external contribution paths, so it cannot be a PR). I'll drop the converter correction from this PR and keep only the Isaac Lab part. fix_root_link=True does not recognize an articulation rooted at its fixed world joint when the joint's body0 targets the asset prim, which is how the fixed importer will root these assets, and raises NotImplementedError: Cannot fix non-rigid articulation root. The change enables that existing joint instead. It does not affect assets converted today, which still keep the root on the link.

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.
@NeoZng NeoZng changed the title Fix floating PhysX articulations of URDF and welded MJCF assets converted with fix_base Enable fix_root_link for articulations rooted at their fixed world joint Sep 30, 2026
@NeoZng

NeoZng commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

Done in 14020d9: the converter correction is dropped, and the PR now only contains the fix_root_link handling, tested with a hand-authored USD in this layout. I updated the title and the description.

@AntoineRichard AntoineRichard left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the revision! Left a couple of comments

Comment on lines +406 to +427
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


Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if this should be here. I think this is more of a utility.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +189 to +209
# 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"]
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +60 to +70
# 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"
{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 762c3fe, with the same helper and the same reduction.

NeoZng added 3 commits October 1, 2026 18:38
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.
@NeoZng

NeoZng commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

@AntoineRichard Thanks for the review. The updates for your comments:

  • _is_world_fixed_joint moved to isaaclab.sim.utils.queries, next to find_global_fixed_joint_prim (762c3fe), and now reuses get_first_matching_ancestor_prim for the ancestor walk (20b68e1). I kept it private, since only the legacy writer and PhysicsManager.fix_articulation_root use it; I can make it public if you prefer.
  • The PhysX and Newton tests author their asset in Python with _author_fixed_joint_root_robot, and only check fix_root_link=True on it (762c3fe).
  • The changelog fragment uses the towncrier format from [Changelog] Compile changelog fragments with towncrier and keep uv.lock in sync #8118, and I merged develop (b27d9fc).

@NeoZng

NeoZng commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

@AntoineRichard I merged develop to resolve the conflicts with #7282 (58c8514). test_initialization_fixed_joint_root now sits with the tests that own their simulation in both files: the PhysX test uses the sim fixture, and the Newton test builds its scene with spawn_assets on the CPU, like the other own-scene tests. I added it to the list in the Newton module docstring. Both test files pass in full, and on develop the new tests still fail with the NotImplementedError. The rest of the PR is unchanged.

@AntoineRichard AntoineRichard left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@NeoZng NeoZng Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@NeoZng NeoZng Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@NeoZng NeoZng Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@NeoZng NeoZng Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@NeoZng

NeoZng commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

@AntoineRichard Thanks for the review. All four points are addressed in 9b9c96f (replies inline); test_schemas.py and the PhysX/Newton fixed-joint-root tests pass.

Comment on lines +134 to +137
if _is_world_fixed_joint(articulation_prim):
joint = UsdPhysics.Joint(articulation_prim)
else:
joint = find_global_fixed_joint_prim(root_path, stage=stage)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@hujc7

hujc7 commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

run-ci

@isaaclab-bot isaaclab-bot Bot added ci:run-docker Trigger the on-demand Docker and GPU CI workflow and removed ci:run-docker Trigger the on-demand Docker and GPU CI workflow labels Oct 15, 2026
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.
@NeoZng

NeoZng commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

@hujc7 Thanks! Pushed f334b27 to update how-to item 5 to the new fix_root_link=False behavior, and updated the PR description to match.

@hujc7

hujc7 commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

run-ci

@isaaclab-bot isaaclab-bot Bot added ci:run-docker Trigger the on-demand Docker and GPU CI workflow and removed ci:run-docker Trigger the on-demand Docker and GPU CI workflow labels Oct 16, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation isaac-lab Related to Isaac Lab team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants