Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
model_exporter.cc
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
15
16#include <algorithm>
17#include <cmath>
18#include <limits>
19#include <memory>
20#include <string>
21#include <utility>
22#include <vector>
23
24#include "absl/container/flat_hash_set.h"
25#include "absl/flags/flag.h"
26#include "absl/status/status.h"
27#include "absl/status/statusor.h"
28#include "absl/strings/ascii.h"
29#include "absl/strings/match.h"
30#include "absl/strings/str_cat.h"
31#include "absl/strings/str_format.h"
32#include "absl/strings/string_view.h"
37#include "ortools/linear_solver/linear_solver.pb.h"
38
39ABSL_RETIRED_FLAG(bool, lp_log_invalid_name, false, "DEPRECATED.");
40
41namespace operations_research {
42namespace {
43
44constexpr double kInfinity = std::numeric_limits<double>::infinity();
45
46class LineBreaker {
47 public:
48 explicit LineBreaker(int max_line_size)
49 : max_line_size_(max_line_size), line_size_(0), output_() {}
50 // Lines are broken in such a way that:
51 // - Strings that are given to Append() are never split.
52 // - Lines are split so that their length doesn't exceed the max length;
53 // unless a single string given to Append() exceeds that length (in which
54 // case it will be put alone on a single unsplit line).
55 void Append(absl::string_view s);
56
57 // Returns true if string s will fit on the current line without adding a
58 // carriage return.
59 bool WillFit(const std::string& s) {
60 return line_size_ + static_cast<int>(s.size()) < max_line_size_;
61 }
62
63 // "Consumes" size characters on the line. Used when starting the constraint
64 // lines.
65 void Consume(int size) { line_size_ += size; }
66
67 std::string GetOutput() const { return output_; }
68
69 private:
70 int max_line_size_;
71 int line_size_;
72 std::string output_;
73};
74
75void LineBreaker::Append(absl::string_view s) {
76 line_size_ += s.size();
77 if (line_size_ > max_line_size_) {
78 line_size_ = s.size();
79 absl::StrAppend(&output_, "\n ");
80 }
81 absl::StrAppend(&output_, s);
82}
83
84class MPModelProtoExporter {
85 public:
86 explicit MPModelProtoExporter(const MPModelProto& model);
87
88 // This type is neither copyable nor movable.
89 MPModelProtoExporter(const MPModelProtoExporter&) = delete;
90 MPModelProtoExporter& operator=(const MPModelProtoExporter&) = delete;
91
92 bool ExportModelAsLpFormat(const MPModelExportOptions& options,
93 std::string* output);
94 bool ExportModelAsMpsFormat(const MPModelExportOptions& options,
95 std::string* output);
96
97 private:
98 // Computes the number of continuous, integer and binary variables.
99 // Called by ExportModelAsLpFormat() and ExportModelAsMpsFormat().
100 void Setup();
101
102 // Computes smart column widths for free MPS format.
103 void ComputeMpsSmartColumnWidths(bool obfuscated);
104
105 // Processes all the proto.name() fields and returns the result in a vector.
106 //
107 // If 'obfuscate' is true, none of names are actually used, and this just
108 // returns a vector of 'prefix' + proto index (1-based).
109 //
110 // If it is false, this tries to keep the original names, but:
111 // - if the first character is forbidden (or name is empty), '_' is added at
112 // the beginning of name.
113 // - all the other forbidden characters are replaced by '_'.
114 // To avoid name conflicts, a '_' followed by an integer is appended to the
115 // result.
116 //
117 // If a name is longer than the maximum allowed name length, the obfuscated
118 // name is used.
119 //
120 // Therefore, a name "$20<=40" for proto #3 could be "_$20__40_1".
121 template <class ListOfProtosWithNameFields>
122 std::vector<std::string> ExtractAndProcessNames(
123 const ListOfProtosWithNameFields& proto, absl::string_view prefix,
124 bool obfuscate, bool log_invalid_names,
125 const std::string& forbidden_first_chars,
126 const std::string& forbidden_chars);
127
128 // Appends a general "Comment" section with useful metadata about the model
129 // to "output".
130 // Note(user): there may be less variables in output than in the original
131 // model, as unused variables are not shown by default. Similarly, there
132 // may be more constraints in a .lp file as in the original model as
133 // a constraint lhs <= term <= rhs will be output as the two constraints
134 // term >= lhs and term <= rhs.
135 void AppendComments(const std::string& separator, std::string* output) const;
136
137 // Appends an MPConstraintProto to the output text. If the constraint has
138 // both an upper and lower bound that are not equal, it splits the constraint
139 // into two constraints, one for the left hand side (_lhs) and one for right
140 // hand side (_rhs).
141 bool AppendConstraint(const MPConstraintProto& ct_proto,
142 const std::string& name, LineBreaker& line_breaker,
143 std::vector<bool>& show_variable, std::string* output);
144
145 // Clears "output" and writes a term to it, in "LP" format. Returns false on
146 // error (for example, var_index is out of range).
147 bool WriteLpTerm(int var_index, double coefficient,
148 std::string* output) const;
149
150 // Appends a pair name, value to "output", formatted to comply with the MPS
151 // standard.
152 void AppendMpsPair(const std::string& name, double value,
153 std::string* output) const;
154
155 // Appends the head of a line, consisting of an id and a name to output.
156 void AppendMpsLineHeader(const std::string& id, const std::string& name,
157 std::string* output) const;
158
159 // Same as AppendMpsLineHeader. Appends an extra new-line at the end the
160 // string pointed to by output.
161 void AppendMpsLineHeaderWithNewLine(const std::string& id,
162 const std::string& name,
163 std::string* output) const;
164
165 // Appends an MPS term in various contexts. The term consists of a head name,
166 // a name, and a value. If the line is not empty, then only the pair
167 // (name, value) is appended. The number of columns, limited to 2 by the MPS
168 // format is also taken care of.
169 void AppendMpsTermWithContext(const std::string& head_name,
170 const std::string& name, double value,
171 std::string* output);
172
173 // Appends a new-line if two columns are already present on the MPS line.
174 // Used by and in complement to AppendMpsTermWithContext.
175 void AppendNewLineIfTwoColumns(std::string* output);
176
177 // When 'integrality' is true, appends columns corresponding to integer
178 // variables. Appends the columns for non-integer variables otherwise.
179 // The sparse matrix must be passed as a vector of columns ('transpose').
180 void AppendMpsColumns(
181 bool integrality,
182 const std::vector<std::vector<std::pair<int, double>>>& transpose,
183 std::string* output);
184
185 // Appends a line describing the bound of a variablenew-line if two columns
186 // are already present on the MPS line.
187 // Used by and in complement to AppendMpsTermWithContext.
188 void AppendMpsBound(const std::string& bound_type, const std::string& name,
189 double value, std::string* output) const;
190
191 const MPModelProto& proto_;
192
193 // Vector of variable names as they will be exported.
194 std::vector<std::string> exported_variable_names_;
195
196 // Vector of constraint names as they will be exported.
197 std::vector<std::string> exported_constraint_names_;
198
199 // Vector of general constraint names as they will be exported.
200 std::vector<std::string> exported_general_constraint_names_;
201
202 // Number of integer variables in proto_.
203 int num_integer_variables_;
204
205 // Number of binary variables in proto_.
206 int num_binary_variables_;
207
208 // Number of continuous variables in proto_.
209 int num_continuous_variables_;
210
211 // Current MPS file column number.
212 int current_mps_column_;
213
214 // Format for MPS file lines.
215 std::unique_ptr<absl::ParsedFormat<'s', 's'>> mps_header_format_;
216 std::unique_ptr<absl::ParsedFormat<'s', 's'>> mps_format_;
217};
218
219} // namespace
220
221absl::StatusOr<std::string> ExportModelAsLpFormat(
222 const MPModelProto& model, const MPModelExportOptions& options) {
223 for (const MPGeneralConstraintProto& general_constraint :
224 model.general_constraint()) {
225 if (!general_constraint.has_indicator_constraint()) {
226 return absl::InvalidArgumentError(
227 "Non-indicator general constraints are not supported.");
228 }
229 }
230 MPModelProtoExporter exporter(model);
231 std::string output;
232 if (!exporter.ExportModelAsLpFormat(options, &output)) {
233 return absl::InvalidArgumentError("Unable to export model.");
234 }
235 return output;
236}
237
238absl::StatusOr<std::string> ExportModelAsMpsFormat(
239 const MPModelProto& model, const MPModelExportOptions& options) {
240 if (model.general_constraint_size() > 0) {
241 return absl::InvalidArgumentError("General constraints are not supported.");
242 }
243 MPModelProtoExporter exporter(model);
244 std::string output;
245 if (!exporter.ExportModelAsMpsFormat(options, &output)) {
246 return absl::InvalidArgumentError("Unable to export model.");
247 }
248 return output;
249}
250
251absl::Status WriteModelToMpsFile(absl::string_view filename,
252 const MPModelProto& model,
253 const MPModelExportOptions& options) {
254 ASSIGN_OR_RETURN(std::string mps_data,
255 ExportModelAsMpsFormat(model, options));
256 return file::SetContents(filename, mps_data, file::Defaults());
257}
258
259namespace {
260MPModelProtoExporter::MPModelProtoExporter(const MPModelProto& model)
261 : proto_(model),
262 num_integer_variables_(0),
263 num_binary_variables_(0),
264 num_continuous_variables_(0),
265 current_mps_column_(0) {}
266
267namespace {
268class NameManager {
269 public:
270 NameManager() : names_set_(), last_n_(1) {}
271 std::string MakeUniqueName(absl::string_view name);
272
273 private:
274 absl::flat_hash_set<std::string> names_set_;
275 int last_n_;
276};
277
278std::string NameManager::MakeUniqueName(absl::string_view name) {
279 std::string result(name);
280 // Find the 'n' so that "name_n" does not already exist.
281 int n = last_n_;
282 while (!names_set_.insert(result).second) {
283 result = absl::StrCat(name, "_", n);
284 ++n;
285 }
286 // We keep the last n used to avoid a quadratic behavior in case
287 // all the names are the same initially.
288 last_n_ = n;
289 return result;
290}
291
292// NOTE: As a special case, an empty name is treated as started with a forbidden
293// character (\0).
294std::string MakeExportableName(const std::string& name,
295 const std::string& forbidden_first_chars,
296 const std::string& forbidden_chars,
297 bool* found_forbidden_char) {
298 // Prepend with "_" all the names starting with a forbidden character.
299 *found_forbidden_char =
300 name.empty() || absl::StrContains(forbidden_first_chars, name[0]);
301 std::string exportable_name =
302 *found_forbidden_char ? absl::StrCat("_", name) : name;
303
304 // Replace all the other forbidden characters with "_".
305 for (char& c : exportable_name) {
306 if (absl::StrContains(forbidden_chars, c)) {
307 c = '_';
308 *found_forbidden_char = true;
309 }
310 }
311 return exportable_name;
312}
313} // namespace
314
315template <class ListOfProtosWithNameFields>
316std::vector<std::string> MPModelProtoExporter::ExtractAndProcessNames(
317 const ListOfProtosWithNameFields& proto, absl::string_view prefix,
318 bool obfuscate, bool log_invalid_names,
319 const std::string& forbidden_first_chars,
320 const std::string& forbidden_chars) {
321 const int num_items = proto.size();
322 std::vector<std::string> result(num_items);
323 NameManager namer;
324 const int num_digits = absl::StrCat(num_items).size();
325 int i = 0;
326 for (const auto& item : proto) {
327 const std::string obfuscated_name =
328 absl::StrFormat("%s%0*d", prefix, num_digits, i);
329 if (obfuscate || !item.has_name()) {
330 result[i] = namer.MakeUniqueName(obfuscated_name);
331 LOG_IF(WARNING, log_invalid_names && !item.has_name())
332 << "Empty name detected, created new name: " << result[i];
333 } else {
334 bool found_forbidden_char = false;
335 const std::string exportable_name =
336 MakeExportableName(item.name(), forbidden_first_chars,
337 forbidden_chars, &found_forbidden_char);
338 result[i] = namer.MakeUniqueName(exportable_name);
339 LOG_IF(WARNING, log_invalid_names && found_forbidden_char)
340 << "Invalid character detected in " << item.name() << ". Changed to "
341 << result[i];
342 // If the name is too long, use the obfuscated name that is guaranteed
343 // to fit. If ever we are able to solve problems with 2^64 variables,
344 // their obfuscated names would fit within 20 characters.
345 const int kMaxNameLength = 255;
346 // Take care of "_rhs" or "_lhs" that may be added in the case of
347 // constraints with both right-hand side and left-hand side.
348 const int kMargin = 4;
349 if (result[i].size() > kMaxNameLength - kMargin) {
350 const std::string old_name = std::move(result[i]);
351 result[i] = namer.MakeUniqueName(obfuscated_name);
352 LOG_IF(WARNING, log_invalid_names) << "Name is too long: " << old_name
353 << " exported as: " << result[i];
354 }
355 }
356
357 // Prepare for the next round.
358 ++i;
359 }
360 return result;
361}
362
363void MPModelProtoExporter::AppendComments(const std::string& separator,
364 std::string* output) const {
365 const char* const sep = separator.c_str();
366 absl::StrAppendFormat(output, "%s Generated by MPModelProtoExporter\n", sep);
367 absl::StrAppendFormat(output, "%s %-16s : %s\n", sep, "Name",
368 proto_.has_name() ? proto_.name().c_str() : "NoName");
369 absl::StrAppendFormat(output, "%s %-16s : %s\n", sep, "Format", "Free");
370 absl::StrAppendFormat(
371 output, "%s %-16s : %d\n", sep, "Constraints",
372 proto_.constraint_size() + proto_.general_constraint_size());
373 absl::StrAppendFormat(output, "%s %-16s : %d\n", sep, "Variables",
374 proto_.variable_size());
375 absl::StrAppendFormat(output, "%s %-14s : %d\n", sep, "Binary",
376 num_binary_variables_);
377 absl::StrAppendFormat(output, "%s %-14s : %d\n", sep, "Integer",
378 num_integer_variables_);
379 absl::StrAppendFormat(output, "%s %-14s : %d\n", sep, "Continuous",
380 num_continuous_variables_);
381}
382
383namespace {
384
385std::string DoubleToStringWithForcedSign(double d) {
386 return absl::StrCat((d < 0 ? "" : "+"), (d));
387}
388
389std::string DoubleToString(double d) { return absl::StrCat((d)); }
390
391} // namespace
392
393bool MPModelProtoExporter::AppendConstraint(const MPConstraintProto& ct_proto,
394 const std::string& name,
395 LineBreaker& line_breaker,
396 std::vector<bool>& show_variable,
397 std::string* output) {
398 for (int i = 0; i < ct_proto.var_index_size(); ++i) {
399 const int var_index = ct_proto.var_index(i);
400 const double coeff = ct_proto.coefficient(i);
401 std::string term;
402 if (!WriteLpTerm(var_index, coeff, &term)) {
403 return false;
404 }
405 line_breaker.Append(term);
406 show_variable[var_index] = coeff != 0.0 || show_variable[var_index];
407 }
408
409 const double lb = ct_proto.lower_bound();
410 const double ub = ct_proto.upper_bound();
411 if (lb == ub) {
412 line_breaker.Append(absl::StrCat(" = ", DoubleToString(ub), "\n"));
413 absl::StrAppend(output, " ", name, ": ", line_breaker.GetOutput());
414 } else {
415 if (ub != +kInfinity) {
416 std::string rhs_name = name;
417 if (lb != -kInfinity) {
418 absl::StrAppend(&rhs_name, "_rhs");
419 }
420 absl::StrAppend(output, " ", rhs_name, ": ", line_breaker.GetOutput());
421 const std::string relation =
422 absl::StrCat(" <= ", DoubleToString(ub), "\n");
423 // Here we have to make sure we do not add the relation to the contents
424 // of line_breaker, which may be used in the subsequent clause.
425 if (!line_breaker.WillFit(relation)) absl::StrAppend(output, "\n ");
426 absl::StrAppend(output, relation);
427 }
428 if (lb != -kInfinity) {
429 std::string lhs_name = name;
430 if (ub != +kInfinity) {
431 absl::StrAppend(&lhs_name, "_lhs");
432 }
433 absl::StrAppend(output, " ", lhs_name, ": ", line_breaker.GetOutput());
434 const std::string relation =
435 absl::StrCat(" >= ", DoubleToString(lb), "\n");
436 if (!line_breaker.WillFit(relation)) absl::StrAppend(output, "\n ");
437 absl::StrAppend(output, relation);
438 }
439 }
440
441 return true;
442}
443
444bool MPModelProtoExporter::WriteLpTerm(int var_index, double coefficient,
445 std::string* output) const {
446 output->clear();
447 if (var_index < 0 || var_index >= proto_.variable_size()) {
448 LOG(DFATAL) << "Reference to out-of-bounds variable index # " << var_index;
449 return false;
450 }
451 if (coefficient != 0.0) {
452 *output = absl::StrCat(DoubleToStringWithForcedSign(coefficient), " ",
453 exported_variable_names_[var_index], " ");
454 }
455 return true;
456}
457
458namespace {
459bool IsBoolean(const MPVariableProto& var) {
460 return var.is_integer() && ceil(var.lower_bound()) == 0.0 &&
461 floor(var.upper_bound()) == 1.0;
462}
463
464void UpdateMaxSize(const std::string& new_string, int* size) {
465 const int new_size = new_string.size();
466 if (new_size > *size) *size = new_size;
467}
468
469void UpdateMaxSize(double new_number, int* size) {
470 UpdateMaxSize(DoubleToString(new_number), size);
471}
472} // namespace
473
474void MPModelProtoExporter::Setup() {
475 num_binary_variables_ = 0;
476 num_integer_variables_ = 0;
477 for (const MPVariableProto& var : proto_.variable()) {
478 if (var.is_integer()) {
479 if (IsBoolean(var)) {
480 ++num_binary_variables_;
481 } else {
482 ++num_integer_variables_;
483 }
484 }
485 }
486 num_continuous_variables_ =
487 proto_.variable_size() - num_binary_variables_ - num_integer_variables_;
488}
489
490void MPModelProtoExporter::ComputeMpsSmartColumnWidths(bool obfuscated) {
491 // Minimum values for aesthetics (if columns are too narrow, MPS files are
492 // difficult to read).
493 int string_field_size = 6;
494 int number_field_size = 6;
495
496 for (const MPVariableProto& var : proto_.variable()) {
497 UpdateMaxSize(var.name(), &string_field_size);
498 UpdateMaxSize(var.objective_coefficient(), &number_field_size);
499 UpdateMaxSize(var.lower_bound(), &number_field_size);
500 UpdateMaxSize(var.upper_bound(), &number_field_size);
501 }
502
503 for (const MPConstraintProto& cst : proto_.constraint()) {
504 UpdateMaxSize(cst.name(), &string_field_size);
505 UpdateMaxSize(cst.lower_bound(), &number_field_size);
506 UpdateMaxSize(cst.upper_bound(), &number_field_size);
507 for (const double coeff : cst.coefficient()) {
508 UpdateMaxSize(coeff, &number_field_size);
509 }
510 }
511
512 // Maximum values for aesthetics. These are also the values used by other
513 // solvers.
514 string_field_size = std::min(string_field_size, 255);
515 number_field_size = std::min(number_field_size, 255);
516
517 // If the model is obfuscated, all names will have the same size, which we
518 // compute here.
519 if (obfuscated) {
520 int max_digits =
521 absl::StrCat(
522 std::max(proto_.variable_size(), proto_.constraint_size()) - 1)
523 .size();
524 string_field_size = std::max(6, max_digits + 1);
525 }
526
527 mps_header_format_ = absl::ParsedFormat<'s', 's'>::New(
528 absl::StrCat(" %-2s %-", string_field_size, "s"));
529 mps_format_ = absl::ParsedFormat<'s', 's'>::New(
530 absl::StrCat(" %-", string_field_size, "s %", number_field_size, "s"));
531}
532
533bool MPModelProtoExporter::ExportModelAsLpFormat(
534 const MPModelExportOptions& options, std::string* output) {
535 output->clear();
536 Setup();
537 const std::string kForbiddenFirstChars = "$.0123456789";
538 const std::string kForbiddenChars = " +-*/<>=:\\";
539 exported_constraint_names_ = ExtractAndProcessNames(
540 proto_.constraint(), "C", options.obfuscate, options.log_invalid_names,
541 kForbiddenFirstChars, kForbiddenChars);
542 exported_general_constraint_names_ = ExtractAndProcessNames(
543 proto_.general_constraint(), "C", options.obfuscate,
544 options.log_invalid_names, kForbiddenFirstChars, kForbiddenChars);
545 exported_variable_names_ = ExtractAndProcessNames(
546 proto_.variable(), "V", options.obfuscate, options.log_invalid_names,
547 kForbiddenFirstChars, kForbiddenChars);
548
549 // Comments section.
550 AppendComments("\\", output);
551 if (options.show_unused_variables) {
552 absl::StrAppendFormat(output, "\\ Unused variables are shown\n");
553 }
554
555 // Objective
556 absl::StrAppend(output, proto_.maximize() ? "Maximize\n" : "Minimize\n");
557 LineBreaker obj_line_breaker(options.max_line_length);
558 obj_line_breaker.Append(" Obj: ");
559 if (proto_.objective_offset() != 0.0) {
560 obj_line_breaker.Append(absl::StrCat(
561 DoubleToStringWithForcedSign(proto_.objective_offset()), " Constant "));
562 }
563 std::vector<bool> show_variable(proto_.variable_size(),
564 options.show_unused_variables);
565 for (int var_index = 0; var_index < proto_.variable_size(); ++var_index) {
566 const double coeff = proto_.variable(var_index).objective_coefficient();
567 std::string term;
568 if (!WriteLpTerm(var_index, coeff, &term)) {
569 return false;
570 }
571 obj_line_breaker.Append(term);
572 show_variable[var_index] = coeff != 0.0 || show_variable[var_index];
573 }
574 // Linear Constraints
575 absl::StrAppend(output, obj_line_breaker.GetOutput(), "\nSubject to\n");
576 for (int cst_index = 0; cst_index < proto_.constraint_size(); ++cst_index) {
577 const MPConstraintProto& ct_proto = proto_.constraint(cst_index);
578 const std::string& name = exported_constraint_names_[cst_index];
579 LineBreaker line_breaker(options.max_line_length);
580 const int kNumFormattingChars = 10; // Overevaluated.
581 // Account for the size of the constraint name + possibly "_rhs" +
582 // the formatting characters here.
583 line_breaker.Consume(kNumFormattingChars + name.size());
584 if (!AppendConstraint(ct_proto, name, line_breaker, show_variable,
585 output)) {
586 return false;
587 }
588 }
589
590 // General Constraints
591 for (int cst_index = 0; cst_index < proto_.general_constraint_size();
592 ++cst_index) {
593 const MPGeneralConstraintProto& ct_proto =
594 proto_.general_constraint(cst_index);
595 const std::string& name = exported_general_constraint_names_[cst_index];
596 LineBreaker line_breaker(options.max_line_length);
597 const int kNumFormattingChars = 10; // Overevaluated.
598 // Account for the size of the constraint name + possibly "_rhs" +
599 // the formatting characters here.
600 line_breaker.Consume(kNumFormattingChars + name.size());
601
602 if (!ct_proto.has_indicator_constraint()) return false;
603 const MPIndicatorConstraint& indicator_ct = ct_proto.indicator_constraint();
604 const int binary_var_index = indicator_ct.var_index();
605 const int binary_var_value = indicator_ct.var_value();
606 if (binary_var_index < 0 || binary_var_index >= proto_.variable_size()) {
607 return false;
608 }
609 show_variable[binary_var_index] = true;
610 line_breaker.Append(absl::StrFormat(
611 "%s = %d -> ", exported_variable_names_[binary_var_index],
612 binary_var_value));
613 if (!AppendConstraint(indicator_ct.constraint(), name, line_breaker,
614 show_variable, output)) {
615 return false;
616 }
617 }
618
619 // Bounds
620 absl::StrAppend(output, "Bounds\n");
621 if (proto_.objective_offset() != 0.0) {
622 absl::StrAppend(output, " 1 <= Constant <= 1\n");
623 }
624 for (int var_index = 0; var_index < proto_.variable_size(); ++var_index) {
625 if (!show_variable[var_index]) continue;
626 const MPVariableProto& var_proto = proto_.variable(var_index);
627 const double lb = var_proto.lower_bound();
628 const double ub = var_proto.upper_bound();
629 if (var_proto.is_integer() && lb == round(lb) && ub == round(ub)) {
630 absl::StrAppendFormat(output, " %.0f <= %s <= %.0f\n", lb,
631 exported_variable_names_[var_index], ub);
632 } else {
633 absl::StrAppend(output, " ");
634 if (lb == -kInfinity && ub == kInfinity) {
635 absl::StrAppend(output, exported_variable_names_[var_index], " free");
636 } else {
637 if (lb != -kInfinity) {
638 absl::StrAppend(output, DoubleToString(lb), " <= ");
639 }
640 absl::StrAppend(output, exported_variable_names_[var_index]);
641 if (ub != kInfinity) {
642 absl::StrAppend(output, " <= ", DoubleToString(ub));
643 }
644 }
645 absl::StrAppend(output, "\n");
646 }
647 }
648
649 // Binaries
650 if (num_binary_variables_ > 0) {
651 absl::StrAppend(output, "Binaries\n");
652 for (int var_index = 0; var_index < proto_.variable_size(); ++var_index) {
653 if (!show_variable[var_index]) continue;
654 const MPVariableProto& var_proto = proto_.variable(var_index);
655 if (IsBoolean(var_proto)) {
656 absl::StrAppendFormat(output, " %s\n",
657 exported_variable_names_[var_index]);
658 }
659 }
660 }
661
662 // Generals
663 if (num_integer_variables_ > 0) {
664 absl::StrAppend(output, "Generals\n");
665 for (int var_index = 0; var_index < proto_.variable_size(); ++var_index) {
666 if (!show_variable[var_index]) continue;
667 const MPVariableProto& var_proto = proto_.variable(var_index);
668 if (var_proto.is_integer() && !IsBoolean(var_proto)) {
669 absl::StrAppend(output, " ", exported_variable_names_[var_index], "\n");
670 }
671 }
672 }
673 absl::StrAppend(output, "End\n");
674 return true;
675}
676
677void MPModelProtoExporter::AppendMpsPair(const std::string& name, double value,
678 std::string* output) const {
679 absl::StrAppendFormat(output, *mps_format_, name, DoubleToString(value));
680}
681
682void MPModelProtoExporter::AppendMpsLineHeader(const std::string& id,
683 const std::string& name,
684 std::string* output) const {
685 absl::StrAppendFormat(output, *mps_header_format_, id, name);
686}
687
688void MPModelProtoExporter::AppendMpsLineHeaderWithNewLine(
689 const std::string& id, const std::string& name, std::string* output) const {
690 AppendMpsLineHeader(id, name, output);
691 absl::StripTrailingAsciiWhitespace(output);
692 absl::StrAppend(output, "\n");
693}
694
695void MPModelProtoExporter::AppendMpsTermWithContext(
696 const std::string& head_name, const std::string& name, double value,
697 std::string* output) {
698 if (current_mps_column_ == 0) {
699 AppendMpsLineHeader("", head_name, output);
700 }
701 AppendMpsPair(name, value, output);
702 AppendNewLineIfTwoColumns(output);
703}
704
705void MPModelProtoExporter::AppendMpsBound(const std::string& bound_type,
706 const std::string& name, double value,
707 std::string* output) const {
708 AppendMpsLineHeader(bound_type, "BOUND", output);
709 AppendMpsPair(name, value, output);
710 absl::StripTrailingAsciiWhitespace(output);
711 absl::StrAppend(output, "\n");
712}
713
714void MPModelProtoExporter::AppendNewLineIfTwoColumns(std::string* output) {
715 ++current_mps_column_;
716 if (current_mps_column_ == 2) {
717 absl::StripTrailingAsciiWhitespace(output);
718 absl::StrAppend(output, "\n");
719 current_mps_column_ = 0;
720 }
721}
722
723void MPModelProtoExporter::AppendMpsColumns(
724 bool integrality,
725 const std::vector<std::vector<std::pair<int, double>>>& transpose,
726 std::string* output) {
727 current_mps_column_ = 0;
728 for (int var_index = 0; var_index < proto_.variable_size(); ++var_index) {
729 const MPVariableProto& var_proto = proto_.variable(var_index);
730 if (var_proto.is_integer() != integrality) continue;
731 const std::string& var_name = exported_variable_names_[var_index];
732 current_mps_column_ = 0;
733 if (var_proto.objective_coefficient() != 0.0) {
734 AppendMpsTermWithContext(var_name, "COST",
735 var_proto.objective_coefficient(), output);
736 }
737 for (const std::pair<int, double>& cst_index_and_coeff :
738 transpose[var_index]) {
739 const std::string& cst_name =
740 exported_constraint_names_[cst_index_and_coeff.first];
741 AppendMpsTermWithContext(var_name, cst_name, cst_index_and_coeff.second,
742 output);
743 }
744 AppendNewLineIfTwoColumns(output);
745 }
746}
747
748bool MPModelProtoExporter::ExportModelAsMpsFormat(
749 const MPModelExportOptions& options, std::string* output) {
750 output->clear();
751 Setup();
752 ComputeMpsSmartColumnWidths(options.obfuscate);
753 const std::string kForbiddenFirstChars = "";
754 const std::string kForbiddenChars = " ";
755 exported_constraint_names_ = ExtractAndProcessNames(
756 proto_.constraint(), "C", options.obfuscate, options.log_invalid_names,
757 kForbiddenFirstChars, kForbiddenChars);
758 exported_variable_names_ = ExtractAndProcessNames(
759 proto_.variable(), "V", options.obfuscate, options.log_invalid_names,
760 kForbiddenFirstChars, kForbiddenChars);
761
762 // Comments.
763 AppendComments("*", output);
764
765 // NAME section.
766 // TODO(user): Obfuscate the model name too if `obfuscate` is true.
767 absl::StrAppendFormat(output, "%-14s%s\n", "NAME", proto_.name());
768
769 if (proto_.maximize()) {
770 absl::StrAppendFormat(output, "OBJSENSE\n MAX\n");
771 }
772
773 // ROWS section.
774 current_mps_column_ = 0;
775 std::string rows_section;
776 AppendMpsLineHeaderWithNewLine("N", "COST", &rows_section);
777 for (int cst_index = 0; cst_index < proto_.constraint_size(); ++cst_index) {
778 const MPConstraintProto& ct_proto = proto_.constraint(cst_index);
779 const double lb = ct_proto.lower_bound();
780 const double ub = ct_proto.upper_bound();
781 const std::string& cst_name = exported_constraint_names_[cst_index];
782 if (lb == -kInfinity && ub == kInfinity) {
783 AppendMpsLineHeaderWithNewLine("N", cst_name, &rows_section);
784 } else if (lb == ub) {
785 AppendMpsLineHeaderWithNewLine("E", cst_name, &rows_section);
786 } else if (lb == -kInfinity) {
787 AppendMpsLineHeaderWithNewLine("L", cst_name, &rows_section);
788 } else {
789 AppendMpsLineHeaderWithNewLine("G", cst_name, &rows_section);
790 }
791 }
792 if (!rows_section.empty()) {
793 absl::StrAppend(output, "ROWS\n", rows_section);
794 }
795
796 // As the information regarding a column needs to be contiguous, we create
797 // a vector associating a variable index to a vector containing the indices
798 // of the constraints where this variable appears.
799 std::vector<std::vector<std::pair<int, double>>> transpose(
800 proto_.variable_size());
801 for (int cst_index = 0; cst_index < proto_.constraint_size(); ++cst_index) {
802 const MPConstraintProto& ct_proto = proto_.constraint(cst_index);
803 for (int k = 0; k < ct_proto.var_index_size(); ++k) {
804 const int var_index = ct_proto.var_index(k);
805 if (var_index < 0 || var_index >= proto_.variable_size()) {
806 LOG(DFATAL) << "In constraint #" << cst_index << ", var_index #" << k
807 << " is " << var_index << ", which is out of bounds.";
808 return false;
809 }
810 const double coeff = ct_proto.coefficient(k);
811 if (coeff != 0.0) {
812 transpose[var_index].push_back(
813 std::pair<int, double>(cst_index, coeff));
814 }
815 }
816 }
817
818 // COLUMNS section.
819 std::string columns_section;
820 AppendMpsColumns(/*integrality=*/true, transpose, &columns_section);
821 if (!columns_section.empty()) {
822 constexpr const char kIntMarkerFormat[] = " %-10s%-36s%-8s\n";
823 columns_section =
824 absl::StrFormat(kIntMarkerFormat, "INTSTART", "'MARKER'", "'INTORG'") +
825 columns_section;
826 absl::StrAppendFormat(&columns_section, kIntMarkerFormat, "INTEND",
827 "'MARKER'", "'INTEND'");
828 }
829 AppendMpsColumns(/*integrality=*/false, transpose, &columns_section);
830 if (!columns_section.empty()) {
831 absl::StrAppend(output, "COLUMNS\n", columns_section);
832 }
833
834 // RHS (right-hand-side) section.
835 current_mps_column_ = 0;
836 std::string rhs_section;
837 // Follow Gurobi's MPS format for objective offsets.
838 // See https://www.gurobi.com/documentation/9.1/refman/mps_format.html
839 if (proto_.objective_offset() != 0) {
840 AppendMpsTermWithContext("RHS", "COST", -proto_.objective_offset(),
841 &rhs_section);
842 }
843 for (int cst_index = 0; cst_index < proto_.constraint_size(); ++cst_index) {
844 const MPConstraintProto& ct_proto = proto_.constraint(cst_index);
845 const double lb = ct_proto.lower_bound();
846 const double ub = ct_proto.upper_bound();
847 const std::string& cst_name = exported_constraint_names_[cst_index];
848 if (lb != -kInfinity) {
849 AppendMpsTermWithContext("RHS", cst_name, lb, &rhs_section);
850 } else if (ub != +kInfinity) {
851 AppendMpsTermWithContext("RHS", cst_name, ub, &rhs_section);
852 }
853 }
854 AppendNewLineIfTwoColumns(&rhs_section);
855 if (!rhs_section.empty()) {
856 absl::StrAppend(output, "RHS\n", rhs_section);
857 }
858
859 // RANGES section.
860 current_mps_column_ = 0;
861 std::string ranges_section;
862 for (int cst_index = 0; cst_index < proto_.constraint_size(); ++cst_index) {
863 const MPConstraintProto& ct_proto = proto_.constraint(cst_index);
864 const double range = fabs(ct_proto.upper_bound() - ct_proto.lower_bound());
865 if (range != 0.0 && range != +kInfinity) {
866 const std::string& cst_name = exported_constraint_names_[cst_index];
867 AppendMpsTermWithContext("RANGE", cst_name, range, &ranges_section);
868 }
869 }
870 AppendNewLineIfTwoColumns(&ranges_section);
871 if (!ranges_section.empty()) {
872 absl::StrAppend(output, "RANGES\n", ranges_section);
873 }
874
875 // BOUNDS section.
876 current_mps_column_ = 0;
877 std::string bounds_section;
878 for (int var_index = 0; var_index < proto_.variable_size(); ++var_index) {
879 const MPVariableProto& var_proto = proto_.variable(var_index);
880 const double lb = var_proto.lower_bound();
881 const double ub = var_proto.upper_bound();
882 const std::string& var_name = exported_variable_names_[var_index];
883
884 if (lb == -kInfinity && ub == +kInfinity) {
885 AppendMpsLineHeader("FR", "BOUND", &bounds_section);
886 absl::StrAppendFormat(&bounds_section, " %s\n", var_name);
887 continue;
888 }
889
890 if (var_proto.is_integer()) {
891 if (IsBoolean(var_proto)) {
892 AppendMpsLineHeader("BV", "BOUND", &bounds_section);
893 absl::StrAppendFormat(&bounds_section, " %s\n", var_name);
894 } else {
895 if (lb == ub) {
896 AppendMpsBound("FX", var_name, lb, &bounds_section);
897 } else {
898 if (lb == -kInfinity) {
899 AppendMpsLineHeader("MI", "BOUND", &bounds_section);
900 absl::StrAppendFormat(&bounds_section, " %s\n", var_name);
901 } else if (lb != 0.0 || ub == kInfinity) {
902 // "LI" can be skipped if it's 0.
903 // There is one exception to that rule: if UI=+inf, we can't skip
904 // LI=0 or the variable will be parsed as binary.
905 AppendMpsBound("LI", var_name, lb, &bounds_section);
906 }
907 if (ub != kInfinity) {
908 AppendMpsBound("UI", var_name, ub, &bounds_section);
909 }
910 }
911 }
912 } else {
913 if (lb == ub) {
914 AppendMpsBound("FX", var_name, lb, &bounds_section);
915 } else {
916 if (lb == -kInfinity) {
917 AppendMpsLineHeader("MI", "BOUND", &bounds_section);
918 absl::StrAppendFormat(&bounds_section, " %s\n", var_name);
919 } else if (lb != 0.0) {
920 AppendMpsBound("LO", var_name, lb, &bounds_section);
921 }
922 if (lb == 0.0 && ub == +kInfinity) {
923 AppendMpsLineHeader("PL", "BOUND", &bounds_section);
924 absl::StrAppendFormat(&bounds_section, " %s\n", var_name);
925 } else if (ub != +kInfinity) {
926 AppendMpsBound("UP", var_name, ub, &bounds_section);
927 }
928 }
929 }
930 }
931 if (!bounds_section.empty()) {
932 absl::StrAppend(output, "BOUNDS\n", bounds_section);
933 }
934
935 absl::StrAppend(output, "ENDATA\n");
936 return true;
937}
938
939} // namespace
940} // namespace operations_research
IntegerValue size
#define ASSIGN_OR_RETURN(lhs, rexpr)
CpModelProto proto
The output proto.
const std::string name
A name for logging purposes.
int64_t value
IntVar * var
GRBmodel * model
ABSL_RETIRED_FLAG(bool, lp_log_invalid_name, false, "DEPRECATED.")
absl::Status SetContents(absl::string_view filename, absl::string_view contents, Options options)
Definition file.cc:242
Options Defaults()
Definition file.h:109
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< std::string > ExportModelAsLpFormat(const MPModelProto &model, const MPModelExportOptions &options)
absl::StatusOr< std::string > ExportModelAsMpsFormat(const MPModelProto &model, const MPModelExportOptions &options)
int64_t coefficient
int var_index
Definition search.cc:3268
const std::optional< Range > & range
Definition statistics.cc:37