Skip to content
27 changes: 27 additions & 0 deletions source/isaaclab/changelog.d/vidurv-schema-frag-mass.minor.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Added
^^^^^

* Added the mass schema-fragment API: the :class:`~isaaclab.sim.schemas.MassFragment` marker and
:class:`~isaaclab.sim.schemas.MassCfg` (writes ``physics:mass`` / ``physics:density`` via
``UsdPhysics.MassAPI``). The legacy :class:`~isaaclab.sim.schemas.MassPropertiesCfg` remains the
canonical name and continues to work unchanged.
* Added :func:`~isaaclab.sim.schemas.apply_mass_properties`, which applies a list of mass fragments
with ``UsdPhysics.MassAPI`` as the implicit anchor.

Changed
^^^^^^^

* Changed the spawner ``mass_props`` slot
(:attr:`~isaaclab.sim.spawners.RigidObjectSpawnerCfg.mass_props`) to also accept a single
:class:`~isaaclab.sim.schemas.MassFragment` or a list of them. Legacy
:class:`~isaaclab.sim.schemas.MassPropertiesCfg` cfgs continue to work through a transition bridge
in the spawn writers.

Fixed
^^^^^

* Fixed :func:`~isaaclab.sim.schemas.apply_mass_properties` to raise ``ValueError`` on an invalid
prim path and to aggregate per-fragment results instead of always returning ``True``, matching
:func:`~isaaclab.sim.schemas.apply_rigid_body_properties`.
* Fixed the spawn writers so an empty ``mass_props`` list is a harmless no-op rather than being
forwarded to :func:`~isaaclab.sim.schemas.define_mass_properties` as an unexpected list.
6 changes: 6 additions & 0 deletions source/isaaclab/isaaclab/sim/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ __all__ = [
"DeformableBodyPropertiesCfg",
"FixedTendonPropertiesCfg",
"JointDriveBaseCfg",
"MassCfg",
"MassFragment",
"MassPropertiesCfg",
"MeshCollisionPropertiesCfg",
"MujocoJointDrivePropertiesCfg",
Expand All @@ -62,6 +64,7 @@ __all__ = [
"RigidBodyFragment",
"SchemaFragment",
"UsdPhysicsRigidBodyCfg",
"apply_mass_properties",
"apply_namespaced",
"apply_rigid_body_properties",
"SDFMeshPropertiesCfg",
Expand Down Expand Up @@ -221,6 +224,8 @@ from .schemas import (
DeformableBodyPropertiesCfg,
FixedTendonPropertiesCfg,
JointDriveBaseCfg,
MassCfg,
MassFragment,
MassPropertiesCfg,
MeshCollisionPropertiesCfg,
PhysxJointDrivePropertiesCfg,
Expand All @@ -234,6 +239,7 @@ from .schemas import (
TriangleMeshSimplificationPropertiesCfg,
UsdPhysicsRigidBodyCfg,
activate_contact_sensors,
apply_mass_properties,
apply_namespaced,
apply_rigid_body_properties,
define_articulation_root_properties,
Expand Down
11 changes: 8 additions & 3 deletions source/isaaclab/isaaclab/sim/converters/mesh_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
from isaacsim.core.experimental.utils.app import enable_extension
from pxr import Gf, Tf, Usd, UsdGeom, UsdPhysics, UsdUtils

from isaaclab.sim import schemas
from isaaclab.sim.converters.asset_converter_base import AssetConverterBase
from isaaclab.sim.converters.mesh_converter_cfg import MeshConverterCfg
from isaaclab.sim.schemas import schemas
from isaaclab.sim.utils import delete_prim, export_prim_to_file

# import logger
Expand Down Expand Up @@ -182,9 +182,14 @@ def _convert_asset(self, cfg: MeshConverterCfg):
# Apply mass and rigid body properties after everything else
# Properties are applied to the top level prim to avoid the case where all instances of this
# asset unintentionally share the same rigid body properties
# apply mass properties
# apply mass properties (transition shim, remove later: fragment list -> apply_*; legacy cfg -> define_*)
if cfg.mass_props is not None:
schemas.define_mass_properties(prim_path=xform_prim.GetPath(), cfg=cfg.mass_props, stage=stage)
# normalize a single fragment to a list so the convenience form routes like a list
mass_frags = [cfg.mass_props] if isinstance(cfg.mass_props, schemas.SchemaFragment) else cfg.mass_props
if isinstance(mass_frags, (list, tuple)) and all(isinstance(f, schemas.SchemaFragment) for f in mass_frags):
schemas.apply_mass_properties(str(xform_prim.GetPath()), mass_frags, stage=stage)
else:
schemas.define_mass_properties(prim_path=xform_prim.GetPath(), cfg=cfg.mass_props, stage=stage)
# 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]
Expand Down
6 changes: 6 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_mass_properties",
"apply_namespaced",
"apply_rigid_body_properties",
"define_actuator_properties",
Expand All @@ -33,6 +34,8 @@ __all__ = [
"DeformableBodyPropertiesBaseCfg",
"DeformableBodyPropertiesCfg",
"JointDriveBaseCfg",
"MassCfg",
"MassFragment",
"MassPropertiesCfg",
"MeshCollisionBaseCfg",
"RigidBodyFragment",
Expand All @@ -55,6 +58,7 @@ from .schemas import (
PHYSX_MESH_COLLISION_CFGS,
USD_MESH_COLLISION_CFGS,
activate_contact_sensors,
apply_mass_properties,
apply_namespaced,
apply_rigid_body_properties,
define_articulation_root_properties,
Expand Down Expand Up @@ -84,6 +88,8 @@ from .schemas_cfg import (
DeformableBodyPropertiesBaseCfg,
DeformableBodyPropertiesCfg,
JointDriveBaseCfg,
MassCfg,
MassFragment,
MassPropertiesCfg,
MeshCollisionBaseCfg,
RigidBodyBaseCfg,
Expand Down
33 changes: 33 additions & 0 deletions source/isaaclab/isaaclab/sim/schemas/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,39 @@ def modify_collision_properties(
"""


def apply_mass_properties(
prim_path: str, fragments: Iterable[schemas_cfg.MassFragment], stage: Usd.Stage | None = None
) -> bool:
"""Apply a list of mass fragments to a prim.

Applies ``UsdPhysics.MassAPI`` as the implicit anchor (the defining schema for mass properties),
then dispatches each fragment via its :attr:`~isaaclab.sim.schemas.SchemaFragment.func`.

Args:
prim_path: The prim path to apply the mass schemas on.
fragments: An iterable of :class:`~isaaclab.sim.schemas.MassFragment` 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)
# fail loudly on an invalid path (matches the legacy define_mass_properties writer)
if not prim.IsValid():
raise ValueError(f"Prim path '{prim_path}' is not valid.")
if not UsdPhysics.MassAPI(prim):
UsdPhysics.MassAPI.Apply(prim)
# aggregate per-fragment results so a reported failure is not masked by the always-applied anchor
success = True
for cfg in fragments:
func = cfg.func if callable(cfg.func) else string_to_callable(cfg.func)
success = bool(func(cfg, prim_path, stage)) and success
return success


def define_mass_properties(prim_path: str, cfg: schemas_cfg.MassPropertiesCfg, stage: Usd.Stage | None = None):
"""Apply the mass schema on the input prim and set its properties.

Expand Down
42 changes: 42 additions & 0 deletions source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,48 @@ class MassPropertiesCfg:
"""


@configclass
class MassFragment(SchemaFragment):
"""Marker base for mass fragments; types the ``mass_props`` slot."""

pass


@configclass
class MassCfg(MassFragment):
"""``physics:*`` mass attributes from `UsdPhysics.MassAPI`_.

The ``UsdPhysics.MassAPI`` schema is applied as the implicit anchor by the mass family writer
(:func:`~isaaclab.sim.schemas.apply_mass_properties`), so this fragment owns no applied schema
of its own. Mirrors the legacy :class:`MassPropertiesCfg`.

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

.. _UsdPhysics.MassAPI: https://openusd.org/dev/api/class_usd_physics_mass_a_p_i.html
"""

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

mass: float | None = None
"""The mass of the rigid body [kg].

Writes ``physics:mass`` via :class:`UsdPhysics.MassAPI`.

Note:
If ``density`` is non-zero, it takes precedence and is used to compute the mass instead.
"""
Comment on lines +433 to +440

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 Note for the mass field is ambiguous: "If non-zero, the mass is ignored" leaves "non-zero" unspecified (does it refer to mass or density?). The USD physics rule is that a non-zero density takes precedence and is used to derive mass. The same ambiguous note exists in MassPropertiesCfg but is copied here as-is.

Suggested change
mass: float | None = None
"""The mass of the rigid body [kg].
Writes ``physics:mass`` via :class:`UsdPhysics.MassAPI`.
Note:
If non-zero, the mass is ignored and the density is used to compute the mass.
"""
mass: float | None = None
"""The mass of the rigid body [kg].
Writes ``physics:mass`` via :class:`UsdPhysics.MassAPI`.
Note:
If ``density`` is non-zero, it takes precedence and is used to compute the mass instead.
"""

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


density: float | None = None
"""The density of the rigid body [kg/m^3].

Writes ``physics:density`` via :class:`UsdPhysics.MassAPI`. The density indirectly defines the
mass of the rigid body. It is generally computed using the collision approximation of the body.
"""


@configclass
class JointDriveBaseCfg:
"""Solver-common properties to define the drive mechanism of a joint.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,9 +353,14 @@ def _spawn_from_usd_file(
# modify collision properties
if cfg.collision_props is not None:
schemas.modify_collision_properties(prim_path, cfg.collision_props)
# modify mass properties
# modify mass properties (transition shim, remove later: fragment list -> apply_*; legacy cfg -> modify_*)
if cfg.mass_props is not None:
schemas.modify_mass_properties(prim_path, cfg.mass_props)
# normalize a single fragment to a list so the convenience form routes like a list
mass_frags = [cfg.mass_props] if isinstance(cfg.mass_props, schemas.SchemaFragment) else cfg.mass_props
if isinstance(mass_frags, (list, tuple)) and all(isinstance(f, schemas.SchemaFragment) for f in mass_frags):
schemas.apply_mass_properties(prim_path, mass_frags)
else:
schemas.modify_mass_properties(prim_path, cfg.mass_props)

# modify articulation root properties
if cfg.articulation_props is not None:
Expand Down
9 changes: 7 additions & 2 deletions source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,9 +441,14 @@ def _spawn_mesh_geom_from_mesh(

# note: we apply the rigid properties to the parent prim in case of rigid objects.
if cfg.rigid_props is not None:
# apply mass properties
# apply mass properties (transition shim, remove later: fragment list -> apply_*; legacy cfg -> define_*)
if cfg.mass_props is not None:
schemas.define_mass_properties(prim_path, cfg.mass_props, stage=stage)
# normalize a single fragment to a list so the convenience form routes like a list
mass_frags = [cfg.mass_props] if isinstance(cfg.mass_props, schemas.SchemaFragment) else cfg.mass_props
if isinstance(mass_frags, (list, tuple)) and all(isinstance(f, schemas.SchemaFragment) for f in mass_frags):
schemas.apply_mass_properties(prim_path, mass_frags, stage=stage)
else:
schemas.define_mass_properties(prim_path, cfg.mass_props, stage=stage)
# 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):
Expand Down
8 changes: 7 additions & 1 deletion source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,13 @@ def _spawn_geom_from_prim_type(
# note: we apply rigid properties in the end to later make the instanceable prim
# apply mass properties
if cfg.mass_props is not None:
schemas.define_mass_properties(prim_path, cfg.mass_props, stage=stage)
# transition shim, remove later: fragment(s) -> apply_*; legacy cfg -> define_*
# normalize a single fragment to a list so the convenience form routes like a list
mass_frags = [cfg.mass_props] if isinstance(cfg.mass_props, schemas.SchemaFragment) else cfg.mass_props
if isinstance(mass_frags, (list, tuple)) and all(isinstance(f, schemas.SchemaFragment) for f in mass_frags):
schemas.apply_mass_properties(prim_path, mass_frags, stage=stage)
else:
schemas.define_mass_properties(prim_path, cfg.mass_props, stage=stage)
# apply rigid body properties
if cfg.rigid_props is not None:
# transition shim, remove later: new fragment list -> apply_*; legacy single cfg -> define_*
Expand Down
10 changes: 8 additions & 2 deletions source/isaaclab/isaaclab/sim/spawners/spawner_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,14 @@ class RigidObjectSpawnerCfg(SpawnerCfg):
to the prim outside of the properties available by default when spawning the prim.
"""

mass_props: schemas.MassPropertiesCfg | None = None
"""Mass properties."""
mass_props: schemas.MassPropertiesCfg | schemas.MassFragment | list[schemas.MassFragment] | None = None
"""Mass properties.

Accepts either a single legacy :class:`~isaaclab.sim.schemas.MassPropertiesCfg` or a list of
:class:`~isaaclab.sim.schemas.MassFragment` fragments (e.g. ``[MassCfg(...)]``). When a fragment
list is given, ``UsdPhysics.MassAPI`` is applied as the implicit anchor and each fragment writes
its own namespace.
"""

rigid_props: schemas.RigidBodyBaseCfg | schemas.RigidBodyFragment | list[schemas.RigidBodyFragment] | None = None
"""Rigid body properties.
Comment on lines 83 to 95

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.

P1 Single MassFragment advertised but silently crashes at runtime

The type annotation adds schemas.MassFragment as a valid single-value option, but all four spawn shims (shapes.py, from_files.py, meshes.py, mesh_converter.py) route only isinstance(cfg.mass_props, (list, tuple)) through apply_mass_properties. A bare MassCfg(mass=1.0) (not wrapped in a list) falls into the else branch and is passed to define_mass_properties / modify_mass_properties, which calls _apply_namespaced_schemas. That helper builds cfg_dict from every dataclass field — including func inherited from SchemaFragment. Because func is non-None and _get_field_declaring_class resolves it to SchemaFragment (whose _usd_namespace = None), _apply_namespaced_schemas raises ValueError: SchemaFragment declares fields ['func'] but does not define '_usd_namespace'. No test exercises the single-fragment path.

Fix: either drop schemas.MassFragment from the union (since the docstring already says "a list of MassFragment fragments") or add a single-fragment guard in each shim, e.g. wrapping with [cfg.mass_props] before dispatching to apply_mass_properties.

Expand Down
Loading
Loading