Skip to content

Commit 4fefc0d

Browse files
jtigue-bdaiakhadke-bdaikellyguo11ooctipus
authored
Adds contact point location reporting to ContactSensor (isaac-sim#2842)
# Description This PR: - Adds ContactSensorCfg.track_contact_points to toggle tracking of contact point locations between sensor bodies and filtered bodies. - Adds ContactSensorCfg.max_contact_data_per_prim to configure the maximum amount of contacts per sensor body. - Adds ContactSensorData.contact_pos_w data field for tracking contact point locations. Fixes # (issue) <!-- As a practice, it is recommended to open an issue to have discussions on the proposed pull request. This makes it easier for the community to keep track of what is being developed or added, and if a given feature is demanded by more than one party. --> ## Type of change <!-- As you go through the list, delete the ones that are not applicable. --> - New feature (non-breaking change which adds functionality) ## Checklist - [ ] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] 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 --------- Signed-off-by: James Tigue <166445701+jtigue-bdai@users.noreply.github.com> Signed-off-by: Kelly Guo <kellyg@nvidia.com> Signed-off-by: ooctipus <zhengyuz@nvidia.com> Co-authored-by: Ashwin Khadke <133695616+akhadke-bdai@users.noreply.github.com> Co-authored-by: Kelly Guo <kellyg@nvidia.com> Co-authored-by: ooctipus <zhengyuz@nvidia.com>
1 parent 42a5bd2 commit 4fefc0d

File tree

7 files changed

+206
-36
lines changed

7 files changed

+206
-36
lines changed

source/isaaclab/config/extension.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
[package]
22

33
# Note: Semantic Versioning is used: https://semver.org/
4-
version = "0.44.10"
4+
version = "0.45.0"
5+
56

67
# Description
78
title = "Isaac Lab framework for Robot Learning"

source/isaaclab/docs/CHANGELOG.rst

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
Changelog
22
---------
33

4+
0.45.0 (2025-08-07)
5+
~~~~~~~~~~~~~~~~~~~
6+
7+
Added
8+
^^^^^
9+
10+
* Added :attr:`~isaaclab.sensors.contact_sensor.ContactSensorCfg.track_contact_points` to toggle tracking of contact
11+
point locations between sensor bodies and filtered bodies.
12+
* Added :attr:`~isaaclab.sensors.contact_sensor.ContactSensorCfg.max_contact_data_per_prim` to configure the maximum
13+
amount of contacts per sensor body.
14+
* Added :attr:`~isaaclab.sensors.contact_sensor.ContactSensorData.contact_pos_w` data field for tracking contact point
15+
locations.
16+
17+
418
0.44.12 (2025-08-12)
519
~~~~~~~~~~~~~~~~~~~
620

@@ -31,6 +45,7 @@ instantaneous term done count at reset. This let to inaccurate aggregation of te
3145
happeningduring the traing. Instead we log the episodic term done.
3246

3347

48+
3449
0.44.9 (2025-07-30)
3550
~~~~~~~~~~~~~~~~~~~
3651

@@ -330,7 +345,6 @@ Changed
330345
Added
331346
^^^^^
332347

333-
334348
* Added unit test for :func:`~isaaclab.utils.math.quat_inv`.
335349

336350
Fixed

source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ def reset(self, env_ids: Sequence[int] | None = None):
159159
self._data.last_air_time[env_ids] = 0.0
160160
self._data.current_contact_time[env_ids] = 0.0
161161
self._data.last_contact_time[env_ids] = 0.0
162+
# reset contact positions
163+
if self.cfg.track_contact_points:
164+
self._data.contact_pos_w[env_ids, :] = torch.nan
165+
# buffer used during contact position aggregation
166+
self._contact_position_aggregate_buffer[env_ids, :] = torch.nan
162167

163168
def find_bodies(self, name_keys: str | Sequence[str], preserve_order: bool = False) -> tuple[list[int], list[str]]:
164169
"""Find bodies in the articulation based on the name keys.
@@ -277,7 +282,9 @@ def _initialize_impl(self):
277282
# create a rigid prim view for the sensor
278283
self._body_physx_view = self._physics_sim_view.create_rigid_body_view(body_names_glob)
279284
self._contact_physx_view = self._physics_sim_view.create_rigid_contact_view(
280-
body_names_glob, filter_patterns=filter_prim_paths_glob
285+
body_names_glob,
286+
filter_patterns=filter_prim_paths_glob,
287+
max_contact_data_count=self.cfg.max_contact_data_count_per_prim * len(body_names) * self._num_envs,
281288
)
282289
# resolve the true count of bodies
283290
self._num_bodies = self.body_physx_view.count // self._num_envs
@@ -303,6 +310,19 @@ def _initialize_impl(self):
303310
if self.cfg.track_pose:
304311
self._data.pos_w = torch.zeros(self._num_envs, self._num_bodies, 3, device=self._device)
305312
self._data.quat_w = torch.zeros(self._num_envs, self._num_bodies, 4, device=self._device)
313+
# -- position of contact points
314+
if self.cfg.track_contact_points:
315+
self._data.contact_pos_w = torch.full(
316+
(self._num_envs, self._num_bodies, self.contact_physx_view.filter_count, 3),
317+
torch.nan,
318+
device=self._device,
319+
)
320+
# buffer used during contact position aggregation
321+
self._contact_position_aggregate_buffer = torch.full(
322+
(self._num_bodies * self._num_envs, self.contact_physx_view.filter_count, 3),
323+
torch.nan,
324+
device=self._device,
325+
)
306326
# -- air/contact time between contacts
307327
if self.cfg.track_air_time:
308328
self._data.last_air_time = torch.zeros(self._num_envs, self._num_bodies, device=self._device)
@@ -357,6 +377,35 @@ def _update_buffers_impl(self, env_ids: Sequence[int]):
357377
pose[..., 3:] = convert_quat(pose[..., 3:], to="wxyz")
358378
self._data.pos_w[env_ids], self._data.quat_w[env_ids] = pose.split([3, 4], dim=-1)
359379

380+
# obtain contact points
381+
if self.cfg.track_contact_points:
382+
_, buffer_contact_points, _, _, buffer_count, buffer_start_indices = (
383+
self.contact_physx_view.get_contact_data(dt=self._sim_physics_dt)
384+
)
385+
# unpack the contact points: see RigidContactView.get_contact_data() documentation for details:
386+
# 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
387+
# buffer_count: (N_envs * N_bodies, N_filters), buffer_contact_points: (N_envs * N_bodies, 3)
388+
counts, starts = buffer_count.view(-1), buffer_start_indices.view(-1)
389+
n_rows, total = counts.numel(), int(counts.sum())
390+
# default to NaN rows
391+
agg = torch.full((n_rows, 3), float("nan"), device=self._device, dtype=buffer_contact_points.dtype)
392+
if total > 0:
393+
row_ids = torch.repeat_interleave(torch.arange(n_rows, device=self._device), counts)
394+
total = row_ids.numel()
395+
396+
block_starts = counts.cumsum(0) - counts
397+
deltas = torch.arange(total, device=counts.device) - block_starts.repeat_interleave(counts)
398+
flat_idx = starts[row_ids] + deltas
399+
400+
pts = buffer_contact_points.index_select(0, flat_idx)
401+
agg = agg.zero_().index_add_(0, row_ids, pts) / counts.clamp_min(1).unsqueeze(1)
402+
agg[counts == 0] = float("nan")
403+
404+
self._contact_position_aggregate_buffer[:] = agg.view(self._num_envs * self.num_bodies, -1, 3)
405+
self._data.contact_pos_w[env_ids] = self._contact_position_aggregate_buffer.view(
406+
self._num_envs, self._num_bodies, self.contact_physx_view.filter_count, 3
407+
)[env_ids]
408+
360409
# obtain the air time
361410
if self.cfg.track_air_time:
362411
# -- time elapsed since last update

source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor_cfg.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,22 @@ class ContactSensorCfg(SensorBaseCfg):
2020
track_pose: bool = False
2121
"""Whether to track the pose of the sensor's origin. Defaults to False."""
2222

23+
track_contact_points: bool = False
24+
"""Whether to track the contact point locations. Defaults to False."""
25+
26+
max_contact_data_count_per_prim: int = 4
27+
"""The maximum number of contacts across all batches of the sensor to keep track of. Default is 4.
28+
29+
This parameter sets the total maximum counts of the simulation across all bodies and environments. The total number
30+
of contacts allowed is max_contact_data_count_per_prim*num_envs*num_sensor_bodies.
31+
32+
.. note::
33+
34+
If the environment is very contact rich it is suggested to increase this parameter to avoid out of bounds memory
35+
errors and loss of contact data leading to inaccurate measurements.
36+
37+
"""
38+
2339
track_air_time: bool = False
2440
"""Whether to track the air/contact time of the bodies (time between contacts). Defaults to False."""
2541

@@ -49,6 +65,7 @@ class ContactSensorCfg(SensorBaseCfg):
4965
single primitive in that environment. If the sensor primitive corresponds to multiple primitives, the
5066
filtering will not work as expected. Please check :class:`~isaaclab.sensors.contact_sensor.ContactSensor`
5167
for more details.
68+
If track_contact_points is true, then filter_prim_paths_expr cannot be an empty list!
5269
"""
5370

5471
visualizer_cfg: VisualizationMarkersCfg = CONTACT_SENSOR_MARKER_CFG.replace(prim_path="/Visuals/ContactSensor")

source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor_data.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,21 @@ class ContactSensorData:
2323
If the :attr:`ContactSensorCfg.track_pose` is False, then this quantity is None.
2424
"""
2525

26+
contact_pos_w: torch.Tensor | None = None
27+
"""Average of the positions of contact points between sensor body and filter prim in world frame.
28+
29+
Shape is (N, B, M, 3), where N is the number of sensors, B is number of bodies in each sensor
30+
and M is the number of filtered bodies.
31+
32+
Collision pairs not in contact will result in nan.
33+
34+
Note:
35+
If the :attr:`ContactSensorCfg.track_contact_points` is False, then this quantity is None.
36+
If the :attr:`ContactSensorCfg.filter_prim_paths_expr` is empty, then this quantity is an empty tensor.
37+
If the :attr:`ContactSensorCfg.max_contact_data_per_prim` is not specified or less than 1, then this quantity
38+
will not be calculated.
39+
"""
40+
2641
quat_w: torch.Tensor | None = None
2742
"""Orientation of the sensor origin in quaternion (w, x, y, z) in world frame.
2843

source/isaaclab/test/sensors/check_contact_sensor.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
# add argparse arguments
2222
parser = argparse.ArgumentParser(description="Contact Sensor Test Script")
23-
parser.add_argument("--num_robots", type=int, default=64, help="Number of robots to spawn.")
23+
parser.add_argument("--num_robots", type=int, default=128, help="Number of robots to spawn.")
2424

2525
# append AppLauncher cli args
2626
AppLauncher.add_app_launcher_args(parser)
@@ -45,6 +45,7 @@
4545
import isaaclab.sim as sim_utils
4646
from isaaclab.assets import Articulation
4747
from isaaclab.sensors.contact_sensor import ContactSensor, ContactSensorCfg
48+
from isaaclab.utils.timer import Timer
4849

4950
##
5051
# Pre-defined configs
@@ -63,9 +64,8 @@ def design_scene():
6364
cfg = sim_utils.GroundPlaneCfg()
6465
cfg.func("/World/defaultGroundPlane", cfg)
6566
# Lights
66-
cfg = sim_utils.SphereLightCfg()
67-
cfg.func("/World/Light/GreySphere", cfg, translation=(4.5, 3.5, 10.0))
68-
cfg.func("/World/Light/WhiteSphere", cfg, translation=(-4.5, 3.5, 10.0))
67+
cfg = sim_utils.DomeLightCfg(intensity=2000)
68+
cfg.func("/World/Light/DomeLight", cfg, translation=(-4.5, 3.5, 10.0))
6969

7070

7171
"""
@@ -103,7 +103,11 @@ def main():
103103
robot = Articulation(cfg=robot_cfg)
104104
# Contact sensor
105105
contact_sensor_cfg = ContactSensorCfg(
106-
prim_path="/World/envs/env_.*/Robot/.*_SHANK", track_air_time=True, debug_vis=not args_cli.headless
106+
prim_path="/World/envs/env_.*/Robot/.*_FOOT",
107+
track_air_time=True,
108+
track_contact_points=True,
109+
debug_vis=False, # not args_cli.headless,
110+
filter_prim_paths_expr=["/World/defaultGroundPlane/GroundPlane/CollisionPlane"],
107111
)
108112
contact_sensor = ContactSensor(cfg=contact_sensor_cfg)
109113
# filter collisions within each environment instance
@@ -126,6 +130,7 @@ def main():
126130
sim_dt = decimation * physics_dt
127131
sim_time = 0.0
128132
count = 0
133+
dt = []
129134
# Simulate physics
130135
while simulation_app.is_running():
131136
# If simulation is stopped, then exit.
@@ -136,14 +141,20 @@ def main():
136141
sim.step(render=False)
137142
continue
138143
# reset
139-
if count % 1000 == 0:
144+
if count % 1000 == 0 and count != 0:
140145
# reset counters
141146
sim_time = 0.0
142147
count = 0
148+
print("=" * 80)
149+
print("avg dt real-time", sum(dt) / len(dt))
150+
print("=" * 80)
151+
143152
# reset dof state
144153
joint_pos, joint_vel = robot.data.default_joint_pos, robot.data.default_joint_vel
145154
robot.write_joint_state_to_sim(joint_pos, joint_vel)
146155
robot.reset()
156+
dt = []
157+
147158
# perform 4 steps
148159
for _ in range(decimation):
149160
# apply actions
@@ -159,6 +170,10 @@ def main():
159170
count += 1
160171
# update the buffers
161172
if sim.is_playing():
173+
with Timer() as timer:
174+
contact_sensor.update(sim_dt, force_recompute=True)
175+
dt.append(timer.time_elapsed)
176+
162177
contact_sensor.update(sim_dt, force_recompute=True)
163178
if count % 100 == 0:
164179
print("Sim-time: ", sim_time)

0 commit comments

Comments
 (0)