Skip to content

Commit

Permalink
Put in a recursive strategy to slowly ramp up new boundary conditions…
Browse files Browse the repository at this point in the history
… when solvers are failing.
  • Loading branch information
tupek2 committed Jan 29, 2025
1 parent 55f781b commit f3520cb
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 43 deletions.
43 changes: 35 additions & 8 deletions src/serac/numerics/equation_solver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@

namespace serac {

/// @brief A simple exception class for recording Newton and TrustRegion failures
class SolverException : public std::exception {
public:
/// constructor
SolverException(const std::string& message) : msg(message) {}

/// the message returned by the exception
const char* what() const noexcept override { return msg.c_str(); }

private:
/// message string
std::string msg;
};

/// Newton solver with a 2-way line-search. Reverts to regular Newton if max_line_search_iterations is set to 0.
class NewtonSolver : public mfem::NewtonSolver {
protected:
Expand Down Expand Up @@ -108,8 +122,11 @@ class NewtonSolver : public mfem::NewtonSolver {
}

if (norm != norm) {
mfem::out << "Initial residual for Newton iteration is undefined/nan." << std::endl;
mfem::out << "Newton: No convergence!\n";
if (print_options.first_and_last) {
mfem::out << "Initial residual for Newton iteration is undefined/nan." << std::endl;
mfem::out << "Newton: No convergence!\n";
}
throw SolverException("Initial residual for Newton iteration is undefined/nan.");
return;
}

Expand Down Expand Up @@ -199,6 +216,9 @@ class NewtonSolver : public mfem::NewtonSolver {
if (!converged && (print_options.summary || print_options.warnings)) {
mfem::out << "Newton: No convergence!\n";
}
if (!converged) {
throw SolverException("Newton algorithm failed to converge.");
}
}
};

Expand Down Expand Up @@ -665,14 +685,17 @@ class TrustRegion : public mfem::NewtonSolver {
mfem::out << ", ||r||/||r_0|| = " << std::setw(13) << (initial_norm != 0.0 ? norm / initial_norm : norm);
mfem::out << ", x_incr = " << std::setw(13) << trResults.d.Norml2();
} else {
mfem::out << ", norm goal = " << std::setw(13) << norm_goal << "\n";
mfem::out << ", norm goal = " << std::setw(13) << norm_goal;
}
mfem::out << '\n';
mfem::out << "\n";
}

if (norm != norm) {
mfem::out << "Initial residual for trust-region iteration is undefined/nan." << std::endl;
mfem::out << "Newton: No convergence!\n";
if (print_options.first_and_last) {
mfem::out << "Initial residual for trust-region iteration is undefined/nan." << std::endl;
mfem::out << "Trust-Region: No convergence!\n";
}
throw SolverException("Initial residual for trust-region iteration is undefined/nan.");
return;
}

Expand Down Expand Up @@ -791,8 +814,8 @@ class TrustRegion : public mfem::NewtonSolver {
happyAboutTrSize = true;
if (print_options.iterations) {
printTrustRegionInfo(realObjective, modelObjective, trResults.cg_iterations_count, tr_size, true);
trResults.cg_iterations_count =
0; // zero this output so it doesn't look like the linesearch is doing cg iterations
// zero this output so it doesn't look like the linesearch is doing cg iterations
trResults.cg_iterations_count = 0;
}
break;
}
Expand Down Expand Up @@ -863,6 +886,10 @@ class TrustRegion : public mfem::NewtonSolver {
mfem::out << "num subspace solves = " << num_subspace_solves << "\n";
mfem::out << "num jacobian_assembles = " << num_jacobian_assembles << "\n";
}

if (!converged) {
throw SolverException("Trust-region algorithm failed to converge.");
}
}
};

