Uni20
Asynchronous tensor and runtime library.
Loading...
Searching...
No Matches
nonsymmetric_arnoldi.hpp
Go to the documentation of this file.
1#pragma once
2
6
7#include <algorithm>
8#include <cmath>
9#include <complex>
10#include <cstddef>
11#include <limits>
12#include <numeric>
13#include <stdexcept>
14#include <utility>
15#include <vector>
16
17namespace uni20::krylov
18{
19
23template <uni20::RealOrComplex Scalar, typename Vector> struct ArnoldiFactorization
24{
25 std::vector<Vector> basis;
27 int step_count = 0;
28 int op_count = 0;
30 bool happy_breakdown = false;
31};
32
35template <uni20::Real Scalar> struct ArnoldiRitzExtraction
36{
37 std::vector<uni20::complex<Scalar>> ritz_values;
38 std::vector<Scalar> residual_bounds;
39 std::vector<RitzReality> reality;
41};
42
45template <uni20::LapackReal Scalar> struct RealSchurRestartSelection
46{
47 std::vector<std::size_t> block_indices;
48 std::size_t scalar_count = 0;
50};
51
61template <uni20::LapackReal Scalar, typename Vector> struct RealSchurCompressedArnoldiFactorization
62{
63 std::vector<Vector> basis;
64 Vector residual;
66 std::vector<Scalar> residual_coupling;
67 std::vector<uni20::complex<Scalar>> eigenvalues;
68 std::vector<RealSchurBlock<Scalar>> blocks;
70 bool happy_breakdown = false;
72};
73
82template <uni20::LapackComplexReal Real, typename Vector> struct ComplexSchurCompressedArnoldiFactorization
83{
85
86 std::vector<Vector> basis;
87 Vector residual;
89 std::vector<Complex> residual_coupling;
90 std::vector<Complex> eigenvalues;
92 bool happy_breakdown = false;
93};
94
95namespace detail
96{
97#if defined(__cpp_lib_constexpr_cmath)
98template <uni20::Real Scalar> constexpr Scalar nonsymmetric_convergence_floor()
99#else
100template <uni20::Real Scalar> Scalar nonsymmetric_convergence_floor()
101#endif
102{
104}
105
106template <uni20::Real Scalar>
107bool nonsymmetric_ritz_converged(uni20::complex<Scalar> value, Scalar residual_bound, Scalar tolerance)
108{
109 Scalar const eps23 = nonsymmetric_convergence_floor<Scalar>();
110 return residual_bound <= tolerance * std::max(eps23, uni20::math::abs(value));
111}
112
113template <uni20::Real Scalar> Scalar arnoldi_breakdown_threshold(Scalar tolerance, Scalar local_scale)
114{
115 Scalar const base_tolerance =
116 tolerance > Scalar{} ? tolerance : Scalar{10} * uni20::numeric_limits<Scalar>::epsilon();
117 return base_tolerance * std::max(Scalar{1}, local_scale);
118}
119
120template <uni20::RealOrComplex Scalar>
121uni20::make_real_t<Scalar> arnoldi_hessenberg_column_scale(uni20::DenseMatrix<Scalar> const& hessenberg,
122 std::size_t column, std::size_t coefficient_count,
123 uni20::make_real_t<Scalar> residual_norm)
124{
126 Real scale = std::max(Real{1}, residual_norm);
127 for (std::size_t row = 0; row < coefficient_count; ++row)
128 {
129 scale = std::max(scale, static_cast<Real>(uni20::math::abs(hessenberg[row, column])));
130 }
131 return scale;
132}
133
134template <uni20::RealOrComplex Scalar>
135uni20::make_real_t<Scalar> arnoldi_compressed_relation_scale(uni20::DenseMatrix<Scalar> const& schur_form,
136 std::vector<Scalar> const& residual_coupling,
137 uni20::make_real_t<Scalar> residual_norm)
138{
140 Real scale = std::max(Real{1}, residual_norm);
141 for (uni20::index_type col = 0; col < schur_form.cols(); ++col)
142 {
143 for (uni20::index_type row = 0; row < schur_form.rows(); ++row)
144 {
145 scale = std::max(scale, static_cast<Real>(uni20::math::abs(schur_form[row, col])));
146 }
147 }
148 for (Scalar const& coupling : residual_coupling)
149 {
150 scale = std::max(scale, static_cast<Real>(residual_norm * uni20::math::abs(coupling)));
151 }
152 return scale;
153}
154
155template <uni20::RealOrComplex Scalar, typename Vector, typename Ops>
156uni20::make_real_t<Scalar> orthogonalize_arnoldi_residual(Ops& ops, std::vector<Vector> const& basis,
157 uni20::DenseMatrix<Scalar>& hessenberg, std::size_t column,
158 Vector& residual)
159{
161 auto norm = [&]() { return norm_or_inner_product<Scalar>(ops, residual); };
162 auto orthogonalize_once = [&]() {
163 Real max_correction{};
164 for (std::size_t row = 0; row < basis.size(); ++row)
165 {
166 Scalar const coefficient = ops.inner_product(basis[row], residual);
167 hessenberg[row, column] += coefficient;
168 max_correction = std::max(max_correction, static_cast<Real>(uni20::math::abs(coefficient)));
169 ops.axpy(residual, -coefficient, basis[row]);
170 }
171 return max_correction;
172 };
173
174 Real const original_norm = norm();
175 (void)orthogonalize_once();
176 Real residual_norm = norm();
177 if (original_norm == Real{} || residual_norm > Real{0.717} * original_norm)
178 {
179 return residual_norm;
180 }
181
182 Real const first_refined_norm = residual_norm;
183 (void)orthogonalize_once();
184 residual_norm = norm();
185 if (first_refined_norm == Real{} || residual_norm <= Real{0.717} * first_refined_norm)
186 {
187 ops.set_zero(residual);
188 return Real{};
189 }
190 return residual_norm;
191}
192
193template <typename Compare> void sort_indices(std::vector<std::size_t>& indices, Compare&& compare)
194{
195 std::ranges::sort(indices, [&](std::size_t lhs, std::size_t rhs) {
196 if (compare(lhs, rhs))
197 {
198 return true;
199 }
200 if (compare(rhs, lhs))
201 {
202 return false;
203 }
204 return lhs < rhs;
205 });
206}
207
208template <uni20::LapackReal Scalar> Scalar schur_block_score(RealSchurBlock<Scalar> const& block, SpectrumPart spectrum)
209{
210 auto const first = block.first_eigenvalue;
211 auto const second = block.size == 2 ? block.second_eigenvalue : block.first_eigenvalue;
212 switch (spectrum)
213 {
215 return std::max(uni20::math::abs(first), uni20::math::abs(second));
217 return std::min(uni20::math::abs(first), uni20::math::abs(second));
219 return std::max(first.real(), second.real());
221 return std::min(first.real(), second.real());
223 return std::max(first.imag(), second.imag());
225 return std::min(first.imag(), second.imag());
229 throw std::invalid_argument("algebraic and both-end selectors are not valid for nonsymmetric Schur restart");
230 }
231 return Scalar{};
232}
233
234template <uni20::LapackReal Scalar> bool larger_is_better(SpectrumPart spectrum)
235{
236 switch (spectrum)
237 {
241 return true;
245 return false;
249 throw std::invalid_argument("algebraic and both-end selectors are not valid for nonsymmetric Schur restart");
250 }
251 return true;
252}
253
254template <uni20::Real Scalar, typename Vector, KrylovMatrixFreeOperator<Vector, Scalar> Ops>
255NonsymmetricEigenResult<Scalar, Vector> make_real_nonsymmetric_arnoldi_result(
256 Ops& ops, ArnoldiFactorization<Scalar, Vector> const& factorization, ArnoldiRitzExtraction<Scalar> const& ritz,
257 std::vector<std::size_t> const& selected, NonsymmetricEigenParams<Scalar> const& params, int iteration_count,
258 int matvec_count, int restart_count, Scalar projected_departure)
259{
260 Scalar const tolerance =
261 params.tolerance > Scalar{} ? params.tolerance : Scalar{100} * uni20::numeric_limits<Scalar>::epsilon();
262
263 NonsymmetricEigenResult<Scalar, Vector> result;
264 result.iteration_count = iteration_count;
265 result.matvec_count = matvec_count;
266 result.eigenvalues.reserve(selected.size());
267 result.residual_bounds.reserve(selected.size());
268 result.reality.reserve(selected.size());
269
270 bool const can_compute_right_eigenvectors =
271 params.compute_eigenvectors &&
272 std::ranges::all_of(selected, [&](std::size_t index) { return ritz.reality[index] == RitzReality::Real; });
273 result.right_eigenvectors.reserve(can_compute_right_eigenvectors ? selected.size() : 0);
274
275 bool saw_complex = false;
276 bool saw_ambiguous = false;
277 for (std::size_t const index : selected)
278 {
279 uni20::complex<Scalar> const value = ritz.ritz_values[index];
280 Scalar const residual_bound = ritz.residual_bounds[index];
281 RitzReality const reality = ritz.reality[index];
282
283 result.eigenvalues.push_back(value);
284 result.residual_bounds.push_back(residual_bound);
285 result.reality.push_back(reality);
286 if (nonsymmetric_ritz_converged(value, residual_bound, tolerance))
287 {
288 ++result.converged_count;
289 }
290
291 saw_complex = saw_complex || reality == RitzReality::Complex;
292 saw_ambiguous = saw_ambiguous || reality == RitzReality::Ambiguous;
293 if (can_compute_right_eigenvectors)
294 {
295 Vector vector = ops.allocate_like(factorization.basis.front());
296 ops.set_zero(vector);
297 for (std::size_t row = 0; row < static_cast<std::size_t>(factorization.step_count); ++row)
298 {
299 ops.axpy(vector, ritz.projected_right_eigenvectors[row, index].real(), factorization.basis[row]);
300 }
301 result.right_eigenvectors.push_back(std::move(vector));
302 }
303 }
304
305 if (params.diagnostics != KrylovDiagnosticsLevel::None)
306 {
307 NonsymmetricArnoldiDiagnostics<Scalar> diagnostics;
308 diagnostics.restart_count = restart_count;
309 diagnostics.op_count = matvec_count;
310 diagnostics.final_projected_dimension = factorization.step_count;
311 diagnostics.final_residual_norm = factorization.residual_norm;
312 diagnostics.projected_departure_from_normality = projected_departure;
313 diagnostics.final_ritz_values = ritz.ritz_values;
314 diagnostics.final_ritz_bounds = ritz.residual_bounds;
315 diagnostics.final_ritz_reality = ritz.reality;
316 result.diagnostics = std::move(diagnostics);
317 }
318
319 if (saw_ambiguous)
320 {
322 }
323 else if (saw_complex)
324 {
325 switch (params.real_policy)
326 {
329 break;
332 break;
335 break;
336 }
337 }
338 else
339 {
340 result.status = result.converged_count == params.eigenvalue_count ? NonsymmetricStatus::Converged
342 }
343 if (result.eigenvalues.size() < static_cast<std::size_t>(params.eigenvalue_count))
344 {
345 result.status = factorization.happy_breakdown ? NonsymmetricStatus::Breakdown : NonsymmetricStatus::NotConverged;
346 }
347
348 return result;
349}
350
351template <uni20::LapackComplexReal Real, typename Vector, KrylovMatrixFreeOperator<Vector, uni20::complex<Real>> Ops>
352NonsymmetricEigenResult<Real, Vector> make_complex_nonsymmetric_arnoldi_result(
353 Ops& ops, ArnoldiFactorization<uni20::complex<Real>, Vector> const& factorization,
354 ArnoldiRitzExtraction<Real> const& ritz, std::vector<std::size_t> const& selected,
355 NonsymmetricEigenParams<Real> const& params, int iteration_count, int matvec_count, int restart_count,
356 Real projected_departure)
357{
359
360 Real const tolerance =
361 params.tolerance > Real{} ? params.tolerance : Real{100} * uni20::numeric_limits<Real>::epsilon();
362
363 NonsymmetricEigenResult<Real, Vector> result;
364 result.iteration_count = iteration_count;
365 result.matvec_count = matvec_count;
366 result.eigenvalues.reserve(selected.size());
367 result.residual_bounds.reserve(selected.size());
368 result.reality.reserve(selected.size());
369 result.right_eigenvectors.reserve(params.compute_eigenvectors ? selected.size() : 0);
370
371 for (std::size_t const index : selected)
372 {
373 Complex const value = ritz.ritz_values[index];
374 Real const residual_bound = ritz.residual_bounds[index];
375
376 result.eigenvalues.push_back(value);
377 result.residual_bounds.push_back(residual_bound);
378 result.reality.push_back(classify_ritz_reality(value, params.complex_pair_tolerance));
379 if (nonsymmetric_ritz_converged(value, residual_bound, tolerance))
380 {
381 ++result.converged_count;
382 }
383
384 if (params.compute_eigenvectors)
385 {
386 Vector vector = ops.allocate_like(factorization.basis.front());
387 ops.set_zero(vector);
388 for (std::size_t row = 0; row < static_cast<std::size_t>(factorization.step_count); ++row)
389 {
390 ops.axpy(vector, ritz.projected_right_eigenvectors[row, index], factorization.basis[row]);
391 }
392 result.right_eigenvectors.push_back(std::move(vector));
393 }
394 }
395
396 if (params.diagnostics != KrylovDiagnosticsLevel::None)
397 {
398 NonsymmetricArnoldiDiagnostics<Real> diagnostics;
399 diagnostics.restart_count = restart_count;
400 diagnostics.op_count = matvec_count;
401 diagnostics.final_projected_dimension = factorization.step_count;
402 diagnostics.final_residual_norm = factorization.residual_norm;
403 diagnostics.projected_departure_from_normality = projected_departure;
404 diagnostics.final_ritz_values = ritz.ritz_values;
405 diagnostics.final_ritz_bounds = ritz.residual_bounds;
406 diagnostics.final_ritz_reality = ritz.reality;
407 result.diagnostics = std::move(diagnostics);
408 }
409
410 result.status = result.converged_count == params.eigenvalue_count ? NonsymmetricStatus::Converged
412 if (result.eigenvalues.size() < static_cast<std::size_t>(params.eigenvalue_count))
413 {
414 result.status = factorization.happy_breakdown ? NonsymmetricStatus::Breakdown : NonsymmetricStatus::NotConverged;
415 }
416 return result;
417}
418} // namespace detail
419
430template <uni20::RealOrComplex Scalar>
432{
433 if (matrix.rows() != matrix.cols())
434 {
435 throw std::invalid_argument("projected_departure_from_normality requires a square matrix");
436 }
437
439
440 std::size_t const order = static_cast<std::size_t>(matrix.rows());
441 Real norm_squared{};
442 for (std::size_t col = 0; col < order; ++col)
443 {
444 for (std::size_t row = 0; row < order; ++row)
445 {
446 Scalar const value = matrix[row, col];
447 norm_squared += uni20::math::abs_squared(value);
448 }
449 }
450
451 Real commutator_norm_squared{};
452 for (std::size_t col = 0; col < order; ++col)
453 {
454 for (std::size_t row = 0; row < order; ++row)
455 {
456 Scalar hth{};
457 Scalar hht{};
458 for (std::size_t inner = 0; inner < order; ++inner)
459 {
460 hth += detail::conjugate_if_complex(matrix[inner, row]) * matrix[inner, col];
461 hht += matrix[row, inner] * detail::conjugate_if_complex(matrix[col, inner]);
462 }
463 Scalar const difference = hth - hht;
464 commutator_norm_squared += uni20::math::abs_squared(difference);
465 }
466 }
467
468 Real const scale = std::max(Real{1}, norm_squared);
469 return uni20::math::sqrt(commutator_norm_squared) / scale;
470}
471
487template <uni20::RealOrComplex Scalar, typename Vector, KrylovMatrixFreeOperator<Vector, Scalar> Ops>
488ArnoldiFactorization<Scalar, Vector>
489arnoldi_factorize(Ops& ops, Vector const& initial, int max_steps,
491{
492 validate_matrix_free_dimensions(ops, initial, "arnoldi_factorize");
493 if (max_steps <= 0)
494 {
495 throw std::invalid_argument("arnoldi_factorize requires a positive step count");
496 }
497
499
500 Vector q0 = ops.allocate_like(initial);
501 ops.copy(q0, initial);
502 Real const initial_norm = norm_or_inner_product<Scalar>(ops, q0);
503 if (initial_norm == Real{})
504 {
505 throw std::invalid_argument("arnoldi_factorize received a zero initial vector");
506 }
507 ops.scal(q0, Scalar{1} / static_cast<Scalar>(initial_norm));
508
509 ArnoldiFactorization<Scalar, Vector> factorization;
510 factorization.hessenberg =
511 uni20::DenseMatrix<Scalar>(static_cast<std::size_t>(max_steps) + 1, static_cast<std::size_t>(max_steps));
512 laset(factorization.hessenberg, Scalar{}, Scalar{}, MatrixFill::All);
513 factorization.basis.push_back(std::move(q0));
514
515 for (int step = 0; step < max_steps; ++step)
516 {
517 Vector residual = ops.allocate_like(initial);
518 ops.matvec(residual, factorization.basis[static_cast<std::size_t>(step)]);
519 ++factorization.op_count;
520
521 Real const beta = detail::orthogonalize_arnoldi_residual<Scalar>(ops, factorization.basis, factorization.hessenberg,
522 static_cast<std::size_t>(step), residual);
523 factorization.residual_norm = beta;
524 factorization.step_count = step + 1;
525 Real const breakdown_scale = detail::arnoldi_hessenberg_column_scale(
526 factorization.hessenberg, static_cast<std::size_t>(step), static_cast<std::size_t>(step + 1), beta);
527 if (beta <= detail::arnoldi_breakdown_threshold(breakdown_tolerance, breakdown_scale))
528 {
529 factorization.happy_breakdown = true;
530 break;
531 }
532
533 factorization.hessenberg[static_cast<std::size_t>(step + 1), static_cast<std::size_t>(step)] =
534 static_cast<Scalar>(beta);
535 ops.scal(residual, Scalar{1} / static_cast<Scalar>(beta));
536 factorization.basis.push_back(std::move(residual));
537 }
538
539 return factorization;
540}
541
547template <uni20::RealOrComplex Scalar, typename Vector>
549{
550 if (factorization.step_count <= 0)
551 {
552 throw std::invalid_argument("arnoldi_projected_hessenberg requires a non-empty Arnoldi factorization");
553 }
554
555 std::size_t const projected_dimension = static_cast<std::size_t>(factorization.step_count);
556 auto const projected_extent = static_cast<uni20::index_type>(projected_dimension);
557 if (factorization.hessenberg.rows() < projected_extent || factorization.hessenberg.cols() < projected_extent)
558 {
559 throw std::invalid_argument("arnoldi_projected_hessenberg received an inconsistent Hessenberg projection");
560 }
561
562 uni20::DenseMatrix<Scalar> projected(projected_dimension, projected_dimension);
563 for (std::size_t col = 0; col < projected_dimension; ++col)
564 {
565 for (std::size_t row = 0; row < projected_dimension; ++row)
566 {
567 projected[row, col] = factorization.hessenberg[row, col];
568 }
569 }
570 return projected;
571}
572
586template <uni20::LapackReal Scalar, typename Vector>
588 Scalar complex_pair_tolerance = Scalar{}, Scalar scale = Scalar{1})
589{
590 if (factorization.step_count <= 0)
591 {
592 throw std::invalid_argument("extract_arnoldi_ritz requires a non-empty Arnoldi factorization");
593 }
594
595 std::size_t const projected_dimension = static_cast<std::size_t>(factorization.step_count);
596 auto const projected_extent = static_cast<uni20::index_type>(projected_dimension);
597 if (factorization.hessenberg.rows() < projected_extent || factorization.hessenberg.cols() < projected_extent)
598 {
599 throw std::invalid_argument("extract_arnoldi_ritz received an inconsistent Hessenberg projection");
600 }
601
603 Scalar hessenberg_scale = Scalar{1};
604 for (std::size_t col = 0; col < projected_dimension; ++col)
605 {
606 for (std::size_t row = 0; row < projected_dimension; ++row)
607 {
608 Scalar const value = factorization.hessenberg[row, col];
609 hessenberg_scale = std::max(hessenberg_scale, uni20::math::abs(value));
610 }
611 }
612
613 auto eigensystem = real_nonsymmetric_eigensystem(std::move(projected), true);
614
615 ArnoldiRitzExtraction<Scalar> result;
616 result.ritz_values = std::move(eigensystem.eigenvalues);
617 result.projected_right_eigenvectors = std::move(eigensystem.right_eigenvectors);
618 result.residual_bounds.resize(projected_dimension, Scalar{});
619 result.reality.resize(projected_dimension, RitzReality::Real);
620
621 Scalar const beta = factorization.happy_breakdown
622 ? Scalar{}
623 : uni20::math::abs(factorization.hessenberg[projected_dimension, projected_dimension - 1]);
624 Scalar const classification_scale = std::max(scale, hessenberg_scale);
625 for (std::size_t col = 0; col < projected_dimension; ++col)
626 {
627 Scalar norm_squared = Scalar{};
628 for (std::size_t row = 0; row < projected_dimension; ++row)
629 {
630 norm_squared += uni20::math::abs_squared(result.projected_right_eigenvectors[row, col]);
631 }
632 Scalar const vector_norm = uni20::math::sqrt(norm_squared);
633 if (vector_norm == Scalar{})
634 {
635 throw std::runtime_error("extract_arnoldi_ritz received a zero projected eigenvector from LAPACK");
636 }
637 Scalar const last_component = uni20::math::abs(result.projected_right_eigenvectors[projected_dimension - 1, col]);
638 result.residual_bounds[col] = beta * last_component / vector_norm;
639 result.reality[col] = classify_ritz_reality(result.ritz_values[col], complex_pair_tolerance, classification_scale);
640 }
641
642 return result;
643}
644
657template <uni20::LapackComplex Complex, typename Vector>
658ArnoldiRitzExtraction<uni20::make_real_t<Complex>>
662{
663 if (factorization.step_count <= 0)
664 {
665 throw std::invalid_argument("extract_complex_arnoldi_ritz requires a non-empty Arnoldi factorization");
666 }
667
669
670 std::size_t const projected_dimension = static_cast<std::size_t>(factorization.step_count);
671 auto const projected_extent = static_cast<uni20::index_type>(projected_dimension);
672 if (factorization.hessenberg.rows() < projected_extent || factorization.hessenberg.cols() < projected_extent)
673 {
674 throw std::invalid_argument("extract_complex_arnoldi_ritz received an inconsistent Hessenberg projection");
675 }
676
678 Real hessenberg_scale = Real{1};
679 for (std::size_t col = 0; col < projected_dimension; ++col)
680 {
681 for (std::size_t row = 0; row < projected_dimension; ++row)
682 {
683 hessenberg_scale = std::max(hessenberg_scale, uni20::math::abs(factorization.hessenberg[row, col]));
684 }
685 }
686
687 auto eigensystem = complex_nonsymmetric_eigensystem<Real>(std::move(projected), true);
688
689 ArnoldiRitzExtraction<Real> result;
690 result.ritz_values = std::move(eigensystem.eigenvalues);
691 result.projected_right_eigenvectors = std::move(eigensystem.right_eigenvectors);
692 result.residual_bounds.resize(projected_dimension, Real{});
693 result.reality.resize(projected_dimension, RitzReality::Real);
694
695 Real const beta = factorization.happy_breakdown
696 ? Real{}
697 : uni20::math::abs(factorization.hessenberg[projected_dimension, projected_dimension - 1]);
698 Real const classification_scale = std::max(scale, hessenberg_scale);
699 for (std::size_t col = 0; col < projected_dimension; ++col)
700 {
701 Real norm_squared = Real{};
702 for (std::size_t row = 0; row < projected_dimension; ++row)
703 {
704 norm_squared += uni20::math::abs_squared(result.projected_right_eigenvectors[row, col]);
705 }
706 Real const vector_norm = uni20::math::sqrt(norm_squared);
707 if (vector_norm == Real{})
708 {
709 throw std::runtime_error("extract_complex_arnoldi_ritz received a zero projected eigenvector from LAPACK");
710 }
711 Real const last_component = uni20::math::abs(result.projected_right_eigenvectors[projected_dimension - 1, col]);
712 result.residual_bounds[col] = beta * last_component / vector_norm;
713 result.reality[col] = classify_ritz_reality(result.ritz_values[col], complex_pair_tolerance, classification_scale);
714 }
715
716 return result;
717}
718
730template <uni20::Real Scalar>
731std::vector<std::size_t> select_nonsymmetric_ritz_indices(std::vector<uni20::complex<Scalar>> const& values,
733{
734 if (params.eigenvalue_count <= 0)
735 {
736 throw std::invalid_argument("nonsymmetric Ritz selection requires a positive eigenvalue count");
737 }
738 if (static_cast<std::size_t>(params.eigenvalue_count) > values.size())
739 {
740 throw std::invalid_argument("nonsymmetric Ritz selection requires eigenvalue_count <= projected size");
741 }
742
743 std::vector<std::size_t> indices(values.size());
744 std::iota(indices.begin(), indices.end(), std::size_t{0});
745
746 switch (params.spectrum)
747 {
749 detail::sort_indices(indices, [&](std::size_t lhs, std::size_t rhs) {
750 return uni20::math::abs(values[lhs]) > uni20::math::abs(values[rhs]);
751 });
752 break;
754 detail::sort_indices(indices, [&](std::size_t lhs, std::size_t rhs) {
755 return uni20::math::abs(values[lhs]) < uni20::math::abs(values[rhs]);
756 });
757 break;
759 detail::sort_indices(indices,
760 [&](std::size_t lhs, std::size_t rhs) { return values[lhs].real() > values[rhs].real(); });
761 break;
763 detail::sort_indices(indices,
764 [&](std::size_t lhs, std::size_t rhs) { return values[lhs].real() < values[rhs].real(); });
765 break;
767 detail::sort_indices(indices,
768 [&](std::size_t lhs, std::size_t rhs) { return values[lhs].imag() > values[rhs].imag(); });
769 break;
771 detail::sort_indices(indices,
772 [&](std::size_t lhs, std::size_t rhs) { return values[lhs].imag() < values[rhs].imag(); });
773 break;
777 throw std::invalid_argument("algebraic and both-end selectors are not valid for nonsymmetric Arnoldi");
778 }
779
780 indices.resize(static_cast<std::size_t>(params.eigenvalue_count));
781 return indices;
782}
783
784namespace detail
785{
786template <uni20::Complex Scalar> std::vector<std::size_t> all_ritz_indices(std::vector<Scalar> const& values)
787{
788 std::vector<std::size_t> indices(values.size());
789 std::iota(indices.begin(), indices.end(), std::size_t{0});
790 return indices;
791}
792} // namespace detail
793
799template <uni20::Real Scalar>
800std::vector<std::size_t> select_nonsymmetric_restart_indices(std::vector<uni20::complex<Scalar>> const& values,
802{
803 NonsymmetricEigenParams<Scalar> restart_params = params;
804 restart_params.eigenvalue_count = effective_nonsymmetric_retained_ritz_count(params, static_cast<int>(values.size()));
805 return select_nonsymmetric_ritz_indices(values, restart_params);
806}
807
819template <uni20::LapackReal Scalar>
822{
823 if (params.eigenvalue_count <= 0)
824 {
825 throw std::invalid_argument("nonsymmetric Schur restart requires a positive eigenvalue count");
826 }
827 std::size_t total_scalar_count = 0;
828 for (auto const& block : blocks)
829 {
830 if (block.size != 1 && block.size != 2)
831 {
832 throw std::invalid_argument("nonsymmetric Schur restart received an invalid block size");
833 }
834 total_scalar_count += block.size;
835 }
836 int const requested_count = effective_nonsymmetric_retained_ritz_count(params, static_cast<int>(total_scalar_count));
837
838 std::vector<std::size_t> indices(blocks.size());
839 std::iota(indices.begin(), indices.end(), std::size_t{0});
840 bool const descending = detail::larger_is_better<Scalar>(params.spectrum);
841 detail::sort_indices(indices, [&](std::size_t lhs, std::size_t rhs) {
842 Scalar const lhs_score = detail::schur_block_score(blocks[lhs], params.spectrum);
843 Scalar const rhs_score = detail::schur_block_score(blocks[rhs], params.spectrum);
844 return descending ? lhs_score > rhs_score : lhs_score < rhs_score;
845 });
846
848 for (std::size_t const index : indices)
849 {
850 if (selection.scalar_count >= static_cast<std::size_t>(requested_count))
851 {
852 break;
853 }
854 selection.block_indices.push_back(index);
855 selection.scalar_count += blocks[index].size;
856 }
857 selection.enlarged_to_preserve_block = selection.scalar_count > static_cast<std::size_t>(requested_count);
858 return selection;
859}
860
874template <uni20::LapackReal Scalar, typename Vector, KrylovMatrixFreeOperator<Vector, Scalar> Ops>
875RealSchurCompressedArnoldiFactorization<Scalar, Vector>
878{
879 if (factorization.step_count <= 0)
880 {
881 throw std::invalid_argument("real Schur Arnoldi compression requires a non-empty factorization");
882 }
883
884 std::size_t const order = static_cast<std::size_t>(factorization.step_count);
885 if (factorization.basis.size() < order || (!factorization.happy_breakdown && factorization.basis.size() <= order))
886 {
887 throw std::invalid_argument("real Schur Arnoldi compression received an inconsistent basis");
888 }
889
891 auto schur = real_schur(std::move(projected), true);
893 if (selection.scalar_count == 0 || selection.scalar_count > order)
894 {
895 throw std::invalid_argument("real Schur Arnoldi compression selected an invalid retained dimension");
896 }
897 schur = reorder_real_schur(std::move(schur), selection.block_indices);
898
899 std::size_t const retained_count = selection.scalar_count;
900 auto transformed_column = [&](std::size_t column) {
901 Vector vector = ops.allocate_like(factorization.basis.front());
902 ops.set_zero(vector);
903 for (std::size_t row = 0; row < order; ++row)
904 {
905 Scalar const coefficient = schur.schur_vectors[row, column];
906 if (coefficient != Scalar{})
907 {
908 ops.axpy(vector, coefficient, factorization.basis[row]);
909 }
910 }
911 return vector;
912 };
913
915 .basis = {},
916 .residual = ops.allocate_like(factorization.basis.front()),
917 .schur_form = uni20::DenseMatrix<Scalar>(retained_count, retained_count),
918 .residual_coupling = std::vector<Scalar>(retained_count, Scalar{}),
919 .eigenvalues = {},
920 .blocks = {},
921 .residual_norm = Scalar{},
922 .happy_breakdown = factorization.happy_breakdown,
923 .enlarged_to_preserve_block = selection.enlarged_to_preserve_block};
924
925 result.basis.reserve(retained_count);
926 for (std::size_t column = 0; column < retained_count; ++column)
927 {
928 result.basis.push_back(transformed_column(column));
929 }
930
931 for (std::size_t col = 0; col < retained_count; ++col)
932 {
933 for (std::size_t row = 0; row < retained_count; ++row)
934 {
935 result.schur_form[row, col] = schur.schur_form[row, col];
936 }
937 }
938
939 result.eigenvalues.assign(schur.eigenvalues.begin(),
940 schur.eigenvalues.begin() + static_cast<std::ptrdiff_t>(retained_count));
941 for (auto const& block : schur.blocks)
942 {
943 if (block.begin >= retained_count)
944 {
945 break;
946 }
947 if (block.begin + block.size > retained_count)
948 {
949 throw std::logic_error("real Schur Arnoldi compression split a Schur block");
950 }
951 result.blocks.push_back(block);
952 }
953
954 ops.set_zero(result.residual);
955 if (!factorization.happy_breakdown)
956 {
957 Scalar const beta = factorization.hessenberg[order, order - 1];
958 ops.copy(result.residual, factorization.basis[order]);
959 ops.scal(result.residual, beta);
960 for (std::size_t col = 0; col < retained_count; ++col)
961 {
962 result.residual_coupling[col] = schur.schur_vectors[order - 1, col];
963 }
964 }
965 result.residual_norm = norm_or_inner_product<Scalar>(ops, result.residual);
966 return result;
967}
968
986template <uni20::LapackReal Scalar, typename Vector, KrylovMatrixFreeOperator<Vector, Scalar> Ops>
987ArnoldiFactorization<Scalar, Vector>
989 std::size_t target_step_count, Scalar breakdown_tolerance = Scalar{})
990{
991 std::size_t const retained_count = compressed.basis.size();
992 if (retained_count == 0)
993 {
994 throw std::invalid_argument("real Schur Arnoldi expansion requires a nonempty retained basis");
995 }
996 if (target_step_count < retained_count)
997 {
998 throw std::invalid_argument("real Schur Arnoldi expansion target is smaller than retained basis");
999 }
1000 auto const retained_extent = static_cast<uni20::index_type>(retained_count);
1001 if (compressed.schur_form.rows() != retained_extent || compressed.schur_form.cols() != retained_extent ||
1002 compressed.residual_coupling.size() != retained_count)
1003 {
1004 throw std::invalid_argument("real Schur Arnoldi expansion received inconsistent compressed dimensions");
1005 }
1006 std::size_t const problem_dimension =
1007 validate_matrix_free_dimensions(ops, compressed.basis.front(), "real Schur Arnoldi expansion");
1008 if (target_step_count > problem_dimension)
1009 {
1010 throw std::invalid_argument("real Schur Arnoldi expansion target exceeds problem dimension");
1011 }
1012
1013 ArnoldiFactorization<Scalar, Vector> factorization;
1014 factorization.hessenberg = uni20::DenseMatrix<Scalar>(target_step_count + 1, target_step_count);
1015 laset(factorization.hessenberg, Scalar{}, Scalar{}, MatrixFill::All);
1016 factorization.basis.reserve(target_step_count + 1);
1017 for (auto const& vector : compressed.basis)
1018 {
1019 Vector copy = ops.allocate_like(vector);
1020 ops.copy(copy, vector);
1021 factorization.basis.push_back(std::move(copy));
1022 }
1023
1024 for (std::size_t col = 0; col < retained_count; ++col)
1025 {
1026 for (std::size_t row = 0; row < retained_count; ++row)
1027 {
1028 factorization.hessenberg[row, col] = compressed.schur_form[row, col];
1029 }
1030 }
1031
1032 factorization.step_count = static_cast<int>(retained_count);
1033 factorization.residual_norm = compressed.residual_norm;
1034 Scalar const compressed_breakdown_scale = detail::arnoldi_compressed_relation_scale(
1035 compressed.schur_form, compressed.residual_coupling, compressed.residual_norm);
1036 if (compressed.residual_norm <= detail::arnoldi_breakdown_threshold(breakdown_tolerance, compressed_breakdown_scale))
1037 {
1038 factorization.happy_breakdown =
1039 compressed.happy_breakdown || compressed.residual_norm <= detail::arnoldi_breakdown_threshold(
1040 breakdown_tolerance, compressed_breakdown_scale);
1041 return factorization;
1042 }
1043
1044 Vector next_vector = ops.allocate_like(compressed.residual);
1045 ops.copy(next_vector, compressed.residual);
1046 ops.scal(next_vector, Scalar{1} / compressed.residual_norm);
1047 factorization.basis.push_back(std::move(next_vector));
1048 for (std::size_t col = 0; col < retained_count; ++col)
1049 {
1050 factorization.hessenberg[retained_count, col] = compressed.residual_norm * compressed.residual_coupling[col];
1051 }
1052 if (target_step_count == retained_count)
1053 {
1054 return factorization;
1055 }
1056
1057 for (std::size_t step = retained_count; step < target_step_count; ++step)
1058 {
1059 Vector residual = ops.allocate_like(factorization.basis[step]);
1060 ops.matvec(residual, factorization.basis[step]);
1061 ++factorization.op_count;
1062
1063 Scalar const beta = detail::orthogonalize_arnoldi_residual<Scalar>(ops, factorization.basis,
1064 factorization.hessenberg, step, residual);
1065 factorization.residual_norm = beta;
1066 factorization.step_count = static_cast<int>(step + 1);
1067 Scalar const breakdown_scale =
1068 detail::arnoldi_hessenberg_column_scale(factorization.hessenberg, step, step + 1, beta);
1069 if (beta <= detail::arnoldi_breakdown_threshold(breakdown_tolerance, breakdown_scale))
1070 {
1071 factorization.happy_breakdown = true;
1072 break;
1073 }
1074
1075 factorization.hessenberg[step + 1, step] = beta;
1076 ops.scal(residual, Scalar{1} / beta);
1077 factorization.basis.push_back(std::move(residual));
1078 }
1079
1080 return factorization;
1081}
1082
1097template <uni20::LapackComplexReal Real, typename Vector, KrylovMatrixFreeOperator<Vector, uni20::complex<Real>> Ops>
1098ComplexSchurCompressedArnoldiFactorization<Real, Vector>
1100 ArnoldiFactorization<uni20::complex<Real>, Vector> const& factorization,
1101 NonsymmetricEigenParams<Real> const& params)
1102{
1103 if (factorization.step_count <= 0)
1104 {
1105 throw std::invalid_argument("complex Schur Arnoldi compression requires a non-empty factorization");
1106 }
1107
1109
1110 std::size_t const order = static_cast<std::size_t>(factorization.step_count);
1111 if (factorization.basis.size() < order || (!factorization.happy_breakdown && factorization.basis.size() <= order))
1112 {
1113 throw std::invalid_argument("complex Schur Arnoldi compression received an inconsistent basis");
1114 }
1115
1117 auto schur = complex_schur<Real>(std::move(projected), true);
1118 std::vector<std::size_t> const retained_indices = select_nonsymmetric_restart_indices(schur.eigenvalues, params);
1119 if (retained_indices.empty() || retained_indices.size() > order)
1120 {
1121 throw std::invalid_argument("complex Schur Arnoldi compression selected an invalid retained dimension");
1122 }
1123 schur = reorder_complex_schur<Real>(std::move(schur), retained_indices);
1124
1125 std::size_t const retained_count = retained_indices.size();
1126 auto transformed_column = [&](std::size_t column) {
1127 Vector vector = ops.allocate_like(factorization.basis.front());
1128 ops.set_zero(vector);
1129 for (std::size_t row = 0; row < order; ++row)
1130 {
1131 Complex const coefficient = schur.schur_vectors[row, column];
1132 if (coefficient != Complex{})
1133 {
1134 ops.axpy(vector, coefficient, factorization.basis[row]);
1135 }
1136 }
1137 return vector;
1138 };
1139
1141 .basis = {},
1142 .residual = ops.allocate_like(factorization.basis.front()),
1143 .schur_form = uni20::DenseMatrix<Complex>(retained_count, retained_count),
1144 .residual_coupling = std::vector<Complex>(retained_count, Complex{}),
1145 .eigenvalues = {},
1146 .residual_norm = Real{},
1147 .happy_breakdown = factorization.happy_breakdown};
1148
1149 result.basis.reserve(retained_count);
1150 for (std::size_t column = 0; column < retained_count; ++column)
1151 {
1152 result.basis.push_back(transformed_column(column));
1153 }
1154
1155 for (std::size_t col = 0; col < retained_count; ++col)
1156 {
1157 for (std::size_t row = 0; row < retained_count; ++row)
1158 {
1159 result.schur_form[row, col] = schur.schur_form[row, col];
1160 }
1161 }
1162
1163 result.eigenvalues.assign(schur.eigenvalues.begin(),
1164 schur.eigenvalues.begin() + static_cast<std::ptrdiff_t>(retained_count));
1165
1166 ops.set_zero(result.residual);
1167 if (!factorization.happy_breakdown)
1168 {
1169 Complex const beta = factorization.hessenberg[order, order - 1];
1170 ops.copy(result.residual, factorization.basis[order]);
1171 ops.scal(result.residual, beta);
1172 for (std::size_t col = 0; col < retained_count; ++col)
1173 {
1174 result.residual_coupling[col] = schur.schur_vectors[order - 1, col];
1175 }
1176 }
1177 result.residual_norm = norm_or_inner_product<Complex>(ops, result.residual);
1178 return result;
1179}
1180
1197template <uni20::LapackComplexReal Real, typename Vector, KrylovMatrixFreeOperator<Vector, uni20::complex<Real>> Ops>
1198ArnoldiFactorization<uni20::complex<Real>, Vector>
1201 std::size_t target_step_count, Real breakdown_tolerance = Real{})
1202{
1204
1205 std::size_t const retained_count = compressed.basis.size();
1206 if (retained_count == 0)
1207 {
1208 throw std::invalid_argument("complex Schur Arnoldi expansion requires a nonempty retained basis");
1209 }
1210 if (target_step_count < retained_count)
1211 {
1212 throw std::invalid_argument("complex Schur Arnoldi expansion target is smaller than retained basis");
1213 }
1214 auto const retained_extent = static_cast<uni20::index_type>(retained_count);
1215 if (compressed.schur_form.rows() != retained_extent || compressed.schur_form.cols() != retained_extent ||
1216 compressed.residual_coupling.size() != retained_count)
1217 {
1218 throw std::invalid_argument("complex Schur Arnoldi expansion received inconsistent compressed dimensions");
1219 }
1220 std::size_t const problem_dimension =
1221 validate_matrix_free_dimensions(ops, compressed.basis.front(), "complex Schur Arnoldi expansion");
1222 if (target_step_count > problem_dimension)
1223 {
1224 throw std::invalid_argument("complex Schur Arnoldi expansion target exceeds problem dimension");
1225 }
1226
1227 ArnoldiFactorization<Complex, Vector> factorization;
1228 factorization.hessenberg = uni20::DenseMatrix<Complex>(target_step_count + 1, target_step_count);
1229 laset(factorization.hessenberg, Complex{}, Complex{}, MatrixFill::All);
1230 factorization.basis.reserve(target_step_count + 1);
1231 for (auto const& vector : compressed.basis)
1232 {
1233 Vector copy = ops.allocate_like(vector);
1234 ops.copy(copy, vector);
1235 factorization.basis.push_back(std::move(copy));
1236 }
1237
1238 for (std::size_t col = 0; col < retained_count; ++col)
1239 {
1240 for (std::size_t row = 0; row < retained_count; ++row)
1241 {
1242 factorization.hessenberg[row, col] = compressed.schur_form[row, col];
1243 }
1244 }
1245
1246 factorization.step_count = static_cast<int>(retained_count);
1247 factorization.residual_norm = compressed.residual_norm;
1248 Real const compressed_breakdown_scale = detail::arnoldi_compressed_relation_scale(
1249 compressed.schur_form, compressed.residual_coupling, compressed.residual_norm);
1250 if (compressed.residual_norm <= detail::arnoldi_breakdown_threshold(breakdown_tolerance, compressed_breakdown_scale))
1251 {
1252 factorization.happy_breakdown =
1253 compressed.happy_breakdown || compressed.residual_norm <= detail::arnoldi_breakdown_threshold(
1254 breakdown_tolerance, compressed_breakdown_scale);
1255 return factorization;
1256 }
1257
1258 Vector next_vector = ops.allocate_like(compressed.residual);
1259 ops.copy(next_vector, compressed.residual);
1260 ops.scal(next_vector, Complex{1} / compressed.residual_norm);
1261 factorization.basis.push_back(std::move(next_vector));
1262 for (std::size_t col = 0; col < retained_count; ++col)
1263 {
1264 factorization.hessenberg[retained_count, col] = compressed.residual_norm * compressed.residual_coupling[col];
1265 }
1266 if (target_step_count == retained_count)
1267 {
1268 return factorization;
1269 }
1270
1271 for (std::size_t step = retained_count; step < target_step_count; ++step)
1272 {
1273 Vector residual = ops.allocate_like(factorization.basis[step]);
1274 ops.matvec(residual, factorization.basis[step]);
1275 ++factorization.op_count;
1276
1277 Real const beta = detail::orthogonalize_arnoldi_residual<Complex>(ops, factorization.basis,
1278 factorization.hessenberg, step, residual);
1279 factorization.residual_norm = beta;
1280 factorization.step_count = static_cast<int>(step + 1);
1281 Real const breakdown_scale =
1282 detail::arnoldi_hessenberg_column_scale(factorization.hessenberg, step, step + 1, beta);
1283 if (beta <= detail::arnoldi_breakdown_threshold(breakdown_tolerance, breakdown_scale))
1284 {
1285 factorization.happy_breakdown = true;
1286 break;
1287 }
1288
1289 factorization.hessenberg[step + 1, step] = beta;
1290 ops.scal(residual, Complex{1} / beta);
1291 factorization.basis.push_back(std::move(residual));
1292 }
1293
1294 return factorization;
1295}
1296
1309template <uni20::LapackReal Scalar, typename Vector, KrylovMatrixFreeOperator<Vector, Scalar> Ops>
1310NonsymmetricEigenResult<Scalar, Vector>
1311real_nonsymmetric_arnoldi_standard(Ops& ops, Vector const& initial, NonsymmetricEigenParams<Scalar> const& params)
1312{
1313 if (params.eigenvalue_count <= 0)
1314 {
1315 throw std::invalid_argument("real nonsymmetric Arnoldi requires a positive eigenvalue count");
1316 }
1317
1318 std::size_t const problem_dimension = validate_matrix_free_dimensions(ops, initial, "real nonsymmetric Arnoldi");
1319 int const basis_limit = effective_nonsymmetric_krylov_dimension(params, problem_dimension);
1320 if (basis_limit <= 0)
1321 {
1322 throw std::invalid_argument("real nonsymmetric Arnoldi requires a positive Krylov dimension");
1323 }
1324 if (basis_limit > static_cast<int>(problem_dimension))
1325 {
1326 throw std::invalid_argument("real nonsymmetric Arnoldi requires krylov_dimension <= problem dimension");
1327 }
1328 if (params.eigenvalue_count > basis_limit)
1329 {
1330 throw std::invalid_argument("real nonsymmetric Arnoldi requires eigenvalue_count <= krylov_dimension");
1331 }
1332
1333 ArnoldiFactorization<Scalar, Vector> factorization = arnoldi_factorize<Scalar>(ops, initial, basis_limit);
1335 bool const undersized_projection = ritz.ritz_values.size() < static_cast<std::size_t>(params.eigenvalue_count);
1336 std::vector<std::size_t> const selected = undersized_projection
1337 ? detail::all_ritz_indices(ritz.ritz_values)
1339 Scalar const projected_departure = projected_departure_from_normality(arnoldi_projected_hessenberg(factorization));
1340 return detail::make_real_nonsymmetric_arnoldi_result(ops, factorization, ritz, selected, params,
1341 factorization.op_count, factorization.op_count, 0,
1342 projected_departure);
1343}
1344
1358template <uni20::LapackComplexReal Real, typename Vector, KrylovMatrixFreeOperator<Vector, uni20::complex<Real>> Ops>
1360 NonsymmetricEigenParams<Real> const& params)
1361{
1362 if (params.eigenvalue_count <= 0)
1363 {
1364 throw std::invalid_argument("complex nonsymmetric Arnoldi requires a positive eigenvalue count");
1365 }
1366
1368
1369 std::size_t const problem_dimension = validate_matrix_free_dimensions(ops, initial, "complex nonsymmetric Arnoldi");
1370 int const basis_limit = effective_nonsymmetric_krylov_dimension(params, problem_dimension);
1371 if (basis_limit <= 0)
1372 {
1373 throw std::invalid_argument("complex nonsymmetric Arnoldi requires a positive Krylov dimension");
1374 }
1375 if (basis_limit > static_cast<int>(problem_dimension))
1376 {
1377 throw std::invalid_argument("complex nonsymmetric Arnoldi requires krylov_dimension <= problem dimension");
1378 }
1379 if (params.eigenvalue_count > basis_limit)
1380 {
1381 throw std::invalid_argument("complex nonsymmetric Arnoldi requires eigenvalue_count <= krylov_dimension");
1382 }
1383 int const requested_retained_count = basis_limit > params.eigenvalue_count
1384 ? effective_nonsymmetric_retained_ritz_count(params, basis_limit)
1385 : params.eigenvalue_count;
1386 if (params.max_iterations < params.eigenvalue_count)
1387 {
1388 throw std::invalid_argument("complex nonsymmetric Arnoldi iteration limit is too small for nev");
1389 }
1390
1391 int const initial_steps = std::min(basis_limit, params.max_iterations);
1392 ArnoldiFactorization<Complex, Vector> factorization = arnoldi_factorize<Complex>(ops, initial, initial_steps);
1393 int matvec_count = factorization.op_count;
1394 int restart_count = 0;
1395
1396 while (true)
1397 {
1399 bool const undersized_projection = ritz.ritz_values.size() < static_cast<std::size_t>(params.eigenvalue_count);
1400 std::vector<std::size_t> const selected = undersized_projection
1401 ? detail::all_ritz_indices(ritz.ritz_values)
1403 Real const projected_departure = projected_departure_from_normality(arnoldi_projected_hessenberg(factorization));
1404 auto make_cycle_result = [&](NonsymmetricEigenParams<Real> const& result_params) {
1405 return detail::make_complex_nonsymmetric_arnoldi_result(ops, factorization, ritz, selected, result_params,
1406 matvec_count, matvec_count, restart_count,
1407 projected_departure);
1408 };
1409 NonsymmetricEigenParams<Real> intermediate_params = params;
1410 intermediate_params.compute_eigenvectors = false;
1411 NonsymmetricEigenResult<Real, Vector> result = make_cycle_result(intermediate_params);
1412 auto make_final_result = [&]() {
1413 if (params.compute_eigenvectors)
1414 {
1415 return make_cycle_result(params);
1416 }
1417 return std::move(result);
1418 };
1419
1420 bool const all_selected_converged = result.converged_count == static_cast<int>(selected.size());
1421 if (all_selected_converged || factorization.happy_breakdown || matvec_count >= params.max_iterations)
1422 {
1423 return make_final_result();
1424 }
1425
1426 if (basis_limit == params.eigenvalue_count ||
1427 static_cast<std::size_t>(factorization.step_count) <= static_cast<std::size_t>(requested_retained_count))
1428 {
1429 return make_final_result();
1430 }
1431
1432 auto compressed = compress_complex_schur_arnoldi_restart<Real>(ops, factorization, params);
1433 ++restart_count;
1434 if (compressed.basis.size() >= static_cast<std::size_t>(basis_limit))
1435 {
1436 return make_final_result();
1437 }
1438
1439 int const remaining_matvecs = params.max_iterations - matvec_count;
1440 if (remaining_matvecs <= 0)
1441 {
1442 return make_final_result();
1443 }
1444 std::size_t const expansion_count = std::min<std::size_t>(
1445 static_cast<std::size_t>(basis_limit) - compressed.basis.size(), static_cast<std::size_t>(remaining_matvecs));
1446 if (expansion_count == 0)
1447 {
1448 return make_final_result();
1449 }
1450 std::size_t const target_step_count = compressed.basis.size() + expansion_count;
1451 factorization = expand_complex_schur_arnoldi_restart<Real>(ops, compressed, target_step_count);
1452 matvec_count += factorization.op_count;
1453 }
1454}
1455
1471template <uni20::LapackReal Scalar, typename Vector, KrylovMatrixFreeOperator<Vector, Scalar> Ops>
1472NonsymmetricEigenResult<Scalar, Vector>
1473real_nonsymmetric_arnoldi_restarted_standard(Ops& ops, Vector const& initial,
1474 NonsymmetricEigenParams<Scalar> const& params)
1475{
1476 if (params.eigenvalue_count <= 0)
1477 {
1478 throw std::invalid_argument("restarted real nonsymmetric Arnoldi requires a positive eigenvalue count");
1479 }
1480 if (params.max_iterations <= 0)
1481 {
1482 throw std::invalid_argument("restarted real nonsymmetric Arnoldi requires a positive iteration limit");
1483 }
1484
1485 std::size_t const problem_dimension =
1486 validate_matrix_free_dimensions(ops, initial, "restarted real nonsymmetric Arnoldi");
1487 int const basis_limit = effective_nonsymmetric_krylov_dimension(params, problem_dimension);
1488 if (basis_limit <= 0)
1489 {
1490 throw std::invalid_argument("restarted real nonsymmetric Arnoldi requires a positive Krylov dimension");
1491 }
1492 if (basis_limit > static_cast<int>(problem_dimension))
1493 {
1494 throw std::invalid_argument("restarted real nonsymmetric Arnoldi requires krylov_dimension <= problem dimension");
1495 }
1496 if (params.eigenvalue_count > basis_limit)
1497 {
1498 throw std::invalid_argument("restarted real nonsymmetric Arnoldi requires eigenvalue_count <= krylov_dimension");
1499 }
1500
1501 int const requested_retained_count = basis_limit > params.eigenvalue_count
1502 ? effective_nonsymmetric_retained_ritz_count(params, basis_limit)
1503 : params.eigenvalue_count;
1504 if (params.max_iterations < params.eigenvalue_count)
1505 {
1506 throw std::invalid_argument("restarted real nonsymmetric Arnoldi iteration limit is too small for nev");
1507 }
1508
1509 int const initial_steps = std::min(basis_limit, params.max_iterations);
1510 ArnoldiFactorization<Scalar, Vector> factorization = arnoldi_factorize<Scalar>(ops, initial, initial_steps);
1511 int matvec_count = factorization.op_count;
1512 int restart_count = 0;
1513
1514 while (true)
1515 {
1517 bool const undersized_projection = ritz.ritz_values.size() < static_cast<std::size_t>(params.eigenvalue_count);
1518 std::vector<std::size_t> const selected = undersized_projection
1519 ? detail::all_ritz_indices(ritz.ritz_values)
1521 Scalar const projected_departure = projected_departure_from_normality(arnoldi_projected_hessenberg(factorization));
1522 auto make_cycle_result = [&](NonsymmetricEigenParams<Scalar> const& result_params) {
1523 return detail::make_real_nonsymmetric_arnoldi_result(ops, factorization, ritz, selected, result_params,
1524 matvec_count, matvec_count, restart_count,
1525 projected_departure);
1526 };
1527 NonsymmetricEigenParams<Scalar> intermediate_params = params;
1528 intermediate_params.compute_eigenvectors = false;
1529 NonsymmetricEigenResult<Scalar, Vector> result = make_cycle_result(intermediate_params);
1530 auto make_final_result = [&]() {
1531 if (params.compute_eigenvectors)
1532 {
1533 return make_cycle_result(params);
1534 }
1535 return std::move(result);
1536 };
1537
1538 bool const all_selected_converged = result.converged_count == static_cast<int>(selected.size());
1539 if (all_selected_converged || factorization.happy_breakdown || matvec_count >= params.max_iterations)
1540 {
1541 return make_final_result();
1542 }
1543
1544 if (basis_limit == params.eigenvalue_count ||
1545 static_cast<std::size_t>(factorization.step_count) <= static_cast<std::size_t>(requested_retained_count))
1546 {
1547 return make_final_result();
1548 }
1549
1550 auto compressed = compress_real_schur_arnoldi_restart<Scalar>(ops, factorization, params);
1551 ++restart_count;
1552 if (compressed.basis.size() >= static_cast<std::size_t>(basis_limit))
1553 {
1554 return make_final_result();
1555 }
1556
1557 int const remaining_matvecs = params.max_iterations - matvec_count;
1558 if (remaining_matvecs <= 0)
1559 {
1560 return make_final_result();
1561 }
1562 std::size_t const expansion_count = std::min<std::size_t>(
1563 static_cast<std::size_t>(basis_limit) - compressed.basis.size(), static_cast<std::size_t>(remaining_matvecs));
1564 if (expansion_count == 0)
1565 {
1566 return make_final_result();
1567 }
1568 std::size_t const target_step_count = compressed.basis.size() + expansion_count;
1569 factorization = expand_real_schur_arnoldi_restart<Scalar>(ops, compressed, target_step_count);
1570 matvec_count += factorization.op_count;
1571 }
1572}
1573
1574} // namespace uni20::krylov
General-purpose owning tensor with immediate or deferred mdspan metadata.
Definition basic_tensor.hpp:109
auto rows() const noexcept
Return the matrix row count for rank-two tensors.
Definition basic_tensor.hpp:768
auto cols() const noexcept
Return the matrix column count for rank-two tensors.
Definition basic_tensor.hpp:776
Concept for complex scalar types.
Definition scalar_concepts.hpp:75
Concept for real scalar types (float, double, etc.).
Definition scalar_concepts.hpp:69
Concept for numeric scalar types (integer, real, or complex).
Definition scalar_concepts.hpp:81
locale_t value
Definition data_table.cpp:49
typename detail::complex_type< Real >::type complex
Project-level complex scalar spelling.
Definition types.hpp:66
typename make_real< T >::type make_real_t
Alias for the underlying real-valued type of T.
Definition scalar_traits.hpp:168
Definition block_tensor_vector.hpp:23
NonsymmetricEigenResult< Scalar, Vector > real_nonsymmetric_arnoldi_standard(Ops &ops, Vector const &initial, NonsymmetricEigenParams< Scalar > const &params)
Run the first native non-restarted real nonsymmetric Arnoldi solver.
Definition nonsymmetric_arnoldi.hpp:1311
int effective_nonsymmetric_retained_ritz_count(NonsymmetricEigenParams< Scalar > const &params, int krylov_dimension)
Return the effective nonsymmetric retained Ritz or Schur count.
Definition matrix_free.hpp:302
ArnoldiFactorization< Scalar, Vector > expand_real_schur_arnoldi_restart(Ops &ops, RealSchurCompressedArnoldiFactorization< Scalar, Vector > const &compressed, std::size_t target_step_count, Scalar breakdown_tolerance=Scalar{})
Expand a compressed real Schur Arnoldi relation back to a requested dimension.
Definition nonsymmetric_arnoldi.hpp:988
NonsymmetricEigenResult< Scalar, Vector > real_nonsymmetric_arnoldi_restarted_standard(Ops &ops, Vector const &initial, NonsymmetricEigenParams< Scalar > const &params)
Run a restarted native real nonsymmetric Arnoldi solver.
Definition nonsymmetric_arnoldi.hpp:1473
ComplexSchurCompressedArnoldiFactorization< Real, Vector > compress_complex_schur_arnoldi_restart(Ops &ops, ArnoldiFactorization< uni20::complex< Real >, Vector > const &factorization, NonsymmetricEigenParams< Real > const &params)
Compress a complex Arnoldi factorization through complex Schur reordering.
Definition nonsymmetric_arnoldi.hpp:1099
std::vector< std::size_t > select_nonsymmetric_restart_indices(std::vector< uni20::complex< Scalar > > const &values, NonsymmetricEigenParams< Scalar > const &params)
Select retained Ritz or Schur indices for a nonsymmetric thick restart.
Definition nonsymmetric_arnoldi.hpp:800
RealSchurRestartSelection< Scalar > select_real_schur_restart_blocks(std::vector< RealSchurBlock< Scalar > > const &blocks, NonsymmetricEigenParams< Scalar > const &params)
Select real Schur blocks for a nonsymmetric thick restart.
Definition nonsymmetric_arnoldi.hpp:820
void laset(uni20::DenseMatrix< Scalar > &matrix, Scalar const &diagonal, Scalar const &off_diagonal, MatrixFill fill)
Set a selected region of a local dense matrix.
Definition dense_linalg.hpp:294
RitzReality
Numerical reality classification for a complex Ritz value from a real Arnoldi solve.
Definition matrix_free.hpp:69
std::vector< std::size_t > select_nonsymmetric_ritz_indices(std::vector< uni20::complex< Scalar > > const &values, NonsymmetricEigenParams< Scalar > const &params)
Select wanted Ritz indices for a nonsymmetric projected spectrum.
Definition nonsymmetric_arnoldi.hpp:731
SpectrumPart
Spectrum selector for matrix-free eigenvalue solvers.
Definition matrix_free.hpp:23
RealNonsymmetricEigensystem< Real > real_nonsymmetric_eigensystem(uni20::DenseMatrix< Real > matrix, bool compute_right_vectors)
Definition dense_subspace.hpp:695
NonsymmetricEigenResult< Real, Vector > complex_nonsymmetric_arnoldi_standard(Ops &ops, Vector const &initial, NonsymmetricEigenParams< Real > const &params)
Run a restarted complex nonsymmetric Arnoldi solver.
Definition nonsymmetric_arnoldi.hpp:1359
RealSchurCompressedArnoldiFactorization< Scalar, Vector > compress_real_schur_arnoldi_restart(Ops &ops, ArnoldiFactorization< Scalar, Vector > const &factorization, NonsymmetricEigenParams< Scalar > const &params)
Compress an Arnoldi factorization through real Schur block reordering.
Definition nonsymmetric_arnoldi.hpp:876
RealSchurDecomposition< Scalar > reorder_real_schur(RealSchurDecomposition< Scalar > decomposition, std::vector< std::size_t > const &leading_block_order)
Reorder a column-major real Schur decomposition by moving selected blocks to the front.
Definition dense_subspace.hpp:574
ArnoldiFactorization< uni20::complex< Real >, Vector > expand_complex_schur_arnoldi_restart(Ops &ops, ComplexSchurCompressedArnoldiFactorization< Real, Vector > const &compressed, std::size_t target_step_count, Real breakdown_tolerance=Real{})
Expand a compressed complex Schur Arnoldi relation back to a requested dimension.
Definition nonsymmetric_arnoldi.hpp:1199
@ RequireRealEigenpairs
Accept only Ritz values that are numerically real; report genuinely complex wanted pairs.
@ AllowRealSchurPairs
Allow real Schur two-plane output for complex conjugate pairs once that output is implemented.
@ PromoteToComplexSuggested
Stop with a promotion recommendation when wanted Ritz values are genuinely complex.
ArnoldiRitzExtraction< Scalar > extract_arnoldi_ritz(ArnoldiFactorization< Scalar, Vector > const &factorization, Scalar complex_pair_tolerance=Scalar{}, Scalar scale=Scalar{1})
Extract projected Ritz values and residual estimates from a real Arnoldi factorization.
Definition nonsymmetric_arnoldi.hpp:587
int effective_nonsymmetric_krylov_dimension(NonsymmetricEigenParams< Scalar > const &params, std::size_t problem_dimension)
Return the effective nonsymmetric Arnoldi subspace dimension.
Definition matrix_free.hpp:234
RitzReality classify_ritz_reality(C theta, uni20::make_real_t< C > tolerance=uni20::make_real_t< C >{}, uni20::make_real_t< C > scale=uni20::make_real_t< C >{1}, uni20::make_real_t< C > ambiguity_factor=uni20::make_real_t< C >{10})
Classify whether a complex Ritz value is numerically real.
Definition matrix_free.hpp:499
ArnoldiRitzExtraction< uni20::make_real_t< Complex > > extract_complex_arnoldi_ritz(ArnoldiFactorization< Complex, Vector > const &factorization, uni20::make_real_t< Complex > complex_pair_tolerance=uni20::make_real_t< Complex >{}, uni20::make_real_t< Complex > scale=uni20::make_real_t< Complex >{1})
Extract projected Ritz values from a complex Arnoldi factorization.
Definition nonsymmetric_arnoldi.hpp:659
std::size_t validate_matrix_free_dimensions(Ops const &ops, Vector const &vector, char const *context)
Validate that an initial/prototype vector matches the matrix-free problem dimension.
Definition matrix_free.hpp:549
uni20::DenseMatrix< Scalar > arnoldi_projected_hessenberg(ArnoldiFactorization< Scalar, Vector > const &factorization)
Copy the square projected Hessenberg block from an Arnoldi factorization.
Definition nonsymmetric_arnoldi.hpp:548
RealSchurDecomposition< Scalar > real_schur(uni20::DenseMatrix< Scalar > matrix, bool compute_vectors)
Compute the real Schur decomposition of a column-major DenseMatrix.
Definition dense_subspace.hpp:368
uni20::make_real_t< Scalar > projected_departure_from_normality(uni20::DenseMatrix< Scalar > const &matrix)
Measure departure from normality of a projected matrix.
Definition nonsymmetric_arnoldi.hpp:431
ArnoldiFactorization< Scalar, Vector > arnoldi_factorize(Ops &ops, Vector const &initial, int max_steps, uni20::make_real_t< Scalar > breakdown_tolerance=uni20::make_real_t< Scalar >{})
Build a full-reorthogonalized Arnoldi factorization.
Definition nonsymmetric_arnoldi.hpp:489
void copy(std::span< Scalar > destination, std::span< Scalar const > source)
Copy a local dense vector.
Definition dense_linalg.hpp:183
constexpr detail::scalar_math::pow_fn pow
Power with the selected scalar's domain, exactness and precision rules.
Definition math.hpp:463
constexpr detail::scalar_math::sqrt_fn sqrt
Square root; exact inputs requiring approximation need an explicit finite precision.
Definition math.hpp:461
constexpr detail::scalar_math::abs_fn abs
Absolute value or complex magnitude, preserving the selected scalar implementation.
Definition math.hpp:459
auto abs_squared(T const &value)
Square the magnitude, evaluating the scalar absolute value once.
Definition math.hpp:681
mpreal norm(mpcomplex const &z)
Definition mpcomplex.hpp:494
void scale(Tensor &tensor, Scalar factor)
Multiply every stored numerical block by a scalar in place.
Definition block_tensor_linear.hpp:400
std::ptrdiff_t index_type
Definition types.hpp:19
mpreal beta(mpreal const &a, mpreal const &x)
Real Beta function; positive rational/integer pairs retain exact values.
Definition mpreal_special.hpp:164
Arnoldi factorization for a nonsymmetric matrix-free operator.
Definition nonsymmetric_arnoldi.hpp:24
std::vector< Vector > basis
Definition nonsymmetric_arnoldi.hpp:25
int op_count
Definition nonsymmetric_arnoldi.hpp:28
uni20::make_real_t< Scalar > residual_norm
Definition nonsymmetric_arnoldi.hpp:29
uni20::DenseMatrix< Scalar > hessenberg
Definition nonsymmetric_arnoldi.hpp:26
bool happy_breakdown
Definition nonsymmetric_arnoldi.hpp:30
int step_count
Definition nonsymmetric_arnoldi.hpp:27
Ritz data extracted from a real Arnoldi Hessenberg projection.
Definition nonsymmetric_arnoldi.hpp:36
uni20::DenseMatrix< uni20::complex< Scalar > > projected_right_eigenvectors
Definition nonsymmetric_arnoldi.hpp:40
std::vector< Scalar > residual_bounds
Definition nonsymmetric_arnoldi.hpp:38
std::vector< uni20::complex< Scalar > > ritz_values
Definition nonsymmetric_arnoldi.hpp:37
std::vector< RitzReality > reality
Definition nonsymmetric_arnoldi.hpp:39
Compressed complex Arnoldi relation after a Schur restart.
Definition nonsymmetric_arnoldi.hpp:83
std::vector< Complex > residual_coupling
Definition nonsymmetric_arnoldi.hpp:89
std::vector< Complex > eigenvalues
Definition nonsymmetric_arnoldi.hpp:90
std::vector< Vector > basis
Definition nonsymmetric_arnoldi.hpp:86
uni20::DenseMatrix< Complex > schur_form
Definition nonsymmetric_arnoldi.hpp:88
Real residual_norm
Definition nonsymmetric_arnoldi.hpp:91
bool happy_breakdown
Definition nonsymmetric_arnoldi.hpp:92
Vector residual
Definition nonsymmetric_arnoldi.hpp:87
Parameters for a native nonsymmetric eigenvalue solve.
Definition matrix_free.hpp:106
Scalar complex_pair_tolerance
Definition matrix_free.hpp:112
int max_iterations
Definition matrix_free.hpp:110
SpectrumPart spectrum
Definition matrix_free.hpp:113
bool compute_eigenvectors
Definition matrix_free.hpp:114
int eigenvalue_count
Definition matrix_free.hpp:107
Matrix-free nonsymmetric eigenvalue result.
Definition matrix_free.hpp:143
int converged_count
Definition matrix_free.hpp:148
Expert eigensystem diagnostics for a dense real nonsymmetric matrix.
Definition dense_subspace.hpp:39
Compressed real Arnoldi relation after a Schur restart.
Definition nonsymmetric_arnoldi.hpp:62
Vector residual
Definition nonsymmetric_arnoldi.hpp:64
bool happy_breakdown
Definition nonsymmetric_arnoldi.hpp:70
uni20::DenseMatrix< Scalar > schur_form
Definition nonsymmetric_arnoldi.hpp:65
std::vector< uni20::complex< Scalar > > eigenvalues
Definition nonsymmetric_arnoldi.hpp:67
std::vector< Vector > basis
Definition nonsymmetric_arnoldi.hpp:63
std::vector< RealSchurBlock< Scalar > > blocks
Definition nonsymmetric_arnoldi.hpp:68
Scalar residual_norm
Definition nonsymmetric_arnoldi.hpp:69
bool enlarged_to_preserve_block
Definition nonsymmetric_arnoldi.hpp:71
std::vector< Scalar > residual_coupling
Definition nonsymmetric_arnoldi.hpp:66
Block-aware selection result for real Schur restart.
Definition nonsymmetric_arnoldi.hpp:46
std::size_t scalar_count
Definition nonsymmetric_arnoldi.hpp:48
std::vector< std::size_t > block_indices
Definition nonsymmetric_arnoldi.hpp:47
bool enlarged_to_preserve_block
Definition nonsymmetric_arnoldi.hpp:49
Project-level numeric limits customization point.
Definition numeric_limits.hpp:16