Skip to content

Joint velocity limits

33 · Joints and drives

A flywheel under a constant 3 N·m torque with a 6 rad/s velocity limit. Without enable_joint_velocity_limits it keeps accelerating. With it, Steel scales velocities that already exceed a limit and adds a pair of clamp rows per limited DOF.

0.00 s

What to look at

  • Off: the flywheel spins up to about 180 rad/s in 3 s.
  • On: it holds 6 rad/s.

Key parameter

enable_joint_velocity_limits

Uses Model.joint_velocity_limit (add_joint_* velocity_limit). velocity_limit_activation_fraction creates the rows only near the limit.

Try changing

  • Set velocity_limit_activation_fraction=0.9.
  • Use drive_mode="rows" with a velocity target above the limit.

Concept

Joint velocity limits

What it is

Steel first scales articulation velocities that already exceed a limit, then adds a pair of clamp rows per limited DOF so the solve cannot exceed it.

In this scene

Under a constant 3 N·m the flywheel reaches 179 rad/s in 3 s without limits and holds 6.0 rad/s with them.

Considerations

Cost
Two rows per limited DOF; velocity_limit_activation_fraction creates them only near the limit.
Accuracy
The gate samples velocity before the solve, so crossing within a step is clamped a step late.
Use it when
Motors with rated speeds.

The script

cd tutorials && python 33_joint_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/33_joint_velocity_limits.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Joint velocity limits: a flywheel under constant torque."""
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
def build():
builder = newton.ModelBuilder(gravity=(0.0, 0.0, 0.0))
hub = builder.add_link(xform=wp.transform((0.0, 0.0, 0.5), wp.quat_identity()))
builder.add_shape_cylinder(
hub, radius=0.2, half_height=0.02, cfg=newton.ModelBuilder.ShapeConfig(density=500.0)
)
builder.add_shape_box(
hub,
xform=wp.transform((0.15, 0.0, 0.03), wp.quat_identity()),
hx=0.04,
hy=0.02,
hz=0.01,
cfg=newton.ModelBuilder.ShapeConfig(density=50.0),
)
j = builder.add_joint_revolute(
-1,
hub,
axis=newton.Axis.Z,
parent_xform=wp.transform((0.0, 0.0, 0.5), wp.quat_identity()),
velocity_limit=6.0,
)
builder.add_articulation([j])
return builder.finalize()
def simulate(rec, label, **options):
model = build()
solver = SolverSteel(model, **options)
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
state_0, state_1, control = model.state(), model.state(), model.control()
control.joint_f.assign(np.array([3.0], dtype=np.float32)) # 3 N m, constant
run = rec.variant(label, model)
for _ in range(rec.frames(3.0)):
run.frame(state_0, omega=state_0.joint_qd.numpy()[0])
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__":
rec = Recorder()
simulate(rec, "velocity limits off (default)")
simulate(rec, "enable_joint_velocity_limits=True", enable_joint_velocity_limits=True)
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