IO 描述符 101#

在本教程中,我们将学习关于IO描述符的内容,它们是什么,如何导出它们,以及如何将它们添加到您的环境中。我们将以Anymal-D机器人为例进行演示,演示如何从环境中导出IO描述符,并使用我们自己的项演示如何将IO描述符附加到自定义动作和观测项。

什么是IO Descriptors?#

在深入研究IO描述符之前,让我们先了解它们是什么,以及它们如何有用。

IO 描述符是一种描述在 Isaac Lab 中使用 ManagerBasedRLEnv 训练的策略的输入和输出的方法。换句话说,它们描述了策略的动作和观测项。该描述用于生成一个 YAML 文件,可以在外部工具中加载,以便运行这些策略,而无需手动输入动作和观测项的配置。

除此之外,IO描述符提供以下信息: - articulation中所有关节的参数。 - 一些仿真参数,包括仿真时间步长和策略时间步长。 - 对于一些动作和观察项,它提供与动作/观察项中出现的关节名称或身体名称相同的顺序。 - 对于观察和动作项,它提供的项的顺序与管理器中出现的顺序完全一致。 使得可以轻松从YAML文件中重建它们。

以下是Anymal-D机器人根据IO描述符生成的YAML文件中动作部分的示例:

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

actions:
- action_type: JointAction
  clip: null
  dtype: torch.float32
  extras:
    description: Joint action term that applies the processed actions to the articulation's
      joints as position commands.
  full_path: isaaclab.envs.mdp.actions.joint_actions.JointPositionAction
  joint_names:
  - LF_HAA
  - LH_HAA
  - RF_HAA
  - RH_HAA
  - LF_HFE
  - LH_HFE
  - RF_HFE
  - RH_HFE
  - LF_KFE
  - LH_KFE
  - RF_KFE
  - RH_KFE
  mdp_type: Action
  name: joint_position_action
  offset:
  - 0.0
  - 0.0
  - 0.0
  - 0.0
  - 0.4000000059604645
  - -0.4000000059604645
  - 0.4000000059604645
  - -0.4000000059604645
  - -0.800000011920929
  - 0.800000011920929

以下是Anymal-D机器人根据IO描述符生成的YAML文件中观测部分的示例片段:

    - RH_HFE
    - LF_KFE
    - LH_KFE
    - RF_KFE
    - RH_KFE
observations:
  policy:
  - dtype: torch.float32
    extras:
      axes:
      - X
      - Y
      - Z
      description: Root linear velocity in the asset's root frame.
      modifiers: null
      units: m/s
    full_path: isaaclab.envs.mdp.observations.base_lin_vel
    mdp_type: Observation
    name: base_lin_vel
    observation_type: RootState
    overloads:
      clip: null
      flatten_history_dim: true
      history_length: 0
      scale: null
    shape:
    - 3
  - dtype: torch.float32
    extras:
      axes:
      - X
      - Y
      - Z
      description: Root angular velocity in the asset's root frame.
      modifiers: null
      units: rad/s
    full_path: isaaclab.envs.mdp.observations.base_ang_vel
    mdp_type: Observation
    name: base_ang_vel
    observation_type: RootState
    overloads:
      clip: null
      flatten_history_dim: true
      history_length: 0
      scale: null
    shape:
    - 3
  - dtype: torch.float32
    extras:
      description: 'The joint positions of the asset w.r.t. the default joint positions.
        Note: Only the joints configured in :attr:`asset_cfg.joint_ids` will have
        their positions returned.'
      modifiers: null
      units: rad
    full_path: isaaclab.envs.mdp.observations.joint_pos_rel
    joint_names:
    - LF_HAA
    - LH_HAA
    - RF_HAA
    - RH_HAA
    - LF_HFE
    - LH_HFE
    - RF_HFE
    - RH_HFE
    - LF_KFE
    - LH_KFE
    - RF_KFE
    - RH_KFE
    joint_pos_offsets:
    - 0.0
    - 0.0
    - 0.0
    - 0.0
    - 0.4000000059604645
    - -0.4000000059604645
    - 0.4000000059604645
    - -0.4000000059604645
    - -0.800000011920929
    - 0.800000011920929
    - -0.800000011920929
    - 0.800000011920929
    mdp_type: Observation
    name: joint_pos_rel
    observation_type: JointState
    overloads:
      clip: null

需要特别说明的是:动作和观测术语均以字典列表的形式返回,而非字典的字典。这种设计可确保术语顺序的严格保留。因此,用户需要通过字典中的 name 键来检索特定动作或观测术语。

