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