Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ When releasing a new version, move the "Unreleased" changes to a new version sec
### Performance

- Recoupling matrices are stored such that they can leverage BLAS, also for mixed complex tensor with real recoupling coefficients.
- Braiding fusion tree blocks no longer constructs and indexes the canonical intermediate blocks of every step: these keep their trees in the order in which they are produced, their buffers are recycled, and blocks with a single tree are braided as a product of scalars (internal)

## [0.17.2](https://github.com/QuantumKitHub/TensorKit.jl/compare/v0.17.1...v0.17.2) - 2026-09-20

Expand Down
104 changes: 72 additions & 32 deletions src/fusiontrees/braiding_manipulations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@ tree with non-zero coefficient, namely `f` with coefficient `1`. This keyword ha
if `BraidingStyle(sectortype(f)) isa SymmetricBraiding`.
"""
function artin_braid(f::FusionTree{I, N}, i; inv::Bool = false) where {I, N}
1 <= i < N || throw(ArgumentError(lazy"Cannot swap outputs i=$i and i+1 out of only $N outputs"))
@assert FusionStyle(I) === UniqueFusion()
return _artin_braid_single(f, i, inv)
end
# also for a tree that is the only one with its uncoupled sectors, whose braid is again a single
# tree (`only` throws otherwise)
function _artin_braid_single(f::FusionTree{I, N}, i, inv::Bool) where {I, N}
1 <= i < N || throw(ArgumentError(lazy"Cannot swap outputs i=$i and i+1 out of only $N outputs"))

uncoupled = f.uncoupled
a, b = uncoupled[i], uncoupled[i + 1]
Expand Down Expand Up @@ -53,7 +58,7 @@ function artin_braid(f::FusionTree{I, N}, i; inv::Bool = false) where {I, N}

if i == 1
c = N > 2 ? inner[1] : coupled′
R = oftype(oneT, (inv ? conj(Rsymbol(b, a, c)) : Rsymbol(a, b, c)))
R = oftype(oneT, (inv ? conj(only(Rsymbol(b, a, c))) : only(Rsymbol(a, b, c))))
f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices)
return f′ => R
end
Expand All @@ -64,21 +69,31 @@ function artin_braid(f::FusionTree{I, N}, i; inv::Bool = false) where {I, N}
a = inner_extended[i - 1]
c = inner_extended[i]
e = inner_extended[i + 1]
c′ = only(a ⊗ d)
c′ = FusionStyle(I) isa UniqueFusion ? only(a ⊗ d) : only(_artin_channels(a, b, d, e))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we put the style check inside the helper function and just use dispatch?

coeff = oftype(
oneT,
if inv
conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * Rsymbol(d, a, c′)
conj(only(Rsymbol(d, c, e)) * only(Fsymbol(d, a, b, e, c′, c))) * only(Rsymbol(d, a, c′))
else
Rsymbol(c, d, e) * conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′))
only(Rsymbol(c, d, e)) * conj(only(Fsymbol(d, a, b, e, c′, c)) * only(Rsymbol(a, d, c′)))
end
)
inner′ = TupleTools.setindex(inner, c′, i - 1)
f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′)
return f′ => coeff
end

function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where {I, N}
# the inner lines `c′` of `a ⊗ d → c′ → c′ ⊗ b → e`, i.e. `intersect(a ⊗ d, e ⊗ conj(b))`,
# without constructing the second fusion product for sectors with a simple unit
function _artin_channels(a::I, b::I, d::I, e::I) where {I <: Sector}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm not mistaken this is currently only used in the implementation that immediately calls only. For that case the iterator etc is probably overkill and just returning the first found entry should suffice. However, I do like this implementation over the intersect call, which typically has to allocate (although in principle all of our sector products are ordered so an intersect could be done easily/efficiently), so it might also be useful to investigate if this helps in the non-unique cases. If so, I would even be in favor of having this function public, maybe in TensorKitSectors since it is quite common, and specialized implementations should be faster/zero allocations

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit, I missed you also have it below. I'm still curious whether it might make sense to use the fact that the products are sorted for a fast intersect iterable

UnitStyle(I) isa SimpleUnit || return intersect(a ⊗ d, e ⊗ conj(b))
return Iterators.filter(c′ -> !iszero(Nsymbol(c′, b, e)), a ⊗ d)
end

function artin_braid(
src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false, canonical::Val = Val(true),
buffers = (nothing, nothing) # a tree vector and a matrix that may be overwritten

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the matrix, since it is fixed size, it might make sense to just have artin_braid!(U, ...) or something similar.

) where {I, N}
1 <= i < N || throw(ArgumentError("Cannot swap outputs i=$i and i+1 out of only $N outputs"))
isempty(src) && return src => zeros(sectorscalartype(I), 0, 0)

Expand All @@ -91,12 +106,12 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where
isdual = src.isdual[1]
isdual′ = TupleTools.setindex(isdual, isdual[i], i + 1)
isdual′ = TupleTools.setindex(isdual′, isdual[i + 1], i)
dst = FusionTreeBlock{I}((uncoupled′, ()), (isdual′, ()); sizehint = length(src))
rows = TreeRows(I, (uncoupled′, ()), (isdual′, ()), length(src), canonical, buffers[1])

oneT = one(sectorscalartype(I))

indexmap = treeindex_map(dst)
U = zeros(sectorscalartype(I), length(dst), length(src))
U = buffers[2] isa Matrix{typeof(oneT)} ? fill!(buffers[2], zero(oneT)) :
zeros(sectorscalartype(I), max(length(rows.trees), length(src)), length(src)) # see `bendright`

if isunit(a) || isunit(b) # braiding with trivial sector: simple and always possible
for (col, (f, f₂)) in enumerate(fusiontrees(src))
Expand All @@ -113,10 +128,10 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where
vertices′ = TupleTools.setindex(vertices′, vertices[i - 1], i)
end
f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′)
row = indexmap[treeindex_data((f′, f₂))]
row = row!(rows, (f′, f₂))
@inbounds U[row, col] = oneT
end
return dst => U
return FusionTreeBlock(rows.trees) => U
end

BraidingStyle(I) isa NoBraiding &&
Expand All @@ -132,7 +147,7 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where
if FusionStyle(I) isa MultiplicityFreeFusion
R = oftype(oneT, (inv ? conj(Rsymbol(b, a, c)) : Rsymbol(a, b, c)))
f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices)
row = indexmap[treeindex_data((f′, f₂))]
row = row!(rows, (f′, f₂))
@inbounds U[row, col] = R
else # GenericFusion
μ = vertices[1]
Expand All @@ -142,7 +157,7 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where
iszero(R) && continue
vertices′ = TupleTools.setindex(vertices, ν, 1)
f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner, vertices′)
row = indexmap[treeindex_data((f′, f₂))]
row = row!(rows, (f′, f₂))
@inbounds U[row, col] = R
end
end
Expand All @@ -155,29 +170,30 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where
c = inner_extended[i]
e = inner_extended[i + 1]
if FusionStyle(I) isa MultiplicityFreeFusion
for c′ in intersect(a ⊗ d, e ⊗ conj(b))
R = inv ? Rsymbol(d, c, e) : Rsymbol(c, d, e)
for c′ in _artin_channels(a, b, d, e)
coeff = if inv
conj(Rsymbol(d, c, e) * Fsymbol(d, a, b, e, c′, c)) * Rsymbol(d, a, c′)
conj(R * Fsymbol(d, a, b, e, c′, c)) * Rsymbol(d, a, c′)
else
Rsymbol(c, d, e) * conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′))
R * conj(Fsymbol(d, a, b, e, c′, c) * Rsymbol(a, d, c′))
end
iszero(coeff) && continue
inner′ = TupleTools.setindex(inner, c′, i - 1)
f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′)
row = indexmap[treeindex_data((f′, f₂))]
row = row!(rows, (f′, f₂))
@inbounds U[row, col] = coeff
end
else # GenericFusion
for c′ in intersect(a ⊗ d, e ⊗ conj(b))
Rmat1 = inv ? Rsymbol(d, c, e)' : Rsymbol(c, d, e)
Rmat1 = inv ? Rsymbol(d, c, e)' : Rsymbol(c, d, e)
μ = vertices[i - 1]
ν = vertices[i]
for c′ in _artin_channels(a, b, d, e)
Rmat2 = inv ? Rsymbol(d, a, c′)' : Rsymbol(a, d, c′)
Fmat = Fsymbol(d, a, b, e, c′, c)
μ = vertices[i - 1]
ν = vertices[i]
for σ in 1:Nsymbol(a, d, c′)
for λ in 1:Nsymbol(c′, b, e)
for σ in axes(Rmat2, 1)
for λ in axes(Fmat, 2)
coeff = zero(oneT)
for ρ in 1:Nsymbol(d, c, e), κ in 1:Nsymbol(d, a, c′)
for ρ in axes(Rmat1, 2), κ in axes(Rmat2, 2)
coeff += Rmat1[ν, ρ] * conj(Fmat[κ, λ, μ, ρ]) *
conj(Rmat2[σ, κ])
end
Expand All @@ -186,15 +202,15 @@ function artin_braid(src::FusionTreeBlock{I, N, 0}, i; inv::Bool = false) where
vertices′ = TupleTools.setindex(vertices′, λ, i)
inner′ = TupleTools.setindex(inner, c′, i - 1)
f′ = FusionTree{I}(uncoupled′, coupled′, isdual′, inner′, vertices′)
row = indexmap[treeindex_data((f′, f₂))]
row = row!(rows, (f′, f₂))
@inbounds U[row, col] = coeff
end
end
end
end
end

return dst => U
return FusionTreeBlock(rows.trees) => U
end

# braid fusion tree
Expand Down Expand Up @@ -312,29 +328,53 @@ end
return (f₁′, f₂′) => coeff1 * coeff2 * coeff3
else
src, (p1, p2), (l1, l2) = key
src = src::FusionTreeBlock{I}

p = linearizepermutation(p1, p2, numout(src), numin(src))
levels = (l1..., reverse(l2)...)

dst, U = repartition(src, numind(src))
if length(src) == 1 # then so is every intermediate block: the braid is a product of scalars
(f, f₀), coeff = _bend_single(_bendleft_single, only(fusiontrees(src)), Val(numin(src)))
for s in permutation2swaps(p)
_check_levels(BraidingStyle(I), levels, s)
inv = levels[s] > levels[s + 1]
f, c = _artin_braid_single(f, s, inv)
coeff *= c
l = levels[s]
levels = TupleTools.setindex(levels, levels[s + 1], s)
levels = TupleTools.setindex(levels, l, s + 1)
end
(f₁′, f₂′), c = _bend_single(_bendright_single, (f, f₀), Val(N₂))
return FusionTreeBlock([(f₁′, f₂′)]) => fill(coeff * c, 1, 1)
end

# the intermediate blocks keep their trees in the order in which they are produced,
# and only the result is brought into canonical order
dst, U = repartition_noncanonical(src, Val(numind(src)))

# the swaps keep the size of the block, so that the tree vector and the matrices of the
# previous step are recycled (except for the trees of `src`, which are part of the key)
buffers = (nothing, nothing)
U_spare = nothing
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
dst′, U_tmp = artin_braid(dst, s; inv, canonical = Val(false), buffers)
buffers = (dst.trees === src.trees ? nothing : dst.trees, U_tmp)
dst = dst′
U_spare, U = U, U_spare isa typeof(U_tmp) ? mul!(U_spare, U_tmp, U) : U_tmp * U
l = levels[s]
levels = TupleTools.setindex(levels, levels[s + 1], s)
levels = TupleTools.setindex(levels, l, s + 1)
end

if N₂ == 0
return dst => U
return canonicalize(dst, U)
else
dst, U_tmp = repartition(dst::FusionTreeBlock{I}, N₁)
dst, U_tmp = repartition_noncanonical(dst::FusionTreeBlock{I}, Val(N₁))
U = U_tmp * U
return dst => U
return canonicalize(dst, U)
end
end
end
Expand Down
62 changes: 42 additions & 20 deletions src/fusiontrees/duality_manipulations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,22 @@ function _bendright_treepair((f₁, f₂)::FusionTreePair)
end

function bendright((f₁, f₂)::FusionTreePair)
I = sectortype((f₁, f₂))
@assert FusionStyle((f₁, f₂)) === UniqueFusion()
return _bendright_single((f₁, f₂))
end
# also for a final fusion vertex without multiplicity (`only` throws otherwise)
function _bendright_single((f₁, f₂)::FusionTreePair)
N₁ = numout((f₁, f₂))
@assert FusionStyle(I) === UniqueFusion()
(a, b, c), (f₁′, f₂′) = _bendright_treepair((f₁, f₂))

# compute the coefficient
coeff₀ = sqrtdim(c) * invsqrtdim(a)
f₁.isdual[N₁] && (coeff₀ *= conj(frobenius_schur_phase(dual(b))))
coeff = coeff₀ * Bsymbol(a, b, c)
coeff = coeff₀ * only(Bsymbol(a, b, c))

return (f₁′, f₂′) => coeff
end
function bendright(src::FusionTreeBlock)
function bendright(src::FusionTreeBlock, canonical::Val = Val(true))
I = sectortype(src)
N₁ = numout(src)
N₂ = numin(src)
Expand All @@ -80,9 +83,10 @@ function bendright(src::FusionTreeBlock)
(src.isdual[2]..., !(src.isdual[1][N₁])),
)

dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst; sizehint = length(src))
indexmap = treeindex_map(dst)
U = zeros(fusionscalartype(I), length(dst), length(src))
rows = TreeRows(I, uncoupled_dst, isdual_dst, length(src), canonical)
# the canonical destination block has more trees than `src` if `src` holds only some of the
# trees with its uncoupled sectors (as in `planar_trace`)
U = zeros(fusionscalartype(I), max(length(rows.trees), length(src)), length(src))

for (col, (f₁, f₂)) in enumerate(fusiontrees(src))
(a, b, c), (f₁′, f₂′) = _bendright_treepair((f₁, f₂))
Expand All @@ -92,7 +96,7 @@ function bendright(src::FusionTreeBlock)
end
if FusionStyle(I) isa MultiplicityFreeFusion
coeff = coeff₀ * Bsymbol(a, b, c)
row = indexmap[treeindex_data((f₁′, f₂′))]
row = row!(rows, (f₁′, f₂′))
@inbounds U[row, col] = coeff
else
Bmat = Bsymbol(a, b, c)
Expand All @@ -106,12 +110,12 @@ function bendright(src::FusionTreeBlock)
iszero(coeff) && continue
vertices₂ = N₂ > 0 ? (f₂.vertices..., ν) : ()
f₂′ = FusionTree(uncoupled₂, coupled₂, isdual₂, inner₂, vertices₂)
row = indexmap[treeindex_data((f₁′, f₂′))]
row = row!(rows, (f₁′, f₂′))
@inbounds U[row, col] = coeff
end
end
end
return dst => U
return FusionTreeBlock(rows.trees) => U
end

@doc """
Expand Down Expand Up @@ -142,10 +146,22 @@ function bendleft((f₁, f₂)::FusionTreePair)
(f₂′, f₁′), coeff = bendright((f₂, f₁))
return (f₁′, f₂′) => conj(coeff)
end
function _bendleft_single((f₁, f₂)::FusionTreePair)
(f₂′, f₁′), coeff = _bendright_single((f₂, f₁))
return (f₁′, f₂′) => conj(coeff)
end

