Sleeping
Basics
Four stacks of ants topple into each other; sleeping islands turn grey.
steel preset (the default), recordedon an NVIDIA RTX PRO 6000 Blackwell.Run it
cd examples# the default preset (steel), then the othersuv run shelf play sleepinguv run shelf play sleeping --solver mujoco# change solver settings liveuv run shelf tune sleepingSource: examples/shelf/00_basics/sleeping/ · ported from newton/examples/mujoco/example_mujoco_sleeping.py @ skild-ai/newton 7565a26b1
Presets
| Preset | Solver | Status | Checked |
|---|---|---|---|
steeldefault | steel | works | 2026-11-05, newton 7565a26b1, NVIDIA GeForce RTX 3090 |
mujoco | mujoco | works | 2026-10-28, newton 7565a26b1, NVIDIA RTX A6000 |
Solvers with no row are untested on this example. The full settings are inpresets.toml.
Code
# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers# SPDX-License-Identifier: Apache-2.0# Modified for Steel Shelf: preset-driven execution and shared runner integration.
############################################################################ Example Sleeping## Drops four nearby stacks of ants and visualizes the solver's sleeping# islands (MuJoCo Warp's or Steel's). The stacks topple toward one another# in a shared world, allowing their islands to merge. Awake articulations use# Newton's display palette, with a shared color for every body in the same# island. Sleeping islands are gray.## Command: python -m shelf run sleeping# python -m shelf run sleeping --scene boxes # steel-webgpu's sleeping scene: 36 boxes in short stacks############################################################################
from __future__ import annotations
import math
import numpy as npimport warp as wp
import newtonimport newton.examplesfrom common import presetsfrom newton.solvers import SolverMuJoCo
STACK_COUNT = 4SCENES = ("ants", "boxes")ARTICULATIONS_PER_STACK = 5STACK_SPACING = 2.5ANT_BASE_HEIGHT = 1.0ANT_VERTICAL_SPACING = 1.25ANT_HORIZONTAL_OFFSET = 0.08ANT_TILT = math.radians(4.0)INACTIVE_COLOR = (0.32, 0.32, 0.32)
# Newton's standard shape palette (Paul Tol's Bright scheme).ISLAND_PALETTE = ( (68.0 / 255.0, 119.0 / 255.0, 170.0 / 255.0), (102.0 / 255.0, 204.0 / 255.0, 238.0 / 255.0), (34.0 / 255.0, 136.0 / 255.0, 51.0 / 255.0), (204.0 / 255.0, 187.0 / 255.0, 68.0 / 255.0), (238.0 / 255.0, 102.0 / 255.0, 119.0 / 255.0), (170.0 / 255.0, 51.0 / 255.0, 119.0 / 255.0), (238.0 / 255.0, 153.0 / 255.0, 51.0 / 255.0), (0.0, 153.0 / 255.0, 136.0 / 255.0),)
# Each solver's sleep state is first reduced to two per-body arrays, which the coloring reads:# body_asleep: 1 if the body's island sleeps# body_island: a key shared by the awake bodies of one island, -1 for a sleeping or unmapped body
@wp.kernel(enable_backward=False)def mujoco_body_sleep( body_tree: wp.array[wp.int32], body_world: wp.array[wp.int32], tree_asleep: wp.array2d[wp.int32], tree_island: wp.array2d[wp.int32], body_asleep: wp.array[wp.int32], body_island: wp.array[wp.int32],): """MuJoCo Warp tracks sleep and islands per kinematic tree.""" body_id = wp.tid() tree_id = body_tree[body_id] world_id = body_world[body_id] body_asleep[body_id] = 0 body_island[body_id] = -1 if tree_id < 0 or world_id < 0: return if tree_asleep[world_id, tree_id] >= 0: body_asleep[body_id] = 1 return tree_capacity = tree_asleep.shape[1] active_island = tree_island[world_id, tree_id] if active_island < 0: # An unconstrained awake tree is its own one-tree island. active_island = tree_capacity + tree_id body_island[body_id] = world_id * (2 * tree_capacity) + active_island
@wp.kernel(enable_backward=False)def steel_body_sleep( body_awake: wp.array[int], island_root: wp.array[int], body_asleep: wp.array[wp.int32], body_island: wp.array[wp.int32],): """Steel tracks sleep per body and names each island by its root articulation.""" body_id = wp.tid() asleep = body_awake[body_id] == 0 body_asleep[body_id] = wp.where(asleep, 1, 0) body_island[body_id] = wp.where(asleep, -1, island_root[body_id])
@wp.kernel(enable_backward=False)def update_shape_colors( shape_body: wp.array[wp.int32], body_asleep: wp.array[wp.int32], body_island: wp.array[wp.int32], palette: wp.array[wp.vec3], inactive_color: wp.vec3, default_color: wp.array[wp.vec3], shape_color: wp.array[wp.vec3],): """Color shapes by their body's live island and sleep state.""" shape_id = wp.tid() body_id = shape_body[shape_id]
if body_id < 0: shape_color[shape_id] = default_color[shape_id] elif body_asleep[body_id] != 0: shape_color[shape_id] = inactive_color elif body_island[body_id] < 0: shape_color[shape_id] = default_color[shape_id] else: shape_color[shape_id] = palette[body_island[body_id] % palette.shape[0]]
def build_articulation(asset_name: str, mujoco: bool) -> newton.ModelBuilder: """Build an actuated articulation whose passive tree is allowed to sleep.""" articulation = newton.ModelBuilder() if mujoco: SolverMuJoCo.register_custom_attributes(articulation) articulation.default_shape_cfg.gap = 0.0
root_body = len(articulation.body_mass) articulation.add_mjcf( newton.examples.get_asset(asset_name), ignore_names=["floor", "ground"], collapse_fixed_joints=False, parse_sites=False, )
if mujoco: # MuJoCo prevents actuated trees from sleeping by default. This passive # example sends zero controls, so explicitly opt the root tree in. articulation.custom_attributes["mujoco:sleep_policy"].values[root_body] = SolverMuJoCo.SleepPolicy.ALLOWED return articulation
def build_stack(mujoco: bool) -> newton.ModelBuilder: """Build one clean vertical stack of five ants.""" ant = build_articulation("nv_ant.xml", mujoco)
stack = newton.ModelBuilder() if mujoco: SolverMuJoCo.register_custom_attributes(stack) for ant_id in range(ARTICULATIONS_PER_STACK): height = ANT_BASE_HEIGHT + ANT_VERTICAL_SPACING * ant_id topple_level = max(0, ant_id - (ARTICULATIONS_PER_STACK - 3)) offset = ANT_HORIZONTAL_OFFSET * topple_level tilt = ANT_TILT if topple_level > 0 else 0.0 stack.add_builder( ant, xform=wp.transform( (offset, 0.0, height), wp.quat_from_axis_angle(wp.vec3(0.0, 1.0, 0.0), tilt), ), label_prefix=f"ant_{ant_id}_", ) return stack
def build_boxes(builder: newton.ModelBuilder) -> None: """steel-webgpu's sleeping scene (tools/scenes.py): 36 boxes in twelve short stacks, slightly jittered.""" rng = np.random.default_rng(3) builder.begin_world(label="boxes") for gx in range(4): for gy in range(3): for level in range(3): p = ((gx - 1.5) * 0.18 + rng.uniform(-0.01, 0.01), (gy - 1) * 0.18, 0.04 + 0.08 * level + 0.02 * level) body = builder.add_body(xform=wp.transform(wp.vec3(*p), wp.quat_rpy(0.0, 0.0, rng.uniform(-0.4, 0.4)))) cfg = newton.ModelBuilder.ShapeConfig() cfg.density = 400.0 builder.add_shape_box(body, hx=0.035, hy=0.035, hz=0.035, cfg=cfg) builder.end_world()
class Example: def __init__(self, viewer, args): """Build the articulation stacks (or the box scene), sleeping solver, and island-color mapping.""" self.preset = presets.load(__file__) self.viewer = viewer self.fps = self.preset.fps self.frame_dt = 1.0 / self.fps self.sim_substeps = self.preset.substeps self.sim_dt = self.frame_dt / self.sim_substeps self.sim_time = 0.0
if not wp.get_device().is_cuda: raise RuntimeError("The sleeping example requires a CUDA device.") if self.preset.name not in ("mujoco", "steel"): raise ValueError( f"The sleeping example needs a solver with sleeping (mujoco, steel), got {self.preset.name}." ) mujoco = self.preset.name == "mujoco"
self.scene = getattr(args, "scene", "ants") builder = newton.ModelBuilder() if mujoco: SolverMuJoCo.register_custom_attributes(builder) builder.add_ground_plane() if self.scene == "boxes": build_boxes(builder) stack_count = 0 else: stack_count = int(args.stack_count) if stack_count <= 0: raise ValueError(f"stack_count must be positive, got {stack_count}.") stack = build_stack(mujoco) if stack_count else None
if stack_count: # Keep every stack in one world so contacts can merge their islands. side_length = math.ceil(math.sqrt(stack_count)) row_count = math.ceil(stack_count / side_length) builder.begin_world(label="ant_stacks") for stack_id in range(stack_count): column = stack_id % side_length row = stack_id // side_length stack_x = (column - 0.5 * (side_length - 1)) * STACK_SPACING stack_y = (row - 0.5 * (row_count - 1)) * STACK_SPACING yaw = math.atan2(-stack_y, -stack_x) if stack_x != 0.0 or stack_y != 0.0 else 0.0 builder.add_builder( stack, xform=wp.transform( (stack_x, stack_y, 0.0), wp.quat_from_axis_angle(wp.vec3(0.0, 0.0, 1.0), yaw), ), label_prefix=f"stack_{stack_id}_", ) builder.end_world() self.model = builder.finalize()
self.state_0 = self.model.state() self.state_1 = self.model.state() self.control = self.model.control()
self.default_shape_color = wp.clone(self.model.shape_color) self.island_palette = wp.array(ISLAND_PALETTE, dtype=wp.vec3, device=self.model.device) self.inactive_color = wp.vec3(*INACTIVE_COLOR) self.body_asleep = wp.zeros(self.model.body_count, dtype=wp.int32, device=self.model.device) self.body_island = wp.full(self.model.body_count, -1, dtype=wp.int32, device=self.model.device) self.build_solver()
self.viewer.set_model(self.model) if self.scene == "boxes": self.viewer.set_camera(pos=wp.vec3(0.9, -1.2, 0.75), pitch=-30.0, yaw=127.0) else: self.viewer.set_camera(pos=wp.vec3(8.0, -10.0, 6.5), pitch=-27.0, yaw=128.0)
self.capture()
def build_solver(self): """Build the solver and everything bound to its instance; the tuner calls this again to swap it.""" self.solver = self.preset.make_solver(self.model) if self.preset.name == "mujoco": # MuJoCo finds its own contacts self.collision_pipeline = self.contacts = None self.body_tree = self._build_body_tree_mapping() else: # Steel takes its contacts from Newton's collision pipeline self.collision_pipeline = newton.CollisionPipeline(self.model) self.contacts = self.collision_pipeline.contacts()
def _build_body_tree_mapping(self) -> wp.array[wp.int32]: """Map Newton body indices to MuJoCo tree indices for visualization.""" body_tree = np.full(self.model.body_count, -1, dtype=np.int32) mujoco_to_newton = self.solver.mjc_body_to_newton.numpy() mujoco_body_tree = self.solver.mjw_model.body_treeid.numpy() for world_body_map in mujoco_to_newton: for mujoco_body, newton_body in enumerate(world_body_map): if newton_body >= 0: body_tree[newton_body] = mujoco_body_tree[mujoco_body]
self.body_tree_np = body_tree return wp.array(body_tree, dtype=wp.int32, device=self.model.device)
def capture(self): """Capture the sleeping simulation step in a CUDA graph.""" with wp.ScopedCapture() as capture: self.simulate() self.graph = capture.graph
def simulate(self): """Advance the passive articulation stacks by one rendered frame.""" for substep in range(self.sim_substeps): self.state_0.clear_forces() self.viewer.apply_forces(self.state_0) if self.collision_pipeline is not None: self.collision_pipeline.collide(self.state_0, self.contacts) self.solver.step(self.state_0, self.state_1, self.control, self.contacts, self.sim_dt) if self.sim_substeps % 2 == 1 and substep == self.sim_substeps - 1: # The captured graph always starts from state_0: with an odd substep count, copy the last # result into it rather than swapping, so each replay continues from the newest state # (Steel's sleeping treats any other input as an external change and wakes the island) self.state_0.assign(self.state_1) else: self.state_0, self.state_1 = self.state_1, self.state_0
def step(self): """Advance the stacks by one rendered frame.""" wp.capture_launch(self.graph) self.sim_time += self.frame_dt
def _update_body_sleep(self): """Reduce the solver's live sleep state to per-body asleep flags and island keys.""" if self.preset.name == "mujoco": inputs = [ self.body_tree, self.model.body_world, self.solver.mjw_data.tree_asleep, self.solver.mjw_data.tree_island, ] kernel = mujoco_body_sleep else: # Steel's sleeping is experimental; its state lives on the solver's `_sleeping`, as Newton's tests read it sleeping = self.solver._sleeping inputs, kernel = [sleeping.body_awake, sleeping.body_island], steel_body_sleep wp.launch( kernel, dim=self.model.body_count, inputs=inputs, outputs=[self.body_asleep, self.body_island], device=self.model.device, )
def _update_shape_colors(self): """Update model display colors from the solver's live island state.""" self._update_body_sleep() wp.launch( update_shape_colors, dim=self.model.shape_count, inputs=[ self.model.shape_body, self.body_asleep, self.body_island, self.island_palette, self.inactive_color, self.default_shape_color, ], outputs=[self.model.shape_color], device=self.model.device, )
def render(self): """Render active islands in color and sleeping islands in gray.""" self._update_shape_colors() self.viewer.begin_frame(self.sim_time) self.viewer.log_state(self.state_0) self.viewer.end_frame()
def test_final(self): """Verify every dynamic shape color matches its tree's island state.""" self._update_shape_colors() if self.preset.name == "steel": self._test_final_steel() return
shape_body = self.model.shape_body.numpy() body_world = self.model.body_world.numpy() shape_color = self.model.shape_color.numpy() tree_asleep = self.solver.mjw_data.tree_asleep.numpy() tree_island = self.solver.mjw_data.tree_island.numpy() tree_capacity = tree_asleep.shape[1] palette = np.asarray(ISLAND_PALETTE, dtype=np.float32)
for shape_id, body_id in enumerate(shape_body): if body_id < 0: continue tree_id = self.body_tree_np[body_id] world_id = body_world[body_id] if tree_id < 0 or world_id < 0: raise AssertionError(f"Dynamic shape {shape_id} has no MuJoCo tree/world mapping.")
if tree_asleep[world_id, tree_id] >= 0: expected = np.asarray(INACTIVE_COLOR, dtype=np.float32) else: active_island = tree_island[world_id, tree_id] if active_island < 0: active_island = tree_capacity + tree_id island_key = world_id * (2 * tree_capacity) + active_island expected = palette[island_key % len(palette)] np.testing.assert_allclose(shape_color[shape_id], expected, rtol=0.0, atol=1.0e-6)
def _test_final_steel(self): """The same check against Steel's per-body sleep state and island roots.""" shape_body = self.model.shape_body.numpy() shape_color = self.model.shape_color.numpy() body_awake = self.solver._sleeping.body_awake.numpy() island_root = self.solver._sleeping.body_island.numpy() palette = np.asarray(ISLAND_PALETTE, dtype=np.float32)
for shape_id, body_id in enumerate(shape_body): if body_id < 0: continue if body_awake[body_id] == 0: expected = np.asarray(INACTIVE_COLOR, dtype=np.float32) elif island_root[body_id] < 0: raise AssertionError(f"Dynamic shape {shape_id} has no Steel island.") else: expected = palette[island_root[body_id] % len(palette)] np.testing.assert_allclose(shape_color[shape_id], expected, rtol=0.0, atol=1.0e-6)
@staticmethod def create_parser(): """Create command-line arguments for stack count.""" parser = newton.examples.create_parser() parser.add_argument("--stack-count", type=int, default=STACK_COUNT, help="Number of articulation stacks.") parser.add_argument("--scene", choices=SCENES, default="ants", help="ants: stacks of ants; boxes: steel-webgpu's 36 boxes.") return parser
if __name__ == "__main__": parser = Example.create_parser() viewer, args = newton.examples.init(parser) newton.examples.run(Example(viewer, args), args)example = "sleeping"source = "newton/examples/mujoco/example_mujoco_sleeping.py @ skild-ai/newton 7565a26b1"default = "steel"
[check]# Arguments `python -m shelf check` passes in test mode (from Newton's test_examples.py).args = ["--stack-count", "2", "--num-frames", "300"]
[gallery]short = "Four stacks of ants topple into each other; sleeping islands turn grey."frame = 240
[xpbd]status = "untested"
[vbd]status = "untested"
[mujoco]status = "works"sim = { fps = 60, substeps = 5 }solver = { enable_sleeping = true, sleep_tolerance = 0.05, iterations = 10, ls_iterations = 10, jacobian = "sparse", njmax = 4096, nconmax = 2048, update_data_interval = 0 }checked = "2026-10-28, newton 7565a26b1, NVIDIA RTX A6000"
[featherstone]status = "untested"
[semi_implicit]status = "untested"
[feather_pgs]status = "untested"
[steel]status = "works"# 5 substeps x 10 iterations, propagation_colored, joint limits on: among the swept settings, the only one whose islands# reliably fall asleep when MuJoCo's do, with lower penetration and joint-limit overshoot than MuJoCo (60/0.86 mm,# 0.10 rad vs 171/2.0 mm, 0.39 rad). With 4 stacks every body is asleep by frame 330 in every run (MuJoCo 300-360);# with the check's 2 stacks by frame 150. The colored sweep gives the same result run to run. The immediate response# is faster (5x4: 4.0 ms vs 6.2 ms) but its islands settle anywhere from frame 330 to past 630 and vary between runs;# 4 colored iterations settle at frame 390 and, with 2 stacks, not within 600 frames; 6 at 450. MuJoCo enforces the# ants' joint limits; without them the legs fold 2.9 rad past them. joint_limit_activation_gap allocates a limit row# only near its limit (35% faster, same result). The 20 piled ants with limit rows overflow 256 articulation rows per# world (rows are dropped and the world never sleeps), so 1024. The 36 boxes of `--scene boxes` need 1542 propagation# rows (contact + friction) at once, past the default propagation_constraint_max of 1024 + 512 = 1536: the dropped rows# kept every box jittering at up to 0.6 m/s and none ever slept (7.1 ms/frame). 2048 lets them all sleep by frame 41# (2.0 ms, penetration peak 0.62 -> 0.18 mm); the ants are unchanged (asleep at frame 314, 4.48 -> 4.44 ms, same# penetration).# enable_sleeping needs a rigid immediate or propagation_colored response and no warm start, compliance or velocity# passes; the thresholds are Steel's defaults.sim = { fps = 60, substeps = 5 }solver = { solve_mode = "matrix_free", iterations = 10, articulated_contact_response = "propagation_colored", enable_joint_limits = true, joint_limit_activation_gap = 0.1, articulation_constraint_max = 1024, propagation_constraint_max = 2048, enable_sleeping = true, sleep_linear_threshold = 0.05, sleep_angular_threshold = 0.15, sleep_quiet_time = 0.5 }checked = "2026-11-05, newton 7565a26b1, NVIDIA GeForce RTX 3090"
[kamino]status = "untested"
[implicit_mpm]status = "untested"
[style3d]status = "untested"Steel docs and tutorials are licensed under Apache-2.0. Third-party notices. "Skild", "Skild AI" and the Skild logo are trademarks of Skild AI and are not licensed under Apache-2.0.