diff --git a/ortools/sat/BUILD b/ortools/sat/BUILD index 96ec6d5ced..64c7931b29 100644 --- a/ortools/sat/BUILD +++ b/ortools/sat/BUILD @@ -87,6 +87,7 @@ cc_library( deps = [ ":cp_model_cc_proto", ":cp_model_loader", + ":cp_model_search", ":cp_model_utils", ":integer", ":integer_search", diff --git a/ortools/sat/cp_model_search.cc b/ortools/sat/cp_model_search.cc index 97e662624f..11f761042e 100644 --- a/ortools/sat/cp_model_search.cc +++ b/ortools/sat/cp_model_search.cc @@ -398,6 +398,9 @@ SatParameters DiversifySearchParameters(const SatParameters& params, // TODO(user): Better stats in the multi thread case. // Should we cumul conflicts, branches... ? +// +// TODO(user): Rename and make sure this also work for pure feasibility problem. +// We do not want to special case this too much by calling other functions. bool MergeOptimizationSolution(const CpSolverResponse& response, bool maximize, CpSolverResponse* best) { // In all cases, we always update the best objective bound similarly. @@ -420,7 +423,7 @@ bool MergeOptimizationSolution(const CpSolverResponse& response, bool maximize, maximize ? response.objective_value() > best->objective_value() : response.objective_value() < best->objective_value(); // TODO(user): return OPTIMAL if objective is tight. - if (is_improving) { + if (is_improving || best->status() == CpSolverStatus::UNKNOWN) { // Overwrite solution and fix best_objective_bound. *best = response; return true; @@ -430,6 +433,15 @@ bool MergeOptimizationSolution(const CpSolverResponse& response, bool maximize, // best_objective_bound. return true; } + + // We still override an equivalent solution, but return false. + // + // TODO(user): this is needed for feasibility (enumerate all solution), + // but might also add diversity to the LNS starting solution. + if (response.objective_value() == best->objective_value() && + best->status() != CpSolverStatus::OPTIMAL) { + *best = response; + } return false; } case CpSolverStatus::INFEASIBLE: { diff --git a/ortools/sat/cp_model_solver.cc b/ortools/sat/cp_model_solver.cc index 37fbd8cd0c..954a5aad63 100644 --- a/ortools/sat/cp_model_solver.cc +++ b/ortools/sat/cp_model_solver.cc @@ -1173,15 +1173,6 @@ std::function NewFeasibleSolutionObserver( }; } -struct SynchronizationFunction { - std::function f; -}; - -void SetSynchronizationFunction(std::function f, - Model* model) { - model->GetOrCreate()->f = std::move(f); -} - #if !defined(__PORTABLE_PLATFORM__) // TODO(user): Support it on android. std::function NewSatParameters( @@ -1208,27 +1199,45 @@ std::function NewSatParameters( } namespace { + +// Returns a valid response with valid objective bounds. +CpSolverResponse GetDefaultResponse(const CpModelProto& model_proto) { + CpSolverResponse response; + response.set_status(CpSolverStatus::UNKNOWN); + if (model_proto.has_objective()) { + const double kInfinity = std::numeric_limits::infinity(); + const bool maximize = model_proto.objective().scaling_factor() < 0.0; + if (maximize) { + response.set_objective_value(-kInfinity); + response.set_best_objective_bound(kInfinity); + } else { + response.set_objective_value(kInfinity); + response.set_best_objective_bound(-kInfinity); + } + } + return response; +} + // Because we also use this function for postsolve, we call it with // is_real_solve set to true and avoid doing non-useful work in this case. CpSolverResponse SolveCpModelInternal( const CpModelProto& model_proto, bool is_real_solve, + bool log_sequential_search, const std::function& external_solution_observer, + SharedResponseManager* shared_response_manager, SharedBoundsManager* shared_bounds_manager, WallTimer* wall_timer, Model* model) { const bool log_search = model->GetOrCreate()->log_search_progress() || VLOG_IS_ON(1); - const bool worker_is_in_parallel_search = model->Get() != nullptr; - const bool is_lns = model->GetOrCreate()->use_lns(); - - if (!worker_is_in_parallel_search && is_real_solve && !is_lns && log_search) { + if (log_search && log_sequential_search) { LOG(INFO) << absl::StrFormat("*** starting to load the model at %.2fs", wall_timer->Get()); } - // Initialize a default invalid response. - CpSolverResponse response; - response.set_status(CpSolverStatus::MODEL_INVALID); + + // Initialize a default response. + CpSolverResponse response = GetDefaultResponse(model_proto); auto fill_response_statistics = [&response, model, wall_timer]() { auto* sat_solver = model->Get(); @@ -1427,6 +1436,9 @@ CpSolverResponse SolveCpModelInternal( // TODO(user): remove argument as it isn't used. Pass solution info instead? std::string solution_info; + if (model->GetOrCreate() != nullptr) { + solution_info = model->GetOrCreate()->worker_name; + } const auto solution_observer = [&model_proto, &response, &num_solutions, &model, &solution_info, &external_solution_observer, objective_var, @@ -1440,53 +1452,19 @@ CpSolverResponse SolveCpModelInternal( external_solution_observer(response); }; - // Objective bounds reporting and sharing. - ObjectiveSynchronizationHelper* helper = - model->GetOrCreate(); - - if (!is_lns && model_proto.has_objective()) { - // Detect sequential mode, register callbacks in that case. - if (!worker_is_in_parallel_search) { - model->GetOrCreate()->worker_id = 0; - auto* integer_trail = model->Get(); - const CpObjectiveProto& obj = model_proto.objective(); - - helper->get_external_best_objective = [&response, integer_trail, &obj, - objective_var]() { - return response.status() != CpSolverStatus::MODEL_INVALID - ? response.objective_value() - : ScaleObjectiveValue( - obj, integer_trail->LevelZeroUpperBound(objective_var) - .value() + - 1); - }; - - helper->get_external_best_bound = [&response, integer_trail, &obj, - objective_var]() { - return response.status() != CpSolverStatus::MODEL_INVALID - ? response.best_objective_bound() - : ScaleObjectiveValue( - obj, integer_trail->LevelZeroLowerBound(objective_var) - .value()); - }; - - helper->set_external_best_bound = - [&response](double objective_value, double objective_best_bound) { - response.set_best_objective_bound(objective_best_bound); - }; - } - + if (shared_response_manager != nullptr && model_proto.has_objective()) { // Watch improved objective best bounds in regular search, or core based // search. It should be disabled for LNS. RegisterObjectiveBestBoundExport(model_proto, log_search, objective_var, - wall_timer, model); - } + wall_timer, shared_response_manager, + model); - // Import objective bounds. - // TODO(user): Support bounds import in LNS and Core based search. - if (model->GetOrCreate()->share_objective_bounds() && - worker_is_in_parallel_search && model_proto.has_objective()) { - RegisterObjectiveBoundsImport(model); + // Import objective bounds. + // TODO(user): Support objective bounds import in LNS and Core based + // search. + if (model->GetOrCreate()->share_objective_bounds()) { + RegisterObjectiveBoundsImport(shared_response_manager, model); + } } // Level zero variable bounds sharing. @@ -1498,7 +1476,7 @@ CpSolverResponse SolveCpModelInternal( model); } - if (!worker_is_in_parallel_search && is_real_solve && !is_lns && log_search) { + if (log_search && log_sequential_search) { LOG(INFO) << absl::StrFormat("*** starting sequential search at %.2fs", wall_timer->Get()); } @@ -1556,8 +1534,10 @@ CpSolverResponse SolveCpModelInternal( } } if (hint_is_valid) { - solution_info = "hint"; + const int old_size = solution_info.size(); + solution_info += " [hint] "; solution_observer(*model); + solution_info.resize(old_size); CHECK(SolutionIsFeasible( model_proto, std::vector(response.solution().begin(), response.solution().end()))); @@ -1591,7 +1571,6 @@ CpSolverResponse SolveCpModelInternal( old_conflict_limit); } - solution_info = ""; if (!model_proto.has_objective()) { while (true) { status = SolveIntegerProblemWithLazyEncoding( @@ -1740,7 +1719,9 @@ void PostsolveResponse(const CpModelProto& model_proto, postsolve_model.Add(operations_research::sat::NewSatParameters(params)); } const CpSolverResponse postsolve_response = SolveCpModelInternal( - mapping_proto, false, [](const CpSolverResponse&) {}, + mapping_proto, /*is_real_solve=*/false, /*log_sequential_search=*/false, + [](const CpSolverResponse&) {}, + /*shared_response_manager=*/nullptr, /*shared_bounds_manager=*/nullptr, wall_timer, &postsolve_model); CHECK_EQ(postsolve_response.status(), CpSolverStatus::FEASIBLE); @@ -1765,6 +1746,7 @@ void PostsolveResponse(const CpModelProto& model_proto, } } +// TODO(user): Uniformize this function with the other one. CpSolverResponse SolvePureSatModel(const CpModelProto& model_proto, WallTimer* wall_timer, Model* model) { std::unique_ptr solver(new SatSolver()); @@ -1953,26 +1935,32 @@ bool UpdateDomain(int64 new_lb, int64 new_ub, CpSolverResponse SolveCpModelWithLNS( const CpModelProto& model_proto, const std::function& observer, - int num_workers, int worker_id, SharedBoundsManager* shared_bounds_manager, - WallTimer* wall_timer, Model* model) { + int num_workers, int worker_id, + SharedResponseManager* shared_response_manager, + SharedBoundsManager* shared_bounds_manager, WallTimer* wall_timer, + Model* model) { SatParameters* parameters = model->GetOrCreate(); - parameters->set_stop_after_first_solution(true); + CpSolverResponse response; - auto* synchro = model->Get(); - if (synchro != nullptr && synchro->f != nullptr) { - response = synchro->f(); - } else { - response = - SolveCpModelInternal(model_proto, /*is_real_solve=*/true, observer, - shared_bounds_manager, wall_timer, model); + if (shared_response_manager != nullptr) { + response = shared_response_manager->GetBestResponse(); + } + if (response.solution().empty()) { + parameters->set_stop_after_first_solution(true); + response = SolveCpModelInternal(model_proto, /*is_real_solve=*/true, + /*log_sequential_search=*/false, observer, + /*shared_response_manager=*/nullptr, + /*shared_bounds_manager=*/nullptr, + wall_timer, model); + shared_response_manager->MergeIntoBestResponse(response); } CpModelProto mutable_model_proto = model_proto; if (response.status() != CpSolverStatus::FEASIBLE) { return response; } + const bool focus_on_decision_variables = parameters->lns_focus_on_decision_variables(); - NeighborhoodGeneratorHelper helper(&mutable_model_proto, focus_on_decision_variables); @@ -2013,7 +2001,7 @@ CpSolverResponse SolveCpModelWithLNS( TimeLimit* limit = model->GetOrCreate(); double deterministic_time = 0.1; - int num_no_progress = 0; + int num_consecutive_not_fully_solved = 0; const int num_threads = std::max(1, parameters->lns_num_threads()); int64 total_num_calls = 0; @@ -2021,17 +2009,8 @@ CpSolverResponse SolveCpModelWithLNS( num_threads, [&]() { // Synchronize with external world. - auto* synchro = model->Get(); - if (synchro != nullptr && synchro->f != nullptr) { - const CpSolverResponse candidate_response = synchro->f(); - if (!candidate_response.solution().empty()) { - double coeff = model_proto.objective().scaling_factor(); - if (coeff == 0.0) coeff = 1.0; - if (candidate_response.objective_value() * coeff < - response.objective_value() * coeff) { - response = candidate_response; - } - } + if (shared_response_manager != nullptr) { + response = shared_response_manager->GetBestResponse(); } // Update the bounds on mutable model proto. @@ -2068,9 +2047,9 @@ CpSolverResponse SolveCpModelWithLNS( // If we didn't see any progress recently, bump the time limit. // TODO(user): Tune the logic and expose the parameters. - if (num_no_progress > 100) { + if (num_consecutive_not_fully_solved > 100) { deterministic_time *= 1.1; - num_no_progress = 0; + num_consecutive_not_fully_solved = 0; } return limit->LimitReached() || response.objective_value() == response.best_objective_bound(); @@ -2118,30 +2097,31 @@ CpSolverResponse SolveCpModelWithLNS( &postsolve_mapping); local_response = SolveCpModelInternal( local_problem, /*is_real_solve=*/true, + /*log_sequential_search=*/false, [](const CpSolverResponse& response) {}, + /*shared_response_manager=*/nullptr, /*shared_bounds_manager=*/nullptr, wall_timer, &local_model); PostsolveResponse(model_proto, mapping_proto, postsolve_mapping, - wall_timer, &local_response); + wall_timer, &local_response); // Should be mutable? + local_response.set_solution_info( + absl::StrCat(local_response.solution_info(), " ", solution_info)); const bool neighborhood_is_reduced = neighborhood.is_reduced; - return [neighborhood_is_reduced, &num_no_progress, &model_proto, - &response, &difficulty, local_response, &observer, limit, - solution_info, &generators, selected_generator, - &total_num_calls]() { + return [neighborhood_is_reduced, &num_consecutive_not_fully_solved, + &model_proto, &response, &difficulty, local_response, &observer, + &generators, selected_generator, &total_num_calls]() { // TODO(user): This is not ideal in multithread because even though // the saved_difficulty will be the same for all thread, we will // Increase()/Decrease() the difficuty sequentially more than once. if (local_response.status() == CpSolverStatus::OPTIMAL || local_response.status() == CpSolverStatus::INFEASIBLE) { - if (neighborhood_is_reduced) { - difficulty.Increase(); - } else { - // We solved the full model here. - response = local_response; - } + num_consecutive_not_fully_solved = 0; + difficulty.Increase(); } else { + ++num_consecutive_not_fully_solved; difficulty.Decrease(); } + // Update the generator record. double objective_diff = 0.0; if (local_response.status() == CpSolverStatus::OPTIMAL || @@ -2162,47 +2142,34 @@ CpSolverResponse SolveCpModelWithLNS( << ", UCB1 Score: " << generators[selected_generator]->GetUCBScore(total_num_calls) << "]"; - if (local_response.status() == CpSolverStatus::FEASIBLE || - local_response.status() == CpSolverStatus::OPTIMAL) { - // If the objective are the same, we override the solution, - // otherwise we just ignore this local solution and increment - // num_no_progress. - double coeff = model_proto.objective().scaling_factor(); - if (coeff == 0.0) coeff = 1.0; - if (local_response.objective_value() * coeff >= - response.objective_value() * coeff) { - if (local_response.objective_value() * coeff > - response.objective_value() * coeff) { - return; - } - ++num_no_progress; - } else { - num_no_progress = 0; - } - // Update the global response. - *(response.mutable_solution()) = local_response.solution(); - response.set_objective_value(local_response.objective_value()); - response.set_wall_time(limit->GetElapsedTime()); - response.set_user_time(response.user_time() + - local_response.user_time()); - response.set_deterministic_time( - response.deterministic_time() + - local_response.deterministic_time()); - if (DEBUG_MODE || FLAGS_cp_model_check_intermediate_solutions) { - CHECK(SolutionIsFeasible( - model_proto, - std::vector(local_response.solution().begin(), - local_response.solution().end()))); - } - if (num_no_progress == 0) { // Improving solution. - response.set_solution_info(solution_info); - observer(response); + // Call the observer if we have a solution. + // + // TODO(user): depending on the problem, the bound sharing may or + // may not restrict the objective though. Uniformize the behavior. + if (local_response.status() == CpSolverStatus::OPTIMAL || + local_response.status() == CpSolverStatus::FEASIBLE) { + if (neighborhood_is_reduced) { + CpSolverResponse globally_valid_solution = + GetDefaultResponse(model_proto); + globally_valid_solution.set_status(CpSolverStatus::FEASIBLE); + *globally_valid_solution.mutable_solution() = + local_response.solution(); + globally_valid_solution.set_solution_info( + local_response.solution_info()); + globally_valid_solution.set_objective_value( + local_response.objective_value()); + observer(globally_valid_solution); + } else { + // We solved the full model here. + observer(local_response); } } }; }); + // TODO(user): move in MergeOptimizationSolution(), but only do that for + // optimization problem. if (response.status() == CpSolverStatus::FEASIBLE) { if (response.objective_value() == response.best_objective_bound()) { response.set_status(CpSolverStatus::OPTIMAL); @@ -2217,9 +2184,9 @@ CpSolverResponse SolveCpModelWithLNS( CpSolverResponse SolveCpModelParallel( const CpModelProto& model_proto, const std::function& observer, - WallTimer* wall_timer, Model* model) { + SharedResponseManager* shared_response_manager, WallTimer* wall_timer, + Model* model) { const SatParameters& params = *model->GetOrCreate(); - const int random_seed = params.random_seed(); CHECK(!params.enumerate_all_solutions()) << "Enumerating all solutions in parallel is not supported."; @@ -2231,20 +2198,6 @@ CpSolverResponse SolveCpModelParallel( stopped = model->GetOrCreate()->ExternalBooleanAsLimit(); } - const bool maximize = model_proto.objective().scaling_factor() < 0.0; - - CpSolverResponse best_response; - if (model_proto.has_objective()) { - const double kInfinity = std::numeric_limits::infinity(); - if (maximize) { - best_response.set_objective_value(-kInfinity); - best_response.set_best_objective_bound(kInfinity); - } else { - best_response.set_objective_value(kInfinity); - best_response.set_best_objective_bound(-kInfinity); - } - } - absl::Mutex mutex; // In the LNS threads, we wait for this notification before starting work. @@ -2255,6 +2208,12 @@ CpSolverResponse SolveCpModelParallel( model->GetOrCreate()->log_search_progress() || VLOG_IS_ON(1); + std::unique_ptr shared_bounds_manager; + if (model->GetOrCreate()->share_level_zero_bounds()) { + shared_bounds_manager = + absl::make_unique(num_search_workers, model_proto); + } + // Collect per-worker parameters and names. std::vector worker_parameters; std::vector worker_names; @@ -2269,79 +2228,6 @@ CpSolverResponse SolveCpModelParallel( "*** starting parallel search at %.2fs with %i workers: [ %s ]", wall_timer->Get(), num_search_workers, absl::StrJoin(worker_names, ", ")); - if (!model_proto.has_objective()) { - { - ThreadPool pool("Parallel_search", num_search_workers); - pool.StartWorkers(); - - for (int worker_id = 0; worker_id < num_search_workers; ++worker_id) { - const std::string worker_name = worker_names[worker_id]; - const SatParameters local_params = worker_parameters[worker_id]; - - pool.Schedule([&model_proto, stopped, local_params, &best_response, - &mutex, worker_name, worker_id, wall_timer, - log_search]() { - Model local_model; - local_model.Add(NewSatParameters(local_params)); - local_model.GetOrCreate()->RegisterExternalBooleanAsLimit( - stopped); - - // Stores info that will be used for logs in the local model. - WorkerInfo* worker_info = local_model.GetOrCreate(); - worker_info->worker_name = worker_name; - worker_info->worker_id = worker_id; - - const CpSolverResponse local_response = SolveCpModelInternal( - model_proto, true, [](const CpSolverResponse& response) {}, - /*shared_bounds_manager=*/nullptr, wall_timer, &local_model); - - absl::MutexLock lock(&mutex); - if (best_response.status() == CpSolverStatus::UNKNOWN) { - best_response = local_response; - } - if (local_response.status() != CpSolverStatus::UNKNOWN) { - CHECK_EQ(local_response.status(), best_response.status()); - LOG_IF(INFO, log_search) << absl::StrFormat( - "#1 %6.2fs %s", wall_timer->Get(), worker_name); - *stopped = true; - } - }); - } - } // for the dtor of the threadpool (join workers) before returning. - return best_response; - } - - // Optimization problem. - const auto get_objective_value = [&mutex, &best_response]() { - absl::MutexLock lock(&mutex); - return best_response.objective_value(); - }; - const auto get_objective_best_bound = [&mutex, &best_response]() { - absl::MutexLock lock(&mutex); - return best_response.best_objective_bound(); - }; - const auto set_objective_best_bound = [&mutex, maximize, &best_response]( - double objective_value, - double objective_best_bound) { - // Broadcast a best bound improving solution. - absl::MutexLock lock(&mutex); - CpSolverResponse lb_response; - lb_response.set_status(CpSolverStatus::UNKNOWN); - lb_response.set_objective_value(objective_value); - lb_response.set_best_objective_bound(objective_best_bound); - CHECK(!MergeOptimizationSolution(lb_response, maximize, &best_response)); - }; - const auto solution_synchronization = [&mutex, &best_response]() { - absl::MutexLock lock(&mutex); - return best_response; - }; - - std::unique_ptr shared_bounds_manager; - if (model->GetOrCreate()->share_level_zero_bounds()) { - shared_bounds_manager = absl::make_unique( - num_search_workers, model_proto); - } - { ThreadPool pool("Parallel_search", num_search_workers); pool.StartWorkers(); @@ -2350,37 +2236,25 @@ CpSolverResponse SolveCpModelParallel( const std::string worker_name = worker_names[worker_id]; const SatParameters local_params = worker_parameters[worker_id]; - const auto solution_observer = [maximize, worker_name, &mutex, - &best_response, &observer, + const auto solution_observer = [worker_name, &mutex, &observer, &first_solution_found_or_search_finished]( const CpSolverResponse& r) { + observer(r); + + // TODO(user): remove? absl::MutexLock lock(&mutex); - // Check is the new solution is actually improving upon the best - // solution found so far. - if (MergeOptimizationSolution(r, maximize, &best_response)) { - // Checks that r is not a pure best-bound improving solution. - CHECK_EQ(r.status(), CpSolverStatus::FEASIBLE); - - best_response.set_solution_info( - absl::StrCat(worker_name, " ", r.solution_info())); - observer(best_response); - - // We have potentially displayed the improving solution, and updated - // the best_response. We can awaken sleeping LNS threads. - if (!first_solution_found_or_search_finished.HasBeenNotified()) { - first_solution_found_or_search_finished.Notify(); - } + // We have found a solution, We can awaken sleeping LNS threads. + if (!first_solution_found_or_search_finished.HasBeenNotified()) { + first_solution_found_or_search_finished.Notify(); } }; - pool.Schedule([&model_proto, solution_observer, solution_synchronization, - get_objective_value, get_objective_best_bound, - set_objective_best_bound, stopped, local_params, worker_id, - &mutex, &best_response, num_search_workers, random_seed, - wall_timer, &first_solution_found_or_search_finished, - &shared_bounds_manager, maximize, worker_name, - log_search]() { + pool.Schedule([&model_proto, solution_observer, stopped, local_params, + worker_id, &mutex, num_search_workers, wall_timer, + &first_solution_found_or_search_finished, + &shared_response_manager, &shared_bounds_manager, + worker_name, log_search]() { Model local_model; local_model.Add(NewSatParameters(local_params)); local_model.GetOrCreate()->RegisterExternalBooleanAsLimit( @@ -2391,18 +2265,6 @@ CpSolverResponse SolveCpModelParallel( worker_info->worker_name = worker_name; worker_info->worker_id = worker_id; - SetSynchronizationFunction(std::move(solution_synchronization), - &local_model); - - ObjectiveSynchronizationHelper* helper = - local_model.GetOrCreate(); - helper->get_external_best_objective = std::move(get_objective_value); - helper->get_external_best_bound = std::move(get_objective_best_bound); - if (local_model.GetOrCreate() - ->share_objective_bounds()) { - helper->set_external_best_bound = std::move(set_objective_best_bound); - } - helper->parallel_mode = true; CpSolverResponse thread_response; if (local_params.use_lns()) { first_solution_found_or_search_finished.WaitForNotification(); @@ -2410,29 +2272,32 @@ CpSolverResponse SolveCpModelParallel( // seeds. thread_response = SolveCpModelWithLNS( model_proto, solution_observer, num_search_workers, worker_id, - shared_bounds_manager.get(), wall_timer, &local_model); + shared_response_manager, shared_bounds_manager.get(), wall_timer, + &local_model); } else { thread_response = SolveCpModelInternal( - model_proto, true, solution_observer, shared_bounds_manager.get(), - wall_timer, &local_model); + model_proto, /*is_real_solve=*/true, + /*log_sequential_search=*/false, solution_observer, + shared_response_manager, shared_bounds_manager.get(), wall_timer, + &local_model); } // Process final solution. Decide which worker has the 'best' // solution. Note that the solution observer may or may not have been // called. { - absl::MutexLock lock(&mutex); LOG_IF(INFO, log_search) << "*** worker '" << worker_name << "' terminates with status " << ProtoEnumToString(thread_response.status()) << " and an objective value of " << thread_response.objective_value(); - MergeOptimizationSolution(thread_response, maximize, &best_response); + shared_response_manager->MergeIntoBestResponse(thread_response); // TODO(user): For now we assume that each worker only terminate when // the time limit is reached or when the problem is solved, so we just // abort all other threads and return. + absl::MutexLock lock(&mutex); *stopped = true; if (!first_solution_found_or_search_finished.HasBeenNotified()) { first_solution_found_or_search_finished.Notify(); @@ -2441,7 +2306,7 @@ CpSolverResponse SolveCpModelParallel( }); } } // for the dtor of the threadpool (join workers) before returning. - return best_response; + return shared_response_manager->GetBestResponse(); } #endif // __PORTABLE_PLATFORM__ @@ -2566,42 +2431,56 @@ CpSolverResponse SolveCpModel(const CpModelProto& model_proto, Model* model) { }; } + SharedResponseManager shared_response_manager(new_model); + const auto& observers = model->GetOrCreate()->observers; int num_solutions = 0; std::function observer_function = [&model_proto, &observers, &num_solutions, &wall_timer, &user_timer, - &postprocess_solution, log_search](const CpSolverResponse& response) { + &shared_response_manager, &postprocess_solution, + log_search](const CpSolverResponse& response) { + // We don't do anything if the solution is not improving and we are + // solving an optimization problem. Otherwise, we do display and forward + // any solution we found. + if (!shared_response_manager.MergeIntoBestResponse(response) && + model_proto.has_objective()) { + return; + } + CpSolverResponse best_response = + shared_response_manager.GetBestResponse(); + + ++num_solutions; if (log_search) { if (model_proto.has_objective()) { const bool maximize = model_proto.objective().scaling_factor() < 0.0; - LogNewSolution(absl::StrCat(++num_solutions), wall_timer.Get(), - maximize ? response.objective_value() - : response.best_objective_bound(), - maximize ? response.best_objective_bound() - : response.objective_value(), - response.solution_info()); + LogNewSolution(absl::StrCat(num_solutions), wall_timer.Get(), + maximize ? best_response.objective_value() + : best_response.best_objective_bound(), + maximize ? best_response.best_objective_bound() + : best_response.objective_value(), + best_response.solution_info()); } else { - LogNewSatSolution(absl::StrCat(++num_solutions), wall_timer.Get(), - response.solution_info()); + LogNewSatSolution(absl::StrCat(num_solutions), wall_timer.Get(), + best_response.solution_info()); } } if (observers.empty()) return; - CpSolverResponse copy = response; - postprocess_solution(©); - if (!copy.solution().empty()) { + postprocess_solution(&best_response); + if (!best_response.solution().empty()) { if (DEBUG_MODE || FLAGS_cp_model_check_intermediate_solutions) { CHECK(SolutionIsFeasible( - model_proto, std::vector(copy.solution().begin(), - copy.solution().end()))); + model_proto, + std::vector(best_response.solution().begin(), + best_response.solution().end()))); } } - copy.set_wall_time(wall_timer.Get()); - copy.set_user_time(user_timer.Get()); + best_response.set_wall_time(wall_timer.Get()); + best_response.set_user_time(user_timer.Get()); for (const auto& observer : observers) { - observer(copy); + observer(best_response); } }; @@ -2613,22 +2492,28 @@ CpSolverResponse SolveCpModel(const CpModelProto& model_proto, Model* model) { #else // __PORTABLE_PLATFORM__ if (params.num_search_workers() > 1) { response = - SolveCpModelParallel(new_model, observer_function, &wall_timer, model); + SolveCpModelParallel(new_model, observer_function, + &shared_response_manager, &wall_timer, model); #endif // __PORTABLE_PLATFORM__ } else if (params.use_lns() && new_model.has_objective() && !params.enumerate_all_solutions()) { // TODO(user,user): Provide a better diversification for different // seeds. const int random_seed = model->GetOrCreate()->random_seed(); - response = SolveCpModelWithLNS(new_model, observer_function, 1, random_seed, - /*shared_bounds_manager=*/nullptr, - &wall_timer, model); - } else { // Normal sequential run. + response = SolveCpModelWithLNS( + new_model, observer_function, 1, random_seed, &shared_response_manager, + /*shared_bounds_manager=*/nullptr, &wall_timer, model); + } else { + // Normal sequential run. response = SolveCpModelInternal( - new_model, /*is_real_solve=*/true, observer_function, + new_model, /*is_real_solve=*/true, /*log_sequential_search=*/true, + observer_function, &shared_response_manager, /*shared_bounds_manager=*/nullptr, &wall_timer, model); } + shared_response_manager.MergeIntoBestResponse(response); + response = shared_response_manager.GetBestResponse(); + postprocess_solution(&response); if (!response.solution().empty()) { CHECK(SolutionIsFeasible(model_proto, diff --git a/ortools/sat/integer_search.cc b/ortools/sat/integer_search.cc index 36e2857144..565d3fef3b 100644 --- a/ortools/sat/integer_search.cc +++ b/ortools/sat/integer_search.cc @@ -525,6 +525,8 @@ SatSolver::Status SolveIntegerProblemWithLazyEncoding( {SequentialSearch({next_decision, SatSolverHeuristic(model)})}, {SatSolverRestartPolicy(model)}, model); } else { + // TODO(user): We might want to restart if external info is available. + // Code a custom restart for this? auto no_restart = []() { return false; }; return SolveProblemWithPortfolioSearch( {SequentialSearch({next_decision, SatSolverHeuristic(model)})}, @@ -627,12 +629,6 @@ SatSolver::Status SolveProblemWithPortfolioSearch( SatSolver* const solver = model->GetOrCreate(); const ObjectiveSynchronizationHelper* helper = model->Get(); - const bool synchronize_objective = - solver->AssumptionLevel() == 0 && helper != nullptr && - helper->get_external_best_objective != nullptr && - helper->objective_var != kNoIntegerVariable && - model->GetOrCreate()->share_objective_bounds() && - model->GetOrCreate()->parallel_mode; // Note that it is important to do the level-zero propagation if it wasn't // already done because EnqueueDecisionAndBackjumpOnConflict() assumes that @@ -662,35 +658,6 @@ SatSolver::Status SolveProblemWithPortfolioSearch( policy_index = (policy_index + 1) % num_policies; } - // Check external objective, and restart if a better one is supplied. - // This code has to be run before the level_zero_propagate_callbacks are - // triggered, as one of them will actually import the new objective bounds. - // TODO(user): Maybe do not check this at each decision. - // TODO(user): Move restart code to the restart part? - if (synchronize_objective) { - const double external_bound = helper->get_external_best_objective(); - CHECK(helper->get_external_best_bound != nullptr); - const double external_best_bound = helper->get_external_best_bound(); - IntegerValue current_objective_upper_bound( - integer_trail->UpperBound(helper->objective_var)); - IntegerValue current_objective_lower_bound( - integer_trail->LowerBound(helper->objective_var)); - IntegerValue new_objective_upper_bound( - std::isfinite(external_bound) - ? helper->UnscaledObjective(external_bound) - 1 - : current_objective_upper_bound.value()); - IntegerValue new_objective_lower_bound( - std::isfinite(external_best_bound) - ? helper->UnscaledObjective(external_best_bound) - : current_objective_lower_bound.value()); - if (new_objective_upper_bound < current_objective_upper_bound || - new_objective_lower_bound > current_objective_lower_bound) { - if (!solver->RestoreSolverToAssumptionLevel()) { - return solver->UnsatStatus(); - } - } - } - if (solver->CurrentDecisionLevel() == 0) { auto* level_zero_callbacks = model->GetOrCreate(); diff --git a/ortools/sat/integer_search.h b/ortools/sat/integer_search.h index a619193a4d..ad4c0a6ae5 100644 --- a/ortools/sat/integer_search.h +++ b/ortools/sat/integer_search.h @@ -188,14 +188,12 @@ SatSolver::Status SolveIntegerProblemWithLazyEncoding(Model* model); // Store relationship between the CpSolverResponse objective and the internal // IntegerVariable the solver tries to minimize. +// +// TODO(user): This belongs to the CpModelMapping class, move there. struct ObjectiveSynchronizationHelper { double scaling_factor = 1.0; double offset = 0.0; IntegerVariable objective_var = kNoIntegerVariable; - std::function get_external_best_objective = nullptr; - std::function get_external_best_bound = nullptr; - std::function set_external_best_bound = nullptr; - bool parallel_mode = false; int64 UnscaledObjective(double value) const { return static_cast(std::round(value / scaling_factor - offset)); diff --git a/ortools/sat/linear_programming_constraint.cc b/ortools/sat/linear_programming_constraint.cc index c2f8966c77..671c9b6729 100644 --- a/ortools/sat/linear_programming_constraint.cc +++ b/ortools/sat/linear_programming_constraint.cc @@ -331,8 +331,7 @@ bool LinearProgrammingConstraint::SolveLp() { const auto status = simplex_.Solve(lp_data_, time_limit_); if (!status.ok()) { - LOG(WARNING) << "The LP solver encountered an error: " - << status.error_message(); + VLOG(1) << "The LP solver encountered an error: " << status.error_message(); simplex_.ClearStateForNextSolve(); return false; } diff --git a/ortools/sat/synchronization.cc b/ortools/sat/synchronization.cc index 977f2bcbfd..366ffc8a05 100644 --- a/ortools/sat/synchronization.cc +++ b/ortools/sat/synchronization.cc @@ -14,9 +14,9 @@ #include "ortools/sat/synchronization.h" #include "absl/container/flat_hash_set.h" -#include "absl/strings/str_join.h" #include "ortools/sat/cp_model.pb.h" #include "ortools/sat/cp_model_loader.h" +#include "ortools/sat/cp_model_search.h" #include "ortools/sat/cp_model_utils.h" #include "ortools/sat/integer.h" #include "ortools/sat/integer_search.h" @@ -26,7 +26,44 @@ namespace operations_research { namespace sat { -SharedBoundsManager::SharedBoundsManager(int num_workers, const CpModelProto& model_proto) +SharedResponseManager::SharedResponseManager(const CpModelProto& proto) { + best_response_.set_status(CpSolverStatus::UNKNOWN); + if (proto.has_objective()) { + const double kInfinity = std::numeric_limits::infinity(); + is_maximize_ = proto.objective().scaling_factor() < 0.0; + if (is_maximize_) { + best_response_.set_objective_value(-kInfinity); + best_response_.set_best_objective_bound(kInfinity); + } else { + best_response_.set_objective_value(kInfinity); + best_response_.set_best_objective_bound(-kInfinity); + } + } +} + +double SharedResponseManager::GetObjectiveValue() { + absl::MutexLock mutex_lock(&mutex_); + return best_response_.objective_value(); +} + +double SharedResponseManager::GetObjectiveBestBound() { + absl::MutexLock mutex_lock(&mutex_); + return best_response_.best_objective_bound(); +} + +CpSolverResponse SharedResponseManager::GetBestResponse() { + absl::MutexLock mutex_lock(&mutex_); + return best_response_; +} + +bool SharedResponseManager::MergeIntoBestResponse( + const CpSolverResponse& response) { + absl::MutexLock mutex_lock(&mutex_); + return MergeOptimizationSolution(response, is_maximize_, &best_response_); +} + +SharedBoundsManager::SharedBoundsManager(int num_workers, + const CpModelProto& model_proto) : num_workers_(num_workers), num_variables_(model_proto.variables_size()), changed_variables_per_workers_(num_workers), @@ -44,8 +81,7 @@ SharedBoundsManager::SharedBoundsManager(int num_workers, const CpModelProto& mo void SharedBoundsManager::ReportPotentialNewBounds( const CpModelProto& model_proto, int worker_id, - const std::string& worker_name, - const std::vector& variables, + const std::string& worker_name, const std::vector& variables, const std::vector& new_lower_bounds, const std::vector& new_upper_bounds) { CHECK_EQ(variables.size(), new_lower_bounds.size()); @@ -70,7 +106,7 @@ void SharedBoundsManager::ReportPotentialNewBounds( if (changed_ub) { upper_bounds_[var] = new_ub; } - + for (int j = 0; j < num_workers_; ++j) { if (worker_id == j) continue; changed_variables_per_workers_[j].Set(var); @@ -78,9 +114,8 @@ void SharedBoundsManager::ReportPotentialNewBounds( if (VLOG_IS_ON(2)) { const IntegerVariableProto& var_proto = model_proto.variables(var); const std::string& var_name = - var_proto.name().empty() - ? absl::StrCat("anonymous_var(", var, ")") - : absl::StrCat(var_proto.name(), "(", var, ")"); + var_proto.name().empty() ? absl::StrCat("anonymous_var(", var, ")") + : var_proto.name(); LOG(INFO) << " '" << worker_name << "' exports new bounds for " << var_name << ": from [" << old_lb << ", " << old_ub << "] to [" << new_lb << ", " << new_ub << "]"; @@ -175,9 +210,9 @@ void RegisterVariableBoundsLevelZeroImport( &new_upper_bounds); bool new_bounds_have_been_imported = false; for (int i = 0; i < model_variables.size(); ++i) { + const int model_var = model_variables[i]; // This can happen if a boolean variables is forced to have an // integer view in one thread, and not in another thread. - const int model_var = model_variables[i]; if (!mapping->IsInteger(model_var)) continue; const IntegerVariable var = mapping->Integer(model_var); const IntegerValue new_lb(new_lower_bounds[i]); @@ -195,7 +230,7 @@ void RegisterVariableBoundsLevelZeroImport( const std::string& var_name = var_proto.name().empty() ? absl::StrCat("anonymous_var(", model_var, ")") - : absl::StrCat(var_proto.name(), "(", model_var, ")"); + : var_proto.name(); LOG(INFO) << " '" << worker_info->worker_name << "' imports new bounds for " << var_name << ": from [" << old_lb << ", " << old_ub << "] to [" << new_lb << ", " @@ -223,50 +258,46 @@ void RegisterVariableBoundsLevelZeroImport( import_level_zero_bounds); } -void RegisterObjectiveBestBoundExport(const CpModelProto& model_proto, - bool log_progress, - IntegerVariable objective_var, - WallTimer* wall_timer, Model* model) { +void RegisterObjectiveBestBoundExport( + const CpModelProto& model_proto, bool log_progress, + IntegerVariable objective_var, WallTimer* wall_timer, + SharedResponseManager* shared_response_manager, Model* model) { + auto* integer_trail = model->Get(); + auto* worker_info = model->GetOrCreate(); + const CpObjectiveProto& obj = model_proto.objective(); const auto broadcast_objective_lower_bound = - [&model_proto, objective_var, wall_timer, model, - log_progress](const std::vector& unused) { - auto* integer_trail = model->Get(); - const CpObjectiveProto& obj = model_proto.objective(); + [obj, objective_var, wall_timer, integer_trail, worker_info, log_progress, + shared_response_manager](const std::vector& unused) { const double new_best_bound = ScaleObjectiveValue( obj, integer_trail->LevelZeroLowerBound(objective_var).value()); - const double new_objective_value = ScaleObjectiveValue( - obj, integer_trail->LevelZeroUpperBound(objective_var).value()); - - const ObjectiveSynchronizationHelper* const helper = - model->GetOrCreate(); - const double current_best_bound = helper->get_external_best_bound(); + const double current_best_bound = + shared_response_manager->GetObjectiveBestBound(); const double current_objective_value = - helper->get_external_best_objective(); + shared_response_manager->GetObjectiveValue(); - // TODO(lperron): Unit test this lambda. - if ((helper->scaling_factor >= 0 && // Unset -> = 0.0 -> minimize. - new_best_bound > current_best_bound) || - (helper->scaling_factor < 0 && - new_best_bound < current_best_bound)) { + // TODO(user): we currently display "inf" for the objective if the first + // update is a bound update. This will go away when I refactor the code + // to not depend on the objective scaling/offset and stay with int64 + // internally. + CpSolverResponse response; + response.set_status(CpSolverStatus::UNKNOWN); + const double kInfinity = std::numeric_limits::infinity(); + if (shared_response_manager->IsMaximize() && + new_best_bound < current_best_bound) { + response.set_objective_value(-kInfinity); + response.set_best_objective_bound(new_best_bound); + shared_response_manager->MergeIntoBestResponse(response); if (log_progress) { - const WorkerInfo* const worker_info = - model->GetOrCreate(); - const double reported_objective_value = - std::isfinite(current_objective_value) ? current_objective_value - : new_objective_value; - if (new_best_bound > current_best_bound) { // minimization. - LogNewSolution("ObjLb", wall_timer->Get(), new_best_bound, - reported_objective_value, - worker_info->worker_name); - } else { - LogNewSolution("ObjUb", wall_timer->Get(), - reported_objective_value, new_best_bound, - worker_info->worker_name); - } + LogNewSolution("ObjUb", wall_timer->Get(), current_objective_value, + new_best_bound, worker_info->worker_name); } - if (helper->set_external_best_bound) { - helper->set_external_best_bound(current_objective_value, - new_best_bound); + } else if (new_best_bound > current_best_bound) { + response.set_objective_value(kInfinity); + response.set_best_objective_bound(new_best_bound); + shared_response_manager->MergeIntoBestResponse(response); + if (log_progress) { + LogNewSolution("ObjLb", wall_timer->Get(), new_best_bound, + current_objective_value, worker_info->worker_name); } } }; @@ -275,19 +306,19 @@ void RegisterObjectiveBestBoundExport(const CpModelProto& model_proto, broadcast_objective_lower_bound); } -void RegisterObjectiveBoundsImport(Model* model) { - SatSolver* const solver = model->GetOrCreate(); - const WorkerInfo* const worker_info = model->GetOrCreate(); - const ObjectiveSynchronizationHelper* const helper = - model->GetOrCreate(); - IntegerTrail* const integer_trail = model->GetOrCreate(); - const auto import_objective_bounds = [model, solver, worker_info, - integer_trail, helper]() { +void RegisterObjectiveBoundsImport( + SharedResponseManager* shared_response_manager, Model* model) { + auto* solver = model->GetOrCreate(); + auto* integer_trail = model->GetOrCreate(); + auto* worker_info = model->GetOrCreate(); + auto* helper = model->GetOrCreate(); + const auto import_objective_bounds = [solver, integer_trail, worker_info, + helper, shared_response_manager]() { if (solver->AssumptionLevel() != 0) return true; + const double external_bound = shared_response_manager->GetObjectiveValue(); + const double external_best_bound = + shared_response_manager->GetObjectiveBestBound(); - CHECK(helper->get_external_best_bound != nullptr); - const double external_bound = helper->get_external_best_objective(); - const double external_best_bound = helper->get_external_best_bound(); const IntegerValue current_objective_upper_bound( integer_trail->UpperBound(helper->objective_var)); const IntegerValue current_objective_lower_bound( diff --git a/ortools/sat/synchronization.h b/ortools/sat/synchronization.h index d83de7dd9f..1e009d5650 100644 --- a/ortools/sat/synchronization.h +++ b/ortools/sat/synchronization.h @@ -29,6 +29,35 @@ namespace operations_research { namespace sat { +// Manages the gobal best solution kept by the solver. +// +// TODO(user): Add two int64 fieds inner_objective_lb/ub and use them instead of +// the one in best_response. This way all the code can assume minimization and +// not care about the offset and scaling. These should only be used for final +// reporting and display! Moreover, that allow to share the currrent objective +// upper bound, that is only valid by assuming we want to find a better +// objective. +class SharedResponseManager { + public: + explicit SharedResponseManager(const CpModelProto& proto); + + double GetObjectiveValue(); + double GetObjectiveBestBound(); + CpSolverResponse GetBestResponse(); + + // TODO(user): Simple objective update probably do not need to go through + // this function, same for solution update. + bool MergeIntoBestResponse(const CpSolverResponse& response); + + // This never changes after construction, so it is thread-safe to access it. + bool IsMaximize() const { return is_maximize_; } + + private: + bool is_maximize_ = false; // const. + CpSolverResponse best_response_; + absl::Mutex mutex_; +}; + // This class manages a pool of lower and upper bounds on a set of variables in // a parallel context. class SharedBoundsManager { @@ -78,16 +107,17 @@ void RegisterVariableBoundsLevelZeroExport( // // Currently, standard search works fine with it. // LNS search and Core based search do not support it -void RegisterObjectiveBoundsImport(Model* model); +void RegisterObjectiveBoundsImport( + SharedResponseManager* shared_response_manager, Model* model); // Registers a callback that will report improving objective best bound. // // TODO(user): A solver can also improve the objective upper bound without // finding a solution and we should maybe share this as well. -void RegisterObjectiveBestBoundExport(const CpModelProto& model_proto, - bool log_progress, - IntegerVariable objective_var, - WallTimer* wall_timer, Model* model); +void RegisterObjectiveBestBoundExport( + const CpModelProto& model_proto, bool log_progress, + IntegerVariable objective_var, WallTimer* wall_timer, + SharedResponseManager* shared_response_manager, Model* model); // Stores information on the worker in the parallel context. struct WorkerInfo {