Skip to content
Closed
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
19 changes: 19 additions & 0 deletions source/isaaclab/changelog.d/vidurv-schema-fragments.minor.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Added
^^^^^

* Added the single-namespace schema-fragment API: :class:`~isaaclab.sim.schemas.SchemaFragment`,
the :class:`~isaaclab.sim.schemas.RigidBodyFragment` marker, and
:class:`~isaaclab.sim.schemas.UsdPhysicsRigidBodyCfg`. Each fragment carries
``_usd_namespace`` / ``_usd_applied_schema`` metadata and a ``func`` applier so a prim can
carry rigid-body properties from multiple USD namespaces at once.
* Added :func:`~isaaclab.sim.schemas.apply_namespaced` (generic fragment writer) and
:func:`~isaaclab.sim.schemas.apply_rigid_body_properties` (applies a list of rigid-body
fragments with ``UsdPhysics.RigidBodyAPI`` as the implicit anchor).

Changed
^^^^^^^

* Changed the spawner ``rigid_props`` slot
(:attr:`~isaaclab.sim.spawners.RigidObjectSpawnerCfg.rigid_props`) to also accept a list of
:class:`~isaaclab.sim.schemas.RigidBodyFragment` fragments. Legacy single cfgs continue to
work through a transition bridge in the spawn writers.
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,10 @@ def _apply_gravity_compensation(self) -> None:
rather than silently receive zeros. To use Pink IK on Newton, keep
``enable_gravity_compensation=False``.
"""
if not self._asset.cfg.spawn.rigid_props.disable_gravity:
_rigid_props = self._asset.cfg.spawn.rigid_props
_rigid_frags = _rigid_props if isinstance(_rigid_props, (list, tuple)) else [_rigid_props]
_disable_gravity = any(getattr(f, "disable_gravity", None) for f in _rigid_frags if f is not None)
if not _disable_gravity:
# ``gravity_compensation_forces`` shape is ``(N, num_joints + num_base_dofs)``.
# Shift actuated-joint ids by ``num_base_dofs`` to skip the leading floating-
# base columns (0 for fixed-base, 6 for floating-base).
Expand Down
10 changes: 10 additions & 0 deletions source/isaaclab/isaaclab/sim/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ __all__ = [
"PhysxJointDrivePropertiesCfg",
"PhysxRigidBodyPropertiesCfg",
"RigidBodyBaseCfg",
"RigidBodyFragment",
"SchemaFragment",
"UsdPhysicsRigidBodyCfg",
"apply_namespaced",
"apply_rigid_body_properties",
"SDFMeshPropertiesCfg",
"SpatialTendonPropertiesCfg",
"TriangleMeshPropertiesCfg",
Expand Down Expand Up @@ -208,6 +213,8 @@ from .schemas import (
PHYSX_MESH_COLLISION_CFGS,
USD_MESH_COLLISION_CFGS,
activate_contact_sensors,
apply_namespaced,
apply_rigid_body_properties,
define_articulation_root_properties,
define_collision_properties,
define_deformable_body_properties,
Expand Down Expand Up @@ -238,6 +245,9 @@ from .schemas import (
PhysxJointDrivePropertiesCfg,
PhysxRigidBodyPropertiesCfg,
RigidBodyBaseCfg,
RigidBodyFragment,
SchemaFragment,
UsdPhysicsRigidBodyCfg,
SDFMeshPropertiesCfg,
SpatialTendonPropertiesCfg,
TriangleMeshPropertiesCfg,
Expand Down
8 changes: 6 additions & 2 deletions source/isaaclab/isaaclab/sim/converters/mesh_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,13 @@ 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
# apply rigid body properties (transition routing: fragment list -> apply_*; legacy cfg -> define_*)
if cfg.rigid_props is not None:
schemas.define_rigid_body_properties(prim_path=xform_prim.GetPath(), cfg=cfg.rigid_props, stage=stage)
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(str(xform_prim.GetPath()), rigid_frags, stage=stage)
else:
schemas.define_rigid_body_properties(prim_path=xform_prim.GetPath(), cfg=cfg.rigid_props, stage=stage)

# Save changes to USD stage
stage.Save()
Expand Down
10 changes: 10 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,8 @@ __all__ = [
"PHYSX_MESH_COLLISION_CFGS",
"USD_MESH_COLLISION_CFGS",
"activate_contact_sensors",
"apply_namespaced",
"apply_rigid_body_properties",
"define_actuator_properties",
"define_articulation_root_properties",
"define_collision_properties",
Expand All @@ -33,6 +35,9 @@ __all__ = [
"JointDriveBaseCfg",
"MassPropertiesCfg",
"MeshCollisionBaseCfg",
"RigidBodyFragment",
"SchemaFragment",
"UsdPhysicsRigidBodyCfg",
"MujocoJointDrivePropertiesCfg",
"MujocoRigidBodyPropertiesCfg",
"NewtonArticulationRootPropertiesCfg",
Expand All @@ -50,6 +55,8 @@ from .schemas import (
PHYSX_MESH_COLLISION_CFGS,
USD_MESH_COLLISION_CFGS,
activate_contact_sensors,
apply_namespaced,
apply_rigid_body_properties,
define_articulation_root_properties,
define_collision_properties,
define_deformable_body_properties,
Expand Down Expand Up @@ -80,6 +87,9 @@ from .schemas_cfg import (
MassPropertiesCfg,
MeshCollisionBaseCfg,
RigidBodyBaseCfg,
RigidBodyFragment,
SchemaFragment,
UsdPhysicsRigidBodyCfg,
)

# Forwarded to isaaclab_newton.sim.schemas via __getattr__ shim
Expand Down
64 changes: 63 additions & 1 deletion source/isaaclab/isaaclab/sim/schemas/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from pxr import Sdf, Usd, UsdGeom, UsdPhysics

from isaaclab.sim.utils.stage import get_current_stage
from isaaclab.utils.string import to_camel_case
from isaaclab.utils.string import string_to_callable, to_camel_case

from ..utils import (
apply_nested,
Expand Down Expand Up @@ -214,6 +214,41 @@ class that declares it (walking the MRO). Each group writes under that class's
safe_set_attribute_on_usd_prim(prim, f"{namespace}:{usd_attr}", value, camel_case=False)


def apply_namespaced(cfg, prim_path: str, stage: Usd.Stage | None = None) -> bool:
"""Default fragment applier: apply the fragment's schema and write its namespaced attrs.

