From f4c3a2098b3638b15374451e56404f8975ba81bd Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 10:23:19 +0100 Subject: [PATCH 01/23] remove TK from project.toml --- Project.toml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Project.toml b/Project.toml index 65904cb..1d8b729 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "MultiTensorKit" uuid = "f0555a46-f681-4ef1-bed5-d64870568636" -authors = ["Boris De Vos, Laurens Lootens and Lukas Devos"] version = "0.1.0" +authors = ["Boris De Vos, Laurens Lootens and Lukas Devos"] [deps] DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" @@ -15,7 +15,6 @@ LinearAlgebra = "1" Pkg = "1" Random = "1" SafeTestsets = "0.1" -TensorKit = "0.16, 0.17" TensorKitSectors = "0.3.7" TensorOperations = "5" Test = "1.10" @@ -27,10 +26,9 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" -TensorKit = "07d1fe3e-3e46-537d-9eac-e9e13d0d4cec" TensorOperations = "6aa20fa7-93e2-5fca-9bc0-fbd0db3c71a2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestExtras = "5ed8adda-3752-4e41-b88a-e8b09835ee3a" [targets] -test = ["Aqua", "LinearAlgebra", "Random", "SafeTestsets", "TensorKit", "TensorOperations", "Test", "TestExtras"] \ No newline at end of file +test = ["Aqua", "LinearAlgebra", "Random", "SafeTestsets", "TensorOperations", "Test", "TestExtras"] From 89a72868ec0d04f2fe5a84ff46ab0999653981a3 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 10:46:09 +0100 Subject: [PATCH 02/23] add TK as weakdep --- Project.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Project.toml b/Project.toml index 1d8b729..beb6f30 100644 --- a/Project.toml +++ b/Project.toml @@ -8,6 +8,9 @@ DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" TensorKitSectors = "13a9c161-d5da-41f0-bcbd-e1a08ae0647f" +[weakdeps] +TensorKit = "07d1fe3e-3e46-537d-9eac-e9e13d0d4cec" + [compat] Aqua = "0.8.9" DelimitedFiles = "1.9" @@ -15,6 +18,7 @@ LinearAlgebra = "1" Pkg = "1" Random = "1" SafeTestsets = "0.1" +TensorKit = "0.17.2" TensorKitSectors = "0.3.7" TensorOperations = "5" Test = "1.10" From 817fac38496ab33d30f36c5734f110cc8a162443 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 10:47:05 +0100 Subject: [PATCH 03/23] fix bounds in constructor --- src/bimodulesector.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bimodulesector.jl b/src/bimodulesector.jl index 4ea5896..a02a80a 100644 --- a/src/bimodulesector.jl +++ b/src/bimodulesector.jl @@ -7,9 +7,9 @@ struct BimoduleSector{Name} <: Sector function BimoduleSector{Name}(i::Int, j::Int, label::Int) where {Name} Name ∈ ImplementedBimoduleSectors || throw(ArgumentError("BimoduleSector $Name not implemented")) - i <= size(BimoduleSector{Name}) && j <= size(BimoduleSector{Name}) || + 1 <= i <= size(BimoduleSector{Name}) && 1 <= j <= size(BimoduleSector{Name}) || throw(DomainError("object outside the matrix $Name")) - return label <= _numlabels(BimoduleSector{Name}, i, j) ? new{Name}(i, j, label) : + return 1 <= label <= _numlabels(BimoduleSector{Name}, i, j) ? new{Name}(i, j, label) : throw(DomainError("label outside category $Name($i, $j)")) end end From b00fcfacd80ea317f97eca6bc0bededfd6d10673 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 12:18:46 +0100 Subject: [PATCH 04/23] clearer domain errors --- src/bimodulesector.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/bimodulesector.jl b/src/bimodulesector.jl index a02a80a..ae348e9 100644 --- a/src/bimodulesector.jl +++ b/src/bimodulesector.jl @@ -7,10 +7,11 @@ struct BimoduleSector{Name} <: Sector function BimoduleSector{Name}(i::Int, j::Int, label::Int) where {Name} Name ∈ ImplementedBimoduleSectors || throw(ArgumentError("BimoduleSector $Name not implemented")) - 1 <= i <= size(BimoduleSector{Name}) && 1 <= j <= size(BimoduleSector{Name}) || - throw(DomainError("object outside the matrix $Name")) + r = size(BimoduleSector{Name}) + (1 <= i <= r && 1 <= j <= r) || + throw(DomainError((i, j), "subcategory outside the $r×$r matrix $Name")) return 1 <= label <= _numlabels(BimoduleSector{Name}, i, j) ? new{Name}(i, j, label) : - throw(DomainError("label outside category $Name($i, $j)")) + throw(DomainError(label, "label outside category $Name($i, $j)")) end end From d9c2354f79f30686f7016b7b3f614ad3c86da742 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 12:22:51 +0100 Subject: [PATCH 05/23] iterate over sector values row-major to match isless --- src/bimodulesector.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/bimodulesector.jl b/src/bimodulesector.jl index ae348e9..7c6cbf2 100644 --- a/src/bimodulesector.jl +++ b/src/bimodulesector.jl @@ -44,16 +44,16 @@ Base.size(::Type{A4Object}) = 7 Base.IteratorSize(::Type{<:SectorValues{<:BimoduleSector}}) = Base.SizeUnknown() -function Base.iterate(iter::SectorValues{<:BimoduleSector}, (I, label) = (1, 1)) +function Base.iterate(iter::SectorValues{<:BimoduleSector}, (n, label) = (1, 1)) A = eltype(iter) s = size(A) - I > s * s && return nothing - i, j = CartesianIndices((s, s))[I].I + n > s * s && return nothing + i, j = fldmod1(n, s) maxlabel = _numlabels(A, i, j) return if label > maxlabel - iterate(iter, (I + 1, 1)) + iterate(iter, (n + 1, 1)) else - A(i, j, label), (I, label + 1) + A(i, j, label), (n, label + 1) end end From 3fcb4ac185bdc5d4a11397616ffa1368405b270a Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 12:46:29 +0100 Subject: [PATCH 06/23] proper Nsymbol column check --- src/bimodulesector.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bimodulesector.jl b/src/bimodulesector.jl index 7c6cbf2..5ce3c2b 100644 --- a/src/bimodulesector.jl +++ b/src/bimodulesector.jl @@ -93,10 +93,12 @@ function extract_Nsymbol(::Type{I}) where {I <: BimoduleSector} filename = joinpath(artifact_path, name, "Nsymbol.txt") isfile(filename) || throw(LoadError(filename, 0, "Nsymbol file not found for $name")) Narray = readdlm(filename) # matrix with 7 columns + size(Narray, 2) == 7 || + throw(ArgumentError("Nsymbol file for $name has wrong number of columns: $(size(Narray, 2)) != 7")) data_dict = Dict{NTuple{3, Int}, Dict{NTuple{3, Int}, Int}}() for row in eachrow(Narray) - i, j, k, a, b, c, N = Int.(@view(row[1:size(I)])) + i, j, k, a, b, c, N = Int.(@view(row[1:7])) colordict = get!(data_dict, (i, j, k), Dict{NTuple{3, Int}, Int}()) push!(colordict, (a, b, c) => N) end From 36e378670dbc44d33e1413fb3c069a82d10c2d6a Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 12:56:07 +0100 Subject: [PATCH 07/23] correctly search for units, allow to error on time --- src/bimodulesector.jl | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/src/bimodulesector.jl b/src/bimodulesector.jl index 5ce3c2b..35ed552 100644 --- a/src/bimodulesector.jl +++ b/src/bimodulesector.jl @@ -135,37 +135,28 @@ end function extract_dual(::Type{I}) where {I <: BimoduleSector} N = _get_Ncache(I) ncats = size(I) - Is = zeros(Int, ncats) + nobj = [maximum(first, keys(N[i, j, j])) for i in 1:ncats, j in 1:ncats] - map(1:ncats) do i + Is = map(1:ncats) do i Niii = N[i, i, i] - nobji = maximum(first, keys(N[i, i, i])) # want to return a leftunit and rightunit for each entry in multifusion cat # leftunit/rightunit needs to at least be the unit object within a fusion cat - Is[i] = findfirst(1:nobji) do a + u = findfirst(1:nobj[i, i]) do a get(Niii, (a, a, a), 0) == 1 || return false # I x I -> I - for othera in 1:nobji - get(Niii, (othera, a, othera), 0) == 1 || return false # a x I -> a - get(Niii, (a, othera, othera), 0) == 1 || return false # I x a -> a - end - # check leftunit - map(1:ncats) do j - nobjj = maximum(first, keys(N[j, j, j])) - for b in 1:nobjj - get(N[i, j, j], (a, b, b), 0) == 1 || return false # I = leftunit(b) - end + # I = leftunit(b) for every b in C_ij, I x b -> b in C_ii x C_ij -> C_ij + for j in 1:ncats, b in 1:nobj[i, j] + get(N[i, i, j], (a, b, b), 0) == 1 || return false end - # check rightunit - map(1:ncats) do k - nobjk = maximum(first, keys(N[k, k, k])) - for c in 1:nobjk - get(N[k, i, k], (c, a, c), 0) == 1 || return false # I = rightunit(c) - end + # I = rightunit(c) for every c in C_ki, c x I -> c in C_ki x C_ii -> C_ki + for k in 1:ncats, c in 1:nobj[k, i] + get(N[k, i, i], (c, a, c), 0) == 1 || return false end return true end + isnothing(u) && throw(ArgumentError("no unit object found in subcategory ($i, $i) of $I")) + return u end allduals = Matrix{Vector{Int}}(undef, ncats, ncats) # ncats square matrix of vectors From b57c798f94be7c37eb53c8889f684a344447b4ae Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 13:53:49 +0100 Subject: [PATCH 08/23] fix ranges of dual calculations --- src/bimodulesector.jl | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/bimodulesector.jl b/src/bimodulesector.jl index 35ed552..86cc7e0 100644 --- a/src/bimodulesector.jl +++ b/src/bimodulesector.jl @@ -160,20 +160,16 @@ function extract_dual(::Type{I}) where {I <: BimoduleSector} end allduals = Matrix{Vector{Int}}(undef, ncats, ncats) # ncats square matrix of vectors - for i in 1:ncats - nobji = maximum(first, keys(N[i, i, i])) - for j in 1:ncats - allduals[i, j] = Int[] - - nobjj = maximum(first, keys(N[j, j, j])) - # the nested vectors contain the duals of the objects in 𝒞_ij, which are in C_ji - Niji = N[i, j, i] # 𝒞_ij x 𝒞_ji -> C_ii - Njij = N[j, i, j] # 𝒞_ji x 𝒞_ij -> C_jj - for i_ob in 1:nobji, j_ob in 1:nobjj - get(Niji, (i_ob, j_ob, Is[i]), 0) == 1 || continue # leftunit(c_ij) ∈ c_ij x c_ji - get(Njij, (j_ob, i_ob, Is[j]), 0) == 1 || continue # rightunit(c_ij) ∈ c_ji x c_ij - push!(allduals[i, j], j_ob) + for i in 1:ncats, j in 1:ncats + Niji = N[i, j, i] # 𝒞_ij x 𝒞_ji -> 𝒞_ii + Njij = N[j, i, j] # 𝒞_ji x 𝒞_ij -> 𝒞_jj + allduals[i, j] = map(1:nobj[i, j]) do m + mbar = findfirst(1:nobj[j, i]) do n + get(Niji, (m, n, Is[i]), 0) == 1 && # leftunit(m) in m x mbar + get(Njij, (n, m, Is[j]), 0) == 1 # rightunit(m) in mbar x m end + isnothing(mbar) && error("no dual found for $I($i, $j, $m)") + return mbar end end return Is, allduals From 10fbfed05987baabcafcfd88b9b5bdb5cb16e8ba Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 14:01:04 +0100 Subject: [PATCH 09/23] add unitstyle, make allunits allocate less and have isunit not build allunits in the fallback --- src/bimodulesector.jl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/bimodulesector.jl b/src/bimodulesector.jl index 86cc7e0..e836af9 100644 --- a/src/bimodulesector.jl +++ b/src/bimodulesector.jl @@ -180,9 +180,15 @@ function TensorKitSectors.unit(a::BimoduleSector) return typeof(a)(a.i, a.i, _get_dual_cache(typeof(a))[1][a.i]) end +TensorKitSectors.UnitStyle(::Type{<:BimoduleSector}) = GenericUnit() + function TensorKitSectors.allunits(::Type{I}) where {I <: BimoduleSector} - s = size(I) - return I[I(i, i, _get_dual_cache(I)[1][i]) for i in 1:s] + Is = _get_dual_cache(I)[1] + return ntuple(i -> I(i, i, Is[i]), Val(size(I))) +end + +function TensorKitSectors.isunit(a::BimoduleSector) + return a.i == a.j && a.label == _get_dual_cache(typeof(a))[1][a.i] end function TensorKitSectors.unit(::Type{<:BimoduleSector}) From d7a6c6991fb8247dd1ec2944acc2a9c34d6c95e2 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 14:16:58 +0100 Subject: [PATCH 10/23] return number of objects matrix in dual cache, allowing easier counting and not hard-coding size --- src/bimodulesector.jl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/bimodulesector.jl b/src/bimodulesector.jl index e836af9..bd03fbe 100644 --- a/src/bimodulesector.jl +++ b/src/bimodulesector.jl @@ -40,7 +40,7 @@ function Base.convert(::Type{BimoduleSector{Name}}, d::NTuple{3, Int}) where {Na return BimoduleSector{Name}(d...) end -Base.size(::Type{A4Object}) = 7 +Base.size(I::Type{<:BimoduleSector}) = size(_get_dual_cache(I)[3], 1) Base.IteratorSize(::Type{<:SectorValues{<:BimoduleSector}}) = Base.SizeUnknown() @@ -57,10 +57,7 @@ function Base.iterate(iter::SectorValues{<:BimoduleSector}, (n, label) = (1, 1)) end end -function Base.length(::SectorValues{I}) where {I <: BimoduleSector} - s = size(I) - return sum(_numlabels(I, i, j) for i in 1:s, j in 1:s) -end +Base.length(::SectorValues{I}) where {I <: BimoduleSector} = sum(_get_dual_cache(I)[3]) TensorKitSectors.FusionStyle(::Type{A4Object}) = GenericFusion() TensorKitSectors.BraidingStyle(::Type{<:BimoduleSector}) = NoBraiding() @@ -77,7 +74,7 @@ function TensorKitSectors.:⊗(a::I, b::I) where {I <: BimoduleSector} end function _numlabels(::Type{T}, i, j) where {T <: BimoduleSector} - return length(_get_dual_cache(T)[2][i, j]) + return _get_dual_cache(T)[3][i, j] end # User-friendly functions @@ -103,6 +100,9 @@ function extract_Nsymbol(::Type{I}) where {I <: BimoduleSector} push!(colordict, (a, b, c) => N) end + ncats = maximum(maximum, keys(data_dict)) + ncats == size(I) || + throw(ArgumentError("Nsymbol data for $name has $ncats subcategories, but size($I) = $(size(I))")) return data_dict end @@ -123,7 +123,7 @@ function TensorKitSectors.Nsymbol(a::I, b::I, c::I) where {I <: BimoduleSector} return get(_get_Ncache(I)[i, j, k], (a.label, b.label, c.label), 0) end -const Dualcache = IdDict{Type{<:BimoduleSector}, Tuple{Vector{Int64}, Matrix{Vector{Int64}}}}() +const Dualcache = IdDict{Type{<:BimoduleSector}, Tuple{Vector{Int64}, Matrix{Vector{Int64}}, Matrix{Int64}}}() function _get_dual_cache(::Type{T}) where {T <: BimoduleSector} global Dualcache @@ -172,7 +172,7 @@ function extract_dual(::Type{I}) where {I <: BimoduleSector} return mbar end end - return Is, allduals + return Is, allduals, nobj end function TensorKitSectors.unit(a::BimoduleSector) From b348ba18c06ab10feff19327192a9c47e4dc3630 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 14:18:37 +0100 Subject: [PATCH 11/23] remove duplicate convert --- src/bimodulesector.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/bimodulesector.jl b/src/bimodulesector.jl index bd03fbe..8ac42da 100644 --- a/src/bimodulesector.jl +++ b/src/bimodulesector.jl @@ -19,8 +19,6 @@ BimoduleSector{Name}(data::NTuple{3, Int}) where {Name} = BimoduleSector{Name}(d BimoduleSectorName(::Type{BimoduleSector{Name}}) where {Name} = Name const A4Object = BimoduleSector{:A4} -Base.convert(::Type{<:BimoduleSector{Name}}, labels::NTuple{3, Int}) where {Name} = BimoduleSector{Name}(labels...) - function Base.show(io::IO, a::BimoduleSector{Name}) where {Name} if get(io, :typeinfo, nothing) === typeof(a) print(io, (a.i, a.j, a.label)) From 8743fe5fdcc925f6d08f950c557a321bf8243d21 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 15:23:04 +0100 Subject: [PATCH 12/23] fix not knowing size at compile time --- src/bimodulesector.jl | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/bimodulesector.jl b/src/bimodulesector.jl index 8ac42da..7c764ae 100644 --- a/src/bimodulesector.jl +++ b/src/bimodulesector.jl @@ -1,4 +1,4 @@ -const ImplementedBimoduleSectors = [:A4] +const ImplementedBimoduleSectors = (:A4,) struct BimoduleSector{Name} <: Sector i::Int @@ -98,9 +98,6 @@ function extract_Nsymbol(::Type{I}) where {I <: BimoduleSector} push!(colordict, (a, b, c) => N) end - ncats = maximum(maximum, keys(data_dict)) - ncats == size(I) || - throw(ArgumentError("Nsymbol data for $name has $ncats subcategories, but size($I) = $(size(I))")) return data_dict end @@ -132,7 +129,7 @@ end function extract_dual(::Type{I}) where {I <: BimoduleSector} N = _get_Ncache(I) - ncats = size(I) + ncats = maximum(maximum, keys(N)) nobj = [maximum(first, keys(N[i, j, j])) for i in 1:ncats, j in 1:ncats] Is = map(1:ncats) do i @@ -181,8 +178,7 @@ end TensorKitSectors.UnitStyle(::Type{<:BimoduleSector}) = GenericUnit() function TensorKitSectors.allunits(::Type{I}) where {I <: BimoduleSector} - Is = _get_dual_cache(I)[1] - return ntuple(i -> I(i, i, Is[i]), Val(size(I))) + return I[I(i, i, u) for (i, u) in enumerate(_get_dual_cache(I)[1])] end function TensorKitSectors.isunit(a::BimoduleSector) From d09aaa2bc8340e2be3e8c5be86dcbcef2832ab41 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 15:25:30 +0100 Subject: [PATCH 13/23] fix seed for test reproducibility, add constructor test --- test/test_A4.jl | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/test/test_A4.jl b/test/test_A4.jl index 59fa329..95f61bf 100644 --- a/test/test_A4.jl +++ b/test/test_A4.jl @@ -9,6 +9,9 @@ using .SectorTestSuite using MultiTensorKit using Test, TestExtras +using Random + +Random.seed!(1234) const MTK = MultiTensorKit @@ -23,6 +26,17 @@ println("----------------------") @time SectorTestSuite.test_sector(I) end +@testset "$Istr construction and ordering" begin + @test_throws DomainError I(1, 1, 0) + @test_throws DomainError I(1, 1, -1) + @test_throws DomainError I(0, 1, 1) + @test_throws DomainError I(r + 1, 1, 1) + @test_throws DomainError I(1, 1, MTK._numlabels(I, 1, 1) + 1) + vals = collect(values(I)) + @test issorted(vals) + @test length(vals) == length(values(I)) +end + @testset "$Istr ($i, $j) basic properties" for i in 1:r, j in 1:r Cii_obs = I.(i, i, MTK._get_dual_cache(I)[2][i, i]) Cij_obs = I.(i, j, MTK._get_dual_cache(I)[2][i, j]) @@ -36,7 +50,6 @@ end u = I.(i, i, MTK._get_dual_cache(I)[1][i]) @test u == @testinferred(leftunit(u)) == @testinferred(rightunit(u)) == @testinferred(unit(u)) - @test isunit(@testinferred(unit(c))) @test dual(c) == I.(i, i, MTK._get_dual_cache(I)[2][i, i][c.label]) end else @@ -159,5 +172,3 @@ end end end end - -# include("TK_compat.jl") \ No newline at end of file From b44ea5dc80d767989730b76454b3632ec09dd4ec Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 15:26:22 +0100 Subject: [PATCH 14/23] be exhaustive with unitarity checks --- test/test_A4.jl | 60 ++++++------------------------------------------- 1 file changed, 7 insertions(+), 53 deletions(-) diff --git a/test/test_A4.jl b/test/test_A4.jl index 95f61bf..10ae5c2 100644 --- a/test/test_A4.jl +++ b/test/test_A4.jl @@ -92,59 +92,13 @@ println("-----------------------------") # explicitly test everything related to F-symbols # other option is to edit smallset to sample more -for i in 1:r, j in 1:r - @testset "Unitarity of $Istr F-move ($i, $j)" begin - if i == j - @testset "Unitarity of fusion F-move ($i, $j)" begin - fusion_objects = I.(i, i, MTK._get_dual_cache(I)[2][i, i]) - for a in fusion_objects, b in fusion_objects, c in fusion_objects - @test SectorTestSuite.F_unitarity_test(a, b, c) - end - end - end - - i != j || continue # do this part only when off-diagonal - mod_objects = I.(i, j, MTK._get_dual_cache(I)[2][i, j]) - left_fusion_objects = I.(i, i, MTK._get_dual_cache(I)[2][i, i]) - right_fusion_objects = I.(j, j, MTK._get_dual_cache(I)[2][j, j]) - - # C x C x M -> M or D x D x Mop -> Mop - @testset "Unitarity of left module F-move ($i, $j)" begin - for a in left_fusion_objects, b in left_fusion_objects, A in mod_objects - @test SectorTestSuite.F_unitarity_test(a, b, A) - end - end - - # M x D x D -> M or Mop x C x C -> Mop - @testset "Unitarity of right module F-move ($i, $j)" begin - for A in mod_objects, b in right_fusion_objects, c in right_fusion_objects - @test SectorTestSuite.F_unitarity_test(A, b, c) - end - end - - # C x M x D -> M or D x Mop x C -> Mop - @testset "Unitarity of bimodule F-move ($i, $j)" begin - for a in left_fusion_objects, A in mod_objects, α in right_fusion_objects - @test SectorTestSuite.F_unitarity_test(a, A, α) - end - end - - @testset "Unitarity of mixed module F-move ($i, $j) and opposite ($j, $i)" begin - modop_objects = I.(j, i, MTK._get_dual_cache(I)[2][j, i]) - - # C x M x Mop -> C or D x Mop x M -> D - # M x Mop x C -> C or Mop x M x D -> D - # Mop x C x M -> D or M x D x Mop -> C - for a in left_fusion_objects, A in mod_objects, Aop in modop_objects - @test SectorTestSuite.F_unitarity_test(a, A, Aop) - @test SectorTestSuite.F_unitarity_test(A, Aop, a) - @test SectorTestSuite.F_unitarity_test(Aop, a, A) - end - - # M x Mop x M -> M or Mop x M x Mop -> Mop - for A in mod_objects, Aop in modop_objects - @test SectorTestSuite.F_unitarity_test(A, Aop, A) - end +@testset "Unitarity of $Istr F-move" begin + objs = collect(values(I)) + for a in objs, b in objs + a.j == b.i || continue + for c in objs + b.j == c.i || continue + @test SectorTestSuite.F_unitarity_test(a, b, c; atol = 1.0e-12, rtol = 1.0e-12) end end end From 52ada8af256fa42d45c4513a1f6be44fbe24f405 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 15:32:37 +0100 Subject: [PATCH 15/23] test duals more rigorously --- test/test_A4.jl | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/test_A4.jl b/test/test_A4.jl index 10ae5c2..61a8f65 100644 --- a/test/test_A4.jl +++ b/test/test_A4.jl @@ -50,7 +50,6 @@ end u = I.(i, i, MTK._get_dual_cache(I)[1][i]) @test u == @testinferred(leftunit(u)) == @testinferred(rightunit(u)) == @testinferred(unit(u)) - @test dual(c) == I.(i, i, MTK._get_dual_cache(I)[2][i, i][c.label]) end else @testset "Basic module properties" begin @@ -61,7 +60,6 @@ end @test unit(d) == rightunit(m) == leftunit(mop) @test_throws DomainError unit(m) @test_throws DomainError unit(mop) - @test dual(m) == I.(j, i, MTK._get_dual_cache(I)[2][i, j][m.label]) end @testset "$Istr Fusion rules" begin @@ -86,6 +84,15 @@ end end end +@testset "$Istr units and duals of all objects" begin + for a in values(I) + ā = dual(a) + @test dual(ā) == a + @test Nsymbol(a, ā, leftunit(a)) == 1 + @test Nsymbol(ā, a, rightunit(a)) == 1 + end +end + println("-----------------------------") println("| F-symbol data tests |") println("-----------------------------") From 4b7b94e6179c812347e0ccf56e75e08ef4ea0067 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 15:43:09 +0100 Subject: [PATCH 16/23] clean up readme where possible --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 91ca994..2a8f3a5 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # MultiTensorKit.jl -[TensorKit.jl](https://github.com/jutho/TensorKit.jl) extension for multi-fusion categories. +[TensorKit.jl](https://github.com/QuantumKitHub/TensorKit.jl) extension for multi-fusion categories. | **Documentation** | **Digital Object Identifier** | |:-----------------:|:-----------------------------:| | [![][docs-stable-img]][docs-stable-url] [![][docs-dev-img]][docs-dev-url] | [![DOI][doi-img]][doi-url] | -| **Build Status** | **PkgEval** | **Coverage** | -|:----------------:|:------------:|:------------:| -| [![CI][ci-img]][ci-url] | [![PkgEval][pkgeval-img]][pkgeval-url] | [![Codecov][codecov-img]][codecov-url] | +| **Build Status** | **PkgEval** | **Coverage** | **Quality assurance** | +|:----------------:|:------------:|:------------:|:---------------------:| +| [![CI][ci-img]][ci-url] | [![PkgEval][pkgeval-img]][pkgeval-url] | [![Codecov][codecov-img]][codecov-url] | [![Aqua QA][aqua-img]][aqua-url] | [docs-stable-img]: https://img.shields.io/badge/docs-stable-blue.svg [docs-stable-url]: https://QuantumKitHub.github.io/MultiTensorKit.jl/stable @@ -29,3 +29,5 @@ [pkgeval-img]: https://JuliaCI.github.io/NanosoldierReports/pkgeval_badges/T/MPSKit.svg [pkgeval-url]: https://JuliaCI.github.io/NanosoldierReports/pkgeval_badges/T/MPSKit.html +[aqua-img]: https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg +[aqua-url]: https://github.com/JuliaTesting/Aqua.jl \ No newline at end of file From 2fa08af8cb6112cbad689a194b76329d84b349ab Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 15:47:12 +0100 Subject: [PATCH 17/23] remove allocations in Fsymbol call --- src/bimodulesector.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/bimodulesector.jl b/src/bimodulesector.jl index 7c764ae..a578b10 100644 --- a/src/bimodulesector.jl +++ b/src/bimodulesector.jl @@ -262,11 +262,14 @@ function TensorKitSectors.Fsymbol(a::I, b::I, c::I, d::I, e::I, f::I) where {I < Nbcf = Nsymbol(b, c, f) Nafd = Nsymbol(a, f, d) - zero_array = zeros(sectorscalartype(I), Nabe, Necd, Nbcf, Nafd) + T = sectorscalartype(I) Nabe > 0 && Necd > 0 && Nbcf > 0 && Nafd > 0 || - return zero_array + return zeros(sectorscalartype(I), Nabe, Necd, Nbcf, Nafd) i, j, k, l = a.i, a.j, b.j, c.j colordict = _get_Fcache(I)[i, j, k, l] - return get!(colordict, (a.label, b.label, c.label, d.label, e.label, f.label), zero_array) + return get!( + () -> zeros(sectorscalartype(I), Nabe, Necd, Nbcf, Nafd), + colordict, (a.label, b.label, c.label, d.label, e.label, f.label) + ) end From 939ac59a2d752c782896a5ac2e4b0e348550ccfa Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 15:48:20 +0100 Subject: [PATCH 18/23] final test cleanup --- test/test_A4.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_A4.jl b/test/test_A4.jl index 61a8f65..cc22b36 100644 --- a/test/test_A4.jl +++ b/test/test_A4.jl @@ -38,16 +38,16 @@ end end @testset "$Istr ($i, $j) basic properties" for i in 1:r, j in 1:r - Cii_obs = I.(i, i, MTK._get_dual_cache(I)[2][i, i]) - Cij_obs = I.(i, j, MTK._get_dual_cache(I)[2][i, j]) - Cji_obs = I.(j, i, MTK._get_dual_cache(I)[2][j, i]) - Cjj_obs = I.(j, j, MTK._get_dual_cache(I)[2][j, j]) + Cii_obs = I.(i, i, 1:MTK._numlabels(I, i, i)) + Cij_obs = I.(i, j, 1:MTK._numlabels(I, i, j)) + Cji_obs = I.(j, i, 1:MTK._numlabels(I, j, i)) + Cjj_obs = I.(j, j, 1:MTK._numlabels(I, j, j)) c, m, mop, d = rand(Cii_obs), rand(Cij_obs), rand(Cji_obs), rand(Cjj_obs) if i == j @testset "Basic fusion properties" begin @test isunit(@testinferred(unit(c))) - u = I.(i, i, MTK._get_dual_cache(I)[1][i]) + u = only(filter(isunit, Cii_obs)) @test u == @testinferred(leftunit(u)) == @testinferred(rightunit(u)) == @testinferred(unit(u)) end From 83a2920a9e823b12d4dc0e1764e3d483a522b18d Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 16:15:16 +0100 Subject: [PATCH 19/23] oopsie --- src/bimodulesector.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bimodulesector.jl b/src/bimodulesector.jl index a578b10..aa11263 100644 --- a/src/bimodulesector.jl +++ b/src/bimodulesector.jl @@ -264,12 +264,12 @@ function TensorKitSectors.Fsymbol(a::I, b::I, c::I, d::I, e::I, f::I) where {I < T = sectorscalartype(I) Nabe > 0 && Necd > 0 && Nbcf > 0 && Nafd > 0 || - return zeros(sectorscalartype(I), Nabe, Necd, Nbcf, Nafd) + return zeros(T, Nabe, Necd, Nbcf, Nafd) i, j, k, l = a.i, a.j, b.j, c.j colordict = _get_Fcache(I)[i, j, k, l] return get!( - () -> zeros(sectorscalartype(I), Nabe, Necd, Nbcf, Nafd), + () -> zeros(T, Nabe, Necd, Nbcf, Nafd), colordict, (a.label, b.label, c.label, d.label, e.label, f.label) ) end From 0efa99eba5f2a7d3e8e3d57b294269f92f4cb27a Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 16:18:51 +0100 Subject: [PATCH 20/23] make random sampling random at least --- test/test_A4.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test_A4.jl b/test/test_A4.jl index cc22b36..fd71523 100644 --- a/test/test_A4.jl +++ b/test/test_A4.jl @@ -12,6 +12,7 @@ using Test, TestExtras using Random Random.seed!(1234) +rng = Random.Xoshiro(1234) const MTK = MultiTensorKit @@ -42,7 +43,7 @@ end Cij_obs = I.(i, j, 1:MTK._numlabels(I, i, j)) Cji_obs = I.(j, i, 1:MTK._numlabels(I, j, i)) Cjj_obs = I.(j, j, 1:MTK._numlabels(I, j, j)) - c, m, mop, d = rand(Cii_obs), rand(Cij_obs), rand(Cji_obs), rand(Cjj_obs) + c, m, mop, d = rand(rng, Cii_obs), rand(rng, Cij_obs), rand(rng, Cji_obs), rand(rng, Cjj_obs) if i == j @testset "Basic fusion properties" begin @@ -67,7 +68,7 @@ end # forbidden fusions for obs in [(c, d), (d, c), (m, m), (mop, mop), (d, m), (m, c), (mop, d), (c, mop)] @test isempty(⊗(obs...)) - @test_throws argerr Nsymbol(obs..., rand([c, m, mop, d])) + @test_throws argerr Nsymbol(obs..., rand(rng, [c, m, mop, d])) end # allowed fusions From 866a62954d544c3740dd7792cd1fec3f20e86ab2 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 16:19:50 +0100 Subject: [PATCH 21/23] change pkgeval link (doesn't matter for now) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2a8f3a5..405df25 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,8 @@ [ci-img]: https://github.com/QuantumKitHub/MultiTensorKit.jl/actions/workflows/Tests.yml/badge.svg [ci-url]: https://github.com/QuantumKitHub/MultiTensorKit.jl/actions/workflows/Tests.yml -[pkgeval-img]: https://JuliaCI.github.io/NanosoldierReports/pkgeval_badges/T/MPSKit.svg -[pkgeval-url]: https://JuliaCI.github.io/NanosoldierReports/pkgeval_badges/T/MPSKit.html +[pkgeval-img]: https://JuliaCI.github.io/NanosoldierReports/pkgeval_badges/M/MultiTensorKit.svg +[pkgeval-url]: https://JuliaCI.github.io/NanosoldierReports/pkgeval_badges/M/MultiTensorKit.html [aqua-img]: https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg [aqua-url]: https://github.com/JuliaTesting/Aqua.jl \ No newline at end of file From 49f46ba2593eea1a7fd989c1b95b64c5b3ce6480 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 16:20:53 +0100 Subject: [PATCH 22/23] column check for Fsymbol.txt as well --- src/bimodulesector.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bimodulesector.jl b/src/bimodulesector.jl index aa11263..8e0c981 100644 --- a/src/bimodulesector.jl +++ b/src/bimodulesector.jl @@ -208,6 +208,7 @@ function extract_Fsymbol(::Type{I}) where {I <: BimoduleSector} @assert isfile(filename) "cannot find $filename" Farray = readdlm(filename) + size(Farray, 2) == 16 || throw(ArgumentError("Fsymbol file for $name has wrong number of columns: $(size(Farray, 2)) != 16")) for ((i, j, k, l), colordict) in convert_Fs(Farray) result[(i, j, k, l)] = Dict{NTuple{6, Int}, Array{ComplexF64, 4}}() for ((a, b, c, d, e, f), Fvals) in colordict From 408eb9ac77b93dd5a460d8c072cb2dfd42b0c2d5 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Fri, 30 Oct 2026 16:29:58 +0100 Subject: [PATCH 23/23] tiny unit label refactor --- src/bimodulesector.jl | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/bimodulesector.jl b/src/bimodulesector.jl index 8e0c981..b6c8551 100644 --- a/src/bimodulesector.jl +++ b/src/bimodulesector.jl @@ -74,6 +74,7 @@ end function _numlabels(::Type{T}, i, j) where {T <: BimoduleSector} return _get_dual_cache(T)[3][i, j] end +_unitlabel(::Type{I}, i::Int) where {I <: BimoduleSector} = _get_dual_cache(I)[1][i] # User-friendly functions # ------------------- @@ -163,7 +164,7 @@ function extract_dual(::Type{I}) where {I <: BimoduleSector} get(Niji, (m, n, Is[i]), 0) == 1 && # leftunit(m) in m x mbar get(Njij, (n, m, Is[j]), 0) == 1 # rightunit(m) in mbar x m end - isnothing(mbar) && error("no dual found for $I($i, $j, $m)") + isnothing(mbar) && throw(ArgumentError("no dual found for $I($i, $j, $m)")) return mbar end end @@ -172,7 +173,7 @@ end function TensorKitSectors.unit(a::BimoduleSector) a.i == a.j || throw(DomainError("unit of module category ($(a.i), $(a.j)) of $(typeof(a)) is ill-defined")) - return typeof(a)(a.i, a.i, _get_dual_cache(typeof(a))[1][a.i]) + return typeof(a)(a.i, a.i, _unitlabel(typeof(a), a.i)) end TensorKitSectors.UnitStyle(::Type{<:BimoduleSector}) = GenericUnit() @@ -182,7 +183,7 @@ function TensorKitSectors.allunits(::Type{I}) where {I <: BimoduleSector} end function TensorKitSectors.isunit(a::BimoduleSector) - return a.i == a.j && a.label == _get_dual_cache(typeof(a))[1][a.i] + return a.i == a.j && a.label == _unitlabel(typeof(a), a.i) end function TensorKitSectors.unit(::Type{<:BimoduleSector}) @@ -190,11 +191,11 @@ function TensorKitSectors.unit(::Type{<:BimoduleSector}) end function TensorKitSectors.leftunit(a::BimoduleSector) - return typeof(a)(a.i, a.i, _get_dual_cache(typeof(a))[1][a.i]) + return typeof(a)(a.i, a.i, _unitlabel(typeof(a), a.i)) end function TensorKitSectors.rightunit(a::BimoduleSector) - return typeof(a)(a.j, a.j, _get_dual_cache(typeof(a))[1][a.j]) + return typeof(a)(a.j, a.j, _unitlabel(typeof(a), a.j)) end function TensorKitSectors.dual(a::BimoduleSector)