Skip to main content

ariadnetor_algorithms/dmrg/heff_block_sparse/
mod.rs

1//! BlockSparse / U(1) variant of the 2-site DMRG local update.
2//!
3//! Mirrors [`super::heff`] (the Dense path) for a
4//! `BlockSparseTensorData<T, S>`-backed chain. The effective
5//! Hamiltonian built from `(left(i), W[i], W[i+1], right(i+2))` is
6//! exposed as a [`crate::krylov::LinearOp<T>`] so the existing Dense
7//! Krylov solvers (Lanczos by default, ARPACK behind the `arpack`
8//! feature) drive it without a separate native BlockSparse Krylov
9//! path.
10//!
11//! ## Flat-buffer adapter
12//!
13//! `LinearOp<T>` operates on `DenseTensor<T>` flat vectors. The
14//! BlockSparse Heff implements `apply(&DenseTensor<T>) -> DenseTensor<T>` via:
15//!
16//! 1. **Scatter** the flat input into a populated
17//!    `BlockSparseTensorData` 2-site tensor whose indices and flux
18//!    match the psi template derived from the MPS sites at
19//!    `(site, site+1)`.
20//! 2. **Contract** through the env / W tensors using
21//!    [`ariadnetor_linalg::tensordot`] in four steps. The
22//!    axis convention mirrors `ariadnetor_mps::inner::braket_bsp` and
23//!    the Phase 6.1 `extend_*_step` kernels; the natural output
24//!    order `lhs_free | rhs_free` ends in
25//!    `[chi_l, d_i, d_{i+1}, chi_r]`, matching the input shape with
26//!    no axis swap.
27//! 3. **Gather** the rank-4 result back into a flat `DenseTensor<T>`
28//!    of the same length, walking the psi template's
29//!    [`BlockSparseTensorData::block_metas`] and looking up each block
30//!    in the contracted output by coordinate.
31//!
32//! Symmetry preservation is structural: the psi template only
33//! allocates flux-allowed blocks, and `tensordot`
34//! propagates flux as `lhs.flux().fuse(rhs.flux())`. With env /
35//! MPO fluxes pre-validated to identity at the entry point, the
36//! matvec output's flux equals `psi.flux()` by construction.
37
38mod operator;
39mod validation;
40
41#[cfg(test)]
42mod tests;
43
44pub(crate) use operator::EffectiveHamiltonian2SiteBlockSparse;
45
46use ariadnetor_core::Scalar;
47use ariadnetor_linalg::{BlockScalars, TruncSvdParams, trunc_svd};
48use ariadnetor_mps::{Mpo, Mps};
49use ariadnetor_tensor::{BlockSparseLayout, BlockSparseStorage, BlockSparseTensor, Host, Sector};
50
51#[cfg(feature = "arpack")]
52use crate::krylov::arpack_smallest;
53use crate::krylov::lanczos_smallest;
54
55use super::heff_error::DmrgHeffError;
56use super::solver::{DmrgScalar, LocalEigensolverParams};
57use ariadnetor_mps::BraketEnvs;
58
59/// Result of a single BlockSparse 2-site DMRG step: smallest local
60/// eigenpair plus the truncated-SVD split of its eigenvector.
61///
62/// `u`, `s`, `vt` are returned **separately** so the caller (the
63/// BlockSparse sweep driver
64/// [`super::sweep_2site`]) decides which side
65/// absorbs `S`. Mirrors [`super::heff::TwoSiteStepResult`] for the
66/// Dense path.
67///
68/// `Debug` is not derived because `BlockSparse: !Debug`; tests that
69/// need to inspect the result destructure its fields directly.
70pub struct TwoSiteStepResultBlockSparse<T: Scalar, S: Sector> {
71    /// Local-block variational minimum (smallest `H_eff` eigenvalue).
72    pub eigenvalue: T::Real,
73    /// Local-eigensolver true residual `‖H v − λ v‖₂`.
74    pub residual: T::Real,
75    /// Number of local-eigensolver iterations.
76    pub iters: usize,
77    /// Whether the local eigensolver reported convergence (Lanczos by
78    /// its absolute true-residual test, ARPACK by its relative-tol
79    /// stopping criterion).
80    pub converged: bool,
81    /// Left singular vectors. Legs `[chi_l, d_i, bond(In)]`,
82    /// `flux = identity()`. Left-canonical at axes `(chi_l, d_i)`.
83    pub u: BlockSparseTensor<T, S>,
84    /// Singular values per fused sector (descending within each
85    /// sector).
86    pub s: BlockScalars<<T as Scalar>::Real, S>,
87    /// Right singular vectors. Legs `[bond(Out), d_{i+1}, chi_r]`,
88    /// `flux = psi_flux`. Right-canonical at axes `(d_{i+1}, chi_r)`.
89    pub vt: BlockSparseTensor<T, S>,
90    /// Frobenius norm of the discarded singular values.
91    pub trunc_err: T::Real,
92}
93
94/// Run a single 2-site DMRG step at sites `(site, site+1)` on a
95/// BlockSparse-backed chain. Mirrors [`super::heff::dmrg_2site_step`]
96/// for the BlockSparse / U(1) path.
97///
98/// Builds the local effective Hamiltonian, drives the selected
99/// local eigensolver (per [`LocalEigensolverParams`] — Lanczos by
100/// default, ARPACK behind the `arpack` feature) via the flat-buffer
101/// adapter, then splits the optimized two-site block via
102/// [`trunc_svd`] with `nrow = 2`. Caller
103/// advances envs separately.
104///
105/// # Errors
106///
107/// - [`DmrgHeffError::InvalidSite`], [`DmrgHeffError::LengthMismatch`],
108///   [`DmrgHeffError::StaleEnv`], [`DmrgHeffError::ShapeMismatch`],
109///   [`DmrgHeffError::InvalidEigensolverParams`],
110///   [`DmrgHeffError::Contract`] — same semantics as
111///   [`super::heff::dmrg_2site_step`].
112/// - [`DmrgHeffError::QnMismatch`] — BlockSparse-specific QN /
113///   Direction / sector / per-site-flux compatibility check
114///   failed. The matvec body's `.expect` calls cannot fire on user
115///   input because every contract pair, MPO well-formedness
116///   property, env-template-compatibility property, and identity-flux
117///   precondition is validated up front.
118/// - [`DmrgHeffError::OrderMismatch`] — BlockSparse-specific: a
119///   contracted operand's layout order diverged from the host
120///   substrate's preferred order (surfaced when building the effective
121///   Hamiltonian).
122/// - [`DmrgHeffError::Lanczos`] — the native Lanczos local eigensolver
123///   produced a non-finite eigenpair. With the `arpack` feature, the
124///   ARPACK arm can instead return `DmrgHeffError::Arpack`.
125pub(crate) fn dmrg_2site_step_block_sparse<T, S>(
126    envs: &BraketEnvs<BlockSparseStorage<T>, BlockSparseLayout<S>>,
127    mps: &Mps<BlockSparseStorage<T>, BlockSparseLayout<S>>,
128    mpo: &Mpo<BlockSparseStorage<T>, BlockSparseLayout<S>>,
129    site: usize,
130    eigensolver: &LocalEigensolverParams,
131    trunc: &TruncSvdParams,
132) -> Result<TwoSiteStepResultBlockSparse<T, S>, DmrgHeffError>
133where
134    T: DmrgScalar,
135    T::Real: Scalar<Real = T::Real>,
136    S: Sector,
137{
138    let v = validation::validate_inputs(envs, mps, mpo, site, eigensolver)?;
139
140    let heff = EffectiveHamiltonian2SiteBlockSparse::new(
141        v.left, v.w_i, v.w_ip1, v.right, v.mps_i, v.mps_ip1,
142    )?;
143    let dim = heff.dim();
144    if dim == 0 {
145        // Per-axis `total_dim() >= 1` checks in `validate_inputs`
146        // ensure individual outer axes are non-empty, but the
147        // combined `psi_flux = mps_i.flux ⊕ mps_ip1.flux` may
148        // still be unreachable on the (axis_0 × axis_1 × axis_1
149        // × axis_2) sector lattice — in which case the
150        // `BlockSparseTensorData::zeros(...)` template allocates
151        // zero blocks. Without this check the underlying solver's
152        // `dim >= 1` precondition fires (Lanczos panics, ARPACK
153        // rejects with `InvalidParam`) on otherwise valid user
154        // input. Doing the check here (post `new`) avoids a second
155        // template allocation that a validation-time check would
156        // have required.
157        return Err(DmrgHeffError::QnMismatch {
158            site,
159            field: "psi_template",
160            detail: format!(
161                "no flux-allowed (q_l, q_p, q_p, q_r) tuple satisfies psi_flux = {:?} \
162                 given MPS[i].axis 0 = {:?}, MPS[i].axis 1 = {:?}, \
163                 MPS[i+1].axis 1 = {:?}, MPS[i+1].axis 2 = {:?}",
164                heff.psi_flux(),
165                v.mps_i.indices()[0].blocks(),
166                v.mps_i.indices()[1].blocks(),
167                v.mps_ip1.indices()[1].blocks(),
168                v.mps_ip1.indices()[2].blocks(),
169            ),
170        });
171    }
172    let (eigenvalue, eigenvector, iters, converged, residual) = match eigensolver {
173        LocalEigensolverParams::Lanczos(p) => {
174            let lan = lanczos_smallest::<T, _>(&heff, dim, p)?;
175            (
176                lan.eigenvalue,
177                lan.eigenvector,
178                lan.iters,
179                lan.converged,
180                lan.residual,
181            )
182        }
183        #[cfg(feature = "arpack")]
184        LocalEigensolverParams::Arpack(p) => {
185            let res = arpack_smallest::<T, _>(&heff, dim, p)?;
186            // See `super::heff::dmrg_2site_step` ARPACK arm for the
187            // rationale: the step-level converged flag tracks
188            // ARPACK's relative-tol stopping (Ok return), not the
189            // absolute-tol divergence indicator that ARPACK exposes
190            // as `ArpackResult.converged`.
191            (
192                res.eigenvalue,
193                res.eigenvector,
194                res.iters,
195                true,
196                res.residual,
197            )
198        }
199    };
200
201    let psi_4d = operator::scatter_flat_to_template(
202        eigenvector.data_slice(),
203        &heff.psi_template,
204        &heff.block_offsets,
205        &heff.block_coords,
206    );
207    let (u, s, vt, trunc_err) = trunc_svd(Host::shared().as_ref(), &psi_4d, 2, trunc)?;
208
209    Ok(TwoSiteStepResultBlockSparse {
210        eigenvalue,
211        residual,
212        iters,
213        converged,
214        u,
215        s,
216        vt,
217        trunc_err,
218    })
219}