Actuators#
Warning
The actuator API is experimental and may change in future releases. Feedback is welcome — please file issues or discussion threads.
Actuators provide composable implementations that read physics simulation state, compute effort, and accumulate (scatter-add) the effort into control arrays for application to the simulation. The caller must zero the output array before stepping actuators each frame. The simulator does not need to be part of Newton: actuators are designed to be reusable anywhere the caller can provide state arrays and consume effort.
Each Actuator instance is vectorized: a single actuator object
operates on a batch of DOF indices in global state and control arrays, allowing
efficient integration into RL workflows with many parallel environments.
The goal is to provide canonical actuator models with support for differentiability and graphable execution where the underlying controller implementation supports it. Actuators are designed to be easy to customize and extend for specific actuator models.
Architecture#
An actuator is composed from three building blocks, applied in this order:
Actuator
├── Delay (optional: delays command inputs by N actuator timesteps)
├── Controller (control law that computes raw effort)
└── Clamping[] (clamps raw effort based on motor-limit modeling)
├── ClampingMaxEffort (±max_effort symmetric clamp)
├── ClampingDCMotor (velocity-dependent saturation)
└── ClampingPositionBased (position-dependent lookup table)
- Delay
Optionally delays command inputs (control targets and feedforward terms) by N actuator timesteps before they reach the controller, modeling communication or processing latency. The delay always produces output; when the buffer is empty or a DOF has
delay_steps == 0, the current command inputs are used directly. When underfilled, the lag is clamped to the available history so the oldest available entry is returned.- Controller
Computes raw actuator effort [N or N·m] from the current simulator state and control targets. This is the actuator’s control law — for example PD, PID, or neural-network-based control. See the individual controller class documentation for the control-law equations.
- Clamping
Clamps raw effort based on motor-limit modeling. This applies post-controller output limits to the computed effort to model motor limits such as saturation, back-EMF losses, performance envelopes, or position-dependent effort limits. Multiple clamping stages can be combined on a single actuator.
The per-step pipeline is:
Delay read → Controller → Clamping → Scatter-add → State updates (controller + delay write)
Controllers and clamping objects are pluggable: implement the
Controller or Clamping base class to add new models.
Note
Current limitations: the first version does not include a transmission model (gear ratios / linkage transforms), supports only single-input single-output (SISO) actuators (one DOF per actuator), and does not model actuator dynamics (inertia, friction, thermal effects).
Usage#
Actuators are registered during model construction with
add_actuator() and are instantiated automatically
when the model is finalized:
builder.add_actuator(
ControllerPD,
index=dof_index,
kp=100.0,
kd=10.0,
delay_steps=5,
clamping=[(ClampingMaxEffort, {"max_effort": 50.0})],
)
model = builder.finalize()
For manual construction (outside of ModelBuilder), compose the
components directly:
indices = wp.array([0], dtype=wp.uint32)
kp = wp.array([100.0], dtype=wp.float32)
kd = wp.array([10.0], dtype=wp.float32)
max_e = wp.array([50.0], dtype=wp.float32)
actuator = Actuator(
indices,
controller=ControllerPD(kp=kp, kd=kd),
delay=Delay(delay_steps=wp.array([5], dtype=wp.int32), max_delay=5),
clamping=[ClampingMaxEffort(max_effort=max_e)],
)
Stateful Actuators#
Controllers that maintain internal state (e.g. ControllerPID with an
integral accumulator, or ControllerNeuralLSTM with hidden/cell state) and
actuators with a Delay require explicit double-buffered state
management. Create two state objects with Actuator.state() and swap them
after each step:
state_0 = model.actuators[0].state()
state_1 = model.actuators[0].state()
state = model.state()
control = model.control()
for step in range(3):
control.joint_f.zero_() # zero output before stepping actuators
model.actuators[0].step(state, control, state_0, state_1, dt=0.01)
state_0, state_1 = state_1, state_0
Stateless actuators (e.g. a plain PD controller without delay) do not require state objects — simply omit them:
# Build a stateless actuator (no delay, stateless controller)
b2 = newton.ModelBuilder()
lk = b2.add_link()
jt = b2.add_joint_revolute(parent=-1, child=lk, axis=newton.Axis.Z)
b2.add_articulation([jt])
b2.add_actuator(ControllerPD, index=b2.joint_qd_start[jt], kp=50.0)
m2 = b2.finalize()
m2.actuators[0].step(m2.state(), m2.control())
Differentiability and Graph Capture#
Whether an actuator supports differentiability and CUDA graph capture depends on
its controller. ControllerPD and ControllerPID are fully
graphable. Neural-network controllers (ControllerNeuralMLP,
ControllerNeuralLSTM) require PyTorch and are not graphable due to
framework interop overhead.
Actuator.is_graphable() returns True when all components can be
captured in a CUDA graph.
Available Components#
Delay#
Delay— circular-buffer delay for control targets (stateful).
Controllers#
ControllerPD— proportional-derivative control law (stateless).ControllerPID— proportional-integral-derivative control law (stateful: integral accumulator with anti-windup clamp).ControllerNeuralMLP— MLP neural-network controller (requires PyTorch, stateful: position/velocity history buffers).ControllerNeuralLSTM— LSTM neural-network controller (requires PyTorch, stateful: hidden/cell state).
See the API documentation for each controller’s control-law equations.
Clamping#
ClampingMaxEffort— symmetric clamp to ±max_effort per actuator.ClampingDCMotor— velocity-dependent effort saturation using the DC motor effort-speed characteristic.ClampingPositionBased— position-dependent effort limits via interpolated lookup table (e.g. for linkage-driven joints).
Multiple clamping objects can be stacked on a single actuator; they are applied in sequence.
Customization#
Any actuator can be assembled from the existing building blocks — mix and
match controllers, clamping stages, and delay to fit a specific use case.
When the built-in components are not sufficient, implement new ones by
subclassing Controller or Clamping.
For example, a custom controller needs to implement
compute(), resolve_arguments(),
is_stateful(), and is_graphable():
compute body is omitted; see existing
controllers for complete examples.#import warp as wp
from newton.actuators import Controller
class MyController(Controller):
@classmethod
def resolve_arguments(cls, args):
return {"gain": args.get("gain", 1.0)}
def __init__(self, gain: wp.array):
self.gain = gain
def is_stateful(self):
return False
def is_graphable(self):
return True
def compute(self, positions, velocities, target_pos, target_vel,
feedforward, pos_indices, vel_indices,
target_pos_indices, target_vel_indices,
forces, state, dt, device=None):
# Launch a Warp kernel that writes effort into `forces`
...
resolve_arguments maps user-provided keyword arguments (from
add_actuator() or USD schemas) to constructor
parameters, filling in defaults where needed.
Similarly, a custom clamping stage subclasses Clamping and implements
modify_forces() (which reads effort from a source buffer and writes bounded effort to a destination buffer).
See Also#
newton.actuators— full API referencenewton.ModelBuilder.add_actuator()— registering actuators during model construction