From 8d3d115b966cc456485c4bd86c42e03e35cd3234 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 7 Oct 2026 17:39:30 -0400 Subject: [PATCH 01/11] Add dotperm for inner products with dimension alignment Align tensor dimensions through matricization before computing the inner product. --- Project.toml | 2 +- src/TensorAlgebra.jl | 3 ++- src/dotperm.jl | 31 +++++++++++++++++++++++++++++ test/test_dotperm.jl | 46 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 src/dotperm.jl create mode 100644 test/test_dotperm.jl diff --git a/Project.toml b/Project.toml index ce16e5d..bc6956e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "TensorAlgebra" uuid = "68bd88dc-f39d-4e12-b2ca-f046b68fcc6a" -version = "0.23.1" +version = "0.23.2" authors = ["ITensor developers and contributors"] [workspace] diff --git a/src/TensorAlgebra.jl b/src/TensorAlgebra.jl index 69e2f30..a871162 100644 --- a/src/TensorAlgebra.jl +++ b/src/TensorAlgebra.jl @@ -5,7 +5,7 @@ export contract, contract!, contractalign, dual, isdual, MatrixAlgebra if VERSION >= v"1.11.0-DEV.469" eval( Meta.parse( - "public AbstractAlgorithm, add!, AddBroadcasted, addends, allocate_output, allocate_project, arguments, axes, bipartition, bipartition_axes, biperm, bipermutedims, bipermutedims!, bipermutedimsopadd!, cat_axis, cat_similar, check_input, concatenate, concatenate!, ConjBroadcasted, contractadd!, ContractAlgorithm, contractopadd!, contractperm, contractperm!, contractpermadd!, contractpermalign, contractpermopadd!, data, datatype, default_algorithm, dims2cat, directsum, eig_full, eig_trunc, eig_vals, eigh_full, eigh_trunc, eigh_vals, fill_map, flattenlinear, has_bipartition, infer_aux_space, invsqrth_safe, is_output_view, is_projected, isidentitybiperm, label_type, left_null, left_orth, left_polar, LinearBroadcasted, linearbroadcasted, lq_compact, lq_full, matricize, MatricizeContract, matricizeop, matricizeop!, matricizeopcopy, matricizeopview, MATRIX_FUNCTIONS, ndims, ndims_codomain, ndims_domain, one, ones_map, operation, output_axes, PermutedDims, permuteddims, permutedims, permutedims!, permutedimsadd!, permutedimsop, permutedimsopadd!, project, project!, project_aux, project_hermitian, projectto!, qr_compact, qr_full, rand_map, randn_map, right_null, right_orth, right_polar, scalar, scale!, scaleadd!, ScaledBroadcasted, select_algorithm, similar_map, size, sqrth_invsqrth_safe, sqrth_safe, sum, svd_compact, svd_full, svd_trunc, svd_vals, TensorOperationsContract, to_range, tr, trivialrange, tryflattenlinear, tryproject, tryproject_aux, twist!, unchecked_project, unchecked_project_aux, ungrade, unmatricize, unmatricize!, unmatricize_factors, unmatricizeadd!, unproject, unscaled, zero!, zeros_map" + "public AbstractAlgorithm, add!, AddBroadcasted, addends, allocate_output, allocate_project, arguments, axes, bipartition, bipartition_axes, biperm, bipermutedims, bipermutedims!, bipermutedimsopadd!, cat_axis, cat_similar, check_input, concatenate, concatenate!, ConjBroadcasted, contractadd!, ContractAlgorithm, contractopadd!, contractperm, contractperm!, contractpermadd!, contractpermalign, contractpermopadd!, data, datatype, default_algorithm, dims2cat, directsum, dotperm, eig_full, eig_trunc, eig_vals, eigh_full, eigh_trunc, eigh_vals, fill_map, flattenlinear, has_bipartition, infer_aux_space, invsqrth_safe, is_output_view, is_projected, isidentitybiperm, label_type, left_null, left_orth, left_polar, LinearBroadcasted, linearbroadcasted, lq_compact, lq_full, matricize, MatricizeContract, matricizeop, matricizeop!, matricizeopcopy, matricizeopview, MATRIX_FUNCTIONS, ndims, ndims_codomain, ndims_domain, one, ones_map, operation, output_axes, PermutedDims, permuteddims, permutedims, permutedims!, permutedimsadd!, permutedimsop, permutedimsopadd!, project, project!, project_aux, project_hermitian, projectto!, qr_compact, qr_full, rand_map, randn_map, right_null, right_orth, right_polar, scalar, scale!, scaleadd!, ScaledBroadcasted, select_algorithm, similar_map, size, sqrth_invsqrth_safe, sqrth_safe, sum, svd_compact, svd_full, svd_trunc, svd_vals, TensorOperationsContract, to_range, tr, trivialrange, tryflattenlinear, tryproject, tryproject_aux, twist!, unchecked_project, unchecked_project_aux, ungrade, unmatricize, unmatricize!, unmatricize_factors, unmatricizeadd!, unproject, unscaled, zero!, zeros_map" ) ) end @@ -17,6 +17,7 @@ include("MatrixAlgebra.jl") include("bituple.jl") include("permutedimsadd.jl") include("matricize.jl") +include("dotperm.jl") include("concatenate.jl") include("directsum.jl") include("dual.jl") diff --git a/src/dotperm.jl b/src/dotperm.jl new file mode 100644 index 0000000..ddbcd51 --- /dev/null +++ b/src/dotperm.jl @@ -0,0 +1,31 @@ +using LinearAlgebra: LinearAlgebra + +""" + dotperm(a, b, perm) + dotperm(a, b, perm_codomain, perm_domain) + +Compute the inner product of `a` with `b` after aligning the dimensions of `b`. +The first argument is conjugated, following `LinearAlgebra.dot`. + +The groups `perm_codomain` and `perm_domain` must partition the dimensions of `b`, +with `length(perm_codomain) == ndims_codomain(a)`. Both operands are matricized +with the codomain/domain split of `a`, then paired using `LinearAlgebra.dot` +on their native matrix representations. The three-argument form uses an empty domain. + +See also [`matricize`](@ref) and [`ndims_codomain`](@ref). +""" +function dotperm(a, b, perm) + return dotperm(a, b, perm, ()) +end + +function dotperm(a, b, perm_codomain, perm_domain) + check_biperm(b, perm_codomain, perm_domain) + length(perm_codomain) == ndims_codomain(a) || throw( + ArgumentError( + "The codomain permutation must match the codomain dimension count of the first operand" + ) + ) + return LinearAlgebra.dot( + matricize(a, Val(ndims_codomain(a))), matricize(b, perm_codomain, perm_domain) + ) +end diff --git a/test/test_dotperm.jl b/test/test_dotperm.jl new file mode 100644 index 0000000..4e19de5 --- /dev/null +++ b/test/test_dotperm.jl @@ -0,0 +1,46 @@ +using LinearAlgebra: dot, norm +using StableRNGs: StableRNG +using TensorAlgebra: dotperm +using TensorKit: TensorKit, FermionParity, Rep, SU₂, U₁, Vect, randn, ←, ⊗ +using TensorOperations: TensorOperations +using Test: @test, @test_throws, @testset + +@testset "dotperm dense (eltype=$elt)" for elt in (Float64, ComplexF64) + rng = StableRNG(123) + a = randn(rng, elt, 2, 3, 4) + b = randn(rng, elt, 3, 4, 2) + @test dotperm(a, a, (1, 2, 3)) ≈ norm(a)^2 + @test dotperm(a, b, (3, 1, 2)) ≈ dot(a, permutedims(b, (3, 1, 2))) + @test dotperm(a, b, (3, 1, 2), ()) ≈ dot(a, permutedims(b, (3, 1, 2))) + @test dotperm(im * a, b, (3, 1, 2)) ≈ -im * dotperm(a, b, (3, 1, 2)) + @test_throws ArgumentError dotperm(a, b, (3, 1), ()) + @test_throws ArgumentError dotperm(a, b, (3, 1, 1), ()) + @test_throws ArgumentError dotperm(a, b, (3, 1), (2,)) + @test_throws DimensionMismatch dotperm(a, zeros(elt, 3, 4, 3), (3, 1, 2)) + @test dotperm(fill(elt(2)), fill(elt(3)), ()) == 6 +end + +@testset "dotperm TensorKit (sector=$sector, eltype=$elt)" for sector in (U₁, SU₂), + elt in (Float64, ComplexF64) + + rng = StableRNG(456) + V = sector === U₁ ? Rep[U₁](0 => 2, 1 => 1) : Rep[SU₂](0 => 2, 1 // 2 => 1) + a = randn(rng, elt, V ⊗ V ← V) + b = randn(rng, elt, V ⊗ V ← V) + @test dotperm(a, a, (1, 2), (3,)) ≈ norm(a)^2 + @test dotperm(a, b, (1, 2), (3,)) ≈ dot(a, b) + @test dotperm(a, b, (2, 1), (3,)) ≈ dot(a, TensorKit.permute(b, ((2, 1), (3,)))) + @test_throws ArgumentError dotperm(a, b, (1, 2, 3)) +end + +@testset "dotperm fermionic TensorKit rebasing" begin + rng = StableRNG(789) + V = Vect[FermionParity](0 => 2, 1 => 2) + a = randn(rng, ComplexF64, V ⊗ V ← V) + b = randn(rng, ComplexF64, V ⊗ V ← V) + @test dotperm(a, b, (2, 1), (3,)) ≈ dot(a, TensorKit.permute(b, ((2, 1), (3,)))) + b_rebased = TensorKit.permute(b, ((1,), (2, 3))) + @test dotperm(a, b_rebased, (1, 2), (3,)) ≈ dot(a, b) + a_rebased = TensorKit.permute(a, ((1,), (2, 3))) + @test dotperm(a, a_rebased, (1, 2), (3,)) ≈ norm(a)^2 +end From 4e2891631a5f0d42e16ca285b104ca65bbe54f3e Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 7 Oct 2026 17:52:12 -0400 Subject: [PATCH 02/11] Add labeled tensor inner products Align tensor dimensions by labels before delegating to dotperm. --- src/TensorAlgebra.jl | 2 +- src/dotperm.jl | 34 ++++++++++++++++++++++++++++++++++ test/test_dotperm.jl | 29 ++++++++++++++++++++++++++++- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/TensorAlgebra.jl b/src/TensorAlgebra.jl index a871162..d5f03e6 100644 --- a/src/TensorAlgebra.jl +++ b/src/TensorAlgebra.jl @@ -5,7 +5,7 @@ export contract, contract!, contractalign, dual, isdual, MatrixAlgebra if VERSION >= v"1.11.0-DEV.469" eval( Meta.parse( - "public AbstractAlgorithm, add!, AddBroadcasted, addends, allocate_output, allocate_project, arguments, axes, bipartition, bipartition_axes, biperm, bipermutedims, bipermutedims!, bipermutedimsopadd!, cat_axis, cat_similar, check_input, concatenate, concatenate!, ConjBroadcasted, contractadd!, ContractAlgorithm, contractopadd!, contractperm, contractperm!, contractpermadd!, contractpermalign, contractpermopadd!, data, datatype, default_algorithm, dims2cat, directsum, dotperm, eig_full, eig_trunc, eig_vals, eigh_full, eigh_trunc, eigh_vals, fill_map, flattenlinear, has_bipartition, infer_aux_space, invsqrth_safe, is_output_view, is_projected, isidentitybiperm, label_type, left_null, left_orth, left_polar, LinearBroadcasted, linearbroadcasted, lq_compact, lq_full, matricize, MatricizeContract, matricizeop, matricizeop!, matricizeopcopy, matricizeopview, MATRIX_FUNCTIONS, ndims, ndims_codomain, ndims_domain, one, ones_map, operation, output_axes, PermutedDims, permuteddims, permutedims, permutedims!, permutedimsadd!, permutedimsop, permutedimsopadd!, project, project!, project_aux, project_hermitian, projectto!, qr_compact, qr_full, rand_map, randn_map, right_null, right_orth, right_polar, scalar, scale!, scaleadd!, ScaledBroadcasted, select_algorithm, similar_map, size, sqrth_invsqrth_safe, sqrth_safe, sum, svd_compact, svd_full, svd_trunc, svd_vals, TensorOperationsContract, to_range, tr, trivialrange, tryflattenlinear, tryproject, tryproject_aux, twist!, unchecked_project, unchecked_project_aux, ungrade, unmatricize, unmatricize!, unmatricize_factors, unmatricizeadd!, unproject, unscaled, zero!, zeros_map" + "public AbstractAlgorithm, add!, AddBroadcasted, addends, allocate_output, allocate_project, arguments, axes, bipartition, bipartition_axes, biperm, bipermutedims, bipermutedims!, bipermutedimsopadd!, cat_axis, cat_similar, check_input, concatenate, concatenate!, ConjBroadcasted, contractadd!, ContractAlgorithm, contractopadd!, contractperm, contractperm!, contractpermadd!, contractpermalign, contractpermopadd!, data, datatype, default_algorithm, dims2cat, directsum, dot, dotperm, eig_full, eig_trunc, eig_vals, eigh_full, eigh_trunc, eigh_vals, fill_map, flattenlinear, has_bipartition, infer_aux_space, invsqrth_safe, is_output_view, is_projected, isidentitybiperm, label_type, left_null, left_orth, left_polar, LinearBroadcasted, linearbroadcasted, lq_compact, lq_full, matricize, MatricizeContract, matricizeop, matricizeop!, matricizeopcopy, matricizeopview, MATRIX_FUNCTIONS, ndims, ndims_codomain, ndims_domain, one, ones_map, operation, output_axes, PermutedDims, permuteddims, permutedims, permutedims!, permutedimsadd!, permutedimsop, permutedimsopadd!, project, project!, project_aux, project_hermitian, projectto!, qr_compact, qr_full, rand_map, randn_map, right_null, right_orth, right_polar, scalar, scale!, scaleadd!, ScaledBroadcasted, select_algorithm, similar_map, size, sqrth_invsqrth_safe, sqrth_safe, sum, svd_compact, svd_full, svd_trunc, svd_vals, TensorOperationsContract, to_range, tr, trivialrange, tryflattenlinear, tryproject, tryproject_aux, twist!, unchecked_project, unchecked_project_aux, ungrade, unmatricize, unmatricize!, unmatricize_factors, unmatricizeadd!, unproject, unscaled, zero!, zeros_map" ) ) end diff --git a/src/dotperm.jl b/src/dotperm.jl index ddbcd51..f658dfd 100644 --- a/src/dotperm.jl +++ b/src/dotperm.jl @@ -29,3 +29,37 @@ function dotperm(a, b, perm_codomain, perm_domain) matricize(a, Val(ndims_codomain(a))), matricize(b, perm_codomain, perm_domain) ) end + +""" + TensorAlgebra.dot(a, labels_a, b, labels_b) + +Compute the inner product of `a` and `b`, aligning the dimensions of `b` by their labels. +Each operand must have one unique label per dimension, and both label sets must match. +The first argument is conjugated, following `LinearAlgebra.dot`. + +This is `TensorAlgebra`'s own function, distinct from `LinearAlgebra.dot`. It preserves +`a`'s codomain/domain split and delegates the aligned pairing to [`dotperm`](@ref). + +# Examples + +```jldoctest +julia> import TensorAlgebra + +julia> a = [1 2; 3 4]; + b = [5 6; 7 8]; + +julia> TensorAlgebra.dot(a, (:i, :j), b, (:j, :i)) +69 +``` + +See also [`dotperm`](@ref) and [`matricize`](@ref). +""" +function dot(a, labels_a, b, labels_b) + labels_a, labels_b = Tuple(labels_a), Tuple(labels_b) + length(labels_a) == ndims(a) && length(labels_b) == ndims(b) || + throw(ArgumentError("Each operand must have one label per dimension")) + allunique(labels_a) && allunique(labels_b) && issetequal(labels_a, labels_b) || + throw(ArgumentError("Inner product labels must be unique and matching")) + perm = map(label -> something(findfirst(isequal(label), labels_b)), labels_a) + return dotperm(a, b, bipartition(perm, Val(ndims_codomain(a)))...) +end diff --git a/test/test_dotperm.jl b/test/test_dotperm.jl index 4e19de5..2a6a0ed 100644 --- a/test/test_dotperm.jl +++ b/test/test_dotperm.jl @@ -1,6 +1,6 @@ using LinearAlgebra: dot, norm using StableRNGs: StableRNG -using TensorAlgebra: dotperm +using TensorAlgebra: TensorAlgebra, dotperm using TensorKit: TensorKit, FermionParity, Rep, SU₂, U₁, Vect, randn, ←, ⊗ using TensorOperations: TensorOperations using Test: @test, @test_throws, @testset @@ -44,3 +44,30 @@ end a_rebased = TensorKit.permute(a, ((1,), (2, 3))) @test dotperm(a, a_rebased, (1, 2), (3,)) ≈ norm(a)^2 end + +@testset "labeled dot" begin + @test TensorAlgebra.dot !== dot + rng = StableRNG(321) + a = randn(rng, ComplexF64, 2, 3, 4) + b = randn(rng, ComplexF64, 3, 4, 2) + expected = dot(a, permutedims(b, (3, 1, 2))) + @test TensorAlgebra.dot(a, (:i, :j, :k), b, (:j, :k, :i)) ≈ expected + @test TensorAlgebra.dot(a, [10, 20, 30], b, [20, 30, 10]) ≈ expected + @test TensorAlgebra.dot(im * a, (:i, :j, :k), b, (:j, :k, :i)) ≈ -im * expected + @test_throws ArgumentError TensorAlgebra.dot(a, (:i, :j), b, (:j, :k, :i)) + @test_throws ArgumentError TensorAlgebra.dot(a, (:i, :j, :k), b, (:j, :k)) + @test_throws ArgumentError TensorAlgebra.dot(a, (:i, :i, :k), b, (:j, :k, :i)) + @test_throws ArgumentError TensorAlgebra.dot(a, (:i, :j, :k), b, (:j, :j, :i)) + @test_throws ArgumentError TensorAlgebra.dot(a, (:i, :j, :k), b, (:j, :k, :l)) + @test TensorAlgebra.dot([1 2; 3 4], (:i, :j), [5 6; 7 8], (:j, :i)) == 69 +end + +@testset "labeled dot fermionic TensorKit rebasing" begin + rng = StableRNG(987) + V = Vect[FermionParity](0 => 2, 1 => 2) + a = randn(rng, ComplexF64, V ⊗ V ← V) + b = randn(rng, ComplexF64, V ⊗ V ← V) + b_rebased = TensorKit.permute(b, ((2,), (1, 3))) + @test TensorAlgebra.dot(a, (:i, :j, :k), b_rebased, (:j, :i, :k)) ≈ dot(a, b) + @test TensorAlgebra.dot(b_rebased, (:j, :i, :k), a, (:i, :j, :k)) ≈ conj(dot(a, b)) +end From 47b55ec14e2ba8bf1cfd635017a65f4c315511b0 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 7 Oct 2026 19:49:31 -0400 Subject: [PATCH 03/11] Use full permutations in dotperm --- src/dotperm.jl | 23 +++++++---------------- test/test_dotperm.jl | 30 +++++++++++++++++++----------- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/dotperm.jl b/src/dotperm.jl index f658dfd..63048c0 100644 --- a/src/dotperm.jl +++ b/src/dotperm.jl @@ -2,29 +2,20 @@ using LinearAlgebra: LinearAlgebra """ dotperm(a, b, perm) - dotperm(a, b, perm_codomain, perm_domain) Compute the inner product of `a` with `b` after aligning the dimensions of `b`. The first argument is conjugated, following `LinearAlgebra.dot`. -The groups `perm_codomain` and `perm_domain` must partition the dimensions of `b`, -with `length(perm_codomain) == ndims_codomain(a)`. Both operands are matricized -with the codomain/domain split of `a`, then paired using `LinearAlgebra.dot` -on their native matrix representations. The three-argument form uses an empty domain. +`perm` must be a permutation of the dimensions of `b`, bringing them into the order of `a`. +Both operands are matricized with the codomain/domain split of `a`, then paired using +`LinearAlgebra.dot` on their native matrix representations. See also [`matricize`](@ref) and [`ndims_codomain`](@ref). """ function dotperm(a, b, perm) - return dotperm(a, b, perm, ()) -end - -function dotperm(a, b, perm_codomain, perm_domain) - check_biperm(b, perm_codomain, perm_domain) - length(perm_codomain) == ndims_codomain(a) || throw( - ArgumentError( - "The codomain permutation must match the codomain dimension count of the first operand" - ) - ) + perm = Tuple(perm) + check_biperm(b, perm, ()) + perm_codomain, perm_domain = bipartition(perm, Val(ndims_codomain(a))) return LinearAlgebra.dot( matricize(a, Val(ndims_codomain(a))), matricize(b, perm_codomain, perm_domain) ) @@ -61,5 +52,5 @@ function dot(a, labels_a, b, labels_b) allunique(labels_a) && allunique(labels_b) && issetequal(labels_a, labels_b) || throw(ArgumentError("Inner product labels must be unique and matching")) perm = map(label -> something(findfirst(isequal(label), labels_b)), labels_a) - return dotperm(a, b, bipartition(perm, Val(ndims_codomain(a)))...) + return dotperm(a, b, perm) end diff --git a/test/test_dotperm.jl b/test/test_dotperm.jl index 2a6a0ed..51b8c8a 100644 --- a/test/test_dotperm.jl +++ b/test/test_dotperm.jl @@ -11,11 +11,11 @@ using Test: @test, @test_throws, @testset b = randn(rng, elt, 3, 4, 2) @test dotperm(a, a, (1, 2, 3)) ≈ norm(a)^2 @test dotperm(a, b, (3, 1, 2)) ≈ dot(a, permutedims(b, (3, 1, 2))) - @test dotperm(a, b, (3, 1, 2), ()) ≈ dot(a, permutedims(b, (3, 1, 2))) + @test dotperm(a, b, [3, 1, 2]) ≈ dot(a, permutedims(b, (3, 1, 2))) @test dotperm(im * a, b, (3, 1, 2)) ≈ -im * dotperm(a, b, (3, 1, 2)) - @test_throws ArgumentError dotperm(a, b, (3, 1), ()) - @test_throws ArgumentError dotperm(a, b, (3, 1, 1), ()) - @test_throws ArgumentError dotperm(a, b, (3, 1), (2,)) + @test_throws ArgumentError dotperm(a, b, (3, 1)) + @test_throws ArgumentError dotperm(a, b, (3, 1, 1)) + @test_throws ArgumentError dotperm(a, b, (3, 1, 4)) @test_throws DimensionMismatch dotperm(a, zeros(elt, 3, 4, 3), (3, 1, 2)) @test dotperm(fill(elt(2)), fill(elt(3)), ()) == 6 end @@ -27,10 +27,10 @@ end V = sector === U₁ ? Rep[U₁](0 => 2, 1 => 1) : Rep[SU₂](0 => 2, 1 // 2 => 1) a = randn(rng, elt, V ⊗ V ← V) b = randn(rng, elt, V ⊗ V ← V) - @test dotperm(a, a, (1, 2), (3,)) ≈ norm(a)^2 - @test dotperm(a, b, (1, 2), (3,)) ≈ dot(a, b) - @test dotperm(a, b, (2, 1), (3,)) ≈ dot(a, TensorKit.permute(b, ((2, 1), (3,)))) - @test_throws ArgumentError dotperm(a, b, (1, 2, 3)) + @test dotperm(a, a, (1, 2, 3)) ≈ norm(a)^2 + @test dotperm(a, b, (1, 2, 3)) ≈ dot(a, b) + @test dotperm(a, b, (2, 1, 3)) ≈ dot(a, TensorKit.permute(b, ((2, 1), (3,)))) + @test_throws ArgumentError dotperm(a, b, (1, 2)) end @testset "dotperm fermionic TensorKit rebasing" begin @@ -38,11 +38,19 @@ end V = Vect[FermionParity](0 => 2, 1 => 2) a = randn(rng, ComplexF64, V ⊗ V ← V) b = randn(rng, ComplexF64, V ⊗ V ← V) - @test dotperm(a, b, (2, 1), (3,)) ≈ dot(a, TensorKit.permute(b, ((2, 1), (3,)))) + @test dotperm(a, b, (2, 1, 3)) ≈ dot(a, TensorKit.permute(b, ((2, 1), (3,)))) b_rebased = TensorKit.permute(b, ((1,), (2, 3))) - @test dotperm(a, b_rebased, (1, 2), (3,)) ≈ dot(a, b) + @test dotperm(a, b_rebased, (1, 2, 3)) ≈ dot(a, b) a_rebased = TensorKit.permute(a, ((1,), (2, 3))) - @test dotperm(a, a_rebased, (1, 2), (3,)) ≈ norm(a)^2 + @test dotperm(a, a_rebased, (1, 2, 3)) ≈ norm(a)^2 + for n in 0:3 + pc, pd = Tuple(1:n), Tuple((n + 1):3) + a_split = TensorKit.permute(a, (pc, pd)) + b_split = TensorKit.permute(b, (pc, pd)) + @test dotperm(a_split, b_rebased, (1, 2, 3)) ≈ dot(a_split, b_split) + end + b_permuted = TensorKit.permute(b, ((2,), (1, 3))) + @test dotperm(a, b_permuted, (2, 1, 3)) ≈ dot(a, b) end @testset "labeled dot" begin From a08d6c2b3976400b35b62d6be9fb70640854c626 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 7 Oct 2026 19:51:06 -0400 Subject: [PATCH 04/11] Tighten dotperm documentation --- src/dotperm.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dotperm.jl b/src/dotperm.jl index 63048c0..5e13ed1 100644 --- a/src/dotperm.jl +++ b/src/dotperm.jl @@ -10,7 +10,7 @@ The first argument is conjugated, following `LinearAlgebra.dot`. Both operands are matricized with the codomain/domain split of `a`, then paired using `LinearAlgebra.dot` on their native matrix representations. -See also [`matricize`](@ref) and [`ndims_codomain`](@ref). +See also [`dot`](@ref). """ function dotperm(a, b, perm) perm = Tuple(perm) From 6db3b7e661e0510d3d20bf81b36b55da2bcf6238 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 7 Oct 2026 20:01:33 -0400 Subject: [PATCH 05/11] Validate aligned axes in tensor inner products --- src/dotperm.jl | 45 ++++++++++++++++++++++++++++---------------- test/test_dotperm.jl | 9 +++++++++ 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/dotperm.jl b/src/dotperm.jl index 5e13ed1..8b5aaa2 100644 --- a/src/dotperm.jl +++ b/src/dotperm.jl @@ -3,33 +3,45 @@ using LinearAlgebra: LinearAlgebra """ dotperm(a, b, perm) -Compute the inner product of `a` with `b` after aligning the dimensions of `b`. -The first argument is conjugated, following `LinearAlgebra.dot`. - -`perm` must be a permutation of the dimensions of `b`, bringing them into the order of `a`. -Both operands are matricized with the codomain/domain split of `a`, then paired using -`LinearAlgebra.dot` on their native matrix representations. +Compute the inner product of `a` and `b` after applying the full dimension permutation +`perm` to `b`, using the codomain/domain split of `a`. +The aligned axes must match: `axes(a, i) == axes(b, perm[i])`. See also [`dot`](@ref). """ function dotperm(a, b, perm) - perm = Tuple(perm) - check_biperm(b, perm, ()) + check_input(dotperm, a, b, perm) + perm = NTuple{ndims(a)}(perm) perm_codomain, perm_domain = bipartition(perm, Val(ndims_codomain(a))) return LinearAlgebra.dot( matricize(a, Val(ndims_codomain(a))), matricize(b, perm_codomain, perm_domain) ) end +function check_input(::typeof(dotperm), a, b, perm) + length(perm) == ndims(a) || + throw( + ArgumentError( + "Permutation length must match the first operand's dimension count" + ) + ) + check_biperm(b, perm, ()) + for i in 1:ndims(a) + axes(a, i) == axes(b, perm[i]) || throw( + DimensionMismatch( + "Inner product axes do not match: axes(a, $i) = $(axes(a, i)), axes(b, $(perm[i])) = $(axes(b, perm[i]))" + ) + ) + end + return nothing +end + """ TensorAlgebra.dot(a, labels_a, b, labels_b) -Compute the inner product of `a` and `b`, aligning the dimensions of `b` by their labels. -Each operand must have one unique label per dimension, and both label sets must match. -The first argument is conjugated, following `LinearAlgebra.dot`. - -This is `TensorAlgebra`'s own function, distinct from `LinearAlgebra.dot`. It preserves -`a`'s codomain/domain split and delegates the aligned pairing to [`dotperm`](@ref). +Compute the inner product of `a` and `b` after permuting the dimensions of `b` to match +the labels of `a`. Each operand must have one unique label per dimension, and both label +sets must match. # Examples @@ -51,6 +63,7 @@ function dot(a, labels_a, b, labels_b) throw(ArgumentError("Each operand must have one label per dimension")) allunique(labels_a) && allunique(labels_b) && issetequal(labels_a, labels_b) || throw(ArgumentError("Inner product labels must be unique and matching")) - perm = map(label -> something(findfirst(isequal(label), labels_b)), labels_a) - return dotperm(a, b, perm) + labels_codomain, labels_domain = bipartition(labels_a, Val(ndims_codomain(a))) + perm_codomain, perm_domain = biperm(labels_b, labels_codomain, labels_domain) + return dotperm(a, b, (perm_codomain..., perm_domain...)) end diff --git a/test/test_dotperm.jl b/test/test_dotperm.jl index 51b8c8a..c4bbc07 100644 --- a/test/test_dotperm.jl +++ b/test/test_dotperm.jl @@ -14,9 +14,12 @@ using Test: @test, @test_throws, @testset @test dotperm(a, b, [3, 1, 2]) ≈ dot(a, permutedims(b, (3, 1, 2))) @test dotperm(im * a, b, (3, 1, 2)) ≈ -im * dotperm(a, b, (3, 1, 2)) @test_throws ArgumentError dotperm(a, b, (3, 1)) + @test_throws ArgumentError dotperm(a, b, [3, 1]) + @test_throws ArgumentError dotperm(a, b, [3, 1, 2, 4]) @test_throws ArgumentError dotperm(a, b, (3, 1, 1)) @test_throws ArgumentError dotperm(a, b, (3, 1, 4)) @test_throws DimensionMismatch dotperm(a, zeros(elt, 3, 4, 3), (3, 1, 2)) + @test_throws DimensionMismatch dotperm(a, zeros(elt, 4, 3, 2), (3, 1, 2)) @test dotperm(fill(elt(2)), fill(elt(3)), ()) == 6 end @@ -67,6 +70,12 @@ end @test_throws ArgumentError TensorAlgebra.dot(a, (:i, :i, :k), b, (:j, :k, :i)) @test_throws ArgumentError TensorAlgebra.dot(a, (:i, :j, :k), b, (:j, :j, :i)) @test_throws ArgumentError TensorAlgebra.dot(a, (:i, :j, :k), b, (:j, :k, :l)) + @test_throws DimensionMismatch TensorAlgebra.dot( + a, + (:i, :j, :k), + zeros(ComplexF64, 4, 3, 2), + (:j, :k, :i) + ) @test TensorAlgebra.dot([1 2; 3 4], (:i, :j), [5 6; 7 8], (:j, :i)) == 69 end From 15398cadccdae9ce321aa540b046e4efded45f91 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 7 Oct 2026 20:02:33 -0400 Subject: [PATCH 06/11] Document dotperm with a permutation example --- src/dotperm.jl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/dotperm.jl b/src/dotperm.jl index 8b5aaa2..3a85b75 100644 --- a/src/dotperm.jl +++ b/src/dotperm.jl @@ -7,6 +7,15 @@ Compute the inner product of `a` and `b` after applying the full dimension permu `perm` to `b`, using the codomain/domain split of `a`. The aligned axes must match: `axes(a, i) == axes(b, perm[i])`. +# Examples + +```jldoctest +julia> import TensorAlgebra + +julia> TensorAlgebra.dotperm([1 2; 3 4], [5 6; 7 8], (2, 1)) +69 +``` + See also [`dot`](@ref). """ function dotperm(a, b, perm) From 0afe0830322a6e4563395455d46bb55770d35936 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 7 Oct 2026 23:03:05 -0400 Subject: [PATCH 07/11] Clarify dotperm axis documentation --- src/dotperm.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dotperm.jl b/src/dotperm.jl index 3a85b75..4cb1300 100644 --- a/src/dotperm.jl +++ b/src/dotperm.jl @@ -5,7 +5,7 @@ using LinearAlgebra: LinearAlgebra Compute the inner product of `a` and `b` after applying the full dimension permutation `perm` to `b`, using the codomain/domain split of `a`. -The aligned axes must match: `axes(a, i) == axes(b, perm[i])`. +The permuted axes must match: `axes(a, i) == axes(b, perm[i])`. # Examples From aecd1c64ffd3922790e1696dca5902e50953fc28 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 7 Oct 2026 23:08:55 -0400 Subject: [PATCH 08/11] Use concrete tuples for labeled dot --- src/dotperm.jl | 10 ++++++---- test/test_dotperm.jl | 2 ++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/dotperm.jl b/src/dotperm.jl index 4cb1300..c522463 100644 --- a/src/dotperm.jl +++ b/src/dotperm.jl @@ -67,12 +67,14 @@ julia> TensorAlgebra.dot(a, (:i, :j), b, (:j, :i)) See also [`dotperm`](@ref) and [`matricize`](@ref). """ function dot(a, labels_a, b, labels_b) - labels_a, labels_b = Tuple(labels_a), Tuple(labels_b) length(labels_a) == ndims(a) && length(labels_b) == ndims(b) || throw(ArgumentError("Each operand must have one label per dimension")) - allunique(labels_a) && allunique(labels_b) && issetequal(labels_a, labels_b) || + labels_a_tuple = NTuple{ndims(a)}(labels_a) + labels_b_tuple = NTuple{ndims(b)}(labels_b) + allunique(labels_a_tuple) && allunique(labels_b_tuple) && + issetequal(labels_a_tuple, labels_b_tuple) || throw(ArgumentError("Inner product labels must be unique and matching")) - labels_codomain, labels_domain = bipartition(labels_a, Val(ndims_codomain(a))) - perm_codomain, perm_domain = biperm(labels_b, labels_codomain, labels_domain) + labels_codomain, labels_domain = bipartition(labels_a_tuple, Val(ndims_codomain(a))) + perm_codomain, perm_domain = biperm(labels_b_tuple, labels_codomain, labels_domain) return dotperm(a, b, (perm_codomain..., perm_domain...)) end diff --git a/test/test_dotperm.jl b/test/test_dotperm.jl index c4bbc07..7ccdc1e 100644 --- a/test/test_dotperm.jl +++ b/test/test_dotperm.jl @@ -66,6 +66,8 @@ end @test TensorAlgebra.dot(a, [10, 20, 30], b, [20, 30, 10]) ≈ expected @test TensorAlgebra.dot(im * a, (:i, :j, :k), b, (:j, :k, :i)) ≈ -im * expected @test_throws ArgumentError TensorAlgebra.dot(a, (:i, :j), b, (:j, :k, :i)) + @test_throws ArgumentError TensorAlgebra.dot(a, [10, 20], b, [20, 30, 10]) + @test_throws ArgumentError TensorAlgebra.dot(a, [10, 20, 30], b, [20, 30, 10, 40]) @test_throws ArgumentError TensorAlgebra.dot(a, (:i, :j, :k), b, (:j, :k)) @test_throws ArgumentError TensorAlgebra.dot(a, (:i, :i, :k), b, (:j, :k, :i)) @test_throws ArgumentError TensorAlgebra.dot(a, (:i, :j, :k), b, (:j, :j, :i)) From 409580e4b88bcfcbb45fa25a7e00ea557a2a6a33 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 8 Oct 2026 11:40:44 -0400 Subject: [PATCH 09/11] Separate labeled inner product validation from permutation mapping --- src/dotperm.jl | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/dotperm.jl b/src/dotperm.jl index c522463..fa6dff2 100644 --- a/src/dotperm.jl +++ b/src/dotperm.jl @@ -67,14 +67,17 @@ julia> TensorAlgebra.dot(a, (:i, :j), b, (:j, :i)) See also [`dotperm`](@ref) and [`matricize`](@ref). """ function dot(a, labels_a, b, labels_b) - length(labels_a) == ndims(a) && length(labels_b) == ndims(b) || - throw(ArgumentError("Each operand must have one label per dimension")) + check_input(dot, a, labels_a, b, labels_b) labels_a_tuple = NTuple{ndims(a)}(labels_a) labels_b_tuple = NTuple{ndims(b)}(labels_b) - allunique(labels_a_tuple) && allunique(labels_b_tuple) && - issetequal(labels_a_tuple, labels_b_tuple) || + perm = tuple_indexin(labels_a_tuple, labels_b_tuple) + return dotperm(a, b, perm) +end + +function check_input(::typeof(dot), a, labels_a, b, labels_b) + length(labels_a) == ndims(a) && length(labels_b) == ndims(b) || + throw(ArgumentError("Each operand must have one label per dimension")) + allunique(labels_a) && allunique(labels_b) && issetequal(labels_a, labels_b) || throw(ArgumentError("Inner product labels must be unique and matching")) - labels_codomain, labels_domain = bipartition(labels_a_tuple, Val(ndims_codomain(a))) - perm_codomain, perm_domain = biperm(labels_b_tuple, labels_codomain, labels_domain) - return dotperm(a, b, (perm_codomain..., perm_domain...)) + return nothing end From 5a6fa903043e1c377050a8803d41d4ff7b8740e6 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 8 Oct 2026 11:47:45 -0400 Subject: [PATCH 10/11] Validate full inner product permutations directly --- src/dotperm.jl | 8 +++----- test/test_dotperm.jl | 1 + 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/dotperm.jl b/src/dotperm.jl index fa6dff2..20855c3 100644 --- a/src/dotperm.jl +++ b/src/dotperm.jl @@ -28,13 +28,11 @@ function dotperm(a, b, perm) end function check_input(::typeof(dotperm), a, b, perm) - length(perm) == ndims(a) || + ndims(a) == length(perm) == ndims(b) || throw( - ArgumentError( - "Permutation length must match the first operand's dimension count" - ) + ArgumentError("Permutation length must match both operands' dimension counts") ) - check_biperm(b, perm, ()) + isperm(perm) || throw(ArgumentError("Invalid dimension permutation")) for i in 1:ndims(a) axes(a, i) == axes(b, perm[i]) || throw( DimensionMismatch( diff --git a/test/test_dotperm.jl b/test/test_dotperm.jl index 7ccdc1e..86bdd7d 100644 --- a/test/test_dotperm.jl +++ b/test/test_dotperm.jl @@ -18,6 +18,7 @@ using Test: @test, @test_throws, @testset @test_throws ArgumentError dotperm(a, b, [3, 1, 2, 4]) @test_throws ArgumentError dotperm(a, b, (3, 1, 1)) @test_throws ArgumentError dotperm(a, b, (3, 1, 4)) + @test_throws ArgumentError dotperm(a, vec(b), (3, 1, 2)) @test_throws DimensionMismatch dotperm(a, zeros(elt, 3, 4, 3), (3, 1, 2)) @test_throws DimensionMismatch dotperm(a, zeros(elt, 4, 3, 2), (3, 1, 2)) @test dotperm(fill(elt(2)), fill(elt(3)), ()) == 6 From fb336b41fd3ca5b5c351d0260ff6f8d2bc01db63 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 8 Oct 2026 12:42:35 -0400 Subject: [PATCH 11/11] Include inner products in public API checks --- test/test_exports.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_exports.jl b/test/test_exports.jl index ca60fdf..67e6b20 100644 --- a/test/test_exports.jl +++ b/test/test_exports.jl @@ -26,7 +26,8 @@ using Test: @test, @testset :contractperm!, :contractpermadd!, :contractpermalign, :contractpermopadd!, :data, :datatype, - :default_algorithm, :dims2cat, :directsum, :eig_full, :eig_trunc, + :default_algorithm, :dims2cat, :directsum, :dot, :dotperm, :eig_full, + :eig_trunc, :eig_vals, :eigh_full, :eigh_trunc, :eigh_vals, :fill_map, :flattenlinear, :has_bipartition, :infer_aux_space, :invsqrth_safe, :is_output_view,