Skip to content
Open
32 changes: 29 additions & 3 deletions source/isaaclab/docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,38 @@ Changelog
0.54.4 (2026-02-13)
~~~~~~~~~~~~~~~~~~~

Changed
^^^^^^^

* Refactored :class:`~isaaclab.utils.wrench_composer.WrenchComposer` to a dual-buffer architecture with separate
global (world-frame) and local (body-frame) buffers. A new
:meth:`~isaaclab.utils.wrench_composer.WrenchComposer.compose_to_body_frame` method rotates global forces/torques into
the body frame at apply time using the current body orientation, then sums with local forces/torques. Composed
wrenches are now applied to PhysX with ``is_global=False``.
* Replaced the ``add_forces_and_torques_at_position`` and ``set_forces_and_torques_at_position`` warp kernels with
``set_forces_to_dual_buffers``, ``add_forces_to_dual_buffers``, ``add_raw_wrench_buffers``, and
``compose_wrench_to_body_frame``.

Fixed
^^^^^

* Fixed :class:`~isaaclab.utils.wrench_composer.WrenchComposer` to correctly handle composed wrenches when link poses
change during simulation. Forces and torques are composed and stored in "mixed" frame representation (global frame
orientation, link frame position) and passed to Physx with `is_global=True`.
* Fixed :class:`~isaaclab.utils.wrench_composer.WrenchComposer` not correctly updating the composed torque from global
positional forces when the body moves. Global positional torques are now stored as ``cross(P, F)`` about the world
origin and corrected at compose time via ``-cross(link_pos, F)`` to produce torque about the current CoM.
* Fixed :meth:`~isaaclab.utils.wrench_composer.WrenchComposer.reset` not clearing the ``_active`` flag when called
with ``slice(None)`` (the path taken by all asset reset methods). Added a ``ValueError`` for unsupported arbitrary
slices.
* Fixed :class:`~isaaclab.utils.wrench_composer.WrenchComposer` producing spurious torque when global forces are
applied without explicit positions. Previously, the compose kernel always applied the ``-cross(link_pos, F)``
correction, causing torque proportional to the body's distance from the world origin. Global forces without
positions are now routed to a separate ``global_force_at_com_w`` buffer that bypasses the positional torque
correction, correctly applying the force at the body's center of mass.

Added
^^^^^

* Added integration tests for :class:`~isaaclab.utils.wrench_composer.WrenchComposer` that validate global and local
force/torque behavior with a full physics simulation in the loop.


0.54.3 (2026-02-04)
Expand Down
52 changes: 24 additions & 28 deletions source/isaaclab/isaaclab/assets/articulation/articulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,30 +228,23 @@ def write_data_to_sim(self):
# write external wrench
if self._instantaneous_wrench_composer.active or self._permanent_wrench_composer.active:
if self._instantaneous_wrench_composer.active:
# Compose instantaneous wrench with permanent wrench
self._instantaneous_wrench_composer.add_forces_and_torques(
forces=self._permanent_wrench_composer.composed_force,
torques=self._permanent_wrench_composer.composed_torque,
body_ids=self._ALL_BODY_INDICES_WP,
env_ids=self._ALL_INDICES_WP,
is_global=True,
)
# Apply both instantaneous and permanent wrench to the simulation
self._instantaneous_wrench_composer.add_raw_buffers_from(self._permanent_wrench_composer)
self._instantaneous_wrench_composer.compose_to_body_frame()
self.root_physx_view.apply_forces_and_torques_at_position(
force_data=self._instantaneous_wrench_composer.composed_force_as_torch.view(-1, 3),
torque_data=self._instantaneous_wrench_composer.composed_torque_as_torch.view(-1, 3),
force_data=self._instantaneous_wrench_composer.out_force_b_as_torch.view(-1, 3),
torque_data=self._instantaneous_wrench_composer.out_torque_b_as_torch.view(-1, 3),
position_data=None,
indices=self._ALL_INDICES,
is_global=True,
is_global=False,
)
else:
# Apply permanent wrench to the simulation
self._permanent_wrench_composer.compose_to_body_frame()
self.root_physx_view.apply_forces_and_torques_at_position(
force_data=self._permanent_wrench_composer.composed_force_as_torch.view(-1, 3),
torque_data=self._permanent_wrench_composer.composed_torque_as_torch.view(-1, 3),
force_data=self._permanent_wrench_composer.out_force_b_as_torch.view(-1, 3),
torque_data=self._permanent_wrench_composer.out_torque_b_as_torch.view(-1, 3),
position_data=None,
indices=self._ALL_INDICES,
is_global=True,
is_global=False,
)
self._instantaneous_wrench_composer.reset()

