From 52fddebb7d14020c79cc7a2e346b7ffba05ccdba Mon Sep 17 00:00:00 2001 From: Antoine Richard Date: Fri, 4 Sep 2026 09:52:58 +0200 Subject: [PATCH 1/2] Allow per-joint saturation_effort in DCMotorCfg The DC motor torque-speed curve is anchored on the stall torque, but saturation_effort was the only actuator limit that could not vary across the joints of a group. Joints behind different gear reductions therefore had to be split into separate actuator groups to get correct curves. Resolve it through resolve_joint_parameter like the other limits, so it accepts a joint-name-pattern dictionary. The USD authoring path in schemas_actuators already expanded dict-shaped saturation_effort, so this brings the config type in line with the rest of the stack. --- docs/source/concepts/actuators.rst | 4 ++- .../fix-unitree-knee-reduction.minor.rst | 7 ++++ .../isaaclab/actuators/actuator_pd.py | 4 ++- .../isaaclab/actuators/actuator_pd_cfg.py | 9 +++-- .../isaaclab/test/actuators/test_dc_motor.py | 33 +++++++++++++++++++ 5 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 source/isaaclab/changelog.d/fix-unitree-knee-reduction.minor.rst diff --git a/docs/source/concepts/actuators.rst b/docs/source/concepts/actuators.rst index fb8ddfd7f186..4e54e04a2af2 100644 --- a/docs/source/concepts/actuators.rst +++ b/docs/source/concepts/actuators.rst @@ -227,7 +227,9 @@ estimates effort telemetry from the current state when the backend does not expo torque limit at :math:`\pm\,\tau_{max}`. **DCMotor.** Adds a linear four-quadrant torque-speed curve. ``saturation_effort`` is the stall -torque, and ``actuator_velocity_limit`` is the no-load speed. +torque, and ``actuator_velocity_limit`` is the no-load speed. Both accept a joint-name-pattern +dictionary, so joints behind different gear reductions can share one group and still get their own +curve — for example a quadruped whose knee sits behind an extra reduction relative to its hip. **DelayedPDActuator.** An ideal PD controller with delayed position, velocity, and effort commands. The delay is sampled uniformly from ``[min_delay, max_delay]`` at reset. diff --git a/source/isaaclab/changelog.d/fix-unitree-knee-reduction.minor.rst b/source/isaaclab/changelog.d/fix-unitree-knee-reduction.minor.rst new file mode 100644 index 000000000000..580614b3d1f3 --- /dev/null +++ b/source/isaaclab/changelog.d/fix-unitree-knee-reduction.minor.rst @@ -0,0 +1,7 @@ +Changed +^^^^^^^ + +* Changed :attr:`~isaaclab.actuators.DCMotorCfg.saturation_effort` to accept a joint-name-pattern + dictionary in addition to a scalar. Joints in one actuator group that sit behind different gear + reductions can now be given their own stall torque, which previously required splitting them into + separate actuator groups. Existing scalar configurations are unaffected. diff --git a/source/isaaclab/isaaclab/actuators/actuator_pd.py b/source/isaaclab/isaaclab/actuators/actuator_pd.py index 9106bea85fcd..744cfd9812e4 100644 --- a/source/isaaclab/isaaclab/actuators/actuator_pd.py +++ b/source/isaaclab/isaaclab/actuators/actuator_pd.py @@ -405,7 +405,9 @@ def __init__(self, cfg: DCMotorCfg, *args, **kwargs): # parse configuration if self.cfg.saturation_effort is None: raise ValueError("The saturation_effort must be provided for the DC motor actuator model.") - self._saturation_effort = self.cfg.saturation_effort + self._saturation_effort = resolve_joint_parameter( + self.cfg.saturation_effort, None, self._joint_names, self._num_envs, self._device + ) # check that quantities are provided if self.cfg.actuator_velocity_limit is None: raise ValueError("The velocity limit must be provided for the DC motor actuator model.") diff --git a/source/isaaclab/isaaclab/actuators/actuator_pd_cfg.py b/source/isaaclab/isaaclab/actuators/actuator_pd_cfg.py index c4cad3e69f41..e21a26e6a6d1 100644 --- a/source/isaaclab/isaaclab/actuators/actuator_pd_cfg.py +++ b/source/isaaclab/isaaclab/actuators/actuator_pd_cfg.py @@ -47,8 +47,13 @@ class DCMotorCfg(IdealPDActuatorCfg): class_type: type["DCMotor"] | str = "{DIR}.actuator_pd:DCMotor" - saturation_effort: float = MISSING - """Peak motor force/torque of the electric DC motor (in N-m).""" + saturation_effort: dict[str, float] | float = MISSING + """Peak motor force/torque of the electric DC motor [N or N·m, depending on joint type]. + + The motor's stall torque reflected at the joint, i.e. the torque produced at zero speed. + Joints in the same group that sit behind different gear reductions need different values, + so this accepts a joint-name-pattern dictionary as well as a scalar. + """ @configclass diff --git a/source/isaaclab/test/actuators/test_dc_motor.py b/source/isaaclab/test/actuators/test_dc_motor.py index a1cc2a620d41..4af94d49b678 100644 --- a/source/isaaclab/test/actuators/test_dc_motor.py +++ b/source/isaaclab/test/actuators/test_dc_motor.py @@ -183,3 +183,36 @@ def test_dc_motor_clip(num_envs, num_joints, device, test_point): expected_clipped_effort[test_point] * torch.ones(num_envs, num_joints, device=device), clipped_effort, ) + + +@pytest.mark.parametrize("device", ["cuda:0", "cpu"]) +def test_dc_motor_clip_with_per_joint_saturation_effort(device): + """Test that a per-joint ``saturation_effort`` gives each joint its own torque-speed curve. + + Joints behind different gear reductions belong to one actuator group but do not share a stall + torque, e.g. the Unitree Go2 calf, which sits behind an extra knee reduction. + """ + joint_names = ["hip", "calf"] + actuator_cfg = DCMotorCfg( + joint_names_expr=joint_names, + stiffness=200.0, + damping=10.0, + actuator_effort_limit=100.0, + actuator_velocity_limit=50.0, + saturation_effort={"hip": 100.0, "calf": 190.0}, + ) + actuator = actuator_cfg.class_type( + actuator_cfg, + joint_names=joint_names, + joint_ids=[0, 1], + num_envs=1, + device=device, + stiffness=actuator_cfg.stiffness, + damping=actuator_cfg.damping, + ) + + # at half the no-load speed each joint delivers half of its own stall torque, and the shared + # effort limit is high enough to clip neither + actuator._joint_vel[:] = 25.0 + clipped_effort = actuator._clip_effort(torch.full((1, 2), 500.0, device=device)) + torch.testing.assert_close(clipped_effort, torch.tensor([[50.0, 95.0]], device=device)) From 5cfe96d7aff261e3fb607fb4f3f7d4a060fa13a6 Mon Sep 17 00:00:00 2001 From: Antoine Richard Date: Fri, 4 Sep 2026 09:53:05 +0200 Subject: [PATCH 2/2] Fix Unitree Go1 and Go2 calf actuator limits Both configurations applied one set of DC motor limits to all twelve leg joints, but the Go1 and Go2 calf sits behind an extra knee reduction (1.50:1 and 1.92:1). The calf was therefore capped at roughly half its rated torque while its torque-speed curve kept motoring up to a no-load speed it cannot reach on hardware. Give the calf its own limits, taken from the joint drives authored in go1.usd and go2.usd, which already carry the reduction. The hip and thigh limits are aligned with the same assets so all three come from one source. --- .../fix-unitree-knee-reduction.minor.rst | 11 +++++++++++ .../isaaclab_assets/robots/unitree.py | 16 ++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 source/isaaclab_assets/changelog.d/fix-unitree-knee-reduction.minor.rst diff --git a/source/isaaclab_assets/changelog.d/fix-unitree-knee-reduction.minor.rst b/source/isaaclab_assets/changelog.d/fix-unitree-knee-reduction.minor.rst new file mode 100644 index 000000000000..b85e7c73bc55 --- /dev/null +++ b/source/isaaclab_assets/changelog.d/fix-unitree-knee-reduction.minor.rst @@ -0,0 +1,11 @@ +Fixed +^^^^^ + +* Fixed the Unitree Go1 and Go2 leg actuator limits ignoring the knee reduction. Both robots applied + the hip and thigh limits to the calf joints, which capped calf torque well below its rated value and + let the torque-speed curve keep motoring past its rated speed. The calf joints now use the limits + 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), + and the hip and thigh limits were aligned with the same assets (23.7 N·m, 30.1 rad/s). + + These robots now produce more calf torque at lower calf speeds, so policies trained on the previous + configuration should be retrained rather than reused directly. diff --git a/source/isaaclab_assets/isaaclab_assets/robots/unitree.py b/source/isaaclab_assets/isaaclab_assets/robots/unitree.py index be6b2399efa9..45c0c7eec929 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/unitree.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/unitree.py @@ -39,9 +39,11 @@ torque_scale=1.0, input_order="pos_vel", input_idx=[0, 1, 2], - actuator_effort_limit=23.7, # taken from spec sheet - actuator_velocity_limit=30.0, # taken from spec sheet - saturation_effort=23.7, # same as effort limit + # the calf sits behind an extra 1.5:1 knee reduction, so its joint-side torque is higher + # and its joint-side speed lower than the hip and thigh. Values match ``go1.usd``. + actuator_effort_limit={".*_hip_joint": 23.7, ".*_thigh_joint": 23.7, ".*_calf_joint": 35.55}, + actuator_velocity_limit={".*_hip_joint": 30.1, ".*_thigh_joint": 30.1, ".*_calf_joint": 20.06}, + saturation_effort={".*_hip_joint": 23.7, ".*_thigh_joint": 23.7, ".*_calf_joint": 35.55}, ) """Configuration of Go1 actuators using MLP model. @@ -171,9 +173,11 @@ actuators={ "base_legs": DCMotorCfg( joint_names_expr=[".*_hip_joint", ".*_thigh_joint", ".*_calf_joint"], - actuator_effort_limit=23.5, - saturation_effort=23.5, - actuator_velocity_limit=30.0, + # the calf sits behind an extra 1.92:1 knee reduction, so its joint-side torque is + # higher and its joint-side speed lower than the hip and thigh. Values match ``go2.usd``. + actuator_effort_limit={".*_hip_joint": 23.7, ".*_thigh_joint": 23.7, ".*_calf_joint": 45.43}, + saturation_effort={".*_hip_joint": 23.7, ".*_thigh_joint": 23.7, ".*_calf_joint": 45.43}, + actuator_velocity_limit={".*_hip_joint": 30.1, ".*_thigh_joint": 30.1, ".*_calf_joint": 15.70}, stiffness=25.0, damping=0.5, friction=0.0,