From 3d28e1b22a4ddda68e0a5f2bbdba8e9e34840662 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Fri, 1 Nov 2024 17:33:03 -0400 Subject: [PATCH 01/14] First version `svd` --- .../src/BlockSparseArrays.jl | 13 ++ .../blocksparsearray/blockdiagonalarray.jl | 7 + .../src/factorizations/svd.jl | 205 ++++++++++++++++++ .../lib/BlockSparseArrays/test/indexing.jl | 56 +++++ 4 files changed, 281 insertions(+) create mode 100644 NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl create mode 100644 NDTensors/src/lib/BlockSparseArrays/src/factorizations/svd.jl create mode 100644 NDTensors/src/lib/BlockSparseArrays/test/indexing.jl diff --git a/NDTensors/src/lib/BlockSparseArrays/src/BlockSparseArrays.jl b/NDTensors/src/lib/BlockSparseArrays/src/BlockSparseArrays.jl index d0a1e4cdd7..bfb2184656 100644 --- a/NDTensors/src/lib/BlockSparseArrays/src/BlockSparseArrays.jl +++ b/NDTensors/src/lib/BlockSparseArrays/src/BlockSparseArrays.jl @@ -1,5 +1,12 @@ module BlockSparseArrays + +# factorizations +include("factorizations/svd.jl") + +# possible upstream contributions include("BlockArraysExtensions/BlockArraysExtensions.jl") + +# interface functions that don't have to specialize include("blocksparsearrayinterface/blocksparsearrayinterface.jl") include("blocksparsearrayinterface/linearalgebra.jl") include("blocksparsearrayinterface/blockzero.jl") @@ -7,6 +14,8 @@ include("blocksparsearrayinterface/broadcast.jl") include("blocksparsearrayinterface/map.jl") include("blocksparsearrayinterface/arraylayouts.jl") include("blocksparsearrayinterface/views.jl") + +# functions defined for any abstractblocksparsearray include("abstractblocksparsearray/abstractblocksparsearray.jl") include("abstractblocksparsearray/wrappedabstractblocksparsearray.jl") include("abstractblocksparsearray/abstractblocksparsematrix.jl") @@ -17,8 +26,12 @@ include("abstractblocksparsearray/sparsearrayinterface.jl") include("abstractblocksparsearray/broadcast.jl") include("abstractblocksparsearray/map.jl") include("abstractblocksparsearray/linearalgebra.jl") + +# functions specifically for BlockSparseArray include("blocksparsearray/defaults.jl") include("blocksparsearray/blocksparsearray.jl") +include("blocksparsearray/blockdiagonalarray.jl") + include("BlockArraysSparseArrayInterfaceExt/BlockArraysSparseArrayInterfaceExt.jl") include("../ext/BlockSparseArraysTensorAlgebraExt/src/BlockSparseArraysTensorAlgebraExt.jl") include("../ext/BlockSparseArraysGradedAxesExt/src/BlockSparseArraysGradedAxesExt.jl") diff --git a/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl b/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl new file mode 100644 index 0000000000..958ddf393c --- /dev/null +++ b/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl @@ -0,0 +1,7 @@ +using LinearAlgebra: Diagonal + +const BlockDiagonal{T,A,Axes,V<:AbstractVector{A}} = BlockSparseMatrix{T,A,Diagonal{A,V},Axes} + +function BlockDiagonal(blocks::AbstractVector{<:AbstractMatrix}) + return BlockSparseArray(Diagonal(blocks), (blockedrange(size.(blocks,1)), blockedrange(size.(blocks,2)))) +end \ No newline at end of file diff --git a/NDTensors/src/lib/BlockSparseArrays/src/factorizations/svd.jl b/NDTensors/src/lib/BlockSparseArrays/src/factorizations/svd.jl new file mode 100644 index 0000000000..db270147f5 --- /dev/null +++ b/NDTensors/src/lib/BlockSparseArrays/src/factorizations/svd.jl @@ -0,0 +1,205 @@ +using LinearAlgebra: + LinearAlgebra, Factorization, Algorithm, default_svd_alg, Adjoint, Transpose +using LinearAlgebra: eigencopy_oftype +using BlockArrays: AbstractBlockMatrix, BlockedArray, BlockedMatrix, BlockedVector +using BlockArrays: BlockLayout + +# Singular Value Decomposition: +# need new type to deal with U and V having possible different types +# this is basically a carbon copy of the LinearAlgebra implementation. +# additionally, by default we implement a fallback to the LinearAlgebra implementation +# in hope to support as many foreign types as possible that chose to extend those methods. + +# TODO: add this to MatrixFactorizations +# TODO: decide where this goes +# TODO: decide whether or not to restrict types to be blocked. +""" + SVD <: Factorization + +Matrix factorization type of the singular value decomposition (SVD) of a matrix `A`. +This is the return type of [`svd(_)`](@ref), the corresponding matrix factorization function. + +If `F::SVD` is the factorization object, `U`, `S`, `V` and `Vt` can be obtained +via `F.U`, `F.S`, `F.V` and `F.Vt`, such that `A = U * Diagonal(S) * Vt`. +The singular values in `S` are sorted in descending order. + +Iterating the decomposition produces the components `U`, `S`, and `V`. + +# Examples +```jldoctest +julia> A = [1. 0. 0. 0. 2.; 0. 0. 3. 0. 0.; 0. 0. 0. 0. 0.; 0. 2. 0. 0. 0.] +4×5 Matrix{Float64}: + 1.0 0.0 0.0 0.0 2.0 + 0.0 0.0 3.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 + 0.0 2.0 0.0 0.0 0.0 + +julia> F = svd(A) +SVD{Float64, Float64, Matrix{Float64}, Vector{Float64}} +U factor: +4×4 Matrix{Float64}: + 0.0 1.0 0.0 0.0 + 1.0 0.0 0.0 0.0 + 0.0 0.0 0.0 1.0 + 0.0 0.0 -1.0 0.0 +singular values: +4-element Vector{Float64}: + 3.0 + 2.23606797749979 + 2.0 + 0.0 +Vt factor: +4×5 Matrix{Float64}: + -0.0 0.0 1.0 -0.0 0.0 + 0.447214 0.0 0.0 0.0 0.894427 + 0.0 -1.0 0.0 0.0 0.0 + 0.0 0.0 0.0 1.0 0.0 + +julia> F.U * Diagonal(F.S) * F.Vt +4×5 Matrix{Float64}: + 1.0 0.0 0.0 0.0 2.0 + 0.0 0.0 3.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 + 0.0 2.0 0.0 0.0 0.0 + +julia> u, s, v = F; # destructuring via iteration + +julia> u == F.U && s == F.S && v == F.V +true +``` +""" +struct SVD{T,Tr,M<:AbstractArray{T},C<:AbstractVector{Tr},N<:AbstractArray{T}} <: + Factorization{T} + U::M + S::C + Vt::N + function SVD{T,Tr,M,C,N}( + U, S, Vt + ) where {T,Tr,M<:AbstractArray{T},C<:AbstractVector{Tr},N<:AbstractArray{T}} + Base.require_one_based_indexing(U, S, Vt) + return new{T,Tr,M,C,N}(U, S, Vt) + end +end +function SVD(U::AbstractArray{T}, S::AbstractVector{Tr}, Vt::AbstractArray{T}) where {T,Tr} + return SVD{T,Tr,typeof(U),typeof(S),typeof(Vt)}(U, S, Vt) +end +function SVD{T}(U::AbstractArray, S::AbstractVector{Tr}, Vt::AbstractArray) where {T,Tr} + return SVD( + convert(AbstractArray{T}, U), + convert(AbstractVector{Tr}, S), + convert(AbstractArray{T}, Vt), + ) +end + +function SVD{T}(F::SVD) where {T} + return SVD( + convert(AbstractMatrix{T}, F.U), + convert(AbstractVector{real(T)}, F.S), + convert(AbstractMatrix{T}, F.Vt), + ) +end +LinearAlgebra.Factorization{T}(F::SVD) where {T} = SVD{T}(F) + +# iteration for destructuring into components +Base.iterate(S::SVD) = (S.U, Val(:S)) +Base.iterate(S::SVD, ::Val{:S}) = (S.S, Val(:V)) +Base.iterate(S::SVD, ::Val{:V}) = (S.V, Val(:done)) +Base.iterate(::SVD, ::Val{:done}) = nothing + +function Base.getproperty(F::SVD, d::Symbol) + if d === :V + return getfield(F, :Vt)' + else + return getfield(F, d) + end +end + +function Base.propertynames(F::SVD, private::Bool=false) + return private ? (:V, fieldnames(typeof(F))...) : (:U, :S, :V, :Vt) +end + +Base.size(A::SVD, dim::Integer) = dim == 1 ? size(A.U, dim) : size(A.Vt, dim) +Base.size(A::SVD) = (size(A, 1), size(A, 2)) + +function Base.show(io::IO, mime::MIME{Symbol("text/plain")}, F::SVD) + summary(io, F) + println(io) + println(io, "U factor:") + show(io, mime, F.U) + println(io, "\nsingular values:") + show(io, mime, F.S) + println(io, "\nVt factor:") + return show(io, mime, F.Vt) +end + +Base.adjoint(usv::SVD) = SVD(adjoint(usv.Vt), usv.S, adjoint(usv.U)) +Base.transpose(usv::SVD) = SVD(transpose(usv.Vt), usv.S, transpose(usv.U)) + +# Conversion +Base.AbstractMatrix(F::SVD) = (F.U * Diagonal(F.S)) * F.Vt +Base.AbstractArray(F::SVD) = AbstractMatrix(F) +Base.Matrix(F::SVD) = Array(AbstractArray(F)) +Base.Array(F::SVD) = Matrix(F) +SVD(usv::SVD) = usv +SVD(usv::LinearAlgebra.SVD) = SVD(usv.U, usv.S, usv.Vt) + +# functions default to LinearAlgebra +# ---------------------------------- +""" + svd!(A; full::Bool = false, alg::Algorithm = default_svd_alg(A)) -> SVD + +`svd!` is the same as [`svd`](@ref), but saves space by +overwriting the input `A`, instead of creating a copy. See documentation of [`svd`](@ref) for details. +""" +svd!(A; kwargs...) = SVD(LinearAlgebra.svd!(A; kwargs...)) + +""" + svd(A; full::Bool = false, alg::Algorithm = default_svd_alg(A)) -> SVD + +Compute the singular value decomposition (SVD) of `A` and return an `SVD` object. + +`U`, `S`, `V` and `Vt` can be obtained from the factorization `F` with `F.U`, +`F.S`, `F.V` and `F.Vt`, such that `A = U * Diagonal(S) * Vt`. +The algorithm produces `Vt` and hence `Vt` is more efficient to extract than `V`. +The singular values in `S` are sorted in descending order. + +Iterating the decomposition produces the components `U`, `S`, and `V`. + +If `full = false` (default), a "thin" SVD is returned. For an ``M +\\times N`` matrix `A`, in the full factorization `U` is ``M \\times M`` +and `V` is ``N \\times N``, while in the thin factorization `U` is ``M +\\times K`` and `V` is ``N \\times K``, where ``K = \\min(M,N)`` is the +number of singular values. + +`alg` specifies which algorithm and LAPACK method to use for SVD: +- `alg = DivideAndConquer()` (default): Calls `LAPACK.gesdd!`. +- `alg = QRIteration()`: Calls `LAPACK.gesvd!` (typically slower but more accurate) . + +!!! compat "Julia 1.3" + The `alg` keyword argument requires Julia 1.3 or later. + +# Examples +```jldoctest +julia> A = rand(4,3); + +julia> F = svd(A); # Store the Factorization Object + +julia> A ≈ F.U * Diagonal(F.S) * F.Vt +true + +julia> U, S, V = F; # destructuring via iteration + +julia> A ≈ U * Diagonal(S) * V' +true + +julia> Uonly, = svd(A); # Store U only + +julia> Uonly == U +true +``` +""" +svd(A; kwargs...) = + SVD(svd!(eigencopy_oftype(A, LinearAlgebra.eigtype(eltype(A))); kwargs...)) + +LinearAlgebra.svdvals(usv::SVD{<:Any,T}) where {T} = (usv.S)::Vector{T} + diff --git a/NDTensors/src/lib/BlockSparseArrays/test/indexing.jl b/NDTensors/src/lib/BlockSparseArrays/test/indexing.jl new file mode 100644 index 0000000000..2e7fa21fd4 --- /dev/null +++ b/NDTensors/src/lib/BlockSparseArrays/test/indexing.jl @@ -0,0 +1,56 @@ +using BlockArrays: + Block, + BlockIndexRange, + BlockRange, + BlockSlice, + BlockVector, + BlockedOneTo, + BlockedUnitRange, + BlockedVector, + blockedrange, + blocklength, + blocklengths, + blocksize, + blocksizes, + blockaxes, + mortar +using LinearAlgebra: Adjoint, mul!, norm +using NDTensors.BlockSparseArrays: + @view!, + BlockSparseArray, + BlockView, + block_nstored, + block_reshape, + block_stored_indices, + view! +using NDTensors.SparseArrayInterface: nstored +using NDTensors.TensorAlgebra: contract + +using Test + +T = Float64 + +# scalar indexing +a = BlockSparseArray{T}([2, 3], [2, 2]) +for i in blockaxes(a, 1), j in blockaxes(a, 2) + a[i, j] = randn(T, blocksizes(a)[Int(i), Int(j)]) +end +a + +a[1, 2] +a[Block(1, 1)] +a[Block.(1:2), Block(1)] +aslice = a[Block.(1:2), 1] +axes(aslice, 1) +axes(aslice, 2) +length(axes(aslice)) == ndims(aslice) + +aslice = a[Block.(1:2), 1:3] +axes(aslice) + +mask = trues(size(a, 2)) +aslice = a[:, mask] +aslice = a[:, [1, 2]] + +a[Block(1, 1)] = randn(T, 2, 2) +a[Block(2, 2)] = randn(T, 2, 2) From 4e7e8e47fa9775fe34632584b3040aa4676fa311 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 6 Nov 2024 19:11:27 -0500 Subject: [PATCH 02/14] Make `svd` work with `BlockArray` --- .../BlockArraysExtensions.jl | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/NDTensors/src/lib/BlockSparseArrays/src/BlockArraysExtensions/BlockArraysExtensions.jl b/NDTensors/src/lib/BlockSparseArrays/src/BlockArraysExtensions/BlockArraysExtensions.jl index 39b4e885ac..46f05f2115 100644 --- a/NDTensors/src/lib/BlockSparseArrays/src/BlockArraysExtensions/BlockArraysExtensions.jl +++ b/NDTensors/src/lib/BlockSparseArrays/src/BlockArraysExtensions/BlockArraysExtensions.jl @@ -589,3 +589,37 @@ macro view!(expr) @capture(expr, array_[indices__]) return :(view!($(esc(array)), $(esc.(indices)...))) end + +# Adjoint simplifications +# ----------------------- +using LinearAlgebra: LinearAlgebra, Adjoint + +function Base.adjoint(A::BlockArrays.BlockMatrix) + return BlockArrays._BlockArray(adjoint(A.blocks), reverse(A.axes)) +end + +# SVD additions +# ------------- +using LinearAlgebra: Algorithm +using BlockArrays: BlockedMatrix + +# svd first calls `eigencopy_oftype` to create something that can be in-place SVD'd +# Here, we hijack this system to determine if there is any structure we can exploit +# default: SVD is most efficient with BlockedArray +function LinearAlgebra.eigencopy_oftype(A::AbstractBlockArray, S) + return BlockedMatrix{S}(A) +end + +function svd!(A::BlockedMatrix; full::Bool=false, alg::Algorithm=default_svd_alg(A)) + F = svd!(parent(A); full, alg) + + # restore block pattern + m = length(F.S) + bsz1, bsz2, bsz3 = BlockArrays.blocksizes(A, 1), [m], BlockArrays.blocksizes(A, 2) + + u = BlockedArray(F.U, bsz1, bsz2) + s = BlockedVector(F.S, bsz2) + vt = BlockedArray(F.Vt, bsz2, bsz3) + return SVD(u, s, vt) +end + From 916829511d15f8761dc75fd843a4e02f35005483 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 6 Nov 2024 19:12:21 -0500 Subject: [PATCH 03/14] Make `svd` work with `AbstractBlockSparseArray` --- .../abstractblocksparsematrix.jl | 21 +++++++ .../blocksparsearray/blockdiagonalarray.jl | 57 ++++++++++++++++++- .../src/blocksparsearray/blocksparsearray.jl | 5 ++ 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/NDTensors/src/lib/BlockSparseArrays/src/abstractblocksparsearray/abstractblocksparsematrix.jl b/NDTensors/src/lib/BlockSparseArrays/src/abstractblocksparsearray/abstractblocksparsematrix.jl index 0c2c578781..7dc52ceec0 100644 --- a/NDTensors/src/lib/BlockSparseArrays/src/abstractblocksparsearray/abstractblocksparsematrix.jl +++ b/NDTensors/src/lib/BlockSparseArrays/src/abstractblocksparsearray/abstractblocksparsematrix.jl @@ -1 +1,22 @@ const AbstractBlockSparseMatrix{T} = AbstractBlockSparseArray{T,2} + +# SVD is implemented by trying to +# 1. Attempt to find a block-diagonal implementation by permuting +# 2. Fallback to AbstractBlockArray implementation via BlockedArray +function svd( + A::AbstractBlockSparseMatrix; full::Bool=false, alg::Algorithm=default_svd_alg(A) +) + T = LinearAlgebra.eigtype(eltype(A)) + A′ = try_make_blockdiagonal(A) + + if isnothing(A′) + # not block-diagonal, fall back to dense case + Adense = eigencopy_oftype(A, T) + return svd!(Adense; full, alg) + end + + # compute block-by-block and permute back + A″, (I, J) = A′ + F = svd!(eigencopy_oftype(A″, T); full, alg) + return SVD(F.U[Block.(I), Block.(J)], F.S, F.Vt) +end \ No newline at end of file diff --git a/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl b/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl index 958ddf393c..f485d8082d 100644 --- a/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl +++ b/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl @@ -1,7 +1,60 @@ +# type alias for block-diagonal using LinearAlgebra: Diagonal -const BlockDiagonal{T,A,Axes,V<:AbstractVector{A}} = BlockSparseMatrix{T,A,Diagonal{A,V},Axes} +const BlockDiagonal{T,A,Axes,V<:AbstractVector{A}} = BlockSparseMatrix{ + T,A,Diagonal{A,V},Axes +} function BlockDiagonal(blocks::AbstractVector{<:AbstractMatrix}) - return BlockSparseArray(Diagonal(blocks), (blockedrange(size.(blocks,1)), blockedrange(size.(blocks,2)))) + return BlockSparseArray( + Diagonal(blocks), (blockedrange(size.(blocks, 1)), blockedrange(size.(blocks, 2))) + ) +end + +# Cast to block-diagonal implementation if permuted-blockdiagonal +function find_permute_blockdiagonal(A) + @show keys = map(Base.Fix2(getproperty, :n), collect(block_stored_indices(A))) + I = first.(keys) + allunique(I) || return nothing + + J = last.(keys) + p = sortperm(J) + Jsorted = J[p] + allunique(Jsorted) || return nothing + + return I[p], Jsorted +end + +""" + try_make_blockdiagonal(A) + +Attempt to find a permutation of blocks that makes `A` blockdiagonal. If unsuccesful, +returns nothing, otherwise returns both the blockdiagonal `B` as well as the permutation `I, J`. +""" +function try_make_blockdiagonal(A::AbstractBlockSparseMatrix) + perm = find_permute_blockdiagonal(A) + isnothing(perm) && return perm + I, J = perm + diagblocks = blocks(A)[tuple.(invperm(I), J)] + return BlockDiagonal(diagblocks), perm +end + +# TODO: block_stored_indices(BlockDiagonal) yields all indices + +# SVD implementation +function LinearAlgebra.eigencopy_oftype(A::BlockDiagonal, S) + diag = map(Base.Fix2(eigencopy_oftype, S), A.blocks.diag) + return BlockDiagonal(diag) +end + +function svd(A::BlockDiagonal; kwargs...) + return svd!(eigencopy_oftype(A, LinearAlgebra.eigtype(eltype(A))); kwargs...) +end +function svd!(A::BlockDiagonal; full::Bool=false, alg::Algorithm=default_svd_alg(A)) + # TODO: handle full + F = map(a -> svd!(a; full, alg), A.blocks.diag) + Us = map(Base.Fix2(getproperty, :U), F) + Ss = map(Base.Fix2(getproperty, :S), F) + Vts = map(Base.Fix2(getproperty, :Vt), F) + return SVD(BlockDiagonal(Us), mortar(Ss), BlockDiagonal(Vts)) end \ No newline at end of file diff --git a/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blocksparsearray.jl b/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blocksparsearray.jl index 50d52a59ca..7e6f75c2f1 100644 --- a/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blocksparsearray.jl +++ b/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blocksparsearray.jl @@ -170,3 +170,8 @@ blockstype(arraytype::Type{<:BlockSparseArray}) = SparseArrayDOK{AbstractArray} ## # TODO: Preserve GPU data! ## return BlockSparseArray{elt}(undef, axes) ## end + +function Base.adjoint(A::BlockSparseMatrix) + T = Base.promote_op(adjoint, eltype(A)) + return BlockSparseArray{T,2}(adjoint(A.blocks), reverse(A.axes)) +end From 6636cc1f4c58517857b409429fb1f3502ea6eedf Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 6 Nov 2024 19:12:30 -0500 Subject: [PATCH 04/14] Add truncation schemes --- .../src/factorizations/svd.jl | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/NDTensors/src/lib/BlockSparseArrays/src/factorizations/svd.jl b/NDTensors/src/lib/BlockSparseArrays/src/factorizations/svd.jl index db270147f5..41561b0c45 100644 --- a/NDTensors/src/lib/BlockSparseArrays/src/factorizations/svd.jl +++ b/NDTensors/src/lib/BlockSparseArrays/src/factorizations/svd.jl @@ -203,3 +203,107 @@ svd(A; kwargs...) = LinearAlgebra.svdvals(usv::SVD{<:Any,T}) where {T} = (usv.S)::Vector{T} +# Truncation schemes +# ------------------ +""" + TuncationScheme + +Abstract supertype for all truncated factorization schemes. +See also [`notrunc`](@ref), [`truncdim`](@ref) and [`truncbelow`](@ref). +""" +abstract type TruncationScheme end + +""" + NoTruncation <: TruncationScheme + notrunc() + +Truncation algorithm that represents no truncation. See also [`notrunc`](@ref) for easily +constructing instances of this type. +""" +struct NoTruncation <: TruncationScheme end +notrunc() = NoTruncation() + +""" + TruncAmount <: TruncationScheme + truncdim(num::Int; by=identity, lt=isless, rev=true) + +Truncation algorithm that truncates by keeping the first `num` singular values, sorted using +the kwargs `by`, `lt` and `rev` as passed to the `Base.sort` algorithm. +""" +struct TruncAmount{B,L} <: TruncationScheme + num::Int + by::B + lt::L + rev::Bool +end +truncdim(n::Int; by=identity, lt=isless, rev=true) = TruncAmount(n, by, lt, rev) + +""" + TruncFilter <: TruncationScheme + truncbelow(ϵ::Real) + +Truncation algorithm that truncates by filter, where `truncbelow` filters all values below a threshold `ϵ`. +""" +struct TruncFilter{F} <: TruncationScheme + f::F +end +function truncbelow(ϵ::Real) + @assert ϵ ≥ zero(ϵ) + return TruncFilter(≥(ϵ)) +end + +""" + truncate(F::Factorization; trunc::TruncationScheme) -> Factorization + +Truncate a factorization using the given truncation algorithm: +- `trunc = notrunc()` (default): Does nothing. +- `trunc = truncdim(n)`: Keeps the largest `n` values. +- `trunc = truncbelow(ϵ)`: Truncates all values below a threshold `ϵ`. +""" +truncate(F::SVD; trunc::TruncationScheme=notrunc()) = _truncate(F, trunc) + +# use _truncate to dispatch on `trunc` +_truncate(usv, ::NoTruncation) = usv + +# note: kept implementations separate for possible future ambiguity reasons +function _truncate(usv::SVD, trunc::TruncAmount) + keep = select_values(usv.S, trunc) + return SVD(usv.U[:, keep], usv.S[keep], usv.Vt[keep, :]) +end +function _truncate(usv::SVD, trunc::TruncFilter) + keep = select_values(usv.S, trunc) + return SVD(usv.U[:, keep], usv.S[keep], usv.Vt[keep, :]) +end + +function select_values(S, trunc::TruncAmount) + return partialsortperm(S, 1:(trunc.num); trunc.lt, trunc.by, trunc.rev) +end +select_values(S, trunc::TruncFilter) = findall(trunc.f, S) + +# For convenience, also add a method to both truncate and decompose +""" + tsvd(A; full::Bool=false, alg=default_svd_alg(A), trunc=notrunc()) + +Compute the truncated singular value decomposition (SVD) of `A`. +This is typically achieved by first computing the full SVD, followed by a filtering based on +the computed singular values. +""" +function tsvd(A; kwargs...) + return tsvd!(eigencopy_oftype(A, LinearAlgebra.eigtype(eltype(A))); kwargs...) +end + +""" + tsvd!(A; full::Bool=false, alg=default_svd_alg(A), trunc=notrunc()) + +Compute the truncated singular value decomposition (SVD) of `A`, saving space by +overwriting `A` in the process. See documentation of [`tsvd`](@ref) for details. +""" +function tsvd!(A; full::Bool=false, alg=default_svd_alg(A), trunc=notrunc()) + return _tsvd!(A, alg, trunc, full) +end + +# default implementation simply dispatches through to `svd` and `truncate`. +function _tsvd!(A, alg, trunc, full) + F = svd!(A; alg, full) + return truncate(F; trunc) +end \ No newline at end of file From b1f397df838aa081b16f90bd1c5a5fa6a87b118d Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 7 Nov 2024 09:49:13 -0500 Subject: [PATCH 05/14] Apply code suggestions Co-authored-by: Matt Fishman --- .../src/BlockArraysExtensions/BlockArraysExtensions.jl | 8 ++++---- .../src/blocksparsearray/blockdiagonalarray.jl | 2 +- .../src/blocksparsearray/blocksparsearray.jl | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/NDTensors/src/lib/BlockSparseArrays/src/BlockArraysExtensions/BlockArraysExtensions.jl b/NDTensors/src/lib/BlockSparseArrays/src/BlockArraysExtensions/BlockArraysExtensions.jl index 46f05f2115..d7b437cd82 100644 --- a/NDTensors/src/lib/BlockSparseArrays/src/BlockArraysExtensions/BlockArraysExtensions.jl +++ b/NDTensors/src/lib/BlockSparseArrays/src/BlockArraysExtensions/BlockArraysExtensions.jl @@ -615,11 +615,11 @@ function svd!(A::BlockedMatrix; full::Bool=false, alg::Algorithm=default_svd_alg # restore block pattern m = length(F.S) - bsz1, bsz2, bsz3 = BlockArrays.blocksizes(A, 1), [m], BlockArrays.blocksizes(A, 2) + bax1, bax2, bax3 = axes(A, 1), blockedrange([m]), axes(A, 2) - u = BlockedArray(F.U, bsz1, bsz2) - s = BlockedVector(F.S, bsz2) - vt = BlockedArray(F.Vt, bsz2, bsz3) + u = BlockedArray(F.U, (bax1, bax2)) + s = BlockedVector(F.S, (bax2,)) + vt = BlockedArray(F.Vt, (bax2, bax3)) return SVD(u, s, vt) end diff --git a/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl b/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl index f485d8082d..8eb3fb11d8 100644 --- a/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl +++ b/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl @@ -52,7 +52,7 @@ function svd(A::BlockDiagonal; kwargs...) end function svd!(A::BlockDiagonal; full::Bool=false, alg::Algorithm=default_svd_alg(A)) # TODO: handle full - F = map(a -> svd!(a; full, alg), A.blocks.diag) + F = map(a -> svd!(a; full, alg), blocks(A).diag) Us = map(Base.Fix2(getproperty, :U), F) Ss = map(Base.Fix2(getproperty, :S), F) Vts = map(Base.Fix2(getproperty, :Vt), F) diff --git a/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blocksparsearray.jl b/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blocksparsearray.jl index 7e6f75c2f1..5d9d3b5012 100644 --- a/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blocksparsearray.jl +++ b/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blocksparsearray.jl @@ -173,5 +173,5 @@ blockstype(arraytype::Type{<:BlockSparseArray}) = SparseArrayDOK{AbstractArray} function Base.adjoint(A::BlockSparseMatrix) T = Base.promote_op(adjoint, eltype(A)) - return BlockSparseArray{T,2}(adjoint(A.blocks), reverse(A.axes)) + return BlockSparseMatrix{T}(adjoint(blocks(A)), dual.(reverse(axes(A)))) end From 454a6127d507c0c5d89c3f41fb1889189a15d117 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 7 Nov 2024 15:13:52 -0500 Subject: [PATCH 06/14] Include tests [WIP] --- .../lib/BlockSparseArrays/test/svd_tests.jl | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 NDTensors/src/lib/BlockSparseArrays/test/svd_tests.jl diff --git a/NDTensors/src/lib/BlockSparseArrays/test/svd_tests.jl b/NDTensors/src/lib/BlockSparseArrays/test/svd_tests.jl new file mode 100644 index 0000000000..254460ae08 --- /dev/null +++ b/NDTensors/src/lib/BlockSparseArrays/test/svd_tests.jl @@ -0,0 +1,125 @@ +using Test +using NDTensors.BlockSparseArrays +using NDTensors.BlockSparseArrays: BlockSparseArray, svd, tsvd, notrunc, truncbelow, truncdim, BlockDiagonal +using BlockArrays +using LinearAlgebra: LinearAlgebra, Diagonal, svdvals + +function test_svd(a, usv) + U, S, V = usv + + @test U * Diagonal(S) * V' ≈ a + @test U' * U ≈ LinearAlgebra.I + @test V' * V ≈ LinearAlgebra.I +end + +# regular matrix +# -------------- +sizes = ((3, 3), (4, 3), (3, 4)) +eltypes = (Float32, Float64, ComplexF64) +@testset "($m, $n) Matrix{$T}" for ((m, n), T) in Iterators.product(sizes, eltypes) + a = rand(3, 3) + usv = @inferred svd(a) + test_svd(a, usv) + + # TODO: type unstable? + usv2 = tsvd(a) + test_svd(a, usv2) + + usv3 = tsvd(a; trunc=truncdim(2)) + @test length(usv3.S) == 2 + @test usv3.U' * usv3.U ≈ LinearAlgebra.I + @test usv3.Vt * usv3.V ≈ LinearAlgebra.I + + s = usv3.S[end] + usv4 = tsvd(a; trunc=truncbelow(s)) + @test length(usv4.S) == 2 + @test usv4.U' * usv4.U ≈ LinearAlgebra.I + @test usv4.Vt * usv4.V ≈ LinearAlgebra.I +end + +# block matrix +# ------------ +blockszs = (([2, 2], [2, 2]), ([2, 2], [2, 3]), ([2, 2, 1], [2, 3]), ([2, 3], [2])) +@testset "($m, $n) BlockMatrix{$T}" for ((m, n), T) in Iterators.product(blockszs, eltypes) + a = mortar([rand(T, i, j) for i in m, j in n]) + usv = svd(a) + test_svd(a, usv) + @test usv.U isa BlockedMatrix + @test usv.Vt isa BlockedMatrix + @test usv.S isa BlockedVector + + usv2 = tsvd(a) + test_svd(a, usv2) + @test usv.U isa BlockedMatrix + @test usv.Vt isa BlockedMatrix + @test usv.S isa BlockedVector + + usv3 = tsvd(a; trunc=truncdim(2)) + @test length(usv3.S) == 2 + @test usv3.U' * usv3.U ≈ LinearAlgebra.I + @test usv3.Vt * usv3.V ≈ LinearAlgebra.I + @test usv.U isa BlockedMatrix + @test usv.Vt isa BlockedMatrix + @test usv.S isa BlockedVector + + s = usv3.S[end] + usv4 = tsvd(a; trunc=truncbelow(s)) + @test length(usv4.S) == 2 + @test usv4.U' * usv4.U ≈ LinearAlgebra.I + @test usv4.Vt * usv4.V ≈ LinearAlgebra.I + @test usv.U isa BlockedMatrix + @test usv.Vt isa BlockedMatrix + @test usv.S isa BlockedVector +end + +# Block-Diagonal matrices +# ----------------------- +@testset "($m, $n) BlockDiagonal{$T}" for ((m, n), T) in Iterators.product(blockszs, eltypes) + a = BlockDiagonal([rand(T, i, j) for (i, j) in zip(m, n)]) + usv = svd(a) + test_svd(a, usv) + @test usv.U isa BlockDiagonal + @test usv.Vt isa BlockDiagonal + @test usv.S isa BlockVector + + usv2 = tsvd(a) + test_svd(a, usv2) + @test usv.U isa BlockDiagonal + @test usv.Vt isa BlockDiagonal + @test usv.S isa BlockVector + + usv3 = tsvd(a; trunc=truncdim(2)) + @test length(usv3.S) == 2 + @test usv3.U' * usv3.U ≈ LinearAlgebra.I + @test usv3.Vt * usv3.V ≈ LinearAlgebra.I + @test usv.U isa BlockDiagonal + @test usv.Vt isa BlockDiagonal + @test usv.S isa BlockVector + + @show s = usv3.S[end] + usv4 = tsvd(a; trunc=truncbelow(s)) + @test length(usv4.S) == 2 + @test usv4.U' * usv4.U ≈ LinearAlgebra.I + @test usv4.Vt * usv4.V ≈ LinearAlgebra.I + @test usv.U isa BlockDiagonal + @test usv.Vt isa BlockDiagonal + @test usv.S isa BlockVector +end + + +a = mortar([rand(2, 2) for i in 1:2, j in 1:3]) +usv = svd(a) +test_svd(a, usv) + +a = mortar([rand(2, 2) for i in 1:3, j in 1:2]) +usv = svd(a) +test_svd(a, usv) + +# blocksparse +# ----------- +a = BlockSparseArray([Block(2, 1), Block(1, 2)], [rand(2, 2), rand(2, 2)], (blockedrange([2, 2]), blockedrange([2, 2]))) +usv = svd(a) +test_svd(a, usv) + + +using NDTensors.BlockSparseArrays: block_stored_indices \ No newline at end of file From 64da33c33c97cc145f28bb7ffdb2d437e001628a Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Mon, 11 Nov 2024 08:49:18 -0500 Subject: [PATCH 07/14] Revert adjoint changes --- .../src/BlockArraysExtensions/BlockArraysExtensions.jl | 9 --------- .../src/blocksparsearray/blocksparsearray.jl | 5 ----- 2 files changed, 14 deletions(-) diff --git a/NDTensors/src/lib/BlockSparseArrays/src/BlockArraysExtensions/BlockArraysExtensions.jl b/NDTensors/src/lib/BlockSparseArrays/src/BlockArraysExtensions/BlockArraysExtensions.jl index d7b437cd82..7cf3945a03 100644 --- a/NDTensors/src/lib/BlockSparseArrays/src/BlockArraysExtensions/BlockArraysExtensions.jl +++ b/NDTensors/src/lib/BlockSparseArrays/src/BlockArraysExtensions/BlockArraysExtensions.jl @@ -590,14 +590,6 @@ macro view!(expr) return :(view!($(esc(array)), $(esc.(indices)...))) end -# Adjoint simplifications -# ----------------------- -using LinearAlgebra: LinearAlgebra, Adjoint - -function Base.adjoint(A::BlockArrays.BlockMatrix) - return BlockArrays._BlockArray(adjoint(A.blocks), reverse(A.axes)) -end - # SVD additions # ------------- using LinearAlgebra: Algorithm @@ -622,4 +614,3 @@ function svd!(A::BlockedMatrix; full::Bool=false, alg::Algorithm=default_svd_alg vt = BlockedArray(F.Vt, (bax2, bax3)) return SVD(u, s, vt) end - diff --git a/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blocksparsearray.jl b/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blocksparsearray.jl index 5d9d3b5012..50d52a59ca 100644 --- a/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blocksparsearray.jl +++ b/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blocksparsearray.jl @@ -170,8 +170,3 @@ blockstype(arraytype::Type{<:BlockSparseArray}) = SparseArrayDOK{AbstractArray} ## # TODO: Preserve GPU data! ## return BlockSparseArray{elt}(undef, axes) ## end - -function Base.adjoint(A::BlockSparseMatrix) - T = Base.promote_op(adjoint, eltype(A)) - return BlockSparseMatrix{T}(adjoint(blocks(A)), dual.(reverse(axes(A)))) -end From dd3226626454b48b03bc6d064bc0f0c605c97ccd Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Mon, 11 Nov 2024 08:51:34 -0500 Subject: [PATCH 08/14] Split off truncated SVD functionality and related truncation schemes --- .../src/BlockSparseArrays.jl | 1 + .../src/factorizations/svd.jl | 107 +----------------- .../src/factorizations/tsvd.jl | 104 +++++++++++++++++ 3 files changed, 106 insertions(+), 106 deletions(-) create mode 100644 NDTensors/src/lib/BlockSparseArrays/src/factorizations/tsvd.jl diff --git a/NDTensors/src/lib/BlockSparseArrays/src/BlockSparseArrays.jl b/NDTensors/src/lib/BlockSparseArrays/src/BlockSparseArrays.jl index bfb2184656..683b3600a7 100644 --- a/NDTensors/src/lib/BlockSparseArrays/src/BlockSparseArrays.jl +++ b/NDTensors/src/lib/BlockSparseArrays/src/BlockSparseArrays.jl @@ -2,6 +2,7 @@ module BlockSparseArrays # factorizations include("factorizations/svd.jl") +include("factorizations/tsvd.jl") # possible upstream contributions include("BlockArraysExtensions/BlockArraysExtensions.jl") diff --git a/NDTensors/src/lib/BlockSparseArrays/src/factorizations/svd.jl b/NDTensors/src/lib/BlockSparseArrays/src/factorizations/svd.jl index 41561b0c45..9960aafef0 100644 --- a/NDTensors/src/lib/BlockSparseArrays/src/factorizations/svd.jl +++ b/NDTensors/src/lib/BlockSparseArrays/src/factorizations/svd.jl @@ -201,109 +201,4 @@ true svd(A; kwargs...) = SVD(svd!(eigencopy_oftype(A, LinearAlgebra.eigtype(eltype(A))); kwargs...)) -LinearAlgebra.svdvals(usv::SVD{<:Any,T}) where {T} = (usv.S)::Vector{T} - -# Truncation schemes -# ------------------ -""" - TuncationScheme - -Abstract supertype for all truncated factorization schemes. -See also [`notrunc`](@ref), [`truncdim`](@ref) and [`truncbelow`](@ref). -""" -abstract type TruncationScheme end - -""" - NoTruncation <: TruncationScheme - notrunc() - -Truncation algorithm that represents no truncation. See also [`notrunc`](@ref) for easily -constructing instances of this type. -""" -struct NoTruncation <: TruncationScheme end -notrunc() = NoTruncation() - -""" - TruncAmount <: TruncationScheme - truncdim(num::Int; by=identity, lt=isless, rev=true) - -Truncation algorithm that truncates by keeping the first `num` singular values, sorted using -the kwargs `by`, `lt` and `rev` as passed to the `Base.sort` algorithm. -""" -struct TruncAmount{B,L} <: TruncationScheme - num::Int - by::B - lt::L - rev::Bool -end -truncdim(n::Int; by=identity, lt=isless, rev=true) = TruncAmount(n, by, lt, rev) - -""" - TruncFilter <: TruncationScheme - truncbelow(ϵ::Real) - -Truncation algorithm that truncates by filter, where `truncbelow` filters all values below a threshold `ϵ`. -""" -struct TruncFilter{F} <: TruncationScheme - f::F -end -function truncbelow(ϵ::Real) - @assert ϵ ≥ zero(ϵ) - return TruncFilter(≥(ϵ)) -end - -""" - truncate(F::Factorization; trunc::TruncationScheme) -> Factorization - -Truncate a factorization using the given truncation algorithm: -- `trunc = notrunc()` (default): Does nothing. -- `trunc = truncdim(n)`: Keeps the largest `n` values. -- `trunc = truncbelow(ϵ)`: Truncates all values below a threshold `ϵ`. -""" -truncate(F::SVD; trunc::TruncationScheme=notrunc()) = _truncate(F, trunc) - -# use _truncate to dispatch on `trunc` -_truncate(usv, ::NoTruncation) = usv - -# note: kept implementations separate for possible future ambiguity reasons -function _truncate(usv::SVD, trunc::TruncAmount) - keep = select_values(usv.S, trunc) - return SVD(usv.U[:, keep], usv.S[keep], usv.Vt[keep, :]) -end -function _truncate(usv::SVD, trunc::TruncFilter) - keep = select_values(usv.S, trunc) - return SVD(usv.U[:, keep], usv.S[keep], usv.Vt[keep, :]) -end - -function select_values(S, trunc::TruncAmount) - return partialsortperm(S, 1:(trunc.num); trunc.lt, trunc.by, trunc.rev) -end -select_values(S, trunc::TruncFilter) = findall(trunc.f, S) - -# For convenience, also add a method to both truncate and decompose -""" - tsvd(A; full::Bool=false, alg=default_svd_alg(A), trunc=notrunc()) - -Compute the truncated singular value decomposition (SVD) of `A`. -This is typically achieved by first computing the full SVD, followed by a filtering based on -the computed singular values. -""" -function tsvd(A; kwargs...) - return tsvd!(eigencopy_oftype(A, LinearAlgebra.eigtype(eltype(A))); kwargs...) -end - -""" - tsvd!(A; full::Bool=false, alg=default_svd_alg(A), trunc=notrunc()) - -Compute the truncated singular value decomposition (SVD) of `A`, saving space by -overwriting `A` in the process. See documentation of [`tsvd`](@ref) for details. -""" -function tsvd!(A; full::Bool=false, alg=default_svd_alg(A), trunc=notrunc()) - return _tsvd!(A, alg, trunc, full) -end - -# default implementation simply dispatches through to `svd` and `truncate`. -function _tsvd!(A, alg, trunc, full) - F = svd!(A; alg, full) - return truncate(F; trunc) -end \ No newline at end of file +LinearAlgebra.svdvals(usv::SVD{<:Any,T}) where {T} = (usv.S)::Vector{T} \ No newline at end of file diff --git a/NDTensors/src/lib/BlockSparseArrays/src/factorizations/tsvd.jl b/NDTensors/src/lib/BlockSparseArrays/src/factorizations/tsvd.jl new file mode 100644 index 0000000000..b4c3251f54 --- /dev/null +++ b/NDTensors/src/lib/BlockSparseArrays/src/factorizations/tsvd.jl @@ -0,0 +1,104 @@ +# Truncation schemes +# ------------------ +""" + TuncationScheme + +Abstract supertype for all truncated factorization schemes. +See also [`notrunc`](@ref), [`truncdim`](@ref) and [`truncbelow`](@ref). +""" +abstract type TruncationScheme end + +""" + NoTruncation <: TruncationScheme + notrunc() + +Truncation algorithm that represents no truncation. See also [`notrunc`](@ref) for easily +constructing instances of this type. +""" +struct NoTruncation <: TruncationScheme end +notrunc() = NoTruncation() + +""" + TruncAmount <: TruncationScheme + truncdim(num::Int; by=identity, lt=isless, rev=true) + +Truncation algorithm that truncates by keeping the first `num` singular values, sorted using +the kwargs `by`, `lt` and `rev` as passed to the `Base.sort` algorithm. +""" +struct TruncAmount{B,L} <: TruncationScheme + num::Int + by::B + lt::L + rev::Bool +end +truncdim(n::Int; by=identity, lt=isless, rev=true) = TruncAmount(n, by, lt, rev) + +""" + TruncFilter <: TruncationScheme + truncbelow(ϵ::Real) + +Truncation algorithm that truncates by filter, where `truncbelow` filters all values below a threshold `ϵ`. +""" +struct TruncFilter{F} <: TruncationScheme + f::F +end +function truncbelow(ϵ::Real) + @assert ϵ ≥ zero(ϵ) + return TruncFilter(≥(ϵ)) +end + +""" + truncate(F::Factorization; trunc::TruncationScheme) -> Factorization + +Truncate a factorization using the given truncation algorithm: +- `trunc = notrunc()` (default): Does nothing. +- `trunc = truncdim(n)`: Keeps the largest `n` values. +- `trunc = truncbelow(ϵ)`: Truncates all values below a threshold `ϵ`. +""" +truncate(F::SVD; trunc::TruncationScheme=notrunc()) = _truncate(F, trunc) + +# use _truncate to dispatch on `trunc` +_truncate(usv, ::NoTruncation) = usv + +# note: kept implementations separate for possible future ambiguity reasons +function _truncate(usv::SVD, trunc::TruncAmount) + keep = select_values(usv.S, trunc) + return SVD(usv.U[:, keep], usv.S[keep], usv.Vt[keep, :]) +end +function _truncate(usv::SVD, trunc::TruncFilter) + keep = select_values(usv.S, trunc) + return SVD(usv.U[:, keep], usv.S[keep], usv.Vt[keep, :]) +end + +function select_values(S, trunc::TruncAmount) + return partialsortperm(S, 1:(trunc.num); trunc.lt, trunc.by, trunc.rev) +end +select_values(S, trunc::TruncFilter) = findall(trunc.f, S) + +# For convenience, also add a method to both truncate and decompose +""" + tsvd(A; full::Bool=false, alg=default_svd_alg(A), trunc=notrunc()) + +Compute the truncated singular value decomposition (SVD) of `A`. +This is typically achieved by first computing the full SVD, followed by a filtering based on +the computed singular values. +""" +function tsvd(A; kwargs...) + return tsvd!(eigencopy_oftype(A, LinearAlgebra.eigtype(eltype(A))); kwargs...) +end + +""" + tsvd!(A; full::Bool=false, alg=default_svd_alg(A), trunc=notrunc()) + +Compute the truncated singular value decomposition (SVD) of `A`, saving space by +overwriting `A` in the process. See documentation of [`tsvd`](@ref) for details. +""" +function tsvd!(A; full::Bool=false, alg=default_svd_alg(A), trunc=notrunc()) + return _tsvd!(A, alg, trunc, full) +end + +# default implementation simply dispatches through to `svd` and `truncate`. +function _tsvd!(A, alg, trunc, full) + F = svd!(A; alg, full) + return truncate(F; trunc) +end \ No newline at end of file From cac85ba81ccb7ae2ece3c3085a5d47e1189f5493 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Mon, 11 Nov 2024 08:54:31 -0500 Subject: [PATCH 09/14] Create private `eigencopy_oftype` to avoid type piracy --- .../src/BlockArraysExtensions/BlockArraysExtensions.jl | 2 +- .../src/blocksparsearray/blockdiagonalarray.jl | 2 +- .../src/lib/BlockSparseArrays/src/factorizations/svd.jl | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/NDTensors/src/lib/BlockSparseArrays/src/BlockArraysExtensions/BlockArraysExtensions.jl b/NDTensors/src/lib/BlockSparseArrays/src/BlockArraysExtensions/BlockArraysExtensions.jl index 7cf3945a03..357949f80d 100644 --- a/NDTensors/src/lib/BlockSparseArrays/src/BlockArraysExtensions/BlockArraysExtensions.jl +++ b/NDTensors/src/lib/BlockSparseArrays/src/BlockArraysExtensions/BlockArraysExtensions.jl @@ -598,7 +598,7 @@ using BlockArrays: BlockedMatrix # svd first calls `eigencopy_oftype` to create something that can be in-place SVD'd # Here, we hijack this system to determine if there is any structure we can exploit # default: SVD is most efficient with BlockedArray -function LinearAlgebra.eigencopy_oftype(A::AbstractBlockArray, S) +function eigencopy_oftype(A::AbstractBlockArray, S) return BlockedMatrix{S}(A) end diff --git a/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl b/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl index 8eb3fb11d8..e87776cacb 100644 --- a/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl +++ b/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl @@ -42,7 +42,7 @@ end # TODO: block_stored_indices(BlockDiagonal) yields all indices # SVD implementation -function LinearAlgebra.eigencopy_oftype(A::BlockDiagonal, S) +function eigencopy_oftype(A::BlockDiagonal, S) diag = map(Base.Fix2(eigencopy_oftype, S), A.blocks.diag) return BlockDiagonal(diag) end diff --git a/NDTensors/src/lib/BlockSparseArrays/src/factorizations/svd.jl b/NDTensors/src/lib/BlockSparseArrays/src/factorizations/svd.jl index 9960aafef0..feba45ca35 100644 --- a/NDTensors/src/lib/BlockSparseArrays/src/factorizations/svd.jl +++ b/NDTensors/src/lib/BlockSparseArrays/src/factorizations/svd.jl @@ -1,6 +1,5 @@ using LinearAlgebra: LinearAlgebra, Factorization, Algorithm, default_svd_alg, Adjoint, Transpose -using LinearAlgebra: eigencopy_oftype using BlockArrays: AbstractBlockMatrix, BlockedArray, BlockedMatrix, BlockedVector using BlockArrays: BlockLayout @@ -201,4 +200,7 @@ true svd(A; kwargs...) = SVD(svd!(eigencopy_oftype(A, LinearAlgebra.eigtype(eltype(A))); kwargs...)) -LinearAlgebra.svdvals(usv::SVD{<:Any,T}) where {T} = (usv.S)::Vector{T} \ No newline at end of file +LinearAlgebra.svdvals(usv::SVD{<:Any,T}) where {T} = (usv.S)::Vector{T} + +# Added here to avoid type-piracy +eigencopy_oftype(A, S) = LinearAlgebra.eigencopy_oftype(A, S) \ No newline at end of file From 5df5d0711dd62393b03a0767e7a20ccc9154e37e Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Mon, 11 Nov 2024 08:58:14 -0500 Subject: [PATCH 10/14] Rename `try_make_blockdiagonal` to `try_to_blockdiagonal` --- .../abstractblocksparsematrix.jl | 2 +- .../src/blocksparsearray/blockdiagonalarray.jl | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/NDTensors/src/lib/BlockSparseArrays/src/abstractblocksparsearray/abstractblocksparsematrix.jl b/NDTensors/src/lib/BlockSparseArrays/src/abstractblocksparsearray/abstractblocksparsematrix.jl index 7dc52ceec0..8741ec80a5 100644 --- a/NDTensors/src/lib/BlockSparseArrays/src/abstractblocksparsearray/abstractblocksparsematrix.jl +++ b/NDTensors/src/lib/BlockSparseArrays/src/abstractblocksparsearray/abstractblocksparsematrix.jl @@ -7,7 +7,7 @@ function svd( A::AbstractBlockSparseMatrix; full::Bool=false, alg::Algorithm=default_svd_alg(A) ) T = LinearAlgebra.eigtype(eltype(A)) - A′ = try_make_blockdiagonal(A) + A′ = try_to_blockdiagonal(A) if isnothing(A′) # not block-diagonal, fall back to dense case diff --git a/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl b/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl index e87776cacb..3384e44c3c 100644 --- a/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl +++ b/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl @@ -12,8 +12,8 @@ function BlockDiagonal(blocks::AbstractVector{<:AbstractMatrix}) end # Cast to block-diagonal implementation if permuted-blockdiagonal -function find_permute_blockdiagonal(A) - @show keys = map(Base.Fix2(getproperty, :n), collect(block_stored_indices(A))) +function try_to_blockdiagonal_perm(A) + keys = map(Base.Fix2(getproperty, :n), collect(block_stored_indices(A))) I = first.(keys) allunique(I) || return nothing @@ -26,13 +26,13 @@ function find_permute_blockdiagonal(A) end """ - try_make_blockdiagonal(A) + try_to_blockdiagonal(A) Attempt to find a permutation of blocks that makes `A` blockdiagonal. If unsuccesful, returns nothing, otherwise returns both the blockdiagonal `B` as well as the permutation `I, J`. """ -function try_make_blockdiagonal(A::AbstractBlockSparseMatrix) - perm = find_permute_blockdiagonal(A) +function try_to_blockdiagonal(A::AbstractBlockSparseMatrix) + perm = try_to_blockdiagonal_perm(A) isnothing(perm) && return perm I, J = perm diagblocks = blocks(A)[tuple.(invperm(I), J)] From b6808cf45046f82b6c0580a24f0641ca7b243a8f Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Mon, 11 Nov 2024 09:24:35 -0500 Subject: [PATCH 11/14] Avoid field access in `try_to_blockdiagonal` --- .../src/blocksparsearray/blockdiagonalarray.jl | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl b/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl index 3384e44c3c..ee2d408111 100644 --- a/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl +++ b/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl @@ -13,15 +13,13 @@ end # Cast to block-diagonal implementation if permuted-blockdiagonal function try_to_blockdiagonal_perm(A) - keys = map(Base.Fix2(getproperty, :n), collect(block_stored_indices(A))) - I = first.(keys) + inds = map(x -> Int.(Tuple(x)), vec(collect(block_stored_indices(A)))) + I = first.(inds) allunique(I) || return nothing - - J = last.(keys) + J = last.(inds) p = sortperm(J) Jsorted = J[p] allunique(Jsorted) || return nothing - return I[p], Jsorted end @@ -35,12 +33,10 @@ function try_to_blockdiagonal(A::AbstractBlockSparseMatrix) perm = try_to_blockdiagonal_perm(A) isnothing(perm) && return perm I, J = perm - diagblocks = blocks(A)[tuple.(invperm(I), J)] + diagblocks = blocks(A)[CartesianIndex.(invperm(I), J)] return BlockDiagonal(diagblocks), perm end -# TODO: block_stored_indices(BlockDiagonal) yields all indices - # SVD implementation function eigencopy_oftype(A::BlockDiagonal, S) diag = map(Base.Fix2(eigencopy_oftype, S), A.blocks.diag) From 4d4813253f539fd32bb37298310bdf913cd1ee79 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Mon, 11 Nov 2024 09:50:54 -0500 Subject: [PATCH 12/14] comment on tests --- .../lib/BlockSparseArrays/test/svd_tests.jl | 91 +++++++++++++------ 1 file changed, 65 insertions(+), 26 deletions(-) diff --git a/NDTensors/src/lib/BlockSparseArrays/test/svd_tests.jl b/NDTensors/src/lib/BlockSparseArrays/test/svd_tests.jl index 254460ae08..3365a5eeca 100644 --- a/NDTensors/src/lib/BlockSparseArrays/test/svd_tests.jl +++ b/NDTensors/src/lib/BlockSparseArrays/test/svd_tests.jl @@ -1,6 +1,7 @@ using Test using NDTensors.BlockSparseArrays -using NDTensors.BlockSparseArrays: BlockSparseArray, svd, tsvd, notrunc, truncbelow, truncdim, BlockDiagonal +using NDTensors.BlockSparseArrays: + BlockSparseArray, svd, tsvd, notrunc, truncbelow, truncdim, BlockDiagonal using BlockArrays using LinearAlgebra: LinearAlgebra, Diagonal, svdvals @@ -17,7 +18,7 @@ end sizes = ((3, 3), (4, 3), (3, 4)) eltypes = (Float32, Float64, ComplexF64) @testset "($m, $n) Matrix{$T}" for ((m, n), T) in Iterators.product(sizes, eltypes) - a = rand(3, 3) + a = rand(m, n) usv = @inferred svd(a) test_svd(a, usv) @@ -74,39 +75,41 @@ end # Block-Diagonal matrices # ----------------------- -@testset "($m, $n) BlockDiagonal{$T}" for ((m, n), T) in Iterators.product(blockszs, eltypes) +@testset "($m, $n) BlockDiagonal{$T}" for ((m, n), T) in + Iterators.product(blockszs, eltypes) a = BlockDiagonal([rand(T, i, j) for (i, j) in zip(m, n)]) usv = svd(a) - test_svd(a, usv) + # TODO: `BlockDiagonal * Adjoint` errors + # test_svd(a, usv) @test usv.U isa BlockDiagonal @test usv.Vt isa BlockDiagonal @test usv.S isa BlockVector - usv2 = tsvd(a) + # usv2 = tsvd(a) test_svd(a, usv2) @test usv.U isa BlockDiagonal @test usv.Vt isa BlockDiagonal @test usv.S isa BlockVector - usv3 = tsvd(a; trunc=truncdim(2)) - @test length(usv3.S) == 2 - @test usv3.U' * usv3.U ≈ LinearAlgebra.I - @test usv3.Vt * usv3.V ≈ LinearAlgebra.I - @test usv.U isa BlockDiagonal - @test usv.Vt isa BlockDiagonal - @test usv.S isa BlockVector - - @show s = usv3.S[end] - usv4 = tsvd(a; trunc=truncbelow(s)) - @test length(usv4.S) == 2 - @test usv4.U' * usv4.U ≈ LinearAlgebra.I - @test usv4.Vt * usv4.V ≈ LinearAlgebra.I - @test usv.U isa BlockDiagonal - @test usv.Vt isa BlockDiagonal - @test usv.S isa BlockVector + # TODO: need to find a slicing fix to make this work + # usv3 = tsvd(a; trunc=truncdim(2)) + # @test length(usv3.S) == 2 + # @test usv3.U' * usv3.U ≈ LinearAlgebra.I + # @test usv3.Vt * usv3.V ≈ LinearAlgebra.I + # @test usv.U isa BlockDiagonal + # @test usv.Vt isa BlockDiagonal + # @test usv.S isa BlockVector + + # @show s = usv3.S[end] + # usv4 = tsvd(a; trunc=truncbelow(s)) + # @test length(usv4.S) == 2 + # @test usv4.U' * usv4.U ≈ LinearAlgebra.I + # @test usv4.Vt * usv4.V ≈ LinearAlgebra.I + # @test usv.U isa BlockDiagonal + # @test usv.Vt isa BlockDiagonal + # @test usv.S isa BlockVector end - a = mortar([rand(2, 2) for i in 1:2, j in 1:3]) usv = svd(a) test_svd(a, usv) @@ -117,9 +120,45 @@ test_svd(a, usv) # blocksparse # ----------- -a = BlockSparseArray([Block(2, 1), Block(1, 2)], [rand(2, 2), rand(2, 2)], (blockedrange([2, 2]), blockedrange([2, 2]))) -usv = svd(a) -test_svd(a, usv) +@testset "($m, $n) BlockDiagonal{$T}" for ((m, n), T) in + Iterators.product(blockszs, eltypes) + a = BlockSparseArray{T}(m, n) + for i in LinearAlgebra.diagind(blocks(a)) + I = CartesianIndices(blocks(a))[i] + a[Block(I.I...)] = rand(T, size(blocks(a)[i])) + end + perm = Random.randperm(length(m)) + a = a[Block.(perm), Block.(1:length(n))] + + # errors because `blocks(a)[CartesianIndex.(...)]` is not implemented + usv = svd(a) + # TODO: `BlockDiagonal * Adjoint` errors + # test_svd(a, usv) + @test usv.U isa BlockDiagonal + @test usv.Vt isa BlockDiagonal + @test usv.S isa BlockVector + # usv2 = tsvd(a) + test_svd(a, usv2) + @test usv.U isa BlockDiagonal + @test usv.Vt isa BlockDiagonal + @test usv.S isa BlockVector -using NDTensors.BlockSparseArrays: block_stored_indices \ No newline at end of file + # TODO: need to find a slicing fix to make this work + # usv3 = tsvd(a; trunc=truncdim(2)) + # @test length(usv3.S) == 2 + # @test usv3.U' * usv3.U ≈ LinearAlgebra.I + # @test usv3.Vt * usv3.V ≈ LinearAlgebra.I + # @test usv.U isa BlockDiagonal + # @test usv.Vt isa BlockDiagonal + # @test usv.S isa BlockVector + + # @show s = usv3.S[end] + # usv4 = tsvd(a; trunc=truncbelow(s)) + # @test length(usv4.S) == 2 + # @test usv4.U' * usv4.U ≈ LinearAlgebra.I + # @test usv4.Vt * usv4.V ≈ LinearAlgebra.I + # @test usv.U isa BlockDiagonal + # @test usv.Vt isa BlockDiagonal + # @test usv.S isa BlockVector +end \ No newline at end of file From 819b9bd559021fac274d4ec3a45282537f4635f3 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 13 Nov 2024 07:13:06 -0500 Subject: [PATCH 13/14] `to_blockdiagonal` now returns blocks --- .../src/blocksparsearray/blockdiagonalarray.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl b/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl index ee2d408111..e20eb8b0c7 100644 --- a/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl +++ b/NDTensors/src/lib/BlockSparseArrays/src/blocksparsearray/blockdiagonalarray.jl @@ -20,7 +20,7 @@ function try_to_blockdiagonal_perm(A) p = sortperm(J) Jsorted = J[p] allunique(Jsorted) || return nothing - return I[p], Jsorted + return Block.(I[p], Jsorted) end """ @@ -32,8 +32,11 @@ returns nothing, otherwise returns both the blockdiagonal `B` as well as the per function try_to_blockdiagonal(A::AbstractBlockSparseMatrix) perm = try_to_blockdiagonal_perm(A) isnothing(perm) && return perm - I, J = perm - diagblocks = blocks(A)[CartesianIndex.(invperm(I), J)] + I = first.(Tuple.(perm)) + J = last.(Tuple.(perm)) + diagblocks = map(invperm(I), J) do i, j + return A[Block(i, j)] + end return BlockDiagonal(diagblocks), perm end From e763ad41b2d20b5afb8fcaaa07c42eee52dc3bb0 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 13 Nov 2024 07:13:22 -0500 Subject: [PATCH 14/14] reenable some tests --- NDTensors/src/lib/BlockSparseArrays/test/svd_tests.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NDTensors/src/lib/BlockSparseArrays/test/svd_tests.jl b/NDTensors/src/lib/BlockSparseArrays/test/svd_tests.jl index 3365a5eeca..74453dcd7a 100644 --- a/NDTensors/src/lib/BlockSparseArrays/test/svd_tests.jl +++ b/NDTensors/src/lib/BlockSparseArrays/test/svd_tests.jl @@ -80,12 +80,12 @@ end a = BlockDiagonal([rand(T, i, j) for (i, j) in zip(m, n)]) usv = svd(a) # TODO: `BlockDiagonal * Adjoint` errors - # test_svd(a, usv) + test_svd(a, usv) @test usv.U isa BlockDiagonal @test usv.Vt isa BlockDiagonal @test usv.S isa BlockVector - # usv2 = tsvd(a) + usv2 = tsvd(a) test_svd(a, usv2) @test usv.U isa BlockDiagonal @test usv.Vt isa BlockDiagonal