Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
lp_parser.cc
Go to the documentation of this file.
1// Copyright 2010-2024 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <algorithm>
17#include <set>
18#include <string>
19#include <vector>
20
21#include "absl/base/attributes.h"
22#include "absl/container/flat_hash_set.h"
23#include "absl/log/check.h"
24#include "absl/status/status.h"
25#include "absl/status/statusor.h"
26#include "absl/strings/match.h"
27#include "absl/strings/numbers.h"
28#include "absl/strings/str_cat.h"
29#include "absl/strings/str_split.h"
30#include "absl/strings/string_view.h"
32#include "ortools/linear_solver/linear_solver.pb.h"
36#if defined(USE_LP_PARSER)
37#include "re2/re2.h"
38#endif // defined(USE_LP_PARSER)
39
40#if defined(USE_LP_PARSER)
41namespace operations_research {
42namespace glop {
43
44namespace {
45
46using StringPiece = ::re2::StringPiece;
47using ::absl::StatusOr;
48
49enum class TokenType {
50 ERROR,
51 END,
52 ADDAND,
53 VALUE,
54 INF,
55 NAME,
56 SIGN_LE,
57 SIGN_EQ,
58 SIGN_GE,
59 COMA,
60};
61
62bool TokenIsBound(TokenType token_type) {
63 if (token_type == TokenType::VALUE || token_type == TokenType::INF) {
64 return true;
65 }
66 return false;
67}
68
69// Not thread safe.
70class LPParser {
71 public:
72 // Accepts the string in LP file format (used by LinearProgram::Dump()).
73 // On success, populates the linear program *lp and returns true. Otherwise,
74 // returns false and leaves *lp in an unspecified state.
75 ABSL_MUST_USE_RESULT bool Parse(absl::string_view model, LinearProgram* lp);
76
77 private:
78 bool ParseEmptyLine(StringPiece line);
79 bool ParseObjective(StringPiece objective);
80 bool ParseIntegerVariablesList(StringPiece line);
81 bool ParseConstraint(StringPiece constraint);
82 TokenType ConsumeToken(StringPiece* sp);
83 bool SetVariableBounds(ColIndex col, Fractional lb, Fractional ub);
84
85 // Linear program populated by the Parse() method. Not owned.
86 LinearProgram* lp_;
87
88 // Contains the last consumed coefficient and name. The name can be the
89 // optimization direction, a constraint name, or a variable name.
90 Fractional consumed_coeff_;
91 std::string consumed_name_;
92
93 // To remember whether the variable bounds had already been set.
94 std::set<ColIndex> bounded_variables_;
95};
96
97bool LPParser::Parse(absl::string_view model, LinearProgram* lp) {
98 lp_ = lp;
99 bounded_variables_.clear();
100 lp_->Clear();
101
102 std::vector<StringPiece> lines =
103 absl::StrSplit(model, ';', absl::SkipEmpty());
104 bool has_objective = false;
105
106 for (StringPiece line : lines) {
107 if (!has_objective && ParseObjective(line)) {
108 has_objective = true;
109 } else if (!ParseConstraint(line) && !ParseIntegerVariablesList(line) &&
110 !ParseEmptyLine(line)) {
111 LOG(INFO) << "Error in line: " << line;
112 return false;
113 }
114 }
115
116 // Bound the non-bounded variables between -inf and +inf. We need to do this,
117 // as glop bounds a variable by default between 0 and +inf.
118 for (ColIndex col(0); col < lp_->num_variables(); ++col) {
119 if (bounded_variables_.find(col) == bounded_variables_.end()) {
120 lp_->SetVariableBounds(col, -kInfinity, +kInfinity);
121 }
122 }
123
124 lp_->CleanUp();
125 return true;
126}
127
128bool LPParser::ParseEmptyLine(StringPiece line) {
129 if (ConsumeToken(&line) == TokenType::END) return true;
130 return false;
131}
132
133bool LPParser::ParseObjective(StringPiece objective) {
134 // Get the required optimization direction.
135 if (ConsumeToken(&objective) != TokenType::NAME) return false;
136 if (absl::EqualsIgnoreCase(consumed_name_, "min")) {
137 lp_->SetMaximizationProblem(false);
138 } else if (absl::EqualsIgnoreCase(consumed_name_, "max")) {
139 lp_->SetMaximizationProblem(true);
140 } else {
141 return false;
142 }
143
144 // Get the optional offset.
145 TokenType token_type = ConsumeToken(&objective);
146 if (token_type == TokenType::VALUE) {
147 lp_->SetObjectiveOffset(consumed_coeff_);
148 token_type = ConsumeToken(&objective);
149 } else {
150 lp_->SetObjectiveOffset(0.0);
151 }
152
153 // Get the addands.
154 while (token_type == TokenType::ADDAND) {
155 const ColIndex col = lp_->FindOrCreateVariable(consumed_name_);
156 if (lp_->objective_coefficients()[col] != 0.0) return false;
157 lp_->SetObjectiveCoefficient(col, consumed_coeff_);
158 token_type = ConsumeToken(&objective);
159 }
160 return token_type == TokenType::END;
161}
162
163bool LPParser::ParseIntegerVariablesList(StringPiece line) {
164 // Get the required "int" or "bin" keyword.
165 bool binary_list = false;
166 if (ConsumeToken(&line) != TokenType::NAME) return false;
167 if (absl::EqualsIgnoreCase(consumed_name_, "bin")) {
168 binary_list = true;
169 } else if (!absl::EqualsIgnoreCase(consumed_name_, "int")) {
170 return false;
171 }
172
173 // Get the list of integer variables, separated by optional comas.
174 TokenType token_type = ConsumeToken(&line);
175 while (token_type == TokenType::ADDAND) {
176 if (consumed_coeff_ != 1.0) return false;
177 const ColIndex col = lp_->FindOrCreateVariable(consumed_name_);
178 lp_->SetVariableType(col, LinearProgram::VariableType::INTEGER);
179 if (binary_list && !SetVariableBounds(col, 0.0, 1.0)) return false;
180 token_type = ConsumeToken(&line);
181 if (token_type == TokenType::COMA) {
182 token_type = ConsumeToken(&line);
183 }
184 }
185
186 // The last token must be END.
187 if (token_type != TokenType::END) return false;
188 return true;
189}
190
191bool LPParser::ParseConstraint(StringPiece constraint) {
192 const StatusOr<ParsedConstraint> parsed_constraint_or_status =
193 ::operations_research::glop::ParseConstraint(constraint);
194 if (!parsed_constraint_or_status.ok()) return false;
195 const ParsedConstraint& parsed_constraint =
196 parsed_constraint_or_status.value();
197
198 // Set the variables bounds without creating new constraints.
199 if (parsed_constraint.name.empty() &&
200 parsed_constraint.coefficients.size() == 1 &&
201 parsed_constraint.coefficients[0] == 1.0) {
202 const ColIndex col =
203 lp_->FindOrCreateVariable(parsed_constraint.variable_names[0]);
204 if (!SetVariableBounds(col, parsed_constraint.lower_bound,
205 parsed_constraint.upper_bound)) {
206 return false;
207 }
208 } else {
209 const RowIndex num_constraints_before_adding_variable =
210 lp_->num_constraints();
211 // The constaint has a name, or there are more than variable, or the
212 // coefficient is not 1. Thus, create and fill a new constraint.
213 // We don't use SetConstraintName() because constraints named that way
214 // cannot be found via FindOrCreateConstraint() (see comment on
215 // SetConstraintName()), which can be useful for tests using ParseLP.
216 const RowIndex row =
217 parsed_constraint.name.empty()
218 ? lp_->CreateNewConstraint()
219 : lp_->FindOrCreateConstraint(parsed_constraint.name);
220 if (lp_->num_constraints() == num_constraints_before_adding_variable) {
221 // No constraints were added, meaning we found one.
222 LOG(INFO) << "Two constraints with the same name: "
223 << parsed_constraint.name;
224 return false;
225 }
226 if (!AreBoundsValid(parsed_constraint.lower_bound,
227 parsed_constraint.upper_bound)) {
228 return false;
229 }
230 lp_->SetConstraintBounds(row, parsed_constraint.lower_bound,
231 parsed_constraint.upper_bound);
232 for (int i = 0; i < parsed_constraint.variable_names.size(); ++i) {
233 const ColIndex variable =
234 lp_->FindOrCreateVariable(parsed_constraint.variable_names[i]);
235 lp_->SetCoefficient(row, variable, parsed_constraint.coefficients[i]);
236 }
237 }
238 return true;
239}
240
241bool LPParser::SetVariableBounds(ColIndex col, Fractional lb, Fractional ub) {
242 if (bounded_variables_.find(col) == bounded_variables_.end()) {
243 // The variable was not bounded yet, thus reset its bounds.
244 bounded_variables_.insert(col);
245 lp_->SetVariableBounds(col, -kInfinity, kInfinity);
246 }
247 // Set the bounds only if their stricter and valid.
248 lb = std::max(lb, lp_->variable_lower_bounds()[col]);
249 ub = std::min(ub, lp_->variable_upper_bounds()[col]);
250 if (!AreBoundsValid(lb, ub)) return false;
251 lp_->SetVariableBounds(col, lb, ub);
252 return true;
253}
254
255TokenType ConsumeToken(StringPiece* sp, std::string* consumed_name,
256 double* consumed_coeff) {
257 DCHECK(consumed_name != nullptr);
258 DCHECK(consumed_coeff != nullptr);
259 // We use LazyRE2 everywhere so that all the patterns are just compiled once
260 // when they are needed for the first time. This speed up the code
261 // significantly. Note that the use of LazyRE2 is thread safe.
262 static const LazyRE2 kEndPattern = {R"(\s*)"};
263
264 // There is nothing more to consume.
265 if (sp->empty() || RE2::FullMatch(*sp, *kEndPattern)) {
266 return TokenType::END;
267 }
268
269 // Return NAME if the next token is a line name, or integer variable list
270 // indicator.
271 static const LazyRE2 kNamePattern1 = {R"(\s*(\w[\w[\]]*):)"};
272 static const LazyRE2 kNamePattern2 = {R"((?i)\s*(int)\s*:?)"};
273 static const LazyRE2 kNamePattern3 = {R"((?i)\s*(bin)\s*:?)"};
274 if (RE2::Consume(sp, *kNamePattern1, consumed_name)) return TokenType::NAME;
275 if (RE2::Consume(sp, *kNamePattern2, consumed_name)) return TokenType::NAME;
276 if (RE2::Consume(sp, *kNamePattern3, consumed_name)) return TokenType::NAME;
277
278 // Return SIGN_* if the next token is a relation sign.
279 static const LazyRE2 kLePattern = {R"(\s*<=?)"};
280 if (RE2::Consume(sp, *kLePattern)) return TokenType::SIGN_LE;
281 static const LazyRE2 kEqPattern = {R"(\s*=)"};
282 if (RE2::Consume(sp, *kEqPattern)) return TokenType::SIGN_EQ;
283 static const LazyRE2 kGePattern = {R"(\s*>=?)"};
284 if (RE2::Consume(sp, *kGePattern)) return TokenType::SIGN_GE;
285
286 // Return COMA if the next token is a coma.
287 static const LazyRE2 kComaPattern = {R"(\s*\,)"};
288 if (RE2::Consume(sp, *kComaPattern)) return TokenType::COMA;
289
290 // Consume all plus and minus signs.
291 std::string sign;
292 int minus_count = 0;
293 static const LazyRE2 kSignPattern = {R"(\s*([-+]{1}))"};
294 while (RE2::Consume(sp, *kSignPattern, &sign)) {
295 if (sign == "-") minus_count++;
296 }
297
298 // Return INF if the next token is an infinite value.
299 static const LazyRE2 kInfPattern = {R"((?i)\s*inf)"};
300 if (RE2::Consume(sp, *kInfPattern)) {
301 *consumed_coeff = minus_count % 2 == 0 ? kInfinity : -kInfinity;
302 return TokenType::INF;
303 }
304
305 // Check if the next token is a value. If it is infinite return INF.
306 std::string coeff;
307 bool has_value = false;
308 static const LazyRE2 kValuePattern = {
309 R"(\s*([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?))"};
310 if (RE2::Consume(sp, *kValuePattern, &coeff)) {
311 if (!absl::SimpleAtod(coeff, consumed_coeff)) {
312 // Note: If absl::SimpleAtod(), Consume(), and kValuePattern are correct,
313 // this should never happen.
314 LOG(ERROR) << "Text: " << coeff << " was matched by RE2 to be "
315 << "a floating point number, but absl::SimpleAtod() failed.";
316 return TokenType::ERROR;
317 }
318 if (!IsFinite(*consumed_coeff)) {
319 VLOG(1) << "Value " << coeff << " treated as infinite.";
320 return TokenType::INF;
321 }
322 has_value = true;
323 } else {
324 *consumed_coeff = 1.0;
325 }
326 if (minus_count % 2 == 1) *consumed_coeff *= -1.0;
327
328 // Return ADDAND (coefficient and name) if the next token is a variable name.
329 // Otherwise, if we found a finite value previously, return VALUE.
330 // Otherwise, return ERROR.
331 std::string multiplication;
332 static const LazyRE2 kAddandPattern = {R"(\s*(\*?)\s*([a-zA-Z_)][\w[\])]*))"};
333 if (RE2::Consume(sp, *kAddandPattern, &multiplication, consumed_name)) {
334 if (!multiplication.empty() && !has_value) return TokenType::ERROR;
335 return TokenType::ADDAND;
336 } else if (has_value) {
337 return TokenType::VALUE;
338 }
339
340 return TokenType::ERROR;
341}
342
343TokenType LPParser::ConsumeToken(StringPiece* sp) {
344 using ::operations_research::glop::ConsumeToken;
345 return ConsumeToken(sp, &consumed_name_, &consumed_coeff_);
346}
347
348} // namespace
349
350StatusOr<ParsedConstraint> ParseConstraint(absl::string_view constraint) {
351 ParsedConstraint parsed_constraint;
352 // Get the name, if present.
353 StringPiece constraint_copy{constraint};
354 std::string consumed_name;
355 Fractional consumed_coeff;
356 if (ConsumeToken(&constraint_copy, &consumed_name, &consumed_coeff) ==
357 TokenType::NAME) {
358 parsed_constraint.name = consumed_name;
359 constraint = constraint_copy;
360 }
361
362 Fractional left_bound;
363 Fractional right_bound;
364 TokenType left_sign(TokenType::END);
365 TokenType right_sign(TokenType::END);
366 absl::flat_hash_set<std::string> used_variables;
367
368 // Get the left bound and the relation sign, if present.
369 TokenType token_type =
370 ConsumeToken(&constraint, &consumed_name, &consumed_coeff);
371 if (TokenIsBound(token_type)) {
372 left_bound = consumed_coeff;
373 left_sign = ConsumeToken(&constraint, &consumed_name, &consumed_coeff);
374 if (left_sign != TokenType::SIGN_LE && left_sign != TokenType::SIGN_EQ &&
375 left_sign != TokenType::SIGN_GE) {
376 return absl::InvalidArgumentError(
377 "Expected an equality/inequality sign for the left bound.");
378 }
379 token_type = ConsumeToken(&constraint, &consumed_name, &consumed_coeff);
380 }
381
382 // Get the addands, if present.
383 while (token_type == TokenType::ADDAND) {
384 if (used_variables.contains(consumed_name)) {
385 return absl::InvalidArgumentError(
386 absl::StrCat("Duplicate variable name: ", consumed_name));
387 }
388 used_variables.insert(consumed_name);
389 parsed_constraint.variable_names.push_back(consumed_name);
390 parsed_constraint.coefficients.push_back(consumed_coeff);
391 token_type = ConsumeToken(&constraint, &consumed_name, &consumed_coeff);
392 }
393
394 // If the left sign was EQ there can be no right side.
395 if (left_sign == TokenType::SIGN_EQ && token_type != TokenType::END) {
396 return absl::InvalidArgumentError(
397 "Equality constraints can have only one bound.");
398 }
399
400 // Get the right sign and the right bound, if present.
401 if (token_type != TokenType::END) {
402 right_sign = token_type;
403 if (right_sign != TokenType::SIGN_LE && right_sign != TokenType::SIGN_EQ &&
404 right_sign != TokenType::SIGN_GE) {
405 return absl::InvalidArgumentError(
406 "Expected an equality/inequality sign for the right bound.");
407 }
408 // If the right sign is EQ, there can be no left side.
409 if (left_sign != TokenType::END && right_sign == TokenType::SIGN_EQ) {
410 return absl::InvalidArgumentError(
411 "Equality constraints can have only one bound.");
412 }
413 if (!TokenIsBound(
414 ConsumeToken(&constraint, &consumed_name, &consumed_coeff))) {
415 return absl::InvalidArgumentError("Bound value was expected.");
416 }
417 right_bound = consumed_coeff;
418 if (ConsumeToken(&constraint, &consumed_name, &consumed_coeff) !=
419 TokenType::END) {
420 return absl::InvalidArgumentError(
421 absl::StrCat("End of input was expected, found: ", constraint));
422 }
423 }
424
425 // There was no constraint!
426 if (left_sign == TokenType::END && right_sign == TokenType::END) {
427 return absl::InvalidArgumentError("The input constraint was empty.");
428 }
429
430 // Calculate bounds to set.
431 parsed_constraint.lower_bound = -kInfinity;
432 parsed_constraint.upper_bound = kInfinity;
433 if (left_sign == TokenType::SIGN_LE || left_sign == TokenType::SIGN_EQ) {
434 parsed_constraint.lower_bound = left_bound;
435 }
436 if (left_sign == TokenType::SIGN_GE || left_sign == TokenType::SIGN_EQ) {
437 parsed_constraint.upper_bound = left_bound;
438 }
439 if (right_sign == TokenType::SIGN_LE || right_sign == TokenType::SIGN_EQ) {
440 parsed_constraint.upper_bound =
441 std::min(parsed_constraint.upper_bound, right_bound);
442 }
443 if (right_sign == TokenType::SIGN_GE || right_sign == TokenType::SIGN_EQ) {
444 parsed_constraint.lower_bound =
445 std::max(parsed_constraint.lower_bound, right_bound);
446 }
447 return parsed_constraint;
448}
449
450bool ParseLp(absl::string_view model, LinearProgram* lp) {
451 LPParser parser;
452 return parser.Parse(model, lp);
453}
454
455} // namespace glop
456
457absl::StatusOr<MPModelProto> ModelProtoFromLpFormat(absl::string_view model) {
458 glop::LinearProgram lp;
459 if (!ParseLp(model, &lp)) {
460 return absl::InvalidArgumentError("Parsing error, see LOGs for details.");
461 }
462 MPModelProto model_proto;
463 LinearProgramToMPModelProto(lp, &model_proto);
464 return model_proto;
465}
466
467} // namespace operations_research
468
469#endif // defined(USE_LP_PARSER)
@ INTEGER
The variable must only take integer values.
GRBmodel * model
ColIndex col
Definition markowitz.cc:187
RowIndex row
Definition markowitz.cc:186
constexpr double kInfinity
Infinity for type Fractional.
Definition lp_types.h:89
bool AreBoundsValid(Fractional lower_bound, Fractional upper_bound)
Definition lp_data.h:707
void LinearProgramToMPModelProto(const LinearProgram &input, MPModelProto *output)
Converts a LinearProgram to a MPModelProto.
bool IsFinite(Fractional value)
Definition lp_types.h:96
In SWIG mode, we don't want anything besides these top-level includes.
int line