Files
ortools-clone/ortools/linear_solver/model_exporter.cc
2024-08-02 19:58:41 +02:00

941 lines
36 KiB
C++

// Copyright 2010-2024 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "ortools/linear_solver/model_exporter.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "absl/container/flat_hash_set.h"
#include "absl/flags/flag.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/ascii.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/string_view.h"
#include "ortools/base/helpers.h"
#include "ortools/base/logging.h"
#include "ortools/base/options.h"
#include "ortools/base/status_macros.h"
#include "ortools/linear_solver/linear_solver.pb.h"
ABSL_RETIRED_FLAG(bool, lp_log_invalid_name, false, "DEPRECATED.");
namespace operations_research {
namespace {
constexpr double kInfinity = std::numeric_limits<double>::infinity();
class LineBreaker {
public:
explicit LineBreaker(int max_line_size)
: max_line_size_(max_line_size), line_size_(0), output_() {}
// Lines are broken in such a way that:
// - Strings that are given to Append() are never split.
// - Lines are split so that their length doesn't exceed the max length;
// unless a single string given to Append() exceeds that length (in which
// case it will be put alone on a single unsplit line).
void Append(absl::string_view s);
// Returns true if string s will fit on the current line without adding a
// carriage return.
bool WillFit(const std::string& s) {
return line_size_ + static_cast<int>(s.size()) < max_line_size_;
}
// "Consumes" size characters on the line. Used when starting the constraint
// lines.
void Consume(int size) { line_size_ += size; }
std::string GetOutput() const { return output_; }
private:
int max_line_size_;
int line_size_;
std::string output_;
};
void LineBreaker::Append(absl::string_view s) {
line_size_ += s.size();
if (line_size_ > max_line_size_) {
line_size_ = s.size();
absl::StrAppend(&output_, "\n ");
}
absl::StrAppend(&output_, s);
}
class MPModelProtoExporter {
public:
explicit MPModelProtoExporter(const MPModelProto& model);
// This type is neither copyable nor movable.
MPModelProtoExporter(const MPModelProtoExporter&) = delete;
MPModelProtoExporter& operator=(const MPModelProtoExporter&) = delete;
bool ExportModelAsLpFormat(const MPModelExportOptions& options,
std::string* output);
bool ExportModelAsMpsFormat(const MPModelExportOptions& options,
std::string* output);
private:
// Computes the number of continuous, integer and binary variables.
// Called by ExportModelAsLpFormat() and ExportModelAsMpsFormat().
void Setup();
// Computes smart column widths for free MPS format.
void ComputeMpsSmartColumnWidths(bool obfuscated);
// Processes all the proto.name() fields and returns the result in a vector.
//
// If 'obfuscate' is true, none of names are actually used, and this just
// returns a vector of 'prefix' + proto index (1-based).
//
// If it is false, this tries to keep the original names, but:
// - if the first character is forbidden (or name is empty), '_' is added at
// the beginning of name.
// - all the other forbidden characters are replaced by '_'.
// To avoid name conflicts, a '_' followed by an integer is appended to the
// result.
//
// If a name is longer than the maximum allowed name length, the obfuscated
// name is used.
//
// Therefore, a name "$20<=40" for proto #3 could be "_$20__40_1".
template <class ListOfProtosWithNameFields>
std::vector<std::string> ExtractAndProcessNames(
const ListOfProtosWithNameFields& proto, absl::string_view prefix,
bool obfuscate, bool log_invalid_names,
const std::string& forbidden_first_chars,
const std::string& forbidden_chars);
// Appends a general "Comment" section with useful metadata about the model
// to "output".
// Note(user): there may be less variables in output than in the original
// model, as unused variables are not shown by default. Similarly, there
// may be more constraints in a .lp file as in the original model as
// a constraint lhs <= term <= rhs will be output as the two constraints
// term >= lhs and term <= rhs.
void AppendComments(const std::string& separator, std::string* output) const;
// Appends an MPConstraintProto to the output text. If the constraint has
// both an upper and lower bound that are not equal, it splits the constraint
// into two constraints, one for the left hand side (_lhs) and one for right
// hand side (_rhs).
bool AppendConstraint(const MPConstraintProto& ct_proto,
const std::string& name, LineBreaker& line_breaker,
std::vector<bool>& show_variable, std::string* output);
// Clears "output" and writes a term to it, in "LP" format. Returns false on
// error (for example, var_index is out of range).
bool WriteLpTerm(int var_index, double coefficient,
std::string* output) const;
// Appends a pair name, value to "output", formatted to comply with the MPS
// standard.
void AppendMpsPair(const std::string& name, double value,
std::string* output) const;
// Appends the head of a line, consisting of an id and a name to output.
void AppendMpsLineHeader(const std::string& id, const std::string& name,
std::string* output) const;
// Same as AppendMpsLineHeader. Appends an extra new-line at the end the
// string pointed to by output.
void AppendMpsLineHeaderWithNewLine(const std::string& id,
const std::string& name,
std::string* output) const;
// Appends an MPS term in various contexts. The term consists of a head name,
// a name, and a value. If the line is not empty, then only the pair
// (name, value) is appended. The number of columns, limited to 2 by the MPS
// format is also taken care of.
void AppendMpsTermWithContext(const std::string& head_name,
const std::string& name, double value,
std::string* output);
// Appends a new-line if two columns are already present on the MPS line.
// Used by and in complement to AppendMpsTermWithContext.
void AppendNewLineIfTwoColumns(std::string* output);
// When 'integrality' is true, appends columns corresponding to integer
// variables. Appends the columns for non-integer variables otherwise.
// The sparse matrix must be passed as a vector of columns ('transpose').
void AppendMpsColumns(
bool integrality,
const std::vector<std::vector<std::pair<int, double>>>& transpose,
std::string* output);
// Appends a line describing the bound of a variablenew-line if two columns
// are already present on the MPS line.
// Used by and in complement to AppendMpsTermWithContext.
void AppendMpsBound(const std::string& bound_type, const std::string& name,
double value, std::string* output) const;
const MPModelProto& proto_;
// Vector of variable names as they will be exported.
std::vector<std::string> exported_variable_names_;
// Vector of constraint names as they will be exported.
std::vector<std::string> exported_constraint_names_;
// Vector of general constraint names as they will be exported.
std::vector<std::string> exported_general_constraint_names_;
// Number of integer variables in proto_.
int num_integer_variables_;
// Number of binary variables in proto_.
int num_binary_variables_;
// Number of continuous variables in proto_.
int num_continuous_variables_;
// Current MPS file column number.
int current_mps_column_;
// Format for MPS file lines.
std::unique_ptr<absl::ParsedFormat<'s', 's'>> mps_header_format_;
std::unique_ptr<absl::ParsedFormat<'s', 's'>> mps_format_;
};
} // namespace
absl::StatusOr<std::string> ExportModelAsLpFormat(
const MPModelProto& model, const MPModelExportOptions& options) {
for (const MPGeneralConstraintProto& general_constraint :
model.general_constraint()) {
if (!general_constraint.has_indicator_constraint()) {
return absl::InvalidArgumentError(
"Non-indicator general constraints are not supported.");
}
}
MPModelProtoExporter exporter(model);
std::string output;
if (!exporter.ExportModelAsLpFormat(options, &output)) {
return absl::InvalidArgumentError("Unable to export model.");
}
return output;
}
absl::StatusOr<std::string> ExportModelAsMpsFormat(
const MPModelProto& model, const MPModelExportOptions& options) {
if (model.general_constraint_size() > 0) {
return absl::InvalidArgumentError("General constraints are not supported.");
}
MPModelProtoExporter exporter(model);
std::string output;
if (!exporter.ExportModelAsMpsFormat(options, &output)) {
return absl::InvalidArgumentError("Unable to export model.");
}
return output;
}
absl::Status WriteModelToMpsFile(absl::string_view filename,
const MPModelProto& model,
const MPModelExportOptions& options) {
ASSIGN_OR_RETURN(std::string mps_data,
ExportModelAsMpsFormat(model, options));
return file::SetContents(filename, mps_data, file::Defaults());
}
namespace {
MPModelProtoExporter::MPModelProtoExporter(const MPModelProto& model)
: proto_(model),
num_integer_variables_(0),
num_binary_variables_(0),
num_continuous_variables_(0),
current_mps_column_(0) {}
namespace {
class NameManager {
public:
NameManager() : names_set_(), last_n_(1) {}
std::string MakeUniqueName(absl::string_view name);
private:
absl::flat_hash_set<std::string> names_set_;
int last_n_;
};
std::string NameManager::MakeUniqueName(absl::string_view name) {
std::string result(name);
// Find the 'n' so that "name_n" does not already exist.
int n = last_n_;
while (!names_set_.insert(result).second) {
result = absl::StrCat(name, "_", n);
++n;
}
// We keep the last n used to avoid a quadratic behavior in case
// all the names are the same initially.
last_n_ = n;
return result;
}
// NOTE: As a special case, an empty name is treated as started with a forbidden
// character (\0).
std::string MakeExportableName(const std::string& name,
const std::string& forbidden_first_chars,
const std::string& forbidden_chars,
bool* found_forbidden_char) {
// Prepend with "_" all the names starting with a forbidden character.
*found_forbidden_char =
name.empty() || absl::StrContains(forbidden_first_chars, name[0]);
std::string exportable_name =
*found_forbidden_char ? absl::StrCat("_", name) : name;
// Replace all the other forbidden characters with "_".
for (char& c : exportable_name) {
if (absl::StrContains(forbidden_chars, c)) {
c = '_';
*found_forbidden_char = true;
}
}
return exportable_name;
}
} // namespace
template <class ListOfProtosWithNameFields>
std::vector<std::string> MPModelProtoExporter::ExtractAndProcessNames(
const ListOfProtosWithNameFields& proto, absl::string_view prefix,
bool obfuscate, bool log_invalid_names,
const std::string& forbidden_first_chars,
const std::string& forbidden_chars) {
const int num_items = proto.size();
std::vector<std::string> result(num_items);
NameManager namer;
const int num_digits = absl::StrCat(num_items).size();
int i = 0;
for (const auto& item : proto) {
const std::string obfuscated_name =
absl::StrFormat("%s%0*d", prefix, num_digits, i);
if (obfuscate || !item.has_name()) {
result[i] = namer.MakeUniqueName(obfuscated_name);
LOG_IF(WARNING, log_invalid_names && !item.has_name())
<< "Empty name detected, created new name: " << result[i];
} else {
bool found_forbidden_char = false;
const std::string exportable_name =
MakeExportableName(item.name(), forbidden_first_chars,
forbidden_chars, &found_forbidden_char);
result[i] = namer.MakeUniqueName(exportable_name);
LOG_IF(WARNING, log_invalid_names && found_forbidden_char)
<< "Invalid character detected in " << item.name() << ". Changed to "
<< result[i];
// If the name is too long, use the obfuscated name that is guaranteed
// to fit. If ever we are able to solve problems with 2^64 variables,
// their obfuscated names would fit within 20 characters.
const int kMaxNameLength = 255;
// Take care of "_rhs" or "_lhs" that may be added in the case of
// constraints with both right-hand side and left-hand side.
const int kMargin = 4;
if (result[i].size() > kMaxNameLength - kMargin) {
const std::string old_name = std::move(result[i]);
result[i] = namer.MakeUniqueName(obfuscated_name);
LOG_IF(WARNING, log_invalid_names) << "Name is too long: " << old_name
<< " exported as: " << result[i];
}
}
// Prepare for the next round.
++i;
}
return result;
}
void MPModelProtoExporter::AppendComments(const std::string& separator,
std::string* output) const {
const char* const sep = separator.c_str();
absl::StrAppendFormat(output, "%s Generated by MPModelProtoExporter\n", sep);
absl::StrAppendFormat(output, "%s %-16s : %s\n", sep, "Name",
proto_.has_name() ? proto_.name().c_str() : "NoName");
absl::StrAppendFormat(output, "%s %-16s : %s\n", sep, "Format", "Free");
absl::StrAppendFormat(
output, "%s %-16s : %d\n", sep, "Constraints",
proto_.constraint_size() + proto_.general_constraint_size());
absl::StrAppendFormat(output, "%s %-16s : %d\n", sep, "Variables",
proto_.variable_size());
absl::StrAppendFormat(output, "%s %-14s : %d\n", sep, "Binary",
num_binary_variables_);
absl::StrAppendFormat(output, "%s %-14s : %d\n", sep, "Integer",
num_integer_variables_);
absl::StrAppendFormat(output, "%s %-14s : %d\n", sep, "Continuous",
num_continuous_variables_);
}
namespace {
std::string DoubleToStringWithForcedSign(double d) {
return absl::StrCat((d < 0 ? "" : "+"), (d));
}
std::string DoubleToString(double d) { return absl::StrCat((d)); }
} // namespace
bool MPModelProtoExporter::AppendConstraint(const MPConstraintProto& ct_proto,
const std::string& name,
LineBreaker& line_breaker,
std::vector<bool>& show_variable,
std::string* output) {
for (int i = 0; i < ct_proto.var_index_size(); ++i) {
const int var_index = ct_proto.var_index(i);
const double coeff = ct_proto.coefficient(i);
std::string term;
if (!WriteLpTerm(var_index, coeff, &term)) {
return false;
}
line_breaker.Append(term);
show_variable[var_index] = coeff != 0.0 || show_variable[var_index];
}
const double lb = ct_proto.lower_bound();
const double ub = ct_proto.upper_bound();
if (lb == ub) {
line_breaker.Append(absl::StrCat(" = ", DoubleToString(ub), "\n"));
absl::StrAppend(output, " ", name, ": ", line_breaker.GetOutput());
} else {
if (ub != +kInfinity) {
std::string rhs_name = name;
if (lb != -kInfinity) {
absl::StrAppend(&rhs_name, "_rhs");
}
absl::StrAppend(output, " ", rhs_name, ": ", line_breaker.GetOutput());
const std::string relation =
absl::StrCat(" <= ", DoubleToString(ub), "\n");
// Here we have to make sure we do not add the relation to the contents
// of line_breaker, which may be used in the subsequent clause.
if (!line_breaker.WillFit(relation)) absl::StrAppend(output, "\n ");
absl::StrAppend(output, relation);
}
if (lb != -kInfinity) {
std::string lhs_name = name;
if (ub != +kInfinity) {
absl::StrAppend(&lhs_name, "_lhs");
}
absl::StrAppend(output, " ", lhs_name, ": ", line_breaker.GetOutput());
const std::string relation =
absl::StrCat(" >= ", DoubleToString(lb), "\n");
if (!line_breaker.WillFit(relation)) absl::StrAppend(output, "\n ");
absl::StrAppend(output, relation);
}
}
return true;
}
bool MPModelProtoExporter::WriteLpTerm(int var_index, double coefficient,
std::string* output) const {
output->clear();
if (var_index < 0 || var_index >= proto_.variable_size()) {
LOG(DFATAL) << "Reference to out-of-bounds variable index # " << var_index;
return false;
}
if (coefficient != 0.0) {
*output = absl::StrCat(DoubleToStringWithForcedSign(coefficient), " ",
exported_variable_names_[var_index], " ");
}
return true;
}
namespace {
bool IsBoolean(const MPVariableProto& var) {
return var.is_integer() && ceil(var.lower_bound()) == 0.0 &&
floor(var.upper_bound()) == 1.0;
}
void UpdateMaxSize(const std::string& new_string, int* size) {
const int new_size = new_string.size();
if (new_size > *size) *size = new_size;
}
void UpdateMaxSize(double new_number, int* size) {
UpdateMaxSize(DoubleToString(new_number), size);
}
} // namespace
void MPModelProtoExporter::Setup() {
num_binary_variables_ = 0;
num_integer_variables_ = 0;
for (const MPVariableProto& var : proto_.variable()) {
if (var.is_integer()) {
if (IsBoolean(var)) {
++num_binary_variables_;
} else {
++num_integer_variables_;
}
}
}
num_continuous_variables_ =
proto_.variable_size() - num_binary_variables_ - num_integer_variables_;
}
void MPModelProtoExporter::ComputeMpsSmartColumnWidths(bool obfuscated) {
// Minimum values for aesthetics (if columns are too narrow, MPS files are
// difficult to read).
int string_field_size = 6;
int number_field_size = 6;
for (const MPVariableProto& var : proto_.variable()) {
UpdateMaxSize(var.name(), &string_field_size);
UpdateMaxSize(var.objective_coefficient(), &number_field_size);
UpdateMaxSize(var.lower_bound(), &number_field_size);
UpdateMaxSize(var.upper_bound(), &number_field_size);
}
for (const MPConstraintProto& cst : proto_.constraint()) {
UpdateMaxSize(cst.name(), &string_field_size);
UpdateMaxSize(cst.lower_bound(), &number_field_size);
UpdateMaxSize(cst.upper_bound(), &number_field_size);
for (const double coeff : cst.coefficient()) {
UpdateMaxSize(coeff, &number_field_size);
}
}
// Maximum values for aesthetics. These are also the values used by other
// solvers.
string_field_size = std::min(string_field_size, 255);
number_field_size = std::min(number_field_size, 255);
// If the model is obfuscated, all names will have the same size, which we
// compute here.
if (obfuscated) {
int max_digits =
absl::StrCat(
std::max(proto_.variable_size(), proto_.constraint_size()) - 1)
.size();
string_field_size = std::max(6, max_digits + 1);
}
mps_header_format_ = absl::ParsedFormat<'s', 's'>::New(
absl::StrCat(" %-2s %-", string_field_size, "s"));
mps_format_ = absl::ParsedFormat<'s', 's'>::New(
absl::StrCat(" %-", string_field_size, "s %", number_field_size, "s"));
}
bool MPModelProtoExporter::ExportModelAsLpFormat(
const MPModelExportOptions& options, std::string* output) {
output->clear();
Setup();
const std::string kForbiddenFirstChars = "$.0123456789";
const std::string kForbiddenChars = " +-*/<>=:\\";
exported_constraint_names_ = ExtractAndProcessNames(
proto_.constraint(), "C", options.obfuscate, options.log_invalid_names,
kForbiddenFirstChars, kForbiddenChars);
exported_general_constraint_names_ = ExtractAndProcessNames(
proto_.general_constraint(), "C", options.obfuscate,
options.log_invalid_names, kForbiddenFirstChars, kForbiddenChars);
exported_variable_names_ = ExtractAndProcessNames(
proto_.variable(), "V", options.obfuscate, options.log_invalid_names,
kForbiddenFirstChars, kForbiddenChars);
// Comments section.
AppendComments("\\", output);
if (options.show_unused_variables) {
absl::StrAppendFormat(output, "\\ Unused variables are shown\n");
}
// Objective
absl::StrAppend(output, proto_.maximize() ? "Maximize\n" : "Minimize\n");
LineBreaker obj_line_breaker(options.max_line_length);
obj_line_breaker.Append(" Obj: ");
if (proto_.objective_offset() != 0.0) {
obj_line_breaker.Append(absl::StrCat(
DoubleToStringWithForcedSign(proto_.objective_offset()), " Constant "));
}
std::vector<bool> show_variable(proto_.variable_size(),
options.show_unused_variables);
for (int var_index = 0; var_index < proto_.variable_size(); ++var_index) {
const double coeff = proto_.variable(var_index).objective_coefficient();
std::string term;
if (!WriteLpTerm(var_index, coeff, &term)) {
return false;
}
obj_line_breaker.Append(term);
show_variable[var_index] = coeff != 0.0 || show_variable[var_index];
}
// Linear Constraints
absl::StrAppend(output, obj_line_breaker.GetOutput(), "\nSubject to\n");
for (int cst_index = 0; cst_index < proto_.constraint_size(); ++cst_index) {
const MPConstraintProto& ct_proto = proto_.constraint(cst_index);
const std::string& name = exported_constraint_names_[cst_index];
LineBreaker line_breaker(options.max_line_length);
const int kNumFormattingChars = 10; // Overevaluated.
// Account for the size of the constraint name + possibly "_rhs" +
// the formatting characters here.
line_breaker.Consume(kNumFormattingChars + name.size());
if (!AppendConstraint(ct_proto, name, line_breaker, show_variable,
output)) {
return false;
}
}
// General Constraints
for (int cst_index = 0; cst_index < proto_.general_constraint_size();
++cst_index) {
const MPGeneralConstraintProto& ct_proto =
proto_.general_constraint(cst_index);
const std::string& name = exported_general_constraint_names_[cst_index];
LineBreaker line_breaker(options.max_line_length);
const int kNumFormattingChars = 10; // Overevaluated.
// Account for the size of the constraint name + possibly "_rhs" +
// the formatting characters here.
line_breaker.Consume(kNumFormattingChars + name.size());
if (!ct_proto.has_indicator_constraint()) return false;
const MPIndicatorConstraint& indicator_ct = ct_proto.indicator_constraint();
const int binary_var_index = indicator_ct.var_index();
const int binary_var_value = indicator_ct.var_value();
if (binary_var_index < 0 || binary_var_index >= proto_.variable_size()) {
return false;
}
show_variable[binary_var_index] = true;
line_breaker.Append(absl::StrFormat(
"%s = %d -> ", exported_variable_names_[binary_var_index],
binary_var_value));
if (!AppendConstraint(indicator_ct.constraint(), name, line_breaker,
show_variable, output)) {
return false;
}
}
// Bounds
absl::StrAppend(output, "Bounds\n");
if (proto_.objective_offset() != 0.0) {
absl::StrAppend(output, " 1 <= Constant <= 1\n");
}
for (int var_index = 0; var_index < proto_.variable_size(); ++var_index) {
if (!show_variable[var_index]) continue;
const MPVariableProto& var_proto = proto_.variable(var_index);
const double lb = var_proto.lower_bound();
const double ub = var_proto.upper_bound();
if (var_proto.is_integer() && lb == round(lb) && ub == round(ub)) {
absl::StrAppendFormat(output, " %.0f <= %s <= %.0f\n", lb,
exported_variable_names_[var_index], ub);
} else {
absl::StrAppend(output, " ");
if (lb == -kInfinity && ub == kInfinity) {
absl::StrAppend(output, exported_variable_names_[var_index], " free");
} else {
if (lb != -kInfinity) {
absl::StrAppend(output, DoubleToString(lb), " <= ");
}
absl::StrAppend(output, exported_variable_names_[var_index]);
if (ub != kInfinity) {
absl::StrAppend(output, " <= ", DoubleToString(ub));
}
}
absl::StrAppend(output, "\n");
}
}
// Binaries
if (num_binary_variables_ > 0) {
absl::StrAppend(output, "Binaries\n");
for (int var_index = 0; var_index < proto_.variable_size(); ++var_index) {
if (!show_variable[var_index]) continue;
const MPVariableProto& var_proto = proto_.variable(var_index);
if (IsBoolean(var_proto)) {
absl::StrAppendFormat(output, " %s\n",
exported_variable_names_[var_index]);
}
}
}
// Generals
if (num_integer_variables_ > 0) {
absl::StrAppend(output, "Generals\n");
for (int var_index = 0; var_index < proto_.variable_size(); ++var_index) {
if (!show_variable[var_index]) continue;
const MPVariableProto& var_proto = proto_.variable(var_index);
if (var_proto.is_integer() && !IsBoolean(var_proto)) {
absl::StrAppend(output, " ", exported_variable_names_[var_index], "\n");
}
}
}
absl::StrAppend(output, "End\n");
return true;
}
void MPModelProtoExporter::AppendMpsPair(const std::string& name, double value,
std::string* output) const {
absl::StrAppendFormat(output, *mps_format_, name, DoubleToString(value));
}
void MPModelProtoExporter::AppendMpsLineHeader(const std::string& id,
const std::string& name,
std::string* output) const {
absl::StrAppendFormat(output, *mps_header_format_, id, name);
}
void MPModelProtoExporter::AppendMpsLineHeaderWithNewLine(
const std::string& id, const std::string& name, std::string* output) const {
AppendMpsLineHeader(id, name, output);
absl::StripTrailingAsciiWhitespace(output);
absl::StrAppend(output, "\n");
}
void MPModelProtoExporter::AppendMpsTermWithContext(
const std::string& head_name, const std::string& name, double value,
std::string* output) {
if (current_mps_column_ == 0) {
AppendMpsLineHeader("", head_name, output);
}
AppendMpsPair(name, value, output);
AppendNewLineIfTwoColumns(output);
}
void MPModelProtoExporter::AppendMpsBound(const std::string& bound_type,
const std::string& name, double value,
std::string* output) const {
AppendMpsLineHeader(bound_type, "BOUND", output);
AppendMpsPair(name, value, output);
absl::StripTrailingAsciiWhitespace(output);
absl::StrAppend(output, "\n");
}
void MPModelProtoExporter::AppendNewLineIfTwoColumns(std::string* output) {
++current_mps_column_;
if (current_mps_column_ == 2) {
absl::StripTrailingAsciiWhitespace(output);
absl::StrAppend(output, "\n");
current_mps_column_ = 0;
}
}
void MPModelProtoExporter::AppendMpsColumns(
bool integrality,
const std::vector<std::vector<std::pair<int, double>>>& transpose,
std::string* output) {
current_mps_column_ = 0;
for (int var_index = 0; var_index < proto_.variable_size(); ++var_index) {
const MPVariableProto& var_proto = proto_.variable(var_index);
if (var_proto.is_integer() != integrality) continue;
const std::string& var_name = exported_variable_names_[var_index];
current_mps_column_ = 0;
if (var_proto.objective_coefficient() != 0.0) {
AppendMpsTermWithContext(var_name, "COST",
var_proto.objective_coefficient(), output);
}
for (const std::pair<int, double>& cst_index_and_coeff :
transpose[var_index]) {
const std::string& cst_name =
exported_constraint_names_[cst_index_and_coeff.first];
AppendMpsTermWithContext(var_name, cst_name, cst_index_and_coeff.second,
output);
}
AppendNewLineIfTwoColumns(output);
}
}
bool MPModelProtoExporter::ExportModelAsMpsFormat(
const MPModelExportOptions& options, std::string* output) {
output->clear();
Setup();
ComputeMpsSmartColumnWidths(options.obfuscate);
const std::string kForbiddenFirstChars = "";
const std::string kForbiddenChars = " ";
exported_constraint_names_ = ExtractAndProcessNames(
proto_.constraint(), "C", options.obfuscate, options.log_invalid_names,
kForbiddenFirstChars, kForbiddenChars);
exported_variable_names_ = ExtractAndProcessNames(
proto_.variable(), "V", options.obfuscate, options.log_invalid_names,
kForbiddenFirstChars, kForbiddenChars);
// Comments.
AppendComments("*", output);
// NAME section.
// TODO(user): Obfuscate the model name too if `obfuscate` is true.
absl::StrAppendFormat(output, "%-14s%s\n", "NAME", proto_.name());
if (proto_.maximize()) {
absl::StrAppendFormat(output, "OBJSENSE\n MAX\n");
}
// ROWS section.
current_mps_column_ = 0;
std::string rows_section;
AppendMpsLineHeaderWithNewLine("N", "COST", &rows_section);
for (int cst_index = 0; cst_index < proto_.constraint_size(); ++cst_index) {
const MPConstraintProto& ct_proto = proto_.constraint(cst_index);
const double lb = ct_proto.lower_bound();
const double ub = ct_proto.upper_bound();
const std::string& cst_name = exported_constraint_names_[cst_index];
if (lb == -kInfinity && ub == kInfinity) {
AppendMpsLineHeaderWithNewLine("N", cst_name, &rows_section);
} else if (lb == ub) {
AppendMpsLineHeaderWithNewLine("E", cst_name, &rows_section);
} else if (lb == -kInfinity) {
AppendMpsLineHeaderWithNewLine("L", cst_name, &rows_section);
} else {
AppendMpsLineHeaderWithNewLine("G", cst_name, &rows_section);
}
}
if (!rows_section.empty()) {
absl::StrAppend(output, "ROWS\n", rows_section);
}
// As the information regarding a column needs to be contiguous, we create
// a vector associating a variable index to a vector containing the indices
// of the constraints where this variable appears.
std::vector<std::vector<std::pair<int, double>>> transpose(
proto_.variable_size());
for (int cst_index = 0; cst_index < proto_.constraint_size(); ++cst_index) {
const MPConstraintProto& ct_proto = proto_.constraint(cst_index);
for (int k = 0; k < ct_proto.var_index_size(); ++k) {
const int var_index = ct_proto.var_index(k);
if (var_index < 0 || var_index >= proto_.variable_size()) {
LOG(DFATAL) << "In constraint #" << cst_index << ", var_index #" << k
<< " is " << var_index << ", which is out of bounds.";
return false;
}
const double coeff = ct_proto.coefficient(k);
if (coeff != 0.0) {
transpose[var_index].push_back(
std::pair<int, double>(cst_index, coeff));
}
}
}
// COLUMNS section.
std::string columns_section;
AppendMpsColumns(/*integrality=*/true, transpose, &columns_section);
if (!columns_section.empty()) {
constexpr const char kIntMarkerFormat[] = " %-10s%-36s%-8s\n";
columns_section =
absl::StrFormat(kIntMarkerFormat, "INTSTART", "'MARKER'", "'INTORG'") +
columns_section;
absl::StrAppendFormat(&columns_section, kIntMarkerFormat, "INTEND",
"'MARKER'", "'INTEND'");
}
AppendMpsColumns(/*integrality=*/false, transpose, &columns_section);
if (!columns_section.empty()) {
absl::StrAppend(output, "COLUMNS\n", columns_section);
}
// RHS (right-hand-side) section.
current_mps_column_ = 0;
std::string rhs_section;
// Follow Gurobi's MPS format for objective offsets.
// See https://www.gurobi.com/documentation/9.1/refman/mps_format.html
if (proto_.objective_offset() != 0) {
AppendMpsTermWithContext("RHS", "COST", -proto_.objective_offset(),
&rhs_section);
}
for (int cst_index = 0; cst_index < proto_.constraint_size(); ++cst_index) {
const MPConstraintProto& ct_proto = proto_.constraint(cst_index);
const double lb = ct_proto.lower_bound();
const double ub = ct_proto.upper_bound();
const std::string& cst_name = exported_constraint_names_[cst_index];
if (lb != -kInfinity) {
AppendMpsTermWithContext("RHS", cst_name, lb, &rhs_section);
} else if (ub != +kInfinity) {
AppendMpsTermWithContext("RHS", cst_name, ub, &rhs_section);
}
}
AppendNewLineIfTwoColumns(&rhs_section);
if (!rhs_section.empty()) {
absl::StrAppend(output, "RHS\n", rhs_section);
}
// RANGES section.
current_mps_column_ = 0;
std::string ranges_section;
for (int cst_index = 0; cst_index < proto_.constraint_size(); ++cst_index) {
const MPConstraintProto& ct_proto = proto_.constraint(cst_index);
const double range = fabs(ct_proto.upper_bound() - ct_proto.lower_bound());
if (range != 0.0 && range != +kInfinity) {
const std::string& cst_name = exported_constraint_names_[cst_index];
AppendMpsTermWithContext("RANGE", cst_name, range, &ranges_section);
}
}
AppendNewLineIfTwoColumns(&ranges_section);
if (!ranges_section.empty()) {
absl::StrAppend(output, "RANGES\n", ranges_section);
}
// BOUNDS section.
current_mps_column_ = 0;
std::string bounds_section;
for (int var_index = 0; var_index < proto_.variable_size(); ++var_index) {
const MPVariableProto& var_proto = proto_.variable(var_index);
const double lb = var_proto.lower_bound();
const double ub = var_proto.upper_bound();
const std::string& var_name = exported_variable_names_[var_index];
if (lb == -kInfinity && ub == +kInfinity) {
AppendMpsLineHeader("FR", "BOUND", &bounds_section);
absl::StrAppendFormat(&bounds_section, " %s\n", var_name);
continue;
}
if (var_proto.is_integer()) {
if (IsBoolean(var_proto)) {
AppendMpsLineHeader("BV", "BOUND", &bounds_section);
absl::StrAppendFormat(&bounds_section, " %s\n", var_name);
} else {
if (lb == ub) {
AppendMpsBound("FX", var_name, lb, &bounds_section);
} else {
if (lb == -kInfinity) {
AppendMpsLineHeader("MI", "BOUND", &bounds_section);
absl::StrAppendFormat(&bounds_section, " %s\n", var_name);
} else if (lb != 0.0 || ub == kInfinity) {
// "LI" can be skipped if it's 0.
// There is one exception to that rule: if UI=+inf, we can't skip
// LI=0 or the variable will be parsed as binary.
AppendMpsBound("LI", var_name, lb, &bounds_section);
}
if (ub != kInfinity) {
AppendMpsBound("UI", var_name, ub, &bounds_section);
}
}
}
} else {
if (lb == ub) {
AppendMpsBound("FX", var_name, lb, &bounds_section);
} else {
if (lb == -kInfinity) {
AppendMpsLineHeader("MI", "BOUND", &bounds_section);
absl::StrAppendFormat(&bounds_section, " %s\n", var_name);
} else if (lb != 0.0) {
AppendMpsBound("LO", var_name, lb, &bounds_section);
}
if (lb == 0.0 && ub == +kInfinity) {
AppendMpsLineHeader("PL", "BOUND", &bounds_section);
absl::StrAppendFormat(&bounds_section, " %s\n", var_name);
} else if (ub != +kInfinity) {
AppendMpsBound("UP", var_name, ub, &bounds_section);
}
}
}
}
if (!bounds_section.empty()) {
absl::StrAppend(output, "BOUNDS\n", bounds_section);
}
absl::StrAppend(output, "ENDATA\n");
return true;
}
} // namespace
} // namespace operations_research