Author and Parse Actuators from USD#

Note

The isaacsim.core.experimental.actuators extension is experimental. The Newton USD schema names (NewtonActuator, NewtonPDControlAPI, …) are part of the shared Newton schema and are also subject to change.

This tutorial shows how to bake an actuator configuration directly into a robot’s USD file, so that the actuator pipeline is reconstructed automatically every time the asset is loaded — by either Isaac Sim or Isaac Lab.

Authoring actuators on the asset is the recommended path when:

  • You want the same robot file to behave identically across applications (Isaac Sim and Isaac Lab).

  • You are bundling a robot package for distribution: hand-tuned gains, effort limits, and motor curves all travel with the file.

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

  • Author NewtonActuator prims with the add_actuator() helper.

  • Inspect the resulting USDA so you can hand-edit it later.

  • Save the stage and re-open it so the actuators are discovered automatically.

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

# Newton Actuators USD example
./python.sh standalone_examples/api/isaacsim.core.experimental.actuators/newton_actuators_usd_example.py

Prerequisites

  • Read Newton Actuators for the actuator pipeline overview.

  • Familiarity with USD references and API schemas.

The Newton actuator schema#

A Newton actuator is a USD prim of type NewtonActuator, placed under the articulation root inside an Actuators scope. It carries:

  • A newton:targets relationship pointing at the joint it drives

  • One controller API schema (e.g. NewtonPDControlAPI, NewtonPIDControlAPI, NewtonNeuralControlAPI) and the corresponding newton:* attributes.

  • Zero or more clamping API schemas (NewtonMaxEffortClampingAPI, NewtonDCMotorClampingAPI, NewtonPositionBasedClampingAPI).

  • An optional delay API schema (NewtonActuatorDelayAPI).

A minimal authored prim looks like:

def NewtonActuator "panda_joint1_actuator" (
    prepend apiSchemas = ["NewtonPDControlAPI", "NewtonMaxEffortClampingAPI"]
)
{
    rel newton:targets = </World/Franka/.../panda_joint1>
    float newton:kp = 400.0
    float newton:kd = 40.0
    float newton:maxEffort = 87.0
}

Any application that understands the Newton USD schema can parse the same prim and recover the same actuator pipeline. This is what lets the same asset move between Isaac Sim and Isaac Lab.

Authoring with add_actuator#

Hand-writing USDA is fine for one-off tweaks, but for setup-time configuration the easiest path is the add_actuator() helper. It takes Python config dataclasses, validates them, defines the NewtonActuator prim under {articulation_root}/Actuators/{name}, applies the appropriate API schemas, and authors the corresponding newton:* attributes — all in one call.

    from isaacsim.core.experimental.actuators import (
        MaxEffortClampingConfig,
        PDControlConfig,
        add_actuator,
    )

    # Per-joint kp, kd, and effort limits
    JOINT_PARAMS = {
        "panda_joint1": dict(kp=67.0, kd=8.0, max_effort=1000.0),
        "panda_joint2": dict(kp=66.0, kd=8.0, max_effort=1000.0),
        "panda_joint3": dict(kp=65.0, kd=8.0, max_effort=1000.0),
        "panda_joint4": dict(kp=64.0, kd=8.0, max_effort=1000.0),
        "panda_joint5": dict(kp=63.0, kd=8.0, max_effort=1000.0),
        "panda_joint6": dict(kp=62.0, kd=8.0, max_effort=1000.0),
        "panda_joint7": dict(kp=61.0, kd=8.0, max_effort=1000.0),
    }

    for joint_name, p in JOINT_PARAMS.items():
        add_actuator(
            franka_path,
            target_names=joint_name,
            name=f"{joint_name}_actuator",
            controller=PDControlConfig(kp=p["kp"], kd=p["kd"]),
            clamping=[MaxEffortClampingConfig(max_effort=p["max_effort"])],
        )

The target_names argument matches against the leaf segment of joint USD paths, so passing "panda_joint1" resolves to whatever full path that joint has under the articulation root. The helper validates that each name matches exactly one joint under the articulation; if a name is ambiguous or absent, it raises ValueError.

Each clamping type may appear at most once per actuator. Multiple clamps can be composed together:

clamping=[
    MaxEffortClampingConfig(max_effort=87.0),
    DCMotorClampingConfig(saturation_effort=120.0, velocity_limit=10.0, max_motor_effort=87.0),
]

Inspecting the authored prims#

The standalone script flattens the Actuators subtree to USDA and prints it to the terminal right after authoring, so you can verify exactly what was written before exporting. The output should look like:

=== Authored Newton actuator prims (USDA) ===
#usda 1.0

