3
3
from spatialmath import Twist3 , SE3
4
4
from spatialmath .base import skew
5
5
from roboticstoolbox .robot import Link , Robot
6
+ from roboticstoolbox .robot .ET import ET
7
+ from roboticstoolbox .robot .ETS import ETS
6
8
7
9
8
10
class PoELink (Link ):
@@ -15,7 +17,11 @@ class PoELink(Link):
15
17
"""
16
18
17
19
def __init__ (self , twist , name = None ):
18
- super ().__init__ ()
20
+ # get ETS of the link in the world frame, given by its twist
21
+ ets = self ._ets_world (twist )
22
+
23
+ # initialize the link with its world frame ETS
24
+ super ().__init__ (ets )
19
25
self .S = Twist3 (twist )
20
26
self .name = name
21
27
@@ -33,6 +39,87 @@ def __str__(self):
33
39
s += "]"
34
40
return s
35
41
42
+ def _ets_world (self , twist : Twist3 ) -> ETS :
43
+ """
44
+ Convert twist to its corresponding ETS
45
+
46
+ This method obtains an SE3 object that corresponds to a given twist and
47
+ returns its ETS in the world frame.
48
+
49
+ :param twist: given twist as Twist3 object
50
+
51
+ References
52
+ ----------
53
+ - D. Huczala, T. Kot, J. Mlotek, J. Suder and M. Pfurner, *An Automated
54
+ Conversion Between Selected Robot Kinematic Representations*, ICCMA 2022,
8000
code>
55
+ Luxembourg, doi: 10.1109/ICCMA56665.2022.10011595
56
+
57
+ """
58
+
59
+ # set base of the robot to the origin
60
+ base = SE3 ()
61
+ # get screw axis components
62
+ w = twist .w
63
+ v = twist .v
64
+
65
+ if isinstance (self , PoEPrismatic ):
66
+ # get the prismatic axis's directional vector component
67
+ a_vec = v
68
+ # vector of the x-axis (default to origin's n vector)
69
+ n_vec = base .n
70
+ # set point on screw axis to origin (prismatic joint has no moment)
71
+ t_vec = base .t
72
+
73
+ elif isinstance (self , PoERevolute ):
74
+ # get the nearest point of the screw axis closest to the origin
75
+ principal_point = np .cross (w , v )
76
+
77
+ # get vector of the x-axis
78
+ if np .isclose (np .linalg .norm (principal_point ), 0.0 ): # the points are
79
+ # coincident
80
+ n_vec = base .n
81
+ else : # along the direction to principal point
82
+ n_vec = principal_point / np .linalg .norm (principal_point )
83
+
84
+ # get the revolute axis directional vector component
85
+ a_vec = w
86
+ # point on screw axis
87
+ t_vec = principal_point
88
+
89
+ else : # not a joint
90
+ n_vec = base .n
91
+ a_vec = base .a
92
+ t_vec = v
93
+
94
+ o_vec = np .cross (a_vec , n_vec )
95
+
96
+ # construct transform from obtained vectors
97
+ twist_as_SE3 = SE3 .OA (o_vec , a_vec )
98
+ twist_as_SE3 .t = t_vec
99
+ # get RPY parameters
100
+ rpy = twist_as_SE3 .rpy ()
101
+
102
+ # prepare list of ETs, due to RPY convention the RPY order is reversed
103
+ et_list = [
104
+ ET .tx (twist_as_SE3 .t [0 ]),
105
+ ET .ty (twist_as_SE3 .t [1 ]),
106
+ ET .tz (twist_as_SE3 .t [2 ]),
107
+ ET .Rz (rpy [2 ]),
108
+ ET .Ry (rpy [1 ]),
109
+ ET .Rx (rpy [0 ]),
110
+ ]
111
+ # remove ETs with empty transform
112
+ et_list = [et for et in et_list if not np .isclose (et .eta , 0.0 )]
113
+
114
+ # assign joint variable at the end of list (if the frame is not base or tool
115
+ # frame)
116
+ if isinstance (self , PoEPrismatic ):
117
+ et_list .append (ET .tz ())
118
+ elif isinstance (self , PoERevolute ):
119
+ et_list .append (ET .Rz ())
120
+
121
+ return ETS (et_list )
122
+
36
123
37
124
class PoERevolute (PoELink ):
38
125
def __init__ (self , axis , point , ** kwargs ):
@@ -83,11 +170,17 @@ def __init__(self, links, T0, **kwargs):
83
170
:seealso: :class:`PoEPrismatic` :class:`PoERevolute`
84
171
"""
85
172
86
- self ._n = len (links )
173
+ # add base link and end-effector link
174
+ links .insert (0 , PoELink (Twist3 ()))
175
+ links .append (PoELink (T0 .twist ()))
87
176
88
177
super ().__init__ (links , ** kwargs )
89
178
self .T0 = T0
90
179
180
+ # update ETS according to the given links order (in PoELink their ETS is
181
+ # given WITH relation to base frame, NOT to previous joint's ETS)
182
+ self ._update_ets ()
183
+
91
184
def __str__ (self ):
92
185
"""
93
186
Pretty prints the PoE Model of the robot.
@@ -110,7 +203,6 @@ def __repr__(self):
110
203
s += ")"
111
204
return s
112
205
113
-
114
206
def nbranches (self ):
115
207
return 0
116
208
@@ -124,11 +216,11 @@ def fkine(self, q):
124
216
:rtype: SE3
125
217
"""
126
218
T = None
127
- for link , qk in zip (self , q ):
219
+ for i in range (self . n ):
128
220
if T is None :
129
- T = link . S .exp (qk )
221
+ T = self . links [ i + 1 ]. S .exp (q [ i ] )
130
222
else :
131
- T *= link . S .exp (qk )
223
+ T *= self . links [ i + 1 ]. S .exp (q [ i ] )
132
224
133
225
return T * self .T0
134
226
@@ -146,9 +238,9 @@ def jacob0(self, q):
146
238
"""
147
239
columns = []
148
240
T = SE3 ()
149
- for link , qk in zip (self , q ):
150
- columns .append (T .Ad () @ link .S .S )
151
- T *= link . S .exp (qk )
241
+ for i in range (self . n ):
242
+ columns .append (T .Ad () @ self . links [ i + 1 ] .S .S )
243
+ T *= self . links [ i + 1 ]. S .exp (q [ i ] )
152
244
T *= self .T0
153
245
J = np .column_stack (columns )
154
246
@@ -168,21 +260,70 @@ def jacobe(self, q):
168
260
"""
169
261
columns = []
170
262
T = SE3 ()
171
- for link , qk in zip (self , q ):
172
- columns .append (T .Ad () @ link .S .S )
173
- T *= link . S .exp (qk )
263
+ for i in range (self . n ):
264
+ columns .append (T .Ad () @ self . links [ i + 1 ] .S .S )
265
+ T *= self . links [ i + 1 ]. S .exp (q [ i ] )
174
266
T *= self .T0
175
267
J = np .column_stack (columns )
176
268
177
269
# convert velocity twist from world frame to EE frame
178
270
return T .inv ().Ad () @ J
179
271
180
- def ets (self ):
181
- return NotImplemented
272
+ def _update_ets (self ):
273
+ """
274
+ Updates ETS of links when PoERobot is initialized according to joint order
275
+
276
+ By default, PoE representation specifies twists in relation to the base frame
277
+ of a robot. Since the PoELinks are initialized prior to PoERobot, their ETS is
278
+ given in the base frame, not in relation between links. This method creates
279
+ partial transforms between links and obtains new ETSs that respect the links
280
+ order.
281
+ """
282
+
283
+ # initialize transformations between joints from joint 1 to ee, related to
284
+ # the world (base) frame
285
+ twist_as_SE3_world = [SE3 (link .Ts ) for link in self .links ]
286
+
287
+ # update the ee since its twist can provide transform with different x-, y-axes
288
+ twist_as_SE3_world [- 1 ] = self .T0
289
+
290
+ # initialize partial transforms
291
+ twist_as_SE3_partial = [SE3 ()] * (self .n + 2 )
292
+
293
+ # get partial transforms between links
294
+ for i in reversed (range (1 , self .n + 2 )):
295
+ twist_as_SE3_partial [i ] = (
296
+ twist_as_SE3_world [i - 1 ].inv () * twist_as_SE3_world [i ]
297
+ )
298
+
299
+ # prepare ET sequence
300
+ for i , tf in enumerate (twist_as_SE3_partial ):
301
+ # get RPY parameters
302
+ rpy = tf .rpy ()
303
+
304
+ # prepare list of ETs, due to RPY convention the RPY order is reversed
305
+ et_list = [
306
+ ET .tx (tf .t [0 ]),
307
+ ET .ty (tf .t [1 ]),
308
+ ET .tz (tf .t [2 ]),
309
+ ET .Rz (rpy [2 ]),
310
+ ET .Ry (rpy [1 ]),
311
+ ET .Rx (rpy [0 ]),
312
+ ]
313
+ # remove ETs with empty transform
314
+ et_list = [et for et in et_list if not np .isclose (et .eta , 0.0 )]
315
+
316
+ # assign joint variable with corresponding index
317
+ if self .links [i ].isrevolute :
318
+ et_list .append (ET .Rz (jindex = i - 1 ))
319
+ elif self .links [i ].isprismatic :
320
+ et_list .append (ET .tz (jindex = i - 1 ))
321
+
322
+ # update the ETS for given link
323
+ self .links [i ].ets = ETS (et_list )
182
324
183
325
184
326
if __name__ == "__main__" : # pragma nocover
185
-
186
327
T0 = SE3 .Trans (2 , 0 , 0 )
187
328
188
329
# rotate about z-axis, through (0,0,0)
0 commit comments