OR-Tools  9.0
cp_model_symmetries.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 
15 
16 #include <cstdint>
17 #include <limits>
18 #include <memory>
19 
20 #include "absl/container/flat_hash_map.h"
21 #include "absl/memory/memory.h"
22 #include "google/protobuf/repeated_field.h"
24 #include "ortools/base/hash.h"
25 #include "ortools/base/map_util.h"
28 
29 namespace operations_research {
30 namespace sat {
31 
32 namespace {
33 struct VectorHash {
34  std::size_t operator()(const std::vector<int64_t>& values) const {
35  size_t hash = 0;
36  for (const int64_t value : values) {
38  }
39  return hash;
40  }
41 };
42 
43 // A simple class to generate equivalence class number for
44 // GenerateGraphForSymmetryDetection().
45 class IdGenerator {
46  public:
47  IdGenerator() {}
48 
49  // If the color was never seen before, then generate a new id, otherwise
50  // return the previously generated id.
51  int GetId(const std::vector<int64_t>& color) {
52  return gtl::LookupOrInsert(&id_map_, color, id_map_.size());
53  }
54 
55  int NextFreeId() const { return id_map_.size(); }
56 
57  private:
58  absl::flat_hash_map<std::vector<int64_t>, int, VectorHash> id_map_;
59 };
60 
61 // Appends values in `repeated_field` to `vector`.
62 //
63 // We use a template as proto int64_t != C++ int64_t in open source.
64 template <typename FieldInt64Type>
65 void Append(
66  const google::protobuf::RepeatedField<FieldInt64Type>& repeated_field,
67  std::vector<int64_t>* vector) {
68  CHECK(vector != nullptr);
69  for (const FieldInt64Type value : repeated_field) {
70  vector->push_back(value);
71  }
72 }
73 
74 // Returns a graph whose automorphisms can be mapped back to the symmetries of
75 // the model described in the given CpModelProto.
76 //
77 // Any permutation of the graph that respects the initial_equivalence_classes
78 // output can be mapped to a symmetry of the given problem simply by taking its
79 // restriction on the first num_variables nodes and interpreting its index as a
80 // variable index. In a sense, a node with a low enough index #i is in
81 // one-to-one correspondence with the variable #i (using the index
82 // representation of variables).
83 //
84 // The format of the initial_equivalence_classes is the same as the one
85 // described in GraphSymmetryFinder::FindSymmetries(). The classes must be dense
86 // in [0, num_classes) and any symmetry will only map nodes with the same class
87 // between each other.
88 template <typename Graph>
89 std::unique_ptr<Graph> GenerateGraphForSymmetryDetection(
90  const CpModelProto& problem, std::vector<int>* initial_equivalence_classes,
91  SolverLogger* logger) {
92  CHECK(initial_equivalence_classes != nullptr);
93 
94  const int num_variables = problem.variables_size();
95  auto graph = absl::make_unique<Graph>();
96 
97  // Each node will be created with a given color. Two nodes of different color
98  // can never be send one into another by a symmetry. The first element of
99  // the color vector will always be the NodeType.
100  //
101  // TODO(user): Using a full int64_t for storing 3 values is not great. We
102  // can optimize this at the price of a bit more code.
103  enum NodeType {
104  VARIABLE_NODE,
105  VAR_COEFFICIENT_NODE,
106  CONSTRAINT_NODE,
107  };
108  IdGenerator color_id_generator;
109  initial_equivalence_classes->clear();
110  auto new_node = [&initial_equivalence_classes, &graph,
111  &color_id_generator](const std::vector<int64_t>& color) {
112  // Since we add nodes one by one, initial_equivalence_classes->size() gives
113  // the number of nodes at any point, which we use as the next node index.
114  const int node = initial_equivalence_classes->size();
115  initial_equivalence_classes->push_back(color_id_generator.GetId(color));
116 
117  // In some corner cases, we create a node but never uses it. We still
118  // want it to be there.
119  graph->AddNode(node);
120  return node;
121  };
122 
123  // For two variables to be in the same equivalence class, they need to have
124  // the same objective coefficient, and the same possible bounds.
125  //
126  // TODO(user): We could ignore the objective coefficients, and just make sure
127  // that when we break symmetry amongst variables, we choose the possibility
128  // with the smallest cost?
129  std::vector<int64_t> objective_by_var(num_variables, 0);
130  for (int i = 0; i < problem.objective().vars_size(); ++i) {
131  const int ref = problem.objective().vars(i);
132  const int var = PositiveRef(ref);
133  const int64_t coeff = problem.objective().coeffs(i);
134  objective_by_var[var] = RefIsPositive(ref) ? coeff : -coeff;
135  }
136 
137  // Create one node for each variable. Note that the code rely on the fact that
138  // the index of a VARIABLE_NODE type is the same as the variable index.
139  std::vector<int64_t> tmp_color;
140  for (int v = 0; v < num_variables; ++v) {
141  tmp_color = {VARIABLE_NODE, objective_by_var[v]};
142  Append(problem.variables(v).domain(), &tmp_color);
143  CHECK_EQ(v, new_node(tmp_color));
144  }
145 
146  // We will lazily create "coefficient nodes" that correspond to a variable
147  // with a given coefficient.
148  absl::flat_hash_map<std::pair<int64_t, int64_t>, int> coefficient_nodes;
149  auto get_coefficient_node = [&new_node, &graph, &coefficient_nodes,
150  &tmp_color](int var, int64_t coeff) {
151  const int var_node = var;
153 
154  // For a coefficient of one, which are the most common, we can optimize the
155  // size of the graph by omitting the coefficient node altogether and using
156  // directly the var_node in this case.
157  if (coeff == 1) return var_node;
158 
159  const auto insert =
160  coefficient_nodes.insert({std::make_pair(var, coeff), 0});
161  if (!insert.second) return insert.first->second;
162 
163  tmp_color = {VAR_COEFFICIENT_NODE, coeff};
164  const int secondary_node = new_node(tmp_color);
165  graph->AddArc(var_node, secondary_node);
166  insert.first->second = secondary_node;
167  return secondary_node;
168  };
169 
170  // For a literal we use the same as a coefficient 1 or -1. We can do that
171  // because literal and (var, coefficient) never appear together in the same
172  // constraint.
173  auto get_literal_node = [&get_coefficient_node](int ref) {
174  return get_coefficient_node(PositiveRef(ref), RefIsPositive(ref) ? 1 : -1);
175  };
176 
177  // Because the implications can be numerous, we encode them without
178  // constraints node by using an arc from the lhs to the rhs. Note that we also
179  // always add the other direction. We use a set to remove duplicates both for
180  // efficiency and to not artificially break symmetries by using multi-arcs.
181  //
182  // Tricky: We cannot use the base variable node here to avoid situation like
183  // both a variable a and b having the same children (not(a), not(b)) in the
184  // graph. Because if that happen, we can permute a and b without permuting
185  // their associated not(a) and not(b) node! To be sure this cannot happen, a
186  // variable node can not have as children a VAR_COEFFICIENT_NODE from another
187  // node. This makes sure that any permutation that touch a variable, must
188  // permute its coefficient nodes accordingly.
189  absl::flat_hash_set<std::pair<int, int>> implications;
190  auto get_implication_node = [&new_node, &graph, &coefficient_nodes,
191  &tmp_color](int ref) {
192  const int var = PositiveRef(ref);
193  const int64_t coeff = RefIsPositive(ref) ? 1 : -1;
194  const auto insert =
195  coefficient_nodes.insert({std::make_pair(var, coeff), 0});
196  if (!insert.second) return insert.first->second;
197  tmp_color = {VAR_COEFFICIENT_NODE, coeff};
198  const int secondary_node = new_node(tmp_color);
199  graph->AddArc(var, secondary_node);
200  insert.first->second = secondary_node;
201  return secondary_node;
202  };
203  auto add_implication = [&get_implication_node, &graph, &implications](
204  int ref_a, int ref_b) {
205  const auto insert = implications.insert({ref_a, ref_b});
206  if (!insert.second) return;
207  graph->AddArc(get_implication_node(ref_a), get_implication_node(ref_b));
208 
209  // Always add the other side.
210  implications.insert({NegatedRef(ref_b), NegatedRef(ref_a)});
211  graph->AddArc(get_implication_node(NegatedRef(ref_b)),
212  get_implication_node(NegatedRef(ref_a)));
213  };
214 
215  // Add constraints to the graph.
216  for (const ConstraintProto& constraint : problem.constraints()) {
217  const int constraint_node = initial_equivalence_classes->size();
218  std::vector<int64_t> color = {CONSTRAINT_NODE,
219  constraint.constraint_case()};
220 
221  switch (constraint.constraint_case()) {
222  case ConstraintProto::CONSTRAINT_NOT_SET:
223  break;
224  case ConstraintProto::kLinear: {
225  // TODO(user): We can use the same trick as for the implications to
226  // encode relations of the form coeff * var_a <= coeff * var_b without
227  // creating a constraint node by directly adding an arc between the two
228  // var coefficient nodes.
229  Append(constraint.linear().domain(), &color);
230  CHECK_EQ(constraint_node, new_node(color));
231  for (int i = 0; i < constraint.linear().vars_size(); ++i) {
232  const int ref = constraint.linear().vars(i);
233  const int variable_node = PositiveRef(ref);
234  const int64_t coeff = RefIsPositive(ref)
235  ? constraint.linear().coeffs(i)
236  : -constraint.linear().coeffs(i);
237  graph->AddArc(get_coefficient_node(variable_node, coeff),
238  constraint_node);
239  }
240  break;
241  }
242  case ConstraintProto::kBoolOr: {
243  CHECK_EQ(constraint_node, new_node(color));
244  for (const int ref : constraint.bool_or().literals()) {
245  graph->AddArc(get_literal_node(ref), constraint_node);
246  }
247  break;
248  }
249  case ConstraintProto::kAtMostOne: {
250  if (constraint.at_most_one().literals().size() == 2) {
251  // Treat it as an implication to avoid creating a node.
252  add_implication(constraint.at_most_one().literals(0),
253  NegatedRef(constraint.at_most_one().literals(1)));
254  break;
255  }
256 
257  CHECK_EQ(constraint_node, new_node(color));
258  for (const int ref : constraint.at_most_one().literals()) {
259  graph->AddArc(get_literal_node(ref), constraint_node);
260  }
261  break;
262  }
263  case ConstraintProto::kExactlyOne: {
264  CHECK_EQ(constraint_node, new_node(color));
265  for (const int ref : constraint.exactly_one().literals()) {
266  graph->AddArc(get_literal_node(ref), constraint_node);
267  }
268  break;
269  }
270  case ConstraintProto::kBoolXor: {
271  CHECK_EQ(constraint_node, new_node(color));
272  for (const int ref : constraint.bool_xor().literals()) {
273  graph->AddArc(get_literal_node(ref), constraint_node);
274  }
275  break;
276  }
277  case ConstraintProto::kBoolAnd: {
278  // The other cases should be presolved before this is called.
279  // TODO(user): not 100% true, this happen on rmatr200-p5, Fix.
280  if (constraint.enforcement_literal_size() != 1) {
281  SOLVER_LOG(
282  logger,
283  "[Symmetry] BoolAnd with multiple enforcement literal are not "
284  "supported in symmetry code:",
285  constraint.ShortDebugString());
286  return nullptr;
287  }
288 
289  CHECK_EQ(constraint.enforcement_literal_size(), 1);
290  const int ref_a = constraint.enforcement_literal(0);
291  for (const int ref_b : constraint.bool_and().literals()) {
292  add_implication(ref_a, ref_b);
293  }
294  break;
295  }
296  default: {
297  // If the model contains any non-supported constraints, return an empty
298  // graph.
299  //
300  // TODO(user): support other types of constraints. Or at least, we
301  // could associate to them an unique node so that their variables can
302  // appear in no symmetry.
303  VLOG(1) << "Unsupported constraint type "
304  << ConstraintCaseName(constraint.constraint_case());
305  return nullptr;
306  }
307  }
308 
309  // For enforcement, we use a similar trick than for the implications.
310  // Because all our constraint arcs are in the direction var_node to
311  // constraint_node, we just use the reverse direction for the enforcement
312  // part. This way we can reuse the same get_literal_node() function.
313  if (constraint.constraint_case() != ConstraintProto::kBoolAnd) {
314  for (const int ref : constraint.enforcement_literal()) {
315  graph->AddArc(constraint_node, get_literal_node(ref));
316  }
317  }
318  }
319 
320  graph->Build();
321  DCHECK_EQ(graph->num_nodes(), initial_equivalence_classes->size());
322 
323  // TODO(user): The symmetry code does not officially support multi-arcs. And
324  // we shouldn't have any as long as there is no duplicates variable in our
325  // constraints (but of course, we can't always guarantee that). That said,
326  // because the symmetry code really only look at the degree, it works as long
327  // as the maximum degree is bounded by num_nodes.
328  const int num_nodes = graph->num_nodes();
329  std::vector<int> in_degree(num_nodes, 0);
330  std::vector<int> out_degree(num_nodes, 0);
331  for (int i = 0; i < num_nodes; ++i) {
332  out_degree[i] = graph->OutDegree(i);
333  for (const int head : (*graph)[i]) {
334  in_degree[head]++;
335  }
336  }
337  for (int i = 0; i < num_nodes; ++i) {
338  if (in_degree[i] >= num_nodes || out_degree[i] >= num_nodes) {
339  SOLVER_LOG(logger, "[Symmetry] Too many multi-arcs in symmetry code.");
340  return nullptr;
341  }
342  }
343 
344  // Because this code is running during presolve, a lot a variable might have
345  // no edges. We do not want to detect symmetries between these.
346  //
347  // Note that this code forces us to "densify" the ids afterwards because the
348  // symmetry detection code relies on that.
349  //
350  // TODO(user): It will probably be more efficient to not even create these
351  // nodes, but we will need a mapping to know the variable <-> node index.
352  int next_id = color_id_generator.NextFreeId();
353  for (int i = 0; i < num_variables; ++i) {
354  if ((*graph)[i].empty()) {
355  (*initial_equivalence_classes)[i] = next_id++;
356  }
357  }
358 
359  // Densify ids.
360  int id = 0;
361  std::vector<int> mapping(next_id, -1);
362  for (int& ref : *initial_equivalence_classes) {
363  if (mapping[ref] == -1) {
364  ref = mapping[ref] = id++;
365  } else {
366  ref = mapping[ref];
367  }
368  }
369 
370  return graph;
371 }
372 } // namespace
373 
375  const SatParameters& params, const CpModelProto& problem,
376  std::vector<std::unique_ptr<SparsePermutation>>* generators,
377  double deterministic_limit, SolverLogger* logger) {
378  CHECK(generators != nullptr);
379  generators->clear();
380 
382 
383  std::vector<int> equivalence_classes;
384  std::unique_ptr<Graph> graph(GenerateGraphForSymmetryDetection<Graph>(
385  problem, &equivalence_classes, logger));
386  if (graph == nullptr) return;
387 
388  SOLVER_LOG(logger, "[Symmetry] Graph for symmetry has ", graph->num_nodes(),
389  " nodes and ", graph->num_arcs(), " arcs.");
390  if (graph->num_nodes() == 0) return;
391 
392  GraphSymmetryFinder symmetry_finder(*graph, /*is_undirected=*/false);
393  std::vector<int> factorized_automorphism_group_size;
394  std::unique_ptr<TimeLimit> time_limit =
395  TimeLimit::FromDeterministicTime(deterministic_limit);
396  const absl::Status status = symmetry_finder.FindSymmetries(
397  &equivalence_classes, generators, &factorized_automorphism_group_size,
398  time_limit.get());
399 
400  // TODO(user): Change the API to not return an error when the time limit is
401  // reached.
402  if (!status.ok()) {
403  SOLVER_LOG(logger,
404  "[Symmetry] GraphSymmetryFinder error: ", status.message());
405  }
406 
407  // Remove from the permutations the part not concerning the variables.
408  // Note that some permutations may become empty, which means that we had
409  // duplicate constraints.
410  double average_support_size = 0.0;
411  int num_generators = 0;
412  int num_duplicate_constraints = 0;
413  for (int i = 0; i < generators->size(); ++i) {
414  SparsePermutation* permutation = (*generators)[i].get();
415  std::vector<int> to_delete;
416  for (int j = 0; j < permutation->NumCycles(); ++j) {
417  // Because variable nodes are in a separate equivalence class than any
418  // other node, a cycle can either contain only variable nodes or none, so
419  // we just need to check one element of the cycle.
420  if (*(permutation->Cycle(j).begin()) >= problem.variables_size()) {
421  to_delete.push_back(j);
422  if (DEBUG_MODE) {
423  // Verify that the cycle's entire support does not touch any variable.
424  for (const int node : permutation->Cycle(j)) {
425  DCHECK_GE(node, problem.variables_size());
426  }
427  }
428  }
429  }
430 
431  permutation->RemoveCycles(to_delete);
432  if (!permutation->Support().empty()) {
433  average_support_size += permutation->Support().size();
434  swap((*generators)[num_generators], (*generators)[i]);
435  ++num_generators;
436  } else {
437  ++num_duplicate_constraints;
438  }
439  }
440  generators->resize(num_generators);
441  average_support_size /= num_generators;
442  SOLVER_LOG(logger, "[Symmetry] Symmetry computation done. time: ",
443  time_limit->GetElapsedTime(),
444  " dtime: ", time_limit->GetElapsedDeterministicTime());
445  if (num_generators > 0) {
446  SOLVER_LOG(logger, "[Symmetry] # of generators: ", num_generators);
447  SOLVER_LOG(logger,
448  "[Symmetry] Average support size: ", average_support_size);
449  if (num_duplicate_constraints > 0) {
450  SOLVER_LOG(logger, "[Symmetry] The model contains ",
451  num_duplicate_constraints, " duplicate constraints !");
452  }
453  }
454 }
455 
456 void DetectAndAddSymmetryToProto(const SatParameters& params,
457  CpModelProto* proto, SolverLogger* logger) {
458  SymmetryProto* symmetry = proto->mutable_symmetry();
459  symmetry->Clear();
460 
461  std::vector<std::unique_ptr<SparsePermutation>> generators;
462  FindCpModelSymmetries(params, *proto, &generators,
463  /*deterministic_limit=*/1.0, logger);
464  if (generators.empty()) return;
465 
466  for (const std::unique_ptr<SparsePermutation>& perm : generators) {
467  SparsePermutationProto* perm_proto = symmetry->add_permutations();
468  const int num_cycle = perm->NumCycles();
469  for (int i = 0; i < num_cycle; ++i) {
470  const int old_size = perm_proto->support().size();
471  for (const int var : perm->Cycle(i)) {
472  perm_proto->add_support(var);
473  }
474  perm_proto->add_cycle_sizes(perm_proto->support().size() - old_size);
475  }
476  }
477 
478  std::vector<std::vector<int>> orbitope = BasicOrbitopeExtraction(generators);
479  if (orbitope.empty()) return;
480  SOLVER_LOG(logger, "[Symmetry] Found orbitope of size ", orbitope.size(),
481  " x ", orbitope[0].size());
482  DenseMatrixProto* matrix = symmetry->add_orbitopes();
483  matrix->set_num_rows(orbitope.size());
484  matrix->set_num_cols(orbitope[0].size());
485  for (const std::vector<int>& row : orbitope) {
486  for (const int entry : row) {
487  matrix->add_entries(entry);
488  }
489  }
490 }
491 
493  const SatParameters& params = context->params();
494  const CpModelProto& proto = *context->working_model;
495 
496  // We need to make sure the proto is up to date before computing symmetries!
497  if (context->working_model->has_objective()) {
498  context->WriteObjectiveToProto();
499  }
500  const int num_vars = proto.variables_size();
501  for (int i = 0; i < num_vars; ++i) {
502  FillDomainInProto(context->DomainOf(i),
503  context->working_model->mutable_variables(i));
504  }
505 
506  // Tricky: the equivalence relation are not part of the proto.
507  // We thus add them temporarily to compute the symmetry.
508  int64_t num_added = 0;
509  const int initial_ct_index = proto.constraints().size();
510  for (int var = 0; var < num_vars; ++var) {
511  if (context->IsFixed(var)) continue;
512  if (context->VariableWasRemoved(var)) continue;
513  if (context->VariableIsNotUsedAnymore(var)) continue;
514 
515  const AffineRelation::Relation r = context->GetAffineRelation(var);
516  if (r.representative == var) continue;
517 
518  ++num_added;
519  ConstraintProto* ct = context->working_model->add_constraints();
520  auto* arg = ct->mutable_linear();
521  arg->add_vars(var);
522  arg->add_coeffs(1);
523  arg->add_vars(r.representative);
524  arg->add_coeffs(-r.coeff);
525  arg->add_domain(r.offset);
526  arg->add_domain(r.offset);
527  }
528 
529  std::vector<std::unique_ptr<SparsePermutation>> generators;
530  FindCpModelSymmetries(params, proto, &generators,
531  /*deterministic_limit=*/1.0, context->logger());
532 
533  // Remove temporary affine relation.
534  context->working_model->mutable_constraints()->DeleteSubrange(
535  initial_ct_index, num_added);
536 
537  if (generators.empty()) return true;
538 
539  // Collect the at most ones.
540  //
541  // Note(user): This relies on the fact that the pointers remain stable when
542  // we adds new constraints. It should be the case, but it is a bit unsafe.
543  // On the other hand it is annoying to deal with both cases below.
544  std::vector<const google::protobuf::RepeatedField<int32_t>*> at_most_ones;
545  for (int i = 0; i < proto.constraints_size(); ++i) {
546  if (proto.constraints(i).constraint_case() == ConstraintProto::kAtMostOne) {
547  at_most_ones.push_back(&proto.constraints(i).at_most_one().literals());
548  }
549  if (proto.constraints(i).constraint_case() ==
550  ConstraintProto::kExactlyOne) {
551  at_most_ones.push_back(&proto.constraints(i).exactly_one().literals());
552  }
553  }
554 
555  // Experimental. Generic approach. First step.
556  //
557  // If an at most one intersect with one or more orbit, in each intersection,
558  // we can fix all but one variable to zero. For now we only test positive
559  // literal, and maximize the number of fixing.
560  std::vector<int> can_be_fixed_to_false;
561  {
562  const std::vector<int> orbits = GetOrbits(num_vars, generators);
563  std::vector<int> orbit_sizes;
564  int max_orbit_size = 0;
565  for (const int rep : orbits) {
566  if (rep == -1) continue;
567  if (rep >= orbit_sizes.size()) orbit_sizes.resize(rep + 1, 0);
568  orbit_sizes[rep]++;
569  max_orbit_size = std::max(max_orbit_size, orbit_sizes[rep]);
570  }
571 
572  std::vector<int> tmp_to_clear;
573  std::vector<int> tmp_sizes(num_vars, 0);
574  for (const google::protobuf::RepeatedField<int32_t>* literals :
575  at_most_ones) {
576  tmp_to_clear.clear();
577 
578  // Compute how many variables we can fix with this at most one.
579  int num_fixable = 0;
580  for (const int literal : *literals) {
581  if (!RefIsPositive(literal)) continue;
582  if (context->IsFixed(literal)) continue;
583 
584  const int var = PositiveRef(literal);
585  const int rep = orbits[var];
586  if (rep == -1) continue;
587 
588  // We count all but the first one in each orbit.
589  if (tmp_sizes[rep] == 0) tmp_to_clear.push_back(rep);
590  if (tmp_sizes[rep] > 0) ++num_fixable;
591  tmp_sizes[rep]++;
592  }
593 
594  // Redo a pass to copy the intersection.
595  if (num_fixable > can_be_fixed_to_false.size()) {
596  can_be_fixed_to_false.clear();
597  for (const int literal : *literals) {
598  if (!RefIsPositive(literal)) continue;
599  if (context->IsFixed(literal)) continue;
600 
601  const int var = PositiveRef(literal);
602  const int rep = orbits[var];
603  if (rep == -1) continue;
604 
605  // We push all but the first one in each orbit.
606  if (tmp_sizes[rep] == 0) can_be_fixed_to_false.push_back(var);
607  tmp_sizes[rep] = 0;
608  }
609  } else {
610  // Sparse clean up.
611  for (const int rep : tmp_to_clear) tmp_sizes[rep] = 0;
612  }
613  }
614 
615  SOLVER_LOG(
616  context->logger(),
617  "[Symmetry] Num fixable by intersecting at_most_one with orbits: ",
618  can_be_fixed_to_false.size(), " largest_orbit: ", max_orbit_size);
619  }
620 
621  // Orbitope approach.
622  //
623  // This is basically the same as the generic approach, but because of the
624  // extra structure, computing the orbit of any stabilizer subgroup is easy.
625  // We look for orbits intersecting at most one constraints, so we can break
626  // symmetry by fixing variables.
627  //
628  // TODO(user): The same effect could be achieved by adding symmetry breaking
629  // constraints of the form "a >= b " between Booleans and let the presolve do
630  // the reduction. This might be less code, but it is also less efficient.
631  // Similarly, when we cannot just fix variables to break symmetries, we could
632  // add these constraints, but it is unclear if we should do it all the time or
633  // not.
634  //
635  // TODO(user): code the generic approach with orbits and stabilizer.
636  std::vector<std::vector<int>> orbitope = BasicOrbitopeExtraction(generators);
637  if (!orbitope.empty()) {
638  SOLVER_LOG(context->logger(), "[Symmetry] Found orbitope of size ",
639  orbitope.size(), " x ", orbitope[0].size());
640  }
641 
642  // Supper simple heuristic to use the orbitope or not.
643  //
644  // In an orbitope with an at most one on each row, we can fix the upper right
645  // triangle. We could use a formula, but the loop is fast enough.
646  //
647  // TODO(user): Compute the stabilizer under the only non-fixed element and
648  // iterate!
649  int max_num_fixed_in_orbitope = 0;
650  if (!orbitope.empty()) {
651  const int num_rows = orbitope[0].size();
652  int size_left = num_rows;
653  for (int col = 0; size_left > 1 && col < orbitope.size(); ++col) {
654  max_num_fixed_in_orbitope += size_left - 1;
655  --size_left;
656  }
657  }
658  if (max_num_fixed_in_orbitope < can_be_fixed_to_false.size()) {
659  for (int i = 0; i < can_be_fixed_to_false.size(); ++i) {
660  const int var = can_be_fixed_to_false[i];
661  context->UpdateRuleStats("symmetry: fixed to false in general orbit");
662  if (!context->SetLiteralToFalse(var)) return false;
663  }
664  return true;
665  }
666  if (orbitope.empty()) return true;
667 
668  // This will always be kept all zero after usage.
669  std::vector<int> tmp_to_clear;
670  std::vector<int> tmp_sizes(num_vars, 0);
671  std::vector<int> tmp_num_positive(num_vars, 0);
672 
673  // TODO(user): The code below requires that no variable appears twice in the
674  // same at most one. In particular lit and not(lit) cannot appear in the same
675  // at most one.
676  for (const google::protobuf::RepeatedField<int32_t>* literals :
677  at_most_ones) {
678  for (const int lit : *literals) {
679  const int var = PositiveRef(lit);
680  CHECK_NE(tmp_sizes[var], 1);
681  tmp_sizes[var] = 1;
682  }
683  for (const int lit : *literals) {
684  tmp_sizes[PositiveRef(lit)] = 0;
685  }
686  }
687 
688  while (!orbitope.empty() && orbitope[0].size() > 1) {
689  const int num_cols = orbitope[0].size();
690  const std::vector<int> orbits = GetOrbitopeOrbits(num_vars, orbitope);
691 
692  // Because in the orbitope case, we have a full symmetry group of the
693  // columns, we can infer more than just using the orbits under a general
694  // permutation group. If an at most one contains two variables from the
695  // orbit, we can infer:
696  // 1/ If the two variables appear positively, then there is an at most one
697  // on the full orbit, and we can set n - 1 variables to zero to break the
698  // symmetry.
699  // 2/ If the two variables appear negatively, then the opposite situation
700  // arise and there is at most one zero on the orbit, we can set n - 1
701  // variables to one.
702  // 3/ If two literals of opposite sign appear, then the only possibility
703  // for the orbit are all at one or all at zero, thus we can mark all
704  // variables as equivalent.
705  //
706  // These property comes from the fact that when we permute a line of the
707  // orbitope in any way, then the position than ends up in the at most one
708  // must never be both at one.
709  //
710  // Note that 1/ can be done without breaking any symmetry, but for 2/ and 3/
711  // by choosing which variable is not fixed, we will break some symmetry, and
712  // we will need to update the orbitope to stabilize this choice before
713  // continuing.
714  //
715  // TODO(user): for 2/ and 3/ we could add an at most one constraint on the
716  // full orbit if it is not already there!
717  //
718  // Note(user): On the miplib, only 1/ happens currently. Not sure with LNS
719  // though.
720  std::vector<bool> all_equivalent_rows(orbitope.size(), false);
721 
722  // The result described above can be generalized if an at most one intersect
723  // many of the orbitope rows, each in at leat two positions. We will track
724  // the set of best rows on which we have an at most one (or at most one
725  // zero) on all their entries.
726  bool at_most_one_in_best_rows; // The alternative is at most one zero.
727  int64_t best_score = 0;
728  std::vector<int> best_rows;
729 
730  std::vector<int> rows_in_at_most_one;
731  for (const google::protobuf::RepeatedField<int32_t>* literals :
732  at_most_ones) {
733  tmp_to_clear.clear();
734  for (const int literal : *literals) {
735  if (context->IsFixed(literal)) continue;
736  const int var = PositiveRef(literal);
737  const int rep = orbits[var];
738  if (rep == -1) continue;
739 
740  if (tmp_sizes[rep] == 0) tmp_to_clear.push_back(rep);
741  tmp_sizes[rep]++;
742  if (RefIsPositive(literal)) tmp_num_positive[rep]++;
743  }
744 
745  int num_positive_direction = 0;
746  int num_negative_direction = 0;
747 
748  // An at most one touching two positions in an orbitope row can possibly
749  // be extended, depending if it has singleton intersection swith other
750  // rows and where.
751  bool possible_extension = false;
752 
753  rows_in_at_most_one.clear();
754  for (const int row : tmp_to_clear) {
755  const int size = tmp_sizes[row];
756  const int num_positive = tmp_num_positive[row];
757  const int num_negative = tmp_sizes[row] - tmp_num_positive[row];
758  tmp_sizes[row] = 0;
759  tmp_num_positive[row] = 0;
760 
761  if (num_positive > 1 && num_negative == 0) {
762  if (size < num_cols) possible_extension = true;
763  rows_in_at_most_one.push_back(row);
764  ++num_positive_direction;
765  } else if (num_positive == 0 && num_negative > 1) {
766  if (size < num_cols) possible_extension = true;
767  rows_in_at_most_one.push_back(row);
768  ++num_negative_direction;
769  } else if (num_positive > 0 && num_negative > 0) {
770  all_equivalent_rows[row] = true;
771  }
772  }
773 
774  if (possible_extension) {
775  context->UpdateRuleStats(
776  "TODO symmetry: possible at most one extension.");
777  }
778 
779  if (num_positive_direction > 0 && num_negative_direction > 0) {
780  return context->NotifyThatModelIsUnsat("Symmetry and at most ones");
781  }
782  const bool direction = num_positive_direction > 0;
783 
784  // Because of symmetry, the choice of the column shouldn't matter (they
785  // will all appear in the same number of constraints of the same types),
786  // however we prefer to fix the variables that seems to touch more
787  // constraints.
788  //
789  // TODO(user): maybe we should simplify the constraint using the variable
790  // we fix before choosing the next row to break symmetry on. If there are
791  // multiple row involved, we could also take the intersection instead of
792  // probably counting the same constraints more than once.
793  int64_t score = 0;
794  for (const int row : rows_in_at_most_one) {
795  score +=
796  context->VarToConstraints(PositiveRef(orbitope[row][0])).size();
797  }
798  if (score > best_score) {
799  at_most_one_in_best_rows = direction;
800  best_score = score;
801  best_rows = rows_in_at_most_one;
802  }
803  }
804 
805  // Mark all the equivalence.
806  // Note that this operation do not change the symmetry group.
807  //
808  // TODO(user): We could remove these rows from the orbitope. Note that
809  // currently this never happen on the miplib (maybe in LNS though).
810  for (int i = 0; i < all_equivalent_rows.size(); ++i) {
811  if (all_equivalent_rows[i]) {
812  for (int j = 1; j < num_cols; ++j) {
813  context->StoreBooleanEqualityRelation(orbitope[i][0], orbitope[i][j]);
814  context->UpdateRuleStats("symmetry: all equivalent in orbit");
815  if (context->ModelIsUnsat()) return false;
816  }
817  }
818  }
819 
820  // Break the symmetry on our set of best rows by picking one columns
821  // and setting all the other entries to zero or one. Note that the at most
822  // one applies to all entries in all rows.
823  //
824  // TODO(user): We don't have any at most one relation on this orbitope,
825  // but we could still add symmetry breaking inequality by picking any matrix
826  // entry and making it the largest/lowest value on its row. This also work
827  // for non-Booleans.
828  if (best_score == 0) {
829  context->UpdateRuleStats(
830  "TODO symmetry: add symmetry breaking inequalities?");
831  break;
832  }
833 
834  // If our symmetry group is valid, they cannot be any variable already
835  // fixed to one (or zero if !at_most_one_in_best_rows). Otherwise all would
836  // be fixed to one and the problem would be unsat.
837  for (const int i : best_rows) {
838  for (int j = 0; j < num_cols; ++j) {
839  const int var = orbitope[i][j];
840  if ((at_most_one_in_best_rows && context->LiteralIsTrue(var)) ||
841  (!at_most_one_in_best_rows && context->LiteralIsFalse(var))) {
842  return context->NotifyThatModelIsUnsat("Symmetry and at most one");
843  }
844  }
845  }
846 
847  // We have an at most one on a set of rows, we will pick a column, and set
848  // all other entries on these rows to zero.
849  //
850  // TODO(user): All choices should be equivalent, but double check?
851  const int best_col = 0;
852  for (const int i : best_rows) {
853  for (int j = 0; j < num_cols; ++j) {
854  if (j == best_col) continue;
855  const int var = orbitope[i][j];
856  if (at_most_one_in_best_rows) {
857  context->UpdateRuleStats("symmetry: fixed to false");
858  if (!context->SetLiteralToFalse(var)) return false;
859  } else {
860  context->UpdateRuleStats("symmetry: fixed to true");
861  if (!context->SetLiteralToTrue(var)) return false;
862  }
863  }
864  }
865 
866  // Remove all best rows.
867  for (const int i : best_rows) orbitope[i].clear();
868  int new_size = 0;
869  for (int i = 0; i < orbitope.size(); ++i) {
870  if (!orbitope[i].empty()) orbitope[new_size++] = orbitope[i];
871  }
872  CHECK_LT(new_size, orbitope.size());
873  orbitope.resize(new_size);
874 
875  // Remove best_col.
876  for (int i = 0; i < orbitope.size(); ++i) {
877  std::swap(orbitope[i][best_col], orbitope[i].back());
878  orbitope[i].pop_back();
879  }
880  }
881 
882  // If we are left with a set of variable than can all be permuted, lets
883  // break the symmetry by ordering them.
884  if (orbitope.size() == 1) {
885  const int num_cols = orbitope[0].size();
886  for (int i = 0; i + 1 < num_cols; ++i) {
887  // Add orbitope[0][i] >= orbitope[0][i+1].
888  ConstraintProto* ct = context->working_model->add_constraints();
889  ct->mutable_linear()->add_coeffs(1);
890  ct->mutable_linear()->add_vars(orbitope[0][i]);
891  ct->mutable_linear()->add_coeffs(-1);
892  ct->mutable_linear()->add_vars(orbitope[0][i + 1]);
893  ct->mutable_linear()->add_domain(0);
894  ct->mutable_linear()->add_domain(std::numeric_limits<int64_t>::max());
895  context->UpdateRuleStats("symmetry: added symmetry breaking inequality");
896  }
897  context->UpdateNewConstraintsVariableUsage();
898  }
899 
900  return true;
901 }
902 
903 } // namespace sat
904 } // namespace operations_research
int64_t max
Definition: alldiff_cst.cc:140
#define CHECK(condition)
Definition: base/logging.h:498
#define CHECK_LT(val1, val2)
Definition: base/logging.h:708
#define CHECK_EQ(val1, val2)
Definition: base/logging.h:705
#define DCHECK_GE(val1, val2)
Definition: base/logging.h:897
#define CHECK_NE(val1, val2)
Definition: base/logging.h:706
#define DCHECK(condition)
Definition: base/logging.h:892
#define DCHECK_EQ(val1, val2)
Definition: base/logging.h:893
#define VLOG(verboselevel)
Definition: base/logging.h:986
absl::Status FindSymmetries(std::vector< int > *node_equivalence_classes_io, std::vector< std::unique_ptr< SparsePermutation > > *generators, std::vector< int > *factorized_automorphism_group_size, TimeLimit *time_limit=nullptr)
void RemoveCycles(const std::vector< int > &cycle_indices)
const std::vector< int > & Support() const
static std::unique_ptr< TimeLimit > FromDeterministicTime(double deterministic_limit)
Creates a time limit object that puts limit only on the deterministic time.
Definition: time_limit.h:144
CpModelProto proto
SharedTimeLimit * time_limit
const Constraint * ct
int64_t value
IntVar * var
Definition: expr_array.cc:1874
GurobiMPCallbackContext * context
const bool DEBUG_MODE
Definition: macros.h:24
ColIndex col
Definition: markowitz.cc:183
RowIndex row
Definition: markowitz.cc:182
int64_t hash
Definition: matrix_utils.cc:61
Collection::value_type::second_type & LookupOrInsert(Collection *const collection, const typename Collection::value_type::first_type &key, const typename Collection::value_type::second_type &value)
Definition: map_util.h:237
void swap(IdMap< K, V > &a, IdMap< K, V > &b)
Definition: id_map.h:263
void DetectAndAddSymmetryToProto(const SatParameters &params, CpModelProto *proto, SolverLogger *logger)
bool DetectAndExploitSymmetriesInPresolve(PresolveContext *context)
bool RefIsPositive(int ref)
std::vector< int > GetOrbitopeOrbits(int n, const std::vector< std::vector< int >> &orbitope)
Graph * GenerateGraphForSymmetryDetection(const LinearBooleanProblem &problem, std::vector< int > *initial_equivalence_classes)
void FindCpModelSymmetries(const SatParameters &params, const CpModelProto &problem, std::vector< std::unique_ptr< SparsePermutation >> *generators, double deterministic_limit, SolverLogger *logger)
void FillDomainInProto(const Domain &domain, ProtoWithDomain *proto)
std::string ConstraintCaseName(ConstraintProto::ConstraintCase constraint_case)
std::vector< std::vector< int > > BasicOrbitopeExtraction(const std::vector< std::unique_ptr< SparsePermutation >> &generators)
std::vector< int > GetOrbits(int n, const std::vector< std::unique_ptr< SparsePermutation >> &generators)
Collection of objects used to extend the Constraint Solver library.
uint64_t Hash(uint64_t num, uint64_t c)
Definition: hash.h:150
ListGraph Graph
Definition: graph.h:2361
Literal literal
Definition: optimization.cc:85
int64_t head
std::vector< int >::const_iterator begin() const
#define SOLVER_LOG(logger,...)
Definition: util/logging.h:63