Coverage for /usr/lib/python3/dist-packages/scipy/interpolate/_rbf.py: 20%

97 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

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 .. legacy:: class 

63 

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

65 instead. 

66 

67 Parameters 

68 ---------- 

69 *args : arrays 

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

71 and d is the array of values at the nodes 

72 function : str or callable, optional 

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

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

75 

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

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

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

79 'linear': r 

80 'cubic': r**3 

81 'quintic': r**5 

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

83 

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

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

86 arguments passed in will be available as well. 

87 

88 epsilon : float, optional 

89 Adjustable constant for gaussian or multiquadrics functions 

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

91 a good start). 

92 smooth : float, optional 

93 Values greater than zero increase the smoothness of the 

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

95 always go through the nodal points in this case. 

96 norm : str, callable, optional 

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

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

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

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

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

102 `scipy.spatial.distances.cdist`. 

103 mode : str, optional 

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

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

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

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

108 

109 

110 Attributes 

111 ---------- 

112 N : int 

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

114 di : ndarray 

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

116 xi : ndarray 

117 The 2-D array of data coordinates. 

118 function : str or callable 

119 The radial basis function. See description under Parameters. 

120 epsilon : float 

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

122 smooth : float 

123 Smoothing parameter. See description under Parameters. 

124 norm : str or callable 

125 The distance function. See description under Parameters. 

126 mode : str 

127 Mode of the interpolation. See description under Parameters. 

128 nodes : ndarray 

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

130 A : internal property, do not use 

131 

132 See Also 

133 -------- 

134 RBFInterpolator 

135 

136 Examples 

137 -------- 

138 >>> import numpy as np 

139 >>> from scipy.interpolate import Rbf 

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

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

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

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

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

145 >>> di.shape 

146 (20,) 

147 

148 """ 

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

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

151 def _h_multiquadric(self, r): 

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

153 

154 def _h_inverse_multiquadric(self, r): 

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

156 

157 def _h_gaussian(self, r): 

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

159 

160 def _h_linear(self, r): 

161 return r 

162 

163 def _h_cubic(self, r): 

164 return r**3 

165 

166 def _h_quintic(self, r): 

167 return r**5 

168 

169 def _h_thin_plate(self, r): 

170 return xlogy(r**2, r) 

171 

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

173 def _init_function(self, r): 

174 if isinstance(self.function, str): 

175 self.function = self.function.lower() 

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

177 'inverse multiquadric': 'inverse_multiquadric', 

178 'thin-plate': 'thin_plate'} 

179 if self.function in _mapped: 

180 self.function = _mapped[self.function] 

181 

182 func_name = "_h_" + self.function 

183 if hasattr(self, func_name): 

184 self._function = getattr(self, func_name) 

185 else: 

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

187 if x.startswith('_h_')] 

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

189 ", ".join(functionlist)) 

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

191 elif callable(self.function): 

192 allow_one = False 

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

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

195 val = self.function 

196 allow_one = True 

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

198 val = self.function.__call__.__func__ 

199 else: 

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

201 "function") 

202 

203 argcount = val.__code__.co_argcount 

204 if allow_one and argcount == 1: 

205 self._function = self.function 

206 elif argcount == 2: 

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

208 else: 

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

210 "arguments.") 

211 

212 a0 = self._function(r) 

213 if a0.shape != r.shape: 

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

215 "the same shape") 

216 return a0 

217 

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

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

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

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

222 # All arrays must have the same number of elements 

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

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

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

226 

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

228 

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

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

231 self._target_dim = 1 

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

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

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

235 else: 

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

237 

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

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

240 

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

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

243 if self.epsilon is None: 

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

245 # on a bounding hypercube 

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

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

248 edges = ximax - ximin 

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

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

251 

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

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

254 

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

256 # function or to save on the object returned. 

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

258 setattr(self, item, value) 

259 

260 # Compute weights 

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

262 # we first factorize the matrix 

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

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

265 for i in range(self._target_dim): 

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

267 else: 

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

269 

270 @property 

271 def A(self): 

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

273 # and, at least technically, public. 

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

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

276 

277 def _call_norm(self, x1, x2): 

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

279 

280 def __call__(self, *args): 

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

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

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

284 if self._target_dim > 1: 

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

286 else: 

287 shp = args[0].shape 

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

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

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