Smith  0.1
Smith is an implicit thermal structural mechanics simulation code.
equation_solver.cpp
1 // Copyright (c) Lawrence Livermore National Security, LLC and
2 // other Smith Project Developers. See the top-level LICENSE file for
3 // details.
4 //
5 // SPDX-License-Identifier: (BSD-3-Clause)
6 
8 #include "smith/numerics/block_preconditioner.hpp"
9 
10 #include <cstdlib>
11 #include <iomanip>
12 #include <iostream>
13 #include <algorithm>
14 #include <cmath>
15 #include <exception>
16 #include <limits>
17 #include <string>
18 #include <tuple>
19 
20 #include "smith/smith_config.hpp"
22 #include "smith/numerics/trust_region_solver.hpp"
24 
25 namespace smith {
26 
27 namespace {
31 class PreconditionerOnlySolver : public mfem::IterativeSolver {
32  public:
33  PreconditionerOnlySolver(MPI_Comm mpi_comm) : mfem::IterativeSolver(mpi_comm) {}
34 
36  void Mult(const mfem::Vector& x, mfem::Vector& y) const override
37  {
38  if (prec) {
39  prec->Mult(x, y);
40  } else {
41  y = x;
42  }
43  }
44 
46  void SetOperator(const mfem::Operator& op) override
47  {
48  if (prec) {
49  prec->SetOperator(op);
50  }
51  width = op.Width();
52  height = op.Height();
53  }
54 
55  private:
56  // Note: mfem::IterativeSolver already has a 'prec' member (mfem::Solver*)
57 };
58 
59 class SolverWithPreconditioner : public mfem::Solver {
60  public:
61  SolverWithPreconditioner(std::unique_ptr<mfem::Solver> linear_solver, std::unique_ptr<mfem::Solver> preconditioner)
62  : linear_solver_(std::move(linear_solver)), preconditioner_(std::move(preconditioner))
63  {
64  SLIC_ERROR_IF(!linear_solver_, "SolverWithPreconditioner requires a non-null linear solver");
65  }
66 
67  void SetOperator(const mfem::Operator& op) override
68  {
69  height = op.Height();
70  width = op.Width();
71  linear_solver_->SetOperator(op);
72  }
73 
74  void Mult(const mfem::Vector& x, mfem::Vector& y) const override { linear_solver_->Mult(x, y); }
75 
76  private:
77  std::unique_ptr<mfem::Solver> linear_solver_;
78  std::unique_ptr<mfem::Solver> preconditioner_;
79 };
80 
81 bool preconditionerSupportsBlockOperator(Preconditioner preconditioner)
82 {
83  switch (preconditioner) {
88  return true;
89  default:
90  return false;
91  }
92 }
93 
94 bool linearSolverSupportsBlockOperator(LinearSolver linear_solver)
95 {
96  switch (linear_solver) {
97  case LinearSolver::CG:
100 #ifdef MFEM_USE_STRUMPACK
102 #endif
103 #ifdef SMITH_USE_PETSC
106 #endif
107  case LinearSolver::None:
108  return true;
109  default:
110  return false;
111  }
112 }
113 
114 bool monolithicizeOperatorIfNeeded(const LinearSolverOptions& linear_options, mfem::Operator& assembled_gradient,
115  mfem::Operator*& gradient_operator)
116 {
117  auto* block_gradient = dynamic_cast<const mfem::BlockOperator*>(&assembled_gradient);
118  if (!block_gradient) {
119  gradient_operator = &assembled_gradient;
120  return false;
121  }
122 
123  if (!requiresMonolithicOperator(linear_options)) {
124  gradient_operator = &assembled_gradient;
125  return false;
126  }
127 
128  gradient_operator = buildMonolithicMatrix(*block_gradient).release();
129  SLIC_DEBUG_ROOT(
130  axom::fmt::format("Automatically monolithicizing block Jacobian for linear solver {} with "
131  "preconditioner {}",
132  linearName(linear_options.linear_solver), preconditionerName(linear_options.preconditioner)));
133  return true;
134 }
135 
136 } // namespace
137 
140 class NewtonSolver : public mfem::NewtonSolver, public ConvergenceManagedNonlinearSolver {
141  protected:
143  mutable mfem::Vector x0;
144 
146  NonlinearSolverOptions nonlinear_options;
147 
149  LinearSolverOptions linear_options;
150 
152  mutable size_t print_level = 0;
153 
155  mutable bool grad_monolithic = false;
156 
157  std::shared_ptr<EquationSolverConvergenceManager> convergence_manager_ = nullptr;
158 
159  public:
161  NewtonSolver(const NonlinearSolverOptions& nonlinear_opts, const LinearSolverOptions& linear_opts)
162  : nonlinear_options(nonlinear_opts), linear_options(linear_opts)
163  {
164  }
165 
166 #ifdef MFEM_USE_MPI
168  NewtonSolver(MPI_Comm comm_, const NonlinearSolverOptions& nonlinear_opts, const LinearSolverOptions& linear_opts)
169  : mfem::NewtonSolver(comm_), nonlinear_options(nonlinear_opts), linear_options(linear_opts)
170  {
171  }
172 #endif
173 
175  virtual ~NewtonSolver()
176  {
177  if (grad_monolithic) delete grad;
178  }
179 
180  void setConvergenceManager(std::shared_ptr<EquationSolverConvergenceManager> convergence_manager) override
181  {
182  convergence_manager_ = std::move(convergence_manager);
183  }
184 
186  ConvergenceStatus evaluateConvergence(const mfem::Vector& x, mfem::Vector& rOut) const
187  {
189  ConvergenceStatus status;
190  status.global_norm = std::numeric_limits<double>::max();
191  status.global_goal = std::numeric_limits<double>::max();
192  try {
193  oper->Mult(x, rOut);
194  if (convergence_manager_) {
195  status = convergence_manager_->evaluate(1.0, rOut);
196  } else {
197  status.block_norms = {Norm(rOut)};
198  status.global_norm = status.block_norms.front();
199  status.global_goal = std::max(rel_tol * initial_norm, abs_tol);
200  status.global_converged = status.global_norm <= status.global_goal;
201  status.converged = status.global_converged;
202  }
203  } catch (const std::exception&) {
204  status.global_norm = std::numeric_limits<double>::max();
205  status.global_goal = std::numeric_limits<double>::max();
206  }
207  return status;
208  }
209 
211  void assembleJacobian(const mfem::Vector& x) const
212  {
214  if (grad_monolithic) {
215  delete grad;
216  grad = nullptr;
217  grad_monolithic = false;
218  }
219  mfem::Operator& assembled_gradient = oper->GetGradient(x);
220  grad_monolithic = monolithicizeOperatorIfNeeded(linear_options, assembled_gradient, grad);
221  }
222 
224  void setPreconditioner() const
225  {
227  prec->SetOperator(*grad);
228  }
229 
231  void solveLinearSystem(const mfem::Vector& r_, mfem::Vector& c_) const
232  {
234  prec->Mult(r_, c_); // c = [DF(x_i)]^{-1} [F(x_i)-b]
235  }
236 
238  void Mult(const mfem::Vector&, mfem::Vector& x) const override
239  {
240  MFEM_ASSERT(oper != NULL, "the Operator is not set (use SetOperator).");
241  MFEM_ASSERT(prec != NULL, "the Solver is not set (use SetSolver).");
242 
243  print_level = print_options.iterations ? 1 : print_level;
244  print_level = print_options.summary ? 2 : print_level;
245 
246  using real_t = mfem::real_t;
247 
248  ConvergenceStatus status = evaluateConvergence(x, r);
249  real_t norm = status.global_norm;
250  initial_norm = norm;
251  if (norm == 0.0) return;
252 
253  if (print_level == 1) {
254  mfem::out << "Newton iteration " << std::setw(3) << 0 << " : ||r|| = " << std::setw(13) << norm << "\n";
255  }
256 
257  prec->iterative_mode = false;
258 
259  int it = 0;
260  for (; true; it++) {
261  MFEM_ASSERT(mfem::IsFinite(norm), "norm = " << norm);
262  if (print_level >= 2) {
263  mfem::out << "Newton iteration " << std::setw(3) << it << " : ||r|| = " << std::setw(13) << norm;
264  if (it > 0) {
265  mfem::out << ", ||r||/||r_0|| = " << std::setw(13) << (initial_norm != 0.0 ? norm / initial_norm : norm);
266  }
267  mfem::out << '\n';
268  }
269 
270  if ((print_level >= 1) && (norm != norm)) {
271  mfem::out << "Initial residual for Newton iteration is undefined/nan.\n";
272  mfem::out << "Newton: No convergence!\n";
273  return;
274  }
275 
276  if (status.converged && it >= nonlinear_options.min_iterations) {
277  converged = true;
278  break;
279  } else if (it >= max_iter) {
280  converged = false;
281  break;
282  }
283 
284  real_t norm_nm1 = norm;
285 
286  assembleJacobian(x);
287  setPreconditioner();
288  solveLinearSystem(r, c);
289 
290  // there must be a better way to do this?
291  x0.SetSize(x.Size());
292  x0 = 0.0;
293  x0.Add(1.0, x);
294 
295  real_t stepScale = 1.0;
296  add(x0, -stepScale, c, x);
297  status = evaluateConvergence(x, r);
298  norm = status.global_norm;
299 
300  const int max_ls_iters = nonlinear_options.max_line_search_iterations;
301  static constexpr real_t reduction = 0.5;
302 
303  const double sufficientDecreaseParam = 0.0; // 1e-15;
304  const double cMagnitudeInR = sufficientDecreaseParam != 0.0 ? std::abs(Dot(c, r)) / norm_nm1 : 0.0;
305 
306  auto is_improved = [=](real_t currentNorm, real_t c_scale) {
307  return currentNorm < norm_nm1 - sufficientDecreaseParam * c_scale * cMagnitudeInR;
308  };
309 
310  // back-track linesearch
311  int ls_iter = 0;
312  int ls_iter_sum = 0;
313  for (; !is_improved(norm, stepScale) && ls_iter < max_ls_iters; ++ls_iter, ++ls_iter_sum) {
314  stepScale *= reduction;
315  add(x0, -stepScale, c, x);
316  status = evaluateConvergence(x, r);
317  norm = status.global_norm;
318  }
319 
320  // try the opposite direction and linesearch back from there
321  if (max_ls_iters > 0 && ls_iter == max_ls_iters && !is_improved(norm, stepScale)) {
322  stepScale = 1.0;
323  add(x0, stepScale, c, x);
324  status = evaluateConvergence(x, r);
325  norm = status.global_norm;
326 
327  ls_iter = 0;
328  for (; !is_improved(norm, stepScale) && ls_iter < max_ls_iters; ++ls_iter, ++ls_iter_sum) {
329  stepScale *= reduction;
330  add(x0, stepScale, c, x);
331  status = evaluateConvergence(x, r);
332  norm = status.global_norm;
333  }
334 
335  // ok, the opposite direction was also terrible, lets go back, cut in half 1 last time and accept it hoping for
336  // the best
337  if (ls_iter == max_ls_iters && !is_improved(norm, stepScale)) {
338  ++ls_iter_sum;
339  stepScale *= reduction;
340  add(x0, -stepScale, c, x);
341  status = evaluateConvergence(x, r);
342  norm = status.global_norm;
343  }
344  }
345 
346  if (ls_iter_sum) {
347  if (print_level >= 2) {
348  mfem::out << "Number of line search steps taken = " << ls_iter_sum << std::endl;
349  }
350  if (print_level >= 2 && (ls_iter_sum == 2 * max_ls_iters + 1)) {
351  mfem::out << "The maximum number of line search cut back have occurred, the resulting residual may not have "
352  "decreased. "
353  << std::endl;
354  }
355  }
356  }
357 
358  final_iter = it;
359  final_norm = norm;
360 
361  if (print_level == 1) {
362  mfem::out << "Newton iteration " << std::setw(3) << final_iter << " : ||r|| = " << std::setw(13) << norm << '\n';
363  }
364  if (!converged && print_level >= 1) { // (print_options.summary || print_options.warnings)) {
365  mfem::out << "Newton: No convergence!\n";
366  }
367  }
368 };
369 
371 struct TrustRegionSettings {
373  double cg_tol = 1e-8;
375  size_t min_cg_iterations = 0; //
377  size_t max_cg_iterations = 10000; //
379  size_t max_cumulative_iteration = 1;
381  double min_tr_size = 1e-13;
383  double t1 = 0.25;
385  double t2 = 1.75;
387  double eta1 = 1e-9;
389  double eta2 = 0.1;
391  double eta3 = 0.6;
393  double eta4 = 4.2;
394 };
395 
397 struct TrustRegionResults {
399  TrustRegionResults(int size)
400  {
401  z.SetSize(size);
402  H_z.SetSize(size);
403  d_old.SetSize(size);
404  H_d_old.SetSize(size);
405  d.SetSize(size);
406  H_d.SetSize(size);
407  Pr.SetSize(size);
408  cauchy_point.SetSize(size);
409  H_cauchy_point.SetSize(size);
410  }
411 
413  void reset()
414  {
415  z = 0.0;
416  cauchy_point = 0.0;
417  }
418 
420  enum class Status
421  {
422  Interior,
423  NegativeCurvature,
424  OnBoundary,
425  NonDescentDirection
426  };
427 
429  mfem::Vector z;
431  mfem::Vector H_z;
433  mfem::Vector d_old;
435  mfem::Vector H_d_old;
437  mfem::Vector d;
439  mfem::Vector H_d;
441  mfem::Vector Pr;
443  mfem::Vector cauchy_point;
445  mfem::Vector H_cauchy_point;
447  Status interior_status = Status::Interior;
449  size_t cg_iterations_count = 0;
450 };
451 
453 void printTrustRegionInfo(double realObjective, double modelObjective, size_t cgIters, double trSize, bool willAccept)
454 {
455  mfem::out << "real energy = " << std::setw(13) << realObjective << ", model energy = " << std::setw(13)
456  << modelObjective << ", cg iter = " << std::setw(7) << cgIters << ", next tr size = " << std::setw(8)
457  << trSize << ", accepting = " << willAccept << std::endl;
458 }
459 
469 class TrustRegion : public mfem::NewtonSolver, public ConvergenceManagedNonlinearSolver {
470  protected:
472  mutable mfem::Vector x_pred;
474  mutable mfem::Vector r_pred;
476  mutable mfem::Vector scratch;
478  mutable std::vector<std::shared_ptr<mfem::Vector>> left_mosts;
480  mutable std::vector<std::shared_ptr<mfem::Vector>> H_left_mosts;
481 
483  NonlinearSolverOptions nonlinear_options;
485  LinearSolverOptions linear_options;
486 
489  Solver& tr_precond;
490 
492  mutable size_t print_level = 0;
493 
495  mutable bool grad_monolithic = false;
496 
497  std::shared_ptr<EquationSolverConvergenceManager> convergence_manager_ = nullptr;
498 
499  public:
501  mutable size_t num_hess_vecs = 0;
503  mutable size_t num_preconds = 0;
505  mutable size_t num_residuals = 0;
507  mutable size_t num_subspace_solves = 0;
509  mutable size_t num_jacobian_assembles = 0;
510 
511 #ifdef MFEM_USE_MPI
513  TrustRegion(MPI_Comm comm_, const NonlinearSolverOptions& nonlinear_opts, const LinearSolverOptions& linear_opts,
514  Solver& tPrec)
515  : mfem::NewtonSolver(comm_), nonlinear_options(nonlinear_opts), linear_options(linear_opts), tr_precond(tPrec)
516  {
517  }
518 #endif
519 
521  virtual ~TrustRegion()
522  {
523  if (grad_monolithic) delete grad;
524  }
525 
526  void setConvergenceManager(std::shared_ptr<EquationSolverConvergenceManager> convergence_manager) override
527  {
528  convergence_manager_ = std::move(convergence_manager);
529  }
530 
532  void projectToBoundaryWithCoefs(mfem::Vector& z, const mfem::Vector& d, double delta, double zz, double zd,
533  double dd) const
534  {
535  // find z + tau d
536  double deltadelta_m_zz = delta * delta - zz;
537  if (deltadelta_m_zz == 0) return; // already on boundary
538  double tau = (std::sqrt(deltadelta_m_zz * dd + zd * zd) - zd) / dd;
539  z.Add(tau, d);
540  }
541 
543  template <typename HessVecFunc>
544  void solveTheSubspaceProblem([[maybe_unused]] mfem::Vector& z, [[maybe_unused]] const HessVecFunc& hess_vec_func,
545  [[maybe_unused]] const std::vector<const mfem::Vector*> ds,
546  [[maybe_unused]] const std::vector<const mfem::Vector*> Hds,
547  [[maybe_unused]] const mfem::Vector& g, [[maybe_unused]] double delta,
548  [[maybe_unused]] int num_leftmost) const
549  {
550 #ifdef SMITH_USE_SLEPC
552  ++num_subspace_solves;
553 
554  std::vector<const mfem::Vector*> directions;
555  for (auto& d : ds) {
556  directions.emplace_back(d);
557  }
558  for (auto& left : left_mosts) {
559  directions.emplace_back(left.get());
560  }
561 
562  std::vector<const mfem::Vector*> H_directions;
563  for (auto& Hd : Hds) {
564  H_directions.emplace_back(Hd);
565  }
566  for (auto& H_left : H_left_mosts) {
567  H_directions.emplace_back(H_left.get());
568  }
569 
570  try {
571  std::tie(directions, H_directions) = removeDependentDirections(directions, H_directions);
572  } catch (const std::exception& e) {
573  if (print_level >= 2) {
574  mfem::out << "remove dependent directions failed with " << e.what() << std::endl;
575  }
576  return;
577  }
578 
579  mfem::Vector b(g);
580  b *= -1;
581 
582  mfem::Vector sol;
583  std::vector<std::shared_ptr<mfem::Vector>> leftvecs;
584  std::vector<double> leftvals;
585  double energy_change;
586 
587  try {
588  std::tie(sol, leftvecs, leftvals, energy_change) =
589  solveSubspaceProblem(directions, H_directions, b, delta, num_leftmost);
590  } catch (const std::exception& e) {
591  if (print_level == 1) {
592  mfem::out << "subspace solve failed with " << e.what() << std::endl;
593  }
594  return;
595  }
596 
597  left_mosts.clear();
598  for (auto& lv : leftvecs) {
599  left_mosts.emplace_back(std::move(lv));
600  }
601 
602  double base_energy = computeEnergy(g, hess_vec_func, z);
603  double subspace_energy = computeEnergy(g, hess_vec_func, sol);
604 
605  if (print_level >= 2) {
606  double leftval = leftvals.size() ? leftvals[0] : 1.0;
607  mfem::out << "Energy using subspace solver from: " << base_energy << ", to: " << subspace_energy << " / "
608  << energy_change << ". Min eig: " << leftval << std::endl;
609  }
610 
611  if (subspace_energy < base_energy) {
612  z = sol;
613  }
614 #endif
615  }
616 
618  void projectToBoundaryBetweenWithCoefs(mfem::Vector& z, const mfem::Vector& y, double trSize, double zz, double zy,
619  double yy) const
620  {
621  double dd = yy - 2 * zy + zz;
622  double zd = zy - zz;
623  double tau = (std::sqrt((trSize * trSize - zz) * dd + zd * zd) - zd) / dd;
624  z.Add(-tau, z);
625  z.Add(tau, y);
626  }
627 
629  void doglegStep(const mfem::Vector& cp, const mfem::Vector& newtonP, double trSize, mfem::Vector& s) const
630  {
632  // MRT, could optimize some of these eventually, compute on the outside and save
633  double cc = Dot(cp, cp);
634  double nn = Dot(newtonP, newtonP);
635  double tt = trSize * trSize;
636 
637  s = 0.0;
638  if (cc >= tt) {
639  add(s, std::sqrt(tt / cc), cp, s);
640  } else if (cc > nn) {
641  if (print_level >= 2) {
642  mfem::out << "cp outside newton, preconditioner likely inaccurate\n";
643  }
644  add(s, 1.0, cp, s);
645  } else if (nn > tt) { // on the dogleg (we have nn >= cc, and tt >= cc)
646  add(s, 1.0, cp, s);
647  double cn = Dot(cp, newtonP);
648  projectToBoundaryBetweenWithCoefs(s, newtonP, trSize, cc, cn, nn);
649  } else {
650  s = newtonP;
651  }
652  }
653 
655  template <typename HessVecFunc>
656  double computeEnergy(const mfem::Vector& r_local, const HessVecFunc& H, const mfem::Vector& z) const
657  {
659  double rz = Dot(r_local, z);
660  mfem::Vector tmp(r_local);
661  tmp = 0.0;
662  H(z, tmp);
663  return rz + 0.5 * Dot(z, tmp);
664  }
665 
667  template <typename HessVecFunc, typename PrecondFunc>
668  void solveTrustRegionModelProblem(const mfem::Vector& r0, mfem::Vector& rCurrent, HessVecFunc hess_vec_func,
669  PrecondFunc precond, const TrustRegionSettings& settings, double& trSize,
670  TrustRegionResults& results) const
671  {
673  // minimize r0@z + 0.5*z@J@z
674  results.interior_status = TrustRegionResults::Status::Interior;
675  results.cg_iterations_count = 0;
676 
677  auto& z = results.z;
678  auto& cgIter = results.cg_iterations_count;
679  auto& d = results.d;
680  auto& Pr = results.Pr;
681  auto& Hd = results.H_d;
682 
683  const double cg_tol_squared = settings.cg_tol * settings.cg_tol;
684 
685  if (Dot(r0, r0) <= cg_tol_squared && settings.min_cg_iterations == 0) {
686  if (print_level >= 2) {
687  mfem::out << "Trust region solution state within tolerance on first iteration."
688  << "\n";
689  }
690  return;
691  }
692 
693  rCurrent = r0;
694  precond(rCurrent, Pr);
695 
696  // d = -Pr
697  d = Pr;
698  d *= -1.0;
699 
700  z = 0.0;
701  double zz = 0.;
702  double rPr = Dot(rCurrent, Pr);
703  double zd = 0.0;
704  double dd = Dot(d, d);
705 
706  // std::cout << "initial energy = " << computeEnergy(r0, hess_vec_func, z) << std::endl;
707 
708  for (cgIter = 1; cgIter <= settings.max_cg_iterations; ++cgIter) {
709  // check if this is a descent direction
710  if (Dot(d, rCurrent) > 0) {
711  d *= -1;
712  results.interior_status = TrustRegionResults::Status::NonDescentDirection;
713  }
714 
715  hess_vec_func(d, Hd);
716  const double curvature = Dot(d, Hd);
717  const double alphaCg = curvature != 0.0 ? rPr / curvature : 0.0;
718 
719  auto& zPred = Pr; // re-use Pr memory.
720  // This predicted step will no longer be used by the time Pr is, so we can avoid an extra
721  // vector floating around
722  add(z, alphaCg, d, zPred);
723  double zzNp1 = Dot(zPred, zPred);
724 
725  const bool go_to_boundary = curvature <= 0 || zzNp1 >= trSize * trSize;
726  if (go_to_boundary) {
727  projectToBoundaryWithCoefs(z, d, trSize, zz, zd, dd);
728  if (curvature <= 0) {
729  results.interior_status = TrustRegionResults::Status::NegativeCurvature;
730  } else {
731  results.interior_status = TrustRegionResults::Status::OnBoundary;
732  }
733  return;
734  }
735 
736  z = zPred;
737 
738  if (results.interior_status == TrustRegionResults::Status::NonDescentDirection) {
739  if (print_level >= 2) {
740  mfem::out << "Found a non descent direction\n";
741  }
742  return;
743  }
744 
745  add(rCurrent, alphaCg, Hd, rCurrent);
746 
747  precond(rCurrent, Pr);
748  double rPrNp1 = Dot(rCurrent, Pr);
749 
750  if (Dot(rCurrent, rCurrent) <= cg_tol_squared && cgIter >= settings.min_cg_iterations) {
751  return;
752  }
753 
754  double beta = rPrNp1 / rPr;
755  rPr = rPrNp1;
756  add(-1.0, Pr, beta, d, d);
757 
758  zz = zzNp1;
759  zd = Dot(z, d);
760  dd = Dot(d, d);
761  }
762  cgIter--; // if all cg iterations are taken, correct for output
763  }
764 
766  void assembleJacobian(const mfem::Vector& x) const
767  {
769  ++num_jacobian_assembles;
770  if (grad_monolithic) {
771  delete grad;
772  grad = nullptr;
773  grad_monolithic = false;
774  }
775  mfem::Operator& assembled_gradient = oper->GetGradient(x);
776  grad_monolithic = monolithicizeOperatorIfNeeded(linear_options, assembled_gradient, grad);
777  }
778 
780  mfem::real_t computeResidual(const mfem::Vector& x_, mfem::Vector& r_) const
781  {
783  ++num_residuals;
784  oper->Mult(x_, r_);
785  return Norm(r_);
786  }
787 
788  ConvergenceStatus evaluateConvergence(const mfem::Vector& x_, mfem::Vector& r_) const
789  {
790  ConvergenceStatus status;
791  status.global_norm = std::numeric_limits<double>::max();
792  status.global_goal = std::numeric_limits<double>::max();
793  try {
794  status.global_norm = computeResidual(x_, r_);
795  if (convergence_manager_) {
796  status = convergence_manager_->evaluate(1.0, r_);
797  } else {
798  status.block_norms = {status.global_norm};
799  status.global_goal = std::max(rel_tol * initial_norm, abs_tol);
800  status.global_converged = status.global_norm <= status.global_goal;
801  status.converged = status.global_converged;
802  }
803  } catch (const std::exception&) {
804  status.global_norm = std::numeric_limits<double>::max();
805  status.global_goal = std::numeric_limits<double>::max();
806  }
807  return status;
808  }
809 
811  void hessVec(const mfem::Vector& x_, mfem::Vector& v_) const
812  {
814  ++num_hess_vecs;
815  grad->Mult(x_, v_);
816  }
817 
819  void precond(const mfem::Vector& x_, mfem::Vector& v_) const
820  {
822  ++num_preconds;
823  tr_precond.Mult(x_, v_);
824  };
825 
827  void Mult(const mfem::Vector&, mfem::Vector& X) const override
828  {
829  MFEM_ASSERT(oper != NULL, "the Operator is not set (use SetOperator).");
830  MFEM_ASSERT(prec != NULL, "the Solver is not set (use SetSolver).");
831 
832  print_level = print_options.iterations ? 1 : print_level;
833  print_level = print_options.summary ? 2 : print_level;
834 
835  using real_t = mfem::real_t;
836 
837  num_hess_vecs = 0;
838  num_preconds = 0;
839  num_residuals = 0;
840  num_subspace_solves = 0;
841  num_jacobian_assembles = 0;
842 
843  ConvergenceStatus status = evaluateConvergence(X, r);
844  real_t norm = status.global_norm;
845  real_t norm_goal = status.global_goal;
846  initial_norm = norm;
847  if (norm == 0.0) return;
848 
849  if (print_level == 1) {
850  mfem::out << "TrustRegion iteration " << std::setw(3) << 0 << " : ||r|| = " << std::setw(13) << norm << "\n";
851  }
852 
853  prec->iterative_mode = false;
854  tr_precond.iterative_mode = false;
855 
856  // local arrays
857  x_pred.SetSize(X.Size());
858  x_pred = 0.0;
859  r_pred.SetSize(X.Size());
860  r_pred = 0.0;
861  scratch.SetSize(X.Size());
862  scratch = 0.0;
863 
864  TrustRegionResults trResults(X.Size());
865  TrustRegionSettings settings;
866  settings.min_cg_iterations = static_cast<size_t>(nonlinear_options.min_iterations);
867  settings.max_cg_iterations = static_cast<size_t>(linear_options.max_iterations);
868  settings.cg_tol = 0.5 * norm_goal;
869 
870  int subspace_option = nonlinear_options.subspace_option;
871  int num_leftmost = nonlinear_options.num_leftmost;
872 
873  scratch = 1.0;
874  double tr_size = nonlinear_options.trust_region_scaling * std::sqrt(Dot(scratch, scratch));
875  size_t cumulative_cg_iters_from_last_precond_update = 0;
876 
877  int it = 0;
878  for (; true; it++) {
879  MFEM_ASSERT(mfem::IsFinite(norm), "norm = " << norm);
880  if (print_level >= 2) {
881  mfem::out << "TrustRegion iteration " << std::setw(3) << it << " : ||r|| = " << std::setw(13) << norm;
882  if (it > 0) {
883  mfem::out << ", ||r||/||r_0|| = " << std::setw(13) << (initial_norm != 0.0 ? norm / initial_norm : norm);
884  mfem::out << ", x_incr = " << std::setw(13) << trResults.d.Norml2();
885  } else {
886  mfem::out << ", norm goal = " << std::setw(13) << norm_goal;
887  }
888  mfem::out << '\n';
889  }
890 
891  if (print_level >= 1 && (norm != norm)) {
892  mfem::out << "Initial residual for trust-region iteration is undefined/nan." << std::endl;
893  mfem::out << "TrustRegion: No convergence!\n";
894  return;
895  }
896 
897  if (status.converged && it >= nonlinear_options.min_iterations) {
898  converged = true;
899  break;
900  } else if (it >= max_iter) {
901  converged = false;
902  break;
903  }
904 
905  assembleJacobian(X);
906 
907  if (it == 0 || (trResults.cg_iterations_count >= settings.max_cg_iterations ||
908  cumulative_cg_iters_from_last_precond_update >= settings.max_cumulative_iteration)) {
909  tr_precond.SetOperator(*grad);
910  cumulative_cg_iters_from_last_precond_update = 0;
911  }
912 
913  auto hess_vec_func = [&](const mfem::Vector& x_, mfem::Vector& v_) { hessVec(x_, v_); };
914  auto precond_func = [&](const mfem::Vector& x_, mfem::Vector& v_) { precond(x_, v_); };
915 
916  double cauchyPointNormSquared = tr_size * tr_size;
917  trResults.reset();
918 
919  hess_vec_func(r, trResults.H_d);
920  const double gKg = Dot(r, trResults.H_d);
921  if (gKg > 0) {
922  const double alphaCp = -Dot(r, r) / gKg;
923  add(trResults.cauchy_point, alphaCp, r, trResults.cauchy_point);
924  cauchyPointNormSquared = Dot(trResults.cauchy_point, trResults.cauchy_point);
925  } else {
926  const double alphaTr = -tr_size / std::sqrt(Dot(r, r));
927  add(trResults.cauchy_point, alphaTr, r, trResults.cauchy_point);
928  if (print_level >= 2) {
929  mfem::out << "Negative curvature un-preconditioned cauchy point direction found."
930  << "\n";
931  }
932  }
933 
934  if (cauchyPointNormSquared >= tr_size * tr_size) {
935  if (print_level >= 2) {
936  mfem::out << "Un-preconditioned gradient cauchy point outside trust region, step size = "
937  << std::sqrt(cauchyPointNormSquared) << "\n";
938  }
939  trResults.cauchy_point *= (tr_size / std::sqrt(cauchyPointNormSquared));
940  trResults.z = trResults.cauchy_point;
941 
942  trResults.cg_iterations_count = 1;
943  trResults.interior_status = TrustRegionResults::Status::OnBoundary;
944  } else {
945  settings.cg_tol = std::max(0.5 * norm_goal, 5e-5 * norm);
946  solveTrustRegionModelProblem(r, scratch, hess_vec_func, precond_func, settings, tr_size, trResults);
947  }
948  cumulative_cg_iters_from_last_precond_update += trResults.cg_iterations_count;
949 
950  bool have_computed_Hvs = false;
951 
952  int lineSearchIter = 0;
953  while (lineSearchIter <= nonlinear_options.max_line_search_iterations) {
954  ++lineSearchIter;
955 
956  doglegStep(trResults.cauchy_point, trResults.z, tr_size, trResults.d);
957 
958  bool use_with_option1 =
959  (subspace_option >= 1) && (trResults.interior_status == TrustRegionResults::Status::NonDescentDirection ||
960  trResults.interior_status == TrustRegionResults::Status::NegativeCurvature ||
961  ((Norm(trResults.d) > (1.0 - 1.0e-6) * tr_size) && lineSearchIter > 1));
962  bool use_with_option2 = (subspace_option >= 2) && (Norm(trResults.d) > (1.0 - 1.0e-6) * tr_size);
963  bool use_with_option3 = (subspace_option >= 3);
964 
965  if (use_with_option1 || use_with_option2 || use_with_option3) {
966  if (!have_computed_Hvs) {
967  have_computed_Hvs = true;
968  hess_vec_func(trResults.z, trResults.H_z);
969  hess_vec_func(trResults.d_old, trResults.H_d_old);
970  hess_vec_func(trResults.cauchy_point, trResults.H_cauchy_point);
971  }
972 
973  H_left_mosts.clear();
974  for (auto& left : left_mosts) {
975  H_left_mosts.emplace_back(std::make_shared<mfem::Vector>(*left));
976  hess_vec_func(*left, *H_left_mosts.back());
977  }
978 
979  std::vector<const mfem::Vector*> ds{&trResults.z, &trResults.d_old, &trResults.cauchy_point};
980  std::vector<const mfem::Vector*> H_ds{&trResults.H_z, &trResults.H_d_old, &trResults.H_cauchy_point};
981  solveTheSubspaceProblem(trResults.d, hess_vec_func, ds, H_ds, r, tr_size, num_leftmost);
982  }
983 
984  static constexpr double roundOffTol = 0.0; // 1e-14;
985 
986  hess_vec_func(trResults.d, trResults.H_d);
987  double dHd = Dot(trResults.d, trResults.H_d);
988  double modelObjective = Dot(r, trResults.d) + 0.5 * dHd - roundOffTol;
989 
990  add(X, trResults.d, x_pred);
991 
992  double realObjective = std::numeric_limits<double>::max();
993  double normPred = std::numeric_limits<double>::max();
994  try {
995  auto predicted_status = evaluateConvergence(x_pred, r_pred);
996  normPred = predicted_status.global_norm;
997  double obj1 = 0.5 * (Dot(r, trResults.d) + Dot(r_pred, trResults.d)) - roundOffTol;
998  realObjective = obj1;
999  if (predicted_status.converged) {
1000  trResults.d_old = trResults.d;
1001  X = x_pred;
1002  r = r_pred;
1003  status = predicted_status;
1004  norm = status.global_norm;
1005  if (print_level >= 2) {
1006  printTrustRegionInfo(realObjective, modelObjective, trResults.cg_iterations_count, tr_size, true);
1007  trResults.cg_iterations_count = 0;
1008  }
1009  break;
1010  }
1011  } catch (const std::exception&) {
1012  realObjective = std::numeric_limits<double>::max();
1013  normPred = std::numeric_limits<double>::max();
1014  }
1015 
1016  double modelImprove = -modelObjective;
1017  double realImprove = -realObjective;
1018 
1019  double rho = realImprove / modelImprove;
1020  if (modelObjective > 0) {
1021  if (print_level >= 2) {
1022  mfem::out << "Found a positive model objective increase. Debug if you see this.\n";
1023  }
1024  rho = realImprove / -modelImprove;
1025  }
1026 
1027  // std::cout << "rho , stuff = " << rho << " " << settings.eta3 << std::endl;
1028  // std::cout << "stat = "<< trResults.interior_status << std::endl;
1029 
1030  if (!(rho >= settings.eta2) ||
1031  rho > settings.eta4) { // not enough progress, decrease trust region. write it this way to handle NaNs.
1032  tr_size *= settings.t1;
1033  } else if ((rho > settings.eta3 && rho <= settings.eta4 &&
1034  trResults.interior_status == TrustRegionResults::Status::OnBoundary) ||
1035  (rho > 0.95 && rho < 1.05 &&
1036  trResults.interior_status ==
1037  TrustRegionResults::Status::NegativeCurvature)) { // good progress, on boundary, increase trust
1038  // region
1039  tr_size *= settings.t2;
1040  }
1041 
1042  // eventually extend to handle this case to handle occasional roundoff issues
1043  // modelRes = g + Jd
1044  // modelResNorm = np.linalg.norm(modelRes)
1045  // realResNorm = np.linalg.norm(gy)
1046  bool willAccept = rho >= settings.eta1 && rho <= settings.eta4; // or (rho >= -0 and realResNorm <= gNorm)
1047 
1048  if (print_level >= 2) {
1049  printTrustRegionInfo(realObjective, modelObjective, trResults.cg_iterations_count, tr_size, willAccept);
1050  trResults.cg_iterations_count =
1051  0; // zero this output so it doesn't look like the linesearch is doing cg iterations
1052  }
1053 
1054  if (willAccept) {
1055  trResults.d_old = trResults.d;
1056  X = x_pred;
1057  r = r_pred;
1058  status = convergence_manager_ ? convergence_manager_->evaluate(1.0, r_pred) : status;
1059  norm = normPred;
1060  break;
1061  }
1062  }
1063  }
1064 
1065  final_iter = it;
1066  final_norm = norm;
1067 
1068  if (print_level == 1) {
1069  mfem::out << "TrustRegion iteration " << std::setw(3) << final_iter << " : ||r|| = " << std::setw(13) << norm
1070  << '\n';
1071  }
1072  if (!converged && print_level >= 1) { // (print_options.summary || print_options.warnings)) {
1073  mfem::out << "TrustRegion: No convergence!\n";
1074  }
1075 
1076  if (false && print_level >= 2) {
1077  mfem::out << "num hess vecs = " << num_hess_vecs << "\n";
1078  mfem::out << "num preconds = " << num_preconds << "\n";
1079  mfem::out << "num residuals = " << num_residuals << "\n";
1080  mfem::out << "num subspace solves = " << num_subspace_solves << "\n";
1081  mfem::out << "num jacobian_assembles = " << num_jacobian_assembles << "\n";
1082  }
1083  }
1084 };
1086 
1088 {
1089  auto [lin_solver, preconditioner] = buildLinearSolverAndPreconditioner(lin_opts, comm);
1090 
1091  lin_solver_ = std::move(lin_solver);
1092  preconditioner_ = std::move(preconditioner);
1093  nonlin_solver_ = buildNonlinearSolver(nonlinear_opts, lin_opts, *preconditioner_, comm);
1094  convergence_manager_ = std::make_shared<EquationSolverConvergenceManager>(comm, nonlinear_opts.absolute_tol,
1095  nonlinear_opts.relative_tol);
1096  attachConvergenceManager();
1097 }
1098 
1099 EquationSolver::EquationSolver(std::unique_ptr<mfem::NewtonSolver> nonlinear_solver,
1100  std::unique_ptr<mfem::Solver> linear_solver,
1101  std::unique_ptr<mfem::Solver> preconditioner)
1102 {
1103  SLIC_ERROR_ROOT_IF(!nonlinear_solver, "Nonlinear solvers must be given to construct an EquationSolver");
1104  SLIC_ERROR_ROOT_IF(!linear_solver, "Linear solvers must be given to construct an EquationSolver");
1105 
1106  nonlin_solver_ = std::move(nonlinear_solver);
1107  lin_solver_ = std::move(linear_solver);
1108  preconditioner_ = std::move(preconditioner);
1109 }
1110 
1111 void EquationSolver::attachConvergenceManager() const
1112 {
1113  if (!convergence_manager_ || !nonlin_solver_) {
1114  return;
1115  }
1116 
1117  if (auto* managed_solver = dynamic_cast<ConvergenceManagedNonlinearSolver*>(nonlin_solver_.get())) {
1118  managed_solver->setConvergenceManager(convergence_manager_);
1119  }
1120 }
1121 
1122 void EquationSolver::initializeConvergenceManager(double abs_tol, double rel_tol, MPI_Comm comm) const
1123 {
1124  if (!convergence_manager_) {
1125  convergence_manager_ = std::make_shared<EquationSolverConvergenceManager>(comm, abs_tol, rel_tol);
1126  } else {
1127  convergence_manager_->setTolerances(abs_tol, rel_tol);
1128  }
1129  attachConvergenceManager();
1130 }
1131 
1132 void EquationSolver::setOperator(const mfem::Operator& op)
1133 {
1134  attachConvergenceManager();
1135  nonlin_solver_->SetOperator(op);
1136 
1137  // Now that the nonlinear solver knows about the operator, we can set its linear solver
1138  if (!nonlin_solver_set_solver_called_) {
1139  nonlin_solver_->SetSolver(linearSolver());
1140  nonlin_solver_set_solver_called_ = true;
1141  }
1142 }
1143 
1144 void EquationSolver::setConvergenceTolerances(double abs_tol, double rel_tol, MPI_Comm comm) const
1145 {
1146  initializeConvergenceManager(abs_tol, rel_tol, comm);
1147 }
1148 
1150 {
1151  if (convergence_manager_) {
1152  convergence_manager_->reset();
1153  }
1154 }
1155 
1156 void EquationSolver::solve(mfem::Vector& x) const
1157 {
1159  mfem::Vector zero(x);
1160  zero = 0.0;
1161  // KINSOL does not handle non-zero RHS, so we enforce that the RHS
1162  // of the nonlinear system is zero
1163  nonlin_solver_->Mult(zero, x);
1164 }
1165 
1166 void SuperLUSolver::Mult(const mfem::Vector& input, mfem::Vector& output) const
1167 {
1168  SLIC_ERROR_ROOT_IF(!superlu_mat_, "Operator must be set prior to solving with SuperLU");
1169 
1170  // Use the underlying MFEM-based solver and SuperLU matrix type to solve the system
1171  superlu_solver_.Mult(input, output);
1172 }
1173 
1189 std::unique_ptr<mfem::HypreParMatrix> buildMonolithicMatrix(const mfem::BlockOperator& block_operator)
1190 {
1191  int row_blocks = block_operator.NumRowBlocks();
1192  int col_blocks = block_operator.NumColBlocks();
1193 
1194  SLIC_ERROR_ROOT_IF(row_blocks != col_blocks, "Attempted to use a direct solver on a non-square block system.");
1195 
1196  mfem::Array2D<const mfem::HypreParMatrix*> hypre_blocks(row_blocks, col_blocks);
1197 
1198  for (int i = 0; i < row_blocks; ++i) {
1199  for (int j = 0; j < col_blocks; ++j) {
1200  // checks for presence of empty (null) blocks, which happen fairly common in multirank contact
1201  if (!block_operator.IsZeroBlock(i, j)) {
1202  auto* hypre_block = dynamic_cast<const mfem::HypreParMatrix*>(&block_operator.GetBlock(i, j));
1203  SLIC_ERROR_ROOT_IF(!hypre_block,
1204  "Trying to use SuperLU on a block operator that does not contain HypreParMatrix blocks.");
1205 
1206  hypre_blocks(i, j) = hypre_block;
1207  } else {
1208  hypre_blocks(i, j) = nullptr;
1209  }
1210  }
1211  }
1212 
1213  // Note that MFEM passes ownership of this matrix to the caller.
1214  // MFEM creates a new monolithic matrix (not a view), so this is a COPY operation.
1215  return std::unique_ptr<mfem::HypreParMatrix>(mfem::HypreParMatrixFromBlocks(hypre_blocks));
1216 }
1217 
1218 void SuperLUSolver::SetOperator(const mfem::Operator& op)
1219 {
1220  // Check if this is a block operator
1221  auto* block_operator = dynamic_cast<const mfem::BlockOperator*>(&op);
1222 
1223  // If it is, make a monolithic system from the underlying blocks
1224  if (block_operator) {
1225  monolithic_mat_ = buildMonolithicMatrix(*block_operator);
1226 
1227  superlu_mat_ = std::make_unique<mfem::SuperLURowLocMatrix>(*monolithic_mat_);
1228  } else {
1229  // If this is not a block system, check that the input operator is a HypreParMatrix as expected
1230  auto* matrix = dynamic_cast<const mfem::HypreParMatrix*>(&op);
1231 
1232  SLIC_ERROR_ROOT_IF(!matrix, "Matrix must be an assembled HypreParMatrix for use with SuperLU");
1233 
1234  superlu_mat_ = std::make_unique<mfem::SuperLURowLocMatrix>(*matrix);
1235  }
1236  height = op.Height();
1237  width = op.Width();
1238  superlu_solver_.SetOperator(*superlu_mat_);
1239 }
1240 
1241 #ifdef MFEM_USE_STRUMPACK
1242 
1243 void StrumpackSolver::Mult(const mfem::Vector& input, mfem::Vector& output) const
1244 {
1245  SLIC_ERROR_ROOT_IF(!strumpack_mat_, "Operator must be set prior to solving with Strumpack");
1246 
1247  // Use the underlying MFEM-based solver and Strumpack matrix type to solve the system
1248  strumpack_solver_.Mult(input, output);
1249 }
1250 
1251 void StrumpackSolver::SetOperator(const mfem::Operator& op)
1252 {
1253  // Check if this is a block operator
1254  auto* block_operator = dynamic_cast<const mfem::BlockOperator*>(&op);
1255 
1256  // If it is, make a monolithic system from the underlying blocks
1257  if (block_operator) {
1258  monolithic_mat_ = buildMonolithicMatrix(*block_operator);
1259 
1260  strumpack_mat_ = std::make_unique<mfem::STRUMPACKRowLocMatrix>(*monolithic_mat_);
1261  } else {
1262  // If this is not a block system, check that the input operator is a HypreParMatrix as expected
1263  auto* matrix = dynamic_cast<const mfem::HypreParMatrix*>(&op);
1264 
1265  SLIC_ERROR_ROOT_IF(!matrix, "Matrix must be an assembled HypreParMatrix for use with Strumpack");
1266 
1267  strumpack_mat_ = std::make_unique<mfem::STRUMPACKRowLocMatrix>(*matrix);
1268  }
1269  height = op.Height();
1270  width = op.Width();
1271  strumpack_solver_.SetOperator(*strumpack_mat_);
1272 }
1273 
1274 #endif
1275 
1276 std::unique_ptr<mfem::NewtonSolver> buildNonlinearSolver(NonlinearSolverOptions nonlinear_opts,
1277  const LinearSolverOptions& linear_opts, mfem::Solver& prec,
1278  MPI_Comm comm)
1279 {
1280  std::unique_ptr<mfem::NewtonSolver> nonlinear_solver;
1281 
1282  if (nonlinear_opts.nonlin_solver == NonlinearSolver::Newton) {
1283  nonlinear_opts.max_line_search_iterations = 0;
1284  SLIC_ERROR_ROOT_IF(nonlinear_opts.min_iterations != 0, "Newton's method does not support nonzero min_iterations");
1285  nonlinear_solver = std::make_unique<NewtonSolver>(comm, nonlinear_opts, linear_opts);
1286  } else if (nonlinear_opts.nonlin_solver == NonlinearSolver::LBFGS) {
1287  nonlinear_opts.max_line_search_iterations = 0;
1288  SLIC_ERROR_ROOT_IF(nonlinear_opts.min_iterations != 0, "LBFGS does not support nonzero min_iterations");
1289  nonlinear_solver = std::make_unique<mfem::LBFGSSolver>(comm);
1290  } else if (nonlinear_opts.nonlin_solver == NonlinearSolver::NewtonLineSearch) {
1291  nonlinear_solver = std::make_unique<NewtonSolver>(comm, nonlinear_opts, linear_opts);
1292  } else if (nonlinear_opts.nonlin_solver == NonlinearSolver::TrustRegion) {
1293  nonlinear_solver = std::make_unique<TrustRegion>(comm, nonlinear_opts, linear_opts, prec);
1294 #ifdef SMITH_USE_PETSC
1295  } else if (nonlinear_opts.nonlin_solver == NonlinearSolver::PetscNewton) {
1296  nonlinear_solver = std::make_unique<mfem_ext::PetscNewtonSolver>(comm, nonlinear_opts);
1297  } else if (nonlinear_opts.nonlin_solver == NonlinearSolver::PetscNewtonBacktracking) {
1298  nonlinear_solver = std::make_unique<mfem_ext::PetscNewtonSolver>(comm, nonlinear_opts);
1299  } else if (nonlinear_opts.nonlin_solver == NonlinearSolver::PetscNewtonCriticalPoint) {
1300  nonlinear_solver = std::make_unique<mfem_ext::PetscNewtonSolver>(comm, nonlinear_opts);
1301  } else if (nonlinear_opts.nonlin_solver == NonlinearSolver::PetscTrustRegion) {
1302  nonlinear_solver = std::make_unique<mfem_ext::PetscNewtonSolver>(comm, nonlinear_opts);
1303 #endif
1304  }
1305  // KINSOL
1306  else {
1307 #ifdef SMITH_USE_SUNDIALS
1308  nonlinear_opts.max_line_search_iterations = 0;
1309  SLIC_ERROR_ROOT_IF(nonlinear_opts.min_iterations != 0, "kinsol solvers do not support min_iterations");
1310 
1311  int kinsol_strat = KIN_NONE;
1312 
1313  switch (nonlinear_opts.nonlin_solver) {
1315  kinsol_strat = KIN_NONE;
1316  break;
1318  kinsol_strat = KIN_LINESEARCH;
1319  break;
1321  kinsol_strat = KIN_PICARD;
1322  break;
1323  default:
1324  kinsol_strat = KIN_NONE;
1325  SLIC_ERROR_ROOT("Unknown KINSOL nonlinear solver type given.");
1326  }
1327  auto kinsol_solver = std::make_unique<mfem::KINSolver>(comm, kinsol_strat, true);
1328  nonlinear_solver = std::move(kinsol_solver);
1329 #else
1330  SLIC_ERROR_ROOT("KINSOL was not enabled when MFEM was built");
1331 #endif
1332  }
1333 
1334  nonlinear_solver->SetRelTol(nonlinear_opts.relative_tol);
1335  nonlinear_solver->SetAbsTol(nonlinear_opts.absolute_tol);
1336  nonlinear_solver->SetMaxIter(nonlinear_opts.max_iterations);
1337  nonlinear_solver->SetPrintLevel(nonlinear_opts.print_level);
1338 
1339  // Iterative mode indicates we do not zero out the initial guess during the
1340  // nonlinear solver call. This is required as we apply the essential boundary
1341  // conditions before the nonlinear solver is applied.
1342  nonlinear_solver->iterative_mode = true;
1343 
1344  return nonlinear_solver;
1345 }
1346 
1347 std::pair<std::unique_ptr<mfem::Solver>, std::unique_ptr<mfem::Solver>> buildLinearSolverAndPreconditioner(
1348  LinearSolverOptions linear_opts, MPI_Comm comm)
1349 {
1350  auto preconditioner = buildPreconditioner(linear_opts, comm);
1351 
1352  if (linear_opts.linear_solver == LinearSolver::SuperLU) {
1353  auto lin_solver = std::make_unique<SuperLUSolver>(linear_opts.print_level, comm);
1354  return {std::move(lin_solver), std::move(preconditioner)};
1355  }
1356 
1357 #ifdef MFEM_USE_STRUMPACK
1358 
1359  if (linear_opts.linear_solver == LinearSolver::Strumpack) {
1360  auto lin_solver = std::make_unique<StrumpackSolver>(linear_opts.print_level, comm);
1361  return {std::move(lin_solver), std::move(preconditioner)};
1362  }
1363 
1364 #endif
1365 
1366  std::unique_ptr<mfem::IterativeSolver> iter_lin_solver;
1367 
1368  switch (linear_opts.linear_solver) {
1369  case LinearSolver::CG:
1370  iter_lin_solver = std::make_unique<mfem::CGSolver>(comm);
1371  break;
1372  case LinearSolver::GMRES:
1373  iter_lin_solver = std::make_unique<mfem::GMRESSolver>(comm);
1374  break;
1375 #ifdef SMITH_USE_PETSC
1376  case LinearSolver::PetscCG:
1377  iter_lin_solver = std::make_unique<smith::mfem_ext::PetscKSPSolver>(comm, KSPCG, std::string());
1378  break;
1380  iter_lin_solver = std::make_unique<smith::mfem_ext::PetscKSPSolver>(comm, KSPGMRES, std::string());
1381  break;
1382 #else
1383  case LinearSolver::PetscCG:
1385  SLIC_ERROR_ROOT("PETSc linear solver requested for non-PETSc build.");
1386  exit(1);
1387  break;
1388 #endif
1389  case LinearSolver::None:
1390  iter_lin_solver = std::make_unique<PreconditionerOnlySolver>(comm);
1391  break;
1392  default:
1393  SLIC_ERROR_ROOT("Linear solver type not recognized.");
1394  exit(1);
1395  }
1396 
1397  iter_lin_solver->SetRelTol(linear_opts.relative_tol);
1398  iter_lin_solver->SetAbsTol(linear_opts.absolute_tol);
1399  iter_lin_solver->SetMaxIter(linear_opts.max_iterations);
1400  iter_lin_solver->SetPrintLevel(linear_opts.print_level);
1401 
1402  if (preconditioner) {
1403  iter_lin_solver->SetPreconditioner(*preconditioner);
1404  }
1405 
1406  return {std::move(iter_lin_solver), std::move(preconditioner)};
1407 }
1408 
1410 {
1411  return !linearSolverSupportsBlockOperator(linear_opts.linear_solver) ||
1412  !preconditionerSupportsBlockOperator(linear_opts.preconditioner);
1413 }
1414 
1415 #ifdef MFEM_USE_AMGX
1416 std::unique_ptr<mfem::AmgXSolver> buildAMGX(const AMGXOptions& options, const MPI_Comm comm)
1417 {
1418  auto amgx = std::make_unique<mfem::AmgXSolver>();
1419  conduit::Node options_node;
1420  options_node["config_version"] = 2;
1421  auto& solver_options = options_node["solver"];
1422  solver_options["solver"] = "AMG";
1423  solver_options["presweeps"] = 1;
1424  solver_options["postsweeps"] = 2;
1425  solver_options["interpolator"] = "D2";
1426  solver_options["max_iters"] = 2;
1427  solver_options["convergence"] = "ABSOLUTE";
1428  solver_options["cycle"] = "V";
1429 
1430  if (options.verbose) {
1431  options_node["solver/obtain_timings"] = 1;
1432  options_node["solver/monitor_residual"] = 1;
1433  options_node["solver/print_solve_stats"] = 1;
1434  }
1435 
1436  // TODO: Use magic_enum here when we can switch to GCC 9+
1437  // This is an immediately-invoked lambda so that the map
1438  // can be const without needed to initialize all the values
1439  // in the constructor
1440  static const auto solver_names = []() {
1441  std::unordered_map<AMGXSolver, std::string> names;
1442  names[AMGXSolver::AMG] = "AMG";
1443  names[AMGXSolver::PCGF] = "PCGF";
1444  names[AMGXSolver::CG] = "CG";
1445  names[AMGXSolver::PCG] = "PCG";
1446  names[AMGXSolver::PBICGSTAB] = "PBICGSTAB";
1447  names[AMGXSolver::BICGSTAB] = "BICGSTAB";
1448  names[AMGXSolver::FGMRES] = "FGMRES";
1449  names[AMGXSolver::JACOBI_L1] = "JACOBI_L1";
1450  names[AMGXSolver::GS] = "GS";
1451  names[AMGXSolver::POLYNOMIAL] = "POLYNOMIAL";
1452  names[AMGXSolver::KPZ_POLYNOMIAL] = "KPZ_POLYNOMIAL";
1453  names[AMGXSolver::BLOCK_JACOBI] = "BLOCK_JACOBI";
1454  names[AMGXSolver::MULTICOLOR_GS] = "MULTICOLOR_GS";
1455  names[AMGXSolver::MULTICOLOR_DILU] = "MULTICOLOR_DILU";
1456  return names;
1457  }();
1458 
1459  options_node["solver/solver"] = solver_names.at(options.solver);
1460  options_node["solver/smoother"] = solver_names.at(options.smoother);
1461 
1462  // Treat the string as the config (not a filename)
1463  amgx->ReadParameters(options_node.to_json(), mfem::AmgXSolver::INTERNAL);
1464  amgx->InitExclusiveGPU(comm);
1465 
1466  return amgx;
1467 }
1468 #endif
1469 
1470 std::unique_ptr<mfem::Solver> buildPreconditioner(LinearSolverOptions linear_opts, [[maybe_unused]] MPI_Comm comm)
1471 {
1472  std::unique_ptr<mfem::Solver> preconditioner_solver;
1473  auto preconditioner = linear_opts.preconditioner;
1474  auto preconditioner_print_level = linear_opts.preconditioner_print_level;
1475 
1476  // Handle the preconditioner - currently just BoomerAMG and HypreSmoother are supported
1477  if (preconditioner == Preconditioner::HypreAMG) {
1478  auto amg_preconditioner = std::make_unique<mfem::HypreBoomerAMG>();
1479  amg_preconditioner->SetPrintLevel(preconditioner_print_level);
1480  preconditioner_solver = std::move(amg_preconditioner);
1481  } else if (preconditioner == Preconditioner::HypreJacobi) {
1482  auto jac_preconditioner = std::make_unique<mfem::HypreSmoother>();
1483  jac_preconditioner->SetType(mfem::HypreSmoother::Type::Jacobi);
1484  preconditioner_solver = std::move(jac_preconditioner);
1485  } else if (preconditioner == Preconditioner::HypreL1Jacobi) {
1486  auto jacl1_preconditioner = std::make_unique<mfem::HypreSmoother>();
1487  jacl1_preconditioner->SetType(mfem::HypreSmoother::Type::l1Jacobi);
1488  preconditioner_solver = std::move(jacl1_preconditioner);
1489  } else if (preconditioner == Preconditioner::HypreGaussSeidel) {
1490  auto gs_preconditioner = std::make_unique<mfem::HypreSmoother>();
1491  gs_preconditioner->SetType(mfem::HypreSmoother::Type::GS);
1492  preconditioner_solver = std::move(gs_preconditioner);
1493  } else if (preconditioner == Preconditioner::HypreILU) {
1494  auto ilu_preconditioner = std::make_unique<mfem::HypreILU>();
1495  ilu_preconditioner->SetLevelOfFill(1);
1496  ilu_preconditioner->SetPrintLevel(preconditioner_print_level);
1497  preconditioner_solver = std::move(ilu_preconditioner);
1498  } else if (preconditioner == Preconditioner::AMGX) {
1499 #ifdef MFEM_USE_AMGX
1500  preconditioner_solver = buildAMGX(linear_opts.amgx_options, comm);
1501 #else
1502  SLIC_ERROR_ROOT("AMGX requested in non-GPU build");
1503 #endif
1504  } else if (preconditioner == Preconditioner::Petsc) {
1505 #ifdef SMITH_USE_PETSC
1506  preconditioner_solver = mfem_ext::buildPetscPreconditioner(linear_opts.petsc_preconditioner, comm);
1507 #else
1508  SLIC_ERROR_ROOT("PETSc preconditioner requested in non-PETSc build");
1509 #endif
1510  } else if (preconditioner == Preconditioner::AMGFContact) {
1511  auto amgfcontact_preconditioner = std::make_unique<mfem::AMGFSolver>();
1512  auto amgfcontact_opts = linear_opts.amgfcontact_options;
1513  amgfcontact_preconditioner->GetAMG().SetPrintLevel(preconditioner_print_level);
1514  amgfcontact_preconditioner->GetAMG().SetSystemsOptions(amgfcontact_opts.dim_systems_options);
1515  amgfcontact_preconditioner->GetAMG().SetRelaxType(amgfcontact_opts.relax_type);
1516  preconditioner_solver = std::move(amgfcontact_preconditioner);
1517  } else if (preconditioner == Preconditioner::BlockDiagonal || preconditioner == Preconditioner::BlockTriangular ||
1518  preconditioner == Preconditioner::BlockSchur) {
1519  std::vector<std::unique_ptr<mfem::Solver>> inner_solvers;
1520  for (const auto& opt : linear_opts.sub_block_linear_solver_options) {
1521  auto [lin, prec] = buildLinearSolverAndPreconditioner(opt, comm);
1522  inner_solvers.push_back(std::make_unique<SolverWithPreconditioner>(std::move(lin), std::move(prec)));
1523  }
1524 
1525  if (preconditioner == Preconditioner::BlockDiagonal) {
1526  preconditioner_solver = std::make_unique<BlockDiagonalPreconditioner>(std::move(inner_solvers));
1527  } else if (preconditioner == Preconditioner::BlockTriangular) {
1528  preconditioner_solver =
1529  std::make_unique<BlockTriangularPreconditioner>(std::move(inner_solvers), linear_opts.block_triangular_type);
1530  } else if (preconditioner == Preconditioner::BlockSchur) {
1531  preconditioner_solver = std::make_unique<BlockSchurPreconditioner>(
1532  std::move(inner_solvers), linear_opts.block_schur_type, linear_opts.schur_approx_type);
1533  }
1534  } else {
1535  SLIC_ERROR_ROOT_IF(preconditioner != Preconditioner::None, "Unknown preconditioner type requested");
1536  }
1537 
1538  return preconditioner_solver;
1539 }
1540 
1541 void EquationSolver::defineInputFileSchema(axom::inlet::Container& container)
1542 {
1543  auto& linear_container = container.addStruct("linear", "Linear Equation Solver Parameters");
1544  linear_container.required().registerVerifier([](const axom::inlet::Container& container_to_verify) {
1545  // Make sure that the provided options match the desired linear solver type
1546  const bool is_iterative = (container_to_verify["type"].get<std::string>() == "iterative") &&
1547  container_to_verify.contains("iterative_options");
1548  const bool is_direct =
1549  (container_to_verify["type"].get<std::string>() == "direct") && container_to_verify.contains("direct_options");
1550  return is_iterative || is_direct;
1551  });
1552 
1553  // Enforce the solver type - must be iterative or direct
1554  linear_container.addString("type", "The type of solver parameters to use (iterative|direct)")
1555  .required()
1556  .validValues({"iterative", "direct"});
1557 
1558  auto& iterative_container = linear_container.addStruct("iterative_options", "Iterative solver parameters");
1559  iterative_container.addDouble("rel_tol", "Relative tolerance for the linear solve.").defaultValue(1.0e-6);
1560  iterative_container.addDouble("abs_tol", "Absolute tolerance for the linear solve.").defaultValue(1.0e-8);
1561  iterative_container.addInt("max_iter", "Maximum iterations for the linear solve.").defaultValue(5000);
1562  iterative_container.addInt("print_level", "Linear print level.").defaultValue(0);
1563  iterative_container.addString("solver_type", "Solver type (gmres|minres|cg).").defaultValue("gmres");
1564  iterative_container.addString("prec_type", "Preconditioner type (JacobiSmoother|L1JacobiSmoother|AMG|ILU|Petsc).")
1565  .defaultValue("JacobiSmoother");
1566  iterative_container.addString("petsc_prec_type", "Type of PETSc preconditioner to use.").defaultValue("jacobi");
1567 
1568  auto& direct_container = linear_container.addStruct("direct_options", "Direct solver parameters");
1569  direct_container.addInt("print_level", "Linear print level.").defaultValue(0);
1570 
1571  // Only needed for nonlinear problems
1572  auto& nonlinear_container = container.addStruct("nonlinear", "Newton Equation Solver Parameters").required(false);
1573  nonlinear_container.addDouble("rel_tol", "Relative tolerance for the Newton solve.").defaultValue(1.0e-2);
1574  nonlinear_container.addDouble("abs_tol", "Absolute tolerance for the Newton solve.").defaultValue(1.0e-4);
1575  nonlinear_container.addInt("max_iter", "Maximum iterations for the Newton solve.").defaultValue(500);
1576  nonlinear_container.addInt("print_level", "Nonlinear print level.").defaultValue(0);
1577  nonlinear_container.addString("solver_type", "Solver type (Newton|KINFullStep|KINLineSearch)").defaultValue("Newton");
1578 }
1579 
1580 } // namespace smith
1581 
1582 using smith::EquationSolver;
1585 
1587 {
1588  LinearSolverOptions options;
1589  std::string type = base["type"];
1590 
1591  if (type == "direct") {
1593  options.print_level = base["direct_options/print_level"];
1594  return options;
1595  }
1596 
1597  auto config = base["iterative_options"];
1598  options.relative_tol = config["rel_tol"];
1599  options.absolute_tol = config["abs_tol"];
1600  options.max_iterations = config["max_iter"];
1601  options.print_level = config["print_level"];
1602  std::string solver_type = config["solver_type"];
1603  if (solver_type == "gmres") {
1605  } else if (solver_type == "cg") {
1607  } else {
1608  std::string msg = std::format("Unknown Linear solver type given: '{0}'", solver_type);
1609  SLIC_ERROR_ROOT(msg);
1610  }
1611  const std::string prec_type = config["prec_type"];
1612  if (prec_type == "JacobiSmoother") {
1614  } else if (prec_type == "L1JacobiSmoother") {
1616  } else if (prec_type == "HypreAMG") {
1618  } else if (prec_type == "ILU") {
1620 #ifdef MFEM_USE_AMGX
1621  } else if (prec_type == "AMGX") {
1623 #endif
1624  } else if (prec_type == "GaussSeidel") {
1626 #ifdef SMITH_USE_PETSC
1627  } else if (prec_type == "Petsc") {
1628  const std::string petsc_prec = config["petsc_prec_type"];
1630  options.petsc_preconditioner = smith::mfem_ext::stringToPetscPCType(petsc_prec);
1631 #endif
1632  } else if (prec_type == "AMGFContact") {
1634  } else {
1635  std::string msg = std::format("Unknown preconditioner type given: '{0}'", prec_type);
1636  SLIC_ERROR_ROOT(msg);
1637  }
1638 
1639  return options;
1640 }
1641 
1643 {
1644  NonlinearSolverOptions options;
1645  options.relative_tol = base["rel_tol"];
1646  options.absolute_tol = base["abs_tol"];
1647  options.max_iterations = base["max_iter"];
1648  options.print_level = base["print_level"];
1649  const std::string solver_type = base["solver_type"];
1650  if (solver_type == "Newton") {
1652  } else if (solver_type == "KINFullStep") {
1654  } else if (solver_type == "KINLineSearch") {
1656  } else if (solver_type == "KINPicard") {
1658  } else {
1659  SLIC_ERROR_ROOT(std::format("Unknown nonlinear solver type given: '{0}'", solver_type));
1660  }
1661  return options;
1662 }
1663 
1665 {
1666  auto lin = base["linear"].get<LinearSolverOptions>();
1667  auto nonlin = base["nonlinear"].get<NonlinearSolverOptions>();
1668 
1669  auto [linear_solver, preconditioner] = smith::buildLinearSolverAndPreconditioner(lin, MPI_COMM_WORLD);
1670 
1671  smith::EquationSolver eq_solver(smith::buildNonlinearSolver(nonlin, lin, *preconditioner, MPI_COMM_WORLD),
1672  std::move(linear_solver), std::move(preconditioner));
1673 
1674  return eq_solver;
1675 }
This class manages the objects typically required to solve a nonlinear set of equations arising from ...
void resetConvergenceState() const
Reset nonlinear convergence state stored across iterations of a single solve.
mfem::Solver & preconditioner()
void setConvergenceTolerances(double abs_tol, double rel_tol, MPI_Comm comm) const
Update scalar convergence tolerances for the managed nonlinear convergence path.
EquationSolver(std::unique_ptr< mfem::NewtonSolver > nonlinear_solver, std::unique_ptr< mfem::Solver > linear_solver, std::unique_ptr< mfem::Solver > preconditioner=nullptr)
void solve(mfem::Vector &x) const
static void defineInputFileSchema(axom::inlet::Container &container)
mfem::Solver & linearSolver()
void setOperator(const mfem::Operator &op)
void SetOperator(const mfem::Operator &op)
Set the underlying matrix operator to use in the solution algorithm.
void Mult(const mfem::Vector &input, mfem::Vector &output) const
Factor and solve the linear system y = Op^{-1} x using DSuperLU.
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: smith.cpp:36
std::unique_ptr< mfem::NewtonSolver > buildNonlinearSolver(NonlinearSolverOptions nonlinear_opts, const LinearSolverOptions &linear_opts, mfem::Solver &prec, MPI_Comm comm)
Build a nonlinear solver using the nonlinear option struct.
bool requiresMonolithicOperator(const LinearSolverOptions &linear_opts)
Return true if the configured linear solve stack requires block operators to be merged.
std::unique_ptr< mfem::HypreParMatrix > buildMonolithicMatrix(const mfem::BlockOperator &block_operator)
Build a monolithic HypreParMatrix from a BlockOperator.
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.
std::string linearName(const LinearSolver &s)
Convert linear solver enums to their string names.
LinearSolver
Linear solution method indicator.
Preconditioner
The type of preconditioner to be used.
SMITH_HOST_DEVICE auto sqrt(dual< gradient_type > x)
implementation of square root for dual numbers
Definition: dual.hpp:308
constexpr SMITH_HOST_DEVICE auto norm(const isotropic_tensor< T, m, m > &I)
compute the Frobenius norm (sqrt(tr(dot(transpose(I), I)))) of an isotropic tensor
SMITH_HOST_DEVICE auto max(dual< gradient_type > a, double b)
Implementation of max for dual numbers.
Definition: dual.hpp:229
constexpr SMITH_HOST_DEVICE int size(const tensor< T, n... > &)
returns the total number of stored values in a tensor
Definition: tensor.hpp:1939
std::unique_ptr< mfem::Solver > buildPreconditioner(LinearSolverOptions linear_opts, [[maybe_unused]] MPI_Comm comm)
Build a preconditioner from the available options.
SMITH_HOST_DEVICE auto abs(dual< gradient_type > x)
Implementation of absolute value function for dual numbers.
Definition: dual.hpp:219
std::string preconditionerName(Preconditioner p)
Convert preconditioner enums to their string names.
Various helper functions and macros for profiling using Caliper.
#define SMITH_MARK_FUNCTION
Definition: profiling.hpp:90
smith::EquationSolver operator()(const axom::inlet::Container &base)
Returns created object from Inlet container.
smith::LinearSolverOptions operator()(const axom::inlet::Container &base)
Returns created object from Inlet container.
smith::NonlinearSolverOptions operator()(const axom::inlet::Container &base)
Returns created object from Inlet container.
Parameters for an iterative linear solution scheme.
int preconditioner_print_level
Debugging print level for the preconditioner.
int max_iterations
Maximum number of iterations.
double relative_tol
Relative tolerance.
int print_level
Debugging print level for the linear solver.
AMGFContactOptions amgfcontact_options
AMGFContact Options, used for Preconditioner::AMGFContact.
Preconditioner preconditioner
PreconditionerOptions selection.
SchurApproxType schur_approx_type
Schur approximation type.
PetscPCType petsc_preconditioner
PETSc preconditioner type.
std::vector< LinearSolverOptions > sub_block_linear_solver_options
Subblock linear solver options for block preconditioners.
BlockSchurType block_schur_type
Block Schur preconditioner factorization type.
double absolute_tol
Absolute tolerance.
AMGXOptions amgx_options
AMGX Options, used for Preconditioner::AMGX.
BlockTriangularType block_triangular_type
Block Triangular Preconditioner factorization type.
LinearSolver linear_solver
Linear solver selection.
Nonlinear solution scheme parameters.
int min_iterations
Minimum number of iterations.
int max_iterations
Maximum number of iterations.
double trust_region_scaling
Scaling for the initial trust region size.
int max_line_search_iterations
Maximum line search cutbacks.
double relative_tol
Relative tolerance.
int print_level
Debug print level.
int num_leftmost
Number of extra leftmost eigenvector to be stored between solves.
double absolute_tol
Absolute tolerance.
NonlinearSolver nonlin_solver
Nonlinear solver selection.
SubSpaceOptions subspace_option
Option for how when the subspace solver should be utilized within trust-region solver.
A sentinel struct for eliding no-op tensor operations.
Definition: tensor.hpp:122