# `n` bends of a tree pair that stays a single tree pair, and the product of their coefficients
_bend_single(bend, f, ::Val{0}) = f => one(sectorscalartype(sectortype(f)))
function _bend_single(bend, f, ::Val{n}) where {n}
f′, c′ = bend(f)
f″, c″ = _bend_single(bend, f′, Val(n - 1))
return f″ => c′ * c″
end

# !! note that this is more or less a copy of bendright through
# (f1, f2) => conj(coeff) for ((f2, f1), coeff) in bendleft(src)
function bendleft(src::FusionTreeBlock)
function bendleft(src::FusionTreeBlock, canonical::Val = Val(true))
I = sectortype(src)
N₁ = numout(src)
N₂ = numin(src)
Expand All @@ -159,9 +175,10 @@ function bendleft(src::FusionTreeBlock)
TupleTools.front(src.isdual[2]),
)

dst = FusionTreeBlock{I}(uncoupled_dst, isdual_dst; sizehint = length(src))
indexmap = treeindex_map(dst)
U = zeros(fusionscalartype(I), length(dst), length(src))
rows = TreeRows(I, uncoupled_dst, isdual_dst, length(src), canonical)
# the canonical destination block has more trees than `src` if `src` holds only some of the
# trees with its uncoupled sectors (as in `planar_trace`)
U = zeros(fusionscalartype(I), max(length(rows.trees), length(src)), length(src))

