Source code for isaaclab.actuators.newton.adapter

# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

"""Newton-actuator adapter shared by Newton, PhysX, and OVPhysX.

Owns the actuator-state lifecycle, the pre-clamp computed-effort buffer,
and the per-step ``step`` / ``reset`` / ``finalize`` calls. The
:meth:`~NewtonActuatorAdapter.from_usd` classmethod parses
``NewtonActuator`` USD prims for PhysX and OVPhysX. Newton populates
``model.actuators`` itself.

DR gain updates bypass the adapter — the articulation writes straight
to controller arrays.
"""

from __future__ import annotations

from collections.abc import Sequence
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, TypeAlias

import numpy as np
import torch
import warp as wp
from newton import Model
from newton._src.utils.selection import FrequencyLayout
from newton.actuators import Actuator, Clamping, Delay
from newton.selection import ArticulationView

from .kernels import (
    build_implicit_dof_mask,
    build_per_dof_env_mask_kernel,
    set_mask_kernel,
    zero_at_indices_kernel,
)

if TYPE_CHECKING:
    from isaaclab.actuators import ActuatorCollection

# ---------------------------------------------------------------------------
# Abstract base — backend-independent logic
# ---------------------------------------------------------------------------


class NewtonActuatorAdapter:
    """Adapter that wraps a list of :class:`newton.actuators.Actuator`.

    Owns the actuator-state lifecycle, DOF-to-actuator bookkeeping,
    stepping, reset, and the pre-clamp computed-effort buffer the
    in-graph telemetry kernel reads on the post-actuator hook.
    """

    @dataclass(frozen=True)
    class ArticulationBinding:
        """Newton fast-path init state for one articulation.

        Returned by :meth:`bind_articulation`. Bundles the implicit-DOF mask
        and the per-articulation view of the adapter's computed-effort buffer.
        """

        implicit_dof_mask: wp.array
        """Per-DOF mask consumed by ``sync_torque_telemetry``; ``1`` on implicit-actuator DOFs, ``0`` otherwise."""

        implicit_dof_mask_owner: torch.Tensor
        """Torch tensor owning the memory :attr:`implicit_dof_mask` aliases; keep referenced for the mask's lifetime."""

        computed_effort_view: wp.array
        """This articulation's slice of the adapter's pre-clamp computed-effort buffer, ``(num_envs, num_joints)``."""

    def __init__(
        self,
        actuators: list[Actuator],
        num_envs: int,
        num_joints: int,
        dof_offset: int,
        device: str,
    ):
        self.actuators = actuators
        self.num_joints = num_joints

        self._num_envs = num_envs
        self._dof_offset = dof_offset
        self._device = device

        # Collect the set of local DOFs covered by some actuator. Only the
        # env-0 slice of each actuator's flat ``indices`` array is needed —
        # later envs are repeats with a constant ``num_joints`` stride.
        managed: set[int] = set()
        for act in actuators:
            all_indices = act.indices.numpy()
            num_per_act = len(all_indices) // num_envs
            for global_dof in all_indices[:num_per_act]:
                local_dof = global_dof - dof_offset
                if 0 <= local_dof < num_joints:
                    managed.add(local_dof)

        if len(managed) == num_joints:
            self.joint_indices: torch.Tensor | slice = slice(None)
        else:
            self.joint_indices = torch.tensor(sorted(managed), dtype=torch.int32, device=device)

        self._states_a = [act.state() for act in actuators]
        self._states_b = [act.state() for act in actuators]

        # Pre-clamp computed effort buffer. Each Newton actuator scatter-adds
        # its raw controller output to ``sim_control.joint_computed_f`` when
        # ``control_computed_output_attr`` is set; we route that to this
        # buffer so the post-actuator telemetry kernel can report the actual
        # computed (pre-clamp) effort instead of mirroring ``joint_f``. The
        # binding onto ``sim_control`` happens in :meth:`finalize`.
        self._computed_effort = wp.zeros(
            num_envs * num_joints,
            dtype=wp.float32,
            device=device,
        )
        self.computed_effort_2d = self._computed_effort.reshape((num_envs, num_joints))
        for act in actuators:
            act.control_computed_output_attr = "joint_computed_f"

    def finalize(self, sim_control: Any) -> None:
        """Bind the pre-clamp computed-effort buffer onto ``sim_control``.

        Args:
            sim_control: The ``sim_control`` object that will be passed
                to :meth:`step` for this adapter's lifetime. Newton's
                ``Control`` on the Newton backend, an
                :class:`~isaaclab.actuators.newton.physx_wrapper.PhysxActuatorWrapper`
                on the PhysX backend.
        """
        sim_control.joint_computed_f = self._computed_effort

    def step(self, sim_state: Any, sim_control: Any, dt: float) -> None:
        """Zero actuated DOFs, step all actuators, and swap state buffers.

        Args:
            sim_state: Object with ``joint_q``, ``joint_qd``, etc.
                Newton ``State`` on the Newton backend,
                :class:`~isaaclab.actuators.newton.physx_wrapper.PhysxActuatorWrapper`
                on the PhysX backend.
            sim_control: Object with ``joint_f``, ``joint_target_q``, etc.
                Newton ``Control`` on the Newton backend,
                :class:`~isaaclab.actuators.newton.physx_wrapper.PhysxActuatorWrapper`
                on the PhysX backend.
            dt: Physics timestep [s].
        """
        # Zero before scatter-add (actuators accumulate into this buffer).
        self._computed_effort.zero_()
        for act in self.actuators:
            wp.launch(
                zero_at_indices_kernel,
                dim=act.indices.shape[0],
                inputs=[sim_control.joint_f, act.indices],
            )
        for act, sa, sb in zip(self.actuators, self._states_a, self._states_b):
            act.step(sim_state, sim_control, sa, sb, dt=dt)
        self._swap_state_buffers()

    def _swap_state_buffers(self) -> None:
        """Advance the actuator state ping-pong after an eager step or graph replay."""
        self._states_a, self._states_b = self._states_b, self._states_a

    def reset(self, env_ids: Sequence[int] | torch.Tensor | None = None) -> None:
        """Reset actuator states for the given environments.

        Args:
            env_ids: Environment indices to reset. ``None`` (or
                ``slice(None)``, which IsaacLab callers sometimes pass)
                resets all environments. Otherwise expects a torch tensor
                or sequence of int indices.

        Newton's :meth:`Actuator.State.reset` expects a per-DOF boolean
        mask of length ``num_actuators`` (= ``num_envs * dofs_per_actuator``),
        not a per-env mask — each entry gates the corresponding column of
        the actuator's state buffers (delay queue, controller integral,
        etc.). We therefore build a per-actuator per-DOF mask from the
        env mask before delegating to each state.
        """
        if env_ids is None or env_ids == slice(None):
            for sa, sb in zip(self._states_a, self._states_b):
                if sa is not None:
                    sa.reset(None)
                if sb is not None:
                    sb.reset(None)
            return

        if isinstance(env_ids, torch.Tensor):
            if env_ids.numel() == 0:
                return
            idx = wp.from_torch(env_ids.to(device=self._device).contiguous().to(torch.int32), dtype=wp.int32)
        else:
            if len(env_ids) == 0:
                return
            idx = wp.array(list(env_ids), dtype=wp.int32, device=self._device)
        env_mask = wp.zeros(self._num_envs, dtype=wp.bool, device=self._device)
        wp.launch(set_mask_kernel, dim=idx.shape[0], inputs=[env_mask, idx], device=self._device)

        for act, sa, sb in zip(self.actuators, self._states_a, self._states_b):
            per_dof_mask = wp.zeros(act.indices.shape[0], dtype=wp.bool, device=self._device)
            wp.launch(
                build_per_dof_env_mask_kernel,
                dim=act.indices.shape[0],
                inputs=[act.indices, env_mask, self._dof_offset, self.num_joints, per_dof_mask],
                device=self._device,
            )
            if sa is not None:
                sa.reset(per_dof_mask)
            if sb is not None:
                sb.reset(per_dof_mask)

    def bind_articulation(
        self,
        *,
        implicit_joint_indices: Sequence[slice | torch.Tensor | None],
        dof_offset: int,
        num_joints: int,
    ) -> ArticulationBinding:
        """Assemble the Newton fast-path init state for one articulation.

        Builds the implicit-DOF mask and slices this adapter's
        computed-effort buffer to the articulation's columns.

        Args:
            implicit_joint_indices: Joint selectors of the articulation's implicit
                actuator groups in public joint order; they define
                :attr:`ArticulationBinding.implicit_dof_mask`.
            dof_offset: Offset of this articulation's DOFs in the adapter's
                env-major global index space (``0`` on PhysX, view-dependent
                on Newton).
            num_joints: Articulation-local joint count. Distinct from
                :attr:`num_joints`, which is the whole-model per-env DOF
                stride used to lay out the actuator index arrays.

        Returns:
            The bundled :class:`ArticulationBinding` for this articulation.
        """
        implicit_dof_mask, implicit_dof_mask_owner = build_implicit_dof_mask(
            implicit_joint_indices, num_joints, self._device
        )
        computed_effort_view = self.computed_effort_2d[:, dof_offset : dof_offset + num_joints]
        return self.ArticulationBinding(
            implicit_dof_mask=implicit_dof_mask,
            implicit_dof_mask_owner=implicit_dof_mask_owner,
            computed_effort_view=computed_effort_view,
        )

    @property
    def is_all_graphable(self) -> bool:
        """``True`` when all actuators are CUDA-graph-safe."""
        return len(self.actuators) > 0 and all(a.is_graphable() for a in self.actuators)

    @property
    def is_stateful(self) -> bool:
        """``True`` when any actuator maintains delay or controller state."""
        return any(a.is_stateful() for a in self.actuators)

    @classmethod
    def from_usd(
        cls,
        stage: Any,
        joint_names: list[str],
        num_envs: int,
        num_joints: int,
        device: str,
        articulation_prim_path: str | None = None,
    ) -> NewtonActuatorAdapter:
        """Build an adapter from ``NewtonActuator`` prims authored on *stage*.

        This is the host-adapter counterpart of Newton's
        ``ModelBuilder.add_usd``. It reads the same prims and constructs matching
        :class:`~newton.actuators.Actuator` objects. Structurally compatible
        joints are merged into one actuator with per-DOF parameter arrays and
        combined indices. Newton backends use ``model.actuators`` instead.

        On PhysX and OVPhysX, :paramref:`joint_names` is in this adapter's local
        public order and defines the local indices assigned to parsed actuator targets.

        Args:
            stage: USD stage containing ``NewtonActuator`` prims.
            joint_names: All articulation joint names in adapter-local public order.
            num_envs: Number of environments.
            num_joints: Number of joints per environment.
            device: Warp device string, for example ``"cuda:0"``.
            articulation_prim_path: Root prim path of environment zero's
                articulation. When set, only prims under this subtree are
                considered; otherwise the whole stage is scanned.

        Returns:
            Adapter whose actuator indices use :paramref:`joint_names` order.

        Raises:
            ValueError: If no authored actuator targets a name in
                :paramref:`joint_names`.
        """
        actuators = _create_actuators_from_usd(
            stage,
            joint_names,
            num_envs,
            num_joints,
            device,
            articulation_prim_path=articulation_prim_path,
        )
        return cls(actuators, num_envs, num_joints, dof_offset=0, device=device)


