Source code for isaaclab.actuators.actuator_collection

# 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

"""Runtime actuator collection for articulations."""

from __future__ import annotations

import copy
import itertools
import logging
import warnings
from collections.abc import Iterator, Mapping, Sequence

import torch
import warp as wp
from prettytable import PrettyTable

from isaaclab.utils.types import ArticulationActions
from isaaclab.utils.warp import ProxyArray
from isaaclab.utils.warp.launch_cache import _WarpLaunchCache

from . import actuator_kernels
from ._compat import _resolve_limit_aliases
from .actuator_base import ActuatorBase, resolve_joint_parameter
from .actuator_base_cfg import ActuatorBaseCfg, _is_implicit_actuator_cfg
from .actuator_control import ActuatorControl
from .actuator_pd import IdealPDActuator, ImplicitActuator

logger = logging.getLogger(__name__)


[docs] class ActuatorCollection(Mapping[str, "ActuatorBase | object"]): """Read-only runtime collection of actuator groups for one articulation. Mapping entries return whoever owns the group. Isaac Lab-executed groups map to their :class:`~isaaclab.actuators.ActuatorBase` model instances. Newton-executed groups map to the Newton :class:`~newton.actuators.Actuator` objects that drive their joints, so users read and modify the owning controller directly. Newton merges structurally identical joints into one actuator, so several groups can map to the same object (or to a tuple when a group spans several); the collection keeps each group's joint indices, which :func:`~isaaclab.actuators.newton.read_group_parameter` and :func:`~isaaclab.actuators.newton.write_group_parameter` use for group-scoped, user-ordered access. Configure membership through :attr:`isaaclab.assets.ArticulationCfg.actuators` before construction; assigning or deleting mapping entries raises :class:`TypeError`. Each joint can belong to at most one group; overlapping joint selections raise :class:`ValueError` during construction. Plain :class:`~isaaclab.actuators.ImplicitActuator` groups are not executed one group at a time: a single internal executor computes all of their joints in one fused kernel launch. All other Lab-executed groups, including subclasses of :class:`~isaaclab.actuators.ImplicitActuator`, execute per group. """ # Initialization.
[docs] def __init__( self, actuator_cfgs: dict[str, ActuatorBaseCfg], control: ActuatorControl, *, debug_value_resolution: bool = False, ): """Initialize the actuator collection. Args: actuator_cfgs: Mapping of actuator group names to actuator configs. control: Backend control bridge for state reads and sim writes. debug_value_resolution: Whether to log actuator value resolution. """ self._control = control self._groups: dict[str, ActuatorBase | object] = {} self._group_joint_names: dict[str, list[str]] = {} self._group_joint_indices: dict[str, slice | torch.Tensor] = {} self._implicit_group_names: set[str] = set() self._native_group_names: set[str] = set() self._debug_value_resolution = debug_value_resolution self._joint_property_resolution_rows: dict[str, dict[str, tuple[tuple[object, ...], ...]]] = {} self._has_implicit_actuators = False self._launch_cache = _WarpLaunchCache(self.device) resolved_cfgs = {name: cfg.copy() for name, cfg in actuator_cfgs.items()} resolved_group_joints = self._resolve_group_joints(resolved_cfgs) self._allocate_buffers() self._target_command = ActuatorTargetCommand(self) self._output_command = ActuatorOutputCommand(self) for name, cfg in resolved_cfgs.items(): _resolve_limit_aliases(name, cfg, resolved_group_joints[name][1]) self._native_group_names = self._control.prepare_native_actuators(self, resolved_cfgs) self._build_groups(resolved_cfgs, resolved_group_joints) self._newton_selection = self._control.finalize_native_actuators(self) if self._native_group_names: if self._newton_selection is None: raise RuntimeError( "The backend declared Newton-executed actuator groups " f"{sorted(self._native_group_names)} but finalize_native_actuators returned no selection." ) for actuator_name in self._native_group_names: self._groups[actuator_name] = self._resolve_newton_group_actuators(actuator_name) self._validate_coverage() self._build_execution_plan() if self._debug_value_resolution: self._print_value_resolution_table() if not self._control.native_actuator_path_active: explicit_group_names = [ name for name, actuator in self._groups.items() if isinstance(actuator, IdealPDActuator) ] if explicit_group_names: warnings.warn( "Isaac Lab execution of explicit actuator models is deprecated. Use Newton actuator execution " f"instead. Affected groups: {', '.join(explicit_group_names)}.", DeprecationWarning, stacklevel=2, )
# Public interface. def __getitem__(self, name: str) -> ActuatorBase | object: return self._groups[name] def __iter__(self) -> Iterator[str]: return iter(self._groups) def __len__(self) -> int: return len(self._groups) def __setitem__(self, name: str, actuator: ActuatorBase) -> None: raise TypeError("ActuatorCollection membership is fixed after initialization.") def __delitem__(self, name: str) -> None: raise TypeError("ActuatorCollection membership is fixed after initialization.") @property def target_command(self) -> ActuatorTargetCommand: """Commands received by the actuator models.""" return self._target_command @property def output_command(self) -> ActuatorOutputCommand: """Processed commands produced for the simulated joints. This view is not submitted-command telemetry for native controllers, which bypass the processed-command arrays. """ return self._output_command @property def num_instances(self) -> int: """Number of articulation instances.""" return self._control.num_instances @property def num_joints(self) -> int: """Number of articulation joints.""" return self._control.num_joints @property def device(self) -> str: """Warp/Torch device string.""" return self._control.device @property def has_implicit_actuators(self) -> bool: """Whether any configured actuator group is implicit.""" return self._has_implicit_actuators @property def computed_effort(self) -> ProxyArray: """Joint efforts computed before clipping [N or N·m, depending on joint type].""" return self._computed_effort_ta @property def applied_effort(self) -> ProxyArray: """Joint efforts applied after clipping [N or N·m, depending on joint type].""" return self._applied_effort_ta # Lifecycle.
[docs] def reset(self, env_ids: Sequence[int] | slice | None = None) -> None: """Reset all actuator group states. Args: env_ids: Environment indices to reset. Defaults to all environments. """ group_env_ids = self._control._normalize_index_sequence(env_ids) for actuator in self._groups.values(): # Newton-executed groups are reset through the backend below. if isinstance(actuator, ActuatorBase): actuator.reset(group_env_ids) self._control.reset_native_actuators(slice(None) if group_env_ids is None else group_env_ids)
[docs] def compute(self, dt: float = 0.0) -> None: """Compute processed actuator commands and telemetry. Args: dt: Physics step size [s]. """ if self._control.compute_native_actuators(self, dt): return if self._implicit_executor is not None: self._implicit_executor.launch(self) joint_pos = self._control.joint_pos joint_vel = self._control.joint_vel for actuator, joint_indices_wp in self._execution_actuators: joint_indices = actuator.joint_indices control_action = ArticulationActions( joint_positions=self.target_command.position.torch[:, joint_indices], joint_velocities=self.target_command.velocity.torch[:, joint_indices], joint_efforts=self.target_command.effort.torch[:, joint_indices], joint_indices=joint_indices, ) control_action = actuator.compute( control_action, joint_pos=joint_pos.torch[:, joint_indices], joint_vel=joint_vel.torch[:, joint_indices], ) self._scatter_actuator_output(actuator, control_action, joint_indices_wp)
[docs] def submit_commands(self) -> None: """Submit processed actuator command buffers through the backend control object.""" self._control.submit_commands(self)
def _newton_group_columns(self, name: str) -> torch.Tensor: """Backend view columns of one group's joints, in group joint order.""" joint_ids = self._group_joint_indices[name] if isinstance(joint_ids, slice): columns = torch.arange(self.num_joints, device=self.device) else: columns = joint_ids.to(self.device, dtype=torch.long) user_to_backend = self._newton_selection.joint_user_to_backend_indices if user_to_backend is not None: columns = torch.tensor(user_to_backend, dtype=torch.long, device=self.device)[columns] return columns # Construction and property resolution. def _allocate_buffers(self) -> None: """Allocate articulation-wide command and telemetry buffers.""" shape = (self.num_instances, self.num_joints) # Staging buffers for the actuators I/O self._joint_pos_target = wp.zeros(shape, dtype=wp.float32, device=self.device) self._joint_vel_target = wp.zeros(shape, dtype=wp.float32, device=self.device) self._joint_effort_target = wp.zeros(shape, dtype=wp.float32, device=self.device) self._joint_pos_target_sim = wp.zeros(shape, dtype=wp.float32, device=self.device) self._joint_vel_target_sim = wp.zeros(shape, dtype=wp.float32, device=self.device) self._joint_effort_target_sim = wp.zeros(shape, dtype=wp.float32, device=self.device) # Telemetry buffers self._computed_effort = wp.zeros(shape, dtype=wp.float32, device=self.device) self._applied_effort = wp.zeros(shape, dtype=wp.float32, device=self.device) self._soft_joint_vel_limits = wp.zeros(shape, dtype=wp.float32, device=self.device) # All joint IDs and masks self._all_joint_ids = wp.array(list(range(self.num_joints)), dtype=wp.int32, device=self.device) self._all_true_env_mask = wp.ones(self.num_instances, dtype=wp.bool, device=self.device) self._all_true_joint_mask = wp.ones(self.num_joints, dtype=wp.bool, device=self.device) # Proxy arrays for the buffers self._joint_pos_target_ta = ProxyArray(self._joint_pos_target) self._joint_vel_target_ta = ProxyArray(self._joint_vel_target) self._joint_effort_target_ta = ProxyArray(self._joint_effort_target) self._joint_pos_target_sim_ta = ProxyArray(self._joint_pos_target_sim) self._joint_vel_target_sim_ta = ProxyArray(self._joint_vel_target_sim) self._joint_effort_target_sim_ta = ProxyArray(self._joint_effort_target_sim) self._computed_effort_ta = ProxyArray(self._computed_effort) self._applied_effort_ta = ProxyArray(self._applied_effort) def _resolve_group_joints( self, actuator_cfgs: dict[str, ActuatorBaseCfg] ) -> dict[str, tuple[ProxyArray, list[str]]]: """Resolve group selectors and reject joints assigned to multiple groups.""" resolved: dict[str, tuple[ProxyArray, list[str]]] = {} joint_owners: dict[str, str] = {} # Resolve exact joint names and indices for actuator_name, actuator_cfg in actuator_cfgs.items(): joint_ids, joint_names = self._control.find_joints(actuator_cfg.joint_names_expr) if len(joint_names) == 0: raise ValueError( f"No joints found for actuator group: {actuator_name} with joint name expression:" f" {actuator_cfg.joint_names_expr}." ) # Check for duplicate joint names for joint_name in joint_names: owner = joint_owners.get(joint_name) if owner is not None and owner != actuator_name: raise ValueError( f"Joint '{joint_name}' is assigned to multiple actuator groups: '{owner}' and" f" '{actuator_name}'." ) joint_owners[joint_name] = actuator_name resolved[actuator_name] = (joint_ids, joint_names) return resolved def _build_groups( self, actuator_cfgs: dict[str, ActuatorBaseCfg], resolved_group_joints: dict[str, tuple[list[int] | ProxyArray, list[str]]], ) -> None: """Construct actuator groups and apply their resolved joint properties. Newton-executed groups never instantiate an Isaac Lab actuator model: the Newton controllers own their parameters, so a Lab model would only hold misleading construction-time snapshots. Their mapping entries are filled with the owning Newton actuator objects after backend finalization. """ construction_records: list[tuple[dict[str, torch.Tensor], torch.Tensor | slice, bool, bool]] = [] for actuator_name, actuator_cfg in actuator_cfgs.items(): joint_ids, joint_names = resolved_group_joints[actuator_name] if len(joint_names) == self.num_joints: actuator_joint_ids: slice | torch.Tensor = slice(None) elif isinstance(joint_ids, ProxyArray): actuator_joint_ids = joint_ids.torch else: actuator_joint_ids = torch.tensor(joint_ids, device=self.device, dtype=torch.int32) self._group_joint_names[actuator_name] = joint_names self._group_joint_indices[actuator_name] = actuator_joint_ids joint_defaults = self._control.get_default_joint_properties(actuator_joint_ids) implicit = _is_implicit_actuator_cfg(actuator_cfg) self._has_implicit_actuators = self._has_implicit_actuators or implicit if implicit: self._implicit_group_names.add(actuator_name) native_managed = actuator_name in self._native_group_names properties, table_rows = self._resolve_joint_properties( actuator_cfg, joint_defaults, joint_names, actuator_joint_ids, ) if native_managed: # placeholder keeps configuration order; replaced by the Newton actuator # objects once the backend selection is finalized. self._groups[actuator_name] = None else: actuator_kwargs = dict( cfg=actuator_cfg, joint_names=joint_names, joint_ids=actuator_joint_ids, num_envs=self.num_instances, device=self.device, stiffness=properties["stiffness"], damping=properties["damping"], actuator_velocity_limit=properties["joint_velocity_limit"], ) if implicit: # implicit groups read the solver limit live after binding; the resolved value # seeds the pre-binding construction buffer. actuator_kwargs["joint_effort_limit"] = properties["joint_effort_limit"] else: # explicit models default their clip limit to the authored joint effort limit. actuator_kwargs["actuator_effort_limit"] = joint_defaults["joint_effort_limit"] self._groups[actuator_name] = actuator_cfg.class_type(**actuator_kwargs) if self._debug_value_resolution: self._joint_property_resolution_rows[actuator_name] = table_rows construction_records.append( ( properties, actuator_joint_ids, implicit, native_managed, ) ) for properties, joint_ids, implicit, native_managed in construction_records: self._control.write_resolved_joint_properties( properties, joint_ids, implicit=implicit, native_managed=native_managed, ) for actuator in self._groups.values(): if isinstance(actuator, ImplicitActuator): actuator._bind_actuator_parameters(self._control) def _resolve_newton_group_actuators(self, name: str) -> object: """Return the Newton actuator object(s) that drive one group's joints. Newton merges structurally identical joints into one actuator, so the returned object can be shared between groups. A single covering actuator is returned directly; a group spanning several returns them as a tuple. """ view = self._newton_selection.view columns = self._newton_group_columns(name) matched = [] for actuator in self._newton_selection.actuators: mapping = wp.to_torch(view._get_actuator_dof_mapping(actuator)) env_columns = mapping.reshape(self.num_instances, -1)[0] if bool((env_columns[columns] >= 0).any()): matched.append(actuator) if not matched: raise RuntimeError(f"No Newton actuator drives any joint of group '{name}'.") return matched[0] if len(matched) == 1 else tuple(matched) def _implicit_group_joint_indices(self) -> list[slice | torch.Tensor]: """Joint selectors of the implicit groups, consumed by backend implicit-DOF masks.""" return [self._group_joint_indices[name] for name in self._implicit_group_names] def _resolve_joint_properties( self, cfg: ActuatorBaseCfg, defaults: dict[str, torch.Tensor], joint_names: list[str], joint_ids: torch.Tensor | slice, ) -> tuple[dict[str, torch.Tensor], dict[str, tuple[tuple[object, ...], ...]]]: """Resolve fresh construction-only joint properties for one actuator group. The solver keeps the authored joint limits unless the configuration overrides them; explicit actuator models no longer widen the solver effort limit. """ values: dict[str, torch.Tensor] = {} resolution_rows: dict[str, tuple[tuple[object, ...], ...]] = {} for cfg_name in ( "stiffness", "damping", "armature", "friction", "dynamic_friction", "viscous_friction", "joint_effort_limit", "joint_velocity_limit", ): default_value = defaults[cfg_name] cfg_value = getattr(cfg, cfg_name) value = self._resolve_joint_property(cfg_value, default_value, joint_names) values[cfg_name] = value if self._debug_value_resolution: rows = self._joint_property_resolution_rows_for( cfg_value, value, default_value, joint_names, joint_ids, ) if rows: resolution_rows[cfg_name] = rows return values, resolution_rows def _resolve_joint_property( self, cfg_value: float | dict[str, float] | None, default_value: torch.Tensor, joint_names: list[str], ) -> torch.Tensor: """Resolve one group-shaped joint property from config and authored defaults.""" return resolve_joint_parameter(cfg_value, default_value, joint_names, self.num_instances, self.device) def _joint_property_resolution_rows_for( self, cfg_value: float | dict[str, float] | None, value: torch.Tensor, default_value: torch.Tensor, joint_names: list[str], joint_ids: torch.Tensor | slice, ) -> tuple[tuple[object, ...], ...]: """Formats joint property resolution rows for debugging. (Actuators property table output.)""" if cfg_value is not None and torch.allclose(value, default_value): return () if isinstance(joint_ids, slice): ids = range(self.num_joints) else: ids = tuple(int(joint_id) for joint_id in joint_ids.tolist()) return tuple( ( name, ids[index], float(default_value[0, index]), "Not Specified" if cfg_value is None else float(value[0, index]), float(default_value[0, index]) if cfg_value is None else float(value[0, index]), ) for index, name in enumerate(joint_names) ) # Execution planning and runtime. def _joint_indices_as_wp(self, actuator: ActuatorBase) -> wp.array(dtype=wp.int32): """Return an actuator group's joint indices as a Warp int32 array.""" if actuator.joint_indices == slice(None) or actuator.joint_indices is None: return self._all_joint_ids joint_indices = actuator.joint_indices if isinstance(joint_indices, wp.array): return joint_indices return wp.from_torch(joint_indices.to(self.device, dtype=torch.int32).contiguous(), dtype=wp.int32) def _joint_indices_as_torch(self, actuator: ActuatorBase) -> torch.Tensor: """Return an actuator group's joint indices as a contiguous Torch int32 tensor.""" if actuator.joint_indices == slice(None) or actuator.joint_indices is None: return torch.arange(self.num_joints, dtype=torch.int32, device=self.device) joint_indices = actuator.joint_indices if isinstance(joint_indices, wp.array): joint_indices = wp.to_torch(joint_indices) return joint_indices.to(self.device, dtype=torch.int32).contiguous() def _build_execution_plan(self) -> None: """Partition the actuator groups into the fused implicit executor and a per-group list. Plain :class:`~isaaclab.actuators.ImplicitActuator` groups only produce effort telemetry, so they are not executed one group at a time: their joint indices are aggregated while parsing and one executor computes all of them in a single fused kernel launch. Subclasses may override :meth:`~ActuatorBase.compute`, so they stay in the per-group list along with the explicit models. On a native actuator path the backend replaces :meth:`compute` entirely and the plan is left empty. """ self._execution_actuators: list[tuple[ActuatorBase, wp.array(dtype=wp.int32)]] = [] implicit_names: list[str] = [] implicit_groups: list[ImplicitActuator] = [] if self._control.native_actuator_path_active: self._implicit_executor = None return for name, group in self._groups.items(): if name in self._native_group_names: continue if type(group) is ImplicitActuator: implicit_names.append(name) implicit_groups.append(group) else: self._execution_actuators.append((group, self._joint_indices_as_wp(group))) self._implicit_executor = ( _ImplicitExecutor(self, tuple(implicit_names), tuple(implicit_groups)) if implicit_groups else None ) def _rebind_state_inputs(self) -> None: """Rebind the implicit executor after backend state storage is replaced. Per-group execution reads backend state through the control object on every :meth:`compute` call, so only the cached implicit launch holds state references that need rebinding. """ if self._implicit_executor is not None: self._implicit_executor.rebind(self) def _scatter_actuator_output( self, actuator: ActuatorBase, control_action: ArticulationActions, joint_indices: wp.array(dtype=wp.int32) | None = None, ) -> None: """Publish one explicit actuator's processed commands and telemetry.""" if joint_indices is None: joint_indices = self._joint_indices_as_wp(actuator) inputs = [ control_action.joint_positions, control_action.joint_velocities, control_action.joint_efforts, actuator.computed_effort, actuator.applied_effort, actuator.actuator_velocity_limit, joint_indices, ] outputs = [ self._joint_pos_target_sim, self._joint_vel_target_sim, self._joint_effort_target_sim, self._computed_effort, self._applied_effort, self._soft_joint_vel_limits, ] wp.launch( actuator_kernels.scatter_explicit_actuator_outputs, dim=(self.num_instances, joint_indices.shape[0]), inputs=inputs, outputs=outputs, device=self.device, ) # Diagnostics. def _validate_coverage(self) -> None: """Warn when actuator groups do not cover the expected movable joints.""" if self.num_joints == 0: return total_act_joints = sum(len(joint_names) for joint_names in self._group_joint_names.values()) expected_joints = self.num_joints - self._control.num_fixed_tendons if total_act_joints != expected_joints: logger.warning( "Actuator groups cover %s joints; expected %s after accounting for fixed tendons.", total_act_joints, expected_joints, ) def _print_value_resolution_table(self) -> None: """Log construction-time differences between authored and configured values.""" table = PrettyTable(["Group", "Property", "Name", "ID", "USD Value", "ActuatorCfg Value", "Applied"]) for actuator_group in self._groups: group_count = 0 for property_name, resolution_details in self._joint_property_resolution_rows[actuator_group].items(): for prop_idx, resolution_detail in enumerate(resolution_details): actuator_group_str = actuator_group if group_count == 0 else "" property_str = property_name if prop_idx == 0 else "" fmt = [f"{value:.2e}" if isinstance(value, float) else str(value) for value in resolution_detail] table.add_row([actuator_group_str, property_str, *fmt]) group_count += 1 logger.warning("\nActuatorCfg-USD Value Discrepancy Resolution (matching values are skipped): \n%s", table)
class _ImplicitExecutor: """Fused executor for the collection's plain :class:`ImplicitActuator` groups. A single group executes as itself. Multiple groups execute through one private shadow actuator covering the union of their joint indices, so the per-step cost is one kernel launch regardless of how the configuration partitions the joints. The logical groups' telemetry tensors are re-pointed at contiguous views of the shadow's buffers, so per-group reads observe the fused results directly. """ _cache_key_counter = itertools.count() """Monotonic launch-cache key source; ``id()`` keys could be reused after garbage collection.""" def __init__(self, collection: ActuatorCollection, names: tuple[str, ...], groups: tuple[ImplicitActuator, ...]): self._cache_key = ("implicit", next(_ImplicitExecutor._cache_key_counter)) self.group_names = names if len(groups) == 1: self.actuator = groups[0] joint_indices = collection._joint_indices_as_torch(groups[0]) else: joint_indices = torch.cat([collection._joint_indices_as_torch(group) for group in groups]) self.actuator = self._build_shadow_actuator(groups, joint_indices) self.joint_indices_wp = wp.from_torch(joint_indices, dtype=wp.int32) self.kernel_inputs: list[wp.array] | None = None self.kernel_outputs: list[wp.array] | None = None self._assemble_kernel_arrays(collection) @staticmethod def _build_shadow_actuator(groups: tuple[ImplicitActuator, ...], joint_indices: torch.Tensor) -> ImplicitActuator: """Build one private shadow actuator covering all the logical groups' joints. Retains the first group's config metadata; replaces the execution tensors below without cloning the logical groups' tensor storage. The groups' telemetry tensors become contiguous views of the shadow's buffers. """ shadow = copy.copy(groups[0]) shadow._joint_names = [name for group in groups for name in group.joint_names] shadow._joint_indices = joint_indices shadow.actuator_velocity_limit = torch.cat([group.actuator_velocity_limit for group in groups], dim=1) shadow.computed_effort = torch.zeros(shadow._num_envs, len(shadow._joint_names), device=shadow._device) shadow.applied_effort = torch.zeros_like(shadow.computed_effort) start = 0 for group in groups: group_slice = slice(start, start + group.num_joints) start += group.num_joints group.computed_effort = shadow.computed_effort[:, group_slice] group.applied_effort = shadow.applied_effort[:, group_slice] return shadow def _assemble_kernel_arrays(self, collection: ActuatorCollection) -> None: """Assemble the implicit kernel argument arrays. Existing argument lists are updated in place so holders of the list objects observe rebound backend state. """ control = collection._control inputs = [ collection._joint_pos_target, collection._joint_vel_target, collection._joint_effort_target, control.joint_pos.warp, control.joint_vel.warp, control.joint_stiffness.warp, control.joint_damping.warp, control.joint_effort_limits.warp, wp.from_torch(self.actuator.actuator_velocity_limit, dtype=wp.float32), self.joint_indices_wp, ] outputs = [ wp.from_torch(self.actuator.computed_effort, dtype=wp.float32), wp.from_torch(self.actuator.applied_effort, dtype=wp.float32), collection._joint_pos_target_sim, collection._joint_vel_target_sim, collection._joint_effort_target_sim, collection._computed_effort, collection._applied_effort, collection._soft_joint_vel_limits, ] if self.kernel_inputs is None: self.kernel_inputs = inputs self.kernel_outputs = outputs else: self.kernel_inputs[:] = inputs self.kernel_outputs[:] = outputs def launch(self, collection: ActuatorCollection) -> None: """Compute all the executor's joints through the cached Warp launch.""" collection._launch_cache.launch( self._cache_key, actuator_kernels.compute_implicit_actuator_batch, dim=(collection.num_instances, self.joint_indices_wp.shape[0]), inputs=self.kernel_inputs, outputs=self.kernel_outputs, ) def rebind(self, collection: ActuatorCollection) -> None: """Reassemble the kernel arguments after backend state storage is replaced.""" self._assemble_kernel_arrays(collection) collection._launch_cache.clear(self._cache_key)
[docs] class ActuatorTargetCommand: """Commands received by the actuator models. Position and velocity commands use joint-side coordinates. All command arrays are indexed by articulation joint, not by motor shaft. Index selectors must contain unique environment and joint indices. Repeated indices dispatch concurrent writes to the same destination and produce an undefined result. Deduplicate selectors or use mask setters. """
[docs] def __init__(self, collection: ActuatorCollection) -> None: """Initialize the command view. Args: collection: Owning actuator collection. """ self._collection = collection
@property def position(self) -> ProxyArray: """Desired positions [m or rad, depending on joint type].""" return self._collection._joint_pos_target_ta @property def velocity(self) -> ProxyArray: """Desired velocities [m/s or rad/s, depending on joint type].""" return self._collection._joint_vel_target_ta @property def effort(self) -> ProxyArray: """Effort commands [N or N·m, depending on joint type].""" return self._collection._joint_effort_target_ta
[docs] def set_position_index( self, *, value: torch.Tensor | wp.array(dtype=wp.float32), joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, full_data: bool = False, ) -> None: """Set desired positions using indices. Args: value: Desired positions [m or rad, depending on joint type]. Shape is ``(len(env_ids), len(joint_ids))``, or ``(num_instances, num_joints)`` when :paramref:`full_data` is true. joint_ids: Joint indices. Defaults to all joints. env_ids: Environment indices. Defaults to all environments. full_data: Whether :paramref:`value` is a full articulation command buffer. """ collection = self._collection env_ids_resolved = collection._control.resolve_env_ids(env_ids) joint_ids_resolved = collection._control.resolve_joint_ids(joint_ids) self._write_index_target( value, env_ids_resolved, joint_ids_resolved, collection._joint_pos_target, full_data=full_data, command_name="position", )
[docs] def set_velocity_index( self, *, value: torch.Tensor | wp.array(dtype=wp.float32), joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, full_data: bool = False, ) -> None: """Set desired velocities using indices. Args: value: Desired velocities [m/s or rad/s, depending on joint type]. Shape is ``(len(env_ids), len(joint_ids))``, or ``(num_instances, num_joints)`` when :paramref:`full_data` is true. joint_ids: Joint indices. Defaults to all joints. env_ids: Environment indices. Defaults to all environments. full_data: Whether :paramref:`value` is a full articulation command buffer. """ collection = self._collection env_ids_resolved = collection._control.resolve_env_ids(env_ids) joint_ids_resolved = collection._control.resolve_joint_ids(joint_ids) self._write_index_target( value, env_ids_resolved, joint_ids_resolved, collection._joint_vel_target, full_data=full_data, command_name="velocity", )
[docs] def set_effort_index( self, *, value: torch.Tensor | wp.array(dtype=wp.float32), joint_ids: Sequence[int] | torch.Tensor | wp.array | None = None, env_ids: Sequence[int] | torch.Tensor | wp.array | None = None, full_data: bool = False, ) -> None: """Set effort commands using indices. Args: value: Effort commands [N or N·m, depending on joint type]. Shape is ``(len(env_ids), len(joint_ids))``, or ``(num_instances, num_joints)`` when :paramref:`full_data` is true. joint_ids: Joint indices. Defaults to all joints. env_ids: Environment indices. Defaults to all environments. full_data: Whether :paramref:`value` is a full articulation command buffer. """ collection = self._collection env_ids_resolved = collection._control.resolve_env_ids(env_ids) joint_ids_resolved = collection._control.resolve_joint_ids(joint_ids) self._write_index_target( value, env_ids_resolved, joint_ids_resolved, collection._joint_effort_target, full_data=full_data, command_name="effort", )
[docs] def set_position_mask( self, *, value: torch.Tensor | wp.array(dtype=wp.float32), joint_mask: wp.array(dtype=wp.bool) | None = None, env_mask: wp.array(dtype=wp.bool) | None = None, ) -> None: """Set desired positions using masks. Args: value: Full articulation position commands [m or rad, depending on joint type]. Shape is ``(num_instances, num_joints)``. joint_mask: Joint selection mask. Defaults to all joints. env_mask: Environment selection mask. Defaults to all environments. """ collection = self._collection env_mask_resolved = self._resolve_mask(env_mask, collection._all_true_env_mask, "env_mask") joint_mask_resolved = self._resolve_mask(joint_mask, collection._all_true_joint_mask, "joint_mask") self._write_mask_target( value, env_mask_resolved, joint_mask_resolved, collection._joint_pos_target, command_name="position", )
[docs] def set_velocity_mask( self, *, value: torch.Tensor | wp.array(dtype=wp.float32), joint_mask: wp.array(dtype=wp.bool) | None = None, env_mask: wp.array(dtype=wp.bool) | None = None, ) -> None: """Set desired velocities using masks. Args: value: Full articulation velocity commands [m/s or rad/s, depending on joint type]. Shape is ``(num_instances, num_joints)``. joint_mask: Joint selection mask. Defaults to all joints. env_mask: Environment selection mask. Defaults to all environments. """ collection = self._collection env_mask_resolved = self._resolve_mask(env_mask, collection._all_true_env_mask, "env_mask") joint_mask_resolved = self._resolve_mask(joint_mask, collection._all_true_joint_mask, "joint_mask") self._write_mask_target( value, env_mask_resolved, joint_mask_resolved, collection._joint_vel_target, command_name="velocity", )
[docs] def set_effort_mask( self, *, value: torch.Tensor | wp.array(dtype=wp.float32), joint_mask: wp.array(dtype=wp.bool) | None = None, env_mask: wp.array(dtype=wp.bool) | None = None, ) -> None: """Set effort commands using masks. Args: value: Full articulation effort commands [N or N·m, depending on joint type]. Shape is ``(num_instances, num_joints)``. joint_mask: Joint selection mask. Defaults to all joints. env_mask: Environment selection mask. Defaults to all environments. """ collection = self._collection env_mask_resolved = self._resolve_mask(env_mask, collection._all_true_env_mask, "env_mask") joint_mask_resolved = self._resolve_mask(joint_mask, collection._all_true_joint_mask, "joint_mask") self._write_mask_target( value, env_mask_resolved, joint_mask_resolved, collection._joint_effort_target, command_name="effort", )
@staticmethod def _resolve_mask( mask: wp.array(dtype=wp.bool) | None, all_true_mask: wp.array(dtype=wp.bool), name: str ) -> wp.array(dtype=wp.bool): """Return the full selection mask for an optional ``wp.bool`` mask argument.""" if mask is None: return all_true_mask if not isinstance(mask, wp.array) or mask.dtype != wp.bool: raise TypeError(f"Expected '{name}' to be a wp.array of dtype wp.bool, got {type(mask)!r}.") return mask def _write_index_target( self, target: torch.Tensor | wp.array(dtype=wp.float32), env_ids: torch.Tensor | wp.array, joint_ids: torch.Tensor | wp.array, target_buffer: wp.array(dtype=wp.float32), *, full_data: bool, command_name: str, ) -> None: collection = self._collection expected_shape = ( (collection.num_instances, collection.num_joints) if full_data else (env_ids.shape[0], joint_ids.shape[0]) ) collection._control.assert_shape_and_dtype(target, expected_shape, wp.float32, "target") wp.launch( actuator_kernels.write_2d_float_with_indices_kernel(env_ids, joint_ids), dim=(env_ids.shape[0], joint_ids.shape[0]), inputs=[target, env_ids, joint_ids, full_data], outputs=[target_buffer], device=collection.device, ) collection._control.stage_user_command(command_name, collection, env_ids, joint_ids, None, None) def _write_mask_target( self, target: torch.Tensor | wp.array(dtype=wp.float32), env_mask: wp.array(dtype=wp.bool), joint_mask: wp.array(dtype=wp.bool), target_buffer: wp.array(dtype=wp.float32), *, command_name: str, ) -> None: collection = self._collection collection._control.assert_shape_and_dtype_mask(target, (env_mask, joint_mask), wp.float32, "target") wp.launch( actuator_kernels.write_2d_float_with_mask, dim=(env_mask.shape[0], joint_mask.shape[0]), inputs=[target, env_mask, joint_mask], outputs=[target_buffer], device=collection.device, ) collection._control.stage_user_command(command_name, collection, None, None, env_mask, joint_mask)
[docs] class ActuatorOutputCommand: """Processed commands produced for the simulated joints. These arrays contain submitted-command telemetry for Isaac Lab-managed actuator models. Native controllers bypass the arrays, so they do not provide submitted-command telemetry on a native path. """
[docs] def __init__(self, collection: ActuatorCollection) -> None: """Initialize the joint command view. Args: collection: Owning actuator collection. """ self._collection = collection
@property def position(self) -> ProxyArray: """Processed position commands [m or rad, depending on joint type].""" return self._collection._joint_pos_target_sim_ta @property def velocity(self) -> ProxyArray: """Processed velocity commands [m/s or rad/s, depending on joint type].""" return self._collection._joint_vel_target_sim_ta @property def effort(self) -> ProxyArray: """Processed effort commands [N or N·m, depending on joint type].""" return self._collection._joint_effort_target_sim_ta