Skip to content

Commit 4dfb823

Browse files
committed
Make PhysX tendon fragments instance-selective
1 parent b44ea65 commit 4dfb823

12 files changed

Lines changed: 559 additions & 171 deletions

File tree

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

Lines changed: 25 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+
PhysxFixedTendonCfg
47+
PhysxFixedTendonAxisCfg
4648
PhysxSpatialTendonPropertiesCfg
49+
PhysxSpatialTendonCfg
4750

4851
.. rubric:: Deformable body
4952

@@ -57,6 +60,9 @@ isaaclab_physx.sim.schemas
5760

5861
.. autosummary::
5962

63+
apply_fixed_tendon
64+
apply_fixed_tendon_axis
65+
apply_spatial_tendon
6066
define_deformable_body_properties
6167
modify_deformable_body_properties
6268

@@ -130,11 +136,30 @@ Tendon
130136
:show-inheritance:
131137
:exclude-members: __init__
132138

139+
.. autoclass:: PhysxFixedTendonCfg
140+
:members:
141+
:show-inheritance:
142+
:exclude-members: __init__
143+
144+
.. autoclass:: PhysxFixedTendonAxisCfg
145+
:members:
146+
:show-inheritance:
147+
:exclude-members: __init__
148+
133149
.. autoclass:: PhysxSpatialTendonPropertiesCfg
134150
:members:
135151
:show-inheritance:
136152
:exclude-members: __init__
137153

154+
.. autoclass:: PhysxSpatialTendonCfg
155+
:members:
156+
:show-inheritance:
157+
:exclude-members: __init__
158+
159+
.. autofunction:: apply_fixed_tendon
160+
.. autofunction:: apply_fixed_tendon_axis
161+
.. autofunction:: apply_spatial_tendon
162+
138163
Deformable Body
139164
---------------
140165

@@ -177,11 +202,9 @@ The following classes are part of the public :mod:`isaaclab_physx.sim.schemas` A
177202
PhysxCollisionCfg
178203
PhysxConvexDecompositionCfg
179204
PhysxConvexHullCfg
180-
PhysxFixedTendonCfg
181205
PhysxJointCfg
182206
PhysxRigidBodyCfg
183207
PhysxSDFMeshCfg
184-
PhysxSpatialTendonCfg
185208
PhysxTriangleMeshCfg
186209
PhysxTriangleMeshSimplificationCfg
187210
RigidBodyPropertiesCfg
@@ -223,9 +246,6 @@ The following classes are part of the public :mod:`isaaclab_physx.sim.schemas` A
223246
.. autoclass:: PhysxConvexHullCfg
224247
:show-inheritance:
225248

226-
.. autoclass:: PhysxFixedTendonCfg
227-
:show-inheritance:
228-
229249
.. autoclass:: PhysxJointCfg
230250
:show-inheritance:
231251

@@ -235,9 +255,6 @@ The following classes are part of the public :mod:`isaaclab_physx.sim.schemas` A
235255
.. autoclass:: PhysxSDFMeshCfg
236256
:show-inheritance:
237257

