Skip to content

Randomizing restitution

69 · Domain randomization

Five balls released about 0.94 m above the ground, each with a sampled shape_material_restitution written before the first step and followed by notify_model_changed(SHAPE_PROPERTIES). The shared ground plane has restitution 0, so the solver uses restitution_combine="max" to let each ball's coefficient decide. Higher coefficients bounce higher and for longer. Without the notification Steel keeps the coefficients from construction (0), and no ball bounces.

0.00 s

What to look at

  • With notify: bounce height grows with the sampled coefficient.
  • Without notify: every ball stops dead.

Key parameter

model.shape_material_restitution

Applied per contact with restitution_combine. Restitution is on by default (enable_restitution=True).

Try changing

  • Use the default "average" combine and give the ground a coefficient too.
  • Randomize the ground instead by giving each world its own floor shape (see Randomizing terrain friction).

Concept

When Steel sees a randomized value

What it is

Steel keeps a solver-side copy of this input, such as its material table, damping buffer, cached body-to-COM transforms or effective armature. Writing the model array changes nothing until notify_model_changed with the matching flag refreshes that copy.

In this scene

Without notify: every ball stops dead.

Considerations

Cost
Call only, default solver, six-link chain and four boxes per world, 1024 worlds (1.12 ms per collide + step): SHAPE_PROPERTIES 84 µs (15 µs captured). Flags measured one at a time; combined calls and feature validation can cost more. This notification does not request a mass-matrix refresh.
Graph capture
These flags can be captured in a reset graph. Kinematic-flag changes, sleeping, and shape flags with sparse factors plus restitution run eagerly; captured calls skip some value checks.
Interactions
world_mask limits which worlds lose their warm start; cached data is refreshed for every world.
Use it when
Throwing, dropping and bouncing tasks.

The script

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

tutorials/dr_restitution.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Restitution randomization: per-world shape_material_restitution + SHAPE_PROPERTIES."""
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
WORLDS = 5
RADIUS = 0.06
def build():
drop = newton.ModelBuilder()
ball = drop.add_body(xform=wp.transform((0.0, 0.0, 1.0), wp.quat_identity()))
drop.add_shape_sphere(ball, radius=RADIUS, cfg=newton.ModelBuilder.ShapeConfig(restitution=0.0))
builder = newton.ModelBuilder()
builder.replicate(drop, WORLDS, spacing=(0.0, 0.25, 0.0))
builder.add_ground_plane(cfg=newton.ModelBuilder.ShapeConfig(restitution=0.0))
return builder.finalize()
def simulate(rec, label, e, notify):
model = build()
# "max" lets the ball's coefficient decide against the shared ground's 0
solver = SolverSteel(model, restitution_combine="max")
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
state_0, state_1, control = model.state(), model.state(), model.control()
world = model.shape_world.numpy()
values = model.shape_material_restitution.numpy()
values[world >= 0] = e[world[world >= 0]]
model.shape_material_restitution.assign(values)
if notify:
solver.notify_model_changed(newton.ModelFlags.SHAPE_PROPERTIES)
run = rec.variant(label, model)
for _ in range(rec.frames(4.0)):
height = state_0.body_q.numpy()[:, 2] - RADIUS
run.frame(state_0, **{f"e {v:.2f}": h for v, h in zip(e, height)})
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__":
rng = np.random.default_rng(4)
e = np.sort(rng.uniform(0.1, 0.9, WORLDS))
rec = Recorder()
simulate(rec, "restitution sampled per world + notify", e, True)
simulate(rec, "restitution sampled per world, no notify", e, 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