test_rotation.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945
  1. from __future__ import division, print_function, absolute_import
  2. import pytest
  3. import numpy as np
  4. from numpy.testing import assert_equal, assert_array_almost_equal
  5. from numpy.testing import assert_allclose
  6. from scipy.spatial.transform import Rotation, Slerp
  7. from scipy.stats import special_ortho_group
  8. from itertools import permutations
  9. def test_generic_quat_matrix():
  10. x = np.array([[3, 4, 0, 0], [5, 12, 0, 0]])
  11. r = Rotation.from_quat(x)
  12. expected_quat = x / np.array([[5], [13]])
  13. assert_array_almost_equal(r.as_quat(), expected_quat)
  14. def test_from_single_1d_quaternion():
  15. x = np.array([3, 4, 0, 0])
  16. r = Rotation.from_quat(x)
  17. expected_quat = x / 5
  18. assert_array_almost_equal(r.as_quat(), expected_quat)
  19. def test_from_single_2d_quaternion():
  20. x = np.array([[3, 4, 0, 0]])
  21. r = Rotation.from_quat(x)
  22. expected_quat = x / 5
  23. assert_array_almost_equal(r.as_quat(), expected_quat)
  24. def test_from_square_quat_matrix():
  25. # Ensure proper norm array broadcasting
  26. x = np.array([
  27. [3, 0, 0, 4],
  28. [5, 0, 12, 0],
  29. [0, 0, 0, 1],
  30. [0, 0, 0, -1]
  31. ])
  32. r = Rotation.from_quat(x)
  33. expected_quat = x / np.array([[5], [13], [1], [1]])
  34. assert_array_almost_equal(r.as_quat(), expected_quat)
  35. def test_malformed_1d_from_quat():
  36. with pytest.raises(ValueError):
  37. Rotation.from_quat(np.array([1, 2, 3]))
  38. def test_malformed_2d_from_quat():
  39. with pytest.raises(ValueError):
  40. Rotation.from_quat(np.array([
  41. [1, 2, 3, 4, 5],
  42. [4, 5, 6, 7, 8]
  43. ]))
  44. def test_zero_norms_from_quat():
  45. x = np.array([
  46. [3, 4, 0, 0],
  47. [0, 0, 0, 0],
  48. [5, 0, 12, 0]
  49. ])
  50. with pytest.raises(ValueError):
  51. Rotation.from_quat(x)
  52. def test_as_dcm_single_1d_quaternion():
  53. quat = [0, 0, 0, 1]
  54. mat = Rotation.from_quat(quat).as_dcm()
  55. # mat.shape == (3,3) due to 1d input
  56. assert_array_almost_equal(mat, np.eye(3))
  57. def test_as_dcm_single_2d_quaternion():
  58. quat = [[0, 0, 1, 1]]
  59. mat = Rotation.from_quat(quat).as_dcm()
  60. assert_equal(mat.shape, (1, 3, 3))
  61. expected_mat = np.array([
  62. [0, -1, 0],
  63. [1, 0, 0],
  64. [0, 0, 1]
  65. ])
  66. assert_array_almost_equal(mat[0], expected_mat)
  67. def test_as_dcm_from_square_input():
  68. quats = [
  69. [0, 0, 1, 1],
  70. [0, 1, 0, 1],
  71. [0, 0, 0, 1],
  72. [0, 0, 0, -1]
  73. ]
  74. mat = Rotation.from_quat(quats).as_dcm()
  75. assert_equal(mat.shape, (4, 3, 3))
  76. expected0 = np.array([
  77. [0, -1, 0],
  78. [1, 0, 0],
  79. [0, 0, 1]
  80. ])
  81. assert_array_almost_equal(mat[0], expected0)
  82. expected1 = np.array([
  83. [0, 0, 1],
  84. [0, 1, 0],
  85. [-1, 0, 0]
  86. ])
  87. assert_array_almost_equal(mat[1], expected1)
  88. assert_array_almost_equal(mat[2], np.eye(3))
  89. assert_array_almost_equal(mat[3], np.eye(3))
  90. def test_as_dcm_from_generic_input():
  91. quats = [
  92. [0, 0, 1, 1],
  93. [0, 1, 0, 1],
  94. [1, 2, 3, 4]
  95. ]
  96. mat = Rotation.from_quat(quats).as_dcm()
  97. assert_equal(mat.shape, (3, 3, 3))
  98. expected0 = np.array([
  99. [0, -1, 0],
  100. [1, 0, 0],
  101. [0, 0, 1]
  102. ])
  103. assert_array_almost_equal(mat[0], expected0)
  104. expected1 = np.array([
  105. [0, 0, 1],
  106. [0, 1, 0],
  107. [-1, 0, 0]
  108. ])
  109. assert_array_almost_equal(mat[1], expected1)
  110. expected2 = np.array([
  111. [0.4, -2, 2.2],
  112. [2.8, 1, 0.4],
  113. [-1, 2, 2]
  114. ]) / 3
  115. assert_array_almost_equal(mat[2], expected2)
  116. def test_from_single_2d_dcm():
  117. dcm = [
  118. [0, 0, 1],
  119. [1, 0, 0],
  120. [0, 1, 0]
  121. ]
  122. expected_quat = [0.5, 0.5, 0.5, 0.5]
  123. assert_array_almost_equal(
  124. Rotation.from_dcm(dcm).as_quat(),
  125. expected_quat)
  126. def test_from_single_3d_dcm():
  127. dcm = np.array([
  128. [0, 0, 1],
  129. [1, 0, 0],
  130. [0, 1, 0]
  131. ]).reshape((1, 3, 3))
  132. expected_quat = np.array([0.5, 0.5, 0.5, 0.5]).reshape((1, 4))
  133. assert_array_almost_equal(
  134. Rotation.from_dcm(dcm).as_quat(),
  135. expected_quat)
  136. def test_from_dcm_calculation():
  137. expected_quat = np.array([1, 1, 6, 1]) / np.sqrt(39)
  138. dcm = np.array([
  139. [-0.8974359, -0.2564103, 0.3589744],
  140. [0.3589744, -0.8974359, 0.2564103],
  141. [0.2564103, 0.3589744, 0.8974359]
  142. ])
  143. assert_array_almost_equal(
  144. Rotation.from_dcm(dcm).as_quat(),
  145. expected_quat)
  146. assert_array_almost_equal(
  147. Rotation.from_dcm(dcm.reshape((1, 3, 3))).as_quat(),
  148. expected_quat.reshape((1, 4)))
  149. def test_dcm_calculation_pipeline():
  150. dcm = special_ortho_group.rvs(3, size=10, random_state=0)
  151. assert_array_almost_equal(Rotation.from_dcm(dcm).as_dcm(), dcm)
  152. def test_from_dcm_ortho_output():
  153. np.random.seed(0)
  154. dcm = np.random.random((100, 3, 3))
  155. ortho_dcm = Rotation.from_dcm(dcm).as_dcm()
  156. mult_result = np.einsum('...ij,...jk->...ik', ortho_dcm,
  157. ortho_dcm.transpose((0, 2, 1)))
  158. eye3d = np.zeros((100, 3, 3))
  159. for i in range(3):
  160. eye3d[:, i, i] = 1.0
  161. assert_array_almost_equal(mult_result, eye3d)
  162. def test_from_1d_single_rotvec():
  163. rotvec = [1, 0, 0]
  164. expected_quat = np.array([0.4794255, 0, 0, 0.8775826])
  165. result = Rotation.from_rotvec(rotvec)
  166. assert_array_almost_equal(result.as_quat(), expected_quat)
  167. def test_from_2d_single_rotvec():
  168. rotvec = [[1, 0, 0]]
  169. expected_quat = np.array([[0.4794255, 0, 0, 0.8775826]])
  170. result = Rotation.from_rotvec(rotvec)
  171. assert_array_almost_equal(result.as_quat(), expected_quat)
  172. def test_from_generic_rotvec():
  173. rotvec = [
  174. [1, 2, 2],
  175. [1, -1, 0.5],
  176. [0, 0, 0]
  177. ]
  178. expected_quat = np.array([
  179. [0.3324983, 0.6649967, 0.6649967, 0.0707372],
  180. [0.4544258, -0.4544258, 0.2272129, 0.7316889],
  181. [0, 0, 0, 1]
  182. ])
  183. assert_array_almost_equal(
  184. Rotation.from_rotvec(rotvec).as_quat(),
  185. expected_quat)
  186. def test_from_rotvec_small_angle():
  187. rotvec = np.array([
  188. [5e-4 / np.sqrt(3), -5e-4 / np.sqrt(3), 5e-4 / np.sqrt(3)],
  189. [0.2, 0.3, 0.4],
  190. [0, 0, 0]
  191. ])
  192. quat = Rotation.from_rotvec(rotvec).as_quat()
  193. # cos(theta/2) ~~ 1 for small theta
  194. assert_allclose(quat[0, 3], 1)
  195. # sin(theta/2) / theta ~~ 0.5 for small theta
  196. assert_allclose(quat[0, :3], rotvec[0] * 0.5)
  197. assert_allclose(quat[1, 3], 0.9639685)
  198. assert_allclose(
  199. quat[1, :3],
  200. np.array([
  201. 0.09879603932153465,
  202. 0.14819405898230198,
  203. 0.19759207864306931
  204. ]))
  205. assert_equal(quat[2], np.array([0, 0, 0, 1]))
  206. def test_malformed_1d_from_rotvec():
  207. with pytest.raises(ValueError, match='Expected `rot_vec` to have shape'):
  208. Rotation.from_rotvec([1, 2])
  209. def test_malformed_2d_from_rotvec():
  210. with pytest.raises(ValueError, match='Expected `rot_vec` to have shape'):
  211. Rotation.from_rotvec([
  212. [1, 2, 3, 4],
  213. [5, 6, 7, 8]
  214. ])
  215. def test_as_generic_rotvec():
  216. quat = np.array([
  217. [1, 2, -1, 0.5],
  218. [1, -1, 1, 0.0003],
  219. [0, 0, 0, 1]
  220. ])
  221. quat /= np.linalg.norm(quat, axis=1)[:, None]
  222. rotvec = Rotation.from_quat(quat).as_rotvec()
  223. angle = np.linalg.norm(rotvec, axis=1)
  224. assert_allclose(quat[:, 3], np.cos(angle/2))
  225. assert_allclose(np.cross(rotvec, quat[:, :3]), np.zeros((3, 3)))
  226. def test_as_rotvec_single_1d_input():
  227. quat = np.array([1, 2, -3, 2])
  228. expected_rotvec = np.array([0.5772381, 1.1544763, -1.7317144])
  229. actual_rotvec = Rotation.from_quat(quat).as_rotvec()
  230. assert_equal(actual_rotvec.shape, (3,))
  231. assert_allclose(actual_rotvec, expected_rotvec)
  232. def test_as_rotvec_single_2d_input():
  233. quat = np.array([[1, 2, -3, 2]])
  234. expected_rotvec = np.array([[0.5772381, 1.1544763, -1.7317144]])
  235. actual_rotvec = Rotation.from_quat(quat).as_rotvec()
  236. assert_equal(actual_rotvec.shape, (1, 3))
  237. assert_allclose(actual_rotvec, expected_rotvec)
  238. def test_rotvec_calc_pipeline():
  239. # Include small angles
  240. rotvec = np.array([
  241. [0, 0, 0],
  242. [1, -1, 2],
  243. [-3e-4, 3.5e-4, 7.5e-5]
  244. ])
  245. assert_allclose(Rotation.from_rotvec(rotvec).as_rotvec(), rotvec)
  246. def test_from_euler_single_rotation():
  247. quat = Rotation.from_euler('z', 90, degrees=True).as_quat()
  248. expected_quat = np.array([0, 0, 1, 1]) / np.sqrt(2)
  249. assert_allclose(quat, expected_quat)
  250. def test_single_intrinsic_extrinsic_rotation():
  251. extrinsic = Rotation.from_euler('z', 90, degrees=True).as_dcm()
  252. intrinsic = Rotation.from_euler('Z', 90, degrees=True).as_dcm()
  253. assert_allclose(extrinsic, intrinsic)
  254. def test_from_euler_rotation_order():
  255. # Intrinsic rotation is same as extrinsic with order reversed
  256. np.random.seed(0)
  257. a = np.random.randint(low=0, high=180, size=(6, 3))
  258. b = a[:, ::-1]
  259. x = Rotation.from_euler('xyz', a, degrees=True).as_quat()
  260. y = Rotation.from_euler('ZYX', b, degrees=True).as_quat()
  261. assert_allclose(x, y)
  262. def test_from_euler_elementary_extrinsic_rotation():
  263. # Simple test to check if extrinsic rotations are implemented correctly
  264. dcm = Rotation.from_euler('zx', [90, 90], degrees=True).as_dcm()
  265. expected_dcm = np.array([
  266. [0, -1, 0],
  267. [0, 0, -1],
  268. [1, 0, 0]
  269. ])
  270. assert_array_almost_equal(dcm, expected_dcm)
  271. def test_from_euler_intrinsic_rotation_312():
  272. angles = [
  273. [30, 60, 45],
  274. [30, 60, 30],
  275. [45, 30, 60]
  276. ]
  277. dcm = Rotation.from_euler('ZXY', angles, degrees=True).as_dcm()
  278. assert_array_almost_equal(dcm[0], np.array([
  279. [0.3061862, -0.2500000, 0.9185587],
  280. [0.8838835, 0.4330127, -0.1767767],
  281. [-0.3535534, 0.8660254, 0.3535534]
  282. ]))
  283. assert_array_almost_equal(dcm[1], np.array([
  284. [0.5334936, -0.2500000, 0.8080127],
  285. [0.8080127, 0.4330127, -0.3995191],
  286. [-0.2500000, 0.8660254, 0.4330127]
  287. ]))
  288. assert_array_almost_equal(dcm[2], np.array([
  289. [0.0473672, -0.6123725, 0.7891491],
  290. [0.6597396, 0.6123725, 0.4355958],
  291. [-0.7500000, 0.5000000, 0.4330127]
  292. ]))
  293. def test_from_euler_intrinsic_rotation_313():
  294. angles = [
  295. [30, 60, 45],
  296. [30, 60, 30],
  297. [45, 30, 60]
  298. ]
  299. dcm = Rotation.from_euler('ZXZ', angles, degrees=True).as_dcm()
  300. assert_array_almost_equal(dcm[0], np.array([
  301. [0.43559574, -0.78914913, 0.4330127],
  302. [0.65973961, -0.04736717, -0.750000],
  303. [0.61237244, 0.61237244, 0.500000]
  304. ]))
  305. assert_array_almost_equal(dcm[1], np.array([
  306. [0.6250000, -0.64951905, 0.4330127],
  307. [0.64951905, 0.1250000, -0.750000],
  308. [0.4330127, 0.750000, 0.500000]
  309. ]))
  310. assert_array_almost_equal(dcm[2], np.array([
  311. [-0.1767767, -0.91855865, 0.35355339],
  312. [0.88388348, -0.30618622, -0.35355339],
  313. [0.4330127, 0.25000000, 0.8660254]
  314. ]))
  315. def test_from_euler_extrinsic_rotation_312():
  316. angles = [
  317. [30, 60, 45],
  318. [30, 60, 30],
  319. [45, 30, 60]
  320. ]
  321. dcm = Rotation.from_euler('zxy', angles, degrees=True).as_dcm()
  322. assert_array_almost_equal(dcm[0], np.array([
  323. [0.91855865, 0.1767767, 0.35355339],
  324. [0.25000000, 0.4330127, -0.8660254],
  325. [-0.30618622, 0.88388348, 0.35355339]
  326. ]))
  327. assert_array_almost_equal(dcm[1], np.array([
  328. [0.96650635, -0.0580127, 0.2500000],
  329. [0.25000000, 0.4330127, -0.8660254],
  330. [-0.0580127, 0.89951905, 0.4330127]
  331. ]))
  332. assert_array_almost_equal(dcm[2], np.array([
  333. [0.65973961, -0.04736717, 0.7500000],
  334. [0.61237244, 0.61237244, -0.5000000],
  335. [-0.43559574, 0.78914913, 0.4330127]
  336. ]))
  337. def test_from_euler_extrinsic_rotation_313():
  338. angles = [
  339. [30, 60, 45],
  340. [30, 60, 30],
  341. [45, 30, 60]
  342. ]
  343. dcm = Rotation.from_euler('zxz', angles, degrees=True).as_dcm()
  344. assert_array_almost_equal(dcm[0], np.array([
  345. [0.43559574, -0.65973961, 0.61237244],
  346. [0.78914913, -0.04736717, -0.61237244],
  347. [0.4330127, 0.75000000, 0.500000]
  348. ]))
  349. assert_array_almost_equal(dcm[1], np.array([
  350. [0.62500000, -0.64951905, 0.4330127],
  351. [0.64951905, 0.12500000, -0.750000],
  352. [0.4330127, 0.75000000, 0.500000]
  353. ]))
  354. assert_array_almost_equal(dcm[2], np.array([
  355. [-0.1767767, -0.88388348, 0.4330127],
  356. [0.91855865, -0.30618622, -0.250000],
  357. [0.35355339, 0.35355339, 0.8660254]
  358. ]))
  359. def test_as_euler_asymmetric_axes():
  360. np.random.seed(0)
  361. n = 10
  362. angles = np.empty((n, 3))
  363. angles[:, 0] = np.random.uniform(low=-np.pi, high=np.pi, size=(n,))
  364. angles[:, 1] = np.random.uniform(low=-np.pi / 2, high=np.pi / 2, size=(n,))
  365. angles[:, 2] = np.random.uniform(low=-np.pi, high=np.pi, size=(n,))
  366. for seq_tuple in permutations('xyz'):
  367. # Extrinsic rotations
  368. seq = ''.join(seq_tuple)
  369. assert_allclose(angles, Rotation.from_euler(seq, angles).as_euler(seq))
  370. # Intrinsic rotations
  371. seq = seq.upper()
  372. assert_allclose(angles, Rotation.from_euler(seq, angles).as_euler(seq))
  373. def test_as_euler_symmetric_axes():
  374. np.random.seed(0)
  375. n = 10
  376. angles = np.empty((n, 3))
  377. angles[:, 0] = np.random.uniform(low=-np.pi, high=np.pi, size=(n,))
  378. angles[:, 1] = np.random.uniform(low=0, high=np.pi, size=(n,))
  379. angles[:, 2] = np.random.uniform(low=-np.pi, high=np.pi, size=(n,))
  380. for axis1 in ['x', 'y', 'z']:
  381. for axis2 in ['x', 'y', 'z']:
  382. if axis1 == axis2:
  383. continue
  384. # Extrinsic rotations
  385. seq = axis1 + axis2 + axis1
  386. assert_allclose(
  387. angles, Rotation.from_euler(seq, angles).as_euler(seq))
  388. # Intrinsic rotations
  389. seq = seq.upper()
  390. assert_allclose(
  391. angles, Rotation.from_euler(seq, angles).as_euler(seq))
  392. def test_as_euler_degenerate_asymmetric_axes():
  393. # Since we cannot check for angle equality, we check for dcm equality
  394. angles = np.array([
  395. [45, 90, 35],
  396. [35, -90, 20],
  397. [35, 90, 25],
  398. [25, -90, 15]
  399. ])
  400. with pytest.warns(UserWarning, match="Gimbal lock"):
  401. for seq_tuple in permutations('xyz'):
  402. # Extrinsic rotations
  403. seq = ''.join(seq_tuple)
  404. rotation = Rotation.from_euler(seq, angles, degrees=True)
  405. dcm_expected = rotation.as_dcm()
  406. angle_estimates = rotation.as_euler(seq, degrees=True)
  407. dcm_estimated = Rotation.from_euler(
  408. seq, angle_estimates, degrees=True
  409. ).as_dcm()
  410. assert_array_almost_equal(dcm_expected, dcm_estimated)
  411. # Intrinsic rotations
  412. seq = seq.upper()
  413. rotation = Rotation.from_euler(seq, angles, degrees=True)
  414. dcm_expected = rotation.as_dcm()
  415. angle_estimates = rotation.as_euler(seq, degrees=True)
  416. dcm_estimated = Rotation.from_euler(
  417. seq, angle_estimates, degrees=True
  418. ).as_dcm()
  419. assert_array_almost_equal(dcm_expected, dcm_estimated)
  420. def test_as_euler_degenerate_symmetric_axes():
  421. # Since we cannot check for angle equality, we check for dcm equality
  422. angles = np.array([
  423. [15, 0, 60],
  424. [35, 0, 75],
  425. [60, 180, 35],
  426. [15, -180, 25],
  427. ])
  428. with pytest.warns(UserWarning, match="Gimbal lock"):
  429. for axis1 in ['x', 'y', 'z']:
  430. for axis2 in ['x', 'y', 'z']:
  431. if axis1 == axis2:
  432. continue
  433. # Extrinsic rotations
  434. seq = axis1 + axis2 + axis1
  435. rotation = Rotation.from_euler(seq, angles, degrees=True)
  436. dcm_expected = rotation.as_dcm()
  437. angle_estimates = rotation.as_euler(seq, degrees=True)
  438. dcm_estimated = Rotation.from_euler(
  439. seq, angle_estimates, degrees=True
  440. ).as_dcm()
  441. assert_array_almost_equal(dcm_expected, dcm_estimated)
  442. # Intrinsic rotations
  443. seq = seq.upper()
  444. rotation = Rotation.from_euler(seq, angles, degrees=True)
  445. dcm_expected = rotation.as_dcm()
  446. angle_estimates = rotation.as_euler(seq, degrees=True)
  447. dcm_estimated = Rotation.from_euler(
  448. seq, angle_estimates, degrees=True
  449. ).as_dcm()
  450. assert_array_almost_equal(dcm_expected, dcm_estimated)
  451. def test_inv():
  452. np.random.seed(0)
  453. n = 10
  454. p = Rotation.from_quat(np.random.normal(size=(n, 4)))
  455. q = p.inv()
  456. p_dcm = p.as_dcm()
  457. q_dcm = q.as_dcm()
  458. result1 = np.einsum('...ij,...jk->...ik', p_dcm, q_dcm)
  459. result2 = np.einsum('...ij,...jk->...ik', q_dcm, p_dcm)
  460. eye3d = np.empty((n, 3, 3))
  461. eye3d[:] = np.eye(3)
  462. assert_array_almost_equal(result1, eye3d)
  463. assert_array_almost_equal(result2, eye3d)
  464. def test_inv_single_rotation():
  465. np.random.seed(0)
  466. p = Rotation.from_quat(np.random.normal(size=(4,)))
  467. q = p.inv()
  468. p_dcm = p.as_dcm()
  469. q_dcm = q.as_dcm()
  470. res1 = np.dot(p_dcm, q_dcm)
  471. res2 = np.dot(q_dcm, p_dcm)
  472. eye = np.eye(3)
  473. assert_array_almost_equal(res1, eye)
  474. assert_array_almost_equal(res2, eye)
  475. x = Rotation.from_quat(np.random.normal(size=(1, 4)))
  476. y = x.inv()
  477. x_dcm = x.as_dcm()
  478. y_dcm = y.as_dcm()
  479. result1 = np.einsum('...ij,...jk->...ik', x_dcm, y_dcm)
  480. result2 = np.einsum('...ij,...jk->...ik', y_dcm, x_dcm)
  481. eye3d = np.empty((1, 3, 3))
  482. eye3d[:] = np.eye(3)
  483. assert_array_almost_equal(result1, eye3d)
  484. assert_array_almost_equal(result2, eye3d)
  485. def test_apply_single_rotation_single_point():
  486. dcm = np.array([
  487. [0, -1, 0],
  488. [1, 0, 0],
  489. [0, 0, 1]
  490. ])
  491. r_1d = Rotation.from_dcm(dcm)
  492. r_2d = Rotation.from_dcm(np.expand_dims(dcm, axis=0))
  493. v_1d = np.array([1, 2, 3])
  494. v_2d = np.expand_dims(v_1d, axis=0)
  495. v1d_rotated = np.array([-2, 1, 3])
  496. v2d_rotated = np.expand_dims(v1d_rotated, axis=0)
  497. assert_allclose(r_1d.apply(v_1d), v1d_rotated)
  498. assert_allclose(r_1d.apply(v_2d), v2d_rotated)
  499. assert_allclose(r_2d.apply(v_1d), v2d_rotated)
  500. assert_allclose(r_2d.apply(v_2d), v2d_rotated)
  501. v1d_inverse = np.array([2, -1, 3])
  502. v2d_inverse = np.expand_dims(v1d_inverse, axis=0)
  503. assert_allclose(r_1d.apply(v_1d, inverse=True), v1d_inverse)
  504. assert_allclose(r_1d.apply(v_2d, inverse=True), v2d_inverse)
  505. assert_allclose(r_2d.apply(v_1d, inverse=True), v2d_inverse)
  506. assert_allclose(r_2d.apply(v_2d, inverse=True), v2d_inverse)
  507. def test_apply_single_rotation_multiple_points():
  508. dcm = np.array([
  509. [0, -1, 0],
  510. [1, 0, 0],
  511. [0, 0, 1]
  512. ])
  513. r1 = Rotation.from_dcm(dcm)
  514. r2 = Rotation.from_dcm(np.expand_dims(dcm, axis=0))
  515. v = np.array([[1, 2, 3], [4, 5, 6]])
  516. v_rotated = np.array([[-2, 1, 3], [-5, 4, 6]])
  517. assert_allclose(r1.apply(v), v_rotated)
  518. assert_allclose(r2.apply(v), v_rotated)
  519. v_inverse = np.array([[2, -1, 3], [5, -4, 6]])
  520. assert_allclose(r1.apply(v, inverse=True), v_inverse)
  521. assert_allclose(r2.apply(v, inverse=True), v_inverse)
  522. def test_apply_multiple_rotations_single_point():
  523. dcm = np.empty((2, 3, 3))
  524. dcm[0] = np.array([
  525. [0, -1, 0],
  526. [1, 0, 0],
  527. [0, 0, 1]
  528. ])
  529. dcm[1] = np.array([
  530. [1, 0, 0],
  531. [0, 0, -1],
  532. [0, 1, 0]
  533. ])
  534. r = Rotation.from_dcm(dcm)
  535. v1 = np.array([1, 2, 3])
  536. v2 = np.expand_dims(v1, axis=0)
  537. v_rotated = np.array([[-2, 1, 3], [1, -3, 2]])
  538. assert_allclose(r.apply(v1), v_rotated)
  539. assert_allclose(r.apply(v2), v_rotated)
  540. v_inverse = np.array([[2, -1, 3], [1, 3, -2]])
  541. assert_allclose(r.apply(v1, inverse=True), v_inverse)
  542. assert_allclose(r.apply(v2, inverse=True), v_inverse)
  543. def test_apply_multiple_rotations_multiple_points():
  544. dcm = np.empty((2, 3, 3))
  545. dcm[0] = np.array([
  546. [0, -1, 0],
  547. [1, 0, 0],
  548. [0, 0, 1]
  549. ])
  550. dcm[1] = np.array([
  551. [1, 0, 0],
  552. [0, 0, -1],
  553. [0, 1, 0]
  554. ])
  555. r = Rotation.from_dcm(dcm)
  556. v = np.array([[1, 2, 3], [4, 5, 6]])
  557. v_rotated = np.array([[-2, 1, 3], [4, -6, 5]])
  558. assert_allclose(r.apply(v), v_rotated)
  559. v_inverse = np.array([[2, -1, 3], [4, 6, -5]])
  560. assert_allclose(r.apply(v, inverse=True), v_inverse)
  561. def test_getitem():
  562. dcm = np.empty((2, 3, 3))
  563. dcm[0] = np.array([
  564. [0, -1, 0],
  565. [1, 0, 0],
  566. [0, 0, 1]
  567. ])
  568. dcm[1] = np.array([
  569. [1, 0, 0],
  570. [0, 0, -1],
  571. [0, 1, 0]
  572. ])
  573. r = Rotation.from_dcm(dcm)
  574. assert_allclose(r[0].as_dcm(), dcm[0])
  575. assert_allclose(r[1].as_dcm(), dcm[1])
  576. assert_allclose(r[:-1].as_dcm(), np.expand_dims(dcm[0], axis=0))
  577. def test_n_rotations():
  578. dcm = np.empty((2, 3, 3))
  579. dcm[0] = np.array([
  580. [0, -1, 0],
  581. [1, 0, 0],
  582. [0, 0, 1]
  583. ])
  584. dcm[1] = np.array([
  585. [1, 0, 0],
  586. [0, 0, -1],
  587. [0, 1, 0]
  588. ])
  589. r = Rotation.from_dcm(dcm)
  590. assert_equal(len(r), 2)
  591. assert_equal(len(r[0]), 1)
  592. assert_equal(len(r[1]), 1)
  593. assert_equal(len(r[:-1]), 1)
  594. def test_quat_ownership():
  595. # Ensure that users cannot accidentally corrupt object
  596. quat = np.array([
  597. [1, 0, 0, 0],
  598. [0, 1, 0, 0],
  599. [0, 0, 1, 0]
  600. ])
  601. r = Rotation.from_quat(quat, normalized=True)
  602. s = r[0:2]
  603. r._quat[0] = np.array([0, -1, 0, 0])
  604. assert_allclose(s._quat[0], np.array([1, 0, 0, 0]))
  605. def test_match_vectors_no_rotation():
  606. x = np.array([[1, 2, 3], [4, 5, 6]])
  607. y = x.copy()
  608. r, p = Rotation.match_vectors(x, y)
  609. assert_array_almost_equal(r.as_dcm(), np.eye(3))
  610. def test_match_vectors_no_noise():
  611. np.random.seed(0)
  612. c = Rotation.from_quat(np.random.normal(size=4))
  613. b = np.random.normal(size=(5, 3))
  614. a = c.apply(b)
  615. est, cov = Rotation.match_vectors(a, b)
  616. assert_allclose(c.as_quat(), est.as_quat())
  617. def test_match_vectors_noise():
  618. np.random.seed(0)
  619. n_vectors = 100
  620. rot = Rotation.from_euler('xyz', np.random.normal(size=3))
  621. vectors = np.random.normal(size=(n_vectors, 3))
  622. result = rot.apply(vectors)
  623. # The paper adds noise as indepedently distributed angular errors
  624. sigma = np.deg2rad(1)
  625. tolerance = 1.5 * sigma
  626. noise = Rotation.from_rotvec(
  627. np.random.normal(
  628. size=(n_vectors, 3),
  629. scale=sigma
  630. )
  631. )
  632. # Attitude errors must preserve norm. Hence apply individual random
  633. # rotations to each vector.
  634. noisy_result = noise.apply(result)
  635. est, cov = Rotation.match_vectors(noisy_result, vectors)
  636. # Use rotation compositions to find out closeness
  637. error_vector = (rot * est.inv()).as_rotvec()
  638. assert_allclose(error_vector[0], 0, atol=tolerance)
  639. assert_allclose(error_vector[1], 0, atol=tolerance)
  640. assert_allclose(error_vector[2], 0, atol=tolerance)
  641. # Check error bounds using covariance matrix
  642. cov *= sigma
  643. assert_allclose(cov[0, 0], 0, atol=tolerance)
  644. assert_allclose(cov[1, 1], 0, atol=tolerance)
  645. assert_allclose(cov[2, 2], 0, atol=tolerance)
  646. def test_random_rotation_shape():
  647. assert_equal(Rotation.random().as_quat().shape, (4,))
  648. assert_equal(Rotation.random(None).as_quat().shape, (4,))
  649. assert_equal(Rotation.random(1).as_quat().shape, (1, 4))
  650. assert_equal(Rotation.random(5).as_quat().shape, (5, 4))
  651. def test_slerp():
  652. np.random.seed(0)
  653. key_rots = Rotation.from_quat(np.random.uniform(size=(5, 4)))
  654. key_quats = key_rots.as_quat()
  655. key_times = [0, 1, 2, 3, 4]
  656. interpolator = Slerp(key_times, key_rots)
  657. times = [0, 0.5, 0.25, 1, 1.5, 2, 2.75, 3, 3.25, 3.60, 4]
  658. interp_rots = interpolator(times)
  659. interp_quats = interp_rots.as_quat()
  660. # Dot products are affected by sign of quaternions
  661. interp_quats[interp_quats[:, -1] < 0] *= -1
  662. # Checking for quaternion equality, perform same operation
  663. key_quats[key_quats[:, -1] < 0] *= -1
  664. # Equality at keyframes, including both endpoints
  665. assert_allclose(interp_quats[0], key_quats[0])
  666. assert_allclose(interp_quats[3], key_quats[1])
  667. assert_allclose(interp_quats[5], key_quats[2])
  668. assert_allclose(interp_quats[7], key_quats[3])
  669. assert_allclose(interp_quats[10], key_quats[4])
  670. # Constant angular velocity between keyframes. Check by equating
  671. # cos(theta) between quaternion pairs with equal time difference.
  672. cos_theta1 = np.sum(interp_quats[0] * interp_quats[2])
  673. cos_theta2 = np.sum(interp_quats[2] * interp_quats[1])
  674. assert_allclose(cos_theta1, cos_theta2)
  675. cos_theta4 = np.sum(interp_quats[3] * interp_quats[4])
  676. cos_theta5 = np.sum(interp_quats[4] * interp_quats[5])
  677. assert_allclose(cos_theta4, cos_theta5)
  678. # theta1: 0 -> 0.25, theta3 : 0.5 -> 1
  679. # Use double angle formula for double the time difference
  680. cos_theta3 = np.sum(interp_quats[1] * interp_quats[3])
  681. assert_allclose(cos_theta3, 2 * (cos_theta1**2) - 1)
  682. # Miscellaneous checks
  683. assert_equal(len(interp_rots), len(times))
  684. def test_slerp_single_rot():
  685. with pytest.raises(ValueError, match="at least 2 rotations"):
  686. r = Rotation.from_quat([1, 2, 3, 4])
  687. Slerp([1], r)
  688. def test_slerp_time_dim_mismatch():
  689. with pytest.raises(ValueError,
  690. match="times to be specified in a 1 dimensional array"):
  691. np.random.seed(0)
  692. r = Rotation.from_quat(np.random.uniform(size=(2, 4)))
  693. t = np.array([[1],
  694. [2]])
  695. Slerp(t, r)
  696. def test_slerp_num_rotations_mismatch():
  697. with pytest.raises(ValueError, match="number of rotations to be equal to "
  698. "number of timestamps"):
  699. np.random.seed(0)
  700. r = Rotation.from_quat(np.random.uniform(size=(5, 4)))
  701. t = np.arange(7)
  702. Slerp(t, r)
  703. def test_slerp_equal_times():
  704. with pytest.raises(ValueError, match="strictly increasing order"):
  705. np.random.seed(0)
  706. r = Rotation.from_quat(np.random.uniform(size=(5, 4)))
  707. t = [0, 1, 2, 2, 4]
  708. Slerp(t, r)
  709. def test_slerp_decreasing_times():
  710. with pytest.raises(ValueError, match="strictly increasing order"):
  711. np.random.seed(0)
  712. r = Rotation.from_quat(np.random.uniform(size=(5, 4)))
  713. t = [0, 1, 3, 2, 4]
  714. Slerp(t, r)
  715. def test_slerp_call_time_dim_mismatch():
  716. np.random.seed(0)
  717. r = Rotation.from_quat(np.random.uniform(size=(5, 4)))
  718. t = np.arange(5)
  719. s = Slerp(t, r)
  720. with pytest.raises(ValueError,
  721. match="times to be specified in a 1 dimensional array"):
  722. interp_times = np.array([[3.5],
  723. [4.2]])
  724. s(interp_times)
  725. def test_slerp_call_time_out_of_range():
  726. np.random.seed(0)
  727. r = Rotation.from_quat(np.random.uniform(size=(5, 4)))
  728. t = np.arange(5) + 1
  729. s = Slerp(t, r)
  730. with pytest.raises(ValueError, match="times must be within the range"):
  731. s([0, 1, 2])
  732. with pytest.raises(ValueError, match="times must be within the range"):
  733. s([1, 2, 6])