Skip to content
Open
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.
- Constructing a `FusionTreeBlock` only takes the fusion product of the shorter side, and `fusionblocks` groups the cached fusion trees of the space instead of constructing a block for every combination of sectors (internal)

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

Expand Down
29 changes: 20 additions & 9 deletions src/fusiontrees/fusiontrees.jl
Original file line number Diff line number Diff line change
Expand Up @@ -140,18 +140,29 @@ function FusionTreeBlock{I}(
end

# note: cs is unique due to ⊗ returning unique fusion results
cs = if N₁ == 0
sort!(collect(filter(isunit, ⊗(uncoupled[2]...))))
elseif N₂ == 0
sort!(collect(filter(isunit, ⊗(uncoupled[1]...))))
# a superset of the coupled sectors suffices, as there are no fusion trees for the others:
# the unit of the other side, or the fusion product of the shorter side, which avoids that of
# the longer side (`vec`: products of `ProductSector`s have a shape)
cs = if N₁ == 0 || N₂ == 0
# the only unit that can couple: on the left of the first and on the right of the last sector
us = N₁ == 0 ? uncoupled[2] : uncoupled[1]
leftunit(first(us)) == rightunit(last(us)) ? [leftunit(first(us))] : I[]
else
sort!(collect(intersect(⊗(uncoupled[1]...), ⊗(uncoupled[2]...))))
sort!(vec(collect(⊗(uncoupled[N₁ <= N₂ ? 1 : 2]...))))
end

for c in cs,
f₂ in fusiontrees(uncoupled[2], c, isdual[2]),
f₁ in fusiontrees(uncoupled[1], c, isdual[1])
push!(trees, (f₁, f₂))
for c in cs
# the shorter side has fusion trees for every candidate, the longer side not necessarily
# (with several units, `c` must also have the units at the ends of the longer side)
us = N₁ <= N₂ ? uncoupled[2] : uncoupled[1]
UnitStyle(I) isa SimpleUnit || N₁ == 0 || N₂ == 0 ||
(leftunit(c) == leftunit(first(us)) && rightunit(c) == rightunit(last(us))) || continue
longer = N₁ <= N₂ ? fusiontrees(uncoupled[2], c, isdual[2]) : fusiontrees(uncoupled[1], c, isdual[1])
isempty(longer) && continue
for f₂ in fusiontrees(uncoupled[2], c, isdual[2]),
f₁ in fusiontrees(uncoupled[1], c, isdual[1])
push!(trees, (f₁, f₂))
end
end
return FusionTreeBlock(trees)
end
Expand Down
17 changes: 11 additions & 6 deletions src/spaces/homspace.jl
Original file line number Diff line number Diff line change
Expand Up @@ -210,13 +210,18 @@ grouped by their uncoupled charges.
function fusionblocks(W::HomSpace)
Comment thread
leburgel marked this conversation as resolved.
I = sectortype(W)
N₁, N₂ = numout(W), numin(W)
isdual_src = (map(isdual, codomain(W)), map(isdual, domain(W)))
fblocks = Vector{FusionTreeBlock{I, N₁, N₂, fusiontreetype(I, N₁, N₂)}}()
for dom_uncoupled_src in sectors(domain(W)), cod_uncoupled_src in sectors(codomain(W))
fs_src = FusionTreeBlock{I}((cod_uncoupled_src, dom_uncoupled_src), isdual_src)
isempty(fs_src) || push!(fblocks, fs_src)
F = fusiontreetype(I, N₁, N₂)
# group the (cached) fusion trees of `W` by their uncoupled sectors
groups = Dict{Tuple{NTuple{N₁, I}, NTuple{N₂, I}}, Vector{F}}()
for (f₁, f₂) in fusiontrees(W)
push!(get!(Vector{F}, groups, (f₁.uncoupled, f₂.uncoupled)), (f₁, f₂))
end
return fblocks
# in the order of `sectors(domain(W))` (outer) and `sectors(codomain(W))` (inner), each of
# which runs fastest over its first space (the sectors of a dual space are sorted by their dual)
key(V, s) = isdual(V) ? dual(s) : s
rank((u₁, u₂)) = (reverse(map(key, domain(W).spaces, u₂)), reverse(map(key, codomain(W).spaces, u₁)))
us = collect(keys(groups))
return map(i -> FusionTreeBlock(groups[us[i]]), sortperm(map(rank, us))) # each rank computed once
end

function diagonalblockstructure(W::HomSpace)
Expand Down
15 changes: 15 additions & 0 deletions test/symmetries/doubletree.jl
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,21 @@ using TensorKitSectors
end
end

UnitStyle(I) isa SimpleUnit && @testset "Double fusion tree: blocks of a HomSpace" begin
V = Vect[I](Dict(s => 1 for s in (out..., incoming)))
W = V ⊗ V' ← V ⊗ V' # never empty: the unit couples both sides
trees = collect(fusiontrees(W))
blocks = TK.fusionblocks(W)
# every block holds the trees of `W` with its uncoupled sectors, in the same order
for block in blocks
@test fusiontrees(block) == filter(f -> (f[1].uncoupled, f[2].uncoupled) == block.uncoupled, trees)
end
@test sum(length, blocks) == length(trees)
# the blocks are ordered as `sectors(domain(W))` (outer) and `sectors(codomain(W))` (inner)
uncoupled = [(u₁, u₂) for u₂ in sectors(domain(W)) for u₁ in sectors(codomain(W))]
@test map(block -> block.uncoupled, blocks) == filter(u -> any(f -> (f[1].uncoupled, f[2].uncoupled) == u, trees), uncoupled)
end

@testset "Double fusion tree: planar trace" begin
if FusionStyle(I) isa UniqueFusion
f1, f1 = src
Expand Down
Loading