test_quaternion.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. #!/usr/bin/env python
  2. """
  3. Unit tests for the quaternion library
  4. """
  5. from __future__ import absolute_import, division, print_function
  6. import unittest
  7. import numpy as np
  8. from pymavlink.quaternion import QuaternionBase, Quaternion
  9. from pymavlink.rotmat import Vector3, Matrix3
  10. __author__ = "Thomas Gubler"
  11. __copyright__ = "Copyright (C) 2014 Thomas Gubler"
  12. __license__ = "GNU Lesser General Public License v3"
  13. __email__ = "thomasgubler@gmail.com"
  14. class QuaternionBaseTest(unittest.TestCase):
  15. """
  16. Class to test QuaternionBase
  17. """
  18. def __init__(self, *args, **kwargs):
  19. """Constructor, set up some data that is reused in many tests"""
  20. super(QuaternionBaseTest, self).__init__(*args, **kwargs)
  21. self.quaternions = self._all_quaternions()
  22. def test_constructor(self):
  23. """Test the constructor functionality"""
  24. # Test the identity case
  25. q = [1, 0, 0, 0]
  26. euler = [0, 0, 0]
  27. dcm = np.eye(3)
  28. self._helper_test_constructor(q, euler, dcm)
  29. # test a case with rotations around all euler angles
  30. q = [0.707106781186547, 0, 0.707106781186547, 0]
  31. euler = [np.radians(90), np.radians(90), np.radians(90)]
  32. dcm = [[0, 0, 1],
  33. [0, 1, 0],
  34. [-1, 0, 0]]
  35. # test a case with rotations around all angles (values from matlab)
  36. q = [0.774519052838329, 0.158493649053890, 0.591506350946110,
  37. 0.158493649053890]
  38. euler = [np.radians(60), np.radians(60), np.radians(60)]
  39. dcm = [[0.25, -0.058012701892219, 0.966506350946110],
  40. [0.433012701892219, 0.899519052838329, -0.058012701892219],
  41. [-0.866025403784439, 0.433012701892219, 0.25]]
  42. self._helper_test_constructor(q, euler, dcm)
  43. # test another case (values from matlab)
  44. q = [0.754971823897152, 0.102564313848771, -0.324261369073765,
  45. -0.560671625082406]
  46. euler = [np.radians(34), np.radians(-22), np.radians(-80)]
  47. dcm = [[0.161003786707723, 0.780067269138261, -0.604626195500121],
  48. [-0.913097848445116, 0.350255780704370, 0.208741963313735],
  49. [0.374606593415912, 0.518474631686401, 0.768670252102276]]
  50. self._helper_test_constructor(q, euler, dcm)
  51. def _helper_test_constructor(self, q, euler, dcm):
  52. """
  53. Helper function for constructor test
  54. Calls constructor for the quaternion from q euler and dcm and checks
  55. if the resulting converions are equivalent to the arguments.
  56. The test for the euler angles is weak as the solution is not unique
  57. :param q: quaternion 4x1, [w, x, y, z]
  58. :param euler: [roll, pitch, yaw], needs to be equivalent to q
  59. :param q: dcm 3x3, needs to be equivalent to q
  60. """
  61. # construct q from a QuaternionBase
  62. quaternion_instance = QuaternionBase(q)
  63. q_test = QuaternionBase(quaternion_instance)
  64. np.testing.assert_almost_equal(q_test.q, q)
  65. q_test = QuaternionBase(quaternion_instance)
  66. np.testing.assert_almost_equal(q_test.dcm, dcm)
  67. q_test = QuaternionBase(quaternion_instance)
  68. q_euler = QuaternionBase(q_test.euler)
  69. assert(np.allclose(q_test.euler, euler) or
  70. np.allclose(q_test.q, q_euler.q))
  71. # construct q from a quaternion
  72. q_test = QuaternionBase(q)
  73. np.testing.assert_almost_equal(q_test.q, q)
  74. q_test = QuaternionBase(q)
  75. np.testing.assert_almost_equal(q_test.dcm, dcm)
  76. q_test = QuaternionBase(q)
  77. q_euler = QuaternionBase(q_test.euler)
  78. assert(np.allclose(q_test.euler, euler) or
  79. np.allclose(q_test.q, q_euler.q))
  80. # construct q from a euler angles
  81. q_test = QuaternionBase(euler)
  82. np.testing.assert_almost_equal(q_test.q, q)
  83. q_test = QuaternionBase(euler)
  84. np.testing.assert_almost_equal(q_test.dcm, dcm)
  85. q_test = QuaternionBase(euler)
  86. q_euler = QuaternionBase(q_test.euler)
  87. assert(np.allclose(q_test.euler, euler) or
  88. np.allclose(q_test.q, q_euler.q))
  89. # construct q from dcm
  90. q_test = QuaternionBase(dcm)
  91. np.testing.assert_almost_equal(q_test.q, q)
  92. q_test = QuaternionBase(dcm)
  93. np.testing.assert_almost_equal(q_test.dcm, dcm)
  94. q_test = QuaternionBase(dcm)
  95. q_euler = QuaternionBase(q_test.euler)
  96. assert(np.allclose(q_test.euler, euler) or
  97. np.allclose(q_test.q, q_euler.q))
  98. def test_norm(self):
  99. # """Tests the norm functions"""
  100. qa = [1, 2, 3, 4]
  101. q = QuaternionBase(qa)
  102. n = np.sqrt(np.dot(qa, qa))
  103. qan = qa / n
  104. self.assertAlmostEqual(n, QuaternionBase.norm_array(qa))
  105. np.testing.assert_almost_equal(qan, QuaternionBase.normalize_array(qa))
  106. np.testing.assert_almost_equal(n, q.norm)
  107. q.normalize()
  108. np.testing.assert_almost_equal(qan, q.q)
  109. self.assertAlmostEqual(1, q.norm)
  110. def _all_angles(self, step=np.radians(45)):
  111. """
  112. Creates a list of all euler angles
  113. :param step: stepsixe in radians
  114. :returns: euler angles [[phi, thea, psi], [phi, theta, psi], ...]
  115. """
  116. e = 0.5
  117. r_phi = np.arange(-np.pi + e, np.pi - e, step)
  118. r_theta = np.arange(-np.pi/2 + e, np.pi/2 - e, step)
  119. r_psi = np.arange(-np.pi + e, np.pi - e, step)
  120. return [[phi, theta, psi] for phi in r_phi for theta in r_theta
  121. for psi in r_psi]
  122. def _all_quaternions(self):
  123. """Generate quaternions from all euler angles"""
  124. return [QuaternionBase(e) for e in self._all_angles()]
  125. def test_conversion(self):
  126. """
  127. Tests forward and backward conversions
  128. """
  129. for q in self.quaternions:
  130. # quaternion -> euler -> quaternion
  131. q0 = q
  132. e = QuaternionBase(q.q).euler
  133. q1 = QuaternionBase(e)
  134. assert q0.close(q1)
  135. # quaternion -> dcm -> quaternion
  136. q0 = q
  137. dcm = QuaternionBase(q.q).dcm
  138. q1 = QuaternionBase(dcm)
  139. assert q0.close(q1)
  140. def test_inversed(self):
  141. """Test inverse"""
  142. for q in self.quaternions:
  143. q_inv = q.inversed
  144. q_inv_inv = q_inv.inversed
  145. assert q.close(q_inv_inv)
  146. def test_mul(self):
  147. """Test multiplication"""
  148. for q in self.quaternions:
  149. for p in self.quaternions:
  150. assert q.close(p * p.inversed * q)
  151. r = p * q
  152. r_dcm = np.dot(p.dcm, q.dcm)
  153. np.testing.assert_almost_equal(r_dcm, r.dcm)
  154. def test_div(self):
  155. """Test division"""
  156. for q in self.quaternions:
  157. for p in self.quaternions:
  158. mul = q * p.inversed
  159. div = q / p
  160. assert mul.close(div)
  161. def test_transform(self):
  162. """Test transform"""
  163. for q in self.quaternions:
  164. q_inv = q.inversed
  165. v = np.array([1, 2, 3])
  166. v1 = q.transform(v)
  167. v1_dcm = np.dot(q.dcm, v)
  168. np.testing.assert_almost_equal(v1, v1_dcm)
  169. # test versus slower solution using multiplication
  170. v1_mul = q * QuaternionBase(np.hstack([0, v])) * q.inversed
  171. np.testing.assert_almost_equal(v1, v1_mul[1:4])
  172. v2 = q_inv.transform(v1)
  173. np.testing.assert_almost_equal(v, v2)
  174. class QuaternionTest(QuaternionBaseTest):
  175. """
  176. Class to test Quaternion
  177. """
  178. def __init__(self, *args, **kwargs):
  179. """Constructor, set up some data that is reused in many tests"""
  180. super(QuaternionTest, self).__init__(*args, **kwargs)
  181. self.quaternions = self._all_quaternions()
  182. def _all_quaternions(self):
  183. """Generate quaternions from all euler angles"""
  184. return [Quaternion(e) for e in self._all_angles()]
  185. def test_constructor(self):
  186. """Test the constructor functionality"""
  187. # Test the identity case
  188. q = [1, 0, 0, 0]
  189. euler = [0, 0, 0]
  190. dcm = Matrix3()
  191. self._helper_test_constructor(q, euler, dcm)
  192. # test a case with rotations around all angles (values from matlab)
  193. q = [0.774519052838329, 0.158493649053890, 0.591506350946110,
  194. 0.158493649053890]
  195. euler = [np.radians(60), np.radians(60), np.radians(60)]
  196. dcm = Matrix3(Vector3(0.25, -0.058012701892219, 0.966506350946110),
  197. Vector3(0.433012701892219, 0.899519052838329,
  198. -0.058012701892219),
  199. Vector3(-0.866025403784439, 0.433012701892219, 0.25))
  200. self._helper_test_constructor(q, euler, dcm)
  201. def _helper_test_constructor(self, q, euler, dcm):
  202. """
  203. Helper function for constructor test
  204. Calls constructor for the quaternion from q euler and dcm and checks
  205. if the resulting converions are equivalent to the arguments.
  206. The test for the euler angles is weak as the solution is not unique
  207. :param q: quaternion 4x1, [w, x, y, z]
  208. :param euler: Vector3(roll, pitch, yaw), needs to be equivalent to q
  209. :param q: Matrix3, needs to be equivalent to q
  210. """
  211. # construct q from a Quaternion
  212. quaternion_instance = Quaternion(q)
  213. q_test = Quaternion(quaternion_instance)
  214. np.testing.assert_almost_equal(q_test.q, q)
  215. q_test = Quaternion(quaternion_instance)
  216. assert q_test.dcm.close(dcm)
  217. q_test = Quaternion(quaternion_instance)
  218. q_euler = Quaternion(q_test.euler)
  219. assert(np.allclose(q_test.euler, euler) or
  220. np.allclose(q_test.q, q_euler.q))
  221. # construct q from a QuaternionBase
  222. quaternion_instance = QuaternionBase(q)
  223. q_test = Quaternion(quaternion_instance)
  224. np.testing.assert_almost_equal(q_test.q, q)
  225. q_test = Quaternion(quaternion_instance)
  226. assert q_test.dcm.close(dcm)
  227. q_test = Quaternion(quaternion_instance)
  228. q_euler = Quaternion(q_test.euler)
  229. assert(np.allclose(q_test.euler, euler) or
  230. np.allclose(q_test.q, q_euler.q))
  231. # construct q from a quaternion
  232. q_test = Quaternion(q)
  233. np.testing.assert_almost_equal(q_test.q, q)
  234. q_test = Quaternion(q)
  235. assert q_test.dcm.close(dcm)
  236. q_test = Quaternion(q)
  237. q_euler = Quaternion(q_test.euler)
  238. assert(np.allclose(q_test.euler, euler) or
  239. np.allclose(q_test.q, q_euler.q))
  240. # # construct q from a euler angles
  241. q_test = Quaternion(euler)
  242. np.testing.assert_almost_equal(q_test.q, q)
  243. q_test = Quaternion(euler)
  244. assert q_test.dcm.close(dcm)
  245. q_test = Quaternion(euler)
  246. q_euler = Quaternion(q_test.euler)
  247. assert(np.allclose(q_test.euler, euler) or
  248. np.allclose(q_test.q, q_euler.q))
  249. # # construct q from dcm (Matrix3 instance)
  250. q_test = Quaternion(dcm)
  251. np.testing.assert_almost_equal(q_test.q, q)
  252. q_test = Quaternion(dcm)
  253. assert q_test.dcm.close(dcm)
  254. q_test = Quaternion(dcm)
  255. q_euler = Quaternion(q_test.euler)
  256. assert(np.allclose(q_test.euler, euler) or
  257. np.allclose(q_test.q, q_euler.q))
  258. def test_conversion(self):
  259. """
  260. Tests forward and backward conversions
  261. """
  262. for q in self.quaternions:
  263. # quaternion -> euler -> quaternion
  264. q0 = q
  265. e = Quaternion(q.q).euler
  266. q1 = Quaternion(e)
  267. assert q0.close(q1)
  268. # quaternion -> dcm (Matrix3) -> quaternion
  269. q0 = q
  270. dcm = Quaternion(q.q).dcm
  271. q1 = Quaternion(dcm)
  272. assert q0.close(q1)
  273. def test_transform(self):
  274. """Test transform"""
  275. for q in self.quaternions:
  276. q_inv = q.inversed
  277. v = Vector3(1, 2, 3)
  278. v1 = q.transform(v)
  279. v1_dcm = q.dcm * v
  280. assert v1.close(v1_dcm)
  281. v2 = q_inv.transform(v1)
  282. assert v.close(v2)
  283. def test_mul(self):
  284. """Test multiplication"""
  285. for q in self.quaternions:
  286. for p in self.quaternions:
  287. assert q.close(p * p.inversed * q)
  288. r = p * q
  289. r_dcm = p.dcm * q.dcm
  290. assert r_dcm.close(r.dcm)
  291. if __name__ == '__main__':
  292. unittest.main()