From 91e6c696b1a218c0f2a2ddfe5a67836ec97803e4 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 2 Jul 2026 16:44:08 -0400 Subject: [PATCH 1/5] Add named project for building symmetry-restricted tensors Adds methods extending TensorAlgebra's `project` and `checked_project` that build a named tensor from a dense array and the indices to attach. The three-argument form takes a codomain/domain split (an operator), the two-argument form a flat list of indices (a state). The index axes select the backend, so the same call builds an `Array`, a block-sparse array, or a `TensorMap`. The named entry strips to the unnamed array plus axes, lowers to `TensorAlgebra.project`, and reattaches the names. --- Project.toml | 4 ++++ src/tensoralgebra.jl | 47 ++++++++++++++++++++++++++++++++++++++ test/test_tensoralgebra.jl | 23 ++++++++++++++++--- test/test_tensorkitext.jl | 31 +++++++++++++++++++++++-- 4 files changed, 100 insertions(+), 5 deletions(-) diff --git a/Project.toml b/Project.toml index f4aea5f..2294195 100644 --- a/Project.toml +++ b/Project.toml @@ -32,6 +32,10 @@ OMEinsumContractionOrders = "6f22d1fd-8eed-4bb7-9776-e7d684900715" TensorKit = "07d1fe3e-3e46-537d-9eac-e9e13d0d4cec" TensorOperations = "6aa20fa7-93e2-5fca-9bc0-fbd0db3c71a2" +[sources.TensorAlgebra] +rev = "mf/projectto-tensormap" +url = "https://github.com/ITensor/TensorAlgebra.jl" + [extensions] ITensorBaseAdaptExt = "Adapt" ITensorBaseMooncakeExt = "Mooncake" diff --git a/src/tensoralgebra.jl b/src/tensoralgebra.jl index e708b69..9bc6cba 100644 --- a/src/tensoralgebra.jl +++ b/src/tensoralgebra.jl @@ -508,3 +508,50 @@ for f in MATRIX_FUNCTIONS end end end + +# +# Projection into a symmetry-restricted named tensor. +# + +""" + TensorAlgebra.project(a::AbstractArray, codomain_inds, domain_inds) -> t + TensorAlgebra.project(a::AbstractArray, inds) -> t + +Build a named tensor from the dense array `a` by projecting it into the +symmetry-restricted space described by the indices. The three-argument form +takes an explicit codomain/domain split (an operator); the two-argument form +takes a flat list of indices (a state, i.e. an empty domain). The index axes +select the backend: dense ranges give an `Array`, graded ranges a block-sparse +array, and TensorKit spaces a `TensorMap`. `a` is indexed positionally in the +order `(codomain_inds..., domain_inds...)`. + +See [`TensorAlgebra.checked_project`](@ref) for a version that verifies nothing +outside the symmetry-allowed blocks was discarded. +""" +function TA.project( + a::AbstractArray, + codomain_inds::Tuple{NamedUnitRange, Vararg{NamedUnitRange}}, + domain_inds::Tuple{Vararg{NamedUnitRange}} + ) + raw = TA.project(a, unnamed.(codomain_inds), unnamed.(domain_inds)) + return nameddims(raw, (name.(codomain_inds)..., name.(domain_inds)...)) +end +function TA.project(a::AbstractArray, inds::Tuple{NamedUnitRange, Vararg{NamedUnitRange}}) + raw = TA.project(a, unnamed.(inds)) + return nameddims(raw, name.(inds)) +end + +function TA.checked_project( + a::AbstractArray, + codomain_inds::Tuple{NamedUnitRange, Vararg{NamedUnitRange}}, + domain_inds::Tuple{Vararg{NamedUnitRange}}; kwargs... + ) + raw = TA.checked_project(a, unnamed.(codomain_inds), unnamed.(domain_inds); kwargs...) + return nameddims(raw, (name.(codomain_inds)..., name.(domain_inds)...)) +end +function TA.checked_project( + a::AbstractArray, inds::Tuple{NamedUnitRange, Vararg{NamedUnitRange}}; kwargs... + ) + raw = TA.checked_project(a, unnamed.(inds); kwargs...) + return nameddims(raw, name.(inds)) +end diff --git a/test/test_tensoralgebra.jl b/test/test_tensoralgebra.jl index 835eabd..1065f60 100644 --- a/test/test_tensoralgebra.jl +++ b/test/test_tensoralgebra.jl @@ -1,11 +1,12 @@ -using ITensorBase: - ITensorBase, dimnames, inds, namedoneto, replacedimnames, uniquename, unname, unnamed +using ITensorBase: ITensorBase, Index, dimnames, inds, name, namedoneto, prime, + replacedimnames, uniquename, unname, unnamed using LinearAlgebra: LinearAlgebra, norm 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 using StableRNGs: StableRNG using TensorAlgebra.MatrixAlgebra: gram_eigh_full, gram_eigh_full_with_pinv -using TensorAlgebra: TensorAlgebra, contract, matricize, unmatricize +using TensorAlgebra: + TensorAlgebra, checked_project, contract, matricize, project, unmatricize using Test: @test, @test_broken, @testset @testset "TensorAlgebra (eltype=$(elt))" for elt in @@ -156,4 +157,20 @@ using Test: @test, @test_broken, @testset @test YXmat ≈ LinearAlgebra.I(size(YXmat, 1)) end end + @testset "project" begin + i = Index(2) + Sz = elt[0.5 0; 0 -0.5] + # the three-argument form builds an operator from the codomain/domain split + top = project(Sz, (prime(i),), (i,)) + @test eltype(top) === elt + @test Set(dimnames(top)) == Set(name.((prime(i), i))) + @test unname(top, (prime(i), i)) == Sz + # `checked_project` accepts the (for dense, always exact) projection + @test unname(checked_project(Sz, (prime(i),), (i,)), (prime(i), i)) == Sz + # the two-argument form builds a state (empty domain) + v = elt[1, 0] + s = project(v, (i,)) + @test dimnames(s) == [name(i)] + @test unname(s, (i,)) == v + end end diff --git a/test/test_tensorkitext.jl b/test/test_tensorkitext.jl index 26930ff..76dd925 100644 --- a/test/test_tensorkitext.jl +++ b/test/test_tensorkitext.jl @@ -1,9 +1,10 @@ -using ITensorBase: ITensorBase, Index, dimnames, name, unnamed +using ITensorBase: ITensorBase, Index, dimnames, name, prime, unnamed using LinearAlgebra: norm using MatrixAlgebraKit: qr_compact, svd_compact using StableRNGs: StableRNG +using TensorAlgebra: checked_project, project using TensorKit: TensorKit, @tensor, AbstractTensorMap, SU2Irrep, U1Irrep, Vect, dim, dual, - scalar, space, ⊗ + scalar, space, ←, ⊗ using Test: @test, @test_throws, @testset # A native TensorKit space flows into `Index`, so an `ITensor` wraps a `TensorMap` directly. @@ -68,4 +69,30 @@ using Test: @test, @test_throws, @testset q, r = qr_compact(a3, (i,), (j, k)) @test sca((q * r) * w) ≈ sca(a3 * w) end + + # `project` builds a `TensorMap`-backed operator/state from a dense basis matrix: the index + # spaces select the backend, so the same call that yields an `Array` on dense indices yields a + # `TensorMap` here, keeping only the symmetry-allowed blocks. + @testset "project" begin + W = Vect[U1Irrep](0 => 1, 1 => 1) + w = Index(W) + Sz = elt[0.5 0; 0 -0.5] + Sx = elt[0 0.5; 0.5 0] + + top = project(Sz, (prime(w),), (w,)) + @test unnamed(top) isa AbstractTensorMap + @test space(unnamed(top)) == (W ← W) + @test Set(dimnames(top)) == Set(name.((prime(w), w))) + + # a charge-breaking operator is projected to zero; `checked_project` rejects the discard + @test norm(unnamed(project(Sx, (prime(w),), (w,)))) == 0 + @test_throws InexactError checked_project(Sx, (prime(w),), (w,); atol = 0, rtol = 0) + + # the two-argument form builds an all-codomain state; only the trivial-charge component + # of the dense vector survives + pv = project(elt[1, 0], (w,)) + @test unnamed(pv) isa AbstractTensorMap + @test norm(unnamed(pv)) ≈ 1 + @test norm(unnamed(project(elt[0, 1], (w,)))) == 0 + end end From e5f904ddad11e411adf794a3087703af0748ff76 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 2 Jul 2026 17:03:53 -0400 Subject: [PATCH 2/5] Handle an empty codomain in named project Route `project`/`checked_project` through a shared `project_nameddims` / `checked_project_nameddims` helper and add the empty-codomain method, so an all-domain projection (`project(a, (), (i,))`, the mirror of the flat all-codomain state) strips names correctly instead of falling through to the unnamed-axis generic. This matches the two-dispatch-entry pattern of the `similar_map` construction hooks. --- src/tensoralgebra.jl | 40 ++++++++++++++++++++++++++++++-------- test/test_tensoralgebra.jl | 4 ++++ test/test_tensorkitext.jl | 6 ++++++ 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/tensoralgebra.jl b/src/tensoralgebra.jl index 9bc6cba..d955d07 100644 --- a/src/tensoralgebra.jl +++ b/src/tensoralgebra.jl @@ -513,6 +513,20 @@ end # Projection into a symmetry-restricted named tensor. # +# Shared implementation: strip to `a`'s axes, lower to the TensorAlgebra hook, and reattach the +# names. The dispatch entries below feed this from either a codomain/domain split or a flat list +# (an empty domain). Two split entries per function read the index type from whichever side is +# non-empty (an empty codomain is the all-domain case, the mirror of the empty-domain state), so +# neither an empty codomain nor an empty domain falls through to the unnamed-axis generic. +function project_nameddims(a, codomain_inds, domain_inds) + raw = TA.project(a, unnamed.(codomain_inds), unnamed.(domain_inds)) + return nameddims(raw, (name.(codomain_inds)..., name.(domain_inds)...)) +end +function checked_project_nameddims(a, codomain_inds, domain_inds; kwargs...) + raw = TA.checked_project(a, unnamed.(codomain_inds), unnamed.(domain_inds); kwargs...) + return nameddims(raw, (name.(codomain_inds)..., name.(domain_inds)...)) +end + """ TensorAlgebra.project(a::AbstractArray, codomain_inds, domain_inds) -> t TensorAlgebra.project(a::AbstractArray, inds) -> t @@ -533,12 +547,17 @@ function TA.project( codomain_inds::Tuple{NamedUnitRange, Vararg{NamedUnitRange}}, domain_inds::Tuple{Vararg{NamedUnitRange}} ) - raw = TA.project(a, unnamed.(codomain_inds), unnamed.(domain_inds)) - return nameddims(raw, (name.(codomain_inds)..., name.(domain_inds)...)) + return project_nameddims(a, codomain_inds, domain_inds) +end +function TA.project( + a::AbstractArray, + codomain_inds::Tuple{}, + domain_inds::Tuple{NamedUnitRange, Vararg{NamedUnitRange}} + ) + return project_nameddims(a, codomain_inds, domain_inds) end function TA.project(a::AbstractArray, inds::Tuple{NamedUnitRange, Vararg{NamedUnitRange}}) - raw = TA.project(a, unnamed.(inds)) - return nameddims(raw, name.(inds)) + return project_nameddims(a, inds, ()) end function TA.checked_project( @@ -546,12 +565,17 @@ function TA.checked_project( codomain_inds::Tuple{NamedUnitRange, Vararg{NamedUnitRange}}, domain_inds::Tuple{Vararg{NamedUnitRange}}; kwargs... ) - raw = TA.checked_project(a, unnamed.(codomain_inds), unnamed.(domain_inds); kwargs...) - return nameddims(raw, (name.(codomain_inds)..., name.(domain_inds)...)) + return checked_project_nameddims(a, codomain_inds, domain_inds; kwargs...) +end +function TA.checked_project( + a::AbstractArray, + codomain_inds::Tuple{}, + domain_inds::Tuple{NamedUnitRange, Vararg{NamedUnitRange}}; kwargs... + ) + return checked_project_nameddims(a, codomain_inds, domain_inds; kwargs...) end function TA.checked_project( a::AbstractArray, inds::Tuple{NamedUnitRange, Vararg{NamedUnitRange}}; kwargs... ) - raw = TA.checked_project(a, unnamed.(inds); kwargs...) - return nameddims(raw, name.(inds)) + return checked_project_nameddims(a, inds, (); kwargs...) end diff --git a/test/test_tensoralgebra.jl b/test/test_tensoralgebra.jl index 1065f60..3998e50 100644 --- a/test/test_tensoralgebra.jl +++ b/test/test_tensoralgebra.jl @@ -172,5 +172,9 @@ using Test: @test, @test_broken, @testset s = project(v, (i,)) @test dimnames(s) == [name(i)] @test unname(s, (i,)) == v + # the empty-codomain form builds an all-domain tensor (mirror of the state) + bra = project(v, (), (i,)) + @test dimnames(bra) == [name(i)] + @test unname(bra, (i,)) == v end end diff --git a/test/test_tensorkitext.jl b/test/test_tensorkitext.jl index 76dd925..c1be719 100644 --- a/test/test_tensorkitext.jl +++ b/test/test_tensorkitext.jl @@ -94,5 +94,11 @@ using Test: @test, @test_throws, @testset @test unnamed(pv) isa AbstractTensorMap @test norm(unnamed(pv)) ≈ 1 @test norm(unnamed(project(elt[0, 1], (w,)))) == 0 + + # the empty-codomain form builds an all-domain `TensorMap` (the mirror case) + cobra = project(elt[1, 0], (), (w,)) + @test unnamed(cobra) isa AbstractTensorMap + @test space(unnamed(cobra)) == (one(W) ← W) + @test Set(dimnames(cobra)) == Set((name(w),)) end end From d92ad55faf13220801341364b92bb3689e7ad221 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 2 Jul 2026 18:22:57 -0400 Subject: [PATCH 3/5] Drop the TensorAlgebra branch pin now that 0.16.4 is registered --- Project.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Project.toml b/Project.toml index 2294195..f4aea5f 100644 --- a/Project.toml +++ b/Project.toml @@ -32,10 +32,6 @@ OMEinsumContractionOrders = "6f22d1fd-8eed-4bb7-9776-e7d684900715" TensorKit = "07d1fe3e-3e46-537d-9eac-e9e13d0d4cec" TensorOperations = "6aa20fa7-93e2-5fca-9bc0-fbd0db3c71a2" -[sources.TensorAlgebra] -rev = "mf/projectto-tensormap" -url = "https://github.com/ITensor/TensorAlgebra.jl" - [extensions] ITensorBaseAdaptExt = "Adapt" ITensorBaseMooncakeExt = "Mooncake" From 4325e43c007128bf5c1e4c72154f66ab0e1d00e6 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 2 Jul 2026 18:29:47 -0400 Subject: [PATCH 4/5] Drop an unresolvable docstring cross-reference in named project --- src/tensoralgebra.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tensoralgebra.jl b/src/tensoralgebra.jl index d955d07..0dfebf3 100644 --- a/src/tensoralgebra.jl +++ b/src/tensoralgebra.jl @@ -539,8 +539,8 @@ select the backend: dense ranges give an `Array`, graded ranges a block-sparse array, and TensorKit spaces a `TensorMap`. `a` is indexed positionally in the order `(codomain_inds..., domain_inds...)`. -See [`TensorAlgebra.checked_project`](@ref) for a version that verifies nothing -outside the symmetry-allowed blocks was discarded. +See `TensorAlgebra.checked_project` for a version that verifies nothing outside +the symmetry-allowed blocks was discarded. """ function TA.project( a::AbstractArray, From c1df9a59c2c51f5ad64e4be230e0412e65a78701 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 2 Jul 2026 19:00:20 -0400 Subject: [PATCH 5/5] Bump version to 0.10.7 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index f4aea5f..d2840c0 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ITensorBase" uuid = "4795dd04-0d67-49bb-8f44-b89c448a1dc7" -version = "0.10.6" +version = "0.10.7" authors = ["ITensor developers and contributors"] [workspace]