Skip to content

Zipper compression for FiniteMPO-FiniteMPS product#470

Open
Yue-Zhengyuan wants to merge 3 commits into
QuantumKitHub:mainfrom
Yue-Zhengyuan:zipper
Open

Zipper compression for FiniteMPO-FiniteMPS product#470
Yue-Zhengyuan wants to merge 3 commits into
QuantumKitHub:mainfrom
Yue-Zhengyuan:zipper

Conversation

@Yue-Zhengyuan

@Yue-Zhengyuan Yue-Zhengyuan commented Jul 22, 2026

Copy link
Copy Markdown
Member

This PR adds approximate((O, ψ), ::Zipper) for open-boundary FiniteMPO acting on FiniteMPS, without normalization:

approximate((O, ψ)::Tuple{Any, <:FiniteMPS}, alg::Zipper)

This provides a streaming MPO-MPS application plus SVD compression path. The algorithm struct Zipper controls the SVD algorithm and truncation strategy.

Motivation

This is useful for PEPS boundary-MPS style contractions (QuantumKitHub/PEPSKit.jl#396), where applying a double-layer transfer MPO to a boundary MPS can temporarily create very large virtual bonds. The existing initialization pattern,

changebonds(O * ψ, SvdCut(; trscheme); normalize=false)

first materializes the full enlarged O * ψ across all sites, then compresses it. That is unnecessarily memory-heavy when the product is only needed as an initialization that will be further optimized with approximate.

The new Zipper algorithm contracts one MPO/MPS site pair at a time from right to left, absorbs the "carry" (residue tensor produced by right-orth gauge conversion) from the already-compressed right block, and immediately truncates before moving left. This avoids storing the fully enlarged product on every site.

Notes

The Zipper approximation matches changebonds(O * ψ, SvdCut(; trscheme); normalize=false) when no meaningful singular values are discarded, but can differ under real truncation because zipper truncates while building the product rather than after materializing the full product.

@Yue-Zhengyuan

Copy link
Copy Markdown
Member Author

Please let me know if there are actually better functions in MPSKit that does more or less the same thing.

@Yue-Zhengyuan

Copy link
Copy Markdown
Member Author

@lkdvos I cannot run the format check on GitHub either. So there's something to be fixed.

@lkdvos

lkdvos commented Jul 22, 2026

Copy link
Copy Markdown
Member

Thanks for getting this started! I'll look into the formatter, let's not worry about that here.

For consistency, it would probably make the most sense to match this into approximate!(out, (O, in), alg, envs = nothing), but I guess that stumbles back on the issue we had before where it is not obvious how to construct the out beforehand. It might be reasonable to simply allow approximate((O, in), alg), and construct a FiniteMPS without any tensors initialized, so just the exterior vectors get reused?

I'm a bit confused about the necessity for GenericMPO.
Usually when we have two legs, the main point is that the MPO is structured in such a way that it acts only on one of the MPS legs, by keeping the layers separate. This then leads to memory and computational benefits, as opposed to just fusing everything together.
Here, there isn't really a lot of benefit to support the GenericMPO, since it requires us to maintain more contractions, and is generally expected to be slightly slower because the fusiontree overhead grows with the number of legs.

As an alternative, I would suggest writing this in terms of primitive pieces that can be overloaded for the PEPS case, which keep the PEPS layers separate to keep the computational benefits in. Here that would probably just be fuse_mpo_mps?

@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.00000% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/algorithms/approximate/zipper.jl 92.00% 2 Missing ⚠️
Files with missing lines Coverage Δ
src/MPSKit.jl 100.00% <ø> (ø)
src/algorithms/approximate/approximate.jl 56.00% <ø> (ø)
src/algorithms/approximate/zipper.jl 92.00% <92.00%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Yue-Zhengyuan

Copy link
Copy Markdown
Member Author

It might be reasonable to simply allow approximate((O, in), alg)

Ah, the same interface debate as compress in QuantumKitHub/PEPSKit.jl#311 ... For alg, should I introduce a new Zipper (something like that) instead of reusing SvdCut? I need to distinguish it from changebonds(O * in, alg).

construct a FiniteMPS without any tensors initialized, so just the exterior vectors get reused?

What is meant by "exterior vectors"?

I'm a bit confused about the necessity for GenericMPO.

Indeed I realized this later. We just overload fuse_mpo_mps for PEPS/PEPO applications. Here in MPSKit I should just write for the simplest one-physical-leg case.

@lkdvos

lkdvos commented Jul 22, 2026

Copy link
Copy Markdown
Member

What is meant by "exterior vectors"?

Here I mean that a FiniteMPS is at its core just a set of vectors (ALs, ARs, ACs, Cs), which are each filled with tensors. I was thinking whether it could be worth it to just initialize the vectors as undef (or perhaps just missing), so no tensors are present but we have something to instantiate into. This bypasses the need to allocate output tensors in advance, but still keeps the same interface?
Let me know if that sounds too far-fetched though, I don't want to overcomplicate things.

Should I introduce a new Zipper (something like that) instead of reusing SvdCut?

I think this is probably sensible, it might additionally store something like a direction or a backend and allocator in the future.

@Yue-Zhengyuan

Copy link
Copy Markdown
Member Author

Initializing with undef just for matching interface is a bit unnecessary for me. What about also calling the function compress? Might be more descriptive than the current zipper.

@lkdvos

lkdvos commented Jul 22, 2026

Copy link
Copy Markdown
Member

It's just really annoying to have both approximate and compress for the same thing though, just based on the distinction of not needing an initial guess, in that case I would much rather make the interface of approximate more adapted to that

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ᵣ)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel that the default _fuse_mpo_mps, which uses @plansor, will cause troubles for iPEPO measurements. For column-by-column contraction from right to left, the default iPEPO arrow convention (top to bottom) will make the MPO virtual spaces dual.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if that really is a problem, since you will presumably have to overload that anyways?

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))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For double-layer iPEPS networks, I'll need to fuse 3 virtual legs. In this case fuser should be the combination of two pair-wise isomorphisms?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if that is really necessary, in principle you can just fuse all of them together, although we might have to adapt the fuser function to also take ProductSpace arguments.

