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)) 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,