""" Functions to operate on polynomials. """ from org.meteoinfo.math.fitting import FittingUtil from org.meteoinfo.ndarray.math import ArrayMath, ArrayUtil from ..core import numeric as NX from ..core import NDArray, atleast_1d from ..linalg import eigvals import warnings __all__ = ['polyfit','polyval','polyder'] def poly(seq_of_zeros): """ Find the coefficients of a polynomial with the given sequence of roots. Returns the coefficients of the polynomial whose leading coefficient is one for the given sequence of zeros (multiple roots must be included in the sequence as many times as their multiplicity; see Examples). A square matrix (or array, which will be treated as a matrix) can also be given, in which case the coefficients of the characteristic polynomial of the matrix are returned. Parameters ---------- seq_of_zeros : array_like, shape (N,) or (N, N) A sequence of polynomial roots, or a square array or matrix object. Returns ------- c : ndarray 1D array of polynomial coefficients from highest to lowest degree: ``c[0] * x**(N) + c[1] * x**(N-1) + ... + c[N-1] * x + c[N]`` where c[0] always equals 1. Raises ------ ValueError If input is the wrong shape (the input must be a 1-D or square 2-D array). See Also -------- polyval : Compute polynomial values. roots : Return the roots of a polynomial. polyfit : Least squares polynomial fit. poly1d : A one-dimensional polynomial class. Notes ----- Specifying the roots of a polynomial still leaves one degree of freedom, typically represented by an undetermined leading coefficient. [1]_ In the case of this function, that coefficient - the first one in the returned array - is always taken as one. (If for some reason you have one other point, the only automatic way presently to leverage that information is to use ``polyfit``.) The characteristic polynomial, :math:`p_a(t)`, of an `n`-by-`n` matrix **A** is given by :math:`p_a(t) = \\mathrm{det}(t\\, \\mathbf{I} - \\mathbf{A})`, where **I** is the `n`-by-`n` identity matrix. [2]_ References ---------- .. [1] M. Sullivan and M. Sullivan, III, "Algebra and Trigonometry, Enhanced With Graphing Utilities," Prentice-Hall, pg. 318, 1996. .. [2] G. Strang, "Linear Algebra and Its Applications, 2nd Edition," Academic Press, pg. 182, 1980. Examples -------- Given a sequence of a polynomial's zeros: >>> np.poly((0, 0, 0)) # Multiple root example array([1., 0., 0., 0.]) The line above represents z**3 + 0*z**2 + 0*z + 0. >>> np.poly((-1./2, 0, 1./2)) array([ 1. , 0. , -0.25, 0. ]) The line above represents z**3 - z/4 >>> np.poly((np.random.random(1)[0], 0, np.random.random(1)[0])) array([ 1. , -0.77086955, 0.08618131, 0. ]) # random Given a square array object: >>> P = np.array([[0, 1./3], [-1./2, 0]]) >>> np.poly(P) array([1. , 0. , 0.16666667]) Note how in all cases the leading coefficient is always 1. """ seq_of_zeros = atleast_1d(seq_of_zeros) sh = seq_of_zeros.shape if len(sh) == 2 and sh[0] == sh[1] and sh[0] != 0: seq_of_zeros = eigvals(seq_of_zeros) elif len(sh) == 1: dt = seq_of_zeros.dtype # Let object arrays slip through, e.g. for arbitrary precision if dt != object: seq_of_zeros = seq_of_zeros.astype(mintypecode(dt.char)) else: raise ValueError("input must be 1d or non-empty square 2d array.") if len(seq_of_zeros) == 0: return 1.0 dt = seq_of_zeros.dtype a = ones((1,), dtype=dt) for zero in seq_of_zeros: a = NX.convolve(a, array([1, -zero], dtype=dt), 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) if NX.all(NX.sort(roots) == NX.sort(roots.conjugate())): a = a.real.copy() return a def polyfit(x, y, degree, func=False): """ Polynomial fitting. :param x: (*array_like*) x data array. :param y: (*array_like*) y data array. :param degree: (*int*) Degree of the fitting polynomial. :param func: (*boolean*) Return fit function (for predict function) or not. Default is ``False``. :returns: Fitting parameters and function (optional). """ if isinstance(x, list): x = NDArray(ArrayUtil.array(x)) if isinstance(y, list): y = NDArray(ArrayUtil.array(y)) r = FittingUtil.polyFit(x.asarray(), y.asarray(), degree) if func: return r[0], r[1], r[2] else: return r[0], r[1] def polyval(p, x): """ Evaluate a polynomial at specific values. 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] If x is a sequence, then p(x) is returned for each element of x. If x is another polynomial then the composite polynomial p(x(t)) is returned. :param p: (*array_like*) 1D array of polynomial coefficients (including coefficients equal to zero) from the highest degree to the constant term. :param x: (*array_like*) A number, an array of numbers, or an instance of poly1d, at which to evaluate p. :returns: Polynomial value """ if isinstance(x, list): x = NDArray(ArrayUtil.array(x)) return NDArray(ArrayMath.polyVal(p, x.asarray())) def polyder(p, m=1): """ Return the derivative of the specified order of a polynomial. .. note:: This forms part of the old polynomial API. Since version 1.4, the new polynomial API defined in `numpy.polynomial` is preferred. A summary of the differences can be found in the :doc:`transition guide `. Parameters ---------- p : poly1d or sequence Polynomial to differentiate. A sequence is interpreted as polynomial coefficients, see `poly1d`. m : int, optional Order of differentiation (default: 1) Returns ------- der : poly1d A new polynomial representing the derivative. See Also -------- polyint : Anti-derivative of a polynomial. poly1d : Class for one-dimensional polynomials. Examples -------- The derivative of the polynomial :math:`x^3 + x^2 + x^1 + 1` is: >>> p = np.poly1d([1,1,1,1]) >>> p2 = np.polyder(p) >>> p2 poly1d([3, 2, 1]) which evaluates to: >>> p2(2.) 17.0 We can verify this, approximating the derivative with ``(f(x + h) - f(x))/h``: >>> (p(2. + 0.001) - p(2.)) / 0.001 17.007000999997857 The fourth-order derivative of a 3rd-order polynomial is zero: >>> np.polyder(p, 2) poly1d([6, 2]) >>> np.polyder(p, 3) poly1d([6]) >>> np.polyder(p, 4) poly1d([0]) """ m = int(m) if m < 0: raise ValueError("Order of derivative must be positive (see polyint)") #truepoly = isinstance(p, poly1d) p = NX.asarray(p) n = len(p) - 1 y = p[:-1] * NX.arange(n, 0, -1) if m == 0: val = p else: val = polyder(y, m - 1) #if truepoly: # val = poly1d(val) return val class poly1d: """ A one-dimensional polynomial class. A convenience class, used to encapsulate "natural" operations on polynomials so that said operations may take on their customary form in code (see Examples). Parameters ---------- c_or_r : array_like The polynomial's coefficients, in decreasing powers, or if the value of the second parameter is True, the polynomial's roots (values where the polynomial evaluates to 0). For example, ``poly1d([1, 2, 3])`` returns an object that represents :math:`x^2 + 2x + 3`, whereas ``poly1d([1, 2, 3], True)`` returns one that represents :math:`(x-1)(x-2)(x-3) = x^3 - 6x^2 + 11x -6`. r : bool, optional If True, `c_or_r` specifies the polynomial's roots; the default is False. variable : str, optional Changes the variable used when printing `p` from `x` to `variable` (see Examples). Examples -------- Construct the polynomial :math:`x^2 + 2x + 3`: >>> p = np.poly1d([1, 2, 3]) >>> print(np.poly1d(p)) 2 1 x + 2 x + 3 Evaluate the polynomial at :math:`x = 0.5`: >>> p(0.5) 4.25 Find the roots: >>> p.r array([-1.+1.41421356j, -1.-1.41421356j]) >>> p(p.r) array([ -4.44089210e-16+0.j, -4.44089210e-16+0.j]) # may vary These numbers in the previous line represent (0, 0) to machine precision Show the coefficients: >>> p.c array([1, 2, 3]) Display the order (the leading zero-coefficients are removed): >>> p.order 2 Show the coefficient of the k-th power in the polynomial (which is equivalent to ``p.c[-(i+1)]``): >>> p[1] 2 Polynomials can be added, subtracted, multiplied, and divided (returns quotient and remainder): >>> p * p poly1d([ 1, 4, 10, 12, 9]) >>> (p**3 + 4) / p (poly1d([ 1., 4., 10., 12., 9.]), poly1d([4.])) ``asarray(p)`` gives the coefficient array, so polynomials can be used in all functions that accept arrays: >>> p**2 # square of polynomial poly1d([ 1, 4, 10, 12, 9]) >>> np.square(p) # square of individual coefficients array([1, 4, 9]) The variable used in the string representation of `p` can be modified, using the `variable` parameter: >>> p = np.poly1d([1,2,3], variable='z') >>> print(p) 2 1 z + 2 z + 3 Construct a polynomial from its roots: >>> np.poly1d([1, 2], True) poly1d([ 1., -3., 2.]) This is the same polynomial as obtained by: >>> np.poly1d([1, -1]) * np.poly1d([1, -2]) poly1d([ 1, -3, 2]) """ __hash__ = None @property def coeffs(self): """ The polynomial coefficients """ return self._coeffs @coeffs.setter def coeffs(self, value): # allowing this makes p.coeffs *= 2 legal if value is not self._coeffs: raise AttributeError("Cannot set attribute") @property def variable(self): """ The name of the polynomial variable """ return self._variable # calculated attributes @property def order(self): """ The order or degree of the polynomial """ return len(self._coeffs) - 1 @property def roots(self): """ The roots of the polynomial, where self(x) == 0 """ return roots(self._coeffs) # our internal _coeffs property need to be backed by __dict__['coeffs'] for # scipy to work correctly. @property def _coeffs(self): return self.__dict__['coeffs'] @_coeffs.setter def _coeffs(self, coeffs): self.__dict__['coeffs'] = coeffs # alias attributes r = roots c = coef = coefficients = coeffs o = order def __init__(self, c_or_r, r=False, variable=None): if isinstance(c_or_r, poly1d): self._variable = c_or_r._variable self._coeffs = c_or_r._coeffs if set(c_or_r.__dict__) - set(self.__dict__): msg = ("In the future extra properties will not be copied " "across when constructing one poly1d from another") warnings.warn(msg, FutureWarning, stacklevel=2) self.__dict__.update(c_or_r.__dict__) if variable is not None: self._variable = variable return if r: c_or_r = poly(c_or_r) c_or_r = atleast_1d(c_or_r) if c_or_r.ndim > 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], dtype=c_or_r.dtype) self._coeffs = c_or_r if variable is None: variable = 'x' self._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): thestr = "0" var = self.variable # Remove leading zeros coeffs = self.coeffs[NX.logical_or.accumulate(self.coeffs != 0)] N = len(coeffs)-1 def fmt_float(q): s = '%.4g' % q if s.endswith('.0000'): s = s[:-5] return s for k, coeff in enumerate(coeffs): if not iscomplex(coeff): coefstr = fmt_float(real(coeff)) elif real(coeff) == 0: coefstr = '%sj' % fmt_float(imag(coeff)) else: coefstr = '(%s + %sj)' % (fmt_float(real(coeff)), fmt_float(imag(coeff))) 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 newstr.startswith('-'): thestr = "%s - %s" % (thestr, newstr[1:]) else: thestr = "%s + %s" % (thestr, newstr) else: thestr = newstr return _raise_power(thestr) def __call__(self, val): return polyval(self.coeffs, val) def __neg__(self): return poly1d(-self.coeffs) def __pos__(self): return self 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) __truediv__ = __div__ def __rdiv__(self, other): if isscalar(other): return poly1d(other/self.coeffs) else: other = poly1d(other) return polydiv(other, self) __rtruediv__ = __rdiv__ def __eq__(self, other): if not isinstance(other, poly1d): return NotImplemented if self.coeffs.shape != other.coeffs.shape: return False return (self.coeffs == other.coeffs).all() def __ne__(self, other): if not isinstance(other, poly1d): return NotImplemented return not self.__eq__(other) def __getitem__(self, val): ind = self.order - val if val > self.order: return self.coeffs.dtype.type(0) if val < 0: return self.coeffs.dtype.type(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._coeffs = NX.concatenate((zr, self.coeffs)) ind = 0 self._coeffs[ind] = val return def __iter__(self): return iter(self.coeffs) def integ(self, m=1, k=0): """ Return an antiderivative (indefinite integral) of this polynomial. Refer to `polyint` for full documentation. See Also -------- polyint : equivalent function """ return poly1d(polyint(self.coeffs, m=m, k=k)) def deriv(self, m=1): """ Return a derivative of this polynomial. Refer to `polyder` for full documentation. See Also -------- polyder : equivalent function """ return poly1d(polyder(self.coeffs, m=m))