Google OR-Tools v9.12
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
solve.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// 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 <cstdio>
53#include <iostream>
54#include <optional>
55#include <string>
56#include <utility>
57
58#include "absl/flags/declare.h"
59#include "absl/flags/flag.h"
60#include "absl/status/status.h"
61#include "absl/status/statusor.h"
62#include "absl/strings/match.h"
63#include "absl/strings/str_cat.h"
64#include "absl/strings/str_format.h"
65#include "absl/time/time.h"
66#include "ortools/base/file.h"
72#include "ortools/linear_solver/linear_solver.pb.h"
77#include "ortools/sat/cp_model.pb.h"
80#include "ortools/util/sigint.h"
81
82ABSL_FLAG(std::string, input, "", "REQUIRED: Input file name.");
83ABSL_FLAG(std::string, sol_hint, "",
84 "Input file name with solution in .sol format.");
85ABSL_FLAG(std::optional<std::string>, solver, std::nullopt,
86 "The solver to use: bop, cbc, clp, glop, glpk_lp, glpk_mip, "
87 "gurobi_lp, gurobi_mip, pdlp, scip, knapsack, sat. If unspecified "
88 "either use MPModelRequest.solver_type if the --input is an "
89 "MPModelRequest and the field is set or use glop.");
90ABSL_FLAG(int, num_threads, 1,
91 "Number of threads to use by the underlying solver.");
92ABSL_FLAG(std::string, params_file, "",
93 "Solver specific parameters file. "
94 "If this flag is set, the --params flag is ignored.");
95ABSL_FLAG(std::string, params, "", "Solver specific parameters");
96ABSL_FLAG(absl::Duration, time_limit, absl::InfiniteDuration(),
97 "It specifies a limit on the solving time. The duration must be must "
98 "be positive. It default to an infinite duration meaning that no "
99 "time limit will be imposed.");
100ABSL_FLAG(std::string, output_csv, "",
101 "If non-empty, write the returned solution in csv format with "
102 "each line formed by a variable name and its value.");
103
104ABSL_FLAG(std::string, dump_format, "text",
105 "Format in which to dump protos (if flags --dump_model, "
106 "--dump_request, or --dump_response are used). Possible values: "
107 "'text', 'binary', 'json' which correspond to text proto format "
108 "binary proto format, and json. If 'binary' or 'json' are used, "
109 "we append '.bin' and '.json' to file names.");
110ABSL_FLAG(bool, dump_gzip, false,
111 "Whether to gzip dumped protos. Appends .gz to their name.");
112ABSL_FLAG(std::string, dump_model, "",
113 "If non-empty, dumps MPModelProto there.");
114ABSL_FLAG(std::string, dump_request, "",
115 "If non-empty, dumps MPModelRequest there.");
116ABSL_FLAG(std::string, dump_response, "",
117 "If non-empty, dumps MPSolutionResponse there.");
118ABSL_FLAG(std::string, sol_file, "",
119 "If non-empty, output the best solution in Miplib .sol format.");
120ABSL_FLAG(std::string, dump_mps, "",
121 "If non-empty, dumps the model in mps format there.");
122
123static 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
129namespace operations_research {
130namespace {
131
132MPModelRequest ReadMipModel(const std::string& input) {
133 MPModelRequest request_proto;
134 MPModelProto model_proto;
135 if (absl::EndsWith(input, ".lp")) {
136#if defined(USE_LP_PARSER)
137 std::string data;
138 CHECK_OK(file::GetContents(input, &data, file::Defaults()));
139 absl::StatusOr<MPModelProto> result = ModelProtoFromLpFormat(data);
140 CHECK_OK(result);
141 model_proto = std::move(result).value();
142#else // !defined(USE_LP_PARSER)
143 LOG(FATAL) << "Support for parsing LP format is not compiled in.";
144#endif // !defined(USE_LP_PARSER)
145 } else if (absl::EndsWith(input, ".mps") ||
146 absl::EndsWith(input, ".mps.gz")) {
147 QCHECK_OK(glop::MPSReader().ParseFile(input, &model_proto))
148 << "Error while parsing the mps file '" << input << "'.";
149 } else {
150 ReadFileToProto(input, &model_proto).IgnoreError();
151 ReadFileToProto(input, &request_proto).IgnoreError();
152 }
153 // If the input is a proto in binary format, both ReadFileToProto could
154 // return true. Instead use the actual number of variables found to test the
155 // correct format of the input.
156 const bool is_model_proto = model_proto.variable_size() > 0;
157 const bool is_request_proto =
158 request_proto.model().variable_size() > 0 ||
159 !request_proto.model_delta().baseline_model_file_path().empty();
160 if (!is_model_proto && !is_request_proto) {
161 LOG(FATAL) << "Failed to parse '" << input
162 << "' as an MPModelProto or an MPModelRequest.";
163 } else {
164 CHECK(!(is_model_proto && is_request_proto));
165 }
166 if (is_request_proto) {
167 LOG(INFO) << "Read input proto as an MPModelRequest.";
168 } else {
169 LOG(INFO) << "Read input proto as an MPModelProto.";
170 model_proto.Swap(request_proto.mutable_model());
171 }
172 return request_proto;
173}
174
175MPSolutionResponse LocalSolve(const MPModelRequest& request_proto) {
176 // TODO(or-core-team): Why doesn't this use MPSolver::SolveWithProto() ?
177
178 // Create the solver, we use the name of the model as the solver name.
179 MPSolver solver(request_proto.model().name(),
181 request_proto.solver_type()));
182 const absl::Status set_num_threads_status =
183 solver.SetNumThreads(absl::GetFlag(FLAGS_num_threads));
184 if (set_num_threads_status.ok()) {
185 LOG(INFO) << "Set number of threads to " << absl::GetFlag(FLAGS_num_threads)
186 << ".";
187 } else if (absl::GetFlag(FLAGS_num_threads) != 1) {
188 LOG(ERROR) << "Failed to set number of threads due to: "
189 << set_num_threads_status.message() << ". Using 1 as default.";
190 }
191 solver.EnableOutput();
192
193 if (request_proto.has_solver_specific_parameters()) {
194 CHECK(solver.SetSolverSpecificParametersAsString(
195 request_proto.solver_specific_parameters()))
196 << "Wrong solver_specific_parameters (bad --params or --params_file ?)";
197 }
198
199 MPSolutionResponse response;
200
201 // Load the model proto into the solver.
202 {
203 std::string error_message;
204 const MPSolverResponseStatus status =
205 solver.LoadModelFromProtoWithUniqueNamesOrDie(request_proto.model(),
206 &error_message);
207 // Note, the underlying MPSolver treats time limit equal to 0 as no limit.
208 if (status != MPSOLVER_MODEL_IS_VALID) {
209 // HACK(user): For SAT solves, when the model is invalid we directly
210 // exit here.
211 if (request_proto.solver_type() ==
212 MPModelRequest::SAT_INTEGER_PROGRAMMING) {
213 sat::CpSolverResponse sat_response;
214 sat_response.set_status(sat::CpSolverStatus::MODEL_INVALID);
215 LOG(INFO) << sat::CpSolverResponseStats(sat_response);
216 exit(1);
217 }
218 response.set_status(status);
219 response.set_status_str(error_message);
220 return response;
221 }
222 }
223 if (request_proto.has_solver_time_limit_seconds()) {
224 solver.SetTimeLimit(
225 absl::Seconds(request_proto.solver_time_limit_seconds()));
226 }
227
228 // Register a signal handler to interrupt the solve when the user presses ^C.
229 // Note that we ignore all previously registered handler here. If SCIP is
230 // used, this handler will be overridden by the one of SCIP that does the same
231 // thing.
232 SigintHandler handler;
233 handler.Register([&solver] { solver.InterruptSolve(); });
234
235 // Solve.
236 const MPSolver::ResultStatus status = solver.Solve();
237
238 // If --verify_solution is true, we already verified it. If not, we add
239 // a verification step here.
240 if ((status == MPSolver::OPTIMAL || status == MPSolver::FEASIBLE) &&
241 !absl::GetFlag(FLAGS_verify_solution)) {
242 const bool verified =
243 solver.VerifySolution(/*tolerance=*/MPSolverParameters().GetDoubleParam(
245 absl::GetFlag(FLAGS_log_verification_errors));
246 LOG(INFO) << "The solution "
247 << (verified ? "was verified." : "didn't pass verification.");
248 }
249
250 // If the solver is a MIP, print the number of nodes.
251 // TODO(user): add the number of nodes to the response, and move this code
252 // to the main Run().
253 if (SolverTypeIsMip(request_proto.solver_type())) {
254 absl::PrintF("%-12s: %d\n", "Nodes", solver.nodes());
255 }
256
257 // Fill and return the response proto.
258 solver.FillSolutionResponseProto(&response);
259 return response;
260}
261
262void Run() {
263 QCHECK(!absl::GetFlag(FLAGS_input).empty()) << "--input is required";
264 QCHECK_GE(absl::GetFlag(FLAGS_time_limit), absl::ZeroDuration())
265 << "--time_limit must be given a positive duration";
266
267 // Parses --solver if set.
268 std::optional<MPSolver::OptimizationProblemType> type;
269 if (const std::optional<std::string> type_flag = absl::GetFlag(FLAGS_solver);
270 type_flag.has_value()) {
272 QCHECK(MPSolver::ParseSolverType(type_flag.value(), &decoded_type))
273 << "Unsupported --solver: " << type_flag.value();
274 type = decoded_type;
275 }
276
277 MPModelRequest request_proto = ReadMipModel(absl::GetFlag(FLAGS_input));
278
279 if (!absl::GetFlag(FLAGS_sol_hint).empty()) {
280 const auto read_sol =
281 ParseSolFile(absl::GetFlag(FLAGS_sol_hint), request_proto.model());
282 CHECK_OK(read_sol.status());
283 const MPSolutionResponse& sol = read_sol.value();
284 if (request_proto.model().has_solution_hint()) {
285 LOG(WARNING) << "Overwriting solution hint found in the request with "
286 << "solution from " << absl::GetFlag(FLAGS_sol_hint);
287 }
288 request_proto.mutable_model()->clear_solution_hint();
289 for (int i = 0; i < sol.variable_value_size(); ++i) {
290 request_proto.mutable_model()->mutable_solution_hint()->add_var_index(i);
291 request_proto.mutable_model()->mutable_solution_hint()->add_var_value(
292 sol.variable_value(i));
293 }
294 }
295
296 printf("%-12s: '%s'\n", "File", absl::GetFlag(FLAGS_input).c_str());
297
298 // Detect format to dump protos.
300 if (absl::GetFlag(FLAGS_dump_format) == "text") {
301 write_format = ProtoWriteFormat::kProtoText;
302 } else if (absl::GetFlag(FLAGS_dump_format) == "binary") {
303 write_format = ProtoWriteFormat::kProtoBinary;
304 } else if (absl::GetFlag(FLAGS_dump_format) == "json") {
305 write_format = ProtoWriteFormat::kJson;
306 } else {
307 LOG(FATAL) << "Unsupported --dump_format: "
308 << absl::GetFlag(FLAGS_dump_format);
309 }
310
311 if (!absl::GetFlag(FLAGS_dump_mps).empty()) {
312 CHECK_OK(WriteModelToMpsFile(absl::GetFlag(FLAGS_dump_mps),
313 request_proto.model()));
314 }
315
316 // Set or override request proto options from the command line flags.
317 if (type.has_value() || !request_proto.has_solver_type()) {
318 request_proto.set_solver_type(static_cast<MPModelRequest::SolverType>(
319 type.value_or(MPSolver::GLOP_LINEAR_PROGRAMMING)));
320 }
321 if (absl::GetFlag(FLAGS_time_limit) != absl::InfiniteDuration()) {
322 LOG(INFO) << "Setting a time limit of " << absl::GetFlag(FLAGS_time_limit);
323 request_proto.set_solver_time_limit_seconds(
324 absl::ToDoubleSeconds(absl::GetFlag(FLAGS_time_limit)));
325 }
326 if (absl::GetFlag(FLAGS_linear_solver_enable_verbose_output)) {
327 request_proto.set_enable_internal_solver_output(true);
328 }
329 if (!absl::GetFlag(FLAGS_params_file).empty()) {
330 CHECK(absl::GetFlag(FLAGS_params).empty())
331 << "--params and --params_file are incompatible";
332 std::string file_contents;
333 CHECK_OK(file::GetContents(absl::GetFlag(FLAGS_params_file), &file_contents,
335 << "Could not read parameters file.";
336 request_proto.set_solver_specific_parameters(file_contents);
337 }
338 if (!absl::GetFlag(FLAGS_params).empty()) {
339 request_proto.set_solver_specific_parameters(absl::GetFlag(FLAGS_params));
340 }
341
342 // If requested, save the model and/or request to file.
343 if (!absl::GetFlag(FLAGS_dump_model).empty()) {
344 CHECK_OK(WriteProtoToFile(absl::GetFlag(FLAGS_dump_model),
345 request_proto.model(), write_format,
346 absl::GetFlag(FLAGS_dump_gzip)));
347 }
348 if (!absl::GetFlag(FLAGS_dump_request).empty()) {
349 CHECK_OK(WriteProtoToFile(absl::GetFlag(FLAGS_dump_request), request_proto,
350 write_format, absl::GetFlag(FLAGS_dump_gzip)));
351 }
352
353 absl::PrintF(
354 "%-12s: %s\n", "Solver",
355 MPModelRequest::SolverType_Name(request_proto.solver_type()).c_str());
356 absl::PrintF("%-12s: %s\n", "Parameters", absl::GetFlag(FLAGS_params));
357 absl::PrintF("%-12s: %d x %d\n", "Dimension",
358 request_proto.model().constraint_size(),
359 request_proto.model().variable_size());
360
361 const absl::Time solve_start_time = absl::Now();
362
363 const MPSolutionResponse response = LocalSolve(request_proto);
364
365 const absl::Duration solving_time = absl::Now() - solve_start_time;
366 const bool has_solution = response.status() == MPSOLVER_OPTIMAL ||
367 response.status() == MPSOLVER_FEASIBLE;
368 absl::PrintF("%-12s: %s\n", "Status",
369 MPSolverResponseStatus_Name(
370 static_cast<MPSolverResponseStatus>(response.status()))
371 .c_str());
372 absl::PrintF("%-12s: %15.15e\n", "Objective",
373 has_solution ? response.objective_value() : 0.0);
374 absl::PrintF("%-12s: %15.15e\n", "BestBound",
375 has_solution ? response.best_objective_bound() : 0.0);
376 absl::PrintF("%-12s: %s\n", "StatusString", response.status_str());
377 absl::PrintF("%-12s: %-6.4g s\n", "Time",
378 absl::ToDoubleSeconds(solving_time));
379
380 // If requested, write the solution, in .sol format (--sol_file), proto
381 // format and/or csv format.
382 if (!absl::GetFlag(FLAGS_sol_file).empty() && has_solution) {
383 std::string sol_string;
384 absl::StrAppend(&sol_string, "=obj= ", response.objective_value(), "\n");
385 for (int i = 0; i < response.variable_value().size(); ++i) {
386 absl::StrAppend(&sol_string, request_proto.model().variable(i).name(),
387 " ", response.variable_value(i), "\n");
388 }
389 LOG(INFO) << "Writing .sol solution to '" << absl::GetFlag(FLAGS_sol_file)
390 << "'.\n";
391 CHECK_OK(file::SetContents(absl::GetFlag(FLAGS_sol_file), sol_string,
392 file::Defaults()));
393 }
394 if (!absl::GetFlag(FLAGS_dump_response).empty() && has_solution) {
395 CHECK_OK(WriteProtoToFile(absl::GetFlag(FLAGS_dump_response), response,
396 write_format, absl::GetFlag(FLAGS_dump_gzip)));
397 }
398 if (!absl::GetFlag(FLAGS_output_csv).empty() && has_solution) {
399 std::string csv_file;
400 for (int i = 0; i < response.variable_value_size(); ++i) {
401 csv_file +=
402 absl::StrFormat("%s,%e\n", request_proto.model().variable(i).name(),
403 response.variable_value(i));
404 }
405 CHECK_OK(file::SetContents(absl::GetFlag(FLAGS_output_csv), csv_file,
406 file::Defaults()));
407 }
408}
409
410} // namespace
411} // namespace operations_research
412
413int main(int argc, char** argv) {
414 InitGoogle(kUsageStr, &argc, &argv, /*remove_flags=*/true);
416}
@ FEASIBLE
feasible, or stopped by limit.
static bool ParseSolverType(absl::string_view solver_id, OptimizationProblemType *type)
static
void InitGoogle(const char *usage, int *argc, char ***argv, bool deprecated)
Definition init_google.h:34
ABSL_FLAG(std::string, sol_hint, "", "Input file name with solution in .sol format.")
int main(int argc, char **argv)
Definition solve.cc:413
time_limit
Definition solve.cc:22
static const char kUsageStr[]
absl::StatusOr< std::string > GetContents(absl::string_view path, Options options)
Definition file.cc:191
absl::Status SetContents(absl::string_view filename, absl::string_view contents, Options options)
Definition file.cc:242
Options Defaults()
Definition file.h:107
std::string CpSolverResponseStats(const CpSolverResponse &response, bool has_objective)
In SWIG mode, we don't want anything besides these top-level includes.
absl::Status WriteModelToMpsFile(absl::string_view filename, const MPModelProto &model, const MPModelExportOptions &options)
absl::StatusOr< glop::DenseRow > ParseSolFile(absl::string_view file_name, const glop::LinearProgram &model)
Parse a solution to model from a file.
Definition sol_reader.cc:35
bool SolverTypeIsMip(MPModelRequest::SolverType solver_type)
There is a homonymous version taking a MPSolver::OptimizationProblemType.
absl::Status 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:142
absl::Status ReadFileToProto(absl::string_view filename, google::protobuf::Message *proto, bool allow_partial)
Definition file_util.cc:53
static int input(yyscan_t yyscanner)