Google OR-Tools v9.9
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
gurobi_solver.cc
Go to the documentation of this file.
1// Copyright 2010-2024 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <algorithm>
17#include <cmath>
18#include <cstddef>
19#include <cstdint>
20#include <limits>
21#include <memory>
22#include <optional>
23#include <string>
24#include <string_view>
25#include <utility>
26#include <vector>
27
28#include "absl/algorithm/container.h"
29#include "absl/container/flat_hash_map.h"
30#include "absl/container/flat_hash_set.h"
31#include "absl/log/check.h"
32#include "absl/memory/memory.h"
33#include "absl/meta/type_traits.h"
34#include "absl/status/status.h"
35#include "absl/status/statusor.h"
36#include "absl/strings/escaping.h"
37#include "absl/strings/str_cat.h"
38#include "absl/strings/str_join.h"
39#include "absl/strings/string_view.h"
40#include "absl/time/clock.h"
41#include "absl/time/time.h"
42#include "absl/types/span.h"
43#include "google/protobuf/repeated_ptr_field.h"
49#include "ortools/math_opt/callback.pb.h"
58#include "ortools/math_opt/infeasible_subsystem.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"
65#include "ortools/math_opt/solvers/gurobi.pb.h"
69#include "ortools/math_opt/sparse_containers.pb.h"
73
74namespace operations_research {
75namespace math_opt {
76namespace {
77
78constexpr SupportedProblemStructures kGurobiSupportedStructures = {
79 .integer_variables = SupportType::kSupported,
80 .multi_objectives = SupportType::kSupported,
81 .quadratic_objectives = SupportType::kSupported,
82 .quadratic_constraints = SupportType::kSupported,
83 .second_order_cone_constraints = SupportType::kSupported,
84 .sos1_constraints = SupportType::kSupported,
85 .sos2_constraints = SupportType::kSupported,
86 .indicator_constraints = SupportType::kSupported};
87
88absl::StatusOr<std::unique_ptr<Gurobi>> GurobiFromInitArgs(
89 const SolverInterface::InitArgs& init_args) {
90 if (kAnyXsanEnabled) {
91 return absl::FailedPreconditionError(
92 "the Gurobi library is not compatible with any sanitizer (MSAN, ASAN "
93 "or TSAN)");
94 }
95
96 // We don't test or return an error for incorrect non streamable arguments
97 // type since it is already tested by the Solver class.
98 const NonStreamableGurobiInitArguments* const non_streamable_args =
99 init_args.non_streamable != nullptr
100 ? init_args.non_streamable->ToNonStreamableGurobiInitArguments()
101 : nullptr;
102 std::unique_ptr<Gurobi> gurobi;
103 if (non_streamable_args != nullptr &&
104 non_streamable_args->primary_env != nullptr) {
105 return Gurobi::NewWithSharedPrimaryEnv(non_streamable_args->primary_env);
106 } else if (init_args.streamable.has_gurobi() &&
107 init_args.streamable.gurobi().has_isv_key()) {
109 GRBenvUniquePtr env,
110 NewPrimaryEnvironment(init_args.streamable.gurobi().isv_key()));
111 return Gurobi::New(std::move(env));
112 } else {
113 return Gurobi::New();
114 }
115}
116
117inline BasisStatusProto ConvertVariableStatus(const int status) {
118 switch (status) {
119 case GRB_BASIC:
120 return BASIS_STATUS_BASIC;
122 return BASIS_STATUS_AT_LOWER_BOUND;
124 return BASIS_STATUS_AT_UPPER_BOUND;
125 case GRB_SUPERBASIC:
126 return BASIS_STATUS_FREE;
127 default:
128 return BASIS_STATUS_UNSPECIFIED;
129 }
130}
131
132inline int GrbVariableStatus(const BasisStatusProto status) {
133 switch (status) {
134 case BASIS_STATUS_BASIC:
135 return GRB_BASIC;
136 case BASIS_STATUS_AT_LOWER_BOUND:
137 case BASIS_STATUS_FIXED_VALUE:
138 return GRB_NONBASIC_LOWER;
139 case BASIS_STATUS_AT_UPPER_BOUND:
140 return GRB_NONBASIC_UPPER;
141 case BASIS_STATUS_FREE:
142 return GRB_SUPERBASIC;
143 case BASIS_STATUS_UNSPECIFIED:
144 default:
145 LOG(FATAL) << "Unexpected invalid initial_basis.";
146 return 0;
147 }
148}
149
150// is_mip indicates if the problem has integer variables or constraints that
151// would cause Gurobi to treat the problem as a MIP, e.g. SOS, indicator.
152absl::StatusOr<GurobiParametersProto> MergeParameters(
153 const SolveParametersProto& solve_parameters, const bool is_mip) {
154 GurobiParametersProto merged_parameters;
155
156 {
157 GurobiParametersProto::Parameter* const parameter =
158 merged_parameters.add_parameters();
159 parameter->set_name(GRB_INT_PAR_LOGTOCONSOLE);
160 parameter->set_value(solve_parameters.enable_output() ? "1" : "0");
161 }
162
163 if (solve_parameters.has_time_limit()) {
164 const double time_limit = absl::ToDoubleSeconds(
165 util_time::DecodeGoogleApiProto(solve_parameters.time_limit()).value());
166 GurobiParametersProto::Parameter* const parameter =
167 merged_parameters.add_parameters();
168 parameter->set_name(GRB_DBL_PAR_TIMELIMIT);
169 parameter->set_value(absl::StrCat(time_limit));
170 }
171
172 if (solve_parameters.has_node_limit()) {
173 GurobiParametersProto::Parameter* const parameter =
174 merged_parameters.add_parameters();
175 parameter->set_name(GRB_DBL_PAR_NODELIMIT);
176 parameter->set_value(absl::StrCat(solve_parameters.node_limit()));
177 }
178
179 if (solve_parameters.has_threads()) {
180 const int threads = solve_parameters.threads();
181 GurobiParametersProto::Parameter* const parameter =
182 merged_parameters.add_parameters();
183 parameter->set_name(GRB_INT_PAR_THREADS);
184 parameter->set_value(absl::StrCat(threads));
185 }
186
187 if (solve_parameters.has_absolute_gap_tolerance()) {
188 const double absolute_gap_tolerance =
189 solve_parameters.absolute_gap_tolerance();
190 GurobiParametersProto::Parameter* const parameter =
191 merged_parameters.add_parameters();
192 parameter->set_name(GRB_DBL_PAR_MIPGAPABS);
193 parameter->set_value(absl::StrCat(absolute_gap_tolerance));
194 }
195
196 if (solve_parameters.has_relative_gap_tolerance()) {
197 const double relative_gap_tolerance =
198 solve_parameters.relative_gap_tolerance();
199 GurobiParametersProto::Parameter* const parameter =
200 merged_parameters.add_parameters();
201 parameter->set_name(GRB_DBL_PAR_MIPGAP);
202 parameter->set_value(absl::StrCat(relative_gap_tolerance));
203 }
204
205 if (solve_parameters.has_cutoff_limit()) {
206 GurobiParametersProto::Parameter* const parameter =
207 merged_parameters.add_parameters();
208 parameter->set_name(GRB_DBL_PAR_CUTOFF);
209 parameter->set_value(absl::StrCat(solve_parameters.cutoff_limit()));
210 }
211
212 if (solve_parameters.has_objective_limit()) {
213 if (!is_mip) {
214 return absl::InvalidArgumentError(
215 "objective_limit is only supported for Gurobi on MIP models");
216 }
217 GurobiParametersProto::Parameter* const parameter =
218 merged_parameters.add_parameters();
219 parameter->set_name(GRB_DBL_PAR_BESTOBJSTOP);
220 parameter->set_value(absl::StrCat(solve_parameters.objective_limit()));
221 }
222
223 if (solve_parameters.has_best_bound_limit()) {
224 if (!is_mip) {
225 return absl::InvalidArgumentError(
226 "best_bound_limit is only supported for Gurobi on MIP models");
227 }
228 GurobiParametersProto::Parameter* const parameter =
229 merged_parameters.add_parameters();
230 parameter->set_name(GRB_DBL_PAR_BESTBDSTOP);
231 parameter->set_value(absl::StrCat(solve_parameters.best_bound_limit()));
232 }
233
234 if (solve_parameters.has_solution_limit()) {
235 GurobiParametersProto::Parameter* const parameter =
236 merged_parameters.add_parameters();
237 parameter->set_name(GRB_INT_PAR_SOLUTIONLIMIT);
238 parameter->set_value(absl::StrCat(solve_parameters.solution_limit()));
239 }
240
241 if (solve_parameters.has_random_seed()) {
242 const int random_seed =
243 std::min(GRB_MAXINT, std::max(solve_parameters.random_seed(), 0));
244 GurobiParametersProto::Parameter* const parameter =
245 merged_parameters.add_parameters();
246 parameter->set_name(GRB_INT_PAR_SEED);
247 parameter->set_value(absl::StrCat(random_seed));
248 }
249
250 if (solve_parameters.has_solution_pool_size()) {
251 GurobiParametersProto::Parameter* const solution_pool_size =
252 merged_parameters.add_parameters();
253 solution_pool_size->set_name(GRB_INT_PAR_POOLSOLUTIONS);
254 solution_pool_size->set_value(
255 absl::StrCat(solve_parameters.solution_pool_size()));
256 }
257
258 if (solve_parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) {
259 GurobiParametersProto::Parameter* const parameter =
260 merged_parameters.add_parameters();
261 parameter->set_name(GRB_INT_PAR_METHOD);
262 switch (solve_parameters.lp_algorithm()) {
263 case LP_ALGORITHM_PRIMAL_SIMPLEX:
264 parameter->set_value(absl::StrCat(GRB_METHOD_PRIMAL));
265 break;
266 case LP_ALGORITHM_DUAL_SIMPLEX:
267 parameter->set_value(absl::StrCat(GRB_METHOD_DUAL));
268 break;
269 case LP_ALGORITHM_BARRIER:
270 parameter->set_value(absl::StrCat(GRB_METHOD_BARRIER));
271 break;
272 case LP_ALGORITHM_FIRST_ORDER:
273 return absl::InvalidArgumentError(
274 "lp_algorithm FIRST_ORDER is not supported for gurobi");
275 default:
276 LOG(FATAL) << "LPAlgorithm: "
277 << ProtoEnumToString(solve_parameters.lp_algorithm())
278 << " unknown, error setting Gurobi parameters";
279 }
280 }
281
282 if (solve_parameters.scaling() != EMPHASIS_UNSPECIFIED) {
283 GurobiParametersProto::Parameter* const parameter =
284 merged_parameters.add_parameters();
285 parameter->set_name(GRB_INT_PAR_SCALEFLAG);
286 switch (solve_parameters.scaling()) {
287 case EMPHASIS_OFF:
288 parameter->set_value(absl::StrCat(0));
289 break;
290 case EMPHASIS_LOW:
291 case EMPHASIS_MEDIUM:
292 parameter->set_value(absl::StrCat(1));
293 break;
294 case EMPHASIS_HIGH:
295 parameter->set_value(absl::StrCat(2));
296 break;
297 case EMPHASIS_VERY_HIGH:
298 parameter->set_value(absl::StrCat(3));
299 break;
300 default:
301 LOG(FATAL) << "Scaling emphasis: "
302 << ProtoEnumToString(solve_parameters.scaling())
303 << " unknown, error setting Gurobi parameters";
304 }
305 }
306
307 if (solve_parameters.cuts() != EMPHASIS_UNSPECIFIED) {
308 GurobiParametersProto::Parameter* const parameter =
309 merged_parameters.add_parameters();
310 parameter->set_name(GRB_INT_PAR_CUTS);
311 switch (solve_parameters.cuts()) {
312 case EMPHASIS_OFF:
313 parameter->set_value(absl::StrCat(0));
314 break;
315 case EMPHASIS_LOW:
316 case EMPHASIS_MEDIUM:
317 parameter->set_value(absl::StrCat(1));
318 break;
319 case EMPHASIS_HIGH:
320 parameter->set_value(absl::StrCat(2));
321 break;
322 case EMPHASIS_VERY_HIGH:
323 parameter->set_value(absl::StrCat(3));
324 break;
325 default:
326 LOG(FATAL) << "Cuts emphasis: "
327 << ProtoEnumToString(solve_parameters.cuts())
328 << " unknown, error setting Gurobi parameters";
329 }
330 }
331
332 if (solve_parameters.heuristics() != EMPHASIS_UNSPECIFIED) {
333 GurobiParametersProto::Parameter* const parameter =
334 merged_parameters.add_parameters();
335 parameter->set_name(GRB_DBL_PAR_HEURISTICS);
336 switch (solve_parameters.heuristics()) {
337 case EMPHASIS_OFF:
338 parameter->set_value(absl::StrCat(0.0));
339 break;
340 case EMPHASIS_LOW:
341 parameter->set_value(absl::StrCat(0.025));
342 break;
343 case EMPHASIS_MEDIUM:
344 // As of Gurobi 9.1 this is the default value.
345 // https://www.gurobi.com/documentation/9.1/refman/heuristics.html
346 parameter->set_value(absl::StrCat(0.05));
347 break;
348 case EMPHASIS_HIGH:
349 parameter->set_value(absl::StrCat(0.1));
350 break;
351 case EMPHASIS_VERY_HIGH:
352 parameter->set_value(absl::StrCat(0.2));
353 break;
354 default:
355 LOG(FATAL) << "Heuristics emphasis: "
356 << ProtoEnumToString(solve_parameters.heuristics())
357 << " unknown, error setting Gurobi parameters";
358 }
359 }
360
361 if (solve_parameters.presolve() != EMPHASIS_UNSPECIFIED) {
362 GurobiParametersProto::Parameter* const parameter =
363 merged_parameters.add_parameters();
364 parameter->set_name(GRB_INT_PAR_PRESOLVE);
365 switch (solve_parameters.presolve()) {
366 case EMPHASIS_OFF:
367 parameter->set_value(absl::StrCat(0));
368 break;
369 case EMPHASIS_LOW:
370 case EMPHASIS_MEDIUM:
371 parameter->set_value(absl::StrCat(1));
372 break;
373 case EMPHASIS_HIGH:
374 case EMPHASIS_VERY_HIGH:
375 parameter->set_value(absl::StrCat(2));
376 break;
377 default:
378 LOG(FATAL) << "Presolve emphasis: "
379 << ProtoEnumToString(solve_parameters.presolve())
380 << " unknown, error setting Gurobi parameters";
381 }
382 }
383
384 if (solve_parameters.has_iteration_limit()) {
385 GurobiParametersProto::Parameter* const iterationlimit =
386 merged_parameters.add_parameters();
387 iterationlimit->set_name(GRB_DBL_PAR_ITERATIONLIMIT);
388 iterationlimit->set_value(absl::StrCat(solve_parameters.iteration_limit()));
389 GurobiParametersProto::Parameter* const bariterlimit =
390 merged_parameters.add_parameters();
391 bariterlimit->set_name(GRB_INT_PAR_BARITERLIMIT);
392 double val = std::min<double>(std::numeric_limits<int32_t>::max(),
393 solve_parameters.iteration_limit());
394 bariterlimit->set_value(absl::StrCat(val));
395 }
396
397 for (const GurobiParametersProto::Parameter& parameter :
398 solve_parameters.gurobi().parameters()) {
399 *merged_parameters.add_parameters() = parameter;
400 }
401
402 return merged_parameters;
403}
404
405absl::StatusOr<int64_t> SafeInt64FromDouble(const double d) {
406 const int64_t result = static_cast<int64_t>(d);
407 if (static_cast<double>(result) != d) {
408 return absl::InternalError(
409 absl::StrCat("Expected double ", d, " to contain an int64_t."));
410 }
411 return result;
412}
413
414const absl::flat_hash_set<CallbackEventProto>& SupportedMIPEvents() {
415 static const auto* const kEvents =
416 new absl::flat_hash_set<CallbackEventProto>({
417 CALLBACK_EVENT_PRESOLVE, CALLBACK_EVENT_SIMPLEX, CALLBACK_EVENT_MIP,
418 CALLBACK_EVENT_MIP_SOLUTION, CALLBACK_EVENT_MIP_NODE,
419 // CALLBACK_EVENT_BARRIER is not supported when solving MIPs; it turns
420 // out that Gurobi uses a barrier algorithm to solve the root node
421 // relaxation (from the traces) but does not call the associated
422 // callback.
423 });
424 return *kEvents;
425}
426
427const absl::flat_hash_set<CallbackEventProto>& SupportedLPEvents() {
428 static const auto* const kEvents =
429 new absl::flat_hash_set<CallbackEventProto>({
430 CALLBACK_EVENT_PRESOLVE,
431 CALLBACK_EVENT_SIMPLEX,
432 CALLBACK_EVENT_BARRIER,
433 });
434 return *kEvents;
435}
436
437// Gurobi names (model, variables and constraints) must be no longer than 255
438// characters; or Gurobi fails with an error.
439constexpr std::size_t kMaxNameSize = 255;
440
441// Returns a string of at most kMaxNameSize max size.
442std::string TruncateName(const std::string_view original_name) {
443 return std::string(
444 original_name.substr(0, std::min(kMaxNameSize, original_name.size())));
445}
446
447// Truncate the names of variables and constraints.
448std::vector<std::string> TruncateNames(
449 const google::protobuf::RepeatedPtrField<std::string>& original_names) {
450 std::vector<std::string> result;
451 result.reserve(original_names.size());
452 for (const std::string& original_name : original_names) {
453 result.push_back(TruncateName(original_name));
454 }
455 return result;
456}
457
458absl::Status SafeGurobiDouble(const double d) {
459 if (std::isfinite(d) && std::abs(d) >= GRB_INFINITY) {
461 << "finite value: " << d << " will be treated as infinite by Gurobi";
462 }
463 return absl::OkStatus();
464}
465
466std::string EscapedNameForLogging(const absl::string_view name) {
467 return absl::StrCat("\"", absl::Utf8SafeCEscape(name), "\"");
468}
469
470constexpr int kDeletedIndex = -1;
471constexpr int kUnsetIndex = -2;
472// Returns a vector of length `size_before_delete` that logically provides a
473// mapping from the starting contiguous range [0, ..., size_before_delete) to
474// a potentially smaller range [0, ..., num_remaining_elems) after deleting
475// each element in `deletes` and shifting the remaining elements such that they
476// are contiguous starting at 0. The elements in the output point to the new
477// shifted index, or `kDeletedIndex` if the starting index was deleted.
478std::vector<int> IndexUpdateMap(const int size_before_delete,
479 absl::Span<const int> deletes) {
480 std::vector<int> result(size_before_delete, kUnsetIndex);
481 for (const int del : deletes) {
482 result[del] = kDeletedIndex;
483 }
484 int next_free = 0;
485 for (int& r : result) {
486 if (r != kDeletedIndex) {
487 r = next_free;
488 ++next_free;
489 }
490 CHECK_GT(r, kUnsetIndex);
491 }
492 return result;
493}
494
495absl::StatusOr<bool> GetIntAttrElementAsBool(Gurobi& gurobi,
496 const char* const name,
497 const int element) {
498 ASSIGN_OR_RETURN(const int value, gurobi.GetIntAttrElement(name, element));
499 const bool cast_value(value);
500 if (static_cast<int>(cast_value) != value) {
502 << "Gurobi unexpectedly returned non-boolean value for " << name
503 << ": " << value;
504 }
505 return cast_value;
506}
507
508class OrAccumulator {
509 public:
510 OrAccumulator() = default;
511 // Propagates any error in `update`. Otherwise, ORs the internal state with
512 // the value in `update`.
513 absl::Status Or(absl::StatusOr<bool> update) {
514 RETURN_IF_ERROR(update.status());
515 value_ |= *update;
516 return absl::OkStatus();
517 }
518 bool value() const { return value_; }
519
520 private:
521 bool value_ = false;
522};
523
524// Propagate any error in `maybe_value`. Otherwise, if `maybe_value` is set,
525// add (`id`, `maybe_value`) as an entry to `target`.
526absl::Status AddMapEntryIfPresent(
527 const int64_t map_id,
528 absl::StatusOr<std::optional<ModelSubsetProto::Bounds>> maybe_value,
529 google::protobuf::Map<int64_t, ModelSubsetProto::Bounds>& target) {
530 RETURN_IF_ERROR(maybe_value.status());
531 if (maybe_value->has_value()) {
532 target[map_id] = **std::move(maybe_value);
533 }
534 return absl::OkStatus();
535}
536
537// Propagate any error in `should_append`. Otherwise, if `should_append` is
538// true, append `id` to `target`.
539absl::Status AppendEntryIfTrue(
540 const int64_t id, absl::StatusOr<bool> should_append,
541 google::protobuf::RepeatedField<int64_t>& target) {
542 RETURN_IF_ERROR(should_append.status());
543 if (*should_append) {
544 target.Add(id);
545 }
546 return absl::OkStatus();
547}
548
549} // namespace
550
551GurobiSolver::GurobiSolver(std::unique_ptr<Gurobi> g_gurobi)
552 : gurobi_(std::move(g_gurobi)) {}
553
554GurobiSolver::GurobiModelElements
555GurobiSolver::LinearConstraintData::DependentElements() const {
556 GurobiModelElements elements;
557 CHECK_NE(constraint_index, kUnspecifiedConstraint);
558 elements.linear_constraints.push_back(constraint_index);
559 if (slack_index != kUnspecifiedIndex) {
560 elements.variables.push_back(slack_index);
561 }
562 return elements;
563}
564
565GurobiSolver::GurobiModelElements
566GurobiSolver::SecondOrderConeConstraintData::DependentElements() const {
567 const auto index_is_valid = [](const auto index) { return index >= 0; };
568 CHECK(absl::c_all_of(slack_variables, index_is_valid));
569 CHECK(absl::c_all_of(slack_constraints, index_is_valid));
570 CHECK_NE(constraint_index, kUnspecifiedConstraint);
571 GurobiModelElements elements{.variables = slack_variables,
572 .linear_constraints = slack_constraints};
573 elements.quadratic_constraints.push_back(constraint_index);
574 return elements;
575}
576
577GurobiSolver::GurobiModelElements
578GurobiSolver::SosConstraintData::DependentElements() const {
579 const auto index_is_valid = [](const auto index) { return index >= 0; };
580 CHECK(absl::c_all_of(slack_variables, index_is_valid));
581 CHECK(absl::c_all_of(slack_constraints, index_is_valid));
582 CHECK_NE(constraint_index, kUnspecifiedConstraint);
583 GurobiModelElements elements{.variables = slack_variables,
584 .linear_constraints = slack_constraints};
585 elements.sos_constraints.push_back(constraint_index);
586 return elements;
587}
588
589absl::StatusOr<TerminationProto> GurobiSolver::ConvertTerminationReason(
590 const int gurobi_status, const SolutionClaims solution_claims,
591 const double best_primal_bound, const double best_dual_bound) {
592 ASSIGN_OR_RETURN(const bool is_maximize, IsMaximize());
593 switch (gurobi_status) {
594 case GRB_OPTIMAL:
595 // TODO(b/290359402): it appears Gurobi could return an infinite
596 // best_dual_bound (e.g in Qp/Qc/Socp/multi-obj tests). If so, we could
597 // improve the bound by using the target absolute and relative GAPs (and
598 // best_primal_bound).
599 return OptimalTerminationProto(best_primal_bound, best_dual_bound);
600 case GRB_INFEASIBLE:
602 is_maximize, solution_claims.dual_feasible_solution_exists
603 ? FEASIBILITY_STATUS_FEASIBLE
604 : FEASIBILITY_STATUS_UNDETERMINED);
605 case GRB_UNBOUNDED:
606 // GRB_UNBOUNDED does necessarily imply the primal is feasible
607 // https://www.gurobi.com/documentation/9.1/refman/optimization_status_codes.html
608 if (solution_claims.primal_feasible_solution_exists) {
609 return UnboundedTerminationProto(is_maximize);
610 }
612 is_maximize,
613 /*dual_feasibility_status=*/FEASIBILITY_STATUS_INFEASIBLE,
614 "Gurobi status GRB_UNBOUNDED");
615 case GRB_INF_OR_UNBD:
617 is_maximize,
618 /*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED,
619 "Gurobi status GRB_UNBOUNDED");
620 case GRB_CUTOFF:
621 return CutoffTerminationProto(is_maximize, "Gurobi status GRB_CUTOFF");
624 LIMIT_ITERATION, best_primal_bound, best_dual_bound,
625 solution_claims.dual_feasible_solution_exists);
626 case GRB_NODE_LIMIT:
628 LIMIT_NODE, best_primal_bound, best_dual_bound,
629 solution_claims.dual_feasible_solution_exists);
630 case GRB_TIME_LIMIT:
632 LIMIT_TIME, best_primal_bound, best_dual_bound,
633 solution_claims.dual_feasible_solution_exists);
636 LIMIT_SOLUTION, best_primal_bound, best_dual_bound,
637 solution_claims.dual_feasible_solution_exists);
638 case GRB_INTERRUPTED:
640 LIMIT_INTERRUPTED, best_primal_bound, best_dual_bound,
641 solution_claims.dual_feasible_solution_exists);
642 case GRB_NUMERIC:
643 return TerminateForReason(is_maximize,
644 TERMINATION_REASON_NUMERICAL_ERROR);
645 case GRB_SUBOPTIMAL:
646 return TerminateForReason(is_maximize, TERMINATION_REASON_IMPRECISE);
648 // Note: maybe we should override
649 // solution_claims.primal_feasible_solution_exists to true or false
650 // depending on whether objective_limit and best_bound_limit triggered
651 // this. Not sure if it's possible to detect this though.
653 LIMIT_OBJECTIVE, best_primal_bound, best_dual_bound,
654 solution_claims.dual_feasible_solution_exists);
655 case GRB_LOADED:
656 return absl::InternalError(
657 "Error creating termination reason, unexpected gurobi status code "
658 "GRB_LOADED.");
659 case GRB_INPROGRESS:
660 return absl::InternalError(
661 "Error creating termination reason, unexpected gurobi status code "
662 "GRB_INPROGRESS.");
663 default:
664 return absl::InternalError(absl::StrCat(
665 "Missing Gurobi optimization status code case: ", gurobi_status));
666 }
667}
668
669absl::StatusOr<bool> GurobiSolver::IsMaximize() const {
670 ASSIGN_OR_RETURN(const int obj_sense,
671 gurobi_->GetIntAttr(GRB_INT_ATTR_MODELSENSE));
672 return obj_sense == GRB_MAXIMIZE;
673}
674
675absl::StatusOr<bool> GurobiSolver::IsMIP() const {
676 ASSIGN_OR_RETURN(const int is_mip, gurobi_->GetIntAttr(GRB_INT_ATTR_IS_MIP));
677 return static_cast<bool>(is_mip);
678}
679
680// TODO(b/204595455): Revisit logic when nonconvex QP support is decided upon
681absl::StatusOr<bool> GurobiSolver::IsQP() const {
682 ASSIGN_OR_RETURN(const int is_qp, gurobi_->GetIntAttr(GRB_INT_ATTR_IS_QP));
683 return static_cast<bool>(is_qp);
684}
685
686absl::StatusOr<bool> GurobiSolver::IsQCP() const {
687 ASSIGN_OR_RETURN(const int is_qcp, gurobi_->GetIntAttr(GRB_INT_ATTR_IS_QCP));
688 return static_cast<bool>(is_qcp);
689}
690
691// TODO(user): switch the use of this function to something closer to
692// GetGurobiDualRay()
693template <typename T>
694void GurobiSolver::GurobiVectorToSparseDoubleVector(
695 const absl::Span<const double> gurobi_values, const T& map,
696 SparseDoubleVectorProto& result,
697 const SparseVectorFilterProto& filter) const {
698 SparseVectorFilterPredicate predicate(filter);
699 for (auto [id, gurobi_data] : map) {
700 const double value = gurobi_values[get_model_index(gurobi_data)];
701 if (predicate.AcceptsAndUpdate(id, value)) {
702 result.add_ids(id);
703 result.add_values(value);
704 }
705 }
706}
707
708absl::Status GurobiSolver::SetGurobiBasis(const BasisProto& basis) {
709 std::vector<int> gurobi_variable_basis_status(num_gurobi_variables_);
710 for (const auto [id, value] : MakeView(basis.variable_status())) {
711 gurobi_variable_basis_status[variables_map_.at(id)] =
712 GrbVariableStatus(static_cast<BasisStatusProto>(value));
713 }
714
715 std::vector<int> gurobi_constraint_basis_status;
716 gurobi_constraint_basis_status.reserve(num_gurobi_lin_cons_);
717 for (const auto [id, value] : MakeView(basis.constraint_status())) {
718 const LinearConstraintData& constraint_data =
719 linear_constraints_map_.at(id);
720 // Non-ranged constraints
721 if (constraint_data.slack_index == kUnspecifiedIndex) {
722 if (value == BASIS_STATUS_BASIC) {
723 gurobi_constraint_basis_status.push_back(kGrbBasicConstraint);
724 } else {
725 gurobi_constraint_basis_status.push_back(kGrbNonBasicConstraint);
726 }
727 // Ranged constraints
728 } else if (value == BASIS_STATUS_BASIC) {
729 // Either constraint or MathOpt slack is basic, but not both (because
730 // columns for MathOpt slack and internal Gurobi slack are linearly
731 // dependent). We choose the MathOpt slack to be basic.
732 gurobi_variable_basis_status[constraint_data.slack_index] = GRB_BASIC;
733 gurobi_constraint_basis_status.push_back(kGrbNonBasicConstraint);
734 } else {
735 gurobi_variable_basis_status[constraint_data.slack_index] =
736 GrbVariableStatus(static_cast<BasisStatusProto>(value));
737 gurobi_constraint_basis_status.push_back(kGrbNonBasicConstraint);
738 }
739 }
740 RETURN_IF_ERROR(gurobi_->SetIntAttrArray(GRB_INT_ATTR_VBASIS,
741 gurobi_variable_basis_status));
742 RETURN_IF_ERROR(gurobi_->SetIntAttrArray(GRB_INT_ATTR_CBASIS,
743 gurobi_constraint_basis_status));
744 return absl::OkStatus();
745}
746
747absl::StatusOr<BasisProto> GurobiSolver::GetGurobiBasis() {
748 BasisProto basis;
750 const std::vector<int> gurobi_variable_basis_status,
751 gurobi_->GetIntAttrArray(GRB_INT_ATTR_VBASIS, num_gurobi_variables_));
752
753 for (auto [variable_id, gurobi_variable_index] : variables_map_) {
754 basis.mutable_variable_status()->add_ids(variable_id);
755 const BasisStatusProto variable_status = ConvertVariableStatus(
756 gurobi_variable_basis_status[gurobi_variable_index]);
757 if (variable_status == BASIS_STATUS_UNSPECIFIED) {
758 return absl::InternalError(
759 absl::StrCat("Invalid Gurobi variable basis status: ",
760 gurobi_variable_basis_status[gurobi_variable_index]));
761 }
762 basis.mutable_variable_status()->add_values(variable_status);
763 }
764
766 const std::vector<int> gurobi_constraint_basis_status,
767 gurobi_->GetIntAttrArray(GRB_INT_ATTR_CBASIS, num_gurobi_lin_cons_));
768 for (auto [constraint_id, gurobi_data] : linear_constraints_map_) {
769 basis.mutable_constraint_status()->add_ids(constraint_id);
770 const int gurobi_constraint_status =
771 gurobi_constraint_basis_status[gurobi_data.constraint_index];
772 if ((gurobi_constraint_status != kGrbBasicConstraint) &&
773 (gurobi_constraint_status != kGrbNonBasicConstraint)) {
774 return absl::InternalError(
775 absl::StrCat("Invalid Gurobi constraint basis status: ",
776 gurobi_constraint_status));
777 }
778 // linear_terms <= upper_bound
779 if (gurobi_data.lower_bound <= -GRB_INFINITY &&
780 gurobi_data.upper_bound < GRB_INFINITY) {
781 if (gurobi_constraint_status == kGrbBasicConstraint) {
782 basis.mutable_constraint_status()->add_values(BASIS_STATUS_BASIC);
783 } else {
784 basis.mutable_constraint_status()->add_values(
785 BASIS_STATUS_AT_UPPER_BOUND);
786 }
787 // linear_terms >= lower_bound
788 } else if (gurobi_data.lower_bound > -GRB_INFINITY &&
789 gurobi_data.upper_bound >= GRB_INFINITY) {
790 if (gurobi_constraint_status == kGrbBasicConstraint) {
791 basis.mutable_constraint_status()->add_values(BASIS_STATUS_BASIC);
792 } else {
793 basis.mutable_constraint_status()->add_values(
794 BASIS_STATUS_AT_LOWER_BOUND);
795 }
796 // linear_terms == xxxxx_bound
797 } else if (gurobi_data.lower_bound == gurobi_data.upper_bound) {
798 if (gurobi_constraint_status == kGrbBasicConstraint) {
799 basis.mutable_constraint_status()->add_values(BASIS_STATUS_BASIC);
800 } else {
801 // TODO(user): consider refining this to
802 // AT_LOWER_BOUND/AT_UPPER_BOUND using the sign of the dual variable.
803 basis.mutable_constraint_status()->add_values(BASIS_STATUS_FIXED_VALUE);
804 }
805 // linear_term - slack == 0 (ranged constraint)
806 } else {
807 const BasisStatusProto slack_status = ConvertVariableStatus(
808 gurobi_variable_basis_status[gurobi_data.slack_index]);
809 if (slack_status == BASIS_STATUS_UNSPECIFIED) {
810 return absl::InternalError(absl::StrCat(
811 "Invalid Gurobi slack variable basis status: ", slack_status));
812 }
813 if ((gurobi_constraint_status == kGrbBasicConstraint) ||
814 (slack_status == BASIS_STATUS_BASIC)) {
815 basis.mutable_constraint_status()->add_values(BASIS_STATUS_BASIC);
816 } else {
817 basis.mutable_constraint_status()->add_values(slack_status);
818 }
819 }
820 }
821 return basis;
822}
823
824absl::StatusOr<DualRayProto> GurobiSolver::GetGurobiDualRay(
825 const SparseVectorFilterProto& linear_constraints_filter,
826 const SparseVectorFilterProto& variables_filter, const bool is_maximize) {
827 // farkas_dual = lambda
828 ASSIGN_OR_RETURN(const std::vector<double> farkas_dual,
829 gurobi_->GetDoubleAttrArray(GRB_DBL_ATTR_FARKASDUAL,
830 num_gurobi_lin_cons_));
831
832 DualRayProto dual_ray;
833
834 // Compute y = -lambda
835 {
836 SparseVectorFilterPredicate predicate(linear_constraints_filter);
837 for (auto [constraint_id, gurobi_data] : linear_constraints_map_) {
838 // constraint_dual_value = y[constraint_id]
839 const double value = -farkas_dual[gurobi_data.constraint_index];
840 if (predicate.AcceptsAndUpdate(constraint_id, value)) {
841 dual_ray.mutable_dual_values()->add_ids(constraint_id);
842 if (is_maximize) {
843 dual_ray.mutable_dual_values()->add_values(-value);
844 } else {
845 dual_ray.mutable_dual_values()->add_values(value);
846 }
847 }
848 }
849 }
850
851 // Compute r = \bar{a} = A^T lambda
852 {
853 SparseVectorFilterPredicate predicate(variables_filter);
854 for (auto [var_id, gurobi_variable_index] : variables_map_) {
855 // reduced_cost_value = r[gurobi_variable_index]
856 // = \bar{a}[gurobi_variable_index]
857 double reduced_cost_value = 0.0;
858 ASSIGN_OR_RETURN(Gurobi::SparseMat column,
859 gurobi_->GetVars(gurobi_variable_index, 1));
860 for (int i = 0; i < column.inds.size(); ++i) {
861 reduced_cost_value += farkas_dual[column.inds[i]] * column.vals[i];
862 }
863 if (predicate.AcceptsAndUpdate(var_id, reduced_cost_value)) {
864 dual_ray.mutable_reduced_costs()->add_ids(var_id);
865 if (is_maximize) {
866 dual_ray.mutable_reduced_costs()->add_values(-reduced_cost_value);
867 } else {
868 dual_ray.mutable_reduced_costs()->add_values(reduced_cost_value);
869 }
870 }
871 }
872 }
873 return dual_ray;
874}
875
876absl::StatusOr<SolveResultProto> GurobiSolver::ExtractSolveResultProto(
877 const absl::Time start, const ModelSolveParametersProto& model_parameters) {
878 SolveResultProto result;
879 ASSIGN_OR_RETURN(const int grb_termination,
880 gurobi_->GetIntAttr(GRB_INT_ATTR_STATUS));
881 SolutionClaims solution_claims;
882 ASSIGN_OR_RETURN(SolutionsAndClaims solution_and_claims,
883 GetSolutions(model_parameters));
884 if (grb_termination == GRB_CUTOFF) {
885 // Cutoff will not be triggered by bounds e.g. for LP dual feasible
886 // solutions. In particular, if the problem is both primal and dual
887 // infeasible, we will not get a bound and should not be returning CUTOFF.
888 //
889 // TODO(b/272268188): test that this has no bad interactions with primal +
890 // dual infeasible problems.
891 solution_and_claims.solutions.clear();
892 solution_and_claims.solution_claims.primal_feasible_solution_exists = false;
893 solution_and_claims.solution_claims.dual_feasible_solution_exists = true;
894 }
895 ASSIGN_OR_RETURN(const double best_primal_bound,
896 GetBestPrimalBound(solution_and_claims.solutions));
897 ASSIGN_OR_RETURN(const double best_dual_bound,
898 GetBestDualBound(solution_and_claims.solutions));
899 solution_claims = solution_and_claims.solution_claims;
900
901 // TODO(b/195295177): Add tests for rays in unbounded MIPs
902 RETURN_IF_ERROR(FillRays(model_parameters, solution_claims, result));
903
904 for (SolutionProto& solution : solution_and_claims.solutions) {
905 *result.add_solutions() = std::move(solution);
906 }
907 // }
908
910 *result.mutable_termination(),
911 ConvertTerminationReason(grb_termination, solution_claims,
912 best_primal_bound, best_dual_bound));
913
914 ASSIGN_OR_RETURN(*result.mutable_solve_stats(), GetSolveStats(start));
915 return std::move(result);
916}
917
918absl::StatusOr<bool> GurobiSolver::AnyElementInIIS(
919 const GurobiModelElements& grb_elements) {
920 OrAccumulator any_in_iis;
921 for (const GurobiVariableIndex grb_index : grb_elements.variables) {
922 RETURN_IF_ERROR(any_in_iis.Or(VariableInIIS(grb_index)));
923 }
924 for (const GurobiLinearConstraintIndex grb_index :
925 grb_elements.linear_constraints) {
926 RETURN_IF_ERROR(any_in_iis.Or(
927 GetIntAttrElementAsBool(*gurobi_, GRB_INT_ATTR_IIS_CONSTR, grb_index)));
928 }
929 for (const GurobiQuadraticConstraintIndex grb_index :
930 grb_elements.quadratic_constraints) {
931 RETURN_IF_ERROR(any_in_iis.Or(GetIntAttrElementAsBool(
932 *gurobi_, GRB_INT_ATTR_IIS_QCONSTR, grb_index)));
933 }
934 for (const GurobiSosConstraintIndex grb_index :
935 grb_elements.sos_constraints) {
936 RETURN_IF_ERROR(any_in_iis.Or(
937 GetIntAttrElementAsBool(*gurobi_, GRB_INT_ATTR_IIS_SOS, grb_index)));
938 }
939 for (const GurobiGeneralConstraintIndex grb_index :
940 grb_elements.general_constraints) {
941 RETURN_IF_ERROR(any_in_iis.Or(GetIntAttrElementAsBool(
942 *gurobi_, GRB_INT_ATTR_IIS_GENCONSTR, grb_index)));
943 }
944 return any_in_iis.value();
945}
946
947// If set, the returned value will have at least one of .lower and/or .upper set
948// to true.
949absl::StatusOr<std::optional<ModelSubsetProto::Bounds>>
950GurobiSolver::VariableBoundsInIIS(const GurobiVariableIndex grb_index) {
952 const bool var_lb_is_in_iis,
953 GetIntAttrElementAsBool(*gurobi_, GRB_INT_ATTR_IIS_LB, grb_index));
955 const bool var_ub_is_in_iis,
956 GetIntAttrElementAsBool(*gurobi_, GRB_INT_ATTR_IIS_UB, grb_index));
957 if (!var_lb_is_in_iis && !var_ub_is_in_iis) {
958 return std::nullopt;
959 }
960 ModelSubsetProto::Bounds bounds;
961 bounds.set_lower(var_lb_is_in_iis);
962 bounds.set_upper(var_ub_is_in_iis);
963 return bounds;
964}
965
966absl::StatusOr<bool> GurobiSolver::VariableInIIS(
967 const GurobiVariableIndex grb_index) {
968 ASSIGN_OR_RETURN(const std::optional<ModelSubsetProto::Bounds> bounds,
969 VariableBoundsInIIS(grb_index));
970 return bounds.has_value();
971}
972
973absl::StatusOr<std::optional<ModelSubsetProto::Bounds>>
974GurobiSolver::LinearConstraintInIIS(const LinearConstraintData& grb_data) {
975 // Attributing which part of an equality/ranged constraint is part of the
976 // IIS can be tricky. So, we take a conservative approach: if anything
977 // associated with this linear constraint is part of the IIS (including
978 // slack variable/constraints), then we mark any finite bound as being part
979 // of the IIS.
980 ASSIGN_OR_RETURN(const bool constr_in_iis,
981 AnyElementInIIS(grb_data.DependentElements()));
982 if (!constr_in_iis) {
983 return std::nullopt;
984 }
985 ModelSubsetProto::Bounds result;
986 result.set_lower(grb_data.lower_bound != -kInf);
987 result.set_upper(grb_data.upper_bound != kInf);
988 return result;
989}
990
991absl::StatusOr<std::optional<ModelSubsetProto::Bounds>>
992GurobiSolver::QuadraticConstraintInIIS(
993 const GurobiQuadraticConstraintIndex grb_index) {
995 const bool constr_in_iis,
996 GetIntAttrElementAsBool(*gurobi_, GRB_INT_ATTR_IIS_QCONSTR, grb_index));
997 if (!constr_in_iis) {
998 return std::nullopt;
999 }
1001 const char constr_sense,
1002 gurobi_->GetCharAttrElement(GRB_CHAR_ATTR_QCSENSE, grb_index));
1003 ModelSubsetProto::Bounds result;
1004 result.set_lower(constr_sense == GRB_EQUAL ||
1005 constr_sense == GRB_GREATER_EQUAL);
1006 result.set_upper(constr_sense == GRB_EQUAL || constr_sense == GRB_LESS_EQUAL);
1007 return result;
1008}
1009
1010absl::StatusOr<ComputeInfeasibleSubsystemResultProto>
1011GurobiSolver::ExtractComputeInfeasibleSubsystemResultProto(
1012 const bool proven_infeasible) {
1013 ComputeInfeasibleSubsystemResultProto result;
1014 if (!proven_infeasible) {
1015 result.set_feasibility(FEASIBILITY_STATUS_UNDETERMINED);
1016 return result;
1017 }
1018 result.set_feasibility(FEASIBILITY_STATUS_INFEASIBLE);
1019 {
1020 ASSIGN_OR_RETURN(const bool is_minimal,
1021 gurobi_->GetIntAttr(GRB_INT_ATTR_IIS_MINIMAL));
1022 result.set_is_minimal(is_minimal);
1023 }
1024 for (const auto [id, grb_index] : variables_map_) {
1025 RETURN_IF_ERROR(AddMapEntryIfPresent(
1026 id, /*maybe_value=*/VariableBoundsInIIS(grb_index),
1027 *result.mutable_infeasible_subsystem()->mutable_variable_bounds()));
1028 {
1029 // Gurobi does not report integrality in its IIS, so we mark any integer
1030 // variable in the model as being involved.
1032 const char var_type,
1033 gurobi_->GetCharAttrElement(GRB_CHAR_ATTR_VTYPE, grb_index));
1034 if (var_type == GRB_BINARY || var_type == GRB_INTEGER) {
1035 result.mutable_infeasible_subsystem()->add_variable_integrality(
1036 grb_index);
1037 }
1038 }
1039 }
1040
1041 for (const auto [id, grb_data] : linear_constraints_map_) {
1042 RETURN_IF_ERROR(AddMapEntryIfPresent(
1043 id, /*maybe_value=*/LinearConstraintInIIS(grb_data),
1044 *result.mutable_infeasible_subsystem()->mutable_linear_constraints()));
1045 }
1046
1047 for (const auto [id, grb_index] : quadratic_constraints_map_) {
1048 RETURN_IF_ERROR(AddMapEntryIfPresent(
1049 id, /*maybe_value=*/QuadraticConstraintInIIS(grb_index),
1050 *result.mutable_infeasible_subsystem()
1051 ->mutable_quadratic_constraints()));
1052 }
1053
1054 for (const auto& [id, grb_data] : soc_constraints_map_) {
1055 RETURN_IF_ERROR(AppendEntryIfTrue(
1056 id, /*should_append=*/AnyElementInIIS(grb_data.DependentElements()),
1057 *result.mutable_infeasible_subsystem()
1058 ->mutable_second_order_cone_constraints()));
1059 }
1060
1061 for (const auto& [id, grb_data] : sos1_constraints_map_) {
1062 RETURN_IF_ERROR(AppendEntryIfTrue(
1063 id, /*should_append=*/AnyElementInIIS(grb_data.DependentElements()),
1064 *result.mutable_infeasible_subsystem()->mutable_sos1_constraints()));
1065 }
1066
1067 for (const auto& [id, grb_data] : sos2_constraints_map_) {
1068 RETURN_IF_ERROR(AppendEntryIfTrue(
1069 id, /*should_append=*/AnyElementInIIS(grb_data.DependentElements()),
1070 *result.mutable_infeasible_subsystem()->mutable_sos2_constraints()));
1071 }
1072
1073 for (const auto& [id, maybe_grb_data] : indicator_constraints_map_) {
1074 if (!maybe_grb_data.has_value()) {
1075 continue;
1076 }
1077 RETURN_IF_ERROR(AppendEntryIfTrue(
1078 id,
1079 /*should_append=*/
1080 GetIntAttrElementAsBool(*gurobi_, GRB_INT_ATTR_IIS_GENCONSTR,
1081 maybe_grb_data->constraint_index),
1082 *result.mutable_infeasible_subsystem()
1083 ->mutable_indicator_constraints()));
1084 }
1085
1086 // Since each xxx_map_ is in insertion order, we do not need to worry about
1087 // sorting the repeated fields in `result`.
1088 return result;
1089}
1090
1091absl::StatusOr<GurobiSolver::SolutionsAndClaims> GurobiSolver::GetSolutions(
1092 const ModelSolveParametersProto& model_parameters) {
1093 // Note that all multi-objective models will have `IsMip()` return true.
1094 ASSIGN_OR_RETURN(const bool is_mip, IsMIP());
1095 ASSIGN_OR_RETURN(const bool is_qp, IsQP());
1096 ASSIGN_OR_RETURN(const bool is_qcp, IsQCP());
1097
1098 if (is_mip) {
1099 return GetMipSolutions(model_parameters);
1100 } else if (is_qcp) {
1101 return GetQcpSolution(model_parameters);
1102 } else if (is_qp) {
1103 return GetQpSolution(model_parameters);
1104 } else {
1105 return GetLpSolution(model_parameters);
1106 }
1107}
1108
1109absl::StatusOr<SolveStatsProto> GurobiSolver::GetSolveStats(
1110 const absl::Time start) const {
1111 SolveStatsProto solve_stats;
1112
1113 CHECK_OK(util_time::EncodeGoogleApiProto(absl::Now() - start,
1114 solve_stats.mutable_solve_time()));
1115
1116 if (gurobi_->IsAttrAvailable(GRB_DBL_ATTR_ITERCOUNT)) {
1117 ASSIGN_OR_RETURN(const double simplex_iters_double,
1118 gurobi_->GetDoubleAttr(GRB_DBL_ATTR_ITERCOUNT));
1119 ASSIGN_OR_RETURN(const int64_t simplex_iters,
1120 SafeInt64FromDouble(simplex_iters_double));
1121 solve_stats.set_simplex_iterations(simplex_iters);
1122 }
1123
1124 if (gurobi_->IsAttrAvailable(GRB_INT_ATTR_BARITERCOUNT)) {
1125 ASSIGN_OR_RETURN(const int barrier_iters,
1126 gurobi_->GetIntAttr(GRB_INT_ATTR_BARITERCOUNT));
1127 solve_stats.set_barrier_iterations(barrier_iters);
1128 }
1129
1130 if (gurobi_->IsAttrAvailable(GRB_DBL_ATTR_NODECOUNT)) {
1131 ASSIGN_OR_RETURN(const double nodes_double,
1132 gurobi_->GetDoubleAttr(GRB_DBL_ATTR_NODECOUNT));
1133 ASSIGN_OR_RETURN(const int64_t nodes, SafeInt64FromDouble(nodes_double));
1134 solve_stats.set_node_count(nodes);
1135 }
1136 return solve_stats;
1137}
1138
1139absl::StatusOr<GurobiSolver::SolutionsAndClaims> GurobiSolver::GetMipSolutions(
1140 const ModelSolveParametersProto& model_parameters) {
1141 int num_solutions = 0;
1142 if (gurobi_->IsAttrAvailable(GRB_INT_ATTR_SOLCOUNT)) {
1143 ASSIGN_OR_RETURN(num_solutions, gurobi_->GetIntAttr(GRB_INT_ATTR_SOLCOUNT));
1144 }
1145 std::vector<SolutionProto> solutions;
1146 solutions.reserve(num_solutions);
1147
1148 for (int i = 0; i < num_solutions; ++i) {
1149 RETURN_IF_ERROR(gurobi_->SetIntParam(GRB_INT_PAR_SOLUTIONNUMBER, i));
1150
1151 PrimalSolutionProto primal_solution;
1152 ASSIGN_OR_RETURN(const double sol_val,
1153 gurobi_->GetDoubleAttr(GRB_DBL_ATTR_POOLOBJVAL));
1154 primal_solution.set_objective_value(sol_val);
1155 if (is_multi_objective_mode()) {
1156 for (const auto [id, grb_index] : multi_objectives_map_) {
1157 RETURN_IF_ERROR(gurobi_->SetIntParam(GRB_INT_PAR_OBJNUMBER, grb_index));
1158 ASSIGN_OR_RETURN(const double obj_val,
1159 gurobi_->GetDoubleAttr(GRB_DBL_ATTR_OBJNVAL));
1160 // If unset, this is the primary objective. We have already queried its
1161 // value via PoolObjVal above.
1162 if (id.has_value()) {
1163 (*primal_solution.mutable_auxiliary_objective_values())[*id] =
1164 obj_val;
1165 }
1166 }
1167 }
1168 // Gurobi v9 provides a feasibility status for the instance as a whole but
1169 // not for each solution, and pool entries may be infeasible. To be
1170 // conservative, we only label the first ("best") solution as primal
1171 // feasible.
1172 primal_solution.set_feasibility_status(
1173 i == 0 ? SOLUTION_STATUS_FEASIBLE : SOLUTION_STATUS_UNDETERMINED);
1175 const std::vector<double> grb_var_values,
1176 gurobi_->GetDoubleAttrArray(GRB_DBL_ATTR_XN, num_gurobi_variables_));
1177 GurobiVectorToSparseDoubleVector(grb_var_values, variables_map_,
1178 *primal_solution.mutable_variable_values(),
1179 model_parameters.variable_values_filter());
1180 *solutions.emplace_back(SolutionProto()).mutable_primal_solution() =
1181 std::move(primal_solution);
1182 }
1183
1184 ASSIGN_OR_RETURN(const int grb_termination,
1185 gurobi_->GetIntAttr(GRB_INT_ATTR_STATUS));
1186 // Set solution claims
1187 ASSIGN_OR_RETURN(const double best_dual_bound, GetGurobiBestDualBound());
1188 // Note: here the existence of a dual solution refers to a dual solution to
1189 // some convex relaxation of the MIP. This convex relaxation can likely be
1190 // interpreted as an LP between the LP relaxation of the MIP and the convex
1191 // hull of feasible solutions of the MIP. However, here we only use the fact
1192 // that best_dual_bound being finite implies the existence of the trivial
1193 // convex relaxation given by (assuming a minimization problem with objective
1194 // function c^T x): min{c^T x : c^T x >= best_dual_bound}.
1195 //
1196 // If this is a multi-objective model, Gurobi v10 does not expose ObjBound.
1197 // Instead, we fake its existence for optimal solves only.
1198 // By convention infeasible MIPs are always dual feasible.
1199 const SolutionClaims solution_claims = {
1200 .primal_feasible_solution_exists = num_solutions > 0,
1201 .dual_feasible_solution_exists =
1202 std::isfinite(best_dual_bound) || grb_termination == GRB_INFEASIBLE ||
1203 (is_multi_objective_mode() && grb_termination == GRB_OPTIMAL)};
1204
1205 // Check consistency of solutions, bounds and statuses.
1206 if (grb_termination == GRB_OPTIMAL && num_solutions == 0) {
1207 return absl::InternalError(
1208 "GRB_INT_ATTR_STATUS == GRB_OPTIMAL, but solution pool is empty.");
1209 }
1210 // As set above, in multi-objective mode the dual bound is not informative and
1211 // it will not pass this validation.
1212 if (!is_multi_objective_mode() && grb_termination == GRB_OPTIMAL &&
1213 !std::isfinite(best_dual_bound)) {
1214 return absl::InternalError(
1215 "GRB_INT_ATTR_STATUS == GRB_OPTIMAL, but GRB_DBL_ATTR_OBJBOUND is "
1216 "unavailable or infinite.");
1217 }
1218
1219 return SolutionsAndClaims{.solutions = std::move(solutions),
1220 .solution_claims = solution_claims};
1221}
1222
1223absl::StatusOr<GurobiSolver::SolutionAndClaim<PrimalSolutionProto>>
1224GurobiSolver::GetConvexPrimalSolutionIfAvailable(
1225 const ModelSolveParametersProto& model_parameters) {
1226 if (!gurobi_->IsAttrAvailable(GRB_DBL_ATTR_X)) {
1227 return SolutionAndClaim<PrimalSolutionProto>{
1228 .solution = std::nullopt, .feasible_solution_exists = false};
1229 }
1230 ASSIGN_OR_RETURN(const int grb_termination,
1231 gurobi_->GetIntAttr(GRB_INT_ATTR_STATUS));
1232
1233 // Get primal solutions if available.
1235 const std::vector<double> grb_var_values,
1236 gurobi_->GetDoubleAttrArray(GRB_DBL_ATTR_X, num_gurobi_variables_));
1237
1238 PrimalSolutionProto primal_solution;
1239 // The objective value may be missing for primal feasible solutions for
1240 // unbounded problems.
1241 // TODO(b/195295177): for GRB_ITERATION_LIMIT an objective value of 0.0 is
1242 // returned which breaks LpIncompleteSolveTest.PrimalSimplexAlgorithm. Explore
1243 // more and make simple example to file a bug.
1244 if (gurobi_->IsAttrAvailable(GRB_DBL_ATTR_OBJVAL) &&
1245 grb_termination != GRB_ITERATION_LIMIT) {
1246 ASSIGN_OR_RETURN(const double sol_val,
1247 gurobi_->GetDoubleAttr(GRB_DBL_ATTR_OBJVAL));
1248 primal_solution.set_objective_value(sol_val);
1249 } else {
1250 double objective_value = 0.0;
1252 const std::vector<double> linear_obj_coefs,
1253 gurobi_->GetDoubleAttrArray(GRB_DBL_ATTR_OBJ, num_gurobi_variables_));
1254 for (int i = 0; i < num_gurobi_variables_; ++i) {
1255 objective_value += linear_obj_coefs[i] * grb_var_values[i];
1256 }
1257 primal_solution.set_objective_value(objective_value);
1258 }
1259
1260 primal_solution.set_feasibility_status(SOLUTION_STATUS_UNDETERMINED);
1261 if (grb_termination == GRB_OPTIMAL) {
1262 primal_solution.set_feasibility_status(SOLUTION_STATUS_FEASIBLE);
1263 } else if (grb_termination == GRB_INFEASIBLE) {
1264 primal_solution.set_feasibility_status(SOLUTION_STATUS_INFEASIBLE);
1265 } else if (PrimalSolutionQualityAvailable()) {
1266 ASSIGN_OR_RETURN(const double solution_quality, GetPrimalSolutionQuality());
1267 ASSIGN_OR_RETURN(const double tolerance,
1268 gurobi_->GetDoubleParam(GRB_DBL_PAR_FEASIBILITYTOL));
1269 if (solution_quality <= tolerance) {
1270 primal_solution.set_feasibility_status(SOLUTION_STATUS_FEASIBLE);
1271 } else {
1272 primal_solution.set_feasibility_status(SOLUTION_STATUS_INFEASIBLE);
1273 }
1274 }
1275
1276 GurobiVectorToSparseDoubleVector(grb_var_values, variables_map_,
1277 *primal_solution.mutable_variable_values(),
1278 model_parameters.variable_values_filter());
1279 const bool primal_feasible_solution_exists =
1280 (primal_solution.feasibility_status() == SOLUTION_STATUS_FEASIBLE);
1281 return SolutionAndClaim<PrimalSolutionProto>{
1282 .solution = std::move(primal_solution),
1283 .feasible_solution_exists = primal_feasible_solution_exists};
1284}
1285
1286bool GurobiSolver::PrimalSolutionQualityAvailable() const {
1287 return gurobi_->IsAttrAvailable(GRB_DBL_ATTR_CONSTR_RESIDUAL) &&
1288 gurobi_->IsAttrAvailable(GRB_DBL_ATTR_CONSTR_VIO) &&
1289 gurobi_->IsAttrAvailable(GRB_DBL_ATTR_BOUND_VIO) &&
1290 gurobi_->IsAttrAvailable(GRB_DBL_ATTR_CONSTR_SRESIDUAL) &&
1291 gurobi_->IsAttrAvailable(GRB_DBL_ATTR_CONSTR_SVIO) &&
1292 gurobi_->IsAttrAvailable(GRB_DBL_ATTR_BOUND_SVIO);
1293}
1294
1295absl::StatusOr<double> GurobiSolver::GetPrimalSolutionQuality() const {
1296 ASSIGN_OR_RETURN(const double constraint_residual,
1297 gurobi_->GetDoubleAttr(GRB_DBL_ATTR_CONSTR_RESIDUAL));
1298 ASSIGN_OR_RETURN(const double constraint_violation,
1299 gurobi_->GetDoubleAttr(GRB_DBL_ATTR_CONSTR_VIO));
1300 ASSIGN_OR_RETURN(const double bound_violation,
1301 gurobi_->GetDoubleAttr(GRB_DBL_ATTR_BOUND_VIO));
1302 ASSIGN_OR_RETURN(const double constraint_scaled_residual,
1303 gurobi_->GetDoubleAttr(GRB_DBL_ATTR_CONSTR_SRESIDUAL));
1304 ASSIGN_OR_RETURN(const double constraint_scaled_violation,
1305 gurobi_->GetDoubleAttr(GRB_DBL_ATTR_CONSTR_SVIO));
1306 ASSIGN_OR_RETURN(const double bound_scaled_violation,
1307 gurobi_->GetDoubleAttr(GRB_DBL_ATTR_BOUND_SVIO));
1308 return std::max({constraint_residual, constraint_violation, bound_violation,
1309 constraint_scaled_residual, constraint_scaled_violation,
1310 bound_scaled_violation});
1311}
1312
1313absl::StatusOr<double> GurobiSolver::GetBestPrimalBound(
1314 absl::Span<const SolutionProto> solutions) const {
1315 // We avoid using GRB_DBL_ATTR_OBJVAL because it may be incorrect on early
1316 // termination and for infeasible solutions (as of Gurobi 9.0.1).
1317 // Note that for (primal) unbounded problems the primal_bound is correctly
1318 // filled by ConvertTerminationReason() possibly overriding the value
1319 // returned by GetBestPrimalBound().
1320 ASSIGN_OR_RETURN(const bool is_maximize, IsMaximize());
1321 double best_objective_value = is_maximize ? -kInf : kInf;
1322 for (const SolutionProto& solution : solutions) {
1323 if (solution.has_primal_solution() &&
1324 solution.primal_solution().feasibility_status() ==
1325 SOLUTION_STATUS_FEASIBLE) {
1326 const double sol_val = solution.primal_solution().objective_value();
1327 best_objective_value = is_maximize
1328 ? std::max(best_objective_value, sol_val)
1329 : std::min(best_objective_value, sol_val);
1330 }
1331 }
1332 return best_objective_value;
1333}
1334
1335absl::StatusOr<double> GurobiSolver::GetBestDualBound(
1336 const std::vector<SolutionProto>& solutions) const {
1337 ASSIGN_OR_RETURN(const bool is_maximize, IsMaximize());
1338 // GetGurobiBestDualBound() returns the correct bound for problems without
1339 // dual solutions (e.g. MIP).
1340 ASSIGN_OR_RETURN(double best_dual_bound, GetGurobiBestDualBound());
1341 // However, GetGurobiBestDualBound() may be incorrect for QP problems.
1342 for (const SolutionProto& solution : solutions) {
1343 if (solution.has_dual_solution() &&
1344 solution.dual_solution().feasibility_status() ==
1345 SOLUTION_STATUS_FEASIBLE &&
1346 solution.dual_solution().has_objective_value()) {
1347 const double sol_val = solution.dual_solution().objective_value();
1348 best_dual_bound = is_maximize ? std::min(best_dual_bound, sol_val)
1349 : std::max(best_dual_bound, sol_val);
1350 }
1351 }
1352 return best_dual_bound;
1353}
1354
1355absl::StatusOr<double> GurobiSolver::GetGurobiBestDualBound() const {
1356 // As of v9.0.2, on multi objective models Gurobi incorrectly reports that
1357 // ObjBound is available. We work around this by adding a check if we are in
1358 // multi objective mode.
1359 if (gurobi_->IsAttrAvailable(GRB_DBL_ATTR_OBJBOUND) &&
1360 !is_multi_objective_mode()) {
1361 ASSIGN_OR_RETURN(const double obj_bound,
1362 gurobi_->GetDoubleAttr(GRB_DBL_ATTR_OBJBOUND));
1363 // Note: Unbounded models return GRB_DBL_ATTR_OBJBOUND = GRB_INFINITY so
1364 // the conversion of +/-GRB_INFINITY to +/-kInf is needed and consistent.
1365 if (std::abs(obj_bound) < GRB_INFINITY) {
1366 return obj_bound;
1367 }
1368 }
1369 ASSIGN_OR_RETURN(const bool is_maximize, IsMaximize());
1370 return is_maximize ? kInf : -kInf;
1371}
1372
1373absl::StatusOr<std::optional<BasisProto>> GurobiSolver::GetBasisIfAvailable() {
1374 if (gurobi_->IsAttrAvailable(GRB_INT_ATTR_VBASIS) &&
1375 gurobi_->IsAttrAvailable(GRB_INT_ATTR_CBASIS)) {
1376 ASSIGN_OR_RETURN(BasisProto basis, GetGurobiBasis());
1377 ASSIGN_OR_RETURN(const int grb_termination,
1378 gurobi_->GetIntAttr(GRB_INT_ATTR_STATUS));
1379 basis.set_basic_dual_feasibility(SOLUTION_STATUS_UNDETERMINED);
1380 if (grb_termination == GRB_OPTIMAL) {
1381 basis.set_basic_dual_feasibility(SOLUTION_STATUS_FEASIBLE);
1382 } else if (grb_termination == GRB_UNBOUNDED) {
1383 basis.set_basic_dual_feasibility(SOLUTION_STATUS_INFEASIBLE);
1384 }
1385 // TODO(b/195295177): double check if the move is needed
1386 return std::move(basis);
1387 }
1388 return std::nullopt;
1389}
1390
1391absl::StatusOr<GurobiSolver::SolutionsAndClaims> GurobiSolver::GetLpSolution(
1392 const ModelSolveParametersProto& model_parameters) {
1393 ASSIGN_OR_RETURN(auto primal_solution_and_claim,
1394 GetConvexPrimalSolutionIfAvailable(model_parameters));
1395 ASSIGN_OR_RETURN(auto dual_solution_and_claim,
1396 GetLpDualSolutionIfAvailable(model_parameters));
1397 ASSIGN_OR_RETURN(auto basis, GetBasisIfAvailable());
1398 const SolutionClaims solution_claims = {
1399 .primal_feasible_solution_exists =
1400 primal_solution_and_claim.feasible_solution_exists,
1401 .dual_feasible_solution_exists =
1402 dual_solution_and_claim.feasible_solution_exists};
1403
1404 if (!primal_solution_and_claim.solution.has_value() &&
1405 !dual_solution_and_claim.solution.has_value() && !basis.has_value()) {
1406 return SolutionsAndClaims{.solution_claims = solution_claims};
1407 }
1408 SolutionsAndClaims solution_and_claims{.solution_claims = solution_claims};
1409 SolutionProto& solution =
1410 solution_and_claims.solutions.emplace_back(SolutionProto());
1411 if (primal_solution_and_claim.solution.has_value()) {
1412 *solution.mutable_primal_solution() =
1413 std::move(*primal_solution_and_claim.solution);
1414 }
1415 if (dual_solution_and_claim.solution.has_value()) {
1416 *solution.mutable_dual_solution() =
1417 std::move(*dual_solution_and_claim.solution);
1418 }
1419 if (basis.has_value()) {
1420 *solution.mutable_basis() = std::move(*basis);
1421 }
1422 return solution_and_claims;
1423}
1424
1425absl::StatusOr<GurobiSolver::SolutionAndClaim<DualSolutionProto>>
1426GurobiSolver::GetLpDualSolutionIfAvailable(
1427 const ModelSolveParametersProto& model_parameters) {
1428 if (!gurobi_->IsAttrAvailable(GRB_DBL_ATTR_PI) ||
1429 !gurobi_->IsAttrAvailable(GRB_DBL_ATTR_RC)) {
1430 return SolutionAndClaim<DualSolutionProto>{
1431 .solution = std::nullopt, .feasible_solution_exists = false};
1432 }
1433
1434 // Note that we can ignore the reduced costs of the slack variables for
1435 // ranged constraints.
1436 DualSolutionProto dual_solution;
1437 bool dual_feasible_solution_exists = false;
1439 const std::vector<double> grb_constraint_duals,
1440 gurobi_->GetDoubleAttrArray(GRB_DBL_ATTR_PI, num_gurobi_lin_cons_));
1441 GurobiVectorToSparseDoubleVector(grb_constraint_duals,
1442 linear_constraints_map_,
1443 *dual_solution.mutable_dual_values(),
1444 model_parameters.dual_values_filter());
1445
1447 const std::vector<double> grb_reduced_cost_values,
1448 gurobi_->GetDoubleAttrArray(GRB_DBL_ATTR_RC, num_gurobi_variables_));
1449 GurobiVectorToSparseDoubleVector(grb_reduced_cost_values, variables_map_,
1450 *dual_solution.mutable_reduced_costs(),
1451 model_parameters.reduced_costs_filter());
1452
1453 ASSIGN_OR_RETURN(const int grb_termination,
1454 gurobi_->GetIntAttr(GRB_INT_ATTR_STATUS));
1455 if (grb_termination == GRB_OPTIMAL &&
1456 gurobi_->IsAttrAvailable(GRB_DBL_ATTR_OBJVAL)) {
1457 ASSIGN_OR_RETURN(const double obj_val,
1458 gurobi_->GetDoubleAttr(GRB_DBL_ATTR_OBJVAL));
1459 dual_solution.set_objective_value(obj_val);
1460 }
1461
1462 dual_solution.set_feasibility_status(SOLUTION_STATUS_UNDETERMINED);
1463 if (grb_termination == GRB_OPTIMAL) {
1464 dual_solution.set_feasibility_status(SOLUTION_STATUS_FEASIBLE);
1465 dual_feasible_solution_exists = true;
1466 } else if (grb_termination == GRB_UNBOUNDED) {
1467 dual_solution.set_feasibility_status(SOLUTION_STATUS_INFEASIBLE);
1468 }
1469 // TODO(b/290359402): We could use gurobi's dual solution quality measures
1470 // for further upgrade the dual feasibility but it likely is only useful
1471 // for phase II of dual simplex because:
1472 // * the quality measures seem to evaluate if the basis is dual feasible
1473 // so for primal simplex we would not improve over checking
1474 // GRB_OPTIMAL.
1475 // * for phase I dual simplex we cannot rely on the quality measures.
1476 // We could also use finiteness of GRB_DBL_ATTR_OBJBOUND to deduce dual
1477 // feasibility.
1478
1479 // Note: GRB_DBL_ATTR_OBJBOUND can sometimes provide the objective value of a
1480 // sub-optimal dual feasible solution.
1481 // Here we only use it to possibly update dual_feasible_solution_exists.
1482 ASSIGN_OR_RETURN(const double best_dual_bound, GetGurobiBestDualBound());
1483 if (dual_feasible_solution_exists || std::isfinite(best_dual_bound)) {
1484 dual_feasible_solution_exists = true;
1485 } else if (grb_termination == GRB_OPTIMAL) {
1486 return absl::InternalError(
1487 "GRB_INT_ATTR_STATUS == GRB_OPTIMAL, but GRB_DBL_ATTR_OBJBOUND is "
1488 "unavailable or infinite, and no dual feasible solution is returned");
1489 }
1490 return SolutionAndClaim<DualSolutionProto>{
1491 .solution = std::move(dual_solution),
1492 .feasible_solution_exists = dual_feasible_solution_exists};
1493}
1494
1495absl::Status GurobiSolver::FillRays(
1496 const ModelSolveParametersProto& model_parameters,
1497 const SolutionClaims solution_claims, SolveResultProto& result) {
1498 ASSIGN_OR_RETURN(const bool is_maximize, IsMaximize());
1499 // GRB_DBL_ATTR_UNBDRAY is sometimes incorrectly available for problems
1500 // without variables. We also give priority to the conclusions obtained from
1501 // dual solutions or bounds.
1502 if (!solution_claims.dual_feasible_solution_exists &&
1503 num_gurobi_variables_ > 0 &&
1504 gurobi_->IsAttrAvailable(GRB_DBL_ATTR_UNBDRAY)) {
1505 ASSIGN_OR_RETURN(const std::vector<double> grb_ray_var_values,
1506 gurobi_->GetDoubleAttrArray(GRB_DBL_ATTR_UNBDRAY,
1507 num_gurobi_variables_));
1508 PrimalRayProto* const primal_ray = result.add_primal_rays();
1509 GurobiVectorToSparseDoubleVector(grb_ray_var_values, variables_map_,
1510 *primal_ray->mutable_variable_values(),
1511 model_parameters.variable_values_filter());
1512 }
1513 // GRB_DBL_ATTR_FARKASDUAL is sometimes incorrectly available for problems
1514 // without constraints. We also give priority to the conclusions obtained from
1515 // primal solutions.
1516 if (!solution_claims.primal_feasible_solution_exists &&
1517 num_gurobi_lin_cons_ > 0 &&
1518 gurobi_->IsAttrAvailable(GRB_DBL_ATTR_FARKASDUAL)) {
1520 DualRayProto dual_ray,
1521 GetGurobiDualRay(model_parameters.dual_values_filter(),
1522 model_parameters.reduced_costs_filter(), is_maximize));
1523 result.mutable_dual_rays()->Add(std::move(dual_ray));
1524 }
1525 return absl::OkStatus();
1526}
1527
1528absl::StatusOr<GurobiSolver::SolutionsAndClaims> GurobiSolver::GetQpSolution(
1529 const ModelSolveParametersProto& model_parameters) {
1530 ASSIGN_OR_RETURN((auto [primal_solution, found_primal_feasible_solution]),
1531 GetConvexPrimalSolutionIfAvailable(model_parameters));
1532 // TODO(b/225189115): Expand QpDualsTest to check maximization problems and
1533 // other edge cases.
1534 ASSIGN_OR_RETURN((auto [dual_solution, found_dual_feasible_solution]),
1535 GetLpDualSolutionIfAvailable(model_parameters));
1536 // TODO(b/280353996): we want to extract the basis here (when we solve via
1537 // simplex), but the existing code extracts a basis which fails our validator.
1538
1539 const SolutionClaims solution_claims = {
1540 .primal_feasible_solution_exists = found_primal_feasible_solution,
1541 .dual_feasible_solution_exists = found_dual_feasible_solution};
1542
1543 if (!primal_solution.has_value()) {
1544 return GurobiSolver::SolutionsAndClaims{.solution_claims = solution_claims};
1545 }
1546 SolutionsAndClaims solution_and_claims{.solution_claims = solution_claims};
1547 SolutionProto& solution =
1548 solution_and_claims.solutions.emplace_back(SolutionProto());
1549 if (primal_solution.has_value()) {
1550 *solution.mutable_primal_solution() = *std::move(primal_solution);
1551 }
1552 if (dual_solution.has_value()) {
1553 *solution.mutable_dual_solution() = *std::move(dual_solution);
1554 }
1555 return solution_and_claims;
1556}
1557
1558absl::StatusOr<GurobiSolver::SolutionsAndClaims> GurobiSolver::GetQcpSolution(
1559 const ModelSolveParametersProto& model_parameters) {
1560 ASSIGN_OR_RETURN((auto [primal_solution, found_primal_feasible_solution]),
1561 GetConvexPrimalSolutionIfAvailable(model_parameters));
1562 // TODO(b/227217735): Expose duals. Note that the user must set the QCPDual
1563 // parameter for Gurobi to return any dual values.
1564 ASSIGN_OR_RETURN((auto [_, found_dual_feasible_solution]),
1565 GetLpDualSolutionIfAvailable(model_parameters));
1566
1567 ASSIGN_OR_RETURN(const int grb_termination,
1568 gurobi_->GetIntAttr(GRB_INT_ATTR_STATUS));
1569 // By default, Gurobi will not return duals for optimally solved QCPs.
1570 const bool proven_feasible = grb_termination == GRB_OPTIMAL;
1571 const SolutionClaims solution_claims = {
1572 .primal_feasible_solution_exists = found_primal_feasible_solution,
1573 .dual_feasible_solution_exists =
1574 found_dual_feasible_solution || proven_feasible};
1575
1576 SolutionsAndClaims solution_and_claims{.solution_claims = solution_claims};
1577 if (primal_solution.has_value()) {
1578 *solution_and_claims.solutions.emplace_back().mutable_primal_solution() =
1579 *std::move(primal_solution);
1580 }
1581 return solution_and_claims;
1582}
1583
1584absl::Status GurobiSolver::SetParameters(
1585 const SolveParametersProto& parameters) {
1586 ASSIGN_OR_RETURN(const bool is_mip, IsMIP());
1587 ASSIGN_OR_RETURN(const GurobiParametersProto gurobi_parameters,
1588 MergeParameters(parameters, is_mip));
1589 std::vector<std::string> parameter_errors;
1590 for (const GurobiParametersProto::Parameter& parameter :
1591 gurobi_parameters.parameters()) {
1592 absl::Status param_status =
1593 gurobi_->SetParam(parameter.name().c_str(), parameter.value());
1594 if (!param_status.ok()) {
1595 parameter_errors.emplace_back(std::move(param_status).message());
1596 }
1597 }
1598 if (!parameter_errors.empty()) {
1599 return absl::InvalidArgumentError(absl::StrJoin(parameter_errors, "; "));
1600 }
1601 return absl::OkStatus();
1602}
1603
1604absl::Status GurobiSolver::AddNewVariables(
1605 const VariablesProto& new_variables) {
1606 const int num_new_variables = new_variables.lower_bounds().size();
1607 std::vector<char> variable_type(num_new_variables);
1608 for (int j = 0; j < num_new_variables; ++j) {
1609 const VariableId id = new_variables.ids(j);
1610 gtl::InsertOrDie(&variables_map_, id, j + num_gurobi_variables_);
1611 variable_type[j] = new_variables.integers(j) ? GRB_INTEGER : GRB_CONTINUOUS;
1612 }
1613 // We need to copy the names, RepeatedPtrField cannot be converted to
1614 // absl::Span<std::string>.
1615 const std::vector<std::string> variable_names =
1616 TruncateNames(new_variables.names());
1617 RETURN_IF_ERROR(gurobi_->AddVars(
1618 /*obj=*/{},
1619 /*lb=*/new_variables.lower_bounds(),
1620 /*ub=*/new_variables.upper_bounds(),
1621 /*vtype=*/variable_type, variable_names));
1622 num_gurobi_variables_ += num_new_variables;
1623
1624 return absl::OkStatus();
1625}
1626
1627absl::Status GurobiSolver::AddSingleObjective(const ObjectiveProto& objective) {
1628 const int model_sense = objective.maximize() ? GRB_MAXIMIZE : GRB_MINIMIZE;
1629 RETURN_IF_ERROR(gurobi_->SetIntAttr(GRB_INT_ATTR_MODELSENSE, model_sense));
1631 gurobi_->SetDoubleAttr(GRB_DBL_ATTR_OBJCON, objective.offset()));
1632 RETURN_IF_ERROR(UpdateDoubleListAttribute(objective.linear_coefficients(),
1633 GRB_DBL_ATTR_OBJ, variables_map_));
1635 ResetQuadraticObjectiveTerms(objective.quadratic_coefficients()));
1636 return absl::OkStatus();
1637}
1638
1639absl::Status GurobiSolver::AddMultiObjectives(
1640 const ObjectiveProto& primary_objective,
1641 const google::protobuf::Map<int64_t, ObjectiveProto>&
1642 auxiliary_objectives) {
1643 absl::flat_hash_set<int64_t> priorities = {primary_objective.priority()};
1644 for (const auto& [id, objective] : auxiliary_objectives) {
1645 const int64_t priority = objective.priority();
1646 if (!priorities.insert(priority).second) {
1648 << "repeated objective priority: " << priority;
1649 }
1650 }
1651 const bool is_maximize = primary_objective.maximize();
1652 RETURN_IF_ERROR(gurobi_->SetIntAttr(
1654 RETURN_IF_ERROR(AddNewMultiObjective(
1655 primary_objective, /*objective_id=*/std::nullopt, is_maximize));
1656 for (const int64_t id : SortedMapKeys(auxiliary_objectives)) {
1658 AddNewMultiObjective(auxiliary_objectives.at(id), id, is_maximize));
1659 }
1660 return absl::OkStatus();
1661}
1662
1663absl::Status GurobiSolver::AddNewMultiObjective(
1664 const ObjectiveProto& objective,
1665 const std::optional<AuxiliaryObjectiveId> objective_id,
1666 const bool is_maximize) {
1667 std::vector<GurobiVariableIndex> var_indices;
1668 var_indices.reserve(objective.linear_coefficients().ids_size());
1669 for (const int64_t var_id : objective.linear_coefficients().ids()) {
1670 var_indices.push_back(variables_map_.at(var_id));
1671 }
1672 const GurobiMultiObjectiveIndex grb_index =
1673 static_cast<int>(multi_objectives_map_.size());
1674 // * MathOpt and Gurobi have different priority orderings (lower and higher
1675 // are more important, respectively). Therefore, we negate priorities from
1676 // MathOpt (which is OK as they are restricted to be nonnegative in MathOpt,
1677 // but not in Gurobi).
1678 // * Tolerances are set to default values, as of Gurobi v9.5.
1679 // * Gurobi exposes only a single objective sense for the entire model. We use
1680 // the objective weight to handle mixing senses across objectives (weight of
1681 // 1 if objective sense agrees with model sense, -1 otherwise).
1682 RETURN_IF_ERROR(gurobi_->SetNthObjective(
1683 /*index=*/grb_index, /*priority=*/static_cast<int>(-objective.priority()),
1684 /*weight=*/objective.maximize() == is_maximize ? +1.0 : -1.0,
1685 /*abs_tol=*/1.0e-6,
1686 /*rel_tol=*/0.0, /*name=*/objective.name(),
1687 /*constant=*/objective.offset(), /*lind=*/var_indices,
1688 /*lval=*/objective.linear_coefficients().values()));
1689 multi_objectives_map_.insert({objective_id, grb_index});
1690 return absl::OkStatus();
1691}
1692
1693// Given a vector of pairs<LinearConstraintId, LinearConstraintData&> add a
1694// slack variable for each of the constraints in the underlying `gurobi_` using
1695// the referenced bounds.
1696absl::Status GurobiSolver::AddNewSlacks(
1697 const std::vector<LinearConstraintData*>& new_slacks) {
1698 // Note that we are really adding the sub-matrix
1699 // D * slack
1700 // to the set of linear constraints, and the D matrix is stored in compressed
1701 // sparse column (CSC) format. In our particular case, D is a diagonal matrix
1702 // with -1.0 coefficients for each new slack in the row indicated in the
1703 // row_indices vector.
1704 const int num_slacks = new_slacks.size();
1705 if (num_slacks == 0) {
1706 return absl::OkStatus();
1707 }
1708 // Build the D matrix in CSC format.
1709 const std::vector<double> column_non_zeros(num_slacks, -1.0);
1710 std::vector<double> lower_bounds;
1711 std::vector<double> upper_bounds;
1712 const std::vector<char> vtypes(num_slacks, GRB_CONTINUOUS);
1713 std::vector<GurobiLinearConstraintIndex> row_indices;
1714 std::vector<int> column_non_zero_begin;
1715 column_non_zero_begin.reserve(num_slacks);
1716 row_indices.reserve(num_slacks);
1717 lower_bounds.reserve(num_slacks);
1718 upper_bounds.reserve(num_slacks);
1719 for (int k = 0; k < num_slacks; ++k) {
1720 CHECK_NE(new_slacks[k], nullptr);
1721 const LinearConstraintData& constraint_data = *new_slacks[k];
1722 row_indices.push_back(constraint_data.constraint_index);
1723 lower_bounds.push_back(constraint_data.lower_bound);
1724 upper_bounds.push_back(constraint_data.upper_bound);
1725 column_non_zero_begin.push_back(k);
1726 }
1727 // Add variables to the underlying model.
1728 RETURN_IF_ERROR(gurobi_->AddVars(/*vbegin=*/column_non_zero_begin,
1729 /*vind=*/row_indices,
1730 /*vval=*/column_non_zeros, /*obj=*/{},
1731 /*lb=*/lower_bounds, /*ub=*/upper_bounds,
1732 /*vtype=*/vtypes, /*names=*/{}));
1733 num_gurobi_variables_ += num_slacks;
1734 return absl::OkStatus();
1735}
1736
1737absl::Status GurobiSolver::AddNewLinearConstraints(
1738 const LinearConstraintsProto& constraints) {
1739 const int num_new_constraints = constraints.lower_bounds().size();
1740
1741 // We need to copy the names, RepeatedPtrField cannot be converted to
1742 // absl::Span<std::string>.
1743 const std::vector<std::string> constraint_names =
1744 TruncateNames(constraints.names());
1745 // Constraints are translated into:
1746 // 1. ax <= upper_bound (if lower bound <= -GRB_INFINITY, and upper_bound
1747 // is finite and less than GRB_INFINITY)
1748 // 2. ax >= lower_bound (if upper bound >= GRB_INFINITY, and lower_bound is
1749 // finite and greater than -GRB_INFINITY)
1750 // 3. ax == xxxxx_bound (if both bounds are finite, equal, and their
1751 // absolute values less than GRB_INFINITY)
1752 // 4. ax - slack = 0.0 (otherwise,
1753 // slack bounds == [lower_bound, upper_bound])
1754 std::vector<double> constraint_rhs;
1755 std::vector<char> constraint_sense;
1756 std::vector<LinearConstraintData*> new_slacks;
1757 constraint_rhs.reserve(num_new_constraints);
1758 constraint_sense.reserve(num_new_constraints);
1759 new_slacks.reserve(num_new_constraints);
1760 for (int i = 0; i < num_new_constraints; ++i) {
1761 const int64_t id = constraints.ids(i);
1762 LinearConstraintData& constraint_data =
1763 gtl::InsertKeyOrDie(&linear_constraints_map_, id);
1764 const double lb = constraints.lower_bounds(i);
1765 const double ub = constraints.upper_bounds(i);
1766 RETURN_IF_ERROR(SafeGurobiDouble(lb))
1767 << "lower bound for linear constraint " << id << ": "
1768 << EscapedNameForLogging(
1769 constraints.names().empty() ? "" : constraints.names(i));
1770 RETURN_IF_ERROR(SafeGurobiDouble(ub))
1771 << "upper bound for linear constraint " << id << ": "
1772 << EscapedNameForLogging(
1773 constraints.names().empty() ? "" : constraints.names(i));
1774 constraint_data.lower_bound = lb;
1775 constraint_data.upper_bound = ub;
1776 constraint_data.constraint_index = i + num_gurobi_lin_cons_;
1777 char sense = GRB_EQUAL;
1778 double rhs = 0.0;
1779 const bool lb_is_grb_neg_inf = lb <= -GRB_INFINITY;
1780 const bool ub_is_grb_pos_inf = ub >= GRB_INFINITY;
1781 if (lb_is_grb_neg_inf && !ub_is_grb_pos_inf) {
1782 sense = GRB_LESS_EQUAL;
1783 rhs = ub;
1784 } else if (!lb_is_grb_neg_inf && ub_is_grb_pos_inf) {
1785 sense = GRB_GREATER_EQUAL;
1786 rhs = lb;
1787 } else if (lb == ub) {
1788 sense = GRB_EQUAL;
1789 rhs = lb;
1790 } else {
1791 // Note that constraints where the lower bound and the upper bound are
1792 // -+infinity translate into a range constraint with an unbounded slack.
1793 constraint_data.slack_index = new_slacks.size() + num_gurobi_variables_;
1794 new_slacks.push_back(&constraint_data);
1795 }
1796 constraint_rhs.emplace_back(rhs);
1797 constraint_sense.emplace_back(sense);
1798 }
1799 // Add all constraints in one call.
1801 gurobi_->AddConstrs(constraint_sense, constraint_rhs, constraint_names));
1802 num_gurobi_lin_cons_ += num_new_constraints;
1803 // Add slacks for true ranged constraints (if needed)
1804 if (!new_slacks.empty()) {
1805 RETURN_IF_ERROR(AddNewSlacks(new_slacks));
1806 }
1807 return absl::OkStatus();
1808}
1809
1810absl::Status GurobiSolver::AddNewQuadraticConstraints(
1811 const google::protobuf::Map<QuadraticConstraintId,
1812 QuadraticConstraintProto>& constraints) {
1813 // Constraints are translated into:
1814 // 1. ax <= upper_bound (if lower bound <= -GRB_INFINITY, and upper_bound
1815 // is finite and less than GRB_INFINITY)
1816 // 2. ax >= lower_bound (if upper bound >= GRB_INFINITY, and lower_bound is
1817 // finite and greater than -GRB_INFINITY)
1818 // 3. ax == xxxxx_bound (if both bounds are finite, equal, and their
1819 // absolute values less than GRB_INFINITY)
1820 // 4. Return an error otherwise, we do not currently support ranged quadratic
1821 // constraints.
1822 for (const auto& [id, constraint] : constraints) {
1823 char sense = GRB_EQUAL;
1824 double rhs = 0.0;
1825 const double lb = constraint.lower_bound();
1826 const double ub = constraint.upper_bound();
1827 RETURN_IF_ERROR(SafeGurobiDouble(lb))
1828 << "lower bound for quadratic constraint " << id << ": "
1829 << EscapedNameForLogging(constraint.name());
1830 RETURN_IF_ERROR(SafeGurobiDouble(ub))
1831 << "upper bound for quadratic constraint " << id << ": "
1832 << EscapedNameForLogging(constraint.name());
1833 const bool lb_is_grb_neg_inf = lb <= -GRB_INFINITY;
1834 const bool ub_is_grb_pos_inf = ub >= GRB_INFINITY;
1835 if (lb_is_grb_neg_inf && ub_is_grb_pos_inf) {
1836 // The constraint is vacuous, so we just skip it.
1837 // TODO(b/227217735): Ensure duals properly account for this constraint.
1838 continue;
1839 } else if (lb_is_grb_neg_inf && !ub_is_grb_pos_inf) {
1840 sense = GRB_LESS_EQUAL;
1841 rhs = ub;
1842 } else if (!lb_is_grb_neg_inf && ub_is_grb_pos_inf) {
1843 sense = GRB_GREATER_EQUAL;
1844 rhs = lb;
1845 } else if (lb == ub) {
1846 sense = GRB_EQUAL;
1847 rhs = lb;
1848 } else {
1849 // We do not currently support ranged quadratic constraints, though it is
1850 // possible to support this if there is a need.
1851 return absl::UnimplementedError(
1852 "ranged quadratic constraints are not currently supported in Gurobi "
1853 "interface");
1854 }
1855 const SparseDoubleVectorProto& linear_coeffs = constraint.linear_terms();
1856 const int num_linear_coeffs = linear_coeffs.ids_size();
1857 std::vector<GurobiVariableIndex> linear_col_index(num_linear_coeffs);
1858 for (int k = 0; k < num_linear_coeffs; ++k) {
1859 linear_col_index[k] = variables_map_.at(linear_coeffs.ids(k));
1860 }
1861 const SparseDoubleMatrixProto& quad_coeffs = constraint.quadratic_terms();
1862 const int num_quad_coeffs = quad_coeffs.row_ids_size();
1863 std::vector<GurobiVariableIndex> quad_row_index(num_quad_coeffs);
1864 std::vector<GurobiVariableIndex> quad_col_index(num_quad_coeffs);
1865 for (int k = 0; k < num_quad_coeffs; ++k) {
1866 quad_row_index[k] = variables_map_.at(quad_coeffs.row_ids(k));
1867 quad_col_index[k] = variables_map_.at(quad_coeffs.column_ids(k));
1868 }
1869 RETURN_IF_ERROR(gurobi_->AddQConstr(
1870 linear_col_index, linear_coeffs.values(), quad_row_index,
1871 quad_col_index, quad_coeffs.coefficients(), sense, rhs,
1872 TruncateName(constraint.name())));
1873 gtl::InsertOrDie(&quadratic_constraints_map_, id, num_gurobi_quad_cons_);
1874 ++num_gurobi_quad_cons_;
1875 }
1876 return absl::OkStatus();
1877}
1878
1879std::optional<GurobiSolver::VariableId> GurobiSolver::TryExtractVariable(
1880 const LinearExpressionProto& expression) {
1881 if (expression.offset() == 0 && expression.ids_size() == 1 &&
1882 expression.coefficients(0) == 1) {
1883 return expression.ids(0);
1884 }
1885 return std::nullopt;
1886}
1887
1888absl::StatusOr<GurobiSolver::VariableEqualToExpression>
1889GurobiSolver::CreateSlackVariableEqualToExpression(
1890 const LinearExpressionProto& expression) {
1891 const GurobiVariableIndex slack_variable = num_gurobi_variables_;
1892 std::vector<GurobiVariableIndex> slack_col_indices = {slack_variable};
1893 std::vector<double> slack_coeffs = {-1.0};
1894 for (int j = 0; j < expression.ids_size(); ++j) {
1895 slack_col_indices.push_back(variables_map_.at(expression.ids(j)));
1896 slack_coeffs.push_back(expression.coefficients(j));
1897 }
1898 RETURN_IF_ERROR(gurobi_->AddVar(0, -kInf, kInf, GRB_CONTINUOUS, ""));
1899 RETURN_IF_ERROR(gurobi_->AddConstr(slack_col_indices, slack_coeffs, GRB_EQUAL,
1900 -expression.offset(), ""));
1901 return VariableEqualToExpression{.variable_index = num_gurobi_variables_++,
1902 .constraint_index = num_gurobi_lin_cons_++};
1903}
1904
1905absl::Status GurobiSolver::AddNewSecondOrderConeConstraints(
1906 const google::protobuf::Map<SecondOrderConeConstraintId,
1907 SecondOrderConeConstraintProto>& constraints) {
1908 for (const auto& [id, constraint] : constraints) {
1909 // The MathOpt proto representation for a second-order cone constraint is:
1910 // ||`constraint`.arguments_to_norm||_2 <= `constraint`.upper_bound.
1911 // Gurobi requires second-order cone constraints to be passed via the
1912 // quadratic constraint API as:
1913 // arg_var[0]^2 + ... + arg_var[d]^2 <= ub_var^2
1914 // ub_var >= 0,
1915 // for variables arg_var[0], ..., arg_var[d], ub_var. To get to this form,
1916 // we add slack variables:
1917 // ub_var = `constraint`.upper_bound
1918 // arg_var[i] = `constraint`.arguments_to_norm[i] for each i
1919 // Note that we elide adding a slack variable/constraint if the expression
1920 // we are setting it equal to is just a variable already in the model.
1921 SecondOrderConeConstraintData& constraint_data =
1922 gtl::InsertKeyOrDie(&soc_constraints_map_, id);
1923 constraint_data.constraint_index = num_gurobi_quad_cons_;
1924 // We force a new variable to be added so that we can add a lower bound on
1925 // it. Otherwise, we must update the model to flush bounds, or risk either
1926 // a Gurobi error, or stomping on a potentially stronger bound.
1928 (const auto [ub_var, ub_cons]),
1929 CreateSlackVariableEqualToExpression(constraint.upper_bound()));
1931 gurobi_->SetDoubleAttrElement(GRB_DBL_ATTR_LB, ub_var, 0.0));
1932 constraint_data.slack_variables.push_back(ub_var);
1933 constraint_data.slack_constraints.push_back(ub_cons);
1934 std::vector<GurobiVariableIndex> quad_var_indices = {ub_var};
1935 std::vector<double> quad_coeffs = {-1.0};
1936 for (const LinearExpressionProto& expression :
1937 constraint.arguments_to_norm()) {
1938 quad_coeffs.push_back(1.0);
1939 if (const std::optional<VariableId> maybe_variable =
1940 TryExtractVariable(expression);
1941 maybe_variable.has_value()) {
1942 quad_var_indices.push_back(variables_map_.at(*maybe_variable));
1943 continue;
1944 }
1945 ASSIGN_OR_RETURN((const auto [arg_var, arg_cons]),
1946 CreateSlackVariableEqualToExpression(expression));
1947 quad_var_indices.push_back(arg_var);
1948 constraint_data.slack_variables.push_back(arg_var);
1949 constraint_data.slack_constraints.push_back(arg_cons);
1950 }
1951 RETURN_IF_ERROR(gurobi_->AddQConstr({}, {}, quad_var_indices,
1952 quad_var_indices, quad_coeffs,
1953 GRB_LESS_EQUAL, 0.0, ""));
1954 ++num_gurobi_quad_cons_;
1955 }
1956 return absl::OkStatus();
1957}
1958
1959absl::Status GurobiSolver::AddNewSosConstraints(
1960 const google::protobuf::Map<AnyConstraintId, SosConstraintProto>&
1961 constraints,
1962 const int sos_type,
1963 absl::flat_hash_map<int64_t, SosConstraintData>& constraints_map) {
1964 for (const auto& [id, constraint] : constraints) {
1965 SosConstraintData& constraint_data =
1966 gtl::InsertKeyOrDie(&constraints_map, id);
1967 constraint_data.constraint_index = num_gurobi_sos_cons_;
1968 std::vector<GurobiVariableIndex> sos_var_indices;
1969 std::vector<double> weights;
1970 // As of v9.0.2, Gurobi does not allow SOS constraints to contain repeated
1971 // variables. So, we track the variables we "reuse" (i.e., were already
1972 // present in the model). Slack variables introduced in
1973 // `ExtractVariableEqualToExpression()` will not be present in the proto
1974 // inputs, so we can safely ignore tracking them.
1975 absl::flat_hash_set<VariableId> reused_variables;
1976 for (int i = 0; i < constraint.expressions_size(); ++i) {
1977 weights.push_back(constraint.weights().empty() ? i + 1
1978 : constraint.weights(i));
1979 if (const std::optional<VariableId> maybe_variable =
1980 TryExtractVariable(constraint.expressions(i));
1981 maybe_variable.has_value() &&
1982 !reused_variables.contains(*maybe_variable)) {
1983 sos_var_indices.push_back(variables_map_.at(*maybe_variable));
1984 reused_variables.insert(*maybe_variable);
1985 if (sos_type == 2) {
1986 // If this variable is deleted, Gurobi will drop the corresponding
1987 // term from the SOS constraint, potentially changing the meaning of
1988 // the constraint.
1989 undeletable_variables_.insert(*maybe_variable);
1990 }
1991 continue;
1992 }
1994 (const auto [var_index, cons_index]),
1995 CreateSlackVariableEqualToExpression(constraint.expressions(i)));
1996 sos_var_indices.push_back(var_index);
1997 constraint_data.slack_variables.push_back(var_index);
1998 constraint_data.slack_constraints.push_back(cons_index);
1999 }
2000 RETURN_IF_ERROR(gurobi_->AddSos({sos_type}, {0}, sos_var_indices, weights));
2001 ++num_gurobi_sos_cons_;
2002 }
2003 return absl::OkStatus();
2004}
2005
2006absl::Status GurobiSolver::AddNewIndicatorConstraints(
2007 const google::protobuf::Map<IndicatorConstraintId,
2008 IndicatorConstraintProto>& constraints) {
2009 for (const auto& [id, constraint] : constraints) {
2010 if (!constraint.has_indicator_id()) {
2011 if (constraint.activate_on_zero()) {
2013 << "MathOpt does not currently support Gurobi models with "
2014 "indicator constraints that activate on zero with unset "
2015 "indicator variables; encountered one at id: "
2016 << id;
2017 }
2018 gtl::InsertOrDie(&indicator_constraints_map_, id, std::nullopt);
2019 continue;
2020 }
2021 const int num_terms = constraint.expression().ids_size();
2022 std::vector<GurobiVariableIndex> grb_ids(num_terms);
2023 for (int k = 0; k < num_terms; ++k) {
2024 grb_ids[k] = variables_map_.at(constraint.expression().ids(k));
2025 }
2026 char sense = GRB_EQUAL;
2027 double rhs = 0.0;
2028 const double lb = constraint.lower_bound();
2029 const double ub = constraint.upper_bound();
2030 RETURN_IF_ERROR(SafeGurobiDouble(lb))
2031 << "lower bound for indicator constraint " << id << ": "
2032 << EscapedNameForLogging(constraint.name());
2033 RETURN_IF_ERROR(SafeGurobiDouble(ub))
2034 << "upper bound for indicator constraint " << id << ": "
2035 << EscapedNameForLogging(constraint.name());
2036 const bool lb_is_grb_neg_inf = lb <= -GRB_INFINITY;
2037 const bool ub_is_grb_pos_inf = ub >= GRB_INFINITY;
2038 if (lb_is_grb_neg_inf && ub_is_grb_pos_inf) {
2039 // The constraint is vacuous, so we just skip it.
2040 continue;
2041 } else if (lb_is_grb_neg_inf && !ub_is_grb_pos_inf) {
2042 sense = GRB_LESS_EQUAL;
2043 rhs = ub;
2044 } else if (!lb_is_grb_neg_inf && ub_is_grb_pos_inf) {
2045 sense = GRB_GREATER_EQUAL;
2046 rhs = lb;
2047 } else if (lb == ub) {
2048 sense = GRB_EQUAL;
2049 rhs = lb;
2050 } else {
2051 // We do not currently support ranged indicator constraints, though it
2052 // is possible to support this if there is a need.
2053 return absl::UnimplementedError(
2054 "ranged indicator constraints are not currently supported in "
2055 "Gurobi "
2056 "interface");
2057 }
2058 RETURN_IF_ERROR(gurobi_->AddIndicator(
2059 /*name=*/constraint.name(),
2060 /*binvar=*/variables_map_.at(constraint.indicator_id()),
2061 /*binval=*/constraint.activate_on_zero() ? 0 : 1,
2062 /*ind=*/grb_ids, /*val=*/constraint.expression().values(),
2063 /*sense=*/sense, /*rhs=*/rhs));
2064 gtl::InsertOrDie(&indicator_constraints_map_, id,
2065 IndicatorConstraintData{
2066 .constraint_index = num_gurobi_gen_cons_,
2067 .indicator_variable_id = constraint.indicator_id()});
2068 ++num_gurobi_gen_cons_;
2069 // Deleting the indicator variable, but not the associated indicator
2070 // constraint, will lead to a Gurobi error.
2071 undeletable_variables_.insert(constraint.indicator_id());
2072 }
2073 return absl::OkStatus();
2074}
2075
2076absl::Status GurobiSolver::ChangeCoefficients(
2077 const SparseDoubleMatrixProto& matrix) {
2078 const int num_coefficients = matrix.row_ids().size();
2079 std::vector<GurobiLinearConstraintIndex> row_index(num_coefficients);
2080 std::vector<GurobiVariableIndex> col_index(num_coefficients);
2081 for (int k = 0; k < num_coefficients; ++k) {
2082 row_index[k] =
2083 linear_constraints_map_.at(matrix.row_ids(k)).constraint_index;
2084 col_index[k] = variables_map_.at(matrix.column_ids(k));
2085 }
2086 return gurobi_->ChgCoeffs(row_index, col_index, matrix.coefficients());
2087}
2088
2089absl::Status GurobiSolver::UpdateDoubleListAttribute(
2090 const SparseDoubleVectorProto& update, const char* attribute_name,
2091 const IdHashMap& id_hash_map) {
2092 if (update.ids_size() == 0) {
2093 return absl::OkStatus();
2094 }
2095 std::vector<int> index;
2096 index.reserve(update.ids_size());
2097 for (const int64_t id : update.ids()) {
2098 index.push_back(id_hash_map.at(id));
2099 }
2100 return gurobi_->SetDoubleAttrList(attribute_name, index, update.values());
2101}
2102
2103absl::Status GurobiSolver::UpdateInt32ListAttribute(
2104 const SparseInt32VectorProto& update, const char* attribute_name,
2105 const IdHashMap& id_hash_map) {
2106 if (update.ids_size() == 0) {
2107 return absl::OkStatus();
2108 }
2109 std::vector<int> index;
2110 index.reserve(update.ids_size());
2111 for (const int64_t id : update.ids()) {
2112 index.push_back(id_hash_map.at(id));
2113 }
2114 return gurobi_->SetIntAttrList(attribute_name, index, update.values());
2115}
2116
2117absl::Status GurobiSolver::LoadModel(const ModelProto& input_model) {
2118 CHECK(gurobi_ != nullptr);
2119 RETURN_IF_ERROR(gurobi_->SetStringAttr(GRB_STR_ATTR_MODELNAME,
2120 TruncateName(input_model.name())));
2121 RETURN_IF_ERROR(AddNewVariables(input_model.variables()));
2122
2123 RETURN_IF_ERROR(AddNewLinearConstraints(input_model.linear_constraints()));
2125 AddNewQuadraticConstraints(input_model.quadratic_constraints()));
2126 RETURN_IF_ERROR(AddNewSecondOrderConeConstraints(
2127 input_model.second_order_cone_constraints()));
2128 RETURN_IF_ERROR(AddNewSosConstraints(input_model.sos1_constraints(),
2129 GRB_SOS_TYPE1, sos1_constraints_map_));
2130 RETURN_IF_ERROR(AddNewSosConstraints(input_model.sos2_constraints(),
2131 GRB_SOS_TYPE2, sos2_constraints_map_));
2133 AddNewIndicatorConstraints(input_model.indicator_constraints()));
2134
2135 RETURN_IF_ERROR(ChangeCoefficients(input_model.linear_constraint_matrix()));
2136
2137 if (input_model.auxiliary_objectives().empty()) {
2138 RETURN_IF_ERROR(AddSingleObjective(input_model.objective()));
2139 } else {
2140 RETURN_IF_ERROR(AddMultiObjectives(input_model.objective(),
2141 input_model.auxiliary_objectives()));
2142 }
2143 return absl::OkStatus();
2144}
2145
2146absl::Status GurobiSolver::ResetQuadraticObjectiveTerms(
2147 const SparseDoubleMatrixProto& terms) {
2148 quadratic_objective_coefficients_.clear();
2149 RETURN_IF_ERROR(gurobi_->DelQ());
2150 const int num_terms = terms.row_ids().size();
2151 if (num_terms > 0) {
2152 std::vector<GurobiVariableIndex> first_var_index(num_terms);
2153 std::vector<GurobiVariableIndex> second_var_index(num_terms);
2154 for (int k = 0; k < num_terms; ++k) {
2155 const VariableId row_id = terms.row_ids(k);
2156 const VariableId column_id = terms.column_ids(k);
2157 first_var_index[k] = variables_map_.at(row_id);
2158 second_var_index[k] = variables_map_.at(column_id);
2159 quadratic_objective_coefficients_[{row_id, column_id}] =
2160 terms.coefficients(k);
2161 }
2162 RETURN_IF_ERROR(gurobi_->AddQpTerms(first_var_index, second_var_index,
2163 terms.coefficients()));
2164 }
2165 return absl::OkStatus();
2166}
2167
2168absl::Status GurobiSolver::UpdateQuadraticObjectiveTerms(
2169 const SparseDoubleMatrixProto& terms) {
2170 CHECK(gurobi_ != nullptr);
2171 const int num_terms = terms.row_ids().size();
2172 if (num_terms > 0) {
2173 std::vector<GurobiVariableIndex> first_var_index(num_terms);
2174 std::vector<GurobiVariableIndex> second_var_index(num_terms);
2175 std::vector<double> coefficient_updates(num_terms);
2176 for (int k = 0; k < num_terms; ++k) {
2177 const VariableId row_id = terms.row_ids(k);
2178 const VariableId column_id = terms.column_ids(k);
2179 first_var_index[k] = variables_map_.at(row_id);
2180 second_var_index[k] = variables_map_.at(column_id);
2181 const std::pair<VariableId, VariableId> qp_term_key(row_id, column_id);
2182 const double new_coefficient = terms.coefficients(k);
2183 // Gurobi will maintain any existing quadratic coefficients unless we
2184 // call GRBdelq (which we don't). So, since stored entries in terms
2185 // specify the target coefficients, we need to compute the difference
2186 // from the existing coefficient with Gurobi, if any.
2187 coefficient_updates[k] =
2188 new_coefficient - quadratic_objective_coefficients_[qp_term_key];
2189 quadratic_objective_coefficients_[qp_term_key] = new_coefficient;
2190 }
2191 RETURN_IF_ERROR(gurobi_->AddQpTerms(first_var_index, second_var_index,
2192 coefficient_updates));
2193 }
2194 return absl::OkStatus();
2195}
2196
2197// Bound changes in constraints can induce new variables, and also remove
2198// some slacks. We first add all new variables, and queue all deletions to be
2199// dealt with later on.
2200absl::Status GurobiSolver::UpdateLinearConstraints(
2201 const LinearConstraintUpdatesProto& constraints_update,
2202 std::vector<GurobiVariableIndex>& deleted_variables_index) {
2203 const SparseDoubleVectorProto& constraint_lower_bounds =
2204 constraints_update.lower_bounds();
2205 const SparseDoubleVectorProto& constraint_upper_bounds =
2206 constraints_update.upper_bounds();
2207
2208 // If no update, just return.
2209 if (constraint_lower_bounds.ids().empty() &&
2210 constraint_upper_bounds.ids().empty()) {
2211 return absl::OkStatus();
2212 }
2213
2214 // We want to avoid changing the right-hand-side, sense, or slacks of each
2215 // constraint more than once. Since we can refer to the same constraint ID
2216 // both in the `constraint_upper_bounds` and `constraint_lower_bounds`
2217 // sparse vectors, we collect all changes into a single structure:
2218 struct UpdateConstraintData {
2219 LinearConstraintId constraint_id;
2220 LinearConstraintData& source;
2221 double new_lower_bound;
2222 double new_upper_bound;
2223 UpdateConstraintData(const LinearConstraintId id,
2224 LinearConstraintData& reference)
2225 : constraint_id(id),
2226 source(reference),
2227 new_lower_bound(reference.lower_bound),
2228 new_upper_bound(reference.upper_bound) {}
2229 };
2230 const int upper_bounds_size = constraint_upper_bounds.ids().size();
2231 const int lower_bounds_size = constraint_lower_bounds.ids().size();
2232 std::vector<UpdateConstraintData> update_vector;
2233 update_vector.reserve(upper_bounds_size + lower_bounds_size);
2234 // We exploit the fact that IDs are sorted in increasing order to merge
2235 // changes into a vector of aggregated changes.
2236 for (int lower_index = 0, upper_index = 0;
2237 lower_index < lower_bounds_size || upper_index < upper_bounds_size;) {
2238 VariableId lower_id = std::numeric_limits<int64_t>::max();
2239 if (lower_index < lower_bounds_size) {
2240 lower_id = constraint_lower_bounds.ids(lower_index);
2241 }
2242 VariableId upper_id = std::numeric_limits<int64_t>::max();
2243 if (upper_index < upper_bounds_size) {
2244 upper_id = constraint_upper_bounds.ids(upper_index);
2245 }
2246 const VariableId id = std::min(lower_id, upper_id);
2247 DCHECK(id < std::numeric_limits<int64_t>::max());
2248 UpdateConstraintData update(id, linear_constraints_map_.at(id));
2249 if (lower_id == upper_id) {
2250 update.new_lower_bound = constraint_lower_bounds.values(lower_index++);
2251 update.new_upper_bound = constraint_upper_bounds.values(upper_index++);
2252 } else if (lower_id < upper_id) {
2253 update.new_lower_bound = constraint_lower_bounds.values(lower_index++);
2254 } else { /* upper_id < lower_id */
2255 update.new_upper_bound = constraint_upper_bounds.values(upper_index++);
2256 }
2257 update_vector.emplace_back(update);
2258 }
2259
2260 // We have grouped all changes in update_vector, now generate changes in
2261 // slack bounds, rhs, senses, new slacks, and deleted_slacks (to be dealt
2262 // with later, outside this function).
2263 // These three vectors keep changes to right-hand-side and senses.
2264 std::vector<char> sense_data;
2265 std::vector<double> rhs_data;
2266 std::vector<GurobiLinearConstraintIndex> rhs_index;
2267 // These three vectors keep changes to bounds on existing slack.
2268 std::vector<double> lower_bound_data;
2269 std::vector<double> upper_bound_data;
2270 std::vector<GurobiVariableIndex> bound_index;
2271 // This vector keep newly introduced slacks.
2272 std::vector<LinearConstraintData*> new_slacks;
2273 // Iterate on the changes, and populate the three possible changes.
2274 for (UpdateConstraintData& update_data : update_vector) {
2275 const bool same_lower_bound =
2276 (update_data.source.lower_bound == update_data.new_lower_bound) ||
2277 ((update_data.source.lower_bound <= -GRB_INFINITY) &&
2278 (update_data.new_lower_bound <= -GRB_INFINITY));
2279 const bool same_upper_bound =
2280 (update_data.source.upper_bound == update_data.new_upper_bound) ||
2281 ((update_data.source.upper_bound >= GRB_INFINITY) &&
2282 (update_data.new_upper_bound >= GRB_INFINITY));
2283 if (same_upper_bound && same_lower_bound) continue;
2284 // Save into linear_constraints_map_[id] the new bounds for the linear
2285 // constraint.
2286 update_data.source.lower_bound = update_data.new_lower_bound;
2287 update_data.source.upper_bound = update_data.new_upper_bound;
2288 bool delete_slack = false;
2289 // Detect the type of constraint to add and store RHS and bounds.
2290 if (update_data.new_lower_bound <= -GRB_INFINITY &&
2291 update_data.new_upper_bound < GRB_INFINITY) {
2292 delete_slack = true;
2293 rhs_index.emplace_back(update_data.source.constraint_index);
2294 rhs_data.emplace_back(update_data.new_upper_bound);
2295 sense_data.emplace_back(GRB_LESS_EQUAL);
2296 } else if (update_data.new_lower_bound > -GRB_INFINITY &&
2297 update_data.new_upper_bound >= GRB_INFINITY) {
2298 delete_slack = true;
2299 rhs_index.emplace_back(update_data.source.constraint_index);
2300 rhs_data.emplace_back(update_data.new_lower_bound);
2301 sense_data.emplace_back(GRB_GREATER_EQUAL);
2302 } else if (update_data.new_lower_bound == update_data.new_upper_bound) {
2303 delete_slack = true;
2304 rhs_index.emplace_back(update_data.source.constraint_index);
2305 rhs_data.emplace_back(update_data.new_lower_bound);
2306 sense_data.emplace_back(GRB_EQUAL);
2307 } else {
2308 // Note that constraints where the lower bound and the upper bound are
2309 // -+infinity translated into a range constraint with an unbounded
2310 // slack.
2311 if (update_data.source.slack_index != kUnspecifiedIndex) {
2312 bound_index.emplace_back(update_data.source.slack_index);
2313 lower_bound_data.emplace_back(update_data.new_lower_bound);
2314 upper_bound_data.emplace_back(update_data.new_upper_bound);
2315 } else {
2316 // Note that if we add a new slack, we must both reset the sense and
2317 // right hand side for the inequality.
2318 rhs_index.emplace_back(update_data.source.constraint_index);
2319 rhs_data.emplace_back(0.0);
2320 sense_data.emplace_back(GRB_EQUAL);
2321 // Update the slack_index in the linear_constraints_map_[id]
2322 update_data.source.slack_index =
2323 new_slacks.size() + num_gurobi_variables_;
2324 // Save the data needed to add the new slack.
2325 new_slacks.push_back(&update_data.source);
2326 }
2327 }
2328 // If the constraint had a slack, and now is marked for deletion, we reset
2329 // the stored slack_index in linear_constraints_map_[id], save the index
2330 // in the list of variables to be deleted later on and remove the
2331 // constraint from slack_map_.
2332 if (delete_slack && update_data.source.slack_index != kUnspecifiedIndex) {
2333 deleted_variables_index.emplace_back(update_data.source.slack_index);
2334 update_data.source.slack_index = kUnspecifiedIndex;
2335 }
2336 }
2337
2338 // Pass down changes to Gurobi.
2339 if (!rhs_index.empty()) {
2341 gurobi_->SetDoubleAttrList(GRB_DBL_ATTR_RHS, rhs_index, rhs_data));
2343 gurobi_->SetCharAttrList(GRB_CHAR_ATTR_SENSE, rhs_index, sense_data));
2344 } // rhs changes
2345 if (!bound_index.empty()) {
2346 RETURN_IF_ERROR(gurobi_->SetDoubleAttrList(GRB_DBL_ATTR_LB, bound_index,
2347 lower_bound_data));
2348 RETURN_IF_ERROR(gurobi_->SetDoubleAttrList(GRB_DBL_ATTR_UB, bound_index,
2349 upper_bound_data));
2350 } // Slack bound changes.
2351
2352 if (!new_slacks.empty()) {
2353 RETURN_IF_ERROR(AddNewSlacks(new_slacks));
2354 }
2355 return absl::OkStatus();
2356}
2357
2358// This function re-assign indices for variables and constraints after
2359// deletion. The updated indices are computed from the previous indices,
2360// sorted in incremental form, but re-assigned so that all indices are
2361// contiguous between [0, num_variables-1], [0, num_linear_constraints-1], and
2362// [0, num_quad_constraints-1], etc.
2363void GurobiSolver::UpdateGurobiIndices(const DeletedIndices& deleted_indices) {
2364 // Recover the updated indices of variables.
2365 if (!deleted_indices.variables.empty()) {
2366 const std::vector<GurobiVariableIndex> old_to_new =
2367 IndexUpdateMap(num_gurobi_variables_, deleted_indices.variables);
2368 for (auto& [_, grb_index] : variables_map_) {
2369 grb_index = old_to_new[grb_index];
2370 CHECK_NE(grb_index, kDeletedIndex);
2371 }
2372 for (auto& [_, lin_con_data] : linear_constraints_map_) {
2373 if (lin_con_data.slack_index != kUnspecifiedIndex) {
2374 lin_con_data.slack_index = old_to_new[lin_con_data.slack_index];
2375 CHECK_NE(lin_con_data.slack_index, kDeletedIndex);
2376 }
2377 }
2378 for (auto& [_, soc_con_data] : soc_constraints_map_) {
2379 for (GurobiVariableIndex& index : soc_con_data.slack_variables) {
2380 index = old_to_new[index];
2381 CHECK_NE(index, kDeletedIndex);
2382 }
2383 }
2384 for (auto& [_, sos1_con_data] : sos1_constraints_map_) {
2385 for (GurobiVariableIndex& index : sos1_con_data.slack_variables) {
2386 index = old_to_new[index];
2387 CHECK_NE(index, kDeletedIndex);
2388 }
2389 }
2390 for (auto& [_, sos2_con_data] : sos2_constraints_map_) {
2391 for (GurobiVariableIndex& index : sos2_con_data.slack_variables) {
2392 index = old_to_new[index];
2393 CHECK_NE(index, kDeletedIndex);
2394 }
2395 }
2396 }
2397 // Recover the updated indices of linear constraints.
2398 if (!deleted_indices.linear_constraints.empty()) {
2399 const std::vector<GurobiLinearConstraintIndex> old_to_new = IndexUpdateMap(
2400 num_gurobi_lin_cons_, deleted_indices.linear_constraints);
2401 for (auto& [_, lin_con_data] : linear_constraints_map_) {
2402 lin_con_data.constraint_index = old_to_new[lin_con_data.constraint_index];
2403 CHECK_NE(lin_con_data.constraint_index, kDeletedIndex);
2404 }
2405 for (auto& [_, soc_con_data] : soc_constraints_map_) {
2406 for (GurobiLinearConstraintIndex& index :
2407 soc_con_data.slack_constraints) {
2408 index = old_to_new[index];
2409 CHECK_NE(index, kDeletedIndex);
2410 }
2411 }
2412 for (auto& [_, sos1_con_data] : sos1_constraints_map_) {
2413 for (GurobiLinearConstraintIndex& index :
2414 sos1_con_data.slack_constraints) {
2415 index = old_to_new[index];
2416 CHECK_NE(index, kDeletedIndex);
2417 }
2418 }
2419 for (auto& [_, sos2_con_data] : sos2_constraints_map_) {
2420 for (GurobiLinearConstraintIndex& index :
2421 sos2_con_data.slack_constraints) {
2422 index = old_to_new[index];
2423 CHECK_NE(index, kDeletedIndex);
2424 }
2425 }
2426 }
2427 // Recover the updated indices of quadratic constraints.
2428 if (!deleted_indices.quadratic_constraints.empty()) {
2429 const std::vector<GurobiQuadraticConstraintIndex> old_to_new =
2430 IndexUpdateMap(num_gurobi_quad_cons_,
2431 deleted_indices.quadratic_constraints);
2432 for (auto& [_, grb_index] : quadratic_constraints_map_) {
2433 grb_index = old_to_new[grb_index];
2434 CHECK_NE(grb_index, kDeletedIndex);
2435 }
2436 for (auto& [_, soc_con_data] : soc_constraints_map_) {
2437 GurobiQuadraticConstraintIndex& grb_index = soc_con_data.constraint_index;
2438 grb_index = old_to_new[soc_con_data.constraint_index];
2439 CHECK_NE(grb_index, kDeletedIndex);
2440 }
2441 }
2442 // Recover the updated indices of SOS constraints.
2443 if (!deleted_indices.sos_constraints.empty()) {
2444 const std::vector<GurobiSosConstraintIndex> old_to_new =
2445 IndexUpdateMap(num_gurobi_sos_cons_, deleted_indices.sos_constraints);
2446 for (auto& [_, sos1_data] : sos1_constraints_map_) {
2447 GurobiSosConstraintIndex& grb_index = sos1_data.constraint_index;
2448 grb_index = old_to_new[grb_index];
2449 CHECK_NE(grb_index, kDeletedIndex);
2450 }
2451 for (auto& [_, sos2_data] : sos2_constraints_map_) {
2452 GurobiSosConstraintIndex& grb_index = sos2_data.constraint_index;
2453 grb_index = old_to_new[grb_index];
2454 CHECK_NE(grb_index, kDeletedIndex);
2455 }
2456 }
2457 // Recover the updated indices of general constraints.
2458 if (!deleted_indices.general_constraints.empty()) {
2459 const std::vector<GurobiGeneralConstraintIndex> old_to_new = IndexUpdateMap(
2460 num_gurobi_gen_cons_, deleted_indices.general_constraints);
2461 for (auto& [_, indicator_data] : indicator_constraints_map_) {
2462 if (!indicator_data.has_value()) {
2463 continue;
2464 }
2465 GurobiGeneralConstraintIndex& grb_index =
2466 indicator_data->constraint_index;
2467 grb_index = old_to_new[grb_index];
2468 CHECK_NE(grb_index, kDeletedIndex);
2469 }
2470 }
2471}
2472
2473absl::StatusOr<bool> GurobiSolver::Update(
2474 const ModelUpdateProto& model_update) {
2475 if (!undeletable_variables_.empty()) {
2476 for (const VariableId id : model_update.deleted_variable_ids()) {
2477 if (undeletable_variables_.contains(id)) {
2478 return false;
2479 }
2480 }
2481 }
2482 if (!UpdateIsSupported(model_update, kGurobiSupportedStructures)) {
2483 return false;
2484 }
2485 // As of 2022-12-06 we do not support incrementalism for multi-objective
2486 // models: not adding/deleting/modifying the auxiliary objectives...
2487 if (const AuxiliaryObjectivesUpdatesProto& objs_update =
2488 model_update.auxiliary_objectives_updates();
2489 !objs_update.deleted_objective_ids().empty() ||
2490 !objs_update.new_objectives().empty() ||
2491 !objs_update.objective_updates().empty()) {
2492 return false;
2493 }
2494 // ...or modifying the primary objective of a multi-objective model.
2495 if (is_multi_objective_mode() && model_update.has_objective_updates()) {
2496 return false;
2497 }
2498
2499 RETURN_IF_ERROR(AddNewVariables(model_update.new_variables()));
2500
2502 AddNewLinearConstraints(model_update.new_linear_constraints()));
2503
2504 RETURN_IF_ERROR(AddNewQuadraticConstraints(
2505 model_update.quadratic_constraint_updates().new_constraints()));
2506 RETURN_IF_ERROR(AddNewSecondOrderConeConstraints(
2507 model_update.second_order_cone_constraint_updates().new_constraints()));
2508 RETURN_IF_ERROR(AddNewSosConstraints(
2509 model_update.sos1_constraint_updates().new_constraints(), GRB_SOS_TYPE1,
2510 sos1_constraints_map_));
2511 RETURN_IF_ERROR(AddNewSosConstraints(
2512 model_update.sos2_constraint_updates().new_constraints(), GRB_SOS_TYPE2,
2513 sos2_constraints_map_));
2514 RETURN_IF_ERROR(AddNewIndicatorConstraints(
2515 model_update.indicator_constraint_updates().new_constraints()));
2516
2518 ChangeCoefficients(model_update.linear_constraint_matrix_updates()));
2519
2520 if (model_update.objective_updates().has_direction_update()) {
2521 const int model_sense = model_update.objective_updates().direction_update()
2522 ? GRB_MAXIMIZE
2523 : GRB_MINIMIZE;
2524 RETURN_IF_ERROR(gurobi_->SetIntAttr(GRB_INT_ATTR_MODELSENSE, model_sense));
2525 }
2526
2527 if (model_update.objective_updates().has_offset_update()) {
2528 RETURN_IF_ERROR(gurobi_->SetDoubleAttr(
2529 GRB_DBL_ATTR_OBJCON, model_update.objective_updates().offset_update()));
2530 }
2531
2532 RETURN_IF_ERROR(UpdateDoubleListAttribute(
2533 model_update.objective_updates().linear_coefficients(), GRB_DBL_ATTR_OBJ,
2534 variables_map_));
2535
2536 RETURN_IF_ERROR(UpdateQuadraticObjectiveTerms(
2537 model_update.objective_updates().quadratic_coefficients()));
2538
2540 UpdateDoubleListAttribute(model_update.variable_updates().lower_bounds(),
2541 GRB_DBL_ATTR_LB, variables_map_));
2542
2544 UpdateDoubleListAttribute(model_update.variable_updates().upper_bounds(),
2545 GRB_DBL_ATTR_UB, variables_map_));
2546
2547 if (model_update.variable_updates().has_integers()) {
2548 const SparseBoolVectorProto& update =
2549 model_update.variable_updates().integers();
2550 std::vector<GurobiVariableIndex> index;
2551 index.reserve(update.ids_size());
2552 for (const int64_t id : update.ids()) {
2553 index.push_back(variables_map_.at(id));
2554 }
2555 std::vector<char> value;
2556 value.reserve(update.values_size());
2557 for (const bool val : update.values()) {
2558 value.push_back(val ? GRB_INTEGER : GRB_CONTINUOUS);
2559 }
2561 gurobi_->SetCharAttrList(GRB_CHAR_ATTR_VTYPE, index, value));
2562 }
2563
2564 // Now we update quadratic_objective_coefficients_, removing any terms where
2565 // either one or both of the involved variables are about to be deleted.
2566 const absl::flat_hash_set<VariableId> variable_ids_to_be_deleted(
2567 model_update.deleted_variable_ids().begin(),
2568 model_update.deleted_variable_ids().end());
2569 // NOTE: Introducing more state and complexity should speed this up, but we
2570 // opt for the simpler approach for now.
2571 for (auto it = quadratic_objective_coefficients_.cbegin();
2572 it != quadratic_objective_coefficients_.cend();
2573 /*incremented in loop*/) {
2574 if (variable_ids_to_be_deleted.contains(it->first.first) ||
2575 variable_ids_to_be_deleted.contains(it->first.second)) {
2576 quadratic_objective_coefficients_.erase(it++);
2577 } else {
2578 ++it;
2579 }
2580 }
2581 // We cache all Gurobi variables and constraint indices that must be
2582 // deleted, and perform deletions at the end of the update call.
2583 DeletedIndices deleted_indices;
2584
2585 RETURN_IF_ERROR(UpdateLinearConstraints(
2586 model_update.linear_constraint_updates(), deleted_indices.variables));
2587
2588 for (const VariableId id : model_update.deleted_variable_ids()) {
2589 deleted_indices.variables.emplace_back(variables_map_.at(id));
2590 variables_map_.erase(id);
2591 }
2592
2593 for (const LinearConstraintId id :
2594 model_update.deleted_linear_constraint_ids()) {
2595 LinearConstraintData& constraint_data = linear_constraints_map_.at(id);
2596 deleted_indices.linear_constraints.push_back(
2597 constraint_data.constraint_index);
2598 if (constraint_data.slack_index != kUnspecifiedIndex) {
2599 deleted_indices.variables.push_back(constraint_data.slack_index);
2600 constraint_data.slack_index = kUnspecifiedIndex;
2601 }
2602 linear_constraints_map_.erase(id);
2603 }
2604
2605 for (const QuadraticConstraintId id :
2606 model_update.quadratic_constraint_updates().deleted_constraint_ids()) {
2607 const GurobiQuadraticConstraintIndex grb_index =
2608 quadratic_constraints_map_.at(id);
2609 deleted_indices.quadratic_constraints.push_back(grb_index);
2610 quadratic_constraints_map_.erase(id);
2611 }
2612
2613 for (const SecondOrderConeConstraintId id :
2614 model_update.second_order_cone_constraint_updates()
2615 .deleted_constraint_ids()) {
2616 deleted_indices.quadratic_constraints.push_back(
2617 soc_constraints_map_.at(id).constraint_index);
2618 for (const GurobiVariableIndex index :
2619 soc_constraints_map_.at(id).slack_variables) {
2620 deleted_indices.variables.push_back(index);
2621 }
2622 for (const GurobiLinearConstraintIndex index :
2623 soc_constraints_map_.at(id).slack_constraints) {
2624 deleted_indices.linear_constraints.push_back(index);
2625 }
2626 soc_constraints_map_.erase(id);
2627 }
2628
2629 const auto sos_updater = [&](const SosConstraintData& sos_constraint) {
2630 deleted_indices.sos_constraints.push_back(sos_constraint.constraint_index);
2631 for (const GurobiVariableIndex index : sos_constraint.slack_variables) {
2632 deleted_indices.variables.push_back(index);
2633 }
2634 for (const GurobiLinearConstraintIndex index :
2635 sos_constraint.slack_constraints) {
2636 deleted_indices.linear_constraints.push_back(index);
2637 }
2638 };
2639 for (const Sos1ConstraintId id :
2640 model_update.sos1_constraint_updates().deleted_constraint_ids()) {
2641 sos_updater(sos1_constraints_map_.at(id));
2642 sos1_constraints_map_.erase(id);
2643 }
2644
2645 for (const Sos2ConstraintId id :
2646 model_update.sos2_constraint_updates().deleted_constraint_ids()) {
2647 sos_updater(sos2_constraints_map_.at(id));
2648 sos2_constraints_map_.erase(id);
2649 }
2650
2651 for (const IndicatorConstraintId id :
2652 model_update.indicator_constraint_updates().deleted_constraint_ids()) {
2653 // Otherwise the constraint is not actually registered with Gurobi.
2654 const auto it = indicator_constraints_map_.find(id);
2655 CHECK(it != indicator_constraints_map_.end()) << "id: " << id;
2656 if (it->second.has_value()) {
2657 deleted_indices.general_constraints.push_back(
2658 it->second->constraint_index);
2659 }
2660 indicator_constraints_map_.erase(it);
2661 }
2662
2663 UpdateGurobiIndices(deleted_indices);
2664
2665 // If we are removing variables or constraints we remove them after adding
2666 // any variable or constraint. This is to avoid problems with
2667 // the numbering of possibly new variables and constraints.
2668 // After that we must update the model so that sequence of updates don't
2669 // interfere with one-another.
2670 if (!deleted_indices.linear_constraints.empty()) {
2671 RETURN_IF_ERROR(gurobi_->DelConstrs(deleted_indices.linear_constraints));
2672 num_gurobi_lin_cons_ -= deleted_indices.linear_constraints.size();
2673 }
2674
2675 if (!deleted_indices.quadratic_constraints.empty()) {
2677 gurobi_->DelQConstrs(deleted_indices.quadratic_constraints));
2678 num_gurobi_quad_cons_ -= deleted_indices.quadratic_constraints.size();
2679 }
2680
2681 if (!deleted_indices.sos_constraints.empty()) {
2682 RETURN_IF_ERROR(gurobi_->DelSos(deleted_indices.sos_constraints));
2683 }
2684
2685 if (!deleted_indices.general_constraints.empty()) {
2687 gurobi_->DelGenConstrs(deleted_indices.general_constraints));
2688 }
2689
2690 if (!deleted_indices.variables.empty()) {
2691 RETURN_IF_ERROR(gurobi_->DelVars(deleted_indices.variables));
2692 num_gurobi_variables_ -= deleted_indices.variables.size();
2693 }
2694
2695 // Synchronize all pending changes.
2696 RETURN_IF_ERROR(gurobi_->UpdateModel());
2697
2698 return true;
2699}
2700
2701absl::StatusOr<std::unique_ptr<GurobiSolver>> GurobiSolver::New(
2702 const ModelProto& input_model, const SolverInterface::InitArgs& init_args) {
2704 return absl::InvalidArgumentError("Gurobi is not correctly installed.");
2705 }
2707 ModelIsSupported(input_model, kGurobiSupportedStructures, "Gurobi"));
2708 if (!input_model.auxiliary_objectives().empty() &&
2709 !input_model.objective().quadratic_coefficients().row_ids().empty()) {
2711 << "Gurobi does not support multiple objective models with "
2712 "quadratic objectives";
2713 }
2714 for (const auto& [id, obj] : input_model.auxiliary_objectives()) {
2715 if (!obj.quadratic_coefficients().row_ids().empty()) {
2717 << "Gurobi does not support multiple objective models with "
2718 "quadratic objectives";
2719 }
2720 }
2721 ASSIGN_OR_RETURN(std::unique_ptr<Gurobi> gurobi,
2722 GurobiFromInitArgs(init_args));
2723 auto gurobi_solver = absl::WrapUnique(new GurobiSolver(std::move(gurobi)));
2724 RETURN_IF_ERROR(gurobi_solver->LoadModel(input_model));
2725 return gurobi_solver;
2726}
2727
2728absl::StatusOr<std::unique_ptr<GurobiSolver::GurobiCallbackData>>
2729GurobiSolver::RegisterCallback(const CallbackRegistrationProto& registration,
2730 const Callback cb,
2731 const MessageCallback message_cb,
2732 const absl::Time start,
2733 SolveInterrupter* const local_interrupter) {
2734 const absl::flat_hash_set<CallbackEventProto> events = EventSet(registration);
2735
2736 // Note that IS_MIP does not necessarily mean the problem has integer
2737 // variables. Please refer to Gurobi's doc for details:
2738 // https://www.gurobi.com/documentation/9.1/refman/ismip.html.
2739 //
2740 // Here we assume that we get MIP related events and use a MIP solving
2741 // strategy when IS_MIP is true.
2742 ASSIGN_OR_RETURN(const int is_mip, gurobi_->GetIntAttr(GRB_INT_ATTR_IS_MIP));
2743
2745 registration, is_mip ? SupportedMIPEvents() : SupportedLPEvents()))
2746 << "for a " << (is_mip ? "MIP" : "LP") << " model";
2747
2748 // Set Gurobi parameters.
2749 if (message_cb != nullptr) {
2750 // Disable logging messages to the console the user wants to handle
2751 // messages.
2752 RETURN_IF_ERROR(gurobi_->SetIntParam(GRB_INT_PAR_LOGTOCONSOLE, 0));
2753 }
2754 if (registration.add_cuts() || registration.add_lazy_constraints()) {
2755 // This is to signal the solver presolve to limit primal transformations
2756 // that precludes crushing cuts to the presolved model.
2757 RETURN_IF_ERROR(gurobi_->SetIntParam(GRB_INT_PAR_PRECRUSH, 1));
2758 }
2759 if (registration.add_lazy_constraints()) {
2760 // This is needed so that the solver knows that some presolve reductions
2761 // can not be performed safely.
2762 RETURN_IF_ERROR(gurobi_->SetIntParam(GRB_INT_PAR_LAZYCONSTRAINTS, 1));
2763 }
2764 return std::make_unique<GurobiCallbackData>(
2765 GurobiCallbackInput{
2766 .user_cb = cb,
2767 .message_cb = message_cb,
2768 .variable_ids = variables_map_,
2769 .num_gurobi_vars = num_gurobi_variables_,
2770 .events = EventToGurobiWhere(events),
2771 .mip_solution_filter = registration.mip_solution_filter(),
2772 .mip_node_filter = registration.mip_node_filter(),
2773 .start = start},
2774 local_interrupter);
2775}
2776
2777absl::StatusOr<InvertedBounds> GurobiSolver::ListInvertedBounds() const {
2778 InvertedBounds inverted_bounds;
2779 {
2781 const std::vector<double> var_lbs,
2782 gurobi_->GetDoubleAttrArray(GRB_DBL_ATTR_LB, num_gurobi_variables_));
2784 const std::vector<double> var_ubs,
2785 gurobi_->GetDoubleAttrArray(GRB_DBL_ATTR_UB, num_gurobi_variables_));
2786 for (const auto& [id, index] : variables_map_) {
2787 if (var_lbs[index] > var_ubs[index]) {
2788 inverted_bounds.variables.push_back(id);
2789 }
2790 }
2791 }
2792 for (const auto& [id, cstr_data] : linear_constraints_map_) {
2793 if (cstr_data.lower_bound > cstr_data.upper_bound) {
2794 inverted_bounds.linear_constraints.push_back(id);
2795 }
2796 }
2797
2798 // Above code have inserted ids in non-stable order.
2799 std::sort(inverted_bounds.variables.begin(), inverted_bounds.variables.end());
2800 std::sort(inverted_bounds.linear_constraints.begin(),
2801 inverted_bounds.linear_constraints.end());
2802 return inverted_bounds;
2803}
2804
2805absl::StatusOr<InvalidIndicators> GurobiSolver::ListInvalidIndicators() const {
2806 InvalidIndicators invalid_indicators;
2807 for (const auto& [constraint_id, indicator_data] :
2808 indicator_constraints_map_) {
2809 if (!indicator_data.has_value()) {
2810 continue;
2811 }
2812 const int64_t indicator_id = indicator_data->indicator_variable_id;
2813 const GurobiVariableIndex variable_index = variables_map_.at(indicator_id);
2814 ASSIGN_OR_RETURN(const double var_lb, gurobi_->GetDoubleAttrElement(
2815 GRB_DBL_ATTR_LB, variable_index));
2816 ASSIGN_OR_RETURN(const double var_ub, gurobi_->GetDoubleAttrElement(
2817 GRB_DBL_ATTR_UB, variable_index));
2819 const char var_type,
2820 gurobi_->GetCharAttrElement(GRB_CHAR_ATTR_VTYPE, variable_index));
2821 if (!(var_type == GRB_BINARY ||
2822 (var_type == GRB_INTEGER && var_lb >= 0.0 && var_ub <= 1.0))) {
2823 invalid_indicators.invalid_indicators.push_back(
2824 {.variable = indicator_id, .constraint = constraint_id});
2825 }
2826 }
2827 // Above code may have inserted ids in non-stable order.
2828 invalid_indicators.Sort();
2829 return invalid_indicators;
2830}
2831
2832bool GurobiSolver::is_multi_objective_mode() const {
2833 return !multi_objectives_map_.empty();
2834}
2835
2836absl::Status GurobiSolver::SetMultiObjectiveTolerances(
2837 const ModelSolveParametersProto& model_parameters) {
2838 const auto set_tolerances =
2839 [&](const GurobiMultiObjectiveIndex index,
2840 const ObjectiveParametersProto& objective_parameters)
2841 -> absl::Status {
2842 RETURN_IF_ERROR(gurobi_->SetIntParam("ObjNumber", index));
2843 if (objective_parameters.has_objective_degradation_absolute_tolerance()) {
2844 RETURN_IF_ERROR(gurobi_->SetDoubleAttr(
2845 "ObjNAbsTol",
2846 objective_parameters.objective_degradation_absolute_tolerance()));
2847 }
2848 if (objective_parameters.has_objective_degradation_relative_tolerance()) {
2849 RETURN_IF_ERROR(gurobi_->SetDoubleAttr(
2850 "ObjNRelTol",
2851 objective_parameters.objective_degradation_relative_tolerance()));
2852 }
2853 return absl::OkStatus();
2854 };
2855 if (model_parameters.has_primary_objective_parameters()) {
2857 set_tolerances(multi_objectives_map_.at(std::nullopt),
2858 model_parameters.primary_objective_parameters()));
2859 }
2860 for (const auto& [id, objective_parameters] :
2861 model_parameters.auxiliary_objective_parameters()) {
2863 set_tolerances(multi_objectives_map_.at(id), objective_parameters));
2864 }
2865 return absl::OkStatus();
2866}
2867
2868absl::Status GurobiSolver::ResetModelParameters(
2869 const ModelSolveParametersProto& model_parameters) {
2870 for (int i = 0; i < model_parameters.branching_priorities().ids_size(); ++i) {
2871 const int64_t var_id = model_parameters.branching_priorities().ids(i);
2872 const GurobiVariableIndex grb_index = variables_map_.at(var_id);
2874 gurobi_->SetIntAttrElement(GRB_INT_ATTR_BRANCHPRIORITY, grb_index, 0))
2875 << "failed to reset branching priority for variable ID " << var_id
2876 << " (Gurobi index = " << grb_index << ")";
2877 }
2878 for (const int64_t lazy_constraint_id :
2879 model_parameters.lazy_linear_constraint_ids()) {
2880 const GurobiLinearConstraintIndex lazy_constraint_index =
2881 linear_constraints_map_.at(lazy_constraint_id).constraint_index;
2883 gurobi_->SetIntAttrElement(GRB_INT_ATTR_LAZY, lazy_constraint_index, 0))
2884 << "failed to reset lazy constraint for lazy constraint ID "
2885 << lazy_constraint_id << " (Gurobi index = " << lazy_constraint_index
2886 << ")";
2887 }
2888 return absl::OkStatus();
2889}
2890
2891absl::StatusOr<SolveResultProto> GurobiSolver::Solve(
2892 const SolveParametersProto& parameters,
2893 const ModelSolveParametersProto& model_parameters,
2894 const MessageCallback message_cb,
2895 const CallbackRegistrationProto& callback_registration, const Callback cb,
2896 SolveInterrupter* const interrupter) {
2897 const absl::Time start = absl::Now();
2898
2899 // Need to run GRBupdatemodel before:
2900 // 1. setting parameters (to test if the problem is a MIP)
2901 // 2. registering callbacks (to test if the problem is a MIP),
2902 // 3. setting basis and getting the obj sense.
2903 // We just run it first.
2904 RETURN_IF_ERROR(gurobi_->UpdateModel());
2905
2906 // Gurobi returns "infeasible" when bounds are inverted.
2907 {
2908 ASSIGN_OR_RETURN(const InvertedBounds inverted_bounds,
2909 ListInvertedBounds());
2910 RETURN_IF_ERROR(inverted_bounds.ToStatus());
2911 }
2912
2913 // Gurobi will silently impose that indicator variables are binary even if
2914 // not so specified by the user in the model. We return an error here if
2915 // this is the case to be consistent across solvers.
2916 {
2917 ASSIGN_OR_RETURN(const InvalidIndicators invalid_indicators,
2918 ListInvalidIndicators());
2919 RETURN_IF_ERROR(invalid_indicators.ToStatus());
2920 }
2921
2922 // We must set the parameters before calling RegisterCallback since it
2923 // changes some parameters depending on the callback registration.
2924 RETURN_IF_ERROR(SetParameters(parameters));
2925
2926 // We use a local interrupter that will triggers the calls to GRBterminate()
2927 // when either the user interrupter is triggered or when a callback returns
2928 // a true `terminate`.
2929 std::unique_ptr<SolveInterrupter> local_interrupter;
2930 if (cb != nullptr || interrupter != nullptr) {
2931 local_interrupter = std::make_unique<SolveInterrupter>();
2932 }
2933 const ScopedSolveInterrupterCallback scoped_terminate_callback(
2934 local_interrupter.get(), [&]() {
2935 // Make an immediate call to GRBterminate() as soon as this
2936 // interrupter is triggered (which may immediately happen in the code
2937 // below when it is chained with the optional user interrupter).
2938 //
2939 // This call may happen too early. This is not an issue since we will
2940 // repeat this call at each call of the Gurobi callback. See the
2941 // comment in GurobiCallbackImpl() for details.
2942 gurobi_->Terminate();
2943 });
2944
2945 // Chain the user interrupter to the local interrupter. If/when the user
2946 // interrupter is triggered, this triggers the local interrupter. This may
2947 // happen immediately if the user interrupter is already triggered.
2948 //
2949 // The local interrupter can also be triggered by a callback returning a
2950 // true `terminate`.
2951 const ScopedSolveInterrupterCallback scoped_chaining_callback(
2952 interrupter, [&]() { local_interrupter->Interrupt(); });
2953
2954 if (model_parameters.has_initial_basis()) {
2955 RETURN_IF_ERROR(SetGurobiBasis(model_parameters.initial_basis()));
2956 }
2957 RETURN_IF_ERROR(gurobi_->SetIntAttr(GRB_INT_ATTR_NUMSTART,
2958 model_parameters.solution_hints_size()));
2959 for (int i = 0; i < model_parameters.solution_hints_size(); ++i) {
2960 RETURN_IF_ERROR(gurobi_->SetIntParam(GRB_INT_PAR_STARTNUMBER, i));
2961 RETURN_IF_ERROR(UpdateDoubleListAttribute(
2962 model_parameters.solution_hints(i).variable_values(),
2963 GRB_DBL_ATTR_START, variables_map_));
2964 }
2966 UpdateInt32ListAttribute(model_parameters.branching_priorities(),
2967 GRB_INT_ATTR_BRANCHPRIORITY, variables_map_));
2968 if (is_multi_objective_mode()) {
2969 RETURN_IF_ERROR(SetMultiObjectiveTolerances(model_parameters));
2970 }
2971 for (const int64_t lazy_constraint_id :
2972 model_parameters.lazy_linear_constraint_ids()) {
2973 const GurobiLinearConstraintIndex lazy_constraint_index =
2974 linear_constraints_map_.at(lazy_constraint_id).constraint_index;
2975 // We select a value of "1" here, which means that the lazy constraints will
2976 // be separated at feasible solutions, and that Gurobi has latitude to
2977 // select which violated constraints to add to the model if multiple are
2978 // violated. This seems like a reasonable default choice for us, but we may
2979 // want to revisit later (or expose this choice to the user).
2980 RETURN_IF_ERROR(gurobi_->SetIntAttrElement(GRB_INT_ATTR_LAZY,
2981 lazy_constraint_index, 1));
2982 }
2983
2984 // Here we register the callback when we either have a user callback or a
2985 // local interrupter. The rationale for doing so when we have only an
2986 // interrupter is explained in GurobiCallbackImpl().
2987 Gurobi::Callback grb_cb = nullptr;
2988 std::unique_ptr<GurobiCallbackData> gurobi_cb_data;
2989 if (cb != nullptr || local_interrupter != nullptr || message_cb != nullptr) {
2990 ASSIGN_OR_RETURN(gurobi_cb_data,
2991 RegisterCallback(callback_registration, cb, message_cb,
2992 start, local_interrupter.get()));
2993 grb_cb = [&gurobi_cb_data](
2994 const Gurobi::CallbackContext& cb_context) -> absl::Status {
2995 return GurobiCallbackImpl(cb_context, gurobi_cb_data->callback_input,
2996 gurobi_cb_data->message_callback_data,
2997 gurobi_cb_data->local_interrupter);
2998 };
2999 }
3000
3001 RETURN_IF_ERROR(gurobi_->Optimize(grb_cb));
3002
3003 // We flush message callbacks before testing for Gurobi error in case where
3004 // the unfinished line of message would help with the error.
3005 if (gurobi_cb_data != nullptr) {
3006 GurobiCallbackImplFlush(gurobi_cb_data->callback_input,
3007 gurobi_cb_data->message_callback_data);
3008 }
3009
3010 ASSIGN_OR_RETURN(SolveResultProto solve_result,
3011 ExtractSolveResultProto(start, model_parameters));
3012 // Reset Gurobi parameters.
3013 // TODO(b/277246682): ensure that resetting parameters does not degrade
3014 // incrementalism performance.
3015 RETURN_IF_ERROR(gurobi_->ResetParameters());
3016 RETURN_IF_ERROR(ResetModelParameters(model_parameters));
3017
3018 return solve_result;
3019}
3020
3021// TODO(b/277339044): Remove code duplication with GurobiSolver::Solve().
3022absl::StatusOr<ComputeInfeasibleSubsystemResultProto>
3023GurobiSolver::ComputeInfeasibleSubsystem(const SolveParametersProto& parameters,
3024 MessageCallback message_cb,
3025 SolveInterrupter* const interrupter) {
3026 const absl::Time start = absl::Now();
3027
3028 // Need to run GRBupdatemodel before:
3029 // 1. setting parameters (to test if the problem is a MIP)
3030 // 2. registering callbacks (to test if the problem is a MIP),
3031 RETURN_IF_ERROR(gurobi_->UpdateModel());
3032
3033 // Gurobi will silently impose that indicator variables are binary even if
3034 // not so specified by the user in the model. We return an error here if
3035 // this is the case to be consistent across solvers.
3036 {
3037 ASSIGN_OR_RETURN(const InvalidIndicators invalid_indicators,
3038 ListInvalidIndicators());
3039 RETURN_IF_ERROR(invalid_indicators.ToStatus());
3040 }
3041
3042 // We must set the parameters before calling RegisterCallback since it
3043 // changes some parameters depending on the callback registration.
3044 RETURN_IF_ERROR(SetParameters(parameters));
3045
3046 // We use a local interrupter that will triggers the calls to
3047 // GRBterminate() when either the user interrupter is triggered or when a
3048 // callback returns a true `terminate`.
3049 std::unique_ptr<SolveInterrupter> local_interrupter;
3050 if (interrupter != nullptr) {
3051 local_interrupter = std::make_unique<SolveInterrupter>();
3052 }
3053 const ScopedSolveInterrupterCallback scoped_terminate_callback(
3054 local_interrupter.get(), [&]() {
3055 // Make an immediate call to GRBterminate() as soon as this
3056 // interrupter is triggered (which may immediately happen in the
3057 // code below when it is chained with the optional user
3058 // interrupter).
3059 //
3060 // This call may happen too early. This is not an issue since we
3061 // will repeat this call at each call of the Gurobi callback. See
3062 // the comment in GurobiCallbackImpl() for details.
3063 gurobi_->Terminate();
3064 });
3065
3066 // Chain the user interrupter to the local interrupter. If/when the user
3067 // interrupter is triggered, this triggers the local interrupter. This may
3068 // happen immediately if the user interrupter is already triggered.
3069 //
3070 // The local interrupter can also be triggered by a callback returning a
3071 // true `terminate`.
3072 const ScopedSolveInterrupterCallback scoped_chaining_callback(
3073 interrupter, [&]() { local_interrupter->Interrupt(); });
3074
3075 // Here we register the callback when we either have a message callback or a
3076 // local interrupter. The rationale for doing so when we have only an
3077 // interrupter is explained in GurobiCallbackImpl().
3078 Gurobi::Callback grb_cb = nullptr;
3079 std::unique_ptr<GurobiCallbackData> gurobi_cb_data;
3080 if (local_interrupter != nullptr || message_cb != nullptr) {
3081 ASSIGN_OR_RETURN(gurobi_cb_data,
3082 RegisterCallback({}, nullptr, message_cb, start,
3083 local_interrupter.get()));
3084 grb_cb = [&gurobi_cb_data](
3085 const Gurobi::CallbackContext& cb_context) -> absl::Status {
3086 return GurobiCallbackImpl(cb_context, gurobi_cb_data->callback_input,
3087 gurobi_cb_data->message_callback_data,
3088 gurobi_cb_data->local_interrupter);
3089 };
3090 }
3091
3092 ASSIGN_OR_RETURN(const bool proven_infeasible, gurobi_->ComputeIIS(grb_cb));
3093
3094 // We flush message callbacks before testing for Gurobi error in case
3095 // where the unfinished line of message would help with the error.
3096 if (gurobi_cb_data != nullptr) {
3097 GurobiCallbackImplFlush(gurobi_cb_data->callback_input,
3098 gurobi_cb_data->message_callback_data);
3099 }
3100
3102 ComputeInfeasibleSubsystemResultProto iis_result,
3103 ExtractComputeInfeasibleSubsystemResultProto(proven_infeasible));
3104 // Reset Gurobi parameters.
3105 // TODO(b/277246682): ensure that resetting parameters does not degrade
3106 // incrementalism performance.
3107 RETURN_IF_ERROR(gurobi_->ResetParameters());
3108
3109 return iis_result;
3110}
3111
3112MATH_OPT_REGISTER_SOLVER(SOLVER_TYPE_GUROBI, GurobiSolver::New)
3113
3114} // namespace math_opt
3115} // namespace operations_research
int64_t max
int64_t min
#define ASSIGN_OR_RETURN(lhs, rexpr)
#define RETURN_IF_ERROR(expr)
static absl::StatusOr< std::unique_ptr< Gurobi > > NewWithSharedPrimaryEnv(GRBenv *primary_env)
Definition g_gurobi.cc:175
std::function< absl::Status(const CallbackContext &)> Callback
Definition g_gurobi.h:222
static absl::StatusOr< std::unique_ptr< Gurobi > > New(GRBenvUniquePtr primary_env=nullptr)
Definition g_gurobi.cc:181
absl::StatusOr< int > GetIntAttrElement(const char *name, int element) const
Definition g_gurobi.cc:689
std::function< void(const std::vector< std::string > &)> MessageCallback
std::function< absl::StatusOr< CallbackResultProto >( const CallbackDataProto &)> Callback
SatParameters parameters
std::unique_ptr< SharedBoundsManager > bounds
These can be nullptr depending on the options.
const std::string name
A name for logging purposes.
int64_t value
absl::Status status
Definition g_gurobi.cc:44
Gurobi * gurobi
Definition g_gurobi.cc:45
double upper_bound
double lower_bound
#define GRB_INT_PAR_BARITERLIMIT
#define GRB_SUPERBASIC
#define GRB_INT_PAR_LOGTOCONSOLE
#define GRB_DBL_ATTR_UB
#define GRB_INT_ATTR_BRANCHPRIORITY
#define GRB_DBL_ATTR_START
#define GRB_DBL_PAR_MIPGAP
#define GRB_DBL_PAR_FEASIBILITYTOL
#define GRB_SOLUTION_LIMIT
#define GRB_MAXIMIZE
#define GRB_INT_PAR_SOLUTIONLIMIT
#define GRB_NONBASIC_LOWER
#define GRB_INT_ATTR_MODELSENSE
#define GRB_INT_PAR_CUTS
#define GRB_INFINITY
#define GRB_INT_ATTR_VBASIS
#define GRB_GREATER_EQUAL
#define GRB_INT_PAR_SEED
#define GRB_DBL_ATTR_NODECOUNT
#define GRB_INT_PAR_PRESOLVE
#define GRB_DBL_PAR_MIPGAPABS
#define GRB_DBL_ATTR_ITERCOUNT
#define GRB_INT_PAR_THREADS
#define GRB_DBL_ATTR_BOUND_SVIO
#define GRB_OPTIMAL
#define GRB_DBL_PAR_CUTOFF
#define GRB_INT_ATTR_IIS_QCONSTR
#define GRB_INTEGER
#define GRB_DBL_PAR_ITERATIONLIMIT
#define GRB_INT_PAR_METHOD
#define GRB_INT_ATTR_IS_QP
#define GRB_DBL_ATTR_PI
#define GRB_DBL_ATTR_OBJVAL
#define GRB_NODE_LIMIT
#define GRB_INT_PAR_LAZYCONSTRAINTS
#define GRB_DBL_ATTR_XN
#define GRB_INT_PAR_OBJNUMBER
#define GRB_CONTINUOUS
#define GRB_INT_PAR_SCALEFLAG
#define GRB_DBL_ATTR_OBJ
#define GRB_DBL_PAR_HEURISTICS
#define GRB_METHOD_BARRIER
#define GRB_INT_ATTR_IS_MIP
#define GRB_DBL_ATTR_CONSTR_RESIDUAL
#define GRB_DBL_ATTR_OBJNVAL
#define GRB_SOS_TYPE1
#define GRB_INT_PAR_POOLSOLUTIONS
#define GRB_CHAR_ATTR_VTYPE
#define GRB_INT_ATTR_NUMSTART
#define GRB_DBL_ATTR_BOUND_VIO
#define GRB_TIME_LIMIT
#define GRB_NONBASIC_UPPER
#define GRB_INT_ATTR_IIS_UB
#define GRB_DBL_ATTR_OBJCON
#define GRB_DBL_PAR_BESTBDSTOP
#define GRB_STR_ATTR_MODELNAME
#define GRB_DBL_ATTR_CONSTR_VIO
#define GRB_DBL_ATTR_RC
#define GRB_DBL_ATTR_CONSTR_SRESIDUAL
#define GRB_LOADED
#define GRB_DBL_ATTR_CONSTR_SVIO
#define GRB_INPROGRESS
#define GRB_INT_ATTR_IS_QCP
#define GRB_DBL_PAR_BESTOBJSTOP
#define GRB_INT_ATTR_IIS_GENCONSTR
#define GRB_INF_OR_UNBD
#define GRB_DBL_ATTR_X
#define GRB_SUBOPTIMAL
#define GRB_INFEASIBLE
#define GRB_INT_ATTR_IIS_MINIMAL
#define GRB_DBL_ATTR_RHS
#define GRB_MAXINT
#define GRB_INT_PAR_STARTNUMBER
#define GRB_EQUAL
#define GRB_SOS_TYPE2
#define GRB_CHAR_ATTR_QCSENSE
#define GRB_CHAR_ATTR_SENSE
#define GRB_CUTOFF
#define GRB_UNBOUNDED
#define GRB_INT_ATTR_IIS_SOS
#define GRB_INT_ATTR_CBASIS
#define GRB_METHOD_DUAL
#define GRB_INT_ATTR_IIS_CONSTR
#define GRB_INT_ATTR_BARITERCOUNT
#define GRB_BASIC
#define GRB_MINIMIZE
#define GRB_INT_ATTR_STATUS
#define GRB_DBL_ATTR_FARKASDUAL
#define GRB_LESS_EQUAL
#define GRB_INTERRUPTED
#define GRB_DBL_ATTR_POOLOBJVAL
#define GRB_DBL_ATTR_LB
#define GRB_INT_PAR_SOLUTIONNUMBER
#define GRB_ITERATION_LIMIT
#define GRB_INT_ATTR_SOLCOUNT
#define GRB_NUMERIC
#define GRB_METHOD_PRIMAL
#define GRB_INT_ATTR_IIS_LB
#define GRB_DBL_PAR_TIMELIMIT
#define GRB_BINARY
#define GRB_DBL_PAR_NODELIMIT
#define GRB_USER_OBJ_LIMIT
#define GRB_DBL_ATTR_UNBDRAY
#define GRB_DBL_ATTR_OBJBOUND
#define GRB_INT_PAR_PRECRUSH
#define GRB_INT_ATTR_LAZY
int constraint_index
time_limit
Definition solve.cc:22
int index
double solution
void InsertOrDie(Collection *const collection, const typename Collection::value_type &value)
Definition map_util.h:154
auto & InsertKeyOrDie(Collection *const collection, const typename Collection::value_type::first_type &key)
Definition map_util.h:173
TerminationProto TerminateForReason(const TerminationReasonProto reason, const absl::string_view detail)
absl::Status ModelIsSupported(const ModelProto &model, const SupportedProblemStructures &support_menu, const absl::string_view solver_name)
std::vector< bool > EventToGurobiWhere(const absl::flat_hash_set< CallbackEventProto > &events)
absl::StatusOr< GRBenvUniquePtr > NewPrimaryEnvironment(std::optional< GurobiInitializerProto::ISVKey > proto_isv_key)
std::function< CallbackResult(const CallbackData &)> Callback
Definition callback.h:93
absl::flat_hash_set< CallbackEventProto > EventSet(const CallbackRegistrationProto &callback_registration)
Returns the callback_registration.request_registration as a set of enums.
TerminationProto OptimalTerminationProto(const double finite_primal_objective, const double dual_objective, const absl::string_view detail)
TerminationProto LimitTerminationProto(const bool is_maximize, const LimitProto limit, const std::optional< double > optional_finite_primal_objective, const std::optional< double > optional_dual_objective, const absl::string_view detail)
TerminationProto InfeasibleOrUnboundedTerminationProto(bool is_maximize, const FeasibilityStatusProto dual_feasibility_status, const absl::string_view detail)
bool UpdateIsSupported(const ModelUpdateProto &update, const SupportedProblemStructures &support_menu)
std::function< void(const std::vector< std::string > &)> MessageCallback
void GurobiCallbackImplFlush(const GurobiCallbackInput &callback_input, MessageCallbackData &message_callback_data)
absl::Status CheckRegisteredCallbackEvents(const CallbackRegistrationProto &registration, const absl::flat_hash_set< CallbackEventProto > &supported_events)
SparseVectorView< T > MakeView(absl::Span< const int64_t > ids, const Collection &values)
TerminationProto CutoffTerminationProto(bool is_maximize, const absl::string_view detail)
Calls NoSolutionFoundTerminationProto() with LIMIT_CUTOFF LIMIT.
absl::Status GurobiCallbackImpl(const Gurobi::CallbackContext &context, const GurobiCallbackInput &callback_input, MessageCallbackData &message_callback_data, SolveInterrupter *const local_interrupter)
std::unique_ptr< GRBenv, GurobiFreeEnv > GRBenvUniquePtr
Definition g_gurobi.h:59
TerminationProto InfeasibleTerminationProto(bool is_maximize, const FeasibilityStatusProto dual_feasibility_status, const absl::string_view detail)
TerminationProto UnboundedTerminationProto(const bool is_maximize, const absl::string_view detail)
std::vector< K > SortedMapKeys(const absl::flat_hash_map< K, V > &in_map)
Definition sorted.h:55
In SWIG mode, we don't want anything besides these top-level includes.
bool GurobiIsCorrectlyInstalled()
std::string ProtoEnumToString(ProtoEnumType enum_value)
Definition proto_utils.h:50
constexpr bool kAnyXsanEnabled
STL namespace.
inline ::absl::StatusOr< absl::Duration > DecodeGoogleApiProto(const google::protobuf::Duration &proto)
Definition protoutil.h:42
inline ::absl::StatusOr< google::protobuf::Duration > EncodeGoogleApiProto(absl::Duration d)
Definition protoutil.h:27
StatusBuilder InternalErrorBuilder()
StatusBuilder UnimplementedErrorBuilder()
StatusBuilder InvalidArgumentErrorBuilder()
int column
int nodes
std::vector< double > lower_bounds
Definition lp_utils.cc:745
std::vector< int > var_indices
Definition lp_utils.cc:743
std::vector< double > upper_bounds
Definition lp_utils.cc:746
int var_index
Definition search.cc:3268
#define MATH_OPT_REGISTER_SOLVER(solver_type, solver_factory)
int64_t start
std::string message
Definition trace.cc:397
double objective_value
The value objective_vector^T * (solution - center_point).