例如,在以下代码段中,我们正在查看 projected_gravity 观测项。 name 键用于标识该项。 full_path 键用于提供到Isaac Lab源代码中用于计算该项的函数的明确路径。还提供了一些标志,如 mdp_typeobservation_type ,它们不具有任何功能影响。它们在这里是为了向用户通知此项属于哪个类别。

      flatten_history_dim: true
      history_length: 0
      scale: null
    shape:
    - 3
  - dtype: torch.float32
    extras:
      axes:
      - X
      - Y
      - Z
      description: Gravity projection on the asset's root frame.
      modifiers: null
      units: m/s^2
    full_path: isaaclab.envs.mdp.observations.projected_gravity
    mdp_type: Observation
    name: projected_gravity
    observation_type: RootState
    overloads:
      clip: null

从环境中导出IO描述符#

在这一节中,我们将讨论如何从环境中导出IO描述符。请记住,此功能仅适用于基于管理器的RL环境。

如果已经使用给定配置对策略进行了训练,那么可以使用以下方法导出IO描述符:

./isaaclab.sh -p scripts/environments/export_io_descriptors.py --task <task_name> --output_dir <output_dir>

例如,如果我们想要导出 Anymal-D 机器人的 IO 描述符,我们可以运行:

./isaaclab.sh -p scripts/environments/export_io_descriptors.py --task Isaac-Velocity-Flat-Anymal-D-v0 --output_dir ./io_descriptors

在训练策略时,也可以在训练开始时请求导出IO描述符。这可以通过在命令行中设置 export_io_descriptors 标志来实现。

./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task Isaac-Velocity-Flat-Anymal-D-v0 --export_io_descriptors
./isaaclab.sh -p scripts/reinforcement_learning/sb3/train.py --task Isaac-Velocity-Flat-Anymal-D-v0 --export_io_descriptors
./isaaclab.sh -p scripts/reinforcement_learning/rl_games/train.py --task Isaac-Velocity-Flat-Anymal-D-v0 --export_io_descriptors
./isaaclab.sh -p scripts/reinforcement_learning/skrl/train.py --task Isaac-Velocity-Flat-Anymal-D-v0 --export_io_descriptors

将IO描述符附加到自定义观察项#

在这个部分,我们将讨论如何将IO描述符附加到自定义观察项。

让我们看看如何将IO描述符附加到一个简单的观察项中:

