From d90f15c2247443324a834d36c6c1fbeabb54220c Mon Sep 17 00:00:00 2001 From: lkdvos Date: Sun, 20 Sep 2026 13:28:40 -0400 Subject: [PATCH 1/8] Draft changelog for v0.17.2 Consolidates the Unreleased section (which already included the real entries added by #526/#532 on merge) with entries for the remaining PRs merged since v0.17.1 (#487-#535), and retitles it as 0.17.2. Co-Authored-By: Claude Sonnet 5 --- docs/src/Changelog.md | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/docs/src/Changelog.md b/docs/src/Changelog.md index fbb216427..170d4ad1d 100644 --- a/docs/src/Changelog.md +++ b/docs/src/Changelog.md @@ -18,30 +18,59 @@ When making changes to this project, please update the "Unreleased" section with When releasing a new version, move the "Unreleased" changes to a new version section with the release date. -## [Unreleased](https://github.com/QuantumKitHub/TensorKit.jl/compare/v0.17.1...HEAD) +## [Unreleased](https://github.com/QuantumKitHub/TensorKit.jl/compare/v0.17.2...HEAD) ### Added +### Changed + +### Deprecated + +### Removed + +### Fixed + +### Performance + +## [0.17.2](https://github.com/QuantumKitHub/TensorKit.jl/compare/v0.17.1...v0.17.2) - 2026-09-20 + +### Added + +- Precompilation workloads for common tensor construction, contraction, index manipulation, and factorization patterns, to reduce time-to-first-use ([#487](https://github.com/QuantumKitHub/TensorKit.jl/pull/487)) +- TimerOutputs-based timers for internal kernels, and documentation for the global cache API, to aid profiling and debugging ([#525](https://github.com/QuantumKitHub/TensorKit.jl/pull/525), [#507](https://github.com/QuantumKitHub/TensorKit.jl/pull/507)) +- Enzyme forward/reverse rules for `flip`, `insertleftunit`, `insertrightunit`, and `removeunit`, and for planar operations ([#488](https://github.com/QuantumKitHub/TensorKit.jl/pull/488), [#489](https://github.com/QuantumKitHub/TensorKit.jl/pull/489), [#474](https://github.com/QuantumKitHub/TensorKit.jl/pull/474)) - `planarcontract!` now supports an arbitrary (cyclic) output permutation `pAB`, in the same way as the non-planar `tensorcontract!`, along with the new helper `TensorKit.planar_contract_indices`, which `@planar` uses to emit canonical index tuples. ([#531](https://github.com/QuantumKitHub/TensorKit.jl/pull/531)) ### Changed + - For sector types with `GenericUnit` such that colorings are not unique, `GradedSpace`, `ProductSpace` and `HomSpace` now check for this compatibility. In particular, this prevents the construction of `TensorMap`s with incompatible colorings, which previously either errored or produced empty tensors inconsistently. ([#515](https://github.com/QuantumKitHub/TensorKit.jl/pull/515)) - `TensorOperations.tensorcontract_structure` now requires the index tuples `pA` and `pB` to be planar (cyclic) partitions for sector types with `GenericUnit()`; hand-written planar kernels should canonicalize their index tuples with `TensorKit.planar_contract_indices` before allocating. ([#531](https://github.com/QuantumKitHub/TensorKit.jl/pull/531)) - +- The allocator is now threaded through `transpose!` in planar operations and through additional `@planar` call sites ([#502](https://github.com/QuantumKitHub/TensorKit.jl/pull/502), [#529](https://github.com/QuantumKitHub/TensorKit.jl/pull/529)) - Index manipulations use a single kernel operating on subblocks addressed by position: `StridedSubblocks` (sector-independent views into the flat data of a `TensorMap`) or `SubblockIterator` (any `AbstractTensorMap`, through `subblock`). The `TreeTransformer`s store the mapping between subblock positions and recoupling coefficients and are cached for every tensor type; conjugated and adjoint operands are handled through this mechanism instead of through `AdjointTensorMap` wrappers (internal) ([#516](https://github.com/QuantumKitHub/TensorKit.jl/issues/516)) ### Deprecated + - The type alias `ZNSpace{N}` is deprecated in favour of `Vect[ZNIrrep{N}]` or `Rep[ℤ{N}]`: a type alias cannot compute the storage type from `N`, so the two only agree for small `N`. ([#511](https://github.com/QuantumKitHub/TensorKit.jl/pull/511)) ### Removed ### Fixed +- Fixed bugs in the `GradedSpace` constructor and in `similar` for `SectorVector` ([#498](https://github.com/QuantumKitHub/TensorKit.jl/pull/498)) +- `braid` for anyonic sectors no longer silently accepts duplicate `levels` ([#503](https://github.com/QuantumKitHub/TensorKit.jl/pull/503)) +- Fixed backend and allocator insertion in the `@planar` macro ([#505](https://github.com/QuantumKitHub/TensorKit.jl/pull/505)) +- `permute` and `transpose` ChainRules rules now support all keyword arguments ([#513](https://github.com/QuantumKitHub/TensorKit.jl/pull/513)) +- Added a `hash` method for `FusionTreeBlock` to fix a caching bug affecting `permute` on `AdjointTensorMap`s ([#518](https://github.com/QuantumKitHub/TensorKit.jl/pull/518)) +- `@planar` and hand-written planar kernels now canonicalize index partitions before allocating the contraction destination, fixing incorrect/erroring allocation for non-planar `pAB`, for `@planar` contractions whose leg-wise partitions are non-planar by themselves, and for contracting operands with different `spacetype`s (e.g. a `BlockTensorMap` with a `TensorMap`) ([#532](https://github.com/QuantumKitHub/TensorKit.jl/pull/532)) - `braid!`, `permute!` and `transpose!` with a `BraidingTensor` source now use the cached fusion tree transformers ([#516](https://github.com/QuantumKitHub/TensorKit.jl/issues/516)) +- Made the ChainRules extension's rules consistent with the corresponding Mooncake and Enzyme rules ([#535](https://github.com/QuantumKitHub/TensorKit.jl/pull/535)) ### Performance -- `GradedSpace` operations (`dim`, `flip`, `⊕`, `⊖`, `fuse`, `infimum`, `supremum`, truncation) are now specialised on the storage type of the degeneracy dimensions, and tuple storage is used only for sector types with at most `TensorKit._NTUPLE_STORAGE_THRESHOLD` sectors so that sector types with many sectors no longer burden the compiler. ([#511](https://github.com/QuantumKitHub/TensorKit.jl/pull/511)) +- `GradedSpace` operations (`dim`, `flip`, `⊕`, `⊖`, `fuse`, `infimum`, `supremum`, truncation) are now specialised on the storage type of the degeneracy dimensions, and tuple storage is used only for sector types with at most `TensorKit._NTUPLE_STORAGE_THRESHOLD` sectors so that sector types with many sectors no longer burden the compiler. ([#511](https://github.com/QuantumKitHub/TensorKit.jl/pull/511)) +- Fixed a performance regression for non-abelian index manipulations, and reduced memory usage of `twist` ([#521](https://github.com/QuantumKitHub/TensorKit.jl/pull/521), [#501](https://github.com/QuantumKitHub/TensorKit.jl/pull/501)) +- More error strings marked as lazy to reduce runtime overhead in non-error paths ([#492](https://github.com/QuantumKitHub/TensorKit.jl/pull/492)) +- Use vector multiplication instead of an explicit loop for truncation error computation ([#443](https://github.com/QuantumKitHub/TensorKit.jl/pull/443)) - In-place `permute!`, `braid!` and `transpose!` with `AdjointTensorMap` sources or destinations, as well as `@tensor` expressions with `conj`, now use the same cached and sector-independent kernel as plain `TensorMap`s; other tensor types (e.g. `DiagonalTensorMap`) also use the cached fusion tree transformers, and `subblocks` iterates without repeating any space-level lookup ([#516](https://github.com/QuantumKitHub/TensorKit.jl/issues/516), [#519](https://github.com/QuantumKitHub/TensorKit.jl/pull/519), [#520](https://github.com/QuantumKitHub/TensorKit.jl/pull/520)) ## [0.17.1](https://github.com/QuantumKitHub/TensorKit.jl/compare/v0.17.0...v0.17.1) - 2026-07-13 From 8655f55cbad4d286d2031cc80f66d6f29a314d56 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Sun, 20 Sep 2026 13:28:41 -0400 Subject: [PATCH 2/8] Bump version to v0.17.2 Co-Authored-By: Claude Sonnet 5 --- CITATION.cff | 4 ++-- Project.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 7fa169338..6c4cf6f73 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -8,9 +8,9 @@ authors: given-names: "Jutho" orcid: "https://orcid.org/0000-0002-0858-291X" title: "TensorKit.jl" -version: "0.17.1" +version: "0.17.2" doi: "10.5281/zenodo.8421339" -date-released: "2026-07-13" +date-released: "2026-09-20" url: "https://github.com/QuantumKitHub/TensorKit.jl" preferred-citation: type: article diff --git a/Project.toml b/Project.toml index 8a93b4358..83093ea5b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "TensorKit" uuid = "07d1fe3e-3e46-537d-9eac-e9e13d0d4cec" -version = "0.17.1" +version = "0.17.2" authors = ["Jutho Haegeman, Lukas Devos"] [workspace] From e52ce0eb39afb2115435fa2a79d817e30551ebed Mon Sep 17 00:00:00 2001 From: lkdvos Date: Sun, 20 Sep 2026 14:04:59 -0400 Subject: [PATCH 3/8] Fix confirmed small bugs from the pre-release audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - isunitspace: require dim(V) == 1 for GenericUnit sectors (#537) - GradedSpace ⊕/supremum: check unit homogeneity of the result (#538) - isconj(::ComplexSpace): return isdual(V) instead of always true (#539) - multi_associator: return a vector, not a scalar, on early-exit for GenericFusion (#540) - split(f, 0): use leftunit(f.coupled) instead of indexing an empty uncoupled tuple (#541) - repartition: return a Pair in the identity branch, matching every other branch (#542) - Mooncake scalar_pullback: accumulate into the tangent instead of overwriting it (#543) - rand/randn/randexp/randisometry(rng, T, space): fix one(domain) typo (#544) - pinv(::DiagonalTensorMap): fix inverted atol/rtol defaulting and empty-tensor throw (#545) - t1 / t2: promote to a float scalartype, matching t1 \ t2 (#546) Co-Authored-By: Claude Sonnet 5 --- ext/TensorKitMooncakeExt/tensoroperations.jl | 2 +- src/fusiontrees/basic_manipulations.jl | 8 +++++--- src/fusiontrees/duality_manipulations.jl | 2 +- src/spaces/complexspace.jl | 2 +- src/spaces/gradedspace.jl | 10 ++++++++-- src/spaces/vectorspaces.jl | 2 +- src/tensors/diagonal.jl | 9 +++------ src/tensors/linalg.jl | 2 +- src/tensors/tensor.jl | 2 +- 9 files changed, 22 insertions(+), 17 deletions(-) diff --git a/ext/TensorKitMooncakeExt/tensoroperations.jl b/ext/TensorKitMooncakeExt/tensoroperations.jl index 342dedef3..c1074bd45 100644 --- a/ext/TensorKitMooncakeExt/tensoroperations.jl +++ b/ext/TensorKitMooncakeExt/tensoroperations.jl @@ -197,7 +197,7 @@ function Mooncake.rrule!!(::CoDual{typeof(TensorKit.scalar)}, t_dt::CoDual{<:Abs t, dt = arrayify(t_dt) val = scalar(t) function scalar_pullback(Δval) - first(blocks(dt))[2][1] = Δval + first(blocks(dt))[2][1] += Δval return NoRData(), NoRData() end return Mooncake.zero_fcodual(val), scalar_pullback diff --git a/src/fusiontrees/basic_manipulations.jl b/src/fusiontrees/basic_manipulations.jl index d3e988a4a..a3a39acaa 100644 --- a/src/fusiontrees/basic_manipulations.jl +++ b/src/fusiontrees/basic_manipulations.jl @@ -36,7 +36,7 @@ true uncoupled₁ = ntuple(n -> f.uncoupled[n], M) isdual₁ = ntuple(n -> f.isdual[n], M) - coupled₁ = M == 0 ? leftunit(f.uncoupled[1]) : innerlines_extended[M] + coupled₁ = M == 0 ? leftunit(f.coupled) : innerlines_extended[M] innerlines₁ = ntuple(n -> f.innerlines[n], max(0, M - 2)) vertices₁ = ntuple(n -> f.vertices[n], max(0, M - 1)) @@ -138,8 +138,10 @@ function multi_associator(long::FusionTree{I, N}, short) where {I, N} length(short) == N - 1 || throw(DimensionMismatch("second fusion tree must have one less uncoupled leg")) uncoupled = long.uncoupled - (uncoupled[2:end] == short.uncoupled && long.isdual[2:end] == short.isdual) || - return zero(sectorscalartype(typeof(long.coupled))) + if !(uncoupled[2:end] == short.uncoupled && long.isdual[2:end] == short.isdual) + return FusionStyle(I) isa MultiplicityFreeFusion ? + zero(sectorscalartype(I)) : zeros(sectorscalartype(I), 1) + end if FusionStyle(I) isa MultiplicityFreeFusion coeff = one(sectorscalartype(I)) diff --git a/src/fusiontrees/duality_manipulations.jl b/src/fusiontrees/duality_manipulations.jl index 6faff78a0..c595afdc2 100644 --- a/src/fusiontrees/duality_manipulations.jl +++ b/src/fusiontrees/duality_manipulations.jl @@ -486,7 +486,7 @@ function _repartition_body(N) return src => one(T) else U = copyto!(zeros(T, length(src), length(src)), LinearAlgebra.I) - return src, U + return src => U end end else diff --git a/src/spaces/complexspace.jl b/src/spaces/complexspace.jl index cb58279e9..eea9dbf7a 100644 --- a/src/spaces/complexspace.jl +++ b/src/spaces/complexspace.jl @@ -48,7 +48,7 @@ Base.axes(V::ComplexSpace, ::Trivial = Trivial()) = Base.OneTo(dim(V)) dual(V::ComplexSpace) = ComplexSpace(dim(V), !isdual(V)) Base.conj(V::ComplexSpace) = dual(V) isdual(V::ComplexSpace) = V.dual -isconj(V::ComplexSpace) = true +isconj(V::ComplexSpace) = isdual(V) flip(V::ComplexSpace) = dual(V) unitspace(::Type{ComplexSpace}) = ComplexSpace(1) diff --git a/src/spaces/gradedspace.jl b/src/spaces/gradedspace.jl index 67169a764..e9211d6cc 100644 --- a/src/spaces/gradedspace.jl +++ b/src/spaces/gradedspace.jl @@ -162,13 +162,16 @@ zerospace(S::Type{<:GradedSpace}) = S() function ⊕(V₁::DictGradedSpace{I}, V₂::DictGradedSpace{I}) where {I <: Sector} dual1 = isdual(V₁) dual1 == isdual(V₂) || throw(SpaceMismatch("Direct sum of a vector space and a dual space does not exist")) - return typeof(V₁)(mergewith(+, V₁.dims, V₂.dims), dual1) + newdims = mergewith(+, V₁.dims, V₂.dims) + _check_unit_homogeneity(I, keys(newdims)) + return typeof(V₁)(newdims, dual1) end function ⊕(V₁::TupleGradedSpace{I}, V₂::TupleGradedSpace{I}) where {I <: Sector} dual1 = isdual(V₁) dual1 == isdual(V₂) || throw(SpaceMismatch("Direct sum of a vector space and a dual space does not exist")) newdims = map(+, V₁.dims, V₂.dims) + _check_unit_homogeneity(I, (values(I)[n] for n in eachindex(newdims) if !iszero(newdims[n]))) return typeof(V₁)(newdims, dual1) end @noinline _throw_not_subspace(V, W) = throw(SpaceMismatch(lazy"$(W) is not a subspace of $(V)")) @@ -248,12 +251,15 @@ function supremum(V₁::TupleGradedSpace{I}, V₂::TupleGradedSpace{I}) where {I Visdual = isdual(V₁) Visdual == isdual(V₂) || throw(SpaceMismatch("Supremum of space and dual space does not exist")) newdims = map(max, V₁.dims, V₂.dims) + _check_unit_homogeneity(I, (values(I)[n] for n in eachindex(newdims) if !iszero(newdims[n]))) return typeof(V₁)(newdims, Visdual) end function supremum(V₁::DictGradedSpace{I}, V₂::DictGradedSpace{I}) where {I <: Sector} Visdual = isdual(V₁) Visdual == isdual(V₂) || throw(SpaceMismatch("Supremum of space and dual space does not exist")) - return typeof(V₁)(mergewith(max, V₁.dims, V₂.dims), Visdual) + newdims = mergewith(max, V₁.dims, V₂.dims) + _check_unit_homogeneity(I, keys(newdims)) + return typeof(V₁)(newdims, Visdual) end hassector(V::GradedSpace{I}, s::I) where {I <: Sector} = dim(V, s) != 0 diff --git a/src/spaces/vectorspaces.jl b/src/spaces/vectorspaces.jl index 7ddce0289..7fb4b00d8 100644 --- a/src/spaces/vectorspaces.jl +++ b/src/spaces/vectorspaces.jl @@ -220,7 +220,7 @@ function isunitspace(V::ElementarySpace) return if isa(UnitStyle(I), SimpleUnit) isisomorphic(V, unitspace(V)) else - (dim(V) == 0 || !all(isunit, sectors(V))) && return false + (dim(V) == 1 && all(isunit, sectors(V))) || return false return true end end diff --git a/src/tensors/diagonal.jl b/src/tensors/diagonal.jl index 5fc6682e3..41fd2925f 100644 --- a/src/tensors/diagonal.jl +++ b/src/tensors/diagonal.jl @@ -366,12 +366,9 @@ end function LinearAlgebra.pinv(d::DiagonalTensorMap; kwargs...) T = eltype(d.data) atol = get(kwargs, :atol, zero(real(T))) - if iszero(atol) - rtol = get(kwargs, :rtol, zero(real(T))) - else - rtol = sqrt(eps(real(float(one(T))))) * length(d.data) - end - pdata = let tol = max(atol, rtol * maximum(abs, d.data)) + default_rtol = sqrt(eps(real(float(one(T))))) * length(d.data) + rtol = get(kwargs, :rtol, atol > 0 ? zero(real(T)) : default_rtol) + pdata = let tol = max(atol, rtol * maximum(abs, d.data; init = zero(real(T)))) map(x -> abs(x) < tol ? zero(x) : pinv(x), d.data) end return DiagonalTensorMap(pdata, d.domain) diff --git a/src/tensors/linalg.jl b/src/tensors/linalg.jl index 657c4649a..50a695654 100644 --- a/src/tensors/linalg.jl +++ b/src/tensors/linalg.jl @@ -417,7 +417,7 @@ end function Base.:(/)(t1::AbstractTensorMap, t2::AbstractTensorMap) domain(t1) == domain(t2) || throw(SpaceMismatch("non-matching domains in t1 / t2")) - T = promote_type(scalartype(t1), scalartype(t2)) + T = float(promote_type(scalartype(t1), scalartype(t2))) t = similar(t1, T, codomain(t1) ← codomain(t2)) for (c, b) in blocks(t) copy!(b, block(t1, c) / block(t2, c)) diff --git a/src/tensors/tensor.jl b/src/tensors/tensor.jl index 0c4697469..1aeb0070c 100644 --- a/src/tensors/tensor.jl +++ b/src/tensors/tensor.jl @@ -393,7 +393,7 @@ for randf in (:rand, :randn, :randexp, :randisometry) function $randfun( rng::Random.AbstractRNG, ::Type{TorA}, codomain::TensorSpace ) where {TorA} - return $randfun(rng, TorA, codomain ← one(domain)) + return $randfun(rng, TorA, codomain ← one(codomain)) end # filling in default eltype From f6e57717b1b8b87b8cd288edab56e8bec87e9c44 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Sun, 20 Sep 2026 14:05:21 -0400 Subject: [PATCH 4/8] Add changelog entry for the audit bugfixes Co-Authored-By: Claude Sonnet 5 --- docs/src/Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/Changelog.md b/docs/src/Changelog.md index 170d4ad1d..e3baac971 100644 --- a/docs/src/Changelog.md +++ b/docs/src/Changelog.md @@ -64,6 +64,7 @@ When releasing a new version, move the "Unreleased" changes to a new version sec - `@planar` and hand-written planar kernels now canonicalize index partitions before allocating the contraction destination, fixing incorrect/erroring allocation for non-planar `pAB`, for `@planar` contractions whose leg-wise partitions are non-planar by themselves, and for contracting operands with different `spacetype`s (e.g. a `BlockTensorMap` with a `TensorMap`) ([#532](https://github.com/QuantumKitHub/TensorKit.jl/pull/532)) - `braid!`, `permute!` and `transpose!` with a `BraidingTensor` source now use the cached fusion tree transformers ([#516](https://github.com/QuantumKitHub/TensorKit.jl/issues/516)) - Made the ChainRules extension's rules consistent with the corresponding Mooncake and Enzyme rules ([#535](https://github.com/QuantumKitHub/TensorKit.jl/pull/535)) +- `isunitspace`, `GradedSpace` `⊕`/`supremum`, `isconj(::ComplexSpace)`, `multi_associator`, `split`, `repartition`, the Mooncake `scalar` rule, `rand`/`randn`/`randexp`/`randisometry` with an explicit `rng`, `pinv(::DiagonalTensorMap)`, and `t1 / t2`: fixed various small bugs found during a pre-release audit ([#537](https://github.com/QuantumKitHub/TensorKit.jl/issues/537), [#538](https://github.com/QuantumKitHub/TensorKit.jl/issues/538), [#539](https://github.com/QuantumKitHub/TensorKit.jl/issues/539), [#540](https://github.com/QuantumKitHub/TensorKit.jl/issues/540), [#541](https://github.com/QuantumKitHub/TensorKit.jl/issues/541), [#542](https://github.com/QuantumKitHub/TensorKit.jl/issues/542), [#543](https://github.com/QuantumKitHub/TensorKit.jl/issues/543), [#544](https://github.com/QuantumKitHub/TensorKit.jl/issues/544), [#545](https://github.com/QuantumKitHub/TensorKit.jl/issues/545), [#546](https://github.com/QuantumKitHub/TensorKit.jl/issues/546)) ### Performance From fe696565040d50f69e16a9fb9d595e1b36293afd Mon Sep 17 00:00:00 2001 From: lkdvos Date: Sun, 20 Sep 2026 14:33:49 -0400 Subject: [PATCH 5/8] Address fable review findings on the audit bugfixes - split(f, 0): also guard innerlines_extended construction, which still indexed the empty uncoupled tuple for a 0-leg tree - pinv(::DiagonalTensorMap): use eps (not sqrt(eps)) for the default rtol, matching _default_rtol's convention and dense LinearAlgebra.pinv Co-Authored-By: Claude Sonnet 5 --- src/fusiontrees/basic_manipulations.jl | 2 +- src/tensors/diagonal.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fusiontrees/basic_manipulations.jl b/src/fusiontrees/basic_manipulations.jl index a3a39acaa..c220591c3 100644 --- a/src/fusiontrees/basic_manipulations.jl +++ b/src/fusiontrees/basic_manipulations.jl @@ -31,7 +31,7 @@ true 0 <= M <= N || throw(ArgumentError("M should be between 0 and N = $N")) - innerlines_extended = (f.uncoupled[1], f.innerlines..., f.coupled) + innerlines_extended = N == 0 ? () : (f.uncoupled[1], f.innerlines..., f.coupled) vertices_extended = (1, f.vertices...) uncoupled₁ = ntuple(n -> f.uncoupled[n], M) diff --git a/src/tensors/diagonal.jl b/src/tensors/diagonal.jl index 41fd2925f..e3bd1df51 100644 --- a/src/tensors/diagonal.jl +++ b/src/tensors/diagonal.jl @@ -366,7 +366,7 @@ end function LinearAlgebra.pinv(d::DiagonalTensorMap; kwargs...) T = eltype(d.data) atol = get(kwargs, :atol, zero(real(T))) - default_rtol = sqrt(eps(real(float(one(T))))) * length(d.data) + default_rtol = eps(real(float(one(T)))) * length(d.data) rtol = get(kwargs, :rtol, atol > 0 ? zero(real(T)) : default_rtol) pdata = let tol = max(atol, rtol * maximum(abs, d.data; init = zero(real(T)))) map(x -> abs(x) < tol ? zero(x) : pinv(x), d.data) From 9c1f77df106eafa55ee116770f7d912d0f4e0bcc Mon Sep 17 00:00:00 2001 From: lkdvos Date: Sun, 20 Sep 2026 15:34:34 -0400 Subject: [PATCH 6/8] Address tuicr review comments on the audit bugfixes - pinv(::DiagonalTensorMap): reuse _default_rtol instead of duplicating its formula - Add regression tests for split(f, 0) on a genuine 0-leg tree and for multi_associator's early-exit branch on a GenericFusion sector Co-Authored-By: Claude Sonnet 5 --- src/tensors/diagonal.jl | 3 +-- test/symmetries/singletree.jl | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/tensors/diagonal.jl b/src/tensors/diagonal.jl index e3bd1df51..be0290b24 100644 --- a/src/tensors/diagonal.jl +++ b/src/tensors/diagonal.jl @@ -366,8 +366,7 @@ end function LinearAlgebra.pinv(d::DiagonalTensorMap; kwargs...) T = eltype(d.data) atol = get(kwargs, :atol, zero(real(T))) - default_rtol = eps(real(float(one(T)))) * length(d.data) - rtol = get(kwargs, :rtol, atol > 0 ? zero(real(T)) : default_rtol) + rtol = get(kwargs, :rtol, atol > 0 ? zero(real(T)) : _default_rtol(d)) pdata = let tol = max(atol, rtol * maximum(abs, d.data; init = zero(real(T)))) map(x -> abs(x) < tol ? zero(x) : pinv(x), d.data) end diff --git a/test/symmetries/singletree.jl b/test/symmetries/singletree.jl index 7ca92c919..f99db1357 100644 --- a/test/symmetries/singletree.jl +++ b/test/symmetries/singletree.jl @@ -98,6 +98,11 @@ using TensorKitSectors f′ = @constinferred TK.join(f₁, f₂) @test f′ == f end + + f0 = FusionTree{I}((), TK.leftunit(one(I)), (), (), ()) + f0₁, f0₂ = @constinferred TK.split(f0, 0) + @test f0₁ == f0 + @test TK.join(f0₁, f0₂) == f0 end @testset "Fusion tree: multi_Fmove" begin @@ -131,6 +136,15 @@ using TensorKitSectors end end @test norm(values(d)) < 1.0e-12 + + short_mismatch = FusionTree{I}( + f′s[1].uncoupled, f′s[1].coupled, map(!, f′s[1].isdual), + f′s[1].innerlines, f′s[1].vertices + ) + if short_mismatch.isdual != f.isdual[2:end] + coeff_mismatch = @constinferred TK.multi_associator(f, short_mismatch) + @test all(iszero, coeff_mismatch) + end end end From 17994e0bd907e97084952902a0cfba6ee35873b5 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Sun, 20 Sep 2026 21:14:01 -0400 Subject: [PATCH 7/8] fix planar issues after MPSKit test rerun --- src/planar/planaroperations.jl | 16 +++++++++++++--- test/symmetries/singletree.jl | 2 +- test/tensors/planar.jl | 20 ++++++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/planar/planaroperations.jl b/src/planar/planaroperations.jl index 5866f7ef9..7c24e1399 100644 --- a/src/planar/planaroperations.jl +++ b/src/planar/planaroperations.jl @@ -268,10 +268,20 @@ function planar_contract_indices( oindA′, cindA′ = _planar_rotate(indA, oindA, cindA) cindB′, oindB′ = _planar_rotate(indB, cindB, oindB) - # if all indices are contracted, fix the residual rotation using the other tensor - if isempty(oindA′) && !isempty(cindA) + # an empty arc leaves a residual rotation, which has to be fixed elsewhere + if isempty(cindA) && isempty(cindB) + # nothing is contracted: both rotations are fixed by the cycle of the destination + NA = length(oindA) + indAB = (pAB[1]..., reverse(pAB[2])...) + posA′, posB′ = _planar_rotate( + indAB, ntuple(identity, Val(NA)), ntuple(n -> NA + n, Val(length(oindB))) + ) + oindA′ = TupleTools.getindices(oindA, posA′) + oindB′ = map(n -> oindB[n - NA], posB′) + elseif isempty(oindA′) + # everything is contracted: the rotation is fixed by the other tensor cindA′ = _rotate_to(cindA′, cindA[something(findfirst(==(first(cindB′)), cindB))]) - elseif isempty(oindB′) && !isempty(cindB) + elseif isempty(oindB′) cindB′ = _rotate_to(cindB′, cindB[something(findfirst(==(first(cindA′)), cindA))]) end TupleTools.sort(tuple.(cindA′, cindB′)) == TupleTools.sort(tuple.(cindA, cindB)) || diff --git a/test/symmetries/singletree.jl b/test/symmetries/singletree.jl index f99db1357..76de24b48 100644 --- a/test/symmetries/singletree.jl +++ b/test/symmetries/singletree.jl @@ -99,7 +99,7 @@ using TensorKitSectors @test f′ == f end - f0 = FusionTree{I}((), TK.leftunit(one(I)), (), (), ()) + f0 = FusionTree{I}((), TK.leftunit(coupled), (), (), ()) f0₁, f0₂ = @constinferred TK.split(f0, 0) @test f0₁ == f0 @test TK.join(f0₁, f0₂) == f0 diff --git a/test/tensors/planar.jl b/test/tensors/planar.jl index 72a933b21..dcf7502e8 100644 --- a/test/tensors/planar.jl +++ b/test/tensors/planar.jl @@ -108,6 +108,14 @@ end pAB3 = ((1, 2), (3, 4, 5)) E′ = force_planar(randn(ℂ^2 ⊗ (ℂ^2)' ← ℂ^5 ⊗ (ℂ^2)' ⊗ ℂ^4)) @test_throws ArgumentError planarcontract!(E′, A′, pA, B′, pB, pAB3, true, true) + + # without contracted indices the cyclic reordering is fixed by `pAB` alone + F′ = force_planar(randn(ℂ^2 ⊗ ℂ^3 ← ℂ^4)) + G′ = force_planar(randn(ℂ^5 ← ℂ^6 ⊗ ℂ^7)) + H′ = force_planar(randn(ℂ^2 ⊗ ℂ^3 ⊗ ℂ^5 ← ℂ^4 ⊗ ℂ^6 ⊗ ℂ^7)) + pF, pG = ((1, 2, 3), ()), ((), (1, 2, 3)) + pFG = ((1, 2, 4), (3, 5, 6)) + @test planarcontract!(H′, F′, pF, G′, pG, pFG, true, false) ≈ F′ ⊗ G′ end @testset "planar_contract_indices" begin @@ -154,6 +162,18 @@ end @test pAB2′ == ((1, 2, 3), (4, 5)) @test last(planar_contract_indices(WA, pA2, WB, pB2, ((2, 1), (3, 4, 5)))) == ((2, 3), (1, 4, 5)) + + # no indices contracted: the rotations are only fixed by the destination + WAo, WBo = ℂ^2 ⊗ ℂ^3 ← ℂ^4, ℂ^5 ← ℂ^6 + pAo, pBo = ((1, 2, 3), ()), ((), (1, 2)) + pAo′, pBo′, pABo′ = @constinferred planar_contract_indices( + WAo, pAo, WBo, pBo, ((1, 2, 4), (3, 5)) + ) + @test (pAo′, pBo′) == (((3, 1, 2), ()), ((), (2, 1))) + @test transpose(WAo, pAo′) isa TensorKit.HomSpace + @test transpose(WBo, pBo′) isa TensorKit.HomSpace + @test TensorOperations.tensorcontract(WAo, pAo′, false, WBo, pBo′, false, pABo′) == + (ℂ^2 ⊗ ℂ^3 ⊗ ℂ^5 ← ℂ^4 ⊗ ℂ^6) end end From 5e22e7fbcf89c81ec6d9d9e7fd407bae6a10d5e2 Mon Sep 17 00:00:00 2001 From: lkdvos Date: Mon, 21 Sep 2026 10:13:05 -0400 Subject: [PATCH 8/8] harden Mooncake scalar pullback --- ext/TensorKitMooncakeExt/tensoroperations.jl | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/ext/TensorKitMooncakeExt/tensoroperations.jl b/ext/TensorKitMooncakeExt/tensoroperations.jl index c1074bd45..2ba9d58ce 100644 --- a/ext/TensorKitMooncakeExt/tensoroperations.jl +++ b/ext/TensorKitMooncakeExt/tensoroperations.jl @@ -195,10 +195,23 @@ end ) function Mooncake.rrule!!(::CoDual{typeof(TensorKit.scalar)}, t_dt::CoDual{<:AbstractTensorMap}) t, dt = arrayify(t_dt) - val = scalar(t) + + # need to identify the correct unit for contributing the pullback + Bs = collect(blocks(t)) + inds = findall(!iszero ∘ last, Bs) + c, val = if isempty(inds) + # for multifusion sectors, several unit blocks can be structurally present and + # simultaneously zero, by convention picking the first here. + # TODO: should we throw an error in this case? + first(first(Bs)), zero(scalartype(t)) + else + c, b = Bs[only(inds)] + c, only(b) + end function scalar_pullback(Δval) - first(blocks(dt))[2][1] += Δval + block(dt, c) .+= Δval return NoRData(), NoRData() end + return Mooncake.zero_fcodual(val), scalar_pullback end