OR-Tools  9.3
gscip_solver.cc
Go to the documentation of this file.
1// Copyright 2010-2021 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 <cstdint>
18#include <functional>
19#include <limits>
20#include <memory>
21#include <optional>
22#include <string>
23#include <utility>
24#include <vector>
25
26#include "absl/container/flat_hash_map.h"
27#include "absl/container/flat_hash_set.h"
28#include "absl/memory/memory.h"
29#include "absl/status/status.h"
30#include "absl/status/statusor.h"
31#include "absl/strings/str_cat.h"
32#include "absl/strings/str_join.h"
33#include "absl/strings/string_view.h"
34#include "absl/time/clock.h"
35#include "absl/time/time.h"
36#include "absl/types/span.h"
43#include "ortools/gscip/gscip.h"
44#include "ortools/gscip/gscip.pb.h"
47#include "ortools/math_opt/callback.pb.h"
53#include "ortools/math_opt/model.pb.h"
54#include "ortools/math_opt/model_parameters.pb.h"
55#include "ortools/math_opt/model_update.pb.h"
56#include "ortools/math_opt/parameters.pb.h"
57#include "ortools/math_opt/result.pb.h"
58#include "ortools/math_opt/solution.pb.h"
61#include "ortools/math_opt/sparse_containers.pb.h"
64#include "scip/scip.h"
65#include "scip/type_cons.h"
66#include "scip/type_event.h"
67#include "scip/type_var.h"
68
69namespace operations_research {
70namespace math_opt {
71
72namespace {
73
74constexpr double kInf = std::numeric_limits<double>::infinity();
75
76int64_t SafeId(const VariablesProto& variables, int index) {
77 if (variables.ids().empty()) {
78 return index;
79 }
80 return variables.ids(index);
81}
82
83const std::string& EmptyString() {
84 static const std::string* const empty_string = new std::string;
85 return *empty_string;
86}
87
88const std::string& SafeName(const VariablesProto& variables, int index) {
89 if (variables.names().empty()) {
90 return EmptyString();
91 }
92 return variables.names(index);
93}
94
95int64_t SafeId(const LinearConstraintsProto& linear_constraints, int index) {
96 if (linear_constraints.ids().empty()) {
97 return index;
98 }
99 return linear_constraints.ids(index);
100}
101
102const std::string& SafeName(const LinearConstraintsProto& linear_constraints,
103 int index) {
104 if (linear_constraints.names().empty()) {
105 return EmptyString();
106 }
107 return linear_constraints.names(index);
108}
109
110absl::flat_hash_map<int64_t, double> SparseDoubleVectorAsMap(
111 const SparseDoubleVectorProto& vector) {
112 CHECK_EQ(vector.ids_size(), vector.values_size());
113 absl::flat_hash_map<int64_t, double> result;
114 result.reserve(vector.ids_size());
115 for (int i = 0; i < vector.ids_size(); ++i) {
116 result[vector.ids(i)] = vector.values(i);
117 }
118 return result;
119}
120
121// Viewing matrix as a list of (row, column, value) tuples stored in row major
122// order, does a linear scan from index scan_start to find the index of the
123// first entry with row >= row_id. Returns the size the tuple list if there is
124// no such entry.
125inline int FindRowStart(const SparseDoubleMatrixProto& matrix,
126 const int64_t row_id, const int scan_start) {
127 int result = scan_start;
128 while (result < matrix.row_ids_size() && matrix.row_ids(result) < row_id) {
129 ++result;
130 }
131 return result;
132}
133
134struct LinearConstraintView {
138 absl::string_view name;
139 absl::Span<const int64_t> variable_ids;
140 absl::Span<const double> coefficients;
141};
142
143// Iterates over the constraints from a LinearConstraints, outputting a
144// LinearConstraintView for each constraint. Requires a SparseDoubleMatrixProto
145// which may have data from additional constraints not in LinearConstraints.
146//
147// The running time to iterate through and read each element once is
148// O(Size(*linear_constraints) + Size(*linear_constraint_matrix)).
149class LinearConstraintIterator {
150 public:
151 LinearConstraintIterator(
152 const LinearConstraintsProto* linear_constraints,
153 const SparseDoubleMatrixProto* linear_constraint_matrix)
154 : linear_constraints_(ABSL_DIE_IF_NULL(linear_constraints)),
155 linear_constraint_matrix_(ABSL_DIE_IF_NULL(linear_constraint_matrix)) {
156 if (NumConstraints(*linear_constraints_) > 0) {
157 const int64_t first_constraint = SafeId(*linear_constraints_, 0);
158 matrix_start_ =
159 FindRowStart(*linear_constraint_matrix_, first_constraint, 0);
160 matrix_end_ = FindRowStart(*linear_constraint_matrix_,
161 first_constraint + 1, matrix_start_);
162 } else {
163 matrix_start_ = NumMatrixNonzeros(*linear_constraint_matrix_);
164 matrix_end_ = matrix_start_;
165 }
166 }
167
168 bool IsDone() const {
169 return current_con_ >= NumConstraints(*linear_constraints_);
170 }
171
172 // Call only if !IsDone(). Runs in O(1).
173 LinearConstraintView Current() const {
174 CHECK(!IsDone());
175 LinearConstraintView result;
176 result.lower_bound = linear_constraints_->lower_bounds(current_con_);
177 result.upper_bound = linear_constraints_->upper_bounds(current_con_);
178 result.name = SafeName(*linear_constraints_, current_con_);
179 result.linear_constraint_id = SafeId(*linear_constraints_, current_con_);
180
181 const auto vars_begin = linear_constraint_matrix_->column_ids().data();
182 result.variable_ids = absl::MakeConstSpan(vars_begin + matrix_start_,
183 vars_begin + matrix_end_);
184 const auto coefficients_begins =
185 linear_constraint_matrix_->coefficients().data();
186 result.coefficients = absl::MakeConstSpan(
187 coefficients_begins + matrix_start_, coefficients_begins + matrix_end_);
188 return result;
189 }
190
191 // Call only if !IsDone().
192 void Next() {
193 CHECK(!IsDone());
194 ++current_con_;
195 if (IsDone()) {
196 matrix_start_ = NumMatrixNonzeros(*linear_constraint_matrix_);
197 matrix_end_ = matrix_start_;
198 return;
199 }
200 const int64_t current_row_id = SafeId(*linear_constraints_, current_con_);
201 matrix_start_ =
202 FindRowStart(*linear_constraint_matrix_, current_row_id, matrix_end_);
203
204 matrix_end_ = FindRowStart(*linear_constraint_matrix_, current_row_id + 1,
205 matrix_start_);
206 }
207
208 private:
209 // NOT OWNED
210 const LinearConstraintsProto* const linear_constraints_;
211 // NOT OWNED
212 const SparseDoubleMatrixProto* const linear_constraint_matrix_;
213 // An index into linear_constraints_, the constraint currently being viewed,
214 // or Size(linear_constraints_) when IsDone().
215 int current_con_ = 0;
216
217 // Informal: the interval [matrix_start_, matrix_end_) gives the indices in
218 // linear_constraint_matrix_ for linear_constraints_[current_con_]
219 //
220 // Invariant: if !IsDone():
221 // * matrix_start_: the first index in linear_constraint_matrix_ with row id
222 // >= RowId(linear_constraints_[current_con_])
223 // * matrix_end_: the first index in linear_constraint_matrix_ with row id
224 // >= RowId(linear_constraints_[current_con_]) + 1
225 //
226 // Implementation note: matrix_start_ and matrix_end_ equal
227 // Size(linear_constraint_matrix_) when IsDone().
228 int matrix_start_ = 0;
229 int matrix_end_ = 0;
230};
231
232inline GScipVarType GScipVarTypeFromIsInteger(const bool is_integer) {
234}
235
236// Used to delay the evaluation of a costly computation until the first time it
237// is actually needed.
238//
239// The typical use is when we have two independent branches that need the same
240// data but we don't want to compute these data if we don't enter any of those
241// branches.
242//
243// Usage:
244// LazyInitialized<Xxx> xxx([&]() {
245// return Xxx(...);
246// });
247//
248// if (predicate_1) {
249// ...
250// f(xxx.GetOrCreate());
251// ...
252// }
253// if (predicate_2) {
254// ...
255// f(xxx.GetOrCreate());
256// ...
257// }
258template <typename T>
259class LazyInitialized {
260 public:
261 // Checks that the input initializer is not nullptr.
262 explicit LazyInitialized(std::function<T()> initializer)
263 : initializer_(ABSL_DIE_IF_NULL(initializer)) {}
264
265 // Returns the value returned by initializer(), calling it the first time.
266 const T& GetOrCreate() {
267 if (!value_) {
268 value_ = initializer_();
269 }
270 return *value_;
271 }
272
273 private:
274 const std::function<T()> initializer_;
275 std::optional<T> value_;
276};
277
278template <typename T>
279SparseDoubleVectorProto FillSparseDoubleVector(
280 const std::vector<int64_t>& ids_in_order,
281 const absl::flat_hash_map<int64_t, T>& id_map,
282 const absl::flat_hash_map<T, double>& value_map,
283 const SparseVectorFilterProto& filter) {
284 SparseVectorFilterPredicate predicate(filter);
285 SparseDoubleVectorProto result;
286 for (const int64_t variable_id : ids_in_order) {
287 const double value = value_map.at(id_map.at(variable_id));
288 if (predicate.AcceptsAndUpdate(variable_id, value)) {
289 result.add_ids(variable_id);
290 result.add_values(value);
291 }
292 }
293 return result;
294}
295
296} // namespace
297
298absl::Status GScipSolver::AddVariables(
299 const VariablesProto& variables,
300 const absl::flat_hash_map<int64_t, double>& linear_objective_coefficients) {
301 for (int i = 0; i < NumVariables(variables); ++i) {
302 const int64_t id = SafeId(variables, i);
303 // SCIP is failing with an assert in SCIPcreateVar() when input bounds are
304 // inverted. That said, it is not an issue if the bounds are created
305 // non-inverted and later changed. Thus here we use this hack to bypass the
306 // assertion in this corner case.
307 const bool inverted_bounds =
308 variables.lower_bounds(i) > variables.upper_bounds(i);
310 SCIP_VAR* const v,
311 gscip_->AddVariable(
312 variables.lower_bounds(i),
313 inverted_bounds ? variables.lower_bounds(i)
314 : variables.upper_bounds(i),
315 gtl::FindWithDefault(linear_objective_coefficients, id),
316 GScipVarTypeFromIsInteger(variables.integers(i)),
317 SafeName(variables, i)));
318 if (inverted_bounds) {
319 RETURN_IF_ERROR(gscip_->SetUb(v, variables.upper_bounds(i)));
320 }
321 gtl::InsertOrDie(&variables_, id, v);
322 }
323 return absl::OkStatus();
324}
325
326absl::Status GScipSolver::UpdateVariables(
327 const VariableUpdatesProto& variable_updates) {
328 for (const auto [id, lb] : MakeView(variable_updates.lower_bounds())) {
329 RETURN_IF_ERROR(gscip_->SetLb(variables_.at(id), lb));
330 }
331 for (const auto [id, ub] : MakeView(variable_updates.upper_bounds())) {
332 RETURN_IF_ERROR(gscip_->SetUb(variables_.at(id), ub));
333 }
334 for (const auto [id, is_integer] : MakeView(variable_updates.integers())) {
335 RETURN_IF_ERROR(gscip_->SetVarType(variables_.at(id),
336 GScipVarTypeFromIsInteger(is_integer)));
337 }
338 return absl::OkStatus();
339}
340
341absl::Status GScipSolver::AddLinearConstraints(
342 const LinearConstraintsProto& linear_constraints,
343 const SparseDoubleMatrixProto& linear_constraint_matrix) {
344 for (LinearConstraintIterator lin_con_it(&linear_constraints,
345 &linear_constraint_matrix);
346 !lin_con_it.IsDone(); lin_con_it.Next()) {
347 const LinearConstraintView current = lin_con_it.Current();
348
349 GScipLinearRange range;
350 range.lower_bound = current.lower_bound;
351 range.upper_bound = current.upper_bound;
352 range.coefficients = std::vector<double>(current.coefficients.begin(),
353 current.coefficients.end());
354 range.variables.reserve(current.variable_ids.size());
355 for (const int64_t var_id : current.variable_ids) {
356 range.variables.push_back(variables_.at(var_id));
357 }
359 SCIP_CONS* const scip_con,
360 gscip_->AddLinearConstraint(range, std::string(current.name)));
361 gtl::InsertOrDie(&linear_constraints_, current.linear_constraint_id,
362 scip_con);
363 }
364 return absl::OkStatus();
365}
366
367absl::Status GScipSolver::UpdateLinearConstraints(
368 const LinearConstraintUpdatesProto linear_constraint_updates,
369 const SparseDoubleMatrixProto& linear_constraint_matrix,
370 const std::optional<int64_t> first_new_var_id,
371 const std::optional<int64_t> first_new_cstr_id) {
372 for (const auto [id, lb] :
373 MakeView(linear_constraint_updates.lower_bounds())) {
375 gscip_->SetLinearConstraintLb(linear_constraints_.at(id), lb));
376 }
377 for (const auto [id, ub] :
378 MakeView(linear_constraint_updates.upper_bounds())) {
380 gscip_->SetLinearConstraintUb(linear_constraints_.at(id), ub));
381 }
382 for (const auto& [lin_con_id, var_coeffs] : SparseSubmatrixByRows(
383 linear_constraint_matrix, /*start_row_id=*/0,
384 /*end_row_id=*/first_new_cstr_id, /*start_col_id=*/0,
385 /*end_col_id=*/first_new_var_id)) {
386 for (const auto& [var_id, value] : var_coeffs) {
387 RETURN_IF_ERROR(gscip_->SetLinearConstraintCoef(
388 linear_constraints_.at(lin_con_id), variables_.at(var_id), value));
389 }
390 }
391 return absl::OkStatus();
392}
393
394GScipParameters::MetaParamValue ConvertMathOptEmphasis(EmphasisProto emphasis) {
395 switch (emphasis) {
396 case EMPHASIS_OFF:
397 return GScipParameters::OFF;
398 case EMPHASIS_LOW:
399 return GScipParameters::FAST;
400 case EMPHASIS_MEDIUM:
401 case EMPHASIS_UNSPECIFIED:
402 return GScipParameters::DEFAULT_META_PARAM_VALUE;
403 case EMPHASIS_HIGH:
404 case EMPHASIS_VERY_HIGH:
405 return GScipParameters::AGGRESSIVE;
406 default:
407 LOG(FATAL) << "Unsupported MathOpt Emphasis value: "
408 << ProtoEnumToString(emphasis)
409 << " unknown, error setting gSCIP parameters";
410 }
411}
412
413absl::StatusOr<GScipParameters> GScipSolver::MergeParameters(
414 const SolveParametersProto& solve_parameters) {
415 // First build the result by translating common parameters to a
416 // GScipParameters, and then merging with user provided gscip_parameters.
417 // This results in user provided solver specific parameters overwriting
418 // common parameters should there be any conflict.
419 GScipParameters result;
420 std::vector<std::string> warnings;
421
422 // By default SCIP catches Ctrl-C but we don't want this behavior when the
423 // users uses SCIP through MathOpt.
424 GScipSetCatchCtrlC(false, &result);
425
426 if (solve_parameters.has_time_limit()) {
428 util_time::DecodeGoogleApiProto(solve_parameters.time_limit()).value(),
429 &result);
430 }
431
432 if (solve_parameters.has_threads()) {
433 GScipSetMaxNumThreads(solve_parameters.threads(), &result);
434 }
435
436 if (solve_parameters.has_relative_gap_tolerance()) {
437 (*result.mutable_real_params())["limits/gap"] =
438 solve_parameters.relative_gap_tolerance();
439 }
440
441 if (solve_parameters.has_absolute_gap_tolerance()) {
442 (*result.mutable_real_params())["limits/absgap"] =
443 solve_parameters.absolute_gap_tolerance();
444 }
445 if (solve_parameters.has_node_limit()) {
446 (*result.mutable_long_params())["limits/totalnodes"] =
447 solve_parameters.node_limit();
448 }
449
450 if (solve_parameters.has_objective_limit()) {
451 warnings.push_back("parameter objective_limit not supported for gSCIP.");
452 }
453 if (solve_parameters.has_best_bound_limit()) {
454 warnings.push_back("parameter best_bound_limit not supported for gSCIP.");
455 }
456
457 if (solve_parameters.has_cutoff_limit()) {
458 result.set_objective_limit(solve_parameters.cutoff_limit());
459 }
460
461 if (solve_parameters.has_solution_limit()) {
462 (*result.mutable_int_params())["limits/solutions"] =
463 solve_parameters.solution_limit();
464 }
465
466 // GScip has also GScipSetOutputEnabled() but this changes the log
467 // level. Setting `silence_output` sets the `quiet` field on the default
468 // message handler of SCIP which removes the output. Here it is important to
469 // use this rather than changing the log level so that if the user registers
470 // for CALLBACK_EVENT_MESSAGE they do get some messages even when
471 // `enable_output` is false.
472 result.set_silence_output(!solve_parameters.enable_output());
473
474 if (solve_parameters.has_random_seed()) {
475 GScipSetRandomSeed(&result, solve_parameters.random_seed());
476 }
477
478 if (solve_parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) {
479 char alg;
480 switch (solve_parameters.lp_algorithm()) {
481 case LP_ALGORITHM_PRIMAL_SIMPLEX:
482 alg = 'p';
483 break;
484 case LP_ALGORITHM_DUAL_SIMPLEX:
485 alg = 'd';
486 break;
487 case LP_ALGORITHM_BARRIER:
488 alg = 'c';
489 break;
490 default:
491 LOG(FATAL) << "LPAlgorithm: "
492 << ProtoEnumToString(solve_parameters.lp_algorithm())
493 << " unknown, error setting gSCIP parameters";
494 }
495 (*result.mutable_char_params())["lp/initalgorithm"] = alg;
496 }
497
498 if (solve_parameters.cuts() != EMPHASIS_UNSPECIFIED) {
499 result.set_separating(ConvertMathOptEmphasis(solve_parameters.cuts()));
500 }
501 if (solve_parameters.heuristics() != EMPHASIS_UNSPECIFIED) {
502 result.set_heuristics(
503 ConvertMathOptEmphasis(solve_parameters.heuristics()));
504 }
505 if (solve_parameters.presolve() != EMPHASIS_UNSPECIFIED) {
506 result.set_presolve(ConvertMathOptEmphasis(solve_parameters.presolve()));
507 }
508 if (solve_parameters.scaling() != EMPHASIS_UNSPECIFIED) {
509 int scaling_value;
510 switch (solve_parameters.scaling()) {
511 case EMPHASIS_OFF:
512 scaling_value = 0;
513 break;
514 case EMPHASIS_LOW:
515 case EMPHASIS_MEDIUM:
516 scaling_value = 1;
517 break;
518 case EMPHASIS_HIGH:
519 case EMPHASIS_VERY_HIGH:
520 scaling_value = 2;
521 break;
522 default:
523 LOG(FATAL) << "Scaling emphasis: "
524 << ProtoEnumToString(solve_parameters.scaling())
525 << " unknown, error setting gSCIP parameters";
526 }
527 (*result.mutable_int_params())["lp/scaling"] = scaling_value;
528 }
529
530 result.MergeFrom(solve_parameters.gscip());
531
532 if (!warnings.empty()) {
533 return absl::InvalidArgumentError(absl::StrJoin(warnings, "; "));
534 }
535 return result;
536}
537
538namespace {
539
540std::string JoinDetails(const std::string& gscip_detail,
541 const std::string& math_opt_detail) {
542 if (gscip_detail.empty()) {
543 return math_opt_detail;
544 }
545 if (math_opt_detail.empty()) {
546 return gscip_detail;
547 }
548 return absl::StrCat(gscip_detail, "; ", math_opt_detail);
549}
550
551ProblemStatusProto GetProblemStatusProto(const GScipOutput::Status gscip_status,
552 const bool has_feasible_solution,
553 const bool has_finite_dual_bound,
554 const bool was_cutoff) {
555 ProblemStatusProto problem_status;
556 if (has_feasible_solution) {
557 problem_status.set_primal_status(FEASIBILITY_STATUS_FEASIBLE);
558 } else {
559 problem_status.set_primal_status(FEASIBILITY_STATUS_UNDETERMINED);
560 }
561 problem_status.set_dual_status(FEASIBILITY_STATUS_UNDETERMINED);
562
563 switch (gscip_status) {
564 case GScipOutput::OPTIMAL:
565 problem_status.set_dual_status(FEASIBILITY_STATUS_FEASIBLE);
566 break;
567 case GScipOutput::INFEASIBLE:
568 if (!was_cutoff) {
569 problem_status.set_primal_status(FEASIBILITY_STATUS_INFEASIBLE);
570 }
571 break;
572 case GScipOutput::UNBOUNDED:
573 problem_status.set_dual_status(FEASIBILITY_STATUS_INFEASIBLE);
574 break;
575 case GScipOutput::INF_OR_UNBD:
576 problem_status.set_primal_or_dual_infeasible(true);
577 break;
578 default:
579 break;
580 }
581 if (has_finite_dual_bound) {
582 problem_status.set_dual_status(FEASIBILITY_STATUS_FEASIBLE);
583 }
584 return problem_status;
585}
586
587absl::StatusOr<TerminationProto> ConvertTerminationReason(
588 const GScipOutput::Status gscip_status,
589 const std::string& gscip_status_detail, const bool has_feasible_solution,
590 const bool had_cutoff) {
591 switch (gscip_status) {
592 case GScipOutput::USER_INTERRUPT:
593 return TerminateForLimit(
594 LIMIT_INTERRUPTED, /*feasible=*/has_feasible_solution,
595 JoinDetails(gscip_status_detail,
596 "underlying gSCIP status: USER_INTERRUPT"));
597 case GScipOutput::NODE_LIMIT:
598 return TerminateForLimit(
599 LIMIT_NODE, /*feasible=*/has_feasible_solution,
600 JoinDetails(gscip_status_detail,
601 "underlying gSCIP status: NODE_LIMIT"));
602 case GScipOutput::TOTAL_NODE_LIMIT:
603 return TerminateForLimit(
604 LIMIT_NODE, /*feasible=*/has_feasible_solution,
605 JoinDetails(gscip_status_detail,
606 "underlying gSCIP status: TOTAL_NODE_LIMIT"));
607 case GScipOutput::STALL_NODE_LIMIT:
608 return TerminateForLimit(LIMIT_SLOW_PROGRESS,
609 /*feasible=*/has_feasible_solution,
610 gscip_status_detail);
611 case GScipOutput::TIME_LIMIT:
612 return TerminateForLimit(LIMIT_TIME, /*feasible=*/has_feasible_solution,
613 gscip_status_detail);
614 case GScipOutput::MEM_LIMIT:
615 return TerminateForLimit(LIMIT_MEMORY, /*feasible=*/has_feasible_solution,
616 gscip_status_detail);
617 case GScipOutput::SOL_LIMIT:
618 return TerminateForLimit(
619 LIMIT_SOLUTION, /*feasible=*/has_feasible_solution,
620 JoinDetails(gscip_status_detail,
621 "underlying gSCIP status: SOL_LIMIT"));
622 case GScipOutput::BEST_SOL_LIMIT:
623 return TerminateForLimit(
624 LIMIT_SOLUTION, /*feasible=*/has_feasible_solution,
625 JoinDetails(gscip_status_detail,
626 "underlying gSCIP status: BEST_SOL_LIMIT"));
627 case GScipOutput::RESTART_LIMIT:
628 return TerminateForLimit(
629 LIMIT_OTHER, /*feasible=*/has_feasible_solution,
630 JoinDetails(gscip_status_detail,
631 "underlying gSCIP status: RESTART_LIMIT"));
632 case GScipOutput::OPTIMAL:
633 return TerminateForReason(
634 TERMINATION_REASON_OPTIMAL,
635 JoinDetails(gscip_status_detail, "underlying gSCIP status: OPTIMAL"));
636 case GScipOutput::GAP_LIMIT:
637 return TerminateForReason(
638 TERMINATION_REASON_OPTIMAL,
639 JoinDetails(gscip_status_detail,
640 "underlying gSCIP status: GAP_LIMIT"));
641 case GScipOutput::INFEASIBLE:
642 if (had_cutoff) {
643 return TerminateForLimit(LIMIT_CUTOFF,
644 /*feasible=*/false, gscip_status_detail);
645 } else {
646 return TerminateForReason(TERMINATION_REASON_INFEASIBLE,
647 gscip_status_detail);
648 }
649 case GScipOutput::UNBOUNDED: {
650 if (has_feasible_solution) {
651 return TerminateForReason(
652 TERMINATION_REASON_UNBOUNDED,
653 JoinDetails(gscip_status_detail,
654 "underlying gSCIP status was UNBOUNDED, both primal "
655 "ray and feasible solution are present"));
656 } else {
657 return TerminateForReason(
658 TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED,
659 JoinDetails(
660 gscip_status_detail,
661 "underlying gSCIP status was UNBOUNDED, but only primal ray "
662 "was given, no feasible solution was found"));
663 }
664 }
665
666 case GScipOutput::INF_OR_UNBD:
667 return TerminateForReason(
668 TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED,
669 JoinDetails(gscip_status_detail,
670 "underlying gSCIP status: INF_OR_UNBD"));
671
672 case GScipOutput::TERMINATE:
673 return TerminateForLimit(
674 LIMIT_INTERRUPTED, /*feasible=*/has_feasible_solution,
675 JoinDetails(gscip_status_detail,
676 "underlying gSCIP status: TERMINATE"));
677 case GScipOutput::INVALID_SOLVER_PARAMETERS:
678 return absl::InvalidArgumentError(gscip_status_detail);
679 case GScipOutput::UNKNOWN:
680 return absl::InternalError(JoinDetails(
681 gscip_status_detail, "Unexpected GScipOutput.status: UNKNOWN"));
682 default:
683 return absl::InternalError(JoinDetails(
684 gscip_status_detail, absl::StrCat("Missing GScipOutput.status case: ",
685 ProtoEnumToString(gscip_status))));
686 }
687}
688
689} // namespace
690
691absl::StatusOr<SolveResultProto> GScipSolver::CreateSolveResultProto(
692 GScipResult gscip_result, const ModelSolveParametersProto& model_parameters,
693 const std::optional<double> cutoff) {
694 SolveResultProto solve_result;
695 const bool is_maximize = gscip_->ObjectiveIsMaximize();
696 // When an objective limit is set, SCIP returns the solutions worse than the
697 // limit, we need to filter these out manually.
698 const auto meets_cutoff = [cutoff, is_maximize](const double obj_value) {
699 if (!cutoff.has_value()) {
700 return true;
701 }
702 if (is_maximize) {
703 return obj_value >= *cutoff;
704 } else {
705 return obj_value <= *cutoff;
706 }
707 };
708
709 LazyInitialized<std::vector<int64_t>> sorted_variables([&]() {
710 std::vector<int64_t> sorted;
711 sorted.reserve(variables_.size());
712 for (const auto& entry : variables_) {
713 sorted.emplace_back(entry.first);
714 }
715 std::sort(sorted.begin(), sorted.end());
716 return sorted;
717 });
718 CHECK_EQ(gscip_result.solutions.size(), gscip_result.objective_values.size());
719 for (int i = 0; i < gscip_result.solutions.size(); ++i) {
720 // GScip ensures the solutions are returned best objective first.
721 if (!meets_cutoff(gscip_result.objective_values[i])) {
722 break;
723 }
724 SolutionProto* const solution = solve_result.add_solutions();
725 PrimalSolutionProto* const primal_solution =
726 solution->mutable_primal_solution();
727 primal_solution->set_objective_value(gscip_result.objective_values[i]);
728 primal_solution->set_feasibility_status(SOLUTION_STATUS_FEASIBLE);
729 *primal_solution->mutable_variable_values() = FillSparseDoubleVector(
730 sorted_variables.GetOrCreate(), variables_, gscip_result.solutions[i],
731 model_parameters.variable_values_filter());
732 }
733 if (!gscip_result.primal_ray.empty()) {
734 *solve_result.add_primal_rays()->mutable_variable_values() =
735 FillSparseDoubleVector(sorted_variables.GetOrCreate(), variables_,
736 gscip_result.primal_ray,
737 model_parameters.variable_values_filter());
738 }
739 const bool has_feasible_solution = solve_result.solutions_size() > 0;
741 *solve_result.mutable_termination(),
742 ConvertTerminationReason(gscip_result.gscip_output.status(),
743 gscip_result.gscip_output.status_detail(),
744 /*has_feasible_solution=*/has_feasible_solution,
745 /*had_cutoff=*/cutoff.has_value()));
746 *solve_result.mutable_solve_stats()->mutable_problem_status() =
747 GetProblemStatusProto(
748 gscip_result.gscip_output.status(),
749 /*has_feasible_solution=*/has_feasible_solution,
750 /*has_finite_dual_bound=*/
751 std::isfinite(gscip_result.gscip_output.stats().best_bound()),
752 /*was_cutoff=*/solve_result.termination().limit() == LIMIT_CUTOFF);
753 SolveStatsProto* const common_stats = solve_result.mutable_solve_stats();
754 const GScipSolvingStats& gscip_stats = gscip_result.gscip_output.stats();
755 common_stats->set_best_dual_bound(gscip_stats.best_bound());
756 // If we found no solutions meeting the cutoff, we have no primal bound.
757 if (has_feasible_solution) {
758 common_stats->set_best_primal_bound(gscip_stats.best_objective());
759 } else {
760 common_stats->set_best_primal_bound(is_maximize ? -kInf : kInf);
761 }
762
763 common_stats->set_node_count(gscip_stats.node_count());
764 common_stats->set_simplex_iterations(gscip_stats.primal_simplex_iterations() +
765 gscip_stats.dual_simplex_iterations());
766 common_stats->set_barrier_iterations(gscip_stats.total_lp_iterations() -
767 common_stats->simplex_iterations());
768 *solve_result.mutable_gscip_output() = std::move(gscip_result.gscip_output);
769 return solve_result;
770}
771
772GScipSolver::GScipSolver(std::unique_ptr<GScip> gscip)
773 : gscip_(std::move(ABSL_DIE_IF_NULL(gscip))) {
774 interrupt_event_handler_.Register(gscip_.get());
775}
776
777absl::StatusOr<std::unique_ptr<SolverInterface>> GScipSolver::New(
778 const ModelProto& model, const InitArgs& init_args) {
779 ASSIGN_OR_RETURN(std::unique_ptr<GScip> gscip, GScip::Create(model.name()));
780 RETURN_IF_ERROR(gscip->SetMaximize(model.objective().maximize()));
781 RETURN_IF_ERROR(gscip->SetObjectiveOffset(model.objective().offset()));
782 // TODO(b/204083726): Remove this check if QP support is added
783 if (!model.objective().quadratic_coefficients().row_ids().empty()) {
784 return absl::InvalidArgumentError(
785 "MathOpt does not currently support SCIP models with quadratic "
786 "objectives");
787 }
788 // Can't be const because it had to be moved into the StatusOr and be
789 // convereted to std::unique_ptr<SolverInterface>.
790 auto solver = absl::WrapUnique(new GScipSolver(std::move(gscip)));
791
792 RETURN_IF_ERROR(solver->AddVariables(
793 model.variables(),
794 SparseDoubleVectorAsMap(model.objective().linear_coefficients())));
795 RETURN_IF_ERROR(solver->AddLinearConstraints(
796 model.linear_constraints(), model.linear_constraint_matrix()));
797
798 return solver;
799}
800
801absl::StatusOr<SolveResultProto> GScipSolver::Solve(
802 const SolveParametersProto& parameters,
803 const ModelSolveParametersProto& model_parameters,
804 const MessageCallback message_cb,
805 const CallbackRegistrationProto& callback_registration, const Callback cb,
806 SolveInterrupter* const interrupter) {
807 const absl::Time start = absl::Now();
808
810 /*supported_events=*/{}));
811
812 const std::unique_ptr<GScipSolverCallbackHandler> callback_handler =
813 GScipSolverCallbackHandler::RegisterIfNeeded(callback_registration, cb,
814 start, gscip_->scip());
815
816 std::unique_ptr<GScipSolverMessageCallbackHandler> message_cb_handler;
817 if (message_cb != nullptr) {
818 message_cb_handler =
819 std::make_unique<GScipSolverMessageCallbackHandler>(message_cb);
820 }
821
822 ASSIGN_OR_RETURN(auto gscip_parameters, MergeParameters(parameters));
823
824 for (const SolutionHintProto& hint : model_parameters.solution_hints()) {
825 absl::flat_hash_map<SCIP_VAR*, double> partial_solution;
826 for (const auto [id, val] : MakeView(hint.variable_values())) {
827 partial_solution.insert({variables_.at(id), val});
828 }
829 RETURN_IF_ERROR(gscip_->SuggestHint(partial_solution).status());
830 }
831 for (const auto [id, value] :
832 MakeView(model_parameters.branching_priorities())) {
833 RETURN_IF_ERROR(gscip_->SetBranchingPriority(variables_.at(id), value));
834 }
835
836 // Before calling solve, set the interrupter on the event handler that calls
837 // SCIPinterruptSolve().
838 if (interrupter != nullptr) {
839 interrupt_event_handler_.interrupter = interrupter;
840 }
841 const auto interrupter_cleanup = absl::MakeCleanup(
842 [&]() { interrupt_event_handler_.interrupter = nullptr; });
843
844 // SCIP returns "infeasible" when the model contain invalid bounds.
845 RETURN_IF_ERROR(ListInvertedBounds().ToStatus());
846
847 ASSIGN_OR_RETURN(GScipResult gscip_result,
848 gscip_->Solve(gscip_parameters,
849 /*legacy_params=*/"",
850 message_cb_handler != nullptr
851 ? message_cb_handler->MessageHandler()
852 : nullptr));
853
854 // Flushes the last unfinished message as early as possible.
855 message_cb_handler.reset();
856
857 if (callback_handler) {
858 RETURN_IF_ERROR(callback_handler->Flush());
859 }
860
862 SolveResultProto result,
863 CreateSolveResultProto(std::move(gscip_result), model_parameters,
864 parameters.has_cutoff_limit()
865 ? std::make_optional(parameters.cutoff_limit())
866 : std::nullopt));
868 absl::Now() - start, result.mutable_solve_stats()->mutable_solve_time()));
869 return result;
870}
871
872absl::flat_hash_set<SCIP_VAR*> GScipSolver::LookupAllVariables(
873 absl::Span<const int64_t> variable_ids) {
874 absl::flat_hash_set<SCIP_VAR*> result;
875 result.reserve(variable_ids.size());
876 for (const int64_t var_id : variable_ids) {
877 result.insert(variables_.at(var_id));
878 }
879 return result;
880}
881
882// Returns the ids of variables and linear constraints with inverted bounds.
883InvertedBounds GScipSolver::ListInvertedBounds() const {
884 // Get the SCIP variables/constraints with inverted bounds.
885 InvertedBounds inverted_bounds;
886 for (const auto& [id, var] : variables_) {
887 if (gscip_->Lb(var) > gscip_->Ub(var)) {
888 inverted_bounds.variables.push_back(id);
889 }
890 }
891 for (const auto& [id, cstr] : linear_constraints_) {
892 if (gscip_->LinearConstraintLb(cstr) > gscip_->LinearConstraintUb(cstr)) {
893 inverted_bounds.linear_constraints.push_back(id);
894 }
895 }
896
897 // Above code have inserted ids in non-stable order.
898 std::sort(inverted_bounds.variables.begin(), inverted_bounds.variables.end());
899 std::sort(inverted_bounds.linear_constraints.begin(),
900 inverted_bounds.linear_constraints.end());
901 return inverted_bounds;
902}
903
904bool GScipSolver::CanUpdate(const ModelUpdateProto& model_update) {
905 return gscip_
906 ->CanSafeBulkDelete(
907 LookupAllVariables(model_update.deleted_variable_ids()))
908 .ok() &&
909 model_update.objective_updates()
910 .quadratic_coefficients()
911 .row_ids()
912 .empty();
913}
914
915absl::Status GScipSolver::Update(const ModelUpdateProto& model_update) {
916 for (const int64_t constraint_id :
917 model_update.deleted_linear_constraint_ids()) {
918 SCIP_CONS* const scip_cons = linear_constraints_.at(constraint_id);
919 linear_constraints_.erase(constraint_id);
920 RETURN_IF_ERROR(gscip_->DeleteConstraint(scip_cons));
921 }
922 {
923 const absl::flat_hash_set<SCIP_VAR*> vars_to_delete =
924 LookupAllVariables(model_update.deleted_variable_ids());
925 for (const int64_t deleted_variable_id :
926 model_update.deleted_variable_ids()) {
927 variables_.erase(deleted_variable_id);
928 }
929 RETURN_IF_ERROR(gscip_->SafeBulkDelete(vars_to_delete));
930 }
931
932 const std::optional<int64_t> first_new_var_id =
933 FirstVariableId(model_update.new_variables());
934 const std::optional<int64_t> first_new_cstr_id =
935 FirstLinearConstraintId(model_update.new_linear_constraints());
936
937 if (model_update.objective_updates().has_direction_update()) {
938 RETURN_IF_ERROR(gscip_->SetMaximize(
939 model_update.objective_updates().direction_update()));
940 }
941 if (model_update.objective_updates().has_offset_update()) {
942 RETURN_IF_ERROR(gscip_->SetObjectiveOffset(
943 model_update.objective_updates().offset_update()));
944 }
945 RETURN_IF_ERROR(UpdateVariables(model_update.variable_updates()));
946 const absl::flat_hash_map<int64_t, double> linear_objective_updates =
947 SparseDoubleVectorAsMap(
948 model_update.objective_updates().linear_coefficients());
949 for (const auto& obj_pair : linear_objective_updates) {
950 // New variables' coefficient is set when the variables are added below.
951 if (!first_new_var_id.has_value() || obj_pair.first < *first_new_var_id) {
953 gscip_->SetObjCoef(variables_.at(obj_pair.first), obj_pair.second));
954 }
955 }
956
957 // Here the model_update.linear_constraint_matrix_updates is split into three
958 // sub-matrix:
959 //
960 // existing new
961 // columns columns
962 // / | \
963 // existing | 1 | 2 |
964 // rows | | |
965 // |---------+---------|
966 // new | |
967 // rows | 3 |
968 // \ /
969 //
970 // The coefficients of sub-matrix 1 are set by UpdateLinearConstraints(), the
971 // ones of sub-matrix 2 by AddVariables() and the ones of the sub-matrix 3 by
972 // AddLinearConstraints(). The rationale here is that SCIPchgCoefLinear() has
973 // a complexity of O(non_zeros). Thus it is inefficient and can lead to O(n^2)
974 // behaviors if it was used for new rows or for new columns. For new rows it
975 // is more efficient to pass all the variables coefficients at once when
976 // building the constraints. For new columns and existing rows, since we can
977 // assume there is no existing coefficient, we can use SCIPaddCoefLinear()
978 // which is O(1). This leads to only use SCIPchgCoefLinear() for changing the
979 // coefficients of existing rows and columns.
980 //
981 // TODO(b/215722113): maybe we could use SCIPaddCoefLinear() for sub-matrix 1.
982
983 // Add new variables.
985 AddVariables(model_update.new_variables(), linear_objective_updates));
986
987 // Update linear constraints properties and sub-matrix 1.
989 UpdateLinearConstraints(model_update.linear_constraint_updates(),
990 model_update.linear_constraint_matrix_updates(),
991 /*first_new_var_id=*/first_new_var_id,
992 /*first_new_cstr_id=*/first_new_cstr_id));
993
994 // Update the sub-matrix 2.
995 const std::optional first_new_variable_id =
996 FirstVariableId(model_update.new_variables());
997 if (first_new_variable_id.has_value()) {
998 for (const auto& [lin_con_id, var_coeffs] :
999 SparseSubmatrixByRows(model_update.linear_constraint_matrix_updates(),
1000 /*start_row_id=*/0,
1001 /*end_row_id=*/first_new_cstr_id,
1002 /*start_col_id=*/*first_new_variable_id,
1003 /*end_col_id=*/std::nullopt)) {
1004 for (const auto& [var_id, value] : var_coeffs) {
1005 // See above why we use AddLinearConstraintCoef().
1006 RETURN_IF_ERROR(gscip_->AddLinearConstraintCoef(
1007 linear_constraints_.at(lin_con_id), variables_.at(var_id), value));
1008 }
1009 }
1010 }
1011
1012 // Add the new constraints and sets sub-matrix 3.
1014 AddLinearConstraints(model_update.new_linear_constraints(),
1015 model_update.linear_constraint_matrix_updates()));
1016 return absl::OkStatus();
1017}
1018
1019GScipSolver::InterruptEventHandler::InterruptEventHandler()
1021 {.name = "interrupt event handler",
1022 .description = "Event handler to call SCIPinterruptSolve() when a "
1023 "user SolveInterrupter is triggered."}) {}
1024
1025SCIP_RETCODE GScipSolver::InterruptEventHandler::Init(GScip* const gscip) {
1026 // We don't register any event if we don't have an interrupter.
1027 if (interrupter == nullptr) {
1028 return SCIP_OKAY;
1029 }
1030
1031 // TODO(b/193537362): see if these events are enough or if we should have more
1032 // of these.
1033 CatchEvent(SCIP_EVENTTYPE_PRESOLVEROUND);
1034 CatchEvent(SCIP_EVENTTYPE_NODEEVENT);
1035
1036 return TryCallInterruptIfNeeded(gscip);
1037}
1038
1039SCIP_RETCODE GScipSolver::InterruptEventHandler::Execute(
1040 const GScipEventHandlerContext context) {
1041 return TryCallInterruptIfNeeded(context.gscip());
1042}
1043
1044SCIP_RETCODE GScipSolver::InterruptEventHandler::TryCallInterruptIfNeeded(
1045 GScip* const gscip) {
1046 if (interrupter == nullptr) {
1047 LOG(WARNING) << "TryCallInterruptIfNeeded() called after interrupter has "
1048 "been reset!";
1049 return SCIP_OKAY;
1050 }
1051
1052 if (!interrupter->IsInterrupted()) {
1053 return SCIP_OKAY;
1054 }
1055
1056 const SCIP_STAGE stage = SCIPgetStage(gscip->scip());
1057 switch (stage) {
1058 case SCIP_STAGE_INIT:
1059 case SCIP_STAGE_FREE:
1060 // This should never happen anyway; but if this happens, we may want to
1061 // know about it in unit tests.
1062 LOG(DFATAL) << "TryCallInterruptIfNeeded() called in stage "
1063 << (stage == SCIP_STAGE_INIT ? "INIT" : "FREE");
1064 return SCIP_OKAY;
1065 case SCIP_STAGE_INITSOLVE:
1066 LOG(WARNING) << "TryCallInterruptIfNeeded() called in INITSOLVE stage; "
1067 "we can't call SCIPinterruptSolve() in this stage.";
1068 return SCIP_OKAY;
1069 default:
1070 return SCIPinterruptSolve(gscip->scip());
1071 }
1072}
1073
1075
1076} // namespace math_opt
1077} // namespace operations_research
#define CHECK(condition)
Definition: base/logging.h:495
#define CHECK_EQ(val1, val2)
Definition: base/logging.h:703
#define CHECK_OK(x)
Definition: base/logging.h:44
#define LOG(severity)
Definition: base/logging.h:420
#define ABSL_DIE_IF_NULL
Definition: base/logging.h:43
#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:273
static std::unique_ptr< GScipSolverCallbackHandler > RegisterIfNeeded(const CallbackRegistrationProto &callback_registration, SolverInterface::Callback callback, absl::Time solve_start, SCIP *scip)
bool CanUpdate(const ModelUpdateProto &model_update) override
absl::Status 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
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
const int WARNING
Definition: log_severity.h:31
const int FATAL
Definition: log_severity.h:32
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
std::optional< int64_t > FirstLinearConstraintId(const LinearConstraintsProto &linear_constraints)
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)
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:406
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)
SparseVectorView< T > MakeView(absl::Span< const int64_t > ids, const Collection &values)
int NumConstraints(const LinearConstraintsProto &linear_constraints)
TerminationProto TerminateForReason(const TerminationReasonProto reason, const absl::string_view detail)
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)
STL namespace.
inline ::absl::StatusOr< absl::Duration > DecodeGoogleApiProto(const google::protobuf::Duration &proto)
Definition: protoutil.h:42
inline ::absl::StatusOr< google::protobuf::Duration > EncodeGoogleApiProto(absl::Duration d)
Definition: protoutil.h:27
int64_t start