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
18 changes: 18 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,18 @@
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.
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 @@ -220,6 +223,8 @@ from .schemas import (
DeformableBodyPropertiesCfg,
FixedTendonPropertiesCfg,
JointDriveBaseCfg,
MassCfg,
MassFragment,
MassPropertiesCfg,
MeshCollisionPropertiesCfg,
PhysxJointDrivePropertiesCfg,
Expand All @@ -233,6 +238,7 @@ from .schemas import (
TriangleMeshSimplificationPropertiesCfg,
UsdPhysicsRigidBodyCfg,
activate_contact_sensors,
apply_mass_properties,
apply_namespaced,
apply_rigid_body_properties,
define_articulation_root_properties,
Expand Down
10 changes: 7 additions & 3 deletions source/isaaclab/isaaclab/sim/converters/mesh_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +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)
# apply rigid body properties (transition routing: fragment list -> apply_*; legacy cfg -> define_*)
mass_frags = cfg.mass_props if isinstance(cfg.mass_props, (list, tuple)) else [cfg.mass_props]
if mass_frags 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]
if rigid_frags and all(isinstance(f, schemas.SchemaFragment) for f in rigid_frags):
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
26 changes: 26 additions & 0 deletions source/isaaclab/isaaclab/sim/schemas/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,32 @@ def modify_collision_properties(
"""


def apply_mass_properties(prim_path: str, fragments, 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)
if not UsdPhysics.MassAPI(prim):
UsdPhysics.MassAPI.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_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 @@ -396,6 +396,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 non-zero, the mass is ignored and the density is used to compute the mass.
"""

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
10 changes: 7 additions & 3 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 @@ -353,9 +353,13 @@ 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)
mass_frags = cfg.mass_props if isinstance(cfg.mass_props, (list, tuple)) else [cfg.mass_props]
if mass_frags 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
10 changes: 7 additions & 3 deletions source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,10 +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)
# apply rigid properties (transition routing: fragment list -> apply_*; legacy cfg -> define_*)
mass_frags = cfg.mass_props if isinstance(cfg.mass_props, (list, tuple)) else [cfg.mass_props]
if mass_frags 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):
schemas.apply_rigid_body_properties(prim_path, rigid_frags, stage=stage)
Expand Down
9 changes: 7 additions & 2 deletions source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,10 +319,15 @@ 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: new fragment list -> apply_*; legacy single cfg -> define_*
mass_frags = cfg.mass_props if isinstance(cfg.mass_props, (list, tuple)) else [cfg.mass_props]
if mass_frags 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 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
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.
Expand Down
112 changes: 112 additions & 0 deletions source/isaaclab/test/sim/test_mass_fragments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

"""Launch Isaac Sim Simulator first."""

from isaaclab.app import AppLauncher

# launch omniverse app
simulation_app = AppLauncher(headless=True).app

"""Rest everything follows."""

from pxr import UsdGeom, UsdPhysics

import isaaclab.sim as sim_utils
from isaaclab.sim import SimulationCfg, SimulationContext


def _make_xform(stage, path="/World/Body"):
UsdGeom.Xform.Define(stage, path)
return stage.GetPrimAtPath(path)


# -------------------------------------------------------------------------------------
# Fragment metadata -- MassFragment marker, MassCfg
# -------------------------------------------------------------------------------------


def test_fragment_metadata_defaults():
from isaaclab.sim.schemas import MassCfg, MassFragment, SchemaFragment

cfg = MassCfg(mass=2.0)
assert isinstance(cfg, MassFragment) and isinstance(cfg, SchemaFragment)
assert type(cfg)._usd_namespace == "physics"
assert type(cfg)._usd_applied_schema is None # anchor applies MassAPI, not the fragment
assert cfg.func == "isaaclab.sim.schemas:apply_namespaced"
assert cfg.mass == 2.0 and cfg.density is None


# -------------------------------------------------------------------------------------
# apply_namespaced writes only the set fields under the physics namespace
# -------------------------------------------------------------------------------------


def test_apply_namespaced_writes_only_set_fields():
from isaaclab.sim.schemas import MassCfg, apply_namespaced

sim_utils.create_new_stage()
SimulationContext(SimulationCfg(dt=0.01))
stage = sim_utils.get_current_stage()
prim = _make_xform(stage)
UsdPhysics.MassAPI.Apply(prim)
apply_namespaced(MassCfg(mass=3.0), "/World/Body", stage)
assert abs(prim.GetAttribute("physics:mass").Get() - 3.0) < 1e-6
# None field must not be authored (density exists as a MassAPI fallback attr)
assert not prim.GetAttribute("physics:density").HasAuthoredValue()


# -------------------------------------------------------------------------------------
# apply_mass_properties dispatch (implicit MassAPI anchor)
# -------------------------------------------------------------------------------------


def test_apply_mass_properties_applies_anchor_and_writes_fields():
from isaaclab.sim.schemas import MassCfg, apply_mass_properties

sim_utils.create_new_stage()
SimulationContext(SimulationCfg(dt=0.01))
stage = sim_utils.get_current_stage()
_make_xform(stage, "/World/B2")
apply_mass_properties("/World/B2", [MassCfg(mass=5.0, density=100.0)], stage)
prim = stage.GetPrimAtPath("/World/B2")
assert bool(UsdPhysics.MassAPI(prim)) # implicit anchor applied
assert abs(prim.GetAttribute("physics:mass").Get() - 5.0) < 1e-6
assert abs(prim.GetAttribute("physics:density").Get() - 100.0) < 1e-6


# -------------------------------------------------------------------------------------
# spawner slot accepts a fragment list + transition routing
# -------------------------------------------------------------------------------------


def test_spawn_shape_with_mass_fragment_list():
from isaaclab.sim.schemas import MassCfg, UsdPhysicsRigidBodyCfg

sim_utils.create_new_stage()
SimulationContext(SimulationCfg(dt=0.01))
cfg = sim_utils.CuboidCfg(
size=(1, 1, 1),
rigid_props=[UsdPhysicsRigidBodyCfg(rigid_body_enabled=True)],
mass_props=[MassCfg(mass=4.0)],
)
cfg.func("/World/Cube", cfg)
prim = sim_utils.get_current_stage().GetPrimAtPath("/World/Cube")
assert bool(UsdPhysics.MassAPI(prim))
assert abs(prim.GetAttribute("physics:mass").Get() - 4.0) < 1e-6


# -------------------------------------------------------------------------------------
# public imports
# -------------------------------------------------------------------------------------


def test_public_imports():
from isaaclab.sim.schemas import ( # noqa: F401
MassCfg,
MassFragment,
SchemaFragment,
apply_mass_properties,
)