From 237d71e7b567e8977d79ac4e14fc22bc55b386c1 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Mon, 13 Jul 2026 13:11:54 -0400 Subject: [PATCH 1/2] Add space for named ranges and accept indices in NamedTensor constructors Adds `space(i::NamedUnitRange)`, which returns the underlying range of a named range with the name dropped (equal to `unnamed` for a `NamedUnitRange`). The `NamedTensor`/`ITensor` constructor now accepts indices (`NamedUnitRange`s such as `Index`) as the dimension names, not only plain names. It uses each index's name and takes the space from the array, so `ITensor(array, (i, j))` works alongside the existing `array[i, j]` and `ITensor(array, name.((i, j)))` forms. A single bare index is still rejected, since it is ambiguous as the names collection. --- Project.toml | 2 +- src/ITensorBase.jl | 9 ++++++++- src/namedtensor.jl | 43 ++++++++++++++++++++++++------------------- src/namedunitrange.jl | 8 ++++++++ test/test_basics.jl | 16 ++++++++++------ test/test_exports.jl | 2 +- 6 files changed, 52 insertions(+), 28 deletions(-) diff --git a/Project.toml b/Project.toml index 3f5a5eb..03d2e07 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ITensorBase" uuid = "4795dd04-0d67-49bb-8f44-b89c448a1dc7" -version = "0.11.4" +version = "0.11.5" authors = ["ITensor developers and contributors"] [workspace] diff --git a/src/ITensorBase.jl b/src/ITensorBase.jl index f0325e2..07770c9 100644 --- a/src/ITensorBase.jl +++ b/src/ITensorBase.jl @@ -7,7 +7,14 @@ export AbstractNamedTensor, NamedTensor, AbstractITensor, ITensor, Index, trycommonind, trynoncommonind, uniqueind, uniqueinds, unioninds, uniquename using Compat: @compat @compat public @names -@compat public IndexName, name, nametype, replacedimnames, setname, unnamed, unnamedtype +@compat public IndexName, + name, + nametype, + replacedimnames, + setname, + space, + unnamed, + unnamedtype @compat public decoration, emptytags, gettag, diff --git a/src/namedtensor.jl b/src/namedtensor.jl index 100f59c..fbdaf61 100644 --- a/src/namedtensor.jl +++ b/src/namedtensor.jl @@ -25,24 +25,10 @@ struct NamedTensor{DimName} <: AbstractNamedTensor{DimName} # `axes`/algebra interface) can be the parent directly. See the TensorKit extension. unnamed::Any dimnames::Vector{DimName} - function NamedTensor{DimName}(unnamed, dimnames) where {DimName} - # 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`/`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) + # The sole inner constructor: enforces the representation invariants (one name per dimension, + # names distinct) on already-collected names. The outer constructors below normalize the + # inputs (strip index names, fix the eltype) and funnel through here. + global function _NamedTensor(unnamed, dimnames::Vector{DimName}) where {DimName} TensorAlgebra.ndims(unnamed) == length(dimnames) || throw(ArgumentError("Number of named dims must match ndims.")) allunique(dimnames) || @@ -51,7 +37,26 @@ struct NamedTensor{DimName} <: AbstractNamedTensor{DimName} end end -NamedTensor(unnamed, dims) = NamedTensor{eltype(dims)}(unnamed, dims) +function NamedTensor{DimName}(unnamed, dimnames) where {DimName} + # A bare index (`NamedUnitRange` such as `Index`) is ambiguous as `dimnames` (it is itself + # an iterable of its range values), so require a tuple/vector of names or indices. + dimnames isa NamedUnitRange && throw( + ArgumentError( + "Got a single index (`NamedUnitRange` such as `Index`) as the dimension names; \ + pass a tuple or vector, e.g. `ITensor(array, (i, j))`." + ) + ) + # `dimnames` can hold plain names or indices; `name` maps an index to its name and is the + # identity on a plain name. An index's space is ignored, since the array carries the axes. + return _NamedTensor(unnamed, collect(DimName, name.(dimnames))) +end + +# The dimension-name type is inferred from the names, so that indices (`NamedUnitRange`s such as +# `Index`) infer `IndexName` rather than the index type. `name` is the identity on plain names. +function NamedTensor(unnamed, dimnames) + dimnames′ = name.(dimnames) + return NamedTensor{eltype(dimnames′)}(unnamed, dimnames′) +end NamedTensor(a::AbstractNamedTensor, inds) = throw(ArgumentError("Already named.")) NamedTensor(a::AbstractNamedTensor) = NamedTensor(unnamed(a), dimnames(a)) diff --git a/src/namedunitrange.jl b/src/namedunitrange.jl index 9f5e12e..044d3a4 100644 --- a/src/namedunitrange.jl +++ b/src/namedunitrange.jl @@ -30,6 +30,14 @@ unnamed(i::NamedUnitRange) = i.value name(i::NamedUnitRange) = i.name unnamedtype(::Type{<:NamedUnitRange{<:Any, <:Any, Unnamed}}) where {Unnamed} = Unnamed +""" + space(i::NamedUnitRange) + +The space of a named range `i`: its underlying (unnamed) range or axis object, with the name +dropped. Equal to [`unnamed`](@ref) for a `NamedUnitRange`. +""" +space(i::NamedUnitRange) = unnamed(i) + # Construct from a space, minting a fresh name of the requested flavor. The space is # anything `to_range` accepts (an `Integer`, an existing range, or a sector-pair vector # when GradedArrays is loaded), so `Index(2)`, `Index(1:3)`, and diff --git a/test/test_basics.jl b/test/test_basics.jl index 6312590..fecad95 100644 --- a/test/test_basics.jl +++ b/test/test_basics.jl @@ -124,14 +124,18 @@ using UUIDs: UUID @test_throws ArgumentError NamedTensor(randn(elt, 4), (:i, :j)) @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, whether as a tuple, a vector, or a single index. + # Passing indices as a tuple or vector builds the tensor, using only their names and + # taking the space from the array. A single bare index still errors (it is ambiguous). 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 NamedTensor(randn(elt, 2, 3), (i, j)) isa ITensor + @test ITensor(randn(elt, 2, 3), (i, j)) isa ITensor + @test ITensor(randn(elt, 2, 3), [i, j]) isa ITensor + t = ITensor(randn(elt, 2, 3), (i, j)) + @test issetequal(name.(inds(t)), name.((i, j))) @test_throws ArgumentError ITensor(randn(elt, 2), i) - # The two supported constructions: index the array (inherit the space from the + # The space is taken from the array, not from the index (a mismatched index dim is ignored). + @test size(unnamed(ITensor(randn(elt, 2, 3), (i, Index(9))))) == (2, 3) + # The other 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 diff --git a/test/test_exports.jl b/test/test_exports.jl index 8a37727..397908d 100644 --- a/test/test_exports.jl +++ b/test/test_exports.jl @@ -12,7 +12,7 @@ using Test: @test, @testset :uniqueind, :uniqueinds, :unioninds, :uniquename, ] publics = [ - :IndexName, :name, :nametype, :replacedimnames, :setname, :unnamed, + :IndexName, :name, :nametype, :replacedimnames, :setname, :space, :unnamed, :unnamedtype, :decoration, :emptytags, :gettag, :gettags, :hastag, :plev, :settags, :tags, :unsettags, From 43a2f68193d97bac10c88a98b04a002551a5259c Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Mon, 13 Jul 2026 13:31:26 -0400 Subject: [PATCH 2/2] Drop Compat dependency and unify NamedTensor constructor normalization Declares the package's public (non-exported) names with the native `public` keyword guarded on `VERSION`, dropping the `Compat` dependency. The guard keeps the package loading on Julia 1.10, where `public` is not yet parsed. Both `NamedTensor` constructors now apply the same bare-index guard and strip names exactly once, instead of the bare method delegating through the parameterized one (which stripped names twice). --- Project.toml | 2 -- src/ITensorBase.jl | 26 +++++++------------------- src/namedtensor.jl | 26 +++++++++++++++----------- 3 files changed, 22 insertions(+), 32 deletions(-) diff --git a/Project.toml b/Project.toml index 03d2e07..42828b3 100644 --- a/Project.toml +++ b/Project.toml @@ -11,7 +11,6 @@ AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" ArrayLayouts = "4c555306-a7a7-4459-81d9-ec55ddd5c99a" Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" -Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MatrixAlgebraKit = "6c742aac-3347-4629-af66-fc926824e5e4" @@ -45,7 +44,6 @@ Accessors = "0.1.39" Adapt = "4.1.1" ArrayLayouts = "1.11" Combinatorics = "1" -Compat = "4.16" ConstructionBase = "1.6" LinearAlgebra = "1.10" MatrixAlgebraKit = "0.2, 0.3, 0.4, 0.5, 0.6" diff --git a/src/ITensorBase.jl b/src/ITensorBase.jl index 07770c9..97d5168 100644 --- a/src/ITensorBase.jl +++ b/src/ITensorBase.jl @@ -5,25 +5,13 @@ export AbstractNamedTensor, NamedTensor, AbstractITensor, ITensor, Index, 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 -@compat public @names -@compat public IndexName, - name, - nametype, - replacedimnames, - setname, - space, - unnamed, - unnamedtype -@compat public decoration, - emptytags, - gettag, - gettags, - hastag, - plev, - settags, - tags, - unsettags +if VERSION >= v"1.11.0-DEV.469" + eval( + Meta.parse( + "public @names, IndexName, name, nametype, replacedimnames, setname, space, unnamed, unnamedtype, decoration, emptytags, gettag, gettags, hastag, plev, settags, tags, unsettags" + ) + ) +end # Named-array machinery (relocated from NamedDimsArrays.jl). include("isnamed.jl") diff --git a/src/namedtensor.jl b/src/namedtensor.jl index fbdaf61..dbba7ea 100644 --- a/src/namedtensor.jl +++ b/src/namedtensor.jl @@ -37,25 +37,29 @@ struct NamedTensor{DimName} <: AbstractNamedTensor{DimName} end end +# `dimnames` can hold plain names or indices (`NamedUnitRange`s such as `Index`): `name` maps an +# index to its name and is the identity on a plain name, so an index's space is ignored (the array +# carries the axes). A single bare index is rejected, since it is ambiguous as `dimnames` (a +# `NamedUnitRange` is itself an iterable of its range values). The two methods repeat this +# normalization rather than one delegating to the other, so each strips names exactly once. function NamedTensor{DimName}(unnamed, dimnames) where {DimName} - # A bare index (`NamedUnitRange` such as `Index`) is ambiguous as `dimnames` (it is itself - # an iterable of its range values), so require a tuple/vector of names or indices. dimnames isa NamedUnitRange && throw( ArgumentError( - "Got a single index (`NamedUnitRange` such as `Index`) as the dimension names; \ - pass a tuple or vector, e.g. `ITensor(array, (i, j))`." + "Got a single index (`NamedUnitRange` such as `Index`) as the dimension names. \ + Pass a tuple or vector, e.g. `ITensor(array, (i, j))`." ) ) - # `dimnames` can hold plain names or indices; `name` maps an index to its name and is the - # identity on a plain name. An index's space is ignored, since the array carries the axes. return _NamedTensor(unnamed, collect(DimName, name.(dimnames))) end - -# The dimension-name type is inferred from the names, so that indices (`NamedUnitRange`s such as -# `Index`) infer `IndexName` rather than the index type. `name` is the identity on plain names. +# The dimension-name type is inferred from the names, so indices infer `IndexName`, not their type. function NamedTensor(unnamed, dimnames) - dimnames′ = name.(dimnames) - return NamedTensor{eltype(dimnames′)}(unnamed, dimnames′) + dimnames isa NamedUnitRange && throw( + ArgumentError( + "Got a single index (`NamedUnitRange` such as `Index`) as the dimension names. \ + Pass a tuple or vector, e.g. `ITensor(array, (i, j))`." + ) + ) + return _NamedTensor(unnamed, collect(name.(dimnames))) end NamedTensor(a::AbstractNamedTensor, inds) = throw(ArgumentError("Already named.")) NamedTensor(a::AbstractNamedTensor) = NamedTensor(unnamed(a), dimnames(a))