Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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
22 changes: 22 additions & 0 deletions docs/source/api/lab/isaaclab.sensors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
MultiMeshRayCasterCameraCfg
Imu
ImuCfg
JointWrenchSensor
JointWrenchSensorData
JointWrenchSensorCfg

Sensor Base
-----------
Expand Down Expand Up @@ -189,3 +192,22 @@ Inertia Measurement Unit
:inherited-members:
:show-inheritance:
:exclude-members: __init__, class_type

Joint Wrench Sensor
-------------------

.. autoclass:: JointWrenchSensor
:members:
:inherited-members:
:show-inheritance:

.. autoclass:: JointWrenchSensorData
:members:
:inherited-members:
:exclude-members: __init__

.. autoclass:: JointWrenchSensorCfg
:members:
:inherited-members:
:show-inheritance:
:exclude-members: __init__, class_type
74 changes: 74 additions & 0 deletions docs/source/migration/migrating_to_isaaclab_3-0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ The following sensor classes also remain in the ``isaaclab`` package with unchan
- :class:`~isaaclab.sensors.Imu`, :class:`~isaaclab.sensors.ImuCfg`, :class:`~isaaclab.sensors.ImuData`
- :class:`~isaaclab.sensors.Pva`, :class:`~isaaclab.sensors.PvaCfg`, :class:`~isaaclab.sensors.PvaData`
- :class:`~isaaclab.sensors.FrameTransformer`, :class:`~isaaclab.sensors.FrameTransformerCfg`, :class:`~isaaclab.sensors.FrameTransformerData`
- :class:`~isaaclab.sensors.JointWrenchSensor`, :class:`~isaaclab.sensors.JointWrenchSensorCfg`,
:class:`~isaaclab.sensors.JointWrenchSensorData`

These sensor classes now use factory patterns that automatically instantiate the appropriate backend
implementation (PhysX by default), maintaining full backward compatibility.
Expand All @@ -179,6 +181,7 @@ you can import from ``isaaclab_physx.sensors``:
from isaaclab_physx.sensors import Imu, ImuData
from isaaclab_physx.sensors import Pva, PvaData
from isaaclab_physx.sensors import FrameTransformer, FrameTransformerData
from isaaclab_physx.sensors import JointWrenchSensor, JointWrenchSensorData


New ``isaaclab_newton`` Extension
Expand All @@ -188,6 +191,8 @@ A new extension ``isaaclab_newton`` provides Newton physics backend implementati

- :class:`~isaaclab_newton.assets.Articulation` and :class:`~isaaclab_newton.assets.ArticulationData`
- :class:`~isaaclab_newton.assets.RigidObject` and :class:`~isaaclab_newton.assets.RigidObjectData`
- :class:`~isaaclab_newton.sensors.JointWrenchSensor` and
:class:`~isaaclab_newton.sensors.JointWrenchSensorData`

These classes implement the same base interfaces as their PhysX counterparts
(:class:`~isaaclab.assets.BaseArticulation`, :class:`~isaaclab.assets.BaseRigidObject`),
Expand Down Expand Up @@ -331,6 +336,75 @@ If you need to track sensor poses in world frame, please use a dedicated sensor
sensor_quat = frame_transformer.data.target_quat_w


Articulation Joint Wrench Data Moved to ``JointWrenchSensor``
-------------------------------------------------------------

The ``ArticulationData.body_incoming_joint_wrench_b`` property has been removed. In
Isaac Lab 3.0, incoming joint reaction wrenches are exposed through
:class:`~isaaclab.sensors.JointWrenchSensor`, which has PhysX and Newton backend
implementations and returns separate force [N] and torque [N·m] buffers.
The sensor reports wrenches in the child-side incoming joint frame, with torque
referenced at the child-side joint anchor.

**Before (Isaac Lab 2.x):**

.. code-block:: python

wrench_b = robot.data.body_incoming_joint_wrench_b.torch[:, body_ids]

**After (Isaac Lab 3.x):**

.. code-block:: python

import torch
from isaaclab.scene import InteractiveSceneCfg
from isaaclab.sensors import JointWrenchSensorCfg

class MySceneCfg(InteractiveSceneCfg):
robot = ROBOT_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
joint_wrench = JointWrenchSensorCfg(prim_path="{ENV_REGEX_NS}/Robot")

sensor = env.scene.sensors["joint_wrench"]
data = sensor.data
wrench_j = torch.cat(
(
data.force.torch[:, body_ids],
data.torque.torch[:, body_ids],
),
dim=-1,
)

Use :attr:`~isaaclab.sensors.BaseJointWrenchSensor.body_names` or
:meth:`~isaaclab.sensors.BaseJointWrenchSensor.find_bodies` to map sensor entries to
articulation body names. PhysX reports one entry for every link, including the articulation
root link. Newton reports the child bodies of reportable incoming joints.

