DMRG3S: strictly single-site DMRG subspace expansion (Hubig et al. 2015)#460
DMRG3S: strictly single-site DMRG subspace expansion (Hubig et al. 2015)#460VinceNeede wants to merge 10 commits into
Conversation
|
Your PR no longer requires formatting changes. Thank you for your contribution! |
Codecov Report❌ Patch coverage is
... and 79 files with indirect coverage changes 🚀 New features to boost your workflow:
|
|
Thanks for this, I had a brief look and I think it already looks quite well. Considering the expansion algorithm itself - without having looked in a lot of detail so definitely feel free to tell me I'm wrong - I think it could be nice to avoid having both an For the TDVP implementation, I definitely agree to flush this one out first, and honestly it's probably easier to tackle and review that in a separate PR. Considering your other question, I'm definitely happy to add the |
Add `FunctionalSchedule`, wrapping an arbitrary `(noise, iter, ϵ) -> noise` callable as a `NoiseSchedule`. Overload `∘` on `NoiseSchedule` to compose two schedules into a `FunctionalSchedule`, following the usual function composition convention (`s2` applied first, `s1` applied to its result). Add a `threshold` keyword to `ExponentialDecay`, snapping the decayed noise to exactly zero once it falls below it -- avoids running the (cheap but non-free) expansion step indefinitely on a vanishingly small amplitude. This also makes `ExponentialDecay ∘ Warmup` a natural way to combine a hard iteration cutoff with smooth decay.
776835c to
f2c34cc
Compare
fix typo in variable name
|
Hi @lkdvos, Pushed a sketch of merging I haven't wired up Since |
lkdvos
left a comment
There was a problem hiding this comment.
Thanks for rebasing onto that other work!
Considering the DMRG constructor, I think I agree that we should exclude being able to set conflicting schemes, but unless I'm missing something that would already not be possible - the gauging algorithm is either a (truncated) orth algorithm or DMRG3S, and in the latter case there is only a single (truncated) orth algorithm in the DMRG3S.
If at all possible, I would like to design the DMRG constructor to be able to take the trscheme still, and have that routed through automatically, or error if both a trscheme and DMRG3S are provided. My experience is that while these "algorithm structs" are very convenient as a developer, it is somewhat harder as a newcomer to the package to keep track of everything, and a simple keyword-argument approach, which possibly does not expose all settings but handles the common cases, helps alleviate some of these issues.
| LoggingExtras.withlevel(; alg.verbosity) do | ||
| @infov 2 loginit!(log, ϵ_global, expectation_value(ψ, H, envs)) | ||
| for iter in 1:(alg.maxiter) | ||
| alg_gauge = _update_alg_gauge(alg.alg_gauge, iter, ϵ_global) |
There was a problem hiding this comment.
A comment here but this involves the other parts of this code:
It might be beneficial to consider passing iter to the local_update! function, such that this alg update can be handled inside that function, similar to how that is handled for the dynamic eigensolver settings.
I very much like the warmup and decay options, so it might even make sense to also unify that into there anyways (in a follow-up PR for sure though, let's try and keep things moving).
The main reason I'm suggesting this is that it is slightly unfortunate to have to pass alg_gauge, even though that is already contained in alg, so it feels slightly more natural to me to pass iter.
There was a problem hiding this comment.
The reason I decided not to pass iter and instead pass alg_gauge is that alg_gauge is only updated once per sweep. Moving the update inside local_update! would mean calling it unnecessarily at every site. I could make the struct mutable to avoid the extra allocations, but there would still be the call overhead. Another option would be to have the scheduler update at each site instead of once per iteration — in the original formulation, it was actually supposed to monitor the change in energy before and after truncation and use that to adapt the noise.
Alternatively, we could update alg (the DMRG struct) in place each sweep instead of threading alg_gauge through as a separate argument — local_update! would then just read alg.alg_gauge directly, which avoids the extra parameter without needing to call the update per site. Either DMRG or DMRG3S would need to become mutable for this to work.
| return iszero(noise) ? NoExpand(alg.alg_gauge) : DMRG3S(noise, alg.schedule, alg.alg_gauge) | ||
| end | ||
|
|
||
| function _get_combiner(::Type{T}, V1, V2) where {T} |
There was a problem hiding this comment.
I think this function is the same as fuser, which already exists somewhere in our utility.jl file I think. You can recover the fused space via only(domain(F)) I think.
There was a problem hiding this comment.
I tried using fuser, but it is restricted to IndexSpace only, while right_virtualspace(H[pos]) returns something like BlockTensorKit.SumSpace{GradedSpace{ProductSector{Tuple{TensorKitSectors.PlanarTrivial, Z2Irrep}}, Tuple{Int64, Int64}}}
Could we extend the method? or is the isomorphism not uniquely defined in other cases?
| V = right_virtualspace(AC) | ||
| combiner, Vpert = _get_combiner(T, V, right_virtualspace(Hi)) | ||
|
|
||
| @plansor pert[-1 -2; -3] := α * El[-1 1; 2] * AC[2 3; 4] * Hi[1 -2; 3 5] * combiner[4, 5; -3] |
There was a problem hiding this comment.
I think this contraction might be the same as our AC_projection contractions, and it could be beneficial to reuse that - the main reason is that we also support density-matrix MPS, which have 2 physical legs and would break this contraction, and reusing that function automatically fixes that.
There was a problem hiding this comment.
I already took a look at AC_projection, and from my understanding, it requires to supply all the enviroments, both left and right, here instead only one is required at a time. I'm not sure it can be adapted to this use case
There was a problem hiding this comment.
yes, but the combiner can be thought of as an environment in this case, so while the code routing is a bit more complex, the contraction is the same as this one:
MPSKit.jl/src/algorithms/derivatives/mpo_derivatives.jl
Lines 69 to 70 in 5bbab08
Summary
Implements DMRG3S — strictly single-site DMRG with subspace expansion — from Hubig, McCulloch, Schollwöck & Wolf, Phys. Rev. B 91, 155115 (2015). Unlike CBE (
alg_expand), which enriches the bond before the local eigensolve, DMRG3S enriches it after, using a perturbation built from the just-optimized tensor, the local Hamiltonian, and the environment. This lets single-site DMRG escape local minima caused by missing quantum-number sectors that CBE-style pre-expansion can't reach.This PR also targets TDVP, pending design agreement
DMRG only for now. TDVP support belongs in this same PR, but I'd like design agreement on the interface below first, since it directly shapes how it plugs into TDVP too. Docs are also on hold for the same reason — no point documenting an interface that might still change.
What's here
find_groundstate!'s left-to-right and right-to-left sweeps from two separate, near-duplicate loops into a single loop overdirection in (:right, :left), dispatching onVal. No behavioral change; this just made room for the new expansion step without doubling its code.alg_post_expandfield onDMRG, dispatching a newpost_expand!step after the eigensolve, alongside the existing pre-eigensolvealg_expand. Default (NoExpand()) reproduces current behavior exactly.DMRG3S, with a composableNoiseSchedulefor the mixing factor (ExponentialDecay,Warmup,∘for combining them).alg_expandandalg_post_expandare independent and can be used together or separately.alg_gauge, since the bond would otherwise grow unboundedly each sweep.Open design questions for maintainers
alg_post_expandsentinel: I used a dedicatedNoExpand()type rather thannothing(whichalg_expanduses). This letsNoExpandcarry its ownpost_expand!method for dispatch, but it's an inconsistency withalg_expand's convention worth flagging.post_expand!call rather than kept separate, because splitting them left the MPS briefly inconsistent between steps (caught via a real bug during development — happy to share details). This does meanpost_expand!needsalg_gaugethreaded in as a parameter, coupling it to the gauge step more tightly thanalg_expandis.TODO before merge