cgse-coordinates 0.17.3__py3-none-any.whl → 0.18.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.
- cgse_coordinates/settings.yaml +0 -16
- {cgse_coordinates-0.17.3.dist-info → cgse_coordinates-0.18.0.dist-info}/METADATA +1 -1
- cgse_coordinates-0.18.0.dist-info/RECORD +16 -0
- {cgse_coordinates-0.17.3.dist-info → cgse_coordinates-0.18.0.dist-info}/entry_points.txt +0 -3
- egse/coordinates/__init__.py +27 -334
- egse/coordinates/avoidance.py +33 -41
- egse/coordinates/cslmodel.py +48 -57
- egse/coordinates/laser_tracker_to_dict.py +16 -25
- egse/coordinates/point.py +544 -418
- egse/coordinates/pyplot.py +117 -105
- egse/coordinates/reference_frame.py +1417 -0
- egse/coordinates/refmodel.py +311 -203
- egse/coordinates/rotation_matrix.py +95 -0
- egse/coordinates/transform3d_addon.py +292 -228
- cgse_coordinates-0.17.3.dist-info/RECORD +0 -16
- egse/coordinates/referenceFrame.py +0 -1251
- egse/coordinates/rotationMatrix.py +0 -82
- {cgse_coordinates-0.17.3.dist-info → cgse_coordinates-0.18.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,1417 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The referenceFrames module provides the class :code:`ReferenceFrames` which defines the affine transformation
|
|
3
|
+
for bringing one reference frame to another.
|
|
4
|
+
|
|
5
|
+
.. todo:: The tests in methods like getPassiveTransformationTo using '==' should be looked at again and maybe
|
|
6
|
+
changed into using the 'is' operator. This because we now have __eq__ implemented.
|
|
7
|
+
|
|
8
|
+
@author: Pierre Royer
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import random
|
|
13
|
+
import string
|
|
14
|
+
import textwrap
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import transforms3d as t3
|
|
18
|
+
|
|
19
|
+
import egse.coordinates.transform3d_addon as t3add
|
|
20
|
+
from egse.coordinates.rotation_matrix import RotationMatrix
|
|
21
|
+
from egse.decorators import deprecate
|
|
22
|
+
from egse.exceptions import InvalidOperationError
|
|
23
|
+
|
|
24
|
+
LOGGER = logging.getLogger(__name__)
|
|
25
|
+
DEBUG = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def transformation_to_string(transformation: np.ndarray) -> str:
|
|
29
|
+
"""Represents the given transformation in a condensed form on one line.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
transformation (np.ndarray): Transformation matrix to be printed.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Given transformation in a condensed form on one line.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
if isinstance(transformation, np.ndarray):
|
|
39
|
+
if np.allclose(transformation, ReferenceFrame._I):
|
|
40
|
+
return "Identity"
|
|
41
|
+
|
|
42
|
+
message = np.array2string(
|
|
43
|
+
transformation,
|
|
44
|
+
separator=",",
|
|
45
|
+
suppress_small=True,
|
|
46
|
+
formatter={"float_kind": lambda x: "%.2f" % x},
|
|
47
|
+
).replace("\n", "")
|
|
48
|
+
return message
|
|
49
|
+
|
|
50
|
+
# We do not want to raise an Exception here since this is mainly used in logging messages
|
|
51
|
+
# and doesn't really harm the execution of the program.
|
|
52
|
+
|
|
53
|
+
return f"ERROR: expected transformation to be an ndarray, type={type(transformation)}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ReferenceFrame(object):
|
|
57
|
+
"""
|
|
58
|
+
A Reference Frame defined in reference frame "ref", i.e.
|
|
59
|
+
defined by the affine transformation bringing the reference frame "ref" onto "self".
|
|
60
|
+
|
|
61
|
+
By default, "ref" is the master refence frame, defined as the identity matrix.
|
|
62
|
+
|
|
63
|
+
:param transformation: 4x4 affine transformation matrix defining this system in "ref" system
|
|
64
|
+
:type transformation: numpy array
|
|
65
|
+
|
|
66
|
+
:param reference_frame: reference system in which this new reference frame is defined
|
|
67
|
+
:type reference_frame: ReferenceFrame
|
|
68
|
+
|
|
69
|
+
:param name: name the reference frame so it can be referenced, set to 'master' when None
|
|
70
|
+
:type name: str
|
|
71
|
+
|
|
72
|
+
:param rotation_config:
|
|
73
|
+
* Is set when using creator ReferenceFrame.fromTranslationRotation()
|
|
74
|
+
* In other cases, is set to a default "szyx"
|
|
75
|
+
(rotations around static axes z, y and x in this order)
|
|
76
|
+
In these other cases, it has no real direct influence,
|
|
77
|
+
except for methods returning the rotation vector (e.g. getRotationVector)
|
|
78
|
+
It is therefore always recommended to pass it to the constructor, even when
|
|
79
|
+
constructing the ReferenceFrame directly from a transformation matrix
|
|
80
|
+
:type rotation_config: str
|
|
81
|
+
|
|
82
|
+
Both the ``transformation`` and the ``ref`` parameters are mandatory.
|
|
83
|
+
|
|
84
|
+
If the reference frame is None, the master reference frame is created.
|
|
85
|
+
|
|
86
|
+
The master reference frame:
|
|
87
|
+
|
|
88
|
+
* is defined by the identity transformation matrix
|
|
89
|
+
* has itself as a reference
|
|
90
|
+
|
|
91
|
+
For convenience we provide the following factory methods:
|
|
92
|
+
|
|
93
|
+
createMaster()
|
|
94
|
+
Create a Master Reference Frame
|
|
95
|
+
|
|
96
|
+
createRotation(..)
|
|
97
|
+
Create a new Reference Frame that is rotated with respect to the given reference frame
|
|
98
|
+
|
|
99
|
+
createTranslation(..)
|
|
100
|
+
Create a new Reference Frame that is a translation with respect to the given reference frame
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
_I = np.identity(4)
|
|
104
|
+
_MASTER = None
|
|
105
|
+
_ROT_CONFIG_DEFAULT = "sxyz"
|
|
106
|
+
_names_used = [None, "Master"]
|
|
107
|
+
_strict_naming = False
|
|
108
|
+
_ACTIVE_DEFAULT = True
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self, transformation: np.ndarray | None, reference_frame, name=None, rotation_config=_ROT_CONFIG_DEFAULT
|
|
112
|
+
):
|
|
113
|
+
"""Initialization of a new reference frame.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
transformation (np.ndarray | None): 4x4 affine transformation matrix defining this system in the given
|
|
117
|
+
reference frame.
|
|
118
|
+
reference_frame:
|
|
119
|
+
name (str | None): Name of the reference frame.
|
|
120
|
+
rotation_config (str): Order in which the rotation about the three axes are chained.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
self.debug = False
|
|
124
|
+
|
|
125
|
+
DEBUG and LOGGER.debug(
|
|
126
|
+
f"transformation={transformation_to_string(transformation)}, reference_frame={reference_frame!r}, name={name}, rot_config={rotation_config}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# All argument testing is done in the __new__() method and we should be save here.
|
|
130
|
+
|
|
131
|
+
self.reference_frame = reference_frame
|
|
132
|
+
self.name = self.__create_name(name)
|
|
133
|
+
self.transformation = transformation
|
|
134
|
+
self.rotation_config = rotation_config
|
|
135
|
+
|
|
136
|
+
self.definition = [self.transformation, self.reference_frame, self.name]
|
|
137
|
+
|
|
138
|
+
self.x = self.get_axis("x")
|
|
139
|
+
self.y = self.get_axis("y")
|
|
140
|
+
self.z = self.get_axis("z")
|
|
141
|
+
|
|
142
|
+
self.linked_to = {}
|
|
143
|
+
self.reference_for = []
|
|
144
|
+
|
|
145
|
+
reference_frame.reference_for.append(self)
|
|
146
|
+
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
def __new__(cls, transformation, ref, name=None, rot_config=_ROT_CONFIG_DEFAULT):
|
|
150
|
+
"""Create a new ReferenceFrame class."""
|
|
151
|
+
|
|
152
|
+
DEBUG and LOGGER.debug(
|
|
153
|
+
f"transformation={transformation_to_string(transformation)}, ref={ref!r}, name={name}, rot_config={rot_config}"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if ref is None:
|
|
157
|
+
msg = (
|
|
158
|
+
"No reference frame was given, if you planned to create a Master Reference Frame, "
|
|
159
|
+
"use ReferenceFrame.createMaster(). "
|
|
160
|
+
)
|
|
161
|
+
LOGGER.error(msg)
|
|
162
|
+
raise ValueError(msg, "REF_IS_NONE")
|
|
163
|
+
|
|
164
|
+
if not isinstance(ref, cls):
|
|
165
|
+
msg = f"The 'ref' keyword argument is not a ReferenceFrame object, but {type(ref)}"
|
|
166
|
+
LOGGER.error(msg)
|
|
167
|
+
raise ValueError(msg, "REF_IS_NOT_CLS")
|
|
168
|
+
|
|
169
|
+
if name == "Master":
|
|
170
|
+
msg = (
|
|
171
|
+
"The 'name' argument cannot be 'Master' unless a Master instance should be created, "
|
|
172
|
+
"in that case, use ReferenceFrame.createMaster()"
|
|
173
|
+
)
|
|
174
|
+
LOGGER.error(msg)
|
|
175
|
+
raise ValueError(msg, "MASTER_NAME_USED")
|
|
176
|
+
|
|
177
|
+
if transformation is None:
|
|
178
|
+
msg = "The 'transformation' argument can not be None, please provide a proper transformation for this reference frame."
|
|
179
|
+
LOGGER.error(msg)
|
|
180
|
+
raise ValueError(msg, "TRANSFORMATION_IS_NONE")
|
|
181
|
+
|
|
182
|
+
if not isinstance(transformation, np.ndarray):
|
|
183
|
+
msg = f"The 'transformation' argument shall be a Numpy ndarray [not a {type(transformation)}], please provide a proper transformation for this reference frame."
|
|
184
|
+
LOGGER.error(msg)
|
|
185
|
+
raise ValueError(msg, "TRANSFORMATION_IS_NOT_NDARRAY")
|
|
186
|
+
|
|
187
|
+
if rot_config is None:
|
|
188
|
+
msg = "The 'rot_config' keyword argument can not be None, do not specify it when you want to use the default value."
|
|
189
|
+
LOGGER.error(msg)
|
|
190
|
+
raise ValueError(msg)
|
|
191
|
+
|
|
192
|
+
_instance = super(ReferenceFrame, cls).__new__(cls)
|
|
193
|
+
|
|
194
|
+
return _instance
|
|
195
|
+
|
|
196
|
+
def find_master(self):
|
|
197
|
+
"""Returns the master frame.
|
|
198
|
+
|
|
199
|
+
The master frame is always at the end of the path, when following the references.
|
|
200
|
+
|
|
201
|
+
Returns: Master frame.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
frame = self
|
|
205
|
+
|
|
206
|
+
while not frame.is_master():
|
|
207
|
+
frame = frame.reference_frame
|
|
208
|
+
|
|
209
|
+
return frame
|
|
210
|
+
|
|
211
|
+
@classmethod
|
|
212
|
+
def create_master(cls):
|
|
213
|
+
"""Creates a master reference frame.
|
|
214
|
+
|
|
215
|
+
A master reference frame is defined w.r.t. itself and is initialised with the identity matrix.
|
|
216
|
+
|
|
217
|
+
The master frame is automatically given the name "Master".
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
master_frame = super(ReferenceFrame, cls).__new__(cls)
|
|
221
|
+
master_frame.name = "Master"
|
|
222
|
+
master_frame.reference_frame = master_frame
|
|
223
|
+
master_frame.transformation = cls._I
|
|
224
|
+
master_frame.rotation_config = cls._ROT_CONFIG_DEFAULT
|
|
225
|
+
master_frame.initialized = True
|
|
226
|
+
master_frame.debug = False
|
|
227
|
+
master_frame.linked_to = {}
|
|
228
|
+
master_frame.reference_for = []
|
|
229
|
+
|
|
230
|
+
DEBUG and LOGGER.debug(
|
|
231
|
+
f"NEW MASTER CREATED: {id(master_frame)}, reference_frame = {id(master_frame.reference_frame)}, name = {master_frame.name}"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return master_frame
|
|
235
|
+
|
|
236
|
+
@classmethod
|
|
237
|
+
def __create_name(cls, name: str = None) -> str:
|
|
238
|
+
"""Creates a unique name for a reference frame.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
name (str): Name for a reference frame.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Unique name for a reference frame.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
if name is None:
|
|
248
|
+
while name in cls._names_used:
|
|
249
|
+
name = "F" + "".join(random.choices(string.ascii_uppercase, k=3))
|
|
250
|
+
return name
|
|
251
|
+
|
|
252
|
+
if cls._strict_naming:
|
|
253
|
+
# Generate a unique name
|
|
254
|
+
|
|
255
|
+
old_name = name
|
|
256
|
+
|
|
257
|
+
while name in cls._names_used:
|
|
258
|
+
name = "F" + "".join(random.choices(string.ascii_uppercase, k=3))
|
|
259
|
+
|
|
260
|
+
LOGGER.warning(
|
|
261
|
+
f"Name ('{old_name}') is already defined, since strict naming is applied, a new unique name was "
|
|
262
|
+
f"created: {name}"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
else:
|
|
266
|
+
if name in cls._names_used:
|
|
267
|
+
DEBUG and LOGGER.warning(
|
|
268
|
+
f"Name ('{name}') is already defined, now you have more than one ReferenceFrame with the same name."
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
cls._names_used.append(name)
|
|
272
|
+
|
|
273
|
+
return name
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def from_translation(
|
|
277
|
+
cls, translation_x: float, translation_y: float, translation_z: float, reference_frame, name: str = None
|
|
278
|
+
):
|
|
279
|
+
"""Creates a reference frame from a translation w.r.t. the given reference frame.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
translation_x (float): Translation along the x-axis.
|
|
283
|
+
translation_y (float): Translation along the y-axis.
|
|
284
|
+
translation_z (float): Translation along the z-axis.
|
|
285
|
+
reference_frame (ReferenceFrame): Reference frame w.r.t. which the translation is performed.
|
|
286
|
+
name (str): Simple, convenient name to identify the reference frame. If no name is provided, a random name
|
|
287
|
+
of four characters starting with 'F' will be generated.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Reference frame, based on the given translation along the axes w.r.t. the given reference frame.
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
affine_matrix = np.identity(4)
|
|
294
|
+
affine_matrix[:3, 3] = [translation_x, translation_y, translation_z]
|
|
295
|
+
|
|
296
|
+
if reference_frame is None:
|
|
297
|
+
raise ValueError(
|
|
298
|
+
"The reference_frame argument can not be None, provide a master or another reference frame."
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
return cls(transformation=affine_matrix, reference_frame=reference_frame, name=name)
|
|
302
|
+
|
|
303
|
+
@classmethod
|
|
304
|
+
def from_rotation(
|
|
305
|
+
cls,
|
|
306
|
+
rotation_x: float,
|
|
307
|
+
rotation_y: float,
|
|
308
|
+
rotation_z: float,
|
|
309
|
+
reference_frame,
|
|
310
|
+
name: str = None,
|
|
311
|
+
rotation_config: str = _ROT_CONFIG_DEFAULT,
|
|
312
|
+
active: bool = _ACTIVE_DEFAULT,
|
|
313
|
+
degrees: bool = True,
|
|
314
|
+
):
|
|
315
|
+
"""Creates a reference frame from a rotation w.r.t. the given reference frame.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
rotation_x (float): Rotation angle about the x-axis.
|
|
319
|
+
rotation_y (float): Rotation angle about the y-axis.
|
|
320
|
+
rotation_z (float): Rotation angle about the z-axis.
|
|
321
|
+
reference_frame (ReferenceFrame): Reference frame w.r.t. which the rotation is performed.
|
|
322
|
+
name (str): Simple, convenient name to identify the reference frame. If no name is provided, a random name
|
|
323
|
+
of four characters starting with 'F' will be generated.
|
|
324
|
+
rotation_config (str): Order in which the rotation about the three axes are chained.
|
|
325
|
+
active (bool): Indicates whether the rotation is active.
|
|
326
|
+
degrees (bool): Indicates whether the rotation angles are specified in degrees, rather than radians.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Reference frame, based on the given rotations about the axes w.r.t. the given reference frame.
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
if degrees:
|
|
333
|
+
rotation_x = np.deg2rad(rotation_x)
|
|
334
|
+
rotation_y = np.deg2rad(rotation_y)
|
|
335
|
+
rotation_z = np.deg2rad(rotation_z)
|
|
336
|
+
rotation_matrix = RotationMatrix(rotation_x, rotation_y, rotation_z, rotation_config, active=active)
|
|
337
|
+
|
|
338
|
+
zoom = np.array([1, 1, 1])
|
|
339
|
+
shear = np.array([0, 0, 0])
|
|
340
|
+
translation = [0, 0, 0]
|
|
341
|
+
|
|
342
|
+
transformation = t3.affines.compose(T=translation, R=rotation_matrix.rotation_matrix, Z=zoom, S=shear)
|
|
343
|
+
|
|
344
|
+
if reference_frame is None:
|
|
345
|
+
raise ValueError(
|
|
346
|
+
"The reference_frame argument can not be None, provide a master or another reference frame."
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
return cls(
|
|
350
|
+
transformation=transformation, reference_frame=reference_frame, name=name, rotation_config=rotation_config
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
@staticmethod
|
|
354
|
+
def from_points(points, plane: str = "xy", use_svd: bool = True, verbose: bool = True):
|
|
355
|
+
"""Finds the best-fitting plane to the given points.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
points (Points): Collection of point to which to fit the plane.
|
|
359
|
+
plane (str): Kind of plane to fit. Must be in ["xy", "yz", "zx"].
|
|
360
|
+
use_svd (bool): Indicates whether to use Single Value Decomposition (SVD).
|
|
361
|
+
verbose (bool): Indicates whether to print verbose output.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Reference frame, based on the given points.
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
return points.best_fitting_plane(plane=plane, use_svd=use_svd, verbose=verbose)
|
|
368
|
+
|
|
369
|
+
@classmethod
|
|
370
|
+
def from_translation_rotation(
|
|
371
|
+
cls,
|
|
372
|
+
translation: np.ndarray,
|
|
373
|
+
rotation: np.ndarray,
|
|
374
|
+
reference_frame,
|
|
375
|
+
name: str = None,
|
|
376
|
+
rotation_config: str = _ROT_CONFIG_DEFAULT,
|
|
377
|
+
active: bool = _ACTIVE_DEFAULT,
|
|
378
|
+
degrees: bool = True,
|
|
379
|
+
):
|
|
380
|
+
"""Creates a reference frame from the given translation and rotation vectors.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
translation (np.ndarray): Translation vector: 3x1 = tx, ty, tz.
|
|
384
|
+
rotation (np.ndarray): Rotation vector: 3x1: rx, ry, rz.
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
reference_frame (ReferenceFrame): Reference frame w.r.t. which the rotation is performed.
|
|
388
|
+
name (str): Simple, convenient name to identify the reference frame. If no name is provided, a random name
|
|
389
|
+
of four characters starting with 'F' will be generated.
|
|
390
|
+
rotation_config (str): Order in which the rotation about the three axes are chained.
|
|
391
|
+
active (bool): Indicates whether the rotation is active.
|
|
392
|
+
degrees (bool): Indicates whether the rotation angles are specified in degrees, rather than radians.
|
|
393
|
+
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
translation = np.array(translation)
|
|
397
|
+
zoom = np.array([1, 1, 1])
|
|
398
|
+
shear = np.array([0, 0, 0])
|
|
399
|
+
if degrees:
|
|
400
|
+
rotation = np.array([np.deg2rad(item) for item in rotation])
|
|
401
|
+
rotation_x, rotation_y, rotation_z = rotation
|
|
402
|
+
|
|
403
|
+
rotation_matrix = RotationMatrix(
|
|
404
|
+
rotation_x, rotation_y, rotation_z, rotation_config=rotation_config, active=active
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
if reference_frame is None:
|
|
408
|
+
raise ValueError(
|
|
409
|
+
"The reference_frame argument can not be None, provide a master or another reference frame."
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
return cls(
|
|
413
|
+
transformation=t3.affines.compose(translation, rotation_matrix.rotation_matrix, Z=zoom, S=shear),
|
|
414
|
+
reference_frame=reference_frame,
|
|
415
|
+
name=name,
|
|
416
|
+
rotation_config=rotation_config,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
def get_translation_vector(self) -> np.ndarray:
|
|
420
|
+
"""Returns the translation vector defining the reference frame.
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
Translation vector: 3x1 = tx, ty, tz.
|
|
424
|
+
"""
|
|
425
|
+
|
|
426
|
+
return self.transformation[:3, 3]
|
|
427
|
+
|
|
428
|
+
def get_rotation_vector(self, degrees: bool = True) -> np.ndarray:
|
|
429
|
+
"""Returns the rotation vector defining the reference frame.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
degrees (bool): Indicates whether the rotation angles are specified in degrees, rather than radians.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Rotation vector: 3x1 = rx, ry, rz.
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
rotation = t3.euler.mat2euler(self.transformation, axes=self.rotation_config)
|
|
439
|
+
|
|
440
|
+
if degrees:
|
|
441
|
+
rotation = np.array([np.rad2deg(item) for item in rotation])
|
|
442
|
+
|
|
443
|
+
return rotation
|
|
444
|
+
|
|
445
|
+
def get_translation_rotation_vectors(self, degrees: bool = True) -> tuple[np.ndarray, np.ndarray]:
|
|
446
|
+
"""Returns the translation and rotation (vectors) defining the reference frame.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
degrees (bool): Indicates whether the rotation angles are specified in degrees, rather than radians.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
Translation vector: 3x1 = tx, ty, tz.
|
|
453
|
+
Rotation vector: 3x1 = rx, ry, rz.
|
|
454
|
+
"""
|
|
455
|
+
|
|
456
|
+
translation = self.get_translation_vector()
|
|
457
|
+
rotation = self.get_rotation_vector(degrees=degrees)
|
|
458
|
+
|
|
459
|
+
return translation, rotation
|
|
460
|
+
|
|
461
|
+
def get_rotation_matrix(self) -> np.ndarray:
|
|
462
|
+
"""Returns the rotation matrix defining the reference frame.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
Rotation matrix defining the reference frame.
|
|
466
|
+
"""
|
|
467
|
+
|
|
468
|
+
result = self.transformation.copy()
|
|
469
|
+
result[:3, 3] = [0.0, 0.0, 0.0]
|
|
470
|
+
|
|
471
|
+
return result
|
|
472
|
+
|
|
473
|
+
def __repr__(self) -> str:
|
|
474
|
+
"""Returns a representation the reference frame.
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
Representation of the reference frame.
|
|
478
|
+
"""
|
|
479
|
+
|
|
480
|
+
return (
|
|
481
|
+
f"ReferenceFrame(transformation={transformation_to_string(self.transformation)}, "
|
|
482
|
+
f"reference_frame={self.reference_frame.name}, name={self.name}, rotation_config={self.rotation_config})"
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
def __str__(self) -> str:
|
|
486
|
+
"""Returns a printable string representation of the reference frame.
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
Printable string representation of the reference frame.
|
|
490
|
+
"""
|
|
491
|
+
|
|
492
|
+
message = textwrap.dedent(
|
|
493
|
+
f"""\
|
|
494
|
+
ReferenceFrame
|
|
495
|
+
name : {self.name}
|
|
496
|
+
reference : {self.reference_frame.name}
|
|
497
|
+
rotation_config : {self.rotation_config}
|
|
498
|
+
links : {[key.name for key in self.linked_to.keys()]}
|
|
499
|
+
transformation:
|
|
500
|
+
[{np.round(self.transformation[0], 3)}
|
|
501
|
+
{np.round(self.transformation[1], 3)}
|
|
502
|
+
{np.round(self.transformation[2], 3)}
|
|
503
|
+
{np.round(self.transformation[3], 3)}]
|
|
504
|
+
translation : {np.round(self.get_translation_vector(), 3)}
|
|
505
|
+
rotation : {np.round(self.get_rotation_vector(), 3)}"""
|
|
506
|
+
)
|
|
507
|
+
return message
|
|
508
|
+
|
|
509
|
+
@deprecate(
|
|
510
|
+
reason=(
|
|
511
|
+
"I do not see the added value of changing the name and "
|
|
512
|
+
"the current method has the side effect to change the name "
|
|
513
|
+
"to a random string when the name argument is already used."
|
|
514
|
+
),
|
|
515
|
+
alternative="the constructor argument to set the name already of the object.",
|
|
516
|
+
)
|
|
517
|
+
def set_name(self, name: str = None) -> None:
|
|
518
|
+
"""Sets or changes the name of the reference frame.
|
|
519
|
+
|
|
520
|
+
Args:
|
|
521
|
+
name (str): New name for the reference frame; if None, a random name will be generated.
|
|
522
|
+
|
|
523
|
+
Raises:
|
|
524
|
+
InvalidOperationError: When you try to change the name of the Master reference frame.
|
|
525
|
+
"""
|
|
526
|
+
|
|
527
|
+
if self.is_master():
|
|
528
|
+
raise InvalidOperationError(
|
|
529
|
+
"You try to change the name of the Master reference frame, which is not allowed."
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
self.name = self.__create_name(name)
|
|
533
|
+
|
|
534
|
+
def add_link(self, reference_frame, transformation=None, _stop: bool = False) -> None:
|
|
535
|
+
"""Adds a link with the given reference frame."""
|
|
536
|
+
|
|
537
|
+
# DONE: set the inverse transformation in the ref to this
|
|
538
|
+
# ref.linkedTo[self] = t3add.affine_inverse(transformation)
|
|
539
|
+
# TODO:
|
|
540
|
+
# remove the _stop keyword
|
|
541
|
+
|
|
542
|
+
# TODO: deprecate transformation as an input variable
|
|
543
|
+
# linkedTo can become a list of reference frames, with no transformation
|
|
544
|
+
# associated to the link. The tfo associated to a link is already
|
|
545
|
+
# checked in real time whenever the link is addressed
|
|
546
|
+
if transformation is None:
|
|
547
|
+
transformation = self.get_active_transformation_to(reference_frame)
|
|
548
|
+
else:
|
|
549
|
+
if DEBUG:
|
|
550
|
+
LOGGER.info(
|
|
551
|
+
"Deprecation warning: transformation will be automatically set to "
|
|
552
|
+
"the current relation between {self.name} and {ref.name}"
|
|
553
|
+
)
|
|
554
|
+
LOGGER.debug("Requested:")
|
|
555
|
+
LOGGER.debug(np.round(transformation, decimals=3))
|
|
556
|
+
LOGGER.debug("Auto (enforced):")
|
|
557
|
+
|
|
558
|
+
transformation = self.get_active_transformation_to(reference_frame)
|
|
559
|
+
|
|
560
|
+
DEBUG and LOGGER.debug(np.round(transformation, decimals=3))
|
|
561
|
+
|
|
562
|
+
self.linked_to[reference_frame] = transformation
|
|
563
|
+
|
|
564
|
+
# TODO simplify this when transformation is deprecated
|
|
565
|
+
# it becomes ref.linked_to[self] = ref.get_active_transformation_to(self)
|
|
566
|
+
reference_frame.linked_to[self] = t3add.affine_inverse(transformation)
|
|
567
|
+
|
|
568
|
+
def remove_link(self, reference_frame) -> None:
|
|
569
|
+
"""Removes the links with the given reference frame (both ways).
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
reference_frame (ReferenceFrame): Reference frame to remove the link with.
|
|
573
|
+
"""
|
|
574
|
+
|
|
575
|
+
# First remove the entry in ref to this
|
|
576
|
+
|
|
577
|
+
if self in reference_frame.linked_to:
|
|
578
|
+
del reference_frame.linked_to[self]
|
|
579
|
+
|
|
580
|
+
# Then remove the entry in this to ref
|
|
581
|
+
|
|
582
|
+
if reference_frame in self.linked_to:
|
|
583
|
+
del self.linked_to[reference_frame]
|
|
584
|
+
|
|
585
|
+
def get_passive_transformation_to(self, target_frame) -> np.ndarray:
|
|
586
|
+
"""Returns the transformation to apply to a point (defined in self) to express it in the target frame.
|
|
587
|
+
|
|
588
|
+
A passive transformation means that the point is static and that we change the reference frame around it.
|
|
589
|
+
|
|
590
|
+
get_passive_transformation_to(self, target_frame) == get_point_transformation_to(self, target_frame)
|
|
591
|
+
|
|
592
|
+
Args:
|
|
593
|
+
target_frame (ReferenceFrame): Reference frame to get the passive transformation to.
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
Passive transformation to apply to a point (defined in self) to express it in the target frame.
|
|
597
|
+
"""
|
|
598
|
+
|
|
599
|
+
DEBUG and LOGGER.debug("PASSIVE TO self {self.name} target {targetFrame.name}")
|
|
600
|
+
if target_frame is self:
|
|
601
|
+
# Nothing to do here, we already have the right coordinates
|
|
602
|
+
|
|
603
|
+
DEBUG and LOGGER.debug("case 1")
|
|
604
|
+
result = np.identity(4)
|
|
605
|
+
|
|
606
|
+
elif target_frame.reference_frame is self:
|
|
607
|
+
# The target frame is defined in self -> The requested transformation is the target frame definition
|
|
608
|
+
DEBUG and LOGGER.debug("=== 2 start ===")
|
|
609
|
+
result = t3add.affine_inverse(target_frame.transformation)
|
|
610
|
+
DEBUG and LOGGER.debug("=== 2 end ===")
|
|
611
|
+
elif target_frame.reference_frame is self.reference_frame:
|
|
612
|
+
# target_frame and self are defined wrt the same reference frame
|
|
613
|
+
# We want
|
|
614
|
+
# self --> target_frame
|
|
615
|
+
# We know
|
|
616
|
+
# target_frame.reference_frame --> target_frame (= target_frame.transformation)
|
|
617
|
+
# self.reference_frame --> self (= self.transformation)
|
|
618
|
+
# That is
|
|
619
|
+
# self --> self.reference_frame is target_frame.reference_frame --> target_frame
|
|
620
|
+
# inverse(definition) target_frame definition
|
|
621
|
+
|
|
622
|
+
# Both reference frames are defined w.r.t. the same reference frame
|
|
623
|
+
|
|
624
|
+
if DEBUG:
|
|
625
|
+
LOGGER.debug("=== 3 start ===")
|
|
626
|
+
LOGGER.debug(" ref \n{0}".format(self.reference_frame))
|
|
627
|
+
LOGGER.debug("===")
|
|
628
|
+
LOGGER.debug("self \n{0}".format(self))
|
|
629
|
+
LOGGER.debug("===")
|
|
630
|
+
LOGGER.debug("target_frame \n{0}".format(target_frame))
|
|
631
|
+
LOGGER.debug("===")
|
|
632
|
+
|
|
633
|
+
self_to_ref = self.transformation
|
|
634
|
+
DEBUG and LOGGER.debug("self_to_ref \n{0}".format(self_to_ref))
|
|
635
|
+
|
|
636
|
+
ref_to_target = t3add.affine_inverse(target_frame.transformation)
|
|
637
|
+
DEBUG and LOGGER.debug("ref_to_target \n{0}".format(ref_to_target))
|
|
638
|
+
|
|
639
|
+
result = np.dot(ref_to_target, self_to_ref)
|
|
640
|
+
DEBUG and LOGGER.debug("result \n{0}".format(result))
|
|
641
|
+
DEBUG and LOGGER.debug("=== 3 end ===")
|
|
642
|
+
else:
|
|
643
|
+
# We are after the transformation from
|
|
644
|
+
# self --> target_frame
|
|
645
|
+
# self --> self.reference_frame --> target_frame.reference_frame --> target_frame
|
|
646
|
+
#
|
|
647
|
+
# We know
|
|
648
|
+
# target_frame.reference_frame --> target_frame (target_frame.transformation)
|
|
649
|
+
# self.reference_frame --> self (self.transformation)
|
|
650
|
+
# but
|
|
651
|
+
# target_frame.reference_frame != self.reference_frame
|
|
652
|
+
# so we need
|
|
653
|
+
# self.reference_frame --> target_frame.reference_frame
|
|
654
|
+
# then we can compose
|
|
655
|
+
# self --> self.reference_frame --> target_frame.reference_frame --> target_frame
|
|
656
|
+
#
|
|
657
|
+
# Note: the transformation self.reference_frame --> target_frame.reference_frame is acquired recursively
|
|
658
|
+
# This relies on the underlying assumption that there exists
|
|
659
|
+
# one unique reference frame that source and self can be linked to
|
|
660
|
+
# (without constraints on the number of links necessary), i.e.
|
|
661
|
+
# that, from a frame to its reference or the opposite, there exists
|
|
662
|
+
# a path between self and target_frame. That is equivalent to
|
|
663
|
+
# the assumption that the entire set of reference frames is connex,
|
|
664
|
+
# i.e. defined upon a unique master reference frame.
|
|
665
|
+
|
|
666
|
+
DEBUG and LOGGER.debug("=== 4 start ===")
|
|
667
|
+
self_to_ref = self.transformation
|
|
668
|
+
self_ref_to_target_ref = self.reference_frame.get_passive_transformation_to(target_frame.reference_frame)
|
|
669
|
+
ref_to_target = t3add.affine_inverse(target_frame.transformation)
|
|
670
|
+
result = np.dot(ref_to_target, np.dot(self_ref_to_target_ref, self_to_ref))
|
|
671
|
+
DEBUG and LOGGER.debug("=== 4 end ===")
|
|
672
|
+
|
|
673
|
+
return result
|
|
674
|
+
|
|
675
|
+
def get_passive_translation_rotation_vectors_to(self, target_frame, degrees: bool = True):
|
|
676
|
+
"""Extracts the translation and rotation vectors from the passive transformation to the target frame.
|
|
677
|
+
|
|
678
|
+
Args:
|
|
679
|
+
target_frame (ReferenceFrame): Reference frame to get the passive transformation to.
|
|
680
|
+
degrees (bool): Indicates if the rotation vector should be in degrees rather than radians.
|
|
681
|
+
|
|
682
|
+
Returns:
|
|
683
|
+
Translation and rotation vectors from the passive transformation to the target frame.
|
|
684
|
+
"""
|
|
685
|
+
|
|
686
|
+
transformation = self.get_passive_transformation_to(target_frame)
|
|
687
|
+
|
|
688
|
+
rotation = t3.euler.mat2euler(transformation, axes=self.rotation_config)
|
|
689
|
+
if degrees:
|
|
690
|
+
rotation = np.array([np.rad2deg(item) for item in rotation])
|
|
691
|
+
translation = transformation[:3, 3]
|
|
692
|
+
|
|
693
|
+
return translation, rotation
|
|
694
|
+
|
|
695
|
+
def get_passive_translation_vector_to(self, target_frame) -> np.ndarray:
|
|
696
|
+
"""Extract the translation vector from the passive transformation to the target frame.
|
|
697
|
+
|
|
698
|
+
Args:
|
|
699
|
+
target_frame (ReferenceFrame): Reference frame to get the passive transformation to.
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
Translation vector from the passive transformation to the target frame.
|
|
703
|
+
"""
|
|
704
|
+
return self.get_passive_translation_rotation_vectors_to(target_frame)[0]
|
|
705
|
+
|
|
706
|
+
def get_passive_rotation_vector_to(self, target_frame, degrees=True):
|
|
707
|
+
"""Extracts the rotation vector from the passive transformation to the target frame.
|
|
708
|
+
|
|
709
|
+
Args:
|
|
710
|
+
target_frame (ReferenceFrame): Reference frame to get the passive transformation to.
|
|
711
|
+
degrees (bool): Indicates if the rotation vector should be in degrees rather than radians.
|
|
712
|
+
|
|
713
|
+
Returns:
|
|
714
|
+
Rotation vector from the passive transformation to the target frame.
|
|
715
|
+
"""
|
|
716
|
+
|
|
717
|
+
return self.get_passive_translation_rotation_vectors_to(target_frame, degrees=degrees)[1]
|
|
718
|
+
|
|
719
|
+
def get_passive_transformation_from(self, source_frame) -> np.ndarray:
|
|
720
|
+
"""Returns the transformation to apply to a point (defined in the source frame) to express it in self.
|
|
721
|
+
|
|
722
|
+
A passive transformation means that the point is static and that we change the reference frame around it.
|
|
723
|
+
|
|
724
|
+
get_passive_transformation_from(self, source_frame) == get_point_transformation_from(self, source_frame)
|
|
725
|
+
|
|
726
|
+
Args:
|
|
727
|
+
source_frame (ReferenceFrame): Reference frame to get the passive transformation from.
|
|
728
|
+
|
|
729
|
+
Returns:
|
|
730
|
+
Passive transformation to apply to a point (defined in the source frame) to express it in self.
|
|
731
|
+
"""
|
|
732
|
+
|
|
733
|
+
DEBUG and LOGGER.debug("PASSIVE FROM self {self.name} source {source.name}")
|
|
734
|
+
return source_frame.get_passive_transformation_to(self)
|
|
735
|
+
|
|
736
|
+
def get_passive_translation_rotation_vectors_from(self, source_frame, degrees: bool = True):
|
|
737
|
+
"""Extracts the translation and rotation vectors from the passive transformation from the source frame.
|
|
738
|
+
|
|
739
|
+
Args:
|
|
740
|
+
source_frame (ReferenceFrame): Reference frame to get the passive transformation from.
|
|
741
|
+
degrees (bool): Indicates if the rotation vector should be in degrees rather than radians.
|
|
742
|
+
|
|
743
|
+
Returns:
|
|
744
|
+
Translation and rotation vectors from the passive transformation from the source frame.
|
|
745
|
+
"""
|
|
746
|
+
|
|
747
|
+
transformation = self.get_passive_transformation_from(source_frame)
|
|
748
|
+
rotation = t3.euler.mat2euler(transformation, axes=self.rotation_config)
|
|
749
|
+
if degrees:
|
|
750
|
+
rotation = np.array([np.rad2deg(item) for item in rotation])
|
|
751
|
+
translation = transformation[:3, 3]
|
|
752
|
+
|
|
753
|
+
return translation, rotation
|
|
754
|
+
|
|
755
|
+
def get_passive_translation_vector_from(self, source_frame):
|
|
756
|
+
"""Extracts the translation vector from the passive transformation from the source frame.
|
|
757
|
+
|
|
758
|
+
Args:
|
|
759
|
+
source_frame (ReferenceFrame): Reference frame to get the passive transformation from.
|
|
760
|
+
|
|
761
|
+
Returns:
|
|
762
|
+
Translation vector from the passive transformation from the source frame.
|
|
763
|
+
"""
|
|
764
|
+
return self.get_passive_translation_rotation_vectors_from(source_frame)[0]
|
|
765
|
+
|
|
766
|
+
def get_passive_rotation_vector_from(self, source_frame, degrees=True):
|
|
767
|
+
"""Extracts the rotation vector from the passive transformation from the source frame.
|
|
768
|
+
|
|
769
|
+
Args:
|
|
770
|
+
source_frame (ReferenceFrame): Reference frame to get the passive transformation from.
|
|
771
|
+
degrees (bool): Indicates if the rotation vector should be in degrees rather than radians.
|
|
772
|
+
|
|
773
|
+
Returns:
|
|
774
|
+
Rotation vector from the passive transformation from the source frame.
|
|
775
|
+
"""
|
|
776
|
+
return self.get_passive_translation_rotation_vectors_from(source_frame, degrees=degrees)[1]
|
|
777
|
+
|
|
778
|
+
def get_active_transformation_to(self, target_frame) -> np.ndarray:
|
|
779
|
+
"""Returns the active transformation to the target frame.
|
|
780
|
+
|
|
781
|
+
Returns:
|
|
782
|
+
Transformation matrix that defines the target frame in the current frame.
|
|
783
|
+
"""
|
|
784
|
+
|
|
785
|
+
DEBUG and LOGGER.debug("ACTIVE TO self {self.name} target {target.name}")
|
|
786
|
+
return target_frame.get_passive_transformation_to(self)
|
|
787
|
+
|
|
788
|
+
def get_active_translation_rotation_vectors_to(
|
|
789
|
+
self, target_frame, degrees: bool = True
|
|
790
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
791
|
+
"""Extracts the translation and rotation vectors from the active transformation to the target frame.
|
|
792
|
+
|
|
793
|
+
Args:
|
|
794
|
+
target_frame (ReferenceFrame): Reference frame to get the active transformation from.
|
|
795
|
+
degrees (bool): Indicates if the rotation vector should be in degrees rather than radians.
|
|
796
|
+
|
|
797
|
+
Returns:
|
|
798
|
+
Translation and rotation vectors from the active transformation to the target frame.
|
|
799
|
+
"""
|
|
800
|
+
|
|
801
|
+
transformation = self.get_active_transformation_to(target_frame)
|
|
802
|
+
rotation = t3.euler.mat2euler(transformation, axes=self.rotation_config)
|
|
803
|
+
if degrees:
|
|
804
|
+
rotation = np.array([np.rad2deg(item) for item in rotation])
|
|
805
|
+
translation = transformation[:3, 3]
|
|
806
|
+
|
|
807
|
+
return translation, rotation
|
|
808
|
+
|
|
809
|
+
def get_active_translation_vector_to(self, target_frame, degrees: bool = True) -> np.ndarray:
|
|
810
|
+
"""Extracts the translation vector from the active transformation to the target frame.
|
|
811
|
+
|
|
812
|
+
Args:
|
|
813
|
+
target_frame (ReferenceFrame): Reference frame to get the active transformation from.
|
|
814
|
+
degrees (bool): Indicates if the rotation vector should be in degrees rather than radians.
|
|
815
|
+
|
|
816
|
+
Returns:
|
|
817
|
+
Translation vector from the active transformation to the target frame.
|
|
818
|
+
"""
|
|
819
|
+
|
|
820
|
+
return self.get_active_translation_rotation_vectors_to(target_frame, degrees=degrees)[0]
|
|
821
|
+
|
|
822
|
+
def get_active_rotation_vector_to(self, target_frame, degrees: bool = True):
|
|
823
|
+
"""Extracts the rotation vector from the active transformation to the target frame.
|
|
824
|
+
|
|
825
|
+
Args:
|
|
826
|
+
target_frame (ReferenceFrame): Reference frame to get the active transformation from.
|
|
827
|
+
degrees (bool): Indicates if the rotation vector should be in degrees rather than radians.
|
|
828
|
+
|
|
829
|
+
Returns:
|
|
830
|
+
Rotation vector from the active transformation to the target frame.
|
|
831
|
+
"""
|
|
832
|
+
|
|
833
|
+
return self.get_active_translation_rotation_vectors_to(target_frame, degrees=degrees)[1]
|
|
834
|
+
|
|
835
|
+
def get_active_transformation_from(self, source_frame):
|
|
836
|
+
"""Returns the active transformation from the source frame.
|
|
837
|
+
|
|
838
|
+
Returns:
|
|
839
|
+
Transformation matrix that defines the current frame in the source frame.
|
|
840
|
+
"""
|
|
841
|
+
|
|
842
|
+
DEBUG and LOGGER.debug("ACTIVE FROM self {self.name} source {source.name}")
|
|
843
|
+
return self.get_passive_transformation_to(source_frame)
|
|
844
|
+
|
|
845
|
+
def get_active_translation_rotation_vectors_from(self, source_frame, degrees: bool = True):
|
|
846
|
+
"""Extracts the translation and rotation vectors from the active transformation from the source frame.
|
|
847
|
+
|
|
848
|
+
Args:
|
|
849
|
+
source_frame (ReferenceFrame): Reference frame to get the active transformation from.
|
|
850
|
+
degrees (bool): Indicates if the rotation vector should be in degrees rather than radians.
|
|
851
|
+
|
|
852
|
+
Returns:
|
|
853
|
+
Translation and rotation vectors from the active transformation from the source frame.
|
|
854
|
+
"""
|
|
855
|
+
|
|
856
|
+
transformation = self.get_active_transformation_from(source_frame)
|
|
857
|
+
rotation = t3.euler.mat2euler(transformation, axes=self.rotation_config)
|
|
858
|
+
if degrees:
|
|
859
|
+
rotation = np.array([np.rad2deg(item) for item in rotation])
|
|
860
|
+
translation = transformation[:3, 3]
|
|
861
|
+
|
|
862
|
+
return translation, rotation
|
|
863
|
+
|
|
864
|
+
def get_active_translation_vector_from(self, source_frame):
|
|
865
|
+
"""Extracts the translation vector from the active transformation from the source frame.
|
|
866
|
+
|
|
867
|
+
Args:
|
|
868
|
+
source_frame (ReferenceFrame): Reference frame to get the active transformation from.
|
|
869
|
+
|
|
870
|
+
Returns:
|
|
871
|
+
Translation vector from the active transformation from the source frame.
|
|
872
|
+
"""
|
|
873
|
+
|
|
874
|
+
return self.get_active_translation_rotation_vectors_from(source_frame)[0]
|
|
875
|
+
|
|
876
|
+
def get_active_rotation_vector_from(self, source_frame, degrees: bool = True):
|
|
877
|
+
"""Extracts the rotation vector from the active transformation from the source frame.
|
|
878
|
+
|
|
879
|
+
Args:
|
|
880
|
+
source_frame (ReferenceFrame): Reference frame to get the active transformation from.
|
|
881
|
+
degrees (bool): Indicates if the rotation vector should be in degrees rather than radians.
|
|
882
|
+
|
|
883
|
+
Returns:
|
|
884
|
+
Rotation vector from the active transformation from the source frame.
|
|
885
|
+
"""
|
|
886
|
+
|
|
887
|
+
return self.get_active_translation_rotation_vectors_from(source_frame, degrees=degrees)[1]
|
|
888
|
+
|
|
889
|
+
def _find_ends(
|
|
890
|
+
self, frame, visited: list = [], ends: list = [], verbose: bool = True, level: int = 1
|
|
891
|
+
) -> tuple[list, list]:
|
|
892
|
+
"""Identifies the linked frames.
|
|
893
|
+
|
|
894
|
+
We discern between two types of frames:
|
|
895
|
+
1. Frames that are linked, either directly or indirectly (via multiple links) to the given frame. These are
|
|
896
|
+
returned as `visited`.
|
|
897
|
+
2. Frames of which the reference frame does not belong to the set of linked frames. These are returned as
|
|
898
|
+
`final_ends`.
|
|
899
|
+
|
|
900
|
+
Args:
|
|
901
|
+
frame (ReferenceFrame): Reference frame to find the linked frames from.
|
|
902
|
+
visited (list): List of frames that have already been visited.
|
|
903
|
+
ends (list): List of frames that have not been visited.
|
|
904
|
+
verbose (bool): Whether to print the progress.
|
|
905
|
+
level (int): Recursion level.
|
|
906
|
+
|
|
907
|
+
Returns:
|
|
908
|
+
Frames of which the reference frame does not belong to the set of linked frames. These are returned as
|
|
909
|
+
`final_ends`.
|
|
910
|
+
Frames of which the reference frame does not belong to the set of linked frames. These are returned as
|
|
911
|
+
`final_ends`.
|
|
912
|
+
"""
|
|
913
|
+
|
|
914
|
+
DEBUG and LOGGER.debug(
|
|
915
|
+
f"{level:-2d}{2 * level * ' '} Current: {frame.name} -- ends: {[f.name for f in ends]} -- visited {[f.name for f in visited]}"
|
|
916
|
+
)
|
|
917
|
+
# if verbose: print (f"{level:-2d}{2*level*' '} Current: {frame.name} -- ends: {[f.name for f in ends]} -- visited {[f.name for f in visited]}")
|
|
918
|
+
|
|
919
|
+
# Establish the set of 'linked_frames' (variable 'visited')
|
|
920
|
+
# The recursive process below keeps unwanted (non-endFrames), namely the
|
|
921
|
+
# frames that are not directly, but well indirectly linked to their reference
|
|
922
|
+
# This case is solved further down
|
|
923
|
+
|
|
924
|
+
if frame not in visited:
|
|
925
|
+
visited.append(frame)
|
|
926
|
+
|
|
927
|
+
if verbose and level:
|
|
928
|
+
level += 1
|
|
929
|
+
|
|
930
|
+
if frame.reference_frame not in frame.linked_to:
|
|
931
|
+
ends.append(frame)
|
|
932
|
+
DEBUG and LOGGER.debug(f"{(10 + 2 * level) * ' '}{frame.name}: new end")
|
|
933
|
+
# if verbose: LOGGER.info(f"{(10+2*level)*' '}{frame.name}: new end")
|
|
934
|
+
|
|
935
|
+
for linked_frame in frame.linked_to:
|
|
936
|
+
ends, visited = self._find_ends(linked_frame, visited=visited, ends=ends, verbose=verbose, level=level)
|
|
937
|
+
|
|
938
|
+
# If frame.reference_frame was linked to frame via an indirect route, reject it
|
|
939
|
+
|
|
940
|
+
final_ends = []
|
|
941
|
+
for aframe in ends:
|
|
942
|
+
if aframe.reference_frame not in ends:
|
|
943
|
+
final_ends.append(aframe)
|
|
944
|
+
|
|
945
|
+
return final_ends, visited
|
|
946
|
+
|
|
947
|
+
def set_transformation(
|
|
948
|
+
self, transformation, updated=None, preserve_links: bool = True, relative: bool = False, verbose: bool = True
|
|
949
|
+
) -> None:
|
|
950
|
+
"""Alters the definition of this coordinate system.
|
|
951
|
+
|
|
952
|
+
If other systems are linked to this one, their definition must be updated accordingly
|
|
953
|
+
|
|
954
|
+
The link set between two reference frames A & B is the active transformation matrix from A to B
|
|
955
|
+
|
|
956
|
+
A.addLink(B, matrix)
|
|
957
|
+
A.getActiveTransformationTo(B) --> matrix
|
|
958
|
+
|
|
959
|
+
The way to update the definition of the present system, and of those linked to it
|
|
960
|
+
depends on the structure of those links.
|
|
961
|
+
|
|
962
|
+
We define:
|
|
963
|
+
- the target frame as the one we want to move / re-define
|
|
964
|
+
- 'linkedFrames' as those directly, or indirectly (i.e. via multiple links)
|
|
965
|
+
linked to the target frame
|
|
966
|
+
- end_frames as the subset of linked_frames which are not linked to their reference (directly or indirectly)
|
|
967
|
+
- side_Frames as the set of frames whose reference is a linked_frame, but not themselves belonging to the linked_frames
|
|
968
|
+
|
|
969
|
+
We can demonstrate that updating the end_frames (Block A below) is sufficient to represent
|
|
970
|
+
the movement of the target frame and all frames directly or indirectly linked to it.
|
|
971
|
+
|
|
972
|
+
This may nevertheless have perverse effects for side_frames. Indeed,
|
|
973
|
+
their reference will (directly or implicitly) be redefined, but they shouldn't:
|
|
974
|
+
they are not linked to their reference --> their location in space (e.g. wrt the master frame)
|
|
975
|
+
should not be affected by the movement of the target frame. This is the aim of block B.
|
|
976
|
+
|
|
977
|
+
For a completely robust solution, 2 steps must be taken
|
|
978
|
+
BLOCK A. apply the right transformation to all "end_frames"
|
|
979
|
+
BLOCK B. Check for frames
|
|
980
|
+
using any of the "visited" frames as a reference
|
|
981
|
+
not linked to its reference
|
|
982
|
+
Correct its so that it doesn't move (it shouldn't be affected by the requested movement)
|
|
983
|
+
This demands a "reference_for" array property
|
|
984
|
+
|
|
985
|
+
Args:
|
|
986
|
+
transformation (np.ndarray): Affine transformation matrix to apply to the frame.
|
|
987
|
+
updated (list): List of frames that have been updated.
|
|
988
|
+
preserve_links (bool): Indicates whether the links of the frame should be preserved.
|
|
989
|
+
relative (bool): Indicates whether the transformation is relative to the current frame.
|
|
990
|
+
verbose (bool): Indicates whether to print verbose output.
|
|
991
|
+
"""
|
|
992
|
+
|
|
993
|
+
# Ruthless, enforced re-definition of one system. Know what you do, or stay away.
|
|
994
|
+
# Semi-unpredictable side effects if the impacted frame has links!
|
|
995
|
+
|
|
996
|
+
if not preserve_links:
|
|
997
|
+
self.transformation = transformation
|
|
998
|
+
return
|
|
999
|
+
|
|
1000
|
+
if updated is None:
|
|
1001
|
+
updated = []
|
|
1002
|
+
|
|
1003
|
+
# visitedFrames = all frames which can be reached from self via invariant links
|
|
1004
|
+
# endFrames = subset of visitedFrames that are at the end of a chain, and must be updated
|
|
1005
|
+
# in order to properly represent the requested movement
|
|
1006
|
+
end_frames, visited_frames = self._find_ends(frame=self, visited=[], ends=[], verbose=verbose)
|
|
1007
|
+
if verbose:
|
|
1008
|
+
LOGGER.info(f"Visited sub-system {[f.name for f in visited_frames]}")
|
|
1009
|
+
LOGGER.info(f"End-frames (movement necessary) {[f.name for f in end_frames]}")
|
|
1010
|
+
|
|
1011
|
+
# All updates are done by relative movements
|
|
1012
|
+
# so we must first compute the relative movement corresponding to the requested absolute movement
|
|
1013
|
+
if not relative:
|
|
1014
|
+
# virtual = what self should become after the (absolute) movement
|
|
1015
|
+
# it allows to compute the relative transformation to be applied and work in relative further down
|
|
1016
|
+
virtual = ReferenceFrame(
|
|
1017
|
+
transformation,
|
|
1018
|
+
reference_frame=self.reference_frame,
|
|
1019
|
+
name="virtual",
|
|
1020
|
+
rotation_config=self.rotation_config,
|
|
1021
|
+
)
|
|
1022
|
+
request = self.get_active_transformation_to(virtual)
|
|
1023
|
+
del virtual
|
|
1024
|
+
else:
|
|
1025
|
+
# If this method is called by applyTransformation,
|
|
1026
|
+
# we are facing a request for a relative movement
|
|
1027
|
+
# In that case the input is directly what we want
|
|
1028
|
+
request = transformation
|
|
1029
|
+
|
|
1030
|
+
# BLOCK B. Check for frames that were impacted but shouldn't have been and correct them
|
|
1031
|
+
# B1. List of frames demanding a correction
|
|
1032
|
+
# 'impacted' are frames having their reference inside the rigid structure moving, but not linked to it
|
|
1033
|
+
# If nothing is done, the movement will implicitly displace them, which is not intended
|
|
1034
|
+
|
|
1035
|
+
# Impacted shall not contain frames that are linked to self (== to any frame in visitedFrames) via any route...
|
|
1036
|
+
# We check if the impacted frames are in visitedFrames:
|
|
1037
|
+
# it is enough to know it's connected to the entire 'solid body' in which self belongs
|
|
1038
|
+
impacted = []
|
|
1039
|
+
for frame in visited_frames:
|
|
1040
|
+
for child in frame.reference_for:
|
|
1041
|
+
# Version 1 : too simple (restores too many frames)
|
|
1042
|
+
# if child not in frame.linkedTo:
|
|
1043
|
+
|
|
1044
|
+
# Version 2 : overkill
|
|
1045
|
+
# child_ends, child_visited = child._findEnds(frame=child,visited=[],ends=[],verbose=verbose)
|
|
1046
|
+
# if frame not in child_visited:
|
|
1047
|
+
|
|
1048
|
+
# Version 3 : just check if the child belongs to the rigid structure...
|
|
1049
|
+
if child not in visited_frames:
|
|
1050
|
+
impacted.append(child)
|
|
1051
|
+
|
|
1052
|
+
DEBUG and LOGGER.debug(f"Impacted (not moving, defined in moving) {[f.name for f in impacted]}")
|
|
1053
|
+
|
|
1054
|
+
# B2. save the location of all impacted frames
|
|
1055
|
+
# tempReference has the only purpose of avoiding that every frame must know the master
|
|
1056
|
+
# It could be any frame without links and defined wrt the master, but the master isn't known here...
|
|
1057
|
+
# TODO : confirm that the master isn't known (e.g. via cls._MASTER)
|
|
1058
|
+
|
|
1059
|
+
temp_master = self.find_master()
|
|
1060
|
+
to_restore = {}
|
|
1061
|
+
|
|
1062
|
+
for frame in impacted:
|
|
1063
|
+
to_restore[frame] = ReferenceFrame(
|
|
1064
|
+
frame.get_active_transformation_from(temp_master),
|
|
1065
|
+
reference_frame=temp_master,
|
|
1066
|
+
name=frame.name + "to_restore",
|
|
1067
|
+
rotation_config=frame.rotation_config,
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
# BLOCK A. apply the right transformation to all "endFrames"
|
|
1071
|
+
|
|
1072
|
+
# Ensure that `untouched` remains unaffected regardless of the update order of the end_frames
|
|
1073
|
+
# self_untouched = ReferenceFrame(
|
|
1074
|
+
# transformation = self.get_active_transformation_from(temp_master),
|
|
1075
|
+
# reference_frame=temp_master,
|
|
1076
|
+
# name=self.name + "_fixed",
|
|
1077
|
+
# rotation_config=self.rotation_config,
|
|
1078
|
+
# )
|
|
1079
|
+
|
|
1080
|
+
self_untouched = ReferenceFrame(
|
|
1081
|
+
transformation=self.transformation,
|
|
1082
|
+
reference_frame=self.reference_frame,
|
|
1083
|
+
name=self.name + "_fixed",
|
|
1084
|
+
rotation_config=self.rotation_config,
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
for bottom in end_frames:
|
|
1088
|
+
up = bottom.get_active_transformation_to(self_untouched)
|
|
1089
|
+
down = self_untouched.get_active_transformation_to(bottom)
|
|
1090
|
+
|
|
1091
|
+
relative_transformation = up @ request @ down
|
|
1092
|
+
|
|
1093
|
+
if DEBUG:
|
|
1094
|
+
LOGGER.debug(f"\nAdjusting {bottom.name} to {self.name}\nUpdated {[i.name for i in updated]}")
|
|
1095
|
+
LOGGER.debug(f"\ninput transformation \n{np.round(transformation, 3)}")
|
|
1096
|
+
LOGGER.debug(
|
|
1097
|
+
f"\nup \n{np.round(up, 3)}\ntransformation\n{np.round(request, 3)}\ndown\n{np.round(down, 3)}"
|
|
1098
|
+
)
|
|
1099
|
+
LOGGER.debug(f"\nrelative_transformation \n{np.round(relative_transformation, 3)}")
|
|
1100
|
+
|
|
1101
|
+
bottom.transformation = bottom.transformation @ relative_transformation
|
|
1102
|
+
|
|
1103
|
+
updated.append(bottom)
|
|
1104
|
+
|
|
1105
|
+
for frame in visited_frames:
|
|
1106
|
+
if frame not in updated:
|
|
1107
|
+
updated.append(frame)
|
|
1108
|
+
|
|
1109
|
+
# Block B
|
|
1110
|
+
# B3. Correction
|
|
1111
|
+
# we must set preserve_inks to False in order to prevent cascading impact from this update
|
|
1112
|
+
# if X1 is impacted with
|
|
1113
|
+
# X1.ref = E1 X1 --> X2 (simple link) E2.ref = X2
|
|
1114
|
+
# where X1 and X2 are "external frames" and E1 and E2 are "endFrames" that will hence move
|
|
1115
|
+
# X1 was impacted by the move of E1, but X2 wasn't
|
|
1116
|
+
# ==> wrt master, neither X1 nor X2 should have moved, but X1 did (via its ref)
|
|
1117
|
+
# and hence its link with X2 is now corrupt
|
|
1118
|
+
# We need to move X1 back to its original location wrt master
|
|
1119
|
+
# if we preserved the links while doing that,
|
|
1120
|
+
# we will move X2, which shouldn't move
|
|
1121
|
+
# (it didn't have to, it didn't and the goal is to restore the validity of the links)
|
|
1122
|
+
#
|
|
1123
|
+
# Direct restoration or the impacted frames at their original location
|
|
1124
|
+
|
|
1125
|
+
for frame in to_restore:
|
|
1126
|
+
frame.transformation = frame.reference_frame.get_active_transformation_to(to_restore[frame])
|
|
1127
|
+
|
|
1128
|
+
del to_restore
|
|
1129
|
+
|
|
1130
|
+
def set_translation_rotation(
|
|
1131
|
+
self,
|
|
1132
|
+
translation: np.ndarray,
|
|
1133
|
+
rotation: np.ndarray,
|
|
1134
|
+
rotation_config: str = _ROT_CONFIG_DEFAULT,
|
|
1135
|
+
active: bool = _ACTIVE_DEFAULT,
|
|
1136
|
+
degrees: bool = True,
|
|
1137
|
+
preserve_links: bool = True,
|
|
1138
|
+
) -> None:
|
|
1139
|
+
"""Alters the definition of this coordinate system.
|
|
1140
|
+
|
|
1141
|
+
Same as `set_transformation`, but here the input is translation and rotation vectors rather than an affine transformation matrix.
|
|
1142
|
+
|
|
1143
|
+
Args:
|
|
1144
|
+
translation (np.ndarray): Translation vector: 3x1 = tx, ty, tz.
|
|
1145
|
+
rotation (np.ndarray): Rotation vector: 3x1: rx, ry, rz.
|
|
1146
|
+
rotation_config (str): Order in which the rotation about the three axes are chained.
|
|
1147
|
+
active (bool): Indicates whether the rotation is active.
|
|
1148
|
+
degrees (bool): Indicates whether the rotation angles are specified in degrees, rather than radians.
|
|
1149
|
+
preserve_links (bool): Indicates whether the links of the frame should be preserved.
|
|
1150
|
+
"""
|
|
1151
|
+
|
|
1152
|
+
translation = np.array(translation)
|
|
1153
|
+
zoom = np.array([1, 1, 1])
|
|
1154
|
+
shear = np.array([0, 0, 0])
|
|
1155
|
+
if degrees:
|
|
1156
|
+
rotation = np.array([np.deg2rad(item) for item in rotation])
|
|
1157
|
+
rotation_x, rotation_y, rotation_z = rotation
|
|
1158
|
+
|
|
1159
|
+
rotation_matrix = RotationMatrix(
|
|
1160
|
+
rotation_x, rotation_y, rotation_z, rotation_config=rotation_config, active=active
|
|
1161
|
+
)
|
|
1162
|
+
|
|
1163
|
+
DEBUG and LOGGER.debug(t3.affines.compose(translation, rotation_matrix.rotation_matrix, Z=zoom, S=shear))
|
|
1164
|
+
|
|
1165
|
+
transformation = t3.affines.compose(translation, rotation_matrix.rotation_matrix, Z=zoom, S=shear)
|
|
1166
|
+
|
|
1167
|
+
self.set_transformation(transformation, preserve_links=preserve_links, relative=False)
|
|
1168
|
+
|
|
1169
|
+
def apply_transformation(self, transformation: np.ndarray, updated=None, preserve_links: bool = True) -> None:
|
|
1170
|
+
"""Applies the given transformation to the current reference frame's definition.
|
|
1171
|
+
|
|
1172
|
+
self.transformation := transformation @ self.transformation
|
|
1173
|
+
|
|
1174
|
+
Args:
|
|
1175
|
+
transformation (np.ndarray): Affine transformation matrix.
|
|
1176
|
+
updated (list, optional): List of frames that have been updated.
|
|
1177
|
+
preserve_links (bool): Indicates whether the links of the frame should be preserved.
|
|
1178
|
+
"""
|
|
1179
|
+
|
|
1180
|
+
if updated is None:
|
|
1181
|
+
updated = []
|
|
1182
|
+
|
|
1183
|
+
self.set_transformation(
|
|
1184
|
+
transformation=transformation,
|
|
1185
|
+
updated=updated,
|
|
1186
|
+
preserve_links=preserve_links,
|
|
1187
|
+
relative=True,
|
|
1188
|
+
)
|
|
1189
|
+
|
|
1190
|
+
def apply_translation_rotation(
|
|
1191
|
+
self,
|
|
1192
|
+
translation: np.ndarray,
|
|
1193
|
+
rotation: np.ndarray,
|
|
1194
|
+
rotation_config=None,
|
|
1195
|
+
active: bool = _ACTIVE_DEFAULT,
|
|
1196
|
+
degrees: bool = True,
|
|
1197
|
+
preserve_links: bool = True,
|
|
1198
|
+
) -> None:
|
|
1199
|
+
"""Applies the given translation and rotation vectors to the current reference frame's definition.
|
|
1200
|
+
|
|
1201
|
+
self.transformation := transformation @ self.transformation
|
|
1202
|
+
|
|
1203
|
+
Args:
|
|
1204
|
+
translation (np.ndarray): Translation vector.
|
|
1205
|
+
rotation (np.ndarray): Rotation vector.
|
|
1206
|
+
rotation_config (str): Order in which the rotation about the three axes are chained.
|
|
1207
|
+
active (bool): Indicates whether the rotation is active.
|
|
1208
|
+
degrees (bool): Indicates whether the rotation angles are specified in degrees, rather than radians.
|
|
1209
|
+
preserve_links (bool): Indicates whether the links of the frame should be preserved.
|
|
1210
|
+
"""
|
|
1211
|
+
|
|
1212
|
+
if rotation_config is None:
|
|
1213
|
+
rotation_config = self.rotation_config
|
|
1214
|
+
|
|
1215
|
+
translation = np.array(translation)
|
|
1216
|
+
zoom = np.array([1, 1, 1])
|
|
1217
|
+
shear = np.array([0, 0, 0])
|
|
1218
|
+
|
|
1219
|
+
if degrees:
|
|
1220
|
+
rotation = np.array([np.deg2rad(item) for item in rotation])
|
|
1221
|
+
rotation_x, rotation_y, rotation_z = rotation
|
|
1222
|
+
|
|
1223
|
+
rotation_matrix = RotationMatrix(
|
|
1224
|
+
rotation_x, rotation_y, rotation_z, rotation_config=rotation_config, active=active
|
|
1225
|
+
)
|
|
1226
|
+
|
|
1227
|
+
transformation = t3.affines.compose(translation, rotation_matrix.rotation_matrix, Z=zoom, S=shear)
|
|
1228
|
+
|
|
1229
|
+
self.apply_transformation(transformation, preserve_links=preserve_links)
|
|
1230
|
+
|
|
1231
|
+
def get_axis(self, axis: str, name: str | None = None):
|
|
1232
|
+
"""Returns a unit vector corresponding to the axis of choice in the current reference frame.
|
|
1233
|
+
|
|
1234
|
+
Args:
|
|
1235
|
+
axis(str) : Axis, in ["x","y","z"].
|
|
1236
|
+
name(str) : Name of the point.
|
|
1237
|
+
|
|
1238
|
+
Returns:
|
|
1239
|
+
Point, corresponding to the vectors defining the axis of choice in `self`.
|
|
1240
|
+
"""
|
|
1241
|
+
|
|
1242
|
+
unit_vectors = dict()
|
|
1243
|
+
|
|
1244
|
+
unit_vectors["x"] = [1, 0, 0]
|
|
1245
|
+
unit_vectors["y"] = [0, 1, 0]
|
|
1246
|
+
unit_vectors["z"] = [0, 0, 1]
|
|
1247
|
+
|
|
1248
|
+
if name is None:
|
|
1249
|
+
name = self.name + axis
|
|
1250
|
+
from egse.coordinates.point import Point
|
|
1251
|
+
|
|
1252
|
+
return Point(unit_vectors[axis], reference_frame=self, name=name)
|
|
1253
|
+
|
|
1254
|
+
def get_normal(self, name: str | None = None):
|
|
1255
|
+
"""Returns a unit vector, normal to the xy-plane.
|
|
1256
|
+
|
|
1257
|
+
This corresponds to [0,0,1] = get_axis("z").
|
|
1258
|
+
|
|
1259
|
+
The output can be used with the Point methods to express that axis in any reference frame.
|
|
1260
|
+
|
|
1261
|
+
Args:
|
|
1262
|
+
name(str | None) : Name of the point.
|
|
1263
|
+
|
|
1264
|
+
Returns:
|
|
1265
|
+
Point, corresponding to the vector defining the normal to the xy-plane in `self`.
|
|
1266
|
+
"""
|
|
1267
|
+
from egse.coordinates.point import Point
|
|
1268
|
+
|
|
1269
|
+
return Point([0, 0, 1], reference_frame=self, name=name)
|
|
1270
|
+
|
|
1271
|
+
def get_origin(self, name: str | None = None):
|
|
1272
|
+
"""Returns the origin in `self`.
|
|
1273
|
+
|
|
1274
|
+
The output can be used with the Point methods to express that axis in any reference frame.
|
|
1275
|
+
|
|
1276
|
+
Args:
|
|
1277
|
+
name (str | None) : Name of the point.
|
|
1278
|
+
|
|
1279
|
+
Returns:
|
|
1280
|
+
Point, corresponding to the vector defining the origin in 'self', i.e. [0,0,0]
|
|
1281
|
+
"""
|
|
1282
|
+
|
|
1283
|
+
from egse.coordinates.point import Point
|
|
1284
|
+
|
|
1285
|
+
return Point([0, 0, 0], reference_frame=self, name=name)
|
|
1286
|
+
|
|
1287
|
+
def is_master(self) -> bool:
|
|
1288
|
+
"""Checks whether this reference frame is a master reference frame.
|
|
1289
|
+
|
|
1290
|
+
Returns:
|
|
1291
|
+
True if this reference frame is a master reference frame; False otherwise.
|
|
1292
|
+
"""
|
|
1293
|
+
transformation = self.transformation
|
|
1294
|
+
|
|
1295
|
+
return (
|
|
1296
|
+
(self.name == self.reference_frame.name)
|
|
1297
|
+
and (transformation.shape[0] == transformation.shape[1])
|
|
1298
|
+
and np.allclose(transformation, np.eye(transformation.shape[0]))
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
def is_same(self, other) -> bool:
|
|
1302
|
+
"""Checks whether this reference frame is the same as another one (except for their name).
|
|
1303
|
+
|
|
1304
|
+
For two reference frames to be considered the same, they must have
|
|
1305
|
+
- The same transformation matrix,
|
|
1306
|
+
- The same reference frame,
|
|
1307
|
+
- The same rotation configuration.
|
|
1308
|
+
|
|
1309
|
+
The name of the reference frames may be different.
|
|
1310
|
+
|
|
1311
|
+
Returns:
|
|
1312
|
+
True if the two reference frames are the same (except for their name); False otherwise.
|
|
1313
|
+
|
|
1314
|
+
TODO This needs further work and testing!
|
|
1315
|
+
"""
|
|
1316
|
+
|
|
1317
|
+
if other is self:
|
|
1318
|
+
DEBUG and LOGGER.debug(
|
|
1319
|
+
"self and other are the same object (beware: this message might occur with recursion from self.ref != self.other)"
|
|
1320
|
+
)
|
|
1321
|
+
return True
|
|
1322
|
+
|
|
1323
|
+
if isinstance(other, ReferenceFrame):
|
|
1324
|
+
DEBUG and LOGGER.debug(f"comparing {self.name} and {other.name}")
|
|
1325
|
+
if not np.array_equal(self.transformation, other.transformation):
|
|
1326
|
+
DEBUG and LOGGER.debug("self.transformation not equals other.transformation")
|
|
1327
|
+
return False
|
|
1328
|
+
if self.rotation_config != other.rotation_config:
|
|
1329
|
+
DEBUG and LOGGER.debug("self.rotation_config not equals other.rot_config")
|
|
1330
|
+
return False
|
|
1331
|
+
# The following tests are here to prevent recursion to go infinite when self and other
|
|
1332
|
+
# point to itself
|
|
1333
|
+
if self.reference_frame is self and other.reference_frame is other:
|
|
1334
|
+
DEBUG and LOGGER.debug("both self.reference_frame and other.reference_frame point to themselves")
|
|
1335
|
+
pass
|
|
1336
|
+
else:
|
|
1337
|
+
DEBUG and LOGGER.debug("one of self.reference_frame or other.ref doesn't points to itself")
|
|
1338
|
+
if self.reference_frame != other.reference_frame:
|
|
1339
|
+
DEBUG and LOGGER.debug("self.reference_frame not equals other.reference_frame")
|
|
1340
|
+
return False
|
|
1341
|
+
if self.name is not other.name:
|
|
1342
|
+
DEBUG and LOGGER.debug(
|
|
1343
|
+
f"When checking two reference frames for equality, only their names differ: '{self.name}' not equals '{other.name}'"
|
|
1344
|
+
)
|
|
1345
|
+
pass
|
|
1346
|
+
return True
|
|
1347
|
+
|
|
1348
|
+
return NotImplemented
|
|
1349
|
+
|
|
1350
|
+
def __eq__(self, other):
|
|
1351
|
+
"""Overrides the default implementation, which basically checks for id(self) == id(other).
|
|
1352
|
+
|
|
1353
|
+
Two Reference Frames are considered equal when:
|
|
1354
|
+
- Their transformation matrices are equal,
|
|
1355
|
+
- Their reference frame is equal,
|
|
1356
|
+
- Their rotation configuration is the same
|
|
1357
|
+
|
|
1358
|
+
TODO: Do we want to insist on the name being equal?
|
|
1359
|
+
YES - for strict testing
|
|
1360
|
+
NO - this might need a new method like is_same(self, other) where the criteria are relaxed
|
|
1361
|
+
|
|
1362
|
+
|
|
1363
|
+
TODO This needs further work and testing!
|
|
1364
|
+
"""
|
|
1365
|
+
|
|
1366
|
+
if other is self:
|
|
1367
|
+
DEBUG and LOGGER.debug(
|
|
1368
|
+
"self and other are the same object (beware: this message might occur with recursion from self.ref != self.other)"
|
|
1369
|
+
)
|
|
1370
|
+
return True
|
|
1371
|
+
|
|
1372
|
+
if isinstance(other, ReferenceFrame):
|
|
1373
|
+
DEBUG and LOGGER.debug(f"comparing {self.name} and {other.name}")
|
|
1374
|
+
if not np.array_equal(self.transformation, other.transformation):
|
|
1375
|
+
DEBUG and LOGGER.debug("self.transformation not equals other.transformation")
|
|
1376
|
+
return False
|
|
1377
|
+
if self.rotation_config != other.rotation_config:
|
|
1378
|
+
DEBUG and LOGGER.debug("self.rot_config not equals other.rot_config")
|
|
1379
|
+
return False
|
|
1380
|
+
# The following tests are here to prevent recursion to go infinite when self and other
|
|
1381
|
+
# point to itself
|
|
1382
|
+
if self.reference_frame is self and other.reference_frame is other:
|
|
1383
|
+
DEBUG and LOGGER.debug("both self.reference_frame and other.reference_frame point to themselves")
|
|
1384
|
+
pass
|
|
1385
|
+
else:
|
|
1386
|
+
DEBUG and LOGGER.debug("one of self.reference_frame or other.ref doesn't points to itself")
|
|
1387
|
+
if self.reference_frame != other.reference_frame:
|
|
1388
|
+
DEBUG and LOGGER.debug("self.ref not equals other.reference_frame")
|
|
1389
|
+
return False
|
|
1390
|
+
if self.name is not other.name:
|
|
1391
|
+
DEBUG and LOGGER.debug(
|
|
1392
|
+
f"When checking two reference frames for equality, only their names differ: '{self.name}' not equals '{other.name}'"
|
|
1393
|
+
)
|
|
1394
|
+
return False
|
|
1395
|
+
|
|
1396
|
+
return True
|
|
1397
|
+
|
|
1398
|
+
return NotImplemented
|
|
1399
|
+
|
|
1400
|
+
def __hash__(self):
|
|
1401
|
+
"""Overrides the default implementation."""
|
|
1402
|
+
|
|
1403
|
+
hash_number = (id(self.rotation_config) + id(self.reference_frame) + id(self.name)) // 16
|
|
1404
|
+
return hash_number
|
|
1405
|
+
|
|
1406
|
+
def __copy__(self):
|
|
1407
|
+
"""Overrides the default implementation."""
|
|
1408
|
+
|
|
1409
|
+
DEBUG and LOGGER.debug(
|
|
1410
|
+
f'Copying {self!r} unless {self.name} is "Master" in which case the Master itself is returned.'
|
|
1411
|
+
)
|
|
1412
|
+
|
|
1413
|
+
if self.is_master():
|
|
1414
|
+
DEBUG and LOGGER.debug(f"Returning Master itself instead of a copy.")
|
|
1415
|
+
return self
|
|
1416
|
+
|
|
1417
|
+
return ReferenceFrame(self.transformation, self.reference_frame, self.name, self.rotation_config)
|