Uni20
Asynchronous tensor and runtime library.
Loading...
Searching...
No Matches
symmetric_lanczos.hpp
Go to the documentation of this file.
1#pragma once
2
6
7#include <algorithm>
8#include <cmath>
9#include <cstddef>
10#include <limits>
11#include <numeric>
12#include <optional>
13#include <span>
14#include <stdexcept>
15#include <type_traits>
16#include <utility>
17#include <vector>
18
19namespace uni20::krylov
20{
21
24template <uni20::Real Scalar> struct SymmetricRestartSelection
25{
26 std::vector<std::size_t> wanted_indices;
27 std::vector<std::size_t> shift_indices;
28 std::vector<Scalar> shifts;
29 std::vector<bool> wanted_converged;
31};
32
35template <uni20::Real Scalar> struct SymmetricRestartPlan
36{
37 std::size_t retained_count = 0;
38 std::size_t shift_count = 0;
40 std::vector<std::size_t> protected_zero_bound_indices;
42};
43
46template <uni20::Real Scalar> struct SymmetricQrShiftResult
47{
48 std::vector<Scalar> diagonal;
49 std::vector<Scalar> subdiagonal;
51};
52
56template <uni20::Real Scalar, typename Vector> struct SymmetricCompressedLanczosFactorization
57{
58 std::vector<Vector> basis;
59 Vector residual;
60 std::vector<Scalar> diagonal;
61 std::vector<Scalar> subdiagonal;
65};
66
67namespace detail
68{
69template <uni20::Real Scalar> struct LanczosOrthogonalizationDiagnostics
70{
71 Scalar max_reorthogonalization_correction = Scalar{};
72 Scalar max_reorthogonalization_correction_ratio = Scalar{};
73 int max_reorthogonalization_passes = 0;
74};
75
76template <uni20::Real Scalar>
77void record_lanczos_orthogonalization_pass(LanczosOrthogonalizationDiagnostics<Scalar>* diagnostics,
78 Scalar max_correction, Scalar reference_norm, int pass_count)
79{
80 if (diagnostics == nullptr)
81 {
82 return;
83 }
84
85 diagnostics->max_reorthogonalization_correction =
86 std::max(diagnostics->max_reorthogonalization_correction, max_correction);
87 if (reference_norm > Scalar{})
88 {
89 diagnostics->max_reorthogonalization_correction_ratio =
90 std::max(diagnostics->max_reorthogonalization_correction_ratio, max_correction / reference_norm);
91 }
92 diagnostics->max_reorthogonalization_passes = std::max(diagnostics->max_reorthogonalization_passes, pass_count);
93}
94
95template <uni20::RealOrComplex Scalar, typename Vector, typename OpOps, typename BOps, typename MetricOps>
96class SymmetricBMetricOps {
97 public:
98 SymmetricBMetricOps(OpOps& op_ops, BOps& b_ops, MetricOps& metric_ops)
99 : op_ops_(op_ops), b_ops_(b_ops), metric_ops_(metric_ops)
100 {}
101
102 [[nodiscard]] std::size_t problem_dimension() const { return op_ops_.problem_dimension(); }
103
104 [[nodiscard]] std::size_t vector_dimension(Vector const& x) const { return op_ops_.vector_dimension(x); }
105
106 [[nodiscard]] Vector allocate_like(Vector const& x) { return op_ops_.allocate_like(x); }
107
108 void copy(Vector& dst, Vector const& src) { op_ops_.copy(dst, src); }
109
110 void axpy(Vector& y, Scalar alpha, Vector const& x) { op_ops_.axpy(y, alpha, x); }
111
112 void scal(Vector& x, Scalar alpha) { op_ops_.scal(x, alpha); }
113
114 void set_zero(Vector& x) { op_ops_.set_zero(x); }
115
116 [[nodiscard]] Scalar inner_product(Vector const& x, Vector const& y)
117 {
118 if (!scratch_.has_value())
119 {
120 scratch_.emplace(b_ops_.allocate_like(y));
121 }
122 return metric_inner_product_or_apply<Scalar>(metric_ops_, b_ops_, op_ops_, x, y, *scratch_);
123 }
124
125 void matvec(Vector& y, Vector const& x) { op_ops_.matvec(y, x); }
126
127 private:
128 OpOps& op_ops_;
129 BOps& b_ops_;
130 MetricOps& metric_ops_;
131 std::optional<Vector> scratch_;
132};
133
134template <uni20::Real Scalar> struct GivensRotation
135{
136 Scalar c = Scalar{1};
137 Scalar s = Scalar{};
138 Scalar r = Scalar{};
139};
140
141template <uni20::Real Scalar> GivensRotation<Scalar> lartg_like(Scalar f, Scalar g)
142{
143 if (g == Scalar{})
144 {
145 return GivensRotation<Scalar>{f < Scalar{} ? Scalar{-1} : Scalar{1}, Scalar{}, uni20::math::abs(f)};
146 }
147 if (f == Scalar{})
148 {
149 return GivensRotation<Scalar>{Scalar{}, g < Scalar{} ? Scalar{-1} : Scalar{1}, uni20::math::abs(g)};
150 }
151
152 using std::copysign;
153 using std::hypot;
154 Scalar const r = copysign(hypot(f, g), f);
155 return GivensRotation<Scalar>{f / r, g / r, r};
156}
157
158template <uni20::Real Scalar>
159void apply_givens_to_transform(uni20::DenseMatrix<Scalar>& q, std::size_t lhs, std::size_t rhs, Scalar c, Scalar s)
160{
161 for (uni20::index_type row = 0; row < q.rows(); ++row)
162 {
163 Scalar const q_lhs = q[row, lhs];
164 Scalar const q_rhs = q[row, rhs];
165 q[row, lhs] = c * q_lhs + s * q_rhs;
166 q[row, rhs] = -s * q_lhs + c * q_rhs;
167 }
168}
169
170#if defined(__cpp_lib_constexpr_cmath)
171template <uni20::Real Scalar> constexpr Scalar ritz_convergence_floor()
172#else
173template <uni20::Real Scalar> Scalar ritz_convergence_floor()
174#endif
175{
176 return uni20::math::pow(uni20::numeric_limits<Scalar>::epsilon(), Scalar{2} / Scalar{3});
177}
178
179template <uni20::Real Scalar> bool symmetric_ritz_converged(Scalar value, Scalar residual_bound, Scalar tolerance)
180{
181 Scalar const eps23 = ritz_convergence_floor<Scalar>();
182 return residual_bound <= tolerance * std::max(eps23, uni20::math::abs(value));
183}
184
185template <uni20::Real Scalar>
186Scalar symmetric_breakdown_threshold(SymmetricEigenParams<Scalar> const& params, Scalar local_scale)
187{
188 Scalar const tolerance = params.breakdown_tolerance > Scalar{}
189 ? params.breakdown_tolerance
191 return tolerance * std::max(Scalar{1}, local_scale);
192}
193
194template <uni20::Real Scalar>
195bool symmetric_happy_breakdown(Scalar residual_norm, SymmetricEigenParams<Scalar> const& params, Scalar local_scale)
196{
197 return residual_norm <= symmetric_breakdown_threshold(params, local_scale);
198}
199
200template <uni20::RealOrComplex Scalar, typename Vector, typename Ops>
201uni20::make_real_t<Scalar> hermitian_projection_scale(Ops& ops, Vector const& action)
202{
203 return norm_or_inner_product<Scalar>(ops, action);
204}
205
206template <uni20::RealOrComplex Scalar>
208hermitian_projection_scalar(Scalar value, uni20::make_real_t<Scalar> local_scale = uni20::make_real_t<Scalar>{1})
209{
211 Real const real_part = static_cast<Real>(uni20::math::real(value));
212 if constexpr (uni20::Complex<Scalar>)
213 {
214 Real const scale = std::max({Real{1}, local_scale, static_cast<Real>(uni20::math::abs(value))});
215 Real const tolerance = Real{100} * uni20::numeric_limits<Real>::epsilon() * scale;
216 if (uni20::math::abs(uni20::math::imag(value)) > tolerance)
217 {
218 throw std::runtime_error("Hermitian Lanczos received a projected diagonal with non-negligible imaginary part");
219 }
220 }
221 return real_part;
222}
223
224template <uni20::RealOrComplex Scalar, typename Vector, typename Ops>
226orthogonalize_lanczos_residual(Ops& ops, std::vector<Vector> const& basis, Vector& residual,
227 LanczosOrthogonalizationDiagnostics<uni20::make_real_t<Scalar>>* diagnostics = nullptr)
228{
230 auto norm = [&]() { return norm_or_inner_product<Scalar>(ops, residual); };
231 auto orthogonalize_once = [&]() {
232 Real max_correction{};
233 for (Vector const& basis_vector : basis)
234 {
235 Scalar const correction = ops.inner_product(basis_vector, residual);
236 max_correction = std::max(max_correction, static_cast<Real>(uni20::math::abs(correction)));
237 ops.axpy(residual, -correction, basis_vector);
238 }
239 return max_correction;
240 };
241
242 Real const original_norm = norm();
243 Real const first_max_correction = orthogonalize_once();
244 record_lanczos_orthogonalization_pass(diagnostics, first_max_correction, original_norm, 1);
245 Real residual_norm = norm();
246 if (original_norm == Real{} || residual_norm > Real{0.717} * original_norm)
247 {
248 return residual_norm;
249 }
250
251 Real const first_refined_norm = residual_norm;
252 Real const second_max_correction = orthogonalize_once();
253 record_lanczos_orthogonalization_pass(diagnostics, second_max_correction, first_refined_norm, 2);
254 residual_norm = norm();
255 if (first_refined_norm == Real{} || residual_norm <= Real{0.717} * first_refined_norm)
256 {
257 ops.set_zero(residual);
258 return Real{};
259 }
260 return residual_norm;
261}
262
263enum class SymmetricResultPolicy
264{
265 Requested,
266 ConvergedWanted
267};
268
269template <uni20::Real Scalar>
270std::size_t effective_retained_ritz_count(SymmetricEigenParams<Scalar> const& params, std::size_t available_count)
271{
272 if (params.eigenvalue_count <= 0)
273 {
274 throw std::invalid_argument("symmetric restart selection requires a positive eigenvalue count");
275 }
276 if (params.spectrum == SpectrumPart::BothEnds && params.eigenvalue_count == 1)
277 {
278 throw std::invalid_argument("both-end symmetric selection requires eigenvalue_count > 1");
279 }
280
281 std::size_t const requested_count = static_cast<std::size_t>(params.eigenvalue_count);
282 std::size_t const retained_count = params.retained_ritz_count > 0
283 ? static_cast<std::size_t>(params.retained_ritz_count)
284 : static_cast<std::size_t>(default_symmetric_retained_ritz_count(
285 params.eigenvalue_count, static_cast<int>(available_count)));
286 if (retained_count < requested_count)
287 {
288 throw std::invalid_argument("retained Ritz count must be at least the requested eigenvalue count");
289 }
290 if (retained_count > available_count)
291 {
292 throw std::invalid_argument("retained Ritz count cannot exceed the projected subspace size");
293 }
294 return retained_count;
295}
296
297template <uni20::Real Scalar>
298std::vector<std::size_t> ordered_symmetric_ritz_indices(std::vector<Scalar> const& values, SpectrumPart spectrum)
299{
300 std::vector<std::size_t> indices(values.size());
301 std::iota(indices.begin(), indices.end(), std::size_t{0});
302
303 switch (spectrum)
304 {
306 std::ranges::sort(indices,
307 [&](std::size_t lhs, std::size_t rhs) { return uni20::math::abs(values[lhs]) < uni20::math::abs(values[rhs]); });
308 break;
310 std::ranges::sort(indices,
311 [&](std::size_t lhs, std::size_t rhs) { return uni20::math::abs(values[lhs]) > uni20::math::abs(values[rhs]); });
312 break;
315 std::ranges::sort(indices, [&](std::size_t lhs, std::size_t rhs) { return values[lhs] < values[rhs]; });
316 break;
319 std::ranges::sort(indices, [&](std::size_t lhs, std::size_t rhs) { return values[lhs] > values[rhs]; });
320 break;
322 std::ranges::sort(indices, [&](std::size_t lhs, std::size_t rhs) { return values[lhs] < values[rhs]; });
323 break;
326 throw std::invalid_argument("imaginary spectrum selectors are not valid for symmetric/Hermitian Lanczos");
327 }
328 return indices;
329}
330
331template <uni20::Real Scalar>
332std::vector<std::size_t> select_symmetric_ritz_indices(std::vector<Scalar> const& values,
333 SymmetricEigenParams<Scalar> const& params)
334{
335 if (params.spectrum == SpectrumPart::BothEnds && params.eigenvalue_count == 1)
336 {
337 throw std::invalid_argument("both-end symmetric selection requires eigenvalue_count > 1");
338 }
339
340 std::vector<std::size_t> indices(values.size());
341 std::iota(indices.begin(), indices.end(), std::size_t{0});
342
343 switch (params.spectrum)
344 {
346 std::ranges::sort(indices,
347 [&](std::size_t lhs, std::size_t rhs) { return uni20::math::abs(values[lhs]) > uni20::math::abs(values[rhs]); });
348 break;
350 std::ranges::sort(indices,
351 [&](std::size_t lhs, std::size_t rhs) { return uni20::math::abs(values[lhs]) < uni20::math::abs(values[rhs]); });
352 break;
355 std::ranges::sort(indices, [&](std::size_t lhs, std::size_t rhs) { return values[lhs] > values[rhs]; });
356 break;
359 std::ranges::sort(indices, [&](std::size_t lhs, std::size_t rhs) { return values[lhs] < values[rhs]; });
360 break;
362 std::ranges::sort(indices, [&](std::size_t lhs, std::size_t rhs) { return values[lhs] < values[rhs]; });
363 {
364 std::size_t const requested_count =
365 std::min<std::size_t>(static_cast<std::size_t>(params.eigenvalue_count), indices.size());
366 std::size_t const low_count = requested_count / 2;
367 std::size_t const high_count = requested_count - low_count;
368 std::vector<std::size_t> both_end_indices;
369 both_end_indices.reserve(requested_count);
370 both_end_indices.insert(both_end_indices.end(), indices.begin(),
371 indices.begin() + static_cast<std::ptrdiff_t>(low_count));
372 both_end_indices.insert(both_end_indices.end(), indices.end() - static_cast<std::ptrdiff_t>(high_count),
373 indices.end());
374 return both_end_indices;
375 }
376 break;
379 throw std::invalid_argument("imaginary spectrum selectors are not valid for symmetric/Hermitian Lanczos");
380 }
381
382 std::size_t const count = std::min<std::size_t>(static_cast<std::size_t>(params.eigenvalue_count), indices.size());
383 indices.resize(count);
384 return indices;
385}
386
387template <uni20::Real Scalar>
388std::vector<Scalar> symmetric_ritz_residual_bounds(Scalar residual_norm,
389 TridiagonalEigensystem<Scalar> const& projected)
390{
391 std::vector<Scalar> residual_bounds(projected.eigenvalues.size(), Scalar{});
392 for (std::size_t i = 0; i < residual_bounds.size(); ++i)
393 {
394 residual_bounds[i] = residual_norm * uni20::math::abs(projected.eigenvectors[projected.eigenvectors.rows() - 1, i]);
395 }
396 return residual_bounds;
397}
398
399template <uni20::Real Scalar>
400void record_symmetric_final_diagnostics(SymmetricLanczosDiagnostics<Scalar>& diagnostics,
401 TridiagonalEigensystem<Scalar> const& projected,
402 std::vector<Scalar> const& residual_bounds,
403 SymmetricEigenParams<Scalar> const& params, Scalar residual_norm)
404{
405 diagnostics.final_projected_dimension = static_cast<int>(projected.eigenvalues.size());
406 diagnostics.final_residual_norm = residual_norm;
407 diagnostics.final_ritz_values = projected.eigenvalues;
408 diagnostics.final_ritz_bounds = residual_bounds;
409 diagnostics.final_selected_indices = select_symmetric_ritz_indices(projected.eigenvalues, params);
410}
411
412template <uni20::Real Scalar>
413void record_symmetric_restart_cycle(SymmetricLanczosDiagnostics<Scalar>& diagnostics, int cycle,
414 TridiagonalEigensystem<Scalar> const& projected,
415 std::vector<Scalar> const& residual_bounds,
416 SymmetricRestartPlan<Scalar> const& restart_plan,
417 SymmetricRestartSelection<Scalar> const& selection, Scalar residual_norm)
418{
419 diagnostics.restart_count =
420 std::max(diagnostics.restart_count, static_cast<int>(diagnostics.restart_cycles.size()) + 1);
421 diagnostics.restart_cycles.push_back(SymmetricRestartCycleDiagnostics<Scalar>{
422 .cycle = cycle,
423 .converged_count = selection.converged_count,
424 .projected_dimension = projected.eigenvalues.size(),
425 .retained_count = restart_plan.retained_count,
426 .shift_count = restart_plan.shift_count,
427 .protected_zero_bound_count = restart_plan.protected_zero_bound_count,
428 .enlarged_for_partial_convergence = restart_plan.enlarged_for_partial_convergence,
429 .residual_norm = residual_norm,
430 .ritz_values = projected.eigenvalues,
431 .ritz_bounds = residual_bounds,
432 .shifts = selection.shifts,
433 .wanted_indices = selection.wanted_indices,
434 .shift_indices = selection.shift_indices,
435 .wanted_converged = selection.wanted_converged,
436 });
437}
438
439template <uni20::LapackRealOrComplex Scalar, typename Vector, KrylovMatrixFreeOperator<Vector, Scalar> Ops>
440EigenResult<uni20::make_real_t<Scalar>, Vector>
441make_symmetric_lanczos_result(Ops& ops, std::vector<Vector> const& basis, uni20::make_real_t<Scalar> residual_norm,
442 TridiagonalEigensystem<uni20::make_real_t<Scalar>>& projected,
443 SymmetricEigenParams<uni20::make_real_t<Scalar>> const& params, int iteration_count,
444 int matvec_count, SymmetricResultPolicy policy = SymmetricResultPolicy::Requested)
445{
447 Real const tolerance =
448 params.tolerance > Real{} ? params.tolerance : Real{100} * uni20::numeric_limits<Real>::epsilon();
449
450 EigenResult<Real, Vector> result;
451 result.iteration_count = iteration_count;
452 result.matvec_count = matvec_count;
453
454 std::vector<std::size_t> selected = select_symmetric_ritz_indices(projected.eigenvalues, params);
455 if (policy == SymmetricResultPolicy::ConvergedWanted)
456 {
457 std::erase_if(selected, [&](std::size_t ritz_index) {
458 Real const residual_bound =
459 residual_norm * uni20::math::abs(projected.eigenvectors[projected.eigenvectors.rows() - 1, ritz_index]);
460 return !symmetric_ritz_converged(projected.eigenvalues[ritz_index], residual_bound, tolerance);
461 });
462 }
463
464 result.eigenvalues.reserve(selected.size());
465 result.residual_bounds.reserve(selected.size());
466 result.eigenvectors.reserve(params.compute_eigenvectors ? selected.size() : 0);
467 for (std::size_t const ritz_index : selected)
468 {
469 result.eigenvalues.push_back(projected.eigenvalues[ritz_index]);
470 Real const residual_bound =
471 residual_norm * uni20::math::abs(projected.eigenvectors[projected.eigenvectors.rows() - 1, ritz_index]);
472 result.residual_bounds.push_back(residual_bound);
473 if (symmetric_ritz_converged(projected.eigenvalues[ritz_index], residual_bound, tolerance))
474 {
475 ++result.converged_count;
476 }
477 if (params.compute_eigenvectors)
478 {
479 Vector vector = ops.allocate_like(basis.front());
480 ops.set_zero(vector);
481 for (std::size_t row = 0; row < basis.size(); ++row)
482 {
483 ops.axpy(vector, static_cast<Scalar>(projected.eigenvectors[row, ritz_index]), basis[row]);
484 }
485 result.eigenvectors.push_back(std::move(vector));
486 }
487 }
488
489 bool const requested_count_available = result.eigenvalues.size() >= static_cast<std::size_t>(params.eigenvalue_count);
490 bool const requested_count_converged = result.converged_count >= params.eigenvalue_count;
491 result.status = requested_count_available && requested_count_converged ? 0 : 1;
492 return result;
493}
494} // namespace detail
495
510template <uni20::Real Scalar>
511SymmetricRestartSelection<Scalar>
512select_symmetric_restart(std::vector<Scalar> const& values, std::vector<Scalar> const& residual_bounds,
513 SymmetricEigenParams<Scalar> const& params, std::size_t shift_count,
514 std::span<std::size_t const> protected_shift_indices = {})
515{
516 if (values.size() != residual_bounds.size())
517 {
518 throw std::invalid_argument("symmetric restart selection received inconsistent Ritz arrays");
519 }
520 std::size_t const wanted_count = detail::effective_retained_ritz_count(params, values.size());
521 std::vector<std::size_t> ordered = detail::ordered_symmetric_ritz_indices(values, params.spectrum);
522 std::vector<std::size_t> wanted_indices;
523 std::vector<std::size_t> unwanted_indices;
524 if (params.spectrum == SpectrumPart::BothEnds)
525 {
526 std::size_t const low_count = wanted_count / 2;
527 std::size_t const high_count = wanted_count - low_count;
528 wanted_indices.reserve(wanted_count);
529 wanted_indices.insert(wanted_indices.end(), ordered.begin(),
530 ordered.begin() + static_cast<std::ptrdiff_t>(low_count));
531 wanted_indices.insert(wanted_indices.end(), ordered.end() - static_cast<std::ptrdiff_t>(high_count), ordered.end());
532 unwanted_indices.assign(ordered.begin() + static_cast<std::ptrdiff_t>(low_count),
533 ordered.end() - static_cast<std::ptrdiff_t>(high_count));
534 }
535 else
536 {
537 wanted_indices.assign(ordered.end() - static_cast<std::ptrdiff_t>(wanted_count), ordered.end());
538 unwanted_indices.assign(ordered.begin(), ordered.end() - static_cast<std::ptrdiff_t>(wanted_count));
539 }
540
541 std::vector<bool> protected_index(values.size(), false);
542 for (std::size_t const index : protected_shift_indices)
543 {
544 if (index >= values.size())
545 {
546 throw std::invalid_argument("symmetric restart selection received an invalid protected Ritz index");
547 }
548 protected_index[index] = true;
549 if (std::ranges::find(wanted_indices, index) == wanted_indices.end())
550 {
551 wanted_indices.push_back(index);
552 }
553 }
554 std::erase_if(unwanted_indices, [&](std::size_t index) { return protected_index[index]; });
555 shift_count = std::min(shift_count, unwanted_indices.size());
556
557 Scalar const tolerance =
558 params.tolerance > Scalar{} ? params.tolerance : Scalar{100} * uni20::numeric_limits<Scalar>::epsilon();
559
560 SymmetricRestartSelection<Scalar> selection;
561 selection.wanted_indices = std::move(wanted_indices);
562 selection.wanted_converged.reserve(selection.wanted_indices.size());
563 for (std::size_t const index : selection.wanted_indices)
564 {
565 bool const converged = detail::symmetric_ritz_converged(values[index], residual_bounds[index], tolerance);
566 selection.wanted_converged.push_back(converged);
567 if (converged)
568 {
569 ++selection.converged_count;
570 }
571 }
572
573 selection.shift_indices.assign(unwanted_indices.begin(),
574 unwanted_indices.begin() + static_cast<std::ptrdiff_t>(shift_count));
575 std::ranges::sort(selection.shift_indices, [&](std::size_t lhs, std::size_t rhs) {
576 return uni20::math::abs(residual_bounds[lhs]) > uni20::math::abs(residual_bounds[rhs]);
577 });
578 selection.shifts.reserve(selection.shift_indices.size());
579 for (std::size_t const index : selection.shift_indices)
580 {
581 selection.shifts.push_back(values[index]);
582 }
583 return selection;
584}
585
600template <uni20::Real Scalar>
602 std::vector<Scalar> const& values,
603 std::vector<Scalar> const& residual_bounds, int converged_count)
604{
605 if (values.size() != residual_bounds.size())
606 {
607 throw std::invalid_argument("symmetric restart planning received inconsistent Ritz arrays");
608 }
609
610 std::size_t const order = values.size();
611 std::size_t retained_count = detail::effective_retained_ritz_count(params, order);
612 if (retained_count >= order)
613 {
614 throw std::invalid_argument("symmetric restart planning requires retained_count < projected size");
615 }
616
617 std::size_t shift_count = order - retained_count;
619 SymmetricRestartSelection<Scalar> selection = select_symmetric_restart(values, residual_bounds, params, shift_count);
620
621 for (std::size_t const index : selection.shift_indices)
622 {
623 if (residual_bounds[index] == Scalar{})
624 {
626 plan.protected_zero_bound_indices.push_back(index);
627 }
628 }
629
630 if (plan.protected_zero_bound_count > 0)
631 {
632 retained_count = std::min(order, retained_count + plan.protected_zero_bound_count);
633 shift_count = order - retained_count;
634 }
635
636 if (converged_count < params.eigenvalue_count && shift_count > 0)
637 {
638 std::size_t const old_retained_count = retained_count;
639 retained_count += std::min<std::size_t>(static_cast<std::size_t>(converged_count), shift_count / 2);
640 if (retained_count == 1 && order >= 6)
641 {
642 retained_count = order / 2;
643 }
644 else if (retained_count == 1 && order > 2)
645 {
646 retained_count = 2;
647 }
648 retained_count = std::min(retained_count, order);
649 shift_count = order - retained_count;
650 plan.enlarged_for_partial_convergence = retained_count > old_retained_count;
651 }
652
653 plan.retained_count = retained_count;
654 plan.shift_count = shift_count;
655 return plan;
656}
657
669template <uni20::Real Scalar>
670SymmetricQrShiftResult<Scalar> apply_symmetric_qr_shifts(std::vector<Scalar> diagonal, std::vector<Scalar> subdiagonal,
671 std::vector<Scalar> const& shifts)
672{
673 std::size_t const order = diagonal.size();
674 if (subdiagonal.size() + 1 != order && !(order == 0 && subdiagonal.empty()))
675 {
676 throw std::invalid_argument("symmetric QR shifts received inconsistent tridiagonal sizes");
677 }
678
680 result.transform = uni20::DenseMatrix<Scalar>(order, order);
681 laset(result.transform, Scalar{1}, Scalar{}, MatrixFill::All);
682 if (order == 0 || shifts.empty())
683 {
684 result.diagonal = std::move(diagonal);
685 result.subdiagonal = std::move(subdiagonal);
686 return result;
687 }
688
690 std::size_t itop = 0;
691 for (Scalar const shift : shifts)
692 {
693 std::size_t istart = itop;
694 while (istart + 1 < order)
695 {
696 std::size_t iend = order - 1;
697 for (std::size_t i = istart; i + 1 < order; ++i)
698 {
699 Scalar const big = uni20::math::abs(diagonal[i]) + uni20::math::abs(diagonal[i + 1]);
700 if (uni20::math::abs(subdiagonal[i]) <= epsilon * big)
701 {
702 subdiagonal[i] = Scalar{};
703 iend = i;
704 break;
705 }
706 }
707
708 if (istart < iend)
709 {
710 Scalar f = diagonal[istart] - shift;
711 Scalar g = subdiagonal[istart];
712 auto rotation = detail::lartg_like(f, g);
713 Scalar c = rotation.c;
714 Scalar s = rotation.s;
715
716 Scalar a1 = c * diagonal[istart] + s * subdiagonal[istart];
717 Scalar a2 = c * subdiagonal[istart] + s * diagonal[istart + 1];
718 Scalar a4 = c * diagonal[istart + 1] - s * subdiagonal[istart];
719 Scalar a3 = c * subdiagonal[istart] - s * diagonal[istart];
720 diagonal[istart] = c * a1 + s * a2;
721 diagonal[istart + 1] = c * a4 - s * a3;
722 subdiagonal[istart] = c * a3 + s * a4;
723 detail::apply_givens_to_transform(result.transform, istart, istart + 1, c, s);
724
725 for (std::size_t i = istart + 1; i < iend; ++i)
726 {
727 f = subdiagonal[i - 1];
728 g = s * subdiagonal[i];
729 subdiagonal[i] *= c;
730 rotation = detail::lartg_like(f, g);
731 c = rotation.c;
732 s = rotation.s;
733 Scalar r = rotation.r;
734 if (r < Scalar{})
735 {
736 r = -r;
737 c = -c;
738 s = -s;
739 }
740
741 subdiagonal[i - 1] = r;
742 a1 = c * diagonal[i] + s * subdiagonal[i];
743 a2 = c * subdiagonal[i] + s * diagonal[i + 1];
744 a3 = c * subdiagonal[i] - s * diagonal[i];
745 a4 = c * diagonal[i + 1] - s * subdiagonal[i];
746 diagonal[i] = c * a1 + s * a2;
747 diagonal[i + 1] = c * a4 - s * a3;
748 subdiagonal[i] = c * a3 + s * a4;
749 detail::apply_givens_to_transform(result.transform, i, i + 1, c, s);
750 }
751 }
752
753 if (iend > 0 && subdiagonal[iend - 1] < Scalar{})
754 {
755 subdiagonal[iend - 1] = -subdiagonal[iend - 1];
756 for (uni20::index_type row = 0; row < result.transform.rows(); ++row)
757 {
758 result.transform[row, iend] = -result.transform[row, iend];
759 }
760 }
761
762 istart = iend + 1;
763 }
764
765 for (std::size_t i = itop; i + 1 < order; ++i)
766 {
767 if (subdiagonal[i] > Scalar{})
768 {
769 break;
770 }
771 itop = i + 1;
772 }
773 }
774
775 for (std::size_t i = itop; i + 1 < order; ++i)
776 {
777 Scalar const big = uni20::math::abs(diagonal[i]) + uni20::math::abs(diagonal[i + 1]);
778 if (uni20::math::abs(subdiagonal[i]) <= epsilon * big)
779 {
780 subdiagonal[i] = Scalar{};
781 }
782 }
783
784 result.diagonal = std::move(diagonal);
785 result.subdiagonal = std::move(subdiagonal);
786 return result;
787}
788
807template <uni20::LapackRealOrComplex Scalar, typename Vector, KrylovMatrixFreeOperator<Vector, Scalar> Ops>
808SymmetricCompressedLanczosFactorization<uni20::make_real_t<Scalar>, Vector>
809compress_symmetric_lanczos_restart(Ops& ops, std::vector<Vector> const& basis, Vector const& residual,
811 std::size_t retained_count)
812{
814 std::size_t const order = basis.size();
815 if (order == 0)
816 {
817 throw std::invalid_argument("symmetric restart compression requires a nonempty basis");
818 }
819 if (shifted.diagonal.size() != order || shifted.subdiagonal.size() + 1 != order)
820 {
821 throw std::invalid_argument("symmetric restart compression received inconsistent tridiagonal sizes");
822 }
823 auto const order_extent = static_cast<uni20::index_type>(order);
824 if (shifted.transform.rows() != order_extent || shifted.transform.cols() != order_extent)
825 {
826 throw std::invalid_argument("symmetric restart compression received an inconsistent transform size");
827 }
828 if (retained_count == 0 || retained_count > order)
829 {
830 throw std::invalid_argument("symmetric restart compression received an invalid retained count");
831 }
832
833 auto transformed_column = [&](std::size_t column) {
834 Vector vector = ops.allocate_like(basis.front());
835 ops.set_zero(vector);
836 for (std::size_t row = 0; row < order; ++row)
837 {
838 Real const coefficient = shifted.transform[row, column];
839 if (coefficient != Real{})
840 {
841 ops.axpy(vector, static_cast<Scalar>(coefficient), basis[row]);
842 }
843 }
844 return vector;
845 };
846
848 .basis = {},
849 .residual = ops.allocate_like(residual),
850 .diagonal = {},
851 .subdiagonal = {},
852 .residual_norm = Real{},
853 .residual_scale = shifted.transform[order - 1, retained_count - 1],
854 .trailing_coupling = retained_count < order ? shifted.subdiagonal[retained_count - 1] : Real{}};
855
856 result.basis.reserve(retained_count);
857 for (std::size_t column = 0; column < retained_count; ++column)
858 {
859 result.basis.push_back(transformed_column(column));
860 }
861
862 result.diagonal.assign(shifted.diagonal.begin(),
863 shifted.diagonal.begin() + static_cast<std::ptrdiff_t>(retained_count));
864 if (retained_count > 1)
865 {
866 result.subdiagonal.assign(shifted.subdiagonal.begin(),
867 shifted.subdiagonal.begin() + static_cast<std::ptrdiff_t>(retained_count - 1));
868 }
869
870 ops.copy(result.residual, residual);
871 ops.scal(result.residual, static_cast<Scalar>(result.residual_scale));
872 if (result.trailing_coupling != Real{})
873 {
874 Vector next_vector = transformed_column(retained_count);
875 ops.axpy(result.residual, static_cast<Scalar>(result.trailing_coupling), next_vector);
876 }
877
878 result.residual_norm = norm_or_inner_product<Scalar>(ops, result.residual);
879 return result;
880}
881
895template <uni20::LapackRealOrComplex Scalar, typename Vector, KrylovMatrixFreeOperator<Vector, Scalar> Ops>
896EigenResult<uni20::make_real_t<Scalar>, Vector>
897symmetric_lanczos_standard(Ops& ops, Vector const& initial,
899{
901 if (params.eigenvalue_count <= 0)
902 {
903 throw std::invalid_argument("symmetric Lanczos requires a positive eigenvalue count");
904 }
905
906 std::size_t const problem_dimension = validate_matrix_free_dimensions(ops, initial, "symmetric Lanczos");
907 int const basis_limit = effective_symmetric_krylov_dimension(params, problem_dimension);
908 if (basis_limit <= 0)
909 {
910 throw std::invalid_argument("symmetric Lanczos requires a positive Krylov dimension");
911 }
912 if (basis_limit > static_cast<int>(problem_dimension))
913 {
914 throw std::invalid_argument("symmetric Lanczos requires krylov_dimension <= problem dimension");
915 }
916 if (params.eigenvalue_count > basis_limit)
917 {
918 throw std::invalid_argument("symmetric Lanczos requires eigenvalue_count <= krylov_dimension");
919 }
920
922 std::vector<Vector> basis;
923 std::vector<Real> diagonal;
924 std::vector<Real> subdiagonal;
925 Real final_residual_norm{};
926 basis.reserve(static_cast<std::size_t>(basis_limit));
927 diagonal.reserve(static_cast<std::size_t>(basis_limit));
928 subdiagonal.reserve(static_cast<std::size_t>(basis_limit > 0 ? basis_limit - 1 : 0));
929
930 Vector start = ops.allocate_like(initial);
931 ops.copy(start, initial);
932 Real const start_norm = norm_or_inner_product<Scalar>(ops, start);
933 if (start_norm <= Real{})
934 {
935 throw std::invalid_argument("symmetric Lanczos initial vector has zero norm");
936 }
937 ops.scal(start, Scalar{1} / static_cast<Scalar>(start_norm));
938 basis.push_back(std::move(start));
939
940 for (int step = 0; step < basis_limit; ++step)
941 {
942 Vector w = ops.allocate_like(basis[static_cast<std::size_t>(step)]);
943 ops.matvec(w, basis[static_cast<std::size_t>(step)]);
944 ++result.matvec_count;
945
946 if (step > 0)
947 {
948 ops.axpy(w, -static_cast<Scalar>(subdiagonal[static_cast<std::size_t>(step - 1)]),
949 basis[static_cast<std::size_t>(step - 1)]);
950 }
951
952 Real const projection_scale = detail::hermitian_projection_scale<Scalar>(ops, w);
953 Real const alpha = detail::hermitian_projection_scalar(ops.inner_product(basis[static_cast<std::size_t>(step)], w),
954 projection_scale);
955 diagonal.push_back(alpha);
956 ops.axpy(w, -static_cast<Scalar>(alpha), basis[static_cast<std::size_t>(step)]);
957
958 Real const beta = detail::orthogonalize_lanczos_residual<Scalar>(ops, basis, w);
959 Real const previous_beta = step > 0 ? subdiagonal[static_cast<std::size_t>(step - 1)] : Real{};
960 Real const breakdown_scale =
961 std::max({Real{1}, projection_scale, uni20::math::abs(alpha), uni20::math::abs(previous_beta)});
962 if (step + 1 == basis_limit || detail::symmetric_happy_breakdown(beta, params, breakdown_scale))
963 {
964 final_residual_norm = beta;
965 break;
966 }
967
968 ops.scal(w, Scalar{1} / static_cast<Scalar>(beta));
969 subdiagonal.push_back(beta);
970 basis.push_back(std::move(w));
971 }
972
973 result.iteration_count = static_cast<int>(diagonal.size());
974 auto projected = symmetric_tridiagonal_eigensystem(std::move(diagonal), std::move(subdiagonal), true);
975 auto final_result = detail::make_symmetric_lanczos_result<Scalar>(ops, basis, final_residual_norm, projected, params,
976 result.iteration_count, result.matvec_count);
977 if (params.diagnostics != KrylovDiagnosticsLevel::None)
978 {
980 diagnostics.op_count = result.matvec_count;
981 std::vector<Real> const residual_bounds = detail::symmetric_ritz_residual_bounds(final_residual_norm, projected);
982 detail::record_symmetric_final_diagnostics(diagnostics, projected, residual_bounds, params, final_residual_norm);
983 final_result.diagnostics = std::move(diagnostics);
984 }
985 return final_result;
986}
987
1001template <uni20::LapackRealOrComplex Scalar, typename Vector, KrylovMatrixFreeOperator<Vector, Scalar> Ops>
1002EigenResult<uni20::make_real_t<Scalar>, Vector>
1003symmetric_lanczos_restarted_standard(Ops& ops, Vector const& initial,
1005{
1007 if (params.eigenvalue_count <= 0)
1008 {
1009 throw std::invalid_argument("restarted symmetric Lanczos requires a positive eigenvalue count");
1010 }
1011 if (params.max_iterations <= 0)
1012 {
1013 throw std::invalid_argument("restarted symmetric Lanczos requires a positive iteration limit");
1014 }
1015
1016 std::size_t const problem_dimension = validate_matrix_free_dimensions(ops, initial, "restarted symmetric Lanczos");
1017 int const basis_limit = effective_symmetric_krylov_dimension(params, problem_dimension);
1018 if (basis_limit <= params.eigenvalue_count)
1019 {
1020 throw std::invalid_argument("restarted symmetric Lanczos requires eigenvalue_count < krylov_dimension");
1021 }
1022 if (basis_limit > static_cast<int>(problem_dimension))
1023 {
1024 throw std::invalid_argument("restarted symmetric Lanczos requires krylov_dimension <= problem dimension");
1025 }
1026
1027 std::size_t const ncv = static_cast<std::size_t>(basis_limit);
1028 std::size_t const retained_count =
1029 static_cast<std::size_t>(effective_symmetric_retained_ritz_count(params, basis_limit));
1030 if (retained_count >= ncv)
1031 {
1032 throw std::invalid_argument("restarted symmetric Lanczos requires retained_ritz_count < krylov_dimension");
1033 }
1034
1035 std::vector<Vector> basis;
1036 std::vector<Real> diagonal;
1037 std::vector<Real> subdiagonal;
1038 basis.reserve(ncv);
1039 diagonal.reserve(ncv);
1040 subdiagonal.reserve(ncv - 1);
1041
1042 Vector start = ops.allocate_like(initial);
1043 ops.copy(start, initial);
1044 Real const start_norm = norm_or_inner_product<Scalar>(ops, start);
1045 if (start_norm <= Real{})
1046 {
1047 throw std::invalid_argument("restarted symmetric Lanczos initial vector has zero norm");
1048 }
1049 ops.scal(start, Scalar{1} / static_cast<Scalar>(start_norm));
1050 basis.push_back(std::move(start));
1051
1052 int matvec_count = 0;
1053 int restart_cycle_count = 0;
1054 int restart_count = 0;
1055 EigenResult<Real, Vector> last_result;
1056 EigenResult<Real, Vector> last_partial_exit_result;
1057 std::optional<SymmetricLanczosDiagnostics<Real>> diagnostics;
1058 if (params.diagnostics != KrylovDiagnosticsLevel::None)
1059 {
1060 diagnostics.emplace();
1061 }
1062
1063 auto finish = [&](EigenResult<Real, Vector> result) {
1064 if (diagnostics.has_value())
1065 {
1066 diagnostics->restart_count = restart_count;
1067 diagnostics->op_count = matvec_count;
1068 result.diagnostics = *diagnostics;
1069 }
1070 return result;
1071 };
1072
1073 for (int cycle = 0; cycle < params.max_iterations; ++cycle)
1074 {
1075 restart_cycle_count = cycle + 1;
1076 Real residual_norm{};
1077 bool cycle_happy_breakdown = false;
1078 Vector residual = ops.allocate_like(basis.front());
1079 ops.set_zero(residual);
1080
1081 while (diagonal.size() < ncv)
1082 {
1083 std::size_t const step = diagonal.size();
1084 if (basis.size() != step + 1)
1085 {
1086 throw std::logic_error("restarted symmetric Lanczos basis/projection state is inconsistent");
1087 }
1088
1089 Vector w = ops.allocate_like(basis[step]);
1090 ops.matvec(w, basis[step]);
1091 ++matvec_count;
1092
1093 if (step > 0)
1094 {
1095 ops.axpy(w, -static_cast<Scalar>(subdiagonal[step - 1]), basis[step - 1]);
1096 }
1097
1098 Real const projection_scale = detail::hermitian_projection_scale<Scalar>(ops, w);
1099 Real const alpha = detail::hermitian_projection_scalar(ops.inner_product(basis[step], w), projection_scale);
1100 diagonal.push_back(alpha);
1101 ops.axpy(w, -static_cast<Scalar>(alpha), basis[step]);
1102
1103 Real const beta = detail::orthogonalize_lanczos_residual<Scalar>(ops, basis, w);
1104 Real const previous_beta = step > 0 ? subdiagonal[step - 1] : Real{};
1105 Real const breakdown_scale =
1106 std::max({Real{1}, projection_scale, uni20::math::abs(alpha), uni20::math::abs(previous_beta)});
1107 cycle_happy_breakdown = detail::symmetric_happy_breakdown(beta, params, breakdown_scale);
1108 residual = std::move(w);
1109 residual_norm = beta;
1110 if (diagonal.size() == ncv || cycle_happy_breakdown)
1111 {
1112 break;
1113 }
1114
1115 ops.scal(residual, Scalar{1} / static_cast<Scalar>(beta));
1116 subdiagonal.push_back(beta);
1117 basis.push_back(std::move(residual));
1118 }
1119
1120 auto projected_for_result = symmetric_tridiagonal_eigensystem(diagonal, subdiagonal, true);
1121 std::vector<Real> residual_bounds = detail::symmetric_ritz_residual_bounds(residual_norm, projected_for_result);
1122 if (diagnostics.has_value())
1123 {
1124 detail::record_symmetric_final_diagnostics(*diagnostics, projected_for_result, residual_bounds, params,
1125 residual_norm);
1126 }
1127 SymmetricEigenParams<Real> cycle_result_params = params;
1128 if (cycle + 1 < params.max_iterations)
1129 {
1130 cycle_result_params.compute_eigenvectors = false;
1131 }
1132 last_result = detail::make_symmetric_lanczos_result<Scalar>(ops, basis, residual_norm, projected_for_result,
1133 cycle_result_params, restart_cycle_count, matvec_count);
1134 last_partial_exit_result = detail::make_symmetric_lanczos_result<Scalar>(
1135 ops, basis, residual_norm, projected_for_result, cycle_result_params, restart_cycle_count, matvec_count,
1136 detail::SymmetricResultPolicy::ConvergedWanted);
1137 if (last_result.status == 0 || cycle_happy_breakdown || diagonal.size() < ncv)
1138 {
1139 if (!cycle_result_params.compute_eigenvectors && params.compute_eigenvectors)
1140 {
1141 return finish(detail::make_symmetric_lanczos_result<Scalar>(ops, basis, residual_norm, projected_for_result,
1142 params, restart_cycle_count, matvec_count));
1143 }
1144 return finish(std::move(last_result));
1145 }
1146
1147 SymmetricRestartPlan<Real> restart_plan =
1148 plan_symmetric_restart(params, projected_for_result.eigenvalues, residual_bounds, last_result.converged_count);
1149 SymmetricEigenParams<Real> restart_params = params;
1150 restart_params.retained_ritz_count = static_cast<int>(restart_plan.retained_count);
1151 SymmetricRestartSelection<Real> const selection =
1152 select_symmetric_restart(projected_for_result.eigenvalues, residual_bounds, restart_params,
1153 restart_plan.shift_count, restart_plan.protected_zero_bound_indices);
1154 restart_plan.shift_count = selection.shifts.size();
1155 restart_plan.retained_count = projected_for_result.eigenvalues.size() - restart_plan.shift_count;
1156 if (selection.shifts.empty())
1157 {
1158 EigenResult<Real, Vector> exit_result =
1159 (!cycle_result_params.compute_eigenvectors && params.compute_eigenvectors)
1160 ? detail::make_symmetric_lanczos_result<Scalar>(ops, basis, residual_norm, projected_for_result, params,
1161 restart_cycle_count, matvec_count,
1162 detail::SymmetricResultPolicy::ConvergedWanted)
1163 : std::move(last_partial_exit_result);
1164 exit_result.status = last_result.converged_count < params.eigenvalue_count ? 2 : 0;
1165 return finish(std::move(exit_result));
1166 }
1167
1168 ++restart_count;
1169 if (diagnostics.has_value() && params.diagnostics == KrylovDiagnosticsLevel::Full)
1170 {
1171 detail::record_symmetric_restart_cycle(*diagnostics, cycle, projected_for_result, residual_bounds, restart_plan,
1172 selection, residual_norm);
1173 }
1174
1175 auto shifted = apply_symmetric_qr_shifts(diagonal, subdiagonal, selection.shifts);
1176 auto compressed =
1177 compress_symmetric_lanczos_restart<Scalar>(ops, basis, residual, shifted, restart_plan.retained_count);
1178
1179 basis = std::move(compressed.basis);
1180 diagonal = std::move(compressed.diagonal);
1181 subdiagonal = std::move(compressed.subdiagonal);
1182 residual = std::move(compressed.residual);
1183 residual_norm = compressed.residual_norm;
1184 Real const compressed_breakdown_scale =
1185 std::max({Real{1}, uni20::math::abs(compressed.residual_scale), uni20::math::abs(compressed.trailing_coupling)});
1186
1187 if (detail::symmetric_happy_breakdown(residual_norm, params, compressed_breakdown_scale))
1188 {
1189 auto projected_after_restart = symmetric_tridiagonal_eigensystem(diagonal, subdiagonal, true);
1190 std::vector<Real> const after_restart_bounds =
1191 detail::symmetric_ritz_residual_bounds(residual_norm, projected_after_restart);
1192 if (diagnostics.has_value())
1193 {
1194 detail::record_symmetric_final_diagnostics(*diagnostics, projected_after_restart, after_restart_bounds, params,
1195 residual_norm);
1196 }
1197 return finish(detail::make_symmetric_lanczos_result<Scalar>(ops, basis, residual_norm, projected_after_restart,
1198 params, restart_cycle_count, matvec_count));
1199 }
1200
1201 ops.scal(residual, Scalar{1} / static_cast<Scalar>(residual_norm));
1202 subdiagonal.push_back(residual_norm);
1203 basis.push_back(std::move(residual));
1204 }
1205
1206 if (last_result.status != 0)
1207 {
1208 last_partial_exit_result.status = 1;
1209 return finish(std::move(last_partial_exit_result));
1210 }
1211 return finish(std::move(last_result));
1212}
1213
1219template <uni20::Real Scalar>
1222{
1223 switch (options.transform)
1224 {
1225 case SymmetricSpectralTransform::Regular:
1226 case SymmetricSpectralTransform::GeneralizedInverse:
1227 return transformed_value;
1228 case SymmetricSpectralTransform::ShiftInvert:
1229 if (transformed_value == Scalar{})
1230 {
1231 throw std::domain_error("shift-invert eigenvalue map received a zero transformed Ritz value");
1232 }
1233 return options.sigma + Scalar{1} / transformed_value;
1234 case SymmetricSpectralTransform::Buckling:
1235 if (transformed_value == Scalar{1})
1236 {
1237 throw std::domain_error("buckling eigenvalue map received a unit transformed Ritz value");
1238 }
1239 return options.sigma * transformed_value / (transformed_value - Scalar{1});
1240 case SymmetricSpectralTransform::Cayley:
1241 if (transformed_value == Scalar{1})
1242 {
1243 throw std::domain_error("Cayley eigenvalue map received a unit transformed Ritz value");
1244 }
1245 return options.sigma * (transformed_value + Scalar{1}) / (transformed_value - Scalar{1});
1246 }
1247 throw std::invalid_argument("unknown symmetric spectral transform");
1248}
1249
1267template <uni20::LapackRealOrComplex Scalar, typename Vector, KrylovMatrixFreeOperator<Vector, Scalar> Ops>
1268EigenResult<uni20::make_real_t<Scalar>, Vector>
1269symmetric_lanczos_restarted_transformed(Ops& op_ops, Vector const& initial,
1272{
1273 auto result = symmetric_lanczos_restarted_standard<Scalar>(op_ops, initial, params);
1274 for (uni20::make_real_t<Scalar>& value : result.eigenvalues)
1275 {
1277 }
1278 return result;
1279}
1280
1306template <uni20::LapackRealOrComplex Scalar, typename Vector, KrylovMatrixFreeOperator<Vector, Scalar> OpOps,
1307 KrylovMatrixFreeOperator<Vector, Scalar> BOps, typename MetricOps>
1309 OpOps& op_ops, BOps& b_ops, MetricOps& metric_ops, Vector const& initial,
1312{
1313 std::size_t const problem_dimension =
1314 validate_matrix_free_dimensions(op_ops, initial, "generalized restarted symmetric Lanczos");
1315 if (b_ops.problem_dimension() != problem_dimension || b_ops.vector_dimension(initial) != problem_dimension)
1316 {
1317 throw std::invalid_argument("generalized restarted symmetric Lanczos B operation dimension does not match");
1318 }
1319
1320 detail::SymmetricBMetricOps<Scalar, Vector, OpOps, BOps, MetricOps> wrapped_ops(op_ops, b_ops, metric_ops);
1321 return symmetric_lanczos_restarted_transformed<Scalar>(wrapped_ops, initial, params, options);
1322}
1323
1341template <uni20::LapackRealOrComplex Scalar, typename Vector, KrylovMatrixFreeOperator<Vector, Scalar> OpOps,
1342 KrylovMatrixFreeOperator<Vector, Scalar> BOps>
1344 OpOps& op_ops, BOps& b_ops, Vector const& initial, SymmetricEigenParams<uni20::make_real_t<Scalar>> const& params,
1346{
1347 return symmetric_lanczos_restarted_generalized_transformed<Scalar>(op_ops, b_ops, b_ops, initial, params, options);
1348}
1349
1350} // 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
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 make_real< T >::type make_real_t
Alias for the underlying real-valued type of T.
Definition scalar_traits.hpp:168
Definition qnum.hpp:403
Definition cli.cpp:6
Definition block_tensor_vector.hpp:23
EigenResult< uni20::make_real_t< Scalar >, Vector > symmetric_lanczos_standard(Ops &ops, Vector const &initial, SymmetricEigenParams< uni20::make_real_t< Scalar > > const &params)
Run the native symmetric/Hermitian Lanczos full-projection solver.
Definition symmetric_lanczos.hpp:897
EigenResult< uni20::make_real_t< Scalar >, Vector > symmetric_lanczos_restarted_transformed(Ops &op_ops, Vector const &initial, SymmetricEigenParams< uni20::make_real_t< Scalar > > const &params, SymmetricSpectralTransformOptions< uni20::make_real_t< Scalar > > const &options)
Run restarted symmetric Lanczos on a caller-supplied transformed operator.
Definition symmetric_lanczos.hpp:1269
void scal(Scalar const &alpha, std::span< Scalar > vector)
Scale a local dense vector in place.
Definition dense_linalg.hpp:193
int effective_symmetric_krylov_dimension(SymmetricEigenParams< Scalar > const &params, std::size_t problem_dimension)
Return the effective symmetric/Hermitian Krylov subspace dimension.
Definition matrix_free.hpp:225
SymmetricRestartPlan< Scalar > plan_symmetric_restart(SymmetricEigenParams< Scalar > const &params, std::vector< Scalar > const &values, std::vector< Scalar > const &residual_bounds, int converged_count)
Adjust restart dimensions for implicit-restart edge cases.
Definition symmetric_lanczos.hpp:601
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
Scalar map_symmetric_transformed_eigenvalue(Scalar transformed_value, SymmetricSpectralTransformOptions< Scalar > const &options)
Map a native symmetric transformed Ritz value back to the original eigenvalue scale.
Definition symmetric_lanczos.hpp:1220
int default_symmetric_retained_ritz_count(int eigenvalue_count, int krylov_dimension)
Return the default symmetric/Hermitian retained Ritz count.
Definition matrix_free.hpp:248
SpectrumPart
Spectrum selector for matrix-free eigenvalue solvers.
Definition matrix_free.hpp:23
void axpy(std::span< Scalar > y, Scalar const &alpha, std::span< Scalar const > x)
Add a scaled local dense vector to another vector.
Definition dense_linalg.hpp:206
int effective_symmetric_retained_ritz_count(SymmetricEigenParams< Scalar > const &params, int krylov_dimension)
Return the effective symmetric/Hermitian retained Ritz count.
Definition matrix_free.hpp:283
SymmetricQrShiftResult< Scalar > apply_symmetric_qr_shifts(std::vector< Scalar > diagonal, std::vector< Scalar > subdiagonal, std::vector< Scalar > const &shifts)
Apply implicit shifted QR sweeps to a symmetric tridiagonal matrix.
Definition symmetric_lanczos.hpp:670
SymmetricRestartSelection< Scalar > select_symmetric_restart(std::vector< Scalar > const &values, std::vector< Scalar > const &residual_bounds, SymmetricEigenParams< Scalar > const &params, std::size_t shift_count, std::span< std::size_t const > protected_shift_indices={})
Select wanted Ritz values and exact-shift candidates for symmetric restart.
Definition symmetric_lanczos.hpp:512
EigenResult< uni20::make_real_t< Scalar >, Vector > symmetric_lanczos_restarted_generalized_transformed(OpOps &op_ops, BOps &b_ops, MetricOps &metric_ops, Vector const &initial, SymmetricEigenParams< uni20::make_real_t< Scalar > > const &params, SymmetricSpectralTransformOptions< uni20::make_real_t< Scalar > > const &options)
Run restarted symmetric Lanczos on a transformed generalized operator with a B metric.
Definition symmetric_lanczos.hpp:1308
TridiagonalEigensystem< Scalar > symmetric_tridiagonal_eigensystem(std::vector< Scalar > diagonal, std::vector< Scalar > subdiagonal, bool compute_vectors)
Solve a real symmetric tridiagonal eigensystem through LAPACK sterf or steqr.
Definition dense_subspace.hpp:288
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
SymmetricCompressedLanczosFactorization< uni20::make_real_t< Scalar >, Vector > compress_symmetric_lanczos_restart(Ops &ops, std::vector< Vector > const &basis, Vector const &residual, SymmetricQrShiftResult< uni20::make_real_t< Scalar > > const &shifted, std::size_t retained_count)
Compress a Lanczos factorization with the accumulated restart transform.
Definition symmetric_lanczos.hpp:809
void copy(std::span< Scalar > destination, std::span< Scalar const > source)
Copy a local dense vector.
Definition dense_linalg.hpp:183
EigenResult< uni20::make_real_t< Scalar >, Vector > symmetric_lanczos_restarted_standard(Ops &ops, Vector const &initial, SymmetricEigenParams< uni20::make_real_t< Scalar > > const &params)
Run a native symmetric/Hermitian Lanczos solve with implicit restarts.
Definition symmetric_lanczos.hpp:1003
auto values(Tensor &tensor)
Definition packed_matrix.hpp:29
std::optional< DenseMatrix< W > > correction(Backend &backend, LuFactorization< F > const &factor, DenseMatrix< R > const &rhs, LuRefinementPrecision< F, W, R > p, std::vector< bool > const &active, std::int64_t target_exponent, SolveInfo &info)
Definition lu_refine.hpp:93
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::abs_fn abs
Absolute value or complex magnitude, preserving the selected scalar implementation.
Definition math.hpp:459
constexpr detail::scalar_math::real_fn real
Read the real component by value; real and integral scalars retain their type.
Definition math.hpp:671
constexpr detail::scalar_math::imag_fn imag
Read the imaginary component by value; real zero retains the input's type and precision.
Definition math.hpp:673
void inner_product(BackendSelector selector, async::Async< OutputTensor > &output, async::Async< LhsTensor > const &lhs, async::Async< RhsTensor > const &rhs)
Schedule an inner product into an explicit async scalar Tensor.
Definition linalg/async/reductions.hpp:455
mpreal norm(mpcomplex const &z)
Definition mpcomplex.hpp:494
mpreal epsilon(Precision precision)
Spacing above one for a specified binary working precision.
Definition mpreal.hpp:805
mpreal hypot(mpreal const &x, mpreal const &y)
Definition mpreal.hpp:660
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 copysign(mpreal const &magnitude, mpreal const &sign)
Transfer the sign while preserving the magnitude's precision and exactness.
Definition mpreal_functions.hpp:224
void set_zero(Tensor &tensor)
Set every stored numerical block to zero without changing structure.
Definition block_tensor_linear.hpp:369
Matrix-free eigenvalue result with solver-owned vector objects.
Definition matrix_free.hpp:355
int status
Definition matrix_free.hpp:362
int matvec_count
Definition matrix_free.hpp:361
int converged_count
Definition matrix_free.hpp:359
int iteration_count
Definition matrix_free.hpp:360
Compressed symmetric Lanczos factorization after implicit restart.
Definition symmetric_lanczos.hpp:57
std::vector< Scalar > subdiagonal
Definition symmetric_lanczos.hpp:61
std::vector< Scalar > diagonal
Definition symmetric_lanczos.hpp:60
Scalar trailing_coupling
Definition symmetric_lanczos.hpp:64
std::vector< Vector > basis
Definition symmetric_lanczos.hpp:58
Vector residual
Definition symmetric_lanczos.hpp:59
Scalar residual_scale
Definition symmetric_lanczos.hpp:63
Scalar residual_norm
Definition symmetric_lanczos.hpp:62
Common parameters for a symmetric or Hermitian eigenvalue solve.
Definition matrix_free.hpp:157
KrylovDiagnosticsLevel diagnostics
Definition matrix_free.hpp:178
int eigenvalue_count
Number of converged eigenpairs requested by the caller.
Definition matrix_free.hpp:159
bool compute_eigenvectors
Definition matrix_free.hpp:177
SpectrumPart spectrum
Definition matrix_free.hpp:176
Scalar tolerance
Definition matrix_free.hpp:170
int max_iterations
Definition matrix_free.hpp:169
int retained_ritz_count
Optional number of Ritz vectors retained internally after restart.
Definition matrix_free.hpp:166
Optional diagnostic state for symmetric Lanczos solves.
Definition matrix_free.hpp:342
int op_count
Definition matrix_free.hpp:344
Result of applying implicit QR shifts to a symmetric tridiagonal projection.
Definition symmetric_lanczos.hpp:47
uni20::DenseMatrix< Scalar > transform
Definition symmetric_lanczos.hpp:50
std::vector< Scalar > subdiagonal
Definition symmetric_lanczos.hpp:49
std::vector< Scalar > diagonal
Definition symmetric_lanczos.hpp:48
Restart dimensions after implicit-restart edge-case adjustment.
Definition symmetric_lanczos.hpp:36
bool enlarged_for_partial_convergence
Definition symmetric_lanczos.hpp:41
std::size_t shift_count
Definition symmetric_lanczos.hpp:38
std::vector< std::size_t > protected_zero_bound_indices
Definition symmetric_lanczos.hpp:40
std::size_t protected_zero_bound_count
Definition symmetric_lanczos.hpp:39
std::size_t retained_count
Definition symmetric_lanczos.hpp:37
Ritz-value selection state used by symmetric implicit restart logic.
Definition symmetric_lanczos.hpp:25
std::vector< std::size_t > wanted_indices
Definition symmetric_lanczos.hpp:26
std::vector< Scalar > shifts
Definition symmetric_lanczos.hpp:28
int converged_count
Definition symmetric_lanczos.hpp:30
std::vector< std::size_t > shift_indices
Definition symmetric_lanczos.hpp:27
std::vector< bool > wanted_converged
Definition symmetric_lanczos.hpp:29
Native symmetric transform options for mapping OP Ritz values.
Definition matrix_free.hpp:90
SymmetricTransformKind transform
Definition matrix_free.hpp:91
Scalar sigma
Definition matrix_free.hpp:92
Project-level numeric limits customization point.
Definition numeric_limits.hpp:16