Expand Down Expand Up @@ -1018,27 +1011,30 @@ def set_external_force_and_torque(
external wrench at (in the local link frame of the bodies).

.. caution::
If the function is called with empty forces and torques, then this function disables the application
of external wrench to the simulation.

.. code-block:: python

# example of disabling external wrench
asset.set_external_force_and_torque(forces=torch.zeros(0, 3), torques=torch.zeros(0, 3))
This method clears **all** internal wrench buffers before writing the provided values.
Any previously set forces or torques (local or global) are discarded. To accumulate
on top of existing values, use ``permanent_wrench_composer.add_forces_and_torques`` instead.

.. note::
This function does not apply the external wrench to the simulation. It only fills the buffers with
the desired values. To apply the external wrench, call the :meth:`write_data_to_sim` function
right before the simulation step.

Args:
forces: External forces in bodies' local frame. Shape is (len(env_ids), len(body_ids), 3).
torques: External torques in bodies' local frame. Shape is (len(env_ids), len(body_ids), 3).
positions: Positions to apply external wrench. Shape is (len(env_ids), len(body_ids), 3). Defaults to None.
forces: External forces. Shape is (len(env_ids), len(body_ids), 3).
When ``is_global=False``, forces are in the bodies' local frame.
When ``is_global=True``, forces are in the world frame.
torques: External torques. Shape is (len(env_ids), len(body_ids), 3).
When ``is_global=False``, torques are in the bodies' local frame.
When ``is_global=True``, torques are in the world frame.
positions: Application points for forces. Shape is (len(env_ids), len(body_ids), 3).
Defaults to None.
When ``is_global=False``, positions are local offsets from the link frame.
When ``is_global=True``, positions are world-frame coordinates. If None,
forces are applied at the body's center of mass (no positional torque).
body_ids: Body indices to apply external wrench to. Defaults to None (all bodies).
env_ids: Environment indices to apply external wrench to. Defaults to None (all instances).
is_global: Whether to apply the external wrench in the global frame. Defaults to False. If set to False,
the external wrench is applied in the link frame of the articulations' bodies.
is_global: Whether forces and torques are in the global (world) frame. Defaults to False.
"""
logger.warning(
"The function 'set_external_force_and_torque' will be deprecated in a future release. Please"
Expand Down
51 changes: 23 additions & 28 deletions source/isaaclab/isaaclab/assets/rigid_object/rigid_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,30 +141,23 @@ def write_data_to_sim(self):
# write external wrench
if self._instantaneous_wrench_composer.active or self._permanent_wrench_composer.active:
if self._instantaneous_wrench_composer.active:
# Compose instantaneous wrench with permanent wrench
self._instantaneous_wrench_composer.add_forces_and_torques(
forces=self._permanent_wrench_composer.composed_force,
torques=self._permanent_wrench_composer.composed_torque,
body_ids=self._ALL_BODY_INDICES_WP,
env_ids=self._ALL_INDICES_WP,
is_global=True,
)
# Apply both instantaneous and permanent wrench to the simulation
self._instantaneous_wrench_composer.add_raw_buffers_from(self._permanent_wrench_composer)
self._instantaneous_wrench_composer.compose_to_body_frame()
self.root_physx_view.apply_forces_and_torques_at_position(
force_data=self._instantaneous_wrench_composer.composed_force_as_torch.view(-1, 3),
torque_data=self._instantaneous_wrench_composer.composed_torque_as_torch.view(-1, 3),
force_data=self._instantaneous_wrench_composer.out_force_b_as_torch.view(-1, 3),
torque_data=self._instantaneous_wrench_composer.out_torque_b_as_torch.view(-1, 3),
position_data=None,
indices=self._ALL_INDICES,
is_global=True,
is_global=False,
)
else:
# Apply permanent wrench to the simulation
self._permanent_wrench_composer.compose_to_body_frame()
self.root_physx_view.apply_forces_and_torques_at_position(
force_data=self._permanent_wrench_composer.composed_force_as_torch.view(-1, 3),
torque_data=self._permanent_wrench_composer.composed_torque_as_torch.view(-1, 3),
force_data=self._permanent_wrench_composer.out_force_b_as_torch.view(-1, 3),
torque_data=self._permanent_wrench_composer.out_torque_b_as_torch.view(-1, 3),
position_data=None,
indices=self._ALL_INDICES,
is_global=True,
is_global=False,
)
self._instantaneous_wrench_composer.reset()

Expand Down Expand Up @@ -417,28 +410,30 @@ def set_external_force_and_torque(
external wrench at (in the local link frame of the bodies).

.. caution::
If the function is called with empty forces and torques, then this function disables the application
of external wrench to the simulation.

.. code-block:: python

# example of disabling external wrench
asset.set_external_force_and_torque(forces=torch.zeros(0, 3), torques=torch.zeros(0, 3))
This method clears **all** internal wrench buffers before writing the provided values.
Any previously set forces or torques (local or global) are discarded. To accumulate
on top of existing values, use ``permanent_wrench_composer.add_forces_and_torques`` instead.

.. note::
This function does not apply the external wrench to the simulation. It only fills the buffers with
the desired values. To apply the external wrench, call the :meth:`write_data_to_sim` function
right before the simulation step.

Args:
forces: External forces in bodies' local frame. Shape is (len(env_ids), len(body_ids), 3).
torques: External torques in bodies' local frame. Shape is (len(env_ids), len(body_ids), 3).
positions: External wrench positions in bodies' local frame. Shape is (len(env_ids), len(body_ids), 3).
forces: External forces. Shape is (len(env_ids), len(body_ids), 3).
When ``is_global=False``, forces are in the bodies' local frame.
When ``is_global=True``, forces are in the world frame.
torques: External torques. Shape is (len(env_ids), len(body_ids), 3).
When ``is_global=False``, torques are in the bodies' local frame.
When ``is_global=True``, torques are in the world frame.
positions: Application points for forces. Shape is (len(env_ids), len(body_ids), 3).
Defaults to None.
When ``is_global=False``, positions are local offsets from the link frame.
When ``is_global=True``, positions are world-frame coordinates. If None,
forces are applied at the body's center of mass (no positional torque).
body_ids: Body indices to apply external wrench to. Defaults to None (all bodies).
env_ids: Environment indices to apply external wrench to. Defaults to None (all instances).
is_global: Whether to apply the external wrench in the global frame. Defaults to False. If set to False,
the external wrench is applied in the link frame of the bodies.
is_global: Whether forces and torques are in the global (world) frame. Defaults to False.
"""
logger.warning(
"The function 'set_external_force_and_torque' will be deprecated in a future release. Please"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,30 +185,23 @@ def write_data_to_sim(self):
# write external wrench
if self._instantaneous_wrench_composer.active or self._permanent_wrench_composer.active:
if self._instantaneous_wrench_composer.active:
# Compose instantaneous wrench with permanent wrench
self._instantaneous_wrench_composer.add_forces_and_torques(
forces=self._permanent_wrench_composer.composed_force,
torques=self._permanent_wrench_composer.composed_torque,
body_ids=self._ALL_OBJ_INDICES_WP,
env_ids=self._ALL_ENV_INDICES_WP,
is_global=True,
)
# Apply both instantaneous and permanent wrench to the simulation
self._instantaneous_wrench_composer.add_raw_buffers_from(self._permanent_wrench_composer)
self._instantaneous_wrench_composer.compose_to_body_frame()
self.root_physx_view.apply_forces_and_torques_at_position(
force_data=self.reshape_data_to_view(self._instantaneous_wrench_composer.composed_force_as_torch),
torque_data=self.reshape_data_to_view(self._instantaneous_wrench_composer.composed_torque_as_torch),
force_data=self.reshape_data_to_view(self._instantaneous_wrench_composer.out_force_b_as_torch),
torque_data=self.reshape_data_to_view(self._instantaneous_wrench_composer.out_torque_b_as_torch),
position_data=None,
indices=self._env_obj_ids_to_view_ids(self._ALL_ENV_INDICES, self._ALL_OBJ_INDICES),
is_global=True,
is_global=False,
)
else:
# Apply permanent wrench to the simulation
self._permanent_wrench_composer.compose_to_body_frame()
self.root_physx_view.apply_forces_and_torques_at_position(
force_data=self.reshape_data_to_view(self._permanent_wrench_composer.composed_force_as_torch),
torque_data=self.reshape_data_to_view(self._permanent_wrench_composer.composed_torque_as_torch),
force_data=self.reshape_data_to_view(self._permanent_wrench_composer.out_force_b_as_torch),
torque_data=self.reshape_data_to_view(self._permanent_wrench_composer.out_torque_b_as_torch),
position_data=None,
indices=self._env_obj_ids_to_view_ids(self._ALL_ENV_INDICES, self._ALL_OBJ_INDICES),
is_global=True,
is_global=False,
)
self._instantaneous_wrench_composer.reset()

Expand Down Expand Up @@ -527,27 +520,30 @@ def set_external_force_and_torque(
into buffers which are then applied to the simulation at every step.

.. caution::
If the function is called with empty forces and torques, then this function disables the application
of external wrench to the simulation.

.. code-block:: python

# example of disabling external wrench
asset.set_external_force_and_torque(forces=torch.zeros(0, 0, 3), torques=torch.zeros(0, 0, 3))
This method clears **all** internal wrench buffers before writing the provided values.
Any previously set forces or torques (local or global) are discarded. To accumulate
on top of existing values, use ``permanent_wrench_composer.add_forces_and_torques`` instead.

.. note::
This function does not apply the external wrench to the simulation. It only fills the buffers with
the desired values. To apply the external wrench, call the :meth:`write_data_to_sim` function
right before the simulation step.

Args:
forces: External forces in bodies' local frame. Shape is (len(env_ids), len(object_ids), 3).
torques: External torques in bodies' local frame. Shape is (len(env_ids), len(object_ids), 3).
positions: External wrench positions in bodies' local frame. Shape is (len(env_ids), len(object_ids), 3).
forces: External forces. Shape is (len(env_ids), len(object_ids), 3).
When ``is_global=False``, forces are in the bodies' local frame.
When ``is_global=True``, forces are in the world frame.
torques: External torques. Shape is (len(env_ids), len(object_ids), 3).
When ``is_global=False``, torques are in the bodies' local frame.
When ``is_global=True``, torques are in the world frame.
positions: Application points for forces. Shape is (len(env_ids), len(object_ids), 3).
Defaults to None.
When ``is_global=False``, positions are local offsets from the link frame.
When ``is_global=True``, positions are world-frame coordinates. If None,
forces are applied at the body's center of mass (no positional torque).
object_ids: Object indices to apply external wrench to. Defaults to None (all objects).
env_ids: Environment indices to apply external wrench to. Defaults to None (all instances).
is_global: Whether to apply the external wrench in the global frame. Defaults to False. If set to False,
the external wrench is applied in the link frame of the bodies.
is_global: Whether forces and torques are in the global (world) frame. Defaults to False.
"""
logger.warning(
"The function 'set_external_force_and_torque' will be deprecated in a future release. Please"
Expand Down
Loading