Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 7 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
32 changes: 28 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,16 @@ pip install -e .
| `ActuatorPD` | Stateless PD controller | No | No |
| `ActuatorPID` | PID controller with integral term | Yes | No |
| `ActuatorDelayedPD` | PD controller with input delay | Yes | No |
| `ActuatorDCMotor` | PD with DC motor velocity-dependent saturation | No | No |
| `ActuatorRemotizedPD` | Delayed PD with angle-dependent torque limits | Yes | No |

#### Control Laws

- **ActuatorPD**: `τ = clamp(G·[constant + act + Kp·(target_pos - q) + Kd·(target_vel - G·v)], ±max_force)`
- **ActuatorPID**: `τ = clamp(G·[constant + act + Kp·(target_pos - q) + Ki·∫e·dt + Kd·(target_vel - G·v)], ±max_force)`
- **ActuatorPD**: `τ = clamp(constant + act + Kp·(target_pos - q) + Kd·(target_vel - v), ±max_force)`
- **ActuatorPID**: `τ = clamp(constant + act + Kp·(target_pos - q) + Ki·∫e·dt + Kd·(target_vel - v), ±max_force)`
- **ActuatorDelayedPD**: Same as PD but with delayed targets (circular buffer)
- **ActuatorDCMotor**: Same PD force computation, but torque is clamped to velocity-dependent bounds from the motor torque-speed curve: `τ_max(v) = clamp(τ_sat·(1 - v/v_max), 0, effort_limit)`, `τ_min(v) = clamp(τ_sat·(-1 - v/v_max), -effort_limit, 0)`, `τ = clamp(τ, τ_min(v), τ_max(v))`
- **ActuatorRemotizedPD**: Same as DelayedPD, but torque limits are interpolated from an angle-dependent lookup table: `τ_limit = interp(q, lookup_table)`

### Base Class Methods

Expand All @@ -54,6 +58,7 @@ Stateful actuators use nested State classes:

- `ActuatorPID.State` - Contains the integral term for PID control
- `ActuatorDelayedPD.State` - Contains circular buffers for delayed targets
- `ActuatorRemotizedPD.State` - Inherits `ActuatorDelayedPD.State` (same delay buffers)

## Workflow

Expand All @@ -79,7 +84,6 @@ pd_actuator = ActuatorPD(
kp=wp.array([100.0, 100.0, 100.0], dtype=wp.float32),
kd=wp.array([10.0, 10.0, 10.0], dtype=wp.float32),
max_force=wp.array([50.0, 50.0, 50.0], dtype=wp.float32),
gear=wp.array([1.0, 1.0, 1.0], dtype=wp.float32),
constant_force=wp.array([0.0, 0.0, 0.0], dtype=wp.float32),
)

Expand All @@ -102,7 +106,6 @@ pid_actuator = ActuatorPID(
kd=wp.array([5.0, 5.0], dtype=wp.float32),
max_force=wp.array([50.0, 50.0], dtype=wp.float32),
integral_max=wp.array([10.0, 10.0], dtype=wp.float32),
gear=wp.array([1.0, 1.0], dtype=wp.float32),
constant_force=wp.array([0.0, 0.0], dtype=wp.float32),
)

Expand All @@ -119,6 +122,27 @@ for step in range(num_steps):
current_state, next_state = next_state, current_state # Swap buffers
```

### DC Motor Actuator

```python
import warp as wp
from newton_actuators import ActuatorDCMotor

indices = wp.array([0, 1], dtype=wp.uint32)
dc_motor = ActuatorDCMotor(
input_indices=indices,
output_indices=indices,
kp=wp.array([200.0, 200.0], dtype=wp.float32),
kd=wp.array([20.0, 20.0], dtype=wp.float32),
max_force=wp.array([50.0, 50.0], dtype=wp.float32),
saturation_effort=wp.array([80.0, 80.0], dtype=wp.float32),
velocity_limit=wp.array([10.0, 10.0], dtype=wp.float32),
)

