Skip to content

Fix LEAPP export of PD gains for explicit actuators#6252

Open
lgulich wants to merge 1 commit into
isaac-sim:developfrom
lgulich:lgulich/leapp-export-actuator-gains
Open

Fix LEAPP export of PD gains for explicit actuators#6252
lgulich wants to merge 1 commit into
isaac-sim:developfrom
lgulich:lgulich/leapp-export-actuator-gains

Conversation

@lgulich

@lgulich lgulich commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Description

LEAPP export bakes zero kp/kd gains into the policy graph for any policy trained with an explicit actuator (e.g. DCMotor, IdealPDActuator).

ExportPatcher._collect_action_static_outputs reads the joint PD gains from data.default_joint_stiffness / data.default_joint_damping. Those buffers only hold the gains of implicit actuators, which write them to the simulation. Explicit actuators compute their PD term internally and apply it as joint effort, leaving the sim-level joint stiffness/damping at 0. As a result the exported graph's *_kp_gains / *_kd_gains outputs are all zero, so a deployment that applies torque = kp·(q* − q) − kd·q̇ produces zero torque and the robot is unactuated (reproduced in Sim2MuJoCo, and it would fail identically on hardware).

The fix sources the gains from asset.actuators (the authoritative source for every actuator model) instead of the sim-level buffers. It is a no-op for implicit actuators, whose data.default_joint_* buffers already equal the actuator gains, and recovers the real gains for explicit actuators.

Fixes the zero-gain LEAPP export for explicit-actuator robots (no tracked issue).

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Verification

Tested with a G1 lower-body locomotion policy that uses DCMotor actuators (legs hip kp=100, knee kp=200; damping 2.5/5.0):

  • Before: exported bundle joint_pos_kp_gains / kd_gains were all 0; the robot collapsed immediately in Sim2MuJoCo (zero actuator torque).
  • After: the bundle carries the real gains — joint_pos_kp_gains = [100, 100, …, 200, 200, …], joint_pos_kd_gains = [2.5, …, 5.0, …] — and the robot holds a stable stand for the full evaluation. Verified by running the exporter with this change as the only modification (no downstream patching).

Checklist

  • I have read and understood the contribution guidelines
  • My change is formatted (black clean; no import changes)
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective (verified manually end-to-end; happy to add a unit test for _effective_joint_gains if preferred)
  • I have added a changelog fragment under source/isaaclab/changelog.d/
  • My name already exists in CONTRIBUTORS.md

The LEAPP exporter read joint PD gains from ``data.default_joint_stiffness`` /
``data.default_joint_damping``. These buffers only hold the gains of implicit
actuators (which write them to the simulation); explicit actuators such as
``DCMotor`` and ``IdealPDActuator`` compute their PD term internally and apply
it as joint effort, leaving the sim-level gains at zero. As a result the exported
policy graph carried zero ``kp``/``kd`` for those joints and the deployed policy
was effectively unactuated (in Sim2MuJoCo and on hardware).

Source the gains from ``asset.actuators`` instead, which is correct for every
actuator model (a no-op for implicit actuators, whose buffers already match).

Signed-off-by: Lionel Gulich <lgulich@nvidia.com>
@github-actions github-actions Bot added the isaac-lab Related to Isaac Lab team label Jun 24, 2026
@greptile-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug where LEAPP export baked zero kp/kd gains into the policy graph for any policy trained with explicit actuators (e.g. DCMotor, IdealPDActuator). The root cause was that _collect_action_static_outputs read gains from data.default_joint_stiffness/data.default_joint_damping, buffers that explicit actuators leave at zero since they apply effort directly rather than writing to sim-level gains.

  • Introduces _effective_joint_gains, a new module-level helper that starts from the sim-level buffers (correct for implicit actuators) and then overwrites each actuator's joint slots with actuator.stiffness/actuator.damping from asset.actuators, recovering the true gains for explicit actuators.
  • Updates _collect_action_static_outputs to call _effective_joint_gains instead of reading data buffers directly, and adds a clear comment explaining the explicit-vs-implicit actuator distinction.

Confidence Score: 4/5

Safe to merge — the fix directly addresses a confirmed deployment bug (zero actuator torque) with no behavioral changes for implicit actuators.