Reads :attr:`~isaaclab.sim.schemas.SchemaFragment._usd_namespace` /
:attr:`~isaaclab.sim.schemas.SchemaFragment._usd_applied_schema` from the cfg's class. If the
fragment owns an applied schema, it is applied (once). Each non-``None`` dataclass field is
written as ``<namespace>:<camelCase(field)>``; the ``func`` field is skipped. ``None`` fields
are left unchanged on the prim (partial update).

Args:
cfg: The fragment instance carrying ``_usd_namespace`` / ``_usd_applied_schema`` metadata.
prim_path: The prim path to author on.
stage: The stage where to find the prim. Defaults to None, in which case the current
stage is used.

Returns:
True if the properties were successfully set.
"""
if stage is None:
stage = get_current_stage()
prim = stage.GetPrimAtPath(prim_path)
namespace = type(cfg)._usd_namespace
applied = type(cfg)._usd_applied_schema
if applied and applied not in prim.GetAppliedSchemas():
prim.AddAppliedSchema(applied)
for f in dataclasses.fields(cfg):
if f.name == "func":
continue
value = getattr(cfg, f.name)
if value is None:
continue
safe_set_attribute_on_usd_prim(prim, f"{namespace}:{to_camel_case(f.name, 'cC')}", value, camel_case=False)
return True


"""
Articulation root properties.
"""
Expand Down Expand Up @@ -386,6 +421,33 @@ def modify_articulation_root_properties(
"""


