Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
callback.py
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
14"""Defines how to request a callback and the input and output of a callback."""
15import dataclasses
16import datetime
17import enum
18import math
19from typing import Dict, List, Mapping, Optional, Set, Union
20
21from ortools.math_opt import callback_pb2
22from ortools.math_opt.python import model
23from ortools.math_opt.python import sparse_containers
24
25
26@enum.unique
27class Event(enum.Enum):
28 """The supported events during a solve for callbacks.
29
30 * UNSPECIFIED: The event is unknown (typically an internal error).
31 * PRESOLVE: The solver is currently running presolve. Gurobi only.
32 * SIMPLEX: The solver is currently running the simplex method. Gurobi only.
33 * MIP: The solver is in the MIP loop (called periodically before starting a
34 new node). Useful for early termination. Note that this event does not
35 provide information on LP relaxations nor about new incumbent solutions.
36 Gurobi only.
37 * MIP_SOLUTION: Called every time a new MIP incumbent is found. Fully
38 supported by Gurobi, partially supported by CP-SAT (you can observe new
39 solutions, but not add lazy constraints).
40 * MIP_NODE: Called inside a MIP node. Note that there is no guarantee that the
41 callback function will be called on every node. That behavior is
42 solver-dependent. Gurobi only.
43
44 Disabling cuts using SolveParameters may interfere with this event being
45 called and/or adding cuts at this event, the behavior is solver specific.
46 * BARRIER: Called in each iterate of an interior point/barrier method. Gurobi
47 only.
48 """
49
50 UNSPECIFIED = callback_pb2.CALLBACK_EVENT_UNSPECIFIED
51 PRESOLVE = callback_pb2.CALLBACK_EVENT_PRESOLVE
52 SIMPLEX = callback_pb2.CALLBACK_EVENT_SIMPLEX
53 MIP = callback_pb2.CALLBACK_EVENT_MIP
54 MIP_SOLUTION = callback_pb2.CALLBACK_EVENT_MIP_SOLUTION
55 MIP_NODE = callback_pb2.CALLBACK_EVENT_MIP_NODE
56 BARRIER = callback_pb2.CALLBACK_EVENT_BARRIER
57
58
59PresolveStats = callback_pb2.CallbackDataProto.PresolveStats
60SimplexStats = callback_pb2.CallbackDataProto.SimplexStats
61BarrierStats = callback_pb2.CallbackDataProto.BarrierStats
62MipStats = callback_pb2.CallbackDataProto.MipStats
63
64
65@dataclasses.dataclass
67 """Input to the solve callback (produced by the solver).
68
69 Attributes:
70 event: The current state of the solver when the callback is run. The event
71 (partially) determines what data is available and what the user is allowed
72 to return.
73 solution: A solution to the primal optimization problem, if available. For
74 Event.MIP_SOLUTION, solution is always present, integral, and feasible.
75 For Event.MIP_NODE, the primal_solution contains the current LP-node
76 relaxation. In some cases, no solution will be available (e.g. because LP
77 was infeasible or the solve was imprecise). Empty for other events.
78 messages: Logs generated by the underlying solver, as a list of strings
79 without new lines (each string is a line). Only filled on Event.MESSAGE.
80 runtime: The time since Solve() was invoked.
81 presolve_stats: Filled for Event.PRESOLVE only.
82 simplex_stats: Filled for Event.SIMPLEX only.
83 barrier_stats: Filled for Event.BARRIER only.
84 mip_stats: Filled for the events MIP, MIP_SOLUTION and MIP_NODE only.
85 """
86
87 event: Event = Event.UNSPECIFIED
88 solution: Optional[Dict[model.Variable, float]] = None
89 messages: List[str] = dataclasses.field(default_factory=list)
90 runtime: datetime.timedelta = datetime.timedelta()
91 presolve_stats: PresolveStats = dataclasses.field(default_factory=PresolveStats)
92 simplex_stats: SimplexStats = dataclasses.field(default_factory=SimplexStats)
93 barrier_stats: BarrierStats = dataclasses.field(default_factory=BarrierStats)
94 mip_stats: MipStats = dataclasses.field(default_factory=MipStats)
95
96
98 cb_data: callback_pb2.CallbackDataProto, mod: model.Model
99) -> CallbackData:
100 """Creates a CallbackData from an equivalent proto.
101
102 Args:
103 cb_data: A protocol buffer with the information the user needs for a
104 callback.
105 mod: The model being solved.
106
107 Returns:
108 An equivalent CallbackData.
109
110 Raises:
111 ValueError: if cb_data is invalid or inconsistent with mod, e.g. cb_data
112 refers to a variable id not in mod.
113 """
114 result = CallbackData()
115 result.event = Event(cb_data.event)
116 if cb_data.HasField("primal_solution_vector"):
117 primal_solution = cb_data.primal_solution_vector
118 result.solution = {
119 mod.get_variable(id): val
120 for (id, val) in zip(primal_solution.ids, primal_solution.values)
121 }
122 result.runtime = cb_data.runtime.ToTimedelta()
123 result.presolve_stats = cb_data.presolve_stats
124 result.simplex_stats = cb_data.simplex_stats
125 result.barrier_stats = cb_data.barrier_stats
126 result.mip_stats = cb_data.mip_stats
127 return result
128
129
130@dataclasses.dataclass
132 """Request the events and input data and reports output types for a callback.
133
134 Note that it is an error to add a constraint in a callback without setting
135 add_cuts and/or add_lazy_constraints to true.
136
137 Attributes:
138 events: When the callback should be invoked, by default, never. If an
139 unsupported event for a solver/model combination is selected, an
140 excecption is raised, see Event above for details.
141 mip_solution_filter: restricts the variable values returned in
142 CallbackData.solution (the callback argument) at each MIP_SOLUTION event.
143 By default, values are returned for all variables.
144 mip_node_filter: restricts the variable values returned in
145 CallbackData.solution (the callback argument) at each MIP_NODE event. By
146 default, values are returned for all variables.
147 add_cuts: The callback may add "user cuts" (linear constraints that
148 strengthen the LP without cutting of integer points) at MIP_NODE events.
149 add_lazy_constraints: The callback may add "lazy constraints" (linear
150 constraints that cut off integer solutions) at MIP_NODE or MIP_SOLUTION
151 events.
152 """
153
154 events: Set[Event] = dataclasses.field(default_factory=set)
155 mip_solution_filter: sparse_containers.VariableFilter = (
156 sparse_containers.VariableFilter()
157 )
158 mip_node_filter: sparse_containers.VariableFilter = (
159 sparse_containers.VariableFilter()
160 )
161 add_cuts: bool = False
162 add_lazy_constraints: bool = False
163
164 def to_proto(self) -> callback_pb2.CallbackRegistrationProto:
165 """Returns an equivalent proto to this CallbackRegistration."""
166 result = callback_pb2.CallbackRegistrationProto()
167 result.request_registration[:] = sorted([event.value for event in self.events])
168 result.mip_solution_filter.CopyFrom(self.mip_solution_filter.to_proto())
169 result.mip_node_filter.CopyFrom(self.mip_node_filter.to_proto())
170 result.add_cuts = self.add_cuts
171 result.add_lazy_constraints = self.add_lazy_constraints
172 return result
173
174
175@dataclasses.dataclass
177 """A linear constraint to add inside a callback.
178
179 Models a constraint of the form:
180 lb <= sum_{i in I} a_i * x_i <= ub
181
182 Two types of generated linear constraints are supported based on is_lazy:
183 * The "lazy constraint" can remove integer points from the feasible
184 region and can be added at event Event.MIP_NODE or
185 Event.MIP_SOLUTION
186 * The "user cut" (on is_lazy=false) strengthens the LP without removing
187 integer points. It can only be added at Event.MIP_NODE.
188
189
190 Attributes:
191 terms: The variables and linear coefficients in the constraint, a_i and x_i
192 in the model above.
193 lower_bound: lb in the model above.
194 upper_bound: ub in the model above.
195 is_lazy: Indicates if the constraint should be interpreted as a "lazy
196 constraint" (cuts off integer solutions) or a "user cut" (strengthens the
197 LP relaxation without cutting of integer solutions).
198 """
199
200 terms: Mapping[model.Variable, float] = dataclasses.field(default_factory=dict)
201 lower_bound: float = -math.inf
202 upper_bound: float = math.inf
203 is_lazy: bool = False
204
206 self,
207 ) -> callback_pb2.CallbackResultProto.GeneratedLinearConstraint:
208 """Returns an equivalent proto for the constraint."""
209 result = callback_pb2.CallbackResultProto.GeneratedLinearConstraint()
210 result.is_lazy = self.is_lazy
211 result.lower_bound = self.lower_bound
212 result.upper_bound = self.upper_bound
213 result.linear_expression.CopyFrom(
214 sparse_containers.to_sparse_double_vector_proto(self.terms)
215 )
216 return result
217
218
219@dataclasses.dataclass
221 """The value returned by a solve callback (produced by the user).
222
223 Attributes:
224 terminate: Stop the solve process and return early. Can be called from any
225 event.
226 generated_constraints: Constraints to add to the model. For details, see
227 GeneratedConstraint documentation.
228 suggested_solutions: A list of solutions (or partially defined solutions) to
229 suggest to the solver. Some solvers (e.g. gurobi) will try and convert a
230 partial solution into a full solution by solving a MIP. Use only for
231 Event.MIP_NODE.
232 """
233
234 terminate: bool = False
235 generated_constraints: List[GeneratedConstraint] = dataclasses.field(
236 default_factory=list
237 )
238 suggested_solutions: List[Mapping[model.Variable, float]] = dataclasses.field(
239 default_factory=list
240 )
241
243 self,
244 bounded_expr: Optional[Union[bool, model.BoundedLinearTypes]] = None,
245 *,
246 lb: Optional[float] = None,
247 ub: Optional[float] = None,
248 expr: Optional[model.LinearTypes] = None,
249 is_lazy: bool,
250 ) -> None:
251 """Adds a linear constraint to the list of generated constraints.
252
253 The constraint can be of two exclusive types: a "lazy constraint" or a
254 "user cut. A "user cut" is a constraint that excludes the current LP
255 solution, but does not cut off any integer-feasible points that satisfy the
256 already added constraints (either in callbacks or through
257 Model.add_linear_constraint()). A "lazy constraint" is a constraint that
258 excludes such integer-feasible points and hence is needed for corrctness of
259 the forlumation.
260
261 The simplest way to specify the constraint is by passing a one-sided or
262 two-sided linear inequality as in:
263 * add_generated_constraint(x + y + 1.0 <= 2.0, is_lazy=True),
264 * add_generated_constraint(x + y >= 2.0, is_lazy=True), or
265 * add_generated_constraint((1.0 <= x + y) <= 2.0, is_lazy=True).
266
267 Note the extra parenthesis for two-sided linear inequalities, which is
268 required due to some language limitations (see
269 https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/).
270 If the parenthesis are omitted, a TypeError will be raised explaining the
271 issue (if this error was not raised the first inequality would have been
272 silently ignored because of the noted language limitations).
273
274 The second way to specify the constraint is by setting lb, ub, and/o expr as
275 in:
276 * add_generated_constraint(expr=x + y + 1.0, ub=2.0, is_lazy=True),
277 * add_generated_constraint(expr=x + y, lb=2.0, is_lazy=True),
278 * add_generated_constraint(expr=x + y, lb=1.0, ub=2.0, is_lazy=True), or
279 * add_generated_constraint(lb=1.0, is_lazy=True).
280 Omitting lb is equivalent to setting it to -math.inf and omiting ub is
281 equivalent to setting it to math.inf.
282
283 These two alternatives are exclusive and a combined call like:
284 * add_generated_constraint(x + y <= 2.0, lb=1.0, is_lazy=True), or
285 * add_generated_constraint(x + y <= 2.0, ub=math.inf, is_lazy=True)
286 will raise a ValueError. A ValueError is also raised if expr's offset is
287 infinite.
288
289 Args:
290 bounded_expr: a linear inequality describing the constraint. Cannot be
291 specified together with lb, ub, or expr.
292 lb: The constraint's lower bound if bounded_expr is omitted (if both
293 bounder_expr and lb are omitted, the lower bound is -math.inf).
294 ub: The constraint's upper bound if bounded_expr is omitted (if both
295 bounder_expr and ub are omitted, the upper bound is math.inf).
296 expr: The constraint's linear expression if bounded_expr is omitted.
297 is_lazy: Whether the constraint is lazy or not.
298 """
299 normalized_inequality = model.as_normalized_linear_inequality(
300 bounded_expr, lb=lb, ub=ub, expr=expr
301 )
302 self.generated_constraints.append(
304 lower_bound=normalized_inequality.lb,
305 terms=normalized_inequality.coefficients,
306 upper_bound=normalized_inequality.ub,
307 is_lazy=is_lazy,
308 )
309 )
310
312 self,
313 bounded_expr: Optional[Union[bool, model.BoundedLinearTypes]] = None,
314 *,
315 lb: Optional[float] = None,
316 ub: Optional[float] = None,
317 expr: Optional[model.LinearTypes] = None,
318 ) -> None:
319 """Shortcut for add_generated_constraint(..., is_lazy=True).."""
321 bounded_expr, lb=lb, ub=ub, expr=expr, is_lazy=True
322 )
323
325 self,
326 bounded_expr: Optional[Union[bool, model.BoundedLinearTypes]] = None,
327 *,
328 lb: Optional[float] = None,
329 ub: Optional[float] = None,
330 expr: Optional[model.LinearTypes] = None,
331 ) -> None:
332 """Shortcut for add_generated_constraint(..., is_lazy=False)."""
334 bounded_expr, lb=lb, ub=ub, expr=expr, is_lazy=False
335 )
336
337 def to_proto(self) -> callback_pb2.CallbackResultProto:
338 """Returns a proto equivalent to this CallbackResult."""
339 result = callback_pb2.CallbackResultProto(terminate=self.terminate)
340 for generated_constraint in self.generated_constraints:
341 result.cuts.add().CopyFrom(generated_constraint.to_proto())
342 for suggested_solution in self.suggested_solutions:
343 result.suggested_solutions.add().CopyFrom(
344 sparse_containers.to_sparse_double_vector_proto(suggested_solution)
345 )
346 return result
callback_pb2.CallbackRegistrationProto to_proto(self)
Definition callback.py:164
None add_user_cut(self, Optional[Union[bool, model.BoundedLinearTypes]] bounded_expr=None, *, Optional[float] lb=None, Optional[float] ub=None, Optional[model.LinearTypes] expr=None)
Definition callback.py:331
callback_pb2.CallbackResultProto to_proto(self)
Definition callback.py:337
None add_lazy_constraint(self, Optional[Union[bool, model.BoundedLinearTypes]] bounded_expr=None, *, Optional[float] lb=None, Optional[float] ub=None, Optional[model.LinearTypes] expr=None)
Definition callback.py:318
None add_generated_constraint(self, Optional[Union[bool, model.BoundedLinearTypes]] bounded_expr=None, *, Optional[float] lb=None, Optional[float] ub=None, Optional[model.LinearTypes] expr=None, bool is_lazy)
Definition callback.py:250
callback_pb2.CallbackResultProto.GeneratedLinearConstraint to_proto(self)
Definition callback.py:207
CallbackData parse_callback_data(callback_pb2.CallbackDataProto cb_data, model.Model mod)
Definition callback.py:99