Commit 17d781ce authored by Mayank Mittal's avatar Mayank Mittal Committed by Mayank Mittal

Fixes parsing of filter prim path expressions in ContactSensor (#385)

PhysX view classes expect the expressions to be in "Glob" format instead
of "Regex". While we did this conversion for the body views, it was
missing for the filter prim paths expressions. This MR fixes this issue.

Fixes https://github.com/NVIDIA-Omniverse/orbit/issues/364

- Bug fix (non-breaking change which fixes an issue)

- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./orbit.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
- [x] I have run all the tests with `./orbit.sh --test` and they pass
- [x] I have updated the changelog and the corresponding version in the
extension's `config/extension.toml` file
- [x] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there
parent 440c57b4
[package] [package]
# Note: Semantic Versioning is used: https://semver.org/ # Note: Semantic Versioning is used: https://semver.org/
version = "0.16.1" version = "0.16.2"
# Description # Description
title = "ORBIT framework for Robot Learning" title = "ORBIT framework for Robot Learning"
......
Changelog Changelog
--------- ---------
0.16.2 (2024-04-26)
~~~~~~~~~~~~~~~~~~~
Fixed
^^^^^
* Fixed parsing of filter prim path expressions in the :class:`omni.isaac.orbit.sensors.ContactSensor` class.
Earlier, the filter prim paths given to the physics view was not being parsed since they were specified as
regex expressions instead of glob expressions.
0.16.1 (2024-04-20) 0.16.1 (2024-04-20)
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
......
...@@ -16,7 +16,7 @@ from pxr import PhysxSchema ...@@ -16,7 +16,7 @@ from pxr import PhysxSchema
import omni.isaac.orbit.sim as sim_utils import omni.isaac.orbit.sim as sim_utils
from omni.isaac.orbit.assets import Articulation, ArticulationCfg, AssetBaseCfg, RigidObject, RigidObjectCfg from omni.isaac.orbit.assets import Articulation, ArticulationCfg, AssetBaseCfg, RigidObject, RigidObjectCfg
from omni.isaac.orbit.sensors import FrameTransformerCfg, SensorBase, SensorBaseCfg from omni.isaac.orbit.sensors import ContactSensorCfg, FrameTransformerCfg, SensorBase, SensorBaseCfg
from omni.isaac.orbit.terrains import TerrainImporter, TerrainImporterCfg from omni.isaac.orbit.terrains import TerrainImporter, TerrainImporterCfg
from .interactive_scene_cfg import InteractiveSceneCfg from .interactive_scene_cfg import InteractiveSceneCfg
...@@ -348,6 +348,11 @@ class InteractiveScene: ...@@ -348,6 +348,11 @@ class InteractiveScene:
target_frame.prim_path = target_frame.prim_path.format(ENV_REGEX_NS=self.env_regex_ns) target_frame.prim_path = target_frame.prim_path.format(ENV_REGEX_NS=self.env_regex_ns)
updated_target_frames.append(target_frame) updated_target_frames.append(target_frame)
asset_cfg.target_frames = updated_target_frames asset_cfg.target_frames = updated_target_frames
elif isinstance(asset_cfg, ContactSensorCfg):
updated_filter_prim_paths_expr = []
for filter_prim_path in asset_cfg.filter_prim_paths_expr:
updated_filter_prim_paths_expr.append(filter_prim_path.format(ENV_REGEX_NS=self.env_regex_ns))
asset_cfg.filter_prim_paths_expr = updated_filter_prim_paths_expr
self._sensors[asset_name] = asset_cfg.class_type(asset_cfg) self._sensors[asset_name] = asset_cfg.class_type(asset_cfg)
elif isinstance(asset_cfg, AssetBaseCfg): elif isinstance(asset_cfg, AssetBaseCfg):
......
...@@ -245,14 +245,18 @@ class ContactSensor(SensorBase): ...@@ -245,14 +245,18 @@ class ContactSensor(SensorBase):
f"Sensor at path '{self.cfg.prim_path}' could not find any bodies with contact reporter API." f"Sensor at path '{self.cfg.prim_path}' could not find any bodies with contact reporter API."
"\nHINT: Make sure to enable 'activate_contact_sensors' in the corresponding asset spawn configuration." "\nHINT: Make sure to enable 'activate_contact_sensors' in the corresponding asset spawn configuration."
) )
# construct regex expression for the body names # construct regex expression for the body names
body_names_regex = r"(" + "|".join(body_names) + r")" body_names_regex = r"(" + "|".join(body_names) + r")"
body_names_regex = f"{self.cfg.prim_path.rsplit('/', 1)[0]}/{body_names_regex}" body_names_regex = f"{self.cfg.prim_path.rsplit('/', 1)[0]}/{body_names_regex}"
# construct a new regex expression # convert regex expressions to glob expressions for PhysX
body_names_glob = body_names_regex.replace(".*", "*")
filter_prim_paths_glob = [expr.replace(".*", "*") for expr in self.cfg.filter_prim_paths_expr]
# create a rigid prim view for the sensor # create a rigid prim view for the sensor
self._body_physx_view = self._physics_sim_view.create_rigid_body_view(body_names_regex.replace(".*", "*")) self._body_physx_view = self._physics_sim_view.create_rigid_body_view(body_names_glob)
self._contact_physx_view = self._physics_sim_view.create_rigid_contact_view( self._contact_physx_view = self._physics_sim_view.create_rigid_contact_view(
body_names_regex.replace(".*", "*"), filter_patterns=self.cfg.filter_prim_paths_expr body_names_glob, filter_patterns=filter_prim_paths_glob
) )
# resolve the true count of bodies # resolve the true count of bodies
self._num_bodies = self.body_physx_view.count // self._num_envs self._num_bodies = self.body_physx_view.count // self._num_envs
......
...@@ -31,19 +31,24 @@ class ContactSensorCfg(SensorBaseCfg): ...@@ -31,19 +31,24 @@ class ContactSensorCfg(SensorBaseCfg):
""" """
filter_prim_paths_expr: list[str] = list() filter_prim_paths_expr: list[str] = list()
"""The list of primitive paths to filter contacts with. """The list of primitive paths (or expressions) to filter contacts with. Defaults to an empty list, in which case
no filtering is applied.
For example, if you want to filter contacts with the ground plane, you can set this to The contact sensor allows reporting contacts between the primitive specified with :attr:`prim_path` and
``["/World/ground_plane"]``. In this case, the contact sensor will only report contacts other primitives in the scene. For instance, in a scene containing a robot, a ground plane and an object,
with the ground plane while using the :meth:`omni.isaac.core.prims.RigidContactView.get_contact_force_matrix` you can obtain individual contact reports of the base of the robot with the ground plane and the object.
method.
.. note::
The expression in the list can contain the environment namespace regex ``{ENV_REGEX_NS}`` which
will be replaced with the environment namespace.
Example: ``{ENV_REGEX_NS}/Object`` will be replaced with ``/World/envs/env_.*/Object``.
If an empty list is provided, then only net contact forces are reported.
""" """
visualizer_cfg: VisualizationMarkersCfg = CONTACT_SENSOR_MARKER_CFG.replace(prim_path="/Visuals/ContactSensor") visualizer_cfg: VisualizationMarkersCfg = CONTACT_SENSOR_MARKER_CFG.replace(prim_path="/Visuals/ContactSensor")
"""The configuration object for the visualization markers. Defaults to CONTACT_SENSOR_MARKER_CFG. """The configuration object for the visualization markers. Defaults to CONTACT_SENSOR_MARKER_CFG.
Note: .. note::
This attribute is only used when debug visualization is enabled. This attribute is only used when debug visualization is enabled.
""" """
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
"""Base class for sensors. """Base class for sensors.
This class defines an interface for sensors similar to how the :class:`omni.isaac.orbit.robot.robot_base.RobotBase` class works. This class defines an interface for sensors similar to how the :class:`omni.isaac.orbit.assets.AssetBase` class works.
Each sensor class should inherit from this class and implement the abstract methods. Each sensor class should inherit from this class and implement the abstract methods.
""" """
......
...@@ -20,8 +20,8 @@ from .spawners.materials import RigidBodyMaterialCfg ...@@ -20,8 +20,8 @@ from .spawners.materials import RigidBodyMaterialCfg
class PhysxCfg: class PhysxCfg:
"""Configuration for PhysX solver-related parameters. """Configuration for PhysX solver-related parameters.
These parameters are used to configure the PhysX solver. For more information, see the PhysX 5 SDK These parameters are used to configure the PhysX solver. For more information, see the `PhysX 5 SDK
documentation. documentation`_.
PhysX 5 supports GPU-accelerated physics simulation. This is enabled by default, but can be disabled PhysX 5 supports GPU-accelerated physics simulation. This is enabled by default, but can be disabled
through the flag `use_gpu`. Unlike CPU PhysX, the GPU simulation feature is not able to dynamically through the flag `use_gpu`. Unlike CPU PhysX, the GPU simulation feature is not able to dynamically
...@@ -29,8 +29,8 @@ class PhysxCfg: ...@@ -29,8 +29,8 @@ class PhysxCfg:
for GPU features. If insufficient buffer sizes are provided, the simulation will fail with errors and for GPU features. If insufficient buffer sizes are provided, the simulation will fail with errors and
lead to adverse behaviors. The buffer sizes can be adjusted through the `gpu_*` parameters. lead to adverse behaviors. The buffer sizes can be adjusted through the `gpu_*` parameters.
References: .. _PhysX 5 SDK documentation: https://nvidia-omniverse.github.io/PhysX/physx/5.3.1/_api_build/class_px_scene_desc.html
* PhysX 5 documentation: https://nvidia-omniverse.github.io/PhysX/
""" """
use_gpu: bool = True use_gpu: bool = True
......
...@@ -19,7 +19,7 @@ import omni.physx ...@@ -19,7 +19,7 @@ import omni.physx
from omni.isaac.core.simulation_context import SimulationContext as _SimulationContext from omni.isaac.core.simulation_context import SimulationContext as _SimulationContext
from omni.isaac.core.utils.viewports import set_camera_view from omni.isaac.core.utils.viewports import set_camera_view
from omni.isaac.version import get_version from omni.isaac.version import get_version
from pxr import Gf, Usd from pxr import Gf, PhysxSchema, Usd, UsdPhysics
from .simulation_cfg import SimulationCfg from .simulation_cfg import SimulationCfg
from .spawners import DomeLightCfg, GroundPlaneCfg from .spawners import DomeLightCfg, GroundPlaneCfg
...@@ -36,7 +36,7 @@ class SimulationContext(_SimulationContext): ...@@ -36,7 +36,7 @@ class SimulationContext(_SimulationContext):
* playing, pausing, stepping and stopping the simulation * playing, pausing, stepping and stopping the simulation
* adding and removing callbacks to different simulation events such as physics stepping, rendering, etc. * adding and removing callbacks to different simulation events such as physics stepping, rendering, etc.
This class inherits from the `omni.isaac.core.simulation_context.SimulationContext`_ class and This class inherits from the :class:`omni.isaac.core.simulation_context.SimulationContext` class and
adds additional functionalities such as setting up the simulation context with a configuration object, adds additional functionalities such as setting up the simulation context with a configuration object,
exposing other commonly used simulator-related functions, and performing version checks of Isaac Sim exposing other commonly used simulator-related functions, and performing version checks of Isaac Sim
to ensure compatibility between releases. to ensure compatibility between releases.
...@@ -65,8 +65,6 @@ class SimulationContext(_SimulationContext): ...@@ -65,8 +65,6 @@ class SimulationContext(_SimulationContext):
Based on above, for most functions in this class there is an equivalent function that is suffixed Based on above, for most functions in this class there is an equivalent function that is suffixed
with ``_async``. The ``_async`` functions are used in the Omniverse extension mode and with ``_async``. The ``_async`` functions are used in the Omniverse extension mode and
the non-``_async`` functions are used in the standalone python script mode. the non-``_async`` functions are used in the standalone python script mode.
.. _omni.isaac.core.simulation_context.SimulationContext: https://docs.omniverse.nvidia.com/py/isaacsim/source/extensions/omni.isaac.core/docs/index.html#module-omni.isaac.core.simulation_context
""" """
class RenderMode(enum.IntEnum): class RenderMode(enum.IntEnum):
...@@ -503,8 +501,8 @@ class SimulationContext(_SimulationContext): ...@@ -503,8 +501,8 @@ class SimulationContext(_SimulationContext):
def _set_additional_physx_params(self): def _set_additional_physx_params(self):
"""Sets additional PhysX parameters that are not directly supported by the parent class.""" """Sets additional PhysX parameters that are not directly supported by the parent class."""
# obtain the physics scene api # obtain the physics scene api
physics_scene = self._physics_context._physics_scene # pyright: ignore [reportPrivateUsage] physics_scene: UsdPhysics.Scene = self._physics_context._physics_scene
physx_scene_api = self._physics_context._physx_scene_api # pyright: ignore [reportPrivateUsage] physx_scene_api: PhysxSchema.PhysxSceneAPI = self._physics_context._physx_scene_api
# assert that scene api is not None # assert that scene api is not None
if physx_scene_api is None: if physx_scene_api is None:
raise RuntimeError("Physics scene API is None! Please create the scene first.") raise RuntimeError("Physics scene API is None! Please create the scene first.")
......
...@@ -70,6 +70,18 @@ class ContactSensorSceneCfg(InteractiveSceneCfg): ...@@ -70,6 +70,18 @@ class ContactSensorSceneCfg(InteractiveSceneCfg):
contact_sensor: ContactSensorCfg = MISSING contact_sensor: ContactSensorCfg = MISSING
"""Contact sensor configuration.""" """Contact sensor configuration."""
shape_2: TestContactSensorRigidObjectCfg = None
"""RigidObject contact prim configuration. Defaults to None, i.e. not included in the scene.
This is a second prim used for testing contact filtering.
"""
contact_sensor_2: ContactSensorCfg = None
"""Contact sensor configuration. Defaults to None, i.e. not included in the scene.
This is a second contact sensor used for testing contact filtering.
"""
## ##
# Scene entity configurations. # Scene entity configurations.
...@@ -223,6 +235,67 @@ class TestContactSensor(unittest.TestCase): ...@@ -223,6 +235,67 @@ class TestContactSensor(unittest.TestCase):
"""Checks contact sensor values for contact time and air time for a sphere collision primitive.""" """Checks contact sensor values for contact time and air time for a sphere collision primitive."""
self._run_contact_sensor_test(shape_cfg=SPHERE_CFG) self._run_contact_sensor_test(shape_cfg=SPHERE_CFG)
def test_cube_stack_contact_filtering(self):
"""Checks contact sensor reporting for filtering stacked cube prims."""
for device in self.devices:
for num_envs in [1, 6, 24]:
with self.subTest(device=device, num_envs=num_envs):
with build_simulation_context(device=device, dt=self.sim_dt, add_lighting=True) as sim:
# Instance new scene for the current terrain and contact prim.
scene_cfg = ContactSensorSceneCfg(num_envs=num_envs, env_spacing=1.0, lazy_sensor_update=False)
scene_cfg.terrain = FLAT_TERRAIN_CFG.replace(prim_path="/World/ground")
# -- cube 1
scene_cfg.shape = CUBE_CFG.replace(prim_path="{ENV_REGEX_NS}/Cube_1")
scene_cfg.shape.init_state.pos = (0, -1.0, 1.0)
# -- cube 2 (on top of cube 1)
scene_cfg.shape_2 = CUBE_CFG.replace(prim_path="{ENV_REGEX_NS}/Cube_2")
scene_cfg.shape_2.init_state.pos = (0, -1.0, 1.525)
# -- contact sensor 1
scene_cfg.contact_sensor = ContactSensorCfg(
prim_path="{ENV_REGEX_NS}/Cube_1",
track_pose=True,
debug_vis=False,
update_period=0.0,
filter_prim_paths_expr=["{ENV_REGEX_NS}/Cube_2"],
)
# -- contact sensor 2
scene_cfg.contact_sensor_2 = ContactSensorCfg(
prim_path="{ENV_REGEX_NS}/Cube_2",
track_pose=True,
debug_vis=False,
update_period=0.0,
filter_prim_paths_expr=["{ENV_REGEX_NS}/Cube_1"],
)
scene = InteractiveScene(scene_cfg)
# Set variables internally for reference
self.sim = sim
self.scene = scene
# Play the simulation
self.sim.reset()
# Extract from scene for type hinting
contact_sensor: ContactSensor = self.scene["contact_sensor"]
contact_sensor_2: ContactSensor = self.scene["contact_sensor_2"]
# Check buffers have the right size
self.assertEqual(contact_sensor.contact_physx_view.filter_count, 1)
self.assertEqual(contact_sensor_2.contact_physx_view.filter_count, 1)
# Reset the contact sensors
self.scene.reset()
# Let the scene come to a rest
for _ in range(20):
self._perform_sim_step()
# Check values for cube 2
torch.testing.assert_close(
contact_sensor_2.data.force_matrix_w[:, :, 0], contact_sensor_2.data.net_forces_w
)
torch.testing.assert_close(
contact_sensor_2.data.force_matrix_w[:, :, 0], contact_sensor.data.force_matrix_w[:, :, 0]
)
""" """
Internal helpers. Internal helpers.
""" """
...@@ -289,7 +362,7 @@ class TestContactSensor(unittest.TestCase): ...@@ -289,7 +362,7 @@ class TestContactSensor(unittest.TestCase):
sensor: The sensor reporting data to be verified by the contact sensor test. sensor: The sensor reporting data to be verified by the contact sensor test.
mode: The contact test mode: either contact with ground plane or air time. mode: The contact test mode: either contact with ground plane or air time.
""" """
# reset tge test state # reset the test state
sensor.reset() sensor.reset()
expected_last_test_contact_time = 0 expected_last_test_contact_time = 0
expected_last_reset_contact_time = 0 expected_last_reset_contact_time = 0
......
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