Fix math_opt build

This commit is contained in:
Corentin Le Molgat
2022-01-14 14:00:30 +01:00
parent 1cefc4cbee
commit 5d47faf9e3
35 changed files with 563 additions and 249 deletions

View File

@@ -35,27 +35,41 @@ namespace absl {
} else /* NOLINT */ \
return ::util::StatusBuilder(status_macro_internal_adaptor)
// Internal helper for concatenating macro values.
#define STATUS_MACROS_CONCAT_NAME_INNER(x, y) x##y
#define STATUS_MACROS_CONCAT_NAME(x, y) STATUS_MACROS_CONCAT_NAME_INNER(x, y)
#define ASSIGN_OR_RETURN_IMPL(statusor, lhs, rexpr) \
auto statusor = (rexpr); \
RETURN_IF_ERROR(statusor.status()); \
lhs = *std::move(statusor)
// Executes an expression that returns an absl::StatusOr, extracting its value
// into the variable defined by lhs (or returning on error).
//
// Example: Assigning to an existing value
// ValueType value;
// ASSIGN_OR_RETURN(value, MaybeGetValue(arg));
// ASSIGN_OR_RETURN((auto [key, val]), MaybeGetValue(arg));
//
// WARNING: ASSIGN_OR_RETURN expands into multiple statements; it cannot be used
// in a single statement (e.g. as the body of an if statement without {})!
#define ASSIGN_OR_RETURN(lhs, rexpr) \
ASSIGN_OR_RETURN_IMPL( \
STATUS_MACROS_CONCAT_NAME(_status_or_value, __COUNTER__), lhs, rexpr);
#define ASSIGN_OR_RETURN(lhs, rexpr) \
STATUS_MACROS_IMPL_ASSIGN_OR_RETURN_( \
STATUS_MACROS_IMPL_CONCAT_(_status_or_value, __COUNTER__), lhs, rexpr);
#define STATUS_MACROS_IMPL_ASSIGN_OR_RETURN_(statusor, lhs, rexpr) \
auto statusor = (rexpr); \
RETURN_IF_ERROR(statusor.status()); \
STATUS_MACROS_IMPL_UNPARENTHESIS(lhs) = std::move(statusor).value()
// Internal helpers for macro expansion.
#define STATUS_MACROS_IMPL_UNPARENTHESIS_INNER(...) \
STATUS_MACROS_IMPL_UNPARENTHESIS_INNER_(__VA_ARGS__)
#define STATUS_MACROS_IMPL_UNPARENTHESIS_INNER_(...) \
STATUS_MACROS_IMPL_VAN##__VA_ARGS__
#define ISH(...) ISH __VA_ARGS__
#define STATUS_MACROS_IMPL_VANISH
// If the input is parenthesized, removes the parentheses. Otherwise expands to
// the input unchanged.
#define STATUS_MACROS_IMPL_UNPARENTHESIS(...) \
STATUS_MACROS_IMPL_UNPARENTHESIS_INNER(ISH __VA_ARGS__)
// Internal helper for concatenating macro values.
#define STATUS_MACROS_IMPL_CONCAT_INNER_(x, y) x##y
#define STATUS_MACROS_IMPL_CONCAT_(x, y) STATUS_MACROS_IMPL_CONCAT_INNER_(x, y)
} // namespace absl

View File

