Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
model_validator.cc
Go to the documentation of this file.
1// Copyright 2010-2024 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <algorithm>
17#include <cmath>
18#include <cstdlib>
19#include <limits>
20#include <optional>
21#include <string>
22#include <utility>
23#include <vector>
24
25#include "absl/container/flat_hash_map.h"
26#include "absl/container/flat_hash_set.h"
27#include "absl/status/status.h"
28#include "absl/status/statusor.h"
29#include "absl/strings/match.h"
30#include "absl/strings/str_cat.h"
31#include "absl/strings/str_format.h"
32#include "absl/strings/string_view.h"
33#include "absl/types/optional.h"
37#include "ortools/linear_solver/linear_solver.pb.h"
38#include "ortools/port/file.h"
42
44 double, model_validator_infinity, 1e100,
45 "Anything above or equal to this magnitude will be considered infinity.");
46
47namespace operations_research {
48namespace {
49
50bool IsNanOrAbsGreaterThanOrEqual(double value, double abs_value_threshold) {
51 return std::isnan(value) || std::abs(value) >= abs_value_threshold;
52}
53
54// Internal method to detect errors in bounds. The object passed as parameter
55// must have "lower_bound" and "upper_bound" fields.
56template <typename BoundedElement>
57std::string FindErrorInBounds(const BoundedElement& element,
58 double abs_value_threshold,
59 const bool accept_trivially_infeasible_bounds) {
60 if (std::isnan(element.lower_bound()) || std::isnan(element.upper_bound()) ||
61 element.lower_bound() >= abs_value_threshold ||
62 element.upper_bound() <= -abs_value_threshold ||
63 (!accept_trivially_infeasible_bounds &&
64 element.lower_bound() > element.upper_bound())) {
65 return absl::StrFormat("Infeasible bounds: [%f, %f]", element.lower_bound(),
66 element.upper_bound());
67 }
68 return "";
69}
70
71// Internal method to detect errors in a single variable.
72std::string FindErrorInMPVariable(
73 const MPVariableProto& variable, double abs_value_threshold,
74 const bool accept_trivially_infeasible_bounds) {
75 const std::string bound_error = FindErrorInBounds(
76 variable, abs_value_threshold, accept_trivially_infeasible_bounds);
77 if (!bound_error.empty()) return bound_error;
78
79 if (!accept_trivially_infeasible_bounds && variable.is_integer() &&
80 ceil(variable.lower_bound()) > floor(variable.upper_bound())) {
81 return absl::StrCat(
82 "Infeasible bounds for integer variable: [", (variable.lower_bound()),
83 ", ", (variable.upper_bound()), "]", " translate to the empty set");
84 }
85 if (IsNanOrAbsGreaterThanOrEqual(variable.objective_coefficient(),
86 abs_value_threshold)) {
87 return absl::StrCat("Invalid objective_coefficient: ",
88 (variable.objective_coefficient()));
89 }
90 return std::string();
91}
92
93// Returns an error message if 'var_indices' contains a duplicate index.
94template <typename Iterable>
95std::string FindDuplicateVarIndex(const Iterable& var_indices,
96 std::vector<bool>* var_mask) {
97 int duplicate_var_index = -1;
98 for (const int var_index : var_indices) {
99 if ((*var_mask)[var_index]) duplicate_var_index = var_index;
100 (*var_mask)[var_index] = true;
101 }
102 // Reset "var_mask" to all false, sparsely.
103 for (const int var_index : var_indices) {
104 (*var_mask)[var_index] = false;
105 }
106 if (duplicate_var_index >= 0) {
107 return absl::StrCat("var_index #", duplicate_var_index,
108 " appears several times");
109 }
110 return "";
111}
112
113// Internal method to detect errors in a single constraint.
114// "var_mask" is a vector<bool> whose size is the number of variables in
115// the model, and it will be all set to false before and after the call.
116std::string FindErrorInMPConstraint(
117 const MPConstraintProto& constraint, std::vector<bool>* var_mask,
118 double abs_value_threshold, const bool accept_trivially_infeasible_bounds) {
119 const std::string bound_error = FindErrorInBounds(
120 constraint, abs_value_threshold, accept_trivially_infeasible_bounds);
121 if (!bound_error.empty()) return bound_error;
122
123 // TODO(user): clarify explicitly, at least in a comment, whether we want
124 // to accept empty constraints (i.e. without variables).
125
126 const int num_vars_in_model = var_mask->size();
127 const int num_vars_in_ct = constraint.var_index_size();
128 const int num_coeffs_in_ct = constraint.coefficient_size();
129 if (num_vars_in_ct != num_coeffs_in_ct) {
130 return absl::StrCat("var_index_size() != coefficient_size() (",
131 num_vars_in_ct, " VS ", num_coeffs_in_ct);
132 }
133 for (int i = 0; i < num_vars_in_ct; ++i) {
134 const int var_index = constraint.var_index(i);
135 if (var_index >= num_vars_in_model || var_index < 0) {
136 return absl::StrCat("var_index(", i, ")=", var_index,
137 " is out of bounds");
138 }
139 const double coeff = constraint.coefficient(i);
140 if (IsNanOrAbsGreaterThanOrEqual(coeff, abs_value_threshold)) {
141 return absl::StrCat("coefficient(", i, ")=", (coeff), " is invalid");
142 }
143 }
144
145 const std::string error =
146 FindDuplicateVarIndex(constraint.var_index(), var_mask);
147 if (!error.empty()) return error;
148
149 // We found no error, all is fine.
150 return std::string();
151}
152
153std::string CroppedConstraintDebugString(const MPConstraintProto& constraint) {
154 const int kMaxPrintedVars = 10;
155
156 MPConstraintProto constraint_light = constraint;
157 std::string suffix_str;
158 if (constraint.var_index_size() > kMaxPrintedVars) {
159 constraint_light.mutable_var_index()->Truncate(kMaxPrintedVars);
160 absl::StrAppend(&suffix_str,
161 " (var_index cropped; size=", constraint.var_index_size(),
162 ").");
163 }
164 if (constraint.coefficient_size() > kMaxPrintedVars) {
165 constraint_light.mutable_coefficient()->Truncate(kMaxPrintedVars);
166 absl::StrAppend(&suffix_str, " (coefficient cropped; size=",
167 constraint.coefficient_size(), ").");
168 }
169 return absl::StrCat("Constraint proto: ",
170 ProtobufShortDebugString(constraint_light), suffix_str);
171}
172
173bool IsBoolean(const MPVariableProto& variable) {
174 if (variable.lower_bound() < 0) return false;
175 if (variable.upper_bound() > 1) return false;
176 return variable.is_integer();
177}
178
179std::string FindErrorInMPIndicatorConstraint(
180 const MPModelProto& model, const MPIndicatorConstraint& indicator,
181 std::vector<bool>* var_mask, double abs_value_threshold,
182 bool accept_trivially_infeasible_bounds) {
183 if (!indicator.has_var_index()) {
184 return "var_index is required.";
185 }
186 const int var_index = indicator.var_index();
187 if (var_index < 0 || var_index >= model.variable_size()) {
188 return absl::StrCat("var_index=", var_index, " is out of bounds.");
189 }
190 if (!IsBoolean(model.variable(var_index))) {
191 return absl::StrCat("var_index=", var_index, " is not Boolean.");
192 }
193 const int var_value = indicator.var_value();
194 if (var_value < 0 || var_value > 1) {
195 return absl::StrCat("var_value=", var_value, " must be 0 or 1.");
196 }
197 const MPConstraintProto& constraint = indicator.constraint();
198 std::string error =
199 FindErrorInMPConstraint(constraint, var_mask, abs_value_threshold,
200 accept_trivially_infeasible_bounds);
201 if (!error.empty()) {
202 // Constraint protos can be huge, theoretically. So we guard against
203 // that.
204 return absl::StrCat(error, " in constraint ",
205 CroppedConstraintDebugString(constraint));
206 }
207 return "";
208}
209
210std::string FindErrorInMPSosConstraint(const MPModelProto& model,
211 const MPSosConstraint& sos,
212 std::vector<bool>* var_mask,
213 double abs_value_threshold) {
214 if (sos.weight_size() != 0 && sos.weight_size() != sos.var_index_size()) {
215 return "weight_size() > 0 and var_index_size() != weight_size()";
216 }
217 for (const int var_index : sos.var_index()) {
218 if (var_index < 0 || var_index >= model.variable_size()) {
219 return absl::StrCat("var_index=", var_index, " is out of bounds.");
220 }
221 }
222 for (int i = 0; i < sos.weight_size(); ++i) {
223 if (IsNanOrAbsGreaterThanOrEqual(sos.weight(i), abs_value_threshold)) {
224 return absl::StrCat("Invalid weight: ", sos.weight(i));
225 }
226 if (i == 0) continue;
227 if (sos.weight(i - 1) >= sos.weight(i)) {
228 return "SOS weights must be strictly increasing";
229 }
230 }
231
232 const std::string error = FindDuplicateVarIndex(sos.var_index(), var_mask);
233 if (!error.empty()) return error;
234
235 return "";
236}
237
238std::string FindErrorInMPQuadraticConstraint(
239 const MPModelProto& model, const MPQuadraticConstraint& qcst,
240 std::vector<bool>* var_mask, double abs_value_threshold,
241 bool accept_trivially_infeasible_bounds) {
242 const int num_vars = model.variable_size();
243
244 if (qcst.var_index_size() != qcst.coefficient_size()) {
245 return "var_index_size() != coefficient_size()";
246 }
247
248 const std::string bound_error = FindErrorInBounds(
249 qcst, abs_value_threshold, accept_trivially_infeasible_bounds);
250 if (!bound_error.empty()) return bound_error;
251
252 for (int i = 0; i < qcst.var_index_size(); ++i) {
253 if (qcst.var_index(i) < 0 || qcst.var_index(i) >= num_vars) {
254 return absl::StrCat("var_index(", i, ")=", qcst.var_index(i),
255 " is invalid.", " It must be in [0, ", num_vars, ")");
256 }
257
258 if (IsNanOrAbsGreaterThanOrEqual(qcst.coefficient(i),
259 abs_value_threshold)) {
260 return absl::StrCat("coefficient(", i, ")=", qcst.coefficient(i),
261 " is invalid");
262 }
263 }
264 const std::string duplicate_error =
265 FindDuplicateVarIndex(qcst.var_index(), var_mask);
266 if (!duplicate_error.empty()) return duplicate_error;
267
268 if (qcst.qvar1_index_size() != qcst.qvar2_index_size() ||
269 qcst.qvar1_index_size() != qcst.qcoefficient_size()) {
270 return "quadratic indices and coefficients must have the same size";
271 }
272 for (int i = 0; i < qcst.qvar1_index_size(); ++i) {
273 if (qcst.qvar1_index(i) >= num_vars || qcst.qvar1_index(i) < 0) {
274 return absl::StrCat("qvar1_index(", i, ")=", qcst.qvar1_index(i),
275 " is invalid.", " It must be in [0, ", num_vars, ")");
276 }
277
278 if (qcst.qvar2_index(i) >= num_vars || qcst.qvar2_index(i) < 0) {
279 return absl::StrCat("qvar2_index(", i, ")=", qcst.qvar2_index(i),
280 " is invalid.", " It must be in [0, ", num_vars, ")");
281 }
282
283 if (IsNanOrAbsGreaterThanOrEqual(qcst.qcoefficient(i),
284 abs_value_threshold)) {
285 return absl::StrCat("qcoefficient(", i, ")=", qcst.qcoefficient(i),
286 " is invalid");
287 }
288 }
289
290 return "";
291}
292
293std::string FindErrorInMPAbsConstraint(const MPModelProto& model,
294 const MPAbsConstraint& abs) {
295 if (!abs.has_var_index()) {
296 return "var_index is required.";
297 }
298 if (!abs.has_resultant_var_index()) {
299 return "resultant_var_index is required.";
300 }
301
302 const int num_vars = model.variable_size();
303 if (abs.var_index() < 0 || abs.var_index() >= num_vars) {
304 return absl::StrCat("var_index=", abs.var_index(), " is invalid.",
305 " It must be in [0, ", num_vars, ")");
306 }
307 if (abs.resultant_var_index() < 0 || abs.resultant_var_index() >= num_vars) {
308 return absl::StrCat("var_index=", abs.resultant_var_index(), " is invalid.",
309 " It must be in [0, ", num_vars, ")");
310 }
311 return "";
312}
313
314std::string FindErrorInMPAndOrConstraint(const MPModelProto& model,
315 const MPArrayConstraint& and_or) {
316 if (and_or.var_index_size() == 0) {
317 return "var_index cannot be empty.";
318 }
319 if (!and_or.has_resultant_var_index()) {
320 return "resultant_var_index is required.";
321 }
322
323 const int num_vars = model.variable_size();
324 for (int i = 0; i < and_or.var_index_size(); ++i) {
325 if (and_or.var_index(i) < 0 || and_or.var_index(i) >= num_vars) {
326 return absl::StrCat("var_index(", i, ")=", and_or.var_index(i),
327 " is invalid.", " It must be in [0, ", num_vars, ")");
328 }
329 if (!IsBoolean(model.variable(and_or.var_index(i)))) {
330 return absl::StrCat("var_index=", i, " is not Boolean.");
331 }
332 }
333 if (and_or.resultant_var_index() < 0 ||
334 and_or.resultant_var_index() >= num_vars) {
335 return absl::StrCat("resultant_var_index=", and_or.resultant_var_index(),
336 " is invalid.", " It must be in [0, ", num_vars, ")");
337 }
338 if (!IsBoolean(model.variable(and_or.resultant_var_index()))) {
339 return absl::StrCat("resultant_var_index is not Boolean.");
340 }
341 return "";
342}
343
344std::string FindErrorInMPMinMaxConstraint(
345 const MPModelProto& model, const MPArrayWithConstantConstraint& min_max,
346 double abs_value_threshold) {
347 if (min_max.var_index_size() == 0) {
348 return "var_index cannot be empty.";
349 }
350 if (!min_max.has_resultant_var_index()) {
351 return "resultant_var_index is required.";
352 }
353
354 if (IsNanOrAbsGreaterThanOrEqual(min_max.constant(), abs_value_threshold)) {
355 return absl::StrCat("Invalid constant: ", (min_max.constant()));
356 }
357
358 const int num_vars = model.variable_size();
359 for (int i = 0; i < min_max.var_index_size(); ++i) {
360 if (min_max.var_index(i) < 0 || min_max.var_index(i) >= num_vars) {
361 return absl::StrCat("var_index(", i, ")=", min_max.var_index(i),
362 " is invalid.", " It must be in [0, ", num_vars, ")");
363 }
364 }
365 if (min_max.resultant_var_index() < 0 ||
366 min_max.resultant_var_index() >= num_vars) {
367 return absl::StrCat("resultant_var_index=", min_max.resultant_var_index(),
368 " is invalid.", " It must be in [0, ", num_vars, ")");
369 }
370 return "";
371}
372
373std::string FindErrorInQuadraticObjective(const MPQuadraticObjective& qobj,
374 int num_vars,
375 double abs_value_threshold) {
376 if (qobj.qvar1_index_size() != qobj.qvar2_index_size() ||
377 qobj.qvar1_index_size() != qobj.coefficient_size()) {
378 return "indices and coefficients must have the same size";
379 }
380
381 for (int i = 0; i < qobj.qvar1_index_size(); ++i) {
382 if (qobj.qvar1_index(i) >= num_vars || qobj.qvar1_index(i) < 0) {
383 return absl::StrCat("qvar1_index(", i, ")=", qobj.qvar1_index(i),
384 " is invalid.", " It must be in [0, ", num_vars, ")");
385 }
386
387 if (qobj.qvar2_index(i) >= num_vars || qobj.qvar2_index(i) < 0) {
388 return absl::StrCat("qvar2_index(", i, ")=", qobj.qvar2_index(i),
389 " is invalid.", " It must be in [0, ", num_vars, ")");
390 }
391
392 if (IsNanOrAbsGreaterThanOrEqual(qobj.coefficient(i),
393 abs_value_threshold)) {
394 return absl::StrCat("coefficient(", i, ")=", (qobj.coefficient(i)),
395 " is invalid");
396 }
397 }
398 return "";
399}
400
401std::string FindErrorInSolutionHint(
402 const PartialVariableAssignment& solution_hint, int num_vars,
403 double abs_value_threshold) {
404 if (solution_hint.var_index_size() != solution_hint.var_value_size()) {
405 return absl::StrCat("var_index_size() != var_value_size() [",
406 solution_hint.var_index_size(), " VS ",
407 solution_hint.var_value_size());
408 }
409 std::vector<bool> var_in_hint(num_vars, false);
410 for (int i = 0; i < solution_hint.var_index_size(); ++i) {
411 const int var_index = solution_hint.var_index(i);
412 if (var_index >= num_vars || var_index < 0) {
413 return absl::StrCat("var_index(", i, ")=", var_index, " is invalid.",
414 " It must be in [0, ", num_vars, ")");
415 }
416 if (var_in_hint[var_index]) {
417 return absl::StrCat("Duplicate var_index = ", var_index);
418 }
419 var_in_hint[var_index] = true;
420 if (IsNanOrAbsGreaterThanOrEqual(solution_hint.var_value(i),
421 abs_value_threshold)) {
422 return absl::StrCat("var_value(", i, ")=", (solution_hint.var_value(i)),
423 " is invalid");
424 }
425 }
426 return std::string();
427}
428
429namespace {
430// Maps the names of variables (or constraints, or other entities) to their
431// index in the MPModelProto. Non-unique names are supported, but are singled
432// out as such, by setting their index (the 'value' of the map entry) to -1.
433template <class NamedEntity>
434absl::flat_hash_map<std::string, int> BuildNameToIndexMap(
435 const google::protobuf::RepeatedPtrField<NamedEntity>& entities) {
436 absl::flat_hash_map<std::string, int> out;
437 for (int i = 0; i < entities.size(); ++i) {
438 int& index = gtl::LookupOrInsert(&out, entities.Get(i).name(), i);
439 if (index != i) index = -1;
440 }
441 return out;
442}
443
444class LazyMPModelNameToIndexMaps {
445 public:
446 explicit LazyMPModelNameToIndexMaps(const MPModelProto& model)
447 : model_(model) {}
448
449 absl::StatusOr<int> LookupName(
450 MPModelProto::Annotation::TargetType target_type,
451 absl::string_view name) {
452 const absl::flat_hash_map<std::string, int>* map = nullptr;
453 switch (target_type) {
454 case MPModelProto::Annotation::VARIABLE_DEFAULT:
455 if (!variable_name_to_index_) {
456 variable_name_to_index_ = BuildNameToIndexMap(model_.variable());
457 }
458 map = &variable_name_to_index_.value();
459 break;
460 case MPModelProto::Annotation::CONSTRAINT:
461 if (!constraint_name_to_index_) {
462 constraint_name_to_index_ = BuildNameToIndexMap(model_.constraint());
463 }
464 map = &constraint_name_to_index_.value();
465 break;
466 case MPModelProto::Annotation::GENERAL_CONSTRAINT:
467 if (!general_constraint_name_to_index_) {
468 general_constraint_name_to_index_ =
469 BuildNameToIndexMap(model_.general_constraint());
470 }
471 map = &general_constraint_name_to_index_.value();
472 break;
473 }
474 const int index = gtl::FindWithDefault(*map, name, -2);
475 if (index == -2) return absl::NotFoundError("name not found");
476 if (index == -1) return absl::InvalidArgumentError("name is not unique");
477 return index;
478 }
479
480 private:
481 const MPModelProto& model_;
482 std::optional<absl::flat_hash_map<std::string, int>> variable_name_to_index_;
483 std::optional<absl::flat_hash_map<std::string, int>>
484 constraint_name_to_index_;
485 std::optional<absl::flat_hash_map<std::string, int>>
486 general_constraint_name_to_index_;
487};
488} // namespace
489
490std::string FindErrorInAnnotation(const MPModelProto::Annotation& annotation,
491 const MPModelProto& model,
492 LazyMPModelNameToIndexMaps* name_maps) {
493 // Checks related to the 'target' fields.
494 if (!annotation.has_target_index() && !annotation.has_target_name()) {
495 return "One of target_index or target_name must be set";
496 }
497 if (!MPModelProto::Annotation::TargetType_IsValid(annotation.target_type())) {
498 return "Invalid target_type";
499 }
500 int num_entitities = -1;
501 switch (annotation.target_type()) {
502 case MPModelProto::Annotation::VARIABLE_DEFAULT:
503 num_entitities = model.variable_size();
504 break;
505 case MPModelProto::Annotation::CONSTRAINT:
506 num_entitities = model.constraint_size();
507 break;
508 case MPModelProto::Annotation::GENERAL_CONSTRAINT:
509 num_entitities = model.general_constraint_size();
510 break;
511 }
512 int target_index = -1;
513 if (annotation.has_target_index()) {
514 target_index = annotation.target_index();
515 if (target_index < 0 || target_index >= num_entitities) {
516 return "Invalid target_index";
517 }
518 }
519 if (annotation.has_target_name()) {
520 if (annotation.has_target_index()) {
521 // No need to build the name lookup maps to verify consistency: we can
522 // even accept a name that is not unique, as long as the pointed entity
523 // (identified by its index) has the right name.
524 std::string name;
525 switch (annotation.target_type()) {
526 case MPModelProto::Annotation::VARIABLE_DEFAULT:
527 name = model.variable(target_index).name();
528 break;
529 case MPModelProto::Annotation::CONSTRAINT:
530 name = model.constraint(target_index).name();
531 break;
532 case MPModelProto::Annotation::GENERAL_CONSTRAINT:
533 name = model.general_constraint(target_index).name();
534 break;
535 }
536 if (annotation.target_name() != name) {
537 return absl::StrFormat(
538 "target_name='%s' doesn't match the name '%s' of target_index=%d",
539 annotation.target_name(), name, target_index);
540 }
541 } else { // !annotation.has_target_index()
542 const absl::StatusOr<int> index_or = name_maps->LookupName(
543 annotation.target_type(), annotation.target_name());
544 if (!index_or.ok()) {
545 return absl::StrCat("Bad target_name: ", index_or.status().message());
546 }
547 target_index = index_or.value();
548 }
549 }
550
551 // As of 2022-02, there are no checks related to the 'payload' fields. They
552 // can be set, unset, everything goes.
553 return "";
554}
555
556} // namespace
557
559 const MPModelProto& model, double abs_value_threshold,
560 const bool accept_trivially_infeasible_bounds) {
561 // NOTE(user): Empty models are considered fine by this function, although
562 // it is not clear whether MPSolver::Solve() will always respond in the same
563 // way, depending on the solvers.
564 if (abs_value_threshold == 0.0) {
565 abs_value_threshold = absl::GetFlag(FLAGS_model_validator_infinity);
566 }
567
568 if (IsNanOrAbsGreaterThanOrEqual(model.objective_offset(),
569 abs_value_threshold)) {
570 return absl::StrCat("Invalid objective_offset: ",
571 (model.objective_offset()));
572 }
573 const int num_vars = model.variable_size();
574 const int num_cts = model.constraint_size();
575
576 // Validate variables.
577 std::string error;
578 for (int i = 0; i < num_vars; ++i) {
579 error = FindErrorInMPVariable(model.variable(i), abs_value_threshold,
580 accept_trivially_infeasible_bounds);
581 if (!error.empty()) {
582 return absl::StrCat("In variable #", i, ": ", error, ". Variable proto: ",
583 ProtobufShortDebugString(model.variable(i)));
584 }
585 }
586
587 // Validate constraints.
588 std::vector<bool> variable_appears(num_vars, false);
589 for (int i = 0; i < num_cts; ++i) {
590 const MPConstraintProto& constraint = model.constraint(i);
591 error = FindErrorInMPConstraint(constraint, &variable_appears,
592 abs_value_threshold,
593 accept_trivially_infeasible_bounds);
594 if (!error.empty()) {
595 // Constraint protos can be huge, theoretically. So we guard against that.
596 return absl::StrCat("In constraint #", i, ": ", error, ". ",
597 CroppedConstraintDebugString(constraint));
598 }
599 }
600
601 // Validate general constraints.
602 for (int i = 0; i < model.general_constraint_size(); ++i) {
603 const MPGeneralConstraintProto& gen_constraint =
604 model.general_constraint(i);
605 std::string error;
606 switch (gen_constraint.general_constraint_case()) {
607 case MPGeneralConstraintProto::kIndicatorConstraint:
608 error = FindErrorInMPIndicatorConstraint(
609 model, gen_constraint.indicator_constraint(), &variable_appears,
610 abs_value_threshold, accept_trivially_infeasible_bounds);
611 break;
612
613 case MPGeneralConstraintProto::kSosConstraint:
614 error =
615 FindErrorInMPSosConstraint(model, gen_constraint.sos_constraint(),
616 &variable_appears, abs_value_threshold);
617 break;
618
619 case MPGeneralConstraintProto::kQuadraticConstraint:
620 error = FindErrorInMPQuadraticConstraint(
621 model, gen_constraint.quadratic_constraint(), &variable_appears,
622 abs_value_threshold, accept_trivially_infeasible_bounds);
623 break;
624
625 case MPGeneralConstraintProto::kAbsConstraint:
626 error =
627 FindErrorInMPAbsConstraint(model, gen_constraint.abs_constraint());
628 break;
629
630 case MPGeneralConstraintProto::kAndConstraint:
631 error = FindErrorInMPAndOrConstraint(model,
632 gen_constraint.and_constraint());
633 break;
634
635 case MPGeneralConstraintProto::kOrConstraint:
636 error =
637 FindErrorInMPAndOrConstraint(model, gen_constraint.or_constraint());
638 break;
639
640 case MPGeneralConstraintProto::kMinConstraint:
641 error = FindErrorInMPMinMaxConstraint(
642 model, gen_constraint.min_constraint(), abs_value_threshold);
643 break;
644
645 case MPGeneralConstraintProto::kMaxConstraint:
646 error = FindErrorInMPMinMaxConstraint(
647 model, gen_constraint.max_constraint(), abs_value_threshold);
648 break;
649 default:
650 return absl::StrCat("Unknown general constraint type ",
651 gen_constraint.general_constraint_case());
652 }
653 if (!error.empty()) {
654 return absl::StrCat("In general constraint #", i, ": ", error);
655 }
656 }
657
658 // Validate objectives.
659 if (model.has_quadratic_objective()) {
660 error = FindErrorInQuadraticObjective(model.quadratic_objective(), num_vars,
661 abs_value_threshold);
662 if (!error.empty()) return absl::StrCat("In quadratic_objective: ", error);
663 }
664
665 // Validate the solution hint.
666 error = FindErrorInSolutionHint(model.solution_hint(), num_vars,
667 abs_value_threshold);
668 if (!error.empty()) {
669 return absl::StrCat("In solution_hint(): ", error);
670 }
671
672 // Validate the annotations.
673 {
674 LazyMPModelNameToIndexMaps name_maps(model);
675 for (int a = 0; a < model.annotation_size(); ++a) {
676 error = FindErrorInAnnotation(model.annotation(a), model, &name_maps);
677 if (!error.empty()) {
678 return absl::StrCat("In annotation #", a, ": ", error);
679 }
680 }
681 }
682
683 return std::string();
684}
685
686std::optional<LazyMutableCopy<MPModelProto>>
688 MPSolutionResponse* response) {
690 return GetMPModelOrPopulateResponse(ref, response);
691}
692
693std::optional<LazyMutableCopy<MPModelProto>> GetMPModelOrPopulateResponse(
694 LazyMutableCopy<MPModelRequest>& request, MPSolutionResponse* response) {
695 CHECK(response != nullptr);
696
697 if (!request->has_model() && !request->has_model_delta()) {
698 response->set_status(MPSOLVER_OPTIMAL);
699 response->set_status_str("Requests without model are considered OPTIMAL");
700 return std::nullopt;
701 }
702 if (request->has_model() && request->has_model_delta()) {
703 response->set_status(MPSOLVER_MODEL_INVALID);
704 response->set_status_str(
705 "Fields 'model' and 'model_delta' are mutually exclusive");
706 return std::nullopt;
707 }
708
709 // Extract the baseline model.
710 // Note that we move it out of the request if we have ownership.
712 if (request.has_ownership()) {
714 std::move(*(request.get_mutable()->mutable_model())));
715 } else {
716 return LazyMutableCopy<MPModelProto>(request->model());
717 }
718 }();
719
720 if (request->has_model_delta()) {
721 // NOTE(user): This library needs to be portable, so we can't include
722 // file/base/helpers.h; see ../port/file.h.
723 std::string contents;
724 const absl::Status file_read_status = PortableFileGetContents(
725 request->model_delta().baseline_model_file_path(), &contents);
726 if (!file_read_status.ok()) {
727 response->set_status(MPSOLVER_MODEL_INVALID);
728 response->set_status_str(
729 "Error when reading model_delta.baseline_model_file_path: '" +
730 file_read_status.ToString());
731 return std::nullopt;
732 }
733 if (!model.get_mutable()->ParseFromString(contents)) {
734 response->set_status(MPSOLVER_MODEL_INVALID);
735 response->set_status_str(
736 absl::StrFormat("The contents of baseline model file '%s' couldn't "
737 "be parsed as a raw serialized MPModelProto",
738 request->model_delta().baseline_model_file_path()));
739 return std::nullopt;
740 }
741 }
742
743 // Validate the baseline model.
744 std::string error = FindErrorInMPModelProto(*model);
745
746 // If the baseline is valid and we have a model delta, validate the delta,
747 // then apply it.
748 if (error.empty() && request->has_model_delta()) {
749 const MPModelDeltaProto& delta = request->model_delta();
751 if (error.empty()) ApplyVerifiedMPModelDelta(delta, model.get_mutable());
752 }
753
754 // Deal with errors.
755 if (!error.empty()) {
756 if (request->enable_internal_solver_output()) {
757 LOG(ERROR) << absl::StrCat("Invalid model: ", error);
758 }
759 response->set_status(absl::StrContains(error, "Infeasible")
760 ? MPSOLVER_INFEASIBLE
761 : MPSOLVER_MODEL_INVALID);
762 response->set_status_str(error);
763 return std::nullopt;
764 }
765
766 if (model->variable_size() == 0 && model->constraint_size() == 0 &&
767 model->general_constraint_size() == 0) {
768 response->set_status(MPSOLVER_OPTIMAL);
769 response->set_objective_value(model->objective_offset());
770 response->set_best_objective_bound(response->objective_value());
771 response->set_status_str(
772 "Requests without variables and constraints are considered OPTIMAL");
773 return std::nullopt;
774 }
775
776 return std::move(model);
777}
778
779// TODO(user): Add a general FindFeasibilityErrorInSolution() and factor out the
780// common code.
781std::string FindFeasibilityErrorInSolutionHint(const MPModelProto& model,
782 double tolerance) {
783 const int num_vars = model.variable_size();
784
785 // First, we validate the solution hint.
786 std::string error =
787 FindErrorInSolutionHint(model.solution_hint(), num_vars,
788 absl::GetFlag(FLAGS_model_validator_infinity));
789 if (!error.empty()) return absl::StrCat("Invalid solution_hint: ", error);
790
791 // Special error message for the empty case.
792 if (num_vars > 0 && model.solution_hint().var_index_size() == 0) {
793 return "Empty solution_hint.";
794 }
795
796 // To be feasible, the hint must not be partial.
797 if (model.solution_hint().var_index_size() != num_vars) {
798 return absl::StrCat("Partial solution_hint: only ",
799 model.solution_hint().var_index_size(), " out of the ",
800 num_vars, " problem variables are set.");
801 }
802
803 // All the values must be exactly in the variable bounds.
804 std::vector<double> var_value(num_vars);
805 for (int i = 0; i < model.solution_hint().var_index_size(); ++i) {
806 const int var_index = model.solution_hint().var_index(i);
807 const double value = model.solution_hint().var_value(i);
808 var_value[var_index] = value;
809 const double lb = model.variable(var_index).lower_bound();
810 const double ub = model.variable(var_index).upper_bound();
811 if (!IsSmallerWithinTolerance(value, ub, tolerance) ||
812 !IsSmallerWithinTolerance(lb, value, tolerance)) {
813 return absl::StrCat("Variable '", model.variable(var_index).name(),
814 "' is set to ", (value),
815 " which is not in the variable bounds [", (lb), ", ",
816 (ub), "] modulo a tolerance of ", (tolerance), ".");
817 }
818 }
819
820 // All the constraints must be satisfiable.
821 for (int cst_index = 0; cst_index < model.constraint_size(); ++cst_index) {
822 const MPConstraintProto& constraint = model.constraint(cst_index);
823 AccurateSum<double> activity;
824 for (int j = 0; j < constraint.var_index_size(); ++j) {
825 activity.Add(constraint.coefficient(j) *
826 var_value[constraint.var_index(j)]);
827 }
828 const double lb = model.constraint(cst_index).lower_bound();
829 const double ub = model.constraint(cst_index).upper_bound();
830 if (!IsSmallerWithinTolerance(activity.Value(), ub, tolerance) ||
831 !IsSmallerWithinTolerance(lb, activity.Value(), tolerance)) {
832 return absl::StrCat(
833 "Constraint '", model.constraint(cst_index).name(), "' has activity ",
834 (activity.Value()), " which is not in the constraint bounds [", (lb),
835 ", ", (ub), "] modulo a tolerance of ", (tolerance), ".");
836 }
837 }
838
839 return "";
840}
841
842std::string FindErrorInMPModelDeltaProto(const MPModelDeltaProto& delta,
843 const MPModelProto& model) {
844 const double abs_value_threshold =
845 absl::GetFlag(FLAGS_model_validator_infinity);
846 int num_vars = model.variable_size();
847 // Validate delta variables.
848 std::string error;
849 absl::flat_hash_set<int> new_var_indices;
850 int max_var_index = num_vars - 1;
851 MPVariableProto tmp_var_proto;
852 for (const auto& pair : delta.variable_overrides()) {
853 const int var_index = pair.first;
854 const MPVariableProto& var_override_proto = pair.second;
855 if (var_index < 0) {
856 error = "Invalid key";
857 } else if (var_index >= num_vars) {
858 max_var_index = std::max(max_var_index, var_index);
859 new_var_indices.insert(var_index);
860 error =
861 FindErrorInMPVariable(var_override_proto, abs_value_threshold,
862 /*accept_trivially_infeasible_bounds=*/false);
863 } else {
864 tmp_var_proto = model.variable(var_index);
865 // NOTE(user): It is OK for the override proto to be empty, i.e. be a
866 // non-override.
867 tmp_var_proto.MergeFrom(var_override_proto);
868 error =
869 FindErrorInMPVariable(tmp_var_proto, abs_value_threshold,
870 /*accept_trivially_infeasible_bounds=*/false);
871 }
872 if (!error.empty()) {
873 return absl::StrFormat(
874 "variable_overrides with key (eg. var index) = %d: %s", var_index,
875 error);
876 }
877 }
878 if (max_var_index != num_vars + new_var_indices.size() - 1) {
879 return absl::StrFormat(
880 "The added and existing variable indices do not form a dense integer "
881 "interval: oldmax=%d, max=%d, num added=%d",
882 num_vars - 1, max_var_index, new_var_indices.size());
883 }
884 // Now we "officially" add the new variables to "num_vars".
885 num_vars += new_var_indices.size();
886
887 // Validate delta constraints. We can avoid going over the full
888 // var_index/coefficient of the original constraint, since the overrides are
889 // self-sufficient (i.e. the override var_index/coefficients are valid iff
890 // they would be valid in a standalone, new constraint). So we use a partial
891 // proto merger to avoid those in the baseline constraint.
892 std::vector<bool> variable_appears(num_vars, false);
893 MPConstraintProto tmp_constraint_proto;
894 const int num_constraints = model.constraint_size();
895 absl::flat_hash_set<int> new_ct_indices;
896 int max_ct_index = num_constraints - 1;
897 for (const auto& pair : delta.constraint_overrides()) {
898 const int ct_index = pair.first;
899 const MPConstraintProto& constraint_override_proto = pair.second;
900 if (ct_index < 0) {
901 error = "Invalid constraint index";
902 } else if (ct_index >= num_constraints) {
903 max_ct_index = std::max(max_ct_index, ct_index);
904 new_ct_indices.insert(ct_index);
905 error = FindErrorInMPConstraint(
906 constraint_override_proto, &variable_appears, abs_value_threshold,
907 /*accept_trivially_infeasible_bounds=*/false);
908 } else {
909 // NOTE(user): We don't need to do the merging of var_index/coefficient:
910 // that part of the merged constraint will be valid iff the override is
911 // valid as a standalone var_index/coefficient map.
912 // So we simply validate a reduced version of the actual "merged"
913 // constraint, by removing the var_index/coefficient of the baseline.
914 // Benefit: the complexity is O(|constraint override|) even if the
915 // baseline constraint was huge.
916 tmp_constraint_proto.Clear();
918 &tmp_constraint_proto);
919 tmp_constraint_proto.MergeFrom(constraint_override_proto);
920 error = FindErrorInMPConstraint(
921 tmp_constraint_proto, &variable_appears, abs_value_threshold,
922 /*accept_trivially_infeasible_bounds=*/false);
923 }
924 if (!error.empty()) {
925 return absl::StrFormat(
926 "constraint_overrides with key (eg. constraint index) = %d: %s",
927 ct_index, error);
928 }
929 }
930 if (max_ct_index != num_constraints + new_ct_indices.size() - 1) {
931 return absl::StrFormat(
932 "The added and existing constraint indices do not form a dense integer "
933 "interval: oldmax=%d, max=%d, num added=%d",
934 num_constraints - 1, max_ct_index, new_ct_indices.size());
935 }
936
937 return "";
938}
939
940void MergeMPConstraintProtoExceptTerms(const MPConstraintProto& from,
941 MPConstraintProto* to) {
942#define COPY_FIELD_IF_PRESENT(field) \
943 if (from.has_##field()) to->set_##field(from.field())
947 COPY_FIELD_IF_PRESENT(is_lazy);
948#undef COPY_FIELD_IF_PRESENT
949}
950
951namespace {
952void PruneZeroTermsInMpConstraint(MPConstraintProto* ct) {
953 // Optimize the fast path (when no term is pruned) by doing a first quick scan
954 // until the first zero.
955 int first_zero = 0;
956 while (first_zero < ct->var_index_size() &&
957 ct->coefficient(first_zero) != 0.0) {
958 ++first_zero;
959 }
960 int num_kept = first_zero;
961 for (int i = first_zero; i < ct->var_index_size(); ++i) {
962 if (ct->coefficient(i) == 0.0) continue;
963 if (num_kept != i) {
964 ct->set_var_index(num_kept, ct->var_index(i));
965 ct->set_coefficient(num_kept, ct->coefficient(i));
966 }
967 ++num_kept;
968 }
969 ct->mutable_var_index()->Truncate(num_kept);
970 ct->mutable_coefficient()->Truncate(num_kept);
971}
972
973// Adds default entries to a repeated message field until it has the wanted
974// size. We don't use google::protobuf::util::Resize() because it's not
975// compatible with 'light' protos.
976template <class T>
977void ExtendRepeatedPtrFieldToSize(const int size, T* repeated_messages) {
978 DCHECK_GE(size, repeated_messages->size());
979 while (repeated_messages->size() < size) repeated_messages->Add();
980}
981} // namespace
982
983void ApplyVerifiedMPModelDelta(const MPModelDeltaProto& delta,
984 MPModelProto* model) {
985 // Apply the delta to the variables: first, resize the variable array.
986 int max_var_index = -1;
987 for (const auto& p : delta.variable_overrides()) {
988 max_var_index = std::max(max_var_index, p.first);
989 }
990 if (max_var_index >= model->variable_size()) {
991 ExtendRepeatedPtrFieldToSize(max_var_index + 1, model->mutable_variable());
992 }
993 // Then, apply the variable overrides.
994 for (const auto& p : delta.variable_overrides()) {
995 model->mutable_variable(p.first)->MergeFrom(p.second);
996 }
997
998 // Apply the delta to the constraints: first, resize the constraint array.
999 int max_ct_index = -1;
1000 for (const auto& p : delta.constraint_overrides()) {
1001 max_ct_index = std::max(max_ct_index, p.first);
1002 }
1003 const int old_num_constraints = model->constraint_size();
1004 if (max_ct_index >= old_num_constraints) {
1005 ExtendRepeatedPtrFieldToSize(max_ct_index + 1, model->mutable_constraint());
1006 }
1007 // Then, apply the constraint overrides.
1008 for (const auto& p : delta.constraint_overrides()) {
1009 const MPConstraintProto& override_ct = p.second;
1010 MPConstraintProto* baseline = model->mutable_constraint(p.first);
1011 // Fast path for added constraints.
1012 if (p.first >= old_num_constraints) {
1013 *baseline = override_ct;
1014 continue;
1015 }
1016 MergeMPConstraintProtoExceptTerms(/*from=*/override_ct, /*to=*/baseline);
1017 // Special case: the override is neutralized.
1018 if (override_ct.has_lower_bound() &&
1019 override_ct.lower_bound() <=
1020 -absl::GetFlag(FLAGS_model_validator_infinity) &&
1021 override_ct.has_upper_bound() &&
1022 override_ct.upper_bound() >=
1023 absl::GetFlag(FLAGS_model_validator_infinity)) {
1024 baseline->clear_var_index();
1025 baseline->clear_coefficient();
1026 continue;
1027 }
1028 // Otherwise we have to apply the term overrides. We can't do that in less
1029 // than O(|baseline| + |override_ct|) because the baseline doesn't have a
1030 // lookup-friendly data structure. But we still try to do it as efficiently
1031 // as possible. In particular, we only use O(|override_ct|) extra memory.
1032 absl::flat_hash_map<int, double> term_overrides;
1033 term_overrides.reserve(override_ct.var_index_size());
1034 for (int i = 0; i < override_ct.var_index_size(); ++i) {
1035 term_overrides[override_ct.var_index(i)] = override_ct.coefficient(i);
1036 }
1037 for (int i = 0; i < baseline->var_index_size(); ++i) {
1038 auto it = term_overrides.find(baseline->var_index(i));
1039 if (it == term_overrides.end()) continue;
1040 baseline->set_coefficient(i, it->second);
1041 it->second = 0.0; // To mark this term override as 'has been applied'.
1042 }
1043 PruneZeroTermsInMpConstraint(baseline);
1044 // Add the term overrides which haven't been used: those are added terms.
1045 for (const auto& p : term_overrides) {
1046 if (p.second != 0.0) {
1047 baseline->add_var_index(p.first);
1048 baseline->add_coefficient(p.second);
1049 }
1050 }
1051 }
1052}
1053
1054} // namespace operations_research
IntegerValue size
void Add(const FpNumber &value)
Adds an FpNumber to the sum.
FpNumber Value() const
Gets the value of the sum.
T * get_mutable()
This will copy the object if we don't already have ownership.
int64_t a
Definition table.cc:44
const std::string name
A name for logging purposes.
const Constraint * ct
int64_t value
double upper_bound
double lower_bound
GRBmodel * model
int ct_index
ABSL_FLAG(double, model_validator_infinity, 1e100, "Anything above or equal to this magnitude will be considered infinity.")
#define COPY_FIELD_IF_PRESENT(field)
int index
Collection::value_type::second_type & LookupOrInsert(Collection *const collection, const typename Collection::value_type::first_type &key, const typename Collection::value_type::second_type &value)
Definition map_util.h:242
const MapUtilMappedT< Collection > & FindWithDefault(const Collection &collection, const KeyType &key, const MapUtilMappedT< Collection > &value)
Definition map_util.h:36
In SWIG mode, we don't want anything besides these top-level includes.
bool IsSmallerWithinTolerance(FloatType x, FloatType y, FloatType tolerance)
Definition fp_utils.h:159
std::optional< LazyMutableCopy< MPModelProto > > ExtractValidMPModelOrPopulateResponseStatus(const MPModelRequest &request, MPSolutionResponse *response)
std::string FindErrorInMPModelDeltaProto(const MPModelDeltaProto &delta, const MPModelProto &model)
void ApplyVerifiedMPModelDelta(const MPModelDeltaProto &delta, MPModelProto *model)
std::string ProtobufShortDebugString(const P &message)
Definition proto_utils.h:41
::absl::Status PortableFileGetContents(absl::string_view file_name, std::string *output)
Definition file.cc:45
std::optional< LazyMutableCopy< MPModelProto > > GetMPModelOrPopulateResponse(LazyMutableCopy< MPModelRequest > &request, MPSolutionResponse *response)
std::string FindErrorInMPModelProto(const MPModelProto &model, double abs_value_threshold, const bool accept_trivially_infeasible_bounds)
std::string FindFeasibilityErrorInSolutionHint(const MPModelProto &model, double tolerance)
void MergeMPConstraintProtoExceptTerms(const MPConstraintProto &from, MPConstraintProto *to)
trees with all degrees equal to
int64_t delta
Definition resource.cc:1709
std::vector< int > var_indices
Definition lp_utils.cc:744
int var_index
Definition search.cc:3268