Serac  0.1
Serac is an implicit thermal strucural mechanics simulation code.
equation_solver.cpp
1 // Copyright (c) 2019-2024, Lawrence Livermore National Security, LLC and
2 // other Serac Project Developers. See the top-level LICENSE file for
3 // details.
4 //
5 // SPDX-License-Identifier: (BSD-3-Clause)
6 
8 
11 #include "serac/serac_config.hpp"
12 
13 namespace serac {
14 
16 {
17  auto [lin_solver, preconditioner] = buildLinearSolverAndPreconditioner(lin_opts, comm);
18 
19  lin_solver_ = std::move(lin_solver);
20  preconditioner_ = std::move(preconditioner);
21  nonlin_solver_ = buildNonlinearSolver(nonlinear_opts, comm);
22 }
23 
24 EquationSolver::EquationSolver(std::unique_ptr<mfem::NewtonSolver> nonlinear_solver,
25  std::unique_ptr<mfem::Solver> linear_solver,
26  std::unique_ptr<mfem::Solver> preconditioner)
27 {
28  SLIC_ERROR_ROOT_IF(!nonlinear_solver, "Nonlinear solvers must be given to construct an EquationSolver");
29  SLIC_ERROR_ROOT_IF(!linear_solver, "Linear solvers must be given to construct an EquationSolver");
30 
31  nonlin_solver_ = std::move(nonlinear_solver);
32  lin_solver_ = std::move(linear_solver);
33  preconditioner_ = std::move(preconditioner);
34 }
35 
36 void EquationSolver::setOperator(const mfem::Operator& op)
37 {
38  nonlin_solver_->SetOperator(op);
39 
40  // Now that the nonlinear solver knows about the operator, we can set its linear solver
41  if (!nonlin_solver_set_solver_called_) {
42  nonlin_solver_->SetSolver(linearSolver());
43  nonlin_solver_set_solver_called_ = true;
44  }
45 }
46 
47 void EquationSolver::solve(mfem::Vector& x) const
48 {
49  mfem::Vector zero(x);
50  zero = 0.0;
51  // KINSOL does not handle non-zero RHS, so we enforce that the RHS
52  // of the nonlinear system is zero
53  nonlin_solver_->Mult(zero, x);
54 }
55 
56 void SuperLUSolver::Mult(const mfem::Vector& input, mfem::Vector& output) const
57 {
58  SLIC_ERROR_ROOT_IF(!superlu_mat_, "Operator must be set prior to solving with SuperLU");
59 
60  // Use the underlying MFEM-based solver and SuperLU matrix type to solve the system
61  superlu_solver_.Mult(input, output);
62 }
63 
72 std::unique_ptr<mfem::HypreParMatrix> buildMonolithicMatrix(const mfem::BlockOperator& block_operator)
73 {
74  int row_blocks = block_operator.NumRowBlocks();
75  int col_blocks = block_operator.NumColBlocks();
76 
77  SLIC_ERROR_ROOT_IF(row_blocks != col_blocks, "Attempted to use a direct solver on a non-square block system.");
78 
79  mfem::Array2D<const mfem::HypreParMatrix*> hypre_blocks(row_blocks, col_blocks);
80 
81  for (int i = 0; i < row_blocks; ++i) {
82  for (int j = 0; j < col_blocks; ++j) {
83  // checks for presence of empty (null) blocks, which happen fairly common in multirank contact
84  if (!block_operator.IsZeroBlock(i, j)) {
85  auto* hypre_block = dynamic_cast<const mfem::HypreParMatrix*>(&block_operator.GetBlock(i, j));
86  SLIC_ERROR_ROOT_IF(!hypre_block,
87  "Trying to use SuperLU on a block operator that does not contain HypreParMatrix blocks.");
88 
89  hypre_blocks(i, j) = hypre_block;
90  } else {
91  hypre_blocks(i, j) = nullptr;
92  }
93  }
94  }
95 
96  // Note that MFEM passes ownership of this matrix to the caller
97  return std::unique_ptr<mfem::HypreParMatrix>(mfem::HypreParMatrixFromBlocks(hypre_blocks));
98 }
99 
100 void SuperLUSolver::SetOperator(const mfem::Operator& op)
101 {
102  // Check if this is a block operator
103  auto* block_operator = dynamic_cast<const mfem::BlockOperator*>(&op);
104 
105  // If it is, make a monolithic system from the underlying blocks
106  if (block_operator) {
107  auto monolithic_mat = buildMonolithicMatrix(*block_operator);
108 
109  superlu_mat_ = std::make_unique<mfem::SuperLURowLocMatrix>(*monolithic_mat);
110  } else {
111  // If this is not a block system, check that the input operator is a HypreParMatrix as expected
112  auto* matrix = dynamic_cast<const mfem::HypreParMatrix*>(&op);
113 
114  SLIC_ERROR_ROOT_IF(!matrix, "Matrix must be an assembled HypreParMatrix for use with SuperLU");
115 
116  superlu_mat_ = std::make_unique<mfem::SuperLURowLocMatrix>(*matrix);
117  }
118 
119  superlu_solver_.SetOperator(*superlu_mat_);
120 }
121 
122 #ifdef MFEM_USE_STRUMPACK
123 
124 void StrumpackSolver::Mult(const mfem::Vector& input, mfem::Vector& output) const
125 {
126  SLIC_ERROR_ROOT_IF(!strumpack_mat_, "Operator must be set prior to solving with Strumpack");
127 
128  // Use the underlying MFEM-based solver and Strumpack matrix type to solve the system
129  strumpack_solver_.Mult(input, output);
130 }
131 
132 void StrumpackSolver::SetOperator(const mfem::Operator& op)
133 {
134  // Check if this is a block operator
135  auto* block_operator = dynamic_cast<const mfem::BlockOperator*>(&op);
136 
137  // If it is, make a monolithic system from the underlying blocks
138  if (block_operator) {
139  auto monolithic_mat = buildMonolithicMatrix(*block_operator);
140 
141  strumpack_mat_ = std::make_unique<mfem::STRUMPACKRowLocMatrix>(*monolithic_mat);
142  } else {
143  // If this is not a block system, check that the input operator is a HypreParMatrix as expected
144  auto* matrix = dynamic_cast<const mfem::HypreParMatrix*>(&op);
145 
146  SLIC_ERROR_ROOT_IF(!matrix, "Matrix must be an assembled HypreParMatrix for use with Strumpack");
147 
148  strumpack_mat_ = std::make_unique<mfem::STRUMPACKRowLocMatrix>(*matrix);
149  }
150 
151  strumpack_solver_.SetOperator(*strumpack_mat_);
152 }
153 
154 #endif
155 
156 std::unique_ptr<mfem::NewtonSolver> buildNonlinearSolver(NonlinearSolverOptions nonlinear_opts, MPI_Comm comm)
157 {
158  std::unique_ptr<mfem::NewtonSolver> nonlinear_solver;
159 
160  if (nonlinear_opts.nonlin_solver == NonlinearSolver::Newton) {
161  nonlinear_solver = std::make_unique<mfem::NewtonSolver>(comm);
162  } else if (nonlinear_opts.nonlin_solver == NonlinearSolver::LBFGS) {
163  nonlinear_solver = std::make_unique<mfem::LBFGSSolver>(comm);
164  }
165  // KINSOL
166  else {
167 #ifdef SERAC_USE_SUNDIALS
168 
169  int kinsol_strat = KIN_NONE;
170 
171  switch (nonlinear_opts.nonlin_solver) {
173  kinsol_strat = KIN_NONE;
174  break;
176  kinsol_strat = KIN_LINESEARCH;
177  break;
179  kinsol_strat = KIN_PICARD;
180  break;
181  default:
182  kinsol_strat = KIN_NONE;
183  SLIC_ERROR_ROOT("Unknown KINSOL nonlinear solver type given.");
184  }
185  auto kinsol_solver = std::make_unique<mfem::KINSolver>(comm, kinsol_strat, true);
186  nonlinear_solver = std::move(kinsol_solver);
187 #else
188  SLIC_ERROR_ROOT("KINSOL was not enabled when MFEM was built");
189 #endif
190  }
191 
192  nonlinear_solver->SetRelTol(nonlinear_opts.relative_tol);
193  nonlinear_solver->SetAbsTol(nonlinear_opts.absolute_tol);
194  nonlinear_solver->SetMaxIter(nonlinear_opts.max_iterations);
195  nonlinear_solver->SetPrintLevel(nonlinear_opts.print_level);
196 
197  // Iterative mode indicates we do not zero out the initial guess during the
198  // nonlinear solver call. This is required as we apply the essential boundary
199  // conditions before the nonlinear solver is applied.
200  nonlinear_solver->iterative_mode = true;
201 
202  return nonlinear_solver;
203 }
204 
205 std::pair<std::unique_ptr<mfem::Solver>, std::unique_ptr<mfem::Solver>> buildLinearSolverAndPreconditioner(
206  LinearSolverOptions linear_opts, MPI_Comm comm)
207 {
208  if (linear_opts.linear_solver == LinearSolver::SuperLU) {
209  auto lin_solver = std::make_unique<SuperLUSolver>(linear_opts.print_level, comm);
210  return {std::move(lin_solver), nullptr};
211  }
212 
213 #ifdef MFEM_USE_STRUMPACK
214 
215  if (linear_opts.linear_solver == LinearSolver::Strumpack) {
216  auto lin_solver = std::make_unique<StrumpackSolver>(linear_opts.print_level, comm);
217  return {std::move(lin_solver), nullptr};
218  }
219 
220 #endif
221 
222  std::unique_ptr<mfem::IterativeSolver> iter_lin_solver;
223 
224  switch (linear_opts.linear_solver) {
225  case LinearSolver::CG:
226  iter_lin_solver = std::make_unique<mfem::CGSolver>(comm);
227  break;
228  case LinearSolver::GMRES:
229  iter_lin_solver = std::make_unique<mfem::GMRESSolver>(comm);
230  break;
231  default:
232  SLIC_ERROR_ROOT("Linear solver type not recognized.");
233  exitGracefully(true);
234  }
235 
236  iter_lin_solver->SetRelTol(linear_opts.relative_tol);
237  iter_lin_solver->SetAbsTol(linear_opts.absolute_tol);
238  iter_lin_solver->SetMaxIter(linear_opts.max_iterations);
239  iter_lin_solver->SetPrintLevel(linear_opts.print_level);
240 
241  auto preconditioner = buildPreconditioner(linear_opts.preconditioner, linear_opts.preconditioner_print_level, comm);
242 
243  if (preconditioner) {
244  iter_lin_solver->SetPreconditioner(*preconditioner);
245  }
246 
247  return {std::move(iter_lin_solver), std::move(preconditioner)};
248 }
249 
250 #ifdef MFEM_USE_AMGX
251 std::unique_ptr<mfem::AmgXSolver> buildAMGX(const AMGXOptions& options, const MPI_Comm comm)
252 {
253  auto amgx = std::make_unique<mfem::AmgXSolver>();
254  conduit::Node options_node;
255  options_node["config_version"] = 2;
256  auto& solver_options = options_node["solver"];
257  solver_options["solver"] = "AMG";
258  solver_options["presweeps"] = 1;
259  solver_options["postsweeps"] = 2;
260  solver_options["interpolator"] = "D2";
261  solver_options["max_iters"] = 2;
262  solver_options["convergence"] = "ABSOLUTE";
263  solver_options["cycle"] = "V";
264 
265  if (options.verbose) {
266  options_node["solver/obtain_timings"] = 1;
267  options_node["solver/monitor_residual"] = 1;
268  options_node["solver/print_solve_stats"] = 1;
269  }
270 
271  // TODO: Use magic_enum here when we can switch to GCC 9+
272  // This is an immediately-invoked lambda so that the map
273  // can be const without needed to initialize all the values
274  // in the constructor
275  static const auto solver_names = []() {
276  std::unordered_map<AMGXSolver, std::string> names;
277  names[AMGXSolver::AMG] = "AMG";
278  names[AMGXSolver::PCGF] = "PCGF";
279  names[AMGXSolver::CG] = "CG";
280  names[AMGXSolver::PCG] = "PCG";
281  names[AMGXSolver::PBICGSTAB] = "PBICGSTAB";
282  names[AMGXSolver::BICGSTAB] = "BICGSTAB";
283  names[AMGXSolver::FGMRES] = "FGMRES";
284  names[AMGXSolver::JACOBI_L1] = "JACOBI_L1";
285  names[AMGXSolver::GS] = "GS";
286  names[AMGXSolver::POLYNOMIAL] = "POLYNOMIAL";
287  names[AMGXSolver::KPZ_POLYNOMIAL] = "KPZ_POLYNOMIAL";
288  names[AMGXSolver::BLOCK_JACOBI] = "BLOCK_JACOBI";
289  names[AMGXSolver::MULTICOLOR_GS] = "MULTICOLOR_GS";
290  names[AMGXSolver::MULTICOLOR_DILU] = "MULTICOLOR_DILU";
291  return names;
292  }();
293 
294  options_node["solver/solver"] = solver_names.at(options.solver);
295  options_node["solver/smoother"] = solver_names.at(options.smoother);
296 
297  // Treat the string as the config (not a filename)
298  amgx->ReadParameters(options_node.to_json(), mfem::AmgXSolver::INTERNAL);
299  amgx->InitExclusiveGPU(comm);
300 
301  return amgx;
302 }
303 #endif
304 
305 std::unique_ptr<mfem::Solver> buildPreconditioner(Preconditioner preconditioner, int print_level,
306  [[maybe_unused]] MPI_Comm comm)
307 {
308  std::unique_ptr<mfem::Solver> preconditioner_solver;
309 
310  // Handle the preconditioner - currently just BoomerAMG and HypreSmoother are supported
311  if (preconditioner == Preconditioner::HypreAMG) {
312  auto amg_preconditioner = std::make_unique<mfem::HypreBoomerAMG>();
313  amg_preconditioner->SetPrintLevel(print_level);
314  preconditioner_solver = std::move(amg_preconditioner);
315  } else if (preconditioner == Preconditioner::HypreJacobi) {
316  auto jac_preconditioner = std::make_unique<mfem::HypreSmoother>();
317  jac_preconditioner->SetType(mfem::HypreSmoother::Type::Jacobi);
318  preconditioner_solver = std::move(jac_preconditioner);
319  } else if (preconditioner == Preconditioner::HypreL1Jacobi) {
320  auto jacl1_preconditioner = std::make_unique<mfem::HypreSmoother>();
321  jacl1_preconditioner->SetType(mfem::HypreSmoother::Type::l1Jacobi);
322  preconditioner_solver = std::move(jacl1_preconditioner);
323  } else if (preconditioner == Preconditioner::HypreGaussSeidel) {
324  auto gs_preconditioner = std::make_unique<mfem::HypreSmoother>();
325  gs_preconditioner->SetType(mfem::HypreSmoother::Type::GS);
326  preconditioner_solver = std::move(gs_preconditioner);
327  } else if (preconditioner == Preconditioner::HypreILU) {
328  auto ilu_preconditioner = std::make_unique<mfem::HypreILU>();
329  ilu_preconditioner->SetLevelOfFill(1);
330  ilu_preconditioner->SetPrintLevel(print_level);
331  preconditioner_solver = std::move(ilu_preconditioner);
332  } else if (preconditioner == Preconditioner::AMGX) {
333 #ifdef MFEM_USE_AMGX
334  preconditioner_solver = buildAMGX(AMGXOptions{}, comm);
335 #else
336  SLIC_ERROR_ROOT("AMGX requested in non-GPU build");
337 #endif
338  } else {
339  SLIC_ERROR_ROOT_IF(preconditioner != Preconditioner::None, "Unknown preconditioner type requested");
340  }
341 
342  return preconditioner_solver;
343 }
344 
345 void EquationSolver::defineInputFileSchema(axom::inlet::Container& container)
346 {
347  auto& linear_container = container.addStruct("linear", "Linear Equation Solver Parameters");
348  linear_container.required().registerVerifier([](const axom::inlet::Container& container_to_verify) {
349  // Make sure that the provided options match the desired linear solver type
350  const bool is_iterative = (container_to_verify["type"].get<std::string>() == "iterative") &&
351  container_to_verify.contains("iterative_options");
352  const bool is_direct =
353  (container_to_verify["type"].get<std::string>() == "direct") && container_to_verify.contains("direct_options");
354  return is_iterative || is_direct;
355  });
356 
357  // Enforce the solver type - must be iterative or direct
358  linear_container.addString("type", "The type of solver parameters to use (iterative|direct)")
359  .required()
360  .validValues({"iterative", "direct"});
361 
362  auto& iterative_container = linear_container.addStruct("iterative_options", "Iterative solver parameters");
363  iterative_container.addDouble("rel_tol", "Relative tolerance for the linear solve.").defaultValue(1.0e-6);
364  iterative_container.addDouble("abs_tol", "Absolute tolerance for the linear solve.").defaultValue(1.0e-8);
365  iterative_container.addInt("max_iter", "Maximum iterations for the linear solve.").defaultValue(5000);
366  iterative_container.addInt("print_level", "Linear print level.").defaultValue(0);
367  iterative_container.addString("solver_type", "Solver type (gmres|minres|cg).").defaultValue("gmres");
368  iterative_container.addString("prec_type", "Preconditioner type (JacobiSmoother|L1JacobiSmoother|AMG|ILU).")
369  .defaultValue("JacobiSmoother");
370 
371  auto& direct_container = linear_container.addStruct("direct_options", "Direct solver parameters");
372  direct_container.addInt("print_level", "Linear print level.").defaultValue(0);
373 
374  // Only needed for nonlinear problems
375  auto& nonlinear_container = container.addStruct("nonlinear", "Newton Equation Solver Parameters").required(false);
376  nonlinear_container.addDouble("rel_tol", "Relative tolerance for the Newton solve.").defaultValue(1.0e-2);
377  nonlinear_container.addDouble("abs_tol", "Absolute tolerance for the Newton solve.").defaultValue(1.0e-4);
378  nonlinear_container.addInt("max_iter", "Maximum iterations for the Newton solve.").defaultValue(500);
379  nonlinear_container.addInt("print_level", "Nonlinear print level.").defaultValue(0);
380  nonlinear_container.addString("solver_type", "Solver type (Newton|KINFullStep|KINLineSearch)").defaultValue("Newton");
381 }
382 
383 } // namespace serac
384 
388 
390 {
391  LinearSolverOptions options;
392  std::string type = base["type"];
393 
394  if (type == "direct") {
396  options.print_level = base["direct_options/print_level"];
397  return options;
398  }
399 
400  auto config = base["iterative_options"];
401  options.relative_tol = config["rel_tol"];
402  options.absolute_tol = config["abs_tol"];
403  options.max_iterations = config["max_iter"];
404  options.print_level = config["print_level"];
405  std::string solver_type = config["solver_type"];
406  if (solver_type == "gmres") {
408  } else if (solver_type == "cg") {
410  } else {
411  std::string msg = axom::fmt::format("Unknown Linear solver type given: '{0}'", solver_type);
412  SLIC_ERROR_ROOT(msg);
413  }
414  const std::string prec_type = config["prec_type"];
415  if (prec_type == "JacobiSmoother") {
417  } else if (prec_type == "L1JacobiSmoother") {
419  } else if (prec_type == "HypreAMG") {
421  } else if (prec_type == "ILU") {
423 #ifdef MFEM_USE_AMGX
424  } else if (prec_type == "AMGX") {
426 #endif
427  } else if (prec_type == "GaussSeidel") {
429  } else {
430  std::string msg = axom::fmt::format("Unknown preconditioner type given: '{0}'", prec_type);
431  SLIC_ERROR_ROOT(msg);
432  }
433 
434  return options;
435 }
436 
438 {
439  NonlinearSolverOptions options;
440  options.relative_tol = base["rel_tol"];
441  options.absolute_tol = base["abs_tol"];
442  options.max_iterations = base["max_iter"];
443  options.print_level = base["print_level"];
444  const std::string solver_type = base["solver_type"];
445  if (solver_type == "Newton") {
447  } else if (solver_type == "KINFullStep") {
449  } else if (solver_type == "KINLineSearch") {
451  } else if (solver_type == "KINPicard") {
453  } else {
454  SLIC_ERROR_ROOT(axom::fmt::format("Unknown nonlinear solver type given: '{0}'", solver_type));
455  }
456  return options;
457 }
458 
460 {
461  auto lin = base["linear"].get<LinearSolverOptions>();
462  auto nonlin = base["nonlinear"].get<NonlinearSolverOptions>();
463 
464  auto [linear_solver, preconditioner] = serac::buildLinearSolverAndPreconditioner(lin, MPI_COMM_WORLD);
465 
466  serac::EquationSolver eq_solver(serac::buildNonlinearSolver(nonlin, MPI_COMM_WORLD), std::move(linear_solver),
467  std::move(preconditioner));
468 
469  return eq_solver;
470 }
This class manages the objects typically required to solve a nonlinear set of equations arising from ...
void setOperator(const mfem::Operator &op)
void solve(mfem::Vector &x) const
mfem::Solver * preconditioner()
static void defineInputFileSchema(axom::inlet::Container &container)
EquationSolver(std::unique_ptr< mfem::NewtonSolver > nonlinear_solver, std::unique_ptr< mfem::Solver > linear_solver, std::unique_ptr< mfem::Solver > preconditioner=nullptr)
mfem::Solver & linearSolver()
void Mult(const mfem::Vector &input, mfem::Vector &output) const
Factor and solve the linear system y = Op^{-1} x using DSuperLU.
void SetOperator(const mfem::Operator &op)
Set the underlying matrix operator to use in the solution algorithm.
This file contains the declaration of an equation solver wrapper.
This file contains the all the necessary functions and macros required for logging as well as a helpe...
Accelerator functionality.
Definition: serac.cpp:38
void exitGracefully(bool error)
Exits the program gracefully after cleaning up necessary tasks.
Definition: terminator.cpp:44
std::pair< std::unique_ptr< mfem::Solver >, std::unique_ptr< mfem::Solver > > buildLinearSolverAndPreconditioner(LinearSolverOptions linear_opts, MPI_Comm comm)
Build the linear solver and its associated preconditioner given a linear options struct.
Preconditioner
The type of preconditioner to be used.
std::unique_ptr< mfem::Solver > buildPreconditioner(Preconditioner preconditioner, int print_level, [[maybe_unused]] MPI_Comm comm)
Build a preconditioner from the available options.
constexpr SERAC_HOST_DEVICE auto type(const tuple< T... > &values)
a function intended to be used for extracting the ith type from a tuple.
Definition: tuple.hpp:274
std::unique_ptr< mfem::NewtonSolver > buildNonlinearSolver(NonlinearSolverOptions nonlinear_opts, MPI_Comm comm)
Build a nonlinear solver using the nonlinear option struct.
std::unique_ptr< mfem::HypreParMatrix > buildMonolithicMatrix(const mfem::BlockOperator &block_operator)
Function for building a monolithic parallel Hypre matrix from a block system of smaller Hypre matrice...
serac::EquationSolver operator()(const axom::inlet::Container &base)
Returns created object from Inlet container.
serac::LinearSolverOptions operator()(const axom::inlet::Container &base)
Returns created object from Inlet container.
serac::NonlinearSolverOptions operator()(const axom::inlet::Container &base)
Returns created object from Inlet container.
Stores the information required to configure a NVIDIA AMGX preconditioner.
Parameters for an iterative linear solution scheme.
Preconditioner preconditioner
PreconditionerOptions selection.
LinearSolver linear_solver
Linear solver selection.
double relative_tol
Relative tolerance.
int preconditioner_print_level
Debugging print level for the preconditioner.
int max_iterations
Maximum number of iterations.
int print_level
Debugging print level for the linear solver.
double absolute_tol
Absolute tolerance.
Nonlinear solution scheme parameters.
int print_level
Debug print level.
double relative_tol
Relative tolerance.
NonlinearSolver nonlin_solver
Nonlinear solver selection.
double absolute_tol
Absolute tolerance.
int max_iterations
Maximum number of iterations.
A sentinel struct for eliding no-op tensor operations.
Definition: tensor.hpp:123
Helper functions for exiting Serac cleanly.