OR-Tools  9.6
boolean_problem.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 
15 
16 #include <algorithm>
17 #include <cmath>
18 #include <cstdint>
19 #include <cstdlib>
20 #include <limits>
21 #include <memory>
22 #include <numeric>
23 #include <string>
24 #include <utility>
25 #include <vector>
26 
27 #include "absl/container/flat_hash_map.h"
28 #include "absl/flags/flag.h"
29 #include "absl/status/status.h"
30 #include "absl/strings/str_format.h"
31 #include "absl/strings/string_view.h"
32 #include "ortools/base/logging.h"
33 #include "ortools/graph/graph.h"
34 #if !defined(__PORTABLE_PLATFORM__)
35 #include "ortools/graph/io.h"
36 #endif // __PORTABLE_PLATFORM__
40 #include "ortools/graph/util.h"
42 #include "ortools/sat/boolean_problem.pb.h"
43 #include "ortools/sat/cp_model.pb.h"
45 #include "ortools/sat/sat_base.h"
46 #include "ortools/sat/sat_parameters.pb.h"
47 #include "ortools/sat/sat_solver.h"
50 
51 ABSL_FLAG(std::string, debug_dump_symmetry_graph_to_file, "",
52  "If this flag is non-empty, an undirected graph whose"
53  " automorphism group is in one-to-one correspondence with the"
54  " symmetries of the SAT problem will be dumped to a file every"
55  " time FindLinearBooleanProblemSymmetries() is called.");
56 
57 namespace operations_research {
58 namespace sat {
59 
60 using util::RemapGraph;
61 
62 void ExtractAssignment(const LinearBooleanProblem& problem,
63  const SatSolver& solver, std::vector<bool>* assignment) {
64  assignment->clear();
65  for (int i = 0; i < problem.num_variables(); ++i) {
66  assignment->push_back(
67  solver.Assignment().LiteralIsTrue(Literal(BooleanVariable(i), true)));
68  }
69 }
70 
71 namespace {
72 
73 // Used by BooleanProblemIsValid() to test that there is no duplicate literals,
74 // that they are all within range and that there is no zero coefficient.
75 //
76 // A non-empty string indicates an error.
77 template <typename LinearTerms>
78 std::string ValidateLinearTerms(const LinearTerms& terms,
79  std::vector<bool>* variable_seen) {
80  // variable_seen already has all items false and is reset before return.
81  std::string err_str;
82  int num_errs = 0;
83  const int max_num_errs = 100;
84  for (int i = 0; i < terms.literals_size(); ++i) {
85  if (terms.literals(i) == 0) {
86  if (++num_errs <= max_num_errs) {
87  err_str += absl::StrFormat("Zero literal at position %d\n", i);
88  }
89  }
90  if (terms.coefficients(i) == 0) {
91  if (++num_errs <= max_num_errs) {
92  err_str += absl::StrFormat("Literal %d has a zero coefficient\n",
93  terms.literals(i));
94  }
95  }
96  const int var = Literal(terms.literals(i)).Variable().value();
97  if (var >= variable_seen->size()) {
98  if (++num_errs <= max_num_errs) {
99  err_str += absl::StrFormat("Out of bound variable %d\n", var);
100  }
101  }
102  if ((*variable_seen)[var]) {
103  if (++num_errs <= max_num_errs) {
104  err_str += absl::StrFormat("Duplicated variable %d\n", var);
105  }
106  }
107  (*variable_seen)[var] = true;
108  }
109 
110  for (int i = 0; i < terms.literals_size(); ++i) {
111  const int var = Literal(terms.literals(i)).Variable().value();
112  (*variable_seen)[var] = false;
113  }
114  if (num_errs) {
115  if (num_errs <= max_num_errs) {
116  err_str = absl::StrFormat("%d validation errors:\n", num_errs) + err_str;
117  } else {
118  err_str =
119  absl::StrFormat("%d validation errors; here are the first %d:\n",
120  num_errs, max_num_errs) +
121  err_str;
122  }
123  }
124  return err_str;
125 }
126 
127 // Converts a linear expression from the protocol buffer format to a vector
128 // of LiteralWithCoeff.
129 template <typename ProtoFormat>
130 std::vector<LiteralWithCoeff> ConvertLinearExpression(
131  const ProtoFormat& input) {
132  std::vector<LiteralWithCoeff> cst;
133  cst.reserve(input.literals_size());
134  for (int i = 0; i < input.literals_size(); ++i) {
135  const Literal literal(input.literals(i));
136  cst.push_back(LiteralWithCoeff(literal, input.coefficients(i)));
137  }
138  return cst;
139 }
140 
141 } // namespace
142 
143 absl::Status ValidateBooleanProblem(const LinearBooleanProblem& problem) {
144  std::vector<bool> variable_seen(problem.num_variables(), false);
145  for (int i = 0; i < problem.constraints_size(); ++i) {
146  const LinearBooleanConstraint& constraint = problem.constraints(i);
147  const std::string error = ValidateLinearTerms(constraint, &variable_seen);
148  if (!error.empty()) {
149  return absl::Status(
150  absl::StatusCode::kInvalidArgument,
151  absl::StrFormat("Invalid constraint %i: ", i) + error);
152  }
153  }
154  const std::string error =
155  ValidateLinearTerms(problem.objective(), &variable_seen);
156  if (!error.empty()) {
157  return absl::Status(absl::StatusCode::kInvalidArgument,
158  absl::StrFormat("Invalid objective: ") + error);
159  }
160  return ::absl::OkStatus();
161 }
162 
163 CpModelProto BooleanProblemToCpModelproto(const LinearBooleanProblem& problem) {
164  CpModelProto result;
165  for (int i = 0; i < problem.num_variables(); ++i) {
166  IntegerVariableProto* var = result.add_variables();
167  if (problem.var_names_size() > i) {
168  var->set_name(problem.var_names(i));
169  }
170  var->add_domain(0);
171  var->add_domain(1);
172  }
173  for (const LinearBooleanConstraint& constraint : problem.constraints()) {
174  ConstraintProto* ct = result.add_constraints();
175  ct->set_name(constraint.name());
176  LinearConstraintProto* linear = ct->mutable_linear();
177  int64_t offset = 0;
178  for (int i = 0; i < constraint.literals_size(); ++i) {
179  // Note that the new format is slightly different.
180  const int lit = constraint.literals(i);
181  const int64_t coeff = constraint.coefficients(i);
182  if (lit > 0) {
183  linear->add_vars(lit - 1);
184  linear->add_coeffs(coeff);
185  } else {
186  // The term was coeff * (1 - var).
187  linear->add_vars(-lit - 1);
188  linear->add_coeffs(-coeff);
189  offset -= coeff;
190  }
191  }
192  linear->add_domain(constraint.has_lower_bound()
193  ? constraint.lower_bound() + offset
194  : std::numeric_limits<int32_t>::min() + offset);
195  linear->add_domain(constraint.has_upper_bound()
196  ? constraint.upper_bound() + offset
197  : std::numeric_limits<int32_t>::max() + offset);
198  }
199  if (problem.has_objective()) {
200  CpObjectiveProto* objective = result.mutable_objective();
201  int64_t offset = 0;
202  for (int i = 0; i < problem.objective().literals_size(); ++i) {
203  const int lit = problem.objective().literals(i);
204  const int64_t coeff = problem.objective().coefficients(i);
205  if (lit > 0) {
206  objective->add_vars(lit - 1);
207  objective->add_coeffs(coeff);
208  } else {
209  objective->add_vars(-lit - 1);
210  objective->add_coeffs(-coeff);
211  offset -= coeff;
212  }
213  }
214  objective->set_offset(offset + problem.objective().offset());
215  objective->set_scaling_factor(problem.objective().scaling_factor());
216  }
217  return result;
218 }
219 
220 void ChangeOptimizationDirection(LinearBooleanProblem* problem) {
221  LinearObjective* objective = problem->mutable_objective();
222  objective->set_scaling_factor(-objective->scaling_factor());
223  objective->set_offset(-objective->offset());
224  // We need 'auto' here to keep the open-source compilation happy
225  // (it uses the public protobuf release).
226  for (auto& coefficients_ref : *objective->mutable_coefficients()) {
227  coefficients_ref = -coefficients_ref;
228  }
229 }
230 
231 bool LoadBooleanProblem(const LinearBooleanProblem& problem,
232  SatSolver* solver) {
233  // TODO(user): Currently, the sat solver can load without any issue
234  // constraints with duplicate variables, so we just output a warning if the
235  // problem is not "valid". Make this a strong check once we have some
236  // preprocessing step to remove duplicates variable in the constraints.
237  const absl::Status status = ValidateBooleanProblem(problem);
238  if (!status.ok()) {
239  LOG(WARNING) << "The given problem is invalid!";
240  }
241 
242  if (solver->parameters().log_search_progress()) {
243  LOG(INFO) << "Loading problem '" << problem.name() << "', "
244  << problem.num_variables() << " variables, "
245  << problem.constraints_size() << " constraints.";
246  }
247  solver->SetNumVariables(problem.num_variables());
248  std::vector<LiteralWithCoeff> cst;
249  int64_t num_terms = 0;
250  int num_constraints = 0;
251  for (const LinearBooleanConstraint& constraint : problem.constraints()) {
252  num_terms += constraint.literals_size();
253  cst = ConvertLinearExpression(constraint);
254  if (!solver->AddLinearConstraint(
255  constraint.has_lower_bound(), Coefficient(constraint.lower_bound()),
256  constraint.has_upper_bound(), Coefficient(constraint.upper_bound()),
257  &cst)) {
258  LOG(INFO) << "Problem detected to be UNSAT when "
259  << "adding the constraint #" << num_constraints
260  << " with name '" << constraint.name() << "'";
261  return false;
262  }
263  ++num_constraints;
264  }
265  if (solver->parameters().log_search_progress()) {
266  LOG(INFO) << "The problem contains " << num_terms << " terms.";
267  }
268  return true;
269 }
270 
271 bool LoadAndConsumeBooleanProblem(LinearBooleanProblem* problem,
272  SatSolver* solver) {
273  const absl::Status status = ValidateBooleanProblem(*problem);
274  if (!status.ok()) {
275  LOG(WARNING) << "The given problem is invalid! " << status.message();
276  }
277  if (solver->parameters().log_search_progress()) {
278 #if !defined(__PORTABLE_PLATFORM__)
279  LOG(INFO) << "LinearBooleanProblem memory: " << problem->SpaceUsedLong();
280 #endif
281  LOG(INFO) << "Loading problem '" << problem->name() << "', "
282  << problem->num_variables() << " variables, "
283  << problem->constraints_size() << " constraints.";
284  }
285  solver->SetNumVariables(problem->num_variables());
286  std::vector<LiteralWithCoeff> cst;
287  int64_t num_terms = 0;
288  int num_constraints = 0;
289 
290  // We will process the constraints backward so we can free the memory used by
291  // each constraint just after processing it. Because of that, we initially
292  // reverse all the constraints to add them in the same order.
293  std::reverse(problem->mutable_constraints()->begin(),
294  problem->mutable_constraints()->end());
295  for (int i = problem->constraints_size() - 1; i >= 0; --i) {
296  const LinearBooleanConstraint& constraint = problem->constraints(i);
297  num_terms += constraint.literals_size();
298  cst = ConvertLinearExpression(constraint);
299  if (!solver->AddLinearConstraint(
300  constraint.has_lower_bound(), Coefficient(constraint.lower_bound()),
301  constraint.has_upper_bound(), Coefficient(constraint.upper_bound()),
302  &cst)) {
303  LOG(INFO) << "Problem detected to be UNSAT when "
304  << "adding the constraint #" << num_constraints
305  << " with name '" << constraint.name() << "'";
306  return false;
307  }
308  delete problem->mutable_constraints()->ReleaseLast();
309  ++num_constraints;
310  }
311  LinearBooleanProblem empty_problem;
312  problem->mutable_constraints()->Swap(empty_problem.mutable_constraints());
313  if (solver->parameters().log_search_progress()) {
314  LOG(INFO) << "The problem contains " << num_terms << " terms.";
315  }
316  return true;
317 }
318 
319 void UseObjectiveForSatAssignmentPreference(const LinearBooleanProblem& problem,
320  SatSolver* solver) {
321  const LinearObjective& objective = problem.objective();
322  CHECK_EQ(objective.literals_size(), objective.coefficients_size());
323  int64_t max_abs_weight = 0;
324  for (const int64_t coefficient : objective.coefficients()) {
325  max_abs_weight = std::max(max_abs_weight, std::abs(coefficient));
326  }
327  const double max_abs_weight_double = max_abs_weight;
328  for (int i = 0; i < objective.literals_size(); ++i) {
329  const Literal literal(objective.literals(i));
330  const int64_t coefficient = objective.coefficients(i);
331  const double abs_weight = std::abs(coefficient) / max_abs_weight_double;
332  // Because this is a minimization problem, we prefer to assign a Boolean
333  // variable to its "low" objective value. So if a literal has a positive
334  // weight when true, we want to set it to false.
335  solver->SetAssignmentPreference(
336  coefficient > 0 ? literal.Negated() : literal, abs_weight);
337  }
338 }
339 
340 bool AddObjectiveUpperBound(const LinearBooleanProblem& problem,
341  Coefficient upper_bound, SatSolver* solver) {
342  std::vector<LiteralWithCoeff> cst =
343  ConvertLinearExpression(problem.objective());
344  return solver->AddLinearConstraint(false, Coefficient(0), true, upper_bound,
345  &cst);
346 }
347 
348 bool AddObjectiveConstraint(const LinearBooleanProblem& problem,
349  bool use_lower_bound, Coefficient lower_bound,
350  bool use_upper_bound, Coefficient upper_bound,
351  SatSolver* solver) {
352  std::vector<LiteralWithCoeff> cst =
353  ConvertLinearExpression(problem.objective());
354  return solver->AddLinearConstraint(use_lower_bound, lower_bound,
355  use_upper_bound, upper_bound, &cst);
356 }
357 
358 Coefficient ComputeObjectiveValue(const LinearBooleanProblem& problem,
359  const std::vector<bool>& assignment) {
360  CHECK_EQ(assignment.size(), problem.num_variables());
361  Coefficient sum(0);
362  const LinearObjective& objective = problem.objective();
363  for (int i = 0; i < objective.literals_size(); ++i) {
364  const Literal literal(objective.literals(i));
365  if (assignment[literal.Variable().value()] == literal.IsPositive()) {
366  sum += objective.coefficients(i);
367  }
368  }
369  return sum;
370 }
371 
372 bool IsAssignmentValid(const LinearBooleanProblem& problem,
373  const std::vector<bool>& assignment) {
374  CHECK_EQ(assignment.size(), problem.num_variables());
375 
376  // Check that all constraints are satisfied.
377  for (const LinearBooleanConstraint& constraint : problem.constraints()) {
378  Coefficient sum(0);
379  for (int i = 0; i < constraint.literals_size(); ++i) {
380  const Literal literal(constraint.literals(i));
381  if (assignment[literal.Variable().value()] == literal.IsPositive()) {
382  sum += constraint.coefficients(i);
383  }
384  }
385  if (constraint.has_lower_bound() && sum < constraint.lower_bound()) {
386  LOG(WARNING) << "Unsatisfied constraint! sum: " << sum << "\n"
387  << ProtobufDebugString(constraint);
388  return false;
389  }
390  if (constraint.has_upper_bound() && sum > constraint.upper_bound()) {
391  LOG(WARNING) << "Unsatisfied constraint! sum: " << sum << "\n"
392  << ProtobufDebugString(constraint);
393  return false;
394  }
395  }
396  return true;
397 }
398 
399 // Note(user): This function makes a few assumptions about the format of the
400 // given LinearBooleanProblem. All constraint coefficients must be 1 (and of the
401 // form >= 1) and all objective weights must be strictly positive.
403  const LinearBooleanProblem& problem) {
404  std::string output;
405  const bool is_wcnf = (problem.objective().coefficients_size() > 0);
406  const LinearObjective& objective = problem.objective();
407 
408  // Hack: We know that all the variables with index greater than this have been
409  // created "artificially" in order to encode a max-sat problem into our
410  // format. Each extra variable appear only once, and was used as a slack to
411  // reify a soft clause.
412  const int first_slack_variable = problem.original_num_variables();
413 
414  // This will contains the objective.
415  absl::flat_hash_map<int, int64_t> literal_to_weight;
416  std::vector<std::pair<int, int64_t>> non_slack_objective;
417 
418  // This will be the weight of the "hard" clauses in the wcnf format. It must
419  // be greater than the sum of the weight of all the soft clauses, so we will
420  // just set it to this sum + 1.
421  int64_t hard_weight = 1;
422  if (is_wcnf) {
423  int i = 0;
424  for (int64_t weight : objective.coefficients()) {
425  CHECK_NE(weight, 0);
426  int signed_literal = objective.literals(i);
427 
428  // There is no direct support for an objective offset in the wcnf format.
429  // So this is not a perfect translation of the objective. It is however
430  // possible to achieve the same effect by adding a new variable x, and two
431  // soft clauses: x with weight offset, and -x with weight offset.
432  //
433  // TODO(user): implement this trick.
434  if (weight < 0) {
435  signed_literal = -signed_literal;
436  weight = -weight;
437  }
438  literal_to_weight[objective.literals(i)] = weight;
439  if (Literal(signed_literal).Variable() < first_slack_variable) {
440  non_slack_objective.push_back(std::make_pair(signed_literal, weight));
441  }
442  hard_weight += weight;
443  ++i;
444  }
445  output += absl::StrFormat("p wcnf %d %d %d\n", first_slack_variable,
446  static_cast<int>(problem.constraints_size() +
447  non_slack_objective.size()),
448  hard_weight);
449  } else {
450  output += absl::StrFormat("p cnf %d %d\n", problem.num_variables(),
451  problem.constraints_size());
452  }
453 
454  std::string constraint_output;
455  for (const LinearBooleanConstraint& constraint : problem.constraints()) {
456  if (constraint.literals_size() == 0) return ""; // Assumption.
457  constraint_output.clear();
458  int64_t weight = hard_weight;
459  for (int i = 0; i < constraint.literals_size(); ++i) {
460  if (constraint.coefficients(i) != 1) return ""; // Assumption.
461  if (is_wcnf && abs(constraint.literals(i)) - 1 >= first_slack_variable) {
462  weight = literal_to_weight[constraint.literals(i)];
463  } else {
464  if (i > 0) constraint_output += " ";
465  constraint_output += Literal(constraint.literals(i)).DebugString();
466  }
467  }
468  if (is_wcnf) {
469  output += absl::StrFormat("%d ", weight);
470  }
471  output += constraint_output + " 0\n";
472  }
473 
474  // Output the rest of the objective as singleton constraints.
475  if (is_wcnf) {
476  for (std::pair<int, int64_t> p : non_slack_objective) {
477  // Since it is falsifying this clause that cost "weigtht", we need to take
478  // its negation.
479  const Literal literal(-p.first);
480  output += absl::StrFormat("%d %s 0\n", p.second, literal.DebugString());
481  }
482  }
483 
484  return output;
485 }
486 
487 void StoreAssignment(const VariablesAssignment& assignment,
488  BooleanAssignment* output) {
489  output->clear_literals();
490  for (BooleanVariable var(0); var < assignment.NumberOfVariables(); ++var) {
491  if (assignment.VariableIsAssigned(var)) {
492  output->add_literals(
494  }
495  }
496 }
497 
498 void ExtractSubproblem(const LinearBooleanProblem& problem,
499  const std::vector<int>& constraint_indices,
500  LinearBooleanProblem* subproblem) {
501  *subproblem = problem;
502  subproblem->set_name("Subproblem of " + problem.name());
503  subproblem->clear_constraints();
504  for (int index : constraint_indices) {
505  CHECK_LT(index, problem.constraints_size());
506  subproblem->add_constraints()->MergeFrom(problem.constraints(index));
507  }
508 }
509 
510 namespace {
511 // A simple class to generate equivalence class number for
512 // GenerateGraphForSymmetryDetection().
513 class IdGenerator {
514  public:
515  IdGenerator() {}
516 
517  // If the pair (type, coefficient) was never seen before, then generate
518  // a new id, otherwise return the previously generated id.
519  int GetId(int type, Coefficient coefficient) {
520  const std::pair<int, int64_t> key(type, coefficient.value());
521  return id_map_.emplace(key, id_map_.size()).first->second;
522  }
523 
524  private:
525  absl::flat_hash_map<std::pair<int, int64_t>, int> id_map_;
526 };
527 } // namespace.
528 
529 // Returns a graph whose automorphisms can be mapped back to the symmetries of
530 // the given LinearBooleanProblem.
531 //
532 // Any permutation of the graph that respects the initial_equivalence_classes
533 // output can be mapped to a symmetry of the given problem simply by taking its
534 // restriction on the first 2 * num_variables nodes and interpreting its index
535 // as a literal index. In a sense, a node with a low enough index #i is in
536 // one-to-one correspondence with a literals #i (using the index representation
537 // of literal).
538 //
539 // The format of the initial_equivalence_classes is the same as the one
540 // described in GraphSymmetryFinder::FindSymmetries(). The classes must be dense
541 // in [0, num_classes) and any symmetry will only map nodes with the same class
542 // between each other.
543 template <typename Graph>
545  const LinearBooleanProblem& problem,
546  std::vector<int>* initial_equivalence_classes) {
547  // First, we convert the problem to its canonical representation.
548  const int num_variables = problem.num_variables();
549  CanonicalBooleanLinearProblem canonical_problem;
550  std::vector<LiteralWithCoeff> cst;
551  for (const LinearBooleanConstraint& constraint : problem.constraints()) {
552  cst = ConvertLinearExpression(constraint);
553  CHECK(canonical_problem.AddLinearConstraint(
554  constraint.has_lower_bound(), Coefficient(constraint.lower_bound()),
555  constraint.has_upper_bound(), Coefficient(constraint.upper_bound()),
556  &cst));
557  }
558 
559  // TODO(user): reserve the memory for the graph? not sure it is worthwhile
560  // since it would require some linear scan of the problem though.
561  Graph* graph = new Graph();
562  initial_equivalence_classes->clear();
563 
564  // We will construct a graph with 3 different types of node that must be
565  // in different equivalent classes.
566  enum NodeType { LITERAL_NODE, CONSTRAINT_NODE, CONSTRAINT_COEFFICIENT_NODE };
567  IdGenerator id_generator;
568 
569  // First, we need one node per literal with an edge between each literal
570  // and its negation.
571  for (int i = 0; i < num_variables; ++i) {
572  // We have two nodes for each variable.
573  // Note that the indices are in [0, 2 * num_variables) and in one to one
574  // correspondence with the index representation of a literal.
575  const Literal literal = Literal(BooleanVariable(i), true);
576  graph->AddArc(literal.Index().value(), literal.NegatedIndex().value());
577  graph->AddArc(literal.NegatedIndex().value(), literal.Index().value());
578  }
579 
580  // We use 0 for their initial equivalence class, but that may be modified
581  // with the objective coefficient (see below).
582  initial_equivalence_classes->assign(
583  2 * num_variables,
584  id_generator.GetId(NodeType::LITERAL_NODE, Coefficient(0)));
585 
586  // Literals with different objective coeffs shouldn't be in the same class.
587  //
588  // We need to canonicalize the objective to regroup literals corresponding
589  // to the same variables. Note that we don't care about the offset or
590  // optimization direction here, we just care about literals with the same
591  // canonical coefficient.
592  Coefficient shift;
593  Coefficient max_value;
594  std::vector<LiteralWithCoeff> expr =
595  ConvertLinearExpression(problem.objective());
596  ComputeBooleanLinearExpressionCanonicalForm(&expr, &shift, &max_value);
597  for (LiteralWithCoeff term : expr) {
598  (*initial_equivalence_classes)[term.literal.Index().value()] =
599  id_generator.GetId(NodeType::LITERAL_NODE, term.coefficient);
600  }
601 
602  // Then, for each constraint, we will have one or more nodes.
603  for (int i = 0; i < canonical_problem.NumConstraints(); ++i) {
604  // First we have a node for the constraint with an equivalence class
605  // depending on the rhs.
606  //
607  // Note: Since we add nodes one by one, initial_equivalence_classes->size()
608  // gives the number of nodes at any point, which we use as next node index.
609  const int constraint_node_index = initial_equivalence_classes->size();
610  initial_equivalence_classes->push_back(id_generator.GetId(
611  NodeType::CONSTRAINT_NODE, canonical_problem.Rhs(i)));
612 
613  // This node will also be connected to all literals of the constraint
614  // with a coefficient of 1. Literals with new coefficients will be grouped
615  // under a new node connected to the constraint_node_index.
616  //
617  // Note that this works because a canonical constraint is sorted by
618  // increasing coefficient value (all positive).
619  int current_node_index = constraint_node_index;
620  Coefficient previous_coefficient(1);
621  for (LiteralWithCoeff term : canonical_problem.Constraint(i)) {
622  if (term.coefficient != previous_coefficient) {
623  current_node_index = initial_equivalence_classes->size();
624  initial_equivalence_classes->push_back(id_generator.GetId(
625  NodeType::CONSTRAINT_COEFFICIENT_NODE, term.coefficient));
626  previous_coefficient = term.coefficient;
627 
628  // Connect this node to the constraint node. Note that we don't
629  // technically need the arcs in both directions, but that may help a bit
630  // the algorithm to find symmetries.
631  graph->AddArc(constraint_node_index, current_node_index);
632  graph->AddArc(current_node_index, constraint_node_index);
633  }
634 
635  // Connect this node to the associated term.literal node. Note that we
636  // don't technically need the arcs in both directions, but that may help a
637  // bit the algorithm to find symmetries.
638  graph->AddArc(current_node_index, term.literal.Index().value());
639  graph->AddArc(term.literal.Index().value(), current_node_index);
640  }
641  }
642  graph->Build();
643  DCHECK_EQ(graph->num_nodes(), initial_equivalence_classes->size());
644  return graph;
645 }
646 
647 void MakeAllLiteralsPositive(LinearBooleanProblem* problem) {
648  // Objective.
649  LinearObjective* mutable_objective = problem->mutable_objective();
650  int64_t objective_offset = 0;
651  for (int i = 0; i < mutable_objective->literals_size(); ++i) {
652  const int signed_literal = mutable_objective->literals(i);
653  if (signed_literal < 0) {
654  const int64_t coefficient = mutable_objective->coefficients(i);
655  mutable_objective->set_literals(i, -signed_literal);
656  mutable_objective->set_coefficients(i, -coefficient);
657  objective_offset += coefficient;
658  }
659  }
660  mutable_objective->set_offset(mutable_objective->offset() + objective_offset);
661 
662  // Constraints.
663  for (LinearBooleanConstraint& constraint :
664  *(problem->mutable_constraints())) {
665  int64_t sum = 0;
666  for (int i = 0; i < constraint.literals_size(); ++i) {
667  if (constraint.literals(i) < 0) {
668  sum += constraint.coefficients(i);
669  constraint.set_literals(i, -constraint.literals(i));
670  constraint.set_coefficients(i, -constraint.coefficients(i));
671  }
672  }
673  if (constraint.has_lower_bound()) {
674  constraint.set_lower_bound(constraint.lower_bound() - sum);
675  }
676  if (constraint.has_upper_bound()) {
677  constraint.set_upper_bound(constraint.upper_bound() - sum);
678  }
679  }
680 }
681 
683  const LinearBooleanProblem& problem,
684  std::vector<std::unique_ptr<SparsePermutation>>* generators) {
686  std::vector<int> equivalence_classes;
687  std::unique_ptr<Graph> graph(
688  GenerateGraphForSymmetryDetection<Graph>(problem, &equivalence_classes));
689  LOG(INFO) << "Graph has " << graph->num_nodes() << " nodes and "
690  << graph->num_arcs() / 2 << " edges.";
691 #if !defined(__PORTABLE_PLATFORM__)
692  if (!absl::GetFlag(FLAGS_debug_dump_symmetry_graph_to_file).empty()) {
693  // Remap the graph nodes to sort them by equivalence classes.
694  std::vector<int> new_node_index(graph->num_nodes(), -1);
695  const int num_classes = 1 + *std::max_element(equivalence_classes.begin(),
696  equivalence_classes.end());
697  std::vector<int> class_size(num_classes, 0);
698  for (const int c : equivalence_classes) ++class_size[c];
699  std::vector<int> next_index_by_class(num_classes, 0);
700  std::partial_sum(class_size.begin(), class_size.end() - 1,
701  next_index_by_class.begin() + 1);
702  for (int node = 0; node < graph->num_nodes(); ++node) {
703  new_node_index[node] = next_index_by_class[equivalence_classes[node]]++;
704  }
705  std::unique_ptr<Graph> remapped_graph = RemapGraph(*graph, new_node_index);
706  const absl::Status status = util::WriteGraphToFile(
707  *remapped_graph, absl::GetFlag(FLAGS_debug_dump_symmetry_graph_to_file),
708  /*directed=*/false, class_size);
709  if (!status.ok()) {
710  LOG(DFATAL) << "Error when writing the symmetry graph to file: "
711  << status;
712  }
713  }
714 #endif // __PORTABLE_PLATFORM__
715  GraphSymmetryFinder symmetry_finder(*graph,
716  /*is_undirected=*/true);
717  std::vector<int> factorized_automorphism_group_size;
718  // TODO(user): inject the appropriate time limit here.
719  CHECK(symmetry_finder
720  .FindSymmetries(&equivalence_classes, generators,
721  &factorized_automorphism_group_size)
722  .ok());
723 
724  // Remove from the permutations the part not concerning the literals.
725  // Note that some permutation may becomes empty, which means that we had
726  // duplicates constraints. TODO(user): Remove them beforehand?
727  double average_support_size = 0.0;
728  int num_generators = 0;
729  for (int i = 0; i < generators->size(); ++i) {
730  SparsePermutation* permutation = (*generators)[i].get();
731  std::vector<int> to_delete;
732  for (int j = 0; j < permutation->NumCycles(); ++j) {
733  if (*(permutation->Cycle(j).begin()) >= 2 * problem.num_variables()) {
734  to_delete.push_back(j);
735  if (DEBUG_MODE) {
736  // Verify that the cycle's entire support does not touch any variable.
737  for (const int node : permutation->Cycle(j)) {
738  DCHECK_GE(node, 2 * problem.num_variables());
739  }
740  }
741  }
742  }
743  permutation->RemoveCycles(to_delete);
744  if (!permutation->Support().empty()) {
745  average_support_size += permutation->Support().size();
746  swap((*generators)[num_generators], (*generators)[i]);
747  ++num_generators;
748  }
749  }
750  generators->resize(num_generators);
751  average_support_size /= num_generators;
752  LOG(INFO) << "# of generators: " << num_generators;
753  LOG(INFO) << "Average support size: " << average_support_size;
754 }
755 
758  LinearBooleanProblem* problem) {
759  Coefficient bound_shift;
760  Coefficient max_value;
761  std::vector<LiteralWithCoeff> cst;
762 
763  // First the objective.
764  cst = ConvertLinearExpression(problem->objective());
765  ApplyLiteralMapping(mapping, &cst, &bound_shift, &max_value);
766  LinearObjective* mutable_objective = problem->mutable_objective();
767  mutable_objective->clear_literals();
768  mutable_objective->clear_coefficients();
769  mutable_objective->set_offset(mutable_objective->offset() -
770  bound_shift.value());
771  for (const LiteralWithCoeff& entry : cst) {
772  mutable_objective->add_literals(entry.literal.SignedValue());
773  mutable_objective->add_coefficients(entry.coefficient.value());
774  }
775 
776  // Now the clauses.
777  for (LinearBooleanConstraint& constraint : *problem->mutable_constraints()) {
778  cst = ConvertLinearExpression(constraint);
779  constraint.clear_literals();
780  constraint.clear_coefficients();
781  ApplyLiteralMapping(mapping, &cst, &bound_shift, &max_value);
782 
783  // Add bound_shift to the bounds and remove a bound if it is now trivial.
784  if (constraint.has_upper_bound()) {
785  constraint.set_upper_bound(constraint.upper_bound() +
786  bound_shift.value());
787  if (max_value <= constraint.upper_bound()) {
788  constraint.clear_upper_bound();
789  }
790  }
791  if (constraint.has_lower_bound()) {
792  constraint.set_lower_bound(constraint.lower_bound() +
793  bound_shift.value());
794  // This is because ApplyLiteralMapping make all coefficient positive.
795  if (constraint.lower_bound() <= 0) {
796  constraint.clear_lower_bound();
797  }
798  }
799 
800  // If the constraint is always true, we just leave it empty.
801  if (constraint.has_lower_bound() || constraint.has_upper_bound()) {
802  for (const LiteralWithCoeff& entry : cst) {
803  constraint.add_literals(entry.literal.SignedValue());
804  constraint.add_coefficients(entry.coefficient.value());
805  }
806  }
807  }
808 
809  // Remove empty constraints.
810  int new_index = 0;
811  const int num_constraints = problem->constraints_size();
812  for (int i = 0; i < num_constraints; ++i) {
813  if (!(problem->constraints(i).literals_size() == 0)) {
814  problem->mutable_constraints()->SwapElements(i, new_index);
815  ++new_index;
816  }
817  }
818  problem->mutable_constraints()->DeleteSubrange(new_index,
819  num_constraints - new_index);
820 
821  // Computes the new number of variables and set it.
822  int num_vars = 0;
823  for (LiteralIndex index : mapping) {
824  if (index >= 0) {
825  num_vars = std::max(num_vars, Literal(index).Variable().value() + 1);
826  }
827  }
828  problem->set_num_variables(num_vars);
829 
830  // TODO(user): The names is currently all scrambled. Do something about it
831  // so that non-fixed variables keep their names.
832  problem->mutable_var_names()->DeleteSubrange(
833  num_vars, problem->var_names_size() - num_vars);
834 }
835 
836 // A simple preprocessing step that does basic probing and removes the
837 // equivalent literals.
839  LinearBooleanProblem* problem) {
840  // TODO(user): expose the number of iterations as a parameter.
841  for (int iter = 0; iter < 6; ++iter) {
842  SatSolver solver;
843  if (!LoadBooleanProblem(*problem, &solver)) {
844  LOG(INFO) << "UNSAT when loading the problem.";
845  }
846 
848  ProbeAndFindEquivalentLiteral(&solver, postsolver, /*drat_writer=*/nullptr,
849  &equiv_map);
850 
851  // We can abort if no information is learned.
852  if (equiv_map.empty() && solver.LiteralTrail().Index() == 0) break;
853 
854  if (equiv_map.empty()) {
855  const int num_literals = 2 * solver.NumVariables();
856  for (LiteralIndex index(0); index < num_literals; ++index) {
857  equiv_map.push_back(index);
858  }
859  }
860 
861  // Fix fixed variables in the equivalence map and in the postsolver.
862  solver.Backtrack(0);
863  for (int i = 0; i < solver.LiteralTrail().Index(); ++i) {
864  const Literal l = solver.LiteralTrail()[i];
865  equiv_map[l.Index()] = kTrueLiteralIndex;
866  equiv_map[l.NegatedIndex()] = kFalseLiteralIndex;
867  postsolver->FixVariable(l);
868  }
869 
870  // Remap the variables into a dense set. All the variables for which the
871  // equiv_map is not the identity are no longer needed.
872  BooleanVariable new_var(0);
874  for (BooleanVariable var(0); var < solver.NumVariables(); ++var) {
875  if (equiv_map[Literal(var, true).Index()] == Literal(var, true).Index()) {
876  var_map.push_back(new_var);
877  ++new_var;
878  } else {
879  var_map.push_back(BooleanVariable(-1));
880  }
881  }
882 
883  // Apply the variable mapping.
884  postsolver->ApplyMapping(var_map);
885  for (LiteralIndex index(0); index < equiv_map.size(); ++index) {
886  if (equiv_map[index] >= 0) {
887  const Literal l(equiv_map[index]);
888  const BooleanVariable image = var_map[l.Variable()];
889  CHECK_NE(image, BooleanVariable(-1));
890  equiv_map[index] = Literal(image, l.IsPositive()).Index();
891  }
892  }
893  ApplyLiteralMappingToBooleanProblem(equiv_map, problem);
894  }
895 }
896 
897 } // namespace sat
898 } // namespace operations_research
int64_t max
Definition: alldiff_cst.cc:140
int64_t min
Definition: alldiff_cst.cc:139
ABSL_FLAG(std::string, debug_dump_symmetry_graph_to_file, "", "If this flag is non-empty, an undirected graph whose" " automorphism group is in one-to-one correspondence with the" " symmetries of the SAT problem will be dumped to a file every" " time FindLinearBooleanProblemSymmetries() is called.")
size_type size() const
bool empty() const
void push_back(const value_type &x)
void RemoveCycles(const std::vector< int > &cycle_indices)
const std::vector< int > & Support() const
bool AddLinearConstraint(bool use_lower_bound, Coefficient lower_bound, bool use_upper_bound, Coefficient upper_bound, std::vector< LiteralWithCoeff > *cst)
const std::vector< LiteralWithCoeff > & Constraint(int i) const
LiteralIndex NegatedIndex() const
Definition: sat_base.h:91
LiteralIndex Index() const
Definition: sat_base.h:90
BooleanVariable Variable() const
Definition: sat_base.h:86
std::string DebugString() const
Definition: sat_base.h:99
void ApplyMapping(const absl::StrongVector< BooleanVariable, BooleanVariable > &mapping)
const Trail & LiteralTrail() const
Definition: sat_solver.h:387
bool AddLinearConstraint(bool use_lower_bound, Coefficient lower_bound, bool use_upper_bound, Coefficient upper_bound, std::vector< LiteralWithCoeff > *cst)
Definition: sat_solver.cc:354
void SetNumVariables(int num_variables)
Definition: sat_solver.cc:86
const SatParameters & parameters() const
Definition: sat_solver.cc:132
void SetAssignmentPreference(Literal literal, double weight)
Definition: sat_solver.h:158
const VariablesAssignment & Assignment() const
Definition: sat_solver.h:388
void Backtrack(int target_level)
Definition: sat_solver.cc:1004
bool VariableIsAssigned(BooleanVariable var) const
Definition: sat_base.h:172
bool LiteralIsTrue(Literal literal) const
Definition: sat_base.h:164
Literal GetTrueLiteralForAssignedVariable(BooleanVariable var) const
Definition: sat_base.h:179
const Constraint * ct
int64_t value
IntVar * var
Definition: expr_array.cc:1874
absl::Status status
Definition: g_gurobi.cc:41
int index
const bool DEBUG_MODE
Definition: macros.h:24
void swap(IdMap< K, V > &a, IdMap< K, V > &b)
Definition: id_map.h:269
std::tuple< int64_t, int64_t, const double > Coefficient
bool AddObjectiveConstraint(const LinearBooleanProblem &problem, bool use_lower_bound, Coefficient lower_bound, bool use_upper_bound, Coefficient upper_bound, SatSolver *solver)
void StoreAssignment(const VariablesAssignment &assignment, BooleanAssignment *output)
Graph * GenerateGraphForSymmetryDetection(const LinearBooleanProblem &problem, std::vector< int > *initial_equivalence_classes)
void UseObjectiveForSatAssignmentPreference(const LinearBooleanProblem &problem, SatSolver *solver)
bool ApplyLiteralMapping(const absl::StrongVector< LiteralIndex, LiteralIndex > &mapping, std::vector< LiteralWithCoeff > *cst, Coefficient *bound_shift, Coefficient *max_value)
void ExtractSubproblem(const LinearBooleanProblem &problem, const std::vector< int > &constraint_indices, LinearBooleanProblem *subproblem)
absl::Status ValidateBooleanProblem(const LinearBooleanProblem &problem)
bool AddObjectiveUpperBound(const LinearBooleanProblem &problem, Coefficient upper_bound, SatSolver *solver)
void FindLinearBooleanProblemSymmetries(const LinearBooleanProblem &problem, std::vector< std::unique_ptr< SparsePermutation >> *generators)
const LiteralIndex kTrueLiteralIndex(-2)
bool ComputeBooleanLinearExpressionCanonicalForm(std::vector< LiteralWithCoeff > *cst, Coefficient *bound_shift, Coefficient *max_value)
const LiteralIndex kFalseLiteralIndex(-3)
bool LoadAndConsumeBooleanProblem(LinearBooleanProblem *problem, SatSolver *solver)
void ApplyLiteralMappingToBooleanProblem(const absl::StrongVector< LiteralIndex, LiteralIndex > &mapping, LinearBooleanProblem *problem)
bool IsAssignmentValid(const LinearBooleanProblem &problem, const std::vector< bool > &assignment)
void ChangeOptimizationDirection(LinearBooleanProblem *problem)
void ProbeAndSimplifyProblem(SatPostsolver *postsolver, LinearBooleanProblem *problem)
Coefficient ComputeObjectiveValue(const LinearBooleanProblem &problem, const std::vector< bool > &assignment)
void ProbeAndFindEquivalentLiteral(SatSolver *solver, SatPostsolver *postsolver, DratProofHandler *drat_proof_handler, absl::StrongVector< LiteralIndex, LiteralIndex > *mapping)
CpModelProto BooleanProblemToCpModelproto(const LinearBooleanProblem &problem)
void MakeAllLiteralsPositive(LinearBooleanProblem *problem)
bool LoadBooleanProblem(const LinearBooleanProblem &problem, SatSolver *solver)
std::string LinearBooleanProblemToCnfString(const LinearBooleanProblem &problem)
void ExtractAssignment(const LinearBooleanProblem &problem, const SatSolver &solver, std::vector< bool > *assignment)
Collection of objects used to extend the Constraint Solver library.
std::string ProtobufDebugString(const P &message)
ListGraph Graph
Definition: graph.h:2398
absl::Status WriteGraphToFile(const Graph &graph, const std::string &filename, bool directed, const std::vector< int > &num_nodes_with_color)
Definition: io.h:98
std::unique_ptr< Graph > RemapGraph(const Graph &graph, const std::vector< int > &new_node_index)
Definition: graph/util.h:277
Literal literal
Definition: optimization.cc:88
int64_t weight
Definition: pack.cc:510
static int input(yyscan_t yyscanner)
IntVar * upper_bound
Definition: routing.cc:1087
IntVar * lower_bound
Definition: routing.cc:1086
int64_t coefficient
std::vector< int >::const_iterator begin() const