Coverage for /usr/lib/python3/dist-packages/scipy/interpolate/_interpolate.py: 17%

784 statements  

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

1__all__ = ['interp1d', 'interp2d', 'lagrange', 'PPoly', 'BPoly', 'NdPPoly'] 

2 

3from math import prod 

4 

5import numpy as np 

6from numpy import (array, transpose, searchsorted, atleast_1d, atleast_2d, 

7 ravel, poly1d, asarray, intp) 

8 

9import scipy.special as spec 

10from scipy.special import comb 

11 

12from . import _fitpack_py 

13from . import dfitpack 

14from ._polyint import _Interpolator1D 

15from . import _ppoly 

16from .interpnd import _ndim_coords_from_arrays 

17from ._bsplines import make_interp_spline, BSpline 

18 

19 

20def lagrange(x, w): 

21 r""" 

22 Return a Lagrange interpolating polynomial. 

23 

24 Given two 1-D arrays `x` and `w,` returns the Lagrange interpolating 

25 polynomial through the points ``(x, w)``. 

26 

27 Warning: This implementation is numerically unstable. Do not expect to 

28 be able to use more than about 20 points even if they are chosen optimally. 

29 

30 Parameters 

31 ---------- 

32 x : array_like 

33 `x` represents the x-coordinates of a set of datapoints. 

34 w : array_like 

35 `w` represents the y-coordinates of a set of datapoints, i.e., f(`x`). 

36 

37 Returns 

38 ------- 

39 lagrange : `numpy.poly1d` instance 

40 The Lagrange interpolating polynomial. 

41 

42 Examples 

43 -------- 

44 Interpolate :math:`f(x) = x^3` by 3 points. 

45 

46 >>> import numpy as np 

47 >>> from scipy.interpolate import lagrange 

48 >>> x = np.array([0, 1, 2]) 

49 >>> y = x**3 

50 >>> poly = lagrange(x, y) 

51 

52 Since there are only 3 points, Lagrange polynomial has degree 2. Explicitly, 

53 it is given by 

54 

55 .. math:: 

56 

57 \begin{aligned} 

58 L(x) &= 1\times \frac{x (x - 2)}{-1} + 8\times \frac{x (x-1)}{2} \\ 

59 &= x (-2 + 3x) 

60 \end{aligned} 

61 

62 >>> from numpy.polynomial.polynomial import Polynomial 

63 >>> Polynomial(poly.coef[::-1]).coef 

64 array([ 0., -2., 3.]) 

65 

66 >>> import matplotlib.pyplot as plt 

67 >>> x_new = np.arange(0, 2.1, 0.1) 

68 >>> plt.scatter(x, y, label='data') 

69 >>> plt.plot(x_new, Polynomial(poly.coef[::-1])(x_new), label='Polynomial') 

70 >>> plt.plot(x_new, 3*x_new**2 - 2*x_new + 0*x_new, 

71 ... label=r"$3 x^2 - 2 x$", linestyle='-.') 

72 >>> plt.legend() 

73 >>> plt.show() 

74 

75 """ 

76 

77 M = len(x) 

78 p = poly1d(0.0) 

79 for j in range(M): 

80 pt = poly1d(w[j]) 

81 for k in range(M): 

82 if k == j: 

83 continue 

84 fac = x[j]-x[k] 

85 pt *= poly1d([1.0, -x[k]])/fac 

86 p += pt 

87 return p 

88 

89 

90# !! Need to find argument for keeping initialize. If it isn't 

91# !! found, get rid of it! 

92 

93 

94dep_mesg = """\ 

95`interp2d` is deprecated in SciPy 1.10 and will be removed in SciPy 1.13.0. 

96 

97For legacy code, nearly bug-for-bug compatible replacements are 

98`RectBivariateSpline` on regular grids, and `bisplrep`/`bisplev` for 

99scattered 2D data. 

100 

101In new code, for regular grids use `RegularGridInterpolator` instead. 

102For scattered data, prefer `LinearNDInterpolator` or 

103`CloughTocher2DInterpolator`. 

104 

105For more details see 

106`https://scipy.github.io/devdocs/notebooks/interp_transition_guide.html` 

107""" 

108 

109class interp2d: 

110 """ 

111 interp2d(x, y, z, kind='linear', copy=True, bounds_error=False, 

112 fill_value=None) 

113 

114 .. deprecated:: 1.10.0 

115 

116 `interp2d` is deprecated in SciPy 1.10 and will be removed in SciPy 

117 1.13.0. 

118 

119 For legacy code, nearly bug-for-bug compatible replacements are 

120 `RectBivariateSpline` on regular grids, and `bisplrep`/`bisplev` for 

121 scattered 2D data. 

122 

123 In new code, for regular grids use `RegularGridInterpolator` instead. 

124 For scattered data, prefer `LinearNDInterpolator` or 

125 `CloughTocher2DInterpolator`. 

126 

127 For more details see 

128 `https://scipy.github.io/devdocs/notebooks/interp_transition_guide.html 

129 <https://scipy.github.io/devdocs/notebooks/interp_transition_guide.html>`_ 

130 

131 

132 Interpolate over a 2-D grid. 

133 

134 `x`, `y` and `z` are arrays of values used to approximate some function 

135 f: ``z = f(x, y)`` which returns a scalar value `z`. This class returns a 

136 function whose call method uses spline interpolation to find the value 

137 of new points. 

138 

139 If `x` and `y` represent a regular grid, consider using 

140 `RectBivariateSpline`. 

141 

142 If `z` is a vector value, consider using `interpn`. 

143 

144 Note that calling `interp2d` with NaNs present in input values, or with 

145 decreasing values in `x` an `y` results in undefined behaviour. 

146 

147 Methods 

148 ------- 

149 __call__ 

150 

151 Parameters 

152 ---------- 

153 x, y : array_like 

154 Arrays defining the data point coordinates. 

155 The data point coordinates need to be sorted by increasing order. 

156 

157 If the points lie on a regular grid, `x` can specify the column 

158 coordinates and `y` the row coordinates, for example:: 

159 

160 >>> x = [0,1,2]; y = [0,3]; z = [[1,2,3], [4,5,6]] 

161 

162 Otherwise, `x` and `y` must specify the full coordinates for each 

163 point, for example:: 

164 

165 >>> x = [0,1,2,0,1,2]; y = [0,0,0,3,3,3]; z = [1,4,2,5,3,6] 

166 

167 If `x` and `y` are multidimensional, they are flattened before use. 

168 z : array_like 

169 The values of the function to interpolate at the data points. If 

170 `z` is a multidimensional array, it is flattened before use assuming 

171 Fortran-ordering (order='F'). The length of a flattened `z` array 

172 is either len(`x`)*len(`y`) if `x` and `y` specify the column and 

173 row coordinates or ``len(z) == len(x) == len(y)`` if `x` and `y` 

174 specify coordinates for each point. 

175 kind : {'linear', 'cubic', 'quintic'}, optional 

176 The kind of spline interpolation to use. Default is 'linear'. 

177 copy : bool, optional 

178 If True, the class makes internal copies of x, y and z. 

179 If False, references may be used. The default is to copy. 

180 bounds_error : bool, optional 

181 If True, when interpolated values are requested outside of the 

182 domain of the input data (x,y), a ValueError is raised. 

183 If False, then `fill_value` is used. 

184 fill_value : number, optional 

185 If provided, the value to use for points outside of the 

186 interpolation domain. If omitted (None), values outside 

187 the domain are extrapolated via nearest-neighbor extrapolation. 

188 

189 See Also 

190 -------- 

191 RectBivariateSpline : 

192 Much faster 2-D interpolation if your input data is on a grid 

193 bisplrep, bisplev : 

194 Spline interpolation based on FITPACK 

195 BivariateSpline : a more recent wrapper of the FITPACK routines 

196 interp1d : 1-D version of this function 

197 RegularGridInterpolator : interpolation on a regular or rectilinear grid 

198 in arbitrary dimensions. 

199 interpn : Multidimensional interpolation on regular grids (wraps 

200 `RegularGridInterpolator` and `RectBivariateSpline`). 

201 

202 Notes 

203 ----- 

204 The minimum number of data points required along the interpolation 

205 axis is ``(k+1)**2``, with k=1 for linear, k=3 for cubic and k=5 for 

206 quintic interpolation. 

207 

208 The interpolator is constructed by `bisplrep`, with a smoothing factor 

209 of 0. If more control over smoothing is needed, `bisplrep` should be 

210 used directly. 

211 

212 The coordinates of the data points to interpolate `xnew` and `ynew` 

213 have to be sorted by ascending order. 

214 `interp2d` is legacy and is not 

215 recommended for use in new code. New code should use 

216 `RegularGridInterpolator` instead. 

217 

218 Examples 

219 -------- 

220 Construct a 2-D grid and interpolate on it: 

221 

222 >>> import numpy as np 

223 >>> from scipy import interpolate 

224 >>> x = np.arange(-5.01, 5.01, 0.25) 

225 >>> y = np.arange(-5.01, 5.01, 0.25) 

226 >>> xx, yy = np.meshgrid(x, y) 

227 >>> z = np.sin(xx**2+yy**2) 

228 >>> f = interpolate.interp2d(x, y, z, kind='cubic') 

229 

230 Now use the obtained interpolation function and plot the result: 

231 

232 >>> import matplotlib.pyplot as plt 

233 >>> xnew = np.arange(-5.01, 5.01, 1e-2) 

234 >>> ynew = np.arange(-5.01, 5.01, 1e-2) 

235 >>> znew = f(xnew, ynew) 

236 >>> plt.plot(x, z[0, :], 'ro-', xnew, znew[0, :], 'b-') 

237 >>> plt.show() 

238 """ 

239 

240 @np.deprecate(old_name='interp2d', message=dep_mesg) 

241 def __init__(self, x, y, z, kind='linear', copy=True, bounds_error=False, 

242 fill_value=None): 

243 x = ravel(x) 

244 y = ravel(y) 

245 z = asarray(z) 

246 

247 rectangular_grid = (z.size == len(x) * len(y)) 

248 if rectangular_grid: 

249 if z.ndim == 2: 

250 if z.shape != (len(y), len(x)): 

251 raise ValueError("When on a regular grid with x.size = m " 

252 "and y.size = n, if z.ndim == 2, then z " 

253 "must have shape (n, m)") 

254 if not np.all(x[1:] >= x[:-1]): 

255 j = np.argsort(x) 

256 x = x[j] 

257 z = z[:, j] 

258 if not np.all(y[1:] >= y[:-1]): 

259 j = np.argsort(y) 

260 y = y[j] 

261 z = z[j, :] 

262 z = ravel(z.T) 

263 else: 

264 z = ravel(z) 

265 if len(x) != len(y): 

266 raise ValueError( 

267 "x and y must have equal lengths for non rectangular grid") 

268 if len(z) != len(x): 

269 raise ValueError( 

270 "Invalid length for input z for non rectangular grid") 

271 

272 interpolation_types = {'linear': 1, 'cubic': 3, 'quintic': 5} 

273 try: 

274 kx = ky = interpolation_types[kind] 

275 except KeyError as e: 

276 raise ValueError( 

277 f"Unsupported interpolation type {repr(kind)}, must be " 

278 f"either of {', '.join(map(repr, interpolation_types))}." 

279 ) from e 

280 

281 if not rectangular_grid: 

282 # TODO: surfit is really not meant for interpolation! 

283 self.tck = _fitpack_py.bisplrep(x, y, z, kx=kx, ky=ky, s=0.0) 

284 else: 

285 nx, tx, ny, ty, c, fp, ier = dfitpack.regrid_smth( 

286 x, y, z, None, None, None, None, 

287 kx=kx, ky=ky, s=0.0) 

288 self.tck = (tx[:nx], ty[:ny], c[:(nx - kx - 1) * (ny - ky - 1)], 

289 kx, ky) 

290 

291 self.bounds_error = bounds_error 

292 self.fill_value = fill_value 

293 self.x, self.y, self.z = (array(a, copy=copy) for a in (x, y, z)) 

294 

295 self.x_min, self.x_max = np.amin(x), np.amax(x) 

296 self.y_min, self.y_max = np.amin(y), np.amax(y) 

297 

298 @np.deprecate(old_name='interp2d', message=dep_mesg) 

299 def __call__(self, x, y, dx=0, dy=0, assume_sorted=False): 