def apply_rigid_body_properties(prim_path: str, fragments, stage: Usd.Stage | None = None) -> bool:
"""Apply a list of rigid-body fragments to a prim.

Applies ``UsdPhysics.RigidBodyAPI`` as the implicit anchor (the defining schema for a rigid
body), then dispatches each fragment via its :attr:`~isaaclab.sim.schemas.SchemaFragment.func`.
Backend fragments carry backend-specific funcs, so core never imports a backend.

Args:
prim_path: The prim path to apply the rigid-body schemas on.
fragments: An iterable of :class:`~isaaclab.sim.schemas.RigidBodyFragment` instances.
stage: The stage where to find the prim. Defaults to None, in which case the current
stage is used.

Returns:
True if the properties were successfully set.
"""
if stage is None:
stage = get_current_stage()
prim = stage.GetPrimAtPath(prim_path)
if not UsdPhysics.RigidBodyAPI(prim):
UsdPhysics.RigidBodyAPI.Apply(prim)
for cfg in fragments:
func = cfg.func if callable(cfg.func) else string_to_callable(cfg.func)
func(cfg, prim_path, stage)
return True


def define_rigid_body_properties(prim_path: str, cfg: schemas_cfg.RigidBodyBaseCfg, stage: Usd.Stage | None = None):
"""Apply the rigid body schema on the input prim and set its properties.

Expand Down
61 changes: 61 additions & 0 deletions source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from __future__ import annotations

import warnings
from collections.abc import Callable
from typing import ClassVar, Literal

from isaaclab.utils.configclass import configclass
Expand Down Expand Up @@ -106,6 +107,66 @@ def _deprecate_field_alias(cfg, alias: str, canonical: str) -> None:
setattr(cfg, alias, None)


@configclass
class SchemaFragment:
"""Base for a single-namespace USD-schema config fragment.

Each subclass mirrors exactly one USD applied schema. The fragment carries class-level
metadata describing which USD namespace its fields write to (:attr:`_usd_namespace`) and
which applied schema, if any, it owns (:attr:`_usd_applied_schema`). The :attr:`func`
field names the callable that applies the fragment to a prim; the default generic applier
(:func:`~isaaclab.sim.schemas.apply_namespaced`) reads the metadata and writes each
non-``None`` field as ``<namespace>:<camelCase(field)>``. Irregular APIs override
:attr:`func` with a custom applier.

.. note::
A fragment present in a spawner slot means its schema is applied. ``None`` fields are
left unchanged on the prim (partial update).
"""

# -- Class metadata (not dataclass fields) --
_usd_namespace: ClassVar[str | None] = None
_usd_applied_schema: ClassVar[str | None] = None

func: Callable | str = "isaaclab.sim.schemas:apply_namespaced"
"""Callable (or its ``module:attr`` import string) that applies this fragment to a prim.

Resolved via :func:`~isaaclab.utils.string.string_to_callable` when a string. The callable
signature is ``func(cfg, prim_path, stage)``.
"""


@configclass
class RigidBodyFragment(SchemaFragment):
"""Marker base for rigid-body fragments; types the ``rigid_props`` slot."""

pass


@configclass
class UsdPhysicsRigidBodyCfg(RigidBodyFragment):
"""``physics:*`` rigid-body attributes from `UsdPhysics.RigidBodyAPI`_.

The ``UsdPhysics.RigidBodyAPI`` schema is applied as the implicit anchor by the rigid-body
family writer, so this fragment owns no applied schema of its own.

.. _UsdPhysics.RigidBodyAPI: https://openusd.org/dev/api/class_usd_physics_rigid_body_a_p_i.html
"""

_usd_namespace: ClassVar[str | None] = "physics"
_usd_applied_schema: ClassVar[str | None] = None # RigidBodyAPI applied by the family anchor

rigid_body_enabled: bool | None = None
"""Whether to enable or disable the rigid body."""

kinematic_enabled: bool | None = None
"""Determines whether the body is kinematic or not.

