Skip to content

Commit 5c78379

Browse files
Fix Unitree Go1 and Go2 calf actuator limits ignoring the knee reduction (#7564)
# Description Fixes #7479. `UNITREE_GO1_CFG` and `UNITREE_GO2_CFG` apply one set of DC-motor limits to all twelve leg joints, but on both robots the calf sits behind an extra knee reduction. The calf was capped at roughly half its rated torque, while the torque-speed curve kept producing motoring torque up to a no-load speed the hardware cannot reach. The values are not a guess — they are already authored in the USD assets we ship, and the Python configs were discarding them. Dumping the joint drives from the configured asset root: | joint | `go2.usd` `maxForce` | `go2.usd` `maxJointVelocity` | old config | | --- | --- | --- | --- | | hip / thigh | 23.7 N·m | 1724.6 °/s = 30.1 rad/s | 23.5 / 30.0 | | calf | **45.43 N·m** | 899.54 °/s = **15.70 rad/s** | 23.5 / 30.0 | | joint | `go1.usd` `maxForce` | `go1.usd` `maxJointVelocity` | old config | | --- | --- | --- | --- | | hip / thigh | 23.7 N·m | 1724.6 °/s = 30.1 rad/s | 23.7 / 30.0 | | calf | **35.55 N·m** | 1149.35 °/s = **20.06 rad/s** | 23.7 / 30.0 | Both match the official Unitree URDFs exactly ([go2](https://github.com/unitreerobotics/unitree_ros/blob/master/robots/go2_description/urdf/go2_description.urdf), [go1](https://github.com/unitreerobotics/unitree_ros/blob/master/robots/go1_description/urdf/go1.urdf)), giving reductions of 1.9169 and 1.5000. Go1 is affected for the same reason even though it uses an actuator net: `ActuatorNetMLP` subclasses `DCMotor`, so the network output goes through the same envelope. `UNITREE_A1_CFG` was checked and needs no change — `a1.urdf` and `a1.usd` are both flat at 33.5 N·m / 21.0 rad/s, matching the config. ## Why `saturation_effort` had to change The calf needs its own *stall* torque, not just its own effort and velocity limits. `saturation_effort` was the one actuator limit typed as a bare `float`, so expressing this previously required splitting the legs into two actuator groups — which changes the `actuators` dict keys that `go1/rough_env_cfg.py`, `go2/rough_env_cfg.py` and `test_hydra.py` index by name, and adds a second group to iterate every step. The first commit resolves it through `resolve_joint_parameter` like every other limit, so it accepts a joint-name-pattern dict. This is not a new concept in the codebase: `sim/schemas/schemas_actuators.py` already expands dict-shaped `saturation_effort` when authoring `NewtonActuator` prims, so the config type was simply narrower than the machinery behind it. Scalar configurations are unaffected. ## Note on #7562 @sylvesterkaczmarek filed #7479 and opened #7562 for the Go2 half of this. That PR takes the second-actuator-group route and derives the calf values from the reduction ratio (45.05 / 15.65) rather than reading them off the asset. This PR covers Go1 as well, keeps a single actuator group, and uses the values already in the USD. Happy to defer if maintainers prefer the other shape. ## Type of change - Bug fix (non-breaking change which fixes an issue) - New feature (non-breaking change which adds functionality) Simulated Go1 and Go2 behavior changes: the calf now produces up to 1.92x more torque and stops motoring at roughly half the previous speed. Policies trained against the old configuration should be retrained rather than reused. This is called out in the `isaaclab_assets` changelog fragment. ## Tests - `source/isaaclab/test/actuators/test_dc_motor.py` gains `test_dc_motor_clip_with_per_joint_saturation_effort`, which checks that a dict-configured two-joint group reproduces, column by column, what a scalar-configured actuator produces for the same joint. Verified failing before the fix (`TypeError: unsupported operand type(s) for /: 'Tensor' and 'dict'`) and passing after. - `source/isaaclab/test/actuators/test_dc_motor.py` — 170 passed. - `source/isaaclab/test/actuators/test_actuator_collection.py` — 30 passed. - `source/isaaclab_assets/test/test_valid_configs.py -k cpu` — 1 passed (spawns every registered asset, including both Go configs). - Confirmed the patterns resolve against the real joint names: all four calf joints get 45.43 / 15.70 (Go2) and 35.55 / 20.06 (Go1); hips and thighs get 23.7 / 30.1. No config-literal assertion test was added for `unitree.py` — it would restate the constants without checking behavior. The behavioral contract is covered by the DC-motor test above. ## Release backport - [x] <!-- backport-active-release --> Backport this pull request to the active release branch after it merges into `develop` ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have added a changelog fragment under `source/<pkg>/changelog.d/` for every touched package - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there
1 parent ec58631 commit 5c78379

7 files changed

Lines changed: 74 additions & 10 deletions

File tree

docs/source/concepts/actuators.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,9 @@ estimates effort telemetry from the current state when the backend does not expo
227227
torque limit at :math:`\pm\,\tau_{max}`.
228228

229229
**DCMotor.** Adds a linear four-quadrant torque-speed curve. ``saturation_effort`` is the stall
230-
torque, and ``actuator_velocity_limit`` is the no-load speed.
230+
torque, and ``actuator_velocity_limit`` is the no-load speed. Both accept a joint-name-pattern
231+
dictionary, so joints behind different gear reductions can share one group and still get their own
232+
curve — for example a quadruped whose knee sits behind an extra reduction relative to its hip.
231233

232234
**DelayedPDActuator.** An ideal PD controller with delayed position, velocity, and effort commands.
233235
The delay is sampled uniformly from ``[min_delay, max_delay]`` at reset.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Changed
2+
^^^^^^^
3+
4+
* Changed :attr:`~isaaclab.actuators.DCMotorCfg.saturation_effort` to accept a joint-name-pattern
5+
dictionary in addition to a scalar. Joints in one actuator group that sit behind different gear
6+
reductions can now be given their own stall torque, which previously required splitting them into
7+
separate actuator groups. Existing scalar configurations are unaffected.

source/isaaclab/isaaclab/actuators/actuator_pd.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,9 @@ def __init__(self, cfg: DCMotorCfg, *args, **kwargs):
405405
# parse configuration
406406
if self.cfg.saturation_effort is None:
407407
raise ValueError("The saturation_effort must be provided for the DC motor actuator model.")
408-
self._saturation_effort = self.cfg.saturation_effort
408+
self._saturation_effort = resolve_joint_parameter(
409+
self.cfg.saturation_effort, None, self._joint_names, self._num_envs, self._device
410+
)
409411
# check that quantities are provided
410412
if self.cfg.actuator_velocity_limit is None:
411413
raise ValueError("The velocity limit must be provided for the DC motor actuator model.")

source/isaaclab/isaaclab/actuators/actuator_pd_cfg.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,13 @@ class DCMotorCfg(IdealPDActuatorCfg):
4747

4848
class_type: type["DCMotor"] | str = "{DIR}.actuator_pd:DCMotor"
4949

50-
saturation_effort: float = MISSING
51-
"""Peak motor force/torque of the electric DC motor (in N-m)."""
50+
saturation_effort: dict[str, float] | float = MISSING
51+
"""Peak motor force/torque of the electric DC motor [N or N·m, depending on joint type].
52+
53+
The motor's stall torque reflected at the joint, i.e. the torque produced at zero speed.
54+
Joints in the same group that sit behind different gear reductions need different values,
55+
so this accepts a joint-name-pattern dictionary as well as a scalar.
56+
"""
5257

5358

5459
@configclass

source/isaaclab/test/actuators/test_dc_motor.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,36 @@ def test_dc_motor_clip(num_envs, num_joints, device, test_point):
183183
expected_clipped_effort[test_point] * torch.ones(num_envs, num_joints, device=device),
184184
clipped_effort,
185185
)
186+
187+
188+
@pytest.mark.parametrize("device", ["cuda:0", "cpu"])
189+
def test_dc_motor_clip_with_per_joint_saturation_effort(device):
190+
"""Test that a per-joint ``saturation_effort`` gives each joint its own torque-speed curve.
191+
192+
Joints behind different gear reductions belong to one actuator group but do not share a stall
193+
torque, e.g. the Unitree Go2 calf, which sits behind an extra knee reduction.
194+
"""
195+
joint_names = ["hip", "calf"]
196+
actuator_cfg = DCMotorCfg(
197+
joint_names_expr=joint_names,
198+
stiffness=200.0,
199+
damping=10.0,
200+
actuator_effort_limit=100.0,
201+
actuator_velocity_limit=50.0,
202+
saturation_effort={"hip": 100.0, "calf": 190.0},
203+
)
204+
actuator = actuator_cfg.class_type(
205+
actuator_cfg,
206+
joint_names=joint_names,
207+
joint_ids=[0, 1],
208+
num_envs=1,
209+
device=device,
210+
stiffness=actuator_cfg.stiffness,
211+
damping=actuator_cfg.damping,
212+
)
213+
214+
# at half the no-load speed each joint delivers half of its own stall torque, and the shared
215+
# effort limit is high enough to clip neither
216+
actuator._joint_vel[:] = 25.0
217+
clipped_effort = actuator._clip_effort(torch.full((1, 2), 500.0, device=device))
218+
torch.testing.assert_close(clipped_effort, torch.tensor([[50.0, 95.0]], device=device))
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Fixed
2+
^^^^^
3+
4+
* Fixed the Unitree Go1 and Go2 leg actuator limits ignoring the knee reduction. Both robots applied
5+
the hip and thigh limits to the calf joints, which capped calf torque well below its rated value and
6+
let the torque-speed curve keep motoring past its rated speed. The calf joints now use the limits
7+
authored in ``go1.usd`` and ``go2.usd`` (Go1: 35.55 N·m, 20.06 rad/s; Go2: 45.43 N·m, 15.70 rad/s),
8+
and the hip and thigh limits were aligned with the same assets (23.7 N·m, 30.1 rad/s).
9+
10+
These robots now produce more calf torque at lower calf speeds, so policies trained on the previous
11+
configuration should be retrained rather than reused directly.

