27#include "absl/algorithm/container.h"
28#include "absl/container/btree_map.h"
29#include "absl/container/flat_hash_map.h"
30#include "absl/container/flat_hash_set.h"
31#include "absl/log/check.h"
32#include "absl/meta/type_traits.h"
33#include "absl/status/status.h"
34#include "absl/strings/str_cat.h"
35#include "absl/strings/str_join.h"
36#include "absl/types/span.h"
37#include "google/protobuf/message.h"
44#include "ortools/sat/cp_model.pb.h"
51#include "ortools/sat/sat_parameters.pb.h"
67 std::size_t operator()(absl::Span<const int64_t> values)
const {
69 for (
const int64_t value : values) {
76struct NodeExprCompare {
77 bool operator()(
const LinearExpressionProto& a,
78 const LinearExpressionProto&
b)
const {
79 if (a.offset() !=
b.offset())
return a.offset() <
b.offset();
80 if (a.vars_size() !=
b.vars_size())
return a.vars_size() <
b.vars_size();
81 for (
int i = 0;
i < a.vars_size(); ++
i) {
82 if (a.vars(
i) !=
b.vars(
i))
return a.vars(
i) <
b.vars(
i);
83 if (a.coeffs(
i) !=
b.coeffs(
i))
return a.coeffs(
i) <
b.coeffs(
i);
93 IdGenerator() =
default;
97 int GetId(
const std::vector<int64_t>& color) {
99 return id_map_.insert({color, id_map_.size()}).first->second;
102 int NextFreeId()
const {
return id_map_.size(); }
105 absl::flat_hash_map<std::vector<int64_t>, int, VectorHash> id_map_;
111template <
typename FieldInt64Type>
113 const google::protobuf::RepeatedField<FieldInt64Type>& repeated_field,
114 std::vector<int64_t>* vector) {
115 CHECK(vector !=
nullptr);
116 for (
const FieldInt64Type value : repeated_field) {
117 vector->push_back(value);
121bool IsIntervalFixedSize(
const IntervalConstraintProto& interval) {
122 if (!interval.size().vars().empty()) {
125 if (interval.start().vars().size() != interval.end().vars().size()) {
128 for (
int i = 0;
i < interval.start().vars().size(); ++
i) {
129 if (interval.start().coeffs(
i) != interval.end().coeffs(
i)) {
132 if (interval.start().vars(
i) != interval.end().vars(
i)) {
136 if (interval.end().offset() !=
137 interval.start().offset() + interval.size().offset()) {
157template <
typename Graph>
159 const CpModelProto& problem, std::vector<int>* initial_equivalence_classes,
160 SolverLogger* logger) {
161 CHECK(initial_equivalence_classes !=
nullptr);
163 const int num_variables = problem.variables_size();
164 auto graph = std::make_unique<Graph>();
174 VAR_COEFFICIENT_NODE,
178 IdGenerator color_id_generator;
179 initial_equivalence_classes->clear();
180 auto new_node_from_id = [&initial_equivalence_classes, &graph](
int color_id) {
183 const int node = initial_equivalence_classes->size();
184 initial_equivalence_classes->push_back(color_id);
188 graph->AddNode(node);
191 auto new_node = [&new_node_from_id,
192 &color_id_generator](
const std::vector<int64_t>& color) {
193 return new_node_from_id(color_id_generator.GetId(color));
201 std::vector<int64_t> objective_by_var(num_variables, 0);
202 for (
int i = 0;
i < problem.objective().vars_size(); ++
i) {
203 const int ref = problem.objective().vars(
i);
205 const int64_t coeff = problem.objective().coeffs(
i);
211 std::vector<int64_t> tmp_color;
212 for (
int v = 0; v < num_variables; ++v) {
213 tmp_color = {VARIABLE_NODE, objective_by_var[v]};
214 Append(problem.variables(v).domain(), &tmp_color);
215 CHECK_EQ(v, new_node(tmp_color));
218 const int color_id_for_coeff_one =
219 color_id_generator.GetId({VAR_COEFFICIENT_NODE, 1});
220 const int color_id_for_coeff_minus_one =
221 color_id_generator.GetId({VAR_COEFFICIENT_NODE, -1});
225 absl::flat_hash_map<std::pair<int64_t, int64_t>,
int> coefficient_nodes;
226 auto get_coefficient_node =
227 [&new_node_from_id, &graph, &coefficient_nodes, &color_id_generator,
228 &tmp_color, color_id_for_coeff_minus_one](
int var, int64_t coeff) {
229 const int var_node = var;
235 if (coeff == 1)
return var_node;
238 coefficient_nodes.insert({std::make_pair(var, coeff), 0});
239 if (!insert.second)
return insert.first->second;
245 color_id = color_id_for_coeff_minus_one;
247 tmp_color = {VAR_COEFFICIENT_NODE, coeff};
248 color_id = color_id_generator.GetId(tmp_color);
250 const int secondary_node = new_node_from_id(color_id);
251 graph->AddArc(var_node, secondary_node);
252 insert.first->second = secondary_node;
253 return secondary_node;
259 auto get_literal_node = [&get_coefficient_node](
int ref) {
275 absl::flat_hash_set<std::pair<int, int>> implications;
276 auto get_implication_node = [&new_node_from_id, &graph, &coefficient_nodes,
277 color_id_for_coeff_one,
278 color_id_for_coeff_minus_one](
int ref) {
282 coefficient_nodes.insert({std::make_pair(var, coeff), 0});
283 if (!insert.second)
return insert.first->second;
284 const int secondary_node = new_node_from_id(
285 coeff == 1 ? color_id_for_coeff_one : color_id_for_coeff_minus_one);
286 graph->AddArc(var, secondary_node);
287 insert.first->second = secondary_node;
288 return secondary_node;
290 auto add_implication = [&get_implication_node, &graph, &implications](
291 int ref_a,
int ref_b) {
292 const auto insert = implications.insert({ref_a, ref_b});
293 if (!insert.second)
return;
294 graph->AddArc(get_implication_node(ref_a), get_implication_node(ref_b));
298 graph->AddArc(get_implication_node(
NegatedRef(ref_b)),
302 auto make_linear_expr_node = [&new_node, &graph, &get_coefficient_node](
303 const LinearExpressionProto& expr,
304 const std::vector<int64_t>& color) {
305 std::vector<int64_t> local_color = color;
306 local_color.push_back(expr.offset());
307 const int local_node = new_node(local_color);
309 for (
int i = 0;
i < expr.vars().size(); ++
i) {
310 const int ref = expr.vars(
i);
312 const int64_t coeff =
314 graph->AddArc(get_coefficient_node(var_node, coeff), local_node);
319 absl::btree_map<LinearExpressionProto, int, NodeExprCompare> expr_nodes;
320 auto shared_linear_expr_node =
321 [&make_linear_expr_node, &expr_nodes](
const LinearExpressionProto& expr) {
322 const auto [it, inserted] = expr_nodes.insert({expr, 0});
324 const std::vector<int64_t> local_color = {VAR_LIN_EXPR_NODE,
326 it->second = make_linear_expr_node(expr, local_color);
332 absl::flat_hash_map<int, int> interval_constraint_index_to_node;
335 for (
int constraint_index = 0; constraint_index < problem.constraints_size();
336 ++constraint_index) {
337 const ConstraintProto& constraint = problem.constraints(constraint_index);
338 const int constraint_node = initial_equivalence_classes->size();
339 std::vector<int64_t> color = {CONSTRAINT_NODE,
340 constraint.constraint_case()};
342 switch (constraint.constraint_case()) {
343 case ConstraintProto::CONSTRAINT_NOT_SET:
348 case ConstraintProto::kLinear: {
353 Append(constraint.linear().domain(), &color);
354 CHECK_EQ(constraint_node, new_node(color));
355 for (
int i = 0;
i < constraint.linear().vars_size(); ++
i) {
356 const int ref = constraint.linear().vars(
i);
359 ? constraint.linear().coeffs(
i)
360 : -constraint.linear().coeffs(
i);
361 graph->AddArc(get_coefficient_node(variable_node, coeff),
366 case ConstraintProto::kAllDiff: {
367 CHECK_EQ(constraint_node, new_node(color));
368 for (
const LinearExpressionProto& expr :
369 constraint.all_diff().exprs()) {
370 graph->AddArc(shared_linear_expr_node(expr), constraint_node);
374 case ConstraintProto::kBoolOr: {
375 CHECK_EQ(constraint_node, new_node(color));
376 for (
const int ref : constraint.bool_or().literals()) {
377 graph->AddArc(get_literal_node(ref), constraint_node);
381 case ConstraintProto::kAtMostOne: {
382 if (constraint.at_most_one().literals().size() == 2) {
384 add_implication(constraint.at_most_one().literals(0),
385 NegatedRef(constraint.at_most_one().literals(1)));
389 CHECK_EQ(constraint_node, new_node(color));
390 for (
const int ref : constraint.at_most_one().literals()) {
391 graph->AddArc(get_literal_node(ref), constraint_node);
395 case ConstraintProto::kExactlyOne: {
396 CHECK_EQ(constraint_node, new_node(color));
397 for (
const int ref : constraint.exactly_one().literals()) {
398 graph->AddArc(get_literal_node(ref), constraint_node);
402 case ConstraintProto::kBoolXor: {
403 CHECK_EQ(constraint_node, new_node(color));
404 for (
const int ref : constraint.bool_xor().literals()) {
405 graph->AddArc(get_literal_node(ref), constraint_node);
409 case ConstraintProto::kBoolAnd: {
410 if (constraint.enforcement_literal_size() > 1) {
411 CHECK_EQ(constraint_node, new_node(color));
412 for (
const int ref : constraint.bool_and().literals()) {
413 graph->AddArc(get_literal_node(ref), constraint_node);
418 CHECK_EQ(constraint.enforcement_literal_size(), 1);
419 const int ref_a = constraint.enforcement_literal(0);
420 for (
const int ref_b : constraint.bool_and().literals()) {
421 add_implication(ref_a, ref_b);
425 case ConstraintProto::kLinMax: {
426 const LinearExpressionProto& target_expr =
427 constraint.lin_max().target();
429 const int target_node = make_linear_expr_node(target_expr, color);
431 for (
int i = 0;
i < constraint.lin_max().exprs_size(); ++
i) {
432 const LinearExpressionProto& expr = constraint.lin_max().exprs(
i);
433 graph->AddArc(shared_linear_expr_node(expr), target_node);
438 case ConstraintProto::kInterval: {
439 static constexpr int kFixedIntervalColor = 0;
440 static constexpr int kNonFixedIntervalColor = 1;
441 if (IsIntervalFixedSize(constraint.interval())) {
442 std::vector<int64_t> local_color = color;
443 local_color.push_back(kFixedIntervalColor);
444 local_color.push_back(constraint.interval().size().offset());
445 const int full_node =
446 make_linear_expr_node(constraint.interval().start(), local_color);
447 CHECK_EQ(full_node, constraint_node);
452 std::vector<int64_t> local_color = color;
453 local_color.push_back(kNonFixedIntervalColor);
455 local_color.push_back(0);
456 const int start_node =
457 make_linear_expr_node(constraint.interval().start(), local_color);
458 local_color.pop_back();
459 CHECK_EQ(start_node, constraint_node);
463 const int size_node =
464 shared_linear_expr_node(constraint.interval().size());
466 local_color.push_back(1);
468 make_linear_expr_node(constraint.interval().end(), local_color);
469 local_color.pop_back();
473 graph->AddArc(start_node, end_node);
474 graph->AddArc(end_node, size_node);
476 interval_constraint_index_to_node[constraint_index] = constraint_node;
479 case ConstraintProto::kNoOverlap: {
483 CHECK_EQ(constraint_node, new_node(color));
484 for (
const int interval : constraint.no_overlap().intervals()) {
485 graph->AddArc(interval_constraint_index_to_node.at(interval),
490 case ConstraintProto::kNoOverlap2D: {
494 CHECK_EQ(constraint_node, new_node(color));
495 std::vector<int64_t> local_color = color;
496 local_color.push_back(0);
497 const int size = constraint.no_overlap_2d().x_intervals().size();
498 const int node_x = new_node(local_color);
499 const int node_y = new_node(local_color);
500 local_color.pop_back();
501 graph->AddArc(constraint_node, node_x);
502 graph->AddArc(constraint_node, node_y);
503 local_color.push_back(1);
504 for (
int i = 0;
i < size; ++
i) {
505 const int box_node = new_node(local_color);
506 graph->AddArc(box_node, constraint_node);
507 const int x = constraint.no_overlap_2d().x_intervals(
i);
508 const int y = constraint.no_overlap_2d().y_intervals(
i);
509 graph->AddArc(interval_constraint_index_to_node.at(x), node_x);
510 graph->AddArc(interval_constraint_index_to_node.at(x), box_node);
511 graph->AddArc(interval_constraint_index_to_node.at(y), node_y);
512 graph->AddArc(interval_constraint_index_to_node.at(y), box_node);
516 case ConstraintProto::kCumulative: {
520 const CumulativeConstraintProto& ct = constraint.cumulative();
521 std::vector<int64_t> capacity_color = color;
522 capacity_color.push_back(0);
523 CHECK_EQ(constraint_node, new_node(capacity_color));
524 graph->AddArc(constraint_node,
525 make_linear_expr_node(ct.capacity(), capacity_color));
527 std::vector<int64_t> task_color = color;
528 task_color.push_back(1);
529 for (
int i = 0;
i < ct.intervals().size(); ++
i) {
530 const int task_node =
531 make_linear_expr_node(ct.demands(
i), task_color);
532 graph->AddArc(task_node, constraint_node);
533 graph->AddArc(task_node,
534 interval_constraint_index_to_node.at(ct.intervals(
i)));
538 case ConstraintProto::kCircuit: {
542 const int num_arcs = constraint.circuit().literals().size();
543 absl::flat_hash_map<int, int> circuit_node_to_symmetry_node;
544 std::vector<int64_t> arc_color = color;
545 arc_color.push_back(1);
546 for (
int i = 0;
i < num_arcs; ++
i) {
547 const int literal = constraint.circuit().literals(
i);
548 const int tail = constraint.circuit().tails(
i);
549 const int head = constraint.circuit().heads(
i);
550 const int arc_node = new_node(arc_color);
551 if (!circuit_node_to_symmetry_node.contains(head)) {
552 circuit_node_to_symmetry_node[head] = new_node(color);
554 const int head_node = circuit_node_to_symmetry_node[head];
555 if (!circuit_node_to_symmetry_node.contains(tail)) {
556 circuit_node_to_symmetry_node[tail] = new_node(color);
558 const int tail_node = circuit_node_to_symmetry_node[tail];
561 graph->AddArc(tail_node, arc_node);
562 graph->AddArc(arc_node, get_literal_node(literal));
563 graph->AddArc(arc_node, head_node);
574 VLOG(1) <<
"Unsupported constraint type "
584 if (constraint.constraint_case() != ConstraintProto::kBoolAnd ||
585 constraint.enforcement_literal().size() > 1) {
586 for (
const int ref : constraint.enforcement_literal()) {
587 graph->AddArc(constraint_node, get_literal_node(ref));
593 DCHECK_EQ(graph->num_nodes(), initial_equivalence_classes->size());
600 const int num_nodes = graph->num_nodes();
601 std::vector<int> in_degree(num_nodes, 0);
602 std::vector<int> out_degree(num_nodes, 0);
603 for (
int i = 0;
i < num_nodes; ++
i) {
604 out_degree[
i] = graph->OutDegree(
i);
605 for (
const int head : (*graph)[
i]) {
609 for (
int i = 0;
i < num_nodes; ++
i) {
610 if (in_degree[
i] >= num_nodes || out_degree[
i] >= num_nodes) {
611 SOLVER_LOG(logger,
"[Symmetry] Too many multi-arcs in symmetry code.");
624 int next_id = color_id_generator.NextFreeId();
625 for (
int i = 0;
i < num_variables; ++
i) {
626 if ((*graph)[
i].empty()) {
627 (*initial_equivalence_classes)[
i] = next_id++;
633 std::vector<int> mapping(next_id, -1);
634 for (
int& ref : *initial_equivalence_classes) {
635 if (mapping[ref] == -1) {
636 ref = mapping[ref] =
id++;
647 const SatParameters& params,
const CpModelProto& problem,
648 std::vector<std::unique_ptr<SparsePermutation>>* generators,
650 CHECK(generators !=
nullptr);
653 if (params.symmetry_level() < 3 && problem.variables().size() > 1e6 &&
654 problem.constraints().size() > 1e6) {
656 "[Symmetry] Problem too large. Skipping. You can use "
657 "symmetry_level:3 or more to force it.");
663 std::vector<int> equivalence_classes;
665 problem, &equivalence_classes, logger));
666 if (graph ==
nullptr)
return;
668 SOLVER_LOG(logger,
"[Symmetry] Graph for symmetry has ",
671 if (graph->num_nodes() == 0)
return;
673 if (params.symmetry_level() < 3 && graph->num_nodes() > 1e6 &&
674 graph->num_arcs() > 1e6) {
676 "[Symmetry] Graph too large. Skipping. You can use "
677 "symmetry_level:3 or more to force it.");
682 std::vector<int> factorized_automorphism_group_size;
686 &equivalence_classes, generators, &factorized_automorphism_group_size,
693 "[Symmetry] GraphSymmetryFinder error: ", status.message());
699 double average_support_size = 0.0;
700 int num_generators = 0;
701 int num_duplicate_constraints = 0;
702 for (
int i = 0;
i < generators->size(); ++
i) {
704 std::vector<int> to_delete;
705 for (
int j = 0; j < permutation->
NumCycles(); ++j) {
709 if (*(permutation->
Cycle(j).
begin()) >= problem.variables_size()) {
710 to_delete.push_back(j);
713 for (
const int node : permutation->
Cycle(j)) {
714 DCHECK_GE(node, problem.variables_size());
721 if (!permutation->
Support().empty()) {
722 average_support_size += permutation->
Support().size();
723 swap((*generators)[num_generators], (*generators)[
i]);
726 ++num_duplicate_constraints;
729 generators->resize(num_generators);
730 SOLVER_LOG(logger,
"[Symmetry] Symmetry computation done. time: ",
732 " dtime: ",
time_limit->GetElapsedDeterministicTime());
733 if (num_generators > 0) {
734 average_support_size /= num_generators;
735 SOLVER_LOG(logger,
"[Symmetry] #generators: ", num_generators,
736 ", average support size: ", average_support_size);
737 if (num_duplicate_constraints > 0) {
738 SOLVER_LOG(logger,
"[Symmetry] The model contains ",
739 num_duplicate_constraints,
" duplicate constraints !");
746void LogOrbitInformation(absl::Span<const int> var_to_orbit_index,
750 int num_touched_vars = 0;
751 std::vector<int> orbit_sizes;
752 for (
int var = 0; var < var_to_orbit_index.size(); ++var) {
753 const int rep = var_to_orbit_index[var];
754 if (rep == -1)
continue;
755 if (rep >= orbit_sizes.size()) orbit_sizes.resize(rep + 1, 0);
759 std::sort(orbit_sizes.begin(), orbit_sizes.end(), std::greater<int>());
760 const int num_orbits = orbit_sizes.size();
761 if (num_orbits > 10) orbit_sizes.resize(10);
762 SOLVER_LOG(logger,
"[Symmetry] ", num_orbits,
" orbits on ", num_touched_vars,
763 " variables with sizes: ", absl::StrJoin(orbit_sizes,
","),
764 (num_orbits > orbit_sizes.size() ?
",..." :
""));
771 SymmetryProto* symmetry = proto->mutable_symmetry();
774 std::vector<std::unique_ptr<SparsePermutation>> generators;
776 params.symmetry_detection_deterministic_time_limit(),
778 if (generators.empty()) {
779 proto->clear_symmetry();
787 const int num_vars = proto->variables().size();
788 const std::vector<int> orbits =
GetOrbits(num_vars, generators);
789 LogOrbitInformation(orbits, logger);
791 for (
const std::unique_ptr<SparsePermutation>& perm : generators) {
792 SparsePermutationProto* perm_proto = symmetry->add_permutations();
793 const int num_cycle = perm->NumCycles();
794 for (
int i = 0;
i < num_cycle; ++
i) {
795 const int old_size = perm_proto->support().size();
796 for (
const int var : perm->Cycle(
i)) {
797 perm_proto->add_support(var);
799 perm_proto->add_cycle_sizes(perm_proto->support().size() - old_size);
804 if (orbitope.empty())
return;
805 SOLVER_LOG(logger,
"[Symmetry] Found orbitope of size ", orbitope.size(),
806 " x ", orbitope[0].size());
807 DenseMatrixProto* matrix = symmetry->add_orbitopes();
808 matrix->set_num_rows(orbitope.size());
809 matrix->set_num_cols(orbitope[0].size());
810 for (
const std::vector<int>& row : orbitope) {
811 for (
const int entry : row) {
812 matrix->add_entries(entry);
833void OrbitAndPropagation(absl::Span<const int> orbits,
int var,
834 std::vector<int>* can_be_fixed_to_false,
835 PresolveContext* context) {
838 if (context->IsFixed(var))
return;
839 if (!context->CanBeUsedAsLiteral(var))
return;
855 auto* sat_solver = model.GetOrCreate<SatSolver>();
856 auto* mapping = model.GetOrCreate<CpModelMapping>();
857 const Literal to_propagate = mapping->Literal(var);
859 const VariablesAssignment& assignment = sat_solver->Assignment();
860 if (assignment.LiteralIsAssigned(to_propagate))
return;
861 sat_solver->EnqueueDecisionAndBackjumpOnConflict(to_propagate);
862 if (sat_solver->CurrentDecisionLevel() != 1)
return;
865 can_be_fixed_to_false->clear();
867 const int orbit_index = orbits[var];
868 const int num_variables = orbits.size();
869 for (
int var = 0; var < num_variables; ++var) {
870 if (orbits[var] != orbit_index)
continue;
874 DCHECK(!context->IsFixed(var));
875 DCHECK(context->CanBeUsedAsLiteral(var));
877 if (assignment.LiteralIsFalse(mapping->Literal(var))) {
878 can_be_fixed_to_false->push_back(var);
881 if (!can_be_fixed_to_false->empty()) {
883 "[Symmetry] Num fixable by binary propagation in orbit: ",
884 can_be_fixed_to_false->size(),
" / ", orbit_size);
888std::vector<int64_t> BuildInequalityCoeffsForOrbitope(
889 absl::Span<const int64_t> maximum_values, int64_t max_linear_size,
890 bool* is_approximated) {
891 std::vector<int64_t> out(maximum_values.size());
892 int64_t range_product = 1;
893 uint64_t greatest_coeff = 0;
894 for (
int i = 0;
i < maximum_values.size(); ++
i) {
895 out[
i] = range_product;
897 std::max(greatest_coeff,
static_cast<uint64_t
>(maximum_values[
i]));
898 range_product =
CapProd(range_product, 1 + maximum_values[
i]);
901 if (range_product <= max_linear_size) {
905 *is_approximated =
false;
908 *is_approximated =
true;
910 const auto compute_approximate_coeffs =
911 [max_linear_size, maximum_values](
double scaling_factor,
912 std::vector<int64_t>* coeffs) ->
bool {
913 int64_t max_size = 0;
914 double cumulative_product_double = 1.0;
915 for (
int i = 0;
i < maximum_values.size(); ++
i) {
916 const int64_t max = maximum_values[
i];
917 const int64_t coeff =
static_cast<int64_t
>(cumulative_product_double);
918 (*coeffs)[
i] = coeff;
919 cumulative_product_double *= scaling_factor * max + 1;
921 if (max_size > max_linear_size)
return false;
927 0.0, 1.0, [&compute_approximate_coeffs, &out](
double scaling_factor) {
928 return compute_approximate_coeffs(scaling_factor, &out);
930 CHECK(compute_approximate_coeffs(scaling, &out));
937 const SatParameters& params = context->
params();
948 int64_t num_added = 0;
949 const int initial_ct_index = proto.constraints().size();
950 const int num_vars = proto.variables_size();
951 for (
int var = 0; var < num_vars; ++var) {
952 if (context->
IsFixed(var))
continue;
960 ConstraintProto* ct = context->
working_model->add_constraints();
961 auto* arg = ct->mutable_linear();
965 arg->add_coeffs(-r.
coeff);
966 arg->add_domain(r.
offset);
967 arg->add_domain(r.
offset);
970 std::vector<std::unique_ptr<SparsePermutation>> generators;
972 params, proto, &generators,
973 context->
params().symmetry_detection_deterministic_time_limit(),
977 context->
working_model->mutable_constraints()->DeleteSubrange(
978 initial_ct_index, num_added);
980 if (generators.empty())
return true;
987 std::vector<const google::protobuf::RepeatedField<int32_t>*> at_most_ones;
988 for (
int i = 0;
i < proto.constraints_size(); ++
i) {
989 if (proto.constraints(
i).constraint_case() == ConstraintProto::kAtMostOne) {
990 at_most_ones.push_back(&proto.constraints(
i).at_most_one().literals());
992 if (proto.constraints(
i).constraint_case() ==
993 ConstraintProto::kExactlyOne) {
994 at_most_ones.push_back(&proto.constraints(
i).exactly_one().literals());
1002 int distinguished_var = -1;
1003 std::vector<int> can_be_fixed_to_false;
1006 const std::vector<int> orbits =
GetOrbits(num_vars, generators);
1007 std::vector<int> orbit_sizes;
1008 int max_orbit_size = 0;
1009 int sum_of_orbit_sizes = 0;
1010 for (
int var = 0; var < num_vars; ++var) {
1011 const int rep = orbits[var];
1012 if (rep == -1)
continue;
1013 if (rep >= orbit_sizes.size()) orbit_sizes.resize(rep + 1, 0);
1014 ++sum_of_orbit_sizes;
1016 if (orbit_sizes[rep] > max_orbit_size) {
1017 distinguished_var = var;
1018 max_orbit_size = orbit_sizes[rep];
1023 LogOrbitInformation(orbits, context->
logger());
1026 if (max_orbit_size > 2) {
1027 OrbitAndPropagation(orbits, distinguished_var, &can_be_fixed_to_false,
1030 const int first_heuristic_size = can_be_fixed_to_false.size();
1042 std::vector<int> var_can_be_true_per_orbit(num_vars, -1);
1044 std::vector<int> tmp_to_clear;
1045 std::vector<int> tmp_sizes(num_vars, 0);
1046 for (
const google::protobuf::RepeatedField<int32_t>* literals :
1048 tmp_to_clear.clear();
1051 int num_fixable = 0;
1052 for (
const int literal : *literals) {
1054 if (context->
IsFixed(literal))
continue;
1057 const int rep = orbits[var];
1058 if (rep == -1)
continue;
1061 if (tmp_sizes[rep] == 0) tmp_to_clear.push_back(rep);
1062 if (tmp_sizes[rep] > 0) ++num_fixable;
1067 if (num_fixable > can_be_fixed_to_false.size()) {
1068 distinguished_var = -1;
1069 can_be_fixed_to_false.clear();
1070 for (
const int literal : *literals) {
1072 if (context->
IsFixed(literal))
continue;
1075 const int rep = orbits[var];
1076 if (rep == -1)
continue;
1077 if (distinguished_var == -1 ||
1078 orbit_sizes[rep] > orbit_sizes[orbits[distinguished_var]]) {
1079 distinguished_var = var;
1083 if (tmp_sizes[rep] == 0) {
1084 can_be_fixed_to_false.push_back(var);
1086 var_can_be_true_per_orbit[rep] = var;
1092 for (
const int rep : tmp_to_clear) tmp_sizes[rep] = 0;
1096 if (can_be_fixed_to_false.size() > first_heuristic_size) {
1099 "[Symmetry] Num fixable by intersecting at_most_one with orbits: ",
1100 can_be_fixed_to_false.size(),
" largest_orbit: ", max_orbit_size);
1120 if (!orbitope.empty()) {
1122 orbitope.size(),
" x ", orbitope[0].size());
1140 if (!orbitope.empty() && context->
working_model->has_objective()) {
1141 const int num_objective_terms = context->
ObjectiveMap().size();
1142 if (orbitope[0].size() == num_objective_terms) {
1143 int num_in_column = 0;
1144 for (
const std::vector<int>& row : orbitope) {
1145 if (context->
ObjectiveMap().contains(row[0])) ++num_in_column;
1147 if (num_in_column == 1) {
1150 CHECK_EQ(num_objective_terms, obj.vars().size());
1151 for (
int i = 1;
i < num_objective_terms; ++
i) {
1153 context->
working_model->add_constraints()->mutable_linear();
1154 new_ct->add_vars(obj.vars(
i - 1));
1155 new_ct->add_vars(obj.vars(
i));
1156 new_ct->add_coeffs(1);
1157 new_ct->add_coeffs(-1);
1158 new_ct->add_domain(0);
1159 new_ct->add_domain(std::numeric_limits<int64_t>::max());
1175 int max_num_fixed_in_orbitope = 0;
1176 if (!orbitope.empty()) {
1177 int size_left = orbitope[0].size();
1178 for (
int col = 0; size_left > 1 && col < orbitope.size(); ++col) {
1179 max_num_fixed_in_orbitope += size_left - 1;
1196 const int num_fixable =
1197 std::max<int>(max_num_fixed_in_orbitope, can_be_fixed_to_false.size());
1198 if ( (
false) && !can_be_fixed_to_false.empty() &&
1199 100 * num_fixable < sum_of_orbit_sizes) {
1201 "[Symmetry] Not fixing anything as gain seems too small.");
1206 if (max_num_fixed_in_orbitope < can_be_fixed_to_false.size()) {
1207 const int orbit_index = orbits[distinguished_var];
1208 int num_in_orbit = 0;
1209 for (
int i = 0;
i < can_be_fixed_to_false.size(); ++
i) {
1210 const int var = can_be_fixed_to_false[
i];
1211 if (orbits[var] == orbit_index) ++num_in_orbit;
1212 context->
UpdateRuleStats(
"symmetry: fixed to false in general orbit");
1213 if (var_can_be_true_per_orbit[orbits[var]] != -1) {
1223 var_can_be_true_per_orbit[orbits[var]],
true, generators);
1230 if (orbit_sizes[orbit_index] > num_in_orbit + 1) {
1232 "symmetry: added orbit symmetry breaking implications");
1234 auto* bool_and = ct->mutable_bool_and();
1235 ct->add_enforcement_literal(
NegatedRef(distinguished_var));
1236 for (
int var = 0; var < num_vars; ++var) {
1237 if (orbits[var] != orbit_index)
continue;
1238 if (var == distinguished_var)
continue;
1239 if (context->
IsFixed(var))
continue;
1246 if (orbitope.empty())
return true;
1249 std::vector<int> tmp_to_clear;
1250 std::vector<int> tmp_sizes(num_vars, 0);
1251 std::vector<int> tmp_num_positive(num_vars, 0);
1256 for (
const google::protobuf::RepeatedField<int32_t>* literals :
1258 for (
const int lit : *literals) {
1260 CHECK_NE(tmp_sizes[var], 1);
1263 for (
const int lit : *literals) {
1268 if (!orbitope.empty() && orbitope[0].size() > 1) {
1269 const int num_cols = orbitope[0].size();
1270 const std::vector<int> orbitope_orbits =
1281 const int num_rows = orbitope.size();
1282 std::vector<bool> row_is_all_equivalent(num_rows,
false);
1283 std::vector<bool> row_has_at_most_one_true(num_rows,
false);
1284 std::vector<bool> row_has_at_most_one_false(num_rows,
false);
1312 for (
const google::protobuf::RepeatedField<int32_t>* literals :
1314 tmp_to_clear.clear();
1315 for (
const int literal : *literals) {
1316 if (context->
IsFixed(literal))
continue;
1318 const int row = orbitope_orbits[var];
1319 if (row == -1)
continue;
1321 if (tmp_sizes[row] == 0) tmp_to_clear.push_back(row);
1333 bool possible_extension =
false;
1339 for (
const int row : tmp_to_clear) {
1340 const int size = tmp_sizes[row];
1341 const int num_positive = tmp_num_positive[row];
1342 const int num_negative = tmp_sizes[row] - tmp_num_positive[row];
1344 tmp_num_positive[row] = 0;
1346 if (num_positive > 0 && num_negative > 0) {
1347 row_is_all_equivalent[row] =
true;
1349 if (num_positive > 1 && num_negative == 0) {
1350 if (size < num_cols) possible_extension =
true;
1351 row_has_at_most_one_true[row] =
true;
1352 }
else if (num_positive == 0 && num_negative > 1) {
1353 if (size < num_cols) possible_extension =
true;
1354 row_has_at_most_one_false[row] =
true;
1358 if (possible_extension) {
1360 "TODO symmetry: possible at most one extension.");
1366 std::vector<std::pair<int, int64_t>> rows_by_score;
1373 for (
int i = 0;
i < num_rows; ++
i) {
1374 if (row_has_at_most_one_true[
i] && row_has_at_most_one_false[
i]) {
1381 if (num_cols == 2 && !row_is_all_equivalent[
i]) {
1396 if (row_is_all_equivalent[
i]) {
1404 if (row_has_at_most_one_false[
i]) {
1406 for (
int j = 0; j < num_cols; ++j) {
1409 }
else if (row_has_at_most_one_true[
i]) {
1411 for (
int j = 0; j < num_cols; ++j) {
1416 for (
int j = 1; j < num_cols; ++j) {
1429 const int64_t score =
1431 if (row_has_at_most_one_true[
i]) {
1432 rows_by_score.push_back({
i, score});
1433 }
else if (row_has_at_most_one_false[
i]) {
1434 rows_by_score.push_back({
i, score});
1447 absl::c_stable_sort(rows_by_score, [](
const std::pair<int, int64_t>& p1,
1448 const std::pair<int, int64_t>& p2) {
1449 return p1.second > p2.second;
1451 int num_processed_rows = 0;
1452 for (
const auto [row, score] : rows_by_score) {
1453 if (num_processed_rows + 1 >= num_cols)
break;
1454 ++num_processed_rows;
1455 if (row_has_at_most_one_true[row]) {
1457 "symmetry: fixed all but one to false in orbitope row");
1458 for (
int j = num_processed_rows; j < num_cols; ++j) {
1462 CHECK(row_has_at_most_one_false[row]);
1464 "symmetry: fixed all but one to true in orbitope row");
1465 for (
int j = num_processed_rows; j < num_cols; ++j) {
1475 if (num_processed_rows > 0) {
1478 for (
int i = num_processed_rows;
i < orbitope.size(); ++
i) {
1479 orbitope[new_size++] = std::move(orbitope[
i]);
1481 CHECK_LT(new_size, orbitope.size());
1482 orbitope.resize(new_size);
1485 for (
int i = 0;
i < orbitope.size(); ++
i) {
1486 CHECK_LT(num_processed_rows, orbitope[
i].size());
1487 orbitope[
i].erase(orbitope[
i].begin(),
1488 orbitope[
i].begin() + num_processed_rows);
1496 if (params.symmetry_level() <= 3)
return true;
1500 if (orbitope.size() == 1) {
1501 const int num_cols = orbitope[0].size();
1502 for (
int i = 0;
i + 1 < num_cols; ++
i) {
1508 "symmetry: added symmetry breaking implication");
1512 ConstraintProto* ct = context->
working_model->add_constraints();
1513 ct->mutable_linear()->add_coeffs(1);
1514 ct->mutable_linear()->add_vars(orbitope[0][
i]);
1515 ct->mutable_linear()->add_coeffs(-1);
1516 ct->mutable_linear()->add_vars(orbitope[0][
i + 1]);
1517 ct->mutable_linear()->add_domain(0);
1518 ct->mutable_linear()->add_domain(std::numeric_limits<int64_t>::max());
1519 context->
UpdateRuleStats(
"symmetry: added symmetry breaking inequality");
1522 }
else if (orbitope.size() > 1) {
1523 std::vector<int64_t> max_values(orbitope.size());
1524 for (
int i = 0;
i < orbitope.size(); ++
i) {
1525 const int var = orbitope[
i][0];
1526 const int64_t max = std::max(std::abs(context->
MaxOf(var)),
1527 std::abs(context->
MinOf(var)));
1528 max_values[
i] = max;
1530 constexpr int kMaxBits = 60;
1531 bool is_approximated;
1532 const std::vector<int64_t> coeffs = BuildInequalityCoeffsForOrbitope(
1533 max_values, (int64_t{1} << kMaxBits), &is_approximated);
1534 for (
int i = 0;
i + 1 < orbitope[0].size(); ++
i) {
1535 ConstraintProto* ct = context->
working_model->add_constraints();
1536 auto* arg = ct->mutable_linear();
1537 for (
int j = 0; j < orbitope.size(); ++j) {
1538 const int64_t coeff = coeffs[j];
1539 arg->add_vars(orbitope[j][
i + 1]);
1540 arg->add_coeffs(coeff);
1541 arg->add_vars(orbitope[j][
i]);
1542 arg->add_coeffs(-coeff);
1543 DCHECK_EQ(context->
MaxOf(orbitope[j][
i + 1]),
1544 context->
MaxOf(orbitope[j][
i]));
1545 DCHECK_EQ(context->
MinOf(orbitope[j][
i + 1]),
1546 context->
MinOf(orbitope[j][
i]));
1549 arg->add_domain(std::numeric_limits<int64_t>::max());
1554 absl::StrCat(
"symmetry: added linear ",
1555 is_approximated ?
"approximated " :
"",
1556 "inequality ordering orbitope columns"),
1557 orbitope[0].size());
1567std::vector<absl::Span<int>> GetCyclesAsSpan(
1568 SparsePermutationProto& permutation) {
1569 std::vector<absl::Span<int>> result;
1571 const int num_cycles = permutation.cycle_sizes().size();
1572 for (
int i = 0;
i < num_cycles; ++
i) {
1573 const int size = permutation.cycle_sizes(
i);
1575 absl::MakeSpan(&permutation.mutable_support()->at(start), size));
1585 std::vector<absl::Span<int>> cycles;
1586 int num_problematic_generators = 0;
1587 for (SparsePermutationProto& generator : *symmetry->mutable_permutations()) {
1599 cycles = GetCyclesAsSpan(generator);
1600 bool problematic =
false;
1602 int new_num_cycles = 0;
1603 const int old_num_cycles = cycles.size();
1604 for (
int i = 0;
i < old_num_cycles; ++
i) {
1605 if (cycles[
i].empty())
continue;
1606 const int reference_var = cycles[
i][0];
1607 const Domain reference_domain = context->
DomainOf(reference_var);
1611 int num_affine_relations = 0;
1612 int num_with_same_representative = 0;
1616 for (
const int var : cycles[
i]) {
1618 if (context->
DomainOf(var) != reference_domain) {
1620 "TODO symmetry: different domain in symmetric variables");
1633 if (affine_relation == reference_relation) {
1634 ++num_with_same_representative;
1636 if (affine_relation.representative != var) {
1637 ++num_affine_relations;
1646 if (problematic)
break;
1648 if (num_fixed > 0) {
1649 if (num_fixed != cycles[
i].size()) {
1651 "TODO symmetry: not all variables fixed in cycle");
1658 if (num_affine_relations > 0) {
1659 if (num_with_same_representative != cycles[
i].size()) {
1661 "TODO symmetry: not all variables have same representative");
1670 if (num_unused > 0) {
1671 if (num_unused != cycles[
i].size()) {
1673 "TODO symmetry: not all variables unused in cycle");
1681 cycles[new_num_cycles++] = cycles[
i];
1685 ++num_problematic_generators;
1686 generator.clear_support();
1687 generator.clear_cycle_sizes();
1691 if (new_num_cycles < old_num_cycles) {
1692 cycles.resize(new_num_cycles);
1693 generator.clear_cycle_sizes();
1694 int new_support_size = 0;
1695 for (
const absl::Span<int> cycle : cycles) {
1696 for (
const int var : cycle) {
1697 generator.set_support(new_support_size++, var);
1699 generator.add_cycle_sizes(cycle.size());
1701 generator.mutable_support()->Truncate(new_support_size);
1705 if (num_problematic_generators > 0) {
1707 " generators where problematic !! Fix.");
1712 const int old_size = symmetry->permutations().size();
1713 for (
int i = 0;
i < old_size; ++
i) {
1714 if (symmetry->permutations(
i).support().empty())
continue;
1715 if (new_size !=
i) {
1716 symmetry->mutable_permutations()->SwapElements(new_size,
i);
1720 if (new_size != old_size) {
1721 symmetry->mutable_permutations()->DeleteSubrange(new_size,
1722 old_size - new_size);
1728 const int num_vars = context->
working_model->variables().size();
1729 std::vector<std::unique_ptr<SparsePermutation>> generators;
1730 for (
const SparsePermutationProto& perm : symmetry->permutations()) {
1734 "[Symmetry] final processing #generators:", generators.size());
1735 const std::vector<int> orbits =
GetOrbits(num_vars, generators);
1736 LogOrbitInformation(orbits, context->
logger());
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)
::util::StaticGraph Graph
bool LoggingIsEnabled() const
Returns true iff logging is enabled.
void RemoveCycles(absl::Span< const int > cycle_indices)
const std::vector< int > & Support() const
Iterator Cycle(int i) const
static std::unique_ptr< TimeLimit > FromDeterministicTime(double deterministic_limit)
Domain DomainOf(int ref) const
const SatParameters & params() const
CpModelProto * working_model
ABSL_MUST_USE_RESULT bool SetLiteralToTrue(int lit)
void AddImplication(int a, int b)
a => b.
ABSL_MUST_USE_RESULT bool SetLiteralToFalse(int lit)
Returns false if the 'lit' doesn't have the desired value in the domain.
ABSL_MUST_USE_RESULT bool NotifyThatModelIsUnsat(absl::string_view message="")
void WriteObjectiveToProto() const
bool IsFixed(int ref) const
bool StoreBooleanEqualityRelation(int ref_a, int ref_b)
bool VariableIsNotUsedAnymore(int ref) const
Returns true if this ref no longer appears in the model.
bool VariableWasRemoved(int ref) const
void UpdateRuleStats(const std::string &name, int num_times=1)
AffineRelation::Relation GetAffineRelation(int ref) const
Returns the representative of ref under the affine relations.
const absl::flat_hash_set< int > & VarToConstraints(int var) const
SolverLogger * logger() const
void WriteVariableDomainsToProto() const
void UpdateNewConstraintsVariableUsage()
Calls UpdateConstraintVariableUsage() on all newly created constraints.
bool CanBeUsedAsLiteral(int ref) const
const absl::flat_hash_map< int, int64_t > & ObjectiveMap() const
int64_t MinOf(int ref) const
int64_t MaxOf(int ref) const
bool ModelIsUnsat() const
SolutionCrush & solution_crush()
void MaybeUpdateVarWithSymmetriesToValue(int var, bool value, absl::Span< const std::unique_ptr< SparsePermutation > > generators)
std::vector< int > GetOrbitopeOrbits(int n, absl::Span< const std::vector< int > > orbitope)
bool LoadModelForProbing(PresolveContext *context, Model *local_model)
bool RefIsPositive(int ref)
bool DetectAndExploitSymmetriesInPresolve(PresolveContext *context)
bool FilterOrbitOnUnusedOrFixedVariables(SymmetryProto *symmetry, PresolveContext *context)
std::string FormatCounter(int64_t num)
Prints a positive number with separators for easier reading (ex: 1'348'065).
bool PossibleIntegerOverflow(const CpModelProto &model, absl::Span< const int > vars, absl::Span< const int64_t > coeffs, int64_t offset)
void DetectAndAddSymmetryToProto(const SatParameters ¶ms, CpModelProto *proto, SolverLogger *logger)
Detects symmetries and fill the symmetry field.
std::vector< int > GetOrbits(int n, absl::Span< const std::unique_ptr< SparsePermutation > > generators)
absl::string_view ConstraintCaseName(ConstraintProto::ConstraintCase constraint_case)
std::unique_ptr< SparsePermutation > CreateSparsePermutationFromProto(int n, const SparsePermutationProto &proto)
Creates a SparsePermutation on [0, n) from its proto representation.
int NegatedRef(int ref)
Small utility functions to deal with negative variable/literal references.
void FindCpModelSymmetries(const SatParameters ¶ms, const CpModelProto &problem, std::vector< std::unique_ptr< SparsePermutation > > *generators, double deterministic_limit, SolverLogger *logger)
std::vector< std::vector< int > > BasicOrbitopeExtraction(absl::Span< const std::unique_ptr< SparsePermutation > > generators)
Graph * GenerateGraphForSymmetryDetection(const LinearBooleanProblem &problem, std::vector< int > *initial_equivalence_classes)
In SWIG mode, we don't want anything besides these top-level includes.
int64_t CapAdd(int64_t x, int64_t y)
int64_t CapProd(int64_t x, int64_t y)
Point BinarySearch(Point x_true, Point x_false, std::function< bool(Point)> f)
util::ReverseArcStaticGraph Graph
Type of graph to use.
uint64_t Hash(uint64_t num, uint64_t c)
std::vector< int >::const_iterator begin() const
#define SOLVER_LOG(logger,...)