From 5f9e5be011e03f0dc23d2beea578019e08cbabaf Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 26 Aug 2026 14:45:54 +0200 Subject: [PATCH 01/19] replace Nsymbol calls or add guards --- src/fusiontrees/iterator.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/fusiontrees/iterator.jl b/src/fusiontrees/iterator.jl index 341929420..ae7629500 100644 --- a/src/fusiontrees/iterator.jl +++ b/src/fusiontrees/iterator.jl @@ -46,6 +46,7 @@ _fusiondim(iters::NTuple{1}, c::I) where {I <: Sector} = Int(c ∈ iters[1]) function _fusiondim(iters::NTuple{2}, c::I) where {I <: Sector} d = 0 for a in iters[1], b in iters[2] + c ∈ a ⊗ b || continue d += Int(Nsymbol(a, b, c)) end return d @@ -104,7 +105,7 @@ function _fusiontree_iterate(uncoupledsectors::NTuple{2}, c::I) where {I <: Sect nextout1 = iterate(outiter1) nextout1 === nothing && return nothing a, outstate1 = nextout1 - while Nsymbol(a, b, c) == 0 + while c ∉ a ⊗ b nextout1 = iterate(outiter1, outstate1) if isnothing(nextout1) nextout2 = iterate(outiter2, outstate2) @@ -133,7 +134,7 @@ function _fusiontree_iterate(uncoupledsectors::NTuple{2}, c::I, out, lines, vert nextout1 = iterate(outiter1) end a, outstate1 = nextout1 - while Nsymbol(a, b, c) == 0 + while c ∉ a ⊗ b nextout1 = iterate(outiter1, outstate1) if isnothing(nextout1) nextout2 = iterate(outiter2, outstate2) From faf03b705533827b7fddaa4339f7cf78e3aedc85 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 2 Sep 2026 11:26:19 +0200 Subject: [PATCH 02/19] Revert "replace Nsymbol calls or add guards" This reverts commit 5f9e5be011e03f0dc23d2beea578019e08cbabaf. --- src/fusiontrees/iterator.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/fusiontrees/iterator.jl b/src/fusiontrees/iterator.jl index ae7629500..341929420 100644 --- a/src/fusiontrees/iterator.jl +++ b/src/fusiontrees/iterator.jl @@ -46,7 +46,6 @@ _fusiondim(iters::NTuple{1}, c::I) where {I <: Sector} = Int(c ∈ iters[1]) function _fusiondim(iters::NTuple{2}, c::I) where {I <: Sector} d = 0 for a in iters[1], b in iters[2] - c ∈ a ⊗ b || continue d += Int(Nsymbol(a, b, c)) end return d @@ -105,7 +104,7 @@ function _fusiontree_iterate(uncoupledsectors::NTuple{2}, c::I) where {I <: Sect nextout1 = iterate(outiter1) nextout1 === nothing && return nothing a, outstate1 = nextout1 - while c ∉ a ⊗ b + while Nsymbol(a, b, c) == 0 nextout1 = iterate(outiter1, outstate1) if isnothing(nextout1) nextout2 = iterate(outiter2, outstate2) @@ -134,7 +133,7 @@ function _fusiontree_iterate(uncoupledsectors::NTuple{2}, c::I, out, lines, vert nextout1 = iterate(outiter1) end a, outstate1 = nextout1 - while c ∉ a ⊗ b + while Nsymbol(a, b, c) == 0 nextout1 = iterate(outiter1, outstate1) if isnothing(nextout1) nextout2 = iterate(outiter2, outstate2) From 4bd06156f83f1844f2a18c02985c28654dd8f828 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 2 Sep 2026 16:54:07 +0200 Subject: [PATCH 03/19] don't allow calling unitspace for genericunit --- src/spaces/gradedspace.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index 53f48dafd..ab728c4eb 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -122,6 +122,8 @@ function flip(V::GradedSpace{I}) where {I <: Sector} end function unitspace(S::Type{<:GradedSpace{I}}) where {I <: Sector} + UnitStyle(I) isa GenericUnit && + throw(ArgumentError("Cannot construct unit space for sector types with semisimple unit structure.")) return S(unit => 1 for unit in allunits(I)) end zerospace(S::Type{<:GradedSpace}) = S() From 8c9ae8def53e14125216264378015e7382973cec Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 2 Sep 2026 16:55:01 +0200 Subject: [PATCH 04/19] check coloring in genericunit productspace construction --- src/spaces/productspace.jl | 19 +++++++++++++++++-- src/spaces/vectorspaces.jl | 37 ++++++++++++++++++++----------------- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/spaces/productspace.jl b/src/spaces/productspace.jl index 1c4b790e4..3a76b4067 100644 --- a/src/spaces/productspace.jl +++ b/src/spaces/productspace.jl @@ -7,7 +7,22 @@ Only tensor products between [`ElementarySpace`](@ref) objects of the same type """ struct ProductSpace{S <: ElementarySpace, N} <: CompositeSpace{S} spaces::NTuple{N, S} - ProductSpace{S, N}(spaces::NTuple{N, S}) where {S <: ElementarySpace, N} = new{S, N}(spaces) + function ProductSpace{S, N}(spaces::NTuple{N, S}) where {S <: ElementarySpace, N} + _check_unit_compatibility(S, spaces) + return new{S, N}(spaces) + end +end + +# check that every product of elementary spaces is compatible color-wise +function _check_unit_compatibility(::Type{S}, spaces::NTuple{N, S}) where {N, S <: ElementarySpace} + UnitStyle(sectortype(S)) isa GenericUnit || return nothing + N == 0 && return nothing + + @inbounds for i in 1:(N - 1) + _rightunit(spaces[i]) == _leftunit(spaces[i + 1]) || + throw(ArgumentError(lazy"spaces $(i) and $(i + 1) have incompatible coloring")) + end + return nothing end function ProductSpace{S, N}(spaces::Vararg{S, N}) where {S <: ElementarySpace, N} @@ -64,7 +79,7 @@ Base.axes(P::ProductSpace) = map(axes, P) Base.axes(P::ProductSpace, n::Int) = axes(P[n]) dual(P::ProductSpace{<:ElementarySpace, 0}) = P -dual(P::ProductSpace) = ProductSpace(map(dual, reverse(P))) +dual(P::ProductSpace) = ProductSpace(map(dual, reverse(P.spaces))) Base.conj(P::ProductSpace{<:ElementarySpace, 0}) = P Base.conj(P::ProductSpace) = ProductSpace(map(conj, P)) diff --git a/src/spaces/vectorspaces.jl b/src/spaces/vectorspaces.jl index 01dc00390..20b5ca4eb 100644 --- a/src/spaces/vectorspaces.jl +++ b/src/spaces/vectorspaces.jl @@ -134,9 +134,8 @@ Always returns `false` for spaces where `V == conj(V)`, i.e. vector spaces over Return the corresponding vector space of type `S` that represents the trivial one-dimensional space, i.e. the space that is isomorphic to the corresponding field. -For vector spaces where `I = sectortype(S)` has a semi-simple unit structure -(`UnitStyle(I) == GenericUnit()`), this returns a multi-dimensional space corresponding to all unit sectors: -`dim(unitspace(V), s) == 1` for all `s in allunits(I)`. +For vector spaces where `I = sectortype(S)` has a semisimple unit structure +(`UnitStyle(I) == GenericUnit()`), this errors. !!! note `unitspace(V)`is different from `one(V)`. The latter returns the empty product space @@ -170,15 +169,17 @@ function leftunitspace(V::ElementarySpace) if UnitStyle(I) isa SimpleUnit return unitspace(typeof(V)) else - !isempty(sectors(V)) || throw(ArgumentError("Cannot determine the left unit of an empty space")) - _allequal(leftunit, sectors(V)) || - throw(ArgumentError(lazy"sectors of $V do not have the same left unit")) - - sector = leftunit(first(sectors(V))) - return spacetype(V)(sector => 1) + return spacetype(V)(_leftunit(V) => 1) end end +function _leftunit(V::ElementarySpace) + !isempty(sectors(V)) || throw(ArgumentError("Cannot determine the left unit of an empty space")) + _allequal(leftunit, sectors(V)) || + throw(ArgumentError(lazy"sectors of $V do not have the same left unit")) + return leftunit(first(sectors(V))) +end + """ rightunitspace(V::S) where {S <: ElementarySpace} -> S @@ -192,22 +193,24 @@ function rightunitspace(V::ElementarySpace) if UnitStyle(I) isa SimpleUnit return unitspace(typeof(V)) else - !isempty(sectors(V)) || throw(ArgumentError("Cannot determine the right unit of an empty space")) - _allequal(rightunit, sectors(V)) || - throw(ArgumentError(lazy"sectors of $V do not have the same right unit")) - - sector = rightunit(first(sectors(V))) - return spacetype(V)(sector => 1) + return spacetype(V)(_rightunit(V) => 1) end end +function _rightunit(V::ElementarySpace) + !isempty(sectors(V)) || throw(ArgumentError("Cannot determine the right unit of an empty space")) + _allequal(rightunit, sectors(V)) || + throw(ArgumentError(lazy"sectors of $V do not have the same right unit")) + return rightunit(first(sectors(V))) +end + """ isunitspace(V::S) where {S <: ElementarySpace} -> Bool Return whether the elementary space `V` is a unit space, i.e. is isomorphic to the trivial one-dimensional space. For vector spaces of type `GradedSpace{I}` where `Sector` `I` has a -semi-simple unit structure, this returns `true` if `V` is isomorphic to either the left, right or -semi-simple unit space. +semisimple unit structure, this returns `true` if `V` is isomorphic to either the left, right or +semisimple unit space. """ function isunitspace(V::ElementarySpace) I = sectortype(V) From d40019396ead0bbe3a0d87cef6d1009cd0559119 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 2 Sep 2026 16:56:29 +0200 Subject: [PATCH 05/19] do the same for homspace --- src/spaces/homspace.jl | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/spaces/homspace.jl b/src/spaces/homspace.jl index 8460f7578..60bba80fa 100644 --- a/src/spaces/homspace.jl +++ b/src/spaces/homspace.jl @@ -9,6 +9,43 @@ to denote categories and their objects, and keep `HomSpace` distinct. struct HomSpace{S <: ElementarySpace, P1 <: CompositeSpace{S}, P2 <: CompositeSpace{S}} codomain::P1 domain::P2 + function HomSpace{S, P1, P2}(codomain::P1, domain::P2) where {S <: ElementarySpace, P1 <: CompositeSpace{S}, P2 <: CompositeSpace{S}} + _check_unit_compatibility(codomain, domain) + return new{S, P1, P2}(codomain, domain) + end +end +function HomSpace(codomain::P1, domain::P2) where {S, P1 <: CompositeSpace{S}, P2 <: CompositeSpace{S}} + return HomSpace{S, P1, P2}(codomain, domain) +end + +function _check_unit_compatibility(codomain::ProductSpace{S}, domain::ProductSpace{S}) where {S <: ElementarySpace} + UnitStyle(sectortype(S)) isa GenericUnit || return nothing + N₁, N₂ = length(codomain), length(domain) + N₁ == N₂ == 0 && return nothing # one() ← one() + + # product spaces themselves already check that their factors are compatible + if N₁ == 0 # codomain is empty, domain is non-empty + VdomL, VdomR = domain[1], domain[N₂] + _leftunit(VdomL) == _rightunit(VdomR) || + throw(ArgumentError("cannot construct HomSpace: domain has incompatible left and right units")) + return nothing + elseif N₂ == 0 # domain is empty, codomain is non-empty + VcodL, VcodR = codomain[1], codomain[N₁] + _leftunit(VcodL) == _rightunit(VcodR) || + throw(ArgumentError("cannot construct HomSpace: codomain has incompatible left and right units")) + return nothing + end + + # codomain and domain are non-empty + # just need to check coupled charge compatibility + VcodL, VdomL = codomain[1], domain[1] + _leftunit(VcodL) == _leftunit(VdomL) || + throw(ArgumentError("cannot construct HomSpace: codomain and domain have incompatible left units")) + + VcodR, VdomR = codomain[N₁], domain[N₂] + _rightunit(VcodR) == _rightunit(VdomR) || + throw(ArgumentError("cannot construct HomSpace: codomain and domain have incompatible right units")) + return nothing end function HomSpace(codomain::S, domain::CompositeSpace{S}) where {S <: ElementarySpace} From e59dc013fe16eda60a611af5c8ae1e579e51d103 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 2 Sep 2026 16:58:10 +0200 Subject: [PATCH 06/19] update tests on new unitspace behavior + new tests on color checks --- test/setup.jl | 1 + test/symmetries/spaces.jl | 42 ++++++++++++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/test/setup.jl b/test/setup.jl index 9c8244dab..2030228a5 100644 --- a/test/setup.jl +++ b/test/setup.jl @@ -7,6 +7,7 @@ export random_fusion export sectorlist, fast_sectorlist # export dim_isapprox export default_spacelist, factorization_spacelist, ad_spacelist +export VIBM, VIBMRepA4 export test_ad_rrule export _isunitary, _isone diff --git a/test/symmetries/spaces.jl b/test/symmetries/spaces.jl index c730bc5ae..cc7ea4dfb 100644 --- a/test/symmetries/spaces.jl +++ b/test/symmetries/spaces.jl @@ -233,12 +233,13 @@ end @test_throws ArgumentError("Sector $(randunit) appears multiple times") GradedSpace(randunit => 1, randunit => 3) @test isunitspace(W) - @test @constinferred(unitspace(V)) == W == unitspace(typeof(V)) if UnitStyle(I) isa SimpleUnit + @test @constinferred(unitspace(V)) == W == unitspace(typeof(V)) @test @constinferred(leftunitspace(V)) == W == @constinferred(rightunitspace(V)) else @test_throws ArgumentError leftunitspace(V) @test_throws ArgumentError rightunitspace(V) + @test_throws ArgumentError unitspace(V) end @test eval_show(W) == W @test isa(V, VectorSpace) @@ -261,8 +262,10 @@ end @test @constinferred(⊕(V, zerospace(V))) == V @test @constinferred(⊕(V, V)) == Vect[I](c => 2dim(V, c) for c in sectors(V)) @test @constinferred(⊕(V, V, V, V)) == Vect[I](c => 4dim(V, c) for c in sectors(V)) - @test @constinferred(⊕(V, unitspace(V))) == Vect[I](c => isunit(c) + dim(V, c) for c in sectors(V)) - @test @constinferred(fuse(V, unitspace(V))) == V + if UnitStyle(I) isa SimpleUnit + @test @constinferred(⊕(V, unitspace(V))) == Vect[I](c => isunit(c) + dim(V, c) for c in sectors(V)) + @test @constinferred(fuse(V, unitspace(V))) == V + end d = Dict{I, Int}() for a in sectors(V), b in sectors(V) for c in a ⊗ b @@ -470,8 +473,9 @@ end @test @constinferred(insertrightunit(one(V1) ← V1, 0)) == (unitspace(V1) ← V1) @test_throws BoundsError insertleftunit(one(V1) ← V1, 0) else - @test_throws ArgumentError insertrightunit(one(V1) ← V1, 0) - @test_throws ArgumentError insertleftunit(one(V1) ← V1, 0) + errmsg = "cannot insert a sensible unit space in the empty product space" + @test_throws ArgumentError(errmsg) insertrightunit(one(V1) ← V1, 0) + @test_throws ArgumentError(errmsg) insertleftunit(one(V1) ← V1, 0) end @test (V1 ⊗ V2 ← V1 ⊗ V2) == @constinferred TensorKit.compose(W, W') @test W == @constinferred permute(W, ((1, 2), (3, 4, 5))) @@ -479,6 +483,34 @@ end end end +@timedtestset "ProductSpace and HomSpace: GenericUnit coloring" for V in (VIBM, VIBMRepA4) + @test UnitStyle(sectortype(V[1])) isa GenericUnit + V1, V2, V3, V4, V5 = V + + @test @constinferred(one(V1)) == ProductSpace{typeof(V1)}(()) + + @test rightunitspace(V1) == leftunitspace(V2) + P1 = @constinferred ProductSpace(V1, V2) + @test @constinferred(⊗(V1, V2)) == P1 + + @test rightunitspace(V2) != leftunitspace(V1) + @test_throws ArgumentError (⊗(V2, V1)) + + @test rightunitspace(V3) == leftunitspace(V4) + @test rightunitspace(V4) == leftunitspace(V5) + P2 = @constinferred ProductSpace(V3, V4, V5) + + @test leftunitspace(P1[1]) == rightunitspace(dual(P2[length(P2)])) + @test HomSpace(P1, P2') isa HomSpace + @test_throws ArgumentError P1 ← P2 + + @test (V1 ← one(V1)) isa HomSpace + @test (one(V1) ← one(V1)) isa HomSpace + + @test leftunitspace(V2) != rightunitspace(V2) + @test_throws ArgumentError V2 ← one(V2) +end + @timedtestset "show and friends" begin V = U1Space(i => 1 for i in 1:3) @test string(V) == "$(type_repr(typeof(V)))(1 => 1, 2 => 1, 3 => 1)" From 3ba8b4395a5814051ab5cae4dfdeae31cf8d79eb Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Sun, 6 Sep 2026 16:27:27 +0200 Subject: [PATCH 07/19] deal with 0-dimensional product spaces + single product spaces for free --- src/spaces/homspace.jl | 2 ++ src/spaces/productspace.jl | 5 ++++- test/symmetries/spaces.jl | 6 ++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/spaces/homspace.jl b/src/spaces/homspace.jl index 60bba80fa..ee540aad5 100644 --- a/src/spaces/homspace.jl +++ b/src/spaces/homspace.jl @@ -22,6 +22,8 @@ function _check_unit_compatibility(codomain::ProductSpace{S}, domain::ProductSpa UnitStyle(sectortype(S)) isa GenericUnit || return nothing N₁, N₂ = length(codomain), length(domain) N₁ == N₂ == 0 && return nothing # one() ← one() + any(V -> isempty(sectors(V)), codomain) && return nothing # zero spaces -> ignore color check + any(V -> isempty(sectors(V)), domain) && return nothing # product spaces themselves already check that their factors are compatible if N₁ == 0 # codomain is empty, domain is non-empty diff --git a/src/spaces/productspace.jl b/src/spaces/productspace.jl index 3a76b4067..610476e9d 100644 --- a/src/spaces/productspace.jl +++ b/src/spaces/productspace.jl @@ -17,9 +17,12 @@ end function _check_unit_compatibility(::Type{S}, spaces::NTuple{N, S}) where {N, S <: ElementarySpace} UnitStyle(sectortype(S)) isa GenericUnit || return nothing N == 0 && return nothing + any(V -> isempty(sectors(V)), spaces) && return nothing # zero spaces -> ignore color check + rightunits = map(_rightunit, spaces) + leftunits = map(_leftunit, spaces) @inbounds for i in 1:(N - 1) - _rightunit(spaces[i]) == _leftunit(spaces[i + 1]) || + rightunits[i] == leftunits[i + 1] || throw(ArgumentError(lazy"spaces $(i) and $(i + 1) have incompatible coloring")) end return nothing diff --git a/test/symmetries/spaces.jl b/test/symmetries/spaces.jl index cc7ea4dfb..9f008dc4e 100644 --- a/test/symmetries/spaces.jl +++ b/test/symmetries/spaces.jl @@ -509,6 +509,12 @@ end @test leftunitspace(V2) != rightunitspace(V2) @test_throws ArgumentError V2 ← one(V2) + + V0 = zerospace(V1) + @test dim(@constinferred(⊗(V1, V2, V0))) == 0 + @test dim(@constinferred(⊗(V2, V1, V0))) == 0 # bad coloring, but zero-dimensional space + @test dim(@constinferred(ProductSpace(V1, V0, V3))) == 0 + @test (ProductSpace(V1, V0, V3) ← V3) isa HomSpace end @timedtestset "show and friends" begin From 67262a079bcbe45c8983ac2d194bc15936defc3d Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Sun, 6 Sep 2026 16:45:11 +0200 Subject: [PATCH 08/19] dual, better error messages --- src/spaces/homspace.jl | 8 ++++---- src/spaces/productspace.jl | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/spaces/homspace.jl b/src/spaces/homspace.jl index ee540aad5..a16a9bdbc 100644 --- a/src/spaces/homspace.jl +++ b/src/spaces/homspace.jl @@ -29,12 +29,12 @@ function _check_unit_compatibility(codomain::ProductSpace{S}, domain::ProductSpa if N₁ == 0 # codomain is empty, domain is non-empty VdomL, VdomR = domain[1], domain[N₂] _leftunit(VdomL) == _rightunit(VdomR) || - throw(ArgumentError("cannot construct HomSpace: domain has incompatible left and right units")) + throw(SpaceMismatch(lazy"domain $domain has incompatible left and right units")) return nothing elseif N₂ == 0 # domain is empty, codomain is non-empty VcodL, VcodR = codomain[1], codomain[N₁] _leftunit(VcodL) == _rightunit(VcodR) || - throw(ArgumentError("cannot construct HomSpace: codomain has incompatible left and right units")) + throw(SpaceMismatch(lazy"codomain $codomain has incompatible left and right units")) return nothing end @@ -42,11 +42,11 @@ function _check_unit_compatibility(codomain::ProductSpace{S}, domain::ProductSpa # just need to check coupled charge compatibility VcodL, VdomL = codomain[1], domain[1] _leftunit(VcodL) == _leftunit(VdomL) || - throw(ArgumentError("cannot construct HomSpace: codomain and domain have incompatible left units")) + throw(SpaceMismatch(lazy"HomSpace $codomain ← $domain has incompatible left units")) VcodR, VdomR = codomain[N₁], domain[N₂] _rightunit(VcodR) == _rightunit(VdomR) || - throw(ArgumentError("cannot construct HomSpace: codomain and domain have incompatible right units")) + throw(SpaceMismatch(lazy"HomSpace $codomain ← $domain has incompatible right units")) return nothing end diff --git a/src/spaces/productspace.jl b/src/spaces/productspace.jl index 610476e9d..ea3ed9e24 100644 --- a/src/spaces/productspace.jl +++ b/src/spaces/productspace.jl @@ -23,7 +23,7 @@ function _check_unit_compatibility(::Type{S}, spaces::NTuple{N, S}) where {N, S leftunits = map(_leftunit, spaces) @inbounds for i in 1:(N - 1) rightunits[i] == leftunits[i + 1] || - throw(ArgumentError(lazy"spaces $(i) and $(i + 1) have incompatible coloring")) + throw(SpaceMismatch(lazy"$(spaces[i]) and $(spaces[i + 1]) have incompatible coloring")) end return nothing end @@ -82,7 +82,7 @@ Base.axes(P::ProductSpace) = map(axes, P) Base.axes(P::ProductSpace, n::Int) = axes(P[n]) dual(P::ProductSpace{<:ElementarySpace, 0}) = P -dual(P::ProductSpace) = ProductSpace(map(dual, reverse(P.spaces))) +dual(P::ProductSpace) = ProductSpace(reverse(map(dual, P))) Base.conj(P::ProductSpace{<:ElementarySpace, 0}) = P Base.conj(P::ProductSpace) = ProductSpace(map(conj, P)) From 4721ea16511536e54cc4b4cb468a9a14a8878269 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Sun, 6 Sep 2026 17:20:22 +0200 Subject: [PATCH 09/19] update tests --- test/symmetries/spaces.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/symmetries/spaces.jl b/test/symmetries/spaces.jl index 9f008dc4e..c56233f7f 100644 --- a/test/symmetries/spaces.jl +++ b/test/symmetries/spaces.jl @@ -494,7 +494,7 @@ end @test @constinferred(⊗(V1, V2)) == P1 @test rightunitspace(V2) != leftunitspace(V1) - @test_throws ArgumentError (⊗(V2, V1)) + @test_throws SpaceMismatch (⊗(V2, V1)) @test rightunitspace(V3) == leftunitspace(V4) @test rightunitspace(V4) == leftunitspace(V5) @@ -502,13 +502,16 @@ end @test leftunitspace(P1[1]) == rightunitspace(dual(P2[length(P2)])) @test HomSpace(P1, P2') isa HomSpace - @test_throws ArgumentError P1 ← P2 + @test_throws SpaceMismatch P1 ← P2 @test (V1 ← one(V1)) isa HomSpace @test (one(V1) ← one(V1)) isa HomSpace @test leftunitspace(V2) != rightunitspace(V2) - @test_throws ArgumentError V2 ← one(V2) + @test_throws SpaceMismatch V2 ← one(V2) + + Vbad = typeof(V1)(first(sectors(V1)) => 1, first(sectors(V3)) => 1) + @test_throws ArgumentError ProductSpace(Vbad) V0 = zerospace(V1) @test dim(@constinferred(⊗(V1, V2, V0))) == 0 From 0f583d53ded770bee7e3c6949dfd15e4fc466e50 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Sun, 6 Sep 2026 17:20:49 +0200 Subject: [PATCH 10/19] update changelog --- docs/src/Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/Changelog.md b/docs/src/Changelog.md index 74a238fc2..d828e0ed2 100644 --- a/docs/src/Changelog.md +++ b/docs/src/Changelog.md @@ -23,6 +23,7 @@ When releasing a new version, move the "Unreleased" changes to a new version sec ### Added ### Changed +- For sector types with `GenericUnit` such that colorings are not unique, `HomSpace`s and `ProductSpace`s 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 From da6d8b1ec0edb810bcd28846276f87b38949affb Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Mon, 7 Sep 2026 16:30:32 +0200 Subject: [PATCH 11/19] fix the easy tests to fix --- test/chainrules/linalg.jl | 2 +- test/enzyme-vi-to/add.jl | 2 +- test/enzyme-vi-to/inner.jl | 2 +- test/enzyme-vi-to/scale.jl | 2 +- test/tensors/linalg.jl | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/chainrules/linalg.jl b/test/chainrules/linalg.jl index 19990cca1..cd3da8b2a 100644 --- a/test/chainrules/linalg.jl +++ b/test/chainrules/linalg.jl @@ -155,7 +155,7 @@ for V in spacelist test_rrule(inv, E; atol, rtol) end - A = randn(T, V[1] ⊗ V[2] ← V[3] ⊗ V[4] ⊗ V[5]) + A = randn(T, V[1] ⊗ V[2] ← (V[3] ⊗ V[4] ⊗ V[5])') test_rrule(LinearAlgebra.adjoint, A; atol, rtol) test_rrule(LinearAlgebra.norm, A, 2; atol, rtol) diff --git a/test/enzyme-vi-to/add.jl b/test/enzyme-vi-to/add.jl index aa4ba4524..4f305267c 100644 --- a/test/enzyme-vi-to/add.jl +++ b/test/enzyme-vi-to/add.jl @@ -20,7 +20,7 @@ fTβs = is_ci ? (Duplicated,) : (Duplicated, Const) α = randn(T) β = randn(T) - CV = V[1] ⊗ V[2] ← V[3] ⊗ V[4] ⊗ V[5] + CV = V[1] ⊗ V[2] ← (V[3] ⊗ V[4] ⊗ V[5])' C = randn(T, CV) A = randn(T, CV) for TC in (Duplicated,), TA in (Duplicated,) diff --git a/test/enzyme-vi-to/inner.jl b/test/enzyme-vi-to/inner.jl index d3d43a6eb..d610bfdfa 100644 --- a/test/enzyme-vi-to/inner.jl +++ b/test/enzyme-vi-to/inner.jl @@ -17,7 +17,7 @@ fTs = is_ci ? (Duplicated,) : (Duplicated, Const) @testset for TC in (Duplicated,), TA in (Duplicated,), f in (identity, adjoint) atol = default_tol(T) rtol = default_tol(T) - CV = V[1] ⊗ V[2] ← V[3] ⊗ V[4] ⊗ V[5] + CV = V[1] ⊗ V[2] ← (V[3] ⊗ V[4] ⊗ V[5])' C = randn(T, CV) A = randn(T, CV) for RT in rTs diff --git a/test/enzyme-vi-to/scale.jl b/test/enzyme-vi-to/scale.jl index 44508612a..1ae5b783a 100644 --- a/test/enzyme-vi-to/scale.jl +++ b/test/enzyme-vi-to/scale.jl @@ -17,7 +17,7 @@ fTαs = is_ci ? (Duplicated,) : (Duplicated, Const) atol = default_tol(T) rtol = default_tol(T) α = randn(T) - CV = V[1] ⊗ V[2] ← V[3] ⊗ V[4] ⊗ V[5] + CV = V[1] ⊗ V[2] ← (V[3] ⊗ V[4] ⊗ V[5])' C = randn(T, CV) A = randn(T, CV) @testset for TC in (Duplicated,) diff --git a/test/tensors/linalg.jl b/test/tensors/linalg.jl index 738e85939..4f3da6c24 100644 --- a/test/tensors/linalg.jl +++ b/test/tensors/linalg.jl @@ -53,7 +53,7 @@ for V in spacelist @test dot(t2, t) ≈ conj(dot(t2', t')) @test dot(t2, t) ≈ dot(t', t2') - if UnitStyle(I) isa SimpleUnit || !isempty(blocksectors(V2 ⊗ V1)) + if UnitStyle(I) isa SimpleUnit i1 = @constinferred(isomorphism(T, V1 ⊗ V2, V2 ⊗ V1)) # can't reverse fusion here when modules are involved i2 = @constinferred(isomorphism(Vector{T}, V2 ⊗ V1, V1 ⊗ V2)) @test i1 * i2 == @constinferred(id(T, V1 ⊗ V2)) From c45237293a8afa6cbfb25967aaae4ac20a4a4486 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 8 Sep 2026 10:38:56 +0200 Subject: [PATCH 12/19] don't pass intermediate spaces to the inner constructors --- src/spaces/homspace.jl | 26 +++++++++++++++++++++++++- src/tensors/tensoroperations.jl | 6 +++--- test/symmetries/spaces.jl | 2 +- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/spaces/homspace.jl b/src/spaces/homspace.jl index a16a9bdbc..0330449d7 100644 --- a/src/spaces/homspace.jl +++ b/src/spaces/homspace.jl @@ -309,6 +309,30 @@ function compose(W::HomSpace{S}, V::HomSpace{S}) where {S} return HomSpace(codomain(W), domain(V)) end +# workaround to permuting after composing intermediate spaces without constructing the latter +function _contractedspace( + A::HomSpace{S}, (oindA, cindA)::Index2Tuple, + B::HomSpace{S}, (cindB, oindB)::Index2Tuple, + (p₁, p₂)::Index2Tuple{N₁, N₂} + ) where {S, N₁, N₂} + NA = length(oindA) + + Acind = map(n -> dual(A[n]), cindA) + Bcind = map(n -> B[n], cindB) + Acind == Bcind || throw(SpaceMismatch(lazy"$(Acind) ≠ $(Bcind)")) + + # the "open" leg at virtual position n, without ever materializing compose(...) + getopen(n) = n <= NA ? A[oindA[n]] : B[oindB[n - NA]] + @show p₁, p₂ + @show oindA, cindA, oindB, cindB + @show A B + @show map(getopen, p₁) map(n -> dual(getopen(n)), p₂) + + cod = ProductSpace{S, N₁}(map(getopen, p₁)) + dom = ProductSpace{S, N₂}(map(n -> dual(getopen(n)), p₂)) + return cod ← dom +end + function TensorOperations.tensorcontract( A::HomSpace, pA::Index2Tuple, conjA::Bool, B::HomSpace, pB::Index2Tuple, conjB::Bool, @@ -329,7 +353,7 @@ function TensorOperations.tensorcontract( pB′ = adjointtensorindices(B, pB) TensorOperations.tensorcontract(A, pA, false, B′, pB′, false, pAB) else - return permute(compose(permute(A, pA), permute(B, pB)), pAB) + _contractedspace(A, pA, B, pB, pAB) end end diff --git a/src/tensors/tensoroperations.jl b/src/tensors/tensoroperations.jl index 147d27b16..8d3054ff1 100644 --- a/src/tensors/tensoroperations.jl +++ b/src/tensors/tensoroperations.jl @@ -176,9 +176,9 @@ function TO.tensorcontract_structure( B::AbstractTensorMap, pB::Index2Tuple, conjB::Bool, pAB::Index2Tuple{N₁, N₂} ) where {N₁, N₂} - sA = TO.tensoradd_structure(A, pA, conjA) - sB = TO.tensoradd_structure(B, pB, conjB) - return permute(compose(sA, sB), pAB) + VA, pA′ = conjA ? (space(A)', adjointtensorindices(A, pA)) : (space(A), pA) + VB, pB′ = conjB ? (space(B)', adjointtensorindices(B, pB)) : (space(B), pB) + return _contractedspace(VA, pA′, VB, pB′, pAB) end function TO.checkcontractible( diff --git a/test/symmetries/spaces.jl b/test/symmetries/spaces.jl index c56233f7f..e5a30d32f 100644 --- a/test/symmetries/spaces.jl +++ b/test/symmetries/spaces.jl @@ -483,7 +483,7 @@ end end end -@timedtestset "ProductSpace and HomSpace: GenericUnit coloring" for V in (VIBM, VIBMRepA4) +@timedtestset "ProductSpace and HomSpace: GenericUnit coloring $(sectortype(V[1]))" for V in (VIBM, VIBMRepA4) @test UnitStyle(sectortype(V[1])) isa GenericUnit V1, V2, V3, V4, V5 = V From bf97eb5665c212ee3af8ea33de33afe692906c94 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 8 Sep 2026 10:41:10 +0200 Subject: [PATCH 13/19] remove debugging code --- src/spaces/homspace.jl | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/spaces/homspace.jl b/src/spaces/homspace.jl index 0330449d7..0bf313f2e 100644 --- a/src/spaces/homspace.jl +++ b/src/spaces/homspace.jl @@ -321,12 +321,7 @@ function _contractedspace( Bcind = map(n -> B[n], cindB) Acind == Bcind || throw(SpaceMismatch(lazy"$(Acind) ≠ $(Bcind)")) - # the "open" leg at virtual position n, without ever materializing compose(...) getopen(n) = n <= NA ? A[oindA[n]] : B[oindB[n - NA]] - @show p₁, p₂ - @show oindA, cindA, oindB, cindB - @show A B - @show map(getopen, p₁) map(n -> dual(getopen(n)), p₂) cod = ProductSpace{S, N₁}(map(getopen, p₁)) dom = ProductSpace{S, N₂}(map(n -> dual(getopen(n)), p₂)) From 871cc58c583ed30b0625883a7321babf4355aa3c Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 8 Sep 2026 15:48:51 +0200 Subject: [PATCH 14/19] check length instead of unitstyle in unitspace --- src/spaces/gradedspace.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index ab728c4eb..90e6502d3 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -122,7 +122,7 @@ function flip(V::GradedSpace{I}) where {I <: Sector} end function unitspace(S::Type{<:GradedSpace{I}}) where {I <: Sector} - UnitStyle(I) isa GenericUnit && + length(allunits(I)) > 1 && throw(ArgumentError("Cannot construct unit space for sector types with semisimple unit structure.")) return S(unit => 1 for unit in allunits(I)) end From 714cf05855838cc6418dcdd145c307fbb2be7361 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Wed, 9 Sep 2026 15:35:45 -0400 Subject: [PATCH 15/19] Revert "check length instead of unitstyle in unitspace" This reverts commit 871cc58c583ed30b0625883a7321babf4355aa3c. --- src/spaces/gradedspace.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index 90e6502d3..ab728c4eb 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -122,7 +122,7 @@ function flip(V::GradedSpace{I}) where {I <: Sector} end function unitspace(S::Type{<:GradedSpace{I}}) where {I <: Sector} - length(allunits(I)) > 1 && + UnitStyle(I) isa GenericUnit && throw(ArgumentError("Cannot construct unit space for sector types with semisimple unit structure.")) return S(unit => 1 for unit in allunits(I)) end From 61c7337d53ca3d21e5d509afff7dfe45611ae133 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Wed, 9 Sep 2026 16:51:27 -0400 Subject: [PATCH 16/19] handle more edge cases --- src/spaces/homspace.jl | 41 ++++++++++++++++---------------------- src/spaces/productspace.jl | 24 ++++++++++++---------- src/spaces/vectorspaces.jl | 36 +++++++++++++++++++++++++-------- test/symmetries/spaces.jl | 16 +++++++++------ 4 files changed, 68 insertions(+), 49 deletions(-) diff --git a/src/spaces/homspace.jl b/src/spaces/homspace.jl index 0bf313f2e..25aaa1c1c 100644 --- a/src/spaces/homspace.jl +++ b/src/spaces/homspace.jl @@ -18,35 +18,28 @@ function HomSpace(codomain::P1, domain::P2) where {S, P1 <: CompositeSpace{S}, P return HomSpace{S, P1, P2}(codomain, domain) end -function _check_unit_compatibility(codomain::ProductSpace{S}, domain::ProductSpace{S}) where {S <: ElementarySpace} +# check that the legs form a closed cycle of composable spaces: +# codomain[1] … codomain[N₁], dual(domain[N₂]) … dual(domain[1]). +function _check_unit_compatibility( + codomain::CompositeSpace{S}, domain::CompositeSpace{S} + ) where {S <: ElementarySpace} UnitStyle(sectortype(S)) isa GenericUnit || return nothing N₁, N₂ = length(codomain), length(domain) - N₁ == N₂ == 0 && return nothing # one() ← one() - any(V -> isempty(sectors(V)), codomain) && return nothing # zero spaces -> ignore color check - any(V -> isempty(sectors(V)), domain) && return nothing - - # product spaces themselves already check that their factors are compatible - if N₁ == 0 # codomain is empty, domain is non-empty - VdomL, VdomR = domain[1], domain[N₂] - _leftunit(VdomL) == _rightunit(VdomR) || - throw(SpaceMismatch(lazy"domain $domain has incompatible left and right units")) + + if N₁ == 0 && N₂ == 0 # one() ← one(): empty cycle return nothing - elseif N₂ == 0 # domain is empty, codomain is non-empty - VcodL, VcodR = codomain[1], codomain[N₁] - _leftunit(VcodL) == _rightunit(VcodR) || + elseif N₁ == 0 # the domain segment closes onto itself + _matchunits(_leftunitof(_units(domain[1])), _rightunitof(_units(domain[N₂]))) || + throw(SpaceMismatch(lazy"domain $domain has incompatible left and right units")) + elseif N₂ == 0 # the codomain segment closes onto itself + _matchunits(_leftunitof(_units(codomain[1])), _rightunitof(_units(codomain[N₁]))) || throw(SpaceMismatch(lazy"codomain $codomain has incompatible left and right units")) - return nothing + else + _matchunits(_rightunitof(_units(codomain[N₁])), _rightunitof(_units(domain[N₂]))) || + throw(SpaceMismatch(lazy"HomSpace $codomain ← $domain has incompatible right units")) + _matchunits(_leftunitof(_units(codomain[1])), _leftunitof(_units(domain[1]))) || + throw(SpaceMismatch(lazy"HomSpace $codomain ← $domain has incompatible left units")) end - - # codomain and domain are non-empty - # just need to check coupled charge compatibility - VcodL, VdomL = codomain[1], domain[1] - _leftunit(VcodL) == _leftunit(VdomL) || - throw(SpaceMismatch(lazy"HomSpace $codomain ← $domain has incompatible left units")) - - VcodR, VdomR = codomain[N₁], domain[N₂] - _rightunit(VcodR) == _rightunit(VdomR) || - throw(SpaceMismatch(lazy"HomSpace $codomain ← $domain has incompatible right units")) return nothing end diff --git a/src/spaces/productspace.jl b/src/spaces/productspace.jl index ea3ed9e24..75ecb196d 100644 --- a/src/spaces/productspace.jl +++ b/src/spaces/productspace.jl @@ -8,22 +8,24 @@ Only tensor products between [`ElementarySpace`](@ref) objects of the same type struct ProductSpace{S <: ElementarySpace, N} <: CompositeSpace{S} spaces::NTuple{N, S} function ProductSpace{S, N}(spaces::NTuple{N, S}) where {S <: ElementarySpace, N} - _check_unit_compatibility(S, spaces) + _check_unit_compatibility(spaces) return new{S, N}(spaces) end end -# check that every product of elementary spaces is compatible color-wise -function _check_unit_compatibility(::Type{S}, spaces::NTuple{N, S}) where {N, S <: ElementarySpace} - UnitStyle(sectortype(S)) isa GenericUnit || return nothing +# check that the factors form an open chain of composable spaces +function _check_unit_compatibility(spaces::Tuple{Vararg{ElementarySpace}}) + N = length(spaces) N == 0 && return nothing - any(V -> isempty(sectors(V)), spaces) && return nothing # zero spaces -> ignore color check - - rightunits = map(_rightunit, spaces) - leftunits = map(_leftunit, spaces) - @inbounds for i in 1:(N - 1) - rightunits[i] == leftunits[i + 1] || - throw(SpaceMismatch(lazy"$(spaces[i]) and $(spaces[i + 1]) have incompatible coloring")) + UnitStyle(sectortype(first(spaces))) isa GenericUnit || return nothing + if N == 1 # no junctions to check, but still validate the single factor + _units(spaces[1]) + return nothing + end + @inbounds for i in 2:N + Vprev, V = spaces[i - 1], spaces[i] + _matchunits(_rightunitof(_units(Vprev)), _leftunitof(_units(V))) || + throw(SpaceMismatch(lazy"$Vprev and $V have incompatible coloring")) end return nothing end diff --git a/src/spaces/vectorspaces.jl b/src/spaces/vectorspaces.jl index 20b5ca4eb..dddfecbed 100644 --- a/src/spaces/vectorspaces.jl +++ b/src/spaces/vectorspaces.jl @@ -174,10 +174,9 @@ function leftunitspace(V::ElementarySpace) end function _leftunit(V::ElementarySpace) - !isempty(sectors(V)) || throw(ArgumentError("Cannot determine the left unit of an empty space")) - _allequal(leftunit, sectors(V)) || - throw(ArgumentError(lazy"sectors of $V do not have the same left unit")) - return leftunit(first(sectors(V))) + u = _units(V) + isnothing(u) && throw(ArgumentError("Cannot determine the left unit of an empty space")) + return _leftunitof(u) end """ @@ -198,12 +197,33 @@ function rightunitspace(V::ElementarySpace) end function _rightunit(V::ElementarySpace) - !isempty(sectors(V)) || throw(ArgumentError("Cannot determine the right unit of an empty space")) - _allequal(rightunit, sectors(V)) || - throw(ArgumentError(lazy"sectors of $V do not have the same right unit")) - return rightunit(first(sectors(V))) + u = _units(V) + isnothing(u) && throw(ArgumentError("Cannot determine the right unit of an empty space")) + return _rightunitof(u) end +# Return the `(leftunit, rightunit)` shared by all sectors of `V`, or `nothing` for the zero +# space, whose coloring is unconstrained and thus acts as a wildcard in unit checks. +function _units(V::ElementarySpace) + s = sectors(V) + isempty(s) && return nothing + l, r = leftunit(first(s)), rightunit(first(s)) + all(c -> leftunit(c) == l && rightunit(c) == r, s) || + throw(SpaceMismatch(lazy"sectors of $V do not share a single left and right unit")) + return (l, r) +end + +_leftunitof(u::Tuple{I, I}) where {I <: Sector} = u[1] +_rightunitof(u::Tuple{I, I}) where {I <: Sector} = u[2] +_leftunitof(::Nothing) = nothing +_rightunitof(::Nothing) = nothing + +# `nothing` acts as a wildcard, being compatible with any coloring +_matchunits(::Nothing, ::Nothing) = true +_matchunits(::Nothing, ::Sector) = true +_matchunits(::Sector, ::Nothing) = true +_matchunits(u₁::Sector, u₂::Sector) = u₁ == u₂ + """ isunitspace(V::S) where {S <: ElementarySpace} -> Bool diff --git a/test/symmetries/spaces.jl b/test/symmetries/spaces.jl index e5a30d32f..4957b1140 100644 --- a/test/symmetries/spaces.jl +++ b/test/symmetries/spaces.jl @@ -237,8 +237,8 @@ end @test @constinferred(unitspace(V)) == W == unitspace(typeof(V)) @test @constinferred(leftunitspace(V)) == W == @constinferred(rightunitspace(V)) else - @test_throws ArgumentError leftunitspace(V) - @test_throws ArgumentError rightunitspace(V) + @test_throws SpaceMismatch leftunitspace(V) + @test_throws SpaceMismatch rightunitspace(V) @test_throws ArgumentError unitspace(V) end @test eval_show(W) == W @@ -511,13 +511,17 @@ end @test_throws SpaceMismatch V2 ← one(V2) Vbad = typeof(V1)(first(sectors(V1)) => 1, first(sectors(V3)) => 1) - @test_throws ArgumentError ProductSpace(Vbad) + @test_throws SpaceMismatch ProductSpace(Vbad) + # zero spaces are wildcards that suppress only the junctions they touch V0 = zerospace(V1) @test dim(@constinferred(⊗(V1, V2, V0))) == 0 - @test dim(@constinferred(⊗(V2, V1, V0))) == 0 # bad coloring, but zero-dimensional space - @test dim(@constinferred(ProductSpace(V1, V0, V3))) == 0 - @test (ProductSpace(V1, V0, V3) ← V3) isa HomSpace + @test_throws SpaceMismatch (⊗(V2, V1, V0)) # V2 ⊗ V1 is incompatible on its own + @test dim(@constinferred(ProductSpace(V1, V0, V3))) == 0 # V0 breaks the chain + @test_throws SpaceMismatch (ProductSpace(V1, V0, V3) ← V3) # left units of V1 and V3 + @test_throws SpaceMismatch ProductSpace(Vbad, V0) # V0 does not hide a bad factor + @test (ProductSpace(V2, V0) ← one(V2)) isa HomSpace + @test (ProductSpace(V0) ← ProductSpace(V0)) isa HomSpace end @timedtestset "show and friends" begin From 779a7a7bd4461703b7d0b43fc39a46bee168684d Mon Sep 17 00:00:00 2001 From: lkdvos Date: Thu, 10 Sep 2026 09:08:17 -0400 Subject: [PATCH 17/19] simplify implementation and test gradedspace color homogeneity --- docs/src/Changelog.md | 2 +- src/spaces/gradedspace.jl | 11 ++++++++ src/spaces/homspace.jl | 8 +++--- src/spaces/productspace.jl | 8 ++---- src/spaces/vectorspaces.jl | 51 +++++++++++++------------------------- 5 files changed, 35 insertions(+), 45 deletions(-) diff --git a/docs/src/Changelog.md b/docs/src/Changelog.md index d828e0ed2..9a6b231b9 100644 --- a/docs/src/Changelog.md +++ b/docs/src/Changelog.md @@ -23,7 +23,7 @@ When releasing a new version, move the "Unreleased" changes to a new version sec ### Added ### Changed -- For sector types with `GenericUnit` such that colorings are not unique, `HomSpace`s and `ProductSpace`s 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)) +- 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 diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index ab728c4eb..6e9b2a990 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -29,6 +29,15 @@ struct GradedSpace{I <: Sector, D} <: ElementarySpace end sectortype(::Type{<:GradedSpace{I}}) where {I <: Sector} = I +# 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 GenericUnit) || isempty(sectors)) && return nothing + l, r = leftunit(first(sectors)), rightunit(first(sectors)) + all(c -> leftunit(c) == l && rightunit(c) == r, sectors) || + throw(SpaceMismatch(lazy"sectors $(collect(sectors)) do not share a single left and right unit")) + return nothing +end + function GradedSpace{I, NTuple{N, Int}}(dims; dual::Bool = false) where {I, N} d = ntuple(n -> 0, N) isset = ntuple(n -> false, N) @@ -40,6 +49,7 @@ function GradedSpace{I, NTuple{N, Int}}(dims; dual::Bool = false) where {I, N} isset = TupleTools.setindex(isset, true, i) d = TupleTools.setindex(d, dc, i) 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) end function GradedSpace{I, NTuple{N, Int}}(dims::Pair; dual::Bool = false) where {I, N} @@ -54,6 +64,7 @@ function GradedSpace{I, SectorDict{I, Int}}(dims; dual::Bool = false) where {I < dc < 0 && throw(ArgumentError(lazy"Sector $k has negative dimension $dc")) !iszero(dc) && push!(d, k => dc) end + _check_unit_homogeneity(I, keys(d)) return GradedSpace{I, SectorDict{I, Int}}(d, dual) end function GradedSpace{I, SectorDict{I, Int}}(dims::Pair; dual::Bool = false) where {I <: Sector} diff --git a/src/spaces/homspace.jl b/src/spaces/homspace.jl index 25aaa1c1c..7f3418944 100644 --- a/src/spaces/homspace.jl +++ b/src/spaces/homspace.jl @@ -29,15 +29,15 @@ function _check_unit_compatibility( if N₁ == 0 && N₂ == 0 # one() ← one(): empty cycle return nothing elseif N₁ == 0 # the domain segment closes onto itself - _matchunits(_leftunitof(_units(domain[1])), _rightunitof(_units(domain[N₂]))) || + _matchunits(_leftunit(domain[1]), _rightunit(domain[N₂])) || throw(SpaceMismatch(lazy"domain $domain has incompatible left and right units")) elseif N₂ == 0 # the codomain segment closes onto itself - _matchunits(_leftunitof(_units(codomain[1])), _rightunitof(_units(codomain[N₁]))) || + _matchunits(_leftunit(codomain[1]), _rightunit(codomain[N₁])) || throw(SpaceMismatch(lazy"codomain $codomain has incompatible left and right units")) else - _matchunits(_rightunitof(_units(codomain[N₁])), _rightunitof(_units(domain[N₂]))) || + _matchunits(_rightunit(codomain[N₁]), _rightunit(domain[N₂])) || throw(SpaceMismatch(lazy"HomSpace $codomain ← $domain has incompatible right units")) - _matchunits(_leftunitof(_units(codomain[1])), _leftunitof(_units(domain[1]))) || + _matchunits(_leftunit(codomain[1]), _leftunit(domain[1])) || throw(SpaceMismatch(lazy"HomSpace $codomain ← $domain has incompatible left units")) end return nothing diff --git a/src/spaces/productspace.jl b/src/spaces/productspace.jl index 75ecb196d..f9f5dcff4 100644 --- a/src/spaces/productspace.jl +++ b/src/spaces/productspace.jl @@ -16,15 +16,11 @@ end # check that the factors form an open chain of composable spaces function _check_unit_compatibility(spaces::Tuple{Vararg{ElementarySpace}}) N = length(spaces) - N == 0 && return nothing + N <= 1 && return nothing # no junctions to check UnitStyle(sectortype(first(spaces))) isa GenericUnit || return nothing - if N == 1 # no junctions to check, but still validate the single factor - _units(spaces[1]) - return nothing - end @inbounds for i in 2:N Vprev, V = spaces[i - 1], spaces[i] - _matchunits(_rightunitof(_units(Vprev)), _leftunitof(_units(V))) || + _matchunits(_rightunit(Vprev), _leftunit(V)) || throw(SpaceMismatch(lazy"$Vprev and $V have incompatible coloring")) end return nothing diff --git a/src/spaces/vectorspaces.jl b/src/spaces/vectorspaces.jl index dddfecbed..5a8a2dc74 100644 --- a/src/spaces/vectorspaces.jl +++ b/src/spaces/vectorspaces.jl @@ -166,17 +166,10 @@ in the vector space. """ function leftunitspace(V::ElementarySpace) I = sectortype(V) - if UnitStyle(I) isa SimpleUnit - return unitspace(typeof(V)) - else - return spacetype(V)(_leftunit(V) => 1) - end -end - -function _leftunit(V::ElementarySpace) - u = _units(V) + UnitStyle(I) isa SimpleUnit && return unitspace(typeof(V)) + u = _leftunit(V) isnothing(u) && throw(ArgumentError("Cannot determine the left unit of an empty space")) - return _leftunitof(u) + return spacetype(V)(u => 1) end """ @@ -189,34 +182,24 @@ in the vector space. """ function rightunitspace(V::ElementarySpace) I = sectortype(V) - if UnitStyle(I) isa SimpleUnit - return unitspace(typeof(V)) - else - return spacetype(V)(_rightunit(V) => 1) - end -end - -function _rightunit(V::ElementarySpace) - u = _units(V) + UnitStyle(I) isa SimpleUnit && return unitspace(typeof(V)) + u = _rightunit(V) isnothing(u) && throw(ArgumentError("Cannot determine the right unit of an empty space")) - return _rightunitof(u) + return spacetype(V)(u => 1) end -# Return the `(leftunit, rightunit)` shared by all sectors of `V`, or `nothing` for the zero -# space, whose coloring is unconstrained and thus acts as a wildcard in unit checks. -function _units(V::ElementarySpace) +# Return the `(leftunit, rightunit)` of `V`, or `(nothing, nothing)` for the zero space, +# whose coloring is unconstrained and thus acts as a wildcard. Elementary spaces are +# homogeneously colored by construction, so any sector determines both. +function _leftrightunit(V::ElementarySpace) s = sectors(V) - isempty(s) && return nothing - l, r = leftunit(first(s)), rightunit(first(s)) - all(c -> leftunit(c) == l && rightunit(c) == r, s) || - throw(SpaceMismatch(lazy"sectors of $V do not share a single left and right unit")) - return (l, r) + isempty(s) && return (nothing, nothing) + c = first(s) + return (leftunit(c), rightunit(c)) end -_leftunitof(u::Tuple{I, I}) where {I <: Sector} = u[1] -_rightunitof(u::Tuple{I, I}) where {I <: Sector} = u[2] -_leftunitof(::Nothing) = nothing -_rightunitof(::Nothing) = nothing +_leftunit(V::ElementarySpace) = _leftrightunit(V)[1] +_rightunit(V::ElementarySpace) = _leftrightunit(V)[2] # `nothing` acts as a wildcard, being compatible with any coloring _matchunits(::Nothing, ::Nothing) = true @@ -229,8 +212,8 @@ _matchunits(u₁::Sector, u₂::Sector) = u₁ == u₂ Return whether the elementary space `V` is a unit space, i.e. is isomorphic to the trivial one-dimensional space. For vector spaces of type `GradedSpace{I}` where `Sector` `I` has a -semisimple unit structure, this returns `true` if `V` is isomorphic to either the left, right or -semisimple unit space. +semisimple unit structure, this returns `true` if `V` is isomorphic to the left or right unit +space of its coloring. """ function isunitspace(V::ElementarySpace) I = sectortype(V) From e90671436479d4e66e68ffd1d4bf807c88c4e3db Mon Sep 17 00:00:00 2001 From: lkdvos Date: Thu, 10 Sep 2026 09:16:25 -0400 Subject: [PATCH 18/19] adapt tests for new restrictions --- test/symmetries/spaces.jl | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/test/symmetries/spaces.jl b/test/symmetries/spaces.jl index 4957b1140..7707b3149 100644 --- a/test/symmetries/spaces.jl +++ b/test/symmetries/spaces.jl @@ -188,12 +188,18 @@ end end @timedtestset "ElementarySpace: $(type_repr(Vect[I]))" for I in sectorlist + u = rand(collect(allunits(I))) if Base.IteratorSize(values(I)) === Base.IsInfinite() set = unique(vcat(allunits(I)..., [randsector(I) for k in 1:10])) - gen = (c => 2 for c in set) else - gen = (values(I)[k] => (k + 1) for k in 1:length(values(I))) + set = values(I) end + if UnitStyle(I) isa GenericUnit + # elementary spaces are homogeneously colored, so restrict to the component of `u`, + # which is closed under `⊗` and `dual`; `u` goes first so that `dim(V, u) == 2` + set = [u; [c for c in set if c != u && leftunit(c) == u == rightunit(c)]] + end + gen = (set[k] => (k + 1) for k in 1:length(set)) V = GradedSpace(gen) @test eval(Meta.parse(type_repr(typeof(V)))) == typeof(V) @test eval_show(V) == V @@ -223,22 +229,26 @@ end @test eval_show(typeof(V)) == typeof(V) # space with no sectors @test dim(@constinferred(zerospace(V))) == 0 - # space with unit(s), always test as if multifusion - W = @constinferred GradedSpace(unit => 1 for unit in allunits(I)) - dict = Dict(unit => 1 for unit in allunits(I)) - @test W == GradedSpace(dict) - @test W == GradedSpace(push!(dict, randsector(I) => 0)) + # space with the unit of the coloring of V + W = @constinferred leftunitspace(V) + if UnitStyle(I) isa SimpleUnit + @test W == GradedSpace(unit => 1 for unit in allunits(I)) + dict = Dict(unit => 1 for unit in allunits(I)) + @test W == GradedSpace(dict) + @test W == GradedSpace(push!(dict, randsector(I) => 0)) + else + # spanning several units is not homogeneously colored, and thus not a valid space + @test_throws SpaceMismatch GradedSpace(unit => 1 for unit in allunits(I)) + end @test @constinferred(zerospace(V)) == GradedSpace(unit => 0 for unit in allunits(I)) randunit = rand(collect(allunits(I))) @test_throws ArgumentError("Sector $(randunit) appears multiple times") GradedSpace(randunit => 1, randunit => 3) @test isunitspace(W) + @test W == @constinferred(rightunitspace(V)) if UnitStyle(I) isa SimpleUnit @test @constinferred(unitspace(V)) == W == unitspace(typeof(V)) - @test @constinferred(leftunitspace(V)) == W == @constinferred(rightunitspace(V)) else - @test_throws SpaceMismatch leftunitspace(V) - @test_throws SpaceMismatch rightunitspace(V) @test_throws ArgumentError unitspace(V) end @test eval_show(W) == W @@ -282,7 +292,6 @@ end @test V ≺ ⊕(V, V) @test !(V ≻ ⊕(V, V)) - u = first(allunits(I)) @test infimum(V, GradedSpace(u => 3)) == GradedSpace(u => 2) @test_throws SpaceMismatch (⊕(V, V')) end @@ -510,8 +519,8 @@ end @test leftunitspace(V2) != rightunitspace(V2) @test_throws SpaceMismatch V2 ← one(V2) - Vbad = typeof(V1)(first(sectors(V1)) => 1, first(sectors(V3)) => 1) - @test_throws SpaceMismatch ProductSpace(Vbad) + # a space spanning two colorings is rejected on construction + @test_throws SpaceMismatch typeof(V1)(first(sectors(V1)) => 1, first(sectors(V3)) => 1) # zero spaces are wildcards that suppress only the junctions they touch V0 = zerospace(V1) @@ -519,7 +528,6 @@ end @test_throws SpaceMismatch (⊗(V2, V1, V0)) # V2 ⊗ V1 is incompatible on its own @test dim(@constinferred(ProductSpace(V1, V0, V3))) == 0 # V0 breaks the chain @test_throws SpaceMismatch (ProductSpace(V1, V0, V3) ← V3) # left units of V1 and V3 - @test_throws SpaceMismatch ProductSpace(Vbad, V0) # V0 does not hide a bad factor @test (ProductSpace(V2, V0) ← one(V2)) isa HomSpace @test (ProductSpace(V0) ← ProductSpace(V0)) isa HomSpace end From 5a5d858de0dec22f7fbef2578a9f019f704ab9dd Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Thu, 10 Sep 2026 12:52:42 -0400 Subject: [PATCH 19/19] Apply batched suggestions from code review Co-authored-by: Jutho --- src/spaces/gradedspace.jl | 2 +- src/spaces/vectorspaces.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index 6e9b2a990..3b9d36dbb 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -31,7 +31,7 @@ sectortype(::Type{<:GradedSpace{I}}) where {I <: Sector} = I # 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 GenericUnit) || isempty(sectors)) && return nothing + (UnitStyle(I) isa SimpleUnit || isempty(sectors)) && return nothing l, r = leftunit(first(sectors)), rightunit(first(sectors)) all(c -> leftunit(c) == l && rightunit(c) == r, sectors) || throw(SpaceMismatch(lazy"sectors $(collect(sectors)) do not share a single left and right unit")) diff --git a/src/spaces/vectorspaces.jl b/src/spaces/vectorspaces.jl index 5a8a2dc74..7ddce0289 100644 --- a/src/spaces/vectorspaces.jl +++ b/src/spaces/vectorspaces.jl @@ -134,7 +134,7 @@ Always returns `false` for spaces where `V == conj(V)`, i.e. vector spaces over Return the corresponding vector space of type `S` that represents the trivial one-dimensional space, i.e. the space that is isomorphic to the corresponding field. -For vector spaces where `I = sectortype(S)` has a semisimple unit structure +For vector spaces where `I = sectortype(S)` has a non-simple unit structure (`UnitStyle(I) == GenericUnit()`), this errors. !!! note