@Yue-Zhengyuan
Yue-Zhengyuan requested a review from lkdvos July 22, 2026 22:57
Comment on lines +17 to +21
"algorithm used for the singular value decomposition"
alg_svd::S = Defaults.alg_svd()

"algorithm used for truncation of the local gauge tensors"
trscheme::TruncationStrategy

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I've started putting these into a single alg_gauge struct instead, that way also things like randomized SVD could be used. I agree that keeping these keyword arguments can be nice though, so it is probably also reasonable to write a custom constructor that can accept them.

local carry
alg_gauge = MatrixAlgebraKit.TruncatedAlgorithm(alg.alg_svd, alg.trscheme)

As = map(N:-1:1) do i

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to go from back to front specifically? I think this needs extra transpositions in many places, mostly because our MPS tensors are N,1, so left_gauge is slightly more efficient than right_gauge.

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))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if that is really necessary, in principle you can just fuse all of them together, although we might have to adapt the fuser function to also take ProductSpace arguments.

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ᵣ)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if that really is a problem, since you will presumably have to overload that anyways?

throw(ArgumentError("Zipper is only implemented for open-boundary MPOs"))
end

ψ′ = copy(ψ)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we need this copy here? And if so, could you restructure it to be approximate((O, psi), alg) = approximate(copy(psi), (O, psi), alg)?

@VinceNeede

Copy link
Copy Markdown
Contributor

Thanks for putting this together — the zip-up contraction is a nice addition. A few questions/comments while reading through it, cross-checking against the original references:

1. Naming

The algorithm is implemented here as Zipper, but the original method (Stoudenmire & White, New J. Phys. 12, 055026 (2010), DOI: 10.1088/1367-2630/12/5/055026 calls it "zip-up." Was the rename intentional (e.g. to avoid a name clash, or because the implementation deviates from the original enough to warrant a different name), or would it be worth aligning the naming with the literature so people searching for "zip-up" can find it?

2. Missing truncation-only sweep

As I understand the original algorithm, it consists of two sweeps: one sweep that does the contraction with only a loose/minimal truncation (to keep the intermediate bond dimension under control without discarding meaningful weight), followed by a second sweep that performs the actual truncation down to the target maxdim/cutoff. In this PR I only see the first sweep (compression with minimal truncation) — is the second, final-truncation sweep intentionally left out for now (e.g. deferred to a follow-up PR), or should it be included here?

Related to this: Paeckel et al. (Annals of Physics 411, 167998 (2019), DOI: 10.1016/j.aop.2019.167998 give a concrete prescription for the intermediate/minimal truncation stage — using a looser bond dimension (2× the target maxdim) and a looser cutoff (cutoff/10) relative to the final truncation values, I think this would be worth implementing as default behavior if the second sweep is added.

3. Orthogonality center of the MPO

Stoudenmire & White also move the orthogonality center of the Hamiltonian/MPO to the last site (given this is done right-to-left here). I don't see that happening in this implementation — is the MPO's orthogonality center supposed to be moved before by the user, or is this something that still needs to be added?

Happy to be corrected on any of this if I'm misreading the code or the references — just want to make sure the implementation matches the papers it's based on before it lands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants