Skip to content
Merged
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
26 changes: 18 additions & 8 deletions docs/source/api/lab_physx/isaaclab_physx.sim.schemas.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ isaaclab_physx.sim.schemas
.. autosummary::

PhysxFixedTendonPropertiesCfg
PhysxTendonAxisRootCfg
PhysxTendonAxisCfg
PhysxSpatialTendonPropertiesCfg
PhysxTendonAttachmentRootCfg

.. rubric:: Deformable body

Expand Down Expand Up @@ -130,11 +133,26 @@ Tendon
:show-inheritance:
:exclude-members: __init__

.. autoclass:: PhysxTendonAxisRootCfg
:members:
:show-inheritance:
:exclude-members: __init__

.. autoclass:: PhysxTendonAxisCfg
:members:
:show-inheritance:
:exclude-members: __init__

.. autoclass:: PhysxSpatialTendonPropertiesCfg
:members:
:show-inheritance:
:exclude-members: __init__

.. autoclass:: PhysxTendonAttachmentRootCfg
:members:
:show-inheritance:
:exclude-members: __init__

Deformable Body
---------------

Expand Down Expand Up @@ -177,11 +195,9 @@ The following classes are part of the public :mod:`isaaclab_physx.sim.schemas` A
PhysxCollisionCfg
PhysxConvexDecompositionCfg
PhysxConvexHullCfg
PhysxFixedTendonCfg
PhysxJointCfg
PhysxRigidBodyCfg
PhysxSDFMeshCfg
PhysxSpatialTendonCfg
PhysxTriangleMeshCfg
PhysxTriangleMeshSimplificationCfg
RigidBodyPropertiesCfg
Expand Down Expand Up @@ -223,9 +239,6 @@ The following classes are part of the public :mod:`isaaclab_physx.sim.schemas` A
.. autoclass:: PhysxConvexHullCfg
:show-inheritance:

.. autoclass:: PhysxFixedTendonCfg
:show-inheritance:

.. autoclass:: PhysxJointCfg
:show-inheritance:

Expand All @@ -235,9 +248,6 @@ The following classes are part of the public :mod:`isaaclab_physx.sim.schemas` A
.. autoclass:: PhysxSDFMeshCfg
:show-inheritance:

.. autoclass:: PhysxSpatialTendonCfg
:show-inheritance:

.. autoclass:: PhysxTriangleMeshCfg
:show-inheritance:

Expand Down
25 changes: 23 additions & 2 deletions docs/source/overview/core-concepts/schema_fragments.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,31 @@ imports a backend:
- tendon-bearing prims (existing tendon instances)
* - ``spatial_tendons_props``
- :func:`~isaaclab.sim.schemas.apply_spatial_tendon_properties`
- tendon attachment root / leaf prims
- tendon attachment root prims

The tendon families are *tune-not-apply*: the tendon topology is authored in the source
asset, so their writers only tune existing instances and never create them.
asset, so their writers only tune existing instances and never create them. PhysX tendon
fragments use ``instance_names`` to select one or more instances on each matching prim;
``None`` (the default) broadcasts to all existing instances.

A fixed tendon is split at the same boundary as the PhysX schemas: the root fragment owns
whole-tendon dynamics and limits, while the axis fragment owns each joint's contribution.
Prim-path matching chooses the joints and ``instance_names`` chooses the tendon on those
joints:

.. code-block:: python

from isaaclab_physx.sim.schemas import PhysxTendonAxisCfg, PhysxTendonAxisRootCfg

fixed_tendons_props = {
"/joints/index_root": [
PhysxTendonAxisRootCfg(instance_names="index_finger", stiffness=10.0),
PhysxTendonAxisCfg(instance_names="index_finger", gearing=[1.0], joint_axis=["rotX"]),
],
"/joints/index_distal": [
PhysxTendonAxisCfg(instance_names="index_finger", gearing=[-0.5], joint_axis=["rotX"]),
],
}

Targeting expressions
---------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Changed
^^^^^^^

