-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdqpy.py
373 lines (319 loc) · 12.8 KB
/
dqpy.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d
import warnings
class DQ(object):
''' Dual Quaternion class'''
__threshold = 1e-12
'''Absolute values below the threshold are considered zero.'''
def __init__(self, values=0.):
if not hasattr(values, '__iter__'):
values = [values]
values = np.asarray(values)
self.q = np.r_[values, np.zeros(8 - values.size)]
def toRound(self, value):
if type(value) is bool:
__threshold = value
# Selection and transformation methods
def p(self, idx=-1):
'''
dq.P returns the primary part of the dual quaternion.
dq.P(index), returns the coefficient corresponding to index.
'''
if idx == -1:
return DQ(self.q[:4])
else:
if idx in range(4):
return self.q[idx]
def d(self, idx=-1):
'''
dq.D returns the dual part of the dual quaternion.
dq.D(index), returns the coefficient corresponding to index.
'''
if idx == -1:
return DQ(self.q[4:])
else:
if idx in range(4):
return self.q[4+idx]
def re(self):
'''Return the real part of the dual quaternion.'''
return DQ(np.r_[self.q[0], 0, 0, 0, self.q[4]])
def im(self):
'''Return the imaginary part of the dual quaternion.'''
return DQ(np.r_[0, self.q[1:4], 0, self.q[5:8]])
@staticmethod
def e():
'''Dual unit'''
return DQ([0, 0, 0, 0, 1])
@staticmethod
def i():
'''Imaginary i'''
return DQ([0, 1, 0, 0])
@staticmethod
def j():
'''Imaginary j'''
return DQ([0, 0, 1, 0])
@staticmethod
def k():
'''Imaginary k'''
return DQ([0, 0, 0, 1])
@staticmethod
def c4():
''''matrix C4 that satisfies the relation vec4(x') = C4 * vec4(x).'''
return np.diag([1, -1, -1, -1])
@staticmethod
def c8():
''''return C8 matrix that satisfies vec8(x') = C8 * vec8(x).'''
return np.diag([1, -1, -1, -1]*2)
def hamiplus4(self):
'''return the positive Hamilton operator (4x4) of the quaternion.'''
return np.array([[self.q[0], -self.q[1], -self.q[2], -self.q[4]],\
[self.q[1], self.q[0], -self.q[3], self.q[2]],\
[self.q[2], self.q[3], self.q[0], -self.q[1]],
[self.q[3], -self.q[2], self.q[1], self.q[0]]])
def hamiminus4(self):
'''return the negative Hamilton operator (4x4) of the quaternion.'''
return np.array([[self.q[0], -self.q[1], -self.q[2], -self.q[4]],\
[self.q[1], self.q[0], self.q[3], -self.q[2]],\
[self.q[2], -self.q[3], self.q[0], self.q[1]],
[self.q[3], self.q[2], -self.q[1], self.q[0]]])
def hamiplus8(self):
'''return the positive Hamilton operator (8x8) of the dual quaternion.'''
return np.array([[self.p().hamiplus4(), np.zeros([4, 4])], \
[self.d().hamiplus4(), self.p().hamiplus4()]])
def haminus8(self):
'''return the negative Hamilton operator (8x8) of the dual quaternion.'''
return np.array([[self.p().hamiminus4(), np.zeros([4, 4])], \
[self.d().hamiminus4(), self.p().hamiminus4()]])
def vec4(self):
'''return the vector with the four dual quaternion coefficients.'''
return self.q[:4]
def vec8(self):
'''return the vector with the eight dual quaternion coefficients.'''
return self.q[:]
# Math methods
def acos(self):
'''
returns the acos of scalar dual quaternion x.
If x is not scalar, the function returns an error.
'''
if self.im() != 0. or self.d() != 0.:
raise ValueError('The dual quaternion is not a scalar.')
return np.arccos(self.q[0])
def rotation_angle(self):
'''returns the rotation angle of dq or an error otherwise.'''
if not self.is_unit():
raise ValueError('The dual quaternion does not have unit norm.')
return self.p().re().acos()*2
def rotation_axis(self):
'''returns the rotation axis (nx*i + ny*j + nz*k) of \
the unit dual quaternion.'''
if not self.is_unit():
raise ValueError('The dual quaternion does not have unit norm.')
phi = np.arccos(self.q[0])
if phi == 0:
# Convention = DQ.k. It could be any rotation axis.
return DQ([0, 0, 0, 1])
else:
return self.p().im() * np.sin(phi)**-1
def translation(self):
'''returns the translation of the unit dual quaternion.'''
if not self.is_unit():
raise ValueError('The dual quaternion does not have unit norm.')
return self.d() * self.p().conj() * 2
def t(self):
'''returns the translation part of dual quaternion.'''
# More specifically, if x = r+DQ.E*(1/2)*p*r,
# T(x) returns 1+DQ.E*(0.5)*p
return self * self.p().conj()
def conj(self):
'''returns the conjugate of dq.'''
return DQ([self.q[0], -self.q[1], -self.q[2], -self.q[3],\
self.q[4], -self.q[5], -self.q[6], -self.q[7]])
def __add__(self, other):
if other.__class__ != DQ:
other = DQ(other)
return DQ(self.q + other.q)
def __sub__(self, other):
if other.__class__ != DQ:
other = DQ(other)
return DQ(self.q - other.q)
@staticmethod
def __quaternion_mul(q_a, q_b):
'''returns quaterion multiplication.'''
mat_a = np.array([[q_a[0], -q_a[1], -q_a[2], -q_a[3]], \
[q_a[1], q_a[0], -q_a[3], q_a[2]], \
[q_a[2], q_a[3], q_a[0], -q_a[1]], \
[q_a[3], -q_a[2], q_a[1], q_a[0]]])
return np.dot(mat_a, q_b)
def __mul__(self, other):
'''returns the standard dual quaternion multiplication.'''
if other.__class__ != DQ:
other = DQ(other)
non_dual = DQ.__quaternion_mul(self.q[:4], other.q[:4])
dual = DQ.__quaternion_mul(self.q[:4], other.q[4:]) + \
DQ.__quaternion_mul(self.q[4:], other.q[:4])
return DQ(np.r_[non_dual, dual])
def inv(self):
'''returns the inverse of the dual quaternion.'''
dq_c = self.conj()
dq_q = self * dq_c
inv = DQ([1/dq_q.q[0], 0, 0, 0, -dq_q.q[4]/(dq_q.q[0]**2)])
return dq_c*inv
def log(self):
'''returns the logarithm of the dual quaternion.'''
if not self.is_unit():
raise ValueError('The log function is currently defined \
only for unit dual quaternions.')
prim = self.rotation_axis() * (self.rotation_angle() * .5)
dual = self.translation() * .5
return prim + dual * DQ.e()
def exp(self):
'''returns the exponential of the pure dual quaternion'''
if self.re() != 0:
raise ValueError('The exponential operation is defined only \
for pure dual quaternions.')
phi = np.linalg.norm(self.p().q[:])
if phi != 0:
prim = self.p() * (np.sin(phi)/phi) + np.cos(phi)
else:
prim = DQ([1])
return prim + self.d() * prim * DQ.e()
def __pow__(self, num):
'''
returns the dual quaternion corresponding to the operation
exp(m*log(dq)), where dq is a dual quaternion and m is a real number.
For the moment, this operation is defined only for unit dual quaternions.
'''
if not isinstance(num, (int, float)):
raise ValueError('The second parameter must be a double.')
return (self.log() * num).exp()
def norm(self):
'''returns the dual scalar corresponding to the norm of dq.'''
dq_a = self.conj()*self
# Taking the square root (compatible with the definition of quaternion norm)
# This is performed based on the Taylor expansion.
if dq_a.p() == 0:
return DQ([0])
else:
dq_a.q[0] = np.sqrt(dq_a.q[0])
dq_a.q[4] = dq_a.q[4] / (2*dq_a.q[0])
for i in range(8):
if np.abs(dq_a.q[i]) < DQ.__threshold:
dq_a.q[i] = 0
return dq_a
def normalize(self):
'''normalizes the dual quaternion.'''
return self * self.norm().inv()
def cross(self, other):
'''returns the cross product between two dual quaternions.'''
return (self*other - other*self) * .5
def dot(self, other):
'''returns the dot product between two dual quaternions.'''
return -(self*other + other*self) * .5
def pinv(self):
'''returns the inverse of dq under the decompositional.'''
conj = self.conj()
tmp = self.t() * conj.t()
return tmp.conj() * conj
def dec(self, other):
'''returns decompositional multiplication between dual quaternions.'''
return self.t() * other.t() * self.p() * other.p()
# Comparison and other methods
def __pos__(self):
return self
def __neg__(self):
return DQ(np.r_[self.q * -1])
def __invert__(self):
'''behaves as conjugate'''
return self.conj()
def is_unit(self):
'''returns True if dq is a unit norm dual quaternion, False otherwise.'''
if self.norm() == 1:
return True
else:
return False
def __eq__(self, other):
if other.__class__ != DQ:
other = DQ(other)
return np.all(np.abs(self.q - other.q) < DQ.__threshold)
def __ne__(self, other):
return not DQ.__eq__(self, other)
def __str__(self):
und = ['', 'i', 'j', 'k']
sig = ['']*4
str1 = []
for i in range(4):
if self.q[i] >= 0:
sig[i] = '+'
if self.q[i] != 0:
str1.append('%s%s%s' % (sig[i], self.q[i], und[i]))
str1 = ' '.join(str1)
if not str1:
str1 = '0'
if np.any(self.q[4:] != 0):
sig = ['']*4
str2 = ['E*(']
for i in range(4):
if self.q[4+i] >= 0 and i > 0:
sig[i] = '+'
if self.q[4+i] != 0:
str2.append('%s%s%s' % (sig[i], self.q[4+i], und[i]))
str2.append(')'); str2 = ' '.join(str2)
if str1[0] == '+':
str1 = str1[1:]
if str2[0] == '+':
str2 = str2[1:]
if str1 != '0':
str1 = '( ' + str1 + ' ) + ' + str2
else:
str1 = str2
return str1
def __repr__(self):
return str(self)
def round(self):
'''Round absolute values below the threshold to zero.'''
for i in range(8):
if np.abs(self.q[i]) < DQ.__threshold:
self.q[i] = 0.;
return DQ(self.q)
def plot(self, scale = 1.):
'''
DQ.plot(dq, OPTIONS) plots the dual quaternion dq.
Ex.: plot(dq, scale=5) will plot dq with the axis scaled by a factor of 5
'''
if self.is_unit():
# create unit vectors and rotate them by the quaternion part of dq.
t1 = DQ.e() * [0, scale*.5, 0, 0] + 1
t2 = DQ.e() * [0, 0, scale*.5, 0] + 1
t3 = DQ.e() * [0, 0, 0, scale*.5] + 1
# vector rotation
xvec = self.p() * t1 * self.p().conj()
yvec = self.p() * t2 * self.p().conj()
zvec = self.p() * t3 * self.p().conj()
# collecting points
xx, xy, xz = np.array([0, xvec.translation().q[1]]) + self.translation().q[1], \
np.array([0, xvec.translation().q[2]]) + self.translation().q[2], \
np.array([0, xvec.translation().q[3]]) + self.translation().q[3]
yx, yy, yz = np.array([0, yvec.translation().q[1]]) + self.translation().q[1], \
np.array([0, yvec.translation().q[2]]) + self.translation().q[2], \
np.array([0, yvec.translation().q[3]]) + self.translation().q[3]
zx, zy, zz = np.array([0, zvec.translation().q[1]]) + self.translation().q[1], \
np.array([0, zvec.translation().q[2]]) + self.translation().q[2], \
np.array([0, zvec.translation().q[3]]) + self.translation().q[3]
fig = plt.figure()
ax = fig.gca(projection='3d')
ax.plot(xx, xy, xz, 'r')
ax.plot(yx, yy, yz, 'g')
ax.plot(zx, zy, zz, 'b')
ax.relim()
#ax.set_aspect('equal')
#ax.set_aspect(1./ax.get_data_ratio())
ax.autoscale_view(True,True,True)
ax.set_xlabel('x (m)')
ax.set_ylabel('y (m)')
ax.set_zlabel('z (m)')
plt.show(block=False)
else:
warnings.warn('Only unit dual quaternions can be plotted!')