OR-Tools  9.6
cp_model_symmetries.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 <stddef.h>
17 
18 #include <algorithm>
19 #include <cstdint>
20 #include <limits>
21 #include <memory>
22 #include <utility>
23 #include <vector>
24 
25 #include "absl/container/flat_hash_map.h"
26 #include "absl/container/flat_hash_set.h"
27 #include "absl/meta/type_traits.h"
28 #include "absl/status/status.h"
29 #include "absl/strings/str_cat.h"
30 #include "absl/strings/str_join.h"
31 #include "google/protobuf/message.h"
34 #include "ortools/base/hash.h"
35 #include "ortools/base/logging.h"
36 #include "ortools/graph/graph.h"
37 #include "ortools/sat/cp_model.pb.h"
40 #include "ortools/sat/model.h"
42 #include "ortools/sat/sat_base.h"
43 #include "ortools/sat/sat_parameters.pb.h"
44 #include "ortools/sat/sat_solver.h"
47 #include "ortools/util/logging.h"
49 
50 namespace operations_research {
51 namespace sat {
52 
53 namespace {
54 struct VectorHash {
55  std::size_t operator()(const std::vector<int64_t>& values) const {
56  size_t hash = 0;
57  for (const int64_t value : values) {
59  }
60  return hash;
61  }
62 };
63 
64 // A simple class to generate equivalence class number for
65 // GenerateGraphForSymmetryDetection().
66 class IdGenerator {
67  public:
68  IdGenerator() {}
69 
70  // If the color was never seen before, then generate a new id, otherwise
71  // return the previously generated id.
72  int GetId(const std::vector<int64_t>& color) {
73  return id_map_.emplace(color, id_map_.size()).first->second;
74  }
75 
76  int NextFreeId() const { return id_map_.size(); }
77 
78  private:
79  absl::flat_hash_map<std::vector<int64_t>, int, VectorHash> id_map_;
80 };
81 
82 // Appends values in `repeated_field` to `vector`.
83 //
84 // We use a template as proto int64_t != C++ int64_t in open source.
85 template <typename FieldInt64Type>
86 void Append(
87  const google::protobuf::RepeatedField<FieldInt64Type>& repeated_field,
88  std::vector<int64_t>* vector) {
89  CHECK(vector != nullptr);
90  for (const FieldInt64Type value : repeated_field) {
91  vector->push_back(value);
92  }
93 }
94 
95 // Returns a graph whose automorphisms can be mapped back to the symmetries of
96 // the model described in the given CpModelProto.
97 //
98 // Any permutation of the graph that respects the initial_equivalence_classes
99 // output can be mapped to a symmetry of the given problem simply by taking its
100 // restriction on the first num_variables nodes and interpreting its index as a
101 // variable index. In a sense, a node with a low enough index #i is in
102 // one-to-one correspondence with the variable #i (using the index
103 // representation of variables).
104 //
105 // The format of the initial_equivalence_classes is the same as the one
106 // described in GraphSymmetryFinder::FindSymmetries(). The classes must be dense
107 // in [0, num_classes) and any symmetry will only map nodes with the same class
108 // between each other.
109 template <typename Graph>
110 std::unique_ptr<Graph> GenerateGraphForSymmetryDetection(
111  const CpModelProto& problem, std::vector<int>* initial_equivalence_classes,
112  SolverLogger* logger) {
113  CHECK(initial_equivalence_classes != nullptr);
114 
115  const int num_variables = problem.variables_size();
116  auto graph = std::make_unique<Graph>();
117 
118  // Each node will be created with a given color. Two nodes of different color
119  // can never be send one into another by a symmetry. The first element of
120  // the color vector will always be the NodeType.
121  //
122  // TODO(user): Using a full int64_t for storing 3 values is not great. We
123  // can optimize this at the price of a bit more code.
124  enum NodeType {
125  VARIABLE_NODE,
126  VAR_COEFFICIENT_NODE,
127  CONSTRAINT_NODE,
128  };
129  IdGenerator color_id_generator;
130  initial_equivalence_classes->clear();
131  auto new_node = [&initial_equivalence_classes, &graph,
132  &color_id_generator](const std::vector<int64_t>& color) {
133  // Since we add nodes one by one, initial_equivalence_classes->size() gives
134  // the number of nodes at any point, which we use as the next node index.
135  const int node = initial_equivalence_classes->size();
136  initial_equivalence_classes->push_back(color_id_generator.GetId(color));
137 
138  // In some corner cases, we create a node but never uses it. We still
139  // want it to be there.
140  graph->AddNode(node);
141  return node;
142  };
143 
144  // For two variables to be in the same equivalence class, they need to have
145  // the same objective coefficient, and the same possible bounds.
146  //
147  // TODO(user): We could ignore the objective coefficients, and just make sure
148  // that when we break symmetry amongst variables, we choose the possibility
149  // with the smallest cost?
150  std::vector<int64_t> objective_by_var(num_variables, 0);
151  for (int i = 0; i < problem.objective().vars_size(); ++i) {
152  const int ref = problem.objective().vars(i);
153  const int var = PositiveRef(ref);
154  const int64_t coeff = problem.objective().coeffs(i);
155  objective_by_var[var] = RefIsPositive(ref) ? coeff : -coeff;
156  }
157 
158  // Create one node for each variable. Note that the code rely on the fact that
159  // the index of a VARIABLE_NODE type is the same as the variable index.
160  std::vector<int64_t> tmp_color;
161  for (int v = 0; v < num_variables; ++v) {
162  tmp_color = {VARIABLE_NODE, objective_by_var[v]};
163  Append(problem.variables(v).domain(), &tmp_color);
164  CHECK_EQ(v, new_node(tmp_color));
165  }
166 
167  // We will lazily create "coefficient nodes" that correspond to a variable
168  // with a given coefficient.
169  absl::flat_hash_map<std::pair<int64_t, int64_t>, int> coefficient_nodes;
170  auto get_coefficient_node = [&new_node, &graph, &coefficient_nodes,
171  &tmp_color](int var, int64_t coeff) {
172  const int var_node = var;
173  DCHECK(RefIsPositive(var));
174 
175  // For a coefficient of one, which are the most common, we can optimize the
176  // size of the graph by omitting the coefficient node altogether and using
177  // directly the var_node in this case.
178  if (coeff == 1) return var_node;
179 
180  const auto insert =
181  coefficient_nodes.insert({std::make_pair(var, coeff), 0});
182  if (!insert.second) return insert.first->second;
183 
184  tmp_color = {VAR_COEFFICIENT_NODE, coeff};
185  const int secondary_node = new_node(tmp_color);
186  graph->AddArc(var_node, secondary_node);
187  insert.first->second = secondary_node;
188  return secondary_node;
189  };
190 
191  // For a literal we use the same as a coefficient 1 or -1. We can do that
192  // because literal and (var, coefficient) never appear together in the same
193  // constraint.
194  auto get_literal_node = [&get_coefficient_node](int ref) {
195  return get_coefficient_node(PositiveRef(ref), RefIsPositive(ref) ? 1 : -1);
196  };
197 
198  // Because the implications can be numerous, we encode them without
199  // constraints node by using an arc from the lhs to the rhs. Note that we also
200  // always add the other direction. We use a set to remove duplicates both for
201  // efficiency and to not artificially break symmetries by using multi-arcs.
202  //
203  // Tricky: We cannot use the base variable node here to avoid situation like
204  // both a variable a and b having the same children (not(a), not(b)) in the
205  // graph. Because if that happen, we can permute a and b without permuting
206  // their associated not(a) and not(b) node! To be sure this cannot happen, a
207  // variable node can not have as children a VAR_COEFFICIENT_NODE from another
208  // node. This makes sure that any permutation that touch a variable, must
209  // permute its coefficient nodes accordingly.
210  absl::flat_hash_set<std::pair<int, int>> implications;
211  auto get_implication_node = [&new_node, &graph, &coefficient_nodes,
212  &tmp_color](int ref) {
213  const int var = PositiveRef(ref);
214  const int64_t coeff = RefIsPositive(ref) ? 1 : -1;
215  const auto insert =
216  coefficient_nodes.insert({std::make_pair(var, coeff), 0});
217  if (!insert.second) return insert.first->second;
218  tmp_color = {VAR_COEFFICIENT_NODE, coeff};
219  const int secondary_node = new_node(tmp_color);
220  graph->AddArc(var, secondary_node);
221  insert.first->second = secondary_node;
222  return secondary_node;
223  };
224  auto add_implication = [&get_implication_node, &graph, &implications](
225  int ref_a, int ref_b) {
226  const auto insert = implications.insert({ref_a, ref_b});
227  if (!insert.second) return;
228  graph->AddArc(get_implication_node(ref_a), get_implication_node(ref_b));
229 
230  // Always add the other side.
231  implications.insert({NegatedRef(ref_b), NegatedRef(ref_a)});
232  graph->AddArc(get_implication_node(NegatedRef(ref_b)),
233  get_implication_node(NegatedRef(ref_a)));
234  };
235 
236  // We need to keep track of this for scheduling constraints.
237  absl::flat_hash_map<int, int> interval_constraint_index_to_node;
238 
239  // Add constraints to the graph.
240  for (int constraint_index = 0; constraint_index < problem.constraints_size();
241  ++constraint_index) {
242  const ConstraintProto& constraint = problem.constraints(constraint_index);
243  const int constraint_node = initial_equivalence_classes->size();
244  std::vector<int64_t> color = {CONSTRAINT_NODE,
245  constraint.constraint_case()};
246 
247  switch (constraint.constraint_case()) {
248  case ConstraintProto::CONSTRAINT_NOT_SET:
249  // TODO(user): We continue for the corner case of a constraint not set
250  // with enforcement literal. We should probably clear this constraint
251  // before reaching here.
252  continue;
253  case ConstraintProto::kLinear: {
254  // TODO(user): We can use the same trick as for the implications to
255  // encode relations of the form coeff * var_a <= coeff * var_b without
256  // creating a constraint node by directly adding an arc between the two
257  // var coefficient nodes.
258  Append(constraint.linear().domain(), &color);
259  CHECK_EQ(constraint_node, new_node(color));
260  for (int i = 0; i < constraint.linear().vars_size(); ++i) {
261  const int ref = constraint.linear().vars(i);
262  const int variable_node = PositiveRef(ref);
263  const int64_t coeff = RefIsPositive(ref)
264  ? constraint.linear().coeffs(i)
265  : -constraint.linear().coeffs(i);
266  graph->AddArc(get_coefficient_node(variable_node, coeff),
267  constraint_node);
268  }
269  break;
270  }
271  case ConstraintProto::kBoolOr: {
272  CHECK_EQ(constraint_node, new_node(color));
273  for (const int ref : constraint.bool_or().literals()) {
274  graph->AddArc(get_literal_node(ref), constraint_node);
275  }
276  break;
277  }
278  case ConstraintProto::kAtMostOne: {
279  if (constraint.at_most_one().literals().size() == 2) {
280  // Treat it as an implication to avoid creating a node.
281  add_implication(constraint.at_most_one().literals(0),
282  NegatedRef(constraint.at_most_one().literals(1)));
283  break;
284  }
285 
286  CHECK_EQ(constraint_node, new_node(color));
287  for (const int ref : constraint.at_most_one().literals()) {
288  graph->AddArc(get_literal_node(ref), constraint_node);
289  }
290  break;
291  }
292  case ConstraintProto::kExactlyOne: {
293  CHECK_EQ(constraint_node, new_node(color));
294  for (const int ref : constraint.exactly_one().literals()) {
295  graph->AddArc(get_literal_node(ref), constraint_node);
296  }
297  break;
298  }
299  case ConstraintProto::kBoolXor: {
300  CHECK_EQ(constraint_node, new_node(color));
301  for (const int ref : constraint.bool_xor().literals()) {
302  graph->AddArc(get_literal_node(ref), constraint_node);
303  }
304  break;
305  }
306  case ConstraintProto::kBoolAnd: {
307  if (constraint.enforcement_literal_size() > 1) {
308  CHECK_EQ(constraint_node, new_node(color));
309  for (const int ref : constraint.bool_and().literals()) {
310  graph->AddArc(get_literal_node(ref), constraint_node);
311  }
312  break;
313  }
314 
315  CHECK_EQ(constraint.enforcement_literal_size(), 1);
316  const int ref_a = constraint.enforcement_literal(0);
317  for (const int ref_b : constraint.bool_and().literals()) {
318  add_implication(ref_a, ref_b);
319  }
320  break;
321  }
322  case ConstraintProto::kInterval: {
323  // We create 3 constraint nodes (for start, size and end) including the
324  // offset. We connect these to their terms like for a linear constraint.
325  std::vector<int> nodes;
326  for (int indicator = 0; indicator <= 2; ++indicator) {
327  const LinearExpressionProto& expr =
328  indicator == 0 ? constraint.interval().start()
329  : indicator == 1 ? constraint.interval().size()
330  : constraint.interval().end();
331 
332  std::vector<int64_t> local_color = color;
333  local_color.push_back(indicator);
334  local_color.push_back(expr.offset());
335  const int local_node = new_node(local_color);
336  nodes.push_back(local_node);
337 
338  for (int i = 0; i < expr.vars().size(); ++i) {
339  const int ref = expr.vars(i);
340  const int var_node = PositiveRef(ref);
341  const int64_t coeff =
342  RefIsPositive(ref) ? expr.coeffs(i) : -expr.coeffs(i);
343  graph->AddArc(get_coefficient_node(var_node, coeff), local_node);
344  }
345  }
346 
347  // We will only map enforcement literal to the start_node below because
348  // it has the same index as the constraint_node.
349  interval_constraint_index_to_node[constraint_index] = constraint_node;
350  CHECK_EQ(nodes[0], constraint_node);
351 
352  // Make sure that if one node is mapped to another one, its other two
353  // components are the same.
354  graph->AddArc(nodes[0], nodes[1]);
355  graph->AddArc(nodes[1], nodes[2]);
356  graph->AddArc(nodes[2], nodes[0]); // TODO(user): not needed?
357  break;
358  }
359  case ConstraintProto::kNoOverlap: {
360  // Note(user): This require that intervals appear before they are used.
361  // We currently enforce this at validation, otherwise we need two passes
362  // here and in a bunch of other places.
363  CHECK_EQ(constraint_node, new_node(color));
364  for (const int interval : constraint.no_overlap().intervals()) {
365  graph->AddArc(interval_constraint_index_to_node.at(interval),
366  constraint_node);
367  }
368  break;
369  }
370  case ConstraintProto::kNoOverlap2D: {
371  // Note(user): This require that intervals appear before they are used.
372  // We currently enforce this at validation, otherwise we need two passes
373  // here and in a bunch of other places.
374  //
375  // TODO(user): With this graph encoding, we loose the symmetry that the
376  // dimension x can be swapped with the dimension y. I think it is
377  // possible to encode this by creating two extra nodes X and
378  // Y, each connected to all the x and all the y, but I have to think
379  // more about it.
380  CHECK_EQ(constraint_node, new_node(color));
381  const int size = constraint.no_overlap_2d().x_intervals().size();
382  for (int i = 0; i < size; ++i) {
383  const int x = constraint.no_overlap_2d().x_intervals(i);
384  const int y = constraint.no_overlap_2d().y_intervals(i);
385  graph->AddArc(interval_constraint_index_to_node.at(x),
386  constraint_node);
387  graph->AddArc(interval_constraint_index_to_node.at(x),
388  interval_constraint_index_to_node.at(y));
389  }
390  break;
391  }
392  default: {
393  // If the model contains any non-supported constraints, return an empty
394  // graph.
395  //
396  // TODO(user): support other types of constraints. Or at least, we
397  // could associate to them an unique node so that their variables can
398  // appear in no symmetry.
399  VLOG(1) << "Unsupported constraint type "
400  << ConstraintCaseName(constraint.constraint_case());
401  return nullptr;
402  }
403  }
404 
405  // For enforcement, we use a similar trick than for the implications.
406  // Because all our constraint arcs are in the direction var_node to
407  // constraint_node, we just use the reverse direction for the enforcement
408  // part. This way we can reuse the same get_literal_node() function.
409  if (constraint.constraint_case() != ConstraintProto::kBoolAnd ||
410  constraint.enforcement_literal().size() > 1) {
411  for (const int ref : constraint.enforcement_literal()) {
412  graph->AddArc(constraint_node, get_literal_node(ref));
413  }
414  }
415  }
416 
417  graph->Build();
418  DCHECK_EQ(graph->num_nodes(), initial_equivalence_classes->size());
419 
420  // TODO(user): The symmetry code does not officially support multi-arcs. And
421  // we shouldn't have any as long as there is no duplicates variable in our
422  // constraints (but of course, we can't always guarantee that). That said,
423  // because the symmetry code really only look at the degree, it works as long
424  // as the maximum degree is bounded by num_nodes.
425  const int num_nodes = graph->num_nodes();
426  std::vector<int> in_degree(num_nodes, 0);
427  std::vector<int> out_degree(num_nodes, 0);
428  for (int i = 0; i < num_nodes; ++i) {
429  out_degree[i] = graph->OutDegree(i);
430  for (const int head : (*graph)[i]) {
431  in_degree[head]++;
432  }
433  }
434  for (int i = 0; i < num_nodes; ++i) {
435  if (in_degree[i] >= num_nodes || out_degree[i] >= num_nodes) {
436  SOLVER_LOG(logger, "[Symmetry] Too many multi-arcs in symmetry code.");
437  return nullptr;
438  }
439  }
440 
441  // Because this code is running during presolve, a lot a variable might have
442  // no edges. We do not want to detect symmetries between these.
443  //
444  // Note that this code forces us to "densify" the ids afterwards because the
445  // symmetry detection code relies on that.
446  //
447  // TODO(user): It will probably be more efficient to not even create these
448  // nodes, but we will need a mapping to know the variable <-> node index.
449  int next_id = color_id_generator.NextFreeId();
450  for (int i = 0; i < num_variables; ++i) {
451  if ((*graph)[i].empty()) {
452  (*initial_equivalence_classes)[i] = next_id++;
453  }
454  }
455 
456  // Densify ids.
457  int id = 0;
458  std::vector<int> mapping(next_id, -1);
459  for (int& ref : *initial_equivalence_classes) {
460  if (mapping[ref] == -1) {
461  ref = mapping[ref] = id++;
462  } else {
463  ref = mapping[ref];
464  }
465  }
466 
467  return graph;
468 }
469 } // namespace
470 
472  const SatParameters& params, const CpModelProto& problem,
473  std::vector<std::unique_ptr<SparsePermutation>>* generators,
474  double deterministic_limit, SolverLogger* logger) {
475  CHECK(generators != nullptr);
476  generators->clear();
477 
478  if (params.symmetry_level() < 3 && problem.variables().size() > 1e6 &&
479  problem.constraints().size() > 1e6) {
480  SOLVER_LOG(logger,
481  "[Symmetry] Problem too large. Skipping. You can use "
482  "symmetry_level:3 or more to force it.");
483  return;
484  }
485 
487 
488  std::vector<int> equivalence_classes;
489  std::unique_ptr<Graph> graph(GenerateGraphForSymmetryDetection<Graph>(
490  problem, &equivalence_classes, logger));
491  if (graph == nullptr) return;
492 
493  SOLVER_LOG(logger, "[Symmetry] Graph for symmetry has ", graph->num_nodes(),
494  " nodes and ", graph->num_arcs(), " arcs.");
495  if (graph->num_nodes() == 0) return;
496 
497  if (params.symmetry_level() < 3 && graph->num_nodes() > 1e6 &&
498  graph->num_arcs() > 1e6) {
499  SOLVER_LOG(logger,
500  "[Symmetry] Graph too large. Skipping. You can use "
501  "symmetry_level:3 or more to force it.");
502  return;
503  }
504 
505  GraphSymmetryFinder symmetry_finder(*graph, /*is_undirected=*/false);
506  std::vector<int> factorized_automorphism_group_size;
507  std::unique_ptr<TimeLimit> time_limit =
508  TimeLimit::FromDeterministicTime(deterministic_limit);
509  const absl::Status status = symmetry_finder.FindSymmetries(
510  &equivalence_classes, generators, &factorized_automorphism_group_size,
511  time_limit.get());
512 
513  // TODO(user): Change the API to not return an error when the time limit is
514  // reached.
515  if (!status.ok()) {
516  SOLVER_LOG(logger,
517  "[Symmetry] GraphSymmetryFinder error: ", status.message());
518  }
519 
520  // Remove from the permutations the part not concerning the variables.
521  // Note that some permutations may become empty, which means that we had
522  // duplicate constraints.
523  double average_support_size = 0.0;
524  int num_generators = 0;
525  int num_duplicate_constraints = 0;
526  for (int i = 0; i < generators->size(); ++i) {
527  SparsePermutation* permutation = (*generators)[i].get();
528  std::vector<int> to_delete;
529  for (int j = 0; j < permutation->NumCycles(); ++j) {
530  // Because variable nodes are in a separate equivalence class than any
531  // other node, a cycle can either contain only variable nodes or none, so
532  // we just need to check one element of the cycle.
533  if (*(permutation->Cycle(j).begin()) >= problem.variables_size()) {
534  to_delete.push_back(j);
535  if (DEBUG_MODE) {
536  // Verify that the cycle's entire support does not touch any variable.
537  for (const int node : permutation->Cycle(j)) {
538  DCHECK_GE(node, problem.variables_size());
539  }
540  }
541  }
542  }
543 
544  permutation->RemoveCycles(to_delete);
545  if (!permutation->Support().empty()) {
546  average_support_size += permutation->Support().size();
547  swap((*generators)[num_generators], (*generators)[i]);
548  ++num_generators;
549  } else {
550  ++num_duplicate_constraints;
551  }
552  }
553  generators->resize(num_generators);
554  average_support_size /= num_generators;
555  SOLVER_LOG(logger, "[Symmetry] Symmetry computation done. time: ",
556  time_limit->GetElapsedTime(),
557  " dtime: ", time_limit->GetElapsedDeterministicTime());
558  if (num_generators > 0) {
559  SOLVER_LOG(logger, "[Symmetry] #generators: ", num_generators,
560  ", average support size: ", average_support_size);
561  if (num_duplicate_constraints > 0) {
562  SOLVER_LOG(logger, "[Symmetry] The model contains ",
563  num_duplicate_constraints, " duplicate constraints !");
564  }
565  }
566 }
567 
568 void DetectAndAddSymmetryToProto(const SatParameters& params,
569  CpModelProto* proto, SolverLogger* logger) {
570  SymmetryProto* symmetry = proto->mutable_symmetry();
571  symmetry->Clear();
572 
573  std::vector<std::unique_ptr<SparsePermutation>> generators;
574  FindCpModelSymmetries(params, *proto, &generators,
575  /*deterministic_limit=*/1.0, logger);
576  if (generators.empty()) {
577  proto->clear_symmetry();
578  return;
579  }
580 
581  for (const std::unique_ptr<SparsePermutation>& perm : generators) {
582  SparsePermutationProto* perm_proto = symmetry->add_permutations();
583  const int num_cycle = perm->NumCycles();
584  for (int i = 0; i < num_cycle; ++i) {
585  const int old_size = perm_proto->support().size();
586  for (const int var : perm->Cycle(i)) {
587  perm_proto->add_support(var);
588  }
589  perm_proto->add_cycle_sizes(perm_proto->support().size() - old_size);
590  }
591  }
592 
593  std::vector<std::vector<int>> orbitope = BasicOrbitopeExtraction(generators);
594  if (orbitope.empty()) return;
595  SOLVER_LOG(logger, "[Symmetry] Found orbitope of size ", orbitope.size(),
596  " x ", orbitope[0].size());
597  DenseMatrixProto* matrix = symmetry->add_orbitopes();
598  matrix->set_num_rows(orbitope.size());
599  matrix->set_num_cols(orbitope[0].size());
600  for (const std::vector<int>& row : orbitope) {
601  for (const int entry : row) {
602  matrix->add_entries(entry);
603  }
604  }
605 }
606 
607 namespace {
608 
609 // Given one Boolean orbit under symmetry, if there is a Boolean at one in this
610 // orbit, then we can always move it to a fixed position (i.e. the given
611 // variable var). Moreover, any variable implied to zero in this orbit by var
612 // being at one can be fixed to zero. This is because, after symmetry breaking,
613 // either var is one, or all the orbit is zero. We also add implications to
614 // enforce this fact, but this is not done in this function.
615 //
616 // TODO(user): If an exactly one / at least one is included in the orbit, then
617 // we can set a given variable to one directly. We can also detect this by
618 // trying to propagate the orbit to all false.
619 //
620 // TODO(user): The same reasonning can be done if fixing the variable to
621 // zero leads to many propagations at one. For general variables, we might be
622 // able to do something too.
623 void OrbitAndPropagation(const std::vector<int>& orbits, int var,
624  std::vector<int>* can_be_fixed_to_false,
625  PresolveContext* context) {
626  // Note that if a variable is fixed in the orbit, then everything should be
627  // fixed.
628  if (context->IsFixed(var)) return;
629  if (!context->CanBeUsedAsLiteral(var)) return;
630 
631  // Lets fix var to true and see what is propagated.
632  //
633  // TODO(user): Ideally we should have a propagator ready for this. Right now
634  // we load the full model if we detected symmetries. We should really combine
635  // this with probing even though this is "breaking" the symmetry so it cannot
636  // be applied as generally as probing.
637  //
638  // TODO(user): Note that probing can also benefit from symmetry, since in
639  // each orbit, only one variable needs to be probed, and any conclusion can
640  // be duplicated to all the variables from an orbit! It is also why we just
641  // need to propagate one variable here.
642  Model model;
643  if (!LoadModelForProbing(context, &model)) return;
644 
645  auto* sat_solver = model.GetOrCreate<SatSolver>();
646  auto* mapping = model.GetOrCreate<CpModelMapping>();
647  const Literal to_propagate = mapping->Literal(var);
648 
649  const VariablesAssignment& assignment = sat_solver->Assignment();
650  if (assignment.LiteralIsAssigned(to_propagate)) return;
651  sat_solver->EnqueueDecisionAndBackjumpOnConflict(to_propagate);
652  if (sat_solver->CurrentDecisionLevel() != 1) return;
653 
654  // We can fix to false any variable that is in the orbit and set to false!
655  can_be_fixed_to_false->clear();
656  int orbit_size = 0;
657  const int orbit_index = orbits[var];
658  const int num_variables = orbits.size();
659  for (int var = 0; var < num_variables; ++var) {
660  if (orbits[var] != orbit_index) continue;
661  ++orbit_size;
662 
663  // By symmetry since same orbit.
664  DCHECK(!context->IsFixed(var));
665  DCHECK(context->CanBeUsedAsLiteral(var));
666 
667  if (assignment.LiteralIsFalse(mapping->Literal(var))) {
668  can_be_fixed_to_false->push_back(var);
669  }
670  }
671  if (!can_be_fixed_to_false->empty()) {
672  SOLVER_LOG(context->logger(),
673  "[Symmetry] Num fixable by binary propagation in orbit: ",
674  can_be_fixed_to_false->size(), " / ", orbit_size);
675  }
676 }
677 
678 } // namespace
679 
681  const SatParameters& params = context->params();
682  const CpModelProto& proto = *context->working_model;
683 
684  // We need to make sure the proto is up to date before computing symmetries!
685  if (context->working_model->has_objective()) {
686  context->WriteObjectiveToProto();
687  }
688  context->WriteVariableDomainsToProto();
689 
690  // Tricky: the equivalence relation are not part of the proto.
691  // We thus add them temporarily to compute the symmetry.
692  int64_t num_added = 0;
693  const int initial_ct_index = proto.constraints().size();
694  const int num_vars = proto.variables_size();
695  for (int var = 0; var < num_vars; ++var) {
696  if (context->IsFixed(var)) continue;
697  if (context->VariableWasRemoved(var)) continue;
698  if (context->VariableIsNotUsedAnymore(var)) continue;
699 
700  const AffineRelation::Relation r = context->GetAffineRelation(var);
701  if (r.representative == var) continue;
702 
703  ++num_added;
704  ConstraintProto* ct = context->working_model->add_constraints();
705  auto* arg = ct->mutable_linear();
706  arg->add_vars(var);
707  arg->add_coeffs(1);
708  arg->add_vars(r.representative);
709  arg->add_coeffs(-r.coeff);
710  arg->add_domain(r.offset);
711  arg->add_domain(r.offset);
712  }
713 
714  std::vector<std::unique_ptr<SparsePermutation>> generators;
715  FindCpModelSymmetries(params, proto, &generators,
716  /*deterministic_limit=*/1.0, context->logger());
717 
718  // Remove temporary affine relation.
719  context->working_model->mutable_constraints()->DeleteSubrange(
720  initial_ct_index, num_added);
721 
722  if (generators.empty()) return true;
723 
724  // Collect the at most ones.
725  //
726  // Note(user): This relies on the fact that the pointers remain stable when
727  // we adds new constraints. It should be the case, but it is a bit unsafe.
728  // On the other hand it is annoying to deal with both cases below.
729  std::vector<const google::protobuf::RepeatedField<int32_t>*> at_most_ones;
730  for (int i = 0; i < proto.constraints_size(); ++i) {
731  if (proto.constraints(i).constraint_case() == ConstraintProto::kAtMostOne) {
732  at_most_ones.push_back(&proto.constraints(i).at_most_one().literals());
733  }
734  if (proto.constraints(i).constraint_case() ==
735  ConstraintProto::kExactlyOne) {
736  at_most_ones.push_back(&proto.constraints(i).exactly_one().literals());
737  }
738  }
739 
740  // We have a few heuristics. The firsts only look at the gobal orbits under
741  // the symmetry group and try to infer Boolean variable fixing via symmetry
742  // breaking. Note that nothing is fixed yet, we will decide later if we fix
743  // these Booleans or not.
744  int distinguished_var = -1;
745  std::vector<int> can_be_fixed_to_false;
746 
747  // Get the global orbits and their size.
748  const std::vector<int> orbits = GetOrbits(num_vars, generators);
749  std::vector<int> orbit_sizes;
750  int max_orbit_size = 0;
751  for (int var = 0; var < num_vars; ++var) {
752  const int rep = orbits[var];
753  if (rep == -1) continue;
754  if (rep >= orbit_sizes.size()) orbit_sizes.resize(rep + 1, 0);
755  orbit_sizes[rep]++;
756  if (orbit_sizes[rep] > max_orbit_size) {
757  distinguished_var = var;
758  max_orbit_size = orbit_sizes[rep];
759  }
760  }
761 
762  // Log orbit info.
763  if (context->logger()->LoggingIsEnabled()) {
764  std::vector<int> sorted_sizes;
765  for (const int s : orbit_sizes) {
766  if (s != 0) sorted_sizes.push_back(s);
767  }
768  std::sort(sorted_sizes.begin(), sorted_sizes.end(), std::greater<int>());
769  const int num_orbits = sorted_sizes.size();
770  if (num_orbits > 10) sorted_sizes.resize(10);
771  SOLVER_LOG(context->logger(), "[Symmetry] ", num_orbits,
772  " orbits with sizes: ", absl::StrJoin(sorted_sizes, ","),
773  (num_orbits > sorted_sizes.size() ? ",..." : ""));
774  }
775 
776  // First heuristic based on propagation, see the function comment.
777  if (max_orbit_size > 2) {
778  OrbitAndPropagation(orbits, distinguished_var, &can_be_fixed_to_false,
779  context);
780  }
781  const int first_heuristic_size = can_be_fixed_to_false.size();
782 
783  // If an at most one intersect with one or more orbit, in each intersection,
784  // we can fix all but one variable to zero. For now we only test positive
785  // literal, and maximize the number of fixing.
786  //
787  // TODO(user): Doing that is not always good, on cod105.mps, fixing variables
788  // instead of letting the innner solver handle Boolean symmetries make the
789  // problem unsolvable instead of easily solved. This is probably because this
790  // fixing do not exploit the full structure of these symmeteries. Note
791  // however that the fixing via propagation above close cod105 even more
792  // efficiently.
793  {
794  std::vector<int> tmp_to_clear;
795  std::vector<int> tmp_sizes(num_vars, 0);
796  for (const google::protobuf::RepeatedField<int32_t>* literals :
797  at_most_ones) {
798  tmp_to_clear.clear();
799 
800  // Compute how many variables we can fix with this at most one.
801  int num_fixable = 0;
802  for (const int literal : *literals) {
803  if (!RefIsPositive(literal)) continue;
804  if (context->IsFixed(literal)) continue;
805 
806  const int var = PositiveRef(literal);
807  const int rep = orbits[var];
808  if (rep == -1) continue;
809 
810  // We count all but the first one in each orbit.
811  if (tmp_sizes[rep] == 0) tmp_to_clear.push_back(rep);
812  if (tmp_sizes[rep] > 0) ++num_fixable;
813  tmp_sizes[rep]++;
814  }
815 
816  // Redo a pass to copy the intersection.
817  if (num_fixable > can_be_fixed_to_false.size()) {
818  distinguished_var = -1;
819  can_be_fixed_to_false.clear();
820  for (const int literal : *literals) {
821  if (!RefIsPositive(literal)) continue;
822  if (context->IsFixed(literal)) continue;
823 
824  const int var = PositiveRef(literal);
825  const int rep = orbits[var];
826  if (rep == -1) continue;
827  if (distinguished_var == -1 ||
828  orbit_sizes[rep] > orbit_sizes[orbits[distinguished_var]]) {
829  distinguished_var = var;
830  }
831 
832  // We push all but the first one in each orbit.
833  if (tmp_sizes[rep] == 0) can_be_fixed_to_false.push_back(var);
834  tmp_sizes[rep] = 0;
835  }
836  } else {
837  // Sparse clean up.
838  for (const int rep : tmp_to_clear) tmp_sizes[rep] = 0;
839  }
840  }
841 
842  if (can_be_fixed_to_false.size() > first_heuristic_size) {
843  SOLVER_LOG(
844  context->logger(),
845  "[Symmetry] Num fixable by intersecting at_most_one with orbits: ",
846  can_be_fixed_to_false.size(), " largest_orbit: ", max_orbit_size);
847  }
848  }
849 
850  // Orbitope approach.
851  //
852  // This is basically the same as the generic approach, but because of the
853  // extra structure, computing the orbit of any stabilizer subgroup is easy.
854  // We look for orbits intersecting at most one constraints, so we can break
855  // symmetry by fixing variables.
856  //
857  // TODO(user): The same effect could be achieved by adding symmetry breaking
858  // constraints of the form "a >= b " between Booleans and let the presolve do
859  // the reduction. This might be less code, but it is also less efficient.
860  // Similarly, when we cannot just fix variables to break symmetries, we could
861  // add these constraints, but it is unclear if we should do it all the time or
862  // not.
863  //
864  // TODO(user): code the generic approach with orbits and stabilizer.
865  std::vector<std::vector<int>> orbitope = BasicOrbitopeExtraction(generators);
866  if (!orbitope.empty()) {
867  SOLVER_LOG(context->logger(), "[Symmetry] Found orbitope of size ",
868  orbitope.size(), " x ", orbitope[0].size());
869  }
870 
871  // HACK for flatzinc wordpress* problem.
872  //
873  // If we have a large orbitope, with one objective term by column, we break
874  // the symmetry by ordering the objective terms. This usually increase
875  // drastically the objective lower bounds we can discover.
876  //
877  // TODO(user): generalize somehow. See if we can exploit this in
878  // lb_tree_search directly. We also have a lot more structure than just the
879  // objective can be ordered. Like if the objective is a max, we can still do
880  // that.
881  //
882  // TODO(user): Actually the constraint we add is really just breaking the
883  // orbitope symmetry on one line. But this line being the objective is key. We
884  // can also explicitly look for a full permutation group of the objective
885  // terms directly instead of finding the largest orbitope first.
886  if (!orbitope.empty() && context->working_model->has_objective()) {
887  const int num_objective_terms = context->ObjectiveMap().size();
888  if (orbitope[0].size() == num_objective_terms) {
889  int num_in_column = 0;
890  for (const std::vector<int>& row : orbitope) {
891  if (context->ObjectiveMap().contains(row[0])) ++num_in_column;
892  }
893  if (num_in_column == 1) {
894  context->WriteObjectiveToProto();
895  const auto& obj = context->working_model->objective();
896  CHECK_EQ(num_objective_terms, obj.vars().size());
897  for (int i = 1; i < num_objective_terms; ++i) {
898  auto* new_ct =
899  context->working_model->add_constraints()->mutable_linear();
900  new_ct->add_vars(obj.vars(i - 1));
901  new_ct->add_vars(obj.vars(i));
902  new_ct->add_coeffs(1);
903  new_ct->add_coeffs(-1);
904  new_ct->add_domain(0);
905  new_ct->add_domain(std::numeric_limits<int64_t>::max());
906  }
907  context->UpdateNewConstraintsVariableUsage();
908  context->UpdateRuleStats("symmetry: objective is one orbitope row.");
909  return true;
910  }
911  }
912  }
913 
914  // Supper simple heuristic to use the orbitope or not.
915  //
916  // In an orbitope with an at most one on each row, we can fix the upper right
917  // triangle. We could use a formula, but the loop is fast enough.
918  //
919  // TODO(user): Compute the stabilizer under the only non-fixed element and
920  // iterate!
921  int max_num_fixed_in_orbitope = 0;
922  if (!orbitope.empty()) {
923  const int num_rows = orbitope[0].size();
924  int size_left = num_rows;
925  for (int col = 0; size_left > 1 && col < orbitope.size(); ++col) {
926  max_num_fixed_in_orbitope += size_left - 1;
927  --size_left;
928  }
929  }
930  if (max_num_fixed_in_orbitope < can_be_fixed_to_false.size()) {
931  const int orbit_index = orbits[distinguished_var];
932  int num_in_orbit = 0;
933  for (int i = 0; i < can_be_fixed_to_false.size(); ++i) {
934  const int var = can_be_fixed_to_false[i];
935  if (orbits[var] == orbit_index) ++num_in_orbit;
936  context->UpdateRuleStats("symmetry: fixed to false in general orbit");
937  if (!context->SetLiteralToFalse(var)) return false;
938  }
939 
940  // Moreover, we can add the implication that in the orbit of
941  // distinguished_var, either everything is false, or var is at one.
942  if (orbit_sizes[orbit_index] > num_in_orbit + 1) {
943  context->UpdateRuleStats(
944  "symmetry: added orbit symmetry breaking implications");
945  auto* ct = context->working_model->add_constraints();
946  auto* bool_and = ct->mutable_bool_and();
947  ct->add_enforcement_literal(NegatedRef(distinguished_var));
948  for (int var = 0; var < num_vars; ++var) {
949  if (orbits[var] != orbit_index) continue;
950  if (var == distinguished_var) continue;
951  if (context->IsFixed(var)) continue;
952  bool_and->add_literals(NegatedRef(var));
953  }
954  context->UpdateNewConstraintsVariableUsage();
955  }
956  return true;
957  }
958  if (orbitope.empty()) return true;
959 
960  // This will always be kept all zero after usage.
961  std::vector<int> tmp_to_clear;
962  std::vector<int> tmp_sizes(num_vars, 0);
963  std::vector<int> tmp_num_positive(num_vars, 0);
964 
965  // TODO(user): The code below requires that no variable appears twice in the
966  // same at most one. In particular lit and not(lit) cannot appear in the same
967  // at most one.
968  for (const google::protobuf::RepeatedField<int32_t>* literals :
969  at_most_ones) {
970  for (const int lit : *literals) {
971  const int var = PositiveRef(lit);
972  CHECK_NE(tmp_sizes[var], 1);
973  tmp_sizes[var] = 1;
974  }
975  for (const int lit : *literals) {
976  tmp_sizes[PositiveRef(lit)] = 0;
977  }
978  }
979 
980  while (!orbitope.empty() && orbitope[0].size() > 1) {
981  const int num_cols = orbitope[0].size();
982  const std::vector<int> orbits = GetOrbitopeOrbits(num_vars, orbitope);
983 
984  // Because in the orbitope case, we have a full symmetry group of the
985  // columns, we can infer more than just using the orbits under a general
986  // permutation group. If an at most one contains two variables from the
987  // orbit, we can infer:
988  // 1/ If the two variables appear positively, then there is an at most one
989  // on the full orbit, and we can set n - 1 variables to zero to break the
990  // symmetry.
991  // 2/ If the two variables appear negatively, then the opposite situation
992  // arise and there is at most one zero on the orbit, we can set n - 1
993  // variables to one.
994  // 3/ If two literals of opposite sign appear, then the only possibility
995  // for the orbit are all at one or all at zero, thus we can mark all
996  // variables as equivalent.
997  //
998  // These property comes from the fact that when we permute a line of the
999  // orbitope in any way, then the position than ends up in the at most one
1000  // must never be both at one.
1001  //
1002  // Note that 1/ can be done without breaking any symmetry, but for 2/ and 3/
1003  // by choosing which variable is not fixed, we will break some symmetry, and
1004  // we will need to update the orbitope to stabilize this choice before
1005  // continuing.
1006  //
1007  // TODO(user): for 2/ and 3/ we could add an at most one constraint on the
1008  // full orbit if it is not already there!
1009  //
1010  // Note(user): On the miplib, only 1/ happens currently. Not sure with LNS
1011  // though.
1012  std::vector<bool> all_equivalent_rows(orbitope.size(), false);
1013 
1014  // The result described above can be generalized if an at most one intersect
1015  // many of the orbitope rows, each in at leat two positions. We will track
1016  // the set of best rows on which we have an at most one (or at most one
1017  // zero) on all their entries.
1018  bool at_most_one_in_best_rows; // The alternative is at most one zero.
1019  int64_t best_score = 0;
1020  std::vector<int> best_rows;
1021 
1022  std::vector<int> rows_in_at_most_one;
1023  for (const google::protobuf::RepeatedField<int32_t>* literals :
1024  at_most_ones) {
1025  tmp_to_clear.clear();
1026  for (const int literal : *literals) {
1027  if (context->IsFixed(literal)) continue;
1028  const int var = PositiveRef(literal);
1029  const int rep = orbits[var];
1030  if (rep == -1) continue;
1031 
1032  if (tmp_sizes[rep] == 0) tmp_to_clear.push_back(rep);
1033  tmp_sizes[rep]++;
1034  if (RefIsPositive(literal)) tmp_num_positive[rep]++;
1035  }
1036 
1037  int num_positive_direction = 0;
1038  int num_negative_direction = 0;
1039 
1040  // An at most one touching two positions in an orbitope row can possibly
1041  // be extended, depending if it has singleton intersection swith other
1042  // rows and where.
1043  bool possible_extension = false;
1044 
1045  rows_in_at_most_one.clear();
1046  for (const int row : tmp_to_clear) {
1047  const int size = tmp_sizes[row];
1048  const int num_positive = tmp_num_positive[row];
1049  const int num_negative = tmp_sizes[row] - tmp_num_positive[row];
1050  tmp_sizes[row] = 0;
1051  tmp_num_positive[row] = 0;
1052 
1053  if (num_positive > 1 && num_negative == 0) {
1054  if (size < num_cols) possible_extension = true;
1055  rows_in_at_most_one.push_back(row);
1056  ++num_positive_direction;
1057  } else if (num_positive == 0 && num_negative > 1) {
1058  if (size < num_cols) possible_extension = true;
1059  rows_in_at_most_one.push_back(row);
1060  ++num_negative_direction;
1061  } else if (num_positive > 0 && num_negative > 0) {
1062  all_equivalent_rows[row] = true;
1063  }
1064  }
1065 
1066  if (possible_extension) {
1067  context->UpdateRuleStats(
1068  "TODO symmetry: possible at most one extension.");
1069  }
1070 
1071  if (num_positive_direction > 0 && num_negative_direction > 0) {
1072  return context->NotifyThatModelIsUnsat("Symmetry and at most ones");
1073  }
1074  const bool direction = num_positive_direction > 0;
1075 
1076  // Because of symmetry, the choice of the column shouldn't matter (they
1077  // will all appear in the same number of constraints of the same types),
1078  // however we prefer to fix the variables that seems to touch more
1079  // constraints.
1080  //
1081  // TODO(user): maybe we should simplify the constraint using the variable
1082  // we fix before choosing the next row to break symmetry on. If there are
1083  // multiple row involved, we could also take the intersection instead of
1084  // probably counting the same constraints more than once.
1085  int64_t score = 0;
1086  for (const int row : rows_in_at_most_one) {
1087  score +=
1088  context->VarToConstraints(PositiveRef(orbitope[row][0])).size();
1089  }
1090  if (score > best_score) {
1091  at_most_one_in_best_rows = direction;
1092  best_score = score;
1093  best_rows = rows_in_at_most_one;
1094  }
1095  }
1096 
1097  // Mark all the equivalence.
1098  // Note that this operation do not change the symmetry group.
1099  //
1100  // TODO(user): We could remove these rows from the orbitope. Note that
1101  // currently this never happen on the miplib (maybe in LNS though).
1102  for (int i = 0; i < all_equivalent_rows.size(); ++i) {
1103  if (all_equivalent_rows[i]) {
1104  for (int j = 1; j < num_cols; ++j) {
1105  context->StoreBooleanEqualityRelation(orbitope[i][0], orbitope[i][j]);
1106  context->UpdateRuleStats("symmetry: all equivalent in orbit");
1107  if (context->ModelIsUnsat()) return false;
1108  }
1109  }
1110  }
1111 
1112  // Break the symmetry on our set of best rows by picking one columns
1113  // and setting all the other entries to zero or one. Note that the at most
1114  // one applies to all entries in all rows.
1115  //
1116  // TODO(user): We don't have any at most one relation on this orbitope,
1117  // but we could still add symmetry breaking inequality by picking any matrix
1118  // entry and making it the largest/lowest value on its row. This also work
1119  // for non-Booleans.
1120  if (best_score == 0) {
1121  context->UpdateRuleStats(
1122  "TODO symmetry: add symmetry breaking inequalities?");
1123  break;
1124  }
1125 
1126  // If our symmetry group is valid, they cannot be any variable already
1127  // fixed to one (or zero if !at_most_one_in_best_rows). Otherwise all would
1128  // be fixed to one and the problem would be unsat.
1129  for (const int i : best_rows) {
1130  for (int j = 0; j < num_cols; ++j) {
1131  const int var = orbitope[i][j];
1132  if ((at_most_one_in_best_rows && context->LiteralIsTrue(var)) ||
1133  (!at_most_one_in_best_rows && context->LiteralIsFalse(var))) {
1134  return context->NotifyThatModelIsUnsat("Symmetry and at most one");
1135  }
1136  }
1137  }
1138 
1139  // We have an at most one on a set of rows, we will pick a column, and set
1140  // all other entries on these rows to zero.
1141  //
1142  // TODO(user): All choices should be equivalent, but double check?
1143  const int best_col = 0;
1144  for (const int i : best_rows) {
1145  for (int j = 0; j < num_cols; ++j) {
1146  if (j == best_col) continue;
1147  const int var = orbitope[i][j];
1148  if (at_most_one_in_best_rows) {
1149  context->UpdateRuleStats("symmetry: fixed to false");
1150  if (!context->SetLiteralToFalse(var)) return false;
1151  } else {
1152  context->UpdateRuleStats("symmetry: fixed to true");
1153  if (!context->SetLiteralToTrue(var)) return false;
1154  }
1155  }
1156  }
1157 
1158  // Remove all best rows.
1159  for (const int i : best_rows) orbitope[i].clear();
1160  int new_size = 0;
1161  for (int i = 0; i < orbitope.size(); ++i) {
1162  if (!orbitope[i].empty()) orbitope[new_size++] = orbitope[i];
1163  }
1164  CHECK_LT(new_size, orbitope.size());
1165  orbitope.resize(new_size);
1166 
1167  // Remove best_col.
1168  for (int i = 0; i < orbitope.size(); ++i) {
1169  std::swap(orbitope[i][best_col], orbitope[i].back());
1170  orbitope[i].pop_back();
1171  }
1172  }
1173 
1174  // If we are left with a set of variable than can all be permuted, lets
1175  // break the symmetry by ordering them.
1176  if (orbitope.size() == 1) {
1177  const int num_cols = orbitope[0].size();
1178  for (int i = 0; i + 1 < num_cols; ++i) {
1179  // Add orbitope[0][i] >= orbitope[0][i+1].
1180  ConstraintProto* ct = context->working_model->add_constraints();
1181  ct->mutable_linear()->add_coeffs(1);
1182  ct->mutable_linear()->add_vars(orbitope[0][i]);
1183  ct->mutable_linear()->add_coeffs(-1);
1184  ct->mutable_linear()->add_vars(orbitope[0][i + 1]);
1185  ct->mutable_linear()->add_domain(0);
1186  ct->mutable_linear()->add_domain(std::numeric_limits<int64_t>::max());
1187  context->UpdateRuleStats("symmetry: added symmetry breaking inequality");
1188  }
1189  context->UpdateNewConstraintsVariableUsage();
1190  }
1191 
1192  return true;
1193 }
1194 
1195 } // namespace sat
1196 } // namespace operations_research
int64_t max
Definition: alldiff_cst.cc:140
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)
double GetElapsedDeterministicTime() const
Definition: time_limit.h:396
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
ModelSharedTimeLimit * time_limit
const Constraint * ct
int64_t value
IntVar * var
Definition: expr_array.cc:1874
absl::Status status
Definition: g_gurobi.cc:41
GRBmodel * model
GurobiMPCallbackContext * context
const bool DEBUG_MODE
Definition: macros.h:24
ColIndex col
Definition: markowitz.cc:186
RowIndex row
Definition: markowitz.cc:185
int64_t hash
Definition: matrix_utils.cc:63
void swap(IdMap< K, V > &a, IdMap< K, V > &b)
Definition: id_map.h:269
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)
bool LoadModelForProbing(PresolveContext *context, Model *local_model)
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:74
ListGraph Graph
Definition: graph.h:2398
Literal literal
Definition: optimization.cc:88
IntervalVar * interval
Definition: resource.cc:101
int64_t head
int nodes
std::vector< int >::const_iterator begin() const
#define SOLVER_LOG(logger,...)
Definition: util/logging.h:69
#define VLOG(verboselevel)
Definition: vlog.h:39