300 """Interpolate the function. 

301 

302 Parameters 

303 ---------- 

304 x : 1-D array 

305 x-coordinates of the mesh on which to interpolate. 

306 y : 1-D array 

307 y-coordinates of the mesh on which to interpolate. 

308 dx : int >= 0, < kx 

309 Order of partial derivatives in x. 

310 dy : int >= 0, < ky 

311 Order of partial derivatives in y. 

312 assume_sorted : bool, optional 

313 If False, values of `x` and `y` can be in any order and they are 

314 sorted first. 

315 If True, `x` and `y` have to be arrays of monotonically 

316 increasing values. 

317 

318 Returns 

319 ------- 

320 z : 2-D array with shape (len(y), len(x)) 

321 The interpolated values. 

322 """ 

323 

324 x = atleast_1d(x) 

325 y = atleast_1d(y) 

326 

327 if x.ndim != 1 or y.ndim != 1: 

328 raise ValueError("x and y should both be 1-D arrays") 

329 

330 if not assume_sorted: 

331 x = np.sort(x, kind="mergesort") 

332 y = np.sort(y, kind="mergesort") 

333 

334 if self.bounds_error or self.fill_value is not None: 

335 out_of_bounds_x = (x < self.x_min) | (x > self.x_max) 

336 out_of_bounds_y = (y < self.y_min) | (y > self.y_max) 

337 

338 any_out_of_bounds_x = np.any(out_of_bounds_x) 

339 any_out_of_bounds_y = np.any(out_of_bounds_y) 

340 

341 if self.bounds_error and (any_out_of_bounds_x or any_out_of_bounds_y): 

342 raise ValueError("Values out of range; x must be in %r, y in %r" 

343 % ((self.x_min, self.x_max), 

344 (self.y_min, self.y_max))) 

345 

346 z = _fitpack_py.bisplev(x, y, self.tck, dx, dy) 

347 z = atleast_2d(z) 

348 z = transpose(z) 

349 

350 if self.fill_value is not None: 

351 if any_out_of_bounds_x: 

352 z[:, out_of_bounds_x] = self.fill_value 

353 if any_out_of_bounds_y: 

354 z[out_of_bounds_y, :] = self.fill_value 

355 

356 if len(z) == 1: 

357 z = z[0] 

358 return array(z) 

359 

360 

361def _check_broadcast_up_to(arr_from, shape_to, name): 

362 """Helper to check that arr_from broadcasts up to shape_to""" 

363 shape_from = arr_from.shape 

364 if len(shape_to) >= len(shape_from): 

365 for t, f in zip(shape_to[::-1], shape_from[::-1]): 

366 if f != 1 and f != t: 

367 break 

368 else: # all checks pass, do the upcasting that we need later 

369 if arr_from.size != 1 and arr_from.shape != shape_to: 

370 arr_from = np.ones(shape_to, arr_from.dtype) * arr_from 

371 return arr_from.ravel() 

372 # at least one check failed 

373 raise ValueError('%s argument must be able to broadcast up ' 

374 'to shape %s but had shape %s' 

375 % (name, shape_to, shape_from)) 

376 

377 

378def _do_extrapolate(fill_value): 

379 """Helper to check if fill_value == "extrapolate" without warnings""" 

380 return (isinstance(fill_value, str) and 

381 fill_value == 'extrapolate') 

382 

383 

384class interp1d(_Interpolator1D): 

385 """ 

386 Interpolate a 1-D function. 

387 

388 .. legacy:: class 

389 

390 `x` and `y` are arrays of values used to approximate some function f: 

391 ``y = f(x)``. This class returns a function whose call method uses 

392 interpolation to find the value of new points. 

393 

394 Parameters 

395 ---------- 

396 x : (npoints, ) array_like 

397 A 1-D array of real values. 

398 y : (..., npoints, ...) array_like 

399 A N-D array of real values. The length of `y` along the interpolation 

400 axis must be equal to the length of `x`. Use the ``axis`` parameter 

401 to select correct axis. Unlike other interpolators, the default 

402 interpolation axis is the last axis of `y`. 

403 kind : str or int, optional 

404 Specifies the kind of interpolation as a string or as an integer 

405 specifying the order of the spline interpolator to use. 

406 The string has to be one of 'linear', 'nearest', 'nearest-up', 'zero', 

407 'slinear', 'quadratic', 'cubic', 'previous', or 'next'. 'zero', 

408 'slinear', 'quadratic' and 'cubic' refer to a spline interpolation of 

409 zeroth, first, second or third order; 'previous' and 'next' simply 

410 return the previous or next value of the point; 'nearest-up' and 

411 'nearest' differ when interpolating half-integers (e.g. 0.5, 1.5) 

412 in that 'nearest-up' rounds up and 'nearest' rounds down. Default 

413 is 'linear'. 

414 axis : int, optional 

415 Axis in the ``y`` array corresponding to the x-coordinate values. Unlike 

416 other interpolators, defaults to ``axis=-1``. 

417 copy : bool, optional 

418 If True, the class makes internal copies of x and y. 

419 If False, references to `x` and `y` are used. The default is to copy. 

420 bounds_error : bool, optional 

421 If True, a ValueError is raised any time interpolation is attempted on 

422 a value outside of the range of x (where extrapolation is 

423 necessary). If False, out of bounds values are assigned `fill_value`. 

424 By default, an error is raised unless ``fill_value="extrapolate"``. 

425 fill_value : array-like or (array-like, array_like) or "extrapolate", optional 

426 - if a ndarray (or float), this value will be used to fill in for 

427 requested points outside of the data range. If not provided, then 

428 the default is NaN. The array-like must broadcast properly to the 

429 dimensions of the non-interpolation axes. 

430 - If a two-element tuple, then the first element is used as a 

431 fill value for ``x_new < x[0]`` and the second element is used for 

432 ``x_new > x[-1]``. Anything that is not a 2-element tuple (e.g., 

433 list or ndarray, regardless of shape) is taken to be a single 

434 array-like argument meant to be used for both bounds as 

435 ``below, above = fill_value, fill_value``. Using a two-element tuple 

436 or ndarray requires ``bounds_error=False``. 

437 

438 .. versionadded:: 0.17.0 

439 - If "extrapolate", then points outside the data range will be 

440 extrapolated. 

441 

442 .. versionadded:: 0.17.0 

443 assume_sorted : bool, optional 

444 If False, values of `x` can be in any order and they are sorted first. 

445 If True, `x` has to be an array of monotonically increasing values. 

446 

447 Attributes 

448 ---------- 

449 fill_value 

450 

451 Methods 

452 ------- 

453 __call__ 

454 

455 See Also 

456 -------- 

457 splrep, splev 

458 Spline interpolation/smoothing based on FITPACK. 

459 UnivariateSpline : An object-oriented wrapper of the FITPACK routines. 

460 interp2d : 2-D interpolation 

461 

462 Notes 

463 ----- 

464 Calling `interp1d` with NaNs present in input values results in 

465 undefined behaviour. 

466 

467 Input values `x` and `y` must be convertible to `float` values like 

468 `int` or `float`. 

469 

470 If the values in `x` are not unique, the resulting behavior is 

471 undefined and specific to the choice of `kind`, i.e., changing 

472 `kind` will change the behavior for duplicates. 

473 

474 

475 Examples 

476 -------- 

477 >>> import numpy as np 

478 >>> import matplotlib.pyplot as plt 

479 >>> from scipy import interpolate 

480 >>> x = np.arange(0, 10) 

481 >>> y = np.exp(-x/3.0) 

482 >>> f = interpolate.interp1d(x, y) 

483 

484 >>> xnew = np.arange(0, 9, 0.1) 

485 >>> ynew = f(xnew) # use interpolation function returned by `interp1d` 

486 >>> plt.plot(x, y, 'o', xnew, ynew, '-') 

487 >>> plt.show() 

488 """ 

489 

490 def __init__(self, x, y, kind='linear', axis=-1, 

491 copy=True, bounds_error=None, fill_value=np.nan, 

492 assume_sorted=False): 

493 """ Initialize a 1-D linear interpolation class.""" 

494 _Interpolator1D.__init__(self, x, y, axis=axis) 

495 

496 self.bounds_error = bounds_error # used by fill_value setter 

497 self.copy = copy 

498 

499 if kind in ['zero', 'slinear', 'quadratic', 'cubic']: 

500 order = {'zero': 0, 'slinear': 1, 

501 'quadratic': 2, 'cubic': 3}[kind] 

502 kind = 'spline' 

503 elif isinstance(kind, int): 

504 order = kind 

505 kind = 'spline' 

506 elif kind not in ('linear', 'nearest', 'nearest-up', 'previous', 

507 'next'): 

508 raise NotImplementedError("%s is unsupported: Use fitpack " 

509 "routines for other types." % kind) 

510 x = array(x, copy=self.copy) 

511 y = array(y, copy=self.copy) 

512 

513 if not assume_sorted: 

514 ind = np.argsort(x, kind="mergesort") 

515 x = x[ind] 

516 y = np.take(y, ind, axis=axis) 

517 

518 if x.ndim != 1: 

519 raise ValueError("the x array must have exactly one dimension.") 

520 if y.ndim == 0: 

521 raise ValueError("the y array must have at least one dimension.") 

522 

523 # Force-cast y to a floating-point type, if it's not yet one 

524 if not issubclass(y.dtype.type, np.inexact): 

525 y = y.astype(np.float_) 

526 

527 # Backward compatibility 

528 self.axis = axis % y.ndim 

529 

530 # Interpolation goes internally along the first axis 

531 self.y = y 

532 self._y = self._reshape_yi(self.y) 

533 self.x = x 

534 del y, x # clean up namespace to prevent misuse; use attributes 

535 self._kind = kind 

536 

537 # Adjust to interpolation kind; store reference to *unbound* 

538 # interpolation methods, in order to avoid circular references to self 

539 # stored in the bound instance methods, and therefore delayed garbage 

540 # collection. See: https://docs.python.org/reference/datamodel.html 

541 if kind in ('linear', 'nearest', 'nearest-up', 'previous', 'next'): 

542 # Make a "view" of the y array that is rotated to the interpolation 

543 # axis. 

544 minval = 1 

545 if kind == 'nearest': 

546 # Do division before addition to prevent possible integer 

547 # overflow 

548 self._side = 'left' 

549 self.x_bds = self.x / 2.0 

550 self.x_bds = self.x_bds[1:] + self.x_bds[:-1] 

551 

552 self._call = self.__class__._call_nearest 

553 elif kind == 'nearest-up': 

554 # Do division before addition to prevent possible integer 

555 # overflow 

556 self._side = 'right' 

557 self.x_bds = self.x / 2.0 

558 self.x_bds = self.x_bds[1:] + self.x_bds[:-1] 

559 

560 self._call = self.__class__._call_nearest 

561 elif kind == 'previous': 

562 # Side for np.searchsorted and index for clipping 

563 self._side = 'left' 

564 self._ind = 0 

565 # Move x by one floating point value to the left 

566 self._x_shift = np.nextafter(self.x, -np.inf) 

567 self._call = self.__class__._call_previousnext 

568 if _do_extrapolate(fill_value): 

569 self._check_and_update_bounds_error_for_extrapolation() 

570 # assume y is sorted by x ascending order here. 

571 fill_value = (np.nan, np.take(self.y, -1, axis)) 

572 elif kind == 'next': 

573 self._side = 'right' 

574 self._ind = 1 

575 # Move x by one floating point value to the right 

576 self._x_shift = np.nextafter(self.x, np.inf) 

577 self._call = self.__class__._call_previousnext 

578 if _do_extrapolate(fill_value): 

579 self._check_and_update_bounds_error_for_extrapolation() 

580 # assume y is sorted by x ascending order here. 

581 fill_value = (np.take(self.y, 0, axis), np.nan) 

582 else: 

583 # Check if we can delegate to numpy.interp (2x-10x faster). 

584 np_types = (np.float_, np.int_) 

585 cond = self.x.dtype in np_types and self.y.dtype in np_types 

586 cond = cond and self.y.ndim == 1 

587 cond = cond and not _do_extrapolate(fill_value) 

588 

589 if cond: 

590 self._call = self.__class__._call_linear_np 

591 else: 

592 self._call = self.__class__._call_linear 

593 else: 

594 minval = order + 1 

595 

596 rewrite_nan = False 

597 xx, yy = self.x, self._y 

598 if order > 1: 

