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