_polyint.py 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029
  1. import warnings
  2. from types import GenericAlias
  3. import numpy as np
  4. from scipy.special import factorial
  5. from scipy._lib._util import (_asarray_validated, float_factorial, check_random_state,
  6. _transition_to_rng)
  7. __all__ = ["KroghInterpolator", "krogh_interpolate",
  8. "BarycentricInterpolator", "barycentric_interpolate",
  9. "approximate_taylor_polynomial"]
  10. def _isscalar(x):
  11. """Check whether x is if a scalar type, or 0-dim"""
  12. return np.isscalar(x) or hasattr(x, 'shape') and x.shape == ()
  13. class _Interpolator1D:
  14. """
  15. Common features in univariate interpolation
  16. Deal with input data type and interpolation axis rolling. The
  17. actual interpolator can assume the y-data is of shape (n, r) where
  18. `n` is the number of x-points, and `r` the number of variables,
  19. and use self.dtype as the y-data type.
  20. Attributes
  21. ----------
  22. _y_axis
  23. Axis along which the interpolation goes in the original array
  24. _y_extra_shape
  25. Additional trailing shape of the input arrays, excluding
  26. the interpolation axis.
  27. dtype
  28. Dtype of the y-data arrays. Can be set via _set_dtype, which
  29. forces it to be float or complex.
  30. Methods
  31. -------
  32. __call__
  33. _prepare_x
  34. _finish_y
  35. _reshape_yi
  36. _set_yi
  37. _set_dtype
  38. _evaluate
  39. """
  40. __slots__ = ('_y_axis', '_y_extra_shape', 'dtype')
  41. # generic type compatibility with scipy-stubs
  42. __class_getitem__ = classmethod(GenericAlias)
  43. def __init__(self, xi=None, yi=None, axis=None):
  44. self._y_axis = axis
  45. self._y_extra_shape = None
  46. self.dtype = None
  47. if yi is not None:
  48. self._set_yi(yi, xi=xi, axis=axis)
  49. def __call__(self, x):
  50. """
  51. Evaluate the interpolant
  52. Parameters
  53. ----------
  54. x : array_like
  55. Point or points at which to evaluate the interpolant.
  56. Returns
  57. -------
  58. y : array_like
  59. Interpolated values. Shape is determined by replacing
  60. the interpolation axis in the original array with the shape of `x`.
  61. Notes
  62. -----
  63. Input values `x` must be convertible to `float` values like `int`
  64. or `float`.
  65. """
  66. x, x_shape = self._prepare_x(x)
  67. y = self._evaluate(x)
  68. return self._finish_y(y, x_shape)
  69. def _evaluate(self, x):
  70. """
  71. Actually evaluate the value of the interpolator.
  72. """
  73. raise NotImplementedError()
  74. def _prepare_x(self, x):
  75. """Reshape input x array to 1-D"""
  76. x = _asarray_validated(x, check_finite=False, as_inexact=True)
  77. x_shape = x.shape
  78. return x.ravel(), x_shape
  79. def _finish_y(self, y, x_shape):
  80. """Reshape interpolated y back to an N-D array similar to initial y"""
  81. y = y.reshape(x_shape + self._y_extra_shape)
  82. if self._y_axis != 0 and x_shape != ():
  83. nx = len(x_shape)
  84. ny = len(self._y_extra_shape)
  85. s = (list(range(nx, nx + self._y_axis))
  86. + list(range(nx)) + list(range(nx+self._y_axis, nx+ny)))
  87. y = y.transpose(s)
  88. return y
  89. def _reshape_yi(self, yi, check=False):
  90. yi = np.moveaxis(np.asarray(yi), self._y_axis, 0)
  91. if check and yi.shape[1:] != self._y_extra_shape:
  92. ok_shape = (f"{self._y_extra_shape[-self._y_axis:]!r} + (N,) + "
  93. f"{self._y_extra_shape[:-self._y_axis]!r}")
  94. raise ValueError(f"Data must be of shape {ok_shape}")
  95. return yi.reshape((yi.shape[0], -1))
  96. def _set_yi(self, yi, xi=None, axis=None):
  97. if axis is None:
  98. axis = self._y_axis
  99. if axis is None:
  100. raise ValueError("no interpolation axis specified")
  101. yi = np.asarray(yi)
  102. shape = yi.shape
  103. if shape == ():
  104. shape = (1,)
  105. if xi is not None and shape[axis] != len(xi):
  106. raise ValueError("x and y arrays must be equal in length along "
  107. "interpolation axis.")
  108. self._y_axis = (axis % yi.ndim)
  109. self._y_extra_shape = yi.shape[:self._y_axis] + yi.shape[self._y_axis+1:]
  110. self.dtype = None
  111. self._set_dtype(yi.dtype)
  112. def _set_dtype(self, dtype, union=False):
  113. if np.issubdtype(dtype, np.complexfloating) \
  114. or np.issubdtype(self.dtype, np.complexfloating):
  115. self.dtype = np.complex128
  116. else:
  117. if not union or self.dtype != np.complex128:
  118. self.dtype = np.float64
  119. class _Interpolator1DWithDerivatives(_Interpolator1D):
  120. def derivatives(self, x, der=None):
  121. """
  122. Evaluate several derivatives of the polynomial at the point `x`
  123. Produce an array of derivatives evaluated at the point `x`.
  124. Parameters
  125. ----------
  126. x : array_like
  127. Point or points at which to evaluate the derivatives
  128. der : int or list or None, optional
  129. How many derivatives to evaluate, or None for all potentially
  130. nonzero derivatives (that is, a number equal to the number
  131. of points), or a list of derivatives to evaluate. This number
  132. includes the function value as the '0th' derivative.
  133. Returns
  134. -------
  135. d : ndarray
  136. Array with derivatives; ``d[j]`` contains the jth derivative.
  137. Shape of ``d[j]`` is determined by replacing the interpolation
  138. axis in the original array with the shape of `x`.
  139. Examples
  140. --------
  141. >>> from scipy.interpolate import KroghInterpolator
  142. >>> KroghInterpolator([0,0,0],[1,2,3]).derivatives(0)
  143. array([1.0,2.0,3.0])
  144. >>> KroghInterpolator([0,0,0],[1,2,3]).derivatives([0,0])
  145. array([[1.0,1.0],
  146. [2.0,2.0],
  147. [3.0,3.0]])
  148. """
  149. x, x_shape = self._prepare_x(x)
  150. y = self._evaluate_derivatives(x, der)
  151. y = y.reshape((y.shape[0],) + x_shape + self._y_extra_shape)
  152. if self._y_axis != 0 and x_shape != ():
  153. nx = len(x_shape)
  154. ny = len(self._y_extra_shape)
  155. s = ([0] + list(range(nx+1, nx + self._y_axis+1))
  156. + list(range(1, nx+1)) +
  157. list(range(nx+1+self._y_axis, nx+ny+1)))
  158. y = y.transpose(s)
  159. return y
  160. def derivative(self, x, der=1):
  161. """
  162. Evaluate a single derivative of the polynomial at the point `x`.
  163. Parameters
  164. ----------
  165. x : array_like
  166. Point or points at which to evaluate the derivatives
  167. der : integer, optional
  168. Which derivative to evaluate (default: first derivative).
  169. This number includes the function value as 0th derivative.
  170. Returns
  171. -------
  172. d : ndarray
  173. Derivative interpolated at the x-points. Shape of `d` is
  174. determined by replacing the interpolation axis in the
  175. original array with the shape of `x`.
  176. Notes
  177. -----
  178. This may be computed by evaluating all derivatives up to the desired
  179. one (using self.derivatives()) and then discarding the rest.
  180. """
  181. x, x_shape = self._prepare_x(x)
  182. y = self._evaluate_derivatives(x, der+1)
  183. return self._finish_y(y[der], x_shape)
  184. def _evaluate_derivatives(self, x, der=None):
  185. """
  186. Actually evaluate the derivatives.
  187. Parameters
  188. ----------
  189. x : array_like
  190. 1D array of points at which to evaluate the derivatives
  191. der : integer, optional
  192. The number of derivatives to evaluate, from 'order 0' (der=1)
  193. to order der-1. If omitted, return all possibly-non-zero
  194. derivatives, ie 0 to order n-1.
  195. Returns
  196. -------
  197. d : ndarray
  198. Array of shape ``(der, x.size, self.yi.shape[1])`` containing
  199. the derivatives from 0 to der-1
  200. """
  201. raise NotImplementedError()
  202. class KroghInterpolator(_Interpolator1DWithDerivatives):
  203. """Krogh interpolator (C∞ smooth).
  204. The polynomial passes through all the pairs ``(xi, yi)``. One may
  205. additionally specify a number of derivatives at each point `xi`;
  206. this is done by repeating the value `xi` and specifying the
  207. derivatives as successive `yi` values.
  208. Allows evaluation of the polynomial and all its derivatives.
  209. For reasons of numerical stability, this function does not compute
  210. the coefficients of the polynomial, although they can be obtained
  211. by evaluating all the derivatives.
  212. Parameters
  213. ----------
  214. xi : array_like, shape (npoints, )
  215. Known x-coordinates. Must be sorted in increasing order.
  216. yi : array_like, shape (..., npoints, ...)
  217. Known y-coordinates. When an xi occurs two or more times in
  218. a row, the corresponding yi's represent derivative values. The length of `yi`
  219. along the interpolation axis must be equal to the length of `xi`. Use the
  220. `axis` parameter to select the correct axis.
  221. axis : int, optional
  222. Axis in the `yi` array corresponding to the x-coordinate values. Defaults to
  223. ``axis=0``.
  224. Notes
  225. -----
  226. Be aware that the algorithms implemented here are not necessarily
  227. the most numerically stable known. Moreover, even in a world of
  228. exact computation, unless the x coordinates are chosen very
  229. carefully - Chebyshev zeros (e.g., cos(i*pi/n)) are a good choice -
  230. polynomial interpolation itself is a very ill-conditioned process
  231. due to the Runge phenomenon. In general, even with well-chosen
  232. x values, degrees higher than about thirty cause problems with
  233. numerical instability in this code.
  234. Based on [1]_.
  235. References
  236. ----------
  237. .. [1] Krogh, "Efficient Algorithms for Polynomial Interpolation
  238. and Numerical Differentiation", 1970.
  239. Examples
  240. --------
  241. To produce a polynomial that is zero at 0 and 1 and has
  242. derivative 2 at 0, call
  243. >>> from scipy.interpolate import KroghInterpolator
  244. >>> KroghInterpolator([0,0,1],[0,2,0])
  245. This constructs the quadratic :math:`2x^2-2x`. The derivative condition
  246. is indicated by the repeated zero in the `xi` array; the corresponding
  247. yi values are 0, the function value, and 2, the derivative value.
  248. For another example, given `xi`, `yi`, and a derivative `ypi` for each
  249. point, appropriate arrays can be constructed as:
  250. >>> import numpy as np
  251. >>> rng = np.random.default_rng()
  252. >>> xi = np.linspace(0, 1, 5)
  253. >>> yi, ypi = rng.random((2, 5))
  254. >>> xi_k, yi_k = np.repeat(xi, 2), np.ravel(np.dstack((yi,ypi)))
  255. >>> KroghInterpolator(xi_k, yi_k)
  256. To produce a vector-valued polynomial, supply a higher-dimensional
  257. array for `yi`:
  258. >>> KroghInterpolator([0,1],[[2,3],[4,5]])
  259. This constructs a linear polynomial giving (2,3) at 0 and (4,5) at 1.
  260. """
  261. def __init__(self, xi, yi, axis=0):
  262. super().__init__(xi, yi, axis)
  263. self.xi = np.asarray(xi)
  264. self.yi = self._reshape_yi(yi)
  265. self.n, self.r = self.yi.shape
  266. if (deg := self.xi.size) > 30:
  267. warnings.warn(f"{deg} degrees provided, degrees higher than about"
  268. " thirty cause problems with numerical instability "
  269. "with 'KroghInterpolator'", stacklevel=2)
  270. c = np.zeros((self.n+1, self.r), dtype=self.dtype)
  271. c[0] = self.yi[0]
  272. Vk = np.zeros((self.n, self.r), dtype=self.dtype)
  273. for k in range(1, self.n):
  274. s = 0
  275. while s <= k and xi[k-s] == xi[k]:
  276. s += 1
  277. s -= 1
  278. Vk[0] = self.yi[k]/float_factorial(s)
  279. for i in range(k-s):
  280. if xi[i] == xi[k]:
  281. raise ValueError("Elements of `xi` can't be equal.")
  282. if s == 0:
  283. Vk[i+1] = (c[i]-Vk[i])/(xi[i]-xi[k])
  284. else:
  285. Vk[i+1] = (Vk[i+1]-Vk[i])/(xi[i]-xi[k])
  286. c[k] = Vk[k-s]
  287. self.c = c
  288. def _evaluate(self, x):
  289. pi = 1
  290. p = np.zeros((len(x), self.r), dtype=self.dtype)
  291. p += self.c[0,np.newaxis,:]
  292. for k in range(1, self.n):
  293. w = x - self.xi[k-1]
  294. pi = w*pi
  295. p += pi[:,np.newaxis] * self.c[k]
  296. return p
  297. def _evaluate_derivatives(self, x, der=None):
  298. n = self.n
  299. r = self.r
  300. if der is None:
  301. der = self.n
  302. pi = np.zeros((n, len(x)))
  303. w = np.zeros((n, len(x)))
  304. pi[0] = 1
  305. p = np.zeros((len(x), self.r), dtype=self.dtype)
  306. p += self.c[0, np.newaxis, :]
  307. for k in range(1, n):
  308. w[k-1] = x - self.xi[k-1]
  309. pi[k] = w[k-1] * pi[k-1]
  310. p += pi[k, :, np.newaxis] * self.c[k]
  311. cn = np.zeros((max(der, n+1), len(x), r), dtype=self.dtype)
  312. cn[:n+1, :, :] += self.c[:n+1, np.newaxis, :]
  313. cn[0] = p
  314. for k in range(1, n):
  315. for i in range(1, n-k+1):
  316. pi[i] = w[k+i-1]*pi[i-1] + pi[i]
  317. cn[k] = cn[k] + pi[i, :, np.newaxis]*cn[k+i]
  318. cn[k] *= float_factorial(k)
  319. cn[n, :, :] = 0
  320. return cn[:der]
  321. def krogh_interpolate(xi, yi, x, der=0, axis=0):
  322. """Convenience function for Krogh interpolation.
  323. See `KroghInterpolator` for more details.
  324. Parameters
  325. ----------
  326. xi : array_like
  327. Interpolation points (known x-coordinates).
  328. yi : array_like
  329. Known y-coordinates, of shape ``(xi.size, R)``. Interpreted as
  330. vectors of length R, or scalars if R=1.
  331. x : array_like
  332. Point or points at which to evaluate the derivatives.
  333. der : int or list or None, optional
  334. How many derivatives to evaluate, or None for all potentially
  335. nonzero derivatives (that is, a number equal to the number
  336. of points), or a list of derivatives to evaluate. This number
  337. includes the function value as the '0th' derivative.
  338. axis : int, optional
  339. Axis in the `yi` array corresponding to the x-coordinate values.
  340. Returns
  341. -------
  342. d : ndarray
  343. If the interpolator's values are R-D then the
  344. returned array will be the number of derivatives by N by R.
  345. If `x` is a scalar, the middle dimension will be dropped; if
  346. the `yi` are scalars then the last dimension will be dropped.
  347. See Also
  348. --------
  349. KroghInterpolator : Krogh interpolator
  350. Notes
  351. -----
  352. Construction of the interpolating polynomial is a relatively expensive
  353. process. If you want to evaluate it repeatedly consider using the class
  354. KroghInterpolator (which is what this function uses).
  355. Examples
  356. --------
  357. We can interpolate 2D observed data using Krogh interpolation:
  358. >>> import numpy as np
  359. >>> import matplotlib.pyplot as plt
  360. >>> from scipy.interpolate import krogh_interpolate
  361. >>> x_observed = np.linspace(0.0, 10.0, 11)
  362. >>> y_observed = np.sin(x_observed)
  363. >>> x = np.linspace(min(x_observed), max(x_observed), num=100)
  364. >>> y = krogh_interpolate(x_observed, y_observed, x)
  365. >>> plt.plot(x_observed, y_observed, "o", label="observation")
  366. >>> plt.plot(x, y, label="krogh interpolation")
  367. >>> plt.legend()
  368. >>> plt.show()
  369. """
  370. P = KroghInterpolator(xi, yi, axis=axis)
  371. if der == 0:
  372. return P(x)
  373. elif _isscalar(der):
  374. return P.derivative(x, der=der)
  375. else:
  376. return P.derivatives(x, der=np.amax(der)+1)[der]
  377. def approximate_taylor_polynomial(f,x,degree,scale,order=None):
  378. """
  379. Estimate the Taylor polynomial of f at x by polynomial fitting.
  380. Parameters
  381. ----------
  382. f : callable
  383. The function whose Taylor polynomial is sought. Should accept
  384. a vector of `x` values.
  385. x : scalar
  386. The point at which the polynomial is to be evaluated.
  387. degree : int
  388. The degree of the Taylor polynomial
  389. scale : scalar
  390. The width of the interval to use to evaluate the Taylor polynomial.
  391. Function values spread over a range this wide are used to fit the
  392. polynomial. Must be chosen carefully.
  393. order : int or None, optional
  394. The order of the polynomial to be used in the fitting; `f` will be
  395. evaluated ``order+1`` times. If None, use `degree`.
  396. Returns
  397. -------
  398. p : poly1d instance
  399. The Taylor polynomial (translated to the origin, so that
  400. for example p(0)=f(x)).
  401. Notes
  402. -----
  403. The appropriate choice of "scale" is a trade-off; too large and the
  404. function differs from its Taylor polynomial too much to get a good
  405. answer, too small and round-off errors overwhelm the higher-order terms.
  406. The algorithm used becomes numerically unstable around order 30 even
  407. under ideal circumstances.
  408. Choosing order somewhat larger than degree may improve the higher-order
  409. terms.
  410. Examples
  411. --------
  412. We can calculate Taylor approximation polynomials of sin function with
  413. various degrees:
  414. >>> import numpy as np
  415. >>> import matplotlib.pyplot as plt
  416. >>> from scipy.interpolate import approximate_taylor_polynomial
  417. >>> x = np.linspace(-10.0, 10.0, num=100)
  418. >>> plt.plot(x, np.sin(x), label="sin curve")
  419. >>> for degree in np.arange(1, 15, step=2):
  420. ... sin_taylor = approximate_taylor_polynomial(np.sin, 0, degree, 1,
  421. ... order=degree + 2)
  422. ... plt.plot(x, sin_taylor(x), label=f"degree={degree}")
  423. >>> plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left',
  424. ... borderaxespad=0.0, shadow=True)
  425. >>> plt.tight_layout()
  426. >>> plt.axis([-10, 10, -10, 10])
  427. >>> plt.show()
  428. """
  429. if order is None:
  430. order = degree
  431. n = order+1
  432. # Choose n points that cluster near the endpoints of the interval in
  433. # a way that avoids the Runge phenomenon. Ensure, by including the
  434. # endpoint or not as appropriate, that one point always falls at x
  435. # exactly.
  436. xs = scale*np.cos(np.linspace(0,np.pi,n,endpoint=n % 1)) + x
  437. P = KroghInterpolator(xs, f(xs))
  438. d = P.derivatives(x,der=degree+1)
  439. return np.poly1d((d/factorial(np.arange(degree+1)))[::-1])
  440. class BarycentricInterpolator(_Interpolator1DWithDerivatives):
  441. r"""Barycentric (Lagrange with improved stability) interpolator (C∞ smooth).
  442. Constructs a polynomial that passes through a given set of points.
  443. Allows evaluation of the polynomial and all its derivatives,
  444. efficient changing of the y-values to be interpolated,
  445. and updating by adding more x- and y-values. For numerical stability, a barycentric
  446. representation is used rather than computing the coefficients of the polynomial
  447. directly.
  448. Parameters
  449. ----------
  450. xi : array_like, shape (npoints, )
  451. 1-D array of x-coordinates of the points the polynomial
  452. should pass through
  453. yi : array_like, shape (..., npoints, ...), optional
  454. N-D array of y-coordinates of the points the polynomial should pass through.
  455. If None, the y values will be supplied later via the `set_y` method.
  456. The length of `yi` along the interpolation axis must be equal to the length
  457. of `xi`. Use the ``axis`` parameter to select correct axis.
  458. axis : int, optional
  459. Axis in the yi array corresponding to the x-coordinate values. Defaults
  460. to ``axis=0``.
  461. wi : array_like, optional
  462. The barycentric weights for the chosen interpolation points `xi`.
  463. If absent or None, the weights will be computed from `xi` (default).
  464. This allows for the reuse of the weights `wi` if several interpolants
  465. are being calculated using the same nodes `xi`, without re-computation. This
  466. also allows for computing the weights explicitly for some choices of
  467. `xi` (see notes).
  468. rng : {None, int, `numpy.random.Generator`}, optional
  469. If `rng` is passed by keyword, types other than `numpy.random.Generator` are
  470. passed to `numpy.random.default_rng` to instantiate a ``Generator``.
  471. If `rng` is already a ``Generator`` instance, then the provided instance is
  472. used. Specify `rng` for repeatable interpolation.
  473. If this argument `random_state` is passed by keyword,
  474. legacy behavior for the argument `random_state` applies:
  475. - If `random_state` is None (or `numpy.random`), the `numpy.random.RandomState`
  476. singleton is used.
  477. - If `random_state` is an int, a new ``RandomState`` instance is used,
  478. seeded with `random_state`.
  479. - If `random_state` is already a ``Generator`` or ``RandomState`` instance then
  480. that instance is used.
  481. .. versionchanged:: 1.15.0
  482. As part of the `SPEC-007 <https://scientific-python.org/specs/spec-0007/>`_
  483. transition from use of `numpy.random.RandomState` to
  484. `numpy.random.Generator` this keyword was changed from `random_state` to `rng`.
  485. For an interim period, both keywords will continue to work (only specify
  486. one of them). After the interim period using the `random_state` keyword will emit
  487. warnings. The behavior of the `random_state` and `rng` keywords is outlined above.
  488. Notes
  489. -----
  490. This method is a variant of Lagrange polynomial interpolation [1]_ based on [2]_.
  491. Instead of using Lagrange's or Newton's formula, the polynomial is represented by
  492. the barycentric formula
  493. .. math::
  494. p(x) =
  495. \frac{\sum_{i=1}^m\ w_i y_i / (x - x_i)}{\sum_{i=1}^m w_i / (x - x_i)},
  496. where :math:`w_i` are the barycentric weights computed with the general formula
  497. .. math::
  498. w_i = \left( \prod_{k \neq i} x_i - x_k \right)^{-1}.
  499. This is the same barycentric form used by `AAA` and `FloaterHormannInterpolator`.
  500. However, in contrast, the weights :math:`w_i` are defined such that
  501. :math:`p(x)` is a polynomial rather than a rational function.
  502. The barycentric representation avoids many of the problems associated with
  503. polynomial interpolation caused by floating-point arithmetic. However, it does not
  504. avoid issues that are intrinsic to polynomial interpolation. Namely, if the
  505. x-coordinates are equally spaced, then the weights can be computed explicitly using
  506. the formula from [2]_
  507. .. math::
  508. w_i = (-1)^i {n \choose i},
  509. where :math:`n` is the number of x-coordinates. As noted in [2]_, this means that
  510. for large :math:`n` the weights vary by exponentially large factors, leading to the
  511. Runge phenomenon.
  512. To avoid this ill-conditioning, the x-coordinates should be clustered at the
  513. endpoints of the interval. An excellent choice of points on the interval
  514. :math:`[a,b]` are Chebyshev points of the second kind
  515. .. math::
  516. x_i = \frac{a + b}{2} + \frac{a - b}{2}\cos(i\pi/n).
  517. in which case the weights can be computed explicitly as
  518. .. math::
  519. w_i = \begin{cases}
  520. (-1)^i/2 & i = 0,n \\
  521. (-1)^i & \text{otherwise}
  522. \end{cases}.
  523. See [2]_ for more infomation. Note that for large :math:`n`, computing the weights
  524. explicitly (see examples) will be faster than the generic formula.
  525. References
  526. ----------
  527. .. [1] https://en.wikipedia.org/wiki/Lagrange_polynomial
  528. .. [2] Jean-Paul Berrut and Lloyd N. Trefethen, "Barycentric Lagrange
  529. Interpolation", SIAM Review 2004 46:3, 501-517
  530. :doi:`10.1137/S0036144502417715`
  531. Examples
  532. --------
  533. To produce a quintic barycentric interpolant approximating the function
  534. :math:`\sin x`, and its first four derivatives, using six randomly-spaced
  535. nodes in :math:`(0, \pi/2)`:
  536. >>> import numpy as np
  537. >>> import matplotlib.pyplot as plt
  538. >>> from scipy.interpolate import BarycentricInterpolator
  539. >>> rng = np.random.default_rng()
  540. >>> xi = rng.random(6) * np.pi/2
  541. >>> 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
  542. >>> P = BarycentricInterpolator(xi, f(xi), random_state=rng)
  543. >>> fig, axs = plt.subplots(5, 1, sharex=True, layout='constrained', figsize=(7,10))
  544. >>> x = np.linspace(0, np.pi, 100)
  545. >>> axs[0].plot(x, P(x), 'r:', x, f(x), 'k--', xi, f(xi), 'xk')
  546. >>> axs[1].plot(x, P.derivative(x), 'r:', x, f_d1(x), 'k--', xi, f_d1(xi), 'xk')
  547. >>> axs[2].plot(x, P.derivative(x, 2), 'r:', x, f_d2(x), 'k--', xi, f_d2(xi), 'xk')
  548. >>> axs[3].plot(x, P.derivative(x, 3), 'r:', x, f_d3(x), 'k--', xi, f_d3(xi), 'xk')
  549. >>> axs[4].plot(x, P.derivative(x, 4), 'r:', x, f_d4(x), 'k--', xi, f_d4(xi), 'xk')
  550. >>> axs[0].set_xlim(0, np.pi)
  551. >>> axs[4].set_xlabel(r"$x$")
  552. >>> axs[4].set_xticks([i * np.pi / 4 for i in range(5)],
  553. ... ["0", r"$\frac{\pi}{4}$", r"$\frac{\pi}{2}$", r"$\frac{3\pi}{4}$", r"$\pi$"])
  554. >>> for ax, label in zip(axs, ("$f(x)$", "$f'(x)$", "$f''(x)$", "$f^{(3)}(x)$", "$f^{(4)}(x)$")):
  555. ... ax.set_ylabel(label)
  556. >>> labels = ['Interpolation nodes', 'True function $f$', 'Barycentric interpolation']
  557. >>> axs[0].legend(axs[0].get_lines()[::-1], labels, bbox_to_anchor=(0., 1.02, 1., .102),
  558. ... loc='lower left', ncols=3, mode="expand", borderaxespad=0., frameon=False)
  559. >>> plt.show()
  560. Next, we show how using Chebyshev points of the second kind avoids the
  561. Runge phenomenon. In this example, we also compute the weights explicitly.
  562. >>> n = 20
  563. >>> def f(x): return np.abs(x) + 0.5*x - x**2
  564. >>> i = np.arange(n)
  565. >>> x_cheb = np.cos(i*np.pi/(n - 1)) # Chebyshev points on [-1, 1]
  566. >>> w_i_cheb = (-1.)**i # Explicit formula for weights of Chebyshev points
  567. >>> w_i_cheb[[0, -1]] /= 2
  568. >>> p_cheb = BarycentricInterpolator(x_cheb, f(x_cheb), wi=w_i_cheb)
  569. >>> x_equi = np.linspace(-1, 1, n)
  570. >>> p_equi = BarycentricInterpolator(x_equi, f(x_equi))
  571. >>> xx = np.linspace(-1, 1, 1000)
  572. >>> fig, ax = plt.subplots()
  573. >>> ax.plot(xx, f(xx), label="Original Function")
  574. >>> ax.plot(xx, p_cheb(xx), "--", label="Chebshev Points")
  575. >>> ax.plot(xx, p_equi(xx), "--", label="Equally Spaced Points")
  576. >>> ax.set(xlabel="$x$", ylabel="$f(x)$", xlim=[-1, 1])
  577. >>> ax.legend()
  578. >>> plt.show()
  579. """ # numpy/numpydoc#87 # noqa: E501
  580. @_transition_to_rng("random_state", replace_doc=False)
  581. def __init__(self, xi, yi=None, axis=0, *, wi=None, rng=None):
  582. super().__init__(xi, yi, axis)
  583. rng = check_random_state(rng)
  584. self.xi = np.asarray(xi, dtype=np.float64)
  585. self.set_yi(yi)
  586. self.n = len(self.xi)
  587. # cache derivative object to avoid re-computing the weights with every call.
  588. self._diff_cij = None
  589. if wi is not None:
  590. self.wi = wi
  591. else:
  592. # See page 510 of Berrut and Trefethen 2004 for an explanation of the
  593. # capacity scaling and the suggestion of using a random permutation of
  594. # the input factors.
  595. # At the moment, the permutation is not performed for xi that are
  596. # appended later through the add_xi interface. It's not clear to me how
  597. # to implement that and it seems that most situations that require
  598. # these numerical stability improvements will be able to provide all
  599. # the points to the constructor.
  600. self._inv_capacity = 4.0 / (np.max(self.xi) - np.min(self.xi))
  601. permute = rng.permutation(self.n, )
  602. inv_permute = np.zeros(self.n, dtype=np.int32)
  603. inv_permute[permute] = np.arange(self.n)
  604. self.wi = np.zeros(self.n)
  605. for i in range(self.n):
  606. dist = self._inv_capacity * (self.xi[i] - self.xi[permute])
  607. dist[inv_permute[i]] = 1.0
  608. prod = np.prod(dist)
  609. if prod == 0.0:
  610. raise ValueError("Interpolation points xi must be"
  611. " distinct.")
  612. self.wi[i] = 1.0 / prod
  613. def set_yi(self, yi, axis=None):
  614. """
  615. Update the y values to be interpolated
  616. The barycentric interpolation algorithm requires the calculation
  617. of weights, but these depend only on the `xi`. The `yi` can be changed
  618. at any time.
  619. Parameters
  620. ----------
  621. yi : array_like
  622. The y-coordinates of the points the polynomial will pass through.
  623. If None, the y values must be supplied later.
  624. axis : int, optional
  625. Axis in the `yi` array corresponding to the x-coordinate values.
  626. """
  627. if yi is None:
  628. self.yi = None
  629. return
  630. self._set_yi(yi, xi=self.xi, axis=axis)
  631. self.yi = self._reshape_yi(yi)
  632. self.n, self.r = self.yi.shape
  633. self._diff_baryint = None
  634. def add_xi(self, xi, yi=None):
  635. """
  636. Add more x values to the set to be interpolated
  637. The barycentric interpolation algorithm allows easy updating by
  638. adding more points for the polynomial to pass through.
  639. Parameters
  640. ----------
  641. xi : array_like
  642. The x coordinates of the points that the polynomial should pass
  643. through.
  644. yi : array_like, optional
  645. The y coordinates of the points the polynomial should pass through.
  646. Should have shape ``(xi.size, R)``; if R > 1 then the polynomial is
  647. vector-valued.
  648. If `yi` is not given, the y values will be supplied later. `yi`
  649. should be given if and only if the interpolator has y values
  650. specified.
  651. Notes
  652. -----
  653. The new points added by `add_xi` are not randomly permuted
  654. so there is potential for numerical instability,
  655. especially for a large number of points. If this
  656. happens, please reconstruct interpolation from scratch instead.
  657. """
  658. if yi is not None:
  659. if self.yi is None:
  660. raise ValueError("No previous yi value to update!")
  661. yi = self._reshape_yi(yi, check=True)
  662. self.yi = np.vstack((self.yi,yi))
  663. else:
  664. if self.yi is not None:
  665. raise ValueError("No update to yi provided!")
  666. old_n = self.n
  667. self.xi = np.concatenate((self.xi,xi))
  668. self.n = len(self.xi)
  669. self.wi **= -1
  670. old_wi = self.wi
  671. self.wi = np.zeros(self.n)
  672. self.wi[:old_n] = old_wi
  673. for j in range(old_n, self.n):
  674. self.wi[:j] *= self._inv_capacity * (self.xi[j]-self.xi[:j])
  675. self.wi[j] = np.multiply.reduce(
  676. self._inv_capacity * (self.xi[:j]-self.xi[j])
  677. )
  678. self.wi **= -1
  679. self._diff_cij = None
  680. self._diff_baryint = None
  681. def __call__(self, x):
  682. """Evaluate the interpolating polynomial at the points x
  683. Parameters
  684. ----------
  685. x : array_like
  686. Point or points at which to evaluate the interpolant.
  687. Returns
  688. -------
  689. y : array_like
  690. Interpolated values. Shape is determined by replacing
  691. the interpolation axis in the original array with the shape of `x`.
  692. Notes
  693. -----
  694. Currently the code computes an outer product between `x` and the
  695. weights, that is, it constructs an intermediate array of size
  696. ``(N, len(x))``, where N is the degree of the polynomial.
  697. """
  698. return _Interpolator1D.__call__(self, x)
  699. def _evaluate(self, x):
  700. if x.size == 0:
  701. p = np.zeros((0, self.r), dtype=self.dtype)
  702. else:
  703. c = x[..., np.newaxis] - self.xi
  704. z = c == 0
  705. c[z] = 1
  706. c = self.wi / c
  707. with np.errstate(divide='ignore'):
  708. p = np.dot(c, self.yi) / np.sum(c, axis=-1)[..., np.newaxis]
  709. # Now fix where x==some xi
  710. r = np.nonzero(z)
  711. if len(r) == 1: # evaluation at a scalar
  712. if len(r[0]) > 0: # equals one of the points
  713. p = self.yi[r[0][0]]
  714. else:
  715. p[r[:-1]] = self.yi[r[-1]]
  716. return p
  717. def derivative(self, x, der=1):
  718. """
  719. Evaluate a single derivative of the polynomial at the point x.
  720. Parameters
  721. ----------
  722. x : array_like
  723. Point or points at which to evaluate the derivatives
  724. der : integer, optional
  725. Which derivative to evaluate (default: first derivative).
  726. This number includes the function value as 0th derivative.
  727. Returns
  728. -------
  729. d : ndarray
  730. Derivative interpolated at the x-points. Shape of `d` is
  731. determined by replacing the interpolation axis in the
  732. original array with the shape of `x`.
  733. """
  734. x, x_shape = self._prepare_x(x)
  735. y = self._evaluate_derivatives(x, der+1, all_lower=False)
  736. return self._finish_y(y, x_shape)
  737. def _evaluate_derivatives(self, x, der=None, all_lower=True):
  738. # NB: der here is not the order of the highest derivative;
  739. # instead, it is the size of the derivatives matrix that
  740. # would be returned with all_lower=True, including the
  741. # '0th' derivative (the undifferentiated function).
  742. # E.g. to evaluate the 5th derivative alone, call
  743. # _evaluate_derivatives(x, der=6, all_lower=False).
  744. if (not all_lower) and (x.size == 0 or self.r == 0):
  745. return np.zeros((0, self.r), dtype=self.dtype)
  746. if (not all_lower) and der == 1:
  747. return self._evaluate(x)
  748. if (not all_lower) and (der > self.n):
  749. return np.zeros((len(x), self.r), dtype=self.dtype)
  750. if der is None:
  751. der = self.n
  752. if all_lower and (x.size == 0 or self.r == 0):
  753. return np.zeros((der, len(x), self.r), dtype=self.dtype)
  754. if self._diff_cij is None:
  755. # c[i,j] = xi[i] - xi[j]
  756. c = self.xi[:, np.newaxis] - self.xi
  757. # avoid division by 0 (diagonal entries are so far zero by construction)
  758. np.fill_diagonal(c, 1)
  759. # c[i,j] = (w[j] / w[i]) / (xi[i] - xi[j]) (equation 9.4)
  760. c = self.wi/ (c * self.wi[..., np.newaxis])
  761. # fill in correct diagonal entries: each column sums to 0
  762. np.fill_diagonal(c, 0)
  763. # calculate diagonal
  764. # c[j,j] = -sum_{i != j} c[i,j] (equation 9.5)
  765. d = -c.sum(axis=1)
  766. # c[i,j] = l_j(x_i)
  767. np.fill_diagonal(c, d)
  768. self._diff_cij = c
  769. if self._diff_baryint is None:
  770. # initialise and cache derivative interpolator and cijs;
  771. # reuse weights wi (which depend only on interpolation points xi),
  772. # to avoid unnecessary re-computation
  773. self._diff_baryint = BarycentricInterpolator(xi=self.xi,
  774. yi=self._diff_cij @ self.yi,
  775. wi=self.wi)
  776. self._diff_baryint._diff_cij = self._diff_cij
  777. if all_lower:
  778. # assemble matrix of derivatives from order 0 to order der-1,
  779. # in the format required by _Interpolator1DWithDerivatives.
  780. cn = np.zeros((der, len(x), self.r), dtype=self.dtype)
  781. for d in range(der):
  782. cn[d, :, :] = self._evaluate_derivatives(x, d+1, all_lower=False)
  783. return cn
  784. # recursively evaluate only the derivative requested
  785. return self._diff_baryint._evaluate_derivatives(x, der-1, all_lower=False)
  786. def barycentric_interpolate(xi, yi, x, axis=0, *, der=0, rng=None):
  787. """Convenience function for barycentric interpolation.
  788. Constructs a polynomial that passes through a given set of points,
  789. then evaluates the polynomial. For reasons of numerical stability,
  790. this function does not compute the coefficients of the polynomial.
  791. This function uses a "barycentric interpolation" method that treats
  792. the problem as a special case of rational function interpolation.
  793. This algorithm is quite stable, numerically, but even in a world of
  794. exact computation, unless the `x` coordinates are chosen very
  795. carefully - Chebyshev zeros (e.g., cos(i*pi/n)) are a good choice -
  796. polynomial interpolation itself is a very ill-conditioned process
  797. due to the Runge phenomenon.
  798. Parameters
  799. ----------
  800. xi : array_like
  801. 1-D array of x coordinates of the points the polynomial should
  802. pass through
  803. yi : array_like
  804. The y coordinates of the points the polynomial should pass through.
  805. x : scalar or array_like
  806. Point or points at which to evaluate the interpolant.
  807. axis : int, optional
  808. Axis in the `yi` array corresponding to the x-coordinate values.
  809. der : int or list or None, optional
  810. How many derivatives to evaluate, or None for all potentially
  811. nonzero derivatives (that is, a number equal to the number
  812. of points), or a list of derivatives to evaluate. This number
  813. includes the function value as the '0th' derivative.
  814. rng : `numpy.random.Generator`, optional
  815. Pseudorandom number generator state. When `rng` is None, a new
  816. `numpy.random.Generator` is created using entropy from the
  817. operating system. Types other than `numpy.random.Generator` are
  818. passed to `numpy.random.default_rng` to instantiate a ``Generator``.
  819. Returns
  820. -------
  821. y : scalar or array_like
  822. Interpolated values. Shape is determined by replacing
  823. the interpolation axis in the original array with the shape of `x`.
  824. See Also
  825. --------
  826. BarycentricInterpolator : Barycentric interpolator
  827. Notes
  828. -----
  829. Construction of the interpolation weights is a relatively slow process.
  830. If you want to call this many times with the same xi (but possibly
  831. varying yi or x) you should use the class `BarycentricInterpolator`.
  832. This is what this function uses internally.
  833. Examples
  834. --------
  835. We can interpolate 2D observed data using barycentric interpolation:
  836. >>> import numpy as np
  837. >>> import matplotlib.pyplot as plt
  838. >>> from scipy.interpolate import barycentric_interpolate
  839. >>> x_observed = np.linspace(0.0, 10.0, 11)
  840. >>> y_observed = np.sin(x_observed)
  841. >>> x = np.linspace(min(x_observed), max(x_observed), num=100)
  842. >>> y = barycentric_interpolate(x_observed, y_observed, x)
  843. >>> plt.plot(x_observed, y_observed, "o", label="observation")
  844. >>> plt.plot(x, y, label="barycentric interpolation")
  845. >>> plt.legend()
  846. >>> plt.show()
  847. """
  848. P = BarycentricInterpolator(xi, yi, axis=axis, rng=rng)
  849. if der == 0:
  850. return P(x)
  851. elif _isscalar(der):
  852. return P.derivative(x, der=der)
  853. else:
  854. return P.derivatives(x, der=np.amax(der)+1)[der]