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}