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
52const 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#[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#[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#[derive(Clone, Copy, Debug, PartialEq)]
330#[non_exhaustive]
331pub enum SpectrumTarget {
332 SmallestAlgebraic,
334 LargestAlgebraic,
336 SmallestMagnitude,
338 LargestMagnitude,
340 BothEnds,
342 Shift(f64),
344}
345
346#[derive(Clone, Debug, PartialEq)]
348#[non_exhaustive]
349pub struct EigshOptions {
350 pub eigenpairs: usize,
352 pub target: SpectrumTarget,
354 pub krylov_dimension: Option<usize>,
356 pub tolerance: f64,
358 pub max_iterations: usize,
360 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 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 pub fn smallest_algebraic(eigenpairs: usize) -> Self {
389 Self::new(eigenpairs, SpectrumTarget::SmallestAlgebraic)
390 }
391
392 pub fn near_shift(eigenpairs: usize, shift: f64) -> Self {
394 Self::new(eigenpairs, SpectrumTarget::Shift(shift))
395 }
396
397 #[must_use]
399 pub fn with_krylov_dimension(mut self, dimension: usize) -> Self {
400 self.krylov_dimension = Some(dimension);
401 self
402 }
403
404 #[must_use]
406 pub fn with_tolerance(mut self, tolerance: f64) -> Self {
407 self.tolerance = tolerance;
408 self
409 }
410
411 #[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 #[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#[derive(Clone, Debug)]
453pub struct Eigensystem {
454 pub eigenvalues: Vec<f64>,
456 pub eigenvectors: Vec<Vec<Complex64>>,
458 pub residuals: Vec<f64>,
460 pub iterations: usize,
462 pub reorthogonalization_passes: usize,
464 pub conditional_second_passes: usize,
466 pub converged: bool,
468}
469
470#[derive(Clone, Debug, Default)]
477pub struct EigshWorkspace {
478 dimension: usize,
479 initial_subspace: Vec<Vec<Complex64>>,
480}
481
482impl EigshWorkspace {
483 pub const fn new() -> Self {
485 Self {
486 dimension: 0,
487 initial_subspace: Vec::new(),
488 }
489 }
490
491 pub fn clear(&mut self) {
493 self.dimension = 0;
494 self.initial_subspace.clear();
495 }
496
497 pub const fn dimension(&self) -> usize {
499 self.dimension
500 }
501
502 pub fn initial_subspace(&self) -> &[Vec<Complex64>] {
504 &self.initial_subspace
505 }
506
507 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
530#[non_exhaustive]
531pub struct EighOptions {
532 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 pub const fn new(return_eigenvectors: bool) -> Self {
547 Self {
548 return_eigenvectors,
549 }
550 }
551
552 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
585pub 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
607pub 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
855pub 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 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 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
2330pub 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
2352pub 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
2379pub 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
2412pub 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
2446pub 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#[derive(Clone, Debug, PartialEq)]
2456#[non_exhaustive]
2457pub struct EvolutionOptions {
2458 pub times: Vec<f64>,
2460 pub krylov_dimension: usize,
2462 pub tolerance: f64,
2464 pub max_substeps: usize,
2466 pub hamiltonian: bool,
2468}
2469
2470impl EvolutionOptions {
2471 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 #[must_use]
2484 pub fn with_krylov_dimension(mut self, dimension: usize) -> Self {
2485 self.krylov_dimension = dimension;
2486 self
2487 }
2488
2489 #[must_use]
2491 pub fn with_tolerance(mut self, tolerance: f64) -> Self {
2492 self.tolerance = tolerance;
2493 self
2494 }
2495
2496 #[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 #[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#[derive(Clone, Debug)]
2534pub struct StateTrajectory {
2535 pub times: Vec<f64>,
2537 pub states: Vec<Vec<Complex64>>,
2539}
2540
2541#[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#[derive(Clone, Debug)]
2559pub struct StateBatchTrajectory {
2560 pub times: Vec<f64>,
2561 pub states: Vec<Vec<Vec<Complex64>>>,
2562}
2563
2564#[derive(Clone, Debug, PartialEq)]
2566#[non_exhaustive]
2567pub struct LanczosOptions {
2568 pub krylov_dimension: usize,
2570 pub tolerance: f64,
2572}
2573
2574impl LanczosOptions {
2575 pub const fn new(krylov_dimension: usize) -> Self {
2577 Self {
2578 krylov_dimension,
2579 tolerance: 1.0e-12,
2580 }
2581 }
2582
2583 #[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#[derive(Clone, Debug)]
2602pub struct LanczosVector {
2603 pub index: usize,
2605 pub vector: Vec<Complex64>,
2607 pub diagonal: f64,
2609 pub next_off_diagonal: f64,
2611}
2612
2613#[derive(Clone, Debug)]
2615pub struct LanczosDecomposition {
2616 pub initial_norm: f64,
2618 pub basis: Vec<Vec<Complex64>>,
2620 pub diagonal: Vec<f64>,
2622 pub off_diagonal: Vec<f64>,
2624}
2625
2626#[derive(Clone, Debug)]
2627pub struct LanczosRitzDecomposition {
2628 pub decomposition: LanczosDecomposition,
2629 pub eigenvalues: Vec<f64>,
2630 pub eigenvectors: Vec<Vec<f64>>,
2632}
2633
2634impl LanczosRitzDecomposition {
2635 pub fn linear_combination(&self, coefficients: &[Complex64]) -> Result<Vec<Complex64>> {
2637 linear_combination_qt(&self.decomposition.basis, coefficients)
2638 }
2639
2640 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
2673pub 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(¤t, &mut applied) {
2701 self.failed = true;
2702 return Some(Err(error));
2703 }
2704 let alpha = inner(¤t, &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(¤t) {
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(¤t)) {
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(¤t)) {
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
2759pub 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
2790pub 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
2815pub 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 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
3146fn 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 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, ¤t_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(¤t_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 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
3371pub 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
3407pub 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
3419pub 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
3542pub 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
3555pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3579pub enum ThermalLanczosMethod {
3580 Ftlm,
3581 Ltlm,
3582}
3583
3584#[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
3781pub 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 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
3879pub 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
3898pub 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 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
4068pub 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
4080pub 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
4140pub 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
4172pub 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
4184pub 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#[derive(Clone, Debug, PartialEq)]
4224#[non_exhaustive]
4225pub struct RhsEvolutionOptions {
4226 pub times: Vec<f64>,
4228 pub max_step: f64,
4230 pub max_substeps: usize,
4232 pub normalize: bool,
4234}
4235
4236impl RhsEvolutionOptions {
4237 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 #[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 #[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
4321pub 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
4378pub 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 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}