Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
bop_portfolio.cc
Go to the documentation of this file.
1// Copyright 2010-2024 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 <stddef.h>
17
18#include <algorithm>
19#include <cstdint>
20#include <limits>
21#include <memory>
22#include <string>
23#include <utility>
24#include <vector>
25
26#include "absl/log/check.h"
27#include "absl/strings/str_format.h"
28#include "absl/strings/string_view.h"
34#include "ortools/bop/bop_fs.h"
35#include "ortools/bop/bop_lns.h"
36#include "ortools/bop/bop_ls.h"
37#include "ortools/bop/bop_parameters.pb.h"
44#include "ortools/sat/boolean_problem.pb.h"
50
51namespace operations_research {
52namespace bop {
53
54using ::operations_research::sat::LinearBooleanProblem;
55using ::operations_research::sat::LinearObjective;
56
57namespace {
58void BuildObjectiveTerms(const LinearBooleanProblem& problem,
59 BopConstraintTerms* objective_terms) {
60 CHECK(objective_terms != nullptr);
61
62 if (!objective_terms->empty()) return;
63
64 const LinearObjective& objective = problem.objective();
65 const size_t num_objective_terms = objective.literals_size();
66 CHECK_EQ(num_objective_terms, objective.coefficients_size());
67 for (int i = 0; i < num_objective_terms; ++i) {
68 CHECK_GT(objective.literals(i), 0);
69 CHECK_NE(objective.coefficients(i), 0);
70
71 const VariableIndex var_id(objective.literals(i) - 1);
72 const int64_t weight = objective.coefficients(i);
73 objective_terms->push_back(BopConstraintTerm(var_id, weight));
74 }
75}
76} // anonymous namespace
77
78//------------------------------------------------------------------------------
79// PortfolioOptimizer
80//------------------------------------------------------------------------------
82 const ProblemState& problem_state, const BopParameters& parameters,
83 const BopSolverOptimizerSet& optimizer_set, absl::string_view name)
85 random_(parameters.random_seed()),
86 state_update_stamp_(ProblemState::kInitialStampValue),
87 objective_terms_(),
88 selector_(),
89 optimizers_(),
90 sat_propagator_(),
91 parameters_(parameters),
92 lower_bound_(-glop::kInfinity),
93 upper_bound_(glop::kInfinity),
94 number_of_consecutive_failing_optimizers_(0) {
95 CreateOptimizers(problem_state.original_problem(), parameters, optimizer_set);
96}
97
99 if (parameters_.log_search_progress() || VLOG_IS_ON(1)) {
100 std::string stats_string;
101 for (OptimizerIndex i(0); i < optimizers_.size(); ++i) {
102 if (selector_->NumCallsForOptimizer(i) > 0) {
103 stats_string += selector_->PrintStats(i);
104 }
105 }
106 if (!stats_string.empty()) {
107 LOG(INFO) << "Stats. #new_solutions/#calls by optimizer:\n" +
108 stats_string;
109 }
110 }
111
112 // Note that unique pointers are not used due to unsupported emplace_back
113 // in ITIVectors.
114 gtl::STLDeleteElements(&optimizers_);
115}
116
117BopOptimizerBase::Status PortfolioOptimizer::SynchronizeIfNeeded(
118 const ProblemState& problem_state) {
119 if (state_update_stamp_ == problem_state.update_stamp()) {
121 }
122 state_update_stamp_ = problem_state.update_stamp();
123
124 // Load any new information into the sat_propagator_.
125 const bool first_time = (sat_propagator_.NumVariables() == 0);
127 LoadStateProblemToSatSolver(problem_state, &sat_propagator_);
129 if (first_time) {
130 // We configure the sat_propagator_ to use the objective as an assignment
131 // preference
132 UseObjectiveForSatAssignmentPreference(problem_state.original_problem(),
133 &sat_propagator_);
134 }
135
136 lower_bound_ = problem_state.GetScaledLowerBound();
137 upper_bound_ = problem_state.solution().IsFeasible()
138 ? problem_state.solution().GetScaledCost()
141}
142
144 const BopParameters& parameters, const ProblemState& problem_state,
146 CHECK(learned_info != nullptr);
147 CHECK(time_limit != nullptr);
148 learned_info->Clear();
149
150 const BopOptimizerBase::Status sync_status =
151 SynchronizeIfNeeded(problem_state);
152 if (sync_status != BopOptimizerBase::CONTINUE) {
153 return sync_status;
154 }
155
156 for (OptimizerIndex i(0); i < optimizers_.size(); ++i) {
157 selector_->SetOptimizerRunnability(
158 i, optimizers_[i]->ShouldBeRun(problem_state));
159 }
160
161 const int64_t init_cost = problem_state.solution().IsFeasible()
162 ? problem_state.solution().GetCost()
163 : std::numeric_limits<int64_t>::max();
164 const double init_deterministic_time =
165 time_limit->GetElapsedDeterministicTime();
166
167 const OptimizerIndex selected_optimizer_id = selector_->SelectOptimizer();
168 if (selected_optimizer_id == kInvalidOptimizerIndex) {
169 LOG(INFO) << "All the optimizers are done.";
171 }
172 BopOptimizerBase* const selected_optimizer =
173 optimizers_[selected_optimizer_id];
174 if (parameters.log_search_progress() || VLOG_IS_ON(1)) {
175 LOG(INFO) << " " << lower_bound_ << " .. " << upper_bound_ << " "
176 << name() << " - " << selected_optimizer->name()
177 << ". Time limit: " << time_limit->GetTimeLeft() << " -- "
178 << time_limit->GetDeterministicTimeLeft();
179 }
180 const BopOptimizerBase::Status optimization_status =
181 selected_optimizer->Optimize(parameters, problem_state, learned_info,
182 time_limit);
183
184 // ABORT means that this optimizer can't be run until we found a new solution.
185 if (optimization_status == BopOptimizerBase::ABORT) {
186 selector_->TemporarilyMarkOptimizerAsUnselectable(selected_optimizer_id);
187 }
188
189 // The gain is defined as 1 for the first solution.
190 // TODO(user): Is 1 the right value? It might be better to use a percentage
191 // of the gap, or use the same gain as for the second solution.
192 const int64_t gain =
193 optimization_status == BopOptimizerBase::SOLUTION_FOUND
194 ? (init_cost == std::numeric_limits<int64_t>::max()
195 ? 1
196 : init_cost - learned_info->solution.GetCost())
197 : 0;
198 const double spent_deterministic_time =
199 time_limit->GetElapsedDeterministicTime() - init_deterministic_time;
200 selector_->UpdateScore(gain, spent_deterministic_time);
201
202 if (optimization_status == BopOptimizerBase::INFEASIBLE ||
203 optimization_status == BopOptimizerBase::OPTIMAL_SOLUTION_FOUND) {
204 return optimization_status;
205 }
206
207 // Stop the portfolio optimizer after too many unsuccessful calls.
208 if (parameters.has_max_number_of_consecutive_failing_optimizer_calls() &&
209 problem_state.solution().IsFeasible()) {
210 number_of_consecutive_failing_optimizers_ =
211 optimization_status == BopOptimizerBase::SOLUTION_FOUND
212 ? 0
213 : number_of_consecutive_failing_optimizers_ + 1;
214 if (number_of_consecutive_failing_optimizers_ >
215 parameters.max_number_of_consecutive_failing_optimizer_calls()) {
217 }
218 }
219
220 // TODO(user): don't penalize the SatCoreBasedOptimizer or the
221 // LinearRelaxation when they improve the lower bound.
222 // TODO(user): Do we want to re-order the optimizers in the selector when
223 // the status is BopOptimizerBase::INFORMATION_FOUND?
225}
226
227void PortfolioOptimizer::AddOptimizer(
228 const LinearBooleanProblem& problem, const BopParameters& parameters,
229 const BopOptimizerMethod& optimizer_method) {
230 switch (optimizer_method.type()) {
231 case BopOptimizerMethod::SAT_CORE_BASED:
232 optimizers_.push_back(new SatCoreBasedOptimizer("SatCoreBasedOptimizer"));
233 break;
234 case BopOptimizerMethod::SAT_LINEAR_SEARCH:
235 optimizers_.push_back(new GuidedSatFirstSolutionGenerator(
237 break;
238 case BopOptimizerMethod::LINEAR_RELAXATION:
239 optimizers_.push_back(
240 new LinearRelaxation(parameters, "LinearRelaxation"));
241 break;
242 case BopOptimizerMethod::LOCAL_SEARCH: {
243 for (int i = 1; i <= parameters.max_num_decisions_in_ls(); ++i) {
244 optimizers_.push_back(new LocalSearchOptimizer(
245 absl::StrFormat("LS_%d", i), i, random_, &sat_propagator_));
246 }
247 } break;
248 case BopOptimizerMethod::RANDOM_FIRST_SOLUTION:
249 optimizers_.push_back(new BopRandomFirstSolutionGenerator(
250 "SATRandomFirstSolution", parameters, &sat_propagator_, random_));
251 break;
252 case BopOptimizerMethod::RANDOM_VARIABLE_LNS:
253 BuildObjectiveTerms(problem, &objective_terms_);
254 optimizers_.push_back(new BopAdaptiveLNSOptimizer(
255 "RandomVariableLns",
256 /*use_lp_to_guide_sat=*/false,
257 new ObjectiveBasedNeighborhood(&objective_terms_, random_),
258 &sat_propagator_));
259 break;
260 case BopOptimizerMethod::RANDOM_VARIABLE_LNS_GUIDED_BY_LP:
261 BuildObjectiveTerms(problem, &objective_terms_);
262 optimizers_.push_back(new BopAdaptiveLNSOptimizer(
263 "RandomVariableLnsWithLp",
264 /*use_lp_to_guide_sat=*/true,
265 new ObjectiveBasedNeighborhood(&objective_terms_, random_),
266 &sat_propagator_));
267 break;
268 case BopOptimizerMethod::RANDOM_CONSTRAINT_LNS:
269 BuildObjectiveTerms(problem, &objective_terms_);
270 optimizers_.push_back(new BopAdaptiveLNSOptimizer(
271 "RandomConstraintLns",
272 /*use_lp_to_guide_sat=*/false,
273 new ConstraintBasedNeighborhood(&objective_terms_, random_),
274 &sat_propagator_));
275 break;
276 case BopOptimizerMethod::RANDOM_CONSTRAINT_LNS_GUIDED_BY_LP:
277 BuildObjectiveTerms(problem, &objective_terms_);
278 optimizers_.push_back(new BopAdaptiveLNSOptimizer(
279 "RandomConstraintLnsWithLp",
280 /*use_lp_to_guide_sat=*/true,
281 new ConstraintBasedNeighborhood(&objective_terms_, random_),
282 &sat_propagator_));
283 break;
284 case BopOptimizerMethod::RELATION_GRAPH_LNS:
285 BuildObjectiveTerms(problem, &objective_terms_);
286 optimizers_.push_back(new BopAdaptiveLNSOptimizer(
287 "RelationGraphLns",
288 /*use_lp_to_guide_sat=*/false,
289 new RelationGraphBasedNeighborhood(problem, random_),
290 &sat_propagator_));
291 break;
292 case BopOptimizerMethod::RELATION_GRAPH_LNS_GUIDED_BY_LP:
293 BuildObjectiveTerms(problem, &objective_terms_);
294 optimizers_.push_back(new BopAdaptiveLNSOptimizer(
295 "RelationGraphLnsWithLp",
296 /*use_lp_to_guide_sat=*/true,
297 new RelationGraphBasedNeighborhood(problem, random_),
298 &sat_propagator_));
299 break;
300 case BopOptimizerMethod::COMPLETE_LNS:
301 BuildObjectiveTerms(problem, &objective_terms_);
302 optimizers_.push_back(
303 new BopCompleteLNSOptimizer("LNS", objective_terms_));
304 break;
305 case BopOptimizerMethod::USER_GUIDED_FIRST_SOLUTION:
306 optimizers_.push_back(new GuidedSatFirstSolutionGenerator(
307 "SATUserGuidedFirstSolution",
309 break;
310 case BopOptimizerMethod::LP_FIRST_SOLUTION:
311 optimizers_.push_back(new GuidedSatFirstSolutionGenerator(
312 "SATLPFirstSolution",
314 break;
315 case BopOptimizerMethod::OBJECTIVE_FIRST_SOLUTION:
316 optimizers_.push_back(new GuidedSatFirstSolutionGenerator(
317 "SATObjectiveFirstSolution",
319 break;
320 default:
321 LOG(FATAL) << "Unknown optimizer type.";
322 }
323}
324
325void PortfolioOptimizer::CreateOptimizers(
326 const LinearBooleanProblem& problem, const BopParameters& parameters,
327 const BopSolverOptimizerSet& optimizer_set) {
328 if (parameters.use_symmetry()) {
329 VLOG(1) << "Finding symmetries of the problem.";
330 std::vector<std::unique_ptr<SparsePermutation>> generators;
331 sat::FindLinearBooleanProblemSymmetries(problem, &generators);
332 std::unique_ptr<sat::SymmetryPropagator> propagator(
333 new sat::SymmetryPropagator);
334 for (int i = 0; i < generators.size(); ++i) {
335 propagator->AddSymmetry(std::move(generators[i]));
336 }
337 sat_propagator_.AddPropagator(propagator.get());
338 sat_propagator_.TakePropagatorOwnership(std::move(propagator));
339 }
340
341 const int max_num_optimizers =
342 optimizer_set.methods_size() + parameters.max_num_decisions_in_ls() - 1;
343 optimizers_.reserve(max_num_optimizers);
344 for (const BopOptimizerMethod& optimizer_method : optimizer_set.methods()) {
345 const OptimizerIndex old_size(optimizers_.size());
346 AddOptimizer(problem, parameters, optimizer_method);
347 }
348
349 selector_ = std::make_unique<OptimizerSelector>(optimizers_);
350}
351
352//------------------------------------------------------------------------------
353// OptimizerSelector
354//------------------------------------------------------------------------------
357 optimizers)
358 : run_infos_(), selected_index_(optimizers.size()) {
359 for (OptimizerIndex i(0); i < optimizers.size(); ++i) {
360 info_positions_.push_back(run_infos_.size());
361 run_infos_.push_back(RunInfo(i, optimizers[i]->name()));
362 }
363}
364
365OptimizerIndex OptimizerSelector::SelectOptimizer() {
366 CHECK_GE(selected_index_, 0);
367
368 do {
369 ++selected_index_;
370 } while (selected_index_ < run_infos_.size() &&
371 !run_infos_[selected_index_].RunnableAndSelectable());
372
373 if (selected_index_ >= run_infos_.size()) {
374 // Select the first possible optimizer.
375 selected_index_ = -1;
376 for (int i = 0; i < run_infos_.size(); ++i) {
377 if (run_infos_[i].RunnableAndSelectable()) {
378 selected_index_ = i;
379 break;
380 }
381 }
382 if (selected_index_ == -1) return kInvalidOptimizerIndex;
383 } else {
384 // Select the next possible optimizer. If none, select the first one.
385 // Check that the time is smaller than all previous optimizers which are
386 // runnable.
387 bool too_much_time_spent = false;
388 const double time_spent =
389 run_infos_[selected_index_].time_spent_since_last_solution;
390 for (int i = 0; i < selected_index_; ++i) {
391 const RunInfo& info = run_infos_[i];
392 if (info.RunnableAndSelectable() &&
393 info.time_spent_since_last_solution < time_spent) {
394 too_much_time_spent = true;
395 break;
396 }
397 }
398 if (too_much_time_spent) {
399 // TODO(user): Remove this recursive call, even if in practice it's
400 // safe because the max depth is the number of optimizers.
401 return SelectOptimizer();
402 }
403 }
404
405 // Select the optimizer.
406 ++run_infos_[selected_index_].num_calls;
407 return run_infos_[selected_index_].optimizer_index;
408}
409
410void OptimizerSelector::UpdateScore(int64_t gain, double time_spent) {
411 const bool new_solution_found = gain != 0;
412 if (new_solution_found) NewSolutionFound(gain);
413 UpdateDeterministicTime(time_spent);
415 const double new_score = time_spent == 0.0 ? 0.0 : gain / time_spent;
416 const double kErosion = 0.2;
417 const double kMinScore = 1E-6;
418
419 RunInfo& info = run_infos_[selected_index_];
420 const double old_score = info.score;
421 info.score =
422 std::max(kMinScore, old_score * (1 - kErosion) + kErosion * new_score);
423
424 if (new_solution_found) { // Solution found
425 UpdateOrder();
426 selected_index_ = run_infos_.size();
427 }
428}
429
431 OptimizerIndex optimizer_index) {
432 run_infos_[info_positions_[optimizer_index]].selectable = false;
433}
435void OptimizerSelector::SetOptimizerRunnability(OptimizerIndex optimizer_index,
436 bool runnable) {
437 run_infos_[info_positions_[optimizer_index]].runnable = runnable;
438}
441 OptimizerIndex optimizer_index) const {
442 const RunInfo& info = run_infos_[info_positions_[optimizer_index]];
443 return absl::StrFormat(
444 " %40s : %3d/%-3d (%6.2f%%) Total gain: %6d Total Dtime: %0.3f "
445 "score: %f\n",
446 info.name, info.num_successes, info.num_calls,
447 100.0 * info.num_successes / info.num_calls, info.total_gain,
448 info.time_spent, info.score);
449}
450
452 OptimizerIndex optimizer_index) const {
453 const RunInfo& info = run_infos_[info_positions_[optimizer_index]];
454 return info.num_calls;
456
458 std::string str;
459 for (int i = 0; i < run_infos_.size(); ++i) {
460 const RunInfo& info = run_infos_[i];
461 LOG(INFO) << " " << info.name << " " << info.total_gain
462 << " / " << info.time_spent << " = " << info.score << " "
463 << info.selectable << " " << info.time_spent_since_last_solution;
464 }
465}
466
467void OptimizerSelector::NewSolutionFound(int64_t gain) {
468 run_infos_[selected_index_].num_successes++;
469 run_infos_[selected_index_].total_gain += gain;
470
471 for (int i = 0; i < run_infos_.size(); ++i) {
472 run_infos_[i].time_spent_since_last_solution = 0;
473 run_infos_[i].selectable = true;
474 }
475}
476
477void OptimizerSelector::UpdateDeterministicTime(double time_spent) {
478 run_infos_[selected_index_].time_spent += time_spent;
479 run_infos_[selected_index_].time_spent_since_last_solution += time_spent;
480}
481
482void OptimizerSelector::UpdateOrder() {
483 // Re-sort optimizers.
484 std::stable_sort(run_infos_.begin(), run_infos_.end(),
485 [](const RunInfo& a, const RunInfo& b) -> bool {
486 if (a.total_gain == 0 && b.total_gain == 0)
487 return a.time_spent < b.time_spent;
488 return a.score > b.score;
489 });
490
491 // Update the positions.
492 for (int i = 0; i < run_infos_.size(); ++i) {
493 info_positions_[run_infos_[i].optimizer_index] = i;
494 }
495}
496
497} // namespace bop
498} // namespace operations_research
IntegerValue size
virtual Status Optimize(const BopParameters &parameters, const ProblemState &problem_state, LearnedInfo *learned_info, TimeLimit *time_limit)=0
@ ABORT
There is no need to call this optimizer again on the same problem state.
Definition bop_base.h:87
const std::string & name() const
Returns the name given at construction.
Definition bop_base.h:54
std::string PrintStats(OptimizerIndex optimizer_index) const
Returns statistics about the given optimizer.
void TemporarilyMarkOptimizerAsUnselectable(OptimizerIndex optimizer_index)
void SetOptimizerRunnability(OptimizerIndex optimizer_index, bool runnable)
void UpdateScore(int64_t gain, double time_spent)
int NumCallsForOptimizer(OptimizerIndex optimizer_index) const
void DebugPrint() const
Prints some debug information. Should not be used in production.
OptimizerSelector(const util_intops::StrongVector< OptimizerIndex, BopOptimizerBase * > &optimizers)
bool ShouldBeRun(const ProblemState &problem_state) const override
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, absl::string_view name)
void TakePropagatorOwnership(std::unique_ptr< SatPropagator > propagator)
Definition sat_solver.h:156
void AddPropagator(SatPropagator *propagator)
int64_t b
Definition table.cc:45
int64_t a
Definition table.cc:44
SatParameters parameters
const std::string name
A name for logging purposes.
absl::Status status
Definition g_gurobi.cc:44
time_limit
Definition solve.cc:22
void STLDeleteElements(T *container)
Definition stl_util.h:372
BopOptimizerBase::Status LoadStateProblemToSatSolver(const ProblemState &problem_state, sat::SatSolver *sat_solver)
Definition bop_util.cc:98
util_intops::StrongVector< SparseIndex, BopConstraintTerm > BopConstraintTerms
Definition bop_types.h:89
const OptimizerIndex kInvalidOptimizerIndex(-1)
constexpr double kInfinity
Infinity for type Fractional.
Definition lp_types.h:89
void FindLinearBooleanProblemSymmetries(const LinearBooleanProblem &problem, std::vector< std::unique_ptr< SparsePermutation > > *generators)
void UseObjectiveForSatAssignmentPreference(const LinearBooleanProblem &problem, SatSolver *solver)
In SWIG mode, we don't want anything besides these top-level includes.
int64_t weight
Definition pack.cc:510
BopSolution solution
New solution. Note that the solution might be infeasible.
Definition bop_base.h:276