diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 90afb3b4c5dd..05a5d459fb7b 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -183,6 +183,7 @@ Guidelines for modifications: * Tsz Ki GAO * Tyler Lum * Victor Khaustov +* Vidur Vij * Virgilio Gómez Lambo * Vladimir Fokow * Wei Yang diff --git a/source/isaaclab/changelog.d/vidurv-schema-fragments.minor.rst b/source/isaaclab/changelog.d/vidurv-schema-fragments.minor.rst new file mode 100644 index 000000000000..49cea07c0068 --- /dev/null +++ b/source/isaaclab/changelog.d/vidurv-schema-fragments.minor.rst @@ -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. diff --git a/source/isaaclab/isaaclab/sim/__init__.pyi b/source/isaaclab/isaaclab/sim/__init__.pyi index ef4b27896b0d..9578caa87b49 100644 --- a/source/isaaclab/isaaclab/sim/__init__.pyi +++ b/source/isaaclab/isaaclab/sim/__init__.pyi @@ -59,6 +59,11 @@ __all__ = [ "PhysxJointDrivePropertiesCfg", "PhysxRigidBodyPropertiesCfg", "RigidBodyBaseCfg", + "RigidBodyFragment", + "SchemaFragment", + "UsdPhysicsRigidBodyCfg", + "apply_namespaced", + "apply_rigid_body_properties", "SDFMeshPropertiesCfg", "SpatialTendonPropertiesCfg", "TriangleMeshPropertiesCfg", @@ -191,8 +196,6 @@ __all__ = [ "XformPrimView", ] -from .simulation_cfg import RenderCfg, SimulationCfg -from .simulation_context import SimulationContext, build_simulation_context from .converters import ( AssetConverterBase, AssetConverterBaseCfg, @@ -207,22 +210,6 @@ from .schemas import ( MESH_APPROXIMATION_TOKENS, PHYSX_MESH_COLLISION_CFGS, USD_MESH_COLLISION_CFGS, - activate_contact_sensors, - define_articulation_root_properties, - define_collision_properties, - define_deformable_body_properties, - define_mass_properties, - define_mesh_collision_properties, - define_rigid_body_properties, - modify_articulation_root_properties, - modify_collision_properties, - modify_deformable_body_properties, - modify_fixed_tendon_properties, - modify_joint_drive_properties, - modify_mass_properties, - modify_mesh_collision_properties, - modify_rigid_body_properties, - modify_spatial_tendon_properties, ArticulationRootPropertiesCfg, BoundingCubePropertiesCfg, BoundingSpherePropertiesCfg, @@ -238,11 +225,34 @@ from .schemas import ( PhysxJointDrivePropertiesCfg, PhysxRigidBodyPropertiesCfg, RigidBodyBaseCfg, + RigidBodyFragment, + SchemaFragment, SDFMeshPropertiesCfg, SpatialTendonPropertiesCfg, TriangleMeshPropertiesCfg, TriangleMeshSimplificationPropertiesCfg, + UsdPhysicsRigidBodyCfg, + activate_contact_sensors, + apply_namespaced, + apply_rigid_body_properties, + define_articulation_root_properties, + define_collision_properties, + define_deformable_body_properties, + define_mass_properties, + define_mesh_collision_properties, + define_rigid_body_properties, + modify_articulation_root_properties, + modify_collision_properties, + modify_deformable_body_properties, + modify_fixed_tendon_properties, + modify_joint_drive_properties, + modify_mass_properties, + modify_mesh_collision_properties, + modify_rigid_body_properties, + modify_spatial_tendon_properties, ) +from .simulation_cfg import RenderCfg, SimulationCfg +from .simulation_context import SimulationContext, build_simulation_context # Forwarded to isaaclab_newton.sim.schemas via __getattr__ shim MujocoJointDrivePropertiesCfg = ... @@ -255,46 +265,22 @@ NewtonMeshCollisionPropertiesCfg = ... NewtonRigidBodyPropertiesCfg = ... NewtonSDFCollisionPropertiesCfg = ... from .spawners import ( - SpawnerCfg, - RigidObjectSpawnerCfg, - DeformableObjectSpawnerCfg, - spawn_from_mjcf, - spawn_from_urdf, - spawn_from_usd, - spawn_from_usd_with_compliant_contact_material, - spawn_ground_plane, - GroundPlaneCfg, - MjcfFileCfg, - UrdfFileCfg, - UsdFileCfg, - UsdFileWithCompliantContactCfg, - spawn_light, + CapsuleCfg, + ConeCfg, + CuboidCfg, + CylinderCfg, CylinderLightCfg, + DeformableBodyMaterialBaseCfg, + DeformableBodyMaterialCfg, + DeformableObjectSpawnerCfg, DiskLightCfg, DistantLightCfg, DomeLightCfg, - LightCfg, - SphereLightCfg, - spawn_rigid_body_material, - spawn_deformable_body_material, - PhysicsMaterialCfg, - RigidBodyMaterialCfg, - DeformableBodyMaterialBaseCfg, - DeformableBodyMaterialCfg, - SurfaceDeformableBodyMaterialBaseCfg, - SurfaceDeformableBodyMaterialCfg, - spawn_from_mdl_file, - spawn_preview_surface, + FisheyeCameraCfg, GlassMdlCfg, + GroundPlaneCfg, + LightCfg, MdlFileCfg, - PreviewSurfaceCfg, - VisualMaterialCfg, - spawn_mesh_capsule, - spawn_mesh_cone, - spawn_mesh_cuboid, - spawn_mesh_cylinder, - spawn_mesh_rectangle, - spawn_mesh_sphere, MeshCapsuleCfg, MeshCfg, MeshConeCfg, @@ -302,82 +288,110 @@ from .spawners import ( MeshCylinderCfg, MeshRectangleCfg, MeshSphereCfg, - spawn_camera, - spawn_sensor_frame, - FisheyeCameraCfg, + MjcfFileCfg, + MultiAssetSpawnerCfg, + MultiUsdFileCfg, + PhysicsMaterialCfg, PinholeCameraCfg, + PreviewSurfaceCfg, + RigidBodyMaterialCfg, + RigidObjectSpawnerCfg, SensorFrameCfg, + ShapeCfg, + SpawnerCfg, + SphereCfg, + SphereLightCfg, + SurfaceDeformableBodyMaterialBaseCfg, + SurfaceDeformableBodyMaterialCfg, + UrdfFileCfg, + UsdFileCfg, + UsdFileWithCompliantContactCfg, + VisualMaterialCfg, + spawn_camera, spawn_capsule, spawn_cone, spawn_cuboid, spawn_cylinder, - spawn_sphere, - CapsuleCfg, - ConeCfg, - CuboidCfg, - CylinderCfg, - ShapeCfg, - SphereCfg, + spawn_deformable_body_material, + spawn_from_mdl_file, + spawn_from_mjcf, + spawn_from_urdf, + spawn_from_usd, + spawn_from_usd_with_compliant_contact_material, + spawn_ground_plane, + spawn_light, + spawn_mesh_capsule, + spawn_mesh_cone, + spawn_mesh_cuboid, + spawn_mesh_cylinder, + spawn_mesh_rectangle, + spawn_mesh_sphere, spawn_multi_asset, spawn_multi_usd_file, - MultiAssetSpawnerCfg, - MultiUsdFileCfg, + spawn_preview_surface, + spawn_rigid_body_material, + spawn_sensor_frame, + spawn_sphere, ) from .utils import ( + add_labels, add_reference_to_stage, - get_stage_up_axis, - traverse_stage, - get_prim_at_path, - get_prim_path, - is_prim_path_valid, - define_prim, - get_prim_type_name, - get_next_free_path, + add_usd_reference, + apply_nested, + bind_physics_material, + bind_visual_material, + change_prim_property, + check_missing_labels, + clear_stage, + clone, + close_stage, + convert_world_pose_to_local, + count_total_labels, + create_new_stage, create_prim, + define_prim, delete_prim, - make_uninstanceable, - set_prim_visibility, - safe_set_attribute_on_usd_schema, - safe_set_attribute_on_usd_prim, - change_prim_property, export_prim_to_file, - apply_nested, - clone, - bind_visual_material, - bind_physics_material, - add_usd_reference, - get_usd_references, - select_usd_variants, - get_next_free_prim_path, - get_first_matching_ancestor_prim, - get_first_matching_child_prim, - get_all_matching_child_prims, find_first_matching_prim, - find_matching_prims, - resolve_matching_prims_from_source, - find_matching_prim_paths, find_global_fixed_joint_prim, - add_labels, + find_matching_prim_paths, + find_matching_prims, + get_all_matching_child_prims, + get_current_stage, + get_current_stage_id, + get_first_matching_ancestor_prim, + get_first_matching_child_prim, get_labels, - remove_labels, - check_missing_labels, - count_total_labels, - resolve_paths, - create_new_stage, + get_next_free_path, + get_next_free_prim_path, + get_prim_at_path, + get_prim_path, + get_prim_type_name, + get_stage_up_axis, + get_usd_references, is_current_stage_in_memory, + is_prim_path_valid, + make_uninstanceable, open_stage, - use_stage, - update_stage, + remove_labels, + resolve_matching_prims_from_source, + resolve_paths, + resolve_prim_pose, + resolve_prim_scale, + safe_set_attribute_on_usd_prim, + safe_set_attribute_on_usd_schema, save_stage, - close_stage, - clear_stage, - get_current_stage, - get_current_stage_id, + select_usd_variants, + set_prim_visibility, standardize_xform_ops, + traverse_stage, + update_stage, + use_stage, validate_standard_xform_ops, - resolve_prim_pose, - resolve_prim_scale, - convert_world_pose_to_local, ) -from .views import BaseFrameView, UsdFrameView, FrameView -from .views import XformPrimView # deprecated alias +from .views import ( + BaseFrameView, + FrameView, + UsdFrameView, + XformPrimView, # deprecated alias +) diff --git a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py index 74ba8b470c3a..f814e4fdb877 100644 --- a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py +++ b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py @@ -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 shim, remove later: 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() diff --git a/source/isaaclab/isaaclab/sim/schemas/__init__.pyi b/source/isaaclab/isaaclab/sim/schemas/__init__.pyi index 49eff741c8c8..af153a60fc63 100644 --- a/source/isaaclab/isaaclab/sim/schemas/__init__.pyi +++ b/source/isaaclab/isaaclab/sim/schemas/__init__.pyi @@ -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", @@ -33,6 +35,9 @@ __all__ = [ "JointDriveBaseCfg", "MassPropertiesCfg", "MeshCollisionBaseCfg", + "RigidBodyFragment", + "SchemaFragment", + "UsdPhysicsRigidBodyCfg", "MujocoJointDrivePropertiesCfg", "MujocoRigidBodyPropertiesCfg", "NewtonArticulationRootPropertiesCfg", @@ -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, @@ -80,6 +87,9 @@ from .schemas_cfg import ( MassPropertiesCfg, MeshCollisionBaseCfg, RigidBodyBaseCfg, + RigidBodyFragment, + SchemaFragment, + UsdPhysicsRigidBodyCfg, ) # Forwarded to isaaclab_newton.sim.schemas via __getattr__ shim diff --git a/source/isaaclab/isaaclab/sim/schemas/schemas.py b/source/isaaclab/isaaclab/sim/schemas/schemas.py index 23f53b105cb3..6617543cad61 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas.py @@ -9,6 +9,7 @@ import dataclasses import logging import math +from collections.abc import Iterable import numpy as np import warp as wp @@ -16,7 +17,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, @@ -214,6 +215,53 @@ 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: schemas_cfg.SchemaFragment, 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 ``:``; 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) + # fail loudly on an invalid path (matches the legacy define_/modify_ writers) + if not prim.IsValid(): + raise ValueError(f"Prim path '{prim_path}' is not valid.") + namespace = type(cfg)._usd_namespace + applied = type(cfg)._usd_applied_schema + # every fragment field is a namespaced USD attribute, so a namespace is required + if namespace is None: + raise ValueError( + f"Fragment '{type(cfg).__name__}' has no '_usd_namespace' set. Every fragment field is" + " authored as ':', so a USD namespace is required; non-USD state must" + " live on the spawner cfg or be passed as a writer keyword argument, not as a fragment" + " field." + ) + if applied and applied not in prim.GetAppliedSchemas(): + prim.AddAppliedSchema(applied) + for f in dataclasses.fields(cfg): + # ``func`` is the only non-USD field; non-scalar values raise in the setter + 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. """ @@ -386,6 +434,40 @@ def modify_articulation_root_properties( """ +def apply_rigid_body_properties( + prim_path: str, fragments: Iterable[schemas_cfg.RigidBodyFragment], 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) + # fail loudly on an invalid path (matches the legacy define_rigid_body_properties writer) + if not prim.IsValid(): + raise ValueError(f"Prim path '{prim_path}' is not valid.") + if not UsdPhysics.RigidBodyAPI(prim): + UsdPhysics.RigidBodyAPI.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_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. diff --git a/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py b/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py index 4eaec2004e91..ac5d8b22c020 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py @@ -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 @@ -106,6 +107,75 @@ 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 ``:``. 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). + + .. important:: + Every dataclass field other than :attr:`func` is authored as a USD attribute + ``<_usd_namespace>:``. A fragment must not carry non-USD/bookkeeping + fields -- such state belongs on the spawner cfg or as a writer keyword argument (this is + why ``fix_root_link`` / ``ensure_drives_exist`` are not fragment fields). The generic + applier (:func:`~isaaclab.sim.schemas.apply_namespaced`) enforces the invariant: it raises + when a fragment has no ``_usd_namespace``, and unsupported (non-scalar) value types raise + when written. + """ + + # -- 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. diff --git a/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py b/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py index 3a6fa939a63e..4545eb23e115 100644 --- a/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py +++ b/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py @@ -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 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) + 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) @@ -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) diff --git a/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py b/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py index cfc7f51b9ff2..27f815343c0a 100644 --- a/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py +++ b/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py @@ -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 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) + else: + schemas.define_rigid_body_properties(prim_path, cfg.rigid_props, stage=stage) diff --git a/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py b/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py index 9e8eafc1c578..0601cbed5411 100644 --- a/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py +++ b/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py @@ -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 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) + else: + schemas.define_rigid_body_properties(prim_path, cfg.rigid_props, stage=stage) diff --git a/source/isaaclab/isaaclab/sim/spawners/spawner_cfg.py b/source/isaaclab/isaaclab/sim/spawners/spawner_cfg.py index 3f1eef72a2fa..1d52d451ba1b 100644 --- a/source/isaaclab/isaaclab/sim/spawners/spawner_cfg.py +++ b/source/isaaclab/isaaclab/sim/spawners/spawner_cfg.py @@ -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 diff --git a/source/isaaclab/test/sim/test_schema_fragments.py b/source/isaaclab/test/sim/test_schema_fragments.py new file mode 100644 index 000000000000..b76239963da2 --- /dev/null +++ b/source/isaaclab/test/sim/test_schema_fragments.py @@ -0,0 +1,237 @@ +# 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.""" + +import pytest + +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) + + +# ------------------------------------------------------------------------------------- +# SchemaFragment base, RigidBodyFragment marker, UsdPhysicsRigidBodyCfg +# ------------------------------------------------------------------------------------- + + +def test_fragment_metadata_defaults(): + from isaaclab.sim.schemas import RigidBodyFragment, SchemaFragment, UsdPhysicsRigidBodyCfg + + cfg = UsdPhysicsRigidBodyCfg(rigid_body_enabled=True) + assert isinstance(cfg, RigidBodyFragment) and isinstance(cfg, SchemaFragment) + assert type(cfg)._usd_namespace == "physics" + assert type(cfg)._usd_applied_schema is None # anchor applies RigidBodyAPI, not the fragment + assert cfg.func == "isaaclab.sim.schemas:apply_namespaced" + assert cfg.rigid_body_enabled is True and cfg.kinematic_enabled is None + + +# ------------------------------------------------------------------------------------- +# apply_namespaced generic applier +# ------------------------------------------------------------------------------------- + + +def test_apply_namespaced_writes_only_set_fields(): + from isaaclab.sim.schemas import UsdPhysicsRigidBodyCfg, apply_namespaced + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + stage = sim_utils.get_current_stage() + prim = _make_xform(stage) + UsdPhysics.RigidBodyAPI.Apply(prim) + apply_namespaced(UsdPhysicsRigidBodyCfg(rigid_body_enabled=True), "/World/Body", stage) + assert prim.GetAttribute("physics:rigidBodyEnabled").Get() is True + # ``kinematicEnabled`` is a RigidBodyAPI fallback attr (so HasAttribute is True), but the + # None field must not be authored by apply_namespaced. + assert not prim.GetAttribute("physics:kinematicEnabled").HasAuthoredValue() + + +# ------------------------------------------------------------------------------------- +# PhysxRigidBodyCfg (isaaclab_physx) +# ------------------------------------------------------------------------------------- + + +def test_physx_rigid_body_fragment_writes_physx_namespace(): + from isaaclab_physx.sim.schemas import PhysxRigidBodyCfg + + from isaaclab.sim.schemas import apply_namespaced + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + stage = sim_utils.get_current_stage() + prim = _make_xform(stage, "/World/B2") + UsdPhysics.RigidBodyAPI.Apply(prim) + apply_namespaced(PhysxRigidBodyCfg(linear_damping=0.1, disable_gravity=True), "/World/B2", stage) + assert abs(prim.GetAttribute("physxRigidBody:linearDamping").Get() - 0.1) < 1e-6 + assert prim.GetAttribute("physxRigidBody:disableGravity").Get() is True + + +# ------------------------------------------------------------------------------------- +# MujocoRigidBodyCfg (isaaclab_newton) +# ------------------------------------------------------------------------------------- + + +def test_mujoco_rigid_body_fragment_writes_mjc_namespace(): + from isaaclab_newton.sim.schemas import MujocoRigidBodyCfg + + from isaaclab.sim.schemas import apply_namespaced + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + stage = sim_utils.get_current_stage() + prim = _make_xform(stage, "/World/B3") + UsdPhysics.RigidBodyAPI.Apply(prim) + apply_namespaced(MujocoRigidBodyCfg(gravcomp=1.0), "/World/B3", stage) + assert abs(prim.GetAttribute("mjc:gravcomp").Get() - 1.0) < 1e-6 + + +# ------------------------------------------------------------------------------------- +# apply_rigid_body_properties dispatch (implicit anchor + multi-namespace) +# ------------------------------------------------------------------------------------- + + +def test_apply_rigid_body_properties_composes_namespaces(): + from isaaclab_newton.sim.schemas import MujocoRigidBodyCfg + from isaaclab_physx.sim.schemas import PhysxRigidBodyCfg + + from isaaclab.sim.schemas import UsdPhysicsRigidBodyCfg, apply_rigid_body_properties + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + stage = sim_utils.get_current_stage() + _make_xform(stage, "/World/B4") + apply_rigid_body_properties( + "/World/B4", + [ + UsdPhysicsRigidBodyCfg(rigid_body_enabled=True), + PhysxRigidBodyCfg(linear_damping=0.2), + MujocoRigidBodyCfg(gravcomp=1.0), + ], + stage, + ) + prim = stage.GetPrimAtPath("/World/B4") + assert bool(UsdPhysics.RigidBodyAPI(prim)) # implicit anchor applied + assert prim.GetAttribute("physics:rigidBodyEnabled").Get() is True + assert abs(prim.GetAttribute("physxRigidBody:linearDamping").Get() - 0.2) < 1e-6 + assert abs(prim.GetAttribute("mjc:gravcomp").Get() - 1.0) < 1e-6 + + +# ------------------------------------------------------------------------------------- +# spawner slot accepts a fragment list + transition routing +# ------------------------------------------------------------------------------------- + + +def test_spawn_shape_with_rigid_fragment_list(): + from isaaclab_physx.sim.schemas import PhysxRigidBodyCfg + + from isaaclab.sim.schemas import 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), PhysxRigidBodyCfg(linear_damping=0.3)], + ) + cfg.func("/World/Cube", cfg) + prim = sim_utils.get_current_stage().GetPrimAtPath("/World/Cube") + assert bool(UsdPhysics.RigidBodyAPI(prim)) + assert abs(prim.GetAttribute("physxRigidBody:linearDamping").Get() - 0.3) < 1e-6 + + +# ------------------------------------------------------------------------------------- +# public imports +# ------------------------------------------------------------------------------------- + + +def test_public_imports(): + from isaaclab_newton.sim.schemas import MujocoRigidBodyCfg # noqa: F401 + from isaaclab_physx.sim.schemas import PhysxRigidBodyCfg # noqa: F401 + + from isaaclab.sim.schemas import ( # noqa: F401 + RigidBodyFragment, + SchemaFragment, + UsdPhysicsRigidBodyCfg, + apply_namespaced, + apply_rigid_body_properties, + ) + + +# ------------------------------------------------------------------------------------- +# Review follow-ups -- prim-validity guard, aggregated return, namespace invariant guard +# ------------------------------------------------------------------------------------- + + +def test_apply_namespaced_raises_on_invalid_prim(): + from isaaclab.sim.schemas import UsdPhysicsRigidBodyCfg, apply_namespaced + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + stage = sim_utils.get_current_stage() + # no prim authored at this path -> GetPrimAtPath returns an invalid prim + with pytest.raises(ValueError): + apply_namespaced(UsdPhysicsRigidBodyCfg(rigid_body_enabled=True), "/World/DoesNotExist", stage) + + +def test_apply_rigid_body_properties_raises_on_invalid_prim(): + from isaaclab.sim.schemas import UsdPhysicsRigidBodyCfg, apply_rigid_body_properties + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + stage = sim_utils.get_current_stage() + with pytest.raises(ValueError): + apply_rigid_body_properties("/World/DoesNotExist", [UsdPhysicsRigidBodyCfg(rigid_body_enabled=True)], stage) + + +def test_apply_rigid_body_properties_aggregates_fragment_results(): + from isaaclab.sim.schemas import UsdPhysicsRigidBodyCfg, apply_rigid_body_properties + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + stage = sim_utils.get_current_stage() + _make_xform(stage, "/World/Agg") + + # a fragment whose applier reports failure must make the aggregate return False + failing = UsdPhysicsRigidBodyCfg(rigid_body_enabled=True) + failing.func = lambda cfg, prim_path, stage=None: False + assert apply_rigid_body_properties("/World/Agg", [failing], stage) is False + + # all-succeeding fragments return True + ok = UsdPhysicsRigidBodyCfg(rigid_body_enabled=True) + assert apply_rigid_body_properties("/World/Agg", [ok], stage) is True + + +def test_apply_namespaced_raises_without_namespace(): + from typing import ClassVar + + from isaaclab.sim.schemas import RigidBodyFragment, apply_namespaced + from isaaclab.utils import configclass + + @configclass + class _NoNamespaceFragment(RigidBodyFragment): + # deliberately leaves ``_usd_namespace`` as None, violating the fragment invariant that + # every field is authored as a namespaced USD attribute + _usd_namespace: ClassVar[str | None] = None + rigid_body_enabled: bool | None = None + + sim_utils.create_new_stage() + SimulationContext(SimulationCfg(dt=0.01)) + stage = sim_utils.get_current_stage() + prim = _make_xform(stage, "/World/NoNs") + UsdPhysics.RigidBodyAPI.Apply(prim) + with pytest.raises(ValueError): + apply_namespaced(_NoNamespaceFragment(rigid_body_enabled=True), "/World/NoNs", stage) diff --git a/source/isaaclab_newton/changelog.d/vidurv-schema-fragments.minor.rst b/source/isaaclab_newton/changelog.d/vidurv-schema-fragments.minor.rst new file mode 100644 index 000000000000..a4cb8ef1dddf --- /dev/null +++ b/source/isaaclab_newton/changelog.d/vidurv-schema-fragments.minor.rst @@ -0,0 +1,7 @@ +Added +^^^^^ + +* Added :class:`~isaaclab_newton.sim.schemas.MujocoRigidBodyCfg`, the ``mjc:*`` single-namespace + rigid-body fragment (``mjc:gravcomp``) for Newton's MuJoCo solver. It composes with + :class:`~isaaclab.sim.schemas.UsdPhysicsRigidBodyCfg` and + :class:`~isaaclab_physx.sim.schemas.PhysxRigidBodyCfg` in a ``rigid_props`` fragment list. diff --git a/source/isaaclab_newton/isaaclab_newton/sim/schemas/__init__.pyi b/source/isaaclab_newton/isaaclab_newton/sim/schemas/__init__.pyi index e546498ee4aa..89ca2d068afb 100644 --- a/source/isaaclab_newton/isaaclab_newton/sim/schemas/__init__.pyi +++ b/source/isaaclab_newton/isaaclab_newton/sim/schemas/__init__.pyi @@ -5,6 +5,7 @@ __all__ = [ "MujocoJointDrivePropertiesCfg", + "MujocoRigidBodyCfg", "MujocoRigidBodyPropertiesCfg", "NewtonArticulationRootPropertiesCfg", "NewtonCollisionPropertiesCfg", @@ -18,6 +19,7 @@ __all__ = [ from .schemas_cfg import ( MujocoJointDrivePropertiesCfg, + MujocoRigidBodyCfg, MujocoRigidBodyPropertiesCfg, NewtonArticulationRootPropertiesCfg, NewtonCollisionPropertiesCfg, diff --git a/source/isaaclab_newton/isaaclab_newton/sim/schemas/schemas_cfg.py b/source/isaaclab_newton/isaaclab_newton/sim/schemas/schemas_cfg.py index a7b0d0082577..4c99379e1ad8 100644 --- a/source/isaaclab_newton/isaaclab_newton/sim/schemas/schemas_cfg.py +++ b/source/isaaclab_newton/isaaclab_newton/sim/schemas/schemas_cfg.py @@ -14,6 +14,7 @@ JointDriveBaseCfg, MeshCollisionBaseCfg, RigidBodyBaseCfg, + RigidBodyFragment, ) from isaaclab.sim.spawners.materials.physics_materials_cfg import RigidBodyMaterialBaseCfg from isaaclab.utils.configclass import configclass @@ -84,6 +85,31 @@ class MujocoRigidBodyPropertiesCfg(NewtonRigidBodyPropertiesCfg): """ +@configclass +class MujocoRigidBodyCfg(RigidBodyFragment): + """``mjc:*`` rigid-body attributes for Newton's MuJoCo solver. + + A single-namespace fragment (see :class:`~isaaclab.sim.schemas.SchemaFragment`) carrying + body-level gravity compensation. The ``mjc`` namespace has no applied schema; the + ``UsdPhysics.RigidBodyAPI`` anchor is applied by + :func:`~isaaclab.sim.schemas.apply_rigid_body_properties`. + + .. note:: + A ``newton:*`` rigid-body fragment is reserved but currently empty (Newton has no native + ``newton:`` rigid-body attributes today). + """ + + _usd_namespace: ClassVar[str | None] = "mjc" + _usd_applied_schema: ClassVar[str | None] = None + + gravcomp: float | None = None + """Gravity compensation scale for the body [dimensionless]. + + ``0.0`` = no compensation; ``1.0`` = full compensation. Written to ``mjc:gravcomp``. Body-level + gravcomp must be set for joint-level ``actuatorgravcomp`` to have any effect. + """ + + @configclass class NewtonJointDrivePropertiesCfg(JointDriveBaseCfg): """Newton-targeted joint drive properties. diff --git a/source/isaaclab_physx/changelog.d/vidurv-schema-fragments.minor.rst b/source/isaaclab_physx/changelog.d/vidurv-schema-fragments.minor.rst new file mode 100644 index 000000000000..6bd8bcac8b86 --- /dev/null +++ b/source/isaaclab_physx/changelog.d/vidurv-schema-fragments.minor.rst @@ -0,0 +1,8 @@ +Added +^^^^^ + +* Added :class:`~isaaclab_physx.sim.schemas.PhysxRigidBodyCfg`, the ``physxRigidBody:*`` + single-namespace rigid-body fragment (PhysX ``PhysxRigidBodyAPI``). It carries the PhysX + damping / velocity-limit / solver-iteration / sleep fields plus ``disable_gravity``, and + composes with :class:`~isaaclab.sim.schemas.UsdPhysicsRigidBodyCfg` in a ``rigid_props`` + fragment list. diff --git a/source/isaaclab_physx/isaaclab_physx/sim/schemas/__init__.pyi b/source/isaaclab_physx/isaaclab_physx/sim/schemas/__init__.pyi index b542edf9f454..10d2502ddf2b 100644 --- a/source/isaaclab_physx/isaaclab_physx/sim/schemas/__init__.pyi +++ b/source/isaaclab_physx/isaaclab_physx/sim/schemas/__init__.pyi @@ -23,6 +23,7 @@ __all__ = [ "PhysxDeformableCollisionPropertiesCfg", "PhysxFixedTendonPropertiesCfg", "PhysxJointDrivePropertiesCfg", + "PhysxRigidBodyCfg", "PhysxRigidBodyPropertiesCfg", "PhysxSDFMeshPropertiesCfg", "PhysxSpatialTendonPropertiesCfg", @@ -57,6 +58,7 @@ from .schemas_cfg import ( PhysxDeformableCollisionPropertiesCfg, PhysxFixedTendonPropertiesCfg, PhysxJointDrivePropertiesCfg, + PhysxRigidBodyCfg, PhysxRigidBodyPropertiesCfg, PhysxSDFMeshPropertiesCfg, PhysxSpatialTendonPropertiesCfg, diff --git a/source/isaaclab_physx/isaaclab_physx/sim/schemas/schemas_cfg.py b/source/isaaclab_physx/isaaclab_physx/sim/schemas/schemas_cfg.py index 2eacd969e949..18a5de023f90 100644 --- a/source/isaaclab_physx/isaaclab_physx/sim/schemas/schemas_cfg.py +++ b/source/isaaclab_physx/isaaclab_physx/sim/schemas/schemas_cfg.py @@ -15,6 +15,7 @@ JointDriveBaseCfg, MeshCollisionBaseCfg, RigidBodyBaseCfg, + RigidBodyFragment, ) from isaaclab.utils.configclass import configclass @@ -261,6 +262,64 @@ class PhysxRigidBodyPropertiesCfg(RigidBodyBaseCfg): """The mass-normalized kinetic energy threshold below which an actor may participate in stabilization.""" +@configclass +class PhysxRigidBodyCfg(RigidBodyFragment): + """``physxRigidBody:*`` rigid-body attributes from `PhysxRigidBodyAPI`_. + + A single-namespace fragment (see :class:`~isaaclab.sim.schemas.SchemaFragment`) for the + PhysX rigid-body add-on schema. Applied alongside :class:`~isaaclab.sim.schemas.UsdPhysicsRigidBodyCfg` + via :func:`~isaaclab.sim.schemas.apply_rigid_body_properties`. + + .. _PhysxRigidBodyAPI: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/104.2/class_physx_schema_physx_rigid_body_a_p_i.html + """ + + _usd_namespace: ClassVar[str | None] = "physxRigidBody" + _usd_applied_schema: ClassVar[str | None] = "PhysxRigidBodyAPI" + + linear_damping: float | None = None + """Linear damping coefficient for the body [1/s].""" + + angular_damping: float | None = None + """Angular damping coefficient for the body [1/s].""" + + max_linear_velocity: float | None = None + """Maximum linear velocity for the body [m/s].""" + + max_angular_velocity: float | None = None + """Maximum angular velocity for the body [deg/s].""" + + max_depenetration_velocity: float | None = None + """Maximum depenetration velocity permitted to be introduced by the solver [m/s].""" + + max_contact_impulse: float | None = None + """The limit on the impulse that may be applied at a contact [N·s].""" + + enable_gyroscopic_forces: bool | None = None + """Enables computation of gyroscopic forces on the rigid body.""" + + retain_accelerations: bool | None = None + """Carries over forces/accelerations over sub-steps.""" + + solver_position_iteration_count: int | None = None + """Solver position iteration counts for the body.""" + + solver_velocity_iteration_count: int | None = None + """Solver velocity iteration counts for the body.""" + + sleep_threshold: float | None = None + """Mass-normalized kinetic energy threshold below which an actor may go to sleep [m²/s²].""" + + stabilization_threshold: float | None = None + """Mass-normalized kinetic energy threshold below which an actor may participate in stabilization [m²/s²].""" + + disable_gravity: bool | None = None + """Disable gravity for the body. + + PhysX honors this per-body via ``physxRigidBody:disableGravity``: setting True excludes the + body from world gravity integration. + """ + + @configclass class RigidBodyPropertiesCfg(PhysxRigidBodyPropertiesCfg): """Deprecated: use :class:`PhysxRigidBodyPropertiesCfg` or :class:`~isaaclab.sim.schemas.RigidBodyBaseCfg`.