Skip to content

Commit 3b4676a

Browse files
Manu KaliaManu Kalia
authored andcommitted
Sparse Eigenbasis Approximation (SEBA) algorithm added to src/eigen.jl with tests.
1 parent 925acef commit 3b4676a

File tree

2 files changed

+107
-0
lines changed

2 files changed

+107
-0
lines changed

src/eigen.jl

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,3 +680,101 @@ AbstractMatrix(F::Eigen) = F.vectors * Diagonal(F.values) / F.vectors
680680
AbstractArray(F::Eigen) = AbstractMatrix(F)
681681
Matrix(F::Eigen) = Array(AbstractArray(F))
682682
Array(F::Eigen) = Matrix(F)
683+
684+
"""
685+
seba(V :: Matrix; Rinit :: Union{Nothing, Matrix} = nothing, maxiter :: Int64 = 5000)
686+
687+
Sparse EigenBasis Algorithm (SEBA) computes a sparse basis for the span of an input collection of vectors. Returns matrices `S`, `R` composed of columns of sparse basis vectors of the subspace spanned by the columns of `V` and the rotation matrix `R` used to achieve this. More details in Froyland et al. (2019), https://doi.org/10.1016/j.cnsns.2019.04.012.
688+
689+
# Examples
690+
```jldoctest
691+
julia> a = [1.0 0.0; 0.0 1.0]
692+
2×2 Matrix{Float64}:
693+
1.0 0.0
694+
0.0 1.0
695+
696+
julia> rot = [cos(pi/3) -sin(pi/3); sin(pi/3) cos(pi/3)]
697+
2×2 Matrix{Float64}:
698+
0.5 -0.866025
699+
0.866025 0.5
700+
701+
julia> S, R = seba(rot*a); S
702+
2×2 Matrix{Float64}:
703+
0.0 1.0
704+
1.0 -0.0
705+
706+
julia> R
707+
2×2 Matrix{Float64}:
708+
0.866025 -0.5
709+
0.5 0.866025
710+
```
711+
"""
712+
function seba(V :: Matrix; Rinit :: Union{Nothing, Matrix} = nothing, maxiter :: Int64 = 5000)
713+
714+
# Inputs:
715+
# V is pxr matrix (r vectors of length p as columns)
716+
# Rinit is an (optional) initial rotation matrix.
717+
# maxiter is the maximum number of iterations allowed
718+
719+
# Outputs:
720+
# S is pxr matrix with columns approximately spanning the column space of V
721+
# R is the optimal rotation that acts on V, which followed by thresholding, produces S
722+
723+
# Begin SEBA algorithm
724+
725+
F = qr(V) # Enforce orthonormality
726+
V = Matrix(F.Q)
727+
p, r = size(V)
728+
μ = 0.99 / sqrt(p)
729+
730+
S = zeros(size(V))
731+
# Perturb near-constant vectors
732+
for j = 1:r
733+
if maximum(V[:, j]) - minimum(V[:, j]) < 1e-14
734+
V[:, j] = V[:, j] .+ (rand(p) .- 1 / 2) * 1e-12
735+
end
736+
end
737+
738+
# Initialise rotation
739+
if Rinit nothing
740+
Rnew = I
741+
else
742+
# Ensure orthonormality of Rinit
743+
F = svd(Rinit)
744+
Rnew = F.U * F.Vt
745+
end
746+
747+
# Define soft-threshold function: soft threshold scalar z by threshold μ
748+
soft_threshold(z, μ) = sign(z) * max(abs(z) - μ, 0)
749+
750+
# Preallocate matrices
751+
R = zeros(r, r)
752+
S = zeros(p, r)
753+
754+
iter = 0
755+
while norm(Rnew - R) > 1e-12 && iter < maxiter
756+
iter = iter + 1
757+
R = Rnew
758+
# Threshold to solve sparse approximation problem
759+
S .= soft_threshold.(V * R', μ)
760+
# Normalize columns of S
761+
foreach(normalize!, eachcol(S))
762+
# Polar decomposition to solve Procrustes problem
763+
F = svd(S' * V)
764+
Rnew = F.U * F.Vt
765+
end
766+
767+
# Choose correct parity of vectors and scale so largest value is 1
768+
for i = 1:r
769+
S[:, i] = S[:, i] * sign(sum(S[:, i]))
770+
S[:, i] = S[:, i] / maximum(S[:, i])
771+
end
772+
773+
# Sort so that most reliable vectors appear first
774+
ind = sortperm(vec(minimum(S, dims=1)), rev=true)
775+
S = S[:, ind]
776+
777+
error = norm(Rnew - R)
778+
return S, R
779+
780+
end

test/eigen.jl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,4 +279,13 @@ end
279279
@test λ == [1.0, 8.0]
280280
end
281281

282+
@testset "SEBA algorithm for real matrices" begin
283+
A = [1.0 0.0; 0.0 1.0]
284+
B = [cos(pi/3) -sin(pi/3); sin(pi/3) cos(pi/3)]
285+
S, R = seba(B*A)
286+
@test S == [0.0 1.0; 1.0 0.0]
287+
@test R [sin(pi/3) -cos(pi/3); cos(pi/3) sin(pi/3)]
288+
end
289+
290+
282291
end # module TestEigen

0 commit comments

Comments
 (0)