Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
sat_decision.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 <algorithm>
17#include <cmath>
18#include <cstdint>
19#include <memory>
20#include <random>
21#include <utility>
22#include <vector>
23
24#include "absl/algorithm/container.h"
25#include "absl/log/check.h"
26#include "absl/types/span.h"
29#include "ortools/sat/model.h"
33#include "ortools/sat/util.h"
34#include "ortools/util/bitset.h"
37
38namespace operations_research {
39namespace sat {
40
42 : parameters_(*(model->GetOrCreate<SatParameters>())),
43 trail_(*model->GetOrCreate<Trail>()),
44 random_(model->GetOrCreate<ModelRandomGenerator>()),
45 ls_hints_(model->GetOrCreate<SharedLsSolutionRepository>()) {}
46
48 const int old_num_variables = activities_.size();
49 DCHECK_GE(num_variables, activities_.size());
50
51 activities_.resize(num_variables, parameters_.initial_variables_activity());
52 tie_breakers_.resize(num_variables, 0.0);
53 num_bumps_.clear();
54 pq_need_update_for_var_at_trail_index_.IncreaseSize(num_variables);
55
56 has_forced_polarity_.resize(num_variables, false);
57 forced_polarity_.resize(num_variables);
58 has_target_polarity_.resize(num_variables, false);
59 target_polarity_.resize(num_variables);
60 var_polarity_.resize(num_variables);
61
62 ResetInitialPolarity(/*from=*/old_num_variables);
63
64 // Update the priority queue. Note that each addition is in O(1) because
65 // the priority is 0.0.
66 var_ordering_.Reserve(num_variables);
67 if (var_ordering_is_initialized_) {
68 for (BooleanVariable var(old_num_variables); var < num_variables; ++var) {
69 var_ordering_.Add({var, 0.0, activities_[var]});
70 }
71 }
72}
73
74void SatDecisionPolicy::BeforeConflict(int trail_index) {
75 if (parameters_.use_erwa_heuristic()) {
76 ++num_conflicts_;
77 num_conflicts_stack_.push_back({trail_.Index(), 1});
78 }
79
80 if (trail_index > target_length_) {
81 target_length_ = trail_index;
82 has_target_polarity_.assign(has_target_polarity_.size(), false);
83 for (int i = 0; i < trail_index; ++i) {
84 const Literal l = trail_[i];
85 has_target_polarity_[l.Variable()] = true;
86 target_polarity_[l.Variable()] = l.IsPositive();
87 }
88 }
89
90 if (trail_index > best_partial_assignment_.size()) {
91 best_partial_assignment_.assign(trail_.IteratorAt(0),
92 trail_.IteratorAt(trail_index));
93 }
94
95 --num_conflicts_until_rephase_;
96 RephaseIfNeeded();
97}
98
99void SatDecisionPolicy::RephaseIfNeeded() {
100 if (parameters_.polarity_rephase_increment() <= 0) return;
101 if (num_conflicts_until_rephase_ > 0) return;
102
103 VLOG(1) << "End of polarity phase " << polarity_phase_
104 << " target_length: " << target_length_
105 << " best_length: " << best_partial_assignment_.size();
106
107 ++polarity_phase_;
108 num_conflicts_until_rephase_ =
109 parameters_.polarity_rephase_increment() * (polarity_phase_ + 1);
110
111 // We always reset the target each time we change phase.
112 target_length_ = 0;
113 has_target_polarity_.assign(has_target_polarity_.size(), false);
114
115 // Cycle between different initial polarities. Note that we already start by
116 // the default polarity, and this code is reached the first time with a
117 // polarity_phase_ of 1.
118 switch (polarity_phase_ % 8) {
119 case 0:
120 ResetInitialPolarity(/*from=*/0);
121 break;
122 case 1:
123 UseLongestAssignmentAsInitialPolarity();
124 break;
125 case 2:
126 ResetInitialPolarity(/*from=*/0, /*inverted=*/true);
127 break;
128 case 3:
129 UseLongestAssignmentAsInitialPolarity();
130 break;
131 case 4:
132 RandomizeCurrentPolarity();
133 break;
134 case 5:
135 UseLongestAssignmentAsInitialPolarity();
136 break;
137 case 6:
138 FlipCurrentPolarity();
139 break;
140 case 7:
141 if (UseLsSolutionAsInitialPolarity()) break;
142 UseLongestAssignmentAsInitialPolarity();
143 break;
144 }
145}
146
148 const int num_variables = activities_.size();
149 variable_activity_increment_ = 1.0;
150 activities_.assign(num_variables, parameters_.initial_variables_activity());
151 tie_breakers_.assign(num_variables, 0.0);
152 num_bumps_.clear();
153 var_ordering_.Clear();
154
155 polarity_phase_ = 0;
156 num_conflicts_until_rephase_ = parameters_.polarity_rephase_increment();
157
158 ResetInitialPolarity(/*from=*/0);
159 has_target_polarity_.assign(num_variables, false);
160 has_forced_polarity_.assign(num_variables, false);
161 best_partial_assignment_.clear();
162
163 num_conflicts_ = 0;
164 num_conflicts_stack_.clear();
165
166 var_ordering_is_initialized_ = false;
167}
168
169void SatDecisionPolicy::ResetInitialPolarity(int from, bool inverted) {
170 // Sets the initial polarity.
171 const int num_variables = activities_.size();
172 for (BooleanVariable var(from); var < num_variables; ++var) {
173 switch (parameters_.initial_polarity()) {
175 var_polarity_[var] = inverted ? false : true;
176 break;
178 var_polarity_[var] = inverted ? true : false;
179 break;
181 var_polarity_[var] = std::uniform_int_distribution<int>(0, 1)(*random_);
182 break;
183 }
184 }
185}
186
187void SatDecisionPolicy::UseLongestAssignmentAsInitialPolarity() {
188 // In this special case, we just overwrite partially the current fixed
189 // polarity and reset the best best_partial_assignment_ for the next such
190 // phase.
191 for (const Literal l : best_partial_assignment_) {
192 var_polarity_[l.Variable()] = l.IsPositive();
193 }
194 best_partial_assignment_.clear();
195}
196
197bool SatDecisionPolicy::UseLsSolutionAsInitialPolarity() {
198 if (!parameters_.polarity_exploit_ls_hints()) return false;
199
200 if (ls_hints_->NumSolutions() == 0) return false;
201
202 // This is in term of proto variable.
203 // TODO(user): use cp_model_mapping. But this is not needed to experiment
204 // on pure sat problems.
205 std::shared_ptr<const SharedLsSolutionRepository::Solution> solution =
206 ls_hints_->GetRandomBiasedSolution(*random_);
207 if (solution->variable_values.size() != var_polarity_.size()) return false;
208
209 for (int i = 0; i < solution->variable_values.size(); ++i) {
210 var_polarity_[BooleanVariable(i)] = solution->variable_values[i] == 1;
211 }
212
213 return false;
214}
215
216void SatDecisionPolicy::FlipCurrentPolarity() {
217 const int num_variables = var_polarity_.size();
218 for (BooleanVariable var; var < num_variables; ++var) {
219 var_polarity_[var] = !var_polarity_[var];
220 }
221}
222
223void SatDecisionPolicy::RandomizeCurrentPolarity() {
224 const int num_variables = var_polarity_.size();
225 for (BooleanVariable var; var < num_variables; ++var) {
226 var_polarity_[var] = std::uniform_int_distribution<int>(0, 1)(*random_);
227 }
228}
229
231 DCHECK_EQ(trail_.CurrentDecisionLevel(), 0);
232 CHECK(!activities_.empty());
233 const double max_activity =
234 *absl::c_max_element(activities_) + variable_activity_increment_;
235 const double kDecay = 0.999;
236 variable_activity_increment_ =
237 max_activity / pow(kDecay, best_partial_assignment_.size() + 1);
238 var_ordering_is_initialized_ = false;
239 if (max_activity + variable_activity_increment_ >
240 parameters_.max_variable_activity_value()) {
241 RescaleVariableActivities(1 / parameters_.max_variable_activity_value());
242 }
243 double weight = 1.0;
244 for (int i = 0; i < best_partial_assignment_.size(); ++i) {
245 const Literal l = best_partial_assignment_[i];
246 weight *= kDecay;
247 activities_[l.Variable()] += weight * variable_activity_increment_;
248 }
249}
250
251void SatDecisionPolicy::InitializeVariableOrdering() {
252 const int num_variables = activities_.size();
253
254 // First, extract the variables without activity, and add the other to the
255 // priority queue.
256 var_ordering_.Clear();
257 tmp_variables_.clear();
258 for (BooleanVariable var(0); var < num_variables; ++var) {
259 if (!trail_.Assignment().VariableIsAssigned(var)) {
260 if (activities_[var] > 0.0) {
261 var_ordering_.Add({var, tie_breakers_[var], activities_[var]});
262 } else {
263 tmp_variables_.push_back(var);
264 }
265 }
266 }
267
268 // Set the order of the other according to the parameters_.
269 // Note that this is just a "preference" since the priority queue will kind
270 // of randomize this. However, it is more efficient than using the tie_breaker
271 // which add a big overhead on the priority queue.
272 //
273 // TODO(user): Experiment and come up with a good set of heuristics.
274 switch (parameters_.preferred_variable_order()) {
276 break;
278 std::reverse(tmp_variables_.begin(), tmp_variables_.end());
279 break;
281 std::shuffle(tmp_variables_.begin(), tmp_variables_.end(), *random_);
282 break;
283 }
284
285 // Add the variables without activity to the queue (in the default order)
286 for (const BooleanVariable var : tmp_variables_) {
287 var_ordering_.Add({var, tie_breakers_[var], 0.0});
288 }
289
290 // Finish the queue initialization.
291 pq_need_update_for_var_at_trail_index_.ClearAndResize(num_variables);
292 pq_need_update_for_var_at_trail_index_.SetAllBefore(trail_.Index());
293 var_ordering_is_initialized_ = true;
294}
295
297 if (!parameters_.use_optimization_hints()) return;
298 DCHECK_GE(weight, 0.0);
299 DCHECK_LE(weight, 1.0);
300
301 has_forced_polarity_[literal.Variable()] = true;
302 forced_polarity_[literal.Variable()] = literal.IsPositive();
303
304 // The tie_breaker is changed, so we need to reinitialize the priority queue.
305 // Note that this doesn't change the activity though.
306 tie_breakers_[literal.Variable()] = weight;
307 var_ordering_is_initialized_ = false;
308}
309
310std::vector<std::pair<Literal, float>> SatDecisionPolicy::AllPreferences()
311 const {
312 std::vector<std::pair<Literal, float>> prefs;
313 for (BooleanVariable var(0); var < var_polarity_.size(); ++var) {
314 // TODO(user): we currently assume that if the tie_breaker is zero then
315 // no preference was set (which is not 100% correct). Fix that.
316 const float value = var_ordering_.GetElement(var.value()).tie_breaker;
317 if (value > 0.0) {
318 prefs.push_back(std::make_pair(Literal(var, var_polarity_[var]), value));
319 }
320 }
321 return prefs;
322}
323
325 absl::Span<const Literal> literals) {
326 if (parameters_.use_erwa_heuristic()) {
327 if (num_bumps_.size() != activities_.size()) {
328 num_bumps_.resize(activities_.size(), 0);
329 }
330 for (const Literal literal : literals) {
331 // Note that we don't really need to bump level 0 variables since they
332 // will never be backtracked over. However it is faster to simply bump
333 // them.
334 ++num_bumps_[literal.Variable()];
335 }
336 return;
337 }
338
339 const double max_activity_value = parameters_.max_variable_activity_value();
340 for (const Literal literal : literals) {
341 const BooleanVariable var = literal.Variable();
342 const int level = trail_.Info(var).level;
343 if (level == 0) continue;
344 activities_[var] += variable_activity_increment_;
345 pq_need_update_for_var_at_trail_index_.Set(trail_.Info(var).trail_index);
346 if (activities_[var] > max_activity_value) {
347 RescaleVariableActivities(1.0 / max_activity_value);
348 }
349 }
350}
351
352void SatDecisionPolicy::RescaleVariableActivities(double scaling_factor) {
353 variable_activity_increment_ *= scaling_factor;
354 for (BooleanVariable var(0); var < activities_.size(); ++var) {
355 activities_[var] *= scaling_factor;
356 }
357
358 // When rescaling the activities of all the variables, the order of the
359 // active variables in the heap will not change, but we still need to update
360 // their weights so that newly inserted elements will compare correctly with
361 // already inserted ones.
362 //
363 // IMPORTANT: we need to reset the full heap from scratch because just
364 // multiplying the current weight by scaling_factor is not guaranteed to
365 // preserve the order. This is because the activity of two entries may go to
366 // zero and the tie-breaking ordering may change their relative order.
367 //
368 // InitializeVariableOrdering() will be called lazily only if needed.
369 var_ordering_is_initialized_ = false;
370}
371
373 variable_activity_increment_ *= 1.0 / parameters_.variable_activity_decay();
374}
375
377 // Lazily initialize var_ordering_ if needed.
378 if (!var_ordering_is_initialized_) {
379 InitializeVariableOrdering();
380 }
381
382 // Choose the variable.
383 BooleanVariable var;
384 const double ratio = parameters_.random_branches_ratio();
385 auto zero_to_one = [this]() {
386 return std::uniform_real_distribution<double>()(*random_);
387 };
388 if (ratio != 0.0 && zero_to_one() < ratio) {
389 while (true) {
390 // TODO(user): This may not be super efficient if almost all the
391 // variables are assigned.
392 std::uniform_int_distribution<int> index_dist(0,
393 var_ordering_.Size() - 1);
394 var = var_ordering_.QueueElement(index_dist(*random_)).var;
395 if (!trail_.Assignment().VariableIsAssigned(var)) break;
396 pq_need_update_for_var_at_trail_index_.Set(trail_.Info(var).trail_index);
397 var_ordering_.Remove(var.value());
398 }
399 } else {
400 // The loop is done this way in order to leave the final choice in the heap.
401 DCHECK(!var_ordering_.IsEmpty());
402 var = var_ordering_.Top().var;
403 while (trail_.Assignment().VariableIsAssigned(var)) {
404 var_ordering_.Pop();
405 pq_need_update_for_var_at_trail_index_.Set(trail_.Info(var).trail_index);
406 DCHECK(!var_ordering_.IsEmpty());
407 var = var_ordering_.Top().var;
408 }
409 }
410
411 // Choose its polarity (i.e. True of False).
412 const double random_ratio = parameters_.random_polarity_ratio();
413 if (random_ratio != 0.0 && zero_to_one() < random_ratio) {
414 return Literal(var, std::uniform_int_distribution<int>(0, 1)(*random_));
415 }
416
417 if (has_forced_polarity_[var]) return Literal(var, forced_polarity_[var]);
418 if (in_stable_phase_ && has_target_polarity_[var]) {
419 return Literal(var, target_polarity_[var]);
420 }
421 return Literal(var, var_polarity_[var]);
422}
423
424void SatDecisionPolicy::PqInsertOrUpdate(BooleanVariable var) {
425 const WeightedVarQueueElement element{var, tie_breakers_[var],
426 activities_[var]};
427 if (var_ordering_.Contains(var.value())) {
428 // Note that the new weight should always be higher than the old one.
429 var_ordering_.IncreasePriority(element);
430 } else {
431 var_ordering_.Add(element);
432 }
433}
434
435void SatDecisionPolicy::Untrail(int target_trail_index) {
436 // TODO(user): avoid looping twice over the trail?
437 if (maybe_enable_phase_saving_ && parameters_.use_phase_saving()) {
438 for (int i = target_trail_index; i < trail_.Index(); ++i) {
439 const Literal l = trail_[i];
440 var_polarity_[l.Variable()] = l.IsPositive();
441 }
442 }
443
444 DCHECK_LT(target_trail_index, trail_.Index());
445 if (parameters_.use_erwa_heuristic()) {
446 if (num_bumps_.size() != activities_.size()) {
447 num_bumps_.resize(activities_.size(), 0);
448 }
449
450 // The ERWA parameter between the new estimation of the learning rate and
451 // the old one. TODO(user): Expose parameters for these values.
452 const double alpha = std::max(0.06, 0.4 - 1e-6 * num_conflicts_);
453
454 // This counts the number of conflicts since the assignment of the variable
455 // at the current trail_index that we are about to untrail.
456 int num_conflicts = 0;
457 int next_num_conflicts_update =
458 num_conflicts_stack_.empty() ? -1
459 : num_conflicts_stack_.back().trail_index;
460
461 int trail_index = trail_.Index();
462 while (trail_index > target_trail_index) {
463 if (next_num_conflicts_update == trail_index) {
464 num_conflicts += num_conflicts_stack_.back().count;
465 num_conflicts_stack_.pop_back();
466 next_num_conflicts_update =
467 num_conflicts_stack_.empty()
468 ? -1
469 : num_conflicts_stack_.back().trail_index;
470 }
471 const BooleanVariable var = trail_[--trail_index].Variable();
472
473 // TODO(user): This heuristic can make this code quite slow because
474 // all the untrailed variable will cause a priority queue update.
475 if (num_conflicts > 0) {
476 const int64_t num_bumps = num_bumps_[var];
477 double new_rate = 0.0;
478 if (num_bumps > 0) {
479 num_bumps_[var] = 0;
480 new_rate = static_cast<double>(num_bumps) / num_conflicts;
481 }
482 activities_[var] = alpha * new_rate + (1 - alpha) * activities_[var];
483 }
484 if (var_ordering_is_initialized_) PqInsertOrUpdate(var);
485 }
486 if (num_conflicts > 0) {
487 if (!num_conflicts_stack_.empty() &&
488 num_conflicts_stack_.back().trail_index == trail_.Index()) {
489 num_conflicts_stack_.back().count += num_conflicts;
490 } else {
491 num_conflicts_stack_.push_back({trail_.Index(), num_conflicts});
492 }
493 }
494 } else {
495 if (!var_ordering_is_initialized_) return;
496
497 // Trail index of the next variable that will need a priority queue update.
498 int to_update = pq_need_update_for_var_at_trail_index_.Top();
499 while (to_update >= target_trail_index) {
500 DCHECK_LT(to_update, trail_.Index());
501 PqInsertOrUpdate(trail_[to_update].Variable());
502 pq_need_update_for_var_at_trail_index_.ClearTop();
503 to_update = pq_need_update_for_var_at_trail_index_.Top();
504 }
505 }
506
507 // Invariant.
508 if (DEBUG_MODE && var_ordering_is_initialized_) {
509 for (int trail_index = trail_.Index() - 1; trail_index > target_trail_index;
510 --trail_index) {
511 const BooleanVariable var = trail_[trail_index].Variable();
512 CHECK(var_ordering_.Contains(var.value()));
513 CHECK_EQ(activities_[var], var_ordering_.GetElement(var.value()).weight);
514 }
515 }
516}
517
518} // namespace sat
519} // namespace operations_research
BooleanVariable Variable() const
Definition sat_base.h:88
void BumpVariableActivities(absl::Span< const Literal > literals)
std::vector< std::pair< Literal, float > > AllPreferences() const
void SetAssignmentPreference(Literal literal, float weight)
::operations_research::sat::SatParameters_Polarity initial_polarity() const
static constexpr VariableOrder IN_RANDOM_ORDER
static constexpr VariableOrder IN_REVERSE_ORDER
static constexpr VariableOrder IN_ORDER
const VariablesAssignment & Assignment() const
Definition sat_base.h:654
bool VariableIsAssigned(BooleanVariable var) const
Definition sat_base.h:214
void assign(size_type n, const value_type &val)
OR-Tools root namespace.
Select next search node to expand Select next item_i to add this new search node to the search Generate a new search node where item_i is not in the knapsack Check validity of this new partial solution(using propagators) - If valid
const bool DEBUG_MODE
Definition radix_sort.h:266