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