For manager-based environments, update observations that used the articulation data property to
depend on the joint-wrench sensor instead:

.. code-block:: python

import isaaclab.envs.mdp as mdp
from isaaclab.managers import SceneEntityCfg
from isaaclab.managers import ObservationTermCfg as ObsTerm
from isaaclab.scene import InteractiveSceneCfg
from isaaclab.sensors import JointWrenchSensorCfg

class MySceneCfg(InteractiveSceneCfg):
robot = ROBOT_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
joint_wrench = JointWrenchSensorCfg(prim_path="{ENV_REGEX_NS}/Robot")

feet_body_forces = ObsTerm(
func=mdp.body_incoming_wrench,
params={
"sensor_cfg": SceneEntityCfg(
"joint_wrench",
body_names=["left_foot", "right_foot"],
)
},
)


Multi-Backend Support: PresetCfg Pattern
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion source/isaaclab/config/extension.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]

# Note: Semantic Versioning is used: https://semver.org/
version = "4.6.22"
version = "4.6.24"

# Description
title = "Isaac Lab framework for Robot Learning"
Expand Down
29 changes: 29 additions & 0 deletions source/isaaclab/docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,35 @@
Changelog
---------

4.6.24 (2026-04-30)
~~~~~~~~~~~~~~~~~~~

Changed
^^^^^^^

* Changed :func:`~isaaclab.envs.mdp.body_incoming_wrench` to read from
:class:`~isaaclab.sensors.JointWrenchSensor`. Pass
``sensor_cfg=SceneEntityCfg("joint_wrench", body_names=...)`` instead of an
articulation asset config.

Removed
^^^^^^^

* Removed ``BaseArticulationData.body_incoming_joint_wrench_b``.
Add :class:`~isaaclab.sensors.JointWrenchSensorCfg` to the scene and read
:attr:`~isaaclab.sensors.JointWrenchSensorData.force` and
:attr:`~isaaclab.sensors.JointWrenchSensorData.torque` instead.


4.6.23 (2026-04-30)
~~~~~~~~~~~~~~~~~~~

Added
^^^^^

* Added :class:`~isaaclab.sensors.JointWrenchSensor`.


4.6.22 (2026-04-27)
~~~~~~~~~~~~~~~~~~~

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -603,23 +603,6 @@ def body_com_pose_b(self) -> ProxyArray:
"""
raise NotImplementedError

@property
@abstractmethod
def body_incoming_joint_wrench_b(self) -> ProxyArray:
"""Joint reaction wrench applied from body parent to child body in parent body frame.

Shape is (num_instances, num_bodies), dtype = wp.spatial_vectorf. In torch this resolves to
(num_instances, num_bodies, 6). All body reaction wrenches are provided including the root body to the
world of an articulation.

For more information on joint wrenches, please check the `PhysX documentation`_ and the
underlying `PhysX Tensor API`_.

.. _PhysX documentation: https://nvidia-omniverse.github.io/PhysX/physx/5.5.1/docs/Articulations.html#link-incoming-joint-force
.. _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
"""
raise NotImplementedError

##
# Joint state properties.
##
Expand Down
21 changes: 13 additions & 8 deletions source/isaaclab/isaaclab/envs/mdp/observations.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
if TYPE_CHECKING:
from isaaclab.assets import Articulation, RigidObject
from isaaclab.envs import ManagerBasedEnv, ManagerBasedRLEnv
from isaaclab.sensors import Camera, Imu, Pva, RayCaster, RayCasterCamera
from isaaclab.sensors import Camera, Imu, JointWrenchSensor, Pva, RayCaster, RayCasterCamera

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


def body_incoming_wrench(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg) -> torch.Tensor:
"""Incoming spatial wrench on bodies of an articulation in the simulation world frame.
def body_incoming_wrench(env: ManagerBasedEnv, sensor_cfg: SceneEntityCfg) -> torch.Tensor:
"""Incoming spatial wrench [N, N·m] on bodies of an articulation in the sensor convention.

This is the 6-D wrench (force and torque) applied to the body link by the incoming joint force.
This is the 6-D wrench (force followed by torque) applied to the body link by the incoming joint force.
"""
# extract the used quantities (to enable type-hinting)
asset: Articulation = env.scene[asset_cfg.name]
# obtain the link incoming forces in world frame
body_incoming_joint_wrench_b = asset.data.body_incoming_joint_wrench_b.torch[:, asset_cfg.body_ids]
return body_incoming_joint_wrench_b.view(env.num_envs, -1)
sensor: JointWrenchSensor = env.scene.sensors[sensor_cfg.name]
sensor_data = sensor.data
force_data = sensor_data.force
torque_data = sensor_data.torque
if force_data is None or torque_data is None:
raise RuntimeError("Joint wrench sensor data is not initialized. Call sim.reset() before reading observations.")
force = force_data.torch[:, sensor_cfg.body_ids]
torque = torque_data.torch[:, sensor_cfg.body_ids]
return torch.cat((force, torque), dim=-1).view(env.num_envs, -1)


