20 #include "absl/container/flat_hash_map.h"
21 #include "absl/memory/memory.h"
22 #include "google/protobuf/repeated_field.h"
34 std::size_t operator()(
const std::vector<int64_t>& values)
const {
36 for (
const int64_t
value : values) {
51 int GetId(
const std::vector<int64_t>& color) {
55 int NextFreeId()
const {
return id_map_.size(); }
58 absl::flat_hash_map<std::vector<int64_t>, int, VectorHash> id_map_;
64 template <
typename FieldInt64Type>
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);
88 template <
typename Graph>
90 const CpModelProto& problem, std::vector<int>* initial_equivalence_classes,
91 SolverLogger* logger) {
92 CHECK(initial_equivalence_classes !=
nullptr);
94 const int num_variables = problem.variables_size();
95 auto graph = absl::make_unique<Graph>();
105 VAR_COEFFICIENT_NODE,
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) {
114 const int node = initial_equivalence_classes->size();
115 initial_equivalence_classes->push_back(color_id_generator.GetId(color));
119 graph->AddNode(node);
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);
133 const int64_t coeff = problem.objective().coeffs(i);
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);
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;
157 if (coeff == 1)
return var_node;
160 coefficient_nodes.insert({std::make_pair(
var, coeff), 0});
161 if (!insert.second)
return insert.first->second;
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;
173 auto get_literal_node = [&get_coefficient_node](
int ref) {
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) {
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;
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));
211 graph->AddArc(get_implication_node(
NegatedRef(ref_b)),
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()};
221 switch (constraint.constraint_case()) {
222 case ConstraintProto::CONSTRAINT_NOT_SET:
224 case ConstraintProto::kLinear: {
229 Append(constraint.linear().domain(), &color);
230 CHECK_EQ(constraint_node, new_node(color));
231 for (
int i = 0; i < constraint.linear().vars_size(); ++i) {
232 const int ref = constraint.linear().vars(i);
235 ? constraint.linear().coeffs(i)
236 : -constraint.linear().coeffs(i);
237 graph->AddArc(get_coefficient_node(variable_node, coeff),
242 case ConstraintProto::kBoolOr: {
243 CHECK_EQ(constraint_node, new_node(color));
244 for (
const int ref : constraint.bool_or().literals()) {
245 graph->AddArc(get_literal_node(ref), constraint_node);
249 case ConstraintProto::kAtMostOne: {
250 if (constraint.at_most_one().literals().size() == 2) {
252 add_implication(constraint.at_most_one().literals(0),
253 NegatedRef(constraint.at_most_one().literals(1)));
257 CHECK_EQ(constraint_node, new_node(color));
258 for (
const int ref : constraint.at_most_one().literals()) {
259 graph->AddArc(get_literal_node(ref), constraint_node);
263 case ConstraintProto::kExactlyOne: {
264 CHECK_EQ(constraint_node, new_node(color));
265 for (
const int ref : constraint.exactly_one().literals()) {
266 graph->AddArc(get_literal_node(ref), constraint_node);
270 case ConstraintProto::kBoolXor: {
271 CHECK_EQ(constraint_node, new_node(color));
272 for (
const int ref : constraint.bool_xor().literals()) {
273 graph->AddArc(get_literal_node(ref), constraint_node);
277 case ConstraintProto::kBoolAnd: {
280 if (constraint.enforcement_literal_size() != 1) {
283 "[Symmetry] BoolAnd with multiple enforcement literal are not "
284 "supported in symmetry code:",
285 constraint.ShortDebugString());
289 CHECK_EQ(constraint.enforcement_literal_size(), 1);
290 const int ref_a = constraint.enforcement_literal(0);
291 for (
const int ref_b : constraint.bool_and().literals()) {
292 add_implication(ref_a, ref_b);
303 VLOG(1) <<
"Unsupported constraint type "
313 if (constraint.constraint_case() != ConstraintProto::kBoolAnd) {
314 for (
const int ref : constraint.enforcement_literal()) {
315 graph->AddArc(constraint_node, get_literal_node(ref));
321 DCHECK_EQ(graph->num_nodes(), initial_equivalence_classes->size());
328 const int num_nodes = graph->num_nodes();
329 std::vector<int> in_degree(num_nodes, 0);
330 std::vector<int> out_degree(num_nodes, 0);
331 for (
int i = 0; i < num_nodes; ++i) {
332 out_degree[i] = graph->OutDegree(i);
333 for (
const int head : (*graph)[i]) {
337 for (
int i = 0; i < num_nodes; ++i) {
338 if (in_degree[i] >= num_nodes || out_degree[i] >= num_nodes) {
339 SOLVER_LOG(logger,
"[Symmetry] Too many multi-arcs in symmetry code.");
352 int next_id = color_id_generator.NextFreeId();
353 for (
int i = 0; i < num_variables; ++i) {
354 if ((*graph)[i].empty()) {
355 (*initial_equivalence_classes)[i] = next_id++;
361 std::vector<int> mapping(next_id, -1);
362 for (
int& ref : *initial_equivalence_classes) {
363 if (mapping[ref] == -1) {
364 ref = mapping[ref] =
id++;
375 const SatParameters& params,
const CpModelProto& problem,
376 std::vector<std::unique_ptr<SparsePermutation>>* generators,
378 CHECK(generators !=
nullptr);
383 std::vector<int> equivalence_classes;
384 std::unique_ptr<Graph> graph(GenerateGraphForSymmetryDetection<Graph>(
385 problem, &equivalence_classes, logger));
386 if (graph ==
nullptr)
return;
388 SOLVER_LOG(logger,
"[Symmetry] Graph for symmetry has ", graph->num_nodes(),
389 " nodes and ", graph->num_arcs(),
" arcs.");
390 if (graph->num_nodes() == 0)
return;
393 std::vector<int> factorized_automorphism_group_size;
397 &equivalence_classes, generators, &factorized_automorphism_group_size,
404 "[Symmetry] GraphSymmetryFinder error: ", status.message());
410 double average_support_size = 0.0;
411 int num_generators = 0;
412 int num_duplicate_constraints = 0;
413 for (
int i = 0; i < generators->size(); ++i) {
415 std::vector<int> to_delete;
416 for (
int j = 0; j < permutation->
NumCycles(); ++j) {
420 if (*(permutation->
Cycle(j).
begin()) >= problem.variables_size()) {
421 to_delete.push_back(j);
424 for (
const int node : permutation->
Cycle(j)) {
425 DCHECK_GE(node, problem.variables_size());
432 if (!permutation->
Support().empty()) {
433 average_support_size += permutation->
Support().size();
434 swap((*generators)[num_generators], (*generators)[i]);
437 ++num_duplicate_constraints;
440 generators->resize(num_generators);
441 average_support_size /= num_generators;
442 SOLVER_LOG(logger,
"[Symmetry] Symmetry computation done. time: ",
444 " dtime: ",
time_limit->GetElapsedDeterministicTime());
445 if (num_generators > 0) {
446 SOLVER_LOG(logger,
"[Symmetry] # of generators: ", num_generators);
448 "[Symmetry] Average support size: ", average_support_size);
449 if (num_duplicate_constraints > 0) {
450 SOLVER_LOG(logger,
"[Symmetry] The model contains ",
451 num_duplicate_constraints,
" duplicate constraints !");
458 SymmetryProto* symmetry =
proto->mutable_symmetry();
461 std::vector<std::unique_ptr<SparsePermutation>> generators;
464 if (generators.empty())
return;
466 for (
const std::unique_ptr<SparsePermutation>& perm : generators) {
467 SparsePermutationProto* perm_proto = symmetry->add_permutations();
468 const int num_cycle = perm->NumCycles();
469 for (
int i = 0; i < num_cycle; ++i) {
470 const int old_size = perm_proto->support().size();
471 for (
const int var : perm->Cycle(i)) {
472 perm_proto->add_support(
var);
474 perm_proto->add_cycle_sizes(perm_proto->support().size() - old_size);
479 if (orbitope.empty())
return;
480 SOLVER_LOG(logger,
"[Symmetry] Found orbitope of size ", orbitope.size(),
481 " x ", orbitope[0].size());
482 DenseMatrixProto* matrix = symmetry->add_orbitopes();
483 matrix->set_num_rows(orbitope.size());
484 matrix->set_num_cols(orbitope[0].size());
485 for (
const std::vector<int>&
row : orbitope) {
486 for (
const int entry :
row) {
487 matrix->add_entries(entry);
493 const SatParameters& params =
context->params();
497 if (
context->working_model->has_objective()) {
498 context->WriteObjectiveToProto();
500 const int num_vars =
proto.variables_size();
501 for (
int i = 0; i < num_vars; ++i) {
503 context->working_model->mutable_variables(i));
508 int64_t num_added = 0;
509 const int initial_ct_index =
proto.constraints().size();
510 for (
int var = 0;
var < num_vars; ++
var) {
512 if (
context->VariableWasRemoved(
var))
continue;
513 if (
context->VariableIsNotUsedAnymore(
var))
continue;
519 ConstraintProto*
ct =
context->working_model->add_constraints();
520 auto* arg =
ct->mutable_linear();
524 arg->add_coeffs(-r.
coeff);
525 arg->add_domain(r.
offset);
526 arg->add_domain(r.
offset);
529 std::vector<std::unique_ptr<SparsePermutation>> generators;
534 context->working_model->mutable_constraints()->DeleteSubrange(
535 initial_ct_index, num_added);
537 if (generators.empty())
return true;
544 std::vector<const google::protobuf::RepeatedField<int32_t>*> at_most_ones;
545 for (
int i = 0; i <
proto.constraints_size(); ++i) {
546 if (
proto.constraints(i).constraint_case() == ConstraintProto::kAtMostOne) {
547 at_most_ones.push_back(&
proto.constraints(i).at_most_one().literals());
549 if (
proto.constraints(i).constraint_case() ==
550 ConstraintProto::kExactlyOne) {
551 at_most_ones.push_back(&
proto.constraints(i).exactly_one().literals());
560 std::vector<int> can_be_fixed_to_false;
562 const std::vector<int> orbits =
GetOrbits(num_vars, generators);
563 std::vector<int> orbit_sizes;
564 int max_orbit_size = 0;
565 for (
const int rep : orbits) {
566 if (rep == -1)
continue;
567 if (rep >= orbit_sizes.size()) orbit_sizes.resize(rep + 1, 0);
569 max_orbit_size =
std::max(max_orbit_size, orbit_sizes[rep]);
572 std::vector<int> tmp_to_clear;
573 std::vector<int> tmp_sizes(num_vars, 0);
574 for (
const google::protobuf::RepeatedField<int32_t>* literals :
576 tmp_to_clear.clear();
580 for (
const int literal : *literals) {
585 const int rep = orbits[
var];
586 if (rep == -1)
continue;
589 if (tmp_sizes[rep] == 0) tmp_to_clear.push_back(rep);
590 if (tmp_sizes[rep] > 0) ++num_fixable;
595 if (num_fixable > can_be_fixed_to_false.size()) {
596 can_be_fixed_to_false.clear();
597 for (
const int literal : *literals) {
602 const int rep = orbits[
var];
603 if (rep == -1)
continue;
606 if (tmp_sizes[rep] == 0) can_be_fixed_to_false.push_back(
var);
611 for (
const int rep : tmp_to_clear) tmp_sizes[rep] = 0;
617 "[Symmetry] Num fixable by intersecting at_most_one with orbits: ",
618 can_be_fixed_to_false.size(),
" largest_orbit: ", max_orbit_size);
637 if (!orbitope.empty()) {
639 orbitope.size(),
" x ", orbitope[0].size());
649 int max_num_fixed_in_orbitope = 0;
650 if (!orbitope.empty()) {
651 const int num_rows = orbitope[0].size();
652 int size_left = num_rows;
653 for (
int col = 0; size_left > 1 &&
col < orbitope.size(); ++
col) {
654 max_num_fixed_in_orbitope += size_left - 1;
658 if (max_num_fixed_in_orbitope < can_be_fixed_to_false.size()) {
659 for (
int i = 0; i < can_be_fixed_to_false.size(); ++i) {
660 const int var = can_be_fixed_to_false[i];
661 context->UpdateRuleStats(
"symmetry: fixed to false in general orbit");
662 if (!
context->SetLiteralToFalse(
var))
return false;
666 if (orbitope.empty())
return true;
669 std::vector<int> tmp_to_clear;
670 std::vector<int> tmp_sizes(num_vars, 0);
671 std::vector<int> tmp_num_positive(num_vars, 0);
676 for (
const google::protobuf::RepeatedField<int32_t>* literals :
678 for (
const int lit : *literals) {
683 for (
const int lit : *literals) {
688 while (!orbitope.empty() && orbitope[0].size() > 1) {
689 const int num_cols = orbitope[0].size();
720 std::vector<bool> all_equivalent_rows(orbitope.size(),
false);
726 bool at_most_one_in_best_rows;
727 int64_t best_score = 0;
728 std::vector<int> best_rows;
730 std::vector<int> rows_in_at_most_one;
731 for (
const google::protobuf::RepeatedField<int32_t>* literals :
733 tmp_to_clear.clear();
734 for (
const int literal : *literals) {
737 const int rep = orbits[
var];
738 if (rep == -1)
continue;
740 if (tmp_sizes[rep] == 0) tmp_to_clear.push_back(rep);
745 int num_positive_direction = 0;
746 int num_negative_direction = 0;
751 bool possible_extension =
false;
753 rows_in_at_most_one.clear();
754 for (
const int row : tmp_to_clear) {
755 const int size = tmp_sizes[
row];
756 const int num_positive = tmp_num_positive[
row];
757 const int num_negative = tmp_sizes[
row] - tmp_num_positive[
row];
759 tmp_num_positive[
row] = 0;
761 if (num_positive > 1 && num_negative == 0) {
762 if (size < num_cols) possible_extension =
true;
763 rows_in_at_most_one.push_back(
row);
764 ++num_positive_direction;
765 }
else if (num_positive == 0 && num_negative > 1) {
766 if (size < num_cols) possible_extension =
true;
767 rows_in_at_most_one.push_back(
row);
768 ++num_negative_direction;
769 }
else if (num_positive > 0 && num_negative > 0) {
770 all_equivalent_rows[
row] =
true;
774 if (possible_extension) {
776 "TODO symmetry: possible at most one extension.");
779 if (num_positive_direction > 0 && num_negative_direction > 0) {
780 return context->NotifyThatModelIsUnsat(
"Symmetry and at most ones");
782 const bool direction = num_positive_direction > 0;
794 for (
const int row : rows_in_at_most_one) {
798 if (score > best_score) {
799 at_most_one_in_best_rows = direction;
801 best_rows = rows_in_at_most_one;
810 for (
int i = 0; i < all_equivalent_rows.size(); ++i) {
811 if (all_equivalent_rows[i]) {
812 for (
int j = 1; j < num_cols; ++j) {
813 context->StoreBooleanEqualityRelation(orbitope[i][0], orbitope[i][j]);
814 context->UpdateRuleStats(
"symmetry: all equivalent in orbit");
815 if (
context->ModelIsUnsat())
return false;
828 if (best_score == 0) {
830 "TODO symmetry: add symmetry breaking inequalities?");
837 for (
const int i : best_rows) {
838 for (
int j = 0; j < num_cols; ++j) {
839 const int var = orbitope[i][j];
840 if ((at_most_one_in_best_rows &&
context->LiteralIsTrue(
var)) ||
841 (!at_most_one_in_best_rows &&
context->LiteralIsFalse(
var))) {
842 return context->NotifyThatModelIsUnsat(
"Symmetry and at most one");
851 const int best_col = 0;
852 for (
const int i : best_rows) {
853 for (
int j = 0; j < num_cols; ++j) {
854 if (j == best_col)
continue;
855 const int var = orbitope[i][j];
856 if (at_most_one_in_best_rows) {
857 context->UpdateRuleStats(
"symmetry: fixed to false");
858 if (!
context->SetLiteralToFalse(
var))
return false;
860 context->UpdateRuleStats(
"symmetry: fixed to true");
861 if (!
context->SetLiteralToTrue(
var))
return false;
867 for (
const int i : best_rows) orbitope[i].clear();
869 for (
int i = 0; i < orbitope.size(); ++i) {
870 if (!orbitope[i].empty()) orbitope[new_size++] = orbitope[i];
872 CHECK_LT(new_size, orbitope.size());
873 orbitope.resize(new_size);
876 for (
int i = 0; i < orbitope.size(); ++i) {
877 std::swap(orbitope[i][best_col], orbitope[i].back());
878 orbitope[i].pop_back();
884 if (orbitope.size() == 1) {
885 const int num_cols = orbitope[0].size();
886 for (
int i = 0; i + 1 < num_cols; ++i) {
888 ConstraintProto*
ct =
context->working_model->add_constraints();
889 ct->mutable_linear()->add_coeffs(1);
890 ct->mutable_linear()->add_vars(orbitope[0][i]);
891 ct->mutable_linear()->add_coeffs(-1);
892 ct->mutable_linear()->add_vars(orbitope[0][i + 1]);
893 ct->mutable_linear()->add_domain(0);
895 context->UpdateRuleStats(
"symmetry: added symmetry breaking inequality");
897 context->UpdateNewConstraintsVariableUsage();
#define CHECK_LT(val1, val2)
#define CHECK_EQ(val1, val2)
#define DCHECK_GE(val1, val2)
#define CHECK_NE(val1, val2)
#define DCHECK(condition)
#define DCHECK_EQ(val1, val2)
#define VLOG(verboselevel)
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(const std::vector< int > &cycle_indices)
const std::vector< int > & Support() const
Iterator Cycle(int i) const
static std::unique_ptr< TimeLimit > FromDeterministicTime(double deterministic_limit)
Creates a time limit object that puts limit only on the deterministic time.
SharedTimeLimit * time_limit
GurobiMPCallbackContext * context
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)
void swap(IdMap< K, V > &a, IdMap< K, V > &b)
void DetectAndAddSymmetryToProto(const SatParameters ¶ms, CpModelProto *proto, SolverLogger *logger)
bool DetectAndExploitSymmetriesInPresolve(PresolveContext *context)
bool RefIsPositive(int ref)
std::vector< int > GetOrbitopeOrbits(int n, const std::vector< std::vector< int >> &orbitope)
Graph * GenerateGraphForSymmetryDetection(const LinearBooleanProblem &problem, std::vector< int > *initial_equivalence_classes)
void FindCpModelSymmetries(const SatParameters ¶ms, const CpModelProto &problem, std::vector< std::unique_ptr< SparsePermutation >> *generators, double deterministic_limit, SolverLogger *logger)
void FillDomainInProto(const Domain &domain, ProtoWithDomain *proto)
std::string ConstraintCaseName(ConstraintProto::ConstraintCase constraint_case)
std::vector< std::vector< int > > BasicOrbitopeExtraction(const std::vector< std::unique_ptr< SparsePermutation >> &generators)
std::vector< int > GetOrbits(int n, const std::vector< std::unique_ptr< SparsePermutation >> &generators)
Collection of objects used to extend the Constraint Solver library.
uint64_t Hash(uint64_t num, uint64_t c)
std::vector< int >::const_iterator begin() const
#define SOLVER_LOG(logger,...)