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_;
64template <
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);
88template <
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()) {
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);
238 ? constraint.linear().coeffs(i)
239 : -constraint.linear().coeffs(i);
240 graph->AddArc(get_coefficient_node(variable_node, coeff),
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);
253 if (constraint.at_most_one().literals().size() == 2) {
255 add_implication(constraint.at_most_one().literals(0),
256 NegatedRef(constraint.at_most_one().literals(1)));
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);
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);
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);
283 if (constraint.enforcement_literal_size() != 1) {
286 "[Symmetry] BoolAnd with multiple enforcement literal are not "
287 "supported in symmetry code:",
288 constraint.ShortDebugString());
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);
306 VLOG(1) <<
"Unsupported constraint type "
317 for (
const int ref : constraint.enforcement_literal()) {
318 graph->AddArc(constraint_node, get_literal_node(ref));
324 DCHECK_EQ(graph->num_nodes(), initial_equivalence_classes->size());
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]) {
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.");
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++;
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++;
379 std::vector<std::unique_ptr<SparsePermutation>>* generators,
381 CHECK(generators !=
nullptr);
386 std::vector<int> equivalence_classes;
387 std::unique_ptr<Graph> graph(GenerateGraphForSymmetryDetection<Graph>(
388 problem, &equivalence_classes, logger));
389 if (graph ==
nullptr)
return;
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;
396 std::vector<int> factorized_automorphism_group_size;
400 &equivalence_classes, generators, &factorized_automorphism_group_size,
407 "[Symmetry] GraphSymmetryFinder error: ", status.message());
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) {
418 std::vector<int> to_delete;
419 for (
int j = 0; j < permutation->
NumCycles(); ++j) {
424 to_delete.push_back(j);
427 for (
const int node : permutation->
Cycle(j)) {
435 if (!permutation->
Support().empty()) {
436 average_support_size += permutation->
Support().size();
437 swap((*generators)[num_generators], (*generators)[i]);
440 ++num_duplicate_constraints;
443 generators->resize(num_generators);
444 average_support_size /= num_generators;
445 SOLVER_LOG(logger,
"[Symmetry] Symmetry computation done. time: ",
448 if (num_generators > 0) {
449 SOLVER_LOG(logger,
"[Symmetry] # of generators: ", num_generators);
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 !");
464 std::vector<std::unique_ptr<SparsePermutation>> generators;
467 if (generators.empty())
return;
469 for (
const std::unique_ptr<SparsePermutation>& perm : generators) {
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)) {
482 if (orbitope.empty())
return;
483 SOLVER_LOG(logger,
"[Symmetry] Found orbitope of size ", orbitope.size(),
484 " x ", orbitope[0].size());
488 for (
const std::vector<int>&
row : orbitope) {
489 for (
const int entry :
row) {
500 if (
context->working_model->has_objective()) {
501 context->WriteObjectiveToProto();
504 for (
int i = 0; i < num_vars; ++i) {
506 context->working_model->mutable_variables(i));
511 int64_t num_added = 0;
513 for (
int var = 0;
var < num_vars; ++
var) {
515 if (
context->VariableWasRemoved(
var))
continue;
516 if (
context->VariableIsNotUsedAnymore(
var))
continue;
523 auto* arg =
ct->mutable_linear();
527 arg->add_coeffs(-r.
coeff);
528 arg->add_domain(r.
offset);
529 arg->add_domain(r.
offset);
532 std::vector<std::unique_ptr<SparsePermutation>> generators;
537 context->working_model->mutable_constraints()->DeleteSubrange(
538 initial_ct_index, num_added);
540 if (generators.empty())
return true;
547 std::vector<const google::protobuf::RepeatedField<int32_t>*> at_most_ones;
563 std::vector<int> can_be_fixed_to_false;
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);
572 max_orbit_size =
std::max(max_orbit_size, orbit_sizes[rep]);
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 :
579 tmp_to_clear.clear();
583 for (
const int literal : *literals) {
588 const int rep = orbits[
var];
589 if (rep == -1)
continue;
592 if (tmp_sizes[rep] == 0) tmp_to_clear.push_back(rep);
593 if (tmp_sizes[rep] > 0) ++num_fixable;
598 if (num_fixable > can_be_fixed_to_false.size()) {
599 can_be_fixed_to_false.clear();
600 for (
const int literal : *literals) {
605 const int rep = orbits[
var];
606 if (rep == -1)
continue;
609 if (tmp_sizes[rep] == 0) can_be_fixed_to_false.push_back(
var);
614 for (
const int rep : tmp_to_clear) tmp_sizes[rep] = 0;
620 "[Symmetry] Num fixable by intersecting at_most_one with orbits: ",
621 can_be_fixed_to_false.size(),
" largest_orbit: ", max_orbit_size);
640 if (!orbitope.empty()) {
642 orbitope.size(),
" x ", orbitope[0].size());
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;
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;
669 if (orbitope.empty())
return true;
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);
679 for (
const google::protobuf::RepeatedField<int32_t>* literals :
681 for (
const int lit : *literals) {
686 for (
const int lit : *literals) {
691 while (!orbitope.empty() && orbitope[0].size() > 1) {
692 const int num_cols = orbitope[0].size();
723 std::vector<bool> all_equivalent_rows(orbitope.size(),
false);
729 bool at_most_one_in_best_rows;
730 int64_t best_score = 0;
731 std::vector<int> best_rows;
733 std::vector<int> rows_in_at_most_one;
734 for (
const google::protobuf::RepeatedField<int32_t>* literals :
736 tmp_to_clear.clear();
737 for (
const int literal : *literals) {
740 const int rep = orbits[
var];
741 if (rep == -1)
continue;
743 if (tmp_sizes[rep] == 0) tmp_to_clear.push_back(rep);
748 int num_positive_direction = 0;
749 int num_negative_direction = 0;
754 bool possible_extension =
false;
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];
762 tmp_num_positive[
row] = 0;
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;
777 if (possible_extension) {
779 "TODO symmetry: possible at most one extension.");
782 if (num_positive_direction > 0 && num_negative_direction > 0) {
783 return context->NotifyThatModelIsUnsat(
"Symmetry and at most ones");
785 const bool direction = num_positive_direction > 0;
797 for (
const int row : rows_in_at_most_one) {
801 if (score > best_score) {
802 at_most_one_in_best_rows = direction;
804 best_rows = rows_in_at_most_one;
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;
831 if (best_score == 0) {
833 "TODO symmetry: add symmetry breaking inequalities?");
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");
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;
863 context->UpdateRuleStats(
"symmetry: fixed to true");
864 if (!
context->SetLiteralToTrue(
var))
return false;
870 for (
const int i : best_rows) orbitope[i].clear();
872 for (
int i = 0; i < orbitope.size(); ++i) {
873 if (!orbitope[i].empty()) orbitope[new_size++] = orbitope[i];
875 CHECK_LT(new_size, orbitope.size());
876 orbitope.resize(new_size);
879 for (
int i = 0; i < orbitope.size(); ++i) {
880 std::swap(orbitope[i][best_col], orbitope[i].back());
881 orbitope[i].pop_back();
887 if (orbitope.size() == 1) {
888 const int num_cols = orbitope[0].size();
889 for (
int i = 0; i + 1 < num_cols; ++i) {
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);
898 context->UpdateRuleStats(
"symmetry: added symmetry breaking inequality");
900 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)
double GetElapsedDeterministicTime() const
const std::vector< int > & Support() const
void RemoveCycles(const std::vector< int > &cycle_indices)
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.
::PROTOBUF_NAMESPACE_ID::int32 literals(int index) const
const ::operations_research::sat::BoolArgumentProto & at_most_one() const
ConstraintCase constraint_case() const
const ::operations_research::sat::BoolArgumentProto & exactly_one() const
::operations_research::sat::SymmetryProto * mutable_symmetry()
int variables_size() const
int constraints_size() const
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()
ModelSharedTimeLimit * 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)
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 ¶ms, 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)
std::vector< int >::const_iterator begin() const
#define SOLVER_LOG(logger,...)