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