Files
vllm-omni/vllm_omni/diffusion/models/gr00t/dataio/state_action/pose.py
T
2026-06-20 11:45:26 +08:00

710 lines
23 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from enum import Enum
from typing import TypeVar
import numpy as np
from numpy.typing import NDArray
from scipy.spatial.transform import Rotation
from vllm_omni.diffusion.models.gr00t.dataio.types import ActionFormat
# TypeVar for self-type preservation in Pose operations
PoseT = TypeVar("PoseT", bound="Pose")
def invert_transformation(transform: NDArray[np.float64]) -> NDArray[np.float64]:
"""
Invert a homogeneous transformation matrix.
Args:
transform: A 4x4 homogeneous transformation matrix
Returns:
The inverse of the transformation matrix (4x4)
"""
R = transform[:3, :3] # Extract the rotation matrix
t = transform[:3, 3] # Extract the translation vector
# Inverse of the rotation matrix is its transpose (since it's orthogonal)
R_inv = R.T
# Inverse of the translation is -R_inv * t
t_inv = -R_inv @ t
# Construct the inverse transformation matrix
T_inv = np.eye(4)
T_inv[:3, :3] = R_inv
T_inv[:3, 3] = t_inv
return T_inv
def relative_transformation(
base_transform: NDArray[np.float64], target_transform: NDArray[np.float64]
) -> NDArray[np.float64]:
"""
Compute the relative transformation between two poses.
Args:
base_transform: Initial 4x4 homogeneous transformation matrix
target_transform: Current 4x4 homogeneous transformation matrix
Returns:
The relative transformation matrix (4x4) from base_transform to target_transform
"""
# Relative transformation is base_transform^{-1} * target_transform
T_relative = invert_transformation(base_transform) @ target_transform
return T_relative
class RotationType(Enum):
"""Supported rotation representation types"""
QUAT = "quat"
EULER = "euler"
ROTVEC = "rotvec"
MATRIX = "matrix"
ROT6D = "rot6d"
class EulerOrder(Enum):
"""Common Euler angle conventions"""
XYZ = "xyz"
ZYX = "zyx"
XZY = "xzy"
YXZ = "yxz"
YZX = "yzx"
ZXY = "zxy"
class QuatOrder(Enum):
"""Quaternion ordering conventions"""
WXYZ = "wxyz" # scalar-first (w, x, y, z)
XYZW = "xyzw" # scalar-last (x, y, z, w)
class Pose:
"""
Abstract base class for robot poses.
This class provides common functionality for different pose representations
including relative pose computation via the subtraction operator.
"""
pose_type: str
def __sub__(self: PoseT, other: PoseT) -> PoseT:
"""
Compute relative transformation between two poses.
For EndEffectorPose: Computes the relative transformation from other to self.
Result represents the transformation needed to go from other's frame to self's frame.
For JointPose: Computes the joint-space difference (self - other).
Args:
other: The reference pose to compute relative transformation from
Returns:
Relative pose (same type as self)
Raises:
TypeError: If poses are not of the same type
Examples:
# End-effector poses
pose1 = EndEffectorPose(translation=[1, 0, 0], rotation=[1,0,0,0],
rotation_type="quat", rotation_order="wxyz")
pose2 = EndEffectorPose(translation=[2, 0, 0], rotation=[1,0,0,0],
rotation_type="quat", rotation_order="wxyz")
relative = pose2 - pose1 # Transformation from pose1 to pose2
# Joint poses
joint1 = JointPose([0.0, 0.5, 1.0])
joint2 = JointPose([0.1, 0.6, 1.2])
joint_diff = joint2 - joint1 # Joint differences: [0.1, 0.1, 0.2]
"""
if type(self) is not type(other):
raise TypeError(
f"Cannot compute relative transformation between different pose types: "
f"{type(self).__name__} and {type(other).__name__}"
)
return self._compute_relative(other)
def _compute_relative(self: PoseT, other: PoseT) -> PoseT:
"""
Internal method to compute relative transformation.
Must be implemented by subclasses.
Args:
other: The reference pose
Returns:
Relative pose
"""
raise NotImplementedError("Subclasses must implement _compute_relative")
def copy(self: PoseT) -> PoseT:
"""
Create a deep copy of this pose.
Must be implemented by subclasses.
Returns:
New Pose instance with copied data
"""
raise NotImplementedError("Subclasses must implement copy")
class JointPose(Pose):
"""
Represents a robot configuration in joint space.
This class stores joint angles/positions for a robot manipulator.
Unlike end-effector poses, joint poses represent the configuration
of all joints in the kinematic chain.
Examples:
# Create a 6-DOF joint configuration
joint_pose = JointPose(
joints=[0.0, -np.pi/4, np.pi/2, 0.0, np.pi/4, 0.0],
joint_names=["shoulder_pan", "shoulder_lift", "elbow",
"wrist_1", "wrist_2", "wrist_3"]
)
# Create with default joint names
joint_pose = JointPose(joints=[0.0, 0.5, 1.0])
# Get as dictionary
joint_dict = joint_pose.to_dict() # {"joint_0": 0.0, ...}
# Access individual joints
first_joint = joint_pose.joints[0]
num_joints = joint_pose.num_joints
# Compute relative joint displacement
joint1 = JointPose([0.0, 0.5, 1.0])
joint2 = JointPose([0.1, 0.6, 1.2])
relative = joint2 - joint1 # [0.1, 0.1, 0.2]
"""
pose_type = "joint"
def __init__(
self,
joints: list | np.ndarray,
joint_names: list | None = None,
):
"""
Initialize a joint pose.
Args:
joints: Joint angles/positions as array-like of shape (n,)
joint_names: Optional list of names for each joint. If None,
defaults to ["joint_0", "joint_1", ...]
"""
super().__init__()
self.joints = np.array(joints, dtype=np.float64)
# Set defaults and validate joint_names
if joint_names is None:
self.joint_names = [f"joint_{i}" for i in range(len(self.joints))]
else:
if len(joint_names) != len(self.joints):
raise ValueError(
f"Number of joint names ({len(joint_names)}) must match number of joints ({len(self.joints)})"
)
self.joint_names = joint_names
@property
def num_joints(self) -> int:
"""
Get the number of joints.
Returns:
Number of joints in the configuration
"""
return len(self.joints)
def to_dict(self) -> dict:
"""
Convert joint configuration to dictionary.
Returns:
Dictionary mapping joint names to joint values
"""
return dict(zip(self.joint_names, self.joints))
def _compute_relative(self, other): # type: ignore[override]
"""
Compute relative joint displacement.
Args:
other: Reference joint pose
Returns:
JointPose representing the joint-space difference (self - other)
Raises:
ValueError: If joint dimensions don't match
"""
if len(self.joints) != len(other.joints):
raise ValueError(
f"Cannot compute relative joint pose: "
f"joint dimensions don't match ({len(self.joints)} vs {len(other.joints)})"
)
relative_joints = self.joints - other.joints
return JointPose(joints=relative_joints, joint_names=self.joint_names)
def copy(self) -> "JointPose":
"""
Create a deep copy of this joint pose.
Returns:
New JointPose instance with copied data
"""
return JointPose(
joints=self.joints.copy(),
joint_names=self.joint_names.copy(),
)
def __repr__(self) -> str:
if len(self.joints) <= 6:
joints_str = np.array2string(self.joints, precision=4, suppress_small=True)
else:
joints_str = f"[{self.joints[0]:.4f}, ..., {self.joints[-1]:.4f}] ({len(self.joints)} joints)"
return f"JointPose(joints={joints_str})"
def __eq__(self, other) -> bool:
if not isinstance(other, JointPose):
return False
return np.allclose(self.joints, other.joints) and self.joint_names == other.joint_names
def __getitem__(self, index) -> float | NDArray[np.float64]:
"""Allow indexing: joint_pose[0] returns first joint value"""
return self.joints[index]
def __len__(self) -> int:
"""Allow len(): len(joint_pose) returns number of joints"""
return len(self.joints)
class EndEffectorPose(Pose):
"""
Represents a single end-effector pose with translation and rotation components.
This class handles Cartesian space representations of robot end-effector poses,
supporting multiple rotation representations (quaternions, Euler angles, rotation
vectors, rotation matrices, etc.).
Examples:
# Create with quaternion (wxyz order)
pose = EndEffectorPose(
translation=[1.0, 2.0, 3.0],
rotation=[1.0, 0.0, 0.0, 0.0],
rotation_type="quat",
rotation_order="wxyz"
)
# Create with Euler angles (degrees by default)
pose = EndEffectorPose(
translation=[1, 2, 3],
rotation=[0, 0, 90],
rotation_type="euler",
rotation_order="xyz"
)
# Create with Euler angles in radians
pose = EndEffectorPose(
translation=[1, 2, 3],
rotation=[0, 0, np.pi/2],
rotation_type="euler",
rotation_order="xyz",
degrees=False
)
# Create from homogeneous matrix
H = np.eye(4)
H[:3, 3] = [1, 2, 3]
pose = EndEffectorPose(homogeneous=H)
# Convert between representations
quat_wxyz = pose.to_rotation("quat", "wxyz")
euler_zyx = pose.to_rotation("euler", "zyx")
rot6d = pose.to_rotation("rot6d")
# Compute relative transformation
pose1 = EndEffectorPose(translation=[1, 0, 0], rotation=[1,0,0,0],
rotation_type="quat", rotation_order="wxyz")
pose2 = EndEffectorPose(translation=[2, 0, 0], rotation=[1,0,0,0],
rotation_type="quat", rotation_order="wxyz")
relative = pose2 - pose1 # Transformation from pose1's frame to pose2's frame
"""
pose_type = "end_effector"
def __init__(
self,
translation: list | np.ndarray | None = None,
rotation: list | np.ndarray | None = None,
rotation_type: str | None = None,
rotation_order: str | None = None,
homogeneous: np.ndarray | None = None,
degrees: bool = True,
):
"""
Initialize an end-effector pose.
Args:
translation: Translation vector [x, y, z]
rotation: Rotation in specified format
rotation_type: Type of rotation ("quat", "euler", "rotvec", "matrix", "rot6d")
rotation_order: Order/convention for the rotation type
homogeneous: Homogeneous transformation matrix (4, 4)
If provided, overrides translation and rotation
degrees: For Euler angles, whether the input is in degrees (default True)
"""
super().__init__()
# Cache for homogeneous matrix
self._homogeneous_cache: NDArray[np.float64] | None = None
self._cache_valid = False
# Handle homogeneous matrix input
if homogeneous is not None:
self._from_homogeneous(homogeneous)
return
# Store translation
self._translation = np.array(translation) if translation is not None else np.zeros(3)
# Store rotation as scipy Rotation object internally
if rotation is not None:
if rotation_type is None:
raise ValueError("rotation_type must be specified when rotation is provided")
self._set_rotation(rotation, rotation_type, rotation_order, degrees)
else:
self._rotation = Rotation.identity()
def _from_homogeneous(self, homogeneous: np.ndarray):
"""Initialize from homogeneous transformation matrix"""
homogeneous = np.array(homogeneous)
# Extract translation (last column, first 3 rows)
self._translation = homogeneous[:3, 3]
# Extract rotation matrix (top-left 3x3)
rotation_matrix = homogeneous[:3, :3]
# Create Rotation object from matrix
self._rotation = Rotation.from_matrix(rotation_matrix)
@staticmethod
def _rot6d_to_matrix(rot6d: np.ndarray) -> np.ndarray:
"""
Convert 6D rotation representation to rotation matrix.
Args:
rot6d: 6D rotation as (6,) array - first two rows of rotation matrix flattened
Returns:
Rotation matrix (3, 3)
"""
rot6d = rot6d.reshape(2, 3)
# First two rows
row1 = rot6d[0]
row2 = rot6d[1]
# Normalize first row
row1 = row1 / np.linalg.norm(row1)
# Gram-Schmidt orthogonalization for second row
row2 = row2 - np.dot(row1, row2) * row1
row2 = row2 / np.linalg.norm(row2)
# Third row is cross product
row3 = np.cross(row1, row2)
# Construct rotation matrix
rotation_matrix = np.vstack([row1, row2, row3])
return rotation_matrix
@staticmethod
def _matrix_to_rot6d(rotation_matrix: np.ndarray) -> np.ndarray:
"""
Convert rotation matrix to 6D rotation representation.
Args:
rotation_matrix: Rotation matrix (3, 3)
Returns:
6D rotation - (6,) array (first two rows flattened)
"""
return rotation_matrix[:2, :].flatten()
def _set_rotation(
self,
rotation: list | np.ndarray,
rotation_type: str,
rotation_order: str | None = None,
degrees: bool = True,
):
"""Internal method to set rotation from various representations"""
rotation = np.array(rotation)
rot_type = RotationType(rotation_type.lower())
if rot_type == RotationType.QUAT:
quat_order = QuatOrder(rotation_order.lower()) if rotation_order else QuatOrder.WXYZ
if quat_order == QuatOrder.WXYZ:
# scipy uses xyzw order, so convert
quat_xyzw = np.array([rotation[1], rotation[2], rotation[3], rotation[0]])
else:
quat_xyzw = rotation
self._rotation = Rotation.from_quat(quat_xyzw)
elif rot_type == RotationType.EULER:
euler_order = EulerOrder(rotation_order.lower()) if rotation_order else EulerOrder.XYZ
self._rotation = Rotation.from_euler(euler_order.value, rotation, degrees=degrees)
elif rot_type == RotationType.ROTVEC:
self._rotation = Rotation.from_rotvec(rotation)
elif rot_type == RotationType.MATRIX:
self._rotation = Rotation.from_matrix(rotation)
elif rot_type == RotationType.ROT6D:
rotation_matrix = self._rot6d_to_matrix(rotation)
self._rotation = Rotation.from_matrix(rotation_matrix)
else:
raise ValueError(f"Unknown rotation type: {rotation_type}")
# Invalidate cache
self._cache_valid = False
@property
def translation(self) -> np.ndarray:
"""
Get translation vector.
Returns:
Translation array - shape (3,)
"""
return self._translation.copy()
@property
def quat_wxyz(self) -> np.ndarray:
"""Get rotation as quaternion in wxyz order (w, x, y, z)"""
return self.to_rotation("quat", "wxyz")
@property
def quat_xyzw(self) -> np.ndarray:
"""Get rotation as quaternion in xyzw order (x, y, z, w)"""
return self.to_rotation("quat", "xyzw")
@property
def euler_xyz(self) -> np.ndarray:
"""Get rotation as Euler angles in xyz order (degrees)"""
return self.to_rotation("euler", "xyz")
@property
def rotvec(self) -> np.ndarray:
"""Get rotation as rotation vector (axis-angle)"""
return self.to_rotation("rotvec")
@property
def rotation_matrix(self) -> np.ndarray:
"""Get rotation as 3x3 rotation matrix"""
return self.to_rotation("matrix")
@property
def rot6d(self) -> np.ndarray:
"""Get rotation as 6D representation (first two rows of rotation matrix)"""
return self.to_rotation("rot6d")
@property
def xyz_rot6d(self) -> np.ndarray:
"""Get pose as concatenated translation and 6D rotation (9,)"""
return np.concatenate([self._translation, self.rot6d])
@property
def xyz_rotvec(self) -> np.ndarray:
"""Get pose as concatenated translation and rotation vector (6,)"""
return np.concatenate([self._translation, self.rotvec])
@property
def homogeneous(self) -> np.ndarray:
"""
Get homogeneous transformation matrix.
Returns:
Homogeneous matrix - shape (4, 4)
"""
if not self._cache_valid:
self._homogeneous_cache = self._compute_homogeneous()
self._cache_valid = True
assert self._homogeneous_cache is not None
return self._homogeneous_cache.copy()
def _compute_homogeneous(self) -> np.ndarray:
"""Compute homogeneous transformation matrix"""
H = np.eye(4)
H[:3, :3] = self._rotation.as_matrix()
H[:3, 3] = self._translation
return H
def to_rotation(
self,
rotation_type: str,
rotation_order: str | None = None,
degrees: bool = True,
) -> np.ndarray:
"""
Get rotation in specified representation.
Args:
rotation_type: Desired type ("quat", "euler", "rotvec", "matrix", "rot6d")
rotation_order: Order/convention for the rotation type
degrees: For Euler angles, return in degrees (default True)
Returns:
Rotation in requested format
- Shape (4,) for quat
- Shape (3,) for euler/rotvec
- Shape (6,) for rot6d
- Shape (3, 3) for matrix
"""
rot_type = RotationType(rotation_type.lower())
if rot_type == RotationType.ROT6D:
rotation_matrix = self._rotation.as_matrix()
return self._matrix_to_rot6d(rotation_matrix)
if rot_type == RotationType.QUAT:
quat_order = QuatOrder(rotation_order.lower()) if rotation_order else QuatOrder.WXYZ
quat_xyzw = self._rotation.as_quat()
if quat_order == QuatOrder.WXYZ:
return np.array([quat_xyzw[3], quat_xyzw[0], quat_xyzw[1], quat_xyzw[2]])
else:
return quat_xyzw
elif rot_type == RotationType.EULER:
euler_order = EulerOrder(rotation_order.lower()) if rotation_order else EulerOrder.XYZ
return self._rotation.as_euler(euler_order.value, degrees=degrees)
elif rot_type == RotationType.ROTVEC:
return self._rotation.as_rotvec()
elif rot_type == RotationType.MATRIX:
return self._rotation.as_matrix()
else:
raise ValueError(f"Unknown rotation type: {rotation_type}")
def to_homogeneous(self) -> np.ndarray:
"""
Convert pose to homogeneous transformation matrix.
(Alias for the homogeneous property)
Returns:
Homogeneous matrix - shape (4, 4)
"""
return self.homogeneous
def set_rotation(
self,
rotation: list | np.ndarray,
rotation_type: str,
rotation_order: str | None = None,
degrees: bool = True,
):
"""
Set rotation from specified representation.
Args:
rotation: Rotation data
rotation_type: Type of rotation ("quat", "euler", "rotvec", "matrix", "rot6d")
rotation_order: Order/convention for the rotation type
degrees: For Euler angles, whether the input is in degrees (default True)
"""
self._set_rotation(rotation, rotation_type, rotation_order, degrees)
def _compute_relative(self, other): # type: ignore[override]
"""
Compute relative transformation from other to self.
The result represents the transformation needed to go from other's frame to self's frame.
Mathematically: T_relative = T_other^{-1} * T_self
Args:
other: Reference end-effector pose
Returns:
EndEffectorPose representing the relative transformation
"""
# Get homogeneous matrices
T_self = self.homogeneous
T_other = other.homogeneous
# Compute relative transformation: T_other^{-1} * T_self
T_relative = relative_transformation(T_other, T_self)
# Create new EndEffectorPose from relative transformation
return EndEffectorPose(homogeneous=T_relative)
@classmethod
def from_action_format(cls, data: np.ndarray, action_format: ActionFormat) -> "EndEffectorPose":
"""
Create an EndEffectorPose from a flat array using the specified action format.
This is the inverse of the xyz_rot6d / xyz_rotvec / homogeneous properties.
Args:
data: Flat array whose layout depends on action_format.
action_format: One of ActionFormat.XYZ_ROT6D, XYZ_ROTVEC, or DEFAULT.
Returns:
EndEffectorPose instance.
"""
if action_format == ActionFormat.XYZ_ROT6D:
return cls(translation=data[:3], rotation=data[3:], rotation_type="rot6d")
elif action_format == ActionFormat.XYZ_ROTVEC:
return cls(translation=data[:3], rotation=data[3:], rotation_type="rotvec")
elif action_format == ActionFormat.DEFAULT:
return cls(homogeneous=data.reshape(4, 4))
else:
raise ValueError(f"Unsupported ActionFormat: {action_format}")
def copy(self) -> "EndEffectorPose":
"""
Create a deep copy of this end-effector pose.
Returns:
New EndEffectorPose instance with copied data
"""
return EndEffectorPose(
translation=self._translation.copy(),
rotation=self._rotation.as_quat(),
rotation_type="quat",
rotation_order="xyzw",
)
def __repr__(self) -> str:
quat = self.to_rotation("quat", "wxyz")
return f"EndEffectorPose(translation={self.translation}, rotation_quat_wxyz={quat})"
def __eq__(self, other) -> bool:
if not isinstance(other, EndEffectorPose):
return False
return np.allclose(self._translation, other._translation) and np.allclose(
self._rotation.as_quat(), other._rotation.as_quat()
)