Source code for mani_skill.agents.controllers.pd_ee_pose

from dataclasses import dataclass, field
from typing import Literal, Optional, Sequence, Union

import numpy as np
import torch
from gymnasium import spaces

from mani_skill.agents.controllers.utils.kinematics import Kinematics
from mani_skill.utils import gym_utils, sapien_utils
from mani_skill.utils.geometry.rotation_conversions import (
    euler_angles_to_matrix,
    matrix_to_euler_angles,
    matrix_to_quaternion,
    quaternion_apply,
    quaternion_multiply,
    quaternion_to_matrix,
)
from mani_skill.utils.structs import Pose
from mani_skill.utils.structs.types import Array, DriveMode

from .base_controller import ControllerConfig
from .pd_joint_pos import PDJointPosController


[docs]class PDEEPosController(PDJointPosController): """The PD EE Position controller. NOTE that on the GPU it is assumed the controlled robot is not a merged articulation and is the same across every sub-scene"""
[docs] config: "PDEEPosControllerConfig"
[docs] _target_pose = None
[docs] def _check_gpu_sim_works(self): assert ( self.config.frame == "root_translation" ), "currently only translation in the root frame for EE control is supported in GPU sim"
[docs] def _initialize_joints(self): self.initial_qpos = None super()._initialize_joints() if self.device.type == "cuda": self._check_gpu_sim_works() self.kinematics = Kinematics( self.config.urdf_path, self.config.ee_link, self.articulation, self.active_joint_indices, ) self.ee_link = self.kinematics.end_link if self.config.root_link_name is not None: self.root_link = sapien_utils.get_obj_by_name( self.articulation.get_links(), self.config.root_link_name ) else: self.root_link = self.articulation.root
[docs] def _initialize_action_space(self): low = np.float32(np.broadcast_to(self.config.pos_lower, 3)) high = np.float32(np.broadcast_to(self.config.pos_upper, 3)) self.single_action_space = spaces.Box(low, high, dtype=np.float32)
@property
[docs] def ee_pos(self): return self.ee_link.pose.p
@property
[docs] def ee_pose(self): return self.ee_link.pose
@property
[docs] def ee_pose_at_base(self): to_base = self.root_link.pose.inv() return to_base * (self.ee_pose)
[docs] def reset(self): super().reset() if self.config.use_target: if self._target_pose is None: self._target_pose = self.ee_pose_at_base else: # TODO (stao): this is a strange way to mask setting individual batched pose parts self._target_pose.raw_pose[ self.scene._reset_mask ] = self.ee_pose_at_base.raw_pose[self.scene._reset_mask]
[docs] def compute_target_pose(self, prev_ee_pose_at_base, action): # Keep the current rotation and change the position if self.config.use_delta: delta_pose = Pose.create(action) if self.config.frame == "root_translation": target_pose = delta_pose * prev_ee_pose_at_base elif self.config.frame == "body_translation": target_pose = prev_ee_pose_at_base * delta_pose else: raise NotImplementedError(self.config.frame) else: assert self.config.frame == "root_translation", self.config.frame target_pose = Pose.create(action) return target_pose
[docs] def set_action(self, action: Array): action = self._preprocess_action(action) self._step = 0 self._start_qpos = self.qpos if self.config.use_target: prev_ee_pose_at_base = self._target_pose else: prev_ee_pose_at_base = self.ee_pose_at_base # we only need to use the target pose for CPU sim or if a virtual target is enabled # if we have no virtual target and using the gpu sim we can directly use the given action without # having to recompute the new target pose based on the action delta. ik_via_target_pose = self.config.use_target or not self.scene.gpu_sim_enabled if ik_via_target_pose: self._target_pose = self.compute_target_pose(prev_ee_pose_at_base, action) pos_only = type(self.config) == PDEEPosControllerConfig if pos_only: action = torch.hstack([action, torch.zeros(action.shape[0], 3, device=self.device)]) self._target_qpos = self.kinematics.compute_ik( pose=self._target_pose if ik_via_target_pose else action, q0=self.articulation.get_qpos(), is_delta_pose=not ik_via_target_pose and self.config.use_delta, current_pose=self.ee_pose_at_base, solver_config=self.config.delta_solver_config, ) if self._target_qpos is None: self._target_qpos = self._start_qpos if self.config.interpolate: self._step_size = (self._target_qpos - self._start_qpos) / self._sim_steps else: self.set_drive_targets(self._target_qpos)
[docs] def get_state(self) -> dict: if self.config.use_target: return {"target_pose": self._target_pose.raw_pose} return {}
[docs] def set_state(self, state: dict): if self.config.use_target: target_pose = state["target_pose"] self._target_pose = Pose.create_from_pq( target_pose[:, :3], target_pose[:, 3:] )
[docs] def __repr__(self): return f"{self.__class__.__name__}(dof={self.single_action_space.shape[0]}, active_joints={len(self.joints)}, end_link={self.config.ee_link}, joints=({', '.join([x.name for x in self.joints])}))"
# TODO (stao): This config should really inherit the pd joint pos controller config @dataclass
[docs]class PDEEPosControllerConfig(ControllerConfig):
[docs] pos_lower: Union[float, Sequence[float]]
"""Lower bound for position control. If a single float then X, Y, and Z rotations are bounded by this value. Otherwise can be three floats to specify each dimensions bounds"""
[docs] pos_upper: Union[float, Sequence[float]]
"""Upper bound for position control. If a single float then X, Y, and Z rotations are bounded by this value. Otherwise can be three floats to specify each dimensions bounds""" # TODO (stao): note stiffness, damping, force limit and friction are properties used by PDJointPos controller, which the PDEEPosController controller inherits from # this should be changed as its difficult to figure out how this code is used
[docs] stiffness: Union[float, Sequence[float]]
[docs] damping: Union[float, Sequence[float]]
[docs] force_limit: Union[float, Sequence[float]] = 1e10
[docs] friction: Union[float, Sequence[float]] = 0.0
"""The name of the end-effector link to control. Note that it does not have to be a end-effector necessarily and could just be any link."""
[docs] urdf_path: str = None
"""Path to the URDF file defining the robot to control."""
[docs] frame: Literal[ "body_translation", "root_translation", ] = "root_translation"
"""Choice of frame to use for translational and rotational control of the end-effector. To learn how these work explicitly with videos of each one's behavior see https://maniskill.readthedocs.io/en/latest/user_guide/concepts/controllers.html#pd-ee-end-effector-pose""" """Optionally set different root link for root translation control (e.g. if root is different than base)"""
[docs] use_delta: bool = True
"""Whether to use delta-action control. If true then actions indicate the delta/change in position via translation and orientation via rotation. If false, then actions indicate in the base frame (typically wherever the root link of the robot is) what pose the end effector should try and reach via inverse kinematics. """
[docs] use_target: bool = False
"""Whether to use the most recent target end-effector pose for control. If false, actions taken in a chosen frame will be taken relative to the instantaneous/current end-effector pose. """
[docs] interpolate: bool = False
[docs] normalize_action: bool = True
"""Whether to normalize each action dimension into a range of [-1, 1]. Normally for most machine learning workflows this is recommended to be kept true."""
[docs] delta_solver_config: dict = field( default_factory=lambda: dict(type="levenberg_marquardt", alpha=1.0) )
"""Configuration for the delta IK solver. Default is `dict(type="levenberg_marquardt", alpha=1.0)`. type can be one of "levenberg_marquardt" or "pseudo_inverse". alpha is a scaling term applied to the delta joint positions generated by the solver. Generally levenberg_marquardt is faster and more accurate than pseudo_inverse and is the recommended option, see https://github.com/haosulab/ManiSkill/issues/955#issuecomment-2742253342 for some analysis on performance."""
[docs] drive_mode: Union[Sequence[DriveMode], DriveMode] = "force"
[docs] controller_cls = PDEEPosController
[docs]class PDEEPoseController(PDEEPosController):
[docs] config: "PDEEPoseControllerConfig"
[docs] def _check_gpu_sim_works(self): assert ( self.config.frame == "root_translation:root_aligned_body_rotation" ), "currently only translation in the root frame for EE control is supported in GPU sim"
[docs] def _initialize_action_space(self): low = np.float32( np.hstack( [ np.broadcast_to(self.config.pos_lower, 3), np.broadcast_to(self.config.rot_lower, 3), ] ) ) high = np.float32( np.hstack( [ np.broadcast_to(self.config.pos_upper, 3), np.broadcast_to(self.config.rot_upper, 3), ] ) ) self.single_action_space = spaces.Box(low, high, dtype=np.float32)
[docs] def _clip_and_scale_action(self, action): # NOTE(xiqiang): rotation should be clipped by norm. pos_action = gym_utils.clip_and_scale_action( action[:, :3], self.action_space_low[:3], self.action_space_high[:3] ) # need to clone here to avoid in place modification of the original action data rot_action = action[:, 3:].clone() rot_norm = torch.linalg.norm(rot_action, axis=1) rot_action[rot_norm > 1] = torch.mul(rot_action, 1 / rot_norm[:, None])[ rot_norm > 1 ] rot_action = rot_action * self.config.rot_lower return torch.hstack([pos_action, rot_action])
[docs] def compute_target_pose(self, prev_ee_pose_at_base: Pose, action): if self.config.use_delta: delta_pos, delta_rot = action[:, 0:3], action[:, 3:6] delta_quat = matrix_to_quaternion(euler_angles_to_matrix(delta_rot, "XYZ")) delta_pose = Pose.create_from_pq(delta_pos, delta_quat) if "root_aligned_body_rotation" in self.config.frame: q = quaternion_multiply(delta_pose.q, prev_ee_pose_at_base.q) if "body_aligned_body_rotation" in self.config.frame: q = quaternion_multiply(prev_ee_pose_at_base.q, delta_pose.q) if "root_translation" in self.config.frame: p = prev_ee_pose_at_base.p + delta_pos if "body_translation" in self.config.frame: p = prev_ee_pose_at_base.p + quaternion_apply( prev_ee_pose_at_base.q, delta_pose.p ) target_pose = Pose.create_from_pq(p, q) else: assert ( self.config.frame == "root_translation:root_aligned_body_rotation" ), self.config.frame target_pos, target_rot = action[:, 0:3], action[:, 3:6] target_quat = matrix_to_quaternion( euler_angles_to_matrix(target_rot, "XYZ") ) target_pose = Pose.create_from_pq(target_pos, target_quat) return target_pose
@dataclass
[docs]class PDEEPoseControllerConfig(PDEEPosControllerConfig):
[docs] rot_lower: Union[float, Sequence[float]] = -2 * np.pi
"""Lower bound for rotation control. If a single float then X, Y, and Z rotations are bounded by this value. Otherwise can be three floats to specify each dimensions bounds"""
[docs] rot_upper: Union[float, Sequence[float]] = 2 * np.pi
"""Upper bound for rotation control. If a single float then X, Y, and Z rotations are bounded by this value. Otherwise can be three floats to specify each dimensions bounds"""
[docs] stiffness: Union[float, Sequence[float]] = None
[docs] damping: Union[float, Sequence[float]] = None
[docs] force_limit: Union[float, Sequence[float]] = 1e10
[docs] friction: Union[float, Sequence[float]] = 0.0
[docs] frame: Literal[ "body_translation:root_aligned_body_rotation", "root_translation:root_aligned_body_rotation", "body_translation:body_aligned_body_rotation", "root_translation:body_aligned_body_rotation", ] = "root_translation:root_aligned_body_rotation"
"""Choice of frame to use for translational and rotational control of the end-effector. To learn how these work explicitly with videos of each one's behavior see https://maniskill.readthedocs.io/en/latest/user_guide/concepts/controllers.html#pd-ee-end-effector-pose"""
[docs] controller_cls = PDEEPoseController