Skip to content

Commit 0555c01

Browse files
dddd-jhtaughz
authored andcommitted
fix: preserve small rotations in quaternion logarithms
1 parent 64f4d76 commit 0555c01

2 files changed

Lines changed: 54 additions & 3 deletions

File tree

‎spatialmath/quaternion.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -406,16 +406,17 @@ def log(self) -> Quaternion:
406406
if smb.iszerovec(self._A[1:4]):
407407
v = np.zeros((3,))
408408
else:
409-
v = math.acos(np.clip(self._A[0] / norm, -1, 1)) * smb.unitvec(
409+
# atan2 retains small angles when s / norm rounds to one.
410+
v = math.atan2(smb.norm(self._A[1:4]), self._A[0]) * smb.unitvec(
410411
self._A[1:4]
411412
)
412413
return Quaternion(s=s, v=v)
413414
else:
414415
v = [
415416
np.zeros((3,))
416417
if smb.iszerovec(A[1:4])
417-
else math.acos(np.clip(A[0] / n, -1, 1)) * smb.unitvec(A[1:4])
418-
for A, n in zip(self._A, norm)
418+
else math.atan2(smb.norm(A[1:4]), A[0]) * smb.unitvec(A[1:4])
419+
for A in self._A
419420
]
420421
return Quaternion(s=s, v=np.array(v))
421422

‎tests/test_quaternion.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,56 @@ def test_log(self):
943943
self.assertIsInstance(qq, UnitQuaternion)
944944
nt.assert_array_almost_equal(qq.vec, np.r_[1, 0, 0, 0])
945945

946+
def test_log_small_rotation(self):
947+
axis = np.array([1.0, -2.0, 3.0]) / math.sqrt(14)
948+
for angle in (1e-12, -1e-9, 1e-7, 1e-4, 0.3):
949+
for scale in (1.0, 3.0):
950+
with self.subTest(angle=angle, scale=scale):
951+
values = scale * np.r_[cos(angle / 2), sin(angle / 2) * axis]
952+
q = UnitQuaternion(values) if scale == 1 else Quaternion(values)
953+
original = q.vec.copy()
954+
qlog = q.log()
955+
nt.assert_allclose(qlog.v, angle / 2 * axis, rtol=1e-13, atol=0)
956+
self.assertAlmostEqual(qlog.s, math.log(scale))
957+
nt.assert_allclose(qlog.exp().vec, q.vec, rtol=1e-13, atol=0)
958+
nt.assert_array_equal(q.vec, original)
959+
960+
def test_log_small_rotation_sequence(self):
961+
angles = np.array([1e-12, -1e-9, 1e-7, 0.3])
962+
axes = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1], [1, 0, 0]])
963+
values = np.column_stack(
964+
(np.cos(angles / 2), np.sin(angles[:, None] / 2) * axes)
965+
)
966+
expected = angles[:, None] / 2 * axes
967+
for scales in (np.ones(4), np.array([2.0, 3.0, 4.0, 5.0])):
968+
with self.subTest(scales=scales):
969+
scaled = scales[:, None] * values
970+
q = (
971+
UnitQuaternion(scaled)
972+
if np.all(scales == 1)
973+
else Quaternion(list(scaled))
974+
)
975+
original = q.vec.copy()
976+
qlog = q.log()
977+
self.assertEqual(len(qlog), len(q))
978+
nt.assert_allclose(qlog.v, expected, rtol=1e-13, atol=0)
979+
nt.assert_allclose(qlog.s, np.log(scales), atol=1e-15)
980+
for i in range(len(q)):
981+
nt.assert_allclose(qlog[i].exp().vec, scaled[i], rtol=1e-13, atol=0)
982+
nt.assert_allclose(qlog[i].vec, q[i].log().vec, rtol=1e-13, atol=0)
983+
nt.assert_array_equal(q.vec, original)
984+
985+
def test_log_near_negative_real(self):
986+
# Keep the principal quaternion branch near pi, rather than folding
987+
# a negative scalar part onto the small-angle branch.
988+
angle = 1e-7
989+
q = Quaternion([-cos(angle), sin(angle), 0, 0])
990+
expected = np.r_[0, pi - angle, 0, 0]
991+
nt.assert_allclose(q.log().vec, expected, rtol=0, atol=1e-15)
992+
nt.assert_allclose(
993+
Quaternion([q, q]).log().vec, [expected, expected], rtol=0, atol=1e-15
994+
)
995+
946996
def test_concat(self):
947997
u = Quaternion()
948998
uu = Quaternion([u, u, u, u])

0 commit comments

Comments
 (0)