Set Up Actuators from Python#

Note

The isaacsim.core.experimental.actuators extension is experimental. APIs may change between releases.

This tutorial walks through building Newton actuators in Python and attaching them to a Franka robot, without authoring any USD.

By the end of this tutorial you’ll know how to:

  • Build an ActuatorConfig from the stock newton.actuators controllers, clamping stages, and delays.

  • Construct an ArticulationActuators from a list of Python configs.

  • Drive an articulation to a position target through the actuator pipeline.

All code examples come from the complete, runnable file newton_actuators_python_example.py:

# Newton Actuators Python example
./python.sh standalone_examples/api/isaacsim.core.experimental.actuators/newton_actuators_python_example.py

Prerequisites

  • Read Newton Actuators for the high-level structure of a Newton actuator pipeline.

Building a stock PD actuator config#

ActuatorConfig is a thin bundle of three components: a required Newton Controller plus an optional list of Clamping stages and an optional Delay. The simplest case is a PD controller with no clamping and no delay:

    import warp as wp
    from isaacsim.core.experimental.actuators import ActuatorConfig
    from newton.actuators import ControllerPD

    # Per-robot Warp arrays.  Even a single-robot scene must size them with
    # n_robots = len(Articulation(paths)).
    pd_config = ActuatorConfig(
        controller=ControllerPD(
            kp=wp.array([kp] * n_robots, dtype=wp.float32),
            kd=wp.array([kd] * n_robots, dtype=wp.float32),
        )
    )

Note

The arrays are sized for n_robots, not for the number of DOFs. When the articulation path matches N robot instances, the same actuator is fanned out across all N; the array entries are the per-instance gains for that one actuator.

Adding clamping and delay#

Most real actuators need at least an effort limit. Append clamping stages to ActuatorConfig.clamping and optionally add a Delay for command-input latency:

    import warp as wp
    from isaacsim.core.experimental.actuators import ActuatorConfig
    from newton.actuators import ClampingMaxEffort, ControllerPD, Delay

    config = ActuatorConfig(
        controller=ControllerPD(
            kp=wp.array([kp] * n_robots, dtype=wp.float32),
            kd=wp.array([kd] * n_robots, dtype=wp.float32),
        ),
        clamping=[
            ClampingMaxEffort(max_effort=wp.array([max_effort] * n_robots, dtype=wp.float32)),
        ],
        delay=Delay(
            delay_steps=wp.array([2] * n_robots, dtype=wp.int32),
            max_delay=2,
        ),
    )

Three clamping types ship with Newton:

  • ClampingMaxEffort — symmetric saturation to [-max_effort, +max_effort].

  • ClampingDCMotor — linear torque-speed envelope for DC motors.

  • ClampingPositionBased — joint-position-dependent effort lookup table.

Constructing ArticulationActuators#

Use from_actuators() to skip USD discovery and supply the configs directly. Each config is paired with the DOF name (the leaf of the joint’s USD path) it should drive:

    from isaacsim.core.experimental.actuators import ArticulationActuators
    from isaacsim.core.experimental.prims import Articulation

    # FRANKA_PRIM_PATH = "/World/Franka"
    # ARM_JOINTS       = ["panda_joint1", "panda_joint2", ..., "panda_joint7"]
    # Single Franka instance, so n_robots = 1.
    n_robots = len(Articulation(FRANKA_PRIM_PATH))

    actuators = [
        (build_pd_actuator_config(n_robots, kp, kd), name) for name, kp, kd in zip(ARM_JOINTS, KP_GAINS, KD_GAINS)
    ]

    actuated = ArticulationActuators.from_actuators(
        FRANKA_PRIM_PATH,
        actuators=actuators,
    )

    # A small armature on every DOF improves numerical stability when joints
    # are driven externally (the Newton-actuator effort can excite high-frequency
    # modes that the implicit USD drive would normally damp).  Real motors and
    # gearboxes also carry rotor inertia, so a non-zero armature is closer to
    # physical reality regardless of stability concerns.
    actuated.articulation.set_dof_armatures(0.1)

If the same DOF name appears twice, or the name does not exist on the articulation, from_actuators raises ValueError with a list of the DOFs available on the articulation.

Note

