From 669f49e8653fd042e97b3091ac050e5b99f513e2 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sun, 16 Mar 2025 15:43:45 -0400 Subject: [PATCH 01/70] implement `foreachblock` --- src/tensors/blockiterator.jl | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/tensors/blockiterator.jl b/src/tensors/blockiterator.jl index b4ec4b87b..1f8dd5b04 100644 --- a/src/tensors/blockiterator.jl +++ b/src/tensors/blockiterator.jl @@ -13,3 +13,28 @@ Base.IteratorEltype(::BlockIterator) = Base.HasEltype() Base.eltype(::Type{<:BlockIterator{T}}) where {T} = blocktype(T) Base.length(iter::BlockIterator) = length(iter.structure) Base.isdone(iter::BlockIterator, state...) = Base.isdone(iter.structure, state...) + +# TODO: fast-path when structures are the same? +# TODO: do we want f(c, bs...) or f(c, bs)? +# TODO: implement scheduler +# TODO: do we prefer `blocks(t, ts...)` instead or as well? +""" + foreachblock(f, t::AbstractTensorMap, ts::AbstractTensorMap...; [scheduler]) + +Apply `f` to each block of `t` and the corresponding blocks of `ts`. +Optionally, `scheduler` can be used to parallelize the computation. +This function is equivalent to the following loop: + +```julia +for (c, b) in blocks(t) + bs = (b, block.(ts, c)...) + f(c, bs) +end +``` +""" +function foreachblock(f, t::AbstractTensorMap, ts::AbstractTensorMap...; scheduler=nothing) + foreach(blocks(t)) do (c, b) + return f(c, (b, block.(ts, c)...)) + end + return nothing +end From 65344897b0efcefbb5511aaefc67c7597cfbccaa Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sun, 16 Mar 2025 15:44:08 -0400 Subject: [PATCH 02/70] Implement `eig_full!` --- Project.toml | 2 ++ src/TensorKit.jl | 3 ++ src/tensors/matrixalgebrakit.jl | 49 +++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 src/tensors/matrixalgebrakit.jl diff --git a/Project.toml b/Project.toml index b148cea6a..505fc11a7 100644 --- a/Project.toml +++ b/Project.toml @@ -6,6 +6,7 @@ version = "0.14.6" [deps] LRUCache = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +MatrixAlgebraKit = "6c742aac-3347-4629-af66-fc926824e5e4" PackageExtensionCompat = "65ce6f38-6b18-4e1d-a461-8949797d7930" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" @@ -31,6 +32,7 @@ Combinatorics = "1" FiniteDifferences = "0.12" LRUCache = "1.0.2" LinearAlgebra = "1" +MatrixAlgebraKit = "0.1.1" PackageExtensionCompat = "1" Random = "1" SparseArrays = "1" diff --git a/src/TensorKit.jl b/src/TensorKit.jl index d94245530..68ba4d65a 100644 --- a/src/TensorKit.jl +++ b/src/TensorKit.jl @@ -104,6 +104,8 @@ using TensorOperations: TensorOperations, @tensor, @tensoropt, @ncon, ncon using TensorOperations: IndexTuple, Index2Tuple, linearize, AbstractBackend const TO = TensorOperations +using MatrixAlgebraKit: MatrixAlgebraKit as MAK + using LRUCache using TensorKitSectors @@ -199,6 +201,7 @@ include("tensors/treetransformers.jl") include("tensors/indexmanipulations.jl") include("tensors/diagonal.jl") include("tensors/truncation.jl") +include("tensors/matrixalgebrakit.jl") include("tensors/factorizations.jl") include("tensors/braidingtensor.jl") diff --git a/src/tensors/matrixalgebrakit.jl b/src/tensors/matrixalgebrakit.jl new file mode 100644 index 000000000..3f5430fb0 --- /dev/null +++ b/src/tensors/matrixalgebrakit.jl @@ -0,0 +1,49 @@ +function MAK.copy_input(::typeof(MAK.eig_full), t::AbstractTensorMap) + return copy_oftype(t, factorisation_scalartype(MAK.eig_full!, t)) +end + +function factorisation_scalartype(::typeof(MAK.eig_full!), t::AbstractTensorMap) + T = complex(scalartype(t)) + return promote_type(ComplexF32, typeof(zero(T) / sqrt(abs2(one(T))))) +end + +function MAK.check_input(::typeof(MAK.eig_full!), t::AbstractTensorMap, (D, V)) + domain(t) == codomain(t) || + throw(ArgumentError("Eigenvalue decomposition requires square input tensor")) + Tc = complex(scalartype(t)) + + (D isa DiagonalTensorMap && + scalartype(D) == Tc && + fuse(domain(t)) == space(D, 1)) || + throw(ArgumentError("`eig_full!` requires diagonal tensor D with isomorphic domain and complex `scalartype`")) + + V isa AbstractTensorMap && + scalartype(V) == Tc && + space(V) == (codomain(t) ← codomain(D)) || + throw(ArgumentError("`eig_full!` requires square tensor V with isomorphic domain and complex `scalartype`")) + + return nothing +end + +function MAK.initialize_output(::typeof(MAK.eig_full!), t::AbstractTensorMap, + ::MAK.LAPACK_EigAlgorithm) + Tc = complex(scalartype(t)) + V_diag = fuse(domain(t)) + return DiagonalTensorMap{Tc}(undef, V_diag), similar(t, Tc, domain(t) ← V_diag) +end + +function MAK.eig_full!(t::AbstractTensorMap, (D, V), alg::MAK.LAPACK_EigAlgorithm) + MAK.check_input(MAK.eig_full!, t, (D, V)) + foreachblock(t, D, V) do (_, (b, d, v)) + d′, v′ = MAK.eig_full!(b, (d, v), alg) + # deal with the case where the output is not the same as the input + d === d′ || copyto!(d, d′) + v === v′ || copyto!(v, v′) + return nothing + end + return D, V +end + +function MAK.default_eig_algorithm(::TensorMap{<:LinearAlgebra.BlasFloat}; kwargs...) + return MAK.LAPACK_Expert(; kwargs...) +end From d90b21010230fe1c6e0c0d63d4d552b88af35128 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sun, 16 Mar 2025 15:46:50 -0400 Subject: [PATCH 03/70] Use `eig_full` in `eig` --- src/tensors/factorizations.jl | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/tensors/factorizations.jl b/src/tensors/factorizations.jl index 2292b6c17..0825fe10a 100644 --- a/src/tensors/factorizations.jl +++ b/src/tensors/factorizations.jl @@ -222,10 +222,7 @@ matrices. See the corresponding documentation for more information. See also `eigen` and `eigh`. """ -function eig(t::AbstractTensorMap, p::Index2Tuple; kwargs...) - tcopy = permutedcopy_oftype(t, factorisation_scalartype(eig, t), p) - return eig!(tcopy; kwargs...) -end +eig(t::AbstractTensorMap, p::Index2Tuple; kwargs...) = MAK.eig_full(t; kwargs...) """ eigh(t::AbstractTensorMap, (leftind, rightind)::Index2Tuple) -> D, V From 49b41e2f30d5f67c01ef37e89b87720f7d2ca0bb Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 19 Mar 2025 11:54:20 -0400 Subject: [PATCH 04/70] Fix factorisation scalartype --- src/tensors/matrixalgebrakit.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tensors/matrixalgebrakit.jl b/src/tensors/matrixalgebrakit.jl index 3f5430fb0..80ff4702c 100644 --- a/src/tensors/matrixalgebrakit.jl +++ b/src/tensors/matrixalgebrakit.jl @@ -4,7 +4,7 @@ end function factorisation_scalartype(::typeof(MAK.eig_full!), t::AbstractTensorMap) T = complex(scalartype(t)) - return promote_type(ComplexF32, typeof(zero(T) / sqrt(abs2(one(T))))) + return promote_type(Float32, typeof(zero(T) / sqrt(abs2(one(T))))) end function MAK.check_input(::typeof(MAK.eig_full!), t::AbstractTensorMap, (D, V)) From 3f9c871a360e8acfa7dffb8f2e59755be40a9502 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 19 Mar 2025 12:14:32 -0400 Subject: [PATCH 05/70] Add scheduler support --- Project.toml | 4 ++++ src/TensorKit.jl | 3 +++ src/tensors/backends.jl | 28 ++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 src/tensors/backends.jl diff --git a/Project.toml b/Project.toml index 505fc11a7..f78526b9d 100644 --- a/Project.toml +++ b/Project.toml @@ -7,8 +7,10 @@ version = "0.14.6" LRUCache = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MatrixAlgebraKit = "6c742aac-3347-4629-af66-fc926824e5e4" +OhMyThreads = "67456a42-1dca-4109-a031-0a68de7e3ad5" PackageExtensionCompat = "65ce6f38-6b18-4e1d-a461-8949797d7930" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +ScopedValues = "7e506255-f358-4e82-b7e4-beb19740aa63" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" Strided = "5e0ebb24-38b0-5f93-81fe-25c709ecae67" TensorKitSectors = "13a9c161-d5da-41f0-bcbd-e1a08ae0647f" @@ -33,8 +35,10 @@ FiniteDifferences = "0.12" LRUCache = "1.0.2" LinearAlgebra = "1" MatrixAlgebraKit = "0.1.1" +OhMyThreads = "0.8.0" PackageExtensionCompat = "1" Random = "1" +ScopedValues = "1.3.0" SparseArrays = "1" Strided = "2" TensorKitSectors = "0.1" diff --git a/src/TensorKit.jl b/src/TensorKit.jl index 68ba4d65a..b5e12b78c 100644 --- a/src/TensorKit.jl +++ b/src/TensorKit.jl @@ -107,6 +107,8 @@ const TO = TensorOperations using MatrixAlgebraKit: MatrixAlgebraKit as MAK using LRUCache +using OhMyThreads +using ScopedValues using TensorKitSectors import TensorKitSectors: dim, BraidingStyle, FusionStyle, ⊠, ⊗ @@ -191,6 +193,7 @@ include("spaces/vectorspaces.jl") #------------------------------------- # general definitions include("tensors/abstracttensor.jl") +include("tensors/backends.jl") include("tensors/blockiterator.jl") include("tensors/tensor.jl") include("tensors/adjoint.jl") diff --git a/src/tensors/backends.jl b/src/tensors/backends.jl new file mode 100644 index 000000000..2f945bc42 --- /dev/null +++ b/src/tensors/backends.jl @@ -0,0 +1,28 @@ +# Scheduler implementation +# ------------------------ +function select_scheduler(scheduler=OhMyThreads.Implementation.NotGiven(); kwargs...) + return if scheduler == OhMyThreads.Implementation.NotGiven() && isempty(kwargs) + Threads.nthreads() > 1 ? SerialScheduler() : DynamicScheduler() + else + OhMyThreads.Implementation._scheduler_from_userinput(scheduler; kwargs...) + end +end + +""" + const blockscheduler = ScopedValue{Scheduler}(SerialScheduler()) + +The default scheduler used when looping over different blocks in the matrix representation of a +tensor. +For controlling this value, see also [`set_blockscheduler`](@ref) and [`with_blockscheduler`](@ref). +""" +const blockscheduler = ScopedValue{Scheduler}(SerialScheduler()) + +""" + with_blockscheduler(f, [scheduler]; kwargs...) + +Run `f` in a scope where the `blockscheduler` is determined by `scheduler' and `kwargs...`. +""" +@inline function with_blockscheduler(f, scheduler=OhMyThreads.Implementation.NotGiven(); + kwargs...) + @with blockscheduler => select_scheduler(scheduler; kwargs...) f() +end From 900200aebdc73076c1eeb9049593e73fd8cf9d08 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 19 Mar 2025 15:59:32 -0400 Subject: [PATCH 06/70] Add BlockAlgorithm --- src/tensors/backends.jl | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/tensors/backends.jl b/src/tensors/backends.jl index 2f945bc42..40cbd1ba5 100644 --- a/src/tensors/backends.jl +++ b/src/tensors/backends.jl @@ -26,3 +26,18 @@ Run `f` in a scope where the `blockscheduler` is determined by `scheduler' and ` kwargs...) @with blockscheduler => select_scheduler(scheduler; kwargs...) f() end + +# TODO: disable for trivial symmetry or small tensors? +default_blockscheduler(t::AbstractTensorMap) = blockscheduler[] + +# MatrixAlgebraKit +# ---------------- +""" + BlockAlgorithm{A,S}(alg, scheduler) + +Generic wrapper for implementing block-wise algorithms. +""" +struct BlockAlgorithm{A,S} <: MatrixAlgebraKit.AbstractAlgorithm + alg::A + scheduler::S +end From b455203cab32ae4a9c602dfbfaeeddca4632cb13 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 19 Mar 2025 15:59:58 -0400 Subject: [PATCH 07/70] Add more matrixalgebra methods --- src/TensorKit.jl | 3 +- src/tensors/blockiterator.jl | 2 +- src/tensors/matrixalgebrakit.jl | 207 ++++++++++++++++++++++++++++---- 3 files changed, 187 insertions(+), 25 deletions(-) diff --git a/src/TensorKit.jl b/src/TensorKit.jl index b5e12b78c..b55b7857d 100644 --- a/src/TensorKit.jl +++ b/src/TensorKit.jl @@ -121,7 +121,7 @@ using Base: @boundscheck, @propagate_inbounds, @constprop, SizeUnknown, HasLength, HasShape, IsInfinite, EltypeUnknown, HasEltype using Base.Iterators: product, filter -using LinearAlgebra: LinearAlgebra +using LinearAlgebra: LinearAlgebra, BlasFloat using LinearAlgebra: norm, dot, normalize, normalize!, tr, axpy!, axpby!, lmul!, rmul!, mul!, ldiv!, rdiv!, adjoint, adjoint!, transpose, transpose!, @@ -129,6 +129,7 @@ using LinearAlgebra: norm, dot, normalize, normalize!, tr, eigen, eigen!, svd, svd!, isposdef, isposdef!, ishermitian, rank, cond, Diagonal, Hermitian +using MatrixAlgebraKit using SparseArrays: SparseMatrixCSC, sparse, nzrange, rowvals, nonzeros diff --git a/src/tensors/blockiterator.jl b/src/tensors/blockiterator.jl index 1f8dd5b04..fd7a1801a 100644 --- a/src/tensors/blockiterator.jl +++ b/src/tensors/blockiterator.jl @@ -34,7 +34,7 @@ end """ function foreachblock(f, t::AbstractTensorMap, ts::AbstractTensorMap...; scheduler=nothing) foreach(blocks(t)) do (c, b) - return f(c, (b, block.(ts, c)...)) + return f(c, (b, map(Base.Fix2(block, c), ts)...)) end return nothing end diff --git a/src/tensors/matrixalgebrakit.jl b/src/tensors/matrixalgebrakit.jl index 80ff4702c..36993331d 100644 --- a/src/tensors/matrixalgebrakit.jl +++ b/src/tensors/matrixalgebrakit.jl @@ -1,49 +1,210 @@ -function MAK.copy_input(::typeof(MAK.eig_full), t::AbstractTensorMap) - return copy_oftype(t, factorisation_scalartype(MAK.eig_full!, t)) +# Generic +# ------- +for f in (:eig_full, :eig_vals, :eig_trunc, :eigh_full, :eigh_vals, :eigh_trunc, :svd_full, + :svd_compact, :svd_vals, :svd_trunc) + @eval function MatrixAlgebraKit.copy_input(::typeof($f), + t::AbstractTensorMap{<:BlasFloat}) + T = factorisation_scalartype($f, t) + return copy_oftype(t, T) + end +end + +# function factorisation_scalartype(::typeof(MAK.eig_full!), t::AbstractTensorMap) +# T = scalartype(t) +# return promote_type(Float32, typeof(zero(T) / sqrt(abs2(one(T))))) +# end + +# Singular value decomposition +# ---------------------------- +function MatrixAlgebraKit.check_input(::typeof(svd_full!), t::AbstractTensorMap, (U, S, Vᴴ)) + V_cod = fuse(codomain(t)) + V_dom = fuse(domain(t)) + + (U isa AbstractTensorMap && + scalartype(U) == scalartype(t) && + space(U) == (codomain(t) ← V_cod)) || + throw(ArgumentError("`svd_full!` requires unitary tensor U with same `scalartype`")) + (S isa AbstractTensorMap && + scalartype(S) == real(scalartype(t)) && + space(S) == (V_cod ← V_dom)) || + throw(ArgumentError("`svd_full!` requires rectangular tensor S with real `scalartype`")) + (Vᴴ isa AbstractTensorMap && + scalartype(Vᴴ) == scalartype(t) && + space(Vᴴ) == (V_dom ← domain(t))) || + throw(ArgumentError("`svd_full!` requires unitary tensor Vᴴ with same `scalartype`")) + + return nothing +end + +function MatrixAlgebraKit.check_input(::typeof(svd_compact!), t::AbstractTensorMap, + (U, S, Vᴴ)) + V_cod = V_dom = infimum(fuse(codomain(t)), fuse(domain(t))) + + (U isa AbstractTensorMap && + scalartype(U) == scalartype(t) && + space(U) == (codomain(t) ← V_cod)) || + throw(ArgumentError("`svd_compact!` requires isometric tensor U with same `scalartype`")) + (S isa DiagonalTensorMap && + scalartype(S) == real(scalartype(t)) && + space(S) == (V_cod ← V_dom)) || + throw(ArgumentError("`svd_compact!` requires diagonal tensor S with real `scalartype`")) + (Vᴴ isa AbstractTensorMap && + scalartype(Vᴴ) == scalartype(t) && + space(Vᴴ) == (V_dom ← domain(t))) || + throw(ArgumentError("`svd_compact!` requires isometric tensor Vᴴ with same `scalartype`")) + + return nothing +end + +# TODO: svd_vals + +function MatrixAlgebraKit.initialize_output(::typeof(svd_full!), t::AbstractTensorMap, + ::MatrixAlgebraKit.AbstractAlgorithm) + V_cod = fuse(codomain(t)) + V_dom = fuse(domain(t)) + U = similar(t, domain(t) ← V_cod) + S = similar(t, real(scalartype(t)), V_cod ← V_dom) + Vᴴ = similar(t, domain(t) ← V_dom) + return U, S, Vᴴ +end + +function MatrixAlgebraKit.initialize_output(::typeof(svd_compact!), t::AbstractTensorMap, + ::MatrixAlgebraKit.AbstractAlgorithm) + V_cod = V_dom = infimum(fuse(codomain(t)), fuse(domain(t))) + U = similar(t, domain(t) ← V_cod) + S = DiagonalTensorMap{real(scalartype(t))}(undef, V_cod ← V_dom) + Vᴴ = similar(t, domain(t) ← V_dom) + return U, S, Vᴴ +end + +# TODO: svd_vals + +function MatrixAlgebraKit.svd_full!(t::AbstractTensorMap, (U, S, Vᴴ), + alg::BlockAlgorithm) + MatrixAlgebraKit.check_input(svd_full!, t, (U, S, Vᴴ)) + + foreachblock(t, U, S, Vᴴ; alg.scheduler) do _, (b, u, s, vᴴ) + if isempty(b) # TODO: remove once MatrixAlgebraKit supports empty matrices + one!(length(u) > 0 ? u : vᴴ) + zerovector!(s) + else + u′, s′, vᴴ′ = MatrixAlgebraKit.svd_full!(b, (u, s, vᴴ), alg.alg) + # deal with the case where the output is not the same as the input + u === u′ || copyto!(u, u′) + s === s′ || copyto!(s, s′) + vᴴ === vᴴ′ || copyto!(vᴴ, vᴴ′) + end + return nothing + end + + return U, S, Vᴴ +end + +function MatrixAlgebraKit.svd_compact!(t::AbstractTensorMap, (U, S, Vᴴ), + alg::BlockAlgorithm) + MatrixAlgebraKit.check_input(svd_compact!, t, (U, S, Vᴴ)) + + foreachblock(t, U, S, Vᴴ; alg.scheduler) do _, (b, u, s, vᴴ) + u′, s′, vᴴ′ = svd_compact!(b, (u, s, vᴴ), alg.alg) + # deal with the case where the output is not the same as the input + u === u′ || copyto!(u, u′) + s === s′ || copyto!(s, s′) + vᴴ === vᴴ′ || copyto!(vᴴ, vᴴ′) + return nothing + end + + return U, S, Vᴴ +end + +function MatrixAlgebraKit.default_svd_algorithm(t::AbstractTensorMap{<:BlasFloat}; + scheduler=default_blockscheduler(t), + kwargs...) + return BlockAlgorithm(LAPACK_DivideAndConquer(; kwargs...), scheduler) end -function factorisation_scalartype(::typeof(MAK.eig_full!), t::AbstractTensorMap) - T = complex(scalartype(t)) - return promote_type(Float32, typeof(zero(T) / sqrt(abs2(one(T))))) +# Eigenvalue decomposition +# ------------------------ +function MatrixAlgebraKit.check_input(::typeof(eigh_full!), t::AbstractTensorMap, (D, V)) + domain(t) == codomain(t) || + throw(ArgumentError("Eigenvalue decomposition requires square input tensor")) + + V_D = fuse(domain(t)) + + (D isa DiagonalTensorMap && + scalartype(D) == real(scalartype(t)) && + V_D == space(D, 1)) || + throw(ArgumentError("`eigh_full!` requires diagonal tensor D with isomorphic domain and real `scalartype`")) + + V isa AbstractTensorMap && + scalartype(V) == scalartype(t) && + space(V) == (codomain(t) ← V_D) || + throw(ArgumentError("`eigh_full!` requires square tensor V with isomorphic domain and equal `scalartype`")) + + return nothing end -function MAK.check_input(::typeof(MAK.eig_full!), t::AbstractTensorMap, (D, V)) +function MatrixAlgebraKit.check_input(::typeof(eig_full!), t::AbstractTensorMap, (D, V)) domain(t) == codomain(t) || throw(ArgumentError("Eigenvalue decomposition requires square input tensor")) Tc = complex(scalartype(t)) + V_D = fuse(domain(t)) (D isa DiagonalTensorMap && scalartype(D) == Tc && - fuse(domain(t)) == space(D, 1)) || + V_D == space(D, 1)) || throw(ArgumentError("`eig_full!` requires diagonal tensor D with isomorphic domain and complex `scalartype`")) V isa AbstractTensorMap && scalartype(V) == Tc && - space(V) == (codomain(t) ← codomain(D)) || + space(V) == (codomain(t) ← V_D) || throw(ArgumentError("`eig_full!` requires square tensor V with isomorphic domain and complex `scalartype`")) return nothing end -function MAK.initialize_output(::typeof(MAK.eig_full!), t::AbstractTensorMap, - ::MAK.LAPACK_EigAlgorithm) +function MatrixAlgebraKit.initialize_output(::typeof(eigh_full!), t::AbstractTensorMap, + ::MatrixAlgebraKit.AbstractAlgorithm) + V_D = fuse(domain(t)) + T = real(scalartype(t)) + D = DiagonalTensorMap{T}(undef, V_D) + V = similar(t, codomain(t) ← V_D) + return D, V +end + +function MatrixAlgebraKit.initialize_output(::typeof(eig_full!), t::AbstractTensorMap, + ::MatrixAlgebraKit.AbstractAlgorithm) + V_D = fuse(domain(t)) Tc = complex(scalartype(t)) - V_diag = fuse(domain(t)) - return DiagonalTensorMap{Tc}(undef, V_diag), similar(t, Tc, domain(t) ← V_diag) + D = DiagonalTensorMap{Tc}(undef, V_D) + V = similar(t, Tc, codomain(t) ← V_D) + return D, V end -function MAK.eig_full!(t::AbstractTensorMap, (D, V), alg::MAK.LAPACK_EigAlgorithm) - MAK.check_input(MAK.eig_full!, t, (D, V)) - foreachblock(t, D, V) do (_, (b, d, v)) - d′, v′ = MAK.eig_full!(b, (d, v), alg) - # deal with the case where the output is not the same as the input - d === d′ || copyto!(d, d′) - v === v′ || copyto!(v, v′) - return nothing +for f in (:eigh_full!, :eig_full!) + @eval function MatrixAlgebraKit.$f(t::AbstractTensorMap, (D, V), + alg::BlockAlgorithm) + MatrixAlgebraKit.check_input($f, t, (D, V)) + + foreachblock(t, D, V; alg.scheduler) do _, (b, d, v) + d′, v′ = $f(b, (d, v), alg.alg) + # deal with the case where the output is not the same as the input + d === d′ || copyto!(d, d′) + v === v′ || copyto!(v, v′) + return nothing + end + + return D, V end - return D, V end -function MAK.default_eig_algorithm(::TensorMap{<:LinearAlgebra.BlasFloat}; kwargs...) - return MAK.LAPACK_Expert(; kwargs...) +function MatrixAlgebraKit.default_eig_algorithm(t::AbstractTensorMap{<:BlasFloat}; + scheduler=default_blockscheduler(t), + kwargs...) + return BlockAlgorithm(LAPACK_Expert(; kwargs...), scheduler) +end +function MatrixAlgebraKit.default_eigh_algorithm(t::AbstractTensorMap{<:BlasFloat}; + scheduler=default_blockscheduler(t), + kwargs...) + return BlockAlgorithm(LAPACK_MultipleRelativelyRobustRepresentations(; kwargs...), + scheduler) end From 45075b0e479c01fd8d29df71e3ee28fd2ee85e20 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 20 Mar 2025 09:12:38 -0400 Subject: [PATCH 08/70] Start switching more factorizations over --- src/tensors/factorizations.jl | 102 +++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 45 deletions(-) diff --git a/src/tensors/factorizations.jl b/src/tensors/factorizations.jl index 0825fe10a..877fc504c 100644 --- a/src/tensors/factorizations.jl +++ b/src/tensors/factorizations.jl @@ -222,7 +222,10 @@ matrices. See the corresponding documentation for more information. See also `eigen` and `eigh`. """ -eig(t::AbstractTensorMap, p::Index2Tuple; kwargs...) = MAK.eig_full(t; kwargs...) +function eig(t::AbstractTensorMap, p::Index2Tuple; kwargs...) + tcopy = permutedcopy_oftype(t, factorisation_scalartype(eig, t), p) + return eig!(tcopy; kwargs...) +end """ eigh(t::AbstractTensorMap, (leftind, rightind)::Index2Tuple) -> D, V @@ -528,6 +531,12 @@ function tsvd!(t::AdjointTensorMap; trunc=NoTruncation(), p::Real=2, alg=SDD()) end # implementation dispatches on algorithm +function _tsvd!(t::TensorMap{<:BlasFloat}, alg::Union{SVD,SDD}, + ::NoTruncation, p::Real=2) + scheduler = default_blockscheduler(t) + svd_alg = alg isa SDD ? LAPACK_DivideAndConquer() : LAPACK_QRIteration() + return MatrixAlgebraKit.svd_compact!(t; alg=BlockAlgorithm(svd_alg, scheduler)) +end function _tsvd!(t::TensorMap{<:RealOrComplexFloat}, alg::Union{SVD,SDD}, trunc::TruncationScheme, p::Real=2) # early return @@ -614,50 +623,53 @@ function LinearAlgebra.eigvals!(t::AdjointTensorMap{<:RealOrComplexFloat}; kwarg for (c, b) in blocks(t)) end -function eigh!(t::TensorMap{<:RealOrComplexFloat}) - InnerProductStyle(t) === EuclideanInnerProduct() || throw_invalid_innerproduct(:eigh!) - domain(t) == codomain(t) || - throw(SpaceMismatch("`eigh!` requires domain and codomain to be the same")) - - T = scalartype(t) - I = sectortype(t) - S = spacetype(t) - dims = SectorDict{I,Int}(c => size(b, 1) for (c, b) in blocks(t)) - W = S(dims) - - Tr = real(T) - A = similarstoragetype(t, Tr) - D = DiagonalTensorMap{Tr,S,A}(undef, W) - V = similar(t, domain(t) ← W) - for (c, b) in blocks(t) - values, vectors = MatrixAlgebra.eigh!(b) - copy!(block(D, c), Diagonal(values)) - copy!(block(V, c), vectors) - end - return D, V -end - -function eig!(t::TensorMap{<:RealOrComplexFloat}; kwargs...) - domain(t) == codomain(t) || - throw(SpaceMismatch("`eig!` requires domain and codomain to be the same")) - - T = scalartype(t) - I = sectortype(t) - S = spacetype(t) - dims = SectorDict{I,Int}(c => size(b, 1) for (c, b) in blocks(t)) - W = S(dims) - - Tc = complex(T) - A = similarstoragetype(t, Tc) - D = DiagonalTensorMap{Tc,S,A}(undef, W) - V = similar(t, Tc, domain(t) ← W) - for (c, b) in blocks(t) - values, vectors = MatrixAlgebra.eig!(b; kwargs...) - copy!(block(D, c), Diagonal(values)) - copy!(block(V, c), vectors) - end - return D, V -end +eigh!(t::TensorMap{<:RealOrComplexFloat}) = eigh_full!(t) +eig!(t::TensorMap{<:RealOrComplexFloat}) = eig_full!(t) + +# function eigh!(t::TensorMap{<:RealOrComplexFloat}) +# InnerProductStyle(t) === EuclideanInnerProduct() || throw_invalid_innerproduct(:eigh!) +# domain(t) == codomain(t) || +# throw(SpaceMismatch("`eigh!` requires domain and codomain to be the same")) + +# T = scalartype(t) +# I = sectortype(t) +# S = spacetype(t) +# dims = SectorDict{I,Int}(c => size(b, 1) for (c, b) in blocks(t)) +# W = S(dims) + +# Tr = real(T) +# A = similarstoragetype(t, Tr) +# D = DiagonalTensorMap{Tr,S,A}(undef, W) +# V = similar(t, domain(t) ← W) +# for (c, b) in blocks(t) +# values, vectors = MatrixAlgebra.eigh!(b) +# copy!(block(D, c), Diagonal(values)) +# copy!(block(V, c), vectors) +# end +# return D, V +# end + +# function eig!(t::TensorMap{<:RealOrComplexFloat}; kwargs...) +# domain(t) == codomain(t) || +# throw(SpaceMismatch("`eig!` requires domain and codomain to be the same")) + +# T = scalartype(t) +# I = sectortype(t) +# S = spacetype(t) +# dims = SectorDict{I,Int}(c => size(b, 1) for (c, b) in blocks(t)) +# W = S(dims) + +# Tc = complex(T) +# A = similarstoragetype(t, Tc) +# D = DiagonalTensorMap{Tc,S,A}(undef, W) +# V = similar(t, Tc, domain(t) ← W) +# for (c, b) in blocks(t) +# values, vectors = MatrixAlgebra.eig!(b; kwargs...) +# copy!(block(D, c), Diagonal(values)) +# copy!(block(V, c), vectors) +# end +# return D, V +# end #--------------------------------------------------# # Checks for hermiticity and positive definiteness # From d2a007114032849a95a20ff30dc423526a3a8606 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 20 Mar 2025 09:32:07 -0400 Subject: [PATCH 09/70] Improve `svd` error messages --- src/tensors/matrixalgebrakit.jl | 69 ++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/src/tensors/matrixalgebrakit.jl b/src/tensors/matrixalgebrakit.jl index 36993331d..9f563ca8f 100644 --- a/src/tensors/matrixalgebrakit.jl +++ b/src/tensors/matrixalgebrakit.jl @@ -9,6 +9,18 @@ for f in (:eig_full, :eig_vals, :eig_trunc, :eigh_full, :eigh_vals, :eigh_trunc, end end +# TODO: move to MatrixAlgebraKit? +macro check_eltype(x, y, f=:identity, g=:eltype) + msg = "unexpected scalar type: " + msg *= string(g) * "(" * string(x) * ") != " + if f == :identity + msg *= string(g) * "(" * string(y) * ")" + else + msg *= string(f) * "(" * string(y) * ")" + end + return :($g($x) == $f($g($y)) || throw(ArgumentError($msg))) +end + # function factorisation_scalartype(::typeof(MAK.eig_full!), t::AbstractTensorMap) # T = scalartype(t) # return promote_type(Float32, typeof(zero(T) / sqrt(abs2(one(T))))) @@ -16,42 +28,43 @@ end # Singular value decomposition # ---------------------------- -function MatrixAlgebraKit.check_input(::typeof(svd_full!), t::AbstractTensorMap, (U, S, Vᴴ)) +const T_USVᴴ = Tuple{<:AbstractTensorMap,<:AbstractTensorMap,<:AbstractTensorMap} + +function MatrixAlgebraKit.check_input(::typeof(svd_full!), t::AbstractTensorMap, + (U, S, Vᴴ)::T_USVᴴ) + # scalartype checks + @check_eltype U t + @check_eltype S t real + @check_eltype Vᴴ t + + # space checks V_cod = fuse(codomain(t)) V_dom = fuse(domain(t)) - - (U isa AbstractTensorMap && - scalartype(U) == scalartype(t) && - space(U) == (codomain(t) ← V_cod)) || - throw(ArgumentError("`svd_full!` requires unitary tensor U with same `scalartype`")) - (S isa AbstractTensorMap && - scalartype(S) == real(scalartype(t)) && - space(S) == (V_cod ← V_dom)) || - throw(ArgumentError("`svd_full!` requires rectangular tensor S with real `scalartype`")) - (Vᴴ isa AbstractTensorMap && - scalartype(Vᴴ) == scalartype(t) && - space(Vᴴ) == (V_dom ← domain(t))) || - throw(ArgumentError("`svd_full!` requires unitary tensor Vᴴ with same `scalartype`")) + space(U) == (codomain(t) ← V_cod) || + throw(SpaceMismatch("`svd_full!(t, (U, S, Vᴴ))` requires `space(U) == (codomain(t) ← fuse(domain(t)))`")) + space(S) == (V_cod ← V_dom) || + throw(SpaceMismatch("`svd_full!(t, (U, S, Vᴴ))` requires `space(S) == (fuse(codomain(t)) ← fuse(domain(t))`")) + space(Vᴴ) == (V_dom ← domain(t)) || + throw(SpaceMismatch("`svd_full!(t, (U, S, Vᴴ))` requires `space(Vᴴ) == (fuse(domain(t)) ← domain(t))`")) return nothing end function MatrixAlgebraKit.check_input(::typeof(svd_compact!), t::AbstractTensorMap, - (U, S, Vᴴ)) - V_cod = V_dom = infimum(fuse(codomain(t)), fuse(domain(t))) + (U, S, Vᴴ)::T_USVᴴ) + # scalartype checks + @check_eltype U t + @check_eltype S t real + @check_eltype Vᴴ t - (U isa AbstractTensorMap && - scalartype(U) == scalartype(t) && - space(U) == (codomain(t) ← V_cod)) || - throw(ArgumentError("`svd_compact!` requires isometric tensor U with same `scalartype`")) - (S isa DiagonalTensorMap && - scalartype(S) == real(scalartype(t)) && - space(S) == (V_cod ← V_dom)) || - throw(ArgumentError("`svd_compact!` requires diagonal tensor S with real `scalartype`")) - (Vᴴ isa AbstractTensorMap && - scalartype(Vᴴ) == scalartype(t) && - space(Vᴴ) == (V_dom ← domain(t))) || - throw(ArgumentError("`svd_compact!` requires isometric tensor Vᴴ with same `scalartype`")) + # space checks + V_cod = V_dom = infimum(fuse(codomain(t)), fuse(domain(t))) + space(U) == (codomain(t) ← V_cod) || + throw(SpaceMismatch("`svd_compact!(t, (U, S, Vᴴ))` requires `space(U) == (codomain(t) ← infimum(fuse(domain(t)), fuse(codomain(t)))`")) + space(S) == (V_cod ← V_dom) || + throw(SpaceMismatch("`svd_compact!(t, (U, S, Vᴴ))` requires diagonal `S` with `domain(S) == (infimum(fuse(codomain(t)), fuse(domain(t)))`")) + space(Vᴴ) == (V_dom ← domain(t)) || + throw(SpaceMismatch("`svd_compact!(t, (U, S, Vᴴ))` requires `space(Vᴴ) == (infimum(fuse(domain(t)), fuse(codomain(t))) ← domain(t))`")) return nothing end From eafe7ea731f6cbadd00a3ea26d3ddc8006f4fc7d Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 20 Mar 2025 09:37:59 -0400 Subject: [PATCH 10/70] more error msg improvements --- src/tensors/matrixalgebrakit.jl | 52 +++++++++++++++++---------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/src/tensors/matrixalgebrakit.jl b/src/tensors/matrixalgebrakit.jl index 9f563ca8f..7858e6c8f 100644 --- a/src/tensors/matrixalgebrakit.jl +++ b/src/tensors/matrixalgebrakit.jl @@ -28,10 +28,11 @@ end # Singular value decomposition # ---------------------------- -const T_USVᴴ = Tuple{<:AbstractTensorMap,<:AbstractTensorMap,<:AbstractTensorMap} +const _T_USVᴴ = Tuple{<:AbstractTensorMap,<:AbstractTensorMap,<:AbstractTensorMap} +const _T_USVᴴ_diag = Tuple{<:AbstractTensorMap,<:DiagonalTensorMap,<:AbstractTensorMap} function MatrixAlgebraKit.check_input(::typeof(svd_full!), t::AbstractTensorMap, - (U, S, Vᴴ)::T_USVᴴ) + (U, S, Vᴴ)::_T_USVᴴ) # scalartype checks @check_eltype U t @check_eltype S t real @@ -51,7 +52,7 @@ function MatrixAlgebraKit.check_input(::typeof(svd_full!), t::AbstractTensorMap, end function MatrixAlgebraKit.check_input(::typeof(svd_compact!), t::AbstractTensorMap, - (U, S, Vᴴ)::T_USVᴴ) + (U, S, Vᴴ)::_T_USVᴴ_diag) # scalartype checks @check_eltype U t @check_eltype S t real @@ -137,40 +138,41 @@ end # Eigenvalue decomposition # ------------------------ -function MatrixAlgebraKit.check_input(::typeof(eigh_full!), t::AbstractTensorMap, (D, V)) +const _T_DV = Tuple{<:DiagonalTensorMap,<:AbstractTensorMap} +function MatrixAlgebraKit.check_input(::typeof(eigh_full!), t::AbstractTensorMap, + (D, V)::_T_DV) domain(t) == codomain(t) || throw(ArgumentError("Eigenvalue decomposition requires square input tensor")) - V_D = fuse(domain(t)) - - (D isa DiagonalTensorMap && - scalartype(D) == real(scalartype(t)) && - V_D == space(D, 1)) || - throw(ArgumentError("`eigh_full!` requires diagonal tensor D with isomorphic domain and real `scalartype`")) + # scalartype checks + @check_eltype D t real + @check_eltype V t - V isa AbstractTensorMap && - scalartype(V) == scalartype(t) && - space(V) == (codomain(t) ← V_D) || - throw(ArgumentError("`eigh_full!` requires square tensor V with isomorphic domain and equal `scalartype`")) + # space checks + V_D = fuse(domain(t)) + V_D == space(D, 1) || + throw(SpaceMismatch("`eigh_full!(t, (D, V))` requires diagonal `D` with `domain(D) == fuse(domain(t))`")) + space(V) == (codomain(t) ← V_D) || + throw(SpaceMismatch("`eigh_full!(t, (D, V))` requires `space(V) == (codomain(t) ← fuse(domain(t)))`")) return nothing end -function MatrixAlgebraKit.check_input(::typeof(eig_full!), t::AbstractTensorMap, (D, V)) +function MatrixAlgebraKit.check_input(::typeof(eig_full!), t::AbstractTensorMap, + (D, V)::_T_DV) domain(t) == codomain(t) || throw(ArgumentError("Eigenvalue decomposition requires square input tensor")) - Tc = complex(scalartype(t)) - V_D = fuse(domain(t)) - (D isa DiagonalTensorMap && - scalartype(D) == Tc && - V_D == space(D, 1)) || - throw(ArgumentError("`eig_full!` requires diagonal tensor D with isomorphic domain and complex `scalartype`")) + # scalartype checks + @check_eltype D t complex + @check_eltype V t complex - V isa AbstractTensorMap && - scalartype(V) == Tc && - space(V) == (codomain(t) ← V_D) || - throw(ArgumentError("`eig_full!` requires square tensor V with isomorphic domain and complex `scalartype`")) + # space checks + V_D = fuse(domain(t)) + V_D == space(D, 1) || + throw(SpaceMismatch("`eig_full!(t, (D, V))` requires diagonal `D` with `domain(D) == fuse(domain(t))`")) + space(V) == (codomain(t) ← V_D) || + throw(SpaceMismatch("`eig_full!(t, (D, V))` requires `space(V) == (codomain(t) ← fuse(domain(t)))`")) return nothing end From dc594ac249d69fc399937e899c62a19c2fc65c0c Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 20 Mar 2025 11:42:26 -0400 Subject: [PATCH 11/70] Properly escape macro hygiene --- src/tensors/matrixalgebrakit.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tensors/matrixalgebrakit.jl b/src/tensors/matrixalgebrakit.jl index 7858e6c8f..f3fbd8039 100644 --- a/src/tensors/matrixalgebrakit.jl +++ b/src/tensors/matrixalgebrakit.jl @@ -18,7 +18,7 @@ macro check_eltype(x, y, f=:identity, g=:eltype) else msg *= string(f) * "(" * string(y) * ")" end - return :($g($x) == $f($g($y)) || throw(ArgumentError($msg))) + return esc(:($g($x) == $f($g($y)) || throw(ArgumentError($msg)))) end # function factorisation_scalartype(::typeof(MAK.eig_full!), t::AbstractTensorMap) From ae1093091c1c0e829932aef4cc152b1d7c538643 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 20 Mar 2025 11:44:58 -0400 Subject: [PATCH 12/70] Fix SVD spaces --- src/tensors/matrixalgebrakit.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tensors/matrixalgebrakit.jl b/src/tensors/matrixalgebrakit.jl index f3fbd8039..3a870bae2 100644 --- a/src/tensors/matrixalgebrakit.jl +++ b/src/tensors/matrixalgebrakit.jl @@ -78,16 +78,16 @@ function MatrixAlgebraKit.initialize_output(::typeof(svd_full!), t::AbstractTens V_dom = fuse(domain(t)) U = similar(t, domain(t) ← V_cod) S = similar(t, real(scalartype(t)), V_cod ← V_dom) - Vᴴ = similar(t, domain(t) ← V_dom) + Vᴴ = similar(t, V_dom ← domain(t)) return U, S, Vᴴ end function MatrixAlgebraKit.initialize_output(::typeof(svd_compact!), t::AbstractTensorMap, ::MatrixAlgebraKit.AbstractAlgorithm) V_cod = V_dom = infimum(fuse(codomain(t)), fuse(domain(t))) - U = similar(t, domain(t) ← V_cod) - S = DiagonalTensorMap{real(scalartype(t))}(undef, V_cod ← V_dom) - Vᴴ = similar(t, domain(t) ← V_dom) + U = similar(t, codomain(t) ← V_cod) + S = DiagonalTensorMap{real(scalartype(t))}(undef, V_cod) + Vᴴ = similar(t, V_dom ← domain(t)) return U, S, Vᴴ end From 91a6540f53a0936004361f5bbac37c71b19a664f Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 20 Mar 2025 12:44:29 -0400 Subject: [PATCH 13/70] Also return `truncerr` --- src/tensors/factorizations.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tensors/factorizations.jl b/src/tensors/factorizations.jl index 877fc504c..c1da8f5e3 100644 --- a/src/tensors/factorizations.jl +++ b/src/tensors/factorizations.jl @@ -535,7 +535,8 @@ function _tsvd!(t::TensorMap{<:BlasFloat}, alg::Union{SVD,SDD}, ::NoTruncation, p::Real=2) scheduler = default_blockscheduler(t) svd_alg = alg isa SDD ? LAPACK_DivideAndConquer() : LAPACK_QRIteration() - return MatrixAlgebraKit.svd_compact!(t; alg=BlockAlgorithm(svd_alg, scheduler)) + return MatrixAlgebraKit.svd_compact!(t; alg=BlockAlgorithm(svd_alg, scheduler))..., + zero(real(scalartype(t))) end function _tsvd!(t::TensorMap{<:RealOrComplexFloat}, alg::Union{SVD,SDD}, trunc::TruncationScheme, p::Real=2) From 63d30832d8e321f4618851752b775d476b3cd77c Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 2 Apr 2025 14:02:00 -0400 Subject: [PATCH 14/70] Add `setdiff` for ElementarySpace --- src/spaces/cartesianspace.jl | 5 +++++ src/spaces/complexspace.jl | 6 ++++++ src/spaces/gradedspace.jl | 6 ++++++ src/spaces/vectorspaces.jl | 8 ++++++++ 4 files changed, 25 insertions(+) diff --git a/src/spaces/cartesianspace.jl b/src/spaces/cartesianspace.jl index f29ed263d..fd38e0c1e 100644 --- a/src/spaces/cartesianspace.jl +++ b/src/spaces/cartesianspace.jl @@ -56,4 +56,9 @@ flip(V::CartesianSpace) = V infimum(V₁::CartesianSpace, V₂::CartesianSpace) = CartesianSpace(min(V₁.d, V₂.d)) supremum(V₁::CartesianSpace, V₂::CartesianSpace) = CartesianSpace(max(V₁.d, V₂.d)) +function Base.setdiff(V::CartesianSpace, W::CartesianSpace) + V ≿ W || throw(ArgumentError("$(W) is not a subspace of $(V)")) + return CartesianSpace(dim(V) - dim(W)) +end + Base.show(io::IO, V::CartesianSpace) = print(io, "ℝ^$(V.d)") diff --git a/src/spaces/complexspace.jl b/src/spaces/complexspace.jl index ff05888b8..51a3056e9 100644 --- a/src/spaces/complexspace.jl +++ b/src/spaces/complexspace.jl @@ -69,4 +69,10 @@ function supremum(V₁::ComplexSpace, V₂::ComplexSpace) throw(SpaceMismatch("Supremum of space and dual space does not exist")) end +function Base.setdiff(V::ComplexSpace, W::ComplexSpace) + (V ≿ W && isdual(V) == isdual(W)) || + throw(ArgumentError("$(W) is not a subspace of $(V)")) + return ComplexSpace(dim(V) - dim(W), isdual(V)) +end + Base.show(io::IO, V::ComplexSpace) = print(io, isdual(V) ? "(ℂ^$(V.d))'" : "ℂ^$(V.d)") diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index 00f97c962..ae7e770ae 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -187,6 +187,12 @@ function supremum(V₁::GradedSpace{I}, V₂::GradedSpace{I}) where {I<:Sector} end end +function Base.setdiff(V::GradedSpace{I}, W::GradedSpace{I}) where {I<:Sector} + V ≿ W && isdual(V) == isdual(W) || + throw(SpaceMismatch("$(W) is not a subspace of $(V)")) + return typeof(V)(c => dim(V, c) - dim(W, c) for c in sectors(V)) +end + function Base.show(io::IO, V::GradedSpace{I}) where {I<:Sector} print(io, type_repr(typeof(V)), "(") seperator = "" diff --git a/src/spaces/vectorspaces.jl b/src/spaces/vectorspaces.jl index 0eb647fc6..91f5b1052 100644 --- a/src/spaces/vectorspaces.jl +++ b/src/spaces/vectorspaces.jl @@ -405,3 +405,11 @@ have the same value. function supremum(V₁::S, V₂::S, V₃::S...) where {S<:ElementarySpace} return supremum(supremum(V₁, V₂), V₃...) end + +""" + setdiff(V::ElementarySpace, W::ElementarySpace) + +Return the set difference of two elementary spaces, i.e. an instance `X::ElementarySpace` +such that `V = W ⊕ X`. +""" +Base.setdiff(V₁::S, V₂::S) where {S<:ElementarySpace} From cf9a34dd355129c8c1d0bb07cb181082ed1a0495 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 2 Apr 2025 14:06:55 -0400 Subject: [PATCH 15/70] Add `qr_` implementations --- src/tensors/matrixalgebrakit.jl | 393 ++++++++++++++++++++++++++++++++ test/factorizations.jl | 218 ++++++++++++++++++ test/paul.jl | 65 ++++++ 3 files changed, 676 insertions(+) create mode 100644 test/factorizations.jl create mode 100644 test/paul.jl diff --git a/src/tensors/matrixalgebrakit.jl b/src/tensors/matrixalgebrakit.jl index 3a870bae2..dd45d6203 100644 --- a/src/tensors/matrixalgebrakit.jl +++ b/src/tensors/matrixalgebrakit.jl @@ -223,3 +223,396 @@ function MatrixAlgebraKit.default_eigh_algorithm(t::AbstractTensorMap{<:BlasFloa return BlockAlgorithm(LAPACK_MultipleRelativelyRobustRepresentations(; kwargs...), scheduler) end + +# QR decomposition +# ---------------- +function MatrixAlgebraKit.check_input(::typeof(qr_full!), t::AbstractTensorMap, + (Q, + R)::Tuple{<:AbstractTensorMap,<:AbstractTensorMap}) + # scalartype checks + @check_eltype Q t + @check_eltype R t + + # space checks + V_Q = fuse(codomain(t)) + space(Q) == (codomain(t) ← V_Q) || + throw(SpaceMismatch("`qr_full!(t, (Q, R))` requires `space(Q) == (codomain(t) ← fuse(codomain(t)))`")) + space(R) == (V_Q ← domain(t)) || + throw(SpaceMismatch("`qr_full!(t, (Q, R))` requires `space(R) == (fuse(codomain(t)) ← domain(t)`")) + + return nothing +end + +function MatrixAlgebraKit.check_input(::typeof(qr_compact!), t::AbstractTensorMap, (Q, R)) + # scalartype checks + @check_eltype Q t + @check_eltype R t + + # space checks + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + space(Q) == (codomain(t) ← V_Q) || + throw(SpaceMismatch("`qr_compact!(t, (Q, R))` requires `space(Q) == (codomain(t) ← infimum(fuse(codomain(t)), fuse(domain(t)))`")) + space(R) == (V_Q ← domain(t)) || + throw(SpaceMismatch("`qr_compact!(t, (Q, R))` requires `space(R) == (infimum(fuse(codomain(t)), fuse(domain(t))) ← domain(t))`")) + + return nothing +end + +function MatrixAlgebraKit.check_input(::typeof(qr_null!), t::AbstractTensorMap, + N::AbstractTensorMap) + # scalartype checks + @check_eltype N t + + # space checks + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + V_N = setdiff(fuse(codomain(t)), V_Q) + space(N) == (codomain(t) ← V_N) || + throw(SpaceMismatch("`qr_null!(t, N)` requires `space(N) == (codomain(t) ← setdiff(fuse(codomain(t)), infimum(fuse(codomain(t)), fuse(domain(t))))`")) + + return nothing +end + +function MatrixAlgebraKit.initialize_output(::typeof(qr_full!), t::AbstractTensorMap, + ::MatrixAlgebraKit.AbstractAlgorithm) + V_Q = fuse(codomain(t)) + Q = similar(t, codomain(t) ← V_Q) + R = similar(t, V_Q ← domain(t)) + return Q, R +end + +function MatrixAlgebraKit.initialize_output(::typeof(qr_compact!), t::AbstractTensorMap, + ::MatrixAlgebraKit.AbstractAlgorithm) + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + Q = similar(t, codomain(t) ← V_Q) + R = similar(t, V_Q ← domain(t)) + return Q, R +end + +function MatrixAlgebraKit.initialize_output(::typeof(qr_null!), t::AbstractTensorMap, + ::MatrixAlgebraKit.AbstractAlgorithm) + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + V_N = setdiff(fuse(codomain(t)), V_Q) + N = similar(t, codomain(t) ← V_N) + return N +end + +function MatrixAlgebraKit.qr_full!(t::AbstractTensorMap, (Q, R), + alg::BlockAlgorithm) + MatrixAlgebraKit.check_input(qr_full!, t, (Q, R)) + + foreachblock(t, Q, R; alg.scheduler) do _, (b, q, r) + q′, r′ = qr_full!(b, (q, r), alg.alg) + # deal with the case where the output is not the same as the input + q === q′ || copyto!(q, q′) + r === r′ || copyto!(r, r′) + return nothing + end + + return Q, R +end + +function MatrixAlgebraKit.qr_compact!(t::AbstractTensorMap, (Q, R), + alg::BlockAlgorithm) + MatrixAlgebraKit.check_input(qr_compact!, t, (Q, R)) + + foreachblock(t, Q, R; alg.scheduler) do _, (b, q, r) + q′, r′ = qr_compact!(b, (q, r), alg.alg) + # deal with the case where the output is not the same as the input + q === q′ || copyto!(q, q′) + r === r′ || copyto!(r, r′) + return nothing + end + + return Q, R +end + +function MatrixAlgebraKit.qr_null!(t::AbstractTensorMap, N, alg::BlockAlgorithm) + MatrixAlgebraKit.check_input(qr_null!, t, N) + + foreachblock(t, N; alg.scheduler) do _, (b, n) + n′ = qr_null!(b, n, alg.alg) + # deal with the case where the output is not the same as the input + n === n′ || copyto!(n, n′) + return nothing + end + + return N +end + +function MatrixAlgebraKit.default_qr_algorithm(t::AbstractTensorMap{<:BlasFloat}; + scheduler=default_blockscheduler(t), + kwargs...) + return BlockAlgorithm(LAPACK_HouseholderQR(; kwargs...), scheduler) +end + +# LQ decomposition +# ---------------- +function MatrixAlgebraKit.check_input(::typeof(lq_full!), t::AbstractTensorMap, (L, Q)) + # scalartype checks + @check_eltype L t + @check_eltype Q t + + # space checks + V_Q = fuse(domain(t)) + space(L) == (codomain(t) ← V_Q) || + throw(SpaceMismatch("`lq_full!(t, (L, Q))` requires `space(L) == (codomain(t) ← fuse(domain(t)))`")) + space(Q) == (V_Q ← domain(t)) || + throw(SpaceMismatch("`lq_full!(t, (L, Q))` requires `space(Q) == (fuse(domain(t)) ← domain(t))`")) + + return nothing +end + +function MatrixAlgebraKit.check_input(::typeof(lq_compact!), t::AbstractTensorMap, (L, Q)) + # scalartype checks + @check_eltype L t + @check_eltype Q t + + # space checks + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + space(L) == (codomain(t) ← V_Q) || + throw(SpaceMismatch("`lq_compact!(t, (L, Q))` requires `space(L) == infimum(fuse(codomain(t)), fuse(domain(t)))`")) + space(Q) == (V_Q ← domain(t)) || + throw(SpaceMismatch("`lq_compact!(t, (L, Q))` requires `space(Q) == infimum(fuse(codomain(t)), fuse(domain(t)))`")) + + return nothing +end + +function MatrixAlgebraKit.check_input(::typeof(lq_null!), t::AbstractTensorMap, N) + # scalartype checks + @check_eltype N t + + # space checks + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + V_N = setdiff(fuse(domain(t)), V_Q) + space(N) == (V_N ← domain(t)) || + throw(SpaceMismatch("`lq_null!(t, N)` requires `space(N) == setdiff(fuse(domain(t)), infimum(fuse(codomain(t)), fuse(domain(t)))`")) + + return nothing +end + +function MatrixAlgebraKit.initialize_output(::typeof(lq_full!), t::AbstractTensorMap, + ::MatrixAlgebraKit.AbstractAlgorithm) + V_Q = fuse(domain(t)) + L = similar(t, codomain(t) ← V_Q) + Q = similar(t, V_Q ← domain(t)) + return L, Q +end + +function MatrixAlgebraKit.initialize_output(::typeof(lq_compact!), t::AbstractTensorMap, + ::MatrixAlgebraKit.AbstractAlgorithm) + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + L = similar(t, codomain(t) ← V_Q) + Q = similar(t, V_Q ← domain(t)) + return L, Q +end + +function MatrixAlgebraKit.initialize_output(::typeof(lq_null!), t::AbstractTensorMap, + ::MatrixAlgebraKit.AbstractAlgorithm) + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + V_N = setdiff(fuse(domain(t)), V_Q) + N = similar(t, V_N ← domain(t)) + return N +end + +function MatrixAlgebraKit.lq_full!(t::AbstractTensorMap, (L, Q), + alg::BlockAlgorithm) + MatrixAlgebraKit.check_input(lq_full!, t, (L, Q)) + + foreachblock(t, L, Q; alg.scheduler) do _, (b, l, q) + l′, q′ = lq_full!(b, (l, q), alg.alg) + # deal with the case where the output is not the same as the input + l === l′ || copyto!(l, l′) + q === q′ || copyto!(q, q′) + return nothing + end + + return L, Q +end + +function MatrixAlgebraKit.lq_compact!(t::AbstractTensorMap, (L, Q), + alg::BlockAlgorithm) + MatrixAlgebraKit.check_input(lq_compact!, t, (L, Q)) + + foreachblock(t, L, Q; alg.scheduler) do _, (b, l, q) + l′, q′ = lq_compact!(b, (l, q), alg.alg) + # deal with the case where the output is not the same as the input + l === l′ || copyto!(l, l′) + q === q′ || copyto!(q, q′) + return nothing + end + + return L, Q +end + +function MatrixAlgebraKit.lq_null!(t::AbstractTensorMap, N, alg::BlockAlgorithm) + MatrixAlgebraKit.check_input(lq_null!, t, N) + + foreachblock(t, N; alg.scheduler) do _, (b, n) + n′ = lq_null!(b, n, alg.alg) + # deal with the case where the output is not the same as the input + n === n′ || copyto!(n, n′) + return nothing + end + + return N +end + +# Polar decomposition +# ------------------- +using MatrixAlgebraKit: PolarViaSVD + +function MatrixAlgebraKit.check_input(::typeof(left_polar!), t, (W, P)) + codomain(t) ≿ domain(t) || + throw(ArgumentError("Polar decomposition requires `codomain(t) ≿ domain(t)`")) + + # scalartype checks + @check_eltype W t + @check_eltype P t + + # space checks + space(W) == (codomain(t) ← fuse(domain(t))) || + throw(SpaceMismatch("`left_polar!(t, (W, P))` requires `space(W) == (codomain(t) ← domain(t))`")) + space(P) == (fuse(domain(t)) ← domain(t)) || + throw(SpaceMismatch("`left_polar!(t, (W, P))` requires `space(P) == (domain(t) ← domain(t))`")) + + return nothing +end + +# TODO: do we really not want to fuse the spaces? +function MatrixAlgebraKit.initialize_output(::typeof(left_polar!), t::AbstractTensorMap) + W = similar(t, codomain(t) ← fuse(domain(t))) + P = similar(t, fuse(domain(t)) ← domain(t)) + return W, P +end + +function MatrixAlgebraKit.left_polar!(t::AbstractTensorMap, WP, alg::BlockAlgorithm) + MatrixAlgebraKit.check_input(left_polar!, t, WP) + + foreachblock(t, WP...; alg.scheduler) do _, (b, w, p) + w′, p′ = left_polar!(b, (w, p), alg.alg) + # deal with the case where the output is not the same as the input + w === w′ || copyto!(w, w′) + p === p′ || copyto!(p, p′) + return nothing + end + + return WP +end + +function MatrixAlgebraKit.default_polar_algorithm(t::AbstractTensorMap{<:BlasFloat}; + scheduler=default_blockscheduler(t), + kwargs...) + return BlockAlgorithm(PolarViaSVD(LAPACK_DivideAndConquer(; kwargs...)), + scheduler) +end + +# Orthogonalization +# ----------------- +function MatrixAlgebraKit.check_input(::typeof(left_orth!), t::AbstractTensorMap, (V, C)) + # scalartype checks + @check_eltype V t + isnothing(C) || @check_eltype C t + + # space checks + V_C = infimum(fuse(codomain(t)), fuse(domain(t))) + space(V) == (codomain(t) ← V_C) || + throw(SpaceMismatch("`left_orth!(t, (V, C))` requires `space(V) == (codomain(t) ← infimum(fuse(codomain(t)), fuse(domain(t))))`")) + isnothing(C) || space(C) == (V_C ← domain(t)) || + throw(SpaceMismatch("`left_orth!(t, (V, C))` requires `space(C) == (infimum(fuse(codomain(t)), fuse(domain(t))) ← domain(t))`")) + + return nothing +end + +function MatrixAlgebraKit.check_input(::typeof(right_orth!), t::AbstractTensorMap, (C, Vᴴ)) + # scalartype checks + isnothing(C) || @check_eltype C t + @check_eltype Vᴴ t + + # space checks + V_C = infimum(fuse(codomain(t)), fuse(domain(t))) + isnothing(C) || space(C) == (codomain(t) ← V_C) || + throw(SpaceMismatch("`right_orth!(t, (C, Vᴴ))` requires `space(C) == (codomain(t) ← infimum(fuse(codomain(t)), fuse(domain(t)))`")) + space(Vᴴ) == (V_dom ← domain(t)) || + throw(SpaceMismatch("`right_orth!(t, (C, Vᴴ))` requires `space(Vᴴ) == (infimum(fuse(codomain(t)), fuse(domain(t))) ← domain(t))`")) + + return nothing +end + +function MatrixAlgebraKit.initialize_output(::typeof(left_orth!), t::AbstractTensorMap) + V_C = infimum(fuse(codomain(t)), fuse(domain(t))) + V = similar(t, codomain(t) ← V_C) + C = similar(t, V_C ← domain(t)) + return V, C +end + +function MatrixAlgebraKit.initialize_output(::typeof(right_orth!), t::AbstractTensorMap) + V_C = infimum(fuse(codomain(t)), fuse(domain(t))) + C = similar(t, codomain(t) ← V_C) + Vᴴ = similar(t, V_C ← domain(t)) + return C, Vᴴ +end + +function MatrixAlgebraKit.left_orth!(t::AbstractTensorMap, VC; kwargs...) + MatrixAlgebraKit.check_input(left_orth!, t, VC) + atol = get(kwargs, :atol, 0) + rtol = get(kwargs, :rtol, 0) + kind = get(kwargs, :kind, iszero(atol) && iszero(rtol) ? :qrpos : :svd) + + if !(iszero(atol) && iszero(rtol)) && kind != :svd + throw(ArgumentError("nonzero tolerance not supported for left_orth with kind=$kind")) + end + + if kind == :qr + alg = get(kwargs, :alg, MatrixAlgebraKit.select_algorithm(qr_compact!, t)) + return qr_compact!(t, VC, alg) + elseif kind == :qrpos + alg = get(kwargs, :alg, + MatrixAlgebraKit.select_algorithm(qr_compact!, t; positive=true)) + return qr_compact!(t, VC, alg) + elseif kind == :polar + alg = get(kwargs, :alg, MatrixAlgebraKit.select_algorithm(left_polar!, t)) + return left_polar!(t, VC, alg) + elseif kind == :svd && iszero(atol) && iszero(rtol) + alg = get(kwargs, :alg, MatrixAlgebraKit.select_algorithm(svd_compact!, t)) + V, C = VC + S = DiagonalTensorMap{real(scalartype(t))}(undef, domain(V) ← codomain(C)) + U, S, Vᴴ = svd_compact!(t, (V, S, C), alg) + return U, lmul!(S, Vᴴ) + elseif kind == :svd + alg_svd = MatrixAlgebraKit.select_algorithm(svd_compact!, t) + trunc = MatrixAlgebraKit.TruncationKeepAbove(atol, rtol) + alg = get(kwargs, :alg, MatrixAlgebraKit.TruncatedAlgorithm(alg_svd, trunc)) + V, C = VC + S = DiagonalTensorMap{real(scalartype(t))}(undef, domain(V) ← codomain(C)) + U, S, Vᴴ = svd_trunc!(t, (V, S, C), alg) + return U, lmul!(S, Vᴴ) + else + throw(ArgumentError("`left_orth!` received unknown value `kind = $kind`")) + end +end + +# Truncation +# ---------- +# TODO: technically we could do this truncation in-place, but this might not be worth it +function MatrixAlgebraKit.truncate!(::typeof(svd_trunc!), (U, S, Vᴴ), + trunc::MatrixAlgebraKit.TruncationKeepAbove) + atol = max(trunc.atol, norm(S) * trunc.rtol) + V_truncated = spacetype(S)(c => findlast(>=(atol), b.diag) for (c, b) in blocks(S)) + + Ũ = similar(U, codomain(U) ← V_truncated) + for (c, b) in blocks(Ũ) + copy!(b, @view(block(U, c)[:, 1:size(b, 2)])) + end + + S̃ = DiagonalTensorMap{scalartype(S)}(undef, V_truncated) + for (c, b) in blocks(S̃) + copy!(b.diag, @view(block(S, c).diag[1:size(b, 1)])) + end + + Ṽᴴ = similar(Vᴴ, V_truncated ← domain(Vᴴ)) + for (c, b) in blocks(Ṽᴴ) + copy!(b, @view(block(Vᴴ, c)[1:size(b, 1), :])) + end + + return Ũ, S̃, Ṽᴴ +end diff --git a/test/factorizations.jl b/test/factorizations.jl new file mode 100644 index 000000000..a30f005e3 --- /dev/null +++ b/test/factorizations.jl @@ -0,0 +1,218 @@ +for V in (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂)#, VSU₃) + V1, V2, V3, V4, V5 = V + @assert V3 * V4 * V2 ≿ V1' * V5' # necessary for leftorth tests + @assert V3 * V4 ≾ V1' * V2' * V5' # necessary for rightorth tests +end + +spacelist = try + if ENV["CI"] == "true" + println("Detected running on CI") + if Sys.iswindows() + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂) + elseif Sys.isapple() + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VfU₁, VfSU₂)#, VSU₃) + else + (Vtr, Vℤ₂, Vfℤ₂, VU₁, VCU₁, VSU₂, VfSU₂)#, VSU₃) + end + else + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂)#, VSU₃) + end +catch + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂)#, VSU₃) +end + +@timedtestset "Factorizatios with symmetry: $(sectortype(first(V)))" for V in spacelist + V1, V2, V3, V4, V5 = V + W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 + for T in (Float32, ComplexF64), adj in (false, true) + t = adj ? rand(T, W)' : rand(T, W) + @testset "leftorth with $alg" for alg in + (TensorKit.QR(), TensorKit.QRpos(), + TensorKit.QL(), TensorKit.QLpos(), + TensorKit.Polar(), TensorKit.SVD(), + TensorKit.SDD()) + Q, R = @constinferred leftorth(t, ((3, 4, 2), (1, 5)); alg=alg) + QdQ = Q' * Q + @test QdQ ≈ one(QdQ) + @test Q * R ≈ permute(t, ((3, 4, 2), (1, 5))) + if alg isa Polar + @test isposdef(R) + @test domain(R) == codomain(R) == space(t, 1)' ⊗ space(t, 5)' + end + end + @testset "leftnull with $alg" for alg in + (TensorKit.QR(), TensorKit.SVD(), + TensorKit.SDD()) + N = @constinferred leftnull(t, ((3, 4, 2), (1, 5)); alg=alg) + NdN = N' * N + @test NdN ≈ one(NdN) + @test norm(N' * permute(t, ((3, 4, 2), (1, 5)))) < + 100 * eps(norm(t)) + end + @testset "rightorth with $alg" for alg in + (TensorKit.RQ(), TensorKit.RQpos(), + TensorKit.LQ(), TensorKit.LQpos(), + TensorKit.Polar(), TensorKit.SVD(), + TensorKit.SDD()) + L, Q = @constinferred rightorth(t, ((3, 4), (2, 1, 5)); alg=alg) + QQd = Q * Q' + @test QQd ≈ one(QQd) + @test L * Q ≈ permute(t, ((3, 4), (2, 1, 5))) + if alg isa Polar + @test isposdef(L) + @test domain(L) == codomain(L) == space(t, 3) ⊗ space(t, 4) + end + end + @testset "rightnull with $alg" for alg in + (TensorKit.LQ(), TensorKit.SVD(), + TensorKit.SDD()) + M = @constinferred rightnull(t, ((3, 4), (2, 1, 5)); alg=alg) + MMd = M * M' + @test MMd ≈ one(MMd) + @test norm(permute(t, ((3, 4), (2, 1, 5))) * M') < + 100 * eps(norm(t)) + end + @testset "tsvd with $alg" for alg in (TensorKit.SVD(), TensorKit.SDD()) + U, S, V = @constinferred tsvd(t, ((3, 4, 2), (1, 5)); alg=alg) + UdU = U' * U + @test UdU ≈ one(UdU) + VVd = V * V' + @test VVd ≈ one(VVd) + t2 = permute(t, ((3, 4, 2), (1, 5))) + @test U * S * V ≈ t2 + + s = LinearAlgebra.svdvals(t2) + s′ = LinearAlgebra.diag(S) + for (c, b) in s + @test b ≈ s′[c] + end + end + @testset "cond and rank" begin + t2 = permute(t, ((3, 4, 2), (1, 5))) + d1 = dim(codomain(t2)) + d2 = dim(domain(t2)) + @test rank(t2) == min(d1, d2) + M = leftnull(t2) + @test rank(M) == max(d1, d2) - min(d1, d2) + t3 = unitary(T, V1 ⊗ V2, V1 ⊗ V2) + @test cond(t3) ≈ one(real(T)) + @test rank(t3) == dim(V1 ⊗ V2) + t4 = randn(T, V1 ⊗ V2, V1 ⊗ V2) + t4 = (t4 + t4') / 2 + vals = LinearAlgebra.eigvals(t4) + λmax = maximum(s -> maximum(abs, s), values(vals)) + λmin = minimum(s -> minimum(abs, s), values(vals)) + @test cond(t4) ≈ λmax / λmin + end + end + @testset "empty tensor" begin + for T in (Float32, ComplexF64) + t = randn(T, V1 ⊗ V2, zero(V1)) + @testset "leftorth with $alg" for alg in + (TensorKit.QR(), TensorKit.QRpos(), + TensorKit.QL(), TensorKit.QLpos(), + TensorKit.Polar(), TensorKit.SVD(), + TensorKit.SDD()) + Q, R = @constinferred leftorth(t; alg=alg) + @test Q == t + @test dim(Q) == dim(R) == 0 + end + @testset "leftnull with $alg" for alg in + (TensorKit.QR(), TensorKit.SVD(), + TensorKit.SDD()) + N = @constinferred leftnull(t; alg=alg) + @test N' * N ≈ id(domain(N)) + @test N * N' ≈ id(codomain(N)) + end + @testset "rightorth with $alg" for alg in + (TensorKit.RQ(), TensorKit.RQpos(), + TensorKit.LQ(), TensorKit.LQpos(), + TensorKit.Polar(), TensorKit.SVD(), + TensorKit.SDD()) + L, Q = @constinferred rightorth(copy(t'); alg=alg) + @test Q == t' + @test dim(Q) == dim(L) == 0 + end + @testset "rightnull with $alg" for alg in + (TensorKit.LQ(), TensorKit.SVD(), + TensorKit.SDD()) + M = @constinferred rightnull(copy(t'); alg=alg) + @test M * M' ≈ id(codomain(M)) + @test M' * M ≈ id(domain(M)) + end + @testset "tsvd with $alg" for alg in (TensorKit.SVD(), TensorKit.SDD()) + U, S, V = @constinferred tsvd(t; alg=alg) + @test U == t + @test dim(U) == dim(S) == dim(V) + end + @testset "cond and rank" begin + @test rank(t) == 0 + W2 = zero(V1) * zero(V2) + t2 = rand(W2, W2) + @test rank(t2) == 0 + @test cond(t2) == 0.0 + end + end + end + @testset "eig and isposdef" begin + for T in (Float32, ComplexF64) + t = rand(T, V1 ⊗ V1' ⊗ V2 ⊗ V2') + D, V = eigen(t, ((1, 3), (2, 4))) + t2 = permute(t, ((1, 3), (2, 4))) + @test t2 * V ≈ V * D + + d = LinearAlgebra.eigvals(t2; sortby=nothing) + d′ = LinearAlgebra.diag(D) + for (c, b) in d + @test b ≈ d′[c] + end + + # Somehow moving these test before the previous one gives rise to errors + # with T=Float32 on x86 platforms. Is this an OpenBLAS issue? + VdV = V' * V + VdV = (VdV + VdV') / 2 + @test isposdef(VdV) + + @test !isposdef(t2) # unlikely for non-hermitian map + t2 = (t2 + t2') + D, V = eigen(t2) + VdV = V' * V + @test VdV ≈ one(VdV) + D̃, Ṽ = @constinferred eigh(t2) + @test D ≈ D̃ + @test V ≈ Ṽ + λ = minimum(minimum(real(LinearAlgebra.diag(b))) + for (c, b) in blocks(D)) + @test cond(Ṽ) ≈ one(real(T)) + @test isposdef(t2) == isposdef(λ) + @test isposdef(t2 - λ * one(t2) + 0.1 * one(t2)) + @test !isposdef(t2 - λ * one(t2) - 0.1 * one(t2)) + end + end + @testset "Tensor truncation" begin + for T in (Float32, ComplexF64), p in (1, 2, 3, Inf), adj in (false, true) + t = adj ? rand(T, V1 ⊗ V2 ⊗ V3, V4 ⊗ V5) : rand(T, V4 ⊗ V5, V1 ⊗ V2 ⊗ V3)' + + U₀, S₀, V₀, = tsvd(t) + t = rmul!(t, 1 / norm(S₀, p)) + U, S, V, ϵ = @constinferred tsvd(t; trunc=truncerr(5e-1), p=p) + # @show p, ϵ + # @show domain(S) + # @test min(space(S,1), space(S₀,1)) != space(S₀,1) + U′, S′, V′, ϵ′ = tsvd(t; trunc=truncerr(nextfloat(ϵ)), p=p) + @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) + U′, S′, V′, ϵ′ = tsvd(t; trunc=truncdim(ceil(Int, dim(domain(S)))), + p=p) + @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) + U′, S′, V′, ϵ′ = tsvd(t; trunc=truncspace(space(S, 1)), p=p) + @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) + # results with truncationcutoff cannot be compared because they don't take degeneracy into account, and thus truncate differently + U, S, V, ϵ = tsvd(t; trunc=truncbelow(1 / dim(domain(S₀))), p=p) + # @show p, ϵ + # @show domain(S) + # @test min(space(S,1), space(S₀,1)) != space(S₀,1) + U′, S′, V′, ϵ′ = tsvd(t; trunc=truncspace(space(S, 1)), p=p) + @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) + end + end +end diff --git a/test/paul.jl b/test/paul.jl new file mode 100644 index 000000000..249ed1bae --- /dev/null +++ b/test/paul.jl @@ -0,0 +1,65 @@ +using Zygote, TensorKit + +_safe_pow(a::Real, pow::Real, tol::Real) = (pow < 0 && abs(a) < tol) ? zero(a) : a^pow + +# Element-wise multiplication of TensorMaps respecting block structure +function _elementwise_mult(a₁::AbstractTensorMap, a₂::AbstractTensorMap) + dst = similar(a₁) + for (k, b) in blocks(dst) + copyto!(b, block(a₁, k) .* block(a₂, k)) + end + return dst +end +""" + sdiag_pow(s, pow::Real; tol::Real=eps(scalartype(s))^(3 / 4)) + +Compute `s^pow` for a diagonal matrix `s`. +""" +function sdiag_pow(s::DiagonalTensorMap, pow::Real; tol::Real=eps(scalartype(s))^(3 / 4)) + # Relative tol w.r.t. largest singular value (use norm(∘, Inf) to make differentiable) + tol *= norm(s, Inf) + spow = DiagonalTensorMap(_safe_pow.(s.data, pow, tol), space(s, 1)) + return spow +end +function sdiag_pow(s::AbstractTensorMap{T,S,1,1}, pow::Real; + tol::Real=eps(scalartype(s))^(3 / 4)) where {T,S} + # Relative tol w.r.t. largest singular value (use norm(∘, Inf) to make differentiable) + tol *= norm(s, Inf) + spow = similar(s) + for (k, b) in blocks(s) + copyto!(block(spow, k), + LinearAlgebra.diagm(_safe_pow.(LinearAlgebra.diag(b), pow, tol))) + end + return spow +end + +function ChainRulesCore.rrule(::typeof(sdiag_pow), + s::AbstractTensorMap, + pow::Real; + tol::Real=eps(scalartype(s))^(3 / 4),) + tol *= norm(s, Inf) + spow = sdiag_pow(s, pow; tol) + spow_minus1_conj = scale!(sdiag_pow(s', pow - 1; tol), pow) + function sdiag_pow_pullback(c̄_) + c̄ = unthunk(c̄_) + return (ChainRulesCore.NoTangent(), _elementwise_mult(c̄, spow_minus1_conj)) + end + return spow, sdiag_pow_pullback +end + +function svd_fixed_point(A, U, S, V) + S⁻¹ = sdiag_pow(S, -1) + return (A * V' * S⁻¹ - U, DiagonalTensorMap(U' * A * V' * S⁻¹) - one(S), + S⁻¹ * U' * A - V) +end + +using Zygote + +V = ComplexSpace(3)^2 +A = randn(ComplexF64, V, V) +U, S, V = tsvd(A) + +Zygote.gradient(A, U, S, V) do A, U, S, V + du, ds, dv = svd_fixed_point(A, U, S, V) + return norm(du) + norm(ds) + norm(dv) +end From 1df86d176d0593957e2f20f2775f0322f4a2352e Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 2 Apr 2025 18:22:43 -0400 Subject: [PATCH 16/70] start adding truncated svd --- src/tensors/matrixalgebrakit.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/tensors/matrixalgebrakit.jl b/src/tensors/matrixalgebrakit.jl index dd45d6203..a1f8682c3 100644 --- a/src/tensors/matrixalgebrakit.jl +++ b/src/tensors/matrixalgebrakit.jl @@ -130,6 +130,12 @@ function MatrixAlgebraKit.svd_compact!(t::AbstractTensorMap, (U, S, Vᴴ), return U, S, Vᴴ end +function MatrixAlgebraKit.svd_trunc!(t::AbstractTensorMap, USVᴴ, + alg::MatrixAlgebraKit.TruncatedAlgorithm) + USVᴴ′ = svd_compact!(t, USVᴴ, alg.alg) + return MatrixAlgebraKit.truncate!(svd_trunc!, USVᴴ′, alg.trunc) +end + function MatrixAlgebraKit.default_svd_algorithm(t::AbstractTensorMap{<:BlasFloat}; scheduler=default_blockscheduler(t), kwargs...) From f11533f443f01b63f06b43fa14913644f8ead10b Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 2 Apr 2025 18:25:33 -0400 Subject: [PATCH 17/70] patch through leftorth --- src/tensors/factorizations.jl | 82 ++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/src/tensors/factorizations.jl b/src/tensors/factorizations.jl index c1da8f5e3..0afbc595c 100644 --- a/src/tensors/factorizations.jl +++ b/src/tensors/factorizations.jl @@ -311,6 +311,13 @@ end #------------------------------------------------------------------------------------------ const RealOrComplexFloat = Union{AbstractFloat,Complex{<:AbstractFloat}} +function _reverse!(t::AbstractTensorMap; dims=:) + for (c, b) in blocks(t) + reverse!(b; dims) + end + return t +end + function leftorth!(t::TensorMap{<:RealOrComplexFloat}; alg::Union{QR,QRpos,QL,QLpos,SVD,SDD,Polar}=QRpos(), atol::Real=zero(float(real(scalartype(t)))), @@ -318,47 +325,44 @@ function leftorth!(t::TensorMap{<:RealOrComplexFloat}; eps(real(float(one(scalartype(t))))) * iszero(atol)) InnerProductStyle(t) === EuclideanInnerProduct() || throw_invalid_innerproduct(:leftorth!) - if !iszero(rtol) - atol = max(atol, rtol * norm(t)) - end - I = sectortype(t) - dims = SectorDict{I,Int}() - # compute QR factorization for each block - if !isempty(blocks(t)) - generator = Base.Iterators.map(blocks(t)) do (c, b) - Qc, Rc = MatrixAlgebra.leftorth!(b, alg, atol) - dims[c] = size(Qc, 2) - return c => (Qc, Rc) + if alg isa QR + return left_orth!(t; kind=:qr, atol, rtol) + elseif alg isa QRpos + return left_orth!(t; kind=:qrpos, atol, rtol) + elseif alg isa SDD + return left_orth!(t; kind=:svd, atol, rtol) + elseif alg isa Polar + return left_orth!(t; kind=:polar, atol, rtol) + elseif alg isa SVD + kind = :svd + if iszero(atol) && iszero(rtol) + alg′ = LAPACK_QRIteration() + return left_orth!(t; kind, alg=BlockAlgorithm(alg′, default_blockscheduler(t)), + atol, rtol) + else + trunc = MatrixAlgebraKit.TruncationKeepAbove(atol, rtol) + svd_alg = LAPACK_QRIteration() + scheduler = default_blockscheduler(t) + alg′ = MatrixAlgebraKit.TruncatedAlgorithm(BlockAlgorithm(svd_alg, scheduler), + trunc) + return left_orth!(t; kind, alg=alg′, atol, rtol) end - QRdata = SectorDict(generator) + elseif alg isa QL + _reverse!(t; dims=2) + Q, R = left_orth!(t; kind=:qr, atol, rtol) + _reverse!(Q; dims=2) + _reverse!(R) + return Q, R + elseif alg isa QLpos + _reverse!(t; dims=2) + Q, R = left_orth!(t; kind=:qrpos, atol, rtol) + _reverse!(Q; dims=2) + _reverse!(R) + return Q, R end - # construct new space - S = spacetype(t) - V = S(dims) - if alg isa Polar - @assert V ≅ domain(t) - W = domain(t) - elseif length(domain(t)) == 1 && domain(t) ≅ V - W = domain(t) - elseif length(codomain(t)) == 1 && codomain(t) ≅ V - W = codomain(t) - else - W = ProductSpace(V) - end - - # construct output tensors - T = float(scalartype(t)) - Q = similar(t, T, codomain(t) ← W) - R = similar(t, T, W ← domain(t)) - if !isempty(blocks(t)) - for (c, (Qc, Rc)) in QRdata - copy!(block(Q, c), Qc) - copy!(block(R, c), Rc) - end - end - return Q, R + throw(ArgumentError("Algorithm $alg not implemented for leftorth!")) end function leftnull!(t::TensorMap{<:RealOrComplexFloat}; @@ -685,8 +689,8 @@ function LinearAlgebra.ishermitian(t::TensorMap) end function LinearAlgebra.isposdef!(t::TensorMap) - domain(t) == codomain(t) || - throw(SpaceMismatch("`isposdef` requires domain and codomain to be the same")) + domain(t) ≅ codomain(t) || + throw(SpaceMismatch("`isposdef` requires domain and codomain to be isomorphic")) InnerProductStyle(spacetype(t)) === EuclideanInnerProduct() || return false for (c, b) in blocks(t) isposdef!(b) || return false From bc0abab5c5369fa5365411eb008275f333d3ab90 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 8 May 2025 16:31:02 -0400 Subject: [PATCH 18/70] Add `isisometry` --- src/TensorKit.jl | 2 +- src/auxiliary/linalg.jl | 2 ++ src/tensors/factorizations.jl | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/TensorKit.jl b/src/TensorKit.jl index b55b7857d..bad0a4dab 100644 --- a/src/TensorKit.jl +++ b/src/TensorKit.jl @@ -73,7 +73,7 @@ export mul!, lmul!, rmul!, adjoint!, pinv, axpy!, axpby! export leftorth, rightorth, leftnull, rightnull, leftorth!, rightorth!, leftnull!, rightnull!, tsvd!, tsvd, eigen, eigen!, eig, eig!, eigh, eigh!, exp, exp!, - isposdef, isposdef!, ishermitian, sylvester, rank, cond + isposdef, isposdef!, ishermitian, isisometry, sylvester, rank, cond export braid, braid!, permute, permute!, transpose, transpose!, twist, twist!, repartition, repartition! export catdomain, catcodomain diff --git a/src/auxiliary/linalg.jl b/src/auxiliary/linalg.jl index 4277be67f..54f993d7e 100644 --- a/src/auxiliary/linalg.jl +++ b/src/auxiliary/linalg.jl @@ -84,6 +84,8 @@ end safesign(s::Real) = ifelse(s < zero(s), -one(s), +one(s)) safesign(s::Complex) = ifelse(iszero(s), one(s), s / abs(s)) +isisometry(A::StridedMatrix; kwargs...) = isapprox(A' * A, LinearAlgebra.I, kwargs...) + function leftorth!(A::StridedMatrix{<:BlasFloat}, alg::Union{QR,QRpos}, atol::Real) iszero(atol) || throw(ArgumentError("nonzero atol not supported by $alg")) m, n = size(A) diff --git a/src/tensors/factorizations.jl b/src/tensors/factorizations.jl index 0afbc595c..9360823b1 100644 --- a/src/tensors/factorizations.jl +++ b/src/tensors/factorizations.jl @@ -268,6 +268,11 @@ function LinearAlgebra.isposdef(t::AbstractTensorMap, (p₁, p₂)::Index2Tuple) return isposdef!(tcopy) end +function isisometry(t::AbstractTensorMap, (p₁, p₂)::Index2Tuple) + t = permute(t, (p₁, p₂); copy=false) + return isisometry(t) +end + function tsvd(t::AbstractTensorMap; kwargs...) tcopy = copy_oftype(t, float(scalartype(t))) return tsvd!(tcopy; kwargs...) @@ -697,3 +702,12 @@ function LinearAlgebra.isposdef!(t::TensorMap) end return true end + +# TODO: tolerances are per-block, not global or weighted - does that matter? +function isisometry(t::AbstractTensorMap; kwargs...) + domain(t) ≾ codomain(t) || return false + for (_, b) in blocks(t) + MatrixAlgebra.isisometry(b; kwargs...) || return false + end + return true +end From 75898d719292dfb83bf83854df323f754685adce Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 8 May 2025 16:31:10 -0400 Subject: [PATCH 19/70] Revert isposdef changes --- src/tensors/factorizations.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tensors/factorizations.jl b/src/tensors/factorizations.jl index 9360823b1..03835a0a4 100644 --- a/src/tensors/factorizations.jl +++ b/src/tensors/factorizations.jl @@ -694,8 +694,8 @@ function LinearAlgebra.ishermitian(t::TensorMap) end function LinearAlgebra.isposdef!(t::TensorMap) - domain(t) ≅ codomain(t) || - throw(SpaceMismatch("`isposdef` requires domain and codomain to be isomorphic")) + domain(t) == codomain(t) || + throw(SpaceMismatch("`isposdef` requires domain and codomain to be the same")) InnerProductStyle(spacetype(t)) === EuclideanInnerProduct() || return false for (c, b) in blocks(t) isposdef!(b) || return false From 6c1e12671caa1b058e1e70de341159f56fb3edfa Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 8 May 2025 16:31:23 -0400 Subject: [PATCH 20/70] preinitialize polar output --- src/tensors/factorizations.jl | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/tensors/factorizations.jl b/src/tensors/factorizations.jl index 03835a0a4..d0fc92bd1 100644 --- a/src/tensors/factorizations.jl +++ b/src/tensors/factorizations.jl @@ -331,19 +331,22 @@ function leftorth!(t::TensorMap{<:RealOrComplexFloat}; InnerProductStyle(t) === EuclideanInnerProduct() || throw_invalid_innerproduct(:leftorth!) + VC = MatrixAlgebraKit.initialize_output(left_orth!, t) + if alg isa QR - return left_orth!(t; kind=:qr, atol, rtol) + return left_orth!(t, VC; kind=:qr, atol, rtol) elseif alg isa QRpos - return left_orth!(t; kind=:qrpos, atol, rtol) + return left_orth!(t, VC; kind=:qrpos, atol, rtol) elseif alg isa SDD - return left_orth!(t; kind=:svd, atol, rtol) + return left_orth!(t, VC; kind=:svd, atol, rtol) elseif alg isa Polar - return left_orth!(t; kind=:polar, atol, rtol) + return left_orth!(t, VC; kind=:polar, atol, rtol) elseif alg isa SVD kind = :svd if iszero(atol) && iszero(rtol) alg′ = LAPACK_QRIteration() - return left_orth!(t; kind, alg=BlockAlgorithm(alg′, default_blockscheduler(t)), + return left_orth!(t, VC; kind, + alg=BlockAlgorithm(alg′, default_blockscheduler(t)), atol, rtol) else trunc = MatrixAlgebraKit.TruncationKeepAbove(atol, rtol) @@ -351,17 +354,17 @@ function leftorth!(t::TensorMap{<:RealOrComplexFloat}; scheduler = default_blockscheduler(t) alg′ = MatrixAlgebraKit.TruncatedAlgorithm(BlockAlgorithm(svd_alg, scheduler), trunc) - return left_orth!(t; kind, alg=alg′, atol, rtol) + return left_orth!(t, VC; kind, alg=alg′, atol, rtol) end elseif alg isa QL _reverse!(t; dims=2) - Q, R = left_orth!(t; kind=:qr, atol, rtol) + Q, R = left_orth!(t, VC; kind=:qr, atol, rtol) _reverse!(Q; dims=2) _reverse!(R) return Q, R elseif alg isa QLpos _reverse!(t; dims=2) - Q, R = left_orth!(t; kind=:qrpos, atol, rtol) + Q, R = left_orth!(t, VC; kind=:qrpos, atol, rtol) _reverse!(Q; dims=2) _reverse!(R) return Q, R From 394c9a340e32834ec3da1e9b4bb5bd30d8e4e8e6 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Fri, 9 May 2025 12:01:14 -0400 Subject: [PATCH 21/70] Bump to MatrixAlgebraKit v0.2 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index f78526b9d..92e90a4c9 100644 --- a/Project.toml +++ b/Project.toml @@ -34,7 +34,7 @@ Combinatorics = "1" FiniteDifferences = "0.12" LRUCache = "1.0.2" LinearAlgebra = "1" -MatrixAlgebraKit = "0.1.1" +MatrixAlgebraKit = "0.2" OhMyThreads = "0.8.0" PackageExtensionCompat = "1" Random = "1" From c2b09218928b6f8c95ee45cc44f44ceb1abf0feb Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Fri, 9 May 2025 12:03:41 -0400 Subject: [PATCH 22/70] Rework left_orth --- src/tensors/factorizations.jl | 85 +++++++++++++++++---------------- src/tensors/matrixalgebrakit.jl | 83 ++++++++++++++++++++------------ 2 files changed, 96 insertions(+), 72 deletions(-) diff --git a/src/tensors/factorizations.jl b/src/tensors/factorizations.jl index d0fc92bd1..f79e70b22 100644 --- a/src/tensors/factorizations.jl +++ b/src/tensors/factorizations.jl @@ -326,51 +326,54 @@ end function leftorth!(t::TensorMap{<:RealOrComplexFloat}; alg::Union{QR,QRpos,QL,QLpos,SVD,SDD,Polar}=QRpos(), atol::Real=zero(float(real(scalartype(t)))), - rtol::Real=(alg ∉ (SVD(), SDD())) ? zero(float(real(scalartype(t)))) : - eps(real(float(one(scalartype(t))))) * iszero(atol)) + rtol::Real=(alg ∉ (SVD(), SDD())) ? + zero(float(real(scalartype(t)))) : + eps(real(float(one(scalartype(t))))) * + iszero(atol)) InnerProductStyle(t) === EuclideanInnerProduct() || throw_invalid_innerproduct(:leftorth!) - - VC = MatrixAlgebraKit.initialize_output(left_orth!, t) - - if alg isa QR - return left_orth!(t, VC; kind=:qr, atol, rtol) - elseif alg isa QRpos - return left_orth!(t, VC; kind=:qrpos, atol, rtol) - elseif alg isa SDD - return left_orth!(t, VC; kind=:svd, atol, rtol) - elseif alg isa Polar - return left_orth!(t, VC; kind=:polar, atol, rtol) - elseif alg isa SVD - kind = :svd - if iszero(atol) && iszero(rtol) - alg′ = LAPACK_QRIteration() - return left_orth!(t, VC; kind, - alg=BlockAlgorithm(alg′, default_blockscheduler(t)), - atol, rtol) - else - trunc = MatrixAlgebraKit.TruncationKeepAbove(atol, rtol) - svd_alg = LAPACK_QRIteration() - scheduler = default_blockscheduler(t) - alg′ = MatrixAlgebraKit.TruncatedAlgorithm(BlockAlgorithm(svd_alg, scheduler), - trunc) - return left_orth!(t, VC; kind, alg=alg′, atol, rtol) - end - elseif alg isa QL - _reverse!(t; dims=2) - Q, R = left_orth!(t, VC; kind=:qr, atol, rtol) - _reverse!(Q; dims=2) - _reverse!(R) - return Q, R - elseif alg isa QLpos - _reverse!(t; dims=2) - Q, R = left_orth!(t, VC; kind=:qrpos, atol, rtol) - _reverse!(Q; dims=2) - _reverse!(R) - return Q, R + if alg == SVD() || alg == SDD() + return _leftorth!(t, alg; atol, rtol) + else + (iszero(atol) && iszero(rtol)) || + throw(ArgumentError("`leftorth!` with nonzero atol or rtol requires SVD or SDD algorithm")) + return _leftorth!(t, alg) end +end - throw(ArgumentError("Algorithm $alg not implemented for leftorth!")) +# this promotes the algorithm to a positional argument for type stability reasons +# since polar has different number of output legs +# TODO: this seems like duplication from MatrixAlgebraKit.left_orth!, but that function +# only has its logic with the output already specified, which breaks for polar +function _leftorth!(t::TensorMap{<:RealOrComplexFloat}, alg::Union{SVD,SDD}; atol::Real, + rtol::Real) + alg′ = alg == SVD() ? MatrixAlgebraKit.LAPACK_QRIteration() : + MatrixAlgebraKit.LAPACK_DivideAndConquer() + alg_svd = BlockAlgorithm(alg′, default_blockscheduler(t)) + if iszero(atol) && iszero(rtol) + U, S, Vᴴ = svd_compact!(t, alg_svd) + return U, lmul!(S, Vᴴ) + else + trunc = MatrixAlgebraKit.TruncationKeepAbove(atol, rtol) + alg_svd = MatrixAlgebraKit.select_algorithm(svd_trunc!, t; trunc, + alg=alg_svd) + + U, S, Vᴴ = svd_trunc!(t, alg_svd) + return U, lmul!(S, Vᴴ) + end +end +function _leftorth!(t::TensorMap{<:RealOrComplexFloat}, alg::Union{QR,QRpos}) + return qr_compact!(t; positive=alg == QRpos()) +end +function _leftorth!(t::TensorMap{<:RealOrComplexFloat}, alg::Union{QL,QLpos}) + _reverse!(t; dims=2) + Q, R = qr_compact!(t; positive=alg == QLpos()) + _reverse!(Q; dims=2) + _reverse!(R) + return Q, R +end +function _leftorth!(t::TensorMap{<:RealOrComplexFloat}, ::Polar) + return MatrixAlgebraKit.left_polar!(t) end function leftnull!(t::TensorMap{<:RealOrComplexFloat}; diff --git a/src/tensors/matrixalgebrakit.jl b/src/tensors/matrixalgebrakit.jl index a1f8682c3..a6952a055 100644 --- a/src/tensors/matrixalgebrakit.jl +++ b/src/tensors/matrixalgebrakit.jl @@ -21,6 +21,18 @@ macro check_eltype(x, y, f=:identity, g=:eltype) return esc(:($g($x) == $f($g($y)) || throw(ArgumentError($msg)))) end +function MatrixAlgebraKit._select_algorithm(_, ::AbstractTensorMap, + alg::MatrixAlgebraKit.AbstractAlgorithm) + return alg +end +function MatrixAlgebraKit._select_algorithm(f, t::AbstractTensorMap, alg::NamedTuple) + return MatrixAlgebraKit.select_algorithm(f, t; alg...) +end + +function _select_truncation(f, ::AbstractTensorMap, + trunc::MatrixAlgebraKit.TruncationStrategy) + return trunc +end # function factorisation_scalartype(::typeof(MAK.eig_full!), t::AbstractTensorMap) # T = scalartype(t) # return promote_type(Float32, typeof(zero(T) / sqrt(abs2(one(T))))) @@ -76,7 +88,7 @@ function MatrixAlgebraKit.initialize_output(::typeof(svd_full!), t::AbstractTens ::MatrixAlgebraKit.AbstractAlgorithm) V_cod = fuse(codomain(t)) V_dom = fuse(domain(t)) - U = similar(t, domain(t) ← V_cod) + U = similar(t, codomain(t) ← V_cod) S = similar(t, real(scalartype(t)), V_cod ← V_dom) Vᴴ = similar(t, V_dom ← domain(t)) return U, S, Vᴴ @@ -476,18 +488,19 @@ function MatrixAlgebraKit.check_input(::typeof(left_polar!), t, (W, P)) @check_eltype P t # space checks - space(W) == (codomain(t) ← fuse(domain(t))) || + space(W) == space(t) || throw(SpaceMismatch("`left_polar!(t, (W, P))` requires `space(W) == (codomain(t) ← domain(t))`")) - space(P) == (fuse(domain(t)) ← domain(t)) || + space(P) == (domain(t) ← domain(t)) || throw(SpaceMismatch("`left_polar!(t, (W, P))` requires `space(P) == (domain(t) ← domain(t))`")) return nothing end # TODO: do we really not want to fuse the spaces? -function MatrixAlgebraKit.initialize_output(::typeof(left_polar!), t::AbstractTensorMap) - W = similar(t, codomain(t) ← fuse(domain(t))) - P = similar(t, fuse(domain(t)) ← domain(t)) +function MatrixAlgebraKit.initialize_output(::typeof(left_polar!), t::AbstractTensorMap, + ::MatrixAlgebraKit.AbstractAlgorithm) + W = similar(t, space(t)) + P = similar(t, domain(t) ← domain(t)) return W, P end @@ -558,40 +571,48 @@ function MatrixAlgebraKit.initialize_output(::typeof(right_orth!), t::AbstractTe return C, Vᴴ end -function MatrixAlgebraKit.left_orth!(t::AbstractTensorMap, VC; kwargs...) - MatrixAlgebraKit.check_input(left_orth!, t, VC) - atol = get(kwargs, :atol, 0) - rtol = get(kwargs, :rtol, 0) - kind = get(kwargs, :kind, iszero(atol) && iszero(rtol) ? :qrpos : :svd) - - if !(iszero(atol) && iszero(rtol)) && kind != :svd - throw(ArgumentError("nonzero tolerance not supported for left_orth with kind=$kind")) +function MatrixAlgebraKit.left_orth!(t::AbstractTensorMap, VC; + trunc=nothing, + kind=isnothing(trunc) ? + :qr : :svd, + alg_qr=(; positive=true), + alg_polar=(;), + alg_svd=(;)) + if !isnothing(trunc) && kind != :svd + throw(ArgumentError("truncation not supported for left_orth with kind=$kind")) end if kind == :qr - alg = get(kwargs, :alg, MatrixAlgebraKit.select_algorithm(qr_compact!, t)) - return qr_compact!(t, VC, alg) - elseif kind == :qrpos - alg = get(kwargs, :alg, - MatrixAlgebraKit.select_algorithm(qr_compact!, t; positive=true)) - return qr_compact!(t, VC, alg) - elseif kind == :polar - alg = get(kwargs, :alg, MatrixAlgebraKit.select_algorithm(left_polar!, t)) - return left_polar!(t, VC, alg) - elseif kind == :svd && iszero(atol) && iszero(rtol) - alg = get(kwargs, :alg, MatrixAlgebraKit.select_algorithm(svd_compact!, t)) + alg_qr′ = MatrixAlgebraKit._select_algorithm(qr_compact!, t, alg_qr) + return qr_compact!(t, VC, alg_qr′) + end + + if kind == :polar + alg_polar′ = MatrixAlgebraKit._select_algorithm(left_polar!, t, alg_polar) + return left_polar!(t, VC, alg_polar′) + end + + if kind == :svd && isnothing(trunc) + alg_svd′ = MatrixAlgebraKit._select_algorithm(svd_compact!, t, alg_svd) V, C = VC S = DiagonalTensorMap{real(scalartype(t))}(undef, domain(V) ← codomain(C)) - U, S, Vᴴ = svd_compact!(t, (V, S, C), alg) + U, S, Vᴴ = svd_compact!(t, (V, S, C), alg_svd′) return U, lmul!(S, Vᴴ) - elseif kind == :svd - alg_svd = MatrixAlgebraKit.select_algorithm(svd_compact!, t) - trunc = MatrixAlgebraKit.TruncationKeepAbove(atol, rtol) - alg = get(kwargs, :alg, MatrixAlgebraKit.TruncatedAlgorithm(alg_svd, trunc)) + end + + if kind == :svd + alg_svd′ = MatrixAlgebraKit._select_algorithm(svd_compact!, t, alg_svd) + alg_svd_trunc = MatrixAlgebraKit.select_algorithm(svd_trunc!, t; trunc, + alg=alg_svd′) V, C = VC S = DiagonalTensorMap{real(scalartype(t))}(undef, domain(V) ← codomain(C)) - U, S, Vᴴ = svd_trunc!(t, (V, S, C), alg) + U, S, Vᴴ = svd_trunc!(t, (V, S, C), alg_svd_trunc) return U, lmul!(S, Vᴴ) + end + + throw(ArgumentError("`left_orth!` received unknown value `kind = $kind`")) +end + else throw(ArgumentError("`left_orth!` received unknown value `kind = $kind`")) end From 940d044803fd61cfc18f0e5ddb4030d300cee919 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Fri, 9 May 2025 12:04:17 -0400 Subject: [PATCH 23/70] Rework left_null --- src/tensors/factorizations.jl | 40 ++++------ src/tensors/matrixalgebrakit.jl | 130 +++++++++++++++++++++++++++++++- 2 files changed, 144 insertions(+), 26 deletions(-) diff --git a/src/tensors/factorizations.jl b/src/tensors/factorizations.jl index f79e70b22..bb991c3c2 100644 --- a/src/tensors/factorizations.jl +++ b/src/tensors/factorizations.jl @@ -383,36 +383,26 @@ function leftnull!(t::TensorMap{<:RealOrComplexFloat}; eps(real(float(one(scalartype(t))))) * iszero(atol)) InnerProductStyle(t) === EuclideanInnerProduct() || throw_invalid_innerproduct(:leftnull!) - if !iszero(rtol) - atol = max(atol, rtol * norm(t)) - end - I = sectortype(t) - dims = SectorDict{I,Int}() - # compute QR factorization for each block - V = codomain(t) - if !isempty(blocksectors(V)) - generator = Base.Iterators.map(blocksectors(V)) do c - Nc = MatrixAlgebra.leftnull!(block(t, c), alg, atol) - dims[c] = size(Nc, 2) - return c => Nc + if alg == SVD() || alg == SDD() + kind = :svd + alg_svd = BlockAlgorithm(alg == SVD() ? MatrixAlgebraKit.LAPACK_QRIteration() : + MatrixAlgebraKit.LAPACK_DivideAndConquer(), + default_blockscheduler(t)) + trunc = if iszero(atol) && iszero(rtol) + nothing + else + (; atol, rtol) end - Ndata = SectorDict(generator) + return left_null!(t; kind, alg_svd, trunc) end - # construct new space - S = spacetype(t) - W = S(dims) + (iszero(atol) && iszero(rtol)) || + throw(ArgumentError("`leftnull!` with nonzero atol or rtol requires SVD or SDD algorithm")) - # construct output tensor - T = float(scalartype(t)) - N = similar(t, T, V ← W) - if !isempty(blocksectors(V)) - for (c, Nc) in Ndata - copy!(block(N, c), Nc) - end - end - return N + kind = :qr + alg_qr = (; positive=alg == QRpos()) + return left_null!(t; kind, alg_qr) end function rightorth!(t::TensorMap{<:RealOrComplexFloat}; diff --git a/src/tensors/matrixalgebrakit.jl b/src/tensors/matrixalgebrakit.jl index a6952a055..f836e51be 100644 --- a/src/tensors/matrixalgebrakit.jl +++ b/src/tensors/matrixalgebrakit.jl @@ -33,6 +33,14 @@ function _select_truncation(f, ::AbstractTensorMap, trunc::MatrixAlgebraKit.TruncationStrategy) return trunc end +function _select_truncation(::typeof(left_null!), ::AbstractTensorMap, trunc::NamedTuple) + return MatrixAlgebraKit.null_truncation_strategy(; trunc...) +end + +function MatrixAlgebraKit.diagview(t::AbstractTensorMap) + return SectorDict(c => MatrixAlgebraKit.diagview(b) for (c, b) in blocks(t)) +end + # function factorisation_scalartype(::typeof(MAK.eig_full!), t::AbstractTensorMap) # T = scalartype(t) # return promote_type(Float32, typeof(zero(T) / sqrt(abs2(one(T))))) @@ -103,6 +111,11 @@ function MatrixAlgebraKit.initialize_output(::typeof(svd_compact!), t::AbstractT return U, S, Vᴴ end +function MatrixAlgebraKit.initialize_output(::typeof(svd_trunc!), t::AbstractTensorMap, + alg::MatrixAlgebraKit.AbstractAlgorithm) + return MatrixAlgebraKit.initialize_output(svd_compact!, t, alg) +end + # TODO: svd_vals function MatrixAlgebraKit.svd_full!(t::AbstractTensorMap, (U, S, Vᴴ), @@ -613,8 +626,69 @@ function MatrixAlgebraKit.left_orth!(t::AbstractTensorMap, VC; throw(ArgumentError("`left_orth!` received unknown value `kind = $kind`")) end +# Nullspace +# --------- +function MatrixAlgebraKit.check_input(::typeof(left_null!), t::AbstractTensorMap, N) + # scalartype checks + @check_eltype N t + + # space checks + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + V_N = setdiff(fuse(codomain(t)), V_Q) + space(N) == (codomain(t) ← V_N) || + throw(SpaceMismatch("`left_null!(t, N)` requires `space(N) == (codomain(t) ← setdiff(fuse(codomain(t)), infimum(fuse(codomain(t)), fuse(domain(t))))`")) + + return nothing +end + +function MatrixAlgebraKit.initialize_output(::typeof(left_null!), t::AbstractTensorMap) + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + V_N = setdiff(fuse(codomain(t)), V_Q) + N = similar(t, codomain(t) ← V_N) + return N +end + +# TODO: the following functions shouldn't be necessary if the AbstractArray restrictions are +# removed +function MatrixAlgebraKit.left_null(t::AbstractTensorMap; kwargs...) + return left_null!(MatrixAlgebraKit.copy_input(left_null, t); kwargs...) +end +function MatrixAlgebraKit.left_null!(t::AbstractTensorMap; kwargs...) + N = MatrixAlgebraKit.initialize_output(left_null!, t) + return left_null!(t, N; kwargs...) +end + +function MatrixAlgebraKit.left_null!(t::AbstractTensorMap, N; + trunc=nothing, + kind=isnothing(trunc) ? :qr : :svd, + alg_qr=(; positive=true), + alg_svd=(;)) + MatrixAlgebraKit.check_input(left_null!, t, N) + + if !isnothing(trunc) && kind != :svd + throw(ArgumentError("truncation not supported for left_null with kind=$kind")) + end + + if kind == :qr + alg_qr′ = MatrixAlgebraKit._select_algorithm(qr_null!, t, alg_qr) + return qr_null!(t, N, alg_qr′) + elseif kind == :svd && isnothing(trunc) + alg_svd′ = MatrixAlgebraKit._select_algorithm(svd_full!, t, alg_svd) + # TODO: refactor into separate function + U, _, _ = svd_full!(t, alg_svd′) + for (c, b) in blocks(N) + bU = block(U, c) + m, n = size(bU) + copy!(b, @view(bU[1:m, (n + 1):m])) + end + return N + elseif kind == :svd + alg_svd′ = MatrixAlgebraKit._select_algorithm(svd_full!, t, alg_svd) + U, S, _ = svd_full!(t, alg_svd′) + trunc′ = _select_truncation(left_null!, t, trunc) + return MatrixAlgebraKit.truncate!(left_null!, (U, S), trunc′) else - throw(ArgumentError("`left_orth!` received unknown value `kind = $kind`")) + throw(ArgumentError("`left_null!` received unknown value `kind = $kind`")) end end @@ -643,3 +717,57 @@ function MatrixAlgebraKit.truncate!(::typeof(svd_trunc!), (U, S, Vᴴ), return Ũ, S̃, Ṽᴴ end + +function MatrixAlgebraKit.truncate!(::typeof(left_null!), + (U, S)::Tuple{<:AbstractTensorMap, + <:AbstractTensorMap}, + strategy::MatrixAlgebraKit.TruncationStrategy) + extended_S = SectorDict(c => vcat(MatrixAlgebraKit.diagview(b), + zeros(eltype(b), max(0, size(b, 2) - size(b, 1)))) + for (c, b) in blocks(S)) + ind = MatrixAlgebraKit.findtruncated(extended_S, strategy) + V_truncated = spacetype(S)(c => length(axes(b, 1)[ind[c]]) for (c, b) in blocks(S)) + Ũ = similar(U, codomain(U) ← V_truncated) + for (c, b) in blocks(Ũ) + copy!(b, @view(block(U, c)[:, ind[c]])) + end + return Ũ +end + +const BlockWiseTruncations = Union{MatrixAlgebraKit.TruncationKeepAbove, + MatrixAlgebraKit.TruncationKeepBelow, + MatrixAlgebraKit.TruncationKeepFiltered} + +# TODO: relative tolerances should be global +function MatrixAlgebraKit.findtruncated(values::SectorDict, strategy::BlockWiseTruncations) + return SectorDict(c => MatrixAlgebraKit.findtruncated(v, strategy) for (c, v) in values) +end +function MatrixAlgebraKit.findtruncated(vals::SectorDict, + strategy::MatrixAlgebraKit.TruncationKeepSorted) + allpairs = mapreduce(vcat, vals) do (c, v) + return map(Base.Fix1(=>, c), axes(v, 1)) + end + by((c, i)) = strategy.sortby(vals[c][i]) + sort!(allpairs; by, strategy.rev) + + howmany = zero(Base.promote_op(dim, valtype(values))) + i = 1 + while i ≤ length(allpairs) + howmany += dim(first(allpairs[i])) + + howmany == strategy.howmany && break + + if howmany > strategy.howmany + i -= 1 + break + end + + i += 1 + end + + ind = SectorDict(c => allpairs[findall(==(c) ∘ first, view(allpairs, 1:i))] + for c in keys(vals)) + filter!(!isempty ∘ last, ind) # TODO: this might not be necessary + + return ind +end From 694a0a73bca113eda8ad10d144008207e257e177 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Fri, 9 May 2025 12:04:27 -0400 Subject: [PATCH 24/70] include temporary tests --- test/factorizations.jl | 146 ++++++++++++++++++++++++++++++++++------- 1 file changed, 123 insertions(+), 23 deletions(-) diff --git a/test/factorizations.jl b/test/factorizations.jl index a30f005e3..ca9b510fb 100644 --- a/test/factorizations.jl +++ b/test/factorizations.jl @@ -1,5 +1,105 @@ +using TestEnv; +TestEnv.activate(); + +using Test +using TestExtras +using Random +using TensorKit +using Combinatorics +using TensorKit: ProductSector, fusiontensor, pentagon_equation, hexagon_equation +using TensorOperations +using Base.Iterators: take, product +# using SUNRepresentations: SUNIrrep +# const SU3Irrep = SUNIrrep{3} +using LinearAlgebra: LinearAlgebra +using Zygote: Zygote +using MatrixAlgebraKit + +const TK = TensorKit + +Random.seed!(1234) + +smallset(::Type{I}) where {I<:Sector} = take(values(I), 5) +function smallset(::Type{ProductSector{Tuple{I1,I2}}}) where {I1,I2} + iter = product(smallset(I1), smallset(I2)) + s = collect(i ⊠ j for (i, j) in iter if dim(i) * dim(j) <= 6) + return length(s) > 6 ? rand(s, 6) : s +end +function smallset(::Type{ProductSector{Tuple{I1,I2,I3}}}) where {I1,I2,I3} + iter = product(smallset(I1), smallset(I2), smallset(I3)) + s = collect(i ⊠ j ⊠ k for (i, j, k) in iter if dim(i) * dim(j) * dim(k) <= 6) + return length(s) > 6 ? rand(s, 6) : s +end +function randsector(::Type{I}) where {I<:Sector} + s = collect(smallset(I)) + a = rand(s) + while a == one(a) # don't use trivial label + a = rand(s) + end + return a +end +function hasfusiontensor(I::Type{<:Sector}) + try + fusiontensor(one(I), one(I), one(I)) + return true + catch e + if e isa MethodError + return false + else + rethrow(e) + end + end +end + +# spaces +Vtr = (ℂ^3, + (ℂ^4)', + ℂ^5, + ℂ^6, + (ℂ^7)') +Vℤ₂ = (ℂ[Z2Irrep](0 => 1, 1 => 1), + ℂ[Z2Irrep](0 => 1, 1 => 2)', + ℂ[Z2Irrep](0 => 3, 1 => 2)', + ℂ[Z2Irrep](0 => 2, 1 => 3), + ℂ[Z2Irrep](0 => 2, 1 => 5)) +Vfℤ₂ = (ℂ[FermionParity](0 => 1, 1 => 1), + ℂ[FermionParity](0 => 1, 1 => 2)', + ℂ[FermionParity](0 => 3, 1 => 2)', + ℂ[FermionParity](0 => 2, 1 => 3), + ℂ[FermionParity](0 => 2, 1 => 5)) +Vℤ₃ = (ℂ[Z3Irrep](0 => 1, 1 => 2, 2 => 2), + ℂ[Z3Irrep](0 => 3, 1 => 1, 2 => 1), + ℂ[Z3Irrep](0 => 2, 1 => 2, 2 => 1)', + ℂ[Z3Irrep](0 => 1, 1 => 2, 2 => 3), + ℂ[Z3Irrep](0 => 1, 1 => 3, 2 => 3)') +VU₁ = (ℂ[U1Irrep](0 => 1, 1 => 2, -1 => 2), + ℂ[U1Irrep](0 => 3, 1 => 1, -1 => 1), + ℂ[U1Irrep](0 => 2, 1 => 2, -1 => 1)', + ℂ[U1Irrep](0 => 1, 1 => 2, -1 => 3), + ℂ[U1Irrep](0 => 1, 1 => 3, -1 => 3)') +VfU₁ = (ℂ[FermionNumber](0 => 1, 1 => 2, -1 => 2), + ℂ[FermionNumber](0 => 3, 1 => 1, -1 => 1), + ℂ[FermionNumber](0 => 2, 1 => 2, -1 => 1)', + ℂ[FermionNumber](0 => 1, 1 => 2, -1 => 3), + ℂ[FermionNumber](0 => 1, 1 => 3, -1 => 3)') +VCU₁ = (ℂ[CU1Irrep]((0, 0) => 1, (0, 1) => 2, 1 => 1), + ℂ[CU1Irrep]((0, 0) => 3, (0, 1) => 0, 1 => 1), + ℂ[CU1Irrep]((0, 0) => 1, (0, 1) => 0, 1 => 2)', + ℂ[CU1Irrep]((0, 0) => 2, (0, 1) => 2, 1 => 1), + ℂ[CU1Irrep]((0, 0) => 2, (0, 1) => 1, 1 => 2)') +VSU₂ = (ℂ[SU2Irrep](0 => 3, 1 // 2 => 1), + ℂ[SU2Irrep](0 => 2, 1 => 1), + ℂ[SU2Irrep](1 // 2 => 1, 1 => 1)', + ℂ[SU2Irrep](0 => 2, 1 // 2 => 2), + ℂ[SU2Irrep](0 => 1, 1 // 2 => 1, 3 // 2 => 1)') +VfSU₂ = (ℂ[FermionSpin](0 => 3, 1 // 2 => 1), + ℂ[FermionSpin](0 => 2, 1 => 1), + ℂ[FermionSpin](1 // 2 => 1, 1 => 1)', + ℂ[FermionSpin](0 => 2, 1 // 2 => 2), + ℂ[FermionSpin](0 => 1, 1 // 2 => 1, 3 // 2 => 1)') for V in (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂)#, VSU₃) V1, V2, V3, V4, V5 = V + @assert V3 * V4 * V2 ≿ V1' * V5' # necessary for leftorth tests @assert V3 * V4 ≾ V1' * V2' * V5' # necessary for rightorth tests end @@ -21,33 +121,33 @@ catch (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂)#, VSU₃) end -@timedtestset "Factorizatios with symmetry: $(sectortype(first(V)))" for V in spacelist + +function test_leftorth(t, p, alg) + Q, R = @inferred leftorth(t, p; alg) + @test Q * R ≈ permute(t, p) + @test isisometry(Q) + if alg isa Polar + @test isposdef(R) + @test domain(R) == codomain(R) == domain(permute(space(t), p)) + end +end +function test_leftnull(t, p, alg) + N = @inferred leftnull(t, p; alg) + @test isisometry(N) + @test norm(N' * permute(t, p)) ≈ 0 atol= 100 * eps(norm(t)) +end + +# @timedtestset "Factorizations with symmetry: $(sectortype(first(V)))" for V in spacelist + V = collect(spacelist)[2] V1, V2, V3, V4, V5 = V W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 for T in (Float32, ComplexF64), adj in (false, true) - t = adj ? rand(T, W)' : rand(T, W) - @testset "leftorth with $alg" for alg in - (TensorKit.QR(), TensorKit.QRpos(), - TensorKit.QL(), TensorKit.QLpos(), - TensorKit.Polar(), TensorKit.SVD(), - TensorKit.SDD()) - Q, R = @constinferred leftorth(t, ((3, 4, 2), (1, 5)); alg=alg) - QdQ = Q' * Q - @test QdQ ≈ one(QdQ) - @test Q * R ≈ permute(t, ((3, 4, 2), (1, 5))) - if alg isa Polar - @test isposdef(R) - @test domain(R) == codomain(R) == space(t, 1)' ⊗ space(t, 5)' - end + t = adj ? rand(T, W)' : rand(T, W); + @testset "leftorth with $alg" for alg in (TensorKit.QR(), TensorKit.QRpos(), TensorKit.QL(), TensorKit.QLpos(), TensorKit.Polar(), TensorKit.SVD(), TensorKit.SDD()) + test_leftorth(t, ((3, 4, 2), (1, 5)), alg) end - @testset "leftnull with $alg" for alg in - (TensorKit.QR(), TensorKit.SVD(), - TensorKit.SDD()) - N = @constinferred leftnull(t, ((3, 4, 2), (1, 5)); alg=alg) - NdN = N' * N - @test NdN ≈ one(NdN) - @test norm(N' * permute(t, ((3, 4, 2), (1, 5)))) < - 100 * eps(norm(t)) + @testset "leftnull with $alg" for alg in (TensorKit.QR(), TensorKit.SVD(), TensorKit.SDD()) + test_leftnull(t, ((3, 4, 2), (1, 5)), alg) end @testset "rightorth with $alg" for alg in (TensorKit.RQ(), TensorKit.RQpos(), From 7f2f6a2f71c04c8b1a4dd3c002f896e08889416c Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Tue, 27 May 2025 11:58:52 -0400 Subject: [PATCH 25/70] Change `Base.setdiff` for `ominus` --- docs/src/lib/spaces.md | 1 + src/spaces/cartesianspace.jl | 11 ++++++----- src/spaces/complexspace.jl | 13 +++++++------ src/spaces/gradedspace.jl | 12 ++++++------ src/spaces/vectorspaces.jl | 18 +++++++++++------- src/tensors/matrixalgebrakit.jl | 18 +++++++++--------- 6 files changed, 40 insertions(+), 33 deletions(-) diff --git a/docs/src/lib/spaces.md b/docs/src/lib/spaces.md index 83350156d..205301d83 100644 --- a/docs/src/lib/spaces.md +++ b/docs/src/lib/spaces.md @@ -90,6 +90,7 @@ dual conj flip ⊕ +⊖ zero(::ElementarySpace) oneunit supremum diff --git a/src/spaces/cartesianspace.jl b/src/spaces/cartesianspace.jl index fd38e0c1e..fe12f3dc6 100644 --- a/src/spaces/cartesianspace.jl +++ b/src/spaces/cartesianspace.jl @@ -49,16 +49,17 @@ sectortype(::Type{CartesianSpace}) = Trivial Base.oneunit(::Type{CartesianSpace}) = CartesianSpace(1) Base.zero(::Type{CartesianSpace}) = CartesianSpace(0) + ⊕(V₁::CartesianSpace, V₂::CartesianSpace) = CartesianSpace(V₁.d + V₂.d) +function ⊖(V::CartesianSpace, W::CartesianSpace) + V ≿ W || throw(ArgumentError("$(W) is not a subspace of $(V)")) + return CartesianSpace(dim(V) - dim(W)) +end + fuse(V₁::CartesianSpace, V₂::CartesianSpace) = CartesianSpace(V₁.d * V₂.d) flip(V::CartesianSpace) = V infimum(V₁::CartesianSpace, V₂::CartesianSpace) = CartesianSpace(min(V₁.d, V₂.d)) supremum(V₁::CartesianSpace, V₂::CartesianSpace) = CartesianSpace(max(V₁.d, V₂.d)) -function Base.setdiff(V::CartesianSpace, W::CartesianSpace) - V ≿ W || throw(ArgumentError("$(W) is not a subspace of $(V)")) - return CartesianSpace(dim(V) - dim(W)) -end - Base.show(io::IO, V::CartesianSpace) = print(io, "ℝ^$(V.d)") diff --git a/src/spaces/complexspace.jl b/src/spaces/complexspace.jl index 51a3056e9..1031db614 100644 --- a/src/spaces/complexspace.jl +++ b/src/spaces/complexspace.jl @@ -50,11 +50,18 @@ Base.conj(V::ComplexSpace) = ComplexSpace(dim(V), !isdual(V)) Base.oneunit(::Type{ComplexSpace}) = ComplexSpace(1) Base.zero(::Type{ComplexSpace}) = ComplexSpace(0) + function ⊕(V₁::ComplexSpace, V₂::ComplexSpace) return isdual(V₁) == isdual(V₂) ? ComplexSpace(dim(V₁) + dim(V₂), isdual(V₁)) : throw(SpaceMismatch("Direct sum of a vector space and its dual does not exist")) end +function ⊖(V::ComplexSpace, W::ComplexSpace) + (V ≿ W && isdual(V) == isdual(W)) || + throw(ArgumentError("$(W) is not a subspace of $(V)")) + return ComplexSpace(dim(V) - dim(W), isdual(V)) +end + fuse(V₁::ComplexSpace, V₂::ComplexSpace) = ComplexSpace(V₁.d * V₂.d) flip(V::ComplexSpace) = dual(V) @@ -69,10 +76,4 @@ function supremum(V₁::ComplexSpace, V₂::ComplexSpace) throw(SpaceMismatch("Supremum of space and dual space does not exist")) end -function Base.setdiff(V::ComplexSpace, W::ComplexSpace) - (V ≿ W && isdual(V) == isdual(W)) || - throw(ArgumentError("$(W) is not a subspace of $(V)")) - return ComplexSpace(dim(V) - dim(W), isdual(V)) -end - Base.show(io::IO, V::ComplexSpace) = print(io, isdual(V) ? "(ℂ^$(V.d))'" : "ℂ^$(V.d)") diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index ae7e770ae..976846e05 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -149,6 +149,12 @@ function ⊕(V₁::GradedSpace{I}, V₂::GradedSpace{I}) where {I<:Sector} return typeof(V₁)(dims; dual=dual1) end +function ⊖(V::GradedSpace{I}, W::GradedSpace{I}) where {I<:Sector} + V ≿ W && isdual(V) == isdual(W) || + throw(SpaceMismatch("$(W) is not a subspace of $(V)")) + return typeof(V)(c => dim(V, c) - dim(W, c) for c in sectors(V)) +end + function flip(V::GradedSpace{I}) where {I<:Sector} if isdual(V) typeof(V)(c => dim(V, c) for c in sectors(V)) @@ -187,12 +193,6 @@ function supremum(V₁::GradedSpace{I}, V₂::GradedSpace{I}) where {I<:Sector} end end -function Base.setdiff(V::GradedSpace{I}, W::GradedSpace{I}) where {I<:Sector} - V ≿ W && isdual(V) == isdual(W) || - throw(SpaceMismatch("$(W) is not a subspace of $(V)")) - return typeof(V)(c => dim(V, c) - dim(W, c) for c in sectors(V)) -end - function Base.show(io::IO, V::GradedSpace{I}) where {I<:Sector} print(io, type_repr(typeof(V)), "(") seperator = "" diff --git a/src/spaces/vectorspaces.jl b/src/spaces/vectorspaces.jl index 91f5b1052..c7d4cfa16 100644 --- a/src/spaces/vectorspaces.jl +++ b/src/spaces/vectorspaces.jl @@ -150,6 +150,17 @@ function ⊕ end ⊕(V::Vararg{VectorSpace}) = foldl(⊕, V) const oplus = ⊕ +""" + ⊖(V::ElementarySpace, W::ElementarySpace) -> X::ElementarySpace + ominus(V::ElementarySpace, W::ElementarySpace) -> X::ElementarySpace + +Return the set difference of two elementary spaces, i.e. an instance `X::ElementarySpace` +such that `V = W ⊕ X`. +""" +⊖(V₁::S, V₂::S) where {S<:ElementarySpace} +⊖(V₁::VectorSpace, V₂::VectorSpace) = ⊖(promote(V₁, V₂)...) +const ominus = ⊖ + """ ⊗(V₁::S, V₂::S, V₃::S...) where {S<:ElementarySpace} -> S @@ -406,10 +417,3 @@ function supremum(V₁::S, V₂::S, V₃::S...) where {S<:ElementarySpace} return supremum(supremum(V₁, V₂), V₃...) end -""" - setdiff(V::ElementarySpace, W::ElementarySpace) - -Return the set difference of two elementary spaces, i.e. an instance `X::ElementarySpace` -such that `V = W ⊕ X`. -""" -Base.setdiff(V₁::S, V₂::S) where {S<:ElementarySpace} diff --git a/src/tensors/matrixalgebrakit.jl b/src/tensors/matrixalgebrakit.jl index f836e51be..f775631c5 100644 --- a/src/tensors/matrixalgebrakit.jl +++ b/src/tensors/matrixalgebrakit.jl @@ -296,9 +296,9 @@ function MatrixAlgebraKit.check_input(::typeof(qr_null!), t::AbstractTensorMap, # space checks V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) - V_N = setdiff(fuse(codomain(t)), V_Q) + V_N = ⊖(fuse(codomain(t)), V_Q) space(N) == (codomain(t) ← V_N) || - throw(SpaceMismatch("`qr_null!(t, N)` requires `space(N) == (codomain(t) ← setdiff(fuse(codomain(t)), infimum(fuse(codomain(t)), fuse(domain(t))))`")) + throw(SpaceMismatch("`qr_null!(t, N)` requires `space(N) == (codomain(t) ← ⊖(fuse(codomain(t)), infimum(fuse(codomain(t)), fuse(domain(t))))`")) return nothing end @@ -322,7 +322,7 @@ end function MatrixAlgebraKit.initialize_output(::typeof(qr_null!), t::AbstractTensorMap, ::MatrixAlgebraKit.AbstractAlgorithm) V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) - V_N = setdiff(fuse(codomain(t)), V_Q) + V_N = ⊖(fuse(codomain(t)), V_Q) N = similar(t, codomain(t) ← V_N) return N end @@ -414,9 +414,9 @@ function MatrixAlgebraKit.check_input(::typeof(lq_null!), t::AbstractTensorMap, # space checks V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) - V_N = setdiff(fuse(domain(t)), V_Q) + V_N = ⊖(fuse(domain(t)), V_Q) space(N) == (V_N ← domain(t)) || - throw(SpaceMismatch("`lq_null!(t, N)` requires `space(N) == setdiff(fuse(domain(t)), infimum(fuse(codomain(t)), fuse(domain(t)))`")) + throw(SpaceMismatch("`lq_null!(t, N)` requires `space(N) == ⊖(fuse(domain(t)), infimum(fuse(codomain(t)), fuse(domain(t)))`")) return nothing end @@ -440,7 +440,7 @@ end function MatrixAlgebraKit.initialize_output(::typeof(lq_null!), t::AbstractTensorMap, ::MatrixAlgebraKit.AbstractAlgorithm) V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) - V_N = setdiff(fuse(domain(t)), V_Q) + V_N = ⊖(fuse(domain(t)), V_Q) N = similar(t, V_N ← domain(t)) return N end @@ -634,16 +634,16 @@ function MatrixAlgebraKit.check_input(::typeof(left_null!), t::AbstractTensorMap # space checks V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) - V_N = setdiff(fuse(codomain(t)), V_Q) + V_N = ⊖(fuse(codomain(t)), V_Q) space(N) == (codomain(t) ← V_N) || - throw(SpaceMismatch("`left_null!(t, N)` requires `space(N) == (codomain(t) ← setdiff(fuse(codomain(t)), infimum(fuse(codomain(t)), fuse(domain(t))))`")) + throw(SpaceMismatch("`left_null!(t, N)` requires `space(N) == (codomain(t) ← ⊖(fuse(codomain(t)), infimum(fuse(codomain(t)), fuse(domain(t))))`")) return nothing end function MatrixAlgebraKit.initialize_output(::typeof(left_null!), t::AbstractTensorMap) V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) - V_N = setdiff(fuse(codomain(t)), V_Q) + V_N = ⊖(fuse(codomain(t)), V_Q) N = similar(t, codomain(t) ← V_N) return N end From 374f65a5c46e97c081e3c3f8ad0d7b429ccb8e66 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Tue, 10 Jun 2025 21:44:23 -0400 Subject: [PATCH 26/70] change blockscheduler to type domain --- src/tensors/backends.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tensors/backends.jl b/src/tensors/backends.jl index 40cbd1ba5..1fc970e72 100644 --- a/src/tensors/backends.jl +++ b/src/tensors/backends.jl @@ -28,7 +28,8 @@ Run `f` in a scope where the `blockscheduler` is determined by `scheduler' and ` end # TODO: disable for trivial symmetry or small tensors? -default_blockscheduler(t::AbstractTensorMap) = blockscheduler[] +default_blockscheduler(t::AbstractTensorMap) = default_blockscheduler(typeof(t)) +default_blockscheduler(::Type{T}) where {T<:AbstractTensorMap} = blockscheduler[] # MatrixAlgebraKit # ---------------- From ec2df6c0d5d74ad5ce7c120679ab018ffd7e0593 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Tue, 10 Jun 2025 21:44:36 -0400 Subject: [PATCH 27/70] make block iterator loop over union of sectors --- src/tensors/blockiterator.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tensors/blockiterator.jl b/src/tensors/blockiterator.jl index fd7a1801a..cbfcd391f 100644 --- a/src/tensors/blockiterator.jl +++ b/src/tensors/blockiterator.jl @@ -33,8 +33,9 @@ end ``` """ function foreachblock(f, t::AbstractTensorMap, ts::AbstractTensorMap...; scheduler=nothing) - foreach(blocks(t)) do (c, b) - return f(c, (b, map(Base.Fix2(block, c), ts)...)) + allsectors = union(blocksectors(t), blocksectors.(ts)...) + foreach(allsectors) do c + return f(c, map(Base.Fix2(block, c), (t, ts...))) end return nothing end From f8561065d156f2811872eb821562276d1c9a80b7 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Tue, 10 Jun 2025 21:45:01 -0400 Subject: [PATCH 28/70] refactor `left_orth` for new matrixalgebrakit version --- src/tensors/factorizations.jl | 78 +++++++++++++++++------------------ 1 file changed, 38 insertions(+), 40 deletions(-) diff --git a/src/tensors/factorizations.jl b/src/tensors/factorizations.jl index bb991c3c2..fe4943ee7 100644 --- a/src/tensors/factorizations.jl +++ b/src/tensors/factorizations.jl @@ -324,57 +324,55 @@ function _reverse!(t::AbstractTensorMap; dims=:) end function leftorth!(t::TensorMap{<:RealOrComplexFloat}; - alg::Union{QR,QRpos,QL,QLpos,SVD,SDD,Polar}=QRpos(), - atol::Real=zero(float(real(scalartype(t)))), - rtol::Real=(alg ∉ (SVD(), SDD())) ? - zero(float(real(scalartype(t)))) : - eps(real(float(one(scalartype(t))))) * - iszero(atol)) + alg::Union{QR,QRpos,QL,QLpos,SVD,SDD,Polar,Nothing}=nothing, + kwargs...) + # atol::Real=zero(float(real(scalartype(t)))), + # rtol::Real=(alg ∉ (SVD(), SDD())) ? + # zero(float(real(scalartype(t)))) : + # eps(real(float(one(scalartype(t))))) * + # iszero(atol)) InnerProductStyle(t) === EuclideanInnerProduct() || throw_invalid_innerproduct(:leftorth!) - if alg == SVD() || alg == SDD() - return _leftorth!(t, alg; atol, rtol) - else - (iszero(atol) && iszero(rtol)) || - throw(ArgumentError("`leftorth!` with nonzero atol or rtol requires SVD or SDD algorithm")) - return _leftorth!(t, alg) - end + return _leftorth!(t, alg; kwargs...) + + # if alg == SVD() || alg == SDD() + # return _leftorth!(t, alg; atol, rtol) + # else + # (iszero(atol) && iszero(rtol)) || + # throw(ArgumentError("`leftorth!` with nonzero atol or rtol requires SVD or SDD algorithm")) + # return _leftorth!(t, alg) + # end end # this promotes the algorithm to a positional argument for type stability reasons # since polar has different number of output legs # TODO: this seems like duplication from MatrixAlgebraKit.left_orth!, but that function # only has its logic with the output already specified, which breaks for polar -function _leftorth!(t::TensorMap{<:RealOrComplexFloat}, alg::Union{SVD,SDD}; atol::Real, - rtol::Real) - alg′ = alg == SVD() ? MatrixAlgebraKit.LAPACK_QRIteration() : - MatrixAlgebraKit.LAPACK_DivideAndConquer() - alg_svd = BlockAlgorithm(alg′, default_blockscheduler(t)) - if iszero(atol) && iszero(rtol) - U, S, Vᴴ = svd_compact!(t, alg_svd) - return U, lmul!(S, Vᴴ) +function _leftorth!(t::TensorMap{<:RealOrComplexFloat}, alg; kwargs...) + trunc = isempty(kwargs) ? nothing : (; kwargs...) + if isnothing(alg) + return left_orth!(t; trunc) + elseif alg == SVD() + return left_orth!(t; kind=:svd, alg_svd=:LAPACK_QRIteration, trunc) + elseif alg == SDD() + return left_orth!(t; kind=:svd, alg_svd=:LAPACK_DivideAndConquer, trunc) + elseif alg == QR() + return left_orth!(t; kind=:qr, alg_qr=(; positive=false), trunc) + elseif alg == QRpos() + return left_orth!(t; kind=:qr, alg_qr=(; positive=true), trunc) + elseif alg == QL() || alg == QLpos() + _reverse!(t; dims=2) + Q, R = left_orth!(t; kind=:qr, alg_qr=(; positive=alg == QLpos()), trunc) + _reverse!(Q; dims=2) + _reverse!(R) + return Q, R + elseif alg == Polar() + return left_orth!(t; kind=:polar, trunc) else - trunc = MatrixAlgebraKit.TruncationKeepAbove(atol, rtol) - alg_svd = MatrixAlgebraKit.select_algorithm(svd_trunc!, t; trunc, - alg=alg_svd) - - U, S, Vᴴ = svd_trunc!(t, alg_svd) - return U, lmul!(S, Vᴴ) + throw(ArgumentError(lazy"Invalid algorithm: $alg")) end end -function _leftorth!(t::TensorMap{<:RealOrComplexFloat}, alg::Union{QR,QRpos}) - return qr_compact!(t; positive=alg == QRpos()) -end -function _leftorth!(t::TensorMap{<:RealOrComplexFloat}, alg::Union{QL,QLpos}) - _reverse!(t; dims=2) - Q, R = qr_compact!(t; positive=alg == QLpos()) - _reverse!(Q; dims=2) - _reverse!(R) - return Q, R -end -function _leftorth!(t::TensorMap{<:RealOrComplexFloat}, ::Polar) - return MatrixAlgebraKit.left_polar!(t) -end + function leftnull!(t::TensorMap{<:RealOrComplexFloat}; alg::Union{QR,QRpos,SVD,SDD}=QRpos(), From ed3c825837925b4589fb5e4322e7996323ca8238 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Tue, 10 Jun 2025 21:51:21 -0400 Subject: [PATCH 29/70] Bunch of simplifications for new matrixalgebrakit versions --- src/tensors/matrixalgebrakit.jl | 260 +++++++++++++------------------- 1 file changed, 101 insertions(+), 159 deletions(-) diff --git a/src/tensors/matrixalgebrakit.jl b/src/tensors/matrixalgebrakit.jl index f775631c5..4481267df 100644 --- a/src/tensors/matrixalgebrakit.jl +++ b/src/tensors/matrixalgebrakit.jl @@ -1,3 +1,11 @@ +# convenience to set default +macro check_space(x, V) + return esc(:($MatrixAlgebraKit.@check_size($x, $V, $space))) +end +macro check_scalar(x, y, op=:identity, eltype=:scalartype) + return esc(:($MatrixAlgebraKit.@check_scalar($x, $y, $op, $eltype))) +end + # Generic # ------- for f in (:eig_full, :eig_vals, :eig_trunc, :eigh_full, :eigh_vals, :eigh_trunc, :svd_full, @@ -7,6 +15,31 @@ for f in (:eig_full, :eig_vals, :eig_trunc, :eigh_full, :eigh_vals, :eigh_trunc, T = factorisation_scalartype($f, t) return copy_oftype(t, T) end + f! = Symbol(f, :!) + @eval function MatrixAlgebraKit.select_algorithm(::typeof($f!), t::AbstractTensorMap, + alg::Alg=nothing; + kwargs...) where {Alg} + return MatrixAlgebraKit.select_algorithm($f!, typeof(t), alg; kwargs...) + end + @eval function MatrixAlgebraKit.select_algorithm(::typeof($f!), ::Type{T}, + alg::Alg=nothing; + scheduler=default_blockscheduler(T), + kwargs...) where {T<:AbstractTensorMap, + Alg} + mat_alg = MatrixAlgebraKit.select_algorithm($f!, blocktype(T), alg; kwargs...) + return BlockAlgorithm(mat_alg, scheduler) + end +end + +for f in (:qr, :lq, :svd, :eig, :eigh, :polar) + default_f_algorithm = Symbol(:default_, f, :_algorithm) + @eval function MatrixAlgebraKit.$default_f_algorithm(::Type{T}; + scheduler=default_blockscheduler(T), + kwargs...) where {T<:AbstractTensorMap} + return BlockAlgorithm(MatrixAlgebraKit.$default_f_algorithm(blocktype(T); + kwargs...), + scheduler) + end end # TODO: move to MatrixAlgebraKit? @@ -21,14 +54,6 @@ macro check_eltype(x, y, f=:identity, g=:eltype) return esc(:($g($x) == $f($g($y)) || throw(ArgumentError($msg)))) end -function MatrixAlgebraKit._select_algorithm(_, ::AbstractTensorMap, - alg::MatrixAlgebraKit.AbstractAlgorithm) - return alg -end -function MatrixAlgebraKit._select_algorithm(f, t::AbstractTensorMap, alg::NamedTuple) - return MatrixAlgebraKit.select_algorithm(f, t; alg...) -end - function _select_truncation(f, ::AbstractTensorMap, trunc::MatrixAlgebraKit.TruncationStrategy) return trunc @@ -41,11 +66,6 @@ function MatrixAlgebraKit.diagview(t::AbstractTensorMap) return SectorDict(c => MatrixAlgebraKit.diagview(b) for (c, b) in blocks(t)) end -# function factorisation_scalartype(::typeof(MAK.eig_full!), t::AbstractTensorMap) -# T = scalartype(t) -# return promote_type(Float32, typeof(zero(T) / sqrt(abs2(one(T))))) -# end - # Singular value decomposition # ---------------------------- const _T_USVᴴ = Tuple{<:AbstractTensorMap,<:AbstractTensorMap,<:AbstractTensorMap} @@ -54,19 +74,16 @@ const _T_USVᴴ_diag = Tuple{<:AbstractTensorMap,<:DiagonalTensorMap,<:AbstractT function MatrixAlgebraKit.check_input(::typeof(svd_full!), t::AbstractTensorMap, (U, S, Vᴴ)::_T_USVᴴ) # scalartype checks - @check_eltype U t - @check_eltype S t real - @check_eltype Vᴴ t + @check_scalar U t + @check_scalar S t real + @check_scalar Vᴴ t # space checks V_cod = fuse(codomain(t)) V_dom = fuse(domain(t)) - space(U) == (codomain(t) ← V_cod) || - throw(SpaceMismatch("`svd_full!(t, (U, S, Vᴴ))` requires `space(U) == (codomain(t) ← fuse(domain(t)))`")) - space(S) == (V_cod ← V_dom) || - throw(SpaceMismatch("`svd_full!(t, (U, S, Vᴴ))` requires `space(S) == (fuse(codomain(t)) ← fuse(domain(t))`")) - space(Vᴴ) == (V_dom ← domain(t)) || - throw(SpaceMismatch("`svd_full!(t, (U, S, Vᴴ))` requires `space(Vᴴ) == (fuse(domain(t)) ← domain(t))`")) + @check_space(U, codomain(t) ← V_cod) + @check_space(S, V_cod ← V_dom) + @check_space(Vᴴ, V_dom ← domain(t)) return nothing end @@ -80,12 +97,9 @@ function MatrixAlgebraKit.check_input(::typeof(svd_compact!), t::AbstractTensorM # space checks V_cod = V_dom = infimum(fuse(codomain(t)), fuse(domain(t))) - space(U) == (codomain(t) ← V_cod) || - throw(SpaceMismatch("`svd_compact!(t, (U, S, Vᴴ))` requires `space(U) == (codomain(t) ← infimum(fuse(domain(t)), fuse(codomain(t)))`")) - space(S) == (V_cod ← V_dom) || - throw(SpaceMismatch("`svd_compact!(t, (U, S, Vᴴ))` requires diagonal `S` with `domain(S) == (infimum(fuse(codomain(t)), fuse(domain(t)))`")) - space(Vᴴ) == (V_dom ← domain(t)) || - throw(SpaceMismatch("`svd_compact!(t, (U, S, Vᴴ))` requires `space(Vᴴ) == (infimum(fuse(domain(t)), fuse(codomain(t))) ← domain(t))`")) + @check_space(U, codomain(t) ← V_cod) + @check_space(S, V_cod ← V_dom) + @check_space(Vᴴ, V_dom ← domain(t)) return nothing end @@ -124,7 +138,7 @@ function MatrixAlgebraKit.svd_full!(t::AbstractTensorMap, (U, S, Vᴴ), foreachblock(t, U, S, Vᴴ; alg.scheduler) do _, (b, u, s, vᴴ) if isempty(b) # TODO: remove once MatrixAlgebraKit supports empty matrices - one!(length(u) > 0 ? u : vᴴ) + MatrixAlgebraKit.one!(length(u) > 0 ? u : vᴴ) zerovector!(s) else u′, s′, vᴴ′ = MatrixAlgebraKit.svd_full!(b, (u, s, vᴴ), alg.alg) @@ -161,12 +175,6 @@ function MatrixAlgebraKit.svd_trunc!(t::AbstractTensorMap, USVᴴ, return MatrixAlgebraKit.truncate!(svd_trunc!, USVᴴ′, alg.trunc) end -function MatrixAlgebraKit.default_svd_algorithm(t::AbstractTensorMap{<:BlasFloat}; - scheduler=default_blockscheduler(t), - kwargs...) - return BlockAlgorithm(LAPACK_DivideAndConquer(; kwargs...), scheduler) -end - # Eigenvalue decomposition # ------------------------ const _T_DV = Tuple{<:DiagonalTensorMap,<:AbstractTensorMap} @@ -176,15 +184,13 @@ function MatrixAlgebraKit.check_input(::typeof(eigh_full!), t::AbstractTensorMap throw(ArgumentError("Eigenvalue decomposition requires square input tensor")) # scalartype checks - @check_eltype D t real - @check_eltype V t + @check_scalar D t real + @check_scalar V t # space checks V_D = fuse(domain(t)) - V_D == space(D, 1) || - throw(SpaceMismatch("`eigh_full!(t, (D, V))` requires diagonal `D` with `domain(D) == fuse(domain(t))`")) - space(V) == (codomain(t) ← V_D) || - throw(SpaceMismatch("`eigh_full!(t, (D, V))` requires `space(V) == (codomain(t) ← fuse(domain(t)))`")) + @check_space(D, V_D ← V_D) + @check_space(V, codomain(t) ← V_D) return nothing end @@ -195,15 +201,13 @@ function MatrixAlgebraKit.check_input(::typeof(eig_full!), t::AbstractTensorMap, throw(ArgumentError("Eigenvalue decomposition requires square input tensor")) # scalartype checks - @check_eltype D t complex - @check_eltype V t complex + @check_scalar D t complex + @check_scalar V t complex # space checks V_D = fuse(domain(t)) - V_D == space(D, 1) || - throw(SpaceMismatch("`eig_full!(t, (D, V))` requires diagonal `D` with `domain(D) == fuse(domain(t))`")) - space(V) == (codomain(t) ← V_D) || - throw(SpaceMismatch("`eig_full!(t, (D, V))` requires `space(V) == (codomain(t) ← fuse(domain(t)))`")) + @check_space(D, V_D ← V_D) + @check_space(V, codomain(t) ← V_D) return nothing end @@ -243,48 +247,32 @@ for f in (:eigh_full!, :eig_full!) end end -function MatrixAlgebraKit.default_eig_algorithm(t::AbstractTensorMap{<:BlasFloat}; - scheduler=default_blockscheduler(t), - kwargs...) - return BlockAlgorithm(LAPACK_Expert(; kwargs...), scheduler) -end -function MatrixAlgebraKit.default_eigh_algorithm(t::AbstractTensorMap{<:BlasFloat}; - scheduler=default_blockscheduler(t), - kwargs...) - return BlockAlgorithm(LAPACK_MultipleRelativelyRobustRepresentations(; kwargs...), - scheduler) -end - # QR decomposition # ---------------- function MatrixAlgebraKit.check_input(::typeof(qr_full!), t::AbstractTensorMap, (Q, R)::Tuple{<:AbstractTensorMap,<:AbstractTensorMap}) # scalartype checks - @check_eltype Q t - @check_eltype R t + @check_scalar Q t + @check_scalar R t # space checks V_Q = fuse(codomain(t)) - space(Q) == (codomain(t) ← V_Q) || - throw(SpaceMismatch("`qr_full!(t, (Q, R))` requires `space(Q) == (codomain(t) ← fuse(codomain(t)))`")) - space(R) == (V_Q ← domain(t)) || - throw(SpaceMismatch("`qr_full!(t, (Q, R))` requires `space(R) == (fuse(codomain(t)) ← domain(t)`")) + @check_space(Q, codomain(t) ← V_Q) + @check_space(R, V_Q ← domain(t)) return nothing end function MatrixAlgebraKit.check_input(::typeof(qr_compact!), t::AbstractTensorMap, (Q, R)) # scalartype checks - @check_eltype Q t - @check_eltype R t + @check_scalar Q t + @check_scalar R t # space checks V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) - space(Q) == (codomain(t) ← V_Q) || - throw(SpaceMismatch("`qr_compact!(t, (Q, R))` requires `space(Q) == (codomain(t) ← infimum(fuse(codomain(t)), fuse(domain(t)))`")) - space(R) == (V_Q ← domain(t)) || - throw(SpaceMismatch("`qr_compact!(t, (Q, R))` requires `space(R) == (infimum(fuse(codomain(t)), fuse(domain(t))) ← domain(t))`")) + @check_space(Q, codomain(t) ← V_Q) + @check_space(R, V_Q ← domain(t)) return nothing end @@ -292,13 +280,12 @@ end function MatrixAlgebraKit.check_input(::typeof(qr_null!), t::AbstractTensorMap, N::AbstractTensorMap) # scalartype checks - @check_eltype N t + @check_scalar N t # space checks V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) V_N = ⊖(fuse(codomain(t)), V_Q) - space(N) == (codomain(t) ← V_N) || - throw(SpaceMismatch("`qr_null!(t, N)` requires `space(N) == (codomain(t) ← ⊖(fuse(codomain(t)), infimum(fuse(codomain(t)), fuse(domain(t))))`")) + @check_space(N, codomain(t) ← V_N) return nothing end @@ -370,11 +357,6 @@ function MatrixAlgebraKit.qr_null!(t::AbstractTensorMap, N, alg::BlockAlgorithm) return N end -function MatrixAlgebraKit.default_qr_algorithm(t::AbstractTensorMap{<:BlasFloat}; - scheduler=default_blockscheduler(t), - kwargs...) - return BlockAlgorithm(LAPACK_HouseholderQR(; kwargs...), scheduler) -end # LQ decomposition # ---------------- @@ -395,28 +377,25 @@ end function MatrixAlgebraKit.check_input(::typeof(lq_compact!), t::AbstractTensorMap, (L, Q)) # scalartype checks - @check_eltype L t - @check_eltype Q t + @check_scalar L t + @check_scalar Q t # space checks V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) - space(L) == (codomain(t) ← V_Q) || - throw(SpaceMismatch("`lq_compact!(t, (L, Q))` requires `space(L) == infimum(fuse(codomain(t)), fuse(domain(t)))`")) - space(Q) == (V_Q ← domain(t)) || - throw(SpaceMismatch("`lq_compact!(t, (L, Q))` requires `space(Q) == infimum(fuse(codomain(t)), fuse(domain(t)))`")) + @check_space(L, codomain(t) ← V_Q) + @check_space(Q, V_Q ← domain(t)) return nothing end function MatrixAlgebraKit.check_input(::typeof(lq_null!), t::AbstractTensorMap, N) # scalartype checks - @check_eltype N t + @check_scalar N t # space checks V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) V_N = ⊖(fuse(domain(t)), V_Q) - space(N) == (V_N ← domain(t)) || - throw(SpaceMismatch("`lq_null!(t, N)` requires `space(N) == ⊖(fuse(domain(t)), infimum(fuse(codomain(t)), fuse(domain(t)))`")) + @check_space(N, V_N ← domain(t)) return nothing end @@ -497,21 +476,19 @@ function MatrixAlgebraKit.check_input(::typeof(left_polar!), t, (W, P)) throw(ArgumentError("Polar decomposition requires `codomain(t) ≿ domain(t)`")) # scalartype checks - @check_eltype W t - @check_eltype P t + @check_scalar W t + @check_scalar P t # space checks - space(W) == space(t) || - throw(SpaceMismatch("`left_polar!(t, (W, P))` requires `space(W) == (codomain(t) ← domain(t))`")) - space(P) == (domain(t) ← domain(t)) || - throw(SpaceMismatch("`left_polar!(t, (W, P))` requires `space(P) == (domain(t) ← domain(t))`")) + @check_space(W, space(t)) + @check_space(P, domain(t) ← domain(t)) return nothing end # TODO: do we really not want to fuse the spaces? function MatrixAlgebraKit.initialize_output(::typeof(left_polar!), t::AbstractTensorMap, - ::MatrixAlgebraKit.AbstractAlgorithm) + ::BlockAlgorithm) W = similar(t, space(t)) P = similar(t, domain(t) ← domain(t)) return W, P @@ -531,41 +508,46 @@ function MatrixAlgebraKit.left_polar!(t::AbstractTensorMap, WP, alg::BlockAlgori return WP end -function MatrixAlgebraKit.default_polar_algorithm(t::AbstractTensorMap{<:BlasFloat}; - scheduler=default_blockscheduler(t), - kwargs...) - return BlockAlgorithm(PolarViaSVD(LAPACK_DivideAndConquer(; kwargs...)), - scheduler) +# Trick to relax the checks of "square" if coming from left_orth +function MatrixAlgebraKit.left_orth_polar!(t::AbstractTensorMap, VC, alg) + alg′ = MatrixAlgebraKit.select_algorithm(left_polar!, t, alg) + return MatrixAlgebraKit.left_orth_polar!(t, VC, alg′) +end +function MatrixAlgebraKit.left_orth_polar!(t::AbstractTensorMap, WP, alg::BlockAlgorithm) + foreachblock(t, WP...; alg.scheduler) do _, (b, w, p) + w′, p′ = left_polar!(b, (w, p), alg.alg) + # deal with the case where the output is not the same as the input + w === w′ || copyto!(w, w′) + p === p′ || copyto!(p, p′) + return nothing + end + return WP end # Orthogonalization # ----------------- function MatrixAlgebraKit.check_input(::typeof(left_orth!), t::AbstractTensorMap, (V, C)) # scalartype checks - @check_eltype V t - isnothing(C) || @check_eltype C t + @check_scalar V t + isnothing(C) || @check_scalar C t # space checks V_C = infimum(fuse(codomain(t)), fuse(domain(t))) - space(V) == (codomain(t) ← V_C) || - throw(SpaceMismatch("`left_orth!(t, (V, C))` requires `space(V) == (codomain(t) ← infimum(fuse(codomain(t)), fuse(domain(t))))`")) - isnothing(C) || space(C) == (V_C ← domain(t)) || - throw(SpaceMismatch("`left_orth!(t, (V, C))` requires `space(C) == (infimum(fuse(codomain(t)), fuse(domain(t))) ← domain(t))`")) + @check_space(V, codomain(t) ← V_C) + isnothing(C) || @check_space(CV_C ← domain(t)) return nothing end function MatrixAlgebraKit.check_input(::typeof(right_orth!), t::AbstractTensorMap, (C, Vᴴ)) # scalartype checks - isnothing(C) || @check_eltype C t - @check_eltype Vᴴ t + isnothing(C) || @check_scalar C t + @check_scalar Vᴴ t # space checks V_C = infimum(fuse(codomain(t)), fuse(domain(t))) - isnothing(C) || space(C) == (codomain(t) ← V_C) || - throw(SpaceMismatch("`right_orth!(t, (C, Vᴴ))` requires `space(C) == (codomain(t) ← infimum(fuse(codomain(t)), fuse(domain(t)))`")) - space(Vᴴ) == (V_dom ← domain(t)) || - throw(SpaceMismatch("`right_orth!(t, (C, Vᴴ))` requires `space(Vᴴ) == (infimum(fuse(codomain(t)), fuse(domain(t))) ← domain(t))`")) + isnothing(C) || @check_space(C, codomain(t) ← V_C) + @check_space(Vᴴ, V_dom ← domain(t)) return nothing end @@ -584,59 +566,16 @@ function MatrixAlgebraKit.initialize_output(::typeof(right_orth!), t::AbstractTe return C, Vᴴ end -function MatrixAlgebraKit.left_orth!(t::AbstractTensorMap, VC; - trunc=nothing, - kind=isnothing(trunc) ? - :qr : :svd, - alg_qr=(; positive=true), - alg_polar=(;), - alg_svd=(;)) - if !isnothing(trunc) && kind != :svd - throw(ArgumentError("truncation not supported for left_orth with kind=$kind")) - end - - if kind == :qr - alg_qr′ = MatrixAlgebraKit._select_algorithm(qr_compact!, t, alg_qr) - return qr_compact!(t, VC, alg_qr′) - end - - if kind == :polar - alg_polar′ = MatrixAlgebraKit._select_algorithm(left_polar!, t, alg_polar) - return left_polar!(t, VC, alg_polar′) - end - - if kind == :svd && isnothing(trunc) - alg_svd′ = MatrixAlgebraKit._select_algorithm(svd_compact!, t, alg_svd) - V, C = VC - S = DiagonalTensorMap{real(scalartype(t))}(undef, domain(V) ← codomain(C)) - U, S, Vᴴ = svd_compact!(t, (V, S, C), alg_svd′) - return U, lmul!(S, Vᴴ) - end - - if kind == :svd - alg_svd′ = MatrixAlgebraKit._select_algorithm(svd_compact!, t, alg_svd) - alg_svd_trunc = MatrixAlgebraKit.select_algorithm(svd_trunc!, t; trunc, - alg=alg_svd′) - V, C = VC - S = DiagonalTensorMap{real(scalartype(t))}(undef, domain(V) ← codomain(C)) - U, S, Vᴴ = svd_trunc!(t, (V, S, C), alg_svd_trunc) - return U, lmul!(S, Vᴴ) - end - - throw(ArgumentError("`left_orth!` received unknown value `kind = $kind`")) -end - # Nullspace # --------- function MatrixAlgebraKit.check_input(::typeof(left_null!), t::AbstractTensorMap, N) # scalartype checks - @check_eltype N t + @check_scalar N t # space checks V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) V_N = ⊖(fuse(codomain(t)), V_Q) - space(N) == (codomain(t) ← V_N) || - throw(SpaceMismatch("`left_null!(t, N)` requires `space(N) == (codomain(t) ← ⊖(fuse(codomain(t)), infimum(fuse(codomain(t)), fuse(domain(t))))`")) + @check_space(N, codomain(t) ← V_N) return nothing end @@ -670,9 +609,11 @@ function MatrixAlgebraKit.left_null!(t::AbstractTensorMap, N; end if kind == :qr + @info "qr" alg_qr′ = MatrixAlgebraKit._select_algorithm(qr_null!, t, alg_qr) return qr_null!(t, N, alg_qr′) elseif kind == :svd && isnothing(trunc) + @info "svd" alg_svd′ = MatrixAlgebraKit._select_algorithm(svd_full!, t, alg_svd) # TODO: refactor into separate function U, _, _ = svd_full!(t, alg_svd′) @@ -683,9 +624,10 @@ function MatrixAlgebraKit.left_null!(t::AbstractTensorMap, N; end return N elseif kind == :svd + @info "svd2" alg_svd′ = MatrixAlgebraKit._select_algorithm(svd_full!, t, alg_svd) - U, S, _ = svd_full!(t, alg_svd′) - trunc′ = _select_truncation(left_null!, t, trunc) + @show U, S, _ = svd_full!(t, alg_svd′) + @show trunc′ = _select_truncation(left_null!, t, @show trunc) return MatrixAlgebraKit.truncate!(left_null!, (U, S), trunc′) else throw(ArgumentError("`left_null!` received unknown value `kind = $kind`")) From 6a18d858c7802ff0acf49cdc70acb4393c54615c Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 11 Jun 2025 21:02:54 -0400 Subject: [PATCH 30/70] Major overhaul --- .../factorizations.jl | 256 +++---- src/TensorKit.jl | 16 +- src/auxiliary/deprecate.jl | 46 +- src/spaces/vectorspaces.jl | 1 - src/tensors/backends.jl | 12 - src/tensors/diagonal.jl | 59 -- src/tensors/factorizations.jl | 707 ----------------- src/tensors/factorizations/deprecations.jl | 1 + src/tensors/factorizations/factorizations.jl | 215 ++++++ src/tensors/factorizations/implementations.jl | 167 ++++ src/tensors/factorizations/interface.jl | 242 ++++++ .../factorizations/matrixalgebrakit.jl | 508 +++++++++++++ src/tensors/factorizations/truncation.jl | 270 +++++++ src/tensors/factorizations/utility.jl | 29 + src/tensors/matrixalgebrakit.jl | 715 ------------------ src/tensors/truncation.jl | 163 ---- test/factorizations.jl | 554 +++++++------- test/runtests.jl | 8 +- test/tensors.jl | 49 +- 19 files changed, 1916 insertions(+), 2102 deletions(-) delete mode 100644 src/tensors/factorizations.jl create mode 100644 src/tensors/factorizations/deprecations.jl create mode 100644 src/tensors/factorizations/factorizations.jl create mode 100644 src/tensors/factorizations/implementations.jl create mode 100644 src/tensors/factorizations/interface.jl create mode 100644 src/tensors/factorizations/matrixalgebrakit.jl create mode 100644 src/tensors/factorizations/truncation.jl create mode 100644 src/tensors/factorizations/utility.jl delete mode 100644 src/tensors/matrixalgebrakit.jl delete mode 100644 src/tensors/truncation.jl diff --git a/ext/TensorKitChainRulesCoreExt/factorizations.jl b/ext/TensorKitChainRulesCoreExt/factorizations.jl index abfb724bc..a91c1dbc0 100644 --- a/ext/TensorKitChainRulesCoreExt/factorizations.jl +++ b/ext/TensorKitChainRulesCoreExt/factorizations.jl @@ -1,16 +1,17 @@ +using MatrixAlgebraKit: svd_compact_pullback! + # Factorizations rules # -------------------- function ChainRulesCore.rrule(::typeof(TensorKit.tsvd!), t::AbstractTensorMap; - trunc::TensorKit.TruncationScheme=TensorKit.NoTruncation(), - p::Real=2, + trunc::TensorKit.TruncationScheme=TensorKit.notrunc(), alg::Union{TensorKit.SVD,TensorKit.SDD}=TensorKit.SDD()) - U, Σ, V⁺, truncerr = tsvd(t; trunc=TensorKit.NoTruncation(), p=p, alg=alg) + U, Σ, V⁺, truncerr = tsvd(t; trunc=TensorKit.notrunc(), alg) - if !(trunc isa TensorKit.NoTruncation) && !isempty(blocksectors(t)) + if !(trunc == TensorKit.notrunc()) && !isempty(blocksectors(t)) Σdata = TensorKit.SectorDict(c => diag(b) for (c, b) in blocks(Σ)) - truncdim = TensorKit._compute_truncdim(Σdata, trunc, p) - truncerr = TensorKit._compute_truncerr(Σdata, truncdim, p) + truncdim = TensorKit._compute_truncdim(Σdata, trunc; p=2) + truncerr = TensorKit._compute_truncerr(Σdata, truncdim; p=2) SVDdata = TensorKit.SectorDict(c => (block(U, c), Σc, block(V⁺, c)) for (c, Σc) in Σdata) @@ -23,12 +24,11 @@ function ChainRulesCore.rrule(::typeof(TensorKit.tsvd!), t::AbstractTensorMap; function tsvd!_pullback(ΔUSVϵ) ΔU, ΔΣ, ΔV⁺, = unthunk.(ΔUSVϵ) Δt = similar(t) - for (c, b) in blocks(Δt) - Uc, Σc, V⁺c = block(U, c), block(Σ, c), block(V⁺, c) - ΔUc, ΔΣc, ΔV⁺c = block(ΔU, c), block(ΔΣ, c), block(ΔV⁺, c) - Σdc = view(Σc, diagind(Σc)) - ΔΣdc = (ΔΣc isa AbstractZero) ? ΔΣc : view(ΔΣc, diagind(ΔΣc)) - svd_pullback!(b, Uc, Σdc, V⁺c, ΔUc, ΔΣdc, ΔV⁺c) + foreachblock(Δt) do (c, b) + USVᴴc = block(U, c), block(Σ, c), block(V⁺, c) + ΔUSVᴴc = block(ΔU, c), block(ΔΣ, c), block(ΔV⁺, c) + svd_compact_pullback!(b, USVᴴc, ΔUSVᴴc) + return nothing end return NoTangent(), Δt end @@ -187,122 +187,122 @@ end # Other implementation considerations for GPU compatibility: # no scalar indexing, lots of broadcasting and views # -function svd_pullback!(ΔA::AbstractMatrix, U::AbstractMatrix, S::AbstractVector, - Vd::AbstractMatrix, ΔU, ΔS, ΔVd; - tol::Real=default_pullback_gaugetol(S)) - - # Basic size checks and determination - m, n = size(U, 1), size(Vd, 2) - size(U, 2) == size(Vd, 1) == length(S) == min(m, n) || throw(DimensionMismatch()) - p = -1 - if !(ΔU isa AbstractZero) - m == size(ΔU, 1) || throw(DimensionMismatch()) - p = size(ΔU, 2) - end - if !(ΔVd isa AbstractZero) - n == size(ΔVd, 2) || throw(DimensionMismatch()) - if p == -1 - p = size(ΔVd, 1) - else - p == size(ΔVd, 1) || throw(DimensionMismatch()) - end - end - if !(ΔS isa AbstractZero) - if p == -1 - p = length(ΔS) - else - p == length(ΔS) || throw(DimensionMismatch()) - end - end - Up = view(U, :, 1:p) - Vp = view(Vd, 1:p, :)' - Sp = view(S, 1:p) - - # rank - r = searchsortedlast(S, tol; rev=true) - - # compute antihermitian part of projection of ΔU and ΔV onto U and V - # also already subtract this projection from ΔU and ΔV - if !(ΔU isa AbstractZero) - UΔU = Up' * ΔU - aUΔU = rmul!(UΔU - UΔU', 1 / 2) - if m > p - ΔU -= Up * UΔU - end - else - aUΔU = fill!(similar(U, (p, p)), 0) - end - if !(ΔVd isa AbstractZero) - VΔV = Vp' * ΔVd' - aVΔV = rmul!(VΔV - VΔV', 1 / 2) - if n > p - ΔVd -= VΔV' * Vp' - end - else - aVΔV = fill!(similar(Vd, (p, p)), 0) - end - - # check whether cotangents arise from gauge-invariance objective function - mask = abs.(Sp' .- Sp) .< tol - Δgauge = norm(view(aUΔU, mask) + view(aVΔV, mask), Inf) - if p > r - rprange = (r + 1):p - Δgauge = max(Δgauge, norm(view(aUΔU, rprange, rprange), Inf)) - Δgauge = max(Δgauge, norm(view(aVΔV, rprange, rprange), Inf)) - end - Δgauge < tol || - @warn "`svd` cotangents sensitive to gauge choice: (|Δgauge| = $Δgauge)" - - UdΔAV = (aUΔU .+ aVΔV) .* safe_inv.(Sp' .- Sp, tol) .+ - (aUΔU .- aVΔV) .* safe_inv.(Sp' .+ Sp, tol) - if !(ΔS isa ZeroTangent) - UdΔAV[diagind(UdΔAV)] .+= real.(ΔS) - # in principle, ΔS is real, but maybe not if coming from an anyonic tensor - end - mul!(ΔA, Up, UdΔAV * Vp') - - if r > p # contribution from truncation - Ur = view(U, :, (p + 1):r) - Vr = view(Vd, (p + 1):r, :)' - Sr = view(S, (p + 1):r) - - if !(ΔU isa AbstractZero) - UrΔU = Ur' * ΔU - if m > r - ΔU -= Ur * UrΔU # subtract this part from ΔU - end - else - UrΔU = fill!(similar(U, (r - p, p)), 0) - end - if !(ΔVd isa AbstractZero) - VrΔV = Vr' * ΔVd' - if n > r - ΔVd -= VrΔV' * Vr' # subtract this part from ΔV - end - else - VrΔV = fill!(similar(Vd, (r - p, p)), 0) - end - - X = (1 // 2) .* ((UrΔU .+ VrΔV) .* safe_inv.(Sp' .- Sr, tol) .+ - (UrΔU .- VrΔV) .* safe_inv.(Sp' .+ Sr, tol)) - Y = (1 // 2) .* ((UrΔU .+ VrΔV) .* safe_inv.(Sp' .- Sr, tol) .- - (UrΔU .- VrΔV) .* safe_inv.(Sp' .+ Sr, tol)) - - # ΔA += Ur * X * Vp' + Up * Y' * Vr' - mul!(ΔA, Ur, X * Vp', 1, 1) - mul!(ΔA, Up * Y', Vr', 1, 1) - end - - if m > max(r, p) && !(ΔU isa AbstractZero) # remaining ΔU is already orthogonal to U[:,1:max(p,r)] - # ΔA += (ΔU .* safe_inv.(Sp', tol)) * Vp' - mul!(ΔA, ΔU .* safe_inv.(Sp', tol), Vp', 1, 1) - end - if n > max(r, p) && !(ΔVd isa AbstractZero) # remaining ΔV is already orthogonal to V[:,1:max(p,r)] - # ΔA += U * (safe_inv.(Sp, tol) .* ΔVd) - mul!(ΔA, Up, safe_inv.(Sp, tol) .* ΔVd, 1, 1) - end - return ΔA -end +# function svd_pullback!(ΔA::AbstractMatrix, U::AbstractMatrix, S::AbstractVector, +# Vd::AbstractMatrix, ΔU, ΔS, ΔVd; +# tol::Real=default_pullback_gaugetol(S)) + +# # Basic size checks and determination +# m, n = size(U, 1), size(Vd, 2) +# size(U, 2) == size(Vd, 1) == length(S) == min(m, n) || throw(DimensionMismatch()) +# p = -1 +# if !(ΔU isa AbstractZero) +# m == size(ΔU, 1) || throw(DimensionMismatch()) +# p = size(ΔU, 2) +# end +# if !(ΔVd isa AbstractZero) +# n == size(ΔVd, 2) || throw(DimensionMismatch()) +# if p == -1 +# p = size(ΔVd, 1) +# else +# p == size(ΔVd, 1) || throw(DimensionMismatch()) +# end +# end +# if !(ΔS isa AbstractZero) +# if p == -1 +# p = length(ΔS) +# else +# p == length(ΔS) || throw(DimensionMismatch()) +# end +# end +# Up = view(U, :, 1:p) +# Vp = view(Vd, 1:p, :)' +# Sp = view(S, 1:p) + +# # rank +# r = searchsortedlast(S, tol; rev=true) + +# # compute antihermitian part of projection of ΔU and ΔV onto U and V +# # also already subtract this projection from ΔU and ΔV +# if !(ΔU isa AbstractZero) +# UΔU = Up' * ΔU +# aUΔU = rmul!(UΔU - UΔU', 1 / 2) +# if m > p +# ΔU -= Up * UΔU +# end +# else +# aUΔU = fill!(similar(U, (p, p)), 0) +# end +# if !(ΔVd isa AbstractZero) +# VΔV = Vp' * ΔVd' +# aVΔV = rmul!(VΔV - VΔV', 1 / 2) +# if n > p +# ΔVd -= VΔV' * Vp' +# end +# else +# aVΔV = fill!(similar(Vd, (p, p)), 0) +# end + +# # check whether cotangents arise from gauge-invariance objective function +# mask = abs.(Sp' .- Sp) .< tol +# Δgauge = norm(view(aUΔU, mask) + view(aVΔV, mask), Inf) +# if p > r +# rprange = (r + 1):p +# Δgauge = max(Δgauge, norm(view(aUΔU, rprange, rprange), Inf)) +# Δgauge = max(Δgauge, norm(view(aVΔV, rprange, rprange), Inf)) +# end +# Δgauge < tol || +# @warn "`svd` cotangents sensitive to gauge choice: (|Δgauge| = $Δgauge)" + +# UdΔAV = (aUΔU .+ aVΔV) .* safe_inv.(Sp' .- Sp, tol) .+ +# (aUΔU .- aVΔV) .* safe_inv.(Sp' .+ Sp, tol) +# if !(ΔS isa ZeroTangent) +# UdΔAV[diagind(UdΔAV)] .+= real.(ΔS) +# # in principle, ΔS is real, but maybe not if coming from an anyonic tensor +# end +# mul!(ΔA, Up, UdΔAV * Vp') + +# if r > p # contribution from truncation +# Ur = view(U, :, (p + 1):r) +# Vr = view(Vd, (p + 1):r, :)' +# Sr = view(S, (p + 1):r) + +# if !(ΔU isa AbstractZero) +# UrΔU = Ur' * ΔU +# if m > r +# ΔU -= Ur * UrΔU # subtract this part from ΔU +# end +# else +# UrΔU = fill!(similar(U, (r - p, p)), 0) +# end +# if !(ΔVd isa AbstractZero) +# VrΔV = Vr' * ΔVd' +# if n > r +# ΔVd -= VrΔV' * Vr' # subtract this part from ΔV +# end +# else +# VrΔV = fill!(similar(Vd, (r - p, p)), 0) +# end + +# X = (1 // 2) .* ((UrΔU .+ VrΔV) .* safe_inv.(Sp' .- Sr, tol) .+ +# (UrΔU .- VrΔV) .* safe_inv.(Sp' .+ Sr, tol)) +# Y = (1 // 2) .* ((UrΔU .+ VrΔV) .* safe_inv.(Sp' .- Sr, tol) .- +# (UrΔU .- VrΔV) .* safe_inv.(Sp' .+ Sr, tol)) + +# # ΔA += Ur * X * Vp' + Up * Y' * Vr' +# mul!(ΔA, Ur, X * Vp', 1, 1) +# mul!(ΔA, Up * Y', Vr', 1, 1) +# end + +# if m > max(r, p) && !(ΔU isa AbstractZero) # remaining ΔU is already orthogonal to U[:,1:max(p,r)] +# # ΔA += (ΔU .* safe_inv.(Sp', tol)) * Vp' +# mul!(ΔA, ΔU .* safe_inv.(Sp', tol), Vp', 1, 1) +# end +# if n > max(r, p) && !(ΔVd isa AbstractZero) # remaining ΔV is already orthogonal to V[:,1:max(p,r)] +# # ΔA += U * (safe_inv.(Sp, tol) .* ΔVd) +# mul!(ΔA, Up, safe_inv.(Sp, tol) .* ΔVd, 1, 1) +# end +# return ΔA +# end function eig_pullback!(ΔA::AbstractMatrix, D::AbstractVector, V::AbstractMatrix, ΔD, ΔV; tol::Real=default_pullback_gaugetol(D)) diff --git a/src/TensorKit.jl b/src/TensorKit.jl index bad0a4dab..60992e0c4 100644 --- a/src/TensorKit.jl +++ b/src/TensorKit.jl @@ -31,7 +31,7 @@ export TruncationScheme export SpaceMismatch, SectorMismatch, IndexError # error types # general vector space methods -export space, field, dual, dim, reduceddim, dims, fuse, flip, isdual, oplus, +export space, field, dual, dim, reduceddim, dims, fuse, flip, isdual, oplus, ominus, insertleftunit, insertrightunit, removeunit # partial order for vector spaces @@ -47,7 +47,7 @@ export ZNSpace, SU2Irrep, U1Irrep, CU1Irrep # bendleft, bendright, foldleft, foldright, cycleclockwise, cycleanticlockwise # some unicode -export ⊕, ⊗, ×, ⊠, ℂ, ℝ, ℤ, ←, →, ≾, ≿, ≅, ≺, ≻ +export ⊕, ⊗, ⊖, ×, ⊠, ℂ, ℝ, ℤ, ←, →, ≾, ≿, ≅, ≺, ≻ export ℤ₂, ℤ₃, ℤ₄, U₁, SU, SU₂, CU₁ export fℤ₂, fU₁, fSU₂ export ℤ₂Space, ℤ₃Space, ℤ₄Space, U₁Space, CU₁Space, SU₂Space @@ -70,8 +70,8 @@ export inner, dot, norm, normalize, normalize!, tr # factorizations export mul!, lmul!, rmul!, adjoint!, pinv, axpy!, axpby! -export leftorth, rightorth, leftnull, rightnull, - leftorth!, rightorth!, leftnull!, rightnull!, +export leftorth, rightorth, leftnull, rightnull, leftpolar, rightpolar, + leftorth!, rightorth!, leftnull!, rightnull!, leftpolar!, rightpolar!, tsvd!, tsvd, eigen, eigen!, eig, eig!, eigh, eigh!, exp, exp!, isposdef, isposdef!, ishermitian, isisometry, sylvester, rank, cond export braid, braid!, permute, permute!, transpose, transpose!, twist, twist!, repartition, @@ -204,11 +204,13 @@ include("tensors/tensoroperations.jl") include("tensors/treetransformers.jl") include("tensors/indexmanipulations.jl") include("tensors/diagonal.jl") -include("tensors/truncation.jl") -include("tensors/matrixalgebrakit.jl") -include("tensors/factorizations.jl") include("tensors/braidingtensor.jl") +include("tensors/factorizations/factorizations.jl") +using .Factorizations +# include("tensors/factorizations/matrixalgebrakit.jl") +# include("tensors/truncation.jl") + # # Planar macros and related functionality # #----------------------------------------- @nospecialize diff --git a/src/auxiliary/deprecate.jl b/src/auxiliary/deprecate.jl index fa7667b2b..b235cbd7c 100644 --- a/src/auxiliary/deprecate.jl +++ b/src/auxiliary/deprecate.jl @@ -1,29 +1,29 @@ import Base: transpose #! format: off -Base.@deprecate(permute(t::AbstractTensorMap, p1::IndexTuple, p2::IndexTuple; copy::Bool=false), - permute(t, (p1, p2); copy=copy)) -Base.@deprecate(transpose(t::AbstractTensorMap, p1::IndexTuple, p2::IndexTuple; copy::Bool=false), - transpose(t, (p1, p2); copy=copy)) -Base.@deprecate(braid(t::AbstractTensorMap, p1::IndexTuple, p2::IndexTuple, levels; copy::Bool=false), - braid(t, (p1, p2), levels; copy=copy)) - -Base.@deprecate(tsvd(t::AbstractTensorMap, p₁::IndexTuple, p₂::IndexTuple; kwargs...), - tsvd(t, (p₁, p₂); kwargs...)) -Base.@deprecate(leftorth(t::AbstractTensorMap, p₁::IndexTuple, p₂::IndexTuple; kwargs...), - leftorth(t, (p₁, p₂); kwargs...)) -Base.@deprecate(rightorth(t::AbstractTensorMap, p₁::IndexTuple, p₂::IndexTuple; kwargs...), - rightorth(t, (p₁, p₂); kwargs...)) -Base.@deprecate(leftnull(t::AbstractTensorMap, p₁::IndexTuple, p₂::IndexTuple; kwargs...), - leftnull(t, (p₁, p₂); kwargs...)) -Base.@deprecate(rightnull(t::AbstractTensorMap, p₁::IndexTuple, p₂::IndexTuple; kwargs...), - rightnull(t, (p₁, p₂); kwargs...)) -Base.@deprecate(LinearAlgebra.eigen(t::AbstractTensorMap, p₁::IndexTuple, p₂::IndexTuple; kwargs...), - LinearAlgebra.eigen(t, (p₁, p₂); kwargs...), false) -Base.@deprecate(eig(t::AbstractTensorMap, p₁::IndexTuple, p₂::IndexTuple; kwargs...), - eig(t, (p₁, p₂); kwargs...)) -Base.@deprecate(eigh(t::AbstractTensorMap, p₁::IndexTuple, p₂::IndexTuple; kwargs...), - eigh(t, (p₁, p₂); kwargs...)) +# Base.@deprecate(permute(t::AbstractTensorMap, p1::IndexTuple, p2::IndexTuple; copy::Bool=false), +# permute(t, (p1, p2); copy=copy)) +# Base.@deprecate(transpose(t::AbstractTensorMap, p1::IndexTuple, p2::IndexTuple; copy::Bool=false), +# transpose(t, (p1, p2); copy=copy)) +# Base.@deprecate(braid(t::AbstractTensorMap, p1::IndexTuple, p2::IndexTuple, levels; copy::Bool=false), +# braid(t, (p1, p2), levels; copy=copy)) + +# Base.@deprecate(tsvd(t::AbstractTensorMap, p₁::IndexTuple, p₂::IndexTuple; kwargs...), +# tsvd(t, (p₁, p₂); kwargs...)) +# Base.@deprecate(leftorth(t::AbstractTensorMap, p₁::IndexTuple, p₂::IndexTuple; kwargs...), +# leftorth(t, (p₁, p₂); kwargs...)) +# Base.@deprecate(rightorth(t::AbstractTensorMap, p₁::IndexTuple, p₂::IndexTuple; kwargs...), +# rightorth(t, (p₁, p₂); kwargs...)) +# Base.@deprecate(leftnull(t::AbstractTensorMap, p₁::IndexTuple, p₂::IndexTuple; kwargs...), +# leftnull(t, (p₁, p₂); kwargs...)) +# Base.@deprecate(rightnull(t::AbstractTensorMap, p₁::IndexTuple, p₂::IndexTuple; kwargs...), +# rightnull(t, (p₁, p₂); kwargs...)) +# Base.@deprecate(LinearAlgebra.eigen(t::AbstractTensorMap, p₁::IndexTuple, p₂::IndexTuple; kwargs...), +# LinearAlgebra.eigen(t, (p₁, p₂); kwargs...), false) +# Base.@deprecate(eig(t::AbstractTensorMap, p₁::IndexTuple, p₂::IndexTuple; kwargs...), +# eig(t, (p₁, p₂); kwargs...)) +# Base.@deprecate(eigh(t::AbstractTensorMap, p₁::IndexTuple, p₂::IndexTuple; kwargs...), +# eigh(t, (p₁, p₂); kwargs...)) for f in (:rand, :randn, :zeros, :ones) @eval begin diff --git a/src/spaces/vectorspaces.jl b/src/spaces/vectorspaces.jl index c7d4cfa16..abc5ad32a 100644 --- a/src/spaces/vectorspaces.jl +++ b/src/spaces/vectorspaces.jl @@ -416,4 +416,3 @@ have the same value. function supremum(V₁::S, V₂::S, V₃::S...) where {S<:ElementarySpace} return supremum(supremum(V₁, V₂), V₃...) end - diff --git a/src/tensors/backends.jl b/src/tensors/backends.jl index 1fc970e72..1083115b9 100644 --- a/src/tensors/backends.jl +++ b/src/tensors/backends.jl @@ -30,15 +30,3 @@ end # TODO: disable for trivial symmetry or small tensors? default_blockscheduler(t::AbstractTensorMap) = default_blockscheduler(typeof(t)) default_blockscheduler(::Type{T}) where {T<:AbstractTensorMap} = blockscheduler[] - -# MatrixAlgebraKit -# ---------------- -""" - BlockAlgorithm{A,S}(alg, scheduler) - -Generic wrapper for implementing block-wise algorithms. -""" -struct BlockAlgorithm{A,S} <: MatrixAlgebraKit.AbstractAlgorithm - alg::A - scheduler::S -end diff --git a/src/tensors/diagonal.jl b/src/tensors/diagonal.jl index 88d0c3b25..5a3840f1b 100644 --- a/src/tensors/diagonal.jl +++ b/src/tensors/diagonal.jl @@ -317,65 +317,6 @@ function LinearAlgebra.isposdef(d::DiagonalTensorMap) return all(isposdef, d.data) end -function eig!(d::DiagonalTensorMap) - return d, one(d) -end -function eigh!(d::DiagonalTensorMap{<:Real}) - return d, one(d) -end -function eigh!(d::DiagonalTensorMap{<:Complex}) - # TODO: should this test for hermiticity? `eigh!(::TensorMap)` also does not do this. - return DiagonalTensorMap(real(d.data), d.domain), one(d) -end - -function leftorth!(d::DiagonalTensorMap; alg=QR(), kwargs...) - @assert alg isa Union{QR,QL} - return one(d), d # TODO: this is only correct for `alg = QR()` or `alg = QL()` -end -function rightorth!(d::DiagonalTensorMap; alg=LQ(), kwargs...) - @assert alg isa Union{LQ,RQ} - return d, one(d) # TODO: this is only correct for `alg = LQ()` or `alg = RQ()` -end -# not much to do here: -leftnull!(d::DiagonalTensorMap; kwargs...) = leftnull!(TensorMap(d); kwargs...) -rightnull!(d::DiagonalTensorMap; kwargs...) = rightnull!(TensorMap(d); kwargs...) - -function tsvd!(d::DiagonalTensorMap; trunc=NoTruncation(), p::Real=2, alg=SDD()) - return _tsvd!(d, alg, trunc, p) -end -# helper function -function _compute_svddata!(d::DiagonalTensorMap, alg::Union{SVD,SDD}) - InnerProductStyle(d) === EuclideanInnerProduct() || throw_invalid_innerproduct(:tsvd!) - I = sectortype(d) - dims = SectorDict{I,Int}() - generator = Base.Iterators.map(blocks(d)) do (c, b) - lb = length(b.diag) - U = zerovector!(similar(b.diag, lb, lb)) - V = zerovector!(similar(b.diag, lb, lb)) - p = sortperm(b.diag; by=abs, rev=true) - for (i, pi) in enumerate(p) - U[pi, i] = MatrixAlgebra.safesign(b.diag[pi]) - V[i, pi] = 1 - end - Σ = abs.(view(b.diag, p)) - dims[c] = lb - return c => (U, Σ, V) - end - SVDdata = SectorDict(generator) - return SVDdata, dims -end - -function LinearAlgebra.svdvals(d::DiagonalTensorMap) - return SectorDict(c => LinearAlgebra.svdvals(b) for (c, b) in blocks(d)) -end -function LinearAlgebra.eigvals(d::DiagonalTensorMap) - return SectorDict(c => LinearAlgebra.eigvals(b) for (c, b) in blocks(d)) -end - -function LinearAlgebra.cond(d::DiagonalTensorMap, p::Real=2) - return LinearAlgebra.cond(Diagonal(d.data), p) -end - # matrix functions for f in (:exp, :cos, :sin, :tan, :cot, :cosh, :sinh, :tanh, :coth, :atan, :acot, :asinh, :sqrt, diff --git a/src/tensors/factorizations.jl b/src/tensors/factorizations.jl deleted file mode 100644 index fe4943ee7..000000000 --- a/src/tensors/factorizations.jl +++ /dev/null @@ -1,707 +0,0 @@ -# Tensor factorization -#---------------------- -function factorisation_scalartype(t::AbstractTensorMap) - T = scalartype(t) - return promote_type(Float32, typeof(zero(T) / sqrt(abs2(one(T))))) -end -factorisation_scalartype(f, t) = factorisation_scalartype(t) - -function permutedcopy_oftype(t::AbstractTensorMap, T::Type{<:Number}, p::Index2Tuple) - return permute!(similar(t, T, permute(space(t), p)), t, p) -end -function copy_oftype(t::AbstractTensorMap, T::Type{<:Number}) - return copy!(similar(t, T), t) -end - -""" - tsvd(t::AbstractTensorMap, (leftind, rightind)::Index2Tuple; - trunc::TruncationScheme = notrunc(), p::Real = 2, alg::Union{SVD, SDD} = SDD()) - -> U, S, V, ϵ - -Compute the (possibly truncated) singular value decomposition such that -`norm(permute(t, (leftind, rightind)) - U * S * V) ≈ ϵ`, where `ϵ` thus represents the truncation error. - -If `leftind` and `rightind` are not specified, the current partition of left and right -indices of `t` is used. In that case, less memory is allocated if one allows the data in -`t` to be destroyed/overwritten, by using `tsvd!(t, trunc = notrunc(), p = 2)`. - -A truncation parameter `trunc` can be specified for the new internal dimension, in which -case a truncated singular value decomposition will be computed. Choices are: -* `notrunc()`: no truncation (default); -* `truncerr(η::Real)`: truncates such that the p-norm of the truncated singular values is - smaller than `η`; -* `truncdim(χ::Int)`: truncates such that the equivalent total dimension of the internal - vector space is no larger than `χ`; -* `truncspace(V)`: truncates such that the dimension of the internal vector space is - smaller than that of `V` in any sector. -* `truncbelow(η::Real)`: truncates such that every singular value is larger then `η` ; - -Truncation options can also be combined using `&`, i.e. `truncbelow(η) & truncdim(χ)` will -choose the truncation space such that every singular value is larger than `η`, and the -equivalent total dimension of the internal vector space is no larger than `χ`. - -The method `tsvd` also returns the truncation error `ϵ`, computed as the `p` norm of the -singular values that were truncated. - -THe keyword `alg` can be equal to `SVD()` or `SDD()`, corresponding to the underlying LAPACK -algorithm that computes the decomposition (`_gesvd` or `_gesdd`). - -Orthogonality requires `InnerProductStyle(t) <: HasInnerProduct`, and `tsvd(!)` -is currently only implemented for `InnerProductStyle(t) === EuclideanInnerProduct()`. -""" -function tsvd(t::AbstractTensorMap, p::Index2Tuple; kwargs...) - tcopy = permutedcopy_oftype(t, factorisation_scalartype(tsvd, t), p) - return tsvd!(tcopy; kwargs...) -end - -function LinearAlgebra.svdvals(t::AbstractTensorMap) - tcopy = copy_oftype(t, factorisation_scalartype(tsvd, t)) - return LinearAlgebra.svdvals!(tcopy) -end - -""" - leftorth(t::AbstractTensorMap, (leftind, rightind)::Index2Tuple; - alg::OrthogonalFactorizationAlgorithm = QRpos()) -> Q, R - -Create orthonormal basis `Q` for indices in `leftind`, and remainder `R` such that -`permute(t, (leftind, rightind)) = Q*R`. - -If `leftind` and `rightind` are not specified, the current partition of left and right -indices of `t` is used. In that case, less memory is allocated if one allows the data in `t` -to be destroyed/overwritten, by using `leftorth!(t, alg = QRpos())`. - -Different algorithms are available, namely `QR()`, `QRpos()`, `SVD()` and `Polar()`. `QR()` -and `QRpos()` use a standard QR decomposition, producing an upper triangular matrix `R`. -`Polar()` produces a Hermitian and positive semidefinite `R`. `QRpos()` corrects the -standard QR decomposition such that the diagonal elements of `R` are positive. Only -`QRpos()` and `Polar()` are unique (no residual freedom) so that they always return the same -result for the same input tensor `t`. - -Orthogonality requires `InnerProductStyle(t) <: HasInnerProduct`, and -`leftorth(!)` is currently only implemented for - `InnerProductStyle(t) === EuclideanInnerProduct()`. -""" -function leftorth(t::AbstractTensorMap, p::Index2Tuple; kwargs...) - tcopy = permutedcopy_oftype(t, factorisation_scalartype(leftorth, t), p) - return leftorth!(tcopy; kwargs...) -end - -""" - rightorth(t::AbstractTensorMap, (leftind, rightind)::Index2Tuple; - alg::OrthogonalFactorizationAlgorithm = LQpos()) -> L, Q - -Create orthonormal basis `Q` for indices in `rightind`, and remainder `L` such that -`permute(t, (leftind, rightind)) = L*Q`. - -If `leftind` and `rightind` are not specified, the current partition of left and right -indices of `t` is used. In that case, less memory is allocated if one allows the data in `t` -to be destroyed/overwritten, by using `rightorth!(t, alg = LQpos())`. - -Different algorithms are available, namely `LQ()`, `LQpos()`, `RQ()`, `RQpos()`, `SVD()` and -`Polar()`. `LQ()` and `LQpos()` produce a lower triangular matrix `L` and are computed using -a QR decomposition of the transpose. `RQ()` and `RQpos()` produce an upper triangular -remainder `L` and only works if the total left dimension is smaller than or equal to the -total right dimension. `LQpos()` and `RQpos()` add an additional correction such that the -diagonal elements of `L` are positive. `Polar()` produces a Hermitian and positive -semidefinite `L`. Only `LQpos()`, `RQpos()` and `Polar()` are unique (no residual freedom) -so that they always return the same result for the same input tensor `t`. - -Orthogonality requires `InnerProductStyle(t) <: HasInnerProduct`, and -`rightorth(!)` is currently only implemented for -`InnerProductStyle(t) === EuclideanInnerProduct()`. -""" -function rightorth(t::AbstractTensorMap, p::Index2Tuple; kwargs...) - tcopy = permutedcopy_oftype(t, factorisation_scalartype(rightorth, t), p) - return rightorth!(tcopy; kwargs...) -end - -""" - leftnull(t::AbstractTensor, (leftind, rightind)::Index2Tuple; - alg::OrthogonalFactorizationAlgorithm = QRpos()) -> N - -Create orthonormal basis for the orthogonal complement of the support of the indices in -`leftind`, such that `N' * permute(t, (leftind, rightind)) = 0`. - -If `leftind` and `rightind` are not specified, the current partition of left and right -indices of `t` is used. In that case, less memory is allocated if one allows the data in `t` -to be destroyed/overwritten, by using `leftnull!(t, alg = QRpos())`. - -Different algorithms are available, namely `QR()` (or equivalently, `QRpos()`), `SVD()` and -`SDD()`. The first assumes that the matrix is full rank and requires `iszero(atol)` and -`iszero(rtol)`. With `SVD()` and `SDD()`, `rightnull` will use the corresponding singular -value decomposition, and one can specify an absolute or relative tolerance for which -singular values are to be considered zero, where `max(atol, norm(t)*rtol)` is used as upper -bound. - -Orthogonality requires `InnerProductStyle(t) <: HasInnerProduct`, and -`leftnull(!)` is currently only implemented for -`InnerProductStyle(t) === EuclideanInnerProduct()`. -""" -function leftnull(t::AbstractTensorMap, p::Index2Tuple; kwargs...) - tcopy = permutedcopy_oftype(t, factorisation_scalartype(leftnull, t), p) - return leftnull!(tcopy; kwargs...) -end - -""" - rightnull(t::AbstractTensor, (leftind, rightind)::Index2Tuple; - alg::OrthogonalFactorizationAlgorithm = LQ(), - atol::Real = 0.0, - rtol::Real = eps(real(float(one(scalartype(t)))))*iszero(atol)) -> N - -Create orthonormal basis for the orthogonal complement of the support of the indices in -`rightind`, such that `permute(t, (leftind, rightind))*N' = 0`. - -If `leftind` and `rightind` are not specified, the current partition of left and right -indices of `t` is used. In that case, less memory is allocated if one allows the data in `t` -to be destroyed/overwritten, by using `rightnull!(t, alg = LQpos())`. - -Different algorithms are available, namely `LQ()` (or equivalently, `LQpos`), `SVD()` and -`SDD()`. The first assumes that the matrix is full rank and requires `iszero(atol)` and -`iszero(rtol)`. With `SVD()` and `SDD()`, `rightnull` will use the corresponding singular -value decomposition, and one can specify an absolute or relative tolerance for which -singular values are to be considered zero, where `max(atol, norm(t)*rtol)` is used as upper -bound. - -Orthogonality requires `InnerProductStyle(t) <: HasInnerProduct`, and -`rightnull(!)` is currently only implemented for -`InnerProductStyle(t) === EuclideanInnerProduct()`. -""" -function rightnull(t::AbstractTensorMap, p::Index2Tuple; kwargs...) - tcopy = permutedcopy_oftype(t, factorisation_scalartype(rightnull, t), p) - return rightnull!(tcopy; kwargs...) -end - -""" - eigen(t::AbstractTensor, (leftind, rightind)::Index2Tuple; kwargs...) -> D, V - -Compute eigenvalue factorization of tensor `t` as linear map from `rightind` to `leftind`. - -If `leftind` and `rightind` are not specified, the current partition of left and right -indices of `t` is used. In that case, less memory is allocated if one allows the data in `t` -to be destroyed/overwritten, by using `eigen!(t)`. Note that the permuted tensor on which -`eigen!` is called should have equal domain and codomain, as otherwise the eigenvalue -decomposition is meaningless and cannot satisfy -``` -permute(t, (leftind, rightind)) * V = V * D -``` - -Accepts the same keyword arguments `scale` and `permute` as `eigen` of dense -matrices. See the corresponding documentation for more information. - -See also `eig` and `eigh` -""" -function LinearAlgebra.eigen(t::AbstractTensorMap, p::Index2Tuple; kwargs...) - tcopy = permutedcopy_oftype(t, factorisation_scalartype(eigen, t), p) - return eigen!(tcopy; kwargs...) -end - -function LinearAlgebra.eigvals(t::AbstractTensorMap; kwargs...) - tcopy = copy_oftype(t, factorisation_scalartype(eigen, t)) - return LinearAlgebra.eigvals!(tcopy; kwargs...) -end - -""" - eig(t::AbstractTensor, (leftind, rightind)::Index2Tuple; kwargs...) -> D, V - -Compute eigenvalue factorization of tensor `t` as linear map from `rightind` to `leftind`. -The function `eig` assumes that the linear map is not hermitian and returns type stable -complex valued `D` and `V` tensors for both real and complex valued `t`. See `eigh` for -hermitian linear maps - -If `leftind` and `rightind` are not specified, the current partition of left and right -indices of `t` is used. In that case, less memory is allocated if one allows the data in -`t` to be destroyed/overwritten, by using `eig!(t)`. Note that the permuted tensor on -which `eig!` is called should have equal domain and codomain, as otherwise the eigenvalue -decomposition is meaningless and cannot satisfy -``` -permute(t, (leftind, rightind)) * V = V * D -``` - -Accepts the same keyword arguments `scale` and `permute` as `eigen` of dense -matrices. See the corresponding documentation for more information. - -See also `eigen` and `eigh`. -""" -function eig(t::AbstractTensorMap, p::Index2Tuple; kwargs...) - tcopy = permutedcopy_oftype(t, factorisation_scalartype(eig, t), p) - return eig!(tcopy; kwargs...) -end - -""" - eigh(t::AbstractTensorMap, (leftind, rightind)::Index2Tuple) -> D, V - -Compute eigenvalue factorization of tensor `t` as linear map from `rightind` to `leftind`. -The function `eigh` assumes that the linear map is hermitian and `D` and `V` tensors with -the same `scalartype` as `t`. See `eig` and `eigen` for non-hermitian tensors. Hermiticity -requires that the tensor acts on inner product spaces, and the current implementation -requires `InnerProductStyle(t) === EuclideanInnerProduct()`. - -If `leftind` and `rightind` are not specified, the current partition of left and right -indices of `t` is used. In that case, less memory is allocated if one allows the data in -`t` to be destroyed/overwritten, by using `eigh!(t)`. Note that the permuted tensor on -which `eigh!` is called should have equal domain and codomain, as otherwise the eigenvalue -decomposition is meaningless and cannot satisfy -``` -permute(t, (leftind, rightind)) * V = V * D -``` - -See also `eigen` and `eig`. -""" -function eigh(t::AbstractTensorMap, p::Index2Tuple; kwargs...) - tcopy = permutedcopy_oftype(t, factorisation_scalartype(eigh, t), p) - return eigh!(tcopy; kwargs...) -end - -""" - isposdef(t::AbstractTensor, (leftind, rightind)::Index2Tuple) -> ::Bool - -Test whether a tensor `t` is positive definite as linear map from `rightind` to `leftind`. - -If `leftind` and `rightind` are not specified, the current partition of left and right -indices of `t` is used. In that case, less memory is allocated if one allows the data in -`t` to be destroyed/overwritten, by using `isposdef!(t)`. Note that the permuted tensor on -which `isposdef!` is called should have equal domain and codomain, as otherwise it is -meaningless. -""" -function LinearAlgebra.isposdef(t::AbstractTensorMap, (p₁, p₂)::Index2Tuple) - tcopy = permutedcopy_oftype(t, factorisation_scalartype(isposdef, t), p) - return isposdef!(tcopy) -end - -function isisometry(t::AbstractTensorMap, (p₁, p₂)::Index2Tuple) - t = permute(t, (p₁, p₂); copy=false) - return isisometry(t) -end - -function tsvd(t::AbstractTensorMap; kwargs...) - tcopy = copy_oftype(t, float(scalartype(t))) - return tsvd!(tcopy; kwargs...) -end -function leftorth(t::AbstractTensorMap; alg::OFA=QRpos(), kwargs...) - tcopy = copy_oftype(t, float(scalartype(t))) - return leftorth!(tcopy; alg=alg, kwargs...) -end -function rightorth(t::AbstractTensorMap; alg::OFA=LQpos(), kwargs...) - tcopy = copy_oftype(t, float(scalartype(t))) - return rightorth!(tcopy; alg=alg, kwargs...) -end -function leftnull(t::AbstractTensorMap; alg::OFA=QR(), kwargs...) - tcopy = copy_oftype(t, float(scalartype(t))) - return leftnull!(tcopy; alg=alg, kwargs...) -end -function rightnull(t::AbstractTensorMap; alg::OFA=LQ(), kwargs...) - tcopy = copy_oftype(t, float(scalartype(t))) - return rightnull!(tcopy; alg=alg, kwargs...) -end -function LinearAlgebra.eigen(t::AbstractTensorMap; kwargs...) - tcopy = copy_oftype(t, float(scalartype(t))) - return eigen!(tcopy; kwargs...) -end -function eig(t::AbstractTensorMap; kwargs...) - tcopy = copy_oftype(t, float(scalartype(t))) - return eig!(tcopy; kwargs...) -end -function eigh(t::AbstractTensorMap; kwargs...) - tcopy = copy_oftype(t, float(scalartype(t))) - return eigh!(tcopy; kwargs...) -end -function LinearAlgebra.isposdef(t::AbstractTensorMap) - tcopy = copy_oftype(t, float(scalartype(t))) - return isposdef!(tcopy) -end - -# Orthogonal factorizations (mutation for recycling memory): -# only possible if scalar type is floating point -# only correct if Euclidean inner product -#------------------------------------------------------------------------------------------ -const RealOrComplexFloat = Union{AbstractFloat,Complex{<:AbstractFloat}} - -function _reverse!(t::AbstractTensorMap; dims=:) - for (c, b) in blocks(t) - reverse!(b; dims) - end - return t -end - -function leftorth!(t::TensorMap{<:RealOrComplexFloat}; - alg::Union{QR,QRpos,QL,QLpos,SVD,SDD,Polar,Nothing}=nothing, - kwargs...) - # atol::Real=zero(float(real(scalartype(t)))), - # rtol::Real=(alg ∉ (SVD(), SDD())) ? - # zero(float(real(scalartype(t)))) : - # eps(real(float(one(scalartype(t))))) * - # iszero(atol)) - InnerProductStyle(t) === EuclideanInnerProduct() || - throw_invalid_innerproduct(:leftorth!) - return _leftorth!(t, alg; kwargs...) - - # if alg == SVD() || alg == SDD() - # return _leftorth!(t, alg; atol, rtol) - # else - # (iszero(atol) && iszero(rtol)) || - # throw(ArgumentError("`leftorth!` with nonzero atol or rtol requires SVD or SDD algorithm")) - # return _leftorth!(t, alg) - # end -end - -# this promotes the algorithm to a positional argument for type stability reasons -# since polar has different number of output legs -# TODO: this seems like duplication from MatrixAlgebraKit.left_orth!, but that function -# only has its logic with the output already specified, which breaks for polar -function _leftorth!(t::TensorMap{<:RealOrComplexFloat}, alg; kwargs...) - trunc = isempty(kwargs) ? nothing : (; kwargs...) - if isnothing(alg) - return left_orth!(t; trunc) - elseif alg == SVD() - return left_orth!(t; kind=:svd, alg_svd=:LAPACK_QRIteration, trunc) - elseif alg == SDD() - return left_orth!(t; kind=:svd, alg_svd=:LAPACK_DivideAndConquer, trunc) - elseif alg == QR() - return left_orth!(t; kind=:qr, alg_qr=(; positive=false), trunc) - elseif alg == QRpos() - return left_orth!(t; kind=:qr, alg_qr=(; positive=true), trunc) - elseif alg == QL() || alg == QLpos() - _reverse!(t; dims=2) - Q, R = left_orth!(t; kind=:qr, alg_qr=(; positive=alg == QLpos()), trunc) - _reverse!(Q; dims=2) - _reverse!(R) - return Q, R - elseif alg == Polar() - return left_orth!(t; kind=:polar, trunc) - else - throw(ArgumentError(lazy"Invalid algorithm: $alg")) - end -end - - -function leftnull!(t::TensorMap{<:RealOrComplexFloat}; - alg::Union{QR,QRpos,SVD,SDD}=QRpos(), - atol::Real=zero(float(real(scalartype(t)))), - rtol::Real=(alg ∉ (SVD(), SDD())) ? zero(float(real(scalartype(t)))) : - eps(real(float(one(scalartype(t))))) * iszero(atol)) - InnerProductStyle(t) === EuclideanInnerProduct() || - throw_invalid_innerproduct(:leftnull!) - - if alg == SVD() || alg == SDD() - kind = :svd - alg_svd = BlockAlgorithm(alg == SVD() ? MatrixAlgebraKit.LAPACK_QRIteration() : - MatrixAlgebraKit.LAPACK_DivideAndConquer(), - default_blockscheduler(t)) - trunc = if iszero(atol) && iszero(rtol) - nothing - else - (; atol, rtol) - end - return left_null!(t; kind, alg_svd, trunc) - end - - (iszero(atol) && iszero(rtol)) || - throw(ArgumentError("`leftnull!` with nonzero atol or rtol requires SVD or SDD algorithm")) - - kind = :qr - alg_qr = (; positive=alg == QRpos()) - return left_null!(t; kind, alg_qr) -end - -function rightorth!(t::TensorMap{<:RealOrComplexFloat}; - alg::Union{LQ,LQpos,RQ,RQpos,SVD,SDD,Polar}=LQpos(), - atol::Real=zero(float(real(scalartype(t)))), - rtol::Real=(alg ∉ (SVD(), SDD())) ? zero(float(real(scalartype(t)))) : - eps(real(float(one(scalartype(t))))) * iszero(atol)) - InnerProductStyle(t) === EuclideanInnerProduct() || - throw_invalid_innerproduct(:rightorth!) - if !iszero(rtol) - atol = max(atol, rtol * norm(t)) - end - I = sectortype(t) - dims = SectorDict{I,Int}() - - # compute LQ factorization for each block - if !isempty(blocks(t)) - generator = Base.Iterators.map(blocks(t)) do (c, b) - Lc, Qc = MatrixAlgebra.rightorth!(b, alg, atol) - dims[c] = size(Qc, 1) - return c => (Lc, Qc) - end - LQdata = SectorDict(generator) - end - - # construct new space - S = spacetype(t) - V = S(dims) - if alg isa Polar - @assert V ≅ codomain(t) - W = codomain(t) - elseif length(codomain(t)) == 1 && codomain(t) ≅ V - W = codomain(t) - elseif length(domain(t)) == 1 && domain(t) ≅ V - W = domain(t) - else - W = ProductSpace(V) - end - - # construct output tensors - T = float(scalartype(t)) - L = similar(t, T, codomain(t) ← W) - Q = similar(t, T, W ← domain(t)) - if !isempty(blocks(t)) - for (c, (Lc, Qc)) in LQdata - copy!(block(L, c), Lc) - copy!(block(Q, c), Qc) - end - end - return L, Q -end - -function rightnull!(t::TensorMap{<:RealOrComplexFloat}; - alg::Union{LQ,LQpos,SVD,SDD}=LQpos(), - atol::Real=zero(float(real(scalartype(t)))), - rtol::Real=(alg ∉ (SVD(), SDD())) ? zero(float(real(scalartype(t)))) : - eps(real(float(one(scalartype(t))))) * iszero(atol)) - InnerProductStyle(t) === EuclideanInnerProduct() || - throw_invalid_innerproduct(:rightnull!) - if !iszero(rtol) - atol = max(atol, rtol * norm(t)) - end - I = sectortype(t) - dims = SectorDict{I,Int}() - - # compute LQ factorization for each block - V = domain(t) - if !isempty(blocksectors(V)) - generator = Base.Iterators.map(blocksectors(V)) do c - Nc = MatrixAlgebra.rightnull!(block(t, c), alg, atol) - dims[c] = size(Nc, 1) - return c => Nc - end - Ndata = SectorDict(generator) - end - - # construct new space - S = spacetype(t) - W = S(dims) - - # construct output tensor - T = float(scalartype(t)) - N = similar(t, T, W ← V) - if !isempty(blocksectors(V)) - for (c, Nc) in Ndata - copy!(block(N, c), Nc) - end - end - return N -end - -function leftorth!(t::AdjointTensorMap; alg::OFA=QRpos()) - InnerProductStyle(t) === EuclideanInnerProduct() || - throw_invalid_innerproduct(:leftorth!) - return map(adjoint, reverse(rightorth!(adjoint(t); alg=alg'))) -end - -function rightorth!(t::AdjointTensorMap; alg::OFA=LQpos()) - InnerProductStyle(t) === EuclideanInnerProduct() || - throw_invalid_innerproduct(:rightorth!) - return map(adjoint, reverse(leftorth!(adjoint(t); alg=alg'))) -end - -function leftnull!(t::AdjointTensorMap; alg::OFA=QR(), kwargs...) - InnerProductStyle(t) === EuclideanInnerProduct() || - throw_invalid_innerproduct(:leftnull!) - return adjoint(rightnull!(adjoint(t); alg=alg', kwargs...)) -end - -function rightnull!(t::AdjointTensorMap; alg::OFA=LQ(), kwargs...) - InnerProductStyle(t) === EuclideanInnerProduct() || - throw_invalid_innerproduct(:rightnull!) - return adjoint(leftnull!(adjoint(t); alg=alg', kwargs...)) -end - -#------------------------------# -# Singular value decomposition # -#------------------------------# -function LinearAlgebra.svdvals!(t::TensorMap{<:RealOrComplexFloat}) - return SectorDict(c => LinearAlgebra.svdvals!(b) for (c, b) in blocks(t)) -end -LinearAlgebra.svdvals!(t::AdjointTensorMap) = svdvals!(adjoint(t)) - -function tsvd!(t::TensorMap{<:RealOrComplexFloat}; - trunc=NoTruncation(), p::Real=2, alg=SDD()) - return _tsvd!(t, alg, trunc, p) -end -function tsvd!(t::AdjointTensorMap; trunc=NoTruncation(), p::Real=2, alg=SDD()) - u, s, vt, err = tsvd!(adjoint(t); trunc=trunc, p=p, alg=alg) - return adjoint(vt), adjoint(s), adjoint(u), err -end - -# implementation dispatches on algorithm -function _tsvd!(t::TensorMap{<:BlasFloat}, alg::Union{SVD,SDD}, - ::NoTruncation, p::Real=2) - scheduler = default_blockscheduler(t) - svd_alg = alg isa SDD ? LAPACK_DivideAndConquer() : LAPACK_QRIteration() - return MatrixAlgebraKit.svd_compact!(t; alg=BlockAlgorithm(svd_alg, scheduler))..., - zero(real(scalartype(t))) -end -function _tsvd!(t::TensorMap{<:RealOrComplexFloat}, alg::Union{SVD,SDD}, - trunc::TruncationScheme, p::Real=2) - # early return - if isempty(blocksectors(t)) - truncerr = zero(real(scalartype(t))) - return _empty_svdtensors(t)..., truncerr - end - - # compute SVD factorization for each block - S = spacetype(t) - SVDdata, dims = _compute_svddata!(t, alg) - Σdata = SectorDict(c => Σ for (c, (U, Σ, V)) in SVDdata) - truncdim = _compute_truncdim(Σdata, trunc, p) - truncerr = _compute_truncerr(Σdata, truncdim, p) - - # construct output tensors - U, Σ, V⁺ = _create_svdtensors(t, SVDdata, truncdim) - return U, Σ, V⁺, truncerr -end - -# helper functions -function _compute_svddata!(t::TensorMap, alg::Union{SVD,SDD}) - InnerProductStyle(t) === EuclideanInnerProduct() || throw_invalid_innerproduct(:tsvd!) - I = sectortype(t) - dims = SectorDict{I,Int}() - generator = Base.Iterators.map(blocks(t)) do (c, b) - U, Σ, V = MatrixAlgebra.svd!(b, alg) - dims[c] = length(Σ) - return c => (U, Σ, V) - end - SVDdata = SectorDict(generator) - return SVDdata, dims -end - -function _create_svdtensors(t::TensorMap{<:RealOrComplexFloat}, SVDdata, dims) - T = scalartype(t) - S = spacetype(t) - W = S(dims) - - Tr = real(T) - A = similarstoragetype(t, Tr) - Σ = DiagonalTensorMap{Tr,S,A}(undef, W) - - U = similar(t, codomain(t) ← W) - V⁺ = similar(t, W ← domain(t)) - for (c, (Uc, Σc, V⁺c)) in SVDdata - r = Base.OneTo(dims[c]) - copy!(block(U, c), view(Uc, :, r)) - copy!(block(Σ, c), Diagonal(view(Σc, r))) - copy!(block(V⁺, c), view(V⁺c, r, :)) - end - return U, Σ, V⁺ -end - -function _empty_svdtensors(t::TensorMap{<:RealOrComplexFloat}) - T = scalartype(t) - S = spacetype(t) - I = sectortype(t) - dims = SectorDict{I,Int}() - W = S(dims) - - Tr = real(T) - A = similarstoragetype(t, Tr) - Σ = DiagonalTensorMap{Tr,S,A}(undef, W) - - U = similar(t, codomain(t) ← W) - V⁺ = similar(t, W ← domain(t)) - return U, Σ, V⁺ -end - -#--------------------------# -# Eigenvalue decomposition # -#--------------------------# -function LinearAlgebra.eigen!(t::TensorMap{<:RealOrComplexFloat}) - return ishermitian(t) ? eigh!(t) : eig!(t) -end - -function LinearAlgebra.eigvals!(t::TensorMap{<:RealOrComplexFloat}; kwargs...) - return SectorDict(c => complex(LinearAlgebra.eigvals!(b; kwargs...)) - for (c, b) in blocks(t)) -end -function LinearAlgebra.eigvals!(t::AdjointTensorMap{<:RealOrComplexFloat}; kwargs...) - return SectorDict(c => conj!(complex(LinearAlgebra.eigvals!(b; kwargs...))) - for (c, b) in blocks(t)) -end - -eigh!(t::TensorMap{<:RealOrComplexFloat}) = eigh_full!(t) -eig!(t::TensorMap{<:RealOrComplexFloat}) = eig_full!(t) - -# function eigh!(t::TensorMap{<:RealOrComplexFloat}) -# InnerProductStyle(t) === EuclideanInnerProduct() || throw_invalid_innerproduct(:eigh!) -# domain(t) == codomain(t) || -# throw(SpaceMismatch("`eigh!` requires domain and codomain to be the same")) - -# T = scalartype(t) -# I = sectortype(t) -# S = spacetype(t) -# dims = SectorDict{I,Int}(c => size(b, 1) for (c, b) in blocks(t)) -# W = S(dims) - -# Tr = real(T) -# A = similarstoragetype(t, Tr) -# D = DiagonalTensorMap{Tr,S,A}(undef, W) -# V = similar(t, domain(t) ← W) -# for (c, b) in blocks(t) -# values, vectors = MatrixAlgebra.eigh!(b) -# copy!(block(D, c), Diagonal(values)) -# copy!(block(V, c), vectors) -# end -# return D, V -# end - -# function eig!(t::TensorMap{<:RealOrComplexFloat}; kwargs...) -# domain(t) == codomain(t) || -# throw(SpaceMismatch("`eig!` requires domain and codomain to be the same")) - -# T = scalartype(t) -# I = sectortype(t) -# S = spacetype(t) -# dims = SectorDict{I,Int}(c => size(b, 1) for (c, b) in blocks(t)) -# W = S(dims) - -# Tc = complex(T) -# A = similarstoragetype(t, Tc) -# D = DiagonalTensorMap{Tc,S,A}(undef, W) -# V = similar(t, Tc, domain(t) ← W) -# for (c, b) in blocks(t) -# values, vectors = MatrixAlgebra.eig!(b; kwargs...) -# copy!(block(D, c), Diagonal(values)) -# copy!(block(V, c), vectors) -# end -# return D, V -# end - -#--------------------------------------------------# -# Checks for hermiticity and positive definiteness # -#--------------------------------------------------# -function LinearAlgebra.ishermitian(t::TensorMap) - domain(t) == codomain(t) || return false - InnerProductStyle(t) === EuclideanInnerProduct() || return false # hermiticity only defined for euclidean - for (c, b) in blocks(t) - ishermitian(b) || return false - end - return true -end - -function LinearAlgebra.isposdef!(t::TensorMap) - domain(t) == codomain(t) || - throw(SpaceMismatch("`isposdef` requires domain and codomain to be the same")) - InnerProductStyle(spacetype(t)) === EuclideanInnerProduct() || return false - for (c, b) in blocks(t) - isposdef!(b) || return false - end - return true -end - -# TODO: tolerances are per-block, not global or weighted - does that matter? -function isisometry(t::AbstractTensorMap; kwargs...) - domain(t) ≾ codomain(t) || return false - for (_, b) in blocks(t) - MatrixAlgebra.isisometry(b; kwargs...) || return false - end - return true -end diff --git a/src/tensors/factorizations/deprecations.jl b/src/tensors/factorizations/deprecations.jl new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/src/tensors/factorizations/deprecations.jl @@ -0,0 +1 @@ + diff --git a/src/tensors/factorizations/factorizations.jl b/src/tensors/factorizations/factorizations.jl new file mode 100644 index 000000000..a83b77a35 --- /dev/null +++ b/src/tensors/factorizations/factorizations.jl @@ -0,0 +1,215 @@ +# Tensor factorization +#---------------------- +# using submodule here to import MatrixAlgebraKit functions without polluting namespace +module Factorizations + +export eig, eig!, eigh, eigh! +export tsvd, tsvd!, svdvals +export leftorth, leftorth!, rightorth, rightorth! +export leftnull, leftnull!, rightnull, rightnull! +export leftpolar, leftpolar!, rightpolar, rightpolar! +export copy_oftype, permutedcopy_oftype +export TruncationScheme, notrunc, truncbelow, truncerr, truncdim, truncspace + +using ..TensorKit +using ..TensorKit: AdjointTensorMap, SectorDict, OFA, blocktype, foreachblock +using ..MatrixAlgebra: MatrixAlgebra + +using LinearAlgebra: LinearAlgebra, BlasFloat +import LinearAlgebra: eigen, eigen!, isposdef, isposdef!, ishermitian + +using TensorOperations: Index2Tuple + +using MatrixAlgebraKit +using MatrixAlgebraKit: AbstractAlgorithm, TruncatedAlgorithm, TruncationStrategy, + NoTruncation, TruncationKeepAbove, TruncationKeepBelow, + TruncationIntersection, TruncationKeepFiltered +import MatrixAlgebraKit: select_algorithm, + default_qr_algorithm, default_lq_algorithm, + default_eig_algorithm, default_eigh_algorithm, + default_svd_algorithm, default_polar_algorithm, + copy_input, check_input, initialize_output, + qr_compact!, qr_full!, qr_null!, lq_compact!, lq_full!, lq_null!, + svd_compact!, svd_full!, svd_trunc!, + eig_full!, eig_trunc!, eigh_full!, eigh_trunc!, + left_polar!, left_orth_polar!, right_polar!, right_orth_polar!, + left_null_svd!, right_null_svd!, + left_orth!, right_orth!, left_null!, right_null!, + truncate!, findtruncated, findtruncated_sorted, + diagview + +include("utility.jl") +include("interface.jl") +include("implementations.jl") +include("matrixalgebrakit.jl") +include("truncation.jl") +include("deprecations.jl") + +""" + isposdef(t::AbstractTensor, (leftind, rightind)::Index2Tuple) -> ::Bool + +Test whether a tensor `t` is positive definite as linear map from `rightind` to `leftind`. + +If `leftind` and `rightind` are not specified, the current partition of left and right +indices of `t` is used. In that case, less memory is allocated if one allows the data in +`t` to be destroyed/overwritten, by using `isposdef!(t)`. Note that the permuted tensor on +which `isposdef!` is called should have equal domain and codomain, as otherwise it is +meaningless. +""" +function LinearAlgebra.isposdef(t::AbstractTensorMap, (p₁, p₂)::Index2Tuple) + tcopy = permutedcopy_oftype(t, factorisation_scalartype(isposdef, t), p) + return isposdef!(tcopy) +end +function LinearAlgebra.isposdef(t::AbstractTensorMap) + tcopy = copy_oftype(t, float(scalartype(t))) + return isposdef!(tcopy) +end + +function isisometry(t::AbstractTensorMap, (p₁, p₂)::Index2Tuple) + t = permute(t, (p₁, p₂); copy=false) + return isisometry(t) +end + +# Orthogonal factorizations (mutation for recycling memory): +# only possible if scalar type is floating point +# only correct if Euclidean inner product +#------------------------------------------------------------------------------------------ +const RealOrComplexFloat = Union{AbstractFloat,Complex{<:AbstractFloat}} + +# AdjointTensorMap +# ---------------- +function leftorth!(t::AdjointTensorMap; alg::OFA=QRpos()) + InnerProductStyle(t) === EuclideanInnerProduct() || + throw_invalid_innerproduct(:leftorth!) + return map(adjoint, reverse(rightorth!(adjoint(t); alg=alg'))) +end + +function rightorth!(t::AdjointTensorMap; alg::OFA=LQpos()) + InnerProductStyle(t) === EuclideanInnerProduct() || + throw_invalid_innerproduct(:rightorth!) + return map(adjoint, reverse(leftorth!(adjoint(t); alg=alg'))) +end + +function leftnull!(t::AdjointTensorMap; alg::OFA=QR(), kwargs...) + InnerProductStyle(t) === EuclideanInnerProduct() || + throw_invalid_innerproduct(:leftnull!) + return adjoint(rightnull!(adjoint(t); alg=alg', kwargs...)) +end + +function rightnull!(t::AdjointTensorMap; alg::OFA=LQ(), kwargs...) + InnerProductStyle(t) === EuclideanInnerProduct() || + throw_invalid_innerproduct(:rightnull!) + return adjoint(leftnull!(adjoint(t); alg=alg', kwargs...)) +end + +function tsvd!(t::AdjointTensorMap; trunc=NoTruncation(), p::Real=2, alg=SDD()) + u, s, vt, err = tsvd!(adjoint(t); trunc=trunc, p=p, alg=alg) + return adjoint(vt), adjoint(s), adjoint(u), err +end + +# DiagonalTensorMap +# ----------------- +function leftorth!(d::DiagonalTensorMap; alg=QR(), kwargs...) + @assert alg isa Union{QR,QL} + return one(d), d # TODO: this is only correct for `alg = QR()` or `alg = QL()` +end +function rightorth!(d::DiagonalTensorMap; alg=LQ(), kwargs...) + @assert alg isa Union{LQ,RQ} + return d, one(d) # TODO: this is only correct for `alg = LQ()` or `alg = RQ()` +end +leftnull!(d::DiagonalTensorMap; kwargs...) = leftnull!(TensorMap(d); kwargs...) +rightnull!(d::DiagonalTensorMap; kwargs...) = rightnull!(TensorMap(d); kwargs...) + +function tsvd!(d::DiagonalTensorMap; trunc=NoTruncation(), p::Real=2, alg=SDD()) + return _tsvd!(d, alg, trunc, p) +end + +# helper function +function _compute_svddata!(d::DiagonalTensorMap, alg::Union{SVD,SDD}) + InnerProductStyle(d) === EuclideanInnerProduct() || throw_invalid_innerproduct(:tsvd!) + I = sectortype(d) + dims = SectorDict{I,Int}() + generator = Base.Iterators.map(blocks(d)) do (c, b) + lb = length(b.diag) + U = zerovector!(similar(b.diag, lb, lb)) + V = zerovector!(similar(b.diag, lb, lb)) + p = sortperm(b.diag; by=abs, rev=true) + for (i, pi) in enumerate(p) + U[pi, i] = MatrixAlgebra.safesign(b.diag[pi]) + V[i, pi] = 1 + end + Σ = abs.(view(b.diag, p)) + dims[c] = lb + return c => (U, Σ, V) + end + SVDdata = SectorDict(generator) + return SVDdata, dims +end + +eig!(d::DiagonalTensorMap) = d, one(d) +eigh!(d::DiagonalTensorMap{<:Real}) = d, one(d) +eigh!(d::DiagonalTensorMap{<:Complex}) = DiagonalTensorMap(real(d.data), d.domain), one(d) + +function LinearAlgebra.svdvals(d::DiagonalTensorMap) + return SectorDict(c => LinearAlgebra.svdvals(b) for (c, b) in blocks(d)) +end +function LinearAlgebra.eigvals(d::DiagonalTensorMap) + return SectorDict(c => LinearAlgebra.eigvals(b) for (c, b) in blocks(d)) +end + +function LinearAlgebra.cond(d::DiagonalTensorMap, p::Real=2) + return LinearAlgebra.cond(Diagonal(d.data), p) +end +#------------------------------# +# Singular value decomposition # +#------------------------------# +function LinearAlgebra.svdvals!(t::TensorMap{<:RealOrComplexFloat}) + return SectorDict(c => LinearAlgebra.svdvals!(b) for (c, b) in blocks(t)) +end +LinearAlgebra.svdvals!(t::AdjointTensorMap) = svdvals!(adjoint(t)) + +#--------------------------# +# Eigenvalue decomposition # +#--------------------------# + +function LinearAlgebra.eigvals!(t::TensorMap{<:RealOrComplexFloat}; kwargs...) + return SectorDict(c => complex(LinearAlgebra.eigvals!(b; kwargs...)) + for (c, b) in blocks(t)) +end +function LinearAlgebra.eigvals!(t::AdjointTensorMap{<:RealOrComplexFloat}; kwargs...) + return SectorDict(c => conj!(complex(LinearAlgebra.eigvals!(b; kwargs...))) + for (c, b) in blocks(t)) +end + +#--------------------------------------------------# +# Checks for hermiticity and positive definiteness # +#--------------------------------------------------# +function LinearAlgebra.ishermitian(t::TensorMap) + domain(t) == codomain(t) || return false + InnerProductStyle(t) === EuclideanInnerProduct() || return false # hermiticity only defined for euclidean + for (c, b) in blocks(t) + ishermitian(b) || return false + end + return true +end + +function LinearAlgebra.isposdef!(t::TensorMap) + domain(t) == codomain(t) || + throw(SpaceMismatch("`isposdef` requires domain and codomain to be the same")) + InnerProductStyle(spacetype(t)) === EuclideanInnerProduct() || return false + for (c, b) in blocks(t) + isposdef!(b) || return false + end + return true +end + +# TODO: tolerances are per-block, not global or weighted - does that matter? +function isisometry(t::AbstractTensorMap; kwargs...) + domain(t) ≾ codomain(t) || return false + for (_, b) in blocks(t) + MatrixAlgebra.isisometry(b; kwargs...) || return false + end + return true +end + +end diff --git a/src/tensors/factorizations/implementations.jl b/src/tensors/factorizations/implementations.jl new file mode 100644 index 000000000..89c0d0a00 --- /dev/null +++ b/src/tensors/factorizations/implementations.jl @@ -0,0 +1,167 @@ +_kindof(::Union{SVD,SDD}) = :svd +_kindof(::Union{QR,QRpos}) = :qr +_kindof(::Union{LQ,LQpos}) = :lq +_kindof(::Polar) = :polar + +for f! in (:svd_compact!, :svd_full!, :left_null_svd!, :right_null_svd!) + @eval function select_algorithm(::typeof($f!), t::T, alg::SVD; + kwargs...) where {T} + isempty(kwargs) || + throw(ArgumentError("Additional keyword arguments are not allowed")) + return LAPACK_QRIteration() + end + @eval function select_algorithm(::typeof($f!), t::AbstractTensorMap, alg::SVD; + kwargs...) + isempty(kwargs) || + throw(ArgumentError("Additional keyword arguments are not allowed")) + return LAPACK_QRIteration() + end + @eval function select_algorithm(::typeof($f!), ::Type{T}, alg::SVD; + kwargs...) where {T<:AbstractTensorMap} + isempty(kwargs) || + throw(ArgumentError("Additional keyword arguments are not allowed")) + return LAPACK_QRIteration() + end + @eval function select_algorithm(::typeof($f!), t::T, alg::SDD; + kwargs...) where {T} + isempty(kwargs) || + throw(ArgumentError("Additional keyword arguments are not allowed")) + return LAPACK_DivideAndConquer() + end + @eval function select_algorithm(::typeof($f!), t::AbstractTensorMap, alg::SDD; + kwargs...) + isempty(kwargs) || + throw(ArgumentError("Additional keyword arguments are not allowed")) + return LAPACK_DivideAndConquer() + end + @eval function select_algorithm(::typeof($f!), ::Type{T}, alg::SDD; + kwargs...) where {T<:AbstractTensorMap} + isempty(kwargs) || + throw(ArgumentError("Additional keyword arguments are not allowed")) + return LAPACK_DivideAndConquer() + end +end + +leftorth!(t::AbstractTensorMap; alg=nothing, kwargs...) = _leftorth!(t, alg; kwargs...) + +function _leftorth!(t::AbstractTensorMap, ::Nothing; kwargs...) + return isempty(kwargs) ? left_orth!(t) : left_orth!(t; trunc=(; kwargs...)) +end +function _leftorth!(t::AbstractTensorMap, alg::Union{QL,QLpos}; kwargs...) + trunc = isempty(kwargs) ? nothing : (; kwargs...) + + if alg == QL() || alg == QLpos() + _reverse!(t; dims=2) + Q, R = left_orth!(t; kind=:qr, alg_qr=(; positive=alg == QLpos()), trunc) + _reverse!(Q; dims=2) + _reverse!(R) + return Q, R + end +end +function _leftorth!(t, alg::OFA; kwargs...) + trunc = isempty(kwargs) ? nothing : (; kwargs...) + + kind = _kindof(alg) + if kind == :svd + return left_orth!(t; kind, alg_svd=alg, trunc) + elseif kind == :qr + alg_qr = (; positive=(alg == QRpos())) + return left_orth!(t; kind, alg_qr, trunc) + elseif kind == :polar + return left_orth!(t; kind, trunc) + else + throw(ArgumentError(lazy"Invalid algorithm: $alg")) + end +end +# fallback to MatrixAlgebraKit version +_leftorth!(t, alg; kwargs...) = left_orth!(t; alg, kwargs...) + +function leftnull!(t::AbstractTensorMap; + alg::Union{QR,QRpos,SVD,SDD,Nothing}=nothing, kwargs...) + InnerProductStyle(t) === EuclideanInnerProduct() || + throw_invalid_innerproduct(:leftnull!) + trunc = isempty(kwargs) ? nothing : (; kwargs...) + + isnothing(alg) && return left_null!(t; trunc) + + kind = _kindof(alg) + if kind == :svd + return left_null!(t; kind, alg_svd=alg, trunc) + elseif kind == :qr + alg_qr = (; positive=(alg == QRpos())) + return left_null!(t; kind, alg_qr, trunc) + else + throw(ArgumentError(lazy"Invalid `leftnull!` algorithm: $alg")) + end +end + +leftpolar!(t::AbstractTensorMap; kwargs...) = left_polar!(t; kwargs...) + +function rightorth!(t::AbstractTensorMap; + alg::Union{LQ,LQpos,RQ,RQpos,SVD,SDD,Polar,Nothing}=nothing, kwargs...) + InnerProductStyle(t) === EuclideanInnerProduct() || + throw_invalid_innerproduct(:rightorth!) + trunc = isempty(kwargs) ? nothing : (; kwargs...) + + isnothing(alg) && return right_orth!(t; trunc) + + if alg == RQ() || alg == RQpos() + _reverse!(t; dims=1) + L, Q = right_orth!(t; kind=:lq, alg_lq=(; positive=alg == RQpos()), trunc) + _reverse!(Q; dims=1) + _reverse!(L) + return L, Q + end + + kind = _kindof(alg) + if kind == :svd + return right_orth!(t; kind, alg_svd=alg, trunc) + elseif kind == :lq + alg_lq = (; positive=(alg == LQpos())) + return right_orth!(t; kind, alg_lq, trunc) + elseif kind == :polar + return right_orth!(t; kind, trunc) + else + throw(ArgumentError(lazy"Invalid `rightorth!` algorithm: $alg")) + end +end + +function rightnull!(t::AbstractTensorMap; + alg::Union{LQ,LQpos,SVD,SDD,Nothing}=nothing, kwargs...) + InnerProductStyle(t) === EuclideanInnerProduct() || + throw_invalid_innerproduct(:rightnull!) + trunc = isempty(kwargs) ? nothing : (; kwargs...) + + isnothing(alg) && return right_null!(t; trunc) + + kind = _kindof(alg) + if kind == :svd + return right_null!(t; kind, alg_svd=alg, trunc) + elseif kind == :lq + alg_lq = (; positive=(alg == LQpos())) + return right_null!(t; kind, alg_lq, trunc) + else + throw(ArgumentError(lazy"Invalid `rightnull!` algorithm: $alg")) + end +end + +rightpolar!(t::AbstractTensorMap; kwargs...) = right_polar!(t; kwargs...) + +# Eigenvalue decomposition +# ------------------------ +eigh!(t::AbstractTensorMap) = eigh_full!(t) +eig!(t::AbstractTensorMap) = eig_full!(t) +eigen!(t::AbstractTensorMap) = ishermitian(t) ? eigh!(t) : eig!(t) + +# Singular value decomposition +# ---------------------------- +function tsvd!(t::AbstractTensorMap; trunc=notrunc(), p=nothing, kwargs...) + InnerProductStyle(t) === EuclideanInnerProduct() || throw_invalid_innerproduct(:tsvd!) + isnothing(p) || Base.depwarn("p is no longer supported", :tsvd!) + + if trunc == notrunc() + return svd_compact!(t; kwargs...) + else + return svd_trunc!(t; trunc, kwargs...) + end +end diff --git a/src/tensors/factorizations/interface.jl b/src/tensors/factorizations/interface.jl new file mode 100644 index 000000000..821bc15c3 --- /dev/null +++ b/src/tensors/factorizations/interface.jl @@ -0,0 +1,242 @@ +@doc """ + tsvd(t::AbstractTensorMap, [(leftind, rightind)::Index2Tuple]; + trunc::TruncationScheme = notrunc(), p::Real = 2, alg::Union{SVD, SDD} = SDD()) + -> U, S, V, ϵ + tsvd!(t::AbstractTensorMap, trunc::TruncationScheme = notrunc(), p::Real = 2, alg::Union{SVD, SDD} = SDD()) + -> U, S, V, ϵ + +Compute the (possibly truncated) singular value decomposition such that +`norm(permute(t, (leftind, rightind)) - U * S * V) ≈ ϵ`, where `ϵ` thus represents the truncation error. + +If `leftind` and `rightind` are not specified, the current partition of left and right +indices of `t` is used. In that case, less memory is allocated if one allows the data in +`t` to be destroyed/overwritten, by using `tsvd!(t, trunc = notrunc(), p = 2)`. + +A truncation parameter `trunc` can be specified for the new internal dimension, in which +case a truncated singular value decomposition will be computed. Choices are: +* `notrunc()`: no truncation (default); +* `truncerr(η::Real)`: truncates such that the p-norm of the truncated singular values is + smaller than `η`; +* `truncdim(χ::Int)`: truncates such that the equivalent total dimension of the internal + vector space is no larger than `χ`; +* `truncspace(V)`: truncates such that the dimension of the internal vector space is + smaller than that of `V` in any sector. +* `truncbelow(η::Real)`: truncates such that every singular value is larger then `η` ; + +Truncation options can also be combined using `&`, i.e. `truncbelow(η) & truncdim(χ)` will +choose the truncation space such that every singular value is larger than `η`, and the +equivalent total dimension of the internal vector space is no larger than `χ`. + +The method `tsvd` also returns the truncation error `ϵ`, computed as the `p` norm of the +singular values that were truncated. + +THe keyword `alg` can be equal to `SVD()` or `SDD()`, corresponding to the underlying LAPACK +algorithm that computes the decomposition (`_gesvd` or `_gesdd`). + +Orthogonality requires `InnerProductStyle(t) <: HasInnerProduct`, and `tsvd(!)` +is currently only implemented for `InnerProductStyle(t) === EuclideanInnerProduct()`. +""" tsvd, tsvd! + +@doc """ + eig(t::AbstractTensorMap, [(leftind, rightind)::Index2Tuple]; kwargs...) -> D, V + eig!(t::AbstractTensorMap; kwargs...) -> D, V + +Compute eigenvalue factorization of tensor `t` as linear map from `rightind` to `leftind`. +The function `eig` assumes that the linear map is not hermitian and returns type stable +complex valued `D` and `V` tensors for both real and complex valued `t`. See `eigh` for +hermitian linear maps + +If `leftind` and `rightind` are not specified, the current partition of left and right +indices of `t` is used. In that case, less memory is allocated if one allows the data in +`t` to be destroyed/overwritten, by using `eig!(t)`. Note that the permuted tensor on +which `eig!` is called should have equal domain and codomain, as otherwise the eigenvalue +decomposition is meaningless and cannot satisfy +``` +permute(t, (leftind, rightind)) * V = V * D +``` + +Accepts the same keyword arguments `scale` and `permute` as `eigen` of dense +matrices. See the corresponding documentation for more information. + +See also `eigen` and `eigh`. +""" eig + +@doc """ + eigh(t::AbstractTensorMap, [(leftind, rightind)::Index2Tuple]; kwargs...) -> D, V + eigh!(t::AbstractTensorMap; kwargs...) -> D, V + +Compute eigenvalue factorization of tensor `t` as linear map from `rightind` to `leftind`. +The function `eigh` assumes that the linear map is hermitian and `D` and `V` tensors with +the same `scalartype` as `t`. See `eig` and `eigen` for non-hermitian tensors. Hermiticity +requires that the tensor acts on inner product spaces, and the current implementation +requires `InnerProductStyle(t) === EuclideanInnerProduct()`. + +If `leftind` and `rightind` are not specified, the current partition of left and right +indices of `t` is used. In that case, less memory is allocated if one allows the data in +`t` to be destroyed/overwritten, by using `eigh!(t)`. Note that the permuted tensor on +which `eigh!` is called should have equal domain and codomain, as otherwise the eigenvalue +decomposition is meaningless and cannot satisfy +``` +permute(t, (leftind, rightind)) * V = V * D +``` + +See also `eigen` and `eig`. +""" eigh, eigh! + +@doc """ + leftorth(t::AbstractTensorMap, (leftind, rightind)::Index2Tuple; + alg::OrthogonalFactorizationAlgorithm = QRpos()) -> Q, R + +Create orthonormal basis `Q` for indices in `leftind`, and remainder `R` such that +`permute(t, (leftind, rightind)) = Q*R`. + +If `leftind` and `rightind` are not specified, the current partition of left and right +indices of `t` is used. In that case, less memory is allocated if one allows the data in `t` +to be destroyed/overwritten, by using `leftorth!(t, alg = QRpos())`. + +Different algorithms are available, namely `QR()`, `QRpos()`, `SVD()` and `Polar()`. `QR()` +and `QRpos()` use a standard QR decomposition, producing an upper triangular matrix `R`. +`Polar()` produces a Hermitian and positive semidefinite `R`. `QRpos()` corrects the +standard QR decomposition such that the diagonal elements of `R` are positive. Only +`QRpos()` and `Polar()` are unique (no residual freedom) so that they always return the same +result for the same input tensor `t`. + +Orthogonality requires `InnerProductStyle(t) <: HasInnerProduct`, and +`leftorth(!)` is currently only implemented for + `InnerProductStyle(t) === EuclideanInnerProduct()`. +""" leftorth, leftorth! + +@doc """ + rightorth(t::AbstractTensorMap, [(leftind, rightind)::Index2Tuple]; + alg::OrthogonalFactorizationAlgorithm = LQpos()) -> L, Q + rightorth!(t::AbstractTensorMap; alg) -> L, Q + +Create orthonormal basis `Q` for indices in `rightind`, and remainder `L` such that +`permute(t, (leftind, rightind)) = L*Q`. + +If `leftind` and `rightind` are not specified, the current partition of left and right +indices of `t` is used. In that case, less memory is allocated if one allows the data in `t` +to be destroyed/overwritten, by using `rightorth!(t, alg = LQpos())`. + +Different algorithms are available, namely `LQ()`, `LQpos()`, `RQ()`, `RQpos()`, `SVD()` and +`Polar()`. `LQ()` and `LQpos()` produce a lower triangular matrix `L` and are computed using +a QR decomposition of the transpose. `RQ()` and `RQpos()` produce an upper triangular +remainder `L` and only works if the total left dimension is smaller than or equal to the +total right dimension. `LQpos()` and `RQpos()` add an additional correction such that the +diagonal elements of `L` are positive. `Polar()` produces a Hermitian and positive +semidefinite `L`. Only `LQpos()`, `RQpos()` and `Polar()` are unique (no residual freedom) +so that they always return the same result for the same input tensor `t`. + +Orthogonality requires `InnerProductStyle(t) <: HasInnerProduct`, and +`rightorth(!)` is currently only implemented for +`InnerProductStyle(t) === EuclideanInnerProduct()`. +""" rightorth, rightorth! + +@doc """ + leftnull(t::AbstractTensorMap, [(leftind, rightind)::Index2Tuple]; + alg::OrthogonalFactorizationAlgorithm = QRpos()) -> N + leftnull!(t::AbstractTensorMap; alg) -> N + +Create orthonormal basis for the orthogonal complement of the support of the indices in +`leftind`, such that `N' * permute(t, (leftind, rightind)) = 0`. + +If `leftind` and `rightind` are not specified, the current partition of left and right +indices of `t` is used. In that case, less memory is allocated if one allows the data in `t` +to be destroyed/overwritten, by using `leftnull!(t, alg = QRpos())`. + +Different algorithms are available, namely `QR()` (or equivalently, `QRpos()`), `SVD()` and +`SDD()`. The first assumes that the matrix is full rank and requires `iszero(atol)` and +`iszero(rtol)`. With `SVD()` and `SDD()`, `rightnull` will use the corresponding singular +value decomposition, and one can specify an absolute or relative tolerance for which +singular values are to be considered zero, where `max(atol, norm(t)*rtol)` is used as upper +bound. + +Orthogonality requires `InnerProductStyle(t) <: HasInnerProduct`, and +`leftnull(!)` is currently only implemented for +`InnerProductStyle(t) === EuclideanInnerProduct()`. +""" leftnull, leftnull! + +@doc """ + rightnull(t::AbstractTensorMap, [(leftind, rightind)::Index2Tuple]; + alg::OrthogonalFactorizationAlgorithm = LQ(), + atol::Real = 0.0, + rtol::Real = eps(real(float(one(scalartype(t)))))*iszero(atol)) -> N + rightnull!(t::AbstractTensorMap; alg, atol, rtol) + +Create orthonormal basis for the orthogonal complement of the support of the indices in +`rightind`, such that `permute(t, (leftind, rightind))*N' = 0`. + +If `leftind` and `rightind` are not specified, the current partition of left and right +indices of `t` is used. In that case, less memory is allocated if one allows the data in `t` +to be destroyed/overwritten, by using `rightnull!(t, alg = LQpos())`. + +Different algorithms are available, namely `LQ()` (or equivalently, `LQpos`), `SVD()` and +`SDD()`. The first assumes that the matrix is full rank and requires `iszero(atol)` and +`iszero(rtol)`. With `SVD()` and `SDD()`, `rightnull` will use the corresponding singular +value decomposition, and one can specify an absolute or relative tolerance for which +singular values are to be considered zero, where `max(atol, norm(t)*rtol)` is used as upper +bound. + +Orthogonality requires `InnerProductStyle(t) <: HasInnerProduct`, and +`rightnull(!)` is currently only implemented for +`InnerProductStyle(t) === EuclideanInnerProduct()`. +""" rightnull, rightnull! + +@doc """ + leftpolar(t::AbstractTensorMap, [(leftind, rightind)::Index2Tuple]; kwargs...) -> W, P + leftpolar!(t::AbstractTensorMap; kwargs...) -> W, P + +Compute the polar decomposition of tensor `t` as linear map from `rightind` to `leftind`. + +If `leftind` and `rightind` are not specified, the current partition of left and right +indices of `t` is used. In that case, less memory is allocated if one allows the data in +`t` to be destroyed/overwritten, by using `eigh!(t)`. + +See also [`rightpolar(!)`](@ref rightpolar). + +""" leftpolar, leftpolar! + +@doc """ + eigen(t::AbstractTensorMap, [(leftind, rightind)::Index2Tuple]; kwargs...) -> D, V + eigen!(t::AbstractTensorMap; kwargs...) -> D, V + +Compute eigenvalue factorization of tensor `t` as linear map from `rightind` to `leftind`. + +If `leftind` and `rightind` are not specified, the current partition of left and right +indices of `t` is used. In that case, less memory is allocated if one allows the data in `t` +to be destroyed/overwritten, by using `eigen!(t)`. Note that the permuted tensor on which +`eigen!` is called should have equal domain and codomain, as otherwise the eigenvalue +decomposition is meaningless and cannot satisfy +``` +permute(t, (leftind, rightind)) * V = V * D +``` + +Accepts the same keyword arguments `scale` and `permute` as `eigen` of dense +matrices. See the corresponding documentation for more information. + +See also [`eig(!)`](@ref eig) and [`eigh(!)`](@ref) +""" eigen(::AbstractTensorMap), eigen!(::AbstractTensorMap) + +for f in + (:tsvd, :eig, :eigh, :eigen, :leftorth, :rightorth, :leftpolar, :rightpolar, :leftnull, + :rightnull) + f! = Symbol(f, :!) + @eval function $f(t::AbstractTensorMap, p::Index2Tuple; kwargs...) + tcopy = permutedcopy_oftype(t, factorisation_scalartype($f, t), p) + return $f!(tcopy; kwargs...) + end + @eval function $f(t::AbstractTensorMap; kwargs...) + tcopy = copy_oftype(t, factorisation_scalartype($f, t)) + return $f!(tcopy; kwargs...) + end +end + +function LinearAlgebra.eigvals(t::AbstractTensorMap; kwargs...) + tcopy = copy_oftype(t, factorisation_scalartype(eigen, t)) + return LinearAlgebra.eigvals!(tcopy; kwargs...) +end + +function LinearAlgebra.svdvals(t::AbstractTensorMap) + tcopy = copy_oftype(t, factorisation_scalartype(tsvd, t)) + return LinearAlgebra.svdvals!(tcopy) +end diff --git a/src/tensors/factorizations/matrixalgebrakit.jl b/src/tensors/factorizations/matrixalgebrakit.jl new file mode 100644 index 000000000..605428f84 --- /dev/null +++ b/src/tensors/factorizations/matrixalgebrakit.jl @@ -0,0 +1,508 @@ +# Algorithm selection +# ------------------- +for f in (:eig_full, :eig_vals, :eig_trunc, :eigh_full, :eigh_vals, :eigh_trunc, :svd_full, + :svd_compact, :svd_vals, :svd_trunc) + @eval function copy_input(::typeof($f), t::AbstractTensorMap{<:BlasFloat}) + T = factorisation_scalartype($f, t) + return copy_oftype(t, T) + end + f! = Symbol(f, :!) + # TODO: can we move this to MAK? + @eval function select_algorithm(::typeof($f!), t::AbstractTensorMap, alg::Alg=nothing; + kwargs...) where {Alg} + return select_algorithm($f!, typeof(t), alg; kwargs...) + end + @eval function select_algorithm(::typeof($f!), ::Type{T}, alg::Alg=nothing; + kwargs...) where {T<:AbstractTensorMap,Alg} + return select_algorithm($f!, blocktype(T), alg; kwargs...) + end +end + +for f in (:qr, :lq, :svd, :eig, :eigh, :polar) + default_f_algorithm = Symbol(:default_, f, :_algorithm) + @eval function $default_f_algorithm(::Type{T}; kwargs...) where {T<:AbstractTensorMap} + return $default_f_algorithm(blocktype(T); kwargs...) + end +end + +function _select_truncation(f, ::AbstractTensorMap, + trunc::MatrixAlgebraKit.TruncationStrategy) + return trunc +end +function _select_truncation(::typeof(left_null!), ::AbstractTensorMap, trunc::NamedTuple) + return MatrixAlgebraKit.null_truncation_strategy(; trunc...) +end + +# Generic Implementations +# ----------------------_ +for f! in (:qr_compact!, :qr_full!, + :lq_compact!, :lq_full!, + :eig_full!, :eigh_full!, + :svd_compact!, :svd_full!, + :left_polar!, :left_orth_polar!, :right_polar!, :right_orth_polar!) + @eval function $f!(t::AbstractTensorMap, F, alg::AbstractAlgorithm) + check_input($f!, t, F) + + foreachblock(t, F...) do _, bs + factors = Base.tail(bs) + factors′ = $f!(first(bs), factors, alg) + # deal with the case where the output is not in-place + for (f′, f) in zip(factors′, factors) + f′ === f || copyto!(f, f′) + end + return nothing + end + + return F + end +end + +# Handle these separately because single N instead of tuple +for f! in (:qr_null!, :lq_null!) + @eval function $f!(t::AbstractTensorMap, N, alg::AbstractAlgorithm) + check_input($f!, t, N) + + foreachblock(t, N) do _, (b, n) + n′ = $f!(b, n, alg) + # deal with the case where the output is not the same as the input + n === n′ || copyto!(n, n′) + return nothing + end + + return N + end +end + +# Singular value decomposition +# ---------------------------- +const _T_USVᴴ = Tuple{<:AbstractTensorMap,<:AbstractTensorMap,<:AbstractTensorMap} +const _T_USVᴴ_diag = Tuple{<:AbstractTensorMap,<:DiagonalTensorMap,<:AbstractTensorMap} + +function check_input(::typeof(svd_full!), t::AbstractTensorMap, (U, S, Vᴴ)::_T_USVᴴ) + # scalartype checks + @check_scalar U t + @check_scalar S t real + @check_scalar Vᴴ t + + # space checks + V_cod = fuse(codomain(t)) + V_dom = fuse(domain(t)) + @check_space(U, codomain(t) ← V_cod) + @check_space(S, V_cod ← V_dom) + @check_space(Vᴴ, V_dom ← domain(t)) + + return nothing +end + +function check_input(::typeof(svd_compact!), t::AbstractTensorMap, (U, S, Vᴴ)::_T_USVᴴ_diag) + # scalartype checks + @check_scalar U t + @check_scalar S t real + @check_scalar Vᴴ t + + # space checks + V_cod = V_dom = infimum(fuse(codomain(t)), fuse(domain(t))) + @check_space(U, codomain(t) ← V_cod) + @check_space(S, V_cod ← V_dom) + @check_space(Vᴴ, V_dom ← domain(t)) + + return nothing +end + +# TODO: svd_vals + +function initialize_output(::typeof(svd_full!), t::AbstractTensorMap, ::AbstractAlgorithm) + V_cod = fuse(codomain(t)) + V_dom = fuse(domain(t)) + U = similar(t, codomain(t) ← V_cod) + S = similar(t, real(scalartype(t)), V_cod ← V_dom) + Vᴴ = similar(t, V_dom ← domain(t)) + return U, S, Vᴴ +end + +function initialize_output(::typeof(svd_compact!), t::AbstractTensorMap, + ::AbstractAlgorithm) + V_cod = V_dom = infimum(fuse(codomain(t)), fuse(domain(t))) + U = similar(t, codomain(t) ← V_cod) + S = DiagonalTensorMap{real(scalartype(t))}(undef, V_cod) + Vᴴ = similar(t, V_dom ← domain(t)) + return U, S, Vᴴ +end + +function initialize_output(::typeof(svd_trunc!), t::AbstractTensorMap, + alg::TruncatedAlgorithm) + return initialize_output(svd_compact!, t, alg.alg) +end + +# TODO: svd_vals + +function svd_trunc!(t::AbstractTensorMap, USVᴴ, alg::TruncatedAlgorithm) + USVᴴ′ = svd_compact!(t, USVᴴ, alg.alg) + return truncate!(svd_trunc!, USVᴴ′, alg.trunc) +end + +# Eigenvalue decomposition +# ------------------------ +const _T_DV = Tuple{<:DiagonalTensorMap,<:AbstractTensorMap} + +function check_input(::typeof(eigh_full!), t::AbstractTensorMap, (D, V)::_T_DV) + domain(t) == codomain(t) || + throw(ArgumentError("Eigenvalue decomposition requires square input tensor")) + + # scalartype checks + @check_scalar D t real + @check_scalar V t + + # space checks + V_D = fuse(domain(t)) + @check_space(D, V_D ← V_D) + @check_space(V, codomain(t) ← V_D) + + return nothing +end + +function check_input(::typeof(eig_full!), t::AbstractTensorMap, (D, V)::_T_DV) + domain(t) == codomain(t) || + throw(ArgumentError("Eigenvalue decomposition requires square input tensor")) + + # scalartype checks + @check_scalar D t complex + @check_scalar V t complex + + # space checks + V_D = fuse(domain(t)) + @check_space(D, V_D ← V_D) + @check_space(V, codomain(t) ← V_D) + + return nothing +end + +function initialize_output(::typeof(eigh_full!), t::AbstractTensorMap, ::AbstractAlgorithm) + V_D = fuse(domain(t)) + T = real(scalartype(t)) + D = DiagonalTensorMap{T}(undef, V_D) + V = similar(t, codomain(t) ← V_D) + return D, V +end + +function initialize_output(::typeof(eig_full!), t::AbstractTensorMap, ::AbstractAlgorithm) + V_D = fuse(domain(t)) + Tc = complex(scalartype(t)) + D = DiagonalTensorMap{Tc}(undef, V_D) + V = similar(t, Tc, codomain(t) ← V_D) + return D, V +end + +# QR decomposition +# ---------------- +const _T_QR = Tuple{<:AbstractTensorMap,<:AbstractTensorMap} + +function check_input(::typeof(qr_full!), t::AbstractTensorMap, (Q, R)::_T_QR) + # scalartype checks + @check_scalar Q t + @check_scalar R t + + # space checks + V_Q = fuse(codomain(t)) + @check_space(Q, codomain(t) ← V_Q) + @check_space(R, V_Q ← domain(t)) + + return nothing +end + +function check_input(::typeof(qr_compact!), t::AbstractTensorMap, (Q, R)::_T_QR) + # scalartype checks + @check_scalar Q t + @check_scalar R t + + # space checks + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + @check_space(Q, codomain(t) ← V_Q) + @check_space(R, V_Q ← domain(t)) + + return nothing +end + +function check_input(::typeof(qr_null!), t::AbstractTensorMap, N::AbstractTensorMap) + # scalartype checks + @check_scalar N t + + # space checks + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + V_N = ⊖(fuse(codomain(t)), V_Q) + @check_space(N, codomain(t) ← V_N) + + return nothing +end + +function initialize_output(::typeof(qr_full!), t::AbstractTensorMap, ::AbstractAlgorithm) + V_Q = fuse(codomain(t)) + Q = similar(t, codomain(t) ← V_Q) + R = similar(t, V_Q ← domain(t)) + return Q, R +end + +function initialize_output(::typeof(qr_compact!), t::AbstractTensorMap, ::AbstractAlgorithm) + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + Q = similar(t, codomain(t) ← V_Q) + R = similar(t, V_Q ← domain(t)) + return Q, R +end + +function initialize_output(::typeof(qr_null!), t::AbstractTensorMap, ::AbstractAlgorithm) + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + V_N = ⊖(fuse(codomain(t)), V_Q) + N = similar(t, codomain(t) ← V_N) + return N +end + +# LQ decomposition +# ---------------- +const _T_LQ = Tuple{<:AbstractTensorMap,<:AbstractTensorMap} + +function check_input(::typeof(lq_full!), t::AbstractTensorMap, (L, Q)::_T_LQ) + # scalartype checks + @check_scalar L t + @check_scalar Q t + + # space checks + V_Q = fuse(domain(t)) + @check_space(L, codomain(t) ← V_Q) + @check_space(Q, V_Q ← domain(t)) + + return nothing +end + +function check_input(::typeof(lq_compact!), t::AbstractTensorMap, (L, Q)::_T_LQ) + # scalartype checks + @check_scalar L t + @check_scalar Q t + + # space checks + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + @check_space(L, codomain(t) ← V_Q) + @check_space(Q, V_Q ← domain(t)) + + return nothing +end + +function check_input(::typeof(lq_null!), t::AbstractTensorMap, N) + # scalartype checks + @check_scalar N t + + # space checks + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + V_N = ⊖(fuse(domain(t)), V_Q) + @check_space(N, V_N ← domain(t)) + + return nothing +end + +function initialize_output(::typeof(lq_full!), t::AbstractTensorMap, ::AbstractAlgorithm) + V_Q = fuse(domain(t)) + L = similar(t, codomain(t) ← V_Q) + Q = similar(t, V_Q ← domain(t)) + return L, Q +end + +function initialize_output(::typeof(lq_compact!), t::AbstractTensorMap, ::AbstractAlgorithm) + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + L = similar(t, codomain(t) ← V_Q) + Q = similar(t, V_Q ← domain(t)) + return L, Q +end + +function initialize_output(::typeof(lq_null!), t::AbstractTensorMap, ::AbstractAlgorithm) + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + V_N = ⊖(fuse(domain(t)), V_Q) + N = similar(t, V_N ← domain(t)) + return N +end + +# Polar decomposition +# ------------------- +const _T_WP = Tuple{<:AbstractTensorMap,<:AbstractTensorMap} +const _T_PWᴴ = Tuple{<:AbstractTensorMap,<:AbstractTensorMap} +using MatrixAlgebraKit: PolarViaSVD + +function check_input(::typeof(left_polar!), t::AbstractTensorMap, (W, P)::_T_WP) + codomain(t) ≿ domain(t) || + throw(ArgumentError("Polar decomposition requires `codomain(t) ≿ domain(t)`")) + + # scalartype checks + @check_scalar W t + @check_scalar P t + + # space checks + @check_space(W, space(t)) + @check_space(P, domain(t) ← domain(t)) + + return nothing +end + +function check_input(::typeof(left_orth_polar!), t::AbstractTensorMap, (W, P)::_T_WP) + codomain(t) ≿ domain(t) || + throw(ArgumentError("Polar decomposition requires `codomain(t) ≿ domain(t)`")) + + # scalartype checks + @check_scalar W t + @check_scalar P t + + # space checks + VW = fuse(domain(t)) + @check_space(W, codomain(t) ← VW) + @check_space(P, VW ← domain(t)) + + return nothing +end + +function initialize_output(::typeof(left_polar!), t::AbstractTensorMap, ::AbstractAlgorithm) + W = similar(t, space(t)) + P = similar(t, domain(t) ← domain(t)) + return W, P +end + +function check_input(::typeof(right_polar!), t::AbstractTensorMap, (P, Wᴴ)::_T_PWᴴ) + domain(t) ≿ codomain(t) || + throw(ArgumentError("Polar decomposition requires `domain(t) ≿ codomain(t)`")) + + # scalartype checks + @check_scalar P t + @check_scalar Wᴴ t + + # space checks + @check_space(P, codomain(t) ← codomain(t)) + @check_space(Wᴴ, space(t)) + + return nothing +end + +function check_input(::typeof(right_orth_polar!), t::AbstractTensorMap, (P, Wᴴ)::_T_PWᴴ) + domain(t) ≿ codomain(t) || + throw(ArgumentError("Polar decomposition requires `domain(t) ≿ codomain(t)`")) + + # scalartype checks + @check_scalar P t + @check_scalar Wᴴ t + + # space checks + VW = fuse(codomain(t)) + @check_space(P, codomain(t) ← VW) + @check_space(Wᴴ, VW ← domain(t)) + + return nothing +end + +function initialize_output(::typeof(right_polar!), t::AbstractTensorMap, + ::AbstractAlgorithm) + P = similar(t, codomain(t) ← codomain(t)) + Wᴴ = similar(t, space(t)) + return Wᴴ, P +end + +# Needed to get algorithm selection to behave +function left_orth_polar!(t::AbstractTensorMap, VC, alg) + alg′ = select_algorithm(left_polar!, t, alg) + return left_orth_polar!(t, VC, alg′) +end +function right_orth_polar!(t::AbstractTensorMap, CVᴴ, alg) + alg′ = select_algorithm(right_polar!, t, alg) + return right_orth_polar!(t, CVᴴ, alg′) +end + +# Orthogonalization +# ----------------- +const _T_VC = Tuple{<:AbstractTensorMap,<:AbstractTensorMap} +const _T_CVᴴ = Tuple{<:AbstractTensorMap,<:AbstractTensorMap} + +function check_input(::typeof(left_orth!), t::AbstractTensorMap, (V, C)::_T_VC) + # scalartype checks + @check_scalar V t + isnothing(C) || @check_scalar C t + + # space checks + V_C = infimum(fuse(codomain(t)), fuse(domain(t))) + @check_space(V, codomain(t) ← V_C) + isnothing(C) || @check_space(C, V_C ← domain(t)) + + return nothing +end + +function check_input(::typeof(right_orth!), t::AbstractTensorMap, (C, Vᴴ)::_T_CVᴴ) + # scalartype checks + isnothing(C) || @check_scalar C t + @check_scalar Vᴴ t + + # space checks + V_C = infimum(fuse(codomain(t)), fuse(domain(t))) + isnothing(C) || @check_space(C, codomain(t) ← V_C) + @check_space(Vᴴ, V_C ← domain(t)) + + return nothing +end + +function initialize_output(::typeof(left_orth!), t::AbstractTensorMap) + V_C = infimum(fuse(codomain(t)), fuse(domain(t))) + V = similar(t, codomain(t) ← V_C) + C = similar(t, V_C ← domain(t)) + return V, C +end + +function initialize_output(::typeof(right_orth!), t::AbstractTensorMap) + V_C = infimum(fuse(codomain(t)), fuse(domain(t))) + C = similar(t, codomain(t) ← V_C) + Vᴴ = similar(t, V_C ← domain(t)) + return C, Vᴴ +end + +# Nullspace +# --------- +function check_input(::typeof(left_null!), t::AbstractTensorMap, N) + # scalartype checks + @check_scalar N t + + # space checks + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + V_N = ⊖(fuse(codomain(t)), V_Q) + @check_space(N, codomain(t) ← V_N) + + return nothing +end + +function check_input(::typeof(right_null!), t::AbstractTensorMap, N) + @check_scalar N t + + # space checks + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + V_N = ⊖(fuse(domain(t)), V_Q) + @check_space(N, V_N ← domain(t)) + + return nothing +end + +function initialize_output(::typeof(left_null!), t::AbstractTensorMap) + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + V_N = ⊖(fuse(codomain(t)), V_Q) + N = similar(t, codomain(t) ← V_N) + return N +end + +function initialize_output(::typeof(right_null!), t::AbstractTensorMap) + V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) + V_N = ⊖(fuse(domain(t)), V_Q) + N = similar(t, V_N ← domain(t)) + return N +end + +for f! in (:left_null_svd!, :right_null_svd!) + @eval function $f!(t::AbstractTensorMap, N, alg, ::Nothing=nothing) + foreachblock(t, N) do _, (b, n) + n′ = $f!(b, n, alg) + # deal with the case where the output is not the same as the input + n === n′ || copyto!(n, n′) + return nothing + end + + return N + end +end diff --git a/src/tensors/factorizations/truncation.jl b/src/tensors/factorizations/truncation.jl new file mode 100644 index 000000000..e9806645a --- /dev/null +++ b/src/tensors/factorizations/truncation.jl @@ -0,0 +1,270 @@ +# truncation.jl +# +# Implements truncation schemes for truncating a tensor with svd, leftorth or rightorth + +notrunc() = NoTruncation() +# deprecate +const TruncationScheme = TruncationStrategy + +struct TruncationError{T<:Real} <: TruncationStrategy + ϵ::T + p::Real +end +truncerr(epsilon::Real, p::Real=2) = TruncationError(epsilon, p) + +# struct TruncationDimension <: TruncationScheme +# dim::Int +# end +@deprecate truncdim(d::Int) truncrank(d) + +struct TruncationSpace{S<:ElementarySpace} <: TruncationStrategy + space::S +end +truncspace(space::ElementarySpace) = TruncationSpace(space) + +struct TruncationCutoff{T<:Real} <: TruncationStrategy + ϵ::T + add_back::Int +end +@deprecate truncbelow(ϵ::Real, add_back::Int=0) begin + add_back == 0 || @warn "add_back is ignored" + trunctol(ϵ) +end +# truncbelow(epsilon::Real, add_back::Int=0) = TruncationCutoff(epsilon, add_back) + +# Compute the total truncation error given truncation dimensions +function _compute_truncerr(Σdata, truncdim, p=2) + I = keytype(Σdata) + S = scalartype(valtype(Σdata)) + return TensorKit._norm((c => @view(v[(get(truncdim, c, 0) + 1):end]) + for (c, v) in Σdata), + p, zero(S)) +end + +# Compute truncation dimensions +# function _compute_truncdim(Σdata, ::NoTruncation, p=2) +# I = keytype(Σdata) +# truncdim = SectorDict{I,Int}(c => length(v) for (c, v) in Σdata) +# return truncdim +# end +# function _compute_truncdim(Σdata, trunc::TruncationDimension, p=2) +# I = keytype(Σdata) +# truncdim = SectorDict{I,Int}(c => length(v) for (c, v) in Σdata) +# while sum(dim(c) * d for (c, d) in truncdim) > trunc.dim +# cmin = _findnexttruncvalue(Σdata, truncdim, p) +# isnothing(cmin) && break +# truncdim[cmin] -= 1 +# end +# return truncdim +# end +function _compute_truncdim(Σdata, trunc::TruncationSpace, p=2) + I = keytype(Σdata) + truncdim = SectorDict{I,Int}(c => min(length(v), dim(trunc.space, c)) + for (c, v) in Σdata) + return truncdim +end + +# function _compute_truncdim(Σdata, trunc::TruncationCutoff, p=2) +# I = keytype(Σdata) +# truncdim = SectorDict{I,Int}(c => length(v) for (c, v) in Σdata) +# for (c, v) in Σdata +# newdim = findlast(Base.Fix2(>, trunc.ϵ), v) +# if newdim === nothing +# truncdim[c] = 0 +# else +# truncdim[c] = newdim +# end +# end +# for i in 1:(trunc.add_back) +# cmax = _findnextgrowvalue(Σdata, truncdim, p) +# isnothing(cmax) && break +# truncdim[cmax] += 1 +# end +# return truncdim +# end + +# Combine truncations +# struct MultipleTruncation{T<:Tuple{Vararg{TruncationScheme}}} <: TruncationScheme +# truncations::T +# end +# function Base.:&(a::MultipleTruncation, b::MultipleTruncation) +# return MultipleTruncation((a.truncations..., b.truncations...)) +# end +# function Base.:&(a::MultipleTruncation, b::TruncationScheme) +# return MultipleTruncation((a.truncations..., b)) +# end +# function Base.:&(a::TruncationScheme, b::MultipleTruncation) +# return MultipleTruncation((a, b.truncations...)) +# end +# Base.:&(a::TruncationScheme, b::TruncationScheme) = MultipleTruncation((a, b)) + +# function _compute_truncdim(Σdata, trunc::MultipleTruncation, p::Real=2) +# truncdim = _compute_truncdim(Σdata, trunc.truncations[1], p) +# for k in 2:length(trunc.truncations) +# truncdimₖ = _compute_truncdim(Σdata, trunc.truncations[k], p) +# for (c, d) in truncdim +# truncdim[c] = min(d, truncdimₖ[c]) +# end +# end +# return truncdim +# end + +# auxiliary function +function _findnexttruncvalue(S, truncdim::SectorDict{I,Int}) where {I<:Sector} + # early return + (isempty(S) || all(iszero, values(truncdim))) && return nothing + σmin, imin = findmin(keys(truncdim)) do c + d = truncdim[c] + return S[c][d] + end + return σmin, keys(truncdim)[imin] +end + +function _findnextgrowvalue(Σdata, truncdim::SectorDict{I,Int}, p::Real) where {I<:Sector} + istruncated = SectorDict{I,Bool}(c => (d < length(Σdata[c])) for (c, d) in truncdim) + # early return + (isempty(Σdata) || !any(values(istruncated))) && return nothing + + # find some suitable starting candidate + cmax = findfirst(istruncated) + σmax = Σdata[cmax][truncdim[cmax] + 1] + + # find the actual maximal singular value + for (c, σs) in Σdata + if istruncated[c] + σ = σs[truncdim[c] + 1] + if σ > σmax + cmax, σmax = c, σ + end + end + end + return cmax +end + +# Truncation +# ---------- + +function truncate!(::typeof(svd_trunc!), (U, S, Vᴴ)::_T_USVᴴ, strategy::TruncationStrategy) + ind = findtruncated_sorted(diagview(S), strategy) + V_truncated = spacetype(S)(c => length(I) for (c, I) in ind) + + Ũ = similar(U, codomain(U) ← V_truncated) + for (c, b) in blocks(Ũ) + I = get(ind, c, nothing) + @assert !isnothing(I) + copy!(b, @view(block(U, c)[:, I])) + end + + S̃ = DiagonalTensorMap{scalartype(S)}(undef, V_truncated) + for (c, b) in blocks(S̃) + I = get(ind, c, nothing) + @assert !isnothing(I) + copy!(b.diag, @view(block(S, c).diag[I])) + end + + Ṽᴴ = similar(Vᴴ, V_truncated ← domain(Vᴴ)) + for (c, b) in blocks(Ṽᴴ) + I = get(ind, c, nothing) + @assert !isnothing(I) + copy!(b, @view(block(Vᴴ, c)[I, :])) + end + + return Ũ, S̃, Ṽᴴ +end + +function findtruncated_sorted(S::SectorDict, strategy::TruncationKeepAbove) + atol = if strategy.rtol > 0 + max(strategy.atol, _norm(S, strategy.p) * strategy.rtol) + else + strategy.atol + end + findtrunc = Base.Fix2(findtruncated_sorted, truncbelow(atol)) + return SectorDict(c => findtrunc(d) for (c, d) in Sd) +end + +function findtruncated_sorted(S::SectorDict, strategy::TruncationKeepBelow) + atol = if strategy.rtol > 0 + max(strategy.atol, _norm(S, strategy.p) * strategy.rtol) + else + strategy.atol + end + findtrunc = Base.Fix2(findtruncated_sorted, truncabove(atol)) + return SectorDict(c => findtrunc(d) for (c, d) in Sd) +end + +function findtruncated_sorted(Sd::SectorDict, strategy::TruncationError) + I = keytype(Sd) + S = real(scalartype(valtype(Sd))) + truncdim = SectorDict{I,Int}(c => length(d) for (c, d) in Sd) + while true + next = _findnexttruncvalue(Sd, truncdim) + isnothing(next) && break + σmin, cmin = next + truncdim[cmin] -= 1 + err = _compute_truncerr(Sd, truncdim, strategy.p) + if err > strategy.ϵ + truncdim[cmin] += 1 + break + end + if truncdim[cmin] == 0 + delete!(truncdim, cmin) + end + end + return SectorDict{I,Base.OneTo{Int}}(c => Base.OneTo(d) for (c, d) in truncdim) +end + +function findtruncated_sorted(Sd::SectorDict, strategy::TruncationKeepSorted) + @assert strategy.by === abs && strategy.rev == true "Not implemented" + I = keytype(Sd) + S = real(scalartype(valtype(Sd))) + truncdim = SectorDict{I,Int}(c => length(d) for (c, d) in Sd) + totaldim = sum(dim(c) * d for (c, d) in truncdim; init=0) + while true + next = _findnexttruncvalue(Sd, truncdim) + isnothing(next) && break + _, cmin = next + truncdim[cmin] -= 1 + totaldim -= dim(cmin) + if totaldim < strategy.howmany + truncdim[cmin] += 1 + break + end + if truncdim[cmin] == 0 + delete!(truncdim, cmin) + end + end + return SectorDict{I,Base.OneTo{Int}}(c => Base.OneTo(d) for (c, d) in truncdim) +end + +function findtruncated_sorted(Sd::SectorDict, strategy::TruncationSpace) + I = keytype(Sd) + return SectorDict{I,Base.OneTo{Int}}(c => Base.OneTo(min(length(d), + dim(strategy.space, c))) + for (c, d) in Sd) +end + +function findtruncated_sorted(Sd::SectorDict, strategy::TruncationKeepFiltered) + return SectorDict(c => findtruncated_sorted(d, strategy) for (c, d) in Sd) +end + +function findtruncated_sorted(Sd::SectorDict, strategy::TruncationIntersection) + inds = map(Base.Fix1(findtruncated_sorted, Sd), strategy) + return SectorDict(c => intersect(map(Base.Fix2(getindex, c), inds)...) + for c in intersect(map(keys, inds)...)) +end + +function MatrixAlgebraKit.truncate!(::typeof(left_null!), + (U, S)::Tuple{<:AbstractTensorMap, + <:AbstractTensorMap}, + strategy::MatrixAlgebraKit.TruncationStrategy) + extended_S = SectorDict(c => vcat(MatrixAlgebraKit.diagview(b), + zeros(eltype(b), max(0, size(b, 2) - size(b, 1)))) + for (c, b) in blocks(S)) + ind = MatrixAlgebraKit.findtruncated(extended_S, strategy) + V_truncated = spacetype(S)(c => length(axes(b, 1)[ind[c]]) for (c, b) in blocks(S)) + Ũ = similar(U, codomain(U) ← V_truncated) + for (c, b) in blocks(Ũ) + copy!(b, @view(block(U, c)[:, ind[c]])) + end + return Ũ +end diff --git a/src/tensors/factorizations/utility.jl b/src/tensors/factorizations/utility.jl new file mode 100644 index 000000000..e23fb8b73 --- /dev/null +++ b/src/tensors/factorizations/utility.jl @@ -0,0 +1,29 @@ +# convenience to set default +macro check_space(x, V) + return esc(:($MatrixAlgebraKit.@check_size($x, $V, $space))) +end +macro check_scalar(x, y, op=:identity, eltype=:scalartype) + return esc(:($MatrixAlgebraKit.@check_scalar($x, $y, $op, $eltype))) +end + +function factorisation_scalartype(t::AbstractTensorMap) + T = scalartype(t) + return promote_type(Float32, typeof(zero(T) / sqrt(abs2(one(T))))) +end +factorisation_scalartype(f, t) = factorisation_scalartype(t) + +function permutedcopy_oftype(t::AbstractTensorMap, T::Type{<:Number}, p::Index2Tuple) + return permute!(similar(t, T, permute(space(t), p)), t, p) +end +function copy_oftype(t::AbstractTensorMap, T::Type{<:Number}) + return copy!(similar(t, T), t) +end + +function _reverse!(t::AbstractTensorMap; dims=:) + for (c, b) in blocks(t) + reverse!(b; dims) + end + return t +end + +diagview(t::AbstractTensorMap) = SectorDict(c => diagview(b) for (c, b) in blocks(t)) diff --git a/src/tensors/matrixalgebrakit.jl b/src/tensors/matrixalgebrakit.jl deleted file mode 100644 index 4481267df..000000000 --- a/src/tensors/matrixalgebrakit.jl +++ /dev/null @@ -1,715 +0,0 @@ -# convenience to set default -macro check_space(x, V) - return esc(:($MatrixAlgebraKit.@check_size($x, $V, $space))) -end -macro check_scalar(x, y, op=:identity, eltype=:scalartype) - return esc(:($MatrixAlgebraKit.@check_scalar($x, $y, $op, $eltype))) -end - -# Generic -# ------- -for f in (:eig_full, :eig_vals, :eig_trunc, :eigh_full, :eigh_vals, :eigh_trunc, :svd_full, - :svd_compact, :svd_vals, :svd_trunc) - @eval function MatrixAlgebraKit.copy_input(::typeof($f), - t::AbstractTensorMap{<:BlasFloat}) - T = factorisation_scalartype($f, t) - return copy_oftype(t, T) - end - f! = Symbol(f, :!) - @eval function MatrixAlgebraKit.select_algorithm(::typeof($f!), t::AbstractTensorMap, - alg::Alg=nothing; - kwargs...) where {Alg} - return MatrixAlgebraKit.select_algorithm($f!, typeof(t), alg; kwargs...) - end - @eval function MatrixAlgebraKit.select_algorithm(::typeof($f!), ::Type{T}, - alg::Alg=nothing; - scheduler=default_blockscheduler(T), - kwargs...) where {T<:AbstractTensorMap, - Alg} - mat_alg = MatrixAlgebraKit.select_algorithm($f!, blocktype(T), alg; kwargs...) - return BlockAlgorithm(mat_alg, scheduler) - end -end - -for f in (:qr, :lq, :svd, :eig, :eigh, :polar) - default_f_algorithm = Symbol(:default_, f, :_algorithm) - @eval function MatrixAlgebraKit.$default_f_algorithm(::Type{T}; - scheduler=default_blockscheduler(T), - kwargs...) where {T<:AbstractTensorMap} - return BlockAlgorithm(MatrixAlgebraKit.$default_f_algorithm(blocktype(T); - kwargs...), - scheduler) - end -end - -# TODO: move to MatrixAlgebraKit? -macro check_eltype(x, y, f=:identity, g=:eltype) - msg = "unexpected scalar type: " - msg *= string(g) * "(" * string(x) * ") != " - if f == :identity - msg *= string(g) * "(" * string(y) * ")" - else - msg *= string(f) * "(" * string(y) * ")" - end - return esc(:($g($x) == $f($g($y)) || throw(ArgumentError($msg)))) -end - -function _select_truncation(f, ::AbstractTensorMap, - trunc::MatrixAlgebraKit.TruncationStrategy) - return trunc -end -function _select_truncation(::typeof(left_null!), ::AbstractTensorMap, trunc::NamedTuple) - return MatrixAlgebraKit.null_truncation_strategy(; trunc...) -end - -function MatrixAlgebraKit.diagview(t::AbstractTensorMap) - return SectorDict(c => MatrixAlgebraKit.diagview(b) for (c, b) in blocks(t)) -end - -# Singular value decomposition -# ---------------------------- -const _T_USVᴴ = Tuple{<:AbstractTensorMap,<:AbstractTensorMap,<:AbstractTensorMap} -const _T_USVᴴ_diag = Tuple{<:AbstractTensorMap,<:DiagonalTensorMap,<:AbstractTensorMap} - -function MatrixAlgebraKit.check_input(::typeof(svd_full!), t::AbstractTensorMap, - (U, S, Vᴴ)::_T_USVᴴ) - # scalartype checks - @check_scalar U t - @check_scalar S t real - @check_scalar Vᴴ t - - # space checks - V_cod = fuse(codomain(t)) - V_dom = fuse(domain(t)) - @check_space(U, codomain(t) ← V_cod) - @check_space(S, V_cod ← V_dom) - @check_space(Vᴴ, V_dom ← domain(t)) - - return nothing -end - -function MatrixAlgebraKit.check_input(::typeof(svd_compact!), t::AbstractTensorMap, - (U, S, Vᴴ)::_T_USVᴴ_diag) - # scalartype checks - @check_eltype U t - @check_eltype S t real - @check_eltype Vᴴ t - - # space checks - V_cod = V_dom = infimum(fuse(codomain(t)), fuse(domain(t))) - @check_space(U, codomain(t) ← V_cod) - @check_space(S, V_cod ← V_dom) - @check_space(Vᴴ, V_dom ← domain(t)) - - return nothing -end - -# TODO: svd_vals - -function MatrixAlgebraKit.initialize_output(::typeof(svd_full!), t::AbstractTensorMap, - ::MatrixAlgebraKit.AbstractAlgorithm) - V_cod = fuse(codomain(t)) - V_dom = fuse(domain(t)) - U = similar(t, codomain(t) ← V_cod) - S = similar(t, real(scalartype(t)), V_cod ← V_dom) - Vᴴ = similar(t, V_dom ← domain(t)) - return U, S, Vᴴ -end - -function MatrixAlgebraKit.initialize_output(::typeof(svd_compact!), t::AbstractTensorMap, - ::MatrixAlgebraKit.AbstractAlgorithm) - V_cod = V_dom = infimum(fuse(codomain(t)), fuse(domain(t))) - U = similar(t, codomain(t) ← V_cod) - S = DiagonalTensorMap{real(scalartype(t))}(undef, V_cod) - Vᴴ = similar(t, V_dom ← domain(t)) - return U, S, Vᴴ -end - -function MatrixAlgebraKit.initialize_output(::typeof(svd_trunc!), t::AbstractTensorMap, - alg::MatrixAlgebraKit.AbstractAlgorithm) - return MatrixAlgebraKit.initialize_output(svd_compact!, t, alg) -end - -# TODO: svd_vals - -function MatrixAlgebraKit.svd_full!(t::AbstractTensorMap, (U, S, Vᴴ), - alg::BlockAlgorithm) - MatrixAlgebraKit.check_input(svd_full!, t, (U, S, Vᴴ)) - - foreachblock(t, U, S, Vᴴ; alg.scheduler) do _, (b, u, s, vᴴ) - if isempty(b) # TODO: remove once MatrixAlgebraKit supports empty matrices - MatrixAlgebraKit.one!(length(u) > 0 ? u : vᴴ) - zerovector!(s) - else - u′, s′, vᴴ′ = MatrixAlgebraKit.svd_full!(b, (u, s, vᴴ), alg.alg) - # deal with the case where the output is not the same as the input - u === u′ || copyto!(u, u′) - s === s′ || copyto!(s, s′) - vᴴ === vᴴ′ || copyto!(vᴴ, vᴴ′) - end - return nothing - end - - return U, S, Vᴴ -end - -function MatrixAlgebraKit.svd_compact!(t::AbstractTensorMap, (U, S, Vᴴ), - alg::BlockAlgorithm) - MatrixAlgebraKit.check_input(svd_compact!, t, (U, S, Vᴴ)) - - foreachblock(t, U, S, Vᴴ; alg.scheduler) do _, (b, u, s, vᴴ) - u′, s′, vᴴ′ = svd_compact!(b, (u, s, vᴴ), alg.alg) - # deal with the case where the output is not the same as the input - u === u′ || copyto!(u, u′) - s === s′ || copyto!(s, s′) - vᴴ === vᴴ′ || copyto!(vᴴ, vᴴ′) - return nothing - end - - return U, S, Vᴴ -end - -function MatrixAlgebraKit.svd_trunc!(t::AbstractTensorMap, USVᴴ, - alg::MatrixAlgebraKit.TruncatedAlgorithm) - USVᴴ′ = svd_compact!(t, USVᴴ, alg.alg) - return MatrixAlgebraKit.truncate!(svd_trunc!, USVᴴ′, alg.trunc) -end - -# Eigenvalue decomposition -# ------------------------ -const _T_DV = Tuple{<:DiagonalTensorMap,<:AbstractTensorMap} -function MatrixAlgebraKit.check_input(::typeof(eigh_full!), t::AbstractTensorMap, - (D, V)::_T_DV) - domain(t) == codomain(t) || - throw(ArgumentError("Eigenvalue decomposition requires square input tensor")) - - # scalartype checks - @check_scalar D t real - @check_scalar V t - - # space checks - V_D = fuse(domain(t)) - @check_space(D, V_D ← V_D) - @check_space(V, codomain(t) ← V_D) - - return nothing -end - -function MatrixAlgebraKit.check_input(::typeof(eig_full!), t::AbstractTensorMap, - (D, V)::_T_DV) - domain(t) == codomain(t) || - throw(ArgumentError("Eigenvalue decomposition requires square input tensor")) - - # scalartype checks - @check_scalar D t complex - @check_scalar V t complex - - # space checks - V_D = fuse(domain(t)) - @check_space(D, V_D ← V_D) - @check_space(V, codomain(t) ← V_D) - - return nothing -end - -function MatrixAlgebraKit.initialize_output(::typeof(eigh_full!), t::AbstractTensorMap, - ::MatrixAlgebraKit.AbstractAlgorithm) - V_D = fuse(domain(t)) - T = real(scalartype(t)) - D = DiagonalTensorMap{T}(undef, V_D) - V = similar(t, codomain(t) ← V_D) - return D, V -end - -function MatrixAlgebraKit.initialize_output(::typeof(eig_full!), t::AbstractTensorMap, - ::MatrixAlgebraKit.AbstractAlgorithm) - V_D = fuse(domain(t)) - Tc = complex(scalartype(t)) - D = DiagonalTensorMap{Tc}(undef, V_D) - V = similar(t, Tc, codomain(t) ← V_D) - return D, V -end - -for f in (:eigh_full!, :eig_full!) - @eval function MatrixAlgebraKit.$f(t::AbstractTensorMap, (D, V), - alg::BlockAlgorithm) - MatrixAlgebraKit.check_input($f, t, (D, V)) - - foreachblock(t, D, V; alg.scheduler) do _, (b, d, v) - d′, v′ = $f(b, (d, v), alg.alg) - # deal with the case where the output is not the same as the input - d === d′ || copyto!(d, d′) - v === v′ || copyto!(v, v′) - return nothing - end - - return D, V - end -end - -# QR decomposition -# ---------------- -function MatrixAlgebraKit.check_input(::typeof(qr_full!), t::AbstractTensorMap, - (Q, - R)::Tuple{<:AbstractTensorMap,<:AbstractTensorMap}) - # scalartype checks - @check_scalar Q t - @check_scalar R t - - # space checks - V_Q = fuse(codomain(t)) - @check_space(Q, codomain(t) ← V_Q) - @check_space(R, V_Q ← domain(t)) - - return nothing -end - -function MatrixAlgebraKit.check_input(::typeof(qr_compact!), t::AbstractTensorMap, (Q, R)) - # scalartype checks - @check_scalar Q t - @check_scalar R t - - # space checks - V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) - @check_space(Q, codomain(t) ← V_Q) - @check_space(R, V_Q ← domain(t)) - - return nothing -end - -function MatrixAlgebraKit.check_input(::typeof(qr_null!), t::AbstractTensorMap, - N::AbstractTensorMap) - # scalartype checks - @check_scalar N t - - # space checks - V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) - V_N = ⊖(fuse(codomain(t)), V_Q) - @check_space(N, codomain(t) ← V_N) - - return nothing -end - -function MatrixAlgebraKit.initialize_output(::typeof(qr_full!), t::AbstractTensorMap, - ::MatrixAlgebraKit.AbstractAlgorithm) - V_Q = fuse(codomain(t)) - Q = similar(t, codomain(t) ← V_Q) - R = similar(t, V_Q ← domain(t)) - return Q, R -end - -function MatrixAlgebraKit.initialize_output(::typeof(qr_compact!), t::AbstractTensorMap, - ::MatrixAlgebraKit.AbstractAlgorithm) - V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) - Q = similar(t, codomain(t) ← V_Q) - R = similar(t, V_Q ← domain(t)) - return Q, R -end - -function MatrixAlgebraKit.initialize_output(::typeof(qr_null!), t::AbstractTensorMap, - ::MatrixAlgebraKit.AbstractAlgorithm) - V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) - V_N = ⊖(fuse(codomain(t)), V_Q) - N = similar(t, codomain(t) ← V_N) - return N -end - -function MatrixAlgebraKit.qr_full!(t::AbstractTensorMap, (Q, R), - alg::BlockAlgorithm) - MatrixAlgebraKit.check_input(qr_full!, t, (Q, R)) - - foreachblock(t, Q, R; alg.scheduler) do _, (b, q, r) - q′, r′ = qr_full!(b, (q, r), alg.alg) - # deal with the case where the output is not the same as the input - q === q′ || copyto!(q, q′) - r === r′ || copyto!(r, r′) - return nothing - end - - return Q, R -end - -function MatrixAlgebraKit.qr_compact!(t::AbstractTensorMap, (Q, R), - alg::BlockAlgorithm) - MatrixAlgebraKit.check_input(qr_compact!, t, (Q, R)) - - foreachblock(t, Q, R; alg.scheduler) do _, (b, q, r) - q′, r′ = qr_compact!(b, (q, r), alg.alg) - # deal with the case where the output is not the same as the input - q === q′ || copyto!(q, q′) - r === r′ || copyto!(r, r′) - return nothing - end - - return Q, R -end - -function MatrixAlgebraKit.qr_null!(t::AbstractTensorMap, N, alg::BlockAlgorithm) - MatrixAlgebraKit.check_input(qr_null!, t, N) - - foreachblock(t, N; alg.scheduler) do _, (b, n) - n′ = qr_null!(b, n, alg.alg) - # deal with the case where the output is not the same as the input - n === n′ || copyto!(n, n′) - return nothing - end - - return N -end - - -# LQ decomposition -# ---------------- -function MatrixAlgebraKit.check_input(::typeof(lq_full!), t::AbstractTensorMap, (L, Q)) - # scalartype checks - @check_eltype L t - @check_eltype Q t - - # space checks - V_Q = fuse(domain(t)) - space(L) == (codomain(t) ← V_Q) || - throw(SpaceMismatch("`lq_full!(t, (L, Q))` requires `space(L) == (codomain(t) ← fuse(domain(t)))`")) - space(Q) == (V_Q ← domain(t)) || - throw(SpaceMismatch("`lq_full!(t, (L, Q))` requires `space(Q) == (fuse(domain(t)) ← domain(t))`")) - - return nothing -end - -function MatrixAlgebraKit.check_input(::typeof(lq_compact!), t::AbstractTensorMap, (L, Q)) - # scalartype checks - @check_scalar L t - @check_scalar Q t - - # space checks - V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) - @check_space(L, codomain(t) ← V_Q) - @check_space(Q, V_Q ← domain(t)) - - return nothing -end - -function MatrixAlgebraKit.check_input(::typeof(lq_null!), t::AbstractTensorMap, N) - # scalartype checks - @check_scalar N t - - # space checks - V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) - V_N = ⊖(fuse(domain(t)), V_Q) - @check_space(N, V_N ← domain(t)) - - return nothing -end - -function MatrixAlgebraKit.initialize_output(::typeof(lq_full!), t::AbstractTensorMap, - ::MatrixAlgebraKit.AbstractAlgorithm) - V_Q = fuse(domain(t)) - L = similar(t, codomain(t) ← V_Q) - Q = similar(t, V_Q ← domain(t)) - return L, Q -end - -function MatrixAlgebraKit.initialize_output(::typeof(lq_compact!), t::AbstractTensorMap, - ::MatrixAlgebraKit.AbstractAlgorithm) - V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) - L = similar(t, codomain(t) ← V_Q) - Q = similar(t, V_Q ← domain(t)) - return L, Q -end - -function MatrixAlgebraKit.initialize_output(::typeof(lq_null!), t::AbstractTensorMap, - ::MatrixAlgebraKit.AbstractAlgorithm) - V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) - V_N = ⊖(fuse(domain(t)), V_Q) - N = similar(t, V_N ← domain(t)) - return N -end - -function MatrixAlgebraKit.lq_full!(t::AbstractTensorMap, (L, Q), - alg::BlockAlgorithm) - MatrixAlgebraKit.check_input(lq_full!, t, (L, Q)) - - foreachblock(t, L, Q; alg.scheduler) do _, (b, l, q) - l′, q′ = lq_full!(b, (l, q), alg.alg) - # deal with the case where the output is not the same as the input - l === l′ || copyto!(l, l′) - q === q′ || copyto!(q, q′) - return nothing - end - - return L, Q -end - -function MatrixAlgebraKit.lq_compact!(t::AbstractTensorMap, (L, Q), - alg::BlockAlgorithm) - MatrixAlgebraKit.check_input(lq_compact!, t, (L, Q)) - - foreachblock(t, L, Q; alg.scheduler) do _, (b, l, q) - l′, q′ = lq_compact!(b, (l, q), alg.alg) - # deal with the case where the output is not the same as the input - l === l′ || copyto!(l, l′) - q === q′ || copyto!(q, q′) - return nothing - end - - return L, Q -end - -function MatrixAlgebraKit.lq_null!(t::AbstractTensorMap, N, alg::BlockAlgorithm) - MatrixAlgebraKit.check_input(lq_null!, t, N) - - foreachblock(t, N; alg.scheduler) do _, (b, n) - n′ = lq_null!(b, n, alg.alg) - # deal with the case where the output is not the same as the input - n === n′ || copyto!(n, n′) - return nothing - end - - return N -end - -# Polar decomposition -# ------------------- -using MatrixAlgebraKit: PolarViaSVD - -function MatrixAlgebraKit.check_input(::typeof(left_polar!), t, (W, P)) - codomain(t) ≿ domain(t) || - throw(ArgumentError("Polar decomposition requires `codomain(t) ≿ domain(t)`")) - - # scalartype checks - @check_scalar W t - @check_scalar P t - - # space checks - @check_space(W, space(t)) - @check_space(P, domain(t) ← domain(t)) - - return nothing -end - -# TODO: do we really not want to fuse the spaces? -function MatrixAlgebraKit.initialize_output(::typeof(left_polar!), t::AbstractTensorMap, - ::BlockAlgorithm) - W = similar(t, space(t)) - P = similar(t, domain(t) ← domain(t)) - return W, P -end - -function MatrixAlgebraKit.left_polar!(t::AbstractTensorMap, WP, alg::BlockAlgorithm) - MatrixAlgebraKit.check_input(left_polar!, t, WP) - - foreachblock(t, WP...; alg.scheduler) do _, (b, w, p) - w′, p′ = left_polar!(b, (w, p), alg.alg) - # deal with the case where the output is not the same as the input - w === w′ || copyto!(w, w′) - p === p′ || copyto!(p, p′) - return nothing - end - - return WP -end - -# Trick to relax the checks of "square" if coming from left_orth -function MatrixAlgebraKit.left_orth_polar!(t::AbstractTensorMap, VC, alg) - alg′ = MatrixAlgebraKit.select_algorithm(left_polar!, t, alg) - return MatrixAlgebraKit.left_orth_polar!(t, VC, alg′) -end -function MatrixAlgebraKit.left_orth_polar!(t::AbstractTensorMap, WP, alg::BlockAlgorithm) - foreachblock(t, WP...; alg.scheduler) do _, (b, w, p) - w′, p′ = left_polar!(b, (w, p), alg.alg) - # deal with the case where the output is not the same as the input - w === w′ || copyto!(w, w′) - p === p′ || copyto!(p, p′) - return nothing - end - return WP -end - -# Orthogonalization -# ----------------- -function MatrixAlgebraKit.check_input(::typeof(left_orth!), t::AbstractTensorMap, (V, C)) - # scalartype checks - @check_scalar V t - isnothing(C) || @check_scalar C t - - # space checks - V_C = infimum(fuse(codomain(t)), fuse(domain(t))) - @check_space(V, codomain(t) ← V_C) - isnothing(C) || @check_space(CV_C ← domain(t)) - - return nothing -end - -function MatrixAlgebraKit.check_input(::typeof(right_orth!), t::AbstractTensorMap, (C, Vᴴ)) - # scalartype checks - isnothing(C) || @check_scalar C t - @check_scalar Vᴴ t - - # space checks - V_C = infimum(fuse(codomain(t)), fuse(domain(t))) - isnothing(C) || @check_space(C, codomain(t) ← V_C) - @check_space(Vᴴ, V_dom ← domain(t)) - - return nothing -end - -function MatrixAlgebraKit.initialize_output(::typeof(left_orth!), t::AbstractTensorMap) - V_C = infimum(fuse(codomain(t)), fuse(domain(t))) - V = similar(t, codomain(t) ← V_C) - C = similar(t, V_C ← domain(t)) - return V, C -end - -function MatrixAlgebraKit.initialize_output(::typeof(right_orth!), t::AbstractTensorMap) - V_C = infimum(fuse(codomain(t)), fuse(domain(t))) - C = similar(t, codomain(t) ← V_C) - Vᴴ = similar(t, V_C ← domain(t)) - return C, Vᴴ -end - -# Nullspace -# --------- -function MatrixAlgebraKit.check_input(::typeof(left_null!), t::AbstractTensorMap, N) - # scalartype checks - @check_scalar N t - - # space checks - V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) - V_N = ⊖(fuse(codomain(t)), V_Q) - @check_space(N, codomain(t) ← V_N) - - return nothing -end - -function MatrixAlgebraKit.initialize_output(::typeof(left_null!), t::AbstractTensorMap) - V_Q = infimum(fuse(codomain(t)), fuse(domain(t))) - V_N = ⊖(fuse(codomain(t)), V_Q) - N = similar(t, codomain(t) ← V_N) - return N -end - -# TODO: the following functions shouldn't be necessary if the AbstractArray restrictions are -# removed -function MatrixAlgebraKit.left_null(t::AbstractTensorMap; kwargs...) - return left_null!(MatrixAlgebraKit.copy_input(left_null, t); kwargs...) -end -function MatrixAlgebraKit.left_null!(t::AbstractTensorMap; kwargs...) - N = MatrixAlgebraKit.initialize_output(left_null!, t) - return left_null!(t, N; kwargs...) -end - -function MatrixAlgebraKit.left_null!(t::AbstractTensorMap, N; - trunc=nothing, - kind=isnothing(trunc) ? :qr : :svd, - alg_qr=(; positive=true), - alg_svd=(;)) - MatrixAlgebraKit.check_input(left_null!, t, N) - - if !isnothing(trunc) && kind != :svd - throw(ArgumentError("truncation not supported for left_null with kind=$kind")) - end - - if kind == :qr - @info "qr" - alg_qr′ = MatrixAlgebraKit._select_algorithm(qr_null!, t, alg_qr) - return qr_null!(t, N, alg_qr′) - elseif kind == :svd && isnothing(trunc) - @info "svd" - alg_svd′ = MatrixAlgebraKit._select_algorithm(svd_full!, t, alg_svd) - # TODO: refactor into separate function - U, _, _ = svd_full!(t, alg_svd′) - for (c, b) in blocks(N) - bU = block(U, c) - m, n = size(bU) - copy!(b, @view(bU[1:m, (n + 1):m])) - end - return N - elseif kind == :svd - @info "svd2" - alg_svd′ = MatrixAlgebraKit._select_algorithm(svd_full!, t, alg_svd) - @show U, S, _ = svd_full!(t, alg_svd′) - @show trunc′ = _select_truncation(left_null!, t, @show trunc) - return MatrixAlgebraKit.truncate!(left_null!, (U, S), trunc′) - else - throw(ArgumentError("`left_null!` received unknown value `kind = $kind`")) - end -end - -# Truncation -# ---------- -# TODO: technically we could do this truncation in-place, but this might not be worth it -function MatrixAlgebraKit.truncate!(::typeof(svd_trunc!), (U, S, Vᴴ), - trunc::MatrixAlgebraKit.TruncationKeepAbove) - atol = max(trunc.atol, norm(S) * trunc.rtol) - V_truncated = spacetype(S)(c => findlast(>=(atol), b.diag) for (c, b) in blocks(S)) - - Ũ = similar(U, codomain(U) ← V_truncated) - for (c, b) in blocks(Ũ) - copy!(b, @view(block(U, c)[:, 1:size(b, 2)])) - end - - S̃ = DiagonalTensorMap{scalartype(S)}(undef, V_truncated) - for (c, b) in blocks(S̃) - copy!(b.diag, @view(block(S, c).diag[1:size(b, 1)])) - end - - Ṽᴴ = similar(Vᴴ, V_truncated ← domain(Vᴴ)) - for (c, b) in blocks(Ṽᴴ) - copy!(b, @view(block(Vᴴ, c)[1:size(b, 1), :])) - end - - return Ũ, S̃, Ṽᴴ -end - -function MatrixAlgebraKit.truncate!(::typeof(left_null!), - (U, S)::Tuple{<:AbstractTensorMap, - <:AbstractTensorMap}, - strategy::MatrixAlgebraKit.TruncationStrategy) - extended_S = SectorDict(c => vcat(MatrixAlgebraKit.diagview(b), - zeros(eltype(b), max(0, size(b, 2) - size(b, 1)))) - for (c, b) in blocks(S)) - ind = MatrixAlgebraKit.findtruncated(extended_S, strategy) - V_truncated = spacetype(S)(c => length(axes(b, 1)[ind[c]]) for (c, b) in blocks(S)) - Ũ = similar(U, codomain(U) ← V_truncated) - for (c, b) in blocks(Ũ) - copy!(b, @view(block(U, c)[:, ind[c]])) - end - return Ũ -end - -const BlockWiseTruncations = Union{MatrixAlgebraKit.TruncationKeepAbove, - MatrixAlgebraKit.TruncationKeepBelow, - MatrixAlgebraKit.TruncationKeepFiltered} - -# TODO: relative tolerances should be global -function MatrixAlgebraKit.findtruncated(values::SectorDict, strategy::BlockWiseTruncations) - return SectorDict(c => MatrixAlgebraKit.findtruncated(v, strategy) for (c, v) in values) -end -function MatrixAlgebraKit.findtruncated(vals::SectorDict, - strategy::MatrixAlgebraKit.TruncationKeepSorted) - allpairs = mapreduce(vcat, vals) do (c, v) - return map(Base.Fix1(=>, c), axes(v, 1)) - end - by((c, i)) = strategy.sortby(vals[c][i]) - sort!(allpairs; by, strategy.rev) - - howmany = zero(Base.promote_op(dim, valtype(values))) - i = 1 - while i ≤ length(allpairs) - howmany += dim(first(allpairs[i])) - - howmany == strategy.howmany && break - - if howmany > strategy.howmany - i -= 1 - break - end - - i += 1 - end - - ind = SectorDict(c => allpairs[findall(==(c) ∘ first, view(allpairs, 1:i))] - for c in keys(vals)) - filter!(!isempty ∘ last, ind) # TODO: this might not be necessary - - return ind -end diff --git a/src/tensors/truncation.jl b/src/tensors/truncation.jl deleted file mode 100644 index e49cdc94d..000000000 --- a/src/tensors/truncation.jl +++ /dev/null @@ -1,163 +0,0 @@ -# truncation.jl -# -# Implements truncation schemes for truncating a tensor with svd, leftorth or rightorth -abstract type TruncationScheme end - -struct NoTruncation <: TruncationScheme -end -notrunc() = NoTruncation() - -struct TruncationError{T<:Real} <: TruncationScheme - ϵ::T -end -truncerr(epsilon::Real) = TruncationError(epsilon) - -struct TruncationDimension <: TruncationScheme - dim::Int -end -truncdim(d::Int) = TruncationDimension(d) - -struct TruncationSpace{S<:ElementarySpace} <: TruncationScheme - space::S -end -truncspace(space::ElementarySpace) = TruncationSpace(space) - -struct TruncationCutoff{T<:Real} <: TruncationScheme - ϵ::T - add_back::Int -end -truncbelow(epsilon::Real, add_back::Int=0) = TruncationCutoff(epsilon, add_back) - -# Compute the total truncation error given truncation dimensions -function _compute_truncerr(Σdata, truncdim, p=2) - I = keytype(Σdata) - S = scalartype(valtype(Σdata)) - return _norm((c => view(v, (truncdim[c] + 1):length(v)) for (c, v) in Σdata), p, - zero(S)) -end - -# Compute truncation dimensions -function _compute_truncdim(Σdata, ::NoTruncation, p=2) - I = keytype(Σdata) - truncdim = SectorDict{I,Int}(c => length(v) for (c, v) in Σdata) - return truncdim -end -function _compute_truncdim(Σdata, trunc::TruncationError, p=2) - I = keytype(Σdata) - S = real(eltype(valtype(Σdata))) - truncdim = SectorDict{I,Int}(c => length(Σc) for (c, Σc) in Σdata) - truncerr = zero(S) - while true - cmin = _findnexttruncvalue(Σdata, truncdim, p) - isnothing(cmin) && break - truncdim[cmin] -= 1 - truncerr = _compute_truncerr(Σdata, truncdim, p) - if truncerr > trunc.ϵ - truncdim[cmin] += 1 - break - end - end - return truncdim -end -function _compute_truncdim(Σdata, trunc::TruncationDimension, p=2) - I = keytype(Σdata) - truncdim = SectorDict{I,Int}(c => length(v) for (c, v) in Σdata) - while sum(dim(c) * d for (c, d) in truncdim) > trunc.dim - cmin = _findnexttruncvalue(Σdata, truncdim, p) - isnothing(cmin) && break - truncdim[cmin] -= 1 - end - return truncdim -end -function _compute_truncdim(Σdata, trunc::TruncationSpace, p=2) - I = keytype(Σdata) - truncdim = SectorDict{I,Int}(c => min(length(v), dim(trunc.space, c)) - for (c, v) in Σdata) - return truncdim -end - -function _compute_truncdim(Σdata, trunc::TruncationCutoff, p=2) - I = keytype(Σdata) - truncdim = SectorDict{I,Int}(c => length(v) for (c, v) in Σdata) - for (c, v) in Σdata - newdim = findlast(Base.Fix2(>, trunc.ϵ), v) - if newdim === nothing - truncdim[c] = 0 - else - truncdim[c] = newdim - end - end - for i in 1:(trunc.add_back) - cmax = _findnextgrowvalue(Σdata, truncdim, p) - isnothing(cmax) && break - truncdim[cmax] += 1 - end - return truncdim -end - -# Combine truncations -struct MultipleTruncation{T<:Tuple{Vararg{TruncationScheme}}} <: TruncationScheme - truncations::T -end -function Base.:&(a::MultipleTruncation, b::MultipleTruncation) - return MultipleTruncation((a.truncations..., b.truncations...)) -end -function Base.:&(a::MultipleTruncation, b::TruncationScheme) - return MultipleTruncation((a.truncations..., b)) -end -function Base.:&(a::TruncationScheme, b::MultipleTruncation) - return MultipleTruncation((a, b.truncations...)) -end -Base.:&(a::TruncationScheme, b::TruncationScheme) = MultipleTruncation((a, b)) - -function _compute_truncdim(Σdata, trunc::MultipleTruncation, p::Real=2) - truncdim = _compute_truncdim(Σdata, trunc.truncations[1], p) - for k in 2:length(trunc.truncations) - truncdimₖ = _compute_truncdim(Σdata, trunc.truncations[k], p) - for (c, d) in truncdim - truncdim[c] = min(d, truncdimₖ[c]) - end - end - return truncdim -end - -# auxiliary function -function _findnexttruncvalue(Σdata, truncdim::SectorDict{I,Int}, p::Real) where {I<:Sector} - # early return - (isempty(Σdata) || all(iszero, values(truncdim))) && return nothing - - # find some suitable starting candidate - cmin = findfirst(>(0), truncdim) - σmin = Σdata[cmin][truncdim[cmin]] - - # find the actual minimum singular value - for (c, σs) in Σdata - if truncdim[c] > 0 - σ = σs[truncdim[c]] - if σ < σmin - cmin, σmin = c, σ - end - end - end - return cmin -end -function _findnextgrowvalue(Σdata, truncdim::SectorDict{I,Int}, p::Real) where {I<:Sector} - istruncated = SectorDict{I,Bool}(c => (d < length(Σdata[c])) for (c, d) in truncdim) - # early return - (isempty(Σdata) || !any(values(istruncated))) && return nothing - - # find some suitable starting candidate - cmax = findfirst(istruncated) - σmax = Σdata[cmax][truncdim[cmax] + 1] - - # find the actual maximal singular value - for (c, σs) in Σdata - if istruncated[c] - σ = σs[truncdim[c] + 1] - if σ > σmax - cmax, σmax = c, σ - end - end - end - return cmax -end diff --git a/test/factorizations.jl b/test/factorizations.jl index ca9b510fb..17642aa93 100644 --- a/test/factorizations.jl +++ b/test/factorizations.jl @@ -1,318 +1,340 @@ using TestEnv; TestEnv.activate(); -using Test -using TestExtras -using Random -using TensorKit -using Combinatorics -using TensorKit: ProductSector, fusiontensor, pentagon_equation, hexagon_equation -using TensorOperations -using Base.Iterators: take, product -# using SUNRepresentations: SUNIrrep -# const SU3Irrep = SUNIrrep{3} -using LinearAlgebra: LinearAlgebra -using Zygote: Zygote -using MatrixAlgebraKit +@testsnippet Setup begin + using Test + using TestExtras + using Random + using TensorKit + using Combinatorics + using TensorKit: ProductSector, fusiontensor, pentagon_equation, hexagon_equation + using TensorOperations + using Base.Iterators: take, product + # using SUNRepresentations: SUNIrrep + # const SU3Irrep = SUNIrrep{3} + using LinearAlgebra: LinearAlgebra + using Zygote: Zygote + using MatrixAlgebraKit -const TK = TensorKit + const TK = TensorKit -Random.seed!(1234) + Random.seed!(1234) -smallset(::Type{I}) where {I<:Sector} = take(values(I), 5) -function smallset(::Type{ProductSector{Tuple{I1,I2}}}) where {I1,I2} - iter = product(smallset(I1), smallset(I2)) - s = collect(i ⊠ j for (i, j) in iter if dim(i) * dim(j) <= 6) - return length(s) > 6 ? rand(s, 6) : s -end -function smallset(::Type{ProductSector{Tuple{I1,I2,I3}}}) where {I1,I2,I3} - iter = product(smallset(I1), smallset(I2), smallset(I3)) - s = collect(i ⊠ j ⊠ k for (i, j, k) in iter if dim(i) * dim(j) * dim(k) <= 6) - return length(s) > 6 ? rand(s, 6) : s -end -function randsector(::Type{I}) where {I<:Sector} - s = collect(smallset(I)) - a = rand(s) - while a == one(a) # don't use trivial label + smallset(::Type{I}) where {I<:Sector} = take(values(I), 5) + function smallset(::Type{ProductSector{Tuple{I1,I2}}}) where {I1,I2} + iter = product(smallset(I1), smallset(I2)) + s = collect(i ⊠ j for (i, j) in iter if dim(i) * dim(j) <= 6) + return length(s) > 6 ? rand(s, 6) : s + end + function smallset(::Type{ProductSector{Tuple{I1,I2,I3}}}) where {I1,I2,I3} + iter = product(smallset(I1), smallset(I2), smallset(I3)) + s = collect(i ⊠ j ⊠ k for (i, j, k) in iter if dim(i) * dim(j) * dim(k) <= 6) + return length(s) > 6 ? rand(s, 6) : s + end + function randsector(::Type{I}) where {I<:Sector} + s = collect(smallset(I)) a = rand(s) + while a == one(a) # don't use trivial label + a = rand(s) + end + return a end - return a -end -function hasfusiontensor(I::Type{<:Sector}) - try - fusiontensor(one(I), one(I), one(I)) - return true - catch e - if e isa MethodError - return false - else - rethrow(e) + function hasfusiontensor(I::Type{<:Sector}) + try + fusiontensor(one(I), one(I), one(I)) + return true + catch e + if e isa MethodError + return false + else + rethrow(e) + end end end -end -# spaces -Vtr = (ℂ^3, - (ℂ^4)', - ℂ^5, - ℂ^6, - (ℂ^7)') -Vℤ₂ = (ℂ[Z2Irrep](0 => 1, 1 => 1), - ℂ[Z2Irrep](0 => 1, 1 => 2)', - ℂ[Z2Irrep](0 => 3, 1 => 2)', - ℂ[Z2Irrep](0 => 2, 1 => 3), - ℂ[Z2Irrep](0 => 2, 1 => 5)) -Vfℤ₂ = (ℂ[FermionParity](0 => 1, 1 => 1), - ℂ[FermionParity](0 => 1, 1 => 2)', - ℂ[FermionParity](0 => 3, 1 => 2)', - ℂ[FermionParity](0 => 2, 1 => 3), - ℂ[FermionParity](0 => 2, 1 => 5)) -Vℤ₃ = (ℂ[Z3Irrep](0 => 1, 1 => 2, 2 => 2), - ℂ[Z3Irrep](0 => 3, 1 => 1, 2 => 1), - ℂ[Z3Irrep](0 => 2, 1 => 2, 2 => 1)', - ℂ[Z3Irrep](0 => 1, 1 => 2, 2 => 3), - ℂ[Z3Irrep](0 => 1, 1 => 3, 2 => 3)') -VU₁ = (ℂ[U1Irrep](0 => 1, 1 => 2, -1 => 2), - ℂ[U1Irrep](0 => 3, 1 => 1, -1 => 1), - ℂ[U1Irrep](0 => 2, 1 => 2, -1 => 1)', - ℂ[U1Irrep](0 => 1, 1 => 2, -1 => 3), - ℂ[U1Irrep](0 => 1, 1 => 3, -1 => 3)') -VfU₁ = (ℂ[FermionNumber](0 => 1, 1 => 2, -1 => 2), - ℂ[FermionNumber](0 => 3, 1 => 1, -1 => 1), - ℂ[FermionNumber](0 => 2, 1 => 2, -1 => 1)', - ℂ[FermionNumber](0 => 1, 1 => 2, -1 => 3), - ℂ[FermionNumber](0 => 1, 1 => 3, -1 => 3)') -VCU₁ = (ℂ[CU1Irrep]((0, 0) => 1, (0, 1) => 2, 1 => 1), - ℂ[CU1Irrep]((0, 0) => 3, (0, 1) => 0, 1 => 1), - ℂ[CU1Irrep]((0, 0) => 1, (0, 1) => 0, 1 => 2)', - ℂ[CU1Irrep]((0, 0) => 2, (0, 1) => 2, 1 => 1), - ℂ[CU1Irrep]((0, 0) => 2, (0, 1) => 1, 1 => 2)') -VSU₂ = (ℂ[SU2Irrep](0 => 3, 1 // 2 => 1), - ℂ[SU2Irrep](0 => 2, 1 => 1), - ℂ[SU2Irrep](1 // 2 => 1, 1 => 1)', - ℂ[SU2Irrep](0 => 2, 1 // 2 => 2), - ℂ[SU2Irrep](0 => 1, 1 // 2 => 1, 3 // 2 => 1)') -VfSU₂ = (ℂ[FermionSpin](0 => 3, 1 // 2 => 1), - ℂ[FermionSpin](0 => 2, 1 => 1), - ℂ[FermionSpin](1 // 2 => 1, 1 => 1)', - ℂ[FermionSpin](0 => 2, 1 // 2 => 2), - ℂ[FermionSpin](0 => 1, 1 // 2 => 1, 3 // 2 => 1)') -for V in (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂)#, VSU₃) - V1, V2, V3, V4, V5 = V + # spaces + Vtr = (ℂ^3, + (ℂ^4)', + ℂ^5, + ℂ^6, + (ℂ^7)') + Vℤ₂ = (ℂ[Z2Irrep](0 => 1, 1 => 1), + ℂ[Z2Irrep](0 => 1, 1 => 2)', + ℂ[Z2Irrep](0 => 3, 1 => 2)', + ℂ[Z2Irrep](0 => 2, 1 => 3), + ℂ[Z2Irrep](0 => 2, 1 => 5)) + Vfℤ₂ = (ℂ[FermionParity](0 => 1, 1 => 1), + ℂ[FermionParity](0 => 1, 1 => 2)', + ℂ[FermionParity](0 => 3, 1 => 2)', + ℂ[FermionParity](0 => 2, 1 => 3), + ℂ[FermionParity](0 => 2, 1 => 5)) + Vℤ₃ = (ℂ[Z3Irrep](0 => 1, 1 => 2, 2 => 2), + ℂ[Z3Irrep](0 => 3, 1 => 1, 2 => 1), + ℂ[Z3Irrep](0 => 2, 1 => 2, 2 => 1)', + ℂ[Z3Irrep](0 => 1, 1 => 2, 2 => 3), + ℂ[Z3Irrep](0 => 1, 1 => 3, 2 => 3)') + VU₁ = (ℂ[U1Irrep](0 => 1, 1 => 2, -1 => 2), + ℂ[U1Irrep](0 => 3, 1 => 1, -1 => 1), + ℂ[U1Irrep](0 => 2, 1 => 2, -1 => 1)', + ℂ[U1Irrep](0 => 1, 1 => 2, -1 => 3), + ℂ[U1Irrep](0 => 1, 1 => 3, -1 => 3)') + VfU₁ = (ℂ[FermionNumber](0 => 1, 1 => 2, -1 => 2), + ℂ[FermionNumber](0 => 3, 1 => 1, -1 => 1), + ℂ[FermionNumber](0 => 2, 1 => 2, -1 => 1)', + ℂ[FermionNumber](0 => 1, 1 => 2, -1 => 3), + ℂ[FermionNumber](0 => 1, 1 => 3, -1 => 3)') + VCU₁ = (ℂ[CU1Irrep]((0, 0) => 1, (0, 1) => 2, 1 => 1), + ℂ[CU1Irrep]((0, 0) => 3, (0, 1) => 0, 1 => 1), + ℂ[CU1Irrep]((0, 0) => 1, (0, 1) => 0, 1 => 2)', + ℂ[CU1Irrep]((0, 0) => 2, (0, 1) => 2, 1 => 1), + ℂ[CU1Irrep]((0, 0) => 2, (0, 1) => 1, 1 => 2)') + VSU₂ = (ℂ[SU2Irrep](0 => 3, 1 // 2 => 1), + ℂ[SU2Irrep](0 => 2, 1 => 1), + ℂ[SU2Irrep](1 // 2 => 1, 1 => 1)', + ℂ[SU2Irrep](0 => 2, 1 // 2 => 2), + ℂ[SU2Irrep](0 => 1, 1 // 2 => 1, 3 // 2 => 1)') + VfSU₂ = (ℂ[FermionSpin](0 => 3, 1 // 2 => 1), + ℂ[FermionSpin](0 => 2, 1 => 1), + ℂ[FermionSpin](1 // 2 => 1, 1 => 1)', + ℂ[FermionSpin](0 => 2, 1 // 2 => 2), + ℂ[FermionSpin](0 => 1, 1 // 2 => 1, 3 // 2 => 1)') + for V in (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂)#, VSU₃) + V1, V2, V3, V4, V5 = V - @assert V3 * V4 * V2 ≿ V1' * V5' # necessary for leftorth tests - @assert V3 * V4 ≾ V1' * V2' * V5' # necessary for rightorth tests -end + @assert V3 * V4 * V2 ≿ V1' * V5' # necessary for leftorth tests + @assert V3 * V4 ≾ V1' * V2' * V5' # necessary for rightorth tests + end -spacelist = try - if ENV["CI"] == "true" - println("Detected running on CI") - if Sys.iswindows() - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂) - elseif Sys.isapple() - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VfU₁, VfSU₂)#, VSU₃) + spacelist = try + if ENV["CI"] == "true" + println("Detected running on CI") + if Sys.iswindows() + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂) + elseif Sys.isapple() + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VfU₁, VfSU₂)#, VSU₃) + else + (Vtr, Vℤ₂, Vfℤ₂, VU₁, VCU₁, VSU₂, VfSU₂)#, VSU₃) + end else - (Vtr, Vℤ₂, Vfℤ₂, VU₁, VCU₁, VSU₂, VfSU₂)#, VSU₃) + (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂)#, VSU₃) end - else + catch (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂)#, VSU₃) end -catch - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂)#, VSU₃) end +@testitem "left orth" setup = [Setup] begin + function test_leftorth(t, p, alg) + Q, R = @inferred leftorth(t, p; alg) + @test Q * R ≈ permute(t, p) + @test isisometry(Q) + end + + p = ((3, 4, 2), (1, 5)) + elts = (Float32, ComplexF64) + algs = (TensorKit.QR(), TensorKit.QRpos(), TensorKit.QL(), TensorKit.QLpos(), + TensorKit.Polar(), TensorKit.SVD(), TensorKit.SDD()) -function test_leftorth(t, p, alg) - Q, R = @inferred leftorth(t, p; alg) - @test Q * R ≈ permute(t, p) - @test isisometry(Q) - if alg isa Polar - @test isposdef(R) - @test domain(R) == codomain(R) == domain(permute(space(t), p)) + testname(V) = "symmetry: $(TensorKit.type_repr(sectortype(first(V))))" + @timedtestset "$(testname(V))" for V in spacelist + W = ⊗(V...) + for T in elts, alg in algs + t = rand(T, W) + test_leftorth(t, p, alg) + tᴴ = t' + test_leftorth(tᴴ, p, alg) + end end end + function test_leftnull(t, p, alg) N = @inferred leftnull(t, p; alg) @test isisometry(N) - @test norm(N' * permute(t, p)) ≈ 0 atol= 100 * eps(norm(t)) + @test norm(N' * permute(t, p)) ≈ 0 atol = 100 * eps(norm(t)) end # @timedtestset "Factorizations with symmetry: $(sectortype(first(V)))" for V in spacelist - V = collect(spacelist)[2] - V1, V2, V3, V4, V5 = V - W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 - for T in (Float32, ComplexF64), adj in (false, true) - t = adj ? rand(T, W)' : rand(T, W); - @testset "leftorth with $alg" for alg in (TensorKit.QR(), TensorKit.QRpos(), TensorKit.QL(), TensorKit.QLpos(), TensorKit.Polar(), TensorKit.SVD(), TensorKit.SDD()) - test_leftorth(t, ((3, 4, 2), (1, 5)), alg) +V = collect(spacelist)[1] +V1, V2, V3, V4, V5 = V +W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 +for T in (Float32, ComplexF64), adj in (false, true) + t = adj ? rand(T, W)' : rand(T, W) + @testset "leftorth with $alg" for alg in + (TensorKit.QR(), TensorKit.QRpos(), TensorKit.QL(), + TensorKit.QLpos(), TensorKit.Polar(), + TensorKit.SVD(), TensorKit.SDD()) + test_leftorth(t, ((3, 4, 2), (1, 5)), alg) + end + @testset "leftnull with $alg" for alg in + (TensorKit.QR(), TensorKit.SVD(), TensorKit.SDD()) + test_leftnull(t, ((3, 4, 2), (1, 5)), alg) + end + @testset "rightorth with $alg" for alg in + (TensorKit.RQ(), TensorKit.RQpos(), + TensorKit.LQ(), TensorKit.LQpos(), + TensorKit.Polar(), TensorKit.SVD(), + TensorKit.SDD()) + L, Q = @constinferred rightorth(t, ((3, 4), (2, 1, 5)); alg=alg) + QQd = Q * Q' + @test QQd ≈ one(QQd) + @test L * Q ≈ permute(t, ((3, 4), (2, 1, 5))) + if alg isa Polar + @test isposdef(L) + @test domain(L) == codomain(L) == space(t, 3) ⊗ space(t, 4) + end + end + @testset "rightnull with $alg" for alg in + (TensorKit.LQ(), TensorKit.SVD(), + TensorKit.SDD()) + M = @constinferred rightnull(t, ((3, 4), (2, 1, 5)); alg=alg) + MMd = M * M' + @test MMd ≈ one(MMd) + @test norm(permute(t, ((3, 4), (2, 1, 5))) * M') < + 100 * eps(norm(t)) + end + @testset "tsvd with $alg" for alg in (TensorKit.SVD(), TensorKit.SDD()) + U, S, V = @constinferred tsvd(t, ((3, 4, 2), (1, 5)); alg=alg) + UdU = U' * U + @test UdU ≈ one(UdU) + VVd = V * V' + @test VVd ≈ one(VVd) + t2 = permute(t, ((3, 4, 2), (1, 5))) + @test U * S * V ≈ t2 + + s = LinearAlgebra.svdvals(t2) + s′ = LinearAlgebra.diag(S) + for (c, b) in s + @test b ≈ s′[c] end - @testset "leftnull with $alg" for alg in (TensorKit.QR(), TensorKit.SVD(), TensorKit.SDD()) - test_leftnull(t, ((3, 4, 2), (1, 5)), alg) + end + @testset "cond and rank" begin + t2 = permute(t, ((3, 4, 2), (1, 5))) + d1 = dim(codomain(t2)) + d2 = dim(domain(t2)) + @test rank(t2) == min(d1, d2) + M = leftnull(t2) + @test rank(M) == max(d1, d2) - min(d1, d2) + t3 = unitary(T, V1 ⊗ V2, V1 ⊗ V2) + @test cond(t3) ≈ one(real(T)) + @test rank(t3) == dim(V1 ⊗ V2) + t4 = randn(T, V1 ⊗ V2, V1 ⊗ V2) + t4 = (t4 + t4') / 2 + vals = LinearAlgebra.eigvals(t4) + λmax = maximum(s -> maximum(abs, s), values(vals)) + λmin = minimum(s -> minimum(abs, s), values(vals)) + @test cond(t4) ≈ λmax / λmin + end +end + +@testset "empty tensor" begin + for T in (Float32, ComplexF64) + T = Float64 + t = randn(T, V1 ⊗ V2, zero(V1)) + @testset "leftorth with $alg" for alg in + (TensorKit.QR(), TensorKit.QRpos(), + TensorKit.QL(), TensorKit.QLpos(), + TensorKit.Polar(), TensorKit.SVD(), + TensorKit.SDD()) + Q, R = @constinferred leftorth(t; alg=alg) + @test Q == t + @test dim(Q) == dim(R) == 0 + end + @testset "leftnull with $alg" for alg in + (TensorKit.QR(), TensorKit.SVD(), + TensorKit.SDD()) + N = @constinferred leftnull(t; alg=alg) + @test N' * N ≈ id(domain(N)) + @test N * N' ≈ id(codomain(N)) end @testset "rightorth with $alg" for alg in (TensorKit.RQ(), TensorKit.RQpos(), TensorKit.LQ(), TensorKit.LQpos(), TensorKit.Polar(), TensorKit.SVD(), TensorKit.SDD()) - L, Q = @constinferred rightorth(t, ((3, 4), (2, 1, 5)); alg=alg) - QQd = Q * Q' - @test QQd ≈ one(QQd) - @test L * Q ≈ permute(t, ((3, 4), (2, 1, 5))) - if alg isa Polar - @test isposdef(L) - @test domain(L) == codomain(L) == space(t, 3) ⊗ space(t, 4) - end + L, Q = @constinferred rightorth(copy(t'); alg=alg) + @test Q == t' + @test dim(Q) == dim(L) == 0 end @testset "rightnull with $alg" for alg in (TensorKit.LQ(), TensorKit.SVD(), TensorKit.SDD()) - M = @constinferred rightnull(t, ((3, 4), (2, 1, 5)); alg=alg) - MMd = M * M' - @test MMd ≈ one(MMd) - @test norm(permute(t, ((3, 4), (2, 1, 5))) * M') < - 100 * eps(norm(t)) + M = @constinferred rightnull(copy(t'); alg=alg) + @test M * M' ≈ id(codomain(M)) + @test M' * M ≈ id(domain(M)) end @testset "tsvd with $alg" for alg in (TensorKit.SVD(), TensorKit.SDD()) - U, S, V = @constinferred tsvd(t, ((3, 4, 2), (1, 5)); alg=alg) - UdU = U' * U - @test UdU ≈ one(UdU) - VVd = V * V' - @test VVd ≈ one(VVd) - t2 = permute(t, ((3, 4, 2), (1, 5))) - @test U * S * V ≈ t2 - - s = LinearAlgebra.svdvals(t2) - s′ = LinearAlgebra.diag(S) - for (c, b) in s - @test b ≈ s′[c] - end + U, S, V = @constinferred tsvd(t; alg=alg) + @test U == t + @test dim(U) == dim(S) == dim(V) end @testset "cond and rank" begin - t2 = permute(t, ((3, 4, 2), (1, 5))) - d1 = dim(codomain(t2)) - d2 = dim(domain(t2)) - @test rank(t2) == min(d1, d2) - M = leftnull(t2) - @test rank(M) == max(d1, d2) - min(d1, d2) - t3 = unitary(T, V1 ⊗ V2, V1 ⊗ V2) - @test cond(t3) ≈ one(real(T)) - @test rank(t3) == dim(V1 ⊗ V2) - t4 = randn(T, V1 ⊗ V2, V1 ⊗ V2) - t4 = (t4 + t4') / 2 - vals = LinearAlgebra.eigvals(t4) - λmax = maximum(s -> maximum(abs, s), values(vals)) - λmin = minimum(s -> minimum(abs, s), values(vals)) - @test cond(t4) ≈ λmax / λmin - end - end - @testset "empty tensor" begin - for T in (Float32, ComplexF64) - t = randn(T, V1 ⊗ V2, zero(V1)) - @testset "leftorth with $alg" for alg in - (TensorKit.QR(), TensorKit.QRpos(), - TensorKit.QL(), TensorKit.QLpos(), - TensorKit.Polar(), TensorKit.SVD(), - TensorKit.SDD()) - Q, R = @constinferred leftorth(t; alg=alg) - @test Q == t - @test dim(Q) == dim(R) == 0 - end - @testset "leftnull with $alg" for alg in - (TensorKit.QR(), TensorKit.SVD(), - TensorKit.SDD()) - N = @constinferred leftnull(t; alg=alg) - @test N' * N ≈ id(domain(N)) - @test N * N' ≈ id(codomain(N)) - end - @testset "rightorth with $alg" for alg in - (TensorKit.RQ(), TensorKit.RQpos(), - TensorKit.LQ(), TensorKit.LQpos(), - TensorKit.Polar(), TensorKit.SVD(), - TensorKit.SDD()) - L, Q = @constinferred rightorth(copy(t'); alg=alg) - @test Q == t' - @test dim(Q) == dim(L) == 0 - end - @testset "rightnull with $alg" for alg in - (TensorKit.LQ(), TensorKit.SVD(), - TensorKit.SDD()) - M = @constinferred rightnull(copy(t'); alg=alg) - @test M * M' ≈ id(codomain(M)) - @test M' * M ≈ id(domain(M)) - end - @testset "tsvd with $alg" for alg in (TensorKit.SVD(), TensorKit.SDD()) - U, S, V = @constinferred tsvd(t; alg=alg) - @test U == t - @test dim(U) == dim(S) == dim(V) - end - @testset "cond and rank" begin - @test rank(t) == 0 - W2 = zero(V1) * zero(V2) - t2 = rand(W2, W2) - @test rank(t2) == 0 - @test cond(t2) == 0.0 - end + @test rank(t) == 0 + W2 = zero(V1) * zero(V2) + t2 = rand(W2, W2) + @test rank(t2) == 0 + @test cond(t2) == 0.0 end end - @testset "eig and isposdef" begin - for T in (Float32, ComplexF64) - t = rand(T, V1 ⊗ V1' ⊗ V2 ⊗ V2') - D, V = eigen(t, ((1, 3), (2, 4))) - t2 = permute(t, ((1, 3), (2, 4))) - @test t2 * V ≈ V * D +end +@testset "eig and isposdef" begin + for T in (Float32, ComplexF64) + t = rand(T, V1 ⊗ V1' ⊗ V2 ⊗ V2') + D, V = eigen(t, ((1, 3), (2, 4))) + t2 = permute(t, ((1, 3), (2, 4))) + @test t2 * V ≈ V * D - d = LinearAlgebra.eigvals(t2; sortby=nothing) - d′ = LinearAlgebra.diag(D) - for (c, b) in d - @test b ≈ d′[c] - end + d = LinearAlgebra.eigvals(t2; sortby=nothing) + d′ = LinearAlgebra.diag(D) + for (c, b) in d + @test b ≈ d′[c] + end - # Somehow moving these test before the previous one gives rise to errors - # with T=Float32 on x86 platforms. Is this an OpenBLAS issue? - VdV = V' * V - VdV = (VdV + VdV') / 2 - @test isposdef(VdV) + # Somehow moving these test before the previous one gives rise to errors + # with T=Float32 on x86 platforms. Is this an OpenBLAS issue? + VdV = V' * V + VdV = (VdV + VdV') / 2 + @test isposdef(VdV) - @test !isposdef(t2) # unlikely for non-hermitian map - t2 = (t2 + t2') - D, V = eigen(t2) - VdV = V' * V - @test VdV ≈ one(VdV) - D̃, Ṽ = @constinferred eigh(t2) - @test D ≈ D̃ - @test V ≈ Ṽ - λ = minimum(minimum(real(LinearAlgebra.diag(b))) - for (c, b) in blocks(D)) - @test cond(Ṽ) ≈ one(real(T)) - @test isposdef(t2) == isposdef(λ) - @test isposdef(t2 - λ * one(t2) + 0.1 * one(t2)) - @test !isposdef(t2 - λ * one(t2) - 0.1 * one(t2)) - end + @test !isposdef(t2) # unlikely for non-hermitian map + t2 = (t2 + t2') + D, V = eigen(t2) + VdV = V' * V + @test VdV ≈ one(VdV) + D̃, Ṽ = @constinferred eigh(t2) + @test D ≈ D̃ + @test V ≈ Ṽ + λ = minimum(minimum(real(LinearAlgebra.diag(b))) + for (c, b) in blocks(D)) + @test cond(Ṽ) ≈ one(real(T)) + @test isposdef(t2) == isposdef(λ) + @test isposdef(t2 - λ * one(t2) + 0.1 * one(t2)) + @test !isposdef(t2 - λ * one(t2) - 0.1 * one(t2)) end - @testset "Tensor truncation" begin - for T in (Float32, ComplexF64), p in (1, 2, 3, Inf), adj in (false, true) - t = adj ? rand(T, V1 ⊗ V2 ⊗ V3, V4 ⊗ V5) : rand(T, V4 ⊗ V5, V1 ⊗ V2 ⊗ V3)' +end +@testset "Tensor truncation" begin + for T in (Float32, ComplexF64), p in (1, 2, 3, Inf), adj in (false, true) + t = adj ? rand(T, V1 ⊗ V2 ⊗ V3, V4 ⊗ V5) : rand(T, V4 ⊗ V5, V1 ⊗ V2 ⊗ V3)' - U₀, S₀, V₀, = tsvd(t) - t = rmul!(t, 1 / norm(S₀, p)) - U, S, V, ϵ = @constinferred tsvd(t; trunc=truncerr(5e-1), p=p) - # @show p, ϵ - # @show domain(S) - # @test min(space(S,1), space(S₀,1)) != space(S₀,1) - U′, S′, V′, ϵ′ = tsvd(t; trunc=truncerr(nextfloat(ϵ)), p=p) - @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) - U′, S′, V′, ϵ′ = tsvd(t; trunc=truncdim(ceil(Int, dim(domain(S)))), - p=p) - @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) - U′, S′, V′, ϵ′ = tsvd(t; trunc=truncspace(space(S, 1)), p=p) - @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) - # results with truncationcutoff cannot be compared because they don't take degeneracy into account, and thus truncate differently - U, S, V, ϵ = tsvd(t; trunc=truncbelow(1 / dim(domain(S₀))), p=p) - # @show p, ϵ - # @show domain(S) - # @test min(space(S,1), space(S₀,1)) != space(S₀,1) - U′, S′, V′, ϵ′ = tsvd(t; trunc=truncspace(space(S, 1)), p=p) - @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) - end + U₀, S₀, V₀, = tsvd(t) + t = rmul!(t, 1 / norm(S₀, p)) + U, S, V, ϵ = @constinferred tsvd(t; trunc=truncerr(5e-1), p=p) + # @show p, ϵ + # @show domain(S) + # @test min(space(S,1), space(S₀,1)) != space(S₀,1) + U′, S′, V′, ϵ′ = tsvd(t; trunc=truncerr(nextfloat(ϵ)), p=p) + @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) + U′, S′, V′, ϵ′ = tsvd(t; trunc=truncdim(ceil(Int, dim(domain(S)))), + p=p) + @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) + U′, S′, V′, ϵ′ = tsvd(t; trunc=truncspace(space(S, 1)), p=p) + @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) + # results with truncationcutoff cannot be compared because they don't take degeneracy into account, and thus truncate differently + U, S, V, ϵ = tsvd(t; trunc=truncbelow(1 / dim(domain(S₀))), p=p) + # @show p, ϵ + # @show domain(S) + # @test min(space(S,1), space(S₀,1)) != space(S₀,1) + U′, S′, V′, ϵ′ = tsvd(t; trunc=truncspace(space(S, 1)), p=p) + @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) end end +# end diff --git a/test/runtests.jl b/test/runtests.jl index 2e716fa18..258fe77f3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -107,11 +107,11 @@ VfSU₂ = (ℂ[FermionSpin](0 => 3, 1 // 2 => 1), # ℂ[SU3Irrep]((0, 0, 0) => 1, (1, 0, 0) => 1, (1, 1, 0) => 1)') Ti = time() -include("fusiontrees.jl") -include("spaces.jl") +# include("fusiontrees.jl") +# include("spaces.jl") include("tensors.jl") -include("diagonal.jl") -include("planar.jl") +# include("diagonal.jl") +# include("planar.jl") # TODO: remove once we know AD is slow on macOS CI if !(Sys.isapple() && get(ENV, "CI", "false") == "true") include("ad.jl") diff --git a/test/tensors.jl b/test/tensors.jl index 00e827d94..00416de51 100644 --- a/test/tensors.jl +++ b/test/tensors.jl @@ -450,10 +450,11 @@ for V in spacelist QdQ = Q' * Q @test QdQ ≈ one(QdQ) @test Q * R ≈ permute(t, ((3, 4, 2), (1, 5))) - if alg isa Polar - @test isposdef(R) - @test domain(R) == codomain(R) == space(t, 1)' ⊗ space(t, 5)' - end + # removed since leftorth now merges legs! + # if alg isa Polar + # @test isposdef(R) + # @test domain(R) == codomain(R) == space(t, 1)' ⊗ space(t, 5)' + # end end @testset "leftnull with $alg" for alg in (TensorKit.QR(), TensorKit.SVD(), @@ -473,10 +474,11 @@ for V in spacelist QQd = Q * Q' @test QQd ≈ one(QQd) @test L * Q ≈ permute(t, ((3, 4), (2, 1, 5))) - if alg isa Polar - @test isposdef(L) - @test domain(L) == codomain(L) == space(t, 3) ⊗ space(t, 4) - end + # removed since rightorth now merges legs! + # if alg isa Polar + # @test isposdef(L) + # @test domain(L) == codomain(L) == space(t, 3) ⊗ space(t, 4) + # end end @testset "rightnull with $alg" for alg in (TensorKit.LQ(), TensorKit.SVD(), @@ -611,23 +613,36 @@ for V in spacelist for t in ts U₀, S₀, V₀, = tsvd(t) t = rmul!(t, 1 / norm(S₀, p)) - U, S, V, ϵ = @constinferred tsvd(t; trunc=truncerr(5e-1), p=p) + U, S, V = @constinferred tsvd(t; trunc=truncerr(5e-1, p)) + ϵ = TensorKit._norm(LinearAlgebra.svdvals(U * S * V - t), p, + zero(scalartype(S))) + p == 2 && @test ϵ < 5e-1 # @show p, ϵ # @show domain(S) # @test min(space(S,1), space(S₀,1)) != space(S₀,1) - U′, S′, V′, ϵ′ = tsvd(t; trunc=truncerr(nextfloat(ϵ)), p=p) + U′, S′, V′ = tsvd(t; trunc=truncerr(ϵ + 10eps(ϵ), p)) + ϵ′ = TensorKit._norm(LinearAlgebra.svdvals(U′ * S′ * V′ - t), p, + zero(scalartype(S))) + @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) - U′, S′, V′, ϵ′ = tsvd(t; trunc=truncdim(ceil(Int, dim(domain(S)))), - p=p) + U′, S′, V′ = tsvd(t; trunc=truncdim(ceil(Int, dim(domain(S))))) + ϵ′ = TensorKit._norm(LinearAlgebra.svdvals(U′ * S′ * V′ - t), p, + zero(scalartype(S))) @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) - U′, S′, V′, ϵ′ = tsvd(t; trunc=truncspace(space(S, 1)), p=p) + U′, S′, V′ = tsvd(t; trunc=truncspace(space(S, 1))) + ϵ′ = TensorKit._norm(LinearAlgebra.svdvals(U′ * S′ * V′ - t), p, + zero(scalartype(S))) @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) # results with truncationcutoff cannot be compared because they don't take degeneracy into account, and thus truncate differently - U, S, V, ϵ = tsvd(t; trunc=truncbelow(1 / dim(domain(S₀))), p=p) + U, S, V = tsvd(t; trunc=truncbelow(1 / dim(domain(S₀)))) + ϵ = TensorKit._norm(LinearAlgebra.svdvals(U * S * V - t), p, + zero(scalartype(S))) # @show p, ϵ # @show domain(S) # @test min(space(S,1), space(S₀,1)) != space(S₀,1) - U′, S′, V′, ϵ′ = tsvd(t; trunc=truncspace(space(S, 1)), p=p) + U′, S′, V′ = tsvd(t; trunc=truncspace(space(S, 1))) + ϵ′ = TensorKit._norm(LinearAlgebra.svdvals(U′ * S′ * V′ - t), p, + zero(scalartype(S))) @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) end end @@ -687,8 +702,8 @@ for V in spacelist for T in (Float32, ComplexF64) tA = rand(T, V1 ⊗ V3, V1 ⊗ V3) tB = rand(T, V2 ⊗ V4, V2 ⊗ V4) - tA = 3 // 2 * leftorth(tA; alg=Polar())[1] - tB = 1 // 5 * leftorth(tB; alg=Polar())[1] + tA = 3 // 2 * leftpolar(tA)[1] + tB = 1 // 5 * leftpolar(tB)[1] tC = rand(T, V1 ⊗ V3, V2 ⊗ V4) t = @constinferred sylvester(tA, tB, tC) @test codomain(t) == V1 ⊗ V3 From 25d03be2914f997142ed6924181de2b4e105243a Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 11 Jun 2025 21:25:39 -0400 Subject: [PATCH 31/70] Clean up truncation --- src/tensors/factorizations/truncation.jl | 205 ++++++----------------- 1 file changed, 48 insertions(+), 157 deletions(-) diff --git a/src/tensors/factorizations/truncation.jl b/src/tensors/factorizations/truncation.jl index e9806645a..4f7cec733 100644 --- a/src/tensors/factorizations/truncation.jl +++ b/src/tensors/factorizations/truncation.jl @@ -1,149 +1,26 @@ -# truncation.jl -# -# Implements truncation schemes for truncating a tensor with svd, leftorth or rightorth - +# Strategies +# ---------- notrunc() = NoTruncation() + # deprecate const TruncationScheme = TruncationStrategy +@deprecate truncdim(d::Int) truncrank(d) +@deprecate truncbelow(ϵ::Real, add_back::Int=0) trunctol(ϵ) +# TODO: add this to MatrixAlgebraKit struct TruncationError{T<:Real} <: TruncationStrategy ϵ::T p::Real end truncerr(epsilon::Real, p::Real=2) = TruncationError(epsilon, p) -# struct TruncationDimension <: TruncationScheme -# dim::Int -# end -@deprecate truncdim(d::Int) truncrank(d) - struct TruncationSpace{S<:ElementarySpace} <: TruncationStrategy space::S end truncspace(space::ElementarySpace) = TruncationSpace(space) -struct TruncationCutoff{T<:Real} <: TruncationStrategy - ϵ::T - add_back::Int -end -@deprecate truncbelow(ϵ::Real, add_back::Int=0) begin - add_back == 0 || @warn "add_back is ignored" - trunctol(ϵ) -end -# truncbelow(epsilon::Real, add_back::Int=0) = TruncationCutoff(epsilon, add_back) - -# Compute the total truncation error given truncation dimensions -function _compute_truncerr(Σdata, truncdim, p=2) - I = keytype(Σdata) - S = scalartype(valtype(Σdata)) - return TensorKit._norm((c => @view(v[(get(truncdim, c, 0) + 1):end]) - for (c, v) in Σdata), - p, zero(S)) -end - -# Compute truncation dimensions -# function _compute_truncdim(Σdata, ::NoTruncation, p=2) -# I = keytype(Σdata) -# truncdim = SectorDict{I,Int}(c => length(v) for (c, v) in Σdata) -# return truncdim -# end -# function _compute_truncdim(Σdata, trunc::TruncationDimension, p=2) -# I = keytype(Σdata) -# truncdim = SectorDict{I,Int}(c => length(v) for (c, v) in Σdata) -# while sum(dim(c) * d for (c, d) in truncdim) > trunc.dim -# cmin = _findnexttruncvalue(Σdata, truncdim, p) -# isnothing(cmin) && break -# truncdim[cmin] -= 1 -# end -# return truncdim -# end -function _compute_truncdim(Σdata, trunc::TruncationSpace, p=2) - I = keytype(Σdata) - truncdim = SectorDict{I,Int}(c => min(length(v), dim(trunc.space, c)) - for (c, v) in Σdata) - return truncdim -end - -# function _compute_truncdim(Σdata, trunc::TruncationCutoff, p=2) -# I = keytype(Σdata) -# truncdim = SectorDict{I,Int}(c => length(v) for (c, v) in Σdata) -# for (c, v) in Σdata -# newdim = findlast(Base.Fix2(>, trunc.ϵ), v) -# if newdim === nothing -# truncdim[c] = 0 -# else -# truncdim[c] = newdim -# end -# end -# for i in 1:(trunc.add_back) -# cmax = _findnextgrowvalue(Σdata, truncdim, p) -# isnothing(cmax) && break -# truncdim[cmax] += 1 -# end -# return truncdim -# end - -# Combine truncations -# struct MultipleTruncation{T<:Tuple{Vararg{TruncationScheme}}} <: TruncationScheme -# truncations::T -# end -# function Base.:&(a::MultipleTruncation, b::MultipleTruncation) -# return MultipleTruncation((a.truncations..., b.truncations...)) -# end -# function Base.:&(a::MultipleTruncation, b::TruncationScheme) -# return MultipleTruncation((a.truncations..., b)) -# end -# function Base.:&(a::TruncationScheme, b::MultipleTruncation) -# return MultipleTruncation((a, b.truncations...)) -# end -# Base.:&(a::TruncationScheme, b::TruncationScheme) = MultipleTruncation((a, b)) - -# function _compute_truncdim(Σdata, trunc::MultipleTruncation, p::Real=2) -# truncdim = _compute_truncdim(Σdata, trunc.truncations[1], p) -# for k in 2:length(trunc.truncations) -# truncdimₖ = _compute_truncdim(Σdata, trunc.truncations[k], p) -# for (c, d) in truncdim -# truncdim[c] = min(d, truncdimₖ[c]) -# end -# end -# return truncdim -# end - -# auxiliary function -function _findnexttruncvalue(S, truncdim::SectorDict{I,Int}) where {I<:Sector} - # early return - (isempty(S) || all(iszero, values(truncdim))) && return nothing - σmin, imin = findmin(keys(truncdim)) do c - d = truncdim[c] - return S[c][d] - end - return σmin, keys(truncdim)[imin] -end - -function _findnextgrowvalue(Σdata, truncdim::SectorDict{I,Int}, p::Real) where {I<:Sector} - istruncated = SectorDict{I,Bool}(c => (d < length(Σdata[c])) for (c, d) in truncdim) - # early return - (isempty(Σdata) || !any(values(istruncated))) && return nothing - - # find some suitable starting candidate - cmax = findfirst(istruncated) - σmax = Σdata[cmax][truncdim[cmax] + 1] - - # find the actual maximal singular value - for (c, σs) in Σdata - if istruncated[c] - σ = σs[truncdim[c] + 1] - if σ > σmax - cmax, σmax = c, σ - end - end - end - return cmax -end - # Truncation # ---------- - function truncate!(::typeof(svd_trunc!), (U, S, Vᴴ)::_T_USVᴴ, strategy::TruncationStrategy) ind = findtruncated_sorted(diagview(S), strategy) V_truncated = spacetype(S)(c => length(I) for (c, I) in ind) @@ -172,29 +49,60 @@ function truncate!(::typeof(svd_trunc!), (U, S, Vᴴ)::_T_USVᴴ, strategy::Trun return Ũ, S̃, Ṽᴴ end -function findtruncated_sorted(S::SectorDict, strategy::TruncationKeepAbove) - atol = if strategy.rtol > 0 - max(strategy.atol, _norm(S, strategy.p) * strategy.rtol) - else - strategy.atol +function truncate!(::typeof(left_null!), + (U, S)::Tuple{<:AbstractTensorMap, + <:AbstractTensorMap}, + strategy::MatrixAlgebraKit.TruncationStrategy) + extended_S = SectorDict(c => vcat(diagview(b), + zeros(eltype(b), max(0, size(b, 2) - size(b, 1)))) + for (c, b) in blocks(S)) + ind = findtruncated(extended_S, strategy) + V_truncated = spacetype(S)(c => length(axes(b, 1)[ind[c]]) for (c, b) in blocks(S)) + Ũ = similar(U, codomain(U) ← V_truncated) + for (c, b) in blocks(Ũ) + copy!(b, @view(block(U, c)[:, ind[c]])) end + return Ũ +end + +# Find truncation +# --------------- +# auxiliary functions +rtol_to_atol(S, p, atol, rtol) = rtol > 0 ? max(atol, _norm(S, p) * rtol) : atol + +function _compute_truncerr(Σdata, truncdim, p=2) + I = keytype(Σdata) + S = scalartype(valtype(Σdata)) + return TensorKit._norm((c => @view(v[(get(truncdim, c, 0) + 1):end]) + for (c, v) in Σdata), + p, zero(S)) +end + +function _findnexttruncvalue(S, truncdim::SectorDict{I,Int}) where {I<:Sector} + # early return + (isempty(S) || all(iszero, values(truncdim))) && return nothing + σmin, imin = findmin(keys(truncdim)) do c + d = truncdim[c] + return S[c][d] + end + return σmin, keys(truncdim)[imin] +end + +# sorted implementations +function findtruncated_sorted(S::SectorDict, strategy::TruncationKeepAbove) + atol = rtol_to_atol(S, strategy.p, strategy.atol, strategy.rtol) findtrunc = Base.Fix2(findtruncated_sorted, truncbelow(atol)) return SectorDict(c => findtrunc(d) for (c, d) in Sd) end function findtruncated_sorted(S::SectorDict, strategy::TruncationKeepBelow) - atol = if strategy.rtol > 0 - max(strategy.atol, _norm(S, strategy.p) * strategy.rtol) - else - strategy.atol - end + atol = rtol_to_atol(S, strategy.p, strategy.atol, strategy.rtol) findtrunc = Base.Fix2(findtruncated_sorted, truncabove(atol)) return SectorDict(c => findtrunc(d) for (c, d) in Sd) end function findtruncated_sorted(Sd::SectorDict, strategy::TruncationError) I = keytype(Sd) - S = real(scalartype(valtype(Sd))) truncdim = SectorDict{I,Int}(c => length(d) for (c, d) in Sd) while true next = _findnexttruncvalue(Sd, truncdim) @@ -216,7 +124,6 @@ end function findtruncated_sorted(Sd::SectorDict, strategy::TruncationKeepSorted) @assert strategy.by === abs && strategy.rev == true "Not implemented" I = keytype(Sd) - S = real(scalartype(valtype(Sd))) truncdim = SectorDict{I,Int}(c => length(d) for (c, d) in Sd) totaldim = sum(dim(c) * d for (c, d) in truncdim; init=0) while true @@ -252,19 +159,3 @@ function findtruncated_sorted(Sd::SectorDict, strategy::TruncationIntersection) return SectorDict(c => intersect(map(Base.Fix2(getindex, c), inds)...) for c in intersect(map(keys, inds)...)) end - -function MatrixAlgebraKit.truncate!(::typeof(left_null!), - (U, S)::Tuple{<:AbstractTensorMap, - <:AbstractTensorMap}, - strategy::MatrixAlgebraKit.TruncationStrategy) - extended_S = SectorDict(c => vcat(MatrixAlgebraKit.diagview(b), - zeros(eltype(b), max(0, size(b, 2) - size(b, 1)))) - for (c, b) in blocks(S)) - ind = MatrixAlgebraKit.findtruncated(extended_S, strategy) - V_truncated = spacetype(S)(c => length(axes(b, 1)[ind[c]]) for (c, b) in blocks(S)) - Ũ = similar(U, codomain(U) ← V_truncated) - for (c, b) in blocks(Ũ) - copy!(b, @view(block(U, c)[:, ind[c]])) - end - return Ũ -end From 7aeba6c93582fb6878e55cc67803077d65cc2929 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 12 Jun 2025 11:46:14 -0400 Subject: [PATCH 32/70] Update tuple formatting --- ext/TensorKitChainRulesCoreExt/factorizations.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/TensorKitChainRulesCoreExt/factorizations.jl b/ext/TensorKitChainRulesCoreExt/factorizations.jl index a91c1dbc0..be72a1cec 100644 --- a/ext/TensorKitChainRulesCoreExt/factorizations.jl +++ b/ext/TensorKitChainRulesCoreExt/factorizations.jl @@ -25,8 +25,8 @@ function ChainRulesCore.rrule(::typeof(TensorKit.tsvd!), t::AbstractTensorMap; ΔU, ΔΣ, ΔV⁺, = unthunk.(ΔUSVϵ) Δt = similar(t) foreachblock(Δt) do (c, b) - USVᴴc = block(U, c), block(Σ, c), block(V⁺, c) - ΔUSVᴴc = block(ΔU, c), block(ΔΣ, c), block(ΔV⁺, c) + USVᴴc = (block(U, c), block(Σ, c), block(V⁺, c)) + ΔUSVᴴc = (block(ΔU, c), block(ΔΣ, c), block(ΔV⁺, c)) svd_compact_pullback!(b, USVᴴc, ΔUSVᴴc) return nothing end From d297ea3feb4f5ae7db93da60ffd8dee3687c59a8 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 12 Jun 2025 11:46:48 -0400 Subject: [PATCH 33/70] Fix scheduler selection --- src/tensors/backends.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tensors/backends.jl b/src/tensors/backends.jl index 1083115b9..0fc8f99f6 100644 --- a/src/tensors/backends.jl +++ b/src/tensors/backends.jl @@ -2,7 +2,7 @@ # ------------------------ function select_scheduler(scheduler=OhMyThreads.Implementation.NotGiven(); kwargs...) return if scheduler == OhMyThreads.Implementation.NotGiven() && isempty(kwargs) - Threads.nthreads() > 1 ? SerialScheduler() : DynamicScheduler() + Threads.nthreads() == 1 ? SerialScheduler() : DynamicScheduler() else OhMyThreads.Implementation._scheduler_from_userinput(scheduler; kwargs...) end From 545d7f6ed11a0153f8c7dda199e6fe67e8a1b4b0 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 12 Jun 2025 11:48:38 -0400 Subject: [PATCH 34/70] Retain `dual` in `ominus` --- src/spaces/gradedspace.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index 976846e05..37f99e89b 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -150,9 +150,10 @@ function ⊕(V₁::GradedSpace{I}, V₂::GradedSpace{I}) where {I<:Sector} end function ⊖(V::GradedSpace{I}, W::GradedSpace{I}) where {I<:Sector} - V ≿ W && isdual(V) == isdual(W) || + dual = isdual(V) + V ≿ W && dual == isdual(W) || throw(SpaceMismatch("$(W) is not a subspace of $(V)")) - return typeof(V)(c => dim(V, c) - dim(W, c) for c in sectors(V)) + return typeof(V)(c => dim(V, c) - dim(W, c) for c in sectors(V); dual) end function flip(V::GradedSpace{I}) where {I<:Sector} From 8537fecf41088ebbd3b3174d954ef78cf6e6e0c3 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 12 Jun 2025 12:48:25 -0400 Subject: [PATCH 35/70] Update blockiterator --- src/tensors/blockiterator.jl | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/tensors/blockiterator.jl b/src/tensors/blockiterator.jl index cbfcd391f..0f1625ef2 100644 --- a/src/tensors/blockiterator.jl +++ b/src/tensors/blockiterator.jl @@ -15,27 +15,32 @@ Base.length(iter::BlockIterator) = length(iter.structure) Base.isdone(iter::BlockIterator, state...) = Base.isdone(iter.structure, state...) # TODO: fast-path when structures are the same? -# TODO: do we want f(c, bs...) or f(c, bs)? # TODO: implement scheduler -# TODO: do we prefer `blocks(t, ts...)` instead or as well? """ - foreachblock(f, t::AbstractTensorMap, ts::AbstractTensorMap...; [scheduler]) + foreachblock(f, ts::AbstractTensorMap...; [scheduler]) Apply `f` to each block of `t` and the corresponding blocks of `ts`. Optionally, `scheduler` can be used to parallelize the computation. This function is equivalent to the following loop: ```julia -for (c, b) in blocks(t) - bs = (b, block.(ts, c)...) +for c in union(blocksectors.(ts)...) + bs = map(t -> block(t, c), ts) f(c, bs) end ``` """ function foreachblock(f, t::AbstractTensorMap, ts::AbstractTensorMap...; scheduler=nothing) - allsectors = union(blocksectors(t), blocksectors.(ts)...) + tensors = (t, ts...) + allsectors = union(blocksectors.(tensors)...) foreach(allsectors) do c - return f(c, map(Base.Fix2(block, c), (t, ts...))) + return f(c, block.(tensors, Ref(c))) + end + return nothing +end +function foreachblock(f, t::AbstractTensorMap; scheduler=nothing) + foreach(blocks(t)) do (c, b) + return f(c, (b,)) end return nothing end From 74758bcc606de6c9667c60963fa4a80010d53b75 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 12 Jun 2025 16:58:44 -0400 Subject: [PATCH 36/70] Update svd rrule --- .../factorizations.jl | 169 ++---------------- 1 file changed, 15 insertions(+), 154 deletions(-) diff --git a/ext/TensorKitChainRulesCoreExt/factorizations.jl b/ext/TensorKitChainRulesCoreExt/factorizations.jl index be72a1cec..e51303085 100644 --- a/ext/TensorKitChainRulesCoreExt/factorizations.jl +++ b/ext/TensorKitChainRulesCoreExt/factorizations.jl @@ -3,40 +3,32 @@ using MatrixAlgebraKit: svd_compact_pullback! # Factorizations rules # -------------------- function ChainRulesCore.rrule(::typeof(TensorKit.tsvd!), t::AbstractTensorMap; - trunc::TensorKit.TruncationScheme=TensorKit.notrunc(), - alg::Union{TensorKit.SVD,TensorKit.SDD}=TensorKit.SDD()) - U, Σ, V⁺, truncerr = tsvd(t; trunc=TensorKit.notrunc(), alg) - - if !(trunc == TensorKit.notrunc()) && !isempty(blocksectors(t)) - Σdata = TensorKit.SectorDict(c => diag(b) for (c, b) in blocks(Σ)) - - truncdim = TensorKit._compute_truncdim(Σdata, trunc; p=2) - truncerr = TensorKit._compute_truncerr(Σdata, truncdim; p=2) - - SVDdata = TensorKit.SectorDict(c => (block(U, c), Σc, block(V⁺, c)) - for (c, Σc) in Σdata) - - Ũ, Σ̃, Ṽ⁺ = TensorKit._create_svdtensors(t, SVDdata, truncdim) + trunc::TruncationStrategy=TensorKit.notrunc(), + kwargs...) + # TODO: I think we can use tsvd! here without issues because we don't actually require + # the data of `t` anymore. + USVᴴ = tsvd(t; trunc=TensorKit.notrunc(), alg) + + if trunc != TensorKit.notrunc() && !isempty(blocksectors(t)) + USVᴴ′ = MatrixAlgebraKit.truncate!(svd_trunc!, USVᴴ, trunc) else - Ũ, Σ̃, Ṽ⁺ = U, Σ, V⁺ + USVᴴ′ = USVᴴ end - function tsvd!_pullback(ΔUSVϵ) - ΔU, ΔΣ, ΔV⁺, = unthunk.(ΔUSVϵ) + function tsvd!_pullback(ΔUSVᴴ′) + ΔUSVᴴ = unthunk.(ΔUSVᴴ′) Δt = similar(t) foreachblock(Δt) do (c, b) - USVᴴc = (block(U, c), block(Σ, c), block(V⁺, c)) - ΔUSVᴴc = (block(ΔU, c), block(ΔΣ, c), block(ΔV⁺, c)) + USVᴴc = block.(USVᴴ, Ref(c)) + ΔUSVᴴc = block.(ΔUSVᴴ, Ref(c)) svd_compact_pullback!(b, USVᴴc, ΔUSVᴴc) return nothing end return NoTangent(), Δt end - function tsvd!_pullback(::Tuple{ZeroTangent,ZeroTangent,ZeroTangent}) - return NoTangent(), ZeroTangent() - end + tsvd!_pullback(::NTuple{3,ZeroTangent}) = NoTangent(), ZeroTangent() - return (Ũ, Σ̃, Ṽ⁺, truncerr), tsvd!_pullback + return USVᴴ′, tsvd!_pullback end function ChainRulesCore.rrule(::typeof(LinearAlgebra.svdvals!), t::AbstractTensorMap) @@ -173,137 +165,6 @@ function uppertriangularind(A::AbstractMatrix) return I end -# SVD_pullback: pullback implementation for general (possibly truncated) SVD -# -# Arguments are U, S and Vd of full (non-truncated, but still thin) SVD, as well as -# cotangent ΔU, ΔS, ΔVd variables of truncated SVD -# -# Checks whether the cotangent variables are such that they would couple to gauge-dependent -# degrees of freedom (phases of singular vectors), and prints a warning if this is the case -# -# An implementation that only uses U, S, and Vd from truncated SVD is also possible, but -# requires solving a Sylvester equation, which does not seem to be supported on GPUs. -# -# Other implementation considerations for GPU compatibility: -# no scalar indexing, lots of broadcasting and views -# -# function svd_pullback!(ΔA::AbstractMatrix, U::AbstractMatrix, S::AbstractVector, -# Vd::AbstractMatrix, ΔU, ΔS, ΔVd; -# tol::Real=default_pullback_gaugetol(S)) - -# # Basic size checks and determination -# m, n = size(U, 1), size(Vd, 2) -# size(U, 2) == size(Vd, 1) == length(S) == min(m, n) || throw(DimensionMismatch()) -# p = -1 -# if !(ΔU isa AbstractZero) -# m == size(ΔU, 1) || throw(DimensionMismatch()) -# p = size(ΔU, 2) -# end -# if !(ΔVd isa AbstractZero) -# n == size(ΔVd, 2) || throw(DimensionMismatch()) -# if p == -1 -# p = size(ΔVd, 1) -# else -# p == size(ΔVd, 1) || throw(DimensionMismatch()) -# end -# end -# if !(ΔS isa AbstractZero) -# if p == -1 -# p = length(ΔS) -# else -# p == length(ΔS) || throw(DimensionMismatch()) -# end -# end -# Up = view(U, :, 1:p) -# Vp = view(Vd, 1:p, :)' -# Sp = view(S, 1:p) - -# # rank -# r = searchsortedlast(S, tol; rev=true) - -# # compute antihermitian part of projection of ΔU and ΔV onto U and V -# # also already subtract this projection from ΔU and ΔV -# if !(ΔU isa AbstractZero) -# UΔU = Up' * ΔU -# aUΔU = rmul!(UΔU - UΔU', 1 / 2) -# if m > p -# ΔU -= Up * UΔU -# end -# else -# aUΔU = fill!(similar(U, (p, p)), 0) -# end -# if !(ΔVd isa AbstractZero) -# VΔV = Vp' * ΔVd' -# aVΔV = rmul!(VΔV - VΔV', 1 / 2) -# if n > p -# ΔVd -= VΔV' * Vp' -# end -# else -# aVΔV = fill!(similar(Vd, (p, p)), 0) -# end - -# # check whether cotangents arise from gauge-invariance objective function -# mask = abs.(Sp' .- Sp) .< tol -# Δgauge = norm(view(aUΔU, mask) + view(aVΔV, mask), Inf) -# if p > r -# rprange = (r + 1):p -# Δgauge = max(Δgauge, norm(view(aUΔU, rprange, rprange), Inf)) -# Δgauge = max(Δgauge, norm(view(aVΔV, rprange, rprange), Inf)) -# end -# Δgauge < tol || -# @warn "`svd` cotangents sensitive to gauge choice: (|Δgauge| = $Δgauge)" - -# UdΔAV = (aUΔU .+ aVΔV) .* safe_inv.(Sp' .- Sp, tol) .+ -# (aUΔU .- aVΔV) .* safe_inv.(Sp' .+ Sp, tol) -# if !(ΔS isa ZeroTangent) -# UdΔAV[diagind(UdΔAV)] .+= real.(ΔS) -# # in principle, ΔS is real, but maybe not if coming from an anyonic tensor -# end -# mul!(ΔA, Up, UdΔAV * Vp') - -# if r > p # contribution from truncation -# Ur = view(U, :, (p + 1):r) -# Vr = view(Vd, (p + 1):r, :)' -# Sr = view(S, (p + 1):r) - -# if !(ΔU isa AbstractZero) -# UrΔU = Ur' * ΔU -# if m > r -# ΔU -= Ur * UrΔU # subtract this part from ΔU -# end -# else -# UrΔU = fill!(similar(U, (r - p, p)), 0) -# end -# if !(ΔVd isa AbstractZero) -# VrΔV = Vr' * ΔVd' -# if n > r -# ΔVd -= VrΔV' * Vr' # subtract this part from ΔV -# end -# else -# VrΔV = fill!(similar(Vd, (r - p, p)), 0) -# end - -# X = (1 // 2) .* ((UrΔU .+ VrΔV) .* safe_inv.(Sp' .- Sr, tol) .+ -# (UrΔU .- VrΔV) .* safe_inv.(Sp' .+ Sr, tol)) -# Y = (1 // 2) .* ((UrΔU .+ VrΔV) .* safe_inv.(Sp' .- Sr, tol) .- -# (UrΔU .- VrΔV) .* safe_inv.(Sp' .+ Sr, tol)) - -# # ΔA += Ur * X * Vp' + Up * Y' * Vr' -# mul!(ΔA, Ur, X * Vp', 1, 1) -# mul!(ΔA, Up * Y', Vr', 1, 1) -# end - -# if m > max(r, p) && !(ΔU isa AbstractZero) # remaining ΔU is already orthogonal to U[:,1:max(p,r)] -# # ΔA += (ΔU .* safe_inv.(Sp', tol)) * Vp' -# mul!(ΔA, ΔU .* safe_inv.(Sp', tol), Vp', 1, 1) -# end -# if n > max(r, p) && !(ΔVd isa AbstractZero) # remaining ΔV is already orthogonal to V[:,1:max(p,r)] -# # ΔA += U * (safe_inv.(Sp, tol) .* ΔVd) -# mul!(ΔA, Up, safe_inv.(Sp, tol) .* ΔVd, 1, 1) -# end -# return ΔA -# end - function eig_pullback!(ΔA::AbstractMatrix, D::AbstractVector, V::AbstractMatrix, ΔD, ΔV; tol::Real=default_pullback_gaugetol(D)) From a1297f617a62d61b641bf9015e9236faf9ca190b Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 12 Jun 2025 17:04:26 -0400 Subject: [PATCH 37/70] Update eig(h) rrule --- .../factorizations.jl | 117 ++++-------------- 1 file changed, 21 insertions(+), 96 deletions(-) diff --git a/ext/TensorKitChainRulesCoreExt/factorizations.jl b/ext/TensorKitChainRulesCoreExt/factorizations.jl index e51303085..7ba026c95 100644 --- a/ext/TensorKitChainRulesCoreExt/factorizations.jl +++ b/ext/TensorKitChainRulesCoreExt/factorizations.jl @@ -1,4 +1,4 @@ -using MatrixAlgebraKit: svd_compact_pullback! +using MatrixAlgebraKit: svd_compact_pullback!, eig_full_pullback!, eigh_full_pullback! # Factorizations rules # -------------------- @@ -46,47 +46,41 @@ function ChainRulesCore.rrule(::typeof(LinearAlgebra.svdvals!), t::AbstractTenso end function ChainRulesCore.rrule(::typeof(TensorKit.eig!), t::AbstractTensorMap; kwargs...) - D, V = eig(t; kwargs...) + DV = eig(t; kwargs...) - function eig!_pullback((_ΔD, _ΔV)) - ΔD, ΔV = unthunk(_ΔD), unthunk(_ΔV) + function eig!_pullback(ΔDV′) + ΔDV = unthunk.(ΔDV′) Δt = similar(t) - for (c, b) in blocks(Δt) - Dc, Vc = block(D, c), block(V, c) - ΔDc, ΔVc = block(ΔD, c), block(ΔV, c) - Ddc = view(Dc, diagind(Dc)) - ΔDdc = (ΔDc isa AbstractZero) ? ΔDc : view(ΔDc, diagind(ΔDc)) - eig_pullback!(b, Ddc, Vc, ΔDdc, ΔVc) + foreachblock(Δt) do (c, b) + DVc = block.(DV, Ref(c)) + ΔDVc = block.(ΔDV, Ref(c)) + eig_full_pullback!(b, DVc, ΔDVc) + return nothing end return NoTangent(), Δt end - function eig!_pullback(::Tuple{ZeroTangent,ZeroTangent}) - return NoTangent(), ZeroTangent() - end + eig!_pullback(::NTuple{2,ZeroTangent}) = NoTangent(), ZeroTangent() - return (D, V), eig!_pullback + return DV, eig!_pullback end function ChainRulesCore.rrule(::typeof(TensorKit.eigh!), t::AbstractTensorMap; kwargs...) - D, V = eigh(t; kwargs...) + DV = eigh(t; kwargs...) - function eigh!_pullback((_ΔD, _ΔV)) - ΔD, ΔV = unthunk(_ΔD), unthunk(_ΔV) + function eigh!_pullback(ΔDV′) + ΔDV = unthunk.(ΔDV′) Δt = similar(t) - for (c, b) in blocks(Δt) - Dc, Vc = block(D, c), block(V, c) - ΔDc, ΔVc = block(ΔD, c), block(ΔV, c) - Ddc = view(Dc, diagind(Dc)) - ΔDdc = (ΔDc isa AbstractZero) ? ΔDc : view(ΔDc, diagind(ΔDc)) - eigh_pullback!(b, Ddc, Vc, ΔDdc, ΔVc) + foreachblock(Δt) do (c, b) + DVc = block.(DV, Ref(c)) + ΔDVc = block.(ΔDV, Ref(c)) + eigh_full_pullback!(b, DVc, ΔDVc) + return nothing end return NoTangent(), Δt end - function eigh!_pullback(::Tuple{ZeroTangent,ZeroTangent}) - return NoTangent(), ZeroTangent() - end + eigh!_pullback(::NTuple{2,ZeroTangent}) = NoTangent(), ZeroTangent() - return (D, V), eigh!_pullback + return DV, eigh!_pullback end function ChainRulesCore.rrule(::typeof(LinearAlgebra.eigvals!), t::AbstractTensorMap; @@ -165,75 +159,6 @@ function uppertriangularind(A::AbstractMatrix) return I end -function eig_pullback!(ΔA::AbstractMatrix, D::AbstractVector, V::AbstractMatrix, ΔD, ΔV; - tol::Real=default_pullback_gaugetol(D)) - - # Basic size checks and determination - n = LinearAlgebra.checksquare(V) - n == length(D) || throw(DimensionMismatch()) - - if !(ΔV isa AbstractZero) - VdΔV = V' * ΔV - - mask = abs.(transpose(D) .- D) .< tol - Δgauge = norm(view(VdΔV, mask), Inf) - Δgauge < tol || - @warn "`eig` cotangents sensitive to gauge choice: (|Δgauge| = $Δgauge)" - - VdΔV .*= conj.(safe_inv.(transpose(D) .- D, tol)) - - if !(ΔD isa AbstractZero) - view(VdΔV, diagind(VdΔV)) .+= ΔD - end - PΔV = V' \ VdΔV - if eltype(ΔA) <: Real - ΔAc = mul!(VdΔV, PΔV, V') # recycle VdΔV memory - ΔA .= real.(ΔAc) - else - mul!(ΔA, PΔV, V') - end - else - PΔV = V' \ Diagonal(ΔD) - if eltype(ΔA) <: Real - ΔAc = PΔV * V' - ΔA .= real.(ΔAc) - else - mul!(ΔA, PΔV, V') - end - end - return ΔA -end - -function eigh_pullback!(ΔA::AbstractMatrix, D::AbstractVector, V::AbstractMatrix, ΔD, ΔV; - tol::Real=default_pullback_gaugetol(D)) - - # Basic size checks and determination - n = LinearAlgebra.checksquare(V) - n == length(D) || throw(DimensionMismatch()) - - if !(ΔV isa AbstractZero) - VdΔV = V' * ΔV - aVdΔV = rmul!(VdΔV - VdΔV', 1 / 2) - - mask = abs.(D' .- D) .< tol - Δgauge = norm(view(aVdΔV, mask)) - Δgauge < tol || - @warn "`eigh` cotangents sensitive to gauge choice: (|Δgauge| = $Δgauge)" - - aVdΔV .*= safe_inv.(D' .- D, tol) - - if !(ΔD isa AbstractZero) - view(aVdΔV, diagind(aVdΔV)) .+= real.(ΔD) - # in principle, ΔD is real, but maybe not if coming from an anyonic tensor - end - # recylce VdΔV space - mul!(ΔA, mul!(VdΔV, V, aVdΔV), V') - else - mul!(ΔA, V * Diagonal(ΔD), V') - end - return ΔA -end - function qr_pullback!(ΔA::AbstractMatrix, Q::AbstractMatrix, R::AbstractMatrix, ΔQ, ΔR; tol::Real=default_pullback_gaugetol(R)) Rd = view(R, diagind(R)) From 5ed31b6b0b5e3494f84d1fca733b9acec7eff694 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 12 Jun 2025 17:41:48 -0400 Subject: [PATCH 38/70] Implement `isposdef` --- src/tensors/factorizations/factorizations.jl | 19 ------------------- src/tensors/factorizations/interface.jl | 14 +++++++++++++- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/tensors/factorizations/factorizations.jl b/src/tensors/factorizations/factorizations.jl index a83b77a35..104cd7a4e 100644 --- a/src/tensors/factorizations/factorizations.jl +++ b/src/tensors/factorizations/factorizations.jl @@ -45,25 +45,6 @@ include("matrixalgebrakit.jl") include("truncation.jl") include("deprecations.jl") -""" - isposdef(t::AbstractTensor, (leftind, rightind)::Index2Tuple) -> ::Bool - -Test whether a tensor `t` is positive definite as linear map from `rightind` to `leftind`. - -If `leftind` and `rightind` are not specified, the current partition of left and right -indices of `t` is used. In that case, less memory is allocated if one allows the data in -`t` to be destroyed/overwritten, by using `isposdef!(t)`. Note that the permuted tensor on -which `isposdef!` is called should have equal domain and codomain, as otherwise it is -meaningless. -""" -function LinearAlgebra.isposdef(t::AbstractTensorMap, (p₁, p₂)::Index2Tuple) - tcopy = permutedcopy_oftype(t, factorisation_scalartype(isposdef, t), p) - return isposdef!(tcopy) -end -function LinearAlgebra.isposdef(t::AbstractTensorMap) - tcopy = copy_oftype(t, float(scalartype(t))) - return isposdef!(tcopy) -end function isisometry(t::AbstractTensorMap, (p₁, p₂)::Index2Tuple) t = permute(t, (p₁, p₂); copy=false) diff --git a/src/tensors/factorizations/interface.jl b/src/tensors/factorizations/interface.jl index 821bc15c3..56d462887 100644 --- a/src/tensors/factorizations/interface.jl +++ b/src/tensors/factorizations/interface.jl @@ -217,9 +217,21 @@ matrices. See the corresponding documentation for more information. See also [`eig(!)`](@ref eig) and [`eigh(!)`](@ref) """ eigen(::AbstractTensorMap), eigen!(::AbstractTensorMap) +@doc """ + isposdef(t::AbstractTensor, [(leftind, rightind)::Index2Tuple]) -> ::Bool + +Test whether a tensor `t` is positive definite as linear map from `rightind` to `leftind`. + +If `leftind` and `rightind` are not specified, the current partition of left and right +indices of `t` is used. In that case, less memory is allocated if one allows the data in +`t` to be destroyed/overwritten, by using `isposdef!(t)`. Note that the permuted tensor on +which `isposdef!` is called should have equal domain and codomain, as otherwise it is +meaningless. +""" isposdef(::AbstractTensorMap), isposdef!(::AbstractTensorMap) + for f in (:tsvd, :eig, :eigh, :eigen, :leftorth, :rightorth, :leftpolar, :rightpolar, :leftnull, - :rightnull) + :rightnull, :isposdef) f! = Symbol(f, :!) @eval function $f(t::AbstractTensorMap, p::Index2Tuple; kwargs...) tcopy = permutedcopy_oftype(t, factorisation_scalartype($f, t), p) From e538bc1f86db5fa705048fe55fdbd0c0b4097587 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 12 Jun 2025 17:55:30 -0400 Subject: [PATCH 39/70] Fix imports --- ext/TensorKitChainRulesCoreExt/TensorKitChainRulesCoreExt.jl | 5 +++++ ext/TensorKitChainRulesCoreExt/factorizations.jl | 2 -- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ext/TensorKitChainRulesCoreExt/TensorKitChainRulesCoreExt.jl b/ext/TensorKitChainRulesCoreExt/TensorKitChainRulesCoreExt.jl index 16c7583d1..98be06676 100644 --- a/ext/TensorKitChainRulesCoreExt/TensorKitChainRulesCoreExt.jl +++ b/ext/TensorKitChainRulesCoreExt/TensorKitChainRulesCoreExt.jl @@ -3,6 +3,7 @@ module TensorKitChainRulesCoreExt using TensorOperations using VectorInterface using TensorKit +using TensorKit: foreachblock using ChainRulesCore using LinearAlgebra using TupleTools @@ -11,6 +12,10 @@ import TensorOperations as TO using TensorOperations: promote_contract, tensoralloc_add, tensoralloc_contract using VectorInterface: promote_scale, promote_add +using MatrixAlgebraKit +using MatrixAlgebraKit: TruncationStrategy, + svd_compact_pullback!, eig_full_pullback!, eigh_full_pullback! + include("utility.jl") include("constructors.jl") include("linalg.jl") diff --git a/ext/TensorKitChainRulesCoreExt/factorizations.jl b/ext/TensorKitChainRulesCoreExt/factorizations.jl index 7ba026c95..fe0f31515 100644 --- a/ext/TensorKitChainRulesCoreExt/factorizations.jl +++ b/ext/TensorKitChainRulesCoreExt/factorizations.jl @@ -1,5 +1,3 @@ -using MatrixAlgebraKit: svd_compact_pullback!, eig_full_pullback!, eigh_full_pullback! - # Factorizations rules # -------------------- function ChainRulesCore.rrule(::typeof(TensorKit.tsvd!), t::AbstractTensorMap; From ee4fb1c655bd87042591dc51d32997fb490b4612 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 12 Jun 2025 17:55:39 -0400 Subject: [PATCH 40/70] Update tests and fixes Small fixes --- .../factorizations.jl | 8 +++---- src/tensors/factorizations/factorizations.jl | 1 - test/ad.jl | 24 +++++++++---------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/ext/TensorKitChainRulesCoreExt/factorizations.jl b/ext/TensorKitChainRulesCoreExt/factorizations.jl index fe0f31515..b80f0b2cd 100644 --- a/ext/TensorKitChainRulesCoreExt/factorizations.jl +++ b/ext/TensorKitChainRulesCoreExt/factorizations.jl @@ -5,7 +5,7 @@ function ChainRulesCore.rrule(::typeof(TensorKit.tsvd!), t::AbstractTensorMap; kwargs...) # TODO: I think we can use tsvd! here without issues because we don't actually require # the data of `t` anymore. - USVᴴ = tsvd(t; trunc=TensorKit.notrunc(), alg) + USVᴴ = tsvd(t; trunc=TensorKit.notrunc(), kwargs...) if trunc != TensorKit.notrunc() && !isempty(blocksectors(t)) USVᴴ′ = MatrixAlgebraKit.truncate!(svd_trunc!, USVᴴ, trunc) @@ -16,7 +16,7 @@ function ChainRulesCore.rrule(::typeof(TensorKit.tsvd!), t::AbstractTensorMap; function tsvd!_pullback(ΔUSVᴴ′) ΔUSVᴴ = unthunk.(ΔUSVᴴ′) Δt = similar(t) - foreachblock(Δt) do (c, b) + foreachblock(Δt) do c, (b,) USVᴴc = block.(USVᴴ, Ref(c)) ΔUSVᴴc = block.(ΔUSVᴴ, Ref(c)) svd_compact_pullback!(b, USVᴴc, ΔUSVᴴc) @@ -49,7 +49,7 @@ function ChainRulesCore.rrule(::typeof(TensorKit.eig!), t::AbstractTensorMap; kw function eig!_pullback(ΔDV′) ΔDV = unthunk.(ΔDV′) Δt = similar(t) - foreachblock(Δt) do (c, b) + foreachblock(Δt) do c, (b,) DVc = block.(DV, Ref(c)) ΔDVc = block.(ΔDV, Ref(c)) eig_full_pullback!(b, DVc, ΔDVc) @@ -68,7 +68,7 @@ function ChainRulesCore.rrule(::typeof(TensorKit.eigh!), t::AbstractTensorMap; k function eigh!_pullback(ΔDV′) ΔDV = unthunk.(ΔDV′) Δt = similar(t) - foreachblock(Δt) do (c, b) + foreachblock(Δt) do c, (b,) DVc = block.(DV, Ref(c)) ΔDVc = block.(ΔDV, Ref(c)) eigh_full_pullback!(b, DVc, ΔDVc) diff --git a/src/tensors/factorizations/factorizations.jl b/src/tensors/factorizations/factorizations.jl index 104cd7a4e..2684d3133 100644 --- a/src/tensors/factorizations/factorizations.jl +++ b/src/tensors/factorizations/factorizations.jl @@ -45,7 +45,6 @@ include("matrixalgebrakit.jl") include("truncation.jl") include("deprecations.jl") - function isisometry(t::AbstractTensorMap, (p₁, p₂)::Index2Tuple) t = permute(t, (p₁, p₂); copy=false) return isisometry(t) diff --git a/test/ad.jl b/test/ad.jl index e5e2d884d..9f5eb2a5b 100644 --- a/test/ad.jl +++ b/test/ad.jl @@ -398,7 +398,7 @@ Vlist = ((ℂ^2, (ℂ^3)', ℂ^3, ℂ^2, (ℂ^2)'), test_rrule(eigh′, H; atol, output_tangent=(ΔD, ΔU)) end - let (U, S, V, ϵ) = tsvd(A) + let (U, S, V) = tsvd(A) ΔU = randn(scalartype(U), space(U)) ΔS = randn(scalartype(S), space(S)) ΔV = randn(scalartype(V), space(V)) @@ -408,54 +408,54 @@ Vlist = ((ℂ^2, (ℂ^3)', ℂ^3, ℂ^2, (ℂ^2)'), mul!(block(ΔU, c), block(U, c), Diagonal(imag(diag(b))), -im, 1) end end - test_rrule(tsvd, A; atol, output_tangent=(ΔU, ΔS, ΔV, 0.0)) + test_rrule(tsvd, A; atol, output_tangent=(ΔU, ΔS, ΔV)) allS = mapreduce(x -> diag(x[2]), vcat, blocks(S)) truncval = (maximum(allS) + minimum(allS)) / 2 - U, S, V, ϵ = tsvd(A; trunc=truncerr(truncval)) + U, S, V = tsvd(A; trunc=truncerr(truncval)) ΔU = randn(scalartype(U), space(U)) ΔS = randn(scalartype(S), space(S)) ΔV = randn(scalartype(V), space(V)) T <: Complex && remove_svdgauge_depence!(ΔU, ΔV, U, S, V) - test_rrule(tsvd, A; atol, output_tangent=(ΔU, ΔS, ΔV, 0.0), + test_rrule(tsvd, A; atol, output_tangent=(ΔU, ΔS, ΔV), fkwargs=(; trunc=truncerr(truncval))) end - let (U, S, V, ϵ) = tsvd(B) + let (U, S, V) = tsvd(B) ΔU = randn(scalartype(U), space(U)) ΔS = randn(scalartype(S), space(S)) ΔV = randn(scalartype(V), space(V)) T <: Complex && remove_svdgauge_depence!(ΔU, ΔV, U, S, V) - test_rrule(tsvd, B; atol, output_tangent=(ΔU, ΔS, ΔV, 0.0)) + test_rrule(tsvd, B; atol, output_tangent=(ΔU, ΔS, ΔV)) Vtrunc = spacetype(S)(TensorKit.SectorDict(c => ceil(Int, size(b, 1) / 2) for (c, b) in blocks(S))) - U, S, V, ϵ = tsvd(B; trunc=truncspace(Vtrunc)) + U, S, V = tsvd(B; trunc=truncspace(Vtrunc)) ΔU = randn(scalartype(U), space(U)) ΔS = randn(scalartype(S), space(S)) ΔV = randn(scalartype(V), space(V)) T <: Complex && remove_svdgauge_depence!(ΔU, ΔV, U, S, V) - test_rrule(tsvd, B; atol, output_tangent=(ΔU, ΔS, ΔV, 0.0), + test_rrule(tsvd, B; atol, output_tangent=(ΔU, ΔS, ΔV), fkwargs=(; trunc=truncspace(Vtrunc))) end - let (U, S, V, ϵ) = tsvd(C) + let (U, S, V) = tsvd(C) ΔU = randn(scalartype(U), space(U)) ΔS = randn(scalartype(S), space(S)) ΔV = randn(scalartype(V), space(V)) T <: Complex && remove_svdgauge_depence!(ΔU, ΔV, U, S, V) - test_rrule(tsvd, C; atol, output_tangent=(ΔU, ΔS, ΔV, 0.0)) + test_rrule(tsvd, C; atol, output_tangent=(ΔU, ΔS, ΔV)) c, = TensorKit.MatrixAlgebra._argmax(x -> sqrt(dim(x[1])) * maximum(diag(x[2])), blocks(S)) trunc = truncdim(round(Int, 2 * dim(c))) - U, S, V, ϵ = tsvd(C; trunc) + U, S, V = tsvd(C; trunc) ΔU = randn(scalartype(U), space(U)) ΔS = randn(scalartype(S), space(S)) ΔV = randn(scalartype(V), space(V)) T <: Complex && remove_svdgauge_depence!(ΔU, ΔV, U, S, V) - test_rrule(tsvd, C; atol, output_tangent=(ΔU, ΔS, ΔV, 0.0), fkwargs=(; trunc)) + test_rrule(tsvd, C; atol, output_tangent=(ΔU, ΔS, ΔV), fkwargs=(; trunc)) end let D = LinearAlgebra.eigvals(C) From 3073b90b4bf7a15f27fcab092f228c541de400ad Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 12 Jun 2025 22:59:27 -0400 Subject: [PATCH 41/70] Clean up tests --- test/factorizations.jl | 340 ----------------------------------------- test/paul.jl | 65 -------- test/runtests.jl | 8 +- 3 files changed, 4 insertions(+), 409 deletions(-) delete mode 100644 test/factorizations.jl delete mode 100644 test/paul.jl diff --git a/test/factorizations.jl b/test/factorizations.jl deleted file mode 100644 index 17642aa93..000000000 --- a/test/factorizations.jl +++ /dev/null @@ -1,340 +0,0 @@ -using TestEnv; -TestEnv.activate(); - -@testsnippet Setup begin - using Test - using TestExtras - using Random - using TensorKit - using Combinatorics - using TensorKit: ProductSector, fusiontensor, pentagon_equation, hexagon_equation - using TensorOperations - using Base.Iterators: take, product - # using SUNRepresentations: SUNIrrep - # const SU3Irrep = SUNIrrep{3} - using LinearAlgebra: LinearAlgebra - using Zygote: Zygote - using MatrixAlgebraKit - - const TK = TensorKit - - Random.seed!(1234) - - smallset(::Type{I}) where {I<:Sector} = take(values(I), 5) - function smallset(::Type{ProductSector{Tuple{I1,I2}}}) where {I1,I2} - iter = product(smallset(I1), smallset(I2)) - s = collect(i ⊠ j for (i, j) in iter if dim(i) * dim(j) <= 6) - return length(s) > 6 ? rand(s, 6) : s - end - function smallset(::Type{ProductSector{Tuple{I1,I2,I3}}}) where {I1,I2,I3} - iter = product(smallset(I1), smallset(I2), smallset(I3)) - s = collect(i ⊠ j ⊠ k for (i, j, k) in iter if dim(i) * dim(j) * dim(k) <= 6) - return length(s) > 6 ? rand(s, 6) : s - end - function randsector(::Type{I}) where {I<:Sector} - s = collect(smallset(I)) - a = rand(s) - while a == one(a) # don't use trivial label - a = rand(s) - end - return a - end - function hasfusiontensor(I::Type{<:Sector}) - try - fusiontensor(one(I), one(I), one(I)) - return true - catch e - if e isa MethodError - return false - else - rethrow(e) - end - end - end - - # spaces - Vtr = (ℂ^3, - (ℂ^4)', - ℂ^5, - ℂ^6, - (ℂ^7)') - Vℤ₂ = (ℂ[Z2Irrep](0 => 1, 1 => 1), - ℂ[Z2Irrep](0 => 1, 1 => 2)', - ℂ[Z2Irrep](0 => 3, 1 => 2)', - ℂ[Z2Irrep](0 => 2, 1 => 3), - ℂ[Z2Irrep](0 => 2, 1 => 5)) - Vfℤ₂ = (ℂ[FermionParity](0 => 1, 1 => 1), - ℂ[FermionParity](0 => 1, 1 => 2)', - ℂ[FermionParity](0 => 3, 1 => 2)', - ℂ[FermionParity](0 => 2, 1 => 3), - ℂ[FermionParity](0 => 2, 1 => 5)) - Vℤ₃ = (ℂ[Z3Irrep](0 => 1, 1 => 2, 2 => 2), - ℂ[Z3Irrep](0 => 3, 1 => 1, 2 => 1), - ℂ[Z3Irrep](0 => 2, 1 => 2, 2 => 1)', - ℂ[Z3Irrep](0 => 1, 1 => 2, 2 => 3), - ℂ[Z3Irrep](0 => 1, 1 => 3, 2 => 3)') - VU₁ = (ℂ[U1Irrep](0 => 1, 1 => 2, -1 => 2), - ℂ[U1Irrep](0 => 3, 1 => 1, -1 => 1), - ℂ[U1Irrep](0 => 2, 1 => 2, -1 => 1)', - ℂ[U1Irrep](0 => 1, 1 => 2, -1 => 3), - ℂ[U1Irrep](0 => 1, 1 => 3, -1 => 3)') - VfU₁ = (ℂ[FermionNumber](0 => 1, 1 => 2, -1 => 2), - ℂ[FermionNumber](0 => 3, 1 => 1, -1 => 1), - ℂ[FermionNumber](0 => 2, 1 => 2, -1 => 1)', - ℂ[FermionNumber](0 => 1, 1 => 2, -1 => 3), - ℂ[FermionNumber](0 => 1, 1 => 3, -1 => 3)') - VCU₁ = (ℂ[CU1Irrep]((0, 0) => 1, (0, 1) => 2, 1 => 1), - ℂ[CU1Irrep]((0, 0) => 3, (0, 1) => 0, 1 => 1), - ℂ[CU1Irrep]((0, 0) => 1, (0, 1) => 0, 1 => 2)', - ℂ[CU1Irrep]((0, 0) => 2, (0, 1) => 2, 1 => 1), - ℂ[CU1Irrep]((0, 0) => 2, (0, 1) => 1, 1 => 2)') - VSU₂ = (ℂ[SU2Irrep](0 => 3, 1 // 2 => 1), - ℂ[SU2Irrep](0 => 2, 1 => 1), - ℂ[SU2Irrep](1 // 2 => 1, 1 => 1)', - ℂ[SU2Irrep](0 => 2, 1 // 2 => 2), - ℂ[SU2Irrep](0 => 1, 1 // 2 => 1, 3 // 2 => 1)') - VfSU₂ = (ℂ[FermionSpin](0 => 3, 1 // 2 => 1), - ℂ[FermionSpin](0 => 2, 1 => 1), - ℂ[FermionSpin](1 // 2 => 1, 1 => 1)', - ℂ[FermionSpin](0 => 2, 1 // 2 => 2), - ℂ[FermionSpin](0 => 1, 1 // 2 => 1, 3 // 2 => 1)') - for V in (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂)#, VSU₃) - V1, V2, V3, V4, V5 = V - - @assert V3 * V4 * V2 ≿ V1' * V5' # necessary for leftorth tests - @assert V3 * V4 ≾ V1' * V2' * V5' # necessary for rightorth tests - end - - spacelist = try - if ENV["CI"] == "true" - println("Detected running on CI") - if Sys.iswindows() - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂) - elseif Sys.isapple() - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VfU₁, VfSU₂)#, VSU₃) - else - (Vtr, Vℤ₂, Vfℤ₂, VU₁, VCU₁, VSU₂, VfSU₂)#, VSU₃) - end - else - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂)#, VSU₃) - end - catch - (Vtr, Vℤ₂, Vfℤ₂, Vℤ₃, VU₁, VfU₁, VCU₁, VSU₂, VfSU₂)#, VSU₃) - end -end - -@testitem "left orth" setup = [Setup] begin - function test_leftorth(t, p, alg) - Q, R = @inferred leftorth(t, p; alg) - @test Q * R ≈ permute(t, p) - @test isisometry(Q) - end - - p = ((3, 4, 2), (1, 5)) - elts = (Float32, ComplexF64) - algs = (TensorKit.QR(), TensorKit.QRpos(), TensorKit.QL(), TensorKit.QLpos(), - TensorKit.Polar(), TensorKit.SVD(), TensorKit.SDD()) - - testname(V) = "symmetry: $(TensorKit.type_repr(sectortype(first(V))))" - @timedtestset "$(testname(V))" for V in spacelist - W = ⊗(V...) - for T in elts, alg in algs - t = rand(T, W) - test_leftorth(t, p, alg) - tᴴ = t' - test_leftorth(tᴴ, p, alg) - end - end -end - -function test_leftnull(t, p, alg) - N = @inferred leftnull(t, p; alg) - @test isisometry(N) - @test norm(N' * permute(t, p)) ≈ 0 atol = 100 * eps(norm(t)) -end - -# @timedtestset "Factorizations with symmetry: $(sectortype(first(V)))" for V in spacelist -V = collect(spacelist)[1] -V1, V2, V3, V4, V5 = V -W = V1 ⊗ V2 ⊗ V3 ⊗ V4 ⊗ V5 -for T in (Float32, ComplexF64), adj in (false, true) - t = adj ? rand(T, W)' : rand(T, W) - @testset "leftorth with $alg" for alg in - (TensorKit.QR(), TensorKit.QRpos(), TensorKit.QL(), - TensorKit.QLpos(), TensorKit.Polar(), - TensorKit.SVD(), TensorKit.SDD()) - test_leftorth(t, ((3, 4, 2), (1, 5)), alg) - end - @testset "leftnull with $alg" for alg in - (TensorKit.QR(), TensorKit.SVD(), TensorKit.SDD()) - test_leftnull(t, ((3, 4, 2), (1, 5)), alg) - end - @testset "rightorth with $alg" for alg in - (TensorKit.RQ(), TensorKit.RQpos(), - TensorKit.LQ(), TensorKit.LQpos(), - TensorKit.Polar(), TensorKit.SVD(), - TensorKit.SDD()) - L, Q = @constinferred rightorth(t, ((3, 4), (2, 1, 5)); alg=alg) - QQd = Q * Q' - @test QQd ≈ one(QQd) - @test L * Q ≈ permute(t, ((3, 4), (2, 1, 5))) - if alg isa Polar - @test isposdef(L) - @test domain(L) == codomain(L) == space(t, 3) ⊗ space(t, 4) - end - end - @testset "rightnull with $alg" for alg in - (TensorKit.LQ(), TensorKit.SVD(), - TensorKit.SDD()) - M = @constinferred rightnull(t, ((3, 4), (2, 1, 5)); alg=alg) - MMd = M * M' - @test MMd ≈ one(MMd) - @test norm(permute(t, ((3, 4), (2, 1, 5))) * M') < - 100 * eps(norm(t)) - end - @testset "tsvd with $alg" for alg in (TensorKit.SVD(), TensorKit.SDD()) - U, S, V = @constinferred tsvd(t, ((3, 4, 2), (1, 5)); alg=alg) - UdU = U' * U - @test UdU ≈ one(UdU) - VVd = V * V' - @test VVd ≈ one(VVd) - t2 = permute(t, ((3, 4, 2), (1, 5))) - @test U * S * V ≈ t2 - - s = LinearAlgebra.svdvals(t2) - s′ = LinearAlgebra.diag(S) - for (c, b) in s - @test b ≈ s′[c] - end - end - @testset "cond and rank" begin - t2 = permute(t, ((3, 4, 2), (1, 5))) - d1 = dim(codomain(t2)) - d2 = dim(domain(t2)) - @test rank(t2) == min(d1, d2) - M = leftnull(t2) - @test rank(M) == max(d1, d2) - min(d1, d2) - t3 = unitary(T, V1 ⊗ V2, V1 ⊗ V2) - @test cond(t3) ≈ one(real(T)) - @test rank(t3) == dim(V1 ⊗ V2) - t4 = randn(T, V1 ⊗ V2, V1 ⊗ V2) - t4 = (t4 + t4') / 2 - vals = LinearAlgebra.eigvals(t4) - λmax = maximum(s -> maximum(abs, s), values(vals)) - λmin = minimum(s -> minimum(abs, s), values(vals)) - @test cond(t4) ≈ λmax / λmin - end -end - -@testset "empty tensor" begin - for T in (Float32, ComplexF64) - T = Float64 - t = randn(T, V1 ⊗ V2, zero(V1)) - @testset "leftorth with $alg" for alg in - (TensorKit.QR(), TensorKit.QRpos(), - TensorKit.QL(), TensorKit.QLpos(), - TensorKit.Polar(), TensorKit.SVD(), - TensorKit.SDD()) - Q, R = @constinferred leftorth(t; alg=alg) - @test Q == t - @test dim(Q) == dim(R) == 0 - end - @testset "leftnull with $alg" for alg in - (TensorKit.QR(), TensorKit.SVD(), - TensorKit.SDD()) - N = @constinferred leftnull(t; alg=alg) - @test N' * N ≈ id(domain(N)) - @test N * N' ≈ id(codomain(N)) - end - @testset "rightorth with $alg" for alg in - (TensorKit.RQ(), TensorKit.RQpos(), - TensorKit.LQ(), TensorKit.LQpos(), - TensorKit.Polar(), TensorKit.SVD(), - TensorKit.SDD()) - L, Q = @constinferred rightorth(copy(t'); alg=alg) - @test Q == t' - @test dim(Q) == dim(L) == 0 - end - @testset "rightnull with $alg" for alg in - (TensorKit.LQ(), TensorKit.SVD(), - TensorKit.SDD()) - M = @constinferred rightnull(copy(t'); alg=alg) - @test M * M' ≈ id(codomain(M)) - @test M' * M ≈ id(domain(M)) - end - @testset "tsvd with $alg" for alg in (TensorKit.SVD(), TensorKit.SDD()) - U, S, V = @constinferred tsvd(t; alg=alg) - @test U == t - @test dim(U) == dim(S) == dim(V) - end - @testset "cond and rank" begin - @test rank(t) == 0 - W2 = zero(V1) * zero(V2) - t2 = rand(W2, W2) - @test rank(t2) == 0 - @test cond(t2) == 0.0 - end - end -end -@testset "eig and isposdef" begin - for T in (Float32, ComplexF64) - t = rand(T, V1 ⊗ V1' ⊗ V2 ⊗ V2') - D, V = eigen(t, ((1, 3), (2, 4))) - t2 = permute(t, ((1, 3), (2, 4))) - @test t2 * V ≈ V * D - - d = LinearAlgebra.eigvals(t2; sortby=nothing) - d′ = LinearAlgebra.diag(D) - for (c, b) in d - @test b ≈ d′[c] - end - - # Somehow moving these test before the previous one gives rise to errors - # with T=Float32 on x86 platforms. Is this an OpenBLAS issue? - VdV = V' * V - VdV = (VdV + VdV') / 2 - @test isposdef(VdV) - - @test !isposdef(t2) # unlikely for non-hermitian map - t2 = (t2 + t2') - D, V = eigen(t2) - VdV = V' * V - @test VdV ≈ one(VdV) - D̃, Ṽ = @constinferred eigh(t2) - @test D ≈ D̃ - @test V ≈ Ṽ - λ = minimum(minimum(real(LinearAlgebra.diag(b))) - for (c, b) in blocks(D)) - @test cond(Ṽ) ≈ one(real(T)) - @test isposdef(t2) == isposdef(λ) - @test isposdef(t2 - λ * one(t2) + 0.1 * one(t2)) - @test !isposdef(t2 - λ * one(t2) - 0.1 * one(t2)) - end -end -@testset "Tensor truncation" begin - for T in (Float32, ComplexF64), p in (1, 2, 3, Inf), adj in (false, true) - t = adj ? rand(T, V1 ⊗ V2 ⊗ V3, V4 ⊗ V5) : rand(T, V4 ⊗ V5, V1 ⊗ V2 ⊗ V3)' - - U₀, S₀, V₀, = tsvd(t) - t = rmul!(t, 1 / norm(S₀, p)) - U, S, V, ϵ = @constinferred tsvd(t; trunc=truncerr(5e-1), p=p) - # @show p, ϵ - # @show domain(S) - # @test min(space(S,1), space(S₀,1)) != space(S₀,1) - U′, S′, V′, ϵ′ = tsvd(t; trunc=truncerr(nextfloat(ϵ)), p=p) - @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) - U′, S′, V′, ϵ′ = tsvd(t; trunc=truncdim(ceil(Int, dim(domain(S)))), - p=p) - @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) - U′, S′, V′, ϵ′ = tsvd(t; trunc=truncspace(space(S, 1)), p=p) - @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) - # results with truncationcutoff cannot be compared because they don't take degeneracy into account, and thus truncate differently - U, S, V, ϵ = tsvd(t; trunc=truncbelow(1 / dim(domain(S₀))), p=p) - # @show p, ϵ - # @show domain(S) - # @test min(space(S,1), space(S₀,1)) != space(S₀,1) - U′, S′, V′, ϵ′ = tsvd(t; trunc=truncspace(space(S, 1)), p=p) - @test (U, S, V, ϵ) == (U′, S′, V′, ϵ′) - end -end -# end diff --git a/test/paul.jl b/test/paul.jl deleted file mode 100644 index 249ed1bae..000000000 --- a/test/paul.jl +++ /dev/null @@ -1,65 +0,0 @@ -using Zygote, TensorKit - -_safe_pow(a::Real, pow::Real, tol::Real) = (pow < 0 && abs(a) < tol) ? zero(a) : a^pow - -# Element-wise multiplication of TensorMaps respecting block structure -function _elementwise_mult(a₁::AbstractTensorMap, a₂::AbstractTensorMap) - dst = similar(a₁) - for (k, b) in blocks(dst) - copyto!(b, block(a₁, k) .* block(a₂, k)) - end - return dst -end -""" - sdiag_pow(s, pow::Real; tol::Real=eps(scalartype(s))^(3 / 4)) - -Compute `s^pow` for a diagonal matrix `s`. -""" -function sdiag_pow(s::DiagonalTensorMap, pow::Real; tol::Real=eps(scalartype(s))^(3 / 4)) - # Relative tol w.r.t. largest singular value (use norm(∘, Inf) to make differentiable) - tol *= norm(s, Inf) - spow = DiagonalTensorMap(_safe_pow.(s.data, pow, tol), space(s, 1)) - return spow -end -function sdiag_pow(s::AbstractTensorMap{T,S,1,1}, pow::Real; - tol::Real=eps(scalartype(s))^(3 / 4)) where {T,S} - # Relative tol w.r.t. largest singular value (use norm(∘, Inf) to make differentiable) - tol *= norm(s, Inf) - spow = similar(s) - for (k, b) in blocks(s) - copyto!(block(spow, k), - LinearAlgebra.diagm(_safe_pow.(LinearAlgebra.diag(b), pow, tol))) - end - return spow -end - -function ChainRulesCore.rrule(::typeof(sdiag_pow), - s::AbstractTensorMap, - pow::Real; - tol::Real=eps(scalartype(s))^(3 / 4),) - tol *= norm(s, Inf) - spow = sdiag_pow(s, pow; tol) - spow_minus1_conj = scale!(sdiag_pow(s', pow - 1; tol), pow) - function sdiag_pow_pullback(c̄_) - c̄ = unthunk(c̄_) - return (ChainRulesCore.NoTangent(), _elementwise_mult(c̄, spow_minus1_conj)) - end - return spow, sdiag_pow_pullback -end - -function svd_fixed_point(A, U, S, V) - S⁻¹ = sdiag_pow(S, -1) - return (A * V' * S⁻¹ - U, DiagonalTensorMap(U' * A * V' * S⁻¹) - one(S), - S⁻¹ * U' * A - V) -end - -using Zygote - -V = ComplexSpace(3)^2 -A = randn(ComplexF64, V, V) -U, S, V = tsvd(A) - -Zygote.gradient(A, U, S, V) do A, U, S, V - du, ds, dv = svd_fixed_point(A, U, S, V) - return norm(du) + norm(ds) + norm(dv) -end diff --git a/test/runtests.jl b/test/runtests.jl index 258fe77f3..2e716fa18 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -107,11 +107,11 @@ VfSU₂ = (ℂ[FermionSpin](0 => 3, 1 // 2 => 1), # ℂ[SU3Irrep]((0, 0, 0) => 1, (1, 0, 0) => 1, (1, 1, 0) => 1)') Ti = time() -# include("fusiontrees.jl") -# include("spaces.jl") +include("fusiontrees.jl") +include("spaces.jl") include("tensors.jl") -# include("diagonal.jl") -# include("planar.jl") +include("diagonal.jl") +include("planar.jl") # TODO: remove once we know AD is slow on macOS CI if !(Sys.isapple() && get(ENV, "CI", "false") == "true") include("ad.jl") From 2a6a7d3734182e960bebb53c3350d08a6dfe3688 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sun, 15 Jun 2025 13:22:07 -0400 Subject: [PATCH 42/70] Bump minimal MatrixAlgebraKit version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 92e90a4c9..881c74926 100644 --- a/Project.toml +++ b/Project.toml @@ -34,7 +34,7 @@ Combinatorics = "1" FiniteDifferences = "0.12" LRUCache = "1.0.2" LinearAlgebra = "1" -MatrixAlgebraKit = "0.2" +MatrixAlgebraKit = "0.2.5" OhMyThreads = "0.8.0" PackageExtensionCompat = "1" Random = "1" From 54fef8132b709fe11aa9336bc017a65a85b4e08b Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sun, 15 Jun 2025 13:40:43 -0400 Subject: [PATCH 43/70] Fix uninitialized cotangents --- ext/TensorKitChainRulesCoreExt/factorizations.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ext/TensorKitChainRulesCoreExt/factorizations.jl b/ext/TensorKitChainRulesCoreExt/factorizations.jl index b80f0b2cd..de4dcbded 100644 --- a/ext/TensorKitChainRulesCoreExt/factorizations.jl +++ b/ext/TensorKitChainRulesCoreExt/factorizations.jl @@ -15,7 +15,7 @@ function ChainRulesCore.rrule(::typeof(TensorKit.tsvd!), t::AbstractTensorMap; function tsvd!_pullback(ΔUSVᴴ′) ΔUSVᴴ = unthunk.(ΔUSVᴴ′) - Δt = similar(t) + Δt = zerovector(t) foreachblock(Δt) do c, (b,) USVᴴc = block.(USVᴴ, Ref(c)) ΔUSVᴴc = block.(ΔUSVᴴ, Ref(c)) @@ -48,7 +48,7 @@ function ChainRulesCore.rrule(::typeof(TensorKit.eig!), t::AbstractTensorMap; kw function eig!_pullback(ΔDV′) ΔDV = unthunk.(ΔDV′) - Δt = similar(t) + Δt = zerovector(t) foreachblock(Δt) do c, (b,) DVc = block.(DV, Ref(c)) ΔDVc = block.(ΔDV, Ref(c)) @@ -67,7 +67,7 @@ function ChainRulesCore.rrule(::typeof(TensorKit.eigh!), t::AbstractTensorMap; k function eigh!_pullback(ΔDV′) ΔDV = unthunk.(ΔDV′) - Δt = similar(t) + Δt = zerovector(t) foreachblock(Δt) do c, (b,) DVc = block.(DV, Ref(c)) ΔDVc = block.(ΔDV, Ref(c)) From 4cdcb112b17ead84bc00e942164b041478dac715 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sun, 15 Jun 2025 13:56:57 -0400 Subject: [PATCH 44/70] Update and use `MatrixAlgebraKit.isisometry` Correctly implement `isisometry` --- src/TensorKit.jl | 2 +- src/auxiliary/linalg.jl | 2 - src/tensors/factorizations/factorizations.jl | 13 +++--- test/tensors.jl | 42 +++++--------------- 4 files changed, 20 insertions(+), 39 deletions(-) diff --git a/src/TensorKit.jl b/src/TensorKit.jl index 60992e0c4..bd65d4eba 100644 --- a/src/TensorKit.jl +++ b/src/TensorKit.jl @@ -73,7 +73,7 @@ export mul!, lmul!, rmul!, adjoint!, pinv, axpy!, axpby! export leftorth, rightorth, leftnull, rightnull, leftpolar, rightpolar, leftorth!, rightorth!, leftnull!, rightnull!, leftpolar!, rightpolar!, tsvd!, tsvd, eigen, eigen!, eig, eig!, eigh, eigh!, exp, exp!, - isposdef, isposdef!, ishermitian, isisometry, sylvester, rank, cond + isposdef, isposdef!, ishermitian, isisometry, isunitary, sylvester, rank, cond export braid, braid!, permute, permute!, transpose, transpose!, twist, twist!, repartition, repartition! export catdomain, catcodomain diff --git a/src/auxiliary/linalg.jl b/src/auxiliary/linalg.jl index 54f993d7e..4277be67f 100644 --- a/src/auxiliary/linalg.jl +++ b/src/auxiliary/linalg.jl @@ -84,8 +84,6 @@ end safesign(s::Real) = ifelse(s < zero(s), -one(s), +one(s)) safesign(s::Complex) = ifelse(iszero(s), one(s), s / abs(s)) -isisometry(A::StridedMatrix; kwargs...) = isapprox(A' * A, LinearAlgebra.I, kwargs...) - function leftorth!(A::StridedMatrix{<:BlasFloat}, alg::Union{QR,QRpos}, atol::Real) iszero(atol) || throw(ArgumentError("nonzero atol not supported by $alg")) m, n = size(A) diff --git a/src/tensors/factorizations/factorizations.jl b/src/tensors/factorizations/factorizations.jl index 2684d3133..864087d88 100644 --- a/src/tensors/factorizations/factorizations.jl +++ b/src/tensors/factorizations/factorizations.jl @@ -184,12 +184,15 @@ function LinearAlgebra.isposdef!(t::TensorMap) end # TODO: tolerances are per-block, not global or weighted - does that matter? -function isisometry(t::AbstractTensorMap; kwargs...) +function MatrixAlgebraKit.is_left_isometry(t::AbstractTensorMap; kwargs...) domain(t) ≾ codomain(t) || return false - for (_, b) in blocks(t) - MatrixAlgebra.isisometry(b; kwargs...) || return false - end - return true + f((c, b)) = MatrixAlgebraKit.is_left_isometry(b; kwargs...) + return all(f, blocks(t)) +end +function MatrixAlgebraKit.is_right_isometry(t::AbstractTensorMap; kwargs...) + domain(t) ≿ codomain(t) || return false + f((c, b)) = MatrixAlgebraKit.is_right_isometry(b; kwargs...) + return all(f, blocks(t)) end end diff --git a/test/tensors.jl b/test/tensors.jl index 00416de51..acf0e1dd4 100644 --- a/test/tensors.jl +++ b/test/tensors.jl @@ -365,9 +365,8 @@ for V in spacelist for T in (Float64, ComplexF64) t1 = randisometry(T, W1, W2) t2 = randisometry(T, W2 ← W2) - @test t1' * t1 ≈ one(t2) - @test t2' * t2 ≈ one(t2) - @test t2 * t2' ≈ one(t2) + @test isisometry(t1) + @test isunitary(t2) P = t1 * t1' @test P * P ≈ P end @@ -447,21 +446,14 @@ for V in spacelist TensorKit.Polar(), TensorKit.SVD(), TensorKit.SDD()) Q, R = @constinferred leftorth(t, ((3, 4, 2), (1, 5)); alg=alg) - QdQ = Q' * Q - @test QdQ ≈ one(QdQ) + @test isisometry(Q) @test Q * R ≈ permute(t, ((3, 4, 2), (1, 5))) - # removed since leftorth now merges legs! - # if alg isa Polar - # @test isposdef(R) - # @test domain(R) == codomain(R) == space(t, 1)' ⊗ space(t, 5)' - # end end @testset "leftnull with $alg" for alg in (TensorKit.QR(), TensorKit.SVD(), TensorKit.SDD()) N = @constinferred leftnull(t, ((3, 4, 2), (1, 5)); alg=alg) - NdN = N' * N - @test NdN ≈ one(NdN) + @test isisometry(N) @test norm(N' * permute(t, ((3, 4, 2), (1, 5)))) < 100 * eps(norm(t)) end @@ -471,30 +463,21 @@ for V in spacelist TensorKit.Polar(), TensorKit.SVD(), TensorKit.SDD()) L, Q = @constinferred rightorth(t, ((3, 4), (2, 1, 5)); alg=alg) - QQd = Q * Q' - @test QQd ≈ one(QQd) + @test isisometry(Q; side=:right) @test L * Q ≈ permute(t, ((3, 4), (2, 1, 5))) - # removed since rightorth now merges legs! - # if alg isa Polar - # @test isposdef(L) - # @test domain(L) == codomain(L) == space(t, 3) ⊗ space(t, 4) - # end end @testset "rightnull with $alg" for alg in (TensorKit.LQ(), TensorKit.SVD(), TensorKit.SDD()) M = @constinferred rightnull(t, ((3, 4), (2, 1, 5)); alg=alg) - MMd = M * M' - @test MMd ≈ one(MMd) + @test isisometry(M; side=:right) @test norm(permute(t, ((3, 4), (2, 1, 5))) * M') < 100 * eps(norm(t)) end @testset "tsvd with $alg" for alg in (TensorKit.SVD(), TensorKit.SDD()) U, S, V = @constinferred tsvd(t, ((3, 4, 2), (1, 5)); alg=alg) - UdU = U' * U - @test UdU ≈ one(UdU) - VVd = V * V' - @test VVd ≈ one(VVd) + @test isisometry(U) + @test isisometry(V; side=:right) t2 = permute(t, ((3, 4, 2), (1, 5))) @test U * S * V ≈ t2 @@ -537,8 +520,7 @@ for V in spacelist (TensorKit.QR(), TensorKit.SVD(), TensorKit.SDD()) N = @constinferred leftnull(t; alg=alg) - @test N' * N ≈ id(domain(N)) - @test N * N' ≈ id(codomain(N)) + @test isunitary(N) end @testset "rightorth with $alg" for alg in (TensorKit.RQ(), TensorKit.RQpos(), @@ -553,8 +535,7 @@ for V in spacelist (TensorKit.LQ(), TensorKit.SVD(), TensorKit.SDD()) M = @constinferred rightnull(copy(t'); alg=alg) - @test M * M' ≈ id(codomain(M)) - @test M' * M ≈ id(domain(M)) + @test isunitary(M) end @testset "tsvd with $alg" for alg in (TensorKit.SVD(), TensorKit.SDD()) U, S, V = @constinferred tsvd(t; alg=alg) @@ -590,8 +571,7 @@ for V in spacelist @test !isposdef(t2) # unlikely for non-hermitian map t2 = (t2 + t2') D, V = eigen(t2) - VdV = V' * V - @test VdV ≈ one(VdV) + @test isisometry(V) D̃, Ṽ = @constinferred eigh(t2) @test D ≈ D̃ @test V ≈ Ṽ From 816c807b576d49534f4d8386c9b9dd00680adde5 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Mon, 16 Jun 2025 19:58:32 -0400 Subject: [PATCH 45/70] Fix missing export --- src/tensors/factorizations/factorizations.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tensors/factorizations/factorizations.jl b/src/tensors/factorizations/factorizations.jl index 864087d88..95683b0d9 100644 --- a/src/tensors/factorizations/factorizations.jl +++ b/src/tensors/factorizations/factorizations.jl @@ -4,7 +4,7 @@ module Factorizations export eig, eig!, eigh, eigh! -export tsvd, tsvd!, svdvals +export tsvd, tsvd!, svdvals, svdvals! export leftorth, leftorth!, rightorth, rightorth! export leftnull, leftnull!, rightnull, rightnull! export leftpolar, leftpolar!, rightpolar, rightpolar! @@ -15,7 +15,7 @@ using ..TensorKit using ..TensorKit: AdjointTensorMap, SectorDict, OFA, blocktype, foreachblock using ..MatrixAlgebra: MatrixAlgebra -using LinearAlgebra: LinearAlgebra, BlasFloat +using LinearAlgebra: LinearAlgebra, BlasFloat, svdvals, svdvals! import LinearAlgebra: eigen, eigen!, isposdef, isposdef!, ishermitian using TensorOperations: Index2Tuple From f0a7e79a4351dba6b52875a8f25deb169e83bb1f Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Mon, 16 Jun 2025 20:11:46 -0400 Subject: [PATCH 46/70] Implement remaining factorization rrules --- .../TensorKitChainRulesCoreExt.jl | 3 +- .../factorizations.jl | 175 +++--------------- 2 files changed, 25 insertions(+), 153 deletions(-) diff --git a/ext/TensorKitChainRulesCoreExt/TensorKitChainRulesCoreExt.jl b/ext/TensorKitChainRulesCoreExt/TensorKitChainRulesCoreExt.jl index 98be06676..36bb4108d 100644 --- a/ext/TensorKitChainRulesCoreExt/TensorKitChainRulesCoreExt.jl +++ b/ext/TensorKitChainRulesCoreExt/TensorKitChainRulesCoreExt.jl @@ -14,7 +14,8 @@ using VectorInterface: promote_scale, promote_add using MatrixAlgebraKit using MatrixAlgebraKit: TruncationStrategy, - svd_compact_pullback!, eig_full_pullback!, eigh_full_pullback! + svd_compact_pullback!, eig_full_pullback!, eigh_full_pullback!, + qr_compact_pullback!, lq_compact_pullback! include("utility.jl") include("constructors.jl") diff --git a/ext/TensorKitChainRulesCoreExt/factorizations.jl b/ext/TensorKitChainRulesCoreExt/factorizations.jl index de4dcbded..95a62b24f 100644 --- a/ext/TensorKitChainRulesCoreExt/factorizations.jl +++ b/ext/TensorKitChainRulesCoreExt/factorizations.jl @@ -99,167 +99,38 @@ end function ChainRulesCore.rrule(::typeof(leftorth!), t::AbstractTensorMap; alg=QRpos()) alg isa TensorKit.QR || alg isa TensorKit.QRpos || error("only `alg=QR()` and `alg=QRpos()` are supported") - Q, R = leftorth(t; alg) - function leftorth!_pullback((_ΔQ, _ΔR)) - ΔQ, ΔR = unthunk(_ΔQ), unthunk(_ΔR) - Δt = similar(t) - for (c, b) in blocks(Δt) - qr_pullback!(b, block(Q, c), block(R, c), block(ΔQ, c), block(ΔR, c)) + QR = leftorth(t; alg) + function leftorth!_pullback(ΔQR′) + ΔQR = unthunk.(ΔQR′) + Δt = zerovector(t) + foreachblock(Δt) do c, (b,) + QRc = block.(QR, Ref(c)) + ΔQRc = block.(ΔQR, Ref(c)) + qr_compact_pullback!(b, QRc, ΔQRc) + return nothing end return NoTangent(), Δt end - leftorth!_pullback(::Tuple{ZeroTangent,ZeroTangent}) = NoTangent(), ZeroTangent() - return (Q, R), leftorth!_pullback + leftorth!_pullback(::NTuple{2,ZeroTangent}) = NoTangent(), ZeroTangent() + + return QR, leftorth!_pullback end function ChainRulesCore.rrule(::typeof(rightorth!), t::AbstractTensorMap; alg=LQpos()) alg isa TensorKit.LQ || alg isa TensorKit.LQpos || error("only `alg=LQ()` and `alg=LQpos()` are supported") - L, Q = rightorth(t; alg) - function rightorth!_pullback((_ΔL, _ΔQ)) - ΔL, ΔQ = unthunk(_ΔL), unthunk(_ΔQ) - Δt = similar(t) - for (c, b) in blocks(Δt) - lq_pullback!(b, block(L, c), block(Q, c), block(ΔL, c), block(ΔQ, c)) + LQ = rightorth(t; alg) + function rightorth!_pullback(ΔLQ′) + ΔLQ = unthunk(ΔLQ′) + Δt = zerovector(t) + foreachblock(Δt) do c, (b,) + LQc = block.(LQ, Ref(c)) + ΔLQc = block.(ΔLQ, Ref(c)) + lq_compact_pullback!(b, LQc, ΔLQc) + return nothing end return NoTangent(), Δt end - rightorth!_pullback(::Tuple{ZeroTangent,ZeroTangent}) = NoTangent(), ZeroTangent() - return (L, Q), rightorth!_pullback -end - -# Corresponding matrix factorisations: implemented as mutating methods -# --------------------------------------------------------------------- -# helper routines -safe_inv(a, tol) = abs(a) < tol ? zero(a) : inv(a) - -function lowertriangularind(A::AbstractMatrix) - m, n = size(A) - I = Vector{Int}(undef, div(m * (m - 1), 2) + m * (n - m)) - offset = 0 - for j in 1:n - r = (j + 1):m - I[offset .- j .+ r] = (j - 1) * m .+ r - offset += length(r) - end - return I -end - -function uppertriangularind(A::AbstractMatrix) - m, n = size(A) - I = Vector{Int}(undef, div(m * (m - 1), 2) + m * (n - m)) - offset = 0 - for i in 1:m - r = (i + 1):n - I[offset .- i .+ r] = i .+ m .* (r .- 1) - offset += length(r) - end - return I -end - -function qr_pullback!(ΔA::AbstractMatrix, Q::AbstractMatrix, R::AbstractMatrix, ΔQ, ΔR; - tol::Real=default_pullback_gaugetol(R)) - Rd = view(R, diagind(R)) - p = something(findlast(≥(tol) ∘ abs, Rd), 0) - m, n = size(R) - - Q1 = view(Q, :, 1:p) - R1 = view(R, 1:p, :) - R11 = view(R, 1:p, 1:p) - - ΔA1 = view(ΔA, :, 1:p) - ΔQ1 = view(ΔQ, :, 1:p) - ΔR1 = view(ΔR, 1:p, :) - - M = similar(R, (p, p)) - ΔR isa AbstractZero || mul!(M, ΔR1, R1') - ΔQ isa AbstractZero || mul!(M, Q1', ΔQ1, -1, !(ΔR isa AbstractZero)) - view(M, lowertriangularind(M)) .= conj.(view(M, uppertriangularind(M))) - if eltype(M) <: Complex - Md = view(M, diagind(M)) - Md .= real.(Md) - end - - ΔA1 .= ΔQ1 - mul!(ΔA1, Q1, M, +1, 1) - - if n > p - R12 = view(R, 1:p, (p + 1):n) - ΔA2 = view(ΔA, :, (p + 1):n) - ΔR12 = view(ΔR, 1:p, (p + 1):n) - - if ΔR isa AbstractZero - ΔA2 .= zero(eltype(ΔA)) - else - mul!(ΔA2, Q1, ΔR12) - mul!(ΔA1, ΔA2, R12', -1, 1) - end - end - if m > p && !(ΔQ isa AbstractZero) # case where R is not full rank - Q2 = view(Q, :, (p + 1):m) - ΔQ2 = view(ΔQ, :, (p + 1):m) - Q1dΔQ2 = Q1' * ΔQ2 - Δgauge = norm(mul!(copy(ΔQ2), Q1, Q1dΔQ2, -1, 1), Inf) - Δgauge < tol || - @warn "`qr` cotangents sensitive to gauge choice: (|Δgauge| = $Δgauge)" - mul!(ΔA1, Q2, Q1dΔQ2', -1, 1) - end - rdiv!(ΔA1, UpperTriangular(R11)') - return ΔA -end - -function lq_pullback!(ΔA::AbstractMatrix, L::AbstractMatrix, Q::AbstractMatrix, ΔL, ΔQ; - tol::Real=default_pullback_gaugetol(L)) - Ld = view(L, diagind(L)) - p = something(findlast(≥(tol) ∘ abs, Ld), 0) - m, n = size(L) - - L1 = view(L, :, 1:p) - L11 = view(L, 1:p, 1:p) - Q1 = view(Q, 1:p, :) - - ΔA1 = view(ΔA, 1:p, :) - ΔQ1 = view(ΔQ, 1:p, :) - ΔL1 = view(ΔL, :, 1:p) - - M = similar(L, (p, p)) - ΔL isa AbstractZero || mul!(M, L1', ΔL1) - ΔQ isa AbstractZero || mul!(M, ΔQ1, Q1', -1, !(ΔL isa AbstractZero)) - view(M, uppertriangularind(M)) .= conj.(view(M, lowertriangularind(M))) - if eltype(M) <: Complex - Md = view(M, diagind(M)) - Md .= real.(Md) - end - - ΔA1 .= ΔQ1 - mul!(ΔA1, M, Q1, +1, 1) - - if m > p - L21 = view(L, (p + 1):m, 1:p) - ΔA2 = view(ΔA, (p + 1):m, :) - ΔL21 = view(ΔL, (p + 1):m, 1:p) - - if ΔL isa AbstractZero - ΔA2 .= zero(eltype(ΔA)) - else - mul!(ΔA2, ΔL21, Q1) - mul!(ΔA1, L21', ΔA2, -1, 1) - end - end - if n > p && !(ΔQ isa AbstractZero) # case where R is not full rank - Q2 = view(Q, (p + 1):n, :) - ΔQ2 = view(ΔQ, (p + 1):n, :) - ΔQ2Q1d = ΔQ2 * Q1' - Δgauge = norm(mul!(copy(ΔQ2), ΔQ2Q1d, Q1, -1, 1)) - Δgauge < tol || - @warn "`lq` cotangents sensitive to gauge choice: (|Δgauge| = $Δgauge)" - mul!(ΔA1, ΔQ2Q1d', Q2, -1, 1) - end - ldiv!(LowerTriangular(L11)', ΔA1) - return ΔA -end - -function default_pullback_gaugetol(a) - n = norm(a, Inf) - return eps(eltype(n))^(3 / 4) * max(n, one(n)) + rightorth!_pullback(::NTuple{2,ZeroTangent}) = NoTangent(), ZeroTangent() + return LQ, rightorth!_pullback end From 46185846037a6718b286ed79565046f5820d5ffa Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Mon, 16 Jun 2025 20:45:30 -0400 Subject: [PATCH 47/70] Implement truncated eigenvalues --- .../factorizations.jl | 92 ++++++------------- src/tensors/factorizations/implementations.jl | 24 ++++- .../factorizations/matrixalgebrakit.jl | 20 ++++ src/tensors/factorizations/truncation.jl | 61 +++++++++++- 4 files changed, 131 insertions(+), 66 deletions(-) diff --git a/ext/TensorKitChainRulesCoreExt/factorizations.jl b/ext/TensorKitChainRulesCoreExt/factorizations.jl index 95a62b24f..0c6924411 100644 --- a/ext/TensorKitChainRulesCoreExt/factorizations.jl +++ b/ext/TensorKitChainRulesCoreExt/factorizations.jl @@ -1,32 +1,38 @@ # Factorizations rules # -------------------- -function ChainRulesCore.rrule(::typeof(TensorKit.tsvd!), t::AbstractTensorMap; - trunc::TruncationStrategy=TensorKit.notrunc(), - kwargs...) - # TODO: I think we can use tsvd! here without issues because we don't actually require - # the data of `t` anymore. - USVᴴ = tsvd(t; trunc=TensorKit.notrunc(), kwargs...) - - if trunc != TensorKit.notrunc() && !isempty(blocksectors(t)) - USVᴴ′ = MatrixAlgebraKit.truncate!(svd_trunc!, USVᴴ, trunc) - else - USVᴴ′ = USVᴴ - end +for f in (:tsvd, :eig, :eigh) + f! = Symbol(f, :!) + f_trunc! = f == :tsvd ? :svd_trunc! : Symbol(f, :_trunc!) + f_pullback = Symbol(f, :_pullback) + f_pullback! = f == :tsvd ? :svd_compact_pullback! : Symbol(f, :_full_pullback!) + @eval function ChainRulesCore.rrule(::typeof(TensorKit.$f!), t::AbstractTensorMap; + trunc::TruncationStrategy=TensorKit.notrunc(), + kwargs...) + # TODO: I think we can use f! here without issues because we don't actually require + # the data of `t` anymore. + F = $f(t; trunc=TensorKit.notrunc(), kwargs...) + + if trunc != TensorKit.notrunc() && !isempty(blocksectors(t)) + F′ = MatrixAlgebraKit.truncate!($f_trunc!, F, trunc) + else + F′ = F + end - function tsvd!_pullback(ΔUSVᴴ′) - ΔUSVᴴ = unthunk.(ΔUSVᴴ′) - Δt = zerovector(t) - foreachblock(Δt) do c, (b,) - USVᴴc = block.(USVᴴ, Ref(c)) - ΔUSVᴴc = block.(ΔUSVᴴ, Ref(c)) - svd_compact_pullback!(b, USVᴴc, ΔUSVᴴc) - return nothing + function $f_pullback(ΔF′) + ΔF = unthunk.(ΔF′) + Δt = zerovector(t) + foreachblock(Δt) do c, (b,) + Fc = block.(F, Ref(c)) + ΔFc = block.(ΔF, Ref(c)) + $f_pullback!(b, Fc, ΔFc) + return nothing + end + return NoTangent(), Δt end - return NoTangent(), Δt - end - tsvd!_pullback(::NTuple{3,ZeroTangent}) = NoTangent(), ZeroTangent() + $f_pullback(::Tuple{ZeroTangent,Vararg{ZeroTangent}}) = NoTangent(), ZeroTangent() - return USVᴴ′, tsvd!_pullback + return F′, $f_pullback + end end function ChainRulesCore.rrule(::typeof(LinearAlgebra.svdvals!), t::AbstractTensorMap) @@ -43,44 +49,6 @@ function ChainRulesCore.rrule(::typeof(LinearAlgebra.svdvals!), t::AbstractTenso return s, svdvals_pullback end -function ChainRulesCore.rrule(::typeof(TensorKit.eig!), t::AbstractTensorMap; kwargs...) - DV = eig(t; kwargs...) - - function eig!_pullback(ΔDV′) - ΔDV = unthunk.(ΔDV′) - Δt = zerovector(t) - foreachblock(Δt) do c, (b,) - DVc = block.(DV, Ref(c)) - ΔDVc = block.(ΔDV, Ref(c)) - eig_full_pullback!(b, DVc, ΔDVc) - return nothing - end - return NoTangent(), Δt - end - eig!_pullback(::NTuple{2,ZeroTangent}) = NoTangent(), ZeroTangent() - - return DV, eig!_pullback -end - -function ChainRulesCore.rrule(::typeof(TensorKit.eigh!), t::AbstractTensorMap; kwargs...) - DV = eigh(t; kwargs...) - - function eigh!_pullback(ΔDV′) - ΔDV = unthunk.(ΔDV′) - Δt = zerovector(t) - foreachblock(Δt) do c, (b,) - DVc = block.(DV, Ref(c)) - ΔDVc = block.(ΔDV, Ref(c)) - eigh_full_pullback!(b, DVc, ΔDVc) - return nothing - end - return NoTangent(), Δt - end - eigh!_pullback(::NTuple{2,ZeroTangent}) = NoTangent(), ZeroTangent() - - return DV, eigh!_pullback -end - function ChainRulesCore.rrule(::typeof(LinearAlgebra.eigvals!), t::AbstractTensorMap; sortby=nothing, kwargs...) @assert sortby === nothing "only `sortby=nothing` is supported" diff --git a/src/tensors/factorizations/implementations.jl b/src/tensors/factorizations/implementations.jl index 89c0d0a00..56d816258 100644 --- a/src/tensors/factorizations/implementations.jl +++ b/src/tensors/factorizations/implementations.jl @@ -149,9 +149,27 @@ rightpolar!(t::AbstractTensorMap; kwargs...) = right_polar!(t; kwargs...) # Eigenvalue decomposition # ------------------------ -eigh!(t::AbstractTensorMap) = eigh_full!(t) -eig!(t::AbstractTensorMap) = eig_full!(t) -eigen!(t::AbstractTensorMap) = ishermitian(t) ? eigh!(t) : eig!(t) +function eigh!(t::AbstractTensorMap; trunc=notrunc(), kwargs...) + InnerProductStyle(t) === EuclideanInnerProduct() || throw_invalid_innerproduct(:eigh!) + if trunc == notrunc() + return eigh_full!(t; kwargs...) + else + return eigh_trunc!(t; trunc, kwargs...) + end +end + +function eig!(t::AbstractTensorMap; trunc=notrunc(), kwargs...) + InnerProductStyle(t) === EuclideanInnerProduct() || throw_invalid_innerproduct(:eig!) + if trunc == notrunc() + return eig_full!(t; kwargs...) + else + return eig_trunc!(t; trunc, kwargs...) + end +end + +function eigen!(t::AbstractTensorMap; kwargs...) + return ishermitian(t) ? eigh!(t; kwargs...) : eig!(t; kwargs...) +end # Singular value decomposition # ---------------------------- diff --git a/src/tensors/factorizations/matrixalgebrakit.jl b/src/tensors/factorizations/matrixalgebrakit.jl index 605428f84..6d34a1e20 100644 --- a/src/tensors/factorizations/matrixalgebrakit.jl +++ b/src/tensors/factorizations/matrixalgebrakit.jl @@ -193,6 +193,26 @@ function initialize_output(::typeof(eig_full!), t::AbstractTensorMap, ::Abstract return D, V end +function initialize_output(::typeof(eigh_trunc!), t::AbstractTensorMap, + alg::TruncatedAlgorithm) + return initialize_output(eigh_full!, t, alg.alg) +end + +function initialize_output(::typeof(eig_trunc!), t::AbstractTensorMap, + alg::TruncatedAlgorithm) + return initialize_output(eig_full!, t, alg.alg) +end + +function eigh_trunc!(t::AbstractTensorMap, DV, alg::TruncatedAlgorithm) + DV′ = eigh_full!(t, DV, alg.alg) + return truncate!(eigh_trunc!, DV′, alg.trunc) +end + +function eig_trunc!(t::AbstractTensorMap, DV, alg::TruncatedAlgorithm) + DV′ = eig_full!(t, DV, alg.alg) + return truncate!(eig_trunc!, DV′, alg.trunc) +end + # QR decomposition # ---------------- const _T_QR = Tuple{<:AbstractTensorMap,<:AbstractTensorMap} diff --git a/src/tensors/factorizations/truncation.jl b/src/tensors/factorizations/truncation.jl index 4f7cec733..e172f17c6 100644 --- a/src/tensors/factorizations/truncation.jl +++ b/src/tensors/factorizations/truncation.jl @@ -65,6 +65,47 @@ function truncate!(::typeof(left_null!), return Ũ end +function truncate!(::typeof(eigh_trunc!), (D, V)::_T_DV, strategy::TruncationStrategy) + ind = findtruncated(diagview(D), strategy) + V_truncated = spacetype(D)(c => length(I) for (c, I) in ind) + + D̃ = DiagonalTensorMap{scalartype(D)}(undef, V_truncated) + for (c, b) in blocks(D̃) + I = get(ind, c, nothing) + @assert !isnothing(I) + copy!(b.diag, @view(block(D, c).diag[I])) + end + + Ṽ = similar(V, V_truncated ← domain(V)) + for (c, b) in blocks(Ṽ) + I = get(ind, c, nothing) + @assert !isnothing(I) + copy!(b, @view(block(V, c)[I, :])) + end + + return D̃, Ṽ +end +function truncate!(::typeof(eig_trunc!), (D, V)::_T_DV, strategy::TruncationStrategy) + ind = findtruncated(diagview(D), strategy) + V_truncated = spacetype(D)(c => length(I) for (c, I) in ind) + + D̃ = DiagonalTensorMap{scalartype(D)}(undef, V_truncated) + for (c, b) in blocks(D̃) + I = get(ind, c, nothing) + @assert !isnothing(I) + copy!(b.diag, @view(block(D, c).diag[I])) + end + + Ṽ = similar(V, V_truncated ← domain(V)) + for (c, b) in blocks(Ṽ) + I = get(ind, c, nothing) + @assert !isnothing(I) + copy!(b, @view(block(V, c)[I, :])) + end + + return D̃, Ṽ +end + # Find truncation # --------------- # auxiliary functions @@ -88,18 +129,28 @@ function _findnexttruncvalue(S, truncdim::SectorDict{I,Int}) where {I<:Sector} return σmin, keys(truncdim)[imin] end -# sorted implementations +# implementations function findtruncated_sorted(S::SectorDict, strategy::TruncationKeepAbove) atol = rtol_to_atol(S, strategy.p, strategy.atol, strategy.rtol) findtrunc = Base.Fix2(findtruncated_sorted, truncbelow(atol)) return SectorDict(c => findtrunc(d) for (c, d) in Sd) end +function findtruncated(S::SectorDict, strategy::TruncationKeepAbove) + atol = rtol_to_atol(S, strategy.p, strategy.atol, strategy.rtol) + findtrunc = Base.Fix2(findtruncated, truncbelow(atol)) + return SectorDict(c => findtrunc(d) for (c, d) in Sd) +end function findtruncated_sorted(S::SectorDict, strategy::TruncationKeepBelow) atol = rtol_to_atol(S, strategy.p, strategy.atol, strategy.rtol) findtrunc = Base.Fix2(findtruncated_sorted, truncabove(atol)) return SectorDict(c => findtrunc(d) for (c, d) in Sd) end +function findtruncated(S::SectorDict, strategy::TruncationKeepBelow) + atol = rtol_to_atol(S, strategy.p, strategy.atol, strategy.rtol) + findtrunc = Base.Fix2(findtruncated, truncabove(atol)) + return SectorDict(c => findtrunc(d) for (c, d) in Sd) +end function findtruncated_sorted(Sd::SectorDict, strategy::TruncationError) I = keytype(Sd) @@ -153,9 +204,17 @@ end function findtruncated_sorted(Sd::SectorDict, strategy::TruncationKeepFiltered) return SectorDict(c => findtruncated_sorted(d, strategy) for (c, d) in Sd) end +function findtruncated(Sd::SectorDict, strategy::TruncationKeepFiltered) + return SectorDict(c => findtruncated(d, strategy) for (c, d) in Sd) +end function findtruncated_sorted(Sd::SectorDict, strategy::TruncationIntersection) inds = map(Base.Fix1(findtruncated_sorted, Sd), strategy) return SectorDict(c => intersect(map(Base.Fix2(getindex, c), inds)...) for c in intersect(map(keys, inds)...)) end +function findtruncated(Sd::SectorDict, strategy::TruncationIntersection) + inds = map(Base.Fix1(findtruncated, Sd), strategy) + return SectorDict(c => intersect(map(Base.Fix2(getindex, c), inds)...) + for c in intersect(map(keys, inds)...)) +end From f7eeaab640a5dc60a8d8792ac1f5fcad0943d800 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Mon, 16 Jun 2025 21:22:54 -0400 Subject: [PATCH 48/70] Implement `TruncationKeepSorted` --- src/tensors/factorizations/truncation.jl | 31 ++++++++++++++++++------ 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/tensors/factorizations/truncation.jl b/src/tensors/factorizations/truncation.jl index e172f17c6..d553c5c93 100644 --- a/src/tensors/factorizations/truncation.jl +++ b/src/tensors/factorizations/truncation.jl @@ -119,14 +119,23 @@ function _compute_truncerr(Σdata, truncdim, p=2) p, zero(S)) end -function _findnexttruncvalue(S, truncdim::SectorDict{I,Int}) where {I<:Sector} +function _findnexttruncvalue(S, truncdim::SectorDict{I,Int}; by=identity, + rev::Bool=true) where {I<:Sector} # early return (isempty(S) || all(iszero, values(truncdim))) && return nothing - σmin, imin = findmin(keys(truncdim)) do c - d = truncdim[c] - return S[c][d] + if rev + σmin, imin = findmin(keys(truncdim)) do c + d = truncdim[c] + return by(S[c][d]) + end + return σmin, keys(truncdim)[imin] + else + σmax, imax = findmax(keys(truncdim)) do c + d = truncdim[c] + return by(S[c][d]) + end + return σmax, keys(truncdim)[imax] end - return σmin, keys(truncdim)[imin] end # implementations @@ -173,12 +182,18 @@ function findtruncated_sorted(Sd::SectorDict, strategy::TruncationError) end function findtruncated_sorted(Sd::SectorDict, strategy::TruncationKeepSorted) - @assert strategy.by === abs && strategy.rev == true "Not implemented" + return findtruncated(Sd, strategy) +end +function findtruncated(Sd::SectorDict, strategy::TruncationKeepSorted) + permutations = SectorDict(c => (sortperm(d; strategy.by, strategy.rev)) + for (c, d) in Sd) + Sd = SectorDict(c => sort(d; strategy.by, strategy.rev) for (c, d) in Sd) + I = keytype(Sd) truncdim = SectorDict{I,Int}(c => length(d) for (c, d) in Sd) totaldim = sum(dim(c) * d for (c, d) in truncdim; init=0) while true - next = _findnexttruncvalue(Sd, truncdim) + next = _findnexttruncvalue(Sd, truncdim; strategy.by, strategy.rev) isnothing(next) && break _, cmin = next truncdim[cmin] -= 1 @@ -191,7 +206,7 @@ function findtruncated_sorted(Sd::SectorDict, strategy::TruncationKeepSorted) delete!(truncdim, cmin) end end - return SectorDict{I,Base.OneTo{Int}}(c => Base.OneTo(d) for (c, d) in truncdim) + return SectorDict(c => permutations[c][Base.OneTo(d)] for (c, d) in truncdim) end function findtruncated_sorted(Sd::SectorDict, strategy::TruncationSpace) From 86408a55adeda96ec5c6b756929f9bfe1bd9696e Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 30 Jul 2025 15:38:22 -0400 Subject: [PATCH 49/70] correctly restrict type --- src/tensors/factorizations/implementations.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tensors/factorizations/implementations.jl b/src/tensors/factorizations/implementations.jl index 56d816258..7edf518f2 100644 --- a/src/tensors/factorizations/implementations.jl +++ b/src/tensors/factorizations/implementations.jl @@ -5,7 +5,7 @@ _kindof(::Polar) = :polar for f! in (:svd_compact!, :svd_full!, :left_null_svd!, :right_null_svd!) @eval function select_algorithm(::typeof($f!), t::T, alg::SVD; - kwargs...) where {T} + kwargs...) where {T<:AbstractTensorMap} isempty(kwargs) || throw(ArgumentError("Additional keyword arguments are not allowed")) return LAPACK_QRIteration() From fab71708cbe4932be177c377de11a83d46c8ee3a Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 30 Jul 2025 15:40:59 -0400 Subject: [PATCH 50/70] canonical use of codomain as first arg --- src/tensors/factorizations/matrixalgebrakit.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tensors/factorizations/matrixalgebrakit.jl b/src/tensors/factorizations/matrixalgebrakit.jl index 6d34a1e20..be6d8438f 100644 --- a/src/tensors/factorizations/matrixalgebrakit.jl +++ b/src/tensors/factorizations/matrixalgebrakit.jl @@ -383,7 +383,7 @@ function initialize_output(::typeof(left_polar!), t::AbstractTensorMap, ::Abstra end function check_input(::typeof(right_polar!), t::AbstractTensorMap, (P, Wᴴ)::_T_PWᴴ) - domain(t) ≿ codomain(t) || + codomain(t) ≾ domain(t) || throw(ArgumentError("Polar decomposition requires `domain(t) ≿ codomain(t)`")) # scalartype checks @@ -398,7 +398,7 @@ function check_input(::typeof(right_polar!), t::AbstractTensorMap, (P, Wᴴ)::_T end function check_input(::typeof(right_orth_polar!), t::AbstractTensorMap, (P, Wᴴ)::_T_PWᴴ) - domain(t) ≿ codomain(t) || + codomain(t) ≾ domain(t) || throw(ArgumentError("Polar decomposition requires `domain(t) ≿ codomain(t)`")) # scalartype checks From 517c4d88b1f7d253f68a92b31962c29cc10e439c Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 30 Jul 2025 15:44:06 -0400 Subject: [PATCH 51/70] import Diagonal --- src/tensors/factorizations/factorizations.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tensors/factorizations/factorizations.jl b/src/tensors/factorizations/factorizations.jl index 95683b0d9..8fd66b8b6 100644 --- a/src/tensors/factorizations/factorizations.jl +++ b/src/tensors/factorizations/factorizations.jl @@ -15,7 +15,7 @@ using ..TensorKit using ..TensorKit: AdjointTensorMap, SectorDict, OFA, blocktype, foreachblock using ..MatrixAlgebra: MatrixAlgebra -using LinearAlgebra: LinearAlgebra, BlasFloat, svdvals, svdvals! +using LinearAlgebra: LinearAlgebra, BlasFloat, Diagonal, svdvals, svdvals! import LinearAlgebra: eigen, eigen!, isposdef, isposdef!, ishermitian using TensorOperations: Index2Tuple From 1351eaf5fcf4a0052b7f23caa6ee775219cf21d0 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 6 Aug 2025 15:36:53 -0400 Subject: [PATCH 52/70] rework algorithm selection using updated matrixalgebrakit --- src/tensors/factorizations/factorizations.jl | 5 +-- src/tensors/factorizations/implementations.jl | 39 ------------------- .../factorizations/matrixalgebrakit.jl | 29 ++++---------- 3 files changed, 8 insertions(+), 65 deletions(-) diff --git a/src/tensors/factorizations/factorizations.jl b/src/tensors/factorizations/factorizations.jl index 8fd66b8b6..df9e0bdc5 100644 --- a/src/tensors/factorizations/factorizations.jl +++ b/src/tensors/factorizations/factorizations.jl @@ -24,10 +24,7 @@ using MatrixAlgebraKit using MatrixAlgebraKit: AbstractAlgorithm, TruncatedAlgorithm, TruncationStrategy, NoTruncation, TruncationKeepAbove, TruncationKeepBelow, TruncationIntersection, TruncationKeepFiltered -import MatrixAlgebraKit: select_algorithm, - default_qr_algorithm, default_lq_algorithm, - default_eig_algorithm, default_eigh_algorithm, - default_svd_algorithm, default_polar_algorithm, +import MatrixAlgebraKit: default_algorithm, copy_input, check_input, initialize_output, qr_compact!, qr_full!, qr_null!, lq_compact!, lq_full!, lq_null!, svd_compact!, svd_full!, svd_trunc!, diff --git a/src/tensors/factorizations/implementations.jl b/src/tensors/factorizations/implementations.jl index 7edf518f2..a53fd1680 100644 --- a/src/tensors/factorizations/implementations.jl +++ b/src/tensors/factorizations/implementations.jl @@ -3,45 +3,6 @@ _kindof(::Union{QR,QRpos}) = :qr _kindof(::Union{LQ,LQpos}) = :lq _kindof(::Polar) = :polar -for f! in (:svd_compact!, :svd_full!, :left_null_svd!, :right_null_svd!) - @eval function select_algorithm(::typeof($f!), t::T, alg::SVD; - kwargs...) where {T<:AbstractTensorMap} - isempty(kwargs) || - throw(ArgumentError("Additional keyword arguments are not allowed")) - return LAPACK_QRIteration() - end - @eval function select_algorithm(::typeof($f!), t::AbstractTensorMap, alg::SVD; - kwargs...) - isempty(kwargs) || - throw(ArgumentError("Additional keyword arguments are not allowed")) - return LAPACK_QRIteration() - end - @eval function select_algorithm(::typeof($f!), ::Type{T}, alg::SVD; - kwargs...) where {T<:AbstractTensorMap} - isempty(kwargs) || - throw(ArgumentError("Additional keyword arguments are not allowed")) - return LAPACK_QRIteration() - end - @eval function select_algorithm(::typeof($f!), t::T, alg::SDD; - kwargs...) where {T} - isempty(kwargs) || - throw(ArgumentError("Additional keyword arguments are not allowed")) - return LAPACK_DivideAndConquer() - end - @eval function select_algorithm(::typeof($f!), t::AbstractTensorMap, alg::SDD; - kwargs...) - isempty(kwargs) || - throw(ArgumentError("Additional keyword arguments are not allowed")) - return LAPACK_DivideAndConquer() - end - @eval function select_algorithm(::typeof($f!), ::Type{T}, alg::SDD; - kwargs...) where {T<:AbstractTensorMap} - isempty(kwargs) || - throw(ArgumentError("Additional keyword arguments are not allowed")) - return LAPACK_DivideAndConquer() - end -end - leftorth!(t::AbstractTensorMap; alg=nothing, kwargs...) = _leftorth!(t, alg; kwargs...) function _leftorth!(t::AbstractTensorMap, ::Nothing; kwargs...) diff --git a/src/tensors/factorizations/matrixalgebrakit.jl b/src/tensors/factorizations/matrixalgebrakit.jl index be6d8438f..ad95a31d2 100644 --- a/src/tensors/factorizations/matrixalgebrakit.jl +++ b/src/tensors/factorizations/matrixalgebrakit.jl @@ -1,27 +1,12 @@ # Algorithm selection # ------------------- -for f in (:eig_full, :eig_vals, :eig_trunc, :eigh_full, :eigh_vals, :eigh_trunc, :svd_full, - :svd_compact, :svd_vals, :svd_trunc) - @eval function copy_input(::typeof($f), t::AbstractTensorMap{<:BlasFloat}) - T = factorisation_scalartype($f, t) - return copy_oftype(t, T) - end - f! = Symbol(f, :!) - # TODO: can we move this to MAK? - @eval function select_algorithm(::typeof($f!), t::AbstractTensorMap, alg::Alg=nothing; - kwargs...) where {Alg} - return select_algorithm($f!, typeof(t), alg; kwargs...) - end - @eval function select_algorithm(::typeof($f!), ::Type{T}, alg::Alg=nothing; - kwargs...) where {T<:AbstractTensorMap,Alg} - return select_algorithm($f!, blocktype(T), alg; kwargs...) - end -end - -for f in (:qr, :lq, :svd, :eig, :eigh, :polar) - default_f_algorithm = Symbol(:default_, f, :_algorithm) - @eval function $default_f_algorithm(::Type{T}; kwargs...) where {T<:AbstractTensorMap} - return $default_f_algorithm(blocktype(T); kwargs...) +for f! in + [:svd_compact!, :svd_full!, :svd_trunc!, :svd_vals!, :qr_compact!, :qr_full!, :qr_null!, + :lq_compact!, :lq_full!, :lq_null!, :eig_full!, :eig_trunc!, :eig_vals!, :eigh_full!, + :eigh_trunc!, :eigh_vals!, :left_polar!, :right_polar!] + @eval function default_algorithm(::typeof($f!), ::Type{T}; + kwargs...) where {T<:AbstractTensorMap} + return default_algorithm($f!, blocktype(T); kwargs...) end end From 538fd4e25c954109c36283f30ce50c2b04b629b8 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 6 Aug 2025 16:00:03 -0400 Subject: [PATCH 53/70] Implement singular- and eigenvalues --- src/tensors/factorizations/factorizations.jl | 7 +-- .../factorizations/matrixalgebrakit.jl | 45 ++++++++++++++++--- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/tensors/factorizations/factorizations.jl b/src/tensors/factorizations/factorizations.jl index df9e0bdc5..40d16c447 100644 --- a/src/tensors/factorizations/factorizations.jl +++ b/src/tensors/factorizations/factorizations.jl @@ -27,13 +27,14 @@ using MatrixAlgebraKit: AbstractAlgorithm, TruncatedAlgorithm, TruncationStrateg import MatrixAlgebraKit: default_algorithm, copy_input, check_input, initialize_output, qr_compact!, qr_full!, qr_null!, lq_compact!, lq_full!, lq_null!, - svd_compact!, svd_full!, svd_trunc!, - eig_full!, eig_trunc!, eigh_full!, eigh_trunc!, + svd_compact!, svd_full!, svd_trunc!, svd_vals!, + eigh_full!, eigh_trunc!, eigh_vals!, + eig_full!, eig_trunc!, eig_vals!, left_polar!, left_orth_polar!, right_polar!, right_orth_polar!, left_null_svd!, right_null_svd!, left_orth!, right_orth!, left_null!, right_null!, truncate!, findtruncated, findtruncated_sorted, - diagview + diagview, isisometry include("utility.jl") include("interface.jl") diff --git a/src/tensors/factorizations/matrixalgebrakit.jl b/src/tensors/factorizations/matrixalgebrakit.jl index ad95a31d2..45979a206 100644 --- a/src/tensors/factorizations/matrixalgebrakit.jl +++ b/src/tensors/factorizations/matrixalgebrakit.jl @@ -42,8 +42,8 @@ for f! in (:qr_compact!, :qr_full!, end end -# Handle these separately because single N instead of tuple -for f! in (:qr_null!, :lq_null!) +# Handle these separately because single output instead of tuple +for f! in (:qr_null!, :lq_null!, :svd_vals!, :eig_vals!, :eigh_vals!) @eval function $f!(t::AbstractTensorMap, N, alg::AbstractAlgorithm) check_input($f!, t, N) @@ -94,7 +94,12 @@ function check_input(::typeof(svd_compact!), t::AbstractTensorMap, (U, S, Vᴴ): return nothing end -# TODO: svd_vals +function check_input(::typeof(svd_vals!), t::AbstractTensorMap, S::SectorDict) + @check_scalar S t real + V_cod = infimum(fuse(codomain(t)), fuse(domain(t))) + @check_space(S, V_cod ← V_dom) + return nothing +end function initialize_output(::typeof(svd_full!), t::AbstractTensorMap, ::AbstractAlgorithm) V_cod = fuse(codomain(t)) @@ -114,12 +119,14 @@ function initialize_output(::typeof(svd_compact!), t::AbstractTensorMap, return U, S, Vᴴ end -function initialize_output(::typeof(svd_trunc!), t::AbstractTensorMap, - alg::TruncatedAlgorithm) +function initialize_output(::typeof(svd_trunc!), t::AbstractTensorMap, alg::TruncatedAlgorithm) return initialize_output(svd_compact!, t, alg.alg) end -# TODO: svd_vals +function initialize_output(::typeof(svd_vals!), t::AbstractTensorMap, alg::AbstractAlgorithm) + V_cod = infimum(fuse(codomain(t)), fuse(domain(t))) + return DiagonalTensorMap{real(scalartype(t))}(undef, V_cod) +end function svd_trunc!(t::AbstractTensorMap, USVᴴ, alg::TruncatedAlgorithm) USVᴴ′ = svd_compact!(t, USVᴴ, alg.alg) @@ -162,6 +169,20 @@ function check_input(::typeof(eig_full!), t::AbstractTensorMap, (D, V)::_T_DV) return nothing end +function check_input(::typeof(eigh_vals!), t::AbstractTensorMap, D::DiagonalTensorMap) + @check_scalar D t real + V_D = fuse(domain(t)) + @check_space(D, V_D ← V_D) + return nothing +end + +function check_input(::typeof(eig_vals!), t::AbstractTensorMap, D::DiagonalTensorMap) + @check_scalar D t complex + V_D = fuse(domain(t)) + @check_space(D, V_D ← V_D) + return nothing +end + function initialize_output(::typeof(eigh_full!), t::AbstractTensorMap, ::AbstractAlgorithm) V_D = fuse(domain(t)) T = real(scalartype(t)) @@ -178,6 +199,18 @@ function initialize_output(::typeof(eig_full!), t::AbstractTensorMap, ::Abstract return D, V end +function initialize_output(::typeof(eigh_vals!), t::AbstractTensorMap, alg::AbstractAlgorithm) + V_D = fuse(domain(t)) + T = real(scalartype(t)) + D = DiagonalTensorMap{Tc}(undef, V_D) +end + +function initialize_output(::typeof(eig_vals!), t::AbstractTensorMap, alg::AbstractAlgorithm) + V_D = fuse(domain(t)) + Tc = complex(scalartype(t)) + D = DiagonalTensorMap{Tc}(undef, V_D) +end + function initialize_output(::typeof(eigh_trunc!), t::AbstractTensorMap, alg::TruncatedAlgorithm) return initialize_output(eigh_full!, t, alg.alg) From c43dff34d6e359961169c37b5d7179031379e934 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 6 Aug 2025 16:38:01 -0400 Subject: [PATCH 54/70] small fixes --- src/tensors/factorizations/implementations.jl | 38 +++++++++++++++---- .../factorizations/matrixalgebrakit.jl | 4 +- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/tensors/factorizations/implementations.jl b/src/tensors/factorizations/implementations.jl index a53fd1680..55d40fad8 100644 --- a/src/tensors/factorizations/implementations.jl +++ b/src/tensors/factorizations/implementations.jl @@ -5,7 +5,7 @@ _kindof(::Polar) = :polar leftorth!(t::AbstractTensorMap; alg=nothing, kwargs...) = _leftorth!(t, alg; kwargs...) -function _leftorth!(t::AbstractTensorMap, ::Nothing; kwargs...) +function _leftorth!(t::AbstractTensorMap, alg::Nothing,; kwargs...) return isempty(kwargs) ? left_orth!(t) : left_orth!(t; trunc=(; kwargs...)) end function _leftorth!(t::AbstractTensorMap, alg::Union{QL,QLpos}; kwargs...) @@ -22,9 +22,14 @@ end function _leftorth!(t, alg::OFA; kwargs...) trunc = isempty(kwargs) ? nothing : (; kwargs...) + Base.depwarn(lazy"$alg is deprecated", :leftorth!) + kind = _kindof(alg) if kind == :svd - return left_orth!(t; kind, alg_svd=alg, trunc) + alg_svd = alg === SVD() ? LAPACK_QRIteration() : + alg === SDD() ? LAPACK_DivideAndConquer() : + throw(ArgumentError(lazy"Unknown algorithm $alg")) + return left_orth!(t; kind, alg_svd, trunc) elseif kind == :qr alg_qr = (; positive=(alg == QRpos())) return left_orth!(t; kind, alg_qr, trunc) @@ -47,7 +52,10 @@ function leftnull!(t::AbstractTensorMap; kind = _kindof(alg) if kind == :svd - return left_null!(t; kind, alg_svd=alg, trunc) + alg_svd = alg === SVD() ? LAPACK_QRIteration() : + alg === SDD() ? LAPACK_DivideAndConquer() : + throw(ArgumentError(lazy"Unknown algorithm $alg")) + return left_null!(t; kind, alg_svd, trunc) elseif kind == :qr alg_qr = (; positive=(alg == QRpos())) return left_null!(t; kind, alg_qr, trunc) @@ -76,7 +84,10 @@ function rightorth!(t::AbstractTensorMap; kind = _kindof(alg) if kind == :svd - return right_orth!(t; kind, alg_svd=alg, trunc) + alg_svd = alg === SVD() ? LAPACK_QRIteration() : + alg === SDD() ? LAPACK_DivideAndConquer() : + throw(ArgumentError(lazy"Unknown algorithm $alg")) + return right_orth!(t; kind, alg_svd, trunc) elseif kind == :lq alg_lq = (; positive=(alg == LQpos())) return right_orth!(t; kind, alg_lq, trunc) @@ -97,7 +108,10 @@ function rightnull!(t::AbstractTensorMap; kind = _kindof(alg) if kind == :svd - return right_null!(t; kind, alg_svd=alg, trunc) + alg_svd = alg === SVD() ? LAPACK_QRIteration() : + alg === SDD() ? LAPACK_DivideAndConquer() : + throw(ArgumentError(lazy"Unknown algorithm $alg")) + return right_null!(t; kind, alg_svd, trunc) elseif kind == :lq alg_lq = (; positive=(alg == LQpos())) return right_null!(t; kind, alg_lq, trunc) @@ -121,6 +135,7 @@ end function eig!(t::AbstractTensorMap; trunc=notrunc(), kwargs...) InnerProductStyle(t) === EuclideanInnerProduct() || throw_invalid_innerproduct(:eig!) + if trunc == notrunc() return eig_full!(t; kwargs...) else @@ -134,13 +149,20 @@ end # Singular value decomposition # ---------------------------- -function tsvd!(t::AbstractTensorMap; trunc=notrunc(), p=nothing, kwargs...) +function tsvd!(t::AbstractTensorMap; trunc=notrunc(), p=nothing, alg=nothing, kwargs...) InnerProductStyle(t) === EuclideanInnerProduct() || throw_invalid_innerproduct(:tsvd!) isnothing(p) || Base.depwarn("p is no longer supported", :tsvd!) + if alg isa OFA + Base.depwarn(lazy"$alg is deprecated", :tsvd!) + alg = alg === SVD() ? LAPACK_QRIteration() : + alg === SDD() ? LAPACK_DivideAndConquer() : + throw(ArgumentError(lazy"Unknown algorithm $alg")) + end + if trunc == notrunc() - return svd_compact!(t; kwargs...) + return svd_compact!(t; alg, kwargs...) else - return svd_trunc!(t; trunc, kwargs...) + return svd_trunc!(t; trunc, alg, kwargs...) end end diff --git a/src/tensors/factorizations/matrixalgebrakit.jl b/src/tensors/factorizations/matrixalgebrakit.jl index 45979a206..f5429cf44 100644 --- a/src/tensors/factorizations/matrixalgebrakit.jl +++ b/src/tensors/factorizations/matrixalgebrakit.jl @@ -440,11 +440,11 @@ end # Needed to get algorithm selection to behave function left_orth_polar!(t::AbstractTensorMap, VC, alg) - alg′ = select_algorithm(left_polar!, t, alg) + alg′ = MatrixAlgebraKit.select_algorithm(left_polar!, t, alg) return left_orth_polar!(t, VC, alg′) end function right_orth_polar!(t::AbstractTensorMap, CVᴴ, alg) - alg′ = select_algorithm(right_polar!, t, alg) + alg′ = MatrixAlgebraKit.select_algorithm(right_polar!, t, alg) return right_orth_polar!(t, CVᴴ, alg′) end From 87f55d4a3cab8d5917f262c29738eb39a211b7c3 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 6 Aug 2025 16:38:51 -0400 Subject: [PATCH 55/70] remove wrongly added modules --- dev/ChainRulesTestUtils | 1 - dev/Strided | 1 - 2 files changed, 2 deletions(-) delete mode 160000 dev/ChainRulesTestUtils delete mode 160000 dev/Strided diff --git a/dev/ChainRulesTestUtils b/dev/ChainRulesTestUtils deleted file mode 160000 index bdd5606c5..000000000 --- a/dev/ChainRulesTestUtils +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bdd5606c59296c45d804732ca2183ecafc9e0b8a diff --git a/dev/Strided b/dev/Strided deleted file mode 160000 index f8a0514bb..000000000 --- a/dev/Strided +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f8a0514bbcf837f87186c8383dba0c35f7822f0f From cad4fbe3b45b3bdeeba37d707497c392a9a35012 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 6 Aug 2025 16:40:17 -0400 Subject: [PATCH 56/70] fix docstring --- src/spaces/vectorspaces.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spaces/vectorspaces.jl b/src/spaces/vectorspaces.jl index 92fd780ef..847182536 100644 --- a/src/spaces/vectorspaces.jl +++ b/src/spaces/vectorspaces.jl @@ -154,8 +154,8 @@ const oplus = ⊕ ⊖(V::ElementarySpace, W::ElementarySpace) -> X::ElementarySpace ominus(V::ElementarySpace, W::ElementarySpace) -> X::ElementarySpace -Return the set difference of two elementary spaces, i.e. an instance `X::ElementarySpace` -such that `V = W ⊕ X`. +Return a space that is equivalent to the orthogonal complement of `W` in `V`, +i.e. an instance `X::ElementarySpace` such that `V = W ⊕ X`. """ ⊖(V₁::S, V₂::S) where {S<:ElementarySpace} ⊖(V₁::VectorSpace, V₂::VectorSpace) = ⊖(promote(V₁, V₂)...) From 0ee4229e8b2db13cbd3bd8b0b15c8339df14ddeb Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 6 Aug 2025 16:42:59 -0400 Subject: [PATCH 57/70] remove leftpolar and rightpolar --- src/TensorKit.jl | 4 ++-- src/tensors/factorizations/factorizations.jl | 1 - src/tensors/factorizations/implementations.jl | 4 ---- src/tensors/factorizations/interface.jl | 16 +--------------- test/tensors.jl | 4 ++-- 5 files changed, 5 insertions(+), 24 deletions(-) diff --git a/src/TensorKit.jl b/src/TensorKit.jl index 9c7bf34f4..87bd2a594 100644 --- a/src/TensorKit.jl +++ b/src/TensorKit.jl @@ -70,8 +70,8 @@ export inner, dot, norm, normalize, normalize!, tr # factorizations export mul!, lmul!, rmul!, adjoint!, pinv, axpy!, axpby! -export leftorth, rightorth, leftnull, rightnull, leftpolar, rightpolar, - leftorth!, rightorth!, leftnull!, rightnull!, leftpolar!, rightpolar!, +export leftorth, rightorth, leftnull, rightnull, + leftorth!, rightorth!, leftnull!, rightnull!, tsvd!, tsvd, eigen, eigen!, eig, eig!, eigh, eigh!, exp, exp!, isposdef, isposdef!, ishermitian, isisometry, isunitary, sylvester, rank, cond export braid, braid!, permute, permute!, transpose, transpose!, twist, twist!, repartition, diff --git a/src/tensors/factorizations/factorizations.jl b/src/tensors/factorizations/factorizations.jl index 40d16c447..eb1913cad 100644 --- a/src/tensors/factorizations/factorizations.jl +++ b/src/tensors/factorizations/factorizations.jl @@ -7,7 +7,6 @@ export eig, eig!, eigh, eigh! export tsvd, tsvd!, svdvals, svdvals! export leftorth, leftorth!, rightorth, rightorth! export leftnull, leftnull!, rightnull, rightnull! -export leftpolar, leftpolar!, rightpolar, rightpolar! export copy_oftype, permutedcopy_oftype export TruncationScheme, notrunc, truncbelow, truncerr, truncdim, truncspace diff --git a/src/tensors/factorizations/implementations.jl b/src/tensors/factorizations/implementations.jl index 55d40fad8..20e5c3135 100644 --- a/src/tensors/factorizations/implementations.jl +++ b/src/tensors/factorizations/implementations.jl @@ -64,8 +64,6 @@ function leftnull!(t::AbstractTensorMap; end end -leftpolar!(t::AbstractTensorMap; kwargs...) = left_polar!(t; kwargs...) - function rightorth!(t::AbstractTensorMap; alg::Union{LQ,LQpos,RQ,RQpos,SVD,SDD,Polar,Nothing}=nothing, kwargs...) InnerProductStyle(t) === EuclideanInnerProduct() || @@ -120,8 +118,6 @@ function rightnull!(t::AbstractTensorMap; end end -rightpolar!(t::AbstractTensorMap; kwargs...) = right_polar!(t; kwargs...) - # Eigenvalue decomposition # ------------------------ function eigh!(t::AbstractTensorMap; trunc=notrunc(), kwargs...) diff --git a/src/tensors/factorizations/interface.jl b/src/tensors/factorizations/interface.jl index 56d462887..78c40e517 100644 --- a/src/tensors/factorizations/interface.jl +++ b/src/tensors/factorizations/interface.jl @@ -182,20 +182,6 @@ Orthogonality requires `InnerProductStyle(t) <: HasInnerProduct`, and `InnerProductStyle(t) === EuclideanInnerProduct()`. """ rightnull, rightnull! -@doc """ - leftpolar(t::AbstractTensorMap, [(leftind, rightind)::Index2Tuple]; kwargs...) -> W, P - leftpolar!(t::AbstractTensorMap; kwargs...) -> W, P - -Compute the polar decomposition of tensor `t` as linear map from `rightind` to `leftind`. - -If `leftind` and `rightind` are not specified, the current partition of left and right -indices of `t` is used. In that case, less memory is allocated if one allows the data in -`t` to be destroyed/overwritten, by using `eigh!(t)`. - -See also [`rightpolar(!)`](@ref rightpolar). - -""" leftpolar, leftpolar! - @doc """ eigen(t::AbstractTensorMap, [(leftind, rightind)::Index2Tuple]; kwargs...) -> D, V eigen!(t::AbstractTensorMap; kwargs...) -> D, V @@ -230,7 +216,7 @@ meaningless. """ isposdef(::AbstractTensorMap), isposdef!(::AbstractTensorMap) for f in - (:tsvd, :eig, :eigh, :eigen, :leftorth, :rightorth, :leftpolar, :rightpolar, :leftnull, + (:tsvd, :eig, :eigh, :eigen, :leftorth, :rightorth, :left_polar, :right_polar, :leftnull, :rightnull, :isposdef) f! = Symbol(f, :!) @eval function $f(t::AbstractTensorMap, p::Index2Tuple; kwargs...) diff --git a/test/tensors.jl b/test/tensors.jl index 3a9243b8c..cd0071b2e 100644 --- a/test/tensors.jl +++ b/test/tensors.jl @@ -686,8 +686,8 @@ for V in spacelist for T in (Float32, ComplexF64) tA = rand(T, V1 ⊗ V3, V1 ⊗ V3) tB = rand(T, V2 ⊗ V4, V2 ⊗ V4) - tA = 3 // 2 * leftpolar(tA)[1] - tB = 1 // 5 * leftpolar(tB)[1] + tA = 3 // 2 * left_polar(tA)[1] + tB = 1 // 5 * left_polar(tB)[1] tC = rand(T, V1 ⊗ V3, V2 ⊗ V4) t = @constinferred sylvester(tA, tB, tC) @test codomain(t) == V1 ⊗ V3 From ef17718bc70217f9f24fa9cef5355fc36b95e6cd Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Wed, 6 Aug 2025 16:44:14 -0400 Subject: [PATCH 58/70] format --- src/tensors/factorizations/implementations.jl | 2 +- src/tensors/factorizations/interface.jl | 3 ++- src/tensors/factorizations/matrixalgebrakit.jl | 16 ++++++++++------ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/tensors/factorizations/implementations.jl b/src/tensors/factorizations/implementations.jl index 20e5c3135..02c59b492 100644 --- a/src/tensors/factorizations/implementations.jl +++ b/src/tensors/factorizations/implementations.jl @@ -5,7 +5,7 @@ _kindof(::Polar) = :polar leftorth!(t::AbstractTensorMap; alg=nothing, kwargs...) = _leftorth!(t, alg; kwargs...) -function _leftorth!(t::AbstractTensorMap, alg::Nothing,; kwargs...) +function _leftorth!(t::AbstractTensorMap, alg::Nothing, ; kwargs...) return isempty(kwargs) ? left_orth!(t) : left_orth!(t; trunc=(; kwargs...)) end function _leftorth!(t::AbstractTensorMap, alg::Union{QL,QLpos}; kwargs...) diff --git a/src/tensors/factorizations/interface.jl b/src/tensors/factorizations/interface.jl index 78c40e517..fc757a298 100644 --- a/src/tensors/factorizations/interface.jl +++ b/src/tensors/factorizations/interface.jl @@ -216,7 +216,8 @@ meaningless. """ isposdef(::AbstractTensorMap), isposdef!(::AbstractTensorMap) for f in - (:tsvd, :eig, :eigh, :eigen, :leftorth, :rightorth, :left_polar, :right_polar, :leftnull, + (:tsvd, :eig, :eigh, :eigen, :leftorth, :rightorth, :left_polar, :right_polar, + :leftnull, :rightnull, :isposdef) f! = Symbol(f, :!) @eval function $f(t::AbstractTensorMap, p::Index2Tuple; kwargs...) diff --git a/src/tensors/factorizations/matrixalgebrakit.jl b/src/tensors/factorizations/matrixalgebrakit.jl index f5429cf44..91aa8a5c0 100644 --- a/src/tensors/factorizations/matrixalgebrakit.jl +++ b/src/tensors/factorizations/matrixalgebrakit.jl @@ -119,11 +119,13 @@ function initialize_output(::typeof(svd_compact!), t::AbstractTensorMap, return U, S, Vᴴ end -function initialize_output(::typeof(svd_trunc!), t::AbstractTensorMap, alg::TruncatedAlgorithm) +function initialize_output(::typeof(svd_trunc!), t::AbstractTensorMap, + alg::TruncatedAlgorithm) return initialize_output(svd_compact!, t, alg.alg) end -function initialize_output(::typeof(svd_vals!), t::AbstractTensorMap, alg::AbstractAlgorithm) +function initialize_output(::typeof(svd_vals!), t::AbstractTensorMap, + alg::AbstractAlgorithm) V_cod = infimum(fuse(codomain(t)), fuse(domain(t))) return DiagonalTensorMap{real(scalartype(t))}(undef, V_cod) end @@ -199,16 +201,18 @@ function initialize_output(::typeof(eig_full!), t::AbstractTensorMap, ::Abstract return D, V end -function initialize_output(::typeof(eigh_vals!), t::AbstractTensorMap, alg::AbstractAlgorithm) +function initialize_output(::typeof(eigh_vals!), t::AbstractTensorMap, + alg::AbstractAlgorithm) V_D = fuse(domain(t)) T = real(scalartype(t)) - D = DiagonalTensorMap{Tc}(undef, V_D) + return D = DiagonalTensorMap{Tc}(undef, V_D) end -function initialize_output(::typeof(eig_vals!), t::AbstractTensorMap, alg::AbstractAlgorithm) +function initialize_output(::typeof(eig_vals!), t::AbstractTensorMap, + alg::AbstractAlgorithm) V_D = fuse(domain(t)) Tc = complex(scalartype(t)) - D = DiagonalTensorMap{Tc}(undef, V_D) + return D = DiagonalTensorMap{Tc}(undef, V_D) end function initialize_output(::typeof(eigh_trunc!), t::AbstractTensorMap, From cc18e954a1809d06622c1cfc4263a34705bd03ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:18:35 +0200 Subject: [PATCH 59/70] Bump actions/checkout from 4 to 5 (#267) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/CI.yml | 4 ++-- .github/workflows/Documentation.yml | 2 +- .github/workflows/DocumentationCleanup.yml | 2 +- .github/workflows/FormatCheck.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 7f94f9373..e7be5d8e6 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -28,7 +28,7 @@ jobs: - macOS-latest - windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.version }} @@ -57,7 +57,7 @@ jobs: - windows-latest continue-on-error: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.version }} diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml index 4908988c9..82540cbdf 100644 --- a/.github/workflows/Documentation.yml +++ b/.github/workflows/Documentation.yml @@ -20,7 +20,7 @@ jobs: os: - ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: julia-actions/setup-julia@latest with: version: ${{ matrix.version }} diff --git a/.github/workflows/DocumentationCleanup.yml b/.github/workflows/DocumentationCleanup.yml index 5be23b964..f8a1e525e 100644 --- a/.github/workflows/DocumentationCleanup.yml +++ b/.github/workflows/DocumentationCleanup.yml @@ -16,7 +16,7 @@ jobs: contents: write steps: - name: Checkout gh-pages branch - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: gh-pages - name: Delete preview and history + push changes diff --git a/.github/workflows/FormatCheck.yml b/.github/workflows/FormatCheck.yml index aa23cd6a2..e112b3028 100644 --- a/.github/workflows/FormatCheck.yml +++ b/.github/workflows/FormatCheck.yml @@ -23,7 +23,7 @@ jobs: with: version: ${{ matrix.version }} - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install JuliaFormatter and format # This will use the latest version by default but you can set the version like so: # From ca2c67a8ead0ea1c6e0c2778a4df7c67721c0040 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sat, 16 Aug 2025 15:32:05 +0200 Subject: [PATCH 60/70] Add citation file (#268) * Add Citation.cff * Add acknowledgements --- CITATION.cff | 27 +++++++++++++++++++++++++++ README.md | 7 +++++++ 2 files changed, 34 insertions(+) create mode 100644 CITATION.cff diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 000000000..a98e1efdb --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,27 @@ +cff-version: 1.2.0 +message: "If you use this software, please cite it as below." +authors: +- family-names: "Devos" + given-names: "Lukas" + orcid: "https://orcid.org/0000-0002-0256-4200" +- family-names: "Haegeman" + given-names: "Jutho" + orcid: "https://orcid.org/0000-0002-0858-291X" +title: "TensorKit.jl" +version: 2.0.4 +doi: 10.5281/zenodo.1234 +date-released: 2017-12-18 +url: "https://github.com/github-linguist/linguist" +preferred-citation: + type: article + authors: + - family-names: "Devos" + given-names: "Lukas" + orcid: "https://orcid.org/0000-0002-0256-4200" + - family-names: "Haegeman" + given-names: "Jutho" + orcid: "https://orcid.org/0000-0002-0858-291X" + doi: "10.48550/arXiv.2508.10076" + journal: "arXiv" + title: "TensorKit.jl: A Julia package for large-scale tensor computations, with a hint of category theory" + year: 2025 diff --git a/README.md b/README.md index 835982c6a..e3acc2c9c 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,7 @@ Check out the [tutorial](https://jutho.github.io/TensorKit.jl/stable/man/tutoria full [documentation](https://jutho.github.io/TensorKit.jl/stable). ## Installation + `TensorKit.jl` can be installed with the Julia package manager. From the Julia REPL, type `]` to enter the Pkg REPL mode and run: ``` @@ -180,3 +181,9 @@ platforms with a 64-bit architecture. Contributions are very welcome, as are feature requests and suggestions. Please open an [issue][issues-url] if you encounter any problems. [issues-url]: https://github.com/Jutho/TensorKit.jl/issues + +## Acknowledgements + +The design and development of the TensorKit.jl package have benefited from countless discussions with many people, including most current and former members of the Quantum Group at Ghent University. +Being an open-source software project developed over the course of many years, we also thank all past, current and future contributors, including the bug reports and feature requests that have shaped this package. +In particular, we like to thank [Maarten Van Damme](@maartenvd), who initiated the MPSKit.jl package on top of TensorKit.jl early-on, and has as such had a strong influence on the development and design decisions of the TensorKit.jl package. From b9f874defd5c12860e5b4842dcdd127d5a6ad741 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Mon, 18 Aug 2025 08:25:35 +0200 Subject: [PATCH 61/70] Fix citation.cff --- CITATION.cff | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index a98e1efdb..628a9c587 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -8,10 +8,10 @@ authors: given-names: "Jutho" orcid: "https://orcid.org/0000-0002-0858-291X" title: "TensorKit.jl" -version: 2.0.4 -doi: 10.5281/zenodo.1234 -date-released: 2017-12-18 -url: "https://github.com/github-linguist/linguist" +version: "0.14.9" +doi: "10.5281/zenodo.8421339" +date-released: "2025-07-18" +url: "https://github.com/Jutho/TensorKit.jl" preferred-citation: type: article authors: From 58dafef7e84cb6770e412ee5a4e92aa2fb303320 Mon Sep 17 00:00:00 2001 From: Katharine Hyatt Date: Mon, 18 Aug 2025 15:40:03 +0200 Subject: [PATCH 62/70] Tests passing --- Project.toml | 4 +- src/TensorKit.jl | 1 + .../factorizations/matrixalgebrakit.jl | 46 +++++++++---------- test/runtests.jl | 6 +-- 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/Project.toml b/Project.toml index 00bc4dfbf..a1847b6dd 100644 --- a/Project.toml +++ b/Project.toml @@ -11,7 +11,6 @@ OhMyThreads = "67456a42-1dca-4109-a031-0a68de7e3ad5" PackageExtensionCompat = "65ce6f38-6b18-4e1d-a461-8949797d7930" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" ScopedValues = "7e506255-f358-4e82-b7e4-beb19740aa63" -SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" Strided = "5e0ebb24-38b0-5f93-81fe-25c709ecae67" TensorKitSectors = "13a9c161-d5da-41f0-bcbd-e1a08ae0647f" TensorOperations = "6aa20fa7-93e2-5fca-9bc0-fbd0db3c71a2" @@ -34,12 +33,11 @@ Combinatorics = "1" FiniteDifferences = "0.12" LRUCache = "1.0.2" LinearAlgebra = "1" -MatrixAlgebraKit = "0.2.5" +MatrixAlgebraKit = "0.3" OhMyThreads = "0.8.0" PackageExtensionCompat = "1" Random = "1" ScopedValues = "1.3.0" -SparseArrays = "1" Strided = "2" TensorKitSectors = "0.1" TensorOperations = "5.1" diff --git a/src/TensorKit.jl b/src/TensorKit.jl index 87bd2a594..4301fe88b 100644 --- a/src/TensorKit.jl +++ b/src/TensorKit.jl @@ -72,6 +72,7 @@ export inner, dot, norm, normalize, normalize!, tr export mul!, lmul!, rmul!, adjoint!, pinv, axpy!, axpby! export leftorth, rightorth, leftnull, rightnull, leftorth!, rightorth!, leftnull!, rightnull!, + left_polar, left_polar!, right_polar, right_polar!, tsvd!, tsvd, eigen, eigen!, eig, eig!, eigh, eigh!, exp, exp!, isposdef, isposdef!, ishermitian, isisometry, isunitary, sylvester, rank, cond export braid, braid!, permute, permute!, transpose, transpose!, twist, twist!, repartition, diff --git a/src/tensors/factorizations/matrixalgebrakit.jl b/src/tensors/factorizations/matrixalgebrakit.jl index 91aa8a5c0..1f40309b6 100644 --- a/src/tensors/factorizations/matrixalgebrakit.jl +++ b/src/tensors/factorizations/matrixalgebrakit.jl @@ -26,7 +26,7 @@ for f! in (:qr_compact!, :qr_full!, :svd_compact!, :svd_full!, :left_polar!, :left_orth_polar!, :right_polar!, :right_orth_polar!) @eval function $f!(t::AbstractTensorMap, F, alg::AbstractAlgorithm) - check_input($f!, t, F) + check_input($f!, t, F, alg) foreachblock(t, F...) do _, bs factors = Base.tail(bs) @@ -45,7 +45,7 @@ end # Handle these separately because single output instead of tuple for f! in (:qr_null!, :lq_null!, :svd_vals!, :eig_vals!, :eigh_vals!) @eval function $f!(t::AbstractTensorMap, N, alg::AbstractAlgorithm) - check_input($f!, t, N) + check_input($f!, t, N, alg) foreachblock(t, N) do _, (b, n) n′ = $f!(b, n, alg) @@ -63,7 +63,7 @@ end const _T_USVᴴ = Tuple{<:AbstractTensorMap,<:AbstractTensorMap,<:AbstractTensorMap} const _T_USVᴴ_diag = Tuple{<:AbstractTensorMap,<:DiagonalTensorMap,<:AbstractTensorMap} -function check_input(::typeof(svd_full!), t::AbstractTensorMap, (U, S, Vᴴ)::_T_USVᴴ) +function check_input(::typeof(svd_full!), t::AbstractTensorMap, (U, S, Vᴴ)::_T_USVᴴ, ::AbstractAlgorithm) # scalartype checks @check_scalar U t @check_scalar S t real @@ -79,7 +79,7 @@ function check_input(::typeof(svd_full!), t::AbstractTensorMap, (U, S, Vᴴ)::_T return nothing end -function check_input(::typeof(svd_compact!), t::AbstractTensorMap, (U, S, Vᴴ)::_T_USVᴴ_diag) +function check_input(::typeof(svd_compact!), t::AbstractTensorMap, (U, S, Vᴴ)::_T_USVᴴ_diag, ::AbstractAlgorithm) # scalartype checks @check_scalar U t @check_scalar S t real @@ -94,7 +94,7 @@ function check_input(::typeof(svd_compact!), t::AbstractTensorMap, (U, S, Vᴴ): return nothing end -function check_input(::typeof(svd_vals!), t::AbstractTensorMap, S::SectorDict) +function check_input(::typeof(svd_vals!), t::AbstractTensorMap, S::SectorDict, ::AbstractAlgorithm) @check_scalar S t real V_cod = infimum(fuse(codomain(t)), fuse(domain(t))) @check_space(S, V_cod ← V_dom) @@ -139,7 +139,7 @@ end # ------------------------ const _T_DV = Tuple{<:DiagonalTensorMap,<:AbstractTensorMap} -function check_input(::typeof(eigh_full!), t::AbstractTensorMap, (D, V)::_T_DV) +function check_input(::typeof(eigh_full!), t::AbstractTensorMap, (D, V)::_T_DV, ::AbstractAlgorithm) domain(t) == codomain(t) || throw(ArgumentError("Eigenvalue decomposition requires square input tensor")) @@ -155,7 +155,7 @@ function check_input(::typeof(eigh_full!), t::AbstractTensorMap, (D, V)::_T_DV) return nothing end -function check_input(::typeof(eig_full!), t::AbstractTensorMap, (D, V)::_T_DV) +function check_input(::typeof(eig_full!), t::AbstractTensorMap, (D, V)::_T_DV, ::AbstractAlgorithm) domain(t) == codomain(t) || throw(ArgumentError("Eigenvalue decomposition requires square input tensor")) @@ -171,14 +171,14 @@ function check_input(::typeof(eig_full!), t::AbstractTensorMap, (D, V)::_T_DV) return nothing end -function check_input(::typeof(eigh_vals!), t::AbstractTensorMap, D::DiagonalTensorMap) +function check_input(::typeof(eigh_vals!), t::AbstractTensorMap, D::DiagonalTensorMap, ::AbstractAlgorithm) @check_scalar D t real V_D = fuse(domain(t)) @check_space(D, V_D ← V_D) return nothing end -function check_input(::typeof(eig_vals!), t::AbstractTensorMap, D::DiagonalTensorMap) +function check_input(::typeof(eig_vals!), t::AbstractTensorMap, D::DiagonalTensorMap, ::AbstractAlgorithm) @check_scalar D t complex V_D = fuse(domain(t)) @check_space(D, V_D ← V_D) @@ -239,7 +239,7 @@ end # ---------------- const _T_QR = Tuple{<:AbstractTensorMap,<:AbstractTensorMap} -function check_input(::typeof(qr_full!), t::AbstractTensorMap, (Q, R)::_T_QR) +function check_input(::typeof(qr_full!), t::AbstractTensorMap, (Q, R)::_T_QR, ::AbstractAlgorithm) # scalartype checks @check_scalar Q t @check_scalar R t @@ -252,7 +252,7 @@ function check_input(::typeof(qr_full!), t::AbstractTensorMap, (Q, R)::_T_QR) return nothing end -function check_input(::typeof(qr_compact!), t::AbstractTensorMap, (Q, R)::_T_QR) +function check_input(::typeof(qr_compact!), t::AbstractTensorMap, (Q, R)::_T_QR, ::AbstractAlgorithm) # scalartype checks @check_scalar Q t @check_scalar R t @@ -265,7 +265,7 @@ function check_input(::typeof(qr_compact!), t::AbstractTensorMap, (Q, R)::_T_QR) return nothing end -function check_input(::typeof(qr_null!), t::AbstractTensorMap, N::AbstractTensorMap) +function check_input(::typeof(qr_null!), t::AbstractTensorMap, N::AbstractTensorMap, ::AbstractAlgorithm) # scalartype checks @check_scalar N t @@ -302,7 +302,7 @@ end # ---------------- const _T_LQ = Tuple{<:AbstractTensorMap,<:AbstractTensorMap} -function check_input(::typeof(lq_full!), t::AbstractTensorMap, (L, Q)::_T_LQ) +function check_input(::typeof(lq_full!), t::AbstractTensorMap, (L, Q)::_T_LQ, ::AbstractAlgorithm) # scalartype checks @check_scalar L t @check_scalar Q t @@ -315,7 +315,7 @@ function check_input(::typeof(lq_full!), t::AbstractTensorMap, (L, Q)::_T_LQ) return nothing end -function check_input(::typeof(lq_compact!), t::AbstractTensorMap, (L, Q)::_T_LQ) +function check_input(::typeof(lq_compact!), t::AbstractTensorMap, (L, Q)::_T_LQ, ::AbstractAlgorithm) # scalartype checks @check_scalar L t @check_scalar Q t @@ -328,7 +328,7 @@ function check_input(::typeof(lq_compact!), t::AbstractTensorMap, (L, Q)::_T_LQ) return nothing end -function check_input(::typeof(lq_null!), t::AbstractTensorMap, N) +function check_input(::typeof(lq_null!), t::AbstractTensorMap, N, ::AbstractAlgorithm) # scalartype checks @check_scalar N t @@ -367,7 +367,7 @@ const _T_WP = Tuple{<:AbstractTensorMap,<:AbstractTensorMap} const _T_PWᴴ = Tuple{<:AbstractTensorMap,<:AbstractTensorMap} using MatrixAlgebraKit: PolarViaSVD -function check_input(::typeof(left_polar!), t::AbstractTensorMap, (W, P)::_T_WP) +function check_input(::typeof(left_polar!), t::AbstractTensorMap, (W, P)::_T_WP, ::AbstractAlgorithm) codomain(t) ≿ domain(t) || throw(ArgumentError("Polar decomposition requires `codomain(t) ≿ domain(t)`")) @@ -382,7 +382,7 @@ function check_input(::typeof(left_polar!), t::AbstractTensorMap, (W, P)::_T_WP) return nothing end -function check_input(::typeof(left_orth_polar!), t::AbstractTensorMap, (W, P)::_T_WP) +function check_input(::typeof(left_orth_polar!), t::AbstractTensorMap, (W, P)::_T_WP, ::AbstractAlgorithm) codomain(t) ≿ domain(t) || throw(ArgumentError("Polar decomposition requires `codomain(t) ≿ domain(t)`")) @@ -404,7 +404,7 @@ function initialize_output(::typeof(left_polar!), t::AbstractTensorMap, ::Abstra return W, P end -function check_input(::typeof(right_polar!), t::AbstractTensorMap, (P, Wᴴ)::_T_PWᴴ) +function check_input(::typeof(right_polar!), t::AbstractTensorMap, (P, Wᴴ)::_T_PWᴴ, ::AbstractAlgorithm) codomain(t) ≾ domain(t) || throw(ArgumentError("Polar decomposition requires `domain(t) ≿ codomain(t)`")) @@ -419,7 +419,7 @@ function check_input(::typeof(right_polar!), t::AbstractTensorMap, (P, Wᴴ)::_T return nothing end -function check_input(::typeof(right_orth_polar!), t::AbstractTensorMap, (P, Wᴴ)::_T_PWᴴ) +function check_input(::typeof(right_orth_polar!), t::AbstractTensorMap, (P, Wᴴ)::_T_PWᴴ, ::AbstractAlgorithm) codomain(t) ≾ domain(t) || throw(ArgumentError("Polar decomposition requires `domain(t) ≿ codomain(t)`")) @@ -457,7 +457,7 @@ end const _T_VC = Tuple{<:AbstractTensorMap,<:AbstractTensorMap} const _T_CVᴴ = Tuple{<:AbstractTensorMap,<:AbstractTensorMap} -function check_input(::typeof(left_orth!), t::AbstractTensorMap, (V, C)::_T_VC) +function check_input(::typeof(left_orth!), t::AbstractTensorMap, (V, C)::_T_VC, ::AbstractAlgorithm) # scalartype checks @check_scalar V t isnothing(C) || @check_scalar C t @@ -470,7 +470,7 @@ function check_input(::typeof(left_orth!), t::AbstractTensorMap, (V, C)::_T_VC) return nothing end -function check_input(::typeof(right_orth!), t::AbstractTensorMap, (C, Vᴴ)::_T_CVᴴ) +function check_input(::typeof(right_orth!), t::AbstractTensorMap, (C, Vᴴ)::_T_CVᴴ, ::AbstractAlgorithm) # scalartype checks isnothing(C) || @check_scalar C t @check_scalar Vᴴ t @@ -499,7 +499,7 @@ end # Nullspace # --------- -function check_input(::typeof(left_null!), t::AbstractTensorMap, N) +function check_input(::typeof(left_null!), t::AbstractTensorMap, N, ::AbstractAlgorithm) # scalartype checks @check_scalar N t @@ -511,7 +511,7 @@ function check_input(::typeof(left_null!), t::AbstractTensorMap, N) return nothing end -function check_input(::typeof(right_null!), t::AbstractTensorMap, N) +function check_input(::typeof(right_null!), t::AbstractTensorMap, N, ::AbstractAlgorithm) @check_scalar N t # space checks diff --git a/test/runtests.jl b/test/runtests.jl index 9e44b8a3a..52e8c5484 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -113,15 +113,15 @@ VSU₂U₁ = (Vect[SU2Irrep ⊠ U1Irrep]((0, 0) => 1, (1 // 2, -1) => 1), # ℂ[SU3Irrep]((0, 0, 0) => 1, (1, 0, 0) => 1, (1, 1, 0) => 1)') Ti = time() -include("fusiontrees.jl") +#include("fusiontrees.jl") include("spaces.jl") include("tensors.jl") include("diagonal.jl") include("planar.jl") # TODO: remove once we know AD is slow on macOS CI -if !(Sys.isapple() && get(ENV, "CI", "false") == "true") && isempty(VERSION.prerelease) +#=if !(Sys.isapple() && get(ENV, "CI", "false") == "true") && isempty(VERSION.prerelease) include("ad.jl") -end +end=# include("bugfixes.jl") Tf = time() printstyled("Finished all tests in ", From 6813a77a27c4bf30008c14db74a49007a62fdc83 Mon Sep 17 00:00:00 2001 From: Katharine Hyatt Date: Thu, 21 Aug 2025 12:35:01 +0200 Subject: [PATCH 63/70] Added a few more eig/eigh/svd tests --- test/diagonal.jl | 10 ++++++++++ test/tensors.jl | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/test/diagonal.jl b/test/diagonal.jl index e5fcaa430..0f67836dd 100644 --- a/test/diagonal.jl +++ b/test/diagonal.jl @@ -200,6 +200,16 @@ diagspacelist = ((ℂ^4)', ℂ[Z2Irrep](0 => 2, 1 => 3), @test all(((s, t),) -> isapprox(s, t), zip(values(LinearAlgebra.eigvals(D)), values(LinearAlgebra.eigvals(t)))) + D, W = @constinferred eig!(t) + @test D === t + @test W == one(t) + @test t * W ≈ W * D + D2, V2 = @constinferred eigh!(t2) + if T <: Real + @test D2 === t2 + end + @test V2 == one(t) + @test t2 * V2 ≈ V2 * D2 end @testset "leftorth with $alg" for alg in (TensorKit.QR(), TensorKit.QL()) Q, R = @constinferred leftorth(t; alg=alg) diff --git a/test/tensors.jl b/test/tensors.jl index cd0071b2e..2b73d49bd 100644 --- a/test/tensors.jl +++ b/test/tensors.jl @@ -490,6 +490,11 @@ for V in spacelist for (c, b) in s @test b ≈ s′[c] end + s = LinearAlgebra.svdvals(t2') + s′ = LinearAlgebra.diag(S') + for (c, b) in s + @test b ≈ s′[c] + end end @testset "cond and rank" begin t2 = permute(t, ((3, 4, 2), (1, 5))) @@ -507,6 +512,10 @@ for V in spacelist λmax = maximum(s -> maximum(abs, s), values(vals)) λmin = minimum(s -> minimum(abs, s), values(vals)) @test cond(t4) ≈ λmax / λmin + vals = LinearAlgebra.eigvals(t4') + λmax = maximum(s -> maximum(abs, s), values(vals)) + λmin = minimum(s -> minimum(abs, s), values(vals)) + @test cond(t4') ≈ λmax / λmin end end @testset "empty tensor" begin From e15ccf8451cfcb67cce069454a8eb8bd064d6ea8 Mon Sep 17 00:00:00 2001 From: Katharine Hyatt Date: Thu, 21 Aug 2025 14:20:17 +0200 Subject: [PATCH 64/70] Excise MatrixAlgebra module entirely to use MatrixAlgebraKit --- src/auxiliary/linalg.jl | 330 ------------------ src/auxiliary/random.jl | 2 +- src/tensors/factorizations/factorizations.jl | 3 +- src/tensors/factorizations/implementations.jl | 2 +- .../factorizations/matrixalgebrakit.jl | 6 +- src/tensors/linalg.jl | 12 +- 6 files changed, 13 insertions(+), 342 deletions(-) diff --git a/src/auxiliary/linalg.jl b/src/auxiliary/linalg.jl index 4277be67f..5be245fc5 100644 --- a/src/auxiliary/linalg.jl +++ b/src/auxiliary/linalg.jl @@ -46,35 +46,6 @@ Base.adjoint(alg::Union{SVD,SDD,Polar}) = alg const OFA = OrthogonalFactorizationAlgorithm const SVDAlg = Union{SVD,SDD} -# Matrix algebra: entrypoint for calling matrix methods from within tensor implementations -#------------------------------------------------------------------------------------------ -module MatrixAlgebra -# TODO: all methods tha twe define here will need an extended version for CuMatrix in the -# CUDA package extension. - -# TODO: other methods to include here: -# mul! (possibly call matmul! instead) -# adjoint! -# sylvester -# exp! -# schur!? -# - -using LinearAlgebra -using LinearAlgebra: BlasFloat, BlasReal, BlasComplex, checksquare - -using ..TensorKit: OrthogonalFactorizationAlgorithm, - QL, QLpos, QR, QRpos, LQ, LQpos, RQ, RQpos, SVD, SDD, Polar - -# only defined in >v1.7 -@static if VERSION < v"1.7-" - _rf_findmax((fm, im), (fx, ix)) = isless(fm, fx) ? (fx, ix) : (fm, im) - _argmax(f, domain) = mapfoldl(x -> (f(x), x), _rf_findmax, domain)[2] -else - _argmax(f, domain) = argmax(f, domain) -end - -# TODO: define for CuMatrix if we support this function one!(A::StridedMatrix) length(A) > 0 || return A copyto!(A, LinearAlgebra.I) @@ -83,304 +54,3 @@ end safesign(s::Real) = ifelse(s < zero(s), -one(s), +one(s)) safesign(s::Complex) = ifelse(iszero(s), one(s), s / abs(s)) - -function leftorth!(A::StridedMatrix{<:BlasFloat}, alg::Union{QR,QRpos}, atol::Real) - iszero(atol) || throw(ArgumentError("nonzero atol not supported by $alg")) - m, n = size(A) - k = min(m, n) - A, T = LAPACK.geqrt!(A, min(minimum(size(A)), 36)) - Q = similar(A, m, k) - for j in 1:k - for i in 1:m - Q[i, j] = i == j - end - end - Q = LAPACK.gemqrt!('L', 'N', A, T, Q) - R = triu!(A[1:k, :]) - - if isa(alg, QRpos) - @inbounds for j in 1:k - s = safesign(R[j, j]) - @simd for i in 1:m - Q[i, j] *= s - end - end - @inbounds for j in size(R, 2):-1:1 - for i in 1:min(k, j) - R[i, j] = R[i, j] * conj(safesign(R[i, i])) - end - end - end - return Q, R -end - -function leftorth!(A::StridedMatrix{<:BlasFloat}, alg::Union{QL,QLpos}, atol::Real) - iszero(atol) || throw(ArgumentError("nonzero atol not supported by $alg")) - m, n = size(A) - @assert m >= n - - nhalf = div(n, 2) - #swap columns in A - @inbounds for j in 1:nhalf, i in 1:m - A[i, j], A[i, n + 1 - j] = A[i, n + 1 - j], A[i, j] - end - Q, R = leftorth!(A, isa(alg, QL) ? QR() : QRpos(), atol) - - #swap columns in Q - @inbounds for j in 1:nhalf, i in 1:m - Q[i, j], Q[i, n + 1 - j] = Q[i, n + 1 - j], Q[i, j] - end - #swap rows and columns in R - @inbounds for j in 1:nhalf, i in 1:n - R[i, j], R[n + 1 - i, n + 1 - j] = R[n + 1 - i, n + 1 - j], R[i, j] - end - if isodd(n) - j = nhalf + 1 - @inbounds for i in 1:nhalf - R[i, j], R[n + 1 - i, j] = R[n + 1 - i, j], R[i, j] - end - end - return Q, R -end - -function leftorth!(A::StridedMatrix{<:BlasFloat}, alg::Union{SVD,SDD,Polar}, atol::Real) - U, S, V = alg isa SVD ? LAPACK.gesvd!('S', 'S', A) : LAPACK.gesdd!('S', A) - if isa(alg, Union{SVD,SDD}) - n = count(s -> s .> atol, S) - if n != length(S) - return U[:, 1:n], lmul!(Diagonal(S[1:n]), V[1:n, :]) - else - return U, lmul!(Diagonal(S), V) - end - else - iszero(atol) || throw(ArgumentError("nonzero atol not supported by $alg")) - # TODO: check Lapack to see if we can recycle memory of A - Q = mul!(A, U, V) - Sq = map!(sqrt, S, S) - SqV = lmul!(Diagonal(Sq), V) - R = SqV' * SqV - return Q, R - end -end - -function leftnull!(A::StridedMatrix{<:BlasFloat}, alg::Union{QR,QRpos}, atol::Real) - iszero(atol) || throw(ArgumentError("nonzero atol not supported by $alg")) - m, n = size(A) - m >= n || throw(ArgumentError("no null space if less rows than columns")) - - A, T = LAPACK.geqrt!(A, min(minimum(size(A)), 36)) - N = similar(A, m, max(0, m - n)) - fill!(N, 0) - for k in 1:(m - n) - N[n + k, k] = 1 - end - return N = LAPACK.gemqrt!('L', 'N', A, T, N) -end - -function leftnull!(A::StridedMatrix{<:BlasFloat}, alg::Union{SVD,SDD}, atol::Real) - size(A, 2) == 0 && return one!(similar(A, (size(A, 1), size(A, 1)))) - U, S, V = alg isa SVD ? LAPACK.gesvd!('A', 'N', A) : LAPACK.gesdd!('A', A) - indstart = count(>(atol), S) + 1 - return U[:, indstart:end] -end - -function rightorth!(A::StridedMatrix{<:BlasFloat}, alg::Union{LQ,LQpos,RQ,RQpos}, - atol::Real) - iszero(atol) || throw(ArgumentError("nonzero atol not supported by $alg")) - # TODO: geqrfp seems a bit slower than geqrt in the intermediate region around - # matrix size 100, which is the interesting region. => Investigate and fix - m, n = size(A) - k = min(m, n) - At = transpose!(similar(A, n, m), A) - - if isa(alg, RQ) || isa(alg, RQpos) - @assert m <= n - - mhalf = div(m, 2) - # swap columns in At - @inbounds for j in 1:mhalf, i in 1:n - At[i, j], At[i, m + 1 - j] = At[i, m + 1 - j], At[i, j] - end - Qt, Rt = leftorth!(At, isa(alg, RQ) ? QR() : QRpos(), atol) - - @inbounds for j in 1:mhalf, i in 1:n - Qt[i, j], Qt[i, m + 1 - j] = Qt[i, m + 1 - j], Qt[i, j] - end - @inbounds for j in 1:mhalf, i in 1:m - Rt[i, j], Rt[m + 1 - i, m + 1 - j] = Rt[m + 1 - i, m + 1 - j], Rt[i, j] - end - if isodd(m) - j = mhalf + 1 - @inbounds for i in 1:mhalf - Rt[i, j], Rt[m + 1 - i, j] = Rt[m + 1 - i, j], Rt[i, j] - end - end - Q = transpose!(A, Qt) - R = transpose!(similar(A, (m, m)), Rt) # TODO: efficient in place - return R, Q - else - Qt, Lt = leftorth!(At, alg', atol) - if m > n - L = transpose!(A, Lt) - Q = transpose!(similar(A, (n, n)), Qt) # TODO: efficient in place - else - Q = transpose!(A, Qt) - L = transpose!(similar(A, (m, m)), Lt) # TODO: efficient in place - end - return L, Q - end -end - -function rightorth!(A::StridedMatrix{<:BlasFloat}, alg::Union{SVD,SDD,Polar}, atol::Real) - U, S, V = alg isa SVD ? LAPACK.gesvd!('S', 'S', A) : LAPACK.gesdd!('S', A) - if isa(alg, Union{SVD,SDD}) - n = count(s -> s .> atol, S) - if n != length(S) - return rmul!(U[:, 1:n], Diagonal(S[1:n])), V[1:n, :] - else - return rmul!(U, Diagonal(S)), V - end - else - iszero(atol) || throw(ArgumentError("nonzero atol not supported by $alg")) - Q = mul!(A, U, V) - Sq = map!(sqrt, S, S) - USq = rmul!(U, Diagonal(Sq)) - L = USq * USq' - return L, Q - end -end - -function rightnull!(A::StridedMatrix{<:BlasFloat}, alg::Union{LQ,LQpos}, atol::Real) - iszero(atol) || throw(ArgumentError("nonzero atol not supported by $alg")) - m, n = size(A) - k = min(m, n) - At = adjoint!(similar(A, n, m), A) - At, T = LAPACK.geqrt!(At, min(k, 36)) - N = similar(A, max(n - m, 0), n) - fill!(N, 0) - for k in 1:(n - m) - N[k, m + k] = 1 - end - return N = LAPACK.gemqrt!('R', eltype(At) <: Real ? 'T' : 'C', At, T, N) -end - -function rightnull!(A::StridedMatrix{<:BlasFloat}, alg::Union{SVD,SDD}, atol::Real) - size(A, 1) == 0 && return one!(similar(A, (size(A, 2), size(A, 2)))) - U, S, V = alg isa SVD ? LAPACK.gesvd!('N', 'A', A) : LAPACK.gesdd!('A', A) - indstart = count(>(atol), S) + 1 - return V[indstart:end, :] -end - -function svd!(A::StridedMatrix{T}, alg::Union{SVD,SDD}) where {T<:BlasFloat} - # fix another type instability in LAPACK wrappers - TT = Tuple{Matrix{T},Vector{real(T)},Matrix{T}} - U, S, V = alg isa SVD ? LAPACK.gesvd!('S', 'S', A)::TT : LAPACK.gesdd!('S', A)::TT - return U, S, V -end - -function eig!(A::StridedMatrix{T}; permute::Bool=true, scale::Bool=true) where {T<:BlasReal} - n = checksquare(A) - n == 0 && return zeros(Complex{T}, 0), zeros(Complex{T}, 0, 0) - - A, DR, DI, VL, VR, _ = LAPACK.geevx!(permute ? (scale ? 'B' : 'P') : - (scale ? 'S' : 'N'), 'N', 'V', 'N', A) - D = complex.(DR, DI) - V = zeros(Complex{T}, n, n) - j = 1 - while j <= n - if DI[j] == 0 - vr = view(VR, :, j) - s = conj(sign(_argmax(abs, vr))) - V[:, j] .= s .* vr - else - vr = view(VR, :, j) - vi = view(VR, :, j + 1) - s = conj(sign(_argmax(abs, vr))) # vectors coming from lapack have already real absmax component - V[:, j] .= s .* (vr .+ im .* vi) - V[:, j + 1] .= s .* (vr .- im .* vi) - j += 1 - end - j += 1 - end - return D, V -end - -function eig!(A::StridedMatrix{T}; permute::Bool=true, - scale::Bool=true) where {T<:BlasComplex} - n = checksquare(A) - n == 0 && return zeros(T, 0), zeros(T, 0, 0) - D, V = LAPACK.geevx!(permute ? (scale ? 'B' : 'P') : (scale ? 'S' : 'N'), 'N', 'V', 'N', - A)[[2, 4]] - for j in 1:n - v = view(V, :, j) - s = conj(sign(_argmax(abs, v))) - v .*= s - end - return D, V -end - -function eigh!(A::StridedMatrix{T}) where {T<:BlasFloat} - n = checksquare(A) - n == 0 && return zeros(real(T), 0), zeros(T, 0, 0) - D, V = LAPACK.syevr!('V', 'A', 'U', A, 0.0, 0.0, 0, 0, -1.0) - for j in 1:n - v = view(V, :, j) - s = conj(sign(_argmax(abs, v))) - v .*= s - end - return D, V -end - -## Old stuff and experiments - -# using LinearAlgebra: BlasFloat, Char, BlasInt, LAPACK, LAPACKException, -# DimensionMismatch, SingularException, PosDefException, chkstride1, -# checksquare, -# triu! - -# TODO: reconsider the following implementation -# Unfortunately, geqrfp seems a bit slower than geqrt in the intermediate region -# around matrix size 100, which is the interesting region. => Investigate and maybe fix -# function _leftorth!(A::StridedMatrix{<:BlasFloat}) -# m, n = size(A) -# A, τ = geqrfp!(A) -# Q = LAPACK.ormqr!('L', 'N', A, τ, eye(eltype(A), m, min(m, n))) -# R = triu!(A[1:min(m, n), :]) -# return Q, R -# end - -# geqrfp!: computes qrpos factorization, missing in Base -# geqrfp!(A::StridedMatrix{<:BlasFloat}) = -# ((m, n) = size(A); geqrfp!(A, similar(A, min(m, n)))) -# -# for (geqrfp, elty, relty) in -# ((:dgeqrfp_, :Float64, :Float64), (:sgeqrfp_, :Float32, :Float32), -# (:zgeqrfp_, :ComplexF64, :Float64), (:cgeqrfp_, :ComplexF32, :Float32)) -# @eval begin -# function geqrfp!(A::StridedMatrix{$elty}, tau::StridedVector{$elty}) -# chkstride1(A, tau) -# m, n = size(A) -# if length(tau) != min(m, n) -# throw(DimensionMismatch("tau has length $(length(tau)), but needs length $(min(m, n))")) -# end -# work = Vector{$elty}(1) -# lwork = BlasInt(-1) -# info = Ref{BlasInt}() -# for i = 1:2 # first call returns lwork as work[1] -# ccall((@blasfunc($geqrfp), liblapack), Nothing, -# (Ptr{BlasInt}, Ptr{BlasInt}, Ptr{$elty}, Ptr{BlasInt}, -# Ptr{$elty}, Ptr{$elty}, Ptr{BlasInt}, Ptr{BlasInt}), -# Ref(m), Ref(n), A, Ref(max(1, stride(A, 2))), -# tau, work, Ref(lwork), info) -# chklapackerror(info[]) -# if i == 1 -# lwork = BlasInt(real(work[1])) -# resize!(work, lwork) -# end -# end -# A, tau -# end -# end -# end - -end diff --git a/src/auxiliary/random.jl b/src/auxiliary/random.jl index bc9df6f65..3289cdc3c 100644 --- a/src/auxiliary/random.jl +++ b/src/auxiliary/random.jl @@ -20,6 +20,6 @@ function randisometry!(rng::Random.AbstractRNG, A::AbstractMatrix) dims = size(A) dims[1] >= dims[2] || throw(DimensionMismatch("cannot create isometric matrix with dimensions $dims; isometry needs to be tall or square")) - Q, = MatrixAlgebra.leftorth!(Random.randn!(rng, A), QRpos(), 0) + Q, = leftorth!(Random.randn!(rng, A); alg=QRpos()) return copy!(A, Q) end diff --git a/src/tensors/factorizations/factorizations.jl b/src/tensors/factorizations/factorizations.jl index eb1913cad..c4db4beb8 100644 --- a/src/tensors/factorizations/factorizations.jl +++ b/src/tensors/factorizations/factorizations.jl @@ -12,7 +12,6 @@ export TruncationScheme, notrunc, truncbelow, truncerr, truncdim, truncspace using ..TensorKit using ..TensorKit: AdjointTensorMap, SectorDict, OFA, blocktype, foreachblock -using ..MatrixAlgebra: MatrixAlgebra using LinearAlgebra: LinearAlgebra, BlasFloat, Diagonal, svdvals, svdvals! import LinearAlgebra: eigen, eigen!, isposdef, isposdef!, ishermitian @@ -112,7 +111,7 @@ function _compute_svddata!(d::DiagonalTensorMap, alg::Union{SVD,SDD}) V = zerovector!(similar(b.diag, lb, lb)) p = sortperm(b.diag; by=abs, rev=true) for (i, pi) in enumerate(p) - U[pi, i] = MatrixAlgebra.safesign(b.diag[pi]) + U[pi, i] = safesign(b.diag[pi]) V[i, pi] = 1 end Σ = abs.(view(b.diag, p)) diff --git a/src/tensors/factorizations/implementations.jl b/src/tensors/factorizations/implementations.jl index 02c59b492..d00703549 100644 --- a/src/tensors/factorizations/implementations.jl +++ b/src/tensors/factorizations/implementations.jl @@ -3,7 +3,7 @@ _kindof(::Union{QR,QRpos}) = :qr _kindof(::Union{LQ,LQpos}) = :lq _kindof(::Polar) = :polar -leftorth!(t::AbstractTensorMap; alg=nothing, kwargs...) = _leftorth!(t, alg; kwargs...) +leftorth!(t; alg=nothing, kwargs...) = _leftorth!(t, alg; kwargs...) function _leftorth!(t::AbstractTensorMap, alg::Nothing, ; kwargs...) return isempty(kwargs) ? left_orth!(t) : left_orth!(t; trunc=(; kwargs...)) diff --git a/src/tensors/factorizations/matrixalgebrakit.jl b/src/tensors/factorizations/matrixalgebrakit.jl index e0270d107..34a888f30 100644 --- a/src/tensors/factorizations/matrixalgebrakit.jl +++ b/src/tensors/factorizations/matrixalgebrakit.jl @@ -3,7 +3,7 @@ for f! in [:svd_compact!, :svd_full!, :svd_trunc!, :svd_vals!, :qr_compact!, :qr_full!, :qr_null!, :lq_compact!, :lq_full!, :lq_null!, :eig_full!, :eig_trunc!, :eig_vals!, :eigh_full!, - :eigh_trunc!, :eigh_vals!, :left_polar!, :right_polar!] + :eigh_trunc!, :eigh_vals!, :left_polar!, :right_polar!, :left_orth!, :right_orth!] @eval function default_algorithm(::typeof($f!), ::Type{T}; kwargs...) where {T<:AbstractTensorMap} return default_algorithm($f!, blocktype(T); kwargs...) @@ -24,7 +24,9 @@ for f! in (:qr_compact!, :qr_full!, :lq_compact!, :lq_full!, :eig_full!, :eigh_full!, :svd_compact!, :svd_full!, - :left_polar!, :left_orth_polar!, :right_polar!, :right_orth_polar!) + :left_polar!, :left_orth_polar!, + :right_polar!, :right_orth_polar!, + :left_orth!, :right_orth!) @eval function $f!(t::AbstractTensorMap, F, alg::AbstractAlgorithm) check_input($f!, t, F, alg) diff --git a/src/tensors/linalg.jl b/src/tensors/linalg.jl index f29bdf809..51567dec7 100644 --- a/src/tensors/linalg.jl +++ b/src/tensors/linalg.jl @@ -59,7 +59,7 @@ function one!(t::AbstractTensorMap) domain(t) == codomain(t) || throw(SectorMismatch("no identity if domain and codomain are different")) for (c, b) in blocks(t) - MatrixAlgebra.one!(b) + one!(b) end return t end @@ -106,7 +106,7 @@ function isomorphism!(t::AbstractTensorMap) domain(t) ≅ codomain(t) || throw(SpaceMismatch(lazy"domain and codomain are not isomorphic: $(space(t))")) for (_, b) in blocks(t) - MatrixAlgebra.one!(b) + one!(b) end return t end @@ -155,7 +155,7 @@ function isometry!(t::AbstractTensorMap) domain(t) ≾ codomain(t) || throw(SpaceMismatch(lazy"domain and codomain are not isometrically embeddable: $(space(t))")) for (_, b) in blocks(t) - MatrixAlgebra.one!(b) + one!(b) end return t end @@ -377,7 +377,7 @@ function Base.inv(t::AbstractTensorMap) T = float(scalartype(t)) tinv = similar(t, T, dom ← cod) for (c, b) in blocks(t) - binv = MatrixAlgebra.one!(block(tinv, c)) + binv = one!(block(tinv, c)) ldiv!(lu(b), binv) end return tinv @@ -449,11 +449,11 @@ for f in (:cos, :sin, :tan, :cot, :cosh, :sinh, :tanh, :coth, :atan, :acot, :asi tf = similar(t, T) if T <: Real for (c, b) in blocks(t) - copy!(block(tf, c), real(MatrixAlgebra.$f(b))) + copy!(block(tf, c), real(MatrixAlgebraKit.$f(b))) end else for (c, b) in blocks(t) - copy!(block(tf, c), MatrixAlgebra.$f(b)) + copy!(block(tf, c), MatrixAlgebraKit.$f(b)) end end return tf From 08b83219472b5285100d2e90f7643a7206fa5d8f Mon Sep 17 00:00:00 2001 From: Katharine Hyatt Date: Thu, 21 Aug 2025 15:10:21 +0200 Subject: [PATCH 65/70] Restore argmax and fix AD test --- src/auxiliary/linalg.jl | 2 ++ test/ad.jl | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/auxiliary/linalg.jl b/src/auxiliary/linalg.jl index 5be245fc5..9d8ac7818 100644 --- a/src/auxiliary/linalg.jl +++ b/src/auxiliary/linalg.jl @@ -54,3 +54,5 @@ end safesign(s::Real) = ifelse(s < zero(s), -one(s), +one(s)) safesign(s::Complex) = ifelse(iszero(s), one(s), s / abs(s)) + +_argmax(f, domain) = argmax(f, domain) diff --git a/test/ad.jl b/test/ad.jl index 9f5eb2a5b..c5c2aabfc 100644 --- a/test/ad.jl +++ b/test/ad.jl @@ -447,7 +447,7 @@ Vlist = ((ℂ^2, (ℂ^3)', ℂ^3, ℂ^2, (ℂ^2)'), T <: Complex && remove_svdgauge_depence!(ΔU, ΔV, U, S, V) test_rrule(tsvd, C; atol, output_tangent=(ΔU, ΔS, ΔV)) - c, = TensorKit.MatrixAlgebra._argmax(x -> sqrt(dim(x[1])) * maximum(diag(x[2])), + c, = TensorKit._argmax(x -> sqrt(dim(x[1])) * maximum(diag(x[2])), blocks(S)) trunc = truncdim(round(Int, 2 * dim(c))) U, S, V = tsvd(C; trunc) From 18b74211eb8fe73852db8acf644905d9a8bbb2dc Mon Sep 17 00:00:00 2001 From: Katharine Hyatt Date: Thu, 21 Aug 2025 17:20:39 +0200 Subject: [PATCH 66/70] Respond to comments --- src/auxiliary/linalg.jl | 8 -------- src/tensors/factorizations/factorizations.jl | 6 ++++-- test/ad.jl | 3 +-- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/auxiliary/linalg.jl b/src/auxiliary/linalg.jl index 9d8ac7818..8a20eb8ac 100644 --- a/src/auxiliary/linalg.jl +++ b/src/auxiliary/linalg.jl @@ -46,13 +46,5 @@ Base.adjoint(alg::Union{SVD,SDD,Polar}) = alg const OFA = OrthogonalFactorizationAlgorithm const SVDAlg = Union{SVD,SDD} -function one!(A::StridedMatrix) - length(A) > 0 || return A - copyto!(A, LinearAlgebra.I) - return A -end - safesign(s::Real) = ifelse(s < zero(s), -one(s), +one(s)) safesign(s::Complex) = ifelse(iszero(s), one(s), s / abs(s)) - -_argmax(f, domain) = argmax(f, domain) diff --git a/src/tensors/factorizations/factorizations.jl b/src/tensors/factorizations/factorizations.jl index c4db4beb8..94124a04f 100644 --- a/src/tensors/factorizations/factorizations.jl +++ b/src/tensors/factorizations/factorizations.jl @@ -7,11 +7,11 @@ export eig, eig!, eigh, eigh! export tsvd, tsvd!, svdvals, svdvals! export leftorth, leftorth!, rightorth, rightorth! export leftnull, leftnull!, rightnull, rightnull! -export copy_oftype, permutedcopy_oftype +export copy_oftype, permutedcopy_oftype, one! export TruncationScheme, notrunc, truncbelow, truncerr, truncdim, truncspace using ..TensorKit -using ..TensorKit: AdjointTensorMap, SectorDict, OFA, blocktype, foreachblock +using ..TensorKit: AdjointTensorMap, SectorDict, OFA, blocktype, foreachblock, one! using LinearAlgebra: LinearAlgebra, BlasFloat, Diagonal, svdvals, svdvals! import LinearAlgebra: eigen, eigen!, isposdef, isposdef!, ishermitian @@ -41,6 +41,8 @@ include("matrixalgebrakit.jl") include("truncation.jl") include("deprecations.jl") +TensorKit.one!(A::AbstractMatrix) = MatrixAlgebraKit.one!(A) + function isisometry(t::AbstractTensorMap, (p₁, p₂)::Index2Tuple) t = permute(t, (p₁, p₂); copy=false) return isisometry(t) diff --git a/test/ad.jl b/test/ad.jl index c5c2aabfc..4013846d0 100644 --- a/test/ad.jl +++ b/test/ad.jl @@ -447,8 +447,7 @@ Vlist = ((ℂ^2, (ℂ^3)', ℂ^3, ℂ^2, (ℂ^2)'), T <: Complex && remove_svdgauge_depence!(ΔU, ΔV, U, S, V) test_rrule(tsvd, C; atol, output_tangent=(ΔU, ΔS, ΔV)) - c, = TensorKit._argmax(x -> sqrt(dim(x[1])) * maximum(diag(x[2])), - blocks(S)) + c, = argmax(x -> sqrt(dim(x[1])) * maximum(diag(x[2])), blocks(S)) trunc = truncdim(round(Int, 2 * dim(c))) U, S, V = tsvd(C; trunc) ΔU = randn(scalartype(U), space(U)) From 59bdfd8637ffea447626736dcf8064c89eaa1beb Mon Sep 17 00:00:00 2001 From: Katharine Hyatt Date: Thu, 21 Aug 2025 18:04:21 +0200 Subject: [PATCH 67/70] Remove unneeded default_algorithm --- src/tensors/factorizations/matrixalgebrakit.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tensors/factorizations/matrixalgebrakit.jl b/src/tensors/factorizations/matrixalgebrakit.jl index 34a888f30..552b48ce6 100644 --- a/src/tensors/factorizations/matrixalgebrakit.jl +++ b/src/tensors/factorizations/matrixalgebrakit.jl @@ -3,7 +3,7 @@ for f! in [:svd_compact!, :svd_full!, :svd_trunc!, :svd_vals!, :qr_compact!, :qr_full!, :qr_null!, :lq_compact!, :lq_full!, :lq_null!, :eig_full!, :eig_trunc!, :eig_vals!, :eigh_full!, - :eigh_trunc!, :eigh_vals!, :left_polar!, :right_polar!, :left_orth!, :right_orth!] + :eigh_trunc!, :eigh_vals!, :left_polar!, :right_polar!] @eval function default_algorithm(::typeof($f!), ::Type{T}; kwargs...) where {T<:AbstractTensorMap} return default_algorithm($f!, blocktype(T); kwargs...) From 7838bc847efa1e762300aaf27c1041ff9d42bb0c Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sun, 31 Aug 2025 20:26:25 +0200 Subject: [PATCH 68/70] clean up handling AdjointTensorMap --- src/tensors/factorizations/adjoint.jl | 90 +++++++++++++++++++ src/tensors/factorizations/factorizations.jl | 32 +------ .../factorizations/matrixalgebrakit.jl | 13 +-- 3 files changed, 94 insertions(+), 41 deletions(-) create mode 100644 src/tensors/factorizations/adjoint.jl diff --git a/src/tensors/factorizations/adjoint.jl b/src/tensors/factorizations/adjoint.jl new file mode 100644 index 000000000..918248c29 --- /dev/null +++ b/src/tensors/factorizations/adjoint.jl @@ -0,0 +1,90 @@ +# AdjointTensorMap +# ---------------- +# 1-arg functions +function initialize_output(::typeof(left_null!), t::AdjointTensorMap, + alg::AbstractAlgorithm) + return adjoint(initialize_output(right_null!, adjoint(t), alg)) +end +function initialize_output(::typeof(right_null!), t::AdjointTensorMap, + alg::AbstractAlgorithm) + return adjoint(initialize_output(left_null!, adjoint(t), alg)) +end + +function left_null!(t::AdjointTensorMap, N::AdjointTensorMap, alg::AbstractAlgorithm) + right_null!(adjoint(t), adjoint(N), alg) + return N +end +function right_null!(t::AdjointTensorMap, N::AdjointTensorMap, alg::AbstractAlgorithm) + left_null!(adjoint(t), adjoint(N), alg) + return N +end + +# 2-arg functions +for (left_f!, right_f!) in zip((:qr_full!, :qr_compact!, :left_polar!, :left_orth!), + (:lq_full!, :lq_compact!, :right_polar!, :right_orth!)) + @eval function initialize_output(::typeof($left_f!), t::AdjointTensorMap, + alg::AbstractAlgorithm) + return reverse(adjoint.(initialize_output($right_f!, adjoint(t), alg))) + end + @eval function initialize_output(::typeof($right_f!), t::AdjointTensorMap, + alg::AbstractAlgorithm) + return reverse(adjoint.(initialize_output($left_f!, adjoint(t), alg))) + end + + @eval function $left_f!(t::AdjointTensorMap, + F::Tuple{AdjointTensorMap,AdjointTensorMap}, + alg::AbstractAlgorithm) + $right_f!(adjoint(t), reverse(adjoint.(F)), alg) + return F + end + @eval function $right_f!(t::AdjointTensorMap, + F::Tuple{AdjointTensorMap,AdjointTensorMap}, + alg::AbstractAlgorithm) + $left_f!(adjoint(t), reverse(adjoint.(F)), alg) + return F + end +end + +# 3-arg functions +for f! in (:svd_full!, :svd_compact!, :svd_trunc!) + @eval function initialize_output(::typeof($f!), t::AdjointTensorMap, + alg::AbstractAlgorithm) + return reverse(adjoint.(initialize_output($f!, adjoint(t), alg))) + end + _TS = f! === :svd_full! ? :AdjointTensorMap : DiagonalTensorMap + @eval function $f!(t::AdjointTensorMap, + F::Tuple{AdjointTensorMap,$_TS,AdjointTensorMap}, + alg::AbstractAlgorithm) + $f!(adjoint(t), reverse(adjoint.(F)), alg) + return F + end +end + +function leftorth!(t::AdjointTensorMap; alg::OFA=QRpos()) + InnerProductStyle(t) === EuclideanInnerProduct() || + throw_invalid_innerproduct(:leftorth!) + return map(adjoint, reverse(rightorth!(adjoint(t); alg=alg'))) +end + +function rightorth!(t::AdjointTensorMap; alg::OFA=LQpos()) + InnerProductStyle(t) === EuclideanInnerProduct() || + throw_invalid_innerproduct(:rightorth!) + return map(adjoint, reverse(leftorth!(adjoint(t); alg=alg'))) +end + +function leftnull!(t::AdjointTensorMap; alg::OFA=QR(), kwargs...) + InnerProductStyle(t) === EuclideanInnerProduct() || + throw_invalid_innerproduct(:leftnull!) + return adjoint(rightnull!(adjoint(t); alg=alg', kwargs...)) +end + +function rightnull!(t::AdjointTensorMap; alg::OFA=LQ(), kwargs...) + InnerProductStyle(t) === EuclideanInnerProduct() || + throw_invalid_innerproduct(:rightnull!) + return adjoint(leftnull!(adjoint(t); alg=alg', kwargs...)) +end + +function tsvd!(t::AdjointTensorMap; trunc=NoTruncation(), p::Real=2, alg=SDD()) + u, s, vt, err = tsvd!(adjoint(t); trunc=trunc, p=p, alg=alg) + return adjoint(vt), adjoint(s), adjoint(u), err +end diff --git a/src/tensors/factorizations/factorizations.jl b/src/tensors/factorizations/factorizations.jl index 94124a04f..db1b1c3c7 100644 --- a/src/tensors/factorizations/factorizations.jl +++ b/src/tensors/factorizations/factorizations.jl @@ -40,6 +40,7 @@ include("implementations.jl") include("matrixalgebrakit.jl") include("truncation.jl") include("deprecations.jl") +include("adjoint.jl") TensorKit.one!(A::AbstractMatrix) = MatrixAlgebraKit.one!(A) @@ -54,37 +55,6 @@ end #------------------------------------------------------------------------------------------ const RealOrComplexFloat = Union{AbstractFloat,Complex{<:AbstractFloat}} -# AdjointTensorMap -# ---------------- -function leftorth!(t::AdjointTensorMap; alg::OFA=QRpos()) - InnerProductStyle(t) === EuclideanInnerProduct() || - throw_invalid_innerproduct(:leftorth!) - return map(adjoint, reverse(rightorth!(adjoint(t); alg=alg'))) -end - -function rightorth!(t::AdjointTensorMap; alg::OFA=LQpos()) - InnerProductStyle(t) === EuclideanInnerProduct() || - throw_invalid_innerproduct(:rightorth!) - return map(adjoint, reverse(leftorth!(adjoint(t); alg=alg'))) -end - -function leftnull!(t::AdjointTensorMap; alg::OFA=QR(), kwargs...) - InnerProductStyle(t) === EuclideanInnerProduct() || - throw_invalid_innerproduct(:leftnull!) - return adjoint(rightnull!(adjoint(t); alg=alg', kwargs...)) -end - -function rightnull!(t::AdjointTensorMap; alg::OFA=LQ(), kwargs...) - InnerProductStyle(t) === EuclideanInnerProduct() || - throw_invalid_innerproduct(:rightnull!) - return adjoint(leftnull!(adjoint(t); alg=alg', kwargs...)) -end - -function tsvd!(t::AdjointTensorMap; trunc=NoTruncation(), p::Real=2, alg=SDD()) - u, s, vt, err = tsvd!(adjoint(t); trunc=trunc, p=p, alg=alg) - return adjoint(vt), adjoint(s), adjoint(u), err -end - # DiagonalTensorMap # ----------------- function leftorth!(d::DiagonalTensorMap; alg=QR(), kwargs...) diff --git a/src/tensors/factorizations/matrixalgebrakit.jl b/src/tensors/factorizations/matrixalgebrakit.jl index 552b48ce6..eb27654a5 100644 --- a/src/tensors/factorizations/matrixalgebrakit.jl +++ b/src/tensors/factorizations/matrixalgebrakit.jl @@ -556,15 +556,8 @@ function initialize_output(::typeof(right_null!), t::AbstractTensorMap) return N end -for f! in (:left_null_svd!, :right_null_svd!) - @eval function $f!(t::AbstractTensorMap, N, alg, ::Nothing=nothing) - foreachblock(t, N) do _, (b, n) - n′ = $f!(b, n, alg) - # deal with the case where the output is not the same as the input - n === n′ || copyto!(n, n′) - return nothing - end - - return N +for (f!, f_svd!) in zip((:left_null!, :right_null!), (:left_null_svd!, :right_null_svd!)) + @eval function $f_svd!(t::AbstractTensorMap, N, alg, ::Nothing=nothing) + return $f!(t, N, alg) end end From 2257dca4ec7a4a8ce8b086dd9474bd579c035856 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sun, 31 Aug 2025 20:29:42 +0200 Subject: [PATCH 69/70] more adjoint specializations --- src/tensors/factorizations/adjoint.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/tensors/factorizations/adjoint.jl b/src/tensors/factorizations/adjoint.jl index 918248c29..f47ad9549 100644 --- a/src/tensors/factorizations/adjoint.jl +++ b/src/tensors/factorizations/adjoint.jl @@ -19,6 +19,13 @@ function right_null!(t::AdjointTensorMap, N::AdjointTensorMap, alg::AbstractAlgo return N end +function MatrixAlgebraKit.is_left_isometry(t::AdjointTensorMap; kwargs...) + return is_right_isometry(adjoint(t); kwargs...) +end +function MatrixAlgebraKit.is_right_isometry(t::AdjointTensorMap; kwargs...) + return is_left_isometry(adjoint(t); kwargs...) +end + # 2-arg functions for (left_f!, right_f!) in zip((:qr_full!, :qr_compact!, :left_polar!, :left_orth!), (:lq_full!, :lq_compact!, :right_polar!, :right_orth!)) From 1bc5d798b4b3eebd13b56252bfad575378fac85d Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sun, 31 Aug 2025 20:32:16 +0200 Subject: [PATCH 70/70] remove previous adjoint specializations --- src/tensors/factorizations/adjoint.jl | 29 --------------------------- 1 file changed, 29 deletions(-) diff --git a/src/tensors/factorizations/adjoint.jl b/src/tensors/factorizations/adjoint.jl index f47ad9549..08154d15e 100644 --- a/src/tensors/factorizations/adjoint.jl +++ b/src/tensors/factorizations/adjoint.jl @@ -66,32 +66,3 @@ for f! in (:svd_full!, :svd_compact!, :svd_trunc!) return F end end - -function leftorth!(t::AdjointTensorMap; alg::OFA=QRpos()) - InnerProductStyle(t) === EuclideanInnerProduct() || - throw_invalid_innerproduct(:leftorth!) - return map(adjoint, reverse(rightorth!(adjoint(t); alg=alg'))) -end - -function rightorth!(t::AdjointTensorMap; alg::OFA=LQpos()) - InnerProductStyle(t) === EuclideanInnerProduct() || - throw_invalid_innerproduct(:rightorth!) - return map(adjoint, reverse(leftorth!(adjoint(t); alg=alg'))) -end - -function leftnull!(t::AdjointTensorMap; alg::OFA=QR(), kwargs...) - InnerProductStyle(t) === EuclideanInnerProduct() || - throw_invalid_innerproduct(:leftnull!) - return adjoint(rightnull!(adjoint(t); alg=alg', kwargs...)) -end - -function rightnull!(t::AdjointTensorMap; alg::OFA=LQ(), kwargs...) - InnerProductStyle(t) === EuclideanInnerProduct() || - throw_invalid_innerproduct(:rightnull!) - return adjoint(leftnull!(adjoint(t); alg=alg', kwargs...)) -end - -function tsvd!(t::AdjointTensorMap; trunc=NoTruncation(), p::Real=2, alg=SDD()) - u, s, vt, err = tsvd!(adjoint(t); trunc=trunc, p=p, alg=alg) - return adjoint(vt), adjoint(s), adjoint(u), err -end