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