# Root isolation of real-rooted integer polynomials

I have a collection of polynomials with non-negative integer coefficients that I know to be real-rooted with no repeated roots by examining Sturm chains for each. I would like to show that they form an interlacing family (hence their convex combinations are real-rooted). Coming from `Maple`, I would use the `realroot` command (based on Descartes’ rule of signs) to find rationally bounded isolating intervals for the real roots of the polynomials. Is there a package that implements something equivalent in `Julia`? For a ballpark idea of scale, I’d like to be able to work with 10^5 polynomials with degree 80, and coefficients on the order of 2^480, but something working out-of-the-box on smaller instances would still be helpful.

You will likely find `IntervalRootFinding` to be helpful here. It will give rigorous bounds. (The issue with `roots` from Polynomials is the high degree will likely yield numeric issues). Here is a sample usage, though it sounds like you might use the `Polynomial(coeffs)` constructor:

``````julia> using Polynomials, IntervalRootFinding, IntervalArithmetic

julia> x = [2,3,4,5,7,8,9];

julia> p = fromroots(Polynomial, x)
Polynomial(-60480 + 100536*x - 68078*x^2 + 24433*x^3 - 5036*x^4 + 598*x^5 - 38*x^6 + x^7)

julia> IntervalRootFinding.roots(x->p(x), -20..20)
7-element Array{Root{Interval{Float64}},1}:
Root([2.99999, 3.00001], :unique)
Root([1.99999, 2.00001], :unique)
Root([7.99999, 8.00001], :unique)
Root([4.99999, 5.00001], :unique)
Root([6.99999, 7.00001], :unique)
Root([8.99999, 9.00001], :unique)
Root([3.99999, 4.00001], :unique)
``````

The `:unique` shows there is a root and only 1 in the specified interval.
The choice of `20..20` can be widened without issue, and could be computed using some bounds on real roots based on coefficients.

To show the nesting, you might do something like this:

``````xs = IntervalRootFinding.roots(x->p(x), -20..20)
ys = IntervalRootFinding.roots(x->Polynomials.derivative(p)(x), -20..20)
xs = sort([i.interval for i in xs])# grab intervals only
ys = sort([i.interval for i in ys]);
all(xs[i] < ys[i] < xs[i+1] for i in 1:length(xs)-1) # true
``````
3 Likes

`IntervalRootFinding.roots` doesn’t seem to be able to take advantage of the fact that the coefficients are integers. I’m finding it chokes on relatively low degree polynomials.

``````using IntervalRootFinding, IntervalArithmetic, Polynomials
bp = [
942438915208811912419937422298363203125
164182217245953398816894035758761902846875
4584900574568933770264468813466870772155175
48995332393110515074735075708247882042540865
266674183150777010544241114017741621823207005
852443280934837985352088128423887894438557515
1738546146302892245736990844587487000484756535
2381558158813900978436742173305983349418813145
2262003889258248241081177038309445610985409335
1516025051068561122302699855213604175083575145
720810987764796866354279087485114863858085005
241341213302325116160821160849326697595681275
55563538585119205063994483187179167406616375
8363912237256094118085070946083688310200625
740493466743082745510080711751444519503125
29215606371473169285018060091249259296875]
intervalroots(a) = IntervalRootFinding.roots(x->a(x), -big(4)..big(0))
poly = Polynomials.Polynomial(bp)
@btime intervalroots(poly)
``````

this takes minutes at 100% CPU usage, though relative low memory. In contrast, in `Maple`, I can run

``````cl := [   942438915208811912419937422298363203125,
164182217245953398816894035758761902846875,
4584900574568933770264468813466870772155175,
48995332393110515074735075708247882042540865,
266674183150777010544241114017741621823207005,
852443280934837985352088128423887894438557515,
1738546146302892245736990844587487000484756535,
2381558158813900978436742173305983349418813145,
2262003889258248241081177038309445610985409335,
1516025051068561122302699855213604175083575145,
720810987764796866354279087485114863858085005,
241341213302325116160821160849326697595681275,
55563538585119205063994483187179167406616375,
8363912237256094118085070946083688310200625,
740493466743082745510080711751444519503125,
29215606371473169285018060091249259296875];
poly := add(cl[i]*x^(i-1), i=1..nops(cl));
realroot(poly,1/2^200);
``````

