From 7dfeb27d5b55b8281f35341741ff3681e301f6a5 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Wed, 30 Sep 2026 10:56:15 -0400 Subject: [PATCH 1/7] Forward a contraction algorithm through named-tensor `contract` and `mul!` Co-Authored-By: Claude Opus 5.5 --- src/tensoralgebra.jl | 27 +++++++++++++++++---------- test/test_tensoralgebra.jl | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/tensoralgebra.jl b/src/tensoralgebra.jl index d40c6c9..8d191e3 100644 --- a/src/tensoralgebra.jl +++ b/src/tensoralgebra.jl @@ -7,12 +7,15 @@ using TensorAlgebra: TensorAlgebra as TA names_setdiff(s1, s2) = setdiff(s1, s2) Base.:*(a1::AbstractNamedTensor, a2::AbstractNamedTensor) = mul_namedtensor(a1, a2) -function mul_namedtensor(a1::AbstractNamedTensor, a2::AbstractNamedTensor) +function mul_namedtensor(a1::AbstractNamedTensor, a2::AbstractNamedTensor; kwargs...) a_dest, names_dest = TA.contract( - unnamed(a1), names(a1), unnamed(a2), names(a2) + unnamed(a1), names(a1), unnamed(a2), names(a2); kwargs... ) return NamedTensor(a_dest, names_dest) end +function TA.contract(a1::AbstractNamedTensor, a2::AbstractNamedTensor; kwargs...) + return mul_namedtensor(a1, a2; kwargs...) +end # Left associative fold/reduction. # Circumvent Base definitions: @@ -37,38 +40,42 @@ end function LA.mul!( a_dest::AbstractNamedTensor, a1::AbstractNamedTensor, a2::AbstractNamedTensor, - α::Number, β::Number + α::Number, β::Number; + kwargs... ) - return mul!_namedtensor(a_dest, a1, a2, α, β) + return mul!_namedtensor(a_dest, a1, a2, α, β; kwargs...) end function mul!_namedtensor( a_dest::AbstractNamedTensor, a1::AbstractNamedTensor, a2::AbstractNamedTensor, - α::Number, β::Number + α::Number, β::Number; + kwargs... ) TA.contractadd!( unnamed(a_dest), names(a_dest), unnamed(a1), names(a1), unnamed(a2), names(a2), - α, β + α, β; kwargs... ) return a_dest end function LA.mul!( a_dest::AbstractNamedTensor, - a1::AbstractNamedTensor, a2::AbstractNamedTensor + a1::AbstractNamedTensor, a2::AbstractNamedTensor; + kwargs... ) - return mul!_namedtensor(a_dest, a1, a2) + return mul!_namedtensor(a_dest, a1, a2; kwargs...) end function mul!_namedtensor( a_dest::AbstractNamedTensor, - a1::AbstractNamedTensor, a2::AbstractNamedTensor + a1::AbstractNamedTensor, a2::AbstractNamedTensor; + kwargs... ) TA.contract!( unnamed(a_dest), names(a_dest), unnamed(a1), names(a1), - unnamed(a2), names(a2) + unnamed(a2), names(a2); kwargs... ) return a_dest end diff --git a/test/test_tensoralgebra.jl b/test/test_tensoralgebra.jl index 8130f69..1b1c589 100644 --- a/test/test_tensoralgebra.jl +++ b/test/test_tensoralgebra.jl @@ -1,6 +1,6 @@ using ITensorBase: ITensorBase, Index, NamedOneTo, id, inds, name, names, operator, prime, rename, unname, unnamed -using LinearAlgebra: norm, tr +using LinearAlgebra: mul!, norm, tr using MatrixAlgebraKit: left_null, left_orth, left_polar, lq_compact, lq_full, qr_compact, qr_full, right_null, right_orth, right_polar, svd_compact, svd_trunc, svd_vals using StableRNGs: StableRNG @@ -225,3 +225,34 @@ using Test: @test, @test_broken, @testset @test name(rn) != name(i) end end + +# Records each call, then contracts with the default algorithm. +struct RecordingContract <: TensorAlgebra.ContractAlgorithm + calls::Base.RefValue{Int} +end +function TensorAlgebra.contractpermopadd!( + alg::RecordingContract, a_dest, perm_dest_codomain, perm_dest_domain, + op1, a1, perm1_codomain, perm1_domain, op2, a2, perm2_codomain, perm2_domain, + α::Number, β::Number + ) + alg.calls[] += 1 + return TensorAlgebra.contractpermopadd!( + TensorAlgebra.MatricizeContract(), a_dest, perm_dest_codomain, perm_dest_domain, + op1, a1, perm1_codomain, perm1_domain, op2, a2, perm2_codomain, perm2_domain, α, β + ) +end + +@testset "contraction forwards `alg`" begin + i, j, k = NamedOneTo(2, "i"), NamedOneTo(3, "j"), NamedOneTo(4, "k") + a, b = randn(i, j), randn(j, k) + alg = RecordingContract(Ref(0)) + @test TensorAlgebra.contract(a, b; alg) ≈ a * b + @test alg.calls[] == 1 + dest = similar(a * b) + mul!(dest, a, b; alg) + @test dest ≈ a * b + @test alg.calls[] == 2 + mul!(dest, a, b, 2, 1; alg) + @test dest ≈ 3 * (a * b) + @test alg.calls[] == 3 +end From 1cfa91dc10a67a5d67559549f7cabd28d79485ea Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Wed, 30 Sep 2026 10:58:42 -0400 Subject: [PATCH 2/7] Add named-tensor `contract!` and `contractopadd!` with operand ops Co-Authored-By: Claude Opus 5.5 --- Project.toml | 2 +- src/tensoralgebra.jl | 22 ++++++++++++++++++++++ test/test_tensoralgebra.jl | 23 +++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 9a97b80..7a5ada9 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ITensorBase" uuid = "4795dd04-0d67-49bb-8f44-b89c448a1dc7" -version = "0.15.1" +version = "0.15.2" authors = ["ITensor developers and contributors"] [workspace] diff --git a/src/tensoralgebra.jl b/src/tensoralgebra.jl index 8d191e3..2e38246 100644 --- a/src/tensoralgebra.jl +++ b/src/tensoralgebra.jl @@ -80,6 +80,28 @@ function mul!_namedtensor( return a_dest end +function TA.contract!( + a_dest::AbstractNamedTensor, a1::AbstractNamedTensor, a2::AbstractNamedTensor; + kwargs... + ) + return mul!_namedtensor(a_dest, a1, a2; kwargs...) +end +function TA.contractopadd!( + a_dest::AbstractNamedTensor, + op1, a1::AbstractNamedTensor, + op2, a2::AbstractNamedTensor, + α::Number, β::Number; + kwargs... + ) + TA.contractopadd!( + unnamed(a_dest), names(a_dest), + op1, unnamed(a1), names(a1), + op2, unnamed(a2), names(a2), + α, β; kwargs... + ) + return a_dest +end + # Locate the named-dimension groups `group1`, `group2` within `a`, returning their two # positional index groups. function nameperm(a::AbstractNamedTensor, group1, group2) diff --git a/test/test_tensoralgebra.jl b/test/test_tensoralgebra.jl index 1b1c589..f470ec1 100644 --- a/test/test_tensoralgebra.jl +++ b/test/test_tensoralgebra.jl @@ -256,3 +256,26 @@ end @test dest ≈ 3 * (a * b) @test alg.calls[] == 3 end + +@testset "named `contract!` and `contractopadd!`" begin + i, j, k = NamedOneTo(2, "i"), NamedOneTo(3, "j"), NamedOneTo(4, "k") + a, b = randn(ComplexF64, i, j), randn(ComplexF64, j, k) + dest = randn(ComplexF64, i, k) + TensorAlgebra.contract!(dest, a, b) + @test dest ≈ a * b + TensorAlgebra.contractopadd!(dest, conj, a, identity, b, true, false) + @test dest ≈ conj(a) * b + TensorAlgebra.contractopadd!(dest, identity, a, conj, b, true, false) + @test dest ≈ a * conj(b) + prev = copy(dest) + TensorAlgebra.contractopadd!(dest, conj, a, identity, b, 2, 1) + @test dest ≈ prev + 2 * (conj(a) * b) + alg = RecordingContract(Ref(0)) + TensorAlgebra.contractopadd!(dest, conj, a, identity, b, true, false; alg) + @test dest ≈ conj(a) * b + @test alg.calls[] == 1 + dest_perm = randn(ComplexF64, k, i) + TensorAlgebra.contractopadd!(dest_perm, conj, a, identity, b, true, false) + @test names(dest_perm) == ["k", "i"] + @test dest_perm ≈ conj(a) * b +end From 4a888d2292fe1dfd0757302b15fb57fba244dcd5 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Wed, 30 Sep 2026 11:04:30 -0400 Subject: [PATCH 3/7] Restrict the allocating named `contract` to `NamedTensor` operands Operator and lazy tensors keep their wrappers through `*`; with the `AbstractNamedTensor` method, `contract` returned a plain `NamedTensor`. Co-Authored-By: Claude Opus 5.5 --- src/namedtensor.jl | 6 ++++++ src/tensoralgebra.jl | 3 --- test/test_tensoralgebra.jl | 12 +++++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/namedtensor.jl b/src/namedtensor.jl index 8dd90ef..52de570 100644 --- a/src/namedtensor.jl +++ b/src/namedtensor.jl @@ -210,3 +210,9 @@ unnamedtype(a::NamedTensor) = typeof(unnamed(a)) unnamedtype(::Type{<:NamedTensor}) = AbstractArray parenttype(a::NamedTensor) = typeof(parent(a)) parenttype(::Type{<:NamedTensor}) = AbstractArray + +# Restricted to `NamedTensor` so operator and lazy operands, whose `*` returns a wrapped +# result, throw a `MethodError` instead of returning a plain `NamedTensor`. +function TensorAlgebra.contract(a1::NamedTensor, a2::NamedTensor; kwargs...) + return mul_namedtensor(a1, a2; kwargs...) +end diff --git a/src/tensoralgebra.jl b/src/tensoralgebra.jl index 2e38246..33f6290 100644 --- a/src/tensoralgebra.jl +++ b/src/tensoralgebra.jl @@ -13,9 +13,6 @@ function mul_namedtensor(a1::AbstractNamedTensor, a2::AbstractNamedTensor; kwarg ) return NamedTensor(a_dest, names_dest) end -function TA.contract(a1::AbstractNamedTensor, a2::AbstractNamedTensor; kwargs...) - return mul_namedtensor(a1, a2; kwargs...) -end # Left associative fold/reduction. # Circumvent Base definitions: diff --git a/test/test_tensoralgebra.jl b/test/test_tensoralgebra.jl index f470ec1..fd67e5a 100644 --- a/test/test_tensoralgebra.jl +++ b/test/test_tensoralgebra.jl @@ -6,7 +6,7 @@ using MatrixAlgebraKit: left_null, left_orth, left_polar, lq_compact, lq_full, q using StableRNGs: StableRNG using TensorAlgebra: TensorAlgebra, contract, directsum, matricize, project, trivialrange, unchecked_project, unmatricize -using Test: @test, @test_broken, @testset +using Test: @test, @test_broken, @test_throws, @testset @testset "TensorAlgebra (eltype=$(elt))" for elt in ( @@ -279,3 +279,13 @@ end @test names(dest_perm) == ["k", "i"] @test dest_perm ≈ conj(a) * b end + +@testset "allocating `contract` rejects wrapped named tensors" begin + i, j, k = NamedOneTo(2, "i"), NamedOneTo(3, "j"), NamedOneTo(4, "k") + a, b = randn(i, j), randn(j, k) + @test_throws MethodError TensorAlgebra.contract(operator(a, [i], [j]), b) + @test_throws MethodError TensorAlgebra.contract( + ITensorBase.lazy(a), + ITensorBase.lazy(b) + ) +end From 282752acc83127d94450425c6eea4c47492159bd Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Wed, 30 Sep 2026 14:33:17 -0400 Subject: [PATCH 4/7] Replace the named `TensorAlgebra` contraction methods with `mulopadd!` Named-tensor contraction uses matrix-operation names: `mul!` (3- and 5-argument, with `alg`) and the new `mulopadd!`. The `TensorAlgebra.contract`, `contract!` and `contractopadd!` methods for named tensors are removed. Co-Authored-By: Claude Opus 5.5 --- src/ITensorBase.jl | 2 +- src/namedtensor.jl | 6 ------ src/tensoralgebra.jl | 18 +++++++++--------- test/test_tensoralgebra.jl | 38 ++++++++++++-------------------------- 4 files changed, 22 insertions(+), 42 deletions(-) diff --git a/src/ITensorBase.jl b/src/ITensorBase.jl index 04471b3..983be9e 100644 --- a/src/ITensorBase.jl +++ b/src/ITensorBase.jl @@ -11,7 +11,7 @@ export AbstractNamedTensor, NamedTensor, AbstractITensor, ITensor, Index, if VERSION >= v"1.11.0-DEV.469" eval( Meta.parse( - "public @names, IndexName, name, names, setname, space, unnamed, unnamedtype, decoration, emptytags, gettag, gettags, hastag, plev, settags, tags, unsettags" + "public @names, IndexName, mulopadd!, name, names, setname, space, unnamed, unnamedtype, decoration, emptytags, gettag, gettags, hastag, plev, settags, tags, unsettags" ) ) end diff --git a/src/namedtensor.jl b/src/namedtensor.jl index 52de570..8dd90ef 100644 --- a/src/namedtensor.jl +++ b/src/namedtensor.jl @@ -210,9 +210,3 @@ unnamedtype(a::NamedTensor) = typeof(unnamed(a)) unnamedtype(::Type{<:NamedTensor}) = AbstractArray parenttype(a::NamedTensor) = typeof(parent(a)) parenttype(::Type{<:NamedTensor}) = AbstractArray - -# Restricted to `NamedTensor` so operator and lazy operands, whose `*` returns a wrapped -# result, throw a `MethodError` instead of returning a plain `NamedTensor`. -function TensorAlgebra.contract(a1::NamedTensor, a2::NamedTensor; kwargs...) - return mul_namedtensor(a1, a2; kwargs...) -end diff --git a/src/tensoralgebra.jl b/src/tensoralgebra.jl index 33f6290..695b85f 100644 --- a/src/tensoralgebra.jl +++ b/src/tensoralgebra.jl @@ -7,9 +7,9 @@ using TensorAlgebra: TensorAlgebra as TA names_setdiff(s1, s2) = setdiff(s1, s2) Base.:*(a1::AbstractNamedTensor, a2::AbstractNamedTensor) = mul_namedtensor(a1, a2) -function mul_namedtensor(a1::AbstractNamedTensor, a2::AbstractNamedTensor; kwargs...) +function mul_namedtensor(a1::AbstractNamedTensor, a2::AbstractNamedTensor) a_dest, names_dest = TA.contract( - unnamed(a1), names(a1), unnamed(a2), names(a2); kwargs... + unnamed(a1), names(a1), unnamed(a2), names(a2) ) return NamedTensor(a_dest, names_dest) end @@ -77,13 +77,13 @@ function mul!_namedtensor( return a_dest end -function TA.contract!( - a_dest::AbstractNamedTensor, a1::AbstractNamedTensor, a2::AbstractNamedTensor; - kwargs... - ) - return mul!_namedtensor(a_dest, a1, a2; kwargs...) -end -function TA.contractopadd!( +""" + mulopadd!(a_dest, op1, a1, op2, a2, α, β; alg = nothing) + +Compute `a_dest = α * op1(a1) * op2(a2) + β * a_dest`, matching dimensions by name. `op1` and +`op2` are `identity` or `conj`, and `alg` is a `TensorAlgebra.ContractAlgorithm`. +""" +function mulopadd!( a_dest::AbstractNamedTensor, op1, a1::AbstractNamedTensor, op2, a2::AbstractNamedTensor, diff --git a/test/test_tensoralgebra.jl b/test/test_tensoralgebra.jl index fd67e5a..207c38c 100644 --- a/test/test_tensoralgebra.jl +++ b/test/test_tensoralgebra.jl @@ -1,12 +1,12 @@ -using ITensorBase: ITensorBase, Index, NamedOneTo, id, inds, name, names, operator, prime, - rename, unname, unnamed +using ITensorBase: ITensorBase, Index, NamedOneTo, id, inds, mulopadd!, name, names, + operator, prime, rename, unname, unnamed using LinearAlgebra: mul!, norm, tr using MatrixAlgebraKit: left_null, left_orth, left_polar, lq_compact, lq_full, qr_compact, qr_full, right_null, right_orth, right_polar, svd_compact, svd_trunc, svd_vals using StableRNGs: StableRNG using TensorAlgebra: TensorAlgebra, contract, directsum, matricize, project, trivialrange, unchecked_project, unmatricize -using Test: @test, @test_broken, @test_throws, @testset +using Test: @test, @test_broken, @testset @testset "TensorAlgebra (eltype=$(elt))" for elt in ( @@ -242,50 +242,36 @@ function TensorAlgebra.contractpermopadd!( ) end -@testset "contraction forwards `alg`" begin +@testset "`mul!` forwards `alg`" begin i, j, k = NamedOneTo(2, "i"), NamedOneTo(3, "j"), NamedOneTo(4, "k") a, b = randn(i, j), randn(j, k) alg = RecordingContract(Ref(0)) - @test TensorAlgebra.contract(a, b; alg) ≈ a * b - @test alg.calls[] == 1 dest = similar(a * b) mul!(dest, a, b; alg) @test dest ≈ a * b - @test alg.calls[] == 2 + @test alg.calls[] == 1 mul!(dest, a, b, 2, 1; alg) @test dest ≈ 3 * (a * b) - @test alg.calls[] == 3 + @test alg.calls[] == 2 end -@testset "named `contract!` and `contractopadd!`" begin +@testset "`mulopadd!`" begin i, j, k = NamedOneTo(2, "i"), NamedOneTo(3, "j"), NamedOneTo(4, "k") a, b = randn(ComplexF64, i, j), randn(ComplexF64, j, k) dest = randn(ComplexF64, i, k) - TensorAlgebra.contract!(dest, a, b) - @test dest ≈ a * b - TensorAlgebra.contractopadd!(dest, conj, a, identity, b, true, false) + mulopadd!(dest, conj, a, identity, b, true, false) @test dest ≈ conj(a) * b - TensorAlgebra.contractopadd!(dest, identity, a, conj, b, true, false) + mulopadd!(dest, identity, a, conj, b, true, false) @test dest ≈ a * conj(b) prev = copy(dest) - TensorAlgebra.contractopadd!(dest, conj, a, identity, b, 2, 1) + mulopadd!(dest, conj, a, identity, b, 2, 1) @test dest ≈ prev + 2 * (conj(a) * b) alg = RecordingContract(Ref(0)) - TensorAlgebra.contractopadd!(dest, conj, a, identity, b, true, false; alg) + mulopadd!(dest, conj, a, identity, b, true, false; alg) @test dest ≈ conj(a) * b @test alg.calls[] == 1 dest_perm = randn(ComplexF64, k, i) - TensorAlgebra.contractopadd!(dest_perm, conj, a, identity, b, true, false) + mulopadd!(dest_perm, conj, a, identity, b, true, false) @test names(dest_perm) == ["k", "i"] @test dest_perm ≈ conj(a) * b end - -@testset "allocating `contract` rejects wrapped named tensors" begin - i, j, k = NamedOneTo(2, "i"), NamedOneTo(3, "j"), NamedOneTo(4, "k") - a, b = randn(i, j), randn(j, k) - @test_throws MethodError TensorAlgebra.contract(operator(a, [i], [j]), b) - @test_throws MethodError TensorAlgebra.contract( - ITensorBase.lazy(a), - ITensorBase.lazy(b) - ) -end From 58da151a17c5f2172586bec0499ff866b0af2f74 Mon Sep 17 00:00:00 2001 From: Jack Dunham <72548217+jack-dunham@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:14:07 -0400 Subject: [PATCH 5/7] Allow passing of `kwargs...` to `contractopadd!` generically in `mulopadd!`. Co-authored-by: Matthew Fishman --- src/tensoralgebra.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tensoralgebra.jl b/src/tensoralgebra.jl index 695b85f..4ea92b9 100644 --- a/src/tensoralgebra.jl +++ b/src/tensoralgebra.jl @@ -78,10 +78,10 @@ function mul!_namedtensor( end """ - mulopadd!(a_dest, op1, a1, op2, a2, α, β; alg = nothing) + mulopadd!(a_dest, op1, a1, op2, a2, α, β; kwargs...) Compute `a_dest = α * op1(a1) * op2(a2) + β * a_dest`, matching dimensions by name. `op1` and -`op2` are `identity` or `conj`, and `alg` is a `TensorAlgebra.ContractAlgorithm`. +`op2` can be `identity` or `conj`, and keyword arguments (such as algorithm selection) are passed to `TensorAlgebra.contractopadd!`. """ function mulopadd!( a_dest::AbstractNamedTensor, From cf02e279d2ee87633088a03c47c2f412359821d0 Mon Sep 17 00:00:00 2001 From: Jack Dunham Date: Fri, 2 Oct 2026 13:17:25 -0400 Subject: [PATCH 6/7] Bump version to 0.15.3. --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 7a5ada9..4955010 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ITensorBase" uuid = "4795dd04-0d67-49bb-8f44-b89c448a1dc7" -version = "0.15.2" +version = "0.15.3" authors = ["ITensor developers and contributors"] [workspace] From 1f496f310c8b1d6c7644101266ff7a8355b1c079 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Fri, 2 Oct 2026 13:48:00 -0400 Subject: [PATCH 7/7] Add mulopadd! to the exports test The public declaration added mulopadd!, so the expected list needs it too. Only Julia 1.11 and newer compares public names, which is why the lts jobs passed. Co-Authored-By: Claude Opus 5 (1M context) --- test/test_exports.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_exports.jl b/test/test_exports.jl index 0c529b4..dd608de 100644 --- a/test/test_exports.jl +++ b/test/test_exports.jl @@ -14,7 +14,7 @@ using Test: @test, @testset :tryuniqueind, :uniqueind, :uniqueinds, :unioninds, :uniquename, ] publics = [ - :IndexName, :name, :names, :setname, :space, :unnamed, + :IndexName, :mulopadd!, :name, :names, :setname, :space, :unnamed, :unnamedtype, :decoration, :emptytags, :gettag, :gettags, :hastag, :plev, :settags, :tags, :unsettags,