Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/scipy/interpolate/_rbf.py: 20%

97 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-12 06:31 +0000

1"""rbf - Radial basis functions for interpolation/smoothing scattered N-D data. 

2 

3Written by John Travers <jtravs@gmail.com>, February 2007 

4Based closely on Matlab code by Alex Chirokov 

5Additional, large, improvements by Robert Hetland 

6Some additional alterations by Travis Oliphant 

7Interpolation with multi-dimensional target domain by Josua Sassen 

8 

9Permission to use, modify, and distribute this software is given under the 

10terms of the SciPy (BSD style) license. See LICENSE.txt that came with 

11this distribution for specifics. 

12 

13NO WARRANTY IS EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. 

14 

15Copyright (c) 2006-2007, Robert Hetland <hetland@tamu.edu> 

16Copyright (c) 2007, John Travers <jtravs@gmail.com> 

17 

18Redistribution and use in source and binary forms, with or without 

19modification, are permitted provided that the following conditions are 

20met: 

21 

22 * Redistributions of source code must retain the above copyright 

23 notice, this list of conditions and the following disclaimer. 

24 

25 * Redistributions in binary form must reproduce the above 

26 copyright notice, this list of conditions and the following 

27 disclaimer in the documentation and/or other materials provided 

28 with the distribution. 

29 

30 * Neither the name of Robert Hetland nor the names of any 

31 contributors may be used to endorse or promote products derived 

32 from this software without specific prior written permission. 

33 

34THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 

35"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 

36LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 

37A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 

38OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 

39SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 

40LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 

41DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 

42THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 

43(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 

44OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 

45""" 

46import numpy as np 

47 

48from scipy import linalg 

49from scipy.special import xlogy 

50from scipy.spatial.distance import cdist, pdist, squareform 

51 

52__all__ = ['Rbf'] 

53 

54 

55class Rbf: 

56 """ 

57 Rbf(*args, **kwargs) 

58 

59 A class for radial basis function interpolation of functions from 

60 N-D scattered data to an M-D domain. 

61 

62 .. note:: 

63 `Rbf` is legacy code, for new usage please use `RBFInterpolator` 

64 instead. 

65 

66 Parameters 

67 ---------- 

68 *args : arrays 

69 x, y, z, ..., d, where x, y, z, ... are the coordinates of the nodes 

70 and d is the array of values at the nodes 

71 function : str or callable, optional 

72 The radial basis function, based on the radius, r, given by the norm 

73 (default is Euclidean distance); the default is 'multiquadric':: 

74 

75 'multiquadric': sqrt((r/self.epsilon)**2 + 1) 

76 'inverse': 1.0/sqrt((r/self.epsilon)**2 + 1) 

77 'gaussian': exp(-(r/self.epsilon)**2) 

78 'linear': r 

79 'cubic': r**3 

80 'quintic': r**5 

81 'thin_plate': r**2 * log(r) 

82 

83 If callable, then it must take 2 arguments (self, r). The epsilon 

84 parameter will be available as self.epsilon. Other keyword 

85 arguments passed in will be available as well. 

86 

87 epsilon : float, optional 

88 Adjustable constant for gaussian or multiquadrics functions 

89 - defaults to approximate average distance between nodes (which is 

90 a good start). 

91 smooth : float, optional 

92 Values greater than zero increase the smoothness of the 

93 approximation. 0 is for interpolation (default), the function will 

94 always go through the nodal points in this case. 

95 norm : str, callable, optional 

96 A function that returns the 'distance' between two points, with 

97 inputs as arrays of positions (x, y, z, ...), and an output as an 

98 array of distance. E.g., the default: 'euclidean', such that the result 

99 is a matrix of the distances from each point in ``x1`` to each point in 

100 ``x2``. For more options, see documentation of 

101 `scipy.spatial.distances.cdist`. 

102 mode : str, optional 

103 Mode of the interpolation, can be '1-D' (default) or 'N-D'. When it is 

104 '1-D' the data `d` will be considered as 1-D and flattened 

105 internally. When it is 'N-D' the data `d` is assumed to be an array of 

106 shape (n_samples, m), where m is the dimension of the target domain. 

107 

108 

109 Attributes 

110 ---------- 

111 N : int 

112 The number of data points (as determined by the input arrays). 

113 di : ndarray 

114 The 1-D array of data values at each of the data coordinates `xi`. 

115 xi : ndarray 

116 The 2-D array of data coordinates. 

117 function : str or callable 

118 The radial basis function. See description under Parameters. 

119 epsilon : float 

120 Parameter used by gaussian or multiquadrics functions. See Parameters. 

121 smooth : float 

122 Smoothing parameter. See description under Parameters. 

123 norm : str or callable 

124 The distance function. See description under Parameters. 

125 mode : str 

126 Mode of the interpolation. See description under Parameters. 

127 nodes : ndarray 

128 A 1-D array of node values for the interpolation. 

129 A : internal property, do not use 

130 

131 See Also 

132 -------- 

133 RBFInterpolator 

134 

135 Examples 

136 -------- 

137 >>> import numpy as np 

138 >>> from scipy.interpolate import Rbf 

139 >>> rng = np.random.default_rng() 

140 >>> x, y, z, d = rng.random((4, 50)) 

141 >>> rbfi = Rbf(x, y, z, d) # radial basis function interpolator instance 

142 >>> xi = yi = zi = np.linspace(0, 1, 20) 

143 >>> di = rbfi(xi, yi, zi) # interpolated values 

144 >>> di.shape 

145 (20,) 

146 

147 """ 

148 # Available radial basis functions that can be selected as strings; 

149 # they all start with _h_ (self._init_function relies on that) 

150 def _h_multiquadric(self, r): 

151 return np.sqrt((1.0/self.epsilon*r)**2 + 1) 

