Skip to content

feat: standalone linsolve primitives - #479

Draft
lkdvos wants to merge 7 commits into
mainfrom
linsolve
Draft

feat: standalone linsolve primitives#479
lkdvos wants to merge 7 commits into
mainfrom
linsolve

Conversation

@lkdvos

@lkdvos lkdvos commented Jul 27, 2026

Copy link
Copy Markdown
Member

This PR adds dedicated routines for linsolve, i.e. solving Ax = b with MPO/MPS.
This was already partially implemented in the propagator and dynamical DMRG stuff, but the goal is to make it a bit more of a first-class citizen and not hardcode the specific linear problems to solve.
Can also be used for time-stepping purposes, doing things like trapezoid rules.

There definitely are some design choices that I'm happy to have discussions about, considering names etc, but one particular choice I made here is to not overload KrylovKit's version.
The reasoning behind this is that the envs were a bit awkward to get in there anyways, and KrylovKit does not have an in-place version to begin with. It does have the annoying side-effect of nameclashes, since I definitely want to export this, so maybe a different name could be useful?

lkdvos and others added 7 commits July 27, 2026 09:39
The `MPO_AC2_Hamiltonian{<:MPSBondTensor, Nothing, Nothing, <:MPSBondTensor}`
action (the operator-free / overlap two-site derivative) was missing a
semicolon in the `rightenv` index, declaring a `(2, 0)` index pattern for a
`(1, 1)` bond tensor. This threw a `TensorOperations.IndexError` for the planar
bond-tensor case (e.g. `approximate(x, b, DMRG2())` on planar states); the
ComplexSpace path happened to avoid the broken method.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Use the fully-qualified `KrylovKit.linsolve` at the internal transfer-matrix /
quasiparticle / fidelity call sites, so that the top-level `linsolve` name can
be owned by MPSKit for the upcoming MPS linear-solver category without shadowing
these tensor-level solves. Behavior-preserving.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a `truncation_factor` field to `DynamicTol` and let `adapt_solver` floor the
retuned tolerance at `truncation_factor * eps_trunc`, i.e.
`tol = clamp(max(truncation_factor*eps_trunc, tol_factor*g_global/sqrt(iter)), tol_min, tol_max)`.
The default `truncation_factor = 0` keeps existing behavior (DMRG's gauge /
eigensolver uses are unchanged); a positive value is used by truncating two-site
sweeps to avoid solving a bond far below what the SVD truncation will discard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a first-class `linsolve`/`linsolve!` for finite MPS, solving
`(a0 + a1*A)*x = b` with `A` an MPO/MPOHamiltonian and `x`, `b` MPS. The
signature is state-first (`linsolve(x0, A, b, [alg], [envs]; a0=0, a1=1, ...)`)
and returns `(x, envs, eps)`, matching find_groundstate/approximate; the shift
follows KrylovKit's convention.

Algorithms are DMRG-style sweeps `DMRGSolve` (single-site) and `DMRGSolve2`
(two-site, rank-adaptive), each parameterized by a local `LinsolveFormulation`:
`Galerkin` (solve the effective system directly; CG/GMRES/BiCGStab) and
`LeastSquares` (normal equations via squared environments). Convergence uses the
local Galerkin residual `||(a0 + a1*A_eff)*AC - b_eff|| / ||b||`, formed as a
local tensor (machine-precision floor, no catastrophic cancellation). The
two-site stop is truncation-aware.

