Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
237 changes: 237 additions & 0 deletions docs/concepts/actuators.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
.. SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
Copy link
Copy Markdown
Owner Author

@adenzler-nvidia adenzler-nvidia Apr 17, 2026

Choose a reason for hiding this comment

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

📝 Review comment (will ship upstream) — v1

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
=========

Actuators provide composable implementations that read physics simulation
state, compute forces, and write the forces back to control arrays for
application to the simulation. 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 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. 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:

.. 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*
timesteps before they reach the controller, allowing the actuator to model
communication or processing latency. The delay always produces output;
when the buffer is still filling, the lag is clamped to the available
history so the most recent data is returned.

**Controller**
Computes raw forces or torques 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 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
Owner Author

Choose a reason for hiding this comment

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

all of these should use test-code, right?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Missed your question on my first pass — my grouping only looked at threads I started. Fixing that in the skill now (see below).

On the substance: yes, confirmed. docs/concepts/articulations.rst uses .. testcode:: 13 times and docs/concepts/collisions.rst uses it 6 times (vs. only 2 .. code-block:: python in collisions, both for intentionally-stub code). The new actuators.rst has 5 .. code-block:: python blocks (lines 82, 104, 134, 146, 206) — none of them in testcode — so the Python examples here are not executed by the doctest runner and can silently rot.

One nuance worth flagging: the custom-controller example at line 206 has a stub body (# Launch a Warp kernel... ...) and won't run cleanly as testcode. Either give it a minimal concrete toy body, or leave that single one as .. code-block:: with a brief comment explaining why. The other four should definitely be testcode.

📝 Review comment (will ship upstream) — v1

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 read → Controller → Clamping → Scatter-add → State updates (controller + delay write)

Controllers and clamping objects are pluggable: implement the
:class:`Controller` or :class:`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
:meth:`~newton.ModelBuilder.add_actuator` and are instantiated automatically
when the model is finalized:

.. code-block:: python

import newton
from newton.actuators import ClampingMaxForce, ControllerPD

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

builder.add_actuator(
ControllerPD,
index=dof_index,
kp=100.0,
kd=10.0,
delay=5,
clamping=[(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,
controller=ControllerPD(kp=kp, kd=kd),
delay=Delay(delay=wp.array([5, 5, 5], dtype=wp.int32, device="cuda:0"), max_delay=5),
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 — simply omit them:

.. code-block:: python

actuator.step(sim_state, sim_control)

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
Owner Author

@adenzler-nvidia adenzler-nvidia Apr 17, 2026

Choose a reason for hiding this comment

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

📝 Review comment (will ship upstream) — v1

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?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

please double-check for accuracy here.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Verified. Controller.compute in newton/_src/actuators/controllers/base.py:76-90 actually signs as:

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):

The rst example at line 219 still uses the old 2-index shape (input_indices, target_indices). Stale, as originally flagged. Ship block stands.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Confirmation: the verification above stands. Re-posting identical content as v2.

📝 Review comment (will ship upstream) — v2

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,
state, dt, device=None):
# 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
Owner Author

@adenzler-nvidia adenzler-nvidia Apr 17, 2026

Choose a reason for hiding this comment

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

📝 Review comment (will ship upstream) — v1

AGENTS.md asks us to run docs/generate_api.py after adding new public-API symbols, and add the new module to the API Reference toctree. I don't see api/newton_actuators in docs/index.rst's API Reference toctree (further down in this file) or a corresponding docs/api/newton_actuators.rst. This entry adds the concept page but the generated API page is missing — Sphinx cross-refs throughout this PR (:mod:newton.actuators, `:class:`ControllerPD, :meth:Actuator.state``, etc.) won't resolve and the new concept page at docs/concepts/actuators.rst will have broken links. Can we add the generated API page and toctree entry before this merges?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

please reword, make it less verbose. something like don't forget to add to the toctree, and direct the author to the right place

Copy link
Copy Markdown
Owner Author

@adenzler-nvidia adenzler-nvidia Apr 17, 2026

Choose a reason for hiding this comment

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

Shorter version:

📝 Review comment (will ship upstream) — v2

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