Files
ortools-clone/ortools/lp_data/mps_reader_template.h
2025-02-24 22:59:21 +01:00

1258 lines
53 KiB
C++

// Copyright 2010-2025 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.
#ifndef OR_TOOLS_LP_DATA_MPS_READER_TEMPLATE_H_
#define OR_TOOLS_LP_DATA_MPS_READER_TEMPLATE_H_
// A templated-reader for MPS (Mathematical Programming System) format.
//
// From Wikipedia https://en.wikipedia.org/wiki/MPS_(format):
// ```
// The format was named after an early IBM LP product[1] and has emerged as a de
// facto standard ASCII medium among most of the commercial LP solvers.
//
// MPS is column-oriented (as opposed to entering the model as equations), and
// all model components (variables, rows, etc.) receive names. MPS is an old
// format, so it is set up for punch cards: Fields start in column 2, 5, 15, 25,
// 40 and 50. Sections of an MPS file are marked by so-called header cards,
// which are distinguished by their starting in column 1. Although it is typical
// to use upper-case throughout the file for historical reasons, many
// MPS-readers will accept mixed-case for anything except the header cards, and
// some allow mixed-case anywhere. The names that you choose for the individual
// entities (constraints or variables) are not important to the solver; one
// should pick meaningful names, or easy names for a post-processing code to
// read.
// ```
//
// For example:
// ```
// NAME TESTPROB
// ROWS
// N COST
// L LIM1
// G LIM2
// E MYEQN
// COLUMNS
// XONE COST 1 LIM1 1
// XONE LIM2 1
// YTWO COST 4 LIM1 1
// YTWO MYEQN -1
// ZTHREE COST 9 LIM2 1
// ZTHREE MYEQN 1
// RHS
// RHS1 LIM1 5 LIM2 10
// RHS1 MYEQN 7
// BOUNDS
// UP BND1 XONE 4
// LO BND1 YTWO -1
// UP BND1 YTWO 1
// ENDATA
// ```
//
// Note that the example, and the previous paragraph, mention that data must
// start at given columns in the text. This is commonly referred to as 'fixed'
// (width) format. In this version of the format, variable and constraint names
// can contain white space, but they are limited to a maximum width of eight
// characters, and each `section` marker must start at column 1.
//
// A common alternative is the so-called `free` format; where names
// can have (in principle) arbitrary length, but no white space, and where each
// data or section line can start with or without white space.
// In both cases the number of fields in each line remain unchanged.
// This implementation supports both `fixed` and `free` (width) format.
//
// TODO(b/284163180): The current behavior is that in free format header lines
// do not start with white space, and data lines must start with at least one
// white space.
//
// Although there is no `one` format (as many solvers have expanded
// it over time to support their own generalizations to MIP; i.e. Mixed Integer
// (Linear) Programming; most support the sections shown in the previous
// example.
//
// In what follows, we describe the format and requirements for each of the
// supported sections. Note that sections must appear in the order in
// this description, and that optional sections can be skipped altogether, but
// if they do appear, the must do so in the order in this description.
//
// Note that variables and constraints are declared in the order in which they
// appear in the file.
// Lines whose first character is '*' are considered comments and ignored;
// empty lines are also ignored.
//
// Section order and data within each section:
//
// - NAME
//
// This optional section has the format:
// 'NAME <optional_name>`
// In fixed format, <optional_name> must start at column 15.
//
// - OBJSENSE
//
// This optional section specifies the objective direction of the problem (min
// or max), by a single line containing either 'MIN' or 'MAX'. In fixed format,
// this field must start at column 2. If no OBJSENSE section is present, the
// problem should be treated as a minimization problem (this is the most widely
// used convention, but the actual behavior is implementation defined).
//
// - ROWS
//
// This is mandatory section, and each following data line is composed of
// lines with two fields:
// ' T RowName'
// where T is one of:
// - N: for no constraint type, usually used to describe objective
// coefficients. The first row of type N is used as objective function. If
// no row is of type N, then the objective function is zero, and the
// problem can be seen a feasibility problem.
// - L: for less than or equal,
// - G: for greater than or equal,
// - E: for equality constraints.
// Right hand side of constraints are zero by default (these can be overridden
// in sections RHS and RANGES). Repeating a 'RowName' is undefined behavior. In
// fixed format, the type appears in column 2 and the row name starts in
// column 5.
//
// - LAZYCONS
//
// This section is optional, and has the same format (and meaning) as the ROWS
// section, i.e. each constraint mentioned here must be new, and each one of
// them defines a constraint of the problem. The only difference is that
// constraints defined in this section are marked as 'lazy', meaning that there
// might be an advantage, when solving the problem, to dynamically add them to
// the solving process on the fly.
//
// - COLUMNS
//
// This is a mandatory section, and each of the following data lines is
// composed of three or five fields with the format:
// ' <ColName> <RowName> <Value> <RowName2> <Value2>'
// where 'RowName' and 'RowName2' are constraints defined in the ROWS or
// LAZYCONS section; 'Value' and 'Value2' are finite values; 'RowName2' and
// 'Value2' are optional. The triplet <RowName,ColName,Value> is added to
// constraint matrix; and, if present, the triplet <RowName2,ColName,Value2> is
// added to the constraint matrix. Note that there is no explicit requirement
// that triplets are unique (and how to treat duplicates is
// implementation-defined) nor sorted.
//
// In fixed format, the column name starts in column 5, the row name for the
// first non-zero starts in column 15, and the value for the first non-zero
// starts in column 25. If a second non-zero is present, the row name starts in
// column 40 and the value starts in column 50.
//
// The COLUMNS section can optionally include (possibly multiple) integrality
// markers. Variables mentioned between a pair of markers are assigned type
// 'Integer' with binary bounds by default (even if the variable appears for the
// first time outside a pair of integrality markers, thus changing its default
// bounds). Refer to the BOUNDS section for how to change these bounds.
//
// The format of these markers is (excluding double quotes):
// - " <IgnoredField> 'MARKER' 'INTORG'",
// - " <ColName> <RowName> <Value> <RowName2> <Value2>"
// ...
// - " <ColName> <RowName> <Value> <RowName2> <Value2>"
// - " <IgnoredField> 'MARKER' 'INTEND'",
// Where the first field is ignored. In fixed format, the fields start in
// columns 5, 15 and 40, respectively. Note that the second field must exactly
// match 'MARKER', and the third field must be 'INTORG' for opening an integer
// section, and 'INTEND' for closing an integer section.
//
// - RHS
//
// This is a mandatory section, and each of the following data lines is
// composed of three or five fields with the format:
// ' <Ignored_Field> <RowName1> <Value1> <OptionalRow2> <OptionalValue2>',
// where the first field is ignored, and <RowName> must have been defined in
// sections ROWS or LAZYCONS with type E, L or G, and where <Value1> is the
// right hand side of <RowName>, and must be a finite value. If <OptionalRow2>
// and <OptionalValue2> are present, the same constraints and behavior applies.
// In fixed format fields start at columns 2, 5, 15, 40 and 50.
//
// You can specify an objective 'Offset' by adding the pair <Objective_Name>
// <Minus_Offset> in one of the data lines of the RHS section.
//
// - RANGES
//
// This is an optional section, and each of the following data lines is
// composed of three or five fields:
// ' <IgnoredField> <RowName> <Range1> <OptionalRowName2> <OptionalRange2>',
// where the first field is ignored, and <RowName> must have been defined in
// sections ROWS or LAZYCONS with type E, L or G, and <Range1> must be a finite
// value. In fixed format fields must start in columns 2, 5, 15, 40 and 50.
//
// The effect of specifying a range depends on the sense of the
// specified row and whether the range has a positive or negative <Range1>:
//
// Row_type Range_value_sign rhs_lower_limit rhs_upper_limit
// G + or - rhs rhs + |range|
// L + or - rhs - |range| rhs
// E + rhs rhs + range
// E - rhs + range rhs
//
// If <OptionalRowName2> and <OptionalRange2> are present, the same constraints
// and behavior applies.
//
// - BOUNDS
//
// Each variable has by default a lower bound of zero, and an upper bound of
// infinity, except if the variable is mentioned between integrality markers and
// is not mentioned in this section, in which case its lower bound is zero, and
// its upper bound is one.
//
// This is a mandatory section, and each of the following data lines is composed
// of three or four fields with the format:
// ' <BoundType> <IgnoredField> <ColName> <Value>',
// - LO: lower bound for variable, <Value> is mandatory, and
// the data line has the effect of setting <Value> <= <ColName>,
// - UP: upper bound for variable, <Value> is mandatory, and the
// data line has the effect of setting <ColName> <= <Value>,
// - FX: for fixed variable, <Value> is mandatory, and the data line has the
// effect of setting <Value> <= <ColName> <= <Value>,
// - FR: for `free` variable, <Value> is optional and ignored if present, and
// the data line has the effect of setting −∞ <= <ColName> <= ∞,
// - MI: infinity lower bound, <Value> is optional and ignored if present, and
// the data line has the effect of setting −∞ <= <ColName>,
// - PL: infinity upper bound, <Value> is optional and ignored if present, and
// the data line has the effect of setting <ColName> <= ∞,
// - BV: binary variable, <Value> is optional and ignored if present, and the
// data line has the effect of setting 0 <= <ColName> <= 1,
// - LI: lower bound for integer variables, same constraints and effect as LO.
// - UI: upper bound for integer variables, same constraints and effect as UP.
// - SC: stands for semi-continuous and indicates that the variable may be zero,
// but if not must be equal to at least the value given (this is not a
// common type of variable, and can easily be described in terms of a
// binary plus a continuous variable and a constraint linking the two, an
// implementation may choose not to support this kind of variable);
// <Value> is mandatory, and is only meaningful if it is strictly
// positive.
// No extra constraints or assumptions are imposed on the order, or the number
// of bound constraints on a variable. Each data line is processed sequentially
// and its effects enforced; regardless of previously set bounds, explicitly or
// by default.
//
// In fixed format, fields start in columns 2, 5, 15 and 25.
//
// - INDICATORS
//
// This is an optional section, and each of the following data lines is
// composed of four fields with the format:
// ' IF <RowName> <ColName> <BinaryValue>',
// where <RowName> is a row defined either in the ROWS or LAZYCONS sections,
// <ColName> is forced to be a binary variable (intersecting previously set
// bounds with the interval [0,1], and requiring it to be integer); the effect
// of the data line is to remove <RowName> from the set of common linear
// constraints (which must be satisfied for all feasible solutions), and
// instead require the constraint to be satisfied only if the binary variable
// <ColName> holds the value <BinaryValue>.
// Note that integer/primal tolerances on variables have surprising effects: if
// a binary variable has the value (1+-tolerance), it is considered to be at
// value 1 for the purposes of indicator constraints.
//
// - ENDDATA
//
// This is a mandatory section, and it should be the last line in a MPS
// file/string. What happens with lines after this section is undefined
// behavior.
//
// Some extended versions (often times incompatible between themselves) of the
// format can be seen here:
//
// https://www.gurobi.com/documentation/10.0/refman/mps_format.html
// https://www.ibm.com/docs/en/icos/22.1.0?topic=standard-records-in-mps-format
// https://lpsolve.sourceforge.net/5.0/mps-format.htm
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <limits>
#include <string>
#include <vector>
#include "absl/container/flat_hash_map.h"
#include "absl/container/flat_hash_set.h"
#include "absl/container/inlined_vector.h"
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/ascii.h"
#include "absl/strings/match.h"
#include "absl/strings/numbers.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_split.h"
#include "absl/strings/string_view.h"
#include "ortools/base/status_macros.h"
#include "ortools/util/filelineiter.h"
namespace operations_research {
// Forms of MPS format supported, either detected automatically, or free
// format, or fixed (width) format.
enum class MPSReaderFormat { kAutoDetect, kFree, kFixed };
// Implementation details.
namespace internal {
// Maximum number of 'fields' in an MPS line, either in fixed or free format.
static constexpr int kNumMpsFields = 6;
// Enum for MPS section ids.
enum class MPSSectionId {
kUnknownSection,
kName,
kObjsense,
kRows,
kLazycons,
kColumns,
kRhs,
kRanges,
kBounds,
kIndicators,
kEndData
};
// Represents a single line of an MPS file (or string), and its corresponding
// fields.
class MPSLineInfo {
public:
// Creates an `MPSLineInfo` for `line`, which should outlive this object. If
// the line is a comment line, does not split the line into fields. Returns
// InvalidArgumentError if:
// * `free_form == false` and `line` contains a forbidden character ('\t')
// after stripping trailing whitespace,
// * `free_form == false` and `line` is not in fixed format, or
// * if when splitting the line into fields too many fields are detected.
static absl::StatusOr<MPSLineInfo> Create(int64_t line_num, bool free_form,
absl::string_view line);
// Returns a view of the line.
absl::string_view GetLine() const { return line_; }
// TODO(b/284163180): Fix handling of sections and data in `free_form`.
// Returns true if the line defines a new section.
bool IsNewSection() const { return line_[0] != '\0' && line_[0] != ' '; }
// Returns the number of fields in the line. What constitutes a 'field'
// depends on the format (fixed or free) used at creation time. See the
// previous description of MPS fixed and free formats for more details.
int GetFieldsSize() const { return fields_.size(); }
// Returns the word starting at position 0 in the line. If the line is empty,
// or if the line starts with a space, returns `""`.
absl::string_view GetFirstWord() const;
// Returns true if the line contains a comment (starting with '*') or
// if it is a blank line.
bool IsCommentOrBlank() const;
// Helper function that returns the index-th field in the line.
// Note that what constitutes a 'field' depends on the format of the line
// (i.e. free form or fixed form). See the previous description of fixed and
// free MPS format for more details. Note that `index` must be smaller than
// `GetFieldsSize()` and non negative, otherwise the behavior is undefined.
// Furthermore, until `SplitLineIntoFields` is called, `GetFieldsSize()` is
// always zero.
absl::string_view GetField(int index) const { return fields_[index]; }
// After calling `SplitLineIntoFields` has been called, returns the offset at
// which to start the parsing of fields within the line. See the preceding
// discussion on free and fixed MPS format for details on what constitutes a
// field in a line.
// If in fixed form, the offset is 0.
// If in fixed form and the number of fields is odd, it is 1,
// otherwise it is 0.
// This is useful when processing RANGES and RHS sections.
// If `SplitLineIntoFields` has not been called before, returns 0.
int GetFieldOffset() const { return free_form_ ? fields_.size() & 1 : 0; }
// Returns an absl::InvalidArgumentError with the given error message,
// postfixed by the line of the .mps file (number and contents).
absl::Status InvalidArgumentError(absl::string_view error_message) const;
// Appends the line of the .mps file (number and contents) to the
// status if it's an error message.
absl::Status AppendLineToError(const absl::Status& status) const;
private:
MPSLineInfo(int64_t line_num, bool free_form, absl::string_view line)
: free_form_{free_form}, line_num_{line_num}, line_{line} {}
// Splits into fields the line.
absl::Status SplitLineIntoFields();
// Returns true if the line matches the fixed format.
bool IsFixedFormat() const;
// Boolean set to true if the reader expects a free-form MPS file.
const bool free_form_;
// Storage of the fields for the line.
absl::InlinedVector<absl::string_view, internal::kNumMpsFields> fields_;
// The current line number (passed at construction time).
const int64_t line_num_;
// The line being parsed (with ASCII trailing white space removed, that
// includes windows end of line, new line, space, vertical tab and horizontal
// tab).
const absl::string_view line_;
};
} // namespace internal
// Templated `MPS` reader. The template class `DataWrapper` must provide:
// type:
// - `IndexType`: for indices of rows and columns.
// functions:
// - `void SetUp()`: Called before parsing. After this function call, the
// internal state of the object should be the same as in creation.
// Note that this function can be called more than once if using
// `MPSReaderFormat::kAutoDetect` format.
// - `void CleanUp()`: Called once, after parsing has been successful, to
// perform any internal clean up if needed.
// - `double ConstraintLowerBound(IndexType index)`: Returns the (currently
// stored) lower bound for 'Constraint[index]'; where 'index' is
// a value previously returned by `FindOrCreateConstraint`.
// - `double ConstraintUpperBound(IndexType index)`: Returns the (currently
// stored) upper bound for 'Constraint[index]'; where 'index' is
// a value previously returned by `FindOrCreateConstraint`.
// - `IndexType FindOrCreateConstraint(absl::string_view row_name)`: Returns
// the (internally assigned) index to the constraint of the given
// name. If `row_name` is new, the constraint must be created with
// a zero lower bound and a zero upper bound.
// - `IndexType FindOrCreateVariable(absl::string_view col_name)`: Returns the
// (internally assigned) index to the variable of the given name.
// Newly created variables should have a zero objective, zero
// lower bound, infinity upper bound, and be considered as
// 'continuous'. If `col_name` is new, the variable must be
// created with a zero objective coefficient, a zero lower bound,
// and an infinity upper bound.
// - `void SetConstraintBounds(IndexType index,double lower_bound, double
// upper_bound)`: Stores lower and upper bounds for
// 'Constraint[index]'. `index` is a value previously returned by
// `FindOrCreateConstraint`.
// - `void SetConstraintCoefficient(IndexType row_index, IndexType col_index,
// double coefficient)`: Stores/Adds a new coefficient for the
// constraint matrix entry (row_index,col_index); where
// `row_index` is a value previously returned by
// `FindOrCreateConstraint`, and `col_index` is a value
// previously returned by `FindOrCreateVariable`.
// - `void SetIsLazy(IndexType row_index)`: Marks 'Constraint[row_index]' as a
// `lazy constraint`, meaning that the constraint is part of the
// problem definition, but it might be advantageous to add them
// as 'cuts' when solving the problem; where `row_index` is a
// value previously returned by `FindOrCreateConstraint`.
// - `void SetName(absl::string_view)`: Stores the model's name.
// - 'void SetObjectiveCoefficient(IndexType index, double coefficient)`:
// Stores `coefficient` as the new objective coefficient for
// 'Variable[index]'; where `index` is a value previously
// returned by `FindOrCreateVariable`.
// - `void SetObjectiveDirection(bool maximize)`: If `maximize==true` the
// parsed model represents a maximization problem, otherwise, or
// if the function is never called, the model is a minimization
// problem.
// - `void SetObjectiveOffset(double)`: Stores the objective offset of the
// model.
// - `void SetVariableTypeToInteger(IndexType index)`: Marks 'Variable[index]'
// as 'integer'; where `index` is a value previously returned by
// `FindOrCreateVariable`.
// - `void SetVariableTypeToSemiContinuous(IndexType index)`: Marks
// 'Variable[index]' as 'semi continuous'; where `index` is a
// value previously returned by `FindOrCreateVariable`.
// - `void SetVariableBounds(IndexType index, double lower_bound, double
// upper_bound)`: Stores the lower and upper bounds for
// 'Variable[index]'; where `index` is a value previously
// returned by `FindOrCreateVariable`.
// - `double VariableLowerBound(IndexType index)`: Returns the (currently)
// stored lower bound for 'Variable[index'; where `index` is a
// value previously returned by `FindOrCreateVariable`.
// - `double VariableUpperBound(IndexType index)`: Returns the (currently)
// stored upper bound for 'Variable[index'; where `index` is a
// value previously returned by `FindOrCreateVariable`.
// - `absl::Status CreateIndicatorConstraint(absl::string_view row_name,
// IndexType col_index, bool var_value)`: Marks constraint named
// `row_name` to be an 'indicator constraint', that should hold
// if 'Variable[col_index]' holds value 0 if `var_value`==false,
// or when 'Variable[col_index]' holds value 1 if
// `var_value`==true. Where `row_name` must have been an argument
// in a previous call to `FindOrCreateConstraint`, and
// `col_index` is a value previously returned by
// `FindOrCreateVariable`. Note that 'Variable[col_index]' should
// be marked as integer and have bounds in {0,1}.
template <class DataWrapper>
class MPSReaderTemplate {
public:
// Type for row and column indices, as provided by `DataWrapper`.
using IndexType = typename DataWrapper::IndexType;
MPSReaderTemplate();
// Parses a file in MPS format; if successful, returns the type of MPS
// format detected (one of `kFree` or `kFixed`). If `form` is either `kFixed`
// or `kFree`, the function will either return `kFixed` (or `kFree`
// respectively) if the input data satisfies the format, or an
// `absl::InvalidArgumentError` otherwise.
absl::StatusOr<MPSReaderFormat> ParseFile(
absl::string_view file_name, DataWrapper* data,
MPSReaderFormat form = MPSReaderFormat::kAutoDetect);
// Parses a string in MPS format; if successful, returns the type of MPS
// format detected (one of `kFree` or `kFixed`). If `form` is either `kFixed`
// or `kFree`, the function will either return `kFixed` (or `kFree`
// respectively) if the input data satisfies the format, or an
// `absl::InvalidArgumentError` otherwise.
absl::StatusOr<MPSReaderFormat> ParseString(
absl::string_view source, DataWrapper* data,
MPSReaderFormat form = MPSReaderFormat::kAutoDetect);
private:
static constexpr double kInfinity = std::numeric_limits<double>::infinity();
// Resets the object to its initial value before reading a new file.
void Reset();
// Displays some information on the last loaded file.
void DisplaySummary();
// Line processor.
absl::Status ProcessLine(absl::string_view line, DataWrapper* data);
// Process section OBJSENSE in MPS file.
absl::Status ProcessObjectiveSenseSection(
const internal::MPSLineInfo& line_info, DataWrapper* data);
// Process section ROWS in the MPS file.
absl::Status ProcessRowsSection(const internal::MPSLineInfo& line_info,
bool is_lazy, DataWrapper* data);
// Process section COLUMNS in the MPS file.
absl::Status ProcessColumnsSection(const internal::MPSLineInfo& line_info,
DataWrapper* data);
// Process section RHS in the MPS file.
absl::Status ProcessRhsSection(const internal::MPSLineInfo& line_info,
DataWrapper* data);
// Process section RANGES in the MPS file.
absl::Status ProcessRangesSection(const internal::MPSLineInfo& line_info,
DataWrapper* data);
// Process section BOUNDS in the MPS file.
absl::Status ProcessBoundsSection(const internal::MPSLineInfo& line_info,
DataWrapper* data);
// Process section INDICATORS in the MPS file.
absl::Status ProcessIndicatorsSection(const internal::MPSLineInfo& line_info,
DataWrapper* data);
// Safely converts a string to a numerical type. Returns an error if the
// string passed as parameter is ill-formed.
absl::StatusOr<double> GetDoubleFromString(
absl::string_view str, const internal::MPSLineInfo& line_info);
absl::StatusOr<bool> GetBoolFromString(
absl::string_view str, const internal::MPSLineInfo& line_info);
// Different types of variables, as defined in the MPS file specification.
// Note these are more precise than the ones in PrimalSimplex.
enum class BoundTypeId {
kUnknownBoundType,
kLowerBound,
kUpperBound,
kFixedVariable,
kFreeVariable,
kInfiniteLowerBound,
kInfiniteUpperBound,
kBinary,
kSemiContinuous
};
// Different types of constraints for a given row.
enum class RowTypeId {
kUnknownRowType,
kEquality,
kLessThan,
kGreaterThan,
kObjective,
kNone
};
// Stores a bound value of a given type, for a given column name.
absl::Status StoreBound(const internal::MPSLineInfo& line_info,
absl::string_view bound_type_mnemonic,
absl::string_view column_name,
absl::string_view bound_value, DataWrapper* data);
// Stores a coefficient value for a column number and a row name.
absl::Status StoreCoefficient(const internal::MPSLineInfo& line_info,
IndexType col, absl::string_view row_name,
absl::string_view row_value, DataWrapper* data);
// Stores a right-hand-side value for a row name.
absl::Status StoreRightHandSide(const internal::MPSLineInfo& line_info,
absl::string_view row_name,
absl::string_view row_value,
DataWrapper* data);
// Stores a range constraint of value row_value for a row name.
absl::Status StoreRange(const internal::MPSLineInfo& line_info,
absl::string_view row_name,
absl::string_view range_value, DataWrapper* data);
// Boolean set to true if the reader expects a free-form MPS file.
bool free_form_;
// Stores the name of the objective row.
std::string objective_name_;
// Id of the current section of MPS file.
internal::MPSSectionId section_;
// Maps section mnemonic --> section id.
absl::flat_hash_map<std::string, internal::MPSSectionId>
section_name_to_id_map_;
// Maps row type mnemonic --> row type id.
absl::flat_hash_map<std::string, RowTypeId> row_name_to_id_map_;
// Maps bound type mnemonic --> bound type id.
absl::flat_hash_map<std::string, BoundTypeId> bound_name_to_id_map_;
// Set of bound type mnemonics that constrain variables to be integer.
absl::flat_hash_set<std::string> integer_type_names_set_;
// The current line number in the file being parsed.
int64_t line_num_;
// A row of Booleans. is_binary_by_default_[col] is true if col
// appeared within a scope started by INTORG and ended with INTEND markers.
// Its size may be larger than the number of variables encountered in the
// model so far.
std::vector<bool> is_binary_by_default_;
// True if the next variable has to be interpreted as an integer variable.
// This is used to support the marker INTORG that starts an integer section
// and INTEND that ends it.
bool in_integer_section_;
// We keep track of the number of unconstrained rows so we can display it to
// the user because other solvers usually ignore them and we don't (they will
// be removed in the preprocessor).
IndexType num_unconstrained_rows_;
};
template <class DataWrapper>
absl::StatusOr<MPSReaderFormat> MPSReaderTemplate<DataWrapper>::ParseFile(
const absl::string_view file_name, DataWrapper* const data,
const MPSReaderFormat form) {
if (data == nullptr) {
return absl::InvalidArgumentError("NULL pointer passed as argument.");
}
if (form != MPSReaderFormat::kFree && form != MPSReaderFormat::kFixed) {
if (ParseFile(file_name, data, MPSReaderFormat::kFixed).ok()) {
return MPSReaderFormat::kFixed;
}
return ParseFile(file_name, data, MPSReaderFormat::kFree);
}
DCHECK(form == MPSReaderFormat::kFree || form == MPSReaderFormat::kFixed);
free_form_ = form == MPSReaderFormat::kFree;
Reset();
data->SetUp();
File* file = nullptr;
RETURN_IF_ERROR(file::Open(file_name, "r", &file, file::Defaults()));
for (const absl::string_view line :
FileLines(file_name, file, FileLineIterator::REMOVE_INLINE_CR)) {
RETURN_IF_ERROR(ProcessLine(line, data));
}
data->CleanUp();
DisplaySummary();
return form;
}
template <class DataWrapper>
absl::StatusOr<MPSReaderFormat> MPSReaderTemplate<DataWrapper>::ParseString(
absl::string_view source, DataWrapper* const data,
const MPSReaderFormat form) {
if (form != MPSReaderFormat::kFree && form != MPSReaderFormat::kFixed) {
if (ParseString(source, data, MPSReaderFormat::kFixed).ok()) {
return MPSReaderFormat::kFixed;
}
return ParseString(source, data, MPSReaderFormat::kFree);
}
DCHECK(form == MPSReaderFormat::kFree || form == MPSReaderFormat::kFixed);
free_form_ = form == MPSReaderFormat::kFree;
Reset();
data->SetUp();
for (absl::string_view line : absl::StrSplit(source, '\n')) {
RETURN_IF_ERROR(ProcessLine(line, data));
}
data->CleanUp();
DisplaySummary();
return form;
}
template <class DataWrapper>
absl::Status MPSReaderTemplate<DataWrapper>::ProcessLine(absl::string_view line,
DataWrapper* data) {
++line_num_;
ASSIGN_OR_RETURN(const internal::MPSLineInfo line_info,
internal::MPSLineInfo::Create(line_num_, free_form_, line));
if (line_info.IsCommentOrBlank()) {
return absl::OkStatus(); // Skip blank lines and comments.
}
// TODO(b/284163180): Fix handling of sections and data in `free_form`.
if (line_info.IsNewSection()) {
if (const auto it = section_name_to_id_map_.find(line_info.GetFirstWord());
it != section_name_to_id_map_.end()) {
section_ = it->second;
} else {
return line_info.InvalidArgumentError("Unknown section.");
}
if (section_ == internal::MPSSectionId::kName) {
// NOTE(user): The name may differ between fixed and free forms. In
// fixed form, the name has at most 8 characters, and starts at a specific
// position in the NAME line. For MIPLIB2010 problems (eg, air04, glass4),
// the name in fixed form ends up being preceded with a whitespace.
// TODO(user): Return an error for fixed form if the problem name
// does not fit.
if (free_form_) {
if (line_info.GetFieldsSize() >= 2) {
data->SetName(line_info.GetField(1));
}
} else {
const std::vector<absl::string_view> free_fields = absl::StrSplit(
line_info.GetLine(), absl::ByAnyChar(" \t"), absl::SkipEmpty());
const absl::string_view free_name =
free_fields.size() >= 2 ? free_fields[1] : "";
const absl::string_view fixed_name =
line_info.GetFieldsSize() >= 3 ? line_info.GetField(2) : "";
if (free_name != fixed_name) {
return line_info.InvalidArgumentError(
"Fixed form invalid: name differs between free and fixed "
"forms.");
}
data->SetName(fixed_name);
}
}
// Supports the case where the direction is on the same line as the
// OBJSENSE keyword.
if (section_ == internal::MPSSectionId::kObjsense &&
line_info.GetFieldsSize() == 2 && free_form_) {
if (absl::StrContains(line_info.GetField(1), "MIN")) {
data->SetObjectiveDirection(/*maximize=*/false);
} else if (absl::StrContains(line_info.GetField(1), "MAX")) {
data->SetObjectiveDirection(/*maximize=*/true);
} else {
return line_info.InvalidArgumentError(
"Invalid inline objective direction.");
}
}
return absl::OkStatus();
}
switch (section_) {
case internal::MPSSectionId::kName:
return line_info.InvalidArgumentError("Second NAME field.");
case internal::MPSSectionId::kObjsense:
return ProcessObjectiveSenseSection(line_info, data);
case internal::MPSSectionId::kRows:
return ProcessRowsSection(line_info, /*is_lazy=*/false, data);
case internal::MPSSectionId::kLazycons:
return ProcessRowsSection(line_info, /*is_lazy=*/true, data);
case internal::MPSSectionId::kColumns:
return ProcessColumnsSection(line_info, data);
case internal::MPSSectionId::kRhs:
return ProcessRhsSection(line_info, data);
case internal::MPSSectionId::kRanges:
return ProcessRangesSection(line_info, data);
case internal::MPSSectionId::kBounds:
return ProcessBoundsSection(line_info, data);
case internal::MPSSectionId::kIndicators:
return ProcessIndicatorsSection(line_info, data);
case internal::MPSSectionId::kEndData: // Do nothing.
break;
default:
return line_info.InvalidArgumentError("Unknown section.");
}
return absl::OkStatus();
}
template <class DataWrapper>
absl::Status MPSReaderTemplate<DataWrapper>::ProcessObjectiveSenseSection(
const internal::MPSLineInfo& line_info, DataWrapper* data) {
absl::string_view field = absl::StripAsciiWhitespace(line_info.GetLine());
if (field != "MIN" && field != "MAX") {
return line_info.InvalidArgumentError(
"Expected objective sense (MAX or MIN).");
}
data->SetObjectiveDirection(/*maximize=*/field == "MAX");
return absl::OkStatus();
}
template <class DataWrapper>
absl::Status MPSReaderTemplate<DataWrapper>::ProcessRowsSection(
const internal::MPSLineInfo& line_info, bool is_lazy, DataWrapper* data) {
if (line_info.GetFieldsSize() < 2) {
return line_info.InvalidArgumentError("Not enough fields in ROWS section.");
}
const absl::string_view row_type_name = line_info.GetField(0);
const absl::string_view row_name = line_info.GetField(1);
const auto it = row_name_to_id_map_.find(row_type_name);
if (it == row_name_to_id_map_.end()) {
return line_info.InvalidArgumentError("Unknown row type.");
}
RowTypeId row_type = it->second;
// The first NONE constraint is used as the objective.
if (objective_name_.empty() && row_type == RowTypeId::kNone) {
row_type = RowTypeId::kObjective;
objective_name_ = std::string(row_name);
} else {
if (row_type == RowTypeId::kNone) {
++num_unconstrained_rows_;
}
const IndexType row = data->FindOrCreateConstraint(row_name);
if (is_lazy) data->SetIsLazy(row);
// The initial row range is [0, 0]. We encode the type in the range by
// setting one of the bounds to +/- infinity.
switch (row_type) {
case RowTypeId::kLessThan:
data->SetConstraintBounds(row, -kInfinity,
data->ConstraintUpperBound(row));
break;
case RowTypeId::kGreaterThan:
data->SetConstraintBounds(row, data->ConstraintLowerBound(row),
kInfinity);
break;
case RowTypeId::kNone:
data->SetConstraintBounds(row, -kInfinity, kInfinity);
break;
case RowTypeId::kEquality:
default:
break;
}
}
return absl::OkStatus();
}
namespace internal {
// Extends the underlying `container` to have length at least `until`+1. Any
// newly added value is initialized to `extend_with_value`.
template <typename T, typename IndexType>
void ExtendContainerWithValueUntil(const IndexType until,
const T extend_with_value,
std::vector<T>& container) {
if (container.size() > until) return;
std::fill_n(std::back_inserter(container), until + 1 - container.size(),
extend_with_value);
}
} // namespace internal
template <class DataWrapper>
absl::Status MPSReaderTemplate<DataWrapper>::ProcessColumnsSection(
const internal::MPSLineInfo& line_info, DataWrapper* data) {
// Take into account the INTORG and INTEND markers.
if (absl::StrContains(line_info.GetLine(), "'MARKER'")) {
if (absl::StrContains(line_info.GetLine(), "'INTORG'")) {
VLOG(2) << "Entering integer marker.\n" << line_info.GetLine();
if (in_integer_section_) {
return line_info.InvalidArgumentError(
"Found INTORG inside the integer section.");
}
in_integer_section_ = true;
} else if (absl::StrContains(line_info.GetLine(), "'INTEND'")) {
VLOG(2) << "Leaving integer marker.\n" << line_info.GetLine();
if (!in_integer_section_) {
return line_info.InvalidArgumentError(
"Found INTEND without corresponding INTORG.");
}
in_integer_section_ = false;
}
return absl::OkStatus();
}
const int start_index = free_form_ ? 0 : 1;
if (line_info.GetFieldsSize() < start_index + 3) {
return line_info.InvalidArgumentError(
"Not enough fields in COLUMNS section.");
}
const absl::string_view column_name = line_info.GetField(start_index + 0);
const absl::string_view row1_name = line_info.GetField(start_index + 1);
const absl::string_view row1_value = line_info.GetField(start_index + 2);
const IndexType col = data->FindOrCreateVariable(column_name);
internal::ExtendContainerWithValueUntil(col, false, is_binary_by_default_);
if (in_integer_section_) {
data->SetVariableTypeToInteger(col);
// The default bounds for integer variables are [0, 1].
data->SetVariableBounds(col, 0.0, 1.0);
is_binary_by_default_[col] = true;
} else {
data->SetVariableBounds(col, 0.0, kInfinity);
}
RETURN_IF_ERROR(
StoreCoefficient(line_info, col, row1_name, row1_value, data));
if (line_info.GetFieldsSize() == start_index + 4) {
return line_info.InvalidArgumentError("Unexpected number of fields.");
}
if (line_info.GetFieldsSize() - start_index > 4) {
const absl::string_view row2_name = line_info.GetField(start_index + 3);
const absl::string_view row2_value = line_info.GetField(start_index + 4);
RETURN_IF_ERROR(
StoreCoefficient(line_info, col, row2_name, row2_value, data));
}
return absl::OkStatus();
}
template <class DataWrapper>
absl::Status MPSReaderTemplate<DataWrapper>::ProcessRhsSection(
const internal::MPSLineInfo& line_info, DataWrapper* data) {
const int start_index = free_form_ ? 0 : 2;
const int offset = start_index + line_info.GetFieldOffset();
if (line_info.GetFieldsSize() < offset + 2) {
return line_info.InvalidArgumentError("Not enough fields in RHS section.");
}
// const absl::string_view rhs_name = line_info.GetField(0); is not used
const absl::string_view row1_name = line_info.GetField(offset);
const absl::string_view row1_value = line_info.GetField(offset + 1);
RETURN_IF_ERROR(StoreRightHandSide(line_info, row1_name, row1_value, data));
if (line_info.GetFieldsSize() - start_index >= 4) {
const absl::string_view row2_name = line_info.GetField(offset + 2);
const absl::string_view row2_value = line_info.GetField(offset + 3);
RETURN_IF_ERROR(StoreRightHandSide(line_info, row2_name, row2_value, data));
}
return absl::OkStatus();
}
template <class DataWrapper>
absl::Status MPSReaderTemplate<DataWrapper>::ProcessRangesSection(
const internal::MPSLineInfo& line_info, DataWrapper* data) {
const int start_index = free_form_ ? 0 : 2;
const int offset = start_index + line_info.GetFieldOffset();
if (line_info.GetFieldsSize() < offset + 2) {
return line_info.InvalidArgumentError("Not enough fields in RHS section.");
}
// const absl::string_view range_name = line_info.GetField(0); is not used
const absl::string_view row1_name = line_info.GetField(offset);
const absl::string_view row1_value = line_info.GetField(offset + 1);
RETURN_IF_ERROR(StoreRange(line_info, row1_name, row1_value, data));
if (line_info.GetFieldsSize() - start_index >= 4) {
const absl::string_view row2_name = line_info.GetField(offset + 2);
const absl::string_view row2_value = line_info.GetField(offset + 3);
RETURN_IF_ERROR(StoreRange(line_info, row2_name, row2_value, data));
}
return absl::OkStatus();
}
template <class DataWrapper>
absl::Status MPSReaderTemplate<DataWrapper>::ProcessBoundsSection(
const internal::MPSLineInfo& line_info, DataWrapper* data) {
if (line_info.GetFieldsSize() < 3) {
return line_info.InvalidArgumentError(
"Not enough fields in BOUNDS section.");
}
const absl::string_view bound_type_mnemonic = line_info.GetField(0);
const absl::string_view column_name = line_info.GetField(2);
const absl::string_view bound_value =
(line_info.GetFieldsSize() >= 4) ? line_info.GetField(3) : "";
return StoreBound(line_info, bound_type_mnemonic, column_name, bound_value,
data);
}
template <class DataWrapper>
absl::Status MPSReaderTemplate<DataWrapper>::ProcessIndicatorsSection(
const internal::MPSLineInfo& line_info, DataWrapper* data) {
// TODO(user): Enforce section order. This section must come after
// anything related to constraints, or we'll have partial data inside the
// indicator constraints.
if (line_info.GetFieldsSize() < 4) {
return line_info.InvalidArgumentError(
"Not enough fields in INDICATORS section.");
}
const absl::string_view type = line_info.GetField(0);
if (type != "IF") {
return line_info.InvalidArgumentError(
"Indicator constraints must start with \"IF\".");
}
const absl::string_view row_name = line_info.GetField(1);
const absl::string_view column_name = line_info.GetField(2);
const absl::string_view column_value = line_info.GetField(3);
bool value;
ASSIGN_OR_RETURN(value, GetBoolFromString(column_value, line_info));
const IndexType col = data->FindOrCreateVariable(column_name);
// Variables used in indicator constraints become Boolean by default.
data->SetVariableTypeToInteger(col);
data->SetVariableBounds(col, std::max(0.0, data->VariableLowerBound(col)),
std::min(1.0, data->VariableUpperBound(col)));
RETURN_IF_ERROR(line_info.AppendLineToError(
data->CreateIndicatorConstraint(row_name, col, value)));
return absl::OkStatus();
}
template <class DataWrapper>
absl::Status MPSReaderTemplate<DataWrapper>::StoreCoefficient(
const internal::MPSLineInfo& line_info, IndexType col,
absl::string_view row_name, absl::string_view row_value,
DataWrapper* data) {
if (row_name.empty() || row_name == "$") {
return absl::OkStatus();
}
double value;
ASSIGN_OR_RETURN(value, GetDoubleFromString(row_value, line_info));
if (value == kInfinity || value == -kInfinity) {
return line_info.InvalidArgumentError(
"Constraint coefficients cannot be infinity.");
}
if (value == 0.0) return absl::OkStatus();
if (row_name == objective_name_) {
data->SetObjectiveCoefficient(col, value);
} else {
const IndexType row = data->FindOrCreateConstraint(row_name);
data->SetConstraintCoefficient(row, col, value);
}
return absl::OkStatus();
}
template <class DataWrapper>
absl::Status MPSReaderTemplate<DataWrapper>::StoreRightHandSide(
const internal::MPSLineInfo& line_info, absl::string_view row_name,
absl::string_view row_value, DataWrapper* data) {
if (row_name.empty()) return absl::OkStatus();
if (row_name != objective_name_) {
const IndexType row = data->FindOrCreateConstraint(row_name);
double value;
ASSIGN_OR_RETURN(value, GetDoubleFromString(row_value, line_info));
// The row type is encoded in the bounds, so at this point we have either
// (-kInfinity, 0.0], [0.0, 0.0] or [0.0, kInfinity). We use the right
// hand side to change any finite bound.
const double lower_bound =
(data->ConstraintLowerBound(row) == -kInfinity) ? -kInfinity : value;
const double upper_bound =
(data->ConstraintUpperBound(row) == kInfinity) ? kInfinity : value;
data->SetConstraintBounds(row, lower_bound, upper_bound);
} else {
// We treat minus the right hand side of COST as the objective offset, in
// line with what the MPS writer does and what Gurobi's MPS format
// expects.
double value;
ASSIGN_OR_RETURN(value, GetDoubleFromString(row_value, line_info));
data->SetObjectiveOffset(-value);
}
return absl::OkStatus();
}
template <class DataWrapper>
absl::Status MPSReaderTemplate<DataWrapper>::StoreRange(
const internal::MPSLineInfo& line_info, absl::string_view row_name,
absl::string_view range_value, DataWrapper* data) {
if (row_name.empty()) return absl::OkStatus();
const IndexType row = data->FindOrCreateConstraint(row_name);
double range;
ASSIGN_OR_RETURN(range, GetDoubleFromString(range_value, line_info));
double lower_bound = data->ConstraintLowerBound(row);
double upper_bound = data->ConstraintUpperBound(row);
if (lower_bound == upper_bound) {
if (range < 0.0) {
lower_bound += range;
} else {
upper_bound += range;
}
}
if (lower_bound == -kInfinity) {
lower_bound = upper_bound - fabs(range);
}
if (upper_bound == kInfinity) {
upper_bound = lower_bound + fabs(range);
}
data->SetConstraintBounds(row, lower_bound, upper_bound);
return absl::OkStatus();
}
template <class DataWrapper>
absl::Status MPSReaderTemplate<DataWrapper>::StoreBound(
const internal::MPSLineInfo& line_info,
absl::string_view bound_type_mnemonic, absl::string_view column_name,
absl::string_view bound_value, DataWrapper* data) {
const auto it = bound_name_to_id_map_.find(bound_type_mnemonic);
if (it == bound_name_to_id_map_.end()) {
return line_info.InvalidArgumentError("Unknown bound type.");
}
const BoundTypeId bound_type_id = it->second;
const IndexType col = data->FindOrCreateVariable(column_name);
if (integer_type_names_set_.count(bound_type_mnemonic) != 0) {
data->SetVariableTypeToInteger(col);
}
internal::ExtendContainerWithValueUntil(col, false, is_binary_by_default_);
double lower_bound = data->VariableLowerBound(col);
double upper_bound = data->VariableUpperBound(col);
// If a variable is binary by default, its status is reset if any bound
// is set on it. We take care to restore the default bounds for general
// integer variables.
if (is_binary_by_default_[col]) {
lower_bound = 0.0;
upper_bound = kInfinity;
}
switch (bound_type_id) {
case BoundTypeId::kLowerBound: {
ASSIGN_OR_RETURN(lower_bound,
GetDoubleFromString(bound_value, line_info));
// TODO(b/285121446): Decide to keep or remove this corner case behavior.
// LI with the value 0.0 specifies general integers with no upper bound.
if (bound_type_mnemonic == "LI" && lower_bound == 0.0) {
upper_bound = kInfinity;
}
break;
}
case BoundTypeId::kUpperBound: {
ASSIGN_OR_RETURN(upper_bound,
GetDoubleFromString(bound_value, line_info));
break;
}
case BoundTypeId::kSemiContinuous: {
ASSIGN_OR_RETURN(upper_bound,
GetDoubleFromString(bound_value, line_info));
data->SetVariableTypeToSemiContinuous(col);
break;
}
case BoundTypeId::kFixedVariable: {
ASSIGN_OR_RETURN(lower_bound,
GetDoubleFromString(bound_value, line_info));
upper_bound = lower_bound;
break;
}
case BoundTypeId::kFreeVariable:
lower_bound = -kInfinity;
upper_bound = +kInfinity;
break;
case BoundTypeId::kInfiniteLowerBound:
lower_bound = -kInfinity;
break;
case BoundTypeId::kInfiniteUpperBound:
upper_bound = +kInfinity;
break;
case BoundTypeId::kBinary:
lower_bound = 0.0;
upper_bound = 1.0;
break;
case BoundTypeId::kUnknownBoundType:
default:
return line_info.InvalidArgumentError("Unknown bound type.");
}
is_binary_by_default_[col] = false;
data->SetVariableBounds(col, lower_bound, upper_bound);
return absl::OkStatus();
}
template <class DataWrapper>
MPSReaderTemplate<DataWrapper>::MPSReaderTemplate()
: free_form_(true),
section_(internal::MPSSectionId::kUnknownSection),
section_name_to_id_map_(),
row_name_to_id_map_(),
bound_name_to_id_map_(),
integer_type_names_set_(),
line_num_(0),
in_integer_section_(false),
num_unconstrained_rows_(0) {
section_name_to_id_map_["NAME"] = internal::MPSSectionId::kName;
section_name_to_id_map_["OBJSENSE"] = internal::MPSSectionId::kObjsense;
section_name_to_id_map_["ROWS"] = internal::MPSSectionId::kRows;
section_name_to_id_map_["LAZYCONS"] = internal::MPSSectionId::kLazycons;
section_name_to_id_map_["COLUMNS"] = internal::MPSSectionId::kColumns;
section_name_to_id_map_["RHS"] = internal::MPSSectionId::kRhs;
section_name_to_id_map_["RANGES"] = internal::MPSSectionId::kRanges;
section_name_to_id_map_["BOUNDS"] = internal::MPSSectionId::kBounds;
section_name_to_id_map_["INDICATORS"] = internal::MPSSectionId::kIndicators;
section_name_to_id_map_["ENDATA"] = internal::MPSSectionId::kEndData;
row_name_to_id_map_["E"] = RowTypeId::kEquality;
row_name_to_id_map_["L"] = RowTypeId::kLessThan;
row_name_to_id_map_["G"] = RowTypeId::kGreaterThan;
row_name_to_id_map_["N"] = RowTypeId::kNone;
bound_name_to_id_map_["LO"] = BoundTypeId::kLowerBound;
bound_name_to_id_map_["UP"] = BoundTypeId::kUpperBound;
bound_name_to_id_map_["FX"] = BoundTypeId::kFixedVariable;
bound_name_to_id_map_["FR"] = BoundTypeId::kFreeVariable;
bound_name_to_id_map_["MI"] = BoundTypeId::kInfiniteLowerBound;
bound_name_to_id_map_["PL"] = BoundTypeId::kInfiniteUpperBound;
bound_name_to_id_map_["BV"] = BoundTypeId::kBinary;
bound_name_to_id_map_["LI"] = BoundTypeId::kLowerBound;
bound_name_to_id_map_["UI"] = BoundTypeId::kUpperBound;
bound_name_to_id_map_["SC"] = BoundTypeId::kSemiContinuous;
// TODO(user): Support 'SI' (semi integer).
integer_type_names_set_.insert("BV");
integer_type_names_set_.insert("LI");
integer_type_names_set_.insert("UI");
}
template <class DataWrapper>
void MPSReaderTemplate<DataWrapper>::Reset() {
line_num_ = 0;
in_integer_section_ = false;
num_unconstrained_rows_ = 0;
objective_name_.clear();
}
template <class DataWrapper>
void MPSReaderTemplate<DataWrapper>::DisplaySummary() {
if (num_unconstrained_rows_ > 0) {
VLOG(1) << "There are " << num_unconstrained_rows_ + 1
<< " unconstrained rows. The first of them (" << objective_name_
<< ") was used as the objective.";
}
}
template <class DataWrapper>
absl::StatusOr<double> MPSReaderTemplate<DataWrapper>::GetDoubleFromString(
absl::string_view str, const internal::MPSLineInfo& line_info) {
double result;
if (!absl::SimpleAtod(str, &result)) {
return line_info.InvalidArgumentError(
absl::StrCat("Failed to convert \"", str, "\" to double."));
}
if (std::isnan(result)) {
return line_info.InvalidArgumentError("Found NaN value.");
}
return result;
}
template <class DataWrapper>
absl::StatusOr<bool> MPSReaderTemplate<DataWrapper>::GetBoolFromString(
absl::string_view str, const internal::MPSLineInfo& line_info) {
int result;
if (!absl::SimpleAtoi(str, &result) || result < 0 || result > 1) {
return line_info.InvalidArgumentError(
absl::StrCat("Failed to convert \"", str, "\" to bool."));
}
return result;
}
} // namespace operations_research
#endif // OR_TOOLS_LP_DATA_MPS_READER_TEMPLATE_H_