Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
gscip_solver.cc
Go to the documentation of this file.
1// Copyright 2010-2024 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <algorithm>
17#include <cmath>
18#include <cstdint>
19#include <limits>
20#include <memory>
21#include <optional>
22#include <string>
23#include <utility>
24#include <vector>
25
26#include "absl/cleanup/cleanup.h"
27#include "absl/container/flat_hash_map.h"
28#include "absl/container/flat_hash_set.h"
29#include "absl/log/check.h"
30#include "absl/log/die_if_null.h"
31#include "absl/memory/memory.h"
32#include "absl/status/status.h"
33#include "absl/status/statusor.h"
34#include "absl/strings/str_cat.h"
35#include "absl/strings/str_join.h"
36#include "absl/strings/string_view.h"
37#include "absl/time/clock.h"
38#include "absl/time/time.h"
39#include "absl/types/span.h"
40#include "google/protobuf/repeated_ptr_field.h"
46#include "ortools/gscip/gscip.h"
47#include "ortools/gscip/gscip.pb.h"
50#include "ortools/math_opt/callback.pb.h"
57#include "ortools/math_opt/infeasible_subsystem.pb.h"
58#include "ortools/math_opt/model.pb.h"
59#include "ortools/math_opt/model_parameters.pb.h"
60#include "ortools/math_opt/model_update.pb.h"
61#include "ortools/math_opt/parameters.pb.h"
62#include "ortools/math_opt/result.pb.h"
63#include "ortools/math_opt/solution.pb.h"
66#include "ortools/math_opt/sparse_containers.pb.h"
70#include "scip/type_cons.h"
71#include "scip/type_var.h"
72
73namespace operations_research {
74namespace math_opt {
75
76namespace {
77
78constexpr double kInf = std::numeric_limits<double>::infinity();
79
80constexpr SupportedProblemStructures kGscipSupportedStructures = {
81 .integer_variables = SupportType::kSupported,
82 .quadratic_objectives = SupportType::kSupported,
83 .quadratic_constraints = SupportType::kSupported,
84 .sos1_constraints = SupportType::kSupported,
85 .sos2_constraints = SupportType::kSupported,
86 .indicator_constraints = SupportType::kSupported};
87
88int64_t SafeId(const VariablesProto& variables, int index) {
89 if (variables.ids().empty()) {
90 return index;
91 }
92 return variables.ids(index);
93}
94
95const std::string& EmptyString() {
96 static const std::string* const empty_string = new std::string;
97 return *empty_string;
98}
99
100const std::string& SafeName(const VariablesProto& variables, int index) {
101 if (variables.names().empty()) {
102 return EmptyString();
103 }
104 return variables.names(index);
105}
106
107int64_t SafeId(const LinearConstraintsProto& linear_constraints, int index) {
108 if (linear_constraints.ids().empty()) {
109 return index;
110 }
111 return linear_constraints.ids(index);
112}
113
114absl::string_view SafeName(const LinearConstraintsProto& linear_constraints,
115 int index) {
116 if (linear_constraints.names().empty()) {
117 return EmptyString();
118 }
119 return linear_constraints.names(index);
120}
121
122absl::flat_hash_map<int64_t, double> SparseDoubleVectorAsMap(
123 const SparseDoubleVectorProto& vector) {
124 CHECK_EQ(vector.ids_size(), vector.values_size());
125 absl::flat_hash_map<int64_t, double> result;
126 result.reserve(vector.ids_size());
127 for (int i = 0; i < vector.ids_size(); ++i) {
128 result[vector.ids(i)] = vector.values(i);
129 }
130 return result;
131}
132
133// Viewing matrix as a list of (row, column, value) tuples stored in row major
134// order, does a linear scan from index scan_start to find the index of the
135// first entry with row >= row_id. Returns the size the tuple list if there is
136// no such entry.
137inline int FindRowStart(const SparseDoubleMatrixProto& matrix,
138 const int64_t row_id, const int scan_start) {
139 int result = scan_start;
140 while (result < matrix.row_ids_size() && matrix.row_ids(result) < row_id) {
141 ++result;
142 }
143 return result;
144}
145
146struct LinearConstraintView {
150 absl::string_view name;
151 absl::Span<const int64_t> variable_ids;
152 absl::Span<const double> coefficients;
153};
154
155// Iterates over the constraints from a LinearConstraints, outputting a
156// LinearConstraintView for each constraint. Requires a SparseDoubleMatrixProto
157// which may have data from additional constraints not in LinearConstraints.
158//
159// The running time to iterate through and read each element once is
160// O(Size(*linear_constraints) + Size(*linear_constraint_matrix)).
161class LinearConstraintIterator {
162 public:
163 LinearConstraintIterator(
164 const LinearConstraintsProto* linear_constraints,
165 const SparseDoubleMatrixProto* linear_constraint_matrix)
166 : linear_constraints_(ABSL_DIE_IF_NULL(linear_constraints)),
167 linear_constraint_matrix_(ABSL_DIE_IF_NULL(linear_constraint_matrix)) {
168 if (NumConstraints(*linear_constraints_) > 0) {
169 const int64_t first_constraint = SafeId(*linear_constraints_, 0);
170 matrix_start_ =
171 FindRowStart(*linear_constraint_matrix_, first_constraint, 0);
172 matrix_end_ = FindRowStart(*linear_constraint_matrix_,
173 first_constraint + 1, matrix_start_);
174 } else {
175 matrix_start_ = NumMatrixNonzeros(*linear_constraint_matrix_);
176 matrix_end_ = matrix_start_;
177 }
178 }
179
180 bool IsDone() const {
181 return current_con_ >= NumConstraints(*linear_constraints_);
182 }
183
184 // Call only if !IsDone(). Runs in O(1).
185 LinearConstraintView Current() const {
186 CHECK(!IsDone());
187 LinearConstraintView result;
188 result.lower_bound = linear_constraints_->lower_bounds(current_con_);
189 result.upper_bound = linear_constraints_->upper_bounds(current_con_);
190 result.name = SafeName(*linear_constraints_, current_con_);
191 result.linear_constraint_id = SafeId(*linear_constraints_, current_con_);
192
193 const auto vars_begin = linear_constraint_matrix_->column_ids().data();
194 result.variable_ids = absl::MakeConstSpan(vars_begin + matrix_start_,
195 vars_begin + matrix_end_);
196 const auto coefficients_begins =
197 linear_constraint_matrix_->coefficients().data();
198 result.coefficients = absl::MakeConstSpan(
199 coefficients_begins + matrix_start_, coefficients_begins + matrix_end_);
200 return result;
201 }
202
203 // Call only if !IsDone().
204 void Next() {
205 CHECK(!IsDone());
206 ++current_con_;
207 if (IsDone()) {
208 matrix_start_ = NumMatrixNonzeros(*linear_constraint_matrix_);
209 matrix_end_ = matrix_start_;
210 return;
211 }
212 const int64_t current_row_id = SafeId(*linear_constraints_, current_con_);
213 matrix_start_ =
214 FindRowStart(*linear_constraint_matrix_, current_row_id, matrix_end_);
215
216 matrix_end_ = FindRowStart(*linear_constraint_matrix_, current_row_id + 1,
217 matrix_start_);
218 }
219
220 private:
221 // NOT OWNED
222 const LinearConstraintsProto* const linear_constraints_;
223 // NOT OWNED
224 const SparseDoubleMatrixProto* const linear_constraint_matrix_;
225 // An index into linear_constraints_, the constraint currently being viewed,
226 // or Size(linear_constraints_) when IsDone().
227 int current_con_ = 0;
228
229 // Informal: the interval [matrix_start_, matrix_end_) gives the indices in
230 // linear_constraint_matrix_ for linear_constraints_[current_con_]
231 //
232 // Invariant: if !IsDone():
233 // * matrix_start_: the first index in linear_constraint_matrix_ with row id
234 // >= RowId(linear_constraints_[current_con_])
235 // * matrix_end_: the first index in linear_constraint_matrix_ with row id
236 // >= RowId(linear_constraints_[current_con_]) + 1
237 //
238 // Implementation note: matrix_start_ and matrix_end_ equal
239 // Size(linear_constraint_matrix_) when IsDone().
240 int matrix_start_ = 0;
241 int matrix_end_ = 0;
242};
243
244inline GScipVarType GScipVarTypeFromIsInteger(const bool is_integer) {
245 return is_integer ? GScipVarType::kInteger : GScipVarType::kContinuous;
246}
247
248template <typename T>
249SparseDoubleVectorProto FillSparseDoubleVector(
251 const absl::flat_hash_map<T, double>& value_map,
252 const SparseVectorFilterProto& filter) {
253 SparseVectorFilterPredicate predicate(filter);
254 SparseDoubleVectorProto result;
255 for (const auto [mathopt_id, scip_id] : id_map) {
256 const double value = value_map.at(scip_id);
257 if (predicate.AcceptsAndUpdate(mathopt_id, value)) {
258 result.add_ids(mathopt_id);
259 result.add_values(value);
260 }
261 }
262 return result;
263}
264
265} // namespace
266
267absl::Status GScipSolver::AddVariables(
268 const VariablesProto& variables,
269 const absl::flat_hash_map<int64_t, double>& linear_objective_coefficients) {
270 for (int i = 0; i < NumVariables(variables); ++i) {
271 const int64_t id = SafeId(variables, i);
272 // SCIP is failing with an assert in SCIPcreateVar() when input bounds are
273 // inverted. That said, it is not an issue if the bounds are created
274 // non-inverted and later changed. Thus here we use this hack to bypass the
275 // assertion in this corner case.
276 const bool inverted_bounds =
277 variables.lower_bounds(i) > variables.upper_bounds(i);
279 SCIP_VAR* const v,
280 gscip_->AddVariable(
281 variables.lower_bounds(i),
282 inverted_bounds ? variables.lower_bounds(i)
283 : variables.upper_bounds(i),
284 gtl::FindWithDefault(linear_objective_coefficients, id),
285 GScipVarTypeFromIsInteger(variables.integers(i)),
286 SafeName(variables, i)));
287 if (inverted_bounds) {
288 const double ub = variables.upper_bounds(i);
289 if (gscip_->VarType(v) == GScipVarType::kBinary && ub != 0.0 &&
290 ub != 1.0) {
291 // gSCIP (and SCIP actually) upgrades the variable type to kBinary if
292 // the bounds passed to AddVariable() are both in {0.0, 1.0}. Changing
293 // the bounds then raises an assertion in SCIP if the bounds is not in
294 // {0.0, 1.0}.
295 RETURN_IF_ERROR(gscip_->SetVarType(v, GScipVarType::kInteger));
296 }
297 RETURN_IF_ERROR(gscip_->SetUb(v, variables.upper_bounds(i)));
298 }
299 gtl::InsertOrDie(&variables_, id, v);
300 }
301 return absl::OkStatus();
302}
303
304absl::StatusOr<bool> GScipSolver::UpdateVariables(
305 const VariableUpdatesProto& variable_updates) {
306 for (const auto [id, is_integer] : MakeView(variable_updates.integers())) {
307 // We intentionally update vartype first to ensure the checks below against
308 // binary variables are against the up-to-date model.
309 SCIP_VAR* const var = variables_.at(id);
310 if (gscip_->VarType(var) == GScipVarType::kBinary) {
311 // We reject bound updates on binary variables as they can lead to
312 // crashes, or unexpected round-trip values if the vartype changes.
313 return false;
314 }
316 gscip_->SetVarType(var, GScipVarTypeFromIsInteger(is_integer)));
317 }
318 for (const auto [id, lb] : MakeView(variable_updates.lower_bounds())) {
319 SCIP_VAR* const var = variables_.at(id);
320 if (gscip_->VarType(var) == GScipVarType::kBinary) {
321 // We reject bound updates on binary variables as they can lead to
322 // crashes, or unexpected round-trip values if the vartype changes.
323 return false;
324 }
325 RETURN_IF_ERROR(gscip_->SetLb(var, lb));
326 }
327 for (const auto [id, ub] : MakeView(variable_updates.upper_bounds())) {
328 SCIP_VAR* const var = variables_.at(id);
329 if (gscip_->VarType(var) == GScipVarType::kBinary) {
330 // We reject bound updates on binary variables as they can lead to
331 // crashes, or unexpected round-trip values if the vartype changes.
332 return false;
333 }
334 RETURN_IF_ERROR(gscip_->SetUb(var, ub));
335 }
336 return true;
337}
338
339// SCIP does not natively support quadratic objectives, so we formulate them
340// using quadratic constraints. We use a epi-/hypo-graph formulation depending
341// on the objective sense:
342// min x'Qx <--> min y s.t. y >= x'Qx
343// max x'Qx <--> max y s.t. y <= x'Qx
344absl::Status GScipSolver::AddQuadraticObjectiveTerms(
345 const SparseDoubleMatrixProto& new_qp_terms, const bool maximize) {
346 const int num_qp_terms = new_qp_terms.row_ids_size();
347 if (num_qp_terms == 0) {
348 return absl::OkStatus();
349 }
351 SCIP_VAR* const qp_auxiliary_variable,
352 gscip_->AddVariable(-kInf, kInf, 1.0, GScipVarType::kContinuous));
353 GScipQuadraticRange range{
354 .lower_bound = maximize ? 0.0 : -kInf,
355 .linear_variables = {qp_auxiliary_variable},
356 .linear_coefficients = {-1.0},
357 .upper_bound = maximize ? kInf : 0.0,
358 };
359 range.quadratic_variables1.reserve(num_qp_terms);
360 range.quadratic_variables2.reserve(num_qp_terms);
361 range.quadratic_coefficients.reserve(num_qp_terms);
362 for (int i = 0; i < num_qp_terms; ++i) {
363 range.quadratic_variables1.push_back(
364 variables_.at(new_qp_terms.row_ids(i)));
365 range.quadratic_variables2.push_back(
366 variables_.at(new_qp_terms.column_ids(i)));
367 range.quadratic_coefficients.push_back(new_qp_terms.coefficients(i));
368 }
369 RETURN_IF_ERROR(gscip_->AddQuadraticConstraint(range).status());
370 has_quadratic_objective_ = true;
371 return absl::OkStatus();
372}
373
374absl::Status GScipSolver::AddLinearConstraints(
375 const LinearConstraintsProto& linear_constraints,
376 const SparseDoubleMatrixProto& linear_constraint_matrix) {
377 for (LinearConstraintIterator lin_con_it(&linear_constraints,
378 &linear_constraint_matrix);
379 !lin_con_it.IsDone(); lin_con_it.Next()) {
380 const LinearConstraintView current = lin_con_it.Current();
381
382 GScipLinearRange range;
383 range.lower_bound = current.lower_bound;
384 range.upper_bound = current.upper_bound;
385 range.coefficients = std::vector<double>(current.coefficients.begin(),
386 current.coefficients.end());
387 range.variables.reserve(current.variable_ids.size());
388 for (const int64_t var_id : current.variable_ids) {
389 range.variables.push_back(variables_.at(var_id));
390 }
392 SCIP_CONS* const scip_con,
393 gscip_->AddLinearConstraint(range, std::string(current.name)));
394 gtl::InsertOrDie(&linear_constraints_, current.linear_constraint_id,
395 scip_con);
396 }
397 return absl::OkStatus();
398}
399
400absl::Status GScipSolver::UpdateLinearConstraints(
401 const LinearConstraintUpdatesProto linear_constraint_updates,
402 const SparseDoubleMatrixProto& linear_constraint_matrix,
403 const std::optional<int64_t> first_new_var_id,
404 const std::optional<int64_t> first_new_cstr_id) {
405 for (const auto [id, lb] :
406 MakeView(linear_constraint_updates.lower_bounds())) {
408 gscip_->SetLinearConstraintLb(linear_constraints_.at(id), lb));
409 }
410 for (const auto [id, ub] :
411 MakeView(linear_constraint_updates.upper_bounds())) {
413 gscip_->SetLinearConstraintUb(linear_constraints_.at(id), ub));
414 }
415 for (const auto& [lin_con_id, var_coeffs] : SparseSubmatrixByRows(
416 linear_constraint_matrix, /*start_row_id=*/0,
417 /*end_row_id=*/first_new_cstr_id, /*start_col_id=*/0,
418 /*end_col_id=*/first_new_var_id)) {
419 for (const auto& [var_id, value] : var_coeffs) {
420 RETURN_IF_ERROR(gscip_->SetLinearConstraintCoef(
421 linear_constraints_.at(lin_con_id), variables_.at(var_id), value));
422 }
423 }
424 return absl::OkStatus();
425}
426
427absl::Status GScipSolver::AddQuadraticConstraints(
428 const google::protobuf::Map<int64_t, QuadraticConstraintProto>&
429 quadratic_constraints) {
430 for (const auto& [id, constraint] : quadratic_constraints) {
431 GScipQuadraticRange range{
432 .lower_bound = constraint.lower_bound(),
433 .upper_bound = constraint.upper_bound(),
434 };
435 {
436 const int num_linear_terms = constraint.linear_terms().ids_size();
437 range.linear_variables.reserve(num_linear_terms);
438 range.linear_coefficients.reserve(num_linear_terms);
439 for (const auto [var_id, coeff] : MakeView(constraint.linear_terms())) {
440 range.linear_variables.push_back(variables_.at(var_id));
441 range.linear_coefficients.push_back(coeff);
442 }
443 }
444 {
445 const SparseDoubleMatrixProto& quad_terms = constraint.quadratic_terms();
446 const int num_quad_terms = constraint.quadratic_terms().row_ids_size();
447 range.quadratic_variables1.reserve(num_quad_terms);
448 range.quadratic_variables2.reserve(num_quad_terms);
449 range.quadratic_coefficients.reserve(num_quad_terms);
450 for (int i = 0; i < num_quad_terms; ++i) {
451 range.quadratic_variables1.push_back(
452 variables_.at(quad_terms.row_ids(i)));
453 range.quadratic_variables2.push_back(
454 variables_.at(quad_terms.column_ids(i)));
455 range.quadratic_coefficients.push_back(quad_terms.coefficients(i));
456 }
457 }
458 ASSIGN_OR_RETURN(SCIP_CONS* const scip_con,
459 gscip_->AddQuadraticConstraint(range, constraint.name()));
460 gtl::InsertOrDie(&quadratic_constraints_, id, scip_con);
461 }
462 return absl::OkStatus();
463}
464
465absl::Status GScipSolver::AddIndicatorConstraints(
466 const google::protobuf::Map<int64_t, IndicatorConstraintProto>&
467 indicator_constraints) {
468 for (const auto& [id, constraint] : indicator_constraints) {
469 if (!constraint.has_indicator_id()) {
470 gtl::InsertOrDie(&indicator_constraints_, id, std::nullopt);
471 continue;
472 }
473 SCIP_VAR* const indicator_var = variables_.at(constraint.indicator_id());
474 // TODO(b/254860940): Properly handle the auxiliary variable that gSCIP may
475 // add if `activate_on_zero()` is true and the indicator constraint is
476 // deleted.
477 GScipIndicatorConstraint data{
478 .indicator_variable = indicator_var,
479 .negate_indicator = constraint.activate_on_zero(),
480 };
481 const double lb = constraint.lower_bound();
482 const double ub = constraint.upper_bound();
483 if (lb > -kInf && ub < kInf) {
485 << "gSCIP does not support indicator constraints with ranged "
486 "implied constraints; bounds are: "
487 << lb << " <= ... <= " << ub;
488 }
489 // SCIP only supports implied constraints of the form ax <= b, so we must
490 // formulate any constraints of the form cx >= d as -cx <= -d.
491 double scaling;
492 if (ub < kInf) {
493 data.upper_bound = ub;
494 scaling = 1.0;
495 } else {
496 data.upper_bound = -lb;
497 scaling = -1.0;
498 }
499 {
500 const int num_terms = constraint.expression().ids_size();
501 data.variables.reserve(num_terms);
502 data.coefficients.reserve(num_terms);
503 for (const auto [var_id, coeff] : MakeView(constraint.expression())) {
504 data.variables.push_back(variables_.at(var_id));
505 data.coefficients.push_back(scaling * coeff);
506 }
507 }
508 ASSIGN_OR_RETURN(SCIP_CONS* const scip_con,
509 gscip_->AddIndicatorConstraint(data, constraint.name()));
510 gtl::InsertOrDie(&indicator_constraints_, id,
511 std::make_pair(scip_con, constraint.indicator_id()));
512 }
513 return absl::OkStatus();
514}
515
516absl::StatusOr<std::pair<SCIP_VAR*, SCIP_CONS*>>
517GScipSolver::AddSlackVariableEqualToExpression(
518 const LinearExpressionProto& expression) {
520 SCIP_VAR * aux_var,
521 gscip_->AddVariable(-kInf, kInf, 0.0, GScipVarType::kContinuous));
522 GScipLinearRange range{
523 .lower_bound = -expression.offset(),
524 .upper_bound = -expression.offset(),
525 };
526 range.variables.push_back(aux_var);
527 range.coefficients.push_back(-1.0);
528 for (int i = 0; i < expression.ids_size(); ++i) {
529 range.variables.push_back(variables_.at(expression.ids(i)));
530 range.coefficients.push_back(expression.coefficients(i));
531 }
532 ASSIGN_OR_RETURN(SCIP_CONS * aux_constr, gscip_->AddLinearConstraint(range));
533 return std::make_pair(aux_var, aux_constr);
534}
535
536absl::Status GScipSolver::AuxiliaryStructureHandler::DeleteStructure(
537 GScip& gscip) {
538 for (SCIP_CONS* const constraint : constraints) {
539 RETURN_IF_ERROR(gscip.DeleteConstraint(constraint));
540 }
541 for (SCIP_VAR* const variable : variables) {
542 RETURN_IF_ERROR(gscip.DeleteVariable(variable));
543 }
544 variables.clear();
545 constraints.clear();
546 return absl::OkStatus();
547}
548
549absl::StatusOr<std::pair<GScipSOSData, GScipSolver::AuxiliaryStructureHandler>>
550GScipSolver::ProcessSosProto(const SosConstraintProto& sos_constraint) {
551 GScipSOSData data;
552 AuxiliaryStructureHandler handler;
553 absl::flat_hash_set<int64_t> reused_variables;
554 for (const LinearExpressionProto& expr : sos_constraint.expressions()) {
555 // If the expression is equivalent to 1 * some_variable, there is no need to
556 // add a slack variable. But, SCIP crashes in debug mode if you repeat a
557 // variable twice within an SOS constraint, so we track the ones we reuse.
558 if (expr.ids_size() == 1 && expr.coefficients(0) == 1.0 &&
559 expr.offset() == 0.0 && !reused_variables.contains(expr.ids(0))) {
560 data.variables.push_back(variables_.at(expr.ids(0)));
561 reused_variables.insert(expr.ids(0));
562 } else {
563 ASSIGN_OR_RETURN((const auto [slack_var, slack_constr]),
564 AddSlackVariableEqualToExpression(expr));
565 handler.variables.push_back(slack_var);
566 handler.constraints.push_back(slack_constr);
567 data.variables.push_back(slack_var);
568 }
569 }
570 for (const double weight : sos_constraint.weights()) {
571 data.weights.push_back(weight);
572 }
573 return std::make_pair(data, handler);
574}
575
576absl::Status GScipSolver::AddSos1Constraints(
577 const google::protobuf::Map<int64_t, SosConstraintProto>&
578 sos1_constraints) {
579 for (const auto& [id, constraint] : sos1_constraints) {
580 ASSIGN_OR_RETURN((auto [sos_data, slack_handler]),
581 ProcessSosProto(constraint));
583 SCIP_CONS* const scip_con,
584 gscip_->AddSOS1Constraint(std::move(sos_data), constraint.name()));
585 gtl::InsertOrDie(&sos1_constraints_, id,
586 std::make_pair(scip_con, std::move(slack_handler)));
587 }
588 return absl::OkStatus();
589}
590
591absl::Status GScipSolver::AddSos2Constraints(
592 const google::protobuf::Map<int64_t, SosConstraintProto>&
593 sos2_constraints) {
594 for (const auto& [id, constraint] : sos2_constraints) {
595 ASSIGN_OR_RETURN((auto [sos_data, slack_handler]),
596 ProcessSosProto(constraint));
597 ASSIGN_OR_RETURN(SCIP_CONS* const scip_con,
598 gscip_->AddSOS2Constraint(sos_data, constraint.name()));
599 gtl::InsertOrDie(&sos2_constraints_, id,
600 std::make_pair(scip_con, std::move(slack_handler)));
601 }
602 return absl::OkStatus();
603}
604
605GScipParameters::MetaParamValue ConvertMathOptEmphasis(EmphasisProto emphasis) {
606 switch (emphasis) {
607 case EMPHASIS_OFF:
608 return GScipParameters::OFF;
609 case EMPHASIS_LOW:
610 return GScipParameters::FAST;
611 case EMPHASIS_MEDIUM:
612 case EMPHASIS_UNSPECIFIED:
613 return GScipParameters::DEFAULT_META_PARAM_VALUE;
614 case EMPHASIS_HIGH:
615 case EMPHASIS_VERY_HIGH:
616 return GScipParameters::AGGRESSIVE;
617 default:
618 LOG(FATAL) << "Unsupported MathOpt Emphasis value: "
619 << ProtoEnumToString(emphasis)
620 << " unknown, error setting gSCIP parameters";
621 }
622}
623
624absl::StatusOr<GScipParameters> GScipSolver::MergeParameters(
625 const SolveParametersProto& solve_parameters) {
626 // First build the result by translating common parameters to a
627 // GScipParameters, and then merging with user provided gscip_parameters.
628 // This results in user provided solver specific parameters overwriting
629 // common parameters should there be any conflict.
630 GScipParameters result;
631 std::vector<std::string> warnings;
632
633 // By default SCIP catches Ctrl-C but we don't want this behavior when the
634 // users uses SCIP through MathOpt.
635 GScipSetCatchCtrlC(false, &result);
636
637 if (solve_parameters.has_time_limit()) {
639 util_time::DecodeGoogleApiProto(solve_parameters.time_limit()).value(),
640 &result);
641 }
642 if (solve_parameters.has_iteration_limit()) {
643 warnings.push_back("parameter iteration_limit not supported for gSCIP.");
644 }
645 if (solve_parameters.has_node_limit()) {
646 (*result.mutable_long_params())["limits/totalnodes"] =
647 solve_parameters.node_limit();
648 }
649 if (solve_parameters.has_cutoff_limit()) {
650 result.set_objective_limit(solve_parameters.cutoff_limit());
651 }
652 if (solve_parameters.has_objective_limit()) {
653 warnings.push_back("parameter objective_limit not supported for gSCIP.");
654 }
655 if (solve_parameters.has_best_bound_limit()) {
656 warnings.push_back("parameter best_bound_limit not supported for gSCIP.");
657 }
658 if (solve_parameters.has_solution_limit()) {
659 (*result.mutable_int_params())["limits/solutions"] =
660 solve_parameters.solution_limit();
661 }
662 // GScip has also GScipSetOutputEnabled() but this changes the log
663 // level. Setting `silence_output` sets the `quiet` field on the default
664 // message handler of SCIP which removes the output. Here it is important to
665 // use this rather than changing the log level so that if the user provides
666 // a MessageCallback function they do get some messages even when
667 // `enable_output` is false.
668 result.set_silence_output(!solve_parameters.enable_output());
669 if (solve_parameters.has_threads()) {
670 GScipSetMaxNumThreads(solve_parameters.threads(), &result);
671 }
672 if (solve_parameters.has_random_seed()) {
673 GScipSetRandomSeed(&result, solve_parameters.random_seed());
674 }
675 if (solve_parameters.has_absolute_gap_tolerance()) {
676 (*result.mutable_real_params())["limits/absgap"] =
677 solve_parameters.absolute_gap_tolerance();
678 }
679 if (solve_parameters.has_relative_gap_tolerance()) {
680 (*result.mutable_real_params())["limits/gap"] =
681 solve_parameters.relative_gap_tolerance();
682 }
683 if (solve_parameters.has_solution_pool_size()) {
684 result.set_num_solutions(solve_parameters.solution_pool_size());
685 // We must set limits/maxsol (the internal solution pool) and
686 // limits/maxorigsol (the number solutions to attempt to transform back to
687 // the user.
688 //
689 // NOTE: As of SCIP 8, limits/maxsol defaults to 100.
690 (*result.mutable_int_params())["limits/maxsol"] =
691 std::max(100, solve_parameters.solution_pool_size());
692 (*result.mutable_int_params())["limits/maxorigsol"] =
693 solve_parameters.solution_pool_size();
694 }
695
696 if (solve_parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) {
697 char alg = 's';
698 switch (solve_parameters.lp_algorithm()) {
699 case LP_ALGORITHM_PRIMAL_SIMPLEX:
700 alg = 'p';
701 break;
702 case LP_ALGORITHM_DUAL_SIMPLEX:
703 alg = 'd';
704 break;
705 case LP_ALGORITHM_BARRIER:
706 // As SCIP is configured in ortools, this is an error, since we are not
707 // connected to any LP solver that runs barrier.
708 warnings.push_back(
709 "parameter lp_algorithm with value BARRIER is not supported for "
710 "gSCIP in ortools.");
711 alg = 'c';
712 break;
713 case LP_ALGORITHM_FIRST_ORDER:
714 warnings.push_back(
715 "parameter lp_algorithm with value FIRST_ORDER is not supported.");
716 break;
717 default:
718 LOG(FATAL) << "LPAlgorithm: "
719 << ProtoEnumToString(solve_parameters.lp_algorithm())
720 << " unknown, error setting gSCIP parameters";
721 }
722 (*result.mutable_char_params())["lp/initalgorithm"] = alg;
723 }
724 if (solve_parameters.presolve() != EMPHASIS_UNSPECIFIED) {
725 result.set_presolve(ConvertMathOptEmphasis(solve_parameters.presolve()));
726 }
727 if (solve_parameters.cuts() != EMPHASIS_UNSPECIFIED) {
728 result.set_separating(ConvertMathOptEmphasis(solve_parameters.cuts()));
729 }
730 if (solve_parameters.heuristics() != EMPHASIS_UNSPECIFIED) {
731 result.set_heuristics(
732 ConvertMathOptEmphasis(solve_parameters.heuristics()));
733 }
734 if (solve_parameters.scaling() != EMPHASIS_UNSPECIFIED) {
735 int scaling_value;
736 switch (solve_parameters.scaling()) {
737 case EMPHASIS_OFF:
738 scaling_value = 0;
739 break;
740 case EMPHASIS_LOW:
741 case EMPHASIS_MEDIUM:
742 scaling_value = 1;
743 break;
744 case EMPHASIS_HIGH:
745 case EMPHASIS_VERY_HIGH:
746 scaling_value = 2;
747 break;
748 default:
749 LOG(FATAL) << "Scaling emphasis: "
750 << ProtoEnumToString(solve_parameters.scaling())
751 << " unknown, error setting gSCIP parameters";
752 }
753 (*result.mutable_int_params())["lp/scaling"] = scaling_value;
754 }
755
756 result.MergeFrom(solve_parameters.gscip());
757
758 if (!warnings.empty()) {
759 return absl::InvalidArgumentError(absl::StrJoin(warnings, "; "));
760 }
761 return result;
762}
763
764namespace {
765
766std::string JoinDetails(absl::string_view gscip_detail,
767 absl::string_view math_opt_detail) {
768 if (gscip_detail.empty()) {
769 return std::string(math_opt_detail);
770 }
771 if (math_opt_detail.empty()) {
772 return std::string(gscip_detail);
773 }
774 return absl::StrCat(gscip_detail, "; ", math_opt_detail);
775}
776
777double GetBestPrimalObjective(
778 const double gscip_best_objective,
779 const google::protobuf::RepeatedPtrField<SolutionProto>& solutions,
780 const bool is_maximize) {
781 double result = gscip_best_objective;
782 for (const auto& solution : solutions) {
783 if (solution.has_primal_solution() &&
784 solution.primal_solution().feasibility_status() ==
785 SOLUTION_STATUS_FEASIBLE) {
786 result =
787 is_maximize
788 ? std::max(result, solution.primal_solution().objective_value())
790 }
791 }
792 return result;
793}
794
795absl::StatusOr<TerminationProto> ConvertTerminationReason(
796 const GScipResult& gscip_result, const bool is_maximize,
797 const google::protobuf::RepeatedPtrField<SolutionProto>& solutions,
798 const bool had_cutoff) {
799 const bool has_feasible_solution = !solutions.empty();
800 const GScipOutput::Status gscip_status = gscip_result.gscip_output.status();
801 absl::string_view gscip_status_detail =
802 gscip_result.gscip_output.status_detail();
803 const GScipSolvingStats& gscip_stats = gscip_result.gscip_output.stats();
804 // SCIP may return primal solutions that are slightly better than
805 // gscip_stats.best_objective().
806 const double best_primal_objective = GetBestPrimalObjective(
807 gscip_stats.best_objective(), solutions, is_maximize);
808 const std::optional<double> optional_finite_primal_objective =
809 has_feasible_solution ? std::make_optional(best_primal_objective)
810 : std::nullopt;
811 // For SCIP, the only indicator for the existence of a dual feasible solution
812 // is a finite dual bound.
813 const std::optional<double> optional_dual_objective =
814 std::isfinite(gscip_stats.best_bound())
815 ? std::make_optional(gscip_stats.best_bound())
816 : std::nullopt;
817 switch (gscip_status) {
818 case GScipOutput::USER_INTERRUPT:
820 is_maximize, LIMIT_INTERRUPTED, optional_finite_primal_objective,
821 optional_dual_objective,
822 JoinDetails(gscip_status_detail,
823 "underlying gSCIP status: USER_INTERRUPT"));
824 case GScipOutput::NODE_LIMIT:
826 is_maximize, LIMIT_NODE, optional_finite_primal_objective,
827 optional_dual_objective,
828 JoinDetails(gscip_status_detail,
829 "underlying gSCIP status: NODE_LIMIT"));
830 case GScipOutput::TOTAL_NODE_LIMIT:
832 is_maximize, LIMIT_NODE, optional_finite_primal_objective,
833 optional_dual_objective,
834 JoinDetails(gscip_status_detail,
835 "underlying gSCIP status: TOTAL_NODE_LIMIT"));
836 case GScipOutput::STALL_NODE_LIMIT:
838 is_maximize, LIMIT_SLOW_PROGRESS, optional_finite_primal_objective,
839 optional_dual_objective, gscip_status_detail);
840 case GScipOutput::TIME_LIMIT:
842 is_maximize, LIMIT_TIME, optional_finite_primal_objective,
843 optional_dual_objective, gscip_status_detail);
844 case GScipOutput::MEM_LIMIT:
846 is_maximize, LIMIT_MEMORY, optional_finite_primal_objective,
847 optional_dual_objective, gscip_status_detail);
848 case GScipOutput::SOL_LIMIT:
850 is_maximize, LIMIT_SOLUTION, optional_finite_primal_objective,
851 optional_dual_objective,
852 JoinDetails(gscip_status_detail,
853 "underlying gSCIP status: SOL_LIMIT"));
854 case GScipOutput::BEST_SOL_LIMIT:
856 is_maximize, LIMIT_SOLUTION, optional_finite_primal_objective,
857 optional_dual_objective,
858 JoinDetails(gscip_status_detail,
859 "underlying gSCIP status: BEST_SOL_LIMIT"));
860 case GScipOutput::RESTART_LIMIT:
862 is_maximize, LIMIT_OTHER, optional_finite_primal_objective,
863 optional_dual_objective,
864 JoinDetails(gscip_status_detail,
865 "underlying gSCIP status: RESTART_LIMIT"));
866 case GScipOutput::OPTIMAL:
868 /*finite_primal_objective=*/best_primal_objective,
869 /*dual_objective=*/gscip_stats.best_bound(),
870 JoinDetails(gscip_status_detail, "underlying gSCIP status: OPTIMAL"));
871 case GScipOutput::GAP_LIMIT:
873 /*finite_primal_objective=*/best_primal_objective,
874 /*dual_objective=*/gscip_stats.best_bound(),
875 JoinDetails(gscip_status_detail,
876 "underlying gSCIP status: GAP_LIMIT"));
877 case GScipOutput::INFEASIBLE:
878 if (had_cutoff) {
879 return CutoffTerminationProto(is_maximize, gscip_status_detail);
880 } else {
881 // By convention infeasible MIPs are always dual feasible.
882 const FeasibilityStatusProto dual_feasibility_status =
883 FEASIBILITY_STATUS_FEASIBLE;
884 return InfeasibleTerminationProto(is_maximize, dual_feasibility_status,
885 gscip_status_detail);
886 }
887 case GScipOutput::UNBOUNDED: {
888 if (has_feasible_solution) {
890 is_maximize,
891 JoinDetails(gscip_status_detail,
892 "underlying gSCIP status was UNBOUNDED, both primal "
893 "ray and feasible solution are present"));
894 } else {
896 is_maximize,
897 /*dual_feasibility_status=*/FEASIBILITY_STATUS_INFEASIBLE,
898 JoinDetails(
899 gscip_status_detail,
900 "underlying gSCIP status was UNBOUNDED, but only primal ray "
901 "was given, no feasible solution was found"));
902 }
903 }
904 case GScipOutput::INF_OR_UNBD:
906 is_maximize,
907 /*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED,
908 JoinDetails(gscip_status_detail,
909 "underlying gSCIP status: INF_OR_UNBD"));
910 case GScipOutput::TERMINATE:
912 is_maximize, LIMIT_INTERRUPTED, optional_finite_primal_objective,
913 optional_dual_objective,
914 JoinDetails(gscip_status_detail,
915 "underlying gSCIP status: TERMINATE"));
916 case GScipOutput::INVALID_SOLVER_PARAMETERS:
917 return absl::InvalidArgumentError(gscip_status_detail);
918 case GScipOutput::UNKNOWN:
919 return absl::InternalError(JoinDetails(
920 gscip_status_detail, "Unexpected GScipOutput.status: UNKNOWN"));
921 default:
922 return absl::InternalError(JoinDetails(
923 gscip_status_detail, absl::StrCat("Missing GScipOutput.status case: ",
924 ProtoEnumToString(gscip_status))));
925 }
926}
927
928} // namespace
929
930absl::StatusOr<SolveResultProto> GScipSolver::CreateSolveResultProto(
931 GScipResult gscip_result, const ModelSolveParametersProto& model_parameters,
932 const std::optional<double> cutoff) {
933 SolveResultProto solve_result;
934 const bool is_maximize = gscip_->ObjectiveIsMaximize();
935 // When an objective limit is set, SCIP returns the solutions worse than the
936 // limit, we need to filter these out manually.
937 const auto meets_cutoff = [cutoff, is_maximize](const double obj_value) {
938 if (!cutoff.has_value()) {
939 return true;
940 }
941 if (is_maximize) {
942 return obj_value >= *cutoff;
943 } else {
944 return obj_value <= *cutoff;
945 }
946 };
947
948 CHECK_EQ(gscip_result.solutions.size(), gscip_result.objective_values.size());
949 for (int i = 0; i < gscip_result.solutions.size(); ++i) {
950 // GScip ensures the solutions are returned best objective first.
951 if (!meets_cutoff(gscip_result.objective_values[i])) {
952 break;
953 }
954 SolutionProto* const solution = solve_result.add_solutions();
955 PrimalSolutionProto* const primal_solution =
956 solution->mutable_primal_solution();
957 primal_solution->set_objective_value(gscip_result.objective_values[i]);
958 primal_solution->set_feasibility_status(SOLUTION_STATUS_FEASIBLE);
959 *primal_solution->mutable_variable_values() =
960 FillSparseDoubleVector(variables_, gscip_result.solutions[i],
961 model_parameters.variable_values_filter());
962 }
963 if (!gscip_result.primal_ray.empty()) {
964 *solve_result.add_primal_rays()->mutable_variable_values() =
965 FillSparseDoubleVector(variables_, gscip_result.primal_ray,
966 model_parameters.variable_values_filter());
967 }
968 ASSIGN_OR_RETURN(*solve_result.mutable_termination(),
969 ConvertTerminationReason(gscip_result, is_maximize,
970 solve_result.solutions(),
971 /*had_cutoff=*/cutoff.has_value()));
972 SolveStatsProto* const common_stats = solve_result.mutable_solve_stats();
973 const GScipSolvingStats& gscip_stats = gscip_result.gscip_output.stats();
974
975 common_stats->set_node_count(gscip_stats.node_count());
976 common_stats->set_simplex_iterations(gscip_stats.primal_simplex_iterations() +
977 gscip_stats.dual_simplex_iterations());
978 common_stats->set_barrier_iterations(gscip_stats.barrier_iterations());
979 *solve_result.mutable_gscip_output() = std::move(gscip_result.gscip_output);
980 return solve_result;
981}
982
983GScipSolver::GScipSolver(std::unique_ptr<GScip> gscip)
984 : gscip_(std::move(ABSL_DIE_IF_NULL(gscip))) {}
985
986absl::StatusOr<std::unique_ptr<SolverInterface>> GScipSolver::New(
987 const ModelProto& model, const InitArgs&) {
988 RETURN_IF_ERROR(ModelIsSupported(model, kGscipSupportedStructures, "SCIP"));
989 ASSIGN_OR_RETURN(std::unique_ptr<GScip> gscip, GScip::Create(model.name()));
990 RETURN_IF_ERROR(gscip->SetMaximize(model.objective().maximize()));
991 RETURN_IF_ERROR(gscip->SetObjectiveOffset(model.objective().offset()));
992 auto solver = absl::WrapUnique(new GScipSolver(std::move(gscip)));
993 RETURN_IF_ERROR(solver->constraint_handler_.Register(solver->gscip_.get()));
994 RETURN_IF_ERROR(solver->AddVariables(
995 model.variables(),
996 SparseDoubleVectorAsMap(model.objective().linear_coefficients())));
997 RETURN_IF_ERROR(solver->AddQuadraticObjectiveTerms(
998 model.objective().quadratic_coefficients(),
999 model.objective().maximize()));
1000 RETURN_IF_ERROR(solver->AddLinearConstraints(
1001 model.linear_constraints(), model.linear_constraint_matrix()));
1003 solver->AddQuadraticConstraints(model.quadratic_constraints()));
1005 solver->AddIndicatorConstraints(model.indicator_constraints()));
1006 RETURN_IF_ERROR(solver->AddSos1Constraints(model.sos1_constraints()));
1007 RETURN_IF_ERROR(solver->AddSos2Constraints(model.sos2_constraints()));
1008 return solver;
1009}
1010
1011absl::StatusOr<SolveResultProto> GScipSolver::Solve(
1012 const SolveParametersProto& parameters,
1013 const ModelSolveParametersProto& model_parameters,
1014 const MessageCallback message_cb,
1015 const CallbackRegistrationProto& callback_registration, Callback cb,
1016 const SolveInterrupter* const interrupter) {
1017 const absl::Time start = absl::Now();
1018
1019 GScip::Interrupter gscip_interrupter;
1020 const ScopedSolveInterrupterCallback scoped_interrupt_cb(
1021 interrupter, [&]() { gscip_interrupter.Interrupt(); });
1022 const bool use_interrupter = interrupter != nullptr || cb != nullptr;
1023
1025 callback_registration,
1026 /*supported_events=*/{CALLBACK_EVENT_MIP_SOLUTION,
1027 CALLBACK_EVENT_MIP_NODE}));
1028 if (constraint_data_ != nullptr) {
1029 return absl::InternalError(
1030 "constraint_data_ should always be null at the start of "
1031 "GScipSolver::Solver()");
1032 }
1033 SCIP_CONS* callback_cons = nullptr;
1034 if (cb != nullptr) {
1035 // NOTE: we must meet the invariant on GScipSolverConstraintData, that when
1036 // user_callback != nullptr, all fields are filled in.
1037 constraint_data_ = std::make_unique<GScipSolverConstraintData>();
1038 constraint_data_->user_callback = std::move(cb);
1039 constraint_data_->SetWhenRunAndAdds(callback_registration);
1040 constraint_data_->solve_start_time = absl::Now();
1041 constraint_data_->variables = &variables_;
1042 constraint_data_->variable_node_filter =
1043 &callback_registration.mip_node_filter();
1044 constraint_data_->variable_solution_filter =
1045 &callback_registration.mip_solution_filter();
1046 constraint_data_->interrupter = &gscip_interrupter;
1047 // NOTE: it is critical that this constraint is added after all other
1048 // constraints, as otherwise, due to what appears to be a bug in SCIP, we
1049 // may run our callback before checking all the constraints in the model,
1050 // see https://listserv.zib.de/pipermail/scip/2023-November/004785.html.
1051 ASSIGN_OR_RETURN(callback_cons,
1052 constraint_handler_.AddCallbackConstraint(
1053 gscip_.get(), "mathopt_callback_constraint",
1054 constraint_data_.get()));
1055 }
1056 const auto cleanup_constraint_data = absl::Cleanup([this]() {
1057 if (constraint_data_ != nullptr) {
1058 *constraint_data_ = {};
1059 }
1060 });
1061
1062 BufferedMessageCallback buffered_message_callback(std::move(message_cb));
1063 auto message_cb_cleanup = absl::MakeCleanup(
1064 [&buffered_message_callback]() { buffered_message_callback.Flush(); });
1065
1066 ASSIGN_OR_RETURN(auto gscip_parameters, MergeParameters(parameters));
1067
1068 for (const SolutionHintProto& hint : model_parameters.solution_hints()) {
1069 absl::flat_hash_map<SCIP_VAR*, double> partial_solution;
1070 for (const auto [id, val] : MakeView(hint.variable_values())) {
1071 partial_solution.insert({variables_.at(id), val});
1072 }
1073 RETURN_IF_ERROR(gscip_->SuggestHint(partial_solution).status());
1074 }
1075 for (const auto [id, value] :
1076 MakeView(model_parameters.branching_priorities())) {
1077 RETURN_IF_ERROR(gscip_->SetBranchingPriority(variables_.at(id), value));
1078 }
1079
1080 // SCIP returns "infeasible" when the model contain invalid bounds.
1081 RETURN_IF_ERROR(ListInvertedBounds().ToStatus());
1082 RETURN_IF_ERROR(ListInvalidIndicators().ToStatus());
1083
1084 GScipMessageHandler gscip_msg_cb = nullptr;
1085 if (buffered_message_callback.has_user_message_callback()) {
1086 gscip_msg_cb = [&buffered_message_callback](
1087 const auto, const absl::string_view message) {
1088 buffered_message_callback.OnMessage(message);
1089 };
1090 }
1091
1093 GScipResult gscip_result,
1094 gscip_->Solve(gscip_parameters,
1095 /*legacy_params=*/"", std::move(gscip_msg_cb),
1096 use_interrupter ? &gscip_interrupter : nullptr));
1097
1098 // Flush the potential last unfinished line.
1099 std::move(message_cb_cleanup).Invoke();
1100
1101 if (callback_cons != nullptr) {
1102 RETURN_IF_ERROR(gscip_->DeleteConstraint(callback_cons));
1103 constraint_data_.reset();
1104 }
1105
1107 SolveResultProto result,
1108 CreateSolveResultProto(std::move(gscip_result), model_parameters,
1109 parameters.has_cutoff_limit()
1110 ? std::make_optional(parameters.cutoff_limit())
1111 : std::nullopt));
1112
1113 // Reset solve-specific model parameters so that they do not leak into the
1114 // next solve. Note that 0 is the default branching priority.
1115 for (const auto [id, unused] :
1116 MakeView(model_parameters.branching_priorities())) {
1117 RETURN_IF_ERROR(gscip_->SetBranchingPriority(variables_.at(id), 0));
1118 }
1119
1121 absl::Now() - start, result.mutable_solve_stats()->mutable_solve_time()));
1122 return result;
1123}
1124
1125absl::flat_hash_set<SCIP_VAR*> GScipSolver::LookupAllVariables(
1126 absl::Span<const int64_t> variable_ids) {
1127 absl::flat_hash_set<SCIP_VAR*> result;
1128 result.reserve(variable_ids.size());
1129 for (const int64_t var_id : variable_ids) {
1130 result.insert(variables_.at(var_id));
1131 }
1132 return result;
1133}
1134
1135// Returns the ids of variables and linear constraints with inverted bounds.
1136InvertedBounds GScipSolver::ListInvertedBounds() const {
1137 // Get the SCIP variables/constraints with inverted bounds.
1138 InvertedBounds inverted_bounds;
1139 for (const auto& [id, var] : variables_) {
1140 if (gscip_->Lb(var) > gscip_->Ub(var)) {
1141 inverted_bounds.variables.push_back(id);
1142 }
1143 }
1144 for (const auto& [id, cstr] : linear_constraints_) {
1145 if (gscip_->LinearConstraintLb(cstr) > gscip_->LinearConstraintUb(cstr)) {
1146 inverted_bounds.linear_constraints.push_back(id);
1147 }
1148 }
1149
1150 // Above code have inserted ids in non-stable order.
1151 std::sort(inverted_bounds.variables.begin(), inverted_bounds.variables.end());
1152 std::sort(inverted_bounds.linear_constraints.begin(),
1153 inverted_bounds.linear_constraints.end());
1154 return inverted_bounds;
1155}
1156
1157InvalidIndicators GScipSolver::ListInvalidIndicators() const {
1158 InvalidIndicators invalid_indicators;
1159 for (const auto& [constraint_id, gscip_data] : indicator_constraints_) {
1160 if (!gscip_data.has_value()) {
1161 continue;
1162 }
1163 const auto [gscip_constraint, indicator_id] = *gscip_data;
1164 SCIP_VAR* const indicator_var = variables_.at(indicator_id);
1165 if (gscip_->VarType(indicator_var) == GScipVarType::kContinuous ||
1166 gscip_->Lb(indicator_var) < 0.0 || gscip_->Ub(indicator_var) > 1.0) {
1167 invalid_indicators.invalid_indicators.push_back(
1168 {.variable = indicator_id, .constraint = constraint_id});
1169 }
1170 }
1171 invalid_indicators.Sort();
1172 return invalid_indicators;
1173}
1174
1175absl::StatusOr<bool> GScipSolver::Update(const ModelUpdateProto& model_update) {
1176 if (!gscip_
1177 ->CanSafeBulkDelete(
1178 LookupAllVariables(model_update.deleted_variable_ids()))
1179 .ok() ||
1180 !UpdateIsSupported(model_update, kGscipSupportedStructures)) {
1181 return false;
1182 }
1183 // As of 2022-09-12 we do not support quadratic objective updates. Therefore,
1184 // if we already have a quadratic objective stored, we reject any update that
1185 // changes the objective sense (which would break the epi-/hypo-graph
1186 // formulation) or changes any quadratic objective coefficients.
1187 if (has_quadratic_objective_ &&
1188 (model_update.objective_updates().has_direction_update() ||
1189 model_update.objective_updates()
1190 .quadratic_coefficients()
1191 .row_ids_size() > 0)) {
1192 return false;
1193 }
1194
1195 for (const int64_t constraint_id :
1196 model_update.deleted_linear_constraint_ids()) {
1197 SCIP_CONS* const scip_cons = linear_constraints_.at(constraint_id);
1198 linear_constraints_.erase(constraint_id);
1199 RETURN_IF_ERROR(gscip_->DeleteConstraint(scip_cons));
1200 }
1201 {
1202 const absl::flat_hash_set<SCIP_VAR*> vars_to_delete =
1203 LookupAllVariables(model_update.deleted_variable_ids());
1204 for (const int64_t deleted_variable_id :
1205 model_update.deleted_variable_ids()) {
1206 variables_.erase(deleted_variable_id);
1207 }
1208 RETURN_IF_ERROR(gscip_->SafeBulkDelete(vars_to_delete));
1209 }
1210
1211 const std::optional<int64_t> first_new_var_id =
1212 FirstVariableId(model_update.new_variables());
1213 const std::optional<int64_t> first_new_cstr_id =
1214 FirstLinearConstraintId(model_update.new_linear_constraints());
1215
1216 if (model_update.objective_updates().has_direction_update()) {
1217 RETURN_IF_ERROR(gscip_->SetMaximize(
1218 model_update.objective_updates().direction_update()));
1219 }
1220 if (model_update.objective_updates().has_offset_update()) {
1221 RETURN_IF_ERROR(gscip_->SetObjectiveOffset(
1222 model_update.objective_updates().offset_update()));
1223 }
1224 if (const auto response = UpdateVariables(model_update.variable_updates());
1225 !response.ok() || !(*response)) {
1226 return response;
1227 }
1228 const absl::flat_hash_map<int64_t, double> linear_objective_updates =
1229 SparseDoubleVectorAsMap(
1230 model_update.objective_updates().linear_coefficients());
1231 for (const auto& obj_pair : linear_objective_updates) {
1232 // New variables' coefficient is set when the variables are added below.
1233 if (!first_new_var_id.has_value() || obj_pair.first < *first_new_var_id) {
1235 gscip_->SetObjCoef(variables_.at(obj_pair.first), obj_pair.second));
1236 }
1237 }
1238
1239 // Here the model_update.linear_constraint_matrix_updates is split into three
1240 // sub-matrix:
1241 //
1242 // existing new
1243 // columns columns
1244 // / | \
1245 // existing | 1 | 2 |
1246 // rows | | |
1247 // |---------+---------|
1248 // new | |
1249 // rows | 3 |
1250 // \ /
1251 //
1252 // The coefficients of sub-matrix 1 are set by UpdateLinearConstraints(), the
1253 // ones of sub-matrix 2 by AddVariables() and the ones of the sub-matrix 3 by
1254 // AddLinearConstraints(). The rationale here is that SCIPchgCoefLinear() has
1255 // a complexity of O(non_zeros). Thus it is inefficient and can lead to O(n^2)
1256 // behaviors if it was used for new rows or for new columns. For new rows it
1257 // is more efficient to pass all the variables coefficients at once when
1258 // building the constraints. For new columns and existing rows, since we can
1259 // assume there is no existing coefficient, we can use SCIPaddCoefLinear()
1260 // which is O(1). This leads to only use SCIPchgCoefLinear() for changing the
1261 // coefficients of existing rows and columns.
1262 //
1263 // TODO(b/215722113): maybe we could use SCIPaddCoefLinear() for sub-matrix 1.
1264
1265 // Add new variables.
1267 AddVariables(model_update.new_variables(), linear_objective_updates));
1268
1269 RETURN_IF_ERROR(AddQuadraticObjectiveTerms(
1270 model_update.objective_updates().quadratic_coefficients(),
1271 gscip_->ObjectiveIsMaximize()));
1272
1273 // Update linear constraints properties and sub-matrix 1.
1275 UpdateLinearConstraints(model_update.linear_constraint_updates(),
1276 model_update.linear_constraint_matrix_updates(),
1277 /*first_new_var_id=*/first_new_var_id,
1278 /*first_new_cstr_id=*/first_new_cstr_id));
1279
1280 // Update the sub-matrix 2.
1281 const std::optional first_new_variable_id =
1282 FirstVariableId(model_update.new_variables());
1283 if (first_new_variable_id.has_value()) {
1284 for (const auto& [lin_con_id, var_coeffs] :
1285 SparseSubmatrixByRows(model_update.linear_constraint_matrix_updates(),
1286 /*start_row_id=*/0,
1287 /*end_row_id=*/first_new_cstr_id,
1288 /*start_col_id=*/*first_new_variable_id,
1289 /*end_col_id=*/std::nullopt)) {
1290 for (const auto& [var_id, value] : var_coeffs) {
1291 // See above why we use AddLinearConstraintCoef().
1292 RETURN_IF_ERROR(gscip_->AddLinearConstraintCoef(
1293 linear_constraints_.at(lin_con_id), variables_.at(var_id), value));
1294 }
1295 }
1296 }
1297
1298 // Add the new constraints and sets sub-matrix 3.
1300 AddLinearConstraints(model_update.new_linear_constraints(),
1301 model_update.linear_constraint_matrix_updates()));
1302
1303 // Quadratic constraint updates.
1304 for (const int64_t constraint_id :
1305 model_update.quadratic_constraint_updates().deleted_constraint_ids()) {
1306 RETURN_IF_ERROR(gscip_->DeleteConstraint(
1307 quadratic_constraints_.extract(constraint_id).mapped()));
1308 }
1309 RETURN_IF_ERROR(AddQuadraticConstraints(
1310 model_update.quadratic_constraint_updates().new_constraints()));
1311
1312 // Indicator constraint updates.
1313 for (const int64_t constraint_id :
1314 model_update.indicator_constraint_updates().deleted_constraint_ids()) {
1315 const auto gscip_data =
1316 indicator_constraints_.extract(constraint_id).mapped();
1317 if (!gscip_data.has_value()) {
1318 continue;
1319 }
1320 SCIP_CONS* const gscip_constraint = gscip_data->first;
1321 CHECK_NE(gscip_constraint, nullptr);
1322 RETURN_IF_ERROR(gscip_->DeleteConstraint(gscip_constraint));
1323 }
1324 RETURN_IF_ERROR(AddIndicatorConstraints(
1325 model_update.indicator_constraint_updates().new_constraints()));
1326
1327 // SOS1 constraint updates.
1328 for (const int64_t constraint_id :
1329 model_update.sos1_constraint_updates().deleted_constraint_ids()) {
1330 auto [gscip_constraint, slack_handler] =
1331 sos1_constraints_.extract(constraint_id).mapped();
1332 RETURN_IF_ERROR(gscip_->DeleteConstraint(gscip_constraint));
1333 RETURN_IF_ERROR(slack_handler.DeleteStructure(*gscip_));
1334 }
1335 RETURN_IF_ERROR(AddSos1Constraints(
1336 model_update.sos1_constraint_updates().new_constraints()));
1337
1338 // SOS2 constraint updates.
1339 for (const int64_t constraint_id :
1340 model_update.sos2_constraint_updates().deleted_constraint_ids()) {
1341 auto [gscip_constraint, slack_handler] =
1342 sos2_constraints_.extract(constraint_id).mapped();
1343 RETURN_IF_ERROR(gscip_->DeleteConstraint(gscip_constraint));
1344 RETURN_IF_ERROR(slack_handler.DeleteStructure(*gscip_));
1345 }
1346 RETURN_IF_ERROR(AddSos2Constraints(
1347 model_update.sos2_constraint_updates().new_constraints()));
1348
1349 return true;
1350}
1351
1352absl::StatusOr<ComputeInfeasibleSubsystemResultProto>
1355 const SolveInterrupter*) {
1356 return absl::UnimplementedError(
1357 "SCIP does not provide a method to compute an infeasible subsystem");
1358}
1359
1361
1362} // namespace math_opt
1363} // namespace operations_research
int64_t min
#define ASSIGN_OR_RETURN(lhs, rexpr)
#define RETURN_IF_ERROR(expr)
mapped_type & at(const key_arg< K > &key)
absl::StatusOr< SCIP_CONS * > AddCallbackConstraint(GScip *gscip, const std::string &constraint_name, const ConstraintData *constraint_data, const GScipConstraintOptions &options=DefaultGScipConstraintOptions())
absl::StatusOr< SolveResultProto > Solve(const SolveParametersProto &parameters, const ModelSolveParametersProto &model_parameters, MessageCallback message_cb, const CallbackRegistrationProto &callback_registration, Callback cb, const SolveInterrupter *interrupter) override
absl::StatusOr< bool > Update(const ModelUpdateProto &model_update) override
static absl::StatusOr< GScipParameters > MergeParameters(const SolveParametersProto &solve_parameters)
absl::StatusOr< ComputeInfeasibleSubsystemResultProto > ComputeInfeasibleSubsystem(const SolveParametersProto &parameters, MessageCallback message_cb, const SolveInterrupter *interrupter) override
static absl::StatusOr< std::unique_ptr< SolverInterface > > New(const ModelProto &model, const InitArgs &init_args)
std::function< void(const std::vector< std::string > &)> MessageCallback
std::function< absl::StatusOr< CallbackResultProto >( const CallbackDataProto &)> Callback
SatParameters parameters
const std::string name
A name for logging purposes.
int64_t value
IntVar * var
int64_t linear_constraint_id
double upper_bound
double lower_bound
absl::Span< const int64_t > variable_ids
absl::Span< const double > coefficients
GRBmodel * model
int index
std::optional< ModelSolveParameters::SolutionHint > hint
double solution
void InsertOrDie(Collection *const collection, const typename Collection::value_type &value)
Definition map_util.h:159
const MapUtilMappedT< Collection > & FindWithDefault(const Collection &collection, const KeyType &key, const MapUtilMappedT< Collection > &value)
Definition map_util.h:36
SparseDoubleVectorProto FillSparseDoubleVector(absl::Span< const int64_t > ids_in_order, const absl::flat_hash_map< int64_t, IndexType > &id_map, const glop::StrictITIVector< IndexType, glop::Fractional > &values, const SparseVectorFilterProto &filter)
absl::Status ModelIsSupported(const ModelProto &model, const SupportedProblemStructures &support_menu, const absl::string_view solver_name)
std::optional< int64_t > FirstLinearConstraintId(const LinearConstraintsProto &linear_constraints)
int NumMatrixNonzeros(const SparseDoubleMatrixProto &matrix)
int NumVariables(const VariablesProto &variables)
TerminationProto OptimalTerminationProto(const double finite_primal_objective, const double dual_objective, const absl::string_view detail)
TerminationProto LimitTerminationProto(const bool is_maximize, const LimitProto limit, const std::optional< double > optional_finite_primal_objective, const std::optional< double > optional_dual_objective, const absl::string_view detail)
TerminationProto InfeasibleOrUnboundedTerminationProto(bool is_maximize, const FeasibilityStatusProto dual_feasibility_status, const absl::string_view detail)
bool UpdateIsSupported(const ModelUpdateProto &update, const SupportedProblemStructures &support_menu)
absl::Status CheckRegisteredCallbackEvents(const CallbackRegistrationProto &registration, const absl::flat_hash_set< CallbackEventProto > &supported_events)
SparseVectorView< T > MakeView(absl::Span< const int64_t > ids, const Collection &values)
TerminationProto CutoffTerminationProto(bool is_maximize, const absl::string_view detail)
Calls NoSolutionFoundTerminationProto() with LIMIT_CUTOFF LIMIT.
int NumConstraints(const LinearConstraintsProto &linear_constraints)
GScipParameters::MetaParamValue ConvertMathOptEmphasis(EmphasisProto emphasis)
SparseSubmatrixRowsView SparseSubmatrixByRows(const SparseDoubleMatrixProto &matrix, const int64_t start_row_id, const std::optional< int64_t > end_row_id, const int64_t start_col_id, const std::optional< int64_t > end_col_id)
std::optional< int64_t > FirstVariableId(const VariablesProto &variables)
TerminationProto InfeasibleTerminationProto(bool is_maximize, const FeasibilityStatusProto dual_feasibility_status, const absl::string_view detail)
TerminationProto UnboundedTerminationProto(const bool is_maximize, const absl::string_view detail)
In SWIG mode, we don't want anything besides these top-level includes.
void GScipSetRandomSeed(GScipParameters *parameters, int random_seed)
void GScipSetCatchCtrlC(const bool catch_ctrl_c, GScipParameters *const parameters)
Sets the misc/catchctrlc property.
std::string ProtoEnumToString(ProtoEnumType enum_value)
Definition proto_utils.h:50
void GScipSetMaxNumThreads(int num_threads, GScipParameters *parameters)
CHECK fails if num_threads < 1.
void GScipSetTimeLimit(absl::Duration time_limit, GScipParameters *parameters)
std::function< void(GScipMessageType type, absl::string_view message)> GScipMessageHandler
STL namespace.
inline ::absl::StatusOr< absl::Duration > DecodeGoogleApiProto(const google::protobuf::Duration &proto)
Definition protoutil.h:42
inline ::absl::StatusOr< google::protobuf::Duration > EncodeGoogleApiProto(absl::Duration d)
Definition protoutil.h:27
StatusBuilder InvalidArgumentErrorBuilder()
bool Next()
int64_t weight
Definition pack.cc:510
#define MATH_OPT_REGISTER_SOLVER(solver_type, solver_factory)
int64_t start
const std::optional< Range > & range
Definition statistics.cc:37
std::string message
Definition trace.cc:397
double objective_value
The value objective_vector^T * (solution - center_point).