Unverified Commit ce2ae923 authored by Antoine RICHARD's avatar Antoine RICHARD Committed by GitHub

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: 's avatarKelly Guo <kellyg@nvidia.com>
parent 3beb2d2b
...@@ -169,6 +169,7 @@ class Articulation(AssetBase): ...@@ -169,6 +169,7 @@ class Articulation(AssetBase):
# reset external wrench # reset external wrench
self._external_force_b[env_ids] = 0.0 self._external_force_b[env_ids] = 0.0
self._external_torque_b[env_ids] = 0.0 self._external_torque_b[env_ids] = 0.0
self._external_wrench_positions_b[env_ids] = 0.0
def write_data_to_sim(self): def write_data_to_sim(self):
"""Write external wrenches and joint commands to the simulation. """Write external wrenches and joint commands to the simulation.
...@@ -182,13 +183,22 @@ class Articulation(AssetBase): ...@@ -182,13 +183,22 @@ class Articulation(AssetBase):
""" """
# write external wrench # write external wrench
if self.has_external_wrench: if self.has_external_wrench:
self.root_physx_view.apply_forces_and_torques_at_position( if self.uses_external_wrench_positions:
force_data=self._external_force_b.view(-1, 3), self.root_physx_view.apply_forces_and_torques_at_position(
torque_data=self._external_torque_b.view(-1, 3), force_data=self._external_force_b.view(-1, 3),
position_data=None, torque_data=self._external_torque_b.view(-1, 3),
indices=self._ALL_INDICES, position_data=self._external_wrench_positions_b.view(-1, 3),
is_global=False, indices=self._ALL_INDICES,
) is_global=self._use_global_wrench_frame,
)
else:
self.root_physx_view.apply_forces_and_torques_at_position(
force_data=self._external_force_b.view(-1, 3),
torque_data=self._external_torque_b.view(-1, 3),
position_data=None,
indices=self._ALL_INDICES,
is_global=self._use_global_wrench_frame,
)
# apply actuator models # apply actuator models
self._apply_actuator_model() self._apply_actuator_model()
...@@ -829,6 +839,7 @@ class Articulation(AssetBase): ...@@ -829,6 +839,7 @@ class Articulation(AssetBase):
self, self,
forces: torch.Tensor, forces: torch.Tensor,
torques: torch.Tensor, torques: torch.Tensor,
positions: torch.Tensor | None = None,
body_ids: Sequence[int] | slice | None = None, body_ids: Sequence[int] | slice | None = None,
env_ids: Sequence[int] | None = None, env_ids: Sequence[int] | None = None,
): ):
...@@ -836,7 +847,8 @@ class Articulation(AssetBase): ...@@ -836,7 +847,8 @@ class Articulation(AssetBase):
For many applications, we want to keep the applied external force on rigid bodies constant over a period of For many applications, we want to keep the applied external force on rigid bodies constant over a period of
time (for instance, during the policy control). This function allows us to store the external force and torque time (for instance, during the policy control). This function allows us to store the external force and torque
into buffers which are then applied to the simulation at every step. into buffers which are then applied to the simulation at every step. Optionally, set the position to apply the
external wrench at (in the local link frame of the bodies).
.. caution:: .. caution::
If the function is called with empty forces and torques, then this function disables the application If the function is called with empty forces and torques, then this function disables the application
...@@ -855,6 +867,7 @@ class Articulation(AssetBase): ...@@ -855,6 +867,7 @@ class Articulation(AssetBase):
Args: Args:
forces: External forces in bodies' local frame. Shape is (len(env_ids), len(body_ids), 3). 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). 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.
body_ids: Body indices to apply external wrench to. Defaults to None (all bodies). 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). env_ids: Environment indices to apply external wrench to. Defaults to None (all instances).
""" """
...@@ -887,6 +900,17 @@ class Articulation(AssetBase): ...@@ -887,6 +900,17 @@ class Articulation(AssetBase):
self._external_force_b.flatten(0, 1)[indices] = forces.flatten(0, 1) self._external_force_b.flatten(0, 1)[indices] = forces.flatten(0, 1)
self._external_torque_b.flatten(0, 1)[indices] = torques.flatten(0, 1) self._external_torque_b.flatten(0, 1)[indices] = torques.flatten(0, 1)
# If the positions are not provided, the behavior and performance of the simulation should not be affected.
if positions is not None:
# Generates a flag that is set for a full simulation step. This is done to avoid discarding
# the external wrench positions when multiple calls to this functions are made with and without positions.
self.uses_external_wrench_positions = True
self._external_wrench_positions_b.flatten(0, 1)[indices] = positions.flatten(0, 1)
else:
# If the positions are not provided, and the flag is set, then we need to ensure that the desired positions are zeroed.
if self.uses_external_wrench_positions:
self._external_wrench_positions_b.flatten(0, 1)[indices] = 0.0
def set_joint_position_target( def set_joint_position_target(
self, target: torch.Tensor, joint_ids: Sequence[int] | slice | None = None, env_ids: Sequence[int] | None = None self, target: torch.Tensor, joint_ids: Sequence[int] | slice | None = None, env_ids: Sequence[int] | None = None
): ):
...@@ -1229,8 +1253,10 @@ class Articulation(AssetBase): ...@@ -1229,8 +1253,10 @@ class Articulation(AssetBase):
# external forces and torques # external forces and torques
self.has_external_wrench = False self.has_external_wrench = False
self.uses_external_wrench_positions = False
self._external_force_b = torch.zeros((self.num_instances, self.num_bodies, 3), device=self.device) self._external_force_b = torch.zeros((self.num_instances, self.num_bodies, 3), device=self.device)
self._external_torque_b = torch.zeros_like(self._external_force_b) self._external_torque_b = torch.zeros_like(self._external_force_b)
self._external_wrench_positions_b = torch.zeros_like(self._external_force_b)
# asset named data # asset named data
self._data.joint_names = self.joint_names self._data.joint_names = self.joint_names
...@@ -1298,6 +1324,15 @@ class Articulation(AssetBase): ...@@ -1298,6 +1324,15 @@ class Articulation(AssetBase):
default_root_state = torch.tensor(default_root_state, dtype=torch.float, device=self.device) default_root_state = torch.tensor(default_root_state, dtype=torch.float, device=self.device)
self._data.default_root_state = default_root_state.repeat(self.num_instances, 1) self._data.default_root_state = default_root_state.repeat(self.num_instances, 1)
# -- external wrench
external_wrench_frame = self.cfg.articulation_external_wrench_frame
if external_wrench_frame == "local":
self._use_global_wrench_frame = False
elif external_wrench_frame == "world":
self._use_global_wrench_frame = True
else:
raise ValueError(f"Invalid external wrench frame: {external_wrench_frame}. Must be 'local' or 'world'.")
# -- joint state # -- joint state
self._data.default_joint_pos = torch.zeros(self.num_instances, self.num_joints, device=self.device) self._data.default_joint_pos = torch.zeros(self.num_instances, self.num_joints, device=self.device)
self._data.default_joint_vel = torch.zeros_like(self._data.default_joint_pos) self._data.default_joint_vel = torch.zeros_like(self._data.default_joint_pos)
......
...@@ -44,6 +44,13 @@ class ArticulationCfg(AssetBaseCfg): ...@@ -44,6 +44,13 @@ class ArticulationCfg(AssetBaseCfg):
If not provided will search for a prim with the ArticulationRootAPI. Should start with a slash. If not provided will search for a prim with the ArticulationRootAPI. Should start with a slash.
""" """
articulation_external_wrench_frame: str = "local"
"""Frame in which external wrenches are applied. Defaults to "local".
If "local", the external wrenches are applied in the local frame of the articulation root.
If "world", the external wrenches are applied in the world frame.
"""
init_state: InitialStateCfg = InitialStateCfg() init_state: InitialStateCfg = InitialStateCfg()
"""Initial state of the articulated object. Defaults to identity pose with zero velocity and zero joint state.""" """Initial state of the articulated object. Defaults to identity pose with zero velocity and zero joint state."""
......
...@@ -103,6 +103,7 @@ class RigidObject(AssetBase): ...@@ -103,6 +103,7 @@ class RigidObject(AssetBase):
# reset external wrench # reset external wrench
self._external_force_b[env_ids] = 0.0 self._external_force_b[env_ids] = 0.0
self._external_torque_b[env_ids] = 0.0 self._external_torque_b[env_ids] = 0.0
self._external_wrench_positions_b[env_ids] = 0.0
def write_data_to_sim(self): def write_data_to_sim(self):
"""Write external wrench to the simulation. """Write external wrench to the simulation.
...@@ -113,13 +114,22 @@ class RigidObject(AssetBase): ...@@ -113,13 +114,22 @@ class RigidObject(AssetBase):
""" """
# write external wrench # write external wrench
if self.has_external_wrench: if self.has_external_wrench:
self.root_physx_view.apply_forces_and_torques_at_position( if self.uses_external_wrench_positions:
force_data=self._external_force_b.view(-1, 3), self.root_physx_view.apply_forces_and_torques_at_position(
torque_data=self._external_torque_b.view(-1, 3), force_data=self._external_force_b.view(-1, 3),
position_data=None, torque_data=self._external_torque_b.view(-1, 3),
indices=self._ALL_INDICES, position_data=self._external_wrench_positions_b.view(-1, 3),
is_global=False, indices=self._ALL_INDICES,
) is_global=self._use_global_wrench_frame,
)
else:
self.root_physx_view.apply_forces_and_torques_at_position(
force_data=self._external_force_b.view(-1, 3),
torque_data=self._external_torque_b.view(-1, 3),
position_data=None,
indices=self._ALL_INDICES,
is_global=self._use_global_wrench_frame,
)
def update(self, dt: float): def update(self, dt: float):
self._data.update(dt) self._data.update(dt)
...@@ -357,6 +367,7 @@ class RigidObject(AssetBase): ...@@ -357,6 +367,7 @@ class RigidObject(AssetBase):
self, self,
forces: torch.Tensor, forces: torch.Tensor,
torques: torch.Tensor, torques: torch.Tensor,
positions: torch.Tensor | None = None,
body_ids: Sequence[int] | slice | None = None, body_ids: Sequence[int] | slice | None = None,
env_ids: Sequence[int] | None = None, env_ids: Sequence[int] | None = None,
): ):
...@@ -364,7 +375,8 @@ class RigidObject(AssetBase): ...@@ -364,7 +375,8 @@ class RigidObject(AssetBase):
For many applications, we want to keep the applied external force on rigid bodies constant over a period of For many applications, we want to keep the applied external force on rigid bodies constant over a period of
time (for instance, during the policy control). This function allows us to store the external force and torque time (for instance, during the policy control). This function allows us to store the external force and torque
into buffers which are then applied to the simulation at every step. into buffers which are then applied to the simulation at every step. Optionally, set the position to apply the
external wrench at (in the local link frame of the bodies).
.. caution:: .. caution::
If the function is called with empty forces and torques, then this function disables the application If the function is called with empty forces and torques, then this function disables the application
...@@ -383,6 +395,7 @@ class RigidObject(AssetBase): ...@@ -383,6 +395,7 @@ class RigidObject(AssetBase):
Args: Args:
forces: External forces in bodies' local frame. Shape is (len(env_ids), len(body_ids), 3). 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). 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). Defaults to None.
body_ids: Body indices to apply external wrench to. Defaults to None (all bodies). 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). env_ids: Environment indices to apply external wrench to. Defaults to None (all instances).
""" """
...@@ -407,6 +420,13 @@ class RigidObject(AssetBase): ...@@ -407,6 +420,13 @@ class RigidObject(AssetBase):
self._external_force_b[env_ids, body_ids] = forces self._external_force_b[env_ids, body_ids] = forces
self._external_torque_b[env_ids, body_ids] = torques self._external_torque_b[env_ids, body_ids] = torques
if positions is not None:
self.uses_external_wrench_positions = True
self._external_wrench_positions_b[env_ids, body_ids] = positions
else:
if self.uses_external_wrench_positions:
self._external_wrench_positions_b[env_ids, body_ids] = 0.0
""" """
Internal helper. Internal helper.
""" """
...@@ -483,6 +503,8 @@ class RigidObject(AssetBase): ...@@ -483,6 +503,8 @@ class RigidObject(AssetBase):
self.has_external_wrench = False self.has_external_wrench = False
self._external_force_b = torch.zeros((self.num_instances, self.num_bodies, 3), device=self.device) self._external_force_b = torch.zeros((self.num_instances, self.num_bodies, 3), device=self.device)
self._external_torque_b = torch.zeros_like(self._external_force_b) self._external_torque_b = torch.zeros_like(self._external_force_b)
self.uses_external_wrench_positions = False
self._external_wrench_positions_b = torch.zeros_like(self._external_force_b)
# set information about rigid body into data # set information about rigid body into data
self._data.body_names = self.body_names self._data.body_names = self.body_names
...@@ -503,6 +525,15 @@ class RigidObject(AssetBase): ...@@ -503,6 +525,15 @@ class RigidObject(AssetBase):
default_root_state = torch.tensor(default_root_state, dtype=torch.float, device=self.device) default_root_state = torch.tensor(default_root_state, dtype=torch.float, device=self.device)
self._data.default_root_state = default_root_state.repeat(self.num_instances, 1) self._data.default_root_state = default_root_state.repeat(self.num_instances, 1)
# -- external wrench
external_wrench_frame = self.cfg.object_external_wrench_frame
if external_wrench_frame == "local":
self._use_global_wrench_frame = False
elif external_wrench_frame == "world":
self._use_global_wrench_frame = True
else:
raise ValueError(f"Invalid external wrench frame: {external_wrench_frame}. Must be 'local' or 'world'.")
""" """
Internal simulation callbacks. Internal simulation callbacks.
""" """
......
...@@ -30,3 +30,10 @@ class RigidObjectCfg(AssetBaseCfg): ...@@ -30,3 +30,10 @@ class RigidObjectCfg(AssetBaseCfg):
init_state: InitialStateCfg = InitialStateCfg() init_state: InitialStateCfg = InitialStateCfg()
"""Initial state of the rigid object. Defaults to identity pose with zero velocity.""" """Initial state of the rigid object. Defaults to identity pose with zero velocity."""
object_external_wrench_frame: str = "local"
"""Frame in which external wrenches are applied. Defaults to "local".
If "local", the external wrenches are applied in the local frame of the articulation root.
If "world", the external wrenches are applied in the world frame.
"""
...@@ -165,6 +165,7 @@ class RigidObjectCollection(AssetBase): ...@@ -165,6 +165,7 @@ class RigidObjectCollection(AssetBase):
# reset external wrench # reset external wrench
self._external_force_b[env_ids[:, None], object_ids] = 0.0 self._external_force_b[env_ids[:, None], object_ids] = 0.0
self._external_torque_b[env_ids[:, None], object_ids] = 0.0 self._external_torque_b[env_ids[:, None], object_ids] = 0.0
self._external_wrench_positions_b[env_ids[:, None], object_ids] = 0.0
def write_data_to_sim(self): def write_data_to_sim(self):
"""Write external wrench to the simulation. """Write external wrench to the simulation.
...@@ -175,13 +176,22 @@ class RigidObjectCollection(AssetBase): ...@@ -175,13 +176,22 @@ class RigidObjectCollection(AssetBase):
""" """
# write external wrench # write external wrench
if self.has_external_wrench: if self.has_external_wrench:
self.root_physx_view.apply_forces_and_torques_at_position( if self.uses_external_wrench_positions:
force_data=self.reshape_data_to_view(self._external_force_b), self.root_physx_view.apply_forces_and_torques_at_position(
torque_data=self.reshape_data_to_view(self._external_torque_b), force_data=self.reshape_data_to_view(self._external_force_b),
position_data=None, torque_data=self.reshape_data_to_view(self._external_torque_b),
indices=self._env_obj_ids_to_view_ids(self._ALL_ENV_INDICES, self._ALL_OBJ_INDICES), position_data=self.reshape_data_to_view(self._external_wrench_positions_b),
is_global=False, indices=self._env_obj_ids_to_view_ids(self._ALL_ENV_INDICES, self._ALL_OBJ_INDICES),
) is_global=self._use_global_wrench_frame,
)
else:
self.root_physx_view.apply_forces_and_torques_at_position(
force_data=self.reshape_data_to_view(self._external_force_b),
torque_data=self.reshape_data_to_view(self._external_torque_b),
position_data=None,
indices=self._env_obj_ids_to_view_ids(self._ALL_ENV_INDICES, self._ALL_OBJ_INDICES),
is_global=self._use_global_wrench_frame,
)
def update(self, dt: float): def update(self, dt: float):
self._data.update(dt) self._data.update(dt)
...@@ -486,6 +496,7 @@ class RigidObjectCollection(AssetBase): ...@@ -486,6 +496,7 @@ class RigidObjectCollection(AssetBase):
self, self,
forces: torch.Tensor, forces: torch.Tensor,
torques: torch.Tensor, torques: torch.Tensor,
positions: torch.Tensor | None = None,
object_ids: slice | torch.Tensor | None = None, object_ids: slice | torch.Tensor | None = None,
env_ids: torch.Tensor | None = None, env_ids: torch.Tensor | None = None,
): ):
...@@ -512,6 +523,7 @@ class RigidObjectCollection(AssetBase): ...@@ -512,6 +523,7 @@ class RigidObjectCollection(AssetBase):
Args: Args:
forces: External forces in bodies' local frame. Shape is (len(env_ids), len(object_ids), 3). 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). 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).
object_ids: Object indices to apply external wrench to. Defaults to None (all objects). 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). env_ids: Environment indices to apply external wrench to. Defaults to None (all instances).
""" """
...@@ -532,6 +544,12 @@ class RigidObjectCollection(AssetBase): ...@@ -532,6 +544,12 @@ class RigidObjectCollection(AssetBase):
# set into internal buffers # set into internal buffers
self._external_force_b[env_ids[:, None], object_ids] = forces self._external_force_b[env_ids[:, None], object_ids] = forces
self._external_torque_b[env_ids[:, None], object_ids] = torques self._external_torque_b[env_ids[:, None], object_ids] = torques
if positions is not None:
self.uses_external_wrench_positions = True
self._external_wrench_positions_b[env_ids[:, None], object_ids] = positions
else:
if self.uses_external_wrench_positions:
self._external_wrench_positions_b[env_ids[:, None], object_ids] = 0.0
""" """
Helper functions. Helper functions.
...@@ -643,6 +661,8 @@ class RigidObjectCollection(AssetBase): ...@@ -643,6 +661,8 @@ class RigidObjectCollection(AssetBase):
self.has_external_wrench = False self.has_external_wrench = False
self._external_force_b = torch.zeros((self.num_instances, self.num_objects, 3), device=self.device) self._external_force_b = torch.zeros((self.num_instances, self.num_objects, 3), device=self.device)
self._external_torque_b = torch.zeros_like(self._external_force_b) self._external_torque_b = torch.zeros_like(self._external_force_b)
self._external_wrench_positions_b = torch.zeros_like(self._external_force_b)
self.uses_external_wrench_positions = False
# set information about rigid body into data # set information about rigid body into data
self._data.object_names = self.object_names self._data.object_names = self.object_names
...@@ -671,6 +691,15 @@ class RigidObjectCollection(AssetBase): ...@@ -671,6 +691,15 @@ class RigidObjectCollection(AssetBase):
default_object_states = torch.cat(default_object_states, dim=1) default_object_states = torch.cat(default_object_states, dim=1)
self._data.default_object_state = default_object_states self._data.default_object_state = default_object_states
# -- external wrench
external_wrench_frame = self.cfg.objects_external_wrench_frame
if external_wrench_frame == "local":
self._use_global_wrench_frame = False
elif external_wrench_frame == "world":
self._use_global_wrench_frame = True
else:
raise ValueError(f"Invalid external wrench frame: {external_wrench_frame}. Must be 'local' or 'world'.")
def _env_obj_ids_to_view_ids( def _env_obj_ids_to_view_ids(
self, env_ids: torch.Tensor, object_ids: Sequence[int] | slice | torch.Tensor self, env_ids: torch.Tensor, object_ids: Sequence[int] | slice | torch.Tensor
) -> torch.Tensor: ) -> torch.Tensor:
......
...@@ -26,3 +26,10 @@ class RigidObjectCollectionCfg: ...@@ -26,3 +26,10 @@ class RigidObjectCollectionCfg:
The keys are the names for the objects, which are used as unique identifiers throughout the code. The keys are the names for the objects, which are used as unique identifiers throughout the code.
""" """
objects_external_wrench_frame: str = "local"
"""Frame in which external wrenches are applied. Defaults to "local".
If "local", the external wrenches are applied in the local frame of the articulation root.
If "world", the external wrenches are applied in the world frame.
"""
...@@ -752,27 +752,40 @@ def test_external_force_buffer(sim, num_articulations, device): ...@@ -752,27 +752,40 @@ def test_external_force_buffer(sim, num_articulations, device):
for step in range(5): for step in range(5):
# initiate force tensor # initiate force tensor
external_wrench_b = torch.zeros(articulation.num_instances, len(body_ids), 6, device=sim.device) external_wrench_b = torch.zeros(articulation.num_instances, len(body_ids), 6, device=sim.device)
external_wrench_positions_b = torch.zeros(articulation.num_instances, len(body_ids), 3, device=sim.device)
if step == 0 or step == 3: if step == 0 or step == 3:
# set a non-zero force # set a non-zero force
force = 1 force = 1
position = 1
else: else:
# set a zero force # set a zero force
force = 0 force = 0
position = 0
# set force value # set force value
external_wrench_b[:, :, 0] = force external_wrench_b[:, :, 0] = force
external_wrench_b[:, :, 3] = force external_wrench_b[:, :, 3] = force
external_wrench_positions_b[:, :, 0] = position
# apply force # apply force
articulation.set_external_force_and_torque( if step == 0 or step == 3:
external_wrench_b[..., :3], external_wrench_b[..., 3:], body_ids=body_ids articulation.set_external_force_and_torque(
) external_wrench_b[..., :3],
external_wrench_b[..., 3:],
body_ids=body_ids,
positions=external_wrench_positions_b,
)
else:
articulation.set_external_force_and_torque(
external_wrench_b[..., :3], external_wrench_b[..., 3:], body_ids=body_ids
)
# check if the articulation's force and torque buffers are correctly updated # check if the articulation's force and torque buffers are correctly updated
for i in range(num_articulations): for i in range(num_articulations):
assert articulation._external_force_b[i, 0, 0].item() == force assert articulation._external_force_b[i, 0, 0].item() == force
assert articulation._external_torque_b[i, 0, 0].item() == force assert articulation._external_torque_b[i, 0, 0].item() == force
assert articulation._external_wrench_positions_b[i, 0, 0].item() == position
# apply action to the articulation # apply action to the articulation
articulation.set_joint_position_target(articulation.data.default_joint_pos.clone()) articulation.set_joint_position_target(articulation.data.default_joint_pos.clone())
...@@ -843,6 +856,71 @@ def test_external_force_on_single_body(sim, num_articulations, device): ...@@ -843,6 +856,71 @@ def test_external_force_on_single_body(sim, num_articulations, device):
assert articulation.data.root_pos_w[i, 2].item() < 0.2 assert articulation.data.root_pos_w[i, 2].item() < 0.2
@pytest.mark.parametrize("num_articulations", [1, 2])
@pytest.mark.parametrize("device", ["cuda:0", "cpu"])
def test_external_force_on_single_body_at_position(sim, num_articulations, device):
"""Test application of external force on the base of the articulation at a given position.
This test verifies that:
1. External forces can be applied to specific bodies
2. The forces affect the articulation's motion correctly
3. The articulation responds to the forces as expected
Args:
sim: The simulation fixture
num_articulations: Number of articulations to test
"""
articulation_cfg = generate_articulation_cfg(articulation_type="anymal")
articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=sim.device)
# Play the simulator
sim.reset()
# Find bodies to apply the force
body_ids, _ = articulation.find_bodies("base")
# Sample a large force
external_wrench_b = torch.zeros(articulation.num_instances, len(body_ids), 6, device=sim.device)
external_wrench_b[..., 2] = 1000.0
external_wrench_positions_b = torch.zeros(articulation.num_instances, len(body_ids), 3, device=sim.device)
external_wrench_positions_b[..., 0] = 0.0
external_wrench_positions_b[..., 1] = 1.0
external_wrench_positions_b[..., 2] = 0.0
# Now we are ready!
for _ in range(5):
# reset root state
root_state = articulation.data.default_root_state.clone()
articulation.write_root_pose_to_sim(root_state[:, :7])
articulation.write_root_velocity_to_sim(root_state[:, 7:])
# reset dof state
joint_pos, joint_vel = (
articulation.data.default_joint_pos,
articulation.data.default_joint_vel,
)
articulation.write_joint_state_to_sim(joint_pos, joint_vel)
# reset articulation
articulation.reset()
# apply force
articulation.set_external_force_and_torque(
external_wrench_b[..., :3],
external_wrench_b[..., 3:],
body_ids=body_ids,
positions=external_wrench_positions_b,
)
# perform simulation
for _ in range(100):
# apply action to the articulation
articulation.set_joint_position_target(articulation.data.default_joint_pos.clone())
articulation.write_data_to_sim()
# perform step
sim.step()
# update buffers
articulation.update(sim.cfg.dt)
# check condition that the articulations have fallen down
for i in range(num_articulations):
assert articulation.data.root_pos_w[i, 2].item() < 0.2
@pytest.mark.parametrize("num_articulations", [1, 2]) @pytest.mark.parametrize("num_articulations", [1, 2])
@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) @pytest.mark.parametrize("device", ["cuda:0", "cpu"])
def test_external_force_on_multiple_bodies(sim, num_articulations, device): def test_external_force_on_multiple_bodies(sim, num_articulations, device):
...@@ -901,6 +979,71 @@ def test_external_force_on_multiple_bodies(sim, num_articulations, device): ...@@ -901,6 +979,71 @@ def test_external_force_on_multiple_bodies(sim, num_articulations, device):
assert articulation.data.root_ang_vel_w[i, 2].item() > 0.1 assert articulation.data.root_ang_vel_w[i, 2].item() > 0.1
@pytest.mark.parametrize("num_articulations", [1, 2])
@pytest.mark.parametrize("device", ["cuda:0", "cpu"])
def test_external_force_on_multiple_bodies_at_position(sim, num_articulations, device):
"""Test application of external force on the legs of the articulation at a given position.
This test verifies that:
1. External forces can be applied to multiple bodies
2. The forces affect the articulation's motion correctly
3. The articulation responds to the forces as expected
Args:
sim: The simulation fixture
num_articulations: Number of articulations to test
"""
articulation_cfg = generate_articulation_cfg(articulation_type="anymal")
articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=sim.device)
# Play the simulator
sim.reset()
# Find bodies to apply the force
body_ids, _ = articulation.find_bodies(".*_SHANK")
# Sample a large force
external_wrench_b = torch.zeros(articulation.num_instances, len(body_ids), 6, device=sim.device)
external_wrench_b[..., 2] = 1000.0
external_wrench_positions_b = torch.zeros(articulation.num_instances, len(body_ids), 3, device=sim.device)
external_wrench_positions_b[..., 0] = 0.0
external_wrench_positions_b[..., 1] = 1.0
external_wrench_positions_b[..., 2] = 0.0
# Now we are ready!
for _ in range(5):
# reset root state
articulation.write_root_pose_to_sim(articulation.data.default_root_state.clone()[:, :7])
articulation.write_root_velocity_to_sim(articulation.data.default_root_state.clone()[:, 7:])
# reset dof state
joint_pos, joint_vel = (
articulation.data.default_joint_pos,
articulation.data.default_joint_vel,
)
articulation.write_joint_state_to_sim(joint_pos, joint_vel)
# reset articulation
articulation.reset()
# apply force
articulation.set_external_force_and_torque(
external_wrench_b[..., :3],
external_wrench_b[..., 3:],
body_ids=body_ids,
positions=external_wrench_positions_b,
)
# perform simulation
for _ in range(100):
# apply action to the articulation
articulation.set_joint_position_target(articulation.data.default_joint_pos.clone())
articulation.write_data_to_sim()
# perform step
sim.step()
# update buffers
articulation.update(sim.cfg.dt)
# check condition
for i in range(num_articulations):
# since there is a moment applied on the articulation, the articulation should rotate
assert torch.abs(articulation.data.root_ang_vel_w[i, 2]).item() > 0.1
@pytest.mark.parametrize("num_articulations", [1, 2]) @pytest.mark.parametrize("num_articulations", [1, 2])
@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) @pytest.mark.parametrize("device", ["cuda:0", "cpu"])
def test_loading_gains_from_usd(sim, num_articulations, device): def test_loading_gains_from_usd(sim, num_articulations, device):
......
...@@ -224,26 +224,39 @@ def test_external_force_buffer(device): ...@@ -224,26 +224,39 @@ def test_external_force_buffer(device):
# initiate force tensor # initiate force tensor
external_wrench_b = torch.zeros(cube_object.num_instances, len(body_ids), 6, device=sim.device) external_wrench_b = torch.zeros(cube_object.num_instances, len(body_ids), 6, device=sim.device)
external_wrench_positions_b = torch.zeros(cube_object.num_instances, len(body_ids), 3, device=sim.device)
if step == 0 or step == 3: if step == 0 or step == 3:
# set a non-zero force # set a non-zero force
force = 1 force = 1
position = 1
else: else:
# set a zero force # set a zero force
force = 0 force = 0
position = 0
# set force value # set force value
external_wrench_b[:, :, 0] = force external_wrench_b[:, :, 0] = force
external_wrench_b[:, :, 3] = force external_wrench_b[:, :, 3] = force
external_wrench_positions_b[:, :, 0] = position
# apply force # apply force
cube_object.set_external_force_and_torque( if step == 0 or step == 3:
external_wrench_b[..., :3], external_wrench_b[..., 3:], body_ids=body_ids cube_object.set_external_force_and_torque(
) external_wrench_b[..., :3],
external_wrench_b[..., 3:],
body_ids=body_ids,
positions=external_wrench_positions_b,
)
else:
cube_object.set_external_force_and_torque(
external_wrench_b[..., :3], external_wrench_b[..., 3:], body_ids=body_ids
)
# check if the cube's force and torque buffers are correctly updated # check if the cube's force and torque buffers are correctly updated
assert cube_object._external_force_b[0, 0, 0].item() == force assert cube_object._external_force_b[0, 0, 0].item() == force
assert cube_object._external_torque_b[0, 0, 0].item() == force assert cube_object._external_torque_b[0, 0, 0].item() == force
assert cube_object._external_wrench_positions_b[0, 0, 0].item() == position
# apply action to the object # apply action to the object
cube_object.write_data_to_sim() cube_object.write_data_to_sim()
...@@ -316,6 +329,70 @@ def test_external_force_on_single_body(num_cubes, device): ...@@ -316,6 +329,70 @@ def test_external_force_on_single_body(num_cubes, device):
assert torch.all(cube_object.data.root_pos_w[1::2, 2] < 1.0) assert torch.all(cube_object.data.root_pos_w[1::2, 2] < 1.0)
@pytest.mark.parametrize("num_cubes", [2, 4])
@pytest.mark.parametrize("device", ["cuda:0", "cpu"])
def test_external_force_on_single_body_at_position(num_cubes, device):
"""Test application of external force on the base of the object at a specific position.
In this test, we apply a force equal to the weight of an object on the base of
one of the objects at 1m in the Y direction, we check that the object rotates around it's X axis.
For the other object, we do not apply any force and check that it falls down.
"""
# Generate cubes scene
with build_simulation_context(device=device, add_ground_plane=True, auto_add_lighting=True) as sim:
sim._app_control_on_stop_handle = None
cube_object, origins = generate_cubes_scene(num_cubes=num_cubes, device=device)
# Play the simulator
sim.reset()
# Find bodies to apply the force
body_ids, body_names = cube_object.find_bodies(".*")
# Sample a force equal to the weight of the object
external_wrench_b = torch.zeros(cube_object.num_instances, len(body_ids), 6, device=sim.device)
external_wrench_positions_b = torch.zeros(cube_object.num_instances, len(body_ids), 3, device=sim.device)
# Every 2nd cube should have a force applied to it
external_wrench_b[0::2, :, 2] = 9.81 * cube_object.root_physx_view.get_masses()[0]
external_wrench_positions_b[0::2, :, 1] = 1.0
# Now we are ready!
for _ in range(5):
# reset root state
root_state = cube_object.data.default_root_state.clone()
# need to shift the position of the cubes otherwise they will be on top of each other
root_state[:, :3] = origins
cube_object.write_root_pose_to_sim(root_state[:, :7])
cube_object.write_root_velocity_to_sim(root_state[:, 7:])
# reset object
cube_object.reset()
# apply force
cube_object.set_external_force_and_torque(
external_wrench_b[..., :3],
external_wrench_b[..., 3:],
positions=external_wrench_positions_b,
body_ids=body_ids,
)
# perform simulation
for _ in range(5):
# apply action to the object
cube_object.write_data_to_sim()
# perform step
sim.step()
# update buffers
cube_object.update(sim.cfg.dt)
# The first object should be rotating around it's X axis
assert torch.all(torch.abs(cube_object.data.root_ang_vel_b[0::2, 0]) > 0.1)
# Second object should have fallen, so it's Z height should be less than initial height of 1.0
assert torch.all(cube_object.data.root_pos_w[1::2, 2] < 1.0)
@pytest.mark.parametrize("num_cubes", [1, 2]) @pytest.mark.parametrize("num_cubes", [1, 2])
@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) @pytest.mark.parametrize("device", ["cuda:0", "cpu"])
def test_set_rigid_object_state(num_cubes, device): def test_set_rigid_object_state(num_cubes, device):
...@@ -848,8 +925,8 @@ def test_body_root_state_properties(num_cubes, device, with_offset): ...@@ -848,8 +925,8 @@ def test_body_root_state_properties(num_cubes, device, with_offset):
lin_vel_rel_root_gt = quat_apply_inverse(root_link_state_w[..., 3:7], root_link_state_w[..., 7:10]) lin_vel_rel_root_gt = quat_apply_inverse(root_link_state_w[..., 3:7], root_link_state_w[..., 7:10])
lin_vel_rel_body_gt = quat_apply_inverse(body_link_state_w[..., 3:7], body_link_state_w[..., 7:10]) lin_vel_rel_body_gt = quat_apply_inverse(body_link_state_w[..., 3:7], body_link_state_w[..., 7:10])
lin_vel_rel_gt = torch.linalg.cross(spin_twist.repeat(num_cubes, 1)[..., 3:], -offset) lin_vel_rel_gt = torch.linalg.cross(spin_twist.repeat(num_cubes, 1)[..., 3:], -offset)
torch.testing.assert_close(lin_vel_rel_gt, lin_vel_rel_root_gt, atol=1e-3, rtol=1e-3) torch.testing.assert_close(lin_vel_rel_gt, lin_vel_rel_root_gt, atol=1e-4, rtol=1e-4)
torch.testing.assert_close(lin_vel_rel_gt, lin_vel_rel_body_gt.squeeze(-2), atol=1e-3, rtol=1e-3) torch.testing.assert_close(lin_vel_rel_gt, lin_vel_rel_body_gt.squeeze(-2), atol=1e-4, rtol=1e-4)
# ang_vel will always match # ang_vel will always match
torch.testing.assert_close(root_state_w[..., 10:], root_com_state_w[..., 10:]) torch.testing.assert_close(root_state_w[..., 10:], root_com_state_w[..., 10:])
......
...@@ -233,22 +233,35 @@ def test_external_force_buffer(sim, device): ...@@ -233,22 +233,35 @@ def test_external_force_buffer(sim, device):
for step in range(5): for step in range(5):
# initiate force tensor # initiate force tensor
external_wrench_b = torch.zeros(object_collection.num_instances, len(object_ids), 6, device=sim.device) external_wrench_b = torch.zeros(object_collection.num_instances, len(object_ids), 6, device=sim.device)
external_wrench_positions_b = torch.zeros(
object_collection.num_instances, len(object_ids), 3, device=sim.device
)
# decide if zero or non-zero force # decide if zero or non-zero force
force = 1 if step == 0 or step == 3 else 0 if step == 0 or step == 3:
force = 1.0
position = 1.0
else:
force = 0.0
position = 0.0
# apply force to the object # apply force to the object
external_wrench_b[:, :, 0] = force external_wrench_b[:, :, 0] = force
external_wrench_b[:, :, 3] = force external_wrench_b[:, :, 3] = force
external_wrench_positions_b[:, :, 0] = position
object_collection.set_external_force_and_torque( object_collection.set_external_force_and_torque(
external_wrench_b[..., :3], external_wrench_b[..., 3:], object_ids=object_ids external_wrench_b[..., :3],
external_wrench_b[..., 3:],
object_ids=object_ids,
positions=external_wrench_positions_b,
) )
# check if the object collection's force and torque buffers are correctly updated # check if the object collection's force and torque buffers are correctly updated
for i in range(num_envs): for i in range(num_envs):
assert object_collection._external_force_b[i, 0, 0].item() == force assert object_collection._external_force_b[i, 0, 0].item() == force
assert object_collection._external_torque_b[i, 0, 0].item() == force assert object_collection._external_torque_b[i, 0, 0].item() == force
assert object_collection._external_wrench_positions_b[i, 0, 0].item() == position
# apply action to the object collection # apply action to the object collection
object_collection.write_data_to_sim() object_collection.write_data_to_sim()
...@@ -303,6 +316,60 @@ def test_external_force_on_single_body(sim, num_envs, num_cubes, device): ...@@ -303,6 +316,60 @@ def test_external_force_on_single_body(sim, num_envs, num_cubes, device):
assert torch.all(object_collection.data.object_link_pos_w[:, 1::2, 2] < 1.0) assert torch.all(object_collection.data.object_link_pos_w[:, 1::2, 2] < 1.0)
@pytest.mark.parametrize("num_envs", [1, 2])
@pytest.mark.parametrize("num_cubes", [1, 4])
@pytest.mark.parametrize("device", ["cuda:0", "cpu"])
def test_external_force_on_single_body_at_position(sim, num_envs, num_cubes, device):
"""Test application of external force on the base of the object at a specific position.
In this test, we apply a force equal to the weight of an object on the base of
one of the objects at 1m in the Y direction, we check that the object rotates around it's X axis.
For the other object, we do not apply any force and check that it falls down.
"""
object_collection, origins = generate_cubes_scene(num_envs=num_envs, num_cubes=num_cubes, device=device)
sim.reset()
# find objects to apply the force
object_ids, object_names = object_collection.find_objects(".*")
# Sample a force equal to the weight of the object
external_wrench_b = torch.zeros(object_collection.num_instances, len(object_ids), 6, device=sim.device)
external_wrench_positions_b = torch.zeros(object_collection.num_instances, len(object_ids), 3, device=sim.device)
# Every 2nd cube should have a force applied to it
external_wrench_b[:, 0::2, 2] = 9.81 * object_collection.data.default_mass[:, 0::2, 0]
external_wrench_positions_b[:, 0::2, 1] = 1.0
for _ in range(5):
# reset object state
object_state = object_collection.data.default_object_state.clone()
# need to shift the position of the cubes otherwise they will be on top of each other
object_state[..., :2] += origins.unsqueeze(1)[..., :2]
object_collection.write_object_state_to_sim(object_state)
# reset object
object_collection.reset()
# apply force
object_collection.set_external_force_and_torque(
external_wrench_b[..., :3],
external_wrench_b[..., 3:],
positions=external_wrench_positions_b,
object_ids=object_ids,
)
for _ in range(10):
# write data to sim
object_collection.write_data_to_sim()
# step sim
sim.step()
# update object collection
object_collection.update(sim.cfg.dt)
# First object should be rotating around it's X axis
assert torch.all(object_collection.data.object_ang_vel_b[:, 0::2, 0] > 0.1)
# Second object should have fallen, so it's Z height should be less than initial height of 1.0
assert torch.all(object_collection.data.object_link_pos_w[:, 1::2, 2] < 1.0)
@pytest.mark.parametrize("num_envs", [1, 3]) @pytest.mark.parametrize("num_envs", [1, 3])
@pytest.mark.parametrize("num_cubes", [1, 2]) @pytest.mark.parametrize("num_cubes", [1, 2])
@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) @pytest.mark.parametrize("device", ["cuda:0", "cpu"])
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment