From e8ef0dd4d3cddf91151a2566e84f3ec1279f09b2 Mon Sep 17 00:00:00 2001 From: Kristoffer Carlsson Date: Wed, 11 Nov 2026 12:16:04 +0100 Subject: [PATCH 1/3] Use a comprehension in `_instantiate_operators` `_instantiate_operators` ran all of the work of the `MPOHamiltonian` constructors inside `map(collect(local_operators)) do term ... end`. Below that `map` call, type inference applies its recursion limit to every nested `map` over a vector whose element type is not concrete, such as `map(dual, S.spaces)` in `dual(::SumSpace)`. The limited results are not cached, so each function between the two `map` calls is inferred again at every call site (JuliaLang/julia#64862). A comprehension gives the same result without a `map` frame at the root. First call of `InfiniteMPOHamiltonian([P, P], (1, 2) => h, (2, 3) => h)` from that issue: | Julia | before | after | |---------------|------------------|------------------| | 1.10.13 | 10.7 s, 4.5 GiB | 9.4 s, 3.5 GiB | | 1.13.3 | 36.0 s, 16.7 GiB | 16.8 s, 7.6 GiB | | 1.14.0-alpha2 | 29.5 s, 16.4 GiB | 14.7 s, 7.9 GiB | Co-Authored-By: Claude Fable 5.2 --- docs/src/changelog.md | 3 +++ src/operators/mpohamiltonian.jl | 9 ++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/src/changelog.md b/docs/src/changelog.md index d608cfead..1213efe7b 100644 --- a/docs/src/changelog.md +++ b/docs/src/changelog.md @@ -31,6 +31,9 @@ When releasing a new version, move the "Unreleased" changes to a new version sec ### Performance +- Constructing an `MPOHamiltonian` from local operators compiles about twice as fast on + Julia 1.13 and 1.14. ([#554](https://github.com/QuantumKitHub/MPSKit.jl/pull/554)) + ## [0.14.1](https://github.com/QuantumKitHub/MPSKit.jl/compare/v0.14.0...v0.14.1) - 2026-11-02 ### Added diff --git a/src/operators/mpohamiltonian.jl b/src/operators/mpohamiltonian.jl index 4b724a4eb..c3f1c4b7d 100644 --- a/src/operators/mpohamiltonian.jl +++ b/src/operators/mpohamiltonian.jl @@ -513,9 +513,12 @@ of two proportional operators need not be related by that same scalar. """ function _instantiate_operators(lattice, local_operators) representatives = Tuple{Any, Any, Any}[] - return map(collect(local_operators)) do term - return instantiate_operator(lattice, _decompose_once!(representatives, term)) - end + # A comprehension rather than `map`: all of the work happens in here, and type inference + # limits `map` calls that are nested within another `map` (JuliaLang/julia#64862). + return [ + instantiate_operator(lattice, _decompose_once!(representatives, term)) + for term in collect(local_operators) + ] end # terms that are not supplied as an `inds => operator` pair -- e.g. a `LocalOperator` -- and From 431d58e42cba1cdad739aee0dbcae84a3d248ff5 Mon Sep 17 00:00:00 2001 From: hz-xiaxz <1806656034@qq.com> Date: Wed, 11 Nov 2026 12:21:13 -0500 Subject: [PATCH 2/3] Avoid abstract inference when constructing Hamiltonians MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Put inference barriers around the calls in `_instantiate_operators` that receive the untyped representatives, and reorder the inverse `swap!` contraction so that `τ'` is not the first `@plansor` tensor. That ordering threw `UndefVarError: τ` (QuantumKitHub/TensorKit.jl#622), and since it was an undefined global it also made the whole method infer abstractly. Co-Authored-By: Claude Opus 5.5 --- src/operators/mpo.jl | 3 ++- src/operators/mpohamiltonian.jl | 17 +++++++++++++---- test/mpo/finite.jl | 17 +++++++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/operators/mpo.jl b/src/operators/mpo.jl index e3935c756..ffee4ebb7 100644 --- a/src/operators/mpo.jl +++ b/src/operators/mpo.jl @@ -471,8 +471,9 @@ function swap!( O₁, O₂ = mpo[i], mpo[i + 1] if inv + # `τ'` must not be the first tensor here (QuantumKitHub/TensorKit.jl#622) @plansor O₂₁[-1 -2 -3; -4 -5 -6] := - τ'[-3 -6; 4 5] * O₁[-2 4; 2 1] * O₂[1 5; 3 -5] * τ[2 3; -1 -4] + O₁[-2 4; 2 1] * O₂[1 5; 3 -5] * τ[2 3; -1 -4] * τ'[-3 -6; 4 5] else @plansor O₂₁[-1 -2 -3; -4 -5 -6] := τ[-3 -6; 4 5] * O₁[-2 4; 2 1] * O₂[1 5; 3 -5] * τ'[2 3; -1 -4] diff --git a/src/operators/mpohamiltonian.jl b/src/operators/mpohamiltonian.jl index c3f1c4b7d..5fb42b4b9 100644 --- a/src/operators/mpohamiltonian.jl +++ b/src/operators/mpohamiltonian.jl @@ -515,8 +515,13 @@ function _instantiate_operators(lattice, local_operators) representatives = Tuple{Any, Any, Any}[] # A comprehension rather than `map`: all of the work happens in here, and type inference # limits `map` calls that are nested within another `map` (JuliaLang/julia#64862). + # `representatives` is untyped, so the calls below are hidden behind inference barriers: + # inferring them would only produce abstract signatures, which are costly to infer and are + # inferred again on every first call instead of being reused. return [ - instantiate_operator(lattice, _decompose_once!(representatives, term)) + Base.inferencebarrier(instantiate_operator)( + lattice, _decompose_once!(representatives, term) + ) for term in collect(local_operators) ] end @@ -538,19 +543,23 @@ function _decompose_once!(representatives, O::AbstractTensorMap) # cached so that it is not recomputed for every term norm_squared_new = real(inner(O, O)) for (O_rep, norm_squared_rep, mpo) in representatives - λ = _proportionality( + λ = Base.inferencebarrier(_proportionality)( O, O_rep; norm_squared_new, norm_squared_old = norm_squared_rep ) isnothing(λ) && continue isone(λ) && return mpo - Os = parent(mpo) - return FiniteMPO([i == lastindex(Os) ? λ * Os[i] : Os[i] for i in eachindex(Os)]) + return Base.inferencebarrier(_rescale_last)(mpo, λ) end mpo = FiniteMPO(O) push!(representatives, (O, norm_squared_new, mpo)) return mpo end +function _rescale_last(mpo, λ) + Os = parent(mpo) + return FiniteMPO([i == lastindex(Os) ? λ * Os[i] : Os[i] for i in eachindex(Os)]) +end + """ _assign_channels!(nonzero_keys, nonzero_opps, local_mpos) diff --git a/test/mpo/finite.jl b/test/mpo/finite.jl index b16d8081e..627e5c055 100644 --- a/test/mpo/finite.jl +++ b/test/mpo/finite.jl @@ -118,3 +118,20 @@ end @test convert(TensorMap, mpo) ≈ O end end + +@testset "swap" begin + Vs = ( + ℂ^2, Vect[FermionParity](0 => 1, 1 => 1), + Vect[FibonacciAnyon](:I => 1, :τ => 1), + ) + for V in Vs + O = randn(ComplexF64, V ⊗ V ← V ⊗ V) + mpo = FiniteMPO(O) + # compare the dense operators: `isapprox` on the MPOs is unreliable at its default + # tolerance (QuantumKitHub/MPSKit.jl#555) + mpo₁ = MPSKit.swap(MPSKit.swap(mpo, 1), 1; inv = true) + @test convert(TensorMap, mpo₁) ≈ O + mpo₂ = MPSKit.swap(MPSKit.swap(mpo, 1; inv = true), 1) + @test convert(TensorMap, mpo₂) ≈ O + end +end From 77b1c3d26e7a615afc23678db9118809b0372bdc Mon Sep 17 00:00:00 2001 From: hz-xiaxz <1806656034@qq.com> Date: Wed, 11 Nov 2026 12:51:51 -0500 Subject: [PATCH 3/3] Remove inference-barrier comment Co-Authored-By: Claude Opus 5.5 --- src/operators/mpohamiltonian.jl | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/operators/mpohamiltonian.jl b/src/operators/mpohamiltonian.jl index 5fb42b4b9..c3f40e129 100644 --- a/src/operators/mpohamiltonian.jl +++ b/src/operators/mpohamiltonian.jl @@ -515,9 +515,6 @@ function _instantiate_operators(lattice, local_operators) representatives = Tuple{Any, Any, Any}[] # A comprehension rather than `map`: all of the work happens in here, and type inference # limits `map` calls that are nested within another `map` (JuliaLang/julia#64862). - # `representatives` is untyped, so the calls below are hidden behind inference barriers: - # inferring them would only produce abstract signatures, which are costly to infer and are - # inferred again on every first call instead of being reused. return [ Base.inferencebarrier(instantiate_operator)( lattice, _decompose_once!(representatives, term)