OR-Tools  9.6
bop_portfolio.cc
Go to the documentation of this file.
1 // Copyright 2010-2022 Google LLC
2 // Licensed under the Apache License, Version 2.0 (the "License");
3 // you may not use this file except in compliance with the License.
4 // You may obtain a copy of the License at
5 //
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13 
15 
16 #include <algorithm>
17 #include <cstdint>
18 #include <limits>
19 #include <memory>
20 #include <string>
21 #include <utility>
22 #include <vector>
23 
24 #include "absl/memory/memory.h"
25 #include "absl/strings/str_format.h"
26 #include "ortools/base/stl_util.h"
28 #include "ortools/bop/bop_fs.h"
29 #include "ortools/bop/bop_lns.h"
30 #include "ortools/bop/bop_ls.h"
31 #include "ortools/bop/bop_util.h"
34 #include "ortools/sat/boolean_problem.pb.h"
35 #include "ortools/sat/symmetry.h"
36 
37 namespace operations_research {
38 namespace bop {
39 
40 using ::operations_research::sat::LinearBooleanProblem;
41 using ::operations_research::sat::LinearObjective;
42 
43 namespace {
44 void BuildObjectiveTerms(const LinearBooleanProblem& problem,
45  BopConstraintTerms* objective_terms) {
46  CHECK(objective_terms != nullptr);
47 
48  if (!objective_terms->empty()) return;
49 
50  const LinearObjective& objective = problem.objective();
51  const size_t num_objective_terms = objective.literals_size();
52  CHECK_EQ(num_objective_terms, objective.coefficients_size());
53  for (int i = 0; i < num_objective_terms; ++i) {
54  CHECK_GT(objective.literals(i), 0);
55  CHECK_NE(objective.coefficients(i), 0);
56 
57  const VariableIndex var_id(objective.literals(i) - 1);
58  const int64_t weight = objective.coefficients(i);
59  objective_terms->push_back(BopConstraintTerm(var_id, weight));
60  }
61 }
62 } // anonymous namespace
63 
64 //------------------------------------------------------------------------------
65 // PortfolioOptimizer
66 //------------------------------------------------------------------------------
68  const ProblemState& problem_state, const BopParameters& parameters,
69  const BopSolverOptimizerSet& optimizer_set, const std::string& name)
71  random_(parameters.random_seed()),
72  state_update_stamp_(ProblemState::kInitialStampValue),
73  objective_terms_(),
74  selector_(),
75  optimizers_(),
76  sat_propagator_(),
77  parameters_(parameters),
78  lower_bound_(-glop::kInfinity),
79  upper_bound_(glop::kInfinity),
80  number_of_consecutive_failing_optimizers_(0) {
81  CreateOptimizers(problem_state.original_problem(), parameters, optimizer_set);
82 }
83 
85  if (parameters_.log_search_progress() || VLOG_IS_ON(1)) {
86  std::string stats_string;
87  for (OptimizerIndex i(0); i < optimizers_.size(); ++i) {
88  if (selector_->NumCallsForOptimizer(i) > 0) {
89  stats_string += selector_->PrintStats(i);
90  }
91  }
92  if (!stats_string.empty()) {
93  LOG(INFO) << "Stats. #new_solutions/#calls by optimizer:\n" +
94  stats_string;
95  }
96  }
97 
98  // Note that unique pointers are not used due to unsupported emplace_back
99  // in ITIVectors.
100  gtl::STLDeleteElements(&optimizers_);
101 }
102 
103 BopOptimizerBase::Status PortfolioOptimizer::SynchronizeIfNeeded(
104  const ProblemState& problem_state) {
105  if (state_update_stamp_ == problem_state.update_stamp()) {
107  }
108  state_update_stamp_ = problem_state.update_stamp();
109 
110  // Load any new information into the sat_propagator_.
111  const bool first_time = (sat_propagator_.NumVariables() == 0);
113  LoadStateProblemToSatSolver(problem_state, &sat_propagator_);
115  if (first_time) {
116  // We configure the sat_propagator_ to use the objective as an assignment
117  // preference
119  &sat_propagator_);
120  }
121 
122  lower_bound_ = problem_state.GetScaledLowerBound();
123  upper_bound_ = problem_state.solution().IsFeasible()
124  ? problem_state.solution().GetScaledCost()
125  : glop::kInfinity;
127 }
128 
130  const BopParameters& parameters, const ProblemState& problem_state,
131  LearnedInfo* learned_info, TimeLimit* time_limit) {
132  CHECK(learned_info != nullptr);
133  CHECK(time_limit != nullptr);
134  learned_info->Clear();
135 
136  const BopOptimizerBase::Status sync_status =
137  SynchronizeIfNeeded(problem_state);
138  if (sync_status != BopOptimizerBase::CONTINUE) {
139  return sync_status;
140  }
141 
142  for (OptimizerIndex i(0); i < optimizers_.size(); ++i) {
143  selector_->SetOptimizerRunnability(
144  i, optimizers_[i]->ShouldBeRun(problem_state));
145  }
146 
147  const int64_t init_cost = problem_state.solution().IsFeasible()
148  ? problem_state.solution().GetCost()
150  const double init_deterministic_time =
151  time_limit->GetElapsedDeterministicTime();
152 
153  const OptimizerIndex selected_optimizer_id = selector_->SelectOptimizer();
154  if (selected_optimizer_id == kInvalidOptimizerIndex) {
155  LOG(INFO) << "All the optimizers are done.";
157  }
158  BopOptimizerBase* const selected_optimizer =
159  optimizers_[selected_optimizer_id];
160  if (parameters.log_search_progress() || VLOG_IS_ON(1)) {
161  LOG(INFO) << " " << lower_bound_ << " .. " << upper_bound_ << " "
162  << name() << " - " << selected_optimizer->name()
163  << ". Time limit: " << time_limit->GetTimeLeft() << " -- "
164  << time_limit->GetDeterministicTimeLeft();
165  }
166  const BopOptimizerBase::Status optimization_status =
167  selected_optimizer->Optimize(parameters, problem_state, learned_info,
168  time_limit);
169 
170  // ABORT means that this optimizer can't be run until we found a new solution.
171  if (optimization_status == BopOptimizerBase::ABORT) {
172  selector_->TemporarilyMarkOptimizerAsUnselectable(selected_optimizer_id);
173  }
174 
175  // The gain is defined as 1 for the first solution.
176  // TODO(user): Is 1 the right value? It might be better to use a percentage
177  // of the gap, or use the same gain as for the second solution.
178  const int64_t gain =
179  optimization_status == BopOptimizerBase::SOLUTION_FOUND
180  ? (init_cost == std::numeric_limits<int64_t>::max()
181  ? 1
182  : init_cost - learned_info->solution.GetCost())
183  : 0;
184  const double spent_deterministic_time =
185  time_limit->GetElapsedDeterministicTime() - init_deterministic_time;
186  selector_->UpdateScore(gain, spent_deterministic_time);
187 
188  if (optimization_status == BopOptimizerBase::INFEASIBLE ||
189  optimization_status == BopOptimizerBase::OPTIMAL_SOLUTION_FOUND) {
190  return optimization_status;
191  }
192 
193  // Stop the portfolio optimizer after too many unsuccessful calls.
194  if (parameters.has_max_number_of_consecutive_failing_optimizer_calls() &&
195  problem_state.solution().IsFeasible()) {
196  number_of_consecutive_failing_optimizers_ =
197  optimization_status == BopOptimizerBase::SOLUTION_FOUND
198  ? 0
199  : number_of_consecutive_failing_optimizers_ + 1;
200  if (number_of_consecutive_failing_optimizers_ >
201  parameters.max_number_of_consecutive_failing_optimizer_calls()) {
203  }
204  }
205 
206  // TODO(user): don't penalize the SatCoreBasedOptimizer or the
207  // LinearRelaxation when they improve the lower bound.
208  // TODO(user): Do we want to re-order the optimizers in the selector when
209  // the status is BopOptimizerBase::INFORMATION_FOUND?
211 }
212 
213 void PortfolioOptimizer::AddOptimizer(
214  const LinearBooleanProblem& problem, const BopParameters& parameters,
215  const BopOptimizerMethod& optimizer_method) {
216  switch (optimizer_method.type()) {
217  case BopOptimizerMethod::SAT_CORE_BASED:
218  optimizers_.push_back(new SatCoreBasedOptimizer("SatCoreBasedOptimizer"));
219  break;
220  case BopOptimizerMethod::SAT_LINEAR_SEARCH:
221  optimizers_.push_back(new GuidedSatFirstSolutionGenerator(
223  break;
224  case BopOptimizerMethod::LINEAR_RELAXATION:
225  optimizers_.push_back(
226  new LinearRelaxation(parameters, "LinearRelaxation"));
227  break;
228  case BopOptimizerMethod::LOCAL_SEARCH: {
229  for (int i = 1; i <= parameters.max_num_decisions_in_ls(); ++i) {
230  optimizers_.push_back(new LocalSearchOptimizer(
231  absl::StrFormat("LS_%d", i), i, random_, &sat_propagator_));
232  }
233  } break;
234  case BopOptimizerMethod::RANDOM_FIRST_SOLUTION:
235  optimizers_.push_back(new BopRandomFirstSolutionGenerator(
236  "SATRandomFirstSolution", parameters, &sat_propagator_, random_));
237  break;
238  case BopOptimizerMethod::RANDOM_VARIABLE_LNS:
239  BuildObjectiveTerms(problem, &objective_terms_);
240  optimizers_.push_back(new BopAdaptiveLNSOptimizer(
241  "RandomVariableLns",
242  /*use_lp_to_guide_sat=*/false,
243  new ObjectiveBasedNeighborhood(&objective_terms_, random_),
244  &sat_propagator_));
245  break;
246  case BopOptimizerMethod::RANDOM_VARIABLE_LNS_GUIDED_BY_LP:
247  BuildObjectiveTerms(problem, &objective_terms_);
248  optimizers_.push_back(new BopAdaptiveLNSOptimizer(
249  "RandomVariableLnsWithLp",
250  /*use_lp_to_guide_sat=*/true,
251  new ObjectiveBasedNeighborhood(&objective_terms_, random_),
252  &sat_propagator_));
253  break;
254  case BopOptimizerMethod::RANDOM_CONSTRAINT_LNS:
255  BuildObjectiveTerms(problem, &objective_terms_);
256  optimizers_.push_back(new BopAdaptiveLNSOptimizer(
257  "RandomConstraintLns",
258  /*use_lp_to_guide_sat=*/false,
259  new ConstraintBasedNeighborhood(&objective_terms_, random_),
260  &sat_propagator_));
261  break;
262  case BopOptimizerMethod::RANDOM_CONSTRAINT_LNS_GUIDED_BY_LP:
263  BuildObjectiveTerms(problem, &objective_terms_);
264  optimizers_.push_back(new BopAdaptiveLNSOptimizer(
265  "RandomConstraintLnsWithLp",
266  /*use_lp_to_guide_sat=*/true,
267  new ConstraintBasedNeighborhood(&objective_terms_, random_),
268  &sat_propagator_));
269  break;
270  case BopOptimizerMethod::RELATION_GRAPH_LNS:
271  BuildObjectiveTerms(problem, &objective_terms_);
272  optimizers_.push_back(new BopAdaptiveLNSOptimizer(
273  "RelationGraphLns",
274  /*use_lp_to_guide_sat=*/false,
275  new RelationGraphBasedNeighborhood(problem, random_),
276  &sat_propagator_));
277  break;
278  case BopOptimizerMethod::RELATION_GRAPH_LNS_GUIDED_BY_LP:
279  BuildObjectiveTerms(problem, &objective_terms_);
280  optimizers_.push_back(new BopAdaptiveLNSOptimizer(
281  "RelationGraphLnsWithLp",
282  /*use_lp_to_guide_sat=*/true,
283  new RelationGraphBasedNeighborhood(problem, random_),
284  &sat_propagator_));
285  break;
286  case BopOptimizerMethod::COMPLETE_LNS:
287  BuildObjectiveTerms(problem, &objective_terms_);
288  optimizers_.push_back(
289  new BopCompleteLNSOptimizer("LNS", objective_terms_));
290  break;
291  case BopOptimizerMethod::USER_GUIDED_FIRST_SOLUTION:
292  optimizers_.push_back(new GuidedSatFirstSolutionGenerator(
293  "SATUserGuidedFirstSolution",
295  break;
296  case BopOptimizerMethod::LP_FIRST_SOLUTION:
297  optimizers_.push_back(new GuidedSatFirstSolutionGenerator(
298  "SATLPFirstSolution",
300  break;
301  case BopOptimizerMethod::OBJECTIVE_FIRST_SOLUTION:
302  optimizers_.push_back(new GuidedSatFirstSolutionGenerator(
303  "SATObjectiveFirstSolution",
305  break;
306  default:
307  LOG(FATAL) << "Unknown optimizer type.";
308  }
309 }
310 
311 void PortfolioOptimizer::CreateOptimizers(
312  const LinearBooleanProblem& problem, const BopParameters& parameters,
313  const BopSolverOptimizerSet& optimizer_set) {
314  if (parameters.use_symmetry()) {
315  VLOG(1) << "Finding symmetries of the problem.";
316  std::vector<std::unique_ptr<SparsePermutation>> generators;
317  sat::FindLinearBooleanProblemSymmetries(problem, &generators);
318  std::unique_ptr<sat::SymmetryPropagator> propagator(
319  new sat::SymmetryPropagator);
320  for (int i = 0; i < generators.size(); ++i) {
321  propagator->AddSymmetry(std::move(generators[i]));
322  }
323  sat_propagator_.AddPropagator(propagator.get());
324  sat_propagator_.TakePropagatorOwnership(std::move(propagator));
325  }
326 
327  const int max_num_optimizers =
328  optimizer_set.methods_size() + parameters.max_num_decisions_in_ls() - 1;
329  optimizers_.reserve(max_num_optimizers);
330  for (const BopOptimizerMethod& optimizer_method : optimizer_set.methods()) {
331  const OptimizerIndex old_size(optimizers_.size());
332  AddOptimizer(problem, parameters, optimizer_method);
333  }
334 
335  selector_ = std::make_unique<OptimizerSelector>(optimizers_);
336 }
337 
338 //------------------------------------------------------------------------------
339 // OptimizerSelector
340 //------------------------------------------------------------------------------
343  : run_infos_(), selected_index_(optimizers.size()) {
344  for (OptimizerIndex i(0); i < optimizers.size(); ++i) {
345  info_positions_.push_back(run_infos_.size());
346  run_infos_.push_back(RunInfo(i, optimizers[i]->name()));
347  }
348 }
349 
351  CHECK_GE(selected_index_, 0);
352 
353  do {
354  ++selected_index_;
355  } while (selected_index_ < run_infos_.size() &&
356  !run_infos_[selected_index_].RunnableAndSelectable());
357 
358  if (selected_index_ >= run_infos_.size()) {
359  // Select the first possible optimizer.
360  selected_index_ = -1;
361  for (int i = 0; i < run_infos_.size(); ++i) {
362  if (run_infos_[i].RunnableAndSelectable()) {
363  selected_index_ = i;
364  break;
365  }
366  }
367  if (selected_index_ == -1) return kInvalidOptimizerIndex;
368  } else {
369  // Select the next possible optimizer. If none, select the first one.
370  // Check that the time is smaller than all previous optimizers which are
371  // runnable.
372  bool too_much_time_spent = false;
373  const double time_spent =
374  run_infos_[selected_index_].time_spent_since_last_solution;
375  for (int i = 0; i < selected_index_; ++i) {
376  const RunInfo& info = run_infos_[i];
377  if (info.RunnableAndSelectable() &&
378  info.time_spent_since_last_solution < time_spent) {
379  too_much_time_spent = true;
380  break;
381  }
382  }
383  if (too_much_time_spent) {
384  // TODO(user): Remove this recursive call, even if in practice it's
385  // safe because the max depth is the number of optimizers.
386  return SelectOptimizer();
387  }
388  }
389 
390  // Select the optimizer.
391  ++run_infos_[selected_index_].num_calls;
392  return run_infos_[selected_index_].optimizer_index;
393 }
394 
395 void OptimizerSelector::UpdateScore(int64_t gain, double time_spent) {
396  const bool new_solution_found = gain != 0;
397  if (new_solution_found) NewSolutionFound(gain);
398  UpdateDeterministicTime(time_spent);
399 
400  const double new_score = time_spent == 0.0 ? 0.0 : gain / time_spent;
401  const double kErosion = 0.2;
402  const double kMinScore = 1E-6;
403 
404  RunInfo& info = run_infos_[selected_index_];
405  const double old_score = info.score;
406  info.score =
407  std::max(kMinScore, old_score * (1 - kErosion) + kErosion * new_score);
408 
409  if (new_solution_found) { // Solution found
410  UpdateOrder();
411  selected_index_ = run_infos_.size();
412  }
413 }
414 
416  OptimizerIndex optimizer_index) {
417  run_infos_[info_positions_[optimizer_index]].selectable = false;
418 }
419 
420 void OptimizerSelector::SetOptimizerRunnability(OptimizerIndex optimizer_index,
421  bool runnable) {
422  run_infos_[info_positions_[optimizer_index]].runnable = runnable;
423 }
424 
426  OptimizerIndex optimizer_index) const {
427  const RunInfo& info = run_infos_[info_positions_[optimizer_index]];
428  return absl::StrFormat(
429  " %40s : %3d/%-3d (%6.2f%%) Total gain: %6d Total Dtime: %0.3f "
430  "score: %f\n",
431  info.name, info.num_successes, info.num_calls,
432  100.0 * info.num_successes / info.num_calls, info.total_gain,
433  info.time_spent, info.score);
434 }
435 
437  OptimizerIndex optimizer_index) const {
438  const RunInfo& info = run_infos_[info_positions_[optimizer_index]];
439  return info.num_calls;
440 }
441 
443  std::string str;
444  for (int i = 0; i < run_infos_.size(); ++i) {
445  const RunInfo& info = run_infos_[i];
446  LOG(INFO) << " " << info.name << " " << info.total_gain
447  << " / " << info.time_spent << " = " << info.score << " "
448  << info.selectable << " " << info.time_spent_since_last_solution;
449  }
450 }
451 
452 void OptimizerSelector::NewSolutionFound(int64_t gain) {
453  run_infos_[selected_index_].num_successes++;
454  run_infos_[selected_index_].total_gain += gain;
455 
456  for (int i = 0; i < run_infos_.size(); ++i) {
457  run_infos_[i].time_spent_since_last_solution = 0;
458  run_infos_[i].selectable = true;
459  }
460 }
461 
462 void OptimizerSelector::UpdateDeterministicTime(double time_spent) {
463  run_infos_[selected_index_].time_spent += time_spent;
464  run_infos_[selected_index_].time_spent_since_last_solution += time_spent;
465 }
466 
467 void OptimizerSelector::UpdateOrder() {
468  // Re-sort optimizers.
469  std::stable_sort(run_infos_.begin(), run_infos_.end(),
470  [](const RunInfo& a, const RunInfo& b) -> bool {
471  if (a.total_gain == 0 && b.total_gain == 0)
472  return a.time_spent < b.time_spent;
473  return a.score > b.score;
474  });
475 
476  // Update the positions.
477  for (int i = 0; i < run_infos_.size(); ++i) {
478  info_positions_[run_infos_[i].optimizer_index] = i;
479  }
480 }
481 
482 } // namespace bop
483 } // namespace operations_research
int64_t max
Definition: alldiff_cst.cc:140
size_type size() const
void push_back(const value_type &x)
A simple class to enforce both an elapsed time limit and a deterministic time limit in the same threa...
Definition: time_limit.h:106
virtual Status Optimize(const BopParameters &parameters, const ProblemState &problem_state, LearnedInfo *learned_info, TimeLimit *time_limit)=0
const std::string & name() const
Definition: bop_base.h:52
void UpdateScore(int64_t gain, double time_spent)
std::string PrintStats(OptimizerIndex optimizer_index) const
int NumCallsForOptimizer(OptimizerIndex optimizer_index) const
OptimizerSelector(const absl::StrongVector< OptimizerIndex, BopOptimizerBase * > &optimizers)
void TemporarilyMarkOptimizerAsUnselectable(OptimizerIndex optimizer_index)
void SetOptimizerRunnability(OptimizerIndex optimizer_index, bool runnable)
bool ShouldBeRun(const ProblemState &problem_state) const override
Definition: bop_portfolio.h:77
Status Optimize(const BopParameters &parameters, const ProblemState &problem_state, LearnedInfo *learned_info, TimeLimit *time_limit) override
PortfolioOptimizer(const ProblemState &problem_state, const BopParameters &parameters, const BopSolverOptimizerSet &optimizer_set, const std::string &name)
const sat::LinearBooleanProblem & original_problem() const
Definition: bop_base.h:204
const BopSolution & solution() const
Definition: bop_base.h:199
void AddPropagator(SatPropagator *propagator)
Definition: sat_solver.cc:448
void TakePropagatorOwnership(std::unique_ptr< SatPropagator > propagator)
Definition: sat_solver.h:150
int64_t b
int64_t a
SatParameters parameters
ModelSharedTimeLimit * time_limit
const std::string name
absl::Status status
Definition: g_gurobi.cc:41
void STLDeleteElements(T *container)
Definition: stl_util.h:372
BopOptimizerBase::Status LoadStateProblemToSatSolver(const ProblemState &problem_state, sat::SatSolver *sat_solver)
Definition: bop_util.cc:89
const OptimizerIndex kInvalidOptimizerIndex(-1)
absl::StrongVector< SparseIndex, BopConstraintTerm > BopConstraintTerms
Definition: bop_types.h:89
constexpr double kInfinity
Definition: lp_types.h:88
void UseObjectiveForSatAssignmentPreference(const LinearBooleanProblem &problem, SatSolver *solver)
void FindLinearBooleanProblemSymmetries(const LinearBooleanProblem &problem, std::vector< std::unique_ptr< SparsePermutation >> *generators)
Collection of objects used to extend the Constraint Solver library.
int64_t weight
Definition: pack.cc:510
BaseVariableAssignmentSelector *const selector_
Definition: search.cc:1932
constexpr double kInfinity
#define VLOG(verboselevel)
Definition: vlog.h:39
#define VLOG_IS_ON(verboselevel)
Definition: vlog_is_on.h:47