599 # Quadratic or cubic spline. If input contains even a single 

600 # nan, then the output is all nans. We cannot just feed data 

601 # with nans to make_interp_spline because it calls LAPACK. 

602 # So, we make up a bogus x and y with no nans and use it 

603 # to get the correct shape of the output, which we then fill 

604 # with nans. 

605 # For slinear or zero order spline, we just pass nans through. 

606 mask = np.isnan(self.x) 

607 if mask.any(): 

608 sx = self.x[~mask] 

609 if sx.size == 0: 

610 raise ValueError("`x` array is all-nan") 

611 xx = np.linspace(np.nanmin(self.x), 

612 np.nanmax(self.x), 

613 len(self.x)) 

614 rewrite_nan = True 

615 if np.isnan(self._y).any(): 

616 yy = np.ones_like(self._y) 

617 rewrite_nan = True 

618 

619 self._spline = make_interp_spline(xx, yy, k=order, 

620 check_finite=False) 

621 if rewrite_nan: 

622 self._call = self.__class__._call_nan_spline 

623 else: 

624 self._call = self.__class__._call_spline 

625 

626 if len(self.x) < minval: 

627 raise ValueError("x and y arrays must have at " 

628 "least %d entries" % minval) 

629 

630 self.fill_value = fill_value # calls the setter, can modify bounds_err 

631 

632 @property 

633 def fill_value(self): 

634 """The fill value.""" 

635 # backwards compat: mimic a public attribute 

636 return self._fill_value_orig 

637 

638 @fill_value.setter 

639 def fill_value(self, fill_value): 

640 # extrapolation only works for nearest neighbor and linear methods 

641 if _do_extrapolate(fill_value): 

642 self._check_and_update_bounds_error_for_extrapolation() 

643 self._extrapolate = True 

644 else: 

645 broadcast_shape = (self.y.shape[:self.axis] + 

646 self.y.shape[self.axis + 1:]) 

647 if len(broadcast_shape) == 0: 

648 broadcast_shape = (1,) 

649 # it's either a pair (_below_range, _above_range) or a single value 

650 # for both above and below range 

651 if isinstance(fill_value, tuple) and len(fill_value) == 2: 

652 below_above = [np.asarray(fill_value[0]), 

653 np.asarray(fill_value[1])] 

654 names = ('fill_value (below)', 'fill_value (above)') 

655 for ii in range(2): 

656 below_above[ii] = _check_broadcast_up_to( 

657 below_above[ii], broadcast_shape, names[ii]) 

658 else: 

659 fill_value = np.asarray(fill_value) 

660 below_above = [_check_broadcast_up_to( 

661 fill_value, broadcast_shape, 'fill_value')] * 2 

662 self._fill_value_below, self._fill_value_above = below_above 

663 self._extrapolate = False 

664 if self.bounds_error is None: 

665 self.bounds_error = True 

666 # backwards compat: fill_value was a public attr; make it writeable 

667 self._fill_value_orig = fill_value 

668 

669 def _check_and_update_bounds_error_for_extrapolation(self): 

670 if self.bounds_error: 

671 raise ValueError("Cannot extrapolate and raise " 

672 "at the same time.") 

673 self.bounds_error = False 

674 

675 def _call_linear_np(self, x_new): 

676 # Note that out-of-bounds values are taken care of in self._evaluate 

677 return np.interp(x_new, self.x, self.y) 

678 

679 def _call_linear(self, x_new): 

680 # 2. Find where in the original data, the values to interpolate 

681 # would be inserted. 

682 # Note: If x_new[n] == x[m], then m is returned by searchsorted. 

683 x_new_indices = searchsorted(self.x, x_new) 

684 

685 # 3. Clip x_new_indices so that they are within the range of 

686 # self.x indices and at least 1. Removes mis-interpolation 

687 # of x_new[n] = x[0] 

688 x_new_indices = x_new_indices.clip(1, len(self.x)-1).astype(int) 

689 

690 # 4. Calculate the slope of regions that each x_new value falls in. 

691 lo = x_new_indices - 1 

692 hi = x_new_indices 

693 

694 x_lo = self.x[lo] 

695 x_hi = self.x[hi] 

696 y_lo = self._y[lo] 

697 y_hi = self._y[hi] 

698 

699 # Note that the following two expressions rely on the specifics of the 

700 # broadcasting semantics. 

701 slope = (y_hi - y_lo) / (x_hi - x_lo)[:, None] 

702 

703 # 5. Calculate the actual value for each entry in x_new. 

704 y_new = slope*(x_new - x_lo)[:, None] + y_lo 

705 

706 return y_new 

707 

708 def _call_nearest(self, x_new): 

709 """ Find nearest neighbor interpolated y_new = f(x_new).""" 

710 

711 # 2. Find where in the averaged data the values to interpolate 

712 # would be inserted. 

713 # Note: use side='left' (right) to searchsorted() to define the 

714 # halfway point to be nearest to the left (right) neighbor 

715 x_new_indices = searchsorted(self.x_bds, x_new, side=self._side) 

716 

717 # 3. Clip x_new_indices so that they are within the range of x indices. 

718 x_new_indices = x_new_indices.clip(0, len(self.x)-1).astype(intp) 

719 

720 # 4. Calculate the actual value for each entry in x_new. 

721 y_new = self._y[x_new_indices] 

722 

723 return y_new 

724 

725 def _call_previousnext(self, x_new): 

726 """Use previous/next neighbor of x_new, y_new = f(x_new).""" 

727 

728 # 1. Get index of left/right value 

729 x_new_indices = searchsorted(self._x_shift, x_new, side=self._side) 

730 

731 # 2. Clip x_new_indices so that they are within the range of x indices. 

732 x_new_indices = x_new_indices.clip(1-self._ind, 

733 len(self.x)-self._ind).astype(intp) 

734 

735 # 3. Calculate the actual value for each entry in x_new. 

736 y_new = self._y[x_new_indices+self._ind-1] 

737 

738 return y_new 

739 

740 def _call_spline(self, x_new): 

741 return self._spline(x_new) 

742 

743 def _call_nan_spline(self, x_new): 

744 out = self._spline(x_new) 

745 out[...] = np.nan 

746 return out 

747 

748 def _evaluate(self, x_new): 

749 # 1. Handle values in x_new that are outside of x. Throw error, 

750 # or return a list of mask array indicating the outofbounds values. 

751 # The behavior is set by the bounds_error variable. 

752 x_new = asarray(x_new) 

753 y_new = self._call(self, x_new) 

754 if not self._extrapolate: 

755 below_bounds, above_bounds = self._check_bounds(x_new) 

756 if len(y_new) > 0: 

757 # Note fill_value must be broadcast up to the proper size 

758 # and flattened to work here 

759 y_new[below_bounds] = self._fill_value_below 

760 y_new[above_bounds] = self._fill_value_above 

761 return y_new 

762 

763 def _check_bounds(self, x_new): 

764 """Check the inputs for being in the bounds of the interpolated data. 

765 

766 Parameters 

767 ---------- 

768 x_new : array 

769 

770 Returns 

771 ------- 

772 out_of_bounds : bool array 

773 The mask on x_new of values that are out of the bounds. 

774 """ 

775 

776 # If self.bounds_error is True, we raise an error if any x_new values 

777 # fall outside the range of x. Otherwise, we return an array indicating 

778 # which values are outside the boundary region. 

779 below_bounds = x_new < self.x[0] 

780 above_bounds = x_new > self.x[-1] 

781 

782 if self.bounds_error and below_bounds.any(): 

783 below_bounds_value = x_new[np.argmax(below_bounds)] 

784 raise ValueError("A value ({}) in x_new is below " 

785 "the interpolation range's minimum value ({})." 

786 .format(below_bounds_value, self.x[0])) 

787 if self.bounds_error and above_bounds.any(): 

788 above_bounds_value = x_new[np.argmax(above_bounds)] 

789 raise ValueError("A value ({}) in x_new is above " 

790 "the interpolation range's maximum value ({})." 

791 .format(above_bounds_value, self.x[-1])) 

792 

793 # !! Should we emit a warning if some values are out of bounds? 

794 # !! matlab does not. 

795 return below_bounds, above_bounds 

796 

797 

798class _PPolyBase: 

799 """Base class for piecewise polynomials.""" 

800 __slots__ = ('c', 'x', 'extrapolate', 'axis') 

801 

802 def __init__(self, c, x, extrapolate=None, axis=0): 

803 self.c = np.asarray(c) 

804 self.x = np.ascontiguousarray(x, dtype=np.float64) 

805 

806 if extrapolate is None: 

807 extrapolate = True 

808 elif extrapolate != 'periodic': 

809 extrapolate = bool(extrapolate) 

810 self.extrapolate = extrapolate 

811 

812 if self.c.ndim < 2: 

813 raise ValueError("Coefficients array must be at least " 

814 "2-dimensional.") 

815 

816 if not (0 <= axis < self.c.ndim - 1): 

817 raise ValueError("axis=%s must be between 0 and %s" % 

818 (axis, self.c.ndim-1)) 

819 

820 self.axis = axis 

821 if axis != 0: 

822 # move the interpolation axis to be the first one in self.c 

823 # More specifically, the target shape for self.c is (k, m, ...), 

824 # and axis !=0 means that we have c.shape (..., k, m, ...) 

825 # ^ 

826 # axis 

827 # So we roll two of them. 

828 self.c = np.moveaxis(self.c, axis+1, 0) 

829 self.c = np.moveaxis(self.c, axis+1, 0) 

830 

831 if self.x.ndim != 1: 

832 raise ValueError("x must be 1-dimensional") 

833 if self.x.size < 2: 

834 raise ValueError("at least 2 breakpoints are needed") 

835 if self.c.ndim < 2: 

836 raise ValueError("c must have at least 2 dimensions") 

837 if self.c.shape[0] == 0: 

838 raise ValueError("polynomial must be at least of order 0") 

839 if self.c.shape[1] != self.x.size-1: 

840 raise ValueError("number of coefficients != len(x)-1") 

841 dx = np.diff(self.x) 

842 if not (np.all(dx >= 0) or np.all(dx <= 0)): 

843 raise ValueError("`x` must be strictly increasing or decreasing.") 

844 

845 dtype = self._get_dtype(self.c.dtype) 

846 self.c = np.ascontiguousarray(self.c, dtype=dtype) 

847 

848 def _get_dtype(self, dtype): 

849 if np.issubdtype(dtype, np.complexfloating) \ 

850 or np.issubdtype(self.c.dtype, np.complexfloating): 

851 return np.complex_ 

852 else: 

853 return np.float_ 

854 

855 @classmethod 

856 def construct_fast(cls, c, x, extrapolate=None, axis=0): 

857 """ 

858 Construct the piecewise polynomial without making checks. 

859 

860 Takes the same parameters as the constructor. Input arguments 

861 ``c`` and ``x`` must be arrays of the correct shape and type. The 

862 ``c`` array can only be of dtypes float and complex, and ``x`` 

863 array must have dtype float. 

864 """ 

865 self = object.__new__(cls) 

866 self.c = c 

867 self.x = x 

868 self.axis = axis 

869 if extrapolate is None: 

870 extrapolate = True 

871 self.extrapolate = extrapolate 

872 return self 

873 

874 def _ensure_c_contiguous(self): 

875 """ 

876 c and x may be modified by the user. The Cython code expects 

877 that they are C contiguous. 

878 """ 

879 if not self.x.flags.c_contiguous: 

880 self.x = self.x.copy() 

881 if not self.c.flags.c_contiguous: 

882 self.c = self.c.copy() 

883 

884 def extend(self, c, x): 

885 """ 

886 Add additional breakpoints and coefficients to the polynomial. 

887 

888 Parameters 

889 ---------- 

890 c : ndarray, size (k, m, ...) 

891 Additional coefficients for polynomials in intervals. Note that 

892 the first additional interval will be formed using one of the 

893 ``self.x`` end points. 

894 x : ndarray, size (m,) 

895 Additional breakpoints. Must be sorted in the same order as 

896 ``self.x`` and either to the right or to the left of the current 

897 breakpoints. 

898 """ 

899 

900 c = np.asarray(c) 

901 x = np.asarray(x) 

902 

903 if c.ndim < 2: 

904 raise ValueError("invalid dimensions for c") 

905 if x.ndim != 1: 

906 raise ValueError("invalid dimensions for x") 

907 if x.shape[0] != c.shape[1]: 

