Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1e04efb
feat(schemas): add articulation-root schema-fragment API
vidurv-nvidia Jun 5, 2026
064ba8e
docs(schemas): mark transition shim if/else for removal post-migration
vidurv-nvidia Jun 5, 2026
af0c392
fix(schemas): resolve articulation root before applying fragments
vidurv-nvidia Jun 9, 2026
e80c7aa
refactor(schemas): reuse get_first_matching_child_prim for root lookup
vidurv-nvidia Jun 9, 2026
3fa37b8
style(schemas): trim verbose comments in articulation root writer
vidurv-nvidia Jun 9, 2026
6892d5d
test(schemas): cover the fix_root_link create-new-joint path
vidurv-nvidia Jun 9, 2026
5dbbc2b
Merge remote-tracking branch 'origin/develop' into vidurv/schema-frag…
vidurv-nvidia Jun 26, 2026
12852bc
Move PhysX fixed-root-joint creation out of core into the PhysX backend
vidurv-nvidia Jun 26, 2026
f1892ef
Add core-level tests for the fixed-root-joint creator registry
vidurv-nvidia Jun 26, 2026
45165ab
Document RuntimeError in apply_articulation_root_properties
vidurv-nvidia Jun 26, 2026
6a0775e
Make fixed-root-joint creation backend-aware so Newton runs on its own
vidurv-nvidia Jun 26, 2026
44d5e35
Keep backend fixed-root-joint hook import free of USD libraries
vidurv-nvidia Jun 27, 2026
4b381e6
Merge remote-tracking branch 'origin/develop' into vidurv/schema-frag…
vidurv-nvidia Jun 27, 2026
5137c59
Select fixed-root-joint creator by active physics-cfg type
vidurv-nvidia Jun 27, 2026
7989544
Author fixed root joint with backend-neutral USD helper
vidurv-nvidia Jun 27, 2026
aae4918
Document why backend creators defer their imports
vidurv-nvidia Jun 27, 2026
62f98e0
Merge remote-tracking branch 'origin/develop' into vidurv/schema-frag…
vidurv-nvidia Jun 28, 2026
c15dde1
Merge develop to pick up docker install fixes (#6262)
vidurv-nvidia Jun 29, 2026
b0dcdd8
Resolve articulation-root fixing via physics manager
vidurv-nvidia Jul 2, 2026
4dc87bb
Move create_fixed_root_joint to sim.utils
vidurv-nvidia Jul 2, 2026
3b9cf1c
Migrate pre-authored root schemas on PhysX relocation
vidurv-nvidia Jul 2, 2026
30f06c4
Merge remote-tracking branch 'upstream/develop' into ooctipus/pr-6272…
ooctipus Jul 4, 2026
d55e0ed
Fix articulation-root fragment integration boundaries
ooctipus Jul 4, 2026
9b2cb9e
Simplify articulation root handling
ooctipus Jul 4, 2026
0d4b413
Test Newton inherited fixed-root handling
ooctipus Jul 4, 2026
7774ba3
Avoid local imports in articulation root handling
ooctipus Jul 4, 2026
280c20a
Keep cyclic fixed-joint query import local
ooctipus Jul 4, 2026
3ec6dae
Merge remote-tracking branch 'origin/develop' into vidurv/schema-frag…
vidurv-nvidia Jul 6, 2026
f6fd754
Defer pxr imports in the base physics manager
vidurv-nvidia Jul 6, 2026
10ecaf9
Merge remote-tracking branch 'origin/develop' into vidurv/schema-frag…
vidurv-nvidia Jul 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Added
^^^^^

* Added the articulation-root schema-fragment API:
:class:`~isaaclab.sim.schemas.ArticulationRootFragment` (marker) and
:func:`~isaaclab.sim.schemas.apply_articulation_root_properties`, which applies a list of
articulation-root fragments with ``UsdPhysics.ArticulationRootAPI`` as a presence-gated anchor
and reproduces the legacy ``fix_root_link`` fixed-joint logic via a spawner-level flag.
* Added the :meth:`~isaaclab.physics.PhysicsManager.fix_articulation_root` capability, which fixes an
articulation base to the world frame and returns the resulting root prim. The base implementation
authors a backend-neutral fixed joint; backends whose parser relocates the articulation root (e.g.
PhysX) override it, so :func:`~isaaclab.sim.schemas.apply_articulation_root_properties` applies every
fragment to the single resulting root regardless of backend.

Changed
^^^^^^^

* Changed the spawner ``articulation_props`` slot
(:attr:`~isaaclab.sim.spawners.UsdFileCfg.articulation_props`) to also accept a list of
:class:`~isaaclab.sim.schemas.ArticulationRootFragment` fragments, and added the spawner-level
:attr:`~isaaclab.sim.spawners.UsdFileCfg.fix_root_link` flag. Legacy single cfgs continue to
work through a transition bridge in the spawn writer.
105 changes: 105 additions & 0 deletions source/isaaclab/isaaclab/physics/physics_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from enum import Enum
from typing import TYPE_CHECKING, Any, ClassVar

from isaaclab.sim.utils.stage import get_current_stage
from isaaclab.utils._device import set_cuda_device

if TYPE_CHECKING:
Expand Down Expand Up @@ -101,6 +102,110 @@ def provides_implicit_damping(cls) -> bool:
"""
return True

@classmethod
def fix_articulation_root(cls, articulation_prim: Any, stage: Any = None) -> Any:
"""Ensure that an articulation root has one enabled world fixed joint.

The base implementation leaves the root in place. Backends whose parser requires a different
root topology may relocate it and return the resulting root prim.

Args:
articulation_prim: The articulation-root prim to fix.
stage: The stage containing the prim. Defaults to the current stage.

Returns:
The articulation-root prim after backend normalization.

Raises:
NotImplementedError: If a new joint is needed and the root is not a rigid body.
"""
# Keep this import local to avoid the SimulationContext -> PhysicsManager ->
# sim.utils.queries -> SimulationContext import cycle.
# Keep pxr local as well: this module is imported while environment configs load (via the
# manager classes), and config loading must not pull USD/omni modules before the simulation
# app starts.
from pxr import Gf, UsdGeom, UsdPhysics # noqa: PLC0415

from isaaclab.sim.utils import find_global_fixed_joint_prim # noqa: PLC0415

if stage is None:
stage = get_current_stage()
root_path = articulation_prim.GetPath().pathString
joint = find_global_fixed_joint_prim(root_path, stage=stage)
if joint is not None:
joint.GetJointEnabledAttr().Set(True)
return articulation_prim
if not articulation_prim.HasAPI(UsdPhysics.RigidBodyAPI):
raise NotImplementedError(f"Cannot fix non-rigid articulation root '{root_path}'.")

joint_path = f"{root_path}/FixedJoint"
index = 0
while stage.GetPrimAtPath(joint_path).IsValid():
index += 1
joint_path = f"{root_path}/FixedJoint{index}"

world_xform = UsdGeom.XformCache().GetLocalToWorldTransform(articulation_prim).RemoveScaleShear()
joint = UsdPhysics.FixedJoint.Define(stage, joint_path)
joint.CreateBody1Rel().SetTargets([articulation_prim.GetPath()])
joint.CreateLocalPos0Attr().Set(Gf.Vec3f(world_xform.ExtractTranslation()))
joint.CreateLocalRot0Attr().Set(Gf.Quatf(world_xform.ExtractRotationQuat()))
return articulation_prim

@staticmethod
def _relocate_articulation_root(
articulation_prim: Any,
companion_schema: str,
companion_namespace: str,
) -> Any:
"""Move root-bearing schemas and authored properties to the root link's parent."""
# Keep pxr local: this module is imported while environment configs load (via the manager
# classes), and config loading must not pull USD/omni modules before the simulation app
# starts.
from pxr import Usd, UsdPhysics # noqa: PLC0415

new_root = articulation_prim.GetParent()
if new_root.HasAPI(UsdPhysics.ArticulationRootAPI):
raise RuntimeError(
f"Cannot relocate '{articulation_prim.GetPath()}' to existing articulation root '{new_root.GetPath()}'."
)

registry = Usd.SchemaRegistry()
root_schema = UsdPhysics.Tokens.PhysicsArticulationRootAPI
schemas_to_move = []
for schema_name in articulation_prim.GetPrimTypeInfo().GetAppliedAPISchemas():
definition = registry.FindAppliedAPIPrimDefinition(schema_name)
if schema_name == companion_schema:
properties = list(articulation_prim.GetAuthoredPropertiesInNamespace(companion_namespace))
elif schema_name == root_schema or (
definition is not None and root_schema in definition.GetAppliedAPISchemas()
):
properties = []
if definition is not None:
for property_name in definition.GetPropertyNames():
prop = articulation_prim.GetProperty(property_name)
if prop and prop.IsAuthored():
properties.append(prop)
else:
continue
schemas_to_move.append((schema_name, properties))

for schema_name, properties in schemas_to_move:
if not new_root.AddAppliedSchema(schema_name):
raise RuntimeError(f"Failed to apply '{schema_name}' to '{new_root.GetPath()}'.")
for prop in properties:
if not prop.FlattenTo(new_root):
raise RuntimeError(f"Failed to move '{prop.GetPath()}' to '{new_root.GetPath()}'.")
for schema_name, _ in schemas_to_move:
if not articulation_prim.RemoveAppliedSchema(schema_name):
raise RuntimeError(f"Failed to remove '{schema_name}' from '{articulation_prim.GetPath()}'.")
if articulation_prim.HasAPI(UsdPhysics.ArticulationRootAPI) or not new_root.HasAPI(
UsdPhysics.ArticulationRootAPI
):
raise RuntimeError(
f"Failed to relocate articulation root '{articulation_prim.GetPath()}' to '{new_root.GetPath()}'."
)
return new_root

@classmethod
def register_callback(
cls,
Expand Down
4 changes: 4 additions & 0 deletions source/isaaclab/isaaclab/sim/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,15 @@ __all__ = [
"MeshCollisionFragment",
"PhysxJointDrivePropertiesCfg",
"PhysxRigidBodyPropertiesCfg",
"ArticulationRootFragment",
"RigidBodyBaseCfg",
"RigidBodyFragment",
"SchemaFragment",
"SpatialTendonFragment",
"UsdPhysicsCollisionCfg",
"UsdPhysicsMeshCollisionCfg",
"UsdPhysicsRigidBodyCfg",
"apply_articulation_root_properties",
"apply_collision_properties",
"apply_fixed_tendon_properties",
"apply_mass_properties",
Expand Down Expand Up @@ -228,6 +230,7 @@ from .schemas import (
MESH_APPROXIMATION_TOKENS,
PHYSX_MESH_COLLISION_CFGS,
USD_MESH_COLLISION_CFGS,
ArticulationRootFragment,
ArticulationRootPropertiesCfg,
BoundingCubePropertiesCfg,
BoundingSpherePropertiesCfg,
Expand Down Expand Up @@ -260,6 +263,7 @@ from .schemas import (
UsdPhysicsMeshCollisionCfg,
UsdPhysicsRigidBodyCfg,
activate_contact_sensors,
apply_articulation_root_properties,
apply_collision_properties,
apply_fixed_tendon_properties,
apply_mass_properties,
Expand Down
4 changes: 4 additions & 0 deletions source/isaaclab/isaaclab/sim/schemas/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ __all__ = [
"PHYSX_MESH_COLLISION_CFGS",
"USD_MESH_COLLISION_CFGS",
"activate_contact_sensors",
"apply_articulation_root_properties",
"apply_collision_properties",
"apply_fixed_tendon_properties",
"apply_mass_properties",
Expand Down Expand Up @@ -48,6 +49,7 @@ __all__ = [
"JointDriveFragment",
"MassPropertiesCfg",
"MeshCollisionBaseCfg",
"ArticulationRootFragment",
"MeshCollisionFragment",
"RigidBodyFragment",
"SchemaFragment",
Expand All @@ -73,6 +75,7 @@ from .schemas import (
PHYSX_MESH_COLLISION_CFGS,
USD_MESH_COLLISION_CFGS,
activate_contact_sensors,
apply_articulation_root_properties,
apply_collision_properties,
apply_fixed_tendon_properties,
apply_mass_properties,
Expand Down Expand Up @@ -104,6 +107,7 @@ from .schemas_actuators import (
)
from .schemas_cfg import (
ArticulationRootBaseCfg,
ArticulationRootFragment,
BoundingCubePropertiesCfg,
BoundingSpherePropertiesCfg,
CollisionBaseCfg,
Expand Down
104 changes: 104 additions & 0 deletions source/isaaclab/isaaclab/sim/schemas/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,110 @@ def apply_namespaced(cfg: schemas_cfg.SchemaFragment, prim_path: str, stage: Usd
"""


def apply_articulation_root_properties(
prim_path: str,
fragments: Iterable[schemas_cfg.ArticulationRootFragment],
stage: Usd.Stage | None = None,
fix_root_link: bool | None = None,
) -> bool:
"""Apply fragments to every top-level articulation root under a prim.

Existing roots are discovered before a fresh anchor is applied, including roots hidden in
instances. Nested roots are pruned, while sibling roots are all processed. Instance roots are
reported but not authored.

When fix_root_link is True, the active physics manager creates or enables the world joint and
returns the backend's final root prim. False only disables an existing joint.

Args:
prim_path: The prim path whose subtree is searched for articulation roots.
fragments: Articulation-root fragments to apply.
stage: The stage containing the prim. Defaults to the current stage.
fix_root_link: Whether to fix the root link. None leaves topology unchanged.

Returns:
True if every writable root and fragment succeeds and no instance root is skipped.

Raises:
TypeError: If fragments contains a non-articulation fragment.
ValueError: If prim_path is invalid.
RuntimeError: If fixing cannot resolve the active backend or relocate the root.
NotImplementedError: If the backend cannot fix the resolved root.
"""
Comment on lines +299 to +307

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.

P2 The Raises section is incomplete. When fix_root_link=True and no creator has been registered (i.e. isaaclab_physx was never imported), the function raises RuntimeError, not one of the two documented exceptions. A caller who catches only ValueError / NotImplementedError will get an unexpected uncaught exception.

Suggested change
Returns:
True if the properties were successfully set.
Raises:
ValueError: When the prim path is not valid.
NotImplementedError: When the root prim is not a rigid body and a fixed joint is to be created.
"""
Returns:
True if the properties were successfully set.
Raises:
ValueError: When the prim path is not valid.
NotImplementedError: When the root prim is not a rigid body and a fixed joint is to be created.
RuntimeError: When ``fix_root_link=True`` and no fixed-root-joint creator has been registered
(i.e. no physics-backend extension such as ``isaaclab_physx`` has been imported).
"""

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.

Addressed in b0dcdd8. The per-backend "no creator registered" case is gone with the registry; the remaining RuntimeError is raised only when a fixed joint must be created but there is no active SimulationContext to resolve the backend from, and the Raises section now documents exactly that plus the propagated NotImplementedError.

fragments = list(fragments)
for fragment in fragments:
if not isinstance(fragment, schemas_cfg.ArticulationRootFragment):
raise TypeError(
f"Expected ArticulationRootFragment, got '{type(fragment).__name__}'."
" Pass legacy cfgs to modify_articulation_root_properties."
)
dispatchers = [
fragment.func if callable(fragment.func) else string_to_callable(fragment.func) for fragment in fragments
]
if stage is None:
stage = get_current_stage()

roots = []
for candidate in get_all_matching_child_prims(
prim_path,
lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI),
stage=stage,
traverse_instance_prims=True,
):
if not any(candidate.GetPath().HasPrefix(root.GetPath()) for root in roots):
roots.append(candidate)

writable_roots = []
skipped_roots = []
for root in roots:
if root.IsInstance() or root.IsInstanceProxy():
skipped_roots.append(root)
else:
writable_roots.append(root)

if not roots and fragments:
root = stage.GetPrimAtPath(prim_path)
if root.IsInstance() or root.IsInstanceProxy():
skipped_roots.append(root)
else:
UsdPhysics.ArticulationRootAPI.Apply(root)
writable_roots.append(root)

if skipped_roots:
logger.warning(
"Skipping articulation-root updates on instanced prims: %s.",
[root.GetPath().pathString for root in skipped_roots],
)
if not writable_roots:
if fix_root_link is not None and not skipped_roots:
logger.warning(
"No articulation root found under '%s': ignoring fix_root_link=%s.", prim_path, fix_root_link
)
return not skipped_roots and fix_root_link is None

if fix_root_link:
from isaaclab.sim import SimulationContext

sim = SimulationContext.instance()
if sim is None:
raise RuntimeError(f"Cannot fix articulation roots under '{prim_path}' without an active simulation.")

success = not skipped_roots
for root in writable_roots:
if fix_root_link:
root = sim.physics_manager.fix_articulation_root(root, stage)
elif fix_root_link is False:
joint = find_global_fixed_joint_prim(root.GetPath().pathString, stage=stage)
if joint is not None:
joint.GetJointEnabledAttr().Set(False)

root_path = root.GetPath().pathString
for fragment, func in zip(fragments, dispatchers):
success = bool(func(fragment, root_path, stage)) and success

return success


def define_articulation_root_properties(
prim_path: str, cfg: schemas_cfg.ArticulationRootBaseCfg, stage: Usd.Stage | None = None
):
Expand Down
15 changes: 15 additions & 0 deletions source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,21 @@ class CollisionFragment(SchemaFragment):
pass


@configclass
class ArticulationRootFragment(SchemaFragment):
"""Marker base for articulation-root fragments; types the ``articulation_props`` slot.

Articulation-root fragments author backend-specific articulation properties (solver
iterations, sleep / stabilization thresholds, self-collision toggles). The defining
``UsdPhysics.ArticulationRootAPI`` anchor is applied by the articulation-root family
writer (:func:`~isaaclab.sim.schemas.apply_articulation_root_properties`) only when the
``articulation_props`` slot carries fragments (presence-gated, matching the legacy
:func:`~isaaclab.sim.schemas.modify_articulation_root_properties` behaviour).
"""

pass


@configclass
class JointDriveFragment(SchemaFragment):
"""Marker base for joint-drive fragments; types the ``joint_drive_props`` slot."""
Expand Down
32 changes: 30 additions & 2 deletions source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,36 @@ def _spawn_from_usd_file(
schemas.modify_mass_properties(prim_path, cfg.mass_props)

# modify articulation root properties
if cfg.articulation_props is not None:
schemas.modify_articulation_root_properties(prim_path, cfg.articulation_props)
# ``fix_root_link`` is a spawner-level topology flag (not a schema property); it is honored on the
# fragment path independently of whether any articulation schema properties were supplied.
articulation_props = cfg.articulation_props
articulation_fix_root_link = cfg.fix_root_link
# transition shim, remove later: route a legacy single cfg (a dataclass, not a fragment) to the
# legacy writer -- it owns its own ``fix_root_link`` field; everything else goes to the fragment
# writer, routing by type so an empty list is still a valid (topology-only) fragment collection
# rather than being mis-sent to the legacy writer.
if (
articulation_props is not None
and not isinstance(articulation_props, (list, tuple))
and not isinstance(articulation_props, schemas.SchemaFragment)
):
if articulation_fix_root_link is not None:
logger.warning(
f"Ignoring the spawner-level 'fix_root_link={articulation_fix_root_link}' because"
" 'articulation_props' is a legacy cfg, which owns its own 'fix_root_link' field. Set"
" it on that cfg instead."
)
schemas.modify_articulation_root_properties(prim_path, articulation_props)

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.

Blocking for the advertised legacy transition: NewtonArticulationRootPropertiesCfg(articulation_enabled=False, self_collision_enabled=True) routed here raises ValueError instead of authoring both fields. The root cause is NewtonArticulationRootPropertiesCfg._usd_field_exceptions = {}, which shadows the inherited mapping required for articulation_enabled.

This breaks the base-field-through-backend-subclass invariant established in the same author's merged #5275. Please inherit/merge the base exception mapping and add a regression comparing this legacy cfg with the equivalent PhysX + Newton fragment list.

else:
articulation_frags = (
list(articulation_props)
if isinstance(articulation_props, (list, tuple))
else ([articulation_props] if isinstance(articulation_props, schemas.SchemaFragment) else [])
)
if articulation_frags or articulation_fix_root_link is not None:
schemas.apply_articulation_root_properties(
prim_path, articulation_frags, fix_root_link=articulation_fix_root_link
)
# modify tendon properties
if cfg.fixed_tendons_props is not None:
# transition shim, remove later: fragment(s) -> apply_*; legacy cfg -> modify_*
Expand Down
Loading
Loading