# Stateless - no state management needed
dc_motor.step(sim_state, sim_control, None, None, dt=0.01)
```

## USD Parsing

The library includes utilities for parsing actuator definitions from USD files:
Expand Down
4 changes: 4 additions & 0 deletions newton_actuators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@

from ._src.actuators import (
Actuator,
ActuatorDCMotor,
ActuatorDelayedPD,
ActuatorPD,
ActuatorPID,
ActuatorRemotizedPD,
)
from ._src.usd_parser import (
ParsedActuator,
Expand All @@ -30,9 +32,11 @@
__all__ = [
"__version__",
"Actuator",
"ActuatorDCMotor",
"ActuatorDelayedPD",
"ActuatorPD",
"ActuatorPID",
"ActuatorRemotizedPD",
"ParsedActuator",
"parse_actuator_prim",
]
4 changes: 4 additions & 0 deletions newton_actuators/_src/actuators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@
# limitations under the License.

from .base import Actuator
from .dc_motor import ActuatorDCMotor
from .delayed_pd import ActuatorDelayedPD
from .pd import ActuatorPD
from .pid import ActuatorPID
from .remotized_pd import ActuatorRemotizedPD

__all__ = [
"Actuator",
"ActuatorDCMotor",
"ActuatorDelayedPD",
"ActuatorPD",
"ActuatorPID",
"ActuatorRemotizedPD",
]
170 changes: 170 additions & 0 deletions newton_actuators/_src/actuators/dc_motor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""DC motor actuator with velocity-dependent torque saturation."""

import math
from typing import Any

import warp as wp

from ..kernels import pd_controller_kernel
from .base import Actuator


class ActuatorDCMotor(Actuator):
"""DC motor actuator with velocity-dependent torque saturation.

Uses the same PD control law as ActuatorPD, but clips torques using the DC motor
torque-speed characteristic instead of a fixed box limit:

τ_max(v) = clamp(τ_sat·(1 - v/v_max), 0, effort_limit)
τ_min(v) = clamp(τ_sat·(-1 - v/v_max), -effort_limit, 0)
τ_applied = clamp(τ_computed, τ_min(v), τ_max(v))

At zero velocity the motor can produce up to ±τ_sat (capped by effort_limit).
As velocity approaches v_max, available torque in the direction of motion drops to zero.
Beyond v_max, no torque can be produced in the direction of motion (back-EMF).

Stateless: no internal memory.
"""

@classmethod
def resolve_arguments(cls, args: dict[str, Any]) -> dict[str, Any]:
"""Resolve arguments with defaults.

Args:
args (dict): User-provided arguments.

Returns:
dict: Arguments with defaults.

Raises:
ValueError: If 'velocity_limit' not provided.
"""
if "velocity_limit" not in args:
raise ValueError("ActuatorDCMotor requires 'velocity_limit' argument")
return {
"kp": args.get("kp", 0.0),
"kd": args.get("kd", 0.0),
"max_force": args.get("max_force", math.inf),
"saturation_effort": args.get("saturation_effort", math.inf),
"velocity_limit": args["velocity_limit"],
"constant_force": args.get("constant_force", 0.0),
}

def __init__(
self,
input_indices: wp.array,
output_indices: wp.array,
kp: wp.array,
kd: wp.array,
max_force: wp.array,
saturation_effort: wp.array,
velocity_limit: wp.array,
constant_force: wp.array = None,
state_pos_attr: str = "joint_q",
state_vel_attr: str = "joint_qd",
control_target_pos_attr: str = "joint_target_pos",
control_target_vel_attr: str = "joint_target_vel",
control_input_attr: str = "joint_act",
control_output_attr: str = "joint_f",
):
"""Initialize DC motor actuator.

Args:
input_indices (wp.array): DOF indices for reading state and targets. Shape (N,).
output_indices (wp.array): DOF indices for writing output. Shape (N,).
kp (wp.array): Proportional gains. Shape (N,).
kd (wp.array): Derivative gains. Shape (N,).
max_force (wp.array): Absolute effort limits (continuous-rated). Shape (N,).
saturation_effort (wp.array): Peak motor torque at stall. Shape (N,).
velocity_limit (wp.array): Maximum joint velocity for torque-speed curve. Shape (N,).
constant_force (wp.array, optional): Constant offsets. Shape (N,). None to skip.
state_pos_attr (str): Attribute on sim_state for positions.
state_vel_attr (str): Attribute on sim_state for velocities.
control_target_pos_attr (str): Attribute on sim_control for target positions.
control_target_vel_attr (str): Attribute on sim_control for target velocities.
control_input_attr (str): Attribute on sim_control for control input. None to skip.
control_output_attr (str): Attribute on sim_control for output forces.
"""
super().__init__(input_indices, output_indices, control_output_attr)

for name, arr in [
("kp", kp),
("kd", kd),
("max_force", max_force),
("saturation_effort", saturation_effort),
("velocity_limit", velocity_limit),
]:
if len(arr) != self.num_actuators:
raise ValueError(f"{name} length ({len(arr)}) must match num_actuators ({self.num_actuators})")

if constant_force is not None and len(constant_force) != self.num_actuators:
raise ValueError(
f"constant_force length ({len(constant_force)}) must match num_actuators ({self.num_actuators})"
)

self.kp = kp
self.kd = kd
self.max_force = max_force
self.saturation_effort = saturation_effort
self.velocity_limit = velocity_limit
self.constant_force = constant_force

self.state_pos_attr = state_pos_attr
self.state_vel_attr = state_vel_attr
self.control_target_pos_attr = control_target_pos_attr
self.control_target_vel_attr = control_target_vel_attr
self.control_input_attr = control_input_attr

def _run_controller(
self,
sim_state: Any,
sim_control: Any,
controller_output: wp.array,
output_indices: wp.array,
current_state: Any,
dt: float,
) -> None:
"""Compute DC motor PD control forces with velocity-dependent saturation."""
control_input = None
if self.control_input_attr is not None:
control_input = getattr(sim_control, self.control_input_attr, None)

wp.launch(
kernel=pd_controller_kernel,
dim=self.num_actuators,
inputs=[
getattr(sim_state, self.state_pos_attr),
getattr(sim_state, self.state_vel_attr),
getattr(sim_control, self.control_target_pos_attr),
getattr(sim_control, self.control_target_vel_attr),
control_input,
self.input_indices,
self.input_indices,
output_indices,
self.kp,
self.kd,
self.max_force,
self.constant_force,
self.saturation_effort,
self.velocity_limit,
None, # lookup_angles (remotized)
None, # lookup_torques (remotized)
0, # lookup_size (remotized)
],
outputs=[controller_output],
)
5 changes: 5 additions & 0 deletions newton_actuators/_src/actuators/delayed_pd.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ def _run_controller(
self.kd,
self.max_force,
self.constant_force,
None, # saturation_effort (DC motor)
None, # velocity_limit (DC motor)
None, # lookup_angles (remotized)
None, # lookup_torques (remotized)
0, # lookup_size (remotized)
],
outputs=[controller_output],
)
Expand Down
5 changes: 5 additions & 0 deletions newton_actuators/_src/actuators/pd.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ def _run_controller(
self.kd,
self.max_force,
self.constant_force,
None, # saturation_effort (DC motor)
None, # velocity_limit (DC motor)
None, # lookup_angles (remotized)
None, # lookup_torques (remotized)
0, # lookup_size (remotized)
],
outputs=[controller_output],
)
Loading