Skip to content

Randomizing armature

72 · Domain randomization

Five drive-held arms on vertical hinges. At 1 s the script writes a sampled joint_armature per world, calls notify_model_changed(JOINT_DOF_INERTIAL_PROPERTIES), and steps the drive target to 90 degrees. More armature means more reflected rotor inertia: a slower, more overshooting response. Steel caches the effective armature, so without the notification every arm responds identically.

0.00 s

What to look at

  • With notify: the response slows and overshoots more as armature grows.
  • Without notify: all five arms move together.

Key parameter

model.joint_armature

Added to the DOF's diagonal of the mass matrix. JOINT_DOF_PROPERTIES also covers it.

Try changing

  • Randomize armature together with drive gains in one notify call (OR the flags).
  • Set armature to 0 on a very light link and watch the response get stiff.

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: all five arms move together.

Considerations

Cost
Call only, default solver, six-link chain and four boxes per world, 1024 worlds (1.12 ms per collide + step): JOINT_DOF_INERTIAL_PROPERTIES 36 µs (9 µ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
Geared motors whose reflected inertia is uncertain.

The script

cd tutorials && python dr_armature.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_armature.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Armature randomization: per-world joint_armature + JOINT_DOF_INERTIAL_PROPERTIES."""
import math
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
def tf(x, y, z):
return wp.transform((x, y, z), wp.quat_identity())
def build():
arm = newton.ModelBuilder()
link = arm.add_link(xform=tf(0.0, 0.0, 0.6))
arm.add_shape_box(link, xform=tf(0.15, 0.0, 0.0), hx=0.15, hy=0.02, hz=0.02)
j = arm.add_joint_revolute(
-1, link, axis=newton.Axis.Z, parent_xform=tf(0.0, 0.0, 0.6), target_ke=5.0, target_kd=0.2
)
arm.add_articulation([j])
builder = newton.ModelBuilder()
builder.replicate(arm, WORLDS, spacing=(0.0, 0.35, 0.0))
return builder.finalize()
def simulate(rec, label, armature, notify):
model = build()
solver = SolverSteel(model)
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 frame in range(rec.frames(4.0)):
if frame == rec.fps: # randomize at 1 s, then step the target to 90 degrees
model.joint_armature.assign(armature.astype(np.float32)) # kg m^2, one DOF per world
if notify:
solver.notify_model_changed(newton.ModelFlags.JOINT_DOF_INERTIAL_PROPERTIES)
control.joint_target_q.fill_(math.radians(90.0))
q = np.degrees(state_0.joint_q.numpy())
run.frame(state_0, **{f"armature {a:.3f}": v for a, v in zip(armature, q)})
for _ in range(SUBSTEPS):
solver.step(state_0, state_1, control, None, DT)
state_0, state_1 = state_1, state_0
run.check(solver)
if __name__ == "__main__":
rng = np.random.default_rng(7)
armature = np.sort(rng.uniform(0.0, 0.05, WORLDS))
rec = Recorder()
simulate(rec, "armature sampled per world + notify", armature, True)
simulate(rec, "armature sampled per world, no notify", armature, 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