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