Skip to content

Commit e413073

Browse files
authored
update documentation for roots, copy edit (#346)
* add docs on roots alternatives; square_free method * doc fix
1 parent e64e542 commit e413073

15 files changed

+357
-52
lines changed

docs/make.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ makedocs(
77
modules = [Polynomials],
88
format = Documenter.HTML(prettyurls = get(ENV, "CI", nothing) == "true"),
99
sitename = "Polynomials.jl",
10-
authors = "Jameson Nash, Keno Fischer, and other contributors",
10+
authors = "Jameson Nash, Keno Fischer, Miles Lucas, John Verzani, and other contributors",
1111
pages = [
1212
"Home" => "index.md",
1313
"Reference/API" => "reference.md",

docs/src/extending.md

+1
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,4 @@ julia> p .+ 2
147147
6
148148
```
149149

150+
The [`Polynomials.PnPolynomial`](@ref) type implements much of this.

docs/src/index.md

+262-13
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ differentiation, evaluation, and root finding over dense univariate polynomials.
66
To install the package, run
77

88
```julia
9-
(v1.2) pkg> add Polynomials
9+
(v1.6) pkg> add Polynomials
1010
```
1111

1212
The package can then be loaded into the current session using
@@ -138,9 +138,7 @@ Polynomial(3 - 2*x)
138138

139139
### Root-finding
140140

141-
Return the roots (zeros) of `p`, with multiplicity. The number of
142-
roots returned is equal to the order of `p`. By design, this is not type-stable,
143-
the returned roots may be real or complex.
141+
Return the `d` roots (or zeros) of the degree `d` polynomial `p`.
144142

145143
```jldoctest
146144
julia> roots(Polynomial([1, 0, -1]))
@@ -159,18 +157,269 @@ julia> roots(Polynomial([0, 0, 1]))
159157
0.0
160158
```
161159

162-
For polynomials with multplicities, the non-exported `Polynomials.Multroot.multroot` method can avoid some numerical issues that `roots` will have.
160+
By design, this is not type-stable; the returned roots may be real or complex.
163161

164-
The `FactoredPolynomial` type stores the roots (with multiplicities) and the leading coefficent of a polynomial. In this example, the `multroot` method is used internally to identify the roots of `p` below, in the conversion from the `Polynomial` type to the `FactoredPolynomial` type:
162+
The default `roots` function uses the eigenvalues of the
163+
[companion](https://en.wikipedia.org/wiki/Companion_matrix) matrix for
164+
a polynomial. This is an `𝑶(n^3)` operation.
165165

166-
```jldoctest
167-
julia> p = Polynomial([24, -50, 35, -10, 1])
168-
Polynomial(24 - 50*x + 35*x^2 - 10*x^3 + x^4)
166+
For polynomials with `BigFloat` coefficients, the
167+
`GenericLinearAlgebra` package can be seamlessly used:
168+
169+
```
170+
julia> p = fromroots(Polynomial{BigFloat}, [1,2,3])
171+
Polynomial(-6.0 + 11.0*x - 6.0*x^2 + 1.0*x^3)
172+
173+
julia> roots(p)
174+
ERROR: MethodError: no method matching eigvals!(::Matrix{BigFloat})
175+
[...]
176+
177+
julia> using GenericLinearAlgebra
178+
179+
julia> roots(p)
180+
3-element Vector{Complex{BigFloat}}:
181+
0.9999999999999999999999999999999999999999999999999999999999999999999999999999655 + 0.0im
182+
1.999999999999999999999999999999999999999999999999999999999999999999999999999931 - 0.0im
183+
2.999999999999999999999999999999999999999999999999999999999999999999999999999793 + 0.0im
184+
```
185+
186+
#### Comments on root finding
187+
188+
* The
189+
[PolynomialRoots.jl](https://github.com/giordano/PolynomialRoots.jl)
190+
package provides an alternative approach for finding complex roots
191+
to univariate polynomials that is more performant than `roots`. It
192+
is based on an algorithm of Skowron and Gould.
193+
194+
```
195+
julia> import PolynomialRoots # import as `roots` conflicts
196+
197+
julia> p = fromroots(Polynomial, [1,2,3])
198+
Polynomial(-6 + 11*x - 6*x^2 + x^3)
199+
200+
julia> PolynomialRoots.roots(coeffs(p))
201+
3-element Vector{ComplexF64}:
202+
3.000000000000001 - 0.0im
203+
1.9999999999999993 + 0.0im
204+
1.0000000000000002 + 0.0im
205+
```
206+
207+
The roots are always returned as complex numbers.
208+
209+
210+
* The
211+
[FastPolynomialRoots](https://github.com/andreasnoack/FastPolynomialRoots.jl)
212+
package provides an interface to FORTRAN code implementing an
213+
algorithm of Aurentz, Mach, Robol, Vandrebril, and Watkins. that can
214+
handle very large polynomials (it is `𝑶(n^2)` and backward
215+
stable). The [AMRVW.jl](https://github.com/jverzani/AMRVW.jl)
216+
package implements the algorithm in Julia, allowing the use of other
217+
number types.
218+
219+
```
220+
julia> import AMRVW # import as `roots` conflicts
221+
222+
julia> AMRVW.roots(float.(coeffs(p)))
223+
3-element Vector{ComplexF64}:
224+
0.9999999999999997 + 0.0im
225+
2.0000000000000036 + 0.0im
226+
2.9999999999999964 + 0.0im
227+
```
228+
229+
The roots are returned as complex numbers.
230+
231+
Both `PolynomialRoots` and `AMRVW` are generic and work with
232+
`BigFloat` coefficients, for example.
233+
234+
The `AMRVW` package works with much larger polynomials than either
235+
`roots` or `Polynomial.roots`. For example, the roots of this 1000
236+
degree random polynomial are quickly and accurately solved for:
237+
238+
```
239+
julia> filter(isreal, AMRVW.roots(rand(1001) .- 1/2))
240+
2-element Vector{ComplexF64}:
241+
0.993739974989572 + 0.0im
242+
1.0014677846996498 + 0.0im
243+
```
244+
245+
* The [Hecke](https://github.com/thofma/Hecke.jl/tree/master/src) package has a `roots` function. The `Hecke` package utilizes the `Arb` library for performant, high-precision numbers:
246+
247+
```
248+
julia> import Hecke # import as `roots` conflicts
249+
250+
julia> Qx, x = Hecke.PolynomialRing(Hecke.QQ)
251+
(Univariate Polynomial Ring in x over Rational Field, x)
252+
253+
julia> q = (x-1)*(x-2)*(x-3)
254+
x^3 - 6*x^2 + 11*x - 6
255+
256+
julia> Hecke.roots(q)
257+
3-element Vector{Nemo.fmpq}:
258+
2
259+
1
260+
3
261+
```
262+
263+
This next polynomial has 3 real roots, 2 of which are in a cluster; `Hecke` quickly identifies them:
264+
265+
```
266+
julia> p = -1 + 254*x - 16129*x^2 + 1*x^17
267+
x^17 - 16129*x^2 + 254*x - 1
268+
269+
julia> filter(isreal, Hecke._roots(p, 200)) # `_roots` not `roots`
270+
3-element Vector{Nemo.acb}:
271+
[0.007874015748031496052667730054749907629383970426203662570129818116411192289734968717460531379762086419 +/- 3.10e-103]
272+
[0.0078740157480314960733165219137540296086246589982151627453855179522742093785877068332663198273096875302 +/- 9.31e-104]
273+
[1.9066348541790688341521872066398429982632947292434604847312536201982593209326201234353468172497707769372732739429697289 +/- 7.14e-119]
274+
```
275+
276+
----
277+
278+
To find just the real roots of a polynomial with real coefficients there are a few additional options to solving for all the roots and filtering by `isreal`.
279+
280+
* The package
281+
[IntervalRootFinding](https://github.com/JuliaIntervals/IntervalRootFinding.jl/)
282+
identifies real zeros of univariate functions and can be used to find
283+
isolating intervals for the real roots. For example,
169284

170-
julia> q = convert(FactoredPolynomial, p) # noisy form of `factor`:
171-
FactoredPolynomial((x - 4.0000000000000036) * (x - 2.9999999999999942) * (x - 1.0000000000000002) * (x - 2.0000000000000018))
172285
```
286+
julia> using Polynomials, IntervalArithmetic
173287
288+
julia> import IntervalRootFinding # its `roots` method conflicts with `roots`
289+
290+
julia> p = fromroots(Polynomial, [1,2,3])
291+
Polynomial(-6 + 11*x - 6*x^2 + x^3)
292+
293+
julia> IntervalRootFinding.roots(x -> p(x), 0..10)
294+
3-element Vector{IntervalRootFinding.Root{Interval{Float64}}}:
295+
Root([0.999999, 1.00001], :unique)
296+
Root([1.99999, 2.00001], :unique)
297+
Root([2.99999, 3.00001], :unique)
298+
```
299+
300+
301+
302+
The output is a set of intervals. Those flagged with `:unique` are guaranteed to contain a unique root.
303+
304+
* The `RealPolynomialRoots` package provides a function `ANewDsc` to find isolating intervals for the roots of a square-free polynomial, specified through its coefficients:
305+
306+
```
307+
julia> using RealPolynomialRoots
308+
309+
julia> st = ANewDsc(coeffs(p))
310+
There were 3 isolating intervals found:
311+
[2.62…, 3.62…]₂₅₆
312+
[1.5…, 2.62…]₂₅₆
313+
[-0.50…, 1.5…]₂₅₆
314+
```
315+
316+
These isolating intervals can be refined to find numeric estimates for the roots over `BigFloat` values.
317+
318+
```
319+
julia> refine_roots(st)
320+
3-element Vector{BigFloat}:
321+
2.99999999999999999999...
322+
2.00000000000000000000...
323+
1.00000000000000000000...
324+
```
325+
326+
This specialized algorithm can identify very nearby roots. For example, returning to this Mignotte-type polynomial:
327+
328+
```
329+
julia> p = SparsePolynomial(Dict(0=>-1, 1=>254, 2=>-16129, 17=>1))
330+
SparsePolynomial(-1 + 254*x - 16129*x^2 + x^17)
331+
332+
julia> ANewDsc(coeffs(p))
333+
There were 3 isolating intervals found:
334+
[1.5…, 3.5…]₅₃
335+
[0.0078740157480314960682066…, 0.0078740157480314960873178…]₁₃₉
336+
[0.0078740157480314960492543…, 0.0078740157480314960682066…]₁₃₉
337+
```
338+
339+
`IntervalRootFinding` has issues disambiguating the clustered roots of this example:
340+
341+
```
342+
julia> IntervalRootFinding.roots(x -> p(x), 0..3.5)
343+
7-element Vector{IntervalRootFinding.Root{Interval{Float64}}}:
344+
Root([1.90663, 1.90664], :unique)
345+
Root([0.00787464, 0.00787468], :unknown)
346+
Root([0.00787377, 0.00787387], :unknown)
347+
Root([0.00787405, 0.00787412], :unknown)
348+
Root([0.00787396, 0.00787406], :unknown)
349+
Root([0.00787425, 0.00787431], :unknown)
350+
Root([0.00787394, 0.00787397], :unknown)
351+
```
352+
353+
For this example, `filter(isreal, Hecke._roots(p))` also isolates the three real roots, but not quite as quickly.
354+
355+
----
356+
357+
Most of the root finding algorithms have issues when the roots have
358+
multiplicities. For example, both `ANewDsc` and `Hecke.roots` assume a
359+
square free polynomial. For non-square free polynomials:
360+
361+
* The `Polynomials.Multroot.multroot` function is available (version v"1.2" or greater) for finding the roots of a polynomial and their multiplicities. This is based on work of Zeng.
362+
363+
Here we see `IntervalRootsFindings.roots` having trouble isolating the roots due to the multiplicites:
364+
365+
```
366+
julia> p = fromroots(Polynomial, [1,2,2,3,3])
367+
Polynomial(-36 + 96*x - 97*x^2 + 47*x^3 - 11*x^4 + x^5)
368+
369+
julia> IntervalRootFinding.roots(x -> p(x), 0..10)
370+
335-element Vector{IntervalRootFinding.Root{Interval{Float64}}}:
371+
Root([1.99999, 2], :unknown)
372+
Root([1.99999, 2], :unknown)
373+
Root([3, 3.00001], :unknown)
374+
Root([2.99999, 3], :unknown)
375+
376+
Root([2.99999, 3], :unknown)
377+
Root([2, 2.00001], :unknown)
378+
```
379+
380+
The `roots` function identifies the roots, but the multiplicities would need identifying:
381+
382+
```
383+
julia> roots(p)
384+
5-element Vector{Float64}:
385+
1.000000000000011
386+
1.9999995886034314
387+
2.0000004113969276
388+
2.9999995304339646
389+
3.0000004695656672
390+
```
391+
392+
393+
Whereas, the roots along with the multiplicity structure are correctly identified with `multroot`:
394+
395+
```
396+
julia> Polynomials.Multroot.multroot(p)
397+
(values = [1.0000000000000004, 1.9999999999999984, 3.0000000000000018], multiplicities = [1, 2, 2], κ = 35.11176306900731, ϵ = 0.0)
398+
```
399+
400+
The `square_free` function can help:
401+
402+
```
403+
julia> q = Polynomials.square_free(p)
404+
ANewDsc(q)
405+
Polynomial(-0.20751433915978448 + 0.38044295512633425*x - 0.20751433915986722*x^2 + 0.03458572319332053*x^3)
406+
407+
julia> IntervalRootFinding.roots(x -> q(x), 0..10)
408+
3-element Vector{IntervalRootFinding.Root{Interval{Float64}}}:
409+
Root([0.999999, 1.00001], :unique)
410+
Root([1.99999, 2.00001], :unique)
411+
Root([2.99999, 3.00001], :unique)
412+
```
413+
414+
Similarly:
415+
416+
```
417+
julia> ANewDsc(coeffs(q))
418+
There were 3 isolating intervals found:
419+
[2.62…, 3.62…]₂₅₆
420+
[1.5…, 2.62…]₂₅₆
421+
[-0.50…, 1.5…]₂₅₆
422+
```
174423

175424
### Fitting arbitrary data
176425

@@ -226,7 +475,7 @@ ChebyshevT(2.5⋅T_0(x) + 2.0⋅T_1(x) + 1.5⋅T_2(x))
226475

227476
### Iteration
228477

229-
If its basis is implicit, then a polynomial may be seen as just a vector of coefficients. Vectors or 1-based, but, for convenience, polynomial types are 0-based, for purposes of indexing (e.g. `getindex`, `setindex!`, `eachindex`). Iteration over a polynomial steps through the underlying coefficients.
478+
If its basis is implicit, then a polynomial may be seen as just a vector of coefficients. Vectors are 1-based, but, for convenience, most polynomial types are naturally 0-based, for purposes of indexing (e.g. `getindex`, `setindex!`, `eachindex`). Iteration over a polynomial steps through the underlying coefficients.
230479

231480
```jldoctest
232481
julia> as = [1,2,3,4,5]; p = Polynomial(as);
@@ -336,7 +585,7 @@ If `q` is non-constant, such as `variable(Polynomial, :y)`, then there would be
336585

337586
The same conversion is done for polynomial multiplication: constant polynomials are treated as numbers; non-constant polynomials must have their symbols match.
338587

339-
There is an oddity though the following two computations look the same, they are technically different:
588+
There is an oddity -- though the following two computations look the same, they are technically different:
340589

341590
```jldoctest natural_inclusion
342591
julia> one(Polynomial, :x) + one(Polynomial, :y)

docs/src/polynomials/polynomial.md

+25
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,39 @@ DocTestSetup = quote
88
end
99
```
1010

11+
## Polynomial
12+
1113
```@docs
1214
Polynomial
1315
```
1416

17+
## Immutable Polynomial
18+
1519
```@docs
1620
ImmutablePolynomial
21+
```
22+
23+
## Sparse Polynomial
24+
25+
```@docs
1726
SparsePolynomial
27+
```
28+
29+
## Laurent Polynomial
30+
31+
```@docs
1832
LaurentPolynomial
1933
```
2034

35+
## Factored Polynomial
36+
37+
```@docs
38+
FactoredPolynomial
39+
```
40+
41+
## Rational Function
42+
```@docs
43+
RationalFunction
44+
```
45+
2146

src/common.jl

+10-4
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,17 @@ _wlstsq(vand, y, W::AbstractMatrix) = qr(vand' * W * vand) \ (vand' * W * y)
168168
"""
169169
roots(::AbstractPolynomial; kwargs...)
170170
171-
Returns the roots of the given polynomial. This is calculated via the eigenvalues of the companion matrix. The `kwargs` are passed to the `LinearAlgeebra.eigvals` call.
171+
Returns the roots, or zeros, of the given polynomial.
172172
173-
!!! note
173+
This is calculated via the eigenvalues of the companion matrix. The `kwargs` are passed to the `LinearAlgeebra.eigvals` call.
174+
175+
!!! Note
176+
177+
The default `roots` implementation is for polynomials in the
178+
standard basis. The companion matrix approach is reasonably fast
179+
and accurate for modest-size polynomials. However, other packages
180+
in the `Julia` ecosystem may be of interest and are mentioned in the documentation.
174181
175-
The [PolynomialRoots.jl](https://github.com/giordano/PolynomialRoots.jl) package provides an alternative that is a bit faster and a bit more accurate; the [FastPolynomialRoots](https://github.com/andreasnoack/FastPolynomialRoots.jl) provides an interface to FORTRAN code implementing an algorithm that can handle very large polynomials (it is `O(n^2)` not `O(n^3)`. The [AMRVW.jl](https://github.com/jverzani/AMRVW.jl) package implements the algorithm in Julia, allowing the use of other number types.
176182
177183
"""
178184
function roots(q::AbstractPolynomial{T}; kwargs...) where {T <: Number}
@@ -544,7 +550,7 @@ isconstant(p::AbstractPolynomial) = degree(p) <= 0
544550
"""
545551
coeffs(::AbstractPolynomial)
546552
547-
Return the coefficient vector `[a_0, a_1, ..., a_n]` of a polynomial.
553+
Return the coefficient vector. For a standard basis polynomial these are `[a_0, a_1, ..., a_n]`.
548554
"""
549555
coeffs(p::AbstractPolynomial) = p.coeffs
550556

0 commit comments

Comments
 (0)