Skip to content

Contact sensors

63 · Solver and runtime

Three 4-box towers. newton.sensors.SensorContact reads each base box's contact force from the solver's contact_f observable, split by counterpart so column 0 is the ground. With sleeping on, settled islands skip their contact rows, so their forces read zero; sleep_skip_constraints=False keeps solving them.

0.00 s

Cost

Time per step, CUDA graph replay, median of 7 interleaved rounds on one NVIDIA RTX PRO 6000 Blackwell Server Edition; ratios are against the first row. Solver allocation is the GPU memory the solver allocates at construction, before warm-up and graph capture. Scene: the three 4-box towers in each world, timed after 4 s of settling.

Option1 world8 worldssolver allocation, 8 worldsresult in this tutorial
no sleeping1.48 ms1.75 ms3 MBsensor reads 39.2 N
enable_sleeping=True0.28 ms 0.19×0.36 ms 0.21×3 MBsensor reads 0 while asleep
sleeping, sleep_skip_constraints=False1.39 ms 0.94×1.94 ms 1.10×3 MBsensor reads 39.3 N

What to look at

  • Without sleeping the ground carries each 4-box tower: 39.2 N against 39.24 N of weight.
  • With sleeping the reading drops to 0 at 0.53 s, when the towers fall asleep.
  • sleep_skip_constraints=False keeps 39.3 N while asleep.

Key parameter

SensorContact(model, sensing_bodies=..., counterpart_shapes=...)

Request its solver_observable_kinds from solver.observables(...), pass them to step, then call sensor.update(state, contacts, observables=...).

Try changing

  • Add counterpart_bodies to split the force between the ground and the box above.
  • Read total_force_friction on a pushed tower.

Concept

Contact sensing and sleeping

What it is

SensorContact sums the solver's per-contact forces onto chosen bodies or shapes, optionally per counterpart. Sleeping islands normally skip their contact rows to save work, so contact_f, and the sensor, read zero under them; the contact is still there.

In this scene

Sleeping drops the base reading from 39.2 N to 0; keeping the rows restores it. In the timing below the sleeping towers step 5.3x faster, and keeping their rows gives that back.

Considerations

Cost
One world: 1.48 ms per step awake, 0.28 ms asleep, 1.39 ms asleep with rows kept; 8 worlds: 1.75, 0.36 and 1.94 ms.
Failure modes
Force thresholds (grasp detection, contact rewards) see a false release when an island sleeps.
Interactions
Any wake event restores the rows; contact_f also needs the contacts passed to observables().
Use it when
Keep sleep_skip_constraints=True when nothing reads forces on resting objects; set it False, or leave sleeping off, when sensors drive decisions.

The script

cd tutorials && python 63_contact_sensors.py prints each variant's final metric; --record out.json also writes the replay. Every tutorial imports replay.py from the same folder.

tutorials/63_contact_sensors.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Contact sensors on Steel contacts, and what sleeping does to their readings."""
import newton
import warp as wp
from newton.sensors import SensorContact
from newton.solvers import SolverSteel
from replay import Recorder
DT = 1.0 / 240.0
SUBSTEPS = 8
def build():
builder = newton.ModelBuilder()
cfg = newton.ModelBuilder.ShapeConfig(mu=0.7)
builder.add_ground_plane(cfg=cfg)
for k in range(3):
for i in range(4):
label = f"base{k}" if i == 0 else f"box{k}_{i}"
box = builder.add_body(
xform=wp.transform((0.3 * k - 0.3, 0.0, 0.05 + 0.1005 * i), wp.quat_identity()),
label=label,
)
builder.add_shape_box(box, hx=0.05, hy=0.05, hz=0.05, cfg=cfg)
return builder.finalize()
def simulate(rec, label, **options):
model = build()
solver = SolverSteel(model, **options)
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
# one sensor per tower base, split by counterpart: column 0 is the ground plane (shape 0)
sensor = SensorContact(
model, sensing_bodies="base*", counterpart_shapes=[0], request_contact_attributes=False
)
kinds = set(sensor.solver_observable_kinds) | (
{"body_awake"} if options.get("enable_sleeping") else set()
)
observed = solver.observables(kinds, contacts=contacts)
state_0, state_1, control = model.state(), model.state(), model.control()
weight = float(model.body_mass.numpy()[0]) * 9.81
run = rec.variant(label, model)
for _ in range(rec.frames(4.0)):
# ground force on a base = the weight of its 4-box tower
reading = sensor.force_matrix.numpy()[:, 0, 2].mean()
awake = observed.body_awake if options.get("enable_sleeping") else None
run.frame(state_0, awake=awake, base_force_n=reading, tower_weight_n=4 * weight)
for _ in range(SUBSTEPS):
pipeline.collide(state_0, contacts)
solver.step(state_0, state_1, control, contacts, DT, observables=observed)
state_0, state_1 = state_1, state_0
sensor.update(state_0, contacts, observables=observed)
run.check(solver)
if __name__ == "__main__":
rec = Recorder()
simulate(rec, "no sleeping")
# sleeping islands skip their contact rows, so their contact forces read zero
simulate(rec, "enable_sleeping=True", enable_sleeping=True)
# keep the rows of sleeping islands: readings stay, the speed-up shrinks
simulate(
rec,
"sleeping, sleep_skip_constraints=False",
enable_sleeping=True,
sleep_skip_constraints=False,
)
rec.save()

Recorded with SolverSteel on Newton 1.9.0.dev0, Warp 1.19.0, NVIDIA RTX PRO 6000 Blackwell Server Edition. Replays read the state back every frame, so they are not a speed benchmark. Script · page text