Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9b7f772
removed redundant buffer
gattra-rai Sep 24, 2025
d0d38ef
added friction force tracking
gattra-rai Sep 24, 2025
af70ae8
added friction force shape test
gattra-rai Sep 24, 2025
54e9ff7
added non contact test
gattra-rai Sep 24, 2025
4c28c6e
validating non nan
gattra-rai Sep 25, 2025
e314774
test to validate vectorized solution
gattra-rai Sep 25, 2025
8b213d5
formatting
gattra-rai Sep 25, 2025
4cdb468
updating doc / version
gattra-rai Sep 25, 2025
5d68bfc
rename func to be more generic
gattra-rai Sep 25, 2025
5059409
added to contributors
gattra-rai Sep 25, 2025
c3dd44d
test to check that friction forces make sense
gattra-rai Sep 25, 2025
e8457a5
formatting
gattra-rai Sep 25, 2025
8f83ed0
tuned forces in test
gattra-rai Sep 25, 2025
6e5567f
adds back friction tracking to check sensor script
gattra-rai Sep 25, 2025
09b8039
docstring / typing
gattra-rai Sep 26, 2025
99d98eb
formatting
gattra-rai Sep 26, 2025
e96e142
test comparing elements of physx friction data with reported sensor f…
gattra-rai Sep 26, 2025
1dd295a
tighter threshold for comparison
gattra-rai Sep 26, 2025
3044f24
Merge branch 'main' into friction-forces
gattra-rai Sep 26, 2025
2568b2c
resetting to zeros
gattra-rai Sep 29, 2025
7fe6bd3
tests pass
gattra-rai Sep 29, 2025
506ce65
add param to docstring
gattra-rai Sep 29, 2025
bc5858c
comment formatting
gattra-rai Sep 29, 2025
243518b
setting gravity in sim cfg
gattra-rai Sep 29, 2025
0baf2fd
remove unused imports
gattra-rai Sep 29, 2025
a08d71c
formatter
gattra-rai Sep 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Guidelines for modifications:
* Pascal Roth
* Sheikh Dawood
* Ossama Ahmed
* Greg Attra

## Contributors

Expand Down
2 changes: 1 addition & 1 deletion source/isaaclab/config/extension.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]

# Note: Semantic Versioning is used: https://semver.org/
version = "0.46.2"
version = "0.46.3"

# Description
title = "Isaac Lab framework for Robot Learning"
Expand Down
9 changes: 9 additions & 0 deletions source/isaaclab/docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
Changelog
---------

0.46.3 (2025-09-25)
~~~~~~~~~~~~~~~~~~~

Added
^^^^^

* Added :attr:`~isaaclab.sensors.contact_sensor.ContactSensorCfg.track_friction_forces` to toggle tracking of friction forces between sensor bodies and filtered bodies.
* Added :attr:`~isaaclab.sensors.contact_sensor.ContactSensorData.friction_forces_w` data field for tracking friction forces.


0.46.2 (2025-09-13)
~~~~~~~~~~~~~~~~~~~
Expand Down
102 changes: 74 additions & 28 deletions source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,9 @@ def reset(self, env_ids: Sequence[int] | None = None):
# reset contact positions
if self.cfg.track_contact_points:
self._data.contact_pos_w[env_ids, :] = torch.nan
# buffer used during contact position aggregation
self._contact_position_aggregate_buffer[env_ids, :] = torch.nan
# reset friction forces
if self.cfg.track_friction_forces:
self._data.friction_forces_w[env_ids, :] = 0.0

def find_bodies(self, name_keys: str | Sequence[str], preserve_order: bool = False) -> tuple[list[int], list[str]]:
"""Find bodies in the articulation based on the name keys.
Expand Down Expand Up @@ -317,10 +318,11 @@ def _initialize_impl(self):
torch.nan,
device=self._device,
)
# buffer used during contact position aggregation
self._contact_position_aggregate_buffer = torch.full(
(self._num_bodies * self._num_envs, self.contact_physx_view.filter_count, 3),
torch.nan,
# -- friction forces at contact points
if self.cfg.track_friction_forces:
self._data.friction_forces_w = torch.full(
(self._num_envs, self._num_bodies, self.contact_physx_view.filter_count, 3),
0.0,
device=self._device,
)
# -- air/contact time between contacts
Expand Down Expand Up @@ -382,28 +384,17 @@ def _update_buffers_impl(self, env_ids: Sequence[int]):
_, buffer_contact_points, _, _, buffer_count, buffer_start_indices = (
self.contact_physx_view.get_contact_data(dt=self._sim_physics_dt)
)
# unpack the contact points: see RigidContactView.get_contact_data() documentation for details:
# https://docs.omniverse.nvidia.com/kit/docs/omni_physics/107.3/extensions/runtime/source/omni.physics.tensors/docs/api/python.html#omni.physics.tensors.impl.api.RigidContactView.get_net_contact_forces
# buffer_count: (N_envs * N_bodies, N_filters), buffer_contact_points: (N_envs * N_bodies, 3)
counts, starts = buffer_count.view(-1), buffer_start_indices.view(-1)
n_rows, total = counts.numel(), int(counts.sum())
# default to NaN rows
agg = torch.full((n_rows, 3), float("nan"), device=self._device, dtype=buffer_contact_points.dtype)
if total > 0:
row_ids = torch.repeat_interleave(torch.arange(n_rows, device=self._device), counts)
total = row_ids.numel()

block_starts = counts.cumsum(0) - counts
deltas = torch.arange(total, device=counts.device) - block_starts.repeat_interleave(counts)
flat_idx = starts[row_ids] + deltas

pts = buffer_contact_points.index_select(0, flat_idx)
agg = agg.zero_().index_add_(0, row_ids, pts) / counts.clamp_min(1).unsqueeze(1)
agg[counts == 0] = float("nan")

self._contact_position_aggregate_buffer[:] = agg.view(self._num_envs * self.num_bodies, -1, 3)
self._data.contact_pos_w[env_ids] = self._contact_position_aggregate_buffer.view(
self._num_envs, self._num_bodies, self.contact_physx_view.filter_count, 3
self._data.contact_pos_w[env_ids] = self._unpack_contact_data(
buffer_contact_points, buffer_count, buffer_start_indices
)[env_ids]

# obtain friction forces
if self.cfg.track_friction_forces:
friction_forces, _, buffer_count, buffer_start_indices = self.contact_physx_view.get_friction_data(
dt=self._sim_physics_dt
)
self._data.friction_forces_w[env_ids] = self._unpack_contact_data(
friction_forces, buffer_count, buffer_start_indices, avg=False, default=0.0
)[env_ids]

# obtain the air time
Expand Down Expand Up @@ -436,6 +427,61 @@ def _update_buffers_impl(self, env_ids: Sequence[int]):
is_contact, self._data.current_contact_time[env_ids] + elapsed_time.unsqueeze(-1), 0.0
)

def _unpack_contact_data(
self,
contact_data: torch.Tensor,
buffer_count: torch.Tensor,
buffer_start_indices: torch.Tensor,
avg: bool = True,
default: float = float("nan"),
) -> torch.Tensor:
"""
Unpacks and aggregates contact data for each (env, body, filter) group.

