Loading...
Searching...
No Matches
fgmres.hpp
Go to the documentation of this file.
1#pragma once
2
4#include "identity_solver.hpp"
9#include "util/table.hpp"
10#include "util/timer.hpp"
11
12namespace terra::linalg::solvers {
13
14template < typename ScalarType >
16{
17 /// @brief Number of inner iterations before restart (FGMRES(m)).
18 int restart = 30;
19
20 /// @brief Relative residual tolerance for convergence.
21 ScalarType relative_residual_tolerance = 1e-6;
22
23 /// @brief Absolute residual tolerance for convergence.
24 ScalarType absolute_residual_tolerance = 1e-6;
25
26 /// @brief Maximum number of total iterations (across all restarts).
27 int max_iterations = 100;
28};
29
30/// @brief Flexible GMRES (FGMRES) iterative solver for nonsymmetric linear systems.
31///
32/// FGMRES allows for varying (flexible) preconditioning at each iteration,
33/// making it suitable for use with inexact or variable preconditioners.
34///
35/// Reference:
36/// @code
37/// Saad, Y. (1993).
38/// A flexible inner-outer preconditioned GMRES algorithm.
39/// SIAM Journal on Scientific Computing, 14(2), 461-469.
40/// @endcode
41///
42/// Satisfies the SolverLike concept (see solver.hpp).
43/// Supports optional right preconditioning via the flexible framework.
44///
45/// @tparam OperatorT Operator type (must satisfy OperatorLike).
46/// @tparam PreconditionerT Preconditioner type (must satisfy SolverLike, defaults to IdentitySolver).
47template < OperatorLike OperatorT, SolverLike PreconditionerT = IdentitySolver< OperatorT > >
48class FGMRES
49{
50 public:
51 /// @brief Operator type to be solved.
52 using OperatorType = OperatorT;
53
54 /// @brief Solution vector type.
56
57 /// @brief Right-hand side vector type.
59
60 /// @brief Scalar type for computations.
61 using ScalarType = typename SolutionVectorType::ScalarType;
62
63 /// @brief Construct an FGMRES solver with a custom preconditioner.
64 ///
65 /// @param tmp Temporary vectors for workspace. Must contain at least 2*restart + 4 vectors:
66 /// - tmp[0]: residual vector r
67 /// - tmp[1 .. restart+1]: Arnoldi basis vectors V_0..V_restart
68 /// - tmp[restart+2 .. 2*restart+1]: preconditioned directions Z_0..Z_{restart-1}
69 /// - tmp[2*restart+2]: work vector w for A*z
70 /// - tmp[2*restart+3]: accumulator vector for solution update
71 /// @param options FGMRES solver parameters (restart, tolerances, max iterations).
72 /// @param statistics Shared pointer to statistics table for logging iteration progress (optional).
73 /// @param preconditioner Preconditioner solver (defaults to identity).
75 const std::vector< SolutionVectorType >& tmp,
76 const FGMRESOptions< ScalarType >& options = {},
77 const std::shared_ptr< util::Table >& statistics = nullptr,
78 const PreconditionerT preconditioner = IdentitySolver< OperatorT >() )
79 : tag_( "fgmres_solver" )
80 , tmp_( tmp )
81 , options_( options )
82 , statistics_( statistics )
83 , preconditioner_( preconditioner )
84 , skip_preconditioner_in_case_of_nan_or_infs_( true )
85 {}
86
87 /// @brief Set a tag string for statistics output identification.
88 /// @param tag Tag string to identify this solver instance in statistics.
89 void set_tag( const std::string& tag ) { tag_ = tag; }
90
91 /// @brief Set the number of inner iterations before restart (FGMRES(m)).
92 /// @param m Restart parameter (must be at least 1).
93 void set_restart( int m ) { options_.restart = std::max( 1, m ); }
94
95 /// @brief Solve the linear system \f$ Ax = b \f$ using flexible GMRES with restarts.
96 ///
97 /// Uses right preconditioning: solves \f$ A M^{-1} y = b \f$ where \f$ x = M^{-1} y \f$.
98 /// The preconditioner M can vary at each iteration (flexibility).
99 ///
100 /// The method builds an Arnoldi basis via modified Gram-Schmidt orthogonalization,
101 /// applies Givens rotations to minimize the least-squares residual in the Krylov subspace,
102 /// and updates the solution. If convergence is not achieved within the restart window,
103 /// the process restarts with the updated solution until max_iterations is reached or
104 /// convergence criteria are satisfied.
105 ///
106 /// @param A Operator (matrix) to solve with.
107 /// @param x Solution vector (input: initial guess, output: final solution).
108 /// @param b Right-hand side vector (input).
110 {
111 util::Timer timer_fgmres_solve( "fgmres_solve" );
112
113 // Compute initial residual r = b - A*x
114 auto& r = tmp_[0];
115 {
116 util::Timer t_mv( "fgmres_matvec" );
117 apply( A, x, r );
118 }
119 lincomb( r, { 1.0, -1.0 }, { b, r } );
120
121 ScalarType beta0 = std::sqrt( dot( r, r ) );
122 const ScalarType initial_residual = beta0;
123
124 if ( statistics_ )
125 {
126 statistics_->add_row(
127 { { "tag", tag_ }, { "iteration", 0 }, { "relative_residual", 1.0 }, { "absolute_residual", beta0 } } );
128 }
129
130 if ( beta0 <= options_.absolute_residual_tolerance )
131 {
132 return;
133 }
134
135 // Validate workspace size.
136 // Required: 1 (r) + (m+1) (V) + m (Z) + 1 (w) + 1 (acc) = 2m + 4
137 const int nTmp = static_cast< int >( tmp_.size() );
138 const int m_req = options_.restart;
139 const int needed = 2 * m_req + 4;
140 if ( nTmp < needed )
141 {
142 std::cerr << "FGMRES: insufficient tmp vectors. Provided " << nTmp
143 << ", required at least (2*restart + 4) = " << needed << " for restart m = " << m_req
144 << std::endl;
145 Kokkos::abort( "FGMRES: insufficient tmp vectors" );
146 }
147
148 // Workspace layout offsets:
149 const int offV = 1; // V_0 .. V_m stored in tmp[1 .. m+1]
150 const int offZ = offV + ( m_req + 1 ); // Z_0 .. Z_{m-1} stored in tmp[offV + m+1 .. offV + 2m]
151 const int idxW = offZ + m_req; // w = A*z_j
152 const int idxAcc = idxW + 1; // accumulator for solution update
153
154 // Allocate small dense arrays for Hessenberg matrix and Givens rotations.
155 Eigen::Matrix< ScalarType, Eigen::Dynamic, Eigen::Dynamic > H( m_req + 1, m_req );
156 Eigen::Matrix< ScalarType, Eigen::Dynamic, 1 > g( m_req + 1 );
157 std::vector< ScalarType > cs( m_req + 1, 0 ), sn( m_req + 1, 0 );
158
159 int total_iters = 0;
160
161 // Outer restart loop
162 while ( total_iters < options_.max_iterations )
163 {
164 // Initialize Arnoldi: V_0 = r / ||r||
165 auto& V0 = tmp_[offV + 0];
166 lincomb( V0, { 1.0 / beta0 }, { r } );
167
168 // Reset dense workspace for this restart cycle
169 H.setZero();
170 g.setZero();
171 std::fill( cs.begin(), cs.end(), ScalarType( 0 ) );
172 std::fill( sn.begin(), sn.end(), ScalarType( 0 ) );
173 g( 0 ) = beta0;
174
175 int inner_its = 0;
176
177 // Inner Arnoldi iteration (build Krylov subspace up to dimension m_req)
178 for ( int j = 0; j < m_req && total_iters < options_.max_iterations; ++j, ++total_iters )
179 {
180 // Apply preconditioner: z_j = M^{-1} v_j
181 auto& vj = tmp_[offV + j];
182 auto& zj = tmp_[offZ + j];
183 assign( zj, 0 );
184 {
185 util::Timer t_pc( "fgmres_precondition" );
186 solve( preconditioner_, A, zj, vj );
187 }
188
189 const bool preconditioner_result_contains_nan_or_inf = has_nan_or_inf( zj );
190
191 if ( skip_preconditioner_in_case_of_nan_or_infs_ && preconditioner_result_contains_nan_or_inf )
192 {
194 << "FGMRES: The preconditioner appears to have produced a vector that has NaN or Inf entries.\n"
195 " This may be a result of the preconditioner or of the input provided by FGMRES.\n"
196 " To at least provide a fix for the first case, we keep the input to the preconditioner\n"
197 " and write it to the output (equivalent of skipping the preconditioner in this iteration).\n"
198 " (Details: total_iters = "
199 << total_iters << ", j = " << j << ")" << std::endl;
200
201 assign( zj, vj );
202 }
203
204 // Apply operator: w = A * z_j
205 auto& w = tmp_[idxW];
206 {
207 util::Timer t_mv( "fgmres_matvec" );
208 apply( A, zj, w );
209 }
210
211 ScalarType h_jp1j;
212 {
213 util::Timer t_gs( "fgmres_gram_schmidt" );
214 // Modified Gram-Schmidt orthogonalization against V_0..V_j
215 for ( int i = 0; i <= j; ++i )
216 {
217 auto& vi = tmp_[offV + i];
218 const ScalarType hij = dot( w, vi );
219 H( i, j ) = hij;
220 lincomb( w, { 1.0, -hij }, { w, vi } );
221 }
222 // Compute h_{j+1,j} and normalize to get v_{j+1}
223 h_jp1j = std::sqrt( dot( w, w ) );
224 }
225
226 // SAFE BREAK 1: Arnoldi Breakdown
227 if ( h_jp1j < std::numeric_limits< ScalarType >::epsilon() * initial_residual )
228 {
229 H( j + 1, j ) = 0;
230 inner_its = j + 1;
231
232 util::logroot << "FGMRES: Arnoldi breakdown. Restarting.\n"
233 " (Details: total_iters = "
234 << total_iters << ", j = " << j << ")" << std::endl;
235
236 break; // Subspace is "full" or converged; exit to solve current least squares
237 }
238
239 H( j + 1, j ) = h_jp1j;
240
241 if ( h_jp1j > ScalarType( 0 ) )
242 {
243 util::Timer t_gs( "fgmres_gram_schmidt" );
244 auto& vjp1 = tmp_[offV + j + 1];
245 lincomb( vjp1, { 1.0 / h_jp1j }, { w } );
246 }
247
248 util::Timer t_hess( "fgmres_hessenberg" );
249
250 // Apply previous Givens rotations to column j of H
251 for ( int i = 0; i < j; ++i )
252 {
253 const ScalarType temp = cs[i] * H( i, j ) + sn[i] * H( i + 1, j );
254 H( i + 1, j ) = -sn[i] * H( i, j ) + cs[i] * H( i + 1, j );
255 H( i, j ) = temp;
256 }
257
258 // Compute new Givens rotation to zero H(j+1, j)
259 {
260 const ScalarType a = H( j, j );
261 const ScalarType b = H( j + 1, j );
262 const ScalarType r_ab = std::hypot( a, b );
263 cs[j] = ( r_ab == ScalarType( 0 ) ) ? ScalarType( 1 ) : a / r_ab;
264 sn[j] = ( r_ab == ScalarType( 0 ) ) ? ScalarType( 0 ) : b / r_ab;
265 H( j, j ) = r_ab;
266 H( j + 1, j ) = 0;
267
268 // SAFE BREAK 2: Stagnation / Singularity
269 if ( std::abs( H( j, j ) ) < std::numeric_limits< ScalarType >::epsilon() )
270 {
271 inner_its = j; // Don't include this column in the back-solve
272
273 util::logroot << "FGMRES: Stagnation after Givens rotation. Restarting.\n"
274 " (Details: total_iters = "
275 << total_iters << ", j = " << j << ")" << std::endl;
276
277 break;
278 }
279
280 // Apply new Givens rotation to g
281 const ScalarType gj = g( j );
282 const ScalarType gj1 = g( j + 1 );
283 g( j ) = cs[j] * gj + sn[j] * gj1;
284 g( j + 1 ) = -sn[j] * gj + cs[j] * gj1;
285 }
286
287 // Estimate residual norm from least-squares problem
288 const ScalarType abs_res = std::abs( g( j + 1 ) );
289 const ScalarType rel_res = abs_res / initial_residual;
290
291 if ( statistics_ )
292 {
293 statistics_->add_row(
294 { { "tag", tag_ },
295 { "iteration", total_iters + 1 },
296 { "relative_residual", rel_res },
297 { "absolute_residual", abs_res } } );
298 }
299
300 // SAFE BREAK 3: NaN detected in residual estimate.
301 // Discard this iteration and return the current best solution.
302 if ( !std::isfinite( abs_res ) )
303 {
305 << "FGMRES: NaN or Inf detected in residual estimate. "
306 "Returning current solution without this iteration's update.\n"
307 " (Details: total_iters = "
308 << total_iters << ", j = " << j << ")" << std::endl;
309
310 inner_its = j; // exclude the corrupted iteration from the back-solve
311 ++total_iters;
312 break;
313 }
314
315 inner_its = j + 1;
316
317 // Check for convergence
318 if ( rel_res <= options_.relative_residual_tolerance || abs_res < options_.absolute_residual_tolerance )
319 {
320 break;
321 }
322 }
323
324 // Solve upper-triangular system R*y = g(0..inner_its-1) via back-substitution
325 Eigen::Matrix< ScalarType, Eigen::Dynamic, 1 > y( inner_its );
326 y.setZero();
327 {
328 util::Timer t_bs( "fgmres_hessenberg" );
329 for ( int row = inner_its - 1; row >= 0; --row )
330 {
331 ScalarType sum = g( row );
332 for ( int col = row + 1; col < inner_its; ++col )
333 sum -= H( row, col ) * y( col );
334 y( row ) = sum / H( row, row );
335 }
336 }
337
338 // Update solution: x += sum_{i=0}^{inner_its-1} y_i * z_i
339 {
340 util::Timer t_upd( "fgmres_restart_update" );
341 auto& acc = tmp_[idxAcc];
342 assign( acc, 0 );
343 for ( int i = 0; i < inner_its; ++i )
344 {
345 auto& zi = tmp_[offZ + i];
346 lincomb( acc, { 1.0, y( i ) }, { acc, zi } );
347 }
348 lincomb( x, { 1.0, 1.0 }, { x, acc } );
349 }
350
351 // Recompute residual r = b - A*x for next restart
352 {
353 util::Timer t_mv( "fgmres_matvec" );
354 apply( A, x, r );
355 }
356 lincomb( r, { 1.0, -1.0 }, { b, r } );
357 beta0 = std::sqrt( dot( r, r ) );
358
359 // Check for final convergence or abort on NaN
360 if ( !std::isfinite( beta0 ) )
361 {
362 util::logroot << "FGMRES: Residual is NaN/Inf after restart. Aborting solve.\n"
363 " (Details: beta0 = "
364 << beta0 << ", total_iters = " << total_iters << ")" << std::endl;
365 return;
366 }
367 if ( beta0 <= options_.absolute_residual_tolerance ||
368 beta0 / initial_residual <= options_.relative_residual_tolerance )
369 {
370 return;
371 }
372 }
373 }
374
375 private:
376 std::string tag_; ///< Tag for statistics output identification.
377
378 std::vector< SolutionVectorType > tmp_; ///< Temporary workspace vectors.
379
380 FGMRESOptions< ScalarType > options_; ///< Solver parameters (restart, tolerances, max_iterations).
381
382 std::shared_ptr< util::Table > statistics_; ///< Statistics table for iteration logging (optional).
383
384 PreconditionerT preconditioner_; ///< Preconditioner solver.
385
386 bool skip_preconditioner_in_case_of_nan_or_infs_;
387};
388
389/// @brief Static assertion: FGMRES satisfies SolverLike concept.
390static_assert(
391 SolverLike<
392 FGMRES< linalg::detail::
393 DummyOperator< linalg::detail::DummyVector< double >, linalg::detail::DummyVector< double > > > > );
394
395} // namespace terra::linalg::solvers
Dummy vector class for concept checks and testing. Implements required vector operations as no-ops.
Definition vector.hpp:210
Flexible GMRES (FGMRES) iterative solver for nonsymmetric linear systems.
Definition fgmres.hpp:49
OperatorT OperatorType
Operator type to be solved.
Definition fgmres.hpp:52
SrcOf< OperatorType > SolutionVectorType
Solution vector type.
Definition fgmres.hpp:55
void set_restart(int m)
Set the number of inner iterations before restart (FGMRES(m)).
Definition fgmres.hpp:93
void set_tag(const std::string &tag)
Set a tag string for statistics output identification.
Definition fgmres.hpp:89
FGMRES(const std::vector< SolutionVectorType > &tmp, const FGMRESOptions< ScalarType > &options={}, const std::shared_ptr< util::Table > &statistics=nullptr, const PreconditionerT preconditioner=IdentitySolver< OperatorT >())
Construct an FGMRES solver with a custom preconditioner.
Definition fgmres.hpp:74
typename SolutionVectorType::ScalarType ScalarType
Scalar type for computations.
Definition fgmres.hpp:61
void solve_impl(OperatorType &A, SolutionVectorType &x, const RHSVectorType &b)
Solve the linear system using flexible GMRES with restarts.
Definition fgmres.hpp:109
DstOf< OperatorType > RHSVectorType
Right-hand side vector type.
Definition fgmres.hpp:58
"Identity solver" for linear systems.
Definition identity_solver.hpp:21
Timer supporting RAII scope or manual stop.
Definition timer.hpp:342
Definition block_preconditioner_2x2.hpp:7
void solve(Solver &solver, Operator &A, SolutionVector &x, const RHSVector &b)
Solve a linear system using the given solver and operator. Calls the solver's solve_impl method.
Definition solver.hpp:51
void lincomb(Vector &y, const std::vector< ScalarOf< Vector > > &c, const std::vector< Vector > &x, const ScalarOf< Vector > &c0)
Compute a linear combination of vectors. Implements: .
Definition vector.hpp:72
Operator::SrcVectorType SrcOf
Alias for the source vector type of an operator.
Definition operator.hpp:145
ScalarOf< Vector > dot(const Vector &y, const Vector &x)
Compute the dot product of two vectors. Implements: .
Definition vector.hpp:118
void apply(LinearForm &L, typename LinearForm::DstVectorType &dst)
Apply a linear form and write to a destination vector.
Definition linear_form.hpp:37
Operator::DstVectorType DstOf
Alias for the destination vector type of an operator.
Definition operator.hpp:149
bool has_nan_or_inf(const Vector &y)
Check if a vector contains NaN or inf entries. Returns true if any entry of is NaN or inf.
Definition vector.hpp:189
void assign(Vector &y, const ScalarOf< Vector > &c0)
Assign a scalar value to a vector. Implements: .
Definition vector.hpp:97
detail::PrefixCout logroot([]() { return detail::log_prefix();})
std::ostream subclass that just logs on root and adds a timestamp for each line.
ScalarType absolute_residual_tolerance
Absolute residual tolerance for convergence.
Definition fgmres.hpp:24
ScalarType relative_residual_tolerance
Relative residual tolerance for convergence.
Definition fgmres.hpp:21
int max_iterations
Maximum number of total iterations (across all restarts).
Definition fgmres.hpp:27
int restart
Number of inner iterations before restart (FGMRES(m)).
Definition fgmres.hpp:18