@@ -119,7 +119,7 @@ cc_library(
"//ortools/math_opt/validators:model_parameters_validator",
"//ortools/math_opt/validators:model_validator",
"//ortools/math_opt/validators:result_validator",
"//ortools/math_opt/validators:solver_parameters_validator",
"//ortools/math_opt/validators:solve_parameters_validator",
"//ortools/port:proto_utils",
"@com_google_absl//absl/memory",
"@com_google_absl//absl/status",

View File

@@ -36,8 +36,6 @@ namespace math_opt {
// * Ids must be unique and increasing (in insertion order).
// * Ids are non-negative.
// * Ids are not equal to std::numeric_limits<int64_t>::max()
// TODO(b/213918209): make sure this is enforced in validators or remove this
// restriction.
// * Ids removed are never reused.
// * Names must be either empty or unique.
class IdNameBiMap {
@@ -104,10 +102,6 @@ struct ModelSummary {
void IdNameBiMap::Insert(const int64_t id, std::string name) {
CHECK_GE(id, next_free_id_);
// TODO(b/213918209): this is not mandatory for a valid model at this point so
// this is a bit incorrect. The correct thing to do would be to have an
// optional<int64_t> for the next_free_id_ and forbid any new id when we reach
// the max but this may be overkill.
CHECK_LT(id, std::numeric_limits<int64_t>::max());
next_free_id_ = id + 1;

View File

@@ -42,7 +42,7 @@
#include "ortools/math_opt/validators/model_parameters_validator.h"
#include "ortools/math_opt/validators/model_validator.h"
#include "ortools/math_opt/validators/result_validator.h"
#include "ortools/math_opt/validators/solver_parameters_validator.h"
#include "ortools/math_opt/validators/solve_parameters_validator.h"
#include "ortools/port/proto_utils.h"
#include "ortools/base/status_macros.h"
@@ -196,7 +196,7 @@ absl::StatusOr<SolveResultProto> Solver::Solve(const SolveArgs& arguments) {
// can be filtered, this should be included in the solver_interface
// implementations.
RETURN_IF_ERROR(ValidateSolverParameters(arguments.parameters))
RETURN_IF_ERROR(ValidateSolveParameters(arguments.parameters))
<< "invalid parameters";
RETURN_IF_ERROR(
ValidateModelSolveParameters(arguments.model_parameters, model_summary_))

View File

@@ -242,7 +242,6 @@ cc_library(
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/synchronization",
#"@com_google_absl//absl/types:source_location",
],
)
@@ -269,8 +268,6 @@ cc_library(
"//ortools/gscip:gscip_cc_proto",
"//ortools/math_opt:parameters_cc_proto",
"//ortools/math_opt/solvers:gurobi_cc_proto",
#"//ortools/math_opt/solvers:osqp_settings_cc_proto",
#"//ortools/pdlp:solvers_cc_proto",
"//ortools/port:proto_utils",
"//ortools/sat:sat_parameters_cc_proto",
"@com_google_absl//absl/status",

View File

@@ -224,15 +224,37 @@ std::optional<E> EnumFromString(const absl::string_view str);
//
// It calls EnumToOptString(), printing the returned value if not nullopt. When
// nullopt it prints the enum numeric value instead.
template <typename E, typename>
std::ostream& operator<<(std::ostream& out, const E value);
template <typename E,
// We must use enable_if here to prevent this overload to be selected
// for other types than ones that implement Enum<E>.
typename = std::enable_if_t<Enum<E>::kIsImplemented>>
std::ostream& operator<<(std::ostream& out, const E value) {
const std::optional<absl::string_view> opt_str = EnumToOptString(value);
if (opt_str.has_value()) {
out << *opt_str;
} else {
out << "<invalid enum (" << static_cast<std::underlying_type_t<E>>(value)
<< ")>";
}
return out;
}
// Overload of operator<< for std::optional<E> when Enum<E> is implemented.
//
// When the value is nullopt, it prints "<unspecified>", else it prints the enum
// value.
template <typename E, typename>
std::ostream& operator<<(std::ostream& out, const std::optional<E> value);
template <typename E,
// We must use enable_if here to prevent this overload to be selected
// for other types than ones that implement Enum<E>.
typename = std::enable_if_t<Enum<E>::kIsImplemented>>
std::ostream& operator<<(std::ostream& out, const std::optional<E> opt_value) {
if (opt_value.has_value()) {
out << *opt_value;
} else {
out << "<unspecified>";
}
return out;
}
////////////////////////////////////////////////////////////////////////////////
// Template functions implementations after this point.
@@ -280,34 +302,6 @@ std::optional<E> EnumFromString(const absl::string_view str) {
return std::nullopt;
}
template <typename E,
// We must use enable_if here to prevent this overload to be selected
// for other types than ones that implement Enum<E>.
typename = std::enable_if_t<Enum<E>::kIsImplemented>>
std::ostream& operator<<(std::ostream& out, const E value) {
const std::optional<absl::string_view> opt_str = EnumToOptString(value);
if (opt_str.has_value()) {
out << *opt_str;
} else {
out << "<invalid enum (" << static_cast<std::underlying_type_t<E>>(value)
<< ")>";
}
return out;
}
template <typename E,
// We must use enable_if here to prevent this overload to be selected
// for other types than ones that implement Enum<E>.
typename = std::enable_if_t<Enum<E>::kIsImplemented>>
std::ostream& operator<<(std::ostream& out, const std::optional<E> opt_value) {
if (opt_value.has_value()) {
out << *opt_value;
} else {
out << "<unspecified>";
}
return out;
}
// Macros that defines the templates specializations for Enum and EnumProto.
//
// The CppEnum parameter is the name of the C++ enum class which values are the

View File

@@ -157,6 +157,18 @@ SolveParametersProto SolveParameters::Proto() const {
if (iteration_limit.has_value()) {
result.set_iteration_limit(*iteration_limit);
}
if (cutoff_limit.has_value()) {
result.set_cutoff_limit(*cutoff_limit);
}
if (objective_limit.has_value()) {
result.set_objective_limit(*objective_limit);
}
if (best_bound_limit.has_value()) {
result.set_best_bound_limit(*best_bound_limit);
}
if (solution_limit.has_value()) {
result.set_solution_limit(*solution_limit);
}
if (threads.has_value()) {
result.set_threads(*threads);
}
@@ -195,6 +207,18 @@ absl::StatusOr<SolveParameters> SolveParameters::FromProto(
if (proto.has_iteration_limit()) {
result.iteration_limit = proto.iteration_limit();
}
if (proto.has_cutoff_limit()) {
result.cutoff_limit = proto.cutoff_limit();
}
if (proto.has_objective_limit()) {
result.objective_limit = proto.objective_limit();
}
if (proto.has_best_bound_limit()) {
result.best_bound_limit = proto.best_bound_limit();
}
if (proto.has_solution_limit()) {
result.solution_limit = proto.solution_limit();
}
if (proto.has_threads()) {
result.threads = proto.threads();
}

View File

@@ -211,6 +211,41 @@ struct SolveParameters {
std::optional<double> relative_gap_limit;
std::optional<double> absolute_gap_limit;
// The solver stops early if it can prove there are no primal solutions at
// least as good as cutoff.
//
// On an early stop, the solver returns termination reason kLimitReached and
// with limit kCutoff and is not required to give any extra solution
// information. Has no effect on the return value if there is no early stop.
//
// It is recommended that you use a tolerance if you want solutions with
// objective exactly equal to cutoff to be returned.
//
// See the user guide for more details and a comparison with best_bound_limit.
std::optional<double> cutoff_limit;
// The solver stops early as soon as it finds a solution at least this good,
// with termination reason kLimitReached and limit kObjective.
std::optional<double> objective_limit;
// The solver stops early as soon as it proves the best bound is at least this
// good, with termination reason kLimitReached and limit kObjective.
//
// See the user guide for a comparison with cutoff_limit.
std::optional<double> best_bound_limit;
// The solver stops early after finding this many feasible solutions, with
// termination reason kLimitReached and limit kSolution. Must be greater than
// zero if set. It is often used get the solver to stop on the first feasible
// solution found. Note that there is no guarantee on the objective value for
// any of the returned solutions.
//
// Solvers will typically not return more solutions than the solution limit,
// but this is not enforced by MathOpt, see also b/214041169.
//
// Currently supported for Gurobi and SCIP, and for CP-SAT only with value 1.
std::optional<int32_t> solution_limit;
// If unset, use the solver default. If set, it must be >= 1.
std::optional<int32_t> threads;

View File

@@ -228,23 +228,6 @@ MessageCallback PrinterMessageCallback(std::ostream& output_stream,
[=](const std::vector<std::string>& messages) { impl->Call(messages); };
}
MessageCallback InfoLoggerMessageCallback(const absl::string_view prefix,
const absl::SourceLocation loc) {
return [=](const std::vector<std::string>& messages) {
for (const std::string& message : messages) {
LOG(INFO).AtLocation(loc) << prefix << message;
}
};
}
MessageCallback VLoggerMessageCallback(int level, absl::string_view prefix,
absl::SourceLocation loc) {
return [=](const std::vector<std::string>& messages) {
for (const std::string& message : messages) {
VLOG(level).AtLocation(loc) << prefix << message;
}
};
}
} // namespace math_opt
} // namespace operations_research

View File

@@ -68,27 +68,6 @@ using MessageCallback = std::function<void(const std::vector<std::string>&)>;
MessageCallback PrinterMessageCallback(std::ostream& output_stream = std::cout,
absl::string_view prefix = "");
// Returns a message callback function that prints each line to LOG(INFO),
// prefixing each line with the given prefix.
//
// Usage:
//
// SolveArguments args;
// args.message_callback = InfoLoggerMessageCallback("[solver] ");
MessageCallback InfoLoggerMessageCallback(
absl::string_view prefix = "",
absl::SourceLocation loc = absl::SourceLocation::current());
// Returns a message callback function that prints each line to VLOG(level),
// prefixing each line with the given prefix.
//
// Usage:
//
// SolveArguments args;
// args.message_callback = VLoggerMessageCallback(1, "[solver] ");
MessageCallback VLoggerMessageCallback(
int level, absl::string_view prefix = "",
absl::SourceLocation loc = absl::SourceLocation::current());
// Arguments passed to Solve() and IncrementalSolver::New() to control the
// instantiation of the solver.

View File

@@ -124,6 +124,8 @@ std::optional<absl::string_view> Enum<Limit>::ToOptString(Limit value) {
return "solution";
case Limit::kMemory:
return "memory";
case Limit::kCutoff:
return "cutoff";
case Limit::kObjective:
return "objective";
case Limit::kNorm:
@@ -140,10 +142,10 @@ std::optional<absl::string_view> Enum<Limit>::ToOptString(Limit value) {
absl::Span<const Limit> Enum<Limit>::AllValues() {
static constexpr Limit kLimitValues[] = {
Limit::kUndetermined, Limit::kIteration, Limit::kTime,
Limit::kNode, Limit::kSolution, Limit::kMemory,
Limit::kObjective, Limit::kNorm, Limit::kInterrupted,
Limit::kSlowProgress, Limit::kOther};
Limit::kUndetermined, Limit::kIteration, Limit::kTime,
Limit::kNode, Limit::kSolution, Limit::kMemory,
Limit::kCutoff, Limit::kObjective, Limit::kNorm,
Limit::kInterrupted, Limit::kSlowProgress, Limit::kOther};
return absl::MakeConstSpan(kLimitValues);
}

View File

@@ -213,6 +213,13 @@ enum class Limit {
// The algorithm stopped because it ran out of memory.
kMemory = LIMIT_MEMORY,
// The solver was run with a cutoff (e.g. SolveParameters.cutoff_limit was
// set) on the objective, indicating that the user did not want any solution
// worse than the cutoff, and the solver concluded there were no solutions at
// least as good as the cutoff. Typically no further solution information is
// provided.
kCutoff = LIMIT_CUTOFF,
// The algorithm stopped because it found a solution better than a minimum
// limit set by the user.
kObjective = LIMIT_OBJECTIVE,

View File

@@ -13,20 +13,20 @@
#include "ortools/math_opt/cpp/variable_and_expressions.h"
#include <string>
#include <cmath>
#include <limits>
#include <utility>
#include <vector>
#include "ortools/base/logging.h"
#include "absl/base/attributes.h"
#include "ortools/base/map_util.h"
#include "ortools/base/int_type.h"
#include "ortools/math_opt/core/model_storage.h"
#include "ortools/math_opt/cpp/key_types.h"
namespace operations_research {
namespace math_opt {
constexpr double kInf = std::numeric_limits<double>::infinity();
#ifdef MATH_OPT_USE_EXPRESSION_COUNTERS
LinearExpression::LinearExpression() { ++num_calls_default_constructor_; }
@@ -91,36 +91,90 @@ double LinearExpression::EvaluateWithDefaultZero(
return result;
}
namespace {
// Streaming formatter for a coefficient of a linear/quadratic term, along with
// any leading "+"/"-"'s to connect it with preceding terms in a sum, and
// potentially a "*" postfix. The `is_first` parameter specifies if the term is
// the first appearing in the sum, in which case the handling of the +/-
// connectors is different.
struct LeadingCoefficientFormatter {
LeadingCoefficientFormatter(const double coeff, const bool is_first)
: coeff(coeff), is_first(is_first) {}
const double coeff;
const bool is_first;
};
std::ostream& operator<<(std::ostream& out,
const LeadingCoefficientFormatter formatter) {
const double coeff = formatter.coeff;
if (formatter.is_first) {
if (coeff == 1.0) {
// Do nothing.
} else if (coeff == -1.0) {
out << "-";
} else {
out << coeff << "*";
}
} else {
if (coeff == 1.0) {
out << " + ";
} else if (coeff == -1.0) {
out << " - ";
} else if (std::isnan(coeff)) {
out << " + nan*";
} else if (coeff >= 0) {
out << " + " << coeff << "*";
} else {
out << " - " << -coeff << "*";
}
}
return out;
}
// Streaming formatter for a constant in a linear/quadratic expression.
struct ConstantFormatter {
ConstantFormatter(const double constant, const bool is_first)
: constant(constant), is_first(is_first) {}
const double constant;
const bool is_first;
};
std::ostream& operator<<(std::ostream& out, const ConstantFormatter formatter) {
const double constant = formatter.constant;
if (formatter.is_first) {
out << constant;
} else if (constant == 0) {
// Do nothing.
} else if (std::isnan(constant)) {
out << " + nan";
} else if (constant > 0) {
out << " + " << constant;
} else {
out << " - " << -constant;
}
return out;
}
} // namespace
std::ostream& operator<<(std::ostream& ostr,
const LinearExpression& expression) {
// TODO(b/169415597): improve linear expression format:
// - use bijective formatting in base10 of the double factors.
// - handle negative coefficients, replacing `... + -3*x ` by `... - 3*x`.
// - make sure to quote the variable name so that we support:
// * variable names contains +, -, ...
// * variable names resembling anonymous variable names.
const std::vector<Variable> sorted_variables = expression.terms_.SortedKeys();
bool first = true;
for (const auto v : sorted_variables) {
if (first) {
const double coeff = expression.terms_.at(v);
if (coeff != 0) {
ostr << LeadingCoefficientFormatter(coeff, first) << v;
first = false;
} else {
ostr << " + ";
}
ostr << expression.terms_.at(v) << "*";
const std::string& name =
expression.terms_.storage()->variable_name(v.typed_id());
if (name.empty()) {
ostr << "[" << v << "]";
} else {
ostr << name;
}
}
if (!first) {
ostr << " + ";
}
ostr << expression.offset();
ostr << ConstantFormatter(expression.offset(), first);
return ostr;
}
@@ -129,9 +183,17 @@ std::ostream& operator<<(std::ostream& ostr,
const BoundedLinearExpression& bounded_expression) {
// TODO(b/170991498): use bijective conversion from double to base-10 string
// to make sure we can reproduce bugs.
ostr << bounded_expression.lower_bound
<< " <= " << bounded_expression.expression
<< " <= " << bounded_expression.upper_bound;
const double lb = bounded_expression.lower_bound;
const double ub = bounded_expression.upper_bound;
if (lb == ub) {
ostr << bounded_expression.expression << " = " << lb;
} else if (lb == -kInf) {
ostr << bounded_expression.expression << "" << ub;
} else if (ub == kInf) {
ostr << bounded_expression.expression << "" << lb;
} else {
ostr << lb << "" << bounded_expression.expression << "" << ub;
}
return ostr;
}
@@ -173,15 +235,16 @@ double QuadraticExpression::EvaluateWithDefaultZero(
}
std::ostream& operator<<(std::ostream& ostr, const QuadraticExpression& expr) {
// TODO(b/169415597): improve quadratic expression formatting.
// TODO(b/169415597): improve quadratic expression formatting. See b/170991498
// for desired improvements for LinearExpression streaming which are also
// applicable here.
bool first = true;
for (const auto v : expr.quadratic_terms().SortedKeys()) {
if (first) {
const double coeff = expr.quadratic_terms().at(v);
if (coeff != 0) {
ostr << LeadingCoefficientFormatter(coeff, first);
first = false;
} else {
ostr << " + ";
}
ostr << expr.quadratic_terms().at(v) << "*";
const Variable first_variable(expr.quadratic_terms().storage(),
v.typed_id().first);
const Variable second_variable(expr.quadratic_terms().storage(),
@@ -193,19 +256,13 @@ std::ostream& operator<<(std::ostream& ostr, const QuadraticExpression& expr) {
}
}
for (const auto v : expr.linear_terms().SortedKeys()) {
if (first) {
const double coeff = expr.linear_terms().at(v);
if (coeff != 0) {
ostr << LeadingCoefficientFormatter(coeff, first) << v;
first = false;
} else {
ostr << " + ";
}
ostr << expr.linear_terms().at(v) << "*" << v;
}
if (!first) {
ostr << " + ";
}
ostr << expr.offset();
ostr << ConstantFormatter(expr.offset(), first);
return ostr;
}

View File

@@ -910,9 +910,13 @@ H AbslHashValue(H h, const Variable& variable) {
}
std::ostream& operator<<(std::ostream& ostr, const Variable& variable) {
// TODO(b/170992529): handle the case of empty variable name and quoting when
// the variable name contains invalid characters.
ostr << variable.name();
// TODO(b/170992529): handle quoting of invalid characters in the name.
const std::string& name = variable.name();
if (name.empty()) {
ostr << "__var#" << variable.id() << "__";
} else {
ostr << name;
}
return ostr;
}

View File

@@ -15,11 +15,11 @@
#include <algorithm>
#include <cstdint>
#include <iterator>
#include <string>
#include <utility>
#include <vector>
#include "ortools/base/integral_types.h"
#include "absl/container/flat_hash_map.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
@@ -253,14 +253,16 @@ absl::StatusOr<::operations_research::MPModelProto> MathOptModelToMPModelProto(
}
const SparseDoubleMatrixProto& origin_qp_terms =
model.objective().quadratic_coefficients();
MPQuadraticObjective& destination_qp_terms =
*output.mutable_quadratic_objective();
for (int k = 0; k < origin_qp_terms.coefficients().size(); ++k) {
destination_qp_terms.add_qvar1_index(
variable_id_to_mp_position[origin_qp_terms.row_ids(k)]);
destination_qp_terms.add_qvar2_index(
variable_id_to_mp_position[origin_qp_terms.column_ids(k)]);
destination_qp_terms.add_coefficient(origin_qp_terms.coefficients(k));
if (!origin_qp_terms.coefficients().empty()) {
MPQuadraticObjective& destination_qp_terms =
*output.mutable_quadratic_objective();
for (int k = 0; k < origin_qp_terms.coefficients().size(); ++k) {
destination_qp_terms.add_qvar1_index(
variable_id_to_mp_position[origin_qp_terms.row_ids(k)]);
destination_qp_terms.add_qvar2_index(
variable_id_to_mp_position[origin_qp_terms.column_ids(k)]);
destination_qp_terms.add_coefficient(origin_qp_terms.coefficients(k));
}
}
// TODO(user): use the constraint iterator from scip_solver.cc here.

View File

@@ -23,7 +23,8 @@ option java_multiple_files = true;
// As used below, we define "#variables" = size(VariablesProto.ids).
message VariablesProto {
// Must be nonnegative and strictly increasing.
// Must be nonnegative and strictly increasing. The max(int64) value can't be
// used.
repeated int64 ids = 1;
// Should have length equal to #variables, values in [-inf, inf).
repeated double lower_bounds = 2;
@@ -73,7 +74,8 @@ message ObjectiveProto {
// As used below, we define "#linear constraints" =
// size(LinearConstraintsProto.ids).
message LinearConstraintsProto {
// Must be nonnegative and strictly increasing.
// Must be nonnegative and strictly increasing. The max(int64) value can't be
// used.
repeated int64 ids = 1;
// Should have length equal to #linear constraints, values in [-inf, inf).
repeated double lower_bounds = 2;

View File

@@ -51,6 +51,7 @@ enum SolverTypeProto {
// It supports solving IPs and can scale MIPs to solve them as IPs.
SOLVER_TYPE_CP_SAT = 4;
SOLVER_TYPE_RESERVED_FIVE = 5;
// GNU Linear Programming Kit (GLPK).
//
@@ -68,6 +69,7 @@ enum SolverTypeProto {
// for details.
SOLVER_TYPE_GLPK = 6;
SOLVER_TYPE_RESERVED_SEVEN = 7;
}
// Selects an algorithm for solving linear programs.
@@ -162,9 +164,46 @@ message SolveParametersProto {
// (e.g. it could be the relative GAP divided by the objective value of the
// feasible solution if this is non-zero). Solvers consider a solution optimal
// if its GAPs are below these limits (most solvers use both versions).
// TODO(b/213603287): rename as relative_gap_tolerance and
// absolute_gap_tolerance.
optional double relative_gap_limit = 17;
optional double absolute_gap_limit = 18;
// The solver stops early if it can prove there are no primal solutions at
// least as good as cutoff.
//
// On an early stop, the solver returns termination reason LIMIT_REACHED and
// with limit CUTOFF and is not required to give any extra solution
// information. Has no effect on the return value if there is no early stop.
//
// It is recommended that you use a tolerance if you want solutions with
// objective exactly equal to cutoff to be returned.
//
// See the user guide for more details and a comparison with best_bound_limit.
optional double cutoff_limit = 20;
// The solver stops early as soon as it finds a solution at least this good,
// with termination reason LIMIT_REACHED and limit LIMIT_OBJECTIVE.
optional double objective_limit = 21;
// The solver stops early as soon as it proves the best bound is at least this
// good, with termination reason LIMIT_REACHED and limit LIMIT_OBJECTIVE.
//
// See the user guide for more details and a comparison with cutoff_limit.
optional double best_bound_limit = 22;
// The solver stops early after finding this many feasible solutions, with
// termination reason LIMIT_REACHED and limit LIMIT_SOLUTION. Must be greater
// than zero if set. It is often used get the solver to stop on the first
// feasible solution found. Note that there is no guarantee on the objective
// value for any of the returned solutions.
//
// Solvers will typically not return more solutions than the solution limit,
// but this is not enforced by MathOpt, see also b/214041169.
//
// Currently supported for Gurobi and SCIP, and for CP-SAT only with value 1.
optional int32 solution_limit = 23;
// Enables printing the solver implementation traces. The location of those
// traces depend on the solver. For SCIP and Gurobi this will be the standard
// output streams. For Glop and CP-SAT this will LOG(INFO).

View File

@@ -187,8 +187,16 @@ enum LimitProto {
// The algorithm stopped because it ran out of memory.
LIMIT_MEMORY = 6;
// The algorithm stopped because it found a solution better than a minimum
// limit set by the user.
// The solver was run with a cutoff (e.g. SolveParameters.cutoff_limit was
// set) on the objective, indicating that the user did not want any solution
// worse than the cutoff, and the solver concluded there were no solutions at
// least as good as the cutoff. Typically no further solution information is
// provided.
LIMIT_CUTOFF = 12;
// The algorithm stopped because it either found a solution or a bound better
// than a limit set by the user (see SolveParameters.objective_limit and
// SolveParameters.best_bound_limit).
LIMIT_OBJECTIVE = 7;
// The algorithm stopped because the norm of an iterate became too large.

View File

@@ -192,7 +192,7 @@ absl::StatusOr<CuttingStockSolution> SolveCuttingStock(
ASSIGN_OR_RETURN(math_opt::SolveResult solve_result, solver->Solve());
if (solve_result.termination.reason !=
math_opt::TerminationReason::kOptimal) {
return absl::InternalErrorBuilder()
return util::InternalErrorBuilder()
<< "Failed to solve leader LP problem at iteration "
<< pricing_round << " termination: " << solve_result.termination;
}
@@ -222,7 +222,7 @@ absl::StatusOr<CuttingStockSolution> SolveCuttingStock(
math_opt::Solve(model, math_opt::SolverType::kCpSat));
if (solve_result.termination.reason !=
math_opt::TerminationReason::kOptimal) {
return absl::InternalErrorBuilder()
return util::InternalErrorBuilder()
<< "Failed to solve final cutting stock MIP, termination: "
<< solve_result.termination;
}

View File

@@ -100,6 +100,27 @@ std::vector<std::string> SetSolveParameters(
if (parameters.has_absolute_gap_limit()) {
sat_parameters.set_absolute_gap_limit(parameters.absolute_gap_limit());
}
if (parameters.has_cutoff_limit()) {
warnings.push_back(
"The cutoff_limit parameter is not supported for CP-SAT.");
}
if (parameters.has_best_bound_limit()) {
warnings.push_back(
"The best_bound_limit parameter is not supported for CP-SAT.");
}
if (parameters.has_objective_limit()) {
warnings.push_back(
"The objective_limit parameter is not supported for CP-SAT.");
}
if (parameters.has_solution_limit()) {
if (parameters.solution_limit() == 1) {
sat_parameters.set_stop_after_first_solution(true);
} else {
warnings.push_back(absl::StrCat(
"The CP-SAT solver only supports value 1 for solution_limit, found: ",
parameters.solution_limit()));
}
}
if (parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) {
warnings.push_back(

View File

@@ -363,6 +363,18 @@ GlopSolver::MergeSolveParameters(const SolveParametersProto& solver_parameters,
"heuristics was set to: ",
ProtoEnumToString(solver_parameters.heuristics())));
}
if (solver_parameters.has_cutoff_limit()) {
warnings.push_back("GLOP does not support 'cutoff_limit' parameter");
}
if (solver_parameters.has_objective_limit()) {
warnings.push_back("GLOP does not support 'objective_limit' parameter");
}
if (solver_parameters.has_best_bound_limit()) {
warnings.push_back("GLOP does not support 'best_bound_limit' parameter");
}
if (solver_parameters.has_solution_limit()) {
warnings.push_back("GLOP does not support 'solution_limit' parameter");
}
return std::make_pair(std::move(result), std::move(warnings));
}
@@ -762,6 +774,11 @@ absl::StatusOr<std::unique_ptr<SolverInterface>> GlopSolver::New(
"Glop does not support quadratic objectives");
}
auto solver = absl::WrapUnique(new GlopSolver);
// By default Glop CHECKs that bounds are always consistent (lb < ub); thus it
// would fail if the initial model or later updates temporarily set inverted
// bounds.
solver->linear_program_.SetDcheckBounds(false);
solver->linear_program_.SetName(model.name());
solver->linear_program_.SetMaximizationProblem(model.objective().maximize());
solver->linear_program_.SetObjectiveOffset(model.objective().offset());

View File

@@ -16,6 +16,7 @@
#include <algorithm>
#include <cstdint>
#include <functional>
#include <limits>
#include <memory>
#include <optional>
#include <string>
@@ -31,6 +32,7 @@
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "absl/strings/string_view.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
@@ -69,6 +71,8 @@ namespace math_opt {
namespace {
constexpr double kInf = std::numeric_limits<double>::infinity();
int64_t SafeId(const VariablesProto& variables, int index) {
if (variables.ids().empty()) {
return index;
@@ -398,13 +402,14 @@ GScipParameters::MetaParamValue ConvertMathOptEmphasis(EmphasisProto emphasis) {
}
}
GScipParameters GScipSolver::MergeParameters(
const SolveParametersProto& solve_parameters) {
std::pair<GScipParameters, std::vector<std::string>>
GScipSolver::MergeParameters(const SolveParametersProto& solve_parameters) {
// First build the result by translating common parameters to a
// GScipParameters, and then merging with user provided gscip_parameters.
// This results in user provided solver specific parameters overwriting
// common parameters should there be any conflict.
GScipParameters result;
std::vector<std::string> warnings;
// By default SCIP catches Ctrl-C but we don't want this behavior when the
// users uses SCIP through MathOpt.
@@ -430,6 +435,22 @@ GScipParameters GScipSolver::MergeParameters(
solve_parameters.absolute_gap_limit();
}
if (solve_parameters.has_objective_limit()) {
warnings.push_back("parameter objective_limit not supported for gSCIP.");
}
if (solve_parameters.has_best_bound_limit()) {
warnings.push_back("parameter best_bound_limit not supported for gSCIP.");
}
if (solve_parameters.has_cutoff_limit()) {
result.set_objective_limit(solve_parameters.cutoff_limit());
}
if (solve_parameters.has_solution_limit()) {
(*result.mutable_int_params())["limits/solutions"] =
solve_parameters.solution_limit();
}
// GScip has also GScipSetOutputEnabled() but this changes the log
// level. Setting `silence_output` sets the `quiet` field on the default
// message handler of SCIP which removes the output. Here it is important to
@@ -496,7 +517,7 @@ GScipParameters GScipSolver::MergeParameters(
result.MergeFrom(solve_parameters.gscip());
return result;
return {std::move(result), std::move(warnings)};
}
namespace {
@@ -514,7 +535,8 @@ std::string JoinDetails(const std::string& gscip_detail,
ProblemStatusProto GetProblemStatusProto(const GScipOutput::Status gscip_status,
const bool has_feasible_solution,
const bool has_finite_dual_bound) {
const bool has_finite_dual_bound,
const bool was_cutoff) {
ProblemStatusProto problem_status;
if (has_feasible_solution) {
problem_status.set_primal_status(FEASIBILITY_STATUS_FEASIBLE);
@@ -528,7 +550,9 @@ ProblemStatusProto GetProblemStatusProto(const GScipOutput::Status gscip_status,
problem_status.set_dual_status(FEASIBILITY_STATUS_FEASIBLE);
break;
case GScipOutput::INFEASIBLE:
problem_status.set_primal_status(FEASIBILITY_STATUS_INFEASIBLE);
if (!was_cutoff) {
problem_status.set_primal_status(FEASIBILITY_STATUS_INFEASIBLE);
}
break;
case GScipOutput::UNBOUNDED:
problem_status.set_dual_status(FEASIBILITY_STATUS_INFEASIBLE);
@@ -547,7 +571,8 @@ ProblemStatusProto GetProblemStatusProto(const GScipOutput::Status gscip_status,
absl::StatusOr<TerminationProto> ConvertTerminationReason(
const GScipOutput::Status gscip_status,
const std::string& gscip_status_detail, const bool has_feasible_solution) {
const std::string& gscip_status_detail, const bool has_feasible_solution,
const bool had_cutoff) {
switch (gscip_status) {
case GScipOutput::USER_INTERRUPT:
return TerminateForLimit(
@@ -591,8 +616,12 @@ absl::StatusOr<TerminationProto> ConvertTerminationReason(
JoinDetails(gscip_status_detail,
"underlying gSCIP status: GAP_LIMIT"));
case GScipOutput::INFEASIBLE:
return TerminateForReason(TERMINATION_REASON_INFEASIBLE,
gscip_status_detail);
if (had_cutoff) {
return TerminateForLimit(LIMIT_CUTOFF, gscip_status_detail);
} else {
return TerminateForReason(TERMINATION_REASON_INFEASIBLE,
gscip_status_detail);
}
case GScipOutput::UNBOUNDED: {
if (has_feasible_solution) {
return TerminateForReason(
@@ -635,21 +664,29 @@ absl::StatusOr<TerminationProto> ConvertTerminationReason(
} // namespace
absl::StatusOr<SolveResultProto> GScipSolver::CreateSolveResultProto(
GScipResult gscip_result,
const ModelSolveParametersProto& model_parameters) {
GScipResult gscip_result, const ModelSolveParametersProto& model_parameters,
const std::optional<double> cutoff) {
SolveResultProto solve_result;
ASSIGN_OR_RETURN(
*solve_result.mutable_termination(),
ConvertTerminationReason(gscip_result.gscip_output.status(),
gscip_result.gscip_output.status_detail(),
!gscip_result.solutions.empty()));
*solve_result.mutable_solve_stats()->mutable_problem_status() =
GetProblemStatusProto(
gscip_result.gscip_output.status(), !gscip_result.solutions.empty(),
std::isfinite(gscip_result.gscip_output.stats().best_bound()));
const int num_solutions = gscip_result.solutions.size();
CHECK_EQ(num_solutions, gscip_result.objective_values.size());
ConvertTerminationReason(
gscip_result.gscip_output.status(),
gscip_result.gscip_output.status_detail(),
/*has_feasible_solution=*/!gscip_result.solutions.empty(),
/*had_cutoff=*/cutoff.has_value()));
const bool is_maximize = gscip_->ObjectiveIsMaximize();
// When an objective limit is set, SCIP returns the solutions worse than the
// limit, we need to filter these out manually.
const auto meets_cutoff = [cutoff, is_maximize](const double obj_value) {
if (!cutoff.has_value()) {
return true;
}
if (is_maximize) {
return obj_value >= *cutoff;
} else {
return obj_value <= *cutoff;
}
};
LazyInitialized<std::vector<int64_t>> sorted_variables([&]() {
std::vector<int64_t> sorted;
@@ -660,7 +697,12 @@ absl::StatusOr<SolveResultProto> GScipSolver::CreateSolveResultProto(
std::sort(sorted.begin(), sorted.end());
return sorted;
});
CHECK_EQ(gscip_result.solutions.size(), gscip_result.objective_values.size());
for (int i = 0; i < gscip_result.solutions.size(); ++i) {
// GScip ensures the solutions are returned best objective first.
if (!meets_cutoff(gscip_result.objective_values[i])) {
break;
}
SolutionProto* const solution = solve_result.add_solutions();
PrimalSolutionProto* const primal_solution =
solution->mutable_primal_solution();
@@ -676,12 +718,24 @@ absl::StatusOr<SolveResultProto> GScipSolver::CreateSolveResultProto(
gscip_result.primal_ray,
model_parameters.variable_values_filter());
}
// TODO(user): add support for the basis and dual solutions in gscip, then
// populate them here.
const bool has_feasible_solution = solve_result.solutions_size() > 0;
*solve_result.mutable_solve_stats()->mutable_problem_status() =
GetProblemStatusProto(
gscip_result.gscip_output.status(),
/*has_feasible_solution=*/has_feasible_solution,
/*has_finite_dual_bound=*/
std::isfinite(gscip_result.gscip_output.stats().best_bound()),
/*was_cutoff=*/solve_result.termination().limit() == LIMIT_CUTOFF);
SolveStatsProto* const common_stats = solve_result.mutable_solve_stats();
const GScipSolvingStats& gscip_stats = gscip_result.gscip_output.stats();
common_stats->set_best_dual_bound(gscip_stats.best_bound());
common_stats->set_best_primal_bound(gscip_stats.best_objective());
// If we found no solutions meeting the cutoff, we have no primal bound.
if (has_feasible_solution) {
common_stats->set_best_primal_bound(gscip_stats.best_objective());
} else {
common_stats->set_best_primal_bound(is_maximize ? -kInf : kInf);
}
common_stats->set_node_count(gscip_stats.node_count());
common_stats->set_simplex_iterations(gscip_stats.primal_simplex_iterations() +
gscip_stats.dual_simplex_iterations());
@@ -741,7 +795,10 @@ absl::StatusOr<SolveResultProto> GScipSolver::Solve(
std::make_unique<GScipSolverMessageCallbackHandler>(message_cb);
}
const GScipParameters gscip_parameters = MergeParameters(parameters);
const auto [gscip_parameters, warnings] = MergeParameters(parameters);
if (parameters.strictness().bad_parameter() && !warnings.empty()) {
return absl::InvalidArgumentError(absl::StrJoin(warnings, "; "));
}
// TODO(user): reorganize gscip to respect warning is error argument on bad
// parameters.
@@ -781,7 +838,13 @@ absl::StatusOr<SolveResultProto> GScipSolver::Solve(
ASSIGN_OR_RETURN(
SolveResultProto result,
CreateSolveResultProto(std::move(gscip_result), model_parameters));
CreateSolveResultProto(std::move(gscip_result), model_parameters,
parameters.has_cutoff_limit()
? std::make_optional(parameters.cutoff_limit())
: std::nullopt));
for (const std::string& warning : warnings) {
result.add_warnings(warning);
}
CHECK_OK(util_time::EncodeGoogleApiProto(
absl::Now() - start, result.mutable_solve_stats()->mutable_solve_time()));
return result;

View File

@@ -55,7 +55,9 @@ class GScipSolver : public SolverInterface {
absl::Status Update(const ModelUpdateProto& model_update) override;
bool CanUpdate(const ModelUpdateProto& model_update) override;
static GScipParameters MergeParameters(
// Returns the merged parameters and a list of warnings for unsupported
// parameters.
static std::pair<GScipParameters, std::vector<std::string>> MergeParameters(
const SolveParametersProto& solve_parameters);
private:
@@ -112,7 +114,8 @@ class GScipSolver : public SolverInterface {
absl::Span<const int64_t> variable_ids);
absl::StatusOr<SolveResultProto> CreateSolveResultProto(
GScipResult gscip_result,
const ModelSolveParametersProto& model_parameters);
const ModelSolveParametersProto& model_parameters,
std::optional<double> cutoff);
const std::unique_ptr<GScip> gscip_;
InterruptEventHandler interrupt_event_handler_;

View File

@@ -13,7 +13,9 @@
#include "ortools/math_opt/solvers/gurobi/g_gurobi.h"
#include <string>
#include <string_view>
#include <utility>
#include "ortools/base/cleanup.h"
#include "absl/status/status.h"

View File

@@ -93,16 +93,6 @@ absl::StatusOr<std::unique_ptr<Gurobi>> GurobiFromInitArgs(
}
}
std::vector<std::string> RepeatedPtrFieldToVec(
const google::protobuf::RepeatedPtrField<std::string>& proto_string_vec) {
std::vector<std::string> result;
result.reserve(proto_string_vec.size());
for (const std::string& str : proto_string_vec) {
result.push_back(str);
}
return result;
}
inline BasisStatusProto ConvertVariableStatus(const int status) {
switch (status) {
case GRB_BASIC:
@@ -180,6 +170,34 @@ GurobiParametersProto MergeParameters(
parameter->set_value(absl::StrCat(relative_gap_limit));
}
if (solve_parameters.has_cutoff_limit()) {
GurobiParametersProto::Parameter* const parameter =
merged_parameters.add_parameters();
parameter->set_name(GRB_DBL_PAR_CUTOFF);
parameter->set_value(absl::StrCat(solve_parameters.cutoff_limit()));
}
if (solve_parameters.has_objective_limit()) {
GurobiParametersProto::Parameter* const parameter =
merged_parameters.add_parameters();
parameter->set_name(GRB_DBL_PAR_BESTOBJSTOP);
parameter->set_value(absl::StrCat(solve_parameters.objective_limit()));
}
if (solve_parameters.has_best_bound_limit()) {
GurobiParametersProto::Parameter* const parameter =
merged_parameters.add_parameters();
parameter->set_name(GRB_DBL_PAR_BESTBDSTOP);
parameter->set_value(absl::StrCat(solve_parameters.best_bound_limit()));
}
if (solve_parameters.has_solution_limit()) {
GurobiParametersProto::Parameter* const parameter =
merged_parameters.add_parameters();
parameter->set_name(GRB_INT_PAR_SOLUTIONLIMIT);
parameter->set_value(absl::StrCat(solve_parameters.solution_limit()));
}
if (solve_parameters.has_random_seed()) {
const int random_seed =
std::min(GRB_MAXINT, std::max(solve_parameters.random_seed(), 0));
@@ -374,6 +392,27 @@ const absl::flat_hash_set<CallbackEventProto>& SupportedLPEvents() {
return *kEvents;
}
// Gurobi names (model, variables and constraints) must be no longer than 255
// characters; or Gurobi fails with an error.
constexpr std::size_t kMaxNameSize = 255;
// Returns a string of at most kMaxNameSize max size.
std::string TruncateName(const std::string_view original_name) {
return std::string(
original_name.substr(0, std::min(kMaxNameSize, original_name.size())));
}
// Truncate the names of variables and constraints.
std::vector<std::string> TruncateNames(
const google::protobuf::RepeatedPtrField<std::string>& original_names) {
std::vector<std::string> result;
result.reserve(original_names.size());
for (const std::string& original_name : original_names) {
result.push_back(TruncateName(original_name));
}
return result;
}
} // namespace
GurobiSolver::GurobiSolver(std::unique_ptr<Gurobi> g_gurobi)
@@ -400,7 +439,7 @@ absl::StatusOr<TerminationProto> GurobiSolver::ConvertTerminationReason(
return TerminateForReason(TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED,
"Gurobi status GRB_INF_OR_UNBD");
case GRB_CUTOFF:
return TerminateForLimit(LIMIT_OBJECTIVE, "Gurobi status GRB_CUTOFF");
return TerminateForLimit(LIMIT_CUTOFF, "Gurobi status GRB_CUTOFF");
case GRB_ITERATION_LIMIT:
return TerminateForLimit(LIMIT_ITERATION);
case GRB_NODE_LIMIT:
@@ -416,8 +455,7 @@ absl::StatusOr<TerminationProto> GurobiSolver::ConvertTerminationReason(
case GRB_SUBOPTIMAL:
return TerminateForReason(TERMINATION_REASON_IMPRECISE);
case GRB_USER_OBJ_LIMIT:
return TerminateForLimit(LIMIT_OBJECTIVE,
"Gurobi status GRB_USR_OBJ_LIMIT");
return TerminateForLimit(LIMIT_OBJECTIVE);
case GRB_LOADED:
return absl::InternalError(
"Error creating termination reason, unexpected gurobi status code "
@@ -1184,7 +1222,7 @@ absl::Status GurobiSolver::AddNewVariables(
// We need to copy the names, RepeatedPtrField cannot be converted to
// absl::Span<std::string>.
const std::vector<std::string> variable_names =
RepeatedPtrFieldToVec(new_variables.names());
TruncateNames(new_variables.names());
RETURN_IF_ERROR(gurobi_->AddVars(
/*obj=*/{},
/*lb=*/new_variables.lower_bounds(),
@@ -1247,7 +1285,7 @@ absl::Status GurobiSolver::AddNewConstraints(
// We need to copy the names, RepeatedPtrField cannot be converted to
// absl::Span<std::string>.
const std::vector<std::string> constraint_names =
RepeatedPtrFieldToVec(constraints.names());
TruncateNames(constraints.names());
// Constraints are translated into:
// 1. ax <= upper_bound (if lower bound <= -GRB_INFINITY, and upper_bound
// is finite and less than GRB_INFINITY)
@@ -1346,8 +1384,8 @@ absl::Status GurobiSolver::UpdateInt32ListAttribute(
absl::Status GurobiSolver::LoadModel(const ModelProto& input_model) {
CHECK(gurobi_ != nullptr);
RETURN_IF_ERROR(
gurobi_->SetStringAttr(GRB_STR_ATTR_MODELNAME, input_model.name()));
RETURN_IF_ERROR(gurobi_->SetStringAttr(GRB_STR_ATTR_MODELNAME,
TruncateName(input_model.name())));
RETURN_IF_ERROR(AddNewVariables(input_model.variables()));
RETURN_IF_ERROR(AddNewConstraints(input_model.linear_constraints()));

View File

@@ -83,6 +83,18 @@ PdlpSolver::MergeParameters(const SolveParametersProto& parameters) {
absl::ToDoubleSeconds(
util_time::DecodeGoogleApiProto(parameters.time_limit()).value()));
}
if (parameters.has_cutoff_limit()) {
warnings.push_back("parameter cutoff_limit not supported for PDLP");
}
if (parameters.has_objective_limit()) {
warnings.push_back("parameter best_objective_limit not supported for PDLP");
}
if (parameters.has_best_bound_limit()) {
warnings.push_back("parameter best_bound_limit not supported for PDLP");
}
if (parameters.has_solution_limit()) {
warnings.push_back("parameter solution_limit not supported for PDLP");
}
if (parameters.has_random_seed()) {
warnings.push_back("parameter random_seed not supported for PDLP");
}

View File

@@ -154,9 +154,9 @@ cc_library(
)
cc_library(
name = "solver_parameters_validator",
srcs = ["solver_parameters_validator.cc"],
hdrs = ["solver_parameters_validator.h"],
name = "solve_parameters_validator",
srcs = ["solve_parameters_validator.cc"],
hdrs = ["solve_parameters_validator.h"],
deps = [
"//ortools/base:protoutil",
"//ortools/base:status_macros",

View File

@@ -116,25 +116,21 @@ absl::Status CheckSortedIdsNotBad(const absl::Span<const int64_t> ids,
}
} // namespace
absl::Status CheckIdsNonnegativeAndStrictlyIncreasing(
absl::Span<const int64_t> ids) {
absl::Status CheckIdsRangeAndStrictlyIncreasing(absl::Span<const int64_t> ids) {
int64_t previous{-1};
for (int i = 0; i < ids.size(); ++i) {
if (ids[i] <= previous) {
std::string error_base = absl::StrCat(
"Expected ids to be nonnegative and strictly increasing, but at "
"index ",
i, ", found id: ", ids[i]);
if (i == 0) {
return util::InvalidArgumentErrorBuilder()
<< error_base << " (a negative id)";
} else {
return util::InvalidArgumentErrorBuilder()
<< error_base << " and at index " << i - 1
<< " found id: " << ids[i - 1];
}
for (int i = 0; i < ids.size(); previous = ids[i], ++i) {
if (ids[i] < 0 || ids[i] == std::numeric_limits<int64_t>::max()) {
return util::InvalidArgumentErrorBuilder()
<< "Expected ids to be nonnegative and not max(int64_t) but at "
"index "
<< i << " found id: " << ids[i];
}
if (ids[i] <= previous) {
return util::InvalidArgumentErrorBuilder()
<< "Expected ids to be strictly increasing, but at index " << i
<< " found id: " << ids[i] << " and at index " << i - 1
<< " found id: " << ids[i - 1];
}
previous = ids[i];
}
return absl::OkStatus();
}

View File

@@ -24,8 +24,9 @@
namespace operations_research {
namespace math_opt {
absl::Status CheckIdsNonnegativeAndStrictlyIncreasing(
absl::Span<const int64_t> ids);
// Checks that the input ids are in [0, max(int64_t)) range and that their are
// strictly increasing.
absl::Status CheckIdsRangeAndStrictlyIncreasing(absl::Span<const int64_t> ids);
// Checks that the elements of ids are a subset of universe.
//

View File

@@ -58,7 +58,7 @@ absl::Status ValidateBranchingPriorities(
absl::Status ValidateSparseVectorFilter(const SparseVectorFilterProto& v,
const IdNameBiMap& valid_ids) {
RETURN_IF_ERROR(CheckIdsNonnegativeAndStrictlyIncreasing(v.filtered_ids()));
RETURN_IF_ERROR(CheckIdsRangeAndStrictlyIncreasing(v.filtered_ids()));
RETURN_IF_ERROR(
CheckIdsSubset(v.filtered_ids(), valid_ids, "filtered_ids", "model IDs"));
if (!v.filter_by_ids() && !v.filtered_ids().empty()) {

View File

@@ -46,7 +46,7 @@ constexpr double kInf = std::numeric_limits<double>::infinity();
absl::Status VariablesValid(const VariablesProto& variables,
const bool check_names) {
RETURN_IF_ERROR(CheckIdsNonnegativeAndStrictlyIncreasing(variables.ids()))
RETURN_IF_ERROR(CheckIdsRangeAndStrictlyIncreasing(variables.ids()))
<< "Bad variable ids";
RETURN_IF_ERROR(
CheckValues(MakeView(variables.ids(), variables.lower_bounds()),
@@ -147,8 +147,7 @@ absl::Status ObjectiveUpdatesValidForModel(
absl::Status LinearConstraintsValid(
const LinearConstraintsProto& linear_constraints, const bool check_names) {
RETURN_IF_ERROR(
CheckIdsNonnegativeAndStrictlyIncreasing(linear_constraints.ids()))
RETURN_IF_ERROR(CheckIdsRangeAndStrictlyIncreasing(linear_constraints.ids()))
<< "Bad linear constraint ids";
RETURN_IF_ERROR(CheckValues(
MakeView(linear_constraints.ids(), linear_constraints.lower_bounds()),
@@ -229,11 +228,11 @@ absl::Status ValidateModel(const ModelProto& model, const bool check_names) {
absl::Status ValidateModelUpdate(const ModelUpdateProto& model_update,
const bool check_names) {
RETURN_IF_ERROR(CheckIdsNonnegativeAndStrictlyIncreasing(
RETURN_IF_ERROR(CheckIdsRangeAndStrictlyIncreasing(
model_update.deleted_linear_constraint_ids()))
<< "ModelUpdateProto.deleted_linear_constraint_ids invalid";
RETURN_IF_ERROR(CheckIdsNonnegativeAndStrictlyIncreasing(
model_update.deleted_variable_ids()))
RETURN_IF_ERROR(
CheckIdsRangeAndStrictlyIncreasing(model_update.deleted_variable_ids()))
<< "ModelUpdateProto.deleted_variable_ids invalid";
RETURN_IF_ERROR(VariableUpdatesValid(model_update.variable_updates()))
<< "ModelUpdateProto.variable_updates invalid";

View File

@@ -236,6 +236,12 @@ absl::Status ValidateTerminationConsistency(const SolveResultProto& result) {
case TERMINATION_REASON_LIMIT_REACHED:
// TODO(b/211677729): update when TERMINATION_REASON_FEASIBLE is added.
// No primal or dual requirements so we check consistency.
if (result.termination().limit() == LIMIT_CUTOFF) {
if (result.solutions_size() > 0) {
return absl::InvalidArgumentError(
"For LIMIT_CUTOFF expected no solutions");
}
}
RETURN_IF_ERROR(CheckPrimalSolutionAndStatusConsistency(result));
RETURN_IF_ERROR(CheckDualSolutionAndStatusConsistency(result));
return absl::OkStatus();

View File

@@ -11,13 +11,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include "ortools/math_opt/validators/solver_parameters_validator.h"
#include "ortools/math_opt/validators/solve_parameters_validator.h"
#include <string>
#include "absl/memory/memory.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "ortools/math_opt/parameters.pb.h"
#include "ortools/base/status_macros.h"
@@ -26,34 +22,52 @@
namespace operations_research {
namespace math_opt {
absl::Status ValidateSolverParameters(const SolveParametersProto& parameters) {
absl::Status ValidateSolveParameters(const SolveParametersProto& parameters) {
RETURN_IF_ERROR(
util_time::DecodeGoogleApiProto(parameters.time_limit()).status())
<< "invalid parameters.time_limit";
<< "invalid SolveParameters.time_limit";
if (parameters.has_threads()) {
if (parameters.threads() <= 0) {
return absl::InvalidArgumentError(
absl::StrCat("parameters.threads = ", parameters.threads(), " <= 0"));
return absl::InvalidArgumentError(absl::StrCat(
"SolveParameters.threads = ", parameters.threads(), " <= 0"));
}
}
if (parameters.has_relative_gap_limit()) {
if (parameters.relative_gap_limit() < 0) {
return absl::InvalidArgumentError(absl::StrCat(
"parameters.relative_gap_limit = ", parameters.relative_gap_limit(),
" < 0"));
return absl::InvalidArgumentError(
absl::StrCat("SolveParameters.relative_gap_limit = ",
parameters.relative_gap_limit(), " < 0"));
}
}
if (parameters.has_absolute_gap_limit()) {
if (parameters.absolute_gap_limit() < 0) {
return absl::InvalidArgumentError(absl::StrCat(
"parameters.absolute_gap_limit = ", parameters.absolute_gap_limit(),
" < 0"));
return absl::InvalidArgumentError(
absl::StrCat("SolveParameters.absolute_gap_limit = ",
parameters.absolute_gap_limit(), " < 0"));
}
}
if (parameters.has_solution_limit() && parameters.solution_limit() <= 0) {
return util::InvalidArgumentErrorBuilder()
<< "SolveParameters.solution_limit = " << parameters.solution_limit()
<< " should be positive.";
}
if (std::isnan(parameters.cutoff_limit())) {
return absl::InvalidArgumentError("SolveParameters.cutoff_limit was NaN");
}
if (std::isnan(parameters.objective_limit())) {
return absl::InvalidArgumentError(
"SolveParameters.objective_limit was NaN");
}
if (std::isnan(parameters.best_bound_limit())) {
return absl::InvalidArgumentError(
"SolveParameters.best_bound_limit was NaN");
}
return absl::OkStatus();
}

View File

@@ -11,8 +11,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef OR_TOOLS_MATH_OPT_VALIDATORS_SOLVER_PARAMETERS_VALIDATOR_H_
#define OR_TOOLS_MATH_OPT_VALIDATORS_SOLVER_PARAMETERS_VALIDATOR_H_
#ifndef OR_TOOLS_MATH_OPT_VALIDATORS_SOLVE_PARAMETERS_VALIDATOR_H_
#define OR_TOOLS_MATH_OPT_VALIDATORS_SOLVE_PARAMETERS_VALIDATOR_H_
#include "absl/status/status.h"
#include "ortools/math_opt/parameters.pb.h"
@@ -20,9 +20,10 @@
namespace operations_research {
namespace math_opt {
absl::Status ValidateSolverParameters(const SolveParametersProto& parameters);
// TODO(b/213697045): some parameters are still not validated.
absl::Status ValidateSolveParameters(const SolveParametersProto& parameters);
} // namespace math_opt
} // namespace operations_research
#endif // OR_TOOLS_MATH_OPT_VALIDATORS_SOLVER_PARAMETERS_VALIDATOR_H_
#endif // OR_TOOLS_MATH_OPT_VALIDATORS_SOLVE_PARAMETERS_VALIDATOR_H_

View File

@@ -49,7 +49,7 @@ template <typename T,
typename = std::enable_if_t<!std::is_floating_point<T>::value> >
absl::Status CheckIdsAndValues(const SparseVectorView<T>& vector_view,
absl::string_view value_name = "values") {
RETURN_IF_ERROR(CheckIdsNonnegativeAndStrictlyIncreasing(vector_view.ids()));
RETURN_IF_ERROR(CheckIdsRangeAndStrictlyIncreasing(vector_view.ids()));
RETURN_IF_ERROR(CheckValues(vector_view, value_name));
return absl::OkStatus();
}
@@ -73,7 +73,7 @@ template <typename T,
absl::Status CheckIdsAndValues(const SparseVectorView<T>& vector_view,
const DoubleOptions& options,
absl::string_view value_name = "values") {
RETURN_IF_ERROR(CheckIdsNonnegativeAndStrictlyIncreasing(vector_view.ids()));
RETURN_IF_ERROR(CheckIdsRangeAndStrictlyIncreasing(vector_view.ids()));
RETURN_IF_ERROR(CheckValues(vector_view, options, value_name));
return absl::OkStatus();
}