Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
cp_model_symmetries.cc
Go to the documentation of this file.
1// Copyright 2010-2025 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 <cstdlib>
21#include <functional>
22#include <limits>
23#include <memory>
24#include <utility>
25#include <vector>
26
27#include "absl/algorithm/container.h"
28#include "absl/container/btree_map.h"
29#include "absl/container/flat_hash_map.h"
30#include "absl/container/flat_hash_set.h"
31#include "absl/log/check.h"
32#include "absl/meta/type_traits.h"
33#include "absl/status/status.h"
34#include "absl/strings/str_cat.h"
35#include "absl/strings/str_join.h"
36#include "absl/types/span.h"
37#include "google/protobuf/message.h"
41#include "ortools/base/hash.h"
43#include "ortools/graph/graph.h"
48#include "ortools/sat/model.h"
55#include "ortools/sat/util.h"
61
62namespace operations_research {
63namespace sat {
64
65namespace {
66struct VectorHash {
67 std::size_t operator()(absl::Span<const int64_t> values) const {
68 size_t hash = 0;
69 for (const int64_t value : values) {
70 hash = util_hash::Hash(value, hash);
71 }
72 return hash;
73 }
74};
75
76struct NodeExprCompare {
77 bool operator()(const LinearExpressionProto& a,
78 const LinearExpressionProto& b) const {
79 if (a.offset() != b.offset()) return a.offset() < b.offset();
80 if (a.vars_size() != b.vars_size()) return a.vars_size() < b.vars_size();
81 for (int i = 0; i < a.vars_size(); ++i) {
82 if (a.vars(i) != b.vars(i)) return a.vars(i) < b.vars(i);
83 if (a.coeffs(i) != b.coeffs(i)) return a.coeffs(i) < b.coeffs(i);
84 }
85 return false;
86 }
87};
88
89// A simple class to generate equivalence class number for
90// GenerateGraphForSymmetryDetection().
91class IdGenerator {
92 public:
93 IdGenerator() = default;
94
95 // If the color was never seen before, then generate a new id, otherwise
96 // return the previously generated id.
97 int GetId(const std::vector<int64_t>& color) {
98 // Do not use try_emplace. It breaks with gcc13 on or-tools.
99 return id_map_.insert({color, id_map_.size()}).first->second;
100 }
101
102 int NextFreeId() const { return id_map_.size(); }
103
104 private:
105 absl::flat_hash_map<std::vector<int64_t>, int, VectorHash> id_map_;
106};
107
108// Appends values in `repeated_field` to `vector`.
109//
110// We use a template as proto int64_t != C++ int64_t in open source.
111template <typename FieldInt64Type>
112void Append(
113 const google::protobuf::RepeatedField<FieldInt64Type>& repeated_field,
114 std::vector<int64_t>* vector) {
115 CHECK(vector != nullptr);
116 for (const FieldInt64Type value : repeated_field) {
117 vector->push_back(value);
118 }
119}
120
121bool IsIntervalFixedSize(const IntervalConstraintProto& interval) {
122 if (!interval.size().vars().empty()) {
123 return false;
124 }
125 if (interval.start().vars().size() != interval.end().vars().size()) {
126 return false;
127 }
128 for (int i = 0; i < interval.start().vars().size(); ++i) {
129 if (interval.start().coeffs(i) != interval.end().coeffs(i)) {
130 return false;
131 }
132 if (interval.start().vars(i) != interval.end().vars(i)) {
133 return false;
134 }
135 }
136 if (interval.end().offset() !=
137 interval.start().offset() + interval.size().offset()) {
138 return false;
139 }
140 return true;
141}
142
143// Returns a graph whose automorphisms can be mapped back to the symmetries of
144// the model described in the given CpModelProto.
145//
146// Any permutation of the graph that respects the initial_equivalence_classes
147// output can be mapped to a symmetry of the given problem simply by taking its
148// restriction on the first num_variables nodes and interpreting its index as a
149// variable index. In a sense, a node with a low enough index #i is in
150// one-to-one correspondence with the variable #i (using the index
151// representation of variables).
152//
153// The format of the initial_equivalence_classes is the same as the one
154// described in GraphSymmetryFinder::FindSymmetries(). The classes must be dense
155// in [0, num_classes) and any symmetry will only map nodes with the same class
156// between each other.
157template <typename Graph>
158std::unique_ptr<Graph> GenerateGraphForSymmetryDetection(
159 const CpModelProto& problem, std::vector<int>* initial_equivalence_classes,
160 SolverLogger* logger) {
161 CHECK(initial_equivalence_classes != nullptr);
162
163 const int num_variables = problem.variables_size();
164 auto graph = std::make_unique<Graph>();
165
166 // Each node will be created with a given color. Two nodes of different color
167 // can never be send one into another by a symmetry. The first element of
168 // the color vector will always be the NodeType.
169 //
170 // TODO(user): Using a full int64_t for storing 3 values is not great. We
171 // can optimize this at the price of a bit more code.
172 enum NodeType {
173 VARIABLE_NODE,
174 VAR_COEFFICIENT_NODE,
175 CONSTRAINT_NODE,
176 VAR_LIN_EXPR_NODE,
177 };
178 IdGenerator color_id_generator;
179 initial_equivalence_classes->clear();
180 auto new_node_from_id = [&initial_equivalence_classes, &graph](int color_id) {
181 // Since we add nodes one by one, initial_equivalence_classes->size() gives
182 // the number of nodes at any point, which we use as the next node index.
183 const int node = initial_equivalence_classes->size();
184 initial_equivalence_classes->push_back(color_id);
185
186 // In some corner cases, we create a node but never uses it. We still
187 // want it to be there.
188 graph->AddNode(node);
189 return node;
190 };
191 auto new_node = [&new_node_from_id,
192 &color_id_generator](const std::vector<int64_t>& color) {
193 return new_node_from_id(color_id_generator.GetId(color));
194 };
195 // For two variables to be in the same equivalence class, they need to have
196 // the same objective coefficient, and the same possible bounds.
197 //
198 // TODO(user): We could ignore the objective coefficients, and just make sure
199 // that when we break symmetry amongst variables, we choose the possibility
200 // with the smallest cost?
201 std::vector<int64_t> objective_by_var(num_variables, 0);
202 for (int i = 0; i < problem.objective().vars_size(); ++i) {
203 const int ref = problem.objective().vars(i);
204 const int var = PositiveRef(ref);
205 const int64_t coeff = problem.objective().coeffs(i);
206 objective_by_var[var] = RefIsPositive(ref) ? coeff : -coeff;
207 }
208
209 // Create one node for each variable. Note that the code rely on the fact that
210 // the index of a VARIABLE_NODE type is the same as the variable index.
211 std::vector<int64_t> tmp_color;
212 for (int v = 0; v < num_variables; ++v) {
213 tmp_color = {VARIABLE_NODE, objective_by_var[v]};
214 Append(problem.variables(v).domain(), &tmp_color);
215 CHECK_EQ(v, new_node(tmp_color));
216 }
217
218 const int color_id_for_coeff_one =
219 color_id_generator.GetId({VAR_COEFFICIENT_NODE, 1});
220 const int color_id_for_coeff_minus_one =
221 color_id_generator.GetId({VAR_COEFFICIENT_NODE, -1});
222
223 // We will lazily create "coefficient nodes" that correspond to a variable
224 // with a given coefficient.
225 absl::flat_hash_map<std::pair<int64_t, int64_t>, int> coefficient_nodes;
226 auto get_coefficient_node =
227 [&new_node_from_id, &graph, &coefficient_nodes, &color_id_generator,
228 &tmp_color, color_id_for_coeff_minus_one](int var, int64_t coeff) {
229 const int var_node = var;
230 DCHECK(RefIsPositive(var));
231
232 // For a coefficient of one, which are the most common, we can optimize
233 // the size of the graph by omitting the coefficient node altogether and
234 // using directly the var_node in this case.
235 if (coeff == 1) return var_node;
236
237 const auto insert =
238 coefficient_nodes.insert({std::make_pair(var, coeff), 0});
239 if (!insert.second) return insert.first->second;
240
241 int color_id;
242 // Because -1 is really common (also used for negated literal), we have
243 // a fast path for it.
244 if (coeff == -1) {
245 color_id = color_id_for_coeff_minus_one;
246 } else {
247 tmp_color = {VAR_COEFFICIENT_NODE, coeff};
248 color_id = color_id_generator.GetId(tmp_color);
249 }
250 const int secondary_node = new_node_from_id(color_id);
251 graph->AddArc(var_node, secondary_node);
252 insert.first->second = secondary_node;
253 return secondary_node;
254 };
255
256 // For a literal we use the same as a coefficient 1 or -1. We can do that
257 // because literal and (var, coefficient) never appear together in the same
258 // constraint.
259 auto get_literal_node = [&get_coefficient_node](int ref) {
260 return get_coefficient_node(PositiveRef(ref), RefIsPositive(ref) ? 1 : -1);
261 };
262
263 // Because the implications can be numerous, we encode them without
264 // constraints node by using an arc from the lhs to the rhs. Note that we also
265 // always add the other direction. We use a set to remove duplicates both for
266 // efficiency and to not artificially break symmetries by using multi-arcs.
267 //
268 // Tricky: We cannot use the base variable node here to avoid situation like
269 // both a variable a and b having the same children (not(a), not(b)) in the
270 // graph. Because if that happen, we can permute a and b without permuting
271 // their associated not(a) and not(b) node! To be sure this cannot happen, a
272 // variable node can not have as children a VAR_COEFFICIENT_NODE from another
273 // node. This makes sure that any permutation that touch a variable, must
274 // permute its coefficient nodes accordingly.
275 absl::flat_hash_set<std::pair<int, int>> implications;
276 auto get_implication_node = [&new_node_from_id, &graph, &coefficient_nodes,
277 color_id_for_coeff_one,
278 color_id_for_coeff_minus_one](int ref) {
279 const int var = PositiveRef(ref);
280 const int64_t coeff = RefIsPositive(ref) ? 1 : -1;
281 const auto insert =
282 coefficient_nodes.insert({std::make_pair(var, coeff), 0});
283 if (!insert.second) return insert.first->second;
284 const int secondary_node = new_node_from_id(
285 coeff == 1 ? color_id_for_coeff_one : color_id_for_coeff_minus_one);
286 graph->AddArc(var, secondary_node);
287 insert.first->second = secondary_node;
288 return secondary_node;
289 };
290 auto add_implication = [&get_implication_node, &graph, &implications](
291 int ref_a, int ref_b) {
292 const auto insert = implications.insert({ref_a, ref_b});
293 if (!insert.second) return;
294 graph->AddArc(get_implication_node(ref_a), get_implication_node(ref_b));
295
296 // Always add the other side.
297 implications.insert({NegatedRef(ref_b), NegatedRef(ref_a)});
298 graph->AddArc(get_implication_node(NegatedRef(ref_b)),
299 get_implication_node(NegatedRef(ref_a)));
300 };
301
302 auto make_linear_expr_node = [&new_node, &graph, &get_coefficient_node](
303 const LinearExpressionProto& expr,
304 const std::vector<int64_t>& color) {
305 std::vector<int64_t> local_color = color;
306 local_color.push_back(expr.offset());
307 const int local_node = new_node(local_color);
308
309 for (int i = 0; i < expr.vars().size(); ++i) {
310 const int ref = expr.vars(i);
311 const int var_node = PositiveRef(ref);
312 const int64_t coeff =
313 RefIsPositive(ref) ? expr.coeffs(i) : -expr.coeffs(i);
314 graph->AddArc(get_coefficient_node(var_node, coeff), local_node);
315 }
316 return local_node;
317 };
318
319 absl::btree_map<LinearExpressionProto, int, NodeExprCompare> expr_nodes;
320 auto shared_linear_expr_node =
321 [&make_linear_expr_node, &expr_nodes](const LinearExpressionProto& expr) {
322 const auto [it, inserted] = expr_nodes.insert({expr, 0});
323 if (inserted) {
324 const std::vector<int64_t> local_color = {VAR_LIN_EXPR_NODE,
325 expr.offset()};
326 it->second = make_linear_expr_node(expr, local_color);
327 }
328 return it->second;
329 };
330
331 // We need to keep track of this for scheduling constraints.
332 absl::flat_hash_map<int, int> interval_constraint_index_to_node;
333
334 // Add constraints to the graph.
335 for (int constraint_index = 0; constraint_index < problem.constraints_size();
336 ++constraint_index) {
337 const ConstraintProto& constraint = problem.constraints(constraint_index);
338 const int constraint_node = initial_equivalence_classes->size();
339 std::vector<int64_t> color = {CONSTRAINT_NODE,
340 constraint.constraint_case()};
341
342 switch (constraint.constraint_case()) {
344 // TODO(user): We continue for the corner case of a constraint not set
345 // with enforcement literal. We should probably clear this constraint
346 // before reaching here.
347 continue;
349 // TODO(user): We can use the same trick as for the implications to
350 // encode relations of the form coeff * var_a <= coeff * var_b without
351 // creating a constraint node by directly adding an arc between the two
352 // var coefficient nodes.
353 Append(constraint.linear().domain(), &color);
354 CHECK_EQ(constraint_node, new_node(color));
355 for (int i = 0; i < constraint.linear().vars_size(); ++i) {
356 const int ref = constraint.linear().vars(i);
357 const int variable_node = PositiveRef(ref);
358 const int64_t coeff = RefIsPositive(ref)
359 ? constraint.linear().coeffs(i)
360 : -constraint.linear().coeffs(i);
361 graph->AddArc(get_coefficient_node(variable_node, coeff),
362 constraint_node);
363 }
364 break;
365 }
367 CHECK_EQ(constraint_node, new_node(color));
368 for (const LinearExpressionProto& expr :
369 constraint.all_diff().exprs()) {
370 graph->AddArc(shared_linear_expr_node(expr), constraint_node);
371 }
372 break;
373 }
375 CHECK_EQ(constraint_node, new_node(color));
376 for (const int ref : constraint.bool_or().literals()) {
377 graph->AddArc(get_literal_node(ref), constraint_node);
378 }
379 break;
380 }
382 if (constraint.at_most_one().literals().size() == 2 &&
383 constraint.enforcement_literal().empty()) {
384 // Treat it as an implication to avoid creating a node.
385 add_implication(constraint.at_most_one().literals(0),
386 NegatedRef(constraint.at_most_one().literals(1)));
387 break;
388 }
389
390 CHECK_EQ(constraint_node, new_node(color));
391 for (const int ref : constraint.at_most_one().literals()) {
392 graph->AddArc(get_literal_node(ref), constraint_node);
393 }
394 break;
395 }
397 CHECK_EQ(constraint_node, new_node(color));
398 for (const int ref : constraint.exactly_one().literals()) {
399 graph->AddArc(get_literal_node(ref), constraint_node);
400 }
401 break;
402 }
404 CHECK_EQ(constraint_node, new_node(color));
405 for (const int ref : constraint.bool_xor().literals()) {
406 graph->AddArc(get_literal_node(ref), constraint_node);
407 }
408 break;
409 }
411 if (constraint.enforcement_literal_size() > 1) {
412 CHECK_EQ(constraint_node, new_node(color));
413 for (const int ref : constraint.bool_and().literals()) {
414 graph->AddArc(get_literal_node(ref), constraint_node);
415 }
416 break;
417 }
418
419 CHECK_EQ(constraint.enforcement_literal_size(), 1);
420 const int ref_a = constraint.enforcement_literal(0);
421 for (const int ref_b : constraint.bool_and().literals()) {
422 add_implication(ref_a, ref_b);
423 }
424 break;
425 }
427 const LinearExpressionProto& target_expr =
428 constraint.lin_max().target();
429
430 const int target_node = make_linear_expr_node(target_expr, color);
431 CHECK_EQ(constraint_node, target_node);
432
433 for (int i = 0; i < constraint.lin_max().exprs_size(); ++i) {
434 const LinearExpressionProto& expr = constraint.lin_max().exprs(i);
435 graph->AddArc(shared_linear_expr_node(expr), target_node);
436 }
437
438 break;
439 }
441 static constexpr int kFixedIntervalColor = 0;
442 static constexpr int kNonFixedIntervalColor = 1;
443 if (IsIntervalFixedSize(constraint.interval())) {
444 std::vector<int64_t> local_color = color;
445 local_color.push_back(kFixedIntervalColor);
446 local_color.push_back(constraint.interval().size().offset());
447 const int full_node =
448 make_linear_expr_node(constraint.interval().start(), local_color);
449 CHECK_EQ(full_node, constraint_node);
450 } else {
451 // We create 3 constraint nodes (for start, size and end) including
452 // the offset. We connect these to their terms like for a linear
453 // constraint.
454 std::vector<int64_t> local_color = color;
455 local_color.push_back(kNonFixedIntervalColor);
456
457 local_color.push_back(0);
458 const int start_node =
459 make_linear_expr_node(constraint.interval().start(), local_color);
460 local_color.pop_back();
461 CHECK_EQ(start_node, constraint_node);
462
463 // We can use a shared node for one of the three. Let's use the size
464 // since it has the most chance of being reused.
465 const int size_node =
466 shared_linear_expr_node(constraint.interval().size());
467
468 local_color.push_back(1);
469 const int end_node =
470 make_linear_expr_node(constraint.interval().end(), local_color);
471 local_color.pop_back();
472
473 // Make sure that if one node is mapped to another one, its other two
474 // components are the same.
475 graph->AddArc(start_node, end_node);
476 graph->AddArc(end_node, size_node);
477 }
478 interval_constraint_index_to_node[constraint_index] = constraint_node;
479 break;
480 }
482 // Note(user): This require that intervals appear before they are used.
483 // We currently enforce this at validation, otherwise we need two passes
484 // here and in a bunch of other places.
485 CHECK_EQ(constraint_node, new_node(color));
486 for (const int interval : constraint.no_overlap().intervals()) {
487 graph->AddArc(interval_constraint_index_to_node.at(interval),
488 constraint_node);
489 }
490 break;
491 }
493 // Note(user): This require that intervals appear before they are used.
494 // We currently enforce this at validation, otherwise we need two passes
495 // here and in a bunch of other places.
496 CHECK_EQ(constraint_node, new_node(color));
497 std::vector<int64_t> local_color = color;
498 local_color.push_back(0);
499 const int size = constraint.no_overlap_2d().x_intervals().size();
500 const int node_x = new_node(local_color);
501 const int node_y = new_node(local_color);
502 local_color.pop_back();
503 graph->AddArc(constraint_node, node_x);
504 graph->AddArc(constraint_node, node_y);
505 local_color.push_back(1);
506 for (int i = 0; i < size; ++i) {
507 const int box_node = new_node(local_color);
508 graph->AddArc(box_node, constraint_node);
509 const int x = constraint.no_overlap_2d().x_intervals(i);
510 const int y = constraint.no_overlap_2d().y_intervals(i);
511 graph->AddArc(interval_constraint_index_to_node.at(x), node_x);
512 graph->AddArc(interval_constraint_index_to_node.at(x), box_node);
513 graph->AddArc(interval_constraint_index_to_node.at(y), node_y);
514 graph->AddArc(interval_constraint_index_to_node.at(y), box_node);
515 }
516 break;
517 }
519 // Note(user): This require that intervals appear before they are used.
520 // We currently enforce this at validation, otherwise we need two passes
521 // here and in a bunch of other places.
522 const CumulativeConstraintProto& ct = constraint.cumulative();
523 std::vector<int64_t> capacity_color = color;
524 capacity_color.push_back(0);
525 CHECK_EQ(constraint_node, new_node(capacity_color));
526 graph->AddArc(constraint_node,
527 make_linear_expr_node(ct.capacity(), capacity_color));
528
529 std::vector<int64_t> task_color = color;
530 task_color.push_back(1);
531 for (int i = 0; i < ct.intervals().size(); ++i) {
532 const int task_node =
533 make_linear_expr_node(ct.demands(i), task_color);
534 graph->AddArc(task_node, constraint_node);
535 graph->AddArc(task_node,
536 interval_constraint_index_to_node.at(ct.intervals(i)));
537 }
538 break;
539 }
541 // Note that this implementation will generate the same graph for a
542 // circuit constraint with two disconnected components and two circuit
543 // constraints with one component each, unless there is an enforcement
544 // literal.
545 const int num_arcs = constraint.circuit().literals().size();
546 absl::flat_hash_map<int, int> circuit_node_to_symmetry_node;
547 if (!constraint.enforcement_literal().empty()) {
548 CHECK_EQ(constraint_node, new_node(color));
549 }
550 std::vector<int64_t> arc_color = color;
551 arc_color.push_back(1);
552 for (int i = 0; i < num_arcs; ++i) {
553 const int literal = constraint.circuit().literals(i);
554 const int tail = constraint.circuit().tails(i);
555 const int head = constraint.circuit().heads(i);
556 const int arc_node = new_node(arc_color);
557 if (!circuit_node_to_symmetry_node.contains(head)) {
558 circuit_node_to_symmetry_node[head] = new_node(color);
559 }
560 const int head_node = circuit_node_to_symmetry_node[head];
561 if (!circuit_node_to_symmetry_node.contains(tail)) {
562 circuit_node_to_symmetry_node[tail] = new_node(color);
563 }
564 const int tail_node = circuit_node_to_symmetry_node[tail];
565 // To make the graph directed, we add two arcs on the head but not on
566 // the tail.
567 if (!constraint.enforcement_literal().empty()) {
568 graph->AddArc(constraint_node, arc_node);
569 }
570 graph->AddArc(tail_node, arc_node);
571 graph->AddArc(arc_node, get_literal_node(literal));
572 graph->AddArc(arc_node, head_node);
573 }
574 break;
575 }
576 default: {
577 // If the model contains any non-supported constraints, return an empty
578 // graph.
579 //
580 // TODO(user): support other types of constraints. Or at least, we
581 // could associate to them an unique node so that their variables can
582 // appear in no symmetry.
583 VLOG(1) << "Unsupported constraint type "
584 << ConstraintCaseName(constraint.constraint_case());
585 return nullptr;
586 }
587 }
588
589 // For enforcement, we use a similar trick than for the implications.
590 // Because all our constraint arcs are in the direction var_node to
591 // constraint_node, we just use the reverse direction for the enforcement
592 // part. This way we can reuse the same get_literal_node() function.
593 if (constraint.constraint_case() != ConstraintProto::kBoolAnd ||
594 constraint.enforcement_literal().size() > 1) {
595 if (!constraint.enforcement_literal().empty()) {
596 CHECK_LT(constraint_node, initial_equivalence_classes->size());
597 }
598 for (const int ref : constraint.enforcement_literal()) {
599 graph->AddArc(constraint_node, get_literal_node(ref));
600 }
601 }
602 }
603
604 graph->Build();
605 DCHECK_EQ(graph->num_nodes(), initial_equivalence_classes->size());
606
607 // TODO(user): The symmetry code does not officially support multi-arcs. And
608 // we shouldn't have any as long as there is no duplicates variable in our
609 // constraints (but of course, we can't always guarantee that). That said,
610 // because the symmetry code really only look at the degree, it works as long
611 // as the maximum degree is bounded by num_nodes.
612 const int num_nodes = graph->num_nodes();
613 std::vector<int> in_degree(num_nodes, 0);
614 std::vector<int> out_degree(num_nodes, 0);
615 for (int i = 0; i < num_nodes; ++i) {
616 out_degree[i] = graph->OutDegree(i);
617 for (const int head : (*graph)[i]) {
618 in_degree[head]++;
619 }
620 }
621 for (int i = 0; i < num_nodes; ++i) {
622 if (in_degree[i] >= num_nodes || out_degree[i] >= num_nodes) {
623 SOLVER_LOG(logger, "[Symmetry] Too many multi-arcs in symmetry code.");
624 return nullptr;
625 }
626 }
627
628 // Because this code is running during presolve, a lot a variable might have
629 // no edges. We do not want to detect symmetries between these.
630 //
631 // Note that this code forces us to "densify" the ids afterwards because the
632 // symmetry detection code relies on that.
633 //
634 // TODO(user): It will probably be more efficient to not even create these
635 // nodes, but we will need a mapping to know the variable <-> node index.
636 int next_id = color_id_generator.NextFreeId();
637 for (int i = 0; i < num_variables; ++i) {
638 if ((*graph)[i].empty()) {
639 (*initial_equivalence_classes)[i] = next_id++;
640 }
641 }
642
643 // Densify ids.
644 int id = 0;
645 std::vector<int> mapping(next_id, -1);
646 for (int& ref : *initial_equivalence_classes) {
647 if (mapping[ref] == -1) {
648 ref = mapping[ref] = id++;
649 } else {
650 ref = mapping[ref];
651 }
652 }
653
654 return graph;
655}
656} // namespace
657
659 const SatParameters& params, const CpModelProto& problem,
660 std::vector<std::unique_ptr<SparsePermutation>>* generators,
661 SolverLogger* logger, TimeLimit* solver_time_limit) {
662 CHECK(generators != nullptr);
663 generators->clear();
664
665 if (params.symmetry_level() < 3 && problem.variables().size() > 1e6 &&
666 problem.constraints().size() > 1e6) {
667 SOLVER_LOG(logger,
668 "[Symmetry] Problem too large. Skipping. You can use "
669 "symmetry_level:3 or more to force it.");
670 return;
671 }
672
674
675 std::vector<int> equivalence_classes;
676 std::unique_ptr<Graph> graph(GenerateGraphForSymmetryDetection<Graph>(
677 problem, &equivalence_classes, logger));
678 if (graph == nullptr) return;
679
680 SOLVER_LOG(logger, "[Symmetry] Graph for symmetry has ",
681 FormatCounter(graph->num_nodes()), " nodes and ",
682 FormatCounter(graph->num_arcs()), " arcs.");
683 if (graph->num_nodes() == 0) return;
684
685 if (params.symmetry_level() < 3 && graph->num_nodes() > 1e6 &&
686 graph->num_arcs() > 1e6) {
687 SOLVER_LOG(logger,
688 "[Symmetry] Graph too large. Skipping. You can use "
689 "symmetry_level:3 or more to force it.");
690 return;
691 }
692
693 std::unique_ptr<TimeLimit> time_limit = TimeLimit::FromDeterministicTime(
695 time_limit->MergeWithGlobalTimeLimit(solver_time_limit);
696 GraphSymmetryFinder symmetry_finder(*graph, /*is_undirected=*/false);
697 std::vector<int> factorized_automorphism_group_size;
698 const absl::Status status = symmetry_finder.FindSymmetries(
699 &equivalence_classes, generators, &factorized_automorphism_group_size,
700 time_limit.get());
701
702 // TODO(user): Change the API to not return an error when the time limit is
703 // reached.
704 if (absl::IsDeadlineExceeded(status)) {
705 SOLVER_LOG(logger, "[Symmetry] Time limit reached: ", status.message());
706 } else if (!status.ok()) {
707 SOLVER_LOG(logger,
708 "[Symmetry] GraphSymmetryFinder error: ", status.message());
709 }
710
711 // Remove from the permutations the part not concerning the variables.
712 // Note that some permutations may become empty, which means that we had
713 // duplicate constraints.
714 double average_support_size = 0.0;
715 int num_generators = 0;
716 int num_duplicate_constraints = 0;
717 for (int i = 0; i < generators->size(); ++i) {
718 SparsePermutation* permutation = (*generators)[i].get();
719 std::vector<int> to_delete;
720 for (int j = 0; j < permutation->NumCycles(); ++j) {
721 // Because variable nodes are in a separate equivalence class than any
722 // other node, a cycle can either contain only variable nodes or none, so
723 // we just need to check one element of the cycle.
724 if (*(permutation->Cycle(j).begin()) >= problem.variables_size()) {
725 to_delete.push_back(j);
726 if (DEBUG_MODE) {
727 // Verify that the cycle's entire support does not touch any variable.
728 for (const int node : permutation->Cycle(j)) {
729 DCHECK_GE(node, problem.variables_size());
730 }
731 }
732 }
733 }
734
735 permutation->RemoveCycles(to_delete);
736 if (!permutation->Support().empty()) {
737 average_support_size += permutation->Support().size();
738 swap((*generators)[num_generators], (*generators)[i]);
739 ++num_generators;
740 } else {
741 ++num_duplicate_constraints;
742 }
743 }
744 generators->resize(num_generators);
745 SOLVER_LOG(logger, "[Symmetry] Symmetry computation done. time: ",
746 time_limit->GetElapsedTime(),
747 " dtime: ", time_limit->GetElapsedDeterministicTime());
748 if (num_generators > 0) {
749 average_support_size /= num_generators;
750 SOLVER_LOG(logger, "[Symmetry] #generators: ", num_generators,
751 ", average support size: ", average_support_size);
752 if (num_duplicate_constraints > 0) {
753 SOLVER_LOG(logger, "[Symmetry] The model contains ",
754 num_duplicate_constraints, " duplicate constraints !");
755 }
756 }
757}
758
759namespace {
760
761void LogOrbitInformation(absl::Span<const int> var_to_orbit_index,
762 SolverLogger* logger) {
763 if (logger == nullptr || !logger->LoggingIsEnabled()) return;
764
765 int num_touched_vars = 0;
766 std::vector<int> orbit_sizes;
767 for (int var = 0; var < var_to_orbit_index.size(); ++var) {
768 const int rep = var_to_orbit_index[var];
769 if (rep == -1) continue;
770 if (rep >= orbit_sizes.size()) orbit_sizes.resize(rep + 1, 0);
771 ++num_touched_vars;
772 orbit_sizes[rep]++;
773 }
774 std::sort(orbit_sizes.begin(), orbit_sizes.end(), std::greater<int>());
775 const int num_orbits = orbit_sizes.size();
776 if (num_orbits > 10) orbit_sizes.resize(10);
777 SOLVER_LOG(logger, "[Symmetry] ", num_orbits, " orbits on ", num_touched_vars,
778 " variables with sizes: ", absl::StrJoin(orbit_sizes, ","),
779 (num_orbits > orbit_sizes.size() ? ",..." : ""));
780}
781
782} // namespace
783
785 CpModelProto* proto, SolverLogger* logger,
786 TimeLimit* time_limit) {
787 SymmetryProto* symmetry = proto->mutable_symmetry();
788 symmetry->Clear();
789
790 std::vector<std::unique_ptr<SparsePermutation>> generators;
791 FindCpModelSymmetries(params, *proto, &generators, logger, time_limit);
792 if (generators.empty()) {
793 proto->clear_symmetry();
794 return;
795 }
796
797 // Log orbit information.
798 //
799 // TODO(user): It might be nice to just add this to the proto rather than
800 // re-reading the generators and recomputing this in a few places.
801 const int num_vars = proto->variables().size();
802 const std::vector<int> orbits = GetOrbits(num_vars, generators);
803 LogOrbitInformation(orbits, logger);
804
805 for (const std::unique_ptr<SparsePermutation>& perm : generators) {
806 SparsePermutationProto* perm_proto = symmetry->add_permutations();
807 const int num_cycle = perm->NumCycles();
808 for (int i = 0; i < num_cycle; ++i) {
809 const int old_size = perm_proto->support().size();
810 for (const int var : perm->Cycle(i)) {
811 perm_proto->add_support(var);
812 }
813 perm_proto->add_cycle_sizes(perm_proto->support().size() - old_size);
814 }
815 }
816
817 std::vector<std::vector<int>> orbitope = BasicOrbitopeExtraction(generators);
818 if (orbitope.empty()) return;
819 SOLVER_LOG(logger, "[Symmetry] Found orbitope of size ", orbitope.size(),
820 " x ", orbitope[0].size());
821 DenseMatrixProto* matrix = symmetry->add_orbitopes();
822 matrix->set_num_rows(orbitope.size());
823 matrix->set_num_cols(orbitope[0].size());
824 for (const std::vector<int>& row : orbitope) {
825 for (const int entry : row) {
826 matrix->add_entries(entry);
827 }
828 }
829}
830
831namespace {
832
833// Given one Boolean orbit under symmetry, if there is a Boolean at one in this
834// orbit, then we can always move it to a fixed position (i.e. the given
835// variable var). Moreover, any variable implied to zero in this orbit by var
836// being at one can be fixed to zero. This is because, after symmetry breaking,
837// either var is one, or all the orbit is zero. We also add implications to
838// enforce this fact, but this is not done in this function.
839//
840// TODO(user): If an exactly one / at least one is included in the orbit, then
841// we can set a given variable to one directly. We can also detect this by
842// trying to propagate the orbit to all false.
843//
844// TODO(user): The same reasonning can be done if fixing the variable to
845// zero leads to many propagations at one. For general variables, we might be
846// able to do something too.
847void OrbitAndPropagation(absl::Span<const int> orbits, int var,
848 std::vector<int>* can_be_fixed_to_false,
849 PresolveContext* context) {
850 // Note that if a variable is fixed in the orbit, then everything should be
851 // fixed.
852 if (context->IsFixed(var)) return;
853 if (!context->CanBeUsedAsLiteral(var)) return;
854
855 // Lets fix var to true and see what is propagated.
856 //
857 // TODO(user): Ideally we should have a propagator ready for this. Right now
858 // we load the full model if we detected symmetries. We should really combine
859 // this with probing even though this is "breaking" the symmetry so it cannot
860 // be applied as generally as probing.
861 //
862 // TODO(user): Note that probing can also benefit from symmetry, since in
863 // each orbit, only one variable needs to be probed, and any conclusion can
864 // be duplicated to all the variables from an orbit! It is also why we just
865 // need to propagate one variable here.
866 Model model;
867 if (!LoadModelForProbing(context, &model)) return;
868
869 auto* sat_solver = model.GetOrCreate<SatSolver>();
870 auto* mapping = model.GetOrCreate<CpModelMapping>();
871 const Literal to_propagate = mapping->Literal(var);
872
873 const VariablesAssignment& assignment = sat_solver->Assignment();
874 if (assignment.LiteralIsAssigned(to_propagate)) return;
875 sat_solver->EnqueueDecisionAndBackjumpOnConflict(to_propagate);
876 if (sat_solver->CurrentDecisionLevel() != 1) return;
877
878 // We can fix to false any variable that is in the orbit and set to false!
879 can_be_fixed_to_false->clear();
880 int orbit_size = 0;
881 const int orbit_index = orbits[var];
882 const int num_variables = orbits.size();
883 for (int var = 0; var < num_variables; ++var) {
884 if (orbits[var] != orbit_index) continue;
885 ++orbit_size;
886
887 // By symmetry since same orbit.
888 DCHECK(!context->IsFixed(var));
889 DCHECK(context->CanBeUsedAsLiteral(var));
890
891 if (assignment.LiteralIsFalse(mapping->Literal(var))) {
892 can_be_fixed_to_false->push_back(var);
893 }
894 }
895 if (!can_be_fixed_to_false->empty()) {
896 SOLVER_LOG(context->logger(),
897 "[Symmetry] Num fixable by binary propagation in orbit: ",
898 can_be_fixed_to_false->size(), " / ", orbit_size);
899 }
900}
901
902std::vector<int64_t> BuildInequalityCoeffsForOrbitope(
903 absl::Span<const int64_t> maximum_values, int64_t max_linear_size,
904 bool* is_approximated) {
905 std::vector<int64_t> out(maximum_values.size());
906 int64_t range_product = 1;
907 uint64_t greatest_coeff = 0;
908 for (int i = 0; i < maximum_values.size(); ++i) {
909 out[i] = range_product;
910 greatest_coeff =
911 std::max(greatest_coeff, static_cast<uint64_t>(maximum_values[i]));
912 range_product = CapProd(range_product, 1 + maximum_values[i]);
913 }
914
915 if (range_product <= max_linear_size) {
916 // The product of all ranges fit in a int64_t. This is good news, that
917 // means we can interpret each row of the matrix as an integer in a
918 // mixed-radix representation and impose row[i] <= row[i+1].
919 *is_approximated = false;
920 return out;
921 }
922 *is_approximated = true;
923
924 const auto compute_approximate_coeffs =
925 [max_linear_size, maximum_values](double scaling_factor,
926 std::vector<int64_t>* coeffs) -> bool {
927 int64_t max_size = 0;
928 double cumulative_product_double = 1.0;
929 for (int i = 0; i < maximum_values.size(); ++i) {
930 const int64_t max = maximum_values[i];
931 const int64_t coeff = static_cast<int64_t>(cumulative_product_double);
932 (*coeffs)[i] = coeff;
933 cumulative_product_double *= scaling_factor * max + 1;
934 max_size = CapAdd(max_size, CapProd(max, coeff));
935 if (max_size > max_linear_size) return false;
936 }
937 return true;
938 };
939
940 const double scaling = BinarySearch<double>(
941 0.0, 1.0, [&compute_approximate_coeffs, &out](double scaling_factor) {
942 return compute_approximate_coeffs(scaling_factor, &out);
943 });
944 CHECK(compute_approximate_coeffs(scaling, &out));
945 return out;
946}
947
948} // namespace
949
951 const SatParameters& params = context->params();
952 const CpModelProto& proto = *context->working_model;
953
954 // We need to make sure the proto is up to date before computing symmetries!
955 if (context->working_model->has_objective()) {
956 context->WriteObjectiveToProto();
957 }
959
960 // Tricky: the equivalence relation are not part of the proto.
961 // We thus add them temporarily to compute the symmetry.
962 int64_t num_added = 0;
963 const int initial_ct_index = proto.constraints().size();
964 const int num_vars = proto.variables_size();
965 for (int var = 0; var < num_vars; ++var) {
966 if (context->IsFixed(var)) continue;
967 if (context->VariableWasRemoved(var)) continue;
968 if (context->VariableIsNotUsedAnymore(var)) continue;
969
970 const AffineRelation::Relation r = context->GetAffineRelation(var);
971 if (r.representative == var) continue;
972
973 ++num_added;
975 auto* arg = ct->mutable_linear();
976 arg->add_vars(var);
977 arg->add_coeffs(1);
978 arg->add_vars(r.representative);
979 arg->add_coeffs(-r.coeff);
980 arg->add_domain(r.offset);
981 arg->add_domain(r.offset);
982 }
983
984 std::vector<std::unique_ptr<SparsePermutation>> generators;
985 FindCpModelSymmetries(params, proto, &generators, context->logger(),
986 context->time_limit());
987
988 // Remove temporary affine relation.
989 context->working_model->mutable_constraints()->DeleteSubrange(
990 initial_ct_index, num_added);
991
992 if (generators.empty()) return true;
993
994 // Collect the at most ones.
995 //
996 // Note(user): This relies on the fact that the pointers remain stable when
997 // we adds new constraints. It should be the case, but it is a bit unsafe.
998 // On the other hand it is annoying to deal with both cases below.
999 std::vector<const google::protobuf::RepeatedField<int32_t>*> at_most_ones;
1000 for (int i = 0; i < proto.constraints_size(); ++i) {
1002 at_most_ones.push_back(&proto.constraints(i).at_most_one().literals());
1003 }
1004 if (proto.constraints(i).constraint_case() ==
1006 at_most_ones.push_back(&proto.constraints(i).exactly_one().literals());
1007 }
1008 }
1009
1010 // We have a few heuristics. The first only look at the global orbits under
1011 // the symmetry group and try to infer Boolean variable fixing via symmetry
1012 // breaking. Note that nothing is fixed yet, we will decide later if we fix
1013 // these Booleans or not.
1014 int distinguished_var = -1;
1015 std::vector<int> can_be_fixed_to_false;
1016
1017 // Get the global orbits and their size.
1018 const std::vector<int> orbits = GetOrbits(num_vars, generators);
1019 std::vector<int> orbit_sizes;
1020 int max_orbit_size = 0;
1021 int sum_of_orbit_sizes = 0;
1022 for (int var = 0; var < num_vars; ++var) {
1023 const int rep = orbits[var];
1024 if (rep == -1) continue;
1025 if (rep >= orbit_sizes.size()) orbit_sizes.resize(rep + 1, 0);
1026 ++sum_of_orbit_sizes;
1027 orbit_sizes[rep]++;
1028 if (orbit_sizes[rep] > max_orbit_size) {
1029 distinguished_var = var;
1030 max_orbit_size = orbit_sizes[rep];
1031 }
1032 }
1033
1034 // Log orbit info.
1035 LogOrbitInformation(orbits, context->logger());
1036
1037 // First heuristic based on propagation, see the function comment.
1038 if (max_orbit_size > 2) {
1039 OrbitAndPropagation(orbits, distinguished_var, &can_be_fixed_to_false,
1040 context);
1041 }
1042 const int first_heuristic_size = can_be_fixed_to_false.size();
1043
1044 // If an at most one intersect with one or more orbit, in each intersection,
1045 // we can fix all but one variable to zero. For now we only test positive
1046 // literal, and maximize the number of fixing.
1047 //
1048 // TODO(user): Doing that is not always good, on cod105.mps, fixing variables
1049 // instead of letting the inner solver handle Boolean symmetries make the
1050 // problem unsolvable instead of easily solved. This is probably because this
1051 // fixing do not exploit the full structure of these symmetries. Note
1052 // however that the fixing via propagation above close cod105 even more
1053 // efficiently.
1054 std::vector<int> var_can_be_true_per_orbit(num_vars, -1);
1055 {
1056 std::vector<int> tmp_to_clear;
1057 std::vector<int> tmp_sizes(num_vars, 0);
1058 for (const google::protobuf::RepeatedField<int32_t>* literals :
1059 at_most_ones) {
1060 tmp_to_clear.clear();
1061
1062 // Compute how many variables we can fix with this at most one.
1063 int num_fixable = 0;
1064 for (const int literal : *literals) {
1065 if (!RefIsPositive(literal)) continue;
1066 if (context->IsFixed(literal)) continue;
1067
1068 const int var = PositiveRef(literal);
1069 const int rep = orbits[var];
1070 if (rep == -1) continue;
1071
1072 // We count all but the first one in each orbit.
1073 if (tmp_sizes[rep] == 0) tmp_to_clear.push_back(rep);
1074 if (tmp_sizes[rep] > 0) ++num_fixable;
1075 tmp_sizes[rep]++;
1076 }
1077
1078 // Redo a pass to copy the intersection.
1079 if (num_fixable > can_be_fixed_to_false.size()) {
1080 distinguished_var = -1;
1081 can_be_fixed_to_false.clear();
1082 for (const int literal : *literals) {
1083 if (!RefIsPositive(literal)) continue;
1084 if (context->IsFixed(literal)) continue;
1085
1086 const int var = PositiveRef(literal);
1087 const int rep = orbits[var];
1088 if (rep == -1) continue;
1089 if (distinguished_var == -1 ||
1090 orbit_sizes[rep] > orbit_sizes[orbits[distinguished_var]]) {
1091 distinguished_var = var;
1092 }
1093
1094 // We push all but the first one in each orbit.
1095 if (tmp_sizes[rep] == 0) {
1096 can_be_fixed_to_false.push_back(var);
1097 } else {
1098 var_can_be_true_per_orbit[rep] = var;
1099 }
1100 tmp_sizes[rep] = 0;
1101 }
1102 } else {
1103 // Sparse clean up.
1104 for (const int rep : tmp_to_clear) tmp_sizes[rep] = 0;
1105 }
1106 }
1107
1108 if (can_be_fixed_to_false.size() > first_heuristic_size) {
1109 SOLVER_LOG(
1110 context->logger(),
1111 "[Symmetry] Num fixable by intersecting at_most_one with orbits: ",
1112 can_be_fixed_to_false.size(), " largest_orbit: ", max_orbit_size);
1113 }
1114 }
1115
1116 // Orbitope approach.
1117 //
1118 // This is basically the same as the generic approach, but because of the
1119 // extra structure, computing the orbit of any stabilizer subgroup is easy.
1120 // We look for orbits intersecting at most one constraints, so we can break
1121 // symmetry by fixing variables.
1122 //
1123 // TODO(user): The same effect could be achieved by adding symmetry breaking
1124 // constraints of the form "a >= b " between Booleans and let the presolve do
1125 // the reduction. This might be less code, but it is also less efficient.
1126 // Similarly, when we cannot just fix variables to break symmetries, we could
1127 // add these constraints, but it is unclear if we should do it all the time or
1128 // not.
1129 //
1130 // TODO(user): code the generic approach with orbits and stabilizer.
1131 std::vector<std::vector<int>> orbitope = BasicOrbitopeExtraction(generators);
1132 if (!orbitope.empty()) {
1133 SOLVER_LOG(context->logger(), "[Symmetry] Found orbitope of size ",
1134 orbitope.size(), " x ", orbitope[0].size());
1135 }
1136
1137 // HACK for flatzinc wordpress* problem.
1138 //
1139 // If we have a large orbitope, with one objective term by column, we break
1140 // the symmetry by ordering the objective terms. This usually increase
1141 // drastically the objective lower bounds we can discover.
1142 //
1143 // TODO(user): generalize somehow. See if we can exploit this in
1144 // lb_tree_search directly. We also have a lot more structure than just the
1145 // objective can be ordered. Like if the objective is a max, we can still do
1146 // that.
1147 //
1148 // TODO(user): Actually the constraint we add is really just breaking the
1149 // orbitope symmetry on one line. But this line being the objective is key. We
1150 // can also explicitly look for a full permutation group of the objective
1151 // terms directly instead of finding the largest orbitope first.
1152 if (!orbitope.empty() && context->working_model->has_objective()) {
1153 const int num_objective_terms = context->ObjectiveMap().size();
1154 if (orbitope[0].size() == num_objective_terms) {
1155 int num_in_column = 0;
1156 for (const std::vector<int>& row : orbitope) {
1157 if (context->ObjectiveMap().contains(row[0])) ++num_in_column;
1158 }
1159 if (num_in_column == 1) {
1160 context->WriteObjectiveToProto();
1161 const auto& obj = context->working_model->objective();
1162 CHECK_EQ(num_objective_terms, obj.vars().size());
1163 for (int i = 1; i < num_objective_terms; ++i) {
1164 auto* new_ct =
1166 new_ct->add_vars(obj.vars(i - 1));
1167 new_ct->add_vars(obj.vars(i));
1168 new_ct->add_coeffs(1);
1169 new_ct->add_coeffs(-1);
1170 new_ct->add_domain(0);
1171 new_ct->add_domain(std::numeric_limits<int64_t>::max());
1172 }
1174 context->UpdateRuleStats("symmetry: objective is one orbitope row.");
1175 return true;
1176 }
1177 }
1178 }
1179
1180 // Super simple heuristic to use the orbitope or not.
1181 //
1182 // In an orbitope with an at most one on each row, we can fix the upper right
1183 // triangle. We could use a formula, but the loop is fast enough.
1184 //
1185 // TODO(user): Compute the stabilizer under the only non-fixed element and
1186 // iterate!
1187 int max_num_fixed_in_orbitope = 0;
1188 if (!orbitope.empty()) {
1189 int size_left = orbitope[0].size();
1190 for (int col = 0; size_left > 1 && col < orbitope.size(); ++col) {
1191 max_num_fixed_in_orbitope += size_left - 1;
1192 --size_left;
1193 }
1194 }
1195
1196 // Fixing just a few variables to break large symmetry can be really bad. See
1197 // for example cdc7-4-3-2.pb.gz where we don't find solution if we do that. On
1198 // the other hand, enabling this make it worse on neos-3083784-nive.pb.gz.
1199 //
1200 // In general, enabling this works better in single thread with max_lp_sym,
1201 // but worse in multi-thread, where less workers are using symmetries, and so
1202 // it is better to fix more stuff.
1203 //
1204 // TODO(user): Tune more, especially as we handle symmetry better. Also the
1205 // estimate is pretty bad, we should probably compute stabilizer and decide
1206 // when we actually know how much we can fix compared to how many symmetry we
1207 // lose.
1208 const int num_fixable =
1209 std::max<int>(max_num_fixed_in_orbitope, can_be_fixed_to_false.size());
1210 if (/* DISABLES CODE */ (false) && !can_be_fixed_to_false.empty() &&
1211 100 * num_fixable < sum_of_orbit_sizes) {
1212 SOLVER_LOG(context->logger(),
1213 "[Symmetry] Not fixing anything as gain seems too small.");
1214 return true;
1215 }
1216
1217 // Fix "can_be_fixed_to_false" instead of the orbitope if it is larger.
1218 if (max_num_fixed_in_orbitope < can_be_fixed_to_false.size()) {
1219 const int orbit_index = orbits[distinguished_var];
1220 int num_in_orbit = 0;
1221 for (int i = 0; i < can_be_fixed_to_false.size(); ++i) {
1222 const int var = can_be_fixed_to_false[i];
1223 if (orbits[var] == orbit_index) ++num_in_orbit;
1224 context->UpdateRuleStats("symmetry: fixed to false in general orbit");
1225 if (var_can_be_true_per_orbit[orbits[var]] != -1) {
1226 // We are breaking the symmetry in a way that makes the hint invalid.
1227 // We want `var` to be false, so we would naively pick a symmetry to
1228 // enforce that. But that will be wrong if we do this twice: after we
1229 // permute the hint to fix the first one we would look for a symmetry
1230 // group element that fixes the second one to false. But there are many
1231 // of those, and picking the wrong one would risk making the first one
1232 // true again. Since this is a AMO, fixing the one that is true doesn't
1233 // have this problem.
1235 var_can_be_true_per_orbit[orbits[var]], true, generators);
1236 }
1237 if (!context->SetLiteralToFalse(var)) return false;
1238 }
1239
1240 // Moreover, we can add the implication that in the orbit of
1241 // distinguished_var, either everything is false, or var is at one.
1242 if (orbit_sizes[orbit_index] > num_in_orbit + 1) {
1243 context->UpdateRuleStats(
1244 "symmetry: added orbit symmetry breaking implications");
1245 auto* ct = context->working_model->add_constraints();
1246 auto* bool_and = ct->mutable_bool_and();
1247 ct->add_enforcement_literal(NegatedRef(distinguished_var));
1248 for (int var = 0; var < num_vars; ++var) {
1249 if (orbits[var] != orbit_index) continue;
1250 if (var == distinguished_var) continue;
1251 if (context->IsFixed(var)) continue;
1252 bool_and->add_literals(NegatedRef(var));
1253 }
1255 }
1256 return true;
1257 }
1258 if (orbitope.empty()) return true;
1259
1260 // This will always be kept all zero after usage.
1261 std::vector<int> tmp_to_clear;
1262 std::vector<int> tmp_sizes(num_vars, 0);
1263 std::vector<int> tmp_num_positive(num_vars, 0);
1264
1265 // TODO(user): The code below requires that no variable appears twice in the
1266 // same at most one. In particular lit and not(lit) cannot appear in the same
1267 // at most one.
1268 for (const google::protobuf::RepeatedField<int32_t>* literals :
1269 at_most_ones) {
1270 for (const int lit : *literals) {
1271 const int var = PositiveRef(lit);
1272 CHECK_NE(tmp_sizes[var], 1);
1273 tmp_sizes[var] = 1;
1274 }
1275 for (const int lit : *literals) {
1276 tmp_sizes[PositiveRef(lit)] = 0;
1277 }
1278 }
1279
1280 if (!orbitope.empty() && orbitope[0].size() > 1) {
1281 const int num_cols = orbitope[0].size();
1282 const std::vector<int> orbitope_orbits =
1283 GetOrbitopeOrbits(num_vars, orbitope);
1284
1285 // Using the orbitope orbits and intersecting at most ones, we will be able
1286 // in some case to derive a property of the literals of one row of the
1287 // orbitope. Namely that:
1288 // - All literals of that row take the same value.
1289 // - At most one literal can be true.
1290 // - At most one literal can be false.
1291 //
1292 // See the comment below for how we can infer this.
1293 const int num_rows = orbitope.size();
1294 std::vector<bool> row_is_all_equivalent(num_rows, false);
1295 std::vector<bool> row_has_at_most_one_true(num_rows, false);
1296 std::vector<bool> row_has_at_most_one_false(num_rows, false);
1297
1298 // Because in the orbitope case, we have a full symmetry group of the
1299 // columns, we can infer more than just using the orbits under a general
1300 // permutation group. If an at most one contains two variables from the
1301 // row, we can infer:
1302 // 1/ If the two variables appear positively, then there is an at most one
1303 // on the full row, and we can set n - 1 variables to zero to break the
1304 // symmetry.
1305 // 2/ If the two variables appear negatively, then the opposite situation
1306 // arise and there is at most one zero on the row, we can set n - 1
1307 // variables to one.
1308 // 3/ If two literals of opposite sign appear, then the only possibility
1309 // for the row are all at one or all at zero, thus we can mark all
1310 // variables as equivalent.
1311 //
1312 // These property comes from the fact that when we permute a line of the
1313 // orbitope in any way, then the position than ends up in the at most one
1314 // must never be both at one.
1315 //
1316 // Note that 3/ can be done without breaking any symmetry, but for 1/ and 2/
1317 // by choosing which variable is not fixed, we will break some symmetry.
1318 //
1319 // TODO(user): for 1/ and 2/ we could add an at most one constraint on the
1320 // full row if it is not already there!
1321 //
1322 // Note(user): On the miplib, only 1/ and 2/ happens currently. Not sure
1323 // with LNS though.
1324 for (const google::protobuf::RepeatedField<int32_t>* literals :
1325 at_most_ones) {
1326 tmp_to_clear.clear();
1327 for (const int literal : *literals) {
1328 if (context->IsFixed(literal)) continue;
1329 const int var = PositiveRef(literal);
1330 const int row = orbitope_orbits[var];
1331 if (row == -1) continue;
1332
1333 if (tmp_sizes[row] == 0) tmp_to_clear.push_back(row);
1334 tmp_sizes[row]++;
1335 if (RefIsPositive(literal)) tmp_num_positive[row]++;
1336 }
1337
1338 // An at most one touching two positions in an orbitope row can be
1339 // extended to include the full row.
1340 //
1341 // Note(user): I am not sure we care about that here. By symmetry, if we
1342 // have an at most one touching two positions, then we should have others
1343 // touching all pair of positions. And the at most one expansion would
1344 // already have extended it. So this is more FYI.
1345 bool possible_extension = false;
1346
1347 // TODO(user): if the same at most one touch more than one row, we can
1348 // deduce more. It is a bit tricky and maybe not frequent enough to make a
1349 // big difference. Also, as we start to fix things, at most one might
1350 // propagate by themselves.
1351 for (const int row : tmp_to_clear) {
1352 const int size = tmp_sizes[row];
1353 const int num_positive = tmp_num_positive[row];
1354 const int num_negative = tmp_sizes[row] - tmp_num_positive[row];
1355 tmp_sizes[row] = 0;
1356 tmp_num_positive[row] = 0;
1357
1358 if (num_positive > 0 && num_negative > 0) {
1359 row_is_all_equivalent[row] = true;
1360 }
1361 if (num_positive > 1 && num_negative == 0) {
1362 if (size < num_cols) possible_extension = true;
1363 row_has_at_most_one_true[row] = true;
1364 } else if (num_positive == 0 && num_negative > 1) {
1365 if (size < num_cols) possible_extension = true;
1366 row_has_at_most_one_false[row] = true;
1367 }
1368 }
1369
1370 if (possible_extension) {
1371 context->UpdateRuleStats(
1372 "TODO symmetry: possible at most one extension.");
1373 }
1374 }
1375
1376 // List the row in "at most one" by score. We will be able to fix a
1377 // "triangle" of literals in order to break some of the symmetry.
1378 std::vector<std::pair<int, int64_t>> rows_by_score;
1379
1380 // Mark all the equivalence or fixed rows.
1381 // Note that this operation do not change the symmetry group.
1382 //
1383 // TODO(user): We could remove these rows from the orbitope. Note that
1384 // currently this never happen on the miplib (maybe in LNS though).
1385 for (int i = 0; i < num_rows; ++i) {
1386 if (row_has_at_most_one_true[i] && row_has_at_most_one_false[i]) {
1387 // If we have both property, it means we have
1388 // - sum_j orbitope[row][j] <= 1
1389 // - sum_j not(orbitope[row][j]) <= 1 which is the same as
1390 // sum_j orbitope[row][j] >= num_cols - 1.
1391 // This is only possible if we have two elements and we don't have
1392 // row_is_all_equivalent.
1393 if (num_cols == 2 && !row_is_all_equivalent[i]) {
1394 // We have [1, 0] or [0, 1].
1395 context->UpdateRuleStats("symmetry: equivalence in orbitope row");
1396 if (!context->StoreBooleanEqualityRelation(
1397 orbitope[i][0], NegatedRef(orbitope[i][1]))) {
1398 return false;
1399 }
1400 if (context->ModelIsUnsat()) return false;
1401 } else {
1402 // No solution.
1403 return context->NotifyThatModelIsUnsat("orbitope and at most one");
1404 }
1405 continue;
1406 }
1407
1408 if (row_is_all_equivalent[i]) {
1409 // Here we proved that the row is either all ones or all zeros.
1410 // This was because we had:
1411 // at_most_one = [x, ~y, ...]
1412 // orbitope = [x, y, ...]
1413 // and by symmetry we have
1414 // at_most_one = [~x, y, ...]
1415 // This for all pairs of positions in that row.
1416 if (row_has_at_most_one_false[i]) {
1417 context->UpdateRuleStats("symmetry: all true in orbitope row");
1418 for (int j = 0; j < num_cols; ++j) {
1419 if (!context->SetLiteralToTrue(orbitope[i][j])) return false;
1420 }
1421 } else if (row_has_at_most_one_true[i]) {
1422 context->UpdateRuleStats("symmetry: all false in orbitope row");
1423 for (int j = 0; j < num_cols; ++j) {
1424 if (!context->SetLiteralToFalse(orbitope[i][j])) return false;
1425 }
1426 } else {
1427 context->UpdateRuleStats("symmetry: all equivalent in orbitope row");
1428 for (int j = 1; j < num_cols; ++j) {
1429 if (!context->StoreBooleanEqualityRelation(orbitope[i][0],
1430 orbitope[i][j])) {
1431 return false;
1432 }
1433 if (context->ModelIsUnsat()) return false;
1434 }
1435 }
1436 continue;
1437 }
1438
1439 // We use as the score the number of constraint in which variables from
1440 // this row participate.
1441 const int64_t score =
1442 context->VarToConstraints(PositiveRef(orbitope[i][0])).size();
1443 if (row_has_at_most_one_true[i]) {
1444 rows_by_score.push_back({i, score});
1445 } else if (row_has_at_most_one_false[i]) {
1446 rows_by_score.push_back({i, score});
1447 }
1448 }
1449
1450 // Break the symmetry by fixing at each step all but one literal to true or
1451 // false. Note that each time we do that for a row, we need to exclude the
1452 // non-fixed column from the rest of the row processing. We thus fix a
1453 // "triangle" of literals.
1454 //
1455 // This is the same as ordering the columns in some lexicographic order and
1456 // using the at_most_ones to fix known position. Note that we can still add
1457 // lexicographic symmetry breaking inequality on the columns as long as we
1458 // do that in the same order as these fixing.
1459 absl::c_stable_sort(rows_by_score, [](const std::pair<int, int64_t>& p1,
1460 const std::pair<int, int64_t>& p2) {
1461 return p1.second > p2.second;
1462 });
1463 int num_processed_rows = 0;
1464 for (const auto [row, score] : rows_by_score) {
1465 if (num_processed_rows + 1 >= num_cols) break;
1466 ++num_processed_rows;
1467 if (row_has_at_most_one_true[row]) {
1468 context->UpdateRuleStats(
1469 "symmetry: fixed all but one to false in orbitope row");
1471 orbitope, row, num_processed_rows - 1, true);
1472 for (int j = num_processed_rows; j < num_cols; ++j) {
1473 if (!context->SetLiteralToFalse(orbitope[row][j])) return false;
1474 }
1475 } else {
1476 CHECK(row_has_at_most_one_false[row]);
1477 context->UpdateRuleStats(
1478 "symmetry: fixed all but one to true in orbitope row");
1480 orbitope, row, num_processed_rows - 1, false);
1481 for (int j = num_processed_rows; j < num_cols; ++j) {
1482 if (!context->SetLiteralToTrue(orbitope[row][j])) return false;
1483 }
1484 }
1485 }
1486
1487 // For correctness of the code below, reduce the orbitope.
1488 //
1489 // TODO(user): This is probably not needed if we add lexicographic
1490 // constraint instead of just breaking a single row below.
1491 if (num_processed_rows > 0) {
1492 // Remove the first num_processed_rows.
1493 int new_size = 0;
1494 for (int i = num_processed_rows; i < orbitope.size(); ++i) {
1495 orbitope[new_size++] = std::move(orbitope[i]);
1496 }
1497 CHECK_LT(new_size, orbitope.size());
1498 orbitope.resize(new_size);
1499
1500 // For each of them remove the first num_processed_rows entries.
1501 for (int i = 0; i < orbitope.size(); ++i) {
1502 CHECK_LT(num_processed_rows, orbitope[i].size());
1503 orbitope[i].erase(orbitope[i].begin(),
1504 orbitope[i].begin() + num_processed_rows);
1505 }
1506 }
1507 }
1508
1509 // The transformations below seems to hurt more than what they help.
1510 // Especially when we handle symmetry during the search like with max_lp_sym
1511 // worker. See for instance neos-948346.pb or map06.pb.gz.
1512 if (params.symmetry_level() <= 3) return true;
1513
1514 // If we are left with a set of variable than can all be permuted, lets
1515 // break the symmetry by ordering them.
1516 if (orbitope.size() == 1) {
1517 const int num_cols = orbitope[0].size();
1518 for (int i = 0; i + 1 < num_cols; ++i) {
1519 // Add orbitope[0][i] >= orbitope[0][i+1].
1520 if (context->CanBeUsedAsLiteral(orbitope[0][i]) &&
1521 context->CanBeUsedAsLiteral(orbitope[0][i + 1])) {
1522 context->AddImplication(orbitope[0][i + 1], orbitope[0][i]);
1523 context->UpdateRuleStats(
1524 "symmetry: added symmetry breaking implication");
1525 continue;
1526 }
1527
1529 ct->mutable_linear()->add_coeffs(1);
1530 ct->mutable_linear()->add_vars(orbitope[0][i]);
1531 ct->mutable_linear()->add_coeffs(-1);
1532 ct->mutable_linear()->add_vars(orbitope[0][i + 1]);
1533 ct->mutable_linear()->add_domain(0);
1534 ct->mutable_linear()->add_domain(std::numeric_limits<int64_t>::max());
1535 context->UpdateRuleStats("symmetry: added symmetry breaking inequality");
1536 }
1538 } else if (orbitope.size() > 1) {
1539 std::vector<int64_t> max_values(orbitope.size());
1540 for (int i = 0; i < orbitope.size(); ++i) {
1541 const int var = orbitope[i][0];
1542 const int64_t max = std::max(std::abs(context->MaxOf(var)),
1543 std::abs(context->MinOf(var)));
1544 max_values[i] = max;
1545 }
1546 constexpr int kMaxBits = 60;
1547 bool is_approximated;
1548 const std::vector<int64_t> coeffs = BuildInequalityCoeffsForOrbitope(
1549 max_values, (int64_t{1} << kMaxBits), &is_approximated);
1550 for (int i = 0; i + 1 < orbitope[0].size(); ++i) {
1552 auto* arg = ct->mutable_linear();
1553 for (int j = 0; j < orbitope.size(); ++j) {
1554 const int64_t coeff = coeffs[j];
1555 arg->add_vars(orbitope[j][i + 1]);
1556 arg->add_coeffs(coeff);
1557 arg->add_vars(orbitope[j][i]);
1558 arg->add_coeffs(-coeff);
1559 DCHECK_EQ(context->MaxOf(orbitope[j][i + 1]),
1560 context->MaxOf(orbitope[j][i]));
1561 DCHECK_EQ(context->MinOf(orbitope[j][i + 1]),
1562 context->MinOf(orbitope[j][i]));
1563 }
1564 arg->add_domain(0);
1565 arg->add_domain(std::numeric_limits<int64_t>::max());
1566 DCHECK(!PossibleIntegerOverflow(*context->working_model, arg->vars(),
1567 arg->coeffs()));
1568 }
1569 context->UpdateRuleStats(
1570 absl::StrCat("symmetry: added linear ",
1571 is_approximated ? "approximated " : "",
1572 "inequality ordering orbitope columns"),
1573 orbitope[0].size());
1575 return true;
1576 }
1577
1578 return true;
1579}
1580
1581namespace {
1582
1583std::vector<absl::Span<int>> GetCyclesAsSpan(
1584 SparsePermutationProto& permutation) {
1585 std::vector<absl::Span<int>> result;
1586 int start = 0;
1587 const int num_cycles = permutation.cycle_sizes().size();
1588 for (int i = 0; i < num_cycles; ++i) {
1589 const int size = permutation.cycle_sizes(i);
1590 result.push_back(
1591 absl::MakeSpan(&permutation.mutable_support()->at(start), size));
1592 start += size;
1593 }
1594 return result;
1595}
1596
1597} // namespace
1598
1600 PresolveContext* context) {
1601 std::vector<absl::Span<int>> cycles;
1602 int num_problematic_generators = 0;
1603 for (SparsePermutationProto& generator : *symmetry->mutable_permutations()) {
1604 // We process each cycle at once.
1605 // If all variables from a cycle are fixed to the same value, this is
1606 // fine and we can just remove the cycle.
1607 //
1608 // TODO(user): These are just basic checks and do not guarantee that we
1609 // properly kept this symmetry in the presolve.
1610 //
1611 // TODO(user): Deal with case where all variable in an orbit has been found
1612 // to be equivalent to each other. Or all variables have affine
1613 // representative, like if all domains where [0][2], we should have remapped
1614 // all such variable to Booleans.
1615 cycles = GetCyclesAsSpan(generator);
1616 bool problematic = false;
1617
1618 int new_num_cycles = 0;
1619 const int old_num_cycles = cycles.size();
1620 for (int i = 0; i < old_num_cycles; ++i) {
1621 if (cycles[i].empty()) continue;
1622 const int reference_var = cycles[i][0];
1623 const Domain reference_domain = context->DomainOf(reference_var);
1624 const AffineRelation::Relation reference_relation =
1625 context->GetAffineRelation(reference_var);
1626
1627 int num_affine_relations = 0;
1628 int num_with_same_representative = 0;
1629
1630 int num_fixed = 0;
1631 int num_unused = 0;
1632 for (const int var : cycles[i]) {
1633 CHECK(RefIsPositive(var));
1634 if (context->DomainOf(var) != reference_domain) {
1635 context->UpdateRuleStats(
1636 "TODO symmetry: different domain in symmetric variables");
1637 problematic = true;
1638 break;
1639 }
1640
1641 if (context->DomainOf(var).IsFixed()) {
1642 ++num_fixed;
1643 continue;
1644 }
1645
1646 // If we have affine relation, we only support the case where they
1647 // are all the same.
1648 const auto affine_relation = context->GetAffineRelation(var);
1649 if (affine_relation == reference_relation) {
1650 ++num_with_same_representative;
1651 }
1652 if (affine_relation.representative != var) {
1653 ++num_affine_relations;
1654 }
1655
1656 if (context->VariableIsNotUsedAnymore(var)) {
1657 ++num_unused;
1658 continue;
1659 }
1660 }
1661
1662 if (problematic) break;
1663
1664 if (num_fixed > 0) {
1665 if (num_fixed != cycles[i].size()) {
1666 context->UpdateRuleStats(
1667 "TODO symmetry: not all variables fixed in cycle");
1668 problematic = true;
1669 break;
1670 }
1671 continue; // We can skip this cycle
1672 }
1673
1674 if (num_affine_relations > 0) {
1675 if (num_with_same_representative != cycles[i].size()) {
1676 context->UpdateRuleStats(
1677 "TODO symmetry: not all variables have same representative");
1678 problematic = true;
1679 break;
1680 }
1681 continue; // We can skip this cycle
1682 }
1683
1684 // Note that the order matter.
1685 // If all have the same representative, we don't care about this one.
1686 if (num_unused > 0) {
1687 if (num_unused != cycles[i].size()) {
1688 context->UpdateRuleStats(
1689 "TODO symmetry: not all variables unused in cycle");
1690 problematic = true;
1691 break;
1692 }
1693 continue; // We can skip this cycle
1694 }
1695
1696 // Lets keep this cycle.
1697 cycles[new_num_cycles++] = cycles[i];
1698 }
1699
1700 if (problematic) {
1701 ++num_problematic_generators;
1702 generator.clear_support();
1703 generator.clear_cycle_sizes();
1704 continue;
1705 }
1706
1707 if (new_num_cycles < old_num_cycles) {
1708 cycles.resize(new_num_cycles);
1709 generator.clear_cycle_sizes();
1710 int new_support_size = 0;
1711 for (const absl::Span<int> cycle : cycles) {
1712 for (const int var : cycle) {
1713 generator.set_support(new_support_size++, var);
1714 }
1715 generator.add_cycle_sizes(cycle.size());
1716 }
1717 generator.mutable_support()->Truncate(new_support_size);
1718 }
1719 }
1720
1721 if (num_problematic_generators > 0) {
1722 SOLVER_LOG(context->logger(), "[Symmetry] ", num_problematic_generators,
1723 " generators where problematic !! Fix.");
1724 }
1725
1726 // Lets remove empty generators.
1727 int new_size = 0;
1728 const int old_size = symmetry->permutations().size();
1729 for (int i = 0; i < old_size; ++i) {
1730 if (symmetry->permutations(i).support().empty()) continue;
1731 if (new_size != i) {
1732 symmetry->mutable_permutations()->SwapElements(new_size, i);
1733 }
1734 ++new_size;
1735 }
1736 if (new_size != old_size) {
1737 symmetry->mutable_permutations()->DeleteSubrange(new_size,
1738 old_size - new_size);
1739 }
1740
1741 // Lets output the new statistics.
1742 // TODO(user): Avoid the reconvertion.
1743 {
1744 const int num_vars = context->working_model->variables().size();
1745 std::vector<std::unique_ptr<SparsePermutation>> generators;
1746 for (const SparsePermutationProto& perm : symmetry->permutations()) {
1747 generators.emplace_back(CreateSparsePermutationFromProto(num_vars, perm));
1748 }
1749 SOLVER_LOG(context->logger(),
1750 "[Symmetry] final processing #generators:", generators.size());
1751 const std::vector<int> orbits = GetOrbits(num_vars, generators);
1752 LogOrbitInformation(orbits, context->logger());
1753 }
1754
1755 return true;
1756}
1757
1758} // namespace sat
1759} // namespace operations_research
Definition model.h:345
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(absl::Span< const int > cycle_indices)
const std::vector< int > & Support() const
static std::unique_ptr< TimeLimit > FromDeterministicTime(double deterministic_limit)
Definition time_limit.h:130
::operations_research::sat::BoolArgumentProto *PROTOBUF_NONNULL mutable_bool_and()
const ::operations_research::sat::BoolArgumentProto & exactly_one() const
const ::operations_research::sat::BoolArgumentProto & at_most_one() const
::operations_research::sat::LinearConstraintProto *PROTOBUF_NONNULL mutable_linear()
::operations_research::sat::ConstraintProto *PROTOBUF_NONNULL mutable_constraints(int index)
const ::operations_research::sat::IntegerVariableProto & variables(int index) const
const ::operations_research::sat::ConstraintProto & constraints(int index) const
::operations_research::sat::SymmetryProto *PROTOBUF_NONNULL mutable_symmetry()
::operations_research::sat::ConstraintProto *PROTOBUF_NONNULL add_constraints()
const ::operations_research::sat::CpObjectiveProto & objective() const
ABSL_MUST_USE_RESULT bool SetLiteralToTrue(int lit)
ABSL_MUST_USE_RESULT bool SetLiteralToFalse(int lit)
ABSL_MUST_USE_RESULT bool NotifyThatModelIsUnsat(absl::string_view message="")
bool StoreBooleanEqualityRelation(int ref_a, int ref_b)
AffineRelation::Relation GetAffineRelation(int ref) const
const absl::flat_hash_set< int > & VarToConstraints(int var) const
const absl::flat_hash_map< int, int64_t > & ObjectiveMap() const
void UpdateRuleStats(std::string_view name, int num_times=1)
void MaybeSwapOrbitopeColumns(absl::Span< const std::vector< int > > orbitope, int row, int pivot_col, bool value)
void MaybeUpdateVarWithSymmetriesToValue(int var, bool value, absl::Span< const std::unique_ptr< SparsePermutation > > generators)
const ::operations_research::sat::SparsePermutationProto & permutations(int index) const
::operations_research::sat::SparsePermutationProto *PROTOBUF_NONNULL mutable_permutations(int index)
::operations_research::sat::SparsePermutationProto *PROTOBUF_NONNULL add_permutations()
::operations_research::sat::DenseMatrixProto *PROTOBUF_NONNULL add_orbitopes()
ABSL_ATTRIBUTE_REINITIALIZES void Clear() PROTOBUF_FINAL
std::vector< int > GetOrbitopeOrbits(int n, absl::Span< const std::vector< int > > orbitope)
bool LoadModelForProbing(PresolveContext *context, Model *local_model)
bool DetectAndExploitSymmetriesInPresolve(PresolveContext *context)
void FindCpModelSymmetries(const SatParameters &params, const CpModelProto &problem, std::vector< std::unique_ptr< SparsePermutation > > *generators, SolverLogger *logger, TimeLimit *solver_time_limit)
void DetectAndAddSymmetryToProto(const SatParameters &params, CpModelProto *proto, SolverLogger *logger, TimeLimit *time_limit)
bool PossibleIntegerOverflow(const CpModelProto &model, absl::Span< const int > vars, absl::Span< const int64_t > coeffs, int64_t offset, std::pair< int64_t, int64_t > *implied_domain)
bool FilterOrbitOnUnusedOrFixedVariables(SymmetryProto *symmetry, PresolveContext *context)
std::vector< int > GetOrbits(int n, absl::Span< const std::unique_ptr< SparsePermutation > > generators)
absl::string_view ConstraintCaseName(ConstraintProto::ConstraintCase constraint_case)
std::unique_ptr< SparsePermutation > CreateSparsePermutationFromProto(int n, const SparsePermutationProto &proto)
std::vector< std::vector< int > > BasicOrbitopeExtraction(absl::Span< const std::unique_ptr< SparsePermutation > > generators)
Graph * GenerateGraphForSymmetryDetection(const LinearBooleanProblem &problem, std::vector< int > *initial_equivalence_classes)
OR-Tools root namespace.
int64_t CapAdd(int64_t x, int64_t y)
const bool DEBUG_MODE
Definition radix_sort.h:266
Point BinarySearch(Point x_true, Point x_false, std::function< bool(Point)> f)
std::string FormatCounter(int64_t num)
Definition logging.cc:30
int64_t CapProd(int64_t x, int64_t y)
util::ReverseArcStaticGraph Graph
ClosedInterval::Iterator begin(ClosedInterval interval)
uint64_t Hash(uint64_t num, uint64_t c)
Definition hash.h:73
std::vector< int >::const_iterator begin() const
#define SOLVER_LOG(logger,...)
Definition logging.h:114