From dc8804062ac8893d336f80738c97405eee7b2924 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 23 Jul 2026 20:05:31 -0400 Subject: [PATCH] Accept bare labels as tags with an empty value Adds bare labels to the accepted tag inputs, so `tags = "i"` is shorthand for `tags = "i" => ""`: a `String` or `Symbol`, a collection of them, or a collection mixing them with `key => value` pairs, each becomes a tag with an empty value. `settags` accepts the same forms, and an empty-value tag now prints as just its key (`flux` rather than `flux=>`). --- Project.toml | 2 +- src/index.jl | 35 ++++++++++++++++++++++++++--------- test/test_tags.jl | 20 ++++++++++++++++++++ 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/Project.toml b/Project.toml index 0adb7f2..b0f35ae 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ITensorBase" uuid = "4795dd04-0d67-49bb-8f44-b89c448a1dc7" -version = "0.13.1" +version = "0.13.2" authors = ["ITensor developers and contributors"] [workspace] diff --git a/src/index.jl b/src/index.jl index cc59e8c..e7301f1 100644 --- a/src/index.jl +++ b/src/index.jl @@ -3,7 +3,11 @@ using Random: AbstractRNG, RandomDevice using TensorAlgebra: TensorAlgebra as TA using UUIDs: UUID, uuid4 -tagpairstring(pair::Pair) = string(first(pair)) * "=>" * string(last(pair)) +# A tag with an empty value is a bare label: print just its key, so `"i" => ""` shows as `i`. +function tagpairstring(pair::Pair) + key, value = string(first(pair)), string(last(pair)) + return isempty(value) ? key : key * "=>" * value +end function tagsstring(tags) tagpairs = collect(tags) # SortedDict iterates in sorted-key order tagpair1, tagpair_rest = Iterators.peel(tagpairs) @@ -56,6 +60,14 @@ 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) +# A bare label (a `String` or `Symbol` with no value) is a tag with an empty value. A lone one +# is a single tag, not a collection to iterate over (a `String` would iterate into its `Char`s), +# so wrap it in a one-element tuple like the varargs `Pair` method above. +for T in (AbstractString, Symbol) + @eval to_symbol_pair(tag::$T) = Symbol(tag) => Symbol("") + @eval to_tags(tag::$T) = to_tags((tag,)) +end + uuid(n::IndexName) = getfield(n, :uuid) # Internal: the stored tags as `Symbol => Symbol`, used by the hot comparison, @@ -174,15 +186,19 @@ end Return a new index or index name with the given tags inserted or overwritten. This is a merge: tags under other keys are kept, and a key that already exists is overwritten. Tags -are given as one or more `key => value` pairs, a collection of pairs, or an `AbstractDict`; -keys and values may be `String`s or `Symbol`s. See also [`unsettags`](@ref), -[`emptytags`](@ref). +are given as one or more `key => value` pairs, bare labels (a `String` or `Symbol`, taken as +a tag with an empty value), a collection mixing these, or an `AbstractDict`; keys and values +may be `String`s or `Symbol`s. See also [`unsettags`](@ref), [`emptytags`](@ref). """ settags(n::IndexName, ps::Pair...) = settags(n, ps) -# A lone `Pair` iterates over its two elements, so the varargs method above needs to exist -# rather than letting a single pair fall through to the `for (k, v) in ps` loop (cf. `to_tags`). +# A lone `Pair` or bare label iterates over its elements, so these single-tag methods need to +# exist rather than letting one fall through to the `for p in ps` loop below (cf. `to_tags`). +for T in (AbstractString, Symbol) + @eval settags(n::IndexName, tag::$T) = settags(n, (tag,)) +end function settags(n::IndexName, ps) - for (k, v) in ps + for p in ps + k, v = to_symbol_pair(p) n = settag(n, k, v) end return n @@ -322,8 +338,9 @@ name, so two indices built the same way are still distinct, and tensors share a only when they share the same `Index`. `tags` and `plev` decorate the freshly minted name, as in `Index(2; tags = "i" => "1", plev = 1)`, -and default to no tags and prime level `0`. `tags` accepts the same inputs as [`settags`](@ref) -(a pair, a collection of pairs, or an `AbstractDict`). +and default to no tags and prime level `0`. `tags` accepts the same inputs as [`settags`](@ref): +a `key => value` pair, a bare label like `"i"` (a `String` or `Symbol`, taken as a tag with an +empty value), a collection mixing these, or an `AbstractDict`. # Examples diff --git a/test/test_tags.jl b/test/test_tags.jl index 8435aa1..1bcf8b3 100644 --- a/test/test_tags.jl +++ b/test/test_tags.jl @@ -31,6 +31,26 @@ newbond(t, keep) = only(filter(!in(keep), collect(inds(t)))) Dict("a" => "9", "b" => "2", "c" => "3") end + @testset "bare labels default to an empty value" begin + # A bare label (String or Symbol) is a tag with an empty value. + @test tags(Index(2; tags = "i")) == Dict("i" => "") + @test tags(Index(2; tags = :i)) == Dict("i" => "") + # A collection of bare labels. + @test tags(Index(2; tags = ["i", "Site"])) == Dict("i" => "", "Site" => "") + @test tags(Index(2; tags = [:i, :Site])) == Dict("i" => "", "Site" => "") + # Bare labels mix with `key => value` pairs in a collection. + @test tags(Index(2; tags = ("Site", "n" => "1"))) == Dict("Site" => "", "n" => "1") + # A bare label is exactly the empty-value pair. + @test tags(Index(2; tags = "flux")) == tags(Index(2; tags = "flux" => "")) + # settags accepts the same bare-label forms. + @test tags(settags(Index(2), "j")) == Dict("j" => "") + @test tags(settags(Index(2), ["a", "b"])) == Dict("a" => "", "b" => "") + # An empty-value tag prints as just its key; a `key => value` tag keeps the `=>`. + @test occursin("flux", sprint(show, Index(2; tags = "flux"))) + @test !occursin("flux=>", sprint(show, Index(2; tags = "flux"))) + @test occursin("n=>1", sprint(show, Index(2; tags = "n" => "1"))) + end + @testset "unsettags / emptytags are permissive" begin i = settags(Index(2), "a" => "1", "b" => "2", "c" => "3") @test tags(unsettags(i, ["a", "zzz"])) == Dict("b" => "2", "c" => "3") # absent key ignored