Source code for isaaclab.envs.mdp.commands.pose_command
# 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
"""Sub-module containing command generators for pose tracking."""
from __future__ import annotations
from collections.abc import Sequence
from typing import TYPE_CHECKING
import torch
from isaaclab.assets import Articulation
from isaaclab.managers import CommandTerm
from isaaclab.markers import VisualizationMarkers
from isaaclab.utils.leapp import POSE7_ELEMENT_NAMES
from isaaclab.utils.math import combine_frame_transforms, compute_pose_error, quat_from_euler_xyz, quat_unique
if TYPE_CHECKING:
from isaaclab.envs import ManagerBasedEnv
from .commands_cfg import UniformPoseCommandCfg
[docs]
class UniformPoseCommand(CommandTerm):
"""Command generator for generating pose commands uniformly.
The command generator generates poses by sampling positions uniformly within specified
regions in cartesian space. For orientation, it samples uniformly the euler angles
(roll-pitch-yaw) and converts them into quaternion representation (x, y, z, w).
The position and orientation commands are generated in the base frame of the robot, and not the
simulation world frame. This means that users need to handle the transformation from the
base frame to the simulation world frame themselves.
.. caution::
Sampling orientations uniformly is not strictly the same as sampling euler angles uniformly.
This is because rotations are defined by 3D non-Euclidean space, and the mapping
from euler angles to rotations is not one-to-one.
"""
cfg: UniformPoseCommandCfg
"""Configuration for the command generator."""
[docs]
def __init__(self, cfg: UniformPoseCommandCfg, env: ManagerBasedEnv):
"""Initialize the command generator class.
Args:
cfg: The configuration parameters for the command generator.
env: The environment object.
"""
# initialize the base class
super().__init__(cfg, env)
# extract the robot and body index for which the command is generated
self.robot: Articulation = env.scene[cfg.asset_name]
self.body_idx = self.robot.find_bodies(cfg.body_name)[0][0]
# create buffers
# -- commands: (x, y, z, qx, qy, qz, qw) in root frame
self.pose_command_b = torch.zeros(self.num_envs, 7, device=self.device)
self.pose_command_b[:, 3] = 1.0
self.pose_command_w = torch.zeros_like(self.pose_command_b)
# -- metrics
self.metrics["position_error"] = torch.zeros(self.num_envs, device=self.device)
self.metrics["orientation_error"] = torch.zeros(self.num_envs, device=self.device)
# -- per-episode sticky success bit (only used when at least one success threshold is set)
self._track_success = (
cfg.position_success_threshold is not None or cfg.orientation_success_threshold is not None
)
if self._track_success:
self._succeeded = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device)
# adds (optional) cmd kind and element names for leapp export
# during export, semantic data about this command will be used to annotate the command input
self.cfg.cmd_kind = self.cfg.cmd_kind or "command/body/pose"
self.cfg.element_names = self.cfg.element_names or POSE7_ELEMENT_NAMES
def __str__(self) -> str:
msg = "UniformPoseCommand:\n"
msg += f"\tCommand dimension: {tuple(self.command.shape[1:])}\n"
msg += f"\tResampling time range: {self.cfg.resampling_time_range}\n"
return msg
"""
Properties
"""
@property
def command(self) -> torch.Tensor:
"""The desired pose command. Shape is (num_envs, 7).
The first three elements correspond to the position, followed by the quaternion orientation in (x, y, z, w).
"""
return self.pose_command_b
def compute_success(self) -> torch.Tensor:
"""Compute whether the current body pose satisfies the configured success thresholds.
Successful entries are also recorded by the episode-level success tracker when enabled.
Returns:
A boolean tensor indicating which environments satisfy every configured threshold.
If no success threshold is configured, all entries are false.
"""
position_error, orientation_error = self._compute_error()
success = self._compute_success(position_error, orientation_error)
if self._track_success:
self._succeeded |= success
return success
"""
Implementation specific functions.
"""
def _update_metrics(self):
position_error, orientation_error = self._compute_error()
self.metrics["position_error"] = position_error
self.metrics["orientation_error"] = orientation_error
if self._track_success:
self._succeeded |= self._compute_success(position_error, orientation_error)
def _compute_error(self) -> tuple[torch.Tensor, torch.Tensor]:
# transform command from base frame to simulation world frame
self.pose_command_w[:, :3], self.pose_command_w[:, 3:] = combine_frame_transforms(
self.robot.data.root_pos_w.torch,
self.robot.data.root_quat_w.torch,
self.pose_command_b[:, :3],
self.pose_command_b[:, 3:],
)
# compute the error
pos_error, rot_error = compute_pose_error(
self.pose_command_w[:, :3],
self.pose_command_w[:, 3:],
self.robot.data.body_pos_w.torch[:, self.body_idx],
self.robot.data.body_quat_w.torch[:, self.body_idx],
)
return torch.linalg.norm(pos_error, dim=-1), torch.linalg.norm(rot_error, dim=-1)
def _compute_success(self, position_error: torch.Tensor, orientation_error: torch.Tensor) -> torch.Tensor:
success = torch.ones(self.num_envs, dtype=torch.bool, device=self.device)
if self.cfg.position_success_threshold is not None:
success &= position_error < self.cfg.position_success_threshold
if self.cfg.orientation_success_threshold is not None:
success &= orientation_error < self.cfg.orientation_success_threshold
if not self._track_success:
success[:] = False
return success
def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, float]:
extras = super().reset(env_ids)
if self._track_success:
if env_ids is None:
env_ids = slice(None)
# Write the unified ``Metrics/success_rate`` directly to env extras so it shares
# a TensorBoard card with the same metric from other tasks.
self._env.extras.setdefault("log", {})["Metrics/success_rate"] = (
self._succeeded[env_ids].float().mean().item()
)
self._succeeded[env_ids] = False
return extras
def _resample_command(self, env_ids: Sequence[int]):
# sample new pose targets
# -- position
r = torch.empty(len(env_ids), device=self.device)
self.pose_command_b[env_ids, 0] = r.uniform_(*self.cfg.ranges.pos_x)
self.pose_command_b[env_ids, 1] = r.uniform_(*self.cfg.ranges.pos_y)
self.pose_command_b[env_ids, 2] = r.uniform_(*self.cfg.ranges.pos_z)
# -- orientation
euler_angles = torch.zeros_like(self.pose_command_b[env_ids, :3])
euler_angles[:, 0].uniform_(*self.cfg.ranges.roll)
euler_angles[:, 1].uniform_(*self.cfg.ranges.pitch)
euler_angles[:, 2].uniform_(*self.cfg.ranges.yaw)
quat = quat_from_euler_xyz(euler_angles[:, 0], euler_angles[:, 1], euler_angles[:, 2])
# make sure the quaternion has real part as positive
self.pose_command_b[env_ids, 3:] = quat_unique(quat) if self.cfg.make_quat_unique else quat
def _update_command(self):
pass
def _set_debug_vis_impl(self, debug_vis: bool):
# create markers if necessary for the first time
if debug_vis:
if not hasattr(self, "goal_pose_visualizer"):
# -- goal pose
self.goal_pose_visualizer = VisualizationMarkers(self.cfg.goal_pose_visualizer_cfg)
# -- current body pose
self.current_pose_visualizer = VisualizationMarkers(self.cfg.current_pose_visualizer_cfg)
# set their visibility to true
self.goal_pose_visualizer.set_visibility(True)
self.current_pose_visualizer.set_visibility(True)
else:
if hasattr(self, "goal_pose_visualizer"):
self.goal_pose_visualizer.set_visibility(False)
self.current_pose_visualizer.set_visibility(False)
def _debug_vis_callback(self, event):
# check if robot is initialized
# note: this is needed in-case the robot is de-initialized. we can't access the data
if not self.robot.is_initialized:
return
# update the markers
environment_ids = self._env.scene._ALL_INDICES
# -- goal pose
self.goal_pose_visualizer.visualize(
self.pose_command_w[:, :3],
self.pose_command_w[:, 3:],
environment_ids=environment_ids,
)
# -- current body pose
body_link_pose_w = self.robot.data.body_link_pose_w.torch[:, self.body_idx]
self.current_pose_visualizer.visualize(
body_link_pose_w[:, :3],
body_link_pose_w[:, 3:7],
environment_ids=environment_ids,
)