123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750 |
- """
- Utility classes and functions for the polynomial modules.
- This module provides: error and warning objects; a polynomial base class;
- and some routines used in both the `polynomial` and `chebyshev` modules.
- Warning objects
- ---------------
- .. autosummary::
- :toctree: generated/
- RankWarning raised in least-squares fit for rank-deficient matrix.
- Functions
- ---------
- .. autosummary::
- :toctree: generated/
- as_series convert list of array_likes into 1-D arrays of common type.
- trimseq remove trailing zeros.
- trimcoef remove small trailing coefficients.
- getdomain return the domain appropriate for a given set of abscissae.
- mapdomain maps points between domains.
- mapparms parameters of the linear map between domains.
- """
- import operator
- import functools
- import warnings
- import numpy as np
- __all__ = [
- 'RankWarning', 'as_series', 'trimseq',
- 'trimcoef', 'getdomain', 'mapdomain', 'mapparms']
- #
- # Warnings and Exceptions
- #
- class RankWarning(UserWarning):
- """Issued by chebfit when the design matrix is rank deficient."""
- pass
- #
- # Helper functions to convert inputs to 1-D arrays
- #
- def trimseq(seq):
- """Remove small Poly series coefficients.
- Parameters
- ----------
- seq : sequence
- Sequence of Poly series coefficients. This routine fails for
- empty sequences.
- Returns
- -------
- series : sequence
- Subsequence with trailing zeros removed. If the resulting sequence
- would be empty, return the first element. The returned sequence may
- or may not be a view.
- Notes
- -----
- Do not lose the type info if the sequence contains unknown objects.
- """
- if len(seq) == 0:
- return seq
- else:
- for i in range(len(seq) - 1, -1, -1):
- if seq[i] != 0:
- break
- return seq[:i+1]
- def as_series(alist, trim=True):
- """
- Return argument as a list of 1-d arrays.
- The returned list contains array(s) of dtype double, complex double, or
- object. A 1-d argument of shape ``(N,)`` is parsed into ``N`` arrays of
- size one; a 2-d argument of shape ``(M,N)`` is parsed into ``M`` arrays
- of size ``N`` (i.e., is "parsed by row"); and a higher dimensional array
- raises a Value Error if it is not first reshaped into either a 1-d or 2-d
- array.
- Parameters
- ----------
- alist : array_like
- A 1- or 2-d array_like
- trim : boolean, optional
- When True, trailing zeros are removed from the inputs.
- When False, the inputs are passed through intact.
- Returns
- -------
- [a1, a2,...] : list of 1-D arrays
- A copy of the input data as a list of 1-d arrays.
- Raises
- ------
- ValueError
- Raised when `as_series` cannot convert its input to 1-d arrays, or at
- least one of the resulting arrays is empty.
- Examples
- --------
- >>> from numpy.polynomial import polyutils as pu
- >>> a = np.arange(4)
- >>> pu.as_series(a)
- [array([0.]), array([1.]), array([2.]), array([3.])]
- >>> b = np.arange(6).reshape((2,3))
- >>> pu.as_series(b)
- [array([0., 1., 2.]), array([3., 4., 5.])]
- >>> pu.as_series((1, np.arange(3), np.arange(2, dtype=np.float16)))
- [array([1.]), array([0., 1., 2.]), array([0., 1.])]
- >>> pu.as_series([2, [1.1, 0.]])
- [array([2.]), array([1.1])]
- >>> pu.as_series([2, [1.1, 0.]], trim=False)
- [array([2.]), array([1.1, 0. ])]
- """
- arrays = [np.array(a, ndmin=1, copy=False) for a in alist]
- if min([a.size for a in arrays]) == 0:
- raise ValueError("Coefficient array is empty")
- if any(a.ndim != 1 for a in arrays):
- raise ValueError("Coefficient array is not 1-d")
- if trim:
- arrays = [trimseq(a) for a in arrays]
- if any(a.dtype == np.dtype(object) for a in arrays):
- ret = []
- for a in arrays:
- if a.dtype != np.dtype(object):
- tmp = np.empty(len(a), dtype=np.dtype(object))
- tmp[:] = a[:]
- ret.append(tmp)
- else:
- ret.append(a.copy())
- else:
- try:
- dtype = np.common_type(*arrays)
- except Exception as e:
- raise ValueError("Coefficient arrays have no common type") from e
- ret = [np.array(a, copy=True, dtype=dtype) for a in arrays]
- return ret
- def trimcoef(c, tol=0):
- """
- Remove "small" "trailing" coefficients from a polynomial.
- "Small" means "small in absolute value" and is controlled by the
- parameter `tol`; "trailing" means highest order coefficient(s), e.g., in
- ``[0, 1, 1, 0, 0]`` (which represents ``0 + x + x**2 + 0*x**3 + 0*x**4``)
- both the 3-rd and 4-th order coefficients would be "trimmed."
- Parameters
- ----------
- c : array_like
- 1-d array of coefficients, ordered from lowest order to highest.
- tol : number, optional
- Trailing (i.e., highest order) elements with absolute value less
- than or equal to `tol` (default value is zero) are removed.
- Returns
- -------
- trimmed : ndarray
- 1-d array with trailing zeros removed. If the resulting series
- would be empty, a series containing a single zero is returned.
- Raises
- ------
- ValueError
- If `tol` < 0
- See Also
- --------
- trimseq
- Examples
- --------
- >>> from numpy.polynomial import polyutils as pu
- >>> pu.trimcoef((0,0,3,0,5,0,0))
- array([0., 0., 3., 0., 5.])
- >>> pu.trimcoef((0,0,1e-3,0,1e-5,0,0),1e-3) # item == tol is trimmed
- array([0.])
- >>> i = complex(0,1) # works for complex
- >>> pu.trimcoef((3e-4,1e-3*(1-i),5e-4,2e-5*(1+i)), 1e-3)
- array([0.0003+0.j , 0.001 -0.001j])
- """
- if tol < 0:
- raise ValueError("tol must be non-negative")
- [c] = as_series([c])
- [ind] = np.nonzero(np.abs(c) > tol)
- if len(ind) == 0:
- return c[:1]*0
- else:
- return c[:ind[-1] + 1].copy()
- def getdomain(x):
- """
- Return a domain suitable for given abscissae.
- Find a domain suitable for a polynomial or Chebyshev series
- defined at the values supplied.
- Parameters
- ----------
- x : array_like
- 1-d array of abscissae whose domain will be determined.
- Returns
- -------
- domain : ndarray
- 1-d array containing two values. If the inputs are complex, then
- the two returned points are the lower left and upper right corners
- of the smallest rectangle (aligned with the axes) in the complex
- plane containing the points `x`. If the inputs are real, then the
- two points are the ends of the smallest interval containing the
- points `x`.
- See Also
- --------
- mapparms, mapdomain
- Examples
- --------
- >>> from numpy.polynomial import polyutils as pu
- >>> points = np.arange(4)**2 - 5; points
- array([-5, -4, -1, 4])
- >>> pu.getdomain(points)
- array([-5., 4.])
- >>> c = np.exp(complex(0,1)*np.pi*np.arange(12)/6) # unit circle
- >>> pu.getdomain(c)
- array([-1.-1.j, 1.+1.j])
- """
- [x] = as_series([x], trim=False)
- if x.dtype.char in np.typecodes['Complex']:
- rmin, rmax = x.real.min(), x.real.max()
- imin, imax = x.imag.min(), x.imag.max()
- return np.array((complex(rmin, imin), complex(rmax, imax)))
- else:
- return np.array((x.min(), x.max()))
- def mapparms(old, new):
- """
- Linear map parameters between domains.
- Return the parameters of the linear map ``offset + scale*x`` that maps
- `old` to `new` such that ``old[i] -> new[i]``, ``i = 0, 1``.
- Parameters
- ----------
- old, new : array_like
- Domains. Each domain must (successfully) convert to a 1-d array
- containing precisely two values.
- Returns
- -------
- offset, scale : scalars
- The map ``L(x) = offset + scale*x`` maps the first domain to the
- second.
- See Also
- --------
- getdomain, mapdomain
- Notes
- -----
- Also works for complex numbers, and thus can be used to calculate the
- parameters required to map any line in the complex plane to any other
- line therein.
- Examples
- --------
- >>> from numpy.polynomial import polyutils as pu
- >>> pu.mapparms((-1,1),(-1,1))
- (0.0, 1.0)
- >>> pu.mapparms((1,-1),(-1,1))
- (-0.0, -1.0)
- >>> i = complex(0,1)
- >>> pu.mapparms((-i,-1),(1,i))
- ((1+1j), (1-0j))
- """
- oldlen = old[1] - old[0]
- newlen = new[1] - new[0]
- off = (old[1]*new[0] - old[0]*new[1])/oldlen
- scl = newlen/oldlen
- return off, scl
- def mapdomain(x, old, new):
- """
- Apply linear map to input points.
- The linear map ``offset + scale*x`` that maps the domain `old` to
- the domain `new` is applied to the points `x`.
- Parameters
- ----------
- x : array_like
- Points to be mapped. If `x` is a subtype of ndarray the subtype
- will be preserved.
- old, new : array_like
- The two domains that determine the map. Each must (successfully)
- convert to 1-d arrays containing precisely two values.
- Returns
- -------
- x_out : ndarray
- Array of points of the same shape as `x`, after application of the
- linear map between the two domains.
- See Also
- --------
- getdomain, mapparms
- Notes
- -----
- Effectively, this implements:
- .. math ::
- x\\_out = new[0] + m(x - old[0])
- where
- .. math ::
- m = \\frac{new[1]-new[0]}{old[1]-old[0]}
- Examples
- --------
- >>> from numpy.polynomial import polyutils as pu
- >>> old_domain = (-1,1)
- >>> new_domain = (0,2*np.pi)
- >>> x = np.linspace(-1,1,6); x
- array([-1. , -0.6, -0.2, 0.2, 0.6, 1. ])
- >>> x_out = pu.mapdomain(x, old_domain, new_domain); x_out
- array([ 0. , 1.25663706, 2.51327412, 3.76991118, 5.02654825, # may vary
- 6.28318531])
- >>> x - pu.mapdomain(x_out, new_domain, old_domain)
- array([0., 0., 0., 0., 0., 0.])
- Also works for complex numbers (and thus can be used to map any line in
- the complex plane to any other line therein).
- >>> i = complex(0,1)
- >>> old = (-1 - i, 1 + i)
- >>> new = (-1 + i, 1 - i)
- >>> z = np.linspace(old[0], old[1], 6); z
- array([-1. -1.j , -0.6-0.6j, -0.2-0.2j, 0.2+0.2j, 0.6+0.6j, 1. +1.j ])
- >>> new_z = pu.mapdomain(z, old, new); new_z
- array([-1.0+1.j , -0.6+0.6j, -0.2+0.2j, 0.2-0.2j, 0.6-0.6j, 1.0-1.j ]) # may vary
- """
- x = np.asanyarray(x)
- off, scl = mapparms(old, new)
- return off + scl*x
- def _nth_slice(i, ndim):
- sl = [np.newaxis] * ndim
- sl[i] = slice(None)
- return tuple(sl)
- def _vander_nd(vander_fs, points, degrees):
- r"""
- A generalization of the Vandermonde matrix for N dimensions
- The result is built by combining the results of 1d Vandermonde matrices,
- .. math::
- W[i_0, \ldots, i_M, j_0, \ldots, j_N] = \prod_{k=0}^N{V_k(x_k)[i_0, \ldots, i_M, j_k]}
- where
- .. math::
- N &= \texttt{len(points)} = \texttt{len(degrees)} = \texttt{len(vander\_fs)} \\
- M &= \texttt{points[k].ndim} \\
- V_k &= \texttt{vander\_fs[k]} \\
- x_k &= \texttt{points[k]} \\
- 0 \le j_k &\le \texttt{degrees[k]}
- Expanding the one-dimensional :math:`V_k` functions gives:
- .. math::
- W[i_0, \ldots, i_M, j_0, \ldots, j_N] = \prod_{k=0}^N{B_{k, j_k}(x_k[i_0, \ldots, i_M])}
- where :math:`B_{k,m}` is the m'th basis of the polynomial construction used along
- dimension :math:`k`. For a regular polynomial, :math:`B_{k, m}(x) = P_m(x) = x^m`.
- Parameters
- ----------
- vander_fs : Sequence[function(array_like, int) -> ndarray]
- The 1d vander function to use for each axis, such as ``polyvander``
- points : Sequence[array_like]
- Arrays of point coordinates, all of the same shape. The dtypes
- will be converted to either float64 or complex128 depending on
- whether any of the elements are complex. Scalars are converted to
- 1-D arrays.
- This must be the same length as `vander_fs`.
- degrees : Sequence[int]
- The maximum degree (inclusive) to use for each axis.
- This must be the same length as `vander_fs`.
- Returns
- -------
- vander_nd : ndarray
- An array of shape ``points[0].shape + tuple(d + 1 for d in degrees)``.
- """
- n_dims = len(vander_fs)
- if n_dims != len(points):
- raise ValueError(
- f"Expected {n_dims} dimensions of sample points, got {len(points)}")
- if n_dims != len(degrees):
- raise ValueError(
- f"Expected {n_dims} dimensions of degrees, got {len(degrees)}")
- if n_dims == 0:
- raise ValueError("Unable to guess a dtype or shape when no points are given")
- # convert to the same shape and type
- points = tuple(np.array(tuple(points), copy=False) + 0.0)
- # produce the vandermonde matrix for each dimension, placing the last
- # axis of each in an independent trailing axis of the output
- vander_arrays = (
- vander_fs[i](points[i], degrees[i])[(...,) + _nth_slice(i, n_dims)]
- for i in range(n_dims)
- )
- # we checked this wasn't empty already, so no `initial` needed
- return functools.reduce(operator.mul, vander_arrays)
- def _vander_nd_flat(vander_fs, points, degrees):
- """
- Like `_vander_nd`, but flattens the last ``len(degrees)`` axes into a single axis
- Used to implement the public ``<type>vander<n>d`` functions.
- """
- v = _vander_nd(vander_fs, points, degrees)
- return v.reshape(v.shape[:-len(degrees)] + (-1,))
- def _fromroots(line_f, mul_f, roots):
- """
- Helper function used to implement the ``<type>fromroots`` functions.
- Parameters
- ----------
- line_f : function(float, float) -> ndarray
- The ``<type>line`` function, such as ``polyline``
- mul_f : function(array_like, array_like) -> ndarray
- The ``<type>mul`` function, such as ``polymul``
- roots
- See the ``<type>fromroots`` functions for more detail
- """
- if len(roots) == 0:
- return np.ones(1)
- else:
- [roots] = as_series([roots], trim=False)
- roots.sort()
- p = [line_f(-r, 1) for r in roots]
- n = len(p)
- while n > 1:
- m, r = divmod(n, 2)
- tmp = [mul_f(p[i], p[i+m]) for i in range(m)]
- if r:
- tmp[0] = mul_f(tmp[0], p[-1])
- p = tmp
- n = m
- return p[0]
- def _valnd(val_f, c, *args):
- """
- Helper function used to implement the ``<type>val<n>d`` functions.
- Parameters
- ----------
- val_f : function(array_like, array_like, tensor: bool) -> array_like
- The ``<type>val`` function, such as ``polyval``
- c, args
- See the ``<type>val<n>d`` functions for more detail
- """
- args = [np.asanyarray(a) for a in args]
- shape0 = args[0].shape
- if not all((a.shape == shape0 for a in args[1:])):
- if len(args) == 3:
- raise ValueError('x, y, z are incompatible')
- elif len(args) == 2:
- raise ValueError('x, y are incompatible')
- else:
- raise ValueError('ordinates are incompatible')
- it = iter(args)
- x0 = next(it)
- # use tensor on only the first
- c = val_f(x0, c)
- for xi in it:
- c = val_f(xi, c, tensor=False)
- return c
- def _gridnd(val_f, c, *args):
- """
- Helper function used to implement the ``<type>grid<n>d`` functions.
- Parameters
- ----------
- val_f : function(array_like, array_like, tensor: bool) -> array_like
- The ``<type>val`` function, such as ``polyval``
- c, args
- See the ``<type>grid<n>d`` functions for more detail
- """
- for xi in args:
- c = val_f(xi, c)
- return c
- def _div(mul_f, c1, c2):
- """
- Helper function used to implement the ``<type>div`` functions.
- Implementation uses repeated subtraction of c2 multiplied by the nth basis.
- For some polynomial types, a more efficient approach may be possible.
- Parameters
- ----------
- mul_f : function(array_like, array_like) -> array_like
- The ``<type>mul`` function, such as ``polymul``
- c1, c2
- See the ``<type>div`` functions for more detail
- """
- # c1, c2 are trimmed copies
- [c1, c2] = as_series([c1, c2])
- if c2[-1] == 0:
- raise ZeroDivisionError()
- lc1 = len(c1)
- lc2 = len(c2)
- if lc1 < lc2:
- return c1[:1]*0, c1
- elif lc2 == 1:
- return c1/c2[-1], c1[:1]*0
- else:
- quo = np.empty(lc1 - lc2 + 1, dtype=c1.dtype)
- rem = c1
- for i in range(lc1 - lc2, - 1, -1):
- p = mul_f([0]*i + [1], c2)
- q = rem[-1]/p[-1]
- rem = rem[:-1] - q*p[:-1]
- quo[i] = q
- return quo, trimseq(rem)
- def _add(c1, c2):
- """ Helper function used to implement the ``<type>add`` functions. """
- # c1, c2 are trimmed copies
- [c1, c2] = as_series([c1, c2])
- if len(c1) > len(c2):
- c1[:c2.size] += c2
- ret = c1
- else:
- c2[:c1.size] += c1
- ret = c2
- return trimseq(ret)
- def _sub(c1, c2):
- """ Helper function used to implement the ``<type>sub`` functions. """
- # c1, c2 are trimmed copies
- [c1, c2] = as_series([c1, c2])
- if len(c1) > len(c2):
- c1[:c2.size] -= c2
- ret = c1
- else:
- c2 = -c2
- c2[:c1.size] += c1
- ret = c2
- return trimseq(ret)
- def _fit(vander_f, x, y, deg, rcond=None, full=False, w=None):
- """
- Helper function used to implement the ``<type>fit`` functions.
- Parameters
- ----------
- vander_f : function(array_like, int) -> ndarray
- The 1d vander function, such as ``polyvander``
- c1, c2
- See the ``<type>fit`` functions for more detail
- """
- x = np.asarray(x) + 0.0
- y = np.asarray(y) + 0.0
- deg = np.asarray(deg)
- # check arguments.
- if deg.ndim > 1 or deg.dtype.kind not in 'iu' or deg.size == 0:
- raise TypeError("deg must be an int or non-empty 1-D array of int")
- if deg.min() < 0:
- raise ValueError("expected deg >= 0")
- if x.ndim != 1:
- raise TypeError("expected 1D vector for x")
- if x.size == 0:
- raise TypeError("expected non-empty vector for x")
- if y.ndim < 1 or y.ndim > 2:
- raise TypeError("expected 1D or 2D array for y")
- if len(x) != len(y):
- raise TypeError("expected x and y to have same length")
- if deg.ndim == 0:
- lmax = deg
- order = lmax + 1
- van = vander_f(x, lmax)
- else:
- deg = np.sort(deg)
- lmax = deg[-1]
- order = len(deg)
- van = vander_f(x, lmax)[:, deg]
- # set up the least squares matrices in transposed form
- lhs = van.T
- rhs = y.T
- if w is not None:
- w = np.asarray(w) + 0.0
- if w.ndim != 1:
- raise TypeError("expected 1D vector for w")
- if len(x) != len(w):
- raise TypeError("expected x and w to have same length")
- # apply weights. Don't use inplace operations as they
- # can cause problems with NA.
- lhs = lhs * w
- rhs = rhs * w
- # set rcond
- if rcond is None:
- rcond = len(x)*np.finfo(x.dtype).eps
- # Determine the norms of the design matrix columns.
- if issubclass(lhs.dtype.type, np.complexfloating):
- scl = np.sqrt((np.square(lhs.real) + np.square(lhs.imag)).sum(1))
- else:
- scl = np.sqrt(np.square(lhs).sum(1))
- scl[scl == 0] = 1
- # Solve the least squares problem.
- c, resids, rank, s = np.linalg.lstsq(lhs.T/scl, rhs.T, rcond)
- c = (c.T/scl).T
- # Expand c to include non-fitted coefficients which are set to zero
- if deg.ndim > 0:
- if c.ndim == 2:
- cc = np.zeros((lmax+1, c.shape[1]), dtype=c.dtype)
- else:
- cc = np.zeros(lmax+1, dtype=c.dtype)
- cc[deg] = c
- c = cc
- # warn on rank reduction
- if rank != order and not full:
- msg = "The fit may be poorly conditioned"
- warnings.warn(msg, RankWarning, stacklevel=2)
- if full:
- return c, [resids, rank, s, rcond]
- else:
- return c
- def _pow(mul_f, c, pow, maxpower):
- """
- Helper function used to implement the ``<type>pow`` functions.
- Parameters
- ----------
- mul_f : function(array_like, array_like) -> ndarray
- The ``<type>mul`` function, such as ``polymul``
- c : array_like
- 1-D array of array of series coefficients
- pow, maxpower
- See the ``<type>pow`` functions for more detail
- """
- # c is a trimmed copy
- [c] = as_series([c])
- power = int(pow)
- if power != pow or power < 0:
- raise ValueError("Power must be a non-negative integer.")
- elif maxpower is not None and power > maxpower:
- raise ValueError("Power is too large")
- elif power == 0:
- return np.array([1], dtype=c.dtype)
- elif power == 1:
- return c
- else:
- # This can be made more efficient by using powers of two
- # in the usual way.
- prd = c
- for i in range(2, power + 1):
- prd = mul_f(prd, c)
- return prd
- def _deprecate_as_int(x, desc):
- """
- Like `operator.index`, but emits a deprecation warning when passed a float
- Parameters
- ----------
- x : int-like, or float with integral value
- Value to interpret as an integer
- desc : str
- description to include in any error message
- Raises
- ------
- TypeError : if x is a non-integral float or non-numeric
- DeprecationWarning : if x is an integral float
- """
- try:
- return operator.index(x)
- except TypeError as e:
- # Numpy 1.17.0, 2019-03-11
- try:
- ix = int(x)
- except TypeError:
- pass
- else:
- if ix == x:
- warnings.warn(
- f"In future, this will raise TypeError, as {desc} will "
- "need to be an integer not just an integral float.",
- DeprecationWarning,
- stacklevel=3
- )
- return ix
- raise TypeError(f"{desc} must be an integer") from e
|