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"
65 std::size_t operator()(absl::Span<const int64_t> values)
const {
67 for (
const int64_t
value : values) {
74struct NodeExprCompare {
75 bool operator()(
const LinearExpressionProto&
a,
76 const LinearExpressionProto&
b)
const {
77 if (
a.offset() !=
b.offset())
return a.offset() <
b.offset();
78 if (
a.vars_size() !=
b.vars_size())
return a.vars_size() <
b.vars_size();
79 for (
int i = 0;
i <
a.vars_size(); ++
i) {
80 if (
a.vars(
i) !=
b.vars(
i))
return a.vars(
i) <
b.vars(
i);
81 if (
a.coeffs(
i) !=
b.coeffs(
i))
return a.coeffs(
i) <
b.coeffs(
i);
91 IdGenerator() =
default;
95 int GetId(
const std::vector<int64_t>& color) {
97 return id_map_.insert({color, id_map_.size()}).first->second;
100 int NextFreeId()
const {
return id_map_.size(); }
103 absl::flat_hash_map<std::vector<int64_t>, int, VectorHash> id_map_;
109template <
typename FieldInt64Type>
111 const google::protobuf::RepeatedField<FieldInt64Type>& repeated_field,
112 std::vector<int64_t>* vector) {
113 CHECK(vector !=
nullptr);
114 for (
const FieldInt64Type
value : repeated_field) {
115 vector->push_back(
value);
119bool IsIntervalFixedSize(
const IntervalConstraintProto&
interval) {
120 if (!
interval.size().vars().empty()) {
155template <
typename Graph>
157 const CpModelProto& problem, std::vector<int>* initial_equivalence_classes,
158 SolverLogger* logger) {
159 CHECK(initial_equivalence_classes !=
nullptr);
161 const int num_variables = problem.variables_size();
162 auto graph = std::make_unique<Graph>();
172 VAR_COEFFICIENT_NODE,
176 IdGenerator color_id_generator;
177 initial_equivalence_classes->clear();
178 auto new_node_from_id = [&initial_equivalence_classes, &
graph](
int color_id) {
181 const int node = initial_equivalence_classes->size();
182 initial_equivalence_classes->push_back(color_id);
186 graph->AddNode(node);
189 auto new_node = [&new_node_from_id,
190 &color_id_generator](
const std::vector<int64_t>& color) {
191 return new_node_from_id(color_id_generator.GetId(color));
199 std::vector<int64_t> objective_by_var(num_variables, 0);
200 for (
int i = 0;
i < problem.objective().vars_size(); ++
i) {
201 const int ref = problem.objective().vars(
i);
203 const int64_t coeff = problem.objective().coeffs(
i);
209 std::vector<int64_t> tmp_color;
210 for (
int v = 0; v < num_variables; ++v) {
211 tmp_color = {VARIABLE_NODE, objective_by_var[v]};
212 Append(problem.variables(v).domain(), &tmp_color);
213 CHECK_EQ(v, new_node(tmp_color));
216 const int color_id_for_coeff_one =
217 color_id_generator.GetId({VAR_COEFFICIENT_NODE, 1});
218 const int color_id_for_coeff_minus_one =
219 color_id_generator.GetId({VAR_COEFFICIENT_NODE, -1});
223 absl::flat_hash_map<std::pair<int64_t, int64_t>,
int> coefficient_nodes;
224 auto get_coefficient_node =
225 [&new_node_from_id, &
graph, &coefficient_nodes, &color_id_generator,
226 &tmp_color, color_id_for_coeff_minus_one](
int var, int64_t coeff) {
227 const int var_node =
var;
233 if (coeff == 1)
return var_node;
236 coefficient_nodes.insert({std::make_pair(
var, coeff), 0});
237 if (!insert.second)
return insert.first->second;
243 color_id = color_id_for_coeff_minus_one;
245 tmp_color = {VAR_COEFFICIENT_NODE, coeff};
246 color_id = color_id_generator.GetId(tmp_color);
248 const int secondary_node = new_node_from_id(color_id);
249 graph->AddArc(var_node, secondary_node);
250 insert.first->second = secondary_node;
251 return secondary_node;
257 auto get_literal_node = [&get_coefficient_node](
int ref) {
273 absl::flat_hash_set<std::pair<int, int>> implications;
274 auto get_implication_node = [&new_node_from_id, &
graph, &coefficient_nodes,
275 color_id_for_coeff_one,
276 color_id_for_coeff_minus_one](
int ref) {
280 coefficient_nodes.insert({std::make_pair(
var, coeff), 0});
281 if (!insert.second)
return insert.first->second;
282 const int secondary_node = new_node_from_id(
283 coeff == 1 ? color_id_for_coeff_one : color_id_for_coeff_minus_one);
285 insert.first->second = secondary_node;
286 return secondary_node;
288 auto add_implication = [&get_implication_node, &
graph, &implications](
289 int ref_a,
int ref_b) {
290 const auto insert = implications.insert({ref_a, ref_b});
291 if (!insert.second)
return;
292 graph->AddArc(get_implication_node(ref_a), get_implication_node(ref_b));
300 auto make_linear_expr_node = [&new_node, &
graph, &get_coefficient_node](
301 const LinearExpressionProto& expr,
302 const std::vector<int64_t>& color) {
303 std::vector<int64_t> local_color = color;
304 local_color.push_back(expr.offset());
305 const int local_node = new_node(local_color);
307 for (
int i = 0;
i < expr.vars().
size(); ++
i) {
308 const int ref = expr.vars(
i);
310 const int64_t coeff =
312 graph->AddArc(get_coefficient_node(var_node, coeff), local_node);
317 absl::btree_map<LinearExpressionProto, int, NodeExprCompare> expr_nodes;
318 auto shared_linear_expr_node =
319 [&make_linear_expr_node, &expr_nodes](
const LinearExpressionProto& expr) {
320 const auto [it, inserted] = expr_nodes.insert({expr, 0});
322 const std::vector<int64_t> local_color = {VAR_LIN_EXPR_NODE,
324 it->second = make_linear_expr_node(expr, local_color);
330 absl::flat_hash_map<int, int> interval_constraint_index_to_node;
336 const int constraint_node = initial_equivalence_classes->size();
337 std::vector<int64_t> color = {CONSTRAINT_NODE,
338 constraint.constraint_case()};
340 switch (constraint.constraint_case()) {
341 case ConstraintProto::CONSTRAINT_NOT_SET:
346 case ConstraintProto::kLinear: {
351 Append(constraint.linear().domain(), &color);
352 CHECK_EQ(constraint_node, new_node(color));
353 for (
int i = 0;
i < constraint.linear().vars_size(); ++
i) {
354 const int ref = constraint.linear().vars(
i);
357 ? constraint.linear().coeffs(
i)
358 : -constraint.linear().coeffs(
i);
359 graph->AddArc(get_coefficient_node(variable_node, coeff),
364 case ConstraintProto::kAllDiff: {
365 CHECK_EQ(constraint_node, new_node(color));
366 for (
const LinearExpressionProto& expr :
367 constraint.all_diff().exprs()) {
368 graph->AddArc(shared_linear_expr_node(expr), constraint_node);
372 case ConstraintProto::kBoolOr: {
373 CHECK_EQ(constraint_node, new_node(color));
374 for (
const int ref : constraint.bool_or().literals()) {
375 graph->AddArc(get_literal_node(ref), constraint_node);
379 case ConstraintProto::kAtMostOne: {
380 if (constraint.at_most_one().literals().size() == 2) {
382 add_implication(constraint.at_most_one().literals(0),
383 NegatedRef(constraint.at_most_one().literals(1)));
387 CHECK_EQ(constraint_node, new_node(color));
388 for (
const int ref : constraint.at_most_one().literals()) {
389 graph->AddArc(get_literal_node(ref), constraint_node);
393 case ConstraintProto::kExactlyOne: {
394 CHECK_EQ(constraint_node, new_node(color));
395 for (
const int ref : constraint.exactly_one().literals()) {
396 graph->AddArc(get_literal_node(ref), constraint_node);
400 case ConstraintProto::kBoolXor: {
401 CHECK_EQ(constraint_node, new_node(color));
402 for (
const int ref : constraint.bool_xor().literals()) {
403 graph->AddArc(get_literal_node(ref), constraint_node);
407 case ConstraintProto::kBoolAnd: {
408 if (constraint.enforcement_literal_size() > 1) {
409 CHECK_EQ(constraint_node, new_node(color));
410 for (
const int ref : constraint.bool_and().literals()) {
411 graph->AddArc(get_literal_node(ref), constraint_node);
416 CHECK_EQ(constraint.enforcement_literal_size(), 1);
417 const int ref_a = constraint.enforcement_literal(0);
418 for (
const int ref_b : constraint.bool_and().literals()) {
419 add_implication(ref_a, ref_b);
423 case ConstraintProto::kLinMax: {
424 const LinearExpressionProto& target_expr =
425 constraint.lin_max().target();
427 const int target_node = make_linear_expr_node(target_expr, color);
429 for (
int i = 0;
i < constraint.lin_max().exprs_size(); ++
i) {
430 const LinearExpressionProto& expr = constraint.lin_max().exprs(
i);
431 graph->AddArc(shared_linear_expr_node(expr), target_node);
436 case ConstraintProto::kInterval: {
437 static constexpr int kFixedIntervalColor = 0;
438 static constexpr int kNonFixedIntervalColor = 1;
439 if (IsIntervalFixedSize(constraint.interval())) {
440 std::vector<int64_t> local_color = color;
441 local_color.push_back(kFixedIntervalColor);
442 local_color.push_back(constraint.interval().size().offset());
443 const int full_node =
444 make_linear_expr_node(constraint.interval().start(), local_color);
445 CHECK_EQ(full_node, constraint_node);
450 std::vector<int64_t> local_color = color;
451 local_color.push_back(kNonFixedIntervalColor);
453 local_color.push_back(0);
454 const int start_node =
455 make_linear_expr_node(constraint.interval().start(), local_color);
456 local_color.pop_back();
457 CHECK_EQ(start_node, constraint_node);
461 const int size_node =
462 shared_linear_expr_node(constraint.interval().size());
464 local_color.push_back(1);
466 make_linear_expr_node(constraint.interval().end(), local_color);
467 local_color.pop_back();
471 graph->AddArc(start_node, end_node);
472 graph->AddArc(end_node, size_node);
477 case ConstraintProto::kNoOverlap: {
481 CHECK_EQ(constraint_node, new_node(color));
482 for (
const int interval : constraint.no_overlap().intervals()) {
483 graph->AddArc(interval_constraint_index_to_node.at(
interval),
488 case ConstraintProto::kNoOverlap2D: {
492 CHECK_EQ(constraint_node, new_node(color));
493 std::vector<int64_t> local_color = color;
494 local_color.push_back(0);
495 const int size = constraint.no_overlap_2d().x_intervals().size();
496 const int node_x = new_node(local_color);
497 const int node_y = new_node(local_color);
498 local_color.pop_back();
499 graph->AddArc(constraint_node, node_x);
500 graph->AddArc(constraint_node, node_y);
501 local_color.push_back(1);
502 for (
int i = 0;
i <
size; ++
i) {
503 const int box_node = new_node(local_color);
504 graph->AddArc(box_node, constraint_node);
505 const int x = constraint.no_overlap_2d().x_intervals(
i);
506 const int y = constraint.no_overlap_2d().y_intervals(
i);
507 graph->AddArc(interval_constraint_index_to_node.at(
x), node_x);
508 graph->AddArc(interval_constraint_index_to_node.at(
x), box_node);
509 graph->AddArc(interval_constraint_index_to_node.at(
y), node_y);
510 graph->AddArc(interval_constraint_index_to_node.at(
y), box_node);
514 case ConstraintProto::kCumulative: {
518 const CumulativeConstraintProto&
ct = constraint.cumulative();
519 std::vector<int64_t> capacity_color = color;
520 capacity_color.push_back(0);
521 CHECK_EQ(constraint_node, new_node(capacity_color));
522 graph->AddArc(constraint_node,
523 make_linear_expr_node(
ct.capacity(), capacity_color));
525 std::vector<int64_t> task_color = color;
526 task_color.push_back(1);
527 for (
int i = 0;
i <
ct.intervals().
size(); ++
i) {
528 const int task_node =
529 make_linear_expr_node(
ct.demands(
i), task_color);
530 graph->AddArc(task_node, constraint_node);
531 graph->AddArc(task_node,
532 interval_constraint_index_to_node.at(
ct.intervals(
i)));
536 case ConstraintProto::kCircuit: {
540 const int num_arcs = constraint.circuit().literals().size();
541 absl::flat_hash_map<int, int> circuit_node_to_symmetry_node;
542 std::vector<int64_t> arc_color = color;
543 arc_color.push_back(1);
544 for (
int i = 0;
i < num_arcs; ++
i) {
545 const int literal = constraint.circuit().literals(
i);
546 const int tail = constraint.circuit().tails(
i);
547 const int head = constraint.circuit().heads(
i);
548 const int arc_node = new_node(arc_color);
549 if (!circuit_node_to_symmetry_node.contains(
head)) {
550 circuit_node_to_symmetry_node[
head] = new_node(color);
552 const int head_node = circuit_node_to_symmetry_node[
head];
553 if (!circuit_node_to_symmetry_node.contains(
tail)) {
554 circuit_node_to_symmetry_node[
tail] = new_node(color);
556 const int tail_node = circuit_node_to_symmetry_node[
tail];
559 graph->AddArc(tail_node, arc_node);
561 graph->AddArc(arc_node, head_node);
572 VLOG(1) <<
"Unsupported constraint type "
582 if (constraint.constraint_case() != ConstraintProto::kBoolAnd ||
583 constraint.enforcement_literal().size() > 1) {
584 for (
const int ref : constraint.enforcement_literal()) {
585 graph->AddArc(constraint_node, get_literal_node(ref));
591 DCHECK_EQ(
graph->num_nodes(), initial_equivalence_classes->size());
598 const int num_nodes =
graph->num_nodes();
599 std::vector<int> in_degree(num_nodes, 0);
600 std::vector<int> out_degree(num_nodes, 0);
601 for (
int i = 0;
i < num_nodes; ++
i) {
602 out_degree[
i] =
graph->OutDegree(
i);
607 for (
int i = 0;
i < num_nodes; ++
i) {
608 if (in_degree[
i] >= num_nodes || out_degree[
i] >= num_nodes) {
609 SOLVER_LOG(logger,
"[Symmetry] Too many multi-arcs in symmetry code.");
622 int next_id = color_id_generator.NextFreeId();
623 for (
int i = 0;
i < num_variables; ++
i) {
624 if ((*
graph)[
i].empty()) {
625 (*initial_equivalence_classes)[
i] = next_id++;
631 std::vector<int> mapping(next_id, -1);
632 for (
int& ref : *initial_equivalence_classes) {
633 if (mapping[ref] == -1) {
634 ref = mapping[ref] =
id++;
645 const SatParameters& params,
const CpModelProto& problem,
646 std::vector<std::unique_ptr<SparsePermutation>>* generators,
648 CHECK(generators !=
nullptr);
651 if (params.symmetry_level() < 3 && problem.variables().size() > 1e6 &&
652 problem.constraints().size() > 1e6) {
654 "[Symmetry] Problem too large. Skipping. You can use "
655 "symmetry_level:3 or more to force it.");
661 std::vector<int> equivalence_classes;
663 problem, &equivalence_classes, logger));
664 if (
graph ==
nullptr)
return;
666 SOLVER_LOG(logger,
"[Symmetry] Graph for symmetry has ",
669 if (
graph->num_nodes() == 0)
return;
671 if (params.symmetry_level() < 3 &&
graph->num_nodes() > 1e6 &&
672 graph->num_arcs() > 1e6) {
674 "[Symmetry] Graph too large. Skipping. You can use "
675 "symmetry_level:3 or more to force it.");
680 std::vector<int> factorized_automorphism_group_size;
684 &equivalence_classes, generators, &factorized_automorphism_group_size,
691 "[Symmetry] GraphSymmetryFinder error: ",
status.message());
697 double average_support_size = 0.0;
698 int num_generators = 0;
699 int num_duplicate_constraints = 0;
700 for (
int i = 0;
i < generators->size(); ++
i) {
702 std::vector<int> to_delete;
703 for (
int j = 0; j < permutation->
NumCycles(); ++j) {
707 if (*(permutation->
Cycle(j).
begin()) >= problem.variables_size()) {
708 to_delete.push_back(j);
711 for (
const int node : permutation->
Cycle(j)) {
712 DCHECK_GE(node, problem.variables_size());
719 if (!permutation->
Support().empty()) {
720 average_support_size += permutation->
Support().size();
721 swap((*generators)[num_generators], (*generators)[
i]);
724 ++num_duplicate_constraints;
727 generators->resize(num_generators);
728 SOLVER_LOG(logger,
"[Symmetry] Symmetry computation done. time: ",
730 " dtime: ",
time_limit->GetElapsedDeterministicTime());
731 if (num_generators > 0) {
732 average_support_size /= num_generators;
733 SOLVER_LOG(logger,
"[Symmetry] #generators: ", num_generators,
734 ", average support size: ", average_support_size);
735 if (num_duplicate_constraints > 0) {
736 SOLVER_LOG(logger,
"[Symmetry] The model contains ",
737 num_duplicate_constraints,
" duplicate constraints !");
744 SymmetryProto* symmetry =
proto->mutable_symmetry();
747 std::vector<std::unique_ptr<SparsePermutation>> generators;
750 if (generators.empty()) {
751 proto->clear_symmetry();
755 for (
const std::unique_ptr<SparsePermutation>& perm : generators) {
756 SparsePermutationProto* perm_proto = symmetry->add_permutations();
757 const int num_cycle = perm->NumCycles();
758 for (
int i = 0;
i < num_cycle; ++
i) {
759 const int old_size = perm_proto->support().size();
760 for (
const int var : perm->Cycle(
i)) {
761 perm_proto->add_support(
var);
763 perm_proto->add_cycle_sizes(perm_proto->support().size() - old_size);
768 if (orbitope.empty())
return;
769 SOLVER_LOG(logger,
"[Symmetry] Found orbitope of size ", orbitope.size(),
770 " x ", orbitope[0].size());
771 DenseMatrixProto* matrix = symmetry->add_orbitopes();
772 matrix->set_num_rows(orbitope.size());
773 matrix->set_num_cols(orbitope[0].
size());
774 for (
const std::vector<int>&
row : orbitope) {
775 for (
const int entry :
row) {
776 matrix->add_entries(entry);
797void OrbitAndPropagation(absl::Span<const int> orbits,
int var,
798 std::vector<int>* can_be_fixed_to_false,
803 if (!
context->CanBeUsedAsLiteral(
var))
return;
819 auto* sat_solver =
model.GetOrCreate<SatSolver>();
820 auto* mapping =
model.GetOrCreate<CpModelMapping>();
821 const Literal to_propagate = mapping->Literal(
var);
823 const VariablesAssignment& assignment = sat_solver->Assignment();
824 if (assignment.LiteralIsAssigned(to_propagate))
return;
825 sat_solver->EnqueueDecisionAndBackjumpOnConflict(to_propagate);
826 if (sat_solver->CurrentDecisionLevel() != 1)
return;
829 can_be_fixed_to_false->clear();
831 const int orbit_index = orbits[
var];
832 const int num_variables = orbits.size();
833 for (
int var = 0;
var < num_variables; ++
var) {
834 if (orbits[
var] != orbit_index)
continue;
841 if (assignment.LiteralIsFalse(mapping->Literal(
var))) {
842 can_be_fixed_to_false->push_back(
var);
845 if (!can_be_fixed_to_false->empty()) {
847 "[Symmetry] Num fixable by binary propagation in orbit: ",
848 can_be_fixed_to_false->size(),
" / ", orbit_size);
852std::vector<int64_t> BuildInequalityCoeffsForOrbitope(
853 absl::Span<const int64_t> maximum_values, int64_t max_linear_size,
854 bool* is_approximated) {
855 std::vector<int64_t> out(maximum_values.size());
856 int64_t range_product = 1;
857 uint64_t greatest_coeff = 0;
858 for (
int i = 0;
i < maximum_values.size(); ++
i) {
859 out[
i] = range_product;
861 std::max(greatest_coeff,
static_cast<uint64_t
>(maximum_values[
i]));
862 range_product =
CapProd(range_product, 1 + maximum_values[
i]);
865 if (range_product <= max_linear_size) {
869 *is_approximated =
false;
872 *is_approximated =
true;
874 const auto compute_approximate_coeffs =
875 [max_linear_size, maximum_values](
double scaling_factor,
876 std::vector<int64_t>* coeffs) ->
bool {
877 int64_t max_size = 0;
878 double cumulative_product_double = 1.0;
879 for (
int i = 0;
i < maximum_values.size(); ++
i) {
880 const int64_t
max = maximum_values[
i];
881 const int64_t coeff =
static_cast<int64_t
>(cumulative_product_double);
882 (*coeffs)[
i] = coeff;
883 cumulative_product_double *= scaling_factor *
max + 1;
885 if (max_size > max_linear_size)
return false;
891 0.0, 1.0, [&compute_approximate_coeffs, &out](
double scaling_factor) {
892 return compute_approximate_coeffs(scaling_factor, &out);
894 CHECK(compute_approximate_coeffs(scaling, &out));
901 const SatParameters& params =
context->params();
905 if (
context->working_model->has_objective()) {
906 context->WriteObjectiveToProto();
908 context->WriteVariableDomainsToProto();
912 int64_t num_added = 0;
913 const int initial_ct_index =
proto.constraints().size();
914 const int num_vars =
proto.variables_size();
915 for (
int var = 0;
var < num_vars; ++
var) {
917 if (
context->VariableWasRemoved(
var))
continue;
918 if (
context->VariableIsNotUsedAnymore(
var))
continue;
924 ConstraintProto*
ct =
context->working_model->add_constraints();
925 auto* arg =
ct->mutable_linear();
929 arg->add_coeffs(-r.
coeff);
930 arg->add_domain(r.
offset);
931 arg->add_domain(r.
offset);
934 std::vector<std::unique_ptr<SparsePermutation>> generators;
939 context->working_model->mutable_constraints()->DeleteSubrange(
940 initial_ct_index, num_added);
942 if (generators.empty())
return true;
949 std::vector<const google::protobuf::RepeatedField<int32_t>*> at_most_ones;
950 for (
int i = 0;
i <
proto.constraints_size(); ++
i) {
951 if (
proto.constraints(
i).constraint_case() == ConstraintProto::kAtMostOne) {
952 at_most_ones.push_back(&
proto.constraints(
i).at_most_one().literals());
954 if (
proto.constraints(
i).constraint_case() ==
955 ConstraintProto::kExactlyOne) {
956 at_most_ones.push_back(&
proto.constraints(
i).exactly_one().literals());
964 int distinguished_var = -1;
965 std::vector<int> can_be_fixed_to_false;
968 const std::vector<int> orbits =
GetOrbits(num_vars, generators);
969 std::vector<int> orbit_sizes;
970 int max_orbit_size = 0;
971 for (
int var = 0;
var < num_vars; ++
var) {
972 const int rep = orbits[
var];
973 if (rep == -1)
continue;
974 if (rep >= orbit_sizes.size()) orbit_sizes.resize(rep + 1, 0);
976 if (orbit_sizes[rep] > max_orbit_size) {
977 distinguished_var =
var;
978 max_orbit_size = orbit_sizes[rep];
983 if (
context->logger()->LoggingIsEnabled()) {
984 std::vector<int> sorted_sizes;
985 for (
const int s : orbit_sizes) {
986 if (s != 0) sorted_sizes.push_back(s);
988 std::sort(sorted_sizes.begin(), sorted_sizes.end(), std::greater<int>());
989 const int num_orbits = sorted_sizes.size();
990 if (num_orbits > 10) sorted_sizes.resize(10);
992 " orbits with sizes: ", absl::StrJoin(sorted_sizes,
","),
993 (num_orbits > sorted_sizes.size() ?
",..." :
""));
997 if (max_orbit_size > 2) {
998 OrbitAndPropagation(orbits, distinguished_var, &can_be_fixed_to_false,
1001 const int first_heuristic_size = can_be_fixed_to_false.size();
1014 std::vector<int> tmp_to_clear;
1015 std::vector<int> tmp_sizes(num_vars, 0);
1016 for (
const google::protobuf::RepeatedField<int32_t>* literals :
1018 tmp_to_clear.clear();
1021 int num_fixable = 0;
1022 for (
const int literal : *literals) {
1027 const int rep = orbits[
var];
1028 if (rep == -1)
continue;
1031 if (tmp_sizes[rep] == 0) tmp_to_clear.push_back(rep);
1032 if (tmp_sizes[rep] > 0) ++num_fixable;
1037 if (num_fixable > can_be_fixed_to_false.size()) {
1038 distinguished_var = -1;
1039 can_be_fixed_to_false.clear();
1040 for (
const int literal : *literals) {
1045 const int rep = orbits[
var];
1046 if (rep == -1)
continue;
1047 if (distinguished_var == -1 ||
1048 orbit_sizes[rep] > orbit_sizes[orbits[distinguished_var]]) {
1049 distinguished_var =
var;
1053 if (tmp_sizes[rep] == 0) can_be_fixed_to_false.push_back(
var);
1058 for (
const int rep : tmp_to_clear) tmp_sizes[rep] = 0;
1062 if (can_be_fixed_to_false.size() > first_heuristic_size) {
1065 "[Symmetry] Num fixable by intersecting at_most_one with orbits: ",
1066 can_be_fixed_to_false.size(),
" largest_orbit: ", max_orbit_size);
1086 if (!orbitope.empty()) {
1088 orbitope.size(),
" x ", orbitope[0].size());
1106 if (!orbitope.empty() &&
context->working_model->has_objective()) {
1107 const int num_objective_terms =
context->ObjectiveMap().size();
1108 if (orbitope[0].
size() == num_objective_terms) {
1109 int num_in_column = 0;
1110 for (
const std::vector<int>&
row : orbitope) {
1111 if (
context->ObjectiveMap().contains(
row[0])) ++num_in_column;
1113 if (num_in_column == 1) {
1114 context->WriteObjectiveToProto();
1115 const auto& obj =
context->working_model->objective();
1116 CHECK_EQ(num_objective_terms, obj.vars().size());
1117 for (
int i = 1;
i < num_objective_terms; ++
i) {
1119 context->working_model->add_constraints()->mutable_linear();
1120 new_ct->add_vars(obj.vars(
i - 1));
1121 new_ct->add_vars(obj.vars(
i));
1122 new_ct->add_coeffs(1);
1123 new_ct->add_coeffs(-1);
1124 new_ct->add_domain(0);
1125 new_ct->add_domain(std::numeric_limits<int64_t>::max());
1127 context->UpdateNewConstraintsVariableUsage();
1128 context->UpdateRuleStats(
"symmetry: objective is one orbitope row.");
1141 int max_num_fixed_in_orbitope = 0;
1142 if (!orbitope.empty()) {
1143 int size_left = orbitope[0].size();
1144 for (
int col = 0; size_left > 1 &&
col < orbitope.size(); ++
col) {
1145 max_num_fixed_in_orbitope += size_left - 1;
1149 if (max_num_fixed_in_orbitope < can_be_fixed_to_false.size()) {
1150 const int orbit_index = orbits[distinguished_var];
1151 int num_in_orbit = 0;
1152 for (
int i = 0;
i < can_be_fixed_to_false.size(); ++
i) {
1153 const int var = can_be_fixed_to_false[
i];
1154 if (orbits[
var] == orbit_index) ++num_in_orbit;
1155 context->UpdateRuleStats(
"symmetry: fixed to false in general orbit");
1156 if (!
context->SetLiteralToFalse(
var))
return false;
1161 if (orbit_sizes[orbit_index] > num_in_orbit + 1) {
1163 "symmetry: added orbit symmetry breaking implications");
1164 auto*
ct =
context->working_model->add_constraints();
1165 auto* bool_and =
ct->mutable_bool_and();
1166 ct->add_enforcement_literal(
NegatedRef(distinguished_var));
1167 for (
int var = 0;
var < num_vars; ++
var) {
1168 if (orbits[
var] != orbit_index)
continue;
1169 if (
var == distinguished_var)
continue;
1173 context->UpdateNewConstraintsVariableUsage();
1177 if (orbitope.empty())
return true;
1180 std::vector<int> tmp_to_clear;
1181 std::vector<int> tmp_sizes(num_vars, 0);
1182 std::vector<int> tmp_num_positive(num_vars, 0);
1187 for (
const google::protobuf::RepeatedField<int32_t>* literals :
1189 for (
const int lit : *literals) {
1191 CHECK_NE(tmp_sizes[
var], 1);
1194 for (
const int lit : *literals) {
1199 if (!orbitope.empty() && orbitope[0].size() > 1) {
1200 const int num_cols = orbitope[0].size();
1201 const std::vector<int> orbitope_orbits =
1212 const int num_rows = orbitope.size();
1213 std::vector<bool> row_is_all_equivalent(num_rows,
false);
1214 std::vector<bool> row_has_at_most_one_true(num_rows,
false);
1215 std::vector<bool> row_has_at_most_one_false(num_rows,
false);
1243 for (
const google::protobuf::RepeatedField<int32_t>* literals :
1245 tmp_to_clear.clear();
1246 for (
const int literal : *literals) {
1249 const int row = orbitope_orbits[
var];
1250 if (
row == -1)
continue;
1252 if (tmp_sizes[
row] == 0) tmp_to_clear.push_back(
row);
1264 bool possible_extension =
false;
1270 for (
const int row : tmp_to_clear) {
1271 const int size = tmp_sizes[
row];
1272 const int num_positive = tmp_num_positive[
row];
1273 const int num_negative = tmp_sizes[
row] - tmp_num_positive[
row];
1275 tmp_num_positive[
row] = 0;
1277 if (num_positive > 0 && num_negative > 0) {
1278 row_is_all_equivalent[
row] =
true;
1280 if (num_positive > 1 && num_negative == 0) {
1281 if (
size < num_cols) possible_extension =
true;
1282 row_has_at_most_one_true[
row] =
true;
1283 }
else if (num_positive == 0 && num_negative > 1) {
1284 if (
size < num_cols) possible_extension =
true;
1285 row_has_at_most_one_false[
row] =
true;
1289 if (possible_extension) {
1291 "TODO symmetry: possible at most one extension.");
1297 std::vector<std::pair<int, int64_t>> rows_by_score;
1304 for (
int i = 0;
i < num_rows; ++
i) {
1305 if (row_has_at_most_one_true[
i] && row_has_at_most_one_false[
i]) {
1312 if (num_cols == 2 && !row_is_all_equivalent[
i]) {
1314 context->UpdateRuleStats(
"symmetry: equivalence in orbitope row");
1315 context->StoreBooleanEqualityRelation(orbitope[
i][0],
1317 if (
context->ModelIsUnsat())
return false;
1320 return context->NotifyThatModelIsUnsat(
"orbitope and at most one");
1325 if (row_is_all_equivalent[
i]) {
1333 if (row_has_at_most_one_false[
i]) {
1334 context->UpdateRuleStats(
"symmetry: all true in orbitope row");
1335 for (
int j = 0; j < num_cols; ++j) {
1336 if (!
context->SetLiteralToTrue(orbitope[
i][j]))
return false;
1338 }
else if (row_has_at_most_one_true[
i]) {
1339 context->UpdateRuleStats(
"symmetry: all false in orbitope row");
1340 for (
int j = 0; j < num_cols; ++j) {
1341 if (!
context->SetLiteralToFalse(orbitope[
i][j]))
return false;
1344 context->UpdateRuleStats(
"symmetry: all equivalent in orbitope row");
1345 for (
int j = 1; j < num_cols; ++j) {
1346 context->StoreBooleanEqualityRelation(orbitope[
i][0],
1348 if (
context->ModelIsUnsat())
return false;
1356 const int64_t score =
1358 if (row_has_at_most_one_true[
i]) {
1359 rows_by_score.push_back({
i, score});
1360 }
else if (row_has_at_most_one_false[
i]) {
1361 rows_by_score.push_back({
i, score});
1374 absl::c_stable_sort(rows_by_score, [](
const std::pair<int, int64_t>& p1,
1375 const std::pair<int, int64_t>& p2) {
1376 return p1.second > p2.second;
1378 int num_processed_rows = 0;
1379 for (
const auto [
row, score] : rows_by_score) {
1380 if (num_processed_rows + 1 >= num_cols)
break;
1381 ++num_processed_rows;
1382 if (row_has_at_most_one_true[
row]) {
1384 "symmetry: fixed all but one to false in orbitope row");
1385 for (
int j = num_processed_rows; j < num_cols; ++j) {
1386 if (!
context->SetLiteralToFalse(orbitope[
row][j]))
return false;
1389 CHECK(row_has_at_most_one_false[
row]);
1391 "symmetry: fixed all but one to true in orbitope row");
1392 for (
int j = num_processed_rows; j < num_cols; ++j) {
1393 if (!
context->SetLiteralToTrue(orbitope[
row][j]))
return false;
1402 if (num_processed_rows > 0) {
1405 for (
int i = num_processed_rows;
i < orbitope.size(); ++
i) {
1406 orbitope[new_size++] = std::move(orbitope[
i]);
1408 CHECK_LT(new_size, orbitope.size());
1409 orbitope.resize(new_size);
1412 for (
int i = 0;
i < orbitope.size(); ++
i) {
1413 CHECK_LT(num_processed_rows, orbitope[
i].
size());
1414 orbitope[
i].erase(orbitope[
i].begin(),
1415 orbitope[
i].begin() + num_processed_rows);
1422 if (orbitope.size() == 1) {
1423 const int num_cols = orbitope[0].size();
1424 for (
int i = 0;
i + 1 < num_cols; ++
i) {
1426 if (
context->CanBeUsedAsLiteral(orbitope[0][
i]) &&
1427 context->CanBeUsedAsLiteral(orbitope[0][
i + 1])) {
1428 context->AddImplication(orbitope[0][
i + 1], orbitope[0][
i]);
1430 "symmetry: added symmetry breaking implication");
1434 ConstraintProto*
ct =
context->working_model->add_constraints();
1435 ct->mutable_linear()->add_coeffs(1);
1436 ct->mutable_linear()->add_vars(orbitope[0][
i]);
1437 ct->mutable_linear()->add_coeffs(-1);
1438 ct->mutable_linear()->add_vars(orbitope[0][
i + 1]);
1439 ct->mutable_linear()->add_domain(0);
1440 ct->mutable_linear()->add_domain(std::numeric_limits<int64_t>::max());
1441 context->UpdateRuleStats(
"symmetry: added symmetry breaking inequality");
1443 context->UpdateNewConstraintsVariableUsage();
1444 }
else if (orbitope.size() > 1 && params.symmetry_level() > 3) {
1445 std::vector<int64_t> max_values(orbitope.size());
1446 for (
int i = 0;
i < orbitope.size(); ++
i) {
1447 const int var = orbitope[
i][0];
1450 max_values[
i] =
max;
1452 constexpr int kMaxBits = 60;
1453 bool is_approximated;
1454 const std::vector<int64_t> coeffs = BuildInequalityCoeffsForOrbitope(
1455 max_values, (int64_t{1} << kMaxBits), &is_approximated);
1456 for (
int i = 0;
i + 1 < orbitope[0].size(); ++
i) {
1457 ConstraintProto*
ct =
context->working_model->add_constraints();
1458 auto* arg =
ct->mutable_linear();
1459 for (
int j = 0; j < orbitope.size(); ++j) {
1460 const int64_t coeff = coeffs[j];
1461 arg->add_vars(orbitope[j][
i + 1]);
1462 arg->add_coeffs(coeff);
1463 arg->add_vars(orbitope[j][
i]);
1464 arg->add_coeffs(-coeff);
1465 DCHECK_EQ(
context->MaxOf(orbitope[j][
i + 1]),
1467 DCHECK_EQ(
context->MinOf(orbitope[j][
i + 1]),
1471 arg->add_domain(std::numeric_limits<int64_t>::max());
1476 absl::StrCat(
"symmetry: added linear ",
1477 is_approximated ?
"approximated " :
"",
1478 "inequality ordering orbitope columns"),
1479 orbitope[0].
size());
1480 context->UpdateNewConstraintsVariableUsage();
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(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)
CpModelProto proto
The output proto.
GurobiMPCallbackContext * context
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)
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)
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,...)