|
__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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
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: |
|
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() |
|
|
|
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 |
|
|
|
|
|
|
|
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(f"{kind} is unsupported: Use fitpack " |
|
"routines for other types.") |
|
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.") |
|
|
|
|
|
if not issubclass(y.dtype.type, np.inexact): |
|
y = y.astype(np.float64) |
|
|
|
|
|
self.axis = axis % y.ndim |
|
|
|
|
|
self.y = y |
|
self._y = self._reshape_yi(self.y) |
|
self.x = x |
|
del y, x |
|
self._kind = kind |
|
|
|
|
|
|
|
|
|
|
|
if kind in ('linear', 'nearest', 'nearest-up', 'previous', 'next'): |
|
|
|
|
|
minval = 1 |
|
if kind == 'nearest': |
|
|
|
|
|
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': |
|
|
|
|
|
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': |
|
|
|
self._side = 'left' |
|
self._ind = 0 |
|
|
|
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() |
|
|
|
fill_value = (np.nan, np.take(self.y, -1, axis)) |
|
elif kind == 'next': |
|
self._side = 'right' |
|
self._ind = 1 |
|
|
|
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() |
|
|
|
fill_value = (np.take(self.y, 0, axis), np.nan) |
|
else: |
|
|
|
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: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
@property |
|
def fill_value(self): |
|
"""The fill value.""" |
|
|
|
return self._fill_value_orig |
|
|
|
@fill_value.setter |
|
def fill_value(self, fill_value): |
|
|
|
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,) |
|
|
|
|
|
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 |
|
|
|
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): |
|
|
|
return np.interp(x_new, self.x, self.y) |
|
|
|
def _call_linear(self, x_new): |
|
|
|
|
|
|
|
x_new_indices = searchsorted(self.x, x_new) |
|
|
|
|
|
|
|
|
|
x_new_indices = x_new_indices.clip(1, len(self.x)-1).astype(int) |
|
|
|
|
|
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] |
|
|
|
|
|
|
|
slope = (y_hi - y_lo) / (x_hi - x_lo)[:, None] |
|
|
|
|
|
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).""" |
|
|
|
|
|
|
|
|
|
|
|
x_new_indices = searchsorted(self.x_bds, x_new, side=self._side) |
|
|
|
|
|
x_new_indices = x_new_indices.clip(0, len(self.x)-1).astype(intp) |
|
|
|
|
|
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).""" |
|
|
|
|
|
x_new_indices = searchsorted(self._x_shift, x_new, side=self._side) |
|
|
|
|
|
x_new_indices = x_new_indices.clip(1-self._ind, |
|
len(self.x)-self._ind).astype(intp) |
|
|
|
|
|
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): |
|
|
|
|
|
|
|
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: |
|
|
|
|
|
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. |
|
""" |
|
|
|
|
|
|
|
|
|
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]}).") |
|
|
|
|
|
|
|
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: |
|
|
|
|
|
|
|
|
|
|
|
|
|
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. |
|
|
|
Notes |
|
----- |
|
This method is not thread safe and must not be executed concurrently |
|
with other methods available in this class. Doing so may cause |
|
unexpected errors or numerical output mismatches. |
|
""" |
|
|
|
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) |
|
|
|
|
|
|
|
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: |
|
|
|
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) |
|
|
|
|
|
if nu == 0: |
|
c2 = self.c.copy() |
|
else: |
|
c2 = self.c[:-nu, :].copy() |
|
|
|
if c2.shape[0] == 0: |
|
|
|
c2 = np.zeros((1,) + c2.shape[1:], dtype=c2.dtype) |
|
|
|
|
|
factor = spec.poch(np.arange(c2.shape[0], 0, -1), nu) |
|
c2 *= factor[(slice(None),) + (None,)*(c2.ndim-1)] |
|
|
|
|
|
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 |
|
|
|
|
|
factor = spec.poch(np.arange(self.c.shape[0], 0, -1), nu) |
|
c[:-nu] /= factor[(slice(None),) + (None,)*(c.ndim-1)] |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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() |
|
|
|
|
|
if extrapolate == 'periodic': |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
a = xs + (a - xs) % period |
|
b = a + left |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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(f".from_bernstein_basis only accepts BPoly instances. " |
|
f"Got {type(bp)} instead.") |
|
|
|
dx = np.diff(bp.x) |
|
k = bp.c.shape[0] - 1 |
|
|
|
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 |
|
|
|
""" |
|
|
|
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 |
|
|
|
|
|
if nu == 0: |
|
c2 = self.c.copy() |
|
else: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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: |
|
|
|
c2 = np.zeros((1,) + c2.shape[1:], dtype=c2.dtype) |
|
|
|
|
|
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 |
|
|
|
|
|
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)] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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] |
|
|
|
""" |
|
|
|
|
|
ib = self.antiderivative() |
|
if extrapolate is None: |
|
extrapolate = self.extrapolate |
|
|
|
|
|
|
|
if extrapolate != 'periodic': |
|
ib.extrapolate = extrapolate |
|
|
|
if extrapolate == 'periodic': |
|
|
|
|
|
|
|
|
|
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)) |
|
|
|
|
|
a = xs + (a - xs) % period |
|
b = a + left |
|
|
|
|
|
|
|
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(f".from_power_basis only accepts PPoly instances. " |
|
f"Got {type(pp)} instead.") |
|
|
|
dx = np.diff(pp.x) |
|
k = pp.c.shape[0] - 1 |
|
|
|
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") |
|
|
|
|
|
m = len(xi) - 1 |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
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] |
|
|
|
|
|
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 |
|
|
|
|
|
if nu == 0: |
|
|
|
return |
|
else: |
|
sl = [slice(None)]*ndim |
|
sl[axis] = slice(None, -nu, None) |
|
c2 = self.c[tuple(sl)] |
|
|
|
if c2.shape[axis] == 0: |
|
|
|
shp = list(c2.shape) |
|
shp[axis] = 1 |
|
c2 = np.zeros(shp, dtype=c2.dtype) |
|
|
|
|
|
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 |
|
|
|
|
|
factor = spec.poch(np.arange(c.shape[0], 0, -1), nu) |
|
c2[:-nu] /= factor[(slice(None),) + (None,)*(c.ndim-1)] |
|
|
|
|
|
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) |
|
|
|
|
|
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 |
|
|
|
|
|
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) |
|
|
|
|
|
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() |
|
|
|
|
|
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 |
|
|