Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
solution_feasibility_checker.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 <cmath>
17#include <string>
18#include <vector>
19
20#include "absl/algorithm/container.h"
21#include "absl/status/status.h"
22#include "absl/status/statusor.h"
23#include "absl/strings/str_cat.h"
24#include "absl/strings/str_format.h"
25#include "absl/strings/str_join.h"
31
33namespace {
34
35absl::Status ValidateOptions(const FeasibilityCheckerOptions& options) {
36 const auto tolerance_is_valid = [](const double tolerance) {
37 return tolerance >= 0 && !std::isnan(tolerance);
38 };
39 if (!tolerance_is_valid(options.absolute_constraint_tolerance)) {
41 << "invalid absolute_constraint_tolerance value: "
42 << options.absolute_constraint_tolerance;
43 }
44 if (!tolerance_is_valid(options.integrality_tolerance)) {
46 << "invalid integrality_tolerance value: "
47 << options.integrality_tolerance;
48 }
49 if (!tolerance_is_valid(options.nonzero_tolerance)) {
51 << "invalid nonzero_tolerance value: " << options.nonzero_tolerance;
52 }
53 return absl::OkStatus();
54}
55
56bool IsNearlyLessThan(const double lhs, const double rhs,
57 const double absolute_tolerance) {
58 return lhs <= rhs + absolute_tolerance;
59}
60
61bool IsNearlyEqualTo(const double actual, const double target,
62 const double absolute_tolerance) {
63 return std::fabs(actual - target) <= absolute_tolerance;
64}
65
66absl::Status ValidateVariables(const Model& model,
67 const VariableMap<double>& variable_values) {
68 for (const Variable variable : model.Variables()) {
69 if (!variable_values.contains(variable)) {
71 << "Variable present in `model` but not `variable_values`: "
72 << variable;
73 }
74 }
75 for (const auto [variable, unused] : variable_values) {
76 if (variable.storage() != model.storage()) {
78 << "Variable present in `variable_values` but not `model`: "
79 << variable;
80 }
81 }
82 return absl::OkStatus();
83}
84
85ModelSubset::Bounds CheckBoundedConstraint(
86 const double expr_value, const double lower_bound, const double upper_bound,
87 const FeasibilityCheckerOptions& options) {
88 return {.lower = !IsNearlyLessThan(lower_bound, expr_value,
89 options.absolute_constraint_tolerance),
90 .upper = !IsNearlyLessThan(expr_value, upper_bound,
91 options.absolute_constraint_tolerance)};
92}
93
94// CHECK-fails if `variable` and `variable_values` come from different models.
95ModelSubset::Bounds CheckVariableBounds(
96 const Variable variable, const VariableMap<double>& variable_values,
97 const FeasibilityCheckerOptions& options) {
98 return CheckBoundedConstraint(variable_values.at(variable),
99 variable.lower_bound(), variable.upper_bound(),
100 options);
101}
102
103// CHECK-fails if `constraint` and `variable_values` come from different models.
104ModelSubset::Bounds CheckLinearConstraint(
105 const LinearConstraint constraint,
106 const VariableMap<double>& variable_values,
107 const FeasibilityCheckerOptions& options) {
108 const BoundedLinearExpression bounded_expr =
109 constraint.AsBoundedLinearExpression();
110 return CheckBoundedConstraint(
111 bounded_expr.expression.Evaluate(variable_values),
112 bounded_expr.lower_bound, bounded_expr.upper_bound, options);
113}
114
115// CHECK-fails if `constraint` and `variable_values` come from different models.
116ModelSubset::Bounds CheckQuadraticConstraint(
117 const QuadraticConstraint constraint,
118 const VariableMap<double>& variable_values,
119 const FeasibilityCheckerOptions& options) {
120 const BoundedQuadraticExpression bounded_expr =
121 constraint.AsBoundedQuadraticExpression();
122 return CheckBoundedConstraint(
123 bounded_expr.expression.Evaluate(variable_values),
124 bounded_expr.lower_bound, bounded_expr.upper_bound, options);
125}
126
127// CHECK-fails if `constraint` and `variable_values` come from different models.
128bool CheckSecondOrderConeConstraint(const SecondOrderConeConstraint constraint,
129 const VariableMap<double>& variable_values,
130 const FeasibilityCheckerOptions& options) {
131 double args_to_norm_value = 0.0;
132 for (const LinearExpression& expr : constraint.ArgumentsToNorm()) {
133 // This is liable to overflow, but if it does so it will return inf, which
134 // will ultimately cause this function to return false.
135 args_to_norm_value += MathUtil::IPow(expr.Evaluate(variable_values), 2);
136 }
137 return IsNearlyLessThan(std::sqrt(args_to_norm_value),
138 constraint.UpperBound().Evaluate(variable_values),
139 options.absolute_constraint_tolerance);
140}
141
142// CHECK-fails if `constraint` and `variable_values` come from different models.
143bool CheckSos1Constraint(const Sos1Constraint constraint,
144 const VariableMap<double>& variable_values,
145 const FeasibilityCheckerOptions& options) {
146 // For expression i: was any expression with index in [0, i) nonzero?
147 bool previous_nonzero = false;
148 for (int i = 0; i < constraint.num_expressions(); ++i) {
149 const bool expr_is_nonzero =
150 !IsNearlyEqualTo(constraint.Expression(i).Evaluate(variable_values),
151 0.0, options.nonzero_tolerance);
152 if (expr_is_nonzero && previous_nonzero) {
153 // We've seen two nonzero expressions, the SOS1 constraint is violated.
154 return false;
155 }
156 previous_nonzero |= expr_is_nonzero;
157 }
158 return true;
159}
160
161// CHECK-fails if `constraint` and `variable_values` come from different models.
162bool CheckSos2Constraint(const Sos2Constraint constraint,
163 const VariableMap<double>& variable_values,
164 const FeasibilityCheckerOptions& options) {
165 // For expression i: was expression i-1 nonzero?
166 bool preceding_was_nonzero = false;
167 // For expression i: was any expression with index in [0, i-1) nonzero?
168 bool any_other_before_was_nonzero = false;
169 for (int i = 0; i < constraint.num_expressions(); ++i) {
170 const bool expr_is_nonzero =
171 !IsNearlyEqualTo(constraint.Expression(i).Evaluate(variable_values),
172 0.0, options.nonzero_tolerance);
173 if (expr_is_nonzero && any_other_before_was_nonzero) {
174 // Expressions i and j are nonzero, for some j in [0, i-1), so the SOS2
175 // constraint is violated.
176 return false;
177 }
178 // Update values for expression i+1.
179 any_other_before_was_nonzero |= preceding_was_nonzero;
180 preceding_was_nonzero = expr_is_nonzero;
181 }
182 return true;
183}
184
185// CHECK-fails if `constraint` and `variable_values` come from different models.
186// Only check the implication, not that the indicator variable is binary.
187bool CheckIndicatorConstraint(const IndicatorConstraint constraint,
188 const VariableMap<double>& variable_values,
189 const FeasibilityCheckerOptions& options) {
190 if (!constraint.indicator_variable().has_value()) {
191 // Null indicator variables mean the constraint is vacuously satisfied.
192 return true;
193 }
194 if (!IsNearlyEqualTo(variable_values.at(*constraint.indicator_variable()),
195 constraint.activate_on_zero() ? 0.0 : 1.0,
196 options.nonzero_tolerance)) {
197 // If the indicator variable is not (nearly) at its indication value, the
198 // constraint holds (there is no implication).
199 return true;
200 }
201 const BoundedLinearExpression bounded_expr = constraint.ImpliedConstraint();
202 // At this point in the function, we know that the implication should hold.
203 // So, the indicator constraint is satisfied iff both sides of the implied
204 // constraints are satisfied.
205 return CheckBoundedConstraint(
206 bounded_expr.expression.Evaluate(variable_values),
207 bounded_expr.lower_bound, bounded_expr.upper_bound, options)
208 .empty();
209}
210
211} // namespace
212
213absl::StatusOr<ModelSubset> CheckPrimalSolutionFeasibility(
214 const Model& model, const VariableMap<double>& variable_values,
215 const FeasibilityCheckerOptions& options) {
216 RETURN_IF_ERROR(ValidateOptions(options));
217 RETURN_IF_ERROR(ValidateVariables(model, variable_values));
218
219 ModelSubset violated_constraints;
220 for (const Variable variable : model.Variables()) {
221 const ModelSubset::Bounds violations =
222 CheckVariableBounds(variable, variable_values, options);
223 if (!violations.empty()) {
224 violated_constraints.variable_bounds[variable] = violations;
225 }
226 if (variable.is_integer()) {
227 const double variable_value = variable_values.at(variable);
228 const double rounded_variable_value = std::round(variable_value);
229 if (std::fabs(rounded_variable_value - variable_value) >
230 options.integrality_tolerance) {
231 violated_constraints.variable_integrality.insert(variable);
232 }
233 }
234 }
235
236 for (const LinearConstraint linear_constraint : model.LinearConstraints()) {
237 const ModelSubset::Bounds violations =
238 CheckLinearConstraint(linear_constraint, variable_values, options);
239 if (!violations.empty()) {
240 violated_constraints.linear_constraints[linear_constraint] = violations;
241 }
242 }
243
244 for (const QuadraticConstraint quadratic_constraint :
245 model.QuadraticConstraints()) {
246 const ModelSubset::Bounds violations = CheckQuadraticConstraint(
247 quadratic_constraint, variable_values, options);
248 if (!violations.empty()) {
249 violated_constraints.quadratic_constraints[quadratic_constraint] =
250 violations;
251 }
252 }
253
254 for (const SecondOrderConeConstraint soc_constraint :
255 model.SecondOrderConeConstraints()) {
256 if (!CheckSecondOrderConeConstraint(soc_constraint, variable_values,
257 options)) {
258 violated_constraints.second_order_cone_constraints.insert(soc_constraint);
259 }
260 }
261
262 for (const Sos1Constraint sos1_constraint : model.Sos1Constraints()) {
263 if (!CheckSos1Constraint(sos1_constraint, variable_values, options)) {
264 violated_constraints.sos1_constraints.insert(sos1_constraint);
265 }
266 }
267
268 for (const Sos2Constraint sos2_constraint : model.Sos2Constraints()) {
269 if (!CheckSos2Constraint(sos2_constraint, variable_values, options)) {
270 violated_constraints.sos2_constraints.insert(sos2_constraint);
271 }
272 }
273
274 for (const IndicatorConstraint indicator_constraint :
275 model.IndicatorConstraints()) {
276 if (!CheckIndicatorConstraint(indicator_constraint, variable_values,
277 options)) {
278 violated_constraints.indicator_constraints.insert(indicator_constraint);
279 }
280 }
281 return violated_constraints;
282}
283
284namespace {
285
286// `variables` and `variable_values` must share a common Model.
287std::string VariableValuesAsString(std::vector<Variable> variables,
288 const VariableMap<double>& variable_values) {
289 absl::c_sort(variables, [](const Variable lhs, const Variable rhs) {
290 return lhs.typed_id() < rhs.typed_id();
291 });
292 return absl::StrCat(
293 "{",
294 absl::StrJoin(variables, ", ",
295 [&](std::string* const out, const Variable variable) {
296 absl::StrAppendFormat(
297 out, "{%s, %v}", absl::FormatStreamed(variable),
298 RoundTripDoubleFormat(variable_values.at(variable)));
299 }),
300 "}");
301}
302
303// Requires T::ToString() and T::NonzeroVariables() for duck-typing.
304template <typename T>
305std::string ViolatedConstraintAsString(
306 const T violated_constraint, const VariableMap<double>& variable_values,
307 const absl::string_view constraint_type) {
308 return absl::StrFormat(
309 "violated %s %s: %s, with variable values %s", constraint_type,
310 absl::FormatStreamed(violated_constraint), violated_constraint.ToString(),
311 VariableValuesAsString(violated_constraint.NonzeroVariables(),
312 variable_values));
313}
314
315// Requires T::ToString() and T::NonzeroVariables() for duck-typing.
316template <typename T>
317void AppendViolatedConstraintsAsStrings(
318 const std::vector<T>& violated_constraints,
319 const VariableMap<double>& variable_values,
320 const absl::string_view constraint_type, std::vector<std::string>& output) {
321 for (const T violated_constraint : violated_constraints) {
322 output.push_back(ViolatedConstraintAsString(
323 violated_constraint, variable_values, constraint_type));
324 }
325}
326
327} // namespace
328
329absl::StatusOr<std::vector<std::string>> ViolatedConstraintsAsStrings(
330 const Model& model, const ModelSubset& violated_constraints,
331 const VariableMap<double>& variable_values) {
332 RETURN_IF_ERROR(violated_constraints.CheckModelStorage(model.storage()))
333 << "violated_constraints and model are inconsistent";
334 RETURN_IF_ERROR(ValidateVariables(model, variable_values));
335
336 std::vector<std::string> result;
337 for (const Variable variable :
338 SortedKeys(violated_constraints.variable_bounds)) {
339 result.push_back(absl::StrFormat(
340 "violated variable bound: %v ≤ %s ≤ %v, with variable value %v",
341 RoundTripDoubleFormat(variable.lower_bound()),
342 absl::FormatStreamed(variable),
343 RoundTripDoubleFormat(variable.upper_bound()),
344 RoundTripDoubleFormat(variable_values.at(variable))));
345 }
346 for (const Variable variable :
347 SortedElements(violated_constraints.variable_integrality)) {
348 result.push_back(absl::StrFormat(
349 "violated variable integrality: %s, with variable value %v",
350 absl::FormatStreamed(variable),
351 RoundTripDoubleFormat(variable_values.at(variable))));
352 }
353 for (const LinearConstraint linear_constraint :
354 SortedKeys(violated_constraints.linear_constraints)) {
355 result.push_back(absl::StrFormat(
356 "violated linear constraint %s: %s, with variable values %s",
357 absl::FormatStreamed(linear_constraint), linear_constraint.ToString(),
358 VariableValuesAsString(model.RowNonzeros(linear_constraint),
359 variable_values)));
360 }
361 AppendViolatedConstraintsAsStrings(
362 SortedKeys(violated_constraints.quadratic_constraints), variable_values,
363 "quadratic constraint", result);
364 AppendViolatedConstraintsAsStrings(
365 SortedElements(violated_constraints.second_order_cone_constraints),
366 variable_values, "second-order cone constraint", result);
367 AppendViolatedConstraintsAsStrings(
368 SortedElements(violated_constraints.sos1_constraints), variable_values,
369 "SOS1 constraint", result);
370 AppendViolatedConstraintsAsStrings(
371 SortedElements(violated_constraints.sos2_constraints), variable_values,
372 "SOS2 constraint", result);
373 AppendViolatedConstraintsAsStrings(
374 SortedElements(violated_constraints.indicator_constraints),
375 variable_values, "indicator constraint", result);
376 return result;
377}
378
379} // namespace operations_research::math_opt
#define RETURN_IF_ERROR(expr)
static T IPow(T base, int exp)
Definition mathutil.h:120
double upper_bound
double lower_bound
GRBmodel * model
An object oriented wrapper for quadratic constraints in ModelStorage.
Definition gurobi_isv.cc:28
absl::flat_hash_map< Variable, V > VariableMap
absl::StatusOr< std::vector< std::string > > ViolatedConstraintsAsStrings(const Model &model, const ModelSubset &violated_constraints, const VariableMap< double > &variable_values)
std::vector< typename Set::key_type > SortedElements(const Set &set)
Definition key_types.h:110
absl::StatusOr< ModelSubset > CheckPrimalSolutionFeasibility(const Model &model, const VariableMap< double > &variable_values, const FeasibilityCheckerOptions &options)
std::vector< typename Map::key_type > SortedKeys(const Map &map)
Definition key_types.h:87
StatusBuilder InvalidArgumentErrorBuilder()
absl::flat_hash_map< Variable, Bounds > variable_bounds
absl::flat_hash_set< SecondOrderConeConstraint > second_order_cone_constraints
absl::Status CheckModelStorage(const ModelStorage *expected_storage) const
absl::flat_hash_map< LinearConstraint, Bounds > linear_constraints
absl::flat_hash_set< IndicatorConstraint > indicator_constraints
absl::flat_hash_map< QuadraticConstraint, Bounds > quadratic_constraints