Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
bop_ls.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
14#include "ortools/bop/bop_ls.h"
15
16#include <algorithm>
17#include <array>
18#include <cstdint>
19#include <cstdlib>
20#include <limits>
21#include <memory>
22#include <string>
23#include <vector>
24
25#include "absl/container/flat_hash_map.h"
26#include "absl/container/flat_hash_set.h"
27#include "absl/log/check.h"
28#include "absl/meta/type_traits.h"
29#include "absl/random/bit_gen_ref.h"
30#include "absl/strings/str_format.h"
31#include "absl/strings/string_view.h"
32#include "absl/types/span.h"
36#include "ortools/bop/bop_parameters.pb.h"
40#include "ortools/sat/boolean_problem.pb.h"
45
46namespace operations_research {
47namespace bop {
48
49using ::operations_research::sat::LinearBooleanConstraint;
50using ::operations_research::sat::LinearBooleanProblem;
51using ::operations_research::sat::LinearObjective;
52
53//------------------------------------------------------------------------------
54// LocalSearchOptimizer
55//------------------------------------------------------------------------------
56
58 int max_num_decisions,
59 absl::BitGenRef random,
60 sat::SatSolver* sat_propagator)
62 state_update_stamp_(ProblemState::kInitialStampValue),
63 max_num_decisions_(max_num_decisions),
64 sat_wrapper_(sat_propagator),
65 assignment_iterator_(),
66 random_(random) {}
67
69
70bool LocalSearchOptimizer::ShouldBeRun(
71 const ProblemState& problem_state) const {
72 return problem_state.solution().IsFeasible();
73}
74
75BopOptimizerBase::Status LocalSearchOptimizer::Optimize(
76 const BopParameters& parameters, const ProblemState& problem_state,
77 LearnedInfo* learned_info, TimeLimit* time_limit) {
78 CHECK(learned_info != nullptr);
79 CHECK(time_limit != nullptr);
80 learned_info->Clear();
81
82 if (assignment_iterator_ == nullptr) {
83 assignment_iterator_ = std::make_unique<LocalSearchAssignmentIterator>(
84 problem_state, max_num_decisions_,
85 parameters.max_num_broken_constraints_in_ls(), random_, &sat_wrapper_);
86 }
87
88 if (state_update_stamp_ != problem_state.update_stamp()) {
89 // We have a new problem_state.
90 state_update_stamp_ = problem_state.update_stamp();
91 assignment_iterator_->Synchronize(problem_state);
92 }
93 assignment_iterator_->SynchronizeSatWrapper();
94
95 double prev_deterministic_time = assignment_iterator_->deterministic_time();
96 assignment_iterator_->UseTranspositionTable(
97 parameters.use_transposition_table_in_ls());
98 assignment_iterator_->UsePotentialOneFlipRepairs(
99 parameters.use_potential_one_flip_repairs_in_ls());
100 int64_t num_assignments_to_explore =
101 parameters.max_number_of_explored_assignments_per_try_in_ls();
102
103 while (!time_limit->LimitReached() && num_assignments_to_explore > 0 &&
104 assignment_iterator_->NextAssignment()) {
105 time_limit->AdvanceDeterministicTime(
106 assignment_iterator_->deterministic_time() - prev_deterministic_time);
107 prev_deterministic_time = assignment_iterator_->deterministic_time();
108 --num_assignments_to_explore;
109 }
110 if (sat_wrapper_.IsModelUnsat()) {
111 // TODO(user): we do that all the time, return an UNSAT satus instead and
112 // do this only once.
113 return problem_state.solution().IsFeasible()
116 }
117
118 // TODO(user): properly abort when we found a new solution and then finished
119 // the ls? note that this is minor.
120 sat_wrapper_.ExtractLearnedInfo(learned_info);
121 if (assignment_iterator_->BetterSolutionHasBeenFound()) {
122 // TODO(user): simply use vector<bool> instead of a BopSolution internally.
123 learned_info->solution = assignment_iterator_->LastReferenceAssignment();
125 }
126
127 if (time_limit->LimitReached()) {
128 // The time limit is reached without finding a solution.
130 }
131
132 if (num_assignments_to_explore <= 0) {
133 // Explore the remaining assignments in a future call to Optimize().
135 }
136
137 // All assignments reachable in max_num_decisions_ or less have been explored,
138 // don't call optimize() with the same initial solution again.
140}
141
142//------------------------------------------------------------------------------
143// BacktrackableIntegerSet
144//------------------------------------------------------------------------------
145
146template <typename IntType>
148 size_ = 0;
149 saved_sizes_.clear();
150 saved_stack_sizes_.clear();
151 stack_.clear();
152 in_stack_.assign(n.value(), false);
153}
154
155template <typename IntType>
157 bool should_be_inside) {
158 size_ += should_be_inside ? 1 : -1;
159 if (!in_stack_[i.value()]) {
160 in_stack_[i.value()] = true;
161 stack_.push_back(i);
162 }
163}
164
165template <typename IntType>
167 saved_stack_sizes_.push_back(stack_.size());
168 saved_sizes_.push_back(size_);
169}
171template <typename IntType>
173 if (saved_stack_sizes_.empty()) {
174 BacktrackAll();
175 } else {
176 for (int i = saved_stack_sizes_.back(); i < stack_.size(); ++i) {
177 in_stack_[stack_[i].value()] = false;
178 }
179 stack_.resize(saved_stack_sizes_.back());
180 saved_stack_sizes_.pop_back();
181 size_ = saved_sizes_.back();
182 saved_sizes_.pop_back();
183 }
185
186template <typename IntType>
188 for (int i = 0; i < stack_.size(); ++i) {
189 in_stack_[stack_[i].value()] = false;
190 }
191 stack_.clear();
192 saved_stack_sizes_.clear();
193 size_ = 0;
194 saved_sizes_.clear();
195}
196
197// Explicit instantiation of BacktrackableIntegerSet.
198// TODO(user): move the code in a separate .h and -inl.h to avoid this.
200
201//------------------------------------------------------------------------------
202// AssignmentAndConstraintFeasibilityMaintainer
203//------------------------------------------------------------------------------
204
207 const LinearBooleanProblem& problem, absl::BitGenRef random)
208 : by_variable_matrix_(problem.num_variables()),
209 constraint_lower_bounds_(),
210 constraint_upper_bounds_(),
211 assignment_(problem, "Assignment"),
212 reference_(problem, "Assignment"),
213 constraint_values_(),
214 flipped_var_trail_backtrack_levels_(),
215 flipped_var_trail_(),
216 constraint_set_hasher_(random) {
217 // Add the objective constraint as the first constraint.
218 const LinearObjective& objective = problem.objective();
219 CHECK_EQ(objective.literals_size(), objective.coefficients_size());
220 for (int i = 0; i < objective.literals_size(); ++i) {
221 CHECK_GT(objective.literals(i), 0);
222 CHECK_NE(objective.coefficients(i), 0);
223
224 const VariableIndex var(objective.literals(i) - 1);
225 const int64_t weight = objective.coefficients(i);
226 by_variable_matrix_[var].push_back(
227 ConstraintEntry(kObjectiveConstraint, weight));
228 }
229 constraint_lower_bounds_.push_back(std::numeric_limits<int64_t>::min());
230 constraint_values_.push_back(0);
231 constraint_upper_bounds_.push_back(std::numeric_limits<int64_t>::max());
232
233 // Add each constraint.
234 ConstraintIndex num_constraints_with_objective(1);
235 for (const LinearBooleanConstraint& constraint : problem.constraints()) {
236 if (constraint.literals_size() <= 2) {
237 // Infeasible binary constraints are automatically repaired by propagation
238 // (when possible). Then there are no needs to consider the binary
239 // constraints here, the propagation is delegated to the SAT propagator.
240 continue;
241 }
242
243 CHECK_EQ(constraint.literals_size(), constraint.coefficients_size());
244 for (int i = 0; i < constraint.literals_size(); ++i) {
245 const VariableIndex var(constraint.literals(i) - 1);
246 const int64_t weight = constraint.coefficients(i);
247 by_variable_matrix_[var].push_back(
248 ConstraintEntry(num_constraints_with_objective, weight));
249 }
250 constraint_lower_bounds_.push_back(
251 constraint.has_lower_bound() ? constraint.lower_bound()
252 : std::numeric_limits<int64_t>::min());
253 constraint_values_.push_back(0);
254 constraint_upper_bounds_.push_back(
255 constraint.has_upper_bound() ? constraint.upper_bound()
256 : std::numeric_limits<int64_t>::max());
257
258 ++num_constraints_with_objective;
259 }
260
261 // Initialize infeasible_constraint_set_;
262 infeasible_constraint_set_.ClearAndResize(
263 ConstraintIndex(constraint_values_.size()));
264
265 CHECK_EQ(constraint_values_.size(), constraint_lower_bounds_.size());
266 CHECK_EQ(constraint_values_.size(), constraint_upper_bounds_.size());
267}
268
269const ConstraintIndex
271
273 const BopSolution& reference_solution) {
274 CHECK(reference_solution.IsFeasible());
275 infeasible_constraint_set_.BacktrackAll();
276
277 assignment_ = reference_solution;
278 reference_ = assignment_;
279 flipped_var_trail_backtrack_levels_.clear();
280 flipped_var_trail_.clear();
281 AddBacktrackingLevel(); // To handle initial propagation.
282
283 // Recompute the value of all constraints.
284 constraint_values_.assign(NumConstraints(), 0);
285 for (VariableIndex var(0); var < assignment_.Size(); ++var) {
286 if (assignment_.Value(var)) {
287 for (const ConstraintEntry& entry : by_variable_matrix_[var]) {
288 constraint_values_[entry.constraint] += entry.weight;
289 }
290 }
291 }
292
293 MakeObjectiveConstraintInfeasible(1);
294}
295
298 for (const VariableIndex var : flipped_var_trail_) {
299 reference_.SetValue(var, assignment_.Value(var));
300 }
301 flipped_var_trail_.clear();
302 flipped_var_trail_backtrack_levels_.clear();
303 AddBacktrackingLevel(); // To handle initial propagation.
304 MakeObjectiveConstraintInfeasible(1);
305}
306
307void AssignmentAndConstraintFeasibilityMaintainer::
308 MakeObjectiveConstraintInfeasible(int delta) {
309 CHECK(IsFeasible());
310 CHECK(flipped_var_trail_.empty());
311 constraint_upper_bounds_[kObjectiveConstraint] =
312 constraint_values_[kObjectiveConstraint] - delta;
313 infeasible_constraint_set_.BacktrackAll();
314 infeasible_constraint_set_.ChangeState(kObjectiveConstraint, true);
315 infeasible_constraint_set_.AddBacktrackingLevel();
317 CHECK(!IsFeasible());
318 if (DEBUG_MODE) {
319 for (ConstraintIndex ct(1); ct < NumConstraints(); ++ct) {
320 CHECK(ConstraintIsFeasible(ct));
321 }
322 }
323}
324
326 absl::Span<const sat::Literal> literals) {
327 for (const sat::Literal& literal : literals) {
328 const VariableIndex var(literal.Variable().value());
329 const bool value = literal.IsPositive();
330 if (assignment_.Value(var) != value) {
331 flipped_var_trail_.push_back(var);
332 assignment_.SetValue(var, value);
333 for (const ConstraintEntry& entry : by_variable_matrix_[var]) {
334 const bool was_feasible = ConstraintIsFeasible(entry.constraint);
335 constraint_values_[entry.constraint] +=
336 value ? entry.weight : -entry.weight;
337 if (ConstraintIsFeasible(entry.constraint) != was_feasible) {
338 infeasible_constraint_set_.ChangeState(entry.constraint,
339 was_feasible);
340 }
341 }
342 }
343 }
344}
345
347 flipped_var_trail_backtrack_levels_.push_back(flipped_var_trail_.size());
348 infeasible_constraint_set_.AddBacktrackingLevel();
349}
350
352 // Backtrack each literal of the last level.
353 for (int i = flipped_var_trail_backtrack_levels_.back();
354 i < flipped_var_trail_.size(); ++i) {
355 const VariableIndex var(flipped_var_trail_[i]);
356 const bool new_value = !assignment_.Value(var);
357 DCHECK_EQ(new_value, reference_.Value(var));
358 assignment_.SetValue(var, new_value);
359 for (const ConstraintEntry& entry : by_variable_matrix_[var]) {
360 constraint_values_[entry.constraint] +=
361 new_value ? entry.weight : -entry.weight;
362 }
363 }
364 flipped_var_trail_.resize(flipped_var_trail_backtrack_levels_.back());
365 flipped_var_trail_backtrack_levels_.pop_back();
366 infeasible_constraint_set_.BacktrackOneLevel();
367}
368
370 while (!flipped_var_trail_backtrack_levels_.empty()) BacktrackOneLevel();
371}
372
373const std::vector<sat::Literal>&
375 if (!constraint_set_hasher_.IsInitialized()) {
376 InitializeConstraintSetHasher();
377 }
378
379 // First, we compute the hash that a Literal should have in order to repair
380 // all the infeasible constraint (ignoring the objective).
381 //
382 // TODO(user): If this starts to show-up in a performance profile, we can
383 // easily maintain this hash incrementally.
384 uint64_t hash = 0;
385 for (const ConstraintIndex ci : PossiblyInfeasibleConstraints()) {
386 const int64_t value = ConstraintValue(ci);
387 if (value > ConstraintUpperBound(ci)) {
388 hash ^= constraint_set_hasher_.Hash(FromConstraintIndex(ci, false));
389 } else if (value < ConstraintLowerBound(ci)) {
390 hash ^= constraint_set_hasher_.Hash(FromConstraintIndex(ci, true));
391 }
392 }
393
394 tmp_potential_repairs_.clear();
395 const auto it = hash_to_potential_repairs_.find(hash);
396 if (it != hash_to_potential_repairs_.end()) {
397 for (const sat::Literal literal : it->second) {
398 // We only returns the flips.
399 if (assignment_.Value(VariableIndex(literal.Variable().value())) !=
400 literal.IsPositive()) {
401 tmp_potential_repairs_.push_back(literal);
402 }
403 }
404 }
405 return tmp_potential_repairs_;
406}
407
409 std::string str;
410 str += "curr: ";
411 for (bool value : assignment_) {
412 str += value ? " 1 " : " 0 ";
413 }
414 str += "\nFlipped variables: ";
415 // TODO(user): show the backtrack levels.
416 for (const VariableIndex var : flipped_var_trail_) {
417 str += absl::StrFormat(" %d", var.value());
418 }
419 str += "\nmin curr max\n";
420 for (ConstraintIndex ct(0); ct < constraint_values_.size(); ++ct) {
421 if (constraint_lower_bounds_[ct] == std::numeric_limits<int64_t>::min()) {
422 str += absl::StrFormat("- %d %d\n", constraint_values_[ct],
423 constraint_upper_bounds_[ct]);
424 } else {
425 str +=
426 absl::StrFormat("%d %d %d\n", constraint_lower_bounds_[ct],
427 constraint_values_[ct], constraint_upper_bounds_[ct]);
428 }
429 }
430 return str;
431}
432
433void AssignmentAndConstraintFeasibilityMaintainer::
434 InitializeConstraintSetHasher() {
435 const int num_constraints_with_objective = constraint_upper_bounds_.size();
436
437 // Initialize the potential one flip repair. Note that we ignore the
438 // objective constraint completely so that we consider a repair even if the
439 // objective constraint is not infeasible.
440 constraint_set_hasher_.Initialize(2 * num_constraints_with_objective);
441 constraint_set_hasher_.IgnoreElement(
442 FromConstraintIndex(kObjectiveConstraint, true));
443 constraint_set_hasher_.IgnoreElement(
444 FromConstraintIndex(kObjectiveConstraint, false));
445 for (VariableIndex var(0); var < by_variable_matrix_.size(); ++var) {
446 // We add two entries, one for a positive flip (from false to true) and one
447 // for a negative flip (from true to false).
448 for (const bool flip_is_positive : {true, false}) {
449 uint64_t hash = 0;
450 for (const ConstraintEntry& entry : by_variable_matrix_[var]) {
451 const bool coeff_is_positive = entry.weight > 0;
452 hash ^= constraint_set_hasher_.Hash(FromConstraintIndex(
453 entry.constraint,
454 /*up=*/flip_is_positive ? coeff_is_positive : !coeff_is_positive));
455 }
456 hash_to_potential_repairs_[hash].push_back(
457 sat::Literal(sat::BooleanVariable(var.value()), flip_is_positive));
458 }
459 }
460}
461
462//------------------------------------------------------------------------------
463// OneFlipConstraintRepairer
464//------------------------------------------------------------------------------
465
467 const LinearBooleanProblem& problem,
468 const AssignmentAndConstraintFeasibilityMaintainer& maintainer,
469 const sat::VariablesAssignment& sat_assignment)
470 : by_constraint_matrix_(problem.constraints_size() + 1),
471 maintainer_(maintainer),
472 sat_assignment_(sat_assignment) {
473 // Fill the by_constraint_matrix_.
474 //
475 // IMPORTANT: The order of the constraint needs to exactly match the one of
476 // the constraint in the AssignmentAndConstraintFeasibilityMaintainer.
477
478 // Add the objective constraint as the first constraint.
479 ConstraintIndex num_constraint(0);
480 const LinearObjective& objective = problem.objective();
481 CHECK_EQ(objective.literals_size(), objective.coefficients_size());
482 for (int i = 0; i < objective.literals_size(); ++i) {
483 CHECK_GT(objective.literals(i), 0);
484 CHECK_NE(objective.coefficients(i), 0);
485
486 const VariableIndex var(objective.literals(i) - 1);
487 const int64_t weight = objective.coefficients(i);
488 by_constraint_matrix_[num_constraint].push_back(
489 ConstraintTerm(var, weight));
490 }
491
492 // Add the non-binary problem constraints.
493 for (const LinearBooleanConstraint& constraint : problem.constraints()) {
494 if (constraint.literals_size() <= 2) {
495 // Infeasible binary constraints are automatically repaired by propagation
496 // (when possible). Then there are no needs to consider the binary
497 // constraints here, the propagation is delegated to the SAT propagator.
498 continue;
499 }
500
501 ++num_constraint;
502 CHECK_EQ(constraint.literals_size(), constraint.coefficients_size());
503 for (int i = 0; i < constraint.literals_size(); ++i) {
504 const VariableIndex var(constraint.literals(i) - 1);
505 const int64_t weight = constraint.coefficients(i);
506 by_constraint_matrix_[num_constraint].push_back(
507 ConstraintTerm(var, weight));
508 }
509 }
510
511 SortTermsOfEachConstraints(problem.num_variables());
512}
513
514const ConstraintIndex OneFlipConstraintRepairer::kInvalidConstraint(-1);
515const TermIndex OneFlipConstraintRepairer::kInitTerm(-1);
517
518ConstraintIndex OneFlipConstraintRepairer::ConstraintToRepair() const {
519 ConstraintIndex selected_ct = kInvalidConstraint;
520 int32_t selected_num_branches = std::numeric_limits<int32_t>::max();
521 int num_infeasible_constraints_left = maintainer_.NumInfeasibleConstraints();
522
523 // Optimization: We inspect the constraints in reverse order because the
524 // objective one will always be first (in our current code) and with some
525 // luck, we will break early instead of fully exploring it.
526 const std::vector<ConstraintIndex>& infeasible_constraints =
527 maintainer_.PossiblyInfeasibleConstraints();
528 for (int index = infeasible_constraints.size() - 1; index >= 0; --index) {
529 const ConstraintIndex& i = infeasible_constraints[index];
530 if (maintainer_.ConstraintIsFeasible(i)) continue;
531 --num_infeasible_constraints_left;
532
533 // Optimization: We return the only candidate without inspecting it.
534 // This is critical at the beginning of the search or later if the only
535 // candidate is the objective constraint which can be really long.
536 if (num_infeasible_constraints_left == 0 &&
537 selected_ct == kInvalidConstraint) {
538 return i;
539 }
540
541 const int64_t constraint_value = maintainer_.ConstraintValue(i);
542 const int64_t lb = maintainer_.ConstraintLowerBound(i);
543 const int64_t ub = maintainer_.ConstraintUpperBound(i);
544
545 int32_t num_branches = 0;
546 for (const ConstraintTerm& term : by_constraint_matrix_[i]) {
547 if (sat_assignment_.VariableIsAssigned(
548 sat::BooleanVariable(term.var.value()))) {
549 continue;
550 }
551 const int64_t new_value =
552 constraint_value +
553 (maintainer_.Assignment(term.var) ? -term.weight : term.weight);
554 if (new_value >= lb && new_value <= ub) {
555 ++num_branches;
556 if (num_branches >= selected_num_branches) break;
557 }
558 }
559
560 // The constraint can't be repaired in one decision.
561 if (num_branches == 0) continue;
562 if (num_branches < selected_num_branches) {
563 selected_ct = i;
564 selected_num_branches = num_branches;
565 if (num_branches == 1) break;
566 }
567 }
568 return selected_ct;
569}
570
572 ConstraintIndex ct_index, TermIndex init_term_index,
573 TermIndex start_term_index) const {
575 by_constraint_matrix_[ct_index];
576 const int64_t constraint_value = maintainer_.ConstraintValue(ct_index);
577 const int64_t lb = maintainer_.ConstraintLowerBound(ct_index);
578 const int64_t ub = maintainer_.ConstraintUpperBound(ct_index);
580 const TermIndex end_term_index(terms.size() + init_term_index + 1);
581 for (TermIndex loop_term_index(
582 start_term_index + 1 +
583 (start_term_index < init_term_index ? terms.size() : 0));
584 loop_term_index < end_term_index; ++loop_term_index) {
585 const TermIndex term_index(loop_term_index % terms.size());
586 const ConstraintTerm term = terms[term_index];
587 if (sat_assignment_.VariableIsAssigned(
588 sat::BooleanVariable(term.var.value()))) {
589 continue;
590 }
591 const int64_t new_value =
592 constraint_value +
593 (maintainer_.Assignment(term.var) ? -term.weight : term.weight);
594 if (new_value >= lb && new_value <= ub) {
595 return term_index;
596 }
597 }
598 return kInvalidTerm;
599}
600
602 TermIndex term_index) const {
603 if (maintainer_.ConstraintIsFeasible(ct_index)) return false;
604 const ConstraintTerm term = by_constraint_matrix_[ct_index][term_index];
605 if (sat_assignment_.VariableIsAssigned(
606 sat::BooleanVariable(term.var.value()))) {
607 return false;
608 }
609 const int64_t new_value =
610 maintainer_.ConstraintValue(ct_index) +
611 (maintainer_.Assignment(term.var) ? -term.weight : term.weight);
612
613 const int64_t lb = maintainer_.ConstraintLowerBound(ct_index);
614 const int64_t ub = maintainer_.ConstraintUpperBound(ct_index);
615 return (new_value >= lb && new_value <= ub);
616}
617
619 TermIndex term_index) const {
620 const ConstraintTerm term = by_constraint_matrix_[ct_index][term_index];
621 const bool value = maintainer_.Assignment(term.var);
622 return sat::Literal(sat::BooleanVariable(term.var.value()), !value);
623}
624
625void OneFlipConstraintRepairer::SortTermsOfEachConstraints(int num_variables) {
627 for (const ConstraintTerm& term :
629 kObjectiveConstraint]) {
630 objective[term.var] = std::abs(term.weight);
631 }
633 by_constraint_matrix_) {
634 std::sort(terms.begin(), terms.end(),
635 [&objective](const ConstraintTerm& a, const ConstraintTerm& b) {
636 return objective[a.var] > objective[b.var];
637 });
638 }
639}
640
641//------------------------------------------------------------------------------
642// SatWrapper
643//------------------------------------------------------------------------------
644
645SatWrapper::SatWrapper(sat::SatSolver* sat_solver) : sat_solver_(sat_solver) {}
646
647void SatWrapper::BacktrackAll() { sat_solver_->Backtrack(0); }
648
649std::vector<sat::Literal> SatWrapper::FullSatTrail() const {
650 std::vector<sat::Literal> propagated_literals;
651 const sat::Trail& trail = sat_solver_->LiteralTrail();
652 for (int trail_index = 0; trail_index < trail.Index(); ++trail_index) {
653 propagated_literals.push_back(trail[trail_index]);
654 }
655 return propagated_literals;
656}
658int SatWrapper::ApplyDecision(sat::Literal decision_literal,
659 std::vector<sat::Literal>* propagated_literals) {
660 CHECK(!sat_solver_->Assignment().VariableIsAssigned(
661 decision_literal.Variable()));
662 CHECK(propagated_literals != nullptr);
663
664 propagated_literals->clear();
665 const int old_decision_level = sat_solver_->CurrentDecisionLevel();
666 const int new_trail_index =
667 sat_solver_->EnqueueDecisionAndBackjumpOnConflict(decision_literal);
668 if (sat_solver_->IsModelUnsat()) {
669 return old_decision_level + 1;
670 }
671
672 // Return the propagated literals, whenever there is a conflict or not.
673 // In case of conflict, these literals will have to be added to the last
674 // decision point after backtrack.
675 const sat::Trail& propagation_trail = sat_solver_->LiteralTrail();
676 for (int trail_index = new_trail_index;
677 trail_index < propagation_trail.Index(); ++trail_index) {
678 propagated_literals->push_back(propagation_trail[trail_index]);
679 }
680
681 return old_decision_level + 1 - sat_solver_->CurrentDecisionLevel();
682}
683
685 const int old_decision_level = sat_solver_->CurrentDecisionLevel();
686 if (old_decision_level > 0) {
687 sat_solver_->Backtrack(old_decision_level - 1);
688 }
689}
690
691void SatWrapper::ExtractLearnedInfo(LearnedInfo* info) {
692 bop::ExtractLearnedInfoFromSatSolver(sat_solver_, info);
693}
695double SatWrapper::deterministic_time() const {
696 return sat_solver_->deterministic_time();
697}
698
699//------------------------------------------------------------------------------
700// LocalSearchAssignmentIterator
701//------------------------------------------------------------------------------
702
704 const ProblemState& problem_state, int max_num_decisions,
705 int max_num_broken_constraints, absl::BitGenRef random,
706 SatWrapper* sat_wrapper)
707 : max_num_decisions_(max_num_decisions),
708 max_num_broken_constraints_(max_num_broken_constraints),
709 maintainer_(problem_state.original_problem(), random),
710 sat_wrapper_(sat_wrapper),
711 repairer_(problem_state.original_problem(), maintainer_,
712 sat_wrapper->SatAssignment()),
713 search_nodes_(),
714 initial_term_index_(
715 problem_state.original_problem().constraints_size() + 1,
716 OneFlipConstraintRepairer::kInitTerm),
717 use_transposition_table_(false),
718 use_potential_one_flip_repairs_(false),
719 num_nodes_(0),
720 num_skipped_nodes_(0),
721 num_improvements_(0),
722 num_improvements_by_one_flip_repairs_(0),
723 num_inspected_one_flip_repairs_(0) {}
724
726 VLOG(1) << "LS " << max_num_decisions_
727 << "\n num improvements: " << num_improvements_
728 << "\n num improvements with one flip repairs: "
729 << num_improvements_by_one_flip_repairs_
730 << "\n num inspected one flip repairs: "
731 << num_inspected_one_flip_repairs_;
732}
733
735 const ProblemState& problem_state) {
736 better_solution_has_been_found_ = false;
737 maintainer_.SetReferenceSolution(problem_state.solution());
738 for (const SearchNode& node : search_nodes_) {
739 initial_term_index_[node.constraint] = node.term_index;
740 }
741 search_nodes_.clear();
742 transposition_table_.clear();
743 num_nodes_ = 0;
744 num_skipped_nodes_ = 0;
745}
747// In order to restore the synchronization from any state, we backtrack
748// everything and retry to take the same decisions as before. We stop at the
749// first one that can't be taken.
751 CHECK_EQ(better_solution_has_been_found_, false);
752 const std::vector<SearchNode> copy = search_nodes_;
753 sat_wrapper_->BacktrackAll();
754 maintainer_.BacktrackAll();
755
756 // Note(user): at this stage, the sat trail contains the fixed variables.
757 // There will almost always be at the same value in the reference solution.
758 // However since the objective may be over-constrained in the sat_solver, it
759 // is possible that some variable where propagated to some other values.
760 maintainer_.Assign(sat_wrapper_->FullSatTrail());
761
762 search_nodes_.clear();
763 for (const SearchNode& node : copy) {
764 if (!repairer_.RepairIsValid(node.constraint, node.term_index)) break;
765 search_nodes_.push_back(node);
766 ApplyDecision(repairer_.GetFlip(node.constraint, node.term_index));
767 }
768}
769
770void LocalSearchAssignmentIterator::UseCurrentStateAsReference() {
771 better_solution_has_been_found_ = true;
772 maintainer_.UseCurrentStateAsReference();
773 sat_wrapper_->BacktrackAll();
774
775 // Note(user): Here, there should be no discrepancies between the fixed
776 // variable and the new reference, so there is no need to do:
777 // maintainer_.Assign(sat_wrapper_->FullSatTrail());
778
779 for (const SearchNode& node : search_nodes_) {
780 initial_term_index_[node.constraint] = node.term_index;
781 }
782 search_nodes_.clear();
783 transposition_table_.clear();
784 num_nodes_ = 0;
785 num_skipped_nodes_ = 0;
786 ++num_improvements_;
787}
788
790 if (sat_wrapper_->IsModelUnsat()) return false;
791 if (maintainer_.IsFeasible()) {
792 UseCurrentStateAsReference();
793 return true;
794 }
795
796 // We only look for potential one flip repairs if we reached the end of the
797 // LS tree. I tried to do that at every level, but it didn't change the
798 // result much on the set-partitionning example I was using.
799 //
800 // TODO(user): Perform more experiments with this.
801 if (use_potential_one_flip_repairs_ &&
802 search_nodes_.size() == max_num_decisions_) {
803 for (const sat::Literal literal : maintainer_.PotentialOneFlipRepairs()) {
804 if (sat_wrapper_->SatAssignment().VariableIsAssigned(
805 literal.Variable())) {
806 continue;
807 }
808 ++num_inspected_one_flip_repairs_;
809
810 // Temporarily apply the potential repair and see if it worked!
811 ApplyDecision(literal);
812 if (maintainer_.IsFeasible()) {
813 num_improvements_by_one_flip_repairs_++;
814 UseCurrentStateAsReference();
815 return true;
816 }
817 maintainer_.BacktrackOneLevel();
818 sat_wrapper_->BacktrackOneLevel();
819 }
820 }
821
822 // If possible, go deeper, i.e. take one more decision.
823 if (!GoDeeper()) {
824 // If not, backtrack to the first node that still has untried way to fix
825 // its associated constraint. Update it to the next untried way.
826 Backtrack();
827 }
828
829 // All nodes have been explored.
830 if (search_nodes_.empty()) {
831 VLOG(1) << std::string(27, ' ') + "LS " << max_num_decisions_
832 << " finished." << " #explored:" << num_nodes_
833 << " #stored:" << transposition_table_.size()
834 << " #skipped:" << num_skipped_nodes_;
835 return false;
836 }
837
838 // Apply the next decision, i.e. the literal of the flipped variable.
839 const SearchNode node = search_nodes_.back();
840 ApplyDecision(repairer_.GetFlip(node.constraint, node.term_index));
841 return true;
842}
843
844// TODO(user): The 1.2 multiplier is an approximation only based on the time
845// spent in the SAT wrapper. So far experiments show a good
846// correlation with real time, but we might want to be more
847// accurate.
849 return sat_wrapper_->deterministic_time() * 1.2;
850}
851
853 std::string str = "Search nodes:\n";
854 for (int i = 0; i < search_nodes_.size(); ++i) {
855 str += absl::StrFormat(" %d: %d %d\n", i,
856 search_nodes_[i].constraint.value(),
857 search_nodes_[i].term_index.value());
858 }
859 return str;
861
862void LocalSearchAssignmentIterator::ApplyDecision(sat::Literal literal) {
863 ++num_nodes_;
864 const int num_backtracks =
865 sat_wrapper_->ApplyDecision(literal, &tmp_propagated_literals_);
866
867 // Sync the maintainer with SAT.
868 if (num_backtracks == 0) {
869 maintainer_.AddBacktrackingLevel();
870 maintainer_.Assign(tmp_propagated_literals_);
871 } else {
872 CHECK_GT(num_backtracks, 0);
873 CHECK_LE(num_backtracks, search_nodes_.size());
874
875 // Only backtrack -1 decisions as the last one has not been pushed yet.
876 for (int i = 0; i < num_backtracks - 1; ++i) {
877 maintainer_.BacktrackOneLevel();
878 }
879 maintainer_.Assign(tmp_propagated_literals_);
880 search_nodes_.resize(search_nodes_.size() - num_backtracks);
881 }
882}
883
884void LocalSearchAssignmentIterator::InitializeTranspositionTableKey(
885 std::array<int32_t, kStoredMaxDecisions>* a) {
886 int i = 0;
887 for (const SearchNode& n : search_nodes_) {
888 // Negated because we already fliped this variable, so GetFlip() will
889 // returns the old value.
890 (*a)[i] = -repairer_.GetFlip(n.constraint, n.term_index).SignedValue();
891 ++i;
892 }
893
894 // 'a' is not zero-initialized, so we need to complete it with zeros.
895 while (i < kStoredMaxDecisions) {
896 (*a)[i] = 0;
897 ++i;
898 }
899}
900
901bool LocalSearchAssignmentIterator::NewStateIsInTranspositionTable(
902 sat::Literal l) {
903 if (search_nodes_.size() + 1 > kStoredMaxDecisions) return false;
904
905 // Fill the transposition table element, i.e. the array 'a' of decisions.
906 std::array<int32_t, kStoredMaxDecisions> a;
907 InitializeTranspositionTableKey(&a);
908 a[search_nodes_.size()] = l.SignedValue();
909 std::sort(a.begin(), a.begin() + 1 + search_nodes_.size());
910
911 if (transposition_table_.find(a) == transposition_table_.end()) {
912 return false;
913 } else {
914 ++num_skipped_nodes_;
915 return true;
916 }
917}
918
919void LocalSearchAssignmentIterator::InsertInTranspositionTable() {
920 // If there is more decision that kStoredMaxDecisions, do nothing.
921 if (search_nodes_.size() > kStoredMaxDecisions) return;
922
923 // Fill the transposition table element, i.e. the array 'a' of decisions.
924 std::array<int32_t, kStoredMaxDecisions> a;
925 InitializeTranspositionTableKey(&a);
926 std::sort(a.begin(), a.begin() + search_nodes_.size());
927
928 transposition_table_.insert(a);
929}
930
931bool LocalSearchAssignmentIterator::EnqueueNextRepairingTermIfAny(
932 ConstraintIndex ct_to_repair, TermIndex term_index) {
933 if (term_index == initial_term_index_[ct_to_repair]) return false;
934 if (term_index == OneFlipConstraintRepairer::kInvalidTerm) {
935 term_index = initial_term_index_[ct_to_repair];
936 }
937 while (true) {
938 term_index = repairer_.NextRepairingTerm(
939 ct_to_repair, initial_term_index_[ct_to_repair], term_index);
940 if (term_index == OneFlipConstraintRepairer::kInvalidTerm) return false;
941 if (!use_transposition_table_ ||
942 !NewStateIsInTranspositionTable(
943 repairer_.GetFlip(ct_to_repair, term_index))) {
944 search_nodes_.push_back(SearchNode(ct_to_repair, term_index));
945 return true;
946 }
947 if (term_index == initial_term_index_[ct_to_repair]) return false;
948 }
949}
950
951bool LocalSearchAssignmentIterator::GoDeeper() {
952 // Can we add one more decision?
953 if (search_nodes_.size() >= max_num_decisions_) {
954 return false;
955 }
956
957 // Is the number of infeasible constraints reasonable?
958 //
959 // TODO(user): Make this parameters dynamic. We can either try lower value
960 // first and increase it later, or try to dynamically change it during the
961 // search. Another idea is to have instead a "max number of constraints that
962 // can be repaired in one decision" and to take into account the number of
963 // decisions left.
964 if (maintainer_.NumInfeasibleConstraints() > max_num_broken_constraints_) {
965 return false;
966 }
967
968 // Can we find a constraint that can be repaired in one decision?
969 const ConstraintIndex ct_to_repair = repairer_.ConstraintToRepair();
971 return false;
972 }
973
974 // Add the new decision.
975 //
976 // TODO(user): Store the last explored term index to not start from -1 each
977 // time. This will be very useful when a backtrack occurred due to the SAT
978 // propagator. Note however that this behavior is already enforced when we use
979 // the transposition table, since we will not explore again the branches
980 // already explored.
981 return EnqueueNextRepairingTermIfAny(ct_to_repair,
983}
984
985void LocalSearchAssignmentIterator::Backtrack() {
986 while (!search_nodes_.empty()) {
987 // We finished exploring this node. Store it in the transposition table so
988 // that the same decisions will not be explored again. Note that the SAT
989 // solver may have learned more the second time the exact same decisions are
990 // seen, but we assume that it is not worth exploring again.
991 if (use_transposition_table_) InsertInTranspositionTable();
992
993 const SearchNode last_node = search_nodes_.back();
994 search_nodes_.pop_back();
995 maintainer_.BacktrackOneLevel();
996 sat_wrapper_->BacktrackOneLevel();
997 if (EnqueueNextRepairingTermIfAny(last_node.constraint,
998 last_node.term_index)) {
999 return;
1000 }
1001 }
1002}
1003
1004} // namespace bop
1005} // namespace operations_research
void SetReferenceSolution(const BopSolution &reference_solution)
Definition bop_ls.cc:278
int64_t ConstraintUpperBound(ConstraintIndex constraint) const
Returns the upper bound of the constraint.
Definition bop_ls.h:371
void Assign(absl::Span< const sat::Literal > literals)
Definition bop_ls.cc:331
int64_t ConstraintValue(ConstraintIndex constraint) const
Definition bop_ls.h:378
const std::vector< ConstraintIndex > & PossiblyInfeasibleConstraints() const
Returns a superset of all the infeasible constraints in the current state.
Definition bop_ls.h:348
int64_t ConstraintLowerBound(ConstraintIndex constraint) const
Returns the lower bound of the constraint.
Definition bop_ls.h:366
const std::vector< sat::Literal > & PotentialOneFlipRepairs()
Definition bop_ls.cc:380
AssignmentAndConstraintFeasibilityMaintainer(const sat::LinearBooleanProblem &problem, absl::BitGenRef random)
bool ConstraintIsFeasible(ConstraintIndex constraint) const
Returns true if the given constraint is currently feasible.
Definition bop_ls.h:383
bool IsFeasible() const
Returns true if there is no infeasible constraint in the current state.
Definition bop_ls.h:338
void ChangeState(IntType i, bool should_be_inside)
Definition bop_ls.cc:160
@ ABORT
There is no need to call this optimizer again on the same problem state.
Definition bop_base.h:87
bool Value(VariableIndex var) const
void SetValue(VariableIndex var, bool value)
LocalSearchAssignmentIterator(const ProblemState &problem_state, int max_num_decisions, int max_num_broken_constraints, absl::BitGenRef random, SatWrapper *sat_wrapper)
Definition bop_ls.cc:715
bool NextAssignment()
Move to the next assignment. Returns false when the search is finished.
Definition bop_ls.cc:801
void Synchronize(const ProblemState &problem_state)
Definition bop_ls.cc:746
LocalSearchOptimizer(absl::string_view name, int max_num_decisions, absl::BitGenRef random, sat::SatSolver *sat_propagator)
Definition bop_ls.cc:59
uint64_t Hash(const std::vector< IntType > &set) const
Definition bop_ls.h:240
bool IsInitialized() const
Returns true if Initialize() has been called with a non-zero size.
Definition bop_ls.h:251
void Initialize(int size)
Initializes the NonOrderedSetHasher to hash sets of integer in [0, n).
Definition bop_ls.h:226
OneFlipConstraintRepairer(const sat::LinearBooleanProblem &problem, const AssignmentAndConstraintFeasibilityMaintainer &maintainer, const sat::VariablesAssignment &sat_assignment)
static const ConstraintIndex kInvalidConstraint
Definition bop_ls.h:473
bool RepairIsValid(ConstraintIndex ct_index, TermIndex term_index) const
Definition bop_ls.cc:609
TermIndex NextRepairingTerm(ConstraintIndex ct_index, TermIndex init_term_index, TermIndex start_term_index) const
Definition bop_ls.cc:579
sat::Literal GetFlip(ConstraintIndex ct_index, TermIndex term_index) const
Definition bop_ls.cc:626
const BopSolution & solution() const
Definition bop_base.h:205
int ApplyDecision(sat::Literal decision_literal, std::vector< sat::Literal > *propagated_literals)
Definition bop_ls.cc:668
void ExtractLearnedInfo(LearnedInfo *info)
Extracts any new information learned during the search.
Definition bop_ls.cc:701
const sat::VariablesAssignment & SatAssignment() const
Return the current solver VariablesAssignment.
Definition bop_ls.h:82
SatWrapper(sat::SatSolver *sat_solver)
Definition bop_ls.cc:655
void BacktrackOneLevel()
Backtracks the last decision if any.
Definition bop_ls.cc:694
void BacktrackAll()
Bactracks all the decisions.
Definition bop_ls.cc:657
std::vector< sat::Literal > FullSatTrail() const
Returns the current state of the solver propagation trail.
Definition bop_ls.cc:659
BooleanVariable Variable() const
Definition sat_base.h:87
int EnqueueDecisionAndBackjumpOnConflict(Literal true_literal)
void Backtrack(int target_level)
const VariablesAssignment & Assignment() const
Definition sat_solver.h:388
const Trail & LiteralTrail() const
Definition sat_solver.h:387
bool VariableIsAssigned(BooleanVariable var) const
Returns true iff the given variable is assigned.
Definition sat_base.h:196
void assign(size_type n, const value_type &val)
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.
const Constraint * ct
int64_t value
IntVar * var
int literal
int ct_index
time_limit
Definition solve.cc:22
int index
const bool DEBUG_MODE
Definition macros.h:24
int64_t hash
void ExtractLearnedInfoFromSatSolver(sat::SatSolver *solver, LearnedInfo *info)
Definition bop_util.cc:109
In SWIG mode, we don't want anything besides these top-level includes.
int64_t weight
Definition pack.cc:510
int64_t delta
Definition resource.cc:1709