airo-spatial-algebra 2025.4.0__py3-none-any.whl

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,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
File without changes
@@ -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,8 @@
1
+ airo_spatial_algebra/__init__.py,sha256=rjd3IQ1sd2Fp5qljG2jkEnZnQx8D8htxo4yCfwwU3s0,159
2
+ airo_spatial_algebra/operations.py,sha256=dZB9ZuKdwh02QugKfgtfbpiBFArDsY5IOKBq_Y1V5a8,3456
3
+ airo_spatial_algebra/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ airo_spatial_algebra/se3.py,sha256=yVATPQxbflx0a0QbV5BktkRHEYIiv3qB5v5wqLdaHQI,8659
5
+ airo_spatial_algebra-2025.4.0.dist-info/METADATA,sha256=b6cM3Bhjvuwknu9b4qtSm530yhnxeIjjq3fTLG8VDWc,442
6
+ airo_spatial_algebra-2025.4.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
7
+ airo_spatial_algebra-2025.4.0.dist-info/top_level.txt,sha256=2KTlO2C3ztpiiyiM0x5WBKivjM7r6HMg96Msy0P7nTo,21
8
+ airo_spatial_algebra-2025.4.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (78.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ airo_spatial_algebra