Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
gurobi_callback.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 <cstdint>
17#include <functional>
18#include <limits>
19#include <optional>
20#include <string>
21#include <utility>
22#include <vector>
23
24#include "absl/container/flat_hash_set.h"
25#include "absl/status/status.h"
26#include "absl/status/statusor.h"
27#include "absl/strings/str_cat.h"
28#include "absl/time/clock.h"
29#include "absl/time/time.h"
30#include "absl/types/span.h"
36#include "ortools/math_opt/callback.pb.h"
40#include "ortools/math_opt/solution.pb.h"
42#include "ortools/math_opt/sparse_containers.pb.h"
44
45namespace operations_research {
46namespace math_opt {
47namespace {
48
49// The number of possible values for "where" that Gurobi's callbacks can stop
50// at, see the table here:
51// https://www.gurobi.com/documentation/9.1/refman/cb_codes.html
52constexpr int kNumGurobiEvents = 9;
53constexpr double kInf = std::numeric_limits<double>::infinity();
54
55template <int where>
56constexpr int CheckedGuroibWhere() {
57 static_assert(where >= 0 && where < kNumGurobiEvents);
58 return where;
59}
60
61inline int GurobiEvent(CallbackEventProto event) {
62 switch (event) {
63 case CALLBACK_EVENT_PRESOLVE:
64 return CheckedGuroibWhere<GRB_CB_PRESOLVE>();
65 case CALLBACK_EVENT_SIMPLEX:
66 return CheckedGuroibWhere<GRB_CB_SIMPLEX>();
67 case CALLBACK_EVENT_MIP:
68 return CheckedGuroibWhere<GRB_CB_MIP>();
69 case CALLBACK_EVENT_MIP_SOLUTION:
70 return CheckedGuroibWhere<GRB_CB_MIPSOL>();
71 case CALLBACK_EVENT_MIP_NODE:
72 return CheckedGuroibWhere<GRB_CB_MIPNODE>();
73 case CALLBACK_EVENT_BARRIER:
74 return CheckedGuroibWhere<GRB_CB_BARRIER>();
75 case CALLBACK_EVENT_UNSPECIFIED:
76 default:
77 LOG(FATAL) << "Unexpected callback event: " << event;
78 }
79}
80
81SparseDoubleVectorProto ApplyFilter(
82 const std::vector<double>& grb_solution,
84 const SparseVectorFilterProto& filter) {
85 SparseVectorFilterPredicate predicate(filter);
86 SparseDoubleVectorProto result;
87 for (const auto [id, grb_index] : var_ids) {
88 const double val = grb_solution[grb_index];
89 if (predicate.AcceptsAndUpdate(id, val)) {
90 result.add_ids(id);
91 result.add_values(val);
92 }
93 }
94 return result;
95}
96
97absl::StatusOr<int64_t> CbGetInt64(const Gurobi::CallbackContext& context,
98 int what) {
99 ASSIGN_OR_RETURN(const double result, context.CbGetDouble(what));
100 int64_t result64 = static_cast<int64_t>(result);
101 if (result != static_cast<double>(result64)) {
102 return absl::InternalError(
103 absl::StrCat("Error converting double attribute: ", what,
104 "with value: ", result, " to int64_t exactly."));
105 }
106 return result64;
107}
108
109absl::StatusOr<bool> CbGetBool(const Gurobi::CallbackContext& context,
110 int what) {
111 ASSIGN_OR_RETURN(const int result, context.CbGetInt(what));
112 bool result_bool = static_cast<bool>(result);
113 if (result != static_cast<int>(result_bool)) {
114 return absl::InternalError(
115 absl::StrCat("Error converting int attribute: ", what,
116 "with value: ", result, " to bool exactly."));
117 }
118 return result_bool;
119}
120
121// Invokes setter on a non-error value in statusor or returns the error.
122//
123// Requires that statusor is a StatusOr<T> and setter(T) is a function.
124#define MO_SET_OR_RET(setter, statusor) \
125 do { \
126 auto eval_status_or = statusor; \
127 RETURN_IF_ERROR(eval_status_or.status()) << __FILE__ << ":" << __LINE__; \
128 setter(*std::move(eval_status_or)); \
129 } while (false)
130
131// Sets the CallbackDataProto.runtime field using the difference between the
132// current wall clock time and the start time of the solve.
133absl::Status SetRuntime(const GurobiCallbackInput& callback_input,
134 CallbackDataProto& callback_data) {
135 return util_time::EncodeGoogleApiProto(absl::Now() - callback_input.start,
136 callback_data.mutable_runtime());
137}
138
139// Returns the data for the next callback. Returns nullopt if no callback is
140// needed.
141absl::StatusOr<std::optional<CallbackDataProto>> CreateCallbackDataProto(
142 const Gurobi::CallbackContext& c, const GurobiCallbackInput& callback_input,
143 MessageCallbackData& message_callback_data) {
144 CallbackDataProto callback_data;
145
146 // Query information from Gurobi.
147 switch (c.where()) {
148 case GRB_CB_PRESOLVE: {
149 callback_data.set_event(CALLBACK_EVENT_PRESOLVE);
150 CallbackDataProto::PresolveStats* const s =
151 callback_data.mutable_presolve_stats();
152 MO_SET_OR_RET(s->set_removed_variables, c.CbGetInt(GRB_CB_PRE_COLDEL));
153 MO_SET_OR_RET(s->set_removed_constraints, c.CbGetInt(GRB_CB_PRE_ROWDEL));
154 MO_SET_OR_RET(s->set_bound_changes, c.CbGetInt(GRB_CB_PRE_BNDCHG));
155 MO_SET_OR_RET(s->set_coefficient_changes, c.CbGetInt(GRB_CB_PRE_COECHG));
156 break;
157 }
158 case GRB_CB_SIMPLEX: {
159 callback_data.set_event(CALLBACK_EVENT_SIMPLEX);
160 CallbackDataProto::SimplexStats* const s =
161 callback_data.mutable_simplex_stats();
162 MO_SET_OR_RET(s->set_iteration_count, CbGetInt64(c, GRB_CB_SPX_ITRCNT));
163 MO_SET_OR_RET(s->set_is_pertubated, CbGetBool(c, GRB_CB_SPX_ISPERT));
164 MO_SET_OR_RET(s->set_objective_value, c.CbGetDouble(GRB_CB_SPX_OBJVAL));
165 MO_SET_OR_RET(s->set_primal_infeasibility,
166 c.CbGetDouble(GRB_CB_SPX_PRIMINF));
167 MO_SET_OR_RET(s->set_dual_infeasibility,
168 c.CbGetDouble(GRB_CB_SPX_DUALINF));
169 break;
170 }
171 case GRB_CB_BARRIER: {
172 callback_data.set_event(CALLBACK_EVENT_BARRIER);
173 CallbackDataProto::BarrierStats* const s =
174 callback_data.mutable_barrier_stats();
175 MO_SET_OR_RET(s->set_iteration_count, c.CbGetInt(GRB_CB_BARRIER_ITRCNT));
176 MO_SET_OR_RET(s->set_primal_objective,
177 c.CbGetDouble(GRB_CB_BARRIER_PRIMOBJ));
178 MO_SET_OR_RET(s->set_dual_objective,
179 c.CbGetDouble(GRB_CB_BARRIER_DUALOBJ));
180 MO_SET_OR_RET(s->set_primal_infeasibility,
181 c.CbGetDouble(GRB_CB_BARRIER_PRIMINF));
182 MO_SET_OR_RET(s->set_dual_infeasibility,
183 c.CbGetDouble(GRB_CB_BARRIER_DUALINF));
184 MO_SET_OR_RET(s->set_complementarity,
185 c.CbGetDouble(GRB_CB_BARRIER_COMPL));
186 break;
187 }
188 case GRB_CB_MIP: {
189 callback_data.set_event(CALLBACK_EVENT_MIP);
190 CallbackDataProto::MipStats* const s = callback_data.mutable_mip_stats();
191 MO_SET_OR_RET(s->set_primal_bound, c.CbGetDouble(GRB_CB_MIP_OBJBST));
192 MO_SET_OR_RET(s->set_dual_bound, c.CbGetDouble(GRB_CB_MIP_OBJBND));
193 MO_SET_OR_RET(s->set_explored_nodes, CbGetInt64(c, GRB_CB_MIP_NODCNT));
194 MO_SET_OR_RET(s->set_open_nodes, CbGetInt64(c, GRB_CB_MIP_NODLFT));
195 MO_SET_OR_RET(s->set_simplex_iterations,
196 CbGetInt64(c, GRB_CB_MIP_ITRCNT));
197 MO_SET_OR_RET(s->set_number_of_solutions_found,
198 c.CbGetInt(GRB_CB_MIP_SOLCNT));
199 MO_SET_OR_RET(s->set_cutting_planes_in_lp, c.CbGetInt(GRB_CB_MIP_CUTCNT));
200
201 break;
202 }
203 case GRB_CB_MIPSOL: {
204 callback_data.set_event(CALLBACK_EVENT_MIP_SOLUTION);
205 CallbackDataProto::MipStats* const s = callback_data.mutable_mip_stats();
206 MO_SET_OR_RET(s->set_primal_bound, c.CbGetDouble(GRB_CB_MIPSOL_OBJBST));
207 MO_SET_OR_RET(s->set_dual_bound, c.CbGetDouble(GRB_CB_MIPSOL_OBJBND));
208 MO_SET_OR_RET(s->set_explored_nodes, CbGetInt64(c, GRB_CB_MIPSOL_NODCNT));
209 MO_SET_OR_RET(s->set_number_of_solutions_found,
210 c.CbGetInt(GRB_CB_MIPSOL_SOLCNT));
211 std::vector<double> var_values(callback_input.num_gurobi_vars);
213 c.CbGetDoubleArray(GRB_CB_MIPSOL_SOL, absl::MakeSpan(var_values)))
214 << "Error reading solution at event MIP_SOLUTION";
215 *callback_data.mutable_primal_solution_vector() =
216 ApplyFilter(var_values, callback_input.variable_ids,
217 callback_input.mip_solution_filter);
218 break;
219 }
220 case GRB_CB_MIPNODE: {
221 callback_data.set_event(CALLBACK_EVENT_MIP_NODE);
222 CallbackDataProto::MipStats* const s = callback_data.mutable_mip_stats();
223 MO_SET_OR_RET(s->set_primal_bound, c.CbGetDouble(GRB_CB_MIPNODE_OBJBST));
224 MO_SET_OR_RET(s->set_dual_bound, c.CbGetDouble(GRB_CB_MIPNODE_OBJBND));
225 MO_SET_OR_RET(s->set_explored_nodes,
226 CbGetInt64(c, GRB_CB_MIPNODE_NODCNT));
227
228 MO_SET_OR_RET(s->set_number_of_solutions_found,
229 c.CbGetInt(GRB_CB_MIPNODE_SOLCNT));
230 const absl::StatusOr<int> grb_status = c.CbGetInt(GRB_CB_MIPNODE_STATUS);
231 RETURN_IF_ERROR(grb_status.status())
232 << "Error reading solution status at event MIP_NODE";
233 if (*grb_status == GRB_OPTIMAL) {
234 std::vector<double> var_values(callback_input.num_gurobi_vars);
236 c.CbGetDoubleArray(GRB_CB_MIPNODE_REL, absl::MakeSpan(var_values)))
237 << "Error reading solution at event MIP_NODE";
238 *callback_data.mutable_primal_solution_vector() =
239 ApplyFilter(var_values, callback_input.variable_ids,
240 callback_input.mip_node_filter);
241 // Note: Gurobi does not offer an objective value for the LP relaxation.
242 }
243 break;
244 }
245 default:
246 LOG(FATAL) << "Unknown gurobi callback code " << c.where();
247 }
248
249 RETURN_IF_ERROR(SetRuntime(callback_input, callback_data))
250 << "Error encoding runtime at callback event: " << c.where();
251
252 return callback_data;
253}
254
255#undef MO_SET_OR_RET
256
257absl::Status ApplyResult(const Gurobi::CallbackContext& context,
258 const GurobiCallbackInput& callback_input,
259 const CallbackResultProto& result,
260 SolveInterrupter& local_interrupter) {
261 for (const CallbackResultProto::GeneratedLinearConstraint& cut :
262 result.cuts()) {
263 std::vector<int> gurobi_vars;
264 gurobi_vars.reserve(cut.linear_expression().ids_size());
265 for (const int64_t id : cut.linear_expression().ids()) {
266 gurobi_vars.push_back(callback_input.variable_ids.at(id));
267 }
268 std::vector<std::pair<char, double>> sense_bound_pairs;
269 if (cut.lower_bound() == cut.upper_bound()) {
270 sense_bound_pairs.emplace_back(GRB_EQUAL, cut.upper_bound());
271 } else {
272 if (cut.upper_bound() < kInf) {
273 sense_bound_pairs.emplace_back(GRB_LESS_EQUAL, cut.upper_bound());
274 }
275 if (cut.lower_bound() > -kInf) {
276 sense_bound_pairs.emplace_back(GRB_GREATER_EQUAL, cut.lower_bound());
277 }
278 }
279 for (const auto [sense, bound] : sense_bound_pairs) {
280 if (cut.is_lazy()) {
282 gurobi_vars, cut.linear_expression().values(), sense, bound));
283 } else {
285 gurobi_vars, cut.linear_expression().values(), sense, bound));
286 }
287 }
288 }
289 for (const SparseDoubleVectorProto& solution_vector :
290 result.suggested_solutions()) {
291 // TODO(b/175829773): we cannot fill in auxiliary variables from range
292 // constraints.
293 std::vector<double> gurobi_var_values(callback_input.num_gurobi_vars,
295 for (const auto [id, value] : MakeView(solution_vector)) {
296 gurobi_var_values[callback_input.variable_ids.at(id)] = value;
297 }
298 RETURN_IF_ERROR(context.CbSolution(gurobi_var_values).status());
299 }
300
301 if (result.terminate()) {
302 local_interrupter.Interrupt();
303 return absl::OkStatus();
304 }
305 return absl::OkStatus();
306}
307
308} // namespace
309
310std::vector<bool> EventToGurobiWhere(
311 const absl::flat_hash_set<CallbackEventProto>& events) {
312 std::vector<bool> result(kNumGurobiEvents);
313 for (const auto event : events) {
314 result[GurobiEvent(event)] = true;
315 }
316 return result;
317}
318
320 const GurobiCallbackInput& callback_input,
321 MessageCallbackData& message_callback_data,
322 SolveInterrupter* const local_interrupter) {
323 // Gurobi 9 ignores early calls to GRBterminate(). For example calling
324 // GRBterminate() in the first call of a MESSAGE callback only will not
325 // interrupt the solve. The rationale is that it is likely Gurobi resets its
326 // own internal "terminated" flag at the beginning of the solve but do make
327 // some callbacks calls first.
328 //
329 // Hence here we make sure to call GRBterminate() for every event once the
330 // interrupter has been triggered. This in particular includes POLLING which
331 // is regularly emitted by Gurobi during the solve.
332 if (local_interrupter != nullptr && local_interrupter->IsInterrupted()) {
333 context.gurobi()->Terminate();
334 }
335
336 // The POLLING event is a way for interactive applications that uses Gurobi
337 // but don't want to deal with threading to regain some kind of interactivity
338 // while a long solve is running by being called back from time to time. No
339 // data can be retrieved from this event. This event if thus not wrapped by
340 // MathOpt.
341 if (context.where() == GRB_CB_POLLING) {
342 return absl::OkStatus();
343 }
344 if (context.where() == GRB_CB_MESSAGE) {
345 if (callback_input.message_cb) {
346 const absl::StatusOr<std::string> msg = context.CbGetMessage();
347 RETURN_IF_ERROR(msg.status())
348 << "Error getting message string in callback";
349 const std::vector<std::string> lines = message_callback_data.Parse(*msg);
350 if (!lines.empty()) {
351 callback_input.message_cb(lines);
352 }
353 }
354 return absl::OkStatus();
355 }
356
357 if (callback_input.user_cb == nullptr ||
358 !callback_input.events[context.where()]) {
359 return absl::OkStatus();
360 }
361 // At this point we know we have a user callback, thus we must have a local
362 // interrupter to deal with termination.
363 CHECK(local_interrupter != nullptr);
364
366 const std::optional<CallbackDataProto> callback_data,
367 CreateCallbackDataProto(context, callback_input, message_callback_data));
368 if (!callback_data) {
369 return absl::OkStatus();
370 }
371 const absl::StatusOr<CallbackResultProto> result =
372 callback_input.user_cb(*callback_data);
373 if (!result.ok()) {
374 local_interrupter->Interrupt();
375 return result.status();
376 }
378 ApplyResult(context, callback_input, *result, *local_interrupter));
379 return absl::OkStatus();
380}
381
383 MessageCallbackData& message_callback_data) {
384 const std::vector<std::string> lines = message_callback_data.Flush();
385 if (lines.empty()) {
386 return;
387 }
388
389 // Here we know that message_callback_data has only been filled-in if
390 // message_cb was not nullptr. Hence it is safe to make this call without
391 // testing.
392 callback_input.message_cb(lines);
393}
394
395} // namespace math_opt
396} // namespace operations_research
#define ASSIGN_OR_RETURN(lhs, rexpr)
#define RETURN_IF_ERROR(expr)
std::vector< std::string > Parse(absl::string_view message)
int64_t value
#define GRB_CB_MIP_NODLFT
#define GRB_CB_SPX_ISPERT
#define GRB_CB_MIPSOL
#define GRB_CB_SPX_OBJVAL
#define GRB_CB_BARRIER
#define GRB_CB_PRE_COECHG
#define GRB_GREATER_EQUAL
#define GRB_CB_MIPSOL_OBJBST
#define GRB_CB_MIP_ITRCNT
#define GRB_CB_MIPSOL_SOLCNT
#define GRB_CB_SPX_PRIMINF
#define GRB_OPTIMAL
#define GRB_CB_MIPNODE_REL
#define GRB_CB_MIPNODE_OBJBND
#define GRB_CB_SIMPLEX
#define GRB_CB_MIPNODE_OBJBST
#define GRB_CB_SPX_ITRCNT
#define GRB_CB_BARRIER_COMPL
#define GRB_CB_MIPSOL_NODCNT
#define GRB_CB_MIPNODE_STATUS
#define GRB_CB_PRE_ROWDEL
#define GRB_CB_BARRIER_PRIMOBJ
#define GRB_CB_PRESOLVE
#define GRB_CB_MIPNODE_SOLCNT
#define GRB_CB_MIP
#define GRB_CB_SPX_DUALINF
#define GRB_EQUAL
#define GRB_CB_MIP_OBJBST
#define GRB_CB_MIPNODE
#define GRB_CB_POLLING
#define GRB_CB_BARRIER_DUALINF
#define GRB_CB_BARRIER_ITRCNT
#define GRB_CB_MESSAGE
#define GRB_CB_MIP_CUTCNT
#define GRB_LESS_EQUAL
#define GRB_CB_BARRIER_DUALOBJ
#define GRB_CB_MIP_SOLCNT
#define GRB_CB_PRE_COLDEL
#define GRB_CB_MIP_NODCNT
#define GRB_UNDEFINED
#define GRB_CB_MIPSOL_SOL
#define GRB_CB_MIPSOL_OBJBND
#define GRB_CB_MIP_OBJBND
#define GRB_CB_BARRIER_PRIMINF
#define GRB_CB_PRE_BNDCHG
#define GRB_CB_MIPNODE_NODCNT
#define MO_SET_OR_RET(setter, statusor)
GurobiMPCallbackContext * context
int where
std::vector< bool > EventToGurobiWhere(const absl::flat_hash_set< CallbackEventProto > &events)
void GurobiCallbackImplFlush(const GurobiCallbackInput &callback_input, MessageCallbackData &message_callback_data)
SparseVectorView< T > MakeView(absl::Span< const int64_t > ids, const Collection &values)
absl::Status GurobiCallbackImpl(const Gurobi::CallbackContext &context, const GurobiCallbackInput &callback_input, MessageCallbackData &message_callback_data, SolveInterrupter *const local_interrupter)
In SWIG mode, we don't want anything besides these top-level includes.
inline ::absl::StatusOr< google::protobuf::Duration > EncodeGoogleApiProto(absl::Duration d)
Definition protoutil.h:27