Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
17b6cf8
feat(schemas): add tendons schema-fragment API
vidurv-nvidia Jun 5, 2026
c37c28e
fix(schemas): tendon appliers return True (apply_nested wrappers retu…
vidurv-nvidia Jun 5, 2026
16e4ec6
docs(schemas): mark transition shim if/else for removal post-migration
vidurv-nvidia Jun 5, 2026
58080fe
style(schemas): trim verbose comments in tendon fragment code
vidurv-nvidia Jun 9, 2026
f454488
Merge branch 'develop' into vidurv/schema-frag-tendons
vidurv-nvidia Jun 25, 2026
2cad30b
Add prim guard and result aggregation to tendon writers
vidurv-nvidia Jun 25, 2026
9971d3f
Enumerate tendon instances directly in fragment appliers
vidurv-nvidia Jun 25, 2026
21295dc
Add Mujoco fixed-tendon fragment and applier
vidurv-nvidia Jun 25, 2026
eeec065
Import schemas package in mesh converter for SchemaFragment
vidurv-nvidia Jun 25, 2026
4df053b
Address tendon review: docs, changelog, and test coverage
vidurv-nvidia Jun 25, 2026
f5c9655
Merge branch 'develop' into vidurv/schema-frag-tendons
vidurv-nvidia Jun 25, 2026
15fbbf3
Descend to child prims when applying tendon fragments
vidurv-nvidia Jun 25, 2026
72fac5f
Mark tendon fragments as not namespace-driven
vidurv-nvidia Jun 25, 2026
2dfbeb4
Share tendon enumeration between fixed and spatial appliers
vidurv-nvidia Jun 25, 2026
f5fad4e
Add legacy-vs-fragment tendon equivalence regression test
vidurv-nvidia Jun 25, 2026
cf26820
Address tendon PR review: empty-list shim routing and types
vidurv-nvidia Jun 25, 2026
66fc36f
Merge remote-tracking branch 'origin/develop' into vidurv/schema-frag…
vidurv-nvidia Jun 26, 2026
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
29 changes: 29 additions & 0 deletions source/isaaclab/changelog.d/vidurv-schema-frag-tendons.minor.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Added
^^^^^

* Added the tendon schema-fragment markers
:class:`~isaaclab.sim.schemas.FixedTendonFragment` and
:class:`~isaaclab.sim.schemas.SpatialTendonFragment`, which type the spawner
``fixed_tendons_props`` / ``spatial_tendons_props`` slots.
* Added :func:`~isaaclab.sim.schemas.apply_fixed_tendon_properties` and
:func:`~isaaclab.sim.schemas.apply_spatial_tendon_properties`, the family writers that
dispatch a list of tendon fragments via each fragment's ``func``. Tendons are a
*tune-not-apply* family, so the writers tune the existing multi-instance tendon schemas
without applying a new anchor schema.

Changed
^^^^^^^

* Changed the spawner ``fixed_tendons_props`` / ``spatial_tendons_props`` slots
(:attr:`~isaaclab.sim.spawners.from_files.FileCfg.fixed_tendons_props`,
:attr:`~isaaclab.sim.spawners.from_files.FileCfg.spatial_tendons_props`) to also accept one
or more tendon fragments. Legacy single cfgs continue to work through a transition bridge in
the spawn writer.
* :func:`~isaaclab.sim.schemas.apply_fixed_tendon_properties` and
:func:`~isaaclab.sim.schemas.apply_spatial_tendon_properties` now raise
``ValueError`` when the prim at ``prim_path`` does not exist in the stage.
Callers that previously relied on an implicit no-op for invalid paths must
either validate the path beforehand or catch ``ValueError``.
The aggregated return value is now ``False`` whenever any fragment applier
reports failure; callers must not assume the return is always ``True`` even
when the prim is valid.
8 changes: 8 additions & 0 deletions source/isaaclab/isaaclab/sim/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ __all__ = [
"ConvexHullPropertiesCfg",
"DeformableBodyPropertiesBaseCfg",
"DeformableBodyPropertiesCfg",
"FixedTendonFragment",
"FixedTendonPropertiesCfg",
"JointDriveBaseCfg",
"MassCfg",
Expand All @@ -64,12 +65,15 @@ __all__ = [
"RigidBodyBaseCfg",
"RigidBodyFragment",
"SchemaFragment",
"SpatialTendonFragment",
"UsdPhysicsCollisionCfg",
"UsdPhysicsRigidBodyCfg",
"apply_collision_properties",
"apply_fixed_tendon_properties",
"apply_mass_properties",
"apply_namespaced",
"apply_rigid_body_properties",
"apply_spatial_tendon_properties",
"SDFMeshPropertiesCfg",
"SpatialTendonPropertiesCfg",
"TriangleMeshPropertiesCfg",
Expand Down Expand Up @@ -226,6 +230,7 @@ from .schemas import (
ConvexHullPropertiesCfg,
DeformableBodyPropertiesBaseCfg,
DeformableBodyPropertiesCfg,
FixedTendonFragment,
FixedTendonPropertiesCfg,
JointDriveBaseCfg,
MassCfg,
Expand All @@ -238,16 +243,19 @@ from .schemas import (
RigidBodyFragment,
SchemaFragment,
SDFMeshPropertiesCfg,
SpatialTendonFragment,
SpatialTendonPropertiesCfg,
TriangleMeshPropertiesCfg,
TriangleMeshSimplificationPropertiesCfg,
UsdPhysicsCollisionCfg,
UsdPhysicsRigidBodyCfg,
activate_contact_sensors,
apply_collision_properties,
apply_fixed_tendon_properties,
apply_mass_properties,
apply_namespaced,
apply_rigid_body_properties,
apply_spatial_tendon_properties,
define_articulation_root_properties,
define_collision_properties,
define_deformable_body_properties,
Expand Down
8 changes: 8 additions & 0 deletions source/isaaclab/isaaclab/sim/schemas/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ __all__ = [
"USD_MESH_COLLISION_CFGS",
"activate_contact_sensors",
"apply_collision_properties",
"apply_fixed_tendon_properties",
"apply_mass_properties",
"apply_namespaced",
"apply_rigid_body_properties",
"apply_spatial_tendon_properties",
"define_actuator_properties",
"define_articulation_root_properties",
"define_collision_properties",
Expand All @@ -35,13 +37,15 @@ __all__ = [
"CollisionFragment",
"DeformableBodyPropertiesBaseCfg",
"DeformableBodyPropertiesCfg",
"FixedTendonFragment",
"JointDriveBaseCfg",
"MassCfg",
"MassFragment",
"MassPropertiesCfg",
"MeshCollisionBaseCfg",
"RigidBodyFragment",
"SchemaFragment",
"SpatialTendonFragment",
"UsdPhysicsCollisionCfg",
"UsdPhysicsRigidBodyCfg",
"MujocoJointDrivePropertiesCfg",
Expand All @@ -62,9 +66,11 @@ from .schemas import (
USD_MESH_COLLISION_CFGS,
activate_contact_sensors,
apply_collision_properties,
apply_fixed_tendon_properties,
apply_mass_properties,
apply_namespaced,
apply_rigid_body_properties,
apply_spatial_tendon_properties,
define_articulation_root_properties,
define_collision_properties,
define_deformable_body_properties,
Expand Down Expand Up @@ -92,6 +98,7 @@ from .schemas_cfg import (
CollisionFragment,
DeformableBodyPropertiesBaseCfg,
DeformableBodyPropertiesCfg,
FixedTendonFragment,
JointDriveBaseCfg,
MassCfg,
MassFragment,
Expand All @@ -100,6 +107,7 @@ from .schemas_cfg import (
RigidBodyBaseCfg,
RigidBodyFragment,
SchemaFragment,
SpatialTendonFragment,
UsdPhysicsCollisionCfg,
UsdPhysicsRigidBodyCfg,
)
Expand Down
93 changes: 93 additions & 0 deletions source/isaaclab/isaaclab/sim/schemas/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,48 @@ def modify_joint_drive_properties(
"""


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

Fixed tendons are a *tune-not-apply* family: the applied ``PhysxTendonAxisRootAPI``
multi-instance schemas already exist on the prim (authored in the source asset). This writer
therefore applies no anchor schema; it only dispatches each fragment via its
:attr:`~isaaclab.sim.schemas.SchemaFragment.func`, which tunes the existing instances.
Backend fragments carry backend-specific funcs, so core never imports a backend.

Each fragment tunes only its own schema and returns ``False`` when that schema is not
present on the prim. A prim carries a single tendon backend, so compose backends across
prims rather than mixing PhysX and Mujoco fragments in one list on one prim.

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

Returns:
True if all fragments applied successfully, False if any fragment reported failure.

Raises:
ValueError: If the prim at ``prim_path`` is not valid.
"""
if stage is None:
stage = get_current_stage()
prim = stage.GetPrimAtPath(prim_path)
# fail loudly on an invalid path (matches the sibling apply_* writers)
if not prim.IsValid():
raise ValueError(f"Prim path '{prim_path}' is not valid.")
# tune-not-apply: the PhysxTendonAxisRootAPI instances already exist; apply no anchor.
# aggregate per-fragment results so a reported failure is not silently masked.
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


@apply_nested
def modify_fixed_tendon_properties(
prim_path: str, cfg: schemas_cfg.PhysxFixedTendonPropertiesCfg, stage: Usd.Stage | None = None
Expand Down Expand Up @@ -1021,6 +1063,9 @@ def modify_fixed_tendon_properties(
Raises:
ValueError: If the input prim path is not valid.
"""
# Retained for backward compatibility with callers passing PhysxFixedTendonPropertiesCfg
# directly. Will be removed in a future release once callers adopt the fragment-based
# apply_fixed_tendon_properties path.
# get stage handle
if stage is None:
stage = get_current_stage()
Expand Down Expand Up @@ -1048,6 +1093,7 @@ def modify_fixed_tendon_properties(
camel_case=False,
)
else:
# NOTE: ``mjc:*`` branch (``MjcTendon`` prim) kept inline; future split candidate into isaaclab_newton.
# only stiffness and damping in the cfg map to mjc attributes
for attr_name, value in cfg.items():
safe_set_attribute_on_usd_prim(
Expand All @@ -1062,6 +1108,50 @@ def modify_fixed_tendon_properties(
"""


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

Spatial tendons are a *tune-not-apply* family: the applied
``PhysxTendonAttachmentRootAPI`` / ``PhysxTendonAttachmentLeafAPI`` multi-instance schemas
already exist on the prim (authored in the source asset). This writer therefore applies no
anchor schema; it only dispatches each fragment via its
:attr:`~isaaclab.sim.schemas.SchemaFragment.func`, which tunes the existing instances.
Backend fragments carry backend-specific funcs, so core never imports a backend.

Each fragment tunes only its own schema and returns ``False`` when that schema is not
present on the prim. A prim carries a single tendon backend, so compose backends across
prims rather than mixing PhysX and Mujoco fragments in one list on one prim.

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

Returns:
True if all fragments applied successfully, False if any fragment reported failure.

Raises:
ValueError: If the prim at ``prim_path`` is not valid.
"""
if stage is None:
stage = get_current_stage()
prim = stage.GetPrimAtPath(prim_path)
# fail loudly on an invalid path (matches the sibling apply_* writers)
if not prim.IsValid():
raise ValueError(f"Prim path '{prim_path}' is not valid.")
# tune-not-apply: the PhysxTendonAttachmentRootAPI / PhysxTendonAttachmentLeafAPI instances
# already exist; apply no anchor.
# aggregate per-fragment results so a reported failure is not silently masked.
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


@apply_nested
def modify_spatial_tendon_properties(
prim_path: str, cfg: schemas_cfg.PhysxSpatialTendonPropertiesCfg, stage: Usd.Stage | None = None
Expand Down Expand Up @@ -1095,6 +1185,9 @@ def modify_spatial_tendon_properties(
Raises:
ValueError: If the input prim path is not valid.
"""
# Retained for backward compatibility with callers passing PhysxSpatialTendonPropertiesCfg
# directly. Will be removed in a future release once callers adopt the fragment-based
# apply_spatial_tendon_properties path.
# obtain stage
if stage is None:
stage = get_current_stage()
Expand Down
29 changes: 29 additions & 0 deletions source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,35 @@ class CollisionFragment(SchemaFragment):
pass


@configclass
class FixedTendonFragment(SchemaFragment):
"""Marker base for fixed-tendon fragments; types the ``fixed_tendons_props`` slot.

Fixed tendons are a *tune-not-apply* family: the applied ``PhysxTendonAxisRootAPI``
multi-instance schemas already exist on the prim (authored in the source asset), so the
family writer (:func:`~isaaclab.sim.schemas.apply_fixed_tendon_properties`) does not apply
any anchor schema; it only tunes the existing instances via each fragment's
:attr:`~isaaclab.sim.schemas.SchemaFragment.func`.
"""

pass


@configclass
class SpatialTendonFragment(SchemaFragment):
"""Marker base for spatial-tendon fragments; types the ``spatial_tendons_props`` slot.

Spatial tendons are a *tune-not-apply* family: the applied
``PhysxTendonAttachmentRootAPI`` / ``PhysxTendonAttachmentLeafAPI`` multi-instance schemas
already exist on the prim (authored in the source asset), so the family writer
(:func:`~isaaclab.sim.schemas.apply_spatial_tendon_properties`) does not apply any anchor
schema; it only tunes the existing instances via each fragment's
:attr:`~isaaclab.sim.schemas.SchemaFragment.func`.
"""

pass


@configclass
class UsdPhysicsCollisionCfg(CollisionFragment):
"""``physics:*`` collision attributes from `UsdPhysics.CollisionAPI`_.
Expand Down
28 changes: 26 additions & 2 deletions source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,9 +372,33 @@ def _spawn_from_usd_file(
schemas.modify_articulation_root_properties(prim_path, cfg.articulation_props)
# modify tendon properties
if cfg.fixed_tendons_props is not None:
schemas.modify_fixed_tendon_properties(prim_path, cfg.fixed_tendons_props)
# transition shim, remove later: fragment(s) -> apply_*; legacy cfg -> modify_*
# normalize a single fragment to a list so the convenience form (and an empty list) route like a list
fixed_tendon_frags = (
[cfg.fixed_tendons_props]
if isinstance(cfg.fixed_tendons_props, schemas.SchemaFragment)
else cfg.fixed_tendons_props
)
if isinstance(fixed_tendon_frags, (list, tuple)) and all(
isinstance(f, schemas.SchemaFragment) for f in fixed_tendon_frags
):
schemas.apply_fixed_tendon_properties(prim_path, fixed_tendon_frags)
else:
schemas.modify_fixed_tendon_properties(prim_path, cfg.fixed_tendons_props)
if cfg.spatial_tendons_props is not None:
schemas.modify_spatial_tendon_properties(prim_path, cfg.spatial_tendons_props)
# transition shim, remove later: fragment(s) -> apply_*; legacy cfg -> modify_*
# normalize a single fragment to a list so the convenience form (and an empty list) route like a list
spatial_tendon_frags = (
[cfg.spatial_tendons_props]
if isinstance(cfg.spatial_tendons_props, schemas.SchemaFragment)
else cfg.spatial_tendons_props
)
if isinstance(spatial_tendon_frags, (list, tuple)) and all(
isinstance(f, schemas.SchemaFragment) for f in spatial_tendon_frags
):
schemas.apply_spatial_tendon_properties(prim_path, spatial_tendon_frags)
else:
schemas.modify_spatial_tendon_properties(prim_path, cfg.spatial_tendons_props)
# define drive API on the joints
# note: these are only for setting low-level simulation properties. all others should be set or are
# and overridden by the articulation/actuator properties.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,23 @@ class FileCfg(RigidObjectSpawnerCfg, DeformableObjectSpawnerCfg):
articulation_props: schemas.ArticulationRootPropertiesCfg | None = None
"""Properties to apply to the articulation root."""

fixed_tendons_props: schemas.FixedTendonPropertiesCfg | None = None
"""Properties to apply to the fixed tendons (if any)."""
fixed_tendons_props: (
schemas.FixedTendonPropertiesCfg | schemas.FixedTendonFragment | list[schemas.FixedTendonFragment] | None
) = None
"""Properties to apply to the fixed tendons (if any).

spatial_tendons_props: schemas.SpatialTendonPropertiesCfg | None = None
"""Properties to apply to the spatial tendons (if any)."""
Accepts either the legacy :class:`~isaaclab_physx.sim.schemas.PhysxFixedTendonPropertiesCfg`
or one or more :class:`~isaaclab.sim.schemas.FixedTendonFragment` instances.
"""

spatial_tendons_props: (
schemas.SpatialTendonPropertiesCfg | schemas.SpatialTendonFragment | list[schemas.SpatialTendonFragment] | None
) = None
"""Properties to apply to the spatial tendons (if any).

Accepts either the legacy :class:`~isaaclab_physx.sim.schemas.PhysxSpatialTendonPropertiesCfg`
or one or more :class:`~isaaclab.sim.schemas.SpatialTendonFragment` instances.
"""

joint_drive_props: schemas.JointDriveBaseCfg | None = None
"""Properties to apply to a joint.
Expand Down
Loading
Loading