Source code for pyrobopath.toolpath.path.transform

from __future__ import annotations
import numpy as np
import quaternion as quat
from colorama import Fore, Style

from pyrobopath.tools.types import *


[docs] def rotate_vec(vec: np.ndarray, rot: quat.quaternion): vec4 = np.zeros(4) vec4[1:] = vec[0:3] rot_vec = rot * quat.as_quat_array(vec4) * rot.conjugate() return quat.as_vector_part(rot_vec)
[docs] class Rotation: """A rotation class that implements spatial rotations on members of the group SO3 (Special Orthogonal) This class internally represents a rotation as a numpy-quaternion. The aim is to make interpolation as fast as possible -- as this is used a lot throughout pyrobopath Class method constructors facilitate constructing rotations with common SO3 parameterizations (i.e. axis rotations, quaternions, homogeneous transformations, Euler angles.) :param orient: The rotational component as {w, x, y, z} (default: [1, 0, 0, 0]) :type orient: Quat """ def __init__(self, orient: Quat | None = None): if orient is None: self.quat = quat.one else: self.quat = orient @property def matrix(self) -> R3x3: """The 3x3 rotation matrix :return: An array view of the 3x3 rotation matrix :rtype: R3x3 """ return quat.as_rotation_matrix(self._orient) @matrix.setter def matrix(self, value: R3x3): """Set the transform from a 4x4 homogeneous transformation :param value: A 4x4 homogeneous transformation matrix :type param: R4x4 """ self._orient = quat.from_rotation_matrix(value[:3, :3]) @property def quat(self) -> quat.quaternion: """Get the rotation as a quaternion :return: The rotation as a quaternion :rtype: quat.quaternion """ return self._orient @quat.setter def quat(self, value: Quat): if isinstance(value, quat.quaternion): self._orient = value elif isinstance(value, np.ndarray) or isinstance(value, list): self._orient = quat.as_quat_array(value) else: raise TypeError( "orient should be a numpy quaternion" "or an array of length 4 {w, x, y, z}" ) def __str__(self) -> str: # fmt: off out = f"{Style.BRIGHT}{Fore.RED}" col_maxes = [max([len(("{:g}").format(x)) for x in col]) for col in self.matrix.T] out_fmt = lambda i, j: ("{:" + str(col_maxes[j]) + "g}").format(self.matrix[i, j]) # fmt: on for i in range(3): out += "".join([out_fmt(i, j) + " " for j in range(3)]) + "\n" out += f"{Style.RESET_ALL}" return out def __eq__(self, other) -> bool: return self.quat == other.quat def __mul__(self, other): if type(self) == type(other): return self @ other if isinstance(other, np.ndarray): return rotate_vec(other, self.quat) else: return NotImplemented def __matmul__(self, other): if type(self) == type(other): return self.__class__(self.quat * other.quat) else: raise ValueError("@ only applies to rotation composition")
[docs] def inv(self) -> Rotation: """ Find the inverse transformation of the SE(3) instance :return: SE(3) inverse :rtype: SE3 """ return self.__class__(self.quat.conjugate())
[docs] def interp(self, other: Rotation, s: float) -> Rotation: """Interpolate between SO(3) instances :param other: the ending orientation at s = 1.0 :type other: SO3 :param s: the interpolation variable s in [0, 1] :type s: float :returns: the SO(3) instances interpolated at value s :rtype: SO(3) """ return self.__class__(quat.slerp(self.quat, other.quat, 0.0, 1.0, s))
def copy(self) -> Rotation: return Rotation(self.quat.copy()) def almost_equal(self, other: Rotation, rtol=1e-05, atol=1e-08) -> bool: return quat.isclose(self.quat, other.quat, rtol=rtol, atol=atol) # type: ignore
[docs] @classmethod def Rx(cls, theta: float) -> Rotation: """Construct a new SO(3) rotation from X-axis rotation :param θ: rotation angle about the X-axis :type θ: float or array_like :return: SE(3) rotation :rtype: SE3 instance """ return cls(quat.from_rotation_vector([theta, 0.0, 0.0]))
[docs] @classmethod def Ry(cls, theta: float) -> Rotation: """Construct a new SO(3) rotation from Y-axis rotation :param θ: rotation angle about the Y-axis :type θ: float or array_like :return: SE(3) rotation :rtype: SE3 instance """ return cls(quat.from_rotation_vector([0.0, theta, 0.0]))
[docs] @classmethod def Rz(cls, theta: float) -> Rotation: """Construct a new SO(3) rotation from Z-axis rotation :param θ: rotation angle about the Z-axis :type θ: float or array_like :return: SE(3) rotation :rtype: SE3 instance """ return cls(quat.from_rotation_vector([0.0, 0.0, theta]))
[docs] @classmethod def Quaternion(cls, w: float, x: float, y: float, z: float) -> Rotation: """Construct a new SO(3) rotation instance from Quaternion q = [w, x, y, z] :param w: the real component :type w: float :param x: the x component of the imaginary vector :type x: float :param y: the y component of the imaginary vector :type y: float :param z: the z component of the imaginary vector :type z: float :return: SE(3) rotation :rtype: Pose instance """ return cls(quat.as_quat_array([w, x, y, z]))
[docs] class Transform: """A transformation class that implements spatial transformations on members of the group SE3 (Special Euclidean) This class internally represents a 3D pose (or transformation) as a translation (3D numpy array) and a rotation (numpy-quaternion). It provides class methods that facilitate constructing transformation with common SE3 parameterizations (i.e. axis rotations, quaternions, homogeneous transformations, Euler angles., translations) :param trans: The translational component (default: [0, 0, 0]) :type trans: ArrayLike3 :param orient: The rotational component as {w, x, y, z} (default: [1, 0, 0, 0]) :type orient: Quat """ def __init__(self, trans: ArrayLike3 | None = None, orient: Quat | None = None): self._trans = np.zeros(3) if trans is None else np.asarray(trans) if orient is None: self.quat = quat.one else: self.quat = orient @property def matrix(self) -> R4x4: """The homogeneous transformation matrix :return: An array view of the 4x4 homogeneous transformation :rtype: R4x4 """ matrix = np.eye(4) matrix[:3, :3] = quat.as_rotation_matrix(self._orient) matrix[:3, 3] = self._trans return matrix @matrix.setter def matrix(self, value: R4x4): """Set the transform from a 4x4 homogeneous transformation :param value: A 4x4 homogeneous transformation matrix :type param: R4x4 """ self._orient = quat.from_rotation_matrix(value[:3, :3]) self._trans = value[:3, 3] @property def t(self) -> R3: """Translation component of SE(3) :return: An array view of the R3 translation :rtype: R3 """ return self._trans @t.setter def t(self, value: R3): """Set the translation of the transformation :param value: A numpy array of length 3 :type param: R3 """ self._trans = value @property def R(self) -> R3x3: """Rotational component of SE(3) :return: A readonly matrix view of the SO(3) rotation :rtype: R3x3 """ return quat.as_rotation_matrix(self._orient) @R.setter def R(self, value: R3x3): self._orient = quat.from_rotation_matrix(value) @property def quat(self) -> quat.quaternion: """Get the rotational component of the tranformation as a quaternion :return: The rotation of the transformation as a quaternion :rtype: Quat """ return self._orient @quat.setter def quat(self, value: Quat): if isinstance(value, quat.quaternion): self._orient = value elif isinstance(value, np.ndarray) or isinstance(value, list): self._orient = quat.as_quat_array(value) else: raise TypeError( "orient should be a numpy quaternion" "or an array of length 4 {w, x, y, z}" ) @property def N(self) -> int: """Dimension of the object's group (superclass property) :return: dimension :rtype: int This corresponds to the dimension of the space, 2D or 3D, to which these rotations or rigid-body motions apply. """ return 3 def __str__(self) -> str: # fmt: off out = f"{Style.BRIGHT}" col_maxes = [max([len(("{:g}").format(x)) for x in col]) for col in self.matrix.T] out_fmt = lambda i, j: ("{:" + str(col_maxes[j]) + "g}").format(self.matrix[i, j]) for i in range(3): out += f"{Fore.RED}" + "".join([out_fmt(i, j) + " " for j in range(3)]) out += f"{Fore.BLUE}{out_fmt(i, 3)}\n" out += (f"{Fore.RESET}" + "".join([out_fmt(3, j) + " " for j in range(4)]) + "\n") out += f"{Style.RESET_ALL}" # fmt: on return out def __eq__(self, other) -> bool: return np.all(self.t == other.t) and self.quat == other.quat # type: ignore def __mul__(self, other): if type(self) == type(other): return self @ other if isinstance(other, np.ndarray): return rotate_vec(other, self.quat) + self.t else: return NotImplemented def __matmul__(self, other): if type(self) == type(other): trans = rotate_vec(other.t, self.quat) + self.t orient = self.quat * other.quat return self.__class__(trans, orient) else: raise ValueError("@ only applies to transform composition") def almost_equal(self, other: Transform, rtol=1e-05, atol=1e-08) -> bool: same_trans = np.isclose(self.t, other.t, rtol=rtol, atol=atol) same_orient = quat.isclose(self.quat, other.quat, rtol=rtol, atol=atol) # type: ignore return bool(np.all(same_trans) and np.all(same_orient)) def copy(self) -> Transform: return Transform(self._trans.copy(), self._orient.copy())
[docs] def inv(self) -> Transform: """Find the inverse transformation of the SE(3) instance :return: SE(3) inverse :rtype: SE3 """ inv = self.copy() inv.quat = self.quat.conjugate() inv.t = rotate_vec(-self.t, inv.quat) return inv
[docs] def interp(self, other: Transform, s: float) -> Transform: """Interpolate between SE(3) instances :param other: the ending orientation at s = 1.0 :type other: SE3 :param s: the interpolation variable s in [0, 1] :type s: float :returns: the SE(3) instances interpolated at value s :rtype: SE(3) """ trans = self.t * (1 - s) + other.t * s orient = quat.slerp(self._orient, other._orient, 0, 1, s) return Transform(trans, orient)
[docs] @classmethod def Trans( cls, x: float | ArrayLike3, y: float | None = None, z: float | None = None ) -> Transform: """Construct a new SE(3) instance from a pure translation :param x: the x value of the translation (if float) or a 3 vector position (if ArrayLike3) :type x: float or ArrayLike3 :param y: the y value of the translation :type y: float :param z: the z value of the translation :type z: float :return: SE(3) translation :rtype: SE3 """ if y is None and z is None: return cls(np.array(x)) return cls(np.array([x, y, z]))
[docs] @classmethod def Rx(cls, theta: float) -> Transform: """Construct a new SE(3) transformation from X-axis rotation :param θ: rotation angle about the X-axis :type θ: float or array_like :return: SE(3) rotation :rtype: SE3 instance """ return cls(orient=quat.from_rotation_vector([theta, 0.0, 0.0]))
[docs] @classmethod def Ry(cls, theta: float) -> Transform: """Construct a new SE(3) transformation from Y-axis rotation :param θ: rotation angle about the Y-axis :type θ: float or array_like :return: SE(3) rotation :rtype: SE3 instance """ return cls(orient=quat.from_rotation_vector([0.0, theta, 0.0]))
[docs] @classmethod def Rz(cls, theta: float) -> Transform: """Construct a new SE(3) transformation from Z-axis rotation :param θ: rotation angle about the Z-axis :type θ: float or array_like :return: SE(3) rotation :rtype: SE3 instance """ return cls(orient=quat.from_rotation_vector([0.0, 0.0, theta]))
[docs] @classmethod def Quaternion(cls, w: float, x: float, y: float, z: float) -> Transform: """Construct a new SE(3) transformation instance from Quaternion q = [w, x, y, z] :param w: the real component :type w: float :param x: the x component of the imaginary vector :type x: float :param y: the y component of the imaginary vector :type y: float :param z: the z component of the imaginary vector :type z: float :return: SE(3) rotation :rtype: Pose instance """ return cls(orient=[w, x, y, z])