908 raise ValueError("Shapes of x {} and c {} are incompatible" 

909 .format(x.shape, c.shape)) 

910 if c.shape[2:] != self.c.shape[2:] or c.ndim != self.c.ndim: 

911 raise ValueError("Shapes of c {} and self.c {} are incompatible" 

912 .format(c.shape, self.c.shape)) 

913 

914 if c.size == 0: 

915 return 

916 

917 dx = np.diff(x) 

918 if not (np.all(dx >= 0) or np.all(dx <= 0)): 

919 raise ValueError("`x` is not sorted.") 

920 

921 if self.x[-1] >= self.x[0]: 

922 if not x[-1] >= x[0]: 

923 raise ValueError("`x` is in the different order " 

924 "than `self.x`.") 

925 

926 if x[0] >= self.x[-1]: 

927 action = 'append' 

928 elif x[-1] <= self.x[0]: 

929 action = 'prepend' 

930 else: 

931 raise ValueError("`x` is neither on the left or on the right " 

932 "from `self.x`.") 

933 else: 

934 if not x[-1] <= x[0]: 

935 raise ValueError("`x` is in the different order " 

936 "than `self.x`.") 

937 

938 if x[0] <= self.x[-1]: 

939 action = 'append' 

940 elif x[-1] >= self.x[0]: 

941 action = 'prepend' 

942 else: 

943 raise ValueError("`x` is neither on the left or on the right " 

944 "from `self.x`.") 

945 

946 dtype = self._get_dtype(c.dtype) 

947 

948 k2 = max(c.shape[0], self.c.shape[0]) 

949 c2 = np.zeros((k2, self.c.shape[1] + c.shape[1]) + self.c.shape[2:], 

950 dtype=dtype) 

951 

952 if action == 'append': 

953 c2[k2-self.c.shape[0]:, :self.c.shape[1]] = self.c 

954 c2[k2-c.shape[0]:, self.c.shape[1]:] = c 

955 self.x = np.r_[self.x, x] 

956 elif action == 'prepend': 

957 c2[k2-self.c.shape[0]:, :c.shape[1]] = c 

958 c2[k2-c.shape[0]:, c.shape[1]:] = self.c 

959 self.x = np.r_[x, self.x] 

960 

961 self.c = c2 

962 

963 def __call__(self, x, nu=0, extrapolate=None): 

964 """ 

965 Evaluate the piecewise polynomial or its derivative. 

966 

967 Parameters 

968 ---------- 

969 x : array_like 

970 Points to evaluate the interpolant at. 

971 nu : int, optional 

972 Order of derivative to evaluate. Must be non-negative. 

973 extrapolate : {bool, 'periodic', None}, optional 

974 If bool, determines whether to extrapolate to out-of-bounds points 

975 based on first and last intervals, or to return NaNs. 

976 If 'periodic', periodic extrapolation is used. 

977 If None (default), use `self.extrapolate`. 

978 

979 Returns 

980 ------- 

981 y : array_like 

982 Interpolated values. Shape is determined by replacing 

983 the interpolation axis in the original array with the shape of x. 

984 

985 Notes 

986 ----- 

987 Derivatives are evaluated piecewise for each polynomial 

988 segment, even if the polynomial is not differentiable at the 

989 breakpoints. The polynomial intervals are considered half-open, 

990 ``[a, b)``, except for the last interval which is closed 

991 ``[a, b]``. 

992 """ 

993 if extrapolate is None: 

994 extrapolate = self.extrapolate 

995 x = np.asarray(x) 

996 x_shape, x_ndim = x.shape, x.ndim 

997 x = np.ascontiguousarray(x.ravel(), dtype=np.float_) 

998 

999 # With periodic extrapolation we map x to the segment 

1000 # [self.x[0], self.x[-1]]. 

1001 if extrapolate == 'periodic': 

1002 x = self.x[0] + (x - self.x[0]) % (self.x[-1] - self.x[0]) 

1003 extrapolate = False 

1004 

1005 out = np.empty((len(x), prod(self.c.shape[2:])), dtype=self.c.dtype) 

1006 self._ensure_c_contiguous() 

1007 self._evaluate(x, nu, extrapolate, out) 

1008 out = out.reshape(x_shape + self.c.shape[2:]) 

1009 if self.axis != 0: 

1010 # transpose to move the calculated values to the interpolation axis 

1011 l = list(range(out.ndim)) 

1012 l = l[x_ndim:x_ndim+self.axis] + l[:x_ndim] + l[x_ndim+self.axis:] 

1013 out = out.transpose(l) 

1014 return out 

1015 

1016 

1017class PPoly(_PPolyBase): 

1018 """ 

1019 Piecewise polynomial in terms of coefficients and breakpoints 

1020 

1021 The polynomial between ``x[i]`` and ``x[i + 1]`` is written in the 

1022 local power basis:: 

1023 

1024 S = sum(c[m, i] * (xp - x[i])**(k-m) for m in range(k+1)) 

1025 

1026 where ``k`` is the degree of the polynomial. 

1027 

1028 Parameters 

1029 ---------- 

1030 c : ndarray, shape (k, m, ...) 

1031 Polynomial coefficients, order `k` and `m` intervals. 

1032 x : ndarray, shape (m+1,) 

1033 Polynomial breakpoints. Must be sorted in either increasing or 

1034 decreasing order. 

1035 extrapolate : bool or 'periodic', optional 

1036 If bool, determines whether to extrapolate to out-of-bounds points 

1037 based on first and last intervals, or to return NaNs. If 'periodic', 

1038 periodic extrapolation is used. Default is True. 

1039 axis : int, optional 

1040 Interpolation axis. Default is zero. 

1041 

1042 Attributes 

1043 ---------- 

1044 x : ndarray 

1045 Breakpoints. 

1046 c : ndarray 

1047 Coefficients of the polynomials. They are reshaped 

1048 to a 3-D array with the last dimension representing 

1049 the trailing dimensions of the original coefficient array. 

1050 axis : int 

1051 Interpolation axis. 

1052 

1053 Methods 

1054 ------- 

1055 __call__ 

1056 derivative 

1057 antiderivative 

1058 integrate 

1059 solve 

1060 roots 

1061 extend 

1062 from_spline 

1063 from_bernstein_basis 

1064 construct_fast 

1065 

1066 See also 

1067 -------- 

1068 BPoly : piecewise polynomials in the Bernstein basis 

1069 

1070 Notes 

1071 ----- 

1072 High-order polynomials in the power basis can be numerically 

1073 unstable. Precision problems can start to appear for orders 

1074 larger than 20-30. 

1075 """ 

1076 

1077 def _evaluate(self, x, nu, extrapolate, out): 

1078 _ppoly.evaluate(self.c.reshape(self.c.shape[0], self.c.shape[1], -1), 

1079 self.x, x, nu, bool(extrapolate), out) 

1080 

1081 def derivative(self, nu=1): 

1082 """ 

1083 Construct a new piecewise polynomial representing the derivative. 

1084 

1085 Parameters 

1086 ---------- 

1087 nu : int, optional 

1088 Order of derivative to evaluate. Default is 1, i.e., compute the 

1089 first derivative. If negative, the antiderivative is returned. 

1090 

1091 Returns 

1092 ------- 

1093 pp : PPoly 

1094 Piecewise polynomial of order k2 = k - n representing the derivative 

1095 of this polynomial. 

1096 

1097 Notes 

1098 ----- 

1099 Derivatives are evaluated piecewise for each polynomial 

1100 segment, even if the polynomial is not differentiable at the 

1101 breakpoints. The polynomial intervals are considered half-open, 

1102 ``[a, b)``, except for the last interval which is closed 

1103 ``[a, b]``. 

1104 """ 

1105 if nu < 0: 

1106 return self.antiderivative(-nu) 

1107 

1108 # reduce order 

1109 if nu == 0: 

1110 c2 = self.c.copy() 

1111 else: 

1112 c2 = self.c[:-nu, :].copy() 

1113 

1114 if c2.shape[0] == 0: 

1115 # derivative of order 0 is zero 

1116 c2 = np.zeros((1,) + c2.shape[1:], dtype=c2.dtype) 

1117 

1118 # multiply by the correct rising factorials 

1119 factor = spec.poch(np.arange(c2.shape[0], 0, -1), nu) 

1120 c2 *= factor[(slice(None),) + (None,)*(c2.ndim-1)] 

1121 

1122 # construct a compatible polynomial 

1123 return self.construct_fast(c2, self.x, self.extrapolate, self.axis) 

1124 

1125 def antiderivative(self, nu=1): 

1126 """ 

1127 Construct a new piecewise polynomial representing the antiderivative. 

1128 

1129 Antiderivative is also the indefinite integral of the function, 

1130 and derivative is its inverse operation. 

1131 

1132 Parameters 

1133 ---------- 

1134 nu : int, optional 

1135 Order of antiderivative to evaluate. Default is 1, i.e., compute 

1136 the first integral. If negative, the derivative is returned. 

1137 

1138 Returns 

1139 ------- 

1140 pp : PPoly 

1141 Piecewise polynomial of order k2 = k + n representing 

1142 the antiderivative of this polynomial. 

1143 

1144 Notes 

1145 ----- 

1146 The antiderivative returned by this function is continuous and 

1147 continuously differentiable to order n-1, up to floating point 

1148 rounding error. 

1149 

1150 If antiderivative is computed and ``self.extrapolate='periodic'``, 

1151 it will be set to False for the returned instance. This is done because 

1152 the antiderivative is no longer periodic and its correct evaluation 

1153 outside of the initially given x interval is difficult. 

1154 """ 

1155 if nu <= 0: 

1156 return self.derivative(-nu) 

1157 

1158 c = np.zeros((self.c.shape[0] + nu, self.c.shape[1]) + self.c.shape[2:], 

1159 dtype=self.c.dtype) 

1160 c[:-nu] = self.c 

1161 

1162 # divide by the correct rising factorials 

1163 factor = spec.poch(np.arange(self.c.shape[0], 0, -1), nu) 

1164 c[:-nu] /= factor[(slice(None),) + (None,)*(c.ndim-1)] 

1165 

1166 # fix continuity of added degrees of freedom 

1167 self._ensure_c_contiguous() 

1168 _ppoly.fix_continuity(c.reshape(c.shape[0], c.shape[1], -1), 

1169 self.x, nu - 1) 

1170 

1171 if self.extrapolate == 'periodic': 

1172 extrapolate = False 

1173 else: 

1174 extrapolate = self.extrapolate 

1175 

1176 # construct a compatible polynomial 

1177 return self.construct_fast(c, self.x, extrapolate, self.axis) 

1178 

1179 def integrate(self, a, b, extrapolate=None): 

1180 """ 

1181 Compute a definite integral over a piecewise polynomial. 

1182 

1183 Parameters 

1184 ---------- 

1185 a : float 

1186 Lower integration bound 

1187 b : float 

1188 Upper integration bound 

1189 extrapolate : {bool, 'periodic', None}, optional 

1190 If bool, determines whether to extrapolate to out-of-bounds points 

1191 based on first and last intervals, or to return NaNs. 

1192 If 'periodic', periodic extrapolation is used. 

1193 If None (default), use `self.extrapolate`. 

1194 

1195 Returns 

1196 ------- 

1197 ig : array_like 

1198 Definite integral of the piecewise polynomial over [a, b] 

1199 """ 

1200 if extrapolate is None: 

1201 extrapolate = self.extrapolate 

1202 

1203 # Swap integration bounds if needed 

1204 sign = 1 

1205 if b < a: 

1206 a, b = b, a 

1207 sign = -1 

1208 

1209 range_int = np.empty((prod(self.c.shape[2:]),), dtype=self.c.dtype) 

1210 self._ensure_c_contiguous() 

1211 

1212 # Compute the integral. 

1213 if extrapolate == 'periodic': 

1214 # Split the integral into the part over period (can be several 

1215 # of them) and the remaining part. 

1216 

1217 xs, xe = self.x[0], self.x[-1] 

1218 period = xe - xs 

1219 interval = b - a 

1220 n_periods, left = divmod(interval, period) 

1221 

1222 if n_periods > 0: 

1223 _ppoly.integrate( 

1224 self.c.reshape(self.c.shape[0], self.c.shape[1], -1), 

1225 self.x, xs, xe, False, out=range_int) 

1226 range_int *= n_periods 

1227 else: 

1228 range_int.fill(0) 

