Skip to content

Restructure IDMRG/IDMRG2 along the lines of DMRG/DMRG2, starting with a finalize callback #503

Description

@lkdvos

DMRG/DMRG2 and IDMRG/IDMRG2 have drifted apart quite a bit, and the infinite versions are missing most of what the finite ones have gained. The most user-visible gap is the finalize callback, but it is not the only one, and I think it is worth restructuring idmrg.jl along the lines of dmrg.jl rather than bolting on a single field.

Motivation

From #480: a user running IDMRG2 at large bond dimension wants to checkpoint the state every couple of sweeps. Since there is no finalize hook, the only way to do that is to chunk the sweeps across separate find_groundstate calls:

for i in 1:10
    psi, envs, = find_groundstate(psi, H, IDMRG2(; trunc = truncrank(chi), maxiter = 2, tol = 1e-12), envs)
    save(...)
end

That is not just inelegant. Every find_groundstate call ends with

ψ′ = InfiniteMPS(it.state.mps.AR; alg_gauge.tol, alg_gauge.maxiter)
envs = recalculate!(it.state.envs, ψ′, it.state.operator, ψ′)

so chunking pays a full re-gauge plus an environment recalculation every maxiter sweeps. At χ ~ 10³–10⁴ that is a real cost, imposed purely by the absence of a callback. With finalize the user writes one call and checkpoints from inside it, exactly as they would with DMRG2 or VUMPS.

The gap

finalize is the headline, but comparing src/algorithms/groundstate/idmrg.jl against src/algorithms/groundstate/dmrg.jl there is a broader set of things the infinite algorithms do not have:

DMRG/DMRG2 IDMRG/IDMRG2
finalize callback yes, finalize(iter, ψ, H, envs) once per sweep no
convergence measure per-bond Galerkin errors ϵ_locals, ϵ_global = maximum(ϵ_locals) norm(C - C_old)
truncation-aware stopping yes, ϵ_global <= max(tol, maximum(ϵ_truncs)) no, bare ϵ ≤ tol
adaptive local eigensolver adapt_solver(...; decay_rate, g_local, g_global, eps_trunc), AdaptiveKrylov by default adapt_solver(...; iter, g_global) only
in-place variant find_groundstate! no
bond expansion hooks alg_expand, alg_gauge (DMRG3S, …) alg_gauge only, and only for the final re-gauge
shared sweep driver local_update! + _num_updates/_sweep_ranges two hand-written 200-line sweep functions

The last row is the reason the rest of the rows look the way they do: _localupdate_sweep_idmrg! and _localupdate_sweep_idmrg2! inline the whole sweep — local update, gauge, environment transfer, edge handling — so there is no seam at which to add per-bond bookkeeping or a callback.

Proposal

Restructure idmrg.jl to mirror dmrg.jl:

  1. Add finalize::F = Defaults._finalize to IDMRG and IDMRG2, called once per sweep with (iter, ψ, H, envs), matching the DMRG/DMRG2/VUMPS signature.
  2. Factor the sweeps into a per-update local_update!(site, direction, ψ, O, alg, envs, …) plus a shared driver, with _num_updates/_sweep_ranges equivalents for the infinite geometry (the edge/wrap-around updates are the tricky part and need to stay explicit).
  3. Once there is a per-update seam: track per-bond ϵ_locals and ϵ_truncs, so the infinite algorithms get the same truncation-aware convergence criterion and can drive AdaptiveKrylov with g_local/decay_rate/eps_trunc rather than only the global error.
  4. Keep norm(C - C_old) available, but consider reporting the Galerkin error as the primary convergence measure for consistency with the finite and VUMPS paths.

Points 1 and 2 are the ones that matter for the reported use case; 3 and 4 are the payoff for doing 2 properly rather than only adding a field.

Backwards compatibility

Adding finalize is additive. Changing the reported convergence measure is not, so that part should probably be staged separately or kept behind the existing ϵ semantics until a breaking release.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions