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