Source code for pytransform3d.rotations._euler

"""Euler angles."""

import math

import numpy as np
from numpy.testing import assert_array_almost_equal

from ._angle import norm_angle, active_matrix_from_angle
from ._constants import half_pi, unitx, unity, unitz, eps
from ._matrix import check_matrix
from ._quaternion import check_quaternion
from ._utils import check_axis_index


[docs] def norm_euler(e, i, j, k): """Normalize Euler angle range. Parameters ---------- e : array-like, shape (3,) Rotation angles in radians about the axes i, j, k in this order. i : int from [0, 1, 2] The first rotation axis (0: x, 1: y, 2: z) j : int from [0, 1, 2] The second rotation axis (0: x, 1: y, 2: z) k : int from [0, 1, 2] The third rotation axis (0: x, 1: y, 2: z) Returns ------- e : array, shape (3,) Extracted rotation angles in radians about the axes i, j, k in this order. The first and last angle are normalized to [-pi, pi]. The middle angle is normalized to either [0, pi] (proper Euler angles) or [-pi/2, pi/2] (Cardan / Tait-Bryan angles). """ check_axis_index("i", i) check_axis_index("j", j) check_axis_index("k", k) alpha, beta, gamma = norm_angle(e) proper_euler = i == k if proper_euler: if beta < 0.0: alpha += np.pi beta *= -1.0 gamma -= np.pi elif abs(beta) > half_pi: alpha += np.pi beta = np.pi - beta gamma -= np.pi return norm_angle([alpha, beta, gamma])
[docs] def euler_near_gimbal_lock(e, i, j, k, tolerance=1e-6): """Check if Euler angles are close to gimbal lock. Parameters ---------- e : array-like, shape (3,) Rotation angles in radians about the axes i, j, k in this order. i : int from [0, 1, 2] The first rotation axis (0: x, 1: y, 2: z) j : int from [0, 1, 2] The second rotation axis (0: x, 1: y, 2: z) k : int from [0, 1, 2] The third rotation axis (0: x, 1: y, 2: z) tolerance : float Tolerance for the comparison. Returns ------- near_gimbal_lock : bool Indicates if the Euler angles are near the gimbal lock singularity. """ e = norm_euler(e, i, j, k) beta = e[1] proper_euler = i == k if proper_euler: return abs(beta) < tolerance or abs(beta - np.pi) < tolerance else: return abs(abs(beta) - half_pi) < tolerance
[docs] def assert_euler_equal(e1, e2, i, j, k, *args, **kwargs): """Raise an assertion if two Euler angles are not approximately equal. Parameters ---------- e1 : array-like, shape (3,) Rotation angles in radians about the axes i, j, k in this order. e2 : array-like, shape (3,) Rotation angles in radians about the axes i, j, k in this order. i : int from [0, 1, 2] The first rotation axis (0: x, 1: y, 2: z) j : int from [0, 1, 2] The second rotation axis (0: x, 1: y, 2: z) k : int from [0, 1, 2] The third rotation axis (0: x, 1: y, 2: z) args : tuple Positional arguments that will be passed to `assert_array_almost_equal` kwargs : dict Positional arguments that will be passed to `assert_array_almost_equal` """ e1 = norm_euler(e1, i, j, k) e2 = norm_euler(e2, i, j, k) assert_array_almost_equal(e1, e2, *args, **kwargs)
[docs] def matrix_from_euler(e, i, j, k, extrinsic): """General method to compute active rotation matrix from any Euler angles. Parameters ---------- e : array-like, shape (3,) Rotation angles in radians about the axes i, j, k in this order. i : int from [0, 1, 2] The first rotation axis (0: x, 1: y, 2: z) j : int from [0, 1, 2] The second rotation axis (0: x, 1: y, 2: z) k : int from [0, 1, 2] The third rotation axis (0: x, 1: y, 2: z) extrinsic : bool Do we use extrinsic transformations? Intrinsic otherwise. Returns ------- R : array, shape (3, 3) Active rotation matrix Raises ------ ValueError If basis is invalid """ check_axis_index("i", i) check_axis_index("j", j) check_axis_index("k", k) alpha, beta, gamma = e if not extrinsic: i, k = k, i alpha, gamma = gamma, alpha R = ( active_matrix_from_angle(k, gamma) .dot(active_matrix_from_angle(j, beta)) .dot(active_matrix_from_angle(i, alpha)) ) return R
def general_intrinsic_euler_from_active_matrix( R, n1, n2, n3, proper_euler, strict_check=True ): """General algorithm to extract intrinsic euler angles from a matrix. The implementation is based on SciPy's implementation: https://github.com/scipy/scipy/blob/743c283bbe79473a03ca2eddaa537661846d8a19/scipy/spatial/transform/_rotation.pyx Parameters ---------- R : array-like, shape (3, 3) Active rotation matrix n1 : array, shape (3,) First rotation axis (basis vector) n2 : array, shape (3,) Second rotation axis (basis vector) n3 : array, shape (3,) Third rotation axis (basis vector) proper_euler : bool Is this an Euler angle convention or a Cardan / Tait-Bryan convention? Proper Euler angles rotate about the same axis twice, for example, z, y', and z''. strict_check : bool, optional (default: True) Raise a ValueError if the rotation matrix is not numerically close enough to a real rotation matrix. Otherwise we print a warning. Returns ------- euler_angles : array, shape (3,) Extracted intrinsic rotation angles in radians about the axes n1, n2, and n3 in this order. The first and last angle are normalized to [-pi, pi]. The middle angle is normalized to either [0, pi] (proper Euler angles) or [-pi/2, pi/2] (Cardan / Tait-Bryan angles). References ---------- .. [1] Shuster, M. D., Markley, F. L. (2006). General Formula for Extracting the Euler Angles. Journal of Guidance, Control, and Dynamics, 29(1), pp 2015-221, doi: 10.2514/1.16622. https://arc.aiaa.org/doi/abs/10.2514/1.16622 """ D = check_matrix(R, strict_check=strict_check) # Differences to the paper: # - we call the angles alpha, beta, and gamma # - we obtain angles from intrinsic rotations, thus some matrices are # transposed like in SciPy's implementation # Step 2 # - Equation 5 n1_cross_n2 = np.cross(n1, n2) lmbda = np.arctan2(np.dot(n1_cross_n2, n3), np.dot(n1, n3)) # - Equation 6 C = np.vstack((n2, n1_cross_n2, n1)) # Step 3 # - Equation 8 CDCT = np.dot(np.dot(C, D), C.T) O = np.dot(CDCT, active_matrix_from_angle(0, lmbda).T) # Step 4 # Fix numerical issue if O_22 is slightly out of range of arccos O_22 = max(min(O[2, 2], 1.0), -1.0) # - Equation 10a beta = lmbda + np.arccos(O_22) safe1 = abs(beta - lmbda) >= np.finfo(float).eps safe2 = abs(beta - lmbda - np.pi) >= np.finfo(float).eps if safe1 and safe2: # Default case, no gimbal lock # Step 5 # - Equation 10b alpha = np.arctan2(O[0, 2], -O[1, 2]) # - Equation 10c gamma = np.arctan2(O[2, 0], O[2, 1]) # Step 7 if proper_euler: valid_beta = 0.0 <= beta <= np.pi else: # Cardan / Tait-Bryan angles valid_beta = -0.5 * np.pi <= beta <= 0.5 * np.pi # - Equation 12 if not valid_beta: alpha += np.pi beta = 2.0 * lmbda - beta gamma -= np.pi else: # Step 6 - Handle gimbal locks # a) gamma = 0.0 if not safe1: # b) alpha = np.arctan2(O[1, 0] - O[0, 1], O[0, 0] + O[1, 1]) else: # c) alpha = np.arctan2(O[1, 0] + O[0, 1], O[0, 0] - O[1, 1]) euler_angles = norm_angle([alpha, beta, gamma]) return euler_angles
[docs] def euler_from_matrix(R, i, j, k, extrinsic, strict_check=True): """General method to extract any Euler angles from active rotation matrix. Parameters ---------- R : array-like, shape (3, 3) Active rotation matrix i : int from [0, 1, 2] The first rotation axis (0: x, 1: y, 2: z) j : int from [0, 1, 2] The second rotation axis (0: x, 1: y, 2: z) k : int from [0, 1, 2] The third rotation axis (0: x, 1: y, 2: z) extrinsic : bool Do we use extrinsic transformations? Intrinsic otherwise. strict_check : bool, optional (default: True) Raise a ValueError if the rotation matrix is not numerically close enough to a real rotation matrix. Otherwise we print a warning. Returns ------- euler_angles : array, shape (3,) Extracted rotation angles in radians about the axes i, j, k in this order. The first and last angle are normalized to [-pi, pi]. The middle angle is normalized to either [0, pi] (proper Euler angles) or [-pi/2, pi/2] (Cardan / Tait-Bryan angles). Raises ------ ValueError If basis is invalid References ---------- .. [1] Shuster, M. D., Markley, F. L. (2006). General Formula for Extracting the Euler Angles. Journal of Guidance, Control, and Dynamics, 29(1), pp 2015-221, doi: 10.2514/1.16622. https://arc.aiaa.org/doi/abs/10.2514/1.16622 """ check_axis_index("i", i) check_axis_index("j", j) check_axis_index("k", k) basis_vectors = [unitx, unity, unitz] proper_euler = i == k if extrinsic: i, k = k, i e = general_intrinsic_euler_from_active_matrix( R, basis_vectors[i], basis_vectors[j], basis_vectors[k], proper_euler, strict_check, ) if extrinsic: e = e[::-1] return e
[docs] def euler_from_quaternion(q, i, j, k, extrinsic): """General method to extract any Euler angles from quaternions. Parameters ---------- q : array-like, shape (4,) Unit quaternion to represent rotation: (w, x, y, z) i : int from [0, 1, 2] The first rotation axis (0: x, 1: y, 2: z) j : int from [0, 1, 2] The second rotation axis (0: x, 1: y, 2: z) k : int from [0, 1, 2] The third rotation axis (0: x, 1: y, 2: z) extrinsic : bool Do we use extrinsic transformations? Intrinsic otherwise. Returns ------- euler_angles : array, shape (3,) Extracted rotation angles in radians about the axes i, j, k in this order. The first and last angle are normalized to [-pi, pi]. The middle angle is normalized to either [0, pi] (proper Euler angles) or [-pi/2, pi/2] (Cardan / Tait-Bryan angles). Raises ------ ValueError If basis is invalid References ---------- .. [1] Bernardes, E., Viollet, S. (2022). Quaternion to Euler angles conversion: A direct, general and computationally efficient method. PLOS ONE, 17(11), doi: 10.1371/journal.pone.0276302. """ q = check_quaternion(q) check_axis_index("i", i) check_axis_index("j", j) check_axis_index("k", k) i += 1 j += 1 k += 1 # The original algorithm assumes extrinsic convention. Hence, we swap # the order of axes for intrinsic rotation. if not extrinsic: i, k = k, i # Proper Euler angles rotate about the same axis in the first and last # rotation. If this is not the case, they are called Cardan or Tait-Bryan # angles and have to be handled differently. proper_euler = i == k if proper_euler: k = 6 - i - j sign = (i - j) * (j - k) * (k - i) // 2 a = q[0] b = q[i] c = q[j] d = q[k] * sign if not proper_euler: a, b, c, d = a - c, b + d, c + a, d - b # Equation 34 is used instead of Equation 35 as atan2 it is numerically # more accurate than acos. angle_j = 2.0 * math.atan2(math.hypot(c, d), math.hypot(a, b)) # Check for singularities if abs(angle_j) <= eps: singularity = 1 elif abs(angle_j - math.pi) <= eps: singularity = 2 else: singularity = 0 # Equation 25 # (theta_1 + theta_3) / 2 half_sum = math.atan2(b, a) # (theta_1 - theta_3) / 2 half_diff = math.atan2(d, c) if singularity == 0: # no singularity # Equation 32 angle_i = half_sum + half_diff angle_k = half_sum - half_diff elif extrinsic: # singularity angle_k = 0.0 if singularity == 1: angle_i = 2.0 * half_sum else: assert singularity == 2 angle_i = 2.0 * half_diff else: # intrinsic, singularity angle_i = 0.0 if singularity == 1: angle_k = 2.0 * half_sum else: assert singularity == 2 angle_k = -2.0 * half_diff if not proper_euler: # Equation 43 angle_j -= math.pi / 2.0 # Equation 44 angle_i *= sign angle_k = norm_angle(angle_k) angle_i = norm_angle(angle_i) if extrinsic: return np.array([angle_k, angle_j, angle_i]) return np.array([angle_i, angle_j, angle_k])