diff --git a/ortools/base/status_macros.h b/ortools/base/status_macros.h index 55daec8b43..6ecaee4a14 100644 --- a/ortools/base/status_macros.h +++ b/ortools/base/status_macros.h @@ -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 diff --git a/ortools/math_opt/core/BUILD.bazel b/ortools/math_opt/core/BUILD.bazel index 2b23e99458..f656e1165b 100644 --- a/ortools/math_opt/core/BUILD.bazel +++ b/ortools/math_opt/core/BUILD.bazel @@ -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", diff --git a/ortools/math_opt/core/model_summary.h b/ortools/math_opt/core/model_summary.h index 35169e410d..d425f48ecf 100644 --- a/ortools/math_opt/core/model_summary.h +++ b/ortools/math_opt/core/model_summary.h @@ -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::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 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::max()); next_free_id_ = id + 1; diff --git a/ortools/math_opt/core/solver.cc b/ortools/math_opt/core/solver.cc index ca9fc62245..ea5fa2df0e 100644 --- a/ortools/math_opt/core/solver.cc +++ b/ortools/math_opt/core/solver.cc @@ -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 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_)) diff --git a/ortools/math_opt/cpp/BUILD.bazel b/ortools/math_opt/cpp/BUILD.bazel index 76b1075242..a06f58a159 100644 --- a/ortools/math_opt/cpp/BUILD.bazel +++ b/ortools/math_opt/cpp/BUILD.bazel @@ -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", diff --git a/ortools/math_opt/cpp/enums.h b/ortools/math_opt/cpp/enums.h index 40098da3c9..783ea1d133 100644 --- a/ortools/math_opt/cpp/enums.h +++ b/ortools/math_opt/cpp/enums.h @@ -224,15 +224,37 @@ std::optional 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 -std::ostream& operator<<(std::ostream& out, const E value); +template . + typename = std::enable_if_t::kIsImplemented>> +std::ostream& operator<<(std::ostream& out, const E value) { + const std::optional opt_str = EnumToOptString(value); + if (opt_str.has_value()) { + out << *opt_str; + } else { + out << ">(value) + << ")>"; + } + return out; +} // Overload of operator<< for std::optional when Enum is implemented. // // When the value is nullopt, it prints "", else it prints the enum // value. -template -std::ostream& operator<<(std::ostream& out, const std::optional value); +template . + typename = std::enable_if_t::kIsImplemented>> +std::ostream& operator<<(std::ostream& out, const std::optional opt_value) { + if (opt_value.has_value()) { + out << *opt_value; + } else { + out << ""; + } + return out; +} //////////////////////////////////////////////////////////////////////////////// // Template functions implementations after this point. @@ -280,34 +302,6 @@ std::optional EnumFromString(const absl::string_view str) { return std::nullopt; } -template . - typename = std::enable_if_t::kIsImplemented>> -std::ostream& operator<<(std::ostream& out, const E value) { - const std::optional opt_str = EnumToOptString(value); - if (opt_str.has_value()) { - out << *opt_str; - } else { - out << ">(value) - << ")>"; - } - return out; -} - -template . - typename = std::enable_if_t::kIsImplemented>> -std::ostream& operator<<(std::ostream& out, const std::optional opt_value) { - if (opt_value.has_value()) { - out << *opt_value; - } else { - out << ""; - } - 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 diff --git a/ortools/math_opt/cpp/parameters.cc b/ortools/math_opt/cpp/parameters.cc index f06993148e..8abad8fd4e 100644 --- a/ortools/math_opt/cpp/parameters.cc +++ b/ortools/math_opt/cpp/parameters.cc @@ -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::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(); } diff --git a/ortools/math_opt/cpp/parameters.h b/ortools/math_opt/cpp/parameters.h index f19e6cc590..6925ff6f4e 100644 --- a/ortools/math_opt/cpp/parameters.h +++ b/ortools/math_opt/cpp/parameters.h @@ -211,6 +211,41 @@ struct SolveParameters { std::optional relative_gap_limit; std::optional 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 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 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 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 solution_limit; + // If unset, use the solver default. If set, it must be >= 1. std::optional threads; diff --git a/ortools/math_opt/cpp/solve.cc b/ortools/math_opt/cpp/solve.cc index 2ee67aa66e..020d5d7180 100644 --- a/ortools/math_opt/cpp/solve.cc +++ b/ortools/math_opt/cpp/solve.cc @@ -228,23 +228,6 @@ MessageCallback PrinterMessageCallback(std::ostream& output_stream, [=](const std::vector& messages) { impl->Call(messages); }; } -MessageCallback InfoLoggerMessageCallback(const absl::string_view prefix, - const absl::SourceLocation loc) { - return [=](const std::vector& 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& messages) { - for (const std::string& message : messages) { - VLOG(level).AtLocation(loc) << prefix << message; - } - }; -} } // namespace math_opt } // namespace operations_research diff --git a/ortools/math_opt/cpp/solve.h b/ortools/math_opt/cpp/solve.h index 521d54f24d..5bcd1bfe63 100644 --- a/ortools/math_opt/cpp/solve.h +++ b/ortools/math_opt/cpp/solve.h @@ -68,27 +68,6 @@ using MessageCallback = std::function&)>; 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. diff --git a/ortools/math_opt/cpp/solve_result.cc b/ortools/math_opt/cpp/solve_result.cc index bae20a0de4..c5b491b214 100644 --- a/ortools/math_opt/cpp/solve_result.cc +++ b/ortools/math_opt/cpp/solve_result.cc @@ -124,6 +124,8 @@ std::optional Enum::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 Enum::ToOptString(Limit value) { absl::Span Enum::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); } diff --git a/ortools/math_opt/cpp/solve_result.h b/ortools/math_opt/cpp/solve_result.h index 2c59da83c7..36025174d5 100644 --- a/ortools/math_opt/cpp/solve_result.h +++ b/ortools/math_opt/cpp/solve_result.h @@ -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, diff --git a/ortools/math_opt/cpp/variable_and_expressions.cc b/ortools/math_opt/cpp/variable_and_expressions.cc index e70878cdc8..30adf9efde 100644 --- a/ortools/math_opt/cpp/variable_and_expressions.cc +++ b/ortools/math_opt/cpp/variable_and_expressions.cc @@ -13,20 +13,20 @@ #include "ortools/math_opt/cpp/variable_and_expressions.h" -#include +#include +#include #include #include #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::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 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; } diff --git a/ortools/math_opt/cpp/variable_and_expressions.h b/ortools/math_opt/cpp/variable_and_expressions.h index 405b50c9b9..f1541c9778 100644 --- a/ortools/math_opt/cpp/variable_and_expressions.h +++ b/ortools/math_opt/cpp/variable_and_expressions.h @@ -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; } diff --git a/ortools/math_opt/io/proto_converter.cc b/ortools/math_opt/io/proto_converter.cc index f4ea8dd900..fbb8fdd326 100644 --- a/ortools/math_opt/io/proto_converter.cc +++ b/ortools/math_opt/io/proto_converter.cc @@ -15,11 +15,11 @@ #include #include +#include #include #include #include -#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. diff --git a/ortools/math_opt/model.proto b/ortools/math_opt/model.proto index 2f3d484731..2b17f703b6 100644 --- a/ortools/math_opt/model.proto +++ b/ortools/math_opt/model.proto @@ -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; diff --git a/ortools/math_opt/parameters.proto b/ortools/math_opt/parameters.proto index c3c5e54146..777824215b 100644 --- a/ortools/math_opt/parameters.proto +++ b/ortools/math_opt/parameters.proto @@ -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). diff --git a/ortools/math_opt/result.proto b/ortools/math_opt/result.proto index e4bf970e1a..05f63bae33 100644 --- a/ortools/math_opt/result.proto +++ b/ortools/math_opt/result.proto @@ -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. diff --git a/ortools/math_opt/samples/cutting_stock.cc b/ortools/math_opt/samples/cutting_stock.cc index ba04975d07..cad24b620f 100644 --- a/ortools/math_opt/samples/cutting_stock.cc +++ b/ortools/math_opt/samples/cutting_stock.cc @@ -192,7 +192,7 @@ absl::StatusOr 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 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; } diff --git a/ortools/math_opt/solvers/cp_sat_solver.cc b/ortools/math_opt/solvers/cp_sat_solver.cc index 6e61dc37bd..03085470c4 100644 --- a/ortools/math_opt/solvers/cp_sat_solver.cc +++ b/ortools/math_opt/solvers/cp_sat_solver.cc @@ -100,6 +100,27 @@ std::vector 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( diff --git a/ortools/math_opt/solvers/glop_solver.cc b/ortools/math_opt/solvers/glop_solver.cc index acf1822afd..d6f441f043 100644 --- a/ortools/math_opt/solvers/glop_solver.cc +++ b/ortools/math_opt/solvers/glop_solver.cc @@ -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> 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()); diff --git a/ortools/math_opt/solvers/gscip_solver.cc b/ortools/math_opt/solvers/gscip_solver.cc index 2af49e82ea..26a2b47bdc 100644 --- a/ortools/math_opt/solvers/gscip_solver.cc +++ b/ortools/math_opt/solvers/gscip_solver.cc @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -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::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> +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 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 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 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 ConvertTerminationReason( } // namespace absl::StatusOr GScipSolver::CreateSolveResultProto( - GScipResult gscip_result, - const ModelSolveParametersProto& model_parameters) { + GScipResult gscip_result, const ModelSolveParametersProto& model_parameters, + const std::optional 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> sorted_variables([&]() { std::vector sorted; @@ -660,7 +697,12 @@ absl::StatusOr 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 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 GScipSolver::Solve( std::make_unique(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 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; diff --git a/ortools/math_opt/solvers/gscip_solver.h b/ortools/math_opt/solvers/gscip_solver.h index 7fe37e0ae7..432faa63f0 100644 --- a/ortools/math_opt/solvers/gscip_solver.h +++ b/ortools/math_opt/solvers/gscip_solver.h @@ -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> MergeParameters( const SolveParametersProto& solve_parameters); private: @@ -112,7 +114,8 @@ class GScipSolver : public SolverInterface { absl::Span variable_ids); absl::StatusOr CreateSolveResultProto( GScipResult gscip_result, - const ModelSolveParametersProto& model_parameters); + const ModelSolveParametersProto& model_parameters, + std::optional cutoff); const std::unique_ptr gscip_; InterruptEventHandler interrupt_event_handler_; diff --git a/ortools/math_opt/solvers/gurobi/g_gurobi.cc b/ortools/math_opt/solvers/gurobi/g_gurobi.cc index f64e737524..a1070055dd 100644 --- a/ortools/math_opt/solvers/gurobi/g_gurobi.cc +++ b/ortools/math_opt/solvers/gurobi/g_gurobi.cc @@ -13,7 +13,9 @@ #include "ortools/math_opt/solvers/gurobi/g_gurobi.h" +#include #include +#include #include "ortools/base/cleanup.h" #include "absl/status/status.h" diff --git a/ortools/math_opt/solvers/gurobi_solver.cc b/ortools/math_opt/solvers/gurobi_solver.cc index 1534095b90..15277194fc 100644 --- a/ortools/math_opt/solvers/gurobi_solver.cc +++ b/ortools/math_opt/solvers/gurobi_solver.cc @@ -93,16 +93,6 @@ absl::StatusOr> GurobiFromInitArgs( } } -std::vector RepeatedPtrFieldToVec( - const google::protobuf::RepeatedPtrField& proto_string_vec) { - std::vector 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& 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 TruncateNames( + const google::protobuf::RepeatedPtrField& original_names) { + std::vector 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 g_gurobi) @@ -400,7 +439,7 @@ absl::StatusOr 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 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. const std::vector 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. const std::vector 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())); diff --git a/ortools/math_opt/solvers/pdlp_solver.cc b/ortools/math_opt/solvers/pdlp_solver.cc index 67c4b14f08..3390372a62 100644 --- a/ortools/math_opt/solvers/pdlp_solver.cc +++ b/ortools/math_opt/solvers/pdlp_solver.cc @@ -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"); } diff --git a/ortools/math_opt/validators/BUILD.bazel b/ortools/math_opt/validators/BUILD.bazel index 532bb482d6..069965c4f7 100644 --- a/ortools/math_opt/validators/BUILD.bazel +++ b/ortools/math_opt/validators/BUILD.bazel @@ -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", diff --git a/ortools/math_opt/validators/ids_validator.cc b/ortools/math_opt/validators/ids_validator.cc index 516e78e44d..293f4384f8 100644 --- a/ortools/math_opt/validators/ids_validator.cc +++ b/ortools/math_opt/validators/ids_validator.cc @@ -116,25 +116,21 @@ absl::Status CheckSortedIdsNotBad(const absl::Span ids, } } // namespace -absl::Status CheckIdsNonnegativeAndStrictlyIncreasing( - absl::Span ids) { +absl::Status CheckIdsRangeAndStrictlyIncreasing(absl::Span 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::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(); } diff --git a/ortools/math_opt/validators/ids_validator.h b/ortools/math_opt/validators/ids_validator.h index 06d2748821..40baeda9ff 100644 --- a/ortools/math_opt/validators/ids_validator.h +++ b/ortools/math_opt/validators/ids_validator.h @@ -24,8 +24,9 @@ namespace operations_research { namespace math_opt { -absl::Status CheckIdsNonnegativeAndStrictlyIncreasing( - absl::Span ids); +// Checks that the input ids are in [0, max(int64_t)) range and that their are +// strictly increasing. +absl::Status CheckIdsRangeAndStrictlyIncreasing(absl::Span ids); // Checks that the elements of ids are a subset of universe. // diff --git a/ortools/math_opt/validators/model_parameters_validator.cc b/ortools/math_opt/validators/model_parameters_validator.cc index db00f710b5..e2f357b666 100644 --- a/ortools/math_opt/validators/model_parameters_validator.cc +++ b/ortools/math_opt/validators/model_parameters_validator.cc @@ -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()) { diff --git a/ortools/math_opt/validators/model_validator.cc b/ortools/math_opt/validators/model_validator.cc index 0d16c66444..63398c0e14 100644 --- a/ortools/math_opt/validators/model_validator.cc +++ b/ortools/math_opt/validators/model_validator.cc @@ -46,7 +46,7 @@ constexpr double kInf = std::numeric_limits::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"; diff --git a/ortools/math_opt/validators/result_validator.cc b/ortools/math_opt/validators/result_validator.cc index ca5dfd1a90..74baf9b434 100644 --- a/ortools/math_opt/validators/result_validator.cc +++ b/ortools/math_opt/validators/result_validator.cc @@ -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(); diff --git a/ortools/math_opt/validators/solver_parameters_validator.cc b/ortools/math_opt/validators/solve_parameters_validator.cc similarity index 53% rename from ortools/math_opt/validators/solver_parameters_validator.cc rename to ortools/math_opt/validators/solve_parameters_validator.cc index c28d6f2b6d..36098199bc 100644 --- a/ortools/math_opt/validators/solver_parameters_validator.cc +++ b/ortools/math_opt/validators/solve_parameters_validator.cc @@ -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 - -#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(); } diff --git a/ortools/math_opt/validators/solver_parameters_validator.h b/ortools/math_opt/validators/solve_parameters_validator.h similarity index 69% rename from ortools/math_opt/validators/solver_parameters_validator.h rename to ortools/math_opt/validators/solve_parameters_validator.h index e992d8da39..d1e63e37fa 100644 --- a/ortools/math_opt/validators/solver_parameters_validator.h +++ b/ortools/math_opt/validators/solve_parameters_validator.h @@ -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_ diff --git a/ortools/math_opt/validators/sparse_vector_validator.h b/ortools/math_opt/validators/sparse_vector_validator.h index 568c583ce8..0e034aed06 100644 --- a/ortools/math_opt/validators/sparse_vector_validator.h +++ b/ortools/math_opt/validators/sparse_vector_validator.h @@ -49,7 +49,7 @@ template ::value> > absl::Status CheckIdsAndValues(const SparseVectorView& 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 & 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(); }