Expand Down
86 changes: 67 additions & 19 deletions src/serac/physics/solid_mechanics.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,10 @@ class SolidMechanics<order, dim, Parameters<parameter_space...>, std::integer_se
SolidMechanics(const NonlinearSolverOptions nonlinear_opts, const LinearSolverOptions lin_opts,
const serac::TimesteppingOptions timestepping_opts, const std::string& physics_name,
std::string mesh_tag, std::vector<std::string> parameter_names = {}, int cycle = 0, double time = 0.0,
bool checkpoint_to_disk = false, bool use_warm_start = true)
bool checkpoint_to_disk = false, bool use_warm_start = true, int max_timestep_cutbacks = 5)
: SolidMechanics(
std::make_unique<EquationSolver>(nonlinear_opts, lin_opts, StateManager::mesh(mesh_tag).GetComm()),
timestepping_opts, physics_name, mesh_tag, parameter_names, cycle, time, checkpoint_to_disk, use_warm_start)
timestepping_opts, physics_name, mesh_tag, parameter_names, cycle, time, checkpoint_to_disk, use_warm_start, max_timestep_cutbacks)
{
}

Expand All @@ -164,7 +164,7 @@ class SolidMechanics<order, dim, Parameters<parameter_space...>, std::integer_se
*/
SolidMechanics(std::unique_ptr<serac::EquationSolver> solver, const serac::TimesteppingOptions timestepping_opts,
const std::string& physics_name, std::string mesh_tag, std::vector<std::string> parameter_names = {},
int cycle = 0, double time = 0.0, bool checkpoint_to_disk = false, bool use_warm_start = true)
int cycle = 0, double time = 0.0, bool checkpoint_to_disk = false, bool use_warm_start = true, int max_timestep_cutbacks = 5)
: BasePhysics(physics_name, mesh_tag, cycle, time, checkpoint_to_disk),
displacement_(
StateManager::newState(H1<order, dim>{}, detail::addPrefix(physics_name, "displacement"), mesh_tag_)),
Expand All @@ -184,7 +184,8 @@ class SolidMechanics<order, dim, Parameters<parameter_space...>, std::integer_se
ode2_(displacement_.space().TrueVSize(),
{.time = time_, .c0 = c0_, .c1 = c1_, .u = u_, .du_dt = v_, .d2u_dt2 = acceleration_}, *nonlin_solver_,
bcs_),
use_warm_start_(use_warm_start)
use_warm_start_(use_warm_start),
max_timestep_cutbacks_(max_timestep_cutbacks)
{
SERAC_MARK_FUNCTION;
SLIC_ERROR_ROOT_IF(mesh_.Dimension() != dim,
Expand Down Expand Up @@ -1176,7 +1177,7 @@ class SolidMechanics<order, dim, Parameters<parameter_space...>, std::integer_se
}

if (is_quasistatic_) {
quasiStaticSolve(dt);
quasiStaticSolve(dt, 1.0, 0);
} else {
// The current ode interface tracks 2 times, one internally which we have a handle to via time_,
// and one here via the step interface.
Expand Down Expand Up @@ -1500,6 +1501,9 @@ class SolidMechanics<order, dim, Parameters<parameter_space...>, std::integer_se
/// @brief A flag denoting whether to compute the warm start for improved robustness
bool use_warm_start_;

/// @brief The maximum number of cut-back in the supplied time-step increment before the solver will give up
int max_timestep_cutbacks_;

/// @brief Coefficient containing the essential boundary values
std::shared_ptr<mfem::VectorCoefficient> disp_bdr_coef_;

Expand All @@ -1518,16 +1522,44 @@ class SolidMechanics<order, dim, Parameters<parameter_space...>, std::integer_se
}...};

