From 45b27a4ca44053dcde4781675063417f5ba8651d Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 4 Aug 2026 15:45:53 +0200 Subject: [PATCH 01/11] define helper function --- src/auxiliary/auxiliary.jl | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/auxiliary/auxiliary.jl b/src/auxiliary/auxiliary.jl index 8e315e27f..486b4ce81 100644 --- a/src/auxiliary/auxiliary.jl +++ b/src/auxiliary/auxiliary.jl @@ -76,3 +76,18 @@ end else _allequal(f, xs) = allequal(f, xs) end + +_alldistinct(::Tuple{}) = true +_alldistinct(t::Tuple) = !in(t[1], tail(t)) && _alldistinct(tail(t)) + +""" + _check_levels(levels::Tuple) -> Nothing + +Check whether all elements of a tuple are distinct, throwing an `ArgumentError` if not. +This is used to validate the `levels` of `braid`. +""" +_check_levels(levels::Tuple) = _alldistinct(levels) || _throw_levels(levels) + +@noinline function _throw_levels(levels) + throw(ArgumentError(lazy"levels must be all distinct, got $levels")) +end From 886d7e7df9f0ffbea3c441eabd2f64d2c8af2166 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 4 Aug 2026 15:46:47 +0200 Subject: [PATCH 02/11] apply helper function + docstring clarification --- src/fusiontrees/braiding_manipulations.jl | 2 ++ src/spaces/homspace.jl | 1 + src/tensors/indexmanipulations.jl | 8 ++++++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/fusiontrees/braiding_manipulations.jl b/src/fusiontrees/braiding_manipulations.jl index 12926269c..8f28cf77e 100644 --- a/src/fusiontrees/braiding_manipulations.jl +++ b/src/fusiontrees/braiding_manipulations.jl @@ -216,6 +216,7 @@ braid(f::FusionTree{I, N}, p::IndexTuple{N}, levels::IndexTuple{N}) where {I, N} braid(f, (p, ()), (levels, ())) function braid(f::FusionTree{I, N}, (p, _)::Index2Tuple{N, 0}, (levels, _)::Index2Tuple{N, 0}) where {I, N} TupleTools.isperm(p) || throw(ArgumentError(lazy"not a valid permutation: $p")) + _check_levels(levels) @assert FusionStyle(I) isa UniqueFusion if BraidingStyle(I) isa SymmetricBraiding # this assumes Fsymbols are 1! coeff = one(sectorscalartype(I)) @@ -282,6 +283,7 @@ function braid(src::Union{FusionTreePair, FusionTreeBlock}, p::Index2Tuple, leve @assert numind(src) == length(p[1]) + length(p[2]) @assert numout(src) == length(levels[1]) && numin(src) == length(levels[2]) @assert TupleTools.isperm((p[1]..., p[2]...)) + _check_levels(levels[1]..., levels[2]...) return fsbraid((src, p, levels)) end diff --git a/src/spaces/homspace.jl b/src/spaces/homspace.jl index 8460f7578..63ad79edb 100644 --- a/src/spaces/homspace.jl +++ b/src/spaces/homspace.jl @@ -228,6 +228,7 @@ function braid(W::HomSpace, (p₁, p₂)::Index2Tuple, levels::IndexTuple) p = (p₁..., p₂...) TupleTools.isperm(p) && length(p) == numind(W) == length(levels) || throw(ArgumentError(lazy"$((p₁, p₂)), $levels is not a valid braiding for $(W)")) + _check_levels(levels) return select(W, (p₁, p₂)) end diff --git a/src/tensors/indexmanipulations.jl b/src/tensors/indexmanipulations.jl index 69f738a24..c41b1e79e 100644 --- a/src/tensors/indexmanipulations.jl +++ b/src/tensors/indexmanipulations.jl @@ -293,8 +293,10 @@ end Compute `tdst = β * tdst + α * braid(tsrc, (p₁, p₂), levels)`, writing the result into `tdst`. The codomain and domain of `tdst` correspond to the indices in `p₁` and `p₂` of `tsrc` respectively. -Here, `levels` is a tuple of length `numind(tsrc)` that assigns a level or height to the indices of `tsrc`, +Here, `levels` is a tuple of length `numind(tsrc)` that assigns a level or depth to the indices of `tsrc`, which determines whether they will braid over or under any other index with which they have to change places. +In other words, a smaller value in `levels` means that the corresponding index will be braided over any other index with a higher value. + Optionally specify a `backend` and `allocator` for the underlying array operation. See also [`braid`](@ref) for creating a new tensor. @@ -323,8 +325,9 @@ end Return tensor `tdst` obtained by braiding the indices of `tsrc`. The codomain and domain of `tdst` correspond to the indices in `p₁` and `p₂` of `tsrc` respectively. -Here, `levels` is a tuple of length `numind(tsrc)` that assigns a level or height to the indices of `tsrc`, +Here, `levels` is a tuple of length `numind(tsrc)` that assigns a level or depth to the indices of `tsrc`, which determines whether they will braid over or under any other index with which they have to change places. +In other words, a smaller value in `levels` means that the corresponding index will be braided over any other index with a higher value. If `copy=false`, `tdst` might share data with `tsrc` whenever possible. Otherwise, a copy is always made. Optionally specify a `backend` and `allocator` for the underlying array operation. @@ -336,6 +339,7 @@ function braid( copy::Bool = false, backend::AbstractBackend = TO.DefaultBackend(), allocator = TO.DefaultAllocator() ) length(levels) == numind(t) || throw(ArgumentError(lazy"length of levels should be $(numind(t)), got $(length(levels))")) + _check_levels(levels) (!copy && p == (codomainind(t), domainind(t))) && return t From a7083d37f641929aab41d3a8e76d8538835e10f7 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 4 Aug 2026 15:46:56 +0200 Subject: [PATCH 03/11] docs clarification --- docs/src/man/indexmanipulations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/man/indexmanipulations.md b/docs/src/man/indexmanipulations.md index fe2c460ef..b9a428ced 100644 --- a/docs/src/man/indexmanipulations.md +++ b/docs/src/man/indexmanipulations.md @@ -67,7 +67,7 @@ removeunit(::AbstractTensorMap, ::Val{i}) where {i} These operations reorder indices and/or move them between domain and codomain by applying the transposing or braiding isomorphisms of the underlying category. They form a hierarchy from most general to most restricted: -- [`braid`](@ref) is the most general: it accepts any permutation and requires a `levels` argument — a tuple of heights, one per index — that determines whether each index crosses over or under the others it has to pass. +- [`braid`](@ref) is the most general: it accepts any permutation and requires a `levels` argument — a tuple of depths, one per index — that determines whether each index crosses over or under the others it has to pass. - [`permute`](@ref) is a simpler interface for sector types with a symmetric braiding (`BraidingStyle(I) isa SymmetricBraiding`), where over- and under-crossings are equivalent and `levels` is therefore not needed. - [`transpose`](@ref) is restricted to *cyclic* permutations (indices do not cross). - [`repartition`](@ref) only moves the codomain/domain boundary without reordering the indices at all. From 4b4fb0a603a9e9b04ecd2fbb77d508e70cb2427f Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 4 Aug 2026 15:47:44 +0200 Subject: [PATCH 04/11] avoid duplicate levels --- src/tensors/braidingtensor.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tensors/braidingtensor.jl b/src/tensors/braidingtensor.jl index add0df48d..29f66f5df 100644 --- a/src/tensors/braidingtensor.jl +++ b/src/tensors/braidingtensor.jl @@ -249,7 +249,7 @@ function planarcontract!( N = numind(B) levels = ( levelsA[cindA[1]], levelsA[cindA[2]], - ntuple(Returns(3), N - 2)..., + ntuple(i -> i + 2, N - 2)..., ) braid!( @@ -303,7 +303,7 @@ function planarcontract!( N = numind(A) M = N - 2 levels = ( - ntuple(Returns(3), M)..., + ntuple(i -> i + 2, M)..., levelsB[cindB[1]], levelsB[cindB[2]], ) From f4286283382008b27782d718a08a6716b344c0ec Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 4 Aug 2026 15:47:58 +0200 Subject: [PATCH 05/11] add tests --- test/symmetries/doubletree.jl | 6 ++++++ test/tensors/indexmanipulations.jl | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/test/symmetries/doubletree.jl b/test/symmetries/doubletree.jl index f0f7484a0..b12f40e77 100644 --- a/test/symmetries/doubletree.jl +++ b/test/symmetries/doubletree.jl @@ -174,6 +174,12 @@ using TensorKitSectors end BraidingStyle(I) isa HasBraiding && @testset "Double fusion tree: permutation and braiding" begin + n = rand(0:(2N)) + p1, p2 = p[1:n], p[(n + 1):(2N)] + levels = ntuple(identity, 2N) + l1, l2 = levels[1:N], levels[(N + 1):(2N)] + @test_throws ArgumentError TensorKit.braid(src, (p1, p2), (TupleTools.setindex(l1, l2[1], 1), l2)) + for n in 0:(2N) p = (randperm(2 * N)...,) p1, p2 = p[1:n], p[(n + 1):(2N)] diff --git a/test/tensors/indexmanipulations.jl b/test/tensors/indexmanipulations.jl index 836418b3f..c3e4cd1de 100644 --- a/test/tensors/indexmanipulations.jl +++ b/test/tensors/indexmanipulations.jl @@ -140,3 +140,13 @@ for V in spacelist end TensorKit.empty_globalcaches!() end + +@timedtestset "braid: invalid levels" begin + t = rand(ComplexF64, ℂ^2 ⊗ ℂ^3 ← ℂ^4) + p = ((2, 1), (3,)) + @test braid(t, p, (1, 3, 2)) isa TensorMap + @test_throws ArgumentError braid(t, p, (1, 2, 2)) # duplicate levels + @test_throws ArgumentError braid(t, p, (1, 2)) # wrong length + @test_throws ArgumentError braid(space(t), p, (1, 2, 2)) + @test_throws ArgumentError braid!(similar(t, permute(space(t), p)), t, p, (1, 2, 2)) +end From 06fbfaffdf6b00d3b0b64097253cdc2e6611ebf4 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 4 Aug 2026 16:30:40 +0200 Subject: [PATCH 06/11] fix typo --- src/fusiontrees/braiding_manipulations.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fusiontrees/braiding_manipulations.jl b/src/fusiontrees/braiding_manipulations.jl index 8f28cf77e..856dd5242 100644 --- a/src/fusiontrees/braiding_manipulations.jl +++ b/src/fusiontrees/braiding_manipulations.jl @@ -283,7 +283,7 @@ function braid(src::Union{FusionTreePair, FusionTreeBlock}, p::Index2Tuple, leve @assert numind(src) == length(p[1]) + length(p[2]) @assert numout(src) == length(levels[1]) && numin(src) == length(levels[2]) @assert TupleTools.isperm((p[1]..., p[2]...)) - _check_levels(levels[1]..., levels[2]...) + _check_levels(tuple(levels[1]..., levels[2]...)) return fsbraid((src, p, levels)) end From ef665d851307bfdc15b578d6100063b2344cfd1d Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 4 Aug 2026 16:31:34 +0200 Subject: [PATCH 07/11] clarifications on duplicate levels + @ref + error strings --- docs/src/man/indexmanipulations.md | 2 +- src/auxiliary/auxiliary.jl | 2 +- src/tensors/indexmanipulations.jl | 1 + test/symmetries/doubletree.jl | 6 ++++-- test/tensors/indexmanipulations.jl | 11 +++++++---- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/src/man/indexmanipulations.md b/docs/src/man/indexmanipulations.md index b9a428ced..15c4a79b0 100644 --- a/docs/src/man/indexmanipulations.md +++ b/docs/src/man/indexmanipulations.md @@ -67,7 +67,7 @@ removeunit(::AbstractTensorMap, ::Val{i}) where {i} These operations reorder indices and/or move them between domain and codomain by applying the transposing or braiding isomorphisms of the underlying category. They form a hierarchy from most general to most restricted: -- [`braid`](@ref) is the most general: it accepts any permutation and requires a `levels` argument — a tuple of depths, one per index — that determines whether each index crosses over or under the others it has to pass. +- [`braid`](@ref) is the most general: it accepts any permutation and requires a `levels` argument — a tuple of depths, one per index — that determines whether each index crosses over or under the others it has to pass. In particular, duplicate levels are not allowed, and the length of `levels` must match the number of indices in the tensor. - [`permute`](@ref) is a simpler interface for sector types with a symmetric braiding (`BraidingStyle(I) isa SymmetricBraiding`), where over- and under-crossings are equivalent and `levels` is therefore not needed. - [`transpose`](@ref) is restricted to *cyclic* permutations (indices do not cross). - [`repartition`](@ref) only moves the codomain/domain boundary without reordering the indices at all. diff --git a/src/auxiliary/auxiliary.jl b/src/auxiliary/auxiliary.jl index 486b4ce81..5de30f182 100644 --- a/src/auxiliary/auxiliary.jl +++ b/src/auxiliary/auxiliary.jl @@ -84,7 +84,7 @@ _alldistinct(t::Tuple) = !in(t[1], tail(t)) && _alldistinct(tail(t)) _check_levels(levels::Tuple) -> Nothing Check whether all elements of a tuple are distinct, throwing an `ArgumentError` if not. -This is used to validate the `levels` of `braid`. +This is used to validate the `levels` of [`braid`](@ref). """ _check_levels(levels::Tuple) = _alldistinct(levels) || _throw_levels(levels) diff --git a/src/tensors/indexmanipulations.jl b/src/tensors/indexmanipulations.jl index c41b1e79e..c141cfebc 100644 --- a/src/tensors/indexmanipulations.jl +++ b/src/tensors/indexmanipulations.jl @@ -328,6 +328,7 @@ The codomain and domain of `tdst` correspond to the indices in `p₁` and `p₂` Here, `levels` is a tuple of length `numind(tsrc)` that assigns a level or depth to the indices of `tsrc`, which determines whether they will braid over or under any other index with which they have to change places. In other words, a smaller value in `levels` means that the corresponding index will be braided over any other index with a higher value. +`levels` can thus not contain duplicate values. If `copy=false`, `tdst` might share data with `tsrc` whenever possible. Otherwise, a copy is always made. Optionally specify a `backend` and `allocator` for the underlying array operation. diff --git a/test/symmetries/doubletree.jl b/test/symmetries/doubletree.jl index b12f40e77..e106249b7 100644 --- a/test/symmetries/doubletree.jl +++ b/test/symmetries/doubletree.jl @@ -175,13 +175,15 @@ using TensorKitSectors BraidingStyle(I) isa HasBraiding && @testset "Double fusion tree: permutation and braiding" begin n = rand(0:(2N)) + p = (randperm(2 * N)...,) p1, p2 = p[1:n], p[(n + 1):(2N)] levels = ntuple(identity, 2N) l1, l2 = levels[1:N], levels[(N + 1):(2N)] - @test_throws ArgumentError TensorKit.braid(src, (p1, p2), (TupleTools.setindex(l1, l2[1], 1), l2)) + l1′ = TupleTools.setindex(l1, l2[1], 1) + err_str = "levels must be all distinct, got $(tuple(l1′..., l2...))" + @test_throws ArgumentError(err_str) TensorKit.braid(src, (p1, p2), (l1′, l2)) for n in 0:(2N) - p = (randperm(2 * N)...,) p1, p2 = p[1:n], p[(n + 1):(2N)] ip = invperm(p) ip1, ip2 = ip[1:N], ip[(N + 1):(2N)] diff --git a/test/tensors/indexmanipulations.jl b/test/tensors/indexmanipulations.jl index c3e4cd1de..801ed5a88 100644 --- a/test/tensors/indexmanipulations.jl +++ b/test/tensors/indexmanipulations.jl @@ -145,8 +145,11 @@ end t = rand(ComplexF64, ℂ^2 ⊗ ℂ^3 ← ℂ^4) p = ((2, 1), (3,)) @test braid(t, p, (1, 3, 2)) isa TensorMap - @test_throws ArgumentError braid(t, p, (1, 2, 2)) # duplicate levels - @test_throws ArgumentError braid(t, p, (1, 2)) # wrong length - @test_throws ArgumentError braid(space(t), p, (1, 2, 2)) - @test_throws ArgumentError braid!(similar(t, permute(space(t), p)), t, p, (1, 2, 2)) + dupe_levels = (1, 2, 2) + dupe_err_str = "levels must be all distinct, got $(dupe_levels)" + len_err_str = "length of levels should be $(numind(t)), got $(length((1, 2)))" + @test_throws ArgumentError(dupe_err_str) braid(t, p, dupe_levels) # duplicate levels + @test_throws ArgumentError(len_err_str) braid(t, p, (1, 2)) # wrong length + @test_throws ArgumentError(dupe_err_str) braid(space(t), p, dupe_levels) + @test_throws ArgumentError(dupe_err_str) braid!(similar(t, permute(space(t), p)), t, p, dupe_levels) end From 0681205be69b4a49fa8d83ac95c45659f908662c Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 4 Aug 2026 17:28:49 +0200 Subject: [PATCH 08/11] check for duplicate levels only when anyonic --- docs/src/man/indexmanipulations.md | 2 +- src/auxiliary/auxiliary.jl | 15 +++++++------- src/fusiontrees/braiding_manipulations.jl | 8 +++++--- src/spaces/homspace.jl | 1 - src/tensors/braidingtensor.jl | 4 ++-- src/tensors/indexmanipulations.jl | 4 ++-- test/symmetries/doubletree.jl | 8 ++++++++ test/tensors/indexmanipulations.jl | 24 +++++++++++------------ 8 files changed, 37 insertions(+), 29 deletions(-) diff --git a/docs/src/man/indexmanipulations.md b/docs/src/man/indexmanipulations.md index 15c4a79b0..19d53f79b 100644 --- a/docs/src/man/indexmanipulations.md +++ b/docs/src/man/indexmanipulations.md @@ -67,7 +67,7 @@ removeunit(::AbstractTensorMap, ::Val{i}) where {i} These operations reorder indices and/or move them between domain and codomain by applying the transposing or braiding isomorphisms of the underlying category. They form a hierarchy from most general to most restricted: -- [`braid`](@ref) is the most general: it accepts any permutation and requires a `levels` argument — a tuple of depths, one per index — that determines whether each index crosses over or under the others it has to pass. In particular, duplicate levels are not allowed, and the length of `levels` must match the number of indices in the tensor. +- [`braid`](@ref) is the most general: it accepts any permutation and requires a `levels` argument — a tuple of depths, one per index — that determines whether each index crosses over or under the others it has to pass. In particular, duplicate levels are not allowed when the braiding is anyonic (`BraidingStyle(I) isa Anyonic`), and the length of `levels` must match the number of indices in the tensor. - [`permute`](@ref) is a simpler interface for sector types with a symmetric braiding (`BraidingStyle(I) isa SymmetricBraiding`), where over- and under-crossings are equivalent and `levels` is therefore not needed. - [`transpose`](@ref) is restricted to *cyclic* permutations (indices do not cross). - [`repartition`](@ref) only moves the codomain/domain boundary without reordering the indices at all. diff --git a/src/auxiliary/auxiliary.jl b/src/auxiliary/auxiliary.jl index 5de30f182..e5ed7cc90 100644 --- a/src/auxiliary/auxiliary.jl +++ b/src/auxiliary/auxiliary.jl @@ -77,17 +77,18 @@ else _allequal(f, xs) = allequal(f, xs) end -_alldistinct(::Tuple{}) = true -_alldistinct(t::Tuple) = !in(t[1], tail(t)) && _alldistinct(tail(t)) - """ _check_levels(levels::Tuple) -> Nothing Check whether all elements of a tuple are distinct, throwing an `ArgumentError` if not. This is used to validate the `levels` of [`braid`](@ref). +For symmetric braidings, this check is skipped since the levels are irrelevant. """ -_check_levels(levels::Tuple) = _alldistinct(levels) || _throw_levels(levels) - -@noinline function _throw_levels(levels) - throw(ArgumentError(lazy"levels must be all distinct, got $levels")) +_check_levels(::SymmetricBraiding, levels, s) = nothing +@inline function _check_levels(::BraidingStyle, levels, s) + levels[s] == levels[s + 1] && _throw_ambiguous_levels(levels[s]) + return nothing +end +@noinline function _throw_ambiguous_levels(l) + throw(ArgumentError(lazy"ambiguous braid: two indices with equal level $l have to cross")) end diff --git a/src/fusiontrees/braiding_manipulations.jl b/src/fusiontrees/braiding_manipulations.jl index 856dd5242..ce08f510c 100644 --- a/src/fusiontrees/braiding_manipulations.jl +++ b/src/fusiontrees/braiding_manipulations.jl @@ -216,9 +216,9 @@ braid(f::FusionTree{I, N}, p::IndexTuple{N}, levels::IndexTuple{N}) where {I, N} braid(f, (p, ()), (levels, ())) function braid(f::FusionTree{I, N}, (p, _)::Index2Tuple{N, 0}, (levels, _)::Index2Tuple{N, 0}) where {I, N} TupleTools.isperm(p) || throw(ArgumentError(lazy"not a valid permutation: $p")) - _check_levels(levels) @assert FusionStyle(I) isa UniqueFusion - if BraidingStyle(I) isa SymmetricBraiding # this assumes Fsymbols are 1! + braid_style = BraidingStyle(I) + if braid_style isa SymmetricBraiding # this assumes Fsymbols are 1! coeff = one(sectorscalartype(I)) for i in 1:N for j in 1:(i - 1) @@ -237,6 +237,7 @@ function braid(f::FusionTree{I, N}, (p, _)::Index2Tuple{N, 0}, (levels, _)::Inde T = sectorscalartype(I) c = one(T) for s in permutation2swaps(p) + _check_levels(braid_style, levels, s) inv = levels[s] > levels[s + 1] f, c′ = artin_braid(f, s; inv) c *= c′ @@ -283,7 +284,6 @@ function braid(src::Union{FusionTreePair, FusionTreeBlock}, p::Index2Tuple, leve @assert numind(src) == length(p[1]) + length(p[2]) @assert numout(src) == length(levels[1]) && numin(src) == length(levels[2]) @assert TupleTools.isperm((p[1]..., p[2]...)) - _check_levels(tuple(levels[1]..., levels[2]...)) return fsbraid((src, p, levels)) end @@ -318,7 +318,9 @@ end dst, U = repartition(src, numind(src)) + braid_style = BraidingStyle(I) for s in permutation2swaps(p) + _check_levels(braid_style, levels, s) inv = levels[s] > levels[s + 1] dst, U_tmp = artin_braid(dst, s; inv) U = U_tmp * U diff --git a/src/spaces/homspace.jl b/src/spaces/homspace.jl index 63ad79edb..8460f7578 100644 --- a/src/spaces/homspace.jl +++ b/src/spaces/homspace.jl @@ -228,7 +228,6 @@ function braid(W::HomSpace, (p₁, p₂)::Index2Tuple, levels::IndexTuple) p = (p₁..., p₂...) TupleTools.isperm(p) && length(p) == numind(W) == length(levels) || throw(ArgumentError(lazy"$((p₁, p₂)), $levels is not a valid braiding for $(W)")) - _check_levels(levels) return select(W, (p₁, p₂)) end diff --git a/src/tensors/braidingtensor.jl b/src/tensors/braidingtensor.jl index 29f66f5df..add0df48d 100644 --- a/src/tensors/braidingtensor.jl +++ b/src/tensors/braidingtensor.jl @@ -249,7 +249,7 @@ function planarcontract!( N = numind(B) levels = ( levelsA[cindA[1]], levelsA[cindA[2]], - ntuple(i -> i + 2, N - 2)..., + ntuple(Returns(3), N - 2)..., ) braid!( @@ -303,7 +303,7 @@ function planarcontract!( N = numind(A) M = N - 2 levels = ( - ntuple(i -> i + 2, M)..., + ntuple(Returns(3), M)..., levelsB[cindB[1]], levelsB[cindB[2]], ) diff --git a/src/tensors/indexmanipulations.jl b/src/tensors/indexmanipulations.jl index c141cfebc..c760fbeea 100644 --- a/src/tensors/indexmanipulations.jl +++ b/src/tensors/indexmanipulations.jl @@ -296,6 +296,7 @@ The codomain and domain of `tdst` correspond to the indices in `p₁` and `p₂` Here, `levels` is a tuple of length `numind(tsrc)` that assigns a level or depth to the indices of `tsrc`, which determines whether they will braid over or under any other index with which they have to change places. In other words, a smaller value in `levels` means that the corresponding index will be braided over any other index with a higher value. +`levels` can thus not contain duplicate values when the braiding is anyonic, as overbraiding and underbraiding are then not equivalent. Optionally specify a `backend` and `allocator` for the underlying array operation. @@ -328,7 +329,7 @@ The codomain and domain of `tdst` correspond to the indices in `p₁` and `p₂` Here, `levels` is a tuple of length `numind(tsrc)` that assigns a level or depth to the indices of `tsrc`, which determines whether they will braid over or under any other index with which they have to change places. In other words, a smaller value in `levels` means that the corresponding index will be braided over any other index with a higher value. -`levels` can thus not contain duplicate values. +`levels` can thus not contain duplicate values when the braiding is anyonic, as overbraiding and underbraiding are then not equivalent. If `copy=false`, `tdst` might share data with `tsrc` whenever possible. Otherwise, a copy is always made. Optionally specify a `backend` and `allocator` for the underlying array operation. @@ -340,7 +341,6 @@ function braid( copy::Bool = false, backend::AbstractBackend = TO.DefaultBackend(), allocator = TO.DefaultAllocator() ) length(levels) == numind(t) || throw(ArgumentError(lazy"length of levels should be $(numind(t)), got $(length(levels))")) - _check_levels(levels) (!copy && p == (codomainind(t), domainind(t))) && return t diff --git a/test/symmetries/doubletree.jl b/test/symmetries/doubletree.jl index e106249b7..bb0619742 100644 --- a/test/symmetries/doubletree.jl +++ b/test/symmetries/doubletree.jl @@ -174,6 +174,14 @@ using TensorKitSectors end BraidingStyle(I) isa HasBraiding && @testset "Double fusion tree: permutation and braiding" begin + if !(BraidingStyle(I) isa SymmetricBraiding) + eq = ntuple(Returns(1), N) # potential problematic level 1 + err_str = "ambiguous braid: two indices with equal level 1 have to cross" + + @test TensorKit.braid(src, (ntuple(identity, N), ntuple(i -> i + N, N)), (eq, eq)) isa Pair # equal levels are fine when the indices never meet + @test_throws ArgumentError(err_str) TensorKit.braid(src, ((2, 1, ntuple(i -> i + 2, N - 2)...), ntuple(i -> i + N, N)), (eq, eq)) # and rejected when they do + end + n = rand(0:(2N)) p = (randperm(2 * N)...,) p1, p2 = p[1:n], p[(n + 1):(2N)] diff --git a/test/tensors/indexmanipulations.jl b/test/tensors/indexmanipulations.jl index 801ed5a88..c4b0d924d 100644 --- a/test/tensors/indexmanipulations.jl +++ b/test/tensors/indexmanipulations.jl @@ -137,19 +137,17 @@ for V in spacelist t2 = braid(copy(t'), p, levels) @test t1 ≈ t2 end + hasbraiding && !symmetricbraiding && @timedtestset "Braid: invalid levels" begin + t = rand(ComplexF64, V1 ⊗ V2 ← V3) + p = ((2, 1), (3,)) + dupe_levels = (2, 2, 1) # level 2 is the duplicate + bad_lengths = (1, 2) + dupe_err_str = "ambiguous braid: two indices with equal level 2 have to cross" + len_err_str = "length of levels should be $(numind(t)), got $(length(bad_lengths))" + @test_throws ArgumentError(dupe_err_str) braid(t, p, dupe_levels) # duplicate levels + @test_throws ArgumentError(len_err_str) braid(t, p, bad_lengths) # wrong length + @test_throws ArgumentError(dupe_err_str) braid!(similar(t, permute(space(t), p)), t, p, dupe_levels) + end end TensorKit.empty_globalcaches!() end - -@timedtestset "braid: invalid levels" begin - t = rand(ComplexF64, ℂ^2 ⊗ ℂ^3 ← ℂ^4) - p = ((2, 1), (3,)) - @test braid(t, p, (1, 3, 2)) isa TensorMap - dupe_levels = (1, 2, 2) - dupe_err_str = "levels must be all distinct, got $(dupe_levels)" - len_err_str = "length of levels should be $(numind(t)), got $(length((1, 2)))" - @test_throws ArgumentError(dupe_err_str) braid(t, p, dupe_levels) # duplicate levels - @test_throws ArgumentError(len_err_str) braid(t, p, (1, 2)) # wrong length - @test_throws ArgumentError(dupe_err_str) braid(space(t), p, dupe_levels) - @test_throws ArgumentError(dupe_err_str) braid!(similar(t, permute(space(t), p)), t, p, dupe_levels) -end From 15bf7f687df8ba7a52bc6653b10b196dc6dd40c8 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 4 Aug 2026 18:32:41 +0200 Subject: [PATCH 09/11] forgot to remove old test --- test/symmetries/doubletree.jl | 9 --------- 1 file changed, 9 deletions(-) diff --git a/test/symmetries/doubletree.jl b/test/symmetries/doubletree.jl index bb0619742..33a93fb9c 100644 --- a/test/symmetries/doubletree.jl +++ b/test/symmetries/doubletree.jl @@ -182,15 +182,6 @@ using TensorKitSectors @test_throws ArgumentError(err_str) TensorKit.braid(src, ((2, 1, ntuple(i -> i + 2, N - 2)...), ntuple(i -> i + N, N)), (eq, eq)) # and rejected when they do end - n = rand(0:(2N)) - p = (randperm(2 * N)...,) - p1, p2 = p[1:n], p[(n + 1):(2N)] - levels = ntuple(identity, 2N) - l1, l2 = levels[1:N], levels[(N + 1):(2N)] - l1′ = TupleTools.setindex(l1, l2[1], 1) - err_str = "levels must be all distinct, got $(tuple(l1′..., l2...))" - @test_throws ArgumentError(err_str) TensorKit.braid(src, (p1, p2), (l1′, l2)) - for n in 0:(2N) p1, p2 = p[1:n], p[(n + 1):(2N)] ip = invperm(p) From ce6aeafb284f7b865292cb70a1802d90f281877a Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 5 Aug 2026 08:43:59 +0200 Subject: [PATCH 10/11] deleted too much --- test/symmetries/doubletree.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/symmetries/doubletree.jl b/test/symmetries/doubletree.jl index 33a93fb9c..4fbe43e90 100644 --- a/test/symmetries/doubletree.jl +++ b/test/symmetries/doubletree.jl @@ -183,6 +183,7 @@ using TensorKitSectors end for n in 0:(2N) + p = (randperm(2 * N)...,) p1, p2 = p[1:n], p[(n + 1):(2N)] ip = invperm(p) ip1, ip2 = ip[1:N], ip[(N + 1):(2N)] From d65fbcdd35552c9e276a3922e0657498d1c37a4f Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 5 Aug 2026 16:24:50 +0200 Subject: [PATCH 11/11] remove docstring sentences [skip ci] --- src/tensors/indexmanipulations.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/tensors/indexmanipulations.jl b/src/tensors/indexmanipulations.jl index c760fbeea..ce4add069 100644 --- a/src/tensors/indexmanipulations.jl +++ b/src/tensors/indexmanipulations.jl @@ -296,7 +296,6 @@ The codomain and domain of `tdst` correspond to the indices in `p₁` and `p₂` Here, `levels` is a tuple of length `numind(tsrc)` that assigns a level or depth to the indices of `tsrc`, which determines whether they will braid over or under any other index with which they have to change places. In other words, a smaller value in `levels` means that the corresponding index will be braided over any other index with a higher value. -`levels` can thus not contain duplicate values when the braiding is anyonic, as overbraiding and underbraiding are then not equivalent. Optionally specify a `backend` and `allocator` for the underlying array operation. @@ -329,7 +328,6 @@ The codomain and domain of `tdst` correspond to the indices in `p₁` and `p₂` Here, `levels` is a tuple of length `numind(tsrc)` that assigns a level or depth to the indices of `tsrc`, which determines whether they will braid over or under any other index with which they have to change places. In other words, a smaller value in `levels` means that the corresponding index will be braided over any other index with a higher value. -`levels` can thus not contain duplicate values when the braiding is anyonic, as overbraiding and underbraiding are then not equivalent. If `copy=false`, `tdst` might share data with `tsrc` whenever possible. Otherwise, a copy is always made. Optionally specify a `backend` and `allocator` for the underlying array operation.