Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.

Commit b247a63

Browse files
authored
Add dcmotor and remotized actuators (#5)
* add * fix * 2026 * fix * fix * fix * add parser logic * fix
1 parent 4eab871 commit b247a63

10 files changed

Lines changed: 864 additions & 8 deletions

File tree

README.md

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,16 @@ pip install -e .
3131
| `ActuatorPD` | Stateless PD controller | No | No |
3232
| `ActuatorPID` | PID controller with integral term | Yes | No |
3333
| `ActuatorDelayedPD` | PD controller with input delay | Yes | No |
34+
| `ActuatorDCMotor` | PD with DC motor velocity-dependent saturation | No | No |
35+
| `ActuatorRemotizedPD` | Delayed PD with angle-dependent torque limits | Yes | No |
3436

3537
#### Control Laws
3638

37-
- **ActuatorPD**: `τ = clamp(G·[constant + act + Kp·(target_pos - q) + Kd·(target_vel - G·v)], ±max_force)`
38-
- **ActuatorPID**: `τ = clamp(G·[constant + act + Kp·(target_pos - q) + Ki·∫e·dt + Kd·(target_vel - G·v)], ±max_force)`
39+
- **ActuatorPD**: `τ = clamp(constant + act + Kp·(target_pos - q) + Kd·(target_vel - v), ±max_force)`
40+
- **ActuatorPID**: `τ = clamp(constant + act + Kp·(target_pos - q) + Ki·∫e·dt + Kd·(target_vel - v), ±max_force)`
3941
- **ActuatorDelayedPD**: Same as PD but with delayed targets (circular buffer)
42+
- **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))`
43+
- **ActuatorRemotizedPD**: Same as DelayedPD, but torque limits are interpolated from an angle-dependent lookup table: `τ_limit = interp(q, lookup_table)`
4044

4145
### Base Class Methods
4246

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

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

5863
## Workflow
5964

@@ -79,7 +84,6 @@ pd_actuator = ActuatorPD(
7984
kp=wp.array([100.0, 100.0, 100.0], dtype=wp.float32),
8085
kd=wp.array([10.0, 10.0, 10.0], dtype=wp.float32),
8186
max_force=wp.array([50.0, 50.0, 50.0], dtype=wp.float32),
82-
gear=wp.array([1.0, 1.0, 1.0], dtype=wp.float32),
8387
constant_force=wp.array([0.0, 0.0, 0.0], dtype=wp.float32),
8488
)
8589

@@ -102,7 +106,6 @@ pid_actuator = ActuatorPID(
102106
kd=wp.array([5.0, 5.0], dtype=wp.float32),
103107
max_force=wp.array([50.0, 50.0], dtype=wp.float32),
104108
integral_max=wp.array([10.0, 10.0], dtype=wp.float32),
105-
gear=wp.array([1.0, 1.0], dtype=wp.float32),
106109
constant_force=wp.array([0.0, 0.0], dtype=wp.float32),
107110
)
108111

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

125+
### DC Motor Actuator
126+
127+
```python
128+
import warp as wp
129+
from newton_actuators import ActuatorDCMotor
130+
131+
indices = wp.array([0, 1], dtype=wp.uint32)
132+
dc_motor = ActuatorDCMotor(
133+
input_indices=indices,
134+
output_indices=indices,
135+
kp=wp.array([200.0, 200.0], dtype=wp.float32),
136+
kd=wp.array([20.0, 20.0], dtype=wp.float32),
137+
max_force=wp.array([50.0, 50.0], dtype=wp.float32),
138+
saturation_effort=wp.array([80.0, 80.0], dtype=wp.float32),
139+
velocity_limit=wp.array([10.0, 10.0], dtype=wp.float32),
140+
)
141+
142+
# Stateless - no state management needed
143+
dc_motor.step(sim_state, sim_control, None, None, dt=0.01)
144+
```
145+
122146
## USD Parsing
123147

124148
The library includes utilities for parsing actuator definitions from USD files:

newton_actuators/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717

1818
from ._src.actuators import (
1919
Actuator,
20+
ActuatorDCMotor,
2021
ActuatorDelayedPD,
2122
ActuatorPD,
2223
ActuatorPID,
24+
ActuatorRemotizedPD,
2325
)
2426
from ._src.usd_parser import (
2527
ParsedActuator,
@@ -30,9 +32,11 @@
3032
__all__ = [
3133
"__version__",
3234
"Actuator",
35+
"ActuatorDCMotor",
3336
"ActuatorDelayedPD",
3437
"ActuatorPD",
3538
"ActuatorPID",
39+
"ActuatorRemotizedPD",
3640
"ParsedActuator",
3741
"parse_actuator_prim",
3842
]

newton_actuators/_src/actuators/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,17 @@
1414
# limitations under the License.
1515

1616
from .base import Actuator
17+
from .dc_motor import ActuatorDCMotor
1718
from .delayed_pd import ActuatorDelayedPD
1819
from .pd import ActuatorPD
1920
from .pid import ActuatorPID
21+
from .remotized_pd import ActuatorRemotizedPD
2022

2123
__all__ = [
2224
"Actuator",
25+
"ActuatorDCMotor",
2326
"ActuatorDelayedPD",
2427
"ActuatorPD",
2528
"ActuatorPID",
29+
"ActuatorRemotizedPD",
2630
]
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""DC motor actuator with velocity-dependent torque saturation."""
17+
18+
import math
19+
from typing import Any
20+
21+
import warp as wp
22+
23+
from ..kernels import pd_controller_kernel
24+
from .base import Actuator
25+
26+
27+
class ActuatorDCMotor(Actuator):
28+
"""DC motor actuator with velocity-dependent torque saturation.
29+
30+
Uses the same PD control law as ActuatorPD, but clips torques using the DC motor
31+
torque-speed characteristic instead of a fixed box limit:
32+
33+
τ_max(v) = clamp(τ_sat·(1 - v/v_max), 0, effort_limit)
34+
τ_min(v) = clamp(τ_sat·(-1 - v/v_max), -effort_limit, 0)
35+
τ_applied = clamp(τ_computed, τ_min(v), τ_max(v))
36+
37+
At zero velocity the motor can produce up to ±τ_sat (capped by effort_limit).
38+
As velocity approaches v_max, available torque in the direction of motion drops to zero.
39+
Beyond v_max, no torque can be produced in the direction of motion (back-EMF).
40+
41+
Stateless: no internal memory.
42+
"""
43+
44+
@classmethod
45+
def resolve_arguments(cls, args: dict[str, Any]) -> dict[str, Any]:
46+
"""Resolve arguments with defaults.
47+
48+
Args:
49+
args (dict): User-provided arguments.
50+
51+
Returns:
52+
dict: Arguments with defaults.
53+
54+
Raises:
55+
ValueError: If 'velocity_limit' not provided.
56+
"""
57+
if "velocity_limit" not in args:
58+
raise ValueError("ActuatorDCMotor requires 'velocity_limit' argument")
59+
return {
60+
"kp": args.get("kp", 0.0),
61+
"kd": args.get("kd", 0.0),
62+
"max_force": args.get("max_force", math.inf),
63+
"saturation_effort": args.get("saturation_effort", math.inf),
64+
"velocity_limit": args["velocity_limit"],
65+
"constant_force": args.get("constant_force", 0.0),
66+
}
67+
68+
def __init__(
69+
self,
70+
input_indices: wp.array,
71+
output_indices: wp.array,
72+
kp: wp.array,
73+
kd: wp.array,
74+
max_force: wp.array,
75+
saturation_effort: wp.array,
76+
velocity_limit: wp.array,
77+
constant_force: wp.array = None,
78+
state_pos_attr: str = "joint_q",
79+
state_vel_attr: str = "joint_qd",
80+
control_target_pos_attr: str = "joint_target_pos",
81+
control_target_vel_attr: str = "joint_target_vel",
82+
control_input_attr: str = "joint_act",
83+
control_output_attr: str = "joint_f",
84+
):
85+
"""Initialize DC motor actuator.
86+
87+
Args:
88+
input_indices (wp.array): DOF indices for reading state and targets. Shape (N,).
89+
output_indices (wp.array): DOF indices for writing output. Shape (N,).
90+
kp (wp.array): Proportional gains. Shape (N,).
91+
kd (wp.array): Derivative gains. Shape (N,).
92+
max_force (wp.array): Absolute effort limits (continuous-rated). Shape (N,).
93+
saturation_effort (wp.array): Peak motor torque at stall. Shape (N,).
94+
velocity_limit (wp.array): Maximum joint velocity for torque-speed curve. Shape (N,).
95+
constant_force (wp.array, optional): Constant offsets. Shape (N,). None to skip.
96+
state_pos_attr (str): Attribute on sim_state for positions.
97+
state_vel_attr (str): Attribute on sim_state for velocities.
98+
control_target_pos_attr (str): Attribute on sim_control for target positions.
99+
control_target_vel_attr (str): Attribute on sim_control for target velocities.
100+
control_input_attr (str): Attribute on sim_control for control input. None to skip.
101+
control_output_attr (str): Attribute on sim_control for output forces.
102+
"""
103+
super().__init__(input_indices, output_indices, control_output_attr)
104+
105+
for name, arr in [
106+
("kp", kp),
107+
("kd", kd),
108+
("max_force", max_force),
109+
("saturation_effort", saturation_effort),
110+
("velocity_limit", velocity_limit),
111+
]:
112+
if len(arr) != self.num_actuators:
113+
raise ValueError(f"{name} length ({len(arr)}) must match num_actuators ({self.num_actuators})")
114+
115+
if constant_force is not None and len(constant_force) != self.num_actuators:
116+
raise ValueError(
117+
f"constant_force length ({len(constant_force)}) must match num_actuators ({self.num_actuators})"
118+
)
119+
120+
self.kp = kp
121+
self.kd = kd
122+
self.max_force = max_force
123+
self.saturation_effort = saturation_effort
124+
self.velocity_limit = velocity_limit
125+
self.constant_force = constant_force
126+
127+
self.state_pos_attr = state_pos_attr
128+
self.state_vel_attr = state_vel_attr
129+
self.control_target_pos_attr = control_target_pos_attr
130+
self.control_target_vel_attr = control_target_vel_attr
131+
self.control_input_attr = control_input_attr
132+
133+
def _run_controller(
134+
self,
135+
sim_state: Any,
136+
sim_control: Any,
137+
controller_output: wp.array,
138+
output_indices: wp.array,
139+
current_state: Any,
140+
dt: float,
141+
) -> None:
142+
"""Compute DC motor PD control forces with velocity-dependent saturation."""
143+
control_input = None
144+
if self.control_input_attr is not None:
145+
control_input = getattr(sim_control, self.control_input_attr, None)
146+
147+
wp.launch(
148+
kernel=pd_controller_kernel,
149+
dim=self.num_actuators,
150+
inputs=[
151+
getattr(sim_state, self.state_pos_attr),
152+
getattr(sim_state, self.state_vel_attr),
153+
getattr(sim_control, self.control_target_pos_attr),
154+
getattr(sim_control, self.control_target_vel_attr),
155+
control_input,
156+
self.input_indices,
157+
self.input_indices,
158+
output_indices,
159+
self.kp,
160+
self.kd,
161+
self.max_force,
162+
self.constant_force,
163+
self.saturation_effort,
164+
self.velocity_limit,
165+
None, # lookup_angles (remotized)
166+
None, # lookup_torques (remotized)
167+
0, # lookup_size (remotized)
168+
],
169+
outputs=[controller_output],
170+
)

newton_actuators/_src/actuators/delayed_pd.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,11 @@ def _run_controller(
164164
self.kd,
165165
self.max_force,
166166
self.constant_force,
167+
None, # saturation_effort (DC motor)
168+
None, # velocity_limit (DC motor)
169+
None, # lookup_angles (remotized)
170+
None, # lookup_torques (remotized)
171+
0, # lookup_size (remotized)
167172
],
168173
outputs=[controller_output],
169174
)

newton_actuators/_src/actuators/pd.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,11 @@ def _run_controller(
132132
self.kd,
133133
self.max_force,
134134
self.constant_force,
135+
None, # saturation_effort (DC motor)
136+
None, # velocity_limit (DC motor)
137+
None, # lookup_angles (remotized)
138+
None, # lookup_torques (remotized)
139+
0, # lookup_size (remotized)
135140
],
136141
outputs=[controller_output],
137142
)

0 commit comments

Comments
 (0)