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