This function vectorizes the following nested loop:

for i in range(self._num_bodies * self._num_envs):
for j in range(self.contact_physx_view.filter_count):
start_index_ij = buffer_start_indices[i, j]
count_ij = buffer_count[i, j]
self._contact_position_aggregate_buffer[i, j, :] = torch.mean(
contact_data[start_index_ij : (start_index_ij + count_ij), :], dim=0
)

For more details, see the RigidContactView.get_contact_data() documentation:
https://docs.omniverse.nvidia.com/kit/docs/omni_physics/107.3/extensions/runtime/source/omni.physics.tensors/docs/api/python.html#omni.physics.tensors.impl.api.RigidContactView.get_net_contact_forces

Args:
contact_data (torch.Tensor): Flat tensor of contact data, shape (N_envs * N_bodies, 3).
buffer_count (torch.Tensor): Number of contact points per (env, body, filter), shape (N_envs * N_bodies, N_filters).
buffer_start_indices (torch.Tensor): Start indices for each (env, body, filter), shape (N_envs * N_bodies, N_filters).
avg (bool, optional): If True, average the contact data for each group; if False, sum the data. Defaults to True.
default (float, optional): Default value to use for groups with zero contacts. Defaults to NaN.

Returns:
torch.Tensor: Aggregated contact data, shape (N_envs, N_bodies, N_filters, 3).
"""
counts, starts = buffer_count.view(-1), buffer_start_indices.view(-1)
n_rows, total = counts.numel(), int(counts.sum())
# default to NaN rows
agg = torch.full((n_rows, 3), default, device=self._device, dtype=contact_data.dtype)
if total > 0:
row_ids = torch.repeat_interleave(torch.arange(n_rows, device=self._device), counts)
total = row_ids.numel()

block_starts = counts.cumsum(0) - counts
deltas = torch.arange(total, device=counts.device) - block_starts.repeat_interleave(counts)
flat_idx = starts[row_ids] + deltas

pts = contact_data.index_select(0, flat_idx)
agg = agg.zero_().index_add_(0, row_ids, pts)
agg = agg / counts.unsqueeze(-1) if avg else agg
agg[counts == 0] = default

return agg.view(self._num_envs * self.num_bodies, -1, 3).view(
self._num_envs, self._num_bodies, self.contact_physx_view.filter_count, 3
)

def _set_debug_vis_impl(self, debug_vis: bool):
# set visibility of markers
# note: parent only deals with callbacks. not their visibility
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ class ContactSensorCfg(SensorBaseCfg):
track_contact_points: bool = False
"""Whether to track the contact point locations. Defaults to False."""

track_friction_forces: bool = False
"""Whether to track the friction forces at the contact points. Defaults to False."""

max_contact_data_count_per_prim: int = 4
"""The maximum number of contacts across all batches of the sensor to keep track of. Default is 4.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,22 @@ class ContactSensorData:
* If the :attr:`ContactSensorCfg.filter_prim_paths_expr` is empty, then this quantity is an empty tensor.
* If the :attr:`ContactSensorCfg.max_contact_data_per_prim` is not specified or less than 1, then this quantity
will not be calculated.
"""

friction_forces_w: torch.Tensor | None = None
"""Average of the friction forces between sensor body and filter prim in world frame.

Shape is (N, B, M, 3), where N is the number of sensors, B is number of bodies in each sensor
and M is the number of filtered bodies.

Collision pairs not in contact will result in NaN.

Note:

* If the :attr:`ContactSensorCfg.track_friction_forces` is False, then this quantity is None.
* If the :attr:`ContactSensorCfg.filter_prim_paths_expr` is empty, then this quantity is an empty tensor.
* If the :attr:`ContactSensorCfg.max_contact_data_per_prim` is not specified or less than 1, then this quantity
will not be calculated.
"""

quat_w: torch.Tensor | None = None
Expand Down
1 change: 1 addition & 0 deletions source/isaaclab/test/sensors/check_contact_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ def main():
prim_path="/World/envs/env_.*/Robot/.*_FOOT",
track_air_time=True,
track_contact_points=True,
track_friction_forces=True,
debug_vis=False, # not args_cli.headless,
filter_prim_paths_expr=["/World/defaultGroundPlane/GroundPlane/CollisionPlane"],
)
Expand Down
Loading
Loading