| exports | kernelspec | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
#%config InlineBackend.figure_format = 'svg'
from pylab import *
import sympy
A polynomial of degree
where the coefficients
:::{prf:theorem} Fundamental Theorem of Algebra
Every non-constant polynomial has at least one root in
If
Some of these roots may coincide; if
:::{prf:theorem}
A polynomial of degree
Note that we may not have any real roots even if all the coefficients are real, for example,
Eigenvalues of a square matrix are the roots of a certain polynomial called the characteristic polynomial. Conversely, the roots of a given polynomial are the eigenvalues of a certain matrix.
The polynomial
can be written as
with all other matrix entries being zero, i.e.,
Then
and every root of
+++
:::{prf:remark}
If
+++
The next function computes the roots by finding eigenvalues, we do not assume that
# Input array a contains coefficient like this
# p(x) = a[0] + a[1] * x + a[2] * x**2 + ... + a[n] * x**n
def roots1(a):
n = len(a) - 1
c = a[0:-1] / a[-1]
A = zeros((n,n))
A[:,-1] = -c
A += diag(ones(n-1), -1)
r = eigvals(A)
return r
Make a polynomial of degree
m = 10
a = 2.0*rand(m) - 1.0
r = roots1(a)
print("roots = \n", r)
Check the roots by evaluating the polynomial at the roots. We use polyval which expects coefficients in opposite order to what we have assumed above.
print("|p(r)| = \n", abs(polyval(a[::-1],r)))
These values are close to machine precision which can be considered to be zero.
The function numpy.roots computes roots by the same method.
r = roots(a[::-1])
print("roots = \n", r)
These roots are same as we got from our own function, but their ordering may be different.
+++
:::{prf:example} Consider the cubic polynomial
whose exact roots are
r = sympy.roots([1,-21,120,-100])
print('Roots = ',r)
for x in r:
print(x)
Now let us perturb the highest coefficient by 1%
The roots are now
r = sympy.roots([sympy.Integer(99)/100,-21,120,-100])
for x in r:
print(sympy.N(x))
While
The roots are now
r = sympy.roots([sympy.Integer(101)/100,-21,120,-100])
for x in r:
print(sympy.N(x))
Again, the double roots are very sensitive to perturbation of the coefficient. We say that the root finding problem is ill-conditioned. :::
Consider a polynomial
Let
where
and let
The change in the root is small, of
If
Now, the change in the root is of
+++
:::{prf:example} The polynomial
has roots
Hence the root
This indicates that the double root is very sensitive to perturbation in the coefficient of
+++
Wilkinson considered the following polynomial
We can find polynomial coefficients using sympy.
x, p = sympy.symbols('x p')
p = 1
for i in range(1,21):
p = p * (x - i)
P = sympy.Poly(p, x)
a = P.coeffs()
for i,coef in enumerate(a):
print("a[%2d] = %d" % (i,coef))
print('Monomial form = ', P)
The coefficients are returned in this order
i.e., a[0] is the coefficient of the largest degree term. We note that the coefficients are very large.
This function computes the polynomial in factored form.
# As a product of factors
def wpoly(x):
p = 1.0
for i in range(1,21):
p = p * (x - i)
return p
Plot the polynomial in
xp = linspace(1,20,1000)
yp = polyval(a,xp)
plot(xp,yp)
plot(arange(1,21),zeros(20),'o',label='Roots')
title("Monomial form")
legend(), grid(True), ylim(-1e13,1e13);
Computing the polynomial as a monomial is subject to lot of rounding errors since the coefficients are very large, see the jagged nature of the curve around
xp = linspace(1,20,1000)
yp = wpoly(xp)
plot(xp,yp)
plot(arange(1,21),zeros(20),'o',label='Roots')
title("Factored form")
legend(), grid(True), ylim(-1e13,1e13);
Find the roots using numpy.roots which computes it using the eigenvalue approach
r = roots(a)
print(r)
The relative error in the roots is shown in the figure.
rexact = arange(20,0,-1)
semilogy(rexact, abs(r-rexact)/rexact, 'o')
xticks(rexact), grid(True), xlabel('Exact root'), ylabel('Relative error');
Randomly perturb the monomial coefficients and find roots
where
eps = 1e-10 # Relative perturbation
nsamp = 100 # 100 random perturbations
for i in range(nsamp):
r = normal(0.0, 1.0, 21)
aa = a * (1 + eps * r)
root = roots(aa)
plot(real(root), imag(root),'k.')
plot(arange(1,21),zeros(20), 'ro')
xlabel('Real'); ylabel('Imag');
The relative perturbation in coefficients is
Using the estimate for root perturbation, we see that if the coefficient
is the condition number of
A large value indicates high sensitity of the root (output) to changes in coefficients (input). The most sensitive root is
:::{attention} The Wilkinson polynomial is a very pathological and atypical example. Because of the large coefficients, even evaluating the polynomial suffers from round off errors. Of course, the mathematical problem itself is ill-conditioned. :::
:::{seealso} See [@Corless2020] for a related explanation of Wilkinson polynomial and other examples.
For some interesting results on roots of random polynomials, see
- https://www.chebfun.org/examples/roots/RandomPolynomials.html
- https://www.chebfun.org/examples/roots/RandomPolys.html
You can produce the plots shown in above links in Python using numpy.polynomial.
Roots of polynomials written in Chebyshev basis can also be obtained as eigenvalues of a colleague matrix [@Trefethen2019]. A general function defined in an interval can be approximated to machine precision by Chebyshev interpolation and the roots of the function in its domain of definition can be obtained as roots of the polynomial [@Boyd2002]. See this in Chebfun: https://www.chebfun.org/docs/guide/guide03.html. :::