Skip to content

PD drives

30 · Joints and drives

Four arms with gravity. Three get a position target of 45° at 0.5 s with different gains: soft and underdamped, soft and damped, stiff and damped. The fourth has only target_kd and follows a velocity target of 2 rad/s from 1 s. By default Steel integrates drives implicitly in the mass matrix, so stiff gains stay stable at large steps.

0.00 s

What to look at

  • k_e 50, k_d 1 overshoots to about 61° and rings.
  • k_e 400 settles closest to 45°; the soft drives sag under gravity.
  • The velocity-driven arm spins continuously.

Key parameter

target_ke / target_kd

Joint arguments; Control.joint_target_q and joint_target_qd set the targets each step.

Try changing

  • Set target_ke=5000 at DT = 1/60.
  • Add effort_limit=1.0 to the stiff arm.

Concept

Implicit drives

What it is

A PD drive pulls a joint toward a target position with stiffness kₑ and toward a target velocity with damping k_d. By default Steel folds h·k_d + h²·kₑ into the mass matrix, an implicit integration that stays stable for stiff gains at large steps.

H̃ = H + h·k_d + h²·kₑ

In this scene

kₑ 50, k_d 1 overshoots to 61°; kₑ 400, k_d 20 settles at 44.7°. Gravity makes soft drives sag.

Considerations

Cost
None extra: the drive is part of the factorization.
Accuracy
Only the explicit drive force is clamped to effort_limit; the implicit part is not.
Interactions
Driven articulations never fall asleep.
Use it when
Default for robot joints; use drive rows when effort limits must hold exactly.

The script

cd tutorials && python 30_pd_drives.py prints each variant's final metric; --record out.json also writes the replay. Every tutorial imports replay.py from the same folder.

tutorials/30_pd_drives.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Joint PD drives: position targets (target_ke, target_kd) and velocity targets (target_kd)."""
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
GAINS = [(50.0, 1.0), (50.0, 10.0), (400.0, 20.0)] # (ke, kd) of the three position-driven arms
def tf(x, y, z):
return wp.transform((x, y, z), wp.quat_identity())
def arm(builder, y, **drive):
link = builder.add_link(xform=tf(0.2, y, 0.6))
builder.add_shape_box(
link, hx=0.2, hy=0.02, hz=0.02, cfg=newton.ModelBuilder.ShapeConfig(density=400.0)
)
builder.add_shape_sphere(
link,
xform=tf(0.2, 0.0, 0.0),
radius=0.04,
cfg=newton.ModelBuilder.ShapeConfig(density=2000.0),
)
j = builder.add_joint_revolute(
-1,
link,
axis=(0.0, -1.0, 0.0),
parent_xform=tf(0.0, y, 0.6),
child_xform=tf(-0.2, 0.0, 0.0),
**drive,
)
builder.add_articulation([j])
def build():
builder = newton.ModelBuilder()
for i, (ke, kd) in enumerate(GAINS):
arm(builder, 0.3 * i - 0.45, target_ke=ke, target_kd=kd)
arm(builder, 0.45, target_kd=5.0) # velocity drive: no stiffness
return builder.finalize()
def simulate(rec, label):
model = build()
solver = SolverSteel(model)
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
state_0, state_1, control = model.state(), model.state(), model.control()
run = rec.variant(label, model)
for frame in range(rec.frames(4.0)):
t = frame / rec.fps
q = np.degrees(state_0.joint_q.numpy())
run.frame(
state_0,
**{f"ke{ke:.0f}_kd{kd:.0f}": q[i] for i, (ke, kd) in enumerate(GAINS)},
spin_rate=state_0.joint_qd.numpy()[3],
)
# step the position target to 45 degrees at 0.5 s; spin the last arm at 2 rad/s after 1 s
control.joint_target_q.assign(
np.array([math.radians(45.0) if t >= 0.5 else 0.0] * 3 + [0.0], dtype=np.float32)
)
control.joint_target_qd.assign(
np.array([0.0, 0.0, 0.0, 2.0 if t >= 1.0 else 0.0], dtype=np.float32)
)
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, "three PD gains and one velocity drive")
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