1229 

1230 # Map a to [xs, xe], b is always a + left. 

1231 a = xs + (a - xs) % period 

1232 b = a + left 

1233 

1234 # If b <= xe then we need to integrate over [a, b], otherwise 

1235 # over [a, xe] and from xs to what is remained. 

1236 remainder_int = np.empty_like(range_int) 

1237 if b <= xe: 

1238 _ppoly.integrate( 

1239 self.c.reshape(self.c.shape[0], self.c.shape[1], -1), 

1240 self.x, a, b, False, out=remainder_int) 

1241 range_int += remainder_int 

1242 else: 

1243 _ppoly.integrate( 

1244 self.c.reshape(self.c.shape[0], self.c.shape[1], -1), 

1245 self.x, a, xe, False, out=remainder_int) 

1246 range_int += remainder_int 

1247 

1248 _ppoly.integrate( 

1249 self.c.reshape(self.c.shape[0], self.c.shape[1], -1), 

1250 self.x, xs, xs + left + a - xe, False, out=remainder_int) 

1251 range_int += remainder_int 

1252 else: 

1253 _ppoly.integrate( 

1254 self.c.reshape(self.c.shape[0], self.c.shape[1], -1), 

1255 self.x, a, b, bool(extrapolate), out=range_int) 

1256 

1257 # Return 

1258 range_int *= sign 

1259 return range_int.reshape(self.c.shape[2:]) 

1260 

1261 def solve(self, y=0., discontinuity=True, extrapolate=None): 

1262 """ 

1263 Find real solutions of the equation ``pp(x) == y``. 

1264 

1265 Parameters 

1266 ---------- 

1267 y : float, optional 

1268 Right-hand side. Default is zero. 

1269 discontinuity : bool, optional 

1270 Whether to report sign changes across discontinuities at 

1271 breakpoints as roots. 

1272 extrapolate : {bool, 'periodic', None}, optional 

1273 If bool, determines whether to return roots from the polynomial 

1274 extrapolated based on first and last intervals, 'periodic' works 

1275 the same as False. If None (default), use `self.extrapolate`. 

1276 

1277 Returns 

1278 ------- 

1279 roots : ndarray 

1280 Roots of the polynomial(s). 

1281 

1282 If the PPoly object describes multiple polynomials, the 

1283 return value is an object array whose each element is an 

1284 ndarray containing the roots. 

1285 

1286 Notes 

1287 ----- 

1288 This routine works only on real-valued polynomials. 

1289 

1290 If the piecewise polynomial contains sections that are 

1291 identically zero, the root list will contain the start point 

1292 of the corresponding interval, followed by a ``nan`` value. 

1293 

1294 If the polynomial is discontinuous across a breakpoint, and 

1295 there is a sign change across the breakpoint, this is reported 

1296 if the `discont` parameter is True. 

1297 

1298 Examples 

1299 -------- 

1300 

1301 Finding roots of ``[x**2 - 1, (x - 1)**2]`` defined on intervals 

1302 ``[-2, 1], [1, 2]``: 

1303 

1304 >>> import numpy as np 

1305 >>> from scipy.interpolate import PPoly 

1306 >>> pp = PPoly(np.array([[1, -4, 3], [1, 0, 0]]).T, [-2, 1, 2]) 

1307 >>> pp.solve() 

1308 array([-1., 1.]) 

1309 """ 

1310 if extrapolate is None: 

1311 extrapolate = self.extrapolate 

1312 

1313 self._ensure_c_contiguous() 

1314 

1315 if np.issubdtype(self.c.dtype, np.complexfloating): 

1316 raise ValueError("Root finding is only for " 

1317 "real-valued polynomials") 

1318 

1319 y = float(y) 

1320 r = _ppoly.real_roots(self.c.reshape(self.c.shape[0], self.c.shape[1], -1), 

1321 self.x, y, bool(discontinuity), 

1322 bool(extrapolate)) 

1323 if self.c.ndim == 2: 

1324 return r[0] 

1325 else: 

1326 r2 = np.empty(prod(self.c.shape[2:]), dtype=object) 

1327 # this for-loop is equivalent to ``r2[...] = r``, but that's broken 

1328 # in NumPy 1.6.0 

1329 for ii, root in enumerate(r): 

1330 r2[ii] = root 

1331 

1332 return r2.reshape(self.c.shape[2:]) 

1333 

1334 def roots(self, discontinuity=True, extrapolate=None): 

1335 """ 

1336 Find real roots of the piecewise polynomial. 

1337 

1338 Parameters 

1339 ---------- 

1340 discontinuity : bool, optional 

1341 Whether to report sign changes across discontinuities at 

1342 breakpoints as roots. 

1343 extrapolate : {bool, 'periodic', None}, optional 

1344 If bool, determines whether to return roots from the polynomial 

1345 extrapolated based on first and last intervals, 'periodic' works 

1346 the same as False. If None (default), use `self.extrapolate`. 

1347 

1348 Returns 

1349 ------- 

1350 roots : ndarray 

1351 Roots of the polynomial(s). 

1352 

1353 If the PPoly object describes multiple polynomials, the 

1354 return value is an object array whose each element is an 

1355 ndarray containing the roots. 

1356 

1357 See Also 

1358 -------- 

1359 PPoly.solve 

1360 """ 

1361 return self.solve(0, discontinuity, extrapolate) 

1362 

1363 @classmethod 

1364 def from_spline(cls, tck, extrapolate=None): 

1365 """ 

1366 Construct a piecewise polynomial from a spline 

1367 

1368 Parameters 

1369 ---------- 

1370 tck 

1371 A spline, as returned by `splrep` or a BSpline object. 

1372 extrapolate : bool or 'periodic', optional 

1373 If bool, determines whether to extrapolate to out-of-bounds points 

1374 based on first and last intervals, or to return NaNs. 

1375 If 'periodic', periodic extrapolation is used. Default is True. 

1376 

1377 Examples 

1378 -------- 

1379 Construct an interpolating spline and convert it to a `PPoly` instance  

1380 

1381 >>> import numpy as np 

1382 >>> from scipy.interpolate import splrep, PPoly 

1383 >>> x = np.linspace(0, 1, 11) 

1384 >>> y = np.sin(2*np.pi*x) 

1385 >>> tck = splrep(x, y, s=0) 

1386 >>> p = PPoly.from_spline(tck) 

1387 >>> isinstance(p, PPoly) 

1388 True 

1389 

1390 Note that this function only supports 1D splines out of the box. 

1391 

1392 If the ``tck`` object represents a parametric spline (e.g. constructed 

1393 by `splprep` or a `BSpline` with ``c.ndim > 1``), you will need to loop 

1394 over the dimensions manually. 

1395 

1396 >>> from scipy.interpolate import splprep, splev 

1397 >>> t = np.linspace(0, 1, 11) 

1398 >>> x = np.sin(2*np.pi*t) 

1399 >>> y = np.cos(2*np.pi*t) 

1400 >>> (t, c, k), u = splprep([x, y], s=0) 

1401 

1402 Note that ``c`` is a list of two arrays of length 11. 

1403 

1404 >>> unew = np.arange(0, 1.01, 0.01) 

1405 >>> out = splev(unew, (t, c, k)) 

1406 

1407 To convert this spline to the power basis, we convert each 

1408 component of the list of b-spline coefficients, ``c``, into the 

1409 corresponding cubic polynomial. 

1410 

1411 >>> polys = [PPoly.from_spline((t, cj, k)) for cj in c] 

1412 >>> polys[0].c.shape 

1413 (4, 14) 

1414 

1415 Note that the coefficients of the polynomials `polys` are in the 

1416 power basis and their dimensions reflect just that: here 4 is the order 

1417 (degree+1), and 14 is the number of intervals---which is nothing but 

1418 the length of the knot array of the original `tck` minus one. 

1419 

1420 Optionally, we can stack the components into a single `PPoly` along 

1421 the third dimension: 

1422 

1423 >>> cc = np.dstack([p.c for p in polys]) # has shape = (4, 14, 2) 

1424 >>> poly = PPoly(cc, polys[0].x) 

1425 >>> np.allclose(poly(unew).T, # note the transpose to match `splev` 

1426 ... out, atol=1e-15) 

1427 True 

1428 

1429 """ 

1430 if isinstance(tck, BSpline): 

1431 t, c, k = tck.tck 

1432 if extrapolate is None: 

1433 extrapolate = tck.extrapolate 

1434 else: 

1435 t, c, k = tck 

1436 

1437 cvals = np.empty((k + 1, len(t)-1), dtype=c.dtype) 

1438 for m in range(k, -1, -1): 

1439 y = _fitpack_py.splev(t[:-1], tck, der=m) 

1440 cvals[k - m, :] = y/spec.gamma(m+1) 

1441 

1442 return cls.construct_fast(cvals, t, extrapolate) 

1443 

1444 @classmethod 

1445 def from_bernstein_basis(cls, bp, extrapolate=None): 

1446 """ 

1447 Construct a piecewise polynomial in the power basis 

1448 from a polynomial in Bernstein basis. 

1449 

1450 Parameters 

1451 ---------- 

1452 bp : BPoly 

1453 A Bernstein basis polynomial, as created by BPoly 

1454 extrapolate : bool or 'periodic', optional 

1455 If bool, determines whether to extrapolate to out-of-bounds points 

1456 based on first and last intervals, or to return NaNs. 

1457 If 'periodic', periodic extrapolation is used. Default is True. 

1458 """ 

1459 if not isinstance(bp, BPoly): 

1460 raise TypeError(".from_bernstein_basis only accepts BPoly instances. " 

1461 "Got %s instead." % type(bp)) 

1462 

1463 dx = np.diff(bp.x) 

1464 k = bp.c.shape[0] - 1 # polynomial order 

1465 

1466 rest = (None,)*(bp.c.ndim-2) 

1467 

1468 c = np.zeros_like(bp.c) 

1469 for a in range(k+1): 

1470 factor = (-1)**a * comb(k, a) * bp.c[a] 

1471 for s in range(a, k+1): 

1472 val = comb(k-a, s-a) * (-1)**s 

1473 c[k-s] += factor * val / dx[(slice(None),)+rest]**s 

1474 

1475 if extrapolate is None: 

1476 extrapolate = bp.extrapolate 

1477 

1478 return cls.construct_fast(c, bp.x, extrapolate, bp.axis) 

1479 

1480 

1481class BPoly(_PPolyBase): 

1482 """Piecewise polynomial in terms of coefficients and breakpoints. 

1483 

1484 The polynomial between ``x[i]`` and ``x[i + 1]`` is written in the 

1485 Bernstein polynomial basis:: 

1486 

1487 S = sum(c[a, i] * b(a, k; x) for a in range(k+1)), 

1488 

1489 where ``k`` is the degree of the polynomial, and:: 

1490 

1491 b(a, k; x) = binom(k, a) * t**a * (1 - t)**(k - a), 

1492 

1493 with ``t = (x - x[i]) / (x[i+1] - x[i])`` and ``binom`` is the binomial 

1494 coefficient. 

1495 

1496 Parameters 

1497 ---------- 

1498 c : ndarray, shape (k, m, ...) 

1499 Polynomial coefficients, order `k` and `m` intervals 

1500 x : ndarray, shape (m+1,) 

1501 Polynomial breakpoints. Must be sorted in either increasing or 

1502 decreasing order. 

1503 extrapolate : bool, optional 

1504 If bool, determines whether to extrapolate to out-of-bounds points 

1505 based on first and last intervals, or to return NaNs. If 'periodic', 

1506 periodic extrapolation is used. Default is True. 

1507 axis : int, optional 

1508 Interpolation axis. Default is zero. 

1509 

1510 Attributes 

1511 ---------- 

1512 x : ndarray 

1513 Breakpoints. 

1514 c : ndarray 

1515 Coefficients of the polynomials. They are reshaped 

1516 to a 3-D array with the last dimension representing 

1517 the trailing dimensions of the original coefficient array. 

1518 axis : int 

1519 Interpolation axis. 

1520 

1521 Methods 

1522 ------- 

1523 __call__ 

1524 extend 

1525 derivative 

1526 antiderivative 

1527 integrate 

1528 construct_fast 

1529 from_power_basis 

1530 from_derivatives 

1531 

1532 See also 

1533 -------- 

1534 PPoly : piecewise polynomials in the power basis 

1535 

1536 Notes 

1537 ----- 

1538 Properties of Bernstein polynomials are well documented in the literature, 

1539 see for example [1]_ [2]_ [3]_. 

1540 

1541 References 

1542 ---------- 

1543 .. [1] https://en.wikipedia.org/wiki/Bernstein_polynomial 

1544 

1545 .. [2] Kenneth I. Joy, Bernstein polynomials, 

1546 http://www.idav.ucdavis.edu/education/CAGDNotes/Bernstein-Polynomials.pdf 

1547 

1548 .. [3] E. H. Doha, A. H. Bhrawy, and M. A. Saker, Boundary Value Problems, 

1549 vol 2011, article ID 829546, :doi:`10.1155/2011/829543`. 

1550 

1551 Examples 

1552 -------- 

1553 >>> from scipy.interpolate import BPoly 

1554 >>> x = [0, 1] 

1555 >>> c = [[1], [2], [3]] 

1556 >>> bp = BPoly(c, x) 

1557 

1558 This creates a 2nd order polynomial 

1559 

1560 .. math:: 

1561 

1562 B(x) = 1 \\times b_{0, 2}(x) + 2 \\times b_{1, 2}(x) + 3 \\times b_{2, 2}(x) \\\\ 

1563 = 1 \\times (1-x)^2 + 2 \\times 2 x (1 - x) + 3 \\times x^2 

1564 

1565 """ 

