Skip to main content

ariadnetor_algorithms/dmrg/
heff_error.rs

1//! Shared error type for the 2-site DMRG step entry points.
2//!
3//! Lives in its own module rather than alongside `dmrg_2site_step`
4//! because the same error type is also produced by
5//! `dmrg_2site_step_block_sparse` (BlockSparse path) — splitting it
6//! out keeps the per-storage entry points decoupled and keeps the
7//! Dense `heff.rs` under the per-file size cap as the operator + the
8//! ARPACK arm grow.
9
10use ariadnetor_linalg::LinalgError;
11
12#[cfg(feature = "arpack")]
13use crate::krylov::ArpackError;
14
15/// Errors raised by the 2-site DMRG step entry points
16/// (`dmrg_2site_step` for the Dense path, and
17/// `dmrg_2site_step_block_sparse` for the
18/// BlockSparse / U(1) path; both crate-internal). Most variants are
19/// produced by both; the
20/// [`DmrgHeffError::QnMismatch`] variant is BlockSparse-specific
21/// and only surfaces from the BlockSparse entry point's QN /
22/// Direction / sector / per-site-flux pre-validation.
23#[derive(Debug, thiserror::Error)]
24#[non_exhaustive]
25pub enum DmrgHeffError {
26    /// `site + 1` was not a valid two-site index for the chain.
27    #[error("two-site index {site} (with {site}+1) out of range for chain of length {n_sites}")]
28    InvalidSite {
29        /// The requested left site of the two-site block.
30        site: usize,
31        /// Chain length the index was checked against.
32        n_sites: usize,
33    },
34    /// The env slot required for the two-site step (`left(site)` for
35    /// the left side, `right(site + 2)` for the right side) was
36    /// `None`. Indicates the caller has not built / advanced the
37    /// envs into a state where this `site` can be optimized.
38    #[error(
39        "{side} env at index {index} is stale (None); build / advance envs into the right \
40         state before stepping"
41    )]
42    StaleEnv {
43        /// Which side's env is stale (`"left"` / `"right"`).
44        side: &'static str,
45        /// Index of the stale (`None`) env slot.
46        index: usize,
47    },
48    /// MPS and MPO chain lengths disagree, or disagree with the
49    /// envs the function was given.
50    #[error("chain length mismatch: mps = {mps}, mpo = {mpo}, envs = {envs}")]
51    LengthMismatch {
52        /// Site count reported by the MPS.
53        mps: usize,
54        /// Site count reported by the MPO.
55        mpo: usize,
56        /// Site count reported by the environments.
57        envs: usize,
58    },
59    /// The selected eigensolver's params (Lanczos or, behind the
60    /// `arpack` feature, ARPACK) violated their preconditions.
61    /// Surfaced here so callers see a fallible `Result` instead of
62    /// the underlying solver panic / upstream error.
63    #[error("invalid local-eigensolver params: {detail}")]
64    InvalidEigensolverParams {
65        /// Human-readable description of the violated precondition.
66        detail: &'static str,
67    },
68    /// A bond / physical dimension on one of the inputs to the
69    /// 2-site step did not match the expectation derived from the
70    /// surrounding tensors. Surfaced *before* the matvec runs so
71    /// the operator's `.expect` calls can stay infallible. `field`
72    /// names the constraint that failed (e.g.,
73    /// `"left.bot_ket vs mps[i].left_bond"`).
74    #[error("shape mismatch at site {site}, {field}: expected {expected}, got {actual}")]
75    ShapeMismatch {
76        /// Left site of the two-site block being stepped.
77        site: usize,
78        /// Names the dimension constraint that failed.
79        field: &'static str,
80        /// Extent derived from the surrounding tensors.
81        expected: usize,
82        /// Extent actually found on the input.
83        actual: usize,
84    },
85    /// A QNIndex / Direction / sector / per-site-flux compatibility
86    /// check on a BlockSparse 2-site step's inputs failed. Surfaced
87    /// up front by `dmrg_2site_step_block_sparse` so the matvec
88    /// body's `.expect` calls cannot fire on user input. `field`
89    /// names the leg pair (or single leg for MPO well-formedness
90    /// checks), and `detail` carries a human-readable summary of
91    /// the offending `(sector list, direction, flux-if-applicable)`
92    /// data on each side.
93    #[error("QN mismatch at site {site}, {field}: {detail}")]
94    QnMismatch {
95        /// Left site of the two-site block being stepped.
96        site: usize,
97        /// Names the offending leg pair (or single leg).
98        field: &'static str,
99        /// Human-readable summary of the incompatible QN data on each side.
100        detail: String,
101    },
102    /// The layout `MemoryOrder` of one of the BlockSparse 2-site
103    /// step's four contracted operands diverged from the host
104    /// substrate's `preferred_order()`. Surfaced by
105    /// `EffectiveHamiltonian2SiteBlockSparse::new`
106    /// before any contract runs so the `apply` body's `.expect`
107    /// calls cannot fire on a mixed-order operand set. `operand`
108    /// names which of the four contracted operands (`"left_env"`,
109    /// `"w_i"`, `"w_ip1"`, `"right_env"`) carried a non-matching
110    /// layout order, and `detail` carries a human-readable summary
111    /// of the offending vs expected layout order. The MPS sites
112    /// passed to `new` are template-derivation-only and not asserted
113    /// here; the psi template they derive is built in host order, so
114    /// the matvec stays self-consistent. `detail` holds a rendered
115    /// `MemoryOrder` to keep that layout type off the public error
116    /// surface (it is workspace-internal).
117    #[error("BlockSparse heff operand `{operand}` has layout order mismatch: {detail}")]
118    OrderMismatch {
119        /// Which of the four contracted operands (`"left_env"`, `"w_i"`,
120        /// `"w_ip1"`, `"right_env"`) carried a non-matching layout order.
121        operand: &'static str,
122        /// Human-readable summary of the offending vs expected layout order.
123        detail: String,
124    },
125    /// An underlying `ariadnetor` linalg call (currently the truncated
126    /// SVD) failed. The matvec body itself is shape-validated up
127    /// front and never reaches this branch.
128    #[error("linalg failure during two-site DMRG step")]
129    Contract(#[from] LinalgError),
130    /// The native Lanczos local eigensolver produced a non-finite
131    /// result (NaN/Inf eigenpair). Forwarded without information loss
132    /// from [`crate::krylov::LanczosError`]. Always present, since
133    /// Lanczos is the default eigensolver and is not feature-gated.
134    #[error("Lanczos failure during two-site DMRG step")]
135    Lanczos(#[from] crate::krylov::LanczosError),
136    /// The ARPACK-backed local eigensolver returned an upstream
137    /// error (parameter validation, ARPACK info codes, max-iter
138    /// without convergence, …). Forwarded without information loss
139    /// from [`crate::krylov::ArpackError`]. Only present when the
140    /// `arpack` feature is enabled.
141    #[cfg(feature = "arpack")]
142    #[error("ARPACK failure during two-site DMRG step")]
143    Arpack(#[from] ArpackError),
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use crate::krylov::LanczosError;
150
151    // The `?` on the Lanczos arm of the DMRG step relies on
152    // `From<LanczosError>` routing to the `Lanczos` variant with the payload
153    // intact. Pin it so the conversion cannot silently decay to a different
154    // variant or drop a diagnostic field.
155    #[test]
156    fn from_lanczos_error_preserves_payload_in_lanczos_variant() {
157        let err: DmrgHeffError = LanczosError::NonFinite {
158            iters: 7,
159            eigenvalue: f64::NAN,
160            residual: f64::INFINITY,
161        }
162        .into();
163        match err {
164            DmrgHeffError::Lanczos(LanczosError::NonFinite {
165                iters,
166                eigenvalue,
167                residual,
168            }) => {
169                assert_eq!(iters, 7);
170                assert!(eigenvalue.is_nan());
171                assert_eq!(residual, f64::INFINITY);
172            }
173            other => panic!("expected DmrgHeffError::Lanczos(NonFinite), got {other:?}"),
174        }
175    }
176}