@generic_io_descriptor(
   units="m/s", axes=["X", "Y", "Z"], observation_type="RootState", on_inspect=[record_shape, record_dtype]
)
def base_lin_vel(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor:
    """Root linear velocity in the asset's root frame."""
    # extract the used quantities (to enable type-hinting)
    asset: RigidObject = env.scene[asset_cfg.name]
    return asset.data.root_lin_vel_b

在这里,我们正在定义一个自定义观测项,称为 base_lin_vel ,用于计算机器人的root线速度。我们还将IO描述符附加到此项上。使用 @generic_io_descriptor 装饰器定义IO描述符。

@generic_io_descriptor 装饰器是一个特殊的装饰器,用于将 IO 描述符附加到自定义观测项。它接受任意参数,用于描述观测项,在这种情况下,我们提供了可能对最终用户有用的额外信息。:

  • 单位: 观测项的单位。

  • axes: 观测项的坐标轴。

  • observation_type: 观测项的类型。

您还会注意到提供了 on_inspect 参数。 这是用于检查观测项的函数列表。 在本例中,我们使用 record_shaperecord_dtype 函数来记录观测项输出的形状和数据类型。

这些函数的定义如下:

def record_shape(output: torch.Tensor, descriptor: GenericObservationIODescriptor, **kwargs) -> None:
    """Record the shape of the output tensor.

    Args:
       output: The output tensor.
       descriptor: The descriptor to record the shape to.
       **kwargs: Additional keyword arguments.
    """
    descriptor.shape = (output.shape[-1],)


def record_dtype(output: torch.Tensor, descriptor: GenericObservationIODescriptor, **kwargs) -> None:
    """Record the dtype of the output tensor.

    Args:
       output: The output tensor.
       descriptor: The descriptor to record the dtype to.
       **kwargs: Additional keyword arguments.
    """
    descriptor.dtype = str(output.dtype)

这些函数始终将观测项的输出张量作为第一个参数,描述符作为第二个参数。在 kwargs 中提供了观测项所有的输入。除了 on_inspect 函数外,装饰器还会在后台调用一些函数来收集观测项的 namedescriptionfull_path 。请注意,添加这个装饰器不会改变观测项的签名,因此可以安全地与观测管理器一起使用!

让我们现在来看一个更复杂的例子: 获取机器人的相对关节位置。

@generic_io_descriptor(
    observation_type="JointState",
    on_inspect=[record_joint_names, record_dtype, record_shape, record_joint_pos_offsets],
    units="rad",
)
def joint_pos_rel(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor:
    """The joint positions of the asset w.r.t. the default joint positions.

    Note: Only the joints configured in :attr:`asset_cfg.joint_ids` will have their positions returned.
    """
    # extract the used quantities (to enable type-hinting)
    asset: Articulation = env.scene[asset_cfg.name]
    return asset.data.joint_pos[:, asset_cfg.joint_ids] - asset.data.default_joint_pos[:, asset_cfg.joint_ids]

与上一个示例类似,我们正在向自定义观测项添加一个IO描述符,其中包含一组函数,用于检查观测项。

要获取关节的名称,我们可以编写以下函数:

def record_joint_names(output: torch.Tensor, descriptor: GenericObservationIODescriptor, **kwargs) -> None:
    """Record the joint names of the output tensor.

    Expects the `asset_cfg` keyword argument to be set.

    Args:
        output: The output tensor.
        descriptor: The descriptor to record the joint names to.
        **kwargs: Additional keyword arguments.
    """
    asset: Articulation = kwargs["env"].scene[kwargs["asset_cfg"].name]
    joint_ids = kwargs["asset_cfg"].joint_ids
    if joint_ids == slice(None, None, None):
        joint_ids = list(range(len(asset.joint_names)))
    descriptor.joint_names = [asset.joint_names[i] for i in joint_ids]

请注意,我们可以通过 kwargs 字典访问观测项的所有输入。因此,我们可以访问包含计算观测项的关节配置的 asset_cfg

要获得偏移量,我们可以编写以下函数:

def record_joint_pos_offsets(output: torch.Tensor, descriptor: GenericObservationIODescriptor, **kwargs):
 """Record the joint position offsets of the output tensor.

 Expects the `asset_cfg` keyword argument to be set.

 Args:
     output: The output tensor.
     descriptor: The descriptor to record the joint position offsets to.
     **kwargs: Additional keyword arguments.
 """
    asset: Articulation = kwargs["env"].scene[kwargs["asset_cfg"].name]
    ids = kwargs["asset_cfg"].joint_ids
    # Get the offsets of the joints for the first robot in the scene.
    # This assumes that all robots have the same joint offsets.
    descriptor.joint_pos_offsets = asset.data.default_joint_pos[:, ids][0]

有了这个想法,您现在应该能够将一个IO描述符附加到您自定义的观测项中! 但是,在我们结束本教程之前,让我们看一下如何将IO描述符附加到自定义动作项中。

将IO描述符附加到自定义操作项。#

在这个部分中,我们将介绍如何将IO描述符附加到自定义操作项中。操作项是从 managers.ActionTerm 类继承的类。要将IO描述符添加到操作项中,我们需要扩展其 ActionTerm.IO_descriptor() 属性。

默认情况下, ActionTerm.IO_descriptor() 属性返回基本描述符,并填充以下字段: - name: 动作项的名称。 - full_path: 动作项的完整路径。 - description: 动作项的描述。 - export: 是否导出动作项。

@property
def IO_descriptor(self) -> GenericActionIODescriptor:
    """The IO descriptor for the action term."""
    self._IO_descriptor.name = re.sub(r"([a-z])([A-Z])", r"\1_\2", self.__class__.__name__).lower()
    self._IO_descriptor.full_path = f"{self.__class__.__module__}.{self.__class__.__name__}"
    self._IO_descriptor.description = " ".join(self.__class__.__doc__.split())
    self._IO_descriptor.export = self.export_IO_descriptor
    return self._IO_descriptor

为了向描述符添加更多信息,我们需要重写 ActionTerm.IO_descriptor() 属性。让我们看一个示例,了解如何向描述符添加关节名称、scale、offset和clip。

@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, torch.Tensor):
        self._IO_descriptor.offset = self._offset[0].detach().cpu().numpy().tolist()
    else:
        self._IO_descriptor.offset = self._offset
    # FIXME: This is not correct. Add list support.
    if self.cfg.clip is not None:
        if isinstance(self._clip, torch.Tensor):
            self._IO_descriptor.clip = self._clip[0].detach().cpu().numpy().tolist()
        else:
            self._IO_descriptor.clip = self._clip
    else:
        self._IO_descriptor.clip = None
    return self._IO_descriptor

这就是了!您现在应该能够将IO描述符附加到自定义动作项上,这样就完成了本教程。