| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029 |
- import warnings
- from types import GenericAlias
- import numpy as np
- from scipy.special import factorial
- from scipy._lib._util import (_asarray_validated, float_factorial, check_random_state,
- _transition_to_rng)
- __all__ = ["KroghInterpolator", "krogh_interpolate",
- "BarycentricInterpolator", "barycentric_interpolate",
- "approximate_taylor_polynomial"]
- def _isscalar(x):
- """Check whether x is if a scalar type, or 0-dim"""
- return np.isscalar(x) or hasattr(x, 'shape') and x.shape == ()
- class _Interpolator1D:
- """
- Common features in univariate interpolation
- Deal with input data type and interpolation axis rolling. The
- actual interpolator can assume the y-data is of shape (n, r) where
- `n` is the number of x-points, and `r` the number of variables,
- and use self.dtype as the y-data type.
- Attributes
- ----------
- _y_axis
- Axis along which the interpolation goes in the original array
- _y_extra_shape
- Additional trailing shape of the input arrays, excluding
- the interpolation axis.
- dtype
- Dtype of the y-data arrays. Can be set via _set_dtype, which
- forces it to be float or complex.
- Methods
- -------
- __call__
- _prepare_x
- _finish_y
- _reshape_yi
- _set_yi
- _set_dtype
- _evaluate
- """
- __slots__ = ('_y_axis', '_y_extra_shape', 'dtype')
- # generic type compatibility with scipy-stubs
- __class_getitem__ = classmethod(GenericAlias)
- def __init__(self, xi=None, yi=None, axis=None):
- self._y_axis = axis
- self._y_extra_shape = None
- self.dtype = None
- if yi is not None:
- self._set_yi(yi, xi=xi, axis=axis)
- def __call__(self, x):
- """
- Evaluate the interpolant
- Parameters
- ----------
- x : array_like
- Point or points at which to evaluate the interpolant.
- Returns
- -------
- y : array_like
- Interpolated values. Shape is determined by replacing
- the interpolation axis in the original array with the shape of `x`.
- Notes
- -----
- Input values `x` must be convertible to `float` values like `int`
- or `float`.
- """
- x, x_shape = self._prepare_x(x)
- y = self._evaluate(x)
- return self._finish_y(y, x_shape)
- def _evaluate(self, x):
- """
- Actually evaluate the value of the interpolator.
- """
- raise NotImplementedError()
- def _prepare_x(self, x):
- """Reshape input x array to 1-D"""
- x = _asarray_validated(x, check_finite=False, as_inexact=True)
- x_shape = x.shape
- return x.ravel(), x_shape
- def _finish_y(self, y, x_shape):
- """Reshape interpolated y back to an N-D array similar to initial y"""
- y = y.reshape(x_shape + self._y_extra_shape)
- if self._y_axis != 0 and x_shape != ():
- nx = len(x_shape)
- ny = len(self._y_extra_shape)
- s = (list(range(nx, nx + self._y_axis))
- + list(range(nx)) + list(range(nx+self._y_axis, nx+ny)))
- y = y.transpose(s)
- return y
- def _reshape_yi(self, yi, check=False):
- yi = np.moveaxis(np.asarray(yi), self._y_axis, 0)
- if check and yi.shape[1:] != self._y_extra_shape:
- ok_shape = (f"{self._y_extra_shape[-self._y_axis:]!r} + (N,) + "
- f"{self._y_extra_shape[:-self._y_axis]!r}")
- raise ValueError(f"Data must be of shape {ok_shape}")
- return yi.reshape((yi.shape[0], -1))
- def _set_yi(self, yi, xi=None, axis=None):
- if axis is None:
- axis = self._y_axis
- if axis is None:
- raise ValueError("no interpolation axis specified")
- yi = np.asarray(yi)
- shape = yi.shape
- if shape == ():
- shape = (1,)
- if xi is not None and shape[axis] != len(xi):
- raise ValueError("x and y arrays must be equal in length along "
- "interpolation axis.")
- self._y_axis = (axis % yi.ndim)
- self._y_extra_shape = yi.shape[:self._y_axis] + yi.shape[self._y_axis+1:]
- self.dtype = None
- self._set_dtype(yi.dtype)
- def _set_dtype(self, dtype, union=False):
- if np.issubdtype(dtype, np.complexfloating) \
- or np.issubdtype(self.dtype, np.complexfloating):
- self.dtype = np.complex128
- else:
- if not union or self.dtype != np.complex128:
- self.dtype = np.float64
- class _Interpolator1DWithDerivatives(_Interpolator1D):
- def derivatives(self, x, der=None):
- """
- Evaluate several derivatives of the polynomial at the point `x`
- Produce an array of derivatives evaluated at the point `x`.
- Parameters
- ----------
- x : array_like
- Point or points at which to evaluate the derivatives
- der : int or list or None, optional
- How many derivatives to evaluate, or None for all potentially
- nonzero derivatives (that is, a number equal to the number
- of points), or a list of derivatives to evaluate. This number
- includes the function value as the '0th' derivative.
- Returns
- -------
- d : ndarray
- Array with derivatives; ``d[j]`` contains the jth derivative.
- Shape of ``d[j]`` is determined by replacing the interpolation
- axis in the original array with the shape of `x`.
- Examples
- --------
- >>> from scipy.interpolate import KroghInterpolator
- >>> KroghInterpolator([0,0,0],[1,2,3]).derivatives(0)
- array([1.0,2.0,3.0])
- >>> KroghInterpolator([0,0,0],[1,2,3]).derivatives([0,0])
- array([[1.0,1.0],
- [2.0,2.0],
- [3.0,3.0]])
- """
- x, x_shape = self._prepare_x(x)
- y = self._evaluate_derivatives(x, der)
- y = y.reshape((y.shape[0],) + x_shape + self._y_extra_shape)
- if self._y_axis != 0 and x_shape != ():
- nx = len(x_shape)
- ny = len(self._y_extra_shape)
- s = ([0] + list(range(nx+1, nx + self._y_axis+1))
- + list(range(1, nx+1)) +
- list(range(nx+1+self._y_axis, nx+ny+1)))
- y = y.transpose(s)
- return y
- def derivative(self, x, der=1):
- """
- Evaluate a single derivative of the polynomial at the point `x`.
- Parameters
- ----------
- x : array_like
- Point or points at which to evaluate the derivatives
- der : integer, optional
- Which derivative to evaluate (default: first derivative).
- This number includes the function value as 0th derivative.
- Returns
- -------
- d : ndarray
- Derivative interpolated at the x-points. Shape of `d` is
- determined by replacing the interpolation axis in the
- original array with the shape of `x`.
- Notes
- -----
- This may be computed by evaluating all derivatives up to the desired
- one (using self.derivatives()) and then discarding the rest.
- """
- x, x_shape = self._prepare_x(x)
- y = self._evaluate_derivatives(x, der+1)
- return self._finish_y(y[der], x_shape)
- def _evaluate_derivatives(self, x, der=None):
- """
- Actually evaluate the derivatives.
- Parameters
- ----------
- x : array_like
- 1D array of points at which to evaluate the derivatives
- der : integer, optional
- The number of derivatives to evaluate, from 'order 0' (der=1)
- to order der-1. If omitted, return all possibly-non-zero
- derivatives, ie 0 to order n-1.
- Returns
- -------
- d : ndarray
- Array of shape ``(der, x.size, self.yi.shape[1])`` containing
- the derivatives from 0 to der-1
- """
- raise NotImplementedError()
- class KroghInterpolator(_Interpolator1DWithDerivatives):
- """Krogh interpolator (C∞ smooth).
- The polynomial passes through all the pairs ``(xi, yi)``. One may
- additionally specify a number of derivatives at each point `xi`;
- this is done by repeating the value `xi` and specifying the
- derivatives as successive `yi` values.
- Allows evaluation of the polynomial and all its derivatives.
- For reasons of numerical stability, this function does not compute
- the coefficients of the polynomial, although they can be obtained
- by evaluating all the derivatives.
- Parameters
- ----------
- xi : array_like, shape (npoints, )
- Known x-coordinates. Must be sorted in increasing order.
- yi : array_like, shape (..., npoints, ...)
- Known y-coordinates. When an xi occurs two or more times in
- a row, the corresponding yi's represent derivative values. The length of `yi`
- along the interpolation axis must be equal to the length of `xi`. Use the
- `axis` parameter to select the correct axis.
- axis : int, optional
- Axis in the `yi` array corresponding to the x-coordinate values. Defaults to
- ``axis=0``.
- Notes
- -----
- Be aware that the algorithms implemented here are not necessarily
- the most numerically stable known. Moreover, even in a world of
- exact computation, unless the x coordinates are chosen very
- carefully - Chebyshev zeros (e.g., cos(i*pi/n)) are a good choice -
- polynomial interpolation itself is a very ill-conditioned process
- due to the Runge phenomenon. In general, even with well-chosen
- x values, degrees higher than about thirty cause problems with
- numerical instability in this code.
- Based on [1]_.
- References
- ----------
- .. [1] Krogh, "Efficient Algorithms for Polynomial Interpolation
- and Numerical Differentiation", 1970.
- Examples
- --------
- To produce a polynomial that is zero at 0 and 1 and has
- derivative 2 at 0, call
- >>> from scipy.interpolate import KroghInterpolator
- >>> KroghInterpolator([0,0,1],[0,2,0])
- This constructs the quadratic :math:`2x^2-2x`. The derivative condition
- is indicated by the repeated zero in the `xi` array; the corresponding
- yi values are 0, the function value, and 2, the derivative value.
- For another example, given `xi`, `yi`, and a derivative `ypi` for each
- point, appropriate arrays can be constructed as:
- >>> import numpy as np
- >>> rng = np.random.default_rng()
- >>> xi = np.linspace(0, 1, 5)
- >>> yi, ypi = rng.random((2, 5))
- >>> xi_k, yi_k = np.repeat(xi, 2), np.ravel(np.dstack((yi,ypi)))
- >>> KroghInterpolator(xi_k, yi_k)
- To produce a vector-valued polynomial, supply a higher-dimensional
- array for `yi`:
- >>> KroghInterpolator([0,1],[[2,3],[4,5]])
- This constructs a linear polynomial giving (2,3) at 0 and (4,5) at 1.
- """
- def __init__(self, xi, yi, axis=0):
- super().__init__(xi, yi, axis)
- self.xi = np.asarray(xi)
- self.yi = self._reshape_yi(yi)
- self.n, self.r = self.yi.shape
- if (deg := self.xi.size) > 30:
- warnings.warn(f"{deg} degrees provided, degrees higher than about"
- " thirty cause problems with numerical instability "
- "with 'KroghInterpolator'", stacklevel=2)
- c = np.zeros((self.n+1, self.r), dtype=self.dtype)
- c[0] = self.yi[0]
- Vk = np.zeros((self.n, self.r), dtype=self.dtype)
- for k in range(1, self.n):
- s = 0
- while s <= k and xi[k-s] == xi[k]:
- s += 1
- s -= 1
- Vk[0] = self.yi[k]/float_factorial(s)
- for i in range(k-s):
- if xi[i] == xi[k]:
- raise ValueError("Elements of `xi` can't be equal.")
- if s == 0:
- Vk[i+1] = (c[i]-Vk[i])/(xi[i]-xi[k])
- else:
- Vk[i+1] = (Vk[i+1]-Vk[i])/(xi[i]-xi[k])
- c[k] = Vk[k-s]
- self.c = c
- def _evaluate(self, x):
- pi = 1
- p = np.zeros((len(x), self.r), dtype=self.dtype)
- p += self.c[0,np.newaxis,:]
- for k in range(1, self.n):
- w = x - self.xi[k-1]
- pi = w*pi
- p += pi[:,np.newaxis] * self.c[k]
- return p
- def _evaluate_derivatives(self, x, der=None):
- n = self.n
- r = self.r
- if der is None:
- der = self.n
- pi = np.zeros((n, len(x)))
- w = np.zeros((n, len(x)))
- pi[0] = 1
- p = np.zeros((len(x), self.r), dtype=self.dtype)
- p += self.c[0, np.newaxis, :]
- for k in range(1, n):
- w[k-1] = x - self.xi[k-1]
- pi[k] = w[k-1] * pi[k-1]
- p += pi[k, :, np.newaxis] * self.c[k]
- cn = np.zeros((max(der, n+1), len(x), r), dtype=self.dtype)
- cn[:n+1, :, :] += self.c[:n+1, np.newaxis, :]
- cn[0] = p
- for k in range(1, n):
- for i in range(1, n-k+1):
- pi[i] = w[k+i-1]*pi[i-1] + pi[i]
- cn[k] = cn[k] + pi[i, :, np.newaxis]*cn[k+i]
- cn[k] *= float_factorial(k)
- cn[n, :, :] = 0
- return cn[:der]
- def krogh_interpolate(xi, yi, x, der=0, axis=0):
- """Convenience function for Krogh interpolation.
- See `KroghInterpolator` for more details.
- Parameters
- ----------
- xi : array_like
- Interpolation points (known x-coordinates).
- yi : array_like
- Known y-coordinates, of shape ``(xi.size, R)``. Interpreted as
- vectors of length R, or scalars if R=1.
- x : array_like
- Point or points at which to evaluate the derivatives.
- der : int or list or None, optional
- How many derivatives to evaluate, or None for all potentially
- nonzero derivatives (that is, a number equal to the number
- of points), or a list of derivatives to evaluate. This number
- includes the function value as the '0th' derivative.
- axis : int, optional
- Axis in the `yi` array corresponding to the x-coordinate values.
- Returns
- -------
- d : ndarray
- If the interpolator's values are R-D then the
- returned array will be the number of derivatives by N by R.
- If `x` is a scalar, the middle dimension will be dropped; if
- the `yi` are scalars then the last dimension will be dropped.
- See Also
- --------
- KroghInterpolator : Krogh interpolator
- Notes
- -----
- Construction of the interpolating polynomial is a relatively expensive
- process. If you want to evaluate it repeatedly consider using the class
- KroghInterpolator (which is what this function uses).
- Examples
- --------
- We can interpolate 2D observed data using Krogh interpolation:
- >>> import numpy as np
- >>> import matplotlib.pyplot as plt
- >>> from scipy.interpolate import krogh_interpolate
- >>> x_observed = np.linspace(0.0, 10.0, 11)
- >>> y_observed = np.sin(x_observed)
- >>> x = np.linspace(min(x_observed), max(x_observed), num=100)
- >>> y = krogh_interpolate(x_observed, y_observed, x)
- >>> plt.plot(x_observed, y_observed, "o", label="observation")
- >>> plt.plot(x, y, label="krogh interpolation")
- >>> plt.legend()
- >>> plt.show()
- """
- P = KroghInterpolator(xi, yi, axis=axis)
- if der == 0:
- return P(x)
- elif _isscalar(der):
- return P.derivative(x, der=der)
- else:
- return P.derivatives(x, der=np.amax(der)+1)[der]
- def approximate_taylor_polynomial(f,x,degree,scale,order=None):
- """
- Estimate the Taylor polynomial of f at x by polynomial fitting.
- Parameters
- ----------
- f : callable
- The function whose Taylor polynomial is sought. Should accept
- a vector of `x` values.
- x : scalar
- The point at which the polynomial is to be evaluated.
- degree : int
- The degree of the Taylor polynomial
- scale : scalar
- The width of the interval to use to evaluate the Taylor polynomial.
- Function values spread over a range this wide are used to fit the
- polynomial. Must be chosen carefully.
- order : int or None, optional
- The order of the polynomial to be used in the fitting; `f` will be
- evaluated ``order+1`` times. If None, use `degree`.
- Returns
- -------
- p : poly1d instance
- The Taylor polynomial (translated to the origin, so that
- for example p(0)=f(x)).
- Notes
- -----
- The appropriate choice of "scale" is a trade-off; too large and the
- function differs from its Taylor polynomial too much to get a good
- answer, too small and round-off errors overwhelm the higher-order terms.
- The algorithm used becomes numerically unstable around order 30 even
- under ideal circumstances.
- Choosing order somewhat larger than degree may improve the higher-order
- terms.
- Examples
- --------
- We can calculate Taylor approximation polynomials of sin function with
- various degrees:
- >>> import numpy as np
- >>> import matplotlib.pyplot as plt
- >>> from scipy.interpolate import approximate_taylor_polynomial
- >>> x = np.linspace(-10.0, 10.0, num=100)
- >>> plt.plot(x, np.sin(x), label="sin curve")
- >>> for degree in np.arange(1, 15, step=2):
- ... sin_taylor = approximate_taylor_polynomial(np.sin, 0, degree, 1,
- ... order=degree + 2)
- ... plt.plot(x, sin_taylor(x), label=f"degree={degree}")
- >>> plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left',
- ... borderaxespad=0.0, shadow=True)
- >>> plt.tight_layout()
- >>> plt.axis([-10, 10, -10, 10])
- >>> plt.show()
- """
- if order is None:
- order = degree
- n = order+1
- # Choose n points that cluster near the endpoints of the interval in
- # a way that avoids the Runge phenomenon. Ensure, by including the
- # endpoint or not as appropriate, that one point always falls at x
- # exactly.
- xs = scale*np.cos(np.linspace(0,np.pi,n,endpoint=n % 1)) + x
- P = KroghInterpolator(xs, f(xs))
- d = P.derivatives(x,der=degree+1)
- return np.poly1d((d/factorial(np.arange(degree+1)))[::-1])
- class BarycentricInterpolator(_Interpolator1DWithDerivatives):
- r"""Barycentric (Lagrange with improved stability) interpolator (C∞ smooth).
- Constructs a polynomial that passes through a given set of points.
- Allows evaluation of the polynomial and all its derivatives,
- efficient changing of the y-values to be interpolated,
- and updating by adding more x- and y-values. For numerical stability, a barycentric
- representation is used rather than computing the coefficients of the polynomial
- directly.
- Parameters
- ----------
- xi : array_like, shape (npoints, )
- 1-D array of x-coordinates of the points the polynomial
- should pass through
- yi : array_like, shape (..., npoints, ...), optional
- N-D array of y-coordinates of the points the polynomial should pass through.
- If None, the y values will be supplied later via the `set_y` method.
- The length of `yi` along the interpolation axis must be equal to the length
- of `xi`. Use the ``axis`` parameter to select correct axis.
- axis : int, optional
- Axis in the yi array corresponding to the x-coordinate values. Defaults
- to ``axis=0``.
- wi : array_like, optional
- The barycentric weights for the chosen interpolation points `xi`.
- If absent or None, the weights will be computed from `xi` (default).
- This allows for the reuse of the weights `wi` if several interpolants
- are being calculated using the same nodes `xi`, without re-computation. This
- also allows for computing the weights explicitly for some choices of
- `xi` (see notes).
- rng : {None, int, `numpy.random.Generator`}, optional
- If `rng` is passed by keyword, types other than `numpy.random.Generator` are
- passed to `numpy.random.default_rng` to instantiate a ``Generator``.
- If `rng` is already a ``Generator`` instance, then the provided instance is
- used. Specify `rng` for repeatable interpolation.
- If this argument `random_state` is passed by keyword,
- legacy behavior for the argument `random_state` applies:
- - If `random_state` is None (or `numpy.random`), the `numpy.random.RandomState`
- singleton is used.
- - If `random_state` is an int, a new ``RandomState`` instance is used,
- seeded with `random_state`.
- - If `random_state` is already a ``Generator`` or ``RandomState`` instance then
- that instance is used.
- .. versionchanged:: 1.15.0
- As part of the `SPEC-007 <https://scientific-python.org/specs/spec-0007/>`_
- transition from use of `numpy.random.RandomState` to
- `numpy.random.Generator` this keyword was changed from `random_state` to `rng`.
- For an interim period, both keywords will continue to work (only specify
- one of them). After the interim period using the `random_state` keyword will emit
- warnings. The behavior of the `random_state` and `rng` keywords is outlined above.
- Notes
- -----
- This method is a variant of Lagrange polynomial interpolation [1]_ based on [2]_.
- Instead of using Lagrange's or Newton's formula, the polynomial is represented by
- the barycentric formula
- .. math::
- p(x) =
- \frac{\sum_{i=1}^m\ w_i y_i / (x - x_i)}{\sum_{i=1}^m w_i / (x - x_i)},
- where :math:`w_i` are the barycentric weights computed with the general formula
- .. math::
- w_i = \left( \prod_{k \neq i} x_i - x_k \right)^{-1}.
- This is the same barycentric form used by `AAA` and `FloaterHormannInterpolator`.
- However, in contrast, the weights :math:`w_i` are defined such that
- :math:`p(x)` is a polynomial rather than a rational function.
- The barycentric representation avoids many of the problems associated with
- polynomial interpolation caused by floating-point arithmetic. However, it does not
- avoid issues that are intrinsic to polynomial interpolation. Namely, if the
- x-coordinates are equally spaced, then the weights can be computed explicitly using
- the formula from [2]_
- .. math::
- w_i = (-1)^i {n \choose i},
- where :math:`n` is the number of x-coordinates. As noted in [2]_, this means that
- for large :math:`n` the weights vary by exponentially large factors, leading to the
- Runge phenomenon.
- To avoid this ill-conditioning, the x-coordinates should be clustered at the
- endpoints of the interval. An excellent choice of points on the interval
- :math:`[a,b]` are Chebyshev points of the second kind
- .. math::
- x_i = \frac{a + b}{2} + \frac{a - b}{2}\cos(i\pi/n).
- in which case the weights can be computed explicitly as
- .. math::
- w_i = \begin{cases}
- (-1)^i/2 & i = 0,n \\
- (-1)^i & \text{otherwise}
- \end{cases}.
- See [2]_ for more infomation. Note that for large :math:`n`, computing the weights
- explicitly (see examples) will be faster than the generic formula.
- References
- ----------
- .. [1] https://en.wikipedia.org/wiki/Lagrange_polynomial
- .. [2] Jean-Paul Berrut and Lloyd N. Trefethen, "Barycentric Lagrange
- Interpolation", SIAM Review 2004 46:3, 501-517
- :doi:`10.1137/S0036144502417715`
- Examples
- --------
- To produce a quintic barycentric interpolant approximating the function
- :math:`\sin x`, and its first four derivatives, using six randomly-spaced
- nodes in :math:`(0, \pi/2)`:
- >>> import numpy as np
- >>> import matplotlib.pyplot as plt
- >>> from scipy.interpolate import BarycentricInterpolator
- >>> rng = np.random.default_rng()
- >>> xi = rng.random(6) * np.pi/2
- >>> f, f_d1, f_d2, f_d3, f_d4 = np.sin, np.cos, lambda x: -np.sin(x), lambda x: -np.cos(x), np.sin
- >>> P = BarycentricInterpolator(xi, f(xi), random_state=rng)
- >>> fig, axs = plt.subplots(5, 1, sharex=True, layout='constrained', figsize=(7,10))
- >>> x = np.linspace(0, np.pi, 100)
- >>> axs[0].plot(x, P(x), 'r:', x, f(x), 'k--', xi, f(xi), 'xk')
- >>> axs[1].plot(x, P.derivative(x), 'r:', x, f_d1(x), 'k--', xi, f_d1(xi), 'xk')
- >>> axs[2].plot(x, P.derivative(x, 2), 'r:', x, f_d2(x), 'k--', xi, f_d2(xi), 'xk')
- >>> axs[3].plot(x, P.derivative(x, 3), 'r:', x, f_d3(x), 'k--', xi, f_d3(xi), 'xk')
- >>> axs[4].plot(x, P.derivative(x, 4), 'r:', x, f_d4(x), 'k--', xi, f_d4(xi), 'xk')
- >>> axs[0].set_xlim(0, np.pi)
- >>> axs[4].set_xlabel(r"$x$")
- >>> axs[4].set_xticks([i * np.pi / 4 for i in range(5)],
- ... ["0", r"$\frac{\pi}{4}$", r"$\frac{\pi}{2}$", r"$\frac{3\pi}{4}$", r"$\pi$"])
- >>> for ax, label in zip(axs, ("$f(x)$", "$f'(x)$", "$f''(x)$", "$f^{(3)}(x)$", "$f^{(4)}(x)$")):
- ... ax.set_ylabel(label)
- >>> labels = ['Interpolation nodes', 'True function $f$', 'Barycentric interpolation']
- >>> axs[0].legend(axs[0].get_lines()[::-1], labels, bbox_to_anchor=(0., 1.02, 1., .102),
- ... loc='lower left', ncols=3, mode="expand", borderaxespad=0., frameon=False)
- >>> plt.show()
- Next, we show how using Chebyshev points of the second kind avoids the
- Runge phenomenon. In this example, we also compute the weights explicitly.
- >>> n = 20
- >>> def f(x): return np.abs(x) + 0.5*x - x**2
- >>> i = np.arange(n)
- >>> x_cheb = np.cos(i*np.pi/(n - 1)) # Chebyshev points on [-1, 1]
- >>> w_i_cheb = (-1.)**i # Explicit formula for weights of Chebyshev points
- >>> w_i_cheb[[0, -1]] /= 2
- >>> p_cheb = BarycentricInterpolator(x_cheb, f(x_cheb), wi=w_i_cheb)
- >>> x_equi = np.linspace(-1, 1, n)
- >>> p_equi = BarycentricInterpolator(x_equi, f(x_equi))
- >>> xx = np.linspace(-1, 1, 1000)
- >>> fig, ax = plt.subplots()
- >>> ax.plot(xx, f(xx), label="Original Function")
- >>> ax.plot(xx, p_cheb(xx), "--", label="Chebshev Points")
- >>> ax.plot(xx, p_equi(xx), "--", label="Equally Spaced Points")
- >>> ax.set(xlabel="$x$", ylabel="$f(x)$", xlim=[-1, 1])
- >>> ax.legend()
- >>> plt.show()
- """ # numpy/numpydoc#87 # noqa: E501
- @_transition_to_rng("random_state", replace_doc=False)
- def __init__(self, xi, yi=None, axis=0, *, wi=None, rng=None):
- super().__init__(xi, yi, axis)
- rng = check_random_state(rng)
- self.xi = np.asarray(xi, dtype=np.float64)
- self.set_yi(yi)
- self.n = len(self.xi)
- # cache derivative object to avoid re-computing the weights with every call.
- self._diff_cij = None
- if wi is not None:
- self.wi = wi
- else:
- # See page 510 of Berrut and Trefethen 2004 for an explanation of the
- # capacity scaling and the suggestion of using a random permutation of
- # the input factors.
- # At the moment, the permutation is not performed for xi that are
- # appended later through the add_xi interface. It's not clear to me how
- # to implement that and it seems that most situations that require
- # these numerical stability improvements will be able to provide all
- # the points to the constructor.
- self._inv_capacity = 4.0 / (np.max(self.xi) - np.min(self.xi))
- permute = rng.permutation(self.n, )
- inv_permute = np.zeros(self.n, dtype=np.int32)
- inv_permute[permute] = np.arange(self.n)
- self.wi = np.zeros(self.n)
- for i in range(self.n):
- dist = self._inv_capacity * (self.xi[i] - self.xi[permute])
- dist[inv_permute[i]] = 1.0
- prod = np.prod(dist)
- if prod == 0.0:
- raise ValueError("Interpolation points xi must be"
- " distinct.")
- self.wi[i] = 1.0 / prod
- def set_yi(self, yi, axis=None):
- """
- Update the y values to be interpolated
- The barycentric interpolation algorithm requires the calculation
- of weights, but these depend only on the `xi`. The `yi` can be changed
- at any time.
- Parameters
- ----------
- yi : array_like
- The y-coordinates of the points the polynomial will pass through.
- If None, the y values must be supplied later.
- axis : int, optional
- Axis in the `yi` array corresponding to the x-coordinate values.
- """
- if yi is None:
- self.yi = None
- return
- self._set_yi(yi, xi=self.xi, axis=axis)
- self.yi = self._reshape_yi(yi)
- self.n, self.r = self.yi.shape
- self._diff_baryint = None
- def add_xi(self, xi, yi=None):
- """
- Add more x values to the set to be interpolated
- The barycentric interpolation algorithm allows easy updating by
- adding more points for the polynomial to pass through.
- Parameters
- ----------
- xi : array_like
- The x coordinates of the points that the polynomial should pass
- through.
- yi : array_like, optional
- The y coordinates of the points the polynomial should pass through.
- Should have shape ``(xi.size, R)``; if R > 1 then the polynomial is
- vector-valued.
- If `yi` is not given, the y values will be supplied later. `yi`
- should be given if and only if the interpolator has y values
- specified.
- Notes
- -----
- The new points added by `add_xi` are not randomly permuted
- so there is potential for numerical instability,
- especially for a large number of points. If this
- happens, please reconstruct interpolation from scratch instead.
- """
- if yi is not None:
- if self.yi is None:
- raise ValueError("No previous yi value to update!")
- yi = self._reshape_yi(yi, check=True)
- self.yi = np.vstack((self.yi,yi))
- else:
- if self.yi is not None:
- raise ValueError("No update to yi provided!")
- old_n = self.n
- self.xi = np.concatenate((self.xi,xi))
- self.n = len(self.xi)
- self.wi **= -1
- old_wi = self.wi
- self.wi = np.zeros(self.n)
- self.wi[:old_n] = old_wi
- for j in range(old_n, self.n):
- self.wi[:j] *= self._inv_capacity * (self.xi[j]-self.xi[:j])
- self.wi[j] = np.multiply.reduce(
- self._inv_capacity * (self.xi[:j]-self.xi[j])
- )
- self.wi **= -1
- self._diff_cij = None
- self._diff_baryint = None
- def __call__(self, x):
- """Evaluate the interpolating polynomial at the points x
- Parameters
- ----------
- x : array_like
- Point or points at which to evaluate the interpolant.
- Returns
- -------
- y : array_like
- Interpolated values. Shape is determined by replacing
- the interpolation axis in the original array with the shape of `x`.
- Notes
- -----
- Currently the code computes an outer product between `x` and the
- weights, that is, it constructs an intermediate array of size
- ``(N, len(x))``, where N is the degree of the polynomial.
- """
- return _Interpolator1D.__call__(self, x)
- def _evaluate(self, x):
- if x.size == 0:
- p = np.zeros((0, self.r), dtype=self.dtype)
- else:
- c = x[..., np.newaxis] - self.xi
- z = c == 0
- c[z] = 1
- c = self.wi / c
- with np.errstate(divide='ignore'):
- p = np.dot(c, self.yi) / np.sum(c, axis=-1)[..., np.newaxis]
- # Now fix where x==some xi
- r = np.nonzero(z)
- if len(r) == 1: # evaluation at a scalar
- if len(r[0]) > 0: # equals one of the points
- p = self.yi[r[0][0]]
- else:
- p[r[:-1]] = self.yi[r[-1]]
- return p
- def derivative(self, x, der=1):
- """
- Evaluate a single derivative of the polynomial at the point x.
- Parameters
- ----------
- x : array_like
- Point or points at which to evaluate the derivatives
- der : integer, optional
- Which derivative to evaluate (default: first derivative).
- This number includes the function value as 0th derivative.
- Returns
- -------
- d : ndarray
- Derivative interpolated at the x-points. Shape of `d` is
- determined by replacing the interpolation axis in the
- original array with the shape of `x`.
- """
- x, x_shape = self._prepare_x(x)
- y = self._evaluate_derivatives(x, der+1, all_lower=False)
- return self._finish_y(y, x_shape)
- def _evaluate_derivatives(self, x, der=None, all_lower=True):
- # NB: der here is not the order of the highest derivative;
- # instead, it is the size of the derivatives matrix that
- # would be returned with all_lower=True, including the
- # '0th' derivative (the undifferentiated function).
- # E.g. to evaluate the 5th derivative alone, call
- # _evaluate_derivatives(x, der=6, all_lower=False).
- if (not all_lower) and (x.size == 0 or self.r == 0):
- return np.zeros((0, self.r), dtype=self.dtype)
- if (not all_lower) and der == 1:
- return self._evaluate(x)
- if (not all_lower) and (der > self.n):
- return np.zeros((len(x), self.r), dtype=self.dtype)
- if der is None:
- der = self.n
- if all_lower and (x.size == 0 or self.r == 0):
- return np.zeros((der, len(x), self.r), dtype=self.dtype)
- if self._diff_cij is None:
- # c[i,j] = xi[i] - xi[j]
- c = self.xi[:, np.newaxis] - self.xi
- # avoid division by 0 (diagonal entries are so far zero by construction)
- np.fill_diagonal(c, 1)
- # c[i,j] = (w[j] / w[i]) / (xi[i] - xi[j]) (equation 9.4)
- c = self.wi/ (c * self.wi[..., np.newaxis])
- # fill in correct diagonal entries: each column sums to 0
- np.fill_diagonal(c, 0)
- # calculate diagonal
- # c[j,j] = -sum_{i != j} c[i,j] (equation 9.5)
- d = -c.sum(axis=1)
- # c[i,j] = l_j(x_i)
- np.fill_diagonal(c, d)
- self._diff_cij = c
- if self._diff_baryint is None:
- # initialise and cache derivative interpolator and cijs;
- # reuse weights wi (which depend only on interpolation points xi),
- # to avoid unnecessary re-computation
- self._diff_baryint = BarycentricInterpolator(xi=self.xi,
- yi=self._diff_cij @ self.yi,
- wi=self.wi)
- self._diff_baryint._diff_cij = self._diff_cij
- if all_lower:
- # assemble matrix of derivatives from order 0 to order der-1,
- # in the format required by _Interpolator1DWithDerivatives.
- cn = np.zeros((der, len(x), self.r), dtype=self.dtype)
- for d in range(der):
- cn[d, :, :] = self._evaluate_derivatives(x, d+1, all_lower=False)
- return cn
- # recursively evaluate only the derivative requested
- return self._diff_baryint._evaluate_derivatives(x, der-1, all_lower=False)
- def barycentric_interpolate(xi, yi, x, axis=0, *, der=0, rng=None):
- """Convenience function for barycentric interpolation.
- Constructs a polynomial that passes through a given set of points,
- then evaluates the polynomial. For reasons of numerical stability,
- this function does not compute the coefficients of the polynomial.
- This function uses a "barycentric interpolation" method that treats
- the problem as a special case of rational function interpolation.
- This algorithm is quite stable, numerically, but even in a world of
- exact computation, unless the `x` coordinates are chosen very
- carefully - Chebyshev zeros (e.g., cos(i*pi/n)) are a good choice -
- polynomial interpolation itself is a very ill-conditioned process
- due to the Runge phenomenon.
- Parameters
- ----------
- xi : array_like
- 1-D array of x coordinates of the points the polynomial should
- pass through
- yi : array_like
- The y coordinates of the points the polynomial should pass through.
- x : scalar or array_like
- Point or points at which to evaluate the interpolant.
- axis : int, optional
- Axis in the `yi` array corresponding to the x-coordinate values.
- der : int or list or None, optional
- How many derivatives to evaluate, or None for all potentially
- nonzero derivatives (that is, a number equal to the number
- of points), or a list of derivatives to evaluate. This number
- includes the function value as the '0th' derivative.
- rng : `numpy.random.Generator`, optional
- Pseudorandom number generator state. When `rng` is None, a new
- `numpy.random.Generator` is created using entropy from the
- operating system. Types other than `numpy.random.Generator` are
- passed to `numpy.random.default_rng` to instantiate a ``Generator``.
- Returns
- -------
- y : scalar or array_like
- Interpolated values. Shape is determined by replacing
- the interpolation axis in the original array with the shape of `x`.
- See Also
- --------
- BarycentricInterpolator : Barycentric interpolator
- Notes
- -----
- Construction of the interpolation weights is a relatively slow process.
- If you want to call this many times with the same xi (but possibly
- varying yi or x) you should use the class `BarycentricInterpolator`.
- This is what this function uses internally.
- Examples
- --------
- We can interpolate 2D observed data using barycentric interpolation:
- >>> import numpy as np
- >>> import matplotlib.pyplot as plt
- >>> from scipy.interpolate import barycentric_interpolate
- >>> x_observed = np.linspace(0.0, 10.0, 11)
- >>> y_observed = np.sin(x_observed)
- >>> x = np.linspace(min(x_observed), max(x_observed), num=100)
- >>> y = barycentric_interpolate(x_observed, y_observed, x)
- >>> plt.plot(x_observed, y_observed, "o", label="observation")
- >>> plt.plot(x, y, label="barycentric interpolation")
- >>> plt.legend()
- >>> plt.show()
- """
- P = BarycentricInterpolator(xi, yi, axis=axis, rng=rng)
- if der == 0:
- return P(x)
- elif _isscalar(der):
- return P.derivative(x, der=der)
- else:
- return P.derivatives(x, der=np.amax(der)+1)[der]
|