#%config InlineBackend.figure_format = 'svg'
from pylab import *
import sympy
5.1Existence of roots¶
A polynomial of degree is a function of the form
where the coefficients may be real or complex.
If is a root of , then it can be written as where is a polynomial of degree . Continuing this way, we see that can be written as
Some of these roots may coincide; if appears times in the above factorization, then it is said to have multiplicity .
Note that we may not have any real roots even if all the coefficients are real, for example, has no real solutions.
5.2Roots from eigenvalues¶
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 is an eigenvalue of the matrix , called the companion matrix. We have taken the coefficient of to be one but if this is not the case, then the coefficients can be scaled and this does not change the roots.
The next function computes the roots by finding eigenvalues, we do not assume that , so we need to scale the coefficients.
# 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 with random coefficients in and find its roots.
m = 10
a = 2.0*rand(m) - 1.0
r = roots1(a)
print("roots = \n", r)
roots =
[-0.42792626+0.9554156j -0.42792626-0.9554156j -1.11699399+0.j
-0.70192041+0.j 0.4352943 +0.58247496j 0.4352943 -0.58247496j
0.92515954+0.39939538j 0.92515954-0.39939538j 10.92391124+0.j ]
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)))
|p(r)| =
[3.68628542e-15 3.68628542e-15 9.21485110e-15 4.44089210e-16
3.77442862e-15 3.77442862e-15 4.89953650e-16 4.89953650e-16
6.08616218e-08]
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)
roots =
[10.92391124+0.j -1.11699399+0.j -0.42792626+0.9554156j
-0.42792626-0.9554156j -0.70192041+0.j 0.92515954+0.39939538j
0.92515954-0.39939538j 0.4352943 +0.58247496j 0.4352943 -0.58247496j]
These roots are same as we got from our own function, but their ordering may be different.
5.3Test for ill-conditioning¶
Consider a polynomial
Let α be a single root, i.e., and . Let us perturb the polynomial into a different polynomial
where
and let be a root of . Then, assuming the perturbation of the root is small, it can be estimated as
The change in the root is small, of ; however, depending on the value of the remaining factors in the above equation, the actual change can be larger than ε.
If α is a double root, then we can estimate the perturbation (Wilkinson)
Now, the change in the root is of which is much larger than ε when . Thus multiple roots can be expected to be very sensitive to changes in the coefficients.
5.4Wilkinson’s polynomial¶
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))
a[ 0] = 1
a[ 1] = -210
a[ 2] = 20615
a[ 3] = -1256850
a[ 4] = 53327946
a[ 5] = -1672280820
a[ 6] = 40171771630
a[ 7] = -756111184500
a[ 8] = 11310276995381
a[ 9] = -135585182899530
a[10] = 1307535010540395
a[11] = -10142299865511450
a[12] = 63030812099294896
a[13] = -311333643161390640
a[14] = 1206647803780373360
a[15] = -3599979517947607200
a[16] = 8037811822645051776
a[17] = -12870931245150988800
a[18] = 13803759753640704000
a[19] = -8752948036761600000
a[20] = 2432902008176640000
print('Monomial form = ', P)
Monomial form = Poly(x**20 - 210*x**19 + 20615*x**18 - 1256850*x**17 + 53327946*x**16 - 1672280820*x**15 + 40171771630*x**14 - 756111184500*x**13 + 11310276995381*x**12 - 135585182899530*x**11 + 1307535010540395*x**10 - 10142299865511450*x**9 + 63030812099294896*x**8 - 311333643161390640*x**7 + 1206647803780373360*x**6 - 3599979517947607200*x**5 + 8037811822645051776*x**4 - 12870931245150988800*x**3 + 13803759753640704000*x**2 - 8752948036761600000*x + 2432902008176640000, x, domain='ZZ')
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 by sampling it on a uniform grid using monomial form.
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 . If we plot it in factored form, it does not suffer so much from rounding errors.
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)
[19.99980929 19.00190982 17.99092135 17.02542715 15.94628672 15.0754938
13.91475559 13.07431403 11.95328325 11.02502293 9.99041304 9.00291529
7.99935583 7.000102 5.99998925 5.00000067 3.99999998 3.
2. 1. ]
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 and each is an independent Gaussian random variable with mean zero and standard deviation 1. We perform 100 random perturbations of the coefficients and plot the resulting roots on a single plot.
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 while the change in roots is , which indicates a very high sensitivity wrt the coefficients. The roots 14,15,16 seem to experience the largest perturbation.
Using the estimate for root perturbation, we see that if the coefficient is changed to , the root changes to , and
is the condition number of wrt perturbations in . The condition number means
A large value indicates high sensitity of the root (output) to changes in coefficients (input). The most sensitive root is wrt perturbations in
- Corless, R. M., & Sevyeri, L. R. (2020). The Runge Example for Interpolation and Wilkinson’s Examples for Rootfinding. SIAM Review, 62(1), 231–243. 10.1137/18M1181985
- Trefethen, L. N. (2019). Approximation Theory and Approximation Practice, Extended Edition. Society for Industrial. 10.1137/1.9781611975949
- Boyd, J. P. (2002). Computing Zeros on a Real Interval through Chebyshev Expansion and Polynomial Rootfinding. SIAM Journal on Numerical Analysis, 40(5), 1666–1682. 10.1137/S0036142901398325