Skip to content

Commit 9cca322

Browse files
committed
Preserve scaled quaternion rotations in logarithms
1 parent 3580f02 commit 9cca322

2 files changed

Lines changed: 16 additions & 13 deletions

File tree

‎spatialmath/quaternion.py‎

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,9 @@ def log(self) -> Quaternion:
381381
382382
\ln \| q \|, \langle \frac{\vec{v}}{\| \vec{v} \|} \cos^{-1} \frac{s}{\| q \|} \rangle
383383
384+
The implementation uses the equivalent :math:`\operatorname{atan2}(\|\vec{v}\|, s)`
385+
to avoid the loss of precision in ``acos`` for small rotation angles.
386+
384387
For a ``UnitQuaternion`` the logarithm is a pure quaternion whose vector
385388
part :math:`\vec{v}` and :math:`\vec{v}/2` is a Euler vector: parallel
386389
to the axis of rotation and whose norm is the magnitude of rotation.
@@ -403,21 +406,17 @@ def log(self) -> Quaternion:
403406
norm = self.norm()
404407
s = np.log(norm)
405408
if len(self) == 1:
406-
if smb.iszerovec(self._A[1:4]):
409+
n = math.hypot(*self._A[1:4])
410+
if n == 0:
407411
v = np.zeros((3,))
408412
else:
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(
411-
self._A[1:4]
412-
)
413+
v = math.atan2(n, self._A[0]) * self._A[1:4] / n
413414
return Quaternion(s=s, v=v)
414415
else:
415-
v = [
416-
np.zeros((3,))
417-
if smb.iszerovec(A[1:4])
418-
else math.atan2(smb.norm(A[1:4]), A[0]) * smb.unitvec(A[1:4])
419-
for A in self._A
420-
]
416+
v = []
417+
for A in self._A:
418+
n = math.hypot(*A[1:4])
419+
v.append(np.zeros((3,)) if n == 0 else math.atan2(n, A[0]) * A[1:4] / n)
421420
return Quaternion(s=s, v=np.array(v))
422421

423422
def exp(self, tol: float = 20) -> Quaternion:

‎tests/test_quaternion.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -946,7 +946,7 @@ def test_log(self):
946946
def test_log_small_rotation(self):
947947
axis = np.array([1.0, -2.0, 3.0]) / math.sqrt(14)
948948
for angle in (1e-12, -1e-9, 1e-7, 1e-4, 0.3):
949-
for scale in (1.0, 3.0):
949+
for scale in (1.0, 3.0, 1e-6):
950950
with self.subTest(angle=angle, scale=scale):
951951
values = scale * np.r_[cos(angle / 2), sin(angle / 2) * axis]
952952
q = UnitQuaternion(values) if scale == 1 else Quaternion(values)
@@ -964,7 +964,11 @@ def test_log_small_rotation_sequence(self):
964964
(np.cos(angles / 2), np.sin(angles[:, None] / 2) * axes)
965965
)
966966
expected = angles[:, None] / 2 * axes
967-
for scales in (np.ones(4), np.array([2.0, 3.0, 4.0, 5.0])):
967+
for scales in (
968+
np.ones(4),
969+
np.array([2.0, 3.0, 4.0, 5.0]),
970+
1e-6 * np.ones(4),
971+
):
968972
with self.subTest(scales=scales):
969973
scaled = scales[:, None] * values
970974
q = (

0 commit comments

Comments
 (0)