1566 

1567 def _evaluate(self, x, nu, extrapolate, out): 

1568 _ppoly.evaluate_bernstein( 

1569 self.c.reshape(self.c.shape[0], self.c.shape[1], -1), 

1570 self.x, x, nu, bool(extrapolate), out) 

1571 

1572 def derivative(self, nu=1): 

1573 """ 

1574 Construct a new piecewise polynomial representing the derivative. 

1575 

1576 Parameters 

1577 ---------- 

1578 nu : int, optional 

1579 Order of derivative to evaluate. Default is 1, i.e., compute the 

1580 first derivative. If negative, the antiderivative is returned. 

1581 

1582 Returns 

1583 ------- 

1584 bp : BPoly 

1585 Piecewise polynomial of order k - nu representing the derivative of 

1586 this polynomial. 

1587 

1588 """ 

1589 if nu < 0: 

1590 return self.antiderivative(-nu) 

1591 

1592 if nu > 1: 

1593 bp = self 

1594 for k in range(nu): 

1595 bp = bp.derivative() 

1596 return bp 

1597 

1598 # reduce order 

1599 if nu == 0: 

1600 c2 = self.c.copy() 

1601 else: 

1602 # For a polynomial 

1603 # B(x) = \sum_{a=0}^{k} c_a b_{a, k}(x), 

1604 # we use the fact that 

1605 # b'_{a, k} = k ( b_{a-1, k-1} - b_{a, k-1} ), 

1606 # which leads to 

1607 # B'(x) = \sum_{a=0}^{k-1} (c_{a+1} - c_a) b_{a, k-1} 

1608 # 

1609 # finally, for an interval [y, y + dy] with dy != 1, 

1610 # we need to correct for an extra power of dy 

1611 

1612 rest = (None,)*(self.c.ndim-2) 

1613 

1614 k = self.c.shape[0] - 1 

1615 dx = np.diff(self.x)[(None, slice(None))+rest] 

1616 c2 = k * np.diff(self.c, axis=0) / dx 

1617 

1618 if c2.shape[0] == 0: 

1619 # derivative of order 0 is zero 

1620 c2 = np.zeros((1,) + c2.shape[1:], dtype=c2.dtype) 

1621 

1622 # construct a compatible polynomial 

1623 return self.construct_fast(c2, self.x, self.extrapolate, self.axis) 

1624 

1625 def antiderivative(self, nu=1): 

1626 """ 

1627 Construct a new piecewise polynomial representing the antiderivative. 

1628 

1629 Parameters 

1630 ---------- 

1631 nu : int, optional 

1632 Order of antiderivative to evaluate. Default is 1, i.e., compute 

1633 the first integral. If negative, the derivative is returned. 

1634 

1635 Returns 

1636 ------- 

1637 bp : BPoly 

1638 Piecewise polynomial of order k + nu representing the 

1639 antiderivative of this polynomial. 

1640 

1641 Notes 

1642 ----- 

1643 If antiderivative is computed and ``self.extrapolate='periodic'``, 

1644 it will be set to False for the returned instance. This is done because 

1645 the antiderivative is no longer periodic and its correct evaluation 

1646 outside of the initially given x interval is difficult. 

1647 """ 

1648 if nu <= 0: 

1649 return self.derivative(-nu) 

1650 

1651 if nu > 1: 

1652 bp = self 

1653 for k in range(nu): 

1654 bp = bp.antiderivative() 

1655 return bp 

1656 

1657 # Construct the indefinite integrals on individual intervals 

1658 c, x = self.c, self.x 

1659 k = c.shape[0] 

1660 c2 = np.zeros((k+1,) + c.shape[1:], dtype=c.dtype) 

1661 

1662 c2[1:, ...] = np.cumsum(c, axis=0) / k 

1663 delta = x[1:] - x[:-1] 

1664 c2 *= delta[(None, slice(None)) + (None,)*(c.ndim-2)] 

1665 

1666 # Now fix continuity: on the very first interval, take the integration 

1667 # constant to be zero; on an interval [x_j, x_{j+1}) with j>0, 

1668 # the integration constant is then equal to the jump of the `bp` at x_j. 

1669 # The latter is given by the coefficient of B_{n+1, n+1} 

1670 # *on the previous interval* (other B. polynomials are zero at the 

1671 # breakpoint). Finally, use the fact that BPs form a partition of unity. 

1672 c2[:,1:] += np.cumsum(c2[k, :], axis=0)[:-1] 

1673 

1674 if self.extrapolate == 'periodic': 

1675 extrapolate = False 

1676 else: 

1677 extrapolate = self.extrapolate 

1678 

1679 return self.construct_fast(c2, x, extrapolate, axis=self.axis) 

1680 

1681 def integrate(self, a, b, extrapolate=None): 

1682 """ 

1683 Compute a definite integral over a piecewise polynomial. 

1684 

1685 Parameters 

1686 ---------- 

1687 a : float 

1688 Lower integration bound 

1689 b : float 

1690 Upper integration bound 

1691 extrapolate : {bool, 'periodic', None}, optional 

1692 Whether to extrapolate to out-of-bounds points based on first 

1693 and last intervals, or to return NaNs. If 'periodic', periodic 

1694 extrapolation is used. If None (default), use `self.extrapolate`. 

1695 

1696 Returns 

1697 ------- 

1698 array_like 

1699 Definite integral of the piecewise polynomial over [a, b] 

1700 

1701 """ 

1702 # XXX: can probably use instead the fact that 

1703 # \int_0^{1} B_{j, n}(x) \dx = 1/(n+1) 

1704 ib = self.antiderivative() 

1705 if extrapolate is None: 

1706 extrapolate = self.extrapolate 

1707 

1708 # ib.extrapolate shouldn't be 'periodic', it is converted to 

1709 # False for 'periodic. in antiderivative() call. 

1710 if extrapolate != 'periodic': 

1711 ib.extrapolate = extrapolate 

1712 

1713 if extrapolate == 'periodic': 

1714 # Split the integral into the part over period (can be several 

1715 # of them) and the remaining part. 

1716 

1717 # For simplicity and clarity convert to a <= b case. 

1718 if a <= b: 

1719 sign = 1 

1720 else: 

1721 a, b = b, a 

1722 sign = -1 

1723 

1724 xs, xe = self.x[0], self.x[-1] 

1725 period = xe - xs 

1726 interval = b - a 

1727 n_periods, left = divmod(interval, period) 

1728 res = n_periods * (ib(xe) - ib(xs)) 

1729 

1730 # Map a and b to [xs, xe]. 

1731 a = xs + (a - xs) % period 

1732 b = a + left 

1733 

1734 # If b <= xe then we need to integrate over [a, b], otherwise 

1735 # over [a, xe] and from xs to what is remained. 

1736 if b <= xe: 

1737 res += ib(b) - ib(a) 

1738 else: 

1739 res += ib(xe) - ib(a) + ib(xs + left + a - xe) - ib(xs) 

1740 

1741 return sign * res 

1742 else: 

1743 return ib(b) - ib(a) 

1744 

1745 def extend(self, c, x): 

1746 k = max(self.c.shape[0], c.shape[0]) 

1747 self.c = self._raise_degree(self.c, k - self.c.shape[0]) 

1748 c = self._raise_degree(c, k - c.shape[0]) 

1749 return _PPolyBase.extend(self, c, x) 

1750 extend.__doc__ = _PPolyBase.extend.__doc__ 

1751 

1752 @classmethod 

1753 def from_power_basis(cls, pp, extrapolate=None): 

1754 """ 

1755 Construct a piecewise polynomial in Bernstein basis 

1756 from a power basis polynomial. 

1757 

1758 Parameters 

1759 ---------- 

1760 pp : PPoly 

1761 A piecewise polynomial in the power basis 

1762 extrapolate : bool or 'periodic', optional 

1763 If bool, determines whether to extrapolate to out-of-bounds points 

1764 based on first and last intervals, or to return NaNs. 

1765 If 'periodic', periodic extrapolation is used. Default is True. 

1766 """ 

1767 if not isinstance(pp, PPoly): 

1768 raise TypeError(".from_power_basis only accepts PPoly instances. " 

1769 "Got %s instead." % type(pp)) 

1770 

1771 dx = np.diff(pp.x) 

1772 k = pp.c.shape[0] - 1 # polynomial order 

1773 

1774 rest = (None,)*(pp.c.ndim-2) 

1775 

1776 c = np.zeros_like(pp.c) 

1777 for a in range(k+1): 

1778 factor = pp.c[a] / comb(k, k-a) * dx[(slice(None),)+rest]**(k-a) 

1779 for j in range(k-a, k+1): 

1780 c[j] += factor * comb(j, k-a) 

1781 

1782 if extrapolate is None: 

1783 extrapolate = pp.extrapolate 

1784 

1785 return cls.construct_fast(c, pp.x, extrapolate, pp.axis) 

1786 

1787 @classmethod 

1788 def from_derivatives(cls, xi, yi, orders=None, extrapolate=None): 

1789 """Construct a piecewise polynomial in the Bernstein basis, 

1790 compatible with the specified values and derivatives at breakpoints. 

1791 

1792 Parameters 

1793 ---------- 

1794 xi : array_like 

1795 sorted 1-D array of x-coordinates 

1796 yi : array_like or list of array_likes 

1797 ``yi[i][j]`` is the ``j``th derivative known at ``xi[i]`` 

1798 orders : None or int or array_like of ints. Default: None. 

1799 Specifies the degree of local polynomials. If not None, some 

1800 derivatives are ignored. 

1801 extrapolate : bool or 'periodic', optional 

1802 If bool, determines whether to extrapolate to out-of-bounds points 

1803 based on first and last intervals, or to return NaNs. 

1804 If 'periodic', periodic extrapolation is used. Default is True. 

1805 

1806 Notes 

1807 ----- 

1808 If ``k`` derivatives are specified at a breakpoint ``x``, the 

1809 constructed polynomial is exactly ``k`` times continuously 

1810 differentiable at ``x``, unless the ``order`` is provided explicitly. 

1811 In the latter case, the smoothness of the polynomial at 

1812 the breakpoint is controlled by the ``order``. 

1813 

1814 Deduces the number of derivatives to match at each end 

1815 from ``order`` and the number of derivatives available. If 

1816 possible it uses the same number of derivatives from 

1817 each end; if the number is odd it tries to take the 

1818 extra one from y2. In any case if not enough derivatives 

1819 are available at one end or another it draws enough to 

1820 make up the total from the other end. 

1821 

1822 If the order is too high and not enough derivatives are available, 

1823 an exception is raised. 

1824 

1825 Examples 

1826 -------- 

1827 

1828 >>> from scipy.interpolate import BPoly 

1829 >>> BPoly.from_derivatives([0, 1], [[1, 2], [3, 4]]) 

1830 

1831 Creates a polynomial `f(x)` of degree 3, defined on `[0, 1]` 

1832 such that `f(0) = 1, df/dx(0) = 2, f(1) = 3, df/dx(1) = 4` 

1833 

1834 >>> BPoly.from_derivatives([0, 1, 2], [[0, 1], [0], [2]]) 

1835 

1836 Creates a piecewise polynomial `f(x)`, such that 

1837 `f(0) = f(1) = 0`, `f(2) = 2`, and `df/dx(0) = 1`. 

1838 Based on the number of derivatives provided, the order of the 

1839 local polynomials is 2 on `[0, 1]` and 1 on `[1, 2]`. 

1840 Notice that no restriction is imposed on the derivatives at 

1841 ``x = 1`` and ``x = 2``. 

1842 

1843 Indeed, the explicit form of the polynomial is:: 

1844 

1845 f(x) = | x * (1 - x), 0 <= x < 1 

1846 | 2 * (x - 1), 1 <= x <= 2 

1847 

1848 So that f'(1-0) = -1 and f'(1+0) = 2 

1849 

1850 """ 

