Skip to content

Randomizing the center of mass

66 · Domain randomization

Five tall blocks standing on the ground, 8 cm wide. At 1 s the script moves each block's body_com sideways by a sampled offset, without moving its shape. Blocks whose center of mass leaves the 4 cm half-width footprint tip over. Steel caches the body-to-COM transforms, so the shift takes effect only after notify_model_changed(BODY_INERTIAL_PROPERTIES); without it every block keeps standing.

0.00 s

What to look at

  • With notify: the blocks with |offset| > 4 cm fall over at 1 s.
  • Without notify: no block moves, whatever its offset.

Key parameter

model.body_com

A body-frame offset of the center of mass. body_q stays the body origin; a free joint's joint_qd is the COM velocity.

Try changing

  • Offset the COM upward instead and push the blocks to see the lower tipping threshold.
  • Randomize the COM of a floating base in the quadruped example.

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: no block moves, whatever its offset.

Considerations

Cost
Call only, default solver, six-link chain and four boxes per world, 1024 worlds (1.12 ms per collide + step): BODY_INERTIAL_PROPERTIES 31 µs (17 µs captured). Flags measured one at a time; combined calls and feature validation can cost more. Body-inertial and joint-DOF notifications also 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
Payloads and grasped objects whose balance point is uncertain.

The script

cd tutorials && python dr_com.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_com.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Center-of-mass randomization: per-world body_com + BODY_INERTIAL_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
HALF_WIDTH = 0.04 # the block tips once its COM leaves the footprint
def build():
block = newton.ModelBuilder()
body = block.add_body(xform=wp.transform((0.0, 0.0, 0.15), wp.quat_identity()))
block.add_shape_box(body, hx=HALF_WIDTH, hy=HALF_WIDTH, hz=0.15)
builder = newton.ModelBuilder()
builder.replicate(block, WORLDS, spacing=(0.0, 0.3, 0.0))
builder.add_ground_plane()
return builder.finalize()
def simulate(rec, label, com_x, notify):
model = build()
solver = SolverSteel(model)
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
state_0, state_1, control = model.state(), model.state(), model.control()
run = rec.variant(label, model)
for frame in range(rec.frames(4.0)):
if frame == rec.fps: # randomize at 1 s, as an episode reset would
com = model.body_com.numpy()
com[:, 0] = com_x[model.body_world.numpy()] # body-frame offset; mass unchanged
model.body_com.assign(com)
if notify:
solver.notify_model_changed(newton.ModelFlags.BODY_INERTIAL_PROPERTIES)
q = state_0.body_q.numpy()
up = [wp.quat_rotate(wp.quat(*x[3:]), wp.vec3(0.0, 0.0, 1.0))[2] for x in q]
tilt = np.degrees(np.arccos(np.clip(up, -1.0, 1.0)))
run.frame(state_0, **{f"com {c * 100:+.2f} cm": t for c, t in zip(com_x, tilt)})
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(1)
com_x = np.sort(rng.uniform(-0.07, 0.07, WORLDS))
rec = Recorder()
simulate(rec, "COM shifted per world + notify", com_x, True)
simulate(rec, "COM shifted per world, no notify", com_x, 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