Add KPM energy truncation (pyitensor): narrower-than-bandwidth dynamical-correlator windows - #96
Merged
Merged
Conversation
…a window narrower than the full bandwidth Implements Holzner et al. (PRB 83, 195115 (2011)) Sec. III-B: after each Chebyshev vector is formed, projects out any locally-high-energy component (per-site Krylov subspace) before it can blow up over the recursion, letting kpm_scale be pushed below the previous safe floor for higher spectral resolution. Also adds a ground-state-anchored rescaling (Eq. 21b) used whenever truncation is enabled, since a correlator's real weight sits just above E0, not around the full bandwidth's midpoint -- the naive "just shrink kpm_scale" approach clips the ground state itself out of the window otherwise (found while building the accuracy tests). Off by default (kpm_energy_truncate=False); pyitensor backend only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DoZKWzhMo7qKoiP12gbcvv
… the existing v3 KPM path Implements Holzner et al. (PRB 83, 195115 (2011)) Sec. III-B directly in mpscpp3/chain_session.h using ITensor's own LocalMPO/diagHermitian, as a wholly independent code path: kpm_dynamical_correlator_truncated() is a separate method from kpm_dynamical_correlator(), and scaled_hamiltonian_ gs_anchored()/kpm_energy_truncate() are separate from scaled_hamiltonian()/ kpm_moments_full()/kpm_moments_accelerated() -- none of the existing, always-safe v3 KPM machinery is touched. kpmdmrg.py now dispatches to this new method for itensor_version=3 when kpm_energy_truncate is enabled, instead of going through the setter+branch pattern used for the pyitensor backend. Found and worked around a pre-existing, unrelated characteristic while testing: ITensor's Error() (used by the existing check_kpm_moment divergence guard) does not translate into a catchable Python exception across the pybind11 boundary here -- it aborts the whole process. Not fixed (out of scope), but test files are written to never deliberately trigger it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DoZKWzhMo7qKoiP12gbcvv
… catchable exception ITensor's Error(...) macro (itensor/util/error.h, vendored) prints its message and calls abort() unconditionally -- it never actually throws, despite ITError being a real std::runtime_error-derived exception type. Found while testing KPM energy truncation: a deliberately-too-narrow kpm_scale on itensor_version 2 or 3 killed the whole Python process with SIGABRT instead of raising a catchable RuntimeError, even though every existing except-RuntimeError-shaped caller (and the pyitensor backend's own equivalent check) assumed otherwise. Confirmed via a minimal reproduction (a print() issued immediately before the call never appeared in output, proving no controlled unwind/flush occurred) that this already affected the pre-existing, untouched kpm_dynamical_correlator path too, in both mpscpp2 and mpscpp3 -- not something introduced by the energy-truncation feature itself. Fixed by changing just this one call site in each file to `throw ITError(...)` directly, which pybind11 correctly auto-translates into a Python RuntimeError (ITError derives from std::runtime_error). Every other Error(...) call site in both files is left untouched -- those indicate genuinely broken internal preconditions, not a user-recoverable condition like an accidentally-too-tight kpm_scale. Added a focused regression test (test_kpm_divergence_guard_catchable.py, parametrized over all three backends) and filled in the "narrow kpm_scale without truncation raises" test in test_kpm_energy_truncation_v3_accuracy.py that was previously omitted because it wasn't safe to run. Verified with both mpscpp2 and mpscpp3 compiled in this worktree: full pytest suite now 289 passed, 0 skipped, 0 failed (previously 280 passed/9 skipped with only v3 compiled). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DoZKWzhMo7qKoiP12gbcvv
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
src/dmrgpy/pyitensor/kpm_energy_truncation.py(energy_truncate()), wired as an opt-in branch insidechain.py's existing_kpm_moments_full/_kpm_moments_accelerated.mpscpp3/chain_session.h'skpm_dynamical_correlator_truncated()/kpm_energy_truncate()/scaled_hamiltonian_gs_anchored(), implemented as wholly independent methods from the existingkpm_dynamical_correlator()/kpm_moments_full()/kpm_moments_accelerated()/scaled_hamiltonian()— none of that existing, always-safe code path is touched. Built using ITensor's ownLocalMPO/diagHermitianmachinery (the same primitives ITensor's own two-sitedmrg()uses for its local effective Hamiltonian).kpm_scalebe pushed below its previous safe floor for higher spectral resolution, instead of always rescaling to the full many-body bandwidth.kpm_energy_truncateis enabled on either backend: found directly while building the accuracy tests that naively narrowing the existing bandwidth-midpoint-centered window clips the ground state itself out of the window before it's ever narrow enough to be useful, since the ground state sits at the spectrum's edge, not its middle.Many_Body_Chainattributes:kpm_energy_truncate(defaultFalse),kpm_truncate_dK,kpm_truncate_nsweeps,kpm_truncate_threshold. Off by default — existingkpm_scalebehavior and golden-value tests are unaffected. Available foritensor_version"python"and3; noitensor_version=2port (mpscpp2 has no equivalent local-effective-Hamiltonian machinery).kpm_scale0.7→0.65): pyitensor costs ~5.5–12.5x more per Chebyshev moment; v3's native port ~15–32x more on the same small system (growing to ~245x per moment at 8 sites,dK=30/nsweeps=10) — the per-site Krylov cost scales with bond dimension. See the user guide's new "Performance note".Bug fix found and fixed along the way
While testing the divergence path (a deliberately too-narrow
kpm_scale), found thatchain_session.h'scheck_kpm_moment()divergence guard — in bothmpscpp2andmpscpp3, pre-existing code, unrelated to this feature — called ITensor'sError(...)macro, which prints a message and callsabort()unconditionally; it never actually throws, despiteITErrorbeing a realstd::runtime_error-derived exception type. This meant a too-tightkpm_scaleonitensor_version2 or 3 killed the whole Python process withSIGABRTinstead of raising a catchableRuntimeError, even though every existingexcept RuntimeError-shaped caller (and the pyitensor backend's own equivalent check) assumed otherwise. Fixed by changing just that one call site in each file tothrow ITError(...)directly (pybind11 auto-translates this to Python'sRuntimeError); every otherError(...)call site in both files — which really do indicate broken internal preconditions — is untouched. New regression test:tests/test_kpm_divergence_guard_catchable.py, parametrized over all three backends.Test plan
tests/test_kpm_energy_truncation.py— standalone pyitensor coverage ofenergy_truncate()itself (no-op when the window is already wide enough; actually removes high-energy weight under a narrow window; independently-recomputed postcondition check on every site's local Krylov spectrum).tests/test_kpm_energy_truncation_accuracy.py— end-to-end pyitensor coverage throughget_dynamical_correlator(submode="KPM"), cross-checked against exact ED.tests/test_kpm_energy_truncation_v3.py— standalone v3 coverage ofkpm_energy_truncate/scaled_hamiltonian_gs_anchored(no-op at full window; real truncation + convergence at a narrow one; ground state stays pinned at anykpm_scale), skipped when the v3 extension isn't compiled.tests/test_kpm_energy_truncation_v3_accuracy.py— end-to-end v3 coverage throughget_dynamical_correlator, including the narrow-window-without-truncation-raises baseline (now safe to test, see bug fix above), a direct cross-backend check against the pyitensor port, and against exact ED.tests/test_kpm_divergence_guard_catchable.py— regression test for the abort-vs-raise bug fix, across all three backends.examples/dynamical_correlator/dynamical_correlator_kpm_energy_truncation— worked example (including reproducing the divergence this fixes, on purpose).docs/user_guide.md/.texupdated for both backends + a performance-note;.texverified to compile cleanly withpdflatex(no errors, no new overfull-hbox warnings).pytest tests/run with both v2 and v3 extensions compiled (via the documented worktree-local build trick, copying the main checkout's prebuiltlibitensor.afor each): 289 passed, 0 skipped, 0 failed, no process aborts anywhere in the suite.🤖 Generated with Claude Code
https://claude.ai/code/session_01DoZKWzhMo7qKoiP12gbcvv