Real roots of a univariate polynomial
Download: Jupyter notebook, Julia script, Maple script (PACE called from Maple through UDX, see Using PACE.jl in Maple). To install PACE and run them, see Running the worksheets.
The input is a polynomial $f \in \mathbb{Q}[x]$, nothing else. The output is a list of disjoint intervals with rational bounds, each holding exactly one real root of $f$, all the real roots being found. The result is exact: the intervals can then be shrunk to any width, the signs of other polynomials at the roots can be decided, and no rounding error can make a root disappear or appear.
We go through the steps one by one: square-free part, isolation, refinement, then two situations where floating-point arithmetic gives a wrong answer and certified isolation does not.
using PACE, AbstractAlgebra, LinearAlgebra1. The input
A polynomial is built with AbstractAlgebra (or Nemo). This one has a double root $1/2$, a triple pair $\pm\sqrt 2$ and the real root of $x^3 - x - 1$ (the plastic number), the other two roots of this cubic being complex:
R, x = polynomial_ring(QQ, "x")
f = (x - 1//2)^2 * (x^2 - 2)^3 * (x^3 - x - 1)x^11 - x^10 - 27//4*x^9 + 6*x^8 + 69//4*x^7 - 49//4*x^6 - 43//2*x^5 + 19//2*x^4 + 15*x^3 - 3*x^2 - 6*x + 2The solvers of PACE read a polynomial as the vector of its coefficients, from the constant term to the leading one:
c = Rational{BigInt}.(collect(coefficients(f)))12-element Vector{Rational{BigInt}}:
2
-6
-3
15
19//2
-43//2
-49//4
69//4
6
-27//4
-1
12. Square-free part
A multiple root is a root of $f$ and of $f'$. Dividing $f$ by $\gcd(f, f')$ keeps each root once, which is what the isolation works on:
squarefree_part(f)x^6 - 1//2*x^5 - 3*x^4 + 1//2*x^3 + 5//2*x^2 + x - 1The isolation below takes it itself; it is shown here only to see what is isolated.
3. Isolation
isolation isolates the real roots and keeps the result, so that it can be refined later without starting over:
iso = isolation(c)Isolation(4 roots, degree 6, not refined)isolate gives its roots, in increasing order:
roots = isolate(iso)4-element Vector{LACE.Root}:
Root(-1.5 .. -1.0)
Root(0.0 .. 1.0)
Root(1.25 .. 1.375)
Root(1.375 .. 1.5)Each root is a Root: either an open interval $]l, r[$ with rational bounds holding exactly one root, or a single rational, equal to the root, when the isolation met the root exactly. Four real roots, as expected; the intervals are as wide as possible: the isolation stops as soon as the roots are separated.
4. Refinement
PACE.refine! shrinks the intervals of the whole isolation to a width below $2^{-\text{prec}}$:
PACE.refine!(iso, 64)
roots = isolate(iso)4-element Vector{LACE.Root}:
Root(-1.4142135623730951 .. -1.4142135623730951)
Root(1//2)
Root(1.324717957244746 .. 1.324717957244746)
Root(1.4142135623730951 .. 1.4142135623730951)The double root $1/2$ is now found exactly. A single root can also be refined alone, to any precision: here $\sqrt 2$ to 1000 bits.
r = roots[end]
PACE.refine!(r, 1000)
Float64(PACE.width(r))5.832897615645118e-303The bounds are exact rationals; compared with $\sqrt 2$ computed by MPFR:
s = setprecision(() -> sqrt(BigFloat(2)), BigFloat, 1100)
PACE.lo(r) < s < PACE.hi(r)true5. Signs of other polynomials at the roots
Which roots of $f$ are roots of $x^2 - 2$, which ones lie above $1/2$? signs_at gives the sign of each polynomial at each root, a zero being decided exactly (by a gcd), not by an approximate value. The polynomials are given by their integer coefficients (PACE.zx_integral clears the denominators):
zx(p) = PACE.zx_integral(Rational{BigInt}.(collect(coefficients(p))))
boxes, signs = signs_at(iso.sf, [zx(x^2 - 2), zx(2x - 1)], [(PACE.lo(r), PACE.hi(r)) for r in roots])
signs4-element Vector{Vector{Int64}}:
[0, -1]
[-1, 0]
[-1, 1]
[0, 1]Row $i$ gives the signs of $x^2-2$ and $2x-1$ at the $i$-th root: $x^2 - 2$ vanishes at the first and last roots, $2x - 1$ at the second one.
6. Two close roots: where floating point fails
Mignotte's polynomial $x^{20} - 2(100x - 1)^2$ has two real roots very close to $1/100$: they differ by about $10^{-22}$.
m = x^20 - 2 * (100x - 1)^2
cm = Rational{BigInt}.(collect(coefficients(m)));The usual floating-point method computes the eigenvalues of the companion matrix (this is what roots does in Matlab):
n = degree(m)
C = zeros(n, n)
for i in 2:n
C[i, i-1] = 1
end
C[:, n] = -Float64.(cm[1:n]) ./ Float64(cm[n+1])
filter(z -> abs(imag(z)) < 1e-6, eigvals(C))4-element Vector{ComplexF64}:
-1.734696440260734 + 0.0im
0.009999999855836147 + 0.0im
0.01000000014416385 + 0.0im
1.7324741845653981 + 0.0imNear $1/100$ it finds a pair of complex conjugate roots, with an imaginary part of about $10^{-10}$: it cannot tell two close real roots from two complex ones, and here it picks the wrong answer. The certified isolation finds four real roots:
isom = isolation(cm)
isolate(isom)4-element Vector{LACE.Root}:
Root(-4.0 .. 0.0)
Root(0.01 .. 0.01)
Root(0.01 .. 0.01)
Root(1.0 .. 2.0)and separates the two close ones once refined:
PACE.refine!(isom, 100)
rm = isolate(isom)
Float64(PACE.lo(rm[3]) - PACE.hi(rm[2]))1.4142135619248986e-22The gap between the two intervals is a proof that the roots are distinct.
7. Higher degrees
The Chebyshev polynomial $T_n$ has $n$ real roots in $]-1, 1[$, crowded near $\pm 1$. With $n = 300$:
function chebyshev(n)
a, b = one(R), x
for _ in 2:n
a, b = b, 2x * b - a
end
n == 0 ? a : b
end
ct = Rational{BigInt}.(collect(coefficients(chebyshev(300))))
t = @elapsed isot = isolation(ct)
length(isot), t(300, 0.147261993)All 300 roots are found, in a fraction of a second. Their refinement to 256 bits:
@elapsed PACE.refine!(isot, 256)0.362037481The extreme root is $\cos(\pi/600)$:
r1 = isolate(isot)[end]
c1 = setprecision(() -> cos(BigFloat(pi) / 600), BigFloat, 300)
PACE.lo(r1) < c1 < PACE.hi(r1)true