# ---------------------------------------------------------------------------
# Component-addressed parameter access via Newton's selection API.
# ---------------------------------------------------------------------------


[docs] def read_group_parameter(collection: ActuatorCollection, name: str, component: str, attr: str) -> torch.Tensor: """Read one live Newton actuator parameter for a native group. Group-scoped, user-ordered reads of the controller-owned storage. For raw component access, use the group's Newton actuator object (the collection mapping entry) directly. Args: collection: The articulation's actuator collection. name: Actuator group name. component: Component kind: ``"controller"``, ``"delay"``, or ``"clamping"``. attr: Parameter name on that component (e.g. ``"kp"``, ``"max_effort"``). Returns: Live values in the group's joint order, shape ``(num_instances, group_num_joints)``, in the parameter's dtype. Units follow the addressed parameter. Raises: ValueError: If the group is not executed by Newton actuators, the component name is unknown, or no actuator exposes the parameter. """ owners = _group_parameter_owners(collection, name, component, attr) view = collection._newton_selection.view values: torch.Tensor | None = None for actuator, owner in owners: # Non-driven DOFs read as zeros, and groups are disjoint, so overlaying is a sum. projected = wp.to_torch(view.get_actuator_parameter(actuator, owner, attr)) values = projected if values is None else values + projected return values[:, collection._newton_group_columns(name)]
[docs] def write_group_parameter( collection: ActuatorCollection, name: str, component: str, attr: str, values: torch.Tensor, env_ids: torch.Tensor | None = None, joint_ids: torch.Tensor | None = None, ) -> None: """Write one Newton actuator parameter for a native group. Group-scoped, user-ordered writes that reach the controller-owned storage through Newton's selection API. For raw component access, use the group's Newton actuator object (the collection mapping entry) directly. Args: collection: The articulation's actuator collection. name: Actuator group name. component: Component kind: ``"controller"``, ``"delay"``, or ``"clamping"``. attr: Parameter name on that component (e.g. ``"kp"``, ``"max_effort"``). values: New values, shape ``(len(env_ids), len(joint_ids))``. Units follow the addressed parameter. env_ids: Environment indices to update. Defaults to all environments. joint_ids: Group-local joint indices to update. Defaults to all of the group's joints. Raises: ValueError: Same conditions as :func:`read_group_parameter`. """ owners = _group_parameter_owners(collection, name, component, attr) view = collection._newton_selection.view device = collection.device columns = collection._newton_group_columns(name) if joint_ids is not None: columns = columns[joint_ids.to(device, dtype=torch.long)] mask = None env_rows: torch.Tensor | None = None if env_ids is not None: env_rows = env_ids.to(device, dtype=torch.long).unsqueeze(1) mask_torch = torch.zeros(collection.num_instances, dtype=torch.bool, device=device) mask_torch[env_rows] = True mask = wp.from_torch(mask_torch, dtype=wp.bool) values = values.to(device) for actuator, owner in owners: current = view.get_actuator_parameter(actuator, owner, attr) current_torch = wp.to_torch(current) if env_rows is None: current_torch[:, columns] = values.to(dtype=current_torch.dtype) else: current_torch[env_rows, columns.unsqueeze(0)] = values.to(dtype=current_torch.dtype) view.set_actuator_parameter(actuator=actuator, component=owner, name=attr, values=current, mask=mask)
def _group_parameter_owners( collection: ActuatorCollection, name: str, component: str, attr: str ) -> list[tuple[Actuator, Any]]: """Resolve the component instances exposing ``attr`` for one native group.""" if name not in collection._groups: raise KeyError(name) if collection._newton_selection is None or name not in collection._native_group_names: raise ValueError(f"Actuator group '{name}' is not executed by Newton actuators.") group_actuators = collection._groups[name] if not isinstance(group_actuators, tuple): group_actuators = (group_actuators,) owners = [ (actuator, owner) for actuator in group_actuators if (owner := resolve_actuator_component(actuator, component, attr)) is not None ] if not owners: raise ValueError(f"No Newton actuator exposes parameter ('{component}', '{attr}').") return owners def resolve_actuator_component(actuator: Actuator, component: str, attr: str) -> Any | None: """Return the component instance that exposes ``attr`` on the addressed component kind. ``component`` selects the actuator's ``"controller"``, ``"delay"``, or ``"clamping"`` entry; the returned object is what Newton's :meth:`~newton.selection.ArticulationView.get_actuator_parameter` and :meth:`~newton.selection.ArticulationView.set_actuator_parameter` take as their ``component`` argument. Returns ``None`` when the component is absent on this actuator or does not expose ``attr``. Raises ``ValueError`` on unknown component names or ambiguous clamping matches. """ if component == "controller": owner = actuator.controller elif component == "delay": owner = getattr(actuator, "delay", None) elif component == "clamping": matches = [entry for entry in (getattr(actuator, "clamping", None) or []) if hasattr(entry, attr)] if len(matches) > 1: names = ", ".join(type(entry).__name__ for entry in matches) raise ValueError(f"Ambiguous clamping parameter '{attr}': exposed by {names}.") owner = matches[0] if matches else None else: raise ValueError(f"Unknown actuator component '{component}'. Expected 'controller', 'delay', or 'clamping'.") if owner is None or not hasattr(owner, attr): return None return owner class LightArticulationView: """Newton's actuator-parameter selection over bare actuators, without a Model. The PhysX-family backends build Newton actuators from USD without a Newton :class:`~newton.Model`, so they cannot construct a real :class:`~newton.selection.ArticulationView`. The view's actuator-parameter section only consumes the placement attributes below, so this stand-in provides them for the PhysX flat layout (one articulation per world, identity joint order, per-world DOF stride equal to the joint count) and borrows the real implementations unchanged. """ def __init__(self, num_envs: int, num_joints: int, device: str): self.world_count = num_envs self.count_per_world = 1 self.device = device self.full_mask = wp.ones(num_envs, dtype=wp.bool, device=device) self.frequency_layouts = { Model.AttributeFrequency.JOINT_DOF: FrequencyLayout( offset=0, stride_between_worlds=num_joints, stride_within_worlds=num_joints, value_count=num_joints, indices=list(range(num_joints)), device=device, ) } # The real implementations, unchanged: they only read the attributes above. get_actuator_parameter = ArticulationView.get_actuator_parameter set_actuator_parameter = ArticulationView.set_actuator_parameter _get_actuator_dof_mapping = ArticulationView._get_actuator_dof_mapping def _resolve_world_mask(self, mask: Sequence[bool] | wp.array | None) -> wp.array: """Normalize a world mask independently of the installed Newton version.""" if mask is None: return self.full_mask if isinstance(mask, wp.array): if mask.dtype is not wp.bool: raise ValueError(f"Expected Boolean mask, got dtype {mask.dtype}") if mask.shape != (self.world_count,): raise ValueError(f"Expected mask shape ({self.world_count},), got {mask.shape}") if mask.device != self.device: raise ValueError(f"Expected mask on device {self.device}, got {mask.device}") return mask try: return wp.array(mask, dtype=wp.bool, shape=(self.world_count,), device=self.device, copy=False) except Exception as error: raise ValueError(f"Expected Boolean mask with shape ({self.world_count},)") from error @dataclass(frozen=True) class NewtonActuatorSelection: """Execution-setup handoff for Newton actuator parameter access. Pure data returned by :meth:`~isaaclab.actuators.ActuatorControl.finalize_native_actuators` and consumed by the group-scoped parameter access functions (:func:`read_group_parameter` / :func:`write_group_parameter`) and by the collection when it maps native groups to their Newton actuator objects. """ view: Any """Newton :class:`~newton.selection.ArticulationView` or :class:`LightArticulationView` over the articulation.""" actuators: list[Actuator] """Newton actuators visible to the view.""" joint_user_to_backend_indices: tuple[int, ...] | None = None """Optional public-to-backend joint permutation for the view's DOF columns.""" # --------------------------------------------------------------------------- # PhysX-only USD parsing # --------------------------------------------------------------------------- _ResolvedComponent: TypeAlias = tuple[type, dict[str, Any]] _ResolvedActuatorSpec: TypeAlias = tuple[int, type, dict[str, Any], list[_ResolvedComponent]] def _actuator_signature( controller_class: type, controller_arguments: dict[str, Any], component_arguments: list[_ResolvedComponent], ) -> tuple: """Build Newton's structural grouping key for a parsed actuator spec.""" def make_hashable(value: Any) -> Any: if isinstance(value, list | tuple): return tuple(make_hashable(item) for item in value) return value def shared_key(component_class: type, resolved: dict[str, Any]) -> tuple: shared_names = getattr(component_class, "SHARED_PARAMS", set()) return tuple(sorted((name, make_hashable(resolved[name])) for name in shared_names if name in resolved)) clamping_key: list[tuple] = [] has_delay = False for comp_cls, resolved in component_arguments: if issubclass(comp_cls, Delay): has_delay = True elif issubclass(comp_cls, Clamping): clamping_key.append((comp_cls, shared_key(comp_cls, resolved))) return (controller_class, has_delay, tuple(clamping_key), shared_key(controller_class, controller_arguments)) def _tile_per_dof_arguments( arguments: list[dict[str, Any]], num_envs: int, dtype: type, device: wp.Device, ) -> dict[str, wp.array]: """Pack per-joint scalar arguments in environment-major order.""" if not arguments: return {} numpy_dtype = np.int32 if dtype == wp.int32 else np.float32 return { name: wp.array( np.tile(np.asarray([per_joint[name] for per_joint in arguments], dtype=numpy_dtype), num_envs), dtype=dtype, device=device, ) for name in arguments[0] } def _create_actuators_from_usd( stage: Any, joint_names: list[str], num_envs: int, num_total_joints: int, device: str, articulation_prim_path: str | None = None, ) -> list[Actuator]: """Parse ``NewtonActuator`` prims and instantiate standalone actuators. This mirrors the actuator construction that Newton's ``ModelBuilder.add_usd`` performs, but operates independently of a Newton ``Model``. It is used on the PhysX backend where there is no Newton simulation — actuators are stepped manually via the adapter. Because PhysX articulations have no free or ball joints, every joint's coordinate count equals its DOF count. A single ``indices`` array is therefore sufficient for all index roles (``indices``, ``pos_indices``, ``target_pos_indices``). Joints with the same controller and clamping structure are merged into one :class:`Actuator`. Scalar parameters (``kp``, ``kd``, ``saturation_effort``, delay, etc.) are packed per DOF. Parameters marked as ``SHARED_PARAMS`` (e.g. ``model_path``, ``lookup_positions``) remain part of the grouping key and are passed through directly. """ from collections import defaultdict # noqa: PLC0415 from newton.actuators import parse_actuator_prim # noqa: PLC0415 from pxr import Usd # noqa: PLC0415 wp_device = wp.get_device(device) joint_name_to_idx: dict[str, int] = {name: i for i, name in enumerate(joint_names)} if articulation_prim_path is not None: root_prim = stage.GetPrimAtPath(articulation_prim_path) else: root_prim = stage.GetPseudoRoot() parsed_per_joint: dict[int, Any] = {} for prim in Usd.PrimRange(root_prim): parsed = parse_actuator_prim(prim) if parsed is None: continue target_name = parsed.target_path.rsplit("/", 1)[-1] if target_name in joint_name_to_idx: parsed_per_joint[joint_name_to_idx[target_name]] = parsed if not parsed_per_joint: raise ValueError(f"No NewtonActuator prims found targeting any of: {joint_names}") groups: dict[tuple, list[_ResolvedActuatorSpec]] = defaultdict(list) for local_idx, parsed in sorted(parsed_per_joint.items()): controller_arguments = parsed.controller_class.resolve_arguments(dict(parsed.controller_kwargs)) component_arguments = [ (comp_cls, comp_cls.resolve_arguments(comp_kwargs)) for comp_cls, comp_kwargs in parsed.component_specs ] sig = _actuator_signature(parsed.controller_class, controller_arguments, component_arguments) groups[sig].append((local_idx, parsed.controller_class, controller_arguments, component_arguments)) actuators = [] for grouped_specs in groups.values(): local_indices = [spec[0] for spec in grouped_specs] controller_class = grouped_specs[0][1] resolved_controllers = [spec[2] for spec in grouped_specs] resolved_components = [spec[3] for spec in grouped_specs] flat_indices = np.array( [idx + e * num_total_joints for e in range(num_envs) for idx in local_indices], dtype=np.uint32, ) indices = wp.array(flat_indices, device=wp_device) # Controller shared_ctrl = getattr(controller_class, "SHARED_PARAMS", set()) ctrl_arguments = [ {key: value for key, value in resolved.items() if key not in shared_ctrl} for resolved in resolved_controllers ] ctrl_shared = {key: value for key, value in resolved_controllers[0].items() if key in shared_ctrl} controller = controller_class( **_tile_per_dof_arguments(ctrl_arguments, num_envs, wp.float32, wp_device), **ctrl_shared, ) # Components (delay + clampings) clamping_components = [ [(comp_cls, resolved) for comp_cls, resolved in components if issubclass(comp_cls, Clamping)] for components in resolved_components ] delay_arguments = [ resolved for components in resolved_components for comp_cls, resolved in components if issubclass(comp_cls, Delay) ] delay = None if delay_arguments: max_delay = max(int(arguments["delay_steps"]) for arguments in delay_arguments) if max_delay > 0: delay = Delay( **_tile_per_dof_arguments(delay_arguments, num_envs, wp.int32, wp_device), max_delay=max_delay, ) clampings = [] for component_index, (comp_cls, _) in enumerate(clamping_components[0]): resolved_clampings = [components[component_index][1] for components in clamping_components] shared_clamp = getattr(comp_cls, "SHARED_PARAMS", set()) clamp_arguments = [ {key: value for key, value in resolved.items() if key not in shared_clamp} for resolved in resolved_clampings ] clamp_shared = {key: value for key, value in resolved_clampings[0].items() if key in shared_clamp} clampings.append( comp_cls( **_tile_per_dof_arguments(clamp_arguments, num_envs, wp.float32, wp_device), **clamp_shared, ) ) actuator = Actuator( indices=indices, controller=controller, delay=delay, clamping=clampings if clampings else None, control_target_pos_attr="joint_target_pos", control_target_vel_attr="joint_target_vel", ) actuators.append(actuator) return actuators