OR-Tools  9.6
sat_cnf_reader.h
Go to the documentation of this file.
1 // Copyright 2010-2022 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 
14 #ifndef OR_TOOLS_SAT_SAT_CNF_READER_H_
15 #define OR_TOOLS_SAT_SAT_CNF_READER_H_
16 
17 #include <cstdint>
18 #include <memory>
19 #include <string>
20 #include <utility>
21 #include <vector>
22 
23 #include "absl/container/btree_map.h"
24 #include "absl/flags/flag.h"
25 #include "absl/strings/numbers.h"
26 #include "absl/strings/str_split.h"
27 #include "absl/strings/string_view.h"
28 #include "absl/types/span.h"
31 #include "ortools/base/logging.h"
32 #include "ortools/base/macros.h"
33 #include "ortools/sat/boolean_problem.pb.h"
34 #include "ortools/sat/cp_model.pb.h"
36 
37 ABSL_FLAG(bool, wcnf_use_strong_slack, true,
38  "If true, when we add a slack variable to reify a soft clause, we "
39  "enforce the fact that when it is true, the clause must be false.");
40 
41 namespace operations_research {
42 namespace sat {
43 
45  explicit LinearBooleanProblemWrapper(LinearBooleanProblem* p) : problem(p) {}
46 
47  void SetNumVariables(int num) { problem->set_num_variables(num); }
48  void SetOriginalNumVariables(int num) {
49  problem->set_original_num_variables(num);
50  }
51 
52  void AddConstraint(absl::Span<const int> clause) {
53  LinearBooleanConstraint* constraint = problem->add_constraints();
54  constraint->mutable_literals()->Reserve(clause.size());
55  constraint->mutable_coefficients()->Reserve(clause.size());
56  constraint->set_lower_bound(1);
57  for (const int literal : clause) {
58  constraint->add_literals(literal);
59  constraint->add_coefficients(1);
60  }
61  }
62 
63  void AddObjectiveTerm(int literal, int64_t value) {
64  CHECK_GE(literal, 0) << "Negative literal not supported.";
65  problem->mutable_objective()->add_literals(literal);
66  problem->mutable_objective()->add_coefficients(value);
67  }
68 
69  void SetObjectiveOffset(int64_t offset) {
70  problem->mutable_objective()->set_offset(offset);
71  }
72 
73  LinearBooleanProblem* problem;
74 };
75 
77  explicit CpModelProtoWrapper(CpModelProto* p) : problem(p) {}
78 
79  void SetNumVariables(int num) {
80  for (int i = 0; i < num; ++i) {
81  IntegerVariableProto* variable = problem->add_variables();
82  variable->add_domain(0);
83  variable->add_domain(1);
84  }
85  }
86 
87  // TODO(user): Not supported. This is only used for displaying a wcnf
88  // solution in cnf format, so it is not useful internally. Instead of adding
89  // another field, we could use the variables names or the search heuristics
90  // to encode this info.
91  void SetOriginalNumVariables(int num) {}
92 
93  int LiteralToRef(int signed_value) {
94  return signed_value > 0 ? signed_value - 1 : signed_value;
95  }
96 
97  void AddConstraint(absl::Span<const int> clause) {
98  auto* constraint = problem->add_constraints()->mutable_bool_or();
99  constraint->mutable_literals()->Reserve(clause.size());
100  for (const int literal : clause) {
101  constraint->add_literals(LiteralToRef(literal));
102  }
103  }
104 
105  void AddObjectiveTerm(int literal, int64_t value) {
106  CHECK_GE(literal, 0) << "Negative literal not supported.";
107  problem->mutable_objective()->add_vars(LiteralToRef(literal));
108  problem->mutable_objective()->add_coeffs(value);
109  }
110 
111  void SetObjectiveOffset(int64_t offset) {
112  problem->mutable_objective()->set_offset(offset);
113  }
114 
115  CpModelProto* problem;
116 };
117 
118 // This class loads a file in cnf file format into a SatProblem.
119 // The format is described here:
120 // http://people.sc.fsu.edu/~jburkardt/data/cnf/cnf.html
121 //
122 // It also support the wcnf input format for partial weighted max-sat problems.
124  public:
125  SatCnfReader() : interpret_cnf_as_max_sat_(false) {}
126 
127  // If called with true, then a cnf file will be converted to the max-sat
128  // problem: Try to minimize the number of unsatisfiable clauses.
129  void InterpretCnfAsMaxSat(bool v) { interpret_cnf_as_max_sat_ = v; }
130 
131  // Loads the given cnf filename into the given proto.
132  bool Load(const std::string& filename, LinearBooleanProblem* problem) {
133  problem->Clear();
134  problem->set_name(ExtractProblemName(filename));
135  LinearBooleanProblemWrapper wrapper(problem);
136  return LoadInternal(filename, &wrapper);
137  }
138  bool Load(const std::string& filename, CpModelProto* problem) {
139  problem->Clear();
140  problem->set_name(ExtractProblemName(filename));
141  CpModelProtoWrapper wrapper(problem);
142  return LoadInternal(filename, &wrapper);
143  }
144 
145  private:
146  template <class Problem>
147  bool LoadInternal(const std::string& filename, Problem* problem) {
148  positive_literal_to_weight_.clear();
149  objective_offset_ = 0;
150  is_wcnf_ = false;
151  end_marker_seen_ = false;
152  hard_weight_ = 0;
153  num_skipped_soft_clauses_ = 0;
154  num_singleton_soft_clauses_ = 0;
155  num_added_clauses_ = 0;
156  num_slack_variables_ = 0;
157 
158  int num_lines = 0;
159  for (const std::string& line : FileLines(filename)) {
160  ++num_lines;
161  ProcessNewLine(line, problem);
162  }
163  if (num_lines == 0) {
164  LOG(FATAL) << "File '" << filename << "' is empty or can't be read.";
165  }
166  problem->SetOriginalNumVariables(num_variables_);
167  problem->SetNumVariables(num_variables_ + num_slack_variables_);
168 
169  // Fill the objective.
170  if (!positive_literal_to_weight_.empty()) {
171  for (const std::pair<int, int64_t> p : positive_literal_to_weight_) {
172  if (p.second != 0) {
173  problem->AddObjectiveTerm(p.first, p.second);
174  }
175  }
176  problem->SetObjectiveOffset(objective_offset_);
177  }
178 
179  if (num_clauses_ != num_added_clauses_ + num_singleton_soft_clauses_ +
180  num_skipped_soft_clauses_) {
181  LOG(ERROR) << "Wrong number of clauses. " << num_clauses_ << " "
182  << num_added_clauses_;
183  return false;
184  }
185  return true;
186  }
187 
188  // Since the problem name is not stored in the cnf format, we infer it from
189  // the file name.
190  static std::string ExtractProblemName(const std::string& filename) {
191  const int found = filename.find_last_of('/');
192  const std::string problem_name =
193  found != std::string::npos ? filename.substr(found + 1) : filename;
194  return problem_name;
195  }
196 
197  int64_t StringPieceAtoi(absl::string_view input) {
198  int64_t value;
199  // Hack: data() is not null terminated, but we do know that it points
200  // inside a string where numbers are separated by " " and since SimpleAtoi
201  // will stop at the first invalid char, this works.
202  CHECK(absl::SimpleAtoi(input, &value));
203  return value;
204  }
205 
206  void ProcessHeader(const std::string& line) {
207  static const char kWordDelimiters[] = " ";
208  words_ = absl::StrSplit(line, kWordDelimiters, absl::SkipEmpty());
209 
210  CHECK_EQ(words_[0], "p");
211  if (words_[1] == "cnf" || words_[1] == "wcnf") {
212  num_variables_ = StringPieceAtoi(words_[2]);
213  num_clauses_ = StringPieceAtoi(words_[3]);
214  if (words_[1] == "wcnf") {
215  is_wcnf_ = true;
216  hard_weight_ = (words_.size() > 4) ? StringPieceAtoi(words_[4]) : 0;
217  }
218  } else {
219  // TODO(user): The ToString() is only required for the open source. Fix.
220  LOG(FATAL) << "Unknown file type: " << words_[1];
221  }
222  }
223 
224  template <class Problem>
225  void ProcessNewLine(const std::string& line, Problem* problem) {
226  if (line.empty() || end_marker_seen_) return;
227  if (line[0] == 'c') return;
228  if (line[0] == '%') {
229  end_marker_seen_ = true;
230  return;
231  }
232  if (line[0] == 'p') {
233  ProcessHeader(line);
234  return;
235  }
236 
237  static const char kWordDelimiters[] = " ";
238  auto splitter = absl::StrSplit(line, kWordDelimiters, absl::SkipEmpty());
239 
240  tmp_clause_.clear();
241  int64_t weight =
242  (!is_wcnf_ && interpret_cnf_as_max_sat_) ? 1 : hard_weight_;
243  bool first = true;
244  bool end_marker_seen = false;
245  for (const absl::string_view word : splitter) {
246  const int64_t signed_value = StringPieceAtoi(word);
247  if (first && is_wcnf_) {
248  // Mathematically, a soft clause of weight 0 can be removed.
249  if (signed_value == 0) {
250  ++num_skipped_soft_clauses_;
251  return;
252  }
253  weight = signed_value;
254  } else {
255  if (signed_value == 0) {
256  end_marker_seen = true;
257  break; // end of clause.
258  }
259  tmp_clause_.push_back(signed_value);
260  }
261  first = false;
262  }
263  if (!end_marker_seen) return;
264 
265  if (weight == hard_weight_) {
266  ++num_added_clauses_;
267  problem->AddConstraint(tmp_clause_);
268  } else {
269  if (tmp_clause_.size() == 1) {
270  // The max-sat formulation of an optimization sat problem with a
271  // linear objective introduces many singleton soft clauses. Because we
272  // natively work with a linear objective, we can just add the cost to
273  // the unique variable of such clause and remove the clause.
274  ++num_singleton_soft_clauses_;
275  const int literal = -tmp_clause_[0];
276  if (literal > 0) {
277  positive_literal_to_weight_[literal] += weight;
278  } else {
279  positive_literal_to_weight_[-literal] -= weight;
280  objective_offset_ += weight;
281  }
282  } else {
283  // The +1 is because a positive literal is the same as the 1-based
284  // variable index.
285  const int slack_literal = num_variables_ + num_slack_variables_ + 1;
286  ++num_slack_variables_;
287 
288  tmp_clause_.push_back(slack_literal);
289 
290  ++num_added_clauses_;
291  problem->AddConstraint(tmp_clause_);
292 
293  if (slack_literal > 0) {
294  positive_literal_to_weight_[slack_literal] += weight;
295  } else {
296  positive_literal_to_weight_[-slack_literal] -= weight;
297  objective_offset_ += weight;
298  }
299 
300  if (absl::GetFlag(FLAGS_wcnf_use_strong_slack)) {
301  // Add the binary implications slack_literal true => all the other
302  // clause literals are false.
303  for (int i = 0; i + 1 < tmp_clause_.size(); ++i) {
304  problem->AddConstraint({-slack_literal, -tmp_clause_[i]});
305  }
306  }
307  }
308  }
309  }
310 
311  bool interpret_cnf_as_max_sat_;
312 
313  int num_clauses_;
314  int num_variables_;
315 
316  // Temporary storage for ProcessNewLine().
317  std::vector<absl::string_view> words_;
318 
319  // We stores the objective in a map because we want the variables to appear
320  // only once in the LinearObjective proto.
321  absl::btree_map<int, int64_t> positive_literal_to_weight_;
322  int64_t objective_offset_;
323 
324  // Used for the wcnf format.
325  bool is_wcnf_;
326  // Some files have text after %. This indicates if we have seen the '%'.
327  bool end_marker_seen_;
328  int64_t hard_weight_;
329 
330  int num_slack_variables_;
331  int num_skipped_soft_clauses_;
332  int num_singleton_soft_clauses_;
333  int num_added_clauses_;
334 
335  std::vector<int> tmp_clause_;
336 
337  DISALLOW_COPY_AND_ASSIGN(SatCnfReader);
338 };
339 
340 } // namespace sat
341 } // namespace operations_research
342 
343 #endif // OR_TOOLS_SAT_SAT_CNF_READER_H_
bool Load(const std::string &filename, CpModelProto *problem)
bool Load(const std::string &filename, LinearBooleanProblem *problem)
int64_t value
Collection of objects used to extend the Constraint Solver library.
Literal literal
Definition: optimization.cc:88
int64_t weight
Definition: pack.cc:510
int line
Definition: parse_proto.cc:31
static int input(yyscan_t yyscanner)
ABSL_FLAG(bool, wcnf_use_strong_slack, true, "If true, when we add a slack variable to reify a soft clause, we " "enforce the fact that when it is true, the clause must be false.")
void AddObjectiveTerm(int literal, int64_t value)
void AddConstraint(absl::Span< const int > clause)
void AddObjectiveTerm(int literal, int64_t value)
void AddConstraint(absl::Span< const int > clause)