Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

from __future__ import division, print_function, absolute_import 

 

__all__ = ['geometric_slerp'] 

 

import warnings 

 

import numpy as np 

from scipy.spatial.distance import euclidean 

 

 

def _geometric_slerp(start, end, t): 

# create an orthogonal basis using QR decomposition 

basis = np.vstack([start, end]) 

Q, R = np.linalg.qr(basis.T) 

signs = 2 * (np.diag(R) >= 0) - 1 

Q = Q.T * signs.T[:, np.newaxis] 

R = R.T * signs.T[:, np.newaxis] 

 

# calculate the angle between `start` and `end` 

c = np.dot(start, end) 

s = np.linalg.det(R) 

omega = np.arctan2(s, c) 

 

# interpolate 

start, end = Q 

s = np.sin(t * omega) 

c = np.cos(t * omega) 

return start * c[:, np.newaxis] + end * s[:, np.newaxis] 

 

 

def geometric_slerp(start, 

end, 

t, 

tol=1e-7): 

""" 

Geometric spherical linear interpolation. 

 

The interpolation occurs along a unit-radius 

great circle arc in arbitrary dimensional space. 

 

Parameters 

---------- 

start : (n_dimensions, ) array-like 

Single n-dimensional input coordinate in a 1-D array-like 

object. `n` must be greater than 1. 

end : (n_dimensions, ) array-like 

Single n-dimensional input coordinate in a 1-D array-like 

object. `n` must be greater than 1. 

t: float or (n_points,) array-like 

A float or array-like of doubles representing interpolation 

parameters, with values required in the inclusive interval 

between 0 and 1. A common approach is to generate the array 

with ``np.linspace(0, 1, n_pts)`` for linearly spaced points. 

Ascending, descending, and scrambled orders are permitted. 

tol: float 

The absolute tolerance for determining if the start and end 

coordinates are antipodes. 

 

Returns 

------- 

result : (t.size, D) 

An array of doubles containing the interpolated 

spherical path and including start and 

end when 0 and 1 t are used. The 

interpolated values should correspond to the 

same sort order provided in the t array. The result 

may be 1-dimensional if ``t`` is a float. 

 

Raises 

------ 

ValueError 

If ``start`` and ``end`` are antipodes, not on the 

unit n-sphere, or for a variety of degenerate conditions. 

 

Notes 

----- 

The implementation is based on the mathematical formula provided in [1]_, 

and the first known presentation of this algorithm, derived from study of 

4-D geometry, is credited to Glenn Davis in a footnote of the original 

quaternion Slerp publication by Ken Shoemake [2]_. 

 

.. versionadded:: 1.5.0 

 

References 

---------- 

.. [1] https://en.wikipedia.org/wiki/Slerp#Geometric_Slerp 

.. [2] Ken Shoemake (1985) Animating rotation with quaternion curves. 

ACM SIGGRAPH Computer Graphics, 19(3): 245-254. 

 

See Also 

-------- 

scipy.spatial.transform.Slerp : 3-D Slerp that works with quaternions 

 

Examples 

-------- 

Interpolate four linearly-spaced values on the circumference of 

a circle spanning 90 degrees: 

 

>>> from scipy.spatial import geometric_slerp 

>>> import matplotlib.pyplot as plt 

>>> fig = plt.figure() 

>>> ax = fig.add_subplot(111) 

>>> start = np.array([1, 0]) 

>>> end = np.array([0, 1]) 

>>> t_vals = np.linspace(0, 1, 4) 

>>> result = geometric_slerp(start, 

... end, 

... t_vals) 

 

The interpolated results should be at 30 degree intervals 

recognizable on the unit circle: 

 

>>> ax.scatter(result[...,0], result[...,1], c='k') 

>>> circle = plt.Circle((0, 0), 1, color='grey') 

>>> ax.add_artist(circle) 

>>> ax.set_aspect('equal') 

>>> plt.show() 

 

Attempting to interpolate between antipodes on a circle is 

ambiguous because there are two possible paths, and on a 

sphere there are infinite possible paths on the geodesic surface. 

Nonetheless, one of the ambiguous paths is returned along 

with a warning: 

 

>>> opposite_pole = np.array([-1, 0]) 

>>> with np.testing.suppress_warnings() as sup: 

... sup.filter(UserWarning) 

... geometric_slerp(start, 

... opposite_pole, 

... t_vals) 

array([[ 1.00000000e+00, 0.00000000e+00], 

[ 5.00000000e-01, 8.66025404e-01], 

[-5.00000000e-01, 8.66025404e-01], 

[-1.00000000e+00, 1.22464680e-16]]) 

 

Extend the original example to a sphere and plot interpolation 

points in 3D: 

 

>>> from mpl_toolkits.mplot3d import proj3d 

>>> fig = plt.figure() 

>>> ax = fig.add_subplot(111, projection='3d') 

 

Plot the unit sphere for reference (optional): 

 

>>> u = np.linspace(0, 2 * np.pi, 100) 

>>> v = np.linspace(0, np.pi, 100) 

>>> x = np.outer(np.cos(u), np.sin(v)) 

>>> y = np.outer(np.sin(u), np.sin(v)) 

>>> z = np.outer(np.ones(np.size(u)), np.cos(v)) 

>>> ax.plot_surface(x, y, z, color='y', alpha=0.1) 

 

Interpolating over a larger number of points 

may provide the appearance of a smooth curve on 

the surface of the sphere, which is also useful 

for discretized integration calculations on a 

sphere surface: 

 

>>> start = np.array([1, 0, 0]) 

>>> end = np.array([0, 0, 1]) 

>>> t_vals = np.linspace(0, 1, 200) 

>>> result = geometric_slerp(start, 

... end, 

... t_vals) 

>>> ax.plot(result[...,0], 

... result[...,1], 

... result[...,2], 

... c='k') 

>>> plt.show() 

""" 

 

start = np.asarray(start, dtype=np.float64) 

end = np.asarray(end, dtype=np.float64) 

 

if start.ndim != 1 or end.ndim != 1: 

raise ValueError("Start and end coordinates " 

"must be one-dimensional") 

 

if start.size != end.size: 

raise ValueError("The dimensions of start and " 

"end must match (have same size)") 

 

if start.size < 2 or end.size < 2: 

raise ValueError("The start and end coordinates must " 

"both be in at least two-dimensional " 

"space") 

 

if np.array_equal(start, end): 

return [start] * np.asarray(t).size 

 

# for points that violate equation for n-sphere 

for coord in [start, end]: 

if not np.allclose(np.linalg.norm(coord), 1.0, 

rtol=1e-9, 

atol=0): 

raise ValueError("start and end are not" 

" on a unit n-sphere") 

 

if not isinstance(tol, float): 

raise ValueError("tol must be a float") 

else: 

tol = np.fabs(tol) 

 

coord_dist = euclidean(start, end) 

 

# diameter of 2 within tolerance means antipodes, which is a problem 

# for all unit n-spheres (even the 0-sphere would have an ambiguous path) 

if np.allclose(coord_dist, 2.0, rtol=0, atol=tol): 

warnings.warn("start and end are antipodes" 

" using the specified tolerance;" 

" this may cause ambiguous slerp paths") 

 

t = np.asarray(t, dtype=np.float64) 

 

if t.size == 0: 

return np.empty((0, start.size)) 

 

if t.min() < 0 or t.max() > 1: 

raise ValueError("interpolation parameter must be in [0, 1]") 

 

if t.ndim == 0: 

return _geometric_slerp(start, 

end, 

np.atleast_1d(t)).ravel() 

else: 

return _geometric_slerp(start, 

end, 

t)