238-
.. autoclass:: PhysxSpatialTendonCfg
239-
:show-inheritance:
240-
241258
.. autoclass:: PhysxTriangleMeshCfg
242259
:show-inheritance:
243260

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 PhysxFixedTendonAxisCfg, PhysxFixedTendonCfg
78+
79+
fixed_tendons_props = {
80+
"/joints/index_root": [
81+
PhysxFixedTendonCfg(instance_names="index_finger", stiffness=10.0),
82+
PhysxFixedTendonAxisCfg(instance_names="index_finger", gearing=[1.0], joint_axis=["rotX"]),
83+
],
84+
"/joints/index_distal": [
85+
PhysxFixedTendonAxisCfg(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: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1629,18 +1629,27 @@ def modify_joint_drive_properties(
16291629
"""
16301630

16311631

1632+
def _applied_schema_instance(applied_schema, schema_type: str) -> str | None:
1633+
"""Return an applied multiple-apply schema's instance when its type matches exactly."""
1634+
applied_type, instance = Usd.SchemaRegistry.GetTypeNameAndInstance(str(applied_schema))
1635+
return instance if applied_type == schema_type and instance else None
1636+
1637+
16321638
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)."""
1639+
"""Whether a prim carries a fixed-tendon representation."""
16341640
if prim.GetTypeName() == "MjcTendon":
16351641
return True
1636-
return any("PhysxTendonAxisRootAPI" in s for s in prim.GetAppliedSchemas())
1642+
fixed_types = {"PhysxTendonAxisRootAPI", "PhysxTendonAxisAPI"}
1643+
return any(
1644+
_applied_schema_instance(schema, schema_type)
1645+
for schema in prim.GetAppliedSchemas()
1646+
for schema_type in fixed_types
1647+
)
16371648

16381649

16391650
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()
1643-
)
1651+
"""Whether a prim carries a spatial-tendon root instance."""
1652+
return any(_applied_schema_instance(schema, "PhysxTendonAttachmentRootAPI") for schema in prim.GetAppliedSchemas())
16441653

16451654

16461655
def apply_fixed_tendon_properties(
@@ -1651,8 +1660,8 @@ def apply_fixed_tendon_properties(
16511660
The prims to author on are matched with :func:`~isaaclab.sim.utils.find_matching_prims`:
16521661
``prim_path_expr`` is a plain regular expression over whole prim paths, so ``[^/]+``
16531662
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.
1663+
fixed-tendon target when it carries an applied ``PhysxTendonAxisRootAPI`` or
1664+
``PhysxTendonAxisAPI`` instance, or is a ``MjcTendon`` prim.
16561665
16571666
Fixed tendons are a *tune-not-apply* family: the tendon topology is authored in the source
16581667
asset, so this writer never creates instances -- it only dispatches each fragment via its
@@ -1740,27 +1749,33 @@ def modify_fixed_tendon_properties(
17401749
# check if prim has fixed tendon applied on it or if the mjc tendon prim exiss
17411750
applied_schemas = tendon_prim.GetAppliedSchemas()
17421751
prim_type = tendon_prim.GetTypeName()
1743-
if not any("PhysxTendonAxisRootAPI" in s for s in applied_schemas) and prim_type != "MjcTendon":
1752+
if (
1753+
not any(_applied_schema_instance(schema, "PhysxTendonAxisRootAPI") for schema in applied_schemas)
1754+
and prim_type != "MjcTendon"
1755+
):
17441756
return False
17451757

17461758
# resolve all available instances of the schema since it is multi-instance
17471759
cfg = cfg.to_dict()
17481760
if prim_type != "MjcTendon":
17491761
for schema_name in applied_schemas:
1750-
if "PhysxTendonAxisRootAPI" not in schema_name:
1762+
instance_name = _applied_schema_instance(schema_name, "PhysxTendonAxisRootAPI")
1763+
if instance_name is None:
17511764
continue
1752-
# set into PhysX API by attribute prefix schema_name: (e.g. PhysxTendonAxisRootAPI:default:stiffness)
17531765
for attr_name, value in cfg.items():
1766+
template = f"physxTendon:__INSTANCE_NAME__:{to_camel_case(attr_name, 'cC')}"
1767+
attribute = Usd.SchemaRegistry.MakeMultipleApplyNameInstance(template, instance_name)
17541768
safe_set_attribute_on_usd_prim(
17551769
tendon_prim,
1756-
f"{schema_name}:{to_camel_case(attr_name, 'cC')}",
1770+
attribute,
17571771
value,
17581772
camel_case=False,
17591773
)
17601774
else:
17611775
# NOTE: ``mjc:*`` branch (``MjcTendon`` prim) kept inline; future split candidate into isaaclab_newton.
17621776
# only stiffness and damping in the cfg map to mjc attributes
1763-
for attr_name, value in cfg.items():
1777+
for attr_name in ("stiffness", "damping"):
1778+
value = cfg.get(attr_name)
17641779
safe_set_attribute_on_usd_prim(
17651780
tendon_prim, f"mjc:{to_camel_case(attr_name, 'cC')}", value, camel_case=False
17661781
)
@@ -1781,8 +1796,7 @@ def apply_spatial_tendon_properties(
17811796
The prims to author on are matched with :func:`~isaaclab.sim.utils.find_matching_prims`:
17821797
``prim_path_expr`` is a plain regular expression over whole prim paths, so ``[^/]+``
17831798
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.
1799+
spatial-tendon target when it carries an applied ``PhysxTendonAttachmentRootAPI`` instance.
17861800
17871801
Spatial tendons are a *tune-not-apply* family: the tendon topology is authored in the
17881802
source asset, so this writer never creates instances -- it only dispatches each fragment
@@ -1837,16 +1851,14 @@ def modify_spatial_tendon_properties(
18371851
through length and limit constraints. For instance, it can be used to set up an equality constraint
18381852
between a driven and passive revolute joints.
18391853
1840-
The schema comprises of attributes that belong to the `PhysxTendonAxisRootAPI`_ schema.
1854+
The schema comprises attributes that belong to the `PhysxTendonAttachmentRootAPI`_ schema.
18411855
18421856
.. note::
18431857
This function is decorated with :func:`apply_nested` that sets the properties to all the prims
18441858
(that have the schema applied on them) under the input prim path.
18451859
18461860
.. _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
18481861
.. _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
18501862
18511863
Args:
18521864
prim_path: The prim path to the tendon attachment.
@@ -1870,20 +1882,21 @@ def modify_spatial_tendon_properties(
18701882
tendon_prim = stage.GetPrimAtPath(prim_path)
18711883
# check if prim has spatial tendon applied on it
18721884
applied_schemas = tendon_prim.GetAppliedSchemas()
1873-
has_spatial = any(
1874-
"PhysxTendonAttachmentRootAPI" in s or "PhysxTendonAttachmentLeafAPI" in s for s in applied_schemas
1875-
)
1885+
has_spatial = any(_applied_schema_instance(schema, "PhysxTendonAttachmentRootAPI") for schema in applied_schemas)
18761886
if not has_spatial:
18771887
return False
18781888

18791889
cfg = cfg.to_dict()
18801890
for schema_name in applied_schemas:
1881-
if "PhysxTendonAttachmentRootAPI" not in schema_name and "PhysxTendonAttachmentLeafAPI" not in schema_name:
1891+
instance_name = _applied_schema_instance(schema_name, "PhysxTendonAttachmentRootAPI")
1892+
if instance_name is None:
18821893
continue
18831894
for attr_name, value in cfg.items():
1895+
template = f"physxTendon:__INSTANCE_NAME__:{to_camel_case(attr_name, 'cC')}"
1896+
attribute = Usd.SchemaRegistry.MakeMultipleApplyNameInstance(template, instance_name)
18841897
safe_set_attribute_on_usd_prim(
18851898
tendon_prim,
1886-
f"{schema_name}:{to_camel_case(attr_name, 'cC')}",
1899+
attribute,
18871900
value,
18881901
camel_case=False,
18891902
)

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

source/isaaclab/test/sim/test_schemas.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -934,13 +934,13 @@ def test_defining_articulation_properties_on_prim(setup_simulation):
934934

935935
@pytest.mark.isaacsim_ci
936936
def test_multi_instance_schema_detection_on_tendon_joints(setup_simulation):
937-
"""Test that multi-instance PhysX tendon schemas are correctly detected via substring matching.
937+
"""Test that multi-instance PhysX tendon schema tokens are recognized with their instance suffixes.
938938
939939
Multi-instance schemas (e.g. PhysxTendonAxisAPI, PhysxTendonAxisRootAPI) appear in
940940
GetAppliedSchemas() as 'SchemaName:instanceName' (e.g. 'PhysxTendonAxisAPI:inst0').
941941
An exact ``in list`` check fails because 'PhysxTendonAxisAPI' != 'PhysxTendonAxisAPI:inst0'.
942-
This test ensures the substring-based detection used by modify_joint_drive_properties
943-
and modify_fixed_tendon_properties handles multi-instance schemas correctly.
942+
This test ensures both the joint-drive skip predicate and the fixed-tendon writer handle
943+
multiple-apply schema tokens correctly.
944944
945945
We call the unwrapped functions directly (via ``__wrapped__``) to bypass the
946946
``@apply_nested`` decorator, which traverses children and does not return the

0 commit comments

Comments
 (0)