Google OR-Tools v9.14
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
sat_runner.cc
Go to the documentation of this file.
1// Copyright 2010-2025 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#include <cstdint>
15#include <cstdlib>
16#include <functional>
17#include <iostream>
18#include <limits>
19#include <memory>
20#include <string>
21#include <vector>
22
23#include "absl/base/thread_annotations.h"
24#include "absl/flags/flag.h"
25#include "absl/flags/parse.h"
26#include "absl/flags/usage.h"
27#include "absl/log/check.h"
28#include "absl/log/flags.h"
29#include "absl/log/initialize.h"
30#include "absl/log/log.h"
31#include "absl/strings/match.h"
32#include "absl/strings/str_format.h"
33#include "absl/strings/str_split.h"
34#include "absl/strings/string_view.h"
35#include "absl/synchronization/mutex.h"
36#include "absl/types/span.h"
37#include "google/protobuf/arena.h"
38#include "google/protobuf/text_format.h"
41#include "ortools/base/path.h"
45#include "ortools/sat/model.h"
52#include "ortools/util/sigint.h"
54
56 std::string, input, "",
57 "Required: input file of the problem to solve. Many format are supported:"
58 ".cnf (sat, max-sat, weighted max-sat), .opb (pseudo-boolean sat/optim) "
59 "and by default the CpModelProto proto (binary or text).");
60
62 std::string, hint_file, "",
63 "Protobuf file containing a CpModelResponse. The solution will be used as a"
64 " hint to bootstrap the search.");
65
66ABSL_FLAG(std::string, domain_file, "",
67 "Protobuf file containing a CpModelResponse. If present, the "
68 "tightened models will be used to reduce the domain of variables.");
69
70ABSL_FLAG(std::string, output, "",
71 "If non-empty, write the response there. By default it uses the "
72 "binary format except if the file extension is '.txt'.");
73
74ABSL_FLAG(std::string, params, "",
75 "Parameters for the sat solver in a text format of the "
76 "SatParameters proto, example: --params=use_conflicts:true.");
77
78ABSL_FLAG(bool, wcnf_use_strong_slack, true,
79 "If true, when we add a slack variable to reify a soft clause, we "
80 "enforce the fact that when it is true, the clause must be false.");
81ABSL_FLAG(bool, fingerprint_intermediate_solutions, false,
82 "Attach the fingerprint of intermediate solutions to the output.");
83ABSL_FLAG(bool, competition_mode, false,
84 "If true, output the log in a competition format.");
85ABSL_FLAG(bool, force_interleave_search, false,
86 "If true, enable interleaved workers when num_workers is 1.");
87
88namespace operations_research {
89namespace sat {
90namespace {
91
92void TryToRemoveSuffix(absl::string_view suffix, std::string* str) {
93 if (file::Extension(*str) == suffix) *str = file::Stem(*str);
94}
95
96std::string ExtractName(absl::string_view full_filename) {
97 std::string filename = std::string(file::Basename(full_filename));
98 // The order is important as '.pb.txt.gz' is a common suffix.
99 TryToRemoveSuffix("gz", &filename);
100 TryToRemoveSuffix("txt", &filename);
101 TryToRemoveSuffix("pb", &filename);
102 TryToRemoveSuffix("pbtxt", &filename);
103 TryToRemoveSuffix("proto", &filename);
104 TryToRemoveSuffix("prototxt", &filename);
105 TryToRemoveSuffix("textproto", &filename);
106 TryToRemoveSuffix("bin", &filename);
107 return filename;
108}
109
110class LastSolutionPrinter {
111 public:
112 // Note that is prints the solution in the PB competition format.
113 void MaybePrintLastSolution() {
114 absl::MutexLock lock(&mutex_);
115 if (last_solution_printed_) return;
116 last_solution_printed_ = true;
117
118 if (last_solution_.empty()) {
119 std::cout << "s UNKNOWN" << std::endl;
120 } else {
121 std::cout << "s SATISFIABLE" << std::endl;
122 std::string line;
123 for (int i = 0; i < num_variables_; ++i) {
124 if (last_solution_[i]) {
125 absl::StrAppend(&line, "x", i + 1, " ");
126 } else {
127 absl::StrAppend(&line, "-x", i + 1, " ");
128 }
129 if (line.size() >= 75) {
130 std::cout << "v " << line << std::endl;
131 line.clear();
132 }
133 }
134 if (!line.empty()) {
135 std::cout << "v " << line << std::endl;
136 }
137 }
138 }
139
140 void set_num_variables(int num_variables) { num_variables_ = num_variables; }
141
142 void set_last_solution(absl::Span<const int64_t> solution) {
143 absl::MutexLock lock(&mutex_);
144 if (last_solution_printed_) return;
145 last_solution_.assign(solution.begin(), solution.end());
146 }
147
148 // Returns false if the solution has already been printed, else mark it as
149 // printed by caller code.
150 bool mark_last_solution_printed() {
151 const absl::MutexLock lock(&mutex_);
152 if (last_solution_printed_) {
153 return false;
154 }
155 last_solution_printed_ = true;
156 return true;
157 }
158
159 private:
160 int num_variables_ = 0;
161 std::vector<int64_t> last_solution_ ABSL_GUARDED_BY(mutex_);
162 bool last_solution_printed_ ABSL_GUARDED_BY(mutex_) = false;
163 absl::Mutex mutex_;
164};
165
166void LogInPbCompetitionFormat(
167 int num_variables, bool has_objective, Model* model,
168 SatParameters* parameters,
169 std::shared_ptr<LastSolutionPrinter> last_solution_printer) {
170 CHECK(last_solution_printer != nullptr);
171 last_solution_printer->set_num_variables(num_variables);
172
173 const auto log_callback = [](const std::string& multi_line_input) {
174 if (multi_line_input.empty()) {
175 std::cout << "c" << std::endl;
176 return;
177 }
178 const std::vector<absl::string_view> lines =
179 absl::StrSplit(multi_line_input, '\n');
180 for (const absl::string_view& line : lines) {
181 std::cout << "c " << line << std::endl;
182 }
183 };
184 model->GetOrCreate<SolverLogger>()->AddInfoLoggingCallback(log_callback);
185 parameters->set_log_to_stdout(false);
186
187 const auto response_callback = [last_solution_printer](
188 const CpSolverResponse& r) {
189 std::cout << "o " << static_cast<int64_t>(r.objective_value()) << std::endl;
190 last_solution_printer->set_last_solution(r.solution());
191 };
192 model->Add(NewFeasibleSolutionObserver(response_callback));
193
194 const auto final_response_callback =
195 [num_variables, has_objective,
196 last_solution_printer](CpSolverResponse* r) {
197 if (!last_solution_printer->mark_last_solution_printed()) return;
198
199 switch (r->status()) {
201 if (has_objective) {
202 std::cout << "s OPTIMUM FOUND " << std::endl;
203 } else {
204 std::cout << "s SATISFIABLE" << std::endl;
205 }
206 break;
208 std::cout << "s SATISFIABLE" << std::endl;
209 break;
211 std::cout << "s UNSATISFIABLE" << std::endl;
212 break;
214 std::cout << "s UNSUPPORTED" << std::endl;
215 break;
217 std::cout << "s UNKNOWN" << std::endl;
218 break;
219 default:
220 break;
221 }
222 if (r->status() == CpSolverStatus::OPTIMAL ||
223 r->status() == CpSolverStatus::FEASIBLE) {
224 std::string line;
225 for (int i = 0; i < num_variables; ++i) {
226 if (r->solution(i)) {
227 absl::StrAppend(&line, "x", i + 1, " ");
228 } else {
229 absl::StrAppend(&line, "-x", i + 1, " ");
230 }
231 if (line.size() >= 75) {
232 std::cout << "v " << line << std::endl;
233 line.clear();
234 }
235 }
236 if (!line.empty()) {
237 std::cout << "v " << line << std::endl;
238 }
239 }
240 };
241 model->GetOrCreate<SharedResponseManager>()->AddFinalResponsePostprocessor(
242 final_response_callback);
243}
244
245void SetInterleavedWorkers(SatParameters* parameters) {
246 // Enable interleaved workers when num_workers is 1.
247 if (parameters->num_workers() == 1) {
248 parameters->set_interleave_search(true);
249 parameters->set_use_rins_lns(false);
250 parameters->add_subsolvers("default_lp");
251 parameters->add_subsolvers("max_lp");
252 parameters->add_subsolvers("quick_restart");
253 parameters->add_subsolvers("core_or_no_lp"); // no_lp if no objective.
254 parameters->set_num_violation_ls(1); // Off if no objective.
255 }
256}
257
258bool LoadProblem(const std::string& filename, absl::string_view hint_file,
259 absl::string_view domain_file, CpModelProto* cp_model,
260 Model* model, SatParameters* parameters,
261 std::shared_ptr<LastSolutionPrinter> last_solution_printer) {
262 if (absl::EndsWith(filename, ".opb") ||
263 absl::EndsWith(filename, ".opb.bz2") ||
264 absl::EndsWith(filename, ".opb.gz") || absl::EndsWith(filename, ".wbo") ||
265 absl::EndsWith(filename, ".wbo.bz2") ||
266 absl::EndsWith(filename, ".wbo.gz")) {
267 OpbReader reader;
268 if (!reader.LoadAndValidate(filename, cp_model)) {
269 if (!reader.model_is_supported()) { // Some constants are too large.
270 if (absl::GetFlag(FLAGS_competition_mode)) {
271 // We output the official UNSUPPORTED status.
272 std::cout << "s UNSUPPORTED" << std::endl;
273 return false; // Bypass the solve part.
274 } else {
275 // Create a dummy model with a single variable that overflows.
276 // This way, the solver will return MODEL_INVALID instead of
277 // crashing.
278 IntegerVariableProto* var = cp_model->add_variables();
279 var->add_domain(std::numeric_limits<int64_t>::min());
280 var->add_domain(std::numeric_limits<int64_t>::max());
281 return true; // Will still call solve() to get the status.
282 }
283 } else {
284 return false; // Bypass the solve part.
285 }
286 }
287
288 if (absl::GetFlag(FLAGS_competition_mode)) {
289 const int num_variables =
290 reader.model_is_supported() ? reader.num_variables() : 1;
291 LogInPbCompetitionFormat(num_variables, cp_model->has_objective(), model,
292 parameters, last_solution_printer);
293 }
294 if (absl::GetFlag(FLAGS_force_interleave_search)) {
295 SetInterleavedWorkers(parameters);
296 }
297 if (parameters->num_workers() >= 2 && parameters->num_workers() <= 15) {
298 // Works better without symmetries in search
299 // TODO(user): Investigate.
300 parameters->add_ignore_subsolvers("max_lp_sym");
301 parameters->add_extra_subsolvers("max_lp");
302 }
303 } else if (absl::EndsWith(filename, ".cnf") ||
304 absl::EndsWith(filename, ".cnf.xz") ||
305 absl::EndsWith(filename, ".cnf.gz") ||
306 absl::EndsWith(filename, ".wcnf") ||
307 absl::EndsWith(filename, ".wcnf.xz") ||
308 absl::EndsWith(filename, ".wcnf.gz")) {
309 SatCnfReader reader(absl::GetFlag(FLAGS_wcnf_use_strong_slack));
310 if (!reader.Load(filename, cp_model)) {
311 LOG(FATAL) << "Cannot load file '" << filename << "'.";
312 }
313 } else {
314 CHECK_OK(ReadFileToProto(filename, cp_model));
315 }
316
317 // Read the hint file.
318 if (!hint_file.empty()) {
319 CpSolverResponse response;
320 CHECK_OK(ReadFileToProto(hint_file, &response));
321 if (!response.solution().empty()) {
322 CHECK_EQ(response.solution_size(), cp_model->variables_size())
323 << "The hint from the response proto is not compatible with the "
324 "model proto";
325
326 cp_model->clear_solution_hint();
327 for (int i = 0; i < cp_model->variables_size(); ++i) {
328 cp_model->mutable_solution_hint()->add_vars(i);
329 cp_model->mutable_solution_hint()->add_values(response.solution(i));
330 }
331 } else {
332 LOG(INFO) << "The response proto has no solutions, ignoring.";
333 }
334 }
335
336 // Read the tightened domain file.
337 if (!domain_file.empty()) {
338 CpSolverResponse response;
339 CHECK_OK(ReadFileToProto(domain_file, &response));
340 if (!response.tightened_variables().empty()) {
341 CHECK_EQ(response.tightened_variables_size(), cp_model->variables_size())
342 << "The tighened variables from the response proto is not "
343 "compatible with the model proto";
344
345 for (int i = 0; i < cp_model->variables_size(); ++i) {
346 IntegerVariableProto* var_proto = cp_model->mutable_variables(i);
347 const Domain tightened_domain =
348 ReadDomainFromProto(response.tightened_variables(i));
349 const Domain new_domain =
350 ReadDomainFromProto(*var_proto).IntersectionWith(tightened_domain);
351 FillDomainInProto(new_domain, var_proto);
352 }
353 } else {
354 LOG(INFO) << "The response proto has no tightened variable domains, "
355 "ignoring.";
356 }
357 }
358
359 // Set the name if not present.
360 if (cp_model->name().empty()) {
361 cp_model->set_name(ExtractName(filename));
362 }
363 return true;
364}
365
366int Run() {
367 Model model;
368 SatParameters parameters;
369 if (absl::GetFlag(FLAGS_input).empty()) {
370 LOG(FATAL) << "Please supply a data file with --input=";
371 }
372
373 // Parse the --params flag.
374 parameters.set_log_search_progress(true);
375 if (!absl::GetFlag(FLAGS_params).empty()) {
376 CHECK(google::protobuf::TextFormat::MergeFromString(
377 absl::GetFlag(FLAGS_params), &parameters))
378 << absl::GetFlag(FLAGS_params);
379 }
380
381 // Read the problem.
382 google::protobuf::Arena arena;
383 CpModelProto* cp_model =
384 google::protobuf::Arena::Create<CpModelProto>(&arena);
385 std::shared_ptr<LastSolutionPrinter> last_solution_printer;
386 if (absl::GetFlag(FLAGS_competition_mode)) {
387 last_solution_printer = std::make_shared<LastSolutionPrinter>();
388 }
389 if (!LoadProblem(absl::GetFlag(FLAGS_input), absl::GetFlag(FLAGS_hint_file),
390 absl::GetFlag(FLAGS_domain_file), cp_model, &model,
391 &parameters, last_solution_printer)) {
392 if (!absl::GetFlag(FLAGS_competition_mode)) {
393 LOG(FATAL) << "Cannot load file '" << absl::GetFlag(FLAGS_input) << "'.";
394 }
395 return EXIT_SUCCESS;
396 }
397
398 model.Add(NewSatParameters(parameters));
399 if (absl::GetFlag(FLAGS_fingerprint_intermediate_solutions)) {
400 // Let's add a solution callback that will display the fingerprint of all
401 // solutions.
402 model.Add(NewFeasibleSolutionLogCallback([](const CpSolverResponse& r) {
403 return absl::StrFormat(
404 "fingerprint: %#x",
406 }));
407 }
408
409 if (absl::GetFlag(FLAGS_competition_mode)) {
410 model.GetOrCreate<SigtermHandler>()->Register([last_solution_printer]() {
411 last_solution_printer->MaybePrintLastSolution();
412 exit(EXIT_SUCCESS);
413 });
414 }
415
416 const CpSolverResponse response = SolveCpModel(*cp_model, &model);
417
418 if (!absl::GetFlag(FLAGS_output).empty()) {
419 if (absl::EndsWith(absl::GetFlag(FLAGS_output), "txt")) {
420 CHECK_OK(file::SetTextProto(absl::GetFlag(FLAGS_output), response,
421 file::Defaults()));
422 } else {
423 CHECK_OK(file::SetBinaryProto(absl::GetFlag(FLAGS_output), response,
424 file::Defaults()));
425 }
426 }
427
428 // The SAT competition requires a particular exit code and since we don't
429 // really use it for any other purpose, we comply.
430 if (response.status() == CpSolverStatus::OPTIMAL) return 10;
431 if (response.status() == CpSolverStatus::FEASIBLE) return 10;
432 if (response.status() == CpSolverStatus::INFEASIBLE) return 20;
433 return EXIT_SUCCESS;
434}
435
436} // namespace
437} // namespace sat
438} // namespace operations_research
439
440static const char kUsage[] =
441 "Usage: see flags.\n"
442 "This program solves a given problem with the CP-SAT solver.";
443
444int main(int argc, char** argv) {
445 absl::InitializeLog();
446 absl::SetProgramUsageMessage(kUsage);
447 absl::ParseCommandLine(argc, argv);
448 return operations_research::sat::Run();
449}
Domain IntersectionWith(const Domain &domain) const
absl::Status SetBinaryProto(absl::string_view file_name, const google::protobuf::Message &proto, Options options)
Definition file.cc:475
absl::string_view Extension(absl::string_view path)
Definition path.cc:133
absl::Status SetTextProto(absl::string_view file_name, const google::protobuf::Message &proto, Options options)
Definition file.cc:447
absl::string_view Stem(absl::string_view path)
Definition path.cc:129
absl::string_view Basename(absl::string_view path)
Definition path.cc:109
Options Defaults()
Definition file.h:85
constexpr uint64_t kDefaultFingerprintSeed
Default seed for fingerprints.
uint64_t FingerprintRepeatedField(const google::protobuf::RepeatedField< T > &sequence, uint64_t seed)
std::function< SatParameters(Model *)> NewSatParameters(absl::string_view params)
void FillDomainInProto(const Domain &domain, ProtoWithDomain *proto)
Serializes a Domain into the domain field of a proto.
CpSolverResponse SolveCpModel(const CpModelProto &model_proto, Model *model)
std::function< void(Model *)> NewFeasibleSolutionLogCallback(const std::function< std::string(const CpSolverResponse &response)> &callback)
Domain ReadDomainFromProto(const ProtoWithDomain &proto)
Reads a Domain from the domain field of a proto.
std::function< void(Model *)> NewFeasibleSolutionObserver(const std::function< void(const CpSolverResponse &response)> &callback)
In SWIG mode, we don't want anything besides these top-level includes.
Select next search node to expand Select next item_i to add this new search node to the search Generate a new search node where item_i is not in the knapsack Check validity of this new partial solution(using propagators) - If valid
absl::Status ReadFileToProto(absl::string_view filename, google::protobuf::Message *proto, bool allow_partial)
Definition file_util.cc:54
static int input(yyscan_t yyscanner)
int main(int argc, char **argv)
ABSL_FLAG(std::string, input, "", "Required: input file of the problem to solve. Many format are supported:" ".cnf (sat, max-sat, weighted max-sat), .opb (pseudo-boolean sat/optim) " "and by default the CpModelProto proto (binary or text).")
static const char kUsage[]