A kinematic body is moved through animated or user-defined poses; the simulation still
derives velocities for it based on the external motion.
"""


@configclass
class ArticulationRootBaseCfg:
"""Solver-common properties to apply to the root of an articulation.
Expand Down
21 changes: 17 additions & 4 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,12 @@ def _spawn_from_usd_file(

# modify rigid body properties
if cfg.rigid_props is not None:
schemas.modify_rigid_body_properties(prim_path, cfg.rigid_props)
# transition routing: 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)
else:
schemas.modify_rigid_body_properties(prim_path, cfg.rigid_props)
# modify collision properties
if cfg.collision_props is not None:
schemas.modify_collision_properties(prim_path, cfg.collision_props)
Expand All @@ -368,10 +373,18 @@ def _spawn_from_usd_file(
# without it — actuatorgravcomp has no effect since there are no forces to route.
# Only auto-populates when the user did not already set ``gravcomp`` themselves;
# an explicit ``MujocoRigidBodyPropertiesCfg(gravcomp=0.5)`` is preserved as-is.
from isaaclab_newton.sim.schemas.schemas_cfg import MujocoJointDrivePropertiesCfg, MujocoRigidBodyPropertiesCfg
from isaaclab_newton.sim.schemas.schemas_cfg import (
MujocoJointDrivePropertiesCfg,
MujocoRigidBodyCfg,
MujocoRigidBodyPropertiesCfg,
)

body_gravcomp_unset = (
not isinstance(cfg.rigid_props, MujocoRigidBodyPropertiesCfg) or cfg.rigid_props.gravcomp is None
# gravcomp may be authored either via the legacy MujocoRigidBodyPropertiesCfg or via a
# MujocoRigidBodyCfg fragment in a rigid_props list. Treat either as "already set".
rigid_props_list = cfg.rigid_props if isinstance(cfg.rigid_props, (list, tuple)) else [cfg.rigid_props]
body_gravcomp_unset = not any(
isinstance(f, (MujocoRigidBodyPropertiesCfg, MujocoRigidBodyCfg)) and f.gravcomp is not None
for f in rigid_props_list
)
if (
isinstance(cfg.joint_drive_props, MujocoJointDrivePropertiesCfg)
Expand Down
8 changes: 6 additions & 2 deletions source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,5 +444,9 @@ 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
schemas.define_rigid_body_properties(prim_path, cfg.rigid_props, stage=stage)
# apply rigid properties (transition routing: 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)
else:
schemas.define_rigid_body_properties(prim_path, cfg.rigid_props, stage=stage)
7 changes: 6 additions & 1 deletion source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,4 +322,9 @@ 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:
schemas.define_rigid_body_properties(prim_path, cfg.rigid_props, stage=stage)
# transition routing: 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)
else:
schemas.define_rigid_body_properties(prim_path, cfg.rigid_props, stage=stage)
11 changes: 9 additions & 2 deletions source/isaaclab/isaaclab/sim/spawners/spawner_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,18 @@ class RigidObjectSpawnerCfg(SpawnerCfg):
mass_props: schemas.MassPropertiesCfg | None = None
"""Mass properties."""

rigid_props: schemas.RigidBodyBaseCfg | None = None
rigid_props: schemas.RigidBodyBaseCfg | schemas.RigidBodyFragment | list[schemas.RigidBodyFragment] | None = None
"""Rigid body properties.

Accepts either a single legacy cfg (e.g. :class:`~isaaclab.sim.schemas.RigidBodyBaseCfg`) or a
list of :class:`~isaaclab.sim.schemas.RigidBodyFragment` fragments
(e.g. ``[UsdPhysicsRigidBodyCfg(...), PhysxRigidBodyCfg(...)]``). When a fragment list is given,
``UsdPhysics.RigidBodyAPI`` is applied as the implicit anchor and each fragment writes its own
namespace.

For making a rigid object static, set the :attr:`schemas.RigidBodyBaseCfg.kinematic_enabled`
as True. This will make the object static and will not be affected by gravity or other forces.
(or :attr:`~isaaclab.sim.schemas.UsdPhysicsRigidBodyCfg.kinematic_enabled`) as True. This will
make the object static and will not be affected by gravity or other forces.
"""

collision_props: schemas.CollisionPropertiesCfg | None = None
Expand Down
4 changes: 3 additions & 1 deletion source/isaaclab/test/controllers/test_differential_ik.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
subtract_frame_transforms,
)

from isaaclab_physx.sim.schemas import PhysxRigidBodyCfg # isort:skip

##
# Pre-defined configs
##
Expand Down Expand Up @@ -100,7 +102,7 @@ def test_ur10_ik_pose_abs(sim):

# Create robot instance
robot_cfg = UR10_CFG.replace(prim_path="/World/envs/env_.*/Robot")
robot_cfg.spawn.rigid_props.disable_gravity = True
next(f for f in robot_cfg.spawn.rigid_props if isinstance(f, PhysxRigidBodyCfg)).disable_gravity = True
robot = Articulation(cfg=robot_cfg)

# Create IK controller
Expand Down
Loading
Loading