Skip to main content

tensor4all_core/
krylov.rs

1//! Krylov subspace methods for solving linear equations with abstract tensors.
2//!
3//! This module provides iterative solvers that work with any type implementing [`TensorVectorSpace`],
4//! enabling their use in tensor network algorithms without requiring dense vector representations.
5//!
6//! # Solvers
7//!
8//! - [`gmres`]: Generalized Minimal Residual Method (GMRES) for non-symmetric systems
9//! - [`hermitian_lanczos_lowest_eigenpair`]: Matrix-free lowest eigenpair solver for Hermitian operators
10//!
11//! # Future Extensions
12//!
13//! - CG (Conjugate Gradient) for symmetric positive definite systems
14//! - BiCGSTAB for non-symmetric systems with better convergence properties
15//!
16//! # Example
17//!
18//! ```
19//! use tensor4all_core::{
20//!     krylov::{gmres, GmresOptions},
21//!     DynIndex, IdxTensor, TensorVectorSpace,
22//! };
23//!
24//! # fn main() -> anyhow::Result<()> {
25//! let i = DynIndex::new_dyn(2);
26//! let rhs = IdxTensor::from_dense(vec![i.clone()], vec![1.0, -1.0])?;
27//! let initial_guess = IdxTensor::from_dense(vec![i.clone()], vec![0.0, 0.0])?;
28//!
29//! let apply_operator = |x: &IdxTensor| Ok(x.clone());
30//! let result = gmres(apply_operator, &rhs, &initial_guess, &GmresOptions::default())?;
31//!
32//! assert!(result.converged);
33//! assert!(result.solution.sub(&rhs)?.maxabs()? < 1e-12);
34//! # Ok(())
35//! # }
36//! ```
37
38use crate::any_scalar::AnyScalar;
39use crate::TensorVectorSpace;
40use anyhow::Result;
41use num_complex::Complex64;
42use std::sync::atomic::{AtomicUsize, Ordering};
43use std::time::{Duration, Instant};
44use tensor4all_tensorbackend::{
45    hermitian_exponential_first_column, lowest_hermitian_eigenpair, Matrix,
46};
47
48/// Error returned by the matrix-free Krylov solvers.
49#[derive(Debug, thiserror::Error)]
50pub enum KrylovError {
51    /// Invalid solver options.
52    #[error("{solver}: invalid options: {reason}")]
53    InvalidOptions {
54        /// Solver that rejected the options.
55        solver: &'static str,
56        /// Description of the invalid option combination.
57        reason: String,
58    },
59    /// The initial vector is numerically zero.
60    #[error("{solver}: initial vector has norm below the breakdown tolerance; provide a nonzero initial vector")]
61    ZeroInitialVector {
62        /// Solver that rejected the initial vector.
63        solver: &'static str,
64    },
65    /// An affine GMRES call supplied only zero coefficients.
66    #[error("gmres_affine: at least one affine coefficient must be nonzero")]
67    NoAffineCoefficient,
68    /// The projected operator was not Hermitian.
69    #[error("{solver}: projected operator is not Hermitian: {source}")]
70    NonHermitian {
71        /// Solver that performed the projection.
72        solver: &'static str,
73        /// Original projected-matrix diagnostic.
74        #[source]
75        source: anyhow::Error,
76    },
77    /// An underlying tensor operation (operator application, vector-space
78    /// arithmetic, or backend call) failed.
79    #[error("{solver}: {source}")]
80    Operation {
81        /// Solver that performed the operation.
82        solver: &'static str,
83        /// Original tensor or backend diagnostic, preserving the full chain.
84        #[source]
85        source: anyhow::Error,
86    },
87}
88
89impl From<anyhow::Error> for KrylovError {
90    fn from(source: anyhow::Error) -> Self {
91        Self::Operation {
92            solver: "krylov",
93            source,
94        }
95    }
96}
97
98impl KrylovError {
99    fn from_anyhow_with_classification(source: anyhow::Error) -> Self {
100        match source.downcast::<KrylovError>() {
101            Ok(classified) => classified,
102            Err(source) => Self::from(source),
103        }
104    }
105}
106
107static GMRES_OP_PROFILE_COUNTER: AtomicUsize = AtomicUsize::new(0);
108
109#[derive(Debug, Clone)]
110struct GmresOpProfile {
111    started: Instant,
112    b_norm: Duration,
113    apply: Duration,
114    inner_product: Duration,
115    axpby: Duration,
116    norm: Duration,
117    scale: Duration,
118    triangular_solve: Duration,
119    solution_update: Duration,
120    apply_calls: usize,
121    inner_product_calls: usize,
122    axpby_calls: usize,
123    norm_calls: usize,
124    scale_calls: usize,
125    triangular_solve_calls: usize,
126    solution_update_calls: usize,
127}
128
129impl Default for GmresOpProfile {
130    fn default() -> Self {
131        Self {
132            started: Instant::now(),
133            b_norm: Duration::ZERO,
134            apply: Duration::ZERO,
135            inner_product: Duration::ZERO,
136            axpby: Duration::ZERO,
137            norm: Duration::ZERO,
138            scale: Duration::ZERO,
139            triangular_solve: Duration::ZERO,
140            solution_update: Duration::ZERO,
141            apply_calls: 0,
142            inner_product_calls: 0,
143            axpby_calls: 0,
144            norm_calls: 0,
145            scale_calls: 0,
146            triangular_solve_calls: 0,
147            solution_update_calls: 0,
148        }
149    }
150}
151
152impl GmresOpProfile {
153    fn measured(&self) -> Duration {
154        self.b_norm
155            + self.apply
156            + self.inner_product
157            + self.axpby
158            + self.norm
159            + self.scale
160            + self.triangular_solve
161            + self.solution_update
162    }
163
164    fn print(&self, id: usize, iterations: usize, residual_norm: f64, converged: bool) {
165        let total = self.started.elapsed();
166        let other = total.saturating_sub(self.measured());
167        eprintln!(
168            "T4A gmres_op_profile #{id}: iterations={iterations} residual={residual_norm:.6e} converged={converged} total_ms={:.3} apply_ms={:.3} apply_calls={} inner_ms={:.3} inner_calls={} axpby_ms={:.3} axpby_calls={} norm_ms={:.3} norm_calls={} scale_ms={:.3} scale_calls={} update_ms={:.3} update_calls={} triangular_ms={:.3} triangular_calls={} b_norm_ms={:.3} other_ms={:.3}",
169            total.as_secs_f64() * 1000.0,
170            self.apply.as_secs_f64() * 1000.0,
171            self.apply_calls,
172            self.inner_product.as_secs_f64() * 1000.0,
173            self.inner_product_calls,
174            self.axpby.as_secs_f64() * 1000.0,
175            self.axpby_calls,
176            self.norm.as_secs_f64() * 1000.0,
177            self.norm_calls,
178            self.scale.as_secs_f64() * 1000.0,
179            self.scale_calls,
180            self.solution_update.as_secs_f64() * 1000.0,
181            self.solution_update_calls,
182            self.triangular_solve.as_secs_f64() * 1000.0,
183            self.triangular_solve_calls,
184            self.b_norm.as_secs_f64() * 1000.0,
185            other.as_secs_f64() * 1000.0,
186        );
187    }
188}
189
190/// Options for GMRES solver.
191/// # Examples
192/// ```
193/// use tensor4all_core::krylov::GmresOptions;
194/// let opts = GmresOptions {
195///     max_iter: 50,
196///     rtol: 1e-8,
197///     max_restarts: 5,
198///     verbose: false,
199///     check_true_residual: true,
200/// };
201/// assert_eq!(opts.max_iter, 50);
202/// assert_eq!(opts.rtol, 1e-8);
203/// ```
204#[derive(Debug, Clone)]
205pub struct GmresOptions {
206    /// Maximum number of iterations (restart cycle length).
207    /// Default: 100
208    pub max_iter: usize,
209
210    /// Convergence tolerance for relative residual norm.
211    /// The solver stops when `||r|| / ||b|| < rtol`.
212    /// Default: 1e-10
213    pub rtol: f64,
214
215    /// Maximum number of restarts.
216    /// Total iterations = max_iter * max_restarts.
217    /// Default: 10
218    pub max_restarts: usize,
219
220    /// Whether to print convergence information.
221    /// Default: false
222    pub verbose: bool,
223
224    /// When true, verify convergence by computing the true residual `||b - A*x|| / ||b||`
225    /// before declaring convergence. This prevents false convergence caused by
226    /// truncation corrupting the Krylov basis orthogonality (see Issue #207).
227    /// Costs one additional `apply_a` call when convergence is detected.
228    /// Default: false
229    pub check_true_residual: bool,
230}
231
232impl Default for GmresOptions {
233    fn default() -> Self {
234        Self {
235            max_iter: 100,
236            rtol: 1e-10,
237            max_restarts: 10,
238            verbose: false,
239            check_true_residual: false,
240        }
241    }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq)]
245enum GmresTolerance {
246    Relative(f64),
247    Absolute(f64),
248}
249
250impl GmresTolerance {
251    fn residual_value(self, residual_norm: f64, b_norm: f64) -> f64 {
252        match self {
253            Self::Relative(_) => residual_norm / b_norm,
254            Self::Absolute(_) => residual_norm,
255        }
256    }
257
258    fn is_converged(self, residual_norm: f64, b_norm: f64) -> bool {
259        match self {
260            Self::Relative(rtol) => residual_norm / b_norm < rtol,
261            Self::Absolute(atol) => residual_norm < atol,
262        }
263    }
264}
265
266/// Result of GMRES solver.
267/// Contains the solution, iteration count, final residual norm, and
268/// convergence status.
269/// # Examples
270/// ```
271/// use tensor4all_core::{DynIndex, IdxTensor, TensorVectorSpace};
272/// use tensor4all_core::krylov::{gmres, GmresOptions};
273/// let i = DynIndex::new_dyn(2);
274/// let b = IdxTensor::from_dense(vec![i.clone()], vec![3.0, 7.0]).unwrap();
275/// let x0 = IdxTensor::from_dense(vec![i.clone()], vec![0.0, 0.0]).unwrap();
276/// let result = gmres(|x: &IdxTensor| Ok(x.clone()), &b, &x0, &GmresOptions::default()).unwrap();
277/// assert!(result.converged);
278/// assert!(result.residual_norm < 1e-10);
279/// ```
280#[derive(Debug, Clone)]
281pub struct GmresResult<T> {
282    /// The solution vector.
283    pub solution: T,
284
285    /// Number of iterations performed.
286    pub iterations: usize,
287
288    /// Final relative residual norm.
289    pub residual_norm: f64,
290
291    /// Whether the solver converged.
292    pub converged: bool,
293}
294
295/// Options for [`hermitian_lanczos_lowest_eigenpair`].
296/// These options control the maximum Krylov subspace dimension, convergence
297/// tolerance, and validation tolerance for the small projected Hermitian matrix.
298/// When in doubt, use [`Default::default`].
299/// # Examples
300/// ```
301/// use tensor4all_core::krylov::HermitianLanczosOptions;
302/// let opts = HermitianLanczosOptions {
303///     max_iter: 32,
304///     rtol: 1.0e-9,
305///     ..HermitianLanczosOptions::default()
306/// };
307/// assert_eq!(opts.max_iter, 32);
308/// assert_eq!(opts.rtol, 1.0e-9);
309/// ```
310#[derive(Debug, Clone)]
311pub struct HermitianLanczosOptions {
312    /// Maximum Krylov subspace dimension.
313    ///
314    /// Larger values usually improve convergence but increase memory and the
315    /// cost of the small Rayleigh-Ritz eigensolve. Default: `64`.
316    pub max_iter: usize,
317
318    /// Relative residual tolerance.
319    ///
320    /// Convergence requires `||A v - lambda v|| <= max(atol, rtol *
321    /// max(1, |lambda|))`. Default: `1e-10`.
322    pub rtol: f64,
323
324    /// Absolute residual tolerance.
325    ///
326    /// This is useful when targeting eigenvalues near zero. Default: `0.0`.
327    pub atol: f64,
328
329    /// Breakdown threshold for the next Krylov vector norm.
330    ///
331    /// If the orthogonalized vector norm is at or below this value, the current
332    /// Krylov subspace is treated as invariant and iteration stops. Default:
333    /// `1e-14`.
334    pub breakdown_tol: f64,
335
336    /// Hermitian validation tolerance for the projected Rayleigh-Ritz matrix.
337    ///
338    /// The projected operator is rejected when `V^dagger A V` is not Hermitian
339    /// within this absolute tolerance. Default: `1e-10`.
340    pub hermitian_tol: f64,
341
342    /// Whether to print per-iteration residual information.
343    ///
344    /// Default: `false`.
345    pub verbose: bool,
346}
347
348impl Default for HermitianLanczosOptions {
349    fn default() -> Self {
350        Self {
351            max_iter: 64,
352            rtol: 1.0e-10,
353            atol: 0.0,
354            breakdown_tol: 1.0e-14,
355            hermitian_tol: 1.0e-10,
356            verbose: false,
357        }
358    }
359}
360
361/// Result of [`hermitian_lanczos_lowest_eigenpair`].
362/// Contains the lowest Ritz eigenpair, the final true residual norm, iteration
363/// count, and convergence status.
364/// # Examples
365/// ```
366/// use tensor4all_core::krylov::HermitianLanczosResult;
367/// let result = HermitianLanczosResult {
368///     eigenvalue: 1.0,
369///     eigenvector: vec![1.0_f64],
370///     iterations: 1,
371///     residual_norm: 0.0,
372///     converged: true,
373/// };
374/// assert!(result.converged);
375/// assert_eq!(result.eigenvalue, 1.0);
376/// ```
377#[derive(Debug, Clone)]
378pub struct HermitianLanczosResult<T> {
379    /// Lowest Ritz eigenvalue.
380    pub eigenvalue: f64,
381
382    /// Corresponding Ritz eigenvector in the original vector space.
383    pub eigenvector: T,
384
385    /// Number of Krylov operator applications used to build the final subspace.
386    pub iterations: usize,
387
388    /// True residual norm `||A v - lambda v||`.
389    pub residual_norm: f64,
390
391    /// Whether the true residual satisfied the requested tolerance.
392    pub converged: bool,
393}
394
395/// Options for [`hermitian_krylov_expm_multiply`].
396/// The routine builds a Hermitian Lanczos basis for a matrix-free operator,
397/// exponentiates the small projected Hermitian matrix, and combines the basis
398/// vectors without materializing the full operator. Scalar convergence checks
399/// and projected eigendecompositions are explicit non-differentiable
400/// boundaries; vector-space tensor operations preserve backend metadata when
401/// the underlying tensor implementation supports it.
402/// # Examples
403/// ```
404/// use tensor4all_core::krylov::HermitianKrylovExpmOptions;
405/// let options = HermitianKrylovExpmOptions {
406///     max_iter: 32,
407///     tol: 1.0e-10,
408///     ..HermitianKrylovExpmOptions::default()
409/// };
410/// assert_eq!(options.max_iter, 32);
411/// assert_eq!(options.tol, 1.0e-10);
412/// ```
413#[derive(Debug, Clone)]
414pub struct HermitianKrylovExpmOptions {
415    /// Maximum Krylov subspace dimension for one exponential application.
416    pub max_iter: usize,
417    /// Maximum number of equal time splits attempted after non-convergence.
418    pub max_time_splits: usize,
419    /// Relative convergence tolerance for the Krylov residual estimate.
420    pub tol: f64,
421    /// Breakdown threshold for zero vectors and invariant Krylov subspaces.
422    pub breakdown_tol: f64,
423    /// Hermitian validation tolerance for projected matrices.
424    pub hermitian_tol: f64,
425    /// Whether to print per-attempt diagnostics.
426    pub verbose: bool,
427}
428
429impl Default for HermitianKrylovExpmOptions {
430    fn default() -> Self {
431        Self {
432            max_iter: 30,
433            max_time_splits: 100,
434            tol: 1.0e-12,
435            breakdown_tol: 1.0e-14,
436            hermitian_tol: 1.0e-10,
437            verbose: false,
438        }
439    }
440}
441
442/// Result of [`hermitian_krylov_expm_multiply`].
443/// # Examples
444/// ```
445/// use tensor4all_core::krylov::HermitianKrylovExpmResult;
446/// let result = HermitianKrylovExpmResult {
447///     output: vec![1.0_f64, 0.0],
448///     iterations: 1,
449///     matvecs: 1,
450///     error_estimate: 0.0,
451///     converged: true,
452///     time_splits: 1,
453/// };
454/// assert!(result.converged);
455/// assert_eq!(result.output[0], 1.0);
456/// ```
457#[derive(Debug, Clone)]
458pub struct HermitianKrylovExpmResult<T> {
459    /// Approximation to `exp(exponent * A) * initial`.
460    pub output: T,
461    /// Total Krylov iterations over all accepted substeps.
462    pub iterations: usize,
463    /// Total matrix-vector products over all accepted substeps.
464    pub matvecs: usize,
465    /// Maximum Krylov residual estimate over accepted substeps.
466    pub error_estimate: f64,
467    /// Whether the requested tolerance was met.
468    pub converged: bool,
469    /// Number of equal time splits used.
470    pub time_splits: usize,
471}
472
473#[derive(Debug, Clone)]
474struct HermitianRitzState {
475    eigenvalue: f64,
476    coefficients: Vec<Complex64>,
477    iterations: usize,
478    residual_estimate: f64,
479}
480
481/// Compute the lowest eigenpair of a Hermitian matrix-free operator.
482/// The operator is supplied as `apply_a`, which maps a vector to `A * vector`.
483/// The algorithm builds an orthonormal Krylov basis using modified
484/// Gram-Schmidt with a second reorthogonalization pass, forms the small
485/// projected matrix `V^dagger A V`, and solves that small Hermitian problem via
486/// tensorbackend. The full operator matrix is never materialized.
487/// # Arguments
488/// * `apply_a` - Matrix-free Hermitian operator application.
489/// * `initial` - Nonzero initial vector that defines the starting Krylov vector.
490/// * `options` - Krylov dimension, convergence tolerances, and Hermitian
491///
492///   validation settings.
493/// # Returns
494/// The lowest Ritz eigenpair and the true residual norm.
495/// # Errors
496/// Returns an error when the operator is not Hermitian-compatible (a shape
497/// mismatch or a [`KrylovError::NonHermitian`] projection failure), when the
498/// options are invalid (a [`KrylovError::InvalidOptions`]), when the initial
499/// vector is numerically zero (a [`KrylovError::ZeroInitialVector`]), or when
500/// an underlying tensor or backend operation fails (a
501/// [`KrylovError::Operation`]). Non-convergence is reported through
502/// [`HermitianLanczosResult::converged`] and does not produce an error.
503/// # Examples
504/// ```
505/// use tensor4all_core::{DynIndex, IdxTensor, TensorVectorSpace};
506/// use tensor4all_core::krylov::{hermitian_lanczos_lowest_eigenpair, HermitianLanczosOptions};
507/// let i = DynIndex::new_dyn(2);
508/// let initial = IdxTensor::from_dense(vec![i.clone()], vec![1.0_f64, 1.0]).unwrap();
509/// let result = hermitian_lanczos_lowest_eigenpair(
510///     |x: &IdxTensor| Ok(x.clone()),
511///     &initial,
512///     &HermitianLanczosOptions::default(),
513/// ).unwrap();
514/// assert!(result.converged);
515/// assert!((result.eigenvalue - 1.0).abs() < 1.0e-12);
516/// ```
517pub fn hermitian_lanczos_lowest_eigenpair<T, F>(
518    apply_a: F,
519    initial: &T,
520    options: &HermitianLanczosOptions,
521) -> std::result::Result<HermitianLanczosResult<T>, KrylovError>
522where
523    T: TensorVectorSpace,
524    F: Fn(&T) -> Result<T>,
525{
526    hermitian_lanczos_lowest_eigenpair_impl(apply_a, initial, options)
527        .map_err(KrylovError::from_anyhow_with_classification)
528}
529
530fn hermitian_lanczos_lowest_eigenpair_impl<T, F>(
531    apply_a: F,
532    initial: &T,
533    options: &HermitianLanczosOptions,
534) -> Result<HermitianLanczosResult<T>>
535where
536    T: TensorVectorSpace,
537    F: Fn(&T) -> Result<T>,
538{
539    let basis_capacity = validate_hermitian_lanczos_options(options)?;
540    initial.validate()?;
541
542    let initial_norm = initial.norm()?;
543    if initial_norm <= options.breakdown_tol {
544        return Err(anyhow::Error::new(KrylovError::ZeroInitialVector {
545            solver: "hermitian_lanczos_lowest_eigenpair",
546        }));
547    }
548
549    let mut basis = Vec::with_capacity(basis_capacity);
550    basis.push(initial.scale(AnyScalar::new_real(1.0 / initial_norm))?);
551    let mut h_cols: Vec<Vec<Complex64>> = Vec::with_capacity(options.max_iter);
552    let mut last_ritz: Option<HermitianRitzState> = None;
553
554    for j in 0..options.max_iter {
555        let w = apply_a(&basis[j])?;
556        if j == 0 {
557            w.validate()?;
558        }
559
560        let mut h_col = Vec::with_capacity(j + 2);
561        let mut w_orth = w;
562
563        for v_i in basis.iter().take(j + 1) {
564            let h_ij = v_i.inner_product(&w_orth)?;
565            h_col.push(any_scalar_to_complex(&h_ij));
566            let neg_h_ij = AnyScalar::new_real(0.0) - h_ij;
567            w_orth = w_orth.axpby(AnyScalar::new_real(1.0), v_i, neg_h_ij)?;
568        }
569
570        for (i, v_i) in basis.iter().take(j + 1).enumerate() {
571            let correction = v_i.inner_product(&w_orth)?;
572            h_col[i] += any_scalar_to_complex(&correction);
573            let neg_correction = AnyScalar::new_real(0.0) - correction;
574            w_orth = w_orth.axpby(AnyScalar::new_real(1.0), v_i, neg_correction)?;
575        }
576
577        let beta = w_orth.norm()?;
578        h_col.push(Complex64::new(beta, 0.0));
579        h_cols.push(h_col);
580
581        let subspace_dim = j + 1;
582        let projected = projected_matrix_from_columns(&h_cols, subspace_dim)?;
583        let ritz =
584            lowest_hermitian_eigenpair(&projected, options.hermitian_tol).map_err(|err| {
585                anyhow::Error::new(KrylovError::NonHermitian {
586                    solver: "hermitian_lanczos_lowest_eigenpair",
587                    source: anyhow::anyhow!("projected operator is not Hermitian: {err}"),
588                })
589            })?;
590        let residual_estimate = beta * ritz.eigenvector[subspace_dim - 1].norm();
591        let threshold = hermitian_lanczos_threshold(ritz.eigenvalue, options);
592        let estimate_converged = residual_estimate <= threshold;
593
594        if options.verbose {
595            eprintln!(
596                "Hermitian Lanczos iter {}: eigenvalue={:.16e} residual_estimate={:.6e} threshold={:.6e}",
597                subspace_dim, ritz.eigenvalue, residual_estimate, threshold
598            );
599        }
600
601        let ritz_state = HermitianRitzState {
602            eigenvalue: ritz.eigenvalue,
603            iterations: subspace_dim,
604            residual_estimate,
605            coefficients: ritz.eigenvector,
606        };
607        if estimate_converged || beta <= options.breakdown_tol {
608            let result = finalize_hermitian_lanczos_result(
609                &apply_a,
610                &basis[..subspace_dim],
611                &ritz_state,
612                options,
613            )?;
614            if result.converged || beta <= options.breakdown_tol {
615                return Ok(result);
616            }
617        }
618        last_ritz = Some(ritz_state);
619        basis.push(w_orth.scale(AnyScalar::new_real(1.0 / beta))?);
620    }
621
622    let ritz_state = last_ritz.ok_or_else(|| {
623        anyhow::Error::new(KrylovError::InvalidOptions {
624            solver: "hermitian_lanczos_lowest_eigenpair",
625            reason: "max_iter must be greater than zero".to_string(),
626        })
627    })?;
628    finalize_hermitian_lanczos_result(
629        &apply_a,
630        &basis[..ritz_state.iterations],
631        &ritz_state,
632        options,
633    )
634}
635
636/// Apply `exp(exponent * A)` to a vector using a matrix-free Hermitian Krylov method.
637/// This routine only materializes small projected Krylov matrices. It never
638/// forms the full matrix for `A`, so it can be used by tensor-network local
639/// solvers whose operator application is available as a closure.
640/// # Arguments
641/// * `apply_a` - Matrix-free Hermitian operator application.
642/// * `exponent` - Scalar exponent, for example `-im * dt` for real-time TDVP.
643/// * `initial` - Initial vector.
644/// * `options` - Krylov dimension, tolerance, and Hermitian validation options.
645/// # Returns
646/// A [`HermitianKrylovExpmResult`] containing the evolved vector and diagnostics.
647/// # Errors
648/// Returns an error when the operator is not Hermitian-compatible (a shape
649/// mismatch or a [`KrylovError::NonHermitian`] projection failure), when the
650/// options are invalid (a [`KrylovError::InvalidOptions`]), when the initial
651/// vector is numerically zero (a [`KrylovError::ZeroInitialVector`]), or when
652/// an underlying tensor or backend operation fails (a
653/// [`KrylovError::Operation`]). Non-convergence is reported through
654/// [`HermitianKrylovExpmResult::converged`] and does not produce an error.
655/// # Examples
656/// ```
657/// use num_complex::Complex64;
658/// use tensor4all_core::krylov::{
659///     hermitian_krylov_expm_multiply, HermitianKrylovExpmOptions,
660/// };
661/// use tensor4all_core::{DynIndex, IdxTensor};
662/// # fn main() -> anyhow::Result<()> {
663/// let index = DynIndex::new_dyn(2);
664/// let initial = IdxTensor::from_dense(
665///     vec![index.clone()],
666///     vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)],
667/// )?;
668/// let options = HermitianKrylovExpmOptions {
669///     max_iter: 4,
670///     tol: 1.0e-12,
671///     ..Default::default()
672/// };
673/// let result = hermitian_krylov_expm_multiply(
674///     |x: &IdxTensor| {
675///         let data = x.to_vec::<Complex64>()?;
676///         IdxTensor::from_dense(
677///             vec![index.clone()],
678///             vec![data[0], Complex64::new(2.0, 0.0) * data[1]],
679///         )
680///         .map_err(anyhow::Error::from)
681///     },
682///     Complex64::new(0.0, -0.25),
683///     &initial,
684///     &options,
685/// )?;
686/// let evolved = result.output.to_vec::<Complex64>()?;
687/// let expected = Complex64::new(0.25_f64.cos(), -0.25_f64.sin());
688/// assert!((evolved[0] - expected).norm() < 1.0e-10);
689/// assert!(evolved[1].norm() < 1.0e-12);
690/// # Ok(())
691/// # }
692/// ```
693pub fn hermitian_krylov_expm_multiply<T, F>(
694    apply_a: F,
695    exponent: Complex64,
696    initial: &T,
697    options: &HermitianKrylovExpmOptions,
698) -> std::result::Result<HermitianKrylovExpmResult<T>, KrylovError>
699where
700    T: TensorVectorSpace,
701    F: FnMut(&T) -> Result<T>,
702{
703    hermitian_krylov_expm_multiply_impl(apply_a, exponent, initial, options)
704        .map_err(KrylovError::from_anyhow_with_classification)
705}
706
707fn hermitian_krylov_expm_multiply_impl<T, F>(
708    mut apply_a: F,
709    exponent: Complex64,
710    initial: &T,
711    options: &HermitianKrylovExpmOptions,
712) -> Result<HermitianKrylovExpmResult<T>>
713where
714    T: TensorVectorSpace,
715    F: FnMut(&T) -> Result<T>,
716{
717    validate_hermitian_krylov_expm_options(options)?;
718    initial.validate()?;
719
720    if exponent == Complex64::new(0.0, 0.0) {
721        return Ok(HermitianKrylovExpmResult {
722            output: initial.clone(),
723            iterations: 0,
724            matvecs: 0,
725            error_estimate: 0.0,
726            converged: true,
727            time_splits: 1,
728        });
729    }
730    if initial.norm()? <= options.breakdown_tol {
731        return Ok(HermitianKrylovExpmResult {
732            output: initial.clone(),
733            iterations: 0,
734            matvecs: 0,
735            error_estimate: 0.0,
736            converged: true,
737            time_splits: 1,
738        });
739    }
740
741    let mut splits = 1usize;
742    loop {
743        let step_exponent = exponent / splits as f64;
744        let mut output = initial.clone();
745        let mut iterations = 0usize;
746        let mut matvecs = 0usize;
747        let mut max_error = 0.0_f64;
748        let mut converged = true;
749
750        for _ in 0..splits {
751            let result =
752                hermitian_krylov_expm_multiply_once(&mut apply_a, step_exponent, &output, options)?;
753            iterations += result.iterations;
754            matvecs += result.matvecs;
755            max_error = max_error.max(result.error_estimate);
756            output = result.output;
757            if !result.converged {
758                converged = false;
759                break;
760            }
761        }
762
763        if converged {
764            return Ok(HermitianKrylovExpmResult {
765                output,
766                iterations,
767                matvecs,
768                error_estimate: max_error,
769                converged: true,
770                time_splits: splits,
771            });
772        }
773
774        if options.verbose {
775            eprintln!(
776                "Hermitian Krylov expm did not converge with {splits} time split(s); retrying"
777            );
778        }
779        if splits >= options.max_time_splits {
780            return Err(anyhow::anyhow!(
781                "hermitian_krylov_expm_multiply did not converge within max_time_splits={} and max_iter={}",
782                options.max_time_splits,
783                options.max_iter
784            ));
785        }
786        splits = splits.saturating_mul(2).min(options.max_time_splits);
787    }
788}
789
790fn hermitian_krylov_expm_multiply_once<T, F>(
791    apply_a: &mut F,
792    exponent: Complex64,
793    initial: &T,
794    options: &HermitianKrylovExpmOptions,
795) -> Result<HermitianKrylovExpmResult<T>>
796where
797    T: TensorVectorSpace,
798    F: FnMut(&T) -> Result<T>,
799{
800    let initial_norm = initial.norm()?;
801    if initial_norm <= options.breakdown_tol {
802        return Ok(HermitianKrylovExpmResult {
803            output: initial.clone(),
804            iterations: 0,
805            matvecs: 0,
806            error_estimate: 0.0,
807            converged: true,
808            time_splits: 1,
809        });
810    }
811
812    let basis_capacity =
813        checked_krylov_capacity(options.max_iter, "hermitian_krylov_expm_multiply")?;
814    let mut basis = Vec::with_capacity(basis_capacity);
815    basis.push(initial.scale(AnyScalar::new_real(1.0 / initial_norm))?);
816    let mut h_cols: Vec<Vec<Complex64>> = Vec::with_capacity(options.max_iter);
817    let mut last_coefficients: Option<Vec<Complex64>> = None;
818    let mut last_error_estimate = f64::INFINITY;
819    let mut last_subspace_dim = 0usize;
820    let threshold = options.tol * initial_norm.max(1.0);
821
822    for j in 0..options.max_iter {
823        let w = apply_a(&basis[j])?;
824        if j == 0 {
825            w.validate()?;
826        }
827        let mut w_orth = w;
828        let mut h_col = Vec::with_capacity(j + 2);
829
830        for v_i in basis.iter().take(j + 1) {
831            let h_ij = v_i.inner_product(&w_orth)?;
832            h_col.push(any_scalar_to_complex(&h_ij));
833            let neg_h_ij = AnyScalar::new_real(0.0) - h_ij;
834            w_orth = w_orth.axpby(AnyScalar::new_real(1.0), v_i, neg_h_ij)?;
835        }
836
837        for (i, v_i) in basis.iter().take(j + 1).enumerate() {
838            let correction = v_i.inner_product(&w_orth)?;
839            h_col[i] += any_scalar_to_complex(&correction);
840            let neg_correction = AnyScalar::new_real(0.0) - correction;
841            w_orth = w_orth.axpby(AnyScalar::new_real(1.0), v_i, neg_correction)?;
842        }
843
844        let beta_next = w_orth.norm()?;
845        h_col.push(Complex64::new(beta_next, 0.0));
846        h_cols.push(h_col);
847
848        let subspace_dim = j + 1;
849        let projected = projected_matrix_from_columns(&h_cols, subspace_dim)?;
850        let coefficients =
851            hermitian_exponential_first_column(&projected, exponent, options.hermitian_tol)
852                .map_err(|err| anyhow::anyhow!("projected operator is not Hermitian: {err}"))?;
853        let error_estimate = if beta_next <= options.breakdown_tol {
854            0.0
855        } else {
856            initial_norm * beta_next * coefficients[subspace_dim - 1].norm()
857        };
858        let converged = beta_next <= options.breakdown_tol || error_estimate <= threshold;
859
860        if options.verbose {
861            eprintln!(
862                "Hermitian Krylov expm iter {}: residual_estimate={:.6e} threshold={:.6e}",
863                subspace_dim, error_estimate, threshold
864            );
865        }
866
867        if converged {
868            let output = finalize_hermitian_krylov_expm_output(
869                &basis[..subspace_dim],
870                &coefficients,
871                initial_norm,
872                options.hermitian_tol,
873            )?;
874            return Ok(HermitianKrylovExpmResult {
875                output,
876                iterations: subspace_dim,
877                matvecs: subspace_dim,
878                error_estimate,
879                converged: true,
880                time_splits: 1,
881            });
882        }
883        last_coefficients = Some(coefficients);
884        last_error_estimate = error_estimate;
885        last_subspace_dim = subspace_dim;
886        basis.push(w_orth.scale(AnyScalar::new_real(1.0 / beta_next))?);
887    }
888
889    let coefficients = last_coefficients.ok_or_else(|| {
890        anyhow::anyhow!("hermitian_krylov_expm_multiply: max_iter must be greater than zero")
891    })?;
892    let output = finalize_hermitian_krylov_expm_output(
893        &basis[..last_subspace_dim],
894        &coefficients,
895        initial_norm,
896        options.hermitian_tol,
897    )?;
898    Ok(HermitianKrylovExpmResult {
899        output,
900        iterations: last_subspace_dim,
901        matvecs: last_subspace_dim,
902        error_estimate: last_error_estimate,
903        converged: false,
904        time_splits: 1,
905    })
906}
907
908fn finalize_hermitian_krylov_expm_output<T>(
909    basis: &[T],
910    coefficients: &[Complex64],
911    initial_norm: f64,
912    hermitian_tol: f64,
913) -> Result<T>
914where
915    T: TensorVectorSpace,
916{
917    let unit_output = combine_basis_with_complex_coefficients(
918        basis,
919        coefficients,
920        hermitian_tol,
921        "hermitian_krylov_expm_multiply",
922    )?;
923    unit_output
924        .scale(AnyScalar::new_real(initial_norm))
925        .map_err(anyhow::Error::new)
926}
927
928/// Solve `A x = b` using GMRES (Generalized Minimal Residual Method).
929/// This implements the restarted GMRES algorithm that works with abstract tensor types
930/// through the [`TensorVectorSpace`] trait's vector space operations.
931/// # Algorithm
932/// GMRES builds an orthonormal basis for the Krylov subspace
933/// `K_m = span{r_0, A r_0, A^2 r_0, ..., A^{m-1} r_0}` and finds the
934/// solution that minimizes `||b - A x||` over this subspace.
935/// # Type Parameters
936/// * `T` - A tensor type implementing `TensorVectorSpace`
937/// * `F` - A function that applies the linear operator: `F(x) = A x`
938/// # Arguments
939/// * `apply_a` - Function that applies the linear operator A to a tensor
940/// * `b` - Right-hand side tensor
941/// * `x0` - Initial guess
942/// * `options` - Solver options
943/// # Returns
944/// A `GmresResult` containing the solution and convergence information.
945/// # Errors
946/// Returns an error when the options have an overflowing capacity (a
947/// [`KrylovError::InvalidOptions`]), when the operator and vectors have
948/// incompatible shapes, when the affine coefficients are all zero (a
949/// [`KrylovError::NoAffineCoefficient`] for affine solves), or when an
950/// underlying tensor or backend operation fails (a [`KrylovError::Operation`]).
951/// Non-convergence is reported through the result's `converged` flag and does
952/// not produce an error.
953///
954pub fn gmres<T, F>(
955    apply_a: F,
956    b: &T,
957    x0: &T,
958    options: &GmresOptions,
959) -> std::result::Result<GmresResult<T>, KrylovError>
960where
961    T: TensorVectorSpace,
962    F: Fn(&T) -> Result<T>,
963{
964    gmres_impl(
965        apply_a,
966        b,
967        x0,
968        options,
969        GmresTolerance::Relative(options.rtol),
970        None,
971    )
972    .map_err(KrylovError::from_anyhow_with_classification)
973}
974
975/// Solve `A x = b` using GMRES with an absolute residual tolerance.
976/// This variant stops when `||b - A*x|| < atol`. The default [`gmres`] API uses
977/// relative residual tolerance and is preferred for scale-independent solves.
978/// # Errors
979/// Returns an error when the options have an overflowing capacity (a
980/// [`KrylovError::InvalidOptions`]), when the operator and vectors have
981/// incompatible shapes, when the affine coefficients are all zero (a
982/// [`KrylovError::NoAffineCoefficient`] for affine solves), or when an
983/// underlying tensor or backend operation fails (a [`KrylovError::Operation`]).
984/// Non-convergence is reported through the result's `converged` flag and does
985/// not produce an error.
986///
987pub fn gmres_with_absolute_tolerance<T, F>(
988    apply_a: F,
989    b: &T,
990    x0: &T,
991    options: &GmresOptions,
992    atol: f64,
993) -> std::result::Result<GmresResult<T>, KrylovError>
994where
995    T: TensorVectorSpace,
996    F: Fn(&T) -> Result<T>,
997{
998    gmres_impl(
999        apply_a,
1000        b,
1001        x0,
1002        options,
1003        GmresTolerance::Absolute(atol),
1004        None,
1005    )
1006    .map_err(KrylovError::from_anyhow_with_classification)
1007}
1008
1009/// Solve `(a0 I + a1 A) x = b` using GMRES with relative residual tolerance.
1010/// The Arnoldi basis is built from the unshifted `A` callback, while affine
1011/// coefficients are applied in the projected Hessenberg problem, matching
1012/// KrylovKit's affine linear-solve convention.
1013/// # Errors
1014/// Returns an error when the operator and vectors have incompatible shapes (a
1015/// shape mismatch), when the affine coefficients are all zero (a
1016/// [`KrylovError::NoAffineCoefficient`] for affine solves), or when an
1017/// underlying tensor or backend operation fails (a [`KrylovError::Operation`]).
1018/// Non-convergence is reported through the result's `converged` flag and does
1019/// not produce an error.
1020///
1021pub fn gmres_affine<T, F>(
1022    apply_a: F,
1023    b: &T,
1024    x0: &T,
1025    a0: AnyScalar,
1026    a1: AnyScalar,
1027    options: &GmresOptions,
1028) -> std::result::Result<GmresResult<T>, KrylovError>
1029where
1030    T: TensorVectorSpace,
1031    F: Fn(&T) -> Result<T>,
1032{
1033    gmres_affine_impl(
1034        apply_a,
1035        b,
1036        x0,
1037        a0,
1038        a1,
1039        options,
1040        GmresTolerance::Relative(options.rtol),
1041    )
1042    .map_err(KrylovError::from_anyhow_with_classification)
1043}
1044
1045/// Solve `(a0 I + a1 A) x = b` using GMRES with an absolute residual tolerance.
1046/// The Arnoldi basis is built from the unshifted `A` callback, while the affine
1047/// coefficients are applied to the small Hessenberg problem. This mirrors
1048/// KrylovKit's `linsolve(operator, b, a0, a1)` algorithm and avoids changing the
1049/// Krylov basis when affine coefficients are present.
1050/// # Errors
1051/// Returns an error when the operator and vectors have incompatible shapes (a
1052/// shape mismatch), when the affine coefficients are all zero (a
1053/// [`KrylovError::NoAffineCoefficient`] for affine solves), or when an
1054/// underlying tensor or backend operation fails (a [`KrylovError::Operation`]).
1055/// Non-convergence is reported through the result's `converged` flag and does
1056/// not produce an error.
1057///
1058pub fn gmres_affine_with_absolute_tolerance<T, F>(
1059    apply_a: F,
1060    b: &T,
1061    x0: &T,
1062    a0: AnyScalar,
1063    a1: AnyScalar,
1064    options: &GmresOptions,
1065    atol: f64,
1066) -> std::result::Result<GmresResult<T>, KrylovError>
1067where
1068    T: TensorVectorSpace,
1069    F: Fn(&T) -> Result<T>,
1070{
1071    gmres_affine_impl(
1072        apply_a,
1073        b,
1074        x0,
1075        a0,
1076        a1,
1077        options,
1078        GmresTolerance::Absolute(atol),
1079    )
1080    .map_err(KrylovError::from_anyhow_with_classification)
1081}
1082
1083fn gmres_affine_impl<T, F>(
1084    apply_a: F,
1085    b: &T,
1086    x0: &T,
1087    a0: AnyScalar,
1088    a1: AnyScalar,
1089    options: &GmresOptions,
1090    tolerance: GmresTolerance,
1091) -> Result<GmresResult<T>>
1092where
1093    T: TensorVectorSpace,
1094    F: Fn(&T) -> Result<T>,
1095{
1096    b.validate()?;
1097    x0.validate()?;
1098
1099    let profile_enabled = std::env::var_os("T4A_GMRES_OP_PROFILE").is_some();
1100    let profile_id = if profile_enabled {
1101        GMRES_OP_PROFILE_COUNTER.fetch_add(1, Ordering::Relaxed)
1102    } else {
1103        0
1104    };
1105    let mut profile = GmresOpProfile::default();
1106    let mut total_iters = 0usize;
1107
1108    macro_rules! finish {
1109        ($result:expr) => {{
1110            let result = $result;
1111            if profile_enabled {
1112                profile.print(
1113                    profile_id,
1114                    result.iterations,
1115                    result.residual_norm,
1116                    result.converged,
1117                );
1118            }
1119            return Ok(result);
1120        }};
1121    }
1122
1123    let started = Instant::now();
1124    let b_norm = b.norm()?;
1125    if profile_enabled {
1126        profile.b_norm += started.elapsed();
1127    }
1128    if b_norm < 1e-15 {
1129        finish!(GmresResult {
1130            solution: x0.clone(),
1131            iterations: 0,
1132            residual_norm: 0.0,
1133            converged: true,
1134        });
1135    }
1136    if a0.is_zero() && a1.is_zero() {
1137        return Err(anyhow::Error::new(KrylovError::NoAffineCoefficient));
1138    }
1139    if a1.is_zero() {
1140        let started = Instant::now();
1141        let solution = b.scale(AnyScalar::new_real(1.0) / a0)?;
1142        if profile_enabled {
1143            profile.scale += started.elapsed();
1144            profile.scale_calls += 1;
1145        }
1146        finish!(GmresResult {
1147            solution,
1148            iterations: 0,
1149            residual_norm: 0.0,
1150            converged: true,
1151        });
1152    }
1153
1154    let mut x = x0.clone();
1155
1156    for restart in 0..options.max_restarts {
1157        let started = Instant::now();
1158        let ax = apply_a(&x)?;
1159        if profile_enabled {
1160            profile.apply += started.elapsed();
1161            profile.apply_calls += 1;
1162        }
1163        if restart == 0 {
1164            ax.validate()?;
1165        }
1166        let started = Instant::now();
1167        let affine_x = x.axpby(a0.clone(), &ax, a1.clone())?;
1168        if profile_enabled {
1169            profile.axpby += started.elapsed();
1170            profile.axpby_calls += 1;
1171        }
1172        let started = Instant::now();
1173        let r = b.axpby(
1174            AnyScalar::new_real(1.0),
1175            &affine_x,
1176            AnyScalar::new_real(-1.0),
1177        )?;
1178        if profile_enabled {
1179            profile.axpby += started.elapsed();
1180            profile.axpby_calls += 1;
1181        }
1182        let started = Instant::now();
1183        let r_norm = r.norm()?;
1184        if profile_enabled {
1185            profile.norm += started.elapsed();
1186            profile.norm_calls += 1;
1187        }
1188        let residual_value = tolerance.residual_value(r_norm, b_norm);
1189        if options.verbose {
1190            eprintln!(
1191                "GMRES restart {}: initial residual = {:.6e}",
1192                restart, residual_value
1193            );
1194        }
1195        if tolerance.is_converged(r_norm, b_norm) {
1196            finish!(GmresResult {
1197                solution: x,
1198                iterations: total_iters,
1199                residual_norm: residual_value,
1200                converged: true,
1201            });
1202        }
1203
1204        let cycle_max_iter = options.max_iter;
1205        let mut v_basis: Vec<T> =
1206            Vec::with_capacity(checked_krylov_capacity(cycle_max_iter, "gmres")?);
1207        let started = Instant::now();
1208        v_basis.push(r.scale(AnyScalar::new_real(1.0 / r_norm))?);
1209        if profile_enabled {
1210            profile.scale += started.elapsed();
1211            profile.scale_calls += 1;
1212        }
1213
1214        let mut h_matrix: Vec<Vec<AnyScalar>> = Vec::with_capacity(cycle_max_iter);
1215        let mut cs: Vec<AnyScalar> = Vec::with_capacity(cycle_max_iter);
1216        let mut sn: Vec<AnyScalar> = Vec::with_capacity(cycle_max_iter);
1217        let mut g: Vec<AnyScalar> = vec![AnyScalar::new_real(r_norm)];
1218        let mut solution_already_updated = false;
1219
1220        for j in 0..cycle_max_iter {
1221            total_iters += 1;
1222
1223            let started = Instant::now();
1224            let w = apply_a(&v_basis[j])?;
1225            if profile_enabled {
1226                profile.apply += started.elapsed();
1227                profile.apply_calls += 1;
1228            }
1229            let mut h_a_col: Vec<AnyScalar> = Vec::with_capacity(j + 2);
1230            let mut w_orth = w;
1231
1232            for v_i in v_basis.iter().take(j + 1) {
1233                let started = Instant::now();
1234                let h_ij = v_i.inner_product(&w_orth)?;
1235                if profile_enabled {
1236                    profile.inner_product += started.elapsed();
1237                    profile.inner_product_calls += 1;
1238                }
1239                h_a_col.push(h_ij.clone());
1240                let neg_h_ij = AnyScalar::new_real(0.0) - h_ij;
1241                let started = Instant::now();
1242                w_orth = w_orth.axpby(AnyScalar::new_real(1.0), v_i, neg_h_ij)?;
1243                if profile_enabled {
1244                    profile.axpby += started.elapsed();
1245                    profile.axpby_calls += 1;
1246                }
1247            }
1248            for (i, v_i) in v_basis.iter().take(j + 1).enumerate() {
1249                let started = Instant::now();
1250                let correction = v_i.inner_product(&w_orth)?;
1251                if profile_enabled {
1252                    profile.inner_product += started.elapsed();
1253                    profile.inner_product_calls += 1;
1254                }
1255                h_a_col[i] = h_a_col[i].clone() + correction.clone();
1256                let neg_correction = AnyScalar::new_real(0.0) - correction;
1257                let started = Instant::now();
1258                w_orth = w_orth.axpby(AnyScalar::new_real(1.0), v_i, neg_correction)?;
1259                if profile_enabled {
1260                    profile.axpby += started.elapsed();
1261                    profile.axpby_calls += 1;
1262                }
1263            }
1264
1265            let started = Instant::now();
1266            let h_jp1_j_real = w_orth.norm()?;
1267            if profile_enabled {
1268                profile.norm += started.elapsed();
1269                profile.norm_calls += 1;
1270            }
1271            h_a_col.push(AnyScalar::new_real(h_jp1_j_real));
1272
1273            let mut h_col: Vec<AnyScalar> = Vec::with_capacity(j + 2);
1274            for h in h_a_col.iter().take(j) {
1275                h_col.push(a1.clone() * h.clone());
1276            }
1277            h_col.push(a0.clone() + a1.clone() * h_a_col[j].clone());
1278            h_col.push(a1.clone() * h_a_col[j + 1].clone());
1279
1280            #[allow(clippy::needless_range_loop)]
1281            for i in 0..j {
1282                let h_i = h_col[i].clone();
1283                let h_ip1 = h_col[i + 1].clone();
1284                let (new_hi, new_hip1) = apply_givens_rotation(&cs[i], &sn[i], &h_i, &h_ip1);
1285                h_col[i] = new_hi;
1286                h_col[i + 1] = new_hip1;
1287            }
1288
1289            let (c_j, s_j) = compute_givens_rotation(&h_col[j], &h_col[j + 1]);
1290            cs.push(c_j.clone());
1291            sn.push(s_j.clone());
1292
1293            let (new_hj, _) = apply_givens_rotation(&c_j, &s_j, &h_col[j], &h_col[j + 1]);
1294            h_col[j] = new_hj;
1295            h_col[j + 1] = AnyScalar::new_real(0.0);
1296
1297            let g_j = g[j].clone();
1298            let g_jp1 = AnyScalar::new_real(0.0);
1299            let (new_gj, new_gjp1) = apply_givens_rotation(&c_j, &s_j, &g_j, &g_jp1);
1300            g[j] = new_gj;
1301            let res_norm = new_gjp1.abs();
1302            g.push(new_gjp1);
1303
1304            h_matrix.push(h_col);
1305            let residual_value = tolerance.residual_value(res_norm, b_norm);
1306            if options.verbose {
1307                eprintln!("GMRES iter {}: residual = {:.6e}", j + 1, residual_value);
1308            }
1309
1310            if tolerance.is_converged(res_norm, b_norm) {
1311                let started = Instant::now();
1312                let y = solve_upper_triangular(&h_matrix, &g[..=j])?;
1313                if profile_enabled {
1314                    profile.triangular_solve += started.elapsed();
1315                    profile.triangular_solve_calls += 1;
1316                }
1317                let started = Instant::now();
1318                x = update_solution(&x, &v_basis[..=j], &y)?;
1319                if profile_enabled {
1320                    profile.solution_update += started.elapsed();
1321                    profile.solution_update_calls += 1;
1322                }
1323                if options.check_true_residual {
1324                    let started = Instant::now();
1325                    let ax_check = apply_a(&x)?;
1326                    if profile_enabled {
1327                        profile.apply += started.elapsed();
1328                        profile.apply_calls += 1;
1329                    }
1330                    let started = Instant::now();
1331                    let affine_check = x.axpby(a0.clone(), &ax_check, a1.clone())?;
1332                    if profile_enabled {
1333                        profile.axpby += started.elapsed();
1334                        profile.axpby_calls += 1;
1335                    }
1336                    let started = Instant::now();
1337                    let r_check = b.axpby(
1338                        AnyScalar::new_real(1.0),
1339                        &affine_check,
1340                        AnyScalar::new_real(-1.0),
1341                    )?;
1342                    if profile_enabled {
1343                        profile.axpby += started.elapsed();
1344                        profile.axpby_calls += 1;
1345                    }
1346                    let started = Instant::now();
1347                    let true_abs_res = r_check.norm()?;
1348                    if profile_enabled {
1349                        profile.norm += started.elapsed();
1350                        profile.norm_calls += 1;
1351                    }
1352                    let true_residual_value = tolerance.residual_value(true_abs_res, b_norm);
1353                    if options.verbose {
1354                        eprintln!(
1355                            "GMRES true residual check: hessenberg={:.6e}, checked={:.6e}",
1356                            residual_value, true_residual_value
1357                        );
1358                    }
1359                    if tolerance.is_converged(true_abs_res, b_norm) {
1360                        finish!(GmresResult {
1361                            solution: x,
1362                            iterations: total_iters,
1363                            residual_norm: true_residual_value,
1364                            converged: true,
1365                        });
1366                    }
1367                    solution_already_updated = true;
1368                    break;
1369                } else {
1370                    finish!(GmresResult {
1371                        solution: x,
1372                        iterations: total_iters,
1373                        residual_norm: residual_value,
1374                        converged: true,
1375                    });
1376                }
1377            }
1378
1379            if h_jp1_j_real > 1e-14 {
1380                let started = Instant::now();
1381                v_basis.push(w_orth.scale(AnyScalar::new_real(1.0 / h_jp1_j_real))?);
1382                if profile_enabled {
1383                    profile.scale += started.elapsed();
1384                    profile.scale_calls += 1;
1385                }
1386            } else {
1387                let started = Instant::now();
1388                let y = solve_upper_triangular(&h_matrix, &g[..=j])?;
1389                if profile_enabled {
1390                    profile.triangular_solve += started.elapsed();
1391                    profile.triangular_solve_calls += 1;
1392                }
1393                let started = Instant::now();
1394                x = update_solution(&x, &v_basis[..=j], &y)?;
1395                if profile_enabled {
1396                    profile.solution_update += started.elapsed();
1397                    profile.solution_update_calls += 1;
1398                }
1399                let started = Instant::now();
1400                let ax_final = apply_a(&x)?;
1401                if profile_enabled {
1402                    profile.apply += started.elapsed();
1403                    profile.apply_calls += 1;
1404                }
1405                let started = Instant::now();
1406                let affine_final = x.axpby(a0.clone(), &ax_final, a1.clone())?;
1407                if profile_enabled {
1408                    profile.axpby += started.elapsed();
1409                    profile.axpby_calls += 1;
1410                }
1411                let started = Instant::now();
1412                let r_final = b.axpby(
1413                    AnyScalar::new_real(1.0),
1414                    &affine_final,
1415                    AnyScalar::new_real(-1.0),
1416                )?;
1417                if profile_enabled {
1418                    profile.axpby += started.elapsed();
1419                    profile.axpby_calls += 1;
1420                }
1421                let started = Instant::now();
1422                let final_abs_res = r_final.norm()?;
1423                if profile_enabled {
1424                    profile.norm += started.elapsed();
1425                    profile.norm_calls += 1;
1426                }
1427                let final_res = tolerance.residual_value(final_abs_res, b_norm);
1428                finish!(GmresResult {
1429                    solution: x,
1430                    iterations: total_iters,
1431                    residual_norm: final_res,
1432                    converged: tolerance.is_converged(final_abs_res, b_norm),
1433                });
1434            }
1435        }
1436
1437        if !solution_already_updated {
1438            let actual_iters = h_matrix.len();
1439            let started = Instant::now();
1440            let y = solve_upper_triangular(&h_matrix, &g[..actual_iters])?;
1441            if profile_enabled {
1442                profile.triangular_solve += started.elapsed();
1443                profile.triangular_solve_calls += 1;
1444            }
1445            let started = Instant::now();
1446            x = update_solution(&x, &v_basis[..actual_iters], &y)?;
1447            if profile_enabled {
1448                profile.solution_update += started.elapsed();
1449                profile.solution_update_calls += 1;
1450            }
1451        }
1452    }
1453
1454    let started = Instant::now();
1455    let ax_final = apply_a(&x)?;
1456    if profile_enabled {
1457        profile.apply += started.elapsed();
1458        profile.apply_calls += 1;
1459    }
1460    let started = Instant::now();
1461    let affine_final = x.axpby(a0, &ax_final, a1)?;
1462    if profile_enabled {
1463        profile.axpby += started.elapsed();
1464        profile.axpby_calls += 1;
1465    }
1466    let started = Instant::now();
1467    let r_final = b.axpby(
1468        AnyScalar::new_real(1.0),
1469        &affine_final,
1470        AnyScalar::new_real(-1.0),
1471    )?;
1472    if profile_enabled {
1473        profile.axpby += started.elapsed();
1474        profile.axpby_calls += 1;
1475    }
1476    let started = Instant::now();
1477    let final_abs_res = r_final.norm()?;
1478    if profile_enabled {
1479        profile.norm += started.elapsed();
1480        profile.norm_calls += 1;
1481    }
1482    let final_res = tolerance.residual_value(final_abs_res, b_norm);
1483
1484    finish!(GmresResult {
1485        solution: x,
1486        iterations: total_iters,
1487        residual_norm: final_res,
1488        converged: tolerance.is_converged(final_abs_res, b_norm),
1489    })
1490}
1491
1492/// Solve `A x = b` using GMRES while enforcing a total iteration limit.
1493/// [`GmresOptions::max_iter`] remains the restart cycle length and
1494/// [`GmresOptions::max_restarts`] remains the maximum number of restart cycles.
1495/// `max_total_iter` caps the total number of Arnoldi steps across all restart
1496/// cycles; the final cycle is shortened when necessary.
1497/// # Errors
1498/// Returns an error when the options have an overflowing capacity (a
1499/// [`KrylovError::InvalidOptions`]), when the operator and vectors have
1500/// incompatible shapes, when the affine coefficients are all zero (a
1501/// [`KrylovError::NoAffineCoefficient`] for affine solves), or when an
1502/// underlying tensor or backend operation fails (a [`KrylovError::Operation`]).
1503/// Non-convergence is reported through the result's `converged` flag and does
1504/// not produce an error.
1505///
1506pub fn gmres_with_total_iteration_limit<T, F>(
1507    apply_a: F,
1508    b: &T,
1509    x0: &T,
1510    options: &GmresOptions,
1511    max_total_iter: usize,
1512) -> std::result::Result<GmresResult<T>, KrylovError>
1513where
1514    T: TensorVectorSpace,
1515    F: Fn(&T) -> Result<T>,
1516{
1517    gmres_impl(
1518        apply_a,
1519        b,
1520        x0,
1521        options,
1522        GmresTolerance::Relative(options.rtol),
1523        Some(max_total_iter),
1524    )
1525    .map_err(KrylovError::from_anyhow_with_classification)
1526}
1527
1528fn gmres_impl<T, F>(
1529    apply_a: F,
1530    b: &T,
1531    x0: &T,
1532    options: &GmresOptions,
1533    tolerance: GmresTolerance,
1534    max_total_iter: Option<usize>,
1535) -> Result<GmresResult<T>>
1536where
1537    T: TensorVectorSpace,
1538    F: Fn(&T) -> Result<T>,
1539{
1540    checked_krylov_capacity(options.max_iter, "gmres")?;
1541
1542    // Validate structural consistency of inputs
1543    b.validate()?;
1544    x0.validate()?;
1545
1546    let b_norm = b.norm()?;
1547    if b_norm < 1e-15 {
1548        // b is effectively zero, return x0
1549        return Ok(GmresResult {
1550            solution: x0.clone(),
1551            iterations: 0,
1552            residual_norm: 0.0,
1553            converged: true,
1554        });
1555    }
1556
1557    let mut x = x0.clone();
1558    let mut total_iters = 0;
1559
1560    for _restart in 0..options.max_restarts {
1561        let cycle_max_iter = match max_total_iter {
1562            Some(limit) => {
1563                let remaining = limit.saturating_sub(total_iters);
1564                if remaining == 0 {
1565                    break;
1566                }
1567                options.max_iter.min(remaining)
1568            }
1569            None => options.max_iter,
1570        };
1571        if cycle_max_iter == 0 {
1572            break;
1573        }
1574
1575        // Compute initial residual: r = b - A*x
1576        let ax = apply_a(&x)?;
1577        // Validate operator output on first restart
1578        if _restart == 0 {
1579            ax.validate()?;
1580        }
1581        // r = 1.0 * b + (-1.0) * ax
1582        let r = b.axpby(AnyScalar::new_real(1.0), &ax, AnyScalar::new_real(-1.0))?;
1583        let r_norm = r.norm()?;
1584        let residual_value = tolerance.residual_value(r_norm, b_norm);
1585
1586        if options.verbose {
1587            eprintln!(
1588                "GMRES restart {}: initial residual = {:.6e}",
1589                _restart, residual_value
1590            );
1591        }
1592
1593        if tolerance.is_converged(r_norm, b_norm) {
1594            return Ok(GmresResult {
1595                solution: x,
1596                iterations: total_iters,
1597                residual_norm: residual_value,
1598                converged: true,
1599            });
1600        }
1601
1602        // Arnoldi process with modified Gram-Schmidt
1603        let mut v_basis: Vec<T> =
1604            Vec::with_capacity(checked_krylov_capacity(cycle_max_iter, "gmres")?);
1605        let mut h_matrix: Vec<Vec<AnyScalar>> = Vec::with_capacity(cycle_max_iter);
1606
1607        // v_0 = r / ||r||
1608        let v0 = r.scale(AnyScalar::new_real(1.0 / r_norm))?;
1609        v_basis.push(v0);
1610
1611        // Initialize Givens rotation storage
1612        let mut cs: Vec<AnyScalar> = Vec::with_capacity(cycle_max_iter);
1613        let mut sn: Vec<AnyScalar> = Vec::with_capacity(cycle_max_iter);
1614        let mut g: Vec<AnyScalar> = vec![AnyScalar::new_real(r_norm)]; // residual in upper Hessenberg space
1615        let mut solution_already_updated = false;
1616
1617        for j in 0..cycle_max_iter {
1618            total_iters += 1;
1619
1620            // w = A * v_j
1621            let w = apply_a(&v_basis[j])?;
1622
1623            // Modified Gram-Schmidt orthogonalization
1624            let mut h_col: Vec<AnyScalar> = Vec::with_capacity(j + 2);
1625            let mut w_orth = w;
1626
1627            for v_i in v_basis.iter().take(j + 1) {
1628                let h_ij = v_i.inner_product(&w_orth)?;
1629                h_col.push(h_ij.clone());
1630                // w_orth = w_orth - h_ij * v_i = 1.0 * w_orth + (-h_ij) * v_i
1631                let neg_h_ij = AnyScalar::new_real(0.0) - h_ij;
1632                w_orth = w_orth.axpby(AnyScalar::new_real(1.0), v_i, neg_h_ij)?;
1633            }
1634
1635            // KrylovKit's default orthogonalizer is ModifiedGramSchmidt2.
1636            // The second pass is important for long Krylov bases and complex
1637            // non-Hermitian local problems.
1638            for (i, v_i) in v_basis.iter().take(j + 1).enumerate() {
1639                let correction = v_i.inner_product(&w_orth)?;
1640                h_col[i] = h_col[i].clone() + correction.clone();
1641                let neg_correction = AnyScalar::new_real(0.0) - correction;
1642                w_orth = w_orth.axpby(AnyScalar::new_real(1.0), v_i, neg_correction)?;
1643            }
1644
1645            let h_jp1_j_real = w_orth.norm()?;
1646            let h_jp1_j = AnyScalar::new_real(h_jp1_j_real);
1647            h_col.push(h_jp1_j);
1648
1649            // Apply previous Givens rotations to new column
1650            #[allow(clippy::needless_range_loop)]
1651            for i in 0..j {
1652                let h_i = h_col[i].clone();
1653                let h_ip1 = h_col[i + 1].clone();
1654                let (new_hi, new_hip1) = apply_givens_rotation(&cs[i], &sn[i], &h_i, &h_ip1);
1655                h_col[i] = new_hi;
1656                h_col[i + 1] = new_hip1;
1657            }
1658
1659            // Compute new Givens rotation for h_col[j] and h_col[j+1]
1660            let (c_j, s_j) = compute_givens_rotation(&h_col[j], &h_col[j + 1]);
1661            cs.push(c_j.clone());
1662            sn.push(s_j.clone());
1663
1664            // Apply new rotation to eliminate h_col[j+1]
1665            let (new_hj, _) = apply_givens_rotation(&c_j, &s_j, &h_col[j], &h_col[j + 1]);
1666            h_col[j] = new_hj;
1667            h_col[j + 1] = AnyScalar::new_real(0.0);
1668
1669            // Apply rotation to g
1670            let g_j = g[j].clone();
1671            let g_jp1 = AnyScalar::new_real(0.0);
1672            let (new_gj, new_gjp1) = apply_givens_rotation(&c_j, &s_j, &g_j, &g_jp1);
1673            g[j] = new_gj;
1674            let res_norm = new_gjp1.abs();
1675            g.push(new_gjp1);
1676
1677            h_matrix.push(h_col);
1678
1679            // Check convergence
1680            let residual_value = tolerance.residual_value(res_norm, b_norm);
1681
1682            if options.verbose {
1683                eprintln!("GMRES iter {}: residual = {:.6e}", j + 1, residual_value);
1684            }
1685
1686            if tolerance.is_converged(res_norm, b_norm) {
1687                // Solve upper triangular system and update x
1688                let y = solve_upper_triangular(&h_matrix, &g[..=j])?;
1689                x = update_solution(&x, &v_basis[..=j], &y)?;
1690                if options.check_true_residual {
1691                    let ax_check = apply_a(&x)?;
1692                    let r_check = b.axpby(
1693                        AnyScalar::new_real(1.0),
1694                        &ax_check,
1695                        AnyScalar::new_real(-1.0),
1696                    )?;
1697                    let true_abs_res = r_check.norm()?;
1698                    let true_residual_value = tolerance.residual_value(true_abs_res, b_norm);
1699
1700                    if options.verbose {
1701                        eprintln!(
1702                            "GMRES true residual check: hessenberg={:.6e}, checked={:.6e}",
1703                            residual_value, true_residual_value
1704                        );
1705                    }
1706
1707                    if tolerance.is_converged(true_abs_res, b_norm) {
1708                        return Ok(GmresResult {
1709                            solution: x,
1710                            iterations: total_iters,
1711                            residual_norm: true_residual_value,
1712                            converged: true,
1713                        });
1714                    }
1715                    solution_already_updated = true;
1716                    break;
1717                } else {
1718                    return Ok(GmresResult {
1719                        solution: x,
1720                        iterations: total_iters,
1721                        residual_norm: residual_value,
1722                        converged: true,
1723                    });
1724                }
1725            }
1726
1727            // Add new basis vector (if not converged and h_jp1_j is not too small)
1728            if h_jp1_j_real > 1e-14 {
1729                let v_jp1 = w_orth.scale(AnyScalar::new_real(1.0 / h_jp1_j_real))?;
1730                v_basis.push(v_jp1);
1731            } else {
1732                // Lucky breakdown - we've found the exact solution in the Krylov subspace
1733                let y = solve_upper_triangular(&h_matrix, &g[..=j])?;
1734                x = update_solution(&x, &v_basis[..=j], &y)?;
1735                let ax_final = apply_a(&x)?;
1736                let r_final = b.axpby(
1737                    AnyScalar::new_real(1.0),
1738                    &ax_final,
1739                    AnyScalar::new_real(-1.0),
1740                )?;
1741                let final_abs_res = r_final.norm()?;
1742                let final_res = tolerance.residual_value(final_abs_res, b_norm);
1743                return Ok(GmresResult {
1744                    solution: x,
1745                    iterations: total_iters,
1746                    residual_norm: final_res,
1747                    converged: tolerance.is_converged(final_abs_res, b_norm),
1748                });
1749            }
1750        }
1751
1752        // End of restart cycle - update x with current solution
1753        if !solution_already_updated {
1754            let actual_iters = h_matrix.len();
1755            let y = solve_upper_triangular(&h_matrix, &g[..actual_iters])?;
1756            x = update_solution(&x, &v_basis[..actual_iters], &y)?;
1757        }
1758    }
1759
1760    // Compute final residual
1761    let ax_final = apply_a(&x)?;
1762    let r_final = b.axpby(
1763        AnyScalar::new_real(1.0),
1764        &ax_final,
1765        AnyScalar::new_real(-1.0),
1766    )?;
1767    let final_abs_res = r_final.norm()?;
1768    let final_res = tolerance.residual_value(final_abs_res, b_norm);
1769
1770    Ok(GmresResult {
1771        solution: x,
1772        iterations: total_iters,
1773        residual_norm: final_res,
1774        converged: tolerance.is_converged(final_abs_res, b_norm),
1775    })
1776}
1777
1778/// Solve `A x = b` using GMRES with optional truncation after each iteration.
1779/// This is an extension of [`gmres`] that allows truncating Krylov basis vectors
1780/// to control bond dimension growth in tensor network representations.
1781/// # Type Parameters
1782/// * `T` - A tensor type implementing `TensorVectorSpace`
1783/// * `F` - A function that applies the linear operator: `F(x) = A x`
1784/// * `Tr` - A function that truncates a tensor in-place: `Tr(&mut x)`
1785/// # Arguments
1786/// * `apply_a` - Function that applies the linear operator A to a tensor
1787/// * `b` - Right-hand side tensor
1788/// * `x0` - Initial guess
1789/// * `options` - Solver options
1790/// * `truncate` - Function that truncates a tensor to control bond dimension
1791/// # Note
1792/// Truncation is applied after each Gram-Schmidt orthogonalization step
1793/// and after the final solution update. This helps control the bond dimension
1794/// growth that would otherwise occur in MPS/MPO representations.
1795/// # Errors
1796/// Returns an error when the options have an overflowing capacity (a
1797/// [`KrylovError::InvalidOptions`]), when the operator and vectors have
1798/// incompatible shapes, when the affine coefficients are all zero (a
1799/// [`KrylovError::NoAffineCoefficient`] for affine solves), or when an
1800/// underlying tensor or backend operation fails (a [`KrylovError::Operation`]).
1801/// Non-convergence is reported through the result's `converged` flag and does
1802/// not produce an error.
1803/// # Examples
1804/// Solve `2x = b` with a no-op truncation function:
1805/// ```
1806/// use tensor4all_core::{DynIndex, IdxTensor, TensorVectorSpace, AnyScalar};
1807/// use tensor4all_core::krylov::{gmres_with_truncation, GmresOptions};
1808/// let i = DynIndex::new_dyn(2);
1809/// let b = IdxTensor::from_dense(vec![i.clone()], vec![4.0, 6.0]).unwrap();
1810/// let x0 = IdxTensor::from_dense(vec![i.clone()], vec![0.0, 0.0]).unwrap();
1811/// // Operator A = 2*I (scales input by 2)
1812/// let apply_a = |x: &IdxTensor| {
1813///     x.scale(AnyScalar::new_real(2.0)).map_err(anyhow::Error::from)
1814/// };
1815/// // No-op truncation
1816/// let truncate = |_x: &mut IdxTensor| Ok(());
1817/// let result = gmres_with_truncation(apply_a, &b, &x0, &GmresOptions::default(), truncate).unwrap();
1818/// assert!(result.converged);
1819/// // Solution should be [2.0, 3.0]
1820/// let expected = IdxTensor::from_dense(vec![i], vec![2.0, 3.0]).unwrap();
1821/// assert!(result.solution.sub(&expected).unwrap().maxabs().unwrap() < 1e-8);
1822/// ```
1823pub fn gmres_with_truncation<T, F, Tr>(
1824    apply_a: F,
1825    b: &T,
1826    x0: &T,
1827    options: &GmresOptions,
1828    truncate: Tr,
1829) -> std::result::Result<GmresResult<T>, KrylovError>
1830where
1831    T: TensorVectorSpace,
1832    F: Fn(&T) -> Result<T>,
1833    Tr: Fn(&mut T) -> Result<()>,
1834{
1835    gmres_with_truncation_impl(apply_a, b, x0, options, truncate)
1836        .map_err(KrylovError::from_anyhow_with_classification)
1837}
1838
1839fn gmres_with_truncation_impl<T, F, Tr>(
1840    apply_a: F,
1841    b: &T,
1842    x0: &T,
1843    options: &GmresOptions,
1844    truncate: Tr,
1845) -> Result<GmresResult<T>>
1846where
1847    T: TensorVectorSpace,
1848    F: Fn(&T) -> Result<T>,
1849    Tr: Fn(&mut T) -> Result<()>,
1850{
1851    checked_krylov_capacity(options.max_iter, "gmres_with_truncation")?;
1852
1853    // Validate structural consistency of inputs
1854    b.validate()?;
1855    x0.validate()?;
1856
1857    let b_norm = b.norm()?;
1858    if b_norm < 1e-15 {
1859        return Ok(GmresResult {
1860            solution: x0.clone(),
1861            iterations: 0,
1862            residual_norm: 0.0,
1863            converged: true,
1864        });
1865    }
1866
1867    let mut x = x0.clone();
1868    let mut total_iters = 0;
1869
1870    for _restart in 0..options.max_restarts {
1871        let ax = apply_a(&x)?;
1872        // Validate operator output on first restart
1873        if _restart == 0 {
1874            ax.validate()?;
1875        }
1876        let mut r = b.axpby(AnyScalar::new_real(1.0), &ax, AnyScalar::new_real(-1.0))?;
1877        truncate(&mut r)?;
1878        let r_norm = r.norm()?;
1879        let rel_res = r_norm / b_norm;
1880
1881        if options.verbose {
1882            eprintln!(
1883                "GMRES restart {}: initial residual = {:.6e}",
1884                _restart, rel_res
1885            );
1886        }
1887
1888        if rel_res < options.rtol {
1889            return Ok(GmresResult {
1890                solution: x,
1891                iterations: total_iters,
1892                residual_norm: rel_res,
1893                converged: true,
1894            });
1895        }
1896
1897        let mut v_basis: Vec<T> = Vec::with_capacity(checked_krylov_capacity(
1898            options.max_iter,
1899            "gmres_with_truncation",
1900        )?);
1901        let mut h_matrix: Vec<Vec<AnyScalar>> = Vec::with_capacity(options.max_iter);
1902
1903        let mut v0 = r.scale(AnyScalar::new_real(1.0 / r_norm))?;
1904        truncate(&mut v0)?;
1905        // After truncation, v0 might not be unit norm and might point in a different direction.
1906        // We need to:
1907        // 1. Renormalize v0 to unit norm for numerical stability
1908        // 2. Recompute g[0] = <r, v0> to maintain the correct relationship
1909        let v0_norm = v0.norm()?;
1910        let effective_g0 = if v0_norm > 1e-15 {
1911            v0 = v0.scale(AnyScalar::new_real(1.0 / v0_norm))?;
1912            // g[0] should be the component of r in the direction of v0
1913            // Since r was truncated and v0 = truncate(r/||r||)/||truncate(r/||r||)||,
1914            // g[0] = <r, v0> ≈ ||r|| * ||truncate(r/||r||)|| = r_norm * v0_norm
1915            r_norm * v0_norm
1916        } else {
1917            r_norm
1918        };
1919        v_basis.push(v0);
1920
1921        let mut cs: Vec<AnyScalar> = Vec::with_capacity(options.max_iter);
1922        let mut sn: Vec<AnyScalar> = Vec::with_capacity(options.max_iter);
1923        let mut g: Vec<AnyScalar> = vec![AnyScalar::new_real(effective_g0)];
1924        let mut solution_already_updated = false;
1925
1926        for j in 0..options.max_iter {
1927            total_iters += 1;
1928
1929            let w = apply_a(&v_basis[j])?;
1930
1931            let mut h_col: Vec<AnyScalar> = Vec::with_capacity(j + 2);
1932            let mut w_orth = w;
1933
1934            for v_i in v_basis.iter().take(j + 1) {
1935                let h_ij = v_i.inner_product(&w_orth)?;
1936                h_col.push(h_ij.clone());
1937                let neg_h_ij = AnyScalar::new_real(0.0) - h_ij;
1938                w_orth = w_orth.axpby(AnyScalar::new_real(1.0), v_i, neg_h_ij)?;
1939            }
1940
1941            // Iterative reorthogonalization with truncation
1942            // Truncation can change the direction of w_orth, breaking orthogonality.
1943            // We iterate until all corrections are below a threshold to ensure
1944            // the Krylov basis remains orthogonal despite truncation.
1945            const REORTH_THRESHOLD: f64 = 1e-12;
1946            const MAX_REORTH_ITERS: usize = 10;
1947
1948            let mut reorth_iter_count = 0;
1949            for reorth_iter in 0..MAX_REORTH_ITERS {
1950                reorth_iter_count = reorth_iter + 1;
1951                let norm_before_truncate = w_orth.norm()?;
1952                truncate(&mut w_orth)?;
1953                let norm_after_truncate = w_orth.norm()?;
1954
1955                let mut max_correction = 0.0;
1956                for (i, v_i) in v_basis.iter().enumerate() {
1957                    let correction = v_i.inner_product(&w_orth)?;
1958                    let correction_abs = correction.abs();
1959                    if correction_abs > max_correction {
1960                        max_correction = correction_abs;
1961                    }
1962                    if correction_abs > REORTH_THRESHOLD {
1963                        let neg_correction = AnyScalar::new_real(0.0) - correction.clone();
1964                        w_orth = w_orth.axpby(AnyScalar::new_real(1.0), v_i, neg_correction)?;
1965                        // Update Hessenberg matrix entry to include correction
1966                        h_col[i] = h_col[i].clone() + correction;
1967                    }
1968                }
1969
1970                if options.verbose {
1971                    eprintln!(
1972                        "  reorth iter {}: norm {:.6e} -> {:.6e}, max_correction = {:.6e}",
1973                        reorth_iter, norm_before_truncate, norm_after_truncate, max_correction
1974                    );
1975                }
1976
1977                // If all corrections are small enough, we're done
1978                if max_correction < REORTH_THRESHOLD {
1979                    break;
1980                }
1981            }
1982
1983            if options.verbose && reorth_iter_count > 1 {
1984                eprintln!("  (needed {} reorth iterations)", reorth_iter_count);
1985            }
1986
1987            let h_jp1_j_real = w_orth.norm()?;
1988            let h_jp1_j = AnyScalar::new_real(h_jp1_j_real);
1989            h_col.push(h_jp1_j);
1990
1991            #[allow(clippy::needless_range_loop)]
1992            for i in 0..j {
1993                let h_i = h_col[i].clone();
1994                let h_ip1 = h_col[i + 1].clone();
1995                let (new_hi, new_hip1) = apply_givens_rotation(&cs[i], &sn[i], &h_i, &h_ip1);
1996                h_col[i] = new_hi;
1997                h_col[i + 1] = new_hip1;
1998            }
1999
2000            let (c_j, s_j) = compute_givens_rotation(&h_col[j], &h_col[j + 1]);
2001            cs.push(c_j.clone());
2002            sn.push(s_j.clone());
2003
2004            let (new_hj, _) = apply_givens_rotation(&c_j, &s_j, &h_col[j], &h_col[j + 1]);
2005            h_col[j] = new_hj;
2006            h_col[j + 1] = AnyScalar::new_real(0.0);
2007
2008            let g_j = g[j].clone();
2009            let g_jp1 = AnyScalar::new_real(0.0);
2010            let (new_gj, new_gjp1) = apply_givens_rotation(&c_j, &s_j, &g_j, &g_jp1);
2011            g[j] = new_gj;
2012            let res_norm = new_gjp1.abs();
2013            g.push(new_gjp1);
2014
2015            h_matrix.push(h_col);
2016
2017            let rel_res = res_norm / b_norm;
2018
2019            if options.verbose {
2020                eprintln!("GMRES iter {}: residual = {:.6e}", j + 1, rel_res);
2021            }
2022
2023            if rel_res < options.rtol {
2024                let y = solve_upper_triangular(&h_matrix, &g[..=j])?;
2025                x = update_solution_truncated(&x, &v_basis[..=j], &y, &truncate)?;
2026
2027                if options.check_true_residual {
2028                    // Verify with true residual to prevent false convergence
2029                    let ax_check = apply_a(&x)?;
2030                    let mut r_check = b.axpby(
2031                        AnyScalar::new_real(1.0),
2032                        &ax_check,
2033                        AnyScalar::new_real(-1.0),
2034                    )?;
2035                    truncate(&mut r_check)?;
2036                    let true_rel_res = r_check.norm()? / b_norm;
2037
2038                    if options.verbose {
2039                        eprintln!(
2040                            "GMRES true residual check: hessenberg={:.6e}, checked={:.6e}",
2041                            rel_res, true_rel_res
2042                        );
2043                    }
2044
2045                    if true_rel_res < options.rtol {
2046                        return Ok(GmresResult {
2047                            solution: x,
2048                            iterations: total_iters,
2049                            residual_norm: true_rel_res,
2050                            converged: true,
2051                        });
2052                    }
2053                    // False convergence detected: x is already updated above,
2054                    // so skip the end-of-cycle update and go to next restart
2055                    solution_already_updated = true;
2056                    break;
2057                } else {
2058                    return Ok(GmresResult {
2059                        solution: x,
2060                        iterations: total_iters,
2061                        residual_norm: rel_res,
2062                        converged: true,
2063                    });
2064                }
2065            }
2066
2067            if h_jp1_j_real > 1e-14 {
2068                // Create v_{j+1} = w_orth / ||w_orth||
2069                // w_orth has already been truncated twice (after orthogonalization and after reorthogonalization)
2070                // so we don't need to truncate again. Scale doesn't increase bond dimensions.
2071                let v_jp1 = w_orth.scale(AnyScalar::new_real(1.0 / h_jp1_j_real))?;
2072                // v_jp1 should have norm ~1.0 by construction
2073                // The Arnoldi relation h_{j+1,j} * v_{j+1} = w_orth is maintained exactly
2074                v_basis.push(v_jp1);
2075            } else {
2076                let y = solve_upper_triangular(&h_matrix, &g[..=j])?;
2077                x = update_solution_truncated(&x, &v_basis[..=j], &y, &truncate)?;
2078                let ax_final = apply_a(&x)?;
2079                let r_final = b.axpby(
2080                    AnyScalar::new_real(1.0),
2081                    &ax_final,
2082                    AnyScalar::new_real(-1.0),
2083                )?;
2084                let final_res = r_final.norm()? / b_norm;
2085                return Ok(GmresResult {
2086                    solution: x,
2087                    iterations: total_iters,
2088                    residual_norm: final_res,
2089                    converged: final_res < options.rtol,
2090                });
2091            }
2092        }
2093
2094        if !solution_already_updated {
2095            let actual_iters = v_basis.len().min(options.max_iter);
2096            let y = solve_upper_triangular(&h_matrix, &g[..actual_iters])?;
2097            x = update_solution_truncated(&x, &v_basis[..actual_iters], &y, &truncate)?;
2098        }
2099    }
2100
2101    let ax_final = apply_a(&x)?;
2102    let r_final = b.axpby(
2103        AnyScalar::new_real(1.0),
2104        &ax_final,
2105        AnyScalar::new_real(-1.0),
2106    )?;
2107    let final_res = r_final.norm()? / b_norm;
2108
2109    Ok(GmresResult {
2110        solution: x,
2111        iterations: total_iters,
2112        residual_norm: final_res,
2113        converged: final_res < options.rtol,
2114    })
2115}
2116
2117/// Options for restarted GMRES with truncation.
2118/// This is used by [`restart_gmres_with_truncation`] which wraps the standard GMRES
2119/// with an outer loop that recomputes the true residual at each restart.
2120/// # Examples
2121/// ```
2122/// use tensor4all_core::krylov::RestartGmresOptions;
2123/// let opts = RestartGmresOptions::new()
2124///     .with_max_outer_iters(10)
2125///     .with_rtol(1e-6)
2126///     .with_inner_max_iter(20)
2127///     .with_inner_max_restarts(2)
2128///     .with_min_reduction(0.99)
2129///     .with_inner_rtol(0.01)
2130///     .with_verbose(false);
2131/// assert_eq!(opts.max_outer_iters, 10);
2132/// assert_eq!(opts.rtol, 1e-6);
2133/// assert_eq!(opts.inner_max_iter, 20);
2134/// assert_eq!(opts.inner_max_restarts, 2);
2135/// assert_eq!(opts.min_reduction, Some(0.99));
2136/// assert_eq!(opts.inner_rtol, Some(0.01));
2137/// ```
2138#[derive(Debug, Clone)]
2139pub struct RestartGmresOptions {
2140    /// Maximum number of outer restart iterations.
2141    /// Default: 20
2142    pub max_outer_iters: usize,
2143
2144    /// Convergence tolerance for relative residual norm (based on true residual).
2145    /// The solver stops when `||b - A*x|| / ||b|| < rtol`.
2146    /// Default: 1e-10
2147    pub rtol: f64,
2148
2149    /// Maximum iterations per inner GMRES cycle.
2150    /// Default: 10
2151    pub inner_max_iter: usize,
2152
2153    /// Number of restarts within each inner GMRES (usually 0).
2154    /// Default: 0
2155    pub inner_max_restarts: usize,
2156
2157    /// Stagnation detection threshold.
2158    /// If the residual reduction ratio exceeds this value (i.e., residual doesn't decrease enough),
2159    /// the solver considers it stagnated.
2160    /// For example, 0.99 means stagnation is detected when residual decreases by less than 1%.
2161    /// Default: None (no stagnation detection)
2162    pub min_reduction: Option<f64>,
2163
2164    /// Inner GMRES relative tolerance.
2165    /// If None, uses 0.1 (solve inner problem loosely).
2166    /// Default: None
2167    pub inner_rtol: Option<f64>,
2168
2169    /// Whether to print convergence information.
2170    /// Default: false
2171    pub verbose: bool,
2172}
2173
2174impl Default for RestartGmresOptions {
2175    fn default() -> Self {
2176        Self {
2177            max_outer_iters: 20,
2178            rtol: 1e-10,
2179            inner_max_iter: 10,
2180            inner_max_restarts: 0,
2181            min_reduction: None,
2182            inner_rtol: None,
2183            verbose: false,
2184        }
2185    }
2186}
2187
2188impl RestartGmresOptions {
2189    /// Create new options with default values.
2190    pub fn new() -> Self {
2191        Self::default()
2192    }
2193
2194    /// Set maximum number of outer iterations.
2195    pub fn with_max_outer_iters(mut self, max_outer_iters: usize) -> Self {
2196        self.max_outer_iters = max_outer_iters;
2197        self
2198    }
2199
2200    /// Set convergence tolerance.
2201    pub fn with_rtol(mut self, rtol: f64) -> Self {
2202        self.rtol = rtol;
2203        self
2204    }
2205
2206    /// Set maximum iterations per inner GMRES cycle.
2207    pub fn with_inner_max_iter(mut self, inner_max_iter: usize) -> Self {
2208        self.inner_max_iter = inner_max_iter;
2209        self
2210    }
2211
2212    /// Set number of restarts within each inner GMRES.
2213    pub fn with_inner_max_restarts(mut self, inner_max_restarts: usize) -> Self {
2214        self.inner_max_restarts = inner_max_restarts;
2215        self
2216    }
2217
2218    /// Set stagnation detection threshold.
2219    pub fn with_min_reduction(mut self, min_reduction: f64) -> Self {
2220        self.min_reduction = Some(min_reduction);
2221        self
2222    }
2223
2224    /// Set inner GMRES relative tolerance.
2225    pub fn with_inner_rtol(mut self, inner_rtol: f64) -> Self {
2226        self.inner_rtol = Some(inner_rtol);
2227        self
2228    }
2229
2230    /// Enable verbose output.
2231    pub fn with_verbose(mut self, verbose: bool) -> Self {
2232        self.verbose = verbose;
2233        self
2234    }
2235}
2236
2237/// Result of restarted GMRES solver.
2238/// # Examples
2239/// ```
2240/// use tensor4all_core::{DynIndex, IdxTensor, AnyScalar};
2241/// use tensor4all_core::krylov::{restart_gmres_with_truncation, RestartGmresOptions};
2242/// let i = DynIndex::new_dyn(2);
2243/// let b = IdxTensor::from_dense(vec![i.clone()], vec![3.0, 5.0]).unwrap();
2244/// let apply_a = |x: &IdxTensor| {
2245///     x.scale(AnyScalar::new_real(3.0)).map_err(anyhow::Error::from)
2246/// };
2247/// let truncate = |_x: &mut IdxTensor| Ok(());
2248/// let result = restart_gmres_with_truncation(
2249///     apply_a, &b, None, &RestartGmresOptions::default(), truncate,
2250/// ).unwrap();
2251/// assert!(result.converged);
2252/// assert!(result.residual_norm < 1e-10);
2253/// assert!(result.outer_iterations <= 20);
2254/// ```
2255#[derive(Debug, Clone)]
2256pub struct RestartGmresResult<T> {
2257    /// The solution vector.
2258    pub solution: T,
2259
2260    /// Total number of inner GMRES iterations performed.
2261    pub iterations: usize,
2262
2263    /// Number of outer restart iterations performed.
2264    pub outer_iterations: usize,
2265
2266    /// Final relative residual norm (true residual).
2267    pub residual_norm: f64,
2268
2269    /// Whether the solver converged.
2270    pub converged: bool,
2271}
2272
2273/// Solve `A x = b` using restarted GMRES with truncation.
2274/// This wraps [`gmres_with_truncation`] with an outer loop that recomputes the true residual
2275/// at each restart. This is particularly useful for MPS/MPO computations where truncation
2276/// can cause the inner GMRES residual to be inaccurate.
2277/// # Algorithm
2278/// ```text
2279/// for outer_iter in 0..max_outer_iters:
2280///     r = b - A*x0          // Compute true residual
2281///     r = truncate(r)
2282///     if ||r|| / ||b|| < rtol:
2283///         return x0         // Converged
2284///     x' = gmres_with_truncation(A, r, 0, inner_options, truncate)
2285///     x0 = truncate(x0 + x')
2286/// ```
2287/// # Type Parameters
2288/// * `T` - A tensor type implementing `TensorVectorSpace`
2289/// * `F` - A function that applies the linear operator: `F(x) = A x`
2290/// * `Tr` - A function that truncates a tensor in-place: `Tr(&mut x)`
2291/// # Arguments
2292/// * `apply_a` - Function that applies the linear operator A to a tensor
2293/// * `b` - Right-hand side tensor
2294/// * `x0` - Initial guess (if None, starts from zero)
2295/// * `options` - Solver options
2296/// * `truncate` - Function that truncates a tensor to control bond dimension
2297/// # Returns
2298/// A `RestartGmresResult` containing the solution and convergence information.
2299/// # Errors
2300/// Returns an error when the options have an overflowing capacity (a
2301/// [`KrylovError::InvalidOptions`]), when the operator and vectors have
2302/// incompatible shapes, when the affine coefficients are all zero (a
2303/// [`KrylovError::NoAffineCoefficient`] for affine solves), or when an
2304/// underlying tensor or backend operation fails (a [`KrylovError::Operation`]).
2305/// Non-convergence is reported through the result's `converged` flag and does
2306/// not produce an error.
2307/// # Examples
2308/// Solve `5x = b` with no truncation:
2309/// ```
2310/// use tensor4all_core::{DynIndex, IdxTensor, TensorVectorSpace, AnyScalar};
2311/// use tensor4all_core::krylov::{restart_gmres_with_truncation, RestartGmresOptions};
2312/// let i = DynIndex::new_dyn(3);
2313/// let b = IdxTensor::from_dense(vec![i.clone()], vec![5.0, 10.0, 15.0]).unwrap();
2314/// let apply_a = |x: &IdxTensor| {
2315///     x.scale(AnyScalar::new_real(5.0)).map_err(anyhow::Error::from)
2316/// };
2317/// let truncate = |_x: &mut IdxTensor| Ok(());
2318/// let result = restart_gmres_with_truncation(
2319///     apply_a, &b, None, &RestartGmresOptions::default(), truncate,
2320/// ).unwrap();
2321/// assert!(result.converged);
2322/// let expected = IdxTensor::from_dense(vec![i], vec![1.0, 2.0, 3.0]).unwrap();
2323/// assert!(result.solution.sub(&expected).unwrap().maxabs().unwrap() < 1e-8);
2324/// ```
2325pub fn restart_gmres_with_truncation<T, F, Tr>(
2326    apply_a: F,
2327    b: &T,
2328    x0: Option<&T>,
2329    options: &RestartGmresOptions,
2330    truncate: Tr,
2331) -> std::result::Result<RestartGmresResult<T>, KrylovError>
2332where
2333    T: TensorVectorSpace,
2334    F: Fn(&T) -> Result<T>,
2335    Tr: Fn(&mut T) -> Result<()>,
2336{
2337    restart_gmres_with_truncation_impl(apply_a, b, x0, options, truncate)
2338        .map_err(KrylovError::from_anyhow_with_classification)
2339}
2340
2341fn restart_gmres_with_truncation_impl<T, F, Tr>(
2342    apply_a: F,
2343    b: &T,
2344    x0: Option<&T>,
2345    options: &RestartGmresOptions,
2346    truncate: Tr,
2347) -> Result<RestartGmresResult<T>>
2348where
2349    T: TensorVectorSpace,
2350    F: Fn(&T) -> Result<T>,
2351    Tr: Fn(&mut T) -> Result<()>,
2352{
2353    // Validate structural consistency of inputs
2354    b.validate()?;
2355    if let Some(x) = x0 {
2356        x.validate()?;
2357    }
2358
2359    let b_norm = b.norm()?;
2360    if b_norm < 1e-15 {
2361        // b is effectively zero, return x0 or zero
2362        let solution = match x0 {
2363            Some(x) => x.clone(),
2364            None => b.scale(AnyScalar::new_real(0.0))?,
2365        };
2366        return Ok(RestartGmresResult {
2367            solution,
2368            iterations: 0,
2369            outer_iterations: 0,
2370            residual_norm: 0.0,
2371            converged: true,
2372        });
2373    }
2374
2375    // Initialize x: use x0 if provided, otherwise start from zero.
2376    // Track whether x is zero to avoid unnecessary bond dimension doubling
2377    // when adding the first correction via axpby.
2378    let mut x_is_zero = x0.is_none();
2379    let mut x = match x0 {
2380        Some(x) => x.clone(),
2381        None => b.scale(AnyScalar::new_real(0.0))?,
2382    };
2383
2384    let mut total_inner_iters = 0;
2385    let mut prev_residual_norm = f64::INFINITY;
2386
2387    // Inner GMRES options
2388    let inner_options = GmresOptions {
2389        max_iter: options.inner_max_iter,
2390        rtol: options.inner_rtol.unwrap_or(0.1), // Solve loosely by default
2391        max_restarts: options.inner_max_restarts.checked_add(1).ok_or_else(|| {
2392            anyhow::Error::new(KrylovError::InvalidOptions {
2393                solver: "restart_gmres_with_truncation",
2394                reason: "inner_max_restarts + 1 overflows usize".to_string(),
2395            })
2396        })?, // +1 because max_restarts=0 means 1 cycle
2397        verbose: options.verbose,
2398        check_true_residual: true, // Always check in restart context to avoid false convergence
2399    };
2400
2401    for outer_iter in 0..options.max_outer_iters {
2402        // Compute true residual: r = b - A*x
2403        let ax = apply_a(&x)?;
2404        // Validate operator output on first outer iteration
2405        if outer_iter == 0 {
2406            ax.validate()?;
2407        }
2408        let mut r = b.axpby(AnyScalar::new_real(1.0), &ax, AnyScalar::new_real(-1.0))?;
2409        truncate(&mut r)?;
2410
2411        let r_norm = r.norm()?;
2412        let rel_res = r_norm / b_norm;
2413
2414        if options.verbose {
2415            eprintln!(
2416                "Restart GMRES outer iter {}: true residual = {:.6e}",
2417                outer_iter, rel_res
2418            );
2419        }
2420
2421        // Check convergence
2422        if rel_res < options.rtol {
2423            return Ok(RestartGmresResult {
2424                solution: x,
2425                iterations: total_inner_iters,
2426                outer_iterations: outer_iter,
2427                residual_norm: rel_res,
2428                converged: true,
2429            });
2430        }
2431
2432        // Check stagnation
2433        if let Some(min_reduction) = options.min_reduction {
2434            if outer_iter > 0 && rel_res > prev_residual_norm * min_reduction {
2435                if options.verbose {
2436                    eprintln!(
2437                        "Restart GMRES stagnated: residual ratio = {:.6e} > {:.6e}",
2438                        rel_res / prev_residual_norm,
2439                        min_reduction
2440                    );
2441                }
2442                return Ok(RestartGmresResult {
2443                    solution: x,
2444                    iterations: total_inner_iters,
2445                    outer_iterations: outer_iter,
2446                    residual_norm: rel_res,
2447                    converged: false,
2448                });
2449            }
2450        }
2451        prev_residual_norm = rel_res;
2452
2453        // Solve A*x' = r using inner GMRES with zero initial guess
2454        // The zero initial guess is created by scaling r by 0
2455        let zero = r.scale(AnyScalar::new_real(0.0))?;
2456        let inner_result =
2457            gmres_with_truncation_impl(&apply_a, &r, &zero, &inner_options, &truncate)?;
2458
2459        total_inner_iters += inner_result.iterations;
2460
2461        if options.verbose {
2462            eprintln!(
2463                "  Inner GMRES: {} iterations, residual = {:.6e}, converged = {}",
2464                inner_result.iterations, inner_result.residual_norm, inner_result.converged
2465            );
2466        }
2467
2468        // Update solution: x = x + x'
2469        // When x is zero (first iteration with no initial guess), use x' directly
2470        // to avoid bond dimension doubling from axpby with a zero tensor.
2471        if x_is_zero {
2472            x = inner_result.solution;
2473            x_is_zero = false;
2474        } else {
2475            x = x.axpby(
2476                AnyScalar::new_real(1.0),
2477                &inner_result.solution,
2478                AnyScalar::new_real(1.0),
2479            )?;
2480        }
2481        truncate(&mut x)?;
2482    }
2483
2484    // Did not converge within max_outer_iters
2485    // Compute final residual
2486    let ax = apply_a(&x)?;
2487    let mut r = b.axpby(AnyScalar::new_real(1.0), &ax, AnyScalar::new_real(-1.0))?;
2488    truncate(&mut r)?;
2489    let final_rel_res = r.norm()? / b_norm;
2490
2491    Ok(RestartGmresResult {
2492        solution: x,
2493        iterations: total_inner_iters,
2494        outer_iterations: options.max_outer_iters,
2495        residual_norm: final_rel_res,
2496        converged: false,
2497    })
2498}
2499
2500fn validate_hermitian_lanczos_options(options: &HermitianLanczosOptions) -> Result<usize> {
2501    if options.max_iter == 0 {
2502        return Err(anyhow::Error::new(KrylovError::InvalidOptions {
2503            solver: "hermitian_lanczos_lowest_eigenpair",
2504            reason: "max_iter must be greater than zero".to_string(),
2505        }));
2506    }
2507    let basis_capacity =
2508        checked_krylov_capacity(options.max_iter, "hermitian_lanczos_lowest_eigenpair")?;
2509    anyhow::ensure!(
2510        options.rtol.is_finite() && options.rtol >= 0.0,
2511        "hermitian_lanczos_lowest_eigenpair: rtol must be finite and non-negative"
2512    );
2513    anyhow::ensure!(
2514        options.atol.is_finite() && options.atol >= 0.0,
2515        "hermitian_lanczos_lowest_eigenpair: atol must be finite and non-negative"
2516    );
2517    anyhow::ensure!(
2518        options.breakdown_tol.is_finite() && options.breakdown_tol >= 0.0,
2519        "hermitian_lanczos_lowest_eigenpair: breakdown_tol must be finite and non-negative"
2520    );
2521    anyhow::ensure!(
2522        options.hermitian_tol.is_finite() && options.hermitian_tol >= 0.0,
2523        "hermitian_lanczos_lowest_eigenpair: hermitian_tol must be finite and non-negative"
2524    );
2525    Ok(basis_capacity)
2526}
2527
2528fn validate_hermitian_krylov_expm_options(options: &HermitianKrylovExpmOptions) -> Result<()> {
2529    checked_krylov_capacity(options.max_iter, "hermitian_krylov_expm_multiply")?;
2530    anyhow::ensure!(
2531        options.max_iter > 0,
2532        "hermitian_krylov_expm_multiply: max_iter must be greater than zero"
2533    );
2534    anyhow::ensure!(
2535        options.max_time_splits > 0,
2536        "hermitian_krylov_expm_multiply: max_time_splits must be greater than zero"
2537    );
2538    anyhow::ensure!(
2539        options.tol.is_finite() && options.tol >= 0.0,
2540        "hermitian_krylov_expm_multiply: tol must be finite and non-negative"
2541    );
2542    anyhow::ensure!(
2543        options.breakdown_tol.is_finite() && options.breakdown_tol >= 0.0,
2544        "hermitian_krylov_expm_multiply: breakdown_tol must be finite and non-negative"
2545    );
2546    anyhow::ensure!(
2547        options.hermitian_tol.is_finite() && options.hermitian_tol >= 0.0,
2548        "hermitian_krylov_expm_multiply: hermitian_tol must be finite and non-negative"
2549    );
2550    Ok(())
2551}
2552
2553fn projected_matrix_from_columns(
2554    h_cols: &[Vec<Complex64>],
2555    dim: usize,
2556) -> Result<Matrix<Complex64>> {
2557    let matrix_len = dim.checked_mul(dim).ok_or_else(|| {
2558        anyhow::anyhow!("Krylov projected matrix shape ({dim}, {dim}) overflows usize")
2559    })?;
2560    let mut data = vec![Complex64::new(0.0, 0.0); matrix_len];
2561    for col in 0..dim {
2562        for row in 0..dim {
2563            if let Some(value) = h_cols.get(col).and_then(|h_col| h_col.get(row)) {
2564                data[row + dim * col] = *value;
2565            }
2566        }
2567    }
2568    Ok(Matrix::from_col_major_vec(dim, dim, data))
2569}
2570
2571fn checked_krylov_capacity(max_iter: usize, solver: &'static str) -> Result<usize> {
2572    max_iter.checked_add(1).ok_or_else(|| {
2573        anyhow::Error::new(KrylovError::InvalidOptions {
2574            solver,
2575            reason: "max_iter + 1 overflows usize".to_string(),
2576        })
2577    })
2578}
2579
2580fn any_scalar_to_complex(value: &AnyScalar) -> Complex64 {
2581    value
2582        .as_c64()
2583        .unwrap_or_else(|| Complex64::new(value.real(), 0.0))
2584}
2585
2586fn any_scalar_from_complex(value: Complex64, tolerance: f64) -> Result<AnyScalar> {
2587    if value.im.abs() <= tolerance {
2588        Ok(AnyScalar::new_real(value.re))
2589    } else {
2590        Ok(AnyScalar::new_complex(value.re, value.im))
2591    }
2592}
2593
2594fn hermitian_lanczos_threshold(eigenvalue: f64, options: &HermitianLanczosOptions) -> f64 {
2595    options.atol.max(options.rtol * eigenvalue.abs().max(1.0))
2596}
2597
2598fn hermitian_true_residual_norm<T, F>(apply_a: &F, eigenvector: &T, eigenvalue: f64) -> Result<f64>
2599where
2600    T: TensorVectorSpace,
2601    F: Fn(&T) -> Result<T>,
2602{
2603    let av = apply_a(eigenvector)?;
2604    let lambda_v = eigenvector.scale(AnyScalar::new_real(eigenvalue))?;
2605    let residual = av.axpby(
2606        AnyScalar::new_real(1.0),
2607        &lambda_v,
2608        AnyScalar::new_real(-1.0),
2609    )?;
2610    Ok(residual.norm()?)
2611}
2612
2613fn finalize_hermitian_lanczos_result<T, F>(
2614    apply_a: &F,
2615    basis: &[T],
2616    ritz: &HermitianRitzState,
2617    options: &HermitianLanczosOptions,
2618) -> Result<HermitianLanczosResult<T>>
2619where
2620    T: TensorVectorSpace,
2621    F: Fn(&T) -> Result<T>,
2622{
2623    let eigenvector = combine_basis_with_coefficients(basis, &ritz.coefficients, options)?;
2624    let residual_norm = hermitian_true_residual_norm(apply_a, &eigenvector, ritz.eigenvalue)?;
2625    let threshold = hermitian_lanczos_threshold(ritz.eigenvalue, options);
2626    let converged = residual_norm <= threshold;
2627
2628    if options.verbose {
2629        eprintln!(
2630            "Hermitian Lanczos final check iter {}: residual_estimate={:.6e} true_residual={:.6e} threshold={:.6e}",
2631            ritz.iterations, ritz.residual_estimate, residual_norm, threshold
2632        );
2633    }
2634
2635    Ok(HermitianLanczosResult {
2636        eigenvalue: ritz.eigenvalue,
2637        eigenvector,
2638        iterations: ritz.iterations,
2639        residual_norm,
2640        converged,
2641    })
2642}
2643
2644fn combine_basis_with_coefficients<T>(
2645    basis: &[T],
2646    coefficients: &[Complex64],
2647    options: &HermitianLanczosOptions,
2648) -> Result<T>
2649where
2650    T: TensorVectorSpace,
2651{
2652    anyhow::ensure!(
2653        !basis.is_empty(),
2654        "hermitian_lanczos_lowest_eigenpair: empty Krylov basis"
2655    );
2656    anyhow::ensure!(
2657        basis.len() == coefficients.len(),
2658        "hermitian_lanczos_lowest_eigenpair: coefficient length {} does not match basis length {}",
2659        coefficients.len(),
2660        basis.len()
2661    );
2662
2663    let mut result = basis[0].scale(any_scalar_from_complex(
2664        coefficients[0],
2665        options.hermitian_tol,
2666    )?)?;
2667    for (basis_vector, coefficient) in basis.iter().zip(coefficients.iter()).skip(1) {
2668        result = result.axpby(
2669            AnyScalar::new_real(1.0),
2670            basis_vector,
2671            any_scalar_from_complex(*coefficient, options.hermitian_tol)?,
2672        )?;
2673    }
2674    Ok(result)
2675}
2676
2677fn combine_basis_with_complex_coefficients<T>(
2678    basis: &[T],
2679    coefficients: &[Complex64],
2680    hermitian_tol: f64,
2681    context: &'static str,
2682) -> Result<T>
2683where
2684    T: TensorVectorSpace,
2685{
2686    anyhow::ensure!(!basis.is_empty(), "{context}: empty Krylov basis");
2687    anyhow::ensure!(
2688        basis.len() == coefficients.len(),
2689        "{context}: coefficient length {} does not match basis length {}",
2690        coefficients.len(),
2691        basis.len()
2692    );
2693
2694    let mut result = basis[0].scale(any_scalar_from_complex(coefficients[0], hermitian_tol)?)?;
2695    for (basis_vector, coefficient) in basis.iter().zip(coefficients.iter()).skip(1) {
2696        result = result.axpby(
2697            AnyScalar::new_real(1.0),
2698            basis_vector,
2699            any_scalar_from_complex(*coefficient, hermitian_tol)?,
2700        )?;
2701    }
2702    Ok(result)
2703}
2704
2705/// Compute Givens rotation coefficients to eliminate b in (a, b).
2706/// This function keeps computation in `AnyScalar` space to preserve AD metadata
2707/// as much as possible.
2708fn compute_givens_rotation(a: &AnyScalar, b: &AnyScalar) -> (AnyScalar, AnyScalar) {
2709    let a_abs = a.abs();
2710    let b_abs = b.abs();
2711    let r = (a_abs * a_abs + b_abs * b_abs).sqrt();
2712    if r < 1e-15 {
2713        (AnyScalar::new_real(1.0), AnyScalar::new_real(0.0))
2714    } else if a_abs < 1e-15 {
2715        (
2716            AnyScalar::new_real(0.0),
2717            b.clone().conj() / AnyScalar::new_real(r),
2718        )
2719    } else {
2720        let phase = a.clone() / AnyScalar::new_real(a_abs);
2721        (
2722            AnyScalar::new_real(a_abs / r),
2723            phase * b.clone().conj() / AnyScalar::new_real(r),
2724        )
2725    }
2726}
2727
2728/// Apply Givens rotation: (c, s) @ (x, y) -> (c*x + s*y, -conj(s)*x + c*y) for complex
2729/// or (c*x + s*y, -s*x + c*y) for real.
2730/// This function keeps computation in `AnyScalar` space to preserve AD metadata
2731/// as much as possible.
2732fn apply_givens_rotation(
2733    c: &AnyScalar,
2734    s: &AnyScalar,
2735    x: &AnyScalar,
2736    y: &AnyScalar,
2737) -> (AnyScalar, AnyScalar) {
2738    let new_x = c.clone() * x.clone() + s.clone() * y.clone();
2739    let new_y = -(s.clone().conj() * x.clone()) + c.clone() * y.clone();
2740    (new_x, new_y)
2741}
2742
2743/// Solve upper triangular system R y = g using back substitution.
2744fn solve_upper_triangular(h: &[Vec<AnyScalar>], g: &[AnyScalar]) -> Result<Vec<AnyScalar>> {
2745    let n = g.len();
2746    if n == 0 {
2747        return Ok(vec![]);
2748    }
2749
2750    let mut y = vec![AnyScalar::new_real(0.0); n];
2751
2752    for i in (0..n).rev() {
2753        let mut sum = g[i].clone();
2754
2755        for j in (i + 1)..n {
2756            // sum = sum - h[j][i] * y[j]
2757            let prod = h[j][i].clone() * y[j].clone();
2758            sum = sum - prod;
2759        }
2760
2761        let h_ii = &h[i][i];
2762        if h_ii.abs() < 1e-15 {
2763            return Err(anyhow::anyhow!(
2764                "Near-singular upper triangular matrix in GMRES"
2765            ));
2766        }
2767
2768        y[i] = sum / h_ii.clone();
2769    }
2770
2771    Ok(y)
2772}
2773
2774/// Update solution: x_new = x + sum_i y_i * v_i
2775fn update_solution<T: TensorVectorSpace>(x: &T, v_basis: &[T], y: &[AnyScalar]) -> Result<T> {
2776    let mut result = x.clone();
2777
2778    for (vi, yi) in v_basis.iter().zip(y.iter()) {
2779        let scaled_vi = vi.scale(yi.clone())?;
2780        // result = result + scaled_vi = 1.0 * result + 1.0 * scaled_vi
2781        result = result.axpby(
2782            AnyScalar::new_real(1.0),
2783            &scaled_vi,
2784            AnyScalar::new_real(1.0),
2785        )?;
2786    }
2787
2788    Ok(result)
2789}
2790
2791/// Update solution with truncation: x_new = truncate(x + sum_i y_i * v_i)
2792fn update_solution_truncated<T, Tr>(
2793    x: &T,
2794    v_basis: &[T],
2795    y: &[AnyScalar],
2796    truncate: &Tr,
2797) -> Result<T>
2798where
2799    T: TensorVectorSpace,
2800    Tr: Fn(&mut T) -> Result<()>,
2801{
2802    let mut result = x.clone();
2803    // Detect if x is effectively zero.
2804    // When x is created via scale(0.0), it preserves the original bond structure
2805    // (e.g., bond dim 4), causing axpby to double bond dimensions unnecessarily.
2806    // By detecting zero, we can use scaled_vi directly, avoiding the doubling.
2807    let mut result_is_zero = x.norm()? == 0.0;
2808
2809    for (vi, yi) in v_basis.iter().zip(y.iter()) {
2810        let scaled_vi = vi.scale(yi.clone())?;
2811        if result_is_zero {
2812            result = scaled_vi;
2813            result_is_zero = false;
2814        } else {
2815            result = result.axpby(
2816                AnyScalar::new_real(1.0),
2817                &scaled_vi,
2818                AnyScalar::new_real(1.0),
2819            )?;
2820        }
2821        // Truncate after each addition to control bond dimension growth
2822        truncate(&mut result)?;
2823    }
2824
2825    Ok(result)
2826}
2827
2828#[cfg(test)]
2829mod tests;