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}