Skip to content

[Bug Report] Newton backend re-enables joint target stiffness/damping for explicit actuators, causing different rest pose from PhysX #5806

@ecstayalive

Description

@ecstayalive

[Bug Report] Newton backend re-enables joint target stiffness/damping for explicit actuators, causing different rest pose from PhysX

Bug Description

When using the Newton backend with an articulation configured with an explicit actuator, such as DelayedPDActuatorCfg / IdealPDActuator, the robot reaches a different joint rest pose compared with the PhysX backend under the same action command.

The issue appears to come from the Newton articulation initialization path. For explicit actuators, IsaacLab correctly calls:

self.write_joint_stiffness_to_sim_index(stiffness=0.0, joint_ids=actuator.joint_indices)
self.write_joint_damping_to_sim_index(damping=0.0, joint_ids=actuator.joint_indices)

This matches the expected behavior: explicit actuators should compute joint torques in IsaacLab, while the simulator-side joint target drive should be disabled.

However, a few lines later, the Newton backend stores the configured actuator stiffness/damping back into:

self.data._sim_bind_joint_stiffness_sim
self.data._sim_bind_joint_damping_sim

These buffers are directly bound to Newton model attributes:

"joint_target_ke"
"joint_target_kd"

Therefore, for explicit actuators, Newton's internal joint target drive is unintentionally re-enabled.

This causes the Newton backend to apply two controllers at the same time:

  1. IsaacLab explicit actuator torque, pulling the joint toward the processed action target.
  2. Newton internal joint target drive, pulling the joint toward the simulator-side joint_target_pos.

For explicit actuators, Newton only writes effort targets every step. It does not write position/velocity targets unless _has_implicit_actuators is true:

self.data._sim_bind_joint_effort.assign(self._joint_effort_target_sim)

if self._has_implicit_actuators:
    self.data._sim_bind_joint_position_target.assign(self._joint_pos_target_sim)
    self.data._sim_bind_joint_velocity_target.assign(self._joint_vel_target_sim)

As a result, the internal Newton target position can remain at its default value, commonly zero, while the explicit actuator tries to track the actual command target. This makes Newton behave differently from PhysX.

Expected Behavior

For explicit actuators:

  • simulator-side joint stiffness/damping should remain zero
  • only explicit actuator effort should be sent to the simulator
  • Newton and PhysX should produce consistent behavior under the same articulation config and action command

For implicit actuators:

  • simulator-side joint stiffness/damping should still be written into the simulation
  • simulator-side position/velocity targets should be written every step
  • Newton's internal joint target drive should remain active

Actual Behavior

For explicit actuators in the Newton backend:

  • simulator-side joint stiffness/damping are first set to zero
  • then later overwritten with actuator stiffness/damping again
  • Newton internal joint_target_ke/kd remains active
  • the articulation is pulled toward the default simulator-side target position, producing a different rest pose from PhysX

Relevant Code

Newton articulation initialization

File:

source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation.py

Current behavior:

if isinstance(actuator, ImplicitActuator):
    self._has_implicit_actuators = True
    self.write_joint_stiffness_to_sim_index(stiffness=actuator.stiffness, joint_ids=actuator.joint_indices)
    self.write_joint_damping_to_sim_index(damping=actuator.damping, joint_ids=actuator.joint_indices)
else:
    self.write_joint_stiffness_to_sim_index(stiffness=0.0, joint_ids=actuator.joint_indices)
    self.write_joint_damping_to_sim_index(damping=0.0, joint_ids=actuator.joint_indices)

# Later:
wp.launch(
    shared_kernels.write_2d_data_to_buffer_with_indices,
    ...
    outputs=[
        self.data._sim_bind_joint_stiffness_sim,
    ],
)

wp.launch(
    shared_kernels.write_2d_data_to_buffer_with_indices,
    ...
    outputs=[
        self.data._sim_bind_joint_damping_sim,
    ],
)

Newton data binding

File:

source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation_data.py

The buffers above are bound to Newton model drive gains:

