From 12bf1222f26228d2bdf1fc7405386389e255c33f Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 8 Jul 2026 14:34:33 -0400 Subject: [PATCH 1/6] =?UTF-8?q?Add=20index-tuple=20constructor=20error,=20?= =?UTF-8?q?index-keyed=20replacedimnames,=20svd=5Ftrunc=20=CF=B5,=20trivia?= =?UTF-8?q?lrange=20for=20named=20ranges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bugfixes and additive index utilities. `ITensor(array, Index-tuple)` now throws with guidance toward `array[i, j]` and `ITensor(array, name.((i, j)))` instead of silently mis-constructing. `replacedimnames` accepts `Index` keys by stripping to the dimname. `svd_trunc` returns the truncation error `ϵ` as a fourth output. And `trivialrange(::NamedUnitRange)` mints a fresh trivial named range on the same backend. --- Project.toml | 6 +++++- docs/Project.toml | 2 +- examples/Project.toml | 2 +- src/abstractnamedtensor.jl | 6 ++++++ src/namedtensor.jl | 20 ++++++++++++------ src/namedunitrange.jl | 11 +++++++++- src/tensoralgebra.jl | 37 +++++++++++++++++++++++++++++++- test/Project.toml | 4 ++-- test/test_basics.jl | 16 ++++++++++---- test/test_tensoralgebra.jl | 43 +++++++++++++++++++++++++++++++++----- 10 files changed, 125 insertions(+), 22 deletions(-) diff --git a/Project.toml b/Project.toml index 0d15309..4b40170 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ITensorBase" uuid = "4795dd04-0d67-49bb-8f44-b89c448a1dc7" -version = "0.10.9" +version = "0.11.0" authors = ["ITensor developers and contributors"] [workspace] @@ -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/nextgen-followups-round1" +url = "https://github.com/ITensor/TensorAlgebra.jl" + [extensions] ITensorBaseAdaptExt = "Adapt" ITensorBaseMooncakeExt = "Mooncake" diff --git a/docs/Project.toml b/docs/Project.toml index f52464c..d54500c 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -12,7 +12,7 @@ path = ".." [compat] Documenter = "1" -ITensorBase = "0.10" +ITensorBase = "0.11" ITensorFormatter = "0.2.27" Literate = "2" MatrixAlgebraKit = "0.2, 0.3, 0.4, 0.5, 0.6" diff --git a/examples/Project.toml b/examples/Project.toml index c1a24b5..5af97d2 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -6,5 +6,5 @@ MatrixAlgebraKit = "6c742aac-3347-4629-af66-fc926824e5e4" path = ".." [compat] -ITensorBase = "0.10" +ITensorBase = "0.11" MatrixAlgebraKit = "0.2, 0.3, 0.4, 0.5, 0.6" diff --git a/src/abstractnamedtensor.jl b/src/abstractnamedtensor.jl index 7e01bc9..11678d5 100644 --- a/src/abstractnamedtensor.jl +++ b/src/abstractnamedtensor.jl @@ -482,7 +482,13 @@ julia> dimnames(replacedimnames(a, :i => :k)) See also [`dimnames`](@ref). """ function replacedimnames end +# Strip an `Index`/`NamedUnitRange` down to its dimension name so an index-keyed pair +# (`i => j`) relabels like the name-keyed pair (`name(i) => name(j)`); `dimnames(a)` holds +# names, so a raw-index key would never match and silently no-op. +to_dimname(x) = x +to_dimname(x::NamedUnitRange) = name(x) function replacedimnames(a::AbstractNamedTensor, replacements::Pair...) + replacements = map(p -> to_dimname(first(p)) => to_dimname(last(p)), replacements) new_dimnames = replace(dimnames(a), replacements...) return nameddims(unnamed(a), new_dimnames) end diff --git a/src/namedtensor.jl b/src/namedtensor.jl index c51c3d1..100f59c 100644 --- a/src/namedtensor.jl +++ b/src/namedtensor.jl @@ -26,15 +26,23 @@ struct NamedTensor{DimName} <: AbstractNamedTensor{DimName} unnamed::Any dimnames::Vector{DimName} function NamedTensor{DimName}(unnamed, dimnames) where {DimName} - dimnames = collect(DimName, dimnames) - # Catch the common ITensors.jl-style mistake of passing indices as the names. - any(dimname -> dimname isa NamedUnitRange, dimnames) && throw( + # Catch the common ITensors.jl-style mistake of passing indices (`NamedUnitRange`s + # such as `Index`) as the names. Checked before `collect(DimName, ...)`, which would + # otherwise fail with an opaque `convert` error when `DimName` is the dimension-name + # type (e.g. `ITensor`'s `IndexName`). + ( + dimnames isa NamedUnitRange || + any(dimname -> dimname isa NamedUnitRange, dimnames) + ) && throw( ArgumentError( - "The `NamedTensor` constructor takes dimension names only, not indices \ - (`NamedUnitRange`s), got $(dimnames). To build a `NamedTensor` from an \ - array and indices, index the array instead, as in `array[i, j]`." + "The `NamedTensor`/`ITensor` constructor takes dimension names, not indices \ + (`NamedUnitRange`s such as `Index`), got $(dimnames). To build a tensor from \ + an array and indices, either index the array to inherit the space from the \ + indices, as in `array[i, j]`, or attach only the names and take the space \ + from the array, as in `ITensor(array, name.((i, j)))`." ) ) + dimnames = collect(DimName, dimnames) TensorAlgebra.ndims(unnamed) == length(dimnames) || throw(ArgumentError("Number of named dims must match ndims.")) allunique(dimnames) || diff --git a/src/namedunitrange.jl b/src/namedunitrange.jl index 7e33d5b..b2dfca4 100644 --- a/src/namedunitrange.jl +++ b/src/namedunitrange.jl @@ -1,4 +1,4 @@ -using TensorAlgebra: to_range +using TensorAlgebra: TensorAlgebra, to_range, trivialrange """ NamedUnitRange{Name} @@ -64,6 +64,15 @@ end # This can be customized to output different named unit range types. namedunitrange(r::AbstractUnitRange, name) = NamedUnitRange(r, name) +# Mint a fresh trivial *named* range matching `r`'s backend: the trivial range of the +# underlying (unnamed) axis, carrying a fresh unique name of `r`'s name type. +function TensorAlgebra.trivialrange(r::NamedUnitRange{Name}) where {Name} + return namedunitrange(trivialrange(unnamed(r)), uniquename(Name)) +end +function TensorAlgebra.trivialrange(r::NamedUnitRange{Name}, n::Integer) where {Name} + return namedunitrange(trivialrange(unnamed(r), n), uniquename(Name)) +end + # Shorthand: attach an existing name to a range. named(r::AbstractUnitRange, name) = namedunitrange(r, name) diff --git a/src/tensoralgebra.jl b/src/tensoralgebra.jl index bcc239a..dad4190 100644 --- a/src/tensoralgebra.jl +++ b/src/tensoralgebra.jl @@ -159,7 +159,7 @@ end # SVD (three-output). # -for f in [:svd_compact, :svd_full, :svd_trunc] +for f in [:svd_compact, :svd_full] f_nameddims = Symbol(f, "_nameddims") @eval begin function MAK.$f( @@ -199,6 +199,41 @@ for f in [:svd_compact, :svd_full, :svd_trunc] end end +# `svd_trunc` mirrors the three-output SVD above but also returns the truncation error `ϵ` +# (the 2-norm of the discarded singular values), matching MatrixAlgebraKit's four-output +# `svd_trunc`, so it is spelled out here rather than sharing the loop. +function MAK.svd_trunc( + a::AbstractNamedTensor, dimnames_codomain, dimnames_domain; kwargs... + ) + return svd_trunc_nameddims(a, dimnames_codomain, dimnames_domain; kwargs...) +end +function svd_trunc_nameddims( + a::AbstractNamedTensor, dimnames_codomain, dimnames_domain; kwargs... + ) + codomain = name.(dimnames_codomain) + domain = name.(dimnames_domain) + u_unnamed, s_unnamed, v_unnamed, ϵ = TA.svd_trunc( + unnamed(a), dimnames(a), codomain, domain; kwargs... + ) + name_u = uniquename(dimnametype(a)) + name_v = uniquename(dimnametype(a)) + u = nameddims(u_unnamed, (codomain..., name_u)) + s = nameddims(s_unnamed, (name_u, name_v)) + v = nameddims(v_unnamed, (name_v, domain...)) + return u, s, v, ϵ +end +function MAK.svd_trunc(a::AbstractNamedTensor, dimnames_codomain; kwargs...) + return svd_trunc_nameddims(a, dimnames_codomain; kwargs...) +end +function svd_trunc_nameddims(a::AbstractNamedTensor, dimnames_codomain; kwargs...) + return MAK.svd_trunc( + a, + dimnames_codomain, + dimnames_setdiff(dimnames(a), name.(dimnames_codomain)); + kwargs... + ) +end + # # Singular values. # diff --git a/test/Project.toml b/test/Project.toml index f8ac67a..6c6d059 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -24,14 +24,14 @@ VectorInterface = "409d34a3-91d5-4945-b6ec-7529ddf182d8" WrappedUnions = "325db55a-9c6c-5b90-b1a2-ec87e7a38c44" [sources.ITensorBase] -path = ".." +path = "/Users/mfishman/.julia/dev/ITensorBase/.worktrees/mf/nextgen-followups-round1" [compat] AbstractTrees = "0.4.5" Adapt = "4" Aqua = "0.8.9" Combinatorics = "1" -ITensorBase = "0.10" +ITensorBase = "0.11" ITensorPkgSkeleton = "0.3.42" JLArrays = "0.2, 0.3" LinearAlgebra = "1.10" diff --git a/test/test_basics.jl b/test/test_basics.jl index 748d3ee..d609c85 100644 --- a/test/test_basics.jl +++ b/test/test_basics.jl @@ -1,6 +1,6 @@ -using ITensorBase: ITensorBase, Index, IndexName, NamedTensor, dimnametype, gettag, hastag, - id, inds, mapinds, name, named, plev, prime, setplev, settag, tags, uniquename, unname, - unnamed, unsettag +using ITensorBase: ITensorBase, ITensor, Index, IndexName, NamedTensor, dimnametype, gettag, + hastag, id, inds, mapinds, name, named, plev, prime, setplev, settag, tags, uniquename, + unname, unnamed, unsettag using Test: @test, @test_broken, @test_throws, @testset using UUIDs: UUID @@ -123,8 +123,16 @@ using UUIDs: UUID @test_throws MethodError NamedTensor(randn(elt, 2, 2), :i, :j) # The constructor takes names only, not indices, so passing indices (the - # ITensors.jl idiom) errors. + # ITensors.jl idiom) errors, whether as a tuple, a vector, or a single index. + i, j = Index.((2, 3)) @test_throws ArgumentError NamedTensor(randn(elt, 2, 2), Index.((2, 2))) + @test_throws ArgumentError ITensor(randn(elt, 2, 3), (i, j)) + @test_throws ArgumentError ITensor(randn(elt, 2, 3), [i, j]) + @test_throws ArgumentError ITensor(randn(elt, 2), i) + # The two supported constructions: index the array (inherit the space from the + # indices), or attach only the names (take the space from the array). + @test randn(elt, 2, 3)[i, j] isa ITensor + @test ITensor(randn(elt, 2, 3), name.((i, j))) isa ITensor i, j = Index.((3, 4)) a = randn(elt, i, j) diff --git a/test/test_tensoralgebra.jl b/test/test_tensoralgebra.jl index 5a8f258..bdd20ea 100644 --- a/test/test_tensoralgebra.jl +++ b/test/test_tensoralgebra.jl @@ -2,11 +2,11 @@ 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 + qr_full, right_null, right_orth, right_polar, svd_compact, svd_trunc, svd_vals using StableRNGs: StableRNG using TensorAlgebra.MatrixAlgebra: gram_eigh_full, gram_eigh_full_with_pinv -using TensorAlgebra: - TensorAlgebra, contract, matricize, project, unchecked_project, unmatricize +using TensorAlgebra: TensorAlgebra, contract, matricize, project, trivialrange, + unchecked_project, unmatricize using Test: @test, @test_broken, @testset @testset "TensorAlgebra (eltype=$(elt))" for elt in @@ -106,11 +106,19 @@ using Test: @test, @test_broken, @testset u, s, v = svd_compact(a, (i, k), (j, l)) @test u * s * v ≈ a - # Test truncation. + # Test truncation. `svd_trunc` returns a fourth output `ϵ`, the truncation error + # (2-norm of the discarded singular values), matching MatrixAlgebraKit. a = randn(elt, i, j, k, l) - u, s, v = svd_trunc(a, (i, k), (j, l); trunc = (; maxrank = 2)) + res = svd_trunc(a, (i, k), (j, l); trunc = (; maxrank = 2)) + @test length(res) == 4 + u, s, v, ϵ = res @test u * s * v ≉ a @test size(s) == (2, 2) + @test ϵ isa Real + @test ϵ ≥ 0 + # `ϵ` equals the 2-norm of the discarded singular values. + vals = svd_vals(a, (i, k), (j, l)) + @test ϵ ≈ norm(sort(vals; rev = true)[3:end]) end @testset "left_null/right_null" begin dims = (2, 2, 2, 2) @@ -177,4 +185,29 @@ using Test: @test, @test_broken, @testset @test dimnames(bra) == [name(i)] @test unname(bra, (i,)) == v end + @testset "replacedimnames with index keys" begin + i, j, k = namedoneto.((2, 3, 2), ("i", "j", "k")) + a = randn(elt, i, j) + # An `Index`-keyed pair relabels like the name-keyed pair rather than silently + # no-opping, and the result stays an `ITensor` (not `NamedTensor{Any}`). + @test dimnames(replacedimnames(a, i => k)) == + dimnames(replacedimnames(a, "i" => "k")) + @test replacedimnames(a, i => k) isa typeof(a) + # Mixed index/name keys and values are accepted. + @test dimnames(replacedimnames(a, i => "k")) == + dimnames(replacedimnames(a, "i" => "k")) + @test dimnames(replacedimnames(a, "i" => k)) == + dimnames(replacedimnames(a, "i" => "k")) + end + @testset "trivialrange on named ranges" begin + i = Index(3) + r = trivialrange(i) + @test r isa Index + @test length(r) == 1 + @test name(r) != name(i) + rn = trivialrange(i, 4) + @test rn isa Index + @test length(rn) == 4 + @test name(rn) != name(i) + end end From 145eab1ca14c58d660e8c848bd42431589f68a29 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 8 Jul 2026 14:51:53 -0400 Subject: [PATCH 2/6] Add whole-tensor index ops (mapinds, prime, noprime, sim), name-only replaceinds, rank-0 similar/one Whole-tensor index manipulation on `AbstractNamedTensor`. `mapinds(f, t)` relabels every index name-only (leaving the space untouched), with `prime(t)`, `noprime(t)`, and `sim(t)` built on it. `replaceinds` becomes a name-only relabel, a synonym for `replacedimnames`, and `sim(i)` mints a fresh id preserving tags and prime level. `similar(a, ())` and `one(a)` give a rank-0 scalar tensor on `a`'s backend. --- src/ITensorBase.jl | 4 +-- src/abstractnamedtensor.jl | 53 ++++++++++++++++++++++++++------ src/index.jl | 41 +++++++++++++++++++++++-- test/test_basics.jl | 62 ++++++++++++++++++++++++++++++++++++-- test/test_exports.jl | 4 +-- 5 files changed, 145 insertions(+), 19 deletions(-) diff --git a/src/ITensorBase.jl b/src/ITensorBase.jl index 72cb9b7..4d19eae 100644 --- a/src/ITensorBase.jl +++ b/src/ITensorBase.jl @@ -2,8 +2,8 @@ module ITensorBase export AbstractNamedTensor, NamedTensor, AbstractITensor, ITensor, Index, NamedUnitRange, aligndims, aligneddims, apply, codomainnames, dimnames, - dimnametype, domainnames, inds, named, nameddims, noprime, operator, prime, - similar_operator, state, uniquename + dimnametype, domainnames, inds, mapinds, named, nameddims, noprime, operator, + prime, replaceinds, sim, similar_operator, state, uniquename using Compat: @compat @compat public @names @compat public IndexName, name, nametype, replacedimnames, setname, unnamed, unnamedtype diff --git a/src/abstractnamedtensor.jl b/src/abstractnamedtensor.jl index 11678d5..1a42ac7 100644 --- a/src/abstractnamedtensor.jl +++ b/src/abstractnamedtensor.jl @@ -454,6 +454,24 @@ function Base.similar( ) return similar_nameddims(a, elt, inds) end + +# Rank-0 (empty named axes): a scalar tensor on `a`'s backend, e.g. a backend-matched unit +# for a product accumulator. Spelled out separately because the tuple forms above require at +# least one `NamedUnitRange`. +Base.similar(a::AbstractNamedTensor, inds::Tuple{}) = similar(a, eltype(a), inds) +function Base.similar(a::AbstractNamedTensor, elt::Type, inds::Tuple{}) + return similar_nameddims(a, elt, inds) +end + +""" + one(a::AbstractNamedTensor) -> AbstractNamedTensor + +Return a rank-0 (scalar) tensor holding `one(scalartype(a))` on `a`'s backend. This is the +multiplicative unit matching `a`'s element type and backend (dense, graded, `TensorMap`, …), +useful as the seed of a product accumulator. +""" +Base.one(a::AbstractNamedTensor) = fill!(similar(a, ()), one(scalartype(a))) + function setdimnames(a::AbstractNamedTensor, dimnames) return nameddims(unnamed(a), dimnames) end @@ -498,18 +516,33 @@ function replacedimnames(f, a::AbstractNamedTensor) end mapdimnames(f, a::AbstractNamedTensor) = replacedimnames(f, a) -# Replace over `axes` (a `Tuple`) rather than `inds` (a `Vector`): `replace` on a `Vector` -# is homogeneous and would fail to convert a replacement index backed by a different range -# type (e.g. `UnitRange` into a `OneTo`-backed vector), whereas a `Tuple` admits the mixed -# element types. The result is splatted into `getindex`, so only the order matters. +""" + replaceinds(a::AbstractNamedTensor, replacements::Pair...) + replaceinds(f, a::AbstractNamedTensor) + +Return a tensor with the same data as `a` but with its indices relabeled, a backend-agnostic +synonym for [`replacedimnames`](@ref). This is a name-only relabel: it rewrites the index +names and leaves the underlying space untouched (replacing the space instead would +scalar-index a graded axis). The first form takes `old => new` pairs, relabeling matching +indices and leaving the rest unchanged; the second replaces each index with `f(index)`. + +See also [`mapinds`](@ref), [`replacedimnames`](@ref). +""" function replaceinds(a::AbstractNamedTensor, replacements::Pair...) - new_inds = replace(axes(a), replacements...) - return unnamed(a)[new_inds...] -end -function replaceinds(f, a::AbstractNamedTensor) - new_inds = replace(f, axes(a)) - return unnamed(a)[new_inds...] + return replacedimnames(a, replacements...) end +replaceinds(f, a::AbstractNamedTensor) = replacedimnames(f, a) + +""" + mapinds(f, a::AbstractNamedTensor) + +Return a tensor with the same data as `a` but with each of its indices replaced by +`f(index)`, a name-only relabel that leaves the underlying space untouched. This is the +whole-tensor index-map primitive behind [`prime`](@ref), [`noprime`](@ref), and +[`sim`](@ref). + +See also [`replaceinds`](@ref). +""" mapinds(f, a::AbstractNamedTensor) = replaceinds(f, a) # `Base.isempty(a::AbstractArray)` is defined as `length(a) == 0`, diff --git a/src/index.jl b/src/index.jl index 4f31493..895be4a 100644 --- a/src/index.jl +++ b/src/index.jl @@ -125,11 +125,13 @@ end """ prime(i) + prime(t::AbstractNamedTensor) Increment the prime level of an index or index name by one, returning a new index that is distinct from `i`. Priming is the usual way to make a second copy of an index that carries the same tags but is not contracted against the original. The inverse is -[`noprime`](@ref), which resets the prime level to zero. +[`noprime`](@ref), which resets the prime level to zero. Given a tensor, prime all of its +indices. # Examples @@ -149,9 +151,11 @@ function prime end """ noprime(i) + noprime(t::AbstractNamedTensor) Reset the prime level of an index or index name to zero, returning a new index. This -undoes any number of [`prime`](@ref) calls. +undoes any number of [`prime`](@ref) calls. Given a tensor, reset the prime level of all of +its indices. # Examples @@ -166,8 +170,34 @@ See also [`prime`](@ref), [`Index`](@ref). """ function noprime end +""" + sim(i) + sim(t::AbstractNamedTensor) + +Return a "similar" index: a new index (or, given a tensor, a tensor with all of its indices +replaced) carrying the same tags and prime level as `i` but a fresh unique identifier, so it +is distinct from `i` and will not contract against it. This is the index-manipulation +spelling of [`uniquename`](@ref) on an index. + +# Examples + +```jldoctest +julia> i = Index(2); + +julia> sim(i) == i +false + +julia> length(sim(i)) +2 +``` + +See also [`uniquename`](@ref), [`prime`](@ref). +""" +function sim end + prime(n::IndexName) = setplev(n, plev(n) + 1) noprime(n::IndexName) = setplev(n, 0) +sim(n::IndexName) = uniquename(n) # Show a short prefix of the `UUID` id rather than the full 36-character string, # enough to disambiguate indices at a glance without dominating the output. A @@ -250,6 +280,13 @@ unsettag(i::Index, tagname) = setname(i, unsettag(name(i), tagname)) setplev(i::Index, plev) = setname(i, setplev(name(i), plev)) prime(i::Index) = setname(i, prime(name(i))) noprime(i::Index) = setname(i, noprime(name(i))) +sim(i::Index) = setname(i, sim(name(i))) + +# Whole-tensor index manipulation: relabel every index name-only via `mapinds`, leaving the +# data and spaces untouched. +prime(a::AbstractNamedTensor) = mapinds(prime, a) +noprime(a::AbstractNamedTensor) = mapinds(noprime, a) +sim(a::AbstractNamedTensor) = mapinds(sim, a) function primestring(plev) if plev < 0 diff --git a/test/test_basics.jl b/test/test_basics.jl index d609c85..ca6c45c 100644 --- a/test/test_basics.jl +++ b/test/test_basics.jl @@ -1,6 +1,6 @@ -using ITensorBase: ITensorBase, ITensor, Index, IndexName, NamedTensor, dimnametype, gettag, - hastag, id, inds, mapinds, name, named, plev, prime, setplev, settag, tags, uniquename, - unname, unnamed, unsettag +using ITensorBase: ITensorBase, AbstractNamedTensor, ITensor, Index, IndexName, NamedTensor, + dimnametype, gettag, hastag, id, inds, mapinds, name, named, noprime, plev, prime, + replaceinds, setplev, settag, sim, tags, uniquename, unname, unnamed, unsettag using Test: @test, @test_broken, @test_throws, @testset using UUIDs: UUID @@ -176,4 +176,60 @@ using UUIDs: UUID @test sprint(show, "text/plain", i) == "Index(length=2|id=$(first(string(id(i)), 8))|X=>Y)" end + @testset "whole-tensor index manipulation" begin + elt = Float64 + i, j = Index.((2, 3)) + a = randn(elt, i, j) + + # `prime`/`noprime` relabel every index name-only, leaving the data untouched. + a′ = prime(a) + @test unnamed(a′) == unnamed(a) + @test issetequal(inds(a′), (prime(i), prime(j))) + @test noprime(a′) == a + @test issetequal(inds(noprime(prime(a′))), (i, j)) + + # `replaceinds` is a name-only synonym for the pair-based relabel. + k, l = Index.((2, 3)) + a_r = replaceinds(a, i => k, j => l) + @test unnamed(a_r) == unnamed(a) + @test issetequal(inds(a_r), (k, l)) + + # `sim` mints fresh ids, so no index of `sim(a)` matches an index of `a`, while the + # data, lengths, tags, and prime levels are preserved. + a_s = sim(a) + @test unnamed(a_s) == unnamed(a) + @test !any(in(inds(a)), inds(a_s)) + @test issetequal(length.(inds(a_s)), length.(inds(a))) + + i2 = settag(prime(Index(2)), "X", "Y") + @test sim(i2) != i2 + @test plev(sim(i2)) == 1 + @test gettag(sim(i2), "X") == "Y" + @test length(sim(i2)) == 2 + end + @testset "rank-0 similar and one" begin + elt = Float64 + i, j = Index.((2, 3)) + a = randn(elt, i, j) + + # `similar(a, ())` mints a scalar (0-dim) tensor on `a`'s backend and element type. + s = similar(a, ()) + @test s isa AbstractNamedTensor + @test eltype(s) === elt + @test isempty(inds(s)) + @test ndims(unnamed(s)) == 0 + fill!(s, 1) + @test unnamed(s)[] == 1 + + s32 = similar(a, Float32, ()) + @test eltype(s32) === Float32 + @test isempty(inds(s32)) + + # `one(a)` is a rank-0 tensor holding `one(eltype(a))`. + o = one(a) + @test o isa AbstractNamedTensor + @test eltype(o) === elt + @test isempty(inds(o)) + @test unnamed(o)[] == one(elt) + end end diff --git a/test/test_exports.jl b/test/test_exports.jl index 9a890c3..0ab9602 100644 --- a/test/test_exports.jl +++ b/test/test_exports.jl @@ -6,8 +6,8 @@ using Test: @test, @testset :Index, :NamedUnitRange, :aligndims, :aligneddims, :apply, :codomainnames, :dimnames, :dimnametype, :domainnames, - :inds, :named, :nameddims, :noprime, :operator, :prime, :similar_operator, - :state, :uniquename, + :inds, :mapinds, :named, :nameddims, :noprime, :operator, :prime, :replaceinds, + :sim, :similar_operator, :state, :uniquename, ] publics = [ :IndexName, :name, :nametype, :replacedimnames, :setname, :unnamed, From bfe7395f485fdaee832bd5c9de186bdb25da4a04 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 8 Jul 2026 15:04:30 -0400 Subject: [PATCH 3/6] Add name-based index-set algebra for AbstractNamedTensor Adds `commoninds`, `uniqueinds`, `unioninds`, `noncommoninds`, and `hascommoninds` on `AbstractNamedTensor`, along with the singular `commonind`/`uniqueind` and their non-erroring `trycommonind`/`trynoncommonind` counterparts. Indices are compared by name, so a shared bond matches even when the two tensors carry it as an index on one and its dual on the other. --- src/ITensorBase.jl | 7 ++- src/abstractnamedtensor.jl | 126 +++++++++++++++++++++++++++++++++++++ test/test_basics.jl | 34 ++++++++++ test/test_exports.jl | 10 +-- 4 files changed, 170 insertions(+), 7 deletions(-) diff --git a/src/ITensorBase.jl b/src/ITensorBase.jl index 4d19eae..94479b1 100644 --- a/src/ITensorBase.jl +++ b/src/ITensorBase.jl @@ -1,9 +1,10 @@ module ITensorBase export AbstractNamedTensor, NamedTensor, AbstractITensor, ITensor, Index, - NamedUnitRange, aligndims, aligneddims, apply, codomainnames, dimnames, - dimnametype, domainnames, inds, mapinds, named, nameddims, noprime, operator, - prime, replaceinds, sim, similar_operator, state, uniquename + NamedUnitRange, aligndims, aligneddims, apply, codomainnames, commonind, commoninds, + dimnames, dimnametype, domainnames, hascommoninds, inds, mapinds, named, nameddims, + noncommoninds, noprime, operator, prime, replaceinds, sim, similar_operator, state, + trycommonind, trynoncommonind, uniqueind, uniqueinds, unioninds, uniquename using Compat: @compat @compat public @names @compat public IndexName, name, nametype, replacedimnames, setname, unnamed, unnamedtype diff --git a/src/abstractnamedtensor.jl b/src/abstractnamedtensor.jl index 1a42ac7..02fad20 100644 --- a/src/abstractnamedtensor.jl +++ b/src/abstractnamedtensor.jl @@ -545,6 +545,132 @@ See also [`replaceinds`](@ref). """ mapinds(f, a::AbstractNamedTensor) = replaceinds(f, a) +# Name-based index-set algebra (the `commoninds`/etc. surface). Layered in three: +# small order-preserving set ops on arbitrary collections, the same ops keyed by index +# name, and the tensor-family functions built on top. + +# Small-collection set operations keyed by a transform `by` (elements compare equal when +# `by(x) == by(y)`). These scan linearly rather than building `Set`s: Base's `Set`-based ops +# hash, and hashing a whole `Index` can be expensive or fall back to iterating a graded axis. +# The intersect/setdiff/union/symdiff forms return elements of the first argument as `Vector`s. +function smallintersect(a, b; by = identity) + return (kb = Iterators.map(by, b); [x for x in a if by(x) ∈ kb]) +end +function smallsetdiff(a, b; by = identity) + return (kb = Iterators.map(by, b); [x for x in a if by(x) ∉ kb]) +end +smallunion(a, b; by = identity) = vcat(collect(a), smallsetdiff(b, a; by)) +smallsymdiff(a, b; by = identity) = vcat(smallsetdiff(a, b; by), smallsetdiff(b, a; by)) +smallisdisjoint(a, b; by = identity) = (kb = Iterators.map(by, b); !any(x -> by(x) ∈ kb, a)) +smallissubset(a, b; by = identity) = (kb = Iterators.map(by, b); all(x -> by(x) ∈ kb, a)) +smallissetequal(a, b; by = identity) = smallissubset(a, b; by) && smallissubset(b, a; by) + +# The small ops keyed by index name (`by = name`) rather than full `Index` equality. On a +# graded axis a shared bond appears as an index on one tensor and its dual (`conj`) on the +# other (same name, opposite arrow), so full-`Index` `==` misses it while the names match. +# On the dense backend the two coincide. +nameintersect(a, b) = smallintersect(a, b; by = name) +namesetdiff(a, b) = smallsetdiff(a, b; by = name) +nameunion(a, b) = smallunion(a, b; by = name) +namesymdiff(a, b) = smallsymdiff(a, b; by = name) +nameisdisjoint(a, b) = smallisdisjoint(a, b; by = name) +nameissubset(a, b) = smallissubset(a, b; by = name) +nameissetequal(a, b) = smallissetequal(a, b; by = name) + +""" + commoninds(a::AbstractNamedTensor, b::AbstractNamedTensor) + +The indices shared by name between `a` and `b`, as a `Vector` in the order they appear in `a`. + +See also [`commonind`](@ref), [`uniqueinds`](@ref), [`hascommoninds`](@ref). +""" +commoninds(a::AbstractNamedTensor, b::AbstractNamedTensor) = nameintersect(inds(a), inds(b)) + +""" + uniqueinds(a::AbstractNamedTensor, b::AbstractNamedTensor) + +The indices of `a` that do not appear by name in `b`, as a `Vector` in the order they appear +in `a`. + +See also [`uniqueind`](@ref), [`commoninds`](@ref), [`noncommoninds`](@ref). +""" +uniqueinds(a::AbstractNamedTensor, b::AbstractNamedTensor) = namesetdiff(inds(a), inds(b)) + +""" + unioninds(a::AbstractNamedTensor, b::AbstractNamedTensor) + +The union by name of the indices of `a` and `b`, as a `Vector`: the indices of `a` followed +by the indices of `b` not already present in `a`. + +See also [`commoninds`](@ref), [`noncommoninds`](@ref). +""" +unioninds(a::AbstractNamedTensor, b::AbstractNamedTensor) = nameunion(inds(a), inds(b)) + +""" + noncommoninds(a::AbstractNamedTensor, b::AbstractNamedTensor) + +The indices not shared by name between `a` and `b` (the symmetric difference), as a `Vector`: +the indices unique to `a` followed by those unique to `b`. + +See also [`uniqueinds`](@ref), [`commoninds`](@ref). +""" +function noncommoninds(a::AbstractNamedTensor, b::AbstractNamedTensor) + return namesymdiff(inds(a), inds(b)) +end + +""" + hascommoninds(a::AbstractNamedTensor, b::AbstractNamedTensor) + +Whether `a` and `b` share any index by name. + +See also [`commoninds`](@ref). +""" +function hascommoninds(a::AbstractNamedTensor, b::AbstractNamedTensor) + return !nameisdisjoint(inds(a), inds(b)) +end + +""" + commonind(a::AbstractNamedTensor, b::AbstractNamedTensor) + +The single index shared by name between `a` and `b`. Errors unless there is exactly one +shared index. Use [`trycommonind`](@ref) to get `nothing` instead of an error. + +See also [`commoninds`](@ref), [`uniqueind`](@ref). +""" +commonind(a::AbstractNamedTensor, b::AbstractNamedTensor) = only(commoninds(a, b)) + +""" + uniqueind(a::AbstractNamedTensor, b::AbstractNamedTensor) + +The single index of `a` that does not appear by name in `b`. Errors unless there is exactly +one such index. Use [`trynoncommonind`](@ref) to get `nothing` instead of an error. + +See also [`uniqueinds`](@ref), [`commonind`](@ref). +""" +uniqueind(a::AbstractNamedTensor, b::AbstractNamedTensor) = only(uniqueinds(a, b)) + +""" + trycommonind(a::AbstractNamedTensor, b::AbstractNamedTensor) + +The single index shared by name between `a` and `b`, or `nothing` if they share no index or +more than one. The non-erroring counterpart of [`commonind`](@ref). +""" +function trycommonind(a::AbstractNamedTensor, b::AbstractNamedTensor) + cs = commoninds(a, b) + return length(cs) == 1 ? only(cs) : nothing +end + +""" + trynoncommonind(a::AbstractNamedTensor, b::AbstractNamedTensor) + +The single index of `a` that does not appear by name in `b`, or `nothing` if there is no such +index or more than one. The non-erroring counterpart of [`uniqueind`](@ref). +""" +function trynoncommonind(a::AbstractNamedTensor, b::AbstractNamedTensor) + us = uniqueinds(a, b) + return length(us) == 1 ? only(us) : nothing +end + # `Base.isempty(a::AbstractArray)` is defined as `length(a) == 0`, # which involves comparing a named integer to an unnamed integer # which isn't well defined. diff --git a/test/test_basics.jl b/test/test_basics.jl index ca6c45c..6267324 100644 --- a/test/test_basics.jl +++ b/test/test_basics.jl @@ -232,4 +232,38 @@ using UUIDs: UUID @test isempty(inds(o)) @test unnamed(o)[] == one(elt) end + @testset "name-based index-set algebra" begin + elt = Float64 + i, j, k = Index.((2, 3, 4)) + a = randn(elt, i, j) + b = randn(elt, j, k) + + namesof(is) = name.(is) + + @test namesof(commoninds(a, b)) == namesof([j]) + @test namesof(uniqueinds(a, b)) == namesof([i]) + @test namesof(unioninds(a, b)) == namesof([i, j, k]) + @test namesof(noncommoninds(a, b)) == namesof([i, k]) + @test hascommoninds(a, b) + + @test name(commonind(a, b)) == name(j) + @test name(uniqueind(a, b)) == name(i) + @test name(trycommonind(a, b)) == name(j) + @test name(trynoncommonind(a, b)) == name(i) + + # No shared index. + c = randn(elt, k) + @test isempty(commoninds(a, c)) + @test !hascommoninds(a, c) + @test isnothing(trycommonind(a, c)) + + # `commonind`/`uniqueind` error unless there is exactly one; the `try*` forms + # return `nothing` instead. + d = randn(elt, i, j) + @test_throws ArgumentError commonind(a, d) + @test isnothing(trycommonind(a, d)) + @test_throws ArgumentError commonind(a, c) + @test_throws ArgumentError uniqueind(a, c) + @test isnothing(trynoncommonind(a, c)) + end end diff --git a/test/test_exports.jl b/test/test_exports.jl index 0ab9602..6375438 100644 --- a/test/test_exports.jl +++ b/test/test_exports.jl @@ -4,10 +4,12 @@ using Test: @test, @testset exports = [ :ITensorBase, :AbstractNamedTensor, :NamedTensor, :AbstractITensor, :ITensor, :Index, :NamedUnitRange, - :aligndims, :aligneddims, :apply, :codomainnames, :dimnames, :dimnametype, - :domainnames, - :inds, :mapinds, :named, :nameddims, :noprime, :operator, :prime, :replaceinds, - :sim, :similar_operator, :state, :uniquename, + :aligndims, :aligneddims, :apply, :codomainnames, :commonind, :commoninds, + :dimnames, :dimnametype, :domainnames, :hascommoninds, + :inds, :mapinds, :named, :nameddims, :noncommoninds, :noprime, :operator, + :prime, + :replaceinds, :sim, :similar_operator, :state, :trycommonind, :trynoncommonind, + :uniqueind, :uniqueinds, :unioninds, :uniquename, ] publics = [ :IndexName, :name, :nametype, :replacedimnames, :setname, :unnamed, From e940ca52c1dc55aa5066f6ef5a85e013978ad59a Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 8 Jul 2026 15:19:53 -0400 Subject: [PATCH 4/6] Apply replaceinds/mapinds function form at the index level The function form of `replaceinds`/`mapinds` forwarded straight to `replacedimnames`, so its `f` received a dimension name rather than the index its docstring describes. Apply `f` to each index instead, relabeling name-only through the pair form, so `replaceinds`/`mapinds` are the index-level counterpart of the name-level `replacedimnames`/`mapdimnames`. --- src/abstractnamedtensor.jl | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/abstractnamedtensor.jl b/src/abstractnamedtensor.jl index 02fad20..2c07fda 100644 --- a/src/abstractnamedtensor.jl +++ b/src/abstractnamedtensor.jl @@ -520,26 +520,26 @@ mapdimnames(f, a::AbstractNamedTensor) = replacedimnames(f, a) replaceinds(a::AbstractNamedTensor, replacements::Pair...) replaceinds(f, a::AbstractNamedTensor) -Return a tensor with the same data as `a` but with its indices relabeled, a backend-agnostic -synonym for [`replacedimnames`](@ref). This is a name-only relabel: it rewrites the index -names and leaves the underlying space untouched (replacing the space instead would -scalar-index a graded axis). The first form takes `old => new` pairs, relabeling matching -indices and leaving the rest unchanged; the second replaces each index with `f(index)`. +Return a tensor with the same data as `a` but with its indices relabeled. Unlike +[`replacedimnames`](@ref), whose function `f` receives a dimension name, `replaceinds` works +at the index level: the pair form takes `old => new` index pairs, and the function form +relabels each index `i` using `f(i)`. Either way this is a name-only relabel, taking just the +name of the replacement and leaving the underlying space untouched (replacing the space +instead would scalar-index a graded axis). Pairs whose index is absent are ignored. See also [`mapinds`](@ref), [`replacedimnames`](@ref). """ function replaceinds(a::AbstractNamedTensor, replacements::Pair...) return replacedimnames(a, replacements...) end -replaceinds(f, a::AbstractNamedTensor) = replacedimnames(f, a) +replaceinds(f, a::AbstractNamedTensor) = replaceinds(a, map(i -> i => f(i), inds(a))...) """ mapinds(f, a::AbstractNamedTensor) -Return a tensor with the same data as `a` but with each of its indices replaced by -`f(index)`, a name-only relabel that leaves the underlying space untouched. This is the -whole-tensor index-map primitive behind [`prime`](@ref), [`noprime`](@ref), and -[`sim`](@ref). +Return a tensor with the same data as `a` but with each index `i` relabeled using `f(i)`, a +name-only relabel that leaves the underlying space untouched. This is the whole-tensor +index-map primitive behind [`prime`](@ref), [`noprime`](@ref), and [`sim`](@ref). See also [`replaceinds`](@ref). """ From 7055ae212ea3124f6f10e2b6b494afc3d842d190 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 8 Jul 2026 15:20:11 -0400 Subject: [PATCH 5/6] Rename the IndexName identifier accessor to uuid and add the id identity constructor Rename the unique-identifier accessor `id` on `IndexName`/`Index` to `uuid` (along with the struct field, the constructor keyword, and the `setuuid` setter), freeing the `id` name for the identity-tensor constructor. Add `id(elt, codomain, domain)`, the from-scratch identity operator over the given codomain and domain indices, a sibling of `one(a, codomain, domain)` that needs only the indices and an element type rather than a prototype tensor. `id` is exported. Breaking: `ITensorBase.id` on an index or index name is now `ITensorBase.uuid`. --- src/ITensorBase.jl | 2 +- src/index.jl | 29 +++++++++++++++-------------- src/tensoralgebra.jl | 33 +++++++++++++++++++++++++++++++++ test/test_basics.jl | 22 +++++++++++----------- test/test_exports.jl | 2 +- test/test_operator.jl | 15 ++++++++++++++- 6 files changed, 75 insertions(+), 28 deletions(-) diff --git a/src/ITensorBase.jl b/src/ITensorBase.jl index 94479b1..1070bf2 100644 --- a/src/ITensorBase.jl +++ b/src/ITensorBase.jl @@ -2,7 +2,7 @@ module ITensorBase export AbstractNamedTensor, NamedTensor, AbstractITensor, ITensor, Index, NamedUnitRange, aligndims, aligneddims, apply, codomainnames, commonind, commoninds, - dimnames, dimnametype, domainnames, hascommoninds, inds, mapinds, named, nameddims, + dimnames, dimnametype, domainnames, hascommoninds, id, inds, mapinds, named, nameddims, noncommoninds, noprime, operator, prime, replaceinds, sim, similar_operator, state, trycommonind, trynoncommonind, uniqueind, uniqueinds, unioninds, uniquename using Compat: @compat diff --git a/src/index.jl b/src/index.jl index 895be4a..1273aa7 100644 --- a/src/index.jl +++ b/src/index.jl @@ -23,15 +23,15 @@ is the dimension-name type behind the legacy ITensor surface, where `Index` is `NamedUnitRange{IndexName}` and [`ITensor`](@ref) is `NamedTensor{IndexName}`. """ struct IndexName <: AbstractName - id::UUID + uuid::UUID tags::SortedDict{Symbol, Symbol} plev::Int end function IndexName( - rng::AbstractRNG = RandomDevice(); id::UUID = uuid4(rng), + rng::AbstractRNG = RandomDevice(); uuid::UUID = uuid4(rng), tags = (), plev::Int = 0 ) - return IndexName(id, to_tags(tags), plev) + return IndexName(uuid, to_tags(tags), plev) end # `uniquename` on an existing `IndexName` keeps its tags and prime level, minting only a # fresh id (the legacy `sim`). The type form drops them: a factorization bond or a fresh @@ -56,7 +56,7 @@ to_symbol_pair(p::Pair) = Symbol(first(p)) => Symbol(last(p)) to_tags(ps::Pair...) = to_tags(ps) to_tags(tags) = SortedDict{Symbol, Symbol}(to_symbol_pair(p) for p in tags) -id(n::IndexName) = getfield(n, :id) +uuid(n::IndexName) = getfield(n, :uuid) # Internal: the stored tags as `Symbol => Symbol`, used by the hot comparison, # hashing, and display paths. `tags` is the public string-valued view of this. @@ -80,27 +80,28 @@ end plev(n::IndexName) = getfield(n, :plev) function Base.:(==)(n1::IndexName, n2::IndexName) - return id(n1) == id(n2) && plev(n1) == plev(n2) && tags_stored(n1) == tags_stored(n2) + return uuid(n1) == uuid(n2) && plev(n1) == plev(n2) && + tags_stored(n1) == tags_stored(n2) end function Base.isequal(n1::IndexName, n2::IndexName) - return isequal(id(n1), id(n2)) && + return isequal(uuid(n1), uuid(n2)) && isequal(plev(n1), plev(n2)) && isequal(tags_stored(n1), tags_stored(n2)) end function Base.isless(n1::IndexName, n2::IndexName) - t1 = (id(n1), plev(n1), keys(tags_stored(n1)), values(tags_stored(n1))) - t2 = (id(n2), plev(n2), keys(tags_stored(n2)), values(tags_stored(n2))) + t1 = (uuid(n1), plev(n1), keys(tags_stored(n1)), values(tags_stored(n1))) + t2 = (uuid(n2), plev(n2), keys(tags_stored(n2)), values(tags_stored(n2))) return isless(t1, t2) end function Base.hash(n::IndexName, h::UInt) h = hash(:IndexName, h) - h = hash(id(n), h) + h = hash(uuid(n), h) h = hash(plev(n), h) h = hash(tags_stored(n), h) return h end -setid(n::IndexName, id) = @set n.id = id +setuuid(n::IndexName, uuid) = @set n.uuid = uuid settags(n::IndexName, tags) = @set n.tags = tags setplev(n::IndexName, plev) = @set n.plev = plev @@ -203,10 +204,10 @@ sim(n::IndexName) = uniquename(n) # enough to disambiguate indices at a glance without dominating the output. A # leading prefix (here the first hyphen-delimited group) is the usual short-id # convention, as in git short hashes and Docker short ids. -shortid(id::UUID) = first(string(id), 8) +shortid(uuid::UUID) = first(string(uuid), 8) function Base.show(io::IO, i::IndexName) - idstr = "id=$(shortid(id(i)))" + idstr = "id=$(shortid(uuid(i)))" tagsstr = !isempty(tags_stored(i)) ? "|$(tagsstring(tags_stored(i)))" : "" primestr = primestring(plev(i)) str = "IndexName($(idstr)$(tagsstr))$(primestr)" @@ -263,7 +264,7 @@ const ITensor = NamedTensor{IndexName} const ITensorOperator = NamedTensorOperator{IndexName} # TODO: Define for `NamedViewIndex`. -id(i::Index) = id(name(i)) +uuid(i::Index) = uuid(name(i)) tags_stored(i::Index) = tags_stored(name(i)) tags(i::Index) = tags(name(i)) plev(i::Index) = plev(name(i)) @@ -303,7 +304,7 @@ end function Base.show(io::IO, i::Index) lenstr = "length=$(length(i))" - idstr = "|id=$(shortid(id(i)))" + idstr = "|id=$(shortid(uuid(i)))" tagsstr = !isempty(tags_stored(i)) ? "|$(tagsstring(tags_stored(i)))" : "" primestr = primestring(plev(i)) str = "Index($(lenstr)$(idstr)$(tagsstr))$(primestr)" diff --git a/src/tensoralgebra.jl b/src/tensoralgebra.jl index dad4190..84f5259 100644 --- a/src/tensoralgebra.jl +++ b/src/tensoralgebra.jl @@ -599,6 +599,39 @@ function one_nameddims( return nameddims(raw, (codomain..., domain...)) end +""" + id(elt::Type, codomain, domain) -> Id + +Construct a from-scratch identity-operator-shaped named tensor over the `codomain` and +`domain` indices, with element type `elt`. The fused codomain and domain sizes must match. +Unlike [`one`](@ref), which follows a prototype tensor, `id` needs only the indices and an +element type, so it is the right primitive when no prototype is in hand. The index axes select +the backend: dense ranges give a dense tensor, graded ranges a block-sparse one. + +# Examples + +```jldoctest +julia> using ITensorBase: apply, id, namedoneto, operator + +julia> i, j, k, l = namedoneto.((2, 3, 2, 3), ("i", "j", "k", "l")); + +julia> Id = operator(id(Float64, (i, j), (k, l)), ("i", "j"), ("k", "l")); + +julia> v = randn(k, l); + +julia> apply(Id, v) ≈ v +true +``` + +See also [`one`](@ref). +""" +function id(elt::Type, codomain, domain) + codomain, domain = Tuple(codomain), Tuple(domain) + m = Matrix{elt}(LA.I, prod(length, codomain), prod(length, domain)) + axissizes = (length.(codomain)..., length.(domain)...) + return TA.project(reshape(m, axissizes), codomain, domain) +end + const MATRIX_FUNCTIONS = [ :exp, :cis, :log, :sqrt, :cbrt, :cos, :sin, :tan, :csc, :sec, :cot, :cosh, :sinh, :tanh, diff --git a/test/test_basics.jl b/test/test_basics.jl index 6267324..4e5a9a4 100644 --- a/test/test_basics.jl +++ b/test/test_basics.jl @@ -1,27 +1,27 @@ using ITensorBase: ITensorBase, AbstractNamedTensor, ITensor, Index, IndexName, NamedTensor, dimnametype, gettag, hastag, id, inds, mapinds, name, named, noprime, plev, prime, - replaceinds, setplev, settag, sim, tags, uniquename, unname, unnamed, unsettag + replaceinds, setplev, settag, sim, tags, uniquename, unname, unnamed, unsettag, uuid using Test: @test, @test_broken, @test_throws, @testset using UUIDs: UUID @testset "ITensorBase" begin @testset "IndexName" begin - n1 = IndexName(; id = UUID(0)) - n2 = IndexName(; id = UUID(0)) + n1 = IndexName(; uuid = UUID(0)) + n2 = IndexName(; uuid = UUID(0)) @test n1 == n2 @test isequal(n1, n2) @test hash(n1) ≡ hash(n2) - n1 = IndexName(; id = UUID(0)) - n2 = IndexName(; id = UUID(1)) + n1 = IndexName(; uuid = UUID(0)) + n2 = IndexName(; uuid = UUID(1)) @test n1 ≠ n2 @test !isequal(n1, n2) @test n1 < n2 @test isless(n1, n2) @test hash(n1) ≠ hash(n2) - n1 = IndexName(; id = UUID(0), plev = 0) - n2 = IndexName(; id = UUID(0), plev = 1) + n1 = IndexName(; uuid = UUID(0), plev = 0) + n2 = IndexName(; uuid = UUID(0), plev = 1) @test n1 ≠ n2 @test !isequal(n1, n2) @test n1 < n2 @@ -49,10 +49,10 @@ using UUIDs: UUID end @testset "uniquename" begin i = settag(prime(Index(2)), "X", "Y") - # On an instance, only the id is fresh: tags and prime level are kept. + # On an instance, only the uuid is fresh: tags and prime level are kept. n = uniquename(name(i)) @test n != name(i) - @test id(n) != id(name(i)) + @test uuid(n) != uuid(name(i)) @test plev(n) == plev(i) @test tags(n) == tags(i) # On the name type, a bare name: no tags, prime level zero. @@ -170,11 +170,11 @@ using UUIDs: UUID @testset "show" begin i = Index(2) @test sprint(show, "text/plain", i) == - "Index(length=2|id=$(first(string(id(i)), 8)))" + "Index(length=2|id=$(first(string(uuid(i)), 8)))" i = settag(Index(2), "X", "Y") @test sprint(show, "text/plain", i) == - "Index(length=2|id=$(first(string(id(i)), 8))|X=>Y)" + "Index(length=2|id=$(first(string(uuid(i)), 8))|X=>Y)" end @testset "whole-tensor index manipulation" begin elt = Float64 diff --git a/test/test_exports.jl b/test/test_exports.jl index 6375438..b2db891 100644 --- a/test/test_exports.jl +++ b/test/test_exports.jl @@ -5,7 +5,7 @@ using Test: @test, @testset :ITensorBase, :AbstractNamedTensor, :NamedTensor, :AbstractITensor, :ITensor, :Index, :NamedUnitRange, :aligndims, :aligneddims, :apply, :codomainnames, :commonind, :commoninds, - :dimnames, :dimnametype, :domainnames, :hascommoninds, + :dimnames, :dimnametype, :domainnames, :hascommoninds, :id, :inds, :mapinds, :named, :nameddims, :noncommoninds, :noprime, :operator, :prime, :replaceinds, :sim, :similar_operator, :state, :trycommonind, :trynoncommonind, diff --git a/test/test_operator.jl b/test/test_operator.jl index 5341db9..cfb08a7 100644 --- a/test/test_operator.jl +++ b/test/test_operator.jl @@ -1,5 +1,5 @@ using ITensorBase: ITensorBase as NDA, NamedTensor, NamedTensorOperator, apply, - codomainnames, dimnames, domainnames, nameddims, namedoneto, operator, product, + codomainnames, dimnames, domainnames, id, nameddims, namedoneto, operator, product, replacedimnames, similar_operator, state, unname, unnamed using LinearAlgebra: I, norm using Random: Random @@ -84,6 +84,19 @@ end @test unname(Id_mat, ("row", "col")) ≈ I(8) end +@testset "id(elt, codomain, domain)" begin + # From-scratch identity map (no prototype): matricized form is the identity matrix. + i, j, k, l = namedoneto.((2, 3, 2, 3), ("i", "j", "k", "l")) + Id = id(Float64, (i, j), (k, l)) + @test eltype(Id) === Float64 + @test issetequal(dimnames(Id), ("i", "j", "k", "l")) + Id_mat = matricize(Id, (i, j) => "row", (k, l) => "col") + @test unname(Id_mat, ("row", "col")) ≈ I(6) + + # The requested element type is honored. + @test eltype(id(ComplexF64, (i, j), (k, l))) === ComplexF64 +end + @testset "similar_operator" begin # Five-arg canonical: explicit element type, axes, codomain, domain names. op = similar_operator(randn(3, 3), Float32, (Base.OneTo(3),), ("i'",), ("i",)) From 02e184ac5f58505291007f3be944b3acf8c9b337 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 8 Jul 2026 17:37:50 -0400 Subject: [PATCH 6/6] Use name in place of the to_dimname helper `name` already strips an index to its dimension name and passes a bare name through unchanged, so `to_dimname` duplicated it. Call `name` directly when normalizing relabel pairs, which handles either an index or a bare name on either side of the pair. --- src/abstractnamedtensor.jl | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/abstractnamedtensor.jl b/src/abstractnamedtensor.jl index 2c07fda..a2a3ba3 100644 --- a/src/abstractnamedtensor.jl +++ b/src/abstractnamedtensor.jl @@ -500,13 +500,12 @@ julia> dimnames(replacedimnames(a, :i => :k)) See also [`dimnames`](@ref). """ function replacedimnames end -# Strip an `Index`/`NamedUnitRange` down to its dimension name so an index-keyed pair -# (`i => j`) relabels like the name-keyed pair (`name(i) => name(j)`); `dimnames(a)` holds -# names, so a raw-index key would never match and silently no-op. -to_dimname(x) = x -to_dimname(x::NamedUnitRange) = name(x) +# `name` strips an `Index`/`NamedUnitRange` to its dimension name and passes a bare name +# through unchanged, so an index-keyed pair (`i => j`) relabels like the name-keyed pair +# (`name(i) => name(j)`). `dimnames(a)` holds names, so a raw-index key would never match and +# silently no-op. function replacedimnames(a::AbstractNamedTensor, replacements::Pair...) - replacements = map(p -> to_dimname(first(p)) => to_dimname(last(p)), replacements) + replacements = map(p -> name(first(p)) => name(last(p)), replacements) new_dimnames = replace(dimnames(a), replacements...) return nameddims(unnamed(a), new_dimnames) end