OR-Tools  9.2
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
29#include "absl/container/flat_hash_map.h"
30#include "absl/container/flat_hash_set.h"
31#include "absl/memory/memory.h"
32#include "absl/status/status.h"
33#include "absl/status/statusor.h"
34#include "absl/strings/str_cat.h"
35#include "absl/strings/str_join.h"
36#include "absl/strings/string_view.h"
37#include "absl/time/clock.h"
38#include "absl/time/time.h"
39#include "absl/types/span.h"
40#include "scip/scip.h"
41#include "scip/type_cons.h"
42#include "scip/type_event.h"
43#include "scip/type_var.h"
45#include "ortools/gscip/gscip.h"
49#include "ortools/math_opt/callback.pb.h"
54#include "ortools/math_opt/model.pb.h"
55#include "ortools/math_opt/model_parameters.pb.h"
56#include "ortools/math_opt/model_update.pb.h"
57#include "ortools/math_opt/parameters.pb.h"
58#include "ortools/math_opt/result.pb.h"
59#include "ortools/math_opt/solution.pb.h"
62#include "ortools/math_opt/sparse_containers.pb.h"
65#include "absl/status/status.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, const int row_id,
126 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().begin();
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().begin();
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 CHECK_GE(id, next_unused_variable_id_);
305 SCIP_VAR* const v,
306 gscip_->AddVariable(
307 variables.lower_bounds(i), variables.upper_bounds(i),
308 gtl::FindWithDefault(linear_objective_coefficients, id),
309 GScipVarTypeFromIsInteger(variables.integers(i)),
310 SafeName(variables, i)));
311 gtl::InsertOrDie(&variables_, id, v);
312 next_unused_variable_id_ = id + 1;
313 }
314 return absl::OkStatus();
315}
316
317absl::Status GScipSolver::UpdateVariables(
318 const VariableUpdatesProto& variable_updates) {
319 for (const auto [id, lb] : MakeView(variable_updates.lower_bounds())) {
320 RETURN_IF_ERROR(gscip_->SetLb(variables_.at(id), lb));
321 }
322 for (const auto [id, ub] : MakeView(variable_updates.upper_bounds())) {
323 RETURN_IF_ERROR(gscip_->SetUb(variables_.at(id), ub));
324 }
325 for (const auto [id, is_integer] : MakeView(variable_updates.integers())) {
326 RETURN_IF_ERROR(gscip_->SetVarType(variables_.at(id),
327 GScipVarTypeFromIsInteger(is_integer)));
328 }
329 return absl::OkStatus();
330}
331
332absl::Status GScipSolver::AddLinearConstraints(
333 const LinearConstraintsProto& linear_constraints,
334 const SparseDoubleMatrixProto& linear_constraint_matrix) {
335 for (LinearConstraintIterator lin_con_it(&linear_constraints,
336 &linear_constraint_matrix);
337 !lin_con_it.IsDone(); lin_con_it.Next()) {
338 const LinearConstraintView current = lin_con_it.Current();
339 CHECK_GE(current.linear_constraint_id, next_unused_linear_constraint_id_);
340
341 GScipLinearRange range;
342 range.lower_bound = current.lower_bound;
343 range.upper_bound = current.upper_bound;
344 range.coefficients = std::vector<double>(current.coefficients.begin(),
345 current.coefficients.end());
346 range.variables.reserve(current.variable_ids.size());
347 for (const int64_t var_id : current.variable_ids) {
348 range.variables.push_back(variables_.at(var_id));
349 }
351 SCIP_CONS* const scip_con,
352 gscip_->AddLinearConstraint(range, std::string(current.name)));
353 gtl::InsertOrDie(&linear_constraints_, current.linear_constraint_id,
354 scip_con);
355 next_unused_linear_constraint_id_ = current.linear_constraint_id + 1;
356 }
357 return absl::OkStatus();
358}
359
360absl::Status GScipSolver::UpdateLinearConstraints(
361 const LinearConstraintUpdatesProto linear_constraint_updates,
362 const SparseDoubleMatrixProto& linear_constraint_matrix) {
363 for (const auto [id, lb] :
364 MakeView(linear_constraint_updates.lower_bounds())) {
366 gscip_->SetLinearConstraintLb(linear_constraints_.at(id), lb));
367 }
368 for (const auto [id, ub] :
369 MakeView(linear_constraint_updates.upper_bounds())) {
371 gscip_->SetLinearConstraintUb(linear_constraints_.at(id), ub));
372 }
373 for (int i = 0; i < linear_constraint_matrix.row_ids_size(); ++i) {
374 const int64_t lin_con_id = linear_constraint_matrix.row_ids(i);
375 if (lin_con_id >= next_unused_linear_constraint_id_) {
376 break;
377 }
378 const int64_t var_id = linear_constraint_matrix.column_ids(i);
379 const double value = linear_constraint_matrix.coefficients(i);
380 RETURN_IF_ERROR(gscip_->SetLinearConstraintCoef(
381 linear_constraints_.at(lin_con_id), variables_.at(var_id), value));
382 }
383 return absl::OkStatus();
384}
385
387 switch (emphasis) {
388 case EMPHASIS_OFF:
390 case EMPHASIS_LOW:
392 case EMPHASIS_MEDIUM:
393 case EMPHASIS_UNSPECIFIED:
395 case EMPHASIS_HIGH:
396 case EMPHASIS_VERY_HIGH:
398 default:
399 LOG(FATAL) << "Unsupported MathOpt Emphasis value: "
400 << ProtoEnumToString(emphasis)
401 << " unknown, error setting gSCIP parameters";
402 }
403}
404
405std::pair<GScipParameters, std::vector<std::string>>
406GScipSolver::MergeParameters(const SolveParametersProto& solve_parameters) {
407 // First build the result by translating common parameters to a
408 // GScipParameters, and then merging with user provided gscip_parameters.
409 // This results in user provided solver specific parameters overwriting
410 // common parameters should there be any conflict.
411 GScipParameters result;
412 std::vector<std::string> warnings;
413
414 // By default SCIP catches Ctrl-C but we don't want this behavior when the
415 // users uses SCIP through MathOpt.
416 GScipSetCatchCtrlC(false, &result);
417
418 if (solve_parameters.has_time_limit()) {
420 util_time::DecodeGoogleApiProto(solve_parameters.time_limit()).value(),
421 &result);
422 }
423
424 if (solve_parameters.has_threads()) {
425 GScipSetMaxNumThreads(solve_parameters.threads(), &result);
426 }
427
428 if (solve_parameters.has_relative_gap_limit()) {
429 (*result.mutable_real_params())["limits/gap"] =
430 solve_parameters.relative_gap_limit();
431 }
432
433 if (solve_parameters.has_absolute_gap_limit()) {
434 (*result.mutable_real_params())["limits/absgap"] =
435 solve_parameters.absolute_gap_limit();
436 }
437
438 if (solve_parameters.has_objective_limit()) {
439 warnings.push_back("parameter objective_limit not supported for gSCIP.");
440 }
441 if (solve_parameters.has_best_bound_limit()) {
442 warnings.push_back("parameter best_bound_limit not supported for gSCIP.");
443 }
444
445 if (solve_parameters.has_cutoff_limit()) {
446 result.set_objective_limit(solve_parameters.cutoff_limit());
447 }
448
449 if (solve_parameters.has_solution_limit()) {
450 (*result.mutable_int_params())["limits/solutions"] =
451 solve_parameters.solution_limit();
452 }
453
454 // GScip has also GScipSetOutputEnabled() but this changes the log
455 // level. Setting `silence_output` sets the `quiet` field on the default
456 // message handler of SCIP which removes the output. Here it is important to
457 // use this rather than changing the log level so that if the user registers
458 // for CALLBACK_EVENT_MESSAGE they do get some messages even when
459 // `enable_output` is false.
460 result.set_silence_output(!solve_parameters.enable_output());
461
462 if (solve_parameters.has_random_seed()) {
463 GScipSetRandomSeed(&result, solve_parameters.random_seed());
464 }
465
466 if (solve_parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) {
467 char alg;
468 switch (solve_parameters.lp_algorithm()) {
469 case LP_ALGORITHM_PRIMAL_SIMPLEX:
470 alg = 'p';
471 break;
472 case LP_ALGORITHM_DUAL_SIMPLEX:
473 alg = 'd';
474 break;
475 case LP_ALGORITHM_BARRIER:
476 alg = 'c';
477 break;
478 default:
479 LOG(FATAL) << "LPAlgorithm: "
480 << ProtoEnumToString(solve_parameters.lp_algorithm())
481 << " unknown, error setting gSCIP parameters";
482 }
483 (*result.mutable_char_params())["lp/initalgorithm"] = alg;
484 }
485
486 if (solve_parameters.cuts() != EMPHASIS_UNSPECIFIED) {
487 result.set_separating(ConvertMathOptEmphasis(solve_parameters.cuts()));
488 }
489 if (solve_parameters.heuristics() != EMPHASIS_UNSPECIFIED) {
490 result.set_heuristics(
491 ConvertMathOptEmphasis(solve_parameters.heuristics()));
492 }
493 if (solve_parameters.presolve() != EMPHASIS_UNSPECIFIED) {
494 result.set_presolve(ConvertMathOptEmphasis(solve_parameters.presolve()));
495 }
496 if (solve_parameters.scaling() != EMPHASIS_UNSPECIFIED) {
497 int scaling_value;
498 switch (solve_parameters.scaling()) {
499 case EMPHASIS_OFF:
500 scaling_value = 0;
501 break;
502 case EMPHASIS_LOW:
503 case EMPHASIS_MEDIUM:
504 scaling_value = 1;
505 break;
506 case EMPHASIS_HIGH:
507 case EMPHASIS_VERY_HIGH:
508 scaling_value = 2;
509 break;
510 default:
511 LOG(FATAL) << "Scaling emphasis: "
512 << ProtoEnumToString(solve_parameters.scaling())
513 << " unknown, error setting gSCIP parameters";
514 }
515 (*result.mutable_int_params())["lp/scaling"] = scaling_value;
516 }
517
518 result.MergeFrom(solve_parameters.gscip());
519
520 return {std::move(result), std::move(warnings)};
521}
522
523namespace {
524
525std::string JoinDetails(const std::string& gscip_detail,
526 const std::string& math_opt_detail) {
527 if (gscip_detail.empty()) {
528 return math_opt_detail;
529 }
530 if (math_opt_detail.empty()) {
531 return gscip_detail;
532 }
533 return absl::StrCat(gscip_detail, "; ", math_opt_detail);
534}
535
536ProblemStatusProto GetProblemStatusProto(const GScipOutput::Status gscip_status,
537 const bool has_feasible_solution,
538 const bool has_finite_dual_bound,
539 const bool was_cutoff) {
540 ProblemStatusProto problem_status;
541 if (has_feasible_solution) {
542 problem_status.set_primal_status(FEASIBILITY_STATUS_FEASIBLE);
543 } else {
544 problem_status.set_primal_status(FEASIBILITY_STATUS_UNDETERMINED);
545 }
546 problem_status.set_dual_status(FEASIBILITY_STATUS_UNDETERMINED);
547
548 switch (gscip_status) {
550 problem_status.set_dual_status(FEASIBILITY_STATUS_FEASIBLE);
551 break;
553 if (!was_cutoff) {
554 problem_status.set_primal_status(FEASIBILITY_STATUS_INFEASIBLE);
555 }
556 break;
558 problem_status.set_dual_status(FEASIBILITY_STATUS_INFEASIBLE);
559 break;
561 problem_status.set_primal_or_dual_infeasible(true);
562 break;
563 default:
564 break;
565 }
566 if (has_finite_dual_bound) {
567 problem_status.set_dual_status(FEASIBILITY_STATUS_FEASIBLE);
568 }
569 return problem_status;
570}
571
572absl::StatusOr<TerminationProto> ConvertTerminationReason(
573 const GScipOutput::Status gscip_status,
574 const std::string& gscip_status_detail, const bool has_feasible_solution,
575 const bool had_cutoff) {
576 switch (gscip_status) {
578 return TerminateForLimit(
579 LIMIT_INTERRUPTED, /*feasible=*/has_feasible_solution,
580 JoinDetails(gscip_status_detail,
581 "underlying gSCIP status: USER_INTERRUPT"));
583 return TerminateForLimit(
584 LIMIT_NODE, /*feasible=*/has_feasible_solution,
585 JoinDetails(gscip_status_detail,
586 "underlying gSCIP status: NODE_LIMIT"));
588 return TerminateForLimit(
589 LIMIT_NODE, /*feasible=*/has_feasible_solution,
590 JoinDetails(gscip_status_detail,
591 "underlying gSCIP status: TOTAL_NODE_LIMIT"));
593 return TerminateForLimit(LIMIT_SLOW_PROGRESS,
594 /*feasible=*/has_feasible_solution,
595 gscip_status_detail);
597 return TerminateForLimit(LIMIT_TIME, /*feasible=*/has_feasible_solution,
598 gscip_status_detail);
600 return TerminateForLimit(LIMIT_MEMORY, /*feasible=*/has_feasible_solution,
601 gscip_status_detail);
603 return TerminateForLimit(
604 LIMIT_SOLUTION, /*feasible=*/has_feasible_solution,
605 JoinDetails(gscip_status_detail,
606 "underlying gSCIP status: SOL_LIMIT"));
608 return TerminateForLimit(
609 LIMIT_SOLUTION, /*feasible=*/has_feasible_solution,
610 JoinDetails(gscip_status_detail,
611 "underlying gSCIP status: BEST_SOL_LIMIT"));
613 return TerminateForLimit(
614 LIMIT_OTHER, /*feasible=*/has_feasible_solution,
615 JoinDetails(gscip_status_detail,
616 "underlying gSCIP status: RESTART_LIMIT"));
618 return TerminateForReason(
619 TERMINATION_REASON_OPTIMAL,
620 JoinDetails(gscip_status_detail, "underlying gSCIP status: OPTIMAL"));
622 return TerminateForReason(
623 TERMINATION_REASON_OPTIMAL,
624 JoinDetails(gscip_status_detail,
625 "underlying gSCIP status: GAP_LIMIT"));
627 if (had_cutoff) {
628 return TerminateForLimit(LIMIT_CUTOFF,
629 /*feasible=*/false, gscip_status_detail);
630 } else {
631 return TerminateForReason(TERMINATION_REASON_INFEASIBLE,
632 gscip_status_detail);
633 }
635 if (has_feasible_solution) {
636 return TerminateForReason(
637 TERMINATION_REASON_UNBOUNDED,
638 JoinDetails(gscip_status_detail,
639 "underlying gSCIP status was UNBOUNDED, both primal "
640 "ray and feasible solution are present"));
641 } else {
642 return TerminateForReason(
643 TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED,
644 JoinDetails(
645 gscip_status_detail,
646 "underlying gSCIP status was UNBOUNDED, but only primal ray "
647 "was given, no feasible solution was found"));
648 }
649 }
650
652 return TerminateForReason(
653 TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED,
654 JoinDetails(gscip_status_detail,
655 "underlying gSCIP status: INF_OR_UNBD"));
656
658 return TerminateForLimit(
659 LIMIT_INTERRUPTED, /*feasible=*/has_feasible_solution,
660 JoinDetails(gscip_status_detail,
661 "underlying gSCIP status: TERMINATE"));
663 return absl::InvalidArgumentError(gscip_status_detail);
665 return absl::InternalError(JoinDetails(
666 gscip_status_detail, "Unexpected GScipOutput.status: UNKNOWN"));
667 default:
668 return absl::InternalError(JoinDetails(
669 gscip_status_detail, absl::StrCat("Missing GScipOutput.status case: ",
670 ProtoEnumToString(gscip_status))));
671 }
672}
673
674} // namespace
675
676absl::StatusOr<SolveResultProto> GScipSolver::CreateSolveResultProto(
677 GScipResult gscip_result, const ModelSolveParametersProto& model_parameters,
678 const std::optional<double> cutoff) {
679 SolveResultProto solve_result;
681 *solve_result.mutable_termination(),
682 ConvertTerminationReason(
683 gscip_result.gscip_output.status(),
684 gscip_result.gscip_output.status_detail(),
685 /*has_feasible_solution=*/!gscip_result.solutions.empty(),
686 /*had_cutoff=*/cutoff.has_value()));
687 const bool is_maximize = gscip_->ObjectiveIsMaximize();
688 // When an objective limit is set, SCIP returns the solutions worse than the
689 // limit, we need to filter these out manually.
690 const auto meets_cutoff = [cutoff, is_maximize](const double obj_value) {
691 if (!cutoff.has_value()) {
692 return true;
693 }
694 if (is_maximize) {
695 return obj_value >= *cutoff;
696 } else {
697 return obj_value <= *cutoff;
698 }
699 };
700
701 LazyInitialized<std::vector<int64_t>> sorted_variables([&]() {
702 std::vector<int64_t> sorted;
703 sorted.reserve(variables_.size());
704 for (const auto& entry : variables_) {
705 sorted.emplace_back(entry.first);
706 }
707 std::sort(sorted.begin(), sorted.end());
708 return sorted;
709 });
710 CHECK_EQ(gscip_result.solutions.size(), gscip_result.objective_values.size());
711 for (int i = 0; i < gscip_result.solutions.size(); ++i) {
712 // GScip ensures the solutions are returned best objective first.
713 if (!meets_cutoff(gscip_result.objective_values[i])) {
714 break;
715 }
716 SolutionProto* const solution = solve_result.add_solutions();
717 PrimalSolutionProto* const primal_solution =
718 solution->mutable_primal_solution();
719 primal_solution->set_objective_value(gscip_result.objective_values[i]);
720 primal_solution->set_feasibility_status(SOLUTION_STATUS_FEASIBLE);
721 *primal_solution->mutable_variable_values() = FillSparseDoubleVector(
722 sorted_variables.GetOrCreate(), variables_, gscip_result.solutions[i],
723 model_parameters.variable_values_filter());
724 }
725 if (!gscip_result.primal_ray.empty()) {
726 *solve_result.add_primal_rays()->mutable_variable_values() =
727 FillSparseDoubleVector(sorted_variables.GetOrCreate(), variables_,
728 gscip_result.primal_ray,
729 model_parameters.variable_values_filter());
730 }
731 const bool has_feasible_solution = solve_result.solutions_size() > 0;
732 *solve_result.mutable_solve_stats()->mutable_problem_status() =
733 GetProblemStatusProto(
734 gscip_result.gscip_output.status(),
735 /*has_feasible_solution=*/has_feasible_solution,
736 /*has_finite_dual_bound=*/
737 std::isfinite(gscip_result.gscip_output.stats().best_bound()),
738 /*was_cutoff=*/solve_result.termination().limit() == LIMIT_CUTOFF);
739 SolveStatsProto* const common_stats = solve_result.mutable_solve_stats();
740 const GScipSolvingStats& gscip_stats = gscip_result.gscip_output.stats();
741 common_stats->set_best_dual_bound(gscip_stats.best_bound());
742 // If we found no solutions meeting the cutoff, we have no primal bound.
743 if (has_feasible_solution) {
744 common_stats->set_best_primal_bound(gscip_stats.best_objective());
745 } else {
746 common_stats->set_best_primal_bound(is_maximize ? -kInf : kInf);
747 }
748
749 common_stats->set_node_count(gscip_stats.node_count());
750 common_stats->set_simplex_iterations(gscip_stats.primal_simplex_iterations() +
751 gscip_stats.dual_simplex_iterations());
752 common_stats->set_barrier_iterations(gscip_stats.total_lp_iterations() -
753 common_stats->simplex_iterations());
754 *solve_result.mutable_gscip_output() = std::move(gscip_result.gscip_output);
755 return solve_result;
756}
757
758GScipSolver::GScipSolver(std::unique_ptr<GScip> gscip)
759 : gscip_(std::move(ABSL_DIE_IF_NULL(gscip))) {
760 interrupt_event_handler_.Register(gscip_.get());
761}
762
763absl::StatusOr<std::unique_ptr<SolverInterface>> GScipSolver::New(
764 const ModelProto& model, const InitArgs& init_args) {
765 ASSIGN_OR_RETURN(std::unique_ptr<GScip> gscip, GScip::Create(model.name()));
766 RETURN_IF_ERROR(gscip->SetMaximize(model.objective().maximize()));
767 RETURN_IF_ERROR(gscip->SetObjectiveOffset(model.objective().offset()));
768 // TODO(b/204083726): Remove this check if QP support is added
769 if (!model.objective().quadratic_coefficients().row_ids().empty()) {
770 return absl::InvalidArgumentError(
771 "MathOpt does not currently support SCIP models with quadratic "
772 "objectives");
773 }
774 // Can't be const because it had to be moved into the StatusOr and be
775 // convereted to std::unique_ptr<SolverInterface>.
776 auto solver = absl::WrapUnique(new GScipSolver(std::move(gscip)));
777
778 RETURN_IF_ERROR(solver->AddVariables(
779 model.variables(),
780 SparseDoubleVectorAsMap(model.objective().linear_coefficients())));
781 RETURN_IF_ERROR(solver->AddLinearConstraints(
782 model.linear_constraints(), model.linear_constraint_matrix()));
783
784 return solver;
785}
786
787absl::StatusOr<SolveResultProto> GScipSolver::Solve(
788 const SolveParametersProto& parameters,
789 const ModelSolveParametersProto& model_parameters,
790 const MessageCallback message_cb,
791 const CallbackRegistrationProto& callback_registration, const Callback cb,
792 SolveInterrupter* const interrupter) {
793 const absl::Time start = absl::Now();
794
796 /*supported_events=*/{}));
797
798 const std::unique_ptr<GScipSolverCallbackHandler> callback_handler =
799 GScipSolverCallbackHandler::RegisterIfNeeded(callback_registration, cb,
800 start, gscip_->scip());
801
802 std::unique_ptr<GScipSolverMessageCallbackHandler> message_cb_handler;
803 if (message_cb != nullptr) {
804 message_cb_handler =
805 std::make_unique<GScipSolverMessageCallbackHandler>(message_cb);
806 }
807
808 const auto [gscip_parameters, warnings] = MergeParameters(parameters);
809 if (parameters.strictness().bad_parameter() && !warnings.empty()) {
810 return absl::InvalidArgumentError(absl::StrJoin(warnings, "; "));
811 }
812 // TODO(user): reorganize gscip to respect warning is error argument on bad
813 // parameters.
814
815 for (const SolutionHintProto& hint : model_parameters.solution_hints()) {
816 absl::flat_hash_map<SCIP_VAR*, double> partial_solution;
817 for (const auto [id, val] : MakeView(hint.variable_values())) {
818 partial_solution.insert({variables_.at(id), val});
819 }
820 RETURN_IF_ERROR(gscip_->SuggestHint(partial_solution).status());
821 }
822 for (const auto [id, value] :
823 MakeView(model_parameters.branching_priorities())) {
824 RETURN_IF_ERROR(gscip_->SetBranchingPriority(variables_.at(id), value));
825 }
826
827 // Before calling solve, set the interrupter on the event handler that calls
828 // SCIPinterruptSolve().
829 if (interrupter != nullptr) {
830 interrupt_event_handler_.interrupter = interrupter;
831 }
832 const auto interrupter_cleanup = absl::MakeCleanup(
833 [&]() { interrupt_event_handler_.interrupter = nullptr; });
834
835 ASSIGN_OR_RETURN(GScipResult gscip_result,
836 gscip_->Solve(gscip_parameters,
837 /*legacy_params=*/"",
838 message_cb_handler != nullptr
839 ? message_cb_handler->MessageHandler()
840 : nullptr));
841
842 // Flushes the last unfinished message as early as possible.
843 message_cb_handler.reset();
844
845 if (callback_handler) {
846 RETURN_IF_ERROR(callback_handler->Flush());
847 }
848
850 SolveResultProto result,
851 CreateSolveResultProto(std::move(gscip_result), model_parameters,
852 parameters.has_cutoff_limit()
853 ? std::make_optional(parameters.cutoff_limit())
854 : std::nullopt));
855 for (const std::string& warning : warnings) {
856 result.add_warnings(warning);
857 }
859 absl::Now() - start, result.mutable_solve_stats()->mutable_solve_time()));
860 return result;
861}
862
863absl::flat_hash_set<SCIP_VAR*> GScipSolver::LookupAllVariables(
864 absl::Span<const int64_t> variable_ids) {
865 absl::flat_hash_set<SCIP_VAR*> result;
866 result.reserve(variable_ids.size());
867 for (const int64_t var_id : variable_ids) {
868 result.insert(variables_.at(var_id));
869 }
870 return result;
871}
872
873bool GScipSolver::CanUpdate(const ModelUpdateProto& model_update) {
874 return gscip_
875 ->CanSafeBulkDelete(
876 LookupAllVariables(model_update.deleted_variable_ids()))
877 .ok() &&
878 model_update.objective_updates()
879 .quadratic_coefficients()
880 .row_ids()
881 .empty();
882}
883
884absl::Status GScipSolver::Update(const ModelUpdateProto& model_update) {
885 for (const int64_t constraint_id :
886 model_update.deleted_linear_constraint_ids()) {
887 SCIP_CONS* const scip_cons = linear_constraints_.at(constraint_id);
888 linear_constraints_.erase(constraint_id);
889 RETURN_IF_ERROR(gscip_->DeleteConstraint(scip_cons));
890 }
891 {
892 const absl::flat_hash_set<SCIP_VAR*> vars_to_delete =
893 LookupAllVariables(model_update.deleted_variable_ids());
894 for (const int64_t deleted_variable_id :
895 model_update.deleted_variable_ids()) {
896 variables_.erase(deleted_variable_id);
897 }
898 RETURN_IF_ERROR(gscip_->SafeBulkDelete(vars_to_delete));
899 }
900 if (model_update.objective_updates().has_direction_update()) {
901 RETURN_IF_ERROR(gscip_->SetMaximize(
902 model_update.objective_updates().direction_update()));
903 }
904 if (model_update.objective_updates().has_offset_update()) {
905 RETURN_IF_ERROR(gscip_->SetObjectiveOffset(
906 model_update.objective_updates().offset_update()));
907 }
908 RETURN_IF_ERROR(UpdateVariables(model_update.variable_updates()));
909 const absl::flat_hash_map<int64_t, double> linear_objective_updates =
910 SparseDoubleVectorAsMap(
911 model_update.objective_updates().linear_coefficients());
912 for (const auto& obj_pair : linear_objective_updates) {
913 if (obj_pair.first < next_unused_variable_id_) {
915 gscip_->SetObjCoef(variables_.at(obj_pair.first), obj_pair.second));
916 }
917 }
919 AddVariables(model_update.new_variables(), linear_objective_updates));
921 UpdateLinearConstraints(model_update.linear_constraint_updates(),
922 model_update.linear_constraint_matrix_updates()));
924 AddLinearConstraints(model_update.new_linear_constraints(),
925 model_update.linear_constraint_matrix_updates()));
926 return absl::OkStatus();
927}
928
929GScipSolver::InterruptEventHandler::InterruptEventHandler()
931 {.name = "interrupt event handler",
932 .description = "Event handler to call SCIPinterruptSolve() when a "
933 "user SolveInterrupter is triggered."}) {}
934
935SCIP_RETCODE GScipSolver::InterruptEventHandler::Init(GScip* const gscip) {
936 // We don't register any event if we don't have an interrupter.
937 if (interrupter == nullptr) {
938 return SCIP_OKAY;
939 }
940
941 // TODO(b/193537362): see if these events are enough or if we should have more
942 // of these.
943 CatchEvent(SCIP_EVENTTYPE_PRESOLVEROUND);
944 CatchEvent(SCIP_EVENTTYPE_NODEEVENT);
945
946 return TryCallInterruptIfNeeded(gscip);
947}
948
949SCIP_RETCODE GScipSolver::InterruptEventHandler::Execute(
950 const GScipEventHandlerContext context) {
951 return TryCallInterruptIfNeeded(context.gscip());
952}
953
954SCIP_RETCODE GScipSolver::InterruptEventHandler::TryCallInterruptIfNeeded(
955 GScip* const gscip) {
956 if (interrupter == nullptr) {
957 LOG(WARNING) << "TryCallInterruptIfNeeded() called after interrupter has "
958 "been reset!";
959 return SCIP_OKAY;
960 }
961
962 if (!interrupter->IsInterrupted()) {
963 return SCIP_OKAY;
964 }
965
966 const SCIP_STAGE stage = SCIPgetStage(gscip->scip());
967 switch (stage) {
968 case SCIP_STAGE_INIT:
969 case SCIP_STAGE_FREE:
970 // This should never happen anyway; but if this happens, we may want to
971 // know about it in unit tests.
972 LOG(DFATAL) << "TryCallInterruptIfNeeded() called in stage "
973 << (stage == SCIP_STAGE_INIT ? "INIT" : "FREE");
974 return SCIP_OKAY;
975 case SCIP_STAGE_INITSOLVE:
976 LOG(WARNING) << "TryCallInterruptIfNeeded() called in INITSOLVE stage; "
977 "we can't call SCIPinterruptSolve() in this stage.";
978 return SCIP_OKAY;
979 default:
980 return SCIPinterruptSolve(gscip->scip());
981 }
982}
983
985
986} // namespace math_opt
987} // namespace operations_research
#define CHECK(condition)
Definition: base/logging.h:495
#define CHECK_EQ(val1, val2)
Definition: base/logging.h:702
#define CHECK_GE(val1, val2)
Definition: base/logging.h:706
#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
static absl::StatusOr< std::unique_ptr< GScip > > Create(const std::string &problem_name)
Definition: gscip.cc:272
static constexpr Status TOTAL_NODE_LIMIT
Definition: gscip.pb.h:1244
static constexpr Status BEST_SOL_LIMIT
Definition: gscip.pb.h:1256
static constexpr Status UNBOUNDED
Definition: gscip.pb.h:1264
static constexpr Status INF_OR_UNBD
Definition: gscip.pb.h:1266
static constexpr Status SOL_LIMIT
Definition: gscip.pb.h:1254
static constexpr Status STALL_NODE_LIMIT
Definition: gscip.pb.h:1246
static constexpr Status TERMINATE
Definition: gscip.pb.h:1268
static constexpr Status GAP_LIMIT
Definition: gscip.pb.h:1252
static constexpr Status RESTART_LIMIT
Definition: gscip.pb.h:1258
static constexpr Status USER_INTERRUPT
Definition: gscip.pb.h:1240
static constexpr Status TIME_LIMIT
Definition: gscip.pb.h:1248
static constexpr Status INVALID_SOLVER_PARAMETERS
Definition: gscip.pb.h:1270
static constexpr Status NODE_LIMIT
Definition: gscip.pb.h:1242
static constexpr Status INFEASIBLE
Definition: gscip.pb.h:1262
static constexpr Status MEM_LIMIT
Definition: gscip.pb.h:1250
GScipOutput_Status Status
Definition: gscip.pb.h:1237
static constexpr Status OPTIMAL
Definition: gscip.pb.h:1260
static constexpr Status UNKNOWN
Definition: gscip.pb.h:1238
::PROTOBUF_NAMESPACE_ID::Map< std::string, double > * mutable_real_params()
Definition: gscip.pb.h:1596
void set_objective_limit(double value)
Definition: gscip.pb.h:1931
static constexpr MetaParamValue FAST
Definition: gscip.pb.h:529
::PROTOBUF_NAMESPACE_ID::Map< std::string, int32_t > * mutable_int_params()
Definition: gscip.pb.h:1538
static constexpr MetaParamValue AGGRESSIVE
Definition: gscip.pb.h:527
void set_presolve(::operations_research::GScipParameters_MetaParamValue value)
Definition: gscip.pb.h:1452
::PROTOBUF_NAMESPACE_ID::Map< std::string, std::string > * mutable_char_params()
Definition: gscip.pb.h:1625
static constexpr MetaParamValue DEFAULT_META_PARAM_VALUE
Definition: gscip.pb.h:525
void MergeFrom(const GScipParameters &from)
Definition: gscip.pb.cc:1488
void set_heuristics(::operations_research::GScipParameters_MetaParamValue value)
Definition: gscip.pb.h:1424
static constexpr MetaParamValue OFF
Definition: gscip.pb.h:531
void set_separating(::operations_research::GScipParameters_MetaParamValue value)
Definition: gscip.pb.h:1480
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 std::pair< GScipParameters, std::vector< std::string > > 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
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
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:120
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)
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:400
TerminationProto TerminateForLimit(const LimitProto limit, const bool feasible, const absl::string_view detail)
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)
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
int index
Definition: pack.cc:509
int64_t start
#define ASSIGN_OR_RETURN(lhs, rexpr)
Definition: status_macros.h:48
#define RETURN_IF_ERROR(expr)
Definition: status_macros.h:29