OR-Tools  9.6
linear_solver/solve.cc
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 // Command line interface to the MPSolver class.
15 // See linear_solver.h and kUsageStr below.
16 //
17 // Examples.
18 //
19 // 1. To run SCIP for 90 seconds, dumping available information use:
20 //
21 // solve --solver=scip \
22 // --time_limit=90s \
23 // --stderrthreshold=0 \
24 // --linear_solver_enable_verbose_output \
25 // --input=/tmp/foo.mps \
26 // --dump_model=/tmp/foo.model \
27 // --dump_request=/tmp/foo.request \
28 // --dump_response=/tmp/foo.response \
29 // >/tmp/foo.out 2>/tmp/foo.err
30 //
31 // 2. To run CP_SAT for 10 minutes with 8 workers, you can use
32 // CP-SAT parameters:
33 //
34 // solve --solver=sat \
35 // --params="max_time_in_seconds:600, num_search_workers:8"
36 // --stderrthreshold=0 \
37 // --input=/tmp/foo.mps \
38 // 2>/tmp/foo.err
39 //
40 // or use the solve binary flags:
41 //
42 // solve --solver=sat \
43 // --time_limit=10m \
44 // --num_threads=8 \
45 // --stderrthreshold=0 \
46 // --input=/tmp/foo.mps \
47 // --dump_model=/tmp/foo.model \
48 // --dump_request=/tmp/foo.request \
49 // --dump_response=/tmp/foo.response \
50 // 2>/tmp/foo.err
51 
52 #include <algorithm>
53 #include <cstdio>
54 #include <string>
55 #include <utility>
56 #include <vector>
57 
58 #include "absl/flags/flag.h"
59 #include "absl/status/status.h"
60 #include "absl/status/statusor.h"
61 #include "absl/strings/match.h"
62 #include "absl/strings/str_format.h"
63 #include "absl/time/time.h"
65 #include "ortools/base/file.h"
66 #include "ortools/base/helpers.h"
69 #include "ortools/base/logging.h"
70 #include "ortools/base/options.h"
72 #include "ortools/linear_solver/linear_solver.pb.h"
76 #include "ortools/sat/cp_model.pb.h"
78 #include "ortools/util/file_util.h"
79 #include "ortools/util/sigint.h"
80 
81 ABSL_FLAG(std::string, input, "", "REQUIRED: Input file name.");
82 ABSL_FLAG(std::string, sol_hint, "",
83  "Input file name with solution in .sol format.");
84 ABSL_FLAG(std::string, solver, "glop",
85  "The solver to use: bop, cbc, clp, glop, glpk_lp, glpk_mip, "
86  "gurobi_lp, gurobi_mip, pdlp, scip, knapsack, sat.");
87 ABSL_FLAG(int, num_threads, 1,
88  "Number of threads to use by the underlying solver.");
89 ABSL_FLAG(std::string, params_file, "",
90  "Solver specific parameters file. "
91  "If this flag is set, the --params flag is ignored.");
92 ABSL_FLAG(std::string, params, "", "Solver specific parameters");
93 ABSL_FLAG(absl::Duration, time_limit, absl::InfiniteDuration(),
94  "It specifies a limit on the solving time. The duration must be must "
95  "be positive. It default to an infinite duration meaning that no "
96  "time limit will be imposed.");
97 ABSL_FLAG(std::string, output_csv, "",
98  "If non-empty, write the returned solution in csv format with "
99  "each line formed by a variable name and its value.");
100 
101 ABSL_FLAG(std::string, dump_format, "text",
102  "Format in which to dump protos (if flags --dump_model, "
103  "--dump_request, or --dump_response are used). Possible values: "
104  "'text', 'binary', 'json' which correspond to text proto format "
105  "binary proto format, and json. If 'binary' or 'json' are used, "
106  "we append '.bin' and '.json' to file names.");
107 ABSL_FLAG(bool, dump_gzip, false,
108  "Whether to gzip dumped protos. Appends .gz to their name.");
109 ABSL_FLAG(std::string, dump_model, "",
110  "If non-empty, dumps MPModelProto there.");
111 ABSL_FLAG(std::string, dump_request, "",
112  "If non-empty, dumps MPModelRequest there.");
113 ABSL_FLAG(std::string, dump_response, "",
114  "If non-empty, dumps MPSolutionResponse there.");
115 ABSL_FLAG(std::string, sol_file, "",
116  "If non-empty, output the best solution in Miplib .sol format.");
117 
118 ABSL_DECLARE_FLAG(bool, verify_solution); // Defined in ./linear_solver.cc
120  bool,
121  linear_solver_enable_verbose_output); // Defined in ./linear_solver.cc
122 
123 static const char kUsageStr[] =
124  "Run MPSolver on the given input file. Many formats are supported: \n"
125  " - a .mps or .mps.gz file,\n"
126  " - an MPModelProto (binary or text, possibly gzipped),\n"
127  " - an MPModelRequest (binary or text, possibly gzipped).";
128 
129 namespace operations_research {
130 namespace {
131 
132 MPModelRequest ReadMipModel(const std::string& input) {
133  MPModelRequest request_proto;
134  MPModelProto model_proto;
135  if (absl::EndsWith(input, ".lp")) {
136  std::string data;
137  CHECK_OK(file::GetContents(input, &data, file::Defaults()));
138  absl::StatusOr<MPModelProto> result = ModelProtoFromLpFormat(data);
139  CHECK_OK(result);
140  model_proto = std::move(result).value();
141  } else if (absl::EndsWith(input, ".mps") ||
142  absl::EndsWith(input, ".mps.gz")) {
143  QCHECK_OK(glop::MPSReader().ParseFile(input, &model_proto))
144  << "Error while parsing the mps file '" << input << "'.";
145  } else {
147  ReadFileToProto(input, &request_proto);
148  }
149  // If the input is a proto in binary format, both ReadFileToProto could
150  // return true. Instead use the actual number of variables found to test the
151  // correct format of the input.
152  const bool is_model_proto = model_proto.variable_size() > 0;
153  const bool is_request_proto =
154  request_proto.model().variable_size() > 0 ||
155  !request_proto.model_delta().baseline_model_file_path().empty();
156  if (!is_model_proto && !is_request_proto) {
157  LOG(FATAL) << "Failed to parse '" << input
158  << "' as an MPModelProto or an MPModelRequest.";
159  } else {
160  CHECK(!(is_model_proto && is_request_proto));
161  }
162  if (is_request_proto) {
163  LOG(INFO) << "Read input proto as an MPModelRequest.";
164  } else {
165  LOG(INFO) << "Read input proto as an MPModelProto.";
166  model_proto.Swap(request_proto.mutable_model());
167  }
168  return request_proto;
169 }
170 
171 MPSolutionResponse LocalSolve(const MPModelRequest& request_proto) {
172  // TODO(or-core-team): Why doesn't this use MPSolver::SolveWithProto() ?
173 
174  // Create the solver, we use the name of the model as the solver name.
175  MPSolver solver(request_proto.model().name(),
177  request_proto.solver_type()));
178  const absl::Status set_num_threads_status =
179  solver.SetNumThreads(absl::GetFlag(FLAGS_num_threads));
180  if (set_num_threads_status.ok()) {
181  LOG(INFO) << "Set number of threads to " << absl::GetFlag(FLAGS_num_threads)
182  << ".";
183  } else if (absl::GetFlag(FLAGS_num_threads) != 1) {
184  LOG(ERROR) << "Failed to set number of threads due to: "
185  << set_num_threads_status.message() << ". Using 1 as default.";
186  }
187  solver.EnableOutput();
188 
189  if (request_proto.has_solver_specific_parameters()) {
190  CHECK(solver.SetSolverSpecificParametersAsString(
191  request_proto.solver_specific_parameters()))
192  << "Wrong solver_specific_parameters (bad --params or --params_file ?)";
193  }
194 
195  MPSolutionResponse response;
196 
197  // Load the model proto into the solver.
198  {
199  std::string error_message;
200  const MPSolverResponseStatus status =
201  solver.LoadModelFromProtoWithUniqueNamesOrDie(request_proto.model(),
202  &error_message);
203  // Note, the underlying MPSolver treats time limit equal to 0 as no limit.
204  if (status != MPSOLVER_MODEL_IS_VALID) {
205  // HACK(user): For SAT solves, when the model is invalid we directly
206  // exit here.
207  if (request_proto.solver_type() ==
208  MPModelRequest::SAT_INTEGER_PROGRAMMING) {
209  sat::CpSolverResponse sat_response;
210  sat_response.set_status(sat::CpSolverStatus::MODEL_INVALID);
211  LOG(INFO) << sat::CpSolverResponseStats(sat_response);
212  exit(1);
213  }
214  response.set_status(status);
215  response.set_status_str(error_message);
216  return response;
217  }
218  }
219  if (request_proto.has_solver_time_limit_seconds()) {
220  solver.SetTimeLimit(
221  absl::Seconds(request_proto.solver_time_limit_seconds()));
222  }
223 
224  // Register a signal handler to interrupt the solve when the user presses ^C.
225  // Note that we ignore all previously registered handler here. If SCIP is
226  // used, this handler will be overridden by the one of SCIP that does the same
227  // thing.
228  SigintHandler handler;
229  handler.Register([&solver] { solver.InterruptSolve(); });
230 
231  // Solve.
232  const MPSolver::ResultStatus status = solver.Solve();
233 
234  // If --verify_solution is true, we already verified it. If not, we add
235  // a verification step here.
237  !absl::GetFlag(FLAGS_verify_solution)) {
238  LOG(INFO) << "Verifying the solution";
239  solver.VerifySolution(/*tolerance=*/MPSolverParameters().GetDoubleParam(
241  /*log_errors=*/true);
242  }
243 
244  // If the solver is a MIP, print the number of nodes.
245  // TODO(user): add the number of nodes to the response, and move this code
246  // to the main Run().
247  if (SolverTypeIsMip(request_proto.solver_type())) {
248  absl::PrintF("%-12s: %d\n", "Nodes", solver.nodes());
249  }
250 
251  // Fill and return the response proto.
252  solver.FillSolutionResponseProto(&response);
253  return response;
254 }
255 
256 void Run() {
257  QCHECK(!absl::GetFlag(FLAGS_input).empty()) << "--input is required";
258  QCHECK_GE(absl::GetFlag(FLAGS_time_limit), absl::ZeroDuration())
259  << "--time_limit must be given a positive duration";
260 
262  CHECK(MPSolver::ParseSolverType(absl::GetFlag(FLAGS_solver), &type))
263  << "Unsupported --solver: " << absl::GetFlag(FLAGS_solver);
264 
265  MPModelRequest request_proto = ReadMipModel(absl::GetFlag(FLAGS_input));
266 
267  if (!absl::GetFlag(FLAGS_sol_hint).empty()) {
268  const auto read_sol =
269  ParseSolFile(absl::GetFlag(FLAGS_sol_hint), request_proto.model());
270  CHECK(read_sol.ok());
271  const MPSolutionResponse sol = read_sol.value();
272  if (request_proto.model().has_solution_hint()) {
273  LOG(WARNING) << "Overwriting solution hint found in the request with "
274  << "solution from " << absl::GetFlag(FLAGS_sol_hint);
275  }
276  request_proto.mutable_model()->clear_solution_hint();
277  for (int i = 0; i < sol.variable_value_size(); ++i) {
278  request_proto.mutable_model()->mutable_solution_hint()->add_var_index(i);
279  request_proto.mutable_model()->mutable_solution_hint()->add_var_value(
280  sol.variable_value(i));
281  }
282  }
283 
284  printf("%-12s: '%s'\n", "File", absl::GetFlag(FLAGS_input).c_str());
285 
286  // Detect format to dump protos.
288  if (absl::GetFlag(FLAGS_dump_format) == "text") {
289  write_format = ProtoWriteFormat::kProtoText;
290  } else if (absl::GetFlag(FLAGS_dump_format) == "binary") {
291  write_format = ProtoWriteFormat::kProtoBinary;
292  } else if (absl::GetFlag(FLAGS_dump_format) == "json") {
293  write_format = ProtoWriteFormat::kJson;
294  } else {
295  LOG(FATAL) << "Unsupported --dump_format: "
296  << absl::GetFlag(FLAGS_dump_format);
297  }
298 
299  // Set or override request proto options from the command line flags.
300  request_proto.set_solver_type(static_cast<MPModelRequest::SolverType>(type));
301  if (absl::GetFlag(FLAGS_time_limit) != absl::InfiniteDuration()) {
302  LOG(INFO) << "Setting a time limit of " << absl::GetFlag(FLAGS_time_limit);
303  request_proto.set_solver_time_limit_seconds(
304  absl::ToDoubleSeconds(absl::GetFlag(FLAGS_time_limit)));
305  }
306  if (absl::GetFlag(FLAGS_linear_solver_enable_verbose_output)) {
307  request_proto.set_enable_internal_solver_output(true);
308  }
309  if (!absl::GetFlag(FLAGS_params_file).empty()) {
310  CHECK(absl::GetFlag(FLAGS_params).empty())
311  << "--params and --params_file are incompatible";
312  std::string file_contents;
313  CHECK_OK(file::GetContents(absl::GetFlag(FLAGS_params_file), &file_contents,
314  file::Defaults()))
315  << "Could not read parameters file.";
316  request_proto.set_solver_specific_parameters(file_contents);
317  }
318  if (!absl::GetFlag(FLAGS_params).empty()) {
319  request_proto.set_solver_specific_parameters(absl::GetFlag(FLAGS_params));
320  }
321 
322  // If requested, save the model and/or request to file.
323  if (!absl::GetFlag(FLAGS_dump_model).empty()) {
324  CHECK(WriteProtoToFile(absl::GetFlag(FLAGS_dump_model),
325  request_proto.model(), write_format,
326  absl::GetFlag(FLAGS_dump_gzip)));
327  }
328  if (!absl::GetFlag(FLAGS_dump_request).empty()) {
329  CHECK(WriteProtoToFile(absl::GetFlag(FLAGS_dump_request), request_proto,
330  write_format, absl::GetFlag(FLAGS_dump_gzip)));
331  }
332 
333  absl::PrintF(
334  "%-12s: %s\n", "Solver",
335  MPModelRequest::SolverType_Name(request_proto.solver_type()).c_str());
336  absl::PrintF("%-12s: %s\n", "Parameters", absl::GetFlag(FLAGS_params));
337  absl::PrintF("%-12s: %d x %d\n", "Dimension",
338  request_proto.model().constraint_size(),
339  request_proto.model().variable_size());
340 
341  const absl::Time solve_start_time = absl::Now();
342 
343  const MPSolutionResponse response = LocalSolve(request_proto);
344 
345  const absl::Duration solving_time = absl::Now() - solve_start_time;
346  const bool has_solution = response.status() == MPSOLVER_OPTIMAL ||
347  response.status() == MPSOLVER_FEASIBLE;
348  absl::PrintF("%-12s: %s\n", "Status",
349  MPSolverResponseStatus_Name(
350  static_cast<MPSolverResponseStatus>(response.status()))
351  .c_str());
352  absl::PrintF("%-12s: %15.15e\n", "Objective",
353  has_solution ? response.objective_value() : 0.0);
354  absl::PrintF("%-12s: %15.15e\n", "BestBound",
355  has_solution ? response.best_objective_bound() : 0.0);
356  absl::PrintF("%-12s: %s\n", "StatusString", response.status_str());
357  absl::PrintF("%-12s: %-6.4g s\n", "Time",
358  absl::ToDoubleSeconds(solving_time));
359 
360  // If requested, write the solution, in .sol format (--sol_file), proto
361  // format and/or csv format.
362  if (!absl::GetFlag(FLAGS_sol_file).empty() && has_solution) {
363  std::string sol_string;
364  absl::StrAppend(&sol_string, "=obj= ", response.objective_value(), "\n");
365  for (int i = 0; i < response.variable_value().size(); ++i) {
366  absl::StrAppend(&sol_string, request_proto.model().variable(i).name(),
367  " ", response.variable_value(i), "\n");
368  }
369  LOG(INFO) << "Writing .sol solution to '" << absl::GetFlag(FLAGS_sol_file)
370  << "'.\n";
371  CHECK_OK(file::SetContents(absl::GetFlag(FLAGS_sol_file), sol_string,
372  file::Defaults()));
373  }
374  if (!absl::GetFlag(FLAGS_dump_response).empty() && has_solution) {
375  CHECK(WriteProtoToFile(absl::GetFlag(FLAGS_dump_response), response,
376  write_format, absl::GetFlag(FLAGS_dump_gzip)));
377  }
378  if (!absl::GetFlag(FLAGS_output_csv).empty() && has_solution) {
379  std::string csv_file;
380  for (int i = 0; i < response.variable_value_size(); ++i) {
381  csv_file +=
382  absl::StrFormat("%s,%e\n", request_proto.model().variable(i).name(),
383  response.variable_value(i));
384  }
385  CHECK_OK(file::SetContents(absl::GetFlag(FLAGS_output_csv), csv_file,
386  file::Defaults()));
387  }
388 }
389 
390 } // namespace
391 } // namespace operations_research
392 
393 int main(int argc, char** argv) {
394  InitGoogle(kUsageStr, &argc, &argv, /*remove_flags=*/true);
395  operations_research::Run();
396 }
ResultStatus
The status of solving the problem.
@ FEASIBLE
feasible, or stopped by limit.
OptimizationProblemType
The type of problems (LP or MIP) that will be solved and the underlying solver (GLOP,...
static bool ParseSolverType(absl::string_view solver_id, OptimizationProblemType *type)
Parses the name of the solver.
@ PRIMAL_TOLERANCE
Advanced usage: tolerance for primal feasibility of basic solutions.
CpModelProto const * model_proto
SharedResponseManager * response
ModelSharedTimeLimit * time_limit
absl::Status status
Definition: g_gurobi.cc:41
void InitGoogle(const char *usage, int *argc, char ***argv, bool deprecated)
Definition: init_google.h:34
int main(int argc, char **argv)
static const char kUsageStr[]
ABSL_DECLARE_FLAG(bool, verify_solution)
ABSL_FLAG(std::string, input, "", "REQUIRED: Input file name.")
A C++ wrapper that provides a simple and unified interface to several linear programming and mixed in...
absl::Status GetContents(const absl::string_view &filename, std::string *output, int flags)
Definition: base/file.cc:164
Options Defaults()
Definition: base/file.h:123
absl::Status SetContents(const absl::string_view &filename, const absl::string_view &contents, int flags)
Definition: base/file.cc:205
void ParseFile(const std::string &filename, bool presolve)
Definition: parser_main.cc:36
std::string CpSolverResponseStats(const CpSolverResponse &response, bool has_objective)
Returns a string with some statistics on the solver response.
Collection of objects used to extend the Constraint Solver library.
bool SolverTypeIsMip(MPModelRequest::SolverType solver_type)
bool WriteProtoToFile(absl::string_view filename, const google::protobuf::Message &proto, ProtoWriteFormat proto_write_format, bool gzipped, bool append_extension_to_file_name)
Definition: file_util.cc:112
absl::StatusOr< glop::DenseRow > ParseSolFile(const std::string &file_name, const glop::LinearProgram &model)
Definition: sol_reader.cc:34
bool ReadFileToProto(absl::string_view filename, google::protobuf::Message *proto)
Definition: file_util.cc:47
static int input(yyscan_t yyscanner)