From f3d440a7642fe10836d36096d42a8b60c5b244b7 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 1 Oct 2026 10:47:49 -0400 Subject: [PATCH 01/16] Bump compat to GradedArrays 0.17 GradedArrays replaced `SectorRange` with its own `Sector`, so the flux constructors here dispatch on that instead. Both sector types are spelled qualified, since GradedArrays and TensorKitSectors each define a `Sector`. Co-Authored-By: Claude Opus 5 (1M context) --- Project.toml | 6 +++++- ext/ITensorBaseGradedArraysExt.jl | 10 +++++----- test/Project.toml | 2 +- test/test_gradedarraysext.jl | 12 +++++------- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/Project.toml b/Project.toml index 4955010..aa96510 100644 --- a/Project.toml +++ b/Project.toml @@ -37,6 +37,10 @@ ITensorBaseMooncakeExt = "Mooncake" ITensorBaseOMEinsumContractionOrdersExt = "OMEinsumContractionOrders" ITensorBaseTensorKitExt = "TensorKit" +[sources.GradedArrays] +rev = "mf/sector-type-overhaul" +url = "https://github.com/ITensor/GradedArrays.jl" + [compat] AbstractTrees = "0.4.5" Accessors = "0.1.39" @@ -44,7 +48,7 @@ Adapt = "4.1.1" ArrayLayouts = "1.11" Combinatorics = "1" ConstructionBase = "1.6" -GradedArrays = "0.16.6" +GradedArrays = "0.17" LinearAlgebra = "1.10" MatrixAlgebraKit = "0.2, 0.3, 0.4, 0.5, 0.6" Mooncake = "0.4.202, 0.5" diff --git a/ext/ITensorBaseGradedArraysExt.jl b/ext/ITensorBaseGradedArraysExt.jl index 4515332..0f0f8d2 100644 --- a/ext/ITensorBaseGradedArraysExt.jl +++ b/ext/ITensorBaseGradedArraysExt.jl @@ -1,10 +1,10 @@ module ITensorBaseGradedArraysExt -using GradedArrays: FusedGradedDiagonal, FusedGradedMatrix, SectorRange +using GradedArrays: GradedArrays as GA, FusedGradedDiagonal, FusedGradedMatrix using ITensorBase: ITensorBase, NamedTensor, name, uniquename, unnamed using Random: AbstractRNG, default_rng using TensorAlgebra: TensorAlgebra as TA -using TensorKitSectors: Sector +using TensorKitSectors: TensorKitSectors as TKS const NamedUnitRange = ITensorBase.NamedUnitRange @@ -39,8 +39,8 @@ end # Flux-canceling constructors at the `Index` level: delegate to the GradedArrays flux backend on # the unnamed axes, then reattach names, so the flux convention lives only in the backend. The -# sector may be a bare `TensorKitSectors.Sector` or a `SectorRange`; this is an extension because -# ITensorBase does not depend on the sector types. +# sector may be a `TKS.Sector` or a `GA.Sector`, the same pair GradedArrays' own flux constructors +# dispatch on. This is an extension because ITensorBase does not depend on the sector types. # Name the delegated result: the physical-leg names followed by a fresh name for the dangling aux # leg, minted of the legs' name type (not hardcoded to `IndexName`). @@ -53,7 +53,7 @@ end # Three signature groups, each carrying a named physical axis so overloading `Base` is not piracy: # nonempty codomain with a (possibly empty) domain, the codomain-only form, and empty codomain with # a nonempty domain. The all-empty flux-only case has no named leg and is left to the backend. -for S in (Sector, SectorRange) +for S in (TKS.Sector, GA.Sector) # Nonempty codomain, domain given (possibly empty). for f in (:rand, :randn) @eval begin diff --git a/test/Project.toml b/test/Project.toml index f4a39e2..90741c8 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -32,7 +32,7 @@ AbstractTrees = "0.4.5" Adapt = "4" Aqua = "0.8.9" Combinatorics = "1" -GradedArrays = "0.16.6" +GradedArrays = "0.17" ITensorBase = "0.15" ITensorPkgSkeleton = "0.3.42" JLArrays = "0.2, 0.3" diff --git a/test/test_gradedarraysext.jl b/test/test_gradedarraysext.jl index 13eb0ad..93b0990 100644 --- a/test/test_gradedarraysext.jl +++ b/test/test_gradedarraysext.jl @@ -1,4 +1,4 @@ -using GradedArrays: U1, sectors +using GradedArrays: U1, fU1, sectors using ITensorBase: ITensorBase, ITensor, Index, align, inds, names, prime, space, unnamed using StableRNGs: StableRNG using TensorAlgebra: TensorAlgebra, dual, isdual, matricize, project, project_aux, @@ -40,16 +40,14 @@ using Test: @test, @test_throws, @testset @test length(inds(randn(rng, U1(1), (i, j)))) == 3 @test length(inds(randn(rng, U1(1), (i,), (j,)))) == 3 - # A bare `TensorKitSectors.Sector` (fermionic) works as the flux. - s = [ - Index([FermionNumber(0) => 2, FermionNumber(1) => 2]; tags = "s" => "$n") for - n in 1:4 - ] + # The flux may be a bare `TensorKitSectors.Sector` even where the axes are graded by the + # GradedArrays sector, and the aux leg comes back carrying the GradedArrays one. + s = [Index([fU1(0) => 2, fU1(1) => 2]; tags = "s" => "$n") for n in 1:4] t = randn(rng, elt, FermionNumber(2), (s[1], s[2], s[3], s[4])) @test length(inds(t)) == 5 auxt = only(setdiff(collect(inds(t)), s)) @test isdual(auxt) && length(auxt) == 1 && - only(sectors(space(auxt))) == FermionNumber(2) + only(sectors(space(auxt))) == fU1(2) # `zeros`/`ones`/`fill` mirror `randn` (`fill` takes the value first). Each carries the # flux on an aux leg the same way. From 4716328a7b642c86eb8e78b58d99b05a8b52838c Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 1 Oct 2026 16:20:18 -0400 Subject: [PATCH 02/16] Pin GradedArrays in the test project, not the root `GradedArrays` is a weak dependency at the root, and Pkg rejects a `[sources]` entry for a package that is not in `deps` or `extras`, so the pin made the whole project fail to resolve. It is a real dependency of the test project, which is where it belongs. Co-Authored-By: Claude Opus 5 (1M context) --- Project.toml | 4 ---- test/Project.toml | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Project.toml b/Project.toml index aa96510..5b940dd 100644 --- a/Project.toml +++ b/Project.toml @@ -37,10 +37,6 @@ ITensorBaseMooncakeExt = "Mooncake" ITensorBaseOMEinsumContractionOrdersExt = "OMEinsumContractionOrders" ITensorBaseTensorKitExt = "TensorKit" -[sources.GradedArrays] -rev = "mf/sector-type-overhaul" -url = "https://github.com/ITensor/GradedArrays.jl" - [compat] AbstractTrees = "0.4.5" Accessors = "0.1.39" diff --git a/test/Project.toml b/test/Project.toml index 90741c8..1a30461 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -24,6 +24,10 @@ UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" VectorInterface = "409d34a3-91d5-4945-b6ec-7529ddf182d8" WrappedUnions = "325db55a-9c6c-5b90-b1a2-ec87e7a38c44" +[sources.GradedArrays] +rev = "mf/sector-type-overhaul" +url = "https://github.com/ITensor/GradedArrays.jl" + [sources.ITensorBase] path = ".." From 7bf6fc5db6fb9ca7ac4dbca756f35e817cf41607 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 1 Oct 2026 16:20:22 -0400 Subject: [PATCH 03/16] Show an index's space so duality is visible An `Index` printed only `length=N`, which gave a graded index no way to show its sectors or which way its arrow points. It now prints the space, and a tensor's summary line asks for the compact form and keeps the length so that one entry per leg stays readable. Co-Authored-By: Claude Opus 5 (1M context) --- src/abstractnamedtensor.jl | 5 +++-- src/index.jl | 14 ++++++++++++-- test/test_basics.jl | 4 ++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/abstractnamedtensor.jl b/src/abstractnamedtensor.jl index 4423c00..828f56b 100644 --- a/src/abstractnamedtensor.jl +++ b/src/abstractnamedtensor.jl @@ -1496,8 +1496,9 @@ end # Copy of `Base.dims2string` defined in `show.jl`. function dims_to_string(d) isempty(d) && return "0-dimensional" - length(d) == 1 && return "$(d[1])-element" - return join(map(string, d), '×') + strs = map(x -> sprint(show, x; context = :compact => true), d) + length(d) == 1 && return "$(only(strs))-element" + return join(strs, '×') end function concretetype_to_string_truncated( diff --git a/src/index.jl b/src/index.jl index baa1249..2b95cee 100644 --- a/src/index.jl +++ b/src/index.jl @@ -424,12 +424,22 @@ function primestring(plev) end end +# The space, so a graded index shows its sectors and its arrow rather than just a total length. +# A compact context asks for the length instead, which is what a tensor's summary line uses to +# stay readable with one entry per leg. function Base.show(io::IO, i::Index) - lenstr = "length=$(length(i))" + spacestr = if get(io, :compact, false) + "length=$(length(i))" + else + # A `Base.OneTo` is how an ungraded space is stored rather than how it is written, so it + # shows as the range `Index` takes. + sp = space(i) + sprint(show, sp isa Base.OneTo ? UnitRange(sp) : sp; context = io) + end idstr = "|id=$(shortid(uuid(i)))" tagsstr = !isempty(tags_stored(i)) ? "|$(tagsstring(tags_stored(i)))" : "" primestr = primestring(plev(i)) - str = "Index($(lenstr)$(idstr)$(tagsstr))$(primestr)" + str = "Index($(spacestr)$(idstr)$(tagsstr))$(primestr)" print(io, str) return nothing end diff --git a/test/test_basics.jl b/test/test_basics.jl index f506924..36d0686 100644 --- a/test/test_basics.jl +++ b/test/test_basics.jl @@ -196,11 +196,11 @@ using UUIDs: UUID @testset "show" begin i = Index(2) @test sprint(show, "text/plain", i) == - "Index(length=2|id=$(first(string(uuid(i)), 8)))" + "Index(1: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(uuid(i)), 8))|X=>Y)" + "Index(1:2|id=$(first(string(uuid(i)), 8))|X=>Y)" end @testset "whole-tensor index manipulation" begin elt = Float64 From 5d41f4aac7648336281e901c658992db5d8f0f09 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 1 Oct 2026 16:51:34 -0400 Subject: [PATCH 04/16] Add a page on constructing symmetric tensors Covers building an `Index` from sector pairs, turning it around with `dual` or `conj`, and reading a tensor's arrows off its legs. The symmetries themselves are GradedArrays', so the page links into its docs for the list rather than restating it. Co-Authored-By: Claude Opus 5 (1M context) --- docs/Project.toml | 8 ++++++++ docs/make.jl | 7 ++++++- docs/src/symmetries.md | 45 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 docs/src/symmetries.md diff --git a/docs/Project.toml b/docs/Project.toml index 453db62..bef12af 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,5 +1,7 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656" +GradedArrays = "bc96ca6e-b7c8-4bb6-888e-c93f838762c2" ITensorBase = "4795dd04-0d67-49bb-8f44-b89c448a1dc7" ITensorFormatter = "b6bf39f1-c9d3-4bad-aad8-593d802f65fd" Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" @@ -7,11 +9,17 @@ MatrixAlgebraKit = "6c742aac-3347-4629-af66-fc926824e5e4" TensorAlgebra = "68bd88dc-f39d-4e12-b2ca-f046b68fcc6a" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +[sources.GradedArrays] +rev = "mf/sector-type-overhaul" +url = "https://github.com/ITensor/GradedArrays.jl" + [sources.ITensorBase] path = ".." [compat] Documenter = "1" +DocumenterInterLinks = "1" +GradedArrays = "0.17" ITensorBase = "0.15" ITensorFormatter = "0.2.27" Literate = "2" diff --git a/docs/make.jl b/docs/make.jl index ae95e9c..f554d9c 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,4 +1,5 @@ using Documenter: Documenter, DocMeta, deploydocs, makedocs +using DocumenterInterLinks: InterLinks using ITensorBase using ITensorFormatter: ITensorFormatter @@ -10,6 +11,8 @@ DocMeta.setdocmeta!(ITensorBase, :DocTestSetup, :(using ITensorBase); recursive ITensorFormatter.make_index!(pkgdir(ITensorBase)) +links = InterLinks("GradedArrays" => "https://itensor.github.io/GradedArrays.jl/dev/") + makedocs(; modules = [ITensorBase], authors = "ITensor developers and contributors", @@ -22,9 +25,11 @@ makedocs(; pages = [ "Home" => "index.md", "User Interface" => "user_interface.md", + "Symmetric tensors" => "symmetries.md", "Developer Interface" => "dev_interface.md", "Reference" => "reference.md", - ] + ], + plugins = [links] ) deploydocs(; diff --git a/docs/src/symmetries.md b/docs/src/symmetries.md new file mode 100644 index 0000000..bcdc576 --- /dev/null +++ b/docs/src/symmetries.md @@ -0,0 +1,45 @@ +# Symmetric tensors + +```@meta +CurrentModule = ITensorBase +``` + +An [`Index`](@ref) can carry a graded space, which makes a tensor over it block sparse and makes +contraction conserve the symmetry. The spaces and the sectors that grade them come from +GradedArrays, whose [symmetry sectors](@extref GradedArrays Symmetry-sectors) page lists the +symmetries available. + +Pass `sector => multiplicity` pairs where an ungraded index takes a length. + +```@example symmetries +using ITensorBase: ITensor, Index, inds +using GradedArrays: U1, dual, isdual + +i = Index([U1(0) => 1, U1(1) => 2]) +``` + +## Duality + +A contraction pairs an index with a dual one, so an index carries an arrow. `dual` turns it +around, and `conj` is an alternative spelling of the same thing. + +```@example symmetries +dual(i) +``` + +```@example symmetries +conj(i) == dual(i) +``` + +A tensor over graded indices stores only the symmetry-allowed blocks. + +```@example symmetries +j = Index([U1(0) => 2, U1(1) => 1]) +a = randn(i, dual(j)) +``` + +`isdual` reports an index's arrow, which is how to read a tensor's duality off its legs. + +```@example symmetries +isdual.(inds(a)) +``` From 95ab0757aff802da50d933ee8d760431fc46e064 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 1 Oct 2026 20:23:14 -0400 Subject: [PATCH 05/16] Show an index's length rather than its range An index built as `Index(2)` stores a `Base.OneTo` and printed as `Index(1:2|id=...)`, which is neither how it was written nor how anyone would write it. It now shows the length, and an index over an explicit range still shows that range. Co-Authored-By: Claude Opus 5 (1M context) --- src/index.jl | 4 ++-- test/test_basics.jl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.jl b/src/index.jl index 2b95cee..c1412e5 100644 --- a/src/index.jl +++ b/src/index.jl @@ -432,9 +432,9 @@ function Base.show(io::IO, i::Index) "length=$(length(i))" else # A `Base.OneTo` is how an ungraded space is stored rather than how it is written, so it - # shows as the range `Index` takes. + # shows as the length `Index` takes. sp = space(i) - sprint(show, sp isa Base.OneTo ? UnitRange(sp) : sp; context = io) + sp isa Base.OneTo ? string(length(sp)) : sprint(show, sp; context = io) end idstr = "|id=$(shortid(uuid(i)))" tagsstr = !isempty(tags_stored(i)) ? "|$(tagsstring(tags_stored(i)))" : "" diff --git a/test/test_basics.jl b/test/test_basics.jl index 36d0686..86e4aa1 100644 --- a/test/test_basics.jl +++ b/test/test_basics.jl @@ -196,11 +196,11 @@ using UUIDs: UUID @testset "show" begin i = Index(2) @test sprint(show, "text/plain", i) == - "Index(1:2|id=$(first(string(uuid(i)), 8)))" + "Index(2|id=$(first(string(uuid(i)), 8)))" i = settag(Index(2), "X", "Y") @test sprint(show, "text/plain", i) == - "Index(1:2|id=$(first(string(uuid(i)), 8))|X=>Y)" + "Index(2|id=$(first(string(uuid(i)), 8))|X=>Y)" end @testset "whole-tensor index manipulation" begin elt = Float64 From a8bbcc358c4994ec7ff0bba04e88ae401838a07b Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 1 Oct 2026 20:23:29 -0400 Subject: [PATCH 06/16] Lead the symmetries page with building a symmetric tensor The page opened on duality, which a reader reaches for only once they have a tensor to apply it to. It now starts from graded indices and the array constructors, then contraction, scaling and addition, and keeps duality and the available symmetries for after. Co-Authored-By: Claude Opus 5 (1M context) --- docs/src/symmetries.md | 56 +++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/docs/src/symmetries.md b/docs/src/symmetries.md index bcdc576..ce0947e 100644 --- a/docs/src/symmetries.md +++ b/docs/src/symmetries.md @@ -4,42 +4,64 @@ CurrentModule = ITensorBase ``` -An [`Index`](@ref) can carry a graded space, which makes a tensor over it block sparse and makes -contraction conserve the symmetry. The spaces and the sectors that grade them come from -GradedArrays, whose [symmetry sectors](@extref GradedArrays Symmetry-sectors) page lists the -symmetries available. - -Pass `sector => multiplicity` pairs where an ungraded index takes a length. +ITensorBase supports tensors that are symmetric under group actions by wrapping ITensors around +[GradedArrays.jl](https://github.com/ITensor/GradedArrays.jl). To get started, build +[`Index`](@ref) objects out of `sector => multiplicity` pairs and pass them to the standard Julia +array constructors (`randn`, `zeros`, and so on): ```@example symmetries -using ITensorBase: ITensor, Index, inds +using ITensorBase: Index, inds using GradedArrays: U1, dual, isdual i = Index([U1(0) => 1, U1(1) => 2]) +j = Index([U1(0) => 2, U1(1) => 1]) +k = Index([U1(0) => 1, U1(1) => 1]) + +a = randn(i, dual(j)) ``` -## Duality +Only the symmetry-allowed blocks are stored. -A contraction pairs an index with a dual one, so an index carries an arrow. `dual` turns it -around, and `conj` is an alternative spelling of the same thing. +Tensors over graded indices contract, scale and add like any others. ```@example symmetries -dual(i) +b = randn(j, dual(k)) +a * b ``` ```@example symmetries -conj(i) == dual(i) +2 * a ``` -A tensor over graded indices stores only the symmetry-allowed blocks. - ```@example symmetries -j = Index([U1(0) => 2, U1(1) => 1]) -a = randn(i, dual(j)) +a + randn(i, dual(j)) ``` -`isdual` reports an index's arrow, which is how to read a tensor's duality off its legs. +## Duality + +`dual` is a GradedArrays function that flips the arrow an index carries, and `isdual` reports +which way it points. A contraction pairs an index with its dual, which is why `b` above is built +over `j` where `a` carries `dual(j)`. ```@example symmetries isdual.(inds(a)) ``` + +A graded array partitions its indices into a codomain and a domain, the output and input legs, +and stores the block diagonal matrix that bipartitioning gives. Domain indices are implicitly +dual, which is why a domain line in the display of `a * b` above carries no `dual(...)` wrapper +even though `isdual` reports that index as dual. See +[codomain and domain](@extref GradedArrays Codomain-and-domain) for the rest, including how the +conventions line up with TensorKit's. + +## Available symmetries + +Some standard symmetries are available such as `Z2`, `fU1` (fermionic `U(1)`) and `SU2`. See +[symmetry sectors](@extref GradedArrays Symmetry-sectors) for the complete list and more details. + +Conserving more than one quantity at once means a product of symmetries, written as a +`NamedTuple` of sectors naming each factor. + +```@example symmetries +Index([(; charge = U1(0), spin = U1(1)) => 1, (; charge = U1(1), spin = U1(0)) => 2]) +``` From 0bf3be543b42790fbc7fad38a6570ca6aecf4378 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 1 Oct 2026 21:01:37 -0400 Subject: [PATCH 07/16] Reword the symmetric tensors docs Introduces duality through what contracts with what, and points at the graded arrays page rather than one section of it. Co-Authored-By: Claude Opus 5 (1M context) --- docs/src/symmetries.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/src/symmetries.md b/docs/src/symmetries.md index ce0947e..550400d 100644 --- a/docs/src/symmetries.md +++ b/docs/src/symmetries.md @@ -34,33 +34,33 @@ a * b ``` ```@example symmetries -a + randn(i, dual(j)) +c = randn(i, dual(j)) +a + c ``` ## Duality `dual` is a GradedArrays function that flips the arrow an index carries, and `isdual` reports -which way it points. A contraction pairs an index with its dual, which is why `b` above is built -over `j` where `a` carries `dual(j)`. +which way it points. Indices can only contract with ones that have opposite duality, for example +the `j` Index of `b` contracts with the `dual(j)` Index of `a`. ```@example symmetries isdual.(inds(a)) ``` -A graded array partitions its indices into a codomain and a domain, the output and input legs, -and stores the block diagonal matrix that bipartitioning gives. Domain indices are implicitly -dual, which is why a domain line in the display of `a * b` above carries no `dual(...)` wrapper -even though `isdual` reports that index as dual. See -[codomain and domain](@extref GradedArrays Codomain-and-domain) for the rest, including how the -conventions line up with TensorKit's. +Note that indices of a `GradedArray` are partitioned into a codomain and a domain, and the +`GradedArray` stores the block diagonal matrix corresponding to the bipartitioning of the +indices. When printing, by convention domain indices are implicitly dual (the format and +conventions are compatible with those from +[TensorKit.jl](https://github.com/Jutho/TensorKit.jl)). For more information see the +documentation on [graded arrays](@extref GradedArrays :doc:`user_interface/graded_arrays`). ## Available symmetries Some standard symmetries are available such as `Z2`, `fU1` (fermionic `U(1)`) and `SU2`. See [symmetry sectors](@extref GradedArrays Symmetry-sectors) for the complete list and more details. -Conserving more than one quantity at once means a product of symmetries, written as a -`NamedTuple` of sectors naming each factor. +You can use named sectors to conserve a product of symmetries. ```@example symmetries Index([(; charge = U1(0), spin = U1(1)) => 1, (; charge = U1(1), spin = U1(0)) => 2]) From 4e46a8b00fa757467069c3e30594272943c69976 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 1 Oct 2026 21:02:35 -0400 Subject: [PATCH 08/16] Use Oxford commas in the symmetric tensors docs Co-Authored-By: Claude Opus 5 (1M context) --- docs/src/symmetries.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/symmetries.md b/docs/src/symmetries.md index 550400d..3e9910e 100644 --- a/docs/src/symmetries.md +++ b/docs/src/symmetries.md @@ -22,7 +22,7 @@ a = randn(i, dual(j)) Only the symmetry-allowed blocks are stored. -Tensors over graded indices contract, scale and add like any others. +Tensors over graded indices contract, scale, and add like any others. ```@example symmetries b = randn(j, dual(k)) @@ -57,7 +57,7 @@ documentation on [graded arrays](@extref GradedArrays :doc:`user_interface/grade ## Available symmetries -Some standard symmetries are available such as `Z2`, `fU1` (fermionic `U(1)`) and `SU2`. See +Some standard symmetries are available such as `Z2`, `fU1` (fermionic `U(1)`), and `SU2`. See [symmetry sectors](@extref GradedArrays Symmetry-sectors) for the complete list and more details. You can use named sectors to conserve a product of symmetries. From 3c12da7c1d9df1f42e4107ce45ce09197fe9ca0b Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 1 Oct 2026 21:04:30 -0400 Subject: [PATCH 09/16] Bump the version to 0.16.0 Dropping GradedArrays 0.16 from compat is breaking, and main has since released 0.15.2. Co-Authored-By: Claude Opus 5 (1M context) --- Project.toml | 2 +- docs/Project.toml | 2 +- test/Project.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index 5b940dd..f27bc0f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ITensorBase" uuid = "4795dd04-0d67-49bb-8f44-b89c448a1dc7" -version = "0.15.3" +version = "0.16.0" authors = ["ITensor developers and contributors"] [workspace] diff --git a/docs/Project.toml b/docs/Project.toml index bef12af..b6da3f1 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -20,7 +20,7 @@ path = ".." Documenter = "1" DocumenterInterLinks = "1" GradedArrays = "0.17" -ITensorBase = "0.15" +ITensorBase = "0.16" ITensorFormatter = "0.2.27" Literate = "2" MatrixAlgebraKit = "0.2, 0.3, 0.4, 0.5, 0.6" diff --git a/test/Project.toml b/test/Project.toml index 1a30461..43d2c68 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -37,7 +37,7 @@ Adapt = "4" Aqua = "0.8.9" Combinatorics = "1" GradedArrays = "0.17" -ITensorBase = "0.15" +ITensorBase = "0.16" ITensorPkgSkeleton = "0.3.42" JLArrays = "0.2, 0.3" LinearAlgebra = "1.10" From d33089faa9e48f955a1f7bbce197214d540f69d2 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 1 Oct 2026 21:32:21 -0400 Subject: [PATCH 10/16] Print an Index as the space it was written with Adds `from_range`, the inverse of `to_range`, so a graded index shows its `sector => multiplicity` pairs instead of the `gradedrange` call that built the range. The test files that pulled `names` into scope now qualify it, since shadowing `Base.names` broke any file included after them. Co-Authored-By: Claude Opus 5 (1M context) --- examples/Project.toml | 2 +- ext/ITensorBaseGradedArraysExt.jl | 6 ++++++ src/ITensorBase.jl | 2 +- src/index.jl | 9 ++++++--- src/namedunitrange.jl | 11 +++++++++++ test/test_exports.jl | 2 +- test/test_gradedarraysext.jl | 15 +++++++++++++-- test/test_linearalgebra.jl | 4 ++-- test/test_mooncakeext.jl | 6 +++--- test/test_tensoralgebra.jl | 22 +++++++++++----------- test/test_tensorkitext.jl | 18 +++++++++--------- test/test_vectorinterface.jl | 16 ++++++++-------- 12 files changed, 72 insertions(+), 41 deletions(-) diff --git a/examples/Project.toml b/examples/Project.toml index c219fcf..d639c85 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -6,5 +6,5 @@ MatrixAlgebraKit = "6c742aac-3347-4629-af66-fc926824e5e4" path = ".." [compat] -ITensorBase = "0.15" +ITensorBase = "0.16" MatrixAlgebraKit = "0.2, 0.3, 0.4, 0.5, 0.6" diff --git a/ext/ITensorBaseGradedArraysExt.jl b/ext/ITensorBaseGradedArraysExt.jl index 0f0f8d2..dfc3081 100644 --- a/ext/ITensorBaseGradedArraysExt.jl +++ b/ext/ITensorBaseGradedArraysExt.jl @@ -8,6 +8,12 @@ using TensorKitSectors: TensorKitSectors as TKS const NamedUnitRange = ITensorBase.NamedUnitRange +# The `sector => multiplicity` pairs `gradedrange` takes, so an `Index` over a graded space +# prints what it was written with rather than the `gradedrange(...)` call that built the range. +function ITensorBase.from_range(g::GA.AbstractGradedOneTo) + return [s => m for (s, m) in zip(GA.sectors(g), GA.datalengths(g))] +end + # GradedArrays defines `unmatricize` for its fused matrices with untyped axes, which ties with # ITensorBase's methods on named axes. Restate those for each fused matrix type so the named # unmatricize of a graded matrix has a unique most-specific method. diff --git a/src/ITensorBase.jl b/src/ITensorBase.jl index 983be9e..b9e3e34 100644 --- a/src/ITensorBase.jl +++ b/src/ITensorBase.jl @@ -11,7 +11,7 @@ export AbstractNamedTensor, NamedTensor, AbstractITensor, ITensor, Index, if VERSION >= v"1.11.0-DEV.469" eval( Meta.parse( - "public @names, IndexName, mulopadd!, name, names, setname, space, unnamed, unnamedtype, decoration, emptytags, gettag, gettags, hastag, plev, settags, tags, unsettags" + "public @names, IndexName, from_range, mulopadd!, name, names, setname, space, unnamed, unnamedtype, decoration, emptytags, gettag, gettags, hastag, plev, settags, tags, unsettags" ) ) end diff --git a/src/index.jl b/src/index.jl index c1412e5..510e172 100644 --- a/src/index.jl +++ b/src/index.jl @@ -431,10 +431,13 @@ function Base.show(io::IO, i::Index) spacestr = if get(io, :compact, false) "length=$(length(i))" else - # A `Base.OneTo` is how an ungraded space is stored rather than how it is written, so it - # shows as the length `Index` takes. + # The space the `Index` was written with rather than the range it stores, with duality + # factored outside it the way a graded range prints its own `dual`. `:typeinfo` drops the + # element-type prefix a vector of `sector => multiplicity` pairs would otherwise carry. sp = space(i) - sp isa Base.OneTo ? string(length(sp)) : sprint(show, sp; context = io) + spec = from_range(TA.isdual(sp) ? TA.dual(sp) : sp) + specstr = sprint(show, spec; context = IOContext(io, :typeinfo => typeof(spec))) + TA.isdual(sp) ? "dual($(specstr))" : specstr end idstr = "|id=$(shortid(uuid(i)))" tagsstr = !isempty(tags_stored(i)) ? "|$(tagsstring(tags_stored(i)))" : "" diff --git a/src/namedunitrange.jl b/src/namedunitrange.jl index f8352cc..67398d7 100644 --- a/src/namedunitrange.jl +++ b/src/namedunitrange.jl @@ -38,6 +38,17 @@ dropped. Equal to [`unnamed`](@ref) for a `NamedUnitRange`. """ space(i::NamedUnitRange) = unnamed(i) +""" + from_range(r) + +The space `r` was built from, the inverse of `TensorAlgebra.to_range`. This is what a range +is written as rather than what it stores, so a `Base.OneTo` gives back its length and, with +GradedArrays loaded, a graded space gives back its `sector => multiplicity` pairs. Used for +printing. Anything else is its own space. +""" +from_range(r) = r +from_range(r::Base.OneTo) = length(r) + # 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_exports.jl b/test/test_exports.jl index dd608de..0687dfd 100644 --- a/test/test_exports.jl +++ b/test/test_exports.jl @@ -14,7 +14,7 @@ using Test: @test, @testset :tryuniqueind, :uniqueind, :uniqueinds, :unioninds, :uniquename, ] publics = [ - :IndexName, :mulopadd!, :name, :names, :setname, :space, :unnamed, + :IndexName, :from_range, :mulopadd!, :name, :names, :setname, :space, :unnamed, :unnamedtype, :decoration, :emptytags, :gettag, :gettags, :hastag, :plev, :settags, :tags, :unsettags, diff --git a/test/test_gradedarraysext.jl b/test/test_gradedarraysext.jl index 93b0990..5148cf2 100644 --- a/test/test_gradedarraysext.jl +++ b/test/test_gradedarraysext.jl @@ -1,5 +1,5 @@ using GradedArrays: U1, fU1, sectors -using ITensorBase: ITensorBase, ITensor, Index, align, inds, names, prime, space, unnamed +using ITensorBase: ITensorBase, ITensor, Index, align, inds, prime, space, unnamed, uuid using StableRNGs: StableRNG using TensorAlgebra: TensorAlgebra, dual, isdual, matricize, project, project_aux, tryproject, tryproject_aux, unchecked_project, unchecked_project_aux, unmatricize @@ -121,7 +121,7 @@ end @test m isa AbstractMatrix{elt} @test size(m) == (length(i) * length(j), length(k)) rt = unmatricize(m, (i, j), (k,)) - @test names(rt) == names(a) + @test ITensorBase.names(rt) == ITensorBase.names(a) @test isdual(inds(rt)[3]) @test unnamed(rt) ≈ unnamed(a) end @@ -185,3 +185,14 @@ end # Naming the dimensions flat claims no split, so it stays available. @test ITensor(m, (i, dual(j))) isa ITensor end + +# An `Index` prints the space it was written with, so a graded one shows its +# `sector => multiplicity` pairs rather than the `gradedrange(...)` call that built the range, +# with `dual` factored outside the pairs. +@testset "GradedArraysExt Index show" begin + i = Index([U1(0) => 1, U1(1) => 2]) + @test sprint(show, "text/plain", i) == + "Index([U1(0) => 1, U1(1) => 2]|id=$(first(string(uuid(i)), 8)))" + @test sprint(show, "text/plain", dual(i)) == + "Index(dual([U1(0) => 1, U1(1) => 2])|id=$(first(string(uuid(i)), 8)))" +end diff --git a/test/test_linearalgebra.jl b/test/test_linearalgebra.jl index d59a96d..8b301e1 100644 --- a/test/test_linearalgebra.jl +++ b/test/test_linearalgebra.jl @@ -1,5 +1,5 @@ import LinearAlgebra as LA -using ITensorBase: Named, names, unname, unnamed +using ITensorBase: ITensorBase, Named, unname, unnamed using Test: @test, @testset @testset "LinearAlgebra (eltype=$(elt))" for elt in @@ -14,5 +14,5 @@ using Test: @test, @testset @test unnamed(LA.lmul!(2, copy(a))) ≈ 2 * unnamed(a) @test unnamed(LA.rdiv!(copy(a), 2)) ≈ unnamed(a) / 2 @test unnamed(LA.ldiv!(2, copy(a))) ≈ 2 \ unnamed(a) - @test LA.dot(a, b) ≈ LA.dot(unnamed(a), unname(b, names(a))) + @test LA.dot(a, b) ≈ LA.dot(unnamed(a), unname(b, ITensorBase.names(a))) end diff --git a/test/test_mooncakeext.jl b/test/test_mooncakeext.jl index b926b94..c0a427c 100644 --- a/test/test_mooncakeext.jl +++ b/test/test_mooncakeext.jl @@ -1,4 +1,4 @@ -using ITensorBase: Name, NamedTensor, NamedUnitRange, inds, name, nameperm, names, +using ITensorBase: ITensorBase, Name, NamedTensor, NamedUnitRange, inds, name, nameperm, names_setdiff, to_inds, uniquename using LinearAlgebra: mul! using Mooncake: Mooncake @@ -23,8 +23,8 @@ using Test: @test, @testset Mooncake.TestUtils.test_rule( rng, nameperm, a1, (i,), (j,); mode, is_primitive ) - Mooncake.TestUtils.test_rule(rng, names, a1; mode, is_primitive) - Mooncake.TestUtils.test_rule(rng, names, a1, 1; mode, is_primitive) + Mooncake.TestUtils.test_rule(rng, ITensorBase.names, a1; mode, is_primitive) + Mooncake.TestUtils.test_rule(rng, ITensorBase.names, a1, 1; mode, is_primitive) Mooncake.TestUtils.test_rule(rng, inds, a1; mode, is_primitive) Mooncake.TestUtils.test_rule(rng, inds, a1, 1; mode, is_primitive) Mooncake.TestUtils.test_rule( diff --git a/test/test_tensoralgebra.jl b/test/test_tensoralgebra.jl index 207c38c..e453767 100644 --- a/test/test_tensoralgebra.jl +++ b/test/test_tensoralgebra.jl @@ -1,5 +1,5 @@ -using ITensorBase: ITensorBase, Index, NamedOneTo, id, inds, mulopadd!, name, names, - operator, prime, rename, unname, unnamed +using ITensorBase: ITensorBase, Index, NamedOneTo, id, inds, mulopadd!, name, operator, + prime, rename, unname, unnamed using LinearAlgebra: mul!, norm, tr 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, svd_vals @@ -185,18 +185,18 @@ using Test: @test, @test_broken, @testset # the three-argument form builds an operator from the codomain/domain split top = project(Sz, (prime(i),), (i,)) @test eltype(top) === elt - @test Set(names(top)) == Set(name.((prime(i), i))) + @test Set(ITensorBase.names(top)) == Set(name.((prime(i), i))) @test unname(top, (prime(i), i)) == Sz # `unchecked_project` skips the (for dense, always exact) verification @test unname(unchecked_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 names(s) == [name(i)] + @test ITensorBase.names(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 names(bra) == [name(i)] + @test ITensorBase.names(bra) == [name(i)] @test unname(bra, (i,)) == v end @testset "rename with index keys" begin @@ -204,14 +204,14 @@ using Test: @test, @test_broken, @testset 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 names(rename(a, i => k)) == - names(rename(a, "i" => "k")) + @test ITensorBase.names(rename(a, i => k)) == + ITensorBase.names(rename(a, "i" => "k")) @test rename(a, i => k) isa typeof(a) # Mixed index/name keys and values are accepted. - @test names(rename(a, i => "k")) == - names(rename(a, "i" => "k")) - @test names(rename(a, "i" => k)) == - names(rename(a, "i" => "k")) + @test ITensorBase.names(rename(a, i => "k")) == + ITensorBase.names(rename(a, "i" => "k")) + @test ITensorBase.names(rename(a, "i" => k)) == + ITensorBase.names(rename(a, "i" => "k")) end @testset "trivialrange on named ranges" begin i = Index(3) diff --git a/test/test_tensorkitext.jl b/test/test_tensorkitext.jl index f179661..845d3dd 100644 --- a/test/test_tensorkitext.jl +++ b/test/test_tensorkitext.jl @@ -1,4 +1,4 @@ -using ITensorBase: ITensorBase, ITensor, Index, align, name, names, prime, unnamed +using ITensorBase: ITensorBase, ITensor, Index, align, name, prime, unnamed using LinearAlgebra: norm using MatrixAlgebraKit: qr_compact, svd_compact using StableRNGs: StableRNG @@ -60,7 +60,7 @@ using Test: @test, @test_throws, @testset # Contraction over the shared (dualized) leg matches a direct TensorKit reference. b = randn(rng, elt, conj(j), k) c = a * b - @test Set(names(c)) == Set(name.((i, k))) + @test Set(ITensorBase.names(c)) == Set(name.((i, k))) ta, tb, gc = unnamed(a), unnamed(b), unnamed(c) @tensor ref[vi; vk] := ta[vi, vj] * tb[vj, vk] @test TK.space(ref) == TK.space(gc) @@ -131,7 +131,7 @@ using Test: @test, @test_throws, @testset cd = randn(rng, elt, (), (j,)) @test unnamed(cd) isa AbstractTensorMap @test TK.space(unnamed(cd)) == (one(Vj) ← Vj) - @test names(cd) == [name(j)] + @test ITensorBase.names(cd) == [name(j)] @test TK.space(unnamed(zeros(elt, (), (j,)))) == (one(Vj) ← Vj) @test_throws MethodError randn(rng, elt, (), ()) @@ -143,7 +143,7 @@ using Test: @test, @test_throws, @testset @test mm isa AbstractTensorMap @test TK.space(mm) == ((Vi ⊗ Vj) ← Vk) rt = unmatricize(mm, (i, j), (k,)) - @test names(rt) == names(ma) + @test ITensorBase.names(rt) == ITensorBase.names(ma) @test TK.space(unnamed(rt)) == TK.space(unnamed(ma)) @test unnamed(rt) ≈ unnamed(ma) @@ -151,17 +151,17 @@ using Test: @test, @test_throws, @testset # result and the map form re-expresses the requested codomain/domain split, both # carrying each index with its arrow to the new position. mf = align(m, (j, i)) - @test names(mf) == [name(j), name(i)] + @test ITensorBase.names(mf) == [name(j), name(i)] @test TK.space(unnamed(mf), 1) == TK.dual(Vj) @test TK.space(unnamed(mf), 2) == Vi md = align(m, (j,), (i,)) - @test names(md) == [name(j), name(i)] + @test ITensorBase.names(md) == [name(j), name(i)] @test TK.space(unnamed(md)) == (TK.dual(Vj) ← TK.dual(Vi)) @test TK.space(unnamed(md), 1) == TK.dual(Vj) @test TK.space(unnamed(md), 2) == Vi # An empty codomain moves both indices into the domain, preserving the outward axes. me = align(m, (), (i, j)) - @test names(me) == [name(i), name(j)] + @test ITensorBase.names(me) == [name(i), name(j)] @test TK.space(unnamed(me)) == (one(Vi) ← (TK.dual(Vi) ⊗ Vj)) @test TK.space(unnamed(me), 1) == Vi @test TK.space(unnamed(me), 2) == TK.dual(Vj) @@ -179,7 +179,7 @@ using Test: @test, @test_throws, @testset top = project(Sz, (prime(w),), (w,)) @test unnamed(top) isa AbstractTensorMap @test TK.space(unnamed(top)) == (W ← W) - @test Set(names(top)) == Set(name.((prime(w), w))) + @test Set(ITensorBase.names(top)) == Set(name.((prime(w), w))) # a charge-breaking operator is projected to zero by `unchecked_project`; the checked # `project` rejects the discard @@ -197,7 +197,7 @@ using Test: @test, @test_throws, @testset cobra = project(elt[1, 0], (), (w,)) @test unnamed(cobra) isa AbstractTensorMap @test TK.space(unnamed(cobra)) == (one(W) ← W) - @test Set(names(cobra)) == Set((name(w),)) + @test Set(ITensorBase.names(cobra)) == Set((name(w),)) end end diff --git a/test/test_vectorinterface.jl b/test/test_vectorinterface.jl index 11724aa..09d2073 100644 --- a/test/test_vectorinterface.jl +++ b/test/test_vectorinterface.jl @@ -1,5 +1,5 @@ import VectorInterface as VI -using ITensorBase: Named, names, unnamed +using ITensorBase: ITensorBase, Named, unnamed using Test: @test, @testset # These name-aware methods are what let an NamedTensor be used directly as a vector in @@ -11,7 +11,7 @@ using Test: @test, @testset a = randn(elt, i, j) b = randn(elt, j, i) ua = unnamed(a) - ub = unnamed(b, names(a)) + ub = unnamed(b, ITensorBase.names(a)) @test VI.scalartype(a) === elt @test VI.scalartype([a, b]) === elt @@ -21,7 +21,7 @@ using Test: @test, @testset z = VI.zerovector(a, ComplexF64) @test VI.scalartype(z) === ComplexF64 @test iszero(unnamed(z)) - @test names(z) == names(a) + @test ITensorBase.names(z) == ITensorBase.names(a) z = VI.zerovector!(copy(a)) @test VI.scalartype(z) === elt @test iszero(unnamed(z)) @@ -42,13 +42,13 @@ using Test: @test, @testset @test unnamed(s) ≈ 2im * ua # add / add! / add!! - @test unnamed(VI.add(b, a), names(a)) ≈ ub + ua - @test unnamed(VI.add(b, a, 2, 3), names(a)) ≈ 3 * ub + 2 * ua - @test unnamed(VI.add!(copy(b), a, 2, 3), names(a)) ≈ 3 * ub + 2 * ua - @test unnamed(VI.add!!(copy(b), a, 2, 3), names(a)) ≈ 3 * ub + 2 * ua + @test unnamed(VI.add(b, a), ITensorBase.names(a)) ≈ ub + ua + @test unnamed(VI.add(b, a, 2, 3), ITensorBase.names(a)) ≈ 3 * ub + 2 * ua + @test unnamed(VI.add!(copy(b), a, 2, 3), ITensorBase.names(a)) ≈ 3 * ub + 2 * ua + @test unnamed(VI.add!!(copy(b), a, 2, 3), ITensorBase.names(a)) ≈ 3 * ub + 2 * ua r = VI.add!!(copy(b), a, 2im, 3) @test VI.scalartype(r) === complex(elt) - @test unnamed(r, names(a)) ≈ 3 * ub + 2im * ua + @test unnamed(r, ITensorBase.names(a)) ≈ 3 * ub + 2im * ua @test VI.inner(a, b) ≈ VI.inner(ua, ub) end From 8e8251a669203dcb6da83d1ff4f8ef537d617077 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 1 Oct 2026 21:41:28 -0400 Subject: [PATCH 11/16] Keep from_range internal Co-Authored-By: Claude Opus 5 (1M context) --- src/ITensorBase.jl | 2 +- src/namedunitrange.jl | 13 +++++-------- test/test_exports.jl | 2 +- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/ITensorBase.jl b/src/ITensorBase.jl index b9e3e34..983be9e 100644 --- a/src/ITensorBase.jl +++ b/src/ITensorBase.jl @@ -11,7 +11,7 @@ export AbstractNamedTensor, NamedTensor, AbstractITensor, ITensor, Index, if VERSION >= v"1.11.0-DEV.469" eval( Meta.parse( - "public @names, IndexName, from_range, mulopadd!, name, names, setname, space, unnamed, unnamedtype, decoration, emptytags, gettag, gettags, hastag, plev, settags, tags, unsettags" + "public @names, IndexName, mulopadd!, name, names, setname, space, unnamed, unnamedtype, decoration, emptytags, gettag, gettags, hastag, plev, settags, tags, unsettags" ) ) end diff --git a/src/namedunitrange.jl b/src/namedunitrange.jl index 67398d7..d1f2ec8 100644 --- a/src/namedunitrange.jl +++ b/src/namedunitrange.jl @@ -38,14 +38,11 @@ dropped. Equal to [`unnamed`](@ref) for a `NamedUnitRange`. """ space(i::NamedUnitRange) = unnamed(i) -""" - from_range(r) - -The space `r` was built from, the inverse of `TensorAlgebra.to_range`. This is what a range -is written as rather than what it stores, so a `Base.OneTo` gives back its length and, with -GradedArrays loaded, a graded space gives back its `sector => multiplicity` pairs. Used for -printing. Anything else is its own space. -""" +# The space `r` was built from, the inverse of `to_range`: what a range is written as rather +# than what it stores, so a `Base.OneTo` gives back its length and, with GradedArrays loaded, a +# graded space gives back its `sector => multiplicity` pairs. Used for printing, and overloaded +# by the extension of whichever package defines the space. Belongs beside `to_range` in +# TensorAlgebra, where every package that takes a space specification could reach it. from_range(r) = r from_range(r::Base.OneTo) = length(r) diff --git a/test/test_exports.jl b/test/test_exports.jl index 0687dfd..dd608de 100644 --- a/test/test_exports.jl +++ b/test/test_exports.jl @@ -14,7 +14,7 @@ using Test: @test, @testset :tryuniqueind, :uniqueind, :uniqueinds, :unioninds, :uniquename, ] publics = [ - :IndexName, :from_range, :mulopadd!, :name, :names, :setname, :space, :unnamed, + :IndexName, :mulopadd!, :name, :names, :setname, :space, :unnamed, :unnamedtype, :decoration, :emptytags, :gettag, :gettags, :hastag, :plev, :settags, :tags, :unsettags, From 4a4f85264afbaa59441ca2699e8f9fe1ea66848e Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 1 Oct 2026 22:02:01 -0400 Subject: [PATCH 12/16] Print a dual Index as dual of the index `dual(Index(...))` is the call that makes it, where a `dual` around the space is not a call at all once the space is a vector of pairs. Co-Authored-By: Claude Opus 5 (1M context) --- src/index.jl | 17 ++++++++++------- test/test_gradedarraysext.jl | 4 ++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/index.jl b/src/index.jl index 510e172..a541002 100644 --- a/src/index.jl +++ b/src/index.jl @@ -428,21 +428,24 @@ end # A compact context asks for the length instead, which is what a tensor's summary line uses to # stay readable with one entry per leg. function Base.show(io::IO, i::Index) + sp = space(i) + # A dual index prints as `dual` of the non-dual one, which is the call that makes it, rather + # than as a `dual` around the space, which is not a call at all once the space is a vector of + # `sector => multiplicity` pairs. + nondual = TA.isdual(sp) ? TA.dual(sp) : sp spacestr = if get(io, :compact, false) "length=$(length(i))" else - # The space the `Index` was written with rather than the range it stores, with duality - # factored outside it the way a graded range prints its own `dual`. `:typeinfo` drops the - # element-type prefix a vector of `sector => multiplicity` pairs would otherwise carry. - sp = space(i) - spec = from_range(TA.isdual(sp) ? TA.dual(sp) : sp) - specstr = sprint(show, spec; context = IOContext(io, :typeinfo => typeof(spec))) - TA.isdual(sp) ? "dual($(specstr))" : specstr + # The space the `Index` was written with rather than the range it stores. `:typeinfo` + # drops the element-type prefix a vector of pairs would otherwise carry. + spec = from_range(nondual) + sprint(show, spec; context = IOContext(io, :typeinfo => typeof(spec))) end idstr = "|id=$(shortid(uuid(i)))" tagsstr = !isempty(tags_stored(i)) ? "|$(tagsstring(tags_stored(i)))" : "" primestr = primestring(plev(i)) str = "Index($(spacestr)$(idstr)$(tagsstr))$(primestr)" + TA.isdual(sp) && (str = "dual($(str))") print(io, str) return nothing end diff --git a/test/test_gradedarraysext.jl b/test/test_gradedarraysext.jl index 5148cf2..240a9d8 100644 --- a/test/test_gradedarraysext.jl +++ b/test/test_gradedarraysext.jl @@ -188,11 +188,11 @@ end # An `Index` prints the space it was written with, so a graded one shows its # `sector => multiplicity` pairs rather than the `gradedrange(...)` call that built the range, -# with `dual` factored outside the pairs. +# and a dual one prints as `dual` of the index rather than of the pairs. @testset "GradedArraysExt Index show" begin i = Index([U1(0) => 1, U1(1) => 2]) @test sprint(show, "text/plain", i) == "Index([U1(0) => 1, U1(1) => 2]|id=$(first(string(uuid(i)), 8)))" @test sprint(show, "text/plain", dual(i)) == - "Index(dual([U1(0) => 1, U1(1) => 2])|id=$(first(string(uuid(i)), 8)))" + "dual(Index([U1(0) => 1, U1(1) => 2]|id=$(first(string(uuid(i)), 8))))" end From e90dfcaf2e6af522f8572ff0fd065c0c7a735522 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 1 Oct 2026 22:02:06 -0400 Subject: [PATCH 13/16] Tighten the prose on the symmetric tensors page Co-Authored-By: Claude Opus 5 (1M context) --- docs/src/symmetries.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/symmetries.md b/docs/src/symmetries.md index 3e9910e..6f21b25 100644 --- a/docs/src/symmetries.md +++ b/docs/src/symmetries.md @@ -20,9 +20,9 @@ k = Index([U1(0) => 1, U1(1) => 1]) a = randn(i, dual(j)) ``` -Only the symmetry-allowed blocks are stored. +A `GradedArray` only stores the symmetry-allowed blocks. -Tensors over graded indices contract, scale, and add like any others. +These tensors support contraction, multiplication by a scalar, and addition. ```@example symmetries b = randn(j, dual(k)) @@ -40,9 +40,9 @@ a + c ## Duality -`dual` is a GradedArrays function that flips the arrow an index carries, and `isdual` reports -which way it points. Indices can only contract with ones that have opposite duality, for example -the `j` Index of `b` contracts with the `dual(j)` Index of `a`. +`dual` flips the duality of an index, and `isdual` returns whether an index is dual. Indices +can only contract with ones that have opposite duality, for example the `j` Index of `b` +contracts with the `dual(j)` Index of `a`. ```@example symmetries isdual.(inds(a)) From 1ac210fe915a4e3b9b196c8a563be884a858ed37 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Thu, 1 Oct 2026 22:29:44 -0400 Subject: [PATCH 14/16] Open the README with a summary of the package Also points the TensorKit link at the repository's current location. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 5 +++++ docs/src/symmetries.md | 2 +- examples/README.jl | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a709184..225fcb4 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,11 @@ [![Code Style](https://img.shields.io/badge/code_style-ITensor-purple)](https://github.com/ITensor/ITensorFormatter.jl) [![Aqua](https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg)](https://github.com/JuliaTesting/Aqua.jl) +A next-generation rewrite of [ITensors.jl](https://github.com/ITensor/ITensors.jl), allowing +arbitrary array backends and a wider range of symmetries. Built on top of +[TensorAlgebra.jl](https://github.com/ITensor/TensorAlgebra.jl), with group symmetric tensors +powered by [GradedArrays.jl](https://github.com/ITensor/GradedArrays.jl). + ## Support diff --git a/docs/src/symmetries.md b/docs/src/symmetries.md index 6f21b25..0fef402 100644 --- a/docs/src/symmetries.md +++ b/docs/src/symmetries.md @@ -52,7 +52,7 @@ Note that indices of a `GradedArray` are partitioned into a codomain and a domai `GradedArray` stores the block diagonal matrix corresponding to the bipartitioning of the indices. When printing, by convention domain indices are implicitly dual (the format and conventions are compatible with those from -[TensorKit.jl](https://github.com/Jutho/TensorKit.jl)). For more information see the +[TensorKit.jl](https://github.com/QuantumKitHub/TensorKit.jl)). For more information see the documentation on [graded arrays](@extref GradedArrays :doc:`user_interface/graded_arrays`). ## Available symmetries diff --git a/examples/README.jl b/examples/README.jl index 96ca7c7..2828078 100644 --- a/examples/README.jl +++ b/examples/README.jl @@ -7,6 +7,11 @@ # [![Code Style](https://img.shields.io/badge/code_style-ITensor-purple)](https://github.com/ITensor/ITensorFormatter.jl) # [![Aqua](https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg)](https://github.com/JuliaTesting/Aqua.jl) +# A next-generation rewrite of [ITensors.jl](https://github.com/ITensor/ITensors.jl), allowing +# arbitrary array backends and a wider range of symmetries. Built on top of +# [TensorAlgebra.jl](https://github.com/ITensor/TensorAlgebra.jl), with group symmetric tensors +# powered by [GradedArrays.jl](https://github.com/ITensor/GradedArrays.jl). + # ## Support # # {CCQ_LOGO} From 8cae596897588839cf56f831db7d77f53d9f6b99 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Fri, 2 Oct 2026 13:00:01 -0400 Subject: [PATCH 15/16] Remove the GradedArrays 0.17 branch pins GradedArrays 0.17.0 is registered, so test/ and docs/ resolve it from the registry. Co-Authored-By: Claude Opus 5 (1M context) --- docs/Project.toml | 4 ---- test/Project.toml | 4 ---- 2 files changed, 8 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index b6da3f1..9c7552b 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -9,10 +9,6 @@ MatrixAlgebraKit = "6c742aac-3347-4629-af66-fc926824e5e4" TensorAlgebra = "68bd88dc-f39d-4e12-b2ca-f046b68fcc6a" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -[sources.GradedArrays] -rev = "mf/sector-type-overhaul" -url = "https://github.com/ITensor/GradedArrays.jl" - [sources.ITensorBase] path = ".." diff --git a/test/Project.toml b/test/Project.toml index 43d2c68..d8eefa7 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -24,10 +24,6 @@ UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" VectorInterface = "409d34a3-91d5-4945-b6ec-7529ddf182d8" WrappedUnions = "325db55a-9c6c-5b90-b1a2-ec87e7a38c44" -[sources.GradedArrays] -rev = "mf/sector-type-overhaul" -url = "https://github.com/ITensor/GradedArrays.jl" - [sources.ITensorBase] path = ".." From 30d4ddffd3cbea4ac0e67053ff651a9dd5b33fa0 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Fri, 2 Oct 2026 18:38:47 -0400 Subject: [PATCH 16/16] Release as a patch rather than a breaking bump Raising the GradedArrays compat floor does not change ITensorBase's own surface, so this follows the breaking upstream as an ordinary patch. Co-Authored-By: Claude Opus 5 (1M context) --- Project.toml | 2 +- docs/Project.toml | 2 +- examples/Project.toml | 2 +- test/Project.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Project.toml b/Project.toml index f27bc0f..0f53689 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ITensorBase" uuid = "4795dd04-0d67-49bb-8f44-b89c448a1dc7" -version = "0.16.0" +version = "0.15.4" authors = ["ITensor developers and contributors"] [workspace] diff --git a/docs/Project.toml b/docs/Project.toml index 9c7552b..99795b2 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -16,7 +16,7 @@ path = ".." Documenter = "1" DocumenterInterLinks = "1" GradedArrays = "0.17" -ITensorBase = "0.16" +ITensorBase = "0.15" 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 d639c85..c219fcf 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -6,5 +6,5 @@ MatrixAlgebraKit = "6c742aac-3347-4629-af66-fc926824e5e4" path = ".." [compat] -ITensorBase = "0.16" +ITensorBase = "0.15" MatrixAlgebraKit = "0.2, 0.3, 0.4, 0.5, 0.6" diff --git a/test/Project.toml b/test/Project.toml index d8eefa7..90741c8 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -33,7 +33,7 @@ Adapt = "4" Aqua = "0.8.9" Combinatorics = "1" GradedArrays = "0.17" -ITensorBase = "0.16" +ITensorBase = "0.15" ITensorPkgSkeleton = "0.3.42" JLArrays = "0.2, 0.3" LinearAlgebra = "1.10"