/// @brief Solve the Quasi-static Newton system
virtual void quasiStaticSolve(double dt)
virtual void quasiStaticSolve(double dt, double step_fraction_of_dt_to_try, int level)
{
// warm start must be called prior to the time update so that the previous Jacobians can be used consistently
// throughout.
warmStartDisplacement(dt);
time_ += dt;

// this method is essentially equivalent to the 1-liner
// u += dot(inv(J), dot(J_elim[:, dofs], (U(t + dt) - u)[dofs]));
nonlin_solver_->solve(displacement_);
if (level > max_timestep_cutbacks_) {
if (mpi_rank_ == 0)
std::cout << "Too many boundary condition cutbacks, accepting solution even though there may be issues. Try "
"increasing the number of load steps "
<< std::endl;
return;
}

static constexpr bool solver_info_print=false;

bool solver_success = true;
try {
// warm start must be called prior to the time update so that the previous Jacobians can be used consistently
// throughout.
if (level == 0) time_ += dt;
warmStartDisplacement(dt, step_fraction_of_dt_to_try);
nonlin_solver_->solve(displacement_);
} catch (const std::exception& e) {
if (mpi_rank_ == 0 && solver_info_print) { mfem::out << "Caught nonlinear solver exception: " << e.what() << std::endl; }
displacement_ -= du_;
solver_success = false;
quasiStaticSolve(dt, 0.5 * step_fraction_of_dt_to_try, level + 1);
quasiStaticSolve(dt, 1.0, level + 1);
}

if (solver_success) {
if (step_fraction_of_dt_to_try == 1.0) {
if (mpi_rank_ == 0 && solver_info_print) {
mfem::out << "SolidMechanics solve succeeded for time " << time_ << " dt = " << dt << std::endl;
}
} else {
if (mpi_rank_ == 0 && solver_info_print) {
mfem::out << "SolidMechanics substep solve succeeded for time " << time_ << " dt = " << dt << std::endl;
}
}
}
}

/**
Expand Down Expand Up @@ -1625,14 +1657,14 @@ class SolidMechanics<order, dim, Parameters<parameter_space...>, std::integer_se
nonlinear solvers will ensure positive definiteness at equilibrium. *Once any parameter is changed, it is no longer
certain to be positive definite, which will cause issues for many types linear solvers.
*/
void warmStartDisplacement(double dt)
void warmStartDisplacement(double dt, double displacement_scale_factor)
{
SERAC_MARK_FUNCTION;

du_ = 0.0;
for (auto& bc : bcs_.essentials()) {
// apply the future boundary conditions, but use the most recent Jacobians stiffness.
bc.setDofs(du_, time_ + dt);
bc.setDofs(du_, time_);
}

auto& constrained_dofs = bcs_.allEssentialTrueDofs();
Expand All @@ -1641,13 +1673,17 @@ class SolidMechanics<order, dim, Parameters<parameter_space...>, std::integer_se
du_[j] -= displacement_(j);
}

if (displacement_scale_factor != 1.0) {
du_ *= displacement_scale_factor;
}

if (use_warm_start_) {
// Update the linearized Jacobian matrix
auto r = (*residual_)(time_ + dt, shape_displacement_, displacement_, acceleration_,
auto r = (*residual_)(time_, shape_displacement_, displacement_, acceleration_,
*parameters_[parameter_indices].state...);

// use the most recently evaluated Jacobian
auto [_, drdu] = (*residual_)(time_, shape_displacement_, differentiate_wrt(displacement_), acceleration_,
auto [_, drdu] = (*residual_)(time_ - dt, shape_displacement_, differentiate_wrt(displacement_), acceleration_,
*parameters_[parameter_indices].previous_state...);
J_.reset();
J_ = assemble(drdu);
Expand All @@ -1665,11 +1701,23 @@ class SolidMechanics<order, dim, Parameters<parameter_space...>, std::integer_se
auto& lin_solver = nonlin_solver_->linearSolver();

lin_solver.SetOperator(*J_);

lin_solver.Mult(r, du_);
}

displacement_ += du_;

// due to solver inaccuracies, the end of step boundary conditions may not be exact at this point
if (displacement_scale_factor == 1.0) {
du_ = 0.0;
for (auto& bc : bcs_.essentials()) {
bc.setDofs(du_, time_);
}
for (int i = 0; i < constrained_dofs.Size(); i++) {
int j = constrained_dofs[i];
du_[j] -= displacement_(j);
}
displacement_ += du_;
}
}
};