for (col, (f₁, f₂)) in enumerate(fusiontrees(src))
(a, b, c), (f₂′, f₁′) = _bendright_treepair((f₂, f₁))
Expand All @@ -171,7 +188,7 @@ function bendleft(src::FusionTreeBlock)
end
if FusionStyle(I) isa MultiplicityFreeFusion
coeff = coeff₀ * Bsymbol(a, b, c)
row = indexmap[treeindex_data((f₁′, f₂′))]
row = row!(rows, (f₁′, f₂′))
@inbounds U[row, col] = conj(coeff)
else
Bmat = Bsymbol(a, b, c)
Expand All @@ -185,12 +202,12 @@ function bendleft(src::FusionTreeBlock)
iszero(coeff) && continue
vertices₁ = N₁ > 0 ? (f₁.vertices..., ν) : ()
f₁′ = FusionTree(uncoupled₁, coupled₁, isdual₁, inner₁, vertices₁)
row = indexmap[treeindex_data((f₁′, f₂′))]
row = row!(rows, (f₁′, f₂′))
@inbounds U[row, col] = conj(coeff)
end
end
end
return dst => U
return FusionTreeBlock(rows.trees) => U
end

@doc """
Expand Down Expand Up @@ -478,7 +495,7 @@ return dst, U
# it, so that it is visible to the generator's world age. Otherwise a precompile workload that
# first triggers `repartition` fails with `UndefVarError: _repartition_body` (at runtime the
# first call happens at a later world age, which masks the ordering issue).
function _repartition_body(N)
function _repartition_body(N, canonical = true)
if N == 0
ex = quote
T = fusionscalartype(sectortype(src))
Expand All @@ -491,13 +508,14 @@ function _repartition_body(N)
end
else
f = N < 0 ? bendleft : bendright
args = canonical ? () : (Val(false),)
ex_rep = Expr(:block)
for _ in 1:(abs(N) - 1)
push!(ex_rep.args, :((dst, Utmp) = $f(dst)))
push!(ex_rep.args, :((dst, Utmp) = $f(dst, $(args...))))
push!(ex_rep.args, :(U = Utmp * U))
end
ex = quote
dst, U = $f(src)
dst, U = $f(src, $(args...))
$ex_rep
return dst => U
end
Expand All @@ -507,6 +525,10 @@ end
@generated function repartition(src::Union{FusionTreePair, FusionTreeBlock}, ::Val{N}) where {N}
return _repartition_body(numout(src) - N)
end
# the same for a block, with the trees of the result in the order in which they are produced
@generated function repartition_noncanonical(src::FusionTreeBlock, ::Val{N}) where {N}
return _repartition_body(numout(src) - N, false)
end

"""
transpose((f₁, f₂)::FusionTreePair{I}, p::Index2Tuple{N₁, N₂}) where {I, N₁, N₂}
Expand Down
Loading
Loading