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