Skip to content

Commit

Permalink
Merge branch 'master' into mz/mul
Browse files Browse the repository at this point in the history
  • Loading branch information
mzgubic authored Feb 11, 2021
2 parents bc7c5f2 + 57d8490 commit 7ce3448
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 1 deletion.
34 changes: 34 additions & 0 deletions src/linalg.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ LinearAlgebra.det(B::BlockDiagonal) = prod(det, blocks(B))
LinearAlgebra.logdet(B::BlockDiagonal) = sum(logdet, blocks(B))
LinearAlgebra.tr(B::BlockDiagonal) = sum(tr, blocks(B))

for f in (:Symmetric, :Hermitian)
@eval LinearAlgebra.$f(B::BlockDiagonal, uplo::Symbol=:U) = BlockDiagonal([$f(block, uplo) for block in blocks(B)])
end

# Real matrices can have Complex eigenvalues; `eigvals` is not type stable.
@static if VERSION < v"1.2.0-DEV.275"
# No convention for sorting complex eigenvalues in earlier versions of Julia.
Expand Down Expand Up @@ -38,6 +42,36 @@ if VERSION < v"1.3.0-DEV.426"
end
end

"""
eigen_blockwise(B::BlockDiagonal, args...; kwargs...) -> values, vectors
Computes the eigendecomposition for each block separately and keeps the block diagonal
structure in the matrix of eigenvectors. Hence any parameters given are applied to each
eigendecomposition separately, but there is e.g. no global sorting of eigenvalues.
"""
function eigen_blockwise(B::BlockDiagonal, args...; kwargs...)
eigens = [eigen(b, args...; kwargs...) for b in blocks(B)]
# promote to common types to avoid vectors of unclear numerical type
# This may happen because eigen is not type stable!
# e.g typeof(eigen(randn(2,2))) may yield Eigen{Float64,Float64,Array{Float64,2},Array{Float64,1}}
# or Eigen{Complex{Float64},Complex{Float64},Array{Complex{Float64},2},Array{Complex{Float64},1}}
values = promote([e.values for e in eigens]...)
vectors = promote([e.vectors for e in eigens]...)
vcat(values...), BlockDiagonal([vectors...])
end

## This function never keeps the block diagonal structure
function LinearAlgebra.eigen(B::BlockDiagonal, args...; kwargs...)
values, vectors = eigen_blockwise(B, args...; kwargs...)
vectors = Matrix(vectors) # always convert to avoid type instability (also it speeds up the permutation step)
@static if VERSION > v"1.2.0-DEV.275"
Eigen(LinearAlgebra.sorteig!(values, vectors, kwargs...)...)
else
Eigen(values, vectors)
end
end


svdvals_blockwise(B::BlockDiagonal) = mapreduce(svdvals, vcat, blocks(B))
LinearAlgebra.svdvals(B::BlockDiagonal) = sort!(svdvals_blockwise(B); rev=true)

Expand Down
90 changes: 89 additions & 1 deletion test/linalg.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using BlockDiagonals
using BlockDiagonals: svd_blockwise
using BlockDiagonals: svd_blockwise, eigen_blockwise
using LinearAlgebra
using Random
using Test
Expand Down Expand Up @@ -53,6 +53,94 @@ using Test
end
end

@testset "Symmetric/Hermitian" begin
@test Symmetric(b1) == Symmetric(Matrix(b1))
@test Symmetric(b1, :L) == Symmetric(Matrix(b1), :L)
@test Hermitian(b1) == Hermitian(Matrix(b1))
@test Hermitian(b1, :L) == Hermitian(Matrix(b1), :L)
end

@testset "Eigen Decomposition" begin

@testset "eigen $name" for (name, B) in [("", b1), ("symmetric", Symmetric(b1)), ("hermitian", Hermitian(b1))]
E = eigen(B)
evals_bd, evecs_bd = E
evals, evecs = eigen(Matrix(B))

@test E isa Eigen
@test Matrix(E) B

# There is no test like @test eigen(B) == eigen(Matrix(B))
# 1. this fails in the complex case. Probably a convergence thing that leads to ever so slight differences
# 2. pre version 1.2 this can't be expected to hold at all because the order of eigenvalues was random
# so I sort the values/vectors (if needed) and then compare them via ≈

@static if VERSION < v"1.2"
# pre-v1.2 we need to sort the eigenvalues consistently
# Since eigenvalues may be complex here, I use this function, which works for this test.
# This test is already somewhat fragile w. r. t. degenerate eigenvalues
# and this just makes this a little worse.
perm_bd = sortperm(real.(evals_bd) + 100*imag.(evals_bd))
evals_bd = evals_bd[perm_bd]
evecs_bd = evecs_bd[:, perm_bd]

perm = sortperm(real.(evals) + 100*imag.(evals))
evals = evals[perm]
evecs = evecs[:, perm]
end

@test evals_bd evals
# comparing vectors is more difficult due to a sign ambiguity
# So we try adding and subtracting the vectors, keeping the smallest magnitude for each entry
# and compare that with something small.
# I performed some tests and the largest deviation I found was ~5e-14
@test all(min.(abs.(evecs_bd - evecs), abs.(evecs_bd + evecs)) .< 1e-13)
end

@testset "eigen_blockwise $name" for (name, B) in [("", b1), ("symmetric", Symmetric(b1)), ("hermitian", Hermitian(b1))]
vals, vecs = eigen_blockwise(B)

# check types. Eltype varies so not checked here (should be ComplexF64, Float64, Float64)
@test vecs isa BlockDiagonal
@test vals isa Vector

# Note: /(Matrix, BlockDiagonal) fails. I think because we can't do factorize(BlockDiagonal)
# this is why I convert to Matrix prior to /
@test B vecs * Diagonal(vals) / Matrix(vecs)

# check by block
cumulative_size = 0
for (i, block) in enumerate(blocks(B))
block_vals = vals[cumulative_size+1:cumulative_size+size(block,1)]
cumulative_size += size(block, 1)
# adapt eltype to the same to block_vals.
# block_vals's eltype is chosen to be compatible across all eigenvalues, thus it might be different
block = convert.(eltype(block_vals), block)

# from here on the code parallel to the test code above
E = Eigen(block_vals, blocks(vecs)[i])
evals_bd, evecs_bd = E
evals, evecs = eigen(block)

@test block Matrix(E)

@static if VERSION < v"1.2"
# sorting if needed
perm_bd = sortperm(real.(evals_bd) + 100*imag.(evals_bd))
evals_bd = evals_bd[perm_bd]
evecs_bd = evecs_bd[:, perm_bd]

perm = sortperm(real.(evals) + 100*imag.(evals))
evals = evals[perm]
evecs = evecs[:, perm]
end

@test evals_bd evals
@test all(min.(abs.(evecs_bd - evecs), abs.(evecs_bd + evecs)) .< 1e-13)
end
end
end

@testset "eigvals on LinearAlgebra types" begin
# `eigvals` has different methods for different types, e.g. Hermitian
b_herm = BlockDiagonal([Hermitian(rand(rng, 3, 3) + I) for _ in 1:3])
Expand Down

0 comments on commit 7ce3448

Please sign in to comment.