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