over "panda"
{
    def "Actuators"
    {
        def NewtonActuator "panda_joint1_actuator" (
            prepend apiSchemas = ["NewtonPDControlAPI", "NewtonMaxEffortClampingAPI"]
        )
        {
            float newton:constEffort = 0
            float newton:kd = 8
            float newton:kp = 67
            float newton:maxEffort = 1000
            rel newton:targets = </panda/panda_link0/panda_joint1>
        }

        def NewtonActuator "panda_joint2_actuator" (
            prepend apiSchemas = ["NewtonPDControlAPI", "NewtonMaxEffortClampingAPI"]
        )
        {
            float newton:constEffort = 0
            float newton:kd = 8
            float newton:kp = 66
            float newton:maxEffort = 1000
            rel newton:targets = </panda/panda_link1/panda_joint2>
        }

        def NewtonActuator "panda_joint3_actuator" (
            prepend apiSchemas = ["NewtonPDControlAPI", "NewtonMaxEffortClampingAPI"]
        )
        {
            float newton:constEffort = 0
            float newton:kd = 8
            float newton:kp = 65
            float newton:maxEffort = 1000
            rel newton:targets = </panda/panda_link2/panda_joint3>
        }

        def NewtonActuator "panda_joint4_actuator" (
            prepend apiSchemas = ["NewtonPDControlAPI", "NewtonMaxEffortClampingAPI"]
        )
        {
            float newton:constEffort = 0
            float newton:kd = 8
            float newton:kp = 64
            float newton:maxEffort = 1000
            rel newton:targets = </panda/panda_link3/panda_joint4>
        }

        def NewtonActuator "panda_joint5_actuator" (
            prepend apiSchemas = ["NewtonPDControlAPI", "NewtonMaxEffortClampingAPI"]
        )
        {
            float newton:constEffort = 0
            float newton:kd = 8
            float newton:kp = 63
            float newton:maxEffort = 1000
            rel newton:targets = </panda/panda_link4/panda_joint5>
        }

        def NewtonActuator "panda_joint6_actuator" (
            prepend apiSchemas = ["NewtonPDControlAPI", "NewtonMaxEffortClampingAPI"]
        )
        {
            float newton:constEffort = 0
            float newton:kd = 8
            float newton:kp = 62
            float newton:maxEffort = 1000
            rel newton:targets = </panda/panda_link5/panda_joint6>
        }

        def NewtonActuator "panda_joint7_actuator" (
            prepend apiSchemas = ["NewtonPDControlAPI", "NewtonMaxEffortClampingAPI"]
        )
        {
            float newton:constEffort = 0
            float newton:kd = 8
            float newton:kp = 61
            float newton:maxEffort = 1000
            rel newton:targets = </panda/panda_link6/panda_joint7>
        }
    }
}

The parent "panda" prim shows up as an over because the printed layer only contains the authored Actuators subtree — the rest of the Franka asset lives in the original USD file and is composed in at load time. Each NewtonActuator carries the controller and clamping API schemas, the corresponding newton:* attributes, and a newton:targets relationship pointing at the joint it drives.

Saving the stage#

Once authored, the actuator prims are part of the stage and persist exactly like any other USD content. Flatten the stage to a single file so it can be distributed:

    stage = stage_utils.get_current_stage(backend="usd")
    stage.Export(out_path.as_posix())

Open the exported file in any USD-aware text editor or USD viewer to confirm the Actuators scope, the newton:* attributes, and the newton:targets relationships are intact.

Discovering authored actuators#

When you construct ArticulationActuators with just an articulation root, it walks the USD subtree, finds every NewtonActuator prim, parses its applied API schemas, and rebuilds the actuator pipeline:

    from isaacsim.core.experimental.actuators import ArticulationActuators

    actuated = ArticulationActuators(franka_path)
    print(f"Discovered {len(actuated.actuators)} actuators")
    print(f"Actuated DOF indices: {actuated.actuated_dof_indices}")

No further wiring is needed — the pre-physics callback is registered automatically and the actuators take over the moment the timeline starts.

Round-trip: author, save, re-open, discover#

The most useful pattern is the full round-trip: author once on a base asset, flatten to disk, then load the saved file in any application that understands the Newton schema:

    success, stage = stage_utils.open_stage(out_path.as_posix())
    assert success, f"Failed to open exported stage at {out_path}"

    # The articulation root is the asset's default prim, preserved by Export.
    franka_path = stage.GetDefaultPrim().GetPath().pathString

    from isaacsim.core.experimental.actuators import ArticulationActuators

    # Use ArticulationActuators as a context manager so its SimulationManager
    # callbacks are deregistered deterministically on exit.
    with ArticulationActuators(franka_path) as actuated:
        # For sim stability, and simulation of the reflected inertia (armature) of the actuator.
        actuated.articulation.set_dof_armatures(0.1)
        try:
            assert len(actuated.actuators) == 7, "Expected 7 actuators on the Franka arm"

            timeline = omni.timeline.get_timeline_interface()
            timeline.play()
            simulation_app.update()
            for _ in range(120):
                simulation_app.update()
        finally:
            omni.timeline.get_timeline_interface().stop()

The same exported file can be loaded in Isaac Lab, where its runtime integration layer parses the same NewtonActuator prims and recovers the same effective control law.

Hand-editing authored actuators#

After authoring, the Franka actuators on the example asset look like this in the saved USDA:

def Scope "Actuators"
{
    def NewtonActuator "panda_joint1_actuator" (
        prepend apiSchemas = ["NewtonPDControlAPI", "NewtonMaxEffortClampingAPI"]
    )
    {
        rel newton:targets = </World/Franka/panda_link0/panda_joint1>
        float newton:kp = 400.0
        float newton:kd = 40.0
        float newton:maxEffort = 87.0
    }

    # ... one prim per joint ...
}

Tweaks like adjusting a gain or raising an effort cap can be made by editing attributes directly.

Limitations#

  • Custom controller subclasses. Subclasses of newton.actuators.Controller defined in user code (see Set Up Actuators from Python) do not have a corresponding USD schema and cannot be authored or discovered this way. Use the Python construction path for those.

  • Homogeneous instances. When the articulation prim is referenced multiple times (a robot fleet), all instances share the same authored actuator parameters.

  • No GUI authoring. There is not yet an Isaac Sim GUI panel for adding NewtonActuator prims; use the Python helper or hand-edit USDA.

What’s next#