Skip to content

Speculative contacts

17 · Impacts and penetration

A 2 cm ball moving at 40 m/s (17 cm per 240 Hz step) is fired at a 1 cm wall with no gravity. The shape gap decides how far apart the pipeline reports contacts. With a 5 mm gap it never sees the ball near the wall and the ball tunnels through. With a 20 cm gap a speculative row lets the ball close the remaining distance in one step and then blocks it.

0.00 s

What to look at

  • 5 mm gap: the ball passes through the wall.
  • 20 cm gap: it stops at the wall face.
  • contact_speculative_scale=0 stops it at the gap instead, and contact_gap_gate=0.01 drops the rows, so it tunnels.

Key parameter

ShapeConfig.gap

Contact detection distance. contact_speculative_scale (default 1) is the fraction of a positive gap a contact may close per step; contact_gap_gate drops contacts farther than a distance.

Try changing

  • Set the speed to 80 m/s with the 20 cm gap.
  • Play at 0.1× to see the single step that closes the gap.

Concept

Speculative contacts

What it is

A contact reported while its shapes are still apart may close the remaining gap φ in one step but no more. Fast objects are stopped at the surface without smaller steps, provided the shape gap covers one step of travel.

target = s · φ / h for φ > 0

In this scene

A 40 m/s ball covers 17 cm per step: a 5 mm gap lets it tunnel, a 20 cm gap stops it at the wall. contact_speculative_scale=0 stops it at the gap, and contact_gap_gate=0.01 drops the rows so it tunnels again.

Considerations

Cost
A larger gap reports more contacts and rows; contact_gap_gate trims the far ones.
Failure modes
A small gap tunnels; a scale below 1 leaves objects hovering.
Use it when
Thin walls and fast projectiles. Size the gap to the fastest speed times the step.

The script

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

tutorials/17_speculative_contacts.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Speculative contacts: a contact gap that covers one step of travel stops a fast ball.
The shape gap decides which contacts the pipeline reports;
contact_gap_gate decides which of them Steel solves.
"""
import newton
import numpy as np
import warp as wp
from newton.solvers import SolverSteel
from replay import Recorder
DT = 1.0 / 240.0
SUBSTEPS = 1 # record every step: the ball covers 17 cm per step
def build(gap):
builder = newton.ModelBuilder(gravity=(0.0, 0.0, 0.0))
cfg = newton.ModelBuilder.ShapeConfig(gap=gap)
builder.add_shape_box(
-1,
xform=wp.transform((0.6, 0.0, 0.3), wp.quat_identity()),
hx=0.005,
hy=0.25,
hz=0.25,
cfg=cfg,
)
ball = builder.add_body(xform=wp.transform((-0.8, 0.0, 0.3), wp.quat_identity()))
builder.add_shape_sphere(
ball, radius=0.02, cfg=newton.ModelBuilder.ShapeConfig(density=2000.0, gap=gap)
)
model = builder.finalize()
model.joint_qd.assign(np.array([40.0, 0, 0, 0, 0, 0], dtype=np.float32))
return model
def simulate(rec, label, gap, **options):
model = build(gap)
solver = SolverSteel(model, **options)
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
state_0, state_1, control = model.state(), model.state(), model.control()
newton.eval_fk(model, model.joint_q, model.joint_qd, state_0)
run = rec.variant(label, model)
for _ in range(rec.frames(0.25)):
run.frame(state_0, x=state_0.body_q.numpy()[0, 0])
for _ in range(SUBSTEPS):
pipeline.collide(state_0, contacts)
solver.step(state_0, state_1, control, contacts, DT)
state_0, state_1 = state_1, state_0
run.check(solver)
if __name__ == "__main__":
rec = Recorder(fps=240)
simulate(rec, "gap 5 mm: tunnels", 0.005)
simulate(rec, "gap 20 cm (scale 1, default)", 0.2)
simulate(rec, "gap 20 cm, contact_speculative_scale=0", 0.2, contact_speculative_scale=0.0)
simulate(rec, "gap 20 cm, contact_gap_gate=0.01", 0.2, contact_gap_gate=0.01)
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