From 81bb7d4f933d4fea476c19ed258e2856b173c1bc Mon Sep 17 00:00:00 2001 From: lkdvos Date: Wed, 23 Sep 2026 11:33:39 -0400 Subject: [PATCH 1/3] Bump version to v0.3.19 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 379300a..ff21a17 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "BlockTensorKit" uuid = "5f87ffc2-9cf1-4a46-8172-465d160bd8cd" -version = "0.3.18" +version = "0.3.19" authors = ["Lukas Devos and contributors"] [deps] From 2e883287462fd1370c111b689d28eb3adf7502d3 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Wed, 23 Sep 2026 13:07:44 -0400 Subject: [PATCH 2/3] Adapt multifusion `SumSpace` to TensorKit v0.17.2 coloring rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TensorKit v0.17.2 (#515) requires every `GradedSpace` to be homogeneously colored, forbids `unitspace` for `GenericUnit` sectors and checks coloring when building `ProductSpace`/`HomSpace`. This broke the multifusion `SumSpace` tests, which flattened heterogeneous sums into a single `GradedSpace`. - `unitspace(::Type{<:SumSpace})` returns one component per simple unit - `leftunitspace`/`rightunitspace` inspect components instead of flattening - `_leftrightunit(::SumSpace)` uses the shared unit, or a wildcard if components differ - tests use `⊞` instead of flattening `⊕`, and expect incompatible products to throw - require TensorKit v0.17.2 Co-Authored-By: Claude Opus 5.5 (1M context) --- Project.toml | 2 +- src/vectorspaces/sumspace.jl | 29 ++++++++++++++++++++++++++--- test/vectorspaces/sumspace.jl | 14 +++++++------- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/Project.toml b/Project.toml index ff21a17..903dfc7 100644 --- a/Project.toml +++ b/Project.toml @@ -36,7 +36,7 @@ MatrixAlgebraKit = "0.6" Random = "1" SafeTestsets = "0.1" Strided = "2.3.3" -TensorKit = "0.17" +TensorKit = "0.17.2" TensorOperations = "5" Test = "1" TestExtras = "0.2, 0.3" diff --git a/src/vectorspaces/sumspace.jl b/src/vectorspaces/sumspace.jl index 8415b62..d57ef21 100644 --- a/src/vectorspaces/sumspace.jl +++ b/src/vectorspaces/sumspace.jl @@ -169,14 +169,37 @@ function TensorKit.fuse(V1::S, V2::S) where {S <: SumSpace} return SumSpace(vec([fuse(v1, v2) for (v1, v2) in Base.product(V1.spaces, V2.spaces)])) end -TensorKit.unitspace(S::Type{<:SumSpace}) = SumSpace(TensorKit.unitspace(eltype(S))) +function TensorKit.unitspace(S::Type{<:SumSpace}) + V = eltype(S) + I = sectortype(V) + TK.UnitStyle(I) isa TK.GenericUnit || return SumSpace(TensorKit.unitspace(V)) + # elementary spaces must be homogeneously colored, so every simple unit gets its own component + return SumSpace([V(u => 1) for u in TK.allunits(I)]) +end TensorKit.zerospace(::Type{SumSpace{S}}) where {S} = SumSpace{S}() +# distinct left (or right) units of the non-empty components, which are each homogeneously colored +_distinctunits(f, S::SumSpace) = unique!([u for u in map(f, S.spaces) if !isnothing(u)]) + function TensorKit.leftunitspace(S::SumSpace) - return SumSpace(leftunitspace(oplus(S))) + TK.UnitStyle(sectortype(S)) isa TK.SimpleUnit && return unitspace(S) + units = _distinctunits(TK._leftunit, S) + isempty(units) && throw(ArgumentError("Cannot determine the left unit of an empty space")) + length(units) == 1 || throw(ArgumentError(lazy"components of $S do not share a single left unit")) + return SumSpace(eltype(S)(only(units) => 1)) end function TensorKit.rightunitspace(S::SumSpace) - return SumSpace(rightunitspace(oplus(S))) + TK.UnitStyle(sectortype(S)) isa TK.SimpleUnit && return unitspace(S) + units = _distinctunits(TK._rightunit, S) + isempty(units) && throw(ArgumentError("Cannot determine the right unit of an empty space")) + length(units) == 1 || throw(ArgumentError(lazy"components of $S do not share a single right unit")) + return SumSpace(eltype(S)(only(units) => 1)) +end +# a unit shared by all components constrains the coloring; otherwise it acts as a wildcard +function TK._leftrightunit(S::SumSpace) + ls = _distinctunits(TK._leftunit, S) + rs = _distinctunits(TK._rightunit, S) + return (length(ls) == 1 ? only(ls) : nothing, length(rs) == 1 ? only(rs) : nothing) end TensorKit.isunitspace(S::SumSpace) = !isempty(S) && all(isunitspace, S.spaces) diff --git a/test/vectorspaces/sumspace.jl b/test/vectorspaces/sumspace.jl index 1c877ba..8226dc9 100644 --- a/test/vectorspaces/sumspace.jl +++ b/test/vectorspaces/sumspace.jl @@ -160,7 +160,6 @@ end using Test, TestExtras using TensorKit: hassector - using BlockTensorKit: ⊕ I = IsingBimodule @@ -186,7 +185,8 @@ end @test @constinferred(dual(V)) == @constinferred(conj(V)) == @constinferred(adjoint(V)) @test field(V) == ℂ - @test unitspace(V) == unitspace(V1) + @test_throws ArgumentError unitspace(V1) + @test unitspace(V) == unitspace(SumSpace(V1)) == ⊞(Vect[I](C0 => 1), Vect[I](D0 => 1)) @test @constinferred(sectortype(V)) == sectortype(V1) @test ((@constinferred sectors(V))...,) == (C0, C1, M, D0, D1) # sorted order @@ -208,7 +208,7 @@ end for W in [WC, WD] @test isunitspace(W) @test W == @constinferred(leftunitspace(W)) == @constinferred(rightunitspace(W)) - @test unitspace(typeof(W)) == ⊞(Vect[IsingBimodule]((1, 1, 0) => 1, (2, 2, 0) => 1)) + @test unitspace(typeof(W)) == ⊞(Vect[I](C0 => 1), Vect[I](D0 => 1)) end @test_throws ArgumentError leftunitspace(V) @@ -217,7 +217,7 @@ end @test rightunitspace(SumSpace(V2, V3)) == WD @test leftunitspace(WMop) == WD && rightunitspace(WMop) == WC @test leftunitspace(WM) == WC && rightunitspace(WM) == WD - @test unitspace(WM) == unitspace(WMop) == ⊞(Vect[IsingBimodule]((1, 1, 0) => 1, (2, 2, 0) => 1)) + @test unitspace(WM) == unitspace(WMop) == ⊞(Vect[I](C0 => 1), Vect[I](D0 => 1)) Wempty = SumSpace(Vect[I]()) Wzero = zerospace(V) @@ -243,8 +243,8 @@ end @test flip(V) ≅ V @test flip(V) ≾ V @test flip(V) ≿ V - @test V ≺ ⊕(V, V) - @test !(V ≻ ⊕(V, V)) + @test V ≺ ⊞(V, V) + @test !(V ≻ ⊞(V, V)) # blocksectors tests @test issetequal(@constinferred(blocksectors(one(V) ← one(V))), (C0, D0)) @@ -254,7 +254,7 @@ end @test issetequal(@constinferred(blocksectors(v^2)), blocksectors(v ← v)) end for v in [WM, WMop] - @test isempty(@constinferred(blocksectors(v^2))) + @test_throws SpaceMismatch v^2 @test issetequal(@constinferred(blocksectors(v ← v)), blocksectors(v)) end end From f24718ebece5649e8de8099c079a95b6079dcd01 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Wed, 23 Sep 2026 15:00:52 -0400 Subject: [PATCH 3/3] Require multifusion SumSpace to be homogeneously colored as a whole The previous fix let leftunitspace/rightunitspace answer for a SumSpace whose components agreed on only one side (left xor right), by checking each side independently and treating disagreement on the other side as a wildcard. That's inconsistent with how a plain GradedSpace works: it is either homogeneously colored (single left AND right unit shared by every sector) or invalid. A SumSpace should follow the same rule as a whole, so leftunitspace/rightunitspace/_leftrightunit again require full agreement on both sides and reject (SpaceMismatch) otherwise, without any special-casing for a partial match. unitspace itself is unaffected by this: TensorKit already forbids it at the type level for GenericUnit sector types, regardless of any given instance's homogeneity, so it throws ArgumentError unconditionally for IsingBimodule-sectored spaces, homogeneous or not. The Multifusion testset is adjusted to expect this throughout, rather than the previous attempt to give unitspace a real answer for such sector types. --- src/vectorspaces/sumspace.jl | 39 +++++++++++++---------------------- test/vectorspaces/sumspace.jl | 36 +++++++++++++++++++------------- 2 files changed, 36 insertions(+), 39 deletions(-) diff --git a/src/vectorspaces/sumspace.jl b/src/vectorspaces/sumspace.jl index d57ef21..1375e69 100644 --- a/src/vectorspaces/sumspace.jl +++ b/src/vectorspaces/sumspace.jl @@ -169,39 +169,28 @@ function TensorKit.fuse(V1::S, V2::S) where {S <: SumSpace} return SumSpace(vec([fuse(v1, v2) for (v1, v2) in Base.product(V1.spaces, V2.spaces)])) end -function TensorKit.unitspace(S::Type{<:SumSpace}) - V = eltype(S) - I = sectortype(V) - TK.UnitStyle(I) isa TK.GenericUnit || return SumSpace(TensorKit.unitspace(V)) - # elementary spaces must be homogeneously colored, so every simple unit gets its own component - return SumSpace([V(u => 1) for u in TK.allunits(I)]) -end +TensorKit.unitspace(S::Type{<:SumSpace}) = SumSpace(TensorKit.unitspace(eltype(S))) TensorKit.zerospace(::Type{SumSpace{S}}) where {S} = SumSpace{S}() -# distinct left (or right) units of the non-empty components, which are each homogeneously colored -_distinctunits(f, S::SumSpace) = unique!([u for u in map(f, S.spaces) if !isnothing(u)]) - function TensorKit.leftunitspace(S::SumSpace) - TK.UnitStyle(sectortype(S)) isa TK.SimpleUnit && return unitspace(S) - units = _distinctunits(TK._leftunit, S) - isempty(units) && throw(ArgumentError("Cannot determine the left unit of an empty space")) - length(units) == 1 || throw(ArgumentError(lazy"components of $S do not share a single left unit")) - return SumSpace(eltype(S)(only(units) => 1)) + return SumSpace(leftunitspace(oplus(S))) end function TensorKit.rightunitspace(S::SumSpace) - TK.UnitStyle(sectortype(S)) isa TK.SimpleUnit && return unitspace(S) - units = _distinctunits(TK._rightunit, S) - isempty(units) && throw(ArgumentError("Cannot determine the right unit of an empty space")) - length(units) == 1 || throw(ArgumentError(lazy"components of $S do not share a single right unit")) - return SumSpace(eltype(S)(only(units) => 1)) + return SumSpace(rightunitspace(oplus(S))) end -# a unit shared by all components constrains the coloring; otherwise it acts as a wildcard +TensorKit.isunitspace(S::SumSpace) = !isempty(S) && all(isunitspace, S.spaces) + +# a `SumSpace` must, as a whole, be homogeneously colored: all of its sectors (across all +# components) share a single left and right unit, exactly like a plain `GradedSpace`. function TK._leftrightunit(S::SumSpace) - ls = _distinctunits(TK._leftunit, S) - rs = _distinctunits(TK._rightunit, S) - return (length(ls) == 1 ? only(ls) : nothing, length(rs) == 1 ? only(rs) : nothing) + s = sectors(S) + isempty(s) && return (nothing, nothing) + c = first(s) + l, r = TK.leftunit(c), TK.rightunit(c) + all(x -> TK.leftunit(x) == l && TK.rightunit(x) == r, s) || + throw(SpaceMismatch(lazy"components of $S do not share a single left and right unit")) + return (l, r) end -TensorKit.isunitspace(S::SumSpace) = !isempty(S) && all(isunitspace, S.spaces) # Promotion and conversion # ------------------------ diff --git a/test/vectorspaces/sumspace.jl b/test/vectorspaces/sumspace.jl index 8226dc9..0e9eb0c 100644 --- a/test/vectorspaces/sumspace.jl +++ b/test/vectorspaces/sumspace.jl @@ -186,7 +186,8 @@ end @test field(V) == ℂ @test_throws ArgumentError unitspace(V1) - @test unitspace(V) == unitspace(SumSpace(V1)) == ⊞(Vect[I](C0 => 1), Vect[I](D0 => 1)) + @test_throws ArgumentError unitspace(V) + @test_throws ArgumentError unitspace(SumSpace(V1)) @test @constinferred(sectortype(V)) == sectortype(V1) @test ((@constinferred sectors(V))...,) == (C0, C1, M, D0, D1) # sorted order @@ -208,20 +209,26 @@ end for W in [WC, WD] @test isunitspace(W) @test W == @constinferred(leftunitspace(W)) == @constinferred(rightunitspace(W)) - @test unitspace(typeof(W)) == ⊞(Vect[I](C0 => 1), Vect[I](D0 => 1)) + @test_throws ArgumentError unitspace(typeof(W)) end - @test_throws ArgumentError leftunitspace(V) - @test_throws ArgumentError rightunitspace(V) - @test leftunitspace(SumSpace(V1, V3)) == WC - @test rightunitspace(SumSpace(V2, V3)) == WD + # a `SumSpace` is only well-defined as a single left/right unit when ALL of its + # components, taken together, are homogeneously colored -- exactly like a plain + # `GradedSpace`. Combinations that mix components with different left and/or right + # units are rejected, even if they happen to agree on one side. + @test_throws SpaceMismatch leftunitspace(V) + @test_throws SpaceMismatch rightunitspace(V) + @test_throws SpaceMismatch leftunitspace(SumSpace(V1, V3)) + @test_throws SpaceMismatch rightunitspace(SumSpace(V2, V3)) @test leftunitspace(WMop) == WD && rightunitspace(WMop) == WC @test leftunitspace(WM) == WC && rightunitspace(WM) == WD - @test unitspace(WM) == unitspace(WMop) == ⊞(Vect[I](C0 => 1), Vect[I](D0 => 1)) + @test_throws ArgumentError unitspace(WM) + @test_throws ArgumentError unitspace(WMop) Wempty = SumSpace(Vect[I]()) Wzero = zerospace(V) - @test unitspace(Wempty) == unitspace(Wzero) + @test_throws ArgumentError unitspace(Wempty) + @test_throws ArgumentError unitspace(Wzero) for f in (leftunitspace, rightunitspace) @test_throws ArgumentError f(Wempty) end @@ -231,9 +238,9 @@ end VMD = SumSpace(V2, V3) @test @constinferred(⊞(V, V)) == SumSpace(vcat(V.spaces, V.spaces)) - @test @constinferred(⊞(VCM, unitspace(VCM))) == SumSpace(vcat(VCM.spaces, unitspace(VCM).spaces)) - @test @constinferred(⊞(VCM, leftunitspace(VCM))) == SumSpace(vcat(VCM.spaces, leftunitspace(VCM).spaces)) - @test @constinferred(⊞(VMD, rightunitspace(VMD))) == SumSpace(vcat(VMD.spaces, rightunitspace(VMD).spaces)) + @test_throws ArgumentError unitspace(VCM) + @test_throws SpaceMismatch leftunitspace(VCM) + @test_throws SpaceMismatch rightunitspace(VMD) @test @constinferred(⊞(V, V, V, V)) == SumSpace(repeat(V.spaces, 4)) @test @constinferred(fuse(VC, VC)) ≅ SumSpace(Vect[I](C0 => 8, C1 => 8)) @@ -248,10 +255,11 @@ end # blocksectors tests @test issetequal(@constinferred(blocksectors(one(V) ← one(V))), (C0, D0)) - @test issetequal(@constinferred(blocksectors(V ← V)), sectors(V)) + @test_throws SpaceMismatch V ← V # `V` itself is not homogeneously colored @test @constinferred(blocksectors(one(V))) == [C0, D0] - for v in [VC, VCM, VMD] - @test issetequal(@constinferred(blocksectors(v^2)), blocksectors(v ← v)) + @test issetequal(@constinferred(blocksectors(VC^2)), blocksectors(VC ← VC)) + for v in [VCM, VMD] + @test_throws SpaceMismatch v^2 # not homogeneously colored end for v in [WM, WMop] @test_throws SpaceMismatch v^2