Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 232 additions & 0 deletions docs/concepts/actuators.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
.. SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per AGENTS.md, user-facing changes (especially Changed/Removed) require a CHANGELOG entry with migration guidance. This PR (a) deletes the newton-actuators dependency, (b) replaces the old ModelBuilder.add_actuator(actuator_class=..., input_indices=..., output_indices=...) signature with a very different add_actuator(controller_class, index, clamping=[...], delay=..., pos_index=..., **ctrl_kwargs) API, and (c) changes ArticulationView.get_actuator_parameter/set_actuator_parameter to require a component argument.

All three are breaking for any existing user. Could we add entries under Changed/Removed with explicit before/after snippets? e.g.

- Remove `ActuatorPD`/`ActuatorPID` etc. in favor of composed
  `ControllerPD` + `ClampingMaxForce` etc. Migrate:
    builder.add_actuator(ActuatorPD, input_indices=[i], kp=..., kd=..., max_force=...)
  →
    builder.add_actuator(ControllerPD, index=i, kp=..., kd=...,
                         clamping=[(ClampingMaxForce, {"max_force": ...})])

.. SPDX-License-Identifier: CC-BY-4.0

.. currentmodule:: newton.actuators

Actuators
=========

Newton provides composable actuator implementations that read physics simulation
state, compute actuator forces, and write the forces back to control arrays for
application to the simulation. The simulator does not need to be part of
Newton: the library is designed to be reusable anywhere the caller can provide
state arrays and consume forces.

