Skip to content

Randomizing joint friction

71 · Domain randomization

Five pendulums released from 70 degrees with sampled dry joint_friction. Steel solves joint friction as one row per DOF, and these rows exist only when the solver is constructed with enable_joint_friction=True; the values themselves are read every step. With friction on, each pendulum sticks at an angle that grows with its friction. With the default solver the values are ignored and all pendulums swing alike.

0.00 s

What to look at

  • enable_joint_friction=True: the pendulums stop at different angles.
  • Default: the friction values have no effect.

Key parameter

SolverSteel(model, enable_joint_friction=True)

Experimental and CUDA only. An eager notification validates the new values and raises ValueError for invalid ones.

Try changing

  • Re-sample joint_friction mid-swing and notify with JOINT_DOF_FORCE_PROPERTIES.
  • Combine joint friction with joint damping.

Concept

When Steel sees a randomized value

What it is

The rows that use this input exist only if the solver was constructed with the feature enabled; the values themselves are read every step. Randomize the values, not whether the feature is on.

In this scene

Default: the friction values have no effect.

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
Turning the feature on later needs a new solver.
Use it when
Gearboxes and joints that stick; build the solver with enable_joint_friction=True once.

The script

cd tutorials && python dr_joint_friction.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_friction.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Joint friction randomization: per-world joint_friction, enabled at construction."""
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(70.0)
builder = newton.ModelBuilder()
builder.replicate(pendulum, WORLDS, spacing=(0.0, 0.3, 0.0))
return builder.finalize()
def simulate(rec, label, friction, enable):
model = build()
# the friction rows exist only if they are enabled here; this cannot be switched on later
solver = SolverSteel(model, enable_joint_friction=enable)
model.joint_friction.assign(friction.astype(np.float32)) # N m, one DOF per world
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"friction {f:.3f}": a for f, a in zip(friction, 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(6)
friction = np.sort(rng.uniform(0.0, 0.6, WORLDS))
rec = Recorder()
simulate(rec, "enable_joint_friction=True", friction, True)
simulate(rec, "enable_joint_friction=False (default): ignored", friction, 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