diff --git a/docs/src/man/indexmanipulations.md b/docs/src/man/indexmanipulations.md index fe2c460ef..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 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. 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 8e315e27f..e5ed7cc90 100644 --- a/src/auxiliary/auxiliary.jl +++ b/src/auxiliary/auxiliary.jl @@ -76,3 +76,19 @@ end else _allequal(f, xs) = allequal(f, xs) end + +""" + _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(::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 12926269c..ce08f510c 100644 --- a/src/fusiontrees/braiding_manipulations.jl +++ b/src/fusiontrees/braiding_manipulations.jl @@ -217,7 +217,8 @@ braid(f::FusionTree{I, N}, p::IndexTuple{N}, levels::IndexTuple{N}) where {I, N} 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")) @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) @@ -236,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′ @@ -316,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/tensors/indexmanipulations.jl b/src/tensors/indexmanipulations.jl index 69f738a24..ce4add069 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. diff --git a/test/symmetries/doubletree.jl b/test/symmetries/doubletree.jl index f0f7484a0..4fbe43e90 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 + 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..c4b0d924d 100644 --- a/test/tensors/indexmanipulations.jl +++ b/test/tensors/indexmanipulations.jl @@ -137,6 +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