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