Local solves default to adaptive tolerances via `Defaults.alg_linsolve()` (a
structure-selected CG/MINRES/GMRES wrapped in `DynamicTol`, with a truncation
floor for two-site); pass a plain solver to disable adaptation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cover the resolvent `(z - H)x = V|gs>` against the analytic `1/(z - E0)` and the
existing `propagator` (both Galerkin and LeastSquares, single- and two-site), a
positive-definite solve via the keyword interface (CG path), adaptive tolerances
including BiCGStab, and a general RHS checked against a dense solve. Residuals
are verified densely (MPS-level +/- does not reliably cancel, cf. #473).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three fixes to the local solves:

- `Defaults.alg_linsolve(; ishermitian = true)` returned a `MINRES` solver, but
  KrylovKit exports the type without implementing `linsolve` for it (see its own
  "TODO: implement MINRES for symmetric but not posdef; for now use GMRES"), so
  the hermitian-indefinite path threw a `MethodError`. Fall back to GMRES too.
- `@doc str a, b` attaches only to the last binding, so exported `linsolve` had
  no docstring at all. Share it explicitly with `@doc (@doc linsolve) linsolve!`.
- A local solve no longer warns about non-convergence when it undershot its own
  adaptive tolerance but still reached `tol * norm(b)`, the accuracy the outer
  sweep needs. Adaptive tolerances routinely dip below the round-off floor of
  the local problem, which made the warning fire on nearly every tightly
  converged sweep.

`LeastSquares` now forms the `A†b` term of the normal equations from the mixed
sandwich <x|A|b> instead of materializing `A*b` as an MPS, avoiding a state of
bond dimension chi_A*chi_b together with its environments. The JordanMPO
effective operator assumes `below === above` and branching on that inside
`AC_hamiltonian` would make the return type of that hot function a union, so the
mixed case goes through the generic sparse MPO derivative in `_mixed_projection`
-- one application per bond, so its cost is irrelevant. This also makes the
formulation usable on `WindowMPS`, which the `A*b` route never could be: there
is no `*(::WindowMPOHamiltonian, ::WindowMPS)`.

Also adds the missing in-place `linsolve!(x0, A, b, ::UnionAlg, envs...)`, the
counterpart of the existing copying method.

Tests cover two-site `LeastSquares` and a `WindowMPS` resolvent for both
formulations.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`corvector.jl` carried its own DMRG sweep, which had drifted from the one in the
`linsolve` category on three counts: it converged on the per-site update norm
||AC' - AC|| (which cannot tell converged from stalled) rather than on the
residual of the linear system, it used a fixed-tolerance GMRES rather than the
structure-selected adaptive solver, and it was single-site only. `propagator` is
now a single method for both flavours whose body is a `linsolve!` call.

Both flavours keep their variational problem unchanged. `NaiveInvert` is a
`Galerkin` solve of `(z - H)x = |psi0>`. `Jeckelmann` still minimizes functional
(14) of Jeckelmann2002 and reconstructs G(z) through equation (11): its operator
is assembled as `LinearCombination((H^2, H), (1, -2w))` over a hand-built
`LazyLincoCache` -- hand-built so that `squaredenvs`' boundary conditions, which
matter for `WindowMPS`, survive -- and its stationarity condition
`((w - H)^2 + eta^2) psi = -eta |psi0>` is scaled by `-1/eta` so that the
right-hand side is `|psi0>` itself. No scaled copy of the right-hand side is
needed and `x` is the identical vector; the shift comes out real, so a real
initial guess stays real.

Consequent changes to the defaults of `DynamicalDMRG`:

- `tol` defaults to `Defaults.tol` rather than `10 * Defaults.tol`, and now
  measures the relative residual of the linear system.
- `solver` defaults to `Defaults.alg_linsolve()` (adaptive tolerances), chosen
  per flavour through an explicit keyword constructor.
- a new `trscheme` field prepends a truncated two-site sweep, so the bond
  dimension no longer has to be fixed by the initial guess.
- iteration logs are labelled `linsolve` instead of `DDMRG`.
- the `Jeckelmann` flavour throws for real `z` instead of returning `NaN`; the
  equation-(11) reconstruction divides by `imag(z)`.

The docstrings claimed that the second return value is `1/(z - H)|psi0>` for
both flavours. That is only true for `NaiveInvert`; for `Jeckelmann` it is the
minimizer of the functional, i.e. the imaginary part of the propagator. Say so.

Adds a `linsolve` section to the algorithms manual: the category was exported
but appeared nowhere in the docs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Jutho

Jutho commented Jul 28, 2026

Copy link
Copy Markdown
Member

It might be good (or not good because to broad) to consider the names of all the high level methods, and try to make them more consistent and streamlined. This would of course be massively breaking, but on the other hand, renaming a method is probably the easiest type of breaking change in terms of deprecations, and the old name can stay around for a very very long time. So what do we currently have:

  • find_groundstate
  • excitations
  • leading_boundary
  • linsolve
  • exact_diagonalization
  • propagator
  • approximate

and maybe

  • changebonds

The current naming is certainly all over the place 😄 .

@Jutho

Jutho commented Jul 28, 2026

Copy link
Copy Markdown
Member

That being said, I do think the current situation is already much nicer than some of the other tensor network packages, which often use the specific algorithm as a method name, and certainly also have a wild variety of names around.

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.

2 participants