Skip to content

Commit d44bac4

Browse files
authored
Merge pull request #89 from haampie/fix-schur-nearly-repeated-eigenvalue-issue
Fix issue with nearly repeated eigenvalue in Schur factorization
2 parents fb898a5 + f843ce5 commit d44bac4

File tree

6 files changed

+109
-28
lines changed

6 files changed

+109
-28
lines changed

src/run.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ function partition_schur_three_way!(R, Q, groups::AbstractVector{Int})
333333
mi = 1
334334
lo = 1
335335

336+
# Be very scared whenever eigenvalues are too close!
336337
while hi length(groups)
337338
group = groups[hi]
338339
blocksize = is_start_of_11_block(R, hi) ? 1 : 2

src/schurfact.jl

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import LinearAlgebra: lmul!, rmul!
55
import Base: Matrix
66

77
@propagate_inbounds is_offdiagonal_small(H::AbstractMatrix{T}, i::Int, tol = eps(real(T))) where {T} =
8-
abs(H[i+1,i]) < tol*(abs(H[i,i]) + abs(H[i+1,i+1]))
8+
abs(H[i+1,i]) tol*(abs(H[i,i]) + abs(H[i+1,i+1]))
99

1010

1111
abstract type SmallRotation end
@@ -352,7 +352,7 @@ function local_schurfact!(H::AbstractMatrix{T}, start::Int, to::Int,
352352
H[from,from-1] = zero(T)
353353
to -= 1
354354
else
355-
# Now we are sure we can work with a 2x2 block H[to-1:to,to-1:to]
355+
# Now we are sure we can work with a 2×2 block H[to-1:to,to-1:to]
356356
# We check if this block has a conjugate eigenpair, which might mean we have
357357
# converged w.r.t. this block if from + 1 == to.
358358
# Otherwise, if from + 1 < to, we do either a single or double shift, based on
@@ -361,22 +361,30 @@ function local_schurfact!(H::AbstractMatrix{T}, start::Int, to::Int,
361361
H₁₁, H₁₂ = H[to-1,to-1], H[to-1,to]
362362
H₂₁, H₂₂ = H[to ,to-1], H[to ,to]
363363

364-
# Matrix determinant and trace
365-
d = H₁₁ * H₂₂ - H₂₁ * H₁₂
366-
t = H₁₁ + H₂₂
367-
discriminant = t * t - 4d
364+
# Scaling to avoid losing precision in the case where we have nearly
365+
# repeated eigenvalues.
366+
scale = abs(H₁₁) + abs(H₁₂) + abs(H₂₁) + abs(H₂₂)
367+
H₁₁ /= scale; H₁₂ /= scale; H₂₁ /= scale; H₂₂ /= scale
368368

369-
if discriminant zero(T)
369+
# Trace and discriminant of small eigenvalue problem.
370+
t = (H₁₁ + H₂₂) / 2
371+
d = (H₁₁ - t) * (H₂₂ - t) - H₁₂ * H₂₁
372+
sqrt_discr = sqrt(abs(d))
373+
374+
# Very important to have a strict comparison here!
375+
if d < zero(T)
370376
# Real eigenvalues.
371377
# Note that if from + 1 == to in this case, then just one additional
372378
# iteration is necessary, since the Wilkinson shift will do an exact shift.
373379

374380
# Determine the Wilkinson shift -- the closest eigenvalue of the 2x2 block
375381
# near H[to,to]
376-
sqr = sqrt(discriminant)
377-
λ₁ = (t + sqr) / 2
378-
λ₂ = (t - sqr) / 2
382+
383+
λ₁ = t + sqrt_discr
384+
λ₂ = t - sqrt_discr
379385
λ = abs(H₂₂ - λ₁) < abs(H₂₂ - λ₂) ? λ₁ : λ₂
386+
λ *= scale
387+
380388
# Run a bulge chase
381389
single_shift_schur!(H, from, to, λ, Q)
382390
else
@@ -389,8 +397,8 @@ function local_schurfact!(H::AbstractMatrix{T}, start::Int, to::Int,
389397
to -= 2
390398
else
391399
# Otherwise we do a double shift!
392-
sqr = sqrt(complex(discriminant))
393-
double_shift_schur!(H, from, to, (t + sqr) / 2, Q)
400+
complex_shift = scale * (t + sqrt_discr * im)
401+
double_shift_schur!(H, from, to, complex_shift, Q)
394402
end
395403
end
396404
end

src/schursort.jl

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ struct CompletelyPivotedLU{T,N,TA<:SMatrix{N,N,T},TP}
6262
A::TA
6363
p::TP
6464
q::TP
65+
singular::Bool
6566
end
6667

6768
struct CompletePivoting end
@@ -76,6 +77,7 @@ function lu(A::SMatrix{N,N,T}, ::Type{CompletePivoting}) where {N,T}
7677
A = MMatrix(A)
7778
p = @MVector fill(N, N)
7879
q = @MVector fill(N, N)
80+
singular = false
7981

8082
# Maybe I should consider doing this recursively.
8183
for k = OneTo(N - 1)
@@ -102,6 +104,12 @@ function lu(A::SMatrix{N,N,T}, ::Type{CompletePivoting}) where {N,T}
102104

103105
Akk = A[k,k]
104106

107+
# It has actually happened :(
108+
if iszero(Akk)
109+
singular = true
110+
break
111+
end
112+
105113
for i = k+1:N
106114
A[i, k] /= Akk
107115
end
@@ -115,8 +123,12 @@ function lu(A::SMatrix{N,N,T}, ::Type{CompletePivoting}) where {N,T}
115123
end
116124
end
117125

126+
if iszero(A[N,N])
127+
singular = true
128+
end
129+
118130
# Back to immutable land!
119-
return CompletelyPivotedLU(SMatrix(A), SVector(p), SVector(q))
131+
return CompletelyPivotedLU(SMatrix(A), SVector(p), SVector(q), singular)
120132
end
121133

122134
function (\)(LU::CompletelyPivotedLU{T,N}, b::SVector{N}) where {T,N}
@@ -158,22 +170,21 @@ end
158170
T(0) -B[1,2] A[2,1] A[2,2]-B[2,2]]
159171

160172
"""
161-
sylv(A, B, C) → X
173+
sylv(A, B, C) → X, singular
162174
163175
Solve A * X - X * B = C for X, where A and B are 1×1 or 2×2 matrices.
164176
165177
It works by recasting the Sylvester equation to a linear system
166178
(I ⊗ A + Bᵀ ⊗ I) vec(X) = vec(C) of size 2 or 4, which is then solved by
167179
Gaussian elimination with complete pivoting.
168-
"""
169-
@inline sylv(A::SMatrix{1,1,T}, B::SMatrix{2,2,T}, C::SMatrix{1,2,T}) where {T} =
170-
SMatrix{1,2,T}(lu(sylvsystem(A, B), CompletePivoting) \ SVector{2,T}(C))
171-
172-
@inline sylv(A::SMatrix{2,2,T}, B::SMatrix{1,1,T}, C::SMatrix{2,1,T}) where {T} =
173-
SMatrix{2,1,T}(lu(sylvsystem(A, B), CompletePivoting) \ SVector{2,T}(C))
174180
175-
@inline sylv(A::SMatrix{2,2,T}, B::SMatrix{2,2,T}, C::SMatrix{2,2,T}) where {T} =
176-
SMatrix{2,2,T}(lu(sylvsystem(A, B), CompletePivoting) \ SVector{4,T}(C))
181+
If the eigenvalues of A and B are equal, then `singular = true`.
182+
"""
183+
@inline function sylv(A::SMatrix{N,N,T}, B::SMatrix{M,M,T}, C::SMatrix{N,M,T}) where {T,N,M}
184+
fact = lu(sylvsystem(A, B), CompletePivoting)
185+
rhs = SVector{N*M,T}(C)
186+
SMatrix{N,M,T}(fact \ rhs), fact.singular
187+
end
177188

178189
"""
179190
swap22_rotations(X) → c₁, s₁, c₂, s₂, c₃, s₃, c₄, s₄
@@ -289,7 +300,10 @@ function swap22!(R::AbstractMatrix{T}, i::Integer, Q = NotWanted()) where {T}
289300
C = @SMatrix [R[i+0,i+2] R[i+0,i+3]; R[i+1,i+2] R[i+1,i+3]]
290301

291302
# A * X - X * B = C
292-
X = sylv(A, B, C)
303+
X, singular = sylv(A, B, C)
304+
305+
# No need to swap if eigenvalues are indistinguishable
306+
singular && return R
293307

294308
# Rotations that upper triangularize X
295309
c₁,s₁, c₂,s₂, c₃,s₃, c₄,s₄ = swap22_rotations(X)
@@ -340,7 +354,10 @@ function swap21!(R::AbstractMatrix{T}, i::Integer, Q = NotWanted()) where {T}
340354
C = @SMatrix [R[i+0,i+2]; R[i+1,i+2]]
341355

342356
# A * X - X * B = C
343-
X = sylv(A, B, C)
357+
X, singular = sylv(A, B, C)
358+
359+
# No need to swap if eigenvalues are indistinguishable
360+
singular && return R
344361

345362
# Rotations that upper triangularize X
346363
c₁,s₁, c₂,s₂ = swap21_rotations(X)
@@ -388,7 +405,10 @@ function swap12!(R::AbstractMatrix{T}, i::Integer, Q = NotWanted()) where {T}
388405
C = @SMatrix [R[i+0,i+1] R[i+0,i+2]]
389406

390407
# A * X - X * B = C
391-
X = sylv(A, B, C)
408+
X, singular = sylv(A, B, C)
409+
410+
# No need to swap if eigenvalues are indistinguishable
411+
singular && return R
392412

393413
# Rotations that upper triangularize X
394414
c₁,s₁, c₂,s₂ = swap12_rotations(X)

test/schurfact.jl

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
# Here we look at some edge cases.
33

44
using Test, LinearAlgebra
5-
using ArnoldiMethod: eigenvalues, local_schurfact!, is_offdiagonal_small
5+
using ArnoldiMethod: eigenvalues, local_schurfact!, is_offdiagonal_small,
6+
NotWanted
67

78
include("utils.jl")
89

@@ -108,6 +109,16 @@ include("utils.jl")
108109
# Test that the eigenvalues of H are the same before and after transformation
109110
@test sort!(eigvals(H), by=realimag) sort!(eigvals(H′), by=realimag)
110111
end
111-
112112
end
113113
end
114+
115+
@testset "Schur with nearly repeated eigenvalues" begin
116+
# Matrix with nearly repeated eigenpair could converge very slowly or
117+
# stagnate completely when there's a tiny perturbation in the computed
118+
# shift.
119+
mat(ε) = [2 0 0 ;
120+
5ε 1-ε 2ε ;
121+
0 3ε 1+ε]
122+
123+
@test local_schurfact!(mat(eps()))
124+
end

test/sort_schur.jl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,4 +251,19 @@ end
251251
@test abs(λs_before[3]) abs(λs_after[1])
252252
@test opnorm(I - Q'Q, 1) < 10eps() # we should be able to get rid of this prefactor?
253253
@test opnorm(A * Q - Q * A′, 1) < opnorm(A, 1) * eps()
254+
end
255+
256+
@testset "Identical eigenvalues should not blow up" begin
257+
A = [1.0 2.0 3.0 4.0;
258+
0.0 1.0 5.0 6.0;
259+
0.0 0.0 1.0 7.0;
260+
0.0 0.0 0.0 1.0]
261+
262+
A′ = copy(A)
263+
swap22!(A′, 1)
264+
@test A == A′
265+
swap12!(A′, 1)
266+
@test A == A′
267+
swap21!(A′, 1)
268+
@test A == A′
254269
end

test/sylvester.jl

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,40 @@
11
using Test
22
using ArnoldiMethod: sylv
33
using StaticArrays
4+
using LinearAlgebra
45

56
@testset "Tiny sylvester equation $T, $s" for T in (Float64, ComplexF64), s = ((2,2),(2,1),(1,2))
67
p, q = s
78
A = @SMatrix rand(T, p, p)
89
B = @SMatrix rand(T, q, q)
910
C = @SMatrix rand(T, p, q)
1011

11-
X = sylv(A, B, C)
12+
X, singular = sylv(A, B, C)
1213

1314
@test A * X - X * B C
15+
@test !singular
16+
end
17+
18+
@testset "Singular sylvester $T" for T in (Float64, ComplexF64)
19+
let
20+
A = @SMatrix T[1 2; 0 1]
21+
B = @SMatrix T[1 3; 0 1]
22+
C = @SMatrix rand(T, 2, 2)
23+
X, singular = sylv(A, B, C)
24+
@test singular
25+
end
26+
let
27+
A = @SMatrix T[1]
28+
B = @SMatrix T[1 3; 0 1]
29+
C = @SMatrix rand(T, 1, 2)
30+
X, singular = sylv(A, B, C)
31+
@test singular
32+
end
33+
let
34+
A = @SMatrix T[1 2; 0 1]
35+
B = @SMatrix T[1]
36+
C = @SMatrix rand(T, 2, 1)
37+
X, singular = sylv(A, B, C)
38+
@test singular
39+
end
1440
end

0 commit comments

Comments
 (0)