1851 xi = np.asarray(xi) 

1852 if len(xi) != len(yi): 

1853 raise ValueError("xi and yi need to have the same length") 

1854 if np.any(xi[1:] - xi[:1] <= 0): 

1855 raise ValueError("x coordinates are not in increasing order") 

1856 

1857 # number of intervals 

1858 m = len(xi) - 1 

1859 

1860 # global poly order is k-1, local orders are <=k and can vary 

1861 try: 

1862 k = max(len(yi[i]) + len(yi[i+1]) for i in range(m)) 

1863 except TypeError as e: 

1864 raise ValueError( 

1865 "Using a 1-D array for y? Please .reshape(-1, 1)." 

1866 ) from e 

1867 

1868 if orders is None: 

1869 orders = [None] * m 

1870 else: 

1871 if isinstance(orders, (int, np.integer)): 

1872 orders = [orders] * m 

1873 k = max(k, max(orders)) 

1874 

1875 if any(o <= 0 for o in orders): 

1876 raise ValueError("Orders must be positive.") 

1877 

1878 c = [] 

1879 for i in range(m): 

1880 y1, y2 = yi[i], yi[i+1] 

1881 if orders[i] is None: 

1882 n1, n2 = len(y1), len(y2) 

1883 else: 

1884 n = orders[i]+1 

