Skip to main content

qmbed/solve/
mod.rs

1use std::sync::Arc;
2
3use nalgebra::{DMatrix, DVector, SymmetricEigen};
4use num_complex::Complex64;
5
6use crate::backend;
7use crate::operator::{
8    ExpOp, LinearOperator, MatrixFormat, ShiftedLinearSolver, TimeDependentOperator,
9    materialize_dense,
10};
11use crate::runtime::CpuRuntime;
12use crate::{QmbedError, Result};
13
14const EXPM_TAYLOR_THETA: &[(usize, f64)] = &[
15    (1, 2.29e-16),
16    (2, 2.58e-8),
17    (3, 1.39e-5),
18    (4, 3.40e-4),
19    (5, 2.40e-3),
20    (6, 9.07e-3),
21    (7, 2.38e-2),
22    (8, 5.00e-2),
23    (9, 8.96e-2),
24    (10, 1.44e-1),
25    (11, 2.14e-1),
26    (12, 3.00e-1),
27    (13, 4.00e-1),
28    (14, 5.14e-1),
29    (15, 6.41e-1),
30    (16, 7.81e-1),
31    (17, 9.31e-1),
32    (18, 1.09),
33    (19, 1.26),
34    (20, 1.44),
35    (21, 1.62),
36    (22, 1.82),
37    (23, 2.01),
38    (24, 2.22),
39    (25, 2.43),
40    (26, 2.64),
41    (27, 2.86),
42    (28, 3.08),
43    (29, 3.31),
44    (30, 3.54),
45    (35, 4.7),
46    (40, 6.0),
47    (45, 7.2),
48    (50, 8.5),
49    (55, 9.9),
50];
51
52// Daniel--Gragg--Kaufman--Stewart reorthogonalization criterion. A second
53// modified Gram--Schmidt pass is useful only when the first pass removes a
54// substantial fraction of the vector norm. Keeping the criterion here makes
55// clustered-spectrum robustness a property of the generic Lanczos backend
56// without charging every iteration for two unconditional O(m n) passes.
57const DGKS_REORTHOGONALIZATION_THRESHOLD: f64 = std::f64::consts::FRAC_1_SQRT_2;
58
59fn shifted_trace_and_one_norm(
60    operator: &(impl LinearOperator + ?Sized),
61) -> Result<(Complex64, f64)> {
62    let (rows, columns) = operator.shape();
63    if rows != columns {
64        return Err(QmbedError::DimensionMismatch(
65            "exponential action requires a square operator".into(),
66        ));
67    }
68    if rows == 0 {
69        return Ok((Complex64::new(0.0, 0.0), 0.0));
70    }
71    let entries = match operator.stored_triplets()? {
72        Some(entries) => entries,
73        None => {
74            let dense = materialize_dense(operator)?;
75            dense
76                .into_iter()
77                .enumerate()
78                .filter_map(|(offset, value)| {
79                    (value.norm() > 0.0).then_some((offset / columns, offset % columns, value))
80                })
81                .collect()
82        }
83    };
84    let mut diagonal = vec![Complex64::new(0.0, 0.0); rows];
85    let mut column_sums = vec![0.0; columns];
86    for (row, column, value) in entries {
87        if !value.re.is_finite() || !value.im.is_finite() {
88            return Err(QmbedError::InvalidOptions(
89                "exponential generator entries must be finite".into(),
90            ));
91        }
92        if row == column {
93            diagonal[row] += value;
94        } else {
95            column_sums[column] += value.norm();
96        }
97    }
98    let trace = diagonal.iter().copied().sum::<Complex64>();
99    let shift = trace / rows as f64;
100    for (column, value) in diagonal.into_iter().enumerate() {
101        column_sums[column] += (value - shift).norm();
102    }
103    Ok((shift, column_sums.into_iter().fold(0.0_f64, f64::max)))
104}
105
106fn vector_infinity_norm(vector: &[Complex64]) -> f64 {
107    vector.iter().map(|value| value.norm()).fold(0.0, f64::max)
108}
109
110/// Prepared Al-Mohy--Higham exponential-action plan.
111///
112/// Construction computes the trace shift, exact stored one-norm, Taylor
113/// degree, and scaling count once. Repeated vector and batch applications then
114/// perform only matrix-vector products and vector updates.
115#[derive(Clone, Debug)]
116pub struct ExpmActionPlan {
117    dimension: usize,
118    coefficient: Complex64,
119    shift: Complex64,
120    degree: usize,
121    scaling: usize,
122    tolerance: f64,
123}
124
125impl ExpmActionPlan {
126    pub fn new(
127        operator: &(impl LinearOperator + ?Sized),
128        coefficient: Complex64,
129        max_degree: usize,
130        tolerance: f64,
131        max_substeps: usize,
132    ) -> Result<Self> {
133        let (rows, columns) = operator.shape();
134        if rows != columns {
135            return Err(QmbedError::DimensionMismatch(
136                "exponential action requires a square operator".into(),
137            ));
138        }
139        if !coefficient.re.is_finite()
140            || !coefficient.im.is_finite()
141            || max_degree == 0
142            || !tolerance.is_finite()
143            || tolerance <= 0.0
144            || max_substeps == 0
145        {
146            return Err(QmbedError::InvalidOptions(
147                "invalid exponential coefficient or numerical controls".into(),
148            ));
149        }
150        let (shift, shifted_one_norm) = shifted_trace_and_one_norm(operator)?;
151        let scaled_norm = coefficient.norm() * shifted_one_norm;
152        let (degree, scaling) = if scaled_norm == 0.0 {
153            (0, 1)
154        } else {
155            EXPM_TAYLOR_THETA
156                .iter()
157                .copied()
158                .filter(|(degree, _)| *degree <= max_degree)
159                .map(|(degree, theta)| {
160                    let scaling = (scaled_norm / theta).ceil().max(1.0) as usize;
161                    (degree, scaling)
162                })
163                .min_by_key(|(degree, scaling)| degree.saturating_mul(*scaling))
164                .ok_or_else(|| {
165                    QmbedError::InvalidOptions(
166                        "Taylor degree is below the minimum supported degree".into(),
167                    )
168                })?
169        };
170        if scaling > max_substeps {
171            return Err(QmbedError::NonConvergence {
172                iterations: max_substeps,
173                residual: scaled_norm,
174            });
175        }
176        Ok(Self {
177            dimension: rows,
178            coefficient,
179            shift,
180            degree,
181            scaling,
182            tolerance,
183        })
184    }
185
186    pub const fn coefficient(&self) -> Complex64 {
187        self.coefficient
188    }
189
190    pub const fn degree(&self) -> usize {
191        self.degree
192    }
193
194    pub const fn scaling(&self) -> usize {
195        self.scaling
196    }
197
198    pub fn apply(
199        &self,
200        operator: &(impl LinearOperator + ?Sized),
201        initial: &[Complex64],
202    ) -> Result<Vec<Complex64>> {
203        if operator.shape() != (self.dimension, self.dimension) || initial.len() != self.dimension {
204            return Err(QmbedError::DimensionMismatch(
205                "exponential plan, operator, and state dimensions do not match".into(),
206            ));
207        }
208        if self.coefficient.norm() <= f64::EPSILON || vector_infinity_norm(initial) == 0.0 {
209            return Ok(initial.to_vec());
210        }
211        let factor = self.coefficient / self.scaling as f64;
212        let eta = (factor * self.shift).exp();
213        if !eta.re.is_finite() || !eta.im.is_finite() {
214            return Err(QmbedError::UnsupportedBackend(
215                "exponential action overflowed its scalar trace shift".into(),
216            ));
217        }
218        let mut state = initial.to_vec();
219        let mut applied = vec![Complex64::new(0.0, 0.0); self.dimension];
220        for _ in 0..self.scaling {
221            let mut term = state.clone();
222            let mut sum = state.clone();
223            let mut previous_norm = vector_infinity_norm(&term);
224            for order in 1..=self.degree {
225                operator.apply(&term, &mut applied)?;
226                let scale = factor / order as f64;
227                for index in 0..self.dimension {
228                    applied[index] = scale * (applied[index] - self.shift * term[index]);
229                }
230                std::mem::swap(&mut term, &mut applied);
231                for (total, value) in sum.iter_mut().zip(&term) {
232                    *total += *value;
233                }
234                let term_norm = vector_infinity_norm(&term);
235                if previous_norm + term_norm
236                    <= self.tolerance * vector_infinity_norm(&sum).max(f64::MIN_POSITIVE)
237                {
238                    break;
239                }
240                previous_norm = term_norm;
241            }
242            for (value, total) in state.iter_mut().zip(sum) {
243                *value = eta * total;
244            }
245        }
246        Ok(state)
247    }
248}
249
250/// Reusable exponential-action plan for vectors and batches.
251#[derive(Clone, Debug)]
252pub struct ExpmMultiplyParallel {
253    inner: ExpOp,
254}
255
256impl ExpmMultiplyParallel {
257    pub fn new(
258        operator: Arc<dyn LinearOperator>,
259        coefficient: Complex64,
260        krylov_dimension: usize,
261        tolerance: f64,
262        max_substeps: usize,
263    ) -> Result<Self> {
264        Ok(Self {
265            inner: ExpOp::new(
266                operator,
267                coefficient,
268                krylov_dimension,
269                tolerance,
270                max_substeps,
271            )?,
272        })
273    }
274
275    pub const fn coefficient(&self) -> Complex64 {
276        self.inner.exponent()
277    }
278
279    pub fn set_coefficient(&mut self, coefficient: Complex64) -> Result<()> {
280        self.inner.set_exponent(coefficient)
281    }
282
283    pub fn apply_in_place(&self, state: &mut [Complex64]) -> Result<()> {
284        let input = state.to_vec();
285        self.inner.apply(&input, state)
286    }
287
288    pub fn apply_batch(&self, states: &[Vec<Complex64>]) -> Result<Vec<Vec<Complex64>>> {
289        self.apply_batch_with_runtime(
290            &CpuRuntime::from_profile(crate::runtime::ExecutionProfile::serial())?,
291            states,
292        )
293    }
294
295    pub fn apply_batch_with_runtime(
296        &self,
297        runtime: &CpuRuntime,
298        states: &[Vec<Complex64>],
299    ) -> Result<Vec<Vec<Complex64>>> {
300        let dimension = self.inner.shape().1;
301        runtime.map_ordered(states, |state| {
302            if state.len() != dimension {
303                return Err(QmbedError::DimensionMismatch(
304                    "exponential batch column has the wrong length".into(),
305                ));
306            }
307            let mut output = vec![Complex64::new(0.0, 0.0); dimension];
308            self.inner.apply(state, &mut output)?;
309            Ok(output)
310        })
311    }
312}
313
314impl LinearOperator for ExpmMultiplyParallel {
315    fn shape(&self) -> (usize, usize) {
316        self.inner.shape()
317    }
318
319    fn format(&self) -> MatrixFormat {
320        MatrixFormat::MatrixFree
321    }
322
323    fn apply(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
324        self.inner.apply(input, output)
325    }
326}
327
328/// Spectral region requested from a selected Hermitian eigensolver.
329#[derive(Clone, Copy, Debug, PartialEq)]
330#[non_exhaustive]
331pub enum SpectrumTarget {
332    /// Algebraically smallest eigenvalues.
333    SmallestAlgebraic,
334    /// Algebraically largest eigenvalues.
335    LargestAlgebraic,
336    /// Eigenvalues with smallest absolute value.
337    SmallestMagnitude,
338    /// Eigenvalues with largest absolute value.
339    LargestMagnitude,
340    /// A balanced selection from both algebraic ends.
341    BothEnds,
342    /// Eigenvalues nearest the supplied real shift.
343    Shift(f64),
344}
345
346/// Controls target selection, search-space size, and convergence for [`eigsh`].
347#[derive(Clone, Debug, PartialEq)]
348#[non_exhaustive]
349pub struct EigshOptions {
350    /// Number of requested eigenpairs; must be smaller than the dimension.
351    pub eigenpairs: usize,
352    /// Region of the real Hermitian spectrum to target.
353    pub target: SpectrumTarget,
354    /// Optional Lanczos or restart window dimension.
355    pub krylov_dimension: Option<usize>,
356    /// Required residual norm for convergence.
357    pub tolerance: f64,
358    /// Maximum Lanczos or restart iteration budget.
359    pub max_iterations: usize,
360    /// Deterministic seed for the initial vector.
361    pub seed: u64,
362}
363
364const GUARANTEED_DENSE_EIGSH_CROSSOVER: usize = 128;
365const AUTOMATIC_DENSE_EIGSH_CROSSOVER: usize = 256;
366const FULL_KRYLOV_DENSE_FALLBACK: usize = 2_048;
367const TRIDIAGONAL_LANCZOS_WINDOW: usize = 96;
368
369fn use_dense_eigsh(dimension: usize, options: &EigshOptions) -> bool {
370    dimension <= GUARANTEED_DENSE_EIGSH_CROSSOVER
371        || (dimension <= AUTOMATIC_DENSE_EIGSH_CROSSOVER && options.krylov_dimension.is_none())
372}
373
374impl EigshOptions {
375    /// Construct solver controls for an arbitrary spectral target.
376    pub fn new(eigenpairs: usize, target: SpectrumTarget) -> Self {
377        Self {
378            eigenpairs,
379            target,
380            krylov_dimension: None,
381            tolerance: 1.0e-10,
382            max_iterations: 1_000,
383            seed: 0,
384        }
385    }
386
387    /// Construct default controls for the algebraically lowest eigenpairs.
388    pub fn smallest_algebraic(eigenpairs: usize) -> Self {
389        Self::new(eigenpairs, SpectrumTarget::SmallestAlgebraic)
390    }
391
392    /// Construct default controls for eigenpairs nearest a real shift.
393    pub fn near_shift(eigenpairs: usize, shift: f64) -> Self {
394        Self::new(eigenpairs, SpectrumTarget::Shift(shift))
395    }
396
397    /// Set the optional Lanczos or restart window dimension.
398    #[must_use]
399    pub fn with_krylov_dimension(mut self, dimension: usize) -> Self {
400        self.krylov_dimension = Some(dimension);
401        self
402    }
403
404    /// Set the required residual norm.
405    #[must_use]
406    pub fn with_tolerance(mut self, tolerance: f64) -> Self {
407        self.tolerance = tolerance;
408        self
409    }
410
411    /// Set the maximum Lanczos or restart iteration budget.
412    #[must_use]
413    pub fn with_max_iterations(mut self, max_iterations: usize) -> Self {
414        self.max_iterations = max_iterations;
415        self
416    }
417
418    /// Set the deterministic initial-vector seed.
419    #[must_use]
420    pub fn with_seed(mut self, seed: u64) -> Self {
421        self.seed = seed;
422        self
423    }
424
425    fn validate(&self, dimension: usize) -> Result<()> {
426        if self.eigenpairs == 0 || self.eigenpairs >= dimension {
427            return Err(QmbedError::InvalidOptions(
428                "eigenpairs must be positive and smaller than the operator dimension".into(),
429            ));
430        }
431        if !self.tolerance.is_finite() || self.tolerance <= 0.0 || self.max_iterations == 0 {
432            return Err(QmbedError::InvalidOptions(
433                "tolerance and max_iterations must be positive".into(),
434            ));
435        }
436        if self
437            .krylov_dimension
438            .is_some_and(|size| size <= self.eigenpairs || size > dimension)
439        {
440            return Err(QmbedError::InvalidOptions(
441                "krylov_dimension must exceed eigenpairs and not exceed dimension".into(),
442            ));
443        }
444        if matches!(self.target, SpectrumTarget::Shift(value) if !value.is_finite()) {
445            return Err(QmbedError::InvalidOptions("shift must be finite".into()));
446        }
447        Ok(())
448    }
449}
450
451/// Values, vectors, residual evidence, and work counters from an eigensolve.
452#[derive(Clone, Debug)]
453pub struct Eigensystem {
454    /// Ordered real eigenvalues or Ritz values.
455    pub eigenvalues: Vec<f64>,
456    /// Column vectors corresponding to `eigenvalues`.
457    pub eigenvectors: Vec<Vec<Complex64>>,
458    /// Norm of `A v - λ v` for each returned pair.
459    pub residuals: Vec<f64>,
460    /// Algorithm iteration count.
461    pub iterations: usize,
462    /// Total modified Gram-Schmidt passes.
463    pub reorthogonalization_passes: usize,
464    /// Selective DGKS second passes triggered by loss of norm.
465    pub conditional_second_passes: usize,
466    /// Whether every requested residual met the tolerance.
467    pub converged: bool,
468}
469
470/// Reusable state for a sequence of related Hermitian eigenproblems.
471///
472/// The workspace keeps the complete converged invariant subspace from the
473/// previous solve. [`eigsh_with_workspace`] preserves it as a thick initial
474/// subspace for restarted windows and combines all of its vectors into a
475/// balanced warm start when the cheaper tridiagonal Lanczos path applies.
476#[derive(Clone, Debug, Default)]
477pub struct EigshWorkspace {
478    dimension: usize,
479    initial_subspace: Vec<Vec<Complex64>>,
480}
481
482impl EigshWorkspace {
483    /// Create an empty workspace that accepts the first solved dimension.
484    pub const fn new() -> Self {
485        Self {
486            dimension: 0,
487            initial_subspace: Vec::new(),
488        }
489    }
490
491    /// Forget the stored dimension and converged invariant subspace.
492    pub fn clear(&mut self) {
493        self.dimension = 0;
494        self.initial_subspace.clear();
495    }
496
497    /// Return the operator dimension associated with the stored subspace.
498    pub const fn dimension(&self) -> usize {
499        self.dimension
500    }
501
502    /// Borrow the complete converged subspace from the previous solve.
503    pub fn initial_subspace(&self) -> &[Vec<Complex64>] {
504        &self.initial_subspace
505    }
506
507    /// Install a user-provided warm-start subspace after validating dimensions.
508    pub fn set_initial_subspace(
509        &mut self,
510        dimension: usize,
511        vectors: impl IntoIterator<Item = Vec<Complex64>>,
512    ) -> Result<()> {
513        let vectors: Vec<_> = vectors.into_iter().collect();
514        for vector in &vectors {
515            validate_eigsh_initial(vector, dimension)?;
516        }
517        self.dimension = dimension;
518        self.initial_subspace = vectors;
519        Ok(())
520    }
521
522    fn update(&mut self, dimension: usize, eigensystem: &Eigensystem) {
523        self.dimension = dimension;
524        self.initial_subspace.clone_from(&eigensystem.eigenvectors);
525    }
526}
527
528/// Controls whether [`eigh_with_options`] retains the complete eigenvector matrix.
529#[derive(Clone, Copy, Debug, Eq, PartialEq)]
530#[non_exhaustive]
531pub struct EighOptions {
532    /// Return eigenvectors when `true`; values and residuals are always returned.
533    pub return_eigenvectors: bool,
534}
535
536impl Default for EighOptions {
537    fn default() -> Self {
538        Self {
539            return_eigenvectors: true,
540        }
541    }
542}
543
544impl EighOptions {
545    /// Construct controls which retain or discard the full eigenvector matrix.
546    pub const fn new(return_eigenvectors: bool) -> Self {
547        Self {
548            return_eigenvectors,
549        }
550    }
551
552    /// Compute eigenvalues and residual evidence without returning vectors.
553    pub const fn values_only() -> Self {
554        Self::new(false)
555    }
556}
557
558pub(crate) fn hermitian_eigenpairs_all(
559    operator: &(impl LinearOperator + ?Sized),
560) -> Result<(Vec<f64>, Vec<Vec<Complex64>>)> {
561    let shape = operator.shape();
562    if shape.0 != shape.1 {
563        return Err(QmbedError::DimensionMismatch(
564            "a square operator is required".into(),
565        ));
566    }
567    let dense = materialize_dense(operator)?;
568    let dimension = shape.0;
569    if dimension == 0 {
570        return Ok((Vec::new(), Vec::new()));
571    }
572    for row in 0..dimension {
573        for column in 0..dimension {
574            if (dense[row * dimension + column] - dense[column * dimension + row].conj()).norm()
575                > 1.0e-12
576            {
577                return Err(QmbedError::NonHermitian);
578            }
579        }
580    }
581    let eigensystem = backend::hermitian_eigenpairs(&dense, dimension)?;
582    Ok((eigensystem.eigenvalues, eigensystem.eigenvectors))
583}
584
585/// Complete eigendecomposition of a finite Hermitian operator.
586pub fn eigh<O>(operator: &O) -> Result<Eigensystem>
587where
588    O: LinearOperator + ?Sized,
589{
590    let (eigenvalues, eigenvectors) = hermitian_eigenpairs_all(operator)?;
591    let residuals = eigenvalues
592        .iter()
593        .zip(&eigenvectors)
594        .map(|(&value, vector)| residual_norm(operator, value, vector))
595        .collect::<Result<Vec<_>>>()?;
596    Ok(Eigensystem {
597        eigenvalues,
598        eigenvectors,
599        residuals,
600        iterations: usize::from(operator.shape().0 != 0),
601        reorthogonalization_passes: 0,
602        conditional_second_passes: 0,
603        converged: true,
604    })
605}
606
607/// Compute the complete Hermitian spectrum with optional vector elision.
608///
609/// The operator is materialized once as dense storage. This routine is for
610/// finite systems where the cubic full-spectrum cost is intentional.
611pub fn eigh_with_options<O>(operator: &O, options: EighOptions) -> Result<Eigensystem>
612where
613    O: LinearOperator + ?Sized,
614{
615    let mut result = eigh(operator)?;
616    if !options.return_eigenvectors {
617        result.eigenvectors.clear();
618    }
619    Ok(result)
620}
621
622fn residual_norm(
623    operator: &(impl LinearOperator + ?Sized),
624    eigenvalue: f64,
625    vector: &[Complex64],
626) -> Result<f64> {
627    let mut applied = vec![Complex64::new(0.0, 0.0); vector.len()];
628    operator.apply(vector, &mut applied)?;
629    Ok(applied
630        .iter()
631        .zip(vector)
632        .map(|(actual, component)| (*actual - eigenvalue * *component).norm_sqr())
633        .sum::<f64>()
634        .sqrt())
635}
636
637fn rayleigh_value_and_residual(
638    operator: &(impl LinearOperator + ?Sized),
639    vector: &[Complex64],
640) -> Result<(f64, f64)> {
641    let mut applied = vec![Complex64::new(0.0, 0.0); vector.len()];
642    operator.apply(vector, &mut applied)?;
643    let norm_squared = inner(vector, vector).re;
644    if !norm_squared.is_finite() || norm_squared <= f64::EPSILON {
645        return Err(QmbedError::NonConvergence {
646            iterations: 0,
647            residual: norm_squared.sqrt(),
648        });
649    }
650    let eigenvalue = inner(vector, &applied).re / norm_squared;
651    let residual = applied
652        .iter()
653        .zip(vector)
654        .map(|(actual, component)| (*actual - eigenvalue * *component).norm_sqr())
655        .sum::<f64>()
656        .sqrt();
657    Ok((eigenvalue, residual))
658}
659
660fn inner(left: &[Complex64], right: &[Complex64]) -> Complex64 {
661    left.iter()
662        .zip(right)
663        .map(|(left_value, right_value)| left_value.conj() * *right_value)
664        .sum()
665}
666
667fn vector_norm(vector: &[Complex64]) -> f64 {
668    vector.iter().map(Complex64::norm_sqr).sum::<f64>().sqrt()
669}
670
671fn validate_eigsh_initial(vector: &[Complex64], dimension: usize) -> Result<()> {
672    if vector.len() != dimension {
673        return Err(QmbedError::DimensionMismatch(
674            "eigsh initial vector does not match the operator".into(),
675        ));
676    }
677    if vector
678        .iter()
679        .any(|value| !value.re.is_finite() || !value.im.is_finite())
680    {
681        return Err(QmbedError::InvalidOptions(
682            "eigsh initial vector must contain only finite values".into(),
683        ));
684    }
685    let norm = vector_norm(vector);
686    if !norm.is_finite() || norm <= f64::EPSILON {
687        return Err(QmbedError::InvalidOptions(
688            "eigsh initial vector must have nonzero finite norm".into(),
689        ));
690    }
691    Ok(())
692}
693
694fn normalize(vector: &mut [Complex64]) -> Result<()> {
695    let norm = vector_norm(vector);
696    if !norm.is_finite() || norm <= f64::EPSILON {
697        return Err(QmbedError::NonConvergence {
698            iterations: 0,
699            residual: norm,
700        });
701    }
702    for value in vector {
703        *value /= norm;
704    }
705    Ok(())
706}
707
708fn balanced_subspace_start(initial_subspace: &[Vec<Complex64>]) -> Result<Vec<Complex64>> {
709    let dimension = initial_subspace
710        .first()
711        .map(Vec::len)
712        .ok_or_else(|| QmbedError::InvalidOptions("initial subspace must not be empty".into()))?;
713    let mut combined = vec![Complex64::new(0.0, 0.0); dimension];
714    let count = initial_subspace.len() as f64;
715    for (index, vector) in initial_subspace.iter().enumerate() {
716        let phase = std::f64::consts::TAU * index as f64 / count;
717        let coefficient = Complex64::from_polar(count.sqrt().recip(), phase);
718        for (value, component) in combined.iter_mut().zip(vector) {
719            *value += coefficient * *component;
720        }
721    }
722    if normalize(&mut combined).is_err() {
723        combined.clone_from(&initial_subspace[0]);
724        normalize(&mut combined)?;
725    }
726    Ok(combined)
727}
728
729fn deterministic_start(dimension: usize, seed: u64) -> Result<Vec<Complex64>> {
730    let mut state = seed | 1;
731    let mut vector = Vec::with_capacity(dimension);
732    for _ in 0..dimension {
733        state = state
734            .wrapping_mul(6_364_136_223_846_793_005)
735            .wrapping_add(1_442_695_040_888_963_407);
736        let mantissa = state >> 11;
737        let value = mantissa as f64 / ((1_u64 << 53) as f64) - 0.5;
738        vector.push(Complex64::new(value, 0.0));
739    }
740    normalize(&mut vector)?;
741    Ok(vector)
742}
743
744fn shifted_apply<O>(
745    operator: &O,
746    shift: f64,
747    input: &[Complex64],
748    output: &mut [Complex64],
749) -> Result<()>
750where
751    O: LinearOperator + ?Sized,
752{
753    operator.apply(input, output)?;
754    for (value, input_value) in output.iter_mut().zip(input) {
755        *value -= shift * *input_value;
756    }
757    Ok(())
758}
759
760fn gmres_shift_invert<O>(
761    operator: &O,
762    shift: f64,
763    right_hand_side: &[Complex64],
764    tolerance: f64,
765    max_iterations: usize,
766) -> Result<Vec<Complex64>>
767where
768    O: LinearOperator + ?Sized,
769{
770    let dimension = right_hand_side.len();
771    let right_norm = vector_norm(right_hand_side);
772    if right_norm <= f64::EPSILON {
773        return Ok(vec![Complex64::new(0.0, 0.0); dimension]);
774    }
775    let restart = dimension.clamp(1, 256);
776    let mut solution = vec![Complex64::new(0.0, 0.0); dimension];
777    let mut residual = right_hand_side.to_vec();
778    let mut applied = vec![Complex64::new(0.0, 0.0); dimension];
779    let mut iterations = 0;
780
781    while iterations < max_iterations {
782        let beta = vector_norm(&residual);
783        if beta <= tolerance * right_norm {
784            return Ok(solution);
785        }
786        let mut first = residual.clone();
787        for value in &mut first {
788            *value /= beta;
789        }
790        let mut basis = vec![first];
791        let cycle = restart.min(max_iterations - iterations);
792        let mut hessenberg = vec![vec![Complex64::new(0.0, 0.0); cycle]; cycle + 1];
793        let mut columns = 0;
794
795        for column in 0..cycle {
796            shifted_apply(operator, shift, &basis[column], &mut applied)?;
797            for _ in 0..2 {
798                for (row, vector) in basis.iter().enumerate() {
799                    let overlap = inner(vector, &applied);
800                    hessenberg[row][column] += overlap;
801                    for (value, basis_value) in applied.iter_mut().zip(vector) {
802                        *value -= overlap * *basis_value;
803                    }
804                }
805            }
806            let next_norm = vector_norm(&applied);
807            hessenberg[column + 1][column] = Complex64::new(next_norm, 0.0);
808            columns = column + 1;
809            if next_norm <= 1.0e-14 {
810                break;
811            }
812            let mut next = applied.clone();
813            for value in &mut next {
814                *value /= next_norm;
815            }
816            basis.push(next);
817        }
818
819        let mut normal = DMatrix::<Complex64>::zeros(columns, columns);
820        let mut projected_rhs = DVector::<Complex64>::zeros(columns);
821        for row in 0..columns {
822            projected_rhs[row] = hessenberg[0][row].conj() * beta;
823            for column in 0..columns {
824                normal[(row, column)] = (0..=columns)
825                    .map(|index| hessenberg[index][row].conj() * hessenberg[index][column])
826                    .sum();
827            }
828        }
829        let coefficients = normal
830            .lu()
831            .solve(&projected_rhs)
832            .ok_or(QmbedError::NonConvergence {
833                iterations,
834                residual: beta,
835            })?;
836        for (coefficient, vector) in coefficients.iter().zip(&basis) {
837            for (value, basis_value) in solution.iter_mut().zip(vector) {
838                *value += *coefficient * *basis_value;
839            }
840        }
841        shifted_apply(operator, shift, &solution, &mut applied)?;
842        for ((value, right_value), applied_value) in
843            residual.iter_mut().zip(right_hand_side).zip(&applied)
844        {
845            *value = *right_value - *applied_value;
846        }
847        iterations += columns;
848    }
849    Err(QmbedError::NonConvergence {
850        iterations,
851        residual: vector_norm(&residual),
852    })
853}
854
855/// Reusable action of `(A - shift I)^{-1}`. Stored CSC operators cache one
856/// sparse factorization; other operators reuse the plan and solve with
857/// restarted GMRES without materializing `A`.
858pub struct ShiftInvertPlan {
859    operator: Arc<dyn LinearOperator>,
860    shift: f64,
861    tolerance: f64,
862    max_iterations: usize,
863    factorization: Option<Box<dyn ShiftedLinearSolver>>,
864}
865
866impl std::fmt::Debug for ShiftInvertPlan {
867    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
868        formatter
869            .debug_struct("ShiftInvertPlan")
870            .field("shape", &self.operator.shape())
871            .field("shift", &self.shift)
872            .field("tolerance", &self.tolerance)
873            .field("max_iterations", &self.max_iterations)
874            .field("factorized", &self.factorization.is_some())
875            .finish()
876    }
877}
878
879impl ShiftInvertPlan {
880    pub fn new(
881        operator: Arc<dyn LinearOperator>,
882        shift: f64,
883        tolerance: f64,
884        max_iterations: usize,
885    ) -> Result<Self> {
886        let shape = operator.shape();
887        if shape.0 != shape.1
888            || !shift.is_finite()
889            || !tolerance.is_finite()
890            || tolerance <= 0.0
891            || max_iterations == 0
892        {
893            return Err(QmbedError::InvalidOptions(
894                "shift-invert needs a square operator, finite shift, positive tolerance, and positive iteration cap"
895                    .into(),
896            ));
897        }
898        let factorization = operator.shifted_solver(shift)?;
899        Ok(Self {
900            operator,
901            shift,
902            tolerance,
903            max_iterations,
904            factorization,
905        })
906    }
907
908    pub const fn shift(&self) -> f64 {
909        self.shift
910    }
911
912    pub const fn is_factorized(&self) -> bool {
913        self.factorization.is_some()
914    }
915
916    pub fn solve(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
917        if input.len() != self.operator.shape().0 || output.len() != input.len() {
918            return Err(QmbedError::DimensionMismatch(
919                "shift-invert input and output must match the operator dimension".into(),
920            ));
921        }
922        if let Some(factorization) = &self.factorization {
923            return factorization.solve(input, output);
924        }
925        let solved = gmres_shift_invert(
926            self.operator.as_ref(),
927            self.shift,
928            input,
929            self.tolerance,
930            self.max_iterations,
931        )?;
932        output.copy_from_slice(&solved);
933        Ok(())
934    }
935}
936
937impl LinearOperator for ShiftInvertPlan {
938    fn shape(&self) -> (usize, usize) {
939        self.operator.shape()
940    }
941
942    fn format(&self) -> MatrixFormat {
943        MatrixFormat::MatrixFree
944    }
945
946    fn apply(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
947        self.solve(input, output)
948    }
949}
950
951fn transformed_apply<O>(
952    operator: &O,
953    options: &EigshOptions,
954    shifted_solver: Option<&dyn ShiftedLinearSolver>,
955    input: &[Complex64],
956    output: &mut [Complex64],
957) -> Result<()>
958where
959    O: LinearOperator + ?Sized,
960{
961    match options.target {
962        SpectrumTarget::Shift(shift) => {
963            if let Some(solver) = shifted_solver {
964                return solver.solve(input, output);
965            }
966            let solved = gmres_shift_invert(
967                operator,
968                shift,
969                input,
970                (options.tolerance * 0.1).min(1.0e-10),
971                options.max_iterations.max(128),
972            )?;
973            output.copy_from_slice(&solved);
974            Ok(())
975        }
976        _ => operator.apply(input, output),
977    }
978}
979
980fn transformed_apply_real<O>(
981    operator: &O,
982    options: &EigshOptions,
983    shifted_solver: Option<&dyn ShiftedLinearSolver>,
984    input: &[f64],
985    output: &mut [f64],
986) -> Result<()>
987where
988    O: LinearOperator + ?Sized,
989{
990    match options.target {
991        SpectrumTarget::Shift(_) => shifted_solver
992            .ok_or_else(|| {
993                QmbedError::UnsupportedBackend(
994                    "real shift-invert requires a reusable real factorization".into(),
995                )
996            })?
997            .solve_real(input, output),
998        _ => operator.apply_real(input, output),
999    }
1000}
1001
1002fn select_indices(values: &[f64], target: SpectrumTarget, count: usize) -> Vec<usize> {
1003    if target == SpectrumTarget::BothEnds {
1004        let mut ordered: Vec<_> = (0..values.len()).collect();
1005        ordered.sort_by(|&left, &right| {
1006            values[left]
1007                .total_cmp(&values[right])
1008                .then_with(|| left.cmp(&right))
1009        });
1010        let lower = count / 2;
1011        let upper = count - lower;
1012        let mut selected = Vec::with_capacity(count);
1013        selected.extend(ordered.iter().take(lower).copied());
1014        selected.extend(ordered.iter().rev().take(upper).copied());
1015        selected.sort_by(|&left, &right| values[left].total_cmp(&values[right]));
1016        return selected;
1017    }
1018    let mut indices: Vec<_> = (0..values.len()).collect();
1019    indices.sort_by(|&left, &right| {
1020        let left_value = values[left];
1021        let right_value = values[right];
1022        let ordering = match target {
1023            SpectrumTarget::SmallestAlgebraic => left_value.total_cmp(&right_value),
1024            SpectrumTarget::LargestAlgebraic => right_value.total_cmp(&left_value),
1025            SpectrumTarget::SmallestMagnitude => left_value.abs().total_cmp(&right_value.abs()),
1026            SpectrumTarget::LargestMagnitude => right_value.abs().total_cmp(&left_value.abs()),
1027            SpectrumTarget::BothEnds => unreachable!(),
1028            SpectrumTarget::Shift(shift) => (left_value - shift)
1029                .abs()
1030                .total_cmp(&(right_value - shift).abs()),
1031        };
1032        ordering.then_with(|| left.cmp(&right))
1033    });
1034    indices.truncate(count);
1035    indices
1036}
1037
1038fn real_inner(left: &[f64], right: &[f64]) -> f64 {
1039    left.iter()
1040        .zip(right)
1041        .map(|(left_value, right_value)| left_value * right_value)
1042        .sum()
1043}
1044
1045fn real_vector_norm(vector: &[f64]) -> f64 {
1046    real_inner(vector, vector).sqrt()
1047}
1048
1049#[cfg_attr(not(test), allow(dead_code))]
1050fn dgks_reorthogonalize_real(basis: &[Vec<f64>], output: &mut [f64]) -> (f64, bool) {
1051    let norm_before_reorthogonalization = real_vector_norm(output);
1052    for vector in basis {
1053        let overlap = real_inner(vector, output);
1054        for (value, basis_value) in output.iter_mut().zip(vector) {
1055            *value -= overlap * *basis_value;
1056        }
1057    }
1058    let norm_after_first_pass = real_vector_norm(output);
1059    if norm_before_reorthogonalization > f64::EPSILON
1060        && norm_after_first_pass
1061            <= DGKS_REORTHOGONALIZATION_THRESHOLD * norm_before_reorthogonalization
1062    {
1063        for vector in basis {
1064            let overlap = real_inner(vector, output);
1065            for (value, basis_value) in output.iter_mut().zip(vector) {
1066                *value -= overlap * *basis_value;
1067            }
1068        }
1069        (real_vector_norm(output), true)
1070    } else {
1071        (norm_after_first_pass, false)
1072    }
1073}
1074
1075#[cfg_attr(not(test), allow(dead_code))]
1076fn dgks_reorthogonalize_complex(basis: &[Vec<Complex64>], output: &mut [Complex64]) -> (f64, bool) {
1077    let norm_before_reorthogonalization = vector_norm(output);
1078    for vector in basis {
1079        let overlap = inner(vector, output);
1080        for (value, basis_value) in output.iter_mut().zip(vector) {
1081            *value -= overlap * *basis_value;
1082        }
1083    }
1084    let norm_after_first_pass = vector_norm(output);
1085    if norm_before_reorthogonalization > f64::EPSILON
1086        && norm_after_first_pass
1087            <= DGKS_REORTHOGONALIZATION_THRESHOLD * norm_before_reorthogonalization
1088    {
1089        for vector in basis {
1090            let overlap = inner(vector, output);
1091            for (value, basis_value) in output.iter_mut().zip(vector) {
1092                *value -= overlap * *basis_value;
1093            }
1094        }
1095        (vector_norm(output), true)
1096    } else {
1097        (norm_after_first_pass, false)
1098    }
1099}
1100
1101fn normalize_real(vector: &mut [f64]) -> Result<()> {
1102    let norm = real_vector_norm(vector);
1103    if !norm.is_finite() || norm <= f64::EPSILON {
1104        return Err(QmbedError::NonConvergence {
1105            iterations: 0,
1106            residual: norm,
1107        });
1108    }
1109    for value in vector {
1110        *value /= norm;
1111    }
1112    Ok(())
1113}
1114
1115#[allow(dead_code)]
1116fn lanczos_eigsh_real<O>(
1117    operator: &O,
1118    options: &EigshOptions,
1119    initial: Option<&[Complex64]>,
1120    shifted_solver: Option<Box<dyn ShiftedLinearSolver>>,
1121) -> Result<Eigensystem>
1122where
1123    O: LinearOperator + ?Sized,
1124{
1125    let dimension = operator.shape().0;
1126    let requested_dimension = options
1127        .krylov_dimension
1128        .unwrap_or_else(|| (8 * options.eigenpairs + 64).max(256));
1129    let krylov_dimension = requested_dimension
1130        .min(options.max_iterations)
1131        .min(dimension);
1132    if krylov_dimension <= options.eigenpairs {
1133        return Err(QmbedError::InvalidOptions(
1134            "the effective Krylov dimension must exceed eigenpairs".into(),
1135        ));
1136    }
1137
1138    let first = if let Some(initial) = initial {
1139        if initial.len() != dimension {
1140            return Err(QmbedError::DimensionMismatch(
1141                "eigsh initial vector does not match the operator".into(),
1142            ));
1143        }
1144        let mut first: Vec<_> = initial.iter().map(|value| value.re).collect();
1145        normalize_real(&mut first)?;
1146        first
1147    } else {
1148        deterministic_start(dimension, options.seed)?
1149            .into_iter()
1150            .map(|value| value.re)
1151            .collect()
1152    };
1153    let mut basis = Vec::with_capacity(krylov_dimension);
1154    basis.push(first);
1155    let mut alphas = Vec::with_capacity(krylov_dimension);
1156    let mut betas = Vec::with_capacity(krylov_dimension.saturating_sub(1));
1157    let mut output = vec![0.0; dimension];
1158    let mut reorthogonalization_passes = 0;
1159    let mut conditional_second_passes = 0;
1160
1161    for iteration in 0..krylov_dimension {
1162        transformed_apply_real(
1163            operator,
1164            options,
1165            shifted_solver.as_deref(),
1166            &basis[iteration],
1167            &mut output,
1168        )?;
1169        let alpha = real_inner(&basis[iteration], &output);
1170        alphas.push(alpha);
1171        for (value, basis_value) in output.iter_mut().zip(&basis[iteration]) {
1172            *value -= alpha * *basis_value;
1173        }
1174        if iteration > 0 {
1175            let beta = betas[iteration - 1];
1176            for (value, previous) in output.iter_mut().zip(&basis[iteration - 1]) {
1177                *value -= beta * *previous;
1178            }
1179        }
1180
1181        let (beta, second_pass) = dgks_reorthogonalize_real(&basis, &mut output);
1182        reorthogonalization_passes += 1 + usize::from(second_pass);
1183        if second_pass {
1184            conditional_second_passes += 1;
1185        }
1186        if iteration + 1 == krylov_dimension || beta <= 1.0e-14 {
1187            break;
1188        }
1189        betas.push(beta);
1190        for value in &mut output {
1191            *value /= beta;
1192        }
1193        basis.push(output.clone());
1194    }
1195
1196    if basis.len() <= options.eigenpairs {
1197        return Err(QmbedError::NonConvergence {
1198            iterations: basis.len(),
1199            residual: f64::INFINITY,
1200        });
1201    }
1202    let size = basis.len();
1203    let mut tridiagonal = DMatrix::<f64>::zeros(size, size);
1204    for index in 0..size {
1205        tridiagonal[(index, index)] = alphas[index];
1206        if index + 1 < size {
1207            tridiagonal[(index, index + 1)] = betas[index];
1208            tridiagonal[(index + 1, index)] = betas[index];
1209        }
1210    }
1211    let decomposition = SymmetricEigen::new(tridiagonal);
1212    let transformed_target = if matches!(options.target, SpectrumTarget::Shift(_)) {
1213        SpectrumTarget::LargestMagnitude
1214    } else {
1215        options.target
1216    };
1217    let indices = select_indices(
1218        decomposition.eigenvalues.as_slice(),
1219        transformed_target,
1220        options.eigenpairs,
1221    );
1222
1223    let mut candidates = Vec::with_capacity(options.eigenpairs);
1224    for index in indices {
1225        let mut vector = vec![0.0; dimension];
1226        for (basis_index, basis_vector) in basis.iter().enumerate() {
1227            let coefficient = decomposition.eigenvectors[(basis_index, index)];
1228            for (value, basis_value) in vector.iter_mut().zip(basis_vector) {
1229                *value += coefficient * *basis_value;
1230            }
1231        }
1232        normalize_real(&mut vector)?;
1233        operator.apply_real(&vector, &mut output)?;
1234        let eigenvalue = real_inner(&vector, &output);
1235        let residual = output
1236            .iter()
1237            .zip(&vector)
1238            .map(|(actual, component)| (actual - eigenvalue * component).powi(2))
1239            .sum::<f64>()
1240            .sqrt();
1241        candidates.push((eigenvalue, vector, residual));
1242    }
1243    candidates.sort_by(|left, right| match options.target {
1244        SpectrumTarget::SmallestAlgebraic => left.0.total_cmp(&right.0),
1245        SpectrumTarget::LargestAlgebraic => right.0.total_cmp(&left.0),
1246        SpectrumTarget::SmallestMagnitude => left.0.abs().total_cmp(&right.0.abs()),
1247        SpectrumTarget::LargestMagnitude => right.0.abs().total_cmp(&left.0.abs()),
1248        SpectrumTarget::BothEnds => left.0.total_cmp(&right.0),
1249        SpectrumTarget::Shift(shift) => (left.0 - shift).abs().total_cmp(&(right.0 - shift).abs()),
1250    });
1251    let residuals: Vec<_> = candidates.iter().map(|candidate| candidate.2).collect();
1252    let failure_residual = candidates
1253        .iter()
1254        .filter_map(|candidate| {
1255            (candidate.2 > options.tolerance * candidate.0.abs().max(1.0)).then_some(candidate.2)
1256        })
1257        .fold(0.0_f64, f64::max);
1258    if failure_residual > 0.0 {
1259        return Err(QmbedError::NonConvergence {
1260            iterations: size,
1261            residual: failure_residual,
1262        });
1263    }
1264    Ok(Eigensystem {
1265        eigenvalues: candidates.iter().map(|candidate| candidate.0).collect(),
1266        eigenvectors: candidates
1267            .into_iter()
1268            .map(|candidate| {
1269                candidate
1270                    .1
1271                    .into_iter()
1272                    .map(|value| Complex64::new(value, 0.0))
1273                    .collect()
1274            })
1275            .collect(),
1276        residuals,
1277        iterations: size,
1278        reorthogonalization_passes,
1279        conditional_second_passes,
1280        converged: true,
1281    })
1282}
1283
1284#[allow(dead_code)]
1285fn lanczos_eigsh<O>(
1286    operator: &O,
1287    options: &EigshOptions,
1288    initial: Option<&[Complex64]>,
1289) -> Result<Eigensystem>
1290where
1291    O: LinearOperator + ?Sized,
1292{
1293    let real_compatible = operator.is_real()
1294        && initial.is_none_or(|vector| vector.iter().all(|value| value.im.abs() <= 1.0e-14));
1295    let mut shifted_solver = match options.target {
1296        SpectrumTarget::Shift(shift) if real_compatible => operator.shifted_solver(shift)?,
1297        _ => None,
1298    };
1299    if real_compatible
1300        && (!matches!(options.target, SpectrumTarget::Shift(_))
1301            || shifted_solver
1302                .as_ref()
1303                .is_some_and(|solver| solver.supports_real()))
1304    {
1305        return lanczos_eigsh_real(operator, options, initial, shifted_solver);
1306    }
1307    let dimension = operator.shape().0;
1308    let requested_dimension = options
1309        .krylov_dimension
1310        .unwrap_or_else(|| (8 * options.eigenpairs + 64).max(256));
1311    let krylov_dimension = requested_dimension
1312        .min(options.max_iterations)
1313        .min(dimension);
1314    if krylov_dimension <= options.eigenpairs {
1315        return Err(QmbedError::InvalidOptions(
1316            "the effective Krylov dimension must exceed eigenpairs".into(),
1317        ));
1318    }
1319
1320    let mut basis = Vec::with_capacity(krylov_dimension);
1321    basis.push(if let Some(initial) = initial {
1322        if initial.len() != dimension {
1323            return Err(QmbedError::DimensionMismatch(
1324                "eigsh initial vector does not match the operator".into(),
1325            ));
1326        }
1327        let mut initial = initial.to_vec();
1328        normalize(&mut initial)?;
1329        initial
1330    } else {
1331        deterministic_start(dimension, options.seed)?
1332    });
1333    let mut alphas = Vec::with_capacity(krylov_dimension);
1334    let mut betas = Vec::with_capacity(krylov_dimension.saturating_sub(1));
1335    let mut output = vec![Complex64::new(0.0, 0.0); dimension];
1336    let mut reorthogonalization_passes = 0;
1337    let mut conditional_second_passes = 0;
1338    if let SpectrumTarget::Shift(shift) = options.target {
1339        if shifted_solver.is_none() {
1340            shifted_solver = operator.shifted_solver(shift)?;
1341        }
1342    }
1343
1344    for iteration in 0..krylov_dimension {
1345        transformed_apply(
1346            operator,
1347            options,
1348            shifted_solver.as_deref(),
1349            &basis[iteration],
1350            &mut output,
1351        )?;
1352        let alpha = inner(&basis[iteration], &output).re;
1353        alphas.push(alpha);
1354        for (value, basis_value) in output.iter_mut().zip(&basis[iteration]) {
1355            *value -= alpha * *basis_value;
1356        }
1357        if iteration > 0 {
1358            let beta = betas[iteration - 1];
1359            for (value, previous) in output.iter_mut().zip(&basis[iteration - 1]) {
1360                *value -= beta * *previous;
1361            }
1362        }
1363
1364        let (beta, second_pass) = dgks_reorthogonalize_complex(&basis, &mut output);
1365        reorthogonalization_passes += 1 + usize::from(second_pass);
1366        if second_pass {
1367            conditional_second_passes += 1;
1368        }
1369        if iteration + 1 == krylov_dimension || beta <= 1.0e-14 {
1370            break;
1371        }
1372        betas.push(beta);
1373        for value in &mut output {
1374            *value /= beta;
1375        }
1376        basis.push(output.clone());
1377    }
1378
1379    if basis.len() <= options.eigenpairs {
1380        return Err(QmbedError::NonConvergence {
1381            iterations: basis.len(),
1382            residual: f64::INFINITY,
1383        });
1384    }
1385    let size = basis.len();
1386    let mut tridiagonal = DMatrix::<f64>::zeros(size, size);
1387    for index in 0..size {
1388        tridiagonal[(index, index)] = alphas[index];
1389        if index + 1 < size {
1390            tridiagonal[(index, index + 1)] = betas[index];
1391            tridiagonal[(index + 1, index)] = betas[index];
1392        }
1393    }
1394    let decomposition = SymmetricEigen::new(tridiagonal);
1395    let transformed_values = decomposition.eigenvalues.as_slice();
1396    let transformed_target = if matches!(options.target, SpectrumTarget::Shift(_)) {
1397        SpectrumTarget::LargestMagnitude
1398    } else {
1399        options.target
1400    };
1401    let indices = select_indices(transformed_values, transformed_target, options.eigenpairs);
1402
1403    let mut candidates = Vec::with_capacity(options.eigenpairs);
1404    for index in indices {
1405        let mut vector = vec![Complex64::new(0.0, 0.0); dimension];
1406        for (basis_index, basis_vector) in basis.iter().enumerate() {
1407            let coefficient = decomposition.eigenvectors[(basis_index, index)];
1408            for (value, basis_value) in vector.iter_mut().zip(basis_vector) {
1409                *value += coefficient * *basis_value;
1410            }
1411        }
1412        normalize(&mut vector)?;
1413        operator.apply(&vector, &mut output)?;
1414        let eigenvalue = inner(&vector, &output).re;
1415        let residual = output
1416            .iter()
1417            .zip(&vector)
1418            .map(|(actual, component)| (*actual - eigenvalue * *component).norm_sqr())
1419            .sum::<f64>()
1420            .sqrt();
1421        candidates.push((eigenvalue, vector, residual));
1422    }
1423    candidates.sort_by(|left, right| match options.target {
1424        SpectrumTarget::SmallestAlgebraic => left.0.total_cmp(&right.0),
1425        SpectrumTarget::LargestAlgebraic => right.0.total_cmp(&left.0),
1426        SpectrumTarget::SmallestMagnitude => left.0.abs().total_cmp(&right.0.abs()),
1427        SpectrumTarget::LargestMagnitude => right.0.abs().total_cmp(&left.0.abs()),
1428        SpectrumTarget::BothEnds => left.0.total_cmp(&right.0),
1429        SpectrumTarget::Shift(shift) => (left.0 - shift).abs().total_cmp(&(right.0 - shift).abs()),
1430    });
1431    let residuals: Vec<_> = candidates.iter().map(|candidate| candidate.2).collect();
1432    let failure_residual = candidates
1433        .iter()
1434        .filter_map(|candidate| {
1435            (candidate.2 > options.tolerance * candidate.0.abs().max(1.0)).then_some(candidate.2)
1436        })
1437        .fold(0.0_f64, f64::max);
1438    if failure_residual > 0.0 {
1439        return Err(QmbedError::NonConvergence {
1440            iterations: size,
1441            residual: failure_residual,
1442        });
1443    }
1444    Ok(Eigensystem {
1445        eigenvalues: candidates.iter().map(|candidate| candidate.0).collect(),
1446        eigenvectors: candidates
1447            .into_iter()
1448            .map(|candidate| candidate.1)
1449            .collect(),
1450        residuals,
1451        iterations: size,
1452        reorthogonalization_passes,
1453        conditional_second_passes,
1454        converged: true,
1455    })
1456}
1457
1458#[derive(Clone)]
1459struct RealRitzCandidate {
1460    value: f64,
1461    vector: Vec<f64>,
1462    residual: f64,
1463    transformed_action: Vec<f64>,
1464}
1465
1466#[derive(Clone)]
1467struct ComplexRitzCandidate {
1468    value: f64,
1469    vector: Vec<Complex64>,
1470    residual: f64,
1471    transformed_action: Vec<Complex64>,
1472}
1473
1474struct RetainedRealVector {
1475    vector: Vec<f64>,
1476    transformed_action: Option<Vec<f64>>,
1477}
1478
1479struct RetainedComplexVector {
1480    vector: Vec<Complex64>,
1481    transformed_action: Option<Vec<Complex64>>,
1482}
1483
1484fn sort_real_candidates(candidates: &mut [RealRitzCandidate], target: SpectrumTarget) {
1485    candidates.sort_by(|left, right| match target {
1486        SpectrumTarget::SmallestAlgebraic => left.value.total_cmp(&right.value),
1487        SpectrumTarget::LargestAlgebraic => right.value.total_cmp(&left.value),
1488        SpectrumTarget::SmallestMagnitude => left.value.abs().total_cmp(&right.value.abs()),
1489        SpectrumTarget::LargestMagnitude => right.value.abs().total_cmp(&left.value.abs()),
1490        SpectrumTarget::BothEnds => left.value.total_cmp(&right.value),
1491        SpectrumTarget::Shift(shift) => (left.value - shift)
1492            .abs()
1493            .total_cmp(&(right.value - shift).abs()),
1494    });
1495}
1496
1497fn sort_complex_candidates(candidates: &mut [ComplexRitzCandidate], target: SpectrumTarget) {
1498    candidates.sort_by(|left, right| match target {
1499        SpectrumTarget::SmallestAlgebraic => left.value.total_cmp(&right.value),
1500        SpectrumTarget::LargestAlgebraic => right.value.total_cmp(&left.value),
1501        SpectrumTarget::SmallestMagnitude => left.value.abs().total_cmp(&right.value.abs()),
1502        SpectrumTarget::LargestMagnitude => right.value.abs().total_cmp(&left.value.abs()),
1503        SpectrumTarget::BothEnds => left.value.total_cmp(&right.value),
1504        SpectrumTarget::Shift(shift) => (left.value - shift)
1505            .abs()
1506            .total_cmp(&(right.value - shift).abs()),
1507    });
1508}
1509
1510fn orthonormalize_real(
1511    locked: &[RealRitzCandidate],
1512    basis: &[Vec<f64>],
1513    vector: Vec<f64>,
1514) -> (Option<Vec<f64>>, usize, usize) {
1515    let (vector, passes, second_passes, _) =
1516        orthonormalize_real_with_projection(locked, basis, vector);
1517    (vector, passes, second_passes)
1518}
1519
1520fn orthonormalize_real_with_projection(
1521    locked: &[RealRitzCandidate],
1522    basis: &[Vec<f64>],
1523    mut vector: Vec<f64>,
1524) -> (Option<Vec<f64>>, usize, usize, Vec<f64>) {
1525    let norm_before = real_vector_norm(&vector);
1526    for candidate in locked {
1527        let overlap = real_inner(&candidate.vector, &vector);
1528        for (value, locked_value) in vector.iter_mut().zip(&candidate.vector) {
1529            *value -= overlap * *locked_value;
1530        }
1531    }
1532    let mut projection = Vec::with_capacity(basis.len());
1533    for basis_vector in basis {
1534        let overlap = real_inner(basis_vector, &vector);
1535        projection.push(overlap);
1536        for (value, basis_value) in vector.iter_mut().zip(basis_vector) {
1537            *value -= overlap * *basis_value;
1538        }
1539    }
1540    let mut norm = real_vector_norm(&vector);
1541    let second_pass =
1542        norm_before > f64::EPSILON && norm <= DGKS_REORTHOGONALIZATION_THRESHOLD * norm_before;
1543    if second_pass {
1544        for candidate in locked {
1545            let overlap = real_inner(&candidate.vector, &vector);
1546            for (value, locked_value) in vector.iter_mut().zip(&candidate.vector) {
1547                *value -= overlap * *locked_value;
1548            }
1549        }
1550        for basis_vector in basis {
1551            let overlap = real_inner(basis_vector, &vector);
1552            for (value, basis_value) in vector.iter_mut().zip(basis_vector) {
1553                *value -= overlap * *basis_value;
1554            }
1555        }
1556        norm = real_vector_norm(&vector);
1557    }
1558    if !norm.is_finite() || norm <= 1.0e-14 {
1559        return (
1560            None,
1561            1 + usize::from(second_pass),
1562            usize::from(second_pass),
1563            projection,
1564        );
1565    }
1566    for value in &mut vector {
1567        *value /= norm;
1568    }
1569    (
1570        Some(vector),
1571        1 + usize::from(second_pass),
1572        usize::from(second_pass),
1573        projection,
1574    )
1575}
1576
1577fn orthonormalize_complex(
1578    locked: &[ComplexRitzCandidate],
1579    basis: &[Vec<Complex64>],
1580    vector: Vec<Complex64>,
1581) -> (Option<Vec<Complex64>>, usize, usize) {
1582    let (vector, passes, second_passes, _) =
1583        orthonormalize_complex_with_projection(locked, basis, vector);
1584    (vector, passes, second_passes)
1585}
1586
1587fn orthonormalize_complex_with_projection(
1588    locked: &[ComplexRitzCandidate],
1589    basis: &[Vec<Complex64>],
1590    mut vector: Vec<Complex64>,
1591) -> (Option<Vec<Complex64>>, usize, usize, Vec<Complex64>) {
1592    let norm_before = vector_norm(&vector);
1593    for candidate in locked {
1594        let overlap = inner(&candidate.vector, &vector);
1595        for (value, locked_value) in vector.iter_mut().zip(&candidate.vector) {
1596            *value -= overlap * *locked_value;
1597        }
1598    }
1599    let mut projection = Vec::with_capacity(basis.len());
1600    for basis_vector in basis {
1601        let overlap = inner(basis_vector, &vector);
1602        projection.push(overlap);
1603        for (value, basis_value) in vector.iter_mut().zip(basis_vector) {
1604            *value -= overlap * *basis_value;
1605        }
1606    }
1607    let mut norm = vector_norm(&vector);
1608    let second_pass =
1609        norm_before > f64::EPSILON && norm <= DGKS_REORTHOGONALIZATION_THRESHOLD * norm_before;
1610    if second_pass {
1611        for candidate in locked {
1612            let overlap = inner(&candidate.vector, &vector);
1613            for (value, locked_value) in vector.iter_mut().zip(&candidate.vector) {
1614                *value -= overlap * *locked_value;
1615            }
1616        }
1617        for basis_vector in basis {
1618            let overlap = inner(basis_vector, &vector);
1619            for (value, basis_value) in vector.iter_mut().zip(basis_vector) {
1620                *value -= overlap * *basis_value;
1621            }
1622        }
1623        norm = vector_norm(&vector);
1624    }
1625    if !norm.is_finite() || norm <= 1.0e-14 {
1626        return (
1627            None,
1628            1 + usize::from(second_pass),
1629            usize::from(second_pass),
1630            projection,
1631        );
1632    }
1633    for value in &mut vector {
1634        *value /= norm;
1635    }
1636    (
1637        Some(vector),
1638        1 + usize::from(second_pass),
1639        usize::from(second_pass),
1640        projection,
1641    )
1642}
1643
1644fn thick_restart_dimension(options: &EigshOptions, dimension: usize) -> usize {
1645    options
1646        .krylov_dimension
1647        .unwrap_or_else(|| (4 * options.eigenpairs + 24).max(32))
1648        .min(dimension)
1649}
1650
1651fn thick_restarted_eigsh_real<O>(
1652    operator: &O,
1653    options: &EigshOptions,
1654    initial_subspace: Option<&[Vec<Complex64>]>,
1655    shifted_solver: Option<Box<dyn ShiftedLinearSolver>>,
1656) -> Result<Eigensystem>
1657where
1658    O: LinearOperator + ?Sized,
1659{
1660    let dimension = operator.shape().0;
1661    let restart_dimension = thick_restart_dimension(options, dimension);
1662    if restart_dimension <= options.eigenpairs {
1663        return Err(QmbedError::InvalidOptions(
1664            "the effective Krylov dimension must exceed eigenpairs".into(),
1665        ));
1666    }
1667    let mut retained = initial_subspace
1668        .unwrap_or_default()
1669        .iter()
1670        .map(|vector| RetainedRealVector {
1671            vector: vector.iter().map(|value| value.re).collect::<Vec<_>>(),
1672            transformed_action: None,
1673        })
1674        .collect::<Vec<_>>();
1675    let mut locked = Vec::<RealRitzCandidate>::new();
1676    let mut iterations = 0usize;
1677    let mut reorthogonalization_passes = 0usize;
1678    let mut conditional_second_passes = 0usize;
1679    let mut last_residual = f64::INFINITY;
1680    let mut cycle = 0_u64;
1681
1682    while iterations < options.max_iterations && locked.len() < options.eigenpairs {
1683        let mut basis = Vec::with_capacity(restart_dimension);
1684        let mut applied = Vec::with_capacity(restart_dimension);
1685        let reuse_actions = !retained.is_empty()
1686            && retained
1687                .iter()
1688                .all(|vector| vector.transformed_action.is_some());
1689        for mut retained_vector in retained.drain(..) {
1690            if reuse_actions {
1691                let norm = real_vector_norm(&retained_vector.vector);
1692                if norm.is_finite() && norm > 1.0e-14 {
1693                    for value in &mut retained_vector.vector {
1694                        *value /= norm;
1695                    }
1696                    let mut action = retained_vector.transformed_action.take().unwrap();
1697                    for value in &mut action {
1698                        *value /= norm;
1699                    }
1700                    basis.push(retained_vector.vector);
1701                    applied.push(action);
1702                }
1703            } else {
1704                let (vector, passes, second_passes) =
1705                    orthonormalize_real(&locked, &basis, retained_vector.vector);
1706                reorthogonalization_passes += passes;
1707                conditional_second_passes += second_passes;
1708                if let Some(vector) = vector {
1709                    basis.push(vector);
1710                }
1711            }
1712            if basis.len() >= restart_dimension / 2 {
1713                break;
1714            }
1715        }
1716        if basis.is_empty() {
1717            let start = deterministic_start(
1718                dimension,
1719                options.seed.wrapping_add(cycle.wrapping_mul(0x9e37_79b9)),
1720            )?
1721            .into_iter()
1722            .map(|value| value.re)
1723            .collect();
1724            let (start, passes, second_passes) = orthonormalize_real(&locked, &basis, start);
1725            reorthogonalization_passes += passes;
1726            conditional_second_passes += second_passes;
1727            if let Some(start) = start {
1728                basis.push(start);
1729            }
1730        }
1731        if basis.is_empty() {
1732            break;
1733        }
1734
1735        let known_actions = applied.len();
1736        let mut projection_columns = Vec::with_capacity(restart_dimension);
1737        if reuse_actions {
1738            for (column, action) in applied[..known_actions].iter().cloned().enumerate() {
1739                if basis.len() >= restart_dimension {
1740                    break;
1741                }
1742                let (residual_direction, passes, second_passes, projection) =
1743                    orthonormalize_real_with_projection(&locked, &basis, action);
1744                reorthogonalization_passes += passes;
1745                conditional_second_passes += second_passes;
1746                projection_columns.push(projection[..=column].to_vec());
1747                if let Some(residual_direction) = residual_direction {
1748                    basis.push(residual_direction);
1749                }
1750            }
1751        }
1752        let mut cursor = known_actions;
1753        while cursor < basis.len()
1754            && iterations < options.max_iterations
1755            && applied.len() < restart_dimension
1756        {
1757            let mut output = vec![0.0; dimension];
1758            transformed_apply_real(
1759                operator,
1760                options,
1761                shifted_solver.as_deref(),
1762                &basis[cursor],
1763                &mut output,
1764            )?;
1765            iterations += 1;
1766            applied.push(output.clone());
1767            let (next, passes, second_passes, projection) =
1768                orthonormalize_real_with_projection(&locked, &basis, output);
1769            reorthogonalization_passes += passes;
1770            conditional_second_passes += second_passes;
1771            projection_columns.push(projection[..=cursor].to_vec());
1772            if basis.len() < restart_dimension {
1773                if let Some(next) = next {
1774                    basis.push(next);
1775                }
1776            }
1777            cursor += 1;
1778        }
1779        basis.truncate(applied.len());
1780        if basis.is_empty() {
1781            break;
1782        }
1783
1784        let size = basis.len();
1785        let mut projected = DMatrix::<f64>::zeros(size, size);
1786        for (column, projection) in projection_columns.iter().enumerate().take(size) {
1787            for (row, &value) in projection.iter().enumerate().take(column + 1) {
1788                projected[(row, column)] = value;
1789                projected[(column, row)] = value;
1790            }
1791        }
1792        let decomposition = SymmetricEigen::new(projected);
1793        let transformed_target = if matches!(options.target, SpectrumTarget::Shift(_)) {
1794            SpectrumTarget::LargestMagnitude
1795        } else {
1796            options.target
1797        };
1798        let active_needed = options.eigenpairs - locked.len();
1799        let keep_limit = active_needed.min(restart_dimension / 2).max(1);
1800        let indices = select_indices(
1801            decomposition.eigenvalues.as_slice(),
1802            transformed_target,
1803            keep_limit.min(size),
1804        );
1805        let mut candidates = Vec::with_capacity(indices.len());
1806        let mut original_action = vec![0.0; dimension];
1807        for index in indices {
1808            let mut vector = vec![0.0; dimension];
1809            let mut transformed_action = vec![0.0; dimension];
1810            for (basis_index, basis_vector) in basis.iter().enumerate() {
1811                let coefficient = decomposition.eigenvectors[(basis_index, index)];
1812                for (value, basis_value) in vector.iter_mut().zip(basis_vector) {
1813                    *value += coefficient * *basis_value;
1814                }
1815                for (value, applied_value) in
1816                    transformed_action.iter_mut().zip(&applied[basis_index])
1817                {
1818                    *value += coefficient * *applied_value;
1819                }
1820            }
1821            let ritz_norm = real_vector_norm(&vector);
1822            if !ritz_norm.is_finite() || ritz_norm <= f64::EPSILON {
1823                return Err(QmbedError::NonConvergence {
1824                    iterations,
1825                    residual: ritz_norm,
1826                });
1827            }
1828            for value in &mut vector {
1829                *value /= ritz_norm;
1830            }
1831            for value in &mut transformed_action {
1832                *value /= ritz_norm;
1833            }
1834            if matches!(options.target, SpectrumTarget::Shift(_)) {
1835                operator.apply_real(&vector, &mut original_action)?;
1836            } else {
1837                original_action.copy_from_slice(&transformed_action);
1838            }
1839            let value = real_inner(&vector, &original_action);
1840            let residual = original_action
1841                .iter()
1842                .zip(&vector)
1843                .map(|(actual, component)| (actual - value * component).powi(2))
1844                .sum::<f64>()
1845                .sqrt();
1846            candidates.push(RealRitzCandidate {
1847                value,
1848                vector,
1849                residual,
1850                transformed_action,
1851            });
1852        }
1853        sort_real_candidates(&mut candidates, options.target);
1854        last_residual = candidates
1855            .iter()
1856            .take(active_needed)
1857            .map(|candidate| candidate.residual)
1858            .fold(0.0_f64, f64::max);
1859
1860        let mut next_retained = Vec::new();
1861        for (rank, candidate) in candidates.into_iter().enumerate() {
1862            let converged =
1863                candidate.residual <= options.tolerance * candidate.value.abs().max(1.0);
1864            if rank < active_needed && converged && locked.len() < options.eigenpairs {
1865                let (vector, passes, second_passes) =
1866                    orthonormalize_real(&locked, &[], candidate.vector);
1867                reorthogonalization_passes += passes;
1868                conditional_second_passes += second_passes;
1869                if let Some(vector) = vector {
1870                    locked.push(RealRitzCandidate {
1871                        vector,
1872                        ..candidate
1873                    });
1874                }
1875            } else if next_retained.len() < keep_limit {
1876                next_retained.push(RetainedRealVector {
1877                    vector: candidate.vector,
1878                    transformed_action: Some(candidate.transformed_action),
1879                });
1880            }
1881        }
1882        retained = next_retained;
1883        cycle = cycle.wrapping_add(1);
1884    }
1885
1886    if locked.len() < options.eigenpairs {
1887        return Err(QmbedError::NonConvergence {
1888            iterations,
1889            residual: last_residual,
1890        });
1891    }
1892    sort_real_candidates(&mut locked, options.target);
1893    locked.truncate(options.eigenpairs);
1894    Ok(Eigensystem {
1895        eigenvalues: locked.iter().map(|candidate| candidate.value).collect(),
1896        eigenvectors: locked
1897            .iter()
1898            .map(|candidate| {
1899                candidate
1900                    .vector
1901                    .iter()
1902                    .map(|value| Complex64::new(*value, 0.0))
1903                    .collect()
1904            })
1905            .collect(),
1906        residuals: locked.iter().map(|candidate| candidate.residual).collect(),
1907        iterations,
1908        reorthogonalization_passes,
1909        conditional_second_passes,
1910        converged: true,
1911    })
1912}
1913
1914fn thick_restarted_eigsh_complex<O>(
1915    operator: &O,
1916    options: &EigshOptions,
1917    initial_subspace: Option<&[Vec<Complex64>]>,
1918    shifted_solver: Option<Box<dyn ShiftedLinearSolver>>,
1919) -> Result<Eigensystem>
1920where
1921    O: LinearOperator + ?Sized,
1922{
1923    let dimension = operator.shape().0;
1924    let restart_dimension = thick_restart_dimension(options, dimension);
1925    if restart_dimension <= options.eigenpairs {
1926        return Err(QmbedError::InvalidOptions(
1927            "the effective Krylov dimension must exceed eigenpairs".into(),
1928        ));
1929    }
1930    let mut retained = initial_subspace
1931        .unwrap_or_default()
1932        .iter()
1933        .cloned()
1934        .map(|vector| RetainedComplexVector {
1935            vector,
1936            transformed_action: None,
1937        })
1938        .collect::<Vec<_>>();
1939    let mut locked = Vec::<ComplexRitzCandidate>::new();
1940    let mut iterations = 0usize;
1941    let mut reorthogonalization_passes = 0usize;
1942    let mut conditional_second_passes = 0usize;
1943    let mut last_residual = f64::INFINITY;
1944    let mut cycle = 0_u64;
1945
1946    while iterations < options.max_iterations && locked.len() < options.eigenpairs {
1947        let mut basis = Vec::with_capacity(restart_dimension);
1948        let mut applied = Vec::with_capacity(restart_dimension);
1949        let reuse_actions = !retained.is_empty()
1950            && retained
1951                .iter()
1952                .all(|vector| vector.transformed_action.is_some());
1953        for mut retained_vector in retained.drain(..) {
1954            if reuse_actions {
1955                let norm = vector_norm(&retained_vector.vector);
1956                if norm.is_finite() && norm > 1.0e-14 {
1957                    for value in &mut retained_vector.vector {
1958                        *value /= norm;
1959                    }
1960                    let mut action = retained_vector.transformed_action.take().unwrap();
1961                    for value in &mut action {
1962                        *value /= norm;
1963                    }
1964                    basis.push(retained_vector.vector);
1965                    applied.push(action);
1966                }
1967            } else {
1968                let (vector, passes, second_passes) =
1969                    orthonormalize_complex(&locked, &basis, retained_vector.vector);
1970                reorthogonalization_passes += passes;
1971                conditional_second_passes += second_passes;
1972                if let Some(vector) = vector {
1973                    basis.push(vector);
1974                }
1975            }
1976            if basis.len() >= restart_dimension / 2 {
1977                break;
1978            }
1979        }
1980        if basis.is_empty() {
1981            let start = deterministic_start(
1982                dimension,
1983                options.seed.wrapping_add(cycle.wrapping_mul(0x9e37_79b9)),
1984            )?;
1985            let (start, passes, second_passes) = orthonormalize_complex(&locked, &basis, start);
1986            reorthogonalization_passes += passes;
1987            conditional_second_passes += second_passes;
1988            if let Some(start) = start {
1989                basis.push(start);
1990            }
1991        }
1992        if basis.is_empty() {
1993            break;
1994        }
1995
1996        let known_actions = applied.len();
1997        let mut projection_columns = Vec::with_capacity(restart_dimension);
1998        if reuse_actions {
1999            for (column, action) in applied[..known_actions].iter().cloned().enumerate() {
2000                if basis.len() >= restart_dimension {
2001                    break;
2002                }
2003                let (residual_direction, passes, second_passes, projection) =
2004                    orthonormalize_complex_with_projection(&locked, &basis, action);
2005                reorthogonalization_passes += passes;
2006                conditional_second_passes += second_passes;
2007                projection_columns.push(projection[..=column].to_vec());
2008                if let Some(residual_direction) = residual_direction {
2009                    basis.push(residual_direction);
2010                }
2011            }
2012        }
2013        let mut cursor = known_actions;
2014        while cursor < basis.len()
2015            && iterations < options.max_iterations
2016            && applied.len() < restart_dimension
2017        {
2018            let mut output = vec![Complex64::new(0.0, 0.0); dimension];
2019            transformed_apply(
2020                operator,
2021                options,
2022                shifted_solver.as_deref(),
2023                &basis[cursor],
2024                &mut output,
2025            )?;
2026            iterations += 1;
2027            applied.push(output.clone());
2028            let (next, passes, second_passes, projection) =
2029                orthonormalize_complex_with_projection(&locked, &basis, output);
2030            reorthogonalization_passes += passes;
2031            conditional_second_passes += second_passes;
2032            projection_columns.push(projection[..=cursor].to_vec());
2033            if basis.len() < restart_dimension {
2034                if let Some(next) = next {
2035                    basis.push(next);
2036                }
2037            }
2038            cursor += 1;
2039        }
2040        basis.truncate(applied.len());
2041        if basis.is_empty() {
2042            break;
2043        }
2044
2045        let size = basis.len();
2046        let mut projected = DMatrix::<Complex64>::zeros(size, size);
2047        for (column, projection) in projection_columns.iter().enumerate().take(size) {
2048            for (row, &value) in projection.iter().enumerate().take(column + 1) {
2049                projected[(row, column)] = value;
2050                projected[(column, row)] = value.conj();
2051            }
2052        }
2053        let decomposition = SymmetricEigen::new(projected);
2054        let transformed_target = if matches!(options.target, SpectrumTarget::Shift(_)) {
2055            SpectrumTarget::LargestMagnitude
2056        } else {
2057            options.target
2058        };
2059        let active_needed = options.eigenpairs - locked.len();
2060        let keep_limit = active_needed.min(restart_dimension / 2).max(1);
2061        let indices = select_indices(
2062            decomposition.eigenvalues.as_slice(),
2063            transformed_target,
2064            keep_limit.min(size),
2065        );
2066        let mut candidates = Vec::with_capacity(indices.len());
2067        let mut original_action = vec![Complex64::new(0.0, 0.0); dimension];
2068        for index in indices {
2069            let mut vector = vec![Complex64::new(0.0, 0.0); dimension];
2070            let mut transformed_action = vec![Complex64::new(0.0, 0.0); dimension];
2071            for (basis_index, basis_vector) in basis.iter().enumerate() {
2072                let coefficient = decomposition.eigenvectors[(basis_index, index)];
2073                for (value, basis_value) in vector.iter_mut().zip(basis_vector) {
2074                    *value += coefficient * *basis_value;
2075                }
2076                for (value, applied_value) in
2077                    transformed_action.iter_mut().zip(&applied[basis_index])
2078                {
2079                    *value += coefficient * *applied_value;
2080                }
2081            }
2082            let ritz_norm = vector_norm(&vector);
2083            if !ritz_norm.is_finite() || ritz_norm <= f64::EPSILON {
2084                return Err(QmbedError::NonConvergence {
2085                    iterations,
2086                    residual: ritz_norm,
2087                });
2088            }
2089            for value in &mut vector {
2090                *value /= ritz_norm;
2091            }
2092            for value in &mut transformed_action {
2093                *value /= ritz_norm;
2094            }
2095            if matches!(options.target, SpectrumTarget::Shift(_)) {
2096                operator.apply(&vector, &mut original_action)?;
2097            } else {
2098                original_action.copy_from_slice(&transformed_action);
2099            }
2100            let value = inner(&vector, &original_action).re;
2101            let residual = original_action
2102                .iter()
2103                .zip(&vector)
2104                .map(|(actual, component)| (*actual - value * *component).norm_sqr())
2105                .sum::<f64>()
2106                .sqrt();
2107            candidates.push(ComplexRitzCandidate {
2108                value,
2109                vector,
2110                residual,
2111                transformed_action,
2112            });
2113        }
2114        sort_complex_candidates(&mut candidates, options.target);
2115        last_residual = candidates
2116            .iter()
2117            .take(active_needed)
2118            .map(|candidate| candidate.residual)
2119            .fold(0.0_f64, f64::max);
2120
2121        let mut next_retained = Vec::new();
2122        for (rank, candidate) in candidates.into_iter().enumerate() {
2123            let converged =
2124                candidate.residual <= options.tolerance * candidate.value.abs().max(1.0);
2125            if rank < active_needed && converged && locked.len() < options.eigenpairs {
2126                let (vector, passes, second_passes) =
2127                    orthonormalize_complex(&locked, &[], candidate.vector);
2128                reorthogonalization_passes += passes;
2129                conditional_second_passes += second_passes;
2130                if let Some(vector) = vector {
2131                    locked.push(ComplexRitzCandidate {
2132                        vector,
2133                        ..candidate
2134                    });
2135                }
2136            } else if next_retained.len() < keep_limit {
2137                next_retained.push(RetainedComplexVector {
2138                    vector: candidate.vector,
2139                    transformed_action: Some(candidate.transformed_action),
2140                });
2141            }
2142        }
2143        retained = next_retained;
2144        cycle = cycle.wrapping_add(1);
2145    }
2146
2147    if locked.len() < options.eigenpairs {
2148        return Err(QmbedError::NonConvergence {
2149            iterations,
2150            residual: last_residual,
2151        });
2152    }
2153    sort_complex_candidates(&mut locked, options.target);
2154    locked.truncate(options.eigenpairs);
2155    Ok(Eigensystem {
2156        eigenvalues: locked.iter().map(|candidate| candidate.value).collect(),
2157        eigenvectors: locked
2158            .iter()
2159            .map(|candidate| candidate.vector.clone())
2160            .collect(),
2161        residuals: locked.iter().map(|candidate| candidate.residual).collect(),
2162        iterations,
2163        reorthogonalization_passes,
2164        conditional_second_passes,
2165        converged: true,
2166    })
2167}
2168
2169fn thick_restarted_eigsh<O>(
2170    operator: &O,
2171    options: &EigshOptions,
2172    initial_subspace: Option<&[Vec<Complex64>]>,
2173) -> Result<Eigensystem>
2174where
2175    O: LinearOperator + ?Sized,
2176{
2177    let real_compatible = operator.is_real()
2178        && initial_subspace.is_none_or(|vectors| {
2179            vectors
2180                .iter()
2181                .all(|vector| vector.iter().all(|value| value.im.abs() <= 1.0e-14))
2182        });
2183    let shifted_solver = match options.target {
2184        SpectrumTarget::Shift(shift) => operator.shifted_solver(shift)?,
2185        _ => None,
2186    };
2187    if real_compatible
2188        && (!matches!(options.target, SpectrumTarget::Shift(_))
2189            || shifted_solver
2190                .as_ref()
2191                .is_some_and(|solver| solver.supports_real()))
2192    {
2193        return thick_restarted_eigsh_real(operator, options, initial_subspace, shifted_solver);
2194    }
2195    thick_restarted_eigsh_complex(operator, options, initial_subspace, shifted_solver)
2196}
2197
2198fn dense_eigsh<O>(operator: &O, options: &EigshOptions) -> Result<Eigensystem>
2199where
2200    O: LinearOperator + ?Sized,
2201{
2202    let (values, vectors) = hermitian_eigenpairs_all(operator)?;
2203    let indices = match options.target {
2204        SpectrumTarget::Shift(shift) => {
2205            let mut indices: Vec<_> = (0..values.len()).collect();
2206            indices.sort_by(|&left, &right| {
2207                (values[left] - shift)
2208                    .abs()
2209                    .total_cmp(&(values[right] - shift).abs())
2210                    .then_with(|| left.cmp(&right))
2211            });
2212            indices.truncate(options.eigenpairs);
2213            indices
2214        }
2215        _ => select_indices(&values, options.target, options.eigenpairs),
2216    };
2217    let eigenvectors: Vec<_> = indices
2218        .iter()
2219        .map(|&index| vectors[index].clone())
2220        .collect();
2221    // Use the selected dense eigenvectors to refine each value with its
2222    // Rayleigh quotient. This shares the operator applications already needed
2223    // for residuals and avoids backend/platform rounding differences at tight
2224    // compatibility tolerances.
2225    let refined = eigenvectors
2226        .iter()
2227        .map(|vector| rayleigh_value_and_residual(operator, vector))
2228        .collect::<Result<Vec<_>>>()?;
2229    let eigenvalues = refined.iter().map(|&(value, _)| value).collect();
2230    let residuals = refined.iter().map(|&(_, residual)| residual).collect();
2231    Ok(Eigensystem {
2232        eigenvalues,
2233        eigenvectors,
2234        residuals,
2235        iterations: 1,
2236        reorthogonalization_passes: 0,
2237        conditional_second_passes: 0,
2238        converged: true,
2239    })
2240}
2241
2242fn expanding_lanczos_eigsh<O>(
2243    operator: &O,
2244    options: &EigshOptions,
2245    initial_subspace: Option<&[Vec<Complex64>]>,
2246) -> Result<Eigensystem>
2247where
2248    O: LinearOperator + ?Sized,
2249{
2250    if options.krylov_dimension.is_none()
2251        && initial_subspace.is_none_or(|vectors| vectors.len() <= 1)
2252    {
2253        let initial = initial_subspace
2254            .and_then(|vectors| vectors.first())
2255            .map(Vec::as_slice);
2256        let dimension = operator.shape().0;
2257        let mut subspace_dimension = (8 * options.eigenpairs + 64)
2258            .max(256)
2259            .min(dimension)
2260            .min(options.max_iterations);
2261        let mut spent_iterations = 0usize;
2262        loop {
2263            let mut attempt = options.clone();
2264            attempt.krylov_dimension = Some(subspace_dimension);
2265            attempt.max_iterations = subspace_dimension;
2266            match lanczos_eigsh(operator, &attempt, initial) {
2267                Ok(mut result) => {
2268                    result.iterations += spent_iterations;
2269                    return Ok(result);
2270                }
2271                Err(QmbedError::NonConvergence {
2272                    iterations,
2273                    residual,
2274                }) => {
2275                    spent_iterations = spent_iterations.saturating_add(iterations);
2276                    if subspace_dimension == dimension && dimension <= FULL_KRYLOV_DENSE_FALLBACK {
2277                        let mut result = dense_eigsh(operator, options)?;
2278                        result.iterations = result.iterations.saturating_add(spent_iterations);
2279                        return Ok(result);
2280                    }
2281                    let remaining = options.max_iterations.saturating_sub(spent_iterations);
2282                    let next_dimension = subspace_dimension
2283                        .saturating_mul(2)
2284                        .min(dimension)
2285                        .min(remaining);
2286                    if next_dimension <= subspace_dimension {
2287                        return Err(QmbedError::NonConvergence {
2288                            iterations: spent_iterations,
2289                            residual,
2290                        });
2291                    }
2292                    subspace_dimension = next_dimension;
2293                }
2294                Err(error) => return Err(error),
2295            }
2296        }
2297    }
2298    // A large enough unrestarted window is still the cheapest path: its
2299    // three-term projection is tridiagonal and avoids the dense Rayleigh--Ritz
2300    // projection required after a thick restart.  Small windows and genuine
2301    // multi-vector starts use the restarted backend below.
2302    if options
2303        .krylov_dimension
2304        .is_some_and(|dimension| dimension >= TRIDIAGONAL_LANCZOS_WINDOW)
2305        && initial_subspace.is_none_or(|vectors| vectors.len() <= 1)
2306    {
2307        let initial = initial_subspace
2308            .and_then(|vectors| vectors.first())
2309            .map(Vec::as_slice);
2310        return lanczos_eigsh(operator, options, initial);
2311    }
2312    match thick_restarted_eigsh(operator, options, initial_subspace) {
2313        Err(QmbedError::NonConvergence {
2314            iterations,
2315            residual: _,
2316        }) if operator.shape().0 <= FULL_KRYLOV_DENSE_FALLBACK
2317            && options.max_iterations >= operator.shape().0
2318            && options
2319                .krylov_dimension
2320                .is_none_or(|dimension| dimension == operator.shape().0) =>
2321        {
2322            let mut result = dense_eigsh(operator, options)?;
2323            result.iterations = iterations.saturating_add(result.iterations);
2324            Ok(result)
2325        }
2326        result => result,
2327    }
2328}
2329
2330/// Selected Hermitian eigenpairs.
2331///
2332/// Small problems use a dense real-symmetric decomposition. Larger problems
2333/// use a matrix-free, fully reorthogonalized Lanczos backend; shift targets
2334/// apply a restarted GMRES inverse without materializing the operator.
2335pub fn eigsh<O>(operator: &O, options: EigshOptions) -> Result<Eigensystem>
2336where
2337    O: LinearOperator + ?Sized,
2338{
2339    let shape = operator.shape();
2340    if shape.0 != shape.1 {
2341        return Err(QmbedError::DimensionMismatch(
2342            "eigsh requires a square operator".into(),
2343        ));
2344    }
2345    options.validate(shape.0)?;
2346    if !use_dense_eigsh(shape.0, &options) {
2347        return expanding_lanczos_eigsh(operator, &options, None);
2348    }
2349    dense_eigsh(operator, &options)
2350}
2351
2352/// Compute selected eigenpairs from one explicit initial vector.
2353///
2354/// The vector must match the square operator dimension and have nonzero finite
2355/// norm. Small problems may still choose the exact dense route.
2356pub fn eigsh_with_initial<O>(
2357    operator: &O,
2358    options: EigshOptions,
2359    initial: &[Complex64],
2360) -> Result<Eigensystem>
2361where
2362    O: LinearOperator + ?Sized,
2363{
2364    let shape = operator.shape();
2365    if shape.0 != shape.1 {
2366        return Err(QmbedError::DimensionMismatch(
2367            "eigsh requires a square operator".into(),
2368        ));
2369    }
2370    options.validate(shape.0)?;
2371    validate_eigsh_initial(initial, shape.0)?;
2372    if use_dense_eigsh(shape.0, &options) {
2373        return eigsh(operator, options);
2374    }
2375    let initial_subspace = vec![initial.to_vec()];
2376    expanding_lanczos_eigsh(operator, &options, Some(&initial_subspace))
2377}
2378
2379/// Solve from a complete user-supplied initial subspace.
2380///
2381/// Related parameter points should pass all previously converged target
2382/// vectors here.  The thick-restart backend preserves the subspace as a
2383/// subspace, so rotations inside a degenerate multiplet do not discard useful
2384/// information.
2385pub fn eigsh_with_initial_subspace<O>(
2386    operator: &O,
2387    options: EigshOptions,
2388    initial_subspace: &[Vec<Complex64>],
2389) -> Result<Eigensystem>
2390where
2391    O: LinearOperator + ?Sized,
2392{
2393    let shape = operator.shape();
2394    if shape.0 != shape.1 {
2395        return Err(QmbedError::DimensionMismatch(
2396            "eigsh requires a square operator".into(),
2397        ));
2398    }
2399    options.validate(shape.0)?;
2400    if initial_subspace.is_empty() {
2401        return eigsh(operator, options);
2402    }
2403    for vector in initial_subspace {
2404        validate_eigsh_initial(vector, shape.0)?;
2405    }
2406    if use_dense_eigsh(shape.0, &options) {
2407        return eigsh(operator, options);
2408    }
2409    expanding_lanczos_eigsh(operator, &options, Some(initial_subspace))
2410}
2411
2412/// Solve one member of a related operator family and update its reusable
2413/// invariant-subspace workspace.
2414///
2415/// A compatible workspace retains all previously converged vectors. Small
2416/// restart windows preserve them as a thick subspace; the cheaper large-window
2417/// tridiagonal route forms a balanced warm start without replacing the generic
2418/// Lanczos algorithm.
2419pub fn eigsh_with_workspace<O>(
2420    operator: &O,
2421    options: EigshOptions,
2422    workspace: &mut EigshWorkspace,
2423) -> Result<Eigensystem>
2424where
2425    O: LinearOperator + ?Sized,
2426{
2427    let dimension = operator.shape().0;
2428    let result = if workspace.dimension == dimension && !workspace.initial_subspace.is_empty() {
2429        if options
2430            .krylov_dimension
2431            .is_none_or(|size| size >= TRIDIAGONAL_LANCZOS_WINDOW)
2432            && workspace.initial_subspace.len() > 1
2433        {
2434            let initial = balanced_subspace_start(&workspace.initial_subspace)?;
2435            eigsh_with_initial(operator, options, &initial)?
2436        } else {
2437            eigsh_with_initial_subspace(operator, options, &workspace.initial_subspace)?
2438        }
2439    } else {
2440        eigsh(operator, options)?
2441    };
2442    workspace.update(dimension, &result);
2443    Ok(result)
2444}
2445
2446/// Return only selected eigenvalues while using the same convergence checks as [`eigsh`].
2447pub fn eigsh_values<O>(operator: &O, options: EigshOptions) -> Result<Vec<f64>>
2448where
2449    O: LinearOperator + ?Sized,
2450{
2451    Ok(eigsh(operator, options)?.eigenvalues)
2452}
2453
2454/// Time grid and adaptive Krylov controls for state evolution.
2455#[derive(Clone, Debug, PartialEq)]
2456#[non_exhaustive]
2457pub struct EvolutionOptions {
2458    /// Finite nondecreasing output times measured from the initial state.
2459    pub times: Vec<f64>,
2460    /// Maximum dimension of each Krylov projection.
2461    pub krylov_dimension: usize,
2462    /// Local error tolerance.
2463    pub tolerance: f64,
2464    /// Maximum accepted and rejected trial intervals.
2465    pub max_substeps: usize,
2466    /// Interpret the generator as `H` and apply `exp(-i H t)` when true.
2467    pub hamiltonian: bool,
2468}
2469
2470impl EvolutionOptions {
2471    /// Construct Hamiltonian evolution controls on the supplied output grid.
2472    pub fn new(times: impl Into<Vec<f64>>) -> Self {
2473        Self {
2474            times: times.into(),
2475            krylov_dimension: 30,
2476            tolerance: 1.0e-10,
2477            max_substeps: 10_000,
2478            hamiltonian: true,
2479        }
2480    }
2481
2482    /// Set the maximum dimension of each Krylov projection.
2483    #[must_use]
2484    pub fn with_krylov_dimension(mut self, dimension: usize) -> Self {
2485        self.krylov_dimension = dimension;
2486        self
2487    }
2488
2489    /// Set the local error tolerance.
2490    #[must_use]
2491    pub fn with_tolerance(mut self, tolerance: f64) -> Self {
2492        self.tolerance = tolerance;
2493        self
2494    }
2495
2496    /// Set the maximum accepted and rejected trial intervals.
2497    #[must_use]
2498    pub fn with_max_substeps(mut self, max_substeps: usize) -> Self {
2499        self.max_substeps = max_substeps;
2500        self
2501    }
2502
2503    /// Select Hamiltonian (`exp(-iHt)`) or direct-generator evolution.
2504    #[must_use]
2505    pub fn with_hamiltonian(mut self, hamiltonian: bool) -> Self {
2506        self.hamiltonian = hamiltonian;
2507        self
2508    }
2509
2510    fn validate(&self) -> Result<()> {
2511        if self.times.is_empty()
2512            || self.times.iter().any(|time| !time.is_finite())
2513            || self.times.windows(2).any(|pair| pair[0] > pair[1])
2514        {
2515            return Err(QmbedError::InvalidOptions(
2516                "times must be a nonempty finite nondecreasing grid".into(),
2517            ));
2518        }
2519        if self.krylov_dimension == 0
2520            || !self.tolerance.is_finite()
2521            || self.tolerance <= 0.0
2522            || self.max_substeps == 0
2523        {
2524            return Err(QmbedError::InvalidOptions(
2525                "evolution controls must be positive".into(),
2526            ));
2527        }
2528        Ok(())
2529    }
2530}
2531
2532/// State vectors sampled at the requested physical times.
2533#[derive(Clone, Debug)]
2534pub struct StateTrajectory {
2535    /// Output times copied from the validated request.
2536    pub times: Vec<f64>,
2537    /// One state vector per output time.
2538    pub states: Vec<Vec<Complex64>>,
2539}
2540
2541/// Algorithmic work performed by Hermitian Krylov time evolution.
2542///
2543/// Wall time depends on the host and sparse backend. These counters expose the
2544/// portable cost drivers needed by verification and performance-regression
2545/// suites without changing the existing [`evolve`] return contract.
2546#[derive(Clone, Debug, Default, PartialEq)]
2547pub struct EvolutionDiagnostics {
2548    pub lanczos_projections: usize,
2549    pub matrix_vector_products: usize,
2550    pub real_lanczos_projections: usize,
2551    pub real_matrix_vector_products: usize,
2552    pub accepted_substeps: usize,
2553    pub rejected_trial_intervals: usize,
2554    pub maximum_estimated_error: f64,
2555}
2556
2557/// Column-oriented batch trajectory: `states[time_index][column_index]`.
2558#[derive(Clone, Debug)]
2559pub struct StateBatchTrajectory {
2560    pub times: Vec<f64>,
2561    pub states: Vec<Vec<Vec<Complex64>>>,
2562}
2563
2564/// Dimension and breakdown tolerance for a Lanczos decomposition.
2565#[derive(Clone, Debug, PartialEq)]
2566#[non_exhaustive]
2567pub struct LanczosOptions {
2568    /// Maximum number of orthonormal Krylov vectors.
2569    pub krylov_dimension: usize,
2570    /// Threshold for norm breakdown and Hermiticity checks.
2571    pub tolerance: f64,
2572}
2573
2574impl LanczosOptions {
2575    /// Construct a decomposition request with the default breakdown tolerance.
2576    pub const fn new(krylov_dimension: usize) -> Self {
2577        Self {
2578            krylov_dimension,
2579            tolerance: 1.0e-12,
2580        }
2581    }
2582
2583    /// Set the norm-breakdown and Hermiticity tolerance.
2584    #[must_use]
2585    pub const fn with_tolerance(mut self, tolerance: f64) -> Self {
2586        self.tolerance = tolerance;
2587        self
2588    }
2589
2590    fn validate(&self) -> Result<()> {
2591        if self.krylov_dimension == 0 || !self.tolerance.is_finite() || self.tolerance <= 0.0 {
2592            return Err(QmbedError::InvalidOptions(
2593                "Lanczos dimension and tolerance must be positive".into(),
2594            ));
2595        }
2596        Ok(())
2597    }
2598}
2599
2600/// One streamed vector and neighboring coefficients of a Lanczos recurrence.
2601#[derive(Clone, Debug)]
2602pub struct LanczosVector {
2603    /// Zero-based position in the Krylov basis.
2604    pub index: usize,
2605    /// Normalized Krylov vector.
2606    pub vector: Vec<Complex64>,
2607    /// Diagonal tridiagonal coefficient.
2608    pub diagonal: f64,
2609    /// Off-diagonal coefficient leading to the next vector.
2610    pub next_off_diagonal: f64,
2611}
2612
2613/// Complete orthonormal basis and real symmetric tridiagonal projection.
2614#[derive(Clone, Debug)]
2615pub struct LanczosDecomposition {
2616    /// Norm removed from the caller's initial vector.
2617    pub initial_norm: f64,
2618    /// Orthonormal Krylov vectors.
2619    pub basis: Vec<Vec<Complex64>>,
2620    /// Diagonal of the projected tridiagonal matrix.
2621    pub diagonal: Vec<f64>,
2622    /// First off-diagonal of the projected tridiagonal matrix.
2623    pub off_diagonal: Vec<f64>,
2624}
2625
2626#[derive(Clone, Debug)]
2627pub struct LanczosRitzDecomposition {
2628    pub decomposition: LanczosDecomposition,
2629    pub eigenvalues: Vec<f64>,
2630    /// Column-oriented eigenvectors of the real symmetric tridiagonal matrix.
2631    pub eigenvectors: Vec<Vec<f64>>,
2632}
2633
2634impl LanczosRitzDecomposition {
2635    /// Lift coefficients in Krylov-basis order back to the full Hilbert space.
2636    pub fn linear_combination(&self, coefficients: &[Complex64]) -> Result<Vec<Complex64>> {
2637        linear_combination_qt(&self.decomposition.basis, coefficients)
2638    }
2639
2640    /// Apply `exp(coefficient * T)` to the first Krylov vector and lift it
2641    /// through the stored Lanczos basis.
2642    pub fn exponential_action(&self, coefficient: Complex64) -> Result<Vec<Complex64>> {
2643        if !coefficient.re.is_finite() || !coefficient.im.is_finite() {
2644            return Err(QmbedError::InvalidOptions(
2645                "Lanczos exponential coefficient must be finite".into(),
2646            ));
2647        }
2648        let dimension = self.eigenvalues.len();
2649        if dimension == 0
2650            || self.eigenvectors.len() != dimension
2651            || self
2652                .eigenvectors
2653                .iter()
2654                .any(|vector| vector.len() != dimension)
2655        {
2656            return Err(QmbedError::InternalState(
2657                "Lanczos Ritz eigensystem is inconsistent".into(),
2658            ));
2659        }
2660        let mut coefficients = vec![Complex64::new(0.0, 0.0); dimension];
2661        for (eigenvalue, eigenvector) in self.eigenvalues.iter().zip(&self.eigenvectors) {
2662            let weight = Complex64::new(eigenvector[0], 0.0)
2663                * (coefficient * *eigenvalue).exp()
2664                * self.decomposition.initial_norm;
2665            for (value, component) in coefficients.iter_mut().zip(eigenvector) {
2666                *value += weight * *component;
2667            }
2668        }
2669        self.linear_combination(&coefficients)
2670    }
2671}
2672
2673/// Iterator that streams a fully reorthogonalized Lanczos recurrence.
2674pub struct LanczosIter<'a, O>
2675where
2676    O: LinearOperator + ?Sized,
2677{
2678    operator: &'a O,
2679    options: LanczosOptions,
2680    index: usize,
2681    previous: Option<Vec<Complex64>>,
2682    current: Option<Vec<Complex64>>,
2683    previous_beta: f64,
2684    history: Vec<Vec<Complex64>>,
2685    failed: bool,
2686}
2687
2688impl<O> Iterator for LanczosIter<'_, O>
2689where
2690    O: LinearOperator + ?Sized,
2691{
2692    type Item = Result<LanczosVector>;
2693
2694    fn next(&mut self) -> Option<Self::Item> {
2695        if self.failed || self.index >= self.options.krylov_dimension {
2696            return None;
2697        }
2698        let current = self.current.take()?;
2699        let mut applied = vec![Complex64::new(0.0, 0.0); current.len()];
2700        if let Err(error) = self.operator.apply(&current, &mut applied) {
2701            self.failed = true;
2702            return Some(Err(error));
2703        }
2704        let alpha = inner(&current, &applied);
2705        if alpha.im.abs() > self.options.tolerance.max(1.0e-10) {
2706            self.failed = true;
2707            return Some(Err(QmbedError::NonHermitian));
2708        }
2709        for (value, basis_value) in applied.iter_mut().zip(&current) {
2710            *value -= alpha.re * *basis_value;
2711        }
2712        if let Some(previous) = &self.previous {
2713            for (value, basis_value) in applied.iter_mut().zip(previous) {
2714                *value -= self.previous_beta * *basis_value;
2715            }
2716        }
2717        let norm_before_reorthogonalization = vector_norm(&applied);
2718        for basis_vector in self.history.iter().chain(std::iter::once(&current)) {
2719            let correction = inner(basis_vector, &applied);
2720            for (value, basis_value) in applied.iter_mut().zip(basis_vector) {
2721                *value -= correction * *basis_value;
2722            }
2723        }
2724        let norm_after_first_pass = vector_norm(&applied);
2725        if norm_before_reorthogonalization > f64::EPSILON
2726            && norm_after_first_pass
2727                <= DGKS_REORTHOGONALIZATION_THRESHOLD * norm_before_reorthogonalization
2728        {
2729            for basis_vector in self.history.iter().chain(std::iter::once(&current)) {
2730                let correction = inner(basis_vector, &applied);
2731                for (value, basis_value) in applied.iter_mut().zip(basis_vector) {
2732                    *value -= correction * *basis_value;
2733                }
2734            }
2735        }
2736        let beta = vector_norm(&applied);
2737        let output = LanczosVector {
2738            index: self.index,
2739            vector: current.clone(),
2740            diagonal: alpha.re,
2741            next_off_diagonal: beta,
2742        };
2743        self.index += 1;
2744        self.history.push(current.clone());
2745        if self.index < self.options.krylov_dimension && beta > self.options.tolerance {
2746            for value in &mut applied {
2747                *value /= beta;
2748            }
2749            self.previous = Some(current);
2750            self.current = Some(applied);
2751            self.previous_beta = beta;
2752        } else {
2753            self.current = None;
2754        }
2755        Some(Ok(output))
2756    }
2757}
2758
2759/// Start a validated streaming Lanczos decomposition.
2760pub fn lanczos_iter<'a, O>(
2761    operator: &'a O,
2762    initial: &'a [Complex64],
2763    options: LanczosOptions,
2764) -> Result<LanczosIter<'a, O>>
2765where
2766    O: LinearOperator + ?Sized,
2767{
2768    options.validate()?;
2769    let shape = operator.shape();
2770    if shape.0 != shape.1 || initial.len() != shape.0 {
2771        return Err(QmbedError::DimensionMismatch(
2772            "Lanczos operator and initial vector do not match".into(),
2773        ));
2774    }
2775    let mut current = initial.to_vec();
2776    normalize(&mut current)?;
2777    let capacity = options.krylov_dimension;
2778    Ok(LanczosIter {
2779        operator,
2780        options,
2781        index: 0,
2782        previous: None,
2783        current: Some(current),
2784        previous_beta: 0.0,
2785        history: Vec::with_capacity(capacity),
2786        failed: false,
2787    })
2788}
2789
2790/// Materialize the full Lanczos basis and tridiagonal coefficients.
2791pub fn lanczos_full<O>(
2792    operator: &O,
2793    initial: &[Complex64],
2794    options: LanczosOptions,
2795) -> Result<LanczosDecomposition>
2796where
2797    O: LinearOperator + ?Sized,
2798{
2799    options.validate()?;
2800    let initial_norm = vector_norm(initial);
2801    let vectors: Vec<_> = lanczos_iter(operator, initial, options)?.collect::<Result<_>>()?;
2802    let off_diagonal = vectors
2803        .iter()
2804        .take(vectors.len().saturating_sub(1))
2805        .map(|vector| vector.next_off_diagonal)
2806        .collect();
2807    Ok(LanczosDecomposition {
2808        initial_norm,
2809        basis: vectors.iter().map(|vector| vector.vector.clone()).collect(),
2810        diagonal: vectors.iter().map(|vector| vector.diagonal).collect(),
2811        off_diagonal,
2812    })
2813}
2814
2815/// Full Lanczos basis plus the sorted eigensystem of its tridiagonal
2816/// projection. This is the reusable native object behind compatibility
2817/// interfaces which expose both Ritz data and later reconstruction.
2818pub fn lanczos_ritz<O>(
2819    operator: &O,
2820    initial: &[Complex64],
2821    options: LanczosOptions,
2822) -> Result<LanczosRitzDecomposition>
2823where
2824    O: LinearOperator + ?Sized,
2825{
2826    let decomposition = lanczos_full(operator, initial, options)?;
2827    let dimension = decomposition.diagonal.len();
2828    let mut tridiagonal = DMatrix::<f64>::zeros(dimension, dimension);
2829    for index in 0..dimension {
2830        tridiagonal[(index, index)] = decomposition.diagonal[index];
2831        if index + 1 < dimension {
2832            tridiagonal[(index, index + 1)] = decomposition.off_diagonal[index];
2833            tridiagonal[(index + 1, index)] = decomposition.off_diagonal[index];
2834        }
2835    }
2836    let eigensystem = SymmetricEigen::new(tridiagonal);
2837    let mut order = (0..dimension).collect::<Vec<_>>();
2838    order.sort_by(|left, right| {
2839        eigensystem.eigenvalues[*left].total_cmp(&eigensystem.eigenvalues[*right])
2840    });
2841    let eigenvalues = order
2842        .iter()
2843        .map(|index| eigensystem.eigenvalues[*index])
2844        .collect();
2845    let eigenvectors = order
2846        .iter()
2847        .map(|column| {
2848            (0..dimension)
2849                .map(|row| eigensystem.eigenvectors[(row, *column)])
2850                .collect()
2851        })
2852        .collect();
2853    Ok(LanczosRitzDecomposition {
2854        decomposition,
2855        eigenvalues,
2856        eigenvectors,
2857    })
2858}
2859
2860struct LanczosProjection {
2861    initial_norm: f64,
2862    basis: ProjectedBasis,
2863    residual_beta: f64,
2864    eigenvalues: Vec<f64>,
2865    eigenvectors: DMatrix<f64>,
2866}
2867
2868enum ProjectedBasis {
2869    Real(Vec<Vec<f64>>),
2870    Complex(Vec<Vec<Complex64>>),
2871}
2872
2873impl ProjectedBasis {
2874    fn len(&self) -> usize {
2875        match self {
2876            Self::Real(basis) => basis.len(),
2877            Self::Complex(basis) => basis.len(),
2878        }
2879    }
2880
2881    fn is_empty(&self) -> bool {
2882        self.len() == 0
2883    }
2884}
2885
2886pub(crate) fn lanczos_spectral_measure(
2887    operator: &(impl LinearOperator + ?Sized),
2888    source: &[Complex64],
2889    krylov_dimension: usize,
2890) -> Result<(Vec<f64>, Vec<f64>)> {
2891    let shape = operator.shape();
2892    if shape.0 != shape.1 || source.len() != shape.0 {
2893        return Err(QmbedError::DimensionMismatch(
2894            "spectral Lanczos requires a square operator matching the source".into(),
2895        ));
2896    }
2897    if krylov_dimension == 0 {
2898        return Err(QmbedError::InvalidOptions(
2899            "spectral Krylov dimension must be positive".into(),
2900        ));
2901    }
2902    let projection = lanczos_projection(operator, source, krylov_dimension)?;
2903    let weights = (0..projection.eigenvalues.len())
2904        .map(|index| projection.initial_norm.powi(2) * projection.eigenvectors[(0, index)].powi(2))
2905        .collect();
2906    Ok((projection.eigenvalues, weights))
2907}
2908
2909fn lanczos_projection(
2910    operator: &(impl LinearOperator + ?Sized),
2911    initial: &[Complex64],
2912    dimension: usize,
2913) -> Result<LanczosProjection> {
2914    let initial_norm = vector_norm(initial);
2915    if initial_norm <= f64::EPSILON {
2916        return Ok(LanczosProjection {
2917            initial_norm,
2918            basis: ProjectedBasis::Complex(Vec::new()),
2919            residual_beta: 0.0,
2920            eigenvalues: Vec::new(),
2921            eigenvectors: DMatrix::zeros(0, 0),
2922        });
2923    }
2924    if operator.is_real() && initial.iter().all(|value| value.im == 0.0) {
2925        return lanczos_projection_real(operator, initial, dimension);
2926    }
2927    let krylov_dimension = dimension.min(initial.len()).max(1);
2928    let mut first = initial.to_vec();
2929    for value in &mut first {
2930        *value /= initial_norm;
2931    }
2932    let mut basis = Vec::with_capacity(krylov_dimension);
2933    basis.push(first);
2934    let mut alphas = Vec::with_capacity(krylov_dimension);
2935    let mut betas = Vec::with_capacity(krylov_dimension.saturating_sub(1));
2936    let mut residual_beta = 0.0;
2937    let mut applied = vec![Complex64::new(0.0, 0.0); initial.len()];
2938
2939    for iteration in 0..krylov_dimension {
2940        operator.apply(&basis[iteration], &mut applied)?;
2941        let alpha = inner(&basis[iteration], &applied);
2942        if alpha.im.abs() > 1.0e-10 {
2943            return Err(QmbedError::NonHermitian);
2944        }
2945        alphas.push(alpha.re);
2946        for (value, basis_value) in applied.iter_mut().zip(&basis[iteration]) {
2947            *value -= alpha.re * *basis_value;
2948        }
2949        if iteration > 0 {
2950            for (value, previous) in applied.iter_mut().zip(&basis[iteration - 1]) {
2951                *value -= betas[iteration - 1] * *previous;
2952            }
2953        }
2954        // Exponential actions use the Hermitian three-term recurrence. Unlike
2955        // the multi-eigenpair solver, they do not need global Ritz-vector
2956        // orthogonality, so avoiding O(m^2 n) reorthogonalization is essential
2957        // for the 100-step paper workflows.
2958        let beta = vector_norm(&applied);
2959        residual_beta = beta;
2960        if iteration + 1 == krylov_dimension || beta <= 1.0e-14 {
2961            break;
2962        }
2963        betas.push(beta);
2964        for value in &mut applied {
2965            *value /= beta;
2966        }
2967        basis.push(applied.clone());
2968    }
2969
2970    let size = basis.len();
2971    let mut tridiagonal = DMatrix::<f64>::zeros(size, size);
2972    for index in 0..size {
2973        tridiagonal[(index, index)] = alphas[index];
2974        if index + 1 < size {
2975            tridiagonal[(index, index + 1)] = betas[index];
2976            tridiagonal[(index + 1, index)] = betas[index];
2977        }
2978    }
2979    let decomposition = SymmetricEigen::new(tridiagonal);
2980    Ok(LanczosProjection {
2981        initial_norm,
2982        basis: ProjectedBasis::Complex(basis),
2983        residual_beta,
2984        eigenvalues: decomposition.eigenvalues.as_slice().to_vec(),
2985        eigenvectors: decomposition.eigenvectors,
2986    })
2987}
2988
2989fn lanczos_projection_real(
2990    operator: &(impl LinearOperator + ?Sized),
2991    initial: &[Complex64],
2992    dimension: usize,
2993) -> Result<LanczosProjection> {
2994    let initial_norm = initial
2995        .iter()
2996        .map(|value| value.re * value.re)
2997        .sum::<f64>()
2998        .sqrt();
2999    let krylov_dimension = dimension.min(initial.len()).max(1);
3000    let mut basis = Vec::with_capacity(krylov_dimension);
3001    basis.push(
3002        initial
3003            .iter()
3004            .map(|value| value.re / initial_norm)
3005            .collect::<Vec<_>>(),
3006    );
3007    let mut alphas = Vec::with_capacity(krylov_dimension);
3008    let mut betas = Vec::with_capacity(krylov_dimension.saturating_sub(1));
3009    let mut residual_beta = 0.0;
3010    let mut applied = vec![0.0; initial.len()];
3011
3012    for iteration in 0..krylov_dimension {
3013        operator.apply_real(&basis[iteration], &mut applied)?;
3014        let alpha = real_inner(&basis[iteration], &applied);
3015        alphas.push(alpha);
3016        for (value, basis_value) in applied.iter_mut().zip(&basis[iteration]) {
3017            *value -= alpha * *basis_value;
3018        }
3019        if iteration > 0 {
3020            let previous_beta = betas[iteration - 1];
3021            for (value, basis_value) in applied.iter_mut().zip(&basis[iteration - 1]) {
3022                *value -= previous_beta * *basis_value;
3023            }
3024        }
3025        let beta = real_vector_norm(&applied);
3026        residual_beta = beta;
3027        if iteration + 1 == krylov_dimension || beta <= 1.0e-14 {
3028            break;
3029        }
3030        betas.push(beta);
3031        for value in &mut applied {
3032            *value /= beta;
3033        }
3034        basis.push(applied.clone());
3035    }
3036
3037    let size = basis.len();
3038    let mut tridiagonal = DMatrix::<f64>::zeros(size, size);
3039    for index in 0..size {
3040        tridiagonal[(index, index)] = alphas[index];
3041        if index + 1 < size {
3042            tridiagonal[(index, index + 1)] = betas[index];
3043            tridiagonal[(index + 1, index)] = betas[index];
3044        }
3045    }
3046    let decomposition = SymmetricEigen::new(tridiagonal);
3047    Ok(LanczosProjection {
3048        initial_norm,
3049        basis: ProjectedBasis::Real(basis),
3050        residual_beta,
3051        eigenvalues: decomposition.eigenvalues.as_slice().to_vec(),
3052        eigenvectors: decomposition.eigenvectors,
3053    })
3054}
3055
3056fn projected_exponential_coefficients(
3057    projection: &LanczosProjection,
3058    interval: f64,
3059    hamiltonian: bool,
3060) -> Vec<Complex64> {
3061    let size = projection.basis.len();
3062    let mut coefficients = vec![Complex64::new(0.0, 0.0); size];
3063    for eigen_index in 0..size {
3064        let exponent = if hamiltonian {
3065            Complex64::new(0.0, -interval * projection.eigenvalues[eigen_index]).exp()
3066        } else {
3067            Complex64::new(interval * projection.eigenvalues[eigen_index], 0.0).exp()
3068        };
3069        let weight = projection.initial_norm * projection.eigenvectors[(0, eigen_index)] * exponent;
3070        for (basis_index, coefficient) in coefficients.iter_mut().enumerate() {
3071            *coefficient += projection.eigenvectors[(basis_index, eigen_index)] * weight;
3072        }
3073    }
3074    coefficients
3075}
3076
3077fn projected_exponential_action_from_coefficients(
3078    projection: &LanczosProjection,
3079    coefficients: &[Complex64],
3080    hamiltonian: bool,
3081    ambient_dimension: usize,
3082) -> Vec<Complex64> {
3083    if projection.basis.is_empty() {
3084        return vec![Complex64::new(0.0, 0.0); ambient_dimension];
3085    }
3086    let mut output = vec![Complex64::new(0.0, 0.0); ambient_dimension];
3087    match &projection.basis {
3088        ProjectedBasis::Real(basis) => {
3089            for (coefficient, vector) in coefficients.iter().zip(basis) {
3090                for (value, basis_value) in output.iter_mut().zip(vector) {
3091                    value.re += coefficient.re * *basis_value;
3092                    value.im += coefficient.im * *basis_value;
3093                }
3094            }
3095        }
3096        ProjectedBasis::Complex(basis) => {
3097            for (coefficient, vector) in coefficients.iter().zip(basis) {
3098                for (value, basis_value) in output.iter_mut().zip(vector) {
3099                    *value += *coefficient * *basis_value;
3100                }
3101            }
3102        }
3103    }
3104    if hamiltonian {
3105        let output_norm = vector_norm(&output);
3106        if output_norm > f64::EPSILON && output_norm.is_finite() {
3107            let scale = projection.initial_norm / output_norm;
3108            for value in &mut output {
3109                *value *= scale;
3110            }
3111        }
3112    }
3113    output
3114}
3115
3116fn projected_exponential_action(
3117    projection: &LanczosProjection,
3118    interval: f64,
3119    hamiltonian: bool,
3120    ambient_dimension: usize,
3121) -> Vec<Complex64> {
3122    let coefficients = projected_exponential_coefficients(projection, interval, hamiltonian);
3123    projected_exponential_action_from_coefficients(
3124        projection,
3125        &coefficients,
3126        hamiltonian,
3127        ambient_dimension,
3128    )
3129}
3130
3131fn projected_residual_amplitude(projection: &LanczosProjection, interval: f64) -> f64 {
3132    let Some(last_index) = projection.basis.len().checked_sub(1) else {
3133        return 0.0;
3134    };
3135    let mut amplitude = Complex64::new(0.0, 0.0);
3136    for eigen_index in 0..projection.eigenvalues.len() {
3137        let phase = Complex64::new(0.0, -interval * projection.eigenvalues[eigen_index]).exp();
3138        amplitude += projection.initial_norm
3139            * projection.eigenvectors[(0, eigen_index)]
3140            * projection.eigenvectors[(last_index, eigen_index)]
3141            * phase;
3142    }
3143    amplitude.norm()
3144}
3145
3146/// A posteriori Hermitian Krylov error estimate.
3147///
3148/// For `V_m exp(-i t T_m) e_1`, the residual norm is
3149/// `beta_m |e_m^T exp(-i t T_m) e_1|`. Duhamel's formula bounds the state
3150/// error by its time integral because the exact Hermitian propagator is
3151/// unitary. The integral is evaluated entirely in the small projected space,
3152/// so rejected trial intervals never materialize ambient-dimension vectors.
3153fn projected_exponential_error_bound(
3154    projection: &LanczosProjection,
3155    interval: f64,
3156    ambient_dimension: usize,
3157) -> f64 {
3158    let duration = interval.abs();
3159    if duration <= f64::EPSILON
3160        || projection.basis.is_empty()
3161        || projection.basis.len() == ambient_dimension
3162        || projection.residual_beta <= 1.0e-14
3163    {
3164        return 0.0;
3165    }
3166
3167    let (minimum, maximum) = projection.eigenvalues.iter().copied().fold(
3168        (f64::INFINITY, f64::NEG_INFINITY),
3169        |(minimum, maximum), value| (minimum.min(value), maximum.max(value)),
3170    );
3171    let phase_span = duration * (maximum - minimum).max(0.0);
3172    let oscillations = (phase_span / std::f64::consts::PI).ceil().min(60.0) as usize;
3173    let mut intervals = (16 + 4 * oscillations).min(256);
3174    if intervals % 2 != 0 {
3175        intervals += 1;
3176    }
3177    let direction = interval.signum();
3178    let step = duration / intervals as f64;
3179    let mut integral = 0.0;
3180    for index in 0..=intervals {
3181        let weight = if index == 0 || index == intervals {
3182            1.0
3183        } else if index % 2 == 0 {
3184            2.0
3185        } else {
3186            4.0
3187        };
3188        integral +=
3189            weight * projected_residual_amplitude(projection, direction * step * index as f64);
3190    }
3191    // Simpson quadrature is highly accurate for the smooth projected
3192    // residual. A modest safety factor protects the accept/reject boundary
3193    // from quadrature and finite-precision Lanczos error.
3194    1.25 * projection.residual_beta * step * integral / 3.0
3195}
3196
3197fn evolve_hermitian_adaptive_grid(
3198    operator: &(impl LinearOperator + ?Sized),
3199    initial: &[Complex64],
3200    options: &EvolutionOptions,
3201) -> Result<(StateTrajectory, EvolutionDiagnostics)> {
3202    let mut states = Vec::with_capacity(options.times.len());
3203    let mut current_state = initial.to_vec();
3204    let mut current_time = 0.0;
3205    let mut output_index = 0;
3206    let mut substeps = 0;
3207    let mut diagnostics = EvolutionDiagnostics::default();
3208    let local_tolerance = options.tolerance / options.times.len().max(1) as f64;
3209
3210    while output_index < options.times.len() {
3211        let target_time = options.times[output_index];
3212        if (target_time - current_time).abs() <= 16.0 * f64::EPSILON * target_time.abs().max(1.0) {
3213            states.push(current_state.clone());
3214            current_time = target_time;
3215            output_index += 1;
3216            continue;
3217        }
3218        if substeps >= options.max_substeps {
3219            return Err(QmbedError::NonConvergence {
3220                iterations: substeps,
3221                residual: (target_time - current_time).abs(),
3222            });
3223        }
3224
3225        let projection = lanczos_projection(operator, &current_state, options.krylov_dimension)?;
3226        diagnostics.lanczos_projections += 1;
3227        diagnostics.matrix_vector_products += projection.basis.len();
3228        if matches!(&projection.basis, ProjectedBasis::Real(_)) {
3229            diagnostics.real_lanczos_projections += 1;
3230            diagnostics.real_matrix_vector_products += projection.basis.len();
3231        }
3232        let scale = vector_norm(&current_state).max(1.0);
3233        let threshold = local_tolerance * scale;
3234
3235        let mut accepted = Vec::new();
3236        for &time in &options.times[output_index..] {
3237            let interval = time - current_time;
3238            let error =
3239                projected_exponential_error_bound(&projection, interval, current_state.len());
3240            diagnostics.maximum_estimated_error = diagnostics.maximum_estimated_error.max(error);
3241            if error > threshold {
3242                diagnostics.rejected_trial_intervals += 1;
3243                break;
3244            }
3245            let candidate =
3246                projected_exponential_action(&projection, interval, true, current_state.len());
3247            accepted.push((time, candidate));
3248        }
3249        if !accepted.is_empty() {
3250            for (_, candidate) in &accepted {
3251                states.push(candidate.clone());
3252            }
3253            let (time, state) = accepted.pop().expect("accepted is nonempty");
3254            current_time = time;
3255            current_state = state;
3256            output_index += accepted.len() + 1;
3257            substeps += 1;
3258            diagnostics.accepted_substeps += 1;
3259            continue;
3260        }
3261
3262        let target_interval = target_time - current_time;
3263        let direction = target_interval.signum();
3264        let minimum_interval = 16.0 * f64::EPSILON * target_time.abs().max(1.0);
3265        let mut rejected_magnitude = target_interval.abs();
3266        let mut accepted_magnitude = rejected_magnitude;
3267        loop {
3268            let interval = direction * accepted_magnitude;
3269            let error =
3270                projected_exponential_error_bound(&projection, interval, current_state.len());
3271            diagnostics.maximum_estimated_error = diagnostics.maximum_estimated_error.max(error);
3272            if error <= threshold || accepted_magnitude <= minimum_interval {
3273                break;
3274            }
3275            diagnostics.rejected_trial_intervals += 1;
3276            rejected_magnitude = accepted_magnitude;
3277            accepted_magnitude *= 0.5;
3278        }
3279
3280        // Halving finds a safe bracket but can undershoot the largest stable
3281        // step by almost a factor of two. Refine only in projected space so a
3282        // projection advances as far as its error budget permits.
3283        if accepted_magnitude > minimum_interval && rejected_magnitude > accepted_magnitude {
3284            for _ in 0..8 {
3285                let trial_magnitude = 0.5 * (accepted_magnitude + rejected_magnitude);
3286                let error = projected_exponential_error_bound(
3287                    &projection,
3288                    direction * trial_magnitude,
3289                    current_state.len(),
3290                );
3291                diagnostics.maximum_estimated_error =
3292                    diagnostics.maximum_estimated_error.max(error);
3293                if error <= threshold {
3294                    accepted_magnitude = trial_magnitude;
3295                } else {
3296                    diagnostics.rejected_trial_intervals += 1;
3297                    rejected_magnitude = trial_magnitude;
3298                }
3299            }
3300        }
3301
3302        let interval = direction * accepted_magnitude;
3303        current_time += interval;
3304        current_state =
3305            projected_exponential_action(&projection, interval, true, current_state.len());
3306        substeps += 1;
3307        diagnostics.accepted_substeps += 1;
3308    }
3309    Ok((
3310        StateTrajectory {
3311            times: options.times.clone(),
3312            states,
3313        },
3314        diagnostics,
3315    ))
3316}
3317
3318pub(crate) fn expm_action(
3319    operator: &(impl LinearOperator + ?Sized),
3320    initial: &[Complex64],
3321    interval: f64,
3322    options: &EvolutionOptions,
3323) -> Result<Vec<Complex64>> {
3324    let shape = operator.shape();
3325    if shape.0 != shape.1 || initial.len() != shape.0 {
3326        return Err(QmbedError::DimensionMismatch(
3327            "evolution requires a square operator matching the state".into(),
3328        ));
3329    }
3330    if interval == 0.0 {
3331        return Ok(initial.to_vec());
3332    }
3333    if options.hamiltonian {
3334        let projection = lanczos_projection(operator, initial, options.krylov_dimension)?;
3335        return Ok(projected_exponential_action(
3336            &projection,
3337            interval,
3338            true,
3339            initial.len(),
3340        ));
3341    }
3342    let exponent = Complex64::new(interval, 0.0);
3343    expm_action_complex(
3344        operator,
3345        initial,
3346        exponent,
3347        options.krylov_dimension,
3348        options.tolerance,
3349        options.max_substeps,
3350    )
3351}
3352
3353pub(crate) fn expm_action_complex(
3354    operator: &(impl LinearOperator + ?Sized),
3355    initial: &[Complex64],
3356    exponent: Complex64,
3357    krylov_dimension: usize,
3358    tolerance: f64,
3359    max_substeps: usize,
3360) -> Result<Vec<Complex64>> {
3361    ExpmActionPlan::new(
3362        operator,
3363        exponent,
3364        krylov_dimension,
3365        tolerance,
3366        max_substeps,
3367    )?
3368    .apply(operator, initial)
3369}
3370
3371/// Time evolution on an arbitrary square stored or matrix-free operator.
3372pub fn evolve_with_diagnostics<O>(
3373    operator: &O,
3374    initial: &[Complex64],
3375    options: EvolutionOptions,
3376) -> Result<(StateTrajectory, EvolutionDiagnostics)>
3377where
3378    O: LinearOperator + ?Sized,
3379{
3380    options.validate()?;
3381    let shape = operator.shape();
3382    if shape.0 != shape.1 || initial.len() != shape.0 {
3383        return Err(QmbedError::DimensionMismatch(
3384            "evolution operator and initial state do not match".into(),
3385        ));
3386    }
3387    let mut states = Vec::with_capacity(options.times.len());
3388    if options.hamiltonian {
3389        return evolve_hermitian_adaptive_grid(operator, initial, &options);
3390    }
3391    let mut state = initial.to_vec();
3392    let mut previous_time = 0.0;
3393    for &time in &options.times {
3394        state = expm_action(operator, &state, time - previous_time, &options)?;
3395        states.push(state.clone());
3396        previous_time = time;
3397    }
3398    Ok((
3399        StateTrajectory {
3400            times: options.times,
3401            states,
3402        },
3403        EvolutionDiagnostics::default(),
3404    ))
3405}
3406
3407/// Time evolution on an arbitrary square stored or matrix-free operator.
3408pub fn evolve<O>(
3409    operator: &O,
3410    initial: &[Complex64],
3411    options: EvolutionOptions,
3412) -> Result<StateTrajectory>
3413where
3414    O: LinearOperator + ?Sized,
3415{
3416    Ok(evolve_with_diagnostics(operator, initial, options)?.0)
3417}
3418
3419/// Exponential action over a time grid. Hermitian Hamiltonians reuse each
3420/// residual-controlled Lanczos projection across the longest accepted prefix.
3421pub fn expm_multiply<O>(
3422    operator: &O,
3423    initial: &[Complex64],
3424    options: EvolutionOptions,
3425) -> Result<StateTrajectory>
3426where
3427    O: LinearOperator + ?Sized,
3428{
3429    evolve(operator, initial, options)
3430}
3431
3432pub fn expm_lanczos<O>(
3433    operator: &O,
3434    initial: &[Complex64],
3435    time: f64,
3436    options: LanczosOptions,
3437) -> Result<Vec<Complex64>>
3438where
3439    O: LinearOperator + ?Sized,
3440{
3441    options.validate()?;
3442    if !time.is_finite() {
3443        return Err(QmbedError::InvalidOptions(
3444            "exponential time must be finite".into(),
3445        ));
3446    }
3447    let projection = lanczos_projection(operator, initial, options.krylov_dimension)?;
3448    Ok(projected_exponential_action(
3449        &projection,
3450        time,
3451        true,
3452        initial.len(),
3453    ))
3454}
3455
3456#[derive(Clone, Debug)]
3457pub struct ThermalIteration {
3458    pub inverse_temperatures: Vec<f64>,
3459    pub log_partition: Vec<f64>,
3460    pub mean_energy: Vec<f64>,
3461    pub krylov_dimension: usize,
3462}
3463
3464fn thermal_lanczos_iteration<O>(
3465    operator: &O,
3466    initial: &[Complex64],
3467    inverse_temperatures: &[f64],
3468    options: LanczosOptions,
3469) -> Result<ThermalIteration>
3470where
3471    O: LinearOperator + ?Sized,
3472{
3473    options.validate()?;
3474    if inverse_temperatures.is_empty()
3475        || inverse_temperatures
3476            .iter()
3477            .any(|beta| !beta.is_finite() || *beta < 0.0)
3478    {
3479        return Err(QmbedError::InvalidOptions(
3480            "inverse temperatures must be nonempty, finite, and nonnegative".into(),
3481        ));
3482    }
3483    let decomposition = lanczos_full(operator, initial, options)?;
3484    let size = decomposition.diagonal.len();
3485    let mut tridiagonal = DMatrix::<f64>::zeros(size, size);
3486    for index in 0..size {
3487        tridiagonal[(index, index)] = decomposition.diagonal[index];
3488        if index + 1 < size {
3489            tridiagonal[(index, index + 1)] = decomposition.off_diagonal[index];
3490            tridiagonal[(index + 1, index)] = decomposition.off_diagonal[index];
3491        }
3492    }
3493    let eigensystem = SymmetricEigen::new(tridiagonal);
3494    let weights: Vec<_> = (0..size)
3495        .map(|index| {
3496            decomposition.initial_norm.powi(2) * eigensystem.eigenvectors[(0, index)].powi(2)
3497        })
3498        .collect();
3499    let hilbert_dimension = operator.shape().0 as f64;
3500    let minimum_energy = eigensystem
3501        .eigenvalues
3502        .iter()
3503        .copied()
3504        .fold(f64::INFINITY, f64::min);
3505    let mut log_partition = Vec::with_capacity(inverse_temperatures.len());
3506    let mut mean_energy = Vec::with_capacity(inverse_temperatures.len());
3507    for &beta in inverse_temperatures {
3508        let boltzmann: Vec<_> = eigensystem
3509            .eigenvalues
3510            .iter()
3511            .zip(&weights)
3512            .map(|(energy, weight)| weight * (-beta * (*energy - minimum_energy)).exp())
3513            .collect();
3514        let projected_partition = boltzmann.iter().sum::<f64>();
3515        if projected_partition <= f64::EPSILON {
3516            return Err(QmbedError::NonConvergence {
3517                iterations: size,
3518                residual: projected_partition,
3519            });
3520        }
3521        log_partition.push(
3522            (hilbert_dimension / decomposition.initial_norm.powi(2)).ln() - beta * minimum_energy
3523                + projected_partition.ln(),
3524        );
3525        mean_energy.push(
3526            boltzmann
3527                .iter()
3528                .zip(eigensystem.eigenvalues.iter())
3529                .map(|(weight, energy)| weight * energy)
3530                .sum::<f64>()
3531                / projected_partition,
3532        );
3533    }
3534    Ok(ThermalIteration {
3535        inverse_temperatures: inverse_temperatures.to_vec(),
3536        log_partition,
3537        mean_energy,
3538        krylov_dimension: size,
3539    })
3540}
3541
3542/// One finite-temperature Lanczos random-vector iteration.
3543pub fn ftlm_static_iteration<O>(
3544    operator: &O,
3545    initial: &[Complex64],
3546    inverse_temperatures: &[f64],
3547    options: LanczosOptions,
3548) -> Result<ThermalIteration>
3549where
3550    O: LinearOperator + ?Sized,
3551{
3552    thermal_lanczos_iteration(operator, initial, inverse_temperatures, options)
3553}
3554
3555/// Low-temperature Lanczos iteration using a ground-energy shifted Boltzmann
3556/// evaluation for numerical stability.
3557pub fn ltlm_static_iteration<O>(
3558    operator: &O,
3559    initial: &[Complex64],
3560    inverse_temperatures: &[f64],
3561    options: LanczosOptions,
3562) -> Result<ThermalIteration>
3563where
3564    O: LinearOperator + ?Sized,
3565{
3566    thermal_lanczos_iteration(operator, initial, inverse_temperatures, options)
3567}
3568
3569#[derive(Clone, Debug)]
3570pub struct ThermalObservableIteration {
3571    pub inverse_temperatures: Vec<f64>,
3572    pub values: std::collections::HashMap<String, Vec<Complex64>>,
3573    pub identity: Vec<f64>,
3574}
3575
3576/// Finite-temperature contraction applied to a precomputed Lanczos
3577/// eigensystem and observable projections.
3578#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3579pub enum ThermalLanczosMethod {
3580    Ftlm,
3581    Ltlm,
3582}
3583
3584/// Observable data after projecting the language- or backend-owned action
3585/// into a Lanczos basis.
3586///
3587/// FTLM stores `m` overlaps `⟨q_i|A|q_0⟩`. LTLM stores the `m × m` matrix
3588/// `⟨q_j|A|q_i⟩` in input-major order (`i * m + j`). This is the narrow
3589/// interface needed by the thermal contraction and does not require Rust to
3590/// own the original observable.
3591#[derive(Clone, Debug)]
3592pub struct ProjectedThermalObservable {
3593    pub name: String,
3594    pub matrix_elements: Vec<Complex64>,
3595}
3596
3597fn validate_thermal_ritz_data(
3598    eigenvalues: &[f64],
3599    eigenvectors: &[Vec<f64>],
3600    observables: &[ProjectedThermalObservable],
3601    inverse_temperatures: &[f64],
3602    method: ThermalLanczosMethod,
3603) -> Result<()> {
3604    let dimension = eigenvalues.len();
3605    if dimension == 0
3606        || eigenvalues.iter().any(|value| !value.is_finite())
3607        || eigenvectors.len() != dimension
3608        || eigenvectors.iter().any(|vector| {
3609            vector.len() != dimension || vector.iter().any(|value| !value.is_finite())
3610        })
3611    {
3612        return Err(QmbedError::DimensionMismatch(
3613            "thermal Lanczos contraction requires a finite square Ritz eigensystem".into(),
3614        ));
3615    }
3616    if observables.is_empty()
3617        || inverse_temperatures.is_empty()
3618        || inverse_temperatures.iter().any(|beta| !beta.is_finite())
3619    {
3620        return Err(QmbedError::InvalidOptions(
3621            "thermal observables and inverse temperatures must be nonempty and valid".into(),
3622        ));
3623    }
3624    let mut names = std::collections::HashSet::new();
3625    let expected = match method {
3626        ThermalLanczosMethod::Ftlm => dimension,
3627        ThermalLanczosMethod::Ltlm => dimension
3628            .checked_mul(dimension)
3629            .ok_or_else(|| QmbedError::DimensionMismatch("Lanczos dimension overflows".into()))?,
3630    };
3631    for observable in observables {
3632        if observable.name.is_empty()
3633            || !names.insert(observable.name.as_str())
3634            || observable.matrix_elements.len() != expected
3635            || observable
3636                .matrix_elements
3637                .iter()
3638                .any(|value| !value.re.is_finite() || !value.im.is_finite())
3639        {
3640            return Err(QmbedError::DimensionMismatch(
3641                "thermal observables require unique names and matching finite projections".into(),
3642            ));
3643        }
3644    }
3645    Ok(())
3646}
3647
3648fn ftlm_projected_contraction(
3649    eigenvalues: &[f64],
3650    eigenvectors: &[Vec<f64>],
3651    observables: &[ProjectedThermalObservable],
3652    inverse_temperatures: &[f64],
3653) -> ThermalObservableIteration {
3654    let dimension = eigenvalues.len();
3655    let coefficients = inverse_temperatures
3656        .iter()
3657        .map(|beta| {
3658            (0..dimension)
3659                .map(|row| {
3660                    (0..dimension)
3661                        .map(|eigen| {
3662                            eigenvectors[eigen][row]
3663                                * eigenvectors[eigen][0]
3664                                * (-beta * eigenvalues[eigen]).exp()
3665                        })
3666                        .sum::<f64>()
3667                })
3668                .collect::<Vec<_>>()
3669        })
3670        .collect::<Vec<_>>();
3671    let identity = coefficients
3672        .iter()
3673        .map(|coefficient| coefficient[0])
3674        .collect();
3675    let values = observables
3676        .iter()
3677        .map(|observable| {
3678            let estimates = coefficients
3679                .iter()
3680                .map(|coefficient| {
3681                    observable
3682                        .matrix_elements
3683                        .iter()
3684                        .zip(coefficient)
3685                        .map(|(overlap, coefficient)| *overlap * *coefficient)
3686                        .sum()
3687                })
3688                .collect();
3689            (observable.name.clone(), estimates)
3690        })
3691        .collect();
3692    ThermalObservableIteration {
3693        inverse_temperatures: inverse_temperatures.to_vec(),
3694        values,
3695        identity,
3696    }
3697}
3698
3699fn ltlm_effective_dimension(
3700    eigenvalues: &[f64],
3701    eigenvectors: &[Vec<f64>],
3702    inverse_temperatures: &[f64],
3703) -> usize {
3704    let minimum_beta = inverse_temperatures
3705        .iter()
3706        .copied()
3707        .fold(f64::INFINITY, f64::min);
3708    let maximum_first_component = eigenvectors
3709        .iter()
3710        .map(|vector| vector[0].abs())
3711        .fold(0.0_f64, f64::max);
3712    eigenvalues
3713        .iter()
3714        .position(|energy| (-energy * minimum_beta).exp() * maximum_first_component < f64::EPSILON)
3715        .unwrap_or(eigenvalues.len())
3716}
3717
3718fn ltlm_projected_contraction(
3719    eigenvalues: &[f64],
3720    eigenvectors: &[Vec<f64>],
3721    observables: &[ProjectedThermalObservable],
3722    inverse_temperatures: &[f64],
3723) -> Result<ThermalObservableIteration> {
3724    let full_dimension = eigenvalues.len();
3725    let dimension = ltlm_effective_dimension(eigenvalues, eigenvectors, inverse_temperatures);
3726    if dimension == 0 {
3727        return Err(QmbedError::NonConvergence {
3728            iterations: 0,
3729            residual: 0.0,
3730        });
3731    }
3732    let identity = inverse_temperatures
3733        .iter()
3734        .map(|beta| {
3735            (0..dimension)
3736                .map(|eigen| eigenvectors[eigen][0].powi(2) * (-beta * eigenvalues[eigen]).exp())
3737                .sum()
3738        })
3739        .collect();
3740    let mut values = std::collections::HashMap::new();
3741    for observable in observables {
3742        let mut transformed = vec![Complex64::new(0.0, 0.0); dimension * dimension];
3743        for left in 0..dimension {
3744            for right in 0..dimension {
3745                let mut value = Complex64::new(0.0, 0.0);
3746                for input in 0..dimension {
3747                    for bra in 0..dimension {
3748                        value += eigenvectors[left][input]
3749                            * observable.matrix_elements[input * full_dimension + bra]
3750                            * eigenvectors[right][bra];
3751                    }
3752                }
3753                transformed[left * dimension + right] = value;
3754            }
3755        }
3756        let estimates = inverse_temperatures
3757            .iter()
3758            .map(|beta| {
3759                let weights = (0..dimension)
3760                    .map(|eigen| eigenvectors[eigen][0] * (-0.5 * beta * eigenvalues[eigen]).exp())
3761                    .collect::<Vec<_>>();
3762                let mut estimate = Complex64::new(0.0, 0.0);
3763                for left in 0..dimension {
3764                    for right in 0..dimension {
3765                        estimate +=
3766                            weights[left] * transformed[left * dimension + right] * weights[right];
3767                    }
3768                }
3769                estimate
3770            })
3771            .collect();
3772        values.insert(observable.name.clone(), estimates);
3773    }
3774    Ok(ThermalObservableIteration {
3775        inverse_temperatures: inverse_temperatures.to_vec(),
3776        values,
3777        identity,
3778    })
3779}
3780
3781/// Contract projected observables with an existing Lanczos Ritz eigensystem.
3782///
3783/// `eigenvectors` are column-oriented, matching [`LanczosRitzDecomposition`].
3784/// This function is useful at language boundaries where an observable may be
3785/// callback-owned but its action can still be projected into the Krylov basis.
3786pub fn thermal_observable_contraction(
3787    method: ThermalLanczosMethod,
3788    eigenvalues: &[f64],
3789    eigenvectors: &[Vec<f64>],
3790    observables: &[ProjectedThermalObservable],
3791    inverse_temperatures: &[f64],
3792) -> Result<ThermalObservableIteration> {
3793    validate_thermal_ritz_data(
3794        eigenvalues,
3795        eigenvectors,
3796        observables,
3797        inverse_temperatures,
3798        method,
3799    )?;
3800    match method {
3801        ThermalLanczosMethod::Ftlm => Ok(ftlm_projected_contraction(
3802            eigenvalues,
3803            eigenvectors,
3804            observables,
3805            inverse_temperatures,
3806        )),
3807        ThermalLanczosMethod::Ltlm => {
3808            ltlm_projected_contraction(eigenvalues, eigenvectors, observables, inverse_temperatures)
3809        }
3810    }
3811}
3812
3813impl LanczosRitzDecomposition {
3814    /// Apply observables to the stored Krylov basis and return only the
3815    /// projected data required by the selected thermal contraction.
3816    pub fn project_thermal_observables(
3817        &self,
3818        observables: &[(String, &dyn LinearOperator)],
3819        method: ThermalLanczosMethod,
3820    ) -> Result<Vec<ProjectedThermalObservable>> {
3821        let dimension = self.decomposition.basis.first().map_or(0, Vec::len);
3822        if observables.is_empty() {
3823            return Err(QmbedError::InvalidOptions(
3824                "thermal observables must be nonempty".into(),
3825            ));
3826        }
3827        let mut names = std::collections::HashSet::new();
3828        let mut projected = Vec::with_capacity(observables.len());
3829        for (name, observable) in observables {
3830            if name.is_empty()
3831                || !names.insert(name.as_str())
3832                || observable.shape() != (dimension, dimension)
3833            {
3834                return Err(QmbedError::DimensionMismatch(
3835                    "thermal observables require unique names and matching square shapes".into(),
3836                ));
3837            }
3838            let input_count = match method {
3839                ThermalLanczosMethod::Ftlm => 1,
3840                ThermalLanczosMethod::Ltlm => self.decomposition.basis.len(),
3841            };
3842            let mut matrix_elements =
3843                Vec::with_capacity(input_count * self.decomposition.basis.len());
3844            let mut applied = vec![Complex64::new(0.0, 0.0); dimension];
3845            for input in 0..input_count {
3846                observable.apply(&self.decomposition.basis[input], &mut applied)?;
3847                matrix_elements.extend(
3848                    self.decomposition
3849                        .basis
3850                        .iter()
3851                        .map(|bra| inner(bra, &applied)),
3852                );
3853            }
3854            projected.push(ProjectedThermalObservable {
3855                name: name.clone(),
3856                matrix_elements,
3857            });
3858        }
3859        Ok(projected)
3860    }
3861
3862    pub fn thermal_observable_iteration(
3863        &self,
3864        method: ThermalLanczosMethod,
3865        observables: &[(String, &dyn LinearOperator)],
3866        inverse_temperatures: &[f64],
3867    ) -> Result<ThermalObservableIteration> {
3868        let projected = self.project_thermal_observables(observables, method)?;
3869        thermal_observable_contraction(
3870            method,
3871            &self.eigenvalues,
3872            &self.eigenvectors,
3873            &projected,
3874            inverse_temperatures,
3875        )
3876    }
3877}
3878
3879/// QuSpin-compatible one-sided FTLM observable estimates.
3880pub fn ftlm_observable_iteration<O>(
3881    hamiltonian: &O,
3882    initial: &[Complex64],
3883    observables: &[(String, &dyn LinearOperator)],
3884    inverse_temperatures: &[f64],
3885    options: LanczosOptions,
3886) -> Result<ThermalObservableIteration>
3887where
3888    O: LinearOperator + ?Sized,
3889{
3890    let decomposition = lanczos_ritz(hamiltonian, initial, options)?;
3891    decomposition.thermal_observable_iteration(
3892        ThermalLanczosMethod::Ftlm,
3893        observables,
3894        inverse_temperatures,
3895    )
3896}
3897
3898/// Symmetric low-temperature Lanczos observable estimates.
3899pub fn ltlm_observable_iteration<O>(
3900    hamiltonian: &O,
3901    initial: &[Complex64],
3902    observables: &[(String, &dyn LinearOperator)],
3903    inverse_temperatures: &[f64],
3904    options: LanczosOptions,
3905) -> Result<ThermalObservableIteration>
3906where
3907    O: LinearOperator + ?Sized,
3908{
3909    let decomposition = lanczos_ritz(hamiltonian, initial, options)?;
3910    decomposition.thermal_observable_iteration(
3911        ThermalLanczosMethod::Ltlm,
3912        observables,
3913        inverse_temperatures,
3914    )
3915}
3916
3917pub fn linear_combination_qt(
3918    basis: &[Vec<Complex64>],
3919    coefficients: &[Complex64],
3920) -> Result<Vec<Complex64>> {
3921    if basis.len() != coefficients.len() || basis.is_empty() {
3922        return Err(QmbedError::DimensionMismatch(
3923            "basis and coefficient counts must be equal and nonzero".into(),
3924        ));
3925    }
3926    let dimension = basis[0].len();
3927    if basis.iter().any(|vector| vector.len() != dimension) {
3928        return Err(QmbedError::DimensionMismatch(
3929            "linear-combination basis vectors must have equal lengths".into(),
3930        ));
3931    }
3932    let mut output = vec![Complex64::new(0.0, 0.0); dimension];
3933    for (coefficient, vector) in coefficients.iter().zip(basis) {
3934        for (value, basis_value) in output.iter_mut().zip(vector) {
3935            *value += *coefficient * *basis_value;
3936        }
3937    }
3938    Ok(output)
3939}
3940
3941fn time_derivative<O>(
3942    operator: &O,
3943    time: f64,
3944    state: &[Complex64],
3945    hamiltonian: bool,
3946) -> Result<Vec<Complex64>>
3947where
3948    O: TimeDependentOperator + ?Sized,
3949{
3950    let mut derivative = vec![Complex64::new(0.0, 0.0); state.len()];
3951    operator.apply_at(time, state, &mut derivative)?;
3952    if hamiltonian {
3953        for value in &mut derivative {
3954            *value *= Complex64::new(0.0, -1.0);
3955        }
3956    }
3957    Ok(derivative)
3958}
3959
3960fn rk4_step<O>(
3961    operator: &O,
3962    time: f64,
3963    state: &[Complex64],
3964    step: f64,
3965    hamiltonian: bool,
3966) -> Result<Vec<Complex64>>
3967where
3968    O: TimeDependentOperator + ?Sized,
3969{
3970    let k1 = time_derivative(operator, time, state, hamiltonian)?;
3971    let stage: Vec<_> = state
3972        .iter()
3973        .zip(&k1)
3974        .map(|(value, derivative)| *value + 0.5 * step * *derivative)
3975        .collect();
3976    let k2 = time_derivative(operator, time + 0.5 * step, &stage, hamiltonian)?;
3977    let stage: Vec<_> = state
3978        .iter()
3979        .zip(&k2)
3980        .map(|(value, derivative)| *value + 0.5 * step * *derivative)
3981        .collect();
3982    let k3 = time_derivative(operator, time + 0.5 * step, &stage, hamiltonian)?;
3983    let stage: Vec<_> = state
3984        .iter()
3985        .zip(&k3)
3986        .map(|(value, derivative)| *value + step * *derivative)
3987        .collect();
3988    let k4 = time_derivative(operator, time + step, &stage, hamiltonian)?;
3989    Ok(state
3990        .iter()
3991        .zip(k1.iter().zip(k2.iter().zip(k3.iter().zip(&k4))))
3992        .map(|(value, (first, (second, (third, fourth))))| {
3993            *value + step * (*first + 2.0 * *second + 2.0 * *third + *fourth) / 6.0
3994        })
3995        .collect())
3996}
3997
3998fn adaptive_time_interval<O>(
3999    operator: &O,
4000    initial_time: f64,
4001    initial: &[Complex64],
4002    interval: f64,
4003    options: &EvolutionOptions,
4004) -> Result<Vec<Complex64>>
4005where
4006    O: TimeDependentOperator + ?Sized,
4007{
4008    if interval == 0.0 {
4009        return Ok(initial.to_vec());
4010    }
4011    let target_time = initial_time + interval;
4012    let direction = interval.signum();
4013    let mut step = direction * interval.abs().min(0.1);
4014    let mut time = initial_time;
4015    let mut state = initial.to_vec();
4016    let mut steps = 0;
4017    let interval_scale = interval.abs().max(1.0);
4018    while direction * (target_time - time) > 16.0 * f64::EPSILON * target_time.abs().max(1.0) {
4019        if steps >= options.max_substeps {
4020            return Err(QmbedError::NonConvergence {
4021                iterations: steps,
4022                residual: (target_time - time).abs(),
4023            });
4024        }
4025        if direction * (time + step - target_time) > 0.0 {
4026            step = target_time - time;
4027        }
4028        let full = rk4_step(operator, time, &state, step, options.hamiltonian)?;
4029        let first_half = rk4_step(operator, time, &state, 0.5 * step, options.hamiltonian)?;
4030        let two_halves = rk4_step(
4031            operator,
4032            time + 0.5 * step,
4033            &first_half,
4034            0.5 * step,
4035            options.hamiltonian,
4036        )?;
4037        let error = full
4038            .iter()
4039            .zip(&two_halves)
4040            .map(|(coarse, fine)| (*coarse - *fine).norm_sqr())
4041            .sum::<f64>()
4042            .sqrt();
4043        let scale = vector_norm(&two_halves).max(1.0);
4044        // Treat `tolerance` as an interval-level budget rather than allowing
4045        // every accepted RK step to spend the full requested error. The
4046        // step/interval fraction makes accumulated long-time error track the
4047        // public tolerance while retaining the usual relative state scaling.
4048        let threshold =
4049            options.tolerance * scale * (step.abs() / interval_scale).clamp(f64::EPSILON, 1.0);
4050        if error <= threshold || step.abs() <= f64::EPSILON * time.abs().max(1.0) {
4051            state = two_halves;
4052            time += step;
4053            steps += 1;
4054            let growth = if error <= f64::EPSILON {
4055                2.0
4056            } else {
4057                (0.9 * (threshold / error).powf(0.2)).clamp(1.0, 2.0)
4058            };
4059            step *= growth;
4060        } else {
4061            let shrink = (0.9 * (threshold / error).powf(0.2)).clamp(0.1, 0.8);
4062            step *= shrink;
4063        }
4064    }
4065    Ok(state)
4066}
4067
4068/// Adaptive fourth-order evolution for an explicitly time-dependent operator.
4069pub fn evolve_time_dependent<O>(
4070    operator: &O,
4071    initial: &[Complex64],
4072    options: EvolutionOptions,
4073) -> Result<StateTrajectory>
4074where
4075    O: TimeDependentOperator + ?Sized,
4076{
4077    evolve_time_dependent_from(operator, initial, 0.0, options)
4078}
4079
4080/// Evolve an explicitly time-dependent operator from an arbitrary absolute
4081/// start time.
4082///
4083/// Unlike shifting the output grid to zero in a language adapter, this keeps
4084/// the times observed by the operator callback in the caller's physical time
4085/// coordinate.
4086pub fn evolve_time_dependent_from<O>(
4087    operator: &O,
4088    initial: &[Complex64],
4089    initial_time: f64,
4090    options: EvolutionOptions,
4091) -> Result<StateTrajectory>
4092where
4093    O: TimeDependentOperator + ?Sized,
4094{
4095    options.validate()?;
4096    if !initial_time.is_finite() || options.times[0] < initial_time {
4097        return Err(QmbedError::InvalidOptions(
4098            "initial time must be finite and no later than the first output time".into(),
4099        ));
4100    }
4101    let shape = operator.shape();
4102    if shape.0 != shape.1 || initial.len() != shape.0 {
4103        return Err(QmbedError::DimensionMismatch(
4104            "time-dependent operator and initial state do not match".into(),
4105        ));
4106    }
4107    let mut states = Vec::with_capacity(options.times.len());
4108    let mut state = initial.to_vec();
4109    let requested_norm = vector_norm(initial);
4110    let mut previous_time = initial_time;
4111    for &time in &options.times {
4112        state = adaptive_time_interval(
4113            operator,
4114            previous_time,
4115            &state,
4116            time - previous_time,
4117            &options,
4118        )?;
4119        if options.hamiltonian && requested_norm > f64::EPSILON {
4120            let norm = vector_norm(&state);
4121            if norm <= f64::EPSILON || !norm.is_finite() {
4122                return Err(QmbedError::NonConvergence {
4123                    iterations: options.max_substeps,
4124                    residual: norm,
4125                });
4126            }
4127            for value in &mut state {
4128                *value *= requested_norm / norm;
4129            }
4130        }
4131        states.push(state.clone());
4132        previous_time = time;
4133    }
4134    Ok(StateTrajectory {
4135        times: options.times,
4136        states,
4137    })
4138}
4139
4140/// Evolve independent column states without changing their column ordering.
4141pub fn evolve_batch<O>(
4142    operator: &O,
4143    initial_columns: &[Vec<Complex64>],
4144    options: EvolutionOptions,
4145) -> Result<StateBatchTrajectory>
4146where
4147    O: LinearOperator + ?Sized,
4148{
4149    if initial_columns.is_empty() {
4150        return Err(QmbedError::InvalidOptions(
4151            "a state batch must contain at least one column".into(),
4152        ));
4153    }
4154    let mut by_column = Vec::with_capacity(initial_columns.len());
4155    for initial in initial_columns {
4156        by_column.push(evolve(operator, initial, options.clone())?);
4157    }
4158    let states = (0..options.times.len())
4159        .map(|time_index| {
4160            by_column
4161                .iter()
4162                .map(|trajectory| trajectory.states[time_index].clone())
4163                .collect()
4164        })
4165        .collect();
4166    Ok(StateBatchTrajectory {
4167        times: options.times,
4168        states,
4169    })
4170}
4171
4172/// Batched counterpart of [`evolve_time_dependent`].
4173pub fn evolve_time_dependent_batch<O>(
4174    operator: &O,
4175    initial_columns: &[Vec<Complex64>],
4176    options: EvolutionOptions,
4177) -> Result<StateBatchTrajectory>
4178where
4179    O: TimeDependentOperator + ?Sized,
4180{
4181    evolve_time_dependent_batch_from(operator, initial_columns, 0.0, options)
4182}
4183
4184/// Batched counterpart of [`evolve_time_dependent_from`].
4185pub fn evolve_time_dependent_batch_from<O>(
4186    operator: &O,
4187    initial_columns: &[Vec<Complex64>],
4188    initial_time: f64,
4189    options: EvolutionOptions,
4190) -> Result<StateBatchTrajectory>
4191where
4192    O: TimeDependentOperator + ?Sized,
4193{
4194    if initial_columns.is_empty() {
4195        return Err(QmbedError::InvalidOptions(
4196            "a state batch must contain at least one column".into(),
4197        ));
4198    }
4199    let mut by_column = Vec::with_capacity(initial_columns.len());
4200    for initial in initial_columns {
4201        by_column.push(evolve_time_dependent_from(
4202            operator,
4203            initial,
4204            initial_time,
4205            options.clone(),
4206        )?);
4207    }
4208    let states = (0..options.times.len())
4209        .map(|time_index| {
4210            by_column
4211                .iter()
4212                .map(|trajectory| trajectory.states[time_index].clone())
4213                .collect()
4214        })
4215        .collect();
4216    Ok(StateBatchTrajectory {
4217        times: options.times,
4218        states,
4219    })
4220}
4221
4222/// Time grid and explicit-step controls for a caller-defined right-hand side.
4223#[derive(Clone, Debug, PartialEq)]
4224#[non_exhaustive]
4225pub struct RhsEvolutionOptions {
4226    /// Finite nondecreasing output times.
4227    pub times: Vec<f64>,
4228    /// Maximum Runge--Kutta step.
4229    pub max_step: f64,
4230    /// Maximum integration substeps.
4231    pub max_substeps: usize,
4232    /// Normalize each returned state.
4233    pub normalize: bool,
4234}
4235
4236impl RhsEvolutionOptions {
4237    /// Construct callable-RHS integration controls.
4238    pub fn new(times: impl Into<Vec<f64>>, max_step: f64) -> Self {
4239        Self {
4240            times: times.into(),
4241            max_step,
4242            max_substeps: 100_000,
4243            normalize: false,
4244        }
4245    }
4246
4247    /// Set the maximum number of integration substeps.
4248    #[must_use]
4249    pub fn with_max_substeps(mut self, max_substeps: usize) -> Self {
4250        self.max_substeps = max_substeps;
4251        self
4252    }
4253
4254    /// Normalize each accepted output state.
4255    #[must_use]
4256    pub fn with_normalization(mut self, normalize: bool) -> Self {
4257        self.normalize = normalize;
4258        self
4259    }
4260
4261    fn validate(&self, initial_time: f64) -> Result<()> {
4262        if !initial_time.is_finite()
4263            || self.times.is_empty()
4264            || self.times.iter().any(|time| !time.is_finite())
4265            || self.times.windows(2).any(|pair| pair[0] > pair[1])
4266            || self.times[0] < initial_time
4267            || !self.max_step.is_finite()
4268            || self.max_step <= 0.0
4269            || self.max_substeps == 0
4270        {
4271            return Err(QmbedError::InvalidOptions(
4272                "invalid callable-RHS time grid or integration controls".into(),
4273            ));
4274        }
4275        Ok(())
4276    }
4277}
4278
4279fn rhs_rk4_step<F>(
4280    derivative: &F,
4281    time: f64,
4282    state: &[Complex64],
4283    step: f64,
4284) -> Result<Vec<Complex64>>
4285where
4286    F: Fn(f64, &[Complex64], &mut [Complex64]) -> Result<()>,
4287{
4288    let dimension = state.len();
4289    let mut k1 = vec![Complex64::new(0.0, 0.0); dimension];
4290    derivative(time, state, &mut k1)?;
4291    let stage: Vec<_> = state
4292        .iter()
4293        .zip(&k1)
4294        .map(|(value, slope)| *value + 0.5 * step * *slope)
4295        .collect();
4296    let mut k2 = vec![Complex64::new(0.0, 0.0); dimension];
4297    derivative(time + 0.5 * step, &stage, &mut k2)?;
4298    let stage: Vec<_> = state
4299        .iter()
4300        .zip(&k2)
4301        .map(|(value, slope)| *value + 0.5 * step * *slope)
4302        .collect();
4303    let mut k3 = vec![Complex64::new(0.0, 0.0); dimension];
4304    derivative(time + 0.5 * step, &stage, &mut k3)?;
4305    let stage: Vec<_> = state
4306        .iter()
4307        .zip(&k3)
4308        .map(|(value, slope)| *value + step * *slope)
4309        .collect();
4310    let mut k4 = vec![Complex64::new(0.0, 0.0); dimension];
4311    derivative(time + step, &stage, &mut k4)?;
4312    Ok(state
4313        .iter()
4314        .zip(k1.iter().zip(k2.iter().zip(k3.iter().zip(&k4))))
4315        .map(|(value, (first, (second, (third, fourth))))| {
4316            *value + step * (*first + 2.0 * *second + 2.0 * *third + *fourth) / 6.0
4317        })
4318        .collect())
4319}
4320
4321/// Integrate an arbitrary complex right-hand side `dstate/dt = f(t, state)`.
4322pub fn evolve_rhs<F>(
4323    initial: &[Complex64],
4324    initial_time: f64,
4325    options: RhsEvolutionOptions,
4326    derivative: F,
4327) -> Result<StateTrajectory>
4328where
4329    F: Fn(f64, &[Complex64], &mut [Complex64]) -> Result<()>,
4330{
4331    options.validate(initial_time)?;
4332    if initial.is_empty() {
4333        return Err(QmbedError::DimensionMismatch(
4334            "callable-RHS state must be nonempty".into(),
4335        ));
4336    }
4337    let mut state = initial.to_vec();
4338    let normalization = vector_norm(initial);
4339    let mut current_time = initial_time;
4340    let mut used_steps = 0_usize;
4341    let mut states = Vec::with_capacity(options.times.len());
4342    for &target_time in &options.times {
4343        let interval = target_time - current_time;
4344        let steps = (interval.abs() / options.max_step).ceil().max(1.0) as usize;
4345        if used_steps.saturating_add(steps) > options.max_substeps {
4346            return Err(QmbedError::NonConvergence {
4347                iterations: used_steps,
4348                residual: interval.abs(),
4349            });
4350        }
4351        let step = interval / steps as f64;
4352        for _ in 0..steps {
4353            state = rhs_rk4_step(&derivative, current_time, &state, step)?;
4354            current_time += step;
4355        }
4356        if options.normalize && normalization > f64::EPSILON {
4357            let norm = vector_norm(&state);
4358            if norm <= f64::EPSILON || !norm.is_finite() {
4359                return Err(QmbedError::NonConvergence {
4360                    iterations: used_steps + steps,
4361                    residual: norm,
4362                });
4363            }
4364            for value in &mut state {
4365                *value *= normalization / norm;
4366            }
4367        }
4368        used_steps += steps;
4369        current_time = target_time;
4370        states.push(state.clone());
4371    }
4372    Ok(StateTrajectory {
4373        times: options.times,
4374        states,
4375    })
4376}
4377
4378/// Liouville-von Neumann evolution of a row-major density matrix under a
4379/// Hermitian static Hamiltonian.
4380pub fn evolve_density<O>(
4381    hamiltonian: &O,
4382    initial_density: &[Complex64],
4383    mut options: RhsEvolutionOptions,
4384) -> Result<StateTrajectory>
4385where
4386    O: LinearOperator + ?Sized,
4387{
4388    let shape = hamiltonian.shape();
4389    if shape.0 != shape.1 || initial_density.len() != shape.0.saturating_mul(shape.0) {
4390        return Err(QmbedError::DimensionMismatch(
4391            "density evolution requires a square Hamiltonian and density matrix".into(),
4392        ));
4393    }
4394    let dimension = shape.0;
4395    options.normalize = false;
4396    evolve_rhs(initial_density, 0.0, options, |_, density, output| {
4397        let mut column = vec![Complex64::new(0.0, 0.0); dimension];
4398        let mut applied = vec![Complex64::new(0.0, 0.0); dimension];
4399        let mut h_rho = vec![Complex64::new(0.0, 0.0); density.len()];
4400        let mut rho_h = vec![Complex64::new(0.0, 0.0); density.len()];
4401        for column_index in 0..dimension {
4402            for row in 0..dimension {
4403                column[row] = density[row * dimension + column_index];
4404            }
4405            hamiltonian.apply(&column, &mut applied)?;
4406            for row in 0..dimension {
4407                h_rho[row * dimension + column_index] = applied[row];
4408            }
4409        }
4410        // For Hermitian H, conjugating a row turns right multiplication by H
4411        // into the same column action used above.
4412        for row in 0..dimension {
4413            for column_index in 0..dimension {
4414                column[column_index] = density[row * dimension + column_index].conj();
4415            }
4416            hamiltonian.apply(&column, &mut applied)?;
4417            for column_index in 0..dimension {
4418                rho_h[row * dimension + column_index] = applied[column_index].conj();
4419            }
4420        }
4421        for index in 0..output.len() {
4422            output[index] = Complex64::new(0.0, -1.0) * (h_rho[index] - rho_h[index]);
4423        }
4424        Ok(())
4425    })
4426}
4427
4428#[cfg(test)]
4429mod tests {
4430    use super::{
4431        Complex64, ProjectedBasis, dgks_reorthogonalize_complex, dgks_reorthogonalize_real,
4432        lanczos_projection, projected_exponential_action, projected_exponential_error_bound,
4433    };
4434    use crate::Result;
4435    use crate::operator::{LinearOperator, MatrixFormat, Operator};
4436
4437    struct ComplexView<'a>(&'a Operator);
4438
4439    impl LinearOperator for ComplexView<'_> {
4440        fn shape(&self) -> (usize, usize) {
4441            self.0.shape()
4442        }
4443
4444        fn format(&self) -> MatrixFormat {
4445            self.0.format()
4446        }
4447
4448        fn apply(&self, input: &[Complex64], output: &mut [Complex64]) -> Result<()> {
4449            self.0.apply(input, output)
4450        }
4451    }
4452
4453    #[test]
4454    fn dgks_second_pass_is_conditional_for_real_and_complex_vectors() {
4455        let real_basis = vec![vec![1.0, 0.0]];
4456        let mut contaminated_real = vec![1.0, 1.0e-12];
4457        let (_, repeated_real) = dgks_reorthogonalize_real(&real_basis, &mut contaminated_real);
4458        assert!(repeated_real);
4459        let mut orthogonal_real = vec![0.0, 1.0];
4460        let (_, repeated_real) = dgks_reorthogonalize_real(&real_basis, &mut orthogonal_real);
4461        assert!(!repeated_real);
4462
4463        let complex_basis = vec![vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)]];
4464        let mut contaminated_complex = vec![Complex64::new(0.0, 1.0), Complex64::new(1.0e-12, 0.0)];
4465        let (_, repeated_complex) =
4466            dgks_reorthogonalize_complex(&complex_basis, &mut contaminated_complex);
4467        assert!(repeated_complex);
4468        let mut orthogonal_complex = vec![Complex64::new(0.0, 0.0), Complex64::new(0.0, 1.0)];
4469        let (_, repeated_complex) =
4470            dgks_reorthogonalize_complex(&complex_basis, &mut orthogonal_complex);
4471        assert!(!repeated_complex);
4472    }
4473
4474    #[test]
4475    fn real_and_complex_projected_bases_define_the_same_krylov_action() {
4476        let operator = Operator::from_triplets(
4477            4,
4478            4,
4479            [
4480                (0, 0, Complex64::new(-1.0, 0.0)),
4481                (0, 1, Complex64::new(0.5, 0.0)),
4482                (1, 0, Complex64::new(0.5, 0.0)),
4483                (1, 1, Complex64::new(0.25, 0.0)),
4484                (1, 2, Complex64::new(-0.7, 0.0)),
4485                (2, 1, Complex64::new(-0.7, 0.0)),
4486                (2, 3, Complex64::new(0.3, 0.0)),
4487                (3, 2, Complex64::new(0.3, 0.0)),
4488            ],
4489            MatrixFormat::Csc,
4490        )
4491        .unwrap();
4492        let initial = [
4493            Complex64::new(1.0, 0.0),
4494            Complex64::new(-0.5, 0.0),
4495            Complex64::new(0.25, 0.0),
4496            Complex64::new(0.75, 0.0),
4497        ];
4498        let real = lanczos_projection(&operator, &initial, 4).unwrap();
4499        let complex = lanczos_projection(&ComplexView(&operator), &initial, 4).unwrap();
4500        assert!(matches!(&real.basis, ProjectedBasis::Real(_)));
4501        assert!(matches!(&complex.basis, ProjectedBasis::Complex(_)));
4502
4503        for interval in [0.1, 1.7] {
4504            let real_state = projected_exponential_action(&real, interval, true, initial.len());
4505            let complex_state =
4506                projected_exponential_action(&complex, interval, true, initial.len());
4507            for (real_value, complex_value) in real_state.iter().zip(complex_state) {
4508                assert!((*real_value - complex_value).norm() < 1.0e-12);
4509            }
4510            let real_error = projected_exponential_error_bound(&real, interval, initial.len());
4511            let complex_error =
4512                projected_exponential_error_bound(&complex, interval, initial.len());
4513            assert!((real_error - complex_error).abs() < 1.0e-12);
4514        }
4515    }
4516}