self._sim_bind_joint_stiffness_sim = self._root_view.get_attribute(
    "joint_target_ke", SimulationManager.get_model()
)[:, 0]

self._sim_bind_joint_damping_sim = self._root_view.get_attribute(
    "joint_target_kd", SimulationManager.get_model()
)[:, 0]

The property docstring says explicit actuators should have zero simulator-side stiffness/damping:

@property
def joint_stiffness(self) -> wp.array:
    """Joint stiffness provided to the simulation.

    In the case of explicit actuators, the value for the corresponding joints is zero.
    """
    return self._sim_bind_joint_stiffness_sim

Minimal Reproduction Pattern

  1. Configure an articulation with an explicit actuator, for example:
DelayedPDActuatorCfg(
    joint_names_expr=[...],
    stiffness=100.0,
    damping=1.0,
)
  1. Run the same scene once with PhysX and once with Newton.
  2. Send the same joint position action.
  3. Observe that the final/rest joint pose differs between the two backends.
  4. Inspect Newton-side simulator joint stiffness/damping after actuator initialization. They remain non-zero for explicit actuators, even though they should be zero.

This becomes especially visible when using JointPositionToLimitsActionCfg or EMAJointPositionToLimitsActionCfg with rescale_to_limits=True. In that case, action 0 is correctly mapped to the midpoint of the joint limits, but Newton additionally pulls toward the default simulator-side target position because the internal drive was unintentionally left active.

Proposed Fix

Store the actuator model's configured stiffness/damping into the actuator-data buffers, not the simulator-bound Newton drive buffers.

Patch:

diff --git a/source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation.py b/source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation.py
--- a/source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation.py
+++ b/source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation.py
@@ -3398,7 +3398,7 @@ class Articulation(BaseArticulation):
                 outputs=[
-                    self.data._sim_bind_joint_stiffness_sim,
+                    self.data._actuator_stiffness,
                 ],
                 device=self.device,
             )
@@ -3411,7 +3411,7 @@ class Articulation(BaseArticulation):
                 outputs=[
-                    self.data._sim_bind_joint_damping_sim,
+                    self.data._actuator_damping,
                 ],
                 device=self.device,
             )

This keeps the existing implicit actuator behavior intact, because the implicit branch still explicitly calls:

self.write_joint_stiffness_to_sim_index(stiffness=actuator.stiffness, ...)
self.write_joint_damping_to_sim_index(damping=actuator.damping, ...)

But for explicit actuators, the simulator-side Newton drive remains disabled as intended.

Why this fix seems correct

PhysX stores configured actuator stiffness/damping separately from the actual simulator-side drive state. For explicit actuators, PhysX sets simulator stiffness/damping to zero after storing the configured values.

Newton already has dedicated buffers:

self._actuator_stiffness
self._actuator_damping

So the configured actuator gains can be preserved without writing them back into joint_target_ke/kd.

Checklist

  • I have checked that the issue is related to IsaacLab and not only Isaac Sim.
  • I have checked for similar issues in the repository.
  • I have identified a likely source-code cause.
  • I have tested a local patch that preserves implicit actuator behavior while fixing explicit actuator behavior.

System Info

  • OS: Ubuntu 24.04.4 LTS, noble
  • Kernel: Linux 6.17.0-29-generic x86_64
  • Python: 3.12.13
  • Isaac Sim: 6.0.0.0
  • IsaacLab package: 4.5.22
  • IsaacLab repo tag: v3.0.0-beta
  • IsaacLab commit: a4a7602
  • IsaacLab commit date: 2026-03-16 23:38:12 -0700
  • isaaclab_newton: 0.5.9
  • isaaclab_physx: 0.5.11
  • isaaclab_tasks: 1.5.11
  • Newton package: 1.0.0
  • newton-actuators: 0.1.0
  • MuJoCo: 3.5.0
  • mujoco-warp: 3.5.0.2
  • warp-lang: 1.12.0
  • PyTorch: 2.10.0+cu128
  • PyTorch CUDA build: 12.8
  • IsaacLab install mode: editable local checkout
  • Local patch status: source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation.py modified with the proposed 2-line fix

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions