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

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import numpy as np
2from scipy.special import factorial
4from scipy._lib._util import _asarray_validated
7__all__ = ["KroghInterpolator", "krogh_interpolate", "BarycentricInterpolator",
8 "barycentric_interpolate", "approximate_taylor_polynomial"]
11def _isscalar(x):
12 """Check whether x is if a scalar type, or 0-dim"""
13 return np.isscalar(x) or hasattr(x, 'shape') and x.shape == ()
16class _Interpolator1D(object):
17 """
18 Common features in univariate interpolation
20 Deal with input data type and interpolation axis rolling. The
21 actual interpolator can assume the y-data is of shape (n, r) where
22 `n` is the number of x-points, and `r` the number of variables,
23 and use self.dtype as the y-data type.
25 Attributes
26 ----------
27 _y_axis
28 Axis along which the interpolation goes in the original array
29 _y_extra_shape
30 Additional trailing shape of the input arrays, excluding
31 the interpolation axis.
32 dtype
33 Dtype of the y-data arrays. Can be set via _set_dtype, which
34 forces it to be float or complex.
36 Methods
37 -------
38 __call__
39 _prepare_x
40 _finish_y
41 _reshape_yi
42 _set_yi
43 _set_dtype
44 _evaluate
46 """
48 __slots__ = ('_y_axis', '_y_extra_shape', 'dtype')
50 def __init__(self, xi=None, yi=None, axis=None):
51 self._y_axis = axis
52 self._y_extra_shape = None
53 self.dtype = None
54 if yi is not None:
55 self._set_yi(yi, xi=xi, axis=axis)
57 def __call__(self, x):
58 """
59 Evaluate the interpolant
61 Parameters
62 ----------
63 x : array_like
64 Points to evaluate the interpolant at.
66 Returns
67 -------
68 y : array_like
69 Interpolated values. Shape is determined by replacing
70 the interpolation axis in the original array with the shape of x.
72 """
73 x, x_shape = self._prepare_x(x)
74 y = self._evaluate(x)
75 return self._finish_y(y, x_shape)
77 def _evaluate(self, x):
78 """
79 Actually evaluate the value of the interpolator.
80 """
81 raise NotImplementedError()
83 def _prepare_x(self, x):
84 """Reshape input x array to 1-D"""
85 x = _asarray_validated(x, check_finite=False, as_inexact=True)
86 x_shape = x.shape
87 return x.ravel(), x_shape
89 def _finish_y(self, y, x_shape):
90 """Reshape interpolated y back to an N-D array similar to initial y"""
91 y = y.reshape(x_shape + self._y_extra_shape)
92 if self._y_axis != 0 and x_shape != ():
93 nx = len(x_shape)
94 ny = len(self._y_extra_shape)
95 s = (list(range(nx, nx + self._y_axis))
96 + list(range(nx)) + list(range(nx+self._y_axis, nx+ny)))
97 y = y.transpose(s)
98 return y
100 def _reshape_yi(self, yi, check=False):
101 yi = np.rollaxis(np.asarray(yi), self._y_axis)
102 if check and yi.shape[1:] != self._y_extra_shape:
103 ok_shape = "%r + (N,) + %r" % (self._y_extra_shape[-self._y_axis:],
104 self._y_extra_shape[:-self._y_axis])
105 raise ValueError("Data must be of shape %s" % ok_shape)
106 return yi.reshape((yi.shape[0], -1))
108 def _set_yi(self, yi, xi=None, axis=None):
109 if axis is None:
110 axis = self._y_axis
111 if axis is None:
112 raise ValueError("no interpolation axis specified")
114 yi = np.asarray(yi)
116 shape = yi.shape
117 if shape == ():
118 shape = (1,)
119 if xi is not None and shape[axis] != len(xi):
120 raise ValueError("x and y arrays must be equal in length along "
121 "interpolation axis.")
123 self._y_axis = (axis % yi.ndim)
124 self._y_extra_shape = yi.shape[:self._y_axis]+yi.shape[self._y_axis+1:]
125 self.dtype = None
126 self._set_dtype(yi.dtype)
128 def _set_dtype(self, dtype, union=False):
129 if np.issubdtype(dtype, np.complexfloating) \
130 or np.issubdtype(self.dtype, np.complexfloating):
131 self.dtype = np.complex_
132 else:
133 if not union or self.dtype != np.complex_:
134 self.dtype = np.float_
137class _Interpolator1DWithDerivatives(_Interpolator1D):
138 def derivatives(self, x, der=None):
139 """
140 Evaluate many derivatives of the polynomial at the point x
142 Produce an array of all derivative values at the point x.
144 Parameters
145 ----------
146 x : array_like
147 Point or points at which to evaluate the derivatives
148 der : int or None, optional
149 How many derivatives to extract; None for all potentially
150 nonzero derivatives (that is a number equal to the number
151 of points). This number includes the function value as 0th
152 derivative.
154 Returns
155 -------
156 d : ndarray
157 Array with derivatives; d[j] contains the jth derivative.
158 Shape of d[j] is determined by replacing the interpolation
159 axis in the original array with the shape of x.
161 Examples
162 --------
163 >>> from scipy.interpolate import KroghInterpolator
164 >>> KroghInterpolator([0,0,0],[1,2,3]).derivatives(0)
165 array([1.0,2.0,3.0])
166 >>> KroghInterpolator([0,0,0],[1,2,3]).derivatives([0,0])
167 array([[1.0,1.0],
168 [2.0,2.0],
169 [3.0,3.0]])
171 """
172 x, x_shape = self._prepare_x(x)
173 y = self._evaluate_derivatives(x, der)
175 y = y.reshape((y.shape[0],) + x_shape + self._y_extra_shape)
176 if self._y_axis != 0 and x_shape != ():
177 nx = len(x_shape)
178 ny = len(self._y_extra_shape)
179 s = ([0] + list(range(nx+1, nx + self._y_axis+1))
180 + list(range(1, nx+1)) +
181 list(range(nx+1+self._y_axis, nx+ny+1)))
182 y = y.transpose(s)
183 return y
185 def derivative(self, x, der=1):
186 """
187 Evaluate one derivative of the polynomial at the point x
189 Parameters
190 ----------
191 x : array_like
192 Point or points at which to evaluate the derivatives
194 der : integer, optional
195 Which derivative to extract. This number includes the
196 function value as 0th derivative.
198 Returns
199 -------
200 d : ndarray
201 Derivative interpolated at the x-points. Shape of d is
202 determined by replacing the interpolation axis in the
203 original array with the shape of x.
205 Notes
206 -----
207 This is computed by evaluating all derivatives up to the desired
208 one (using self.derivatives()) and then discarding the rest.
210 """
211 x, x_shape = self._prepare_x(x)
212 y = self._evaluate_derivatives(x, der+1)
213 return self._finish_y(y[der], x_shape)
216class KroghInterpolator(_Interpolator1DWithDerivatives):
217 """
218 Interpolating polynomial for a set of points.
220 The polynomial passes through all the pairs (xi,yi). One may
221 additionally specify a number of derivatives at each point xi;
222 this is done by repeating the value xi and specifying the
223 derivatives as successive yi values.
225 Allows evaluation of the polynomial and all its derivatives.
226 For reasons of numerical stability, this function does not compute
227 the coefficients of the polynomial, although they can be obtained
228 by evaluating all the derivatives.
230 Parameters
231 ----------
232 xi : array_like, length N
233 Known x-coordinates. Must be sorted in increasing order.
234 yi : array_like
235 Known y-coordinates. When an xi occurs two or more times in
236 a row, the corresponding yi's represent derivative values.
237 axis : int, optional
238 Axis in the yi array corresponding to the x-coordinate values.
240 Notes
241 -----
242 Be aware that the algorithms implemented here are not necessarily
243 the most numerically stable known. Moreover, even in a world of
244 exact computation, unless the x coordinates are chosen very
245 carefully - Chebyshev zeros (e.g., cos(i*pi/n)) are a good choice -
246 polynomial interpolation itself is a very ill-conditioned process
247 due to the Runge phenomenon. In general, even with well-chosen
248 x values, degrees higher than about thirty cause problems with
249 numerical instability in this code.
251 Based on [1]_.
253 References
254 ----------
255 .. [1] Krogh, "Efficient Algorithms for Polynomial Interpolation
256 and Numerical Differentiation", 1970.
258 Examples
259 --------
260 To produce a polynomial that is zero at 0 and 1 and has
261 derivative 2 at 0, call
263 >>> from scipy.interpolate import KroghInterpolator
264 >>> KroghInterpolator([0,0,1],[0,2,0])
266 This constructs the quadratic 2*X**2-2*X. The derivative condition
267 is indicated by the repeated zero in the xi array; the corresponding
268 yi values are 0, the function value, and 2, the derivative value.
270 For another example, given xi, yi, and a derivative ypi for each
271 point, appropriate arrays can be constructed as:
273 >>> xi = np.linspace(0, 1, 5)
274 >>> yi, ypi = np.random.rand(2, 5)
275 >>> xi_k, yi_k = np.repeat(xi, 2), np.ravel(np.dstack((yi,ypi)))
276 >>> KroghInterpolator(xi_k, yi_k)
278 To produce a vector-valued polynomial, supply a higher-dimensional
279 array for yi:
281 >>> KroghInterpolator([0,1],[[2,3],[4,5]])
283 This constructs a linear polynomial giving (2,3) at 0 and (4,5) at 1.
285 """
287 def __init__(self, xi, yi, axis=0):
288 _Interpolator1DWithDerivatives.__init__(self, xi, yi, axis)
290 self.xi = np.asarray(xi)
291 self.yi = self._reshape_yi(yi)
292 self.n, self.r = self.yi.shape
294 c = np.zeros((self.n+1, self.r), dtype=self.dtype)
295 c[0] = self.yi[0]
296 Vk = np.zeros((self.n, self.r), dtype=self.dtype)
297 for k in range(1, self.n):
298 s = 0
299 while s <= k and xi[k-s] == xi[k]:
300 s += 1
301 s -= 1
302 Vk[0] = self.yi[k]/float(factorial(s))
303 for i in range(k-s):
304 if xi[i] == xi[k]:
305 raise ValueError("Elements if `xi` can't be equal.")
306 if s == 0:
307 Vk[i+1] = (c[i]-Vk[i])/(xi[i]-xi[k])
308 else:
309 Vk[i+1] = (Vk[i+1]-Vk[i])/(xi[i]-xi[k])
310 c[k] = Vk[k-s]
311 self.c = c
313 def _evaluate(self, x):
314 pi = 1
315 p = np.zeros((len(x), self.r), dtype=self.dtype)
316 p += self.c[0,np.newaxis,:]
317 for k in range(1, self.n):
318 w = x - self.xi[k-1]
319 pi = w*pi
320 p += pi[:,np.newaxis] * self.c[k]
321 return p
323 def _evaluate_derivatives(self, x, der=None):
324 n = self.n
325 r = self.r
327 if der is None:
328 der = self.n
329 pi = np.zeros((n, len(x)))
330 w = np.zeros((n, len(x)))
331 pi[0] = 1
332 p = np.zeros((len(x), self.r), dtype=self.dtype)
333 p += self.c[0, np.newaxis, :]
335 for k in range(1, n):
336 w[k-1] = x - self.xi[k-1]
337 pi[k] = w[k-1] * pi[k-1]
338 p += pi[k, :, np.newaxis] * self.c[k]
340 cn = np.zeros((max(der, n+1), len(x), r), dtype=self.dtype)
341 cn[:n+1, :, :] += self.c[:n+1, np.newaxis, :]
342 cn[0] = p
343 for k in range(1, n):
344 for i in range(1, n-k+1):
345 pi[i] = w[k+i-1]*pi[i-1] + pi[i]
346 cn[k] = cn[k] + pi[i, :, np.newaxis]*cn[k+i]
347 cn[k] *= factorial(k)
349 cn[n, :, :] = 0
350 return cn[:der]
353def krogh_interpolate(xi, yi, x, der=0, axis=0):
354 """
355 Convenience function for polynomial interpolation.
357 See `KroghInterpolator` for more details.
359 Parameters
360 ----------
361 xi : array_like
362 Known x-coordinates.
363 yi : array_like
364 Known y-coordinates, of shape ``(xi.size, R)``. Interpreted as
365 vectors of length R, or scalars if R=1.
366 x : array_like
367 Point or points at which to evaluate the derivatives.
368 der : int or list, optional
369 How many derivatives to extract; None for all potentially
370 nonzero derivatives (that is a number equal to the number
371 of points), or a list of derivatives to extract. This number
372 includes the function value as 0th derivative.
373 axis : int, optional
374 Axis in the yi array corresponding to the x-coordinate values.
376 Returns
377 -------
378 d : ndarray
379 If the interpolator's values are R-D then the
380 returned array will be the number of derivatives by N by R.
381 If `x` is a scalar, the middle dimension will be dropped; if
382 the `yi` are scalars then the last dimension will be dropped.
384 See Also
385 --------
386 KroghInterpolator : Krogh interpolator
388 Notes
389 -----
390 Construction of the interpolating polynomial is a relatively expensive
391 process. If you want to evaluate it repeatedly consider using the class
392 KroghInterpolator (which is what this function uses).
394 Examples
395 --------
396 We can interpolate 2D observed data using krogh interpolation:
398 >>> import matplotlib.pyplot as plt
399 >>> from scipy.interpolate import krogh_interpolate
400 >>> x_observed = np.linspace(0.0, 10.0, 11)
401 >>> y_observed = np.sin(x_observed)
402 >>> x = np.linspace(min(x_observed), max(x_observed), num=100)
403 >>> y = krogh_interpolate(x_observed, y_observed, x)
404 >>> plt.plot(x_observed, y_observed, "o", label="observation")
405 >>> plt.plot(x, y, label="krogh interpolation")
406 >>> plt.legend()
407 >>> plt.show()
409 """
410 P = KroghInterpolator(xi, yi, axis=axis)
411 if der == 0:
412 return P(x)
413 elif _isscalar(der):
414 return P.derivative(x,der=der)
415 else:
416 return P.derivatives(x,der=np.amax(der)+1)[der]
419def approximate_taylor_polynomial(f,x,degree,scale,order=None):
420 """
421 Estimate the Taylor polynomial of f at x by polynomial fitting.
423 Parameters
424 ----------
425 f : callable
426 The function whose Taylor polynomial is sought. Should accept
427 a vector of `x` values.
428 x : scalar
429 The point at which the polynomial is to be evaluated.
430 degree : int
431 The degree of the Taylor polynomial
432 scale : scalar
433 The width of the interval to use to evaluate the Taylor polynomial.
434 Function values spread over a range this wide are used to fit the
435 polynomial. Must be chosen carefully.
436 order : int or None, optional
437 The order of the polynomial to be used in the fitting; `f` will be
438 evaluated ``order+1`` times. If None, use `degree`.
440 Returns
441 -------
442 p : poly1d instance
443 The Taylor polynomial (translated to the origin, so that
444 for example p(0)=f(x)).
446 Notes
447 -----
448 The appropriate choice of "scale" is a trade-off; too large and the
449 function differs from its Taylor polynomial too much to get a good
450 answer, too small and round-off errors overwhelm the higher-order terms.
451 The algorithm used becomes numerically unstable around order 30 even
452 under ideal circumstances.
454 Choosing order somewhat larger than degree may improve the higher-order
455 terms.
457 Examples
458 --------
459 We can calculate Taylor approximation polynomials of sin function with
460 various degrees:
462 >>> import matplotlib.pyplot as plt
463 >>> from scipy.interpolate import approximate_taylor_polynomial
464 >>> x = np.linspace(-10.0, 10.0, num=100)
465 >>> plt.plot(x, np.sin(x), label="sin curve")
466 >>> for degree in np.arange(1, 15, step=2):
467 ... sin_taylor = approximate_taylor_polynomial(np.sin, 0, degree, 1,
468 ... order=degree + 2)
469 ... plt.plot(x, sin_taylor(x), label=f"degree={degree}")
470 >>> plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left',
471 ... borderaxespad=0.0, shadow=True)
472 >>> plt.tight_layout()
473 >>> plt.axis([-10, 10, -10, 10])
474 >>> plt.show()
476 """
477 if order is None:
478 order = degree
480 n = order+1
481 # Choose n points that cluster near the endpoints of the interval in
482 # a way that avoids the Runge phenomenon. Ensure, by including the
483 # endpoint or not as appropriate, that one point always falls at x
484 # exactly.
485 xs = scale*np.cos(np.linspace(0,np.pi,n,endpoint=n % 1)) + x
487 P = KroghInterpolator(xs, f(xs))
488 d = P.derivatives(x,der=degree+1)
490 return np.poly1d((d/factorial(np.arange(degree+1)))[::-1])
493class BarycentricInterpolator(_Interpolator1D):
494 """The interpolating polynomial for a set of points
496 Constructs a polynomial that passes through a given set of points.
497 Allows evaluation of the polynomial, efficient changing of the y
498 values to be interpolated, and updating by adding more x values.
499 For reasons of numerical stability, this function does not compute
500 the coefficients of the polynomial.
502 The values yi need to be provided before the function is
503 evaluated, but none of the preprocessing depends on them, so rapid
504 updates are possible.
506 Parameters
507 ----------
508 xi : array_like
509 1-D array of x coordinates of the points the polynomial
510 should pass through
511 yi : array_like, optional
512 The y coordinates of the points the polynomial should pass through.
513 If None, the y values will be supplied later via the `set_y` method.
514 axis : int, optional
515 Axis in the yi array corresponding to the x-coordinate values.
517 Notes
518 -----
519 This class uses a "barycentric interpolation" method that treats
520 the problem as a special case of rational function interpolation.
521 This algorithm is quite stable, numerically, but even in a world of
522 exact computation, unless the x coordinates are chosen very
523 carefully - Chebyshev zeros (e.g., cos(i*pi/n)) are a good choice -
524 polynomial interpolation itself is a very ill-conditioned process
525 due to the Runge phenomenon.
527 Based on Berrut and Trefethen 2004, "Barycentric Lagrange Interpolation".
529 """
530 def __init__(self, xi, yi=None, axis=0):
531 _Interpolator1D.__init__(self, xi, yi, axis)
533 self.xi = np.asfarray(xi)
534 self.set_yi(yi)
535 self.n = len(self.xi)
537 self.wi = np.zeros(self.n)
538 self.wi[0] = 1
539 for j in range(1, self.n):
540 self.wi[:j] *= (self.xi[j]-self.xi[:j])
541 self.wi[j] = np.multiply.reduce(self.xi[:j]-self.xi[j])
542 self.wi **= -1
544 def set_yi(self, yi, axis=None):
545 """
546 Update the y values to be interpolated
548 The barycentric interpolation algorithm requires the calculation
549 of weights, but these depend only on the xi. The yi can be changed
550 at any time.
552 Parameters
553 ----------
554 yi : array_like
555 The y coordinates of the points the polynomial should pass through.
556 If None, the y values will be supplied later.
557 axis : int, optional
558 Axis in the yi array corresponding to the x-coordinate values.
560 """
561 if yi is None:
562 self.yi = None
563 return
564 self._set_yi(yi, xi=self.xi, axis=axis)
565 self.yi = self._reshape_yi(yi)
566 self.n, self.r = self.yi.shape
568 def add_xi(self, xi, yi=None):
569 """
570 Add more x values to the set to be interpolated
572 The barycentric interpolation algorithm allows easy updating by
573 adding more points for the polynomial to pass through.
575 Parameters
576 ----------
577 xi : array_like
578 The x coordinates of the points that the polynomial should pass
579 through.
580 yi : array_like, optional
581 The y coordinates of the points the polynomial should pass through.
582 Should have shape ``(xi.size, R)``; if R > 1 then the polynomial is
583 vector-valued.
584 If `yi` is not given, the y values will be supplied later. `yi` should
585 be given if and only if the interpolator has y values specified.
587 """
588 if yi is not None:
589 if self.yi is None:
590 raise ValueError("No previous yi value to update!")
591 yi = self._reshape_yi(yi, check=True)
592 self.yi = np.vstack((self.yi,yi))
593 else:
594 if self.yi is not None:
595 raise ValueError("No update to yi provided!")
596 old_n = self.n
597 self.xi = np.concatenate((self.xi,xi))
598 self.n = len(self.xi)
599 self.wi **= -1
600 old_wi = self.wi
601 self.wi = np.zeros(self.n)
602 self.wi[:old_n] = old_wi
603 for j in range(old_n, self.n):
604 self.wi[:j] *= (self.xi[j]-self.xi[:j])
605 self.wi[j] = np.multiply.reduce(self.xi[:j]-self.xi[j])
606 self.wi **= -1
608 def __call__(self, x):
609 """Evaluate the interpolating polynomial at the points x
611 Parameters
612 ----------
613 x : array_like
614 Points to evaluate the interpolant at.
616 Returns
617 -------
618 y : array_like
619 Interpolated values. Shape is determined by replacing
620 the interpolation axis in the original array with the shape of x.
622 Notes
623 -----
624 Currently the code computes an outer product between x and the
625 weights, that is, it constructs an intermediate array of size
626 N by len(x), where N is the degree of the polynomial.
627 """
628 return _Interpolator1D.__call__(self, x)
630 def _evaluate(self, x):
631 if x.size == 0:
632 p = np.zeros((0, self.r), dtype=self.dtype)
633 else:
634 c = x[...,np.newaxis]-self.xi
635 z = c == 0
636 c[z] = 1
637 c = self.wi/c
638 p = np.dot(c,self.yi)/np.sum(c,axis=-1)[...,np.newaxis]
639 # Now fix where x==some xi
640 r = np.nonzero(z)
641 if len(r) == 1: # evaluation at a scalar
642 if len(r[0]) > 0: # equals one of the points
643 p = self.yi[r[0][0]]
644 else:
645 p[r[:-1]] = self.yi[r[-1]]
646 return p
649def barycentric_interpolate(xi, yi, x, axis=0):
650 """
651 Convenience function for polynomial interpolation.
653 Constructs a polynomial that passes through a given set of points,
654 then evaluates the polynomial. For reasons of numerical stability,
655 this function does not compute the coefficients of the polynomial.
657 This function uses a "barycentric interpolation" method that treats
658 the problem as a special case of rational function interpolation.
659 This algorithm is quite stable, numerically, but even in a world of
660 exact computation, unless the `x` coordinates are chosen very
661 carefully - Chebyshev zeros (e.g., cos(i*pi/n)) are a good choice -
662 polynomial interpolation itself is a very ill-conditioned process
663 due to the Runge phenomenon.
665 Parameters
666 ----------
667 xi : array_like
668 1-D array of x coordinates of the points the polynomial should
669 pass through
670 yi : array_like
671 The y coordinates of the points the polynomial should pass through.
672 x : scalar or array_like
673 Points to evaluate the interpolator at.
674 axis : int, optional
675 Axis in the yi array corresponding to the x-coordinate values.
677 Returns
678 -------
679 y : scalar or array_like
680 Interpolated values. Shape is determined by replacing
681 the interpolation axis in the original array with the shape of x.
683 See Also
684 --------
685 BarycentricInterpolator : Bary centric interpolator
687 Notes
688 -----
689 Construction of the interpolation weights is a relatively slow process.
690 If you want to call this many times with the same xi (but possibly
691 varying yi or x) you should use the class `BarycentricInterpolator`.
692 This is what this function uses internally.
694 Examples
695 --------
696 We can interpolate 2D observed data using barycentric interpolation:
698 >>> import matplotlib.pyplot as plt
699 >>> from scipy.interpolate import barycentric_interpolate
700 >>> x_observed = np.linspace(0.0, 10.0, 11)
701 >>> y_observed = np.sin(x_observed)
702 >>> x = np.linspace(min(x_observed), max(x_observed), num=100)
703 >>> y = barycentric_interpolate(x_observed, y_observed, x)
704 >>> plt.plot(x_observed, y_observed, "o", label="observation")
705 >>> plt.plot(x, y, label="barycentric interpolation")
706 >>> plt.legend()
707 >>> plt.show()
709 """
710 return BarycentricInterpolator(xi, yi, axis=axis)(x)