diff --git a/ortools/linear_solver/gurobi_interface.cc b/ortools/linear_solver/gurobi_interface.cc index 219f771be1..5c4a6d2486 100644 --- a/ortools/linear_solver/gurobi_interface.cc +++ b/ortools/linear_solver/gurobi_interface.cc @@ -83,7 +83,7 @@ class GurobiInterface : public MPSolverInterface { // Solves the problem using the parameter values specified. MPSolver::ResultStatus Solve(const MPSolverParameters& param) override; absl::optional DirectlySolveProto( - const MPModelRequest& request) override; + const MPModelRequest& request, std::atomic* interrupt) override; // Writes the model. void Write(const std::string& filename) override; @@ -160,7 +160,7 @@ class GurobiInterface : public MPSolverInterface { return 0.0; } - // TODO(user,user): Not yet working. + // TODO(user): Not yet working. LOG(DFATAL) << "ComputeExactConditionNumber not implemented for" << " GUROBI_LINEAR_PROGRAMMING"; return 0.0; @@ -717,7 +717,7 @@ bool GurobiInterface::AddIndicatorConstraint(MPConstraint* const ct) { return !IsContinuous(); } -void GurobiInterface::AddVariable(MPVariable* const ct) { +void GurobiInterface::AddVariable(MPVariable* const var) { sync_status_ = MUST_RELOAD; } @@ -1245,7 +1245,7 @@ MPSolver::ResultStatus GurobiInterface::Solve(const MPSolverParameters& param) { result_status_ = MPSolver::UNBOUNDED; break; case GRB_INF_OR_UNBD: - // TODO(user,user): We could introduce our own "infeasible or + // TODO(user): We could introduce our own "infeasible or // unbounded" status. result_status_ = MPSolver::INFEASIBLE; break; @@ -1318,7 +1318,10 @@ MPSolver::ResultStatus GurobiInterface::Solve(const MPSolverParameters& param) { } absl::optional GurobiInterface::DirectlySolveProto( - const MPModelRequest& request) { + const MPModelRequest& request, std::atomic* interrupt) { + // Interruption via atomic is not directly supported by Gurobi. + if (interrupt != nullptr) return absl::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, env_); @@ -1362,7 +1365,7 @@ bool GurobiInterface::NextSolution() { var->set_solution_value( grb_variable_values.at(mp_var_to_gurobi_var_.at(i))); } - // TODO(user,user): This reset may not be necessary, investigate. + // TODO(user): This reset may not be necessary, investigate. GRBresetparams(GRBgetenv(model_)); return true; } diff --git a/ortools/linear_solver/linear_solver.cc b/ortools/linear_solver/linear_solver.cc index 859c845c9b..04b110df45 100644 --- a/ortools/linear_solver/linear_solver.cc +++ b/ortools/linear_solver/linear_solver.cc @@ -859,7 +859,7 @@ void AppendStatusStr(const std::string& msg, MPSolutionResponse* response) { // static void MPSolver::SolveWithProto(const MPModelRequest& model_request, MPSolutionResponse* response, - const std::atomic* interrupt) { + std::atomic* interrupt) { CHECK(response != nullptr); if (interrupt != nullptr && @@ -880,13 +880,11 @@ void MPSolver::SolveWithProto(const MPModelRequest& model_request, // 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. - if (interrupt == nullptr) { - auto optional_response = - solver.interface_->DirectlySolveProto(model_request); - if (optional_response) { - *response = std::move(optional_response).value(); - return; - } + auto optional_response = + solver.interface_->DirectlySolveProto(model_request, interrupt); + if (optional_response) { + *response = std::move(optional_response).value(); + return; } const absl::optional> optional_model = @@ -950,7 +948,7 @@ void MPSolver::SolveWithProto(const MPModelRequest& model_request, constexpr absl::Duration kPollDelay = absl::Microseconds(100); constexpr absl::Duration kMaxInterruptionDelay = absl::Seconds(10); - while (!interrupt) { + while (!interrupt->load()) { if (solve_finished.HasBeenNotified()) return; absl::SleepFor(kPollDelay); } @@ -1004,12 +1002,12 @@ void MPSolver::SolveWithProto(const MPModelRequest& model_request, thread_pool.StartWorkers(); thread_pool.Schedule(polling_func); - // Make sure the interruption notification didn't arrived while waiting to + // Make sure the interruption notification didn't arrive while waiting to // be scheduled. - if (!interrupt) { + if (!interrupt->load()) { solver.Solve(); solver.FillSolutionResponseProto(response); - } else { + } else { // *interrupt == true response->set_status(MPSOLVER_CANCELLED_BY_USER); response->set_status_str( "Solve not started, because the user set the atomic in " @@ -1220,15 +1218,18 @@ absl::Status MPSolver::LoadSolutionFromProto(const MPSolutionResponse& response, } void MPSolver::Clear() { + { + absl::MutexLock lock(&global_count_mutex_); + global_num_variables_ += variables_.size(); + global_num_constraints_ += constraints_.size(); + } MutableObjective()->Clear(); gtl::STLDeleteElements(&variables_); gtl::STLDeleteElements(&constraints_); - variables_.clear(); if (variable_name_to_index_) { variable_name_to_index_->clear(); } variable_is_extracted_.clear(); - constraints_.clear(); if (constraint_name_to_index_) { constraint_name_to_index_->clear(); } @@ -1742,6 +1743,25 @@ bool MPSolver::SupportsCallbacks() const { return interface_->SupportsCallbacks(); } +// Global counters. +absl::Mutex MPSolver::global_count_mutex_(absl::kConstInit); +int64_t MPSolver::global_num_variables_ = 0; +int64_t MPSolver::global_num_constraints_ = 0; + +// static +int64_t MPSolver::global_num_variables() { + // Why not ReaderMutexLock? See go/totw/197#when-are-shared-locks-useful. + absl::MutexLock lock(&global_count_mutex_); + return global_num_variables_; +} + +// static +int64_t MPSolver::global_num_constraints() { + // Why not ReaderMutexLock? See go/totw/197#when-are-shared-locks-useful. + absl::MutexLock lock(&global_count_mutex_); + return global_num_constraints_; +} + bool MPSolverResponseStatusIsRpcError(MPSolverResponseStatus status) { switch (status) { // Cases that don't yield an RPC error when they happen on the server. diff --git a/ortools/linear_solver/linear_solver.h b/ortools/linear_solver/linear_solver.h index 9d54216fcd..8c8635a735 100644 --- a/ortools/linear_solver/linear_solver.h +++ b/ortools/linear_solver/linear_solver.h @@ -552,13 +552,16 @@ class MPSolver { */ static void SolveWithProto(const MPModelRequest& model_request, MPSolutionResponse* response, - const std::atomic* interrupt = nullptr); + // `interrupt` is non-const because the internal + // solver may set it to true itself, in some cases. + std::atomic* interrupt = nullptr); static bool SolverTypeSupportsInterruption( const MPModelRequest::SolverType solver) { // Interruption requires that MPSolver::InterruptSolve is supported for the // underlying solver. Interrupting requests using SCIP is also not supported - // as of 2021/08/23, since InterruptSolve is not thread-safe for SCIP. + // as of 2021/08/23, since InterruptSolve is not go/thread-safe + // for SCIP (see e.g. cl/350545631 for details). return solver == MPModelRequest::GLOP_LINEAR_PROGRAMMING || solver == MPModelRequest::GUROBI_LINEAR_PROGRAMMING || solver == MPModelRequest::GUROBI_MIXED_INTEGER_PROGRAMMING || @@ -800,6 +803,12 @@ class MPSolver { void SetCallback(MPCallback* mp_callback); bool SupportsCallbacks() const; + // Global counters of variables and constraints ever created across all + // MPSolver instances. Those are only updated after the destruction + // (or Clear()) of each MPSolver instance. + static int64_t global_num_variables(); + static int64_t global_num_constraints(); + // DEPRECATED: Use TimeLimit() and SetTimeLimit(absl::Duration) instead. // NOTE: These deprecated functions used the convention time_limit = 0 to mean // "no limit", which now corresponds to time_limit_ = InfiniteDuration(). @@ -905,6 +914,10 @@ class MPSolver { // Permanent storage for SetSolverSpecificParametersAsString(). std::string solver_specific_parameter_string_; + static absl::Mutex global_count_mutex_; + static int64_t global_num_variables_ ABSL_GUARDED_BY(global_count_mutex_); + static int64_t global_num_constraints_ ABSL_GUARDED_BY(global_count_mutex_); + MPSolverResponseStatus LoadModelFromProtoInternal( const MPModelProto& input_model, bool clear_names, bool check_model_validity, std::string* error_message); @@ -1564,11 +1577,17 @@ class MPSolverInterface { // solution is optimal. virtual MPSolver::ResultStatus Solve(const MPSolverParameters& param) = 0; - // Directly solves a MPModelRequest, bypassing the MPSolver data structures - // entirely. Returns {} (eg. absl::nullopt) if the feature is not supported by - // the underlying solver. + // Attempts to directly solve 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 absl::optional DirectlySolveProto( - const MPModelRequest& request) { + const MPModelRequest& request, + // `interrupt` is non-const because the internal + // solver may set it to true itself, in some cases. + std::atomic* interrupt) { return absl::nullopt; } diff --git a/ortools/linear_solver/python/linear_solver.i b/ortools/linear_solver/python/linear_solver.i index c46b1b721f..02fab644a5 100644 --- a/ortools/linear_solver/python/linear_solver.i +++ b/ortools/linear_solver/python/linear_solver.i @@ -88,6 +88,13 @@ from ortools.linear_solver.linear_solver_natural_api import VariableExpr return error_message; } + // Ditto for LoadModelFromProtoWithUniqueNamesOrDie() + std::string LoadModelFromProtoWithUniqueNamesOrDie(const operations_research::MPModelProto& input_model) { + std::string error_message; + $self->LoadModelFromProtoWithUniqueNamesOrDie(input_model, &error_message); + return error_message; + } + // Change the API of LoadSolutionFromProto() to simply return a boolean. bool LoadSolutionFromProto( const operations_research::MPSolutionResponse& response, diff --git a/ortools/linear_solver/samples/integer_programming_example.cc b/ortools/linear_solver/samples/integer_programming_example.cc index 5e67ec6a7a..468e6a112c 100644 --- a/ortools/linear_solver/samples/integer_programming_example.cc +++ b/ortools/linear_solver/samples/integer_programming_example.cc @@ -71,7 +71,6 @@ void IntegerProgrammingExample() { // [END objective] // [START solve] - solver->SetNumThreads(1); const MPSolver::ResultStatus result_status = solver->Solve(); // Check that the problem has an optimal solution. if (result_status != MPSolver::OPTIMAL) { diff --git a/ortools/linear_solver/samples/integer_programming_example.py b/ortools/linear_solver/samples/integer_programming_example.py index e6bfb5363a..1ec2c9e35f 100755 --- a/ortools/linear_solver/samples/integer_programming_example.py +++ b/ortools/linear_solver/samples/integer_programming_example.py @@ -64,7 +64,6 @@ def IntegerProgrammingExample(): # Solve the problem and print the solution. # [START print_solution] - solver.SetNumThreads(1) solver.Solve() # Print the objective value of the solution. print('Maximum objective function value = %d' % solver.Objective().Value()) diff --git a/ortools/linear_solver/sat_interface.cc b/ortools/linear_solver/sat_interface.cc index 3c4982774a..31552a620a 100644 --- a/ortools/linear_solver/sat_interface.cc +++ b/ortools/linear_solver/sat_interface.cc @@ -46,6 +46,8 @@ class SatInterface : public MPSolverInterface { // ----- Solve ----- MPSolver::ResultStatus Solve(const MPSolverParameters& param) override; + absl::optional DirectlySolveProto( + const MPModelRequest& request, std::atomic* interrupt) override; bool InterruptSolve() override; // ----- Model modifications and extraction ----- @@ -101,7 +103,7 @@ class SatInterface : public MPSolverInterface { std::atomic interrupt_solve_; sat::SatParameters parameters_; - int num_threads_ = 8; + int num_threads_ = 0; }; SatInterface::SatInterface(MPSolver* const solver) @@ -182,6 +184,26 @@ MPSolver::ResultStatus SatInterface::Solve(const MPSolverParameters& param) { return result_status_; } +absl::optional SatInterface::DirectlySolveProto( + const MPModelRequest& request, std::atomic* interrupt) { + absl::StatusOr status_or = + SatSolveProto(request, interrupt); + if (status_or.ok()) return std::move(status_or).value(); + if (request.enable_internal_solver_output()) { + LOG(INFO) << "Failed SAT solve: " << status_or.status(); + } + MPSolutionResponse response; + // As of 2021-08, the sole non-OK status returned by SatSolveProto is an + // INVALID_ARGUMENT error caused by invalid solver parameters. + // TODO(user): Move that conversion to SatSolveProto, which should always + // return a MPSolutionResponse, even for errors. + response.set_status(absl::IsInvalidArgument(status_or.status()) + ? MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS + : MPSOLVER_ABNORMAL); + response.set_status_str(status_or.status().ToString()); + return response; +} + bool SatInterface::InterruptSolve() { interrupt_solve_ = true; return true; @@ -263,10 +285,9 @@ void SatInterface::ExtractNewConstraints() { NonIncrementalChange(); } void SatInterface::ExtractObjective() { NonIncrementalChange(); } void SatInterface::SetParameters(const MPSolverParameters& param) { - // By default, we use 8 threads as it allows to try a good set of orthogonal - // parameters. This can be overridden by the user. parameters_.set_num_search_workers(num_threads_); parameters_.set_log_search_progress(!quiet_); + parameters_.set_linearization_level(2); SetCommonParameters(param); } diff --git a/ortools/linear_solver/sat_proto_solver.cc b/ortools/linear_solver/sat_proto_solver.cc index e99c3c4090..33c017ee74 100644 --- a/ortools/linear_solver/sat_proto_solver.cc +++ b/ortools/linear_solver/sat_proto_solver.cc @@ -67,11 +67,9 @@ MPSolverResponseStatus ToMPSolverResponseStatus(sat::CpSolverStatus status, absl::StatusOr SatSolveProto( MPModelRequest request, std::atomic* interrupt_solve, - std::function logging_callback) { - // By default, we use 8 threads as it allows to try a good set of orthogonal - // parameters. This can be overridden by the user. + std::function logging_callback, + std::function solution_callback) { sat::SatParameters params; - params.set_num_search_workers(8); params.set_log_search_progress(request.enable_internal_solver_output()); if (request.has_solver_specific_parameters()) { // See EncodeSatParametersAsString() documentation. @@ -91,9 +89,9 @@ absl::StatusOr SatSolveProto( } } if (request.has_solver_time_limit_seconds()) { - params.set_max_time_in_seconds( - static_cast(request.solver_time_limit_seconds()) / 1000.0); + params.set_max_time_in_seconds(request.solver_time_limit_seconds()); } + params.set_linearization_level(2); // 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 @@ -125,18 +123,20 @@ absl::StatusOr SatSolveProto( const glop::GlopParameters glop_params; MPModelProto* const mp_model = request.mutable_model(); std::vector> for_postsolve; - const auto status = - ApplyMipPresolveSteps(glop_params, mp_model, &for_postsolve, &logger); - if (status == MPSolverResponseStatus::MPSOLVER_INFEASIBLE) { - if (params.log_search_progress()) { - // This is needed for our benchmark scripts. - sat::CpSolverResponse cp_response; - cp_response.set_status(sat::CpSolverStatus::INFEASIBLE); - LOG(INFO) << CpSolverResponseStats(cp_response); + if (!params.enumerate_all_solutions()) { + const auto status = + ApplyMipPresolveSteps(glop_params, mp_model, &for_postsolve, &logger); + if (status == MPSolverResponseStatus::MPSOLVER_INFEASIBLE) { + if (params.log_search_progress()) { + // This is needed for our benchmark scripts. + sat::CpSolverResponse cp_response; + cp_response.set_status(sat::CpSolverStatus::INFEASIBLE); + LOG(INFO) << CpSolverResponseStats(cp_response); + } + response.set_status(MPSolverResponseStatus::MPSOLVER_INFEASIBLE); + response.set_status_str("Problem proven infeasible during MIP presolve"); + return response; } - response.set_status(MPSolverResponseStatus::MPSOLVER_INFEASIBLE); - response.set_status_str("Problem proven infeasible during MIP presolve"); - return response; } // We need to do that before the automatic detection of integers. @@ -210,6 +210,33 @@ absl::StatusOr SatSolveProto( interrupt_solve); } + auto post_solve = [&](const sat::CpSolverResponse& cp_response) { + MPSolution mp_solution; + mp_solution.set_objective_value(cp_response.objective_value()); + // Postsolve the bound shift and scaling. + glop::ProblemSolution glop_solution((glop::RowIndex(old_num_constraints)), + (glop::ColIndex(old_num_variables))); + for (int v = 0; v < glop_solution.primal_values.size(); ++v) { + glop_solution.primal_values[glop::ColIndex(v)] = + static_cast(cp_response.solution(v)) / var_scaling[v]; + } + for (int i = for_postsolve.size(); --i >= 0;) { + for_postsolve[i]->RecoverSolution(&glop_solution); + } + for (int v = 0; v < glop_solution.primal_values.size(); ++v) { + mp_solution.add_variable_value( + glop_solution.primal_values[glop::ColIndex(v)]); + } + return mp_solution; + }; + + if (solution_callback != nullptr) { + sat_model.Add(sat::NewFeasibleSolutionObserver( + [&](const sat::CpSolverResponse& cp_response) { + solution_callback(post_solve(cp_response)); + })); + } + // Solve. const sat::CpSolverResponse cp_response = sat::SolveCpModel(cp_model, &sat_model); @@ -223,20 +250,9 @@ absl::StatusOr SatSolveProto( response.status() == MPSOLVER_OPTIMAL) { response.set_objective_value(cp_response.objective_value()); response.set_best_objective_bound(cp_response.best_objective_bound()); - - // Postsolve the bound shift and scaling. - glop::ProblemSolution solution((glop::RowIndex(old_num_constraints)), - (glop::ColIndex(old_num_variables))); - for (int v = 0; v < solution.primal_values.size(); ++v) { - solution.primal_values[glop::ColIndex(v)] = - static_cast(cp_response.solution(v)) / var_scaling[v]; - } - for (int i = for_postsolve.size(); --i >= 0;) { - for_postsolve[i]->RecoverSolution(&solution); - } - for (int v = 0; v < solution.primal_values.size(); ++v) { - response.add_variable_value(solution.primal_values[glop::ColIndex(v)]); - } + MPSolution post_solved_solution = post_solve(cp_response); + *response.mutable_variable_value() = + std::move(*post_solved_solution.mutable_variable_value()); } return response; diff --git a/ortools/linear_solver/sat_proto_solver.h b/ortools/linear_solver/sat_proto_solver.h index ff6ac5c042..00c77e0089 100644 --- a/ortools/linear_solver/sat_proto_solver.h +++ b/ortools/linear_solver/sat_proto_solver.h @@ -43,9 +43,15 @@ namespace operations_research { // log_to_stdout is true so even with a callback, the logs will appear on stdout // too unless log_to_stdout is set to false. The enable_internal_solver_output // in the request will act as the SAT parameter log_search_progress. +// +// The optional solution_callback will be called on each intermediate solution +// found by the solver. The solver may call solution_callback from multiple +// threads, but it will ensure that at most one thread executes +// solution_callback at a time. absl::StatusOr SatSolveProto( MPModelRequest request, std::atomic* interrupt_solve = nullptr, - std::function logging_callback = nullptr); + std::function logging_callback = nullptr, + std::function solution_callback = nullptr); // Returns a string that should be used in MPModelRequest's // solver_specific_parameters field to encode the SAT parameters. diff --git a/ortools/linear_solver/scip_interface.cc b/ortools/linear_solver/scip_interface.cc index 47181fda52..058d4089b7 100644 --- a/ortools/linear_solver/scip_interface.cc +++ b/ortools/linear_solver/scip_interface.cc @@ -66,7 +66,7 @@ class SCIPInterface : public MPSolverInterface { void SetOptimizationDirection(bool maximize) override; MPSolver::ResultStatus Solve(const MPSolverParameters& param) override; absl::optional DirectlySolveProto( - const MPModelRequest& request) override; + const MPModelRequest& request, std::atomic* interrupt) override; void Reset() override; void SetVariableBounds(int var_index, double lb, double ub) override; @@ -860,10 +860,13 @@ void SCIPInterface::SetSolution(SCIP_SOL* solution) { } absl::optional SCIPInterface::DirectlySolveProto( - const MPModelRequest& request) { + const MPModelRequest& request, std::atomic* interrupt) { // ScipSolveProto doesn't solve concurrently. if (solver_->GetNumThreads() > 1) return absl::nullopt; + // Interruption via atomic is not directly supported by SCIP. + if (interrupt != nullptr) return absl::nullopt; + 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 diff --git a/ortools/linear_solver/scip_proto_solver.cc b/ortools/linear_solver/scip_proto_solver.cc index 6c4d5d7ef4..256a719db4 100644 --- a/ortools/linear_solver/scip_proto_solver.cc +++ b/ortools/linear_solver/scip_proto_solver.cc @@ -882,7 +882,6 @@ absl::StatusOr ScipSolveProto( // NOTE(user): As of SCIP 7.0.1, getting the pointer to all // solutions is as fast as getting the pointer to the best solution. - // See google3/scip/scip_sol.c?l=2264&rcl=322332899. SCIP_SOL** const scip_solutions = SCIPgetSols(scip); response.set_objective_value(SCIPgetSolOrigObj(scip, scip_solutions[0])); response.set_best_objective_bound(SCIPgetDualbound(scip));