Skip to content

Commit ee000fb

Browse files
authored
Fixes for Newton-type diagonalization (#42)
* Fixes for Newton-type diagonalization * Fix format * Fix build * Fix format
1 parent 8f564bb commit ee000fb

File tree

5 files changed

+158
-96
lines changed

5 files changed

+158
-96
lines changed

src/cluster.jl

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""
2+
cluster_eigenvalues(_atol, v)
3+
4+
Clustering the values `v` following [CGT97].
5+
6+
[CGT97] Corless, R. M.; Gianni, P. M. & Trager, B. M.
7+
*A reordered Schur factorization method for zero-dimensional polynomial systems with multiple roots*
8+
Proceedings of the 1997 international symposium on Symbolic and algebraic computation, 1997, 133-140
9+
"""
10+
function cluster_eigenvalues(_atol, v)
11+
A = typeof(_atol(1))
12+
V = real(eltype(v))
13+
14+
ONE = one(one(V) / one(A))
15+
16+
clusters = Vector{Int}[]
17+
λ = V[]
18+
atol = A[]
19+
# condition_number requires that conjugate pair need to be treated together so we first need to handle them
20+
# If they are in the same cluster then pair them, otherwise it is complex solution so we reject them
21+
i = firstindex(v)
22+
while i <= lastindex(v)
23+
if isreal(v[i])
24+
push!(clusters, [i])
25+
push!(λ, real(v[i]))
26+
push!(atol, _atol(i))
27+
i += 1
28+
else
29+
@assert i < lastindex(v) && !isreal(v[i+1])
30+
pairatol = _atol([i, i + 1])
31+
if abs(v[i] - v[i+1]) / pairatol < ONE
32+
# Pair conjugate pairs into a real eigenvalue
33+
push!(clusters, [i, i + 1])
34+
push!(λ, real((v[i] + v[i+1]) / 2)) # The imaginary part should be zero anyway
35+
push!(atol, pairatol)
36+
end
37+
i += 2
38+
end
39+
end
40+
41+
# For eigenvalues not clustered yet, their eigenvalues is quite large.
42+
# Therefore, if we cluster all i, j close enough at once we might cluster too much
43+
# The technique used here is to cluster only the closest pair.
44+
# Once they are matched, a new atol is computed and if the cluster is complete,
45+
# this atol will be small which will avoid addition of new eigenvalues.
46+
while true
47+
σ = sortperm(λ)
48+
I = 0
49+
J = 0
50+
best = ONE
51+
for _i in eachindex(σ)
52+
i = σ[_i]
53+
for _j in 1:(_i-1)
54+
j = σ[_j]
55+
d = abs(λ[i] - λ[j]) / min(atol[i], atol[j])
56+
if d < best
57+
I = i
58+
J = j
59+
best = d
60+
end
61+
end
62+
end
63+
if best < ONE
64+
# merge I with J
65+
nI = length(clusters[I])
66+
nJ = length(clusters[J])
67+
λ[I] = (λ[I] * nI + λ[J] * nJ) / (nI + nJ)
68+
append!(clusters[I], clusters[J])
69+
atol[I] = _atol(clusters[I])
70+
deleteat!(λ, J)
71+
deleteat!(clusters, J)
72+
deleteat!(atol, J)
73+
else
74+
break
75+
end
76+
end
77+
78+
return clusters
79+
end

src/multiplication_matrices.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ function solve(
2020
return _solve_multiplication_matrices(Ms.matrices, λ, solver)
2121
end
2222

23+
include("cluster.jl")
2324
include("schur.jl")
2425
include("newton_type.jl")
2526

src/newton_type.jl

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,13 @@ export NewtonTypeDiagonalization
44

55
# norm of off diagonal terms of a square matrix
66
function norm_off(M)
7-
if size(M[1], 1) > 1
7+
n = LinearAlgebra.checksquare(M)
8+
if n > 1
89
return sqrt(
9-
sum(
10-
abs2(M[i, j]) + abs2(M[j, i]) for i in 1:size(M, 1) for
11-
j in i+1:size(M, 1)
12-
),
10+
sum(abs2(M[i, j]) + abs2(M[j, i]) for i in 1:n for j in i+1:n),
1311
)
1412
else
15-
return 0.0
13+
return zero(eltype(M))
1614
end
1715
end
1816

@@ -81,12 +79,12 @@ end
8179
NewtonTypeDiagonalization() = NewtonTypeDiagonalization(10, 1e-3, 5e-2)
8280

8381
function _eigvecs(M::AbstractMatrix{BigFloat})
84-
ev = LinearAlgebra.schur(Float64.(M)).vectors
85-
# `eigvecs` is failing some tests with a non-invertible `ev`
86-
#ev = LinearAlgebra.eigvecs(Float64.(M))
82+
ev = _eigvecs(Float64.(M))
8783
return convert(Matrix{BigFloat}, ev)
8884
end
89-
_eigvecs(M::AbstractMatrix) = LinearAlgebra.eigvecs
85+
# `eigvecs` is failing some tests with a non-invertible `ev`
86+
#_eigvecs(M::AbstractMatrix) = LinearAlgebra.eigvecs(M)
87+
_eigvecs(M::AbstractMatrix) = LinearAlgebra.schur(M).vectors
9088

9189
function _solve_multiplication_matrices(M, λ, solver::NewtonTypeDiagonalization)
9290
@assert length(M) == length(λ)
@@ -134,4 +132,30 @@ function _solve_multiplication_matrices(M, λ, solver::NewtonTypeDiagonalization
134132
end
135133

136134
return [[D[j+1][i, i] / D[1][i, i] for j in 1:n] for i in 1:r]
135+
136+
# # I implemented this when I was analysing the result after only zero iteration so unsure if it's useful
137+
# d = LinearAlgebra.Diagonal(sqrt.(inv.(LinearAlgebra.diag(D[1]))))
138+
# Λ = [d * D[j+1] * d for j in 1:n]
139+
140+
# # `Λ` can be decomposed into blocks corresponding to the same eigenvalue
141+
# # These blocks can have off-diagonal entries so we further diagonalize
142+
# # with
143+
# # FIXME `solver.tol` or `solver.ε` or ?
144+
# sub_solver = ReorderedSchurMultiplicationMatricesSolver(solver.ε, solver.rng)
145+
# sols = Vector{eltype(Λ[1])}[]
146+
# i = 1
147+
# while i <= r
148+
# j = findfirst((i+1):r) do j
149+
# all(1:n) do k
150+
# # FIXME `solver.tol` or `solver.ε` or ?
151+
# return !isapprox(Λ[k][j, j], Λ[k][i, i], rtol = solver.ε)
152+
# end
153+
# end
154+
# j = something(j, r - i + 1)
155+
# I = i:(i+j-1)
156+
# sub_matrices = MultiplicationMatrices([Λ[j][I, I] for j in 1:n])
157+
# append!(sols, solve(sub_matrices, sub_solver))
158+
# i += j
159+
# end
160+
# return sols
137161
end

src/schur.jl

Lines changed: 4 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -34,78 +34,12 @@ end
3434
function _clusterordschur(M::AbstractMatrix{<:Real}, ɛ)
3535
# M = Z * T * Z' and "values" gives the eigenvalues
3636
sf = LinearAlgebra.schur(M)
37-
Z = sf.Z
38-
v = sf.values
3937
# documentation says that the error on the eigenvalues is ɛ * norm(T) / condition_number
4038
nT = norm(sf.T)
41-
42-
_atol(I) = ɛ * nT / condition_number(sf, I)
43-
44-
A = typeof(_atol(1))
45-
V = real(eltype(v))
46-
47-
ONE = one(one(V) / one(A))
48-
49-
clusters = Vector{Int}[]
50-
λ = V[]
51-
atol = A[]
52-
# condition_number requires that conjugate pair need to be treated together so we first need to handle them
53-
# If they are in the same cluster then pair them, otherwise it is complex solution so we reject them
54-
i = firstindex(v)
55-
while i <= lastindex(v)
56-
if isreal(v[i])
57-
push!(clusters, [i])
58-
push!(λ, v[i])
59-
push!(atol, _atol(i))
60-
i += 1
61-
else
62-
@assert i < lastindex(v) && !isreal(v[i+1])
63-
pairatol = _atol([i, i + 1])
64-
if abs(v[i] - v[i+1]) / pairatol < ONE
65-
# Pair conjugate pairs into a real eigenvalue
66-
push!(clusters, [i, i + 1])
67-
push!(λ, real((v[i] + v[i+1]) / 2)) # The imaginary part should be zero anyway
68-
push!(atol, pairatol)
69-
end
70-
i += 2
71-
end
72-
end
73-
σ = sortperm(λ)
74-
75-
# For eigenvalues not clustered yet, their eigenvalues is quite large.
76-
# Therefore, if we cluster all i, j close enough at once we might cluster too much
77-
# The technique used here is to cluster only the closest pair.
78-
# Once they are matched, a new atol is computed and if the cluster is complete,
79-
# this atol will be small which will avoid addition of new eigenvalues.
80-
while true
81-
I = 0
82-
J = 0
83-
best = ONE
84-
for i in eachindex(clusters)
85-
for j in 1:(i-1)
86-
d = abs(λ[i] - λ[j]) / min(atol[i], atol[j])
87-
if d < best
88-
I = i
89-
J = j
90-
best = d
91-
end
92-
end
93-
end
94-
if best < ONE
95-
# merge I with J
96-
nI = length(clusters[I])
97-
nJ = length(clusters[J])
98-
λ[I] = (λ[I] * nI + λ[J] * nJ) / (nI + nJ)
99-
append!(clusters[I], clusters[J])
100-
atol[I] = _atol(clusters[I])
101-
deleteat!(λ, J)
102-
deleteat!(clusters, J)
103-
deleteat!(atol, J)
104-
else
105-
break
106-
end
39+
clusters = cluster_eigenvalues(sf.values) do I
40+
return ɛ * nT / condition_number(sf, I)
10741
end
108-
return Z, clusters
42+
return sf.Z, clusters
10943
end
11044

11145
"""
@@ -143,7 +77,7 @@ function _solve_multiplication_matrices(
14377
n = length(λ)
14478
Z, clusters = clusterordschur(sum.* Ms), solver.ɛ)
14579
r = length(clusters)
146-
vals = [zeros(T, n) for k in 1:r]
80+
vals = [zeros(T, n) for _ in 1:r]
14781
for k in 1:r
14882
nk = length(clusters[k])
14983
for j in clusters[k]

test/solve.jl

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ end
2626
-1 0 1 -2 -1 0
2727
-1 0 1 -2 -2 -1
2828
]
29-
@test sort.(SemialgebraicSets.clusterordschur(A, sqrt(eps(Float64)))[2]) ==
30-
[[2], [1, 5, 6]]
29+
clusters = SemialgebraicSets.clusterordschur(A, sqrt(eps(Float64)))[2]
30+
@test sort(sort.(clusters); by = first) == [[1, 5, 6], [2]]
3131
end
3232

