robotics-geometry 0.1.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.
- robotics_geometry/.mypy_cache/.gitignore +2 -0
- robotics_geometry/.mypy_cache/3.11/cache.0.db +0 -0
- robotics_geometry/.mypy_cache/3.11/cache.1.db +0 -0
- robotics_geometry/.mypy_cache/3.11/cache.10.db +0 -0
- robotics_geometry/.mypy_cache/3.11/cache.11.db +0 -0
- robotics_geometry/.mypy_cache/3.11/cache.12.db +0 -0
- robotics_geometry/.mypy_cache/3.11/cache.13.db +0 -0
- robotics_geometry/.mypy_cache/3.11/cache.14.db +0 -0
- robotics_geometry/.mypy_cache/3.11/cache.15.db +0 -0
- robotics_geometry/.mypy_cache/3.11/cache.2.db +0 -0
- robotics_geometry/.mypy_cache/3.11/cache.3.db +0 -0
- robotics_geometry/.mypy_cache/3.11/cache.4.db +0 -0
- robotics_geometry/.mypy_cache/3.11/cache.5.db +0 -0
- robotics_geometry/.mypy_cache/3.11/cache.6.db +0 -0
- robotics_geometry/.mypy_cache/3.11/cache.7.db +0 -0
- robotics_geometry/.mypy_cache/3.11/cache.8.db +0 -0
- robotics_geometry/.mypy_cache/3.11/cache.9.db +0 -0
- robotics_geometry/.mypy_cache/CACHEDIR.TAG +3 -0
- robotics_geometry/__init__.py +1 -0
- robotics_geometry/functions.py +44 -0
- robotics_geometry/py.typed +0 -0
- robotics_geometry/rigid_body2.py +328 -0
- robotics_geometry/ros/__init__.py +20 -0
- robotics_geometry/ros/msgs_mockup.py +79 -0
- robotics_geometry/workspace2.py +295 -0
- robotics_geometry-0.1.0.dist-info/METADATA +12 -0
- robotics_geometry-0.1.0.dist-info/RECORD +28 -0
- robotics_geometry-0.1.0.dist-info/WHEEL +4 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Geometry library for mobile robotics applications."""
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Common utility functions."""
|
|
2
|
+
|
|
3
|
+
# standard imports
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
from .ros import Quaternion
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def normalize_angle(angle: float) -> float:
|
|
10
|
+
"""Normalize an angle to the range (-pi, pi]."""
|
|
11
|
+
return math.pi - ((math.pi - angle) % (2 * math.pi))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def yaw_from_quaternion(quaternion: Quaternion) -> float:
|
|
15
|
+
"""Compute the yaw angle from a quaternion.
|
|
16
|
+
|
|
17
|
+
Warning: other rotation components are ignored.
|
|
18
|
+
|
|
19
|
+
Arguments:
|
|
20
|
+
quaternion: a `geometry_msgs.msg.Quaternion`.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
The yaw angle in radians in the range (-pi, pi].
|
|
24
|
+
"""
|
|
25
|
+
x = quaternion.x
|
|
26
|
+
y = quaternion.y
|
|
27
|
+
z = quaternion.z
|
|
28
|
+
w = quaternion.w
|
|
29
|
+
return normalize_angle(math.atan2(2 * (w * z + x * y),
|
|
30
|
+
w * w + x * x - y * y - z * z))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def quaternion_from_yaw(yaw: float) -> Quaternion:
|
|
34
|
+
"""Compute the quaternion from a yaw angle.
|
|
35
|
+
|
|
36
|
+
Arguments:
|
|
37
|
+
yaw: The yaw angle in radians.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
A `geometry_msgs.msg.Quaternion`.
|
|
41
|
+
"""
|
|
42
|
+
w = math.cos(yaw / 2)
|
|
43
|
+
z = math.sin(yaw / 2)
|
|
44
|
+
return Quaternion(x=0., y=0., z=z, w=w)
|
|
File without changes
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""Definition of pose and transformation in a 2D workspace."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
# standard imports
|
|
6
|
+
import dataclasses
|
|
7
|
+
import math
|
|
8
|
+
from types import NotImplementedType
|
|
9
|
+
from typing import Iterator, Self, overload
|
|
10
|
+
|
|
11
|
+
# local imports
|
|
12
|
+
from .functions import normalize_angle, quaternion_from_yaw, yaw_from_quaternion
|
|
13
|
+
from .ros import Point, Pose, Transform, Vector3
|
|
14
|
+
from .workspace2 import Point2, Vector2
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
18
|
+
class Transform2:
|
|
19
|
+
"""2D transformation.
|
|
20
|
+
|
|
21
|
+
Rotation is done before translation.
|
|
22
|
+
"""
|
|
23
|
+
tx: float = 0.0
|
|
24
|
+
"""`x` coordinate of the translation."""
|
|
25
|
+
ty: float = 0.0
|
|
26
|
+
"""`y` coordinate of the translation."""
|
|
27
|
+
angle: float = 0.0
|
|
28
|
+
"""Rotation angle in radians."""
|
|
29
|
+
translation: Vector2 = dataclasses.field(
|
|
30
|
+
init=False, repr=False, compare=False)
|
|
31
|
+
"""Translation vector (``Vector2(tx, ty)``)."""
|
|
32
|
+
|
|
33
|
+
def __post_init__(self) -> None:
|
|
34
|
+
"""Create translation."""
|
|
35
|
+
# `self.translation = ...` would not work since the class is frozen
|
|
36
|
+
# `super().__setattr__(...)` does not work due to slots (https://github.com/python-attrs/attrs/issues/1038#issuecomment-1269542786)
|
|
37
|
+
super(Transform2, self).__setattr__(
|
|
38
|
+
'translation', Vector2(self.tx, self.ty))
|
|
39
|
+
|
|
40
|
+
def __iter__(self) -> Iterator[float]:
|
|
41
|
+
"""Iterate over `tx`, `ty`, and `angle`.
|
|
42
|
+
|
|
43
|
+
Allows unpacking.
|
|
44
|
+
"""
|
|
45
|
+
yield self.tx
|
|
46
|
+
yield self.ty
|
|
47
|
+
yield self.angle
|
|
48
|
+
|
|
49
|
+
def keys(self) -> Iterator[str]:
|
|
50
|
+
"""Return the coordinate keys.
|
|
51
|
+
|
|
52
|
+
In conjunction to :meth:`__getitem__`, allows dictionary unpacking.
|
|
53
|
+
"""
|
|
54
|
+
yield 'tx'
|
|
55
|
+
yield 'ty'
|
|
56
|
+
yield 'angle'
|
|
57
|
+
|
|
58
|
+
def __getitem__(self, key: str) -> float:
|
|
59
|
+
"""Return the coordinate value for the given key.
|
|
60
|
+
|
|
61
|
+
In conjunction with :meth:`keys`, allows dictionary unpacking.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
KeyError: if the key is not a valid coordinate name.
|
|
65
|
+
"""
|
|
66
|
+
return getattr(self, key)
|
|
67
|
+
|
|
68
|
+
@overload
|
|
69
|
+
def __matmul__(self, other: Transform2) -> Transform2: ...
|
|
70
|
+
@overload
|
|
71
|
+
def __matmul__(self, other: object) -> Transform2 | NotImplementedType: ...
|
|
72
|
+
def __matmul__(
|
|
73
|
+
self, other: Transform2 | object
|
|
74
|
+
) -> Transform2 | NotImplementedType:
|
|
75
|
+
"""Compose two transformations."""
|
|
76
|
+
match other:
|
|
77
|
+
case Transform2():
|
|
78
|
+
return Transform2(
|
|
79
|
+
*(self.translation + other.translation.rotate(self.angle)),
|
|
80
|
+
angle=normalize_angle(self.angle + other.angle))
|
|
81
|
+
case _:
|
|
82
|
+
return NotImplemented
|
|
83
|
+
|
|
84
|
+
@overload
|
|
85
|
+
def __mul__(self, other: Vector2) -> Vector2: ...
|
|
86
|
+
@overload
|
|
87
|
+
def __mul__(self, other: Point2) -> Point2: ...
|
|
88
|
+
@overload
|
|
89
|
+
def __mul__(self, other: Pose2) -> Pose2: ...
|
|
90
|
+
def __mul__(
|
|
91
|
+
self, other: Vector2 | Point2 | Pose2 | object
|
|
92
|
+
) -> Vector2 | Point2 | Pose2 | NotImplementedType:
|
|
93
|
+
"""Apply transformation.
|
|
94
|
+
|
|
95
|
+
Transform a vector or point by this transformation.
|
|
96
|
+
|
|
97
|
+
Arguments:
|
|
98
|
+
other: the object to transform.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The transformed vector or point, or ``NotImplemented`` if the type
|
|
102
|
+
is not supported.
|
|
103
|
+
"""
|
|
104
|
+
match other:
|
|
105
|
+
case Vector2():
|
|
106
|
+
return self.translation + other.rotate(self.angle)
|
|
107
|
+
case Point2():
|
|
108
|
+
return Point2(
|
|
109
|
+
**(self.translation + Vector2(*other).rotate(self.angle))
|
|
110
|
+
)
|
|
111
|
+
case Pose2():
|
|
112
|
+
return Pose2(**(self * other.position),
|
|
113
|
+
theta=normalize_angle(self.angle + other.theta)
|
|
114
|
+
)
|
|
115
|
+
case _:
|
|
116
|
+
return NotImplemented
|
|
117
|
+
|
|
118
|
+
def isclose_to(
|
|
119
|
+
self, other: Self, *, rel_tol: float = 1e-09,
|
|
120
|
+
abs_tol_m: float = 0.0, abs_tol_rad: float = 0.0
|
|
121
|
+
) -> bool:
|
|
122
|
+
"""Approximate transformation equality.
|
|
123
|
+
|
|
124
|
+
Warning: there are two absolute tolerances (one for the translation and
|
|
125
|
+
the other for the rotation).
|
|
126
|
+
|
|
127
|
+
Arguments:
|
|
128
|
+
other: the other transformation to compare with.
|
|
129
|
+
rel_tol: the relative tolerance for comparison.
|
|
130
|
+
abs_tol_m: the absolute tolerance for the translation component.
|
|
131
|
+
abs_tol_rad: the absolute tolerance for the rotation component.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
``True`` if the transformations are close, ``False`` otherwise.
|
|
135
|
+
"""
|
|
136
|
+
return (
|
|
137
|
+
math.isclose(self.tx, other.tx, rel_tol=rel_tol, abs_tol=abs_tol_m)
|
|
138
|
+
and math.isclose(
|
|
139
|
+
self.ty, other.ty, rel_tol=rel_tol, abs_tol=abs_tol_m)
|
|
140
|
+
and (abs(normalize_angle(self.angle - other.angle))
|
|
141
|
+
<= max(rel_tol * max(abs(normalize_angle(self.angle)),
|
|
142
|
+
abs(normalize_angle(other.angle))),
|
|
143
|
+
abs_tol_rad))
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def __invert__(self) -> Self:
|
|
147
|
+
"""Inverse transformation."""
|
|
148
|
+
sa, ca = math.sin(self.angle), math.cos(self.angle)
|
|
149
|
+
return type(self)(
|
|
150
|
+
-self.tx * ca - self.ty * sa,
|
|
151
|
+
self.tx * sa - self.ty * ca,
|
|
152
|
+
normalize_angle(-self.angle)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def from_pose2(cls, pose2: Pose2) -> Self:
|
|
157
|
+
"""Create transform from pose.
|
|
158
|
+
|
|
159
|
+
Warning: assuming that the pose is of rigid body A expressed with
|
|
160
|
+
respect to frame B, the transform will map coordinates expressed in
|
|
161
|
+
frame A into frame B.
|
|
162
|
+
"""
|
|
163
|
+
return cls(*pose2)
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def from_transform_msg(cls, transform: Transform) -> Self:
|
|
167
|
+
"""Create a transform from a ROS 2 ``Transform`` message.
|
|
168
|
+
|
|
169
|
+
The ``z`` coordinate of the translation is ignored.
|
|
170
|
+
"""
|
|
171
|
+
return cls(
|
|
172
|
+
transform.translation.x,
|
|
173
|
+
transform.translation.y,
|
|
174
|
+
yaw_from_quaternion(transform.rotation),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def to_transform_msg(self) -> Transform:
|
|
178
|
+
"""Convert this transform to a ROS 2 ``Transform`` message.
|
|
179
|
+
|
|
180
|
+
The translation ``z`` component is set to ``0.0``.
|
|
181
|
+
"""
|
|
182
|
+
return Transform(
|
|
183
|
+
translation=Vector3(x=self.tx, y=self.ty, z=0.0), # ty:ignore[invalid-argument-type]
|
|
184
|
+
rotation=quaternion_from_yaw(self.angle) # ty:ignore[invalid-argument-type]
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
189
|
+
class Pose2:
|
|
190
|
+
"""2D pose."""
|
|
191
|
+
x: float = 0.
|
|
192
|
+
"""`x` coordinate."""
|
|
193
|
+
y: float = 0.
|
|
194
|
+
"""`y` coordinate."""
|
|
195
|
+
theta: float = 0.
|
|
196
|
+
"""Orientation."""
|
|
197
|
+
position: Point2 = dataclasses.field(
|
|
198
|
+
init=False, repr=False, compare=False)
|
|
199
|
+
"""Position (``Point2(x, y)``)."""
|
|
200
|
+
# ? add orientation field as alias for theta?
|
|
201
|
+
|
|
202
|
+
def __post_init__(self) -> None:
|
|
203
|
+
"""Create position."""
|
|
204
|
+
super(Pose2, self).__setattr__(
|
|
205
|
+
'position', Point2(self.x, self.y)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def __iter__(self) -> Iterator[float]:
|
|
209
|
+
"""Iterate over ``x``, ``y``, and ``theta``.
|
|
210
|
+
|
|
211
|
+
Allows unpacking.
|
|
212
|
+
"""
|
|
213
|
+
yield self.x
|
|
214
|
+
yield self.y
|
|
215
|
+
yield self.theta
|
|
216
|
+
|
|
217
|
+
def keys(self) -> Iterator[str]:
|
|
218
|
+
"""Return the coordinate keys.
|
|
219
|
+
|
|
220
|
+
In conjunction to :meth:`__getitem__`, allows dictionary unpacking.
|
|
221
|
+
"""
|
|
222
|
+
yield 'x'
|
|
223
|
+
yield 'y'
|
|
224
|
+
yield 'theta'
|
|
225
|
+
|
|
226
|
+
def __getitem__(self, key: str) -> float:
|
|
227
|
+
"""Return the coordinate value for the given key.
|
|
228
|
+
|
|
229
|
+
In conjunction with :meth:`keys`, allows dictionary unpacking.
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
KeyError: if the key is not a valid coordinate name.
|
|
233
|
+
"""
|
|
234
|
+
return getattr(self, key)
|
|
235
|
+
|
|
236
|
+
def isclose_to(
|
|
237
|
+
self, other: Self, *, rel_tol: float = 1e-09,
|
|
238
|
+
abs_tol_m: float = 0.0, abs_tol_rad: float = 0.0
|
|
239
|
+
) -> bool:
|
|
240
|
+
"""Approximate pose equality.
|
|
241
|
+
|
|
242
|
+
Warning: there are two absolute tolerances (one for the position and
|
|
243
|
+
the other for the orientation).
|
|
244
|
+
|
|
245
|
+
Arguments:
|
|
246
|
+
other: the other pose to compare with.
|
|
247
|
+
rel_tol: the relative tolerance for comparison.
|
|
248
|
+
abs_tol_m: the absolute tolerance for the position component.
|
|
249
|
+
abs_tol_rad: the absolute tolerance for the orientation component.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
``True`` if the poses are close, ``False`` otherwise.
|
|
253
|
+
"""
|
|
254
|
+
return (
|
|
255
|
+
math.isclose(self.x, other.x, rel_tol=rel_tol, abs_tol=abs_tol_m)
|
|
256
|
+
and math.isclose(
|
|
257
|
+
self.y, other.y, rel_tol=rel_tol, abs_tol=abs_tol_m)
|
|
258
|
+
and (abs(normalize_angle(self.theta - other.theta))
|
|
259
|
+
<= max(rel_tol * max(abs(normalize_angle(self.theta)),
|
|
260
|
+
abs(normalize_angle(other.theta))),
|
|
261
|
+
abs_tol_rad))
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def distance_to(self, other: Self, weight: float = 1.0) -> float:
|
|
265
|
+
"""Compute the distance between two poses.
|
|
266
|
+
|
|
267
|
+
Arguments:
|
|
268
|
+
other: the other pose.
|
|
269
|
+
weight: the weight to apply to the orientation component.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
the weighted distance between the two poses.
|
|
273
|
+
"""
|
|
274
|
+
return math.sqrt(
|
|
275
|
+
(self.x - other.x)**2
|
|
276
|
+
+ (self.y - other.y)**2
|
|
277
|
+
+ weight**2 * normalize_angle(self.theta - other.theta)**2
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
@classmethod
|
|
281
|
+
def from_transform2(cls, transform2: Transform2) -> Self:
|
|
282
|
+
"""Create pose from transform.
|
|
283
|
+
|
|
284
|
+
Warning: assuming that the transform object maps coordinates from
|
|
285
|
+
frame A to frame B, the pose corresponds to the rigid body A expressed
|
|
286
|
+
with respect to frame B.
|
|
287
|
+
"""
|
|
288
|
+
return cls(*transform2)
|
|
289
|
+
|
|
290
|
+
@classmethod
|
|
291
|
+
def from_pose_msg(cls, pose: Pose) -> Self:
|
|
292
|
+
"""Create pose from a ROS 2 ``Pose`` message.
|
|
293
|
+
|
|
294
|
+
The 3D pose is projected down to 2D.
|
|
295
|
+
"""
|
|
296
|
+
return cls(
|
|
297
|
+
pose.position.x,
|
|
298
|
+
pose.position.y,
|
|
299
|
+
yaw_from_quaternion(pose.orientation)
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
def to_pose_msg(self) -> Pose:
|
|
303
|
+
"""Convert this pose to a ROS 2 ``Pose`` message."""
|
|
304
|
+
return Pose(
|
|
305
|
+
position=Point(x=self.x, y=self.y), # ty:ignore[invalid-argument-type]
|
|
306
|
+
orientation=quaternion_from_yaw(self.theta), # ty:ignore[invalid-argument-type]
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def move_by(self, other: Transform2 | Pose2) -> Pose2:
|
|
310
|
+
"""Move pose by another transform or pose.
|
|
311
|
+
|
|
312
|
+
It corresponds to adding a small displacement (``other``) on top of the
|
|
313
|
+
current pose.
|
|
314
|
+
"""
|
|
315
|
+
self_transform = Transform2.from_pose2(self)
|
|
316
|
+
match other:
|
|
317
|
+
case Transform2():
|
|
318
|
+
return type(self).from_transform2(self_transform @ other)
|
|
319
|
+
case Pose2():
|
|
320
|
+
return self_transform * other
|
|
321
|
+
|
|
322
|
+
def pose_relative_to(self, reference: Pose2) -> Pose2:
|
|
323
|
+
"""Compute current pose relative to the reference.
|
|
324
|
+
|
|
325
|
+
It corresponds to the current pose expressed with respect to the frame
|
|
326
|
+
attached to the reference pose.
|
|
327
|
+
"""
|
|
328
|
+
return ~Transform2.from_pose2(reference) * self
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""ROS-related helpers and message definitions."""
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from geometry_msgs.msg import (
|
|
5
|
+
Point,
|
|
6
|
+
Pose,
|
|
7
|
+
Quaternion,
|
|
8
|
+
Transform,
|
|
9
|
+
Vector3,
|
|
10
|
+
)
|
|
11
|
+
except ImportError:
|
|
12
|
+
from .msgs_mockup import (
|
|
13
|
+
Point,
|
|
14
|
+
Pose,
|
|
15
|
+
Quaternion,
|
|
16
|
+
Transform,
|
|
17
|
+
Vector3,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = ["Point", "Pose", "Quaternion", "Transform", "Vector3"]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Mockups for ROS message types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
# standard import
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(slots=True)
|
|
10
|
+
class Point:
|
|
11
|
+
"""Mockup for the `geometry_msgs.msg.Point` ROS message."""
|
|
12
|
+
|
|
13
|
+
x: float = 0.0
|
|
14
|
+
y: float = 0.0
|
|
15
|
+
z: float = 0.0
|
|
16
|
+
|
|
17
|
+
def __repr__(self) -> str:
|
|
18
|
+
"""Pretend to be a ``geometry_msgs.msg.Point``."""
|
|
19
|
+
return (f"geometry_msgs.msg.Point(x={self.x}, y={self.y}, "
|
|
20
|
+
f"z={self.z})")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(slots=True)
|
|
24
|
+
class Quaternion:
|
|
25
|
+
"""Mockup for the `geometry_msgs.msg.Quaternion` ROS message."""
|
|
26
|
+
|
|
27
|
+
x: float = 0.0
|
|
28
|
+
y: float = 0.0
|
|
29
|
+
z: float = 0.0
|
|
30
|
+
w: float = 1.0
|
|
31
|
+
|
|
32
|
+
def __repr__(self) -> str:
|
|
33
|
+
"""Pretend to be a ``geometry_msgs.msg.Quaternion``."""
|
|
34
|
+
return (f"geometry_msgs.msg.Quaternion(x={self.x}, y={self.y}, "
|
|
35
|
+
f"z={self.z}, w={self.w})")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(slots=True)
|
|
39
|
+
class Pose:
|
|
40
|
+
"""Mockup for the `geometry_msgs.msg.Pose` ROS message."""
|
|
41
|
+
|
|
42
|
+
position: Point = field(default_factory=Point)
|
|
43
|
+
orientation: Quaternion = field(default_factory=Quaternion)
|
|
44
|
+
|
|
45
|
+
def __repr__(self) -> str:
|
|
46
|
+
"""Pretend to be a ``geometry_msgs.msg.Pose``."""
|
|
47
|
+
return (
|
|
48
|
+
"geometry_msgs.msg.Pose(position="
|
|
49
|
+
f"{self.position}, orientation={self.orientation})"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(slots=True)
|
|
54
|
+
class Vector3:
|
|
55
|
+
"""Mockup for the `geometry_msgs.msg.Vector3` ROS message."""
|
|
56
|
+
|
|
57
|
+
x: float = 0.0
|
|
58
|
+
y: float = 0.0
|
|
59
|
+
z: float = 0.0
|
|
60
|
+
|
|
61
|
+
def __repr__(self) -> str:
|
|
62
|
+
"""Pretend to be a ``geometry_msgs.msg.Vector3``."""
|
|
63
|
+
return (f"geometry_msgs.msg.Vector3(x={self.x}, y={self.y}, "
|
|
64
|
+
f"z={self.z})")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(slots=True)
|
|
68
|
+
class Transform:
|
|
69
|
+
"""Mockup for the `geometry_msgs.msg.Transform` ROS message."""
|
|
70
|
+
|
|
71
|
+
translation: Vector3 = field(default_factory=Vector3)
|
|
72
|
+
rotation: Quaternion = field(default_factory=Quaternion)
|
|
73
|
+
|
|
74
|
+
def __repr__(self) -> str:
|
|
75
|
+
"""Pretend to be a ``geometry_msgs.msg.Transform``."""
|
|
76
|
+
return (
|
|
77
|
+
"geometry_msgs.msg.Transform(translation="
|
|
78
|
+
f"{self.translation}, rotation={self.rotation})"
|
|
79
|
+
)
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Definition of vectors and points of a 2D workspace."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
# standard imports
|
|
6
|
+
import dataclasses
|
|
7
|
+
import math
|
|
8
|
+
from types import NotImplementedType
|
|
9
|
+
from typing import Iterator, Self, overload
|
|
10
|
+
|
|
11
|
+
# local imports
|
|
12
|
+
from .ros import Point, Vector3
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
16
|
+
class Coord2:
|
|
17
|
+
"""2D coordinates.
|
|
18
|
+
|
|
19
|
+
This base class holds the coordinates and provides utility methods that
|
|
20
|
+
are common to both points and vectors such as distance computation and
|
|
21
|
+
approximate equality.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
x: float = 0.0
|
|
25
|
+
"""`x` coordinate."""
|
|
26
|
+
y: float = 0.0
|
|
27
|
+
"""`y` coordinate."""
|
|
28
|
+
|
|
29
|
+
def __iter__(self) -> Iterator[float]:
|
|
30
|
+
"""Iterate over the coordinates.
|
|
31
|
+
|
|
32
|
+
Allows unpacking.
|
|
33
|
+
"""
|
|
34
|
+
yield self.x
|
|
35
|
+
yield self.y
|
|
36
|
+
|
|
37
|
+
def keys(self) -> Iterator[str]:
|
|
38
|
+
"""Return the coordinate keys.
|
|
39
|
+
|
|
40
|
+
In conjunction to :meth:`__getitem__`, allows dictionary unpacking.
|
|
41
|
+
"""
|
|
42
|
+
yield from self.__slots__
|
|
43
|
+
|
|
44
|
+
def __getitem__(self, key: str) -> float:
|
|
45
|
+
"""Return the coordinate value for the given key.
|
|
46
|
+
|
|
47
|
+
In conjunction with :meth:`keys`, allows dictionary unpacking.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
KeyError: if the key is not a valid coordinate name.
|
|
51
|
+
"""
|
|
52
|
+
return getattr(self, key)
|
|
53
|
+
|
|
54
|
+
def dist(self, other: Self, *, order: int | float = 2) -> float:
|
|
55
|
+
"""Minkowski distance between two coordinates.
|
|
56
|
+
|
|
57
|
+
This method computes the `Minkowski distance <https://en.wikipedia.org/wiki/Minkowski_distance>`__
|
|
58
|
+
between two coordinates.
|
|
59
|
+
|
|
60
|
+
Arguments:
|
|
61
|
+
other: the other element.
|
|
62
|
+
order: a positive number, ``math.inf`` or ``-math.inf``
|
|
63
|
+
(default: 2).
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
The Minkowski distance between the two coordinates.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ValueError: if ``order`` is not a supported value.
|
|
70
|
+
"""
|
|
71
|
+
dx = self.x - other.x
|
|
72
|
+
dy = self.y - other.y
|
|
73
|
+
match order:
|
|
74
|
+
case 2:
|
|
75
|
+
return math.hypot(dx, dy)
|
|
76
|
+
case math.inf:
|
|
77
|
+
return max(abs(dx), abs(dy))
|
|
78
|
+
case float(f) if f == -math.inf:
|
|
79
|
+
return min(abs(dx), abs(dy))
|
|
80
|
+
case float(f) | int(f) if f > 0.0:
|
|
81
|
+
return (abs(dx) ** order + abs(dy) ** order) ** (1.0 / order)
|
|
82
|
+
case _:
|
|
83
|
+
raise ValueError(f"unsupported {order=}")
|
|
84
|
+
|
|
85
|
+
def isclose_to(
|
|
86
|
+
self, other: Self, *, rel_tol: float = 1e-09, abs_tol: float = 0.0
|
|
87
|
+
) -> bool:
|
|
88
|
+
"""Approximate equality of two elements.
|
|
89
|
+
|
|
90
|
+
It uses :func:`math.isclose` to compare each coordinate; the elements
|
|
91
|
+
are considered equal if all coordinates are close.
|
|
92
|
+
|
|
93
|
+
Arguments:
|
|
94
|
+
other: the other element to compare with.
|
|
95
|
+
rel_tol: the relative tolerance for comparison.
|
|
96
|
+
abs_tol: the absolute tolerance for comparison.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
``True`` if the elements are close, ``False`` otherwise.
|
|
100
|
+
"""
|
|
101
|
+
return all(
|
|
102
|
+
math.isclose(a, b, rel_tol=rel_tol, abs_tol=abs_tol)
|
|
103
|
+
for a, b in zip(self, other)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Vector2(Coord2):
|
|
108
|
+
"""2D vector with standard operations and utility methods."""
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def from_vector3(cls, vector3: Vector3) -> Self:
|
|
112
|
+
"""Create a 2D vector from a ROS 2 ``Vector3`` message.
|
|
113
|
+
|
|
114
|
+
The ``z`` component is ignored.
|
|
115
|
+
"""
|
|
116
|
+
return cls(vector3.x, vector3.y)
|
|
117
|
+
|
|
118
|
+
def to_vector3(self) -> Vector3:
|
|
119
|
+
"""Convert this 2D vector to a ROS 2 ``Vector3`` message.
|
|
120
|
+
|
|
121
|
+
The ``z`` component is set to ``0.0``.
|
|
122
|
+
"""
|
|
123
|
+
return Vector3(**self, z=0.)
|
|
124
|
+
|
|
125
|
+
def __neg__(self) -> Self:
|
|
126
|
+
"""Negate the vector."""
|
|
127
|
+
return type(self)(-self.x, -self.y)
|
|
128
|
+
|
|
129
|
+
@overload
|
|
130
|
+
def __add__(self, other: Vector2 | Iterator[float]) -> Self: ...
|
|
131
|
+
@overload
|
|
132
|
+
def __add__(self, other: int | float) -> Self: ...
|
|
133
|
+
@overload
|
|
134
|
+
def __add__(self, other: Point2) -> Point2: ...
|
|
135
|
+
@overload
|
|
136
|
+
def __add__(self, other: object) -> Self | Point2 | NotImplementedType: ...
|
|
137
|
+
def __add__(self, other: object) -> Self | Point2 | NotImplementedType:
|
|
138
|
+
"""Add two vectors or point translation."""
|
|
139
|
+
match other:
|
|
140
|
+
case Vector2(x=ox, y=oy) | (float(ox), float(oy)):
|
|
141
|
+
return type(self)(self.x + ox, self.y + oy)
|
|
142
|
+
case Point2(x=ox, y=oy):
|
|
143
|
+
return type(other)(self.x + ox, self.y + oy)
|
|
144
|
+
case int() | float():
|
|
145
|
+
return type(self)(self.x + other, self.y + other)
|
|
146
|
+
case _:
|
|
147
|
+
return NotImplemented
|
|
148
|
+
|
|
149
|
+
__radd__ = __add__
|
|
150
|
+
|
|
151
|
+
@overload
|
|
152
|
+
def __sub__(self, other: Vector2 | Iterator[float]) -> Self: ...
|
|
153
|
+
@overload
|
|
154
|
+
def __sub__(self, other: int | float) -> Self: ...
|
|
155
|
+
@overload
|
|
156
|
+
def __sub__(self, other: object) -> Self | NotImplementedType: ...
|
|
157
|
+
def __sub__(self, other: object) -> Self | NotImplementedType:
|
|
158
|
+
"""Subtract two vectors or shift."""
|
|
159
|
+
match other:
|
|
160
|
+
case Vector2(x=ox, y=oy) | (float(ox), float(oy)):
|
|
161
|
+
return type(self)(self.x - ox, self.y - oy)
|
|
162
|
+
case int() | float():
|
|
163
|
+
return type(self)(self.x - other, self.y - other)
|
|
164
|
+
case _:
|
|
165
|
+
return NotImplemented
|
|
166
|
+
|
|
167
|
+
@overload
|
|
168
|
+
def __mul__(self, other: int | float) -> Self: ...
|
|
169
|
+
@overload
|
|
170
|
+
def __mul__(self, other: object) -> Self | NotImplementedType: ...
|
|
171
|
+
def __mul__(self, other: object) -> Self | NotImplementedType:
|
|
172
|
+
"""Multiply by a scalar."""
|
|
173
|
+
match other:
|
|
174
|
+
case int() | float():
|
|
175
|
+
return type(self)(self.x * other, self.y * other)
|
|
176
|
+
case _:
|
|
177
|
+
return NotImplemented
|
|
178
|
+
|
|
179
|
+
__rmul__ = __mul__
|
|
180
|
+
|
|
181
|
+
@overload
|
|
182
|
+
def __truediv__(self, other: int | float) -> Self: ...
|
|
183
|
+
@overload
|
|
184
|
+
def __truediv__(self, other: object) -> Self | NotImplementedType: ...
|
|
185
|
+
def __truediv__(self, other: object) -> Self | NotImplementedType:
|
|
186
|
+
"""Divide by a scalar."""
|
|
187
|
+
match other:
|
|
188
|
+
case int() | float():
|
|
189
|
+
return type(self)(self.x / other, self.y / other)
|
|
190
|
+
case _:
|
|
191
|
+
return NotImplemented
|
|
192
|
+
|
|
193
|
+
def dot(self, other: Vector2) -> float:
|
|
194
|
+
"""Compute dot product with another vector."""
|
|
195
|
+
return self.x * other.x + self.y * other.y
|
|
196
|
+
|
|
197
|
+
def cross(self, other: Vector2) -> float:
|
|
198
|
+
"""Compute magnitude of the cross product with another vector."""
|
|
199
|
+
return self.x * other.y - self.y * other.x
|
|
200
|
+
|
|
201
|
+
def norm(self, *, order: int | float = 2) -> float:
|
|
202
|
+
"""Compute vector norm.
|
|
203
|
+
|
|
204
|
+
It computes the `p-norm <https://en.wikipedia.org/wiki/Norm_(mathematics)#p-norm>`__
|
|
205
|
+
of the vector.
|
|
206
|
+
|
|
207
|
+
Arguments:
|
|
208
|
+
order: a positive number, ``math.inf`` or ``-math.inf`` (default: 2)
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
The norm of the vector.
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
ValueError: if ``order`` is not a supported value.
|
|
215
|
+
"""
|
|
216
|
+
match order:
|
|
217
|
+
case 2:
|
|
218
|
+
return math.hypot(*self)
|
|
219
|
+
case math.inf:
|
|
220
|
+
return max(abs(self.x), abs(self.y))
|
|
221
|
+
case float(f) if f == -math.inf:
|
|
222
|
+
return min(abs(self.x), abs(self.y))
|
|
223
|
+
case float(f) | int(f) if f > 0.0:
|
|
224
|
+
x, y = self
|
|
225
|
+
return (abs(x) ** order + abs(y) ** order) ** (1.0 / order)
|
|
226
|
+
case _:
|
|
227
|
+
raise ValueError(f"unsupported {order=}")
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def orientation(self) -> float:
|
|
231
|
+
"""Orientation of the vector.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
The orientation with respect to the `x` axis in radians.
|
|
235
|
+
"""
|
|
236
|
+
return math.atan2(self.y, self.x)
|
|
237
|
+
|
|
238
|
+
def rotate(self, angle: float) -> Vector2:
|
|
239
|
+
"""Compute the rotated vector.
|
|
240
|
+
|
|
241
|
+
Arguments:
|
|
242
|
+
angle: the rotation angle in radians.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
The rotated vector.
|
|
246
|
+
"""
|
|
247
|
+
sa, ca = math.sin(angle), math.cos(angle)
|
|
248
|
+
x, y = self
|
|
249
|
+
return Vector2(x * ca - y * sa, x * sa + y * ca)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class Point2(Coord2):
|
|
253
|
+
"""2D point with standard operations and utility methods."""
|
|
254
|
+
|
|
255
|
+
@classmethod
|
|
256
|
+
def from_point_msg(cls, point: Point) -> Self:
|
|
257
|
+
"""Create a 2D point from a ROS 2 ``Point`` message.
|
|
258
|
+
|
|
259
|
+
The ``z`` component is ignored.
|
|
260
|
+
"""
|
|
261
|
+
return cls(point.x, point.y)
|
|
262
|
+
|
|
263
|
+
def to_point_msg(self) -> Point:
|
|
264
|
+
"""Convert this 2D point to a ROS 2 ``Point`` message.
|
|
265
|
+
|
|
266
|
+
The ``z`` component is set to ``0.0``.
|
|
267
|
+
"""
|
|
268
|
+
return Point(x=self.x, y=self.y, z=0.)
|
|
269
|
+
|
|
270
|
+
def __add__(self, other: Vector2 | object) -> Point2 | NotImplementedType:
|
|
271
|
+
"""Translate by a vector."""
|
|
272
|
+
match other:
|
|
273
|
+
case Vector2(x=ox, y=oy):
|
|
274
|
+
return Point2(self.x + ox, self.y + oy)
|
|
275
|
+
case _:
|
|
276
|
+
return NotImplemented
|
|
277
|
+
|
|
278
|
+
@overload
|
|
279
|
+
def __sub__(self, other: Point2) -> Vector2: ...
|
|
280
|
+
@overload
|
|
281
|
+
def __sub__(self, other: Vector2) -> Point2: ...
|
|
282
|
+
@overload
|
|
283
|
+
def __sub__(self, other: object) -> Point2 | Vector2 | NotImplementedType:
|
|
284
|
+
...
|
|
285
|
+
def __sub__(
|
|
286
|
+
self, other: Point2 | Vector2 | object
|
|
287
|
+
) -> Point2 | Vector2 | NotImplementedType:
|
|
288
|
+
"""Compute vector between two points."""
|
|
289
|
+
match other:
|
|
290
|
+
case Point2(x=ox, y=oy):
|
|
291
|
+
return Vector2(self.x - ox, self.y - oy)
|
|
292
|
+
case Vector2(x=ox, y=oy):
|
|
293
|
+
return Point2(self.x - ox, self.y - oy)
|
|
294
|
+
case _:
|
|
295
|
+
return NotImplemented
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: robotics-geometry
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Geometry library for robotics applications
|
|
5
|
+
Author: Francis Colas
|
|
6
|
+
Author-email: Francis Colas <francis.colas@inria.fr>
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
Geometry library for mobile robotics applications.
|
|
11
|
+
|
|
12
|
+
See the [documentation](https://colas.gitlabpages.inria.fr/robotics-geometry/index.html) for more information.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
robotics_geometry/.mypy_cache/.gitignore,sha256=amnaZw0RUw038PDP3HvtMLeOpkNOJPenMgi5guKdMiw,34
|
|
2
|
+
robotics_geometry/.mypy_cache/3.11/cache.0.db,sha256=uUMUfCwsj46mQNfiGAKTTlLh4vBCCWfjZRCIJccYU3c,106496
|
|
3
|
+
robotics_geometry/.mypy_cache/3.11/cache.1.db,sha256=WEYmbMUFLeK0CiVDxyftRtvSjpKBKDbtO0WjdtgFm_w,167936
|
|
4
|
+
robotics_geometry/.mypy_cache/3.11/cache.10.db,sha256=IwbyKIwop0668-7ApQk45tittDBMmE5VwQ-6mPzEwuU,155648
|
|
5
|
+
robotics_geometry/.mypy_cache/3.11/cache.11.db,sha256=iWJ-j1Eijhu7SBof5VrXmbf8BiieeC1ThnkNh0wsHUI,81920
|
|
6
|
+
robotics_geometry/.mypy_cache/3.11/cache.12.db,sha256=3RkljoNaelvlN-NhRTYjHJVDn_8tdE4TT-BYH20crB0,40960
|
|
7
|
+
robotics_geometry/.mypy_cache/3.11/cache.13.db,sha256=8QO19GEf_JFyAavIUNgAdLT7OCEKSwmkQPajlbiQQ0I,208896
|
|
8
|
+
robotics_geometry/.mypy_cache/3.11/cache.14.db,sha256=0bjb3Ya5iQvC-ua95Yczi03fxcf8_abFuoPj6kooQuM,65536
|
|
9
|
+
robotics_geometry/.mypy_cache/3.11/cache.15.db,sha256=cEd5rglLeXpCzsXocyypTYbHGOxxnWho-HjfafHiqQc,200704
|
|
10
|
+
robotics_geometry/.mypy_cache/3.11/cache.2.db,sha256=_2Clv1SmsL6AkCv8ZEbqMO56vfWs-9Q-qadZs_PXYgQ,73728
|
|
11
|
+
robotics_geometry/.mypy_cache/3.11/cache.3.db,sha256=qqoSPNMkq78CXg9TJd4FCDWz02H7Hd_ZGrduAnEN7pY,36864
|
|
12
|
+
robotics_geometry/.mypy_cache/3.11/cache.4.db,sha256=0q596s1HpwX43JhSXhR24R-CjwnCRNgWkijOufNlBXU,204800
|
|
13
|
+
robotics_geometry/.mypy_cache/3.11/cache.5.db,sha256=nBHXTFaqIDOIwaf8iU02d5L9rZwmnxL0fCanerefb38,94208
|
|
14
|
+
robotics_geometry/.mypy_cache/3.11/cache.6.db,sha256=0VcFquFaxkJfATQzfxUyjuE82nWt6QNrNqIvRIj-69A,73728
|
|
15
|
+
robotics_geometry/.mypy_cache/3.11/cache.7.db,sha256=M0yYuHIbSKRO8EfOUzcnRS7J3WIUyp0903EMWMz78mY,24576
|
|
16
|
+
robotics_geometry/.mypy_cache/3.11/cache.8.db,sha256=1ZkMUczd965OpYZKjc8Z7wziWi_UsKUMshjVrGDQ4Vg,118784
|
|
17
|
+
robotics_geometry/.mypy_cache/3.11/cache.9.db,sha256=LmO_2Qx4MzJ_XBDNt2foPxArFjHRdwi9t2JdeQx7-8g,786432
|
|
18
|
+
robotics_geometry/.mypy_cache/CACHEDIR.TAG,sha256=8cE6_FVTWMkDOw8fMKqhd_6IvaQPS4okWYQA1UeHatw,190
|
|
19
|
+
robotics_geometry/__init__.py,sha256=9yzxoDSltMZkEjrmmWpFlg7mAp-jWxecPLXeHkG_D0M,57
|
|
20
|
+
robotics_geometry/functions.py,sha256=QrYPHBUidYBrxE-ZETGN9A9jlYXhMskyS_O3nvZ9w2s,1077
|
|
21
|
+
robotics_geometry/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
robotics_geometry/rigid_body2.py,sha256=V4prdbSwpR5aBBGSmrFr_F0pIIoDR0zJE2jbcNdY8CQ,11022
|
|
23
|
+
robotics_geometry/ros/__init__.py,sha256=1BZAx5fpe5v3eBYVrQHk_OrP2ZhbKwqgUjQVqcBFV1s,393
|
|
24
|
+
robotics_geometry/ros/msgs_mockup.py,sha256=yOCZlwSNS7yYMWeIOKVS6L3JNMYv6Qv8X2ZvjKdxBnM,2157
|
|
25
|
+
robotics_geometry/workspace2.py,sha256=E--FDMh-YUmMVnJlJrFa_DU_2c7a3k2PVoYf0qQ7Y7s,9512
|
|
26
|
+
robotics_geometry-0.1.0.dist-info/WHEEL,sha256=5XVLiN68MV2VpIWvmvgvvELLyDEnIzAqfKmdusfYfHI,80
|
|
27
|
+
robotics_geometry-0.1.0.dist-info/METADATA,sha256=cSBpa48Ob8HYKrei32KJ5wJKkxDVQaIhd7TMr2ir9uY,416
|
|
28
|
+
robotics_geometry-0.1.0.dist-info/RECORD,,
|