Skip to content

Commit 88d94ea

Browse files
AntoineRichardMayankm96kellyguo11
authored
Adds WrenchComposer to handle temporary/permanent wrenches (isaac-sim#3287)
# Description Adds the ability to compose forces onto rigid bodies, rigid body collections and articulations. This should help implement drones, boats, and satellites into the framework. ## Usage ```python # Permanent forces can now be composed: # Adding two forces in a single step on the same body asset.permanent_wrench_composer.set_forces_and_torques(forces=torch.ones(1, 1, 3), env_ids=[0], object_ids=[0]) # Compose local and global forces together asset.permanent_wrench_composer.add_forces_and_torques(forces=torch.ones(1, 1, 3), env_ids=[0], object_ids=[1], is_global=True) # Adding torques to the same body asset.permanent_wrench_composer.add_forces_and_torques(torques=torch.ones(1, 1, 3), env_ids=[0], object_ids=[0]) #Adding forces and torques to the same body asset.permanent_wrench_composer.add_forces_and_torques(forces=torch.ones(1, 1, 3), torques=torch.ones(1, 1, 3), env_ids=[0], object_ids=[0]) # Adding forces and torques to the same body with different positions asset.permanent_wrench_composer.add_forces_and_torques(forces=torch.ones(1, 1, 3), torques=torch.ones(1, 1, 3), env_ids=[0], object_ids=[0], positions=torch.ones(1, 1, 3)) # Adding forces and torques to the same body with different positions in the global frame. Note, it composes local and global wrenches seamlessly. asset.permanent_wrench_composer.add_forces_and_torques(forces=torch.ones(1, 1, 3), torques=torch.ones(1, 1, 3), env_ids=[0], object_ids=[0], positions=torch.ones(1, 1, 3), is_global=True) # We can now apply instantaneous wrenches that are only applied for a single simulation step: asset.instantaneous_wrench_composer.add_forces_and_torques(forces=torch.ones(1, 1, 3), env_ids=[0], object_ids=[0]) asset.instantaneous_wrench_composer.add_forces_and_torques(forces=torch.ones(1, 1, 3), env_ids=[0], object_ids=[0]) asset.instantaneous_wrench_composer.add_forces_and_torques(forces=torch.ones(1, 1, 3), env_ids=[0], object_ids=[0]) # The instantaneous wrenches and the permanent wrenches are composed automatically when the wrenches are written to the simulation. The instantaneous wrenches are reseted after being written to the sim. ``` Fixes isaac-sim#3286 ## Type of change - New feature (non-breaking change which adds functionality) - This change requires a documentation update ## Checklist - [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: Antoine RICHARD <antoiner@nvidia.com> Signed-off-by: Kelly Guo <kellyg@nvidia.com> Co-authored-by: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> Co-authored-by: Kelly Guo <kellyg@nvidia.com>
1 parent 3d42bff commit 88d94ea

15 files changed

Lines changed: 1778 additions & 299 deletions

File tree

scripts/demos/quadcopter.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,11 @@ def main():
101101
forces = torch.zeros(robot.num_instances, 4, 3, device=sim.device)
102102
torques = torch.zeros_like(forces)
103103
forces[..., 2] = robot_mass * gravity / 4.0
104-
robot.set_external_force_and_torque(forces, torques, body_ids=prop_body_ids)
104+
robot.permanent_wrench_composer.set_forces_and_torques(
105+
forces=forces,
106+
torques=torques,
107+
body_ids=prop_body_ids,
108+
)
105109
robot.write_data_to_sim()
106110
# perform step
107111
sim.step()

source/isaaclab/docs/CHANGELOG.rst

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

4+
0.53.2 (2026-01-14)
5+
~~~~~~~~~~~~~~~~~~~
6+
7+
Added
8+
^^^^^
9+
10+
* Added :class:`~isaaclab.assets.utils.wrench_composer.WrenchComposer` to compose forces and torques at the body's center of mass frame.
11+
* Added :meth:`~isaaclab.assets.Articulation.instantaneous_wrench_composer` to add or set instantaneous external wrenches to the articulation.
12+
* Added :meth:`~isaaclab.assets.Articulation.permanent_wrench_composer` to add or set permanent external wrenches to the articulation.
13+
* Added :meth:`~isaaclab.assets.RigidObject.instantaneous_wrench_composer` to add or set instantaneous external wrenches to the rigid object.
14+
* Added :meth:`~isaaclab.assets.RigidObject.permanent_wrench_composer` to add or set permanent external wrenches to the rigid object.
15+
* Added :meth:`~isaaclab.assets.RigidObjectCollection.instantaneous_wrench_composer` to add or set instantaneous external wrenches to the rigid object collection.
16+
* Added :meth:`~isaaclab.assets.RigidObjectCollection.permanent_wrench_composer` to add or set permanent external wrenches to the rigid object collection.
17+
* Added unit tests for the wrench composer.
18+
* Added kernels for the wrench composer in the :mod:`isaaclab.utils.warp.kernels` module.
19+
20+
Changed
21+
^^^^^^^
22+
23+
* Deprecated :meth:`~isaaclab.assets.Articulation.set_external_force_and_torque` in favor of :meth:`~isaaclab.assets.Articulation.permanent_wrench_composer.set_forces_and_torques`.
24+
* Deprecated :meth:`~isaaclab.assets.RigidObject.set_external_force_and_torque` in favor of :meth:`~isaaclab.assets.RigidObject.permanent_wrench_composer.set_forces_and_torques`.
25+
* Deprecated :meth:`~isaaclab.assets.RigidObjectCollection.set_external_force_and_torque` in favor of :meth:`~isaaclab.assets.RigidObjectCollection.permanent_wrench_composer.set_forces_and_torques`.
26+
* Modified the tests of the articulation, rigid object, and rigid object collection to use the new permanent and instantaneous external wrench functions and test them.
27+
428
0.53.1 (2026-01-08)
529
~~~~~~~~~~~~~~~~~~~
630

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

Lines changed: 85 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from typing import TYPE_CHECKING
1414

1515
import torch
16+
import warp as wp
1617
from prettytable import PrettyTable
1718

1819
import omni.physics.tensors.impl.api as physx
@@ -25,6 +26,7 @@
2526
from isaaclab.actuators import ActuatorBase, ActuatorBaseCfg, ImplicitActuator
2627
from isaaclab.utils.types import ArticulationActions
2728
from isaaclab.utils.version import get_isaac_sim_version
29+
from isaaclab.utils.wrench_composer import WrenchComposer
2830

2931
from ..asset_base import AssetBase
3032
from .articulation_data import ArticulationData
@@ -169,6 +171,35 @@ def root_physx_view(self) -> physx.ArticulationView:
169171
"""
170172
return self._root_physx_view
171173

174+
@property
175+
def instantaneous_wrench_composer(self) -> WrenchComposer:
176+
"""Instantaneous wrench composer.
177+
178+
Returns a :class:`~isaaclab.utils.wrench_composer.WrenchComposer` instance. Wrenches added or set to this wrench
179+
composer are only valid for the current simulation step. At the end of the simulation step, the wrenches set
180+
to this object are discarded. This is useful to apply forces that change all the time, things like drag forces
181+
for instance.
182+
183+
Note:
184+
Permanent wrenches are composed into the instantaneous wrench before the instantaneous wrenches are
185+
applied to the simulation.
186+
"""
187+
return self._instantaneous_wrench_composer
188+
189+
@property
190+
def permanent_wrench_composer(self) -> WrenchComposer:
191+
"""Permanent wrench composer.
192+
193+
Returns a :class:`~isaaclab.utils.wrench_composer.WrenchComposer` instance. Wrenches added or set to this wrench
194+
composer are persistent and are applied to the simulation at every step. This is useful to apply forces that
195+
are constant over a period of time, things like the thrust of a motor for instance.
196+
197+
Note:
198+
Permanent wrenches are composed into the instantaneous wrench before the instantaneous wrenches are
199+
applied to the simulation.
200+
"""
201+
return self._permanent_wrench_composer
202+
172203
"""
173204
Operations.
174205
"""
@@ -180,10 +211,9 @@ def reset(self, env_ids: Sequence[int] | None = None):
180211
# reset actuators
181212
for actuator in self.actuators.values():
182213
actuator.reset(env_ids)
183-
# reset external wrench
184-
self._external_force_b[env_ids] = 0.0
185-
self._external_torque_b[env_ids] = 0.0
186-
self._external_wrench_positions_b[env_ids] = 0.0
214+
# reset external wrenches.
215+
self._instantaneous_wrench_composer.reset(env_ids)
216+
self._permanent_wrench_composer.reset(env_ids)
187217

188218
def write_data_to_sim(self):
189219
"""Write external wrenches and joint commands to the simulation.
@@ -196,23 +226,33 @@ def write_data_to_sim(self):
196226
This ensures that the external wrench is applied at every simulation step.
197227
"""
198228
# write external wrench
199-
if self.has_external_wrench:
200-
if self.uses_external_wrench_positions:
229+
if self._instantaneous_wrench_composer.active or self._permanent_wrench_composer.active:
230+
if self._instantaneous_wrench_composer.active:
231+
# Compose instantaneous wrench with permanent wrench
232+
self._instantaneous_wrench_composer.add_forces_and_torques(
233+
forces=self._permanent_wrench_composer.composed_force,
234+
torques=self._permanent_wrench_composer.composed_torque,
235+
body_ids=self._ALL_BODY_INDICES_WP,
236+
env_ids=self._ALL_INDICES_WP,
237+
)
238+
# Apply both instantaneous and permanent wrench to the simulation
201239
self.root_physx_view.apply_forces_and_torques_at_position(
202-
force_data=self._external_force_b.view(-1, 3),
203-
torque_data=self._external_torque_b.view(-1, 3),
204-
position_data=self._external_wrench_positions_b.view(-1, 3),
240+
force_data=self._instantaneous_wrench_composer.composed_force_as_torch.view(-1, 3),
241+
torque_data=self._instantaneous_wrench_composer.composed_torque_as_torch.view(-1, 3),
242+
position_data=None,
205243
indices=self._ALL_INDICES,
206-
is_global=self._use_global_wrench_frame,
244+
is_global=False,
207245
)
208246
else:
247+
# Apply permanent wrench to the simulation
209248
self.root_physx_view.apply_forces_and_torques_at_position(
210-
force_data=self._external_force_b.view(-1, 3),
211-
torque_data=self._external_torque_b.view(-1, 3),
249+
force_data=self._permanent_wrench_composer.composed_force_as_torch.view(-1, 3),
250+
torque_data=self._permanent_wrench_composer.composed_torque_as_torch.view(-1, 3),
212251
position_data=None,
213252
indices=self._ALL_INDICES,
214-
is_global=self._use_global_wrench_frame,
253+
is_global=False,
215254
)
255+
self._instantaneous_wrench_composer.reset()
216256

217257
# apply actuator models
218258
self._apply_actuator_model()
@@ -985,18 +1025,6 @@ def set_external_force_and_torque(
9851025
# example of disabling external wrench
9861026
asset.set_external_force_and_torque(forces=torch.zeros(0, 3), torques=torch.zeros(0, 3))
9871027
988-
.. caution::
989-
If the function is called consecutively with and with different values for ``is_global``, then the
990-
all the external wrenches will be applied in the frame specified by the last call.
991-
992-
.. code-block:: python
993-
994-
# example of setting external wrench in the global frame
995-
asset.set_external_force_and_torque(forces=torch.ones(1, 1, 3), env_ids=[0], is_global=True)
996-
# example of setting external wrench in the link frame
997-
asset.set_external_force_and_torque(forces=torch.ones(1, 1, 3), env_ids=[1], is_global=False)
998-
# Both environments will have the external wrenches applied in the link frame
999-
10001028
.. note::
10011029
This function does not apply the external wrench to the simulation. It only fills the buffers with
10021030
the desired values. To apply the external wrench, call the :meth:`write_data_to_sim` function
@@ -1011,53 +1039,42 @@ def set_external_force_and_torque(
10111039
is_global: Whether to apply the external wrench in the global frame. Defaults to False. If set to False,
10121040
the external wrench is applied in the link frame of the articulations' bodies.
10131041
"""
1014-
if forces.any() or torques.any():
1015-
self.has_external_wrench = True
1016-
else:
1017-
self.has_external_wrench = False
1042+
logger.warning(
1043+
"The function 'set_external_force_and_torque' will be deprecated in a future release. Please"
1044+
" use 'permanent_wrench_composer.set_forces_and_torques' instead."
1045+
)
1046+
if forces is None and torques is None:
1047+
logger.warning("No forces or torques provided. No permanent external wrench will be applied.")
10181048

10191049
# resolve all indices
10201050
# -- env_ids
10211051
if env_ids is None:
1022-
env_ids = self._ALL_INDICES
1052+
env_ids = self._ALL_INDICES_WP
10231053
elif not isinstance(env_ids, torch.Tensor):
1024-
env_ids = torch.tensor(env_ids, dtype=torch.long, device=self.device)
1054+
env_ids = wp.array(env_ids, dtype=wp.int32, device=self.device)
1055+
else:
1056+
env_ids = wp.from_torch(env_ids.to(torch.int32), dtype=wp.int32)
10251057
# -- body_ids
10261058
if body_ids is None:
1027-
body_ids = torch.arange(self.num_bodies, dtype=torch.long, device=self.device)
1059+
body_ids = self._ALL_BODY_INDICES_WP
10281060
elif isinstance(body_ids, slice):
1029-
body_ids = torch.arange(self.num_bodies, dtype=torch.long, device=self.device)[body_ids]
1030-
elif not isinstance(body_ids, torch.Tensor):
1031-
body_ids = torch.tensor(body_ids, dtype=torch.long, device=self.device)
1032-
1033-
# note: we need to do this complicated indexing since torch doesn't support multi-indexing
1034-
# create global body indices from env_ids and env_body_ids
1035-
# (env_id * total_bodies_per_env) + body_id
1036-
indices = body_ids.repeat(len(env_ids), 1) + env_ids.unsqueeze(1) * self.num_bodies
1037-
indices = indices.view(-1)
1038-
# set into internal buffers
1039-
# note: these are applied in the write_to_sim function
1040-
self._external_force_b.flatten(0, 1)[indices] = forces.flatten(0, 1)
1041-
self._external_torque_b.flatten(0, 1)[indices] = torques.flatten(0, 1)
1042-
1043-
if is_global != self._use_global_wrench_frame:
1044-
logger.warning(
1045-
f"The external wrench frame has been changed from {self._use_global_wrench_frame} to {is_global}. This"
1046-
" may lead to unexpected behavior."
1061+
body_ids = wp.from_torch(
1062+
torch.arange(self.num_bodies, dtype=torch.int32, device=self.device)[body_ids], dtype=wp.int32
10471063
)
1048-
self._use_global_wrench_frame = is_global
1049-
1050-
# If the positions are not provided, the behavior and performance of the simulation should not be affected.
1051-
if positions is not None:
1052-
# Generates a flag that is set for a full simulation step. This is done to avoid discarding
1053-
# the external wrench positions when multiple calls to this functions are made with and without positions.
1054-
self.uses_external_wrench_positions = True
1055-
self._external_wrench_positions_b.flatten(0, 1)[indices] = positions.flatten(0, 1)
1064+
elif not isinstance(body_ids, torch.Tensor):
1065+
body_ids = wp.array(body_ids, dtype=wp.int32, device=self.device)
10561066
else:
1057-
# If the positions are not provided, and the flag is set, then we need to ensure
1058-
# that the desired positions are zeroed.
1059-
if self.uses_external_wrench_positions:
1060-
self._external_wrench_positions_b.flatten(0, 1)[indices] = 0.0
1067+
body_ids = wp.from_torch(body_ids.to(torch.int32), dtype=wp.int32)
1068+
1069+
# Write to wrench composer
1070+
self._permanent_wrench_composer.set_forces_and_torques(
1071+
forces=wp.from_torch(forces, dtype=wp.vec3f) if forces is not None else None,
1072+
torques=wp.from_torch(torques, dtype=wp.vec3f) if torques is not None else None,
1073+
positions=wp.from_torch(positions, dtype=wp.vec3f) if positions is not None else None,
1074+
body_ids=body_ids,
1075+
env_ids=env_ids,
1076+
is_global=is_global,
1077+
)
10611078

10621079
def set_joint_position_target(
10631080
self, target: torch.Tensor, joint_ids: Sequence[int] | slice | None = None, env_ids: Sequence[int] | None = None
@@ -1576,14 +1593,13 @@ def _initialize_impl(self):
15761593
def _create_buffers(self):
15771594
# constants
15781595
self._ALL_INDICES = torch.arange(self.num_instances, dtype=torch.long, device=self.device)
1596+
self._ALL_BODY_INDICES = torch.arange(self.num_bodies, dtype=torch.long, device=self.device)
1597+
self._ALL_INDICES_WP = wp.from_torch(self._ALL_INDICES.to(torch.int32), dtype=wp.int32)
1598+
self._ALL_BODY_INDICES_WP = wp.from_torch(self._ALL_BODY_INDICES.to(torch.int32), dtype=wp.int32)
15791599

1580-
# external forces and torques
1581-
self.has_external_wrench = False
1582-
self.uses_external_wrench_positions = False
1583-
self._external_force_b = torch.zeros((self.num_instances, self.num_bodies, 3), device=self.device)
1584-
self._external_torque_b = torch.zeros_like(self._external_force_b)
1585-
self._external_wrench_positions_b = torch.zeros_like(self._external_force_b)
1586-
self._use_global_wrench_frame = False
1600+
# external wrench composer
1601+
self._instantaneous_wrench_composer = WrenchComposer(self)
1602+
self._permanent_wrench_composer = WrenchComposer(self)
15871603

15881604
# asset named data
15891605
self._data.joint_names = self.joint_names

0 commit comments

Comments
 (0)