Skip to content

Sleeping islands

50 · Solver and runtime

Four towers settle and fall asleep (shown in blue) once their bodies stay below 0.05 m/s and 0.15 rad/s for 0.5 s. Islands are rebuilt every step from contacts, and sleeping islands skip their rows and their dynamics. A ball pushed at 7 s wakes only the tower it hits. A second scene runs 64 worlds of the same towers, with the default collision pipeline and with CollisionPipeline(deterministic=True); the replay shows world 0 and the chart counts every world.

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: four 5-box towers per world, timed after 4 s of settling.

Option1 world64 worlds1024 worldssolver allocation, 1024 worldsresult in this tutorial
enable_sleeping=False1.98 ms2.35 ms3.34 ms702 MB
enable_sleeping=True0.77 ms 0.39×2.45 ms 1.04×3.43 ms 1.03×709 MB1 world: all asleep at 2.1 s; 64 and 1024 worlds: most stay awake with this default pipeline

What to look at

  • One world: the towers settle, and by about 2.1 s every body is asleep.
  • At 7 s the ball wakes and topples one tower; the other three stay asleep.
  • 64 worlds, default pipeline: only 7% of the bodies are asleep after 6 s. The pipeline can write the same contacts in a different order on each call, and sleeping rules out warm start, so resting boxes keep a jitter whose fast tail reaches the 0.15 rad/s threshold.
  • 64 worlds, CollisionPipeline(deterministic=True): with sorted contacts the jitter was lower in these runs, and every body is asleep by 1.0 s.

Key parameter

enable_sleeping

Experimental. sleep_linear_threshold, sleep_angular_threshold and sleep_quiet_time set when an island sleeps; the body_awake observable reports it.

Try changing

  • Set sleep_quiet_time=2.0.
  • Pass sleep_skip_constraints=False to keep the rows of sleeping islands.
  • Keep the default pipeline in the 64-world scene and set sleep_angular_threshold=0.3.

Concept

Sleeping islands

What it is

Bodies in contact are joined into islands every step. A supported island whose bodies stay below 0.05 m/s and 0.15 rad/s for 0.5 s falls asleep: its state is frozen and its rows and dynamics are skipped until a wake event such as a force, a contact with a moving body or lost support.

In this scene

All 21 bodies are asleep at 2.1 s; the ball pushed at 7 s wakes one tower plus itself. In 64 worlds, 7% of the bodies sleep after 6 s with the default pipeline and all of them by 1.0 s with the deterministic one.

Considerations

Cost
In one world sleeping cut the step from 2.0 to 0.77 ms in this table; the saving depends on how many islands are asleep in the timed window, which varies between runs. The table's 64 and 1024 worlds use the default pipeline and show no gain, because most bodies stay awake. In a separate measurement on the same GPU with CollisionPipeline(deterministic=True), every island slept and a 1024-world solver step took 1.09 ms instead of 3.49 ms.
Failure modes
With the default pipeline, whether islands sleep varies between runs and does not change monotonically with the world count: in separate runs 8 worlds mostly slept, while 16 to 128 worlds left most bodies awake. Nothing in this scene couples the worlds. Check body_awake in batched setups.
Interactions
Not with warm start, velocity iterations or adhesion; driven and sprung articulations stay awake; contact_f reads zero under sleeping bodies.
Use it when
Large mostly-resting scenes, with CollisionPipeline(deterministic=True) or a larger sleep_angular_threshold in batched setups; measure on your own setup.

The script

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

tutorials/50_sleeping.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Sleeping islands: settled towers fall asleep and skip their work until something wakes them."""
from types import SimpleNamespace
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 = 8
KICK_AT = 7.0 # s
WORLDS = 64
CFG = newton.ModelBuilder.ShapeConfig(mu=0.7)
def add_towers(builder):
for k in range(4):
for i in range(5):
box = builder.add_body(
xform=wp.transform((0.5 * k - 0.75, 0.0, 0.05 + 0.1005 * i), wp.quat_identity())
)
builder.add_shape_box(box, hx=0.05, hy=0.05, hz=0.05, cfg=CFG)
def build():
builder = newton.ModelBuilder()
builder.add_ground_plane(cfg=CFG)
add_towers(builder)
ball = builder.add_body(xform=wp.transform((-0.25, -1.6, 0.08), wp.quat_identity()))
builder.add_shape_sphere(
ball, radius=0.08, cfg=newton.ModelBuilder.ShapeConfig(density=3000.0, mu=0.3)
)
return builder.finalize(), ball
def simulate(rec, label):
model, ball = build()
solver = SolverSteel(model, enable_sleeping=True) # thresholds: 0.05 m/s, 0.15 rad/s for 0.5 s
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
observed = solver.observables({"body_awake"})
state_0, state_1, control = model.state(), model.state(), model.control()
kick = np.zeros((model.body_count, 6), dtype=np.float32)
# one step of this force gives the ball 4 m/s toward the second tower
kick[ball, 1] = model.body_mass.numpy()[ball] * 4.0 / DT
run = rec.variant(label, model, scene="one")
for frame in range(rec.frames(10.0)):
awake = observed.body_awake
run.frame(state_0, awake=awake, awake_bodies=awake.numpy().sum())
for step in range(SUBSTEPS):
state_0.clear_forces()
if frame == int(KICK_AT * rec.fps) and step == 0:
state_0.body_f.assign(kick) # a nonzero force also wakes the ball's island
pipeline.collide(state_0, contacts)
solver.step(state_0, state_1, control, contacts, DT, observables=observed)
state_0, state_1 = state_1, state_0
run.check(solver)
def simulate_worlds(rec, deterministic):
"""WORLDS copies of the towers; the replay shows world 0, the chart counts every world."""
towers = newton.ModelBuilder()
add_towers(towers)
builder = newton.ModelBuilder()
builder.add_ground_plane(cfg=CFG)
builder.replicate(towers, WORLDS)
model = builder.finalize()
shown = newton.ModelBuilder()
shown.add_ground_plane(cfg=CFG)
add_towers(shown)
shown = shown.finalize()
solver = SolverSteel(model, enable_sleeping=True)
# the default pipeline can order contacts differently on each collide; this one sorts them
pipeline = newton.CollisionPipeline(model, deterministic=deterministic)
contacts = pipeline.contacts()
observed = solver.observables({"body_awake"})
state_0, state_1, control = model.state(), model.state(), model.control()
pipe = "CollisionPipeline(deterministic=True)" if deterministic else "default pipeline"
run = rec.variant(f"{WORLDS} worlds, {pipe}", shown, scene="worlds")
for _ in range(rec.frames(6.0)):
awake = observed.body_awake
asleep_pct = 100.0 * (1.0 - awake.numpy().mean())
first = SimpleNamespace(body_q=state_0.body_q[: shown.body_count])
run.frame(first, awake=awake[: shown.body_count], asleep_pct=asleep_pct)
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
run.check(solver)
if __name__ == "__main__":
rec = Recorder()
simulate(rec, "enable_sleeping=True")
for deterministic in [False, True]:
simulate_worlds(rec, deterministic)
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