From 4c5fe137d564abe35a81f142db1f1d72c6b598f0 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Wed, 22 Jul 2026 17:03:17 +0800 Subject: [PATCH 1/7] Fix formatting --- test/algorithms/dynamical_dmrg.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/test/algorithms/dynamical_dmrg.jl b/test/algorithms/dynamical_dmrg.jl index 146c43af3..8e07757f4 100644 --- a/test/algorithms/dynamical_dmrg.jl +++ b/test/algorithms/dynamical_dmrg.jl @@ -60,4 +60,3 @@ end @test data ≈ predicted atol = 1.0e-8 end end - From e013be9b250104d9e2dc10b4c942cdaff73e616f Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Wed, 22 Jul 2026 17:17:51 +0800 Subject: [PATCH 2/7] Add finite MPO-MPS zipper compression --- src/MPSKit.jl | 3 +- src/algorithms/changebonds/zipper.jl | 59 +++++++++++++++++++++++++++ src/operators/mpo.jl | 29 ++++++++++++++ src/states/abstractmps.jl | 19 +++++++-- test/algorithms/zipper.jl | 60 ++++++++++++++++++++++++++++ 5 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 src/algorithms/changebonds/zipper.jl create mode 100644 test/algorithms/zipper.jl diff --git a/src/MPSKit.jl b/src/MPSKit.jl index 2ef8cf900..ee55dbfb8 100644 --- a/src/MPSKit.jl +++ b/src/MPSKit.jl @@ -36,7 +36,7 @@ export excitations export FiniteExcited, QuasiparticleAnsatz, ChepigaAnsatz, ChepigaAnsatz2 export time_evolve, timestep, timestep!, make_time_mpo export TDVP, TDVP2, WI, WII, TaylorCluster -export changebonds, changebonds! +export changebonds, changebonds!, zipper export VUMPSSvdCut, OptimalExpand, SvdCut, RandExpand, SketchedExpand export propagator export DynamicalDMRG, NaiveInvert, Jeckelmann @@ -157,6 +157,7 @@ include("algorithms/changebonds/changebonds.jl") include("algorithms/changebonds/optimalexpand.jl") include("algorithms/changebonds/vumpssvd.jl") include("algorithms/changebonds/svdcut.jl") +include("algorithms/changebonds/zipper.jl") include("algorithms/changebonds/randexpand.jl") include("algorithms/changebonds/sketchedexpand.jl") diff --git a/src/algorithms/changebonds/zipper.jl b/src/algorithms/changebonds/zipper.jl new file mode 100644 index 000000000..15d2c35ce --- /dev/null +++ b/src/algorithms/changebonds/zipper.jl @@ -0,0 +1,59 @@ +@doc """ + zipper(O::FiniteMPO{<:GenericMPOTensor}, ψ::FiniteMPS, trscheme; alg_svd=Defaults.alg_svd()) -> ψ′ + zipper(O::FiniteMPO{<:GenericMPOTensor}, ψ::FiniteMPS, alg::SvdCut) -> ψ′ + +Apply a finite open-boundary MPO `O` to a finite MPS `ψ` using a right-to-left +zipper sweep. The MPO and MPS are contracted one site at a time, and the enlarged +virtual bond is truncated immediately using `trscheme`. + +This is an unnormalized compression of `O * ψ`, comparable to +`changebonds(O * ψ, SvdCut(; trscheme); normalize=false)`, but without storing +the fully enlarged product on every site. +""" +function zipper end + +function _mpo_input_physicalspace(O) + return prod(x -> dual(space(O, x)), (numout(O) + 1):(numind(O) - 1)) +end + +function zipper( + O::FiniteMPO{<:GenericMPOTensor}, ψ::FiniteMPS, trscheme::TruncationStrategy; + alg_svd = Defaults.alg_svd() + ) + return zipper(O, ψ, SvdCut(; alg_svd, trscheme)) +end + +function zipper(O::FiniteMPO{<:GenericMPOTensor}, ψ::FiniteMPS, alg::SvdCut) + N = check_length(O, ψ) + if !isunitspace(left_virtualspace(O, 1)) || !isunitspace(right_virtualspace(O, N)) + throw(ArgumentError("zipper is only implemented for open-boundary MPOs")) + end + + ψ′ = copy(ψ) + T = TensorOperations.promote_contract(scalartype(O), scalartype(ψ)) + A = TensorKit.similarstoragetype(eltype(ψ), T) + + Fᵣ = fuser(A, right_virtualspace(ψ′, N), right_virtualspace(O, N)) + local carry + alg_gauge = MatrixAlgebraKit.TruncatedAlgorithm(alg.alg_svd, alg.trscheme) + + As = map(N:-1:1) do i + Aψ = i == 1 ? ψ′.AC[1] : ψ′.AR[i] + physicalspace(Aψ) == _mpo_input_physicalspace(O[i]) || + throw(SpaceMismatch("MPO input physical space does not match MPS physical space at site $i")) + Fₗ = fuser(A, left_virtualspace(ψ′, i), left_virtualspace(O, i)) + Aᶻ = _fuse_mpo_mps(O[i], Aψ, Fₗ, Fᵣ) + i < N && (Aᶻ = Aᶻ * carry) + + if i == 1 + return Aᶻ + else + C, AR = right_gauge(Aᶻ, alg_gauge) + carry = C + Fᵣ = Fₗ + return AR + end + end + + return FiniteMPS(reverse(As); normalize = false, overwrite = true) +end diff --git a/src/operators/mpo.jl b/src/operators/mpo.jl index 7bf0d16ca..3d385d5c9 100644 --- a/src/operators/mpo.jl +++ b/src/operators/mpo.jl @@ -271,6 +271,35 @@ function _fuse_mpo_mps(O::MPOTensor, A::MPSTensor, Fₗ, Fᵣ) @plansor A′[-1 -2; -3] := Fₗ[-1; 1 3] * A[1 2; 4] * O[3 -2; 2 5] * conj(Fᵣ[-3; 4 5]) return A′ isa AbstractBlockTensorMap ? TensorMap(A′) : A′ end +@generated function _fuse_mpo_mps( + O::GenericMPOTensor{S, Nₒ}, A::GenericMPSTensor{S, Nₐ}, Fₗ, Fᵣ + ) where {S, Nₒ, Nₐ} + Nₒ == Nₐ || + return :(throw(ArgumentError("MPO input physical legs should match MPS physical legs"))) + + mps_left = 1 + mpo_left = 2 + mps_right = 3 + mpo_right = 4 + phys_in = 5:(Nₐ + 3) + + out_phys = -(2:Nₒ) + out_right = -(Nₒ + 1) + + t_out = tensorexpr(:A′, (-(1:Nₒ)...,), out_right) + t_left = tensorexpr(:Fₗ, -1, (mps_left, mpo_left)) + t_mps = tensorexpr(:A, (mps_left, phys_in...), mps_right) + t_mpo = tensorexpr(:O, (mpo_left, out_phys...), (phys_in..., mpo_right)) + t_right = tensorexpr(:Fᵣ, out_right, (mps_right, mpo_right)) + ex = macroexpand( + @__MODULE__, :(@plansor $t_out ≔ $t_left * $t_mps * $t_mpo * conj($t_right)) + ) + + return quote + $ex + return A′ isa AbstractBlockTensorMap ? TensorMap(A′) : A′ + end +end function Base.:*(mpo::FiniteMPO{<:MPOTensor}, x::AbstractTensorMap) @assert length(mpo) > 1 diff --git a/src/states/abstractmps.jl b/src/states/abstractmps.jl index 40785968b..40a9a6643 100644 --- a/src/states/abstractmps.jl +++ b/src/states/abstractmps.jl @@ -2,13 +2,23 @@ Tensor types ===========================================================================================# +""" + GenericMPOTensor{S,N} + +Tensor type for representing local MPO tensors with matching numbers of codomain +and domain legs. The first codomain leg is the left virtual leg, the last domain +leg is the right virtual leg, the remaining codomain legs are output physical +legs, and the remaining domain legs are input physical legs. +""" +const GenericMPOTensor{S, N} = AbstractTensorMap{T, S, N, N} where {T} """ MPOTensor{S} -Tensor type for representing local MPO tensors, with the index convention `W ⊗ S ← N ⊗ E`, -where `N`, `E`, `S` and `W` denote the north, east, south and west virtual spaces respectively. +Tensor type for representing single-physical-leg local MPO tensors, with the +index convention `W ⊗ S ← N ⊗ E`, where `N`, `E`, `S` and `W` denote the north, +east, south and west virtual spaces respectively. """ -const MPOTensor{S} = AbstractTensorMap{T, S, 2, 2} where {T} +const MPOTensor{S} = GenericMPOTensor{S, 2} const MPSBondTensor{S} = AbstractTensorMap{T, S, 1, 1} where {T} const GenericMPSTensor{S, N} = AbstractTensorMap{T, S, N, 1} where {T} # some functions are also defined for "general mps tensors" (used in peps code) const MPSTensor{S} = GenericMPSTensor{S, 2} # the usual mps tensors on which we work @@ -214,6 +224,7 @@ Return the virtual space of the bond to the left of sites `pos`. function left_virtualspace end left_virtualspace(A::GenericMPSTensor) = space(A, 1) left_virtualspace(O::MPOTensor) = space(O, 1) +left_virtualspace(O::GenericMPOTensor) = space(O, 1) left_virtualspace(ψ::AbstractMPS) = map(Base.Fix1(left_virtualspace, ψ), eachsite(ψ)) """ @@ -228,6 +239,7 @@ Return the virtual space of the bond to the right of site(s) `pos`. function right_virtualspace end right_virtualspace(A::GenericMPSTensor) = space(A, numind(A))' right_virtualspace(O::MPOTensor) = space(O, 4)' +right_virtualspace(O::GenericMPOTensor) = space(O, numind(O))' right_virtualspace(ψ::AbstractMPS) = map(Base.Fix1(right_virtualspace, ψ), eachsite(ψ)) """ @@ -240,6 +252,7 @@ physicalspace(A::MPSTensor) = space(A, 2) physicalspace(A::GenericMPSTensor) = prod(x -> space(A, x), 2:(numind(A) - 1)) physicalspace(O::MPOTensor) = space(O, 2) physicalspace(O::AbstractBlockTensorMap{<:Any, <:Any, 2, 2}) = only(space(O, 2)) +physicalspace(O::GenericMPOTensor) = prod(x -> space(O, x), 2:numout(O)) physicalspace(ψ::AbstractMPS) = map(Base.Fix1(physicalspace, ψ), eachsite(ψ)) """ diff --git a/test/algorithms/zipper.jl b/test/algorithms/zipper.jl new file mode 100644 index 000000000..c64ecfbf5 --- /dev/null +++ b/test/algorithms/zipper.jl @@ -0,0 +1,60 @@ +println(" +----------------------------- +| Zipper tests | +----------------------------- +") + +using .TestSetup +using Test +using MPSKit +using TensorKit +using TensorKit: ℙ +using Random + +spacelist = [(ℙ^4, ℙ^3), (Rep[SU₂](1 => 1), Rep[SU₂](0 => 2, 1 => 2, 2 => 1))] + +@testset "Finite MPO-MPS zipper $(spacetype(pspace))" for (pspace, Dspace) in spacelist + Random.seed!(1357) + L = 6 + Wspace = Dspace + Vspaces = [oneunit(Wspace); fill(Wspace, L - 1); oneunit(Wspace)] + O = FiniteMPO( + [rand(ComplexF64, Vspaces[i] ⊗ pspace ← pspace ⊗ Vspaces[i + 1]) for i in 1:L] + ) + ψ = FiniteMPS(rand, ComplexF64, L, pspace, Dspace) + O_copy = copy(O) + ψ_copy = copy(ψ) + + trscheme = trunctol(; atol = 1.0e-10) + ref = changebonds(O * ψ, SvdCut(; trscheme); normalize = false) + got = zipper(O, ψ, trscheme) + + @test norm(ref - got) / norm(ref) < 1.0e-10 + @test norm(ψ - ψ_copy) < 1.0e-12 + @test all(i -> norm(O[i] - O_copy[i]) < 1.0e-12, 1:length(O)) + + Dcut = 4 + got_tr = zipper(O, ψ, truncrank(Dcut)) + @test maximum(i -> dim(left_virtualspace(got_tr, i)), 2:length(got_tr)) <= Dcut +end + +@testset "Density-matrix FiniteMPS zipper $(spacetype(pcomp))" for (pcomp, Dspace) in [ + (ℙ^2 ⊗ (ℙ^2)', ℙ^6), + (Rep[SU₂](1 // 2 => 1) ⊗ Rep[SU₂](1 // 2 => 1)', Rep[SU₂](0 => 4, 1 => 3)), + ] + Random.seed!(9753) + L = 6 + Wspace = Dspace + Vspaces = [oneunit(Wspace); fill(Wspace, L - 1); oneunit(Wspace)] + O = FiniteMPO( + [rand(ComplexF64, Vspaces[i] ⊗ pcomp ← pcomp ⊗ Vspaces[i + 1]) for i in 1:L] + ) + ψ = FiniteMPS(rand, ComplexF64, fill(pcomp, L), Dspace) + + trscheme = trunctol(; atol = 1.0e-10) + ref = changebonds(O * ψ, SvdCut(; trscheme); normalize = false) + got = zipper(O, ψ, trscheme) + + @test numind(got.AC[L ÷ 2]) == 4 + @test norm(ref - got) / norm(ref) < 1.0e-10 +end From 85f6dbd83ceda54ac41d0776d9f86aa761decb59 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Thu, 23 Jul 2026 06:36:10 +0800 Subject: [PATCH 3/7] Change to `approximate` interface --- src/MPSKit.jl | 5 ++- src/algorithms/approximate/approximate.jl | 6 ++- .../{changebonds => approximate}/zipper.jl | 38 +++++++++---------- src/operators/mpo.jl | 29 -------------- src/states/abstractmps.jl | 19 ++-------- test/algorithms/zipper.jl | 25 +----------- 6 files changed, 30 insertions(+), 92 deletions(-) rename src/algorithms/{changebonds => approximate}/zipper.jl (57%) diff --git a/src/MPSKit.jl b/src/MPSKit.jl index ee55dbfb8..ed369c5ea 100644 --- a/src/MPSKit.jl +++ b/src/MPSKit.jl @@ -36,8 +36,9 @@ export excitations export FiniteExcited, QuasiparticleAnsatz, ChepigaAnsatz, ChepigaAnsatz2 export time_evolve, timestep, timestep!, make_time_mpo export TDVP, TDVP2, WI, WII, TaylorCluster -export changebonds, changebonds!, zipper +export changebonds, changebonds! export VUMPSSvdCut, OptimalExpand, SvdCut, RandExpand, SketchedExpand +export Zipper export propagator export DynamicalDMRG, NaiveInvert, Jeckelmann export exact_diagonalization, fidelity_susceptibility @@ -157,7 +158,6 @@ include("algorithms/changebonds/changebonds.jl") include("algorithms/changebonds/optimalexpand.jl") include("algorithms/changebonds/vumpssvd.jl") include("algorithms/changebonds/svdcut.jl") -include("algorithms/changebonds/zipper.jl") include("algorithms/changebonds/randexpand.jl") include("algorithms/changebonds/sketchedexpand.jl") @@ -189,6 +189,7 @@ include("algorithms/statmech/idmrg.jl") include("algorithms/fidelity_susceptibility.jl") include("algorithms/approximate/approximate.jl") +include("algorithms/approximate/zipper.jl") include("algorithms/approximate/vomps.jl") include("algorithms/approximate/fvomps.jl") include("algorithms/approximate/idmrg.jl") diff --git a/src/algorithms/approximate/approximate.jl b/src/algorithms/approximate/approximate.jl index 6967dcef0..b90f64770 100644 --- a/src/algorithms/approximate/approximate.jl +++ b/src/algorithms/approximate/approximate.jl @@ -4,10 +4,11 @@ approximate!(ψ₀, (O, ψ), algorithm, [environments]) -> (ψ, environments, ϵ) approximate(ψ₀, ψ, algorithm, [environments]) -> (ψ, environments, ϵ) approximate!(ψ₀, ψ, algorithm, [environments]) -> (ψ, environments, ϵ) + approximate((O, ψ), algorithm) -> ψ′ Compute an approximation to the application of an operator `O` to the state `ψ` in the form -of an MPS `ψ₀`. If only a state `ψ` is supplied instead of the `(O, ψ)` pair, `ψ₀` is -approximated directly to `ψ` (i.e. `O` is taken to be the identity). +of an MPS, using initial guess `ψ₀`. If only a state `ψ` is supplied instead of the `(O, ψ)` pair, +`ψ₀` is approximated directly to `ψ` (i.e. `O` is taken to be the identity). **Not every algorithm supports every combination of arguments below** — see the per-algorithm notes at the end of this docstring before picking one. @@ -41,6 +42,7 @@ infinite algorithms always require an explicit `(O, ψ)` tuple, and **`VOMPS` ha |:--------- |:----------------------------- |:---------------------------------- |:------------------:|:--------------:| | `DMRG` | single-site, fixes bond dim | `AbstractFiniteMPS` | ✅ | ✅ | | `DMRG2` | two-site, truncates via `trscheme` | `AbstractFiniteMPS` | ✅ | ✅ | +| `Zipper` | right-to-left streaming MPO-MPS compression | none, uses `(O, ψ)` directly | ❌ | ❌ | | `IDMRG` | single-site, thermodynamic limit | `InfiniteMPS` / `MultilineMPS` | ❌ (tuple only) | ✅ | | `IDMRG2` | two-site, thermodynamic limit, needs unit cell ≥ 2 | `InfiniteMPS` / `MultilineMPS` | ❌ (tuple only) | ✅ | | `VOMPS` | tangent-space truncation | `InfiniteMPS` / `MultilineMPS` | ❌ (tuple only) | ❌ (out-of-place only) | diff --git a/src/algorithms/changebonds/zipper.jl b/src/algorithms/approximate/zipper.jl similarity index 57% rename from src/algorithms/changebonds/zipper.jl rename to src/algorithms/approximate/zipper.jl index 15d2c35ce..43167a471 100644 --- a/src/algorithms/changebonds/zipper.jl +++ b/src/algorithms/approximate/zipper.jl @@ -1,32 +1,30 @@ -@doc """ - zipper(O::FiniteMPO{<:GenericMPOTensor}, ψ::FiniteMPS, trscheme; alg_svd=Defaults.alg_svd()) -> ψ′ - zipper(O::FiniteMPO{<:GenericMPOTensor}, ψ::FiniteMPS, alg::SvdCut) -> ψ′ +""" + Zipper(; alg_svd=Defaults.alg_svd(), trscheme) + +Algorithm that approximates an open-boundary finite MPO-MPS product using a right-to-left +zipper sweep. The MPO and MPS are contracted one site at a time, and the enlarged virtual +bond is truncated immediately using `trscheme`. + +Use as: -Apply a finite open-boundary MPO `O` to a finite MPS `ψ` using a right-to-left -zipper sweep. The MPO and MPS are contracted one site at a time, and the enlarged -virtual bond is truncated immediately using `trscheme`. + approximate((O, ψ), Zipper(; trscheme)) -This is an unnormalized compression of `O * ψ`, comparable to +This returns an unnormalized compression of `O * ψ`, comparable to `changebonds(O * ψ, SvdCut(; trscheme); normalize=false)`, but without storing the fully enlarged product on every site. """ -function zipper end - -function _mpo_input_physicalspace(O) - return prod(x -> dual(space(O, x)), (numout(O) + 1):(numind(O) - 1)) -end +@kwdef struct Zipper{S} <: Algorithm + "algorithm used for the singular value decomposition" + alg_svd::S = Defaults.alg_svd() -function zipper( - O::FiniteMPO{<:GenericMPOTensor}, ψ::FiniteMPS, trscheme::TruncationStrategy; - alg_svd = Defaults.alg_svd() - ) - return zipper(O, ψ, SvdCut(; alg_svd, trscheme)) + "algorithm used for truncation of the local gauge tensors" + trscheme::TruncationStrategy end -function zipper(O::FiniteMPO{<:GenericMPOTensor}, ψ::FiniteMPS, alg::SvdCut) +function approximate((O, ψ)::Tuple{Any, <:FiniteMPS}, alg::Zipper) N = check_length(O, ψ) if !isunitspace(left_virtualspace(O, 1)) || !isunitspace(right_virtualspace(O, N)) - throw(ArgumentError("zipper is only implemented for open-boundary MPOs")) + throw(ArgumentError("Zipper is only implemented for open-boundary MPOs")) end ψ′ = copy(ψ) @@ -39,7 +37,7 @@ function zipper(O::FiniteMPO{<:GenericMPOTensor}, ψ::FiniteMPS, alg::SvdCut) As = map(N:-1:1) do i Aψ = i == 1 ? ψ′.AC[1] : ψ′.AR[i] - physicalspace(Aψ) == _mpo_input_physicalspace(O[i]) || + physicalspace(Aψ) == physicalspace(O[i]) || throw(SpaceMismatch("MPO input physical space does not match MPS physical space at site $i")) Fₗ = fuser(A, left_virtualspace(ψ′, i), left_virtualspace(O, i)) Aᶻ = _fuse_mpo_mps(O[i], Aψ, Fₗ, Fᵣ) diff --git a/src/operators/mpo.jl b/src/operators/mpo.jl index 3d385d5c9..7bf0d16ca 100644 --- a/src/operators/mpo.jl +++ b/src/operators/mpo.jl @@ -271,35 +271,6 @@ function _fuse_mpo_mps(O::MPOTensor, A::MPSTensor, Fₗ, Fᵣ) @plansor A′[-1 -2; -3] := Fₗ[-1; 1 3] * A[1 2; 4] * O[3 -2; 2 5] * conj(Fᵣ[-3; 4 5]) return A′ isa AbstractBlockTensorMap ? TensorMap(A′) : A′ end -@generated function _fuse_mpo_mps( - O::GenericMPOTensor{S, Nₒ}, A::GenericMPSTensor{S, Nₐ}, Fₗ, Fᵣ - ) where {S, Nₒ, Nₐ} - Nₒ == Nₐ || - return :(throw(ArgumentError("MPO input physical legs should match MPS physical legs"))) - - mps_left = 1 - mpo_left = 2 - mps_right = 3 - mpo_right = 4 - phys_in = 5:(Nₐ + 3) - - out_phys = -(2:Nₒ) - out_right = -(Nₒ + 1) - - t_out = tensorexpr(:A′, (-(1:Nₒ)...,), out_right) - t_left = tensorexpr(:Fₗ, -1, (mps_left, mpo_left)) - t_mps = tensorexpr(:A, (mps_left, phys_in...), mps_right) - t_mpo = tensorexpr(:O, (mpo_left, out_phys...), (phys_in..., mpo_right)) - t_right = tensorexpr(:Fᵣ, out_right, (mps_right, mpo_right)) - ex = macroexpand( - @__MODULE__, :(@plansor $t_out ≔ $t_left * $t_mps * $t_mpo * conj($t_right)) - ) - - return quote - $ex - return A′ isa AbstractBlockTensorMap ? TensorMap(A′) : A′ - end -end function Base.:*(mpo::FiniteMPO{<:MPOTensor}, x::AbstractTensorMap) @assert length(mpo) > 1 diff --git a/src/states/abstractmps.jl b/src/states/abstractmps.jl index 40a9a6643..40785968b 100644 --- a/src/states/abstractmps.jl +++ b/src/states/abstractmps.jl @@ -2,23 +2,13 @@ Tensor types ===========================================================================================# -""" - GenericMPOTensor{S,N} - -Tensor type for representing local MPO tensors with matching numbers of codomain -and domain legs. The first codomain leg is the left virtual leg, the last domain -leg is the right virtual leg, the remaining codomain legs are output physical -legs, and the remaining domain legs are input physical legs. -""" -const GenericMPOTensor{S, N} = AbstractTensorMap{T, S, N, N} where {T} """ MPOTensor{S} -Tensor type for representing single-physical-leg local MPO tensors, with the -index convention `W ⊗ S ← N ⊗ E`, where `N`, `E`, `S` and `W` denote the north, -east, south and west virtual spaces respectively. +Tensor type for representing local MPO tensors, with the index convention `W ⊗ S ← N ⊗ E`, +where `N`, `E`, `S` and `W` denote the north, east, south and west virtual spaces respectively. """ -const MPOTensor{S} = GenericMPOTensor{S, 2} +const MPOTensor{S} = AbstractTensorMap{T, S, 2, 2} where {T} const MPSBondTensor{S} = AbstractTensorMap{T, S, 1, 1} where {T} const GenericMPSTensor{S, N} = AbstractTensorMap{T, S, N, 1} where {T} # some functions are also defined for "general mps tensors" (used in peps code) const MPSTensor{S} = GenericMPSTensor{S, 2} # the usual mps tensors on which we work @@ -224,7 +214,6 @@ Return the virtual space of the bond to the left of sites `pos`. function left_virtualspace end left_virtualspace(A::GenericMPSTensor) = space(A, 1) left_virtualspace(O::MPOTensor) = space(O, 1) -left_virtualspace(O::GenericMPOTensor) = space(O, 1) left_virtualspace(ψ::AbstractMPS) = map(Base.Fix1(left_virtualspace, ψ), eachsite(ψ)) """ @@ -239,7 +228,6 @@ Return the virtual space of the bond to the right of site(s) `pos`. function right_virtualspace end right_virtualspace(A::GenericMPSTensor) = space(A, numind(A))' right_virtualspace(O::MPOTensor) = space(O, 4)' -right_virtualspace(O::GenericMPOTensor) = space(O, numind(O))' right_virtualspace(ψ::AbstractMPS) = map(Base.Fix1(right_virtualspace, ψ), eachsite(ψ)) """ @@ -252,7 +240,6 @@ physicalspace(A::MPSTensor) = space(A, 2) physicalspace(A::GenericMPSTensor) = prod(x -> space(A, x), 2:(numind(A) - 1)) physicalspace(O::MPOTensor) = space(O, 2) physicalspace(O::AbstractBlockTensorMap{<:Any, <:Any, 2, 2}) = only(space(O, 2)) -physicalspace(O::GenericMPOTensor) = prod(x -> space(O, x), 2:numout(O)) physicalspace(ψ::AbstractMPS) = map(Base.Fix1(physicalspace, ψ), eachsite(ψ)) """ diff --git a/test/algorithms/zipper.jl b/test/algorithms/zipper.jl index c64ecfbf5..84735af3f 100644 --- a/test/algorithms/zipper.jl +++ b/test/algorithms/zipper.jl @@ -27,34 +27,13 @@ spacelist = [(ℙ^4, ℙ^3), (Rep[SU₂](1 => 1), Rep[SU₂](0 => 2, 1 => 2, 2 = trscheme = trunctol(; atol = 1.0e-10) ref = changebonds(O * ψ, SvdCut(; trscheme); normalize = false) - got = zipper(O, ψ, trscheme) + got = approximate((O, ψ), Zipper(; trscheme)) @test norm(ref - got) / norm(ref) < 1.0e-10 @test norm(ψ - ψ_copy) < 1.0e-12 @test all(i -> norm(O[i] - O_copy[i]) < 1.0e-12, 1:length(O)) Dcut = 4 - got_tr = zipper(O, ψ, truncrank(Dcut)) + got_tr = approximate((O, ψ), Zipper(; trscheme = truncrank(Dcut))) @test maximum(i -> dim(left_virtualspace(got_tr, i)), 2:length(got_tr)) <= Dcut end - -@testset "Density-matrix FiniteMPS zipper $(spacetype(pcomp))" for (pcomp, Dspace) in [ - (ℙ^2 ⊗ (ℙ^2)', ℙ^6), - (Rep[SU₂](1 // 2 => 1) ⊗ Rep[SU₂](1 // 2 => 1)', Rep[SU₂](0 => 4, 1 => 3)), - ] - Random.seed!(9753) - L = 6 - Wspace = Dspace - Vspaces = [oneunit(Wspace); fill(Wspace, L - 1); oneunit(Wspace)] - O = FiniteMPO( - [rand(ComplexF64, Vspaces[i] ⊗ pcomp ← pcomp ⊗ Vspaces[i + 1]) for i in 1:L] - ) - ψ = FiniteMPS(rand, ComplexF64, fill(pcomp, L), Dspace) - - trscheme = trunctol(; atol = 1.0e-10) - ref = changebonds(O * ψ, SvdCut(; trscheme); normalize = false) - got = zipper(O, ψ, trscheme) - - @test numind(got.AC[L ÷ 2]) == 4 - @test norm(ref - got) / norm(ref) < 1.0e-10 -end From 46ca0b743d05b5fbdb068dae7178d8169c7650bd Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Fri, 24 Jul 2026 07:08:17 +0800 Subject: [PATCH 4/7] Remove unnecessary copy --- src/algorithms/approximate/zipper.jl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/algorithms/approximate/zipper.jl b/src/algorithms/approximate/zipper.jl index 43167a471..671b2cce0 100644 --- a/src/algorithms/approximate/zipper.jl +++ b/src/algorithms/approximate/zipper.jl @@ -27,19 +27,18 @@ function approximate((O, ψ)::Tuple{Any, <:FiniteMPS}, alg::Zipper) throw(ArgumentError("Zipper is only implemented for open-boundary MPOs")) end - ψ′ = copy(ψ) T = TensorOperations.promote_contract(scalartype(O), scalartype(ψ)) A = TensorKit.similarstoragetype(eltype(ψ), T) - Fᵣ = fuser(A, right_virtualspace(ψ′, N), right_virtualspace(O, N)) + Fᵣ = fuser(A, right_virtualspace(ψ, N), right_virtualspace(O, N)) local carry alg_gauge = MatrixAlgebraKit.TruncatedAlgorithm(alg.alg_svd, alg.trscheme) As = map(N:-1:1) do i - Aψ = i == 1 ? ψ′.AC[1] : ψ′.AR[i] + Aψ = i == 1 ? ψ.AC[1] : ψ.AR[i] physicalspace(Aψ) == physicalspace(O[i]) || throw(SpaceMismatch("MPO input physical space does not match MPS physical space at site $i")) - Fₗ = fuser(A, left_virtualspace(ψ′, i), left_virtualspace(O, i)) + Fₗ = fuser(A, left_virtualspace(ψ, i), left_virtualspace(O, i)) Aᶻ = _fuse_mpo_mps(O[i], Aψ, Fₗ, Fᵣ) i < N && (Aᶻ = Aᶻ * carry) From 404fdbb9f3bdcc73a6f9f081770dfb50815a8848 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Fri, 24 Jul 2026 07:22:06 +0800 Subject: [PATCH 5/7] Switch to left-to-right zipper --- src/algorithms/approximate/zipper.jl | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/algorithms/approximate/zipper.jl b/src/algorithms/approximate/zipper.jl index 671b2cce0..1881cf053 100644 --- a/src/algorithms/approximate/zipper.jl +++ b/src/algorithms/approximate/zipper.jl @@ -1,7 +1,7 @@ """ Zipper(; alg_svd=Defaults.alg_svd(), trscheme) -Algorithm that approximates an open-boundary finite MPO-MPS product using a right-to-left +Algorithm that approximates an open-boundary finite MPO-MPS product using a left-to-right zipper sweep. The MPO and MPS are contracted one site at a time, and the enlarged virtual bond is truncated immediately using `trscheme`. @@ -30,27 +30,27 @@ function approximate((O, ψ)::Tuple{Any, <:FiniteMPS}, alg::Zipper) T = TensorOperations.promote_contract(scalartype(O), scalartype(ψ)) A = TensorKit.similarstoragetype(eltype(ψ), T) - Fᵣ = fuser(A, right_virtualspace(ψ, N), right_virtualspace(O, N)) + Fₗ = fuser(A, left_virtualspace(ψ, 1), left_virtualspace(O, 1)) local carry alg_gauge = MatrixAlgebraKit.TruncatedAlgorithm(alg.alg_svd, alg.trscheme) - As = map(N:-1:1) do i + As = map(1:N) do i Aψ = i == 1 ? ψ.AC[1] : ψ.AR[i] physicalspace(Aψ) == physicalspace(O[i]) || throw(SpaceMismatch("MPO input physical space does not match MPS physical space at site $i")) - Fₗ = fuser(A, left_virtualspace(ψ, i), left_virtualspace(O, i)) + Fᵣ = fuser(A, right_virtualspace(ψ, i), right_virtualspace(O, i)) Aᶻ = _fuse_mpo_mps(O[i], Aψ, Fₗ, Fᵣ) - i < N && (Aᶻ = Aᶻ * carry) + i > 1 && (Aᶻ = _mul_front(carry, Aᶻ)) - if i == 1 + if i == N return Aᶻ else - C, AR = right_gauge(Aᶻ, alg_gauge) + AL, C, _ = left_gauge(Aᶻ, alg_gauge) carry = C - Fᵣ = Fₗ - return AR + Fₗ = Fᵣ + return AL end end - return FiniteMPS(reverse(As); normalize = false, overwrite = true) + return FiniteMPS(As; normalize = false, overwrite = true) end From 8757f954788f4143ab5278119d87b83122c97b71 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Fri, 24 Jul 2026 07:24:03 +0800 Subject: [PATCH 6/7] Add reference --- docs/src/assets/mpskit.bib | 17 +++++++++++++++++ src/algorithms/approximate/zipper.jl | 12 ++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/docs/src/assets/mpskit.bib b/docs/src/assets/mpskit.bib index 296f4194b..cf9e3fd0b 100644 --- a/docs/src/assets/mpskit.bib +++ b/docs/src/assets/mpskit.bib @@ -385,6 +385,23 @@ @misc{shen2025 keywords = {Condensed Matter - Strongly Correlated Electrons,High Energy Physics - Theory} } +@article{sinha2024, + title = {Efficient Representation of Minimally Entangled Typical Thermal States in Two Dimensions via Projected Entangled Pair States}, + author = {Sinha, Aritra and Rams, Marek M. and Dziarmaga, Jacek}, + year = {2024}, + month = jan, + journal = {Physical Review B}, + volume = {109}, + number = {4}, + pages = {045136}, + publisher = {American Physical Society}, + doi = {10.1103/PhysRevB.109.045136}, + url = {https://link.aps.org/doi/10.1103/PhysRevB.109.045136}, + eprint = {2310.08533}, + archiveprefix = {arXiv}, + primaryclass = {quant-ph} +} + @article{tang2025, title = {Matrix Product State Fixed Points of Non-{{Hermitian}} Transfer Matrices}, author = {Tang, Wei and Verstraete, Frank and Haegeman, Jutho}, diff --git a/src/algorithms/approximate/zipper.jl b/src/algorithms/approximate/zipper.jl index 1881cf053..f8f12581a 100644 --- a/src/algorithms/approximate/zipper.jl +++ b/src/algorithms/approximate/zipper.jl @@ -1,17 +1,17 @@ """ - Zipper(; alg_svd=Defaults.alg_svd(), trscheme) +$(TYPEDEF) Algorithm that approximates an open-boundary finite MPO-MPS product using a left-to-right zipper sweep. The MPO and MPS are contracted one site at a time, and the enlarged virtual bond is truncated immediately using `trscheme`. -Use as: +## Fields - approximate((O, ψ), Zipper(; trscheme)) +$(TYPEDFIELDS) -This returns an unnormalized compression of `O * ψ`, comparable to -`changebonds(O * ψ, SvdCut(; trscheme); normalize=false)`, but without storing -the fully enlarged product on every site. +## References + +* [Sinha et al. Phys. Rev. B 109 (2024)](@cite sinha2024) """ @kwdef struct Zipper{S} <: Algorithm "algorithm used for the singular value decomposition" From 2b8c493f9995cceab2cc2fb6eaf6bda82c0bfc44 Mon Sep 17 00:00:00 2001 From: Yue Zhengyuan Date: Fri, 24 Jul 2026 07:33:56 +0800 Subject: [PATCH 7/7] Change constructors of `Zipper` --- src/algorithms/approximate/zipper.jl | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/algorithms/approximate/zipper.jl b/src/algorithms/approximate/zipper.jl index f8f12581a..ca9d89fd9 100644 --- a/src/algorithms/approximate/zipper.jl +++ b/src/algorithms/approximate/zipper.jl @@ -9,16 +9,25 @@ bond is truncated immediately using `trscheme`. $(TYPEDFIELDS) +## Constructors + + Zipper(; trscheme, alg_svd=Defaults.alg_svd()) + Zipper(alg_gauge) + +Create a `Zipper` algorithm with the given truncated gauge algorithm, or by passing a +truncation scheme and singular value decomposition algorithm. + ## References -* [Sinha et al. Phys. Rev. B 109 (2024)](@cite sinha2024) +- [Sinha et al. Phys. Rev. B 109 (2024)](@cite sinha2024) """ -@kwdef struct Zipper{S} <: Algorithm - "algorithm used for the singular value decomposition" - alg_svd::S = Defaults.alg_svd() +struct Zipper{G} <: Algorithm + "algorithm used for gauging and truncating the local tensors" + alg_gauge::G +end - "algorithm used for truncation of the local gauge tensors" - trscheme::TruncationStrategy +function Zipper(; trscheme::TruncationStrategy, alg_svd = Defaults.alg_svd()) + return Zipper(MatrixAlgebraKit.TruncatedAlgorithm(alg_svd, trscheme)) end function approximate((O, ψ)::Tuple{Any, <:FiniteMPS}, alg::Zipper) @@ -32,7 +41,6 @@ function approximate((O, ψ)::Tuple{Any, <:FiniteMPS}, alg::Zipper) Fₗ = fuser(A, left_virtualspace(ψ, 1), left_virtualspace(O, 1)) local carry - alg_gauge = MatrixAlgebraKit.TruncatedAlgorithm(alg.alg_svd, alg.trscheme) As = map(1:N) do i Aψ = i == 1 ? ψ.AC[1] : ψ.AR[i] @@ -45,7 +53,7 @@ function approximate((O, ψ)::Tuple{Any, <:FiniteMPS}, alg::Zipper) if i == N return Aᶻ else - AL, C, _ = left_gauge(Aᶻ, alg_gauge) + AL, C, _ = left_gauge(Aᶻ, alg.alg_gauge) carry = C Fₗ = Fᵣ return AL