diff --git a/ortools/linear_solver/BUILD.bazel b/ortools/linear_solver/BUILD.bazel index eba622ca9a..839b542e22 100644 --- a/ortools/linear_solver/BUILD.bazel +++ b/ortools/linear_solver/BUILD.bazel @@ -84,7 +84,7 @@ config_setting( bool_flag( name = "with_highs", - build_setting_default = True, + build_setting_default = False, ) config_setting( @@ -267,7 +267,12 @@ cc_library( "//ortools/base:stl_util", "//ortools/base:timer", "//ortools/gurobi:environment", - "//ortools/linear_solver/proto_solver", + "//ortools/gurobi:gurobi_util", + "//ortools/linear_solver/proto_solver:glop_proto_solver", + "//ortools/linear_solver/proto_solver:gurobi_proto_solver", + "//ortools/linear_solver/proto_solver:pdlp_proto_solver", + "//ortools/linear_solver/proto_solver:sat_proto_solver", + "//ortools/linear_solver/proto_solver:scip_proto_solver", "//ortools/port:file", "//ortools/port:proto_utils", "//ortools/sat:cp_model_cc_proto", diff --git a/ortools/linear_solver/glop_interface.cc b/ortools/linear_solver/glop_interface.cc index 0824846702..ba2545d28b 100644 --- a/ortools/linear_solver/glop_interface.cc +++ b/ortools/linear_solver/glop_interface.cc @@ -20,6 +20,7 @@ #include #include "absl/base/attributes.h" +#include "absl/log/check.h" #include "ortools/base/logging.h" #include "ortools/glop/lp_solver.h" #include "ortools/glop/parameters.pb.h" @@ -145,7 +146,7 @@ MPSolver::ResultStatus GLOPInterface::Solve(const MPSolverParameters& param) { result_status_ = GlopToMPSolverResultStatus(status); objective_value_ = lp_solver_.GetObjectiveValue(); - const size_t num_vars = solver_->variables_.size(); + const int num_vars = solver_->variables_.size(); column_status_.resize(num_vars, MPSolver::FREE); for (int var_id = 0; var_id < num_vars; ++var_id) { MPVariable* const var = solver_->variables_[var_id]; @@ -164,7 +165,7 @@ MPSolver::ResultStatus GLOPInterface::Solve(const MPSolverParameters& param) { column_status_.at(var_id) = GlopToMPSolverVariableStatus(variable_status); } - const size_t num_constraints = solver_->constraints_.size(); + const int num_constraints = solver_->constraints_.size(); row_status_.resize(num_constraints, MPSolver::FREE); for (int ct_id = 0; ct_id < num_constraints; ++ct_id) { MPConstraint* const ct = solver_->constraints_[ct_id]; diff --git a/ortools/linear_solver/gurobi_interface.cc b/ortools/linear_solver/gurobi_interface.cc index 5a76943e84..b516b4a2bb 100644 --- a/ortools/linear_solver/gurobi_interface.cc +++ b/ortools/linear_solver/gurobi_interface.cc @@ -47,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -65,6 +66,7 @@ #include "ortools/base/map_util.h" #include "ortools/base/timer.h" #include "ortools/gurobi/environment.h" +#include "ortools/gurobi/gurobi_util.h" #include "ortools/linear_solver/linear_solver.h" #include "ortools/linear_solver/linear_solver_callback.h" #include "ortools/linear_solver/proto_solver/gurobi_proto_solver.h" @@ -1222,6 +1224,12 @@ MPSolver::ResultStatus GurobiInterface::Solve(const MPSolverParameters& param) { CheckedGurobiCall(GRBsetintparam( GRBgetenv(model_), GRB_INT_PAR_LAZYCONSTRAINTS, gurobi_lazy_constraint)); + // Logs all parameters not at default values in the model environment. + if (!quiet()) { + std::cout << GurobiParamInfoForLogging(GRBgetenv(model_), + /*one_liner_output=*/true); + } + // Solve timer.Restart(); const int status = GRBoptimize(model_); diff --git a/ortools/linear_solver/proto_solver/BUILD.bazel b/ortools/linear_solver/proto_solver/BUILD.bazel index 2eba856333..916039e42e 100644 --- a/ortools/linear_solver/proto_solver/BUILD.bazel +++ b/ortools/linear_solver/proto_solver/BUILD.bazel @@ -13,64 +13,159 @@ package(default_visibility = ["//visibility:public"]) -# This works on a fixed set of solvers. -# By default SCIP, GUROBI, PDLP, and CP-SAT interface are included. cc_library( - name = "proto_solver", - srcs = [ - "gurobi_proto_solver.cc", - "highs_proto_solver.cc", - "pdlp_proto_solver.cc", - "sat_proto_solver.cc", - "sat_solver_utils.cc", - "scip_proto_solver.cc", - ], - hdrs = [ - "gurobi_proto_solver.h", - "highs_proto_solver.h", - "pdlp_proto_solver.h", - "sat_proto_solver.h", - "sat_solver_utils.h", - "scip_proto_solver.h", - ], - copts = [ - "-DUSE_PDLP", - "-DUSE_SCIP", - "-DUSE_HIGHS", - ], + name = "proto_utils", + hdrs = ["proto_utils.h"], + visibility = ["//visibility:public"], deps = [ - "//ortools/base", - "//ortools/base:accurate_sum", - "//ortools/base:dynamic_library", - "//ortools/base:hash", - "//ortools/base:map_util", - "//ortools/base:status_macros", - "//ortools/base:stl_util", - "//ortools/base:timer", - "//ortools/bop:bop_parameters_cc_proto", - "//ortools/bop:integral_solver", + "//ortools/port:proto_utils", + "@com_google_absl//absl/log:check", + "@com_google_protobuf//:protobuf", + ], +) + +cc_library( + name = "glop_proto_solver", + srcs = ["glop_proto_solver.cc"], + hdrs = ["glop_proto_solver.h"], + deps = [ + ":proto_utils", "//ortools/glop:lp_solver", "//ortools/glop:parameters_cc_proto", - "//ortools/gscip:legacy_scip_params", - "//ortools/gurobi:environment", + "//ortools/glop:parameters_validation", "//ortools/linear_solver:linear_solver_cc_proto", - "//ortools/linear_solver:model_exporter", "//ortools/linear_solver:model_validator", - "//ortools/linear_solver:scip_with_glop", + "//ortools/lp_data", + "//ortools/lp_data:base", + "//ortools/lp_data:proto_utils", + "//ortools/port:proto_utils", + "//ortools/util:logging", + "//ortools/util:time_limit", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/strings", + ], +) + +cc_library( + name = "pdlp_proto_solver", + srcs = ["pdlp_proto_solver.cc"], + hdrs = ["pdlp_proto_solver.h"], + deps = [ + "//ortools/base:logging", + "//ortools/linear_solver:linear_solver_cc_proto", + "//ortools/linear_solver:model_validator", + "//ortools/pdlp:iteration_stats", "//ortools/pdlp:primal_dual_hybrid_gradient", + "//ortools/pdlp:quadratic_program", "//ortools/pdlp:solve_log_cc_proto", "//ortools/pdlp:solvers_cc_proto", - "//ortools/port:file", + "//ortools/port:proto_utils", + "//ortools/util:lazy_mutable_copy", + "@com_google_absl//absl/status:statusor", + ], +) + +cc_library( + name = "sat_solver_utils", + srcs = ["sat_solver_utils.cc"], + hdrs = ["sat_solver_utils.h"], + deps = [ + "//ortools/glop:parameters_cc_proto", + "//ortools/glop:preprocessor", + "//ortools/linear_solver:linear_solver_cc_proto", + "//ortools/lp_data:proto_utils", + "//ortools/util:logging", + "@com_google_absl//absl/memory", + ], +) + +cc_library( + name = "sat_proto_solver", + srcs = ["sat_proto_solver.cc"], + hdrs = ["sat_proto_solver.h"], + deps = [ + ":proto_utils", + ":sat_solver_utils", + "//ortools/glop:preprocessor", + "//ortools/linear_solver:linear_solver_cc_proto", + "//ortools/linear_solver:model_validator", + "//ortools/lp_data", + "//ortools/lp_data:base", "//ortools/port:proto_utils", "//ortools/sat:cp_model_cc_proto", "//ortools/sat:cp_model_solver", "//ortools/sat:lp_utils", - "//ortools/util:fp_utils", + "//ortools/sat:model", + "//ortools/sat:parameters_validation", + "//ortools/sat:sat_parameters_cc_proto", + "//ortools/util:logging", + "//ortools/util:time_limit", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/types:span", + ], +) + +cc_library( + name = "scip_proto_solver", + srcs = ["scip_proto_solver.cc"], + hdrs = ["scip_proto_solver.h"], + defines = ["USE_SCIP"], + deps = [ + "//ortools/base", + "//ortools/base:cleanup", + "//ortools/base:timer", + "//ortools/gscip:legacy_scip_params", + "//ortools/linear_solver:linear_solver_cc_proto", + "//ortools/linear_solver:model_validator", + "//ortools/linear_solver:scip_helper_macros", "//ortools/util:lazy_mutable_copy", + "@com_google_absl//absl/cleanup", + "@com_google_absl//absl/container:btree", + "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", - "@com_google_absl//absl/synchronization", + "@com_google_absl//absl/strings:str_format", + "@com_google_absl//absl/time", + "@scip//:libscip", + ], +) + +cc_library( + name = "gurobi_proto_solver", + srcs = ["gurobi_proto_solver.cc"], + hdrs = ["gurobi_proto_solver.h"], + deps = [ + "//ortools/base:cleanup", + "//ortools/base:timer", + "//ortools/gurobi:environment", + "//ortools/linear_solver:linear_solver_cc_proto", + "//ortools/linear_solver:model_validator", + "//ortools/util:lazy_mutable_copy", + "@com_google_absl//absl/base:core_headers", + "@com_google_absl//absl/cleanup", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + "@com_google_absl//absl/time", "@com_google_absl//absl/types:optional", ], ) + +cc_library( + name = "highs_proto_solver", + srcs = ["highs_proto_solver.cc"], + hdrs = ["highs_proto_solver.h"], + deps = [ + "@com_google_absl//absl/status:statusor", + "//ortools/linear_solver:linear_solver_cc_proto", + "//ortools/linear_solver:model_validator", + "//ortools/port:proto_utils", + ], +) \ No newline at end of file diff --git a/ortools/linear_solver/proto_solver/glop_proto_solver.cc b/ortools/linear_solver/proto_solver/glop_proto_solver.cc new file mode 100644 index 0000000000..46ffc48db8 --- /dev/null +++ b/ortools/linear_solver/proto_solver/glop_proto_solver.cc @@ -0,0 +1,221 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/linear_solver/proto_solver/glop_proto_solver.h" + +#include +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/strings/str_cat.h" +#include "ortools/base/logging.h" +#include "ortools/glop/lp_solver.h" +#include "ortools/glop/parameters_validation.h" +#include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/linear_solver/model_validator.h" +#include "ortools/linear_solver/proto_solver/proto_utils.h" +#include "ortools/lp_data/lp_data.h" +#include "ortools/lp_data/lp_types.h" +#include "ortools/lp_data/proto_utils.h" +#include "ortools/port/proto_utils.h" +#include "ortools/util/logging.h" +#include "ortools/util/time_limit.h" + +namespace operations_research { + +namespace { + +MPSolutionResponse ModelInvalidResponse(SolverLogger& logger, + std::string message) { + SOLVER_LOG(&logger, "Invalid model/parameters in glop_solve_proto.\n", + message); + + MPSolutionResponse response; + response.set_status(MPSolverResponseStatus::MPSOLVER_MODEL_INVALID); + response.set_status_str(message); + return response; +} + +MPSolverResponseStatus ToMPSolverResultStatus(glop::ProblemStatus s) { + switch (s) { + case glop::ProblemStatus::OPTIMAL: + return MPSOLVER_OPTIMAL; + case glop::ProblemStatus::PRIMAL_FEASIBLE: + return MPSOLVER_FEASIBLE; + + // Note(user): MPSolver does not have the equivalent of + // INFEASIBLE_OR_UNBOUNDED however UNBOUNDED is almost never relevant in + // applications, so we decided to report this status as INFEASIBLE since + // it should almost always be the case. Historically, we where reporting + // ABNORMAL, but that was more confusing than helpful. + // + // TODO(user): We could argue that it is infeasible to find the optimal of + // an unbounded problem. So it might just be simpler to completely get rid + // of the MpSolver::UNBOUNDED status that seems to never be used + // programmatically. + case glop::ProblemStatus::INFEASIBLE_OR_UNBOUNDED: // PASS_THROUGH_INTENDED + case glop::ProblemStatus::PRIMAL_INFEASIBLE: // PASS_THROUGH_INTENDED + case glop::ProblemStatus::DUAL_UNBOUNDED: + return MPSOLVER_INFEASIBLE; + + case glop::ProblemStatus::DUAL_INFEASIBLE: // PASS_THROUGH_INTENDED + case glop::ProblemStatus::PRIMAL_UNBOUNDED: + return MPSOLVER_UNBOUNDED; + + case glop::ProblemStatus::DUAL_FEASIBLE: // PASS_THROUGH_INTENDED + case glop::ProblemStatus::INIT: + return MPSOLVER_NOT_SOLVED; + + case glop::ProblemStatus::ABNORMAL: // PASS_THROUGH_INTENDED + case glop::ProblemStatus::IMPRECISE: // PASS_THROUGH_INTENDED + case glop::ProblemStatus::INVALID_PROBLEM: + return MPSOLVER_ABNORMAL; + } + LOG(DFATAL) << "Invalid glop::ProblemStatus " << s; + return MPSOLVER_ABNORMAL; +} + +} // namespace + +MPSolutionResponse GlopSolveProto( + MPModelRequest request, std::atomic* interrupt_solve, + std::function logging_callback) { + glop::GlopParameters params; + params.set_log_search_progress(request.enable_internal_solver_output()); + + // TODO(user): We do not support all the parameters here. In particular the + // logs before the solver is called will not be appended to the response. Fix + // that, and remove code duplication for the logger config. One way should be + // to not touch/configure anything if the logger is already created while + // calling SolveCpModel() and call a common config function from here or from + // inside Solve()? + SolverLogger logger; + if (logging_callback != nullptr) { + logger.AddInfoLoggingCallback(logging_callback); + } + logger.EnableLogging(params.log_search_progress()); + logger.SetLogToStdOut(params.log_to_stdout()); + + // Set it now so that it can be overwritten by the solver specific parameters. + if (request.has_solver_specific_parameters()) { + // See EncodeParametersAsString() documentation. + if (!std::is_base_of::value) { + if (!params.MergeFromString(request.solver_specific_parameters())) { + return ModelInvalidResponse( + logger, + "solver_specific_parameters is not a valid binary stream of the " + "GLOPParameters proto"); + } + } else { + if (!ProtobufTextFormatMergeFromString( + request.solver_specific_parameters(), ¶ms)) { + return ModelInvalidResponse( + logger, + "solver_specific_parameters is not a valid textual representation " + "of the GlopParameters proto"); + } + } + } + if (request.has_solver_time_limit_seconds()) { + params.set_max_time_in_seconds(request.solver_time_limit_seconds()); + } + + if (!request.model().general_constraint().empty()) { + return ModelInvalidResponse(logger, + "GLOP does not support general constraints"); + } + + // Model validation and delta handling. + MPSolutionResponse response; + if (!ExtractValidMPModelInPlaceOrPopulateResponseStatus(&request, + &response)) { + // Note that the ExtractValidMPModelInPlaceOrPopulateResponseStatus() can + // also close trivial model (empty or trivially infeasible). So this is not + // always the MODEL_INVALID status. + return response; + } + + { + const std::string error = glop::ValidateParameters(params); + if (!error.empty()) { + return ModelInvalidResponse( + logger, absl::StrCat("Invalid Glop parameters: ", error)); + } + } + + glop::LinearProgram linear_program; + MPModelProtoToLinearProgram(request.model(), &linear_program); + + glop::LPSolver lp_solver; + lp_solver.SetParameters(params); + + // TimeLimit and interrupt solve. + std::unique_ptr time_limit = + TimeLimit::FromParameters(lp_solver.GetParameters()); + if (interrupt_solve != nullptr) { + if (interrupt_solve->load()) { + response.set_status(MPSOLVER_CANCELLED_BY_USER); + response.set_status_str( + "Solve not started, because the user set the atomic in " + "MPSolver::SolveWithProto() to true before solving could " + "start."); + return response; + } else { + time_limit->RegisterExternalBooleanAsLimit(interrupt_solve); + } + } + + // Solve and set response status. + const glop::ProblemStatus status = + lp_solver.SolveWithTimeLimit(linear_program, time_limit.get()); + const MPSolverResponseStatus result_status = ToMPSolverResultStatus(status); + response.set_status(result_status); + + // Fill in solution. + if (result_status == MPSOLVER_OPTIMAL || result_status == MPSOLVER_FEASIBLE) { + response.set_objective_value(lp_solver.GetObjectiveValue()); + + const int num_vars = request.model().variable_size(); + for (int var_id = 0; var_id < num_vars; ++var_id) { + const glop::Fractional solution_value = + lp_solver.variable_values()[glop::ColIndex(var_id)]; + response.add_variable_value(solution_value); + + const glop::Fractional reduced_cost = + lp_solver.reduced_costs()[glop::ColIndex(var_id)]; + response.add_reduced_cost(reduced_cost); + } + } + + if (result_status == MPSOLVER_UNKNOWN_STATUS && interrupt_solve != nullptr && + interrupt_solve->load()) { + response.set_status(MPSOLVER_CANCELLED_BY_USER); + } + + const size_t num_constraints = request.model().constraint_size(); + for (int ct_id = 0; ct_id < num_constraints; ++ct_id) { + const glop::Fractional dual_value = + lp_solver.dual_values()[glop::RowIndex(ct_id)]; + response.add_dual_value(dual_value); + } + + return response; +} + +std::string GlopSolverVersion() { return glop::LPSolver::GlopVersion(); } + +} // namespace operations_research diff --git a/ortools/linear_solver/proto_solver/glop_proto_solver.h b/ortools/linear_solver/proto_solver/glop_proto_solver.h new file mode 100644 index 0000000000..85d56fe22c --- /dev/null +++ b/ortools/linear_solver/proto_solver/glop_proto_solver.h @@ -0,0 +1,55 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_GLOP_PROTO_SOLVER_H_ +#define OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_GLOP_PROTO_SOLVER_H_ + +#include +#include +#include + +#include "ortools/glop/parameters.pb.h" +#include "ortools/linear_solver/linear_solver.pb.h" + +namespace operations_research { + +// Solve the input LP model with the GLOP solver. +// +// If possible, std::move the request into this function call to avoid a copy. +// +// If you need to change the solver parameters, please use the +// EncodeParametersAsString() function to set the solver_specific_parameters +// field. +// +// The optional interrupt_solve can be used to interrupt the solve early. It +// must only be set to true, never reset to false. It is also used internally by +// the solver that will set it to true for its own internal logic. As a +// consequence the caller should ignore the stored value and should not use the +// same atomic for different concurrent calls. +// +// The optional logging_callback will be called when the GLOP parameter +// log_search_progress is set to true. Passing a callback will disable the +// default logging to INFO. Note though that by default the GLOP parameter +// log_to_stdout is true so even with a callback, the logs will appear on stdout +// too unless log_to_stdout is set to false. The enable_internal_solver_output +// in the request will act as the GLOP parameter log_search_progress. +MPSolutionResponse GlopSolveProto( + MPModelRequest request, std::atomic* interrupt_solve = nullptr, + std::function logging_callback = nullptr); + +// Returns a string that describes the version of the GLOP solver. +std::string GlopSolverVersion(); + +} // namespace operations_research + +#endif // OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_GLOP_PROTO_SOLVER_H_ diff --git a/ortools/linear_solver/proto_solver/proto_utils.h b/ortools/linear_solver/proto_solver/proto_utils.h new file mode 100644 index 0000000000..7595ec0ec6 --- /dev/null +++ b/ortools/linear_solver/proto_solver/proto_utils.h @@ -0,0 +1,67 @@ +// Copyright 2010-2022 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_PROTO_UTILS_H_ +#define OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_PROTO_UTILS_H_ + +#include +#include + +#include "absl/log/check.h" +#include "google/protobuf/message.h" +#include "ortools/port/proto_utils.h" + +namespace operations_research { + +#if defined(PROTOBUF_INTERNAL_IMPL) +using google::protobuf::Message; +#else +using google::protobuf::Message; +#endif + +// Returns a string that should be used in MPModelRequest's +// solver_specific_parameters field to encode the glop parameters. +// +// The returned string's content depends on the version of the proto library +// that is linked in the binary. +// +// By default it will contain the textual representation of the input proto. +// But when the proto-lite is used, it will contain the binary stream of the +// proto instead since it is not possible to build the textual representation in +// that case. +// +// This function will test if the proto-lite is used and expect a binary stream +// when it is the case. So in order for your code to be portable, you should +// always use this function to set the specific parameters. +// +// Proto-lite disables some features of protobufs and messages inherit from +// MessageLite directly instead of inheriting from Message (which is itself a +// specialization of MessageLite). +// See https://protobuf.dev/reference/cpp/cpp-generated/#message for details. +template +std::string EncodeParametersAsString(const P& parameters) { + if constexpr (!std::is_base_of::value) { + // Here we use SerializeToString() instead of SerializeAsString() since the + // later ignores errors and returns an empty string instead (which can be a + // valid value when no fields are set). + std::string bytes; + CHECK(parameters.SerializeToString(&bytes)); + return bytes; + } + + return ProtobufShortDebugString(parameters); +} + +} // namespace operations_research + +#endif // OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_PROTO_UTILS_H_ diff --git a/ortools/linear_solver/proto_solver/sat_proto_solver.cc b/ortools/linear_solver/proto_solver/sat_proto_solver.cc index e6ee312e32..cd4e6fc90f 100644 --- a/ortools/linear_solver/proto_solver/sat_proto_solver.cc +++ b/ortools/linear_solver/proto_solver/sat_proto_solver.cc @@ -27,14 +27,13 @@ #include #include "absl/log/check.h" -#include "absl/status/status.h" -#include "absl/status/statusor.h" #include "absl/strings/str_cat.h" #include "absl/types/span.h" #include "ortools/base/logging.h" #include "ortools/glop/preprocessor.h" #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/linear_solver/model_validator.h" +#include "ortools/linear_solver/proto_solver/proto_utils.h" #include "ortools/linear_solver/proto_solver/sat_solver_utils.h" #include "ortools/lp_data/lp_data.h" #include "ortools/lp_data/lp_types.h" @@ -52,19 +51,6 @@ namespace operations_research { namespace { -#if defined(PROTOBUF_INTERNAL_IMPL) -using google::protobuf::Message; -#else -using google::protobuf::Message; -#endif - -// Proto-lite disables some features of protos and messages inherit from -// MessageLite directly instead of inheriting from Message (which is itself a -// specialization of MessageLite). -// See https://protobuf.dev/reference/cpp/cpp-generated/#message for details. -constexpr bool kProtoLiteSatParameters = - !std::is_base_of::value; - MPSolverResponseStatus ToMPSolverResponseStatus(sat::CpSolverStatus status, bool has_objective) { switch (status) { @@ -116,10 +102,9 @@ MPSolutionResponse InfeasibleResponse(SolverLogger& logger, return response; } -MPSolutionResponse ModelInvalidResponse(SolverLogger& logger, +MPSolutionResponse InvalidModelResponse(SolverLogger& logger, std::string message) { - SOLVER_LOG(&logger, "Invalid model/parameters in sat_solve_proto.\n", - message); + SOLVER_LOG(&logger, "Invalid model in sat_solve_proto.\n", message); // This is needed for our benchmark scripts. if (logger.LoggingIsEnabled()) { @@ -134,35 +119,32 @@ MPSolutionResponse ModelInvalidResponse(SolverLogger& logger, return response; } +MPSolutionResponse InvalidParametersResponse(SolverLogger& logger, + std::string message) { + SOLVER_LOG(&logger, "Invalid parameters in sat_solve_proto.\n", message); + + // This is needed for our benchmark scripts. + if (logger.LoggingIsEnabled()) { + sat::CpSolverResponse cp_response; + cp_response.set_status(sat::CpSolverStatus::MODEL_INVALID); + SOLVER_LOG(&logger, CpSolverResponseStats(cp_response)); + } + + MPSolutionResponse response; + response.set_status( + MPSolverResponseStatus::MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS); + response.set_status_str(message); + return response; +} + } // namespace -absl::StatusOr SatSolveProto( +MPSolutionResponse SatSolveProto( MPModelRequest request, std::atomic* interrupt_solve, std::function logging_callback, std::function solution_callback) { sat::SatParameters params; params.set_log_search_progress(request.enable_internal_solver_output()); - // Set it now so that it can be overwritten by the solver specific parameters. - if (request.has_solver_specific_parameters()) { - // See EncodeSatParametersAsString() documentation. - if (kProtoLiteSatParameters) { - if (!params.MergeFromString(request.solver_specific_parameters())) { - return absl::InvalidArgumentError( - "solver_specific_parameters is not a valid binary stream of the " - "SatParameters proto"); - } - } else { - if (!ProtobufTextFormatMergeFromString( - request.solver_specific_parameters(), ¶ms)) { - return absl::InvalidArgumentError( - "solver_specific_parameters is not a valid textual representation " - "of the SatParameters proto"); - } - } - } - if (request.has_solver_time_limit_seconds()) { - params.set_max_time_in_seconds(request.solver_time_limit_seconds()); - } // TODO(user): We do not support all the parameters here. In particular the // logs before the solver is called will not be appended to the response. Fix @@ -177,6 +159,46 @@ absl::StatusOr SatSolveProto( logger.EnableLogging(params.log_search_progress()); logger.SetLogToStdOut(params.log_to_stdout()); + // Set it now so that it can be overwritten by the solver specific parameters. + if (request.has_solver_specific_parameters()) { + // See EncodeSatParametersAsString() documentation. + if constexpr (!std::is_base_of::value) { + if (!params.MergeFromString(request.solver_specific_parameters())) { + return InvalidParametersResponse( + logger, + "solver_specific_parameters is not a valid binary stream of the " + "SatParameters proto"); + } + } else { + if (!ProtobufTextFormatMergeFromString( + request.solver_specific_parameters(), ¶ms)) { + return InvalidParametersResponse( + logger, + "solver_specific_parameters is not a valid textual representation " + "of the SatParameters proto"); + } + } + } + + // Validate parameters. + { + const std::string error = sat::ValidateParameters(params); + if (!error.empty()) { + return InvalidParametersResponse( + logger, absl::StrCat("Invalid CP-SAT parameters: ", error)); + } + } + + // Reconfigure the logger in case the solver_specific_parameters overwrite its + // configuration. Note that the invalid parameter message will be logged + // before that though according to request.enable_internal_solver_output(). + logger.EnableLogging(params.log_search_progress()); + logger.SetLogToStdOut(params.log_to_stdout()); + + if (request.has_solver_time_limit_seconds()) { + params.set_max_time_in_seconds(request.solver_time_limit_seconds()); + } + // Model validation and delta handling. MPSolutionResponse response; if (!ExtractValidMPModelInPlaceOrPopulateResponseStatus(&request, @@ -200,15 +222,7 @@ absl::StatusOr SatSolveProto( MPModelProto* const mp_model = request.mutable_model(); if (!sat::MPModelProtoValidationBeforeConversion(params, *mp_model, &logger)) { - return ModelInvalidResponse(logger, "Extra CP-SAT validation failed."); - } - - { - const std::string error = sat::ValidateParameters(params); - if (!error.empty()) { - return ModelInvalidResponse( - logger, absl::StrCat("Invalid CP-SAT parameters: ", error)); - } + return InvalidModelResponse(logger, "Extra CP-SAT validation failed."); } // This is good to do before any presolve. @@ -236,7 +250,7 @@ absl::StatusOr SatSolveProto( return InfeasibleResponse( logger, "Problem proven infeasible during MIP presolve"); case glop::ProblemStatus::INVALID_PROBLEM: - return ModelInvalidResponse( + return InvalidModelResponse( logger, "Problem detected invalid during MIP presolve"); default: // TODO(user): We put the INFEASIBLE_OR_UNBOUNBED case here since there @@ -293,7 +307,7 @@ absl::StatusOr SatSolveProto( } } if (!all_integer) { - return ModelInvalidResponse( + return InvalidModelResponse( logger, "The model contains non-integer variables but the parameter " "'only_solve_ip' was set. Change this parameter if you " @@ -305,7 +319,7 @@ absl::StatusOr SatSolveProto( sat::CpModelProto cp_model; if (!ConvertMPModelProtoToCpModelProto(params, *mp_model, &cp_model, &logger)) { - return ModelInvalidResponse(logger, + return InvalidModelResponse(logger, "Failed to convert model into CP-SAT model"); } DCHECK_EQ(cp_model.variables().size(), var_scaling.size()); @@ -433,19 +447,6 @@ absl::StatusOr SatSolveProto( return response; } -std::string EncodeSatParametersAsString(const sat::SatParameters& parameters) { - if (kProtoLiteSatParameters) { - // Here we use SerializeToString() instead of SerializeAsString() since the - // later ignores errors and returns an empty string instead (which can be a - // valid value when no fields are set). - std::string bytes; - CHECK(parameters.SerializeToString(&bytes)); - return bytes; - } - - return ProtobufShortDebugString(parameters); -} - std::string SatSolverVersion() { return sat::CpSatSolverVersion(); } } // namespace operations_research diff --git a/ortools/linear_solver/proto_solver/sat_proto_solver.h b/ortools/linear_solver/proto_solver/sat_proto_solver.h index 3b05df85ba..f8cfc5ae04 100644 --- a/ortools/linear_solver/proto_solver/sat_proto_solver.h +++ b/ortools/linear_solver/proto_solver/sat_proto_solver.h @@ -18,7 +18,6 @@ #include #include -#include "absl/status/statusor.h" #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/sat/sat_parameters.pb.h" #include "ortools/util/logging.h" @@ -50,27 +49,11 @@ namespace operations_research { // found by the solver. The solver may call solution_callback from multiple // threads, but it will ensure that at most one thread executes // solution_callback at a time. -absl::StatusOr SatSolveProto( +MPSolutionResponse SatSolveProto( MPModelRequest request, std::atomic* interrupt_solve = nullptr, std::function logging_callback = nullptr, std::function solution_callback = nullptr); -// Returns a string that should be used in MPModelRequest's -// solver_specific_parameters field to encode the SAT parameters. -// -// The returned string's content depends on the version of the proto library -// that is linked in the binary. -// -// By default it will contain the textual representation of the input proto. -// But when the proto-lite is used, it will contain the binary stream of the -// proto instead since it is not possible to build the textual representation in -// that case. -// -// The SatSolveProto() function will test if the proto-lite is used and expect a -// binary stream when it is the case. So in order for your code to be portable, -// you should always use this function to set the specific parameters. -std::string EncodeSatParametersAsString(const sat::SatParameters& parameters); - // Returns a string that describes the version of the CP-SAT solver. std::string SatSolverVersion(); diff --git a/ortools/linear_solver/python/solve_model.py b/ortools/linear_solver/python/solve_model.py new file mode 100644 index 0000000000..0dd55c072f --- /dev/null +++ b/ortools/linear_solver/python/solve_model.py @@ -0,0 +1,57 @@ +# Copyright 2010-2022 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Minimal solver python binary.""" + +from collections.abc import Sequence + +from absl import app +from absl import flags + +from ortools.linear_solver.python import model_builder + +_INPUT = flags.DEFINE_string("input", "", "Input file to load and solve.") +_PARAMS = flags.DEFINE_string("params", "", "Solver parameters in string format.") +_SOLVER = flags.DEFINE_string("solver", "sat", "Solver type to solve the model with.") + + +def main(argv: Sequence[str]) -> None: + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + + model = model_builder.ModelBuilder() + + # Load MPS file. + if not model.import_from_mps_file(_INPUT.value): + print(f"Cannot import MPS file: '{_INPUT.value}'") + return + + # Create solver. + solver = model_builder.ModelSolver(_SOLVER.value) + if not solver.solver_is_supported(): + print(f"Cannot create solver with name '{_SOLVER.value}'") + return + + # Set parameters. + if _PARAMS.value: + solver.set_solver_specific_parameters(_PARAMS.value) + + # Enable the output of the solver. + solver.enable_output(True) + + # And solve. + solver.solve(model) + + +if __name__ == "__main__": + app.run(main) diff --git a/ortools/linear_solver/samples/assignment_groups_mip.py b/ortools/linear_solver/samples/assignment_groups_mip.py index d19edef796..a752e688ee 100644 --- a/ortools/linear_solver/samples/assignment_groups_mip.py +++ b/ortools/linear_solver/samples/assignment_groups_mip.py @@ -109,39 +109,39 @@ def main(): # Group1 constraint_g1 = solver.Constraint(1, 1) - for i in range(len(group1)): + for index, _ in enumerate(group1): # a*b can be transformed into 0 <= a + b - 2*p <= 1 with p in [0,1] # p is True if a AND b, False otherwise constraint = solver.Constraint(0, 1) - constraint.SetCoefficient(work[group1[i][0]], 1) - constraint.SetCoefficient(work[group1[i][1]], 1) - p = solver.BoolVar(f"g1_p{i}") + constraint.SetCoefficient(work[group1[index][0]], 1) + constraint.SetCoefficient(work[group1[index][1]], 1) + p = solver.BoolVar(f"g1_p{index}") constraint.SetCoefficient(p, -2) constraint_g1.SetCoefficient(p, 1) # Group2 constraint_g2 = solver.Constraint(1, 1) - for i in range(len(group2)): + for index, _ in enumerate(group2): # a*b can be transformed into 0 <= a + b - 2*p <= 1 with p in [0,1] # p is True if a AND b, False otherwise constraint = solver.Constraint(0, 1) - constraint.SetCoefficient(work[group2[i][0]], 1) - constraint.SetCoefficient(work[group2[i][1]], 1) - p = solver.BoolVar(f"g2_p{i}") + constraint.SetCoefficient(work[group2[index][0]], 1) + constraint.SetCoefficient(work[group2[index][1]], 1) + p = solver.BoolVar(f"g2_p{index}") constraint.SetCoefficient(p, -2) constraint_g2.SetCoefficient(p, 1) # Group3 constraint_g3 = solver.Constraint(1, 1) - for i in range(len(group3)): + for index, _ in enumerate(group3): # a*b can be transformed into 0 <= a + b - 2*p <= 1 with p in [0,1] # p is True if a AND b, False otherwise constraint = solver.Constraint(0, 1) - constraint.SetCoefficient(work[group3[i][0]], 1) - constraint.SetCoefficient(work[group3[i][1]], 1) - p = solver.BoolVar(f"g3_p{i}") + constraint.SetCoefficient(work[group3[index][0]], 1) + constraint.SetCoefficient(work[group3[index][1]], 1) + p = solver.BoolVar(f"g3_p{index}") constraint.SetCoefficient(p, -2) constraint_g3.SetCoefficient(p, 1) diff --git a/ortools/linear_solver/samples/bin_packing_mb.py b/ortools/linear_solver/samples/bin_packing_mb.py index ae7d76b808..3cd8466075 100644 --- a/ortools/linear_solver/samples/bin_packing_mb.py +++ b/ortools/linear_solver/samples/bin_packing_mb.py @@ -25,7 +25,7 @@ from ortools.linear_solver.python import model_builder # [START program_part1] # [START data_model] -def create_data_model(): +def create_data_model() -> tuple[pd.DataFrame, pd.DataFrame]: """Create the data for the example.""" items_str = """ diff --git a/ortools/linear_solver/samples/mip_var_array.py b/ortools/linear_solver/samples/mip_var_array.py index 17861c330e..84fae73e5a 100644 --- a/ortools/linear_solver/samples/mip_var_array.py +++ b/ortools/linear_solver/samples/mip_var_array.py @@ -13,6 +13,7 @@ # limitations under the License. """MIP example that uses a variable array.""" + # [START program] # [START import] from ortools.linear_solver import pywraplp diff --git a/ortools/linear_solver/samples/multiple_knapsack_mip.py b/ortools/linear_solver/samples/multiple_knapsack_mip.py index 821d4e777e..9dbe5e9dbb 100644 --- a/ortools/linear_solver/samples/multiple_knapsack_mip.py +++ b/ortools/linear_solver/samples/multiple_knapsack_mip.py @@ -90,7 +90,8 @@ def main(): for i in data["all_items"]: if x[i, b].solution_value() > 0: print( - f"Item {i} weight: {data['weights'][i]} value: {data['values'][i]}" + f"Item {i} weight: {data['weights'][i]} value:" + f" {data['values'][i]}" ) bin_weight += data["weights"][i] bin_value += data["values"][i] diff --git a/ortools/linear_solver/sat_interface.cc b/ortools/linear_solver/sat_interface.cc index a08ce62c2d..44fb489894 100644 --- a/ortools/linear_solver/sat_interface.cc +++ b/ortools/linear_solver/sat_interface.cc @@ -24,6 +24,7 @@ #include "ortools/base/logging.h" #include "ortools/linear_solver/linear_solver.h" #include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/linear_solver/proto_solver/proto_utils.h" #include "ortools/linear_solver/proto_solver/sat_proto_solver.h" #include "ortools/port/proto_utils.h" #include "ortools/sat/cp_model.pb.h" @@ -130,14 +131,11 @@ MPSolver::ResultStatus SatInterface::Solve(const MPSolverParameters& param) { MPModelRequest request; solver_->ExportModelToProto(request.mutable_model()); - request.set_solver_specific_parameters( - EncodeSatParametersAsString(parameters_)); + request.set_solver_specific_parameters(EncodeParametersAsString(parameters_)); request.set_enable_internal_solver_output(!quiet_); - const absl::StatusOr status_or = - SatSolveProto(std::move(request), &interrupt_solve_); - if (!status_or.ok()) return MPSolver::ABNORMAL; - const MPSolutionResponse& response = status_or.value(); + const MPSolutionResponse response = + SatSolveProto(std::move(request), &interrupt_solve_); // The solution must be marked as synchronized even when no solution exists. sync_status_ = SOLUTION_SYNCHRONIZED; @@ -156,22 +154,7 @@ MPSolver::ResultStatus SatInterface::Solve(const MPSolverParameters& param) { std::optional SatInterface::DirectlySolveProto( const MPModelRequest& request, std::atomic* interrupt) { - absl::StatusOr status_or = - SatSolveProto(request, interrupt); - if (status_or.ok()) return std::move(status_or).value(); - if (request.enable_internal_solver_output()) { - LOG(INFO) << "Failed SAT solve: " << status_or.status(); - } - MPSolutionResponse response; - // As of 2021-08, the sole non-OK status returned by SatSolveProto is an - // INVALID_ARGUMENT error caused by invalid solver parameters. - // TODO(user): Move that conversion to SatSolveProto, which should always - // return a MPSolutionResponse, even for errors. - response.set_status(absl::IsInvalidArgument(status_or.status()) - ? MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS - : MPSOLVER_ABNORMAL); - response.set_status_str(status_or.status().ToString()); - return response; + return SatSolveProto(request, interrupt); } bool SatInterface::InterruptSolve() { diff --git a/ortools/linear_solver/wrappers/BUILD.bazel b/ortools/linear_solver/wrappers/BUILD.bazel index 0d84f084f7..ae25cac533 100644 --- a/ortools/linear_solver/wrappers/BUILD.bazel +++ b/ortools/linear_solver/wrappers/BUILD.bazel @@ -40,6 +40,11 @@ cc_library( "//ortools/linear_solver", "//ortools/linear_solver:linear_solver_cc_proto", "//ortools/linear_solver:model_exporter", + "//ortools/linear_solver/proto_solver:glop_proto_solver", + "//ortools/linear_solver/proto_solver:gurobi_proto_solver", + "//ortools/linear_solver/proto_solver:pdlp_proto_solver", + "//ortools/linear_solver/proto_solver:sat_proto_solver", + "//ortools/linear_solver/proto_solver:scip_proto_solver", "//ortools/lp_data:lp_parser", "//ortools/lp_data:mps_reader", "//ortools/util:logging", diff --git a/ortools/linear_solver/wrappers/model_builder_helper.cc b/ortools/linear_solver/wrappers/model_builder_helper.cc index 3673036808..206b93f520 100644 --- a/ortools/linear_solver/wrappers/model_builder_helper.cc +++ b/ortools/linear_solver/wrappers/model_builder_helper.cc @@ -27,6 +27,8 @@ #include "ortools/base/options.h" #include "ortools/linear_solver/linear_solver.h" #include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/linear_solver/model_exporter.h" +#include "ortools/linear_solver/proto_solver/glop_proto_solver.h" #include "ortools/linear_solver/proto_solver/sat_proto_solver.h" #if defined(USE_SCIP) #include "ortools/linear_solver/proto_solver/scip_proto_solver.h" @@ -537,18 +539,12 @@ void ModelSolverHelper::Solve(const ModelBuilderHelper& model) { } switch (solver_type_.value()) { case MPModelRequest::GLOP_LINEAR_PROGRAMMING: { - // TODO(user): Enable log_callback support. - MPSolutionResponse temp; - MPSolver::SolveWithProto(request, &temp, &interrupt_solve_); - response_ = std::move(temp); + response_ = GlopSolveProto(request, &interrupt_solve_, log_callback_); break; } case MPModelRequest::SAT_INTEGER_PROGRAMMING: { - const auto temp = + response_ = SatSolveProto(request, &interrupt_solve_, log_callback_, nullptr); - if (temp.ok()) { - response_ = std::move(temp.value()); - } break; } #if defined(USE_SCIP)