@@ -680,3 +680,101 @@ AbstractMatrix(F::Eigen) = F.vectors * Diagonal(F.values) / F.vectors
680680AbstractArray (F:: Eigen ) = AbstractMatrix (F)
681681Matrix (F:: Eigen ) = Array (AbstractArray (F))
682682Array (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
0 commit comments