From 738d23fe4aa69785a7ac281cab48287b479144ef Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Mon, 22 Jul 2019 11:10:30 -0700 Subject: [PATCH] direct gurobi proto solve; support SOS in gurobi proto solve --- ortools/linear_solver/gurobi_proto_solver.cc | 80 +++++++++++++++----- ortools/linear_solver/java/linear_solver.i | 1 + ortools/linear_solver/linear_expr.cc | 64 ++++++++++++++++ ortools/linear_solver/linear_expr.h | 12 +++ ortools/linear_solver/linear_solver.cc | 20 ++--- ortools/linear_solver/linear_solver.proto | 3 +- 6 files changed, 151 insertions(+), 29 deletions(-) diff --git a/ortools/linear_solver/gurobi_proto_solver.cc b/ortools/linear_solver/gurobi_proto_solver.cc index 3030225e85..9b8ba4a43d 100644 --- a/ortools/linear_solver/gurobi_proto_solver.cc +++ b/ortools/linear_solver/gurobi_proto_solver.cc @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -63,6 +64,39 @@ util::Status CreateEnvironment(GRBenv** gurobi) { } return util::OkStatus(); } + +int AddSosConstraint(const MPSosConstraint& sos_cst, GRBmodel* gurobi_model, + std::vector* tmp_variables, + std::vector* tmp_weights) { + CHECK(gurobi_model != nullptr); + CHECK(tmp_variables != nullptr); + CHECK(tmp_weights != nullptr); + + tmp_variables->resize(sos_cst.var_index_size(), 0); + for (int v = 0; v < sos_cst.var_index_size(); ++v) { + (*tmp_variables)[v] = sos_cst.var_index(v); + } + tmp_weights->resize(sos_cst.var_index_size(), 0); + if (sos_cst.weight_size() == sos_cst.var_index_size()) { + for (int w = 0; w < sos_cst.weight_size(); ++w) { + (*tmp_weights)[w] = sos_cst.weight(w); + } + } else { + DCHECK_EQ(sos_cst.weight_size(), 0); + // Gurobi requires variable weights in their SOS constraints. + std::iota(tmp_weights->begin(), tmp_weights->end(), 1); + } + + std::vector types = {sos_cst.type() == MPSosConstraint::SOS1_DEFAULT + ? GRB_SOS_TYPE1 + : GRB_SOS_TYPE2}; + std::vector begins = {0}; + return GRBaddsos(gurobi_model, /*numsos=*/1, + /*nummembers=*/sos_cst.var_index_size(), + /*types=*/types.data(), + /*beg=*/begins.data(), /*ind=*/tmp_variables->data(), + /*weight*/ tmp_weights->data()); +} } // namespace util::StatusOr GurobiSolveProto( @@ -79,11 +113,6 @@ util::StatusOr GurobiSolveProto( return util::UnimplementedError( "Solver-specific parameters not supported."); } - if (model.general_constraint_size() > 0) { - // TODO(user): Move indicator constraint logic from linear_solver.cc to - // this file. - return util::UnimplementedError("General constraints not supported."); - } if (model.has_solution_hint()) { // TODO(user): Support solution hints. return util::UnimplementedError("Solution hint not supported."); @@ -169,6 +198,23 @@ util::StatusOr GurobiSolveProto( /*upper=*/constraint.upper_bound(), /*constrname=*/constraint.name().c_str())); } + + for (const auto& gen_cst : model.general_constraint()) { + switch (gen_cst.general_constraint_case()) { + // TODO(user): Move indicator constraint logic from + // linear_solver.cc to this file. + case MPGeneralConstraintProto::kSosConstraint: { + RETURN_IF_GUROBI_ERROR(AddSosConstraint(gen_cst.sos_constraint(), + gurobi_model, &ct_variables, + &ct_coefficients)); + break; + } + default: + return util::UnimplementedError( + absl::StrFormat("General constraints of type %i not supported.", + gen_cst.general_constraint_case())); + } + } } RETURN_IF_GUROBI_ERROR(GRBsetintattr(gurobi_model, GRB_INT_ATTR_MODELSENSE, @@ -230,25 +276,23 @@ util::StatusOr GurobiSolveProto( RETURN_IF_GUROBI_ERROR( GRBgetdblattr(gurobi_model, GRB_DBL_ATTR_OBJVAL, &objective_value)); response.set_objective_value(objective_value); - if (has_integer_variables) { - double best_objective_bound = 0; - const int error = GRBgetdblattr(gurobi_model, GRB_DBL_ATTR_OBJBOUND, - &best_objective_bound); - if (response.status() == MPSOLVER_OPTIMAL && - error == GRB_ERROR_DATA_NOT_AVAILABLE) { - // If the presolve deletes all variables, there's no best bound. - response.set_best_objective_bound(objective_value); - } else { - RETURN_IF_GUROBI_ERROR(error); - response.set_best_objective_bound(best_objective_bound); - } + double best_objective_bound = 0; + const int error = GRBgetdblattr(gurobi_model, GRB_DBL_ATTR_OBJBOUND, + &best_objective_bound); + if (response.status() == MPSOLVER_OPTIMAL && + error == GRB_ERROR_DATA_NOT_AVAILABLE) { + // If the presolve deletes all variables, there's no best bound. + response.set_best_objective_bound(objective_value); + } else { + RETURN_IF_GUROBI_ERROR(error); + response.set_best_objective_bound(best_objective_bound); } response.mutable_variable_value()->Resize(variable_size, 0); RETURN_IF_GUROBI_ERROR( GRBgetdblattrarray(gurobi_model, GRB_DBL_ATTR_X, 0, variable_size, response.mutable_variable_value()->mutable_data())); - if (!has_integer_variables) { + if (!has_integer_variables && model.general_constraint_size() == 0) { response.mutable_dual_value()->Resize(model.constraint_size(), 0); RETURN_IF_GUROBI_ERROR(GRBgetdblattrarray( gurobi_model, GRB_DBL_ATTR_PI, 0, model.constraint_size(), diff --git a/ortools/linear_solver/java/linear_solver.i b/ortools/linear_solver/java/linear_solver.i index 38d7d0d930..58733b9471 100644 --- a/ortools/linear_solver/java/linear_solver.i +++ b/ortools/linear_solver/java/linear_solver.i @@ -375,6 +375,7 @@ PROTO2_RETURN( %unignore operations_research::MPSolver::AT_UPPER_BOUND; // no test %unignore operations_research::MPSolver::FIXED_VALUE; // no test %unignore operations_research::MPSolver::BASIC; +%unignore operations_research::MPSolver::SetStartingLpBasis; // MPVariable: writer API. %rename (setInteger) operations_research::MPVariable::SetInteger; diff --git a/ortools/linear_solver/linear_expr.cc b/ortools/linear_solver/linear_expr.cc index 3c9f14a7b2..bfbcc96c77 100644 --- a/ortools/linear_solver/linear_expr.cc +++ b/ortools/linear_solver/linear_expr.cc @@ -15,6 +15,7 @@ #include +#include "absl/strings/str_join.h" #include "ortools/base/logging.h" #include "ortools/linear_solver/linear_solver.h" @@ -79,6 +80,69 @@ double LinearExpr::SolutionValue() const { return solution; } +namespace { + +void AppendTerm(const double coef, const std::string& var_name, + const bool is_first, std::string* s) { + if (is_first) { + if (coef == 1.0) { + absl::StrAppend(s, var_name); + } else if (coef == -1.0) { + absl::StrAppend(s, "-", var_name); + } else { + absl::StrAppend(s, coef, "*", var_name); + } + } else { + const std::string op = coef < 0 ? "-" : "+"; + const double abs_coef = std::abs(coef); + if (abs_coef == 1.0) { + absl::StrAppend(s, " ", op, " ", var_name); + } else { + absl::StrAppend(s, " ", op, " ", abs_coef, "*", var_name); + } + } +} + +void AppendOffset(const double offset, const bool is_first, std::string* s) { + if (is_first) { + absl::StrAppend(s, offset); + } else { + if (offset != 0.0) { + const std::string op = offset < 0 ? "-" : "+"; + absl::StrAppend(s, " ", op, " ", std::abs(offset)); + } + } +} + +} // namespace + +std::string LinearExpr::ToString() const { + std::vector vars_in_order; + for (const auto& var_val_pair : terms_) { + vars_in_order.push_back(var_val_pair.first); + } + std::sort(vars_in_order.begin(), vars_in_order.end(), + [](const MPVariable* v, const MPVariable* u) { + return v->index() < u->index(); + }); + std::string result; + bool is_first = true; + for (const MPVariable* var : vars_in_order) { + // MPSolver gives names to all variables, even if you don't. + DCHECK(!var->name().empty()); + AppendTerm(terms_.at(var), var->name(), is_first, &result); + is_first = false; + } + AppendOffset(offset_, is_first, &result); + // TODO(user): support optionally cropping long strings. + return result; +} + +std::ostream& operator<<(std::ostream& stream, const LinearExpr& linear_expr) { + stream << linear_expr.ToString(); + return stream; +} + LinearExpr operator+(LinearExpr lhs, const LinearExpr& rhs) { lhs += rhs; return lhs; diff --git a/ortools/linear_solver/linear_expr.h b/ortools/linear_solver/linear_expr.h index ea73648443..cc68fb4a60 100644 --- a/ortools/linear_solver/linear_expr.h +++ b/ortools/linear_solver/linear_expr.h @@ -75,6 +75,10 @@ * * \code LinearExpr e1 = LinearExpr(x) + (y + 5); \endcode * * \code LinearExpr e1 = y + 5 + LinearExpr(x); \endcode */ + +#include +#include + #include "absl/container/flat_hash_map.h" namespace operations_research { @@ -147,11 +151,19 @@ class LinearExpr { */ double SolutionValue() const; + /** + * A human readable representation of this. Variables will be printed in order + * of lowest index first. + */ + std::string ToString() const; + private: double offset_; absl::flat_hash_map terms_; }; +std::ostream& operator<<(std::ostream& stream, const LinearExpr& linear_expr); + // NOTE(user): in the ops below, the non-"const LinearExpr&" are intentional. // We need to create a new LinearExpr for the result, so we lose nothing by // passing one argument by value, mutating it, and then returning it. In diff --git a/ortools/linear_solver/linear_solver.cc b/ortools/linear_solver/linear_solver.cc index 6c6476d307..8b77183bf8 100644 --- a/ortools/linear_solver/linear_solver.cc +++ b/ortools/linear_solver/linear_solver.cc @@ -474,31 +474,31 @@ const #else constexpr #endif -NamedOptimizationProblemType kOptimizationProblemTypeNames[] = { - {MPSolver::GLOP_LINEAR_PROGRAMMING, "glop"}, + NamedOptimizationProblemType kOptimizationProblemTypeNames[] = { + {MPSolver::GLOP_LINEAR_PROGRAMMING, "glop"}, #if defined(USE_GLPK) - {MPSolver::GLPK_LINEAR_PROGRAMMING, "glpk_lp"}, + {MPSolver::GLPK_LINEAR_PROGRAMMING, "glpk_lp"}, #endif #if defined(USE_CLP) - {MPSolver::CLP_LINEAR_PROGRAMMING, "clp"}, + {MPSolver::CLP_LINEAR_PROGRAMMING, "clp"}, #endif #if defined(USE_GUROBI) - {MPSolver::GUROBI_LINEAR_PROGRAMMING, "gurobi_lp"}, + {MPSolver::GUROBI_LINEAR_PROGRAMMING, "gurobi_lp"}, #endif #if defined(USE_SCIP) - {MPSolver::SCIP_MIXED_INTEGER_PROGRAMMING, "scip"}, + {MPSolver::SCIP_MIXED_INTEGER_PROGRAMMING, "scip"}, #endif #if defined(USE_CBC) - {MPSolver::CBC_MIXED_INTEGER_PROGRAMMING, "cbc"}, + {MPSolver::CBC_MIXED_INTEGER_PROGRAMMING, "cbc"}, #endif #if defined(USE_GLPK) - {MPSolver::GLPK_MIXED_INTEGER_PROGRAMMING, "glpk_mip"}, + {MPSolver::GLPK_MIXED_INTEGER_PROGRAMMING, "glpk_mip"}, #endif #if defined(USE_BOP) - {MPSolver::BOP_INTEGER_PROGRAMMING, "bop"}, + {MPSolver::BOP_INTEGER_PROGRAMMING, "bop"}, #endif #if defined(USE_GUROBI) - {MPSolver::GUROBI_MIXED_INTEGER_PROGRAMMING, "gurobi_mip"}, + {MPSolver::GUROBI_MIXED_INTEGER_PROGRAMMING, "gurobi_mip"}, #endif }; diff --git a/ortools/linear_solver/linear_solver.proto b/ortools/linear_solver/linear_solver.proto index 5c31f106c7..f94c84fbdb 100644 --- a/ortools/linear_solver/linear_solver.proto +++ b/ortools/linear_solver/linear_solver.proto @@ -150,7 +150,8 @@ message MPSosConstraint { repeated int32 var_index = 2; // Optional: SOS weights. If non-empty, must be of the same size as - // "var_index", and strictly increasing. + // "var_index", and strictly increasing. If empty and required by the + // underlying solver, the 1..n sequence will be given as weights. // SUBTLE: The weights can help the solver make branch-and-bound decisions // that fit the underlying optimization model: after each LP relaxation, it // will compute the "average weight" of the SOS variables, weighted by value