Skip to content

Commit

Permalink
[solvers] Throw when IpoptSolver options are invalid (#22062)
Browse files Browse the repository at this point in the history
  • Loading branch information
jwnimmer-tri authored Oct 21, 2024
1 parent ce39a4b commit 4f23672
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 14 deletions.
1 change: 1 addition & 0 deletions solvers/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -1485,6 +1485,7 @@ drake_cc_googletest(
":second_order_cone_program_examples",
"//common:temp_directory",
"//common/test_utilities:eigen_matrix_compare",
"//common/test_utilities:expect_throws_message",
] + select({
"//conditions:default": [
"@ipopt",
Expand Down
53 changes: 39 additions & 14 deletions solvers/ipopt_solver.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,40 +22,65 @@ using Ipopt::SolverReturn;
namespace drake {
namespace solvers {
namespace {

void SetAppOptions(const SolverOptions& options, Ipopt::IpoptApplication* app) {
// Wrap our calls to IPOPT to check for errors (i.e., unknown options).
const auto set_double_option = [&app](const std::string& name, double value) {
const bool success = app->Options()->SetNumericValue(name, value);
if (!success) {
throw std::logic_error(fmt::format(
"Error setting IPOPT floating-point option {}={}", name, value));
}
};
const auto set_int_option = [&app](const std::string& name, int value) {
const bool success = app->Options()->SetIntegerValue(name, value);
if (!success) {
throw std::logic_error(
fmt::format("Error setting IPOPT integer option {}={}", name, value));
}
};
const auto set_string_option = [&app](const std::string& name,
const std::string& value) {
const bool success = app->Options()->SetStringValue(name, value);
if (!success) {
throw std::logic_error(fmt::format(
"Error setting IPOPT string option {}='{}'", name, value));
}
};

// Turn off the banner.
app->Options()->SetStringValue("sb", "yes");
set_string_option("sb", "yes");

// The default linear solver is MA27, but it is not freely redistributable so
// we cannot use it. MUMPS is the only compatible linear solver guaranteed to
// be available on both macOS and Ubuntu. In versions of IPOPT prior to 3.13,
// it would correctly determine that MUMPS was the only available solver, but
// its behavior changed to instead error having unsuccessfully tried to dlopen
// a nonexistent hsl library that would contain MA27.
app->Options()->SetStringValue("linear_solver", "mumps");
set_string_option("linear_solver", "mumps");

// The default tolerance.
const double tol = 1.05e-10; // Note: SNOPT is only 1e-6, but in #3712 we
// diagnosed that the CompareMatrices tolerance needed to be the sqrt of the
// constr_viol_tol
app->Options()->SetNumericValue("tol", tol);
app->Options()->SetNumericValue("constr_viol_tol", tol);
app->Options()->SetNumericValue("acceptable_tol", tol);
app->Options()->SetNumericValue("acceptable_constr_viol_tol", tol);

app->Options()->SetStringValue("hessian_approximation", "limited-memory");
set_double_option("tol", tol);
set_double_option("constr_viol_tol", tol);
set_double_option("acceptable_tol", tol);
set_double_option("acceptable_constr_viol_tol", tol);
set_string_option("hessian_approximation", "limited-memory");

// Note: 0 <= print_level <= 12, with higher numbers more verbose; 4 is very
// useful for debugging. Otherwise, we default to printing nothing. The user
// can always select an arbitrary print level, by setting the ipopt-specific
// option name directly.
const int verbose_level = 4;
const int print_level = options.get_print_to_console() ? verbose_level : 0;
app->Options()->SetIntegerValue("print_level", print_level);
set_int_option("print_level", print_level);

const std::string& output_file = options.get_print_file_name();
if (!output_file.empty()) {
app->Options()->SetStringValue("output_file", output_file);
app->Options()->SetIntegerValue("file_print_level", verbose_level);
set_int_option("file_print_level", verbose_level);
set_string_option("output_file", output_file);
}

// IPOPT does not support setting the number of threads so we ignore
Expand All @@ -64,13 +89,13 @@ void SetAppOptions(const SolverOptions& options, Ipopt::IpoptApplication* app) {
// The solver-specific options will trump our defaults.
const SolverId self = IpoptSolver::id();
for (const auto& [name, value] : options.GetOptionsDouble(self)) {
app->Options()->SetNumericValue(name, value);
set_double_option(name, value);
}
for (const auto& [name, value] : options.GetOptionsInt(self)) {
app->Options()->SetIntegerValue(name, value);
set_int_option(name, value);
}
for (const auto& [name, value] : options.GetOptionsStr(self)) {
app->Options()->SetStringValue(name, value);
set_string_option(name, value);
}
}

Expand Down
36 changes: 36 additions & 0 deletions solvers/test/ipopt_solver_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <gtest/gtest.h>

#include "drake/common/temp_directory.h"
#include "drake/common/test_utilities/expect_throws_message.h"
#include "drake/solvers/mathematical_program.h"
#include "drake/solvers/test/linear_program_examples.h"
#include "drake/solvers/test/mathematical_program_test_util.h"
Expand Down Expand Up @@ -293,6 +294,41 @@ GTEST_TEST(IpoptSolverTest, SolverOptionsVerbosity) {
}
}

GTEST_TEST(IpoptSolverTest, UnknownOptions) {
MathematicalProgram prog;
auto x = prog.NewContinuousVariables(1);
prog.AddLinearCost(x(0));
SolverOptions options_double;
options_double.SetOption(IpoptSolver::id(), "foobar_double", 2.5);
SolverOptions options_int;
options_int.SetOption(IpoptSolver::id(), "foobar_int", 3);
SolverOptions options_string;
options_string.SetOption(IpoptSolver::id(), "foobar_string", "four");
IpoptSolver solver;
if (solver.is_available()) {
DRAKE_EXPECT_THROWS_MESSAGE(solver.Solve(prog, {}, options_double),
".*float.*foobar.*");
DRAKE_EXPECT_THROWS_MESSAGE(solver.Solve(prog, {}, options_int),
".*int.*foobar.*");
DRAKE_EXPECT_THROWS_MESSAGE(solver.Solve(prog, {}, options_string),
".*string.*foobar.*");
}
}

GTEST_TEST(IpoptSolverTest, UnsupportedLinearSolver) {
MathematicalProgram prog;
auto x = prog.NewContinuousVariables(1);
prog.AddLinearCost(x(0));
SolverOptions options;
// This is a valid option name, but an invalid option value.
options.SetOption(IpoptSolver::id(), "linear_solver", "foobar");
IpoptSolver solver;
if (solver.is_available()) {
DRAKE_EXPECT_THROWS_MESSAGE(solver.Solve(prog, {}, options),
".*option.*linear_solver.*foobar.*");
}
}

// This is to verify we can set the print out file through CommonSolverOption.
GTEST_TEST(IpoptSolverTest, PrintToFile) {
MathematicalProgram prog;
Expand Down

0 comments on commit 4f23672

Please sign in to comment.