diff --git a/docs/src/Changelog.md b/docs/src/Changelog.md index 9a6b231b9..282799375 100644 --- a/docs/src/Changelog.md +++ b/docs/src/Changelog.md @@ -26,12 +26,14 @@ When releasing a new version, move the "Unreleased" changes to a new version sec - For sector types with `GenericUnit` such that colorings are not unique, `GradedSpace`, `ProductSpace` and `HomSpace` now check for this compatibility. In particular, this prevents the construction of `TensorMap`s with incompatible colorings, which previously either errored or produced empty tensors inconsistently. ([#515](https://github.com/QuantumKitHub/TensorKit.jl/pull/515)) ### Deprecated +- The type alias `ZNSpace{N}` is deprecated in favour of `Vect[ZNIrrep{N}]` or `Rep[ℤ{N}]`: a type alias cannot compute the storage type from `N`, so the two only agree for small `N`. ([#511](https://github.com/QuantumKitHub/TensorKit.jl/pull/511)) ### Removed ### Fixed ### Performance +- `GradedSpace` operations (`dim`, `flip`, `⊕`, `⊖`, `fuse`, `infimum`, `supremum`, truncation) are now specialised on the storage type of the degeneracy dimensions, and tuple storage is used only for sector types with at most `TensorKit._NTUPLE_STORAGE_THRESHOLD` sectors so that sector types with many sectors no longer burden the compiler. ([#511](https://github.com/QuantumKitHub/TensorKit.jl/pull/511)) ## [0.17.1](https://github.com/QuantumKitHub/TensorKit.jl/compare/v0.17.0...v0.17.1) - 2026-07-13 diff --git a/docs/src/lib/spaces.md b/docs/src/lib/spaces.md index fbeaacb6d..d2bedc868 100644 --- a/docs/src/lib/spaces.md +++ b/docs/src/lib/spaces.md @@ -37,13 +37,18 @@ Vect Rep ``` +The storage that `Vect[I]` selects for the degeneracy dimensions depends on how many sectors `I` has: + +```@docs +TensorKit.sectorstoragetype +``` + In this respect, there are also a number of type aliases for the `GradedSpace` types associated with the most common sectors, namely ```julia -const ZNSpace{N} = Vect[ZNIrrep{N}] -const Z2Space = ZNSpace{2} -const Z3Space = ZNSpace{3} -const Z4Space = ZNSpace{4} +const Z2Space = Rep[ℤ{2}] +const Z3Space = Rep[ℤ{3}] +const Z4Space = Rep[ℤ{4}] const U1Space = Rep[U₁] const CU1Space = Rep[CU₁] const SU2Space = Rep[SU₂] diff --git a/docs/src/man/gradedspaces.md b/docs/src/man/gradedspaces.md index 69342dcba..2d0f986c2 100644 --- a/docs/src/man/gradedspaces.md +++ b/docs/src/man/gradedspaces.md @@ -25,16 +25,17 @@ However, this is mostly to lower the barrier, as really the instances of `Graded ## Implementation details As mentioned, the way in which the degeneracy dimensions ``n_a`` are stored depends on the specific sector type `I`, more specifically on the `IteratorSize` of `values(I)`. -If `IteratorSize(values(I)) isa Union{IsInfinite, SizeUnknown}`, the different sectors ``a`` and their corresponding degeneracy ``n_a`` are stored as key value pairs in an `Associative` array, i.e. a dictionary `dims::SectorDict`. +If `IteratorSize(values(I)) isa Union{IsInfinite, SizeUnknown}`, or if `values(I)` has a known length that exceeds `TensorKit._NTUPLE_STORAGE_THRESHOLD`, the different sectors ``a`` and their corresponding degeneracy ``n_a`` are stored as key value pairs in an `Associative` array, i.e. a dictionary `dims::SectorDict`. As the total number of sectors in `values(I)` can be infinite, only sectors ``a`` for which ``n_a`` are stored. Here, `SectorDict` is a constant type alias for a specific dictionary implementation, which currently resorts to `SortedVectorDict` implemented in TensorKit.jl. Hence, the sectors and their corresponding dimensions are stored as two matching lists (`Vector` instances), which are ordered based on the property `isless(a::I, b::I)`. This ensures that the space ``V = ⨁_a ℂ^{n_a} ⊗ R_{a}`` has some unique canonical order in the direct sum decomposition, such that two different but equal instances created independently always match. -If `IteratorSize(values(I)) isa Union{HasLength, HasShape}`, the degeneracy dimensions `n_a` are stored for all sectors `a ∈ values(I)` (also if `n_a == 0`) in a tuple, more specifically a `NTuple{N, Int}` with `N = length(values(I))`. +If `IteratorSize(values(I)) isa Union{HasLength, HasShape}` and `N = length(values(I))` is at most `TensorKit._NTUPLE_STORAGE_THRESHOLD`, the degeneracy dimensions `n_a` are stored for all sectors `a ∈ values(I)` (also if `n_a == 0`) in a tuple, more specifically a `NTuple{N, Int}`. The methods `getindex(values(I), i)` and `findindex(values(I), a)` are used to map between a sector `a ∈ values(I)` and a corresponding index `i ∈ 1:N`. As `N` is a compile time constant, these types can be created in a type stable manner. -Note however that this implies that for large values of `N`, it can be beneficial to define `IteratorSize(values(a)) = SizeUnknown()` to not overly burden the compiler. +For larger `N` this would overly burden the compiler, which is precisely why the dictionary storage takes over above the threshold. +The exact threshold is documented with [`TensorKit.sectorstoragetype`](@ref), which reports the storage type of a given sector type; the canonical space type is always obtained as `Vect[I]`. ## Constructing instances diff --git a/src/TensorKit.jl b/src/TensorKit.jl index c418fe944..41a7d8ae1 100644 --- a/src/TensorKit.jl +++ b/src/TensorKit.jl @@ -44,7 +44,7 @@ export infimum, supremum, isisomorphic, ismonomorphic, isepimorphic export sectortype, sectors, hassector export unit, rightunit, leftunit, allunits, isunit, otimes, deligneproduct, timereversed export Nsymbol, Fsymbol, Rsymbol, Bsymbol, frobenius_schur_phase, frobenius_schur_indicator, twist, fusiontensor -export sectorscalartype, fusionscalartype, braidingscalartype +export sectorscalartype, fusionscalartype, braidingscalartype, dimscalartype # Export methods for fusion trees export fusiontrees, braid, permute, transpose @@ -107,7 +107,6 @@ export empty_globalcaches! # Imports #--------- using TupleTools -using TupleTools: StaticLength using Strided diff --git a/src/auxiliary/dicts.jl b/src/auxiliary/dicts.jl index 626599c01..642373e19 100644 --- a/src/auxiliary/dicts.jl +++ b/src/auxiliary/dicts.jl @@ -89,21 +89,12 @@ end Base.empty(::SortedVectorDict, ::Type{K}, ::Type{V}) where {K, V} = SortedVectorDict{K, V}() Base.empty!(d::SortedVectorDict) = (empty!(d.keys); empty!(d.values); return d) -# _searchsortedfirst(v::Vector, k) = searchsortedfirst(v, k) -function _searchsortedfirst(v::Vector, k) - i = 1 - @inbounds while i <= length(v) && isless(v[i], k) - i += 1 - end - return i -end - function Base.delete!(d::SortedVectorDict{K}, k) where {K} key = convert(K, k) if !isequal(k, key) return d end - i = _searchsortedfirst(d.keys, key) + i = searchsortedfirst(d.keys, key) if i <= length(d) && isequal(d.keys[i], key) deleteat!(d.keys, i) deleteat!(d.values, i) @@ -118,7 +109,7 @@ function Base.haskey(d::SortedVectorDict{K}, k) where {K} if !isequal(k, key) return false end - i = _searchsortedfirst(d.keys, key) + i = searchsortedfirst(d.keys, key) return (i <= length(d) && isequal(d.keys[i], key)) end function Base.getindex(d::SortedVectorDict{K}, k) where {K} @@ -126,7 +117,7 @@ function Base.getindex(d::SortedVectorDict{K}, k) where {K} if !isequal(k, key) throw(KeyError(k)) end - i = _searchsortedfirst(d.keys, key) + i = searchsortedfirst(d.keys, key) @inbounds if (i <= length(d) && isequal(d.keys[i], key)) return d.values[i] else @@ -138,7 +129,7 @@ function Base.setindex!(d::SortedVectorDict{K}, v, k) where {K} if !isequal(k, key) throw(ArgumentError("$k is not a valid key for type $K")) end - i = _searchsortedfirst(d.keys, key) + i = searchsortedfirst(d.keys, key) if i <= length(d) && isequal(d.keys[i], key) d.values[i] = v else @@ -153,7 +144,7 @@ function Base.get(d::SortedVectorDict{K}, k, default) where {K} if !isequal(k, key) return default end - i = _searchsortedfirst(d.keys, key) + i = searchsortedfirst(d.keys, key) @inbounds begin return (i <= length(d) && isequal(d.keys[i], key)) ? d.values[i] : default end @@ -163,7 +154,7 @@ function Base.get(f::Union{Function, Type}, d::SortedVectorDict{K}, k) where {K} if !isequal(k, key) return f() end - i = _searchsortedfirst(d.keys, key) + i = searchsortedfirst(d.keys, key) @inbounds begin return (i <= length(d) && isequal(d.keys[i], key)) ? d.values[i] : f() end @@ -186,6 +177,72 @@ function Base.:(==)(d1::SortedVectorDict, d2::SortedVectorDict) return true end +# merge two SortedVectorDicts of `GradedSpace` dimensions: `combine(v1, v2)` is applied to keys +# present in both, `unmatched1(v)`/`unmatched2(v)` to keys present in only the first/second dict, +# which may keep the value, drop the entry by returning `nothing`, or throw. Zero results are +# always dropped, since `GradedSpace` never stores an explicit zero dimension. +function _sortedmerge( + combine::F, unmatched1::F1, unmatched2::F2, + d1::SortedVectorDict{K, V}, d2::SortedVectorDict{K, V} + ) where {F, F1, F2, K, V <: Integer} + k1, v1 = d1.keys, d1.values + k2, v2 = d2.keys, d2.values + n1, n2 = length(k1), length(k2) + len = _mergelength(unmatched1, unmatched2, n1, n2) + ks = Vector{K}(undef, len) + vs = Vector{V}(undef, len) + i, j, n = 1, 1, 0 + @inbounds while i <= n1 && j <= n2 + a, b = k1[i], k2[j] + if isless(a, b) + n = _mergestore!(ks, vs, n, a, unmatched1(v1[i])) + i += 1 + elseif isless(b, a) + n = _mergestore!(ks, vs, n, b, unmatched2(v2[j])) + j += 1 + else + n = _mergestore!(ks, vs, n, a, combine(v1[i], v2[j])) + i += 1 + j += 1 + end + end + @inbounds while i <= n1 + n = _mergestore!(ks, vs, n, k1[i], unmatched1(v1[i])) + i += 1 + end + @inbounds while j <= n2 + n = _mergestore!(ks, vs, n, k2[j], unmatched2(v2[j])) + j += 1 + end + resize!(ks, n) + resize!(vs, n) + return SortedVectorDict{K, V}(ks, vs) +end +# write into slot `n + 1` and only advance the length when the value is nonzero +@inline function _mergestore!(ks, vs, n, k, d) + @inbounds ks[n + 1] = k + @inbounds vs[n + 1] = d + return n + !iszero(d) +end +@inline _mergestore!(ks, vs, n, k, ::Nothing) = n + +# upper bound on the number of entries the merge can produce: a dropping handler contributes none +function _mergelength(unmatched1, unmatched2, n1, n2) + drop = Returns(nothing) + return if unmatched1 == drop + unmatched2 == drop ? min(n1, n2) : n2 + else + unmatched2 == drop ? n1 : n1 + n2 + end +end + +function Base.mergewith(combine, d1::SortedVectorDict{K, V}, d2::SortedVectorDict{K, V}) where {K, V <: Integer} + # keys occurring in only one of the dicts are kept, except for `min`, where a missing sector + # has dimension zero and thus drops out of the result + unmatched = combine == min ? Returns(nothing) : identity + return _sortedmerge(combine, unmatched, unmatched, d1, d2) +end + """ Hashed(value, hashfunction = Base.hash, isequal = Base.isequal) diff --git a/src/factorizations/factorizations.jl b/src/factorizations/factorizations.jl index 6ec8c7da2..e49cc8b29 100644 --- a/src/factorizations/factorizations.jl +++ b/src/factorizations/factorizations.jl @@ -6,7 +6,8 @@ module Factorizations export copy_oftype, factorisation_scalartype, one!, truncspace using ..TensorKit -using ..TensorKit: AdjointTensorMap, SectorDict, SectorVector, +using ..TensorKit: AdjointTensorMap, DictGradedSpace, SectorDict, SectorVector, + TupleGradedSpace, blocktype, foreachblock, one!, similar_diagonal, similarstoragetype using ..TensorKit: GLOBAL_TIMER diff --git a/src/factorizations/pullbacks.jl b/src/factorizations/pullbacks.jl index b910a184c..a74acf8df 100644 --- a/src/factorizations/pullbacks.jl +++ b/src/factorizations/pullbacks.jl @@ -32,8 +32,8 @@ for pullback! in (:svd_pullback!, :eig_pullback!, :eigh_pullback!) kwargs... ) foreachblock(Δt, t) do c, (Δb, b) - haskey(inds, c) || return nothing - ind = inds[c] + ind = get(inds, c, nothing) + isnothing(ind) && return nothing Fc = block.(F, Ref(c)) ΔFc = block.(ΔF, Ref(c)) MAK.$pullback!(Δb, b, Fc, ΔFc, ind; kwargs...) diff --git a/src/factorizations/truncation.jl b/src/factorizations/truncation.jl index 2facfd2e9..bd6956440 100644 --- a/src/factorizations/truncation.jl +++ b/src/factorizations/truncation.jl @@ -35,32 +35,59 @@ _blocklength(ax::Base.OneTo, ind::AbstractVector{<:Integer}) = length(ind) _blocklength(ax::Base.OneTo, ind::AbstractVector{Bool}) = count(ind) function truncate_space(V::ElementarySpace, inds) + @assert !isdual(V) return spacetype(V)(c => _blocklength(dim(V, c), ind) for (c, ind) in pairs(inds)) end +function truncate_space(V::TupleGradedSpace{I, N}, inds) where {I <: Sector, N} + @assert !isdual(V) + vals = values(I) + # `inds` has a key for every sector with nonzero `dim(V, c)`, so zero-dim sectors are skipped rather than looked up. + newdims = ntuple(N) do n + d = V.dims[n] + return iszero(d) ? 0 : _blocklength(d, inds[vals[n]]) + end + return typeof(V)(newdims, false) +end +function truncate_space(V::DictGradedSpace{I}, inds) where {I <: Sector} + @assert !isdual(V) + # `inds` (a `SectorDict` or `SectorVector`, depending on the truncation strategy) always + # iterates in sorted order by sector, so no need to sort again here. + ks, vs = Vector{I}(undef, 0), Vector{Int}(undef, 0) + sizehint!(ks, length(inds)) + sizehint!(vs, length(inds)) + for (c, ind) in pairs(inds) + len = _blocklength(dim(V, c), ind) + if !iszero(len) + push!(ks, c) + push!(vs, len) + end + end + return typeof(V)(SectorDict{I, Int}(ks, vs), false) +end function truncate_domain!(tdst::AbstractTensorMap, tsrc::AbstractTensorMap, inds) for (c, b) in blocks(tdst) - I = get(inds, c, nothing) - @assert !isnothing(I) + ind = get(inds, c, nothing) + @assert !isnothing(ind) b′ = block(tsrc, c) - b .= view(b′, :, I) + b .= view(b′, :, ind) end return tdst end function truncate_codomain!(tdst::AbstractTensorMap, tsrc::AbstractTensorMap, inds) for (c, b) in blocks(tdst) - I = get(inds, c, nothing) - @assert !isnothing(I) + ind = get(inds, c, nothing) + @assert !isnothing(ind) b′ = block(tsrc, c) - b .= view(b′, I, :) + b .= view(b′, ind, :) end return tdst end function truncate_diagonal!(Ddst::DiagonalTensorMap, Dsrc::DiagonalTensorMap, inds) for (c, b) in blocks(Ddst) - I = get(inds, c, nothing) - @assert !isnothing(I) - diagview(b) .= view(diagview(block(Dsrc, c)), I) + ind = get(inds, c, nothing) + @assert !isnothing(ind) + diagview(b) .= view(diagview(block(Dsrc, c)), ind) end return Ddst end @@ -69,17 +96,17 @@ function MAK.truncate( ::typeof(svd_trunc!), (U, S, Vᴴ)::NTuple{3, AbstractTensorMap}, strategy::TruncationStrategy ) - ind = MAK.findtruncated_svd(diagview(S), strategy) - V_truncated = truncate_space(space(S, 1), ind) + inds = MAK.findtruncated_svd(diagview(S), strategy) + V_truncated = truncate_space(space(S, 1), inds) Ũ = similar(U, codomain(U) ← V_truncated) - truncate_domain!(Ũ, U, ind) + truncate_domain!(Ũ, U, inds) S̃ = similar_diagonal(S, V_truncated) - truncate_diagonal!(S̃, S, ind) + truncate_diagonal!(S̃, S, inds) Ṽᴴ = similar(Vᴴ, V_truncated ← domain(Vᴴ)) - truncate_codomain!(Ṽᴴ, Vᴴ, ind) + truncate_codomain!(Ṽᴴ, Vᴴ, inds) - return (Ũ, S̃, Ṽᴴ), ind + return (Ũ, S̃, Ṽᴴ), inds end function MAK.truncate( @@ -89,11 +116,11 @@ function MAK.truncate( for (c, b) in blocks(S) copyto!(extended_S[c], diagview(b)) # copyto! since `b` might be shorter end - ind = MAK.findtruncated(extended_S, strategy) - V_truncated = truncate_space(space(S, 1), ind) + inds = MAK.findtruncated(extended_S, strategy) + V_truncated = truncate_space(space(S, 1), inds) Ũ = similar(U, codomain(U) ← V_truncated) - truncate_domain!(Ũ, U, ind) - return Ũ, ind + truncate_domain!(Ũ, U, inds) + return Ũ, inds end function MAK.truncate( ::typeof(right_null!), (S, Vᴴ)::NTuple{2, AbstractTensorMap}, strategy::TruncationStrategy @@ -102,11 +129,11 @@ function MAK.truncate( for (c, b) in blocks(S) copyto!(extended_S[c], diagview(b)) # copyto! since `b` might be shorter end - ind = MAK.findtruncated(extended_S, strategy) - V_truncated = truncate_space(dual(space(S, 2)), ind) + inds = MAK.findtruncated(extended_S, strategy) + V_truncated = truncate_space(dual(space(S, 2)), inds) Ṽᴴ = similar(Vᴴ, V_truncated ← domain(Vᴴ)) - truncate_codomain!(Ṽᴴ, Vᴴ, ind) - return Ṽᴴ, ind + truncate_codomain!(Ṽᴴ, Vᴴ, inds) + return Ṽᴴ, inds end # special case `NoTruncation` for null: should keep exact zeros due to rectangularity @@ -114,20 +141,20 @@ end function MAK.truncate( ::typeof(left_null!), (U, S)::NTuple{2, AbstractTensorMap}, strategy::NoTruncation ) - ind = SectorDict(c => (size(b, 2) + 1):size(b, 1) for (c, b) in blocks(S)) - V_truncated = truncate_space(space(S, 1), ind) + inds = SectorDict(c => (size(b, 2) + 1):size(b, 1) for (c, b) in blocks(S)) + V_truncated = truncate_space(space(S, 1), inds) Ũ = similar(U, codomain(U) ← V_truncated) - truncate_domain!(Ũ, U, ind) - return Ũ, ind + truncate_domain!(Ũ, U, inds) + return Ũ, inds end function MAK.truncate( ::typeof(right_null!), (S, Vᴴ)::NTuple{2, AbstractTensorMap}, strategy::NoTruncation ) - ind = SectorDict(c => (size(b, 1) + 1):size(b, 2) for (c, b) in blocks(S)) - V_truncated = truncate_space(dual(space(S, 2)), ind) + inds = SectorDict(c => (size(b, 1) + 1):size(b, 2) for (c, b) in blocks(S)) + V_truncated = truncate_space(dual(space(S, 2)), inds) Ṽᴴ = similar(Vᴴ, V_truncated ← domain(Vᴴ)) - truncate_codomain!(Ṽᴴ, Vᴴ, ind) - return Ṽᴴ, ind + truncate_codomain!(Ṽᴴ, Vᴴ, inds) + return Ṽᴴ, inds end for f! in (:eig_trunc!, :eigh_trunc!) @@ -136,16 +163,16 @@ for f! in (:eig_trunc!, :eigh_trunc!) (D, V)::Tuple{DiagonalTensorMap, AbstractTensorMap}, strategy::TruncationStrategy ) - ind = MAK.findtruncated(diagview(D), strategy) - V_truncated = truncate_space(space(D, 1), ind) + inds = MAK.findtruncated(diagview(D), strategy) + V_truncated = truncate_space(space(D, 1), inds) D̃ = similar_diagonal(D, V_truncated) - truncate_diagonal!(D̃, D, ind) + truncate_diagonal!(D̃, D, inds) Ṽ = similar(V, codomain(V) ← V_truncated) - truncate_domain!(Ṽ, V, ind) + truncate_domain!(Ṽ, V, inds) - return (D̃, Ṽ), ind + return (D̃, Ṽ), inds end end diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index 3b9d36dbb..67169a764 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -1,3 +1,6 @@ +# budget on how many sectors are worth specializing the compiler on +const _NTUPLE_STORAGE_THRESHOLD = 8 + """ struct GradedSpace{I<:Sector, D} <: ElementarySpace GradedSpace{I,D}(dims; dual::Bool = false) where {I<:Sector, D} @@ -12,16 +15,16 @@ and pivotal (pre-, multi-) fusion category. Here `dims` represents the degeneracy or multiplicity of every sector. The data structure `D` of `dims` will depend on the result `Base.IteratorSize(values(I))`. -If the result is of type `HasLength` or `HasShape`, `dims` will be stored in a -`NTuple{N,Int}` with `N = length(values(I))`. This requires that a sector `s::I` can be -transformed into an index via `s == getindex(values(I), i)` and -`i == findindex(values(I), s)`. If `Base.IteratorElsize(values(I))` results `IsInfinite()` -or `SizeUnknown()`, a `SectorDict{I,Int}` is used to store the non-zero degeneracy -dimensions with the corresponding sector as key. The parameter `D` is hidden from the user -and should typically be of no concern. - -The concrete type `GradedSpace{I,D}` with correct `D` can be obtained as `Vect[I]`, or if -`I == Irrep[G]` for some `G<:Group`, as `Rep[G]`. +If the result is of type `HasLength` or `HasShape` and `N = length(values(I))` does not exceed +`$_NTUPLE_STORAGE_THRESHOLD`, `dims` will be stored in a `NTuple{N,Int}`. This requires that a +sector `s::I` can be transformed into an index via `s == getindex(values(I), i)` and +`i == findindex(values(I), s)`. Otherwise, i.e. for more sectors than that or if +`Base.IteratorSize(values(I))` results `IsInfinite()` or `SizeUnknown()`, a `SectorDict{I,Int}` +is used to store the non-zero degeneracy dimensions with the corresponding sector as key. +The parameter `D` is hidden from the user and should typically be of no concern. + +The concrete type `GradedSpace{I,D}` with correct `D` can be obtained as `Vect[I]`, +or as `Rep[G]` if `I == Irrep[G]` for some `G<:Group`. """ struct GradedSpace{I <: Sector, D} <: ElementarySpace dims::D @@ -29,6 +32,10 @@ struct GradedSpace{I <: Sector, D} <: ElementarySpace end sectortype(::Type{<:GradedSpace{I}}) where {I <: Sector} = I +# the two storage variants that `sectorstoragetype` selects between, used for dispatch +const TupleGradedSpace{I, N} = GradedSpace{I, NTuple{N, Int}} +const DictGradedSpace{I} = GradedSpace{I, SectorDict{I, Int}} + # elementary spaces are homogeneously colored: all sectors share a left and a right unit. function _check_unit_homogeneity(::Type{I}, sectors) where {I <: Sector} (UnitStyle(I) isa SimpleUnit || isempty(sectors)) && return nothing @@ -39,18 +46,20 @@ function _check_unit_homogeneity(::Type{I}, sectors) where {I <: Sector} end function GradedSpace{I, NTuple{N, Int}}(dims; dual::Bool = false) where {I, N} - d = ntuple(n -> 0, N) - isset = ntuple(n -> false, N) + @assert N <= 64 "the `UInt64` bitmask tracking which sectors have been set holds 64 bits" + d = TupleTools.MutableNTuple(ntuple(Returns(0), N)) + mask = zero(UInt64) for (c, dc) in dims k = convert(I, c) i = findindex(values(I), k) - k = dc < 0 && throw(ArgumentError(lazy"Sector $k has negative dimension $dc")) - isset[i] && throw(ArgumentError(lazy"Sector $c appears multiple times")) - isset = TupleTools.setindex(isset, true, i) - d = TupleTools.setindex(d, dc, i) + dc < 0 && throw(ArgumentError(lazy"Sector $k has negative dimension $dc")) + bit = one(UInt64) << (i - 1) + iszero(mask & bit) || throw(ArgumentError(lazy"Sector $c appears multiple times")) + mask |= bit + d[i] = dc end _check_unit_homogeneity(I, (values(I)[n] for n in 1:N if !iszero(d[n]))) - return GradedSpace{I, NTuple{N, Int}}(d, dual) + return GradedSpace{I, NTuple{N, Int}}(Tuple(d), dual) end function GradedSpace{I, NTuple{N, Int}}(dims::Pair; dual::Bool = false) where {I, N} return GradedSpace{I, NTuple{N, Int}}((dims,); dual = dual) @@ -100,22 +109,22 @@ GradedSpace(g::AbstractDict; dual::Bool = false) = GradedSpace(g...; dual = dual field(::Type{<:GradedSpace}) = ℂ InnerProductStyle(::Type{<:GradedSpace}) = EuclideanInnerProduct() -function dim(V::GradedSpace) - init = 0 * dim(first(allunits(sectortype(V)))) - return sum(c -> dim(c) * dim(V, c), sectors(V); init = init) +function dim(V::GradedSpace{I}) where {I <: Sector} + init = zero(dimscalartype(I)) + return sum(((c, d),) -> dim(c) * d, blockdims(V); init) end -function dim(V::GradedSpace{I, <:AbstractDict}, c::I) where {I <: Sector} +function dim(V::DictGradedSpace{I}, c::I) where {I <: Sector} return get(V.dims, isdual(V) ? dual(c) : c, 0) end -function dim(V::GradedSpace{I, <:Tuple}, c::I) where {I <: Sector} +function dim(V::TupleGradedSpace{I}, c::I) where {I <: Sector} return V.dims[findindex(values(I), isdual(V) ? dual(c) : c)] end Base.axes(V::GradedSpace) = Base.OneTo(dim(V)) function Base.axes(V::GradedSpace{I}, c::I) where {I <: Sector} offset = 0 - for c′ in sectors(V) + for (c′, d′) in blockdims(V) c′ == c && break - offset += dim(c′) * dim(V, c′) + offset += dim(c′) * d′ end return (offset + 1):(offset + dim(c) * dim(V, c)) end @@ -126,11 +135,22 @@ isdual(V::GradedSpace) = V.dual isconj(V::GradedSpace) = isdual(V) function flip(V::GradedSpace{I}) where {I <: Sector} return if isdual(V) - typeof(V)(c => dim(V, c) for c in sectors(V)) + typeof(V)(blockdims(V)) else - typeof(V)(dual(c) => dim(V, c) for c in sectors(V))' + typeof(V)(dual(c) => d for (c, d) in blockdims(V))' end end +# the permutation of `values(I)` induced by `dual`; only depends on the type, so it folds away +Base.@assume_effects :foldable function _dualpermutation(::Type{I}, ::Val{N}) where {I <: Sector, N} + vals = values(I) + return ntuple(n -> findindex(vals, dual(vals[n])), Val(N)) +end +function flip(V::TupleGradedSpace{I, N}) where {I <: Sector, N} + # `flip` maps `c => d` to `dual(c) => d` and negates `isdual`, which for tuple storage is + # just a fixed permutation of the dims, so the generic constructor can be skipped + newdims = TupleTools.getindices(V.dims, _dualpermutation(I, Val(N))) + return GradedSpace{I, NTuple{N, Int}}(newdims, !isdual(V)) +end function unitspace(S::Type{<:GradedSpace{I}}) where {I <: Sector} UnitStyle(I) isa GenericUnit && @@ -139,72 +159,137 @@ function unitspace(S::Type{<:GradedSpace{I}}) where {I <: Sector} end zerospace(S::Type{<:GradedSpace}) = S() -# TODO: the following methods can probably be implemented more efficiently for -# `FiniteGradedSpace`, but we don't expect them to be used often in hot loops, so -# these generic definitions (which are still quite efficient) are good for now. -function ⊕(V₁::GradedSpace{I}, V₂::GradedSpace{I}) where {I <: Sector} +function ⊕(V₁::DictGradedSpace{I}, V₂::DictGradedSpace{I}) where {I <: Sector} + dual1 = isdual(V₁) + dual1 == isdual(V₂) || throw(SpaceMismatch("Direct sum of a vector space and a dual space does not exist")) + return typeof(V₁)(mergewith(+, V₁.dims, V₂.dims), dual1) +end +function ⊕(V₁::TupleGradedSpace{I}, V₂::TupleGradedSpace{I}) where {I <: Sector} dual1 = isdual(V₁) dual1 == isdual(V₂) || throw(SpaceMismatch("Direct sum of a vector space and a dual space does not exist")) - dims = SectorDict{I, Int}() - for c in union(sectors(V₁), sectors(V₂)) - cout = ifelse(dual1, dual(c), c) - dims[cout] = dim(V₁, c) + dim(V₂, c) - end - return typeof(V₁)(dims; dual = dual1) -end -function ⊖(V::GradedSpace{I}, W::GradedSpace{I}) where {I <: Sector} - 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); dual) -end - -function fuse(V₁::GradedSpace{I}, V₂::GradedSpace{I}) where {I <: Sector} - dims = SectorDict{I, Int}() - for a in sectors(V₁), b in sectors(V₂) + newdims = map(+, V₁.dims, V₂.dims) + return typeof(V₁)(newdims, dual1) +end +@noinline _throw_not_subspace(V, W) = throw(SpaceMismatch(lazy"$(W) is not a subspace of $(V)")) + +# `⊖` as a callable, carrying the spaces to report which subspace condition was violated: +# as a combiner it subtracts and validates in one pass, and as an unmatched handler it rejects +# a sector of `W` that does not occur in `V` +struct SubtractDims{S} + V::S + W::S +end +(f::SubtractDims)(dV, dW) = dV < dW ? _throw_not_subspace(f.V, f.W) : dV - dW +(f::SubtractDims)(d) = _throw_not_subspace(f.V, f.W) + +function ⊖(V::TupleGradedSpace{I}, W::TupleGradedSpace{I}) where {I <: Sector} + dualV = isdual(V) + dualV == isdual(W) || _throw_not_subspace(V, W) + newdims = map(SubtractDims(V, W), V.dims, W.dims) # single unrolled pass + return typeof(V)(newdims, dualV) +end +function ⊖(V::DictGradedSpace{I}, W::DictGradedSpace{I}) where {I <: Sector} + dualV = isdual(V) + dualV == isdual(W) || _throw_not_subspace(V, W) + subtract = SubtractDims(V, W) + return typeof(V)(_sortedmerge(subtract, identity, subtract, V.dims, W.dims), dualV) +end + +function fuse(V₁::DictGradedSpace{I}, V₂::DictGradedSpace{I}) where {I <: Sector} + acc = Dict{I, Int}() # Accumulation into Dict is more efficient than repeated insertion in sorted vector + for (a, da) in blockdims(V₁), (b, db) in blockdims(V₂) + dab = da * db for c in a ⊗ b - dims[c] = get(dims, c, 0) + Nsymbol(a, b, c) * dim(V₁, a) * dim(V₂, b) + acc[c] = get(acc, c, 0) + Nsymbol(a, b, c) * dab end end - return typeof(V₁)(dims) + ks0 = collect(keys(acc)) + vs0 = collect(values(acc)) + perm = sortperm(ks0) + return typeof(V₁)(SectorDict{I, Int}(ks0[perm], vs0[perm]), false) +end +function fuse(V₁::TupleGradedSpace{I, N}, V₂::TupleGradedSpace{I, N}) where {I <: Sector, N} + vals = values(I) + dual1, dual2 = isdual(V₁), isdual(V₂) + newdims = zeros(Int, N) + @inbounds for na in 1:N + da = V₁.dims[na] + iszero(da) && continue + a₀ = vals[na] + a = dual1 ? dual(a₀) : a₀ + for nb in 1:N + db = V₂.dims[nb] + iszero(db) && continue + b₀ = vals[nb] + b = dual2 ? dual(b₀) : b₀ + dab = da * db + for c in a ⊗ b + nc = findindex(vals, c) + newdims[nc] += Nsymbol(a, b, c) * dab + end + end + end + return typeof(V₁)(ntuple(i -> @inbounds(newdims[i]), Val(N)), false) end -function infimum(V₁::GradedSpace{I}, V₂::GradedSpace{I}) where {I <: Sector} +function infimum(V₁::TupleGradedSpace{I}, V₂::TupleGradedSpace{I}) where {I <: Sector} Visdual = isdual(V₁) - Visdual == isdual(V₂) || - throw(SpaceMismatch("Infimum of space and dual space does not exist")) - return typeof(V₁)( - (Visdual ? dual(c) : c) => min(dim(V₁, c), dim(V₂, c)) - for c in intersect(sectors(V₁), sectors(V₂)); dual = Visdual - ) + Visdual == isdual(V₂) || throw(SpaceMismatch("Infimum of space and dual space does not exist")) + newdims = map(min, V₁.dims, V₂.dims) + return typeof(V₁)(newdims, Visdual) end -function supremum(V₁::GradedSpace{I}, V₂::GradedSpace{I}) where {I <: Sector} +function infimum(V₁::DictGradedSpace{I}, V₂::DictGradedSpace{I}) where {I <: Sector} Visdual = isdual(V₁) - Visdual == isdual(V₂) || - throw(SpaceMismatch("Supremum of space and dual space does not exist")) - return typeof(V₁)( - (Visdual ? dual(c) : c) => max(dim(V₁, c), dim(V₂, c)) - for c in union(sectors(V₁), sectors(V₂)); dual = Visdual - ) + Visdual == isdual(V₂) || throw(SpaceMismatch("Infimum of space and dual space does not exist")) + return typeof(V₁)(mergewith(min, V₁.dims, V₂.dims), Visdual) +end +function supremum(V₁::TupleGradedSpace{I}, V₂::TupleGradedSpace{I}) where {I <: Sector} + Visdual = isdual(V₁) + Visdual == isdual(V₂) || throw(SpaceMismatch("Supremum of space and dual space does not exist")) + newdims = map(max, V₁.dims, V₂.dims) + return typeof(V₁)(newdims, Visdual) +end +function supremum(V₁::DictGradedSpace{I}, V₂::DictGradedSpace{I}) where {I <: Sector} + Visdual = isdual(V₁) + Visdual == isdual(V₂) || throw(SpaceMismatch("Supremum of space and dual space does not exist")) + return typeof(V₁)(mergewith(max, V₁.dims, V₂.dims), Visdual) end hassector(V::GradedSpace{I}, s::I) where {I <: Sector} = dim(V, s) != 0 -function sectors(V::GradedSpace{I, <:AbstractDict}) where {I <: Sector} +function sectors(V::DictGradedSpace{I}) where {I <: Sector} return SectorSet{I}(s -> isdual(V) ? dual(s) : s, keys(V.dims)) end -function sectors(V::GradedSpace{I, NTuple{N, Int}}) where {I <: Sector, N} +function sectors(V::TupleGradedSpace{I, N}) where {I <: Sector, N} return SectorSet{I}(Iterators.filter(n -> V.dims[n] != 0, 1:N)) do n return isdual(V) ? dual(values(I)[n]) : values(I)[n] end end +""" + blockdims(V::GradedSpace) + +Return an iterator over the non-zero blocks of the graded space `V`. +These blocks contain the `Sector`s and their corresponding degeneracy, +i.e. the number of times the sector appears in the direct sum decomposition of `V`. +""" +function blockdims(V::DictGradedSpace{I}) where {I <: Sector} + return ((isdual(V) ? dual(c) : c) => d for (c, d) in V.dims) +end +function blockdims(V::TupleGradedSpace{I, N}) where {I <: Sector, N} + vals = values(I) + return ( + (isdual(V) ? dual(vals[n]) : vals[n]) => V.dims[n] + for n in 1:N if !iszero(V.dims[n]) + ) +end + Base.hash(V::GradedSpace, h::UInt) = hash(V.dual, hash(V.dims, h)) function Base.:(==)(V₁::GradedSpace, V₂::GradedSpace) return sectortype(V₁) == sectortype(V₂) && (V₁.dims == V₂.dims) && V₁.dual == V₂.dual end -function sectorhash(V::GradedSpace{I, NTuple{N, Int}}, h::UInt) where {I, N} +function sectorhash(V::TupleGradedSpace{I, N}, h::UInt) where {I, N} return hash(iszero.(V.dims), hash(isdual(V), h)) end function sectorequal(V₁::GradedSpace{I, D}, V₂::GradedSpace{I, D}) where {I, N, D <: NTuple{N, Int}} @@ -212,7 +297,7 @@ function sectorequal(V₁::GradedSpace{I, D}, V₂::GradedSpace{I, D}) where {I, return iszero(d₁) == iszero(d₂) end end -function sectorhash(V::GradedSpace{I, <:SectorDict}, h::UInt) where {I} +function sectorhash(V::DictGradedSpace{I}, h::UInt) where {I} return hash(keys(V.dims), hash(isdual(V), h)) end function sectorequal(V₁::GradedSpace{I, D}, V₂::GradedSpace{I, D}) where {I, D <: SectorDict} @@ -231,7 +316,7 @@ function Base.show(io::IO, V::GradedSpace) cls = ")" end - v = [c => dim(V, c) for c in sectors(V)] + v = collect(blockdims(V)) # logic stolen from Base.show_vector limited = get(io, :limit, false)::Bool @@ -262,7 +347,7 @@ function Base.show(io::IO, ::MIME"text/plain", V::GradedSpace) # print detailed sector information - hijack Base.Vector printing print(io, ":\n") isdual(V) && (V = dual(V)) - print_data = [c => dim(V, c) for c in sectors(V)] + print_data = collect(blockdims(V)) ioc = IOContext(io, :typeinfo => eltype(print_data)) Base.print_matrix(ioc, print_data) @@ -280,13 +365,21 @@ specify `D`. const Vect = SpaceTable() Base.getindex(::SpaceTable) = ComplexSpace Base.getindex(::SpaceTable, ::Type{Trivial}) = ComplexSpace -function Base.getindex(::SpaceTable, I::Type{<:Sector}) +Base.getindex(::SpaceTable, I::Type{<:Sector}) = GradedSpace{I, sectorstoragetype(I)} + +""" + sectorstoragetype(I::Type{<:Sector}) -> Type + +The storage type `D` used for the `dims` field of `GradedSpace{I, D}`. +This is `NTuple{N,Int}` with `N = length(values(I))` if `I` has a finite, known length +of at most `$_NTUPLE_STORAGE_THRESHOLD`, or `SectorDict{I,Int}` otherwise. +""" +Base.@assume_effects :foldable function sectorstoragetype(::Type{I}) where {I <: Sector} if Base.IteratorSize(values(I)) isa Union{HasLength, HasShape} N = length(values(I)) - return GradedSpace{I, NTuple{N, Int}} - else - return GradedSpace{I, SectorDict{I, Int}} + N <= _NTUPLE_STORAGE_THRESHOLD && return NTuple{N, Int} end + return SectorDict{I, Int} end Base.getindex(::ComplexNumbers, I::Type{<:Sector}) = Vect[I] @@ -322,17 +415,33 @@ function type_repr(::Type{<:GradedSpace{ProductSector{T}}}) where end # Specific constructors for Z_N +""" + const ZNSpace{N} + +Type alias for the tuple-backed `GradedSpace{ZNIrrep{N}, NTuple{N,Int}}`. + +!!! warning "Deprecated" + A type alias cannot compute the storage type from `N`, so this only coincides with + `Vect[ZNIrrep{N}]` while `N <= $_NTUPLE_STORAGE_THRESHOLD`. Use `Vect[ZNIrrep{N}]` instead. +""" const ZNSpace{N} = GradedSpace{ZNIrrep{N}, NTuple{N, Int}} -ZNSpace{N}(dims::NTuple{N, Int}; dual::Bool = false) where {N} = ZNSpace{N}(dims, dual) -ZNSpace{N}(dims::Vararg{Int, N}; dual::Bool = false) where {N} = ZNSpace{N}(dims, dual) -ZNSpace(dims::NTuple{N, Int}; dual::Bool = false) where {N} = ZNSpace{N}(dims, dual) -ZNSpace(dims::Vararg{Int, N}; dual::Bool = false) where {N} = ZNSpace{N}(dims, dual) +@noinline function _throw_znspace_storage(N) + msg = lazy"`ZNSpace{$N}` is not the canonical space type for `ZNIrrep{$N}`, which stores more than $(_NTUPLE_STORAGE_THRESHOLD) sectors in a `SectorDict`; use `Vect[ZNIrrep{$N}]` instead" + return throw(ArgumentError(msg)) +end +function ZNSpace{N}(dims::NTuple{N, Int}; dual::Bool = false) where {N} + N <= _NTUPLE_STORAGE_THRESHOLD || _throw_znspace_storage(N) + return ZNSpace{N}(dims, dual) +end +ZNSpace{N}(dims::Vararg{Int, N}; dual::Bool = false) where {N} = ZNSpace{N}(dims; dual) +ZNSpace(dims::NTuple{N, Int}; dual::Bool = false) where {N} = ZNSpace{N}(dims; dual) +ZNSpace(dims::Vararg{Int, N}; dual::Bool = false) where {N} = ZNSpace{N}(dims; dual) # TODO: Do we still need all of those # ASCII type aliases -const Z2Space = ZNSpace{2} -const Z3Space = ZNSpace{3} -const Z4Space = ZNSpace{4} +const Z2Space = Vect[ZNIrrep{2}] +const Z3Space = Vect[ZNIrrep{3}] +const Z4Space = Vect[ZNIrrep{4}] const U1Space = Rep[U₁] const CU1Space = Rep[CU₁] const SU2Space = Rep[SU₂] diff --git a/src/tensors/sectorvector.jl b/src/tensors/sectorvector.jl index f586d4aef..5c11c66e3 100644 --- a/src/tensors/sectorvector.jl +++ b/src/tensors/sectorvector.jl @@ -53,7 +53,9 @@ Base.valtype(::Type{SectorVector{T, I, A}}) where {T, I, A} = SubArray{T, 1, A, Base.keys(v::SectorVector) = keys(v.structure) Base.values(v::SectorVector) = (v[c] for c in keys(v)) -Base.pairs(v::SectorVector) = SectorDict(c => v[c] for c in keys(v)) +# lazy, like `blocks(::AbstractTensorMap)`: the blocks are views, and lookups go through +# `getindex`/`get`/`block` on the `SectorVector` itself rather than through this iterator +Base.pairs(v::SectorVector) = Base.Iterators.map(((c, r),) -> c => view(parent(v), r), v.structure) Base.get(v::SectorVector{<:Any, I}, key::I, default) where {I} = haskey(v, key) ? v[key] : default Base.haskey(v::SectorVector{<:Any, I}, key::I) where {I} = key in keys(v) diff --git a/test/tensors/sectorvector.jl b/test/tensors/sectorvector.jl index 1c8b268e6..22832404c 100644 --- a/test/tensors/sectorvector.jl +++ b/test/tensors/sectorvector.jl @@ -83,7 +83,7 @@ spacelist = ( end @test collect(@testinferred(keys(v))) == collect(sectors(V)) @test collect(values(v)) == [v[c] for c in keys(v)] - @test pairs(v) == Dict(c => v[c] for c in keys(v)) + @test Dict(pairs(v)) == Dict(c => v[c] for c in keys(v)) end @timedtestset "VectorInterface ($T)" begin