2243 lines
78 KiB
Python
2243 lines
78 KiB
Python
|
__all__ = ['interp1d', 'interp2d', 'lagrange', 'PPoly', 'BPoly', 'NdPPoly']
|
||
|
|
||
|
from math import prod
|
||
|
|
||
|
import numpy as np
|
||
|
from numpy import array, asarray, intp, poly1d, searchsorted
|
||
|
|
||
|
import scipy.special as spec
|
||
|
from scipy._lib._util import copy_if_needed
|
||
|
from scipy.special import comb
|
||
|
|
||
|
from . import _fitpack_py
|
||
|
from ._polyint import _Interpolator1D
|
||
|
from . import _ppoly
|
||
|
from .interpnd import _ndim_coords_from_arrays
|
||
|
from ._bsplines import make_interp_spline, BSpline
|
||
|
|
||
|
|
||
|
def lagrange(x, w):
|
||
|
r"""
|
||
|
Return a Lagrange interpolating polynomial.
|
||
|
|
||
|
Given two 1-D arrays `x` and `w,` returns the Lagrange interpolating
|
||
|
polynomial through the points ``(x, w)``.
|
||
|
|
||
|
Warning: This implementation is numerically unstable. Do not expect to
|
||
|
be able to use more than about 20 points even if they are chosen optimally.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
x : array_like
|
||
|
`x` represents the x-coordinates of a set of datapoints.
|
||
|
w : array_like
|
||
|
`w` represents the y-coordinates of a set of datapoints, i.e., f(`x`).
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
lagrange : `numpy.poly1d` instance
|
||
|
The Lagrange interpolating polynomial.
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
Interpolate :math:`f(x) = x^3` by 3 points.
|
||
|
|
||
|
>>> import numpy as np
|
||
|
>>> from scipy.interpolate import lagrange
|
||
|
>>> x = np.array([0, 1, 2])
|
||
|
>>> y = x**3
|
||
|
>>> poly = lagrange(x, y)
|
||
|
|
||
|
Since there are only 3 points, Lagrange polynomial has degree 2. Explicitly,
|
||
|
it is given by
|
||
|
|
||
|
.. math::
|
||
|
|
||
|
\begin{aligned}
|
||
|
L(x) &= 1\times \frac{x (x - 2)}{-1} + 8\times \frac{x (x-1)}{2} \\
|
||
|
&= x (-2 + 3x)
|
||
|
\end{aligned}
|
||
|
|
||
|
>>> from numpy.polynomial.polynomial import Polynomial
|
||
|
>>> Polynomial(poly.coef[::-1]).coef
|
||
|
array([ 0., -2., 3.])
|
||
|
|
||
|
>>> import matplotlib.pyplot as plt
|
||
|
>>> x_new = np.arange(0, 2.1, 0.1)
|
||
|
>>> plt.scatter(x, y, label='data')
|
||
|
>>> plt.plot(x_new, Polynomial(poly.coef[::-1])(x_new), label='Polynomial')
|
||
|
>>> plt.plot(x_new, 3*x_new**2 - 2*x_new + 0*x_new,
|
||
|
... label=r"$3 x^2 - 2 x$", linestyle='-.')
|
||
|
>>> plt.legend()
|
||
|
>>> plt.show()
|
||
|
|
||
|
"""
|
||
|
|
||
|
M = len(x)
|
||
|
p = poly1d(0.0)
|
||
|
for j in range(M):
|
||
|
pt = poly1d(w[j])
|
||
|
for k in range(M):
|
||
|
if k == j:
|
||
|
continue
|
||
|
fac = x[j]-x[k]
|
||
|
pt *= poly1d([1.0, -x[k]])/fac
|
||
|
p += pt
|
||
|
return p
|
||
|
|
||
|
|
||
|
# !! Need to find argument for keeping initialize. If it isn't
|
||
|
# !! found, get rid of it!
|
||
|
|
||
|
|
||
|
err_mesg = """\
|
||
|
`interp2d` has been removed in SciPy 1.14.0.
|
||
|
|
||
|
For legacy code, nearly bug-for-bug compatible replacements are
|
||
|
`RectBivariateSpline` on regular grids, and `bisplrep`/`bisplev` for
|
||
|
scattered 2D data.
|
||
|
|
||
|
In new code, for regular grids use `RegularGridInterpolator` instead.
|
||
|
For scattered data, prefer `LinearNDInterpolator` or
|
||
|
`CloughTocher2DInterpolator`.
|
||
|
|
||
|
For more details see
|
||
|
https://scipy.github.io/devdocs/tutorial/interpolate/interp_transition_guide.html
|
||
|
"""
|
||
|
|
||
|
class interp2d:
|
||
|
"""
|
||
|
interp2d(x, y, z, kind='linear', copy=True, bounds_error=False,
|
||
|
fill_value=None)
|
||
|
|
||
|
.. versionremoved:: 1.14.0
|
||
|
|
||
|
`interp2d` has been removed in SciPy 1.14.0.
|
||
|
|
||
|
For legacy code, nearly bug-for-bug compatible replacements are
|
||
|
`RectBivariateSpline` on regular grids, and `bisplrep`/`bisplev` for
|
||
|
scattered 2D data.
|
||
|
|
||
|
In new code, for regular grids use `RegularGridInterpolator` instead.
|
||
|
For scattered data, prefer `LinearNDInterpolator` or
|
||
|
`CloughTocher2DInterpolator`.
|
||
|
|
||
|
For more details see :ref:`interp-transition-guide`.
|
||
|
"""
|
||
|
def __init__(self, x, y, z, kind='linear', copy=True, bounds_error=False,
|
||
|
fill_value=None):
|
||
|
raise NotImplementedError(err_mesg)
|
||
|
|
||
|
|
||
|
def _check_broadcast_up_to(arr_from, shape_to, name):
|
||
|
"""Helper to check that arr_from broadcasts up to shape_to"""
|
||
|
shape_from = arr_from.shape
|
||
|
if len(shape_to) >= len(shape_from):
|
||
|
for t, f in zip(shape_to[::-1], shape_from[::-1]):
|
||
|
if f != 1 and f != t:
|
||
|
break
|
||
|
else: # all checks pass, do the upcasting that we need later
|
||
|
if arr_from.size != 1 and arr_from.shape != shape_to:
|
||
|
arr_from = np.ones(shape_to, arr_from.dtype) * arr_from
|
||
|
return arr_from.ravel()
|
||
|
# at least one check failed
|
||
|
raise ValueError(f'{name} argument must be able to broadcast up '
|
||
|
f'to shape {shape_to} but had shape {shape_from}')
|
||
|
|
||
|
|
||
|
def _do_extrapolate(fill_value):
|
||
|
"""Helper to check if fill_value == "extrapolate" without warnings"""
|
||
|
return (isinstance(fill_value, str) and
|
||
|
fill_value == 'extrapolate')
|
||
|
|
||
|
|
||
|
class interp1d(_Interpolator1D):
|
||
|
"""
|
||
|
Interpolate a 1-D function.
|
||
|
|
||
|
.. legacy:: class
|
||
|
|
||
|
For a guide to the intended replacements for `interp1d` see
|
||
|
:ref:`tutorial-interpolate_1Dsection`.
|
||
|
|
||
|
`x` and `y` are arrays of values used to approximate some function f:
|
||
|
``y = f(x)``. This class returns a function whose call method uses
|
||
|
interpolation to find the value of new points.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
x : (npoints, ) array_like
|
||
|
A 1-D array of real values.
|
||
|
y : (..., npoints, ...) array_like
|
||
|
A N-D array of real values. The length of `y` along the interpolation
|
||
|
axis must be equal to the length of `x`. Use the ``axis`` parameter
|
||
|
to select correct axis. Unlike other interpolators, the default
|
||
|
interpolation axis is the last axis of `y`.
|
||
|
kind : str or int, optional
|
||
|
Specifies the kind of interpolation as a string or as an integer
|
||
|
specifying the order of the spline interpolator to use.
|
||
|
The string has to be one of 'linear', 'nearest', 'nearest-up', 'zero',
|
||
|
'slinear', 'quadratic', 'cubic', 'previous', or 'next'. 'zero',
|
||
|
'slinear', 'quadratic' and 'cubic' refer to a spline interpolation of
|
||
|
zeroth, first, second or third order; 'previous' and 'next' simply
|
||
|
return the previous or next value of the point; 'nearest-up' and
|
||
|
'nearest' differ when interpolating half-integers (e.g. 0.5, 1.5)
|
||
|
in that 'nearest-up' rounds up and 'nearest' rounds down. Default
|
||
|
is 'linear'.
|
||
|
axis : int, optional
|
||
|
Axis in the ``y`` array corresponding to the x-coordinate values. Unlike
|
||
|
other interpolators, defaults to ``axis=-1``.
|
||
|
copy : bool, optional
|
||
|
If ``True``, the class makes internal copies of x and y. If ``False``,
|
||
|
references to ``x`` and ``y`` are used if possible. The default is to copy.
|
||
|
bounds_error : bool, optional
|
||
|
If True, a ValueError is raised any time interpolation is attempted on
|
||
|
a value outside of the range of x (where extrapolation is
|
||
|
necessary). If False, out of bounds values are assigned `fill_value`.
|
||
|
By default, an error is raised unless ``fill_value="extrapolate"``.
|
||
|
fill_value : array-like or (array-like, array_like) or "extrapolate", optional
|
||
|
- if a ndarray (or float), this value will be used to fill in for
|
||
|
requested points outside of the data range. If not provided, then
|
||
|
the default is NaN. The array-like must broadcast properly to the
|
||
|
dimensions of the non-interpolation axes.
|
||
|
- If a two-element tuple, then the first element is used as a
|
||
|
fill value for ``x_new < x[0]`` and the second element is used for
|
||
|
``x_new > x[-1]``. Anything that is not a 2-element tuple (e.g.,
|
||
|
list or ndarray, regardless of shape) is taken to be a single
|
||
|
array-like argument meant to be used for both bounds as
|
||
|
``below, above = fill_value, fill_value``. Using a two-element tuple
|
||
|
or ndarray requires ``bounds_error=False``.
|
||
|
|
||
|
.. versionadded:: 0.17.0
|
||
|
- If "extrapolate", then points outside the data range will be
|
||
|
extrapolated.
|
||
|
|
||
|
.. versionadded:: 0.17.0
|
||
|
assume_sorted : bool, optional
|
||
|
If False, values of `x` can be in any order and they are sorted first.
|
||
|
If True, `x` has to be an array of monotonically increasing values.
|
||
|
|
||
|
Attributes
|
||
|
----------
|
||
|
fill_value
|
||
|
|
||
|
Methods
|
||
|
-------
|
||
|
__call__
|
||
|
|
||
|
See Also
|
||
|
--------
|
||
|
splrep, splev
|
||
|
Spline interpolation/smoothing based on FITPACK.
|
||
|
UnivariateSpline : An object-oriented wrapper of the FITPACK routines.
|
||
|
interp2d : 2-D interpolation
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
Calling `interp1d` with NaNs present in input values results in
|
||
|
undefined behaviour.
|
||
|
|
||
|
Input values `x` and `y` must be convertible to `float` values like
|
||
|
`int` or `float`.
|
||
|
|
||
|
If the values in `x` are not unique, the resulting behavior is
|
||
|
undefined and specific to the choice of `kind`, i.e., changing
|
||
|
`kind` will change the behavior for duplicates.
|
||
|
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
>>> import numpy as np
|
||
|
>>> import matplotlib.pyplot as plt
|
||
|
>>> from scipy import interpolate
|
||
|
>>> x = np.arange(0, 10)
|
||
|
>>> y = np.exp(-x/3.0)
|
||
|
>>> f = interpolate.interp1d(x, y)
|
||
|
|
||
|
>>> xnew = np.arange(0, 9, 0.1)
|
||
|
>>> ynew = f(xnew) # use interpolation function returned by `interp1d`
|
||
|
>>> plt.plot(x, y, 'o', xnew, ynew, '-')
|
||
|
>>> plt.show()
|
||
|
"""
|
||
|
|
||
|
def __init__(self, x, y, kind='linear', axis=-1,
|
||
|
copy=True, bounds_error=None, fill_value=np.nan,
|
||
|
assume_sorted=False):
|
||
|
""" Initialize a 1-D linear interpolation class."""
|
||
|
_Interpolator1D.__init__(self, x, y, axis=axis)
|
||
|
|
||
|
self.bounds_error = bounds_error # used by fill_value setter
|
||
|
|
||
|
# `copy` keyword semantics changed in NumPy 2.0, once that is
|
||
|
# the minimum version this can use `copy=None`.
|
||
|
self.copy = copy
|
||
|
if not copy:
|
||
|
self.copy = copy_if_needed
|
||
|
|
||
|
if kind in ['zero', 'slinear', 'quadratic', 'cubic']:
|
||
|
order = {'zero': 0, 'slinear': 1,
|
||
|
'quadratic': 2, 'cubic': 3}[kind]
|
||
|
kind = 'spline'
|
||
|
elif isinstance(kind, int):
|
||
|
order = kind
|
||
|
kind = 'spline'
|
||
|
elif kind not in ('linear', 'nearest', 'nearest-up', 'previous',
|
||
|
'next'):
|
||
|
raise NotImplementedError("%s is unsupported: Use fitpack "
|
||
|
"routines for other types." % kind)
|
||
|
x = array(x, copy=self.copy)
|
||
|
y = array(y, copy=self.copy)
|
||
|
|
||
|
if not assume_sorted:
|
||
|
ind = np.argsort(x, kind="mergesort")
|
||
|
x = x[ind]
|
||
|
y = np.take(y, ind, axis=axis)
|
||
|
|
||
|
if x.ndim != 1:
|
||
|
raise ValueError("the x array must have exactly one dimension.")
|
||
|
if y.ndim == 0:
|
||
|
raise ValueError("the y array must have at least one dimension.")
|
||
|
|
||
|
# Force-cast y to a floating-point type, if it's not yet one
|
||
|
if not issubclass(y.dtype.type, np.inexact):
|
||
|
y = y.astype(np.float64)
|
||
|
|
||
|
# Backward compatibility
|
||
|
self.axis = axis % y.ndim
|
||
|
|
||
|
# Interpolation goes internally along the first axis
|
||
|
self.y = y
|
||
|
self._y = self._reshape_yi(self.y)
|
||
|
self.x = x
|
||
|
del y, x # clean up namespace to prevent misuse; use attributes
|
||
|
self._kind = kind
|
||
|
|
||
|
# Adjust to interpolation kind; store reference to *unbound*
|
||
|
# interpolation methods, in order to avoid circular references to self
|
||
|
# stored in the bound instance methods, and therefore delayed garbage
|
||
|
# collection. See: https://docs.python.org/reference/datamodel.html
|
||
|
if kind in ('linear', 'nearest', 'nearest-up', 'previous', 'next'):
|
||
|
# Make a "view" of the y array that is rotated to the interpolation
|
||
|
# axis.
|
||
|
minval = 1
|
||
|
if kind == 'nearest':
|
||
|
# Do division before addition to prevent possible integer
|
||
|
# overflow
|
||
|
self._side = 'left'
|
||
|
self.x_bds = self.x / 2.0
|
||
|
self.x_bds = self.x_bds[1:] + self.x_bds[:-1]
|
||
|
|
||
|
self._call = self.__class__._call_nearest
|
||
|
elif kind == 'nearest-up':
|
||
|
# Do division before addition to prevent possible integer
|
||
|
# overflow
|
||
|
self._side = 'right'
|
||
|
self.x_bds = self.x / 2.0
|
||
|
self.x_bds = self.x_bds[1:] + self.x_bds[:-1]
|
||
|
|
||
|
self._call = self.__class__._call_nearest
|
||
|
elif kind == 'previous':
|
||
|
# Side for np.searchsorted and index for clipping
|
||
|
self._side = 'left'
|
||
|
self._ind = 0
|
||
|
# Move x by one floating point value to the left
|
||
|
self._x_shift = np.nextafter(self.x, -np.inf)
|
||
|
self._call = self.__class__._call_previousnext
|
||
|
if _do_extrapolate(fill_value):
|
||
|
self._check_and_update_bounds_error_for_extrapolation()
|
||
|
# assume y is sorted by x ascending order here.
|
||
|
fill_value = (np.nan, np.take(self.y, -1, axis))
|
||
|
elif kind == 'next':
|
||
|
self._side = 'right'
|
||
|
self._ind = 1
|
||
|
# Move x by one floating point value to the right
|
||
|
self._x_shift = np.nextafter(self.x, np.inf)
|
||
|
self._call = self.__class__._call_previousnext
|
||
|
if _do_extrapolate(fill_value):
|
||
|
self._check_and_update_bounds_error_for_extrapolation()
|
||
|
# assume y is sorted by x ascending order here.
|
||
|
fill_value = (np.take(self.y, 0, axis), np.nan)
|
||
|
else:
|
||
|
# Check if we can delegate to numpy.interp (2x-10x faster).
|
||
|
np_dtypes = (np.dtype(np.float64), np.dtype(int))
|
||
|
cond = self.x.dtype in np_dtypes and self.y.dtype in np_dtypes
|
||
|
cond = cond and self.y.ndim == 1
|
||
|
cond = cond and not _do_extrapolate(fill_value)
|
||
|
|
||
|
if cond:
|
||
|
self._call = self.__class__._call_linear_np
|
||
|
else:
|
||
|
self._call = self.__class__._call_linear
|
||
|
else:
|
||
|
minval = order + 1
|
||
|
|
||
|
rewrite_nan = False
|
||
|
xx, yy = self.x, self._y
|
||
|
if order > 1:
|
||
|
# Quadratic or cubic spline. If input contains even a single
|
||
|
# nan, then the output is all nans. We cannot just feed data
|
||
|
# with nans to make_interp_spline because it calls LAPACK.
|
||
|
# So, we make up a bogus x and y with no nans and use it
|
||
|
# to get the correct shape of the output, which we then fill
|
||
|
# with nans.
|
||
|
# For slinear or zero order spline, we just pass nans through.
|
||
|
mask = np.isnan(self.x)
|
||
|
if mask.any():
|
||
|
sx = self.x[~mask]
|
||
|
if sx.size == 0:
|
||
|
raise ValueError("`x` array is all-nan")
|
||
|
xx = np.linspace(np.nanmin(self.x),
|
||
|
np.nanmax(self.x),
|
||
|
len(self.x))
|
||
|
rewrite_nan = True
|
||
|
if np.isnan(self._y).any():
|
||
|
yy = np.ones_like(self._y)
|
||
|
rewrite_nan = True
|
||
|
|
||
|
self._spline = make_interp_spline(xx, yy, k=order,
|
||
|
check_finite=False)
|
||
|
if rewrite_nan:
|
||
|
self._call = self.__class__._call_nan_spline
|
||
|
else:
|
||
|
self._call = self.__class__._call_spline
|
||
|
|
||
|
if len(self.x) < minval:
|
||
|
raise ValueError("x and y arrays must have at "
|
||
|
"least %d entries" % minval)
|
||
|
|
||
|
self.fill_value = fill_value # calls the setter, can modify bounds_err
|
||
|
|
||
|
@property
|
||
|
def fill_value(self):
|
||
|
"""The fill value."""
|
||
|
# backwards compat: mimic a public attribute
|
||
|
return self._fill_value_orig
|
||
|
|
||
|
@fill_value.setter
|
||
|
def fill_value(self, fill_value):
|
||
|
# extrapolation only works for nearest neighbor and linear methods
|
||
|
if _do_extrapolate(fill_value):
|
||
|
self._check_and_update_bounds_error_for_extrapolation()
|
||
|
self._extrapolate = True
|
||
|
else:
|
||
|
broadcast_shape = (self.y.shape[:self.axis] +
|
||
|
self.y.shape[self.axis + 1:])
|
||
|
if len(broadcast_shape) == 0:
|
||
|
broadcast_shape = (1,)
|
||
|
# it's either a pair (_below_range, _above_range) or a single value
|
||
|
# for both above and below range
|
||
|
if isinstance(fill_value, tuple) and len(fill_value) == 2:
|
||
|
below_above = [np.asarray(fill_value[0]),
|
||
|
np.asarray(fill_value[1])]
|
||
|
names = ('fill_value (below)', 'fill_value (above)')
|
||
|
for ii in range(2):
|
||
|
below_above[ii] = _check_broadcast_up_to(
|
||
|
below_above[ii], broadcast_shape, names[ii])
|
||
|
else:
|
||
|
fill_value = np.asarray(fill_value)
|
||
|
below_above = [_check_broadcast_up_to(
|
||
|
fill_value, broadcast_shape, 'fill_value')] * 2
|
||
|
self._fill_value_below, self._fill_value_above = below_above
|
||
|
self._extrapolate = False
|
||
|
if self.bounds_error is None:
|
||
|
self.bounds_error = True
|
||
|
# backwards compat: fill_value was a public attr; make it writeable
|
||
|
self._fill_value_orig = fill_value
|
||
|
|
||
|
def _check_and_update_bounds_error_for_extrapolation(self):
|
||
|
if self.bounds_error:
|
||
|
raise ValueError("Cannot extrapolate and raise "
|
||
|
"at the same time.")
|
||
|
self.bounds_error = False
|
||
|
|
||
|
def _call_linear_np(self, x_new):
|
||
|
# Note that out-of-bounds values are taken care of in self._evaluate
|
||
|
return np.interp(x_new, self.x, self.y)
|
||
|
|
||
|
def _call_linear(self, x_new):
|
||
|
# 2. Find where in the original data, the values to interpolate
|
||
|
# would be inserted.
|
||
|
# Note: If x_new[n] == x[m], then m is returned by searchsorted.
|
||
|
x_new_indices = searchsorted(self.x, x_new)
|
||
|
|
||
|
# 3. Clip x_new_indices so that they are within the range of
|
||
|
# self.x indices and at least 1. Removes mis-interpolation
|
||
|
# of x_new[n] = x[0]
|
||
|
x_new_indices = x_new_indices.clip(1, len(self.x)-1).astype(int)
|
||
|
|
||
|
# 4. Calculate the slope of regions that each x_new value falls in.
|
||
|
lo = x_new_indices - 1
|
||
|
hi = x_new_indices
|
||
|
|
||
|
x_lo = self.x[lo]
|
||
|
x_hi = self.x[hi]
|
||
|
y_lo = self._y[lo]
|
||
|
y_hi = self._y[hi]
|
||
|
|
||
|
# Note that the following two expressions rely on the specifics of the
|
||
|
# broadcasting semantics.
|
||
|
slope = (y_hi - y_lo) / (x_hi - x_lo)[:, None]
|
||
|
|
||
|
# 5. Calculate the actual value for each entry in x_new.
|
||
|
y_new = slope*(x_new - x_lo)[:, None] + y_lo
|
||
|
|
||
|
return y_new
|
||
|
|
||
|
def _call_nearest(self, x_new):
|
||
|
""" Find nearest neighbor interpolated y_new = f(x_new)."""
|
||
|
|
||
|
# 2. Find where in the averaged data the values to interpolate
|
||
|
# would be inserted.
|
||
|
# Note: use side='left' (right) to searchsorted() to define the
|
||
|
# halfway point to be nearest to the left (right) neighbor
|
||
|
x_new_indices = searchsorted(self.x_bds, x_new, side=self._side)
|
||
|
|
||
|
# 3. Clip x_new_indices so that they are within the range of x indices.
|
||
|
x_new_indices = x_new_indices.clip(0, len(self.x)-1).astype(intp)
|
||
|
|
||
|
# 4. Calculate the actual value for each entry in x_new.
|
||
|
y_new = self._y[x_new_indices]
|
||
|
|
||
|
return y_new
|
||
|
|
||
|
def _call_previousnext(self, x_new):
|
||
|
"""Use previous/next neighbor of x_new, y_new = f(x_new)."""
|
||
|
|
||
|
# 1. Get index of left/right value
|
||
|
x_new_indices = searchsorted(self._x_shift, x_new, side=self._side)
|
||
|
|
||
|
# 2. Clip x_new_indices so that they are within the range of x indices.
|
||
|
x_new_indices = x_new_indices.clip(1-self._ind,
|
||
|
len(self.x)-self._ind).astype(intp)
|
||
|
|
||
|
# 3. Calculate the actual value for each entry in x_new.
|
||
|
y_new = self._y[x_new_indices+self._ind-1]
|
||
|
|
||
|
return y_new
|
||
|
|
||
|
def _call_spline(self, x_new):
|
||
|
return self._spline(x_new)
|
||
|
|
||
|
def _call_nan_spline(self, x_new):
|
||
|
out = self._spline(x_new)
|
||
|
out[...] = np.nan
|
||
|
return out
|
||
|
|
||
|
def _evaluate(self, x_new):
|
||
|
# 1. Handle values in x_new that are outside of x. Throw error,
|
||
|
# or return a list of mask array indicating the outofbounds values.
|
||
|
# The behavior is set by the bounds_error variable.
|
||
|
x_new = asarray(x_new)
|
||
|
y_new = self._call(self, x_new)
|
||
|
if not self._extrapolate:
|
||
|
below_bounds, above_bounds = self._check_bounds(x_new)
|
||
|
if len(y_new) > 0:
|
||
|
# Note fill_value must be broadcast up to the proper size
|
||
|
# and flattened to work here
|
||
|
y_new[below_bounds] = self._fill_value_below
|
||
|
y_new[above_bounds] = self._fill_value_above
|
||
|
return y_new
|
||
|
|
||
|
def _check_bounds(self, x_new):
|
||
|
"""Check the inputs for being in the bounds of the interpolated data.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
x_new : array
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
out_of_bounds : bool array
|
||
|
The mask on x_new of values that are out of the bounds.
|
||
|
"""
|
||
|
|
||
|
# If self.bounds_error is True, we raise an error if any x_new values
|
||
|
# fall outside the range of x. Otherwise, we return an array indicating
|
||
|
# which values are outside the boundary region.
|
||
|
below_bounds = x_new < self.x[0]
|
||
|
above_bounds = x_new > self.x[-1]
|
||
|
|
||
|
if self.bounds_error and below_bounds.any():
|
||
|
below_bounds_value = x_new[np.argmax(below_bounds)]
|
||
|
raise ValueError(f"A value ({below_bounds_value}) in x_new is below "
|
||
|
f"the interpolation range's minimum value ({self.x[0]}).")
|
||
|
if self.bounds_error and above_bounds.any():
|
||
|
above_bounds_value = x_new[np.argmax(above_bounds)]
|
||
|
raise ValueError(f"A value ({above_bounds_value}) in x_new is above "
|
||
|
f"the interpolation range's maximum value ({self.x[-1]}).")
|
||
|
|
||
|
# !! Should we emit a warning if some values are out of bounds?
|
||
|
# !! matlab does not.
|
||
|
return below_bounds, above_bounds
|
||
|
|
||
|
|
||
|
class _PPolyBase:
|
||
|
"""Base class for piecewise polynomials."""
|
||
|
__slots__ = ('c', 'x', 'extrapolate', 'axis')
|
||
|
|
||
|
def __init__(self, c, x, extrapolate=None, axis=0):
|
||
|
self.c = np.asarray(c)
|
||
|
self.x = np.ascontiguousarray(x, dtype=np.float64)
|
||
|
|
||
|
if extrapolate is None:
|
||
|
extrapolate = True
|
||
|
elif extrapolate != 'periodic':
|
||
|
extrapolate = bool(extrapolate)
|
||
|
self.extrapolate = extrapolate
|
||
|
|
||
|
if self.c.ndim < 2:
|
||
|
raise ValueError("Coefficients array must be at least "
|
||
|
"2-dimensional.")
|
||
|
|
||
|
if not (0 <= axis < self.c.ndim - 1):
|
||
|
raise ValueError(f"axis={axis} must be between 0 and {self.c.ndim-1}")
|
||
|
|
||
|
self.axis = axis
|
||
|
if axis != 0:
|
||
|
# move the interpolation axis to be the first one in self.c
|
||
|
# More specifically, the target shape for self.c is (k, m, ...),
|
||
|
# and axis !=0 means that we have c.shape (..., k, m, ...)
|
||
|
# ^
|
||
|
# axis
|
||
|
# So we roll two of them.
|
||
|
self.c = np.moveaxis(self.c, axis+1, 0)
|
||
|
self.c = np.moveaxis(self.c, axis+1, 0)
|
||
|
|
||
|
if self.x.ndim != 1:
|
||
|
raise ValueError("x must be 1-dimensional")
|
||
|
if self.x.size < 2:
|
||
|
raise ValueError("at least 2 breakpoints are needed")
|
||
|
if self.c.ndim < 2:
|
||
|
raise ValueError("c must have at least 2 dimensions")
|
||
|
if self.c.shape[0] == 0:
|
||
|
raise ValueError("polynomial must be at least of order 0")
|
||
|
if self.c.shape[1] != self.x.size-1:
|
||
|
raise ValueError("number of coefficients != len(x)-1")
|
||
|
dx = np.diff(self.x)
|
||
|
if not (np.all(dx >= 0) or np.all(dx <= 0)):
|
||
|
raise ValueError("`x` must be strictly increasing or decreasing.")
|
||
|
|
||
|
dtype = self._get_dtype(self.c.dtype)
|
||
|
self.c = np.ascontiguousarray(self.c, dtype=dtype)
|
||
|
|
||
|
def _get_dtype(self, dtype):
|
||
|
if np.issubdtype(dtype, np.complexfloating) \
|
||
|
or np.issubdtype(self.c.dtype, np.complexfloating):
|
||
|
return np.complex128
|
||
|
else:
|
||
|
return np.float64
|
||
|
|
||
|
@classmethod
|
||
|
def construct_fast(cls, c, x, extrapolate=None, axis=0):
|
||
|
"""
|
||
|
Construct the piecewise polynomial without making checks.
|
||
|
|
||
|
Takes the same parameters as the constructor. Input arguments
|
||
|
``c`` and ``x`` must be arrays of the correct shape and type. The
|
||
|
``c`` array can only be of dtypes float and complex, and ``x``
|
||
|
array must have dtype float.
|
||
|
"""
|
||
|
self = object.__new__(cls)
|
||
|
self.c = c
|
||
|
self.x = x
|
||
|
self.axis = axis
|
||
|
if extrapolate is None:
|
||
|
extrapolate = True
|
||
|
self.extrapolate = extrapolate
|
||
|
return self
|
||
|
|
||
|
def _ensure_c_contiguous(self):
|
||
|
"""
|
||
|
c and x may be modified by the user. The Cython code expects
|
||
|
that they are C contiguous.
|
||
|
"""
|
||
|
if not self.x.flags.c_contiguous:
|
||
|
self.x = self.x.copy()
|
||
|
if not self.c.flags.c_contiguous:
|
||
|
self.c = self.c.copy()
|
||
|
|
||
|
def extend(self, c, x):
|
||
|
"""
|
||
|
Add additional breakpoints and coefficients to the polynomial.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
c : ndarray, size (k, m, ...)
|
||
|
Additional coefficients for polynomials in intervals. Note that
|
||
|
the first additional interval will be formed using one of the
|
||
|
``self.x`` end points.
|
||
|
x : ndarray, size (m,)
|
||
|
Additional breakpoints. Must be sorted in the same order as
|
||
|
``self.x`` and either to the right or to the left of the current
|
||
|
breakpoints.
|
||
|
"""
|
||
|
|
||
|
c = np.asarray(c)
|
||
|
x = np.asarray(x)
|
||
|
|
||
|
if c.ndim < 2:
|
||
|
raise ValueError("invalid dimensions for c")
|
||
|
if x.ndim != 1:
|
||
|
raise ValueError("invalid dimensions for x")
|
||
|
if x.shape[0] != c.shape[1]:
|
||
|
raise ValueError(f"Shapes of x {x.shape} and c {c.shape} are incompatible")
|
||
|
if c.shape[2:] != self.c.shape[2:] or c.ndim != self.c.ndim:
|
||
|
raise ValueError(
|
||
|
f"Shapes of c {c.shape} and self.c {self.c.shape} are incompatible"
|
||
|
)
|
||
|
|
||
|
if c.size == 0:
|
||
|
return
|
||
|
|
||
|
dx = np.diff(x)
|
||
|
if not (np.all(dx >= 0) or np.all(dx <= 0)):
|
||
|
raise ValueError("`x` is not sorted.")
|
||
|
|
||
|
if self.x[-1] >= self.x[0]:
|
||
|
if not x[-1] >= x[0]:
|
||
|
raise ValueError("`x` is in the different order "
|
||
|
"than `self.x`.")
|
||
|
|
||
|
if x[0] >= self.x[-1]:
|
||
|
action = 'append'
|
||
|
elif x[-1] <= self.x[0]:
|
||
|
action = 'prepend'
|
||
|
else:
|
||
|
raise ValueError("`x` is neither on the left or on the right "
|
||
|
"from `self.x`.")
|
||
|
else:
|
||
|
if not x[-1] <= x[0]:
|
||
|
raise ValueError("`x` is in the different order "
|
||
|
"than `self.x`.")
|
||
|
|
||
|
if x[0] <= self.x[-1]:
|
||
|
action = 'append'
|
||
|
elif x[-1] >= self.x[0]:
|
||
|
action = 'prepend'
|
||
|
else:
|
||
|
raise ValueError("`x` is neither on the left or on the right "
|
||
|
"from `self.x`.")
|
||
|
|
||
|
dtype = self._get_dtype(c.dtype)
|
||
|
|
||
|
k2 = max(c.shape[0], self.c.shape[0])
|
||
|
c2 = np.zeros((k2, self.c.shape[1] + c.shape[1]) + self.c.shape[2:],
|
||
|
dtype=dtype)
|
||
|
|
||
|
if action == 'append':
|
||
|
c2[k2-self.c.shape[0]:, :self.c.shape[1]] = self.c
|
||
|
c2[k2-c.shape[0]:, self.c.shape[1]:] = c
|
||
|
self.x = np.r_[self.x, x]
|
||
|
elif action == 'prepend':
|
||
|
c2[k2-self.c.shape[0]:, :c.shape[1]] = c
|
||
|
c2[k2-c.shape[0]:, c.shape[1]:] = self.c
|
||
|
self.x = np.r_[x, self.x]
|
||
|
|
||
|
self.c = c2
|
||
|
|
||
|
def __call__(self, x, nu=0, extrapolate=None):
|
||
|
"""
|
||
|
Evaluate the piecewise polynomial or its derivative.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
x : array_like
|
||
|
Points to evaluate the interpolant at.
|
||
|
nu : int, optional
|
||
|
Order of derivative to evaluate. Must be non-negative.
|
||
|
extrapolate : {bool, 'periodic', None}, optional
|
||
|
If bool, determines whether to extrapolate to out-of-bounds points
|
||
|
based on first and last intervals, or to return NaNs.
|
||
|
If 'periodic', periodic extrapolation is used.
|
||
|
If None (default), use `self.extrapolate`.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
y : array_like
|
||
|
Interpolated values. Shape is determined by replacing
|
||
|
the interpolation axis in the original array with the shape of x.
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
Derivatives are evaluated piecewise for each polynomial
|
||
|
segment, even if the polynomial is not differentiable at the
|
||
|
breakpoints. The polynomial intervals are considered half-open,
|
||
|
``[a, b)``, except for the last interval which is closed
|
||
|
``[a, b]``.
|
||
|
"""
|
||
|
if extrapolate is None:
|
||
|
extrapolate = self.extrapolate
|
||
|
x = np.asarray(x)
|
||
|
x_shape, x_ndim = x.shape, x.ndim
|
||
|
x = np.ascontiguousarray(x.ravel(), dtype=np.float64)
|
||
|
|
||
|
# With periodic extrapolation we map x to the segment
|
||
|
# [self.x[0], self.x[-1]].
|
||
|
if extrapolate == 'periodic':
|
||
|
x = self.x[0] + (x - self.x[0]) % (self.x[-1] - self.x[0])
|
||
|
extrapolate = False
|
||
|
|
||
|
out = np.empty((len(x), prod(self.c.shape[2:])), dtype=self.c.dtype)
|
||
|
self._ensure_c_contiguous()
|
||
|
self._evaluate(x, nu, extrapolate, out)
|
||
|
out = out.reshape(x_shape + self.c.shape[2:])
|
||
|
if self.axis != 0:
|
||
|
# transpose to move the calculated values to the interpolation axis
|
||
|
l = list(range(out.ndim))
|
||
|
l = l[x_ndim:x_ndim+self.axis] + l[:x_ndim] + l[x_ndim+self.axis:]
|
||
|
out = out.transpose(l)
|
||
|
return out
|
||
|
|
||
|
|
||
|
class PPoly(_PPolyBase):
|
||
|
"""
|
||
|
Piecewise polynomial in terms of coefficients and breakpoints
|
||
|
|
||
|
The polynomial between ``x[i]`` and ``x[i + 1]`` is written in the
|
||
|
local power basis::
|
||
|
|
||
|
S = sum(c[m, i] * (xp - x[i])**(k-m) for m in range(k+1))
|
||
|
|
||
|
where ``k`` is the degree of the polynomial.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
c : ndarray, shape (k, m, ...)
|
||
|
Polynomial coefficients, order `k` and `m` intervals.
|
||
|
x : ndarray, shape (m+1,)
|
||
|
Polynomial breakpoints. Must be sorted in either increasing or
|
||
|
decreasing order.
|
||
|
extrapolate : bool or 'periodic', optional
|
||
|
If bool, determines whether to extrapolate to out-of-bounds points
|
||
|
based on first and last intervals, or to return NaNs. If 'periodic',
|
||
|
periodic extrapolation is used. Default is True.
|
||
|
axis : int, optional
|
||
|
Interpolation axis. Default is zero.
|
||
|
|
||
|
Attributes
|
||
|
----------
|
||
|
x : ndarray
|
||
|
Breakpoints.
|
||
|
c : ndarray
|
||
|
Coefficients of the polynomials. They are reshaped
|
||
|
to a 3-D array with the last dimension representing
|
||
|
the trailing dimensions of the original coefficient array.
|
||
|
axis : int
|
||
|
Interpolation axis.
|
||
|
|
||
|
Methods
|
||
|
-------
|
||
|
__call__
|
||
|
derivative
|
||
|
antiderivative
|
||
|
integrate
|
||
|
solve
|
||
|
roots
|
||
|
extend
|
||
|
from_spline
|
||
|
from_bernstein_basis
|
||
|
construct_fast
|
||
|
|
||
|
See also
|
||
|
--------
|
||
|
BPoly : piecewise polynomials in the Bernstein basis
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
High-order polynomials in the power basis can be numerically
|
||
|
unstable. Precision problems can start to appear for orders
|
||
|
larger than 20-30.
|
||
|
"""
|
||
|
|
||
|
def _evaluate(self, x, nu, extrapolate, out):
|
||
|
_ppoly.evaluate(self.c.reshape(self.c.shape[0], self.c.shape[1], -1),
|
||
|
self.x, x, nu, bool(extrapolate), out)
|
||
|
|
||
|
def derivative(self, nu=1):
|
||
|
"""
|
||
|
Construct a new piecewise polynomial representing the derivative.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
nu : int, optional
|
||
|
Order of derivative to evaluate. Default is 1, i.e., compute the
|
||
|
first derivative. If negative, the antiderivative is returned.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
pp : PPoly
|
||
|
Piecewise polynomial of order k2 = k - n representing the derivative
|
||
|
of this polynomial.
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
Derivatives are evaluated piecewise for each polynomial
|
||
|
segment, even if the polynomial is not differentiable at the
|
||
|
breakpoints. The polynomial intervals are considered half-open,
|
||
|
``[a, b)``, except for the last interval which is closed
|
||
|
``[a, b]``.
|
||
|
"""
|
||
|
if nu < 0:
|
||
|
return self.antiderivative(-nu)
|
||
|
|
||
|
# reduce order
|
||
|
if nu == 0:
|
||
|
c2 = self.c.copy()
|
||
|
else:
|
||
|
c2 = self.c[:-nu, :].copy()
|
||
|
|
||
|
if c2.shape[0] == 0:
|
||
|
# derivative of order 0 is zero
|
||
|
c2 = np.zeros((1,) + c2.shape[1:], dtype=c2.dtype)
|
||
|
|
||
|
# multiply by the correct rising factorials
|
||
|
factor = spec.poch(np.arange(c2.shape[0], 0, -1), nu)
|
||
|
c2 *= factor[(slice(None),) + (None,)*(c2.ndim-1)]
|
||
|
|
||
|
# construct a compatible polynomial
|
||
|
return self.construct_fast(c2, self.x, self.extrapolate, self.axis)
|
||
|
|
||
|
def antiderivative(self, nu=1):
|
||
|
"""
|
||
|
Construct a new piecewise polynomial representing the antiderivative.
|
||
|
|
||
|
Antiderivative is also the indefinite integral of the function,
|
||
|
and derivative is its inverse operation.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
nu : int, optional
|
||
|
Order of antiderivative to evaluate. Default is 1, i.e., compute
|
||
|
the first integral. If negative, the derivative is returned.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
pp : PPoly
|
||
|
Piecewise polynomial of order k2 = k + n representing
|
||
|
the antiderivative of this polynomial.
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
The antiderivative returned by this function is continuous and
|
||
|
continuously differentiable to order n-1, up to floating point
|
||
|
rounding error.
|
||
|
|
||
|
If antiderivative is computed and ``self.extrapolate='periodic'``,
|
||
|
it will be set to False for the returned instance. This is done because
|
||
|
the antiderivative is no longer periodic and its correct evaluation
|
||
|
outside of the initially given x interval is difficult.
|
||
|
"""
|
||
|
if nu <= 0:
|
||
|
return self.derivative(-nu)
|
||
|
|
||
|
c = np.zeros((self.c.shape[0] + nu, self.c.shape[1]) + self.c.shape[2:],
|
||
|
dtype=self.c.dtype)
|
||
|
c[:-nu] = self.c
|
||
|
|
||
|
# divide by the correct rising factorials
|
||
|
factor = spec.poch(np.arange(self.c.shape[0], 0, -1), nu)
|
||
|
c[:-nu] /= factor[(slice(None),) + (None,)*(c.ndim-1)]
|
||
|
|
||
|
# fix continuity of added degrees of freedom
|
||
|
self._ensure_c_contiguous()
|
||
|
_ppoly.fix_continuity(c.reshape(c.shape[0], c.shape[1], -1),
|
||
|
self.x, nu - 1)
|
||
|
|
||
|
if self.extrapolate == 'periodic':
|
||
|
extrapolate = False
|
||
|
else:
|
||
|
extrapolate = self.extrapolate
|
||
|
|
||
|
# construct a compatible polynomial
|
||
|
return self.construct_fast(c, self.x, extrapolate, self.axis)
|
||
|
|
||
|
def integrate(self, a, b, extrapolate=None):
|
||
|
"""
|
||
|
Compute a definite integral over a piecewise polynomial.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
a : float
|
||
|
Lower integration bound
|
||
|
b : float
|
||
|
Upper integration bound
|
||
|
extrapolate : {bool, 'periodic', None}, optional
|
||
|
If bool, determines whether to extrapolate to out-of-bounds points
|
||
|
based on first and last intervals, or to return NaNs.
|
||
|
If 'periodic', periodic extrapolation is used.
|
||
|
If None (default), use `self.extrapolate`.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
ig : array_like
|
||
|
Definite integral of the piecewise polynomial over [a, b]
|
||
|
"""
|
||
|
if extrapolate is None:
|
||
|
extrapolate = self.extrapolate
|
||
|
|
||
|
# Swap integration bounds if needed
|
||
|
sign = 1
|
||
|
if b < a:
|
||
|
a, b = b, a
|
||
|
sign = -1
|
||
|
|
||
|
range_int = np.empty((prod(self.c.shape[2:]),), dtype=self.c.dtype)
|
||
|
self._ensure_c_contiguous()
|
||
|
|
||
|
# Compute the integral.
|
||
|
if extrapolate == 'periodic':
|
||
|
# Split the integral into the part over period (can be several
|
||
|
# of them) and the remaining part.
|
||
|
|
||
|
xs, xe = self.x[0], self.x[-1]
|
||
|
period = xe - xs
|
||
|
interval = b - a
|
||
|
n_periods, left = divmod(interval, period)
|
||
|
|
||
|
if n_periods > 0:
|
||
|
_ppoly.integrate(
|
||
|
self.c.reshape(self.c.shape[0], self.c.shape[1], -1),
|
||
|
self.x, xs, xe, False, out=range_int)
|
||
|
range_int *= n_periods
|
||
|
else:
|
||
|
range_int.fill(0)
|
||
|
|
||
|
# Map a to [xs, xe], b is always a + left.
|
||
|
a = xs + (a - xs) % period
|
||
|
b = a + left
|
||
|
|
||
|
# If b <= xe then we need to integrate over [a, b], otherwise
|
||
|
# over [a, xe] and from xs to what is remained.
|
||
|
remainder_int = np.empty_like(range_int)
|
||
|
if b <= xe:
|
||
|
_ppoly.integrate(
|
||
|
self.c.reshape(self.c.shape[0], self.c.shape[1], -1),
|
||
|
self.x, a, b, False, out=remainder_int)
|
||
|
range_int += remainder_int
|
||
|
else:
|
||
|
_ppoly.integrate(
|
||
|
self.c.reshape(self.c.shape[0], self.c.shape[1], -1),
|
||
|
self.x, a, xe, False, out=remainder_int)
|
||
|
range_int += remainder_int
|
||
|
|
||
|
_ppoly.integrate(
|
||
|
self.c.reshape(self.c.shape[0], self.c.shape[1], -1),
|
||
|
self.x, xs, xs + left + a - xe, False, out=remainder_int)
|
||
|
range_int += remainder_int
|
||
|
else:
|
||
|
_ppoly.integrate(
|
||
|
self.c.reshape(self.c.shape[0], self.c.shape[1], -1),
|
||
|
self.x, a, b, bool(extrapolate), out=range_int)
|
||
|
|
||
|
# Return
|
||
|
range_int *= sign
|
||
|
return range_int.reshape(self.c.shape[2:])
|
||
|
|
||
|
def solve(self, y=0., discontinuity=True, extrapolate=None):
|
||
|
"""
|
||
|
Find real solutions of the equation ``pp(x) == y``.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
y : float, optional
|
||
|
Right-hand side. Default is zero.
|
||
|
discontinuity : bool, optional
|
||
|
Whether to report sign changes across discontinuities at
|
||
|
breakpoints as roots.
|
||
|
extrapolate : {bool, 'periodic', None}, optional
|
||
|
If bool, determines whether to return roots from the polynomial
|
||
|
extrapolated based on first and last intervals, 'periodic' works
|
||
|
the same as False. If None (default), use `self.extrapolate`.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
roots : ndarray
|
||
|
Roots of the polynomial(s).
|
||
|
|
||
|
If the PPoly object describes multiple polynomials, the
|
||
|
return value is an object array whose each element is an
|
||
|
ndarray containing the roots.
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
This routine works only on real-valued polynomials.
|
||
|
|
||
|
If the piecewise polynomial contains sections that are
|
||
|
identically zero, the root list will contain the start point
|
||
|
of the corresponding interval, followed by a ``nan`` value.
|
||
|
|
||
|
If the polynomial is discontinuous across a breakpoint, and
|
||
|
there is a sign change across the breakpoint, this is reported
|
||
|
if the `discont` parameter is True.
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
|
||
|
Finding roots of ``[x**2 - 1, (x - 1)**2]`` defined on intervals
|
||
|
``[-2, 1], [1, 2]``:
|
||
|
|
||
|
>>> import numpy as np
|
||
|
>>> from scipy.interpolate import PPoly
|
||
|
>>> pp = PPoly(np.array([[1, -4, 3], [1, 0, 0]]).T, [-2, 1, 2])
|
||
|
>>> pp.solve()
|
||
|
array([-1., 1.])
|
||
|
"""
|
||
|
if extrapolate is None:
|
||
|
extrapolate = self.extrapolate
|
||
|
|
||
|
self._ensure_c_contiguous()
|
||
|
|
||
|
if np.issubdtype(self.c.dtype, np.complexfloating):
|
||
|
raise ValueError("Root finding is only for "
|
||
|
"real-valued polynomials")
|
||
|
|
||
|
y = float(y)
|
||
|
r = _ppoly.real_roots(self.c.reshape(self.c.shape[0], self.c.shape[1], -1),
|
||
|
self.x, y, bool(discontinuity),
|
||
|
bool(extrapolate))
|
||
|
if self.c.ndim == 2:
|
||
|
return r[0]
|
||
|
else:
|
||
|
r2 = np.empty(prod(self.c.shape[2:]), dtype=object)
|
||
|
# this for-loop is equivalent to ``r2[...] = r``, but that's broken
|
||
|
# in NumPy 1.6.0
|
||
|
for ii, root in enumerate(r):
|
||
|
r2[ii] = root
|
||
|
|
||
|
return r2.reshape(self.c.shape[2:])
|
||
|
|
||
|
def roots(self, discontinuity=True, extrapolate=None):
|
||
|
"""
|
||
|
Find real roots of the piecewise polynomial.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
discontinuity : bool, optional
|
||
|
Whether to report sign changes across discontinuities at
|
||
|
breakpoints as roots.
|
||
|
extrapolate : {bool, 'periodic', None}, optional
|
||
|
If bool, determines whether to return roots from the polynomial
|
||
|
extrapolated based on first and last intervals, 'periodic' works
|
||
|
the same as False. If None (default), use `self.extrapolate`.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
roots : ndarray
|
||
|
Roots of the polynomial(s).
|
||
|
|
||
|
If the PPoly object describes multiple polynomials, the
|
||
|
return value is an object array whose each element is an
|
||
|
ndarray containing the roots.
|
||
|
|
||
|
See Also
|
||
|
--------
|
||
|
PPoly.solve
|
||
|
"""
|
||
|
return self.solve(0, discontinuity, extrapolate)
|
||
|
|
||
|
@classmethod
|
||
|
def from_spline(cls, tck, extrapolate=None):
|
||
|
"""
|
||
|
Construct a piecewise polynomial from a spline
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
tck
|
||
|
A spline, as returned by `splrep` or a BSpline object.
|
||
|
extrapolate : bool or 'periodic', optional
|
||
|
If bool, determines whether to extrapolate to out-of-bounds points
|
||
|
based on first and last intervals, or to return NaNs.
|
||
|
If 'periodic', periodic extrapolation is used. Default is True.
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
Construct an interpolating spline and convert it to a `PPoly` instance
|
||
|
|
||
|
>>> import numpy as np
|
||
|
>>> from scipy.interpolate import splrep, PPoly
|
||
|
>>> x = np.linspace(0, 1, 11)
|
||
|
>>> y = np.sin(2*np.pi*x)
|
||
|
>>> tck = splrep(x, y, s=0)
|
||
|
>>> p = PPoly.from_spline(tck)
|
||
|
>>> isinstance(p, PPoly)
|
||
|
True
|
||
|
|
||
|
Note that this function only supports 1D splines out of the box.
|
||
|
|
||
|
If the ``tck`` object represents a parametric spline (e.g. constructed
|
||
|
by `splprep` or a `BSpline` with ``c.ndim > 1``), you will need to loop
|
||
|
over the dimensions manually.
|
||
|
|
||
|
>>> from scipy.interpolate import splprep, splev
|
||
|
>>> t = np.linspace(0, 1, 11)
|
||
|
>>> x = np.sin(2*np.pi*t)
|
||
|
>>> y = np.cos(2*np.pi*t)
|
||
|
>>> (t, c, k), u = splprep([x, y], s=0)
|
||
|
|
||
|
Note that ``c`` is a list of two arrays of length 11.
|
||
|
|
||
|
>>> unew = np.arange(0, 1.01, 0.01)
|
||
|
>>> out = splev(unew, (t, c, k))
|
||
|
|
||
|
To convert this spline to the power basis, we convert each
|
||
|
component of the list of b-spline coefficients, ``c``, into the
|
||
|
corresponding cubic polynomial.
|
||
|
|
||
|
>>> polys = [PPoly.from_spline((t, cj, k)) for cj in c]
|
||
|
>>> polys[0].c.shape
|
||
|
(4, 14)
|
||
|
|
||
|
Note that the coefficients of the polynomials `polys` are in the
|
||
|
power basis and their dimensions reflect just that: here 4 is the order
|
||
|
(degree+1), and 14 is the number of intervals---which is nothing but
|
||
|
the length of the knot array of the original `tck` minus one.
|
||
|
|
||
|
Optionally, we can stack the components into a single `PPoly` along
|
||
|
the third dimension:
|
||
|
|
||
|
>>> cc = np.dstack([p.c for p in polys]) # has shape = (4, 14, 2)
|
||
|
>>> poly = PPoly(cc, polys[0].x)
|
||
|
>>> np.allclose(poly(unew).T, # note the transpose to match `splev`
|
||
|
... out, atol=1e-15)
|
||
|
True
|
||
|
|
||
|
"""
|
||
|
if isinstance(tck, BSpline):
|
||
|
t, c, k = tck.tck
|
||
|
if extrapolate is None:
|
||
|
extrapolate = tck.extrapolate
|
||
|
else:
|
||
|
t, c, k = tck
|
||
|
|
||
|
cvals = np.empty((k + 1, len(t)-1), dtype=c.dtype)
|
||
|
for m in range(k, -1, -1):
|
||
|
y = _fitpack_py.splev(t[:-1], tck, der=m)
|
||
|
cvals[k - m, :] = y/spec.gamma(m+1)
|
||
|
|
||
|
return cls.construct_fast(cvals, t, extrapolate)
|
||
|
|
||
|
@classmethod
|
||
|
def from_bernstein_basis(cls, bp, extrapolate=None):
|
||
|
"""
|
||
|
Construct a piecewise polynomial in the power basis
|
||
|
from a polynomial in Bernstein basis.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
bp : BPoly
|
||
|
A Bernstein basis polynomial, as created by BPoly
|
||
|
extrapolate : bool or 'periodic', optional
|
||
|
If bool, determines whether to extrapolate to out-of-bounds points
|
||
|
based on first and last intervals, or to return NaNs.
|
||
|
If 'periodic', periodic extrapolation is used. Default is True.
|
||
|
"""
|
||
|
if not isinstance(bp, BPoly):
|
||
|
raise TypeError(".from_bernstein_basis only accepts BPoly instances. "
|
||
|
"Got %s instead." % type(bp))
|
||
|
|
||
|
dx = np.diff(bp.x)
|
||
|
k = bp.c.shape[0] - 1 # polynomial order
|
||
|
|
||
|
rest = (None,)*(bp.c.ndim-2)
|
||
|
|
||
|
c = np.zeros_like(bp.c)
|
||
|
for a in range(k+1):
|
||
|
factor = (-1)**a * comb(k, a) * bp.c[a]
|
||
|
for s in range(a, k+1):
|
||
|
val = comb(k-a, s-a) * (-1)**s
|
||
|
c[k-s] += factor * val / dx[(slice(None),)+rest]**s
|
||
|
|
||
|
if extrapolate is None:
|
||
|
extrapolate = bp.extrapolate
|
||
|
|
||
|
return cls.construct_fast(c, bp.x, extrapolate, bp.axis)
|
||
|
|
||
|
|
||
|
class BPoly(_PPolyBase):
|
||
|
"""Piecewise polynomial in terms of coefficients and breakpoints.
|
||
|
|
||
|
The polynomial between ``x[i]`` and ``x[i + 1]`` is written in the
|
||
|
Bernstein polynomial basis::
|
||
|
|
||
|
S = sum(c[a, i] * b(a, k; x) for a in range(k+1)),
|
||
|
|
||
|
where ``k`` is the degree of the polynomial, and::
|
||
|
|
||
|
b(a, k; x) = binom(k, a) * t**a * (1 - t)**(k - a),
|
||
|
|
||
|
with ``t = (x - x[i]) / (x[i+1] - x[i])`` and ``binom`` is the binomial
|
||
|
coefficient.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
c : ndarray, shape (k, m, ...)
|
||
|
Polynomial coefficients, order `k` and `m` intervals
|
||
|
x : ndarray, shape (m+1,)
|
||
|
Polynomial breakpoints. Must be sorted in either increasing or
|
||
|
decreasing order.
|
||
|
extrapolate : bool, optional
|
||
|
If bool, determines whether to extrapolate to out-of-bounds points
|
||
|
based on first and last intervals, or to return NaNs. If 'periodic',
|
||
|
periodic extrapolation is used. Default is True.
|
||
|
axis : int, optional
|
||
|
Interpolation axis. Default is zero.
|
||
|
|
||
|
Attributes
|
||
|
----------
|
||
|
x : ndarray
|
||
|
Breakpoints.
|
||
|
c : ndarray
|
||
|
Coefficients of the polynomials. They are reshaped
|
||
|
to a 3-D array with the last dimension representing
|
||
|
the trailing dimensions of the original coefficient array.
|
||
|
axis : int
|
||
|
Interpolation axis.
|
||
|
|
||
|
Methods
|
||
|
-------
|
||
|
__call__
|
||
|
extend
|
||
|
derivative
|
||
|
antiderivative
|
||
|
integrate
|
||
|
construct_fast
|
||
|
from_power_basis
|
||
|
from_derivatives
|
||
|
|
||
|
See also
|
||
|
--------
|
||
|
PPoly : piecewise polynomials in the power basis
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
Properties of Bernstein polynomials are well documented in the literature,
|
||
|
see for example [1]_ [2]_ [3]_.
|
||
|
|
||
|
References
|
||
|
----------
|
||
|
.. [1] https://en.wikipedia.org/wiki/Bernstein_polynomial
|
||
|
|
||
|
.. [2] Kenneth I. Joy, Bernstein polynomials,
|
||
|
http://www.idav.ucdavis.edu/education/CAGDNotes/Bernstein-Polynomials.pdf
|
||
|
|
||
|
.. [3] E. H. Doha, A. H. Bhrawy, and M. A. Saker, Boundary Value Problems,
|
||
|
vol 2011, article ID 829546, :doi:`10.1155/2011/829543`.
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
>>> from scipy.interpolate import BPoly
|
||
|
>>> x = [0, 1]
|
||
|
>>> c = [[1], [2], [3]]
|
||
|
>>> bp = BPoly(c, x)
|
||
|
|
||
|
This creates a 2nd order polynomial
|
||
|
|
||
|
.. math::
|
||
|
|
||
|
B(x) = 1 \\times b_{0, 2}(x) + 2 \\times b_{1, 2}(x) + 3
|
||
|
\\times b_{2, 2}(x) \\\\
|
||
|
= 1 \\times (1-x)^2 + 2 \\times 2 x (1 - x) + 3 \\times x^2
|
||
|
|
||
|
""" # noqa: E501
|
||
|
|
||
|
def _evaluate(self, x, nu, extrapolate, out):
|
||
|
_ppoly.evaluate_bernstein(
|
||
|
self.c.reshape(self.c.shape[0], self.c.shape[1], -1),
|
||
|
self.x, x, nu, bool(extrapolate), out)
|
||
|
|
||
|
def derivative(self, nu=1):
|
||
|
"""
|
||
|
Construct a new piecewise polynomial representing the derivative.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
nu : int, optional
|
||
|
Order of derivative to evaluate. Default is 1, i.e., compute the
|
||
|
first derivative. If negative, the antiderivative is returned.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
bp : BPoly
|
||
|
Piecewise polynomial of order k - nu representing the derivative of
|
||
|
this polynomial.
|
||
|
|
||
|
"""
|
||
|
if nu < 0:
|
||
|
return self.antiderivative(-nu)
|
||
|
|
||
|
if nu > 1:
|
||
|
bp = self
|
||
|
for k in range(nu):
|
||
|
bp = bp.derivative()
|
||
|
return bp
|
||
|
|
||
|
# reduce order
|
||
|
if nu == 0:
|
||
|
c2 = self.c.copy()
|
||
|
else:
|
||
|
# For a polynomial
|
||
|
# B(x) = \sum_{a=0}^{k} c_a b_{a, k}(x),
|
||
|
# we use the fact that
|
||
|
# b'_{a, k} = k ( b_{a-1, k-1} - b_{a, k-1} ),
|
||
|
# which leads to
|
||
|
# B'(x) = \sum_{a=0}^{k-1} (c_{a+1} - c_a) b_{a, k-1}
|
||
|
#
|
||
|
# finally, for an interval [y, y + dy] with dy != 1,
|
||
|
# we need to correct for an extra power of dy
|
||
|
|
||
|
rest = (None,)*(self.c.ndim-2)
|
||
|
|
||
|
k = self.c.shape[0] - 1
|
||
|
dx = np.diff(self.x)[(None, slice(None))+rest]
|
||
|
c2 = k * np.diff(self.c, axis=0) / dx
|
||
|
|
||
|
if c2.shape[0] == 0:
|
||
|
# derivative of order 0 is zero
|
||
|
c2 = np.zeros((1,) + c2.shape[1:], dtype=c2.dtype)
|
||
|
|
||
|
# construct a compatible polynomial
|
||
|
return self.construct_fast(c2, self.x, self.extrapolate, self.axis)
|
||
|
|
||
|
def antiderivative(self, nu=1):
|
||
|
"""
|
||
|
Construct a new piecewise polynomial representing the antiderivative.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
nu : int, optional
|
||
|
Order of antiderivative to evaluate. Default is 1, i.e., compute
|
||
|
the first integral. If negative, the derivative is returned.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
bp : BPoly
|
||
|
Piecewise polynomial of order k + nu representing the
|
||
|
antiderivative of this polynomial.
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
If antiderivative is computed and ``self.extrapolate='periodic'``,
|
||
|
it will be set to False for the returned instance. This is done because
|
||
|
the antiderivative is no longer periodic and its correct evaluation
|
||
|
outside of the initially given x interval is difficult.
|
||
|
"""
|
||
|
if nu <= 0:
|
||
|
return self.derivative(-nu)
|
||
|
|
||
|
if nu > 1:
|
||
|
bp = self
|
||
|
for k in range(nu):
|
||
|
bp = bp.antiderivative()
|
||
|
return bp
|
||
|
|
||
|
# Construct the indefinite integrals on individual intervals
|
||
|
c, x = self.c, self.x
|
||
|
k = c.shape[0]
|
||
|
c2 = np.zeros((k+1,) + c.shape[1:], dtype=c.dtype)
|
||
|
|
||
|
c2[1:, ...] = np.cumsum(c, axis=0) / k
|
||
|
delta = x[1:] - x[:-1]
|
||
|
c2 *= delta[(None, slice(None)) + (None,)*(c.ndim-2)]
|
||
|
|
||
|
# Now fix continuity: on the very first interval, take the integration
|
||
|
# constant to be zero; on an interval [x_j, x_{j+1}) with j>0,
|
||
|
# the integration constant is then equal to the jump of the `bp` at x_j.
|
||
|
# The latter is given by the coefficient of B_{n+1, n+1}
|
||
|
# *on the previous interval* (other B. polynomials are zero at the
|
||
|
# breakpoint). Finally, use the fact that BPs form a partition of unity.
|
||
|
c2[:,1:] += np.cumsum(c2[k, :], axis=0)[:-1]
|
||
|
|
||
|
if self.extrapolate == 'periodic':
|
||
|
extrapolate = False
|
||
|
else:
|
||
|
extrapolate = self.extrapolate
|
||
|
|
||
|
return self.construct_fast(c2, x, extrapolate, axis=self.axis)
|
||
|
|
||
|
def integrate(self, a, b, extrapolate=None):
|
||
|
"""
|
||
|
Compute a definite integral over a piecewise polynomial.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
a : float
|
||
|
Lower integration bound
|
||
|
b : float
|
||
|
Upper integration bound
|
||
|
extrapolate : {bool, 'periodic', None}, optional
|
||
|
Whether to extrapolate to out-of-bounds points based on first
|
||
|
and last intervals, or to return NaNs. If 'periodic', periodic
|
||
|
extrapolation is used. If None (default), use `self.extrapolate`.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
array_like
|
||
|
Definite integral of the piecewise polynomial over [a, b]
|
||
|
|
||
|
"""
|
||
|
# XXX: can probably use instead the fact that
|
||
|
# \int_0^{1} B_{j, n}(x) \dx = 1/(n+1)
|
||
|
ib = self.antiderivative()
|
||
|
if extrapolate is None:
|
||
|
extrapolate = self.extrapolate
|
||
|
|
||
|
# ib.extrapolate shouldn't be 'periodic', it is converted to
|
||
|
# False for 'periodic. in antiderivative() call.
|
||
|
if extrapolate != 'periodic':
|
||
|
ib.extrapolate = extrapolate
|
||
|
|
||
|
if extrapolate == 'periodic':
|
||
|
# Split the integral into the part over period (can be several
|
||
|
# of them) and the remaining part.
|
||
|
|
||
|
# For simplicity and clarity convert to a <= b case.
|
||
|
if a <= b:
|
||
|
sign = 1
|
||
|
else:
|
||
|
a, b = b, a
|
||
|
sign = -1
|
||
|
|
||
|
xs, xe = self.x[0], self.x[-1]
|
||
|
period = xe - xs
|
||
|
interval = b - a
|
||
|
n_periods, left = divmod(interval, period)
|
||
|
res = n_periods * (ib(xe) - ib(xs))
|
||
|
|
||
|
# Map a and b to [xs, xe].
|
||
|
a = xs + (a - xs) % period
|
||
|
b = a + left
|
||
|
|
||
|
# If b <= xe then we need to integrate over [a, b], otherwise
|
||
|
# over [a, xe] and from xs to what is remained.
|
||
|
if b <= xe:
|
||
|
res += ib(b) - ib(a)
|
||
|
else:
|
||
|
res += ib(xe) - ib(a) + ib(xs + left + a - xe) - ib(xs)
|
||
|
|
||
|
return sign * res
|
||
|
else:
|
||
|
return ib(b) - ib(a)
|
||
|
|
||
|
def extend(self, c, x):
|
||
|
k = max(self.c.shape[0], c.shape[0])
|
||
|
self.c = self._raise_degree(self.c, k - self.c.shape[0])
|
||
|
c = self._raise_degree(c, k - c.shape[0])
|
||
|
return _PPolyBase.extend(self, c, x)
|
||
|
extend.__doc__ = _PPolyBase.extend.__doc__
|
||
|
|
||
|
@classmethod
|
||
|
def from_power_basis(cls, pp, extrapolate=None):
|
||
|
"""
|
||
|
Construct a piecewise polynomial in Bernstein basis
|
||
|
from a power basis polynomial.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
pp : PPoly
|
||
|
A piecewise polynomial in the power basis
|
||
|
extrapolate : bool or 'periodic', optional
|
||
|
If bool, determines whether to extrapolate to out-of-bounds points
|
||
|
based on first and last intervals, or to return NaNs.
|
||
|
If 'periodic', periodic extrapolation is used. Default is True.
|
||
|
"""
|
||
|
if not isinstance(pp, PPoly):
|
||
|
raise TypeError(".from_power_basis only accepts PPoly instances. "
|
||
|
"Got %s instead." % type(pp))
|
||
|
|
||
|
dx = np.diff(pp.x)
|
||
|
k = pp.c.shape[0] - 1 # polynomial order
|
||
|
|
||
|
rest = (None,)*(pp.c.ndim-2)
|
||
|
|
||
|
c = np.zeros_like(pp.c)
|
||
|
for a in range(k+1):
|
||
|
factor = pp.c[a] / comb(k, k-a) * dx[(slice(None),)+rest]**(k-a)
|
||
|
for j in range(k-a, k+1):
|
||
|
c[j] += factor * comb(j, k-a)
|
||
|
|
||
|
if extrapolate is None:
|
||
|
extrapolate = pp.extrapolate
|
||
|
|
||
|
return cls.construct_fast(c, pp.x, extrapolate, pp.axis)
|
||
|
|
||
|
@classmethod
|
||
|
def from_derivatives(cls, xi, yi, orders=None, extrapolate=None):
|
||
|
"""Construct a piecewise polynomial in the Bernstein basis,
|
||
|
compatible with the specified values and derivatives at breakpoints.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
xi : array_like
|
||
|
sorted 1-D array of x-coordinates
|
||
|
yi : array_like or list of array_likes
|
||
|
``yi[i][j]`` is the ``j``\\ th derivative known at ``xi[i]``
|
||
|
orders : None or int or array_like of ints. Default: None.
|
||
|
Specifies the degree of local polynomials. If not None, some
|
||
|
derivatives are ignored.
|
||
|
extrapolate : bool or 'periodic', optional
|
||
|
If bool, determines whether to extrapolate to out-of-bounds points
|
||
|
based on first and last intervals, or to return NaNs.
|
||
|
If 'periodic', periodic extrapolation is used. Default is True.
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
If ``k`` derivatives are specified at a breakpoint ``x``, the
|
||
|
constructed polynomial is exactly ``k`` times continuously
|
||
|
differentiable at ``x``, unless the ``order`` is provided explicitly.
|
||
|
In the latter case, the smoothness of the polynomial at
|
||
|
the breakpoint is controlled by the ``order``.
|
||
|
|
||
|
Deduces the number of derivatives to match at each end
|
||
|
from ``order`` and the number of derivatives available. If
|
||
|
possible it uses the same number of derivatives from
|
||
|
each end; if the number is odd it tries to take the
|
||
|
extra one from y2. In any case if not enough derivatives
|
||
|
are available at one end or another it draws enough to
|
||
|
make up the total from the other end.
|
||
|
|
||
|
If the order is too high and not enough derivatives are available,
|
||
|
an exception is raised.
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
|
||
|
>>> from scipy.interpolate import BPoly
|
||
|
>>> BPoly.from_derivatives([0, 1], [[1, 2], [3, 4]])
|
||
|
|
||
|
Creates a polynomial `f(x)` of degree 3, defined on `[0, 1]`
|
||
|
such that `f(0) = 1, df/dx(0) = 2, f(1) = 3, df/dx(1) = 4`
|
||
|
|
||
|
>>> BPoly.from_derivatives([0, 1, 2], [[0, 1], [0], [2]])
|
||
|
|
||
|
Creates a piecewise polynomial `f(x)`, such that
|
||
|
`f(0) = f(1) = 0`, `f(2) = 2`, and `df/dx(0) = 1`.
|
||
|
Based on the number of derivatives provided, the order of the
|
||
|
local polynomials is 2 on `[0, 1]` and 1 on `[1, 2]`.
|
||
|
Notice that no restriction is imposed on the derivatives at
|
||
|
``x = 1`` and ``x = 2``.
|
||
|
|
||
|
Indeed, the explicit form of the polynomial is::
|
||
|
|
||
|
f(x) = | x * (1 - x), 0 <= x < 1
|
||
|
| 2 * (x - 1), 1 <= x <= 2
|
||
|
|
||
|
So that f'(1-0) = -1 and f'(1+0) = 2
|
||
|
|
||
|
"""
|
||
|
xi = np.asarray(xi)
|
||
|
if len(xi) != len(yi):
|
||
|
raise ValueError("xi and yi need to have the same length")
|
||
|
if np.any(xi[1:] - xi[:1] <= 0):
|
||
|
raise ValueError("x coordinates are not in increasing order")
|
||
|
|
||
|
# number of intervals
|
||
|
m = len(xi) - 1
|
||
|
|
||
|
# global poly order is k-1, local orders are <=k and can vary
|
||
|
try:
|
||
|
k = max(len(yi[i]) + len(yi[i+1]) for i in range(m))
|
||
|
except TypeError as e:
|
||
|
raise ValueError(
|
||
|
"Using a 1-D array for y? Please .reshape(-1, 1)."
|
||
|
) from e
|
||
|
|
||
|
if orders is None:
|
||
|
orders = [None] * m
|
||
|
else:
|
||
|
if isinstance(orders, (int, np.integer)):
|
||
|
orders = [orders] * m
|
||
|
k = max(k, max(orders))
|
||
|
|
||
|
if any(o <= 0 for o in orders):
|
||
|
raise ValueError("Orders must be positive.")
|
||
|
|
||
|
c = []
|
||
|
for i in range(m):
|
||
|
y1, y2 = yi[i], yi[i+1]
|
||
|
if orders[i] is None:
|
||
|
n1, n2 = len(y1), len(y2)
|
||
|
else:
|
||
|
n = orders[i]+1
|
||
|
n1 = min(n//2, len(y1))
|
||
|
n2 = min(n - n1, len(y2))
|
||
|
n1 = min(n - n2, len(y2))
|
||
|
if n1+n2 != n:
|
||
|
mesg = ("Point %g has %d derivatives, point %g"
|
||
|
" has %d derivatives, but order %d requested" % (
|
||
|
xi[i], len(y1), xi[i+1], len(y2), orders[i]))
|
||
|
raise ValueError(mesg)
|
||
|
|
||
|
if not (n1 <= len(y1) and n2 <= len(y2)):
|
||
|
raise ValueError("`order` input incompatible with"
|
||
|
" length y1 or y2.")
|
||
|
|
||
|
b = BPoly._construct_from_derivatives(xi[i], xi[i+1],
|
||
|
y1[:n1], y2[:n2])
|
||
|
if len(b) < k:
|
||
|
b = BPoly._raise_degree(b, k - len(b))
|
||
|
c.append(b)
|
||
|
|
||
|
c = np.asarray(c)
|
||
|
return cls(c.swapaxes(0, 1), xi, extrapolate)
|
||
|
|
||
|
@staticmethod
|
||
|
def _construct_from_derivatives(xa, xb, ya, yb):
|
||
|
r"""Compute the coefficients of a polynomial in the Bernstein basis
|
||
|
given the values and derivatives at the edges.
|
||
|
|
||
|
Return the coefficients of a polynomial in the Bernstein basis
|
||
|
defined on ``[xa, xb]`` and having the values and derivatives at the
|
||
|
endpoints `xa` and `xb` as specified by `ya` and `yb`.
|
||
|
The polynomial constructed is of the minimal possible degree, i.e.,
|
||
|
if the lengths of `ya` and `yb` are `na` and `nb`, the degree
|
||
|
of the polynomial is ``na + nb - 1``.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
xa : float
|
||
|
Left-hand end point of the interval
|
||
|
xb : float
|
||
|
Right-hand end point of the interval
|
||
|
ya : array_like
|
||
|
Derivatives at `xa`. ``ya[0]`` is the value of the function, and
|
||
|
``ya[i]`` for ``i > 0`` is the value of the ``i``\ th derivative.
|
||
|
yb : array_like
|
||
|
Derivatives at `xb`.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
array
|
||
|
coefficient array of a polynomial having specified derivatives
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
This uses several facts from life of Bernstein basis functions.
|
||
|
First of all,
|
||
|
|
||
|
.. math:: b'_{a, n} = n (b_{a-1, n-1} - b_{a, n-1})
|
||
|
|
||
|
If B(x) is a linear combination of the form
|
||
|
|
||
|
.. math:: B(x) = \sum_{a=0}^{n} c_a b_{a, n},
|
||
|
|
||
|
then :math: B'(x) = n \sum_{a=0}^{n-1} (c_{a+1} - c_{a}) b_{a, n-1}.
|
||
|
Iterating the latter one, one finds for the q-th derivative
|
||
|
|
||
|
.. math:: B^{q}(x) = n!/(n-q)! \sum_{a=0}^{n-q} Q_a b_{a, n-q},
|
||
|
|
||
|
with
|
||
|
|
||
|
.. math:: Q_a = \sum_{j=0}^{q} (-)^{j+q} comb(q, j) c_{j+a}
|
||
|
|
||
|
This way, only `a=0` contributes to :math: `B^{q}(x = xa)`, and
|
||
|
`c_q` are found one by one by iterating `q = 0, ..., na`.
|
||
|
|
||
|
At ``x = xb`` it's the same with ``a = n - q``.
|
||
|
|
||
|
"""
|
||
|
ya, yb = np.asarray(ya), np.asarray(yb)
|
||
|
if ya.shape[1:] != yb.shape[1:]:
|
||
|
raise ValueError(
|
||
|
f"Shapes of ya {ya.shape} and yb {yb.shape} are incompatible"
|
||
|
)
|
||
|
|
||
|
dta, dtb = ya.dtype, yb.dtype
|
||
|
if (np.issubdtype(dta, np.complexfloating) or
|
||
|
np.issubdtype(dtb, np.complexfloating)):
|
||
|
dt = np.complex128
|
||
|
else:
|
||
|
dt = np.float64
|
||
|
|
||
|
na, nb = len(ya), len(yb)
|
||
|
n = na + nb
|
||
|
|
||
|
c = np.empty((na+nb,) + ya.shape[1:], dtype=dt)
|
||
|
|
||
|
# compute coefficients of a polynomial degree na+nb-1
|
||
|
# walk left-to-right
|
||
|
for q in range(0, na):
|
||
|
c[q] = ya[q] / spec.poch(n - q, q) * (xb - xa)**q
|
||
|
for j in range(0, q):
|
||
|
c[q] -= (-1)**(j+q) * comb(q, j) * c[j]
|
||
|
|
||
|
# now walk right-to-left
|
||
|
for q in range(0, nb):
|
||
|
c[-q-1] = yb[q] / spec.poch(n - q, q) * (-1)**q * (xb - xa)**q
|
||
|
for j in range(0, q):
|
||
|
c[-q-1] -= (-1)**(j+1) * comb(q, j+1) * c[-q+j]
|
||
|
|
||
|
return c
|
||
|
|
||
|
@staticmethod
|
||
|
def _raise_degree(c, d):
|
||
|
r"""Raise a degree of a polynomial in the Bernstein basis.
|
||
|
|
||
|
Given the coefficients of a polynomial degree `k`, return (the
|
||
|
coefficients of) the equivalent polynomial of degree `k+d`.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
c : array_like
|
||
|
coefficient array, 1-D
|
||
|
d : integer
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
array
|
||
|
coefficient array, 1-D array of length `c.shape[0] + d`
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
This uses the fact that a Bernstein polynomial `b_{a, k}` can be
|
||
|
identically represented as a linear combination of polynomials of
|
||
|
a higher degree `k+d`:
|
||
|
|
||
|
.. math:: b_{a, k} = comb(k, a) \sum_{j=0}^{d} b_{a+j, k+d} \
|
||
|
comb(d, j) / comb(k+d, a+j)
|
||
|
|
||
|
"""
|
||
|
if d == 0:
|
||
|
return c
|
||
|
|
||
|
k = c.shape[0] - 1
|
||
|
out = np.zeros((c.shape[0] + d,) + c.shape[1:], dtype=c.dtype)
|
||
|
|
||
|
for a in range(c.shape[0]):
|
||
|
f = c[a] * comb(k, a)
|
||
|
for j in range(d+1):
|
||
|
out[a+j] += f * comb(d, j) / comb(k+d, a+j)
|
||
|
return out
|
||
|
|
||
|
|
||
|
class NdPPoly:
|
||
|
"""
|
||
|
Piecewise tensor product polynomial
|
||
|
|
||
|
The value at point ``xp = (x', y', z', ...)`` is evaluated by first
|
||
|
computing the interval indices `i` such that::
|
||
|
|
||
|
x[0][i[0]] <= x' < x[0][i[0]+1]
|
||
|
x[1][i[1]] <= y' < x[1][i[1]+1]
|
||
|
...
|
||
|
|
||
|
and then computing::
|
||
|
|
||
|
S = sum(c[k0-m0-1,...,kn-mn-1,i[0],...,i[n]]
|
||
|
* (xp[0] - x[0][i[0]])**m0
|
||
|
* ...
|
||
|
* (xp[n] - x[n][i[n]])**mn
|
||
|
for m0 in range(k[0]+1)
|
||
|
...
|
||
|
for mn in range(k[n]+1))
|
||
|
|
||
|
where ``k[j]`` is the degree of the polynomial in dimension j. This
|
||
|
representation is the piecewise multivariate power basis.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
c : ndarray, shape (k0, ..., kn, m0, ..., mn, ...)
|
||
|
Polynomial coefficients, with polynomial order `kj` and
|
||
|
`mj+1` intervals for each dimension `j`.
|
||
|
x : ndim-tuple of ndarrays, shapes (mj+1,)
|
||
|
Polynomial breakpoints for each dimension. These must be
|
||
|
sorted in increasing order.
|
||
|
extrapolate : bool, optional
|
||
|
Whether to extrapolate to out-of-bounds points based on first
|
||
|
and last intervals, or to return NaNs. Default: True.
|
||
|
|
||
|
Attributes
|
||
|
----------
|
||
|
x : tuple of ndarrays
|
||
|
Breakpoints.
|
||
|
c : ndarray
|
||
|
Coefficients of the polynomials.
|
||
|
|
||
|
Methods
|
||
|
-------
|
||
|
__call__
|
||
|
derivative
|
||
|
antiderivative
|
||
|
integrate
|
||
|
integrate_1d
|
||
|
construct_fast
|
||
|
|
||
|
See also
|
||
|
--------
|
||
|
PPoly : piecewise polynomials in 1D
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
High-order polynomials in the power basis can be numerically
|
||
|
unstable.
|
||
|
|
||
|
"""
|
||
|
|
||
|
def __init__(self, c, x, extrapolate=None):
|
||
|
self.x = tuple(np.ascontiguousarray(v, dtype=np.float64) for v in x)
|
||
|
self.c = np.asarray(c)
|
||
|
if extrapolate is None:
|
||
|
extrapolate = True
|
||
|
self.extrapolate = bool(extrapolate)
|
||
|
|
||
|
ndim = len(self.x)
|
||
|
if any(v.ndim != 1 for v in self.x):
|
||
|
raise ValueError("x arrays must all be 1-dimensional")
|
||
|
if any(v.size < 2 for v in self.x):
|
||
|
raise ValueError("x arrays must all contain at least 2 points")
|
||
|
if c.ndim < 2*ndim:
|
||
|
raise ValueError("c must have at least 2*len(x) dimensions")
|
||
|
if any(np.any(v[1:] - v[:-1] < 0) for v in self.x):
|
||
|
raise ValueError("x-coordinates are not in increasing order")
|
||
|
if any(a != b.size - 1 for a, b in zip(c.shape[ndim:2*ndim], self.x)):
|
||
|
raise ValueError("x and c do not agree on the number of intervals")
|
||
|
|
||
|
dtype = self._get_dtype(self.c.dtype)
|
||
|
self.c = np.ascontiguousarray(self.c, dtype=dtype)
|
||
|
|
||
|
@classmethod
|
||
|
def construct_fast(cls, c, x, extrapolate=None):
|
||
|
"""
|
||
|
Construct the piecewise polynomial without making checks.
|
||
|
|
||
|
Takes the same parameters as the constructor. Input arguments
|
||
|
``c`` and ``x`` must be arrays of the correct shape and type. The
|
||
|
``c`` array can only be of dtypes float and complex, and ``x``
|
||
|
array must have dtype float.
|
||
|
|
||
|
"""
|
||
|
self = object.__new__(cls)
|
||
|
self.c = c
|
||
|
self.x = x
|
||
|
if extrapolate is None:
|
||
|
extrapolate = True
|
||
|
self.extrapolate = extrapolate
|
||
|
return self
|
||
|
|
||
|
def _get_dtype(self, dtype):
|
||
|
if np.issubdtype(dtype, np.complexfloating) \
|
||
|
or np.issubdtype(self.c.dtype, np.complexfloating):
|
||
|
return np.complex128
|
||
|
else:
|
||
|
return np.float64
|
||
|
|
||
|
def _ensure_c_contiguous(self):
|
||
|
if not self.c.flags.c_contiguous:
|
||
|
self.c = self.c.copy()
|
||
|
if not isinstance(self.x, tuple):
|
||
|
self.x = tuple(self.x)
|
||
|
|
||
|
def __call__(self, x, nu=None, extrapolate=None):
|
||
|
"""
|
||
|
Evaluate the piecewise polynomial or its derivative
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
x : array-like
|
||
|
Points to evaluate the interpolant at.
|
||
|
nu : tuple, optional
|
||
|
Orders of derivatives to evaluate. Each must be non-negative.
|
||
|
extrapolate : bool, optional
|
||
|
Whether to extrapolate to out-of-bounds points based on first
|
||
|
and last intervals, or to return NaNs.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
y : array-like
|
||
|
Interpolated values. Shape is determined by replacing
|
||
|
the interpolation axis in the original array with the shape of x.
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
Derivatives are evaluated piecewise for each polynomial
|
||
|
segment, even if the polynomial is not differentiable at the
|
||
|
breakpoints. The polynomial intervals are considered half-open,
|
||
|
``[a, b)``, except for the last interval which is closed
|
||
|
``[a, b]``.
|
||
|
|
||
|
"""
|
||
|
if extrapolate is None:
|
||
|
extrapolate = self.extrapolate
|
||
|
else:
|
||
|
extrapolate = bool(extrapolate)
|
||
|
|
||
|
ndim = len(self.x)
|
||
|
|
||
|
x = _ndim_coords_from_arrays(x)
|
||
|
x_shape = x.shape
|
||
|
x = np.ascontiguousarray(x.reshape(-1, x.shape[-1]), dtype=np.float64)
|
||
|
|
||
|
if nu is None:
|
||
|
nu = np.zeros((ndim,), dtype=np.intc)
|
||
|
else:
|
||
|
nu = np.asarray(nu, dtype=np.intc)
|
||
|
if nu.ndim != 1 or nu.shape[0] != ndim:
|
||
|
raise ValueError("invalid number of derivative orders nu")
|
||
|
|
||
|
dim1 = prod(self.c.shape[:ndim])
|
||
|
dim2 = prod(self.c.shape[ndim:2*ndim])
|
||
|
dim3 = prod(self.c.shape[2*ndim:])
|
||
|
ks = np.array(self.c.shape[:ndim], dtype=np.intc)
|
||
|
|
||
|
out = np.empty((x.shape[0], dim3), dtype=self.c.dtype)
|
||
|
self._ensure_c_contiguous()
|
||
|
|
||
|
_ppoly.evaluate_nd(self.c.reshape(dim1, dim2, dim3),
|
||
|
self.x,
|
||
|
ks,
|
||
|
x,
|
||
|
nu,
|
||
|
bool(extrapolate),
|
||
|
out)
|
||
|
|
||
|
return out.reshape(x_shape[:-1] + self.c.shape[2*ndim:])
|
||
|
|
||
|
def _derivative_inplace(self, nu, axis):
|
||
|
"""
|
||
|
Compute 1-D derivative along a selected dimension in-place
|
||
|
May result to non-contiguous c array.
|
||
|
"""
|
||
|
if nu < 0:
|
||
|
return self._antiderivative_inplace(-nu, axis)
|
||
|
|
||
|
ndim = len(self.x)
|
||
|
axis = axis % ndim
|
||
|
|
||
|
# reduce order
|
||
|
if nu == 0:
|
||
|
# noop
|
||
|
return
|
||
|
else:
|
||
|
sl = [slice(None)]*ndim
|
||
|
sl[axis] = slice(None, -nu, None)
|
||
|
c2 = self.c[tuple(sl)]
|
||
|
|
||
|
if c2.shape[axis] == 0:
|
||
|
# derivative of order 0 is zero
|
||
|
shp = list(c2.shape)
|
||
|
shp[axis] = 1
|
||
|
c2 = np.zeros(shp, dtype=c2.dtype)
|
||
|
|
||
|
# multiply by the correct rising factorials
|
||
|
factor = spec.poch(np.arange(c2.shape[axis], 0, -1), nu)
|
||
|
sl = [None]*c2.ndim
|
||
|
sl[axis] = slice(None)
|
||
|
c2 *= factor[tuple(sl)]
|
||
|
|
||
|
self.c = c2
|
||
|
|
||
|
def _antiderivative_inplace(self, nu, axis):
|
||
|
"""
|
||
|
Compute 1-D antiderivative along a selected dimension
|
||
|
May result to non-contiguous c array.
|
||
|
"""
|
||
|
if nu <= 0:
|
||
|
return self._derivative_inplace(-nu, axis)
|
||
|
|
||
|
ndim = len(self.x)
|
||
|
axis = axis % ndim
|
||
|
|
||
|
perm = list(range(ndim))
|
||
|
perm[0], perm[axis] = perm[axis], perm[0]
|
||
|
perm = perm + list(range(ndim, self.c.ndim))
|
||
|
|
||
|
c = self.c.transpose(perm)
|
||
|
|
||
|
c2 = np.zeros((c.shape[0] + nu,) + c.shape[1:],
|
||
|
dtype=c.dtype)
|
||
|
c2[:-nu] = c
|
||
|
|
||
|
# divide by the correct rising factorials
|
||
|
factor = spec.poch(np.arange(c.shape[0], 0, -1), nu)
|
||
|
c2[:-nu] /= factor[(slice(None),) + (None,)*(c.ndim-1)]
|
||
|
|
||
|
# fix continuity of added degrees of freedom
|
||
|
perm2 = list(range(c2.ndim))
|
||
|
perm2[1], perm2[ndim+axis] = perm2[ndim+axis], perm2[1]
|
||
|
|
||
|
c2 = c2.transpose(perm2)
|
||
|
c2 = c2.copy()
|
||
|
_ppoly.fix_continuity(c2.reshape(c2.shape[0], c2.shape[1], -1),
|
||
|
self.x[axis], nu-1)
|
||
|
|
||
|
c2 = c2.transpose(perm2)
|
||
|
c2 = c2.transpose(perm)
|
||
|
|
||
|
# Done
|
||
|
self.c = c2
|
||
|
|
||
|
def derivative(self, nu):
|
||
|
"""
|
||
|
Construct a new piecewise polynomial representing the derivative.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
nu : ndim-tuple of int
|
||
|
Order of derivatives to evaluate for each dimension.
|
||
|
If negative, the antiderivative is returned.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
pp : NdPPoly
|
||
|
Piecewise polynomial of orders (k[0] - nu[0], ..., k[n] - nu[n])
|
||
|
representing the derivative of this polynomial.
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
Derivatives are evaluated piecewise for each polynomial
|
||
|
segment, even if the polynomial is not differentiable at the
|
||
|
breakpoints. The polynomial intervals in each dimension are
|
||
|
considered half-open, ``[a, b)``, except for the last interval
|
||
|
which is closed ``[a, b]``.
|
||
|
|
||
|
"""
|
||
|
p = self.construct_fast(self.c.copy(), self.x, self.extrapolate)
|
||
|
|
||
|
for axis, n in enumerate(nu):
|
||
|
p._derivative_inplace(n, axis)
|
||
|
|
||
|
p._ensure_c_contiguous()
|
||
|
return p
|
||
|
|
||
|
def antiderivative(self, nu):
|
||
|
"""
|
||
|
Construct a new piecewise polynomial representing the antiderivative.
|
||
|
|
||
|
Antiderivative is also the indefinite integral of the function,
|
||
|
and derivative is its inverse operation.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
nu : ndim-tuple of int
|
||
|
Order of derivatives to evaluate for each dimension.
|
||
|
If negative, the derivative is returned.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
pp : PPoly
|
||
|
Piecewise polynomial of order k2 = k + n representing
|
||
|
the antiderivative of this polynomial.
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
The antiderivative returned by this function is continuous and
|
||
|
continuously differentiable to order n-1, up to floating point
|
||
|
rounding error.
|
||
|
|
||
|
"""
|
||
|
p = self.construct_fast(self.c.copy(), self.x, self.extrapolate)
|
||
|
|
||
|
for axis, n in enumerate(nu):
|
||
|
p._antiderivative_inplace(n, axis)
|
||
|
|
||
|
p._ensure_c_contiguous()
|
||
|
return p
|
||
|
|
||
|
def integrate_1d(self, a, b, axis, extrapolate=None):
|
||
|
r"""
|
||
|
Compute NdPPoly representation for one dimensional definite integral
|
||
|
|
||
|
The result is a piecewise polynomial representing the integral:
|
||
|
|
||
|
.. math::
|
||
|
|
||
|
p(y, z, ...) = \int_a^b dx\, p(x, y, z, ...)
|
||
|
|
||
|
where the dimension integrated over is specified with the
|
||
|
`axis` parameter.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
a, b : float
|
||
|
Lower and upper bound for integration.
|
||
|
axis : int
|
||
|
Dimension over which to compute the 1-D integrals
|
||
|
extrapolate : bool, optional
|
||
|
Whether to extrapolate to out-of-bounds points based on first
|
||
|
and last intervals, or to return NaNs.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
ig : NdPPoly or array-like
|
||
|
Definite integral of the piecewise polynomial over [a, b].
|
||
|
If the polynomial was 1D, an array is returned,
|
||
|
otherwise, an NdPPoly object.
|
||
|
|
||
|
"""
|
||
|
if extrapolate is None:
|
||
|
extrapolate = self.extrapolate
|
||
|
else:
|
||
|
extrapolate = bool(extrapolate)
|
||
|
|
||
|
ndim = len(self.x)
|
||
|
axis = int(axis) % ndim
|
||
|
|
||
|
# reuse 1-D integration routines
|
||
|
c = self.c
|
||
|
swap = list(range(c.ndim))
|
||
|
swap.insert(0, swap[axis])
|
||
|
del swap[axis + 1]
|
||
|
swap.insert(1, swap[ndim + axis])
|
||
|
del swap[ndim + axis + 1]
|
||
|
|
||
|
c = c.transpose(swap)
|
||
|
p = PPoly.construct_fast(c.reshape(c.shape[0], c.shape[1], -1),
|
||
|
self.x[axis],
|
||
|
extrapolate=extrapolate)
|
||
|
out = p.integrate(a, b, extrapolate=extrapolate)
|
||
|
|
||
|
# Construct result
|
||
|
if ndim == 1:
|
||
|
return out.reshape(c.shape[2:])
|
||
|
else:
|
||
|
c = out.reshape(c.shape[2:])
|
||
|
x = self.x[:axis] + self.x[axis+1:]
|
||
|
return self.construct_fast(c, x, extrapolate=extrapolate)
|
||
|
|
||
|
def integrate(self, ranges, extrapolate=None):
|
||
|
"""
|
||
|
Compute a definite integral over a piecewise polynomial.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
ranges : ndim-tuple of 2-tuples float
|
||
|
Sequence of lower and upper bounds for each dimension,
|
||
|
``[(a[0], b[0]), ..., (a[ndim-1], b[ndim-1])]``
|
||
|
extrapolate : bool, optional
|
||
|
Whether to extrapolate to out-of-bounds points based on first
|
||
|
and last intervals, or to return NaNs.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
ig : array_like
|
||
|
Definite integral of the piecewise polynomial over
|
||
|
[a[0], b[0]] x ... x [a[ndim-1], b[ndim-1]]
|
||
|
|
||
|
"""
|
||
|
|
||
|
ndim = len(self.x)
|
||
|
|
||
|
if extrapolate is None:
|
||
|
extrapolate = self.extrapolate
|
||
|
else:
|
||
|
extrapolate = bool(extrapolate)
|
||
|
|
||
|
if not hasattr(ranges, '__len__') or len(ranges) != ndim:
|
||
|
raise ValueError("Range not a sequence of correct length")
|
||
|
|
||
|
self._ensure_c_contiguous()
|
||
|
|
||
|
# Reuse 1D integration routine
|
||
|
c = self.c
|
||
|
for n, (a, b) in enumerate(ranges):
|
||
|
swap = list(range(c.ndim))
|
||
|
swap.insert(1, swap[ndim - n])
|
||
|
del swap[ndim - n + 1]
|
||
|
|
||
|
c = c.transpose(swap)
|
||
|
|
||
|
p = PPoly.construct_fast(c, self.x[n], extrapolate=extrapolate)
|
||
|
out = p.integrate(a, b, extrapolate=extrapolate)
|
||
|
c = out.reshape(c.shape[2:])
|
||
|
|
||
|
return c
|