Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
sat_cnf_reader.h
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
14#ifndef OR_TOOLS_SAT_SAT_CNF_READER_H_
15#define OR_TOOLS_SAT_SAT_CNF_READER_H_
16
17#include <algorithm>
18#include <cstdint>
19#include <string>
20#include <utility>
21#include <vector>
22
23#include "absl/container/btree_map.h"
24#include "absl/log/check.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"
30#include "ortools/sat/boolean_problem.pb.h"
31#include "ortools/sat/cp_model.pb.h"
33
34namespace operations_research {
35namespace sat {
36
37// This implement the implicit contract needed by the SatCnfReader class.
39 public:
40 explicit LinearBooleanProblemWrapper(LinearBooleanProblem* p) : problem_(p) {}
41
42 // In the new 2022 .wcnf format, we don't know the number of variable before
43 // hand (no header). So when this is called (after all the constraint have
44 // been added), we need to re-index the slack so that they are after the
45 // variable of the original problem.
46 void SetSizeAndPostprocess(int num_variables, int num_slacks) {
47 problem_->set_num_variables(num_variables + num_slacks);
48 problem_->set_original_num_variables(num_variables);
49 for (const int c : to_postprocess_) {
50 auto* literals = problem_->mutable_constraints(c)->mutable_literals();
51 const int last_index = literals->size() - 1;
52 const int last = (*literals)[last_index];
53 (*literals)[last_index] =
54 last >= 0 ? last + num_variables : last - num_variables;
55 }
56 }
57
58 // If last_is_slack is true, then the last literal is assumed to be a slack
59 // with index in [-num_slacks, num_slacks]. We will re-index it at the end in
60 // SetSizeAndPostprocess().
61 void AddConstraint(absl::Span<const int> clause, bool last_is_slack = false) {
62 if (last_is_slack)
63 to_postprocess_.push_back(problem_->constraints().size());
64 LinearBooleanConstraint* constraint = problem_->add_constraints();
65 constraint->mutable_literals()->Reserve(clause.size());
66 constraint->mutable_coefficients()->Reserve(clause.size());
67 constraint->set_lower_bound(1);
68 for (const int literal : clause) {
69 constraint->add_literals(literal);
70 constraint->add_coefficients(1);
71 }
72 }
73
74 void AddObjectiveTerm(int literal, int64_t value) {
75 CHECK_GE(literal, 0) << "Negative literal not supported.";
76 problem_->mutable_objective()->add_literals(literal);
77 problem_->mutable_objective()->add_coefficients(value);
78 }
79
80 void SetObjectiveOffset(int64_t offset) {
81 problem_->mutable_objective()->set_offset(offset);
82 }
83
84 private:
85 LinearBooleanProblem* problem_;
86 std::vector<int> to_postprocess_;
87};
88
89// This implement the implicit contract needed by the SatCnfReader class.
91 public:
92 explicit CpModelProtoWrapper(CpModelProto* p) : problem_(p) {}
93
94 void SetSizeAndPostprocess(int num_variables, int num_slacks) {
95 for (int i = 0; i < num_variables + num_slacks; ++i) {
96 IntegerVariableProto* variable = problem_->add_variables();
97 variable->add_domain(0);
98 variable->add_domain(1);
99 }
100 for (const int c : to_postprocess_) {
101 auto* literals = problem_->mutable_constraints(c)
102 ->mutable_bool_or()
103 ->mutable_literals();
104 const int last_index = literals->size() - 1;
105 const int last = (*literals)[last_index];
106 (*literals)[last_index] =
107 last >= 0 ? last + num_variables : last - num_variables;
108 }
109 }
110
111 int LiteralToRef(int signed_value) {
112 return signed_value > 0 ? signed_value - 1 : signed_value;
113 }
114
115 void AddConstraint(absl::Span<const int> clause, bool last_is_slack = false) {
116 if (last_is_slack)
117 to_postprocess_.push_back(problem_->constraints().size());
118 auto* constraint = problem_->add_constraints()->mutable_bool_or();
119 constraint->mutable_literals()->Reserve(clause.size());
120 for (const int literal : clause) {
121 constraint->add_literals(LiteralToRef(literal));
122 }
123 }
124
125 void AddObjectiveTerm(int literal, int64_t value) {
126 CHECK_GE(literal, 0) << "Negative literal not supported.";
127 problem_->mutable_objective()->add_vars(LiteralToRef(literal));
128 problem_->mutable_objective()->add_coeffs(value);
129 }
130
131 void SetObjectiveOffset(int64_t offset) {
132 problem_->mutable_objective()->set_offset(offset);
133 }
134
135 private:
136 CpModelProto* problem_;
137 std::vector<int> to_postprocess_;
138};
139
140// This class loads a file in cnf file format into a SatProblem.
141// The format is described here:
142// http://people.sc.fsu.edu/~jburkardt/data/cnf/cnf.html
143//
144// It also support the wcnf input format for partial weighted max-sat problems.
146 public:
147 explicit SatCnfReader(bool wcnf_use_strong_slack = true)
148 : interpret_cnf_as_max_sat_(false),
149 wcnf_use_strong_slack_(wcnf_use_strong_slack) {}
150
151 // This type is neither copyable nor movable.
152 SatCnfReader(const SatCnfReader&) = delete;
154
155 // If called with true, then a cnf file will be converted to the max-sat
156 // problem: Try to minimize the number of unsatisfiable clauses.
157 void InterpretCnfAsMaxSat(bool v) { interpret_cnf_as_max_sat_ = v; }
158
159 // Loads the given cnf filename into the given proto.
160 bool Load(const std::string& filename, LinearBooleanProblem* problem) {
161 problem->Clear();
162 problem->set_name(ExtractProblemName(filename));
163 LinearBooleanProblemWrapper wrapper(problem);
164 return LoadInternal(filename, &wrapper);
165 }
166 bool Load(const std::string& filename, CpModelProto* problem) {
167 problem->Clear();
168 problem->set_name(ExtractProblemName(filename));
169 CpModelProtoWrapper wrapper(problem);
170 return LoadInternal(filename, &wrapper);
171 }
172
173 private:
174 template <class Problem>
175 bool LoadInternal(const std::string& filename, Problem* problem) {
176 is_wcnf_ = false;
177 objective_offset_ = 0;
178 positive_literal_to_weight_.clear();
179
180 end_marker_seen_ = false;
181 hard_weight_ = 0;
182 num_skipped_soft_clauses_ = 0;
183 num_singleton_soft_clauses_ = 0;
184 num_added_clauses_ = 0;
185 num_slack_variables_ = 0;
186
187 num_variables_ = 0;
188 num_clauses_ = 0;
189 actual_num_variables_ = 0;
190
191 int num_lines = 0;
192 for (const std::string& line : FileLines(filename)) {
193 ++num_lines;
194 ProcessNewLine(line, problem);
195 }
196 if (num_lines == 0) {
197 LOG(FATAL) << "File '" << filename << "' is empty or can't be read.";
198 }
199
200 if (num_variables_ > 0 && num_variables_ != actual_num_variables_) {
201 LOG(ERROR) << "Wrong number of variables ! Expected:" << num_variables_
202 << " Seen:" << actual_num_variables_;
203 }
204
205 problem->SetSizeAndPostprocess(actual_num_variables_, num_slack_variables_);
206
207 // Fill the objective.
208 if (!positive_literal_to_weight_.empty() ||
209 !slack_literal_to_weight_.empty()) {
210 for (const std::pair<int, int64_t> p : positive_literal_to_weight_) {
211 if (p.second != 0) {
212 problem->AddObjectiveTerm(p.first, p.second);
213 }
214 }
215 for (const std::pair<int, int64_t> p : slack_literal_to_weight_) {
216 if (p.second != 0) {
217 problem->AddObjectiveTerm(actual_num_variables_ + p.first, p.second);
218 }
219 }
220 problem->SetObjectiveOffset(objective_offset_);
221 }
222
223 // Some file from the max-sat competition seems to have the wrong number of
224 // clause !? I checked manually, so still parse them with best effort.
225 const int total_seen = num_added_clauses_ + num_singleton_soft_clauses_ +
226 num_skipped_soft_clauses_;
227 if (num_clauses_ > 0 && num_clauses_ != total_seen) {
228 LOG(ERROR) << "Wrong number of clauses ! Expected:" << num_clauses_
229 << " Seen:" << total_seen;
230 }
231 return true;
232 }
233
234 // Since the problem name is not stored in the cnf format, we infer it from
235 // the file name.
236 static std::string ExtractProblemName(const std::string& filename) {
237 const int found = filename.find_last_of('/');
238 const std::string problem_name =
239 found != std::string::npos ? filename.substr(found + 1) : filename;
240 return problem_name;
241 }
242
243 void ProcessHeader(const std::string& line) {
244 static const char kWordDelimiters[] = " ";
245 words_ = absl::StrSplit(line, kWordDelimiters, absl::SkipEmpty());
246
247 CHECK_EQ(words_[0], "p");
248 if (words_[1] == "cnf" || words_[1] == "wcnf") {
249 CHECK(absl::SimpleAtoi(words_[2], &num_variables_));
250 CHECK(absl::SimpleAtoi(words_[3], &num_clauses_));
251 if (words_[1] == "wcnf") {
252 is_wcnf_ = true;
253 hard_weight_ = 0;
254 if (words_.size() > 4) {
255 CHECK(absl::SimpleAtoi(words_[4], &hard_weight_));
256 }
257 }
258 } else {
259 LOG(FATAL) << "Unknown file type: " << words_[1];
260 }
261 }
262
263 template <class Problem>
264 void ProcessNewLine(const std::string& line, Problem* problem) {
265 if (line.empty() || end_marker_seen_) return;
266 if (line[0] == 'c') return;
267 if (line[0] == '%') {
268 end_marker_seen_ = true;
269 return;
270 }
271 if (line[0] == 'p') {
272 ProcessHeader(line);
273 return;
274 }
275
276 // The new wcnf format do not have header p line anymore.
277 if (num_variables_ == 0) {
278 is_wcnf_ = true;
279 }
280
281 static const char kWordDelimiters[] = " ";
282 auto splitter = absl::StrSplit(line, kWordDelimiters, absl::SkipEmpty());
283
284 tmp_clause_.clear();
285 int64_t weight =
286 (!is_wcnf_ && interpret_cnf_as_max_sat_) ? 1 : hard_weight_;
287 bool first = true;
288 bool end_marker_seen = false;
289 for (const absl::string_view word : splitter) {
290 if (first && is_wcnf_) {
291 first = false;
292 if (word == "h") {
293 // Hard clause in the new 2022 format.
294 // Note that hard_weight_ == 0 here.
295 weight = hard_weight_;
296 } else {
297 CHECK(absl::SimpleAtoi(word, &weight));
298 CHECK_GE(weight, 0);
299
300 // A soft clause of weight 0 can be removed.
301 if (weight == 0) {
302 ++num_skipped_soft_clauses_;
303 return;
304 }
305 }
306 continue;
307 }
308
309 int signed_value;
310 CHECK(absl::SimpleAtoi(word, &signed_value));
311 if (signed_value == 0) {
312 end_marker_seen = true;
313 break; // end of clause.
314 }
315
316 actual_num_variables_ = std::max(actual_num_variables_,
317 std::max(signed_value, -signed_value));
318 tmp_clause_.push_back(signed_value);
319 }
320 if (!end_marker_seen) return;
321
322 if (weight == hard_weight_) {
323 ++num_added_clauses_;
324 problem->AddConstraint(tmp_clause_);
325 } else {
326 if (tmp_clause_.size() == 1) {
327 // The max-sat formulation of an optimization sat problem with a
328 // linear objective introduces many singleton soft clauses. Because we
329 // natively work with a linear objective, we can just add the cost to
330 // the unique variable of such clause and remove the clause.
331 ++num_singleton_soft_clauses_;
332 const int literal = -tmp_clause_[0];
333 if (literal > 0) {
334 positive_literal_to_weight_[literal] += weight;
335 } else {
336 positive_literal_to_weight_[-literal] -= weight;
337 objective_offset_ += weight;
338 }
339 } else {
340 // The +1 is because a positive literal is the same as the 1-based
341 // variable index.
342 const int slack_literal = ++num_slack_variables_;
343
344 slack_literal_to_weight_[slack_literal] += weight;
345 tmp_clause_.push_back(slack_literal);
346
347 ++num_added_clauses_;
348 problem->AddConstraint(tmp_clause_, /*last_is_slack=*/true);
349
350 if (wcnf_use_strong_slack_) {
351 // Add the binary implications slack_literal true => all the other
352 // clause literals are false.
353 for (int i = 0; i + 1 < tmp_clause_.size(); ++i) {
354 problem->AddConstraint({-tmp_clause_[i], -slack_literal},
355 /*last_is_slack=*/true);
356 }
357 }
358 }
359 }
360 }
361
362 bool interpret_cnf_as_max_sat_;
363 const bool wcnf_use_strong_slack_;
364
365 int num_clauses_ = 0;
366 int num_variables_ = 0;
367 int actual_num_variables_ = 0;
368
369 // Temporary storage for ProcessNewLine().
370 std::vector<absl::string_view> words_;
371
372 // We stores the objective in a map because we want the variables to appear
373 // only once in the LinearObjective proto.
374 int64_t objective_offset_;
375 absl::btree_map<int, int64_t> positive_literal_to_weight_;
376 absl::btree_map<int, int64_t> slack_literal_to_weight_;
377
378 // Used for the wcnf format.
379 bool is_wcnf_;
380 // Some files have text after %. This indicates if we have seen the '%'.
381 bool end_marker_seen_;
382 int64_t hard_weight_ = 0;
383
384 int num_slack_variables_;
385 int num_skipped_soft_clauses_;
386 int num_singleton_soft_clauses_;
387 int num_added_clauses_;
388
389 std::vector<int> tmp_clause_;
390};
391
392} // namespace sat
393} // namespace operations_research
394
395#endif // OR_TOOLS_SAT_SAT_CNF_READER_H_
This implement the implicit contract needed by the SatCnfReader class.
void SetSizeAndPostprocess(int num_variables, int num_slacks)
void AddConstraint(absl::Span< const int > clause, bool last_is_slack=false)
void AddObjectiveTerm(int literal, int64_t value)
This implement the implicit contract needed by the SatCnfReader class.
void SetSizeAndPostprocess(int num_variables, int num_slacks)
void AddObjectiveTerm(int literal, int64_t value)
void AddConstraint(absl::Span< const int > clause, bool last_is_slack=false)
bool Load(const std::string &filename, CpModelProto *problem)
SatCnfReader(const SatCnfReader &)=delete
This type is neither copyable nor movable.
bool Load(const std::string &filename, LinearBooleanProblem *problem)
Loads the given cnf filename into the given proto.
SatCnfReader & operator=(const SatCnfReader &)=delete
SatCnfReader(bool wcnf_use_strong_slack=true)
int64_t value
int literal
In SWIG mode, we don't want anything besides these top-level includes.
int64_t weight
Definition pack.cc:510
int line