test_dual_quaternions.py
Go to the documentation of this file.
1 import os
2 from unittest import TestCase
3 from dual_quaternions import DualQuaternion
4 import numpy as np
5 from pyquaternion import Quaternion
6 
7 
8 class TestDualQuaternion(TestCase):
9 
10  def setUp(self):
11  self.identity_dq = DualQuaternion.identity()
12  self.random_dq = DualQuaternion.from_quat_pose_array(np.array([1,2,3,4,5,6,7]))
13  self.other_random_dq = DualQuaternion.from_quat_pose_array(np.array([0.2,0.1,0.3,0.07,1.2,0.9,0.2]))
14  self.normalized_dq = self.random_dq.normalized()
15 
16  def test_creation(self):
17  # from dual quaternion array: careful, need to supply a normalized DQ
18  dql = np.array([0.7071067811, 0.7071067811, 0, 0, -3.535533905, 3.535533905, 1.767766952, -1.767766952])
19  dq1 = DualQuaternion.from_dq_array(dql)
20  dq2 = DualQuaternion.from_dq_array(dql)
21  self.assertEqual(dq1, dq2)
22  # from quaternion + translation array
23  dq3 = DualQuaternion.from_quat_pose_array(np.array([1, 2, 3, 4, 5, 6, 7]))
24  dq4 = DualQuaternion.from_quat_pose_array([1, 2, 3, 4, 5, 6, 7])
25  self.assertEqual(dq3, dq4)
26  # from homogeneous transformation matrix
27  T = np.array([[1, 0, 0, 2], [0, 1, 0, 3], [0, 0, 1, 1], [0, 0, 0, 1]])
28  dq7 = DualQuaternion.from_homogeneous_matrix(T)
29  self.assertEqual(dq7.q_r, Quaternion())
30  self.assertEqual(dq7.translation(), [2, 3, 1])
31  try:
32  np.testing.assert_array_almost_equal(dq7.homogeneous_matrix(), T)
33  except AssertionError as e:
34  self.fail(e)
35  # from a point
36  dq8 = DualQuaternion.from_translation_vector([4, 6, 8])
37  self.assertEqual(dq8.translation(), [4, 6, 8])
38 
39  def test_unit(self):
40  q_r_unit = Quaternion(1, 0, 0, 0)
41  q_d_zero = Quaternion(0, 0, 0, 0)
42  unit_dq = DualQuaternion(q_r_unit, q_d_zero)
43  self.assertEqual(self.identity_dq, unit_dq)
44  # unit dual quaternion multiplied with another unit quaternion should yield unit
45  self.assertEqual(self.identity_dq * self.identity_dq, self.identity_dq)
46 
47  def test_add(self):
48  dq1 = DualQuaternion.from_translation_vector([4, 6, 8])
49  dq2 = DualQuaternion.from_translation_vector([1, 2, 3])
50  sum = dq1 + dq2
51  self.assertEqual(sum.q_d, Quaternion(0., 2.5, 4., 5.5))
52 
53  def test_mult(self):
54  # quaternion multiplication. Compare with homogeneous transformation matrices
55  theta1 = np.pi / 180 * 20 # 20 deg
56  T_pure_rot = np.array([[1., 0., 0., 0.],
57  [0., np.cos(theta1), -np.sin(theta1), 0.],
58  [0., np.sin(theta1), np.cos(theta1), 0.],
59  [0., 0., 0., 1.]])
60  dq_pure_rot = DualQuaternion.from_homogeneous_matrix(T_pure_rot)
61  T_pure_trans = np.array([[1., 0., 0., 1.],
62  [0., 1., 0., 2.],
63  [0., 0., 1., 3.],
64  [0., 0., 0., 1.]])
65  dq_pure_trans = DualQuaternion.from_homogeneous_matrix(T_pure_trans)
66 
67  T_double_rot = np.dot(T_pure_rot, T_pure_rot)
68  dq_double_rot = dq_pure_rot * dq_pure_rot
69  try:
70  np.testing.assert_array_almost_equal(T_double_rot, dq_double_rot.homogeneous_matrix())
71  except AssertionError as e:
72  self.fail(e)
73 
74  T_double_trans = np.dot(T_pure_trans, T_pure_trans)
75  dq_double_trans = dq_pure_trans * dq_pure_trans
76  try:
77  np.testing.assert_array_almost_equal(T_double_trans, dq_double_trans.homogeneous_matrix())
78  except AssertionError as e:
79  self.fail(e)
80 
81  # composed: trans and rot
82  T_composed = np.dot(T_pure_rot, T_pure_trans)
83  dq_composed = dq_pure_rot * dq_pure_trans
84  dq_composed = dq_pure_rot * dq_pure_trans
85  try:
86  np.testing.assert_array_almost_equal(T_composed, dq_composed.homogeneous_matrix())
87  except AssertionError as e:
88  self.fail(e)
89 
90  def test_div(self):
91  try:
92  np.testing.assert_array_almost_equal((self.random_dq/self.random_dq).dq_array(),
93  self.identity_dq.dq_array())
94  np.testing.assert_array_almost_equal((self.random_dq/self.identity_dq).dq_array(),
95  self.random_dq.dq_array())
96  except AssertionError as e:
97  self.fail(e)
98 
99  def test_inverse(self):
100  # use known matrix inversion
101  T_1_2 = np.array([[0, 1, 0, 2], [-1, 0, 0, 4], [0, 0, 1, 6], [0, 0, 0, 1]])
102  T_2_1 = np.array([[0, -1, 0, 4], [1, 0, 0, -2], [0, 0, 1, -6], [0, 0, 0, 1]])
103  dq_1_2 = DualQuaternion.from_homogeneous_matrix(T_1_2)
104  dq_2_1 = DualQuaternion.from_homogeneous_matrix(T_2_1)
105 
106  try:
107  np.testing.assert_array_almost_equal(dq_2_1.homogeneous_matrix(), dq_1_2.inverse().homogeneous_matrix())
108  except AssertionError as e:
109  self.fail(e)
110 
111  def test_equal(self):
112  self.assertEqual(self.identity_dq, DualQuaternion.identity())
113  self.assertEqual(self.identity_dq, DualQuaternion(-Quaternion(1, 0, 0, 0), -Quaternion(0, 0, 0, 0)))
114  self.assertFalse(self.identity_dq == DualQuaternion(Quaternion(1, 0, 0, 1), -Quaternion(0, 0, 0, 0)))
115  theta1 = np.pi / 180 * 20 # 20 deg
116  T_pure_rot = np.array([[1., 0., 0., 0.],
117  [0., np.cos(theta1), -np.sin(theta1), 0.],
118  [0., np.sin(theta1), np.cos(theta1), 0.],
119  [0., 0., 0., 1.]])
120  dq_pure_rot = DualQuaternion.from_homogeneous_matrix(T_pure_rot)
121  # manually flip sign on terms
122  dq_pure_rot.q_r = - dq_pure_rot.q_r
123  dq_pure_rot.q_d = - dq_pure_rot.q_d
124  try:
125  np.testing.assert_array_almost_equal(dq_pure_rot.homogeneous_matrix(), T_pure_rot)
126  except AssertionError as e:
127  self.fail(e)
128  dq_pure_rot.q_d = - dq_pure_rot.q_d
129  try:
130  np.testing.assert_array_almost_equal(dq_pure_rot.homogeneous_matrix(), T_pure_rot)
131  except AssertionError as e:
132  self.fail(e)
133  dq_pure_rot.q_r = - dq_pure_rot.q_r
134  try:
135  np.testing.assert_array_almost_equal(dq_pure_rot.homogeneous_matrix(), T_pure_rot)
136  except AssertionError as e:
137  self.fail(e)
138 
140  # test that __str__ and __repr__ are working
141  self.assertTrue(isinstance(repr(self.identity_dq), str))
142  self.assertTrue(isinstance(self.identity_dq.__str__(), str))
143 
145  dq = self.normalized_dq * self.normalized_dq.quaternion_conjugate()
146  # a normalized quaternion multiplied with its quaternion conjugate should yield unit dual quaternion
147  self.assertEqual(dq, DualQuaternion.identity())
148 
149  # test that the conjugate corresponds to the inverse of it's matrix representation
150  matr = self.normalized_dq.homogeneous_matrix()
151  inv = np.linalg.inv(matr)
152  self.assertEqual(DualQuaternion.from_homogeneous_matrix(inv), self.normalized_dq.quaternion_conjugate())
153 
154  # (dq1 @ dq2)* ?= dq2* @ dq1*
155  res1 = (self.random_dq * self.other_random_dq).quaternion_conjugate()
156  res2 = self.other_random_dq.quaternion_conjugate() * self.random_dq.quaternion_conjugate()
157  self.assertEqual(res1, res2)
158 
160  # 1. starting from a homogeneous matrix
161  theta1 = np.pi/2 # 90 deg
162  trans = [10., 5., 0.]
163  H1 = np.array([[1., 0., 0., trans[0]],
164  [0., np.cos(theta1), -np.sin(theta1), trans[1]],
165  [0., np.sin(theta1), np.cos(theta1), trans[2]],
166  [0., 0., 0., 1.]])
167  # check that if we convert to DQ and back to homogeneous matrix, we get the same result
168  double_conv1 = DualQuaternion.from_homogeneous_matrix(H1).homogeneous_matrix()
169  try:
170  np.testing.assert_array_almost_equal(H1, double_conv1)
171  except AssertionError as e:
172  self.fail(e)
173  # check that dual quaternions are also equal
174  dq1 = DualQuaternion.from_homogeneous_matrix(H1)
175  dq_double1 = DualQuaternion.from_homogeneous_matrix(double_conv1)
176  self.assertEqual(dq1, dq_double1)
177 
178  # 2. starting from a DQ
179  dq_trans = DualQuaternion.from_translation_vector([10, 5, 0])
180  dq_rot = DualQuaternion.from_dq_array([np.cos(theta1 / 2), np.sin(theta1 / 2), 0, 0, 0, 0, 0, 0])
181  dq2 = dq_trans * dq_rot
182  # check that this is the same as the previous DQ
183  self.assertEqual(dq2, dq1)
184  # check that if we convert to homogeneous matrix and back, we get the same result
185  double_conv2 = DualQuaternion.from_homogeneous_matrix(dq2.homogeneous_matrix())
186  self.assertEqual(dq2, double_conv2)
187 
189  # dual number conjugate doesn't behave as you would expect given its special definition
190  # (dq1 @ dq2)* ?= dq1* @ dq2* This is a different order than the other conjugates!
191  res1 = (self.random_dq * self.other_random_dq).dual_number_conjugate()
192  res2 = self.random_dq.dual_number_conjugate() * self.other_random_dq.dual_number_conjugate()
193  self.assertEqual(res1, res2)
194 
196  dq = self.normalized_dq * self.normalized_dq.combined_conjugate()
197  # a normalized quaternion multiplied with its combined conjugate should yield unit rotation
198  self.assertAlmostEqual(dq.q_r, Quaternion())
199  # (dq1 @ dq2)* ?= dq2* @ dq1*
200  res1 = (self.random_dq * self.other_random_dq).combined_conjugate()
201  res2 = self.other_random_dq.combined_conjugate() * self.random_dq.combined_conjugate()
202  self.assertEqual(res1, res2)
203 
204  def test_normalize(self):
205  self.assertTrue(self.identity_dq.is_normalized())
206  self.assertEqual(self.identity_dq.normalized(), self.identity_dq)
207  unnormalized_dq = DualQuaternion.from_quat_pose_array([1, 2, 3, 4, 5, 6, 7])
208  unnormalized_dq.normalize() # now normalized!
209  self.assertTrue(unnormalized_dq.is_normalized())
210 
211  def test_transform(self):
212  # transform a point from one frame (f2) to another (f1)
213  point_f2 = [1, 1, 0]
214  self.assertEqual(self.identity_dq.transform_point(point_f2), point_f2)
215 
216  # test that quaternion transform and matrix transform yield the same result
217  T_f1_f2 = np.array([[1, 0, 0, 2],
218  [0, 0.54028748, -0.8414805, 3],
219  [0, 0.8414805, 0.54028748, 1],
220  [0, 0, 0, 1]])
221  dq_f1_f2 = DualQuaternion.from_homogeneous_matrix(T_f1_f2)
222 
223  # point is in f2, transformation will express it in f1
224  point_f1_matrix = np.dot(T_f1_f2, np.expand_dims(np.array(point_f2 + [1]), 1))
225  point_f1_dq = np.array(dq_f1_f2.transform_point(point_f2))
226  try:
227  np.testing.assert_array_almost_equal(point_f1_matrix[:3].T.flatten(), point_f1_dq.flatten(), decimal=3)
228  except AssertionError as e:
229  self.fail(e)
230 
231  def test_screw(self):
232  # test unit
233  l, m, theta, d = self.identity_dq.screw()
234  self.assertEqual(d, 0)
235  self.assertEqual(theta, 0)
236 
237  # test pure translation
238  trans = [10, 5, 0]
239  dq_trans = DualQuaternion.from_translation_vector(trans)
240  l, m, theta, d = dq_trans.screw()
241  self.assertAlmostEqual(d, np.linalg.norm(trans), 2)
242  self.assertAlmostEqual(theta, 0)
243 
244  # test pure rotation
245  theta1 = np.pi/2
246  dq_rot = DualQuaternion.from_dq_array([np.cos(theta1 / 2), np.sin(theta1 / 2), 0, 0, 0, 0, 0, 0])
247  l, m, theta, d = dq_rot.screw()
248  self.assertAlmostEqual(theta, theta1)
249 
250  # test simple rotation and translation: rotate in the plane of a coordinate system with the screw axis offset
251  # along +y. Rotate around z axis so that the coordinate system stays in the plane. Translate along z-axis
252  theta2 = np.pi/2
253  dq_rot2 = DualQuaternion.from_dq_array([np.cos(theta2 / 2), 0, 0, np.sin(theta2 / 2), 0, 0, 0, 0])
254  dist_axis = 5.
255  displacement_z = 3.
256  dq_trans = DualQuaternion.from_translation_vector([dist_axis*np.sin(theta2), dist_axis*(1.-np.cos(theta2)),
257  displacement_z])
258  dq_comb = dq_trans * dq_rot2
259  l, m, theta, d = dq_comb.screw()
260  try:
261  # the direction of the axis should align with the z axis of the origin
262  np.testing.assert_array_almost_equal(l, np.array([0, 0, 1]), decimal=3)
263  # m = p x l with p any point on the line
264  np.testing.assert_array_almost_equal(np.cross(np.array([[0, dist_axis, 0]]), l).flatten(), m)
265  except AssertionError as e:
266  self.fail(e)
267  self.assertAlmostEqual(d, displacement_z) # only displacement along z should exist here
268  self.assertAlmostEqual(theta, theta2) # the angles should be the same
269 
270  def test_from_screw(self):
271  # construct an axis along the positive z-axis
272  l = np.array([0, 0, 1])
273  # pick a point on the axis that defines it's location
274  p = np.array([-1, 0, 0])
275  # moment vector
276  m = np.cross(p, l)
277  theta = np.pi/2
278  d = 3.
279  # this corresponds to a rotation around the axis parallel with the origin's z-axis through the point p
280  # the resulting transformation should move the origin to a DQ with elements:
281  desired_dq_rot = DualQuaternion.from_quat_pose_array([np.cos(theta/2), 0, 0, np.sin(theta/2), 0, 0, 0])
282  desired_dq_trans = DualQuaternion.from_translation_vector([-1, 1, d])
283  desired_dq = desired_dq_trans * desired_dq_rot
284  dq = DualQuaternion.from_screw(l, m, theta, d)
285  self.assertEqual(dq, desired_dq)
286 
288  # start with a random valid dual quaternion
289  dq = DualQuaternion.from_quat_pose_array([0.5, 0.3, 0.1, 0.4, 2, 5, -2])
290  lr, mr, thetar, dr = dq.screw()
291  dq_reconstructed = DualQuaternion.from_screw(lr, mr, thetar, dr)
292  self.assertEqual(dq, dq_reconstructed)
293 
294  # start with some screw parameters
295  l1 = np.array([0.4, 0.2, 0.5])
296  l1 /= np.linalg.norm(l1) # make sure l1 is normalized
297  # pick some point away from the origin
298  p1 = np.array([2.3, 0.9, 1.1])
299  m1 = np.cross(p1, l1)
300  d1 = 4.32
301  theta1 = 1.94
302  dq1 = DualQuaternion.from_screw(l1, m1, theta1, d1)
303  l2, m2, theta2, d2 = dq1.screw()
304  try:
305  np.testing.assert_array_almost_equal(l1, l2, decimal=3)
306  np.testing.assert_array_almost_equal(l1, l2, decimal=3)
307  except AssertionError as e:
308  self.fail(e)
309  self.assertAlmostEqual(theta1, theta2)
310  self.assertAlmostEqual(d1, d2)
311 
313  # get the cwd so we can create a couple test files that we'll remove later
314  dir = os.getcwd()
315  self.identity_dq.save(dir + '/identity.json')
316  # load it back in
317  loaded_unit = DualQuaternion.from_file(dir + '/identity.json')
318  self.assertEqual(self.identity_dq, loaded_unit)
319  # clean up
320  os.remove(dir + '/identity.json')
321 
323  self.assertRaises(IOError, DualQuaternion.from_file, 'boguspath')
324 
326  """test Screw Linear Interpolation for diff position, same orientation"""
327  dq1 = DualQuaternion.from_translation_vector([2, 2, 2])
328  dq2 = DualQuaternion.from_translation_vector([3, 4, -2])
329  interpolated1 = DualQuaternion.sclerp(dq1, dq2, 0.5)
330  expected1 = DualQuaternion.from_translation_vector([2.5, 3, 0])
331  self.assertEqual(interpolated1, expected1)
332  interpolated2 = DualQuaternion.sclerp(dq1, dq2, 0.1)
333  expected2 = DualQuaternion.from_translation_vector([2.1, 2.2, 1.6])
334  self.assertEqual(interpolated2, expected2)
335 
337  """test Screw Linear Interpolation for diff orientation, same position"""
338  T_id = DualQuaternion.identity().homogeneous_matrix()
339  T_id[0:2, 0:2] = np.array([[0, -1], [1, 0]]) # rotate 90 around z
340  dq2 = DualQuaternion.from_homogeneous_matrix(T_id)
341  interpolated1 = DualQuaternion.sclerp(self.identity_dq, dq2, 0.5)
342  T_exp = DualQuaternion.identity().homogeneous_matrix()
343  sq22 = np.sqrt(2)/2
344  T_exp[0:2, 0:2] = np.array([[sq22, -sq22], [sq22, sq22]]) # rotate 45 around z
345  expected1 = DualQuaternion.from_homogeneous_matrix(T_exp)
346  self.assertEqual(interpolated1, expected1)
347  interpolated2 = DualQuaternion.sclerp(self.identity_dq, dq2, 0)
348  interpolated3 = DualQuaternion.sclerp(self.identity_dq, dq2, 1)
349  self.assertEqual(interpolated2, self.identity_dq)
350  self.assertEqual(interpolated3, dq2)
351 
352  def test_sclerp_screw(self):
353  """Interpolating with ScLERP should yield same result as interpolating with screw parameters
354  ScLERP is a screw motion interpolation with constant rotation and translation speeds. We can
355  simply interpolate screw parameters theta and d and we should get the same result.
356  """
357  taus = [0., 0.23, 0.6, 1.0]
358  l, m, theta, d = self.normalized_dq.screw()
359  for tau in taus:
360  # interpolate using sclerp
361  interpolated_dq = DualQuaternion.sclerp(self.identity_dq, self.normalized_dq, tau)
362  # interpolate using screw: l and m stay the same, theta and d vary with tau
363  interpolated_dq_screw = DualQuaternion.from_screw(l, m, tau*theta, tau*d)
364  self.assertEqual(interpolated_dq, interpolated_dq_screw)
365 
366  def test_pow(self):
367  expected_result = self.normalized_dq * self.normalized_dq
368  received_result = self.normalized_dq.pow(2)
369  self.assertEqual(received_result, expected_result)
370 
371  expected_result = self.random_dq * self.random_dq
372  received_result = self.random_dq.pow(2)
373  self.assertEqual(received_result, expected_result)


dual_quaternions
Author(s): achille
autogenerated on Mon Aug 17 2020 03:24:44