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