airo-spatial-algebra 2025.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: airo_spatial_algebra
3
+ Version: 2025.4.0
4
+ Summary: Code for working with SE3 poses, transforms... for robotic manipulation at the Ghent University AI and Robotics Lab
5
+ Author: Thomas Lips
6
+ Author-email: thomas.lips@ugent.be
7
+ Requires-Dist: numpy<2.0
8
+ Requires-Dist: scipy
9
+ Requires-Dist: spatialmath-python
10
+ Requires-Dist: airo-typing==2025.4.0
11
+ Dynamic: author
12
+ Dynamic: author-email
13
+ Dynamic: requires-dist
14
+ Dynamic: summary
@@ -0,0 +1,7 @@
1
+ # airo-spatial-algebra
2
+
3
+ This package provides functionality to work with SE3 poses, transforms etc. The heavy lifting is done by Peter Corke's [spatial-math](https://github.com/petercorke/spatialmath-python) package. We simply wrap a subset of its features to make it more verbose and self-explanatory.
4
+
5
+ The package contains an `SE3Container` class for converting between different representations of SE3 poses (and hence SO3 rotations as well).
6
+ Furthermore a few common operations on points and poses (such as changing the frame in which they are represented) are provided for convenience.
7
+
@@ -0,0 +1,4 @@
1
+ from airo_spatial_algebra.operations import transform_points
2
+ from airo_spatial_algebra.se3 import SE3Container
3
+
4
+ __all__ = ["SE3Container", "transform_points"]
@@ -0,0 +1,82 @@
1
+ """
2
+ This file contains some operations on points and poses.
3
+
4
+ It also defines a helper class for a collection of points, but this is only used internally
5
+ to allow the user to directly interact with np.arrays to reduce friction.
6
+
7
+ i.e. the user can call <transform>(points) where points is just a numpy array,
8
+ instead of having to first convert the points to a specific format.
9
+ """
10
+
11
+ import numpy as np
12
+ from airo_typing import HomogeneousMatrixType, Vector3DArrayType, Vectors3DType
13
+
14
+
15
+ class _HomogeneousPoints:
16
+ """Helper class to facilitate multiplicating 4x4 matrices with one or more 3D points.
17
+ This class internally handles the addition / removal of a dimension to the points.
18
+ """
19
+
20
+ # TODO: extend to generic dimensions (1D,2D,3D).
21
+ def __init__(self, points: Vectors3DType):
22
+ if not self.is_valid_points_type(points):
23
+ raise ValueError(f"Invalid argument for {_HomogeneousPoints.__name__}.__init__ ")
24
+
25
+ points = _HomogeneousPoints.ensure_array_2d(points)
26
+ self._homogeneous_points = np.concatenate([points, np.ones((points.shape[0], 1), dtype=np.float32)], axis=1)
27
+
28
+ @staticmethod
29
+ def is_valid_points_type(points: Vectors3DType) -> bool:
30
+ if len(points.shape) == 1:
31
+ if len(points) == 3:
32
+ return True
33
+ elif len(points.shape) == 2:
34
+ if points.shape[1] == 3:
35
+ return True
36
+ return False
37
+
38
+ @staticmethod
39
+ def ensure_array_2d(points: Vectors3DType) -> Vector3DArrayType:
40
+ """If points is a single shape (3,) point, then it is reshaped to (1,3)."""
41
+ if len(points.shape) == 1:
42
+ if len(points) != 3:
43
+ raise ValueError("points has only one dimension, but it's length is not 3")
44
+ points = points.reshape((1, 3))
45
+ return points
46
+
47
+ @property
48
+ def homogeneous_points(self) -> np.ndarray:
49
+ """Nx4 matrix representing the homogeneous points"""
50
+ return self._homogeneous_points
51
+
52
+ @property
53
+ def points(self) -> Vectors3DType:
54
+ """Nx3 matrix representing the points"""
55
+ # normalize points (for safety, should never be necessary with affine transforms)
56
+ # but we've had bugs of this type with projection operations, so better safe than sorry?
57
+ scalars = self._homogeneous_points[:, 3][:, np.newaxis]
58
+ points = self.homogeneous_points[:, :3] / scalars
59
+ # TODO: if the original poitns was (1,3) matrix, then the resulting points would be a (3,) vector.
60
+ # Is this desirable? and if not, how to avoid it?
61
+ if points.shape[0] == 1:
62
+ # single point -> create vector from 1x3 matrix
63
+ return points[0]
64
+ else:
65
+ return points
66
+
67
+ def apply_transform(self, homogeneous_transform_matrix: HomogeneousMatrixType) -> None:
68
+ self._homogeneous_points = (homogeneous_transform_matrix @ self.homogeneous_points.transpose()).transpose()
69
+
70
+
71
+ def transform_points(homogeneous_transform_matrix: HomogeneousMatrixType, points: Vectors3DType) -> Vectors3DType:
72
+ """Applies a transform to a (set of) point(s).
73
+
74
+ Args:
75
+ homogeneous_transform_matrix (HomogeneousMatrixType): _description_
76
+ points (PointsType): _description_
77
+ Returns:
78
+ PointsType: (3,) vector or (N,3) matrix.
79
+ """
80
+ homogeneous_points = _HomogeneousPoints(points)
81
+ homogeneous_points.apply_transform(homogeneous_transform_matrix)
82
+ return homogeneous_points.points
@@ -0,0 +1,214 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional # use class as type for class methods
4
+
5
+ import numpy as np
6
+ from airo_typing import (
7
+ AxisAngleType,
8
+ EulerAnglesType,
9
+ HomogeneousMatrixType,
10
+ QuaternionType,
11
+ RotationMatrixType,
12
+ RotationVectorType,
13
+ Vector3DType,
14
+ )
15
+ from scipy.spatial.transform import Rotation
16
+ from spatialmath import SE3, UnitQuaternion
17
+ from spatialmath.base import trnorm
18
+
19
+
20
+ class SE3Container:
21
+ """A container class for SE3 elements. These elements are used to represent the 3D Pose of an element A in a frame B,
22
+ or stated differently the transform from frame B to frame A.
23
+
24
+ Conventions:
25
+ translations are in meters,rotations in radians.
26
+ quaternions are scalar-last and normalized.
27
+ euler angles are the angles of consecutive rotations around the original X-Y-Z axis (in that order).
28
+
29
+ Note that ther exist many different types of euler angels that differ in the order of axes,
30
+ and in whether they rotate around the original and the new axes. We chose this convention as it is the most common in robotics
31
+ and also easy to reason about. use the Scipy.transform.Rotation class if you need to convert from/to other formats.
32
+
33
+ This is a wrapper around the SE3 class of Peter Corke's Spatial Math Library: https://petercorke.github.io/spatialmath-python/
34
+ The scope if this class is not to perform arbitrary calculations on SE3 elements,
35
+ it is merely a 'simplified and more readable' wrapper
36
+ that facilitates creating/retrieving position and/or orientations in various formats.
37
+
38
+ If you need support for calculations and/or more ways to create SE3 elements, use Peter Corke's Spatial Math Library directly.
39
+ You can decide this on the fly as you can always access the SE3 attribute of this class or instantiate this class from an SE3 object
40
+ """
41
+
42
+ def __init__(self, se3: SE3) -> None: # type: ignore
43
+ self.se3 = se3
44
+
45
+ @classmethod
46
+ def random(cls) -> SE3Container:
47
+ """A random SE3 element with translations in the [-1,1]^3 cube."""
48
+ return cls(SE3.Rand())
49
+
50
+ @classmethod
51
+ def from_translation(cls, translation: Vector3DType) -> SE3Container:
52
+ """creates a translation-only SE3 element"""
53
+ return cls(SE3.Trans(translation.tolist()))
54
+
55
+ @classmethod
56
+ def from_homogeneous_matrix(cls, matrix: HomogeneousMatrixType) -> SE3Container:
57
+ _assert_is_se3_matrix(matrix)
58
+ return cls(SE3(matrix))
59
+
60
+ @classmethod
61
+ def from_rotation_matrix_and_translation(
62
+ cls, rotation_matrix: RotationMatrixType, translation: Optional[Vector3DType] = None
63
+ ) -> SE3Container:
64
+ _assert_is_so3_matrix(rotation_matrix)
65
+ return cls(SE3.Rt(rotation_matrix, translation))
66
+
67
+ @classmethod
68
+ def from_rotation_vector_and_translation(
69
+ cls, rotation_vector: RotationVectorType, translation: Optional[Vector3DType] = None
70
+ ) -> SE3Container:
71
+ return cls(SE3.Rt(Rotation.from_rotvec(rotation_vector).as_matrix(), translation))
72
+
73
+ @classmethod
74
+ def from_quaternion_and_translation(
75
+ cls, quaternion: QuaternionType, translation: Optional[Vector3DType] = None
76
+ ) -> SE3Container:
77
+ q = UnitQuaternion(quaternion[3], quaternion[:3]) # scalar-first in math lib
78
+ return cls(SE3.Rt(q.R, translation))
79
+
80
+ @classmethod
81
+ def from_euler_angles_and_translation(
82
+ cls, euler_angels: EulerAnglesType, translation: Optional[Vector3DType] = None
83
+ ) -> SE3Container:
84
+ # convert from extrinsic XYZ to rotmatrix
85
+ # bc SE3.Eul does not accept translation
86
+ rot_matrix = Rotation.from_euler("xyz", euler_angels, degrees=False).as_matrix()
87
+ return cls.from_rotation_matrix_and_translation(rot_matrix, translation)
88
+
89
+ @classmethod
90
+ def from_orthogonal_base_vectors_and_translation(
91
+ cls,
92
+ x_axis: Vector3DType,
93
+ y_axis: Vector3DType,
94
+ z_axis: Vector3DType,
95
+ translation: Optional[Vector3DType] = None,
96
+ ) -> SE3Container:
97
+ # create orientation matrix with base vectors as columns
98
+ orientation_matrix = np.zeros((3, 3))
99
+ for i, axis in enumerate([x_axis, y_axis, z_axis]):
100
+ orientation_matrix[:, i] = axis / np.linalg.norm(axis)
101
+
102
+ _assert_is_so3_matrix(orientation_matrix)
103
+
104
+ return cls(SE3.Rt(orientation_matrix, translation))
105
+
106
+ @property
107
+ def orientation_as_quaternion(self) -> QuaternionType:
108
+ angle, vec = self.se3.angvec()
109
+ scalar_first_quaternion = UnitQuaternion.AngVec(angle, vec).A
110
+ return self.scalar_first_quaternion_to_scalar_last(scalar_first_quaternion)
111
+
112
+ @property
113
+ def orientation_as_euler_angles(self) -> EulerAnglesType:
114
+ zyx_ordered_angles = self.se3.eul()
115
+ # convert from intrinsic ZYZ to extrinsic xyz
116
+ return Rotation.from_euler("ZYZ", zyx_ordered_angles, degrees=False).as_euler("xyz", degrees=False)
117
+
118
+ @property
119
+ def orientation_as_axis_angle(self) -> AxisAngleType:
120
+ angle, axis = self.se3.angvec()
121
+ return axis.astype(np.float64), float(angle)
122
+
123
+ @property
124
+ def orientation_as_rotation_vector(self) -> Vector3DType:
125
+ axis, angle = self.orientation_as_axis_angle
126
+ if axis is None:
127
+ return np.zeros(3)
128
+ return angle * axis
129
+
130
+ @property
131
+ def rotation_matrix(self) -> RotationMatrixType:
132
+ return self.se3.R
133
+
134
+ @property
135
+ def homogeneous_matrix(self) -> HomogeneousMatrixType:
136
+ return self.se3.A
137
+
138
+ @property
139
+ def translation(self) -> Vector3DType:
140
+ # TODO: should this be named position or translation?
141
+ return self.se3.t
142
+
143
+ @property
144
+ def x_axis(self) -> Vector3DType:
145
+ """also called normal vector. This is the first column of the rotation matrix"""
146
+ return self.se3.n
147
+
148
+ @property
149
+ def y_axis(self) -> Vector3DType:
150
+ """also colled orientation vector. This is the second column of the rotation matrix"""
151
+ return self.se3.o
152
+
153
+ @property
154
+ def z_axis(self) -> Vector3DType:
155
+ """also called approach vector. This is the third column of the rotation matrix"""
156
+ return self.se3.a
157
+
158
+ def __str__(self) -> str:
159
+ return str(f"SE3 -> \n {self.homogeneous_matrix}")
160
+
161
+ @staticmethod
162
+ def scalar_first_quaternion_to_scalar_last(scalar_first_quaternion: np.ndarray) -> QuaternionType:
163
+ scalar_last_quaternion = np.roll(scalar_first_quaternion, -1)
164
+ return scalar_last_quaternion
165
+
166
+ @staticmethod
167
+ def scalar_last_quaternion_to_scalar_first(scalar_last_quaternion: QuaternionType) -> np.ndarray:
168
+ scalar_first_quaternion = np.roll(scalar_last_quaternion, 1)
169
+ return scalar_first_quaternion
170
+
171
+
172
+ def normalize_so3_matrix(matrix: np.ndarray) -> np.ndarray:
173
+ """normalize an SO3 matrix (i.e. a rotation matrix) to be orthogonal and have determinant 1 (right-handed coordinate system)
174
+ see https://en.wikipedia.org/wiki/3D_rotation_group
175
+
176
+ Can be used to fix numerical issues with rotation matrices
177
+
178
+ will make sure x,y,z are unit vectors, then
179
+ will construct new x vector as y cross z, then construct new y vector as z cross x, so that x,y,z are orthogonal
180
+
181
+ """
182
+ assert matrix.shape == (3, 3), "matrix is not a 3x3 matrix"
183
+ return trnorm(matrix)
184
+
185
+
186
+ def _assert_is_so3_matrix(matrix: np.ndarray) -> None:
187
+ """check if matrix is a valid SO3 matrix
188
+ this requires the matrix to be orthogonal (base vectors are perpendicular) and have determinant 1 (right-handed coordinate system)
189
+ see https://en.wikipedia.org/wiki/3D_rotation_group
190
+
191
+ This function will raise a ValueError if the matrix is not valid
192
+
193
+ """
194
+ if matrix.shape != (3, 3):
195
+ raise ValueError("matrix is not a 3x3 matrix")
196
+ if not np.allclose(matrix @ matrix.T, np.eye(3)):
197
+ raise ValueError(
198
+ "matrix is not orthnormal, i.e. its base vectors are not perpendicular. If you are sure this is a numerical issue, use normalize_so3_matrix()"
199
+ )
200
+ if not np.allclose(np.linalg.det(matrix), 1):
201
+ raise ValueError("matrix does not have determinant 1 (not right-handed)")
202
+
203
+
204
+ def _assert_is_se3_matrix(matrix: np.ndarray) -> None:
205
+ """check if matrix is a valid SE3 matrix (i.e. a valid pose)
206
+ this requires the rotation part to be a valid SO3 matrix and the translation part to be a 3D vector
207
+
208
+ This function will raise a ValueError if the matrix is not valid
209
+ """
210
+ if matrix.shape != (4, 4):
211
+ raise ValueError("matrix is not a 4x4 matrix")
212
+ if not np.allclose(matrix[3, :], np.array([0, 0, 0, 1])):
213
+ raise ValueError("last row of matrix is not [0,0,0,1]")
214
+ _assert_is_so3_matrix(matrix[:3, :3])
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: airo_spatial_algebra
3
+ Version: 2025.4.0
4
+ Summary: Code for working with SE3 poses, transforms... for robotic manipulation at the Ghent University AI and Robotics Lab
5
+ Author: Thomas Lips
6
+ Author-email: thomas.lips@ugent.be
7
+ Requires-Dist: numpy<2.0
8
+ Requires-Dist: scipy
9
+ Requires-Dist: spatialmath-python
10
+ Requires-Dist: airo-typing==2025.4.0
11
+ Dynamic: author
12
+ Dynamic: author-email
13
+ Dynamic: requires-dist
14
+ Dynamic: summary
@@ -0,0 +1,13 @@
1
+ README.md
2
+ setup.py
3
+ airo_spatial_algebra/__init__.py
4
+ airo_spatial_algebra/operations.py
5
+ airo_spatial_algebra/py.typed
6
+ airo_spatial_algebra/se3.py
7
+ airo_spatial_algebra.egg-info/PKG-INFO
8
+ airo_spatial_algebra.egg-info/SOURCES.txt
9
+ airo_spatial_algebra.egg-info/dependency_links.txt
10
+ airo_spatial_algebra.egg-info/requires.txt
11
+ airo_spatial_algebra.egg-info/top_level.txt
12
+ test/test_operations.py
13
+ test/test_se3_container.py
@@ -0,0 +1,4 @@
1
+ numpy<2.0
2
+ scipy
3
+ spatialmath-python
4
+ airo-typing==2025.4.0
@@ -0,0 +1 @@
1
+ airo_spatial_algebra
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,17 @@
1
+ import pathlib
2
+
3
+ import setuptools
4
+
5
+ root_folder = pathlib.Path(__file__).parents[1]
6
+ setuptools.setup(
7
+ name="airo_spatial_algebra",
8
+ version="2025.4.0",
9
+ description="Code for working with SE3 poses, transforms... for robotic manipulation at the Ghent University AI and Robotics Lab",
10
+ author="Thomas Lips",
11
+ author_email="thomas.lips@ugent.be",
12
+ install_requires=["numpy<2.0", "scipy", "spatialmath-python", "airo-typing==2025.4.0"],
13
+ packages=setuptools.find_packages(exclude=["test"]),
14
+ # include py.typed to declare type information is available, see
15
+ # https://mypy.readthedocs.io/en/stable/installed_packages.html#making-pep-561-compatible-packages
16
+ package_data={"airo_spatial_algebra": ["py.typed"]},
17
+ )
@@ -0,0 +1,39 @@
1
+ import numpy as np
2
+ import pytest
3
+ from airo_spatial_algebra.operations import _HomogeneousPoints, transform_points
4
+ from airo_spatial_algebra.se3 import SE3Container
5
+
6
+
7
+ def test_helper_class_creation():
8
+ point = np.array([1.0, 2, 3])
9
+ hpoints = _HomogeneousPoints(point)
10
+ assert hpoints._homogeneous_points.shape == (1, 4)
11
+ assert hpoints._homogeneous_points[0, -1] == 1.0
12
+
13
+ homogeneous_point = np.array([1, 2, 3, 1])
14
+ with pytest.raises(ValueError):
15
+ _HomogeneousPoints(homogeneous_point)
16
+
17
+ points = np.arange(6).reshape(2, 3)
18
+ hpoints = _HomogeneousPoints(points)
19
+ assert hpoints._homogeneous_points.shape == (2, 4)
20
+ # check that the scale is 1.0
21
+ assert hpoints._homogeneous_points[0, -1] == 1.0
22
+
23
+ wronglyshaped_points = np.arange(6).reshape(3, 2)
24
+ with pytest.raises(ValueError):
25
+ _HomogeneousPoints(wronglyshaped_points)
26
+
27
+
28
+ @pytest.mark.parametrize("points", [np.arange(6).astype(np.float32).reshape(2, 3), np.array([1.0, 2, 3])])
29
+ def test_helper_class_properties(points):
30
+ hpoints = _HomogeneousPoints(points)
31
+ assert np.isclose(points, hpoints.points).all()
32
+ assert hpoints.homogeneous_points.shape == (points.size // 3, 4)
33
+
34
+
35
+ def test_transform_points():
36
+ points = np.arange(6).astype(np.float32).reshape(2, 3)
37
+ transform = SE3Container.random()
38
+ transformed_points = transform_points(transform.homogeneous_matrix, points)
39
+ assert np.isclose(transformed_points[0], transform.rotation_matrix @ points[0] + transform.translation).all()
@@ -0,0 +1,132 @@
1
+ import numpy as np
2
+ import pytest
3
+ from airo_spatial_algebra import SE3Container
4
+ from airo_spatial_algebra.se3 import _assert_is_so3_matrix, normalize_so3_matrix
5
+ from scipy.spatial.transform import Rotation
6
+ from spatialmath import SE3, SO3
7
+
8
+
9
+ @pytest.fixture(autouse=True)
10
+ def seed():
11
+ """fixture that will run before each test in this module to make them deterministic.
12
+ Random poses etc are generated with spatialmath,
13
+ which usesnp.random"""
14
+ np.random.seed(2022)
15
+
16
+
17
+ def test_from_translation_and_homogeneous():
18
+ pose = np.eye(4)
19
+ translation = np.array([1, 2, 3.0])
20
+ pose[:3, 3] = translation
21
+ se3 = SE3Container.from_translation(translation)
22
+ assert np.isclose(se3.homogeneous_matrix, pose).all()
23
+
24
+
25
+ def test_from_hom_matrix():
26
+ pose = SE3.Rand().A
27
+ se3 = SE3Container.from_homogeneous_matrix(pose)
28
+ assert np.isclose(se3.homogeneous_matrix, pose).all()
29
+
30
+
31
+ def test_from_bad_hom_matrix():
32
+ pose = SE3.Rand().A
33
+ pose[3, 3] += 0.01
34
+ with pytest.raises(ValueError):
35
+ SE3Container.from_homogeneous_matrix(pose)
36
+
37
+
38
+ def test_from_rot_mat():
39
+ # no translation
40
+ rot_matrix = SO3.Rand().R
41
+ pose = np.eye(4)
42
+ pose[:3, :3] = rot_matrix
43
+ se3 = SE3Container.from_rotation_matrix_and_translation(rot_matrix)
44
+ assert np.isclose(se3.homogeneous_matrix, pose).all()
45
+
46
+ # with translation
47
+ trans = np.array([1, 2, 3.2])
48
+ pose[:3, 3] = trans
49
+ se3 = SE3Container.from_rotation_matrix_and_translation(rot_matrix, trans)
50
+ assert np.isclose(se3.homogeneous_matrix, pose).all()
51
+
52
+
53
+ def test_from_bad_rot_mat():
54
+ rot_matrix = SO3.Rand().R
55
+ rot_matrix[0, 0] += 0.01
56
+ with pytest.raises(ValueError):
57
+ SE3Container.from_rotation_matrix_and_translation(rot_matrix)
58
+
59
+
60
+ def test_from_base_vectors():
61
+ rot_matrix = np.eye(3)
62
+ se3 = SE3Container.from_orthogonal_base_vectors_and_translation(
63
+ rot_matrix[:, 0], rot_matrix[:, 1], rot_matrix[:, 2], np.array([1, 2, 3])
64
+ )
65
+ assert np.isclose(se3.rotation_matrix, rot_matrix).all()
66
+
67
+
68
+ def test_from_rotation_vector():
69
+ angle, axis = SO3.Rand().angvec()
70
+ rot_vec = angle * axis
71
+ se3 = SE3Container.from_rotation_vector_and_translation(rot_vec)
72
+ assert np.isclose(se3.orientation_as_rotation_vector, rot_vec).all()
73
+
74
+
75
+ def test_quaternions():
76
+ quat = [0, 0, 0, 1.0] # no rotation scalar-last
77
+ se3 = SE3Container.from_quaternion_and_translation(quat)
78
+ assert np.isclose(se3.rotation_matrix, np.eye(3)).all()
79
+ assert np.isclose(se3.translation, np.zeros(3)).all()
80
+ assert np.isclose(quat, se3.orientation_as_quaternion).all()
81
+
82
+
83
+ def test_euler():
84
+ euler = [np.pi, np.pi / 4, np.pi / 5] # 90 degs of X
85
+ rot_matrix = Rotation.from_euler("xyz", euler).as_matrix()
86
+ se3 = SE3Container.from_euler_angles_and_translation(euler)
87
+ assert np.isclose(se3.rotation_matrix, rot_matrix).all()
88
+ assert np.isclose(se3.orientation_as_euler_angles, euler).all()
89
+
90
+
91
+ def test_get_axes_functions():
92
+ se3 = SE3Container.random()
93
+ hom_matrix = se3.homogeneous_matrix
94
+ for i, atrr in enumerate([se3.x_axis, se3.y_axis, se3.z_axis]):
95
+ assert np.isclose(atrr, hom_matrix[:3, i]).all()
96
+
97
+
98
+ def test_repr():
99
+ se3 = SE3Container.random()
100
+ print(se3)
101
+
102
+
103
+ def test_all_orientation_reprs_are_equivalent():
104
+ se3 = SE3Container.random()
105
+ rot_matrix = se3.rotation_matrix
106
+ assert np.isclose(Rotation.from_rotvec(se3.orientation_as_rotation_vector).as_matrix(), rot_matrix).all()
107
+ assert np.isclose(Rotation.from_euler("xyz", se3.orientation_as_euler_angles).as_matrix(), rot_matrix).all()
108
+ assert np.isclose(Rotation.from_quat(se3.orientation_as_quaternion).as_matrix(), rot_matrix).all()
109
+
110
+
111
+ def test_quaternion_scalar_conversion():
112
+ se3 = SE3Container.random()
113
+ quat = se3.orientation_as_quaternion
114
+ assert np.isclose(
115
+ quat,
116
+ SE3Container.scalar_first_quaternion_to_scalar_last(SE3Container.scalar_last_quaternion_to_scalar_first(quat)),
117
+ ).all()
118
+
119
+
120
+ def test_axis_angle_dtypes():
121
+ se3 = SE3Container.from_homogeneous_matrix(np.identity(4))
122
+ axis, angle = se3.orientation_as_axis_angle
123
+ assert isinstance(angle, float)
124
+ assert isinstance(axis, np.ndarray)
125
+ assert axis.dtype == np.float64
126
+
127
+
128
+ def test_normalize_so3():
129
+ so3 = SO3.Rand().R
130
+ so3[0, 0] += 0.01
131
+ normalized_so3 = normalize_so3_matrix(so3)
132
+ _assert_is_so3_matrix(normalized_so3)