Contact Sensor#
The contact sensor is designed to return the net contact force acting on a given ridgid body. The sensor is written to behave as a physical object, and so the “scope” of the contact sensor is limited to the body (or bodies) that defines it. There are multiple ways to define this scope, depending on your need to filter the forces coming from the contact.
By default, the reported force is the total contact force, but your application may only care about contact forces due to specific objects. Retrieving contact forces from specific objects requires filtering, and this can only be done in a “many-to-one” way. A multi-legged robot that needs filterable contact information for its feet would require one sensor per foot to be defined in the environment, but a robotic hand with contact sensors on the tips of each finger can be defined with a single sensor.
Consider a simple environment with an Anymal Quadruped and a block
from isaaclab.utils.configclass import configclass
##
# Pre-defined configs
##
from isaaclab_assets.robots.anymal import ANYMAL_C_CFG # isort: skip
@configclass
class ContactSensorSceneCfg(InteractiveSceneCfg):
"""Design the scene with sensors on the robot."""
# ground plane
ground = AssetBaseCfg(prim_path="/World/defaultGroundPlane", spawn=sim_utils.GroundPlaneCfg())
# lights
dome_light = AssetBaseCfg(
prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75))
)
# robot
robot = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
# Rigid Object
cube = RigidObjectCfg(
prim_path="{ENV_REGEX_NS}/Cube",
spawn=sim_utils.CuboidCfg(
size=(0.5, 0.5, 0.1),
rigid_props=sim_utils.RigidBodyPropertiesCfg(),
mass_props=sim_utils.MassPropertiesCfg(mass=100.0),
collision_props=sim_utils.CollisionPropertiesCfg(),
physics_material=sim_utils.RigidBodyMaterialCfg(static_friction=1.0),
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0), metallic=0.2),
),
init_state=RigidObjectCfg.InitialStateCfg(pos=(0.5, 0.5, 0.05)),
)
contact_forces_LF = ContactSensorCfg(
prim_path="{ENV_REGEX_NS}/Robot/LF_FOOT",
update_period=0.0,
history_length=6,
debug_vis=True,
filter_prim_paths_expr=["{ENV_REGEX_NS}/Cube"],
)
contact_forces_RF = ContactSensorCfg(
prim_path="{ENV_REGEX_NS}/Robot/RF_FOOT",
update_period=0.0,
history_length=6,
debug_vis=True,
filter_prim_paths_expr=["{ENV_REGEX_NS}/Cube"],
We define the sensors on the feet of the robot in two different ways. The front feet are independent sensors (one sensor body per foot) and the “Cube” is placed under the left foot. The hind feet are defined as a single sensor with multiple bodies.
We can then run the scene and print the data from the sensors
def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene):
.
.
.
# Simulate physics
while simulation_app.is_running():
.
.
.
# print information from the sensors
print("-------------------------------")
print(scene["contact_forces_LF"])
print("Received force matrix of: ", scene["contact_forces_LF"].data.force_matrix_w)
print("Received contact force of: ", scene["contact_forces_LF"].data.net_forces_w)
print("-------------------------------")
print(scene["contact_forces_RF"])
print("Received force matrix of: ", scene["contact_forces_RF"].data.force_matrix_w)
print("Received contact force of: ", scene["contact_forces_RF"].data.net_forces_w)
print("-------------------------------")
print(scene["contact_forces_H"])
print("Received force matrix of: ", scene["contact_forces_H"].data.force_matrix_w)
print("Received contact force of: ", scene["contact_forces_H"].data.net_forces_w)
Here, we print both the net contact force and the filtered force matrix for each contact sensor defined in the scene. The front left and front right feet report the following
-------------------------------
Contact sensor @ '/World/envs/env_.*/Robot/LF_FOOT':
view type : <class 'omni.physics.tensors.api.RigidBodyView'>
update period (s) : 0.0
number of bodies : 1
body names : ['LF_FOOT']
Received force matrix of: tensor([[[[-1.3923e-05, 1.5727e-04, 1.1032e+02]]]], device='cuda:0')
Received contact force of: tensor([[[-1.3923e-05, 1.5727e-04, 1.1032e+02]]], device='cuda:0')
-------------------------------
Contact sensor @ '/World/envs/env_.*/Robot/RF_FOOT':
view type : <class 'omni.physics.tensors.api.RigidBodyView'>
update period (s) : 0.0
number of bodies : 1
body names : ['RF_FOOT']
Received force matrix of: tensor([[[[0., 0., 0.]]]], device='cuda:0')
Received contact force of: tensor([[[1.3529e-05, 0.0000e+00, 1.0069e+02]]], device='cuda:0')
Notice that even with filtering, both sensors report the net contact force acting on the foot. However, the “force matrix” on the right foot is zero because that foot isn’t in contact with the filtered body, /World/envs/env_.*/Cube. Now, checkout the data coming from the hind feet!
-------------------------------
Contact sensor @ '/World/envs/env_.*/Robot/.*H_FOOT':
view type : <class 'omni.physics.tensors.api.RigidBodyView'>
update period (s) : 0.0
number of bodies : 2
body names : ['LH_FOOT', 'RH_FOOT']
Received force matrix of: None
Received contact force of: tensor([[[9.7227e-06, 0.0000e+00, 7.2364e+01],
[2.4322e-05, 0.0000e+00, 1.8102e+02]]], device='cuda:0')
In this case, the contact sensor has two bodies: the left and right hind feet. When the force matrix is queried, the result is None because this is a many body sensor, and presently Isaac Lab only supports “many to one” contact force filtering. Unlike the single body contact sensor, the reported force tensor has multiple entries, with each “row” corresponding to the contact force on a single body of the sensor (matching the ordering at construction).
Code for contact_sensor.py
1# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
2# All rights reserved.
3#
4# SPDX-License-Identifier: BSD-3-Clause
5
6"""Launch Isaac Sim Simulator first."""
7
8import argparse
9
10from isaaclab.app import AppLauncher
11
12# add argparse arguments
13parser = argparse.ArgumentParser(description="Example on using the contact sensor.")
14parser.add_argument("--num_envs", type=int, default=1, help="Number of environments to spawn.")
15parser.add_argument(
16 "--physics",
17 default="isaacsim_physx",
18 choices=["isaacsim_physx"],
19 help="Physics backend.",
20)
21# append AppLauncher cli args
22AppLauncher.add_app_launcher_args(parser)
23# demos should open Kit visualizer by default
24parser.set_defaults(visualizer=["kit"])
25# parse the arguments
26args_cli = parser.parse_args()
27
28# launch omniverse app
29app_launcher = AppLauncher(args_cli)
30simulation_app = app_launcher.app
31
32"""Rest everything follows."""
33
34import torch
35
36import isaaclab.sim as sim_utils
37from isaaclab.assets import AssetBaseCfg, RigidObjectCfg
38from isaaclab.scene import InteractiveScene, InteractiveSceneCfg
39from isaaclab.sensors import ContactSensorCfg
40from isaaclab.utils.configclass import configclass
41
42##
43# Pre-defined configs
44##
45from isaaclab_assets.robots.anymal import ANYMAL_C_CFG # isort: skip
46
47
48@configclass
49class ContactSensorSceneCfg(InteractiveSceneCfg):
50 """Design the scene with sensors on the robot."""
51
52 # ground plane
53 ground = AssetBaseCfg(prim_path="/World/defaultGroundPlane", spawn=sim_utils.GroundPlaneCfg())
54
55 # lights
56 dome_light = AssetBaseCfg(
57 prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75))
58 )
59
60 # robot
61 robot = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
62
63 # Rigid Object
64 cube = RigidObjectCfg(
65 prim_path="{ENV_REGEX_NS}/Cube",
66 spawn=sim_utils.CuboidCfg(
67 size=(0.5, 0.5, 0.1),
68 rigid_props=sim_utils.RigidBodyPropertiesCfg(),
69 mass_props=sim_utils.MassPropertiesCfg(mass=100.0),
70 collision_props=sim_utils.CollisionPropertiesCfg(),
71 physics_material=sim_utils.RigidBodyMaterialCfg(static_friction=1.0),
72 visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0), metallic=0.2),
73 ),
74 init_state=RigidObjectCfg.InitialStateCfg(pos=(0.5, 0.5, 0.05)),
75 )
76
77 contact_forces_LF = ContactSensorCfg(
78 prim_path="{ENV_REGEX_NS}/Robot/LF_FOOT",
79 update_period=0.0,
80 history_length=6,
81 debug_vis=True,
82 filter_prim_paths_expr=["{ENV_REGEX_NS}/Cube"],
83 )
84
85 contact_forces_RF = ContactSensorCfg(
86 prim_path="{ENV_REGEX_NS}/Robot/RF_FOOT",
87 update_period=0.0,
88 history_length=6,
89 debug_vis=True,
90 filter_prim_paths_expr=["{ENV_REGEX_NS}/Cube"],
91 )
92
93 contact_forces_H = ContactSensorCfg(
94 prim_path="{ENV_REGEX_NS}/Robot/.*H_FOOT",
95 update_period=0.0,
96 history_length=6,
97 debug_vis=True,
98 )
99
100
101def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene):
102 """Run the simulator."""
103 # Define simulation stepping
104 sim_dt = sim.get_physics_dt()
105 sim_time = 0.0
106 count = 0
107
108 # Simulate physics
109 while simulation_app.is_running():
110 if count % 500 == 0:
111 # reset counter
112 count = 0
113 # reset the scene entities
114 # root state
115 # we offset the root state by the origin since the states are written in simulation world frame
116 # if this is not done, then the robots will be spawned at the (0, 0, 0) of the simulation world
117 root_pose = scene["robot"].data.default_root_pose.torch.clone()
118 root_pose[:, :3] += scene.env_origins
119 scene["robot"].write_root_pose_to_sim_index(root_pose=root_pose)
120 root_vel = scene["robot"].data.default_root_vel.torch.clone()
121 scene["robot"].write_root_velocity_to_sim_index(root_velocity=root_vel)
122 # set joint positions with some noise
123 joint_pos, joint_vel = (
124 scene["robot"].data.default_joint_pos.torch.clone(),
125 scene["robot"].data.default_joint_vel.torch.clone(),
126 )
127 joint_pos += torch.rand_like(joint_pos) * 0.1
128 scene["robot"].write_joint_position_to_sim_index(position=joint_pos)
129 scene["robot"].write_joint_velocity_to_sim_index(velocity=joint_vel)
130 # clear internal buffers
131 scene.reset()
132 print("[INFO]: Resetting robot state...")
133 # Apply default actions to the robot
134 # -- generate actions/commands
135 targets = scene["robot"].data.default_joint_pos.torch
136 # -- apply action to the robot
137 scene["robot"].set_joint_position_target_index(target=targets)
138 # -- write data to sim
139 scene.write_data_to_sim()
140 # perform step
141 sim.step()
142 # update sim-time
143 sim_time += sim_dt
144 count += 1
145 # update buffers
146 scene.update(sim_dt)
147
148 # print information from the sensors
149 print("-------------------------------")
150 print(scene["contact_forces_LF"])
151 print("Received force matrix of: ", scene["contact_forces_LF"].data.force_matrix_w)
152 print("Received contact force of: ", scene["contact_forces_LF"].data.net_forces_w)
153 print("-------------------------------")
154 print(scene["contact_forces_RF"])
155 print("Received force matrix of: ", scene["contact_forces_RF"].data.force_matrix_w)
156 print("Received contact force of: ", scene["contact_forces_RF"].data.net_forces_w)
157 print("-------------------------------")
158 print(scene["contact_forces_H"])
159 print("Received force matrix of: ", scene["contact_forces_H"].data.force_matrix_w)
160 print("Received contact force of: ", scene["contact_forces_H"].data.net_forces_w)
161
162
163def main():
164 """Main function."""
165
166 # Initialize the simulation context
167 sim_cfg = sim_utils.SimulationCfg(dt=0.005, device=args_cli.device)
168 sim = sim_utils.SimulationContext(sim_cfg)
169 # Set main camera
170 sim.set_camera_view(eye=[3.5, 3.5, 3.5], target=[0.0, 0.0, 0.0])
171 # design scene
172 scene_cfg = ContactSensorSceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0)
173 scene = InteractiveScene(scene_cfg)
174 # Play the simulator
175 sim.reset()
176 # Now we are ready!
177 print("[INFO]: Setup complete...")
178 # Run the simulator
179 run_simulator(sim, scene)
180
181
182if __name__ == "__main__":
183 # run the main function
184 main()
185 # close sim app
186 simulation_app.close()