Skip to content

Add KPM energy truncation (pyitensor): narrower-than-bandwidth dynamical-correlator windows - #96

Merged
joselado merged 3 commits into
masterfrom
kpm-energy-truncation
Jul 30, 2026
Merged

Add KPM energy truncation (pyitensor): narrower-than-bandwidth dynamical-correlator windows#96
joselado merged 3 commits into
masterfrom
kpm-energy-truncation

Conversation

@joselado

@joselado joselado commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • Implements KPM energy truncation for finite chains per Holzner, Weichselbaum, McCulloch & von Delft, "Chebyshev matrix product state approach for spectral functions", PRB 83, 195115 (2011), Sec. III-B, on two independent backends:
    • pyitensor: src/dmrgpy/pyitensor/kpm_energy_truncation.py (energy_truncate()), wired as an opt-in branch inside chain.py's existing _kpm_moments_full/_kpm_moments_accelerated.
    • ITensor v3 (C++): mpscpp3/chain_session.h's kpm_dynamical_correlator_truncated()/kpm_energy_truncate()/scaled_hamiltonian_gs_anchored(), implemented as wholly independent methods from the existing kpm_dynamical_correlator()/kpm_moments_full()/kpm_moments_accelerated()/scaled_hamiltonian() — none of that existing, always-safe code path is touched. Built using ITensor's own LocalMPO/diagHermitian machinery (the same primitives ITensor's own two-site dmrg() uses for its local effective Hamiltonian).
  • After every Chebyshev vector is formed, a dedicated sweep projects out any locally-high-energy component (per-site Krylov subspace diagonalization + threshold cut) before it can blow up over the recursion — this is what lets kpm_scale be pushed below its previous safe floor for higher spectral resolution, instead of always rescaling to the full many-body bandwidth.
  • Also adds a ground-state-anchored rescaling convention (the paper's own Eq. 21b), used automatically whenever kpm_energy_truncate is 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.
  • New Many_Body_Chain attributes: kpm_energy_truncate (default False), kpm_truncate_dK, kpm_truncate_nsweeps, kpm_truncate_threshold. Off by default — existing kpm_scale behavior and golden-value tests are unaffected. Available for itensor_version "python" and 3; no itensor_version=2 port (mpscpp2 has no equivalent local-effective-Hamiltonian machinery).
  • Performance: this is a controlled slowdown, not a speedup — it buys resolution/feasibility, not raw speed. Measured directly (4-site chain, kpm_scale 0.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 that chain_session.h's check_kpm_moment() divergence guard — in both mpscpp2 and mpscpp3, pre-existing code, unrelated to this feature — called ITensor's Error(...) macro, which prints a message and calls abort() unconditionally; it never actually throws, despite ITError being a real std::runtime_error-derived exception type. This meant a too-tight 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. Fixed by changing just that one call site in each file to throw ITError(...) directly (pybind11 auto-translates this to Python's RuntimeError); every other Error(...) 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 of energy_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 through get_dynamical_correlator(submode="KPM"), cross-checked against exact ED.
  • tests/test_kpm_energy_truncation_v3.py — standalone v3 coverage of kpm_energy_truncate/scaled_hamiltonian_gs_anchored (no-op at full window; real truncation + convergence at a narrow one; ground state stays pinned at any kpm_scale), skipped when the v3 extension isn't compiled.
  • tests/test_kpm_energy_truncation_v3_accuracy.py — end-to-end v3 coverage through get_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/.tex updated for both backends + a performance-note; .tex verified to compile cleanly with pdflatex (no errors, no new overfull-hbox warnings).
  • Full pytest tests/ run with both v2 and v3 extensions compiled (via the documented worktree-local build trick, copying the main checkout's prebuilt libitensor.a for each): 289 passed, 0 skipped, 0 failed, no process aborts anywhere in the suite.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DoZKWzhMo7qKoiP12gbcvv

joselado and others added 3 commits July 30, 2026 04:00
…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
@joselado
joselado marked this pull request as ready for review July 30, 2026 10:08
@joselado
joselado merged commit 51357d8 into master Jul 30, 2026
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.

1 participant