def pva_orientation(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("pva")) -> torch.Tensor:
Expand Down
12 changes: 12 additions & 0 deletions source/isaaclab/isaaclab/sensors/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ __all__ = [
"Imu",
"ImuCfg",
"ImuData",
"BaseJointWrenchSensor",
"BaseJointWrenchSensorData",
"JointWrenchSensor",
"JointWrenchSensorCfg",
"JointWrenchSensorData",
"BasePva",
"BasePvaData",
"Pva",
Expand Down Expand Up @@ -83,6 +88,13 @@ from .frame_transformer import (
FrameTransformerData,
)
from .imu import BaseImu, BaseImuData, Imu, ImuCfg, ImuData
from .joint_wrench import (
BaseJointWrenchSensor,
BaseJointWrenchSensorData,
JointWrenchSensor,
JointWrenchSensorCfg,
JointWrenchSensorData,
)
from .pva import BasePva, BasePvaData, Pva, PvaCfg, PvaData
from .ray_caster import (
MultiMeshRayCaster,
Expand Down
10 changes: 10 additions & 0 deletions source/isaaclab/isaaclab/sensors/joint_wrench/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

"""Joint Wrench Sensor."""

from isaaclab.utils.module import lazy_export

lazy_export()
18 changes: 18 additions & 0 deletions source/isaaclab/isaaclab/sensors/joint_wrench/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

__all__ = [
"BaseJointWrenchSensor",
"BaseJointWrenchSensorData",
"JointWrenchSensor",
"JointWrenchSensorCfg",
"JointWrenchSensorData",
]

from .base_joint_wrench_sensor import BaseJointWrenchSensor
from .base_joint_wrench_sensor_data import BaseJointWrenchSensorData
from .joint_wrench_sensor import JointWrenchSensor
from .joint_wrench_sensor_cfg import JointWrenchSensorCfg
from .joint_wrench_sensor_data import JointWrenchSensorData
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

from __future__ import annotations

from abc import abstractmethod
from collections.abc import Sequence
from typing import TYPE_CHECKING

import warp as wp

import isaaclab.utils.string as string_utils

from ..sensor_base import SensorBase
from .base_joint_wrench_sensor_data import BaseJointWrenchSensorData

if TYPE_CHECKING:
from .joint_wrench_sensor_cfg import JointWrenchSensorCfg


class BaseJointWrenchSensor(SensorBase):
"""The joint reaction wrench sensor.

Reports incoming joint wrenches for the bodies selected by the backend as
split force [N] / torque [N·m] pairs expressed in the
``INCOMING_JOINT_FRAME`` convention. Use :attr:`body_names` or
:meth:`find_bodies` to map entries to articulation bodies.
"""

cfg: JointWrenchSensorCfg
"""The configuration parameters."""

__backend_name__: str = "base"
"""The name of the backend for the joint wrench sensor."""

def __init__(self, cfg: JointWrenchSensorCfg):
"""Initialize the joint wrench sensor.

Args:
cfg: The configuration parameters.
"""
super().__init__(cfg)

"""
Properties
"""

@property
@abstractmethod
def data(self) -> BaseJointWrenchSensorData:
"""The sensor data container, populated after simulation initialization."""
raise NotImplementedError

@property
@abstractmethod
def body_names(self) -> list[str]:
"""Ordered names of the bodies whose incoming joint wrench is reported."""
raise NotImplementedError

@property
def num_bodies(self) -> int:
"""Number of bodies whose incoming joint wrench is reported."""
return len(self.body_names)

"""
Operations
"""

def find_bodies(self, name_keys: str | Sequence[str], preserve_order: bool = False) -> tuple[list[int], list[str]]:
"""Find reported bodies based on name keys.

Args:
name_keys: A regular expression or list of regular expressions to match the body names.
preserve_order: Whether to preserve the order of the name keys in the output. Defaults to False.

Returns:
The matching body indices and names.
"""
return string_utils.resolve_matching_names(name_keys, self.body_names, preserve_order)

"""
Implementation - Abstract methods to be implemented by backend-specific subclasses.
"""

@abstractmethod
def _initialize_impl(self) -> None:
"""Initialize the sensor handles and internal buffers.

Subclasses should call ``super()._initialize_impl()`` first to
initialize the common sensor infrastructure from
:class:`~isaaclab.sensors.SensorBase`.
"""
super()._initialize_impl()

@abstractmethod
def _update_buffers_impl(self, env_mask: wp.array) -> None:
raise NotImplementedError
Loading
Loading