3333
function testelements(X, Y; atol = Base.rtoldefault(Float64), kwargs...)
@@ -61,39 +61,43 @@ newton_solver = NewtonTypeDiagonalization()
6161

6262
function zero_dimensional_ideal(solver)
6363
Mod.@polyvar x y z
64+
6465
V = @set x == y
6566
@test !is_zero_dimensional(V)
6667
@test_throws ErrorException iterate(V)
6768
@test_throws ErrorException length(V)
69+
6870
V = @set 4x^2 == -5x && 3x^3 == 0 solver
6971
@test V.solver.solver === solver
7072
@test is_zero_dimensional(V)
7173
testelementstypes(V, [[0]])
74+
7275
V = @set y == x^2 && z == x^3 solver
7376
@test !is_zero_dimensional(V)
74-
if solver isa ReorderedSchurMultiplicationMatricesSolver
75-
# FIXME NewtonType should cluster, it finds `(0, 0)` 3 times
77+
78+
if !(solver isa NewtonTypeDiagonalization)
7679
V = @set x^3 == 2x * y && x^2 * y == 2y^2 + x solver
7780
@test is_zero_dimensional(V)
7881
testelementstypes(V, [[0, 0]])
7982
end
83+
8084
V = @set x == 1 solver
8185
@test is_zero_dimensional(V)
8286
testelementstypes(V, [[1]])
87+
8388
V = @set x == 1 && y == 2 solver
8489
@test is_zero_dimensional(V)
8590
testelementstypes(V, [[1, 2]])
91+
8692
V = @set x == 4 && y^2 == x solver
8793
@test is_zero_dimensional(V)
8894
testelementstypes(V, [[4, 2], [4, -2]])
95+
8996
V = @set x^2 + x == 6 && y == x + 1 solver
9097
@test is_zero_dimensional(V)
9198
testelements(V, [[2, 3], [-3, -2]])
92-
if solver isa ReorderedSchurMultiplicationMatricesSolver
93-
# FIXME NewtonType finds:`
94-
# [[2, √2], [-3, 0], [-3, 0], [2, -√2]]
95-
# The `[-3, 0]` should be removed, it corresponds
96-
# `[-3, √3 im]`, `[-3, -√3 im]` which is a complex pair
99+
100+
if !(solver isa NewtonTypeDiagonalization)
97101
V = @set x^2 + x == 6 && y^2 == x solver
98102
@test is_zero_dimensional(V)
99103
testelements(V, [[2, 2], [2, -√2]])
@@ -107,30 +111,37 @@ end
107111