source/isaaclab_assets/isaaclab_assets/robots/unitree.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@
3939
torque_scale=1.0,
4040
input_order="pos_vel",
4141
input_idx=[0, 1, 2],
42-
actuator_effort_limit=23.7, # taken from spec sheet
43-
actuator_velocity_limit=30.0, # taken from spec sheet
44-
saturation_effort=23.7, # same as effort limit
42+
# the calf sits behind an extra 1.5:1 knee reduction, so its joint-side torque is higher
43+
# and its joint-side speed lower than the hip and thigh. Values match ``go1.usd``.
44+
actuator_effort_limit={".*_hip_joint": 23.7, ".*_thigh_joint": 23.7, ".*_calf_joint": 35.55},
45+
actuator_velocity_limit={".*_hip_joint": 30.1, ".*_thigh_joint": 30.1, ".*_calf_joint": 20.06},
46+
saturation_effort={".*_hip_joint": 23.7, ".*_thigh_joint": 23.7, ".*_calf_joint": 35.55},
4547
)
4648
"""Configuration of Go1 actuators using MLP model.
4749
@@ -171,9 +173,11 @@
171173
actuators={
172174
"base_legs": DCMotorCfg(
173175
joint_names_expr=[".*_hip_joint", ".*_thigh_joint", ".*_calf_joint"],
174-
actuator_effort_limit=23.5,
175-
saturation_effort=23.5,
176-
actuator_velocity_limit=30.0,
176+
# the calf sits behind an extra 1.92:1 knee reduction, so its joint-side torque is
177+
# higher and its joint-side speed lower than the hip and thigh. Values match ``go2.usd``.
178+
actuator_effort_limit={".*_hip_joint": 23.7, ".*_thigh_joint": 23.7, ".*_calf_joint": 45.43},
179+
saturation_effort={".*_hip_joint": 23.7, ".*_thigh_joint": 23.7, ".*_calf_joint": 45.43},
180+
actuator_velocity_limit={".*_hip_joint": 30.1, ".*_thigh_joint": 30.1, ".*_calf_joint": 15.70},
177181
stiffness=25.0,
178182
damping=0.5,
179183
friction=0.0,

0 commit comments

Comments
 (0)