Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b30f205
specialise dim to storage type
borisdevos Aug 11, 2026
41f43fd
oplus and ominus
borisdevos Aug 11, 2026
619804d
infimum and supremum
borisdevos Aug 11, 2026
0967bd0
fuse
borisdevos Aug 11, 2026
60186d5
truncate_space
borisdevos Aug 11, 2026
8f90be2
restore binary search for sectordicts
borisdevos Aug 11, 2026
ae10f5b
refactor sorted merge procedure
borisdevos Aug 11, 2026
47a5f34
speed up fuse slightly with sortperm
borisdevos Aug 11, 2026
411dc44
import thing
borisdevos Aug 11, 2026
0adec3b
splat with type annotation above Val
borisdevos Aug 12, 2026
13677f3
actually don't splat, but construct directly where previously a vecto…
borisdevos Aug 21, 2026
399e121
make slightly more readable maybe perhaps
borisdevos Aug 21, 2026
d2a41f1
truncate_space always has non-dual entry spaces
borisdevos Aug 25, 2026
d7bdb07
overkill iszero check in dim
borisdevos Aug 25, 2026
271e96b
assert truncate_space spaces being non-dual
borisdevos Aug 26, 2026
335785f
introduce `sectorstoragetype`
borisdevos Aug 28, 2026
d79931e
specialise truncation code to sectorstoragetype
borisdevos Aug 28, 2026
d42aa28
put `_sortedmerge` in `Base.mergewith` and use where possible
borisdevos Aug 28, 2026
f8b0288
introduce and use `blockdims`
borisdevos Sep 1, 2026
2fbf6fe
introduce the ntuple cutoff
borisdevos Sep 2, 2026
2a3da90
Merge branch 'main' of https://github.com/QuantumKitHub/TensorKit.jl …
borisdevos Sep 2, 2026
e5877dc
apply code suggestions
borisdevos Sep 4, 2026
820d222
slight refactor of sortmerge implementation
lkdvos Sep 8, 2026
09e0d6f
introduce `FullVectorDict` and use it
borisdevos Sep 9, 2026
040b8c3
remove some reminder comments
borisdevos Sep 9, 2026
24186a0
Merge branch 'main' of https://github.com/QuantumKitHub/TensorKit.jl …
borisdevos Sep 9, 2026
b653bbf
avoid `NTuple{N,Int}(::Vector)` conversion cliff at N == 32
lkdvos Sep 9, 2026
8808e76
build tuple storage in a `MutableNTuple` with a bitmask
lkdvos Sep 9, 2026
dd3da4e
specialise `flip` to tuple storage
lkdvos Sep 9, 2026
4855f8f
make `ominus` a single pass
lkdvos Sep 9, 2026
7d474d7
replace `_builddensemap` by `sectormap` in dicts.jl
lkdvos Sep 9, 2026
2779e27
document why `fuse` accumulates into a `Dict`
lkdvos Sep 9, 2026
65752de
Merge branch 'main' into bd/gradedspace-storage
lkdvos Sep 9, 2026
34cf1c8
Merge branch 'main' into bd/gradedspace-storage
lkdvos Sep 10, 2026
aa04e15
address review: drop `FullVectorDict` and the `sectormap` paradigm
lkdvos Sep 15, 2026
192cf48
fix `pairs(::SectorVector)` for non-`SubArray` views
lkdvos Sep 15, 2026
d1802c5
review: storage-variant aliases, shared merge handlers, lazy `pairs`
lkdvos Sep 15, 2026
47a876f
Merge branch 'main' into bd/gradedspace-storage
lkdvos Sep 15, 2026
1ea0e8c
Apply batched suggestions from code review [skip ci]
lkdvos Sep 15, 2026
f26eccc
review: drop StaticLength, simplify truncate_space, avoid re-sorting …
lkdvos Sep 16, 2026
5521fac
test: fix pairs(::SectorVector) comparison against Dict
lkdvos Sep 16, 2026
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
2 changes: 2 additions & 0 deletions docs/src/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ When releasing a new version, move the "Unreleased" changes to a new version sec
- For sector types with `GenericUnit` such that colorings are not unique, `GradedSpace`, `ProductSpace` and `HomSpace` now check for this compatibility. In particular, this prevents the construction of `TensorMap`s with incompatible colorings, which previously either errored or produced empty tensors inconsistently. ([#515](https://github.com/QuantumKitHub/TensorKit.jl/pull/515))

### Deprecated
- The type alias `ZNSpace{N}` is deprecated in favour of `Vect[ZNIrrep{N}]` or `Rep[ℤ{N}]`: a type alias cannot compute the storage type from `N`, so the two only agree for small `N`. ([#511](https://github.com/QuantumKitHub/TensorKit.jl/pull/511))

### Removed

### Fixed

### Performance
- `GradedSpace` operations (`dim`, `flip`, `⊕`, `⊖`, `fuse`, `infimum`, `supremum`, truncation) are now specialised on the storage type of the degeneracy dimensions, and tuple storage is used only for sector types with at most `TensorKit._NTUPLE_STORAGE_THRESHOLD` sectors so that sector types with many sectors no longer burden the compiler. ([#511](https://github.com/QuantumKitHub/TensorKit.jl/pull/511))

## [0.17.1](https://github.com/QuantumKitHub/TensorKit.jl/compare/v0.17.0...v0.17.1) - 2026-07-13

Expand Down
13 changes: 9 additions & 4 deletions docs/src/lib/spaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,18 @@ Vect
Rep
```

The storage that `Vect[I]` selects for the degeneracy dimensions depends on how many sectors `I` has:

```@docs
TensorKit.sectorstoragetype
```

In this respect, there are also a number of type aliases for the `GradedSpace` types associated with the most common sectors, namely

```julia
const ZNSpace{N} = Vect[ZNIrrep{N}]
const Z2Space = ZNSpace{2}
const Z3Space = ZNSpace{3}
const Z4Space = ZNSpace{4}
const Z2Space = Rep[ℤ{2}]
const Z3Space = Rep[ℤ{3}]
const Z4Space = Rep[ℤ{4}]
const U1Space = Rep[U₁]
const CU1Space = Rep[CU₁]
const SU2Space = Rep[SU₂]
Expand Down
7 changes: 4 additions & 3 deletions docs/src/man/gradedspaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,17 @@ However, this is mostly to lower the barrier, as really the instances of `Graded
## Implementation details

As mentioned, the way in which the degeneracy dimensions ``n_a`` are stored depends on the specific sector type `I`, more specifically on the `IteratorSize` of `values(I)`.
If `IteratorSize(values(I)) isa Union{IsInfinite, SizeUnknown}`, the different sectors ``a`` and their corresponding degeneracy ``n_a`` are stored as key value pairs in an `Associative` array, i.e. a dictionary `dims::SectorDict`.
If `IteratorSize(values(I)) isa Union{IsInfinite, SizeUnknown}`, or if `values(I)` has a known length that exceeds `TensorKit._NTUPLE_STORAGE_THRESHOLD`, the different sectors ``a`` and their corresponding degeneracy ``n_a`` are stored as key value pairs in an `Associative` array, i.e. a dictionary `dims::SectorDict`.
As the total number of sectors in `values(I)` can be infinite, only sectors ``a`` for which ``n_a`` are stored.
Here, `SectorDict` is a constant type alias for a specific dictionary implementation, which currently resorts to `SortedVectorDict` implemented in TensorKit.jl.
Hence, the sectors and their corresponding dimensions are stored as two matching lists (`Vector` instances), which are ordered based on the property `isless(a::I, b::I)`.
This ensures that the space ``V = ⨁_a ℂ^{n_a} ⊗ R_{a}`` has some unique canonical order in the direct sum decomposition, such that two different but equal instances created independently always match.

If `IteratorSize(values(I)) isa Union{HasLength, HasShape}`, the degeneracy dimensions `n_a` are stored for all sectors `a ∈ values(I)` (also if `n_a == 0`) in a tuple, more specifically a `NTuple{N, Int}` with `N = length(values(I))`.
If `IteratorSize(values(I)) isa Union{HasLength, HasShape}` and `N = length(values(I))` is at most `TensorKit._NTUPLE_STORAGE_THRESHOLD`, the degeneracy dimensions `n_a` are stored for all sectors `a ∈ values(I)` (also if `n_a == 0`) in a tuple, more specifically a `NTuple{N, Int}`.
The methods `getindex(values(I), i)` and `findindex(values(I), a)` are used to map between a sector `a ∈ values(I)` and a corresponding index `i ∈ 1:N`.
As `N` is a compile time constant, these types can be created in a type stable manner.
Note however that this implies that for large values of `N`, it can be beneficial to define `IteratorSize(values(a)) = SizeUnknown()` to not overly burden the compiler.
For larger `N` this would overly burden the compiler, which is precisely why the dictionary storage takes over above the threshold.
The exact threshold is documented with [`TensorKit.sectorstoragetype`](@ref), which reports the storage type of a given sector type; the canonical space type is always obtained as `Vect[I]`.

## Constructing instances

Expand Down
3 changes: 1 addition & 2 deletions src/TensorKit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export infimum, supremum, isisomorphic, ismonomorphic, isepimorphic
export sectortype, sectors, hassector
export unit, rightunit, leftunit, allunits, isunit, otimes, deligneproduct, timereversed
export Nsymbol, Fsymbol, Rsymbol, Bsymbol, frobenius_schur_phase, frobenius_schur_indicator, twist, fusiontensor
export sectorscalartype, fusionscalartype, braidingscalartype
export sectorscalartype, fusionscalartype, braidingscalartype, dimscalartype

# Export methods for fusion trees
export fusiontrees, braid, permute, transpose
Expand Down Expand Up @@ -107,7 +107,6 @@ export empty_globalcaches!
# Imports
#---------
using TupleTools
using TupleTools: StaticLength

using Strided

Expand Down
87 changes: 72 additions & 15 deletions src/auxiliary/dicts.jl
Original file line number Diff line number Diff line change
Expand Up @@ -89,21 +89,12 @@ end
Base.empty(::SortedVectorDict, ::Type{K}, ::Type{V}) where {K, V} = SortedVectorDict{K, V}()
Base.empty!(d::SortedVectorDict) = (empty!(d.keys); empty!(d.values); return d)

# _searchsortedfirst(v::Vector, k) = searchsortedfirst(v, k)
function _searchsortedfirst(v::Vector, k)
i = 1
@inbounds while i <= length(v) && isless(v[i], k)
i += 1
end
return i
end

function Base.delete!(d::SortedVectorDict{K}, k) where {K}
key = convert(K, k)
if !isequal(k, key)
return d
end
i = _searchsortedfirst(d.keys, key)
i = searchsortedfirst(d.keys, key)
if i <= length(d) && isequal(d.keys[i], key)
deleteat!(d.keys, i)
deleteat!(d.values, i)
Expand All @@ -118,15 +109,15 @@ function Base.haskey(d::SortedVectorDict{K}, k) where {K}
if !isequal(k, key)
return false
end
i = _searchsortedfirst(d.keys, key)
i = searchsortedfirst(d.keys, key)
return (i <= length(d) && isequal(d.keys[i], key))
end
function Base.getindex(d::SortedVectorDict{K}, k) where {K}
key = convert(K, k)
if !isequal(k, key)
throw(KeyError(k))
end
i = _searchsortedfirst(d.keys, key)
i = searchsortedfirst(d.keys, key)
@inbounds if (i <= length(d) && isequal(d.keys[i], key))
return d.values[i]
else
Expand All @@ -138,7 +129,7 @@ function Base.setindex!(d::SortedVectorDict{K}, v, k) where {K}
if !isequal(k, key)
throw(ArgumentError("$k is not a valid key for type $K"))
end
i = _searchsortedfirst(d.keys, key)
i = searchsortedfirst(d.keys, key)
if i <= length(d) && isequal(d.keys[i], key)
d.values[i] = v
else
Expand All @@ -153,7 +144,7 @@ function Base.get(d::SortedVectorDict{K}, k, default) where {K}
if !isequal(k, key)
return default
end
i = _searchsortedfirst(d.keys, key)
i = searchsortedfirst(d.keys, key)
@inbounds begin
return (i <= length(d) && isequal(d.keys[i], key)) ? d.values[i] : default
end
Expand All @@ -163,7 +154,7 @@ function Base.get(f::Union{Function, Type}, d::SortedVectorDict{K}, k) where {K}
if !isequal(k, key)
return f()
end
i = _searchsortedfirst(d.keys, key)
i = searchsortedfirst(d.keys, key)
@inbounds begin
return (i <= length(d) && isequal(d.keys[i], key)) ? d.values[i] : f()
end
Expand All @@ -186,6 +177,72 @@ function Base.:(==)(d1::SortedVectorDict, d2::SortedVectorDict)
return true
end

# merge two SortedVectorDicts of `GradedSpace` dimensions: `combine(v1, v2)` is applied to keys
# present in both, `unmatched1(v)`/`unmatched2(v)` to keys present in only the first/second dict,
# which may keep the value, drop the entry by returning `nothing`, or throw. Zero results are
# always dropped, since `GradedSpace` never stores an explicit zero dimension.
function _sortedmerge(
combine::F, unmatched1::F1, unmatched2::F2,
d1::SortedVectorDict{K, V}, d2::SortedVectorDict{K, V}
) where {F, F1, F2, K, V <: Integer}
k1, v1 = d1.keys, d1.values
k2, v2 = d2.keys, d2.values
n1, n2 = length(k1), length(k2)
len = _mergelength(unmatched1, unmatched2, n1, n2)
ks = Vector{K}(undef, len)
vs = Vector{V}(undef, len)
i, j, n = 1, 1, 0
@inbounds while i <= n1 && j <= n2
a, b = k1[i], k2[j]
if isless(a, b)
n = _mergestore!(ks, vs, n, a, unmatched1(v1[i]))
i += 1
elseif isless(b, a)
n = _mergestore!(ks, vs, n, b, unmatched2(v2[j]))
j += 1
else
n = _mergestore!(ks, vs, n, a, combine(v1[i], v2[j]))
i += 1
j += 1
end
end
@inbounds while i <= n1
n = _mergestore!(ks, vs, n, k1[i], unmatched1(v1[i]))
i += 1
end
@inbounds while j <= n2
n = _mergestore!(ks, vs, n, k2[j], unmatched2(v2[j]))
j += 1
end
resize!(ks, n)
resize!(vs, n)
return SortedVectorDict{K, V}(ks, vs)
end
# write into slot `n + 1` and only advance the length when the value is nonzero
@inline function _mergestore!(ks, vs, n, k, d)
@inbounds ks[n + 1] = k
@inbounds vs[n + 1] = d
return n + !iszero(d)
Comment thread
Jutho marked this conversation as resolved.
end
@inline _mergestore!(ks, vs, n, k, ::Nothing) = n

# upper bound on the number of entries the merge can produce: a dropping handler contributes none
function _mergelength(unmatched1, unmatched2, n1, n2)
drop = Returns(nothing)
return if unmatched1 == drop
unmatched2 == drop ? min(n1, n2) : n2
else
unmatched2 == drop ? n1 : n1 + n2
end
end

function Base.mergewith(combine, d1::SortedVectorDict{K, V}, d2::SortedVectorDict{K, V}) where {K, V <: Integer}
# keys occurring in only one of the dicts are kept, except for `min`, where a missing sector
# has dimension zero and thus drops out of the result
unmatched = combine == min ? Returns(nothing) : identity
return _sortedmerge(combine, unmatched, unmatched, d1, d2)
end

"""
Hashed(value, hashfunction = Base.hash, isequal = Base.isequal)

Expand Down
3 changes: 2 additions & 1 deletion src/factorizations/factorizations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ module Factorizations
export copy_oftype, factorisation_scalartype, one!, truncspace

using ..TensorKit
using ..TensorKit: AdjointTensorMap, SectorDict, SectorVector,
using ..TensorKit: AdjointTensorMap, DictGradedSpace, SectorDict, SectorVector,
TupleGradedSpace,
blocktype, foreachblock, one!,
similar_diagonal, similarstoragetype
using ..TensorKit: GLOBAL_TIMER
Expand Down
4 changes: 2 additions & 2 deletions src/factorizations/pullbacks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ for pullback! in (:svd_pullback!, :eig_pullback!, :eigh_pullback!)
kwargs...
)
foreachblock(Δt, t) do c, (Δb, b)
haskey(inds, c) || return nothing
ind = inds[c]
ind = get(inds, c, nothing)
isnothing(ind) && return nothing
Fc = block.(F, Ref(c))
ΔFc = block.(ΔF, Ref(c))
MAK.$pullback!(Δb, b, Fc, ΔFc, ind; kwargs...)
Expand Down
Loading
Loading