Skip to content

Commit fddee2b

Browse files
authored
Adds controllers module and ControllerJointImpendance implementation. (#3598)
1 parent 4d64341 commit fddee2b

17 files changed

Lines changed: 1916 additions & 1 deletion

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- Add contact examples for Newton's cradle, a balance bird, and a domino spiral
1616
- Document geometry-pair contact behavior and clarify that MuJoCo Warp currently produces a single contact for cylinder--box pairs even with MultiCCD enabled.
1717
- Add `ViewerUSD(points_as_spheres=...)` to render `log_points` particles as a `UsdGeom.PointInstancer` of sphere prototypes; enabled by default (opt out with `points_as_spheres=False` for flat `UsdGeom.Points` splats)
18+
- Add experimental `newton.controllers` module with `ControllerBase` base class, `ControllerJointImpedance`, and `ControllerJointImpedanceModelFree` for GPU-accelerated, vectorized joint-space impedance control.
1819
- Add list-of-pattern and explicit-index selectors to `ArticulationView`.
1920
- Add `newton[onnx]` for ONNX policy inference through Warp-NN; `ControllerNeuralMLP`, `ControllerNeuralLSTM`, and RL policy examples can run exported `.onnx` policies without requiring PyTorch for ONNX execution.
2021
- Add three VBD contact examples — `vbd_rigid_rigid_contact`, `vbd_soft_rigid_contact`, and `vbd_soft_rigid_mix_contact` — demonstrating rigid-rigid, soft (particle-rigid), and mixed cloth-bag contacts

docs/api/_toctree.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
api/newton
1010
api/newton_actuators
11+
api/newton_controllers
1112
api/newton_geometry
1213
api/newton_ik
1314
api/newton_math

docs/api/newton.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ newton
1010
.. rubric:: Submodules
1111

1212
- :doc:`newton.actuators <newton_actuators>`
13+
- :doc:`newton.controllers <newton_controllers>`
1314
- :doc:`newton.geometry <newton_geometry>`
1415
- :doc:`newton.ik <newton_ik>`
1516
- :doc:`newton.math <newton_math>`

docs/api/newton_controllers.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
.. SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
2+
.. SPDX-License-Identifier: CC-BY-4.0
3+
4+
newton.controllers
5+
==================
6+
7+
GPU-accelerated, vectorized control laws.
8+
9+
This module provides standalone controllers that compute signals
10+
for the robot to track. Each controller is a concrete
11+
subclass of :class:`ControllerBase`.
12+
13+
.. experimental::
14+
15+
.. py:module:: newton.controllers
16+
.. currentmodule:: newton.controllers
17+
18+
.. rubric:: Classes
19+
20+
.. autosummary::
21+
:toctree: _generated
22+
:nosignatures:
23+
24+
ControllerBase
25+
ControllerJointImpedance
26+
ControllerJointImpedanceModelFree

newton/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,11 @@
121121
# ==================================================================================
122122
# submodule APIs
123123
# ==================================================================================
124-
from . import actuators, geometry, ik, math, selection, sensors, solvers, usd, utils, viewer # noqa: E402
124+
from . import actuators, controllers, geometry, ik, math, selection, sensors, solvers, usd, utils, viewer # noqa: E402
125125

126126
__all__ += [
127127
"actuators",
128+
"controllers",
128129
"geometry",
129130
"ik",
130131
"math",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from .controller import ControllerBase
5+
from .impl import ControllerJointImpedance, ControllerJointImpedanceModelFree
6+
7+
__all__ = [
8+
"ControllerBase",
9+
"ControllerJointImpedance",
10+
"ControllerJointImpedanceModelFree",
11+
]
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Abstract base for Newton controllers."""
5+
6+
from __future__ import annotations
7+
8+
from abc import ABC, abstractmethod
9+
from typing import Generic, TypeVar
10+
11+
import warp as wp
12+
13+
InputT = TypeVar("InputT")
14+
OutputT = TypeVar("OutputT")
15+
16+
17+
class ControllerBase(ABC, Generic[InputT, OutputT]):
18+
"""Abstract interface for a single Newton control law.
19+
20+
Every concrete control law (joint impedance, differential IK, …) subclasses
21+
:class:`ControllerBase` directly. There is no framework-level composition: users
22+
who want to combine multiple control laws call each one's :meth:`step`
23+
in sequence themselves.
24+
25+
Subclasses are responsible for:
26+
27+
- :meth:`is_graphable`: predicate the user can query to decide whether
28+
graph capture is possible.
29+
- :meth:`input`, :meth:`output`: allocate fresh typed input/output structs.
30+
Baked-in arrays (gains passed as a ``wp.array`` at construction) are
31+
stored on the controller and do **not** appear on the input struct.
32+
- :meth:`step`: read the input struct's live arrays, run kernels, write
33+
the output struct's live arrays. Writes are slot-replacing (``=``, not
34+
``+=``); composing laws is the user's job.
35+
"""
36+
37+
@abstractmethod
38+
def is_graphable(self) -> bool:
39+
"""Whether :meth:`step` is safe to capture in a graph."""
40+
41+
@abstractmethod
42+
def input(self) -> InputT:
43+
"""Allocate a fresh input struct with zero-initialised arrays.
44+
45+
The user typically reassigns fields to point at live data buffers.
46+
Fields for disabled features are ``None``.
47+
"""
48+
49+
@abstractmethod
50+
def output(self) -> OutputT:
51+
"""Allocate a fresh output struct."""
52+
53+
@abstractmethod
54+
def step(
55+
self,
56+
*,
57+
inputs: InputT,
58+
outputs: OutputT,
59+
dt: float | wp.array[wp.float32],
60+
) -> None:
61+
"""Run one control step.
62+
63+
Args:
64+
inputs: Populated input struct (see :meth:`input`).
65+
outputs: Output struct to write into (see :meth:`output`).
66+
dt: Step duration [s].
67+
"""
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from .joint_impedance import ControllerJointImpedance, ControllerJointImpedanceModelFree
5+
6+
__all__ = [
7+
"ControllerJointImpedance",
8+
"ControllerJointImpedanceModelFree",
9+
]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from .model_based import ControllerJointImpedance
5+
from .model_free import ControllerJointImpedanceModelFree
6+
7+
__all__ = [
8+
"ControllerJointImpedance",
9+
"ControllerJointImpedanceModelFree",
10+
]
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Shared Warp kernels for :class:`~newton.controllers.ControllerJointImpedance`."""
5+
6+
import numpy as np
7+
import warp as wp
8+
9+
10+
def _idx_max(idx: wp.array[wp.uint32]) -> int:
11+
"""Return the minimum flat-array size needed to hold all indices."""
12+
return int(np.max(idx.numpy())) + 1
13+
14+
15+
@wp.kernel
16+
def _pd_term_kernel(
17+
joint_q: wp.array2d[wp.float32], # (robot_count, max_dofs)
18+
joint_qd: wp.array2d[wp.float32], # (robot_count, max_dofs)
19+
joint_q_des: wp.array2d[wp.float32], # (robot_count, max_dofs)
20+
joint_qd_des: wp.array2d[wp.float32], # (robot_count, max_dofs)
21+
stiffness: wp.array2d[wp.float32], # (robot_count, max_dofs)
22+
damping: wp.array2d[wp.float32], # (robot_count, max_dofs)
23+
dofs_per_robot: wp.array[wp.int32], # (robot_count,)
24+
out: wp.array2d[wp.float32], # (robot_count, max_dofs)
25+
):
26+
robot, dof = wp.tid()
27+
if dof >= dofs_per_robot[robot]:
28+
return
29+
out[robot, dof] = stiffness[robot, dof] * (joint_q_des[robot, dof] - joint_q[robot, dof]) + damping[robot, dof] * (
30+
joint_qd_des[robot, dof] - joint_qd[robot, dof]
31+
)
32+
33+
34+
@wp.kernel
35+
def _add_term_kernel(
36+
term: wp.array2d[wp.float32], # (robot_count, max_dofs)
37+
dofs_per_robot: wp.array[wp.int32], # (robot_count,)
38+
tau: wp.array2d[wp.float32], # (robot_count, max_dofs)
39+
):
40+
robot, dof = wp.tid()
41+
if dof >= dofs_per_robot[robot]:
42+
return
43+
tau[robot, dof] = tau[robot, dof] + term[robot, dof]
44+
45+
46+
@wp.kernel
47+
def _mass_matrix_multiply_kernel(
48+
M: wp.array3d[wp.float32], # (robot_count, max_dofs, max_dofs)
49+
vec: wp.array2d[wp.float32], # (robot_count, max_dofs)
50+
dofs_per_robot: wp.array[wp.int32], # (robot_count,)
51+
out: wp.array2d[wp.float32], # (robot_count, max_dofs)
52+
):
53+
robot, dof = wp.tid()
54+
if dof >= dofs_per_robot[robot]:
55+
return
56+
acc = float(0.0)
57+
for col in range(dofs_per_robot[robot]):
58+
acc = acc + M[robot, dof, col] * vec[robot, col]
59+
out[robot, dof] = acc
60+
61+
62+
@wp.kernel
63+
def _gather_dof_flat_kernel(
64+
src: wp.array[wp.float32], # flat sim array
65+
indices: wp.array[wp.uint32], # (total_dofs,) — concatenated per-robot, no padding
66+
dst: wp.array[wp.float32], # flat output (total_dofs,)
67+
):
68+
flat = wp.tid()
69+
dst[flat] = src[indices[flat]]
70+
71+
72+
@wp.kernel
73+
def _gather_dof_kernel(
74+
src: wp.array[wp.float32], # flat sim array
75+
dof_indices: wp.array[wp.uint32], # (total_dofs,) — concatenated per-robot indices
76+
dof_offsets: wp.array[wp.int32], # (robot_count,) — start of each robot in dof_indices
77+
dofs_per_robot: wp.array[wp.int32], # (robot_count,)
78+
dst: wp.array2d[wp.float32], # (robot_count, max_dofs)
79+
):
80+
robot, dof = wp.tid()
81+
if dof >= dofs_per_robot[robot]:
82+
return
83+
dst[robot, dof] = src[dof_indices[dof_offsets[robot] + dof]]
84+
85+
86+
@wp.kernel
87+
def _scatter_dof_kernel(
88+
src: wp.array2d[wp.float32], # (robot_count, max_dofs)
89+
dof_indices: wp.array[wp.uint32], # (total_dofs,) — concatenated per-robot indices
90+
dof_offsets: wp.array[wp.int32], # (robot_count,) — start of each robot in dof_indices
91+
dofs_per_robot: wp.array[wp.int32], # (robot_count,)
92+
dst: wp.array[wp.float32], # flat sim output
93+
):
94+
robot, dof = wp.tid()
95+
if dof >= dofs_per_robot[robot]:
96+
return
97+
dst[dof_indices[dof_offsets[robot] + dof]] = src[robot, dof]

0 commit comments

Comments
 (0)