Skip to content

Commit 9d955dc

Browse files
committed
deprecate: UnitQuaternion.interp1's shortest parameter is a no-op
Every UnitQuaternion has a non-negative scalar part by construction (qunit()), and interp1 always interpolates from the identity quaternion, whose dot product with any unit quaternion is just that quaternion's own scalar part. The negative-dot-product condition `shortest` checks for to detect "the long way round" can therefore never occur here -- confirmed empirically (2000 random compositions, plus explicit UnitQuaternion([-1,0,0,0]) construction, all canonicalize to non-negative scalar part). Drops the dead branch, warns on shortest=True, documents why in a docstring note, and pins the no-op behavior (identical results for True/False, warns only when True) with a regression test.
1 parent 8b83c1f commit 9d955dc

2 files changed

Lines changed: 50 additions & 9 deletions

File tree

spatialmath/quaternion.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# pylint: disable=invalid-name
1717
from __future__ import annotations
1818
import math
19+
import warnings
1920
import numpy as np
2021
from typing import Any
2122
import spatialmath.base as smb
@@ -1978,7 +1979,7 @@ def interp1(self, s: float = 0, shortest: Optional[bool] = False) -> UnitQuatern
19781979
"""
19791980
Interpolate a unit quaternion
19801981
1981-
:param shortest: Take the shortest path along the great circle
1982+
:param shortest: deprecated, has no effect
19821983
:param s: interpolation coefficient, range 0 to 1, or number of steps
19831984
:type s: array_like or int
19841985
:return: interpolated unit quaternion
@@ -2009,10 +2010,32 @@ def interp1(self, s: float = 0, shortest: Optional[bool] = False) -> UnitQuatern
20092010
20102011
.. note:: values of ``s`` are silently clipped to the range [0, 1]
20112012
2013+
.. note:: ``shortest`` can never change the result here. Every
2014+
``UnitQuaternion`` has a non-negative scalar part by construction
2015+
(see :func:`~spatialmath.base.quaternions.qunit`), and this method
2016+
always interpolates *from* the identity quaternion ``[1,0,0,0]``
2017+
-- whose dot product with any unit quaternion is just that
2018+
quaternion's own (always non-negative) scalar part. A negative
2019+
dot product is what ``shortest`` checks for to detect "the long
2020+
way round", and that condition can never occur here, unlike
2021+
:meth:`interp`, where it can.
2022+
2023+
.. deprecated:: 1.1.17
2024+
``shortest`` has no effect and will be removed in a future
2025+
release.
2026+
20122027
:seealso: :func:`~spatialmath.base.quaternions.qslerp`
20132028
"""
20142029
# TODO allow self to have len() > 1
20152030

2031+
if shortest:
2032+
warnings.warn(
2033+
"shortest has no effect on interp1 and will be removed in a "
2034+
"future release: interpolation from the identity quaternion "
2035+
"is always the shortest path",
2036+
DeprecationWarning,
2037+
)
2038+
20162039
if isinstance(s, int) and s > 1:
20172040
s = np.linspace(0, 1, s)
20182041
else:
@@ -2022,14 +2045,6 @@ def interp1(self, s: float = 0, shortest: Optional[bool] = False) -> UnitQuatern
20222045
q = self.vec
20232046
dot = q[0] # s
20242047

2025-
# If the dot product is negative, the quaternions
2026-
# have opposite handed-ness and slerp won't take
2027-
# the shorter path. Fix by reversing one quaternion.
2028-
if shortest:
2029-
if dot < 0:
2030-
q = -q
2031-
dot = -dot
2032-
20332048
# shouldn't be needed by handle numerical errors: -eps, 1+eps cases
20342049
dot = np.clip(dot, -1, 1) # Clip within domain of acos()
20352050

tests/test_quaternion.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from math import pi
33
import numpy.testing as nt
44
import unittest
5+
import warnings
56

67
from spatialmath import *
78
from spatialmath.base import *
@@ -701,6 +702,31 @@ def test_interp(self):
701702
# qcompare( qq(6), UnitQuaternion.Rx(pi) )
702703
# TODO interp
703704

705+
def test_interp1_shortest_deprecated(self):
706+
# shortest never had an effect on interp1: every UnitQuaternion has
707+
# a non-negative scalar part by construction (qunit()), and interp1
708+
# always interpolates from the identity quaternion, whose dot
709+
# product with any unit quaternion is just that quaternion's own
710+
# (always non-negative) scalar part. So the "long way round" branch
711+
# shortest exists to avoid is unreachable. Pin that True/False give
712+
# identical results, that it warns, and that leaving it at its
713+
# default doesn't.
714+
q = UnitQuaternion.Rx(4.5) # would be "the long way" if reachable
715+
716+
for s in (0, 0.25, 0.5, 0.75, 1):
717+
qcompare(q.interp1(s, shortest=False), q.interp1(s, shortest=True))
718+
719+
for a, b in zip(q.interp1(11, shortest=False), q.interp1(11, shortest=True)):
720+
qcompare(a, b)
721+
722+
with self.assertWarns(DeprecationWarning):
723+
q.interp1(0.5, shortest=True)
724+
725+
with warnings.catch_warnings():
726+
warnings.simplefilter("error")
727+
q.interp1(0.5) # default shortest=False must not warn
728+
q.interp1(0.5, shortest=False)
729+
704730
def test_increment(self):
705731
q = UnitQuaternion()
706732

0 commit comments

Comments
 (0)