Expand Down
3 changes: 2 additions & 1 deletion src/serac/physics/solid_mechanics_contact.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,8 @@ class SolidMechanicsContact<order, dim, Parameters<parameter_space...>,

protected:
/// @brief Solve the Quasi-static Newton system
void quasiStaticSolve(double dt) override
void quasiStaticSolve(double dt, [[maybe_unused]] double step_fraction_of_dt_to_try,
[[maybe_unused]] int level) override
{
// warm start must be called prior to the time update so that the previous Jacobians can be used consistently
// throughout. warm start for contact needs to include the previous stiffness terms associated with contact
Expand Down
6 changes: 3 additions & 3 deletions src/serac/physics/tests/contact_patch_tied.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ TEST_P(ContactPatchTied, patch)

NonlinearSolverOptions nonlinear_options{.nonlin_solver = NonlinearSolver::Newton,
.relative_tol = 1.0e-10,
.absolute_tol = 1.0e-10,
.max_iterations = 20,
.print_level = 1};
.absolute_tol = 5.0e-10,
.max_iterations = 25,
.print_level = 0};

ContactOptions contact_options{.method = ContactMethod::SingleMortar,
.enforcement = GetParam().first,
Expand Down
2 changes: 1 addition & 1 deletion src/serac/physics/tests/lce_Brighenti_tensile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ TEST(LiquidCrystalElastomer, Brighenti)

// Construct a solid mechanics solver
LinearSolverOptions linear_options = {
.linear_solver = LinearSolver::GMRES,
.linear_solver = LinearSolver::CG,
.preconditioner = Preconditioner::HypreAMG,
.relative_tol = 1.0e-6,
.absolute_tol = 1.0e-14,
Expand Down
20 changes: 9 additions & 11 deletions src/serac/physics/tests/solid_statics_patch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -308,21 +308,19 @@ double pressure_error()
Domain boundary = EntireBoundary(pmesh);

// Construct a solid mechanics solver
#ifdef SERAC_USE_SUNDIALS
serac::NonlinearSolverOptions nonlin_solver_options{.nonlin_solver = NonlinearSolver::KINBacktrackingLineSearch,
.relative_tol = 0.0,
.absolute_tol = 1.0e-14,
.max_iterations = 30};
#else
serac::NonlinearSolverOptions nonlin_solver_options{
.nonlin_solver = NonlinearSolver::Newton, .relative_tol = 0.0, .absolute_tol = 1.0e-14, .max_iterations = 30};
#endif

auto equation_solver = std::make_unique<EquationSolver>(
nonlin_solver_options, serac::solid_mechanics::default_linear_options, pmesh.GetComm());
LinearSolverOptions cg_solver_options{.linear_solver = LinearSolver::CG,
.preconditioner = Preconditioner::HypreJacobi,
.relative_tol = 1.0e-6,
.absolute_tol = 1.0e-16,
.max_iterations = 500,
.print_level = 0};

SolidMechanics<p, dim> solid(std::move(equation_solver), solid_mechanics::default_quasistatic_options, "solid",
mesh_tag);
auto equation_solver = std::make_unique<EquationSolver>(nonlin_solver_options, cg_solver_options, pmesh.GetComm());

SolidMechanics<p, dim> solid(std::move(equation_solver), solid_mechanics::default_quasistatic_options, "solid", mesh_tag, std::vector<std::string>{}, 0, 0.0, false, true, 2);

solid_mechanics::NeoHookean mat{.density = 1.0, .K = 1.0, .G = 1.0};

Expand Down

0 comments on commit f3520cb

Please sign in to comment.