Each :class:`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. The actuator subsystem is 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:

.. code-block:: text

Actuator
├── Delay (optional: delays control targets by N timesteps)
├── Controller (control law that computes raw forces)
└── Clamping[] (clamps raw forces based on motor-limit modeling)
├── ClampingMaxForce (±max_force box clamp)
├── ClampingDCMotor (velocity-dependent saturation)
└── ClampingPositionBased (angle-dependent lookup table)

**Delay**
Optionally delays the control targets (e.g. position or velocity) by *N*
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question I had in the USD PR applies here too, does delay apply also to FF terms and should we therefore prefer the control inputs instead of specific target language?

I'm aware I initially suggested the more narrow control targets, sorry.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, delay is applied to all the inputs.

timesteps before they reach the controller, allowing the actuator to model
communication or processing latency. While the delay buffer is still
filling, no forces are produced.

**Controller**
Computes raw forces or torques from the current simulator state and control
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given my comments on the USD PR and having looked at PhysX perf envelope and lab API docs, should we standardize to effort to mean force/torque in this PR also?

Not going to comment further in this PR we can decide first then make a single refactor.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. I can use the word effort

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 forces based on motor-limit modeling. This applies
post-controller output limits to the computed forces or torques to model
motor limits such as saturation, back-EMF losses, performance envelopes, or
angle-dependent torque limits. Multiple clamping stages can be combined on
a single actuator.

The per-step pipeline is:

.. code-block:: text

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per Newton docs convention (articulations.rst, collisions.rst), runnable Python examples in concept pages use .. testcode:: so they're exercised by the doctest builder and can't silently rot. This file uses .. code-block:: python for every Python example (lines 82, 104, 134, 146, 206) — none are executed.

Could we convert the first four (builder-level usage, manual construction, USD loading, selection API) to .. testcode:: with appropriate named groups so setup state carries across? The custom-controller example at line 206 has a stub body (...) that won't execute cleanly — either give it a minimal concrete toy body so testcode works, or keep that one as .. code-block:: with a note explaining why.

Delay → Controller → Clamping → Scatter-add to output

Controllers and clamping objects are pluggable: implement the
:class:`Controller` or :class:`Clamping` base class to add new models.

.. note::
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make this more prominent in the intro.

Should this also have an experimental feature disclaimer as we discussed? We don't need it if you are fairly sure about the future-proofness of the API with upcoming changes like transmission.


**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
:meth:`~newton.ModelBuilder.add_actuator` and are instantiated automatically
when the model is finalized:

.. code-block:: python

import newton
from newton.actuators import ControllerPD, ClampingMaxForce, Delay

builder = newton.ModelBuilder()
# ... add links, joints, articulations ...

builder.add_actuator(
controller_type=ControllerPD,
index=dof_index,
kp=100.0,
kd=10.0,
clamping_types=[ClampingMaxForce],
max_force=50.0,
)

model = builder.finalize()

For manual construction (outside of :class:`~newton.ModelBuilder`), compose the
components directly:

.. code-block:: python

import warp as wp
from newton.actuators import Actuator, ControllerPD, ClampingMaxForce, Delay

indices = wp.array([0, 1, 2], dtype=wp.uint32, device="cuda:0")
kp = wp.array([100.0, 100.0, 100.0], dtype=wp.float32, device="cuda:0")
kd = wp.array([10.0, 10.0, 10.0], dtype=wp.float32, device="cuda:0")
max_f = wp.array([50.0, 50.0, 50.0], dtype=wp.float32, device="cuda:0")

actuator = Actuator(
indices,
delay=Delay(delay=5),
controller=ControllerPD(kp=kp, kd=kd),
clamping=[ClampingMaxForce(max_force=max_f)],
)

# In the simulation loop:
actuator.step(sim_state, sim_control, state_a, state_b, dt=0.01)


Stateful Actuators
------------------

Controllers that maintain internal state (e.g. :class:`ControllerPID` with an
integral accumulator, or :class:`ControllerNetLSTM` with hidden/cell state) and
actuators with a :class:`Delay` require explicit double-buffered state
management. Create two state objects with :meth:`Actuator.state` and swap them
after each step:

.. code-block:: python

state_a = actuator.state()
state_b = actuator.state()

for step in range(num_steps):
actuator.step(sim_state, sim_control, state_a, state_b, dt=dt)
state_a, state_b = state_b, state_a # swap

Stateless actuators (e.g. a plain PD controller without delay) do not require
state objects — pass ``None`` for both.

Differentiability and Graph Capture
-----------------------------------

Whether an actuator supports differentiability and CUDA graph capture depends on
its controller. :class:`ControllerPD` and :class:`ControllerPID` are fully
graphable. Neural-network controllers (:class:`ControllerNetMLP`,
:class:`ControllerNetLSTM`) require PyTorch and are not graphable due to
framework interop overhead.

:meth:`Actuator.is_graphable` returns ``True`` when all components can be
captured in a CUDA graph.

Available Components
--------------------

Delay
^^^^^

* :class:`Delay` — circular-buffer delay for control targets (stateful).

Controllers
^^^^^^^^^^^

* :class:`ControllerPD` — proportional-derivative control law (stateless).
* :class:`ControllerPID` — proportional-integral-derivative control law
(stateful: integral accumulator with anti-windup clamp).
* :class:`ControllerNetMLP` — MLP neural-network controller (requires
PyTorch, stateful: position/velocity history buffers).
* :class:`ControllerNetLSTM` — LSTM neural-network controller (requires
PyTorch, stateful: hidden/cell state).

See the API documentation for each controller's control-law equations.

Clamping
^^^^^^^^

* :class:`ClampingMaxForce` — symmetric box clamp to ±max_force per actuator.
* :class:`ClampingDCMotor` — velocity-dependent torque saturation using the DC
motor torque-speed characteristic.
* :class:`ClampingPositionBased` — angle-dependent torque 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 :class:`Controller` or :class:`Clamping`.

For example, a custom controller needs to implement
:meth:`~Controller.compute` and :meth:`~Controller.resolve_arguments`:

.. code-block:: python

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 compute(self, positions, velocities, target_pos, target_vel,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example signature here is stale vs. Controller.compute in base.py:

def compute(self, positions, velocities, target_pos, target_vel,
            feedforward, input_indices, target_indices, forces,
            state, dt, device=None):

The real signature uses pos_indices, vel_indices, target_pos_indices, target_vel_indices (four index arrays, not two). Anyone following this example to write a custom controller will get a TypeError on kernel launch. Could we update the example to match the 4-index layout?

feedforward, input_indices, target_indices, forces,
num_actuators, state, dt):
# Launch a Warp kernel that writes into `forces`
...

``resolve_arguments`` maps user-provided keyword arguments (from
:meth:`~newton.ModelBuilder.add_actuator` or USD schemas) to constructor
parameters, filling in defaults where needed.

Similarly, a custom clamping stage subclasses :class:`Clamping` and implements
:meth:`~Clamping.modify_forces`.

See Also
--------

* :mod:`newton.actuators` — full API reference
* :meth:`newton.ModelBuilder.add_actuator` — registering actuators during
model construction
2 changes: 1 addition & 1 deletion docs/guide/release.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Pre-release planning
- Determine target version (``X.Y.Z``).
* - ☐
- Confirm dependency versions and availability: warp-lang, mujoco,
mujoco-warp, newton-usd-schemas, newton-actuators.
mujoco-warp, newton-usd-schemas.
* - ☐
- Set timeline: code freeze → RC1 → testing window → GA.
* - ☐
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Newton Physics
Custom Attributes <concepts/custom_attributes>
Extended Attributes <concepts/extended_attributes>
Collisions and Contacts <concepts/collisions>
Actuators <concepts/actuators>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget to add api/newton_actuators to the API Reference toctree (further down in this file) and run docs/generate_api.py to generate the page. Per AGENTS.md — and without it, the :mod:newton.actuators / `:class:`ControllerPD / :meth:Actuator.state`` cross-refs in the new concept doc won't resolve.


.. toctree::
:maxdepth: 1
Expand Down
3 changes: 2 additions & 1 deletion newton/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,10 @@
# ==================================================================================
# submodule APIs
# ==================================================================================
from . import geometry, ik, math, selection, sensors, solvers, usd, utils, viewer # noqa: E402
from . import actuators, geometry, ik, math, selection, sensors, solvers, usd, utils, viewer # noqa: E402

__all__ += [
"actuators",
"geometry",
"ik",
"math",
Expand Down
24 changes: 24 additions & 0 deletions newton/_src/actuators/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
# SPDX-License-Identifier: Apache-2.0

from .actuator import Actuator
from .clamping import Clamping, ClampingDCMotor, ClampingMaxForce, ClampingPositionBased
from .controllers import Controller, ControllerNetLSTM, ControllerNetMLP, ControllerPD, ControllerPID
from .delay import Delay
from .usd_parser import ActuatorParsed, parse_actuator_prim

__all__ = [
"Actuator",
"ActuatorParsed",
"Clamping",
"ClampingDCMotor",
"ClampingMaxForce",
"ClampingPositionBased",
"Controller",
"ControllerNetLSTM",
"ControllerNetMLP",
"ControllerPD",
"ControllerPID",
"Delay",
"parse_actuator_prim",
]
Loading
Loading