import numpy as np
import matplotlib.pyplot as plt
import quaternionic
from splines.quaternion import Squad, CatmullRom, UnitQuaternion, canonicalized
from helper import angles2quat, animate_rotations, display_animation
# Some helpful utilities for converting between `splines` and `quaternionic`
def uq(q):
return UnitQuaternion.from_unit_xyzw(q.vector.tolist() + [q.w[()],])
def q(uq):
try:
iterator = iter(uq)
except TypeError:
return quaternionic.array(uq.wxyz)
else:
return quaternionic.array([uqi.wxyz for uqi in uq])
class QuaternionicSquad:
def __init__(self, Rs, t):
self.Rs = Rs
self.grid = t
def evaluate(self, t):
try:
iterator = iter(t)
except TypeError:
return uq(quaternionic.squad(self.Rs, self.grid, np.array([t])[0]))
else:
return list(map(uq, quaternionic.squad(self.Rs, self.grid, np.array(t))))
def evaluate(spline, frames=200):
times = np.linspace(
spline.grid[0], spline.grid[-1], frames, endpoint=False)
return spline.evaluate(times)
To show the phenomenon, we need a large angle, followed by multiple small angles with large changes in rotation axis in between.
See https://splines.readthedocs.io/en/0.3.0/euclidean/catmull-rom-properties.html#Wrong-Tangent-Vectors for a similar situation in the context of Euclidean splines.
rotations = [
angles2quat(0, 0, 0),
angles2quat(90, 0, -45),
angles2quat(-45, 45, -90),
angles2quat(135, -35, 90),
angles2quat(134.5, -34, 91),
angles2quat(134.8, -33, 89),
angles2quat(90, 0, 0),
]
rotations_periodic = list(canonicalized(rotations + rotations[:1]))
# centripetal parameterization
angles = np.array([
a.rotation_to(b).angle
for a, b in zip(rotations_periodic, rotations_periodic[1:])])
t = times = np.concatenate([[0], np.cumsum(np.sqrt(angles))])
cr = CatmullRom(rotations, times, endconditions='closed')
sq = Squad(rotations, times)
Rs = q(rotations_periodic)
qs = QuaternionicSquad(Rs, t)
ani = animate_rotations({
'CatmullRom': evaluate(cr),
'quaternionic': evaluate(qs),
'Squad': evaluate(sq),
})
display_animation(ani, default_mode='reflect')
The following function tests whether a spline segment violates an expected property of centripetal splines: A point on a centripetal spline never moves towards its preceding control point and it never moves away from the following control point.
In the Euclidan case, "moving towards" means "reducing the Euclidean distance", but in case of rotations, I guess it should mean "reducing the angle".
def test_segment(spline, idx):
time_slice = np.linspace(times[idx], times[idx + 1], 100)
first = spline.evaluate(time_slice[0])
last = spline.evaluate(time_slice[-1])
from_first = 0
to_last = 999
for time in time_slice:
current = spline.evaluate(time)
new_from_first = first.rotation_to(current).angle
if new_from_first < from_first:
print(
'moving towards beginning:',
np.degrees(from_first - new_from_first),
'degrees')
from_first = new_from_first
new_to_last = current.rotation_to(last).angle
if to_last < new_to_last:
print(
'moving away from end:',
np.degrees(new_to_last - to_last),
'degrees')
to_last = new_to_last
test_segment(qs, 3)
moving away from end: 0.04251790429439041 degrees moving away from end: 0.04219564384246715 degrees moving away from end: 0.04150234992015471 degrees moving away from end: 0.04050053028270019 degrees moving away from end: 0.03924338469480051 degrees moving away from end: 0.03777599396975741 degrees moving away from end: 0.036136470684017465 degrees moving away from end: 0.034357010617156125 degrees moving away from end: 0.03246482028100159 degrees moving away from end: 0.030482916893078214 degrees moving away from end: 0.028430808042368742 degrees moving away from end: 0.026325063606409387 degrees moving away from end: 0.02417979384275729 degrees moving away from end: 0.022007047660861725 degrees moving away from end: 0.01981714358723515 degrees moving away from end: 0.01761894465297093 degrees moving away from end: 0.01542008679177635 degrees moving away from end: 0.013227168699713944 degrees moving away from end: 0.011045909987558639 degrees moving away from end: 0.008881283091400136 degrees moving away from end: 0.006737623603063278 degrees moving away from end: 0.00461872271198087 degrees moving away from end: 0.0025279049125143975 degrees moving away from end: 0.00046809343363793865 degrees moving towards beginning: 0.0007636520470607473 degrees moving towards beginning: 0.0019306541124280098 degrees moving towards beginning: 0.0029964477575349035 degrees moving towards beginning: 0.003958029762242423 degrees moving towards beginning: 0.004812293860346799 degrees moving towards beginning: 0.0055560606189812 degrees moving towards beginning: 0.006186117805882994 degrees moving towards beginning: 0.006699273239999579 degrees moving towards beginning: 0.007092421827595733 degrees moving towards beginning: 0.007362628712670851 degrees moving towards beginning: 0.007507229835820806 degrees moving towards beginning: 0.00752395072131709 degrees moving towards beginning: 0.007411043379121012 degrees moving towards beginning: 0.007167439720403406 degrees moving towards beginning: 0.006792918482982611 degrees moving towards beginning: 0.006288280182117135 degrees moving towards beginning: 0.0056555226445377 degrees moving towards beginning: 0.004898007006134115 degrees moving towards beginning: 0.00402060231539938 degrees moving towards beginning: 0.0030297950811594763 degrees moving towards beginning: 0.0019337501340351347 degrees moving towards beginning: 0.0007423102050714165 degrees
Those values are quite small, but they are too large to be explained by numerical errors.
For comparison, splines.quaternion.CatmullRom
does not show any violations:
test_segment(cr, 3)
OTOH, splines.quaternion.Squad
is horribly broken,
as can already be seen in the animation above.
It shows many very strong violations:
test_segment(sq, 3)
moving away from end: 1.3133170202117306 degrees moving away from end: 1.4501663384341534 degrees moving away from end: 1.4364327933258805 degrees moving away from end: 1.3920809101986682 degrees moving away from end: 1.3385784202944015 degrees moving away from end: 1.2818789386713245 degrees moving away from end: 1.2241445631176497 degrees moving away from end: 1.1663108375329938 degrees moving away from end: 1.1088354688100244 degrees moving away from end: 1.0519637147394127 degrees moving away from end: 0.9958365510396983 degrees moving away from end: 0.9405396667355803 degrees moving away from end: 0.8861275727738371 degrees moving away from end: 0.8326362841932913 degrees moving away from end: 0.7800903692482025 degrees moving away from end: 0.728507052923939 degrees moving away from end: 0.6778987017172159 degrees moving away from end: 0.6282743802254745 degrees moving away from end: 0.5796408556001247 degrees moving away from end: 0.5320032628178517 degrees moving away from end: 0.4853655556069825 degrees moving away from end: 0.43973081845877315 degrees moving away from end: 0.3951014865611879 degrees moving away from end: 0.35147950344484746 degrees moving away from end: 0.3088664357147681 degrees moving away from end: 0.2672635577180594 degrees moving away from end: 0.2266719148179367 degrees moving away from end: 0.18709237124555742 degrees moving away from end: 0.14852564667158127 degrees moving away from end: 0.11097234444339876 degrees moving away from end: 0.07443297358383837 degrees moving away from end: 0.03890796608502238 degrees moving away from end: 0.004397690605702296 degrees moving towards beginning: 0.028019330530445412 degrees moving towards beginning: 0.06049816362141216 degrees moving towards beginning: 0.09195798264786814 degrees moving towards beginning: 0.12239866204664794 degrees moving towards beginning: 0.15182006421771654 degrees moving towards beginning: 0.1802220380651948 degrees moving towards beginning: 0.20760441733433957 degrees moving towards beginning: 0.23396701870835404 degrees moving towards beginning: 0.25930963962786285 degrees moving towards beginning: 0.2836320557827745 degrees moving towards beginning: 0.3069340182224039 degrees moving towards beginning: 0.32921525001634794 degrees moving towards beginning: 0.3504754423824877 degrees moving towards beginning: 0.37071425018955056 degrees moving towards beginning: 0.3899312867072679 degrees moving towards beginning: 0.4081261174752765 degrees moving towards beginning: 0.42529825309333263 degrees moving towards beginning: 0.4414471407437852 degrees moving towards beginning: 0.45657215416213864 degrees moving towards beginning: 0.4706725817401467 degrees moving towards beginning: 0.48374761234981317 degrees moving towards beginning: 0.495796318389566 degrees moving towards beginning: 0.5068176354138286 degrees moving towards beginning: 0.5168103375423453 degrees moving towards beginning: 0.5257730076458076 degrees moving towards beginning: 0.533704000993826 degrees moving towards beginning: 0.5406014007211395 degrees moving towards beginning: 0.5464629629478424 degrees moving towards beginning: 0.5512860487534345 degrees moving towards beginning: 0.5550675393431881 degrees moving towards beginning: 0.5578037295430148 degrees moving towards beginning: 0.5594901931766214 degrees moving towards beginning: 0.5601216116368192 degrees moving towards beginning: 0.5596915539060827 degrees moving towards beginning: 0.5581921919527674 degrees moving towards beginning: 0.5556139293111734 degrees moving towards beginning: 0.5519449119035659 degrees moving towards beginning: 0.5471703774961518 degrees moving towards beginning: 0.5412717816494864 degrees moving towards beginning: 0.534225610698602 degrees moving towards beginning: 0.5260017514166814 degrees moving towards beginning: 0.5165612254885563 degrees moving towards beginning: 0.5058530034042313 degrees moving towards beginning: 0.4938094696808236 degrees moving towards beginning: 0.48033989364389035 degrees moving towards beginning: 0.4653209315825912 degrees moving towards beginning: 0.44858270589285426 degrees moving towards beginning: 0.42988835697873884 degrees moving towards beginning: 0.4089042562503983 degrees moving towards beginning: 0.3851578830611524 degrees moving towards beginning: 0.35798272812598414 degrees moving towards beginning: 0.3264604452895887 degrees moving towards beginning: 0.2894046202010862 degrees moving towards beginning: 0.24551414872646515 degrees moving towards beginning: 0.19395987832486242 degrees moving towards beginning: 0.13566607450168003 degrees moving towards beginning: 0.07482778712103061 degrees moving towards beginning: 0.018752213463824944 degrees
It might have to do with the given rotations only being a few degrees apart, but the corresponding quadrangle points being 80 degrees apart.
[[np.degrees(q1.rotation_to(q2).angle) for q1, q2 in zip(s, s[1:])]
for s in sq.segments]
[[43.43053443955812, 201.425451045242, 68.65042003001439], [42.78042749313507, 218.75738304716862, 43.750742539077635], [40.64644948148499, 192.84727344849514, 0.8108986479117376], [82.14290799234173, 83.40210257650686, 1.028206557680564], [0.7194621500313412, 33.90120498435847, 35.28968878589794], [0.9421711007382751, 112.51301588966085, 30.04526935426295], [29.98951788830831, 158.70143939409056, 47.494212824200446]]