Skip to content

Commit 798f04c

Browse files
ooctipusisaaclab-bot[bot]
authored andcommitted
[USD] Make PhysX tendon fragments schema- and instance-specific (#7534)
(cherry picked from commit 29bcb09)
1 parent 2af0251 commit 798f04c

14 files changed

Lines changed: 471 additions & 642 deletions

File tree

docs/source/api/lab_physx/isaaclab_physx.sim.schemas.rst

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ isaaclab_physx.sim.schemas
4343
.. autosummary::
4444

4545
PhysxFixedTendonPropertiesCfg
46+
PhysxTendonAxisRootCfg
47+
PhysxTendonAxisCfg
4648
PhysxSpatialTendonPropertiesCfg
49+
PhysxTendonAttachmentRootCfg
4750

4851
.. rubric:: Deformable body
4952

@@ -130,11 +133,26 @@ Tendon
130133
:show-inheritance:
131134
:exclude-members: __init__
132135

136+
.. autoclass:: PhysxTendonAxisRootCfg
137+
:members:
138+
:show-inheritance:
139+
:exclude-members: __init__
140+
141+
.. autoclass:: PhysxTendonAxisCfg
142+
:members:
143+
:show-inheritance:
144+
:exclude-members: __init__
145+
133146
.. autoclass:: PhysxSpatialTendonPropertiesCfg
134147
:members:
135148
:show-inheritance:
136149
:exclude-members: __init__
137150

151+
.. autoclass:: PhysxTendonAttachmentRootCfg
152+
:members:
153+
:show-inheritance:
154+
:exclude-members: __init__
155+
138156
Deformable Body
139157
---------------
140158

@@ -177,11 +195,9 @@ The following classes are part of the public :mod:`isaaclab_physx.sim.schemas` A
177195
PhysxCollisionCfg
178196
PhysxConvexDecompositionCfg
179197
PhysxConvexHullCfg
180-
PhysxFixedTendonCfg
181198
PhysxJointCfg
182199
PhysxRigidBodyCfg
183200
PhysxSDFMeshCfg
184-
PhysxSpatialTendonCfg
185201
PhysxTriangleMeshCfg
186202
PhysxTriangleMeshSimplificationCfg
187203
RigidBodyPropertiesCfg
@@ -223,9 +239,6 @@ The following classes are part of the public :mod:`isaaclab_physx.sim.schemas` A
223239
.. autoclass:: PhysxConvexHullCfg
224240
:show-inheritance:
225241

226-
.. autoclass:: PhysxFixedTendonCfg
227-
:show-inheritance:
228-
229242
.. autoclass:: PhysxJointCfg
230243
:show-inheritance:
231244

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

238-
.. autoclass:: PhysxSpatialTendonCfg
239-
:show-inheritance:
240-
241251
.. autoclass:: PhysxTriangleMeshCfg
242252
:show-inheritance:
243253

docs/source/overview/core-concepts/schema_fragments.rst

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,31 @@ imports a backend:
6060
- tendon-bearing prims (existing tendon instances)
6161
* - ``spatial_tendons_props``
6262
- :func:`~isaaclab.sim.schemas.apply_spatial_tendon_properties`
63-
- tendon attachment root / leaf prims
63+
- tendon attachment root prims
6464

6565
The tendon families are *tune-not-apply*: the tendon topology is authored in the source
66-
asset, so their writers only tune existing instances and never create them.
66+
asset, so their writers only tune existing instances and never create them. PhysX tendon
67+
fragments use ``instance_names`` to select one or more instances on each matching prim;
68+
``None`` (the default) broadcasts to all existing instances.
69+
70+
A fixed tendon is split at the same boundary as the PhysX schemas: the root fragment owns
71+
whole-tendon dynamics and limits, while the axis fragment owns each joint's contribution.
72+
Prim-path matching chooses the joints and ``instance_names`` chooses the tendon on those
73+
joints:
74+
75+
.. code-block:: python
76+
77+
from isaaclab_physx.sim.schemas import PhysxTendonAxisCfg, PhysxTendonAxisRootCfg
78+
79+
fixed_tendons_props = {
80+
"/joints/index_root": [
81+
PhysxTendonAxisRootCfg(instance_names="index_finger", stiffness=10.0),
82+
PhysxTendonAxisCfg(instance_names="index_finger", gearing=[1.0], joint_axis=["rotX"]),
83+
],
84+
"/joints/index_distal": [
85+
PhysxTendonAxisCfg(instance_names="index_finger", gearing=[-0.5], joint_axis=["rotX"]),
86+
],
87+
}
6788
6889
Targeting expressions
6990
---------------------
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Changed
2+
^^^^^^^
3+
4+
* Extended :func:`~isaaclab.sim.schemas.apply_fixed_tendon_properties` to target both
5+
``PhysxTendonAxisRootAPI`` and ``PhysxTendonAxisAPI`` instances, allowing backend fragments to
6+
configure whole fixed tendons and their individual joint-axis contributions separately.
7+
8+
Fixed
9+
^^^^^
10+
11+
* Fixed the legacy fixed- and spatial-tendon writers authoring properties under the applied API
12+
type name. They now use the schema-owned ``physxTendon:<instance>:*`` namespace that PhysX reads.

source/isaaclab/isaaclab/sim/schemas/schemas.py

Lines changed: 52 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1629,18 +1629,49 @@ def modify_joint_drive_properties(
16291629
"""
16301630

16311631

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

1636+
def _write_tendon_properties(prim, values, schema_type):
1637+
authored = False
1638+
for schema in prim.GetAppliedSchemas():
1639+
applied_type, instance = Usd.SchemaRegistry.GetTypeNameAndInstance(str(schema))
1640+
if applied_type != schema_type or not instance:
1641+
continue
1642+
authored = True
1643+
for name, value in values.items():
1644+
attribute = f"physxTendon:{instance}:{to_camel_case(name, 'cC')}"
1645+
safe_set_attribute_on_usd_prim(prim, attribute, value, camel_case=False)
1646+
return authored
16381647

1639-
def _is_spatial_tendon_target(prim: Usd.Prim) -> bool:
1640-
"""Whether a prim carries a spatial-tendon multi-apply instance."""
1641-
return any(
1642-
"PhysxTendonAttachmentRootAPI" in s or "PhysxTendonAttachmentLeafAPI" in s for s in prim.GetAppliedSchemas()
1648+
1649+
def _apply_tendon_fragments(prim_path_expr, fragments, schema_types, prim_types, family, stage):
1650+
fragments = list(fragments)
1651+
if stage is None:
1652+
stage = get_current_stage()
1653+
if not fragments:
1654+
return True
1655+
targets, _, any_skipped = _match_fragment_targets(
1656+
prim_path_expr,
1657+
lambda prim: prim.GetTypeName() in prim_types
1658+
or any(
1659+
Usd.SchemaRegistry.GetTypeNameAndInstance(str(schema))[0] in schema_types
1660+
for schema in prim.GetAppliedSchemas()
1661+
),
1662+
stage,
16431663
)
1664+
if not targets:
1665+
logger.warning("No %s-tendon targets matched expression '%s'; nothing was authored.", family, prim_path_expr)
1666+
return False
1667+
success = not any_skipped
1668+
for cfg in fragments:
1669+
func = cfg.func if callable(cfg.func) else string_to_callable(cfg.func)
1670+
fragment_hit = False
1671+
for target in targets:
1672+
fragment_hit |= bool(func(cfg, target.GetPath().pathString, stage))
1673+
success = fragment_hit and success
1674+
return success
16441675

16451676

16461677
def apply_fixed_tendon_properties(
@@ -1651,8 +1682,8 @@ def apply_fixed_tendon_properties(
16511682
The prims to author on are matched with :func:`~isaaclab.sim.utils.find_matching_prims`:
16521683
``prim_path_expr`` is a plain regular expression over whole prim paths, so ``[^/]+``
16531684
selects one path segment and ``/World/Robot/.*`` every descendant of a prim. A matched prim is a
1654-
fixed-tendon target when it carries an applied ``PhysxTendonAxisRootAPI`` multi-apply
1655-
instance or is a ``MjcTendon`` prim.
1685+
fixed-tendon target when it carries an applied ``PhysxTendonAxisRootAPI`` or
1686+
``PhysxTendonAxisAPI`` instance, or is a ``MjcTendon`` prim.
16561687
16571688
Fixed tendons are a *tune-not-apply* family: the tendon topology is authored in the source
16581689
asset, so this writer never creates instances -- it only dispatches each fragment via its
@@ -1675,26 +1706,7 @@ def apply_fixed_tendon_properties(
16751706
Returns:
16761707
True if every fragment tuned at least one target and no instanced prim was skipped.
16771708
"""
1678-
fragments = list(fragments)
1679-
if stage is None:
1680-
stage = get_current_stage()
1681-
if not fragments:
1682-
return True
1683-
targets, _, any_skipped = _match_fragment_targets(prim_path_expr, _is_fixed_tendon_target, stage)
1684-
if not targets:
1685-
logger.warning("No fixed-tendon targets matched expression '%s'; nothing was authored.", prim_path_expr)
1686-
return False
1687-
# per-fragment any-target aggregation: a fragment fails only when it tuned no target at all,
1688-
# since each backend's func legitimately no-ops on the other backend's tendon prims.
1689-
success = not any_skipped
1690-
for cfg in fragments:
1691-
func = cfg.func if callable(cfg.func) else string_to_callable(cfg.func)
1692-
fragment_hit = False
1693-
for target in targets:
1694-
if func(cfg, target.GetPath().pathString, stage):
1695-
fragment_hit = True
1696-
success = fragment_hit and success
1697-
return success
1709+
return _apply_tendon_fragments(prim_path_expr, fragments, _FIXED_TENDON_SCHEMAS, ("MjcTendon",), "fixed", stage)
16981710

16991711

17001712
@apply_nested
@@ -1735,36 +1747,12 @@ def modify_fixed_tendon_properties(
17351747
if stage is None:
17361748
stage = get_current_stage()
17371749

1738-
# get USD prim
17391750
tendon_prim = stage.GetPrimAtPath(prim_path)
1740-
# check if prim has fixed tendon applied on it or if the mjc tendon prim exiss
1741-
applied_schemas = tendon_prim.GetAppliedSchemas()
1742-
prim_type = tendon_prim.GetTypeName()
1743-
if not any("PhysxTendonAxisRootAPI" in s for s in applied_schemas) and prim_type != "MjcTendon":
1744-
return False
1745-
1746-
# resolve all available instances of the schema since it is multi-instance
1747-
cfg = cfg.to_dict()
1748-
if prim_type != "MjcTendon":
1749-
for schema_name in applied_schemas:
1750-
if "PhysxTendonAxisRootAPI" not in schema_name:
1751-
continue
1752-
# set into PhysX API by attribute prefix schema_name: (e.g. PhysxTendonAxisRootAPI:default:stiffness)
1753-
for attr_name, value in cfg.items():
1754-
safe_set_attribute_on_usd_prim(
1755-
tendon_prim,
1756-
f"{schema_name}:{to_camel_case(attr_name, 'cC')}",
1757-
value,
1758-
camel_case=False,
1759-
)
1760-
else:
1761-
# NOTE: ``mjc:*`` branch (``MjcTendon`` prim) kept inline; future split candidate into isaaclab_newton.
1762-
# only stiffness and damping in the cfg map to mjc attributes
1763-
for attr_name, value in cfg.items():
1764-
safe_set_attribute_on_usd_prim(
1765-
tendon_prim, f"mjc:{to_camel_case(attr_name, 'cC')}", value, camel_case=False
1766-
)
1767-
# success
1751+
values = cfg.to_dict()
1752+
if tendon_prim.GetTypeName() != "MjcTendon":
1753+
return _write_tendon_properties(tendon_prim, values, "PhysxTendonAxisRootAPI")
1754+
for name in ("stiffness", "damping"):
1755+
safe_set_attribute_on_usd_prim(tendon_prim, f"mjc:{name}", values.get(name), camel_case=False)
17681756
return True
17691757

17701758

@@ -1781,8 +1769,7 @@ def apply_spatial_tendon_properties(
17811769
The prims to author on are matched with :func:`~isaaclab.sim.utils.find_matching_prims`:
17821770
``prim_path_expr`` is a plain regular expression over whole prim paths, so ``[^/]+``
17831771
selects one path segment and ``/World/Robot/.*`` every descendant of a prim. A matched prim is a
1784-
spatial-tendon target when it carries an applied ``PhysxTendonAttachmentRootAPI`` or
1785-
``PhysxTendonAttachmentLeafAPI`` multi-apply instance.
1772+
spatial-tendon target when it carries an applied ``PhysxTendonAttachmentRootAPI`` instance.
17861773
17871774
Spatial tendons are a *tune-not-apply* family: the tendon topology is authored in the
17881775
source asset, so this writer never creates instances -- it only dispatches each fragment
@@ -1805,26 +1792,7 @@ def apply_spatial_tendon_properties(
18051792
Returns:
18061793
True if every fragment tuned at least one target and no instanced prim was skipped.
18071794
"""
1808-
fragments = list(fragments)
1809-
if stage is None:
1810-
stage = get_current_stage()
1811-
if not fragments:
1812-
return True
1813-
targets, _, any_skipped = _match_fragment_targets(prim_path_expr, _is_spatial_tendon_target, stage)
1814-
if not targets:
1815-
logger.warning("No spatial-tendon targets matched expression '%s'; nothing was authored.", prim_path_expr)
1816-
return False
1817-
# per-fragment any-target aggregation: a fragment fails only when it tuned no target at all,
1818-
# since each backend's func legitimately no-ops on the other backend's tendon prims.
1819-
success = not any_skipped
1820-
for cfg in fragments:
1821-
func = cfg.func if callable(cfg.func) else string_to_callable(cfg.func)
1822-
fragment_hit = False
1823-
for target in targets:
1824-
if func(cfg, target.GetPath().pathString, stage):
1825-
fragment_hit = True
1826-
success = fragment_hit and success
1827-
return success
1795+
return _apply_tendon_fragments(prim_path_expr, fragments, _SPATIAL_TENDON_SCHEMAS, (), "spatial", stage)
18281796

18291797

18301798
@apply_nested
@@ -1837,16 +1805,14 @@ def modify_spatial_tendon_properties(
18371805
through length and limit constraints. For instance, it can be used to set up an equality constraint
18381806
between a driven and passive revolute joints.
18391807
1840-
The schema comprises of attributes that belong to the `PhysxTendonAxisRootAPI`_ schema.
1808+
The schema comprises attributes that belong to the `PhysxTendonAttachmentRootAPI`_ schema.
18411809
18421810
.. note::
18431811
This function is decorated with :func:`apply_nested` that sets the properties to all the prims
18441812
(that have the schema applied on them) under the input prim path.
18451813
18461814
.. _spatial tendon: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/_api_build/classPxArticulationSpatialTendon.html
1847-
.. _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
18481815
.. _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
1849-
.. _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
18501816
18511817
Args:
18521818
prim_path: The prim path to the tendon attachment.
@@ -1866,29 +1832,8 @@ def modify_spatial_tendon_properties(
18661832
# obtain stage
18671833
if stage is None:
18681834
stage = get_current_stage()
1869-
# get USD prim
18701835
tendon_prim = stage.GetPrimAtPath(prim_path)
1871-
# check if prim has spatial tendon applied on it
1872-
applied_schemas = tendon_prim.GetAppliedSchemas()
1873-
has_spatial = any(
1874-
"PhysxTendonAttachmentRootAPI" in s or "PhysxTendonAttachmentLeafAPI" in s for s in applied_schemas
1875-
)
1876-
if not has_spatial:
1877-
return False
1878-
1879-
cfg = cfg.to_dict()
1880-
for schema_name in applied_schemas:
1881-
if "PhysxTendonAttachmentRootAPI" not in schema_name and "PhysxTendonAttachmentLeafAPI" not in schema_name:
1882-
continue
1883-
for attr_name, value in cfg.items():
1884-
safe_set_attribute_on_usd_prim(
1885-
tendon_prim,
1886-
f"{schema_name}:{to_camel_case(attr_name, 'cC')}",
1887-
value,
1888-
camel_case=False,
1889-
)
1890-
# success
1891-
return True
1836+
return _write_tendon_properties(tendon_prim, cfg.to_dict(), "PhysxTendonAttachmentRootAPI")
18921837

18931838

18941839
"""

source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,12 @@ class SchemaFragment:
123123
left unchanged on the prim (partial update).
124124
125125
.. important::
126-
Every dataclass field other than :attr:`func` is authored as a USD attribute
127-
``<_usd_namespace>:<camelCase(field)>``. A fragment must not carry non-USD/bookkeeping
128-
fields -- such state belongs on the spawner cfg or as a writer keyword argument (this is
129-
why ``fix_root_link`` / ``ensure_drives_exist`` are not fragment fields). The generic
130-
applier (:func:`~isaaclab.sim.schemas.apply_namespaced`) enforces the invariant: it raises
131-
when a fragment has no ``_usd_namespace``, and unsupported (non-scalar) value types raise
132-
when written.
126+
For fragments using :func:`~isaaclab.sim.schemas.apply_namespaced`, every dataclass field
127+
other than :attr:`func` is authored as ``<_usd_namespace>:<camelCase(field)>``. Irregular
128+
schemas may use a custom applier for value conversion. The PhysX tendon fragments are a
129+
narrow exception: their custom appliers consume ``instance_names`` to address an existing
130+
multiple-apply schema instance and never author it. Do not add bookkeeping fields or treat
131+
multiple-apply behavior as a generic core-fragment convention.
133132
"""
134133

135134
# -- Class metadata (not dataclass fields) --
@@ -226,10 +225,10 @@ class MeshCollisionFragment(SchemaFragment):
226225
class FixedTendonFragment(SchemaFragment):
227226
"""Marker base for fixed-tendon fragments; types the ``fixed_tendons_props`` slot.
228227
229-
Fixed tendons are a *tune-not-apply* family: the applied ``PhysxTendonAxisRootAPI``
230-
multi-instance schemas already exist on the prim (authored in the source asset), so the
231-
family writer (:func:`~isaaclab.sim.schemas.apply_fixed_tendon_properties`) does not apply
232-
any anchor schema; it only tunes the existing instances via each fragment's
228+
Fixed tendons are a *tune-not-apply* family: the applied ``PhysxTendonAxisRootAPI`` and
229+
``PhysxTendonAxisAPI`` instances already exist on joint prims (authored in the source asset),
230+
so the family writer (:func:`~isaaclab.sim.schemas.apply_fixed_tendon_properties`) does not
231+
apply an anchor schema; it only tunes existing instances via each fragment's
233232
:attr:`~isaaclab.sim.schemas.SchemaFragment.func`.
234233
"""
235234

@@ -241,10 +240,9 @@ class SpatialTendonFragment(SchemaFragment):
241240
"""Marker base for spatial-tendon fragments; types the ``spatial_tendons_props`` slot.
242241
243242
Spatial tendons are a *tune-not-apply* family: the applied
244-
``PhysxTendonAttachmentRootAPI`` / ``PhysxTendonAttachmentLeafAPI`` multi-instance schemas
245-
already exist on the prim (authored in the source asset), so the family writer
246-
(:func:`~isaaclab.sim.schemas.apply_spatial_tendon_properties`) does not apply any anchor
247-
schema; it only tunes the existing instances via each fragment's
243+
``PhysxTendonAttachmentRootAPI`` instances already exist on the prim (authored in the source
244+
asset), so the family writer (:func:`~isaaclab.sim.schemas.apply_spatial_tendon_properties`)
245+
does not apply an anchor schema; it only tunes existing root instances via each fragment's
248246
:attr:`~isaaclab.sim.schemas.SchemaFragment.func`.
249247
"""
250248

0 commit comments

Comments
 (0)