OR-Tools  9.1
boolean_problem.cc
Go to the documentation of this file.
1// Copyright 2010-2021 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <algorithm>
17#include <cstdint>
18#include <cstdlib>
19#include <limits>
20#include <numeric>
21#include <utility>
22
23#include "absl/container/flat_hash_map.h"
24#include "absl/status/status.h"
25#include "absl/strings/str_format.h"
29#if !defined(__PORTABLE_PLATFORM__)
30#include "ortools/graph/io.h"
31#endif // __PORTABLE_PLATFORM__
33#include "ortools/base/hash.h"
37#include "ortools/graph/util.h"
40
41ABSL_FLAG(std::string, debug_dump_symmetry_graph_to_file, "",
42 "If this flag is non-empty, an undirected graph whose"
43 " automorphism group is in one-to-one correspondence with the"
44 " symmetries of the SAT problem will be dumped to a file every"
45 " time FindLinearBooleanProblemSymmetries() is called.");
46
47namespace operations_research {
48namespace sat {
49
51
53 const SatSolver& solver, std::vector<bool>* assignment) {
54 assignment->clear();
55 for (int i = 0; i < problem.num_variables(); ++i) {
56 assignment->push_back(
57 solver.Assignment().LiteralIsTrue(Literal(BooleanVariable(i), true)));
58 }
59}
60
61namespace {
62
63// Used by BooleanProblemIsValid() to test that there is no duplicate literals,
64// that they are all within range and that there is no zero coefficient.
65//
66// A non-empty string indicates an error.
67template <typename LinearTerms>
68std::string ValidateLinearTerms(const LinearTerms& terms,
69 std::vector<bool>* variable_seen) {
70 // variable_seen already has all items false and is reset before return.
71 std::string err_str;
72 int num_errs = 0;
73 const int max_num_errs = 100;
74 for (int i = 0; i < terms.literals_size(); ++i) {
75 if (terms.literals(i) == 0) {
76 if (++num_errs <= max_num_errs) {
77 err_str += absl::StrFormat("Zero literal at position %d\n", i);
78 }
79 }
80 if (terms.coefficients(i) == 0) {
81 if (++num_errs <= max_num_errs) {
82 err_str += absl::StrFormat("Literal %d has a zero coefficient\n",
83 terms.literals(i));
84 }
85 }
86 const int var = Literal(terms.literals(i)).Variable().value();
87 if (var >= variable_seen->size()) {
88 if (++num_errs <= max_num_errs) {
89 err_str += absl::StrFormat("Out of bound variable %d\n", var);
90 }
91 }
92 if ((*variable_seen)[var]) {
93 if (++num_errs <= max_num_errs) {
94 err_str += absl::StrFormat("Duplicated variable %d\n", var);
95 }
96 }
97 (*variable_seen)[var] = true;
98 }
99
100 for (int i = 0; i < terms.literals_size(); ++i) {
101 const int var = Literal(terms.literals(i)).Variable().value();
102 (*variable_seen)[var] = false;
103 }
104 if (num_errs) {
105 if (num_errs <= max_num_errs) {
106 err_str = absl::StrFormat("%d validation errors:\n", num_errs) + err_str;
107 } else {
108 err_str =
109 absl::StrFormat("%d validation errors; here are the first %d:\n",
110 num_errs, max_num_errs) +
111 err_str;
112 }
113 }
114 return err_str;
115}
116
117// Converts a linear expression from the protocol buffer format to a vector
118// of LiteralWithCoeff.
119template <typename ProtoFormat>
120std::vector<LiteralWithCoeff> ConvertLinearExpression(
121 const ProtoFormat& input) {
122 std::vector<LiteralWithCoeff> cst;
123 cst.reserve(input.literals_size());
124 for (int i = 0; i < input.literals_size(); ++i) {
125 const Literal literal(input.literals(i));
126 cst.push_back(LiteralWithCoeff(literal, input.coefficients(i)));
127 }
128 return cst;
129}
130
131} // namespace
132
133absl::Status ValidateBooleanProblem(const LinearBooleanProblem& problem) {
134 std::vector<bool> variable_seen(problem.num_variables(), false);
135 for (int i = 0; i < problem.constraints_size(); ++i) {
136 const LinearBooleanConstraint& constraint = problem.constraints(i);
137 const std::string error = ValidateLinearTerms(constraint, &variable_seen);
138 if (!error.empty()) {
139 return absl::Status(
140 absl::StatusCode::kInvalidArgument,
141 absl::StrFormat("Invalid constraint %i: ", i) + error);
142 }
143 }
144 const std::string error =
145 ValidateLinearTerms(problem.objective(), &variable_seen);
146 if (!error.empty()) {
147 return absl::Status(absl::StatusCode::kInvalidArgument,
148 absl::StrFormat("Invalid objective: ") + error);
149 }
150 return ::absl::OkStatus();
151}
152
154 CpModelProto result;
155 for (int i = 0; i < problem.num_variables(); ++i) {
157 if (problem.var_names_size() > i) {
158 var->set_name(problem.var_names(i));
159 }
160 var->add_domain(0);
161 var->add_domain(1);
162 }
163 for (const LinearBooleanConstraint& constraint : problem.constraints()) {
165 ct->set_name(constraint.name());
166 LinearConstraintProto* linear = ct->mutable_linear();
167 int64_t offset = 0;
168 for (int i = 0; i < constraint.literals_size(); ++i) {
169 // Note that the new format is slightly different.
170 const int lit = constraint.literals(i);
171 const int64_t coeff = constraint.coefficients(i);
172 if (lit > 0) {
173 linear->add_vars(lit - 1);
174 linear->add_coeffs(coeff);
175 } else {
176 // The term was coeff * (1 - var).
177 linear->add_vars(-lit - 1);
178 linear->add_coeffs(-coeff);
179 offset -= coeff;
180 }
181 }
182 linear->add_domain(constraint.has_lower_bound()
183 ? constraint.lower_bound() + offset
185 linear->add_domain(constraint.has_upper_bound()
186 ? constraint.upper_bound() + offset
188 }
189 if (problem.has_objective()) {
190 CpObjectiveProto* objective = result.mutable_objective();
191 int64_t offset = 0;
192 for (int i = 0; i < problem.objective().literals_size(); ++i) {
193 const int lit = problem.objective().literals(i);
194 const int64_t coeff = problem.objective().coefficients(i);
195 if (lit > 0) {
196 objective->add_vars(lit - 1);
197 objective->add_coeffs(coeff);
198 } else {
199 objective->add_vars(-lit - 1);
200 objective->add_coeffs(-coeff);
201 offset -= coeff;
202 }
203 }
204 objective->set_offset(offset + problem.objective().offset());
205 objective->set_scaling_factor(problem.objective().scaling_factor());
206 }
207 return result;
208}
209
211 LinearObjective* objective = problem->mutable_objective();
212 objective->set_scaling_factor(-objective->scaling_factor());
213 objective->set_offset(-objective->offset());
214 // We need 'auto' here to keep the open-source compilation happy
215 // (it uses the public protobuf release).
216 for (auto& coefficients_ref : *objective->mutable_coefficients()) {
217 coefficients_ref = -coefficients_ref;
218 }
219}
220
222 SatSolver* solver) {
223 // TODO(user): Currently, the sat solver can load without any issue
224 // constraints with duplicate variables, so we just output a warning if the
225 // problem is not "valid". Make this a strong check once we have some
226 // preprocessing step to remove duplicates variable in the constraints.
227 const absl::Status status = ValidateBooleanProblem(problem);
228 if (!status.ok()) {
229 LOG(WARNING) << "The given problem is invalid!";
230 }
231
232 if (solver->parameters().log_search_progress()) {
233 LOG(INFO) << "Loading problem '" << problem.name() << "', "
234 << problem.num_variables() << " variables, "
235 << problem.constraints_size() << " constraints.";
236 }
237 solver->SetNumVariables(problem.num_variables());
238 std::vector<LiteralWithCoeff> cst;
239 int64_t num_terms = 0;
240 int num_constraints = 0;
241 for (const LinearBooleanConstraint& constraint : problem.constraints()) {
242 num_terms += constraint.literals_size();
243 cst = ConvertLinearExpression(constraint);
244 if (!solver->AddLinearConstraint(
245 constraint.has_lower_bound(), Coefficient(constraint.lower_bound()),
246 constraint.has_upper_bound(), Coefficient(constraint.upper_bound()),
247 &cst)) {
248 LOG(INFO) << "Problem detected to be UNSAT when "
249 << "adding the constraint #" << num_constraints
250 << " with name '" << constraint.name() << "'";
251 return false;
252 }
253 ++num_constraints;
254 }
255 if (solver->parameters().log_search_progress()) {
256 LOG(INFO) << "The problem contains " << num_terms << " terms.";
257 }
258 return true;
259}
260
262 SatSolver* solver) {
263 const absl::Status status = ValidateBooleanProblem(*problem);
264 if (!status.ok()) {
265 LOG(WARNING) << "The given problem is invalid! " << status.message();
266 }
267 if (solver->parameters().log_search_progress()) {
268#if !defined(__PORTABLE_PLATFORM__)
269 LOG(INFO) << "LinearBooleanProblem memory: " << problem->SpaceUsedLong();
270#endif
271 LOG(INFO) << "Loading problem '" << problem->name() << "', "
272 << problem->num_variables() << " variables, "
273 << problem->constraints_size() << " constraints.";
274 }
275 solver->SetNumVariables(problem->num_variables());
276 std::vector<LiteralWithCoeff> cst;
277 int64_t num_terms = 0;
278 int num_constraints = 0;
279
280 // We will process the constraints backward so we can free the memory used by
281 // each constraint just after processing it. Because of that, we initially
282 // reverse all the constraints to add them in the same order.
283 std::reverse(problem->mutable_constraints()->begin(),
284 problem->mutable_constraints()->end());
285 for (int i = problem->constraints_size() - 1; i >= 0; --i) {
286 const LinearBooleanConstraint& constraint = problem->constraints(i);
287 num_terms += constraint.literals_size();
288 cst = ConvertLinearExpression(constraint);
289 if (!solver->AddLinearConstraint(
290 constraint.has_lower_bound(), Coefficient(constraint.lower_bound()),
291 constraint.has_upper_bound(), Coefficient(constraint.upper_bound()),
292 &cst)) {
293 LOG(INFO) << "Problem detected to be UNSAT when "
294 << "adding the constraint #" << num_constraints
295 << " with name '" << constraint.name() << "'";
296 return false;
297 }
298 delete problem->mutable_constraints()->ReleaseLast();
299 ++num_constraints;
300 }
301 LinearBooleanProblem empty_problem;
302 problem->mutable_constraints()->Swap(empty_problem.mutable_constraints());
303 if (solver->parameters().log_search_progress()) {
304 LOG(INFO) << "The problem contains " << num_terms << " terms.";
305 }
306 return true;
307}
308
310 SatSolver* solver) {
311 const LinearObjective& objective = problem.objective();
312 CHECK_EQ(objective.literals_size(), objective.coefficients_size());
313 int64_t max_abs_weight = 0;
314 for (const int64_t coefficient : objective.coefficients()) {
315 max_abs_weight = std::max(max_abs_weight, std::abs(coefficient));
316 }
317 const double max_abs_weight_double = max_abs_weight;
318 for (int i = 0; i < objective.literals_size(); ++i) {
319 const Literal literal(objective.literals(i));
320 const int64_t coefficient = objective.coefficients(i);
321 const double abs_weight = std::abs(coefficient) / max_abs_weight_double;
322 // Because this is a minimization problem, we prefer to assign a Boolean
323 // variable to its "low" objective value. So if a literal has a positive
324 // weight when true, we want to set it to false.
326 coefficient > 0 ? literal.Negated() : literal, abs_weight);
327 }
328}
329
332 std::vector<LiteralWithCoeff> cst =
333 ConvertLinearExpression(problem.objective());
334 return solver->AddLinearConstraint(false, Coefficient(0), true, upper_bound,
335 &cst);
336}
337
339 bool use_lower_bound, Coefficient lower_bound,
340 bool use_upper_bound, Coefficient upper_bound,
341 SatSolver* solver) {
342 std::vector<LiteralWithCoeff> cst =
343 ConvertLinearExpression(problem.objective());
344 return solver->AddLinearConstraint(use_lower_bound, lower_bound,
345 use_upper_bound, upper_bound, &cst);
346}
347
349 const std::vector<bool>& assignment) {
350 CHECK_EQ(assignment.size(), problem.num_variables());
351 Coefficient sum(0);
352 const LinearObjective& objective = problem.objective();
353 for (int i = 0; i < objective.literals_size(); ++i) {
354 const Literal literal(objective.literals(i));
355 if (assignment[literal.Variable().value()] == literal.IsPositive()) {
356 sum += objective.coefficients(i);
357 }
358 }
359 return sum;
360}
361
363 const std::vector<bool>& assignment) {
364 CHECK_EQ(assignment.size(), problem.num_variables());
365
366 // Check that all constraints are satisfied.
367 for (const LinearBooleanConstraint& constraint : problem.constraints()) {
368 Coefficient sum(0);
369 for (int i = 0; i < constraint.literals_size(); ++i) {
370 const Literal literal(constraint.literals(i));
371 if (assignment[literal.Variable().value()] == literal.IsPositive()) {
372 sum += constraint.coefficients(i);
373 }
374 }
375 if (constraint.has_lower_bound() && sum < constraint.lower_bound()) {
376 LOG(WARNING) << "Unsatisfied constraint! sum: " << sum << "\n"
377 << ProtobufDebugString(constraint);
378 return false;
379 }
380 if (constraint.has_upper_bound() && sum > constraint.upper_bound()) {
381 LOG(WARNING) << "Unsatisfied constraint! sum: " << sum << "\n"
382 << ProtobufDebugString(constraint);
383 return false;
384 }
385 }
386 return true;
387}
388
389// Note(user): This function makes a few assumptions about the format of the
390// given LinearBooleanProblem. All constraint coefficients must be 1 (and of the
391// form >= 1) and all objective weights must be strictly positive.
393 const LinearBooleanProblem& problem) {
394 std::string output;
395 const bool is_wcnf = (problem.objective().coefficients_size() > 0);
396 const LinearObjective& objective = problem.objective();
397
398 // Hack: We know that all the variables with index greater than this have been
399 // created "artificially" in order to encode a max-sat problem into our
400 // format. Each extra variable appear only once, and was used as a slack to
401 // reify a soft clause.
402 const int first_slack_variable = problem.original_num_variables();
403
404 // This will contains the objective.
405 absl::flat_hash_map<int, int64_t> literal_to_weight;
406 std::vector<std::pair<int, int64_t>> non_slack_objective;
407
408 // This will be the weight of the "hard" clauses in the wcnf format. It must
409 // be greater than the sum of the weight of all the soft clauses, so we will
410 // just set it to this sum + 1.
411 int64_t hard_weight = 1;
412 if (is_wcnf) {
413 int i = 0;
414 for (int64_t weight : objective.coefficients()) {
415 CHECK_NE(weight, 0);
416 int signed_literal = objective.literals(i);
417
418 // There is no direct support for an objective offset in the wcnf format.
419 // So this is not a perfect translation of the objective. It is however
420 // possible to achieve the same effect by adding a new variable x, and two
421 // soft clauses: x with weight offset, and -x with weight offset.
422 //
423 // TODO(user): implement this trick.
424 if (weight < 0) {
425 signed_literal = -signed_literal;
426 weight = -weight;
427 }
428 literal_to_weight[objective.literals(i)] = weight;
429 if (Literal(signed_literal).Variable() < first_slack_variable) {
430 non_slack_objective.push_back(std::make_pair(signed_literal, weight));
431 }
432 hard_weight += weight;
433 ++i;
434 }
435 output += absl::StrFormat("p wcnf %d %d %d\n", first_slack_variable,
436 static_cast<int>(problem.constraints_size() +
437 non_slack_objective.size()),
438 hard_weight);
439 } else {
440 output += absl::StrFormat("p cnf %d %d\n", problem.num_variables(),
441 problem.constraints_size());
442 }
443
444 std::string constraint_output;
445 for (const LinearBooleanConstraint& constraint : problem.constraints()) {
446 if (constraint.literals_size() == 0) return ""; // Assumption.
447 constraint_output.clear();
448 int64_t weight = hard_weight;
449 for (int i = 0; i < constraint.literals_size(); ++i) {
450 if (constraint.coefficients(i) != 1) return ""; // Assumption.
451 if (is_wcnf && abs(constraint.literals(i)) - 1 >= first_slack_variable) {
452 weight = literal_to_weight[constraint.literals(i)];
453 } else {
454 if (i > 0) constraint_output += " ";
455 constraint_output += Literal(constraint.literals(i)).DebugString();
456 }
457 }
458 if (is_wcnf) {
459 output += absl::StrFormat("%d ", weight);
460 }
461 output += constraint_output + " 0\n";
462 }
463
464 // Output the rest of the objective as singleton constraints.
465 if (is_wcnf) {
466 for (std::pair<int, int64_t> p : non_slack_objective) {
467 // Since it is falsifying this clause that cost "weigtht", we need to take
468 // its negation.
469 const Literal literal(-p.first);
470 output += absl::StrFormat("%d %s 0\n", p.second, literal.DebugString());
471 }
472 }
473
474 return output;
475}
476
478 BooleanAssignment* output) {
479 output->clear_literals();
480 for (BooleanVariable var(0); var < assignment.NumberOfVariables(); ++var) {
481 if (assignment.VariableIsAssigned(var)) {
482 output->add_literals(
484 }
485 }
486}
487
489 const std::vector<int>& constraint_indices,
490 LinearBooleanProblem* subproblem) {
491 *subproblem = problem;
492 subproblem->set_name("Subproblem of " + problem.name());
493 subproblem->clear_constraints();
494 for (int index : constraint_indices) {
495 CHECK_LT(index, problem.constraints_size());
496 subproblem->add_constraints()->MergeFrom(problem.constraints(index));
497 }
498}
499
500namespace {
501// A simple class to generate equivalence class number for
502// GenerateGraphForSymmetryDetection().
503class IdGenerator {
504 public:
505 IdGenerator() {}
506
507 // If the pair (type, coefficient) was never seen before, then generate
508 // a new id, otherwise return the previously generated id.
509 int GetId(int type, Coefficient coefficient) {
510 const std::pair<int, int64_t> key(type, coefficient.value());
511 return gtl::LookupOrInsert(&id_map_, key, id_map_.size());
512 }
513
514 private:
515 absl::flat_hash_map<std::pair<int, int64_t>, int> id_map_;
516};
517} // namespace.
518
519// Returns a graph whose automorphisms can be mapped back to the symmetries of
520// the given LinearBooleanProblem.
521//
522// Any permutation of the graph that respects the initial_equivalence_classes
523// output can be mapped to a symmetry of the given problem simply by taking its
524// restriction on the first 2 * num_variables nodes and interpreting its index
525// as a literal index. In a sense, a node with a low enough index #i is in
526// one-to-one correspondence with a literals #i (using the index representation
527// of literal).
528//
529// The format of the initial_equivalence_classes is the same as the one
530// described in GraphSymmetryFinder::FindSymmetries(). The classes must be dense
531// in [0, num_classes) and any symmetry will only map nodes with the same class
532// between each other.
533template <typename Graph>
535 const LinearBooleanProblem& problem,
536 std::vector<int>* initial_equivalence_classes) {
537 // First, we convert the problem to its canonical representation.
538 const int num_variables = problem.num_variables();
539 CanonicalBooleanLinearProblem canonical_problem;
540 std::vector<LiteralWithCoeff> cst;
541 for (const LinearBooleanConstraint& constraint : problem.constraints()) {
542 cst = ConvertLinearExpression(constraint);
543 CHECK(canonical_problem.AddLinearConstraint(
544 constraint.has_lower_bound(), Coefficient(constraint.lower_bound()),
545 constraint.has_upper_bound(), Coefficient(constraint.upper_bound()),
546 &cst));
547 }
548
549 // TODO(user): reserve the memory for the graph? not sure it is worthwhile
550 // since it would require some linear scan of the problem though.
551 Graph* graph = new Graph();
552 initial_equivalence_classes->clear();
553
554 // We will construct a graph with 3 different types of node that must be
555 // in different equivalent classes.
556 enum NodeType { LITERAL_NODE, CONSTRAINT_NODE, CONSTRAINT_COEFFICIENT_NODE };
557 IdGenerator id_generator;
558
559 // First, we need one node per literal with an edge between each literal
560 // and its negation.
561 for (int i = 0; i < num_variables; ++i) {
562 // We have two nodes for each variable.
563 // Note that the indices are in [0, 2 * num_variables) and in one to one
564 // correspondence with the index representation of a literal.
565 const Literal literal = Literal(BooleanVariable(i), true);
566 graph->AddArc(literal.Index().value(), literal.NegatedIndex().value());
567 graph->AddArc(literal.NegatedIndex().value(), literal.Index().value());
568 }
569
570 // We use 0 for their initial equivalence class, but that may be modified
571 // with the objective coefficient (see below).
572 initial_equivalence_classes->assign(
573 2 * num_variables,
574 id_generator.GetId(NodeType::LITERAL_NODE, Coefficient(0)));
575
576 // Literals with different objective coeffs shouldn't be in the same class.
577 //
578 // We need to canonicalize the objective to regroup literals corresponding
579 // to the same variables. Note that we don't care about the offset or
580 // optimization direction here, we just care about literals with the same
581 // canonical coefficient.
582 Coefficient shift;
583 Coefficient max_value;
584 std::vector<LiteralWithCoeff> expr =
585 ConvertLinearExpression(problem.objective());
586 ComputeBooleanLinearExpressionCanonicalForm(&expr, &shift, &max_value);
587 for (LiteralWithCoeff term : expr) {
588 (*initial_equivalence_classes)[term.literal.Index().value()] =
589 id_generator.GetId(NodeType::LITERAL_NODE, term.coefficient);
590 }
591
592 // Then, for each constraint, we will have one or more nodes.
593 for (int i = 0; i < canonical_problem.NumConstraints(); ++i) {
594 // First we have a node for the constraint with an equivalence class
595 // depending on the rhs.
596 //
597 // Note: Since we add nodes one by one, initial_equivalence_classes->size()
598 // gives the number of nodes at any point, which we use as next node index.
599 const int constraint_node_index = initial_equivalence_classes->size();
600 initial_equivalence_classes->push_back(id_generator.GetId(
601 NodeType::CONSTRAINT_NODE, canonical_problem.Rhs(i)));
602
603 // This node will also be connected to all literals of the constraint
604 // with a coefficient of 1. Literals with new coefficients will be grouped
605 // under a new node connected to the constraint_node_index.
606 //
607 // Note that this works because a canonical constraint is sorted by
608 // increasing coefficient value (all positive).
609 int current_node_index = constraint_node_index;
610 Coefficient previous_coefficient(1);
611 for (LiteralWithCoeff term : canonical_problem.Constraint(i)) {
612 if (term.coefficient != previous_coefficient) {
613 current_node_index = initial_equivalence_classes->size();
614 initial_equivalence_classes->push_back(id_generator.GetId(
615 NodeType::CONSTRAINT_COEFFICIENT_NODE, term.coefficient));
616 previous_coefficient = term.coefficient;
617
618 // Connect this node to the constraint node. Note that we don't
619 // technically need the arcs in both directions, but that may help a bit
620 // the algorithm to find symmetries.
621 graph->AddArc(constraint_node_index, current_node_index);
622 graph->AddArc(current_node_index, constraint_node_index);
623 }
624
625 // Connect this node to the associated term.literal node. Note that we
626 // don't technically need the arcs in both directions, but that may help a
627 // bit the algorithm to find symmetries.
628 graph->AddArc(current_node_index, term.literal.Index().value());
629 graph->AddArc(term.literal.Index().value(), current_node_index);
630 }
631 }
632 graph->Build();
633 DCHECK_EQ(graph->num_nodes(), initial_equivalence_classes->size());
634 return graph;
635}
636
638 // Objective.
639 LinearObjective* mutable_objective = problem->mutable_objective();
640 int64_t objective_offset = 0;
641 for (int i = 0; i < mutable_objective->literals_size(); ++i) {
642 const int signed_literal = mutable_objective->literals(i);
643 if (signed_literal < 0) {
644 const int64_t coefficient = mutable_objective->coefficients(i);
645 mutable_objective->set_literals(i, -signed_literal);
646 mutable_objective->set_coefficients(i, -coefficient);
647 objective_offset += coefficient;
648 }
649 }
650 mutable_objective->set_offset(mutable_objective->offset() + objective_offset);
651
652 // Constraints.
653 for (LinearBooleanConstraint& constraint :
654 *(problem->mutable_constraints())) {
655 int64_t sum = 0;
656 for (int i = 0; i < constraint.literals_size(); ++i) {
657 if (constraint.literals(i) < 0) {
658 sum += constraint.coefficients(i);
659 constraint.set_literals(i, -constraint.literals(i));
660 constraint.set_coefficients(i, -constraint.coefficients(i));
661 }
662 }
663 if (constraint.has_lower_bound()) {
664 constraint.set_lower_bound(constraint.lower_bound() - sum);
665 }
666 if (constraint.has_upper_bound()) {
667 constraint.set_upper_bound(constraint.upper_bound() - sum);
668 }
669 }
670}
671
673 const LinearBooleanProblem& problem,
674 std::vector<std::unique_ptr<SparsePermutation>>* generators) {
676 std::vector<int> equivalence_classes;
677 std::unique_ptr<Graph> graph(
678 GenerateGraphForSymmetryDetection<Graph>(problem, &equivalence_classes));
679 LOG(INFO) << "Graph has " << graph->num_nodes() << " nodes and "
680 << graph->num_arcs() / 2 << " edges.";
681#if !defined(__PORTABLE_PLATFORM__)
682 if (!absl::GetFlag(FLAGS_debug_dump_symmetry_graph_to_file).empty()) {
683 // Remap the graph nodes to sort them by equivalence classes.
684 std::vector<int> new_node_index(graph->num_nodes(), -1);
685 const int num_classes = 1 + *std::max_element(equivalence_classes.begin(),
686 equivalence_classes.end());
687 std::vector<int> class_size(num_classes, 0);
688 for (const int c : equivalence_classes) ++class_size[c];
689 std::vector<int> next_index_by_class(num_classes, 0);
690 std::partial_sum(class_size.begin(), class_size.end() - 1,
691 next_index_by_class.begin() + 1);
692 for (int node = 0; node < graph->num_nodes(); ++node) {
693 new_node_index[node] = next_index_by_class[equivalence_classes[node]]++;
694 }
695 std::unique_ptr<Graph> remapped_graph = RemapGraph(*graph, new_node_index);
696 const absl::Status status = util::WriteGraphToFile(
697 *remapped_graph, absl::GetFlag(FLAGS_debug_dump_symmetry_graph_to_file),
698 /*directed=*/false, class_size);
699 if (!status.ok()) {
700 LOG(DFATAL) << "Error when writing the symmetry graph to file: "
701 << status;
702 }
703 }
704#endif // __PORTABLE_PLATFORM__
705 GraphSymmetryFinder symmetry_finder(*graph,
706 /*is_undirected=*/true);
707 std::vector<int> factorized_automorphism_group_size;
708 // TODO(user): inject the appropriate time limit here.
709 CHECK_OK(symmetry_finder.FindSymmetries(&equivalence_classes, generators,
710 &factorized_automorphism_group_size));
711
712 // Remove from the permutations the part not concerning the literals.
713 // Note that some permutation may becomes empty, which means that we had
714 // duplicates constraints. TODO(user): Remove them beforehand?
715 double average_support_size = 0.0;
716 int num_generators = 0;
717 for (int i = 0; i < generators->size(); ++i) {
718 SparsePermutation* permutation = (*generators)[i].get();
719 std::vector<int> to_delete;
720 for (int j = 0; j < permutation->NumCycles(); ++j) {
721 if (*(permutation->Cycle(j).begin()) >= 2 * problem.num_variables()) {
722 to_delete.push_back(j);
723 if (DEBUG_MODE) {
724 // Verify that the cycle's entire support does not touch any variable.
725 for (const int node : permutation->Cycle(j)) {
726 DCHECK_GE(node, 2 * problem.num_variables());
727 }
728 }
729 }
730 }
731 permutation->RemoveCycles(to_delete);
732 if (!permutation->Support().empty()) {
733 average_support_size += permutation->Support().size();
734 swap((*generators)[num_generators], (*generators)[i]);
735 ++num_generators;
736 }
737 }
738 generators->resize(num_generators);
739 average_support_size /= num_generators;
740 LOG(INFO) << "# of generators: " << num_generators;
741 LOG(INFO) << "Average support size: " << average_support_size;
742}
743
746 LinearBooleanProblem* problem) {
747 Coefficient bound_shift;
748 Coefficient max_value;
749 std::vector<LiteralWithCoeff> cst;
750
751 // First the objective.
752 cst = ConvertLinearExpression(problem->objective());
753 ApplyLiteralMapping(mapping, &cst, &bound_shift, &max_value);
754 LinearObjective* mutable_objective = problem->mutable_objective();
755 mutable_objective->clear_literals();
756 mutable_objective->clear_coefficients();
757 mutable_objective->set_offset(mutable_objective->offset() -
758 bound_shift.value());
759 for (const LiteralWithCoeff& entry : cst) {
760 mutable_objective->add_literals(entry.literal.SignedValue());
761 mutable_objective->add_coefficients(entry.coefficient.value());
762 }
763
764 // Now the clauses.
765 for (LinearBooleanConstraint& constraint : *problem->mutable_constraints()) {
766 cst = ConvertLinearExpression(constraint);
767 constraint.clear_literals();
768 constraint.clear_coefficients();
769 ApplyLiteralMapping(mapping, &cst, &bound_shift, &max_value);
770
771 // Add bound_shift to the bounds and remove a bound if it is now trivial.
772 if (constraint.has_upper_bound()) {
773 constraint.set_upper_bound(constraint.upper_bound() +
774 bound_shift.value());
775 if (max_value <= constraint.upper_bound()) {
776 constraint.clear_upper_bound();
777 }
778 }
779 if (constraint.has_lower_bound()) {
780 constraint.set_lower_bound(constraint.lower_bound() +
781 bound_shift.value());
782 // This is because ApplyLiteralMapping make all coefficient positive.
783 if (constraint.lower_bound() <= 0) {
784 constraint.clear_lower_bound();
785 }
786 }
787
788 // If the constraint is always true, we just leave it empty.
789 if (constraint.has_lower_bound() || constraint.has_upper_bound()) {
790 for (const LiteralWithCoeff& entry : cst) {
791 constraint.add_literals(entry.literal.SignedValue());
792 constraint.add_coefficients(entry.coefficient.value());
793 }
794 }
795 }
796
797 // Remove empty constraints.
798 int new_index = 0;
799 const int num_constraints = problem->constraints_size();
800 for (int i = 0; i < num_constraints; ++i) {
801 if (!(problem->constraints(i).literals_size() == 0)) {
802 problem->mutable_constraints()->SwapElements(i, new_index);
803 ++new_index;
804 }
805 }
806 problem->mutable_constraints()->DeleteSubrange(new_index,
807 num_constraints - new_index);
808
809 // Computes the new number of variables and set it.
810 int num_vars = 0;
811 for (LiteralIndex index : mapping) {
812 if (index >= 0) {
813 num_vars = std::max(num_vars, Literal(index).Variable().value() + 1);
814 }
815 }
816 problem->set_num_variables(num_vars);
817
818 // TODO(user): The names is currently all scrambled. Do something about it
819 // so that non-fixed variables keep their names.
820 problem->mutable_var_names()->DeleteSubrange(
821 num_vars, problem->var_names_size() - num_vars);
822}
823
824// A simple preprocessing step that does basic probing and removes the
825// equivalent literals.
827 LinearBooleanProblem* problem) {
828 // TODO(user): expose the number of iterations as a parameter.
829 for (int iter = 0; iter < 6; ++iter) {
830 SatSolver solver;
831 if (!LoadBooleanProblem(*problem, &solver)) {
832 LOG(INFO) << "UNSAT when loading the problem.";
833 }
834
836 ProbeAndFindEquivalentLiteral(&solver, postsolver, /*drat_writer=*/nullptr,
837 &equiv_map);
838
839 // We can abort if no information is learned.
840 if (equiv_map.empty() && solver.LiteralTrail().Index() == 0) break;
841
842 if (equiv_map.empty()) {
843 const int num_literals = 2 * solver.NumVariables();
844 for (LiteralIndex index(0); index < num_literals; ++index) {
845 equiv_map.push_back(index);
846 }
847 }
848
849 // Fix fixed variables in the equivalence map and in the postsolver.
850 solver.Backtrack(0);
851 for (int i = 0; i < solver.LiteralTrail().Index(); ++i) {
852 const Literal l = solver.LiteralTrail()[i];
853 equiv_map[l.Index()] = kTrueLiteralIndex;
854 equiv_map[l.NegatedIndex()] = kFalseLiteralIndex;
855 postsolver->FixVariable(l);
856 }
857
858 // Remap the variables into a dense set. All the variables for which the
859 // equiv_map is not the identity are no longer needed.
860 BooleanVariable new_var(0);
862 for (BooleanVariable var(0); var < solver.NumVariables(); ++var) {
863 if (equiv_map[Literal(var, true).Index()] == Literal(var, true).Index()) {
864 var_map.push_back(new_var);
865 ++new_var;
866 } else {
867 var_map.push_back(BooleanVariable(-1));
868 }
869 }
870
871 // Apply the variable mapping.
872 postsolver->ApplyMapping(var_map);
873 for (LiteralIndex index(0); index < equiv_map.size(); ++index) {
874 if (equiv_map[index] >= 0) {
875 const Literal l(equiv_map[index]);
876 const BooleanVariable image = var_map[l.Variable()];
877 CHECK_NE(image, BooleanVariable(-1));
878 equiv_map[index] = Literal(image, l.IsPositive()).Index();
879 }
880 }
881 ApplyLiteralMappingToBooleanProblem(equiv_map, problem);
882 }
883}
884
885} // namespace sat
886} // namespace operations_research
int64_t max
Definition: alldiff_cst.cc:140
int64_t min
Definition: alldiff_cst.cc:139
#define CHECK(condition)
Definition: base/logging.h:491
#define CHECK_LT(val1, val2)
Definition: base/logging.h:701
#define CHECK_EQ(val1, val2)
Definition: base/logging.h:698
#define CHECK_OK(x)
Definition: base/logging.h:42
#define DCHECK_GE(val1, val2)
Definition: base/logging.h:890
#define CHECK_NE(val1, val2)
Definition: base/logging.h:699
#define LOG(severity)
Definition: base/logging.h:416
#define DCHECK_EQ(val1, val2)
Definition: base/logging.h:886
ABSL_FLAG(std::string, debug_dump_symmetry_graph_to_file, "", "If this flag is non-empty, an undirected graph whose" " automorphism group is in one-to-one correspondence with the" " symmetries of the SAT problem will be dumped to a file every" " time FindLinearBooleanProblemSymmetries() is called.")
size_type size() const
bool empty() const
void push_back(const value_type &x)
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)
const std::vector< int > & Support() const
void RemoveCycles(const std::vector< int > &cycle_indices)
void add_literals(::PROTOBUF_NAMESPACE_ID::int32 value)
bool AddLinearConstraint(bool use_lower_bound, Coefficient lower_bound, bool use_upper_bound, Coefficient upper_bound, std::vector< LiteralWithCoeff > *cst)
const std::vector< LiteralWithCoeff > & Constraint(int i) const
::operations_research::sat::ConstraintProto * add_constraints()
::operations_research::sat::IntegerVariableProto * add_variables()
::operations_research::sat::CpObjectiveProto * mutable_objective()
void add_coeffs(::PROTOBUF_NAMESPACE_ID::int64 value)
void add_vars(::PROTOBUF_NAMESPACE_ID::int32 value)
::PROTOBUF_NAMESPACE_ID::int64 upper_bound() const
::PROTOBUF_NAMESPACE_ID::int64 lower_bound() const
void Swap(LinearBooleanConstraint *other)
void MergeFrom(const LinearBooleanConstraint &from)
::operations_research::sat::LinearBooleanConstraint * mutable_constraints(int index)
::operations_research::sat::LinearBooleanConstraint * add_constraints()
void set_name(ArgT0 &&arg0, ArgT... args)
void set_num_variables(::PROTOBUF_NAMESPACE_ID::int32 value)
const ::operations_research::sat::LinearObjective & objective() const
::PROTOBUF_NAMESPACE_ID::int32 num_variables() const
const ::operations_research::sat::LinearBooleanConstraint & constraints(int index) const
const std::string & var_names(int index) const
::PROTOBUF_NAMESPACE_ID::int32 original_num_variables() const
::operations_research::sat::LinearObjective * mutable_objective()
void add_domain(::PROTOBUF_NAMESPACE_ID::int64 value)
Definition: cp_model.pb.h:7474
void add_coeffs(::PROTOBUF_NAMESPACE_ID::int64 value)
Definition: cp_model.pb.h:7427
void add_vars(::PROTOBUF_NAMESPACE_ID::int32 value)
Definition: cp_model.pb.h:7380
void add_coefficients(::PROTOBUF_NAMESPACE_ID::int64 value)
::PROTOBUF_NAMESPACE_ID::int64 coefficients(int index) const
void set_coefficients(int index, ::PROTOBUF_NAMESPACE_ID::int64 value)
void set_literals(int index, ::PROTOBUF_NAMESPACE_ID::int32 value)
void add_literals(::PROTOBUF_NAMESPACE_ID::int32 value)
::PROTOBUF_NAMESPACE_ID::RepeatedField< ::PROTOBUF_NAMESPACE_ID::int64 > * mutable_coefficients()
::PROTOBUF_NAMESPACE_ID::int32 literals(int index) const
LiteralIndex NegatedIndex() const
Definition: sat_base.h:86
LiteralIndex Index() const
Definition: sat_base.h:85
BooleanVariable Variable() const
Definition: sat_base.h:81
std::string DebugString() const
Definition: sat_base.h:94
void ApplyMapping(const absl::StrongVector< BooleanVariable, BooleanVariable > &mapping)
bool AddLinearConstraint(bool use_lower_bound, Coefficient lower_bound, bool use_upper_bound, Coefficient upper_bound, std::vector< LiteralWithCoeff > *cst)
Definition: sat_solver.cc:300
void SetNumVariables(int num_variables)
Definition: sat_solver.cc:65
const SatParameters & parameters() const
Definition: sat_solver.cc:111
const VariablesAssignment & Assignment() const
Definition: sat_solver.h:363
const Trail & LiteralTrail() const
Definition: sat_solver.h:362
void SetAssignmentPreference(Literal literal, double weight)
Definition: sat_solver.h:151
void Backtrack(int target_level)
Definition: sat_solver.cc:889
bool VariableIsAssigned(BooleanVariable var) const
Definition: sat_base.h:159
bool LiteralIsTrue(Literal literal) const
Definition: sat_base.h:151
Literal GetTrueLiteralForAssignedVariable(BooleanVariable var) const
Definition: sat_base.h:166
const Constraint * ct
int64_t value
IntVar * var
Definition: expr_array.cc:1874
double upper_bound
double lower_bound
const int WARNING
Definition: log_severity.h:31
const int INFO
Definition: log_severity.h:31
const bool DEBUG_MODE
Definition: macros.h:24
Collection::value_type::second_type & LookupOrInsert(Collection *const collection, const typename Collection::value_type::first_type &key, const typename Collection::value_type::second_type &value)
Definition: map_util.h:237
void swap(IdMap< K, V > &a, IdMap< K, V > &b)
Definition: id_map.h:263
std::tuple< int64_t, int64_t, const double > Coefficient
bool AddObjectiveConstraint(const LinearBooleanProblem &problem, bool use_lower_bound, Coefficient lower_bound, bool use_upper_bound, Coefficient upper_bound, SatSolver *solver)
void StoreAssignment(const VariablesAssignment &assignment, BooleanAssignment *output)
void UseObjectiveForSatAssignmentPreference(const LinearBooleanProblem &problem, SatSolver *solver)
bool ApplyLiteralMapping(const absl::StrongVector< LiteralIndex, LiteralIndex > &mapping, std::vector< LiteralWithCoeff > *cst, Coefficient *bound_shift, Coefficient *max_value)
void ExtractSubproblem(const LinearBooleanProblem &problem, const std::vector< int > &constraint_indices, LinearBooleanProblem *subproblem)
absl::Status ValidateBooleanProblem(const LinearBooleanProblem &problem)
bool AddObjectiveUpperBound(const LinearBooleanProblem &problem, Coefficient upper_bound, SatSolver *solver)
const LiteralIndex kTrueLiteralIndex(-2)
bool ComputeBooleanLinearExpressionCanonicalForm(std::vector< LiteralWithCoeff > *cst, Coefficient *bound_shift, Coefficient *max_value)
const LiteralIndex kFalseLiteralIndex(-3)
bool LoadAndConsumeBooleanProblem(LinearBooleanProblem *problem, SatSolver *solver)
void ApplyLiteralMappingToBooleanProblem(const absl::StrongVector< LiteralIndex, LiteralIndex > &mapping, LinearBooleanProblem *problem)
bool IsAssignmentValid(const LinearBooleanProblem &problem, const std::vector< bool > &assignment)
void ChangeOptimizationDirection(LinearBooleanProblem *problem)
void ProbeAndSimplifyProblem(SatPostsolver *postsolver, LinearBooleanProblem *problem)
void FindLinearBooleanProblemSymmetries(const LinearBooleanProblem &problem, std::vector< std::unique_ptr< SparsePermutation > > *generators)
Coefficient ComputeObjectiveValue(const LinearBooleanProblem &problem, const std::vector< bool > &assignment)
void ProbeAndFindEquivalentLiteral(SatSolver *solver, SatPostsolver *postsolver, DratProofHandler *drat_proof_handler, absl::StrongVector< LiteralIndex, LiteralIndex > *mapping)
CpModelProto BooleanProblemToCpModelproto(const LinearBooleanProblem &problem)
void MakeAllLiteralsPositive(LinearBooleanProblem *problem)
bool LoadBooleanProblem(const LinearBooleanProblem &problem, SatSolver *solver)
std::string LinearBooleanProblemToCnfString(const LinearBooleanProblem &problem)
void ExtractAssignment(const LinearBooleanProblem &problem, const SatSolver &solver, std::vector< bool > *assignment)
Graph * GenerateGraphForSymmetryDetection(const LinearBooleanProblem &problem, std::vector< int > *initial_equivalence_classes)
Collection of objects used to extend the Constraint Solver library.
std::string ProtobufDebugString(const P &message)
ListGraph Graph
Definition: graph.h:2361
absl::Status WriteGraphToFile(const Graph &graph, const std::string &filename, bool directed, const std::vector< int > &num_nodes_with_color)
Definition: io.h:235
std::unique_ptr< Graph > RemapGraph(const Graph &graph, const std::vector< int > &new_node_index)
Definition: graph/util.h:276
Literal literal
Definition: optimization.cc:85
int64_t weight
Definition: pack.cc:510
int index
Definition: pack.cc:509
static int input(yyscan_t yyscanner)
int64_t coefficient
std::vector< int >::const_iterator begin() const