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