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