Skip to main content

ariadnetor_algorithms/dmrg/
solver.rs

1//! Local-eigensolver selection for the 2-site DMRG step.
2//!
3//! [`LocalEigensolverParams`] is the runtime selector used by
4//! [`super::sweep::DmrgSweepParams`] (and through it both
5//! [`super::heff::dmrg_2site_step`] and
6//! [`super::heff_block_sparse::dmrg_2site_step_block_sparse`]) to
7//! pick which Krylov solver drives the local eigenpair extraction.
8//!
9//! Lanczos is always available. An ARPACK-backed variant lives behind
10//! the `arpack` feature gate; its presence in the enum is itself
11//! `cfg`-gated so callers cannot select it at compile time without
12//! enabling the feature.
13//!
14//! Helper functions [`validate_eigensolver_params`] and
15//! [`eigensolver_tol`] centralize the per-variant param sanity checks
16//! and tolerance extraction so [`super::sweep::sweep_2site`] and the
17//! heff entry points don't drift.
18
19use ariadnetor_core::Scalar;
20
21#[cfg(feature = "arpack")]
22use crate::krylov::ArpackParams;
23use crate::krylov::LanczosParams;
24
25/// Runtime-selectable local eigensolver for the 2-site DMRG step.
26///
27/// `Lanczos` is the default; `Arpack` requires the `arpack` Cargo
28/// feature.
29#[derive(Debug, Clone)]
30#[non_exhaustive]
31pub enum LocalEigensolverParams {
32    /// In-tree Lanczos with full reorthogonalization
33    /// ([`crate::krylov::lanczos_smallest`]).
34    Lanczos(LanczosParams),
35    /// ARPACK-NG-backed solver
36    /// ([`crate::krylov::arpack_smallest`]). Only constructible when
37    /// the `arpack` feature is enabled.
38    #[cfg(feature = "arpack")]
39    Arpack(ArpackParams),
40}
41
42impl Default for LocalEigensolverParams {
43    fn default() -> Self {
44        Self::Lanczos(LanczosParams::default())
45    }
46}
47
48impl From<LanczosParams> for LocalEigensolverParams {
49    fn from(p: LanczosParams) -> Self {
50        Self::Lanczos(p)
51    }
52}
53
54#[cfg(feature = "arpack")]
55impl From<ArpackParams> for LocalEigensolverParams {
56    fn from(p: ArpackParams) -> Self {
57        Self::Arpack(p)
58    }
59}
60
61/// Validate the per-variant solver params (max_iter, tol). The
62/// `T::Real` representability check is left to the caller because it
63/// depends on the storage's element type.
64///
65/// Returns the `&'static str` detail of the first failing constraint
66/// so the caller can wrap it into either
67/// [`super::sweep::DmrgSweepError::InvalidParams`] or
68/// [`super::heff_error::DmrgHeffError::InvalidEigensolverParams`]
69/// without duplicating the per-variant logic.
70pub(crate) fn validate_eigensolver_params(
71    params: &LocalEigensolverParams,
72) -> Result<(), &'static str> {
73    match params {
74        LocalEigensolverParams::Lanczos(p) => {
75            if p.max_iter == 0 {
76                return Err("lanczos.max_iter must be >= 1");
77            }
78            if !p.tol.is_finite() {
79                return Err("lanczos.tol must be finite");
80            }
81            if p.tol < 0.0 {
82                return Err("lanczos.tol must be non-negative");
83            }
84            Ok(())
85        }
86        #[cfg(feature = "arpack")]
87        LocalEigensolverParams::Arpack(p) => {
88            if p.max_iter == 0 {
89                return Err("arpack.max_iter must be >= 1");
90            }
91            if !p.tol.is_finite() {
92                return Err("arpack.tol must be finite");
93            }
94            // ARPACK rejects tol == 0 (would request its
95            // machine-epsilon default and silently break the
96            // converged flag).
97            if p.tol <= 0.0 {
98                return Err("arpack.tol must be strictly positive");
99            }
100            Ok(())
101        }
102    }
103}
104
105/// Extract the variant's `tol` field as `f64` for cross-variant
106/// downstream casts (e.g. `try_real_from_f64`).
107pub(crate) fn eigensolver_tol(params: &LocalEigensolverParams) -> f64 {
108    match params {
109        LocalEigensolverParams::Lanczos(p) => p.tol,
110        #[cfg(feature = "arpack")]
111        LocalEigensolverParams::Arpack(p) => p.tol,
112    }
113}
114
115/// Trait alias bundling `Scalar` with the per-feature solver bounds
116/// the heff entry points need.
117///
118/// Without the `arpack` feature this is just `Scalar`. With the
119/// feature on, it additionally requires `crate::krylov::ArpackScalar`,
120/// which `arpack_smallest` demands. Both `Scalar` and `ArpackScalar`
121/// are sealed to the same set of scalar types
122/// (`f32`/`f64`/`Complex<f32>`/`Complex<f64>`), so toggling the
123/// feature does not restrict any existing caller — every supported
124/// scalar already satisfies both.
125#[cfg(not(feature = "arpack"))]
126pub trait DmrgScalar: Scalar {}
127#[cfg(not(feature = "arpack"))]
128impl<T: Scalar> DmrgScalar for T {}
129
130#[cfg(feature = "arpack")]
131pub trait DmrgScalar: Scalar + crate::krylov::ArpackScalar {}
132#[cfg(feature = "arpack")]
133impl<T: Scalar + crate::krylov::ArpackScalar> DmrgScalar for T {}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    // The `From` conversions and `eigensolver_tol` have no caller in tests;
140    // these pin the payload-preserving behavior so conversions cannot decay
141    // to `Default::default()` and the accessor cannot decay to a constant.
142
143    #[test]
144    fn from_lanczos_preserves_payload() {
145        // Values chosen to differ from `LanczosParams::default()` (200 / 1e-10)
146        // so a `Default::default()` substitution is observable.
147        let p = LocalEigensolverParams::from(LanczosParams {
148            max_iter: 7,
149            tol: 0.5,
150            seed: None,
151        });
152        match p {
153            LocalEigensolverParams::Lanczos(q) => {
154                assert_eq!(q.max_iter, 7);
155                assert_eq!(q.tol, 0.5);
156            }
157            #[cfg(feature = "arpack")]
158            other => panic!("expected Lanczos variant, got {other:?}"),
159        }
160    }
161
162    #[test]
163    fn eigensolver_tol_reads_lanczos_tol() {
164        let p = LocalEigensolverParams::Lanczos(LanczosParams {
165            tol: 0.5,
166            ..LanczosParams::default()
167        });
168        assert_eq!(eigensolver_tol(&p), 0.5);
169    }
170
171    #[cfg(feature = "arpack")]
172    #[test]
173    fn from_arpack_preserves_payload_and_tol() {
174        let p = LocalEigensolverParams::from(ArpackParams {
175            tol: 0.5,
176            max_iter: 7,
177            ncv: None,
178        });
179        match &p {
180            LocalEigensolverParams::Arpack(q) => {
181                assert_eq!(q.max_iter, 7);
182                assert_eq!(q.tol, 0.5);
183            }
184            other => panic!("expected Arpack variant, got {other:?}"),
185        }
186        assert_eq!(eigensolver_tol(&p), 0.5);
187    }
188}