Uni20
Asynchronous tensor and runtime library.
Loading...
Searching...
No Matches
krylov_exponential.hpp
Go to the documentation of this file.
1#pragma once
2
8
9#include <algorithm>
10#include <cmath>
11#include <concepts>
12#include <cstddef>
13#include <limits>
14#include <optional>
15#include <stdexcept>
16#include <type_traits>
17#include <utility>
18#include <vector>
19
20namespace uni20::krylov
21{
22
44
68
88
90[[nodiscard]] inline int default_exponential_krylov_dimension(std::size_t problem_dimension)
91{
92 if (problem_dimension > static_cast<std::size_t>(uni20::numeric_limits<int>::max()))
93 {
94 throw std::overflow_error("Krylov exponential problem dimension exceeds the supported int range");
95 }
96 return std::min(static_cast<int>(problem_dimension), 30);
97}
98
100[[nodiscard]] inline int default_adaptive_exponential_krylov_dimension(std::size_t problem_dimension)
101{
102 if (problem_dimension > static_cast<std::size_t>(uni20::numeric_limits<int>::max()))
103 {
104 throw std::overflow_error("Krylov exponential problem dimension exceeds the supported int range");
105 }
106 return std::min(static_cast<int>(problem_dimension), 64);
107}
108
109template <uni20::Real Scalar>
111{
112 return params.relative_tolerance > Scalar{};
113}
114
116template <uni20::Real Scalar>
118 std::size_t problem_dimension)
119{
120 if (params.krylov_dimension > 0)
121 {
122 return params.krylov_dimension;
123 }
126 : default_exponential_krylov_dimension(problem_dimension);
127}
128
129namespace detail
130{
131
132template <uni20::Real Scalar> Scalar exponential_breakdown_tolerance(KrylovExponentialParams<Scalar> const& params)
133{
134 return params.breakdown_tolerance > Scalar{} ? params.breakdown_tolerance
136}
137
138template <uni20::Real Scalar> void validate_krylov_exponential_params(KrylovExponentialParams<Scalar> const& params)
139{
140 if (params.krylov_dimension < 0)
141 {
142 throw std::invalid_argument("Krylov exponential requires a non-negative krylov_dimension");
143 }
144 if (params.minimum_krylov_dimension < 0)
145 {
146 throw std::invalid_argument("Krylov exponential requires a non-negative minimum_krylov_dimension");
147 }
148 if (params.relative_tolerance < Scalar{} || !uni20::isfinite(params.relative_tolerance))
149 {
150 throw std::invalid_argument("Krylov exponential requires a finite non-negative relative_tolerance");
151 }
152 if (params.estimate_safety_factor < Scalar{1} || !uni20::isfinite(params.estimate_safety_factor))
153 {
154 throw std::invalid_argument("Krylov exponential requires estimate_safety_factor >= 1");
155 }
156 if (params.breakdown_tolerance < Scalar{} || !uni20::isfinite(params.breakdown_tolerance))
157 {
158 throw std::invalid_argument("Krylov exponential requires a finite non-negative breakdown_tolerance");
159 }
160}
161
162template <uni20::Real Scalar>
163[[nodiscard]] int effective_exponential_minimum_krylov_dimension(KrylovExponentialParams<Scalar> const& params,
164 int basis_limit)
165{
166 int const minimum = params.minimum_krylov_dimension > 0 ? params.minimum_krylov_dimension : 1;
167 if (minimum > basis_limit)
168 {
169 throw std::invalid_argument("Krylov exponential requires minimum_krylov_dimension <= krylov_dimension");
170 }
171 return minimum;
172}
173
174template <uni20::Real Scalar>
175[[nodiscard]] Scalar hermitian_exponential_target_error(KrylovExponentialParams<Scalar> const& params,
176 Scalar initial_norm)
177{
179 {
180 return Scalar{};
181 }
182 Scalar const target = params.relative_tolerance * initial_norm;
183 if (!uni20::isfinite(target))
184 {
185 throw std::overflow_error("Krylov exponential relative tolerance target overflowed");
186 }
187 return target;
188}
189
190template <uni20::Real Scalar>
191[[nodiscard]] bool exponential_estimate_satisfies_target(Scalar estimate, Scalar target_error,
192 KrylovExponentialParams<Scalar> const& params)
193{
195 {
196 return true;
197 }
198 return estimate <= target_error / params.estimate_safety_factor;
199}
200
201template <uni20::Real Real, typename TimeScalar> uni20::complex<Real> exponential_time_as_complex(TimeScalar time)
202{
203 if constexpr (uni20::Complex<TimeScalar>)
204 {
205 return static_cast<uni20::complex<Real>>(time);
206 }
207 else
208 {
209 return uni20::complex<Real>{static_cast<Real>(time), Real{}};
210 }
211}
212
213template <uni20::Real Real, typename TimeScalar> bool hermitian_time_is_unitary(TimeScalar time)
214{
215 uni20::complex<Real> const complex_time = exponential_time_as_complex<Real>(time);
216 Real const magnitude = static_cast<Real>(uni20::math::abs(complex_time));
217 Real const real_part = static_cast<Real>(uni20::math::abs(uni20::math::real(complex_time)));
218 Real const tolerance = Real{100} * uni20::numeric_limits<Real>::epsilon() * std::max(Real{1}, magnitude);
219 return real_part <= tolerance;
220}
221
222template <uni20::Real Real, typename TimeScalar>
223void validate_hermitian_adaptive_error_model(KrylovExponentialParams<Real> const& params, TimeScalar time)
224{
226 {
227 return;
228 }
229 if (params.assume_nonexpansive || hermitian_time_is_unitary<Real>(time))
230 {
231 return;
232 }
233 throw std::invalid_argument(
234 "Hermitian Krylov adaptive tolerance requires a nonexpansive action; use pure imaginary time or set "
235 "assume_nonexpansive when the caller has verified the model");
236}
237
238template <uni20::RealOrComplex Scalar, typename Vector> struct HermitianExponentialProjection
239{
240 std::vector<Vector> basis;
247 uni20::make_real_t<Scalar> max_reorthogonalization_correction = uni20::make_real_t<Scalar>{};
248 uni20::make_real_t<Scalar> max_reorthogonalization_correction_ratio = uni20::make_real_t<Scalar>{};
249 int max_reorthogonalization_passes = 0;
250 int op_count = 0;
251 bool happy_breakdown = false;
252};
253
254template <uni20::Real Real>
255uni20::DenseMatrix<Real> make_hermitian_projected_tridiagonal(std::vector<Real> const& diagonal,
256 std::vector<Real> const& subdiagonal)
257{
258 if (diagonal.size() > 0 && subdiagonal.size() + 1 != diagonal.size())
259 {
260 throw std::invalid_argument("Hermitian Krylov tridiagonal data have inconsistent sizes");
261 }
262 uni20::DenseMatrix<Real> projected(diagonal.size(), diagonal.size());
263 laset(projected, Real{}, Real{}, MatrixFill::All);
264 for (std::size_t i = 0; i < diagonal.size(); ++i)
265 {
266 projected[i, i] = diagonal[i];
267 if (i + 1 < diagonal.size())
268 {
269 projected[i, i + 1] = subdiagonal[i];
270 projected[i + 1, i] = subdiagonal[i];
271 }
272 }
273 return projected;
274}
275
276template <uni20::RealOrComplex Scalar, typename Vector, KrylovMatrixFreeOperator<Vector, Scalar> Ops>
277void record_exponential_basis_orthogonality(Ops& ops, HermitianExponentialProjection<Scalar, Vector>& projection)
278{
280 Real frobenius_squared{};
281 for (std::size_t row = 0; row < projection.basis.size(); ++row)
282 {
283 for (std::size_t col = row; col < projection.basis.size(); ++col)
284 {
285 Scalar const entry = ops.inner_product(projection.basis[row], projection.basis[col]);
286 if (row == col)
287 {
288 Real const error = static_cast<Real>(uni20::math::abs(entry - Scalar{1}));
289 projection.basis_max_diag_error = std::max(projection.basis_max_diag_error, error);
290 frobenius_squared += error * error;
291 }
292 else
293 {
294 Real const magnitude = static_cast<Real>(uni20::math::abs(entry));
295 projection.basis_max_offdiag = std::max(projection.basis_max_offdiag, magnitude);
296 frobenius_squared += Real{2} * magnitude * magnitude;
297 }
298 }
299 }
300 projection.basis_frobenius_error = static_cast<Real>(uni20::math::sqrt(frobenius_squared));
301}
302
303template <uni20::RealOrComplex Scalar, typename Vector, KrylovMatrixFreeOperator<Vector, Scalar> Ops,
304 typename StopPredicate>
305HermitianExponentialProjection<Scalar, Vector>
306hermitian_exponential_projection(Ops& ops, Vector const& initial,
307 KrylovExponentialParams<uni20::make_real_t<Scalar>> const& params, int basis_limit,
308 StopPredicate&& stop)
309{
311 std::size_t const problem_dimension = validate_matrix_free_dimensions(ops, initial, "Hermitian Krylov exponential");
312 if (basis_limit <= 0)
313 {
314 throw std::invalid_argument("Hermitian Krylov exponential requires a positive Krylov dimension");
315 }
316 if (basis_limit > static_cast<int>(problem_dimension))
317 {
318 throw std::invalid_argument("Hermitian Krylov exponential requires krylov_dimension <= problem dimension");
319 }
320
321 Real const breakdown_tolerance = exponential_breakdown_tolerance(params);
322
323 HermitianExponentialProjection<Scalar, Vector> result;
324 result.basis.reserve(static_cast<std::size_t>(basis_limit));
325 std::vector<Real> diagonal;
326 std::vector<Real> subdiagonal;
327 diagonal.reserve(static_cast<std::size_t>(basis_limit));
328 subdiagonal.reserve(static_cast<std::size_t>(std::max(0, basis_limit - 1)));
329 LanczosOrthogonalizationDiagnostics<Real> orthogonalization_diagnostics;
330 auto* orthogonalization_diagnostics_ptr =
331 params.diagnostics == KrylovDiagnosticsLevel::None ? nullptr : &orthogonalization_diagnostics;
332
333 Vector start = ops.allocate_like(initial);
334 ops.copy(start, initial);
335 result.initial_norm = norm_or_inner_product<Scalar>(ops, start);
336 if (result.initial_norm == Real{})
337 {
338 ops.set_zero(start);
339 result.projected = uni20::DenseMatrix<Real>();
340 result.happy_breakdown = true;
341 return result;
342 }
343 ops.scal(start, Scalar{1} / static_cast<Scalar>(result.initial_norm));
344 result.basis.push_back(std::move(start));
345
346 for (int step = 0; step < basis_limit; ++step)
347 {
348 Vector residual = ops.allocate_like(result.basis[static_cast<std::size_t>(step)]);
349 ops.matvec(residual, result.basis[static_cast<std::size_t>(step)]);
350 ++result.op_count;
351
352 if (step > 0)
353 {
354 ops.axpy(residual, -static_cast<Scalar>(subdiagonal[static_cast<std::size_t>(step - 1)]),
355 result.basis[static_cast<std::size_t>(step - 1)]);
356 }
357
358 Real const projection_scale = detail::hermitian_projection_scale<Scalar>(ops, residual);
359 Real const alpha = detail::hermitian_projection_scalar(
360 ops.inner_product(result.basis[static_cast<std::size_t>(step)], residual), projection_scale);
361 diagonal.push_back(alpha);
362 ops.axpy(residual, -static_cast<Scalar>(alpha), result.basis[static_cast<std::size_t>(step)]);
363
364 Real const beta =
365 detail::orthogonalize_lanczos_residual<Scalar>(ops, result.basis, residual, orthogonalization_diagnostics_ptr);
366 result.residual_norm = beta;
367 bool const happy_breakdown = beta <= breakdown_tolerance;
368 bool const accepted = stop(result.initial_norm, diagonal, subdiagonal, beta, happy_breakdown);
369 if (accepted || step + 1 == basis_limit || happy_breakdown)
370 {
371 result.happy_breakdown = happy_breakdown;
372 break;
373 }
374
375 ops.scal(residual, Scalar{1} / static_cast<Scalar>(beta));
376 subdiagonal.push_back(beta);
377 result.basis.push_back(std::move(residual));
378 }
379
380 result.projected = make_hermitian_projected_tridiagonal(diagonal, subdiagonal);
381 if (params.diagnostics != KrylovDiagnosticsLevel::None)
382 {
383 result.max_reorthogonalization_correction = orthogonalization_diagnostics.max_reorthogonalization_correction;
384 result.max_reorthogonalization_correction_ratio =
385 orthogonalization_diagnostics.max_reorthogonalization_correction_ratio;
386 result.max_reorthogonalization_passes = orthogonalization_diagnostics.max_reorthogonalization_passes;
387 record_exponential_basis_orthogonality<Scalar>(ops, result);
388 }
389 return result;
390}
391
392template <uni20::RealOrComplex Scalar, typename Vector, KrylovMatrixFreeOperator<Vector, Scalar> Ops>
393HermitianExponentialProjection<Scalar, Vector>
394hermitian_exponential_projection(Ops& ops, Vector const& initial,
395 KrylovExponentialParams<uni20::make_real_t<Scalar>> const& params, int basis_limit)
396{
397 auto never_stop = [](uni20::make_real_t<Scalar>, std::vector<uni20::make_real_t<Scalar>> const&,
398 std::vector<uni20::make_real_t<Scalar>> const&, uni20::make_real_t<Scalar>,
399 bool) static { return false; };
400 return hermitian_exponential_projection<Scalar>(ops, initial, params, basis_limit, never_stop);
401}
402
403template <uni20::RealOrComplex Scalar, typename Vector, KrylovMatrixFreeOperator<Vector, Scalar> Ops>
404Vector combine_exponential_basis(Ops& ops, Vector const& prototype, std::vector<Vector> const& basis,
405 std::vector<Scalar> const& coefficients, std::size_t basis_count)
406{
407 if (basis.size() < basis_count || basis_count != coefficients.size())
408 {
409 throw std::invalid_argument("Krylov exponential basis and coefficient sizes do not match");
410 }
411
412 Vector action = ops.allocate_like(prototype);
413 ops.set_zero(action);
414 for (std::size_t i = 0; i < basis_count; ++i)
415 {
416 if (coefficients[i] != Scalar{})
417 {
418 ops.axpy(action, coefficients[i], basis[i]);
419 }
420 }
421 return action;
422}
423
424template <typename Scalar>
425std::vector<Scalar> first_column_scaled(uni20::DenseMatrix<Scalar> const& exponential, Scalar scale)
426{
427 std::vector<Scalar> coefficients(static_cast<std::size_t>(exponential.rows()));
428 for (uni20::index_type row = 0; row < exponential.rows(); ++row)
429 {
430 coefficients[row] = scale * exponential[row, 0];
431 }
432 return coefficients;
433}
434
435template <uni20::RealOrComplex Scalar>
436std::vector<Scalar> first_real_column_scaled(uni20::DenseMatrix<uni20::make_real_t<Scalar>> const& exponential,
438{
439 std::vector<Scalar> coefficients(static_cast<std::size_t>(exponential.rows()));
440 for (uni20::index_type row = 0; row < exponential.rows(); ++row)
441 {
442 coefficients[row] = static_cast<Scalar>(scale * exponential[row, 0]);
443 }
444 return coefficients;
445}
446
447template <uni20::RealOrComplex Scalar>
448uni20::make_real_t<Scalar> endpoint_defect_estimate(uni20::make_real_t<Scalar> residual_norm,
449 std::vector<Scalar> const& coefficients)
450{
451 // Final-time defect estimate h_{m+1,m} |e_m^* exp(t H_m) e_1| scaled by
452 // ||v|| through the stored coefficients. See docs/krylov/exponential_estimators.md:
453 // [BotchevGrimmHochbruck2013] for the residual/defect view and [JiaLv2015]
454 // for related a posteriori estimates.
455 if (coefficients.empty())
456 {
458 }
459 return residual_norm * static_cast<uni20::make_real_t<Scalar>>(uni20::math::abs(coefficients.back()));
460}
461
462template <uni20::Real Real>
463uni20::complex<Real> projected_defect_delta(TridiagonalEigensystem<Real> const& eigensystem, uni20::complex<Real> time,
464 Real theta)
465{
467 std::size_t const dimension = eigensystem.eigenvalues.size();
468 Complex result{};
469 for (std::size_t col = 0; col < dimension; ++col)
470 {
471 Real const weight = eigensystem.eigenvectors[dimension - 1, col] * eigensystem.eigenvectors[0, col];
472 result += Complex{weight, Real{}} * uni20::math::exp(time * (theta * eigensystem.eigenvalues[col]));
473 }
474 return result;
475}
476
477template <uni20::Real Real, typename TimeScalar>
478Real hermitian_projected_defect_integral_estimate(uni20::DenseMatrix<Real> const& projected, Real initial_norm,
479 Real residual_norm, TimeScalar time)
480{
481 if (projected.rows() != projected.cols())
482 {
483 throw std::invalid_argument("Hermitian Krylov defect integral requires a square projected matrix");
484 }
485 if (projected.rows() == 0 || initial_norm == Real{} || residual_norm == Real{})
486 {
487 return Real{};
488 }
489
490 uni20::complex<Real> const complex_time = exponential_time_as_complex<Real>(time);
491 Real const time_magnitude = static_cast<Real>(uni20::math::abs(complex_time));
492 if (time_magnitude == Real{})
493 {
494 return Real{};
495 }
496
497 std::size_t const dimension = static_cast<std::size_t>(projected.rows());
498 std::vector<Real> diagonal(dimension);
499 std::vector<Real> subdiagonal(dimension > 0 ? dimension - 1 : 0);
500 for (std::size_t i = 0; i < dimension; ++i)
501 {
502 diagonal[i] = projected[i, i];
503 if (i + 1 < dimension)
504 {
505 subdiagonal[i] = projected[i + 1, i];
506 }
507 }
508
509 auto const eigensystem = symmetric_tridiagonal_eigensystem(std::move(diagonal), std::move(subdiagonal), true);
510
511 // [JaweckiAuzingerKoch2020] writes the Krylov error through the defect
512 // integral L_m(t)v = int_0^t exp((t-s)A) D_m(s)v ds.
513 // For the unitary Hermitian case used by TDVP, ||exp((t-s)A)|| = 1 after
514 // folding the -i into the time coefficient, so the global action error is
515 // bounded by ||v|| h_{m+1,m} int_0^|t| |delta_m(s)| ds. We integrate the
516 // projected scalar delta over theta in [0, 1]. The projected matrix is tiny
517 // compared with a tensor-network matvec, so a deterministic 1024-panel
518 // Simpson rule is a deliberate simple policy rather than a performance risk.
519 constexpr int panel_count = 1024;
520 static_assert(panel_count % 2 == 0);
521
522 Real weighted_sum = static_cast<Real>(uni20::math::abs(projected_defect_delta(eigensystem, complex_time, Real{}))) +
523 static_cast<Real>(uni20::math::abs(projected_defect_delta(eigensystem, complex_time, Real{1})));
524 for (int panel = 1; panel < panel_count; ++panel)
525 {
526 Real const theta = static_cast<Real>(panel) / static_cast<Real>(panel_count);
527 Real const weight = panel % 2 == 0 ? Real{2} : Real{4};
528 weighted_sum +=
529 weight * static_cast<Real>(uni20::math::abs(projected_defect_delta(eigensystem, complex_time, theta)));
530 }
531
532 Real const integral = weighted_sum / (Real{3} * static_cast<Real>(panel_count));
533 return initial_norm * residual_norm * time_magnitude * integral;
534}
535
536template <typename TimeScalar, uni20::LapackScalar OutputScalar, typename InputScalar>
537 requires std::constructible_from<OutputScalar, TimeScalar>
538uni20::DenseMatrix<OutputScalar> projected_exponential(uni20::DenseMatrix<InputScalar> const& projected,
539 TimeScalar time)
540{
541 uni20::DenseMatrix<OutputScalar> scaled(projected.rows(), projected.cols());
542 OutputScalar const coefficient = static_cast<OutputScalar>(time);
543 for (uni20::index_type row = 0; row < projected.rows(); ++row)
544 {
545 for (uni20::index_type col = 0; col < projected.cols(); ++col)
546 {
547 scaled[row, col] = coefficient * static_cast<OutputScalar>(projected[row, col]);
548 }
549 }
550 uni20::DenseMatrix<OutputScalar> result(projected.rows(), projected.cols());
552 return result;
553}
554
555template <uni20::RealOrComplex Scalar, typename Vector>
556KrylovExponentialDiagnostics<uni20::make_real_t<Scalar>>
557make_exponential_diagnostics(KrylovExponentialResult<Scalar, Vector> const& result)
558{
559 return KrylovExponentialDiagnostics<uni20::make_real_t<Scalar>>{
560 .op_count = result.matvec_count,
561 .projected_dimension = result.projected_dimension,
562 .initial_norm = result.initial_norm,
563 .final_residual_norm = result.final_residual_norm,
564 .target_error = result.target_error,
565 .error_estimate = result.error_estimate,
566 .endpoint_defect_estimate = result.endpoint_defect_estimate,
567 .defect_integral_estimate = result.defect_integral_estimate,
568 .converged = result.converged};
569}
570
571template <uni20::RealOrComplex Scalar, typename Vector>
572void attach_exponential_diagnostics(KrylovExponentialResult<Scalar, Vector>& result, KrylovDiagnosticsLevel level,
573 KrylovExponentialDiagnostics<uni20::make_real_t<Scalar>> diagnostics)
574{
575 if (level == KrylovDiagnosticsLevel::None)
576 {
577 return;
578 }
579
580 result.diagnostics = diagnostics;
581}
582
583template <uni20::RealOrComplex Scalar, typename Vector>
584void attach_exponential_diagnostics(KrylovExponentialResult<Scalar, Vector>& result, KrylovDiagnosticsLevel level)
585{
586 attach_exponential_diagnostics(result, level, make_exponential_diagnostics(result));
587}
588
589template <uni20::LapackScalar Scalar, typename TimeScalar, typename Vector,
590 KrylovMatrixFreeOperator<Vector, Scalar> Ops>
591KrylovExponentialResult<Scalar, Vector>
592make_hermitian_exponential_result(Ops& ops, Vector const& initial,
593 HermitianExponentialProjection<Scalar, Vector> const& projection, TimeScalar time,
594 KrylovExponentialParams<uni20::make_real_t<Scalar>> const& params)
595{
597 using ProjectedScalar = std::conditional_t<uni20::Complex<Scalar>, Scalar, Real>;
598
599 uni20::DenseMatrix<ProjectedScalar> const exponential =
600 projected_exponential<TimeScalar, ProjectedScalar>(projection.projected, time);
601 std::vector<Scalar> const coefficients =
602 first_column_scaled(exponential, static_cast<ProjectedScalar>(projection.initial_norm));
603 Vector action = combine_exponential_basis<Scalar>(ops, initial, projection.basis, coefficients, coefficients.size());
604 Real const endpoint_defect = endpoint_defect_estimate<Scalar>(projection.residual_norm, coefficients);
605 Real const defect_integral = hermitian_projected_defect_integral_estimate(
606 projection.projected, projection.initial_norm, projection.residual_norm, time);
607 Real const target_error = hermitian_exponential_target_error(params, projection.initial_norm);
608 bool const converged =
609 projection.happy_breakdown || exponential_estimate_satisfies_target(defect_integral, target_error, params);
610
611 KrylovExponentialResult<Scalar, Vector> result{.action = std::move(action),
612 .error_estimate = defect_integral,
613 .endpoint_defect_estimate = endpoint_defect,
614 .defect_integral_estimate = defect_integral,
615 .initial_norm = projection.initial_norm,
616 .final_residual_norm = projection.residual_norm,
617 .target_error = target_error,
618 .projected_dimension = static_cast<int>(projection.projected.rows()),
619 .matvec_count = projection.op_count,
620 .converged = converged,
621 .happy_breakdown = projection.happy_breakdown,
622 .diagnostics = std::nullopt};
623 auto diagnostics = make_exponential_diagnostics(result);
624 diagnostics.basis_max_diag_error = projection.basis_max_diag_error;
625 diagnostics.basis_max_offdiag = projection.basis_max_offdiag;
626 diagnostics.basis_frobenius_error = projection.basis_frobenius_error;
627 diagnostics.max_reorthogonalization_correction = projection.max_reorthogonalization_correction;
628 diagnostics.max_reorthogonalization_correction_ratio = projection.max_reorthogonalization_correction_ratio;
629 diagnostics.max_reorthogonalization_passes = projection.max_reorthogonalization_passes;
630 attach_exponential_diagnostics(result, params.diagnostics, diagnostics);
631 return result;
632}
633
634} // namespace detail
635
654template <uni20::LapackScalar Scalar, typename TimeScalar, typename Vector,
655 KrylovMatrixFreeOperator<Vector, Scalar> Ops>
656 requires(std::constructible_from<uni20::make_real_t<Scalar>, TimeScalar> ||
657 (uni20::Complex<Scalar> && std::constructible_from<Scalar, TimeScalar>))
658KrylovExponentialResult<Scalar, Vector>
659hermitian_krylov_exponential_action(Ops& ops, Vector const& initial, TimeScalar time,
660 KrylovExponentialParams<uni20::make_real_t<Scalar>> const& params = {})
661{
663
664 detail::validate_krylov_exponential_params(params);
665 detail::validate_hermitian_adaptive_error_model(params, time);
666 std::size_t const problem_dimension = validate_matrix_free_dimensions(ops, initial, "Hermitian Krylov exponential");
667 int const basis_limit = effective_exponential_krylov_dimension(params, problem_dimension);
668 int const minimum_dimension = detail::effective_exponential_minimum_krylov_dimension(params, basis_limit);
669 bool const adaptive = exponential_adaptive_acceptance_enabled(params);
670
671 auto adaptive_stop = [&](Real initial_norm, std::vector<Real> const& diagonal, std::vector<Real> const& subdiagonal,
672 Real residual_norm, bool happy_breakdown) {
673 if (!adaptive || happy_breakdown || static_cast<int>(diagonal.size()) < minimum_dimension)
674 {
675 return false;
676 }
677 uni20::DenseMatrix<Real> const projected = detail::make_hermitian_projected_tridiagonal(diagonal, subdiagonal);
678 Real const estimate =
679 detail::hermitian_projected_defect_integral_estimate(projected, initial_norm, residual_norm, time);
680 Real const target_error = detail::hermitian_exponential_target_error(params, initial_norm);
681 return detail::exponential_estimate_satisfies_target(estimate, target_error, params);
682 };
683
684 auto projection = detail::hermitian_exponential_projection<Scalar>(ops, initial, params, basis_limit, adaptive_stop);
685 if (projection.initial_norm == Real{})
686 {
687 Vector action = ops.allocate_like(initial);
688 ops.set_zero(action);
689 KrylovExponentialResult<Scalar, Vector> result{.action = std::move(action),
690 .initial_norm = projection.initial_norm,
691 .final_residual_norm = projection.residual_norm,
692 .target_error = Real{},
693 .projected_dimension = 0,
694 .matvec_count = projection.op_count,
695 .converged = true,
696 .happy_breakdown = true,
697 .diagnostics = std::nullopt};
698 detail::attach_exponential_diagnostics(result, params.diagnostics);
699 return result;
700 }
701
702 auto result = detail::make_hermitian_exponential_result<Scalar>(ops, initial, projection, time, params);
703 if (adaptive && !result.converged && params.throw_on_nonconvergence)
704 {
705 throw std::runtime_error("Hermitian Krylov exponential did not satisfy the requested relative_tolerance");
706 }
707 return result;
708}
709
720template <uni20::LapackScalar Scalar, typename TimeScalar, typename Vector,
721 KrylovMatrixFreeOperator<Vector, Scalar> Ops>
722 requires(std::constructible_from<uni20::make_real_t<Scalar>, TimeScalar> ||
723 (uni20::Complex<Scalar> && std::constructible_from<Scalar, TimeScalar>))
724KrylovExponentialResult<Scalar, Vector>
725nonsymmetric_krylov_exponential_action(Ops& ops, Vector const& initial, TimeScalar time,
726 KrylovExponentialParams<uni20::make_real_t<Scalar>> const& params = {})
727{
729
730 detail::validate_krylov_exponential_params(params);
732 {
733 throw std::invalid_argument("Nonsymmetric Krylov exponential adaptive relative_tolerance is not implemented yet");
734 }
735
736 std::size_t const problem_dimension =
737 validate_matrix_free_dimensions(ops, initial, "nonsymmetric Krylov exponential");
738 int const basis_limit = effective_exponential_krylov_dimension(params, problem_dimension);
739 if (basis_limit <= 0)
740 {
741 throw std::invalid_argument("nonsymmetric Krylov exponential requires a positive Krylov dimension");
742 }
743 if (basis_limit > static_cast<int>(problem_dimension))
744 {
745 throw std::invalid_argument("nonsymmetric Krylov exponential requires krylov_dimension <= problem dimension");
746 }
747
748 Vector start = ops.allocate_like(initial);
749 ops.copy(start, initial);
750 Real const initial_norm = norm_or_inner_product<Scalar>(ops, start);
751 if (initial_norm == Real{})
752 {
753 ops.set_zero(start);
754 KrylovExponentialResult<Scalar, Vector> result{.action = std::move(start),
755 .initial_norm = initial_norm,
756 .final_residual_norm = Real{},
757 .target_error = Real{},
758 .projected_dimension = 0,
759 .converged = true,
760 .happy_breakdown = true,
761 .diagnostics = std::nullopt};
762 detail::attach_exponential_diagnostics(result, params.diagnostics);
763 return result;
764 }
765
766 ArnoldiFactorization<Scalar, Vector> factorization =
767 arnoldi_factorize<Scalar>(ops, initial, basis_limit, detail::exponential_breakdown_tolerance(params));
768 uni20::DenseMatrix<Scalar> const projected = arnoldi_projected_hessenberg(factorization);
769 uni20::DenseMatrix<Scalar> const exponential = detail::projected_exponential<TimeScalar, Scalar>(projected, time);
770 std::vector<Scalar> const coefficients = detail::first_column_scaled(exponential, static_cast<Scalar>(initial_norm));
771 Vector action =
772 detail::combine_exponential_basis<Scalar>(ops, initial, factorization.basis, coefficients, coefficients.size());
773 Real const endpoint_defect = detail::endpoint_defect_estimate<Scalar>(factorization.residual_norm, coefficients);
774
775 KrylovExponentialResult<Scalar, Vector> result{.action = std::move(action),
776 .error_estimate = endpoint_defect,
777 .endpoint_defect_estimate = endpoint_defect,
778 .initial_norm = initial_norm,
779 .final_residual_norm = factorization.residual_norm,
780 .target_error = Real{},
781 .projected_dimension = factorization.step_count,
782 .matvec_count = factorization.op_count,
783 .converged = true,
784 .happy_breakdown = factorization.happy_breakdown,
785 .diagnostics = std::nullopt};
786 detail::attach_exponential_diagnostics(result, params.diagnostics);
787 return result;
788}
789
790} // 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 scalar types with a configured dense LAPACK backend.
Definition scalar_concepts.hpp:147
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
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
constexpr bool isfinite(I const &x) noexcept
Returns whether an integer scalar is finite.
Definition math.hpp:121
action
The executable decides whether to calculate or render an informational/error result.
Definition cli.hpp:23
DiagnosticMagnitude< R > magnitude(R const &value)
Definition gmres_diagnostics.hpp:98
Definition block_tensor_vector.hpp:23
KrylovDiagnosticsLevel
Amount of solver-internal diagnostic state to retain.
Definition matrix_free.hpp:37
KrylovExponentialResult< Scalar, Vector > nonsymmetric_krylov_exponential_action(Ops &ops, Vector const &initial, TimeScalar time, KrylovExponentialParams< uni20::make_real_t< Scalar > > const &params={})
Approximate exp(t A) v for a nonsymmetric matrix-free operator.
Definition krylov_exponential.hpp:725
bool exponential_adaptive_acceptance_enabled(KrylovExponentialParams< Scalar > const &params)
Definition krylov_exponential.hpp:110
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
int default_adaptive_exponential_krylov_dimension(std::size_t problem_dimension)
Return the default adaptive Krylov projection cap for exponential actions.
Definition krylov_exponential.hpp:100
int default_exponential_krylov_dimension(std::size_t problem_dimension)
Return the default Krylov projection dimension for exponential actions.
Definition krylov_exponential.hpp:90
int effective_exponential_krylov_dimension(KrylovExponentialParams< Scalar > const &params, std::size_t problem_dimension)
Return the effective Krylov projection dimension for exponential actions.
Definition krylov_exponential.hpp:117
KrylovExponentialResult< Scalar, Vector > hermitian_krylov_exponential_action(Ops &ops, Vector const &initial, TimeScalar time, KrylovExponentialParams< uni20::make_real_t< Scalar > > const &params={})
Approximate exp(t A) v for a Hermitian matrix-free operator.
Definition krylov_exponential.hpp:659
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
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
void matrix_exponential(BackendSelector selector, async::Async< OutputTensor > &output, async::Async< InputTensor > const &input, TimeScalar &&time)
Schedule fixed-output matrix exponentiation through an explicit selector.
Definition async/matrix_exponential.hpp:64
constexpr detail::scalar_math::exp_fn exp
Natural exponential with scalar-specific precision handling.
Definition math.hpp:465
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
constexpr detail::scalar_math::real_fn real
Read the real component by value; real and integral scalars retain their type.
Definition math.hpp:671
Definition async/async.hpp:26
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
Optional diagnostic state for Krylov exponential actions.
Definition krylov_exponential.hpp:48
Scalar target_error
Definition krylov_exponential.hpp:53
Scalar basis_max_offdiag
Definition krylov_exponential.hpp:61
int projected_dimension
Definition krylov_exponential.hpp:50
int op_count
Definition krylov_exponential.hpp:49
Scalar defect_integral_estimate
Time-integrated Hermitian/Lanczos defect estimate; zero for nonsymmetric Arnoldi actions.
Definition krylov_exponential.hpp:59
Scalar error_estimate
Best available action error estimate for the selected Krylov path.
Definition krylov_exponential.hpp:55
Scalar final_residual_norm
Definition krylov_exponential.hpp:52
int max_reorthogonalization_passes
Definition krylov_exponential.hpp:65
Scalar endpoint_defect_estimate
Final-time defect estimate ||v|| h_{m+1,m} |e_m^* exp(t H_m) e_1|.
Definition krylov_exponential.hpp:57
Scalar max_reorthogonalization_correction
Definition krylov_exponential.hpp:63
Scalar basis_frobenius_error
Definition krylov_exponential.hpp:62
Scalar basis_max_diag_error
Definition krylov_exponential.hpp:60
Scalar initial_norm
Definition krylov_exponential.hpp:51
bool converged
Definition krylov_exponential.hpp:66
Scalar max_reorthogonalization_correction_ratio
Definition krylov_exponential.hpp:64
Parameters for Krylov exponential actions.
Definition krylov_exponential.hpp:26
int krylov_dimension
Fixed projection dimension, or adaptive maximum when relative_tolerance > 0.
Definition krylov_exponential.hpp:28
Scalar estimate_safety_factor
Multiplier applied to the error estimate before comparing with the target.
Definition krylov_exponential.hpp:34
bool assume_nonexpansive
Permit adaptive acceptance for Hermitian actions not known automatically to be unitary.
Definition krylov_exponential.hpp:38
bool throw_on_nonconvergence
Throw if adaptive Hermitian acceptance does not reach the requested tolerance.
Definition krylov_exponential.hpp:40
Scalar breakdown_tolerance
Absolute threshold for invariant-subspace breakdown; zero selects a machine default.
Definition krylov_exponential.hpp:36
Scalar relative_tolerance
Relative error target for nonexpansive Hermitian actions; zero keeps fixed-dimension mode.
Definition krylov_exponential.hpp:32
KrylovDiagnosticsLevel diagnostics
Optional diagnostic retention level.
Definition krylov_exponential.hpp:42
int minimum_krylov_dimension
Minimum projection dimension before adaptive acceptance; zero selects 1.
Definition krylov_exponential.hpp:30
Matrix-free approximation to exp(t A) v.
Definition krylov_exponential.hpp:73
uni20::make_real_t< Scalar > endpoint_defect_estimate
Definition krylov_exponential.hpp:76
Vector action
Definition krylov_exponential.hpp:74
uni20::make_real_t< Scalar > initial_norm
Definition krylov_exponential.hpp:79
uni20::make_real_t< Scalar > error_estimate
Definition krylov_exponential.hpp:75
uni20::make_real_t< Scalar > defect_integral_estimate
Time-integrated Hermitian/Lanczos defect estimate; zero for nonsymmetric Arnoldi actions.
Definition krylov_exponential.hpp:78
std::optional< KrylovExponentialDiagnostics< uni20::make_real_t< Scalar > > > diagnostics
Definition krylov_exponential.hpp:86
uni20::make_real_t< Scalar > final_residual_norm
Definition krylov_exponential.hpp:80
bool converged
Definition krylov_exponential.hpp:84
int matvec_count
Definition krylov_exponential.hpp:83
uni20::make_real_t< Scalar > target_error
Definition krylov_exponential.hpp:81
int projected_dimension
Definition krylov_exponential.hpp:82
bool happy_breakdown
Definition krylov_exponential.hpp:85
Project-level numeric limits customization point.
Definition numeric_limits.hpp:16