diff --git a/ortools/linear_solver/gurobi_proto_solver.cc b/ortools/linear_solver/gurobi_proto_solver.cc index faacbf35aa..b5e985a2c5 100644 --- a/ortools/linear_solver/gurobi_proto_solver.cc +++ b/ortools/linear_solver/gurobi_proto_solver.cc @@ -122,6 +122,67 @@ int AddQuadraticConstraint(const MPGeneralConstraintProto& gen_cst, return GRB_OK; } + +int AddAndConstraint(const MPGeneralConstraintProto& gen_cst, + GRBmodel* gurobi_model, std::vector* tmp_variables) { + CHECK(gurobi_model != nullptr); + CHECK(tmp_variables != nullptr); + + auto and_cst = gen_cst.and_constraint(); + return GRBaddgenconstrAnd( + gurobi_model, + /*name=*/gen_cst.name().c_str(), + /*resvar=*/and_cst.resultant_var_index(), + /*nvars=*/and_cst.var_index_size(), + /*vars=*/and_cst.mutable_var_index()->mutable_data()); +} + +int AddOrConstraint(const MPGeneralConstraintProto& gen_cst, + GRBmodel* gurobi_model, std::vector* tmp_variables) { + CHECK(gurobi_model != nullptr); + CHECK(tmp_variables != nullptr); + + auto or_cst = gen_cst.or_constraint(); + return GRBaddgenconstrOr(gurobi_model, + /*name=*/gen_cst.name().c_str(), + /*resvar=*/or_cst.resultant_var_index(), + /*nvars=*/or_cst.var_index_size(), + /*vars=*/or_cst.mutable_var_index()->mutable_data()); +} + +int AddMinConstraint(const MPGeneralConstraintProto& gen_cst, + GRBmodel* gurobi_model, std::vector* tmp_variables) { + CHECK(gurobi_model != nullptr); + CHECK(tmp_variables != nullptr); + + auto min_cst = gen_cst.min_constraint(); + return GRBaddgenconstrMin( + gurobi_model, + /*name=*/gen_cst.name().c_str(), + /*resvar=*/min_cst.resultant_var_index(), + /*nvars=*/min_cst.var_index_size(), + /*vars=*/min_cst.mutable_var_index()->mutable_data(), + /*constant=*/min_cst.has_constant() + ? min_cst.constant() + : std::numeric_limits::infinity()); +} + +int AddMaxConstraint(const MPGeneralConstraintProto& gen_cst, + GRBmodel* gurobi_model, std::vector* tmp_variables) { + CHECK(gurobi_model != nullptr); + CHECK(tmp_variables != nullptr); + + auto max_cst = gen_cst.max_constraint(); + return GRBaddgenconstrMax( + gurobi_model, + /*name=*/gen_cst.name().c_str(), + /*resvar=*/max_cst.resultant_var_index(), + /*nvars=*/max_cst.var_index_size(), + /*vars=*/max_cst.mutable_var_index()->mutable_data(), + /*constant=*/max_cst.has_constant() + ? max_cst.constant() + : -std::numeric_limits::infinity()); +} } // namespace util::StatusOr GurobiSolveProto( @@ -238,6 +299,34 @@ util::StatusOr GurobiSolveProto( RETURN_IF_GUROBI_ERROR(AddQuadraticConstraint(gen_cst, gurobi_model)); break; } + case MPGeneralConstraintProto::kAbsConstraint: { + RETURN_IF_GUROBI_ERROR(GRBaddgenconstrAbs( + gurobi_model, + /*name=*/gen_cst.name().c_str(), + /*resvar=*/gen_cst.abs_constraint().resultant_var_index(), + /*argvar=*/gen_cst.abs_constraint().var_index())); + break; + } + case MPGeneralConstraintProto::kAndConstraint: { + RETURN_IF_GUROBI_ERROR( + AddAndConstraint(gen_cst, gurobi_model, &ct_variables)); + break; + } + case MPGeneralConstraintProto::kOrConstraint: { + RETURN_IF_GUROBI_ERROR( + AddOrConstraint(gen_cst, gurobi_model, &ct_variables)); + break; + } + case MPGeneralConstraintProto::kMinConstraint: { + RETURN_IF_GUROBI_ERROR( + AddMinConstraint(gen_cst, gurobi_model, &ct_variables)); + break; + } + case MPGeneralConstraintProto::kMaxConstraint: { + RETURN_IF_GUROBI_ERROR( + AddMaxConstraint(gen_cst, gurobi_model, &ct_variables)); + break; + } default: return util::UnimplementedError( absl::StrFormat("General constraints of type %i not supported.", diff --git a/ortools/linear_solver/linear_solver.cc b/ortools/linear_solver/linear_solver.cc index 043e605ccd..b323edd07a 100644 --- a/ortools/linear_solver/linear_solver.cc +++ b/ortools/linear_solver/linear_solver.cc @@ -695,9 +695,11 @@ MPSolverResponseStatus MPSolver::LoadModelFromProtoInternal( break; } default: - *error_message = - absl::StrCat("Solver doesn't support general constraints of type ", - general_constraint.general_constraint_case()); + *error_message = absl::StrFormat( + "Optimizing general constraints of type %i is only supported " + "through direct proto solves. Please use MPSolver::SolveWithProto, " + "or the solver's direct proto solve function.", + general_constraint.general_constraint_case()); return MPSOLVER_MODEL_INVALID; } } diff --git a/ortools/linear_solver/linear_solver.proto b/ortools/linear_solver/linear_solver.proto index 8a93c37e26..186fbfc2f1 100644 --- a/ortools/linear_solver/linear_solver.proto +++ b/ortools/linear_solver/linear_solver.proto @@ -111,8 +111,16 @@ message MPGeneralConstraintProto { MPSosConstraint sos_constraint = 3; MPQuadraticConstraint quadratic_constraint = 4; MPAbsConstraint abs_constraint = 5; - MPAndConstraint and_constraint = 6; - MPOrConstraint or_constraint = 7; + // All variables in "and" constraints must be Boolean. + // resultant_var = and(var_1, var_2... var_n) + MPArrayConstraint and_constraint = 6; + // All variables in "or" constraints must be Boolean. + // resultant_var = or(var_1, var_2... var_n) + MPArrayConstraint or_constraint = 7; + // resultant_var = min(var_1, var_2, ..., constant) + MPArrayWithConstantConstraint min_constraint = 8; + // resultant_var = max(var_1, var_2, ..., constant) + MPArrayWithConstantConstraint max_constraint = 9; } } @@ -142,7 +150,7 @@ message MPSosConstraint { enum Type { // At most one variable in `var_index` must be non-zero. SOS1_DEFAULT = 0; - // At most two consecutive variables from `var_index` must be non-zero (i.e. + // At most two consecutive variables from `var_index` can be non-zero (i.e. // for some i, var_index[i] and var_index[i+1]). See // http://www.eudoxus.com/lp-training/5/5-6-special-ordered-sets-of-type-2 SOS2 = 1; @@ -205,22 +213,21 @@ message MPAbsConstraint { optional int32 resultant_var_index = 2; } -// Sets a binary variable's value equal to one if and only if all variables in a -// set of binary variables are true. -message MPAndConstraint { +// Sets a variable's value equal to a function on a set of variables. +message MPArrayConstraint { // Variable indices are relative to the "variable" field in MPModelProto. - // resultant_var = and(var_1, var_2... var_n) repeated int32 var_index = 1; optional int32 resultant_var_index = 2; } -// Sets a binary variable's value equal to one if and only if at least one -// variable in a set of binary variables is true. -message MPOrConstraint { +// Sets a variable's value equal to a function on a set of variables and, +// optionally, a constant. +message MPArrayWithConstantConstraint { // Variable indices are relative to the "variable" field in MPModelProto. - // resultant_var = or(var_1, var_2... var_n) + // resultant_var = f(var_1, var_2, ..., constant) repeated int32 var_index = 1; - optional int32 resultant_var_index = 2; + optional double constant = 2; + optional int32 resultant_var_index = 3; } // Quadratic part of a model's objective. Added with other objectives (such as diff --git a/ortools/linear_solver/model_validator.cc b/ortools/linear_solver/model_validator.cc index 4fe097250e..c90c0fb897 100644 --- a/ortools/linear_solver/model_validator.cc +++ b/ortools/linear_solver/model_validator.cc @@ -263,9 +263,8 @@ std::string FindErrorInMPAbsConstraint(const MPModelProto& model, return ""; } -template std::string FindErrorInMPAndOrConstraint(const MPModelProto& model, - const MPAndOrConstraint& and_or) { + const MPArrayConstraint& and_or) { if (and_or.var_index_size() == 0) { return "var_index cannot be empty."; } @@ -294,6 +293,34 @@ std::string FindErrorInMPAndOrConstraint(const MPModelProto& model, return ""; } +std::string FindErrorInMPMinMaxConstraint( + const MPModelProto& model, const MPArrayWithConstantConstraint& min_max) { + if (min_max.var_index_size() == 0) { + return "var_index cannot be empty."; + } + if (!min_max.has_resultant_var_index()) { + return "resultant_var_index is required."; + } + + if (!std::isfinite(min_max.constant())) { + return absl::StrCat("Invalid constant: ", (min_max.constant())); + } + + const int num_vars = model.variable_size(); + for (int i = 0; i < min_max.var_index_size(); ++i) { + if (min_max.var_index(i) < 0 || min_max.var_index(i) >= num_vars) { + return absl::StrCat("var_index(", i, ")=", min_max.var_index(i), + " is invalid.", " It must be in [0, ", num_vars, ")"); + } + } + if (min_max.resultant_var_index() < 0 || + min_max.resultant_var_index() >= num_vars) { + return absl::StrCat("resultant_var_index=", min_max.resultant_var_index(), + " is invalid.", " It must be in [0, ", num_vars, ")"); + } + return ""; +} + std::string FindErrorInQuadraticObjective(const MPQuadraticObjective& qobj, int num_vars) { if (qobj.qvar1_index_size() != qobj.qvar2_index_size() || @@ -417,6 +444,15 @@ std::string FindErrorInMPModelProto(const MPModelProto& model) { FindErrorInMPAndOrConstraint(model, gen_constraint.or_constraint()); break; + case MPGeneralConstraintProto::kMinConstraint: + error = FindErrorInMPMinMaxConstraint(model, + gen_constraint.min_constraint()); + break; + + case MPGeneralConstraintProto::kMaxConstraint: + error = FindErrorInMPMinMaxConstraint(model, + gen_constraint.max_constraint()); + break; default: return absl::StrCat("Unknown general constraint type ", gen_constraint.general_constraint_case()); diff --git a/ortools/linear_solver/scip_proto_solver.cc b/ortools/linear_solver/scip_proto_solver.cc index 121b0ed34c..31f567f779 100644 --- a/ortools/linear_solver/scip_proto_solver.cc +++ b/ortools/linear_solver/scip_proto_solver.cc @@ -332,10 +332,9 @@ util::Status AddQuadraticConstraint( // y = x OR y = -x util::Status AddAbsConstraint(const MPGeneralConstraintProto& gen_cst, const std::vector& scip_variables, - SCIP* scip, - std::vector* scip_constraints) { + SCIP* scip, SCIP_CONS** scip_cst) { CHECK(scip != nullptr); - CHECK(scip_constraints != nullptr); + CHECK(scip_cst != nullptr); CHECK(gen_cst.has_abs_constraint()); const auto& abs = gen_cst.abs_constraint(); SCIP_VAR* scip_var = scip_variables[abs.var_index()]; @@ -351,15 +350,16 @@ util::Status AddAbsConstraint(const MPGeneralConstraintProto& gen_cst, std::vector cons; auto add_abs_constraint = [&](const std::string& name_prefix) -> util::Status { - scip_constraints->push_back(nullptr); + SCIP_CONS* scip_cons; const std::string name = gen_cst.has_name() ? absl::StrCat(gen_cst.name(), name_prefix) : ""; RETURN_IF_SCIP_ERROR(SCIPcreateConsBasicLinear( - scip, /*cons=*/&scip_constraints->back(), + scip, /*cons=*/&scip_cons, /*name=*/name.c_str(), /*nvars=*/2, /*vars=*/vars.data(), /*vals=*/vals.data(), /*lhs=*/0.0, /*rhs=*/0.0)); // Note that the constraints are, by design, not added into the model using // SCIPaddCons. + cons.push_back(scip_cons); return util::OkStatus(); }; @@ -367,20 +367,18 @@ util::Status AddAbsConstraint(const MPGeneralConstraintProto& gen_cst, vars = {scip_resultant_var, scip_var}; vals = {1, 1}; RETURN_IF_ERROR(add_abs_constraint("_neg")); - cons.push_back(scip_constraints->back()); // Create an intermediary constraint such that y = x vals = {1, -1}; RETURN_IF_ERROR(add_abs_constraint("_pos")); - cons.push_back(scip_constraints->back()); // Activate at least one of the two above constraints. const std::string name = gen_cst.has_name() ? absl::StrCat(gen_cst.name(), "_disj") : ""; RETURN_IF_SCIP_ERROR(SCIPcreateConsBasicDisjunction( - scip, /*cons=*/&scip_constraints->back(), /*name=*/name.c_str(), + scip, /*cons=*/scip_cst, /*name=*/name.c_str(), /*nconss=*/2, /*conss=*/cons.data(), /*relaxcons=*/nullptr)); - RETURN_IF_SCIP_ERROR(SCIPaddCons(scip, scip_constraints->back())); + RETURN_IF_SCIP_ERROR(SCIPaddCons(scip, *scip_cst)); return util::OkStatus(); } @@ -433,6 +431,96 @@ util::Status AddOrConstraint(const MPGeneralConstraintProto& gen_cst, return util::OkStatus(); } +// Models the constraint y = min(x1, x2, ... xn, c) with c being a constant with +// - n + 1 constraints to ensure y <= min(x1, x2, ... xn, c) +// - one disjunction constraint among all of the possible y = x1, y = x2, ... +// y = xn, y = c constraints +// Does the equivalent thing for max (with y >= max(...) instead). +util::Status AddMinMaxConstraint(const MPGeneralConstraintProto& gen_cst, + const std::vector& scip_variables, + SCIP* scip, SCIP_CONS** scip_cst, + std::vector* scip_constraints, + std::vector* tmp_variables) { + CHECK(scip != nullptr); + CHECK(scip_cst != nullptr); + CHECK(tmp_variables != nullptr); + CHECK(gen_cst.has_min_constraint() || gen_cst.has_max_constraint()); + const auto& minmax = gen_cst.has_min_constraint() ? gen_cst.min_constraint() + : gen_cst.max_constraint(); + SCIP_VAR* scip_resultant_var = scip_variables[minmax.resultant_var_index()]; + + std::vector vars; + std::vector vals; + std::vector cons; + auto add_lin_constraint = [&](const std::string& name_prefix, + double lower_bound = 0.0, + double upper_bound = 0.0) -> util::Status { + SCIP_CONS* scip_cons; + const std::string name = + gen_cst.has_name() ? absl::StrCat(gen_cst.name(), name_prefix) : ""; + RETURN_IF_SCIP_ERROR(SCIPcreateConsBasicLinear( + scip, /*cons=*/&scip_cons, + /*name=*/name.c_str(), /*nvars=*/2, /*vars=*/vars.data(), + /*vals=*/vals.data(), /*lhs=*/lower_bound, /*rhs=*/upper_bound)); + // Note that the constraints are, by design, not added into the model using + // SCIPaddCons. + cons.push_back(scip_cons); + return util::OkStatus(); + }; + + // Create intermediary constraints such that y = xi + for (const int var_index : minmax.var_index()) { + vars = {scip_resultant_var, scip_variables[var_index]}; + vals = {1, -1}; + RETURN_IF_ERROR(add_lin_constraint(absl::StrCat("_", var_index))); + } + + // Create an intermediary constraint such that y = c + if (minmax.has_constant()) { + vars = {scip_resultant_var}; + vals = {1}; + RETURN_IF_ERROR( + add_lin_constraint("_constant", minmax.constant(), minmax.constant())); + } + + // Activate at least one of the above constraints. + const std::string name = + gen_cst.has_name() ? absl::StrCat(gen_cst.name(), "_disj") : ""; + RETURN_IF_SCIP_ERROR(SCIPcreateConsBasicDisjunction( + scip, /*cons=*/scip_cst, /*name=*/name.c_str(), + /*nconss=*/2, /*conss=*/cons.data(), /*relaxcons=*/nullptr)); + RETURN_IF_SCIP_ERROR(SCIPaddCons(scip, *scip_cst)); + + // Add all of the inequality constraints. + constexpr double kInfinity = std::numeric_limits::infinity(); + cons.clear(); + for (const int var_index : minmax.var_index()) { + vars = {scip_resultant_var, scip_variables[var_index]}; + vals = {1, -1}; + if (gen_cst.has_min_constraint()) { + RETURN_IF_ERROR(add_lin_constraint(absl::StrCat("_ineq_", var_index), + -kInfinity, 0.0)); + } else { + RETURN_IF_ERROR(add_lin_constraint(absl::StrCat("_ineq_", var_index), 0.0, + kInfinity)); + } + } + vars = {scip_resultant_var}; + vals = {1}; + if (gen_cst.has_min_constraint()) { + RETURN_IF_ERROR(add_lin_constraint(absl::StrCat("_ineq_constant"), + -kInfinity, minmax.constant())); + } else { + RETURN_IF_ERROR(add_lin_constraint(absl::StrCat("_ineq_constant"), + minmax.constant(), kInfinity)); + } + for (SCIP_CONS* scip_cons : cons) { + scip_constraints->push_back(scip_cons); + RETURN_IF_SCIP_ERROR(SCIPaddCons(scip, scip_cons)); + } + return util::OkStatus(); +} + util::Status AddQuadraticObjective(const MPQuadraticObjective& quadobj, SCIP* scip, std::vector* scip_variables, @@ -689,7 +777,7 @@ util::StatusOr ScipSolveProto( } case MPGeneralConstraintProto::kAbsConstraint: { RETURN_IF_ERROR(AddAbsConstraint(gen_cst, scip_variables, scip, - &scip_constraints)); + &scip_constraints[lincst_size + c])); break; } case MPGeneralConstraintProto::kAndConstraint: { @@ -704,6 +792,13 @@ util::StatusOr ScipSolveProto( &ct_variables)); break; } + case MPGeneralConstraintProto::kMinConstraint: + case MPGeneralConstraintProto::kMaxConstraint: { + RETURN_IF_ERROR(AddMinMaxConstraint( + gen_cst, scip_variables, scip, &scip_constraints[lincst_size + c], + &scip_constraints, &ct_variables)); + break; + } default: return util::UnimplementedError( absl::StrFormat("General constraints of type %i not supported.",