You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Fix slerp at coincident/antipodal endpoints, and SO2/SE2.interp1() (#193)
Two crashes in the interpolation methods, found sweeping the whole slerp
surface (angle between the endpoints from 0 to pi across both singular
ends, `s` over [0,1], `shortest` either way).
### slerp singularities
The slerp weights `sin((1-s)t)/sin(t)` and `sin(s.t)/sin(t)` are
singular wherever `sin(t)` vanishes: at `t = 0` (coincident endpoints)
and at `t = pi` (antipodal endpoints, which are the same rotation under
the double cover). `qslerp()` guards only `t = 0`;
`UnitQuaternion.interp()` and `.interp1()` re-derive the weights inline
and guard neither.
```python
q = UnitQuaternion.Rx(0.3)
q.interp(q, 0.5) # ZeroDivisionError
UnitQuaternion().interp1(0.5) # ZeroDivisionError
UnitQuaternion.Rx(pi).interp(UnitQuaternion.Rx(-pi), 0.5, shortest=True) # ZeroDivisionError
UnitQuaternion.Rx(pi).interp(UnitQuaternion.Rx(-pi), 5) # TypeError
qslerp(q.vec, -q.vec, 0.5) # [0 0 0 0], not a unit quaternion
```
`acos(dotprod)` loses the small angle to rounding at both ends, so
`sin(acos(dotprod))` is a poor denominator. This takes `sin(t)` directly
as the length of the component of `q1` orthogonal to `q0`, which keeps
full relative precision, and gets `t` from `atan2`. The only degenerate
case left is `sin(t) == 0`, where the endpoints are the same rotation
and so is every interpolate. The two `UnitQuaternion` methods now call
`qslerp()`, which their own `:seealso:` already pointed at, so the
formula lives in one place.
Checked against a 50-digit `q0 exp(s log(q0^-1 q1))` reference —
Lie-group form, so it shares no algebra with the sin-weight formula —
over the full range of `t` and `s`:
- endpoints within 1e-6 of antipodal: worst error 4.4e-5 -> 2.7e-10 rad
- largest deviation from unit norm anywhere: 8.2e6 -> 2.8e-4 (just short
of antipodal, `qslerp` was returning quaternions with a norm in the
millions)
- ordinary angles move by at most 1 ulp, and the patched surface agrees
with `scipy.spatial.transform.Slerp` to 1.4e-15 rad over 10000 random
pairs
Exactly antipodal with `shortest=False` has no unique great circle, so
there is no correct answer there; it now returns a unit quaternion for
the rotation both endpoints share instead of a norm-1e6 vector or `[0 0
0 0]`.
### SO2/SE2.interp1()
The fix for #33 dropped the `start` local but replaced its uses only in
the `N == 3` branch, so `SE2(1, 2, 0.3).interp1(0.5)` has raised
`NameError: name 'start' is not defined` ever since — #33 did report it
for both SE2 and SE3. `interp1()` had no test coverage in either
dimension.
---
4 new test cases, all red on master. The existing `test_slerp` and
`test_interp` values were already correct and pass unchanged. Full suite
green, `black` 23.10.0 clean.
---------
Co-authored-by: Peter Corke <peter.i.corke@gmail.com>
0 commit comments