Skip to content

Randomizing joint velocity limits

75 · Domain randomization

Five rotors spun up by the same small constant torque, each with a sampled joint_velocity_limit. Steel enforces velocity limits as rows only when the solver is constructed with enable_joint_velocity_limits=True; the limit values are read every step. With the rows on, each rotor levels off at its own limit; with the default solver they all keep accelerating.

0.00 s

What to look at

  • enable_joint_velocity_limits=True: each rotor plateaus at its sampled limit.
  • Default: the limits are ignored.

Key parameter

SolverSteel(model, enable_joint_velocity_limits=True)

One row per limited DOF; the rows count against the articulation row capacity.

Try changing

  • Lower a limit mid-run and watch the rotor brake to it.
  • Drive the rotors with a velocity target instead of a torque.

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 limits are ignored.

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
Motors with uncertain top speed; build with enable_joint_velocity_limits=True.

The script

cd tutorials && python dr_velocity_limits.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_velocity_limits.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Velocity-limit randomization: per-world joint_velocity_limit, enabled at construction."""
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
TORQUE = 0.05 # N m, constant on every rotor
def tf(x, y, z):
return wp.transform((x, y, z), wp.quat_identity())
def build():
rotor = newton.ModelBuilder()
hub = rotor.add_link(xform=tf(0.0, 0.0, 0.5))
rotor.add_shape_cylinder(
hub, radius=0.12, half_height=0.02, cfg=newton.ModelBuilder.ShapeConfig(density=500.0)
)
rotor.add_shape_box(hub, xform=tf(0.1, 0.0, 0.03), hx=0.03, hy=0.015, hz=0.01) # a marker
j = rotor.add_joint_revolute(-1, hub, axis=newton.Axis.Z, parent_xform=tf(0.0, 0.0, 0.5))
rotor.add_articulation([j])
builder = newton.ModelBuilder()
builder.replicate(rotor, WORLDS, spacing=(0.0, 0.35, 0.0))
return builder.finalize()
def simulate(rec, label, limit, enable):
model = build()
# the velocity-limit rows exist only if they are enabled here
solver = SolverSteel(model, enable_joint_velocity_limits=enable)
model.joint_velocity_limit.assign(limit.astype(np.float32)) # rad/s, 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()
control.joint_f.fill_(TORQUE)
run = rec.variant(label, model)
for _ in range(rec.frames(3.0)):
omega = state_0.joint_qd.numpy()
run.frame(state_0, **{f"limit {v:.1f} rad/s": w for v, w in zip(limit, omega)})
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(14)
limit = np.sort(rng.uniform(2.0, 12.0, WORLDS))
rec = Recorder()
simulate(rec, "enable_joint_velocity_limits=True", limit, True)
simulate(rec, "velocity limits off (default): ignored", limit, 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