* Extended :func:`~isaaclab.sim.schemas.apply_fixed_tendon_properties` to target both
``PhysxTendonAxisRootAPI`` and ``PhysxTendonAxisAPI`` instances, allowing backend fragments to
configure whole fixed tendons and their individual joint-axis contributions separately.

Fixed
^^^^^

* Fixed the legacy fixed- and spatial-tendon writers authoring properties under the applied API
type name. They now use the schema-owned ``physxTendon:<instance>:*`` namespace that PhysX reads.
159 changes: 52 additions & 107 deletions source/isaaclab/isaaclab/sim/schemas/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -1629,18 +1629,49 @@ def modify_joint_drive_properties(
"""


def _is_fixed_tendon_target(prim: Usd.Prim) -> bool:
"""Whether a prim carries a fixed-tendon representation (PhysX multi-apply instance or MjcTendon prim)."""
if prim.GetTypeName() == "MjcTendon":
return True
return any("PhysxTendonAxisRootAPI" in s for s in prim.GetAppliedSchemas())
_FIXED_TENDON_SCHEMAS = ("PhysxTendonAxisRootAPI", "PhysxTendonAxisAPI")
_SPATIAL_TENDON_SCHEMAS = ("PhysxTendonAttachmentRootAPI",)


def _write_tendon_properties(prim, values, schema_type):
authored = False
for schema in prim.GetAppliedSchemas():
applied_type, instance = Usd.SchemaRegistry.GetTypeNameAndInstance(str(schema))
if applied_type != schema_type or not instance:
continue
authored = True
for name, value in values.items():
attribute = f"physxTendon:{instance}:{to_camel_case(name, 'cC')}"
safe_set_attribute_on_usd_prim(prim, attribute, value, camel_case=False)
return authored

def _is_spatial_tendon_target(prim: Usd.Prim) -> bool:
"""Whether a prim carries a spatial-tendon multi-apply instance."""
return any(
"PhysxTendonAttachmentRootAPI" in s or "PhysxTendonAttachmentLeafAPI" in s for s in prim.GetAppliedSchemas()

def _apply_tendon_fragments(prim_path_expr, fragments, schema_types, prim_types, family, stage):
fragments = list(fragments)
if stage is None:
stage = get_current_stage()
if not fragments:
return True
targets, _, any_skipped = _match_fragment_targets(
prim_path_expr,
lambda prim: prim.GetTypeName() in prim_types
or any(
Usd.SchemaRegistry.GetTypeNameAndInstance(str(schema))[0] in schema_types
for schema in prim.GetAppliedSchemas()
),
stage,
)
if not targets:
logger.warning("No %s-tendon targets matched expression '%s'; nothing was authored.", family, prim_path_expr)
return False
success = not any_skipped
for cfg in fragments:
func = cfg.func if callable(cfg.func) else string_to_callable(cfg.func)
fragment_hit = False
for target in targets:
fragment_hit |= bool(func(cfg, target.GetPath().pathString, stage))
success = fragment_hit and success
return success


def apply_fixed_tendon_properties(
Expand All @@ -1651,8 +1682,8 @@ def apply_fixed_tendon_properties(
The prims to author on are matched with :func:`~isaaclab.sim.utils.find_matching_prims`:
``prim_path_expr`` is a plain regular expression over whole prim paths, so ``[^/]+``
selects one path segment and ``/World/Robot/.*`` every descendant of a prim. A matched prim is a
fixed-tendon target when it carries an applied ``PhysxTendonAxisRootAPI`` multi-apply
instance or is a ``MjcTendon`` prim.
fixed-tendon target when it carries an applied ``PhysxTendonAxisRootAPI`` or
``PhysxTendonAxisAPI`` instance, or is a ``MjcTendon`` prim.

Fixed tendons are a *tune-not-apply* family: the tendon topology is authored in the source
asset, so this writer never creates instances -- it only dispatches each fragment via its
Expand All @@ -1675,26 +1706,7 @@ def apply_fixed_tendon_properties(
Returns:
True if every fragment tuned at least one target and no instanced prim was skipped.
"""
fragments = list(fragments)
if stage is None:
stage = get_current_stage()
if not fragments:
return True
targets, _, any_skipped = _match_fragment_targets(prim_path_expr, _is_fixed_tendon_target, stage)
if not targets:
logger.warning("No fixed-tendon targets matched expression '%s'; nothing was authored.", prim_path_expr)
return False
# per-fragment any-target aggregation: a fragment fails only when it tuned no target at all,
# since each backend's func legitimately no-ops on the other backend's tendon prims.
success = not any_skipped
for cfg in fragments:
func = cfg.func if callable(cfg.func) else string_to_callable(cfg.func)
fragment_hit = False
for target in targets:
if func(cfg, target.GetPath().pathString, stage):
fragment_hit = True
success = fragment_hit and success
return success
return _apply_tendon_fragments(prim_path_expr, fragments, _FIXED_TENDON_SCHEMAS, ("MjcTendon",), "fixed", stage)


@apply_nested
Expand Down Expand Up @@ -1735,36 +1747,12 @@ def modify_fixed_tendon_properties(
if stage is None:
stage = get_current_stage()

# get USD prim
tendon_prim = stage.GetPrimAtPath(prim_path)
# check if prim has fixed tendon applied on it or if the mjc tendon prim exiss
applied_schemas = tendon_prim.GetAppliedSchemas()
prim_type = tendon_prim.GetTypeName()
if not any("PhysxTendonAxisRootAPI" in s for s in applied_schemas) and prim_type != "MjcTendon":
return False

# resolve all available instances of the schema since it is multi-instance
cfg = cfg.to_dict()
if prim_type != "MjcTendon":
for schema_name in applied_schemas:
if "PhysxTendonAxisRootAPI" not in schema_name:
continue
# set into PhysX API by attribute prefix schema_name: (e.g. PhysxTendonAxisRootAPI:default:stiffness)
for attr_name, value in cfg.items():
safe_set_attribute_on_usd_prim(
tendon_prim,
f"{schema_name}:{to_camel_case(attr_name, 'cC')}",
value,
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(
tendon_prim, f"mjc:{to_camel_case(attr_name, 'cC')}", value, camel_case=False
)
# success
values = cfg.to_dict()
if tendon_prim.GetTypeName() != "MjcTendon":
return _write_tendon_properties(tendon_prim, values, "PhysxTendonAxisRootAPI")
for name in ("stiffness", "damping"):
safe_set_attribute_on_usd_prim(tendon_prim, f"mjc:{name}", values.get(name), camel_case=False)
return True


Expand All @@ -1781,8 +1769,7 @@ def apply_spatial_tendon_properties(
The prims to author on are matched with :func:`~isaaclab.sim.utils.find_matching_prims`:
``prim_path_expr`` is a plain regular expression over whole prim paths, so ``[^/]+``
selects one path segment and ``/World/Robot/.*`` every descendant of a prim. A matched prim is a
spatial-tendon target when it carries an applied ``PhysxTendonAttachmentRootAPI`` or
``PhysxTendonAttachmentLeafAPI`` multi-apply instance.
spatial-tendon target when it carries an applied ``PhysxTendonAttachmentRootAPI`` instance.

Spatial tendons are a *tune-not-apply* family: the tendon topology is authored in the
source asset, so this writer never creates instances -- it only dispatches each fragment
Expand All @@ -1805,26 +1792,7 @@ def apply_spatial_tendon_properties(
Returns:
True if every fragment tuned at least one target and no instanced prim was skipped.
"""
fragments = list(fragments)
if stage is None:
stage = get_current_stage()
if not fragments:
return True
targets, _, any_skipped = _match_fragment_targets(prim_path_expr, _is_spatial_tendon_target, stage)
if not targets:
logger.warning("No spatial-tendon targets matched expression '%s'; nothing was authored.", prim_path_expr)
return False
# per-fragment any-target aggregation: a fragment fails only when it tuned no target at all,
# since each backend's func legitimately no-ops on the other backend's tendon prims.
success = not any_skipped
for cfg in fragments:
func = cfg.func if callable(cfg.func) else string_to_callable(cfg.func)
fragment_hit = False
for target in targets:
if func(cfg, target.GetPath().pathString, stage):
fragment_hit = True
success = fragment_hit and success
return success
return _apply_tendon_fragments(prim_path_expr, fragments, _SPATIAL_TENDON_SCHEMAS, (), "spatial", stage)


@apply_nested
Expand All @@ -1837,16 +1805,14 @@ def modify_spatial_tendon_properties(
through length and limit constraints. For instance, it can be used to set up an equality constraint
between a driven and passive revolute joints.

The schema comprises of attributes that belong to the `PhysxTendonAxisRootAPI`_ schema.
The schema comprises attributes that belong to the `PhysxTendonAttachmentRootAPI`_ schema.

.. note::
This function is decorated with :func:`apply_nested` that sets the properties to all the prims
(that have the schema applied on them) under the input prim path.

.. _spatial tendon: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/_api_build/classPxArticulationSpatialTendon.html
.. _PhysxTendonAxisRootAPI: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/104.2/class_physx_schema_physx_tendon_axis_root_a_p_i.html
.. _PhysxTendonAttachmentRootAPI: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/104.2/class_physx_schema_physx_tendon_attachment_root_a_p_i.html
.. _PhysxTendonAttachmentLeafAPI: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/104.2/class_physx_schema_physx_tendon_attachment_leaf_a_p_i.html

Args:
prim_path: The prim path to the tendon attachment.
Expand All @@ -1866,29 +1832,8 @@ def modify_spatial_tendon_properties(
# obtain stage
if stage is None:
stage = get_current_stage()
# get USD prim
tendon_prim = stage.GetPrimAtPath(prim_path)
# check if prim has spatial tendon applied on it
applied_schemas = tendon_prim.GetAppliedSchemas()
has_spatial = any(
"PhysxTendonAttachmentRootAPI" in s or "PhysxTendonAttachmentLeafAPI" in s for s in applied_schemas
)
if not has_spatial:
return False

cfg = cfg.to_dict()
for schema_name in applied_schemas:
if "PhysxTendonAttachmentRootAPI" not in schema_name and "PhysxTendonAttachmentLeafAPI" not in schema_name:
continue
for attr_name, value in cfg.items():
safe_set_attribute_on_usd_prim(
tendon_prim,
f"{schema_name}:{to_camel_case(attr_name, 'cC')}",
value,
camel_case=False,
)
# success
return True
return _write_tendon_properties(tendon_prim, cfg.to_dict(), "PhysxTendonAttachmentRootAPI")


"""
Expand Down
28 changes: 13 additions & 15 deletions source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,12 @@ class SchemaFragment:
left unchanged on the prim (partial update).

.. important::
Every dataclass field other than :attr:`func` is authored as a USD attribute
``<_usd_namespace>:<camelCase(field)>``. 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.
For fragments using :func:`~isaaclab.sim.schemas.apply_namespaced`, every dataclass field
other than :attr:`func` is authored as ``<_usd_namespace>:<camelCase(field)>``. Irregular
schemas may use a custom applier for value conversion. The PhysX tendon fragments are a
narrow exception: their custom appliers consume ``instance_names`` to address an existing
multiple-apply schema instance and never author it. Do not add bookkeeping fields or treat
multiple-apply behavior as a generic core-fragment convention.
"""

# -- Class metadata (not dataclass fields) --
Expand Down Expand Up @@ -226,10 +225,10 @@ class MeshCollisionFragment(SchemaFragment):
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
Fixed tendons are a *tune-not-apply* family: the applied ``PhysxTendonAxisRootAPI`` and
``PhysxTendonAxisAPI`` instances already exist on joint prims (authored in the source asset),
so the family writer (:func:`~isaaclab.sim.schemas.apply_fixed_tendon_properties`) does not
apply an anchor schema; it only tunes existing instances via each fragment's
:attr:`~isaaclab.sim.schemas.SchemaFragment.func`.
"""

Expand All @@ -241,10 +240,9 @@ 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
``PhysxTendonAttachmentRootAPI`` instances 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 an anchor schema; it only tunes existing root instances via each fragment's
:attr:`~isaaclab.sim.schemas.SchemaFragment.func`.
"""

Expand Down
Loading
Loading