OR-Tools  9.6
linear_solver/model_validator.cc
Go to the documentation of this file.
1 // Copyright 2010-2022 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 <limits>
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/status/status.h"
27 #include "absl/status/statusor.h"
28 #include "absl/strings/match.h"
29 #include "absl/strings/str_cat.h"
30 #include "absl/strings/str_format.h"
31 #include "absl/types/optional.h"
34 #include "ortools/base/map_util.h"
35 #include "ortools/linear_solver/linear_solver.pb.h"
36 #include "ortools/port/file.h"
38 #include "ortools/util/fp_utils.h"
40 
42  double, model_validator_infinity, 1e100,
43  "Anything above or equal to this magnitude will be considered infinity.");
44 
45 namespace operations_research {
46 namespace {
47 
48 bool 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.
54 template <typename BoundedElement>
55 std::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.
70 std::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.
92 template <typename Iterable>
93 std::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.
114 std::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 
151 std::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 
171 bool 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 
177 std::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 
208 std::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 
236 std::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 
291 std::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 
312 std::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 
342 std::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 
371 std::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 
399 std::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 
427 namespace {
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.
431 template <class NamedEntity>
432 absl::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 
442 class 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  const std::string& 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 
488 std::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()) {
500  case MPModelProto::Annotation::VARIABLE_DEFAULT:
501  num_entitities = model.variable_size();
502  break;
503  case MPModelProto::Annotation::CONSTRAINT:
504  num_entitities = model.constraint_size();
505  break;
506  case MPModelProto::Annotation::GENERAL_CONSTRAINT:
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()) {
524  case MPModelProto::Annotation::VARIABLE_DEFAULT:
525  name = model.variable(target_index).name();
526  break;
527  case MPModelProto::Annotation::CONSTRAINT:
528  name = model.constraint(target_index).name();
529  break;
530  case MPModelProto::Annotation::GENERAL_CONSTRAINT:
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: ",
581  ProtobufShortDebugString(model.variable(i)));
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()) {
605  case MPGeneralConstraintProto::kIndicatorConstraint:
606  error = FindErrorInMPIndicatorConstraint(
607  model, gen_constraint.indicator_constraint(), &variable_appears,
608  abs_value_threshold, accept_trivially_infeasible_bounds);
609  break;
610 
611  case MPGeneralConstraintProto::kSosConstraint:
612  error =
613  FindErrorInMPSosConstraint(model, gen_constraint.sos_constraint(),
614  &variable_appears, abs_value_threshold);
615  break;
616 
617  case MPGeneralConstraintProto::kQuadraticConstraint:
618  error = FindErrorInMPQuadraticConstraint(
619  model, gen_constraint.quadratic_constraint(), &variable_appears,
620  abs_value_threshold, accept_trivially_infeasible_bounds);
621  break;
622 
623  case MPGeneralConstraintProto::kAbsConstraint:
624  error =
625  FindErrorInMPAbsConstraint(model, gen_constraint.abs_constraint());
626  break;
627 
628  case MPGeneralConstraintProto::kAndConstraint:
629  error = FindErrorInMPAndOrConstraint(model,
630  gen_constraint.and_constraint());
631  break;
632 
633  case MPGeneralConstraintProto::kOrConstraint:
634  error =
635  FindErrorInMPAndOrConstraint(model, gen_constraint.or_constraint());
636  break;
637 
638  case MPGeneralConstraintProto::kMinConstraint:
639  error = FindErrorInMPMinMaxConstraint(
640  model, gen_constraint.min_constraint(), abs_value_threshold);
641  break;
642 
643  case MPGeneralConstraintProto::kMaxConstraint:
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 
684 std::optional<LazyMutableCopy<MPModelProto>>
685 ExtractValidMPModelOrPopulateResponseStatus(const MPModelRequest& request,
686  MPSolutionResponse* response) {
687  CHECK(response != nullptr);
688 
689  if (!request.has_model() && !request.has_model_delta()) {
690  response->set_status(MPSOLVER_OPTIMAL);
691  response->set_status_str("Requests without model are considered OPTIMAL");
692  return std::nullopt;
693  }
694  if (request.has_model() && request.has_model_delta()) {
695  response->set_status(MPSOLVER_MODEL_INVALID);
696  response->set_status_str(
697  "Fields 'model' and 'model_delta' are mutually exclusive");
698  return std::nullopt;
699  }
700 
701  // Extract the baseline model.
702  LazyMutableCopy<MPModelProto> model(request.model());
703  if (request.has_model_delta()) {
704  // NOTE(user): This library needs to be portable, so we can't include
705  // ortools/base/helpers.h; see ../port/file.h.
706  std::string contents;
707  const absl::Status file_read_status = PortableFileGetContents(
708  request.model_delta().baseline_model_file_path(), &contents);
709  if (!file_read_status.ok()) {
710  response->set_status(MPSOLVER_MODEL_INVALID);
711  response->set_status_str(
712  "Error when reading model_delta.baseline_model_file_path: '" +
713  file_read_status.ToString());
714  return std::nullopt;
715  }
716  if (!model.get_mutable()->ParseFromString(contents)) {
717  response->set_status(MPSOLVER_MODEL_INVALID);
718  response->set_status_str(
719  absl::StrFormat("The contents of baseline model file '%s' couldn't "
720  "be parsed as a raw serialized MPModelProto",
721  request.model_delta().baseline_model_file_path()));
722  return std::nullopt;
723  }
724  }
725 
726  // Validate the baseline model.
727  std::string error = FindErrorInMPModelProto(model.get());
728 
729  // If the baseline is valid and we have a model delta, validate the delta,
730  // then apply it.
731  if (error.empty() && request.has_model_delta()) {
732  const MPModelDeltaProto& delta = request.model_delta();
733  error = FindErrorInMPModelDeltaProto(delta, model.get());
734  if (error.empty()) ApplyVerifiedMPModelDelta(delta, model.get_mutable());
735  }
736 
737  // Deal with errors.
738  if (!error.empty()) {
739  if (request.enable_internal_solver_output()) {
740  LOG(ERROR) << absl::StrCat("Invalid model: ", error);
741  }
742  response->set_status(absl::StrContains(error, "Infeasible")
743  ? MPSOLVER_INFEASIBLE
744  : MPSOLVER_MODEL_INVALID);
745  response->set_status_str(error);
746  return std::nullopt;
747  }
748 
749  if (model.get().variable_size() == 0 && model.get().constraint_size() == 0 &&
750  model.get().general_constraint_size() == 0) {
751  response->set_status(MPSOLVER_OPTIMAL);
752  response->set_objective_value(model.get().objective_offset());
753  response->set_best_objective_bound(response->objective_value());
754  response->set_status_str(
755  "Requests without variables and constraints are considered OPTIMAL");
756  return std::nullopt;
757  }
758 
759  return std::move(model);
760 }
761 
763  MPModelRequest* request, MPSolutionResponse* response) {
764  std::optional<LazyMutableCopy<MPModelProto>> lazy_copy =
766  if (!lazy_copy) return false;
767  if (lazy_copy->was_copied()) {
768  lazy_copy->get_mutable()->Swap(request->mutable_model());
769  }
770  return true;
771 }
772 
773 // TODO(user): Add a general FindFeasibilityErrorInSolution() and factor out the
774 // common code.
775 std::string FindFeasibilityErrorInSolutionHint(const MPModelProto& model,
776  double tolerance) {
777  const int num_vars = model.variable_size();
778 
779  // First, we validate the solution hint.
780  std::string error =
781  FindErrorInSolutionHint(model.solution_hint(), num_vars,
782  absl::GetFlag(FLAGS_model_validator_infinity));
783  if (!error.empty()) return absl::StrCat("Invalid solution_hint: ", error);
784 
785  // Special error message for the empty case.
786  if (num_vars > 0 && model.solution_hint().var_index_size() == 0) {
787  return "Empty solution_hint.";
788  }
789 
790  // To be feasible, the hint must not be partial.
791  if (model.solution_hint().var_index_size() != num_vars) {
792  return absl::StrCat("Partial solution_hint: only ",
793  model.solution_hint().var_index_size(), " out of the ",
794  num_vars, " problem variables are set.");
795  }
796 
797  // All the values must be exactly in the variable bounds.
798  std::vector<double> var_value(num_vars);
799  for (int i = 0; i < model.solution_hint().var_index_size(); ++i) {
800  const int var_index = model.solution_hint().var_index(i);
801  const double value = model.solution_hint().var_value(i);
802  var_value[var_index] = value;
803  const double lb = model.variable(var_index).lower_bound();
804  const double ub = model.variable(var_index).upper_bound();
805  if (!IsSmallerWithinTolerance(value, ub, tolerance) ||
806  !IsSmallerWithinTolerance(lb, value, tolerance)) {
807  return absl::StrCat("Variable '", model.variable(var_index).name(),
808  "' is set to ", (value),
809  " which is not in the variable bounds [", (lb), ", ",
810  (ub), "] modulo a tolerance of ", (tolerance), ".");
811  }
812  }
813 
814  // All the constraints must be satisfiable.
815  for (int cst_index = 0; cst_index < model.constraint_size(); ++cst_index) {
816  const MPConstraintProto& constraint = model.constraint(cst_index);
817  AccurateSum<double> activity;
818  for (int j = 0; j < constraint.var_index_size(); ++j) {
819  activity.Add(constraint.coefficient(j) *
820  var_value[constraint.var_index(j)]);
821  }
822  const double lb = model.constraint(cst_index).lower_bound();
823  const double ub = model.constraint(cst_index).upper_bound();
824  if (!IsSmallerWithinTolerance(activity.Value(), ub, tolerance) ||
825  !IsSmallerWithinTolerance(lb, activity.Value(), tolerance)) {
826  return absl::StrCat(
827  "Constraint '", model.constraint(cst_index).name(), "' has activity ",
828  (activity.Value()), " which is not in the constraint bounds [", (lb),
829  ", ", (ub), "] modulo a tolerance of ", (tolerance), ".");
830  }
831  }
832 
833  return "";
834 }
835 
836 std::string FindErrorInMPModelDeltaProto(const MPModelDeltaProto& delta,
837  const MPModelProto& model) {
838  const double abs_value_threshold =
839  absl::GetFlag(FLAGS_model_validator_infinity);
840  int num_vars = model.variable_size();
841  // Validate delta variables.
842  std::string error;
843  absl::flat_hash_set<int> new_var_indices;
844  int max_var_index = num_vars - 1;
845  MPVariableProto tmp_var_proto;
846  for (const auto& pair : delta.variable_overrides()) {
847  const int var_index = pair.first;
848  const MPVariableProto& var_override_proto = pair.second;
849  if (var_index < 0) {
850  error = "Invalid key";
851  } else if (var_index >= num_vars) {
852  max_var_index = std::max(max_var_index, var_index);
853  new_var_indices.insert(var_index);
854  error =
855  FindErrorInMPVariable(var_override_proto, abs_value_threshold,
856  /*accept_trivially_infeasible_bounds=*/false);
857  } else {
858  tmp_var_proto = model.variable(var_index);
859  // NOTE(user): It is OK for the override proto to be empty, i.e. be a
860  // non-override.
861  tmp_var_proto.MergeFrom(var_override_proto);
862  error =
863  FindErrorInMPVariable(tmp_var_proto, abs_value_threshold,
864  /*accept_trivially_infeasible_bounds=*/false);
865  }
866  if (!error.empty()) {
867  return absl::StrFormat(
868  "variable_overrides with key (eg. var index) = %d: %s", var_index,
869  error);
870  }
871  }
872  if (max_var_index != num_vars + new_var_indices.size() - 1) {
873  return absl::StrFormat(
874  "The added and existing variable indices do not form a dense integer "
875  "interval: oldmax=%d, max=%d, num added=%d",
876  num_vars - 1, max_var_index, new_var_indices.size());
877  }
878  // Now we "officially" add the new variables to "num_vars".
879  num_vars += new_var_indices.size();
880 
881  // Validate delta constraints. We can avoid going over the full
882  // var_index/coefficient of the original constraint, since the overrides are
883  // self-sufficient (i.e. the override var_index/coefficients are valid iff
884  // they would be valid in a standalone, new constraint). So we use a partial
885  // proto merger to avoid those in the baseline constraint.
886  std::vector<bool> variable_appears(num_vars, false);
887  MPConstraintProto tmp_constraint_proto;
888  const int num_constraints = model.constraint_size();
889  absl::flat_hash_set<int> new_ct_indices;
890  int max_ct_index = num_constraints - 1;
891  for (const auto& pair : delta.constraint_overrides()) {
892  const int ct_index = pair.first;
893  const MPConstraintProto& constraint_override_proto = pair.second;
894  if (ct_index < 0) {
895  error = "Invalid constraint index";
896  } else if (ct_index >= num_constraints) {
897  max_ct_index = std::max(max_ct_index, ct_index);
898  new_ct_indices.insert(ct_index);
899  error = FindErrorInMPConstraint(
900  constraint_override_proto, &variable_appears, abs_value_threshold,
901  /*accept_trivially_infeasible_bounds=*/false);
902  } else {
903  // NOTE(user): We don't need to do the merging of var_index/coefficient:
904  // that part of the merged constraint will be valid iff the override is
905  // valid as a standalone var_index/coefficient map.
906  // So we simply validate a reduced version of the actual "merged"
907  // constraint, by removing the var_index/coefficient of the baseline.
908  // Benefit: the complexity is O(|constraint override|) even if the
909  // baseline constraint was huge.
910  tmp_constraint_proto.Clear();
911  MergeMPConstraintProtoExceptTerms(model.constraint(ct_index),
912  &tmp_constraint_proto);
913  tmp_constraint_proto.MergeFrom(constraint_override_proto);
914  error = FindErrorInMPConstraint(
915  tmp_constraint_proto, &variable_appears, abs_value_threshold,
916  /*accept_trivially_infeasible_bounds=*/false);
917  }
918  if (!error.empty()) {
919  return absl::StrFormat(
920  "constraint_overrides with key (eg. constraint index) = %d: %s",
921  ct_index, error);
922  }
923  }
924  if (max_ct_index != num_constraints + new_ct_indices.size() - 1) {
925  return absl::StrFormat(
926  "The added and existing constraint indices do not form a dense integer "
927  "interval: oldmax=%d, max=%d, num added=%d",
928  num_constraints - 1, max_ct_index, new_ct_indices.size());
929  }
930 
931  return "";
932 }
933 
934 void MergeMPConstraintProtoExceptTerms(const MPConstraintProto& from,
935  MPConstraintProto* to) {
936 #define COPY_FIELD_IF_PRESENT(field) \
937  if (from.has_##field()) to->set_##field(from.field())
941  COPY_FIELD_IF_PRESENT(is_lazy);
942 #undef COPY_FIELD_IF_PRESENT
943 }
944 
945 namespace {
946 void PruneZeroTermsInMpConstraint(MPConstraintProto* ct) {
947  // Optimize the fast path (when no term is pruned) by doing a first quick scan
948  // until the first zero.
949  int first_zero = 0;
950  while (first_zero < ct->var_index_size() &&
951  ct->coefficient(first_zero) != 0.0) {
952  ++first_zero;
953  }
954  int num_kept = first_zero;
955  for (int i = first_zero; i < ct->var_index_size(); ++i) {
956  if (ct->coefficient(i) == 0.0) continue;
957  if (num_kept != i) {
958  ct->set_var_index(num_kept, ct->var_index(i));
959  ct->set_coefficient(num_kept, ct->coefficient(i));
960  }
961  ++num_kept;
962  }
963  ct->mutable_var_index()->Truncate(num_kept);
964  ct->mutable_coefficient()->Truncate(num_kept);
965 }
966 
967 // Adds default entries to a repeated message field until it has the wanted
968 // size. We don't use google::protobuf::util::Resize() because it's not
969 // compatible with 'light' protos.
970 template <class T>
971 void ExtendRepeatedPtrFieldToSize(const int size, T* repeated_messages) {
972  DCHECK_GE(size, repeated_messages->size());
973  while (repeated_messages->size() < size) repeated_messages->Add();
974 }
975 } // namespace
976 
977 void ApplyVerifiedMPModelDelta(const MPModelDeltaProto& delta,
978  MPModelProto* model) {
979  // Apply the delta to the variables: first, resize the variable array.
980  int max_var_index = -1;
981  for (const auto& p : delta.variable_overrides()) {
982  max_var_index = std::max(max_var_index, p.first);
983  }
984  if (max_var_index >= model->variable_size()) {
985  ExtendRepeatedPtrFieldToSize(max_var_index + 1, model->mutable_variable());
986  }
987  // Then, apply the variable overrides.
988  for (const auto& p : delta.variable_overrides()) {
989  model->mutable_variable(p.first)->MergeFrom(p.second);
990  }
991 
992  // Apply the delta to the constraints: first, resize the constraint array.
993  int max_ct_index = -1;
994  for (const auto& p : delta.constraint_overrides()) {
995  max_ct_index = std::max(max_ct_index, p.first);
996  }
997  const int old_num_constraints = model->constraint_size();
998  if (max_ct_index >= old_num_constraints) {
999  ExtendRepeatedPtrFieldToSize(max_ct_index + 1, model->mutable_constraint());
1000  }
1001  // Then, apply the constraint overrides.
1002  for (const auto& p : delta.constraint_overrides()) {
1003  const MPConstraintProto& override_ct = p.second;
1004  MPConstraintProto* baseline = model->mutable_constraint(p.first);
1005  // Fast path for added constraints.
1006  if (p.first >= old_num_constraints) {
1007  *baseline = override_ct;
1008  continue;
1009  }
1010  MergeMPConstraintProtoExceptTerms(/*from=*/override_ct, /*to=*/baseline);
1011  // Special case: the override is neutralized.
1012  if (override_ct.has_lower_bound() &&
1013  override_ct.lower_bound() <=
1014  -absl::GetFlag(FLAGS_model_validator_infinity) &&
1015  override_ct.has_upper_bound() &&
1016  override_ct.upper_bound() >=
1017  absl::GetFlag(FLAGS_model_validator_infinity)) {
1018  baseline->clear_var_index();
1019  baseline->clear_coefficient();
1020  continue;
1021  }
1022  // Otherwise we have to apply the term overrides. We can't do that in less
1023  // than O(|baseline| + |override_ct|) because the baseline doesn't have a
1024  // lookup-friendly data structure. But we still try to do it as efficiently
1025  // as possible. In particular, we only use O(|override_ct|) extra memory.
1026  absl::flat_hash_map<int, double> term_overrides;
1027  term_overrides.reserve(override_ct.var_index_size());
1028  for (int i = 0; i < override_ct.var_index_size(); ++i) {
1029  term_overrides[override_ct.var_index(i)] = override_ct.coefficient(i);
1030  }
1031  for (int i = 0; i < baseline->var_index_size(); ++i) {
1032  auto it = term_overrides.find(baseline->var_index(i));
1033  if (it == term_overrides.end()) continue;
1034  baseline->set_coefficient(i, it->second);
1035  it->second = 0.0; // To mark this term override as 'has been applied'.
1036  }
1037  PruneZeroTermsInMpConstraint(baseline);
1038  // Add the term overrides which haven't been used: those are added terms.
1039  for (const auto& p : term_overrides) {
1040  if (p.second != 0.0) {
1041  baseline->add_var_index(p.first);
1042  baseline->add_coefficient(p.second);
1043  }
1044  }
1045  }
1046 }
1047 
1048 } // namespace operations_research
int64_t max
Definition: alldiff_cst.cc:140
void Add(const FpNumber &value)
Definition: accurate_sum.h:29
int64_t a
SharedResponseManager * response
const std::string name
const Constraint * ct
int64_t value
GRBmodel * model
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:237
const Collection::value_type::second_type & FindWithDefault(const Collection &collection, const typename Collection::value_type::first_type &key, const typename Collection::value_type::second_type &value)
Definition: map_util.h:29
Collection of objects used to extend the Constraint Solver library.
bool ExtractValidMPModelInPlaceOrPopulateResponseStatus(MPModelRequest *request, MPSolutionResponse *response)
Like ExtractValidMPModelOrPopulateResponseStatus(), but works in-place: if the MPModel needed extract...
bool IsSmallerWithinTolerance(FloatType x, FloatType y, FloatType tolerance)
Definition: fp_utils.h:157
std::string FindErrorInMPModelDeltaProto(const MPModelDeltaProto &delta, const MPModelProto &model)
Like FindErrorInMPModelProto, but for a MPModelDeltaProto applied to a given baseline model (assumed ...
void ApplyVerifiedMPModelDelta(const MPModelDeltaProto &delta, MPModelProto *model)
std::string ProtobufShortDebugString(const P &message)
std::optional< LazyMutableCopy< MPModelProto > > ExtractValidMPModelOrPopulateResponseStatus(const MPModelRequest &request, MPSolutionResponse *response)
If the model is valid and non-empty, returns it (possibly after extracting the model_delta).
std::string FindErrorInMPModelProto(const MPModelProto &model, double abs_value_threshold, const bool accept_trivially_infeasible_bounds)
Returns an empty string iff the model is valid and not trivially infeasible.
::absl::Status PortableFileGetContents(absl::string_view file_name, std::string *output)
Definition: port/file.cc:45
std::string FindFeasibilityErrorInSolutionHint(const MPModelProto &model, double tolerance)
Returns an empty string if the solution hint given in the model is a feasible solution.
void MergeMPConstraintProtoExceptTerms(const MPConstraintProto &from, MPConstraintProto *to)
int64_t delta
Definition: resource.cc:1695
IntVar * upper_bound
Definition: routing.cc:1087
IntVar * lower_bound
Definition: routing.cc:1086
std::vector< int > var_indices