import numpy as np
from .so3 import RToAxisAngle, rpy_to_R, skew
[docs]
def screw_axis_from_w_q(w: np.ndarray, q: np.ndarray) -> np.ndarray:
"""Build a revolute screw axis from a rotation axis and a point on the axis.
A revolute screw axis has zero pitch, so the linear component is
determined entirely by the cross-product rule:
.. math::
\\mathcal{S} = \\begin{bmatrix} \\hat{\\omega} \\\\ v \\end{bmatrix},
\\quad v = -\\hat{\\omega} \\times q = -[\\hat{\\omega}]\\, q
Args:
w: Unit rotation axis :math:`\\hat{\\omega} \\in \\mathbb{R}^3`.
q: Any point on the rotation axis :math:`q \\in \\mathbb{R}^3`.
Returns:
6-vector :math:`\\mathcal{S} = [\\hat{\\omega}^\\top,\\, v^\\top]^\\top`.
"""
w = np.asarray(w, dtype=float).reshape(
3,
)
q = np.asarray(q, dtype=float).reshape(
3,
)
return np.concatenate([w, -skew(w) @ q])
[docs]
def vec_to_se3(V: np.ndarray) -> np.ndarray:
"""Lift a 6-vector twist to its 4×4 :math:`\\mathfrak{se}(3)` matrix (hat map).
.. math::
\\mathcal{V} = \\begin{bmatrix} \\omega \\\\ v \\end{bmatrix}
\\;\\longmapsto\\;
[\\mathcal{V}] = \\begin{bmatrix} [\\omega] & v \\\\ 0 & 0 \\end{bmatrix}
\\in \\mathfrak{se}(3)
Args:
V: 6-vector :math:`[\\omega^\\top, v^\\top]^\\top` of twist coordinates.
Returns:
4×4 :math:`\\mathfrak{se}(3)` matrix :math:`[\\mathcal{V}]`.
"""
hat_V = np.zeros((4, 4))
hat_V[:3, :3] = skew(V[:3])
hat_V[:3, 3] = V[3:]
return hat_V
[docs]
def exp_screw_hat(hat_S: np.ndarray, theta: float) -> np.ndarray:
"""Compute the matrix exponential :math:`e^{[\\mathcal{S}]\\theta} \\in SE(3)`.
For a revolute-type screw axis (:math:`\\|\\omega\\| > 0`):
.. math::
T = e^{[\\mathcal{S}]\\theta} =
\\begin{bmatrix}
e^{[\\omega]\\theta} & G(\\theta)\\, v \\\\
0 & 1
\\end{bmatrix}
where :math:`e^{[\\omega]\\theta}` uses Rodrigues' formula and
.. math::
G(\\theta) = I\\theta + (1-\\cos\\theta)[\\omega]
+ (\\theta - \\sin\\theta)[\\omega]^2.
For a pure-translation axis (:math:`\\omega = 0`):
.. math::
T = \\begin{bmatrix} I & v\\,\\theta \\\\ 0 & 1 \\end{bmatrix}.
Args:
hat_S: 4×4 :math:`\\mathfrak{se}(3)` matrix :math:`[\\mathcal{S}]`.
theta: Joint displacement (radians for revolute, metres for prismatic).
Returns:
4×4 homogeneous transformation :math:`T \\in SE(3)`.
"""
hat_w = hat_S[:3, :3]
v = hat_S[:3, 3]
w = np.array([hat_w[2, 1], hat_w[0, 2], hat_w[1, 0]])
wn = np.linalg.norm(w)
T = np.eye(4)
if wn < 1e-12:
# pure translation
T[:3, :3] = np.eye(3)
T[:3, 3] = v * theta
return T
R = np.eye(3) + np.sin(theta) * hat_w + (1 - np.cos(theta)) * (hat_w @ hat_w)
G = (
np.eye(3) * theta
+ (1 - np.cos(theta)) * hat_w
+ (theta - np.sin(theta)) * (hat_w @ hat_w)
)
T[:3, :3] = R
T[:3, 3] = G @ v
return T
[docs]
def exp_screw_axis(S: np.ndarray, theta: float) -> np.ndarray:
"""Compute the matrix exponential :math:`e^{[\\mathcal{S}]\\theta} \\in SE(3)`.
Convenience wrapper around :func:`exp_screw_hat` that accepts the 6-vector
form of the screw axis directly.
.. math::
T = e^{[\\mathcal{S}]\\theta}, \\quad \\mathcal{S} \\in \\mathbb{R}^6.
Args:
S: 6-vector :math:`[\\omega^\\top, v^\\top]^\\top` screw axis.
theta: Joint displacement (radians for revolute, metres for prismatic).
Returns:
4×4 homogeneous transformation :math:`T \\in SE(3)`.
"""
hat_S = vec_to_se3(S)
return exp_screw_hat(hat_S, theta)
[docs]
def log_screw_axis(T: np.ndarray) -> tuple[np.ndarray, float]:
"""Compute the matrix logarithm of an SE(3) element (inverse of :func:`exp_screw_axis`).
Returns the screw axis :math:`\\mathcal{S}` and displacement :math:`\\theta`
such that :math:`T = e^{[\\mathcal{S}]\\theta}`.
For the revolute case (:math:`\\|\\omega\\| > 0`):
.. math::
\\theta = \\arccos\\!\\left(\\frac{\\operatorname{tr}(R)-1}{2}\\right),
\\qquad
v = G^{-1}(\\theta)\\, p
where
.. math::
G^{-1}(\\theta) = \\frac{I}{\\theta}
- \\frac{[\\omega]}{2}
+ \\left(\\frac{1}{\\theta} - \\frac{1}{2}\\cot\\frac{\\theta}{2}\\right)[\\omega]^2.
For the pure-translation case (:math:`\\omega = 0`):
.. math::
\\mathcal{S} = [0,\\, \\hat{v}]^\\top, \\quad \\theta = \\|p\\|.
Args:
T: 4×4 homogeneous transformation :math:`T \\in SE(3)`.
Returns:
``(S, theta)`` where *S* is the 6-vector screw axis and *theta* is
the scalar displacement.
"""
R = T[:3, :3]
p = T[:3, 3]
w, theta = RToAxisAngle(R)
if np.linalg.norm(w * theta) < 1e-12:
# pure translation
v = p / np.linalg.norm(p)
return np.concatenate([np.zeros(3), v]), float(np.linalg.norm(p))
G_inv = (
np.eye(3) / theta
- 0.5 * skew(w)
+ (1 / theta - 0.5 / np.tan(theta / 2)) * (skew(w) @ skew(w))
)
v = G_inv @ p
return np.concatenate([w, v]), float(theta)
[docs]
def inv_SE3(T: np.ndarray) -> np.ndarray:
"""Compute the closed-form inverse of a homogeneous transformation.
Exploits the structure of :math:`SE(3)` to avoid a generic matrix inversion:
.. math::
T^{-1} = \\begin{bmatrix} R^\\top & -R^\\top p \\\\ 0 & 1 \\end{bmatrix}
Args:
T: 4×4 homogeneous transformation :math:`T \\in SE(3)`.
Returns:
4×4 inverse :math:`T^{-1} \\in SE(3)`.
"""
T = np.asarray(T, dtype=float).reshape(4, 4)
R = T[:3, :3]
p = T[:3, 3]
T_inv = np.eye(4)
T_inv[:3, :3] = R.T
T_inv[:3, 3] = -R.T @ p
return T_inv
[docs]
def adjoint(T: np.ndarray) -> np.ndarray:
"""Compute the 6×6 adjoint representation :math:`[\\text{Ad}_T]` of an SE(3) element.
.. math::
[\\text{Ad}_T] =
\\begin{bmatrix} R & 0 \\\\ [p]R & R \\end{bmatrix}
where :math:`[p]` is the skew-symmetric matrix of the translation :math:`p`.
Args:
T: 4×4 homogeneous transformation :math:`T \\in SE(3)`.
Returns:
6×6 adjoint matrix :math:`[\\text{Ad}_T]`.
"""
T = np.asarray(T, dtype=float).reshape(4, 4)
R = T[:3, :3]
p = T[:3, 3]
return np.block([[R, np.zeros((3, 3))], [skew(p) @ R, R]])
[docs]
def adjoint_inverse(T: np.ndarray) -> np.ndarray:
"""Compute the 6×6 inverse adjoint :math:`[\\text{Ad}_{T^{-1}}]`.
.. math::
[\\text{Ad}_{T^{-1}}] =
\\begin{bmatrix} R^\\top & 0 \\\\ -R^\\top[p] & R^\\top \\end{bmatrix}
This is equivalent to :math:`[\\text{Ad}_T]^{-1}` but computed without
a matrix inversion.
Args:
T: 4×4 homogeneous transformation :math:`T \\in SE(3)`.
Returns:
6×6 inverse adjoint matrix :math:`[\\text{Ad}_{T^{-1}}]`.
"""
T = np.asarray(T, dtype=float).reshape(4, 4)
R = T[:3, :3]
p = T[:3, 3]
Ad_T_inv = np.block([[R.T, np.zeros((3, 3))], [-R.T @ skew(p), R.T]])
return Ad_T_inv