Skip to content

Commit ce2ae92

Browse files
Adds position to set external forces and torques (#1680)
# Description This PR adds the option to set wrench positions to `set_external_force_and_torque`. This is a non-breaking change as the positions are passed as an optional argument. When no positions are set, the function defaults to the original implementation, that is, no positions are passed to PhysX. The PR also adds tests to check that the position values are correctly set into their buffer, but does not check if the resulting wrenches are correct. I did test the Quadcopter task before and after this PR and the training results are exactly the same. As of now, the function follows the original layout. But it could make sense to offer the option to set the position in either the link frame or the CoM frame. This would follow the recent changes made to the set_pose and set_velocity methods for instance. However, this would be a breaking change. Hence, for now, this has not been implemented. One could also argue, that this could be done prior to feeding the positions outside this method. Please let me know what you feel is best, and I'll update the PR accordingly. If one wanted to test the resulting wrenches, it would require a simple test articulation like a 1kg sphere that would be used to accurately compute the expected velocities. This is also feasible, but I feel like this test is more on the PhysX side of things, let me know. This change will require an update of the API documentation to include the position argument. Fixes #1678 ## 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` - [ ] 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 - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Co-authored-by: Kelly Guo <[email protected]>
1 parent 3beb2d2 commit ce2ae92

File tree

9 files changed

+436
-33
lines changed

9 files changed

+436
-33
lines changed

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

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ def reset(self, env_ids: Sequence[int] | None = None):
169169
# reset external wrench
170170
self._external_force_b[env_ids] = 0.0
171171
self._external_torque_b[env_ids] = 0.0
172+
self._external_wrench_positions_b[env_ids] = 0.0
172173

173174
def write_data_to_sim(self):
174175
"""Write external wrenches and joint commands to the simulation.
@@ -182,13 +183,22 @@ def write_data_to_sim(self):
182183
"""
183184
# write external wrench
184185
if self.has_external_wrench:
185-
self.root_physx_view.apply_forces_and_torques_at_position(
186-
force_data=self._external_force_b.view(-1, 3),
187-
torque_data=self._external_torque_b.view(-1, 3),
188-
position_data=None,
189-
indices=self._ALL_INDICES,
190-
is_global=False,
191-
)
186+
if self.uses_external_wrench_positions:
187+
self.root_physx_view.apply_forces_and_torques_at_position(
188+
force_data=self._external_force_b.view(-1, 3),
189+
torque_data=self._external_torque_b.view(-1, 3),
190+
position_data=self._external_wrench_positions_b.view(-1, 3),
191+
indices=self._ALL_INDICES,
192+
is_global=self._use_global_wrench_frame,
193+
)
194+
else:
195+
self.root_physx_view.apply_forces_and_torques_at_position(
196+
force_data=self._external_force_b.view(-1, 3),
197+
torque_data=self._external_torque_b.view(-1, 3),
198+
position_data=None,
199+
indices=self._ALL_INDICES,
200+
is_global=self._use_global_wrench_frame,
201+
)
192202

193203
# apply actuator models
194204
self._apply_actuator_model()
@@ -829,14 +839,16 @@ def set_external_force_and_torque(
829839
self,
830840
forces: torch.Tensor,
831841
torques: torch.Tensor,
842+
positions: torch.Tensor | None = None,
832843
body_ids: Sequence[int] | slice | None = None,
833844
env_ids: Sequence[int] | None = None,
834845
):
835846
"""Set external force and torque to apply on the asset's bodies in their local frame.
836847
837848
For many applications, we want to keep the applied external force on rigid bodies constant over a period of
838849
time (for instance, during the policy control). This function allows us to store the external force and torque
839-
into buffers which are then applied to the simulation at every step.
850+
into buffers which are then applied to the simulation at every step. Optionally, set the position to apply the
851+
external wrench at (in the local link frame of the bodies).
840852
841853
.. caution::
842854
If the function is called with empty forces and torques, then this function disables the application
@@ -855,6 +867,7 @@ def set_external_force_and_torque(
855867
Args:
856868
forces: External forces in bodies' local frame. Shape is (len(env_ids), len(body_ids), 3).
857869
torques: External torques in bodies' local frame. Shape is (len(env_ids), len(body_ids), 3).
870+
positions: Positions to apply external wrench. Shape is (len(env_ids), len(body_ids), 3). Defaults to None.
858871
body_ids: Body indices to apply external wrench to. Defaults to None (all bodies).
859872
env_ids: Environment indices to apply external wrench to. Defaults to None (all instances).
860873
"""
@@ -887,6 +900,17 @@ def set_external_force_and_torque(
887900
self._external_force_b.flatten(0, 1)[indices] = forces.flatten(0, 1)
888901
self._external_torque_b.flatten(0, 1)[indices] = torques.flatten(0, 1)
889902

903+
# If the positions are not provided, the behavior and performance of the simulation should not be affected.
904+
if positions is not None:
905+
# Generates a flag that is set for a full simulation step. This is done to avoid discarding
906+
# the external wrench positions when multiple calls to this functions are made with and without positions.
907+
self.uses_external_wrench_positions = True
908+
self._external_wrench_positions_b.flatten(0, 1)[indices] = positions.flatten(0, 1)
909+
else:
910+
# If the positions are not provided, and the flag is set, then we need to ensure that the desired positions are zeroed.
911+
if self.uses_external_wrench_positions:
912+
self._external_wrench_positions_b.flatten(0, 1)[indices] = 0.0
913+
890914
def set_joint_position_target(
891915
self, target: torch.Tensor, joint_ids: Sequence[int] | slice | None = None, env_ids: Sequence[int] | None = None
892916
):
@@ -1229,8 +1253,10 @@ def _create_buffers(self):
12291253

12301254
# external forces and torques
12311255
self.has_external_wrench = False
1256+
self.uses_external_wrench_positions = False
12321257
self._external_force_b = torch.zeros((self.num_instances, self.num_bodies, 3), device=self.device)
12331258
self._external_torque_b = torch.zeros_like(self._external_force_b)
1259+
self._external_wrench_positions_b = torch.zeros_like(self._external_force_b)
12341260

12351261
# asset named data
12361262
self._data.joint_names = self.joint_names
@@ -1298,6 +1324,15 @@ def _process_cfg(self):
12981324
default_root_state = torch.tensor(default_root_state, dtype=torch.float, device=self.device)
12991325
self._data.default_root_state = default_root_state.repeat(self.num_instances, 1)
13001326

1327+
# -- external wrench
1328+
external_wrench_frame = self.cfg.articulation_external_wrench_frame
1329+
if external_wrench_frame == "local":
1330+
self._use_global_wrench_frame = False
1331+
elif external_wrench_frame == "world":
1332+
self._use_global_wrench_frame = True
1333+
else:
1334+
raise ValueError(f"Invalid external wrench frame: {external_wrench_frame}. Must be 'local' or 'world'.")
1335+
13011336
# -- joint state
13021337
self._data.default_joint_pos = torch.zeros(self.num_instances, self.num_joints, device=self.device)
13031338
self._data.default_joint_vel = torch.zeros_like(self._data.default_joint_pos)

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ class InitialStateCfg(AssetBaseCfg.InitialStateCfg):
4444
If not provided will search for a prim with the ArticulationRootAPI. Should start with a slash.
4545
"""
4646

47+
articulation_external_wrench_frame: str = "local"
48+
"""Frame in which external wrenches are applied. Defaults to "local".
49+
50+
If "local", the external wrenches are applied in the local frame of the articulation root.
51+
If "world", the external wrenches are applied in the world frame.
52+
"""
53+
4754
init_state: InitialStateCfg = InitialStateCfg()
4855
"""Initial state of the articulated object. Defaults to identity pose with zero velocity and zero joint state."""
4956

source/isaaclab/isaaclab/assets/rigid_object/rigid_object.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ def reset(self, env_ids: Sequence[int] | None = None):
103103
# reset external wrench
104104
self._external_force_b[env_ids] = 0.0
105105
self._external_torque_b[env_ids] = 0.0
106+
self._external_wrench_positions_b[env_ids] = 0.0
106107

107108
def write_data_to_sim(self):
108109
"""Write external wrench to the simulation.
@@ -113,13 +114,22 @@ def write_data_to_sim(self):
113114
"""
114115
# write external wrench
115116
if self.has_external_wrench:
116-
self.root_physx_view.apply_forces_and_torques_at_position(
117-
force_data=self._external_force_b.view(-1, 3),
118-
torque_data=self._external_torque_b.view(-1, 3),
119-
position_data=None,
120-
indices=self._ALL_INDICES,
121-
is_global=False,
122-
)
117+
if self.uses_external_wrench_positions:
118+
self.root_physx_view.apply_forces_and_torques_at_position(
119+
force_data=self._external_force_b.view(-1, 3),
120+
torque_data=self._external_torque_b.view(-1, 3),
121+
position_data=self._external_wrench_positions_b.view(-1, 3),
122+
indices=self._ALL_INDICES,
123+
is_global=self._use_global_wrench_frame,
124+
)
125+
else:
126+
self.root_physx_view.apply_forces_and_torques_at_position(
127+
force_data=self._external_force_b.view(-1, 3),
128+
torque_data=self._external_torque_b.view(-1, 3),
129+
position_data=None,
130+
indices=self._ALL_INDICES,
131+
is_global=self._use_global_wrench_frame,
132+
)
123133

124134
def update(self, dt: float):
125135
self._data.update(dt)
@@ -357,14 +367,16 @@ def set_external_force_and_torque(
357367
self,
358368
forces: torch.Tensor,
359369
torques: torch.Tensor,
370+
positions: torch.Tensor | None = None,
360371
body_ids: Sequence[int] | slice | None = None,
361372
env_ids: Sequence[int] | None = None,
362373
):
363374
"""Set external force and torque to apply on the asset's bodies in their local frame.
364375
365376
For many applications, we want to keep the applied external force on rigid bodies constant over a period of
366377
time (for instance, during the policy control). This function allows us to store the external force and torque
367-
into buffers which are then applied to the simulation at every step.
378+
into buffers which are then applied to the simulation at every step. Optionally, set the position to apply the
379+
external wrench at (in the local link frame of the bodies).
368380
369381
.. caution::
370382
If the function is called with empty forces and torques, then this function disables the application
@@ -383,6 +395,7 @@ def set_external_force_and_torque(
383395
Args:
384396
forces: External forces in bodies' local frame. Shape is (len(env_ids), len(body_ids), 3).
385397
torques: External torques in bodies' local frame. Shape is (len(env_ids), len(body_ids), 3).
398+
positions: External wrench positions in bodies' local frame. Shape is (len(env_ids), len(body_ids), 3). Defaults to None.
386399
body_ids: Body indices to apply external wrench to. Defaults to None (all bodies).
387400
env_ids: Environment indices to apply external wrench to. Defaults to None (all instances).
388401
"""
@@ -407,6 +420,13 @@ def set_external_force_and_torque(
407420
self._external_force_b[env_ids, body_ids] = forces
408421
self._external_torque_b[env_ids, body_ids] = torques
409422

423+
if positions is not None:
424+
self.uses_external_wrench_positions = True
425+
self._external_wrench_positions_b[env_ids, body_ids] = positions
426+
else:
427+
if self.uses_external_wrench_positions:
428+
self._external_wrench_positions_b[env_ids, body_ids] = 0.0
429+
410430
"""
411431
Internal helper.
412432
"""
@@ -483,6 +503,8 @@ def _create_buffers(self):
483503
self.has_external_wrench = False
484504
self._external_force_b = torch.zeros((self.num_instances, self.num_bodies, 3), device=self.device)
485505
self._external_torque_b = torch.zeros_like(self._external_force_b)
506+
self.uses_external_wrench_positions = False
507+
self._external_wrench_positions_b = torch.zeros_like(self._external_force_b)
486508

487509
# set information about rigid body into data
488510
self._data.body_names = self.body_names
@@ -503,6 +525,15 @@ def _process_cfg(self):
503525
default_root_state = torch.tensor(default_root_state, dtype=torch.float, device=self.device)
504526
self._data.default_root_state = default_root_state.repeat(self.num_instances, 1)
505527

528+
# -- external wrench
529+
external_wrench_frame = self.cfg.object_external_wrench_frame
530+
if external_wrench_frame == "local":
531+
self._use_global_wrench_frame = False
532+
elif external_wrench_frame == "world":
533+
self._use_global_wrench_frame = True
534+
else:
535+
raise ValueError(f"Invalid external wrench frame: {external_wrench_frame}. Must be 'local' or 'world'.")
536+
506537
"""
507538
Internal simulation callbacks.
508539
"""

source/isaaclab/isaaclab/assets/rigid_object/rigid_object_cfg.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,10 @@ class InitialStateCfg(AssetBaseCfg.InitialStateCfg):
3030

3131
init_state: InitialStateCfg = InitialStateCfg()
3232
"""Initial state of the rigid object. Defaults to identity pose with zero velocity."""
33+
34+
object_external_wrench_frame: str = "local"
35+
"""Frame in which external wrenches are applied. Defaults to "local".
36+
37+
If "local", the external wrenches are applied in the local frame of the articulation root.
38+
If "world", the external wrenches are applied in the world frame.
39+
"""

source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ def reset(self, env_ids: torch.Tensor | None = None, object_ids: slice | torch.T
165165
# reset external wrench
166166
self._external_force_b[env_ids[:, None], object_ids] = 0.0
167167
self._external_torque_b[env_ids[:, None], object_ids] = 0.0
168+
self._external_wrench_positions_b[env_ids[:, None], object_ids] = 0.0
168169

169170
def write_data_to_sim(self):
170171
"""Write external wrench to the simulation.
@@ -175,13 +176,22 @@ def write_data_to_sim(self):
175176
"""
176177
# write external wrench
177178
if self.has_external_wrench:
178-
self.root_physx_view.apply_forces_and_torques_at_position(
179-
force_data=self.reshape_data_to_view(self._external_force_b),
180-
torque_data=self.reshape_data_to_view(self._external_torque_b),
181-
position_data=None,
182-
indices=self._env_obj_ids_to_view_ids(self._ALL_ENV_INDICES, self._ALL_OBJ_INDICES),
183-
is_global=False,
184-
)
179+
if self.uses_external_wrench_positions:
180+
self.root_physx_view.apply_forces_and_torques_at_position(
181+
force_data=self.reshape_data_to_view(self._external_force_b),
182+
torque_data=self.reshape_data_to_view(self._external_torque_b),
183+
position_data=self.reshape_data_to_view(self._external_wrench_positions_b),
184+
indices=self._env_obj_ids_to_view_ids(self._ALL_ENV_INDICES, self._ALL_OBJ_INDICES),
185+
is_global=self._use_global_wrench_frame,
186+
)
187+
else:
188+
self.root_physx_view.apply_forces_and_torques_at_position(
189+
force_data=self.reshape_data_to_view(self._external_force_b),
190+
torque_data=self.reshape_data_to_view(self._external_torque_b),
191+
position_data=None,
192+
indices=self._env_obj_ids_to_view_ids(self._ALL_ENV_INDICES, self._ALL_OBJ_INDICES),
193+
is_global=self._use_global_wrench_frame,
194+
)
185195

186196
def update(self, dt: float):
187197
self._data.update(dt)
@@ -486,6 +496,7 @@ def set_external_force_and_torque(
486496
self,
487497
forces: torch.Tensor,
488498
torques: torch.Tensor,
499+
positions: torch.Tensor | None = None,
489500
object_ids: slice | torch.Tensor | None = None,
490501
env_ids: torch.Tensor | None = None,
491502
):
@@ -512,6 +523,7 @@ def set_external_force_and_torque(
512523
Args:
513524
forces: External forces in bodies' local frame. Shape is (len(env_ids), len(object_ids), 3).
514525
torques: External torques in bodies' local frame. Shape is (len(env_ids), len(object_ids), 3).
526+
positions: External wrench positions in bodies' local frame. Shape is (len(env_ids), len(object_ids), 3).
515527
object_ids: Object indices to apply external wrench to. Defaults to None (all objects).
516528
env_ids: Environment indices to apply external wrench to. Defaults to None (all instances).
517529
"""
@@ -532,6 +544,12 @@ def set_external_force_and_torque(
532544
# set into internal buffers
533545
self._external_force_b[env_ids[:, None], object_ids] = forces
534546
self._external_torque_b[env_ids[:, None], object_ids] = torques
547+
if positions is not None:
548+
self.uses_external_wrench_positions = True
549+
self._external_wrench_positions_b[env_ids[:, None], object_ids] = positions
550+
else:
551+
if self.uses_external_wrench_positions:
552+
self._external_wrench_positions_b[env_ids[:, None], object_ids] = 0.0
535553

536554
"""
537555
Helper functions.
@@ -643,6 +661,8 @@ def _create_buffers(self):
643661
self.has_external_wrench = False
644662
self._external_force_b = torch.zeros((self.num_instances, self.num_objects, 3), device=self.device)
645663
self._external_torque_b = torch.zeros_like(self._external_force_b)
664+
self._external_wrench_positions_b = torch.zeros_like(self._external_force_b)
665+
self.uses_external_wrench_positions = False
646666

647667
# set information about rigid body into data
648668
self._data.object_names = self.object_names
@@ -671,6 +691,15 @@ def _process_cfg(self):
671691
default_object_states = torch.cat(default_object_states, dim=1)
672692
self._data.default_object_state = default_object_states
673693

694+
# -- external wrench
695+
external_wrench_frame = self.cfg.objects_external_wrench_frame
696+
if external_wrench_frame == "local":
697+
self._use_global_wrench_frame = False
698+
elif external_wrench_frame == "world":
699+
self._use_global_wrench_frame = True
700+
else:
701+
raise ValueError(f"Invalid external wrench frame: {external_wrench_frame}. Must be 'local' or 'world'.")
702+
674703
def _env_obj_ids_to_view_ids(
675704
self, env_ids: torch.Tensor, object_ids: Sequence[int] | slice | torch.Tensor
676705
) -> torch.Tensor:

source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection_cfg.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,10 @@ class RigidObjectCollectionCfg:
2626
2727
The keys are the names for the objects, which are used as unique identifiers throughout the code.
2828
"""
29+
30+
objects_external_wrench_frame: str = "local"
31+
"""Frame in which external wrenches are applied. Defaults to "local".
32+
33+
If "local", the external wrenches are applied in the local frame of the articulation root.
34+
If "world", the external wrenches are applied in the world frame.
35+
"""

0 commit comments

Comments
 (0)