Contact Sensor#
The contact sensor is designed to return contact forces acting on a given rigid 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.
The aggregate normal force is reported as net_normal_forces_w. When supported by the backend and enabled with track_friction_forces, the aggregate friction force is reported as net_friction_forces_w. The total contact force is therefore
On Newton, net_forces_w is this total. PhysX and OVPhysX cannot compute a total contact force, so net_forces_w returns net_normal_forces_w and warns; this is a known limitation planned to be fixed in a later release. Use the explicit normal / friction properties when the split matters. History buffers are available for both normal and friction quantities (net_normal_forces_w_history, net_friction_forces_w_history, normal_force_matrix_w_history, and friction_force_matrix_w_history).
Your application may only care about contact forces due to specific objects. Retrieving contact forces from specific objects requires filtering. The per-filter normal and friction values are exposed as normal_force_matrix_w and friction_force_matrix_w. Summing a force matrix over its filter dimension only reconstructs the corresponding net force when the filters cover every contacting object.
PhysX does not expose an unfiltered aggregate friction force through its tensor API. Accessing net_friction_forces_w raises NotImplementedError. friction_forces_w is that aggregate quantity; on PhysX it returns friction_force_matrix_w and warns. Newton exposes both aggregate and filtered friction forces.
Consider a simple environment with an Anymal Quadruped and a block
##
# Pre-defined configs
##
from isaaclab_assets.robots.anymal import ANYMAL_C_CFG # isort: skip
if TYPE_CHECKING:
from isaaclab.scene import InteractiveScene
@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"],
track_friction_forces=args_cli.physics == "newton_mjwarp",
)
contact_forces_RF = ContactSensorCfg(
prim_path="{ENV_REGEX_NS}/Robot/RF_FOOT",
update_period=0.0,
history_length=6,
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.normal_force_matrix_w)
print("Received contact force of: ", scene["contact_forces_LF"].data.net_normal_forces_w)
print("-------------------------------")
print(scene["contact_forces_RF"])
print("Received force matrix of: ", scene["contact_forces_RF"].data.normal_force_matrix_w)
print("Received contact force of: ", scene["contact_forces_RF"].data.net_normal_forces_w)
print("-------------------------------")
print(scene["contact_forces_H"])
print("Received force matrix of: ", scene["contact_forces_H"].data.normal_force_matrix_w)
print("Received contact force of: ", scene["contact_forces_H"].data.net_normal_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
9from typing import TYPE_CHECKING, cast
10
11from isaaclab.app import add_launcher_args, launch_simulation
12
13# add argparse arguments
14parser = argparse.ArgumentParser(description="Example on using the contact sensor.")
15parser.add_argument("--num_envs", type=int, default=1, help="Number of environments to spawn.")
16parser.add_argument(
17 "--physics",
18 default="isaacsim_physx",
19 choices=["isaacsim_physx", "newton_mjwarp"],
20 help="Physics backend.",
21)
22# append launcher CLI args
23add_launcher_args(parser)
24# demos should open Kit visualizer by default
25parser.set_defaults(visualizer=["kit"])
26# parse the arguments
27args_cli = parser.parse_args()
28
29"""Rest everything follows."""
30
31import torch
32
33import isaaclab.sim as sim_utils
34from isaaclab.assets import AssetBaseCfg, RigidObjectCfg
35from isaaclab.physics import PhysicsCfg
36from isaaclab.scene import InteractiveSceneCfg
37from isaaclab.sensors import ContactSensorCfg
38from isaaclab.utils.configclass import configclass
39
40##
41# Pre-defined configs
42##
43from isaaclab_assets.robots.anymal import ANYMAL_C_CFG # isort: skip
44
45if TYPE_CHECKING:
46 from isaaclab.scene import InteractiveScene
47
48
49@configclass
50class ContactSensorSceneCfg(InteractiveSceneCfg):
51 """Design the scene with sensors on the robot."""
52
53 # ground plane
54 ground = AssetBaseCfg(prim_path="/World/defaultGroundPlane", spawn=sim_utils.GroundPlaneCfg())
55
56 # lights
57 dome_light = AssetBaseCfg(
58 prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75))
59 )
60
61 # robot
62 robot = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
63
64 # Rigid Object
65 cube = RigidObjectCfg(
66 prim_path="{ENV_REGEX_NS}/Cube",
67 spawn=sim_utils.CuboidCfg(
68 size=(0.5, 0.5, 0.1),
69 rigid_props=sim_utils.RigidBodyPropertiesCfg(),
70 mass_props=sim_utils.MassPropertiesCfg(mass=100.0),
71 collision_props=sim_utils.CollisionPropertiesCfg(),
72 physics_material=sim_utils.RigidBodyMaterialCfg(static_friction=1.0),
73 visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0), metallic=0.2),
74 ),
75 init_state=RigidObjectCfg.InitialStateCfg(pos=(0.5, 0.5, 0.05)),
76 )
77
78 contact_forces_LF = ContactSensorCfg(
79 prim_path="{ENV_REGEX_NS}/Robot/LF_FOOT",
80 update_period=0.0,
81 history_length=6,
82 debug_vis=True,
83 filter_prim_paths_expr=["{ENV_REGEX_NS}/Cube"],
84 track_friction_forces=args_cli.physics == "newton_mjwarp",
85 )
86
87 contact_forces_RF = ContactSensorCfg(
88 prim_path="{ENV_REGEX_NS}/Robot/RF_FOOT",
89 update_period=0.0,
90 history_length=6,
91 debug_vis=True,
92 filter_prim_paths_expr=["{ENV_REGEX_NS}/Cube"],
93 track_friction_forces=args_cli.physics == "newton_mjwarp",
94 )
95
96 contact_forces_H = ContactSensorCfg(
97 prim_path="{ENV_REGEX_NS}/Robot/.*H_FOOT",
98 update_period=0.0,
99 history_length=6,
100 debug_vis=True,
101 track_friction_forces=args_cli.physics == "newton_mjwarp",
102 )
103
104
105def run_simulator(sim: sim_utils.SimulationContext, scene: "InteractiveScene"):
106 """Run the simulator."""
107 # Define simulation stepping
108 sim_dt = sim.get_physics_dt()
109 sim_time = 0.0
110 count = 0
111
112 # Simulate physics
113 while sim.is_headless_or_exist_active_visualizer():
114 if count % 500 == 0:
115 # reset counter
116 count = 0
117 # reset the scene entities
118 # root state
119 # we offset the root state by the origin since the states are written in simulation world frame
120 # if this is not done, then the robots will be spawned at the (0, 0, 0) of the simulation world
121 root_pose = scene["robot"].data.default_root_pose.torch.clone()
122 root_pose[:, :3] += scene.env_origins
123 scene["robot"].write_root_pose_to_sim_index(root_pose=root_pose)
124 root_vel = scene["robot"].data.default_root_vel.torch.clone()
125 scene["robot"].write_root_velocity_to_sim_index(root_velocity=root_vel)
126 # set joint positions with some noise
127 joint_pos, joint_vel = (
128 scene["robot"].data.default_joint_pos.torch.clone(),
129 scene["robot"].data.default_joint_vel.torch.clone(),
130 )
131 joint_pos += torch.rand_like(joint_pos) * 0.1
132 scene["robot"].write_joint_position_to_sim_index(position=joint_pos)
133 scene["robot"].write_joint_velocity_to_sim_index(velocity=joint_vel)
134 # clear internal buffers
135 scene.reset()
136 print("[INFO]: Resetting robot state...")
137 # Apply default actions to the robot
138 # -- generate actions/commands
139 targets = scene["robot"].data.default_joint_pos.torch
140 # -- apply action to the robot
141 scene["robot"].set_joint_position_target_index(target=targets)
142 # -- write data to sim
143 scene.write_data_to_sim()
144 # perform step
145 sim.step()
146 # update sim-time
147 sim_time += sim_dt
148 count += 1
149 # update buffers
150 scene.update(sim_dt)
151
152 # print information from the sensors
153 print("-------------------------------")
154 print(scene["contact_forces_LF"])
155 print("Received force matrix of: ", scene["contact_forces_LF"].data.normal_force_matrix_w)
156 print("Received contact force of: ", scene["contact_forces_LF"].data.net_normal_forces_w)
157 print("-------------------------------")
158 print(scene["contact_forces_RF"])
159 print("Received force matrix of: ", scene["contact_forces_RF"].data.normal_force_matrix_w)
160 print("Received contact force of: ", scene["contact_forces_RF"].data.net_normal_forces_w)
161 print("-------------------------------")
162 print(scene["contact_forces_H"])
163 print("Received force matrix of: ", scene["contact_forces_H"].data.normal_force_matrix_w)
164 print("Received contact force of: ", scene["contact_forces_H"].data.net_normal_forces_w)
165 if args_cli.physics == "newton_mjwarp":
166 print("Received friction force of: ", scene["contact_forces_H"].data.net_friction_forces_w)
167
168
169def main():
170 """Main function."""
171
172 with launch_simulation(cfg=PhysicsCfg(), launcher_args=args_cli) as physics_cfg:
173 # Initialize the simulation context
174 sim_cfg = sim_utils.SimulationCfg(dt=0.005, device=args_cli.device, physics=physics_cfg)
175 sim = sim_utils.SimulationContext(sim_cfg)
176 # Set main camera
177 sim.set_camera_view(eye=(3.5, 3.5, 3.5), target=(0.0, 0.0, 0.0))
178 # design scene
179 scene_cfg = ContactSensorSceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0)
180 scene_class = cast(type["InteractiveScene"], scene_cfg.class_type)
181 scene = scene_class(scene_cfg)
182 # Play the simulator
183 sim.reset()
184 # Now we are ready!
185 print("[INFO]: Setup complete...")
186 # Run the simulator
187 run_simulator(sim, scene)
188
189
190if __name__ == "__main__":
191 # run the main function
192 main()