# Source code for spatialmath.pose3d

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-

    from collections import UserList
    import numpy as np
    import math

    from spatialmath.base import argcheck
    import spatialmath.base as tr
    from spatialmath import super_pose as sp

    # ============================== SO3 =====================================#

    class SO3(sp.SMPose):
        """
        SO(3) subclass

        This subclass represents rotations in 3D space.  Internally it is a 3x3 orthogonal matrix belonging
        to the group SO(3).

        .. inheritance-diagram::
        """

        def __init__(self, arg=None, *, check=True):
            """
            Construct new SO(3) object

            - ``SO3()`` is an SO3 instance representing null rotation -- the identity matrix
            - ``SO3(R)`` is an SO3 instance with rotation matrix R which is a 3x3 numpy array representing an valid rotation matrix.  If ``check``
              is ``True`` check the matrix value.
            - ``SO3([R1, R2, ... RN])`` where each Ri is a 3x3 numpy array of rotation matrices, is
              an SO3 instance containing N rotations. If ``check`` is ``True``
              then each matrix is checked for validity.
            - ``SO3([X1, X2, ... XN])`` where each Xi is an SO3 instance, is an SO3 instance containing N rotations.

            :seealso: `SMPose.pose_arghandler`
            """
            super().__init__()  # activate the UserList semantics

            if arg is None:
                # empty constructor
                if type(self) is SO3:
                    self.data = [np.eye(3)]  # identity rotation
            else:
                super()._arghandler(arg, check=check)

    # ------------------------------------------------------------------------ #

        @property
        def R(self):
            """
            SO(3) or SE(3) as rotation matrix

            :return: rotational component
            :rtype: numpy.ndarray, shape=(3,3)

            ``x.R`` returns the rotation matrix, when `x` is `SO3` or `SE3`. If `len(x)` is:

            - 1, return an ndarray with shape=(3,3)
            - N>1, return ndarray with shape=(N,3,3)
            """
            if len(self) == 1:
                return self.A[:3, :3]
            else:
                return np.array([x[:3, :3] for x in self.A])

        @property
        def n(self):
            """
            Normal vector of SO(3) or SE(3)

            :return: normal vector
            :rtype: numpy.ndarray, shape=(3,)

            Is the first column of the rotation submatrix, sometimes called the normal
            vector.  Parallel to the x-axis of the frame defined by this pose.
            """
            return self.A[:3, 0]

        @property
        def o(self):
            """
            Orientation vector of SO(3) or SE(3)

            :return: orientation vector
            :rtype: numpy.ndarray, shape=(3,)

            Is the second column of the rotation submatrix, sometimes called the orientation
            vector.  Parallel to the y-axis of the frame defined by this pose.
            """
            return self.A[:3, 1]

        @property
        def a(self):
            """
            Approach vector of SO(3) or SE(3)

            :return: approach vector
            :rtype: numpy.ndarray, shape=(3,)

            Is the third column of the rotation submatrix, sometimes called the approach
            vector.  Parallel to the z-axis of the frame defined by this pose.
            """
            return self.A[:3, 2]

    # ------------------------------------------------------------------------ #

        def inv(self):
            """
            Inverse of SO(3)

            :param self: pose
            :type self: SE3 instance
            :return: inverse
            :rtype: SO2

            Returns the inverse, which for elements of SO(3) is the transpose.
            """
            if len(self) == 1:
                return SO3(self.A.T)
            else:
                return SO3([x.T for x in self.A])

        def eul(self, unit='deg'):
            """
            SO(3) or SE(3) as Euler angles

            :param unit: angular units: 'rad' [default], or 'deg'
            :type unit: str
            :return: 3-vector of Euler angles
            :rtype: numpy.ndarray, shape=(3,)

            ``x.eul`` is the Euler angle representation of the rotation.  Euler angles are
            a 3-vector :math:`(\phi, \theta, \psi)` which correspond to consecutive
            rotations about the Z, Y, Z axes respectively.

            If `len(x)` is:

            - 1, return an ndarray with shape=(3,)
            - N>1, return ndarray with shape=(N,3)

            - ndarray with shape=(3,), if len(R) == 1
            - ndarray with shape=(N,3), if len(R) = N > 1

            :seealso: :func:`~spatialmath.pose3d.SE3.Eul`, ::func:`spatialmath.base.transforms3d.tr2eul`
            """
            if len(self) == 1:
                return tr.tr2eul(self.A, unit=unit)
            else:
                return np.array([tr.tr2eul(x, unit=unit) for x in self.A]).T

        def rpy(self, unit='deg', order='zyx'):
            """
            SO(3) or SE(3) as roll-pitch-yaw angles

            :param order: angle sequence order, default to 'zyx'
            :type order: str
            :param unit: angular units: 'rad' [default], or 'deg'
            :type unit: str
            :return: 3-vector of roll-pitch-yaw angles
            :rtype: numpy.ndarray, shape=(3,)

            ``x.rpy`` is the roll-pitch-yaw angle representation of the rotation.  The angles are
            a 3-vector :math:`(r, p, y)` which correspond to successive rotations about the axes
            specified by ``order``:

                - 'zyx' [default], rotate by yaw about the z-axis, then by pitch about the new y-axis,
                  then by roll about the new x-axis.  Convention for a mobile robot with x-axis forward
                  and y-axis sideways.
                - 'xyz', rotate by yaw about the x-axis, then by pitch about the new y-axis,
                  then by roll about the new z-axis. Covention for a robot gripper with z-axis forward
                  and y-axis between the gripper fingers.
                - 'yxz', rotate by yaw about the y-axis, then by pitch about the new x-axis,
                  then by roll about the new z-axis. Convention for a camera with z-axis parallel
                  to the optic axis and x-axis parallel to the pixel rows.

            If `len(x)` is:

            - 1, return an ndarray with shape=(3,)
            - N>1, return ndarray with shape=(N,3)

            :seealso: :func:`~spatialmath.pose3d.SE3.RPY`, ::func:`spatialmath.base.transforms3d.tr2rpy`
            """
            if len(self) == 1:
                return tr.tr2rpy(self.A, unit=unit)
            else:
                return np.array([tr.tr2rpy(x, unit=unit) for x in self.A]).T

        def Ad(self):
            """
            Adjoint of SO(3)

            :return: adjoint matrix
            :rtype: numpy.ndarray, shape=(6,6)

            - ``SE3.Ad`` is the 6x6 adjoint matrix

            :seealso: Twist.ad.

            """

            return np.r_[ np.c_[self.R, tr.skew(self.t) @ self.R],
                          np.c_[np.zeros((3,3)), self.R]
                            ]
    # ------------------------------------------------------------------------ #

        @staticmethod
        def isvalid(x):
            """
            Test if matrix is valid SO(3)

            :param x: matrix to test
            :type x: numpy.ndarray
            :return: true if the matrix is a valid element of SO(3), ie. it is a 3x3
                orthonormal matrix with determinant of +1.
            :rtype: bool

            :seealso: :func:`~spatialmath.base.transform3d.isrot`
            """
            return tr.isrot(x, check=True)

    # ---------------- variant constructors ---------------------------------- #

        @classmethod
        def Rx(cls, theta, unit='rad'):
            """
            Construct a new SO(3) from X-axis rotation

            :param theta: rotation angle about the X-axis
            :type theta: float or array_like
            :param unit: angular units: 'rad' [default], or 'deg'
            :type unit: str
            :return: SO(3) rotation
            :rtype: SO3 instance

            - ``SE3.Rx(theta)`` is an SO(3) rotation of ``theta`` radians about the x-axis
            - ``SE3.Rx(theta, "deg")`` as above but ``theta`` is in degrees

            If ``theta`` is an array then the result is a sequence of rotations defined by consecutive
            elements.

            Example::

                >>> x = SO3.Rx(np.linspace(0, math.pi, 20))
                >>> len(x)
                20
                >>> x[7]
                SO3(array([[ 1.        ,  0.        ,  0.        ],
                           [ 0.        ,  0.40169542, -0.91577333],
                           [ 0.        ,  0.91577333,  0.40169542]]))
            """
            return cls([tr.rotx(x, unit=unit) for x in argcheck.getvector(theta)], check=False)

        @classmethod
        def Ry(cls, theta, unit='rad'):
            """
            Construct a new SO(3) from Y-axis rotation

            :param theta: rotation angle about Y-axis
            :type theta: float or array_like
            :param unit: angular units: 'rad' [default], or 'deg'
            :type unit: str
            :return: SO(3) rotation
            :rtype: SO3 instance

            - ``SO3.Ry(theta)`` is an SO(3) rotation of ``theta`` radians about the y-axis
            - ``SO3.Ry(theta, "deg")`` as above but ``theta`` is in degrees

            If ``theta`` is an array then the result is a sequence of rotations defined by consecutive
            elements.

            Example::

                >>> x = SO3.Ry(np.linspace(0, math.pi, 20))
                >>> len(x)
                20
                >>> x[7]
                >>> x[7]
                SO3(array([[ 0.40169542,  0.        ,  0.91577333],
                           [ 0.        ,  1.        ,  0.        ],
                           [-0.91577333,  0.        ,  0.40169542]]))
            """
            return cls([tr.roty(x, unit=unit) for x in argcheck.getvector(theta)], check=False)

        @classmethod
        def Rz(cls, theta, unit='rad'):
            """
            Construct a new SO(3) from Z-axis rotation

            :param theta: rotation angle about Z-axis
            :type theta: float or array_like
            :param unit: angular units: 'rad' [default], or 'deg'
            :type unit: str
            :return: SO(3) rotation
            :rtype: SO3 instance

            - ``SO3.Rz(theta)`` is an SO(3) rotation of ``theta`` radians about the z-axis
            - ``SO3.Rz(theta, "deg")`` as above but ``theta`` is in degrees

            If ``theta`` is an array then the result is a sequence of rotations defined by consecutive
            elements.

            Example::

                >>> x = SE3.Rz(np.linspace(0, math.pi, 20))
                >>> len(x)
                20
                SO3(array([[ 0.40169542, -0.91577333,  0.        ],
                           [ 0.91577333,  0.40169542,  0.        ],
                           [ 0.        ,  0.        ,  1.        ]]))
            """
            return cls([tr.rotz(x, unit=unit) for x in argcheck.getvector(theta)], check=False)

        @classmethod
        def Rand(cls, N=1):
            """
            Construct a new SO(3) from random rotation

            :param N: number of random rotations
            :type N: int
            :return: SO(3) rotation matrix
            :rtype: SO3 instance

            - ``SO3.Rand()`` is a random SO(3) rotation.
            - ``SO3.Rand(N)`` is a sequence of N random rotations.

            Example::

                >>> x = SO3.Rand()
                >>> x
                SO3(array([[ 0.1805082 , -0.97959019,  0.08842995],
                           [-0.98357187, -0.17961408,  0.01803234],
                           [-0.00178104, -0.0902322 , -0.99591916]]))

            :seealso: :func:`spatialmath.quaternion.UnitQuaternion.Rand`
            """
            return cls([tr.q2r(tr.rand()) for i in range(0, N)], check=False)

        @classmethod
        def Eul(cls, angles, *, unit='rad'):
            r"""
            Construct a new SO(3) from Euler angles

            :param angles: Euler angles
            :type angles: array_like or numpy.ndarray with shape=(N,3)
            :param unit: angular units: 'rad' [default], or 'deg'
            :type unit: str
            :return: SO(3) rotation
            :rtype: SO3 instance

            ``SO3.Eul(angles)`` is an SO(3) rotation defined by a 3-vector of Euler angles :math:`(\phi, \theta, \psi)` which
            correspond to consecutive rotations about the Z, Y, Z axes respectively.

            If ``angles`` is an Nx3 matrix then the result is a sequence of rotations each defined by Euler angles
            correponding to the rows of ``angles``.

            :seealso: :func:`~spatialmath.pose3d.SE3.eul`, :func:`~spatialmath.pose3d.SE3.Eul`, :func:`spatialmath.base.transforms3d.eul2r`
            """
            if argcheck.isvector(angles, 3):
                return cls(tr.eul2r(angles, unit=unit))
            else:
                return cls([tr.eul2r(a, unit=unit) for a in angles])

        @classmethod
        def RPY(cls, angles, *, order='zyx', unit='rad'):
            r"""
            Construct a new SO(3) from roll-pitch-yaw angles

            :param angles: roll-pitch-yaw angles
            :type angles: array_like or numpy.ndarray with shape=(N,3)
            :param unit: angular units: 'rad' [default], or 'deg'
            :type unit: str
            :param unit: rotation order: 'zyx' [default], 'xyz', or 'yxz'
            :type unit: str
            :return: SO(3) rotation
            :rtype: SO3 instance

            ``SO3.RPY(angles)`` is an SO(3) rotation defined by a 3-vector of roll, pitch, yaw angles :math:`(r, p, y)`
              which correspond to successive rotations about the axes specified by ``order``:

                - 'zyx' [default], rotate by yaw about the z-axis, then by pitch about the new y-axis,
                  then by roll about the new x-axis.  Convention for a mobile robot with x-axis forward
                  and y-axis sideways.
                - 'xyz', rotate by yaw about the x-axis, then by pitch about the new y-axis,
                  then by roll about the new z-axis. Covention for a robot gripper with z-axis forward
                  and y-axis between the gripper fingers.
                - 'yxz', rotate by yaw about the y-axis, then by pitch about the new x-axis,
                  then by roll about the new z-axis. Convention for a camera with z-axis parallel
                  to the optic axis and x-axis parallel to the pixel rows.

            If ``angles`` is an Nx3 matrix then the result is a sequence of rotations each defined by RPY angles
            correponding to the rows of angles.

            :seealso: :func:`~spatialmath.pose3d.SE3.rpy`, :func:`~spatialmath.pose3d.SE3.RPY`, :func:`spatialmath.base.transforms3d.rpy2r`
            """
            if argcheck.isvector(angles, 3):
                    return cls(tr.rpy2r(angles, order=order, unit=unit))
            else:
                return cls([tr.rpy2r(a, order=order, unit=unit) for a in angles])

        @classmethod
        def OA(cls, o, a):
            """
            Construct a new SO(3) from two vectors

            :param o: 3-vector parallel to Y- axis
            :type o: array_like
            :param a: 3-vector parallel to the Z-axis
            :type o: array_like
            :return: SO(3) rotation
            :rtype: SO3 instance

            ``SO3.OA(O, A)`` is an SO(3) rotation defined in terms of
            vectors parallel to the Y- and Z-axes of its reference frame.  In robotics these axes are
            respectively called the *orientation* and *approach* vectors defined such that
            R = [N, O, A] and N = O x A.

            Notes:

            - Only the ``A`` vector is guaranteed to have the same direction in the resulting
              rotation matrix
            - ``O`` and ``A`` do not have to be unit-length, they are normalized
            - ``O`` and ``A` do not have to be orthogonal, so long as they are not parallel

            :seealso: :func:`spatialmath.base.transforms3d.oa2r`
            """
            return cls(tr.oa2r(o, a), check=False)

        @classmethod
        def AngVec(cls, theta, v, *, unit='rad'):
            r"""
            Construct a new SO(3) rotation matrix from rotation angle and axis

            :param theta: rotation
            :type theta: float
            :param unit: angular units: 'rad' [default], or 'deg'
            :type unit: str
            :param v: rotation axis, 3-vector
            :type v: array_like
            :return: SO(3) rotation
            :rtype: SO3 instance

            ``SO3.AngVec(theta, V)`` is an SO(3) rotation defined by
            a rotation of ``THETA`` about the vector ``V``.

            If :math:`\theta \eq 0` the result in an identity matrix, otherwise
            ``V`` must have a finite length, ie. :math:`|V| > 0`.

            :seealso: :func:`~spatialmath.pose3d.SE3.angvec`, :func:`spatialmath.base.transforms3d.angvec2r`
            """
            return cls(tr.angvec2r(theta, v, unit=unit), check=False)

        @classmethod
        def Exp(cls, S, check=True, so3=True):
            """
            Create an SO(3) rotation matrix from so(3)

            :param S: Lie algebra so(3)
            :type S: numpy ndarray
            :param check: check that passed matrix is valid so(3), default True
            :type check: bool
            :param so3: input is an so(3) matrix (default True)
            :type so3: bool
            :return: SO(3) rotation
            :rtype: SO3 instance

            - ``SO3.Exp(S)`` is an SO(3) rotation defined by its Lie algebra
              which is a 3x3 so(3) matrix (skew symmetric)
            - ``SO3.Exp(t)`` is an SO(3) rotation defined by a 3-element twist
              vector (the unique elements of the so(3) skew-symmetric matrix)
            - ``SO3.Exp(T)`` is a sequence of SO(3) rotations defined by an Nx3 matrix
              of twist vectors, one per row.

            Note:
            - if :math:`\theta \eq 0` the result in an identity matrix
            - an input 3x3 matrix is ambiguous, it could be the first or third case above.  In this
              case the parameter `so3` is the decider.

            :seealso: :func:`spatialmath.base.transforms3d.trexp`, :func:`spatialmath.base.transformsNd.skew`
            """
            if argcheck.ismatrix(S, (-1,3)) and not so3:
                return cls([tr.trexp(s, check=check) for s in S])
            else:
                return cls(tr.trexp(S, check=check), check=False)

    # ============================== SE3 =====================================#

    class SE3(SO3):

        def __init__(self, x=None, y=None, z=None, *, check=True):
            """
            Construct new SE(3) object

            :param x: translation distance along the X-axis
            :type x: float
            :param y: translation distance along the Y-axis
            :type y: float
            :param z: translation distance along the Z-axis
            :type z: float
            :return: 4x4 homogeneous transformation matrix
            :rtype: SE3 instance

            - ``SE3()`` is a null motion -- the identity matrix
            - ``SE3(x, y, z)`` is a pure translation of (x,y,z)
            - ``SE3(T)`` where T is a 4x4 numpy array representing an SE(3) matrix.  If ``check``
              is ``True`` check the matrix belongs to SE(3).
            - ``SE3([T1, T2, ... TN])`` where each Ti is a 4x4 numpy array representing an SE(3) matrix, is
              an SE3 instance containing N rotations. If ``check`` is ``True``
              check the matrix belongs to SE(3).
            - ``SE3([X1, X2, ... XN])`` where each Xi is an SE3 instance, is an SE3 instance containing N rotations.
            """
            super().__init__()  # activate the UserList semantics

            if x is None:
                # SE3()
                # empty constructor
                self.data = [np.eye(4)]
            elif y is not None and z is not None:
                    # SE3(x, y, z)
                    self.data = [tr.transl(x, y, z)]
            elif y is None and z is None:
                if argcheck.isvector(x, 3):
                    # SE3( [x, y, z] )
                    self.data = [tr.transl(x)]
                elif isinstance(x, np.ndarray) and x.shape[1] == 3:
                    # SE3( Nx3 )
                    self.data = [tr.transl(T) for T in x]
                else:
                    super()._arghandler(x, check=check)
            else:
                raise ValueError('bad argument to constructor')

    # ------------------------------------------------------------------------ #

        @property
        def t(self):
            """
            Translational component of SE(3)

            :param self: SE(3)
            :type self: SE3 instance
            :return: translational component
            :rtype: numpy.ndarray

            ``T.t`` returns an:

            - ndarray with shape=(3,), if len(T) == 1
            - ndarray with shape=(N,3), if len(T) = N > 1
            """
            if len(self) == 1:
                return self.A[:3, 3]
            else:
                return np.array([x[:3, 3] for x in self.A])

    # ------------------------------------------------------------------------ #

        def inv(self):
            r"""
            Inverse of SE(3)

            :return: inverse
            :rtype: SE3

            Returns the inverse taking into account its structure

            :math:`T = \left[ \begin{array}{cc} R & t \\ 0 & 1 \end{array} \right], T^{-1} = \left[ \begin{array}{cc} R^T & -R^T t \\ 0 & 1 \end{array} \right]`

            :seealso: :func:`~spatialmath.base.transform3d.trinv`
            """
            if len(self) == 1:
                return SE3(tr.trinv(self.A))
            else:
                return SE3([tr.trinv(x) for x in self.A])

        def delta(self, X2):
            r"""
            Difference of SE(3)

            :param X1:
            :type X1: SE3
            :return: differential motion vector
            :rtype: numpy.ndarray, shape=(6,)

            - ``X1.delta(T2)`` is the differential motion (6x1) corresponding to
              infinitessimal motion (in the X1 frame) from pose X1 to X2.

            The vector :math:`d = [\delta_x, \delta_y, \delta_z, \theta_x, \theta_y, \theta_z`
            represents infinitessimal translation and rotation.

            Notes:

            - the displacement is only an approximation to the motion T, and assumes
              that X1 ~ X2.
            - Can be considered as an approximation to the effect of spatial velocity over a
              a time interval, average spatial velocity multiplied by time.

            Reference: Robotics, Vision & Control: Second Edition, P. Corke, Springer 2016; p67.

            :seealso: :func:`~spatialmath.base.transform3d.tr2delta`
            """
            return tr.tr2delta(self.A, X1.A)
    # ------------------------------------------------------------------------ #

        @staticmethod
        def isvalid(x):
            """
            Test if matrix is valid SE(3)

            :param x: matrix to test
            :type x: numpy.ndarray
            :return: true of the matrix is 4x4 and a valid element of SE(3), ie. it is an
                homogeneous transformation matrix.
            :rtype: bool

            :seealso: :func:`~spatialmath.base.transform3d.ishom`
            """
            return tr.ishom(x, check=True)

    # ---------------- variant constructors ---------------------------------- #

        @classmethod
        def Rx(cls, theta, unit='rad'):
            """
            Create SE(3) pure rotation about the X-axis

            :param theta: rotation angle about X-axis
            :type theta: float
            :param unit: angular units: 'rad' [default], or 'deg'
            :type unit: str
            :return: 4x4 homogeneous transformation matrix
            :rtype: SE3 instance

            - ``SE3.Rx(THETA)`` is an SO(3) rotation of THETA radians about the x-axis
            - ``SE3.Rx(THETA, "deg")`` as above but THETA is in degrees

            If ``theta`` is an array then the result is a sequence of rotations defined by consecutive
            elements.
            """
            return cls([tr.trotx(x, unit) for x in argcheck.getvector(theta)])

        @classmethod
        def Ry(cls, theta, unit='rad'):
            """
            Create SE(3) pure rotation about the Y-axis

            :param theta: rotation angle about X-axis
            :type theta: float
            :param unit: angular units: 'rad' [default], or 'deg'
            :type unit: str
            :return: 4x4 homogeneous transformation matrix
            :rtype: SE3 instance

            - ``SE3.Ry(THETA)`` is an SO(3) rotation of THETA radians about the y-axis
            - ``SE3.Ry(THETA, "deg")`` as above but THETA is in degrees

            If ``theta`` is an array then the result is a sequence of rotations defined by consecutive
            elements.
            """
            return cls([tr.troty(x, unit) for x in argcheck.getvector(theta)])

        @classmethod
        def Rz(cls, theta, unit='rad'):
            """
            Create SE(3) pure rotation about the Z-axis

            :param theta: rotation angle about Z-axis
            :type theta: float
            :param unit: angular units: 'rad' [default], or 'deg'
            :type unit: str
            :return: 4x4 homogeneous transformation matrix
            :rtype: SE3 instance

            - ``SE3.Rz(THETA)`` is an SO(3) rotation of THETA radians about the z-axis
            - ``SE3.Rz(THETA, "deg")`` as above but THETA is in degrees

            If ``theta`` is an array then the result is a sequence of rotations defined by consecutive
            elements.
            """
            return cls([tr.trotz(x, unit) for x in argcheck.getvector(theta)])

        @classmethod
        def Rand(cls, *, xrange=[-1, 1], yrange=[-1, 1], zrange=[-1, 1], N=1):
            """
            Create a random SE(3)

            :param xrange: x-axis range [min,max], defaults to [-1, 1]
            :type xrange: 2-element sequence, optional
            :param yrange: y-axis range [min,max], defaults to [-1, 1]
            :type yrange: 2-element sequence, optional
            :param zrange: z-axis range [min,max], defaults to [-1, 1]
            :type zrange: 2-element sequence, optional
            :param N: number of random transforms
            :type N: int
            :return: homogeneous transformation matrix
            :rtype: SE3 instance

            Return an SE3 instance with random rotation and translation.

            - ``SE3.Rand()`` is a random SE(3) translation.
            - ``SE3.Rand(N)`` is an SE3 object containing a sequence of N random
              poses.

            :seealso: `~spatialmath.quaternion.UnitQuaternion.Rand`
            """
            X = np.random.uniform(low=xrange[0], high=xrange[1], size=N)  # random values in the range
            Y = np.random.uniform(low=yrange[0], high=yrange[1], size=N)  # random values in the range
            Z = np.random.uniform(low=yrange[0], high=zrange[1], size=N)  # random values in the range
            R = SO3.Rand(N=N)
            return cls([tr.transl(x, y, z) @ tr.r2t(r.A) for (x, y, z, r) in zip(X, Y, Z, R)])

        @classmethod
        def Eul(cls, angles, unit='rad'):
            """
            Create an SE(3) pure rotation from Euler angles

            :param angles: 3-vector of Euler angles
            :type angles: array_like or numpy.ndarray with shape=(N,3)
            :param unit: angular units: 'rad' [default], or 'deg'
            :type unit: str
            :return: 4x4 homogeneous transformation matrix
            :rtype: SE3 instance

            ``SE3.Eul(ANGLES)`` is an SO(3) rotation defined by a 3-vector of Euler angles :math:`(\phi, \theta, \psi)` which
            correspond to consecutive rotations about the Z, Y, Z axes respectively.

            If ``angles`` is an Nx3 matrix then the result is a sequence of rotations each defined by Euler angles
            correponding to the rows of angles.

            :seealso: :func:`~spatialmath.pose3d.SE3.eul`, :func:`~spatialmath.pose3d.SE3.Eul`, :func:`spatialmath.base.transforms3d.eul2r`
            """
            if argcheck.isvector(angles, 3):
                return cls(tr.eul2tr(angles, unit=unit))
            else:
                return cls([tr.eul2tr(a, unit=unit) for a in angles])

        @classmethod
        def RPY(cls, angles, order='zyx', unit='rad'):
            """
            Create an SO(3) pure rotation from roll-pitch-yaw angles

            :param angles: 3-vector of roll-pitch-yaw angles
            :type angles: array_like or numpy.ndarray with shape=(N,3)
            :param unit: angular units: 'rad' [default], or 'deg'
            :type unit: str
            :param unit: rotation order: 'zyx' [default], 'xyz', or 'yxz'
            :type unit: str
            :return: 4x4 homogeneous transformation matrix
            :rtype: SE3 instance

            ``SE3.RPY(ANGLES)`` is an SE(3) rotation defined by a 3-vector of roll, pitch, yaw angles :math:`(r, p, y)`
              which correspond to successive rotations about the axes specified by ``order``:

                - 'zyx' [default], rotate by yaw about the z-axis, then by pitch about the new y-axis,
                  then by roll about the new x-axis.  Convention for a mobile robot with x-axis forward
                  and y-axis sideways.
                - 'xyz', rotate by yaw about the x-axis, then by pitch about the new y-axis,
                  then by roll about the new z-axis. Covention for a robot gripper with z-axis forward
                  and y-axis between the gripper fingers.
                - 'yxz', rotate by yaw about the y-axis, then by pitch about the new x-axis,
                  then by roll about the new z-axis. Convention for a camera with z-axis parallel
                  to the optic axis and x-axis parallel to the pixel rows.

            If ``angles`` is an Nx3 matrix then the result is a sequence of rotations each defined by RPY angles
            correponding to the rows of angles.

            :seealso: :func:`~spatialmath.pose3d.SE3.rpy`, :func:`~spatialmath.pose3d.SE3.RPY`, :func:`spatialmath.base.transforms3d.rpy2r`
            """
            if argcheck.isvector(angles, 3):
                    return cls(tr.rpy2tr(angles, order=order, unit=unit))
            else:
                return cls([tr.rpy2tr(a, order=order, unit=unit) for a in angles])

        @classmethod
        def OA(cls, o, a):
            """
            Create SE(3) pure rotation from two vectors

            :param o: 3-vector parallel to Y- axis
            :type o: array_like
            :param a: 3-vector parallel to the Z-axis
            :type o: array_like
            :return: 4x4 homogeneous transformation matrix
            :rtype: SE3 instance

            ``SE3.OA(O, A)`` is an SE(3) rotation defined in terms of
            vectors parallel to the Y- and Z-axes of its reference frame.  In robotics these axes are
            respectively called the orientation and approach vectors defined such that
            R = [N O A] and N = O x A.

            Notes:

            - The A vector is the only guaranteed to have the same direction in the resulting
              rotation matrix
            - O and A do not have to be unit-length, they are normalized
            - O and A do not have to be orthogonal, so long as they are not parallel
            - The vectors O and A are parallel to the Y- and Z-axes of the equivalent coordinate frame.

            :seealso: :func:`spatialmath.base.transforms3d.oa2r`
            """
            return cls(tr.oa2tr(o, a))

        @classmethod
        def AngVec(cls, theta, v, *, unit='rad'):
            """
            Create an SE(3) pure rotation matrix from rotation angle and axis

            :param theta: rotation
            :type theta: float
            :param unit: angular units: 'rad' [default], or 'deg'
            :type unit: str
            :param v: rotation axis, 3-vector
            :type v: array_like
            :return: 4x4 homogeneous transformation matrix
            :rtype: SE3 instance

            ``SE3.AngVec(THETA, V)`` is an SE(3) rotation defined by
            a rotation of ``THETA`` about the vector ``V``.

            Notes:

            - If ``THETA == 0`` then return identity matrix.
            - If ``THETA ~= 0`` then ``V`` must have a finite length.

            :seealso: :func:`~spatialmath.pose3d.SE3.angvec`, :func:`spatialmath.base.transforms3d.angvec2r`
            """
            return cls(tr.angvec2tr(theta, v, unit=unit))

        @classmethod
        def Exp(cls, S):
            """
            Create an SE(3) rotation matrix from se(3)

            :param S: Lie algebra se(3)
            :type S: numpy ndarray
            :return: 3x3 rotation matrix
            :rtype: SO3 instance

            - ``SE3.Exp(S)`` is an SE(3) rotation defined by its Lie algebra
              which is a 3x3 se(3) matrix (skew symmetric)
            - ``SE3.Exp(t)`` is an SE(3) rotation defined by a 6-element twist
              vector (the unique elements of the se(3) skew-symmetric matrix)

            :seealso: :func:`spatialmath.base.transforms3d.trexp`, :func:`spatialmath.base.transformsNd.skew`
            """
            if isinstance(S, np.ndarray) and S.shape[1] == 6:
                return cls([tr.trexp(s) for s in S])
            else:
                return cls(tr.trexp(S), check=False)

        @classmethod
        def Tx(cls, x):
            """
            Create SE(3) translation along the X-axis

            :param theta: translation distance along the X-axis
            :type theta: float
            :return: 4x4 homogeneous transformation matrix
            :rtype: SE3 instance

            `SE3.Tz(D)`` is an SE(3) translation of D along the x-axis
            """
            return cls(tr.transl(x, 0, 0))

        @classmethod
        def Ty(cls, y):
            """
            Create SE(3) translation along the Y-axis

            :param theta: translation distance along the Y-axis
            :type theta: float
            :return: 4x4 homogeneous transformation matrix
            :rtype: SE3 instance

            `SE3.Tz(D)`` is an SE(3) translation of D along the y-axis
            """
            return cls(tr.transl(0, y, 0))

        @classmethod
        def Tz(cls, z):
            """
            Create SE(3) translation along the Z-axis

            :param theta: translation distance along the Z-axis
            :type theta: float
            :return: 4x4 homogeneous transformation matrix
            :rtype: SE3 instance

            `SE3.Tz(D)`` is an SE(3) translation of D along the z-axis
            """
            return cls(tr.transl(0, 0, z))

        @classmethod
        def Delta(cls, d):
            r"""
            Create SE(3) from diffential motion

            :param d: differential motion
            :type d: 6-element array_like
            :return: 4x4 homogeneous transformation matrix
            :rtype: SE3 instance

            - ``T = delta2tr(d)`` is an SE(3) representing differential
              motion :math:`d = [\delta_x, \delta_y, \delta_z, \theta_x, \theta_y, \theta_z`.

            Reference: Robotics, Vision & Control: Second Edition, P. Corke, Springer 2016; p67.

            :seealso: :func:`~delta`, :func:`~spatialmath.base.transform3d.delta2tr`

            """
            return tr.tr2delta(self.A, X1.A)

    if __name__ == '__main__':   # pragma: no cover

        import pathlib
        import os.path

        exec(open(os.path.join(pathlib.Path(__file__).parent.absolute(), "test_pose3d.py")).read())