Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#716: Write file when nonlinear solver does not converge #717

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions include/pressio/solvers_nonlinear/impl/functions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#ifndef PRESSIO_SOLVERS_NONLINEAR_IMPL_FUNCTIONS_HPP_
#define PRESSIO_SOLVERS_NONLINEAR_IMPL_FUNCTIONS_HPP_

#include <fstream>

namespace pressio{ namespace nonlinearsolvers{ namespace impl{

#ifdef PRESSIO_ENABLE_CXX20
Expand Down Expand Up @@ -447,5 +449,23 @@ void compute_norm_internal_diagnostics(const RegistryType & reg,
};//end switch
}

// =====================================================

// Write a text file with the error that caused the
// nonlinear solver to exit without converging.
inline void writeNonlinearSolverTerminationFile(const std::string terminationString) {
int rank = 0;
#if defined PRESSIO_ENABLE_TPL_MPI
int flag = 0; MPI_Initialized( &flag );
if (flag == 1) MPI_Comm_rank(MPI_COMM_WORLD, &rank);
#endif
if (rank == 0) {
const std::string fileName = "nonlinear_solver_failed.txt";
std::ofstream file; file.open(fileName);
file << terminationString << std::endl;
file.close();
}
}

}}}
#endif // PRESSIO_SOLVERS_NONLINEAR_IMPL_FUNCTIONS_HPP_
19 changes: 16 additions & 3 deletions include/pressio/solvers_nonlinear/impl/root_finder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@ void root_solving_loop_impl(NewtonTag /*problemTag*/,
}
catch (::pressio::eh::ResidualEvaluationFailureUnrecoverable const &e){
PRESSIOLOG_CRITICAL(e.what());
writeNonlinearSolverTerminationFile(e.what());
throw ::pressio::eh::NonlinearSolveFailure();
}
catch (::pressio::eh::ResidualHasNans const &e){
PRESSIOLOG_CRITICAL(e.what());
writeNonlinearSolverTerminationFile(e.what());
throw ::pressio::eh::NonlinearSolveFailure();
}

Expand All @@ -79,6 +81,9 @@ void root_solving_loop_impl(NewtonTag /*problemTag*/,
/* stage 4*/
if (mustStop(iStep)){
PRESSIOLOG_DEBUG("nonlinsolver: stopping");
writeNonlinearSolverTerminationFile(
"Root Finder: Reached maximum number of iterations before convergence."
);
break;
Copy link
Collaborator Author

@cwschilly cwschilly Feb 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log file doesn't seem to be getting written if we hit the maximum number of iterations. I don't see anything obvious. Do you all?

@eparish1 If I understand your question correctly, it is being written out here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cwschilly Yes, for some reason this isn't getting hit in a test cases where I'm hitting the max iterations. I'll re-investigate our code.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cwschilly @fnrizzi

I may be misunderstanding the code so please correct me.

We want the solver to write an error to file if it hits the max iterations w/o converging. I think we currently don't have this behavior because 'mustStop' won't return true if the stopEnum is set to be, e.g., residual convergence, and we hit the max iterations. My understanding is that, even if we are at the max iterations, it will hit the switch statement on line 44 unless we have the maxIters enum. As is, I think it might actually write that the solver failed if in a case where it's converging (e.g., line 46 in root_finder.hpp returns true).

I think we need another block or a change to mustStop.

}

Expand All @@ -89,13 +94,15 @@ void root_solving_loop_impl(NewtonTag /*problemTag*/,
updater(reg, objective, currentObjValue);
}
catch (::pressio::eh::LineSearchStepTooSmall const &e) {
// nicely exist the solve
// nicely exit the solve
PRESSIOLOG_WARN(e.what());
writeNonlinearSolverTerminationFile(e.what());
break;
}
catch (::pressio::eh::LineSearchObjFunctionChangeTooSmall const &e) {
// nicely exist the solve
// nicely exit the solve
PRESSIOLOG_WARN(e.what());
writeNonlinearSolverTerminationFile(e.what());
break;
}
}
Expand Down Expand Up @@ -307,14 +314,20 @@ class RootFinder : public RegistryType
Update currentUpdateCriterion() const { return updateEnValue_; }
void setUpdateCriterion(Update value) { updateEnValue_ = value; }

