|
| 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 | + z̃ = 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