diff --git a/Project.toml b/Project.toml index 07f05b69a..607558519 100644 --- a/Project.toml +++ b/Project.toml @@ -1,9 +1,10 @@ name = "TensorKit" uuid = "07d1fe3e-3e46-537d-9eac-e9e13d0d4cec" -authors = ["Jutho Haegeman, Lukas Devos"] version = "0.16.3" +authors = ["Jutho Haegeman, Lukas Devos"] [deps] +Dictionaries = "85a47980-9c8c-11e8-2b9f-f7ca1fa99fb4" LRUCache = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MatrixAlgebraKit = "6c742aac-3347-4629-af66-fc926824e5e4" @@ -41,6 +42,7 @@ CUDA = "5.9" ChainRulesCore = "1" ChainRulesTestUtils = "1" Combinatorics = "1" +Dictionaries = "0.4" FiniteDifferences = "0.12" GPUArrays = "11.3.1" JET = "0.9, 0.10, 0.11" diff --git a/docs/src/lib/tensors.md b/docs/src/lib/tensors.md index ea491a843..7b12cdf18 100644 --- a/docs/src/lib/tensors.md +++ b/docs/src/lib/tensors.md @@ -97,7 +97,6 @@ In `TensorMap` instances, all data is gathered in a single `AbstractVector`, whi To obtain information about the structure of the data, you can use: ```@docs -fusionblockstructure(::AbstractTensorMap) dim(::AbstractTensorMap) blocksectors(::AbstractTensorMap) hasblock(::AbstractTensorMap, ::Sector) diff --git a/ext/TensorKitMooncakeExt/utility.jl b/ext/TensorKitMooncakeExt/utility.jl index 779a4e017..ceb32d867 100644 --- a/ext/TensorKitMooncakeExt/utility.jl +++ b/ext/TensorKitMooncakeExt/utility.jl @@ -62,7 +62,8 @@ end Mooncake.tangent_type(::Type{<:VectorSpace}) = Mooncake.NoTangent Mooncake.tangent_type(::Type{<:HomSpace}) = Mooncake.NoTangent -@zero_derivative DefaultCtx Tuple{typeof(TensorKit.fusionblockstructure), Any} +@zero_derivative DefaultCtx Tuple{typeof(TensorKit.sectorstructure), Any} +@zero_derivative DefaultCtx Tuple{typeof(TensorKit.degeneracystructure), Any} @zero_derivative DefaultCtx Tuple{typeof(TensorKit.select), HomSpace, Index2Tuple} @zero_derivative DefaultCtx Tuple{typeof(TensorKit.flip), HomSpace, Any} diff --git a/src/TensorKit.jl b/src/TensorKit.jl index 3523d751f..032db36c0 100644 --- a/src/TensorKit.jl +++ b/src/TensorKit.jl @@ -118,6 +118,7 @@ const TO = TensorOperations using MatrixAlgebraKit +using Dictionaries: Dictionaries, Dictionary, Indices, gettoken, gettokenvalue using LRUCache using OhMyThreads using ScopedValues @@ -200,6 +201,23 @@ include("fusiontrees/fusiontrees.jl") #------------------------------------------- include("spaces/vectorspaces.jl") +# ElementarySpace types +include("spaces/cartesianspace.jl") +include("spaces/complexspace.jl") +include("spaces/generalspace.jl") +include("spaces/gradedspace.jl") +include("spaces/planarspace.jl") + +# CompositeSpace types +include("spaces/productspace.jl") +include("spaces/deligne.jl") + +# HomSpace +include("spaces/homspace.jl") + +# Derived information +include("spaces/structure.jl") + # Multithreading settings #------------------------- const TRANSFORMER_THREADS = Ref(1) diff --git a/src/auxiliary/dicts.jl b/src/auxiliary/dicts.jl index f43838010..8c6b47217 100644 --- a/src/auxiliary/dicts.jl +++ b/src/auxiliary/dicts.jl @@ -263,3 +263,25 @@ function Base.:(==)(d1::SortedVectorDict, d2::SortedVectorDict) end return true end + +""" + Hashed(value, hashfunction = Base.hash, isequal = Base.isequal) + +Wrapper struct to alter the `hash` and `isequal` implementations of a given value. +This is useful in the contexts of dictionaries, where you either want to customize the hashfunction, +or consider various values as equal with a different notion of equality. +""" +struct Hashed{T, H <: Function, E <: Function} + val::T + hashf::H + eqf::E +end + +Hashed(val, hashf = Base.hash, eqf = Base.isequal) = + Hashed{typeof(val), typeof(hashf), typeof(eqf)}(val, hashf, eqf) + +Base.parent(h::Hashed) = h.val +Base.hash(h::Hashed, seed::UInt) = h.hashf(parent(h), seed) +# Note: requires the equality functions to be equal to avoid asymmetric results +Base.isequal(h1::Hashed{<:Any, <:Any, E}, h2::Hashed{<:Any, <:Any, E}) where {E} = + h1.eqf(parent(h1), parent(h2)) diff --git a/src/fusiontrees/braiding_manipulations.jl b/src/fusiontrees/braiding_manipulations.jl index 8c589a3e0..b4a1f6661 100644 --- a/src/fusiontrees/braiding_manipulations.jl +++ b/src/fusiontrees/braiding_manipulations.jl @@ -299,44 +299,44 @@ Base.@assume_effects :foldable function _fsdicttype(::Type{T}) where {I, N₁, N return Pair{FusionTreeBlock{I, N₁, N₂, Tuple{F₁, F₂}}, Matrix{E}} end -@cached function fsbraid(key::K)::_fsdicttype(K) where {I, N₁, N₂, K <: FSPBraidKey{I, N₁, N₂}} - ((f₁, f₂), (p1, p2), (l1, l2)) = key - p = linearizepermutation(p1, p2, length(f₁), length(f₂)) - levels = (l1..., reverse(l2)...) - (f, f0), coeff1 = repartition((f₁, f₂), N₁ + N₂) - f′, coeff2 = braid(f, p, levels) - (f₁′, f₂′), coeff3 = repartition((f′, f0), N₁) - return (f₁′, f₂′) => coeff1 * coeff2 * coeff3 -end -@cached function fsbraid(key::K)::_fsdicttype(K) where {I, N₁, N₂, K <: FSBBraidKey{I, N₁, N₂}} - src, (p1, p2), (l1, l2) = key +@cached function fsbraid(key::K)::_fsdicttype(K) where {I, N₁, N₂, K <: Union{FSPBraidKey{I, N₁, N₂}, FSBBraidKey{I, N₁, N₂}}} + if K <: FSPBraidKey + ((f₁, f₂), (p1, p2), (l1, l2)) = key + p = linearizepermutation(p1, p2, length(f₁), length(f₂)) + levels = (l1..., reverse(l2)...) + (f, f0), coeff1 = repartition((f₁, f₂), N₁ + N₂) + f′, coeff2 = braid(f, p, levels) + (f₁′, f₂′), coeff3 = repartition((f′, f0), N₁) + return (f₁′, f₂′) => coeff1 * coeff2 * coeff3 - p = linearizepermutation(p1, p2, numout(src), numin(src)) - levels = (l1..., reverse(l2)...) + else + src, (p1, p2), (l1, l2) = key - dst, U = repartition(src, numind(src)) + p = linearizepermutation(p1, p2, numout(src), numin(src)) + levels = (l1..., reverse(l2)...) - for s in permutation2swaps(p) - inv = levels[s] > levels[s + 1] - dst, U_tmp = artin_braid(dst, s; inv) - U = U_tmp * U - l = levels[s] - levels = TupleTools.setindex(levels, levels[s + 1], s) - levels = TupleTools.setindex(levels, l, s + 1) - end + dst, U = repartition(src, numind(src)) - if N₂ == 0 - return dst => U - else - dst, U_tmp = repartition(dst, N₁) - U = U_tmp * U - return dst => U + for s in permutation2swaps(p) + inv = levels[s] > levels[s + 1] + dst, U_tmp = artin_braid(dst, s; inv) + U = U_tmp * U + l = levels[s] + levels = TupleTools.setindex(levels, levels[s + 1], s) + levels = TupleTools.setindex(levels, l, s + 1) + end + + if N₂ == 0 + return dst => U + else + dst, U_tmp = repartition(dst, N₁) + U = U_tmp * U + return dst => U + end end end -CacheStyle(::typeof(fsbraid), k::FSPBraidKey{I}) where {I} = - FusionStyle(I) isa UniqueFusion ? NoCache() : GlobalLRUCache() -CacheStyle(::typeof(fsbraid), k::FSBBraidKey{I}) where {I} = +CacheStyle(::typeof(fsbraid), k::Union{FSPBraidKey{I}, FSBBraidKey{I}}) where {I} = FusionStyle(I) isa UniqueFusion ? NoCache() : GlobalLRUCache() """ diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index d3be67a5f..8bde2fa04 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -191,6 +191,21 @@ 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} + 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}} + return isdual(V₁) == isdual(V₂) && all(zip(V₁.dims, V₂.dims)) do (d₁, d₂) + return iszero(d₁) == iszero(d₂) + end +end +function sectorhash(V::GradedSpace{I, <:SectorDict}, 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} + return isdual(V₁) == isdual(V₂) && keys(V₁.dims) == keys(V₂.dims) +end + Base.summary(io::IO, V::GradedSpace) = print(io, type_repr(typeof(V))) function Base.show(io::IO, V::GradedSpace) diff --git a/src/spaces/homspace.jl b/src/spaces/homspace.jl index f5c250658..9ef8a5d51 100644 --- a/src/spaces/homspace.jl +++ b/src/spaces/homspace.jl @@ -37,12 +37,20 @@ function Base.:(==)(W₁::HomSpace, W₂::HomSpace) return (W₁.codomain == W₂.codomain) && (W₁.domain == W₂.domain) end +function sectorequal(W₁::HomSpace, W₂::HomSpace) + return sectorequal(codomain(W₁), codomain(W₂)) && sectorequal(domain(W₁), domain(W₂)) +end +function sectorhash(W::HomSpace, h::UInt) + h = sectorhash(codomain(W), h) + h = sectorhash(domain(W), h) + return h +end + spacetype(::Type{<:HomSpace{S}}) where {S} = S const TensorSpace{S <: ElementarySpace} = Union{S, ProductSpace{S}} const TensorMapSpace{S <: ElementarySpace, N₁, N₂} = HomSpace{ - S, ProductSpace{S, N₁}, - ProductSpace{S, N₂}, + S, ProductSpace{S, N₁}, ProductSpace{S, N₂}, } numout(::Type{TensorMapSpace{S, N₁, N₂}}) where {S, N₁, N₂} = N₁ @@ -62,29 +70,25 @@ end →(dom::VectorSpace, codom::VectorSpace) = ←(codom, dom) function Base.show(io::IO, W::HomSpace) - if length(W.codomain) == 1 - print(io, W.codomain[1]) - else - print(io, W.codomain) - end - print(io, " ← ") - return if length(W.domain) == 1 - print(io, W.domain[1]) - else - print(io, W.domain) - end + return print( + io, + numout(W) == 1 ? codomain(W)[1] : codomain(W), + " ← ", + numin(W) == 1 ? domain(W)[1] : domain(W) + ) end """ - blocksectors(W::HomSpace) + blocksectors(W::HomSpace) -> Indices{I} -Return an iterator over the different unique coupled sector labels, i.e. the intersection -of the different fusion outputs that can be obtained by fusing the sectors present in the -domain, as well as from the codomain. +Return an `Indices` of all coupled sectors for `W`. The result is cached based on the +sector structure of `W` (ignoring degeneracy dimensions). -See also [`hasblock`](@ref). +See also [`hasblock`](@ref), [`blockstructure`](@ref). """ -function blocksectors(W::HomSpace) +blocksectors(W::HomSpace) = sectorstructure(W).blocksectors + +function _blocksectors(W::HomSpace) sectortype(W) === Trivial && return OneOrNoneIterator(dim(domain(W)) != 0 && dim(codomain(W)) != 0, Trivial()) @@ -113,30 +117,51 @@ Query whether a coupled sector `c` appears in both the codomain and domain of `W See also [`blocksectors`](@ref). """ -hasblock(W::HomSpace, c::Sector) = hasblock(codomain(W), c) && hasblock(domain(W), c) +hasblock(W::HomSpace, c::Sector) = c in blocksectors(W) """ - dim(W::HomSpace) + dim(W::HomSpace) -> Int Return the total dimension of a `HomSpace`, i.e. the number of linearly independent morphisms that can be constructed within this space. """ -function dim(W::HomSpace) - d = 0 - for c in blocksectors(W) - d += blockdim(codomain(W), c) * blockdim(domain(W), c) - end - return d -end +dim(W::HomSpace) = degeneracystructure(W).totaldim dims(W::HomSpace) = (dims(codomain(W))..., dims(domain(W))...) """ - fusiontrees(W::HomSpace) + blockstructure(W::HomSpace) -> Dictionary + +Return a `Dictionary` mapping each coupled sector `c::I` to a tuple `((d₁, d₂), r)`, +where `d₁` and `d₂` are the block dimensions for the codomain and domain respectively, +and `r` is the corresponding index range in the flat data vector. + +See also [`degeneracystructure`](@ref), [`subblockstructure`](@ref). +""" +blockstructure(W::HomSpace) = Dictionary(blocksectors(W), degeneracystructure(W).blockstructure) + +""" + fusiontrees(W::HomSpace) -> Indices{Tuple{F₁,F₂}} + +Return an `Indices` of all valid fusion tree pairs `(f₁, f₂)` for `W`, providing a +bijection to sequential integer positions via `gettoken`/`gettokenvalue`. The result is +cached based on the sector structure of `W` (ignoring degeneracy dimensions), so +`HomSpace`s that share the same sectors, dualities, and index count will reuse the same +object. + +See also [`sectorstructure`](@ref), [`subblockstructure`](@ref). +""" +fusiontrees(W::HomSpace) = sectorstructure(W).fusiontrees -Return the fusiontrees corresponding to all valid fusion channels of a given `HomSpace`. """ -fusiontrees(W::HomSpace) = fusionblockstructure(W).fusiontreelist + subblockstructure(W::HomSpace) -> Dictionary + +Return a `Dictionary` mapping each fusion tree pair `(f₁, f₂)` to its +[`StridedStructure`](@ref) `(sizes, strides, offset)`. + +See also [`degeneracystructure`](@ref), [`blockstructure`](@ref). +""" +subblockstructure(W::HomSpace) = Dictionary(fusiontrees(W), degeneracystructure(W).subblockstructure) """ fusionblocks(W::HomSpace) @@ -156,6 +181,20 @@ function fusionblocks(W::HomSpace) return fblocks end +function diagonalblockstructure(W::HomSpace) + ((numin(W) == numout(W) == 1) && domain(W) == codomain(W)) || + throw(SpaceMismatch("Diagonal only support on V←V with a single space V")) + structure = SectorDict{sectortype(W), UnitRange{Int}}() # range + offset = 0 + dom = domain(W)[1] + for c in blocksectors(W) + d = dim(dom, c) + structure[c] = offset .+ (1:d) + offset += d + end + return structure +end + # Operations on HomSpaces # ----------------------- """ @@ -308,125 +347,3 @@ function removeunit(P::HomSpace, ::Val{i}) where {i} return codomain(P) ← removeunit(domain(P), Val(i - numout(P))) end end - -# Block and fusion tree ranges: structure information for building tensors -#-------------------------------------------------------------------------- - -# sizes, strides, offset -const StridedStructure{N} = Tuple{NTuple{N, Int}, NTuple{N, Int}, Int} - -struct FusionBlockStructure{I, N, F₁, F₂} - totaldim::Int - blockstructure::SectorDict{I, Tuple{Tuple{Int, Int}, UnitRange{Int}}} - fusiontreelist::Vector{Tuple{F₁, F₂}} - fusiontreestructure::Vector{StridedStructure{N}} - fusiontreeindices::FusionTreeDict{Tuple{F₁, F₂}, Int} -end - -function fusionblockstructuretype(W::HomSpace) - N₁ = length(codomain(W)) - N₂ = length(domain(W)) - N = N₁ + N₂ - I = sectortype(W) - F₁ = fusiontreetype(I, N₁) - F₂ = fusiontreetype(I, N₂) - return FusionBlockStructure{I, N, F₁, F₂} -end - -@cached function fusionblockstructure(W::HomSpace)::fusionblockstructuretype(W) - codom = codomain(W) - dom = domain(W) - N₁ = length(codom) - N₂ = length(dom) - I = sectortype(W) - F₁ = fusiontreetype(I, N₁) - F₂ = fusiontreetype(I, N₂) - - # output structure - blockstructure = SectorDict{I, Tuple{Tuple{Int, Int}, UnitRange{Int}}}() # size, range - fusiontreelist = Vector{Tuple{F₁, F₂}}() - fusiontreestructure = Vector{Tuple{NTuple{N₁ + N₂, Int}, NTuple{N₁ + N₂, Int}, Int}}() # size, strides, offset - - # temporary data structures - splittingtrees = Vector{F₁}() - splittingstructure = Vector{Tuple{Int, Int}}() - - # main computational routine - blockoffset = 0 - for c in blocksectors(W) - empty!(splittingtrees) - empty!(splittingstructure) - - offset₁ = 0 - for f₁ in fusiontrees(codom, c) - push!(splittingtrees, f₁) - d₁ = dim(codom, f₁.uncoupled) - push!(splittingstructure, (offset₁, d₁)) - offset₁ += d₁ - end - blockdim₁ = offset₁ - strides = (1, blockdim₁) - - offset₂ = 0 - for f₂ in fusiontrees(dom, c) - s₂ = f₂.uncoupled - d₂ = dim(dom, s₂) - for (f₁, (offset₁, d₁)) in zip(splittingtrees, splittingstructure) - push!(fusiontreelist, (f₁, f₂)) - totaloffset = blockoffset + offset₂ * blockdim₁ + offset₁ - subsz = (dims(codom, f₁.uncoupled)..., dims(dom, f₂.uncoupled)...) - @assert !any(isequal(0), subsz) - substr = _subblock_strides(subsz, (d₁, d₂), strides) - push!(fusiontreestructure, (subsz, substr, totaloffset)) - end - offset₂ += d₂ - end - blockdim₂ = offset₂ - blocksize = (blockdim₁, blockdim₂) - blocklength = blockdim₁ * blockdim₂ - blockrange = (blockoffset + 1):(blockoffset + blocklength) - blockoffset = last(blockrange) - blockstructure[c] = (blocksize, blockrange) - end - - fusiontreeindices = sizehint!( - FusionTreeDict{Tuple{F₁, F₂}, Int}(), length(fusiontreelist) - ) - for (i, f₁₂) in enumerate(fusiontreelist) - fusiontreeindices[f₁₂] = i - end - totaldim = blockoffset - structure = FusionBlockStructure( - totaldim, blockstructure, fusiontreelist, fusiontreestructure, fusiontreeindices - ) - return structure -end - -function _subblock_strides(subsz, sz, str) - sz_simplify = Strided.StridedViews._simplifydims(sz, str) - strides = Strided.StridedViews._computereshapestrides(subsz, sz_simplify...) - isnothing(strides) && - throw(ArgumentError("unexpected error in computing subblock strides")) - return strides -end - -function CacheStyle(::typeof(fusionblockstructure), W::HomSpace) - return GlobalLRUCache() -end - -# Diagonal ranges -#---------------- -# TODO: is this something we want to cache? -function diagonalblockstructure(W::HomSpace) - ((numin(W) == numout(W) == 1) && domain(W) == codomain(W)) || - throw(SpaceMismatch("Diagonal only support on V←V with a single space V")) - structure = SectorDict{sectortype(W), UnitRange{Int}}() # range - offset = 0 - dom = domain(W)[1] - for c in blocksectors(W) - d = dim(dom, c) - structure[c] = offset .+ (1:d) - offset += d - end - return structure -end diff --git a/src/spaces/productspace.jl b/src/spaces/productspace.jl index 419d8acb1..f86e1fd9f 100644 --- a/src/spaces/productspace.jl +++ b/src/spaces/productspace.jl @@ -218,6 +218,18 @@ Base.:(==)(P1::ProductSpace, P2::ProductSpace) = false # hashing S is necessary to have different hashes for empty productspace with different S Base.hash(P::ProductSpace{S}, h::UInt) where {S} = hash(P.spaces, hash(S, h)) +function sectorequal(P₁::V, P₂::V) where {V <: ProductSpace} + return all(sectorequal(w₁, w₂) for (w₁, w₂) in zip(P₁, P₂)) +end +sectorequal(::ProductSpace, ::ProductSpace) = false + +function sectorhash(P::ProductSpace, h::UInt) + for w in P + h = sectorhash(w, h) + end + return h +end + # Default construction from product of spaces #--------------------------------------------- ⊗(V::ElementarySpace, Vrest::ElementarySpace...) = ProductSpace(V, Vrest...) diff --git a/src/spaces/structure.jl b/src/spaces/structure.jl new file mode 100644 index 000000000..9a4ddf5c0 --- /dev/null +++ b/src/spaces/structure.jl @@ -0,0 +1,198 @@ +# sizes, strides, offset +const StridedStructure{N} = Tuple{NTuple{N, Int}, NTuple{N, Int}, Int} + +# SectorStructure: sector-dependent characterization of HomSpaces +# --------------------------------------------------------------- +""" + SectorStructure{I <: Sector, F <: FusionTreePair} + +Sector-only structure of a `HomSpace`: the coupled sectors and all valid fusion tree pairs, +depending only on which sectors appear (not their degeneracy dimensions). Shared across +`HomSpace`s with the same sector structure. + +## Fields +- `blocksectors`: `Indices` of all coupled sectors `c::I`. +- `fusiontrees`: `Indices` of all valid fusion tree pairs `(f₁, f₂)`, in canonical order. + +See also [`sectorstructure`](@ref), [`DegeneracyStructure`](@ref). +""" +struct SectorStructure{I <: Sector, F <: FusionTreePair{I}} + blocksectors::Indices{I} + fusiontrees::Indices{F} +end + +Base.@assume_effects :foldable function sectorstructuretype(key::Hashed{S}) where {S <: HomSpace} + I = sectortype(S) + F = fusiontreetype(I, numout(S), numin(S)) + return SectorStructure{I, F} +end + +""" + sectorstructure(W::HomSpace) -> SectorStructure + +Return the [`SectorStructure`](@ref) for `W`, containing the coupled sectors and fusion tree +pairs as `Indices`. The result is cached based on the sector structure of `W` (ignoring +degeneracy dimensions). + +See also [`degeneracystructure`](@ref), [`fusiontrees`](@ref), [`blocksectors`](@ref). +""" sectorstructure(::HomSpace) +sectorstructure(W::HomSpace) = sectorstructure(Hashed(W, sectorhash, sectorequal)) + +@cached function sectorstructure(key::Hashed{S})::sectorstructuretype(key) where {S <: HomSpace} + W = parent(key) + codom, dom = codomain(W), domain(W) + + I = sectortype(S) + F = fusiontreetype(I, numout(S), numin(S)) + bs = Vector{I}() + trees = Vector{F}() + + for c in _blocksectors(W) + push!(bs, c) + offset = length(trees) + n₁ = 0 + for f₂ in fusiontrees(dom, c) + if n₁ == 0 + # First f₂ for this sector: enumerate codomain trees and record how many there are. + for f₁ in fusiontrees(codom, c) + push!(trees, (f₁, f₂)) + end + n₁ = length(trees) - offset + else + # Subsequent f₂s: the codomain trees are already in the list at + # offset .+ (1:n₁), so read them back instead of recomputing. + for j in offset .+ (1:n₁) + push!(trees, (trees[j][1], f₂)) + end + end + end + end + + return SectorStructure{I, F}(Indices(bs), Indices(trees)) +end + +CacheStyle(::typeof(sectorstructure), ::Hashed{<:HomSpace}) = GlobalLRUCache() + +# DegeneracyStructure: degeneracy-dependent characterization of HomSpaces +# ----------------------------------------------------------------------- +""" + DegeneracyStructure{N} + +Degeneracy-dependent structure of a `HomSpace`: the block sizes, ranges, and sub-block +strides that depend on the degeneracy (multiplicity) dimensions. Specific to a given +`HomSpace` instance. + +## Fields +- `totaldim`: total number of elements in the flat data vector. +- `blockstructure`: `Vector` of `((d₁, d₂), range)` values, one per coupled sector, in the + same order as [`sectorstructure`](@ref)`.blocksectors`. +- `subblockstructure`: `Vector` of [`StridedStructure`](@ref) `(sizes, strides, offset)` + values, one per fusion tree pair, in the same order as [`sectorstructure`](@ref)`.fusiontrees`. + +See also [`degeneracystructure`](@ref), [`SectorStructure`](@ref). +""" +struct DegeneracyStructure{N} + totaldim::Int + blockstructure::Vector{Tuple{Tuple{Int, Int}, UnitRange{Int}}} + subblockstructure::Vector{StridedStructure{N}} +end + +function degeneracystructuretype(W::HomSpace) + N = length(codomain(W)) + length(domain(W)) + return DegeneracyStructure{N} +end + +""" + degeneracystructure(W::HomSpace) -> DegeneracyStructure + +Compute the [`DegeneracyStructure`](@ref) for `W`, describing block sizes, data ranges, and +sub-block strides. The result is cached per `HomSpace` instance (keyed by object identity, +since degeneracy dimensions affect the block sizes and offsets). + +See also [`sectorstructure`](@ref), [`blockstructure`](@ref), [`subblockstructure`](@ref). +""" degeneracystructure(::HomSpace) +@cached function degeneracystructure(W::HomSpace)::degeneracystructuretype(W) + codom = codomain(W) + dom = domain(W) + N = length(codom) + length(dom) + + ss = sectorstructure(W) + treelist = ss.fusiontrees + L = length(treelist) + structurevalues = sizehint!(Vector{StridedStructure{N}}(), L) + blockvalues = Vector{Tuple{Tuple{Int, Int}, UnitRange{Int}}}(undef, length(ss.blocksectors)) + + # temporary data structures + splittingstructure = Vector{NTuple{numout(W), Int}}() + + blockoffset = 0 + tree_index = 1 + block_index = 1 + while tree_index <= L + f₁, f₂ = gettokenvalue(treelist, tree_index) + c = f₁.coupled + + # compute subblock structure + # splitting tree data + empty!(splittingstructure) + offset₁ = 0 + for i in tree_index:L + f₁′, f₂′ = gettokenvalue(treelist, i) + f₂′ == f₂ || break + s₁ = f₁′.uncoupled + d₁s = dims(codom, s₁) + d₁ = prod(d₁s) + offset₁ += d₁ + push!(splittingstructure, d₁s) + end + blockdim₁ = offset₁ + n₁ = length(splittingstructure) + strides = (1, blockdim₁) + + # fusion tree data and combine + offset₂ = 0 + n₂ = 0 + for i in tree_index:n₁:L + f₁′, f₂′ = gettokenvalue(treelist, i) + f₂′.coupled == c || break + n₂ += 1 + s₂ = f₂′.uncoupled + d₂s = dims(dom, s₂) + d₂ = prod(d₂s) + offset₁ = 0 + for d₁s in splittingstructure + d₁ = prod(d₁s) + totaloffset = blockoffset + offset₂ * blockdim₁ + offset₁ + subsz = (d₁s..., d₂s...) + @assert !any(==(0), subsz) + substr = _subblock_strides(subsz, (d₁, d₂), strides) + push!(structurevalues, (subsz, substr, totaloffset)) + offset₁ += d₁ + end + offset₂ += d₂ + end + + # compute block structure + blockdim₂ = offset₂ + blockrange = (blockoffset + 1):(blockoffset + blockdim₁ * blockdim₂) + blockvalues[block_index] = ((blockdim₁, blockdim₂), blockrange) + + # reset + blockoffset = last(blockrange) + tree_index += n₁ * n₂ + block_index += 1 + end + @assert length(structurevalues) == L + + return DegeneracyStructure(blockoffset, blockvalues, structurevalues) +end + +function _subblock_strides(subsz, sz, str) + sz_simplify = Strided.StridedViews._simplifydims(sz, str) + strides = Strided.StridedViews._computereshapestrides(subsz, sz_simplify...) + isnothing(strides) && + throw(ArgumentError("unexpected error in computing subblock strides")) + return strides +end + +CacheStyle(::typeof(degeneracystructure), ::HomSpace) = GlobalLRUCache() diff --git a/src/spaces/vectorspaces.jl b/src/spaces/vectorspaces.jl index 867d18959..2e3ea01bd 100644 --- a/src/spaces/vectorspaces.jl +++ b/src/spaces/vectorspaces.jl @@ -363,6 +363,18 @@ Return an iterator over the different sectors of `V`. """ function sectors end +function sectorequal(V₁::ElementarySpace, V₂::ElementarySpace) + isdual(V₁) == isdual(V₂) || return false + return issetequal(sectors(V₁), sectors(V₂)) +end +function sectorhash(V::ElementarySpace, h::UInt) + h = hash(isdual(V), h) + for s in sectors(V) + h = hash(s, h) + end + return h +end + # Composite vector spaces #------------------------- """ @@ -405,33 +417,6 @@ end blocksectors(V::ElementarySpace) = collect(sectors(V)) blockdim(V::ElementarySpace, c::Sector) = dim(V, c) -# Specific realizations of ElementarySpace types -#------------------------------------------------ -# spaces without internal structure -include("cartesianspace.jl") -include("complexspace.jl") -include("generalspace.jl") - -# space with internal structure corresponding to the irreducible representations of -# a group, or more generally, the simple objects of a fusion category. -include("gradedspace.jl") -include("planarspace.jl") - -# Specific realizations of CompositeSpace types -#----------------------------------------------- -# a tensor product of N elementary spaces of the same type S -include("productspace.jl") -# deligne tensor product -include("deligne.jl") - -# Other examples might include: -# symmetric and antisymmetric subspace of a tensor product of identical vector spaces -# ... - -# HomSpace: space of morphisms -#------------------------------ -include("homspace.jl") - # Partial order for vector spaces #--------------------------------- """ diff --git a/src/tensors/abstracttensor.jl b/src/tensors/abstracttensor.jl index 6c6e72840..d7d520b43 100644 --- a/src/tensors/abstracttensor.jl +++ b/src/tensors/abstracttensor.jl @@ -319,21 +319,13 @@ numind(t::AbstractTensorMap) = numind(typeof(t)) # tensor characteristics: data structure and properties #------------------------------------------------------ -""" - fusionblockstructure(t::AbstractTensorMap) -> TensorStructure - -Return the necessary structure information to decompose a tensor in blocks labeled by -coupled sectors and in subblocks labeled by a splitting-fusion tree couple. -""" -fusionblockstructure(t::AbstractTensorMap) = fusionblockstructure(space(t)) - """ dim(t::AbstractTensorMap) -> Int The total number of free parameters of a tensor, discounting the entries that are fixed by symmetry. This is also the dimension of the `HomSpace` on which the `TensorMap` is defined. """ -dim(t::AbstractTensorMap) = fusionblockstructure(t).totaldim +dim(t::AbstractTensorMap) = dim(space(t)) dims(t::AbstractTensorMap) = dims(space(t)) @@ -342,7 +334,7 @@ dims(t::AbstractTensorMap) = dims(space(t)) Return an iterator over all coupled sectors of a tensor. """ -blocksectors(t::AbstractTensorMap) = keys(fusionblockstructure(t).blockstructure) +blocksectors(t::AbstractTensorMap) = blocksectors(space(t)) """ hasblock(t::AbstractTensorMap, c::Sector) -> Bool @@ -351,46 +343,15 @@ Verify whether a tensor has a block corresponding to a coupled sector `c`. """ hasblock(t::AbstractTensorMap, c::Sector) = c ∈ blocksectors(t) -# TODO: convenience methods, do we need them? -# """ -# blocksize(t::AbstractTensorMap, c::Sector) -> Tuple{Int,Int} - -# Return the size of the matrix block of a tensor corresponding to a coupled sector `c`. - -# See also [`blockdim`](@ref) and [`blockrange`](@ref). -# """ -# function blocksize(t::AbstractTensorMap, c::Sector) -# return fusionblockstructure(t).blockstructure[c][1] -# end - -# """ -# blockdim(t::AbstractTensorMap, c::Sector) -> Int - -# Return the total dimension (length) of the matrix block of a tensor corresponding to -# a coupled sector `c`. - -# See also [`blocksize`](@ref) and [`blockrange`](@ref). -# """ -# function blockdim(t::AbstractTensorMap, c::Sector) -# return *(blocksize(t, c)...) -# end - -# """ -# blockrange(t::AbstractTensorMap, c::Sector) -> UnitRange{Int} - -# Return the range at which to find the matrix block of a tensor corresponding to a -# coupled sector `c`, within the total data vector of length `dim(t)`. -# """ -# function blockrange(t::AbstractTensorMap, c::Sector) -# return fusionblockstructure(t).blockstructure[c][2] -# end +blockstructure(t::AbstractTensorMap) = blockstructure(space(t)) +subblockstructure(t::AbstractTensorMap) = subblockstructure(space(t)) """ fusiontrees(t::AbstractTensorMap) Return an iterator over all splitting - fusion tree pairs of a tensor. """ -fusiontrees(t::AbstractTensorMap) = fusionblockstructure(t).fusiontreelist +fusiontrees(t::AbstractTensorMap) = fusiontrees(space(t)) fusiontreetype(t::AbstractTensorMap) = fusiontreetype(typeof(t)) function fusiontreetype(::Type{T}) where {T <: AbstractTensorMap} diff --git a/src/tensors/braidingtensor.jl b/src/tensors/braidingtensor.jl index c2bae5cf9..d9dbba56c 100644 --- a/src/tensors/braidingtensor.jl +++ b/src/tensors/braidingtensor.jl @@ -165,10 +165,9 @@ function block(b::BraidingTensor, s::Sector) return data end - structure = fusionblockstructure(b) - base_offset = first(structure.blockstructure[s][2]) - 1 + base_offset = first(blockstructure(b)[s][2]) - 1 - for ((f₁, f₂), (sz, str, off)) in zip(structure.fusiontreelist, structure.fusiontreestructure) + for ((f₁, f₂), (sz, str, off)) in pairs(subblockstructure(space(b))) (f₁.coupled == f₂.coupled == s) || continue r = _braiding_factor(f₁, f₂) isnothing(r) && continue diff --git a/src/tensors/tensor.jl b/src/tensors/tensor.jl index 598356f04..342c83186 100644 --- a/src/tensors/tensor.jl +++ b/src/tensors/tensor.jl @@ -15,8 +15,7 @@ struct TensorMap{T, S <: IndexSpace, N₁, N₂, A <: DenseVector{T}} <: Abstrac function TensorMap{T, S, N₁, N₂, A}( ::UndefInitializer, space::TensorMapSpace{S, N₁, N₂} ) where {T, S <: IndexSpace, N₁, N₂, A <: DenseVector{T}} - d = fusionblockstructure(space).totaldim - data = A(undef, d) + data = A(undef, dim(space)) if !isbitstype(T) zerovector!(data) end @@ -31,8 +30,7 @@ struct TensorMap{T, S <: IndexSpace, N₁, N₂, A <: DenseVector{T}} <: Abstrac I = sectortype(S) T <: Real && !(sectorscalartype(I) <: Real) && @warn("Tensors with real data might be incompatible with sector type $I", maxlog = 1) - d = fusionblockstructure(space).totaldim - length(data) == d || throw(DimensionMismatch("invalid length of data")) + length(data) == dim(space) || throw(DimensionMismatch("invalid length of data")) return new{T, S, N₁, N₂, A}(data, space) end end @@ -453,14 +451,14 @@ end #------------------------------------------------- block(t::TensorMap, c::Sector) = blocks(t)[c] -blocks(t::TensorMap) = BlockIterator(t, fusionblockstructure(t).blockstructure) +blocks(t::TensorMap) = BlockIterator(t, blockstructure(space(t))) function blocktype(::Type{TensorMap{T, S, N₁, N₂, A}}) where {T, S, N₁, N₂, A <: Vector{T}} return Base.ReshapedArray{T, 2, SubArray{T, 1, A, Tuple{UnitRange{Int}}, true}, Tuple{}} end function Base.iterate(iter::BlockIterator{<:TensorMap}, state...) - next = iterate(iter.structure, state...) + next = iterate(pairs(iter.structure), state...) isnothing(next) && return next (c, (sz, r)), newstate = next return c => reshape(view(iter.t.data, r), sz), newstate @@ -468,16 +466,18 @@ end function Base.getindex(iter::BlockIterator{<:TensorMap}, c::Sector) sectortype(iter.t) === typeof(c) || throw(SectorMismatch()) - (d₁, d₂), r = get(iter.structure, c) do - # is s is not a key, at least one of the two dimensions will be zero: + found, token = gettoken(iter.structure, c) + if found + (d₁, d₂), r = gettokenvalue(iter.structure, token) + return reshape(view(iter.t.data, r), (d₁, d₂)) + else + # if c is not a key, at least one of the two dimensions will be zero: # it then does not matter where exactly we construct a view in `t.data`, # as it will have length zero anyway - d₁′ = blockdim(codomain(iter.t), c) - d₂′ = blockdim(domain(iter.t), c) - l = d₁′ * d₂′ - return (d₁′, d₂′), 1:l + d₁ = blockdim(codomain(iter.t), c) + d₂ = blockdim(domain(iter.t), c) + return reshape(view(iter.t.data, 1:(d₁ * d₂)), (d₁, d₂)) end - return reshape(view(iter.t.data, r), (d₁, d₂)) end # Getting and setting the data at the subblock level @@ -485,13 +485,11 @@ end function subblock( t::TensorMap{T, S, N₁, N₂}, (f₁, f₂)::Tuple{FusionTree{I, N₁}, FusionTree{I, N₂}} ) where {T, S, N₁, N₂, I <: Sector} - structure = fusionblockstructure(t) - @boundscheck begin - haskey(structure.fusiontreeindices, (f₁, f₂)) || throw(SectorMismatch()) - end + fts = subblockstructure(space(t)) + found, token = gettoken(fts, (f₁, f₂)) + @boundscheck found || throw(SectorMismatch(lazy"fusion tree pair ($(f₁, f₂)) is not present")) @inbounds begin - i = structure.fusiontreeindices[(f₁, f₂)] - sz, str, offset = structure.fusiontreestructure[i] + sz, str, offset = gettokenvalue(fts, token) return StridedView(t.data, sz, str, offset) end end diff --git a/src/tensors/tensoroperations.jl b/src/tensors/tensoroperations.jl index 7c9f8db11..3fc79cf0c 100644 --- a/src/tensors/tensoroperations.jl +++ b/src/tensors/tensoroperations.jl @@ -9,8 +9,7 @@ function TO.tensoralloc( ::Type{TT}, structure::TensorMapSpace, istemp::Val, allocator = TO.DefaultAllocator() ) where {TT <: AbstractTensorMap} A = storagetype(TT) - dim = fusionblockstructure(structure).totaldim - data = TO.tensoralloc(A, dim, istemp, allocator) + data = TO.tensoralloc(A, dim(structure), istemp, allocator) TT′ = tensormaptype(spacetype(structure), numout(structure), numin(structure), typeof(data)) return TT′(data, structure) end diff --git a/src/tensors/treetransformers.jl b/src/tensors/treetransformers.jl index 5b11bb779..30ec1de0f 100644 --- a/src/tensors/treetransformers.jl +++ b/src/tensors/treetransformers.jl @@ -16,20 +16,16 @@ end function AbelianTreeTransformer(transform, p, Vdst, Vsrc) t₀ = Base.time() permute(Vsrc, p) == Vdst || throw(SpaceMismatch("Incompatible spaces for permuting.")) - structure_dst = fusionblockstructure(Vdst) - structure_src = fusionblockstructure(Vsrc) - - L = length(structure_src.fusiontreelist) + fts_src = subblockstructure(Vsrc) + fts_dst = subblockstructure(Vdst) + L = length(fts_src) T = sectorscalartype(sectortype(Vdst)) N = numind(Vsrc) data = Vector{Tuple{T, StridedStructure{N}, StridedStructure{N}}}(undef, L) - for i in 1:L - f₁, f₂ = structure_src.fusiontreelist[i] - (f₃, f₄), coeff = transform((f₁, f₂)) - j = structure_dst.fusiontreeindices[(f₃, f₄)] - stridestructure_dst = structure_dst.fusiontreestructure[j] - stridestructure_src = structure_src.fusiontreestructure[i] + for (i, (f_src, stridestructure_src)) in enumerate(pairs(fts_src)) + f_dst, coeff = transform(f_src) + stridestructure_dst = fts_dst[f_dst] data[i] = (coeff, stridestructure_dst, stridestructure_src) end @@ -58,11 +54,8 @@ end function GenericTreeTransformer(transform, p, Vdst, Vsrc) t₀ = Base.time() permute(Vsrc, p) == Vdst || throw(SpaceMismatch("Incompatible spaces for permuting.")) - structure_dst = fusionblockstructure(Vdst) - fusionstructure_dst = structure_dst.fusiontreestructure - structure_src = fusionblockstructure(Vsrc) - fusionstructure_src = structure_src.fusiontreestructure - + fusionstructure_dst = subblockstructure(Vdst) + fusionstructure_src = subblockstructure(Vsrc) I = sectortype(Vsrc) T = sectorscalartype(I) N = numind(Vdst) @@ -83,23 +76,14 @@ function GenericTreeTransformer(transform, p, Vdst, Vsrc) local_counter > nblocks && break fs_src = fblocks[local_counter] fs_dst, U = transform(fs_src) - matrix = copy(transpose(U)) # TODO: should we avoid this - - trees_src = fusiontrees(fs_src) - inds_src = map(Base.Fix1(getindex, structure_src.fusiontreeindices), trees_src) - trees_dst = fusiontrees(fs_dst) - inds_dst = map(Base.Fix1(getindex, structure_dst.fusiontreeindices), trees_dst) + sz_src, newstructs_src = repack_transformer_structure(fusionstructure_src, fusiontrees(fs_src)) + sz_dst, newstructs_dst = repack_transformer_structure(fusionstructure_dst, fusiontrees(fs_dst)) + data[local_counter] = U, (sz_dst, newstructs_dst), (sz_src, newstructs_src) - # size is shared between blocks, so repack: - # from [(sz, strides, offset), ...] to (sz, [(strides, offset), ...]) - sz_src, newstructs_src = repack_transformer_structure( - fusionstructure_src, inds_src + @debug( + "Created recoupling block for uncoupled: $(fs_src.uncoupled)", + sz = size(U), sparsity = count(!iszero, U) / length(U) ) - sz_dst, newstructs_dst = repack_transformer_structure( - fusionstructure_dst, inds_dst - ) - - data[local_counter] = (matrix, (sz_dst, newstructs_dst), (sz_src, newstructs_src)) end end end @@ -107,22 +91,14 @@ function GenericTreeTransformer(transform, p, Vdst, Vsrc) else for (i, fs_src) in enumerate(fblocks) fs_dst, U = transform(fs_src) + sz_src, newstructs_src = repack_transformer_structure(fusionstructure_src, fusiontrees(fs_src)) + sz_dst, newstructs_dst = repack_transformer_structure(fusionstructure_dst, fusiontrees(fs_dst)) + data[i] = U, (sz_dst, newstructs_dst), (sz_src, newstructs_src) - trees_src = fusiontrees(fs_src) - inds_src = map(Base.Fix1(getindex, structure_src.fusiontreeindices), trees_src) - trees_dst = fusiontrees(fs_dst) - inds_dst = map(Base.Fix1(getindex, structure_dst.fusiontreeindices), trees_dst) - - # size is shared between blocks, so repack: - # from [(sz, strides, offset), ...] to (sz, [(strides, offset), ...]) - sz_src, newstructs_src = repack_transformer_structure( - fusionstructure_src, inds_src - ) - sz_dst, newstructs_dst = repack_transformer_structure( - fusionstructure_dst, inds_dst + @debug( + "Created recoupling block for uncoupled: $(fs_src.uncoupled)", + sz = size(U), sparsity = count(!iszero, U) / length(U) ) - - data[i] = U, (sz_dst, newstructs_dst), (sz_src, newstructs_src) end transformer = GenericTreeTransformer{T, N}(data) end @@ -143,9 +119,12 @@ function GenericTreeTransformer(transform, p, Vdst, Vsrc) return transformer end -function repack_transformer_structure(structures, ids) - sz = structures[first(ids)][1] - strides_offsets = map(i -> (structures[i][2], structures[i][3]), ids) +function repack_transformer_structure(structures::Dictionary, trees) + sz = structures[first(trees)][1] + strides_offsets = map(trees) do f + _, stride, offset = structures[f] + return stride, offset + end return sz, strides_offsets end diff --git a/test/other/hashed.jl b/test/other/hashed.jl new file mode 100644 index 000000000..ae0c18915 --- /dev/null +++ b/test/other/hashed.jl @@ -0,0 +1,48 @@ +using Test, TestExtras +using TensorKit +using TensorKit: Hashed + +@testset "Hashed" begin + @testset "default constructor" begin + h1 = @constinferred Hashed(42) + h2 = Hashed(42) + @test isequal(h1, h2) + @test hash(h1) == hash(h2) + @test parent(h1) == 42 + end + + @testset "custom hash function" begin + # hash only the length, ignoring contents + lenhash = (v, seed) -> hash(length(v), seed) + h1 = Hashed([1, 2, 3], lenhash) + h2 = Hashed([4, 5, 6], lenhash) + @test hash(h1) == hash(h2) + h3 = Hashed([1, 2], lenhash) + @test hash(h1) != hash(h3) + end + + @testset "custom isequal" begin + # consider vectors equal if they have the same length + lenequal = (a, b) -> length(a) == length(b) + h1 = Hashed([1, 2, 3], Base.hash, lenequal) + h2 = Hashed([4, 5, 6], Base.hash, lenequal) + h3 = Hashed([1, 2], Base.hash, lenequal) + @test isequal(h1, h2) + @test !isequal(h1, h3) + end + + @testset "Dict key usage" begin + d = Dict(Hashed(1) => "one", Hashed(2) => "two") + @test d[Hashed(1)] == "one" + @test d[Hashed(2)] == "two" + @test length(d) == 2 + end + + @testset "Dict with custom hash and isequal" begin + lenhash = (v, seed) -> hash(length(v), seed) + lenequal = (a, b) -> length(a) == length(b) + d = Dict(Hashed([1, 2, 3], lenhash, lenequal) => "length3") + # lookup with different contents but same length should succeed + @test d[Hashed([7, 8, 9], lenhash, lenequal)] == "length3" + end +end diff --git a/test/symmetries/spaces.jl b/test/symmetries/spaces.jl index 75e9fd0b2..aeb71be35 100644 --- a/test/symmetries/spaces.jl +++ b/test/symmetries/spaces.jl @@ -1,6 +1,6 @@ using Test, TestExtras using TensorKit -using TensorKit: hassector, type_repr, HomSpace +using TensorKit: hassector, type_repr, HomSpace, sectorequal, sectorhash # TODO: remove this once type_repr works for all included types using TensorKitSectors @@ -476,4 +476,75 @@ end @test sprint((x, y) -> show(x, MIME"text/plain"(), y), V') == "$(type_repr(typeof(V)))(…)' of dim 3:\n 1 => 1\n 2 => 1\n 3 => 1" end +@timedtestset "sectorequal and sectorhash" begin + @timedtestset "CartesianSpace" begin + # Both spaces have only Trivial sector, dims don't matter + @test sectorequal(ℝ^3, ℝ^5) + @test !sectorequal(ℝ^3, ℝ^0) # zero space has no sectors + @test sectorhash(ℝ^3, UInt(0)) == sectorhash(ℝ^5, UInt(0)) + # CartesianSpace has no dual, so all spaces compare equal sectorwise + @test sectorhash(ℝ^3, UInt(0)) == sectorhash((ℝ^3)', UInt(0)) + end + + @timedtestset "ComplexSpace" begin + # Both have Trivial sector; only dual flag distinguishes them + @test sectorequal(ℂ^3, ℂ^5) + @test !sectorequal(ℂ^3, (ℂ^3)') # dual differs + @test sectorhash(ℂ^3, UInt(0)) == sectorhash(ℂ^5, UInt(0)) + @test sectorhash(ℂ^3, UInt(0)) != sectorhash((ℂ^3)', UInt(0)) + end + + @timedtestset "GradedSpace (NTuple storage)" begin + # Z2Irrep has a finite sector set → NTuple{2,Int} storage + V1 = ℤ₂Space(0 => 1, 1 => 2) + V2 = ℤ₂Space(0 => 2, 1 => 1) # same sectors, different dims + V3 = ℤ₂Space(0 => 1) # sector 1 absent (dim=0) + @test sectorequal(V1, V2) + @test !sectorequal(V1, V3) + @test !sectorequal(V1, V1') # dual differs + @test sectorhash(V1, UInt(0)) == sectorhash(V2, UInt(0)) + @test sectorhash(V1, UInt(0)) != sectorhash(V3, UInt(0)) + @test sectorhash(V1, UInt(0)) != sectorhash(V1', UInt(0)) + end + + @timedtestset "GradedSpace (SectorDict storage)" begin + # U1Irrep has infinite sectors → SectorDict storage + Va = U1Space(0 => 1, 1 => 2, -1 => 2) + Vb = U1Space(0 => 3, 1 => 1, -1 => 1) # same sectors, different dims + Vc = U1Space(0 => 1, 1 => 2) # -1 absent + @test sectorequal(Va, Vb) + @test !sectorequal(Va, Vc) + @test !sectorequal(Va, Va') + @test sectorhash(Va, UInt(0)) == sectorhash(Vb, UInt(0)) + @test sectorhash(Va, UInt(0)) != sectorhash(Vc, UInt(0)) + @test sectorhash(Va, UInt(0)) != sectorhash(Va', UInt(0)) + end + + @timedtestset "ProductSpace" begin + V1 = ℤ₂Space(0 => 1, 1 => 2) + V2 = ℤ₂Space(0 => 2, 1 => 1) + V3 = ℤ₂Space(0 => 1) + P1 = V1 ⊗ V2 + P2 = V2 ⊗ V1 # same sectors per slot but different order + P3 = V1 ⊗ V3 + @test sectorequal(P1, V1 ⊗ ℤ₂Space(0 => 3, 1 => 5)) + @test sectorequal(P1, P2) + @test !sectorequal(P1, P3) + @test sectorhash(P1, UInt(0)) == sectorhash(V1 ⊗ ℤ₂Space(0 => 3, 1 => 5), UInt(0)) + @test sectorhash(P1, UInt(0)) != sectorhash(P3, UInt(0)) + end + + @timedtestset "HomSpace" begin + V1 = ℤ₂Space(0 => 1, 1 => 2) + V2 = ℤ₂Space(0 => 2, 1 => 1) + W1 = V1 ⊗ V2 ← V1 + W2 = ℤ₂Space(0 => 5, 1 => 3) ⊗ ℤ₂Space(0 => 1, 1 => 7) ← ℤ₂Space(0 => 2, 1 => 1) + W3 = V1 ← V1 + @test sectorequal(W1, W2) + @test sectorhash(W1, UInt(0)) == sectorhash(W2, UInt(0)) + @test !sectorequal(W1, W3) + @test sectorhash(W1, UInt(0)) != sectorhash(W3, UInt(0)) + end +end + TensorKit.empty_globalcaches!()