Created
December 24, 2022 04:27
-
-
Save goromal/fb15f44150ca4e0951acaee443f72d3e to your computer and use it in GitHub Desktop.
Deduce rotation conventions used in a software library (Python)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import numpy as np | |
from typing import Callable, Tuple | |
import functools | |
class EulerAnglesOrder: | |
NONE = 0 | |
@staticmethod | |
def toStr(v): | |
if v != EulerAnglesOrder.NONE: | |
return f"R = R({v[0]})R({v[1]})R({v[2]})" | |
else: | |
return "--" | |
class DirectionalityConvention: | |
NONE = 0 | |
B2W = 1 | |
W2B = 2 | |
@staticmethod | |
def toStr(v): | |
if v == DirectionalityConvention.B2W: | |
return "Body-to-World" | |
elif v == DirectionalityConvention.W2B: | |
return "World-to-Body" | |
else: | |
return "--" | |
class FunctionConvention: | |
NONE = 0 | |
ACTIVE = 1 | |
PASSIVE = 2 | |
@staticmethod | |
def toStr(v): | |
if v == FunctionConvention.ACTIVE: | |
return "Active" | |
elif v == FunctionConvention.PASSIVE: | |
return "Passive" | |
else: | |
return "--" | |
class QuatOrderingConvention: | |
NONE = 0 | |
SCALARFIRST = 1 | |
SCALARLAST = 2 | |
@staticmethod | |
def toStr(v): | |
if v == QuatOrderingConvention.SCALARFIRST: | |
return "Scalar First" | |
elif v == QuatOrderingConvention.SCALARLAST: | |
return "Scalar Last" | |
else: | |
return "--" | |
class QuatHandednessConvention: | |
NONE = 0 | |
RIGHT = 1 | |
LEFT = 2 | |
@staticmethod | |
def toStr(v): | |
if v == QuatHandednessConvention.RIGHT: | |
return "Right-Handed" | |
elif v == QuatHandednessConvention.LEFT: | |
return "Left-Handed" | |
else: | |
return "--" | |
def _directionality_from_mat(R: np.ndarray, axis: str) -> int: | |
if axis == "x": | |
if R[1, 2] < 0: | |
return DirectionalityConvention.B2W | |
else: | |
return DirectionalityConvention.W2B | |
elif axis == "y": | |
if R[2, 0] < 0: | |
return DirectionalityConvention.B2W | |
else: | |
return DirectionalityConvention.W2B | |
elif axis == "z": | |
if R[0, 1] < 0: | |
return DirectionalityConvention.B2W | |
else: | |
return DirectionalityConvention.W2B | |
else: | |
return DirectionalityConvention.NONE | |
def find_axis_angle_conventions(libname: str, Rfunc: Callable[[np.ndarray, float], np.ndarray]) -> None: | |
x = np.array([1.0, 0.0, 0.0]) | |
rod_d = _directionality_from_mat(Rfunc(x, 1.0), "x") | |
conventionStr = f""" | |
Axis-Angle Conventions for {libname}: | |
Rodrigues Directionality: {DirectionalityConvention.toStr(rod_d)} | |
""" | |
print(conventionStr) | |
def find_euler_conventions(libname: str, Rfunc: Callable[[float, float, float], np.ndarray]) -> None: | |
euler_o_arg = ["-", "-", "-"] | |
for i in range(3): | |
euler_args = [0.0, 0.0, 0.0] | |
euler_args[i] = 1.0 | |
R = Rfunc(*euler_args) | |
if abs(R[0, 0] - 1.0) < 1e-4: | |
euler_o_arg[i] = "x" | |
elif abs(R[1, 1] - 1.0) < 1e-4: | |
euler_o_arg[i] = "y" | |
elif abs(R[2, 2] - 1.0) < 1e-4: | |
euler_o_arg[i] = "z" | |
def rot_comparator(a, b): | |
# WARNING: sorting could be incomplete if there are two of the same axis | |
idx_a, axis_a = a | |
idx_b, axis_b = b | |
euler_args = [0.0, 0.0, 0.0] | |
euler_args[idx_a] = 1.0 | |
euler_args[idx_b] = 1.0 | |
R = Rfunc(*euler_args) | |
if axis_a == "x": | |
if axis_b == "x": | |
return 0 | |
elif axis_b == "y": | |
if abs(R[1, 0]) < 1e-4: | |
return 1 | |
else: | |
return -1 | |
else: | |
if abs(R[2, 0]) < 1e-4: | |
return 1 | |
else: | |
return -1 | |
elif axis_a == "y": | |
if axis_b == "x": | |
if abs(R[0, 1]) < 1e-4: | |
return 1 | |
else: | |
return -1 | |
elif axis_b == "y": | |
return 0 | |
else: | |
if abs(R[2, 1]) < 1e-4: | |
return 1 | |
else: | |
return -1 | |
elif axis_a == "z": | |
if axis_b == "x": | |
if abs(R[0, 2]) < 1e-4: | |
return 1 | |
else: | |
return -1 | |
elif axis_b == "y": | |
if abs(R[1, 2]) < 1e-4: | |
return 1 | |
else: | |
return -1 | |
else: | |
return 0 | |
else: | |
return 0 | |
tagged_args = [(i, c) for i, c in enumerate(euler_o_arg)] | |
sorted_tagged_args = sorted( | |
tagged_args, | |
key = functools.cmp_to_key(lambda a, b: rot_comparator(a, b)) | |
) | |
euler_o_mat = [tup[1] for tup in sorted_tagged_args] | |
euler_d = _directionality_from_mat(Rfunc(1.0, 0.0, 0.0), euler_o_arg[0]) | |
conventionStr = f""" | |
Euler Angle Conventions for {libname}: | |
Euler Argument Order: {euler_o_arg} | |
Euler Matrix Order: {EulerAnglesOrder.toStr(euler_o_mat)} | |
Euler Directionality: {DirectionalityConvention.toStr(euler_d)} | |
""" | |
print(conventionStr) | |
def find_quaternion_conventions( | |
libname: str, | |
Rfunc: Callable[[float, float, float, float], np.ndarray], | |
Qfunc: Callable[[Tuple[float, float, float, float], Tuple[float, float, float, float]], Tuple[float, float, float, float]] | |
) -> None: | |
R = Rfunc(1.0, 0.0, 0.0, 0.0) | |
if np.allclose(R, np.eye(3)): | |
quat_o = QuatOrderingConvention.SCALARFIRST | |
else: | |
R = Rfunc(0.0, 0.0, 0.0, 1.0) | |
if np.allclose(R, np.eye(3)): | |
quat_o = QuatOrderingConvention.SCALARLAST | |
else: | |
quat_o = QuatOrderingConvention.NONE | |
x = 0 | |
if quat_o == QuatOrderingConvention.SCALARFIRST: | |
(_, x, _, _) = Qfunc( | |
(0.0, 0.0, np.sqrt(3.0) / 2.0, 1.0 / 2.0), | |
(0.0, 0.0, 1.0 / 2.0, np.sqrt(3.0) / 2.0) | |
) | |
elif quat_o == QuatOrderingConvention.SCALARLAST: | |
(_, x, _, _) = Qfunc( | |
(0.0, np.sqrt(3.0) / 2.0, 1.0 / 2.0, 0.0), | |
(0.0, 1.0 / 2.0, np.sqrt(3.0) / 2.0, 0.0) | |
) | |
if abs(x - 0.5) < 1e-4: | |
quat_h = QuatHandednessConvention.RIGHT | |
elif abs(x + 0.5) < 1e-4: | |
quat_h = QuatHandednessConvention.LEFT | |
else: | |
quat_h = QuatHandednessConvention.NONE | |
quat_d = DirectionalityConvention.NONE | |
if quat_o == QuatOrderingConvention.SCALARFIRST: | |
quat_d = _directionality_from_mat(Rfunc(np.sqrt(2.0) / 2.0, np.sqrt(2.0) / 2.0, 0.0, 0.0), "x") | |
elif quat_o == QuatOrderingConvention.SCALARLAST: | |
quat_d = _directionality_from_mat(Rfunc(0.0, np.sqrt(2.0) / 2.0, 0.0, np.sqrt(2.0) / 2.0), "x") | |
quat_f = FunctionConvention.NONE | |
if quat_d == DirectionalityConvention.B2W: | |
quat_f = FunctionConvention.PASSIVE | |
elif quat_d == DirectionalityConvention.W2B: | |
if quat_o == QuatOrderingConvention.SCALARFIRST: | |
R = Rfunc(np.sqrt(2.0) / 2.0, 0.0, 0.0, np.sqrt(2.0) / 2.0) | |
if abs(R[0, 1] + 1.0) < 1e-4: | |
quat_f = FunctionConvention.PASSIVE | |
elif abs(R[0, 1] - 1.0) < 1e-4: | |
quat_f = FunctionConvention.ACTIVE | |
elif quat_o == QuatOrderingConvention.SCALARLAST: | |
R = Rfunc(0.0, 0.0, np.sqrt(2.0) / 2.0, np.sqrt(2.0) / 2.0) | |
if abs(R[0, 1] + 1.0) < 1e-4: | |
quat_f = FunctionConvention.PASSIVE | |
elif abs(R[0, 1] - 1.0) < 1e-4: | |
quat_f = FunctionConvention.ACTIVE | |
conventionStr = f""" | |
Quaternion Conventions for {libname}: | |
Quaternion Ordering: {QuatOrderingConvention.toStr(quat_o)} | |
Quaternion Handedness: {QuatHandednessConvention.toStr(quat_h)} | |
Quaternion Function: {FunctionConvention.toStr(quat_f)} | |
Quaternion Directionality: {DirectionalityConvention.toStr(quat_d)} | |
""" | |
print(conventionStr) | |
introstr = """ | |
NOTE: You're running find_rotational_conventions.py standalone, but it's most useful as an import to deduce the rotational conventions of the particular library that you're using. | |
Running an example from the following source code: | |
========= | |
test.py |================================================================= | |
========= | | |
from find_rotational_conventions import ( | | |
find_euler_conventions, | | |
find_axis_angle_conventions, | | |
find_quaternion_conventions, | | |
) | | |
import numpy as np | | |
from typing import Tuple | | |
from geometry import SO3 # https://github.com/goromal/geometry | | |
| | |
LIBNAME = "manif-geom-cpp/geometry" # Library being tested | | |
| | |
def euler2R(arg1: float, arg2: float, arg3: float) -> np.ndarray: | | |
return SO3.fromEuler(arg1, arg2, arg3).R() | | |
| | |
def axisAngle2R(axis: np.ndarray, angle: float) -> np.ndarray: | | |
return SO3.fromAxisAngle(axis, angle).R() | | |
| | |
def quat2R(q1: float, q2: float, q3: float, q4: float) -> np.ndarray: | | |
return SO3.fromQuat(q1, q2, q3, q4).R() | | |
| | |
def quatComp( | | |
q1: Tuple[float, float, float, float], | | |
q2: Tuple[float, float, float, float] | | |
) -> Tuple[float, float, float, float]: | | |
q = SO3.fromQuat(*q1) * SO3.fromQuat(*q2) | | |
return (q.w(), q.x(), q.y(), q.z()) | | |
| | |
find_axis_angle_conventions(LIBNAME, axisAngle2R) | | |
find_euler_conventions(LIBNAME, euler2R) | | |
find_quaternion_conventions(LIBNAME, quat2R, quatComp) | | |
| | |
========================================================================== | |
With output: | |
""" | |
def main(): | |
from geometry import SO3 | |
print(introstr) | |
LIBNAME = "manif-geom-cpp/geometry" | |
def euler2R(arg1: float, arg2: float, arg3: float) -> np.ndarray: | |
return SO3.fromEuler(arg1, arg2, arg3).R() | |
def axisAngle2R(axis: np.ndarray, angle: float) -> np.ndarray: | |
return SO3.fromAxisAngle(axis, angle).R() | |
def quat2R(q1: float, q2: float, q3: float, q4: float) -> np.ndarray: | |
return SO3.fromQuat(q1, q2, q3, q4).R() | |
def quatComp(q1: Tuple[float, float, float, float], q2: Tuple[float, float, float, float]) -> Tuple[float, float, float, float]: | |
q = SO3.fromQuat(*q1) * SO3.fromQuat(*q2) | |
return (q.w(), q.x(), q.y(), q.z()) | |
find_axis_angle_conventions(LIBNAME, axisAngle2R) | |
find_euler_conventions(LIBNAME, euler2R) | |
find_quaternion_conventions(LIBNAME, quat2R, quatComp) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment