diff --git a/ortools/gurobi/isv_public/gurobi_isv.cc b/ortools/gurobi/isv_public/gurobi_isv.cc index 6a70b86648..88364c7cd7 100644 --- a/ortools/gurobi/isv_public/gurobi_isv.cc +++ b/ortools/gurobi/isv_public/gurobi_isv.cc @@ -50,9 +50,16 @@ absl::StatusOr NewPrimaryEnvFromISVKey(const GurobiIsvKey& isv_key) { << "): " << GRBgeterrormsg(primary_env); }; RETURN_IF_ERROR(handle_failure(GRBemptyenv(&primary_env), "GRBemptyenv()")); + // We want to turn off logging before setting the ISV key so that it doesn't + // leak. We store the original logging state, and reset it at the end. + int original_output_flag; + RETURN_IF_ERROR( + handle_failure(GRBgetintparam(primary_env, GRB_INT_PAR_OUTPUTFLAG, + &original_output_flag), + "getting original GRB_INT_PAR_OUTPUTFLAG value")); RETURN_IF_ERROR( handle_failure(GRBsetintparam(primary_env, GRB_INT_PAR_OUTPUTFLAG, 0), - "setting GRB_INT_PAR_OUTPUTFLAG")); + "turning off GRB_INT_PAR_OUTPUTFLAG")); RETURN_IF_ERROR(handle_failure( GRBsetstrparam(primary_env, "GURO_PAR_ISVNAME", isv_key.name.c_str()), "setting GURO_PAR_ISVNAME")); @@ -70,6 +77,10 @@ absl::StatusOr NewPrimaryEnvFromISVKey(const GurobiIsvKey& isv_key) { GRBsetstrparam(primary_env, "GURO_PAR_ISVKEY", isv_key.key.c_str()), "setting GURO_PAR_ISVKEY")); RETURN_IF_ERROR(handle_failure(GRBstartenv(primary_env), "GRBstartenv()")); + // Reset output flag to its original value. + RETURN_IF_ERROR(handle_failure( + GRBsetintparam(primary_env, GRB_INT_PAR_OUTPUTFLAG, original_output_flag), + "resetting GRB_INT_PAR_OUTPUTFLAG")); // Environment initialization succeeded, we don't want to free it upon exiting // this function. std::move(primary_env_cleanup).Cancel(); diff --git a/ortools/linear_solver/BUILD.bazel b/ortools/linear_solver/BUILD.bazel index 5998d21f0c..b05f16c931 100644 --- a/ortools/linear_solver/BUILD.bazel +++ b/ortools/linear_solver/BUILD.bazel @@ -413,5 +413,6 @@ cc_library( deps = [ ":linear_solver", ":linear_solver_cc_proto", + "//ortools/util:solve_interrupter", ], ) diff --git a/ortools/linear_solver/glop_interface.cc b/ortools/linear_solver/glop_interface.cc index 0a34d3a47d..0b45c2dd31 100644 --- a/ortools/linear_solver/glop_interface.cc +++ b/ortools/linear_solver/glop_interface.cc @@ -17,6 +17,7 @@ #include #include #include +#include #include #include "absl/base/attributes.h" @@ -26,9 +27,11 @@ #include "ortools/glop/parameters.pb.h" #include "ortools/linear_solver/glop_utils.h" #include "ortools/linear_solver/linear_solver.h" +#include "ortools/linear_solver/proto_solver/glop_proto_solver.h" #include "ortools/lp_data/lp_data.h" #include "ortools/lp_data/lp_types.h" #include "ortools/port/proto_utils.h" +#include "ortools/util/lazy_mutable_copy.h" #include "ortools/util/time_limit.h" namespace operations_research { @@ -43,6 +46,15 @@ class GLOPInterface : public MPSolverInterface { MPSolver::ResultStatus Solve(const MPSolverParameters& param) override; bool InterruptSolve() override; + // ----- Directly solve proto is supported --- + bool SupportsDirectlySolveProto(std::atomic* interrupt) const override { + return true; + } + MPSolutionResponse DirectlySolveProto(LazyMutableCopy request, + std::atomic* interrupt) override { + return GlopSolveProto(std::move(request), interrupt); + } + // ----- Model modifications and extraction ----- void Reset() override; void SetOptimizationDirection(bool maximize) override; diff --git a/ortools/linear_solver/gurobi_interface.cc b/ortools/linear_solver/gurobi_interface.cc index 5b787a0ae8..13600d440e 100644 --- a/ortools/linear_solver/gurobi_interface.cc +++ b/ortools/linear_solver/gurobi_interface.cc @@ -58,6 +58,7 @@ #include "absl/base/attributes.h" #include "absl/container/flat_hash_set.h" +#include "absl/log/check.h" #include "absl/status/status.h" #include "absl/strings/match.h" #include "absl/strings/str_format.h" @@ -70,6 +71,8 @@ #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" +#include "ortools/linear_solver/proto_solver/proto_utils.h" +#include "ortools/util/lazy_mutable_copy.h" #include "ortools/util/time_limit.h" ABSL_FLAG(int, num_gurobi_threads, 0, @@ -89,8 +92,22 @@ class GurobiInterface : public MPSolverInterface { // ----- Solve ----- // Solves the problem using the parameter values specified. MPSolver::ResultStatus Solve(const MPSolverParameters& param) override; - std::optional DirectlySolveProto( - const MPModelRequest& request, std::atomic* interrupt) override; + + // ----- Directly solve proto is supported without interrupt --- + bool SupportsDirectlySolveProto(std::atomic* interrupt) const override { + return interrupt == nullptr; + } + MPSolutionResponse DirectlySolveProto(LazyMutableCopy request, + std::atomic* interrupt) override { + DCHECK_EQ(interrupt, nullptr); + const bool log_error = request->enable_internal_solver_output(); + + // Here we reuse the Gurobi environment to support single-use license that + // forbids creating a second environment if one already exists. + return ConvertStatusOrMPSolutionResponse( + log_error, GurobiSolveProto(std::move(request), global_env_)); + } + // Writes the model. void Write(const std::string& filename) override; @@ -1333,28 +1350,6 @@ MPSolver::ResultStatus GurobiInterface::Solve(const MPSolverParameters& param) { return result_status_; } -std::optional GurobiInterface::DirectlySolveProto( - const MPModelRequest& request, std::atomic* interrupt) { - // Interruption via atomic is not directly supported by Gurobi. - if (interrupt != nullptr) return std::nullopt; - - // Here we reuse the Gurobi environment to support single-use license that - // forbids creating a second environment if one already exists. - const auto status_or = GurobiSolveProto(request, global_env_); - if (status_or.ok()) return status_or.value(); - // Special case: if something is not implemented yet, fall back to solving - // through MPSolver. - if (absl::IsUnimplemented(status_or.status())) return std::nullopt; - - if (request.enable_internal_solver_output()) { - LOG(INFO) << "Invalid Gurobi status: " << status_or.status(); - } - MPSolutionResponse response; - response.set_status(MPSOLVER_NOT_SOLVED); - response.set_status_str(status_or.status().ToString()); - return response; -} - bool GurobiInterface::NextSolution() { // Next solution only supported for MIP if (!mip_) return false; diff --git a/ortools/linear_solver/highs_interface.cc b/ortools/linear_solver/highs_interface.cc index 5e21908564..240733052d 100644 --- a/ortools/linear_solver/highs_interface.cc +++ b/ortools/linear_solver/highs_interface.cc @@ -21,6 +21,7 @@ #include #include "absl/base/attributes.h" +#include "absl/log/check.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_cat.h" @@ -30,7 +31,9 @@ #include "ortools/linear_solver/linear_solver.h" #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/linear_solver/proto_solver/highs_proto_solver.h" +#include "ortools/linear_solver/proto_solver/proto_utils.h" #include "ortools/port/proto_utils.h" +#include "ortools/util/lazy_mutable_copy.h" namespace operations_research { @@ -41,8 +44,18 @@ class HighsInterface : public MPSolverInterface { // ----- Solve ----- MPSolver::ResultStatus Solve(const MPSolverParameters& param) override; - std::optional DirectlySolveProto( - const MPModelRequest& request, std::atomic* interrupt) override; + + // ----- Directly solve proto is supported without interrupt --- + bool SupportsDirectlySolveProto(std::atomic* interrupt) const override { + return interrupt == nullptr; + } + MPSolutionResponse DirectlySolveProto(LazyMutableCopy request, + std::atomic* interrupt) override { + DCHECK_EQ(interrupt, nullptr); + const bool log_error = request->enable_internal_solver_output(); + return ConvertStatusOrMPSolutionResponse( + log_error, HighsSolveProto(std::move(request))); + } // ----- Model modifications and extraction ----- void Reset() override; @@ -150,10 +163,6 @@ MPSolver::ResultStatus HighsInterface::Solve(const MPSolverParameters& param) { sync_status_ = SOLUTION_SYNCHRONIZED; result_status_ = static_cast(response->status()); LOG_IF(DFATAL, !response->has_solver_specific_info()) << *response; - // if (!solve_log_.ParseFromString(response->solver_specific_info())) { - // LOG(DFATAL) << "Unable to parse Highs's SolveLog from - // solver_specific_info"; - // } if (response->status() == MPSOLVER_FEASIBLE || response->status() == MPSOLVER_OPTIMAL) { @@ -166,22 +175,6 @@ MPSolver::ResultStatus HighsInterface::Solve(const MPSolverParameters& param) { return result_status_; } -std::optional HighsInterface::DirectlySolveProto( - const MPModelRequest& request, std::atomic* interrupt) { - if (interrupt) return std::nullopt; - absl::StatusOr response = HighsSolveProto(request); - - if (!response.ok()) { - LOG(ERROR) << "Unexpected error solving with Highs: " << response.status(); - MPSolutionResponse error_response; - error_response.set_status(MPSolverResponseStatus::MPSOLVER_ABNORMAL); - error_response.set_status_str(response.status().ToString()); - return error_response; - } - - return *response; -} - void HighsInterface::Reset() { ResetExtractionInformation(); } void HighsInterface::SetOptimizationDirection(bool maximize) { diff --git a/ortools/linear_solver/linear_solver.cc b/ortools/linear_solver/linear_solver.cc index d9c5b8f9cc..7dceb5dcf2 100644 --- a/ortools/linear_solver/linear_solver.cc +++ b/ortools/linear_solver/linear_solver.cc @@ -1006,16 +1006,24 @@ void AppendStatusStr(const std::string& msg, MPSolutionResponse* response) { absl::StrCat(response->status_str(), (response->status_str().empty() ? "" : "\n"), msg)); } + } // namespace // static void MPSolver::SolveWithProto(const MPModelRequest& model_request, MPSolutionResponse* response, std::atomic* interrupt) { + return SolveLazyMutableRequest(model_request, response, interrupt); +} + +// static +void MPSolver::SolveLazyMutableRequest(LazyMutableCopy request, + MPSolutionResponse* response, + std::atomic* interrupt) { CHECK(response != nullptr); if (interrupt != nullptr && - !SolverTypeSupportsInterruption(model_request.solver_type())) { + !SolverTypeSupportsInterruption(request->solver_type())) { response->set_status(MPSOLVER_INCOMPATIBLE_OPTIONS); response->set_status_str( "Called MPSolver::SolveWithProto with an underlying solver that " @@ -1023,57 +1031,51 @@ void MPSolver::SolveWithProto(const MPModelRequest& model_request, return; } - MPSolver solver(model_request.model().name(), - static_cast( - model_request.solver_type())); - if (model_request.enable_internal_solver_output()) { + MPSolver solver( + request->model().name(), + static_cast(request->solver_type())); + if (request->enable_internal_solver_output()) { solver.EnableOutput(); std::cout << "MPModelRequest info:\n" - << GetMPModelRequestLoggingInfo(model_request) << std::endl; + << GetMPModelRequestLoggingInfo(*request) << std::endl; } - // If interruption support is not required, we don't need access to the - // underlying solver and can solve it directly if the interface supports it. - auto optional_response = - solver.interface_->DirectlySolveProto(model_request, interrupt); - if (optional_response) { - *response = std::move(optional_response).value(); + // If the solver supports it, we can std::move() the request since we will + // return right after this in all cases. + if (solver.interface_->SupportsDirectlySolveProto(interrupt)) { + *response = + solver.interface_->DirectlySolveProto(std::move(request), interrupt); return; } + // Validate and extract model delta. Also deal with trivial problems. const std::optional> optional_model = - ExtractValidMPModelOrPopulateResponseStatus(model_request, response); - if (!optional_model) { - LOG_IF(WARNING, model_request.enable_internal_solver_output()) - << "Failed to extract a valid model from protocol buffer. Status: " - << ProtoEnumToString(response->status()) << " (" - << response->status() << "): " << response->status_str(); - return; - } + GetMPModelOrPopulateResponse(request, response); + if (!optional_model) return; + std::string error_message; response->set_status(solver.LoadModelFromProtoInternal( - optional_model->get(), /*name_policy=*/DEFAULT_CLEAR_NAMES, + **optional_model, /*name_policy=*/DEFAULT_CLEAR_NAMES, /*check_model_validity=*/false, &error_message)); // Even though we don't re-check model validity here, there can be some // problems found by LoadModelFromProto, eg. unsupported features. if (response->status() != MPSOLVER_MODEL_IS_VALID) { response->set_status_str(error_message); - LOG_IF(WARNING, model_request.enable_internal_solver_output()) + LOG_IF(WARNING, request->enable_internal_solver_output()) << "LoadModelFromProtoInternal() failed even though the model was " << "valid! Status: " << ProtoEnumToString(response->status()) << " (" << response->status() << "); Error: " << error_message; return; } - if (model_request.has_solver_time_limit_seconds()) { - solver.SetTimeLimit( - absl::Seconds(model_request.solver_time_limit_seconds())); + if (request->has_solver_time_limit_seconds()) { + solver.SetTimeLimit(absl::Seconds(request->solver_time_limit_seconds())); } std::string warning_message; - if (model_request.has_solver_specific_parameters()) { + if (request->has_solver_specific_parameters()) { if (!solver.SetSolverSpecificParametersAsString( - model_request.solver_specific_parameters())) { - if (model_request.ignore_solver_specific_parameters_failure()) { + request->solver_specific_parameters())) { + if (request->ignore_solver_specific_parameters_failure()) { // We'll add a warning message in status_str after the solve. warning_message = "Warning: the solver specific parameters were not successfully " @@ -1097,8 +1099,7 @@ void MPSolver::SolveWithProto(const MPModelRequest& model_request, { absl::Notification solve_finished; auto polling_func = [&interrupt, &solve_finished, &solver, - &interrupted_by_user, &interrupt_time, - &model_request]() { + &interrupted_by_user, &interrupt_time, &request]() { constexpr absl::Duration kPollDelay = absl::Microseconds(100); constexpr absl::Duration kMaxInterruptionDelay = absl::Seconds(10); @@ -1141,7 +1142,7 @@ void MPSolver::SolveWithProto(const MPModelRequest& model_request, "underlying solver, despite repeated calls over at least " << absl::FormatDuration(kMaxInterruptionDelay) << ". Solver type used: " - << MPModelRequest_SolverType_Name(model_request.solver_type()); + << MPModelRequest_SolverType_Name(request->solver_type()); // Note that in opt builds, the polling thread terminates here with an // error message, but we let Solve() finish, ignoring the user diff --git a/ortools/linear_solver/linear_solver.h b/ortools/linear_solver/linear_solver.h index f75c92e465..dcb023ca57 100644 --- a/ortools/linear_solver/linear_solver.h +++ b/ortools/linear_solver/linear_solver.h @@ -164,6 +164,7 @@ #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/linear_solver/linear_solver_callback.h" #include "ortools/port/proto_utils.h" +#include "ortools/util/lazy_mutable_copy.h" ABSL_DECLARE_FLAG(bool, linear_solver_enable_verbose_output); ABSL_DECLARE_FLAG(bool, log_verification_errors); @@ -572,6 +573,9 @@ class MPSolver { * other solver type immediately returns an MPSOLVER_INCOMPATIBLE_OPTIONS * error. * + * `interrupt` is non-const because the internal solver may set it to true + * itself, in some cases. + * * Note(user): This attempts to first use `DirectlySolveProto()` (if * implemented). Consequently, this most likely does *not* override any of * the default parameters of the underlying solver. This behavior *differs* @@ -581,8 +585,20 @@ class MPSolver { ABSL_DEPRECATED("Prefer SolveMPModel() from solve_mp_model.h.") static void SolveWithProto(const MPModelRequest& model_request, MPSolutionResponse* response, - // `interrupt` is non-const because the internal - // solver may set it to true itself, in some cases. + std::atomic* interrupt = nullptr); + + /** + * This version support both `const MPModelRequest&` and `MPModelRequest&&` + * for the request. When using the second form, it will try to delete the + * request as soon as it is translated to the solver internal representation. + * This saves peak memory usage. + * + * Note that we need a different name and can't just accept MPModelRequest&& + * otherwise we have swig issues. + */ + ABSL_DEPRECATED("Prefer SolveMPModel() from solve_mp_model.h.") + static void SolveLazyMutableRequest(LazyMutableCopy request, + MPSolutionResponse* response, std::atomic* interrupt = nullptr); ABSL_DEPRECATED( @@ -1646,18 +1662,23 @@ class MPSolverInterface { // solution is optimal. virtual MPSolver::ResultStatus Solve(const MPSolverParameters& param) = 0; - // Attempts to directly solve a MPModelRequest, bypassing the MPSolver data + // DirectlySolveProto() shall only be used if SupportsDirectlySolveProto() is + // true. + // + // DirectlySolveProto() solves a MPModelRequest, bypassing the MPSolver data // structures entirely. Like MPSolver::SolveWithProto(), optionally takes in // an 'interrupt' boolean. - // Returns {} (eg. absl::nullopt) if direct-solve is not supported by the - // underlying solver (possibly because interrupt != nullptr), in which case - // the user should fall back to using MPSolver. - virtual std::optional DirectlySolveProto( - const MPModelRequest& /*request*/, + virtual bool SupportsDirectlySolveProto( + std::atomic* /*interrupt*/) const { + return false; + } + virtual MPSolutionResponse DirectlySolveProto( + LazyMutableCopy /*request*/, // `interrupt` is non-const because the internal // solver may set it to true itself, in some cases. std::atomic* /*interrupt*/) { - return std::nullopt; + LOG(DFATAL) << "Default implementation should never be called."; + return MPSolutionResponse(); } // Writes the model using the solver internal write function. Currently only diff --git a/ortools/linear_solver/model_validator.cc b/ortools/linear_solver/model_validator.cc index 1d922fc80e..b851fd6af1 100644 --- a/ortools/linear_solver/model_validator.cc +++ b/ortools/linear_solver/model_validator.cc @@ -685,14 +685,20 @@ std::string FindErrorInMPModelProto( std::optional> ExtractValidMPModelOrPopulateResponseStatus(const MPModelRequest& request, MPSolutionResponse* response) { + LazyMutableCopy ref(request); + return GetMPModelOrPopulateResponse(ref, response); +} + +std::optional> GetMPModelOrPopulateResponse( + LazyMutableCopy& request, MPSolutionResponse* response) { CHECK(response != nullptr); - if (!request.has_model() && !request.has_model_delta()) { + if (!request->has_model() && !request->has_model_delta()) { response->set_status(MPSOLVER_OPTIMAL); response->set_status_str("Requests without model are considered OPTIMAL"); return std::nullopt; } - if (request.has_model() && request.has_model_delta()) { + if (request->has_model() && request->has_model_delta()) { response->set_status(MPSOLVER_MODEL_INVALID); response->set_status_str( "Fields 'model' and 'model_delta' are mutually exclusive"); @@ -700,13 +706,22 @@ ExtractValidMPModelOrPopulateResponseStatus(const MPModelRequest& request, } // Extract the baseline model. - LazyMutableCopy model(request.model()); - if (request.has_model_delta()) { + // Note that we move it out of the request if we have ownership. + LazyMutableCopy model = [&]() { + if (request.has_ownership()) { + return LazyMutableCopy( + std::move(*(request.get_mutable()->mutable_model()))); + } else { + return LazyMutableCopy(request->model()); + } + }(); + + if (request->has_model_delta()) { // NOTE(user): This library needs to be portable, so we can't include - // ortools/base/helpers.h; see ../port/file.h. + // file/base/helpers.h; see ../port/file.h. std::string contents; const absl::Status file_read_status = PortableFileGetContents( - request.model_delta().baseline_model_file_path(), &contents); + request->model_delta().baseline_model_file_path(), &contents); if (!file_read_status.ok()) { response->set_status(MPSOLVER_MODEL_INVALID); response->set_status_str( @@ -719,25 +734,25 @@ ExtractValidMPModelOrPopulateResponseStatus(const MPModelRequest& request, response->set_status_str( absl::StrFormat("The contents of baseline model file '%s' couldn't " "be parsed as a raw serialized MPModelProto", - request.model_delta().baseline_model_file_path())); + request->model_delta().baseline_model_file_path())); return std::nullopt; } } // Validate the baseline model. - std::string error = FindErrorInMPModelProto(model.get()); + std::string error = FindErrorInMPModelProto(*model); // If the baseline is valid and we have a model delta, validate the delta, // then apply it. - if (error.empty() && request.has_model_delta()) { - const MPModelDeltaProto& delta = request.model_delta(); - error = FindErrorInMPModelDeltaProto(delta, model.get()); + if (error.empty() && request->has_model_delta()) { + const MPModelDeltaProto& delta = request->model_delta(); + error = FindErrorInMPModelDeltaProto(delta, *model); if (error.empty()) ApplyVerifiedMPModelDelta(delta, model.get_mutable()); } // Deal with errors. if (!error.empty()) { - if (request.enable_internal_solver_output()) { + if (request->enable_internal_solver_output()) { LOG(ERROR) << absl::StrCat("Invalid model: ", error); } response->set_status(absl::StrContains(error, "Infeasible") @@ -747,10 +762,10 @@ ExtractValidMPModelOrPopulateResponseStatus(const MPModelRequest& request, return std::nullopt; } - if (model.get().variable_size() == 0 && model.get().constraint_size() == 0 && - model.get().general_constraint_size() == 0) { + if (model->variable_size() == 0 && model->constraint_size() == 0 && + model->general_constraint_size() == 0) { response->set_status(MPSOLVER_OPTIMAL); - response->set_objective_value(model.get().objective_offset()); + response->set_objective_value(model->objective_offset()); response->set_best_objective_bound(response->objective_value()); response->set_status_str( "Requests without variables and constraints are considered OPTIMAL"); @@ -760,17 +775,6 @@ ExtractValidMPModelOrPopulateResponseStatus(const MPModelRequest& request, return std::move(model); } -bool ExtractValidMPModelInPlaceOrPopulateResponseStatus( - MPModelRequest* request, MPSolutionResponse* response) { - std::optional> lazy_copy = - ExtractValidMPModelOrPopulateResponseStatus(*request, response); - if (!lazy_copy) return false; - if (lazy_copy->was_copied()) { - lazy_copy->get_mutable()->Swap(request->mutable_model()); - } - return true; -} - // TODO(user): Add a general FindFeasibilityErrorInSolution() and factor out the // common code. std::string FindFeasibilityErrorInSolutionHint(const MPModelProto& model, diff --git a/ortools/linear_solver/model_validator.h b/ortools/linear_solver/model_validator.h index e62d9bac6d..dfbff51a33 100644 --- a/ortools/linear_solver/model_validator.h +++ b/ortools/linear_solver/model_validator.h @@ -59,12 +59,12 @@ ExtractValidMPModelOrPopulateResponseStatus(const MPModelRequest& request, MPSolutionResponse* response); /** - * Like ExtractValidMPModelOrPopulateResponseStatus(), but works in-place: - * if the MPModel needed extraction, it will be populated in the request, and - * it returns the success boolean. + * Same as ExtractValidMPModelOrPopulateResponseStatus() but if we already + * have ownership of the request, do not do any copy even when needed. + * Note that the MPModelProto in the request will be cleared in this case. */ -bool ExtractValidMPModelInPlaceOrPopulateResponseStatus( - MPModelRequest* request, MPSolutionResponse* response); +std::optional> GetMPModelOrPopulateResponse( + LazyMutableCopy& request, MPSolutionResponse* response); /** * Returns an empty string if the solution hint given in the model is a feasible diff --git a/ortools/linear_solver/pdlp_interface.cc b/ortools/linear_solver/pdlp_interface.cc index e5c5fea2db..3d756c4176 100644 --- a/ortools/linear_solver/pdlp_interface.cc +++ b/ortools/linear_solver/pdlp_interface.cc @@ -25,14 +25,15 @@ #include "absl/status/statusor.h" #include "absl/strings/str_cat.h" #include "absl/types/optional.h" -#include "google/protobuf/text_format.h" #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/pdlp_proto_solver.h" +#include "ortools/linear_solver/proto_solver/proto_utils.h" #include "ortools/pdlp/solve_log.pb.h" #include "ortools/pdlp/solvers.pb.h" #include "ortools/port/proto_utils.h" +#include "ortools/util/lazy_mutable_copy.h" namespace operations_research { @@ -43,8 +44,17 @@ class PdlpInterface : public MPSolverInterface { // ----- Solve ----- MPSolver::ResultStatus Solve(const MPSolverParameters& param) override; - std::optional DirectlySolveProto( - const MPModelRequest& request, std::atomic* interrupt) override; + + bool SupportsDirectlySolveProto(std::atomic* interrupt) const override { + return true; + } + MPSolutionResponse DirectlySolveProto(LazyMutableCopy request, + std::atomic* interrupt) override { + const bool log_error = request->enable_internal_solver_output(); + return ConvertStatusOrMPSolutionResponse( + log_error, PdlpSolveProto(std::move(request), + /*relax_integer_variables=*/true, interrupt)); + } // ----- Model modifications and extraction ----- void Reset() override; @@ -143,7 +153,7 @@ MPSolver::ResultStatus PdlpInterface::Solve(const MPSolverParameters& param) { if (!google::protobuf::TextFormat::PrintToString( parameters_, request.mutable_solver_specific_parameters())) { LOG(QFATAL) << "Error converting parameters to text format: " - << parameters_.DebugString(); + << ProtobufDebugString(parameters_); } absl::StatusOr response = PdlpSolveProto( request, /*relax_integer_variables=*/true, &interrupt_solver_); @@ -180,22 +190,6 @@ MPSolver::ResultStatus PdlpInterface::Solve(const MPSolverParameters& param) { return result_status_; } -std::optional PdlpInterface::DirectlySolveProto( - const MPModelRequest& request, std::atomic* interrupt) { - absl::StatusOr response = - PdlpSolveProto(request, /*relax_integer_variables=*/true, interrupt); - - if (!response.ok()) { - LOG(ERROR) << "Unexpected error solving with PDLP: " << response.status(); - MPSolutionResponse error_response; - error_response.set_status(MPSolverResponseStatus::MPSOLVER_ABNORMAL); - error_response.set_status_str(response.status().ToString()); - return error_response; - } - - return *response; -} - void PdlpInterface::Reset() { ResetExtractionInformation(); } void PdlpInterface::SetOptimizationDirection(bool maximize) { diff --git a/ortools/linear_solver/proto_solver/BUILD.bazel b/ortools/linear_solver/proto_solver/BUILD.bazel index 6fc8c3e616..deaca746d4 100644 --- a/ortools/linear_solver/proto_solver/BUILD.bazel +++ b/ortools/linear_solver/proto_solver/BUILD.bazel @@ -39,6 +39,7 @@ cc_library( "//ortools/lp_data:base", "//ortools/lp_data:proto_utils", "//ortools/port:proto_utils", + "//ortools/util:lazy_mutable_copy", "//ortools/util:logging", "//ortools/util:time_limit", "@com_google_absl//absl/log", @@ -99,6 +100,7 @@ cc_library( "//ortools/sat:model", "//ortools/sat:parameters_validation", "//ortools/sat:sat_parameters_cc_proto", + "//ortools/util:lazy_mutable_copy", "//ortools/util:logging", "//ortools/util:time_limit", "@com_google_absl//absl/log", @@ -169,6 +171,7 @@ cc_library( "//ortools/linear_solver:linear_solver_cc_proto", "//ortools/linear_solver:model_validator", "//ortools/port:proto_utils", + "//ortools/util:lazy_mutable_copy", "@com_google_absl//absl/status:statusor", ], ) diff --git a/ortools/linear_solver/proto_solver/glop_proto_solver.cc b/ortools/linear_solver/proto_solver/glop_proto_solver.cc index 2ff34e9fb9..5fb474e054 100644 --- a/ortools/linear_solver/proto_solver/glop_proto_solver.cc +++ b/ortools/linear_solver/proto_solver/glop_proto_solver.cc @@ -17,8 +17,10 @@ #include #include #include +#include #include #include +#include #include "absl/log/check.h" #include "absl/strings/str_cat.h" @@ -32,6 +34,7 @@ #include "ortools/lp_data/lp_types.h" #include "ortools/lp_data/proto_utils.h" #include "ortools/port/proto_utils.h" +#include "ortools/util/lazy_mutable_copy.h" #include "ortools/util/logging.h" #include "ortools/util/time_limit.h" @@ -41,8 +44,7 @@ namespace { MPSolutionResponse ModelInvalidResponse(SolverLogger& logger, std::string message) { - SOLVER_LOG(&logger, "Invalid model/parameters in glop_solve_proto.\n", - message); + SOLVER_LOG(&logger, "Invalid model in glop_solve_proto.\n", message); MPSolutionResponse response; response.set_status(MPSolverResponseStatus::MPSOLVER_MODEL_INVALID); @@ -50,6 +52,17 @@ MPSolutionResponse ModelInvalidResponse(SolverLogger& logger, return response; } +MPSolutionResponse ModelInvalidParametersResponse(SolverLogger& logger, + std::string message) { + SOLVER_LOG(&logger, "Invalid parameters in glop_solve_proto.\n", message); + + MPSolutionResponse response; + response.set_status( + MPSolverResponseStatus::MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS); + response.set_status_str(message); + return response; +} + MPSolverResponseStatus ToMPSolverResultStatus(glop::ProblemStatus s) { switch (s) { case glop::ProblemStatus::OPTIMAL: @@ -92,10 +105,10 @@ MPSolverResponseStatus ToMPSolverResultStatus(glop::ProblemStatus s) { } // namespace MPSolutionResponse GlopSolveProto( - MPModelRequest request, std::atomic* interrupt_solve, + LazyMutableCopy request, std::atomic* interrupt_solve, std::function logging_callback) { glop::GlopParameters params; - params.set_log_search_progress(request.enable_internal_solver_output()); + 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 @@ -111,54 +124,56 @@ MPSolutionResponse GlopSolveProto( 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()) { + if (request->has_solver_specific_parameters()) { // See EncodeParametersAsString() documentation. if (!std::is_base_of::value) { - if (!params.MergeFromString(request.solver_specific_parameters())) { - return ModelInvalidResponse( + if (!params.MergeFromString(request->solver_specific_parameters())) { + return ModelInvalidParametersResponse( logger, "solver_specific_parameters is not a valid binary stream of the " "GLOPParameters proto"); } } else { if (!ProtobufTextFormatMergeFromString( - request.solver_specific_parameters(), ¶ms)) { - return ModelInvalidResponse( + request->solver_specific_parameters(), ¶ms)) { + return ModelInvalidParametersResponse( 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; + if (request->has_solver_time_limit_seconds()) { + params.set_max_time_in_seconds(request->solver_time_limit_seconds()); } { const std::string error = glop::ValidateParameters(params); if (!error.empty()) { - return ModelInvalidResponse( + return ModelInvalidParametersResponse( logger, absl::StrCat("Invalid Glop parameters: ", error)); } } + MPSolutionResponse response; glop::LinearProgram linear_program; - MPModelProtoToLinearProgram(request.model(), &linear_program); + + // Model validation and delta handling. + { + std::optional> optional_model = + GetMPModelOrPopulateResponse(request, &response); + if (!optional_model) return response; + + const MPModelProto& mp_model = **optional_model; + if (!mp_model.general_constraint().empty()) { + return ModelInvalidResponse(logger, + "GLOP does not support general constraints"); + } + + // Convert and clear the request and mp_model as it is no longer needed. + MPModelProtoToLinearProgram(mp_model, &linear_program); + std::move(request).dispose(); + } glop::LPSolver lp_solver; lp_solver.SetParameters(params); @@ -189,7 +204,7 @@ MPSolutionResponse GlopSolveProto( if (result_status == MPSOLVER_OPTIMAL || result_status == MPSOLVER_FEASIBLE) { response.set_objective_value(lp_solver.GetObjectiveValue()); - const int num_vars = request.model().variable_size(); + const int num_vars = linear_program.num_variables().value(); for (int var_id = 0; var_id < num_vars; ++var_id) { const glop::Fractional solution_value = lp_solver.variable_values()[glop::ColIndex(var_id)]; @@ -206,7 +221,7 @@ MPSolutionResponse GlopSolveProto( response.set_status(MPSOLVER_CANCELLED_BY_USER); } - const size_t num_constraints = request.model().constraint_size(); + const int num_constraints = linear_program.num_constraints().value(); for (int ct_id = 0; ct_id < num_constraints; ++ct_id) { const glop::Fractional dual_value = lp_solver.dual_values()[glop::RowIndex(ct_id)]; diff --git a/ortools/linear_solver/proto_solver/glop_proto_solver.h b/ortools/linear_solver/proto_solver/glop_proto_solver.h index 4efa110c84..f3704dc488 100644 --- a/ortools/linear_solver/proto_solver/glop_proto_solver.h +++ b/ortools/linear_solver/proto_solver/glop_proto_solver.h @@ -20,6 +20,7 @@ #include "ortools/glop/parameters.pb.h" #include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/util/lazy_mutable_copy.h" namespace operations_research { @@ -44,7 +45,8 @@ namespace operations_research { // 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, + LazyMutableCopy request, + std::atomic* interrupt_solve = nullptr, std::function logging_callback = nullptr); // Returns a string that describes the version of the GLOP solver. diff --git a/ortools/linear_solver/proto_solver/gurobi_proto_solver.cc b/ortools/linear_solver/proto_solver/gurobi_proto_solver.cc index 8ff8e2e89c..eb31bbcf7b 100644 --- a/ortools/linear_solver/proto_solver/gurobi_proto_solver.cc +++ b/ortools/linear_solver/proto_solver/gurobi_proto_solver.cc @@ -272,12 +272,12 @@ absl::Status SetSolverSpecificParameters(absl::string_view parameters, } absl::StatusOr GurobiSolveProto( - const MPModelRequest& request, GRBenv* gurobi_env) { + LazyMutableCopy request, GRBenv* gurobi_env) { MPSolutionResponse response; const absl::optional> optional_model = - ExtractValidMPModelOrPopulateResponseStatus(request, &response); + GetMPModelOrPopulateResponse(request, &response); if (!optional_model) return response; - const MPModelProto& model = optional_model->get(); + const MPModelProto& model = **optional_model; // We set `gurobi_env` to point to a new environment if no existing one is // provided. We must make sure that we free this environment when we exit this @@ -316,9 +316,9 @@ absl::StatusOr GurobiSolveProto( /*varnames=*/nullptr)); GRBenv* const model_env = GRBgetenv(gurobi_model); - if (request.has_solver_specific_parameters()) { + if (request->has_solver_specific_parameters()) { const auto parameters_status = SetSolverSpecificParameters( - request.solver_specific_parameters(), model_env); + request->solver_specific_parameters(), model_env); if (!parameters_status.ok()) { response.set_status(MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS); response.set_status_str( @@ -326,13 +326,14 @@ absl::StatusOr GurobiSolveProto( return response; } } - if (request.solver_time_limit_seconds() > 0) { - RETURN_IF_GUROBI_ERROR(GRBsetdblparam(model_env, GRB_DBL_PAR_TIMELIMIT, - request.solver_time_limit_seconds())); + if (request->solver_time_limit_seconds() > 0) { + RETURN_IF_GUROBI_ERROR( + GRBsetdblparam(model_env, GRB_DBL_PAR_TIMELIMIT, + request->solver_time_limit_seconds())); } RETURN_IF_GUROBI_ERROR( GRBsetintparam(model_env, GRB_INT_PAR_OUTPUTFLAG, - request.enable_internal_solver_output())); + request->enable_internal_solver_output())); const int variable_size = model.variable_size(); bool has_integer_variables = false; @@ -348,7 +349,7 @@ absl::StatusOr GurobiSolveProto( lb[v] = variable.lower_bound(); ub[v] = variable.upper_bound(); ctype[v] = variable.is_integer() && - request.solver_type() == + request->solver_type() == MPModelRequest::GUROBI_MIXED_INTEGER_PROGRAMMING ? GRB_INTEGER : GRB_CONTINUOUS; @@ -575,7 +576,7 @@ absl::StatusOr GurobiSolveProto( response.mutable_dual_value()->mutable_data())); } const int additional_solutions = std::min( - solution_count, std::min(request.populate_additional_solutions_up_to(), + solution_count, std::min(request->populate_additional_solutions_up_to(), std::numeric_limits::max() - 1) + 1); for (int i = 1; i < additional_solutions; ++i) { diff --git a/ortools/linear_solver/proto_solver/gurobi_proto_solver.h b/ortools/linear_solver/proto_solver/gurobi_proto_solver.h index be8c98fc70..c91301108f 100644 --- a/ortools/linear_solver/proto_solver/gurobi_proto_solver.h +++ b/ortools/linear_solver/proto_solver/gurobi_proto_solver.h @@ -21,6 +21,7 @@ #include "absl/strings/string_view.h" #include "ortools/gurobi/environment.h" #include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/util/lazy_mutable_copy.h" namespace operations_research { @@ -34,11 +35,11 @@ namespace operations_research { // Please note though that the provided environment should not be actively used // by another thread at the same time. absl::StatusOr GurobiSolveProto( - const MPModelRequest& request, GRBenv* gurobi_env = nullptr); + LazyMutableCopy request, GRBenv* gurobi_env = nullptr); // Set parameters specified in the string. The format of the string is a series // of tokens separated by either '\n' or by ',' characters. -// Any token whose first character is a '#' or has zero length is skiped. +// Any token whose first character is a '#' or has zero length is skipped. // Comment tokens (i.e. those starting with #) can contain ',' characters. // Any other token has the form: // parameter_name(separator)value diff --git a/ortools/linear_solver/proto_solver/highs_proto_solver.cc b/ortools/linear_solver/proto_solver/highs_proto_solver.cc index f3119373d8..d00189abdf 100644 --- a/ortools/linear_solver/proto_solver/highs_proto_solver.cc +++ b/ortools/linear_solver/proto_solver/highs_proto_solver.cc @@ -26,10 +26,12 @@ #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/linear_solver/model_validator.h" #include "ortools/port/proto_utils.h" +#include "ortools/util/lazy_mutable_copy.h" namespace operations_research { -absl::StatusOr HighsSolveProto(MPModelRequest request) { +absl::StatusOr HighsSolveProto( + LazyMutableCopy request) { return absl::UnimplementedError("Highs support is not yet implemented"); } diff --git a/ortools/linear_solver/proto_solver/highs_proto_solver.h b/ortools/linear_solver/proto_solver/highs_proto_solver.h index cf94d22e10..a9abbf965d 100644 --- a/ortools/linear_solver/proto_solver/highs_proto_solver.h +++ b/ortools/linear_solver/proto_solver/highs_proto_solver.h @@ -19,11 +19,13 @@ #include "absl/status/statusor.h" #include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/util/lazy_mutable_copy.h" namespace operations_research { // Solve the input MIP model with the HIGHS solver. -absl::StatusOr HighsSolveProto(MPModelRequest request); +absl::StatusOr HighsSolveProto( + LazyMutableCopy request); } // namespace operations_research diff --git a/ortools/linear_solver/proto_solver/pdlp_proto_solver.cc b/ortools/linear_solver/proto_solver/pdlp_proto_solver.cc index 10f71fdff6..a14f7eab38 100644 --- a/ortools/linear_solver/proto_solver/pdlp_proto_solver.cc +++ b/ortools/linear_solver/proto_solver/pdlp_proto_solver.cc @@ -34,53 +34,49 @@ namespace operations_research { absl::StatusOr PdlpSolveProto( - const MPModelRequest& request, const bool relax_integer_variables, + LazyMutableCopy request, const bool relax_integer_variables, const std::atomic* interrupt_solve) { pdlp::PrimalDualHybridGradientParams params; - if (request.enable_internal_solver_output()) { + if (request->enable_internal_solver_output()) { params.set_verbosity_level(3); } else { params.set_verbosity_level(0); } - MPSolutionResponse error_response; - if (!ProtobufTextFormatMergeFromString(request.solver_specific_parameters(), + MPSolutionResponse response; + if (!ProtobufTextFormatMergeFromString(request->solver_specific_parameters(), ¶ms)) { - error_response.set_status( + response.set_status( MPSolverResponseStatus::MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS); - return error_response; + return response; } if (interrupt_solve != nullptr && interrupt_solve->load() == true) { - error_response.set_status(MPSolverResponseStatus::MPSOLVER_NOT_SOLVED); - return error_response; + response.set_status(MPSolverResponseStatus::MPSOLVER_NOT_SOLVED); + return response; } - if (request.has_solver_time_limit_seconds()) { + if (request->has_solver_time_limit_seconds()) { params.mutable_termination_criteria()->set_time_sec_limit( - request.solver_time_limit_seconds()); + request->solver_time_limit_seconds()); } - const absl::optional> optional_model = - ExtractValidMPModelOrPopulateResponseStatus(request, &error_response); - if (!optional_model) { - LOG_IF(WARNING, request.enable_internal_solver_output()) - << "Failed to extract a valid model from protocol buffer. Status: " - << ProtoEnumToString(error_response.status()) - << " (" << error_response.status() - << "): " << error_response.status_str(); - return error_response; - } + std::optional> optional_model = + GetMPModelOrPopulateResponse(request, &response); + if (!optional_model) return response; ASSIGN_OR_RETURN( pdlp::QuadraticProgram qp, - pdlp::QpFromMpModelProto(optional_model->get(), relax_integer_variables)); - const double objective_scaling_factor = qp.objective_scaling_factor; + pdlp::QpFromMpModelProto(**optional_model, relax_integer_variables)); + // We can now clear the request and optional_model. + std::move(request).dispose(); + optional_model.reset(); + + const double objective_scaling_factor = qp.objective_scaling_factor; pdlp::SolverResult pdhg_result = pdlp::PrimalDualHybridGradient(std::move(qp), params, interrupt_solve); // PDLP's statuses don't map very cleanly to MPSolver statuses. Do the best // we can for now. - MPSolutionResponse response; switch (pdhg_result.solve_log.termination_reason()) { case pdlp::TERMINATION_REASON_OPTIMAL: response.set_status(MPSOLVER_OPTIMAL); diff --git a/ortools/linear_solver/proto_solver/pdlp_proto_solver.h b/ortools/linear_solver/proto_solver/pdlp_proto_solver.h index 2c30b55036..b2b0dd4357 100644 --- a/ortools/linear_solver/proto_solver/pdlp_proto_solver.h +++ b/ortools/linear_solver/proto_solver/pdlp_proto_solver.h @@ -18,6 +18,7 @@ #include "absl/status/statusor.h" #include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/util/lazy_mutable_copy.h" namespace operations_research { @@ -25,6 +26,9 @@ namespace operations_research { // MPModelRequest. Users of this interface should be aware of the size // limitations of MPModelProto (see, e.g., large_linear_program.proto). // +// If possible, std::move the request into this function call so that its +// memory can be reclaimed early. +// // The optional interrupt_solve can be used to interrupt the solve early. The // solver will periodically check its value and stop if it holds true. // @@ -37,7 +41,8 @@ namespace operations_research { // pdlp::QuadraticProgram fails. The lack of an error does not imply success. // Check the SolveLog's termination_reason for more refined status details. absl::StatusOr PdlpSolveProto( - const MPModelRequest& request, bool relax_integer_variables = false, + LazyMutableCopy request, + bool relax_integer_variables = false, const std::atomic* interrupt_solve = nullptr); } // namespace operations_research diff --git a/ortools/linear_solver/proto_solver/proto_utils.h b/ortools/linear_solver/proto_solver/proto_utils.h index 9184e7c46f..5561634b73 100644 --- a/ortools/linear_solver/proto_solver/proto_utils.h +++ b/ortools/linear_solver/proto_solver/proto_utils.h @@ -16,9 +16,13 @@ #include #include +#include #include "absl/log/check.h" +#include "absl/status/statusor.h" #include "google/protobuf/message.h" +#include "ortools/base/logging.h" +#include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/port/proto_utils.h" namespace operations_research { @@ -29,6 +33,25 @@ using google::protobuf::Message; using google::protobuf::Message; #endif +// Some SolveWithProto() returns a StatusOr, this utility +// just convert bad absl::StatusOr to a proper error in MPModelResponse. +// +// TODO(user): All SolveWithProto() should just fill the appropriate response +// instead. +inline MPSolutionResponse ConvertStatusOrMPSolutionResponse( + bool log_error, absl::StatusOr response) { + if (!response.ok()) { + if (log_error) { + LOG(ERROR) << "Error status: " << response.status(); + } + MPSolutionResponse error_response; + error_response.set_status(MPSolverResponseStatus::MPSOLVER_ABNORMAL); + error_response.set_status_str(response.status().ToString()); + return error_response; + } + return std::move(response).value(); +} + // Returns a string that should be used in MPModelRequest's // solver_specific_parameters field to encode the glop parameters. // diff --git a/ortools/linear_solver/proto_solver/sat_proto_solver.cc b/ortools/linear_solver/proto_solver/sat_proto_solver.cc index c5c44fb71d..d95f218c8b 100644 --- a/ortools/linear_solver/proto_solver/sat_proto_solver.cc +++ b/ortools/linear_solver/proto_solver/sat_proto_solver.cc @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +45,7 @@ #include "ortools/sat/model.h" #include "ortools/sat/parameters_validation.h" #include "ortools/sat/sat_parameters.pb.h" +#include "ortools/util/lazy_mutable_copy.h" #include "ortools/util/logging.h" #include "ortools/util/time_limit.h" @@ -140,11 +142,11 @@ MPSolutionResponse InvalidParametersResponse(SolverLogger& logger, } // namespace MPSolutionResponse SatSolveProto( - MPModelRequest request, std::atomic* interrupt_solve, + LazyMutableCopy 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()); + 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 @@ -160,10 +162,10 @@ MPSolutionResponse SatSolveProto( 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()) { + if (request->has_solver_specific_parameters()) { // See EncodeSatParametersAsString() documentation. if constexpr (!std::is_base_of::value) { - if (!params.MergeFromString(request.solver_specific_parameters())) { + if (!params.MergeFromString(request->solver_specific_parameters())) { return InvalidParametersResponse( logger, "solver_specific_parameters is not a valid binary stream of the " @@ -171,7 +173,7 @@ MPSolutionResponse SatSolveProto( } } else { if (!ProtobufTextFormatMergeFromString( - request.solver_specific_parameters(), ¶ms)) { + request->solver_specific_parameters(), ¶ms)) { return InvalidParametersResponse( logger, "solver_specific_parameters is not a valid textual representation " @@ -195,14 +197,15 @@ MPSolutionResponse SatSolveProto( 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()); + 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, - &response)) { + std::optional> optional_model = + GetMPModelOrPopulateResponse(request, &response); + if (!optional_model) { // Note that the ExtractValidMPModelInPlaceOrPopulateResponseStatus() can // also close trivial model (empty or trivially infeasible). So this is not // always the MODEL_INVALID status. @@ -217,12 +220,20 @@ MPSolutionResponse SatSolveProto( return response; } + // We will presolve directly on the MPModelProto, so get a copy or transfer + // ownership from the LazyMutableCopy(). + std::unique_ptr mp_model = + std::move(optional_model).value().copy_or_move_as_unique_ptr(); + + // The request is no longer needed after this. + // Important: we need to copy the model above before clearing this. + std::move(request).dispose(); + // We start by some extra validation since our code do not accept any kind // of input. - MPModelProto* const mp_model = request.mutable_model(); if (params.mip_treat_high_magnitude_bounds_as_infinity()) { - sat::ChangeLargeBoundsToInfinity(params.mip_max_valid_magnitude(), mp_model, - &logger); + sat::ChangeLargeBoundsToInfinity(params.mip_max_valid_magnitude(), + mp_model.get(), &logger); } if (!sat::MPModelProtoValidationBeforeConversion(params, *mp_model, &logger)) { @@ -230,22 +241,23 @@ MPSolutionResponse SatSolveProto( } // This is good to do before any presolve. - if (!sat::MakeBoundsOfIntegerVariablesInteger(params, mp_model, &logger)) { + if (!sat::MakeBoundsOfIntegerVariablesInteger(params, mp_model.get(), + &logger)) { return InfeasibleResponse(logger, "An integer variable has an empty domain"); } // Coefficients really close to zero can cause issues. // We remove them right away according to our parameters. - RemoveNearZeroTerms(params, mp_model, &logger); + RemoveNearZeroTerms(params, mp_model.get(), &logger); // Note(user): the LP presolvers API is a bit weird and keep a reference to // the given GlopParameters, so we need to make sure it outlive them. const glop::GlopParameters glop_params; std::vector> for_postsolve; if (!params.enumerate_all_solutions() && params.mip_presolve_level() > 0) { - const glop::ProblemStatus status = - ApplyMipPresolveSteps(glop_params, mp_model, &for_postsolve, &logger); + const glop::ProblemStatus status = ApplyMipPresolveSteps( + glop_params, mp_model.get(), &for_postsolve, &logger); switch (status) { case glop::ProblemStatus::INIT: // Continue with the solve. @@ -276,7 +288,7 @@ MPSolutionResponse SatSolveProto( } // We need to do that before the automatic detection of integers. - RemoveNearZeroTerms(params, mp_model, &logger); + RemoveNearZeroTerms(params, mp_model.get(), &logger); SOLVER_LOG(&logger, ""); SOLVER_LOG(&logger, "Scaling to pure integer problem."); @@ -284,8 +296,9 @@ MPSolutionResponse SatSolveProto( const int num_variables = mp_model->variable_size(); std::vector var_scaling(num_variables, 1.0); if (params.mip_automatically_scale_variables()) { - var_scaling = sat::DetectImpliedIntegers(mp_model, &logger); - if (!sat::MakeBoundsOfIntegerVariablesInteger(params, mp_model, &logger)) { + var_scaling = sat::DetectImpliedIntegers(mp_model.get(), &logger); + if (!sat::MakeBoundsOfIntegerVariablesInteger(params, mp_model.get(), + &logger)) { return InfeasibleResponse( logger, "A detected integer variable has an empty domain"); } @@ -295,7 +308,7 @@ MPSolutionResponse SatSolveProto( ? std::numeric_limits::infinity() : params.mip_max_bound(); const std::vector other_scaling = sat::ScaleContinuousVariables( - params.mip_var_scaling(), max_bound, mp_model); + params.mip_var_scaling(), max_bound, mp_model.get()); for (int i = 0; i < var_scaling.size(); ++i) { var_scaling[i] *= other_scaling[i]; } @@ -330,18 +343,17 @@ MPSolutionResponse SatSolveProto( DCHECK_EQ(cp_model.variables().size(), mp_model->variable().size()); // Copy and scale the hint if there is one. - if (request.model().has_solution_hint()) { + if (mp_model->has_solution_hint()) { auto* cp_model_hint = cp_model.mutable_solution_hint(); - const int size = request.model().solution_hint().var_index().size(); + const int size = mp_model->solution_hint().var_index().size(); for (int i = 0; i < size; ++i) { - const int var = request.model().solution_hint().var_index(i); + const int var = mp_model->solution_hint().var_index(i); if (var >= var_scaling.size()) continue; // To handle weird hint input values, we cap any large value to +/- // mip_max_bound() which is also the min/max value of any variable once // scaled. - double value = - request.model().solution_hint().var_value(i) * var_scaling[var]; + double value = mp_model->solution_hint().var_value(i) * var_scaling[var]; if (std::abs(value) > params.mip_max_bound()) { value = value > 0 ? params.mip_max_bound() : -params.mip_max_bound(); } @@ -351,10 +363,11 @@ MPSolutionResponse SatSolveProto( } } - // We no longer need the request. Reclaim its memory. + // We no longer need the mp_model after this, reclaime its memory. const int old_num_variables = mp_model->variable().size(); const int old_num_constraints = mp_model->constraint().size(); - request.Clear(); + const bool is_maximize = mp_model->maximize(); + mp_model.reset(); // Configure model. sat::Model sat_model; @@ -438,7 +451,6 @@ MPSolutionResponse SatSolveProto( temp.set_objective_value(obj); *response.add_additional_solutions() = post_solve(temp); } - const bool is_maximize = request.model().maximize(); std::sort(response.mutable_additional_solutions()->pointer_begin(), response.mutable_additional_solutions()->pointer_end(), [is_maximize](const MPSolution* left, const MPSolution* right) { diff --git a/ortools/linear_solver/proto_solver/sat_proto_solver.h b/ortools/linear_solver/proto_solver/sat_proto_solver.h index a038d009b3..ca4a5c2dc1 100644 --- a/ortools/linear_solver/proto_solver/sat_proto_solver.h +++ b/ortools/linear_solver/proto_solver/sat_proto_solver.h @@ -19,6 +19,7 @@ #include #include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/util/lazy_mutable_copy.h" #include "ortools/util/logging.h" namespace operations_research { @@ -49,7 +50,8 @@ namespace operations_research { // threads, but it will ensure that at most one thread executes // solution_callback at a time. MPSolutionResponse SatSolveProto( - MPModelRequest request, std::atomic* interrupt_solve = nullptr, + LazyMutableCopy request, + std::atomic* interrupt_solve = nullptr, std::function logging_callback = nullptr, std::function solution_callback = nullptr); diff --git a/ortools/linear_solver/proto_solver/scip_proto_solver.cc b/ortools/linear_solver/proto_solver/scip_proto_solver.cc index 587989349a..f07b419a58 100644 --- a/ortools/linear_solver/proto_solver/scip_proto_solver.cc +++ b/ortools/linear_solver/proto_solver/scip_proto_solver.cc @@ -690,12 +690,13 @@ std::string FindErrorInMPModelForScip(const MPModelProto& model, SCIP* scip) { } absl::StatusOr ScipSolveProto( - const MPModelRequest& request) { + LazyMutableCopy request) { MPSolutionResponse response; const absl::optional> optional_model = - ExtractValidMPModelOrPopulateResponseStatus(request, &response); + GetMPModelOrPopulateResponse(request, &response); if (!optional_model) return response; - const MPModelProto& model = optional_model->get(); + const MPModelProto& model = **optional_model; + SCIP* scip = nullptr; std::vector scip_variables(model.variable_size(), nullptr); std::vector scip_constraints( @@ -734,7 +735,7 @@ absl::StatusOr ScipSolveProto( } const auto parameters_status = LegacyScipSetSolverSpecificParameters( - request.solver_specific_parameters(), scip); + request->solver_specific_parameters(), scip); if (!parameters_status.ok()) { response.set_status(MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS); response.set_status_str( @@ -749,12 +750,12 @@ absl::StatusOr ScipSolveProto( // running SCIP with time limit 10s each will both terminate after ~5s. RETURN_IF_SCIP_ERROR( SCIPsetIntParam(scip, "timing/clocktype", SCIP_CLOCKTYPE_WALL)); - if (request.solver_time_limit_seconds() > 0 && - request.solver_time_limit_seconds() < 1e20) { - RETURN_IF_SCIP_ERROR(SCIPsetRealParam(scip, "limits/time", - request.solver_time_limit_seconds())); + if (request->solver_time_limit_seconds() > 0 && + request->solver_time_limit_seconds() < 1e20) { + RETURN_IF_SCIP_ERROR(SCIPsetRealParam( + scip, "limits/time", request->solver_time_limit_seconds())); } - SCIPsetMessagehdlrQuiet(scip, !request.enable_internal_solver_output()); + SCIPsetMessagehdlrQuiet(scip, !request->enable_internal_solver_output()); RETURN_IF_SCIP_ERROR(SCIPcreateProbBasic(scip, model.name().c_str())); if (model.maximize()) { @@ -901,7 +902,7 @@ absl::StatusOr ScipSolveProto( const int solution_count = std::min(SCIPgetNSols(scip), - std::min(request.populate_additional_solutions_up_to(), + std::min(request->populate_additional_solutions_up_to(), std::numeric_limits::max() - 1) + 1); if (solution_count > 0) { diff --git a/ortools/linear_solver/proto_solver/scip_proto_solver.h b/ortools/linear_solver/proto_solver/scip_proto_solver.h index 17fae5edc5..50bcfc43a7 100644 --- a/ortools/linear_solver/proto_solver/scip_proto_solver.h +++ b/ortools/linear_solver/proto_solver/scip_proto_solver.h @@ -18,6 +18,7 @@ #include "absl/status/statusor.h" #include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/util/lazy_mutable_copy.h" #include "scip/type_scip.h" namespace operations_research { @@ -27,7 +28,7 @@ namespace operations_research { // 1e-7, and the gap limit to 0.0001 (whereas SCIP defaults are 1e-6 and 0, // respectively, and they are being used here). absl::StatusOr ScipSolveProto( - const MPModelRequest& request); + LazyMutableCopy request); std::string FindErrorInMPModelForScip(const MPModelProto& model, SCIP* scip); diff --git a/ortools/linear_solver/proto_solver/xpress_proto_solver.cc b/ortools/linear_solver/proto_solver/xpress_proto_solver.cc index d56b770cc9..5071c68d9e 100644 --- a/ortools/linear_solver/proto_solver/xpress_proto_solver.cc +++ b/ortools/linear_solver/proto_solver/xpress_proto_solver.cc @@ -280,7 +280,8 @@ namespace operations_research { // return absl::StrJoin(error_messages, "\n"); // } -MPSolutionResponse XPressSolveProto(const MPModelRequest& request) { +MPSolutionResponse XPressSolveProto( + LazyMutableCopy request) { MPSolutionResponse response; response.set_status(MPSolverResponseStatus::MPSOLVER_SOLVER_TYPE_UNAVAILABLE); diff --git a/ortools/linear_solver/proto_solver/xpress_proto_solver.h b/ortools/linear_solver/proto_solver/xpress_proto_solver.h index e3b339dab2..02b8b99b32 100644 --- a/ortools/linear_solver/proto_solver/xpress_proto_solver.h +++ b/ortools/linear_solver/proto_solver/xpress_proto_solver.h @@ -14,14 +14,14 @@ #ifndef OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_XPRESS_PROTO_SOLVER_H_ #define OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_XPRESS_PROTO_SOLVER_H_ -#include - #include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/util/lazy_mutable_copy.h" namespace operations_research { // Solves the input request. -MPSolutionResponse XPressSolveProto(const MPModelRequest& request); +MPSolutionResponse XPressSolveProto( + LazyMutableCopy request); } // namespace operations_research diff --git a/ortools/linear_solver/python/linear_solver_natural_api.py b/ortools/linear_solver/python/linear_solver_natural_api.py index 2d92391bd6..f869e8e9e3 100644 --- a/ortools/linear_solver/python/linear_solver_natural_api.py +++ b/ortools/linear_solver/python/linear_solver_natural_api.py @@ -30,7 +30,7 @@ import numbers inf = float("inf") -class _FakeMPVariableRepresentingTheConstantOffset(object): +class _FakeMPVariableRepresentingTheConstantOffset: """A dummy class for a singleton instance used to represent the constant. To represent linear expressions, we store a dictionary @@ -56,7 +56,7 @@ def CastToLinExp(v): return v -class LinearExpr(object): +class LinearExpr: """Holds linear expressions. A linear expression is essentially an offset (floating-point value), and a @@ -178,6 +178,9 @@ class VariableExpr(LinearExpr): def __init__(self, mpvar): self.__var = mpvar + def __str__(self): + return str(self.__var) + def AddSelfToCoeffMapOrStack(self, coeffs, multiplier, stack): coeffs[self.__var] += multiplier @@ -222,7 +225,16 @@ class SumArray(LinearExpr): self.__array = [CastToLinExp(elem) for elem in array] def __str__(self): - return "({})".format(" + ".join(map(str, self.__array))) + parts = [] + for term in map(str, self.__array): + if not parts: + parts.append(term) + continue + if term[0] == "-": + parts.append(" - " + term[1:]) + else: + parts.append(" + " + term) + return f'({"".join(parts)})' def AddSelfToCoeffMapOrStack(self, coeffs, multiplier, stack): # Append elements in reversed order so that the first popped from the stack @@ -240,7 +252,7 @@ def Sum(*args): SumCst = Sum # pylint: disable=invalid-name -class LinearConstraint(object): +class LinearConstraint: """Represents a linear constraint: LowerBound <= LinearExpr <= UpperBound.""" def __init__(self, expr, lb, ub): diff --git a/ortools/linear_solver/samples/BUILD.bazel b/ortools/linear_solver/samples/BUILD.bazel index e273128b33..d8dd4836ab 100644 --- a/ortools/linear_solver/samples/BUILD.bazel +++ b/ortools/linear_solver/samples/BUILD.bazel @@ -28,6 +28,8 @@ code_sample_cc(name = "mip_var_array") code_sample_cc(name = "multiple_knapsack_mip") +#code_sample_cc(name = "network_design_ilph") + code_sample_cc(name = "simple_lp_program") code_sample_cc(name = "simple_mip_program") diff --git a/ortools/linear_solver/samples/BasicExample.cs b/ortools/linear_solver/samples/BasicExample.cs index 8aa512a483..6f7c4b6527 100644 --- a/ortools/linear_solver/samples/BasicExample.cs +++ b/ortools/linear_solver/samples/BasicExample.cs @@ -15,6 +15,7 @@ // [START program] // [START import] using System; +using Google.OrTools.Init; using Google.OrTools.LinearSolver; // [END import] @@ -22,11 +23,14 @@ public class BasicExample { static void Main() { + Console.WriteLine("Google.OrTools version: " + OrToolsVersion.VersionString()); + // [START solver] // Create the linear solver with the GLOP backend. Solver solver = Solver.CreateSolver("GLOP"); if (solver is null) { + Console.WriteLine("Could not create solver GLOP"); return; } // [END solver] @@ -40,10 +44,10 @@ public class BasicExample // [END variables] // [START constraints] - // Create a linear constraint, 0 <= x + y <= 2. - Constraint ct = solver.MakeConstraint(0.0, 2.0, "ct"); - ct.SetCoefficient(x, 1); - ct.SetCoefficient(y, 1); + // Create a linear constraint, x + y <= 2. + Constraint constraint = solver.MakeConstraint(double.NegativeInfinity, 2.0, "constraint"); + constraint.SetCoefficient(x, 1); + constraint.SetCoefficient(y, 1); Console.WriteLine("Number of constraints = " + solver.NumConstraints()); // [END constraints] @@ -57,15 +61,37 @@ public class BasicExample // [END objective] // [START solve] - solver.Solve(); + Console.WriteLine("Solving with " + solver.SolverVersion()); + Solver.ResultStatus resultStatus = solver.Solve(); // [END solve] // [START print_solution] + Console.WriteLine("Status: " + resultStatus); + if (resultStatus != Solver.ResultStatus.OPTIMAL) + { + Console.WriteLine("The problem does not have an optimal solution!"); + if (resultStatus == Solver.ResultStatus.FEASIBLE) + { + Console.WriteLine("A potentially suboptimal solution was found"); + } + else + { + Console.WriteLine("The solver could not solve the problem."); + return; + } + } + Console.WriteLine("Solution:"); Console.WriteLine("Objective value = " + solver.Objective().Value()); Console.WriteLine("x = " + x.SolutionValue()); Console.WriteLine("y = " + y.SolutionValue()); // [END print_solution] + + // [START advanced] + Console.WriteLine("Advanced usage:"); + Console.WriteLine("Problem solved in " + solver.WallTime() + " milliseconds"); + Console.WriteLine("Problem solved in " + solver.Iterations() + " iterations"); + // [END advanced] } } // [END program] diff --git a/ortools/linear_solver/samples/BasicExample.java b/ortools/linear_solver/samples/BasicExample.java index 563c79085a..7b80279a4f 100644 --- a/ortools/linear_solver/samples/BasicExample.java +++ b/ortools/linear_solver/samples/BasicExample.java @@ -14,8 +14,10 @@ // Minimal example to call the GLOP solver. // [START program] package com.google.ortools.linearsolver.samples; + // [START import] import com.google.ortools.Loader; +import com.google.ortools.init.OrToolsVersion; import com.google.ortools.linearsolver.MPConstraint; import com.google.ortools.linearsolver.MPObjective; import com.google.ortools.linearsolver.MPSolver; @@ -25,10 +27,19 @@ import com.google.ortools.linearsolver.MPVariable; /** Minimal Linear Programming example to showcase calling the solver. */ public final class BasicExample { public static void main(String[] args) { + // [START loader] Loader.loadNativeLibraries(); + // [END loader] + + System.out.println("Google OR-Tools version: " + OrToolsVersion.getVersionString()); + // [START solver] // Create the linear solver with the GLOP backend. MPSolver solver = MPSolver.createSolver("GLOP"); + if (solver == null) { + System.out.println("Could not create solver GLOP"); + return; + } // [END solver] // [START variables] @@ -40,8 +51,9 @@ public final class BasicExample { // [END variables] // [START constraints] - // Create a linear constraint, 0 <= x + y <= 2. - MPConstraint ct = solver.makeConstraint(0.0, 2.0, "ct"); + double infinity = Double.POSITIVE_INFINITY; + // Create a linear constraint, x + y <= 2. + MPConstraint ct = solver.makeConstraint(-infinity, 2.0, "ct"); ct.setCoefficient(x, 1); ct.setCoefficient(y, 1); @@ -57,15 +69,33 @@ public final class BasicExample { // [END objective] // [START solve] - solver.solve(); + System.out.println("Solving with " + solver.solverVersion()); + final MPSolver.ResultStatus resultStatus = solver.solve(); // [END solve] // [START print_solution] + System.out.println("Status: " + resultStatus); + if (resultStatus != MPSolver.ResultStatus.OPTIMAL) { + System.out.println("The problem does not have an optimal solution!"); + if (resultStatus == MPSolver.ResultStatus.FEASIBLE) { + System.out.println("A potentially suboptimal solution was found"); + } else { + System.out.println("The solver could not solve the problem."); + return; + } + } + System.out.println("Solution:"); System.out.println("Objective value = " + objective.value()); System.out.println("x = " + x.solutionValue()); System.out.println("y = " + y.solutionValue()); // [END print_solution] + + // [START advanced] + System.out.println("Advanced usage:"); + System.out.println("Problem solved in " + solver.wallTime() + " milliseconds"); + System.out.println("Problem solved in " + solver.iterations() + " iterations"); + // [END advanced] } private BasicExample() {} diff --git a/ortools/linear_solver/samples/basic_example.cc b/ortools/linear_solver/samples/basic_example.cc index 0f46c5440d..22e1dc77e9 100644 --- a/ortools/linear_solver/samples/basic_example.cc +++ b/ortools/linear_solver/samples/basic_example.cc @@ -11,20 +11,31 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Minimal example to call the GLOP solver. // [START program] +// Minimal example to call the GLOP solver. // [START import] +#include #include -#include +#include "absl/flags/flag.h" +#include "absl/log/flags.h" +#include "ortools/base/init_google.h" +#include "ortools/base/logging.h" +#include "ortools/init/init.h" #include "ortools/linear_solver/linear_solver.h" // [END import] namespace operations_research { void BasicExample() { + LOG(INFO) << "Google OR-Tools version : " << OrToolsVersion::VersionString(); + // [START solver] // Create the linear solver with the GLOP backend. std::unique_ptr solver(MPSolver::CreateSolver("GLOP")); + if (!solver) { + LOG(WARNING) << "Could not create solver GLOP"; + return; + } // [END solver] // [START variables] @@ -36,8 +47,9 @@ void BasicExample() { // [END variables] // [START constraints] - // Create a linear constraint, 0 <= x + y <= 2. - MPConstraint* const ct = solver->MakeRowConstraint(0.0, 2.0, "ct"); + // Create a linear constraint, x + y <= 2. + const double infinity = solver->infinity(); + MPConstraint* const ct = solver->MakeRowConstraint(-infinity, 2.0, "ct"); ct->SetCoefficient(x, 1); ct->SetCoefficient(y, 1); @@ -53,19 +65,40 @@ void BasicExample() { // [END objective] // [START solve] - solver->Solve(); + LOG(INFO) << "Solving with " << solver->SolverVersion(); + const MPSolver::ResultStatus result_status = solver->Solve(); // [END solve] // [START print_solution] - LOG(INFO) << "Solution:" << std::endl; + // Check that the problem has an optimal solution. + LOG(INFO) << "Status: " << result_status; + if (result_status != MPSolver::OPTIMAL) { + LOG(INFO) << "The problem does not have an optimal solution!"; + if (result_status == MPSolver::FEASIBLE) { + LOG(INFO) << "A potentially suboptimal solution was found"; + } else { + LOG(WARNING) << "The solver could not solve the problem."; + return; + } + } + + LOG(INFO) << "Solution:"; LOG(INFO) << "Objective value = " << objective->Value(); LOG(INFO) << "x = " << x->solution_value(); LOG(INFO) << "y = " << y->solution_value(); // [END print_solution] + + // [START advanced] + LOG(INFO) << "Advanced usage:"; + LOG(INFO) << "Problem solved in " << solver->wall_time() << " milliseconds"; + LOG(INFO) << "Problem solved in " << solver->iterations() << " iterations"; + // [END advanced] } } // namespace operations_research -int main() { +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetFlag(&FLAGS_stderrthreshold, 0); operations_research::BasicExample(); return EXIT_SUCCESS; } diff --git a/ortools/linear_solver/samples/basic_example.py b/ortools/linear_solver/samples/basic_example.py index 2f3530b01b..1a2fbaced0 100644 --- a/ortools/linear_solver/samples/basic_example.py +++ b/ortools/linear_solver/samples/basic_example.py @@ -15,31 +15,36 @@ """Minimal example to call the GLOP solver.""" # [START program] # [START import] +from ortools.init.python import init from ortools.linear_solver import pywraplp # [END import] def main(): + print("Google OR-Tools version:", init.OrToolsVersion.version_string()) + # [START solver] # Create the linear solver with the GLOP backend. solver = pywraplp.Solver.CreateSolver("GLOP") if not solver: + print("Could not create solver GLOP") return # [END solver] # [START variables] # Create the variables x and y. - x = solver.NumVar(0, 1, "x") - y = solver.NumVar(0, 2, "y") + x_var = solver.NumVar(0, 1, "x") + y_var = solver.NumVar(0, 2, "y") print("Number of variables =", solver.NumVariables()) # [END variables] # [START constraints] - # Create a linear constraint, 0 <= x + y <= 2. - ct = solver.Constraint(0, 2, "ct") - ct.SetCoefficient(x, 1) - ct.SetCoefficient(y, 1) + infinity = solver.infinity() + # Create a linear constraint, x + y <= 2. + constraint = solver.Constraint(-infinity, 2, "ct") + constraint.SetCoefficient(x_var, 1) + constraint.SetCoefficient(y_var, 1) print("Number of constraints =", solver.NumConstraints()) # [END constraints] @@ -47,24 +52,44 @@ def main(): # [START objective] # Create the objective function, 3 * x + y. objective = solver.Objective() - objective.SetCoefficient(x, 3) - objective.SetCoefficient(y, 1) + objective.SetCoefficient(x_var, 3) + objective.SetCoefficient(y_var, 1) objective.SetMaximization() # [END objective] # [START solve] print(f"Solving with {solver.SolverVersion()}") - solver.Solve() + result_status = solver.Solve() # [END solve] # [START print_solution] + print(f"Status: {result_status}") + if result_status != pywraplp.Solver.OPTIMAL: + print("The problem does not have an optimal solution!") + if result_status == pywraplp.Solver.FEASIBLE: + print("A potentially suboptimal solution was found") + else: + print("The solver could not solve the problem.") + return + print("Solution:") print("Objective value =", objective.Value()) - print("x =", x.solution_value()) - print("y =", y.solution_value()) + print("x =", x_var.solution_value()) + print("y =", y_var.solution_value()) # [END print_solution] + # [START advanced] + print("Advanced usage:") + print(f"Problem solved in {solver.wall_time():d} milliseconds") + print(f"Problem solved in {solver.iterations():d} iterations") + # [END advanced] + if __name__ == "__main__": + init.CppBridge.init_logging("basic_example.py") + cpp_flags = init.CppFlags() + cpp_flags.stderrthreshold = True + cpp_flags.log_prefix = False + init.CppBridge.set_flags(cpp_flags) main() # [END program] diff --git a/ortools/linear_solver/samples/code_samples.bzl b/ortools/linear_solver/samples/code_samples.bzl index 50b5bf8f90..51eae70e1b 100644 --- a/ortools/linear_solver/samples/code_samples.bzl +++ b/ortools/linear_solver/samples/code_samples.bzl @@ -21,6 +21,7 @@ def code_sample_cc(name): srcs = [name + ".cc"], deps = [ "//ortools/base", + "//ortools/init", "//ortools/linear_solver", "//ortools/linear_solver:linear_solver_cc_proto", ], @@ -33,6 +34,7 @@ def code_sample_cc(name): deps = [ ":" + name + "_cc", "//ortools/base", + "//ortools/init", "//ortools/linear_solver", "//ortools/linear_solver:linear_solver_cc_proto", ], @@ -48,6 +50,7 @@ def code_sample_py(name): requirement("protobuf"), requirement("numpy"), requirement("pandas"), + "//ortools/init/python:init", "//ortools/linear_solver/python:model_builder", ], python_version = "PY3", @@ -60,6 +63,7 @@ def code_sample_py(name): srcs = [name + ".py"], main = name + ".py", data = [ + "//ortools/init/python:init", "//ortools/linear_solver/python:model_builder", ], deps = [ @@ -80,6 +84,7 @@ def code_sample_java(name): main_class = "com.google.ortools.linearsolver.samples." + name, test_class = "com.google.ortools.linearsolver.samples." + name, deps = [ + "//ortools/init/java:init", "//ortools/linear_solver/java:modelbuilder", "//ortools/java/com/google/ortools/modelbuilder", "//ortools/java/com/google/ortools:Loader", diff --git a/ortools/linear_solver/sat_interface.cc b/ortools/linear_solver/sat_interface.cc index 811b14d1a5..78b617eb62 100644 --- a/ortools/linear_solver/sat_interface.cc +++ b/ortools/linear_solver/sat_interface.cc @@ -29,6 +29,7 @@ #include "ortools/port/proto_utils.h" #include "ortools/sat/cp_model.pb.h" #include "ortools/sat/cp_model_solver.h" +#include "ortools/util/lazy_mutable_copy.h" namespace operations_research { @@ -39,10 +40,17 @@ class SatInterface : public MPSolverInterface { // ----- Solve ----- MPSolver::ResultStatus Solve(const MPSolverParameters& param) override; - std::optional DirectlySolveProto( - const MPModelRequest& request, std::atomic* interrupt) override; bool InterruptSolve() override; + // ----- Directly solve proto is supported --- + bool SupportsDirectlySolveProto(std::atomic* interrupt) const override { + return true; + } + MPSolutionResponse DirectlySolveProto(LazyMutableCopy request, + std::atomic* interrupt) override { + return SatSolveProto(std::move(request), interrupt); + } + // ----- Model modifications and extraction ----- void Reset() override; void SetOptimizationDirection(bool maximize) override; @@ -152,11 +160,6 @@ MPSolver::ResultStatus SatInterface::Solve(const MPSolverParameters& param) { return result_status_; } -std::optional SatInterface::DirectlySolveProto( - const MPModelRequest& request, std::atomic* interrupt) { - return SatSolveProto(request, interrupt); -} - bool SatInterface::InterruptSolve() { interrupt_solve_ = true; return true; diff --git a/ortools/linear_solver/scip_interface.cc b/ortools/linear_solver/scip_interface.cc index 86349a9d60..b2c780c78e 100644 --- a/ortools/linear_solver/scip_interface.cc +++ b/ortools/linear_solver/scip_interface.cc @@ -39,9 +39,11 @@ #include "ortools/linear_solver/linear_solver.h" #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/linear_solver/linear_solver_callback.h" +#include "ortools/linear_solver/proto_solver/proto_utils.h" #include "ortools/linear_solver/proto_solver/scip_proto_solver.h" #include "ortools/linear_solver/scip_callback.h" #include "ortools/linear_solver/scip_helper_macros.h" +#include "ortools/util/lazy_mutable_copy.h" #include "scip/cons_indicator.h" #include "scip/scip.h" #include "scip/scip_copy.h" @@ -69,8 +71,11 @@ class SCIPInterface : public MPSolverInterface { void SetOptimizationDirection(bool maximize) override; MPSolver::ResultStatus Solve(const MPSolverParameters& param) override; - std::optional DirectlySolveProto( - const MPModelRequest& request, std::atomic* interrupt) override; + + bool SupportsDirectlySolveProto(std::atomic* interrupt) const override; + MPSolutionResponse DirectlySolveProto(LazyMutableCopy request, + std::atomic* interrupt) override; + void Reset() override; double infinity() override; @@ -865,27 +870,22 @@ void SCIPInterface::SetSolution(SCIP_SOL* solution) { } } -std::optional SCIPInterface::DirectlySolveProto( - const MPModelRequest& request, std::atomic* interrupt) { +bool SCIPInterface::SupportsDirectlySolveProto( + std::atomic* interrupt) const { // ScipSolveProto doesn't solve concurrently. - if (solver_->GetNumThreads() > 1) return std::nullopt; + if (solver_->GetNumThreads() > 1) return false; // Interruption via atomic is not directly supported by SCIP. - if (interrupt != nullptr) return std::nullopt; + if (interrupt != nullptr) return false; - const auto status_or = ScipSolveProto(request); - if (status_or.ok()) return status_or.value(); - // Special case: if something is not implemented yet, fall back to solving - // through MPSolver. - if (absl::IsUnimplemented(status_or.status())) return std::nullopt; - - if (request.enable_internal_solver_output()) { - LOG(INFO) << "Invalid SCIP status: " << status_or.status(); + return true; } - MPSolutionResponse response; - response.set_status(MPSOLVER_NOT_SOLVED); - response.set_status_str(status_or.status().ToString()); - return response; + +MPSolutionResponse SCIPInterface::DirectlySolveProto( + LazyMutableCopy request, std::atomic* interrupt) { + const bool log_error = request->enable_internal_solver_output(); + return ConvertStatusOrMPSolutionResponse(log_error, + ScipSolveProto(std::move(request))); } int SCIPInterface::SolutionCount() { return SCIPgetNSols(scip_); } diff --git a/ortools/linear_solver/solve.cc b/ortools/linear_solver/solve.cc index e135ccc19c..7069b5e1f0 100644 --- a/ortools/linear_solver/solve.cc +++ b/ortools/linear_solver/solve.cc @@ -51,6 +51,7 @@ #include #include +#include #include #include @@ -78,9 +79,11 @@ ABSL_FLAG(std::string, input, "", "REQUIRED: Input file name."); ABSL_FLAG(std::string, sol_hint, "", "Input file name with solution in .sol format."); -ABSL_FLAG(std::string, solver, "glop", +ABSL_FLAG(std::optional, solver, std::nullopt, "The solver to use: bop, cbc, clp, glop, glpk_lp, glpk_mip, " - "gurobi_lp, gurobi_mip, pdlp, scip, knapsack, sat."); + "gurobi_lp, gurobi_mip, pdlp, scip, knapsack, sat. If unspecified " + "either use MPModelRequest.solver_type if the --input is an " + "MPModelRequest and the field is set or use glop."); ABSL_FLAG(int, num_threads, 1, "Number of threads to use by the underlying solver."); ABSL_FLAG(std::string, params_file, "", @@ -263,9 +266,15 @@ void Run() { QCHECK_GE(absl::GetFlag(FLAGS_time_limit), absl::ZeroDuration()) << "--time_limit must be given a positive duration"; - MPSolver::OptimizationProblemType type; - CHECK(MPSolver::ParseSolverType(absl::GetFlag(FLAGS_solver), &type)) - << "Unsupported --solver: " << absl::GetFlag(FLAGS_solver); + // Parses --solver if set. + std::optional type; + if (const std::optional type_flag = absl::GetFlag(FLAGS_solver); + type_flag.has_value()) { + MPSolver::OptimizationProblemType decoded_type; + QCHECK(MPSolver::ParseSolverType(type_flag.value(), &decoded_type)) + << "Unsupported --solver: " << type_flag.value(); + type = decoded_type; + } MPModelRequest request_proto = ReadMipModel(absl::GetFlag(FLAGS_input)); @@ -302,7 +311,10 @@ void Run() { } // Set or override request proto options from the command line flags. - request_proto.set_solver_type(static_cast(type)); + if (type.has_value() || !request_proto.has_solver_type()) { + request_proto.set_solver_type(static_cast( + type.value_or(MPSolver::GLOP_LINEAR_PROGRAMMING))); + } if (absl::GetFlag(FLAGS_time_limit) != absl::InfiniteDuration()) { LOG(INFO) << "Setting a time limit of " << absl::GetFlag(FLAGS_time_limit); request_proto.set_solver_time_limit_seconds( diff --git a/ortools/linear_solver/solve_mp_model.cc b/ortools/linear_solver/solve_mp_model.cc index de322ffd09..1a490b67f9 100644 --- a/ortools/linear_solver/solve_mp_model.cc +++ b/ortools/linear_solver/solve_mp_model.cc @@ -15,18 +15,42 @@ #include #include +#include #include "ortools/linear_solver/linear_solver.h" #include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/util/solve_interrupter.h" namespace operations_research { -MPSolutionResponse SolveMPModel(const MPModelRequest& model_request, - std::atomic* interrupt) { // TODO(b/311704821): this function should not delegate to MPSolver, also true // for the functions below. +MPSolutionResponse SolveMPModel(const MPModelRequest& model_request, + SolveInterrupter* interrupter) { MPSolutionResponse response; - MPSolver::SolveWithProto(model_request, &response, interrupt); + if (interrupter != nullptr) { + std::atomic atomic_bool = false; + ScopedSolveInterrupterCallback cleanup( + interrupter, [&atomic_bool] { atomic_bool.store(true); }); + MPSolver::SolveLazyMutableRequest(model_request, &response, &atomic_bool); + } else { + MPSolver::SolveLazyMutableRequest(model_request, &response); + } + return response; +} + +MPSolutionResponse SolveMPModel(MPModelRequest&& model_request, + SolveInterrupter* interrupter) { + MPSolutionResponse response; + if (interrupter != nullptr) { + std::atomic atomic_bool = false; + ScopedSolveInterrupterCallback cleanup( + interrupter, [&atomic_bool] { atomic_bool.store(true); }); + MPSolver::SolveLazyMutableRequest(std::move(model_request), &response, + &atomic_bool); + } else { + MPSolver::SolveLazyMutableRequest(std::move(model_request), &response); + } return response; } diff --git a/ortools/linear_solver/solve_mp_model.h b/ortools/linear_solver/solve_mp_model.h index b276c1d274..d56754d248 100644 --- a/ortools/linear_solver/solve_mp_model.h +++ b/ortools/linear_solver/solve_mp_model.h @@ -22,23 +22,30 @@ #include #include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/util/solve_interrupter.h" namespace operations_research { /** * Solves the model encoded by a MPModelRequest protocol buffer and returns the - * solution encoded as a MPSolutionResponse. The solve is stopped prematurely - * if interrupt is non-null at set to true during (or before) solving. - * Interruption is only supported if SolverTypeSupportsInterruption() returns - * true for the requested solver. Passing a non-null interruption with any - * other solver type immediately returns an MPSOLVER_INCOMPATIBLE_OPTIONS - * error. + * solution encoded as a MPSolutionResponse. + * + * If interrupter is non-null, one can call interrupter->Interrupt() to stop the + * solver earlier. Interruption is only supported if + * SolverTypeSupportsInterruption() returns true for the requested solver. + * Passing a non-null pointer with any other solver type immediately returns an + * MPSOLVER_INCOMPATIBLE_OPTIONS error. */ -MPSolutionResponse SolveMPModel( - const MPModelRequest& model_request, - // `interrupt` is non-const because the internal - // solver may set it to true itself, in some cases. - std::atomic* interrupt = nullptr); +MPSolutionResponse SolveMPModel(const MPModelRequest& model_request, + SolveInterrupter* interrupter = nullptr); + +/** + * This version should be preferred if the request is not needed afterwards. + * It will allows to reclaim the request memory as soon as it is converted to + * one of the solver internal data representation. + */ +MPSolutionResponse SolveMPModel(MPModelRequest&& request, + SolveInterrupter* interrupter = nullptr); bool SolverTypeSupportsInterruption(MPModelRequest::SolverType solver); diff --git a/ortools/linear_solver/users_allowing_model_storage.cc b/ortools/linear_solver/users_allowing_model_storage.cc new file mode 100644 index 0000000000..9ec377becb --- /dev/null +++ b/ortools/linear_solver/users_allowing_model_storage.cc @@ -0,0 +1,78 @@ +// Copyright 2010-2024 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/users_allowing_model_storage.h" + +#include "absl/container/flat_hash_set.h" +#include "absl/strings/string_view.h" + +namespace operations_research { +const absl::flat_hash_set& UsersAllowingModelStorage() { + static const auto* const set = new absl::flat_hash_set{ + // Approved by default. + "operations-research", + + // Approved hmajaya@ on 2019/05/17 by e-mail. + "apex-eng", + + // Approved by jhuchette@ on 2024-02-29 by code review. + "apps-capacity-auxon", + "autocap-automation", + "autocap-solver-access", + + // Approved by mlubin@, dapplegate@, and bwydrowski@ on 2019/05/17 + // by e-mail. As of 2020/04/08, prod queries are sent by "muppet-packer". + "blokus-prod", + "blokus-planning", + "blokus-packer-dev", + "muppet-packer", + + // Approved by sjoakley@ on 2019/10/22 by e-mail. + "cloud-capacity", + "techinfra-capacity", + + // Approved by sgowal@ on 2019/05/17 by e-mail. + "deepmind-research", + + // Approved by yxz@ on 2019/05/17 by e-mail. As of 2020/04/09, many + // queries are sent by "logs-placement". + "logs-front-door", + "logs-front-door-unprivileged", + "logs-placement", + + // Approved by ansha@ on 2019/05/17 by e-mail. We add netarch-wand-* mdb + // groups explicitly, because as of 2019/10/22 our naive logic collects + // a model iff the mdb group listed here matches exactly the mdb group + // of the RPC sender (i.e., we do not check group transitive memberships, + // and here all netarch-wand-* groups belong to tetraligh-jobs). + "tetralight-jobs", + "netarch-wand-prod", + "netarch-wand-dev", + "netarch-wand-test", + + // Approved by haoxu@ on 2019/05/17 by e-mail. + // As of 2019/10/22, some models are sent by user xiaob@ (instead of + // raptical@), so we add the user explicitly to this allowlist. + "cluster-planning-urp-state-runner", + "cluster-planning-urp-compute", + "raptical", + "xiaob", + + // Approved by nharsha@ and mattard@ on 2019/05/17 by e-mail. + "resource-planning-optimization", + "resource-planning-optimization-eng-team", + "resource-portal-test", + }; + return *set; +} +} // namespace operations_research diff --git a/ortools/linear_solver/users_allowing_model_storage.h b/ortools/linear_solver/users_allowing_model_storage.h new file mode 100644 index 0000000000..481c9b7310 --- /dev/null +++ b/ortools/linear_solver/users_allowing_model_storage.h @@ -0,0 +1,28 @@ +// Copyright 2010-2024 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_USERS_ALLOWING_MODEL_STORAGE_H_ +#define OR_TOOLS_LINEAR_SOLVER_USERS_ALLOWING_MODEL_STORAGE_H_ + +#include "absl/container/flat_hash_set.h" +#include "absl/strings/string_view.h" + +namespace operations_research { +// List of *exact* MDB users who agreed that we store their MIP/LP/math +// (anonymized) models. +// IMPORTANT: The MDB user has to match exactly with an item in this list: we +// don't do ACL expansion, regexp matching or anything alike. +const absl::flat_hash_set& UsersAllowingModelStorage(); +} // namespace operations_research + +#endif // OR_TOOLS_LINEAR_SOLVER_USERS_ALLOWING_MODEL_STORAGE_H_ diff --git a/ortools/linear_solver/wrappers/BUILD.bazel b/ortools/linear_solver/wrappers/BUILD.bazel index 727d5eae32..f958ac2cc4 100644 --- a/ortools/linear_solver/wrappers/BUILD.bazel +++ b/ortools/linear_solver/wrappers/BUILD.bazel @@ -49,6 +49,7 @@ cc_library( "//ortools/lp_data:lp_parser", "//ortools/lp_data:mps_reader", "//ortools/util:logging", + "//ortools/util:solve_interrupter", "//ortools/xpress:environment", ], ) diff --git a/ortools/linear_solver/wrappers/model_builder_helper.cc b/ortools/linear_solver/wrappers/model_builder_helper.cc index 1ae565dc8f..a1f24be23b 100644 --- a/ortools/linear_solver/wrappers/model_builder_helper.cc +++ b/ortools/linear_solver/wrappers/model_builder_helper.cc @@ -466,7 +466,7 @@ std::optional ModelSolverHelper::SolveRequest( request.solver_type()))) { return std::nullopt; } - return SolveMPModel(request, &interrupt_solve_); + return SolveMPModel(request, &interrupter_); } namespace { @@ -567,19 +567,20 @@ void ModelSolverHelper::Solve(const ModelBuilderHelper& model) { } switch (solver_type_.value()) { case MPModelRequest::GLOP_LINEAR_PROGRAMMING: { - response_ = GlopSolveProto(request, &interrupt_solve_, log_callback_); + response_ = + GlopSolveProto(std::move(request), &interrupt_solve_, log_callback_); break; } case MPModelRequest::SAT_INTEGER_PROGRAMMING: { - response_ = - SatSolveProto(request, &interrupt_solve_, log_callback_, nullptr); + response_ = SatSolveProto(std::move(request), &interrupt_solve_, + log_callback_, nullptr); break; } #if defined(USE_SCIP) case MPModelRequest::SCIP_MIXED_INTEGER_PROGRAMMING: { // TODO(user): Enable log_callback support. // TODO(user): Enable interrupt_solve. - const auto temp = ScipSolveProto(request); + const auto temp = ScipSolveProto(std::move(request)); if (temp.ok()) { response_ = std::move(temp.value()); } @@ -588,7 +589,7 @@ void ModelSolverHelper::Solve(const ModelBuilderHelper& model) { #endif // defined(USE_SCIP) #if defined(USE_PDLP) case MPModelRequest::PDLP_LINEAR_PROGRAMMING: { - const auto temp = PdlpSolveProto(request); + const auto temp = PdlpSolveProto(std::move(request)); if (temp.ok()) { response_ = std::move(temp.value()); } @@ -598,7 +599,7 @@ void ModelSolverHelper::Solve(const ModelBuilderHelper& model) { case MPModelRequest:: GUROBI_LINEAR_PROGRAMMING: // ABSL_FALLTHROUGH_INTENDED case MPModelRequest::GUROBI_MIXED_INTEGER_PROGRAMMING: { - const auto temp = GurobiSolveProto(request); + const auto temp = GurobiSolveProto(std::move(request)); if (temp.ok()) { response_ = std::move(temp.value()); } @@ -640,6 +641,7 @@ void ModelSolverHelper::SetLogCallbackFromDirectorClass( void ModelSolverHelper::ClearLogCallback() { log_callback_ = nullptr; } bool ModelSolverHelper::InterruptSolve() { + interrupter_.Interrupt(); interrupt_solve_ = true; return true; } diff --git a/ortools/linear_solver/wrappers/model_builder_helper.h b/ortools/linear_solver/wrappers/model_builder_helper.h index 36221ad6bb..24525afaa8 100644 --- a/ortools/linear_solver/wrappers/model_builder_helper.h +++ b/ortools/linear_solver/wrappers/model_builder_helper.h @@ -24,6 +24,7 @@ #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/linear_solver/model_exporter.h" #include "ortools/util/logging.h" +#include "ortools/util/solve_interrupter.h" namespace operations_research { @@ -166,7 +167,6 @@ class ModelSolverHelper { void Solve(const ModelBuilderHelper& model); // Only used by the CVXPY interface. Does not store the response internally. - // interrupt_solve_ is passed to the solve method. std::optional SolveRequest(const MPModelRequest& request); // Returns true if the interrupt signal was correctly sent, that is if the @@ -203,6 +203,7 @@ class ModelSolverHelper { // TODO(user): set parameters. private: + SolveInterrupter interrupter_; std::atomic interrupt_solve_ = false; std::function log_callback_; std::optional response_; diff --git a/ortools/math_opt/python/compute_infeasible_subsystem_result.py b/ortools/math_opt/python/compute_infeasible_subsystem_result.py index bf77203ca0..4f981cfba7 100644 --- a/ortools/math_opt/python/compute_infeasible_subsystem_result.py +++ b/ortools/math_opt/python/compute_infeasible_subsystem_result.py @@ -14,7 +14,7 @@ """Data types for the result of calling `mathopt.compute_infeasible_subsystem.""" import dataclasses -from typing import Mapping +from typing import FrozenSet, Mapping import immutabledict @@ -75,7 +75,7 @@ class ModelSubset: variable_bounds: Mapping[ model.Variable, ModelSubsetBounds ] = immutabledict.immutabledict() - variable_integrality: frozenset[model.Variable] = frozenset() + variable_integrality: FrozenSet[model.Variable] = frozenset() linear_constraints: Mapping[ model.LinearConstraint, ModelSubsetBounds ] = immutabledict.immutabledict() diff --git a/ortools/math_opt/solver_tests/ip_model_solve_parameters_tests.cc b/ortools/math_opt/solver_tests/ip_model_solve_parameters_tests.cc index 27ec82508e..df7b5a3628 100644 --- a/ortools/math_opt/solver_tests/ip_model_solve_parameters_tests.cc +++ b/ortools/math_opt/solver_tests/ip_model_solve_parameters_tests.cc @@ -367,23 +367,27 @@ TEST_P(LazyConstraintsTest, LazyConstraintsImposedOnModel) { } // The problem is: -// min x -// s.t. x >= 0 (c) -// -1 <= x <= 1 -// x integer +// min y +// s.t. y >= x (c) +// y >= -x (d) +// -1 <= x, y <= 1 +// x, y integer // // With a node limit of 0 and solver parameters set to disable presolve, we // expect a dual bound equal to the LP relaxation bound (which is 0). However, -// if c is a lazy constraint, it is not included in the LP relaxation, and the -// bound instead is -1. +// if c and d are lazy constraints, they are not included in the LP relaxation, +// and the bound instead is -1. TEST_P(LazyConstraintsTest, AnnotationsAreSetProperly) { Model model; Variable x = model.AddIntegerVariable(-1, 1, "x"); - const LinearConstraint c = model.AddLinearConstraint(x >= 0); - model.Minimize(x); + Variable y = model.AddIntegerVariable(-1, 1, "y"); + const LinearConstraint c = model.AddLinearConstraint(y >= x); + const LinearConstraint d = model.AddLinearConstraint(y >= -x); + model.Minimize(y); - SolveArguments args = {.parameters = NerfedSolveParams(), - .model_parameters = {.lazy_linear_constraints = {c}}}; + SolveArguments args = { + .parameters = NerfedSolveParams(), + .model_parameters = {.lazy_linear_constraints = {c, d}}}; args.parameters.node_limit = 0; ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, TestedSolver(), args)); @@ -394,17 +398,20 @@ TEST_P(LazyConstraintsTest, AnnotationsAreSetProperly) { // Same setting as in AnnotationsAreSetProperly above, but we solve twice with // an incremental solver: first with the lazy constraint annotations, and then // without. If the annotations are cleared after the first, then we expect the -// second to solve the entire LP (including c), giving a dual bound of 0. +// second to solve the entire LP (including c and d), giving a dual bound of 0. TEST_P(LazyConstraintsTest, AnnotationsAreClearedAfterSolve) { Model model; Variable x = model.AddIntegerVariable(-1, 1, "x"); - const LinearConstraint c = model.AddLinearConstraint(x >= 0); - model.Minimize(x); + Variable y = model.AddIntegerVariable(-1, 1, "y"); + const LinearConstraint c = model.AddLinearConstraint(y >= x); + const LinearConstraint d = model.AddLinearConstraint(y >= -x); + model.Minimize(y); ASSERT_OK_AND_ASSIGN(const auto solver, IncrementalSolver::New(&model, TestedSolver())); - SolveArguments args = {.parameters = NerfedSolveParams(), - .model_parameters = {.lazy_linear_constraints = {c}}}; + SolveArguments args = { + .parameters = NerfedSolveParams(), + .model_parameters = {.lazy_linear_constraints = {c, d}}}; args.parameters.node_limit = 0; ASSERT_OK_AND_ASSIGN(const SolveResult bad_result, solver->Solve(args)); ASSERT_THAT(bad_result, TerminatesWithReasonNoSolutionFound(Limit::kNode)); diff --git a/ortools/math_opt/solver_tests/second_order_cone_tests.cc b/ortools/math_opt/solver_tests/second_order_cone_tests.cc index 0f2e6d08ae..a11a5dab6e 100644 --- a/ortools/math_opt/solver_tests/second_order_cone_tests.cc +++ b/ortools/math_opt/solver_tests/second_order_cone_tests.cc @@ -60,7 +60,7 @@ using ::testing::status::StatusIs; // A bit larger than expected; as of 2023-01-31 Gurobi produces slightly // inaccurate solutions on some of the tests. -constexpr double kTolerance = 1.0e-4; +constexpr double kTolerance = 1.0e-3; constexpr absl::string_view kNoSocSupportMessage = "This test is disabled as the solver does not support second-order cone " "constraints"; diff --git a/ortools/math_opt/solvers/gscip_solver.cc b/ortools/math_opt/solvers/gscip_solver.cc index 7ec575c15e..2d2361c48d 100644 --- a/ortools/math_opt/solvers/gscip_solver.cc +++ b/ortools/math_opt/solvers/gscip_solver.cc @@ -111,7 +111,7 @@ int64_t SafeId(const LinearConstraintsProto& linear_constraints, int index) { return linear_constraints.ids(index); } -const std::string& SafeName(const LinearConstraintsProto& linear_constraints, +absl::string_view SafeName(const LinearConstraintsProto& linear_constraints, int index) { if (linear_constraints.names().empty()) { return EmptyString(); diff --git a/ortools/math_opt/solvers/gurobi/g_gurobi.cc b/ortools/math_opt/solvers/gurobi/g_gurobi.cc index 221442aaef..bc035e1118 100644 --- a/ortools/math_opt/solvers/gurobi/g_gurobi.cc +++ b/ortools/math_opt/solvers/gurobi/g_gurobi.cc @@ -541,8 +541,15 @@ absl::StatusOr Gurobi::ComputeIIS(Callback cb) { RETURN_IF_ERROR(scope->Release()); return false; } else if (error == kGrbOk) { + // If Gurobi v11 terminates at a limit before determining if the model is + // feasible or not, it will return an OK error code but then will fail to + // return anything about the IIS it does not have. To detect this case, we + // query the minimality attribute: we know that our env is valid at this + // point, and this should fail iff an IIS is present, i.e., Gurobi proved + // that the model was infeasible. + const bool has_iis = GetIntAttr(GRB_INT_ATTR_IIS_MINIMAL).ok(); RETURN_IF_ERROR(scope->Release()); - return true; + return has_iis; } RETURN_IF_ERROR(ToStatus(error)); return scope->Release(); diff --git a/ortools/math_opt/solvers/gurobi/g_gurobi.h b/ortools/math_opt/solvers/gurobi/g_gurobi.h index d1af139ed4..5072db8215 100644 --- a/ortools/math_opt/solvers/gurobi/g_gurobi.h +++ b/ortools/math_opt/solvers/gurobi/g_gurobi.h @@ -466,9 +466,8 @@ class Gurobi { // // Returns: // * a status if Gurobi errors, - // * false if Gurobi determines that the model is feasible, or - // * true otherwise (e.g., infeasibility is proven or a limit is reached). - // setup/teardown, and true otherwise. + // * true if Gurobi proves that the model is infeasible, or + // * false otherwise (e.g., feasibility is proven or a limit is reached). // // The callback, if specified, is set before solving and cleared after. absl::StatusOr ComputeIIS(Callback cb = nullptr); diff --git a/ortools/math_opt/testing/BUILD.bazel b/ortools/math_opt/testing/BUILD.bazel index 6df27bf172..e80e4e0a4f 100644 --- a/ortools/math_opt/testing/BUILD.bazel +++ b/ortools/math_opt/testing/BUILD.bazel @@ -11,12 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -package(default_visibility = [ - "//ortools/graph:__subpackages__", - "//ortools/math_opt:__subpackages__", - "//ortools/models:__subpackages__", - "//ortools/stochastic_optimization:__subpackages__", -]) +package(default_visibility = ["//ortools:__subpackages__"]) cc_library( name = "param_name", diff --git a/ortools/pdlp/solve_log.proto b/ortools/pdlp/solve_log.proto index 282048499a..2b940dd0c5 100644 --- a/ortools/pdlp/solve_log.proto +++ b/ortools/pdlp/solve_log.proto @@ -18,6 +18,10 @@ syntax = "proto2"; package operations_research.pdlp; +option java_package = "com.google.ortools.pdlp"; +option java_multiple_files = true; +option csharp_namespace = "Google.OrTools.PDLP"; + import "ortools/pdlp/solvers.proto"; // Easy-to-compute statistics for the quadratic program. diff --git a/ortools/pdlp/solvers.proto b/ortools/pdlp/solvers.proto index d128b13079..215dfd594d 100644 --- a/ortools/pdlp/solvers.proto +++ b/ortools/pdlp/solvers.proto @@ -15,6 +15,10 @@ syntax = "proto2"; package operations_research.pdlp; +option java_package = "com.google.ortools.pdlp"; +option java_multiple_files = true; +option csharp_namespace = "Google.OrTools.PDLP"; + import "ortools/glop/parameters.proto"; enum OptimalityNorm { diff --git a/ortools/routing/BUILD.bazel b/ortools/routing/BUILD.bazel index c0ca227f4b..4a0f28d7c5 100644 --- a/ortools/routing/BUILD.bazel +++ b/ortools/routing/BUILD.bazel @@ -242,7 +242,7 @@ cc_test( deps = [ ":tsplib_parser", "//ortools/base", - "//ortools/base:filesystem", + "//ortools/base:file", "//ortools/base:memfile", "@com_google_absl//absl/container:btree", "@com_google_absl//absl/strings", diff --git a/ortools/util/BUILD.bazel b/ortools/util/BUILD.bazel index 9b41007594..191060df45 100644 --- a/ortools/util/BUILD.bazel +++ b/ortools/util/BUILD.bazel @@ -506,3 +506,17 @@ cc_library( "@com_google_protobuf//:protobuf", ], ) + +cc_library( + name = "solve_interrupter", + srcs = ["solve_interrupter.cc"], + hdrs = ["solve_interrupter.h"], + deps = [ + "//ortools/base", + "//ortools/base:intops", + "//ortools/base:linked_hash_map", + "@com_google_absl//absl/base:core_headers", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/synchronization", + ], +) diff --git a/ortools/util/csharp/proto.i b/ortools/util/csharp/proto.i index 47d21fe6a8..1de439644e 100644 --- a/ortools/util/csharp/proto.i +++ b/ortools/util/csharp/proto.i @@ -32,19 +32,14 @@ // if the C++ function returns a protocol message: // MyProto* foo(); // Use PROTO2_RETURN macro: -// PROTO2_RETURN(MyProto, Google.Proto.Protos.Test.MyProto, true) -// -// Replace true by false if the C++ function returns a pointer to a -// protocol message object whose ownership is not transferred to the -// (C++) caller. +// PROTO2_RETURN(MyProto, Google.Proto.Protos.Test.MyProto) // // Passing each protocol message from C# to C++ by value. Each ProtocolMessage // is serialized into byte[] when it is passed from C# to C++, the C++ code // deserializes into C++ native protocol message. // // @param CppProtoType the fully qualified C++ protocol message type -// @param CSharpProtoType the corresponding fully qualified C# protocol message -// type +// @param CSharpProtoType the corresponding fully qualified C# protocol message type // @param param_name the parameter name %define PROTO_INPUT(CppProtoType, CSharpProtoType, param_name) %typemap(ctype) PROTO_TYPE* INPUT, PROTO_TYPE& INPUT "int " #param_name "_size, uint8_t*" @@ -69,6 +64,12 @@ %apply PROTO_TYPE* INPUT { CppProtoType* param_name } %enddef // end PROTO_INPUT +// Return protocol message from C++ to C#. +// Each protocol message is serialized into byte[] when it is returned +// from C++. +// +// @param CppProtoType the fully qualified C++ protocol message type +// @param CSharpProtoType the corresponding fully qualified C# protocol message type %define PROTO2_RETURN(CppProtoType, CSharpProtoType) %typemap(ctype) CppProtoType "uint8_t*" %typemap(imtype) CppProtoType "System.IntPtr" @@ -104,3 +105,20 @@ %enddef // end PROTO2_RETURN +// SWIG Macro for mapping protocol message enum type. +// @param CppEnumProto the C++ protocol message enum type +// @param CSharpEnumProto the corresponding C# protocol message enum type +%define PROTO_ENUM_RETURN(CppEnumProto, CSharpEnumProto) +%typemap(ctype) CppEnumProto "int" +%typemap(imtype) CppEnumProto "int" +%typemap(cstype) CppEnumProto "CSharpEnumProto" + +// From CppEnumProto to ctype (in wrap.cxx code) +%typemap(out) CppEnumProto %{ $result = $1; %} + +// From imtype to cstype (in .cs code) +%typemap(csout) CppEnumProto { + return (CSharpEnumProto) $imcall; +} +%enddef // end PROTO_ENUM_RETURN + diff --git a/ortools/util/filelineiter.h b/ortools/util/filelineiter.h index a2d7b689da..79a2dc5337 100644 --- a/ortools/util/filelineiter.h +++ b/ortools/util/filelineiter.h @@ -142,7 +142,7 @@ class FileLines { // Please prefer the other constructor combined with file::Open() in new code // so that missing files are properly detected. This version would only print // a warning and act as if the file was empty. - explicit FileLines(const std::string& filename, + explicit FileLines(absl::string_view filename, int options = FileLineIterator::DEFAULT) : FileLines( filename, diff --git a/ortools/util/fp_utils.cc b/ortools/util/fp_utils.cc index bf82c1dca4..e44d20cd56 100644 --- a/ortools/util/fp_utils.cc +++ b/ortools/util/fp_utils.cc @@ -72,9 +72,9 @@ void ComputeScalingErrors(absl::Span input, } template -void GetBestScalingOfDoublesToInt64(const std::vector& input, - const std::vector& lb, - const std::vector& ub, +void GetBestScalingOfDoublesToInt64(absl::Span input, + absl::Span lb, + absl::Span ub, int64_t max_absolute_sum, double* scaling_factor) { const double kInfinity = std::numeric_limits::infinity(); diff --git a/ortools/util/java/functions.i b/ortools/util/java/functions.i index 3c7d41cc5e..1835b7e449 100644 --- a/ortools/util/java/functions.i +++ b/ortools/util/java/functions.i @@ -54,7 +54,7 @@ // Abbreviation of the java type corresponding to the given CType. // Eg. JAVA_ABBREV(int64_t) expands to "J". -#define JAVA_ABBREV_int64 "J" +#define JAVA_ABBREV_int64_t "J" #define JAVA_ABBREV_int "I" #define JAVA_ABBREV_bool "Z" #define JAVA_ABBREV(x) JAVA_ABBREV_ ## x diff --git a/ortools/util/java/proto.i b/ortools/util/java/proto.i index e7ece58490..f686846aba 100644 --- a/ortools/util/java/proto.i +++ b/ortools/util/java/proto.i @@ -23,9 +23,7 @@ // if the C++ function returns a protocol message: // MyProto* foo(); // Use PROTO2_RETURN macro: -// PROTO2_RETURN(MyProto, com.google.proto.protos.test.MyProto, giveOwnership) -// -> the 'giveOwnership' parameter should be true iff the C++ function -// returns a new proto which should be deleted by the client. +// PROTO2_RETURN(MyProto, com.google.proto.protos.test.MyProto) // // Passing each protocol message from Java to C++ by value. Each ProtocolMessage // is serialized into byte[] when it is passed from Java to C++, the C++ code @@ -90,3 +88,22 @@ jenv->SetByteArrayRegion($result, 0, size, buf.get()); } %enddef // PROTO2_RETURN + +// SWIG Macro for mapping protocol message enum type. +// @param CppEnumProto the C++ protocol message enum type +// @param JavaEnumProto the corresponding Java protocol message enum type +%define PROTO_ENUM_RETURN(CppEnumProto, JavaEnumProto) +%typemap(jni) CppEnumProto "jint" +%typemap(jtype) CppEnumProto "int" +%typemap(jstype) CppEnumProto "JavaEnumProto" + +// From CppEnumProto to jni (in wrap.cxx code) +%typemap(out) CppEnumProto %{ $result = $1; %} + +// From jtype to jstype (in .java code) +%typemap(javaout) CppEnumProto { + return JavaEnumProto.forNumber($jnicall); +} + +%enddef // end PROTO_ENUM_RETURN + diff --git a/ortools/util/lazy_mutable_copy.h b/ortools/util/lazy_mutable_copy.h index a9a357bbe3..6af6c4f5aa 100644 --- a/ortools/util/lazy_mutable_copy.h +++ b/ortools/util/lazy_mutable_copy.h @@ -16,8 +16,6 @@ #include -#include "absl/memory/memory.h" - namespace operations_research { // LazyMutableCopy is a helper class for making an on-demand copy of an @@ -34,40 +32,84 @@ namespace operations_research { // void ProcessProto(LazyMutableCopy input) { // pass by copy // ... // } -// At the call site: ProcessProto({const_ref_to_my_proto}); +// At the call site: ProcessProto(const_ref_to_my_proto); // // In basic usage, a LazyMutableCopy is in one of two states: // - original: points to the const original. No memory allocated. // - copy: points to a mutable copy of the original and owns it. Owning the // copy means that the destructor will delete it, like std::unique_ptr<>. -// This is what you get by calling get_mutable(). +// This is what you get by calling get_mutable() or constructing it with +// a move. template class LazyMutableCopy { public: - // You always construct a LazyMutableCopy with a const reference to an object, + // You can construct a LazyMutableCopy with a const reference to an object, // which must outlive this class (unless get_mutable() was called). LazyMutableCopy(const T& obj) // NOLINT(google-explicit-constructor) - : original_(&obj) {} + : ptr_(&obj) {} - // You can move a LazyMutableCopy, much like a std::unique_ptr<> or a const*. - // We simply rely on the default move constructors being available. + // The other option is to construct a LazyMutableCopy with a std::move(T). + // In this case you transfer ownership and you can mutate it for free. + LazyMutableCopy(T&& obj) // NOLINT(google-explicit-constructor) + : copy_(std::make_unique(std::move(obj))), ptr_(copy_.get()) {} - const T& get() const { return copy_ != nullptr ? *copy_ : *original_; } + // You can move a LazyMutableCopy but not copy it, much like a + // std::unique_ptr<>. + LazyMutableCopy(LazyMutableCopy&&) = default; + LazyMutableCopy(const LazyMutableCopy&) = delete; + class LazyMutableCopy& operator=(LazyMutableCopy&&) = default; + class LazyMutableCopy& operator=(const LazyMutableCopy&) = delete; + + // This will copy the object if we don't already have ownership. T* get_mutable() { - if (copy_ == nullptr) { - copy_ = std::make_unique(*original_); - original_ = nullptr; + if (copy_ == nullptr && ptr_ != nullptr) { + copy_ = std::make_unique(*ptr_); + ptr_ = copy_.get(); } return copy_.get(); } + // Lazily make a copy if not already done and transfer ownership from this + // class to the returned std::unique_ptr. Calling this function leaves the + // class in a state where the only valid operations is to assign it a new + // value. + // + // We force a call via + // std::move(lazy_mutable_copy).copy_or_move_as_unique_ptr() to make it + // clearer that lazy_mutable_copy shouldn't really be used after this. + std::unique_ptr copy_or_move_as_unique_ptr() && { + if (copy_ == nullptr && ptr_ != nullptr) { + std::unique_ptr result = std::make_unique(*ptr_); + ptr_ = nullptr; + return result; + } + ptr_ = nullptr; + return std::move(copy_); + } + // True iff get_mutable() was called at least once (in which case the object - // was copied). - bool was_copied() const { return copy_ != nullptr; } + // was copied) or if we constructed this via std::move(). + bool has_ownership() const { return copy_ != nullptr; } + + // Standard smart pointer accessor, but only for const purpose. + // Undefined if the class contains no object. + const T* get() const { return ptr_; } + const T& operator*() const { return *ptr_; } + const T* operator->() const { return ptr_; } + + // Destroys any owned value. Calling this function leaves the class in a state + // where the only valid operations is to assign it a new value. + // + // We force a call via std::move(lazy_mutable_copy).dispose() to make it + // clearer that lazy_mutable_copy shouldn't really be used after this. + void dispose() && { + ptr_ = nullptr; + copy_ = nullptr; + } private: - const T* original_; std::unique_ptr copy_; + const T* ptr_ = nullptr; }; } // namespace operations_research diff --git a/ortools/util/solve_interrupter.cc b/ortools/util/solve_interrupter.cc new file mode 100644 index 0000000000..fc85b2b926 --- /dev/null +++ b/ortools/util/solve_interrupter.cc @@ -0,0 +1,102 @@ +// Copyright 2010-2024 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/util/solve_interrupter.h" + +#include +#include +#include +#include +#include + +#include "absl/synchronization/mutex.h" +#include "ortools/base/linked_hash_map.h" +#include "ortools/base/logging.h" +#include "ortools/base/strong_int.h" + +namespace operations_research { + +void SolveInterrupter::Interrupt() { + const absl::MutexLock lock(&mutex_); + + // Here we don't use compare_exchange_strong since we need to hold the lock + // before changing the value of interrupted_ anyway. So there is no need to + // use this complex function. + if (interrupted_.load()) { + // We must not call the callbacks more than once. + return; + } + + // We need to change this value while holding the lock since in + // AddInterruptionCallback() we must know if we need to call the new callback + // of if this function has called it. + interrupted_ = true; + + // We are holding the lock while calling callbacks. This make it impossible to + // call Interrupt(), AddInterruptionCallback(), or + // RemoveInterruptionCallback() from a callback but it ensures that external + // code that can modify callbacks_ will wait the end of Interrupt. + for (const auto& [callback_id, callback] : callbacks_) { + callback(); + } +} + +SolveInterrupter::CallbackId SolveInterrupter::AddInterruptionCallback( + Callback callback) { + const absl::MutexLock lock(&mutex_); + + // We must make this call while holding the lock since we want to be sure that + // the calls to the callbacks_ won't occur before we registered the new + // one. If we were not holding the lock, this could return false and before we + // could add the new callback to callbacks_, the Interrupt() function may + // still have called them. + // + // We make the call before putting the callback in the map to since we need to + // move it in place. + if (interrupted_.load()) { + callback(); + } + + const CallbackId id = next_callback_id_; + ++next_callback_id_; + CHECK(callbacks_.try_emplace(id, std::move(callback)).second); + return id; +} + +void SolveInterrupter::RemoveInterruptionCallback(CallbackId id) { + const absl::MutexLock lock(&mutex_); + CHECK_EQ(callbacks_.erase(id), 1) << "unregistered callback id: " << id; +} + +ScopedSolveInterrupterCallback::ScopedSolveInterrupterCallback( + SolveInterrupter* const interrupter, SolveInterrupter::Callback callback) + : interrupter_(interrupter), + callback_id_( + interrupter != nullptr + ? std::make_optional( + interrupter->AddInterruptionCallback(std::move(callback))) + : std::nullopt) {} + +ScopedSolveInterrupterCallback::~ScopedSolveInterrupterCallback() { + RemoveCallbackIfNecessary(); +} + +void ScopedSolveInterrupterCallback::RemoveCallbackIfNecessary() { + if (callback_id_) { + CHECK_NE(interrupter_, nullptr); + interrupter_->RemoveInterruptionCallback(*callback_id_); + callback_id_.reset(); + } +} + +} // namespace operations_research diff --git a/ortools/util/solve_interrupter.h b/ortools/util/solve_interrupter.h new file mode 100644 index 0000000000..7cfb044a10 --- /dev/null +++ b/ortools/util/solve_interrupter.h @@ -0,0 +1,149 @@ +// Copyright 2010-2024 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_UTIL_SOLVE_INTERRUPTER_H_ +#define OR_TOOLS_UTIL_SOLVE_INTERRUPTER_H_ + +#include +#include +#include +#include +#include + +#include "absl/base/thread_annotations.h" +#include "absl/strings/string_view.h" +#include "absl/synchronization/mutex.h" +#include "ortools/base/linked_hash_map.h" +#include "ortools/base/strong_int.h" + +namespace operations_research { + +// Interrupter used by solvers to know if/when they should interrupt the solve. +// +// Once triggered with Interrupt(), an interrupter can't be reset. It can be +// triggered from any thread. +// +// Thread-safety: APIs on this class are safe to call concurrently from multiple +// threads. +class SolveInterrupter { + public: + // Id used to identify a callback. + DEFINE_STRONG_INT_TYPE(CallbackId, int64_t); + + using Callback = std::function; + + SolveInterrupter() = default; + + SolveInterrupter(const SolveInterrupter&) = delete; + SolveInterrupter& operator=(const SolveInterrupter&) = delete; + + // Interrupts the solve as soon as possible. + // + // Once requested the interruption can't be reset. The user should use a new + // SolveInterrupter for later solves. + // + // It is safe to call this function multiple times. Only the first call will + // have visible effects; other calls will be ignored. + void Interrupt(); + + // Returns true if the solve interruption has been requested. + // + // This API is fast; it costs the read of an atomic. + inline bool IsInterrupted() const { return interrupted_.load(); } + + // Registers a callback to be called when the interruption is requested. + // + // The callback is immediately called if the interrupter has already been + // triggered or if it is triggered during the registration. This is typically + // useful for a solver implementation so that it does not have to test + // IsInterrupted() to do the same thing it does in the callback. Simply + // registering the callback is enough. + // + // The callback function can't make calls to AddInterruptionCallback(), + // RemoveInterruptionCallback() and Interrupt(). This would result is a + // deadlock. Calling IsInterrupted() is fine though. + CallbackId AddInterruptionCallback(Callback callback); + + // Unregisters a callback previously registered. It fails (with a CHECK) if + // the callback was already unregistered or unkonwn. After this calls returns, + // the caller can assume the callback won't be called. + // + // This function can't be called from a callback since this would result in a + // deadlock. + void RemoveInterruptionCallback(CallbackId id); + + private: + // This atomic must never be reset to false! + // + // The mutex_ should be held when setting it to true. + std::atomic interrupted_ = false; + + absl::Mutex mutex_; + + // The id to use for the next registered callback. + CallbackId next_callback_id_ ABSL_GUARDED_BY(mutex_) = {}; + + // The list of callbacks. We use a linked_hash_map to make sure the order of + // calls to callback when the interrupter is triggered is stable. + gtl::linked_hash_map callbacks_ ABSL_GUARDED_BY(mutex_); +}; + +// Class implementing RAII for interruption callbacks. +// +// Usage: +// +// SolveInterrupter* const interrupter = ...; +// { +// const ScopedSolveInterrupterCallback scoped_intr_cb(interrupter, [](){ +// // Do something when/if interrupter is not nullptr and is triggered. +// } +// ... +// } +// // At this point, the callback will have been removed. +// +// The function RemoveCallbackIfNecessary() can be used to remove the callback +// before the destruction of this object. +class ScopedSolveInterrupterCallback { + public: + // Adds a callback to the interrupter if it is not nullptr. Does nothing when + // interrupter is nullptr. + ScopedSolveInterrupterCallback(SolveInterrupter* interrupter, + SolveInterrupter::Callback callback); + + ScopedSolveInterrupterCallback(const ScopedSolveInterrupterCallback&) = + delete; + ScopedSolveInterrupterCallback& operator=( + const ScopedSolveInterrupterCallback&) = delete; + + // Removes the callback if necessary. + ~ScopedSolveInterrupterCallback(); + + // Removes the callback from the interrupter. If it has already been removed + // by a previous call or if a null interrupter was passed to the constructor, + // this function has no effect. + void RemoveCallbackIfNecessary(); + + // Returns the optional interrupter. + SolveInterrupter* interrupter() const { return interrupter_; } + + private: + // Optional interrupter. + SolveInterrupter* const interrupter_; + + // Unset after the callback has been reset. + std::optional callback_id_; +}; + +} // namespace operations_research + +#endif // OR_TOOLS_UTIL_SOLVE_INTERRUPTER_H_