Source code for isaaclab.sensors.contact_sensor.base_contact_sensor
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations
import warnings
from abc import abstractmethod
from collections.abc import Sequence
from typing import TYPE_CHECKING
import warp as wp
import isaaclab.utils.string as string_utils
from isaaclab.utils.warp import ProxyArray
from ..sensor_base import SensorBase
from .base_contact_sensor_data import BaseContactSensorData
if TYPE_CHECKING:
from .contact_sensor_cfg import ContactSensorCfg
[docs]
class BaseContactSensor(SensorBase):
"""A contact reporting sensor.
The contact sensor reports the normal contact forces on a rigid body in the world frame.
It relies on the physics engine's contact reporting API to be activated on the rigid bodies.
To enable the contact reporter on a rigid body, please make sure to enable the
:attr:`isaaclab.sim.spawner.RigidObjectSpawnerCfg.activate_contact_sensors` on your
asset spawner configuration. This will enable the contact reporter on all the rigid bodies
in the asset.
The sensor can be configured to report the contact forces on a set of bodies with a given
filter pattern using the :attr:`ContactSensorCfg.filter_prim_paths_expr`. This is useful
when you want to report the contact forces between the sensor bodies and a specific set of
bodies in the scene. The data can be accessed using the :attr:`ContactSensorData.force_matrix_w`.
The PhysX backend only supports one-to-many filtered contact reporting: a single sensor
body filtered against multiple partners. For many-to-many, create separate sensors per
body. The Newton backend supports many-to-many natively.
"""
cfg: ContactSensorCfg
"""The configuration parameters."""
__backend_name__: str = "base"
"""The name of the backend for the contact sensor."""
[docs]
def __init__(self, cfg: ContactSensorCfg):
"""Initializes the contact sensor object.
Args:
cfg: The configuration parameters.
"""
# initialize base class
super().__init__(cfg)
# check that config is valid
if cfg.history_length < 0:
raise ValueError(f"History length must be greater than 0! Received: {cfg.history_length}")
def update(self, dt: float, force_recompute: bool = False) -> None:
"""Advance the sensor timestamp and update history-bearing buffers.
Args:
dt: Time elapsed since the previous sensor update [s].
force_recompute: Whether to recompute the sensor buffers regardless of their
configured update period. Defaults to False.
"""
super().update(dt, force_recompute=force_recompute)
if self._is_initialized and self.cfg.history_length > 0:
self._update_outdated_buffers()
def __str__(self) -> str:
"""Returns: A string containing information about the instance."""
return (
f"Contact sensor @ '{self.cfg.prim_path}': \n"
f"\tupdate period (s) : {self.cfg.update_period}\n"
f"\tnumber of sensors : {self.num_sensors}\n"
f"\tbody names : {self.body_names}\n"
)
"""
Properties
"""
@property
@abstractmethod
def num_instances(self) -> int | None:
"""Number of instances of the sensor."""
raise NotImplementedError(f"Num instances is not implemented for {self.__class__.__name__}.")
@property
@abstractmethod
def data(self) -> BaseContactSensorData:
"""Data from the sensor."""
raise NotImplementedError(f"Data is not implemented for {self.__class__.__name__}.")
@property
@abstractmethod
def num_sensors(self) -> int:
"""Number of sensors per environment."""
raise NotImplementedError(f"Num sensors is not implemented for {self.__class__.__name__}.")
@property
@abstractmethod
def body_names(self) -> list[str] | None:
"""Ordered names of shapes or bodies with contact sensors attached."""
raise NotImplementedError(f"Body names is not implemented for {self.__class__.__name__}.")
@property
@abstractmethod
def contact_view(self) -> None:
"""View for the contact forces captured.
.. note::
None if there is no view associated with the sensor.
"""
raise NotImplementedError(f"Contact view is not implemented for {self.__class__.__name__}.")
"""
Operations
"""
@abstractmethod
def reset(self, env_ids: Sequence[int] | None = None, env_mask: wp.array(dtype=wp.bool) | None = None):
"""Resets the sensor.
Args:
env_ids: The indices of the environments to reset. Defaults to None: all the environments are reset.
env_mask: The masks of the environments to reset. Defaults to None: all the environments are reset.
"""
# reset the timers and counters
super().reset(env_ids, env_mask)
def find_sensors(self, name_keys: str | Sequence[str], preserve_order: bool = False) -> tuple[list[int], list[str]]:
"""Find sensors in the contact sensor based on the name keys.
Args:
name_keys: A regular expression or a list of regular expressions to match the body names.
preserve_order: Whether to preserve the order of the name keys in the output. Defaults to False.
Returns:
A tuple of lists containing the sensor indices and names.
"""
return string_utils.resolve_matching_names(name_keys, self.body_names, preserve_order)
@abstractmethod
def compute_first_contact(self, dt: float, abs_tol: float = 1.0e-8) -> ProxyArray:
"""Checks if bodies that have established contact within the last :attr:`dt` seconds.
This function checks if the bodies have established contact within the last :attr:`dt` seconds
by comparing the current contact time with the given time period. If the contact time is less
than the given time period, then the bodies are considered to be in contact.
.. note::
The function assumes that :attr:`dt` is a factor of the sensor update time-step. In other
words :math:`dt / dt_sensor = n`, where :math:`n` is a natural number. This is always true
if the sensor is updated by the physics or the environment stepping time-step and the sensor
is read by the environment stepping time-step.
Args:
dt: The time period since the contact was established.
abs_tol: The absolute tolerance for the comparison.
Returns:
A boolean tensor indicating the bodies that have established contact within the last
:attr:`dt` seconds. Shape is (N, B), where N is the number of sensors and B is the
number of bodies in each sensor.
Raises:
RuntimeError: If the sensor is not configured to track contact time.
"""
raise NotImplementedError(f"Compute first contact is not implemented for {self.__class__.__name__}.")
@abstractmethod
def compute_first_air(self, dt: float, abs_tol: float = 1.0e-8) -> ProxyArray:
"""Checks if bodies that have broken contact within the last :attr:`dt` seconds.
This function checks if the bodies have broken contact within the last :attr:`dt` seconds
by comparing the current air time with the given time period. If the air time is less
than the given time period, then the bodies are considered to not be in contact.
.. note::
It assumes that :attr:`dt` is a factor of the sensor update time-step. In other words,
:math:`dt / dt_sensor = n`, where :math:`n` is a natural number. This is always true if
the sensor is updated by the physics or the environment stepping time-step and the sensor
is read by the environment stepping time-step.
Args:
dt: The time period since the contract is broken.
abs_tol: The absolute tolerance for the comparison.
Returns:
A boolean tensor indicating the bodies that have broken contact within the last :attr:`dt` seconds.
Shape is (N, B), where N is the number of sensors and B is the number of bodies in each sensor.
Raises:
RuntimeError: If the sensor is not configured to track contact time.
"""
raise NotImplementedError(f"Compute first air is not implemented for {self.__class__.__name__}.")
"""
Implementation.
"""
@abstractmethod
def _initialize_impl(self):
super()._initialize_impl()
@abstractmethod
def _create_buffers(self):
"""Creates the buffers for the sensor data."""
raise NotImplementedError(f"Create buffers is not implemented for {self.__class__.__name__}.")
@abstractmethod
def _update_buffers_impl(self, env_mask: wp.array | None):
"""Fills the buffers of the sensor data.
Args:
env_mask: Mask of the environments to update. None: update all environments.
"""
raise NotImplementedError(f"Update buffers is not implemented for {self.__class__.__name__}.")
def _invalidate_initialize_callback(self, event):
"""Invalidates the scene elements."""
# call parent
super()._invalidate_initialize_callback(event)
# set all existing views to None to invalidate them
# TODO: invalidate NewtonManager if necessary
@property
def num_bodies(self) -> int:
"""Deprecated property. Please use `num_sensors` instead."""
warnings.warn(
"The `num_bodies` property will be deprecated in a future release. Please use `num_sensors` instead.",
DeprecationWarning,
stacklevel=2,
)
return self.num_sensors
def find_bodies(self, name_keys: str | Sequence[str], preserve_order: bool = False) -> tuple[list[int], list[str]]:
"""Deprecated method. Please use `find_sensors` instead."""
warnings.warn(
"The `find_bodies` method will be deprecated in a future release. Please use `find_sensors` instead.",
DeprecationWarning,
stacklevel=2,
)
return self.find_sensors(name_keys, preserve_order)