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
6 changes: 6 additions & 0 deletions docs/source/api/lab/isaaclab.sensors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
Imu
ImuCfg
JointWrenchSensor
JointWrenchSensorData
JointWrenchSensorCfg

Sensor Base
Expand Down Expand Up @@ -200,6 +201,11 @@ Joint Wrench Sensor
:inherited-members:
:show-inheritance:

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

.. autoclass:: JointWrenchSensorCfg
:members:
:inherited-members:
Expand Down
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
15 changes: 15 additions & 0 deletions source/isaaclab/changelog.d/pr-5458-merge-develop.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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.
1 change: 0 additions & 1 deletion source/isaaclab/docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
Changelog
---------


4.6.27 (2026-05-01)
~~~~~~~~~~~~~~~~~~~

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

@property
@abstractmethod
@leapp_tensor_semantics(kind=InputKindEnum.WRENCH)
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
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

Expand All @@ -20,11 +23,12 @@
class BaseJointWrenchSensor(SensorBase):
"""The joint reaction wrench sensor.

Reports the incoming joint wrench on each joint of an articulation as a
split force [N] / torque [N·m] pair expressed in the
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 (child-side joint frame, child-side
joint anchor reference point). Backends convert from their native
representation to this convention internally.
representation to this convention internally. Use :attr:`body_names` or
:meth:`find_bodies` to map entries to articulation bodies.
"""

cfg: JointWrenchSensorCfg
Expand Down Expand Up @@ -57,6 +61,27 @@ 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.
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ def force(self) -> ProxyArray | None:

Expressed in the frame selected by
:attr:`~isaaclab.sensors.JointWrenchSensorCfg.convention`. Shape is
``(num_envs, num_joints)``, dtype ``wp.vec3f``. In torch this resolves
to ``(num_envs, num_joints, 3)``. ``None`` before the simulation is
``(num_envs, num_bodies)``, dtype ``wp.vec3f``. In torch this resolves
to ``(num_envs, num_bodies, 3)``. ``None`` before the simulation is
initialized.
"""
raise NotImplementedError
Expand All @@ -35,8 +35,8 @@ def torque(self) -> ProxyArray | None:

Expressed in the frame selected by
:attr:`~isaaclab.sensors.JointWrenchSensorCfg.convention`. Shape is
``(num_envs, num_joints)``, dtype ``wp.vec3f``. In torch this resolves
to ``(num_envs, num_joints, 3)``. ``None`` before the simulation is
``(num_envs, num_bodies)``, dtype ``wp.vec3f``. In torch this resolves
to ``(num_envs, num_bodies, 3)``. ``None`` before the simulation is
initialized.
"""
raise NotImplementedError
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
if TYPE_CHECKING:
from isaaclab_newton.sensors.joint_wrench import JointWrenchSensor as NewtonJointWrenchSensor
from isaaclab_newton.sensors.joint_wrench import JointWrenchSensorData as NewtonJointWrenchSensorData
from isaaclab_physx.sensors.joint_wrench import JointWrenchSensor as PhysXJointWrenchSensor
from isaaclab_physx.sensors.joint_wrench import JointWrenchSensorData as PhysXJointWrenchSensorData


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

data: BaseJointWrenchSensorData | NewtonJointWrenchSensorData
data: BaseJointWrenchSensorData | PhysXJointWrenchSensorData | NewtonJointWrenchSensorData

def __new__(cls, *args, **kwargs) -> BaseJointWrenchSensor | NewtonJointWrenchSensor:
def __new__(cls, *args, **kwargs) -> BaseJointWrenchSensor | PhysXJointWrenchSensor | NewtonJointWrenchSensor:
"""Create a new instance of a joint-wrench sensor based on the backend."""
return super().__new__(cls, *args, **kwargs)
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ class JointWrenchSensorCfg(SensorBaseCfg):
"""Coordinate convention for the reported wrench. Defaults to ``"incoming_joint_frame"``.

- ``"incoming_joint_frame"`` — child-side joint frame, child-side joint anchor as reference point.
Matches what a real 6-axis F/T sensor mounted at the joint would measure. This is the same
as PhysX convention in IsaacLab2.3
Matches what a real 6-axis F/T sensor mounted at the joint would measure. Backends convert
their native solver outputs to this convention.
"""
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@

if TYPE_CHECKING:
from isaaclab_newton.sensors.joint_wrench import JointWrenchSensorData as NewtonJointWrenchSensorData
from isaaclab_physx.sensors.joint_wrench import JointWrenchSensorData as PhysXJointWrenchSensorData


class JointWrenchSensorData(FactoryBase, BaseJointWrenchSensorData):
"""Factory for creating joint-wrench sensor data instances."""

def __new__(cls, *args, **kwargs) -> BaseJointWrenchSensorData | NewtonJointWrenchSensorData:
def __new__(
cls, *args, **kwargs
) -> BaseJointWrenchSensorData | PhysXJointWrenchSensorData | NewtonJointWrenchSensorData:
"""Create a new instance of joint-wrench sensor data based on the backend."""
return super().__new__(cls, *args, **kwargs)
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ def __init__(
# Body properties
self._body_mass: wp.array | None = None
self._body_inertia: wp.array | None = None
self._body_incoming_joint_wrench_b: wp.array | None = None

# Tendon properties (fixed)
self._fixed_tendon_stiffness: wp.array | None = None
Expand Down Expand Up @@ -187,7 +186,6 @@ def __init__(
self._body_com_pose_b_ta: ProxyArray | None = None
self._body_mass_ta: ProxyArray | None = None
self._body_inertia_ta: ProxyArray | None = None
self._body_incoming_joint_wrench_b_ta: ProxyArray | None = None
self._fixed_tendon_stiffness_ta: ProxyArray | None = None
self._fixed_tendon_damping_ta: ProxyArray | None = None
self._fixed_tendon_limit_stiffness_ta: ProxyArray | None = None
Expand Down Expand Up @@ -830,18 +828,6 @@ def body_inertia(self) -> ProxyArray:
self._body_inertia_ta = ProxyArray(self._body_inertia)
return self._body_inertia_ta

@property
def body_incoming_joint_wrench_b(self) -> ProxyArray:
"""Body incoming joint wrenches. dtype=wp.spatial_vectorf, shape: (N, num_bodies)."""
if self._body_incoming_joint_wrench_b is None:
self._body_incoming_joint_wrench_b = wp.zeros(
(self._num_instances, self._num_bodies, 6), dtype=wp.float32, device=self.device
).view(wp.spatial_vectorf)
self._body_incoming_joint_wrench_b_ta = None
if self._body_incoming_joint_wrench_b_ta is None:
self._body_incoming_joint_wrench_b_ta = ProxyArray(self._body_incoming_joint_wrench_b)
return self._body_incoming_joint_wrench_b_ta

# -- Derived properties --

@property
Expand Down Expand Up @@ -1176,10 +1162,6 @@ def set_body_inertia(self, value: torch.Tensor) -> None:
self._body_inertia = wp.from_torch(value.to(self.device).contiguous())
self._body_inertia_ta = None

def set_body_incoming_joint_wrench_b(self, value: torch.Tensor) -> None:
self._body_incoming_joint_wrench_b = wp.from_torch(value.to(self.device).contiguous())
self._body_incoming_joint_wrench_b_ta = None

def set_fixed_tendon_stiffness(self, value: torch.Tensor) -> None:
self._fixed_tendon_stiffness = wp.from_torch(value.to(self.device).contiguous())
self._fixed_tendon_stiffness_ta = None
Expand Down
Loading
Loading