Skip to content

Randomizing joint damping

70 · Domain randomization

Five free-swinging pendulums released from 60 degrees. Each world gets a sampled joint_damping, followed by notify_model_changed(JOINT_DOF_FORCE_PROPERTIES). Higher damping makes the swing decay faster. Steel copies joint_damping into its own buffer, so without the notification every pendulum keeps the original (zero) damping and they all swing together.

0.00 s

What to look at

  • With notify: five different decay rates.
  • Without notify: five identical, undamped swings.

Key parameter

model.joint_damping

Passive viscous damping per DOF. JOINT_DOF_PROPERTIES also covers it.

Try changing

  • Randomize passive springs: joint_stiffness needs JOINT_DOF_FORCE_PROPERTIES and joint_rest_q needs JOINT_REFERENCE_POSE_PROPERTIES; OR the two flags (or use JOINT_DOF_PROPERTIES) when changing both.
  • Index DOFs through model.joint_dof_world_start for robots with many joints per world.

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: five identical, undamped swings.

Considerations

Cost
Call only, default solver, six-link chain and four boxes per world, 1024 worlds (1.12 ms per collide + step): JOINT_DOF_FORCE_PROPERTIES 25 µs (7 µ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
Passive joints and motors with uncertain viscous losses; keep damping small on light links.

The script

cd tutorials && python dr_joint_damping.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_joint_damping.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Joint damping randomization: per-world joint_damping + JOINT_DOF_FORCE_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():
pendulum = newton.ModelBuilder()
link = pendulum.add_link(xform=tf(0.0, 0.0, 0.8))
pendulum.add_shape_capsule(link, xform=tf(0.0, 0.0, -0.2), radius=0.015, half_height=0.18)
pendulum.add_shape_sphere(link, xform=tf(0.0, 0.0, -0.4), radius=0.05)
j = pendulum.add_joint_revolute(-1, link, axis=newton.Axis.Y, parent_xform=tf(0.0, 0.0, 0.8))
pendulum.add_articulation([j])
pendulum.joint_q[-1] = math.radians(60.0)
builder = newton.ModelBuilder()
builder.replicate(pendulum, WORLDS, spacing=(0.0, 0.3, 0.0))
return builder.finalize()
def simulate(rec, label, damping, notify):
model = build()
solver = SolverSteel(model)
# one DOF per world here; in general index DOFs through model.joint_qd_start
model.joint_damping.assign(damping.astype(np.float32))
if notify:
solver.notify_model_changed(newton.ModelFlags.JOINT_DOF_FORCE_PROPERTIES)
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 _ in range(rec.frames(5.0)):
q = np.degrees(state_0.joint_q.numpy())
run.frame(state_0, **{f"damping {d:.3f}": a for d, a in zip(damping, 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(5)
damping = np.sort(rng.uniform(0.0, 0.2, WORLDS)) # N m s / rad
rec = Recorder()
simulate(rec, "joint_damping sampled per world + notify", damping, True)
simulate(rec, "joint_damping sampled per world, no notify", damping, 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