Skip to content

Commit 7361ece

Browse files
Add PhysX joint wrench sensor (#5458)
# Description Adds the PhysX backend for `JointWrenchSensor`. This PR is built on top of #5412. Until that PR lands, this comparison also includes the Newton joint-wrench sensor commits from #5412. The PhysX sensor creates a PhysX articulation view and reads incoming joint wrenches from `ArticulationView.get_link_incoming_joint_force()`. It then converts the raw PhysX body1-frame, body-origin wrench into the shared `incoming_joint_frame` convention: child-side joint frame with torque referenced at the child-side joint anchor. PhysX and Newton joint-wrench sensors also resolve nested USD articulation roots before creating their articulation views so asset-level paths such as `{ENV_REGEX_NS}/Robot` work for classic Ant/Humanoid assets. This also removes `ArticulationData.body_incoming_joint_wrench_b` from the articulation data APIs, migrates manager/direct environments that used it to `JointWrenchSensor`, and documents the Isaac Lab 3.0 migration path. Classic Ant/Humanoid Newton presets now use the same wrench observations as PhysX and pass short RSL-RL training smokes. @camevor can you review? Fixes # (issue) N/A ## Type of change - New feature (non-breaking change which adds functionality) - Breaking change (existing functionality will not work without user modification) - Documentation update ## Screenshots N/A ## Tests - `./isaaclab.sh -f` - `docker exec isaac-lab-base bash -lc "cd /workspace/isaaclab && ./isaaclab.sh -p -m pytest source/isaaclab_physx/test/sensors/test_joint_wrench_sensor.py"` - 11 passed - `docker exec isaac-lab-base bash -lc "cd /workspace/isaaclab && ./isaaclab.sh -p -m pytest source/isaaclab_newton/test/sensors/test_joint_wrench_sensor.py"` - 10 passed - `docker exec isaac-lab-base bash -lc "cd /workspace/isaaclab && ./isaaclab.sh -p -m pytest source/isaaclab_tasks/test/test_environments.py -k 'Isaac-Ant-v0 or Isaac-Humanoid-v0'"` - 4 passed - `docker exec isaac-lab-base bash -lc "cd /workspace/isaaclab && ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task Isaac-Ant-v0 --num_envs 16 --max_iterations 1 --headless --device cuda presets=newton agent.num_steps_per_env=8"` - passed - `docker exec isaac-lab-base bash -lc "cd /workspace/isaaclab && ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task Isaac-Humanoid-v0 --num_envs 16 --max_iterations 1 --headless --device cuda presets=newton agent.num_steps_per_env=8"` - passed - `docker exec isaac-lab-base bash -lc "cd /workspace/isaaclab && ./isaaclab.sh -p -m pytest source/isaaclab/test/test_mock_interfaces/test_mock_data_properties.py"` - 253 passed ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Signed-off-by: camevor <camevor@nvidia.com> Co-authored-by: camevor <camevor@nvidia.com>
1 parent 2644c1e commit 7361ece

38 files changed

Lines changed: 1137 additions & 433 deletions

File tree

docs/source/api/lab/isaaclab.sensors.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
Imu
3838
ImuCfg
3939
JointWrenchSensor
40+
JointWrenchSensorData
4041
JointWrenchSensorCfg
4142

4243
Sensor Base
@@ -200,6 +201,11 @@ Joint Wrench Sensor
200201
:inherited-members:
201202
:show-inheritance:
202203

204+
.. autoclass:: JointWrenchSensorData
205+
:members:
206+
:inherited-members:
207+
:exclude-members: __init__
208+
203209
.. autoclass:: JointWrenchSensorCfg
204210
:members:
205211
:inherited-members:

docs/source/migration/migrating_to_isaaclab_3-0.rst

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ The following sensor classes also remain in the ``isaaclab`` package with unchan
158158
- :class:`~isaaclab.sensors.Imu`, :class:`~isaaclab.sensors.ImuCfg`, :class:`~isaaclab.sensors.ImuData`
159159
- :class:`~isaaclab.sensors.Pva`, :class:`~isaaclab.sensors.PvaCfg`, :class:`~isaaclab.sensors.PvaData`
160160
- :class:`~isaaclab.sensors.FrameTransformer`, :class:`~isaaclab.sensors.FrameTransformerCfg`, :class:`~isaaclab.sensors.FrameTransformerData`
161+
- :class:`~isaaclab.sensors.JointWrenchSensor`, :class:`~isaaclab.sensors.JointWrenchSensorCfg`,
162+
:class:`~isaaclab.sensors.JointWrenchSensorData`
161163

162164
These sensor classes now use factory patterns that automatically instantiate the appropriate backend
163165
implementation (PhysX by default), maintaining full backward compatibility.
@@ -179,6 +181,7 @@ you can import from ``isaaclab_physx.sensors``:
179181
from isaaclab_physx.sensors import Imu, ImuData
180182
from isaaclab_physx.sensors import Pva, PvaData
181183
from isaaclab_physx.sensors import FrameTransformer, FrameTransformerData
184+
from isaaclab_physx.sensors import JointWrenchSensor, JointWrenchSensorData
182185
183186
184187
New ``isaaclab_newton`` Extension
@@ -188,6 +191,8 @@ A new extension ``isaaclab_newton`` provides Newton physics backend implementati
188191

189192
- :class:`~isaaclab_newton.assets.Articulation` and :class:`~isaaclab_newton.assets.ArticulationData`
190193
- :class:`~isaaclab_newton.assets.RigidObject` and :class:`~isaaclab_newton.assets.RigidObjectData`
194+
- :class:`~isaaclab_newton.sensors.JointWrenchSensor` and
195+
:class:`~isaaclab_newton.sensors.JointWrenchSensorData`
191196

192197
These classes implement the same base interfaces as their PhysX counterparts
193198
(:class:`~isaaclab.assets.BaseArticulation`, :class:`~isaaclab.assets.BaseRigidObject`),
@@ -331,6 +336,75 @@ If you need to track sensor poses in world frame, please use a dedicated sensor
331336
sensor_quat = frame_transformer.data.target_quat_w
332337
333338
339+
Articulation Joint Wrench Data Moved to ``JointWrenchSensor``
340+
-------------------------------------------------------------
341+
342+
The ``ArticulationData.body_incoming_joint_wrench_b`` property has been removed. In
343+
Isaac Lab 3.0, incoming joint reaction wrenches are exposed through
344+
:class:`~isaaclab.sensors.JointWrenchSensor`, which has PhysX and Newton backend
345+
implementations and returns separate force [N] and torque [N·m] buffers.
346+
The sensor reports wrenches in the child-side incoming joint frame, with torque
347+
referenced at the child-side joint anchor.
348+
349+
**Before (Isaac Lab 2.x):**
350+
351+
.. code-block:: python
352+
353+
wrench_b = robot.data.body_incoming_joint_wrench_b.torch[:, body_ids]
354+
355+
**After (Isaac Lab 3.x):**
356+
357+
.. code-block:: python
358+
359+
import torch
360+
from isaaclab.scene import InteractiveSceneCfg
361+
from isaaclab.sensors import JointWrenchSensorCfg
362+
363+
class MySceneCfg(InteractiveSceneCfg):
364+
robot = ROBOT_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
365+
joint_wrench = JointWrenchSensorCfg(prim_path="{ENV_REGEX_NS}/Robot")
366+
367+
sensor = env.scene.sensors["joint_wrench"]
368+
data = sensor.data
369+
wrench_j = torch.cat(
370+
(
371+
data.force.torch[:, body_ids],
372+
data.torque.torch[:, body_ids],
373+
),
374+
dim=-1,
375+
)
376+
377+
Use :attr:`~isaaclab.sensors.BaseJointWrenchSensor.body_names` or
378+
:meth:`~isaaclab.sensors.BaseJointWrenchSensor.find_bodies` to map sensor entries to
379+
articulation body names. PhysX reports one entry for every link, including the articulation
380+
root link. Newton reports the child bodies of reportable incoming joints.
381+
382+
For manager-based environments, update observations that used the articulation data property to
383+
depend on the joint-wrench sensor instead:
384+
385+
.. code-block:: python
386+
387+
import isaaclab.envs.mdp as mdp
388+
from isaaclab.managers import SceneEntityCfg
389+
from isaaclab.managers import ObservationTermCfg as ObsTerm
390+
from isaaclab.scene import InteractiveSceneCfg
391+
from isaaclab.sensors import JointWrenchSensorCfg
392+
393+
class MySceneCfg(InteractiveSceneCfg):
394+
robot = ROBOT_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
395+
joint_wrench = JointWrenchSensorCfg(prim_path="{ENV_REGEX_NS}/Robot")
396+
397+
feet_body_forces = ObsTerm(
398+
func=mdp.body_incoming_wrench,
399+
params={
400+
"sensor_cfg": SceneEntityCfg(
401+
"joint_wrench",
402+
body_names=["left_foot", "right_foot"],
403+
)
404+
},
405+
)
406+
407+
334408
Multi-Backend Support: PresetCfg Pattern
335409
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
336410

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Changed
2+
^^^^^^^
3+
4+
* Changed :func:`~isaaclab.envs.mdp.body_incoming_wrench` to read from
5+
:class:`~isaaclab.sensors.JointWrenchSensor`. Pass
6+
``sensor_cfg=SceneEntityCfg("joint_wrench", body_names=...)`` instead of an
7+
articulation asset config.
8+
9+
Removed
10+
^^^^^^^
11+
12+
* Removed ``BaseArticulationData.body_incoming_joint_wrench_b``. Add
13+
:class:`~isaaclab.sensors.JointWrenchSensorCfg` to the scene and read
14+
:attr:`~isaaclab.sensors.JointWrenchSensorData.force` and
15+
:attr:`~isaaclab.sensors.JointWrenchSensorData.torque` instead.

source/isaaclab/docs/CHANGELOG.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
Changelog
22
---------
33

4-
54
4.6.27 (2026-05-01)
65
~~~~~~~~~~~~~~~~~~~
76

source/isaaclab/isaaclab/assets/articulation/base_articulation_data.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -664,24 +664,6 @@ def body_com_pose_b(self) -> ProxyArray:
664664
"""
665665
raise NotImplementedError
666666

667-
@property
668-
@abstractmethod
669-
@leapp_tensor_semantics(kind=InputKindEnum.WRENCH)
670-
def body_incoming_joint_wrench_b(self) -> ProxyArray:
671-
"""Joint reaction wrench applied from body parent to child body in parent body frame.
672-
673-
Shape is (num_instances, num_bodies), dtype = wp.spatial_vectorf. In torch this resolves to
674-
(num_instances, num_bodies, 6). All body reaction wrenches are provided including the root body to the
675-
world of an articulation.
676-
677-
For more information on joint wrenches, please check the `PhysX documentation`_ and the
678-
underlying `PhysX Tensor API`_.
679-
680-
.. _PhysX documentation: https://nvidia-omniverse.github.io/PhysX/physx/5.5.1/docs/Articulations.html#link-incoming-joint-force
681-
.. _PhysX Tensor API: https://docs.omniverse.nvidia.com/kit/docs/omni_physics/latest/extensions/runtime/source/omni.physics.tensors/docs/api/python.html#omni.physics.tensors.api.ArticulationView.get_link_incoming_joint_force
682-
"""
683-
raise NotImplementedError
684-
685667
##
686668
# Joint state properties.
687669
##

source/isaaclab/isaaclab/envs/mdp/observations.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
if TYPE_CHECKING:
2424
from isaaclab.assets import Articulation, RigidObject
2525
from isaaclab.envs import ManagerBasedEnv, ManagerBasedRLEnv
26-
from isaaclab.sensors import Camera, Imu, Pva, RayCaster, RayCasterCamera
26+
from isaaclab.sensors import Camera, Imu, JointWrenchSensor, Pva, RayCaster, RayCasterCamera
2727

2828
from isaaclab.envs.utils.io_descriptors import (
2929
generic_io_descriptor,
@@ -304,16 +304,21 @@ def height_scan(env: ManagerBasedEnv, sensor_cfg: SceneEntityCfg, offset: float
304304
return sensor.data.pos_w.torch[:, 2].unsqueeze(1) - sensor.data.ray_hits_w.torch[..., 2] - offset
305305

306306

307-
def body_incoming_wrench(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg) -> torch.Tensor:
308-
"""Incoming spatial wrench on bodies of an articulation in the simulation world frame.
307+
def body_incoming_wrench(env: ManagerBasedEnv, sensor_cfg: SceneEntityCfg) -> torch.Tensor:
308+
"""Incoming spatial wrench [N, N·m] on bodies of an articulation in the sensor convention.
309309
310-
This is the 6-D wrench (force and torque) applied to the body link by the incoming joint force.
310+
This is the 6-D wrench (force followed by torque) applied to the body link by the incoming joint force.
311311
"""
312312
# extract the used quantities (to enable type-hinting)
313-
asset: Articulation = env.scene[asset_cfg.name]
314-
# obtain the link incoming forces in world frame
315-
body_incoming_joint_wrench_b = asset.data.body_incoming_joint_wrench_b.torch[:, asset_cfg.body_ids]
316-
return body_incoming_joint_wrench_b.view(env.num_envs, -1)
313+
sensor: JointWrenchSensor = env.scene.sensors[sensor_cfg.name]
314+
sensor_data = sensor.data
315+
force_data = sensor_data.force
316+
torque_data = sensor_data.torque
317+
if force_data is None or torque_data is None:
318+
raise RuntimeError("Joint wrench sensor data is not initialized. Call sim.reset() before reading observations.")
319+
force = force_data.torch[:, sensor_cfg.body_ids]
320+
torque = torque_data.torch[:, sensor_cfg.body_ids]
321+
return torch.cat((force, torque), dim=-1).view(env.num_envs, -1)
317322

318323

319324
def pva_orientation(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("pva")) -> torch.Tensor:

source/isaaclab/isaaclab/sensors/joint_wrench/base_joint_wrench_sensor.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66
from __future__ import annotations
77

88
from abc import abstractmethod
9+
from collections.abc import Sequence
910
from typing import TYPE_CHECKING
1011

1112
import warp as wp
1213

14+
import isaaclab.utils.string as string_utils
15+
1316
from ..sensor_base import SensorBase
1417
from .base_joint_wrench_sensor_data import BaseJointWrenchSensorData
1518

@@ -20,11 +23,12 @@
2023
class BaseJointWrenchSensor(SensorBase):
2124
"""The joint reaction wrench sensor.
2225
23-
Reports the incoming joint wrench on each joint of an articulation as a
24-
split force [N] / torque [N·m] pair expressed in the
26+
Reports incoming joint wrenches for the bodies selected by the backend as
27+
split force [N] / torque [N·m] pairs expressed in the
2528
``INCOMING_JOINT_FRAME`` convention (child-side joint frame, child-side
2629
joint anchor reference point). Backends convert from their native
27-
representation to this convention internally.
30+
representation to this convention internally. Use :attr:`body_names` or
31+
:meth:`find_bodies` to map entries to articulation bodies.
2832
"""
2933

3034
cfg: JointWrenchSensorCfg
@@ -57,6 +61,27 @@ def body_names(self) -> list[str]:
5761
"""Ordered names of the bodies whose incoming joint wrench is reported."""
5862
raise NotImplementedError
5963

64+
@property
65+
def num_bodies(self) -> int:
66+
"""Number of bodies whose incoming joint wrench is reported."""
67+
return len(self.body_names)
68+
69+
"""
70+
Operations
71+
"""
72+
73+
def find_bodies(self, name_keys: str | Sequence[str], preserve_order: bool = False) -> tuple[list[int], list[str]]:
74+
"""Find reported bodies based on name keys.
75+
76+
Args:
77+
name_keys: A regular expression or list of regular expressions to match the body names.
78+
preserve_order: Whether to preserve the order of the name keys in the output. Defaults to False.
79+
80+
Returns:
81+
The matching body indices and names.
82+
"""
83+
return string_utils.resolve_matching_names(name_keys, self.body_names, preserve_order)
84+
6085
"""
6186
Implementation - Abstract methods to be implemented by backend-specific subclasses.
6287
"""

source/isaaclab/isaaclab/sensors/joint_wrench/base_joint_wrench_sensor_data.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ def force(self) -> ProxyArray | None:
2222
2323
Expressed in the frame selected by
2424
:attr:`~isaaclab.sensors.JointWrenchSensorCfg.convention`. Shape is
25-
``(num_envs, num_joints)``, dtype ``wp.vec3f``. In torch this resolves
26-
to ``(num_envs, num_joints, 3)``. ``None`` before the simulation is
25+
``(num_envs, num_bodies)``, dtype ``wp.vec3f``. In torch this resolves
26+
to ``(num_envs, num_bodies, 3)``. ``None`` before the simulation is
2727
initialized.
2828
"""
2929
raise NotImplementedError
@@ -35,8 +35,8 @@ def torque(self) -> ProxyArray | None:
3535
3636
Expressed in the frame selected by
3737
:attr:`~isaaclab.sensors.JointWrenchSensorCfg.convention`. Shape is
38-
``(num_envs, num_joints)``, dtype ``wp.vec3f``. In torch this resolves
39-
to ``(num_envs, num_joints, 3)``. ``None`` before the simulation is
38+
``(num_envs, num_bodies)``, dtype ``wp.vec3f``. In torch this resolves
39+
to ``(num_envs, num_bodies, 3)``. ``None`` before the simulation is
4040
initialized.
4141
"""
4242
raise NotImplementedError

source/isaaclab/isaaclab/sensors/joint_wrench/joint_wrench_sensor.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
if TYPE_CHECKING:
1616
from isaaclab_newton.sensors.joint_wrench import JointWrenchSensor as NewtonJointWrenchSensor
1717
from isaaclab_newton.sensors.joint_wrench import JointWrenchSensorData as NewtonJointWrenchSensorData
18+
from isaaclab_physx.sensors.joint_wrench import JointWrenchSensor as PhysXJointWrenchSensor
19+
from isaaclab_physx.sensors.joint_wrench import JointWrenchSensorData as PhysXJointWrenchSensorData
1820

1921

2022
class JointWrenchSensor(FactoryBase, BaseJointWrenchSensor):
2123
"""Factory for creating joint-wrench sensor instances."""
2224

23-
data: BaseJointWrenchSensorData | NewtonJointWrenchSensorData
25+
data: BaseJointWrenchSensorData | PhysXJointWrenchSensorData | NewtonJointWrenchSensorData
2426

25-
def __new__(cls, *args, **kwargs) -> BaseJointWrenchSensor | NewtonJointWrenchSensor:
27+
def __new__(cls, *args, **kwargs) -> BaseJointWrenchSensor | PhysXJointWrenchSensor | NewtonJointWrenchSensor:
2628
"""Create a new instance of a joint-wrench sensor based on the backend."""
2729
return super().__new__(cls, *args, **kwargs)

source/isaaclab/isaaclab/sensors/joint_wrench/joint_wrench_sensor_cfg.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ class JointWrenchSensorCfg(SensorBaseCfg):
2525
"""Coordinate convention for the reported wrench. Defaults to ``"incoming_joint_frame"``.
2626
2727
- ``"incoming_joint_frame"`` — child-side joint frame, child-side joint anchor as reference point.
28-
Matches what a real 6-axis F/T sensor mounted at the joint would measure. This is the same
29-
as PhysX convention in IsaacLab2.3
28+
Matches what a real 6-axis F/T sensor mounted at the joint would measure. Backends convert
29+
their native solver outputs to this convention.
3030
"""

0 commit comments

Comments
 (0)