// query/set stop criterion, tolerance
// query/set stop criterion, tolerance, line search params
Stop currentStopCriterion() const { return stopEnValue_; }
void setStopCriterion(Stop value) { stopEnValue_ = value; }
void setStopTolerance(NormValueType value) { stopTolerance_ = value; }
void setMaxIterations(int newMax) { maxIters_ = newMax; }
auto & getLineSearchParameters(){
return parameters_;
}
auto addLineSearchParameter(scalar_trait_t<StateType> new_param) {
if (!parameters_) {
parameters_ = std::vector<scalar_trait_t<StateType>>{};
}
parameters_->push_back(new_param);
}

template<class SystemType, class _Tag = Tag>
std::enable_if_t< std::is_same<_Tag, NewtonTag>::value >
Expand Down
2 changes: 1 addition & 1 deletion include/pressio/solvers_nonlinear/solvers_exceptions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ class LineSearchStepTooSmall : public std::exception{
};

class LineSearchObjFunctionChangeTooSmall : public std::exception{
std::string myerr_ = "Line serach objective function change too small";
std::string myerr_ = "Line search objective function change too small";
std::string append_ = {};

public:
Expand Down
4 changes: 4 additions & 0 deletions tests/functional_small/solvers_nonlinear/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ if (PRESSIO_ENABLE_TPL_EIGEN)
set(SRC1 ${CMAKE_CURRENT_SOURCE_DIR}/${name}.cc)
add_serial_utest(${TESTING_LEVEL}_solvers_nonlinear_${name} ${SRC1})

set(name newton_problem1_eigen_fail)
set(SRC1 ${CMAKE_CURRENT_SOURCE_DIR}/${name}.cc)
add_serial_utest(${TESTING_LEVEL}_solvers_nonlinear_${name} ${SRC1})

set(name newton_matrixfree_problem1_eigen)
set(SRC1 ${CMAKE_CURRENT_SOURCE_DIR}/${name}.cc)
add_serial_utest(${TESTING_LEVEL}_solvers_nonlinear_${name} ${SRC1})
Expand Down
166 changes: 166 additions & 0 deletions tests/functional_small/solvers_nonlinear/newton_problem1_eigen_fail.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@

#include <gtest/gtest.h>
#include "pressio/solvers_linear.hpp"
#include "pressio/solvers_nonlinear_newton.hpp"
#include "./problems/problem1.hpp"

#include <filesystem>

/**
* Tests all failure conditions RootFinder nonlinear solver.
* Any failure to converge should write out "nonlinear_solver_fail.txt"
* with the corresponding error message.
*/

enum class FailType {
ResidualEvaluationFailureUnrecoverable,
ResidualHasNans,
MaximumIterations,
LineSearchStepTooSmall,
LineSearchObjFunctionChangeTooSmall
};

const std::unordered_map<FailType, std::string> failMsgs = {
{
FailType::ResidualEvaluationFailureUnrecoverable,
std::string(pressio::eh::ResidualEvaluationFailureUnrecoverable().what())
},
{
FailType::ResidualHasNans,
std::string(pressio::eh::ResidualHasNans().what())
},
{
FailType::MaximumIterations,
"Root Finder: Reached maximum number of iterations before convergence."
},
{
FailType::LineSearchStepTooSmall,
std::string(pressio::eh::LineSearchStepTooSmall().what())
},
{
FailType::LineSearchObjFunctionChangeTooSmall,
std::string(pressio::eh::LineSearchObjFunctionChangeTooSmall().what())
}
};

template <FailType failType>
struct AdaptedProblem1 : pressio::solvers::test::Problem1
{
void residualAndJacobian(
const state_type& x,
residual_type& res,
std::optional<jacobian_type*> Jin) const
{
if constexpr (failType == FailType::ResidualHasNans) {
// This exception is never explicitly thrown in Pressio,
// so we have to throw it manually here.
throw pressio::eh::ResidualHasNans();
} else if constexpr (failType == FailType::ResidualEvaluationFailureUnrecoverable) {
// This exception is only thrown if it catches either of
// the following:
// VelocityFailureUnrecoverable
// DiscreteTimeResidualFailureUnrecoverable
// However, those are never explicitly thrown, and so
// we have to manually throw the exception here.
throw pressio::eh::ResidualEvaluationFailureUnrecoverable();
} else if constexpr (failType == FailType::LineSearchObjFunctionChangeTooSmall) {
// Makes the difference between new and old objective values
// negligible, which should trigger the exception
res(0) = 1e-14 * x(0);
res(1) = 1e-14 * x(1);
} else {
// Standard values from Problem1 test.
res(0) = x(0)*x(0)*x(0) + x(1) - 1.0;
res(1) = -x(0) + x(1)*x(1)*x(1) + 1.0;
}

if (Jin){
auto & jac = *Jin.value();
jac.coeffRef(0, 0) = 3.0*x(0)*x(0);
jac.coeffRef(0, 1) = 1.0;
jac.coeffRef(1, 0) = -1.0;
jac.coeffRef(1, 1) = 3.0*x(1)*x(1);
}
}
};

template <FailType failType>
void run_impl()
{
using namespace pressio;
using problem_t = AdaptedProblem1<failType>;
using state_t = typename problem_t::state_type;
using jacobian_t = typename problem_t::jacobian_type;

using lin_solver_t = linearsolvers::Solver<linearsolvers::iterative::LSCG, jacobian_t>;
lin_solver_t linearSolverObj;

problem_t sys;
state_t y(2);
auto nonLinSolver = create_newton_solver(sys, linearSolverObj);

if constexpr (failType == FailType::MaximumIterations) {
// The max iteration will be hit before this converges.
y(0) = 1000; y(1) = -1000;
} else if constexpr (failType == FailType::LineSearchStepTooSmall) {
// First we make sure we are using an Updater that can throw this
// exception. Then we make the backtrack condition tiny, so alpha
// keeps getting smaller and smaller until the exception is thrown.
const auto updateMethod = nonlinearsolvers::Update::BacktrackStrictlyDecreasingObjective;
nonLinSolver.setUpdateCriterion(updateMethod);
nonLinSolver.addLineSearchParameter(1e-10);
} else if constexpr (failType == FailType::LineSearchObjFunctionChangeTooSmall) {
// Armijo is the only updater that can throw this exception.
// The rest of the failure should be caused by residualAndJacobian function.
const auto updateMethod = nonlinearsolvers::Update::Armijo;
nonLinSolver.setUpdateCriterion(updateMethod);
} else {
// Standard values from Problem1 test
y(0) = 0.001; y(1) = 0.0001;
}

try {
nonLinSolver.solve(y);
} catch (...) {}

// Ensure that the file was written out
std::string fileName = "nonlinear_solver_failed.txt";
ASSERT_TRUE(std::filesystem::exists(fileName));

// Read the file to make sure it contains the correct error
int rank = 0;
#if defined PRESSIO_ENABLE_TPL_MPI
int flag = 0; MPI_Initialized( &flag );
if (flag == 1) MPI_Comm_rank(MPI_COMM_WORLD, &rank);
#endif
if (rank == 0) {
std::ifstream file(fileName);
std::string failMsg; std::getline(file, failMsg);
ASSERT_NE(failMsg.find(failMsgs.at(failType)), std::string::npos);
file.close();
std::filesystem::remove(fileName);
}
}

TEST(solvers_nonlinear, problem1_residual_failure) {
run_impl<FailType::ResidualEvaluationFailureUnrecoverable>();
}

TEST(solvers_nonlinear, problem1_residual_has_nans){
run_impl<FailType::ResidualHasNans>();
}

TEST(solvers_nonlinear, problem1_max_iterations){
run_impl<FailType::MaximumIterations>();
}

TEST(solvers_nonlinear, problem1_small_step) {
run_impl<FailType::LineSearchStepTooSmall>();
}

// CANNOT TEST: RootFinder does not support Armijo,
// which is the only updater that would throw this error.
//
// TEST(solvers_nonlinear, problem1_small_obj_ftn_change) {
// run_impl<FailType::LineSearchObjFunctionChangeTooSmall>();
// }
Loading