to get interval that isolate to intervals of width 2^(-200) in on the order of 1/50th of a second.

I have a naive, hacked together, Sturm chain approach in Julia that gets me much wider intervals in a couple of seconds, but I can’t see how I’m going to make up that many orders of magnitude of difference.

I’m way out of my depth here, but have you looked at https://en.wikipedia.org/wiki/Real-root_isolation?

1 Like

That looks like about what I need. The Recent Improvements section there, sure makes it look like there should be some interval arithmetic trick close by. I guess tomorrow’s project is to try and limit the number of intermediate values for all those change of variables clean when working with `BigInts`.

I don’t see how the fact that the coefficients are integers will help? But it’s very interesting that Maple is so fast.

You might also be interested in HomotopyContinuation.jl

1 Like

It seems that you can get some initial information quickly using Float64:

``````julia> bp2 = Interval{Float64}.(bp);

julia> poly2 = Polynomials.Polynomial(bp2);

julia> roots(x -> poly2(x), -Inf..Inf, Bisection, 1.0)
22-element Vector{Root{Interval{Float64}}}:
Root([-6.95784, -6.40872], :unknown)
Root([-9.129, -8.57124], :unknown)
[etc]
``````

Then hopefully refining with BigFloat will be faster.

For fast polynomial manipulations in Julia I believe you can use https://github.com/Nemocas/AbstractAlgebra.jl

If you have an approximate root, you can verify it, e.g.:

``````julia> using Roots

julia> rts = find_zeros(x -> poly(x), -10, 10);   # approximate: Roots.jl

julia> big_roots = roots(x -> poly(x), big(rts[3] ± 1e-5), Newton, 1e-50)    # rigorous: IntervalRootFinding.jl
1-element Vector{Root{Interval{BigFloat}}}:
Root([-3.39911, -3.3991]₂₅₆, :unique)

julia> diam(interval(big_roots[1]))
7.879417721966192043308957487195098830393286784342275257087148835580431618280549e-70
``````

This finds an interval of with ~10^(-69) in which a unique root is proved to exist.

You can also use `Hecke/Nemo`. Here are the timings on my very dated laptop:

``````julia> using Hecke

julia> Qx, x = PolynomialRing(QQ)
(Univariate Polynomial Ring in x over Rational Field, x)

julia> Qx, x = PolynomialRing(QQ);

julia> f = Qx(bp);

julia> Hecke._roots(f, 200) # the balls will have radious < 2^(-200)
15-element Array{acb,1}:
[-3.920616711153724253790204590529063281656171296624620351746200485310655120855543613013541271680907275607223979079 +/- 4.84e-112] + i*0
[-3.706111517064614641012473797293410340554769655675971950139715152216337896330719648922654909467811143497620428030 +/- 7.80e-112] + i*0
[-3.399101345956385184809590396663823245233910492969600172371668237761238821534096232299586114103416940774067560924 +/- 8.79e-112] + i*0
[-3.035412954945010421040655247164632911783923123071964813560272462311419306648072410827429032399236488318520657029 +/- 9.05e-112] + i*0
[-2.642241820837020072256321446404090852721123523348319017917035488525807038562259125380960186537327785108752839951 +/- 5.03e-112] + i*0
[-2.240386668347612606849921371192140625120311772030273906303162991172574142894086307045961801604703579352289007669 +/- 3.04e-112] + i*0
[-1.8459392275279762188118911328074682418106652190523122728768478452769447419809494076465189839407551707688825849549 +/- 9.17e-113] + i*0
[-1.4714364265485984347457103358546916040139602018877260092947033620076406186360124337515593788219523840023676498250 +/- 3.09e-113] + i*0
[-1.12667548938228648371659194465060333123610781848166932944102272185644329811383422404304887752307268162794786601481 +/- 6.41e-114] + i*0
[-0.819305999801997298040859319753994185201032318699276601025148883053951656453384749232848418410546346810549845454155 +/- 6.49e-115] + i*0
[-0.5552656322189970256982534594202473203239575869892228528269814900273624281956924496928014910474082527863381439566599 +/- 8.75e-116] + i*0
[-0.33910202510401947795464384494964958658273285483978651160525210708434910971533965735125598235860614612011137885597364 +/- 4.76e-117] + i*0
[-0.174209253846368310831930193848706110784111374105168275102590356950016921390554301061214369088829872621188752931599096 +/- 4.12e-118] + i*0
[-0.06299838108334910672357500092170790024222765409891867028284948240638078820689793485367480324463392976269126659634877413 +/- 9.09e-120] + i*0
[-0.007015398195279156379264993894770551409664575420748371105657755069724334773977198396431121866041220489066572460776894302 +/- 2.04e-121] + i*0

