Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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,17 @@
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.

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.
4 changes: 4 additions & 0 deletions source/isaaclab/isaaclab/sim/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@ __all__ = [
"NewtonSDFCollisionPropertiesCfg",
"PhysxJointDrivePropertiesCfg",
"PhysxRigidBodyPropertiesCfg",
"ArticulationRootFragment",
"RigidBodyBaseCfg",
"RigidBodyFragment",
"SchemaFragment",
"UsdPhysicsRigidBodyCfg",
"apply_articulation_root_properties",
"apply_namespaced",
"apply_rigid_body_properties",
"SDFMeshPropertiesCfg",
Expand Down Expand Up @@ -210,6 +212,7 @@ from .schemas import (
MESH_APPROXIMATION_TOKENS,
PHYSX_MESH_COLLISION_CFGS,
USD_MESH_COLLISION_CFGS,
ArticulationRootFragment,
ArticulationRootPropertiesCfg,
BoundingCubePropertiesCfg,
BoundingSpherePropertiesCfg,
Expand All @@ -233,6 +236,7 @@ from .schemas import (
TriangleMeshSimplificationPropertiesCfg,
UsdPhysicsRigidBodyCfg,
activate_contact_sensors,
apply_articulation_root_properties,
apply_namespaced,
apply_rigid_body_properties,
define_articulation_root_properties,
Expand Down
2 changes: 1 addition & 1 deletion source/isaaclab/isaaclab/sim/converters/mesh_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def _convert_asset(self, cfg: MeshConverterCfg):
# apply mass properties
if cfg.mass_props is not None:
schemas.define_mass_properties(prim_path=xform_prim.GetPath(), cfg=cfg.mass_props, stage=stage)
# apply rigid body properties (transition routing: fragment list -> apply_*; legacy cfg -> define_*)
# apply rigid body properties (transition shim, remove later: fragment list -> apply_*; legacy cfg -> define_*)
if cfg.rigid_props is not None:
rigid_frags = cfg.rigid_props if isinstance(cfg.rigid_props, (list, tuple)) else [cfg.rigid_props]
if rigid_frags and all(isinstance(f, schemas.SchemaFragment) for f in rigid_frags):
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_namespaced",
"apply_rigid_body_properties",
"define_actuator_properties",
Expand Down Expand Up @@ -35,6 +36,7 @@ __all__ = [
"JointDriveBaseCfg",
"MassPropertiesCfg",
"MeshCollisionBaseCfg",
"ArticulationRootFragment",
"RigidBodyFragment",
"SchemaFragment",
"UsdPhysicsRigidBodyCfg",
Expand All @@ -55,6 +57,7 @@ from .schemas import (
PHYSX_MESH_COLLISION_CFGS,
USD_MESH_COLLISION_CFGS,
activate_contact_sensors,
apply_articulation_root_properties,
apply_namespaced,
apply_rigid_body_properties,
define_articulation_root_properties,
Expand All @@ -78,6 +81,7 @@ from .schemas_actuators import (
)
from .schemas_cfg import (
ArticulationRootBaseCfg,
ArticulationRootFragment,
BoundingCubePropertiesCfg,
BoundingSpherePropertiesCfg,
CollisionBaseCfg,
Expand Down
129 changes: 129 additions & 0 deletions source/isaaclab/isaaclab/sim/schemas/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
create_prim,
find_global_fixed_joint_prim,
get_all_matching_child_prims,
get_first_matching_child_prim,
safe_set_attribute_on_usd_prim,
safe_set_attribute_on_usd_schema,
)
Expand Down Expand Up @@ -254,6 +255,134 @@ def apply_namespaced(cfg, prim_path: str, stage: Usd.Stage | None = None) -> boo
"""


def apply_articulation_root_properties(
prim_path: str,
fragments,
stage: Usd.Stage | None = None,
fix_root_link: bool | None = None,
) -> bool:
"""Apply a list of articulation-root fragments to a prim.

Resolves the articulation root before writing: USD assets author
``UsdPhysics.ArticulationRootAPI`` on a child prim (the root link / fixed joint), so this
writer descends the subtree under ``prim_path`` and tunes the existing root in place (matching
the legacy :func:`modify_articulation_root_properties` writer). Only when no prim in the
subtree carries the root does it apply ``UsdPhysics.ArticulationRootAPI`` on ``prim_path``
itself (define-fresh, e.g. for primitive or programmatic spawns). This guarantees exactly one
articulation root rather than stamping a duplicate on the input prim.

Each fragment is then dispatched to the resolved root via its
:attr:`~isaaclab.sim.schemas.SchemaFragment.func`. Backend fragments carry backend-specific
funcs, so core never imports a backend. Finally, if ``fix_root_link`` is not ``None``, the
world-to-root fixed-joint logic from the legacy :func:`modify_articulation_root_properties`
writer is applied to the resolved root.

Args:
prim_path: The prim path to search for the articulation root under (the root may be on a
descendant); the schemas are applied to the resolved root prim.
fragments: An iterable of :class:`~isaaclab.sim.schemas.ArticulationRootFragment` instances.
stage: The stage where to find the prim. Defaults to None, in which case the current
stage is used.
fix_root_link: Whether to fix the root link of the articulation. This is a non-USD,
spawner-level behaviour flag (it is not a fragment field). See
:attr:`~isaaclab.sim.spawners.UsdFileCfg.fix_root_link` for the semantics. Defaults
to None, in which case the root link is not modified.

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.
"""
if stage is None:
stage = get_current_stage()
# tune the existing root in place (it may live on a child prim); instance proxies can't be
# authored on, so don't traverse them
articulation_prim = get_first_matching_child_prim(
prim_path,
lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI),
stage,
traverse_instance_prims=False,
)
# no existing root in the subtree: define one on the input prim
if articulation_prim is None:
articulation_prim = stage.GetPrimAtPath(prim_path)
UsdPhysics.ArticulationRootAPI.Apply(articulation_prim)
root_path = articulation_prim.GetPath().pathString
# dispatch each fragment to the resolved root via its own applier
for cfg in fragments:
func = cfg.func if callable(cfg.func) else string_to_callable(cfg.func)
func(cfg, root_path, stage)

# fix root link based on input
# we do the fixed joint processing later to not interfere with setting other properties.
# this logic is reproduced from the legacy ``modify_articulation_root_properties`` writer.
if fix_root_link is not None:
# check if a global fixed joint exists under the resolved root prim
existing_fixed_joint_prim = find_global_fixed_joint_prim(root_path)

# if we found a fixed joint, enable/disable it based on the input
# otherwise, create a fixed joint between the world and the root link
if existing_fixed_joint_prim is not None:
logger.info(
f"Found an existing fixed joint for the articulation: '{root_path}'. Setting it to: {fix_root_link}."
)
existing_fixed_joint_prim.GetJointEnabledAttr().Set(fix_root_link)
elif fix_root_link:
logger.info(f"Creating a fixed joint for the articulation: '{root_path}'.")

# note: we have to assume that the root prim is a rigid body,
# i.e. we don't handle the case where the root prim is not a rigid body but has articulation api on it
# Currently, there is no obvious way to get first rigid body link identified by the PhysX parser
if not articulation_prim.HasAPI(UsdPhysics.RigidBodyAPI):
raise NotImplementedError(
f"The articulation prim '{root_path}' does not have the RigidBodyAPI applied."
" To create a fixed joint, we need to determine the first rigid body link in"
" the articulation tree. However, this is not implemented yet."
)

# create a fixed joint between the root link and the world frame
from omni.physx.scripts import utils as physx_utils

physx_utils.createJoint(stage=stage, joint_type="Fixed", from_prim=None, to_prim=articulation_prim)

# Having a fixed joint on a rigid body is not treated as "fixed base articulation".
# instead, it is treated as a part of the maximal coordinate tree.
# Moving the articulation root to the parent solves this issue. This is a limitation of the PhysX parser.
# get parent prim
parent_prim = articulation_prim.GetParent()
# apply api to parent
UsdPhysics.ArticulationRootAPI.Apply(parent_prim)
parent_applied = parent_prim.GetAppliedSchemas()
if "PhysxArticulationAPI" not in parent_applied:
parent_prim.AddAppliedSchema("PhysxArticulationAPI")

# copy the attributes
# -- usd attributes
usd_articulation_api = UsdPhysics.ArticulationRootAPI(articulation_prim)
for attr_name in usd_articulation_api.GetSchemaAttributeNames():
attr = articulation_prim.GetAttribute(attr_name)
parent_attr = parent_prim.GetAttribute(attr_name)
if not parent_attr:
parent_attr = parent_prim.CreateAttribute(attr_name, attr.GetTypeName())
parent_attr.Set(attr.Get())
# -- physx attributes (copy by name prefix)
for attr in articulation_prim.GetAttributes():
aname = attr.GetName()
if aname.startswith("physxArticulation:"):
parent_attr = parent_prim.GetAttribute(aname)
if not parent_attr:
parent_attr = parent_prim.CreateAttribute(aname, attr.GetTypeName())
parent_attr.Set(attr.Get())

# remove api from root
articulation_prim.RemoveAppliedSchema("PhysxArticulationAPI")
articulation_prim.RemoveAPI(UsdPhysics.ArticulationRootAPI)

return True


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 @@ -167,6 +167,21 @@ class UsdPhysicsRigidBodyCfg(RigidBodyFragment):
"""


@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
:meth:`~isaaclab.sim.schemas.modify_articulation_root_properties` behaviour).
"""

pass


@configclass
class ArticulationRootBaseCfg:
"""Solver-common properties to apply to the root of an articulation.
Expand Down
13 changes: 11 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 @@ -344,7 +344,7 @@ def _spawn_from_usd_file(

# modify rigid body properties
if cfg.rigid_props is not None:
# transition routing: new fragment list -> apply_*; legacy single cfg -> modify_*
# transition shim, remove later: new fragment list -> apply_*; legacy single cfg -> modify_*
rigid_frags = cfg.rigid_props if isinstance(cfg.rigid_props, (list, tuple)) else [cfg.rigid_props]
if rigid_frags and all(isinstance(f, schemas.SchemaFragment) for f in rigid_frags):
schemas.apply_rigid_body_properties(prim_path, rigid_frags)
Expand All @@ -359,7 +359,16 @@ def _spawn_from_usd_file(

# modify articulation root properties
if cfg.articulation_props is not None:
schemas.modify_articulation_root_properties(prim_path, cfg.articulation_props)
# transition shim, remove later: new fragment list -> apply_*; legacy single cfg -> modify_*
articulation_frags = (
cfg.articulation_props if isinstance(cfg.articulation_props, (list, tuple)) else [cfg.articulation_props]
)
if articulation_frags and all(isinstance(f, schemas.SchemaFragment) for f in articulation_frags):
schemas.apply_articulation_root_properties(
prim_path, articulation_frags, fix_root_link=getattr(cfg, "fix_root_link", None)
)
else:
schemas.modify_articulation_root_properties(prim_path, cfg.articulation_props)
# modify tendon properties
if cfg.fixed_tendons_props is not None:
schemas.modify_fixed_tendon_properties(prim_path, cfg.fixed_tendons_props)
Expand Down
32 changes: 30 additions & 2 deletions source/isaaclab/isaaclab/sim/spawners/from_files/from_files_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,36 @@ class FileCfg(RigidObjectSpawnerCfg, DeformableObjectSpawnerCfg):
scale: tuple[float, float, float] | None = None
"""Scale of the asset. Defaults to None, in which case the scale is not modified."""

articulation_props: schemas.ArticulationRootPropertiesCfg | None = None
"""Properties to apply to the articulation root."""
articulation_props: (
schemas.ArticulationRootBaseCfg
| schemas.ArticulationRootFragment
| list[schemas.ArticulationRootFragment]
| None
) = None
"""Properties to apply to the articulation root.

Accepts either a single legacy cfg (e.g. :class:`~isaaclab.sim.schemas.ArticulationRootBaseCfg`)
or a list of :class:`~isaaclab.sim.schemas.ArticulationRootFragment` fragments
(e.g. ``[PhysxArticulationCfg(...), NewtonArticulationCfg(...)]``). When a fragment list is
given, ``UsdPhysics.ArticulationRootAPI`` is applied as the anchor (presence-gated) and each
fragment writes its own namespace.
"""

fix_root_link: bool | None = None
"""Whether to fix the root link of the articulation. Defaults to None.

This is a non-USD, spawner-level behaviour flag consumed by
:func:`~isaaclab.sim.schemas.apply_articulation_root_properties` when
:attr:`articulation_props` is given as a fragment list:

* If set to None, the root link is not modified.
* If the articulation already has a fixed root link, this flag enables or disables the fixed joint.
* If the articulation does not have a fixed root link, this flag creates a fixed joint between the
world frame and the root link (named "FixedJoint" under the articulation prim).

When :attr:`articulation_props` is given as a legacy cfg, set
:attr:`~isaaclab.sim.schemas.ArticulationRootBaseCfg.fix_root_link` on that cfg instead.
"""

fixed_tendons_props: schemas.FixedTendonPropertiesCfg | None = None
"""Properties to apply to the fixed tendons (if any)."""
Expand Down
2 changes: 1 addition & 1 deletion source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ def _spawn_mesh_geom_from_mesh(
# apply mass properties
if cfg.mass_props is not None:
schemas.define_mass_properties(prim_path, cfg.mass_props, stage=stage)
# apply rigid properties (transition routing: fragment list -> apply_*; legacy cfg -> define_*)
# apply rigid properties (transition shim, remove later: fragment list -> apply_*; legacy cfg -> define_*)
rigid_frags = cfg.rigid_props if isinstance(cfg.rigid_props, (list, tuple)) else [cfg.rigid_props]
if rigid_frags and all(isinstance(f, schemas.SchemaFragment) for f in rigid_frags):
schemas.apply_rigid_body_properties(prim_path, rigid_frags, stage=stage)
Expand Down
2 changes: 1 addition & 1 deletion source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ def _spawn_geom_from_prim_type(
schemas.define_mass_properties(prim_path, cfg.mass_props, stage=stage)
# apply rigid body properties
if cfg.rigid_props is not None:
# transition routing: new fragment list -> apply_*; legacy single cfg -> define_*
# transition shim, remove later: new fragment list -> apply_*; legacy single cfg -> define_*
rigid_frags = cfg.rigid_props if isinstance(cfg.rigid_props, (list, tuple)) else [cfg.rigid_props]
if rigid_frags and all(isinstance(f, schemas.SchemaFragment) for f in rigid_frags):
schemas.apply_rigid_body_properties(prim_path, rigid_frags, stage=stage)
Expand Down
Loading
Loading