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