Source code for isaaclab_experimental.envs.mdp.actions.joint_actions

# 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

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

import numpy as np
import warp as wp

import isaaclab.utils.string as string_utils
from isaaclab.assets.articulation import Articulation

from isaaclab_experimental.managers.action_manager import ActionTerm
from isaaclab_experimental.utils.warp import resolve_1d_mask, zero_masked_2d

if TYPE_CHECKING:
    from isaaclab.envs import ManagerBasedEnv
    from isaaclab.envs.utils.io_descriptors import GenericActionIODescriptor

    from . import actions_cfg

# import logger
logger = logging.getLogger(__name__)


@wp.kernel
def _process_joint_actions_kernel(
    # input
    actions: wp.array(dtype=wp.float32, ndim=2),
    action_offset: int,
    # params
    scale: wp.array(dtype=wp.float32),
    offset: wp.array(dtype=wp.float32),
    clip: wp.array(dtype=wp.float32, ndim=2),
    # output
    raw_out: wp.array(dtype=wp.float32, ndim=2),
    processed_out: wp.array(dtype=wp.float32, ndim=2),
):
    env_id, j = wp.tid()
    col = action_offset + j

    a = actions[env_id, col]
    raw_out[env_id, j] = a

    x = a * scale[j] + offset[j]
    low = clip[j, 0]
    high = clip[j, 1]
    if x < low:
        x = low
    if x > high:
        x = high
    processed_out[env_id, j] = x


