From 701c5497e80ce91c3783924e05a1593c2e043e2e Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Fri, 30 Jan 2026 11:35:56 -0500 Subject: [PATCH 01/14] add utility constructor --- src/tensors/tensor.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/tensors/tensor.jl b/src/tensors/tensor.jl index 526f4e489..6e6cce626 100644 --- a/src/tensors/tensor.jl +++ b/src/tensors/tensor.jl @@ -105,6 +105,11 @@ TensorMapWithStorage{T, A}(::UndefInitializer, codomain::TensorSpace, domain::Te TensorMapWithStorage{T, A}(undef, codomain ← domain) TensorWithStorage{T, A}(::UndefInitializer, V::TensorSpace) where {T, A} = TensorMapWithStorage{T, A}(undef, V ← one(V)) +# Utility constructors +# -------------------- +TensorMap(t::TensorMap) = copy(t) + + # raw data constructors # --------------------- # - dispatch starts with TensorMap{T}(::DenseVector{T}, ...) From 157915fd22cdbb0c4c34bca2a19d1c1bc546fe63 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Fri, 30 Jan 2026 11:53:04 -0500 Subject: [PATCH 02/14] add `check_spacetype` utility function --- src/planar/planaroperations.jl | 3 +-- src/spaces/vectorspaces.jl | 26 +++++++++++++++++++------- src/tensors/diagonal.jl | 4 ++-- src/tensors/indexmanipulations.jl | 4 ++-- src/tensors/linalg.jl | 5 ++--- src/tensors/tensoroperations.jl | 9 ++++----- 6 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/planar/planaroperations.jl b/src/planar/planaroperations.jl index a06f4431d..982a848f8 100644 --- a/src/planar/planaroperations.jl +++ b/src/planar/planaroperations.jl @@ -69,8 +69,7 @@ function planartrace!( α::Number, β::Number, backend, allocator ) - (S = spacetype(C)) == spacetype(A) || - throw(SpaceMismatch("incompatible spacetypes")) + S = check_spacetype(C, A) if BraidingStyle(sectortype(S)) == Bosonic() return trace_permute!(C, A, (p₁, p₂), (q₁, q₂), α, β, backend) end diff --git a/src/spaces/vectorspaces.jl b/src/spaces/vectorspaces.jl index 5ef171c31..867d18959 100644 --- a/src/spaces/vectorspaces.jl +++ b/src/spaces/vectorspaces.jl @@ -376,19 +376,31 @@ abstract type CompositeSpace{S <: ElementarySpace} <: VectorSpace end InnerProductStyle(::Type{<:CompositeSpace{S}}) where {S} = InnerProductStyle(S) """ - spacetype(a) -> Type{S<:IndexSpace} - spacetype(::Type) -> Type{S<:IndexSpace} + spacetype(a) -> Type{S <: IndexSpace} + spacetype(::Type) -> Type{S <: IndexSpace} -Return the type of the elementary space `S` of object `a` (e.g. a tensor). Also works in -type domain. +Return the type of the elementary space `S` of object `a` (e.g. a tensor). +Also works in type domain. """ spacetype(x) = spacetype(typeof(x)) -function spacetype(::Type{T}) where {T} - throw(MethodError(spacetype, (T,))) -end +spacetype(::Type{T}) where {T} = throw(MethodError(spacetype, (T,))) spacetype(::Type{E}) where {E <: ElementarySpace} = E spacetype(::Type{S}) where {E, S <: CompositeSpace{E}} = E +""" + check_spacetype(Bool, x, y, z...) -> Bool + check_spacetype(x, y, z...) -> Type{<:IndexSpace} + +Check whether the given inputs have matching spacetypes. +The first signature returns a `Bool` to indicate whether all spacetypes are equal, +while the second will return the spacetype if all types are equal, and throw a [`SpaceMismatch`](@ref) if not. +""" +check_spacetype(::Type{Bool}, x, y, z...) = _allequal(spacetype, (x, y, z...)) +@noinline function check_spacetype(x, y, z...) + check_spacetype(Bool, x, y, z...) || throw(SpaceMismatch("incompatible space types")) + return spacetype(x) +end + # make ElementarySpace instances behave similar to ProductSpace instances blocksectors(V::ElementarySpace) = collect(sectors(V)) blockdim(V::ElementarySpace, c::Sector) = dim(V, c) diff --git a/src/tensors/diagonal.jl b/src/tensors/diagonal.jl index c191bb6b5..4064ea910 100644 --- a/src/tensors/diagonal.jl +++ b/src/tensors/diagonal.jl @@ -263,8 +263,8 @@ function TO.tensorcontract_type( M = similarstoragetype(A, TC) M == similarstoragetype(B, TC) || throw(ArgumentError("incompatible storage types:\n$(M) ≠ $(similarstoragetype(B, TC))")) - spacetype(A) == spacetype(B) || throw(SpaceMismatch("incompatible space types")) - return DiagonalTensorMap{TC, spacetype(A), M} + S = check_spacetype(A, B) + return DiagonalTensorMap{TC, S, M} end function TO.tensoralloc( diff --git a/src/tensors/indexmanipulations.jl b/src/tensors/indexmanipulations.jl index 0c918e85e..d6a073ec9 100644 --- a/src/tensors/indexmanipulations.jl +++ b/src/tensors/indexmanipulations.jl @@ -413,7 +413,7 @@ end spacecheck_transform(f, tdst::AbstractTensorMap, tsrc::AbstractTensorMap, args...) = spacecheck_transform(f, space(tdst), space(tsrc), args...) @noinline function spacecheck_transform(f, Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple) - spacetype(Vdst) == spacetype(Vsrc) || throw(SectorMismatch("incompatible sector types")) + check_spacetype(Vdst, Vsrc) f(Vsrc, p) == Vdst || throw( SpaceMismatch( @@ -427,7 +427,7 @@ spacecheck_transform(f, tdst::AbstractTensorMap, tsrc::AbstractTensorMap, args.. return nothing end @noinline function spacecheck_transform(::typeof(braid), Vdst::TensorMapSpace, Vsrc::TensorMapSpace, p::Index2Tuple, levels::IndexTuple) - spacetype(Vdst) == spacetype(Vsrc) || throw(SectorMismatch("incompatible sector types")) + check_spacetype(Vdst, Vsrc) braid(Vsrc, p, levels) == Vdst || throw( SpaceMismatch( diff --git a/src/tensors/linalg.jl b/src/tensors/linalg.jl index d174bef71..e122821a0 100644 --- a/src/tensors/linalg.jl +++ b/src/tensors/linalg.jl @@ -538,8 +538,7 @@ absorb(tdst::AbstractTensorMap, tsrc::AbstractTensorMap) = absorb!(copy(tdst), t function absorb!(tdst::AbstractTensorMap, tsrc::AbstractTensorMap) numin(tdst) == numin(tsrc) && numout(tdst) == numout(tsrc) || throw(DimensionError("Incompatible number of indices for source and destination")) - S = spacetype(tdst) - S == spacetype(tsrc) || throw(SpaceMismatch("incompatible spacetypes")) + S = check_spacetype(tdst, tsrc) dom = mapreduce(infimum, ⊗, domain(tdst), domain(tsrc); init = one(S)) cod = mapreduce(infimum, ⊗, codomain(tdst), codomain(tsrc); init = one(S)) for (f1, f2) in fusiontrees(cod ← dom) @@ -561,7 +560,7 @@ new `TensorMap` instance whose codomain is `codomain(t1) ⊗ codomain(t2)` and w is `domain(t1) ⊗ domain(t2)`. """ function ⊗(A::AbstractTensorMap, B::AbstractTensorMap) - (S = spacetype(A)) === spacetype(B) || throw(SpaceMismatch("incompatible space types")) + check_spacetype(A, B) # allocate destination with correct scalartype pA = ((codomainind(A)..., domainind(A)...), ()) diff --git a/src/tensors/tensoroperations.jl b/src/tensors/tensoroperations.jl index 6b01d1b2c..9454dcf87 100644 --- a/src/tensors/tensoroperations.jl +++ b/src/tensors/tensoroperations.jl @@ -103,7 +103,7 @@ end VB::TensorMapSpace, pB::Index2Tuple, conjB::Bool, pAB::Index2Tuple ) - spacetype(VC) == spacetype(VA) == spacetype(VB) || throw(SectorMismatch("incompatible sector types")) + check_spacetype(VC, VA, VB) TO.tensorcontract(VA, pA, conjA, VB, pB, conjB, pAB) == VC || throw( SpaceMismatch( @@ -153,11 +153,11 @@ function TO.tensorcontract_type( B::AbstractTensorMap, ::Index2Tuple, ::Bool, ::Index2Tuple{N₁, N₂} ) where {N₁, N₂} - spacetype(A) == spacetype(B) || throw(SpaceMismatch("incompatible space types")) + S = check_spacetype(A, B) I = sectortype(A) TC′ = isreal(I) ? TC : complex(TC) M = promote_storagetype(similarstoragetype(A, TC′), similarstoragetype(B, TC′)) - return tensormaptype(spacetype(A), N₁, N₂, M) + return tensormaptype(S, N₁, N₂, M) end # TODO: handle actual promotion rule system @@ -213,8 +213,7 @@ function trace_permute!( backend = TO.DefaultBackend() ) # some input checks - (S = spacetype(tdst)) == spacetype(tsrc) || - throw(SpaceMismatch("incompatible spacetypes")) + S = check_spacetype(tdst, tsrc) if !(BraidingStyle(sectortype(S)) isa SymmetricBraiding) throw(SectorMismatch("only tensors with symmetric braiding rules can be contracted; try `@planar` instead")) end From 05b997ef191a6124b6003029fb27b00cb8e6b171 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Fri, 30 Jan 2026 14:59:26 -0500 Subject: [PATCH 03/14] `compose_dest` without complex promotion --- src/tensors/diagonal.jl | 9 +++++++++ src/tensors/linalg.jl | 16 +++++++--------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/tensors/diagonal.jl b/src/tensors/diagonal.jl index 4064ea910..ed3ae42c5 100644 --- a/src/tensors/diagonal.jl +++ b/src/tensors/diagonal.jl @@ -291,6 +291,15 @@ function Base.zero(d::DiagonalTensorMap) return DiagonalTensorMap(zero.(d.data), d.domain) end +function compose_dest(A::DiagonalTensorMap, B::DiagonalTensorMap) + S = check_spacetype(A, B) + TC = TO.promote_contract(scalartype(A), scalartype(B), One) + M = promote_storagetype(similarstoragetype(A, TC), similarstoragetype(B, TC)) + TTC = DiagonalTensorMap{TC, S, M} + structure = codomain(A) ← domain(B) + return TO.tensoralloc(TTC, structure, Val(false)) +end + function LinearAlgebra.mul!( dC::DiagonalTensorMap, dA::DiagonalTensorMap, dB::DiagonalTensorMap, α::Number, β::Number ) diff --git a/src/tensors/linalg.jl b/src/tensors/linalg.jl index e122821a0..e28ec8153 100644 --- a/src/tensors/linalg.jl +++ b/src/tensors/linalg.jl @@ -19,17 +19,15 @@ LinearAlgebra.normalize!(t::AbstractTensorMap, p::Real = 2) = scale!(t, inv(norm LinearAlgebra.normalize(t::AbstractTensorMap, p::Real = 2) = scale(t, inv(norm(t, p))) # destination allocation for matrix multiplication +# note that we don't fall back to `tensoralloc_contract` since that needs to account for +# permutations, which might require complex scalartypes even if the inputs are real. function compose_dest(A::AbstractTensorMap, B::AbstractTensorMap) + S = check_spacetype(A, B) TC = TO.promote_contract(scalartype(A), scalartype(B), One) - pA = (codomainind(A), domainind(A)) - pB = (codomainind(B), domainind(B)) - pAB = (codomainind(A), ntuple(i -> i + numout(A), numin(B))) - return TO.tensoralloc_contract( - TC, - A, pA, false, - B, pB, false, - pAB, Val(false) - ) + M = promote_storagetype(similarstoragetype(A, TC), similarstoragetype(B, TC)) + TTC = tensormaptype(S, numout(A), numin(B), M) + structure = codomain(A) ← domain(B) + return TO.tensoralloc(TTC, structure, Val(false)) end """ From f878cc729af7778bc2fb853e09d2f0d9fecf6e14 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Fri, 30 Jan 2026 14:59:36 -0500 Subject: [PATCH 04/14] use `promote_storagetype` --- src/tensors/diagonal.jl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/tensors/diagonal.jl b/src/tensors/diagonal.jl index ed3ae42c5..169ef38c9 100644 --- a/src/tensors/diagonal.jl +++ b/src/tensors/diagonal.jl @@ -260,10 +260,8 @@ function TO.tensorcontract_type( B::DiagonalTensorMap, ::Index2Tuple{1, 1}, ::Bool, ::Index2Tuple{1, 1} ) - M = similarstoragetype(A, TC) - M == similarstoragetype(B, TC) || - throw(ArgumentError("incompatible storage types:\n$(M) ≠ $(similarstoragetype(B, TC))")) S = check_spacetype(A, B) + M = promote_storagetype(similarstoragetype(A, TC), similarstoragetype(B, TC)) return DiagonalTensorMap{TC, S, M} end From 48a9fe31178fbc7841a40ad564d162933250b7bb Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Fri, 30 Jan 2026 14:59:44 -0500 Subject: [PATCH 05/14] twist! with complex promotion --- src/tensors/indexmanipulations.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/tensors/indexmanipulations.jl b/src/tensors/indexmanipulations.jl index d6a073ec9..a08504c32 100644 --- a/src/tensors/indexmanipulations.jl +++ b/src/tensors/indexmanipulations.jl @@ -296,6 +296,10 @@ function twist!(t::AbstractTensorMap, inds; inv::Bool = false) throw(ArgumentError(msg)) end has_shared_twist(t, inds) && return t + + (scalartype(t) <: Real && !(sectorscalartype(sectortype(t)) <: Real)) && + throw(ArgumentError("No in-place `twist!` for a real tensor with complex sector type")) + N₁ = numout(t) for (f₁, f₂) in fusiontrees(t) θ = prod(i -> i <= N₁ ? twist(f₁.uncoupled[i]) : twist(f₂.uncoupled[i - N₁]), inds) @@ -317,7 +321,8 @@ See [`twist!`](@ref) for storing the result in place. """ function twist(t::AbstractTensorMap, inds; inv::Bool = false, copy::Bool = false) !copy && has_shared_twist(t, inds) && return t - return twist!(Base.copy(t), inds; inv) + tdst = similar(t, sectorscalartype(sectortype(t)) <: Real ? scalartype(t) : complex(scalartype(t))) + return twist!(tdst, inds; inv) end # Methods which change the number of indices, implement using `Val(i)` for type inference From 7de05cb36a0497f0e72e76056a088c6d58043291 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Fri, 30 Jan 2026 15:01:23 -0500 Subject: [PATCH 06/14] flip with complex promotion --- src/tensors/indexmanipulations.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tensors/indexmanipulations.jl b/src/tensors/indexmanipulations.jl index a08504c32..2c5b5e7ae 100644 --- a/src/tensors/indexmanipulations.jl +++ b/src/tensors/indexmanipulations.jl @@ -15,7 +15,7 @@ Return a new tensor that is isomorphic to `t` but where the arrows on the indice """ function flip(t::AbstractTensorMap, I; inv::Bool = false) P = flip(space(t), I) - t′ = similar(t, P) + t′ = sectorscalartype(sectortype(t)) <: Real ? similar(t, P) : similar(t, complex(scalartype(t)), P) for (f₁, f₂) in fusiontrees(t) (f₁′, f₂′), factor = only(flip(f₁, f₂, I; inv)) scale!(t′[f₁′, f₂′], t[f₁, f₂], factor) From 688434e981673d098ccab7fd21819844c430e200 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Fri, 30 Jan 2026 15:35:10 -0500 Subject: [PATCH 07/14] try and centralize tensor allocation --- src/tensors/abstracttensor.jl | 5 +++ src/tensors/diagonal.jl | 3 +- src/tensors/indexmanipulations.jl | 71 ++++++++++--------------------- src/tensors/tensoroperations.jl | 6 +-- 4 files changed, 31 insertions(+), 54 deletions(-) diff --git a/src/tensors/abstracttensor.jl b/src/tensors/abstracttensor.jl index 4b3149a4c..0f580c17c 100644 --- a/src/tensors/abstracttensor.jl +++ b/src/tensors/abstracttensor.jl @@ -103,6 +103,11 @@ similarstoragetype(::Type{D}, ::Type{T}) where {D <: AbstractDict{<:Sector, <:Ab similarstoragetype(::Type{T}) where {T <: Number} = Vector{T} +# helper function to determine the scalartype taking into account that recouplings might happen +recoupled_scalartype(::Type{T}, ::Type{I}) where {T <: Number, I <: Sector} = isreal(I) ? T : complex(T) +recoupled_scalartype(t::AbstractTensorMap) = recoupled_scalartype(typeof(t)) +recoupled_scalartype(::Type{T}) where {T <: AbstractTensorMap} = recoupled_scalartype(scalartype(T), sectortype(T)) + # tensor characteristics: space and index information #----------------------------------------------------- """ diff --git a/src/tensors/diagonal.jl b/src/tensors/diagonal.jl index 169ef38c9..f25a81961 100644 --- a/src/tensors/diagonal.jl +++ b/src/tensors/diagonal.jl @@ -261,7 +261,8 @@ function TO.tensorcontract_type( ::Index2Tuple{1, 1} ) S = check_spacetype(A, B) - M = promote_storagetype(similarstoragetype(A, TC), similarstoragetype(B, TC)) + TC′ = recoupled_scalartype(TC, sectortype(S)) + M = promote_storagetype(similarstoragetype(A, TC′), similarstoragetype(B, TC′)) return DiagonalTensorMap{TC, S, M} end diff --git a/src/tensors/indexmanipulations.jl b/src/tensors/indexmanipulations.jl index 2c5b5e7ae..041eec608 100644 --- a/src/tensors/indexmanipulations.jl +++ b/src/tensors/indexmanipulations.jl @@ -15,7 +15,7 @@ Return a new tensor that is isomorphic to `t` but where the arrows on the indice """ function flip(t::AbstractTensorMap, I; inv::Bool = false) P = flip(space(t), I) - t′ = sectorscalartype(sectortype(t)) <: Real ? similar(t, P) : similar(t, complex(scalartype(t)), P) + t′ = similar(t, recoupled_scalartype(t), P) for (f₁, f₂) in fusiontrees(t) (f₁′, f₂′), factor = only(flip(f₁, f₂, I; inv)) scale!(t′[f₁′, f₂′], t[f₁, f₂], factor) @@ -39,53 +39,35 @@ See [`permute`](@ref) for creating a new tensor and [`add_permute!`](@ref) for a end """ - permute(tsrc::AbstractTensorMap, (p₁, p₂)::Index2Tuple; - copy::Bool=false) - -> tdst::TensorMap + permute(tsrc::AbstractTensorMap, (p₁, p₂)::Index2Tuple; copy::Bool = false) -> tdst::TensorMap Return tensor `tdst` obtained by permuting the indices of `tsrc`. The codomain and domain of `tdst` correspond to the indices in `p₁` and `p₂` of `tsrc` respectively. -If `copy=false`, `tdst` might share data with `tsrc` whenever possible. Otherwise, a copy is always made. +If `copy = false`, `tdst` might share data with `tsrc` whenever possible. +Otherwise, a copy is always made. To permute into an existing destination, see [permute!](@ref) and [`add_permute!`](@ref) """ -function permute( - t::AbstractTensorMap, (p₁, p₂)::Index2Tuple{N₁, N₂}; copy::Bool = false - ) where {N₁, N₂} - space′ = permute(space(t), (p₁, p₂)) - # share data if possible - if !copy && p₁ === codomainind(t) && p₂ === domainind(t) - return t - end - # general case - @inbounds begin - return permute!(similar(t, space′), t, (p₁, p₂)) - end -end -function permute(t::TensorMap, (p₁, p₂)::Index2Tuple{N₁, N₂}; copy::Bool = false) where {N₁, N₂} - space′ = permute(space(t), (p₁, p₂)) +function permute(t::AbstractTensorMap, (p₁, p₂)::Index2Tuple; copy::Bool = false) # share data if possible if !copy if p₁ === codomainind(t) && p₂ === domainind(t) return t - elseif has_shared_permute(t, (p₁, p₂)) + elseif t isa TensorMap && has_shared_permute(t, (p₁, p₂)) return TensorMap(t.data, space′) end end + tdst = TO.tensoralloc_add(scalartype(t), t, (p₁, p₂), false, Val(false)) # general case - @inbounds begin - return permute!(similar(t, space′), t, (p₁, p₂)) - end + return @inbounds permute!(tdst, t, (p₁, p₂)) end function permute(t::AdjointTensorMap, (p₁, p₂)::Index2Tuple; copy::Bool = false) p₁′ = adjointtensorindices(t, p₂) p₂′ = adjointtensorindices(t, p₁) return adjoint(permute(adjoint(t), (p₁′, p₂′); copy)) end -function permute(t::AbstractTensorMap, p::IndexTuple; copy::Bool = false) - return permute(t, (p, ()); copy) -end +permute(t::AbstractTensorMap, p::IndexTuple; copy::Bool = false) = permute(t, (p, ()); copy) function has_shared_permute(t::AbstractTensorMap, (p₁, p₂)::Index2Tuple) return (p₁ === codomainind(t) && p₂ === domainind(t)) @@ -145,18 +127,14 @@ To braid into an existing destination, see [braid!](@ref) and [`add_braid!`](@re function braid( t::AbstractTensorMap, (p₁, p₂)::Index2Tuple, levels::IndexTuple; copy::Bool = false ) - @assert length(levels) == numind(t) - if BraidingStyle(sectortype(t)) isa SymmetricBraiding - return permute(t, (p₁, p₂); copy = copy) - end - if !copy && p₁ == codomainind(t) && p₂ == domainind(t) - return t - end + length(levels) == numind(t) || throw(ArgumentError("invalid levels")) + + BraidingStyle(sectortype(t)) isa SymmetricBraiding && return permute(t, (p₁, p₂); copy = copy) + (!copy && p₁ == codomainind(t) && p₂ == domainind(t)) && return t + # general case - space′ = permute(space(t), (p₁, p₂)) - @inbounds begin - return braid!(similar(t, space′), t, (p₁, p₂), levels) - end + tdst = TO.tensoralloc_add(scalartype(t), t, (p₁, p₂), false, Val(false)) + return @inbounds braid!(tdst, t, (p₁, p₂), levels) end # TODO: braid for `AdjointTensorMap`; think about how to map the `levels` argument. @@ -199,17 +177,12 @@ function LinearAlgebra.transpose( t::AbstractTensorMap, (p₁, p₂)::Index2Tuple = _transpose_indices(t); copy::Bool = false ) - if sectortype(t) === Trivial - return permute(t, (p₁, p₂); copy = copy) - end - if !copy && p₁ == codomainind(t) && p₂ == domainind(t) - return t - end + sectortype(t) === Trivial && return permute(t, (p₁, p₂); copy) + (!copy && p₁ == codomainind(t) && p₂ == domainind(t)) && return t + # general case - space′ = permute(space(t), (p₁, p₂)) - @inbounds begin - return transpose!(similar(t, space′), t, (p₁, p₂)) - end + tdst = TO.tensoralloc_add(scalartype(t), t, (p₁, p₂), false, Val(false)) + return @inbounds transpose!(tdst, t, (p₁, p₂)) end function LinearAlgebra.transpose( @@ -321,7 +294,7 @@ See [`twist!`](@ref) for storing the result in place. """ function twist(t::AbstractTensorMap, inds; inv::Bool = false, copy::Bool = false) !copy && has_shared_twist(t, inds) && return t - tdst = similar(t, sectorscalartype(sectortype(t)) <: Real ? scalartype(t) : complex(scalartype(t))) + tdst = TO.tensoralloc_add(scalartype(t), (codomainind(t), domainind(t)), false, Val(false)) return twist!(tdst, inds; inv) end diff --git a/src/tensors/tensoroperations.jl b/src/tensors/tensoroperations.jl index 9454dcf87..bc5d4beeb 100644 --- a/src/tensors/tensoroperations.jl +++ b/src/tensors/tensoroperations.jl @@ -54,8 +54,7 @@ end function TO.tensoradd_type( TC, A::AbstractTensorMap, ::Index2Tuple{N₁, N₂}, ::Bool ) where {N₁, N₂} - I = sectortype(A) - M = similarstoragetype(A, sectorscalartype(I) <: Real ? TC : complex(TC)) + M = similarstoragetype(A, recoupled_scalartype(TC, sectortype(A))) return tensormaptype(spacetype(A), N₁, N₂, M) end @@ -154,8 +153,7 @@ function TO.tensorcontract_type( ::Index2Tuple{N₁, N₂} ) where {N₁, N₂} S = check_spacetype(A, B) - I = sectortype(A) - TC′ = isreal(I) ? TC : complex(TC) + TC′ = recoupled_scalartype(TC, sectortype(S)) M = promote_storagetype(similarstoragetype(A, TC′), similarstoragetype(B, TC′)) return tensormaptype(S, N₁, N₂, M) end From dc98966f3101e23d76b22064eb170b4f73598df6 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Fri, 30 Jan 2026 15:56:40 -0500 Subject: [PATCH 08/14] update link in docs --- docs/src/lib/tensors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/lib/tensors.md b/docs/src/lib/tensors.md index 3e9520cbc..6bb19dee7 100644 --- a/docs/src/lib/tensors.md +++ b/docs/src/lib/tensors.md @@ -184,7 +184,7 @@ type `add_transform!`, for additional expert-mode options that allows for additi scaling, as well as the selection of a custom backend. ```@docs -permute(::AbstractTensorMap, ::Index2Tuple{N₁,N₂}) where {N₁,N₂} +permute(::AbstractTensorMap, ::Index2Tuple) braid(::AbstractTensorMap, ::Index2Tuple, ::IndexTuple) transpose(::AbstractTensorMap, ::Index2Tuple) repartition(::AbstractTensorMap, ::Int, ::Int) From 561bbedb13ba971d60e950f6b686a370dce4235d Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Fri, 30 Jan 2026 15:58:27 -0500 Subject: [PATCH 09/14] fix reference to unused variable --- src/tensors/indexmanipulations.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tensors/indexmanipulations.jl b/src/tensors/indexmanipulations.jl index 041eec608..d30a732a5 100644 --- a/src/tensors/indexmanipulations.jl +++ b/src/tensors/indexmanipulations.jl @@ -55,7 +55,7 @@ function permute(t::AbstractTensorMap, (p₁, p₂)::Index2Tuple; copy::Bool = f if p₁ === codomainind(t) && p₂ === domainind(t) return t elseif t isa TensorMap && has_shared_permute(t, (p₁, p₂)) - return TensorMap(t.data, space′) + return TensorMap(t.data, permute(space(t), (p₁, p₂))) end end tdst = TO.tensoralloc_add(scalartype(t), t, (p₁, p₂), false, Val(false)) From 8eaf44595c803e103754e74a9816478b7888a106 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Fri, 30 Jan 2026 17:18:41 -0500 Subject: [PATCH 10/14] fix missing argument --- src/tensors/indexmanipulations.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tensors/indexmanipulations.jl b/src/tensors/indexmanipulations.jl index d30a732a5..2bc3815a9 100644 --- a/src/tensors/indexmanipulations.jl +++ b/src/tensors/indexmanipulations.jl @@ -294,7 +294,7 @@ See [`twist!`](@ref) for storing the result in place. """ function twist(t::AbstractTensorMap, inds; inv::Bool = false, copy::Bool = false) !copy && has_shared_twist(t, inds) && return t - tdst = TO.tensoralloc_add(scalartype(t), (codomainind(t), domainind(t)), false, Val(false)) + tdst = TO.tensoralloc_add(scalartype(t), t, (codomainind(t), domainind(t)), false, Val(false)) return twist!(tdst, inds; inv) end From cb734d2eed05e9dc3117bb319b5fcdffcd230dba Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Sat, 31 Jan 2026 10:31:37 -0500 Subject: [PATCH 11/14] actually twist correct tensor... --- src/tensors/indexmanipulations.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tensors/indexmanipulations.jl b/src/tensors/indexmanipulations.jl index 2bc3815a9..63dfe7780 100644 --- a/src/tensors/indexmanipulations.jl +++ b/src/tensors/indexmanipulations.jl @@ -295,6 +295,7 @@ See [`twist!`](@ref) for storing the result in place. function twist(t::AbstractTensorMap, inds; inv::Bool = false, copy::Bool = false) !copy && has_shared_twist(t, inds) && return t tdst = TO.tensoralloc_add(scalartype(t), t, (codomainind(t), domainind(t)), false, Val(false)) + copy!(tdst, t) return twist!(tdst, inds; inv) end From dc468ec7b80e75b53ee8410859c2a1e32119b103 Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Mon, 2 Feb 2026 10:47:29 -0500 Subject: [PATCH 12/14] centralize `promote_op` functions to compute scalartypes --- Project.toml | 2 +- src/tensors/abstracttensor.jl | 6 ---- src/tensors/diagonal.jl | 2 +- src/tensors/indexmanipulations.jl | 60 +++++++++++++++++++++---------- src/tensors/tensoroperations.jl | 4 +-- 5 files changed, 46 insertions(+), 28 deletions(-) diff --git a/Project.toml b/Project.toml index ddcbe4141..22a4915d7 100644 --- a/Project.toml +++ b/Project.toml @@ -52,7 +52,7 @@ Random = "1" SafeTestsets = "0.1" ScopedValues = "1.3.0" Strided = "2" -TensorKitSectors = "0.3.3" +TensorKitSectors = "0.3.5" TensorOperations = "5.1" Test = "1" TestExtras = "0.2,0.3" diff --git a/src/tensors/abstracttensor.jl b/src/tensors/abstracttensor.jl index 0f580c17c..823280e07 100644 --- a/src/tensors/abstracttensor.jl +++ b/src/tensors/abstracttensor.jl @@ -102,12 +102,6 @@ similarstoragetype(::Type{D}, ::Type{T}) where {D <: AbstractDict{<:Sector, <:Ab # default storage type for numbers similarstoragetype(::Type{T}) where {T <: Number} = Vector{T} - -# helper function to determine the scalartype taking into account that recouplings might happen -recoupled_scalartype(::Type{T}, ::Type{I}) where {T <: Number, I <: Sector} = isreal(I) ? T : complex(T) -recoupled_scalartype(t::AbstractTensorMap) = recoupled_scalartype(typeof(t)) -recoupled_scalartype(::Type{T}) where {T <: AbstractTensorMap} = recoupled_scalartype(scalartype(T), sectortype(T)) - # tensor characteristics: space and index information #----------------------------------------------------- """ diff --git a/src/tensors/diagonal.jl b/src/tensors/diagonal.jl index f25a81961..447a4b8eb 100644 --- a/src/tensors/diagonal.jl +++ b/src/tensors/diagonal.jl @@ -261,7 +261,7 @@ function TO.tensorcontract_type( ::Index2Tuple{1, 1} ) S = check_spacetype(A, B) - TC′ = recoupled_scalartype(TC, sectortype(S)) + TC′ = promote_permute(TC, sectortype(S)) M = promote_storagetype(similarstoragetype(A, TC′), similarstoragetype(B, TC′)) return DiagonalTensorMap{TC, S, M} end diff --git a/src/tensors/indexmanipulations.jl b/src/tensors/indexmanipulations.jl index 63dfe7780..906ad6379 100644 --- a/src/tensors/indexmanipulations.jl +++ b/src/tensors/indexmanipulations.jl @@ -1,5 +1,28 @@ # Index manipulations #--------------------- + +# find the scalartype after applying operations: take into account fusion and/or braiding +# might need to become Float or Complex to capture complex recoupling coefficients but don't alter precision +for (operation, manipulation) in ( + :flip => :sector, :twist => :braiding, + :transpose => :fusion, :permute => :sector, :braid => :sector, + ) + promote_op = Symbol(:promote_, operation) + manipulation_scalartype = Symbol(manipulation, :scalartype) + + @eval begin + $promote_op(t::AbstractTensorMap) = $promote_op(typeof(t)) + $promote_op(::Type{T}) where {T <: AbstractTensorMap} = + $promote_op(scalartype(T), sectortype(T)) + $promote_op(::Type{T}, ::Type{I}) where {T <: Number, I <: Sector} = + sectorscalartype(I) <: Integer ? T : + sectorscalartype(I) <: Real ? float(T) : complex(T) + # TODO: currently the manipulations all use sectorscalartype, change to: + # $manipulation_scalartype(I) <: Integer ? T : + # $manipulation_scalartype(I) <: Real ? float(T) : complex(T) + end +end + """ flip(t::AbstractTensorMap, I) -> t′::AbstractTensorMap @@ -15,7 +38,7 @@ Return a new tensor that is isomorphic to `t` but where the arrows on the indice """ function flip(t::AbstractTensorMap, I; inv::Bool = false) P = flip(space(t), I) - t′ = similar(t, recoupled_scalartype(t), P) + t′ = similar(t, promote_flip(t), P) for (f₁, f₂) in fusiontrees(t) (f₁′, f₂′), factor = only(flip(f₁, f₂, I; inv)) scale!(t′[f₁′, f₂′], t[f₁, f₂], factor) @@ -49,18 +72,19 @@ Otherwise, a copy is always made. To permute into an existing destination, see [permute!](@ref) and [`add_permute!`](@ref) """ -function permute(t::AbstractTensorMap, (p₁, p₂)::Index2Tuple; copy::Bool = false) +function permute(t::AbstractTensorMap, p::Index2Tuple; copy::Bool = false) # share data if possible if !copy - if p₁ === codomainind(t) && p₂ === domainind(t) + if p == (codomainind(t), domainind(t)) return t - elseif t isa TensorMap && has_shared_permute(t, (p₁, p₂)) - return TensorMap(t.data, permute(space(t), (p₁, p₂))) + elseif t isa TensorMap && has_shared_permute(t, p) + return TensorMap(t.data, permute(space(t), p)) end end - tdst = TO.tensoralloc_add(scalartype(t), t, (p₁, p₂), false, Val(false)) + # general case - return @inbounds permute!(tdst, t, (p₁, p₂)) + tdst = similar(t, promote_permute(t), permute(space(t), p)) + return @inbounds permute!(tdst, t, p) end function permute(t::AdjointTensorMap, (p₁, p₂)::Index2Tuple; copy::Bool = false) p₁′ = adjointtensorindices(t, p₂) @@ -125,16 +149,16 @@ If `copy=false`, `tdst` might share data with `tsrc` whenever possible. Otherwis To braid into an existing destination, see [braid!](@ref) and [`add_braid!`](@ref) """ function braid( - t::AbstractTensorMap, (p₁, p₂)::Index2Tuple, levels::IndexTuple; copy::Bool = false + t::AbstractTensorMap, p::Index2Tuple, levels::IndexTuple; copy::Bool = false ) length(levels) == numind(t) || throw(ArgumentError("invalid levels")) - BraidingStyle(sectortype(t)) isa SymmetricBraiding && return permute(t, (p₁, p₂); copy = copy) - (!copy && p₁ == codomainind(t) && p₂ == domainind(t)) && return t + BraidingStyle(sectortype(t)) isa SymmetricBraiding && return permute(t, p; copy) + (!copy && p == (codomainind(t), domainind(t))) && return t # general case - tdst = TO.tensoralloc_add(scalartype(t), t, (p₁, p₂), false, Val(false)) - return @inbounds braid!(tdst, t, (p₁, p₂), levels) + tdst = similar(t, promote_braid(t), permute(space(t), p)) + return @inbounds braid!(tdst, t, p, levels) end # TODO: braid for `AdjointTensorMap`; think about how to map the `levels` argument. @@ -174,15 +198,15 @@ If `copy=false`, `tdst` might share data with `tsrc` whenever possible. Otherwis To permute into an existing destination, see [permute!](@ref) and [`add_permute!`](@ref) """ function LinearAlgebra.transpose( - t::AbstractTensorMap, (p₁, p₂)::Index2Tuple = _transpose_indices(t); + t::AbstractTensorMap, p::Index2Tuple = _transpose_indices(t); copy::Bool = false ) - sectortype(t) === Trivial && return permute(t, (p₁, p₂); copy) - (!copy && p₁ == codomainind(t) && p₂ == domainind(t)) && return t + sectortype(t) === Trivial && return permute(t, p; copy) + (!copy && p == (codomainind(t), domainind(t))) && return t # general case - tdst = TO.tensoralloc_add(scalartype(t), t, (p₁, p₂), false, Val(false)) - return @inbounds transpose!(tdst, t, (p₁, p₂)) + tdst = similar(t, promote_transpose(t), permute(space(t), p)) + return @inbounds transpose!(tdst, t, p) end function LinearAlgebra.transpose( @@ -294,7 +318,7 @@ See [`twist!`](@ref) for storing the result in place. """ function twist(t::AbstractTensorMap, inds; inv::Bool = false, copy::Bool = false) !copy && has_shared_twist(t, inds) && return t - tdst = TO.tensoralloc_add(scalartype(t), t, (codomainind(t), domainind(t)), false, Val(false)) + tdst = similar(t, promote_twist(t)) copy!(tdst, t) return twist!(tdst, inds; inv) end diff --git a/src/tensors/tensoroperations.jl b/src/tensors/tensoroperations.jl index bc5d4beeb..81894d140 100644 --- a/src/tensors/tensoroperations.jl +++ b/src/tensors/tensoroperations.jl @@ -54,7 +54,7 @@ end function TO.tensoradd_type( TC, A::AbstractTensorMap, ::Index2Tuple{N₁, N₂}, ::Bool ) where {N₁, N₂} - M = similarstoragetype(A, recoupled_scalartype(TC, sectortype(A))) + M = similarstoragetype(A, promote_permute(TC, sectortype(A))) return tensormaptype(spacetype(A), N₁, N₂, M) end @@ -153,7 +153,7 @@ function TO.tensorcontract_type( ::Index2Tuple{N₁, N₂} ) where {N₁, N₂} S = check_spacetype(A, B) - TC′ = recoupled_scalartype(TC, sectortype(S)) + TC′ = promote_permute(TC, sectortype(S)) M = promote_storagetype(similarstoragetype(A, TC′), similarstoragetype(B, TC′)) return tensormaptype(S, N₁, N₂, M) end From 0f494394574a46d03185908e4c2b06e236f2140d Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Mon, 2 Feb 2026 10:47:37 -0500 Subject: [PATCH 13/14] use `dimscalartype` --- src/tensors/linalg.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tensors/linalg.jl b/src/tensors/linalg.jl index e28ec8153..fa6397caf 100644 --- a/src/tensors/linalg.jl +++ b/src/tensors/linalg.jl @@ -290,8 +290,8 @@ function LinearAlgebra.rank( t::AbstractTensorMap; atol::Real = 0, rtol::Real = atol > 0 ? 0 : _default_rtol(t) ) - r = 0 * dim(first(allunits(sectortype(t)))) - dim(t) == 0 && return r + r = zero(dimscalartype(sectortype(t))) + iszero(dim(t)) && return r S = MatrixAlgebraKit.svd_vals(t) tol = max(atol, rtol * maximum(parent(S))) for (c, b) in pairs(S) @@ -323,7 +323,7 @@ end function LinearAlgebra.tr(t::AbstractTensorMap) domain(t) == codomain(t) || throw(SpaceMismatch("Trace of a tensor only exist when domain == codomain")) - s = zero(scalartype(t)) + s = zero(scalartype(t)) * zero(dimscalartype(sectortype(t))) for (c, b) in blocks(t) s += dim(c) * tr(b) end From 9a1d9965d352ccddc6de80633c538347d12b87ba Mon Sep 17 00:00:00 2001 From: Lukas Devos Date: Mon, 2 Feb 2026 16:58:32 -0500 Subject: [PATCH 14/14] Apply suggestions from code review Co-authored-by: Jutho --- src/tensors/linalg.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tensors/linalg.jl b/src/tensors/linalg.jl index fa6397caf..b1fb26a09 100644 --- a/src/tensors/linalg.jl +++ b/src/tensors/linalg.jl @@ -290,8 +290,9 @@ function LinearAlgebra.rank( t::AbstractTensorMap; atol::Real = 0, rtol::Real = atol > 0 ? 0 : _default_rtol(t) ) - r = zero(dimscalartype(sectortype(t))) - iszero(dim(t)) && return r + r = dim(t) + iszero(r) && return r + r = zero(r) S = MatrixAlgebraKit.svd_vals(t) tol = max(atol, rtol * maximum(parent(S))) for (c, b) in pairs(S)