From d4a5edf458d2cf721470f1534893f84b1838f5ca Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 11 Aug 2026 15:34:04 +0200 Subject: [PATCH 01/18] treat multiline consistently as 1d sequence of T, but keep size/axes referring to the 2d shape --- src/states/multilinemps.jl | 3 --- src/utility/multiline.jl | 14 +++++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/states/multilinemps.jl b/src/states/multilinemps.jl index 524280ffc..f5c1c85ac 100644 --- a/src/states/multilinemps.jl +++ b/src/states/multilinemps.jl @@ -101,11 +101,8 @@ TensorKit.normalize!(a::MultilineMPS) = (normalize!.(parent(a)); return a) Base.convert(::Type{MultilineMPS}, st::InfiniteMPS) = Multiline([st]) Base.convert(::Type{InfiniteMPS}, st::MultilineMPS) = only(st) -Base.eltype(t::MultilineMPS) = eltype(t[1]) Base.copy!(ψ::MultilineMPS, ϕ::MultilineMPS) = (copy!.(parent(ψ), parent(ϕ)); ψ) -Base.isfinite(::Type{<:MultilineMPS}) = false - for f_space in (:physicalspace, :left_virtualspace, :right_virtualspace) @eval $f_space(t::MultilineMPS, i::Int, j::Int) = $f_space(t[i], j) @eval $f_space(t::MultilineMPS, I::CartesianIndex{2}) = $f_space(t, Tuple(I)...) diff --git a/src/utility/multiline.jl b/src/utility/multiline.jl index ed4107cf2..b2e932753 100644 --- a/src/utility/multiline.jl +++ b/src/utility/multiline.jl @@ -25,14 +25,17 @@ Multiline(data::AbstractVector{T}) where {T} = Multiline{T}(data) # ----------------------- Base.parent(m::Multiline) = m.data Base.size(m::Multiline) = (length(parent(m)), length(parent(m)[1])) -Base.size(m::Multiline, i::Int) = i == 1 ? length(parent(m)) : i == 2 ? length(parent(m)[1]) : error() -Base.length(m::Multiline) = prod(size(m)) -function Base.axes(m::Multiline, i::Int) - return i == 1 ? axes(parent(m), 1) : - i == 2 ? axes(parent(m)[1], 1) : throw(ArgumentError("Invalid index $i")) +function Base.size(m::Multiline, i::Int) # acts like abstract array + return i == 1 ? length(parent(m)) : i == 2 ? length(parent(m)[1]) : 1 +end +Base.length(m::Multiline) = length(parent(m)) +function Base.axes(m::Multiline, d::Int) + return d <= 2 ? axes(m)[d] : Base.OneTo(1) # matches size end Base.eachindex(m::Multiline) = CartesianIndices(size(m)) Base.isfinite(m::Multiline) = isfinite(typeof(m)) +Base.isfinite(::Type{Multiline{T}}) where {T} = isfinite(T) +Base.eltype(::Type{Multiline{T}}) where {T} = T eachsite(m::Multiline) = eachsite(first(parent(m))) @@ -102,6 +105,7 @@ end VectorInterface.add!!(x::Multiline, y::Multiline, α::Number, β::Number) = add!(x, y, α, β) +# is it intentional that a nontrivial multilinemps of normalised rows never has norm 1? function VectorInterface.inner(x::Multiline, y::Multiline) T = VectorInterface.promote_inner(x, y) init = zero(T) From 2dd1db0823717a4088107e507f08717b9e1766d8 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 11 Aug 2026 15:40:47 +0200 Subject: [PATCH 02/18] fix multiplying with multilinempo --- src/operators/multilinempo.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/operators/multilinempo.jl b/src/operators/multilinempo.jl index 7432cf91a..4517bf5bf 100644 --- a/src/operators/multilinempo.jl +++ b/src/operators/multilinempo.jl @@ -39,12 +39,17 @@ Base.convert(::Type{InfiniteMPO}, t::MultilineMPO{<:InfiniteMPO}) = only(t) function Base.:*(mpo::MultilineMPO, st::MultilineMPS) size(mpo) == size(st) || throw(ArgumentError("dimension mismatch")) - return Multiline(map(*, zip(mpo, st))) + # row i of the operator maps row i of the state onto row i + 1 + return Multiline(circshift(map(*, parent(mpo), parent(st)), 1)) end +#TODO: docstring mentioning that (mpo1*mpo2)*multilinemps != mpo1*(mpo2*multilinemps) in general +# because of the row-shifting behavior of MultilineMPO function Base.:*(mpo1::MultilineMPO, mpo2::MultilineMPO) size(mpo1) == size(mpo2) || throw(ArgumentError("dimension mismatch")) - return Multiline(map(*, zip(mpo1, mpo2))) + # `mpo2[i]` maps row i onto row i + 1, where `mpo1[i + 1]` picks up: the resulting + # operator spans two rows of the lattice and maps row i onto row i + 2 + return Multiline(map(*, circshift(parent(mpo1), -1), parent(mpo2))) end for f_space in (:physicalspace, :left_virtualspace, :right_virtualspace) From 0fca974f7acf53f72f8f5541ef6769be6b0d84bd Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 11 Aug 2026 15:42:00 +0200 Subject: [PATCH 03/18] more parent calls in svdcut --- src/algorithms/changebonds/svdcut.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/algorithms/changebonds/svdcut.jl b/src/algorithms/changebonds/svdcut.jl index 295e48557..fc7cbb4ed 100644 --- a/src/algorithms/changebonds/svdcut.jl +++ b/src/algorithms/changebonds/svdcut.jl @@ -86,14 +86,14 @@ function changebonds!(mpo::FiniteMPO, alg::SvdCut) end # TODO: this assumes the MPO is infinite, and does weird things for finite MPOs. -function changebonds(ψ::InfiniteMPO, alg::SvdCut) - return convert(InfiniteMPO, changebonds(convert(InfiniteMPS, ψ), alg)) +function changebonds(mpo::InfiniteMPO, alg::SvdCut) + return convert(InfiniteMPO, changebonds(convert(InfiniteMPS, mpo), alg)) end -function changebonds(ψ::MultilineMPO, alg::SvdCut) - return convert(MultilineMPO, changebonds(convert(MultilineMPS, ψ), alg)) +function changebonds(mpo::MultilineMPO, alg::SvdCut) + return Multiline(map(Base.Fix2(changebonds, alg), parent(mpo))) end function changebonds(ψ::MultilineMPS, alg::SvdCut) - return Multiline(map(x -> changebonds(x, alg), ψ.data)) + return Multiline(map(Base.Fix2(changebonds, alg), parent(ψ))) end function changebonds(ψ::InfiniteMPS, alg::SvdCut) copied = copy.(ψ.AL) From f48a18c898d6538624fe54cc6db09ce7b4fe702c Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 11 Aug 2026 15:48:12 +0200 Subject: [PATCH 04/18] more moms and dads --- src/states/orthoview.jl | 8 ++++---- src/utility/multiline.jl | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/states/orthoview.jl b/src/states/orthoview.jl index 4bbddf2a1..aec9e7118 100644 --- a/src/states/orthoview.jl +++ b/src/states/orthoview.jl @@ -278,10 +278,10 @@ Base.axes(psi::CView{<:AbstractFiniteMPS}) = map(n -> 0:(n - 1), size(psi)) Base.size(psi::CView{<:Multiline{<:InfiniteMPS}}) = size(psi.parent) function Base.size(psi::CView{<:Multiline{<:AbstractFiniteMPS}}) - return (length(psi.parent.data), length(first(psi.parent.data)) + 1) + return (length(parent(psi.parent)), length(first(parent(psi.parent))) + 1) end function Base.axes(psi::CView{<:Multiline{<:AbstractFiniteMPS}}) - return (Base.OneTo(length(psi.parent.data)), 0:length(first(psi.parent.data))) + return (Base.OneTo(length(parent(psi.parent))), 0:length(first(parent(psi.parent)))) end #the checkbounds for multiline objects needs to be changed, as the first index is periodic @@ -291,10 +291,10 @@ function Base.checkbounds( psi::Union{ACView{<:Multiline}, ALView{<:Multiline}, ARView{<:Multiline}, CView{<:Multiline}}, a, b ) - return if first(psi.parent.data) isa InfiniteMPS + return if first(parent(psi.parent)) isa InfiniteMPS true else - checkbounds(Bool, CView(first(psi.parent.data)), b) + checkbounds(Bool, CView(first(parent(psi.parent))), b) end end diff --git a/src/utility/multiline.jl b/src/utility/multiline.jl index b2e932753..0608f877f 100644 --- a/src/utility/multiline.jl +++ b/src/utility/multiline.jl @@ -56,7 +56,7 @@ Base.reverse(A::Multiline) = Multiline(reverse(parent(A))) Base.only(A::Multiline) = only(parent(A)) function Base.repeat(A::Multiline, rows::Int, cols::Int) - inner = map(Base.Fix2(repeat, cols), A.data) + inner = map(Base.Fix2(repeat, cols), parent(A)) outer = repeat(inner, rows) return Multiline(outer) end From ce6464fbdf0b3a5651c603ad19d36096490c4349 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 11 Aug 2026 15:50:35 +0200 Subject: [PATCH 05/18] some grassmann changes due to 1d consistency --- src/algorithms/grassmann.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/algorithms/grassmann.jl b/src/algorithms/grassmann.jl index 1f55dca38..5248c3940 100644 --- a/src/algorithms/grassmann.jl +++ b/src/algorithms/grassmann.jl @@ -216,12 +216,12 @@ function fg( backend::AbstractBackend = DefaultBackend(), scheduler::Scheduler = MPSKit.Defaults.scheduler[], ) - @assert length(state) == 1 "not implemented" + @assert size(state, 1) == 1 "not implemented" @timeit timeroutput "envs (parallel)" recalculate!(envs, state, operator, state; timeroutput) f = @timeit timeroutput "expval" expectation_value(state, operator, envs) isapprox(imag(f), 0; atol = eps(abs(f))^(3 / 4)) || @warn "MPO might not be Hermitian: $f" - A = Core.Compiler.return_type(Grassmann.project, Tuple{eltype(state), eltype(state)}) + A = Core.Compiler.return_type(Grassmann.project, Tuple{site_type(state), site_type(state)}) gs = Matrix{A}(undef, size(state)) allocator = default_allocator(state, scheduler) @timeit timeroutput "gradient" tforeach(eachindex(state); scheduler) do i From fed22a315922847475edd40d82ba5cb81b10f96b Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 11 Aug 2026 15:52:55 +0200 Subject: [PATCH 06/18] more mothers and fathers --- src/algorithms/changebonds/randexpand.jl | 2 +- src/operators/multilinempo.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/algorithms/changebonds/randexpand.jl b/src/algorithms/changebonds/randexpand.jl index 484deda5c..a1ba7d621 100644 --- a/src/algorithms/changebonds/randexpand.jl +++ b/src/algorithms/changebonds/randexpand.jl @@ -56,7 +56,7 @@ function changebonds!(ψ::InfiniteMPS, alg::RandExpand) end function changebonds!(ψ::MultilineMPS, alg::RandExpand) - foreach(Base.Fix2(changebonds!, alg), ψ.data) + foreach(Base.Fix2(changebonds!, alg), parent(ψ)) return ψ end diff --git a/src/operators/multilinempo.jl b/src/operators/multilinempo.jl index 4517bf5bf..92777feff 100644 --- a/src/operators/multilinempo.jl +++ b/src/operators/multilinempo.jl @@ -26,7 +26,7 @@ MultilineMPO(mpos::AbstractVector{<:AbstractMPO}) = Multiline(mpos) MultilineMPO(t::MPOTensor) = MultilineMPO(PeriodicMatrix(fill(t, 1, 1))) # allow indexing with two indices -Base.getindex(t::MultilineMPO, ::Colon, j::Int) = Base.getindex.(t.data, j) +Base.getindex(t::MultilineMPO, ::Colon, j::Int) = Base.getindex.(parent(t), j) Base.getindex(t::MultilineMPO, i::Int, j) = Base.getindex(t[i], j) Base.getindex(t::MultilineMPO, I::CartesianIndex{2}) = t[I.I...] From 1c43ce8b0232286def9c5730e8cdfcb122a2c763 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 11 Aug 2026 16:00:28 +0200 Subject: [PATCH 07/18] prevent finitempo support at constructor level --- src/operators/multilinempo.jl | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/operators/multilinempo.jl b/src/operators/multilinempo.jl index 92777feff..666316c1c 100644 --- a/src/operators/multilinempo.jl +++ b/src/operators/multilinempo.jl @@ -16,13 +16,10 @@ Type that represents multiple lines of `MPO` objects. """ const MultilineMPO = Multiline{<:AbstractMPO} -function MultilineMPO(Os::AbstractMatrix) - return MultilineMPO(map(FiniteMPO, eachrow(Os))) -end function MultilineMPO(Os::PeriodicMatrix) return MultilineMPO(map(InfiniteMPO, eachrow(Os))) end -MultilineMPO(mpos::AbstractVector{<:AbstractMPO}) = Multiline(mpos) +MultilineMPO(mpos::AbstractVector{<:InfiniteMPO}) = Multiline(mpos) MultilineMPO(t::MPOTensor) = MultilineMPO(PeriodicMatrix(fill(t, 1, 1))) # allow indexing with two indices @@ -31,10 +28,9 @@ Base.getindex(t::MultilineMPO, i::Int, j) = Base.getindex(t[i], j) Base.getindex(t::MultilineMPO, I::CartesianIndex{2}) = t[I.I...] # converters -Base.convert(::Type{MultilineMPO}, t::AbstractMPO) = Multiline([t]) +Base.convert(::Type{MultilineMPO}, t::InfiniteMPO) = Multiline([t]) Base.convert(::Type{DenseMPO}, t::MultilineMPO{<:DenseMPO}) = only(t) Base.convert(::Type{SparseMPO}, t::MultilineMPO{<:SparseMPO}) = only(t) -Base.convert(::Type{FiniteMPO}, t::MultilineMPO{<:FiniteMPO}) = only(t) Base.convert(::Type{InfiniteMPO}, t::MultilineMPO{<:InfiniteMPO}) = only(t) function Base.:*(mpo::MultilineMPO, st::MultilineMPS) From 43ad6351602a381bab6ff5e299f95c4392c6eb77 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 11 Aug 2026 16:02:01 +0200 Subject: [PATCH 08/18] prevent constructing multiline of hamiltonians --- src/algorithms/expval.jl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/algorithms/expval.jl b/src/algorithms/expval.jl index 191d2f93c..549040548 100644 --- a/src/algorithms/expval.jl +++ b/src/algorithms/expval.jl @@ -193,10 +193,6 @@ function expectation_value( return contract_mpo_expval(ψ.AC[i, j], GL, O[i, j], GR, ψ.AC[i + 1, j]) end end -function expectation_value(ψ::MultilineMPS, mpo::MultilineMPO, envs...) - # TODO: fix environments - return prod(x -> expectation_value(x...), zip(parent(ψ), parent(mpo))) -end # fallback function expectation_value(ψ::AbstractMPS, mpo::AbstractMPO, envs...) return dot(ψ, mpo, ψ) / dot(ψ, ψ) From 3b3b013ea6ebf623e3c178f17096e0b048c5df8d Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 11 Aug 2026 16:02:33 +0200 Subject: [PATCH 09/18] remove multiline finitemps code --- src/states/orthoview.jl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/states/orthoview.jl b/src/states/orthoview.jl index aec9e7118..82c44f536 100644 --- a/src/states/orthoview.jl +++ b/src/states/orthoview.jl @@ -277,12 +277,6 @@ Base.size(psi::CView{<:AbstractFiniteMPS}) = (length(psi.parent) + 1,) Base.axes(psi::CView{<:AbstractFiniteMPS}) = map(n -> 0:(n - 1), size(psi)) Base.size(psi::CView{<:Multiline{<:InfiniteMPS}}) = size(psi.parent) -function Base.size(psi::CView{<:Multiline{<:AbstractFiniteMPS}}) - return (length(parent(psi.parent)), length(first(parent(psi.parent))) + 1) -end -function Base.axes(psi::CView{<:Multiline{<:AbstractFiniteMPS}}) - return (Base.OneTo(length(parent(psi.parent))), 0:length(first(parent(psi.parent)))) -end #the checkbounds for multiline objects needs to be changed, as the first index is periodic #however if it is a Multiline(Infinitemps), then the second index is also periodic! From b77b31fb62def165a8bc10f6b93ab9ae3a039713 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 11 Aug 2026 16:51:57 +0200 Subject: [PATCH 10/18] multiline tests with exact results --- test/algorithms/statmech.jl | 13 ++++++++ test/operators/multilinempo.jl | 56 ++++++++++++++++++++++++++++++++++ test/setup/testsetup.jl | 20 ++++++++++++ test/states/multilinemps.jl | 1 + 4 files changed, 90 insertions(+) create mode 100644 test/operators/multilinempo.jl diff --git a/test/algorithms/statmech.jl b/test/algorithms/statmech.jl index 8a736c02b..0b434b72b 100644 --- a/test/algorithms/statmech.jl +++ b/test/algorithms/statmech.jl @@ -123,4 +123,17 @@ using TensorKit: ℙ e = expectation_value(ψ, (O_mpo, 1 => E)) @test e ≈ e_th atol = 1.0e-2 end + + @testset "MultilineMPS expectation_value (multi-row)" begin + # reuses the exact permutation-MPO fixed point: O[i]*ψ[i] == ψ[i+1] exactly + ψ = MultilineMPS([product_mps(1), product_mps(2), product_mps(3)]) + O = MultilineMPO([perm_mpo([2, 3, 1]), perm_mpo([1, 3, 2]), perm_mpo([3, 2, 1])]) + @test expectation_value(ψ, O) ≈ 1 atol = 1.0e-10 + + # `Multiline([H, H])` of Hamiltonian bypasses the MultilineMPO(...) construction guard + # expectation_value must still reject it + H = transverse_field_ising() + ψ2 = MultilineMPS([InfiniteMPS(ℂ^2, ℂ^4), InfiniteMPS(ℂ^2, ℂ^4)]) + @test_throws MethodError expectation_value(ψ2, Multiline([H, H])) + end end diff --git a/test/operators/multilinempo.jl b/test/operators/multilinempo.jl new file mode 100644 index 000000000..f6556a180 --- /dev/null +++ b/test/operators/multilinempo.jl @@ -0,0 +1,56 @@ +println(" +---------------------------- +| MultilineMPO tests | +---------------------------- +") + +using .TestSetup +using Test, TestExtras +using MPSKit +using TensorKit + +d = 3 +P = ℂ^d +V = ℂ^1 + +# ψ[i] = |i⟩ (3 rows, 1 column), O[i]|i⟩ = |i + 1⟩ exactly, so ψ is the exact λ = 1 fixed point of O +# this properly tests row-shift convention: row i of a MultilineMPO maps row i of the state onto row i + 1 +ψ = MultilineMPS([product_mps(1), product_mps(2), product_mps(3)]) +O = MultilineMPO( + [ + perm_mpo([2, 3, 1]), # row 1: |1⟩ -> |2⟩ + perm_mpo([1, 3, 2]), # row 2: |2⟩ -> |3⟩ + perm_mpo([3, 2, 1]), # row 3: |3⟩ -> |1⟩ + ] +) + +@testset "MultilineMPO * MultilineMPS" begin + for i in 1:3 + @test abs(dot(ψ[i + 1], O[i] * ψ[i])) ≈ 1 atol = 1.0e-10 + @test abs(dot(ψ[i], O[i] * ψ[i])) ≈ 0 atol = 1.0e-10 + end + + Oψ = O * ψ + @test length(parent(Oψ)) == 3 # not rows * cols lines + @test size(Oψ) == size(ψ) == (3, 1) + for i in 1:3 + @test abs(dot(ψ[i], Oψ[i])) ≈ 1 atol = 1.0e-10 # O * ψ reproduces ψ + end +end + +@testset "MultilineMPO * MultilineMPO" begin + # O[i+1] * O[i] maps row i onto row i+2, O[i] * O[i] does not + for i in 1:3 + @test abs(dot(ψ[i + 2], (O[i + 1] * O[i]) * ψ[i])) ≈ 1 atol = 1.0e-10 + end + @test !isfinite(O) + @test !isfinite(typeof(O)) + + OO = O * O + @test !isfinite(OO) + @test length(parent(OO)) == 3 + @test size(OO) == (3, 1) + for i in 1:3 + @test abs(dot(ψ[i + 2], OO[i] * ψ[i])) ≈ 1 atol = 1.0e-10 + end +end diff --git a/test/setup/testsetup.jl b/test/setup/testsetup.jl index 5aacebd2b..d9f7761a9 100644 --- a/test/setup/testsetup.jl +++ b/test/setup/testsetup.jl @@ -24,6 +24,7 @@ export transverse_field_ising, heisenberg_XXX, bilinear_biquadratic_model, XY_mo export classical_ising_tensors, classical_ising, sixvertex export bad_initial_state export SCHEDULERS, with_scheduler +export perm_mpo, product_mps # using TensorOperations @@ -291,4 +292,23 @@ function bad_initial_state(H, L; T = ComplexF64, n_states = 20, n_fixed = 3) end end +# functions for multiline tests +# bond-dim-1 permutation MPO with O|p⟩ = |π[p]⟩ +function perm_mpo(pi) + d = 3 + P, V = ℂ^d, ℂ^1 + t = zeros(ComplexF64, 1, d, d, 1) + for pin in 1:d + t[1, pi[pin], pin, 1] = 1.0 + end + return InfiniteMPO([TensorMap(t, V ⊗ P ← P ⊗ V)]) +end +function product_mps(k) + d = 3 + P, V = ℂ^d, ℂ^1 + a = zeros(ComplexF64, 1, d, 1) + a[1, k, 1] = 1.0 + return InfiniteMPS([TensorMap(a, V ⊗ P ← V)]) +end + end diff --git a/test/states/multilinemps.jl b/test/states/multilinemps.jl index cf5456f6f..c02e4eb4d 100644 --- a/test/states/multilinemps.jl +++ b/test/states/multilinemps.jl @@ -27,6 +27,7 @@ using TensorKit: ℙ @test TensorKit.sectortype(ψ) == sectortype(D) @test !isfinite(typeof(ψ)) + @test !isfinite(ψ) @test physicalspace(ψ) == fill(d, 2, 2) @test all(x -> x ≾ D, left_virtualspace(ψ)) From 20bc99daf59a4c81527f2cab50347eb459d4f808 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 11 Aug 2026 17:00:55 +0200 Subject: [PATCH 11/18] clarifying docstrings --- src/algorithms/expval.jl | 7 +++++++ src/operators/multilinempo.jl | 8 ++++++-- src/states/multilinemps.jl | 5 +++++ src/utility/multiline.jl | 15 ++++++++++++++- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/algorithms/expval.jl b/src/algorithms/expval.jl index 549040548..0f80d5262 100644 --- a/src/algorithms/expval.jl +++ b/src/algorithms/expval.jl @@ -32,6 +32,13 @@ the operator is a `AbstractTensorMap` that acts on the physical space of a singl return value is the total over one unit cell; divide by `length(ψ)` to obtain a per-site value. +!!! note "MultilineMPS/MultilineMPO" + For a `MultilineMPS`/`MultilineMPO{<:InfiniteMPO}` pair, `O` is applied with the usual + row-shift convention (row `i` of `O` pairs `ψ[i]` against `ψ[i + 1]`). + The returned value is the fixed-point contraction, not a normalized `⟨ψ|O|ψ⟩ / ⟨ψ|ψ⟩` in the sense of + the single-line methods. + `MultilineMPO` lines of `InfiniteMPOHamiltonian` (or any non-`InfiniteMPO` line type) are not supported. + # Examples ```jldoctest diff --git a/src/operators/multilinempo.jl b/src/operators/multilinempo.jl index 666316c1c..c4d7df8f9 100644 --- a/src/operators/multilinempo.jl +++ b/src/operators/multilinempo.jl @@ -5,10 +5,14 @@ Type that represents multiple lines of `MPO` objects. +Only `InfiniteMPO` lines are supported currently. +Hamiltonians and finite MPOs are rejected by the constructors. + # Constructors - MultilineMPO(mpos::AbstractVector{<:Union{SparseMPO, DenseMPO}}) - MultilineMPO(Os::AbstractMatrix{<:MPOTensor}) + MultilineMPO(mpos::AbstractVector{<:InfiniteMPO}) + MultilineMPO(Os::PeriodicMatrix{<:MPOTensor}) + MultilineMPO(t::MPOTensor) # See also diff --git a/src/states/multilinemps.jl b/src/states/multilinemps.jl index f5c1c85ac..02ee181a7 100644 --- a/src/states/multilinemps.jl +++ b/src/states/multilinemps.jl @@ -27,6 +27,11 @@ Type that represents multiple lines of [`InfiniteMPS`](@ref) objects. - `AC`: center-gauged MPS tensors - `C`: gauge (bond) tensors +Only `InfiniteMPS` lines are supported. +Note that `length`, `eltype` and iteration refer to the lines (so e.g. `length(ψ) == nrows`), +while `size` refers to the `(nrows, ncols)` lattice shape. +See [`Multiline`](@ref) for details. + # See also [`Multiline`](@ref) diff --git a/src/utility/multiline.jl b/src/utility/multiline.jl index 0608f877f..4ce730bc8 100644 --- a/src/utility/multiline.jl +++ b/src/utility/multiline.jl @@ -2,7 +2,20 @@ $(TYPEDEF) Object that represents multiple lines of objects of type `T`. Typically used to represent -multiple lines of `InfiniteMPS` (`MultilineMPS`) or MPO (`Multiline{<:AbstractMPO}`). +multiple lines of `InfiniteMPS` (`MultilineMPS`) or `InfiniteMPO` (`MultilineMPO`). + +`Multiline` plays two different, orthogonal roles at once, and its Base overloads are split +accordingly: + +- As a sequence of lines, matching what is actually stored: `length`, `eltype`, `iterate` + and `m[i]` (a single integer index) all refer to the `T`-typed lines themselves, i.e. + `length(m) == nrows` and `m[i]::T`. +- As a lattice, describing the 2D shape spanned by the lines together: `size(m)` is + `(nrows, ncols)`, and `axes`/`eachindex` follow `size`. + +These two views disagree on purpose (`length(m) != prod(size(m))`). +Code that wants to work line-by-line should use `m[i]`/`parent(m)`, +while code that wants the lattice shape should use `size`. # Fields From 69303552913ed47fc900feea4f5e9503bc43cca9 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 11 Aug 2026 17:31:56 +0200 Subject: [PATCH 12/18] add some more info on multiline in the docs --- docs/src/man/operators.md | 19 +++++++++++++++++++ docs/src/man/states.md | 30 ++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/docs/src/man/operators.md b/docs/src/man/operators.md index 05525add2..d38b771e1 100644 --- a/docs/src/man/operators.md +++ b/docs/src/man/operators.md @@ -274,3 +274,22 @@ a collection (direct sum) of spaces, one for each row/column. ```@example operators left_virtualspace(H_ising, 1), right_virtualspace(H_ising, 1), physicalspace(H_ising, 1) ``` + +## MultilineMPO + +Much like a [`MultilineMPS`](@ref) is a repeating set of `InfiniteMPS` lines, a [`MultilineMPO`](@ref) is a repeating set of `InfiniteMPO` lines. +This is typically the row-to-row or column-to-column transfer matrix of a 2D classical partition function, or a boundary MPO in the context of PEPS. +See the `MultilineMPS` section of the [states](@ref um_states) page for the row-shift convention relating a `MultilineMPO` to the `MultilineMPS` it acts on: row `i` of the operator maps row `i` of the state onto row `i + 1`. + +```@example operators +mpo_multi = MultilineMPO([mpo, mpo]) +``` + +Only `InfiniteMPO` lines are supported; neither Hamiltonians nor finite MPOs are allowed currently. +This is enforced by the constructors rather than by the type itself, so `MultilineMPO([H, H])` for a Hamiltonian `H` throws a `MethodError` right at the construction site, instead of producing an object that only fails once used. + +!!! note "`*` between `MultilineMPO`s does not compose the way you might expect" + Because of the `+1` row shift, multiplying two `MultilineMPO`s produces an operator that spans two rows of the lattice: `(O1 * O2)` maps row `i` onto row `i + 2`. + As a consequence, `(O1 * O2) * ψ != O1 * (O2 * ψ)` in general, for any definition of the product. + The two sides are genuinely different operations (one fused two-row step, versus two sequential one-row steps). + Treat `O1 * O2` as a "fused double row", not as an operator that can be composed further. diff --git a/docs/src/man/states.md b/docs/src/man/states.md index 168b2117c..a59d6436a 100644 --- a/docs/src/man/states.md +++ b/docs/src/man/states.md @@ -170,5 +170,35 @@ col = 2 al = state.AL[row, col]; ``` +### The row-shift convention + +The row direction and the column direction of a `MultilineMPS` play different roles. +Within a row, a `MultilineMPS` behaves exactly like the `InfiniteMPS` it repeats: columns are periodic, and `state.AL[row, col]`, `state.AR[row, col]`, `state.C[row, col]` and `state.AC[row, col]` behave exactly as they would for `state[row]::InfiniteMPS`. + +The row direction is the direction along which a transfer matrix, represented as a [`MultilineMPO`](@ref), is applied. +By convention, row `i` of a `MultilineMPO` maps row `i` of the state onto row `i + 1`, so that + +```julia +(O * ψ)[i] == O[i - 1] * ψ[i - 1] +``` + +i.e. applying `O` shifts every row up by one. +This convention is not just a property of `*`: it is baked into `environments`, `expectation_value`, and the derivative machinery as well, all of which pair `ψ[row + 1]` against `O[row]`. +It is also why `O * ψ` requires `size(O, 1) == size(ψ, 1)`, rather than the row counts of operator and state being independent. + +### What is currently supported + +Only lines that are themselves infinite are supported: a `MultilineMPS` is always built out of +[`InfiniteMPS`](@ref) lines, never [`FiniteMPS`](@ref) or [`WindowMPS`](@ref) ones. +This matches the intended use case of rows/columns in an infinite 2D network. +The constructors reject finite lines. + +### Subtleties + +- **`size` vs. iteration:** `size(state)` returns `(nrows, ncols)`, describing the lattice shape. +Iterating over a `MultilineMPS` (or indexing it with a single integer, `state[i]`) instead yields the individual `InfiniteMPS` *lines*, so `length(state) == nrows`, not `nrows * ncols`. +Code that wants to operate line-by-line should use `state[i]`/`parent(state)`, while code that wants the lattice shape should use `size`. +- **Norms and inner products:** `dot`/`norm` sum the contribution of every row, so a `MultilineMPS` whose individual rows are each normalized to 1 does *not* itself have norm 1: `norm(state) == sqrt(nrows)` for `nrows` identical normalized rows. + These objects are also used extensively in the context of [PEPSKit.jl](https://github.com/QuantumKitHub/PEPSKit.jl). From 741c14c8357653ea445008bf327e6f61df9706b1 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 11 Aug 2026 17:38:18 +0200 Subject: [PATCH 13/18] spacetype, sectortype and storagetype of multiline --- src/utility/multiline.jl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/utility/multiline.jl b/src/utility/multiline.jl index 4ce730bc8..e50e40157 100644 --- a/src/utility/multiline.jl +++ b/src/utility/multiline.jl @@ -118,7 +118,7 @@ end VectorInterface.add!!(x::Multiline, y::Multiline, α::Number, β::Number) = add!(x, y, α, β) -# is it intentional that a nontrivial multilinemps of normalised rows never has norm 1? +# FIXME? is it intentional that a nontrivial multilinemps of normalised rows never has norm 1? function VectorInterface.inner(x::Multiline, y::Multiline) T = VectorInterface.promote_inner(x, y) init = zero(T) @@ -134,6 +134,7 @@ site_type(::Type{Multiline{S}}) where {S} = site_type(S) bond_type(::Type{Multiline{S}}) where {S} = bond_type(S) site_type(st::Multiline) = site_type(typeof(st)) bond_type(st::Multiline) = bond_type(typeof(st)) -TensorKit.sectortype(::Type{Multiline{T}}) where {T} = sectortype(T) -TensorKit.spacetype(::Type{Multiline{T}}) where {T} = spacetype(T) -TensorKit.storagetype(::Type{Multiline{T}}) where {T} = storagetype(T) +for ftype in (:spacetype, :sectortype, :storagetype) + @eval TensorKit.$ftype(::Type{Multiline{T}}) where {T} = $ftype(T) + @eval TensorKit.$ftype(m::Multiline) = $ftype(typeof(m)) +end From 9e6e21524f5e6b72041619a025583e8de91525c8 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 11 Aug 2026 18:21:11 +0200 Subject: [PATCH 14/18] custom show for multilinemps/mpo --- src/utility/show.jl | 51 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/utility/show.jl b/src/utility/show.jl index 2709b7d43..5228c41db 100644 --- a/src/utility/show.jl +++ b/src/utility/show.jl @@ -10,6 +10,23 @@ for T in (:FiniteMPS, :InfiniteMPS, :FiniteMPO, :InfiniteMPO, :FiniteMPOHamilton end end +function Base.summary(io::IO, m::MultilineMPS) + R, C = size(m) + D = maximum(dim, left_virtualspace(m)) + E = scalartype(m) + S = TensorKit.type_repr(spacetype(m)) + print(io, R, "×", C, " MultilineMPS(", E, ", ", S, ") with maximal dimension ", D) + return nothing +end +function Base.summary(io::IO, m::MultilineMPO) + R, C = size(m) + D = maximum(dim, left_virtualspace(m)) + E = scalartype(m) + S = TensorKit.type_repr(spacetype(m)) + print(io, R, "×", C, " MultilineMPO(", E, ", ", S, ") with maximal dimension ", D) + return nothing +end + function Base.show(io::IO, ::MIME"text/plain", ψ::FiniteMPS) summary(io, ψ) get(io, :compact, false)::Bool && return nothing @@ -164,6 +181,40 @@ function Base.show(io::IO, ::MIME"text/plain", mpo::AbstractMPO) return nothing end +function Base.show(io::IO, ::MIME"text/plain", m::Union{MultilineMPS, MultilineMPO}) + summary(io, m) + get(io, :compact, false)::Bool && return nothing + println(io, ":") + + R = size(m, 1) + limit = get(io, :limit, true)::Bool + # rows are typically few so the row budget itself is small and fixed + # rather than computed from `displaysize` + half_rows = (limit && R > 4) ? 2 : R + shown = R <= 2 * half_rows ? (1:R) : [1:half_rows; (R - half_rows + 1):R] + + for (k, i) in enumerate(shown) + println(io, "row ", i, ":") + show(io, MIME"text/plain"(), m[i]) # dispatches to the line's own show for correct connectors + println(io) + if k < length(shown) + next = shown[k + 1] + if next == i + 1 + if m isa MultilineMPO + println(io, " ↓ (row ", i, " maps onto row ", next, ")") + else + println(io, " ⋮") + end + else + println(io, " ⋮\n ⋮") + end + end + end + m isa MultilineMPO && println(io, " ↓ (row ", R, " maps onto row 1)") + + return nothing +end + function Base.summary(io::IO, envs::Union{FiniteEnvironments, InfiniteEnvironments}) print(io, length(envs.GLs), "-site ") Base.showarg(io, envs, true) From 39cd0ab6dbcca7914cfacc481542a0cec69cdbb0 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 11 Aug 2026 18:22:04 +0200 Subject: [PATCH 15/18] remove todo --- src/operators/multilinempo.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/operators/multilinempo.jl b/src/operators/multilinempo.jl index c4d7df8f9..9936ebf62 100644 --- a/src/operators/multilinempo.jl +++ b/src/operators/multilinempo.jl @@ -43,8 +43,6 @@ function Base.:*(mpo::MultilineMPO, st::MultilineMPS) return Multiline(circshift(map(*, parent(mpo), parent(st)), 1)) end -#TODO: docstring mentioning that (mpo1*mpo2)*multilinemps != mpo1*(mpo2*multilinemps) in general -# because of the row-shifting behavior of MultilineMPO function Base.:*(mpo1::MultilineMPO, mpo2::MultilineMPO) size(mpo1) == size(mpo2) || throw(ArgumentError("dimension mismatch")) # `mpo2[i]` maps row i onto row i + 1, where `mpo1[i + 1]` picks up: the resulting From f9eb600daf318ee0431feff3631ae3d077423dc7 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Tue, 11 Aug 2026 20:08:16 +0200 Subject: [PATCH 16/18] add to changelog --- docs/src/changelog.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/src/changelog.md b/docs/src/changelog.md index 1788564bd..79c3c4d12 100644 --- a/docs/src/changelog.md +++ b/docs/src/changelog.md @@ -38,6 +38,7 @@ When releasing a new version, move the "Unreleased" changes to a new version sec by `MPSKit.default_allocator`, instead of leaving them to the garbage collector (two-site DMRG: -64% allocations, -57% GC time, -23% wall time). Disable with `MPSKit.Defaults.set_buffering!(false)`. ([#467](https://github.com/QuantumKitHub/MPSKit.jl/pull/467)) +- Custom `show`/`summary` for `MultilineMPS`/`MultilineMPO`. Each row is now rendered via each row's own display, and row shifting is shown explicitly for `MultilineMPO`. ### Changed @@ -70,11 +71,23 @@ When releasing a new version, move the "Unreleased" changes to a new version sec - `correlator` now throws an `ArgumentError` when the sites are not ordered as `i < j`. Previously such a call only logged an `@error` and then continued into a contraction that is not the requested correlator. ([#489](https://github.com/QuantumKitHub/MPSKit.jl/pull/489)) +- `Multiline` (and therefore `MultilineMPS`/`MultilineMPO`) now consistently treats + `length`/`eltype`/`iterate`/`m[i]` as referring to the individual lines it stores + (`length(m) == nrows`), while `size`/`axes`/`eachindex` refer to the `(nrows, ncols)` lattice + shape. +- `MultilineMPO` construction is now restricted to `InfiniteMPO` lines. Constructing one from + Hamiltonians or finite MPOs now throws a `MethodError` at the construction site instead of producing an object that fails + later. The `AbstractMatrix` constructor that silently built finite-line `MultilineMPO`s was + removed. ### Deprecated ### Removed +- `expectation_value(::MultilineMPS, ::MultilineMPO, envs...)` fallback method, which silently + computed a meaningless value (`prod` instead of `sum`, no row shift, `envs` ignored) for any + `MultilineMPO` line type not covered by the guarded method. Most notably this prevents a fallback for `InfiniteMPOHamiltonian`, a legal but never-meaningful `Multiline` line type. + ### Fixed - `isfinite(::WindowMPOHamiltonian)` was undefined. ([#489](https://github.com/QuantumKitHub/MPSKit.jl/pull/489)) @@ -96,6 +109,19 @@ When releasing a new version, move the "Unreleased" changes to a new version sec and the right virtual leg of `mpo[end]` and contracted the two — which at length 1 is the *same* tensor, so it returned `O * O` on twice the physical space instead of `O`. ([#484](https://github.com/QuantumKitHub/MPSKit.jl/pull/484)) +- `MultilineMPO * MultilineMPS` and `MultilineMPO * MultilineMPO` (`*`) never worked at all + (`MethodError`, from `map(*, zip(...))` calling `*` on a 2-tuple). Fixing the immediate error + still left a shape bug (`O * ψ` produced an `nrows * ncols`-line object with `#undef` entries + past `nrows`) and a row-shift bug (`O * ψ` had fidelity 0 with the expected state on every + row). `*` now correctly applies the convention that row `i` of a `MultilineMPO` maps row `i` + of the state onto row `i + 1`. +- `isfinite(::MultilineMPO)` threw (`isfinite(typeof(m))` had no matching type-level method for + `Multiline`). +- `changebonds(::MultilineMPO, ::SvdCut)` threw (`convert(MultilineMPS, ::MultilineMPO)` has no + method). +- `axes(m::Multiline, i)` threw for `i > 2`, but now returns `Base.OneTo(1)`, matching Base's own + out-of-range convention (already the case for `size(m, i)`). +- `spacetype`/`sectortype`/`storagetype` on a `Multiline` instance were undefined. Only the type-level methods existed. ### Performance From 9a17e40014cb9a1d2a8a2b78a70d25ea617b3db1 Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 12 Aug 2026 10:09:00 +0200 Subject: [PATCH 17/18] import stuff --- src/algorithms/grassmann.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/algorithms/grassmann.jl b/src/algorithms/grassmann.jl index 5248c3940..694cc13eb 100644 --- a/src/algorithms/grassmann.jl +++ b/src/algorithms/grassmann.jl @@ -11,7 +11,7 @@ The module exports nothing, and all references to it should be qualified, e.g. module GrassmannMPS using ..MPSKit -using ..MPSKit: AbstractMPSEnvironments, InfiniteEnvironments, MultilineEnvironments, +using ..MPSKit: AbstractMPSEnvironments, InfiniteEnvironments, MultilineEnvironments, site_type, AC_projection, recalculate!, TimerOutput, DISABLED_TIMER, @timeit, default_allocator using TensorOperations: AbstractBackend, DefaultBackend using TensorKit From 379c63472690637652e602c083a937d947fa0a2f Mon Sep 17 00:00:00 2001 From: Boris De Vos Date: Wed, 12 Aug 2026 10:19:05 +0200 Subject: [PATCH 18/18] account for multiline envs correctly in expval --- src/algorithms/expval.jl | 9 +++++++-- src/environments/multiline_envs.jl | 4 ++++ test/algorithms/statmech.jl | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/algorithms/expval.jl b/src/algorithms/expval.jl index 0f80d5262..9cbd616c3 100644 --- a/src/algorithms/expval.jl +++ b/src/algorithms/expval.jl @@ -187,8 +187,13 @@ end function expectation_value(ψ::FiniteQP, mpo::FiniteMPO) return expectation_value(convert(FiniteMPS, ψ), mpo) end -function expectation_value(ψ::InfiniteMPS, mpo::InfiniteMPO, envs...) - return expectation_value(convert(MultilineMPS, ψ), convert(MultilineMPO, mpo), envs...) +function expectation_value(ψ::InfiniteMPS, mpo::InfiniteMPO) + return expectation_value(convert(MultilineMPS, ψ), convert(MultilineMPO, mpo)) +end +function expectation_value(ψ::InfiniteMPS, mpo::InfiniteMPO, envs::AbstractMPSEnvironments) + return expectation_value( + convert(MultilineMPS, ψ), convert(MultilineMPO, mpo), convert(MultilineEnvironments, envs) + ) end function expectation_value( ψ::MultilineMPS, O::MultilineMPO{<:InfiniteMPO}, diff --git a/src/environments/multiline_envs.jl b/src/environments/multiline_envs.jl index a8e1d73f4..d100009e2 100644 --- a/src/environments/multiline_envs.jl +++ b/src/environments/multiline_envs.jl @@ -98,3 +98,7 @@ function transfer_rightenv!( ) return transfer_rightenv!(envs, below, O, above, site) end + +# converters +Base.convert(::Type{MultilineEnvironments}, envs::InfiniteEnvironments) = Multiline([envs]) +Base.convert(::Type{InfiniteEnvironments}, envs::MultilineEnvironments) = only(envs) diff --git a/test/algorithms/statmech.jl b/test/algorithms/statmech.jl index 0b434b72b..4eeaebc82 100644 --- a/test/algorithms/statmech.jl +++ b/test/algorithms/statmech.jl @@ -7,6 +7,7 @@ println(" using .TestSetup using Test, TestExtras using MPSKit +using MPSKit: Multiline using TensorKit using TensorKit: ℙ