The core logic is sound: reading actuator.stiffness/actuator.damping from asset.actuators is the authoritative source and is a no-op for implicit actuators. The one subtle shift is that _effective_joint_gains couples the two gain buffers: if either default_joint_stiffness or default_joint_damping is absent it returns (None, None), whereas the original exported whichever was independently available. This is unlikely to matter in any real asset but is a quiet behavioral narrowing worth knowing about.

export_annotator.py — specifically the (None, None) early-return condition in _effective_joint_gains and the two now-equivalent if kp_gains is not None / if kd_gains is not None guards in the caller.

Important Files Changed

Filename Overview
source/isaaclab/isaaclab/utils/leapp/export_annotator.py Adds _effective_joint_gains helper that reads PD gains from asset.actuators rather than data.default_joint_stiffness/default_joint_damping, correctly sourcing gains for explicit actuators (DCMotor, IdealPDActuator, etc.) that write zero to the sim-level buffers. One minor behavior change: the function returns (None, None) if either buffer is absent, whereas the original exported whichever was available independently.
source/isaaclab/changelog.d/lgulich-leapp-export-actuator-gains.rst New changelog fragment accurately describing the bug and fix for LEAPP export of PD gains for explicit actuators.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["_collect_action_static_outputs(action_manager)"] --> B["For each action term"]
    B --> C["get real_asset, joint_ids"]
    C --> D["_effective_joint_gains(real_asset)"]
    D --> E["Read data.default_joint_stiffness / default_joint_damping"]
    E --> F{"Either buffer None?"}
    F -- Yes --> G["return (None, None)"]
    F -- No --> H["kp = stiffness.torch.clone()\nkd = damping.torch.clone()"]
    H --> I["For each actuator in asset.actuators"]
    I --> J["kp[:, actuator.joint_indices] = actuator.stiffness\nkd[:, actuator.joint_indices] = actuator.damping"]
    J --> K["return (kp, kd)"]
    K --> L{"kp_gains is not None?"}
    G --> L
    L -- Yes --> M["Append TensorSemantics for kp_gains"]
    L -- No --> N["Skip"]
    M --> O{"kd_gains is not None?"}
    N --> O
    O -- Yes --> P["Append TensorSemantics for kd_gains"]
    O -- No --> Q["Skip"]

    style D fill:#d4edda,stroke:#28a745
    style J fill:#d4edda,stroke:#28a745
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["_collect_action_static_outputs(action_manager)"] --> B["For each action term"]
    B --> C["get real_asset, joint_ids"]
    C --> D["_effective_joint_gains(real_asset)"]
    D --> E["Read data.default_joint_stiffness / default_joint_damping"]
    E --> F{"Either buffer None?"}
    F -- Yes --> G["return (None, None)"]
    F -- No --> H["kp = stiffness.torch.clone()\nkd = damping.torch.clone()"]
    H --> I["For each actuator in asset.actuators"]
    I --> J["kp[:, actuator.joint_indices] = actuator.stiffness\nkd[:, actuator.joint_indices] = actuator.damping"]
    J --> K["return (kp, kd)"]
    K --> L{"kp_gains is not None?"}
    G --> L
    L -- Yes --> M["Append TensorSemantics for kp_gains"]
    L -- No --> N["Skip"]
    M --> O{"kd_gains is not None?"}
    N --> O
    O -- Yes --> P["Append TensorSemantics for kd_gains"]
    O -- No --> Q["Skip"]

    style D fill:#d4edda,stroke:#28a745
    style J fill:#d4edda,stroke:#28a745
Loading

Reviews (1): Last reviewed commit: "Fix LEAPP export of PD gains for explici..." | Re-trigger Greptile

Comment on lines +79 to +80
if stiffness is None or damping is None:
return None, None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Asymmetric None check changes prior behavior

The original code independently checked and exported kp_gains and kd_gains — if only one buffer was present, it was still exported. _effective_joint_gains now returns (None, None) when either buffer is absent (stiffness is None or damping is None), so a hypothetical asset that exposes only default_joint_stiffness (or only default_joint_damping) would now silently skip both exports. While this combination is uncommon in practice, the downstream calling code's two independent if kp_gains is not None / if kd_gains is not None guards suggest the caller expects they can be independently None, which is no longer possible after this change.

@frlai

frlai commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

looks good. Thanks for catching this. Not sure how common it is to have kp but not kd and vice versa. If it is not possible we can probably simplify the the downstream tensor semantic creation logic a little. if it is possible then we should not return (none, none) from the effective joint gains function.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

isaac-lab Related to Isaac Lab team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants