# Polynomials And Rational Functions

# Introduction

The `ratfun` module provides classes for defining polynomial and rational function (ratio of two polynomials) objects. These objects can be used in arithmetic expressions and evaluated at a particular point.

# Features

- Create polynomials and rational functions with an arbitrary number of coefficients. The coefficients can be any numeric type (conversion to complex is the test of a numeric type).

- Support the usual arithmetic operations (+, -, *, /, **). Polynomials also support %%, and divmod.

- Combine polynomials, rational functions, and ordinary numbers together in arithmetic expressions.

- Automatically handles rational and extended precision floating point coefficients. This includes converting integers to rationals when a division is performed.

- Evaluate a polynomial or rational function by calling it.

- Find the roots of a polynomial to any desired precision. Using the default tolerance, the method converges exactly even for the ill-conditioned Wilkinson polynomial of degree 100.

# Limitations

- Only supports polynomials and rational functions in a single variable.

- Mixing Python float and complex numbers with polynomials or rational functions that have mpq or cmpq coefficients cause exceptions. There are methods to convert all of the coefficients to an appropriate type to overcome this problem.

- Operating on high degree rational functions can get very slow due to the GCD calculations which are not optimized.

# Installation

The ratfun module is distributed as part of the rpncalc package. See the installation instructions for that package. If you don't want the entire rpncalc package, install the clnum package and copy the ratfun module to anywhere on your Python path.

# Examples

These examples assume the following imports.

    from ratfun import Polynomial as poly, RationalFunction as rat
    from clnum import mpq

If you are using the module from the RPN calculator package, the import is slightly modified.

    from rpncalc.ratfun import Polynomial as poly, RationalFunction as rat
    from clnum import mpq