152 

153 def _h_inverse_multiquadric(self, r): 

154 return 1.0/np.sqrt((1.0/self.epsilon*r)**2 + 1) 

155 

156 def _h_gaussian(self, r): 

157 return np.exp(-(1.0/self.epsilon*r)**2) 

158 

159 def _h_linear(self, r): 

160 return r 

161 

162 def _h_cubic(self, r): 

163 return r**3 

164 

165 def _h_quintic(self, r): 

166 return r**5 

167 

168 def _h_thin_plate(self, r): 

169 return xlogy(r**2, r) 

170 

171 # Setup self._function and do smoke test on initial r 

172 def _init_function(self, r): 

173 if isinstance(self.function, str): 

174 self.function = self.function.lower() 

175 _mapped = {'inverse': 'inverse_multiquadric', 

176 'inverse multiquadric': 'inverse_multiquadric', 

177 'thin-plate': 'thin_plate'} 

178 if self.function in _mapped: 

179 self.function = _mapped[self.function] 

180 

181 func_name = "_h_" + self.function 

182 if hasattr(self, func_name): 

183 self._function = getattr(self, func_name) 

184 else: 

185 functionlist = [x[3:] for x in dir(self) 

186 if x.startswith('_h_')] 

187 raise ValueError("function must be a callable or one of " + 

188 ", ".join(functionlist)) 

189 self._function = getattr(self, "_h_"+self.function) 

190 elif callable(self.function): 

191 allow_one = False 

192 if hasattr(self.function, 'func_code') or \ 

193 hasattr(self.function, '__code__'): 

194 val = self.function 

195 allow_one = True 

196 elif hasattr(self.function, "__call__"): 

197 val = self.function.__call__.__func__ 

198 else: 

199 raise ValueError("Cannot determine number of arguments to " 

200 "function") 

201 

202 argcount = val.__code__.co_argcount 

203 if allow_one and argcount == 1: 

204 self._function = self.function 

205 elif argcount == 2: 

206 self._function = self.function.__get__(self, Rbf) 

207 else: 

208 raise ValueError("Function argument must take 1 or 2 " 

209 "arguments.") 

210 

211 a0 = self._function(r) 

212 if a0.shape != r.shape: 

213 raise ValueError("Callable must take array and return array of " 

214 "the same shape") 

215 return a0 

216 

217 def __init__(self, *args, **kwargs): 

218 # `args` can be a variable number of arrays; we flatten them and store 

219 # them as a single 2-D array `xi` of shape (n_args-1, array_size), 

220 # plus a 1-D array `di` for the values. 

221 # All arrays must have the same number of elements 

222 self.xi = np.asarray([np.asarray(a, dtype=np.float_).flatten() 

223 for a in args[:-1]]) 

224 self.N = self.xi.shape[-1] 

225 

226 self.mode = kwargs.pop('mode', '1-D') 

227 

228 if self.mode == '1-D': 

229 self.di = np.asarray(args[-1]).flatten() 

230 self._target_dim = 1 

231 elif self.mode == 'N-D': 

232 self.di = np.asarray(args[-1]) 

233 self._target_dim = self.di.shape[-1] 

234 else: 

235 raise ValueError("Mode has to be 1-D or N-D.") 

236 

237 if not all([x.size == self.di.shape[0] for x in self.xi]): 

238 raise ValueError("All arrays must be equal length.") 

239 

240 self.norm = kwargs.pop('norm', 'euclidean') 

241 self.epsilon = kwargs.pop('epsilon', None) 

242 if self.epsilon is None: 

243 # default epsilon is the "the average distance between nodes" based 

244 # on a bounding hypercube 

245 ximax = np.amax(self.xi, axis=1) 

246 ximin = np.amin(self.xi, axis=1) 

247 edges = ximax - ximin 

248 edges = edges[np.nonzero(edges)] 

249 self.epsilon = np.power(np.prod(edges)/self.N, 1.0/edges.size) 

250 

251 self.smooth = kwargs.pop('smooth', 0.0) 

252 self.function = kwargs.pop('function', 'multiquadric') 

253 

254 # attach anything left in kwargs to self for use by any user-callable 

255 # function or to save on the object returned. 

256 for item, value in kwargs.items(): 

257 setattr(self, item, value) 

258 

259 # Compute weights 

260 if self._target_dim > 1: # If we have more than one target dimension, 

261 # we first factorize the matrix 

262 self.nodes = np.zeros((self.N, self._target_dim), dtype=self.di.dtype) 

263 lu, piv = linalg.lu_factor(self.A) 

264 for i in range(self._target_dim): 

265 self.nodes[:, i] = linalg.lu_solve((lu, piv), self.di[:, i]) 

266 else: 

267 self.nodes = linalg.solve(self.A, self.di) 

268 

269 @property 

270 def A(self): 

271 # this only exists for backwards compatibility: self.A was available 

272 # and, at least technically, public. 

273 r = squareform(pdist(self.xi.T, self.norm)) # Pairwise norm 

274 return self._init_function(r) - np.eye(self.N)*self.smooth 

275 

276 def _call_norm(self, x1, x2): 

277 return cdist(x1.T, x2.T, self.norm) 

278 

279 def __call__(self, *args): 

280 args = [np.asarray(x) for x in args] 

281 if not all([x.shape == y.shape for x in args for y in args]): 

282 raise ValueError("Array lengths must be equal") 

283 if self._target_dim > 1: 

284 shp = args[0].shape + (self._target_dim,) 

285 else: 

286 shp = args[0].shape 

287 xa = np.asarray([a.flatten() for a in args], dtype=np.float_) 

288 r = self._call_norm(xa, self.xi) 

289 return np.dot(self._function(r), self.nodes).reshape(shp)