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