summaryrefslogtreecommitdiff
path: root/numpy/lib/polynomial.py
diff options
context:
space:
mode:
Diffstat (limited to 'numpy/lib/polynomial.py')
-rw-r--r--numpy/lib/polynomial.py657
1 files changed, 657 insertions, 0 deletions
diff --git a/numpy/lib/polynomial.py b/numpy/lib/polynomial.py
new file mode 100644
index 000000000..b35926900
--- /dev/null
+++ b/numpy/lib/polynomial.py
@@ -0,0 +1,657 @@
+"""
+Functions to operate on polynomials.
+"""
+
+__all__ = ['poly', 'roots', 'polyint', 'polyder', 'polyadd',
+ 'polysub', 'polymul', 'polydiv', 'polyval', 'poly1d',
+ 'polyfit', 'RankWarning']
+
+import re
+import warnings
+import numpy.core.numeric as NX
+
+from numpy.core import isscalar, abs
+from numpy.lib.getlimits import finfo
+from numpy.lib.twodim_base import diag, vander
+from numpy.lib.shape_base import hstack, atleast_1d
+from numpy.lib.function_base import trim_zeros, sort_complex
+eigvals = None
+lstsq = None
+_single_eps = finfo(NX.single).eps
+_double_eps = finfo(NX.double).eps
+
+class RankWarning(UserWarning):
+ """Issued by polyfit when Vandermonde matrix is rank deficient.
+ """
+ pass
+
+def get_linalg_funcs():
+ "Look for linear algebra functions in numpy"
+ global eigvals, lstsq
+ from numpy.dual import eigvals, lstsq
+ return
+
+def _eigvals(arg):
+ "Return the eigenvalues of the argument"
+ try:
+ return eigvals(arg)
+ except TypeError:
+ get_linalg_funcs()
+ return eigvals(arg)
+
+def _lstsq(X, y, rcond):
+ "Do least squares on the arguments"
+ try:
+ return lstsq(X, y, rcond)
+ except TypeError:
+ get_linalg_funcs()
+ return lstsq(X, y, rcond)
+
+def poly(seq_of_zeros):
+ """ Return a sequence representing a polynomial given a sequence of roots.
+
+ If the input is a matrix, return the characteristic polynomial.
+
+ Example:
+
+ >>> b = roots([1,3,1,5,6])
+ >>> poly(b)
+ array([ 1., 3., 1., 5., 6.])
+
+ """
+ seq_of_zeros = atleast_1d(seq_of_zeros)
+ sh = seq_of_zeros.shape
+ if len(sh) == 2 and sh[0] == sh[1]:
+ seq_of_zeros = _eigvals(seq_of_zeros)
+ elif len(sh) ==1:
+ pass
+ else:
+ raise ValueError, "input must be 1d or square 2d array."
+
+ if len(seq_of_zeros) == 0:
+ return 1.0
+
+ a = [1]
+ for k in range(len(seq_of_zeros)):
+ a = NX.convolve(a, [1, -seq_of_zeros[k]], mode='full')
+
+ if issubclass(a.dtype.type, NX.complexfloating):
+ # if complex roots are all complex conjugates, the roots are real.
+ roots = NX.asarray(seq_of_zeros, complex)
+ pos_roots = sort_complex(NX.compress(roots.imag > 0, roots))
+ neg_roots = NX.conjugate(sort_complex(
+ NX.compress(roots.imag < 0,roots)))
+ if (len(pos_roots) == len(neg_roots) and
+ NX.alltrue(neg_roots == pos_roots)):
+ a = a.real.copy()
+
+ return a
+
+def roots(p):
+ """ Return the roots of the polynomial coefficients in p.
+
+ The values in the rank-1 array p are coefficients of a polynomial.
+ If the length of p is n+1 then the polynomial is
+ p[0] * x**n + p[1] * x**(n-1) + ... + p[n-1]*x + p[n]
+ """
+ # If input is scalar, this makes it an array
+ p = atleast_1d(p)
+ if len(p.shape) != 1:
+ raise ValueError,"Input must be a rank-1 array."
+
+ # find non-zero array entries
+ non_zero = NX.nonzero(NX.ravel(p))[0]
+
+ # Return an empty array if polynomial is all zeros
+ if len(non_zero) == 0:
+ return NX.array([])
+
+ # find the number of trailing zeros -- this is the number of roots at 0.
+ trailing_zeros = len(p) - non_zero[-1] - 1
+
+ # strip leading and trailing zeros
+ p = p[int(non_zero[0]):int(non_zero[-1])+1]
+
+ # casting: if incoming array isn't floating point, make it floating point.
+ if not issubclass(p.dtype.type, (NX.floating, NX.complexfloating)):
+ p = p.astype(float)
+
+ N = len(p)
+ if N > 1:
+ # build companion matrix and find its eigenvalues (the roots)
+ A = diag(NX.ones((N-2,), p.dtype), -1)
+ A[0, :] = -p[1:] / p[0]
+ roots = _eigvals(A)
+ else:
+ roots = NX.array([])
+
+ # tack any zeros onto the back of the array
+ roots = hstack((roots, NX.zeros(trailing_zeros, roots.dtype)))
+ return roots
+
+def polyint(p, m=1, k=None):
+ """Return the mth analytical integral of the polynomial p.
+
+ If k is None, then zero-valued constants of integration are used.
+ otherwise, k should be a list of length m (or a scalar if m=1) to
+ represent the constants of integration to use for each integration
+ (starting with k[0])
+ """
+ m = int(m)
+ if m < 0:
+ raise ValueError, "Order of integral must be positive (see polyder)"
+ if k is None:
+ k = NX.zeros(m, float)
+ k = atleast_1d(k)
+ if len(k) == 1 and m > 1:
+ k = k[0]*NX.ones(m, float)
+ if len(k) < m:
+ raise ValueError, \
+ "k must be a scalar or a rank-1 array of length 1 or >m."
+ if m == 0:
+ return p
+ else:
+ truepoly = isinstance(p, poly1d)
+ p = NX.asarray(p)
+ y = NX.zeros(len(p)+1, float)
+ y[:-1] = p*1.0/NX.arange(len(p), 0, -1)
+ y[-1] = k[0]
+ val = polyint(y, m-1, k=k[1:])
+ if truepoly:
+ val = poly1d(val)
+ return val
+
+def polyder(p, m=1):
+ """Return the mth derivative of the polynomial p.
+ """
+ m = int(m)
+ truepoly = isinstance(p, poly1d)
+ p = NX.asarray(p)
+ n = len(p)-1
+ y = p[:-1] * NX.arange(n, 0, -1)
+ if m < 0:
+ raise ValueError, "Order of derivative must be positive (see polyint)"
+ if m == 0:
+ return p
+ else:
+ val = polyder(y, m-1)
+ if truepoly:
+ val = poly1d(val)
+ return val
+
+def polyfit(x, y, deg, rcond=None, full=False):
+ """Least squares polynomial fit.
+
+ Required arguments
+
+ x -- vector of sample points
+ y -- vector or 2D array of values to fit
+ deg -- degree of the fitting polynomial
+
+ Keyword arguments
+
+ rcond -- relative condition number of the fit (default len(x)*eps)
+ full -- return full diagnostic output (default False)
+
+ Returns
+
+ full == False -- coefficients
+ full == True -- coefficients, residuals, rank, singular values, rcond.
+
+ Warns
+
+ RankWarning -- if rank is reduced and not full output
+
+ Do a best fit polynomial of degree 'deg' of 'x' to 'y'. Return value is a
+ vector of polynomial coefficients [pk ... p1 p0]. Eg, for n=2
+
+ p2*x0^2 + p1*x0 + p0 = y1
+ p2*x1^2 + p1*x1 + p0 = y1
+ p2*x2^2 + p1*x2 + p0 = y2
+ .....
+ p2*xk^2 + p1*xk + p0 = yk
+
+
+ Method: if X is a the Vandermonde Matrix computed from x (see
+ http://mathworld.wolfram.com/VandermondeMatrix.html), then the
+ polynomial least squares solution is given by the 'p' in
+
+ X*p = y
+
+ where X is a len(x) x N+1 matrix, p is a N+1 length vector, and y
+ is a len(x) x 1 vector
+
+ This equation can be solved as
+
+ p = (XT*X)^-1 * XT * y
+
+ where XT is the transpose of X and -1 denotes the inverse. However, this
+ method is susceptible to rounding errors and generally the singular value
+ decomposition is preferred and that is the method used here. The singular
+ value method takes a paramenter, 'rcond', which sets a limit on the
+ relative size of the smallest singular value to be used in solving the
+ equation. This may result in lowering the rank of the Vandermonde matrix,
+ in which case a RankWarning is issued. If polyfit issues a RankWarning, try
+ a fit of lower degree or replace x by x - x.mean(), both of which will
+ generally improve the condition number. The routine already normalizes the
+ vector x by its maximum absolute value to help in this regard. The rcond
+ parameter may also be set to a value smaller than its default, but this may
+ result in bad fits. The current default value of rcond is len(x)*eps, where
+ eps is the relative precision of the floating type being used, generally
+ around 1e-7 and 2e-16 for IEEE single and double precision respectively.
+ This value of rcond is fairly conservative but works pretty well when x -
+ x.mean() is used in place of x.
+
+ The warnings can be turned off by:
+
+ >>> import numpy
+ >>> import warnings
+ >>> warnings.simplefilter('ignore',numpy.RankWarning)
+
+ DISCLAIMER: Power series fits are full of pitfalls for the unwary once the
+ degree of the fit becomes large or the interval of sample points is badly
+ centered. The basic problem is that the powers x**n are generally a poor
+ basis for the functions on the sample interval with the result that the
+ Vandermonde matrix is ill conditioned and computation of the polynomial
+ values is sensitive to coefficient error. The quality of the resulting fit
+ should be checked against the data whenever the condition number is large,
+ as the quality of polynomial fits *can not* be taken for granted. If all
+ you want to do is draw a smooth curve through the y values and polyfit is
+ not doing the job, try centering the sample range or look into
+ scipy.interpolate, which includes some nice spline fitting functions that
+ may be of use.
+
+ For more info, see
+ http://mathworld.wolfram.com/LeastSquaresFittingPolynomial.html,
+ but note that the k's and n's in the superscripts and subscripts
+ on that page. The linear algebra is correct, however.
+
+ See also polyval
+
+ """
+ order = int(deg) + 1
+ x = NX.asarray(x) + 0.0
+ y = NX.asarray(y) + 0.0
+
+ # check arguments.
+ if deg < 0 :
+ raise ValueError, "expected deg >= 0"
+ if x.ndim != 1 or 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 x.shape[0] != y.shape[0] :
+ raise TypeError, "expected x and y to have same length"
+
+ # set rcond
+ if rcond is None :
+ xtype = x.dtype
+ if xtype == NX.single or xtype == NX.csingle :
+ rcond = len(x)*_single_eps
+ else :
+ rcond = len(x)*_double_eps
+
+ # scale x to improve condition number
+ scale = abs(x).max()
+ if scale != 0 :
+ x /= scale
+
+ # solve least squares equation for powers of x
+ v = vander(x, order)
+ c, resids, rank, s = _lstsq(v, y, rcond)
+
+ # warn on rank reduction, which indicates an ill conditioned matrix
+ if rank != order and not full:
+ msg = "Polyfit may be poorly conditioned"
+ warnings.warn(msg, RankWarning)
+
+ # scale returned coefficients
+ if scale != 0 :
+ c /= vander([scale], order)[0]
+
+ if full :
+ return c, resids, rank, s, rcond
+ else :
+ return c
+
+
+
+def polyval(p, x):
+ """Evaluate the polynomial p at x. If x is a polynomial then composition.
+
+ Description:
+
+ If p is of length N, this function returns the value:
+ p[0]*(x**N-1) + p[1]*(x**N-2) + ... + p[N-2]*x + p[N-1]
+
+ x can be a sequence and p(x) will be returned for all elements of x.
+ or x can be another polynomial and the composite polynomial p(x) will be
+ returned.
+
+ Notice: This can produce inaccurate results for polynomials with
+ significant variability. Use carefully.
+ """
+ p = NX.asarray(p)
+ if isinstance(x, poly1d):
+ y = 0
+ else:
+ x = NX.asarray(x)
+ y = NX.zeros_like(x)
+ for i in range(len(p)):
+ y = x * y + p[i]
+ return y
+
+def polyadd(a1, a2):
+ """Adds two polynomials represented as sequences
+ """
+ truepoly = (isinstance(a1, poly1d) or isinstance(a2, poly1d))
+ a1 = atleast_1d(a1)
+ a2 = atleast_1d(a2)
+ diff = len(a2) - len(a1)
+ if diff == 0:
+ val = a1 + a2
+ elif diff > 0:
+ zr = NX.zeros(diff, a1.dtype)
+ val = NX.concatenate((zr, a1)) + a2
+ else:
+ zr = NX.zeros(abs(diff), a2.dtype)
+ val = a1 + NX.concatenate((zr, a2))
+ if truepoly:
+ val = poly1d(val)
+ return val
+
+def polysub(a1, a2):
+ """Subtracts two polynomials represented as sequences
+ """
+ truepoly = (isinstance(a1, poly1d) or isinstance(a2, poly1d))
+ a1 = atleast_1d(a1)
+ a2 = atleast_1d(a2)
+ diff = len(a2) - len(a1)
+ if diff == 0:
+ val = a1 - a2
+ elif diff > 0:
+ zr = NX.zeros(diff, a1.dtype)
+ val = NX.concatenate((zr, a1)) - a2
+ else:
+ zr = NX.zeros(abs(diff), a2.dtype)
+ val = a1 - NX.concatenate((zr, a2))
+ if truepoly:
+ val = poly1d(val)
+ return val
+
+
+def polymul(a1, a2):
+ """Multiplies two polynomials represented as sequences.
+ """
+ truepoly = (isinstance(a1, poly1d) or isinstance(a2, poly1d))
+ a1,a2 = poly1d(a1),poly1d(a2)
+ val = NX.convolve(a1, a2)
+ if truepoly:
+ val = poly1d(val)
+ return val
+
+def polydiv(u, v):
+ """Computes q and r polynomials so that u(s) = q(s)*v(s) + r(s)
+ and deg r < deg v.
+ """
+ truepoly = (isinstance(u, poly1d) or isinstance(u, poly1d))
+ u = atleast_1d(u)
+ v = atleast_1d(v)
+ m = len(u) - 1
+ n = len(v) - 1
+ scale = 1. / v[0]
+ q = NX.zeros((m-n+1,), float)
+ r = u.copy()
+ for k in range(0, m-n+1):
+ d = scale * r[k]
+ q[k] = d
+ r[k:k+n+1] -= d*v
+ while NX.allclose(r[0], 0, rtol=1e-14) and (r.shape[-1] > 1):
+ r = r[1:]
+ if truepoly:
+ q = poly1d(q)
+ r = poly1d(r)
+ return q, r
+
+_poly_mat = re.compile(r"[*][*]([0-9]*)")
+def _raise_power(astr, wrap=70):
+ n = 0
+ line1 = ''
+ line2 = ''
+ output = ' '
+ while 1:
+ mat = _poly_mat.search(astr, n)
+ if mat is None:
+ break
+ span = mat.span()
+ power = mat.groups()[0]
+ partstr = astr[n:span[0]]
+ n = span[1]
+ toadd2 = partstr + ' '*(len(power)-1)
+ toadd1 = ' '*(len(partstr)-1) + power
+ if ((len(line2)+len(toadd2) > wrap) or \
+ (len(line1)+len(toadd1) > wrap)):
+ output += line1 + "\n" + line2 + "\n "
+ line1 = toadd1
+ line2 = toadd2
+ else:
+ line2 += partstr + ' '*(len(power)-1)
+ line1 += ' '*(len(partstr)-1) + power
+ output += line1 + "\n" + line2
+ return output + astr[n:]
+
+
+class poly1d(object):
+ """A one-dimensional polynomial class.
+
+ p = poly1d([1,2,3]) constructs the polynomial x**2 + 2 x + 3
+
+ p(0.5) evaluates the polynomial at the location
+ p.r is a list of roots
+ p.c is the coefficient array [1,2,3]
+ p.order is the polynomial order (after leading zeros in p.c are removed)
+ p[k] is the coefficient on the kth power of x (backwards from
+ sequencing the coefficient array.
+
+ polynomials can be added, substracted, multplied and divided (returns
+ quotient and remainder).
+ asarray(p) will also give the coefficient array, so polynomials can
+ be used in all functions that accept arrays.
+
+ p = poly1d([1,2,3], variable='lambda') will use lambda in the
+ string representation of p.
+ """
+ coeffs = None
+ order = None
+ variable = None
+ def __init__(self, c_or_r, r=0, variable=None):
+ if isinstance(c_or_r, poly1d):
+ for key in c_or_r.__dict__.keys():
+ self.__dict__[key] = c_or_r.__dict__[key]
+ if variable is not None:
+ self.__dict__['variable'] = variable
+ return
+ if r:
+ c_or_r = poly(c_or_r)
+ c_or_r = atleast_1d(c_or_r)
+ if len(c_or_r.shape) > 1:
+ raise ValueError, "Polynomial must be 1d only."
+ c_or_r = trim_zeros(c_or_r, trim='f')
+ if len(c_or_r) == 0:
+ c_or_r = NX.array([0.])
+ self.__dict__['coeffs'] = c_or_r
+ self.__dict__['order'] = len(c_or_r) - 1
+ if variable is None:
+ variable = 'x'
+ self.__dict__['variable'] = variable
+
+ def __array__(self, t=None):
+ if t:
+ return NX.asarray(self.coeffs, t)
+ else:
+ return NX.asarray(self.coeffs)
+
+ def __repr__(self):
+ vals = repr(self.coeffs)
+ vals = vals[6:-1]
+ return "poly1d(%s)" % vals
+
+ def __len__(self):
+ return self.order
+
+ def __str__(self):
+ N = self.order
+ thestr = "0"
+ var = self.variable
+ for k in range(len(self.coeffs)):
+ coefstr ='%.4g' % abs(self.coeffs[k])
+ if coefstr[-4:] == '0000':
+ coefstr = coefstr[:-5]
+ power = (N-k)
+ if power == 0:
+ if coefstr != '0':
+ newstr = '%s' % (coefstr,)
+ else:
+ if k == 0:
+ newstr = '0'
+ else:
+ newstr = ''
+ elif power == 1:
+ if coefstr == '0':
+ newstr = ''
+ elif coefstr == 'b':
+ newstr = var
+ else:
+ newstr = '%s %s' % (coefstr, var)
+ else:
+ if coefstr == '0':
+ newstr = ''
+ elif coefstr == 'b':
+ newstr = '%s**%d' % (var, power,)
+ else:
+ newstr = '%s %s**%d' % (coefstr, var, power)
+
+ if k > 0:
+ if newstr != '':
+ if self.coeffs[k] < 0:
+ thestr = "%s - %s" % (thestr, newstr)
+ else:
+ thestr = "%s + %s" % (thestr, newstr)
+ elif (k == 0) and (newstr != '') and (self.coeffs[k] < 0):
+ thestr = "-%s" % (newstr,)
+ else:
+ thestr = newstr
+ return _raise_power(thestr)
+
+
+ def __call__(self, val):
+ return polyval(self.coeffs, val)
+
+ def __mul__(self, other):
+ if isscalar(other):
+ return poly1d(self.coeffs * other)
+ else:
+ other = poly1d(other)
+ return poly1d(polymul(self.coeffs, other.coeffs))
+
+ def __rmul__(self, other):
+ if isscalar(other):
+ return poly1d(other * self.coeffs)
+ else:
+ other = poly1d(other)
+ return poly1d(polymul(self.coeffs, other.coeffs))
+
+ def __add__(self, other):
+ other = poly1d(other)
+ return poly1d(polyadd(self.coeffs, other.coeffs))
+
+ def __radd__(self, other):
+ other = poly1d(other)
+ return poly1d(polyadd(self.coeffs, other.coeffs))
+
+ def __pow__(self, val):
+ if not isscalar(val) or int(val) != val or val < 0:
+ raise ValueError, "Power to non-negative integers only."
+ res = [1]
+ for _ in range(val):
+ res = polymul(self.coeffs, res)
+ return poly1d(res)
+
+ def __sub__(self, other):
+ other = poly1d(other)
+ return poly1d(polysub(self.coeffs, other.coeffs))
+
+ def __rsub__(self, other):
+ other = poly1d(other)
+ return poly1d(polysub(other.coeffs, self.coeffs))
+
+ def __div__(self, other):
+ if isscalar(other):
+ return poly1d(self.coeffs/other)
+ else:
+ other = poly1d(other)
+ return polydiv(self, other)
+
+ def __rdiv__(self, other):
+ if isscalar(other):
+ return poly1d(other/self.coeffs)
+ else:
+ other = poly1d(other)
+ return polydiv(other, self)
+
+ def __eq__(self, other):
+ return (self.coeffs == other.coeffs).all()
+
+ def __ne__(self, other):
+ return (self.coeffs != other.coeffs).any()
+
+ def __setattr__(self, key, val):
+ raise ValueError, "Attributes cannot be changed this way."
+
+ def __getattr__(self, key):
+ if key in ['r', 'roots']:
+ return roots(self.coeffs)
+ elif key in ['c','coef','coefficients']:
+ return self.coeffs
+ elif key in ['o']:
+ return self.order
+ else:
+ try:
+ return self.__dict__[key]
+ except KeyError:
+ raise AttributeError("'%s' has no attribute '%s'" % (self.__class__, key))
+
+ def __getitem__(self, val):
+ ind = self.order - val
+ if val > self.order:
+ return 0
+ if val < 0:
+ return 0
+ return self.coeffs[ind]
+
+ def __setitem__(self, key, val):
+ ind = self.order - key
+ if key < 0:
+ raise ValueError, "Does not support negative powers."
+ if key > self.order:
+ zr = NX.zeros(key-self.order, self.coeffs.dtype)
+ self.__dict__['coeffs'] = NX.concatenate((zr, self.coeffs))
+ self.__dict__['order'] = key
+ ind = 0
+ self.__dict__['coeffs'][ind] = val
+ return
+
+ def integ(self, m=1, k=0):
+ """Return the mth analytical integral of this polynomial.
+ See the documentation for polyint.
+ """
+ return poly1d(polyint(self.coeffs, m=m, k=k))
+
+ def deriv(self, m=1):
+ """Return the mth derivative of this polynomial.
+ """
+ return poly1d(polyder(self.coeffs, m=m))
+
+# Stuff to do on module import
+
+warnings.simplefilter('always',RankWarning)