Contact Sensor

Contact Sensor#

A contact sensor with filtering

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

\[\boldsymbol{f}_{total} = \boldsymbol{f}_{normal} + \boldsymbol{f}_{friction}.\]

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')
The contact sensor visualization

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()