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:
- Add
finalize::F = Defaults._finalize to IDMRG and IDMRG2, called once per sweep with (iter, ψ, H, envs), matching the DMRG/DMRG2/VUMPS signature.
- 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).
- 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.
- 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.
DMRG/DMRG2andIDMRG/IDMRG2have 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 thefinalizecallback, but it is not the only one, and I think it is worth restructuringidmrg.jlalong the lines ofdmrg.jlrather than bolting on a single field.Motivation
From #480: a user running
IDMRG2at large bond dimension wants to checkpoint the state every couple of sweeps. Since there is nofinalizehook, the only way to do that is to chunk the sweeps across separatefind_groundstatecalls:That is not just inelegant. Every
find_groundstatecall ends withso chunking pays a full re-gauge plus an environment recalculation every
maxitersweeps. At χ ~ 10³–10⁴ that is a real cost, imposed purely by the absence of a callback. Withfinalizethe user writes one call and checkpoints from inside it, exactly as they would withDMRG2orVUMPS.The gap
finalizeis the headline, but comparingsrc/algorithms/groundstate/idmrg.jlagainstsrc/algorithms/groundstate/dmrg.jlthere is a broader set of things the infinite algorithms do not have:DMRG/DMRG2IDMRG/IDMRG2finalizecallbackfinalize(iter, ψ, H, envs)once per sweepϵ_locals,ϵ_global = maximum(ϵ_locals)norm(C - C_old)ϵ_global <= max(tol, maximum(ϵ_truncs))ϵ ≤ toladapt_solver(...; decay_rate, g_local, g_global, eps_trunc),AdaptiveKrylovby defaultadapt_solver(...; iter, g_global)onlyfind_groundstate!alg_expand,alg_gauge(DMRG3S, …)alg_gaugeonly, and only for the final re-gaugelocal_update!+_num_updates/_sweep_rangesThe 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.jlto mirrordmrg.jl:finalize::F = Defaults._finalizetoIDMRGandIDMRG2, called once per sweep with(iter, ψ, H, envs), matching theDMRG/DMRG2/VUMPSsignature.local_update!(site, direction, ψ, O, alg, envs, …)plus a shared driver, with_num_updates/_sweep_rangesequivalents for the infinite geometry (the edge/wrap-around updates are the tricky part and need to stay explicit).ϵ_localsandϵ_truncs, so the infinite algorithms get the same truncation-aware convergence criterion and can driveAdaptiveKrylovwithg_local/decay_rate/eps_truncrather than only the global error.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
finalizeis 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.