Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/scipy/interpolate/interpolate.py : 10%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1__all__ = ['interp1d', 'interp2d', 'lagrange', 'PPoly', 'BPoly', 'NdPPoly',
2 'RegularGridInterpolator', 'interpn']
4import itertools
5import warnings
6import functools
7import operator
9import numpy as np
10from numpy import (array, transpose, searchsorted, atleast_1d, atleast_2d,
11 ravel, poly1d, asarray, intp)
13import scipy.special as spec
14from scipy.special import comb
15from scipy._lib._util import prod
17from . import fitpack
18from . import dfitpack
19from . import _fitpack
20from .polyint import _Interpolator1D
21from . import _ppoly
22from .fitpack2 import RectBivariateSpline
23from .interpnd import _ndim_coords_from_arrays
24from ._bsplines import make_interp_spline, BSpline
27def lagrange(x, w):
28 r"""
29 Return a Lagrange interpolating polynomial.
31 Given two 1-D arrays `x` and `w,` returns the Lagrange interpolating
32 polynomial through the points ``(x, w)``.
34 Warning: This implementation is numerically unstable. Do not expect to
35 be able to use more than about 20 points even if they are chosen optimally.
37 Parameters
38 ----------
39 x : array_like
40 `x` represents the x-coordinates of a set of datapoints.
41 w : array_like
42 `w` represents the y-coordinates of a set of datapoints, i.e., f(`x`).
44 Returns
45 -------
46 lagrange : `numpy.poly1d` instance
47 The Lagrange interpolating polynomial.
49 Examples
50 --------
51 Interpolate :math:`f(x) = x^3` by 3 points.
53 >>> from scipy.interpolate import lagrange
54 >>> x = np.array([0, 1, 2])
55 >>> y = x**3
56 >>> poly = lagrange(x, y)
58 Since there are only 3 points, Lagrange polynomial has degree 2. Explicitly,
59 it is given by
61 .. math::
63 \begin{aligned}
64 L(x) &= 1\times \frac{x (x - 2)}{-1} + 8\times \frac{x (x-1)}{2} \\
65 &= x (-2 + 3x)
66 \end{aligned}
68 >>> from numpy.polynomial.polynomial import Polynomial
69 >>> Polynomial(poly).coef
70 array([ 3., -2., 0.])
72 """
74 M = len(x)
75 p = poly1d(0.0)
76 for j in range(M):
77 pt = poly1d(w[j])
78 for k in range(M):
79 if k == j:
80 continue
81 fac = x[j]-x[k]
82 pt *= poly1d([1.0, -x[k]])/fac
83 p += pt
84 return p
87# !! Need to find argument for keeping initialize. If it isn't
88# !! found, get rid of it!
91class interp2d(object):
92 """
93 interp2d(x, y, z, kind='linear', copy=True, bounds_error=False,
94 fill_value=None)
96 Interpolate over a 2-D grid.
98 `x`, `y` and `z` are arrays of values used to approximate some function
99 f: ``z = f(x, y)``. This class returns a function whose call method uses
100 spline interpolation to find the value of new points.
102 If `x` and `y` represent a regular grid, consider using
103 RectBivariateSpline.
105 Note that calling `interp2d` with NaNs present in input values results in
106 undefined behaviour.
108 Methods
109 -------
110 __call__
112 Parameters
113 ----------
114 x, y : array_like
115 Arrays defining the data point coordinates.
117 If the points lie on a regular grid, `x` can specify the column
118 coordinates and `y` the row coordinates, for example::
120 >>> x = [0,1,2]; y = [0,3]; z = [[1,2,3], [4,5,6]]
122 Otherwise, `x` and `y` must specify the full coordinates for each
123 point, for example::
125 >>> x = [0,1,2,0,1,2]; y = [0,0,0,3,3,3]; z = [1,2,3,4,5,6]
127 If `x` and `y` are multidimensional, they are flattened before use.
128 z : array_like
129 The values of the function to interpolate at the data points. If
130 `z` is a multidimensional array, it is flattened before use. The
131 length of a flattened `z` array is either
132 len(`x`)*len(`y`) if `x` and `y` specify the column and row coordinates
133 or ``len(z) == len(x) == len(y)`` if `x` and `y` specify coordinates
134 for each point.
135 kind : {'linear', 'cubic', 'quintic'}, optional
136 The kind of spline interpolation to use. Default is 'linear'.
137 copy : bool, optional
138 If True, the class makes internal copies of x, y and z.
139 If False, references may be used. The default is to copy.
140 bounds_error : bool, optional
141 If True, when interpolated values are requested outside of the
142 domain of the input data (x,y), a ValueError is raised.
143 If False, then `fill_value` is used.
144 fill_value : number, optional
145 If provided, the value to use for points outside of the
146 interpolation domain. If omitted (None), values outside
147 the domain are extrapolated via nearest-neighbor extrapolation.
149 See Also
150 --------
151 RectBivariateSpline :
152 Much faster 2-D interpolation if your input data is on a grid
153 bisplrep, bisplev :
154 Spline interpolation based on FITPACK
155 BivariateSpline : a more recent wrapper of the FITPACK routines
156 interp1d : 1-D version of this function
158 Notes
159 -----
160 The minimum number of data points required along the interpolation
161 axis is ``(k+1)**2``, with k=1 for linear, k=3 for cubic and k=5 for
162 quintic interpolation.
164 The interpolator is constructed by `bisplrep`, with a smoothing factor
165 of 0. If more control over smoothing is needed, `bisplrep` should be
166 used directly.
168 Examples
169 --------
170 Construct a 2-D grid and interpolate on it:
172 >>> from scipy import interpolate
173 >>> x = np.arange(-5.01, 5.01, 0.25)
174 >>> y = np.arange(-5.01, 5.01, 0.25)
175 >>> xx, yy = np.meshgrid(x, y)
176 >>> z = np.sin(xx**2+yy**2)
177 >>> f = interpolate.interp2d(x, y, z, kind='cubic')
179 Now use the obtained interpolation function and plot the result:
181 >>> import matplotlib.pyplot as plt
182 >>> xnew = np.arange(-5.01, 5.01, 1e-2)
183 >>> ynew = np.arange(-5.01, 5.01, 1e-2)
184 >>> znew = f(xnew, ynew)
185 >>> plt.plot(x, z[0, :], 'ro-', xnew, znew[0, :], 'b-')
186 >>> plt.show()
187 """
189 def __init__(self, x, y, z, kind='linear', copy=True, bounds_error=False,
190 fill_value=None):
191 x = ravel(x)
192 y = ravel(y)
193 z = asarray(z)
195 rectangular_grid = (z.size == len(x) * len(y))
196 if rectangular_grid:
197 if z.ndim == 2:
198 if z.shape != (len(y), len(x)):
199 raise ValueError("When on a regular grid with x.size = m "
200 "and y.size = n, if z.ndim == 2, then z "
201 "must have shape (n, m)")
202 if not np.all(x[1:] >= x[:-1]):
203 j = np.argsort(x)
204 x = x[j]
205 z = z[:, j]
206 if not np.all(y[1:] >= y[:-1]):
207 j = np.argsort(y)
208 y = y[j]
209 z = z[j, :]
210 z = ravel(z.T)
211 else:
212 z = ravel(z)
213 if len(x) != len(y):
214 raise ValueError(
215 "x and y must have equal lengths for non rectangular grid")
216 if len(z) != len(x):
217 raise ValueError(
218 "Invalid length for input z for non rectangular grid")
220 try:
221 kx = ky = {'linear': 1,
222 'cubic': 3,
223 'quintic': 5}[kind]
224 except KeyError:
225 raise ValueError("Unsupported interpolation type.")
227 if not rectangular_grid:
228 # TODO: surfit is really not meant for interpolation!
229 self.tck = fitpack.bisplrep(x, y, z, kx=kx, ky=ky, s=0.0)
230 else:
231 nx, tx, ny, ty, c, fp, ier = dfitpack.regrid_smth(
232 x, y, z, None, None, None, None,
233 kx=kx, ky=ky, s=0.0)
234 self.tck = (tx[:nx], ty[:ny], c[:(nx - kx - 1) * (ny - ky - 1)],
235 kx, ky)
237 self.bounds_error = bounds_error
238 self.fill_value = fill_value
239 self.x, self.y, self.z = [array(a, copy=copy) for a in (x, y, z)]
241 self.x_min, self.x_max = np.amin(x), np.amax(x)
242 self.y_min, self.y_max = np.amin(y), np.amax(y)
244 def __call__(self, x, y, dx=0, dy=0, assume_sorted=False):
245 """Interpolate the function.
247 Parameters
248 ----------
249 x : 1-D array
250 x-coordinates of the mesh on which to interpolate.
251 y : 1-D array
252 y-coordinates of the mesh on which to interpolate.
253 dx : int >= 0, < kx
254 Order of partial derivatives in x.
255 dy : int >= 0, < ky
256 Order of partial derivatives in y.
257 assume_sorted : bool, optional
258 If False, values of `x` and `y` can be in any order and they are
259 sorted first.
260 If True, `x` and `y` have to be arrays of monotonically
261 increasing values.
263 Returns
264 -------
265 z : 2-D array with shape (len(y), len(x))
266 The interpolated values.
267 """
269 x = atleast_1d(x)
270 y = atleast_1d(y)
272 if x.ndim != 1 or y.ndim != 1:
273 raise ValueError("x and y should both be 1-D arrays")
275 if not assume_sorted:
276 x = np.sort(x)
277 y = np.sort(y)
279 if self.bounds_error or self.fill_value is not None:
280 out_of_bounds_x = (x < self.x_min) | (x > self.x_max)
281 out_of_bounds_y = (y < self.y_min) | (y > self.y_max)
283 any_out_of_bounds_x = np.any(out_of_bounds_x)
284 any_out_of_bounds_y = np.any(out_of_bounds_y)
286 if self.bounds_error and (any_out_of_bounds_x or any_out_of_bounds_y):
287 raise ValueError("Values out of range; x must be in %r, y in %r"
288 % ((self.x_min, self.x_max),
289 (self.y_min, self.y_max)))
291 z = fitpack.bisplev(x, y, self.tck, dx, dy)
292 z = atleast_2d(z)
293 z = transpose(z)
295 if self.fill_value is not None:
296 if any_out_of_bounds_x:
297 z[:, out_of_bounds_x] = self.fill_value
298 if any_out_of_bounds_y:
299 z[out_of_bounds_y, :] = self.fill_value
301 if len(z) == 1:
302 z = z[0]
303 return array(z)
306def _check_broadcast_up_to(arr_from, shape_to, name):
307 """Helper to check that arr_from broadcasts up to shape_to"""
308 shape_from = arr_from.shape
309 if len(shape_to) >= len(shape_from):
310 for t, f in zip(shape_to[::-1], shape_from[::-1]):
311 if f != 1 and f != t:
312 break
313 else: # all checks pass, do the upcasting that we need later
314 if arr_from.size != 1 and arr_from.shape != shape_to:
315 arr_from = np.ones(shape_to, arr_from.dtype) * arr_from
316 return arr_from.ravel()
317 # at least one check failed
318 raise ValueError('%s argument must be able to broadcast up '
319 'to shape %s but had shape %s'
320 % (name, shape_to, shape_from))
323def _do_extrapolate(fill_value):
324 """Helper to check if fill_value == "extrapolate" without warnings"""
325 return (isinstance(fill_value, str) and
326 fill_value == 'extrapolate')
329class interp1d(_Interpolator1D):
330 """
331 Interpolate a 1-D function.
333 `x` and `y` are arrays of values used to approximate some function f:
334 ``y = f(x)``. This class returns a function whose call method uses
335 interpolation to find the value of new points.
337 Note that calling `interp1d` with NaNs present in input values results in
338 undefined behaviour.
340 Parameters
341 ----------
342 x : (N,) array_like
343 A 1-D array of real values.
344 y : (...,N,...) array_like
345 A N-D array of real values. The length of `y` along the interpolation
346 axis must be equal to the length of `x`.
347 kind : str or int, optional
348 Specifies the kind of interpolation as a string
349 ('linear', 'nearest', 'zero', 'slinear', 'quadratic', 'cubic',
350 'previous', 'next', where 'zero', 'slinear', 'quadratic' and 'cubic'
351 refer to a spline interpolation of zeroth, first, second or third
352 order; 'previous' and 'next' simply return the previous or next value
353 of the point) or as an integer specifying the order of the spline
354 interpolator to use.
355 Default is 'linear'.
356 axis : int, optional
357 Specifies the axis of `y` along which to interpolate.
358 Interpolation defaults to the last axis of `y`.
359 copy : bool, optional
360 If True, the class makes internal copies of x and y.
361 If False, references to `x` and `y` are used. The default is to copy.
362 bounds_error : bool, optional
363 If True, a ValueError is raised any time interpolation is attempted on
364 a value outside of the range of x (where extrapolation is
365 necessary). If False, out of bounds values are assigned `fill_value`.
366 By default, an error is raised unless ``fill_value="extrapolate"``.
367 fill_value : array-like or (array-like, array_like) or "extrapolate", optional
368 - if a ndarray (or float), this value will be used to fill in for
369 requested points outside of the data range. If not provided, then
370 the default is NaN. The array-like must broadcast properly to the
371 dimensions of the non-interpolation axes.
372 - If a two-element tuple, then the first element is used as a
373 fill value for ``x_new < x[0]`` and the second element is used for
374 ``x_new > x[-1]``. Anything that is not a 2-element tuple (e.g.,
375 list or ndarray, regardless of shape) is taken to be a single
376 array-like argument meant to be used for both bounds as
377 ``below, above = fill_value, fill_value``.
379 .. versionadded:: 0.17.0
380 - If "extrapolate", then points outside the data range will be
381 extrapolated.
383 .. versionadded:: 0.17.0
384 assume_sorted : bool, optional
385 If False, values of `x` can be in any order and they are sorted first.
386 If True, `x` has to be an array of monotonically increasing values.
388 Attributes
389 ----------
390 fill_value
392 Methods
393 -------
394 __call__
396 See Also
397 --------
398 splrep, splev
399 Spline interpolation/smoothing based on FITPACK.
400 UnivariateSpline : An object-oriented wrapper of the FITPACK routines.
401 interp2d : 2-D interpolation
403 Examples
404 --------
405 >>> import matplotlib.pyplot as plt
406 >>> from scipy import interpolate
407 >>> x = np.arange(0, 10)
408 >>> y = np.exp(-x/3.0)
409 >>> f = interpolate.interp1d(x, y)
411 >>> xnew = np.arange(0, 9, 0.1)
412 >>> ynew = f(xnew) # use interpolation function returned by `interp1d`
413 >>> plt.plot(x, y, 'o', xnew, ynew, '-')
414 >>> plt.show()
415 """
417 def __init__(self, x, y, kind='linear', axis=-1,
418 copy=True, bounds_error=None, fill_value=np.nan,
419 assume_sorted=False):
420 """ Initialize a 1-D linear interpolation class."""
421 _Interpolator1D.__init__(self, x, y, axis=axis)
423 self.bounds_error = bounds_error # used by fill_value setter
424 self.copy = copy
426 if kind in ['zero', 'slinear', 'quadratic', 'cubic']:
427 order = {'zero': 0, 'slinear': 1,
428 'quadratic': 2, 'cubic': 3}[kind]
429 kind = 'spline'
430 elif isinstance(kind, int):
431 order = kind
432 kind = 'spline'
433 elif kind not in ('linear', 'nearest', 'previous', 'next'):
434 raise NotImplementedError("%s is unsupported: Use fitpack "
435 "routines for other types." % kind)
436 x = array(x, copy=self.copy)
437 y = array(y, copy=self.copy)
439 if not assume_sorted:
440 ind = np.argsort(x)
441 x = x[ind]
442 y = np.take(y, ind, axis=axis)
444 if x.ndim != 1:
445 raise ValueError("the x array must have exactly one dimension.")
446 if y.ndim == 0:
447 raise ValueError("the y array must have at least one dimension.")
449 # Force-cast y to a floating-point type, if it's not yet one
450 if not issubclass(y.dtype.type, np.inexact):
451 y = y.astype(np.float_)
453 # Backward compatibility
454 self.axis = axis % y.ndim
456 # Interpolation goes internally along the first axis
457 self.y = y
458 self._y = self._reshape_yi(self.y)
459 self.x = x
460 del y, x # clean up namespace to prevent misuse; use attributes
461 self._kind = kind
462 self.fill_value = fill_value # calls the setter, can modify bounds_err
464 # Adjust to interpolation kind; store reference to *unbound*
465 # interpolation methods, in order to avoid circular references to self
466 # stored in the bound instance methods, and therefore delayed garbage
467 # collection. See: https://docs.python.org/reference/datamodel.html
468 if kind in ('linear', 'nearest', 'previous', 'next'):
469 # Make a "view" of the y array that is rotated to the interpolation
470 # axis.
471 minval = 2
472 if kind == 'nearest':
473 # Do division before addition to prevent possible integer
474 # overflow
475 self.x_bds = self.x / 2.0
476 self.x_bds = self.x_bds[1:] + self.x_bds[:-1]
478 self._call = self.__class__._call_nearest
479 elif kind == 'previous':
480 # Side for np.searchsorted and index for clipping
481 self._side = 'left'
482 self._ind = 0
483 # Move x by one floating point value to the left
484 self._x_shift = np.nextafter(self.x, -np.inf)
485 self._call = self.__class__._call_previousnext
486 elif kind == 'next':
487 self._side = 'right'
488 self._ind = 1
489 # Move x by one floating point value to the right
490 self._x_shift = np.nextafter(self.x, np.inf)
491 self._call = self.__class__._call_previousnext
492 else:
493 # Check if we can delegate to numpy.interp (2x-10x faster).
494 cond = self.x.dtype == np.float_ and self.y.dtype == np.float_
495 cond = cond and self.y.ndim == 1
496 cond = cond and not _do_extrapolate(fill_value)
498 if cond:
499 self._call = self.__class__._call_linear_np
500 else:
501 self._call = self.__class__._call_linear
502 else:
503 minval = order + 1
505 rewrite_nan = False
506 xx, yy = self.x, self._y
507 if order > 1:
508 # Quadratic or cubic spline. If input contains even a single
509 # nan, then the output is all nans. We cannot just feed data
510 # with nans to make_interp_spline because it calls LAPACK.
511 # So, we make up a bogus x and y with no nans and use it
512 # to get the correct shape of the output, which we then fill
513 # with nans.
514 # For slinear or zero order spline, we just pass nans through.
515 mask = np.isnan(self.x)
516 if mask.any():
517 sx = self.x[~mask]
518 if sx.size == 0:
519 raise ValueError("`x` array is all-nan")
520 xx = np.linspace(np.nanmin(self.x),
521 np.nanmax(self.x),
522 len(self.x))
523 rewrite_nan = True
524 if np.isnan(self._y).any():
525 yy = np.ones_like(self._y)
526 rewrite_nan = True
528 self._spline = make_interp_spline(xx, yy, k=order,
529 check_finite=False)
530 if rewrite_nan:
531 self._call = self.__class__._call_nan_spline
532 else:
533 self._call = self.__class__._call_spline
535 if len(self.x) < minval:
536 raise ValueError("x and y arrays must have at "
537 "least %d entries" % minval)
539 @property
540 def fill_value(self):
541 """The fill value."""
542 # backwards compat: mimic a public attribute
543 return self._fill_value_orig
545 @fill_value.setter
546 def fill_value(self, fill_value):
547 # extrapolation only works for nearest neighbor and linear methods
548 if _do_extrapolate(fill_value):
549 if self.bounds_error:
550 raise ValueError("Cannot extrapolate and raise "
551 "at the same time.")
552 self.bounds_error = False
553 self._extrapolate = True
554 else:
555 broadcast_shape = (self.y.shape[:self.axis] +
556 self.y.shape[self.axis + 1:])
557 if len(broadcast_shape) == 0:
558 broadcast_shape = (1,)
559 # it's either a pair (_below_range, _above_range) or a single value
560 # for both above and below range
561 if isinstance(fill_value, tuple) and len(fill_value) == 2:
562 below_above = [np.asarray(fill_value[0]),
563 np.asarray(fill_value[1])]
564 names = ('fill_value (below)', 'fill_value (above)')
565 for ii in range(2):
566 below_above[ii] = _check_broadcast_up_to(
567 below_above[ii], broadcast_shape, names[ii])
568 else:
569 fill_value = np.asarray(fill_value)
570 below_above = [_check_broadcast_up_to(
571 fill_value, broadcast_shape, 'fill_value')] * 2
572 self._fill_value_below, self._fill_value_above = below_above
573 self._extrapolate = False
574 if self.bounds_error is None:
575 self.bounds_error = True
576 # backwards compat: fill_value was a public attr; make it writeable
577 self._fill_value_orig = fill_value
579 def _call_linear_np(self, x_new):
580 # Note that out-of-bounds values are taken care of in self._evaluate
581 return np.interp(x_new, self.x, self.y)
583 def _call_linear(self, x_new):
584 # 2. Find where in the original data, the values to interpolate
585 # would be inserted.
586 # Note: If x_new[n] == x[m], then m is returned by searchsorted.
587 x_new_indices = searchsorted(self.x, x_new)
589 # 3. Clip x_new_indices so that they are within the range of
590 # self.x indices and at least 1. Removes mis-interpolation
591 # of x_new[n] = x[0]
592 x_new_indices = x_new_indices.clip(1, len(self.x)-1).astype(int)
594 # 4. Calculate the slope of regions that each x_new value falls in.
595 lo = x_new_indices - 1
596 hi = x_new_indices
598 x_lo = self.x[lo]
599 x_hi = self.x[hi]
600 y_lo = self._y[lo]
601 y_hi = self._y[hi]
603 # Note that the following two expressions rely on the specifics of the
604 # broadcasting semantics.
605 slope = (y_hi - y_lo) / (x_hi - x_lo)[:, None]
607 # 5. Calculate the actual value for each entry in x_new.
608 y_new = slope*(x_new - x_lo)[:, None] + y_lo
610 return y_new
612 def _call_nearest(self, x_new):
613 """ Find nearest neighbor interpolated y_new = f(x_new)."""
615 # 2. Find where in the averaged data the values to interpolate
616 # would be inserted.
617 # Note: use side='left' (right) to searchsorted() to define the
618 # halfway point to be nearest to the left (right) neighbor
619 x_new_indices = searchsorted(self.x_bds, x_new, side='left')
621 # 3. Clip x_new_indices so that they are within the range of x indices.
622 x_new_indices = x_new_indices.clip(0, len(self.x)-1).astype(intp)
624 # 4. Calculate the actual value for each entry in x_new.
625 y_new = self._y[x_new_indices]
627 return y_new
629 def _call_previousnext(self, x_new):
630 """Use previous/next neighbor of x_new, y_new = f(x_new)."""
632 # 1. Get index of left/right value
633 x_new_indices = searchsorted(self._x_shift, x_new, side=self._side)
635 # 2. Clip x_new_indices so that they are within the range of x indices.
636 x_new_indices = x_new_indices.clip(1-self._ind,
637 len(self.x)-self._ind).astype(intp)
639 # 3. Calculate the actual value for each entry in x_new.
640 y_new = self._y[x_new_indices+self._ind-1]
642 return y_new
644 def _call_spline(self, x_new):
645 return self._spline(x_new)
647 def _call_nan_spline(self, x_new):
648 out = self._spline(x_new)
649 out[...] = np.nan
650 return out
652 def _evaluate(self, x_new):
653 # 1. Handle values in x_new that are outside of x. Throw error,
654 # or return a list of mask array indicating the outofbounds values.
655 # The behavior is set by the bounds_error variable.
656 x_new = asarray(x_new)
657 y_new = self._call(self, x_new)
658 if not self._extrapolate:
659 below_bounds, above_bounds = self._check_bounds(x_new)
660 if len(y_new) > 0:
661 # Note fill_value must be broadcast up to the proper size
662 # and flattened to work here
663 y_new[below_bounds] = self._fill_value_below
664 y_new[above_bounds] = self._fill_value_above
665 return y_new
667 def _check_bounds(self, x_new):
668 """Check the inputs for being in the bounds of the interpolated data.
670 Parameters
671 ----------
672 x_new : array
674 Returns
675 -------
676 out_of_bounds : bool array
677 The mask on x_new of values that are out of the bounds.
678 """
680 # If self.bounds_error is True, we raise an error if any x_new values
681 # fall outside the range of x. Otherwise, we return an array indicating
682 # which values are outside the boundary region.
683 below_bounds = x_new < self.x[0]
684 above_bounds = x_new > self.x[-1]
686 # !! Could provide more information about which values are out of bounds
687 if self.bounds_error and below_bounds.any():
688 raise ValueError("A value in x_new is below the interpolation "
689 "range.")
690 if self.bounds_error and above_bounds.any():
691 raise ValueError("A value in x_new is above the interpolation "
692 "range.")
694 # !! Should we emit a warning if some values are out of bounds?
695 # !! matlab does not.
696 return below_bounds, above_bounds
699class _PPolyBase(object):
700 """Base class for piecewise polynomials."""
701 __slots__ = ('c', 'x', 'extrapolate', 'axis')
703 def __init__(self, c, x, extrapolate=None, axis=0):
704 self.c = np.asarray(c)
705 self.x = np.ascontiguousarray(x, dtype=np.float64)
707 if extrapolate is None:
708 extrapolate = True
709 elif extrapolate != 'periodic':
710 extrapolate = bool(extrapolate)
711 self.extrapolate = extrapolate
713 if self.c.ndim < 2:
714 raise ValueError("Coefficients array must be at least "
715 "2-dimensional.")
717 if not (0 <= axis < self.c.ndim - 1):
718 raise ValueError("axis=%s must be between 0 and %s" %
719 (axis, self.c.ndim-1))
721 self.axis = axis
722 if axis != 0:
723 # roll the interpolation axis to be the first one in self.c
724 # More specifically, the target shape for self.c is (k, m, ...),
725 # and axis !=0 means that we have c.shape (..., k, m, ...)
726 # ^
727 # axis
728 # So we roll two of them.
729 self.c = np.rollaxis(self.c, axis+1)
730 self.c = np.rollaxis(self.c, axis+1)
732 if self.x.ndim != 1:
733 raise ValueError("x must be 1-dimensional")
734 if self.x.size < 2:
735 raise ValueError("at least 2 breakpoints are needed")
736 if self.c.ndim < 2:
737 raise ValueError("c must have at least 2 dimensions")
738 if self.c.shape[0] == 0:
739 raise ValueError("polynomial must be at least of order 0")
740 if self.c.shape[1] != self.x.size-1:
741 raise ValueError("number of coefficients != len(x)-1")
742 dx = np.diff(self.x)
743 if not (np.all(dx >= 0) or np.all(dx <= 0)):
744 raise ValueError("`x` must be strictly increasing or decreasing.")
746 dtype = self._get_dtype(self.c.dtype)
747 self.c = np.ascontiguousarray(self.c, dtype=dtype)
749 def _get_dtype(self, dtype):
750 if np.issubdtype(dtype, np.complexfloating) \
751 or np.issubdtype(self.c.dtype, np.complexfloating):
752 return np.complex_
753 else:
754 return np.float_
756 @classmethod
757 def construct_fast(cls, c, x, extrapolate=None, axis=0):
758 """
759 Construct the piecewise polynomial without making checks.
761 Takes the same parameters as the constructor. Input arguments
762 ``c`` and ``x`` must be arrays of the correct shape and type. The
763 ``c`` array can only be of dtypes float and complex, and ``x``
764 array must have dtype float.
765 """
766 self = object.__new__(cls)
767 self.c = c
768 self.x = x
769 self.axis = axis
770 if extrapolate is None:
771 extrapolate = True
772 self.extrapolate = extrapolate
773 return self
775 def _ensure_c_contiguous(self):
776 """
777 c and x may be modified by the user. The Cython code expects
778 that they are C contiguous.
779 """
780 if not self.x.flags.c_contiguous:
781 self.x = self.x.copy()
782 if not self.c.flags.c_contiguous:
783 self.c = self.c.copy()
785 def extend(self, c, x, right=None):
786 """
787 Add additional breakpoints and coefficients to the polynomial.
789 Parameters
790 ----------
791 c : ndarray, size (k, m, ...)
792 Additional coefficients for polynomials in intervals. Note that
793 the first additional interval will be formed using one of the
794 ``self.x`` end points.
795 x : ndarray, size (m,)
796 Additional breakpoints. Must be sorted in the same order as
797 ``self.x`` and either to the right or to the left of the current
798 breakpoints.
799 right
800 Deprecated argument. Has no effect.
802 .. deprecated:: 0.19
803 """
804 if right is not None:
805 warnings.warn("`right` is deprecated and will be removed.")
807 c = np.asarray(c)
808 x = np.asarray(x)
810 if c.ndim < 2:
811 raise ValueError("invalid dimensions for c")
812 if x.ndim != 1:
813 raise ValueError("invalid dimensions for x")
814 if x.shape[0] != c.shape[1]:
815 raise ValueError("x and c have incompatible sizes")
816 if c.shape[2:] != self.c.shape[2:] or c.ndim != self.c.ndim:
817 raise ValueError("c and self.c have incompatible shapes")
819 if c.size == 0:
820 return
822 dx = np.diff(x)
823 if not (np.all(dx >= 0) or np.all(dx <= 0)):
824 raise ValueError("`x` is not sorted.")
826 if self.x[-1] >= self.x[0]:
827 if not x[-1] >= x[0]:
828 raise ValueError("`x` is in the different order "
829 "than `self.x`.")
831 if x[0] >= self.x[-1]:
832 action = 'append'
833 elif x[-1] <= self.x[0]:
834 action = 'prepend'
835 else:
836 raise ValueError("`x` is neither on the left or on the right "
837 "from `self.x`.")
838 else:
839 if not x[-1] <= x[0]:
840 raise ValueError("`x` is in the different order "
841 "than `self.x`.")
843 if x[0] <= self.x[-1]:
844 action = 'append'
845 elif x[-1] >= self.x[0]:
846 action = 'prepend'
847 else:
848 raise ValueError("`x` is neither on the left or on the right "
849 "from `self.x`.")
851 dtype = self._get_dtype(c.dtype)
853 k2 = max(c.shape[0], self.c.shape[0])
854 c2 = np.zeros((k2, self.c.shape[1] + c.shape[1]) + self.c.shape[2:],
855 dtype=dtype)
857 if action == 'append':
858 c2[k2-self.c.shape[0]:, :self.c.shape[1]] = self.c
859 c2[k2-c.shape[0]:, self.c.shape[1]:] = c
860 self.x = np.r_[self.x, x]
861 elif action == 'prepend':
862 c2[k2-self.c.shape[0]:, :c.shape[1]] = c
863 c2[k2-c.shape[0]:, c.shape[1]:] = self.c
864 self.x = np.r_[x, self.x]
866 self.c = c2
868 def __call__(self, x, nu=0, extrapolate=None):
869 """
870 Evaluate the piecewise polynomial or its derivative.
872 Parameters
873 ----------
874 x : array_like
875 Points to evaluate the interpolant at.
876 nu : int, optional
877 Order of derivative to evaluate. Must be non-negative.
878 extrapolate : {bool, 'periodic', None}, optional
879 If bool, determines whether to extrapolate to out-of-bounds points
880 based on first and last intervals, or to return NaNs.
881 If 'periodic', periodic extrapolation is used.
882 If None (default), use `self.extrapolate`.
884 Returns
885 -------
886 y : array_like
887 Interpolated values. Shape is determined by replacing
888 the interpolation axis in the original array with the shape of x.
890 Notes
891 -----
892 Derivatives are evaluated piecewise for each polynomial
893 segment, even if the polynomial is not differentiable at the
894 breakpoints. The polynomial intervals are considered half-open,
895 ``[a, b)``, except for the last interval which is closed
896 ``[a, b]``.
897 """
898 if extrapolate is None:
899 extrapolate = self.extrapolate
900 x = np.asarray(x)
901 x_shape, x_ndim = x.shape, x.ndim
902 x = np.ascontiguousarray(x.ravel(), dtype=np.float_)
904 # With periodic extrapolation we map x to the segment
905 # [self.x[0], self.x[-1]].
906 if extrapolate == 'periodic':
907 x = self.x[0] + (x - self.x[0]) % (self.x[-1] - self.x[0])
908 extrapolate = False
910 out = np.empty((len(x), prod(self.c.shape[2:])), dtype=self.c.dtype)
911 self._ensure_c_contiguous()
912 self._evaluate(x, nu, extrapolate, out)
913 out = out.reshape(x_shape + self.c.shape[2:])
914 if self.axis != 0:
915 # transpose to move the calculated values to the interpolation axis
916 l = list(range(out.ndim))
917 l = l[x_ndim:x_ndim+self.axis] + l[:x_ndim] + l[x_ndim+self.axis:]
918 out = out.transpose(l)
919 return out
922class PPoly(_PPolyBase):
923 """
924 Piecewise polynomial in terms of coefficients and breakpoints
926 The polynomial between ``x[i]`` and ``x[i + 1]`` is written in the
927 local power basis::
929 S = sum(c[m, i] * (xp - x[i])**(k-m) for m in range(k+1))
931 where ``k`` is the degree of the polynomial.
933 Parameters
934 ----------
935 c : ndarray, shape (k, m, ...)
936 Polynomial coefficients, order `k` and `m` intervals.
937 x : ndarray, shape (m+1,)
938 Polynomial breakpoints. Must be sorted in either increasing or
939 decreasing order.
940 extrapolate : bool or 'periodic', optional
941 If bool, determines whether to extrapolate to out-of-bounds points
942 based on first and last intervals, or to return NaNs. If 'periodic',
943 periodic extrapolation is used. Default is True.
944 axis : int, optional
945 Interpolation axis. Default is zero.
947 Attributes
948 ----------
949 x : ndarray
950 Breakpoints.
951 c : ndarray
952 Coefficients of the polynomials. They are reshaped
953 to a 3-D array with the last dimension representing
954 the trailing dimensions of the original coefficient array.
955 axis : int
956 Interpolation axis.
958 Methods
959 -------
960 __call__
961 derivative
962 antiderivative
963 integrate
964 solve
965 roots
966 extend
967 from_spline
968 from_bernstein_basis
969 construct_fast
971 See also
972 --------
973 BPoly : piecewise polynomials in the Bernstein basis
975 Notes
976 -----
977 High-order polynomials in the power basis can be numerically
978 unstable. Precision problems can start to appear for orders
979 larger than 20-30.
980 """
981 def _evaluate(self, x, nu, extrapolate, out):
982 _ppoly.evaluate(self.c.reshape(self.c.shape[0], self.c.shape[1], -1),
983 self.x, x, nu, bool(extrapolate), out)
985 def derivative(self, nu=1):
986 """
987 Construct a new piecewise polynomial representing the derivative.
989 Parameters
990 ----------
991 nu : int, optional
992 Order of derivative to evaluate. Default is 1, i.e., compute the
993 first derivative. If negative, the antiderivative is returned.
995 Returns
996 -------
997 pp : PPoly
998 Piecewise polynomial of order k2 = k - n representing the derivative
999 of this polynomial.
1001 Notes
1002 -----
1003 Derivatives are evaluated piecewise for each polynomial
1004 segment, even if the polynomial is not differentiable at the
1005 breakpoints. The polynomial intervals are considered half-open,
1006 ``[a, b)``, except for the last interval which is closed
1007 ``[a, b]``.
1008 """
1009 if nu < 0:
1010 return self.antiderivative(-nu)
1012 # reduce order
1013 if nu == 0:
1014 c2 = self.c.copy()
1015 else:
1016 c2 = self.c[:-nu, :].copy()
1018 if c2.shape[0] == 0:
1019 # derivative of order 0 is zero
1020 c2 = np.zeros((1,) + c2.shape[1:], dtype=c2.dtype)
1022 # multiply by the correct rising factorials
1023 factor = spec.poch(np.arange(c2.shape[0], 0, -1), nu)
1024 c2 *= factor[(slice(None),) + (None,)*(c2.ndim-1)]
1026 # construct a compatible polynomial
1027 return self.construct_fast(c2, self.x, self.extrapolate, self.axis)
1029 def antiderivative(self, nu=1):
1030 """
1031 Construct a new piecewise polynomial representing the antiderivative.
1033 Antiderivative is also the indefinite integral of the function,
1034 and derivative is its inverse operation.
1036 Parameters
1037 ----------
1038 nu : int, optional
1039 Order of antiderivative to evaluate. Default is 1, i.e., compute
1040 the first integral. If negative, the derivative is returned.
1042 Returns
1043 -------
1044 pp : PPoly
1045 Piecewise polynomial of order k2 = k + n representing
1046 the antiderivative of this polynomial.
1048 Notes
1049 -----
1050 The antiderivative returned by this function is continuous and
1051 continuously differentiable to order n-1, up to floating point
1052 rounding error.
1054 If antiderivative is computed and ``self.extrapolate='periodic'``,
1055 it will be set to False for the returned instance. This is done because
1056 the antiderivative is no longer periodic and its correct evaluation
1057 outside of the initially given x interval is difficult.
1058 """
1059 if nu <= 0:
1060 return self.derivative(-nu)
1062 c = np.zeros((self.c.shape[0] + nu, self.c.shape[1]) + self.c.shape[2:],
1063 dtype=self.c.dtype)
1064 c[:-nu] = self.c
1066 # divide by the correct rising factorials
1067 factor = spec.poch(np.arange(self.c.shape[0], 0, -1), nu)
1068 c[:-nu] /= factor[(slice(None),) + (None,)*(c.ndim-1)]
1070 # fix continuity of added degrees of freedom
1071 self._ensure_c_contiguous()
1072 _ppoly.fix_continuity(c.reshape(c.shape[0], c.shape[1], -1),
1073 self.x, nu - 1)
1075 if self.extrapolate == 'periodic':
1076 extrapolate = False
1077 else:
1078 extrapolate = self.extrapolate
1080 # construct a compatible polynomial
1081 return self.construct_fast(c, self.x, extrapolate, self.axis)
1083 def integrate(self, a, b, extrapolate=None):
1084 """
1085 Compute a definite integral over a piecewise polynomial.
1087 Parameters
1088 ----------
1089 a : float
1090 Lower integration bound
1091 b : float
1092 Upper integration bound
1093 extrapolate : {bool, 'periodic', None}, optional
1094 If bool, determines whether to extrapolate to out-of-bounds points
1095 based on first and last intervals, or to return NaNs.
1096 If 'periodic', periodic extrapolation is used.
1097 If None (default), use `self.extrapolate`.
1099 Returns
1100 -------
1101 ig : array_like
1102 Definite integral of the piecewise polynomial over [a, b]
1103 """
1104 if extrapolate is None:
1105 extrapolate = self.extrapolate
1107 # Swap integration bounds if needed
1108 sign = 1
1109 if b < a:
1110 a, b = b, a
1111 sign = -1
1113 range_int = np.empty((prod(self.c.shape[2:]),), dtype=self.c.dtype)
1114 self._ensure_c_contiguous()
1116 # Compute the integral.
1117 if extrapolate == 'periodic':
1118 # Split the integral into the part over period (can be several
1119 # of them) and the remaining part.
1121 xs, xe = self.x[0], self.x[-1]
1122 period = xe - xs
1123 interval = b - a
1124 n_periods, left = divmod(interval, period)
1126 if n_periods > 0:
1127 _ppoly.integrate(
1128 self.c.reshape(self.c.shape[0], self.c.shape[1], -1),
1129 self.x, xs, xe, False, out=range_int)
1130 range_int *= n_periods
1131 else:
1132 range_int.fill(0)
1134 # Map a to [xs, xe], b is always a + left.
1135 a = xs + (a - xs) % period
1136 b = a + left
1138 # If b <= xe then we need to integrate over [a, b], otherwise
1139 # over [a, xe] and from xs to what is remained.
1140 remainder_int = np.empty_like(range_int)
1141 if b <= xe:
1142 _ppoly.integrate(
1143 self.c.reshape(self.c.shape[0], self.c.shape[1], -1),
1144 self.x, a, b, False, out=remainder_int)
1145 range_int += remainder_int
1146 else:
1147 _ppoly.integrate(
1148 self.c.reshape(self.c.shape[0], self.c.shape[1], -1),
1149 self.x, a, xe, False, out=remainder_int)
1150 range_int += remainder_int
1152 _ppoly.integrate(
1153 self.c.reshape(self.c.shape[0], self.c.shape[1], -1),
1154 self.x, xs, xs + left + a - xe, False, out=remainder_int)
1155 range_int += remainder_int
1156 else:
1157 _ppoly.integrate(
1158 self.c.reshape(self.c.shape[0], self.c.shape[1], -1),
1159 self.x, a, b, bool(extrapolate), out=range_int)
1161 # Return
1162 range_int *= sign
1163 return range_int.reshape(self.c.shape[2:])
1165 def solve(self, y=0., discontinuity=True, extrapolate=None):
1166 """
1167 Find real solutions of the the equation ``pp(x) == y``.
1169 Parameters
1170 ----------
1171 y : float, optional
1172 Right-hand side. Default is zero.
1173 discontinuity : bool, optional
1174 Whether to report sign changes across discontinuities at
1175 breakpoints as roots.
1176 extrapolate : {bool, 'periodic', None}, optional
1177 If bool, determines whether to return roots from the polynomial
1178 extrapolated based on first and last intervals, 'periodic' works
1179 the same as False. If None (default), use `self.extrapolate`.
1181 Returns
1182 -------
1183 roots : ndarray
1184 Roots of the polynomial(s).
1186 If the PPoly object describes multiple polynomials, the
1187 return value is an object array whose each element is an
1188 ndarray containing the roots.
1190 Notes
1191 -----
1192 This routine works only on real-valued polynomials.
1194 If the piecewise polynomial contains sections that are
1195 identically zero, the root list will contain the start point
1196 of the corresponding interval, followed by a ``nan`` value.
1198 If the polynomial is discontinuous across a breakpoint, and
1199 there is a sign change across the breakpoint, this is reported
1200 if the `discont` parameter is True.
1202 Examples
1203 --------
1205 Finding roots of ``[x**2 - 1, (x - 1)**2]`` defined on intervals
1206 ``[-2, 1], [1, 2]``:
1208 >>> from scipy.interpolate import PPoly
1209 >>> pp = PPoly(np.array([[1, -4, 3], [1, 0, 0]]).T, [-2, 1, 2])
1210 >>> pp.solve()
1211 array([-1., 1.])
1212 """
1213 if extrapolate is None:
1214 extrapolate = self.extrapolate
1216 self._ensure_c_contiguous()
1218 if np.issubdtype(self.c.dtype, np.complexfloating):
1219 raise ValueError("Root finding is only for "
1220 "real-valued polynomials")
1222 y = float(y)
1223 r = _ppoly.real_roots(self.c.reshape(self.c.shape[0], self.c.shape[1], -1),
1224 self.x, y, bool(discontinuity),
1225 bool(extrapolate))
1226 if self.c.ndim == 2:
1227 return r[0]
1228 else:
1229 r2 = np.empty(prod(self.c.shape[2:]), dtype=object)
1230 # this for-loop is equivalent to ``r2[...] = r``, but that's broken
1231 # in NumPy 1.6.0
1232 for ii, root in enumerate(r):
1233 r2[ii] = root
1235 return r2.reshape(self.c.shape[2:])
1237 def roots(self, discontinuity=True, extrapolate=None):
1238 """
1239 Find real roots of the the piecewise polynomial.
1241 Parameters
1242 ----------
1243 discontinuity : bool, optional
1244 Whether to report sign changes across discontinuities at
1245 breakpoints as roots.
1246 extrapolate : {bool, 'periodic', None}, optional
1247 If bool, determines whether to return roots from the polynomial
1248 extrapolated based on first and last intervals, 'periodic' works
1249 the same as False. If None (default), use `self.extrapolate`.
1251 Returns
1252 -------
1253 roots : ndarray
1254 Roots of the polynomial(s).
1256 If the PPoly object describes multiple polynomials, the
1257 return value is an object array whose each element is an
1258 ndarray containing the roots.
1260 See Also
1261 --------
1262 PPoly.solve
1263 """
1264 return self.solve(0, discontinuity, extrapolate)
1266 @classmethod
1267 def from_spline(cls, tck, extrapolate=None):
1268 """
1269 Construct a piecewise polynomial from a spline
1271 Parameters
1272 ----------
1273 tck
1274 A spline, as returned by `splrep` or a BSpline object.
1275 extrapolate : bool or 'periodic', optional
1276 If bool, determines whether to extrapolate to out-of-bounds points
1277 based on first and last intervals, or to return NaNs.
1278 If 'periodic', periodic extrapolation is used. Default is True.
1279 """
1280 if isinstance(tck, BSpline):
1281 t, c, k = tck.tck
1282 if extrapolate is None:
1283 extrapolate = tck.extrapolate
1284 else:
1285 t, c, k = tck
1287 cvals = np.empty((k + 1, len(t)-1), dtype=c.dtype)
1288 for m in range(k, -1, -1):
1289 y = fitpack.splev(t[:-1], tck, der=m)
1290 cvals[k - m, :] = y/spec.gamma(m+1)
1292 return cls.construct_fast(cvals, t, extrapolate)
1294 @classmethod
1295 def from_bernstein_basis(cls, bp, extrapolate=None):
1296 """
1297 Construct a piecewise polynomial in the power basis
1298 from a polynomial in Bernstein basis.
1300 Parameters
1301 ----------
1302 bp : BPoly
1303 A Bernstein basis polynomial, as created by BPoly
1304 extrapolate : bool or 'periodic', optional
1305 If bool, determines whether to extrapolate to out-of-bounds points
1306 based on first and last intervals, or to return NaNs.
1307 If 'periodic', periodic extrapolation is used. Default is True.
1308 """
1309 if not isinstance(bp, BPoly):
1310 raise TypeError(".from_bernstein_basis only accepts BPoly instances. "
1311 "Got %s instead." % type(bp))
1313 dx = np.diff(bp.x)
1314 k = bp.c.shape[0] - 1 # polynomial order
1316 rest = (None,)*(bp.c.ndim-2)
1318 c = np.zeros_like(bp.c)
1319 for a in range(k+1):
1320 factor = (-1)**a * comb(k, a) * bp.c[a]
1321 for s in range(a, k+1):
1322 val = comb(k-a, s-a) * (-1)**s
1323 c[k-s] += factor * val / dx[(slice(None),)+rest]**s
1325 if extrapolate is None:
1326 extrapolate = bp.extrapolate
1328 return cls.construct_fast(c, bp.x, extrapolate, bp.axis)
1331class BPoly(_PPolyBase):
1332 """Piecewise polynomial in terms of coefficients and breakpoints.
1334 The polynomial between ``x[i]`` and ``x[i + 1]`` is written in the
1335 Bernstein polynomial basis::
1337 S = sum(c[a, i] * b(a, k; x) for a in range(k+1)),
1339 where ``k`` is the degree of the polynomial, and::
1341 b(a, k; x) = binom(k, a) * t**a * (1 - t)**(k - a),
1343 with ``t = (x - x[i]) / (x[i+1] - x[i])`` and ``binom`` is the binomial
1344 coefficient.
1346 Parameters
1347 ----------
1348 c : ndarray, shape (k, m, ...)
1349 Polynomial coefficients, order `k` and `m` intervals
1350 x : ndarray, shape (m+1,)
1351 Polynomial breakpoints. Must be sorted in either increasing or
1352 decreasing order.
1353 extrapolate : bool, optional
1354 If bool, determines whether to extrapolate to out-of-bounds points
1355 based on first and last intervals, or to return NaNs. If 'periodic',
1356 periodic extrapolation is used. Default is True.
1357 axis : int, optional
1358 Interpolation axis. Default is zero.
1360 Attributes
1361 ----------
1362 x : ndarray
1363 Breakpoints.
1364 c : ndarray
1365 Coefficients of the polynomials. They are reshaped
1366 to a 3-D array with the last dimension representing
1367 the trailing dimensions of the original coefficient array.
1368 axis : int
1369 Interpolation axis.
1371 Methods
1372 -------
1373 __call__
1374 extend
1375 derivative
1376 antiderivative
1377 integrate
1378 construct_fast
1379 from_power_basis
1380 from_derivatives
1382 See also
1383 --------
1384 PPoly : piecewise polynomials in the power basis
1386 Notes
1387 -----
1388 Properties of Bernstein polynomials are well documented in the literature,
1389 see for example [1]_ [2]_ [3]_.
1391 References
1392 ----------
1393 .. [1] https://en.wikipedia.org/wiki/Bernstein_polynomial
1395 .. [2] Kenneth I. Joy, Bernstein polynomials,
1396 http://www.idav.ucdavis.edu/education/CAGDNotes/Bernstein-Polynomials.pdf
1398 .. [3] E. H. Doha, A. H. Bhrawy, and M. A. Saker, Boundary Value Problems,
1399 vol 2011, article ID 829546, :doi:`10.1155/2011/829543`.
1401 Examples
1402 --------
1403 >>> from scipy.interpolate import BPoly
1404 >>> x = [0, 1]
1405 >>> c = [[1], [2], [3]]
1406 >>> bp = BPoly(c, x)
1408 This creates a 2nd order polynomial
1410 .. math::
1412 B(x) = 1 \\times b_{0, 2}(x) + 2 \\times b_{1, 2}(x) + 3 \\times b_{2, 2}(x) \\\\
1413 = 1 \\times (1-x)^2 + 2 \\times 2 x (1 - x) + 3 \\times x^2
1415 """
1417 def _evaluate(self, x, nu, extrapolate, out):
1418 _ppoly.evaluate_bernstein(
1419 self.c.reshape(self.c.shape[0], self.c.shape[1], -1),
1420 self.x, x, nu, bool(extrapolate), out)
1422 def derivative(self, nu=1):
1423 """
1424 Construct a new piecewise polynomial representing the derivative.
1426 Parameters
1427 ----------
1428 nu : int, optional
1429 Order of derivative to evaluate. Default is 1, i.e., compute the
1430 first derivative. If negative, the antiderivative is returned.
1432 Returns
1433 -------
1434 bp : BPoly
1435 Piecewise polynomial of order k - nu representing the derivative of
1436 this polynomial.
1438 """
1439 if nu < 0:
1440 return self.antiderivative(-nu)
1442 if nu > 1:
1443 bp = self
1444 for k in range(nu):
1445 bp = bp.derivative()
1446 return bp
1448 # reduce order
1449 if nu == 0:
1450 c2 = self.c.copy()
1451 else:
1452 # For a polynomial
1453 # B(x) = \sum_{a=0}^{k} c_a b_{a, k}(x),
1454 # we use the fact that
1455 # b'_{a, k} = k ( b_{a-1, k-1} - b_{a, k-1} ),
1456 # which leads to
1457 # B'(x) = \sum_{a=0}^{k-1} (c_{a+1} - c_a) b_{a, k-1}
1458 #
1459 # finally, for an interval [y, y + dy] with dy != 1,
1460 # we need to correct for an extra power of dy
1462 rest = (None,)*(self.c.ndim-2)
1464 k = self.c.shape[0] - 1
1465 dx = np.diff(self.x)[(None, slice(None))+rest]
1466 c2 = k * np.diff(self.c, axis=0) / dx
1468 if c2.shape[0] == 0:
1469 # derivative of order 0 is zero
1470 c2 = np.zeros((1,) + c2.shape[1:], dtype=c2.dtype)
1472 # construct a compatible polynomial
1473 return self.construct_fast(c2, self.x, self.extrapolate, self.axis)
1475 def antiderivative(self, nu=1):
1476 """
1477 Construct a new piecewise polynomial representing the antiderivative.
1479 Parameters
1480 ----------
1481 nu : int, optional
1482 Order of antiderivative to evaluate. Default is 1, i.e., compute
1483 the first integral. If negative, the derivative is returned.
1485 Returns
1486 -------
1487 bp : BPoly
1488 Piecewise polynomial of order k + nu representing the
1489 antiderivative of this polynomial.
1491 Notes
1492 -----
1493 If antiderivative is computed and ``self.extrapolate='periodic'``,
1494 it will be set to False for the returned instance. This is done because
1495 the antiderivative is no longer periodic and its correct evaluation
1496 outside of the initially given x interval is difficult.
1497 """
1498 if nu <= 0:
1499 return self.derivative(-nu)
1501 if nu > 1:
1502 bp = self
1503 for k in range(nu):
1504 bp = bp.antiderivative()
1505 return bp
1507 # Construct the indefinite integrals on individual intervals
1508 c, x = self.c, self.x
1509 k = c.shape[0]
1510 c2 = np.zeros((k+1,) + c.shape[1:], dtype=c.dtype)
1512 c2[1:, ...] = np.cumsum(c, axis=0) / k
1513 delta = x[1:] - x[:-1]
1514 c2 *= delta[(None, slice(None)) + (None,)*(c.ndim-2)]
1516 # Now fix continuity: on the very first interval, take the integration
1517 # constant to be zero; on an interval [x_j, x_{j+1}) with j>0,
1518 # the integration constant is then equal to the jump of the `bp` at x_j.
1519 # The latter is given by the coefficient of B_{n+1, n+1}
1520 # *on the previous interval* (other B. polynomials are zero at the
1521 # breakpoint). Finally, use the fact that BPs form a partition of unity.
1522 c2[:,1:] += np.cumsum(c2[k, :], axis=0)[:-1]
1524 if self.extrapolate == 'periodic':
1525 extrapolate = False
1526 else:
1527 extrapolate = self.extrapolate
1529 return self.construct_fast(c2, x, extrapolate, axis=self.axis)
1531 def integrate(self, a, b, extrapolate=None):
1532 """
1533 Compute a definite integral over a piecewise polynomial.
1535 Parameters
1536 ----------
1537 a : float
1538 Lower integration bound
1539 b : float
1540 Upper integration bound
1541 extrapolate : {bool, 'periodic', None}, optional
1542 Whether to extrapolate to out-of-bounds points based on first
1543 and last intervals, or to return NaNs. If 'periodic', periodic
1544 extrapolation is used. If None (default), use `self.extrapolate`.
1546 Returns
1547 -------
1548 array_like
1549 Definite integral of the piecewise polynomial over [a, b]
1551 """
1552 # XXX: can probably use instead the fact that
1553 # \int_0^{1} B_{j, n}(x) \dx = 1/(n+1)
1554 ib = self.antiderivative()
1555 if extrapolate is None:
1556 extrapolate = self.extrapolate
1558 # ib.extrapolate shouldn't be 'periodic', it is converted to
1559 # False for 'periodic. in antiderivative() call.
1560 if extrapolate != 'periodic':
1561 ib.extrapolate = extrapolate
1563 if extrapolate == 'periodic':
1564 # Split the integral into the part over period (can be several
1565 # of them) and the remaining part.
1567 # For simplicity and clarity convert to a <= b case.
1568 if a <= b:
1569 sign = 1
1570 else:
1571 a, b = b, a
1572 sign = -1
1574 xs, xe = self.x[0], self.x[-1]
1575 period = xe - xs
1576 interval = b - a
1577 n_periods, left = divmod(interval, period)
1578 res = n_periods * (ib(xe) - ib(xs))
1580 # Map a and b to [xs, xe].
1581 a = xs + (a - xs) % period
1582 b = a + left
1584 # If b <= xe then we need to integrate over [a, b], otherwise
1585 # over [a, xe] and from xs to what is remained.
1586 if b <= xe:
1587 res += ib(b) - ib(a)
1588 else:
1589 res += ib(xe) - ib(a) + ib(xs + left + a - xe) - ib(xs)
1591 return sign * res
1592 else:
1593 return ib(b) - ib(a)
1595 def extend(self, c, x, right=None):
1596 k = max(self.c.shape[0], c.shape[0])
1597 self.c = self._raise_degree(self.c, k - self.c.shape[0])
1598 c = self._raise_degree(c, k - c.shape[0])
1599 return _PPolyBase.extend(self, c, x, right)
1600 extend.__doc__ = _PPolyBase.extend.__doc__
1602 @classmethod
1603 def from_power_basis(cls, pp, extrapolate=None):
1604 """
1605 Construct a piecewise polynomial in Bernstein basis
1606 from a power basis polynomial.
1608 Parameters
1609 ----------
1610 pp : PPoly
1611 A piecewise polynomial in the power basis
1612 extrapolate : bool or 'periodic', optional
1613 If bool, determines whether to extrapolate to out-of-bounds points
1614 based on first and last intervals, or to return NaNs.
1615 If 'periodic', periodic extrapolation is used. Default is True.
1616 """
1617 if not isinstance(pp, PPoly):
1618 raise TypeError(".from_power_basis only accepts PPoly instances. "
1619 "Got %s instead." % type(pp))
1621 dx = np.diff(pp.x)
1622 k = pp.c.shape[0] - 1 # polynomial order
1624 rest = (None,)*(pp.c.ndim-2)
1626 c = np.zeros_like(pp.c)
1627 for a in range(k+1):
1628 factor = pp.c[a] / comb(k, k-a) * dx[(slice(None),)+rest]**(k-a)
1629 for j in range(k-a, k+1):
1630 c[j] += factor * comb(j, k-a)
1632 if extrapolate is None:
1633 extrapolate = pp.extrapolate
1635 return cls.construct_fast(c, pp.x, extrapolate, pp.axis)
1637 @classmethod
1638 def from_derivatives(cls, xi, yi, orders=None, extrapolate=None):
1639 """Construct a piecewise polynomial in the Bernstein basis,
1640 compatible with the specified values and derivatives at breakpoints.
1642 Parameters
1643 ----------
1644 xi : array_like
1645 sorted 1-D array of x-coordinates
1646 yi : array_like or list of array_likes
1647 ``yi[i][j]`` is the ``j``th derivative known at ``xi[i]``
1648 orders : None or int or array_like of ints. Default: None.
1649 Specifies the degree of local polynomials. If not None, some
1650 derivatives are ignored.
1651 extrapolate : bool or 'periodic', optional
1652 If bool, determines whether to extrapolate to out-of-bounds points
1653 based on first and last intervals, or to return NaNs.
1654 If 'periodic', periodic extrapolation is used. Default is True.
1656 Notes
1657 -----
1658 If ``k`` derivatives are specified at a breakpoint ``x``, the
1659 constructed polynomial is exactly ``k`` times continuously
1660 differentiable at ``x``, unless the ``order`` is provided explicitly.
1661 In the latter case, the smoothness of the polynomial at
1662 the breakpoint is controlled by the ``order``.
1664 Deduces the number of derivatives to match at each end
1665 from ``order`` and the number of derivatives available. If
1666 possible it uses the same number of derivatives from
1667 each end; if the number is odd it tries to take the
1668 extra one from y2. In any case if not enough derivatives
1669 are available at one end or another it draws enough to
1670 make up the total from the other end.
1672 If the order is too high and not enough derivatives are available,
1673 an exception is raised.
1675 Examples
1676 --------
1678 >>> from scipy.interpolate import BPoly
1679 >>> BPoly.from_derivatives([0, 1], [[1, 2], [3, 4]])
1681 Creates a polynomial `f(x)` of degree 3, defined on `[0, 1]`
1682 such that `f(0) = 1, df/dx(0) = 2, f(1) = 3, df/dx(1) = 4`
1684 >>> BPoly.from_derivatives([0, 1, 2], [[0, 1], [0], [2]])
1686 Creates a piecewise polynomial `f(x)`, such that
1687 `f(0) = f(1) = 0`, `f(2) = 2`, and `df/dx(0) = 1`.
1688 Based on the number of derivatives provided, the order of the
1689 local polynomials is 2 on `[0, 1]` and 1 on `[1, 2]`.
1690 Notice that no restriction is imposed on the derivatives at
1691 ``x = 1`` and ``x = 2``.
1693 Indeed, the explicit form of the polynomial is::
1695 f(x) = | x * (1 - x), 0 <= x < 1
1696 | 2 * (x - 1), 1 <= x <= 2
1698 So that f'(1-0) = -1 and f'(1+0) = 2
1700 """
1701 xi = np.asarray(xi)
1702 if len(xi) != len(yi):
1703 raise ValueError("xi and yi need to have the same length")
1704 if np.any(xi[1:] - xi[:1] <= 0):
1705 raise ValueError("x coordinates are not in increasing order")
1707 # number of intervals
1708 m = len(xi) - 1
1710 # global poly order is k-1, local orders are <=k and can vary
1711 try:
1712 k = max(len(yi[i]) + len(yi[i+1]) for i in range(m))
1713 except TypeError:
1714 raise ValueError("Using a 1-D array for y? Please .reshape(-1, 1).")
1716 if orders is None:
1717 orders = [None] * m
1718 else:
1719 if isinstance(orders, (int, np.integer)):
1720 orders = [orders] * m
1721 k = max(k, max(orders))
1723 if any(o <= 0 for o in orders):
1724 raise ValueError("Orders must be positive.")
1726 c = []
1727 for i in range(m):
1728 y1, y2 = yi[i], yi[i+1]
1729 if orders[i] is None:
1730 n1, n2 = len(y1), len(y2)
1731 else:
1732 n = orders[i]+1
1733 n1 = min(n//2, len(y1))
1734 n2 = min(n - n1, len(y2))
1735 n1 = min(n - n2, len(y2))
1736 if n1+n2 != n:
1737 mesg = ("Point %g has %d derivatives, point %g"
1738 " has %d derivatives, but order %d requested" % (
1739 xi[i], len(y1), xi[i+1], len(y2), orders[i]))
1740 raise ValueError(mesg)
1742 if not (n1 <= len(y1) and n2 <= len(y2)):
1743 raise ValueError("`order` input incompatible with"
1744 " length y1 or y2.")
1746 b = BPoly._construct_from_derivatives(xi[i], xi[i+1],
1747 y1[:n1], y2[:n2])
1748 if len(b) < k:
1749 b = BPoly._raise_degree(b, k - len(b))
1750 c.append(b)
1752 c = np.asarray(c)
1753 return cls(c.swapaxes(0, 1), xi, extrapolate)
1755 @staticmethod
1756 def _construct_from_derivatives(xa, xb, ya, yb):
1757 r"""Compute the coefficients of a polynomial in the Bernstein basis
1758 given the values and derivatives at the edges.
1760 Return the coefficients of a polynomial in the Bernstein basis
1761 defined on ``[xa, xb]`` and having the values and derivatives at the
1762 endpoints `xa` and `xb` as specified by `ya`` and `yb`.
1763 The polynomial constructed is of the minimal possible degree, i.e.,
1764 if the lengths of `ya` and `yb` are `na` and `nb`, the degree
1765 of the polynomial is ``na + nb - 1``.
1767 Parameters
1768 ----------
1769 xa : float
1770 Left-hand end point of the interval
1771 xb : float
1772 Right-hand end point of the interval
1773 ya : array_like
1774 Derivatives at `xa`. `ya[0]` is the value of the function, and
1775 `ya[i]` for ``i > 0`` is the value of the ``i``th derivative.
1776 yb : array_like
1777 Derivatives at `xb`.
1779 Returns
1780 -------
1781 array
1782 coefficient array of a polynomial having specified derivatives
1784 Notes
1785 -----
1786 This uses several facts from life of Bernstein basis functions.
1787 First of all,
1789 .. math:: b'_{a, n} = n (b_{a-1, n-1} - b_{a, n-1})
1791 If B(x) is a linear combination of the form
1793 .. math:: B(x) = \sum_{a=0}^{n} c_a b_{a, n},
1795 then :math: B'(x) = n \sum_{a=0}^{n-1} (c_{a+1} - c_{a}) b_{a, n-1}.
1796 Iterating the latter one, one finds for the q-th derivative
1798 .. math:: B^{q}(x) = n!/(n-q)! \sum_{a=0}^{n-q} Q_a b_{a, n-q},
1800 with
1802 .. math:: Q_a = \sum_{j=0}^{q} (-)^{j+q} comb(q, j) c_{j+a}
1804 This way, only `a=0` contributes to :math: `B^{q}(x = xa)`, and
1805 `c_q` are found one by one by iterating `q = 0, ..., na`.
1807 At ``x = xb`` it's the same with ``a = n - q``.
1809 """
1810 ya, yb = np.asarray(ya), np.asarray(yb)
1811 if ya.shape[1:] != yb.shape[1:]:
1812 raise ValueError('ya and yb have incompatible dimensions.')
1814 dta, dtb = ya.dtype, yb.dtype
1815 if (np.issubdtype(dta, np.complexfloating) or
1816 np.issubdtype(dtb, np.complexfloating)):
1817 dt = np.complex_
1818 else:
1819 dt = np.float_
1821 na, nb = len(ya), len(yb)
1822 n = na + nb
1824 c = np.empty((na+nb,) + ya.shape[1:], dtype=dt)
1826 # compute coefficients of a polynomial degree na+nb-1
1827 # walk left-to-right
1828 for q in range(0, na):
1829 c[q] = ya[q] / spec.poch(n - q, q) * (xb - xa)**q
1830 for j in range(0, q):
1831 c[q] -= (-1)**(j+q) * comb(q, j) * c[j]
1833 # now walk right-to-left
1834 for q in range(0, nb):
1835 c[-q-1] = yb[q] / spec.poch(n - q, q) * (-1)**q * (xb - xa)**q
1836 for j in range(0, q):
1837 c[-q-1] -= (-1)**(j+1) * comb(q, j+1) * c[-q+j]
1839 return c
1841 @staticmethod
1842 def _raise_degree(c, d):
1843 r"""Raise a degree of a polynomial in the Bernstein basis.
1845 Given the coefficients of a polynomial degree `k`, return (the
1846 coefficients of) the equivalent polynomial of degree `k+d`.
1848 Parameters
1849 ----------
1850 c : array_like
1851 coefficient array, 1-D
1852 d : integer
1854 Returns
1855 -------
1856 array
1857 coefficient array, 1-D array of length `c.shape[0] + d`
1859 Notes
1860 -----
1861 This uses the fact that a Bernstein polynomial `b_{a, k}` can be
1862 identically represented as a linear combination of polynomials of
1863 a higher degree `k+d`:
1865 .. math:: b_{a, k} = comb(k, a) \sum_{j=0}^{d} b_{a+j, k+d} \
1866 comb(d, j) / comb(k+d, a+j)
1868 """
1869 if d == 0:
1870 return c
1872 k = c.shape[0] - 1
1873 out = np.zeros((c.shape[0] + d,) + c.shape[1:], dtype=c.dtype)
1875 for a in range(c.shape[0]):
1876 f = c[a] * comb(k, a)
1877 for j in range(d+1):
1878 out[a+j] += f * comb(d, j) / comb(k+d, a+j)
1879 return out
1882class NdPPoly(object):
1883 """
1884 Piecewise tensor product polynomial
1886 The value at point ``xp = (x', y', z', ...)`` is evaluated by first
1887 computing the interval indices `i` such that::
1889 x[0][i[0]] <= x' < x[0][i[0]+1]
1890 x[1][i[1]] <= y' < x[1][i[1]+1]
1891 ...
1893 and then computing::
1895 S = sum(c[k0-m0-1,...,kn-mn-1,i[0],...,i[n]]
1896 * (xp[0] - x[0][i[0]])**m0
1897 * ...
1898 * (xp[n] - x[n][i[n]])**mn
1899 for m0 in range(k[0]+1)
1900 ...
1901 for mn in range(k[n]+1))
1903 where ``k[j]`` is the degree of the polynomial in dimension j. This
1904 representation is the piecewise multivariate power basis.
1906 Parameters
1907 ----------
1908 c : ndarray, shape (k0, ..., kn, m0, ..., mn, ...)
1909 Polynomial coefficients, with polynomial order `kj` and
1910 `mj+1` intervals for each dimension `j`.
1911 x : ndim-tuple of ndarrays, shapes (mj+1,)
1912 Polynomial breakpoints for each dimension. These must be
1913 sorted in increasing order.
1914 extrapolate : bool, optional
1915 Whether to extrapolate to out-of-bounds points based on first
1916 and last intervals, or to return NaNs. Default: True.
1918 Attributes
1919 ----------
1920 x : tuple of ndarrays
1921 Breakpoints.
1922 c : ndarray
1923 Coefficients of the polynomials.
1925 Methods
1926 -------
1927 __call__
1928 construct_fast
1930 See also
1931 --------
1932 PPoly : piecewise polynomials in 1D
1934 Notes
1935 -----
1936 High-order polynomials in the power basis can be numerically
1937 unstable.
1939 """
1941 def __init__(self, c, x, extrapolate=None):
1942 self.x = tuple(np.ascontiguousarray(v, dtype=np.float64) for v in x)
1943 self.c = np.asarray(c)
1944 if extrapolate is None:
1945 extrapolate = True
1946 self.extrapolate = bool(extrapolate)
1948 ndim = len(self.x)
1949 if any(v.ndim != 1 for v in self.x):
1950 raise ValueError("x arrays must all be 1-dimensional")
1951 if any(v.size < 2 for v in self.x):
1952 raise ValueError("x arrays must all contain at least 2 points")
1953 if c.ndim < 2*ndim:
1954 raise ValueError("c must have at least 2*len(x) dimensions")
1955 if any(np.any(v[1:] - v[:-1] < 0) for v in self.x):
1956 raise ValueError("x-coordinates are not in increasing order")
1957 if any(a != b.size - 1 for a, b in zip(c.shape[ndim:2*ndim], self.x)):
1958 raise ValueError("x and c do not agree on the number of intervals")
1960 dtype = self._get_dtype(self.c.dtype)
1961 self.c = np.ascontiguousarray(self.c, dtype=dtype)
1963 @classmethod
1964 def construct_fast(cls, c, x, extrapolate=None):
1965 """
1966 Construct the piecewise polynomial without making checks.
1968 Takes the same parameters as the constructor. Input arguments
1969 ``c`` and ``x`` must be arrays of the correct shape and type. The
1970 ``c`` array can only be of dtypes float and complex, and ``x``
1971 array must have dtype float.
1973 """
1974 self = object.__new__(cls)
1975 self.c = c
1976 self.x = x
1977 if extrapolate is None:
1978 extrapolate = True
1979 self.extrapolate = extrapolate
1980 return self
1982 def _get_dtype(self, dtype):
1983 if np.issubdtype(dtype, np.complexfloating) \
1984 or np.issubdtype(self.c.dtype, np.complexfloating):
1985 return np.complex_
1986 else:
1987 return np.float_
1989 def _ensure_c_contiguous(self):
1990 if not self.c.flags.c_contiguous:
1991 self.c = self.c.copy()
1992 if not isinstance(self.x, tuple):
1993 self.x = tuple(self.x)
1995 def __call__(self, x, nu=None, extrapolate=None):
1996 """
1997 Evaluate the piecewise polynomial or its derivative
1999 Parameters
2000 ----------
2001 x : array-like
2002 Points to evaluate the interpolant at.
2003 nu : tuple, optional
2004 Orders of derivatives to evaluate. Each must be non-negative.
2005 extrapolate : bool, optional
2006 Whether to extrapolate to out-of-bounds points based on first
2007 and last intervals, or to return NaNs.
2009 Returns
2010 -------
2011 y : array-like
2012 Interpolated values. Shape is determined by replacing
2013 the interpolation axis in the original array with the shape of x.
2015 Notes
2016 -----
2017 Derivatives are evaluated piecewise for each polynomial
2018 segment, even if the polynomial is not differentiable at the
2019 breakpoints. The polynomial intervals are considered half-open,
2020 ``[a, b)``, except for the last interval which is closed
2021 ``[a, b]``.
2023 """
2024 if extrapolate is None:
2025 extrapolate = self.extrapolate
2026 else:
2027 extrapolate = bool(extrapolate)
2029 ndim = len(self.x)
2031 x = _ndim_coords_from_arrays(x)
2032 x_shape = x.shape
2033 x = np.ascontiguousarray(x.reshape(-1, x.shape[-1]), dtype=np.float_)
2035 if nu is None:
2036 nu = np.zeros((ndim,), dtype=np.intc)
2037 else:
2038 nu = np.asarray(nu, dtype=np.intc)
2039 if nu.ndim != 1 or nu.shape[0] != ndim:
2040 raise ValueError("invalid number of derivative orders nu")
2042 dim1 = prod(self.c.shape[:ndim])
2043 dim2 = prod(self.c.shape[ndim:2*ndim])
2044 dim3 = prod(self.c.shape[2*ndim:])
2045 ks = np.array(self.c.shape[:ndim], dtype=np.intc)
2047 out = np.empty((x.shape[0], dim3), dtype=self.c.dtype)
2048 self._ensure_c_contiguous()
2050 _ppoly.evaluate_nd(self.c.reshape(dim1, dim2, dim3),
2051 self.x,
2052 ks,
2053 x,
2054 nu,
2055 bool(extrapolate),
2056 out)
2058 return out.reshape(x_shape[:-1] + self.c.shape[2*ndim:])
2060 def _derivative_inplace(self, nu, axis):
2061 """
2062 Compute 1-D derivative along a selected dimension in-place
2063 May result to non-contiguous c array.
2064 """
2065 if nu < 0:
2066 return self._antiderivative_inplace(-nu, axis)
2068 ndim = len(self.x)
2069 axis = axis % ndim
2071 # reduce order
2072 if nu == 0:
2073 # noop
2074 return
2075 else:
2076 sl = [slice(None)]*ndim
2077 sl[axis] = slice(None, -nu, None)
2078 c2 = self.c[tuple(sl)]
2080 if c2.shape[axis] == 0:
2081 # derivative of order 0 is zero
2082 shp = list(c2.shape)
2083 shp[axis] = 1
2084 c2 = np.zeros(shp, dtype=c2.dtype)
2086 # multiply by the correct rising factorials
2087 factor = spec.poch(np.arange(c2.shape[axis], 0, -1), nu)
2088 sl = [None]*c2.ndim
2089 sl[axis] = slice(None)
2090 c2 *= factor[tuple(sl)]
2092 self.c = c2
2094 def _antiderivative_inplace(self, nu, axis):
2095 """
2096 Compute 1-D antiderivative along a selected dimension
2097 May result to non-contiguous c array.
2098 """
2099 if nu <= 0:
2100 return self._derivative_inplace(-nu, axis)
2102 ndim = len(self.x)
2103 axis = axis % ndim
2105 perm = list(range(ndim))
2106 perm[0], perm[axis] = perm[axis], perm[0]
2107 perm = perm + list(range(ndim, self.c.ndim))
2109 c = self.c.transpose(perm)
2111 c2 = np.zeros((c.shape[0] + nu,) + c.shape[1:],
2112 dtype=c.dtype)
2113 c2[:-nu] = c
2115 # divide by the correct rising factorials
2116 factor = spec.poch(np.arange(c.shape[0], 0, -1), nu)
2117 c2[:-nu] /= factor[(slice(None),) + (None,)*(c.ndim-1)]
2119 # fix continuity of added degrees of freedom
2120 perm2 = list(range(c2.ndim))
2121 perm2[1], perm2[ndim+axis] = perm2[ndim+axis], perm2[1]
2123 c2 = c2.transpose(perm2)
2124 c2 = c2.copy()
2125 _ppoly.fix_continuity(c2.reshape(c2.shape[0], c2.shape[1], -1),
2126 self.x[axis], nu-1)
2128 c2 = c2.transpose(perm2)
2129 c2 = c2.transpose(perm)
2131 # Done
2132 self.c = c2
2134 def derivative(self, nu):
2135 """
2136 Construct a new piecewise polynomial representing the derivative.
2138 Parameters
2139 ----------
2140 nu : ndim-tuple of int
2141 Order of derivatives to evaluate for each dimension.
2142 If negative, the antiderivative is returned.
2144 Returns
2145 -------
2146 pp : NdPPoly
2147 Piecewise polynomial of orders (k[0] - nu[0], ..., k[n] - nu[n])
2148 representing the derivative of this polynomial.
2150 Notes
2151 -----
2152 Derivatives are evaluated piecewise for each polynomial
2153 segment, even if the polynomial is not differentiable at the
2154 breakpoints. The polynomial intervals in each dimension are
2155 considered half-open, ``[a, b)``, except for the last interval
2156 which is closed ``[a, b]``.
2158 """
2159 p = self.construct_fast(self.c.copy(), self.x, self.extrapolate)
2161 for axis, n in enumerate(nu):
2162 p._derivative_inplace(n, axis)
2164 p._ensure_c_contiguous()
2165 return p
2167 def antiderivative(self, nu):
2168 """
2169 Construct a new piecewise polynomial representing the antiderivative.
2171 Antiderivative is also the indefinite integral of the function,
2172 and derivative is its inverse operation.
2174 Parameters
2175 ----------
2176 nu : ndim-tuple of int
2177 Order of derivatives to evaluate for each dimension.
2178 If negative, the derivative is returned.
2180 Returns
2181 -------
2182 pp : PPoly
2183 Piecewise polynomial of order k2 = k + n representing
2184 the antiderivative of this polynomial.
2186 Notes
2187 -----
2188 The antiderivative returned by this function is continuous and
2189 continuously differentiable to order n-1, up to floating point
2190 rounding error.
2192 """
2193 p = self.construct_fast(self.c.copy(), self.x, self.extrapolate)
2195 for axis, n in enumerate(nu):
2196 p._antiderivative_inplace(n, axis)
2198 p._ensure_c_contiguous()
2199 return p
2201 def integrate_1d(self, a, b, axis, extrapolate=None):
2202 r"""
2203 Compute NdPPoly representation for one dimensional definite integral
2205 The result is a piecewise polynomial representing the integral:
2207 .. math::
2209 p(y, z, ...) = \int_a^b dx\, p(x, y, z, ...)
2211 where the dimension integrated over is specified with the
2212 `axis` parameter.
2214 Parameters
2215 ----------
2216 a, b : float
2217 Lower and upper bound for integration.
2218 axis : int
2219 Dimension over which to compute the 1-D integrals
2220 extrapolate : bool, optional
2221 Whether to extrapolate to out-of-bounds points based on first
2222 and last intervals, or to return NaNs.
2224 Returns
2225 -------
2226 ig : NdPPoly or array-like
2227 Definite integral of the piecewise polynomial over [a, b].
2228 If the polynomial was 1D, an array is returned,
2229 otherwise, an NdPPoly object.
2231 """
2232 if extrapolate is None:
2233 extrapolate = self.extrapolate
2234 else:
2235 extrapolate = bool(extrapolate)
2237 ndim = len(self.x)
2238 axis = int(axis) % ndim
2240 # reuse 1-D integration routines
2241 c = self.c
2242 swap = list(range(c.ndim))
2243 swap.insert(0, swap[axis])
2244 del swap[axis + 1]
2245 swap.insert(1, swap[ndim + axis])
2246 del swap[ndim + axis + 1]
2248 c = c.transpose(swap)
2249 p = PPoly.construct_fast(c.reshape(c.shape[0], c.shape[1], -1),
2250 self.x[axis],
2251 extrapolate=extrapolate)
2252 out = p.integrate(a, b, extrapolate=extrapolate)
2254 # Construct result
2255 if ndim == 1:
2256 return out.reshape(c.shape[2:])
2257 else:
2258 c = out.reshape(c.shape[2:])
2259 x = self.x[:axis] + self.x[axis+1:]
2260 return self.construct_fast(c, x, extrapolate=extrapolate)
2262 def integrate(self, ranges, extrapolate=None):
2263 """
2264 Compute a definite integral over a piecewise polynomial.
2266 Parameters
2267 ----------
2268 ranges : ndim-tuple of 2-tuples float
2269 Sequence of lower and upper bounds for each dimension,
2270 ``[(a[0], b[0]), ..., (a[ndim-1], b[ndim-1])]``
2271 extrapolate : bool, optional
2272 Whether to extrapolate to out-of-bounds points based on first
2273 and last intervals, or to return NaNs.
2275 Returns
2276 -------
2277 ig : array_like
2278 Definite integral of the piecewise polynomial over
2279 [a[0], b[0]] x ... x [a[ndim-1], b[ndim-1]]
2281 """
2283 ndim = len(self.x)
2285 if extrapolate is None:
2286 extrapolate = self.extrapolate
2287 else:
2288 extrapolate = bool(extrapolate)
2290 if not hasattr(ranges, '__len__') or len(ranges) != ndim:
2291 raise ValueError("Range not a sequence of correct length")
2293 self._ensure_c_contiguous()
2295 # Reuse 1D integration routine
2296 c = self.c
2297 for n, (a, b) in enumerate(ranges):
2298 swap = list(range(c.ndim))
2299 swap.insert(1, swap[ndim - n])
2300 del swap[ndim - n + 1]
2302 c = c.transpose(swap)
2304 p = PPoly.construct_fast(c, self.x[n], extrapolate=extrapolate)
2305 out = p.integrate(a, b, extrapolate=extrapolate)
2306 c = out.reshape(c.shape[2:])
2308 return c
2311class RegularGridInterpolator(object):
2312 """
2313 Interpolation on a regular grid in arbitrary dimensions
2315 The data must be defined on a regular grid; the grid spacing however may be
2316 uneven. Linear and nearest-neighbor interpolation are supported. After
2317 setting up the interpolator object, the interpolation method (*linear* or
2318 *nearest*) may be chosen at each evaluation.
2320 Parameters
2321 ----------
2322 points : tuple of ndarray of float, with shapes (m1, ), ..., (mn, )
2323 The points defining the regular grid in n dimensions.
2325 values : array_like, shape (m1, ..., mn, ...)
2326 The data on the regular grid in n dimensions.
2328 method : str, optional
2329 The method of interpolation to perform. Supported are "linear" and
2330 "nearest". This parameter will become the default for the object's
2331 ``__call__`` method. Default is "linear".
2333 bounds_error : bool, optional
2334 If True, when interpolated values are requested outside of the
2335 domain of the input data, a ValueError is raised.
2336 If False, then `fill_value` is used.
2338 fill_value : number, optional
2339 If provided, the value to use for points outside of the
2340 interpolation domain. If None, values outside
2341 the domain are extrapolated.
2343 Methods
2344 -------
2345 __call__
2347 Notes
2348 -----
2349 Contrary to LinearNDInterpolator and NearestNDInterpolator, this class
2350 avoids expensive triangulation of the input data by taking advantage of the
2351 regular grid structure.
2353 If any of `points` have a dimension of size 1, linear interpolation will
2354 return an array of `nan` values. Nearest-neighbor interpolation will work
2355 as usual in this case.
2357 .. versionadded:: 0.14
2359 Examples
2360 --------
2361 Evaluate a simple example function on the points of a 3-D grid:
2363 >>> from scipy.interpolate import RegularGridInterpolator
2364 >>> def f(x, y, z):
2365 ... return 2 * x**3 + 3 * y**2 - z
2366 >>> x = np.linspace(1, 4, 11)
2367 >>> y = np.linspace(4, 7, 22)
2368 >>> z = np.linspace(7, 9, 33)
2369 >>> data = f(*np.meshgrid(x, y, z, indexing='ij', sparse=True))
2371 ``data`` is now a 3-D array with ``data[i,j,k] = f(x[i], y[j], z[k])``.
2372 Next, define an interpolating function from this data:
2374 >>> my_interpolating_function = RegularGridInterpolator((x, y, z), data)
2376 Evaluate the interpolating function at the two points
2377 ``(x,y,z) = (2.1, 6.2, 8.3)`` and ``(3.3, 5.2, 7.1)``:
2379 >>> pts = np.array([[2.1, 6.2, 8.3], [3.3, 5.2, 7.1]])
2380 >>> my_interpolating_function(pts)
2381 array([ 125.80469388, 146.30069388])
2383 which is indeed a close approximation to
2384 ``[f(2.1, 6.2, 8.3), f(3.3, 5.2, 7.1)]``.
2386 See also
2387 --------
2388 NearestNDInterpolator : Nearest neighbor interpolation on unstructured
2389 data in N dimensions
2391 LinearNDInterpolator : Piecewise linear interpolant on unstructured data
2392 in N dimensions
2394 References
2395 ----------
2396 .. [1] Python package *regulargrid* by Johannes Buchner, see
2397 https://pypi.python.org/pypi/regulargrid/
2398 .. [2] Wikipedia, "Trilinear interpolation",
2399 https://en.wikipedia.org/wiki/Trilinear_interpolation
2400 .. [3] Weiser, Alan, and Sergio E. Zarantonello. "A note on piecewise linear
2401 and multilinear table interpolation in many dimensions." MATH.
2402 COMPUT. 50.181 (1988): 189-196.
2403 https://www.ams.org/journals/mcom/1988-50-181/S0025-5718-1988-0917826-0/S0025-5718-1988-0917826-0.pdf
2405 """
2406 # this class is based on code originally programmed by Johannes Buchner,
2407 # see https://github.com/JohannesBuchner/regulargrid
2409 def __init__(self, points, values, method="linear", bounds_error=True,
2410 fill_value=np.nan):
2411 if method not in ["linear", "nearest"]:
2412 raise ValueError("Method '%s' is not defined" % method)
2413 self.method = method
2414 self.bounds_error = bounds_error
2416 if not hasattr(values, 'ndim'):
2417 # allow reasonable duck-typed values
2418 values = np.asarray(values)
2420 if len(points) > values.ndim:
2421 raise ValueError("There are %d point arrays, but values has %d "
2422 "dimensions" % (len(points), values.ndim))
2424 if hasattr(values, 'dtype') and hasattr(values, 'astype'):
2425 if not np.issubdtype(values.dtype, np.inexact):
2426 values = values.astype(float)
2428 self.fill_value = fill_value
2429 if fill_value is not None:
2430 fill_value_dtype = np.asarray(fill_value).dtype
2431 if (hasattr(values, 'dtype') and not
2432 np.can_cast(fill_value_dtype, values.dtype,
2433 casting='same_kind')):
2434 raise ValueError("fill_value must be either 'None' or "
2435 "of a type compatible with values")
2437 for i, p in enumerate(points):
2438 if not np.all(np.diff(p) > 0.):
2439 raise ValueError("The points in dimension %d must be strictly "
2440 "ascending" % i)
2441 if not np.asarray(p).ndim == 1:
2442 raise ValueError("The points in dimension %d must be "
2443 "1-dimensional" % i)
2444 if not values.shape[i] == len(p):
2445 raise ValueError("There are %d points and %d values in "
2446 "dimension %d" % (len(p), values.shape[i], i))
2447 self.grid = tuple([np.asarray(p) for p in points])
2448 self.values = values
2450 def __call__(self, xi, method=None):
2451 """
2452 Interpolation at coordinates
2454 Parameters
2455 ----------
2456 xi : ndarray of shape (..., ndim)
2457 The coordinates to sample the gridded data at
2459 method : str
2460 The method of interpolation to perform. Supported are "linear" and
2461 "nearest".
2463 """
2464 method = self.method if method is None else method
2465 if method not in ["linear", "nearest"]:
2466 raise ValueError("Method '%s' is not defined" % method)
2468 ndim = len(self.grid)
2469 xi = _ndim_coords_from_arrays(xi, ndim=ndim)
2470 if xi.shape[-1] != len(self.grid):
2471 raise ValueError("The requested sample points xi have dimension "
2472 "%d, but this RegularGridInterpolator has "
2473 "dimension %d" % (xi.shape[1], ndim))
2475 xi_shape = xi.shape
2476 xi = xi.reshape(-1, xi_shape[-1])
2478 if self.bounds_error:
2479 for i, p in enumerate(xi.T):
2480 if not np.logical_and(np.all(self.grid[i][0] <= p),
2481 np.all(p <= self.grid[i][-1])):
2482 raise ValueError("One of the requested xi is out of bounds "
2483 "in dimension %d" % i)
2485 indices, norm_distances, out_of_bounds = self._find_indices(xi.T)
2486 if method == "linear":
2487 result = self._evaluate_linear(indices,
2488 norm_distances,
2489 out_of_bounds)
2490 elif method == "nearest":
2491 result = self._evaluate_nearest(indices,
2492 norm_distances,
2493 out_of_bounds)
2494 if not self.bounds_error and self.fill_value is not None:
2495 result[out_of_bounds] = self.fill_value
2497 return result.reshape(xi_shape[:-1] + self.values.shape[ndim:])
2499 def _evaluate_linear(self, indices, norm_distances, out_of_bounds):
2500 # slice for broadcasting over trailing dimensions in self.values
2501 vslice = (slice(None),) + (None,)*(self.values.ndim - len(indices))
2503 # find relevant values
2504 # each i and i+1 represents a edge
2505 edges = itertools.product(*[[i, i + 1] for i in indices])
2506 values = 0.
2507 for edge_indices in edges:
2508 weight = 1.
2509 for ei, i, yi in zip(edge_indices, indices, norm_distances):
2510 weight *= np.where(ei == i, 1 - yi, yi)
2511 values += np.asarray(self.values[edge_indices]) * weight[vslice]
2512 return values
2514 def _evaluate_nearest(self, indices, norm_distances, out_of_bounds):
2515 idx_res = [np.where(yi <= .5, i, i + 1)
2516 for i, yi in zip(indices, norm_distances)]
2517 return self.values[tuple(idx_res)]
2519 def _find_indices(self, xi):
2520 # find relevant edges between which xi are situated
2521 indices = []
2522 # compute distance to lower edge in unity units
2523 norm_distances = []
2524 # check for out of bounds xi
2525 out_of_bounds = np.zeros((xi.shape[1]), dtype=bool)
2526 # iterate through dimensions
2527 for x, grid in zip(xi, self.grid):
2528 i = np.searchsorted(grid, x) - 1
2529 i[i < 0] = 0
2530 i[i > grid.size - 2] = grid.size - 2
2531 indices.append(i)
2532 norm_distances.append((x - grid[i]) /
2533 (grid[i + 1] - grid[i]))
2534 if not self.bounds_error:
2535 out_of_bounds += x < grid[0]
2536 out_of_bounds += x > grid[-1]
2537 return indices, norm_distances, out_of_bounds
2540def interpn(points, values, xi, method="linear", bounds_error=True,
2541 fill_value=np.nan):
2542 """
2543 Multidimensional interpolation on regular grids.
2545 Parameters
2546 ----------
2547 points : tuple of ndarray of float, with shapes (m1, ), ..., (mn, )
2548 The points defining the regular grid in n dimensions.
2550 values : array_like, shape (m1, ..., mn, ...)
2551 The data on the regular grid in n dimensions.
2553 xi : ndarray of shape (..., ndim)
2554 The coordinates to sample the gridded data at
2556 method : str, optional
2557 The method of interpolation to perform. Supported are "linear" and
2558 "nearest", and "splinef2d". "splinef2d" is only supported for
2559 2-dimensional data.
2561 bounds_error : bool, optional
2562 If True, when interpolated values are requested outside of the
2563 domain of the input data, a ValueError is raised.
2564 If False, then `fill_value` is used.
2566 fill_value : number, optional
2567 If provided, the value to use for points outside of the
2568 interpolation domain. If None, values outside
2569 the domain are extrapolated. Extrapolation is not supported by method
2570 "splinef2d".
2572 Returns
2573 -------
2574 values_x : ndarray, shape xi.shape[:-1] + values.shape[ndim:]
2575 Interpolated values at input coordinates.
2577 Notes
2578 -----
2580 .. versionadded:: 0.14
2582 Examples
2583 --------
2584 Evaluate a simple example function on the points of a regular 3-D grid:
2586 >>> from scipy.interpolate import interpn
2587 >>> def value_func_3d(x, y, z):
2588 ... return 2 * x + 3 * y - z
2589 >>> x = np.linspace(0, 5)
2590 >>> y = np.linspace(0, 5)
2591 >>> z = np.linspace(0, 5)
2592 >>> points = (x, y, z)
2593 >>> values = value_func_3d(*np.meshgrid(*points))
2595 Evaluate the interpolating function at a point
2597 >>> point = np.array([2.21, 3.12, 1.15])
2598 >>> print(interpn(points, values, point))
2599 [11.72]
2601 See also
2602 --------
2603 NearestNDInterpolator : Nearest neighbor interpolation on unstructured
2604 data in N dimensions
2606 LinearNDInterpolator : Piecewise linear interpolant on unstructured data
2607 in N dimensions
2609 RegularGridInterpolator : Linear and nearest-neighbor Interpolation on a
2610 regular grid in arbitrary dimensions
2612 RectBivariateSpline : Bivariate spline approximation over a rectangular mesh
2614 """
2615 # sanity check 'method' kwarg
2616 if method not in ["linear", "nearest", "splinef2d"]:
2617 raise ValueError("interpn only understands the methods 'linear', "
2618 "'nearest', and 'splinef2d'. You provided %s." %
2619 method)
2621 if not hasattr(values, 'ndim'):
2622 values = np.asarray(values)
2624 ndim = values.ndim
2625 if ndim > 2 and method == "splinef2d":
2626 raise ValueError("The method splinef2d can only be used for "
2627 "2-dimensional input data")
2628 if not bounds_error and fill_value is None and method == "splinef2d":
2629 raise ValueError("The method splinef2d does not support extrapolation.")
2631 # sanity check consistency of input dimensions
2632 if len(points) > ndim:
2633 raise ValueError("There are %d point arrays, but values has %d "
2634 "dimensions" % (len(points), ndim))
2635 if len(points) != ndim and method == 'splinef2d':
2636 raise ValueError("The method splinef2d can only be used for "
2637 "scalar data with one point per coordinate")
2639 # sanity check input grid
2640 for i, p in enumerate(points):
2641 if not np.all(np.diff(p) > 0.):
2642 raise ValueError("The points in dimension %d must be strictly "
2643 "ascending" % i)
2644 if not np.asarray(p).ndim == 1:
2645 raise ValueError("The points in dimension %d must be "
2646 "1-dimensional" % i)
2647 if not values.shape[i] == len(p):
2648 raise ValueError("There are %d points and %d values in "
2649 "dimension %d" % (len(p), values.shape[i], i))
2650 grid = tuple([np.asarray(p) for p in points])
2652 # sanity check requested xi
2653 xi = _ndim_coords_from_arrays(xi, ndim=len(grid))
2654 if xi.shape[-1] != len(grid):
2655 raise ValueError("The requested sample points xi have dimension "
2656 "%d, but this RegularGridInterpolator has "
2657 "dimension %d" % (xi.shape[1], len(grid)))
2659 for i, p in enumerate(xi.T):
2660 if bounds_error and not np.logical_and(np.all(grid[i][0] <= p),
2661 np.all(p <= grid[i][-1])):
2662 raise ValueError("One of the requested xi is out of bounds "
2663 "in dimension %d" % i)
2665 # perform interpolation
2666 if method == "linear":
2667 interp = RegularGridInterpolator(points, values, method="linear",
2668 bounds_error=bounds_error,
2669 fill_value=fill_value)
2670 return interp(xi)
2671 elif method == "nearest":
2672 interp = RegularGridInterpolator(points, values, method="nearest",
2673 bounds_error=bounds_error,
2674 fill_value=fill_value)
2675 return interp(xi)
2676 elif method == "splinef2d":
2677 xi_shape = xi.shape
2678 xi = xi.reshape(-1, xi.shape[-1])
2680 # RectBivariateSpline doesn't support fill_value; we need to wrap here
2681 idx_valid = np.all((grid[0][0] <= xi[:, 0], xi[:, 0] <= grid[0][-1],
2682 grid[1][0] <= xi[:, 1], xi[:, 1] <= grid[1][-1]),
2683 axis=0)
2684 result = np.empty_like(xi[:, 0])
2686 # make a copy of values for RectBivariateSpline
2687 interp = RectBivariateSpline(points[0], points[1], values[:])
2688 result[idx_valid] = interp.ev(xi[idx_valid, 0], xi[idx_valid, 1])
2689 result[np.logical_not(idx_valid)] = fill_value
2691 return result.reshape(xi_shape[:-1])
2694# backward compatibility wrapper
2695class _ppform(PPoly):
2696 """
2697 Deprecated piecewise polynomial class.
2699 New code should use the `PPoly` class instead.
2701 """
2703 def __init__(self, coeffs, breaks, fill=0.0, sort=False):
2704 warnings.warn("_ppform is deprecated -- use PPoly instead",
2705 category=DeprecationWarning)
2707 if sort:
2708 breaks = np.sort(breaks)
2709 else:
2710 breaks = np.asarray(breaks)
2712 PPoly.__init__(self, coeffs, breaks)
2714 self.coeffs = self.c
2715 self.breaks = self.x
2716 self.K = self.coeffs.shape[0]
2717 self.fill = fill
2718 self.a = self.breaks[0]
2719 self.b = self.breaks[-1]
2721 def __call__(self, x):
2722 return PPoly.__call__(self, x, 0, False)
2724 def _evaluate(self, x, nu, extrapolate, out):
2725 PPoly._evaluate(self, x, nu, extrapolate, out)
2726 out[~((x >= self.a) & (x <= self.b))] = self.fill
2727 return out
2729 @classmethod
2730 def fromspline(cls, xk, cvals, order, fill=0.0):
2731 # Note: this spline representation is incompatible with FITPACK
2732 N = len(xk)-1
2733 sivals = np.empty((order+1, N), dtype=float)
2734 for m in range(order, -1, -1):
2735 fact = spec.gamma(m+1)
2736 res = _fitpack._bspleval(xk[:-1], xk, cvals, order, m)
2737 res /= fact
2738 sivals[order-m, :] = res
2739 return cls(sivals, xk, fill=fill)