108112
function projective_zero_dimensional_ideal(solver)
109113
Mod.@polyvar x y z
114+
110115
V = projective_algebraic_set([x - y], solver)
111116
@test is_zero_dimensional(V)
112117
testelementstypes(V, [[1, 1]])
118+
113119
V = @set x + y == z solver
114120
V.projective = true
115121
@test !is_zero_dimensional(V)
122+
116123
V = @set y == 2x solver
117124
V.projective = true
118125
@test is_zero_dimensional(V)
119126
testelementstypes(V, [[1, 2]])
127+
120128
V = @set x + y == y solver
121129
V.projective = true
122130
@test is_zero_dimensional(V)
123131
testelementstypes(V, [[0, 1]])
132+
124133
V = projective_algebraic_set([x + y - x])
125134
@test is_zero_dimensional(V)
126-
return testelementstypes(V, [[1, 0]])
135+
testelementstypes(V, [[1, 0]])
136+
return
127137
end
128138

129139
@testset "Projective zero-dimensional ideal" begin
130140
projective_zero_dimensional_ideal(schur_solver)
141+
projective_zero_dimensional_ideal(newton_solver)
131142
end
132143

133-
@testset "Example 5.1 of CGT97" begin
144+
function cgt96_e51(solver)
134145
ɛ = 1e-4
135146
= [
136147
1-ɛ 0
@@ -150,24 +161,33 @@ end
150161
Z Iɛ
151162
]
152163
α = 0.219
153-
testelements(
164+
return testelements(
154165
SemialgebraicSets._solve_multiplication_matrices(
155166
[A, B],
156167
[α, 1 - α],
157-
ReorderedSchurMultiplicationMatricesSolver{Float64}(),
168+
solver,
158169
),
159170
[[1.0, -1.0], [1.0, 1.0], [-1.0, 1.0]];
160171
rtol = 1e-7,
161172
)
162173
end
163174

164-
@testset "Example 4.3 of MD95" begin
175+
@testset "Example 5.1 of CGT97" begin
176+
@testset "Schur" begin
177+
cgt96_e51(schur_solver)
178+
end
179+
# @testset "Newton" begin
180+
# cgt96_e51(newton_solver)
181+
# end
182+
end
183+
184+
function md95_e43(solver)
165185
Mod.@polyvar x y
166-
V = @set x^2 + 4y^4 == 4y^2 && 3x^4 + y^2 == 5x^3 schur_solver
186+
V = @set x^2 + 4y^4 == 4y^2 && 3x^4 + y^2 == 5x^3 solver
167187
# This test is tricky because in the schur decomposition, the 4 last eigenvalues are e.g. 3.4e-7, -1.7e-7+3e-7im, -1.7e-7-3e-7im, -6e-16
168188
# the second and third do not seem that close but when the three first are averaged it is very close to zero.
169189
@test is_zero_dimensional(V)
170-
testelementstypes(
190+
return testelementstypes(
171191
V,
172192
[
173193
[0.66209555, 0.935259169],
@@ -179,6 +199,10 @@ end
179199
)
180200
end
181201

202+
@testset "Example 4.3 of MD95" begin
203+
md95_e43(schur_solver)
204+
end
205+
182206
@testset "Example 5.2 of CGT97" begin
183207
Mod.@polyvar x y z
184208
V =

0 commit comments

Comments
 (0)