[docs] class JointAction(ActionTerm): r"""Base class for joint actions. This action term performs pre-processing of the raw actions using affine transformations (scale and offset). These transformations can be configured to be applied to a subset of the articulation's joints. Mathematically, the action term is defined as: .. math:: \text{action} = \text{offset} + \text{scaling} \times \text{input action} where :math:`\text{action}` is the action that is sent to the articulation's actuated joints, :math:`\text{offset}` is the offset applied to the input action, :math:`\text{scaling}` is the scaling applied to the input action, and :math:`\text{input action}` is the input action from the user. Based on above, this kind of action transformation ensures that the input and output actions are in the same units and dimensions. The child classes of this action term can then map the output action to a specific desired command of the articulation's joints (e.g. position, velocity, etc.). """ cfg: actions_cfg.JointActionCfg """The configuration of the action term.""" _asset: Articulation """The articulation asset on which the action term is applied.""" _scale: wp.array """The scaling factor applied to the input action.""" _offset: wp.array """The offset applied to the input action.""" _clip: wp.array """The clip applied to the input action.""" _joint_mask: wp.array """A persistent joint mask for capturable action application."""
[docs] def __init__(self, cfg: actions_cfg.JointActionCfg, env: ManagerBasedEnv) -> None: # initialize the action term super().__init__(cfg, env) # resolve the joints over which the action term is applied self._joint_ids, self._joint_names = self._asset.find_joints( self.cfg.joint_names, preserve_order=self.cfg.preserve_order ) self._num_joints = len(self._joint_ids) # log the resolved joint names for debugging logger.info( f"Resolved joint names for the action term {self.__class__.__name__}:" f" {self._joint_names} [{self._joint_ids}]" ) # Avoid indexing across all joints for efficiency if self._num_joints == self._asset.num_joints and not self.cfg.preserve_order: self._joint_ids = slice(None) # FIXME: ArticulationData.resolve_joint_mask is not available on this branch. # Port resolve_*_mask methods from dev/newton when articulation_data is aligned. _all_joint_mask = wp.ones((self._asset.num_joints,), dtype=wp.bool, device=self.device) _scratch_joint_mask = wp.zeros((self._asset.num_joints,), dtype=wp.bool, device=self.device) self._joint_mask = wp.clone( resolve_1d_mask( ids=self._joint_ids, mask=None, all_mask=_all_joint_mask, scratch_mask=_scratch_joint_mask, device=self.device, ) ) # create tensors for raw and processed actions (Warp) self._raw_actions = wp.zeros((self.num_envs, self.action_dim), dtype=wp.float32, device=self.device) self._processed_actions = wp.zeros_like(self.raw_actions) # FIXME: dev/newton set_joint_effort_target accepts partial data + joint_mask. Our branch # has separate _index (partial data) and _mask (full data) variants. Pre-compute joint_ids # as warp array for the _index variant. if self._joint_ids == slice(None): self._joint_ids_wp = None # None means all joints else: self._joint_ids_wp = wp.array(list(self._joint_ids), dtype=wp.int32, device=self.device) # parse scale if isinstance(cfg.scale, (float, int)): self._scale = wp.array([float(cfg.scale)] * self.action_dim, dtype=wp.float32, device=self.device) elif isinstance(cfg.scale, dict): scale_per_joint = [1.0] * self.action_dim # resolve the dictionary config index_list, _, value_list = string_utils.resolve_matching_names_values(self.cfg.scale, self._joint_names) for idx, value in zip(index_list, value_list): scale_per_joint[idx] = float(value) self._scale = wp.array(scale_per_joint, dtype=wp.float32, device=self.device) else: raise ValueError(f"Unsupported scale type: {type(cfg.scale)}. Supported types are float and dict.") # parse offset if isinstance(cfg.offset, (float, int)): self._offset = wp.array([float(cfg.offset)] * self.action_dim, dtype=wp.float32, device=self.device) elif isinstance(cfg.offset, dict): offset_per_joint = [0.0] * self.action_dim # resolve the dictionary config index_list, _, value_list = string_utils.resolve_matching_names_values(self.cfg.offset, self._joint_names) for idx, value in zip(index_list, value_list): offset_per_joint[idx] = float(value) self._offset = wp.array(offset_per_joint, dtype=wp.float32, device=self.device) else: raise ValueError(f"Unsupported offset type: {type(cfg.offset)}. Supported types are float and dict.") # parse clip clip_low = [-float("inf")] * self.action_dim clip_high = [float("inf")] * self.action_dim if self.cfg.clip is not None: if isinstance(cfg.clip, dict): index_list, _, value_list = string_utils.resolve_matching_names_values(self.cfg.clip, self._joint_names) for idx, value in zip(index_list, value_list): clip_low[idx] = float(value[0]) clip_high[idx] = float(value[1]) else: raise ValueError(f"Unsupported clip type: {type(cfg.clip)}. Supported types are dict.") clip_np = np.column_stack([clip_low, clip_high]).astype(np.float32) self._clip = wp.array(clip_np, dtype=wp.float32, device=self.device)
""" Properties. """ @property def action_dim(self) -> int: return self._num_joints @property def raw_actions(self) -> wp.array: return self._raw_actions @property def processed_actions(self) -> wp.array: return self._processed_actions @property def IO_descriptor(self) -> GenericActionIODescriptor: """The IO descriptor of the action term. This descriptor is used to describe the action term of the joint action. It adds the following information to the base descriptor: - joint_names: The names of the joints. - scale: The scale of the action term. - offset: The offset of the action term. - clip: The clip of the action term. Returns: The IO descriptor of the action term. """ super().IO_descriptor self._IO_descriptor.shape = (self.action_dim,) self._IO_descriptor.dtype = str(self.raw_actions.dtype) self._IO_descriptor.action_type = "JointAction" self._IO_descriptor.joint_names = self._joint_names self._IO_descriptor.scale = self._scale # This seems to be always [4xNum_joints] IDK why. Need to check. if isinstance(self._offset, wp.array): self._IO_descriptor.offset = self._offset.numpy().tolist() else: self._IO_descriptor.offset = None # FIXME: This is not correct. Add list support. if self.cfg.clip is not None: if isinstance(self._clip, wp.array): self._IO_descriptor.clip = self._clip.numpy().tolist() else: self._IO_descriptor.clip = None else: self._IO_descriptor.clip = None return self._IO_descriptor """ Operations. """ def process_actions(self, actions: wp.array, action_offset: int = 0): wp.launch( kernel=_process_joint_actions_kernel, dim=(self.num_envs, self.action_dim), inputs=[ actions, int(action_offset), self._scale, self._offset, self._clip, self._raw_actions, self._processed_actions, ], device=self.device, ) def reset(self, env_mask: wp.array | None = None) -> None: """Resets the action term (mask-based).""" if env_mask is None: self._raw_actions.fill_(0.0) return wp.launch( kernel=zero_masked_2d, dim=(self.num_envs, self.action_dim), inputs=[env_mask, self._raw_actions], device=self.device, )
[docs] class JointPositionAction(JointAction): """Joint action term that applies the processed actions to the articulation's joints as position commands. Warp-first override of :class:`isaaclab.envs.mdp.actions.JointPositionAction`. """ cfg: actions_cfg.JointPositionActionCfg """The configuration of the action term."""
[docs] def __init__(self, cfg: actions_cfg.JointPositionActionCfg, env: ManagerBasedEnv): super().__init__(cfg, env) # use default joint positions as offset if cfg.use_default_offset: defaults_np = self._asset.data.default_joint_pos.warp.numpy() if isinstance(self._joint_ids, slice): offset_vals = defaults_np[0, :].tolist() else: offset_vals = [float(defaults_np[0, jid]) for jid in self._joint_ids] self._offset = wp.array(offset_vals, dtype=wp.float32, device=self.device)
def apply_actions(self): self._asset.set_joint_position_target_index(target=self.processed_actions, joint_ids=self._joint_ids_wp)
[docs] class JointEffortAction(JointAction): """Joint action term that applies the processed actions to the articulation's joints as effort commands.""" cfg: actions_cfg.JointEffortActionCfg """The configuration of the action term."""
[docs] def __init__(self, cfg: actions_cfg.JointEffortActionCfg, env: ManagerBasedEnv): super().__init__(cfg, env)
def apply_actions(self): # set joint effort targets # FIXME: dev/newton uses set_joint_effort_target(data, joint_mask=) which accepts # partial data. Our branch uses the separate _index variant for partial data. self._asset.set_joint_effort_target_index(target=self.processed_actions, joint_ids=self._joint_ids_wp)