1885 n1 = min(n//2, len(y1)) 

1886 n2 = min(n - n1, len(y2)) 

1887 n1 = min(n - n2, len(y2)) 

1888 if n1+n2 != n: 

1889 mesg = ("Point %g has %d derivatives, point %g" 

1890 " has %d derivatives, but order %d requested" % ( 

1891 xi[i], len(y1), xi[i+1], len(y2), orders[i])) 

1892 raise ValueError(mesg) 

1893 

1894 if not (n1 <= len(y1) and n2 <= len(y2)): 

1895 raise ValueError("`order` input incompatible with" 

1896 " length y1 or y2.") 

1897 

1898 b = BPoly._construct_from_derivatives(xi[i], xi[i+1], 

1899 y1[:n1], y2[:n2]) 

1900 if len(b) < k: 

1901 b = BPoly._raise_degree(b, k - len(b)) 

1902 c.append(b) 

1903 

1904 c = np.asarray(c) 

1905 return cls(c.swapaxes(0, 1), xi, extrapolate) 

1906 

1907 @staticmethod 

1908 def _construct_from_derivatives(xa, xb, ya, yb): 

1909 r"""Compute the coefficients of a polynomial in the Bernstein basis 

1910 given the values and derivatives at the edges. 

1911 

1912 Return the coefficients of a polynomial in the Bernstein basis 

1913 defined on ``[xa, xb]`` and having the values and derivatives at the 

1914 endpoints `xa` and `xb` as specified by `ya`` and `yb`. 

1915 The polynomial constructed is of the minimal possible degree, i.e., 

1916 if the lengths of `ya` and `yb` are `na` and `nb`, the degree 

1917 of the polynomial is ``na + nb - 1``. 

1918 

1919 Parameters 

1920 ---------- 

1921 xa : float 

1922 Left-hand end point of the interval 

1923 xb : float 

1924 Right-hand end point of the interval 

1925 ya : array_like 

1926 Derivatives at `xa`. `ya[0]` is the value of the function, and 

1927 `ya[i]` for ``i > 0`` is the value of the ``i``th derivative. 

1928 yb : array_like 

1929 Derivatives at `xb`. 

1930 

1931 Returns 

1932 ------- 

1933 array 

1934 coefficient array of a polynomial having specified derivatives 

1935 

1936 Notes 

1937 ----- 

1938 This uses several facts from life of Bernstein basis functions. 

1939 First of all, 

1940 

1941 .. math:: b'_{a, n} = n (b_{a-1, n-1} - b_{a, n-1}) 

1942 

1943 If B(x) is a linear combination of the form 

1944 

1945 .. math:: B(x) = \sum_{a=0}^{n} c_a b_{a, n}, 

1946 

1947 then :math: B'(x) = n \sum_{a=0}^{n-1} (c_{a+1} - c_{a}) b_{a, n-1}. 

1948 Iterating the latter one, one finds for the q-th derivative 

1949 

1950 .. math:: B^{q}(x) = n!/(n-q)! \sum_{a=0}^{n-q} Q_a b_{a, n-q}, 

1951 

1952 with 

1953 

1954 .. math:: Q_a = \sum_{j=0}^{q} (-)^{j+q} comb(q, j) c_{j+a} 

1955 

1956 This way, only `a=0` contributes to :math: `B^{q}(x = xa)`, and 

1957 `c_q` are found one by one by iterating `q = 0, ..., na`. 

1958 

1959 At ``x = xb`` it's the same with ``a = n - q``. 

1960 

1961 """ 

1962 ya, yb = np.asarray(ya), np.asarray(yb) 

1963 if ya.shape[1:] != yb.shape[1:]: 

1964 raise ValueError('Shapes of ya {} and yb {} are incompatible' 

1965 .format(ya.shape, yb.shape)) 

1966 

1967 dta, dtb = ya.dtype, yb.dtype 

1968 if (np.issubdtype(dta, np.complexfloating) or 

1969 np.issubdtype(dtb, np.complexfloating)): 

1970 dt = np.complex_ 

1971 else: 

1972 dt = np.float_ 

1973 

1974 na, nb = len(ya), len(yb) 

1975 n = na + nb 

1976 

1977 c = np.empty((na+nb,) + ya.shape[1:], dtype=dt) 

1978 

1979 # compute coefficients of a polynomial degree na+nb-1 

1980 # walk left-to-right 

1981 for q in range(0, na): 

1982 c[q] = ya[q] / spec.poch(n - q, q) * (xb - xa)**q 

1983 for j in range(0, q): 

1984 c[q] -= (-1)**(j+q) * comb(q, j) * c[j] 

1985 

1986 # now walk right-to-left 

1987 for q in range(0, nb): 

1988 c[-q-1] = yb[q] / spec.poch(n - q, q) * (-1)**q * (xb - xa)**q 

1989 for j in range(0, q): 

1990 c[-q-1] -= (-1)**(j+1) * comb(q, j+1) * c[-q+j] 

1991 

1992 return c 

1993 

1994 @staticmethod 

1995 def _raise_degree(c, d): 

1996 r"""Raise a degree of a polynomial in the Bernstein basis. 

1997 

1998 Given the coefficients of a polynomial degree `k`, return (the 

1999 coefficients of) the equivalent polynomial of degree `k+d`. 

2000 

2001 Parameters 

2002 ---------- 

2003 c : array_like 

2004 coefficient array, 1-D 

2005 d : integer 

2006 

2007 Returns 

2008 ------- 

2009 array 

2010 coefficient array, 1-D array of length `c.shape[0] + d` 

2011 

2012 Notes 

2013 ----- 

2014 This uses the fact that a Bernstein polynomial `b_{a, k}` can be 

2015 identically represented as a linear combination of polynomials of 

2016 a higher degree `k+d`: 

2017 

2018 .. math:: b_{a, k} = comb(k, a) \sum_{j=0}^{d} b_{a+j, k+d} \ 

2019 comb(d, j) / comb(k+d, a+j) 

2020 

2021 """ 

2022 if d == 0: 

2023 return c 

2024 

2025 k = c.shape[0] - 1 

2026 out = np.zeros((c.shape[0] + d,) + c.shape[1:], dtype=c.dtype) 

2027 

2028 for a in range(c.shape[0]): 

2029 f = c[a] * comb(k, a) 

2030 for j in range(d+1): 

2031 out[a+j] += f * comb(d, j) / comb(k+d, a+j) 

2032 return out 

2033 

2034 

2035class NdPPoly: 

2036 """ 

2037 Piecewise tensor product polynomial 

2038 

2039 The value at point ``xp = (x', y', z', ...)`` is evaluated by first 

2040 computing the interval indices `i` such that:: 

2041 

2042 x[0][i[0]] <= x' < x[0][i[0]+1] 

2043 x[1][i[1]] <= y' < x[1][i[1]+1] 

2044 ... 

2045 

2046 and then computing:: 

2047 

2048 S = sum(c[k0-m0-1,...,kn-mn-1,i[0],...,i[n]] 

2049 * (xp[0] - x[0][i[0]])**m0 

2050 * ... 

2051 * (xp[n] - x[n][i[n]])**mn 

2052 for m0 in range(k[0]+1) 

2053 ... 

2054 for mn in range(k[n]+1)) 

2055 

2056 where ``k[j]`` is the degree of the polynomial in dimension j. This 

2057 representation is the piecewise multivariate power basis. 

2058 

2059 Parameters 

2060 ---------- 

2061 c : ndarray, shape (k0, ..., kn, m0, ..., mn, ...) 

2062 Polynomial coefficients, with polynomial order `kj` and 

2063 `mj+1` intervals for each dimension `j`. 

2064 x : ndim-tuple of ndarrays, shapes (mj+1,) 

2065 Polynomial breakpoints for each dimension. These must be 

2066 sorted in increasing order. 

2067 extrapolate : bool, optional 

2068 Whether to extrapolate to out-of-bounds points based on first 

2069 and last intervals, or to return NaNs. Default: True. 

2070 

2071 Attributes 

2072 ---------- 

2073 x : tuple of ndarrays 

2074 Breakpoints. 

2075 c : ndarray 

2076 Coefficients of the polynomials. 

2077 

2078 Methods 

2079 ------- 

2080 __call__ 

2081 derivative 

2082 antiderivative 

2083 integrate 

2084 integrate_1d 

2085 construct_fast 

2086 

2087 See also 

2088 -------- 

2089 PPoly : piecewise polynomials in 1D 

2090 

2091 Notes 

2092 ----- 

2093 High-order polynomials in the power basis can be numerically 

2094 unstable. 

2095 

2096 """ 

2097 

2098 def __init__(self, c, x, extrapolate=None): 

2099 self.x = tuple(np.ascontiguousarray(v, dtype=np.float64) for v in x) 

2100 self.c = np.asarray(c) 

2101 if extrapolate is None: 

2102 extrapolate = True 

2103 self.extrapolate = bool(extrapolate) 

2104 

2105 ndim = len(self.x) 

2106 if any(v.ndim != 1 for v in self.x): 

2107 raise ValueError("x arrays must all be 1-dimensional") 

2108 if any(v.size < 2 for v in self.x): 

2109 raise ValueError("x arrays must all contain at least 2 points") 

2110 if c.ndim < 2*ndim: 

2111 raise ValueError("c must have at least 2*len(x) dimensions") 

2112 if any(np.any(v[1:] - v[:-1] < 0) for v in self.x): 

2113 raise ValueError("x-coordinates are not in increasing order") 

2114 if any(a != b.size - 1 for a, b in zip(c.shape[ndim:2*ndim], self.x)): 

2115 raise ValueError("x and c do not agree on the number of intervals") 

2116 

2117 dtype = self._get_dtype(self.c.dtype) 

2118 self.c = np.ascontiguousarray(self.c, dtype=dtype) 

2119 

2120 @classmethod 

2121 def construct_fast(cls, c, x, extrapolate=None): 

2122 """ 

2123 Construct the piecewise polynomial without making checks. 

2124 

2125 Takes the same parameters as the constructor. Input arguments 

2126 ``c`` and ``x`` must be arrays of the correct shape and type. The 

2127 ``c`` array can only be of dtypes float and complex, and ``x`` 

2128 array must have dtype float. 

2129 

2130 """ 

2131 self = object.__new__(cls) 

2132 self.c = c 

2133 self.x = x 

2134 if extrapolate is None: 

2135 extrapolate = True 

2136 self.extrapolate = extrapolate 

2137 return self 

2138 

2139 def _get_dtype(self, dtype): 

2140 if np.issubdtype(dtype, np.complexfloating) \ 

2141 or np.issubdtype(self.c.dtype, np.complexfloating): 

2142 return np.complex_ 

2143 else: 

2144 return np.float_ 

2145 

2146 def _ensure_c_contiguous(self): 

2147 if not self.c.flags.c_contiguous: 

2148 self.c = self.c.copy() 

2149 if not isinstance(self.x, tuple): 

2150 self.x = tuple(self.x) 

2151 

2152 def __call__(self, x, nu=None, extrapolate=None): 

2153 """ 

2154 Evaluate the piecewise polynomial or its derivative 

2155 

2156 Parameters 

2157 ---------- 

2158 x : array-like 

2159 Points to evaluate the interpolant at. 

2160 nu : tuple, optional 

2161 Orders of derivatives to evaluate. Each must be non-negative. 

2162 extrapolate : bool, optional 

2163 Whether to extrapolate to out-of-bounds points based on first 

2164 and last intervals, or to return NaNs. 

2165 

2166 Returns 

2167 ------- 

2168 y : array-like 

2169 Interpolated values. Shape is determined by replacing 

2170 the interpolation axis in the original array with the shape of x. 

2171 

2172 Notes 

2173 ----- 

2174 Derivatives are evaluated piecewise for each polynomial 

2175 segment, even if the polynomial is not differentiable at the 

2176 breakpoints. The polynomial intervals are considered half-open, 

2177 ``[a, b)``, except for the last interval which is closed 

2178 ``[a, b]``. 

2179 

2180 """ 

2181 if extrapolate is None: 

2182 extrapolate = self.extrapolate 

2183 else: 

2184 extrapolate = bool(extrapolate) 

2185 

2186 ndim = len(self.x) 

2187 

2188 x = _ndim_coords_from_arrays(x) 

2189 x_shape = x.shape 

2190 x = np.ascontiguousarray(x.reshape(-1, x.shape[-1]), dtype=np.float_) 

2191 

2192 if nu is None: 

2193 nu = np.zeros((ndim,), dtype=np.intc) 

2194 else: 

2195 nu = np.asarray(nu, dtype=np.intc) 

2196 if nu.ndim != 1 or nu.shape[0] != ndim: 

2197 raise ValueError("invalid number of derivative orders nu") 

2198 

2199 dim1 = prod(self.c.shape[:ndim]) 

2200 dim2 = prod(self.c.shape[ndim:2*ndim]) 

2201 dim3 = prod(self.c.shape[2*ndim:]) 

2202 ks = np.array(self.c.shape[:ndim], dtype=np.intc) 

2203 

2204 out = np.empty((x.shape[0], dim3), dtype=self.c.dtype) 

2205 self._ensure_c_contiguous() 

2206 

2207 _ppoly.evaluate_nd(self.c.reshape(dim1, dim2, dim3), 

2208 self.x, 

2209 ks, 

2210 x, 

2211 nu, 

2212 bool(extrapolate), 

2213 out) 

2214 

2215 return out.reshape(x_shape[:-1] + self.c.shape[2*ndim:]) 

2216 

2217 def _derivative_inplace(self, nu, axis): 

2218 """ 

2219 Compute 1-D derivative along a selected dimension in-place 

2220 May result to non-contiguous c array. 

2221 """ 

2222 if nu < 0: 

2223 return self._antiderivative_inplace(-nu, axis) 

2224 

2225 ndim = len(self.x) 

2226 axis = axis % ndim 

2227 

2228 # reduce order 

2229 if nu == 0: 

2230 # noop 

2231 return 

2232 else: 

2233 sl = [slice(None)]*ndim 

2234 sl[axis] = slice(None, -nu, None) 

2235 c2 = self.c[tuple(sl)] 

2236 

2237 if c2.shape[axis] == 0: 

2238 # derivative of order 0 is zero 

2239 shp = list(c2.shape) 

2240 shp[axis] = 1 

2241 c2 = np.zeros(shp, dtype=c2.dtype) 

2242 

2243 # multiply by the correct rising factorials 

2244 factor = spec.poch(np.arange(c2.shape[axis], 0, -1), nu) 

2245 sl = [None]*c2.ndim 

2246 sl[axis] = slice(None) 

2247 c2 *= factor[tuple(sl)] 

2248 

2249 self.c = c2 

2250 

2251 def _antiderivative_inplace(self, nu, axis): 

2252 """ 

2253 Compute 1-D antiderivative along a selected dimension 

2254 May result to non-contiguous c array. 

2255 """ 

2256 if nu <= 0: 

2257 return self._derivative_inplace(-nu, axis) 

2258 

2259 ndim = len(self.x) 

2260 axis = axis % ndim 

2261 

2262 perm = list(range(ndim)) 

2263 perm[0], perm[axis] = perm[axis], perm[0] 

2264 perm = perm + list(range(ndim, self.c.ndim)) 

2265 

2266 c = self.c.transpose(perm) 

2267 

2268 c2 = np.zeros((c.shape[0] + nu,) + c.shape[1:], 

2269 dtype=c.dtype) 

2270 c2[:-nu] = c 

2271 

2272 # divide by the correct rising factorials 

2273 factor = spec.poch(np.arange(c.shape[0], 0, -1), nu) 

2274 c2[:-nu] /= factor[(slice(None),) + (None,)*(c.ndim-1)] 

2275 

2276 # fix continuity of added degrees of freedom 

2277 perm2 = list(range(c2.ndim)) 

2278 perm2[1], perm2[ndim+axis] = perm2[ndim+axis], perm2[1] 

2279 

2280 c2 = c2.transpose(perm2) 

2281 c2 = c2.copy() 

2282 _ppoly.fix_continuity(c2.reshape(c2.shape[0], c2.shape[1], -1), 

2283 self.x[axis], nu-1) 

2284 

2285 c2 = c2.transpose(perm2) 

2286 c2 = c2.transpose(perm) 

2287 

2288 # Done 

2289 self.c = c2 

2290 

2291 def derivative(self, nu): 

2292 """ 

2293 Construct a new piecewise polynomial representing the derivative. 

2294 

2295 Parameters 

2296 ---------- 

2297 nu : ndim-tuple of int 

2298 Order of derivatives to evaluate for each dimension. 

2299 If negative, the antiderivative is returned. 

2300 

2301 Returns 

2302 ------- 

2303 pp : NdPPoly 

2304 Piecewise polynomial of orders (k[0] - nu[0], ..., k[n] - nu[n]) 

2305 representing the derivative of this polynomial. 

2306 

2307 Notes 

2308 ----- 

2309 Derivatives are evaluated piecewise for each polynomial 

2310 segment, even if the polynomial is not differentiable at the 

2311 breakpoints. The polynomial intervals in each dimension are 

2312 considered half-open, ``[a, b)``, except for the last interval 

2313 which is closed ``[a, b]``. 

2314 

2315 """ 

2316 p = self.construct_fast(self.c.copy(), self.x, self.extrapolate) 

2317 

2318 for axis, n in enumerate(nu): 

2319 p._derivative_inplace(n, axis) 

2320 

2321 p._ensure_c_contiguous() 

2322 return p 

2323 

2324 def antiderivative(self, nu): 

2325 """ 

2326 Construct a new piecewise polynomial representing the antiderivative. 

2327 

2328 Antiderivative is also the indefinite integral of the function, 

2329 and derivative is its inverse operation. 

2330 

2331 Parameters 

2332 ---------- 

2333 nu : ndim-tuple of int 

2334 Order of derivatives to evaluate for each dimension. 

2335 If negative, the derivative is returned. 

2336 

2337 Returns 

2338 ------- 

2339 pp : PPoly 

2340 Piecewise polynomial of order k2 = k + n representing 

2341 the antiderivative of this polynomial. 

2342 

2343 Notes 

2344 ----- 

2345 The antiderivative returned by this function is continuous and 

2346 continuously differentiable to order n-1, up to floating point 

2347 rounding error. 

2348 

2349 """ 

2350 p = self.construct_fast(self.c.copy(), self.x, self.extrapolate) 

2351 

2352 for axis, n in enumerate(nu): 

2353 p._antiderivative_inplace(n, axis) 

2354 

2355 p._ensure_c_contiguous() 

2356 return p 

2357 

2358 def integrate_1d(self, a, b, axis, extrapolate=None): 

2359 r""" 

2360 Compute NdPPoly representation for one dimensional definite integral 

2361 

2362 The result is a piecewise polynomial representing the integral: 

2363 

2364 .. math:: 

2365 

2366 p(y, z, ...) = \int_a^b dx\, p(x, y, z, ...) 

2367 

2368 where the dimension integrated over is specified with the 

2369 `axis` parameter. 

2370 

2371 Parameters 

2372 ---------- 

2373 a, b : float 

2374 Lower and upper bound for integration. 

2375 axis : int 

2376 Dimension over which to compute the 1-D integrals 

2377 extrapolate : bool, optional 

2378 Whether to extrapolate to out-of-bounds points based on first 

2379 and last intervals, or to return NaNs. 

2380 

2381 Returns 

2382 ------- 

2383 ig : NdPPoly or array-like 

2384 Definite integral of the piecewise polynomial over [a, b]. 

2385 If the polynomial was 1D, an array is returned, 

2386 otherwise, an NdPPoly object. 

2387 

2388 """ 

2389 if extrapolate is None: 

2390 extrapolate = self.extrapolate 

2391 else: 

2392 extrapolate = bool(extrapolate) 

2393 

2394 ndim = len(self.x) 

2395 axis = int(axis) % ndim 

2396 

2397 # reuse 1-D integration routines 

2398 c = self.c 

2399 swap = list(range(c.ndim)) 

2400 swap.insert(0, swap[axis]) 

2401 del swap[axis + 1] 

2402 swap.insert(1, swap[ndim + axis]) 

2403 del swap[ndim + axis + 1] 

2404 

2405 c = c.transpose(swap) 

2406 p = PPoly.construct_fast(c.reshape(c.shape[0], c.shape[1], -1), 

2407 self.x[axis], 

2408 extrapolate=extrapolate) 

2409 out = p.integrate(a, b, extrapolate=extrapolate) 

2410 

2411 # Construct result 

2412 if ndim == 1: 

2413 return out.reshape(c.shape[2:]) 

2414 else: 

2415 c = out.reshape(c.shape[2:]) 

2416 x = self.x[:axis] + self.x[axis+1:] 

2417 return self.construct_fast(c, x, extrapolate=extrapolate) 

2418 

2419 def integrate(self, ranges, extrapolate=None): 

2420 """ 

2421 Compute a definite integral over a piecewise polynomial. 

2422 

2423 Parameters 

2424 ---------- 

2425 ranges : ndim-tuple of 2-tuples float 

2426 Sequence of lower and upper bounds for each dimension, 

2427 ``[(a[0], b[0]), ..., (a[ndim-1], b[ndim-1])]`` 

2428 extrapolate : bool, optional 

2429 Whether to extrapolate to out-of-bounds points based on first 

2430 and last intervals, or to return NaNs. 

2431 

2432 Returns 

2433 ------- 

2434 ig : array_like 

2435 Definite integral of the piecewise polynomial over 

2436 [a[0], b[0]] x ... x [a[ndim-1], b[ndim-1]] 

2437 

2438 """ 

2439 

2440 ndim = len(self.x) 

2441 

2442 if extrapolate is None: 

2443 extrapolate = self.extrapolate 

2444 else: 

2445 extrapolate = bool(extrapolate) 

2446 

2447 if not hasattr(ranges, '__len__') or len(ranges) != ndim: 

2448 raise ValueError("Range not a sequence of correct length") 

2449 

2450 self._ensure_c_contiguous() 

2451 

2452 # Reuse 1D integration routine 

2453 c = self.c 

2454 for n, (a, b) in enumerate(ranges): 

2455 swap = list(range(c.ndim)) 

2456 swap.insert(1, swap[ndim - n]) 

2457 del swap[ndim - n + 1] 

2458 

2459 c = c.transpose(swap) 

2460 

2461 p = PPoly.construct_fast(c, self.x[n], extrapolate=extrapolate) 

2462 out = p.integrate(a, b, extrapolate=extrapolate) 

2463 c = out.reshape(c.shape[2:]) 

2464 

2465 return c