From ec2c9de6270f2f948ed2069cefd04eb28900eabd Mon Sep 17 00:00:00 2001 From: lkdvos Date: Tue, 15 Sep 2026 14:28:31 -0400 Subject: [PATCH 1/2] Intercept `TO.tensoradd!` instead of `TensorKit.add_transform!` The blockwise `tensoradd` implementations were only reachable through `TensorKit`'s internals: `TO.tensoradd!` delegated to `permute!`, and the `add_transform!` methods defined here caught the kernel. Neither is a contract `TensorKit` owes us, and both stopped holding on TensorKit's index-manipulation refactor (QuantumKitHub/TensorKit.jl#526), where `TO.tensoradd!` calls the braid kernel directly and `add_transform!` gained a `conjsrc` argument. Nothing errors in that case: dense block tensors survive via TensorKit's generic subblock fallback, while sparse ones silently produce zeros, because the fallback writes through blocks a sparse container never materialized. Implement `TO.tensoradd!` for the block tensor types instead. That is the public entry point `@tensor` lowers to, so it is reachable no matter how TensorKit arranges its kernels, and `conjA` is handled explicitly rather than relying on it having been unwrapped into an adjoint beforehand. The mixed block/plain methods take the concrete `TensorMap` so that `(BlockTensorMap, SparseBlockTensorMap)` and the reverse resolve to the general method rather than being ambiguous. The added testset calls `TO.tensoradd!` directly rather than through `@tensor`, so this stays covered independently of TensorKit's routing. Co-Authored-By: Claude Opus 5 (1M context) --- src/tensors/tensoroperations.jl | 51 +++++++++++++++++++++++++++++++++ test/linalg/tensoroperations.jl | 23 +++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/src/tensors/tensoroperations.jl b/src/tensors/tensoroperations.jl index a4fb563..a0342c2 100644 --- a/src/tensors/tensoroperations.jl +++ b/src/tensors/tensoroperations.jl @@ -1,5 +1,56 @@ # TensorOperations # ---------------- + +# tensoradd! +# ---------- +# `TensorKit` is free to reorganize its own index manipulation kernels, and `TO.tensoradd!` does +# not necessarily route through `permute!` or `add_transform!`. Intercepting the public +# `TO.tensoradd!` entry point instead keeps the blockwise implementations reachable regardless. +function TO.tensoradd!( + C::BlockTensorMap, A::BlockTensorMap, pA::Index2Tuple, conjA::Bool, + α::Number, β::Number, backend, allocator + ) + Cdata = parent(C) + Adata = permutedims(StridedView(parent(A)), (pA[1]..., pA[2]...)) + @inbounds for I in eachindex(Cdata, Adata) + Cdata[I] = TO.tensoradd!(Cdata[I], Adata[I], pA, conjA, α, β, backend, allocator) + end + return C +end +function TO.tensoradd!( + C::AbstractBlockTensorMap, A::AbstractBlockTensorMap, pA::Index2Tuple, conjA::Bool, + α::Number, β::Number, backend, allocator + ) + scale!(C, β) + p_lin = (pA[1]..., pA[2]...) + @inbounds for (I, v) in nonzero_pairs(A) + I′ = CartesianIndex(TT.getindices(I.I, p_lin)) + C[I′] = TO.tensoradd!(C[I′], v, pA, conjA, α, One(), backend, allocator) + end + return C +end +# adjoints are absorbed into the conjugation flag and the permutation +function TO.tensoradd!( + C::AbstractBlockTensorMap, A::AdjointBlockTensorMap, pA::Index2Tuple, conjA::Bool, + α::Number, β::Number, backend, allocator + ) + return TO.tensoradd!(C, A', adjointtensorindices(A, pA), !conjA, α, β, backend, allocator) +end +# a block tensor holding a single block is interchangeable with that block +function TO.tensoradd!( + C::TensorMap, A::BlockTensorMap, pA::Index2Tuple, conjA::Bool, + α::Number, β::Number, backend, allocator + ) + return TO.tensoradd!(C, only(A), pA, conjA, α, β, backend, allocator) +end +function TO.tensoradd!( + C::BlockTensorMap, A::TensorMap, pA::Index2Tuple, conjA::Bool, + α::Number, β::Number, backend, allocator + ) + TO.tensoradd!(only(C), A, pA, conjA, α, β, backend, allocator) + return C +end + function TO.tensoradd_type( TC, A::AbstractBlockTensorMap, ::Index2Tuple{N₁, N₂}, ::Bool ) where {N₁, N₂} diff --git a/test/linalg/tensoroperations.jl b/test/linalg/tensoroperations.jl index a588237..51dc4b6 100644 --- a/test/linalg/tensoroperations.jl +++ b/test/linalg/tensoroperations.jl @@ -3,6 +3,7 @@ using BlockTensorKit using TensorKit using TensorOperations using Random +using LinearAlgebra: norm ## Vtr = ( @@ -50,6 +51,28 @@ end end ## +# `TO.tensoradd!` is called directly here rather than through `@tensor`, so that the blockwise +# implementations stay covered independently of how TensorKit routes its index manipulations. +@testset "tensoradd! entry point" begin + for T in (Float32, ComplexF32), Asparse in (false, true), p in (((3, 2, 1, 5, 4), ()), ((4, 5), (1, 3, 2))), + conjA in (false, true) + A = !Asparse ? randn(T, W) : sprand(T, W, 0.5) + C = TensorOperations.tensoralloc_add(T, A, p, conjA, Val(false)) + Cref = TensorOperations.tensoralloc_add(T, convert(TensorMap, A), p, conjA, Val(false)) + TensorOperations.tensoradd!( + C, A, p, conjA, one(T), zero(T), + TensorOperations.DefaultBackend(), TensorOperations.DefaultAllocator() + ) + TensorOperations.tensoradd!( + Cref, convert(TensorMap, A), p, conjA, one(T), zero(T), + TensorOperations.DefaultBackend(), TensorOperations.DefaultAllocator() + ) + @test convert(TensorMap, C) ≈ Cref + @test norm(C) ≈ norm(A) + end +end +## + @testset "tensortrace" begin for T in (Float32, ComplexF32) A = randn(T, W[1] ⊗ W[2] ← W[2] ⊗ W[3]) From 001be8e57d29dbe1f5fe3839a546c517cbe02f75 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Tue, 15 Sep 2026 14:48:24 -0400 Subject: [PATCH 2/2] Pin the planar entry points in the tests `planaradd!`, `planartrace!` and `planarcontract!` are reached through `transpose!`, `trace_permute!` and `contract!`, so they currently land on methods defined here and are not affected by TensorKit#526. They rest on the same undocumented delegation that broke `tensoradd!` though, and there was no planar coverage here at all, so pin them directly. Co-Authored-By: Claude Opus 5 (1M context) --- test/linalg/tensoroperations.jl | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/linalg/tensoroperations.jl b/test/linalg/tensoroperations.jl index 51dc4b6..67ad59a 100644 --- a/test/linalg/tensoroperations.jl +++ b/test/linalg/tensoroperations.jl @@ -73,6 +73,42 @@ end end ## +# The planar entry points are reached through `transpose!`, `trace_permute!` and `contract!` +# rather than through methods defined here, so they are pinned separately: a change in how +# TensorKit routes them would otherwise go unnoticed until it reached users. +@testset "planar entry points" begin + for T in (Float32, ComplexF32), Asparse in (false, true) + A = !Asparse ? randn(T, W) : sprand(T, W, 0.5) + Ad = convert(TensorMap, A) + # `(p₁..., reverse(p₂)...)` must be a cyclic rotation of (1, 2, 3, 5, 4) for this `W` + for p in (((1, 2, 3), (4, 5)), ((1, 2), (4, 5, 3)), ((2, 3, 5), (1, 4)), ((3, 5, 4), (2, 1))) + C = TensorOperations.tensoralloc_add(T, A, p, false, Val(false)) + Cd = TensorOperations.tensoralloc_add(T, Ad, p, false, Val(false)) + TensorKit.planaradd!(C, A, p, one(T), zero(T)) + TensorKit.planaradd!(Cd, Ad, p, one(T), zero(T)) + @test convert(TensorMap, C) ≈ Cd + @test norm(C) ≈ norm(A) + end + + # the planar order of a 2 <- 2 tensor is (1, 2, 4, 3), so legs 2 and 4 are the + # cyclically adjacent pair that can be traced + WB = W[1] ⊗ W[2] ← W[3] ⊗ W[2] + B = !Asparse ? randn(T, WB) : sprand(T, WB, 0.7) + @planar C1[a; b] := B[a c; b c] + @planar C2[a; b] := convert(TensorMap, B)[a c; b c] + @test convert(TensorMap, C1) ≈ C2 + + WA, WB = W[1] ← W[3], W[3] ← W[2]' + D = !Asparse ? randn(T, WA) : sprand(T, WA, 0.7) + E = !Asparse ? randn(T, WB) : sprand(T, WB, 0.7) + @planar F1[a; b] := D[a; c] * E[c; b] + @planar F2[a; b] := convert(TensorMap, D)[a; c] * convert(TensorMap, E)[c; b] + @test convert(TensorMap, F1) ≈ F2 + end +end + +## + @testset "tensortrace" begin for T in (Float32, ComplexF32) A = randn(T, W[1] ⊗ W[2] ← W[2] ⊗ W[3])