The construction snippet ends with a call to set_dof_armatures(). An armature is virtual rotor inertia that many physics engines add to joints to improve numerical stability. For externally-driven joints the Newton-actuator effort can excite high-frequency dynamics that the implicit USD drive normally damps for you. Real motors and gearboxes also carry significant rotor inertia, so a non-zero armature is closer to physical reality regardless of stability concerns. If you observe high-frequency instability, increasing the armature value may help.

Trying the non-ideal actuators#

The companion script supports a --non-ideal flag that swaps the simple PD configs for the PD + per-joint effort clamp + 2-step input delay variant from the previous section:

./python.sh standalone_examples/api/isaacsim.core.experimental.actuators/newton_actuators_python_example.py --non-ideal

Running with this flag is a useful sanity check: the same robot still tracks the position target, but with saturated effort and delayed commands — closer to real hardware behavior.

Driving the robot#

By default, ArticulationActuators registers a callback that runs immediately before every physics step. On each call the callback reads the current joint state and target, evaluates each actuator’s step pipeline, and writes the resulting joint effort to the articulation. You don’t drive this loop yourself; once the ArticulationActuators wrapper is constructed, the actuators are live.

To send commands, write to the underlying Articulation exactly as you would without actuators; the callback picks the targets up on the next tick:

    articulation = actuated.articulation

    timeline = omni.timeline.get_timeline_interface()
    timeline.play()
    simulation_app.update()

    # Send a per-DOF position target.  The PD controller reads it each tick.
    # Values match the Franka robot descriptor's default_q (home pose).
    target_positions = [0.8, -1.3, 0.0, -2.87, 0.0, 2.0, 0.75]
    arm_dof_indices = articulation.get_dof_indices(ARM_JOINTS)
    articulation.set_dof_position_targets(
        positions=target_positions,
        dof_indices=arm_dof_indices,
    )

    for _ in range(num_steps):  # ~4 seconds at 60 Hz
        simulation_app.update()
Franka Panda driven to its home pose by the Newton PD actuators built in this tutorial.

The Franka converges to the home pose under the Newton PD actuators, then has a feedforward applied at one joint.#

You can also send a feedforward effort that is added on top of the controller output via set_dof_feedforward_effort_targets(). With kp and kd set to zero this becomes a pure open-loop torque drive:

    # Feedforward effort is added on top of the controller output every tick.
    # With kp = kd = 0 it becomes the entire output — a pure open-loop torque drive.
    feedforward_efforts = [50.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
    actuated.set_dof_feedforward_effort_targets(
        feedforward_efforts,
        dof_indices=arm_dof_indices,
    )

Note

You don’t have to attach an explicit actuator to every joint. In this tutorial the seven arm joints get explicit Newton actuators while the two finger joints do not; those finger joints keep their authored UsdPhysics.DriveAPI stiffness and damping and behave exactly as they would on a stock Franka.

Note

The method set_dof_feedforward_effort_targets() only affects joints that have an explicit actuator. A feedforward value written to a DOF without an explicit actuator has no effect.

When you’re done, tear the wrapper down. ArticulationActuators is a context manager; __exit__ calls actuated.close(), which deregisters all SimulationManager callbacks owned by the instance. The recommended idiom is to construct the wrapper directly inside the with statement so its lifetime is bounded by the block (construct below is whichever factory you wrote — for example from_actuators() or the construct_articulation_actuators helper from earlier in this tutorial):

        # Recommended teardown pattern: construct ``ArticulationActuators`` inside
        # the ``with`` statement so the wrapper's lifetime is bounded by the block.
        # ``__exit__`` calls ``actuated.close()``, which deregisters every
        # ``SimulationManager`` lifecycle callback owned by the instance and is
        # guaranteed to run even if the body raises.
        with construct() as actuated:
            drive_to_target(actuated)

If a with block isn’t convenient — for example, when the construction and use happen in different scopes — call actuated.close() explicitly when finished. Either path unhooks the pre-physics callback so the wrapper stops updating actuator efforts each tick; the articulation itself is unaffected and continues to exist on the stage.

Note

The built-in controllers, clampings, and delays cover almost every case. If you do need something custom, ActuatorConfig accepts any user-written subclass of newton.actuators.Controller, Clamping, or Delay — refer to the Newton documentation for the base-class contracts. Custom subclasses cannot be authored in USD; attach them via from_actuators().

What’s next#