julia> @btime Hecke._roots(f, 200);
4.501 ms (3677 allocations: 204.58 KiB)
``````

This is internally using http://arblib.org/.

P.S. To convert to `BigFloat` you can do `BigFloat(real(x))`, but you must handle the global precision yourself.

2 Likes

I’m not really clear myself, I just know from browsing the `Maple` documentation, that they rely on integer (or rational) coefficients, presumably because it simplifies something. Page 2 of https://arxiv.org/pdf/1605.00410.pdf seems to suggest that one of the problems is being able to guarantee that it’s possible to determine the sign of a polynomial near its roots. It also seems (page 8, paragraph 2), that there might be some implications to a heuristic that switches between MPFI and exact arithmetic depending on which is expected to be cheaper for a given subproblem.

Another guarantee from integer coefficients is that it’s possible to pre-filter and ensure no rational roots, so we don’t have to worry about corner cases where a root falls exactly at an end-point of an interval.

2 Likes

Thanks. That’s definitely fast enough to fit into my pipeline.

I wanted to see if my initial suggestion to use `IntervalRootFinding` could be sped up paying more attention to the polynomial evaluation time and the underlying numbers used, so tried this:

``````using IntervalRootFinding, IntervalArithmetic, Polynomials
bp = [
942438915208811912419937422298363203125
164182217245953398816894035758761902846875
4584900574568933770264468813466870772155175
48995332393110515074735075708247882042540865
266674183150777010544241114017741621823207005
852443280934837985352088128423887894438557515
1738546146302892245736990844587487000484756535
2381558158813900978436742173305983349418813145
2262003889258248241081177038309445610985409335
1516025051068561122302699855213604175083575145
720810987764796866354279087485114863858085005
241341213302325116160821160849326697595681275
55563538585119205063994483187179167406616375
8363912237256094118085070946083688310200625
740493466743082745510080711751444519503125
29215606371473169285018060091249259296875]

bps = [convert(ArbFloat, string(b)) for b in bp]
p = ImmutablePolynomial(bps)
# error with show
# Error showing value of type Tuple{ArbFloat{116},ArbFloat{116}}:
# ERROR: MethodError: no method matching Array{String,1}(::Int64)
a,b = -convert(ArbFloat, "4"), convert(ArbFloat, "0");
IntervalRootFinding.roots(f,a..b) # error about a..b
#ERROR: MethodError: no method matching ArbFloat{116}(::String)
IntervalRootFinding.roots(f,Interval(a,b))
# ERROR: MethodError: *(::ArbFloat{116}, ::Interval{ArbFloat{116}}) is ambiguous. Candidates:
``````

So a bunch of errors, the last one non-circumventable. I could file issues with `ArbFloats` for the first two, but the method ambiguity doesn’t have a natural place to resolve.