Skip to content

Commit 48d62e2

Browse files
authored
Merge pull request #260 from jverzani/multroot
add multroot function
2 parents d8111d0 + 473f53b commit 48d62e2

File tree

4 files changed

+338
-17
lines changed

4 files changed

+338
-17
lines changed

src/Polynomials.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ include("polynomials/ImmutablePolynomial.jl")
2020
include("polynomials/SparsePolynomial.jl")
2121
include("polynomials/LaurentPolynomial.jl")
2222
include("polynomials/ngcd.jl")
23+
include("polynomials/multroot.jl")
2324

2425
include("polynomials/ChebyshevT.jl")
2526

src/polynomials/multroot.jl

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
module Multroot
2+
3+
export multroot
4+
5+
using ..Polynomials
6+
using LinearAlgebra
7+
8+
"""
9+
multroot(p; verbose=false, kwargs...)
10+
11+
Use `multroot` algorithm of
12+
[Zeng](https://www.ams.org/journals/mcom/2005-74-250/S0025-5718-04-01692-8/S0025-5718-04-01692-8.pdf)
13+
to identify roots of polynomials with suspected multiplicities over
14+
`Float64` values, typically.
15+
16+
Example:
17+
18+
```jldoctest
19+
julia> using Polynomials
20+
21+
julia> p = fromroots([sqrt(2), sqrt(2), sqrt(2), 1, 1])
22+
Polynomial(-2.8284271247461907 + 11.656854249492383*x - 19.07106781186548*x^2 + 15.485281374238573*x^3 - 6.242640687119286*x^4 + 1.0*x^5)
23+
24+
julia> roots(p)
25+
5-element Array{Complex{Float64},1}:
26+
0.999999677417768 + 0.0im
27+
1.0000003225831504 + 0.0im
28+
1.4141705716005981 + 0.0im
29+
1.4142350577588885 - 3.72273772278647e-5im
30+
1.4142350577588885 + 3.72273772278647e-5im
31+
32+
julia> Polynomials.Multroot.multroot(p)
33+
(values = [0.9999999999999993, 1.4142135623730958], multiplicities = [2, 3], κ = 5.218455674370637, ϵ = 2.8736226244218195e-16)
34+
```
35+
36+
The algorithm has two stages. First it uses `pejorative_manifold` to
37+
identify the number of distinct roots and their multiplicities. This
38+
uses the fact if `p=Π(x-zᵢ)ˡⁱ`, `u=gcd(p, p′)`, and `u⋅v=p` then
39+
`v=Π(x-zi)` is square free and contains the roots of `p` and `u` holds
40+
the multiplicity details, which are identified by recursive usage of
41+
`ncgd`, which identifies `u` and `v` above even if numeric
42+
uncertainties are present.
43+
44+
Second it uses `pejorative_root` to improve a set of initial guesses
45+
for the roots under the assumption the multiplicity structure is
46+
correct using a Newton iteration scheme.
47+
48+
The following tolerances, passed through to `pejorative_manifold` by
49+
`kwargs...`, are all used in the first stage, to identify the
50+
multiplicity structure:
51+
52+
* `θ`: the singular value threshold, set to `1e-8`. This is used by
53+
`Polynomials.ngcd` to assess if a matrix is rank deficient by
54+
comparing the smallest singular value to `θ ⋅ ||p||₂`.
55+
56+
* `ρ`: the initial residual tolerance, set to `1e-10`. This is passed
57+
to `Polynomials.ngcd`, the GCD finding algorithm as a relative tolerance.
58+
59+
* `ϕ`: A scale factor, set to `100`. As the `ngcd` algorithm is called
60+
recursively, this allows the residual tolerance to scale up to match
61+
increasing numeric errors.
62+
63+
Returns a named tuple with
64+
65+
* `values`: the identified roots
66+
* `multiplicities`: the corresponding multiplicities
67+
* `κ`: the estimated condition number
68+
* `ϵ`: the backward error, `||p̃ - p||_w`.
69+
70+
If the wrong multiplicity structure is identified in step 1, then
71+
typically either `κ` or `ϵ` will be large. The estimated forward
72+
error, `||z̃ - z||₂`, is bounded up to higher order terms by `κ ⋅ ϵ`.
73+
This will be displayed if `verbose=true` is specified.
74+
75+
For polynomials of degree 20 or higher, it is often the case the `l`
76+
is misidentified.
77+
78+
"""
79+
function multroot(p::Polynomials.StandardBasisPolynomial{T}; verbose=false,
80+
kwargs...) where {T}
81+
82+
z, l = pejorative_manifold(p; kwargs...)
83+
= pejorative_root(p, z, l)
84+
κ, ϵ = stats(p, z̃, l)
85+
86+
verbose && show_stats(κ, ϵ)
87+
88+
(values = z̃, multiplicities = l, κ = κ, ϵ = ϵ)
89+
90+
end
91+
92+
# The multiplicity structure, `l`, gives rise to a pejorative manifold `Πₗ = {Gₗ(z) | z∈ Cᵐ}`.
93+
function pejorative_manifold(p::Polynomials.StandardBasisPolynomial{T};
94+
ρ = 1e-10, # initial residual tolerance
95+
θ = 1e-8, # zero singular-value threshold
96+
ϕ = 100) where {T}
97+
98+
zT = zero(float(real(T)))
99+
u, v, w, θ′, κ = Polynomials.ngcd(p, derivative(p),
100+
atol=ρ*norm(p), satol = θ*norm(p),
101+
rtol = zT, srtol = zT)
102+
103+
zs = roots(v)
104+
nrts = length(zs)
105+
ls = ones(Int, nrts)
106+
107+
while !Polynomials.isconstant(u)
108+
109+
normp = 1 + norm(u, 2)
110+
ρ′ = max*normp, ϕ * θ′) # paper uses just latter
111+
u, v, w, θ′, κ = Polynomials.ngcd(u, derivative(u),
112+
atol=ρ′, satol = θ*normp,
113+
rtol = zT, srtol = zT,
114+
minⱼ = degree(u) - nrts # truncate search
115+
)
116+
117+
rts = roots(v)
118+
nrts = length(rts)
119+
120+
for z in rts
121+
tmp, ind = findmin(abs.(zs .- z))
122+
ls[ind] = ls[ind] + 1
123+
end
124+
125+
end
126+
127+
zs, ls # estimate for roots, multiplicities
128+
129+
end
130+
131+
"""
132+
pejorative_root(p, zs, ls; kwargs...)
133+
134+
Find a *pejorative* *root* for `p` given multiplicity structure `ls` and initial guess `zs`.
135+
136+
The pejorative manifold for a multplicity structure `l` is denoted `{Gₗ(z) | z ∈ Cᵐ}`. A pejorative
137+
root is a least squares minimizer of `F(z) = W ⋅ [Gₗ(z) - a]`. Here `a ~ (p_{n-1}, p_{n-2}, …, p_1, p_0) / p_n` and `W` are weights given by `min(1, 1/|aᵢ|)`. When `l` is the mathematically correct structure, then `F` will be `0` for a pejorative root. If `l` is not correct, then the backward error `||p̃ - p||_w` is typically large, where `p̃ = Π (x-z̃ᵢ)ˡⁱ`.
138+
139+
This follows Algorithm 1 of [Zeng](https://www.ams.org/journals/mcom/2005-74-250/S0025-5718-04-01692-8/S0025-5718-04-01692-8.pdf)
140+
"""
141+
function pejorative_root(p::Polynomials.StandardBasisPolynomial,
142+
zs::Vector{S}, ls::Vector{Int}; kwargs...) where {S}
143+
ps = (reverse coeffs)(p)
144+
pejorative_root(ps, zs, ls; kwargs...)
145+
end
146+
147+
# p is [1, a_{n-1}, a_{n-2}, ..., a_1, a_0]
148+
function pejorative_root(p, zs::Vector{S}, ls::Vector{Int};
149+
τ = eps(float(real(S)))^(2/3),
150+
maxsteps = 100, # linear or quadratic
151+
verbose=false
152+
) where {S}
153+
154+
## Solve WJ Δz = W(Gl(z) - a)
155+
## using weights min(1/|aᵢ|), i ≠ 1
156+
157+
m,n = sum(ls), length(zs)
158+
159+
# storage
160+
a = p[2:end]./p[1] # a ~ (p[n-1], p[n-2], ..., p[0])/p[n]
161+
W = Diagonal([min(1, 1/abs(aᵢ)) for aᵢ in a])
162+
J = zeros(S, m, n)
163+
G = zeros(S, 1 + m)
164+
Δₖ = zeros(S, n)
165+
zₖs = copy(zs) # updated
166+
167+
cvg = false
168+
δₖ₀ = -Inf
169+
for ctr in 1:maxsteps
170+
171+
evalJ!(J, zₖs, ls)
172+
evalG!(G, zₖs, ls)
173+
174+
Δₖ .= (W*J) \ (W*(view(G, 2:1+m) .- a)) # weighted least squares
175+
176+
δₖ₁ = norm(Δₖ, 2)
177+
Δ = δₖ₀ - δₖ₁
178+
179+
if ctr > 10 && δₖ₁ >= δₖ₀
180+
δₖ₀ < δₖ₁ && @warn "Increasing Δ, terminating search"
181+
cvg = true
182+
break
183+
end
184+
185+
zₖs .-= Δₖ
186+
187+
if δₖ₁^2 <= (δₖ₀ - δₖ₁) * τ
188+
cvg = true
189+
break
190+
end
191+
192+
δₖ₀ = δₖ₁
193+
end
194+
verbose && show_stats(stats(p, zₖs, ls)...)
195+
196+
if cvg
197+
return zₖs
198+
else
199+
@info ("""
200+
The multiplicity count may be in error: the initial guess for the roots failed
201+
to converge to a pejorative root.
202+
""")
203+
return(zₘs)
204+
end
205+
206+
end
207+
208+
# Helper: compute Π (x-zᵢ)^lᵢ directly
209+
function evalG!(G, zs::Vector{T}, ls::Vector) where {T}
210+
211+
G .= zero(T)
212+
G[1] = one(T)
213+
214+
ctr = 1
215+
for (z,l) in zip(zs, ls)
216+
for _ in 1:l
217+
for j in length(G)-1:-1:1#ctr
218+
G[1 + j] -= z * G[j]
219+
end
220+
ctr += 1
221+
end
222+
end
223+
224+
nothing
225+
end
226+
227+
# For eachcolumn, computes (3.8)
228+
# -lⱼ * (x - xⱼ)^(lⱼ-1) * Π_{k ≠ j} (x - zₖ)^lₖ
229+
function evalJ!(J, zs::Vector{T}, ls::Vector) where {T}
230+
231+
J .= 0
232+
n, m = sum(ls), length(zs)
233+
234+
evalG!(view(J, 1:1+n-m, 1), zs, ls .- 1)
235+
for (j′, lⱼ) in enumerate(reverse(ls))
236+
for i in 1+n-m:-1:1
237+
J[i, m - j′ + 1] = -lⱼ * J[i, 1]
238+
end
239+
end
240+
241+
for (j, lⱼ) in enumerate(ls)
242+
for (k, zₖ) in enumerate(zs)
243+
k == j && continue
244+
for i in n-1:-1:1
245+
J[1+i,j] -= zₖ * J[i,j]
246+
end
247+
end
248+
end
249+
nothing
250+
end
251+
252+
# Defn (3.5) condition number of z with respect to the multiplicity l and weight W
253+
cond_zl(p::AbstractPolynomial, zs::Vector{S}, ls) where {S} = cond_zl(reverse(coeffs(p)), zs, ls)
254+
function cond_zl(p, zs::Vector{S}, ls) where {S}
255+
J = zeros(S, sum(ls), length(zs))
256+
W = diagm(0 => [min(1, 1/abs(aᵢ)) for aᵢ in p[2:end]])
257+
evalJ!(J, zs, ls)
258+
F = qr(W*J)
259+
_, σ, _ = Polynomials.NGCD.smallest_singular_value(F.R)
260+
1 / σ
261+
end
262+
263+
backward_error(p::AbstractPolynomial, zs::Vector{S}, ls) where {S} =
264+
backward_error(reverse(coeffs(p)), zs, ls)
265+
function backward_error(p, z̃s::Vector{S}, ls) where {S}
266+
# || ã - a ||w
267+
G = zeros(S, 1 + sum(ls))
268+
evalG!(G, z̃s, ls)
269+
a = p[2:end]./p[1]
270+
W = Diagonal([min(1, 1/abs(aᵢ)) for aᵢ in a])
271+
u = G[2:end] .- a
272+
norm(W*u,2)
273+
end
274+
275+
function stats(p, zs, ls)
276+
cond_zl(p, zs, ls), backward_error(p, zs, ls)
277+
end
278+
279+
function show_stats(κ, ϵ)
280+
println("")
281+
println("Condition number κ(z; l) = ", κ)
282+
println("Backward error ||ã - a||w = ", ϵ)
283+
println("Estimated forward root error ||z̃ - z||₂ = ", κ * ϵ)
284+
println("")
285+
end
286+
287+
end

0 commit comments

Comments
 (0)