## Object Creation

    %(p:print poly(0)
    p:print poly(1)
    p:print poly(1+1j)
    p:print poly(1,2,3)
    p:# (coefficient, exponent) pairs form.  Pairs can be in any order.
    p:print poly((mpq(1,3),3), (mpq(1,2),2))
    p:print poly([(mpq(1,3),3), (mpq(1,2),2)]) # Sequence of pairs is accepted
    p:print rat(0)
    p:print rat(0.5)
    p:print rat(1+1j)
    p:print rat(1,2)
    p:print rat((-1,1),(1,1))
    p:print rat((1,-2,1),(1,2,1))
    p:print rat((1,2,1),(2,2))  # Reduced to lowest terms and normalized.
    hp:#)s

## Arithmetic

    %(p:x, x2, x3 = poly([(1,1)]), poly([(1,2)]), poly([(1,3)])
    p:print x, x2, x3
    p:print x, x**2, x**3
    p:print .5*x + 1.3*x2 + (1+1j)*x3 + 1
    p:print poly(1,0.5,1.3,1+1j)
    p:print .5*x + 1.3*x2 + (1+1j)*x3 + 1 - poly(1,0.5,1.3,1+1j)
    p:print .5*x + mpq(1,3)*x2
    p:a = 1*x3 + 3*x2 + 3*x + 1
    p:b = poly(1,1)
    p:print a,b
    p:print a/b
    p:print a%b
    p:q,r = divmod(a,b)
    p:print q,r
    p:q,r = divmod(poly(-1,3,-3,1), poly(1,2,1))
    p:print q,r
    p:print q*poly(1,2,1) + r
    p:z = rat((-1,1),(1,1))
    p:print z
    p:print z*z
    p:print z**3
    p:print z**-3
    p:print z**3 * z**-3
    p:p = poly(1,2,1)
    p:print p
    p:print p+z
    p:print z-p
    p:print p*z
    p:rat.screenWidth = 0 # Suppress multi-line string format.
    p:print p/z
    p:rat.screenWidth = 80
    p:print z/p
    hp:#)s

## Methods

### Polynomials

    %(p:p = poly(1,2,1)
    p:print p
    p:print p.coef(0), p.coef(1), p.coef(2), p.coef(3), p.coef(4)
    p:print p.deg    # Degree of the polynomial
    p:print p.deriv  # Derivative of the polynomial
    p:print p.integ  # Integral of the polynomial
    p:print list(p.sample(0,2,5)) # Evaluate at 5 points in the interval [0..2]
    p:print p.float()   # Convert the coefficients to float
    p:print p.complex() # Convert the coefficients to complex
    p:print list(poly(1,0,0,0,2).coefAsPairs())
    hp:#)s

### Rational Functions

    %(p:z = rat((-1,1),(1,1))
    p:print z
    p:print z.numer  # Numerator polynomial
    p:print z.denom  # Denominator polynomial
    p:print z.deriv  # Derivative of the rational function
    p:print list(z.sample(0,2,5)) # Evaluate at 5 points in the interval [0..2]
    p:print z.float()   # Convert the coefficients to float
    p:print z.complex() # Convert the coefficients to complex
    hp:#)s

## Extended Example

For a more complex example, let's compute a rational function approximation to the natural log function. This expansion can be found in a number of math handbooks. Note that the expansion is done by evaluating a polynomial at a rational function.

    %(p:z = rat((-1,1),(1,1))
    p:print z
    p:p = poly([(mpq(1,n), n) for n in xrange(1,8,2)])
    p:print p
    p:aln = 2*p(z)
    p:print aln
    p:print float(aln(1))  # Evaluate a rational function by calling it.
    p:print float(aln(2))
    p:from math import log
    p:err = [abs(y - log(x)) for x,y in aln.float().sample(1,2,100)]
    p:print len(err)
    p:print err[0], err[50], err[-1]
    p:print max(err)
    hp:#)s

## Roots of Polynomials

There are a number of discussions of polynomial root finders on the Internet (see Numerical Recipes or Wikipedia). Laguerre's method is used in the ratfun root finder because it handles ill-conditioned polynomials, complex roots, and multiple roots (convergence is only linear in this case). Unlike the version in Numerical Recipes, this implementation uses high precision complex arithmetic to avoid non-converging limit cycles.

Since the Wilkinson polynomial is ill-conditioned, we will use it as an example.

    %(p:from rpncalc.ratfun import polyFromRoots, sortRoots
    p:p = polyFromRoots(range(1,21))
    p:roots = p.roots()
    p:for r in roots: print r,
    hp:#)s

In the example above, all of the roots were found exactly. Now use the root finder in Numpy on the same polynomial.

    %(hp:from rpncalc.ratfun import polyFromRoots, sortRoots
    hp:p = polyFromRoots(range(1,21))
    p:import numpy
    p:roots = sortRoots(numpy.roots(p.float()._coef[::-1]))
    p:for r in roots: print r
    p:print max([abs(((i+1)-r)/(i+1)) for i,r in enumerate(roots)])
    hp:#)s

Investigate the source of the ill-conditioned behavior by evaluating the polynomial near the largest root. This demonstrates that the polynomial oscillates with very large excursions between the roots.

    %(hp:from rpncalc.ratfun import polyFromRoots, sortRoots
    hp:p = polyFromRoots(range(1,21))
    p:print len(str(20**20)) # Number of digits needed
    p:prec = 37 # Add another 10 digits
    p:dx = mpf('1e-9', prec)
    p:print p(20+dx), p(20-dx)
    p:dx = mpf('1e-18', prec)
    p:print p(20+dx), p(20-dx)
    hp:#)s

Now change one of the coefficients by a small amount and observe what happens to the roots.

    %(hp:from rpncalc.ratfun import polyFromRoots, sortRoots, Polynomial as poly
    hp:p = polyFromRoots(range(1,21))
    p:p1 = p + poly([(mpq(-1,2**23),19)])
    p:print p.coef(19), float(p1.coef(19))
    p:roots = p1.roots(eps=1e-20)
    p:for r in roots: print r
    p:print max([float(abs(p1(r))) for r in roots])
    hp:#)s

### Multiple Roots

A polynomial can have a root repeated multiple times. Let p=(x - r)m, then p is a polynomial with a single root r of multiplicity m. This type of polynomial causes problems for iterative root-finding methods like Laguerre's method because the graph of the polynomial is flat in the region surrounding the root. The higher the multiplicity, the flatter the graph. As a consequence of this, Laguerre's method converges only linearly when approaching a multiple root. So what can be done to make the convergence faster?

If you take the derivative of a polynomial with a root of multiplicity greater than one, you will find that the multiplicity is reduced by one. Computing the greatest common divisor of the derivative polynomial with the original will identify all the extra common factors which can then be removed by dividing into the original polynomial. The resulting polynomial will contain only unique roots. This polynomial can then be solved using a standard root finder. The following example shows this reduction.

    %(hp:from rpncalc.ratfun import gcd, Polynomial as poly
    p:p = poly(1,1)**5
    p:print p
    p:q = gcd(p, p.deriv)
    p:print p/q
    p:print (p/q).roots()
    hp:#)s

The method uniqueRoots performs the above operations and returns a list of the unique roots. It is often possible to find the roots exactly with this technique.

    %(hp:from rpncalc.ratfun import Polynomial as poly
    p:p = poly(-mpq(1,3),1)**5 * poly(-mpq(1,7),1)**7 *  poly(-mpq(1,11),1)**10
    p:print p.deg
    p:for r in p.uniqueRoots(): print mpq(r),
    hp:#)s

If the coefficients of the polynomial are not exact, uniqueRoots may not be able to determine the multiplicity. In this case, it drops back to using the roots method on the original polynomial. Consequently, you loose nothing but the time it takes to compute a derivative and a gcd.

The multiplicity of each root can be determined by repeatedly dividing by the monomial factor until there is a remainder. However, doing this reliably with approximate roots is a difficult problem so there is no method provided to determine the multiplicities.