ortools.math_opt.python.callback

Defines how to request a callback and the input and output of a callback.

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

The supported events during a solve for callbacks.

  • UNSPECIFIED: The event is unknown (typically an internal error).
  • PRESOLVE: The solver is currently running presolve. Gurobi only.
  • SIMPLEX: The solver is currently running the simplex method. Gurobi only.
  • MIP: The solver is in the MIP loop (called periodically before starting a new node). Useful for early termination. Note that this event does not provide information on LP relaxations nor about new incumbent solutions. Gurobi only.
  • MIP_SOLUTION: Called every time a new MIP incumbent is found. Fully supported by Gurobi, partially supported by CP-SAT (you can observe new solutions, but not add lazy constraints).
  • MIP_NODE: Called inside a MIP node. Note that there is no guarantee that the callback function will be called on every node. That behavior is solver-dependent. Gurobi only.

    Disabling cuts using SolveParameters may interfere with this event being called and/or adding cuts at this event, the behavior is solver specific.

  • BARRIER: Called in each iterate of an interior point/barrier method. Gurobi only.
UNSPECIFIED = <Event.UNSPECIFIED: 0>
PRESOLVE = <Event.PRESOLVE: 1>
SIMPLEX = <Event.SIMPLEX: 2>
MIP = <Event.MIP: 3>
MIP_SOLUTION = <Event.MIP_SOLUTION: 4>
MIP_NODE = <Event.MIP_NODE: 5>
BARRIER = <Event.BARRIER: 6>
class PresolveStats(google._upb._message.Message, google.protobuf.message.Message):

A ProtocolMessage

DESCRIPTOR = <google._upb._message.Descriptor object>
class SimplexStats(google._upb._message.Message, google.protobuf.message.Message):

A ProtocolMessage

DESCRIPTOR = <google._upb._message.Descriptor object>
class BarrierStats(google._upb._message.Message, google.protobuf.message.Message):

A ProtocolMessage

DESCRIPTOR = <google._upb._message.Descriptor object>
class MipStats(google._upb._message.Message, google.protobuf.message.Message):

A ProtocolMessage

DESCRIPTOR = <google._upb._message.Descriptor object>
@dataclasses.dataclass
class CallbackData:
68@dataclasses.dataclass
69class CallbackData:
70    """Input to the solve callback (produced by the solver).
71
72    Attributes:
73      event: The current state of the solver when the callback is run. The event
74        (partially) determines what data is available and what the user is allowed
75        to return.
76      solution: A solution to the primal optimization problem, if available. For
77        Event.MIP_SOLUTION, solution is always present, integral, and feasible.
78        For Event.MIP_NODE, the primal_solution contains the current LP-node
79        relaxation. In some cases, no solution will be available (e.g. because LP
80        was infeasible or the solve was imprecise). Empty for other events.
81      messages: Logs generated by the underlying solver, as a list of strings
82        without new lines (each string is a line). Only filled on Event.MESSAGE.
83      runtime: The time since Solve() was invoked.
84      presolve_stats: Filled for Event.PRESOLVE only.
85      simplex_stats: Filled for Event.SIMPLEX only.
86      barrier_stats: Filled for Event.BARRIER only.
87      mip_stats: Filled for the events MIP, MIP_SOLUTION and MIP_NODE only.
88    """
89
90    event: Event = Event.UNSPECIFIED
91    solution: Optional[Dict[variables.Variable, float]] = None
92    messages: List[str] = dataclasses.field(default_factory=list)
93    runtime: datetime.timedelta = datetime.timedelta()
94    presolve_stats: PresolveStats = dataclasses.field(default_factory=PresolveStats)
95    simplex_stats: SimplexStats = dataclasses.field(default_factory=SimplexStats)
96    barrier_stats: BarrierStats = dataclasses.field(default_factory=BarrierStats)
97    mip_stats: MipStats = dataclasses.field(default_factory=MipStats)

Input to the solve callback (produced by the solver).

Attributes:
  • event: The current state of the solver when the callback is run. The event (partially) determines what data is available and what the user is allowed to return.
  • solution: A solution to the primal optimization problem, if available. For Event.MIP_SOLUTION, solution is always present, integral, and feasible. For Event.MIP_NODE, the primal_solution contains the current LP-node relaxation. In some cases, no solution will be available (e.g. because LP was infeasible or the solve was imprecise). Empty for other events.
  • messages: Logs generated by the underlying solver, as a list of strings without new lines (each string is a line). Only filled on Event.MESSAGE.
  • runtime: The time since Solve() was invoked.
  • presolve_stats: Filled for Event.PRESOLVE only.
  • simplex_stats: Filled for Event.SIMPLEX only.
  • barrier_stats: Filled for Event.BARRIER only.
  • mip_stats: Filled for the events MIP, MIP_SOLUTION and MIP_NODE only.
CallbackData( event: Event = <Event.UNSPECIFIED: 0>, solution: Optional[Dict[ortools.math_opt.python.variables.Variable, float]] = None, messages: List[str] = <factory>, runtime: datetime.timedelta = datetime.timedelta(0), presolve_stats: PresolveStats = <factory>, simplex_stats: SimplexStats = <factory>, barrier_stats: BarrierStats = <factory>, mip_stats: MipStats = <factory>)
event: Event = <Event.UNSPECIFIED: 0>
solution: Optional[Dict[ortools.math_opt.python.variables.Variable, float]] = None
messages: List[str]
runtime: datetime.timedelta = datetime.timedelta(0)
presolve_stats: PresolveStats
simplex_stats: SimplexStats
barrier_stats: BarrierStats
mip_stats: MipStats
100def parse_callback_data(
101    cb_data: callback_pb2.CallbackDataProto, mod: model.Model
102) -> CallbackData:
103    """Creates a CallbackData from an equivalent proto.
104
105    Args:
106      cb_data: A protocol buffer with the information the user needs for a
107        callback.
108      mod: The model being solved.
109
110    Returns:
111      An equivalent CallbackData.
112
113    Raises:
114      ValueError: if cb_data is invalid or inconsistent with mod, e.g. cb_data
115      refers to a variable id not in mod.
116    """
117    result = CallbackData()
118    result.event = Event(cb_data.event)
119    if cb_data.HasField("primal_solution_vector"):
120        primal_solution = cb_data.primal_solution_vector
121        result.solution = {
122            mod.get_variable(id): val
123            for (id, val) in zip(primal_solution.ids, primal_solution.values)
124        }
125    result.runtime = cb_data.runtime.ToTimedelta()
126    result.presolve_stats = cb_data.presolve_stats
127    result.simplex_stats = cb_data.simplex_stats
128    result.barrier_stats = cb_data.barrier_stats
129    result.mip_stats = cb_data.mip_stats
130    return result

Creates a CallbackData from an equivalent proto.

Arguments:
  • cb_data: A protocol buffer with the information the user needs for a callback.
  • mod: The model being solved.
Returns:

An equivalent CallbackData.

Raises:
  • ValueError: if cb_data is invalid or inconsistent with mod, e.g. cb_data
  • refers to a variable id not in mod.
@dataclasses.dataclass
class CallbackRegistration:
133@dataclasses.dataclass
134class CallbackRegistration:
135    """Request the events and input data and reports output types for a callback.
136
137    Note that it is an error to add a constraint in a callback without setting
138    add_cuts and/or add_lazy_constraints to true.
139
140    Attributes:
141      events: When the callback should be invoked, by default, never. If an
142        unsupported event for a solver/model combination is selected, an
143        excecption is raised, see Event above for details.
144      mip_solution_filter: restricts the variable values returned in
145        CallbackData.solution (the callback argument) at each MIP_SOLUTION event.
146        By default, values are returned for all variables.
147      mip_node_filter: restricts the variable values returned in
148        CallbackData.solution (the callback argument) at each MIP_NODE event. By
149        default, values are returned for all variables.
150      add_cuts: The callback may add "user cuts" (linear constraints that
151        strengthen the LP without cutting of integer points) at MIP_NODE events.
152      add_lazy_constraints: The callback may add "lazy constraints" (linear
153        constraints that cut off integer solutions) at MIP_NODE or MIP_SOLUTION
154        events.
155    """
156
157    events: Set[Event] = dataclasses.field(default_factory=set)
158    mip_solution_filter: sparse_containers.VariableFilter = (
159        sparse_containers.VariableFilter()
160    )
161    mip_node_filter: sparse_containers.VariableFilter = (
162        sparse_containers.VariableFilter()
163    )
164    add_cuts: bool = False
165    add_lazy_constraints: bool = False
166
167    def to_proto(self) -> callback_pb2.CallbackRegistrationProto:
168        """Returns an equivalent proto to this CallbackRegistration."""
169        result = callback_pb2.CallbackRegistrationProto()
170        result.request_registration[:] = sorted([event.value for event in self.events])
171        result.mip_solution_filter.CopyFrom(self.mip_solution_filter.to_proto())
172        result.mip_node_filter.CopyFrom(self.mip_node_filter.to_proto())
173        result.add_cuts = self.add_cuts
174        result.add_lazy_constraints = self.add_lazy_constraints
175        return result

Request the events and input data and reports output types for a callback.

Note that it is an error to add a constraint in a callback without setting add_cuts and/or add_lazy_constraints to true.

Attributes:
  • events: When the callback should be invoked, by default, never. If an unsupported event for a solver/model combination is selected, an excecption is raised, see Event above for details.
  • mip_solution_filter: restricts the variable values returned in CallbackData.solution (the callback argument) at each MIP_SOLUTION event. By default, values are returned for all variables.
  • mip_node_filter: restricts the variable values returned in CallbackData.solution (the callback argument) at each MIP_NODE event. By default, values are returned for all variables.
  • add_cuts: The callback may add "user cuts" (linear constraints that strengthen the LP without cutting of integer points) at MIP_NODE events.
  • add_lazy_constraints: The callback may add "lazy constraints" (linear constraints that cut off integer solutions) at MIP_NODE or MIP_SOLUTION events.
events: Set[Event]
add_cuts: bool = False
add_lazy_constraints: bool = False
def to_proto(self) -> ortools.math_opt.callback_pb2.CallbackRegistrationProto:
167    def to_proto(self) -> callback_pb2.CallbackRegistrationProto:
168        """Returns an equivalent proto to this CallbackRegistration."""
169        result = callback_pb2.CallbackRegistrationProto()
170        result.request_registration[:] = sorted([event.value for event in self.events])
171        result.mip_solution_filter.CopyFrom(self.mip_solution_filter.to_proto())
172        result.mip_node_filter.CopyFrom(self.mip_node_filter.to_proto())
173        result.add_cuts = self.add_cuts
174        result.add_lazy_constraints = self.add_lazy_constraints
175        return result

Returns an equivalent proto to this CallbackRegistration.

@dataclasses.dataclass
class GeneratedConstraint:
178@dataclasses.dataclass
179class GeneratedConstraint:
180    """A linear constraint to add inside a callback.
181
182    Models a constraint of the form:
183      lb <= sum_{i in I} a_i * x_i <= ub
184
185    Two types of generated linear constraints are supported based on is_lazy:
186      * The "lazy constraint" can remove integer points from the feasible
187        region and can be added at event Event.MIP_NODE or
188        Event.MIP_SOLUTION
189      * The "user cut" (on is_lazy=false) strengthens the LP without removing
190        integer points. It can only be added at Event.MIP_NODE.
191
192
193    Attributes:
194      terms: The variables and linear coefficients in the constraint, a_i and x_i
195        in the model above.
196      lower_bound: lb in the model above.
197      upper_bound: ub in the model above.
198      is_lazy: Indicates if the constraint should be interpreted as a "lazy
199        constraint" (cuts off integer solutions) or a "user cut" (strengthens the
200        LP relaxation without cutting of integer solutions).
201    """
202
203    terms: Mapping[variables.Variable, float] = dataclasses.field(default_factory=dict)
204    lower_bound: float = -math.inf
205    upper_bound: float = math.inf
206    is_lazy: bool = False
207
208    def to_proto(
209        self,
210    ) -> callback_pb2.CallbackResultProto.GeneratedLinearConstraint:
211        """Returns an equivalent proto for the constraint."""
212        result = callback_pb2.CallbackResultProto.GeneratedLinearConstraint()
213        result.is_lazy = self.is_lazy
214        result.lower_bound = self.lower_bound
215        result.upper_bound = self.upper_bound
216        result.linear_expression.CopyFrom(
217            sparse_containers.to_sparse_double_vector_proto(self.terms)
218        )
219        return result

A linear constraint to add inside a callback.

Models a constraint of the form:

lb <= sum_{i in I} a_i * x_i <= ub

Two types of generated linear constraints are supported based on is_lazy:

  • The "lazy constraint" can remove integer points from the feasible region and can be added at event Event.MIP_NODE or Event.MIP_SOLUTION
  • The "user cut" (on is_lazy=false) strengthens the LP without removing integer points. It can only be added at Event.MIP_NODE.
Attributes:
  • terms: The variables and linear coefficients in the constraint, a_i and x_i in the model above.
  • lower_bound: lb in the model above.
  • upper_bound: ub in the model above.
  • is_lazy: Indicates if the constraint should be interpreted as a "lazy constraint" (cuts off integer solutions) or a "user cut" (strengthens the LP relaxation without cutting of integer solutions).
GeneratedConstraint( terms: Mapping[ortools.math_opt.python.variables.Variable, float] = <factory>, lower_bound: float = -inf, upper_bound: float = inf, is_lazy: bool = False)
lower_bound: float = -inf
upper_bound: float = inf
is_lazy: bool = False
208    def to_proto(
209        self,
210    ) -> callback_pb2.CallbackResultProto.GeneratedLinearConstraint:
211        """Returns an equivalent proto for the constraint."""
212        result = callback_pb2.CallbackResultProto.GeneratedLinearConstraint()
213        result.is_lazy = self.is_lazy
214        result.lower_bound = self.lower_bound
215        result.upper_bound = self.upper_bound
216        result.linear_expression.CopyFrom(
217            sparse_containers.to_sparse_double_vector_proto(self.terms)
218        )
219        return result

Returns an equivalent proto for the constraint.

@dataclasses.dataclass
class CallbackResult:
222@dataclasses.dataclass
223class CallbackResult:
224    """The value returned by a solve callback (produced by the user).
225
226    Attributes:
227      terminate: When true it tells the solver to interrupt the solve as soon as
228        possible.
229
230        It can be set from any event. This is equivalent to using a
231        SolveInterrupter and triggering it from the callback.
232
233        Some solvers don't support interruption, in that case this is simply
234        ignored and the solve terminates as usual. On top of that solvers may not
235        immediately stop the solve. Thus the user should expect the callback to
236        still be called after they set `terminate` to true in a previous
237        call. Returning with `terminate` false after having previously returned
238        true won't cancel the interruption.
239      generated_constraints: Constraints to add to the model. For details, see
240        GeneratedConstraint documentation.
241      suggested_solutions: A list of solutions (or partially defined solutions) to
242        suggest to the solver. Some solvers (e.g. gurobi) will try and convert a
243        partial solution into a full solution by solving a MIP. Use only for
244        Event.MIP_NODE.
245    """
246
247    terminate: bool = False
248    generated_constraints: List[GeneratedConstraint] = dataclasses.field(
249        default_factory=list
250    )
251    suggested_solutions: List[Mapping[variables.Variable, float]] = dataclasses.field(
252        default_factory=list
253    )
254
255    def add_generated_constraint(
256        self,
257        bounded_expr: Optional[Union[bool, variables.BoundedLinearTypes]] = None,
258        *,
259        lb: Optional[float] = None,
260        ub: Optional[float] = None,
261        expr: Optional[variables.LinearTypes] = None,
262        is_lazy: bool,
263    ) -> None:
264        """Adds a linear constraint to the list of generated constraints.
265
266        The constraint can be of two exclusive types: a "lazy constraint" or a
267        "user cut. A "user cut" is a constraint that excludes the current LP
268        solution, but does not cut off any integer-feasible points that satisfy the
269        already added constraints (either in callbacks or through
270        Model.add_linear_constraint()). A "lazy constraint" is a constraint that
271        excludes such integer-feasible points and hence is needed for corrctness of
272        the forlumation.
273
274        The simplest way to specify the constraint is by passing a one-sided or
275        two-sided linear inequality as in:
276          * add_generated_constraint(x + y + 1.0 <= 2.0, is_lazy=True),
277          * add_generated_constraint(x + y >= 2.0, is_lazy=True), or
278          * add_generated_constraint((1.0 <= x + y) <= 2.0, is_lazy=True).
279
280        Note the extra parenthesis for two-sided linear inequalities, which is
281        required due to some language limitations (see
282        https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/).
283        If the parenthesis are omitted, a TypeError will be raised explaining the
284        issue (if this error was not raised the first inequality would have been
285        silently ignored because of the noted language limitations).
286
287        The second way to specify the constraint is by setting lb, ub, and/o expr as
288        in:
289          * add_generated_constraint(expr=x + y + 1.0, ub=2.0, is_lazy=True),
290          * add_generated_constraint(expr=x + y, lb=2.0, is_lazy=True),
291          * add_generated_constraint(expr=x + y, lb=1.0, ub=2.0, is_lazy=True), or
292          * add_generated_constraint(lb=1.0, is_lazy=True).
293        Omitting lb is equivalent to setting it to -math.inf and omiting ub is
294        equivalent to setting it to math.inf.
295
296        These two alternatives are exclusive and a combined call like:
297          * add_generated_constraint(x + y <= 2.0, lb=1.0, is_lazy=True), or
298          * add_generated_constraint(x + y <= 2.0, ub=math.inf, is_lazy=True)
299        will raise a ValueError. A ValueError is also raised if expr's offset is
300        infinite.
301
302        Args:
303          bounded_expr: a linear inequality describing the constraint. Cannot be
304            specified together with lb, ub, or expr.
305          lb: The constraint's lower bound if bounded_expr is omitted (if both
306            bounder_expr and lb are omitted, the lower bound is -math.inf).
307          ub: The constraint's upper bound if bounded_expr is omitted (if both
308            bounder_expr and ub are omitted, the upper bound is math.inf).
309          expr: The constraint's linear expression if bounded_expr is omitted.
310          is_lazy: Whether the constraint is lazy or not.
311        """
312        norm_ineq = normalized_inequality.as_normalized_linear_inequality(
313            bounded_expr, lb=lb, ub=ub, expr=expr
314        )
315        self.generated_constraints.append(
316            GeneratedConstraint(
317                lower_bound=norm_ineq.lb,
318                terms=norm_ineq.coefficients,
319                upper_bound=norm_ineq.ub,
320                is_lazy=is_lazy,
321            )
322        )
323
324    def add_lazy_constraint(
325        self,
326        bounded_expr: Optional[Union[bool, variables.BoundedLinearTypes]] = None,
327        *,
328        lb: Optional[float] = None,
329        ub: Optional[float] = None,
330        expr: Optional[variables.LinearTypes] = None,
331    ) -> None:
332        """Shortcut for add_generated_constraint(..., is_lazy=True).."""
333        self.add_generated_constraint(
334            bounded_expr, lb=lb, ub=ub, expr=expr, is_lazy=True
335        )
336
337    def add_user_cut(
338        self,
339        bounded_expr: Optional[Union[bool, variables.BoundedLinearTypes]] = None,
340        *,
341        lb: Optional[float] = None,
342        ub: Optional[float] = None,
343        expr: Optional[variables.LinearTypes] = None,
344    ) -> None:
345        """Shortcut for add_generated_constraint(..., is_lazy=False)."""
346        self.add_generated_constraint(
347            bounded_expr, lb=lb, ub=ub, expr=expr, is_lazy=False
348        )
349
350    def to_proto(self) -> callback_pb2.CallbackResultProto:
351        """Returns a proto equivalent to this CallbackResult."""
352        result = callback_pb2.CallbackResultProto(terminate=self.terminate)
353        for generated_constraint in self.generated_constraints:
354            result.cuts.add().CopyFrom(generated_constraint.to_proto())
355        for suggested_solution in self.suggested_solutions:
356            result.suggested_solutions.add().CopyFrom(
357                sparse_containers.to_sparse_double_vector_proto(suggested_solution)
358            )
359        return result

The value returned by a solve callback (produced by the user).

Attributes:
  • terminate: When true it tells the solver to interrupt the solve as soon as possible.

    It can be set from any event. This is equivalent to using a SolveInterrupter and triggering it from the callback.

    Some solvers don't support interruption, in that case this is simply ignored and the solve terminates as usual. On top of that solvers may not immediately stop the solve. Thus the user should expect the callback to still be called after they set terminate to true in a previous call. Returning with terminate false after having previously returned true won't cancel the interruption.

  • generated_constraints: Constraints to add to the model. For details, see GeneratedConstraint documentation.
  • suggested_solutions: A list of solutions (or partially defined solutions) to suggest to the solver. Some solvers (e.g. gurobi) will try and convert a partial solution into a full solution by solving a MIP. Use only for Event.MIP_NODE.
CallbackResult( terminate: bool = False, generated_constraints: List[GeneratedConstraint] = <factory>, suggested_solutions: List[Mapping[ortools.math_opt.python.variables.Variable, float]] = <factory>)
terminate: bool = False
generated_constraints: List[GeneratedConstraint]
suggested_solutions: List[Mapping[ortools.math_opt.python.variables.Variable, float]]
def add_generated_constraint( self, bounded_expr: Union[bool, ortools.math_opt.python.bounded_expressions.LowerBoundedExpression[ForwardRef('LinearBase')], ortools.math_opt.python.bounded_expressions.UpperBoundedExpression[ForwardRef('LinearBase')], ortools.math_opt.python.bounded_expressions.BoundedExpression[ForwardRef('LinearBase')], ortools.math_opt.python.variables.VarEqVar, NoneType] = None, *, lb: Optional[float] = None, ub: Optional[float] = None, expr: Union[int, float, ForwardRef('LinearBase'), NoneType] = None, is_lazy: bool) -> None:
255    def add_generated_constraint(
256        self,
257        bounded_expr: Optional[Union[bool, variables.BoundedLinearTypes]] = None,
258        *,
259        lb: Optional[float] = None,
260        ub: Optional[float] = None,
261        expr: Optional[variables.LinearTypes] = None,
262        is_lazy: bool,
263    ) -> None:
264        """Adds a linear constraint to the list of generated constraints.
265
266        The constraint can be of two exclusive types: a "lazy constraint" or a
267        "user cut. A "user cut" is a constraint that excludes the current LP
268        solution, but does not cut off any integer-feasible points that satisfy the
269        already added constraints (either in callbacks or through
270        Model.add_linear_constraint()). A "lazy constraint" is a constraint that
271        excludes such integer-feasible points and hence is needed for corrctness of
272        the forlumation.
273
274        The simplest way to specify the constraint is by passing a one-sided or
275        two-sided linear inequality as in:
276          * add_generated_constraint(x + y + 1.0 <= 2.0, is_lazy=True),
277          * add_generated_constraint(x + y >= 2.0, is_lazy=True), or
278          * add_generated_constraint((1.0 <= x + y) <= 2.0, is_lazy=True).
279
280        Note the extra parenthesis for two-sided linear inequalities, which is
281        required due to some language limitations (see
282        https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/).
283        If the parenthesis are omitted, a TypeError will be raised explaining the
284        issue (if this error was not raised the first inequality would have been
285        silently ignored because of the noted language limitations).
286
287        The second way to specify the constraint is by setting lb, ub, and/o expr as
288        in:
289          * add_generated_constraint(expr=x + y + 1.0, ub=2.0, is_lazy=True),
290          * add_generated_constraint(expr=x + y, lb=2.0, is_lazy=True),
291          * add_generated_constraint(expr=x + y, lb=1.0, ub=2.0, is_lazy=True), or
292          * add_generated_constraint(lb=1.0, is_lazy=True).
293        Omitting lb is equivalent to setting it to -math.inf and omiting ub is
294        equivalent to setting it to math.inf.
295
296        These two alternatives are exclusive and a combined call like:
297          * add_generated_constraint(x + y <= 2.0, lb=1.0, is_lazy=True), or
298          * add_generated_constraint(x + y <= 2.0, ub=math.inf, is_lazy=True)
299        will raise a ValueError. A ValueError is also raised if expr's offset is
300        infinite.
301
302        Args:
303          bounded_expr: a linear inequality describing the constraint. Cannot be
304            specified together with lb, ub, or expr.
305          lb: The constraint's lower bound if bounded_expr is omitted (if both
306            bounder_expr and lb are omitted, the lower bound is -math.inf).
307          ub: The constraint's upper bound if bounded_expr is omitted (if both
308            bounder_expr and ub are omitted, the upper bound is math.inf).
309          expr: The constraint's linear expression if bounded_expr is omitted.
310          is_lazy: Whether the constraint is lazy or not.
311        """
312        norm_ineq = normalized_inequality.as_normalized_linear_inequality(
313            bounded_expr, lb=lb, ub=ub, expr=expr
314        )
315        self.generated_constraints.append(
316            GeneratedConstraint(
317                lower_bound=norm_ineq.lb,
318                terms=norm_ineq.coefficients,
319                upper_bound=norm_ineq.ub,
320                is_lazy=is_lazy,
321            )
322        )

Adds a linear constraint to the list of generated constraints.

The constraint can be of two exclusive types: a "lazy constraint" or a "user cut. A "user cut" is a constraint that excludes the current LP solution, but does not cut off any integer-feasible points that satisfy the already added constraints (either in callbacks or through Model.add_linear_constraint()). A "lazy constraint" is a constraint that excludes such integer-feasible points and hence is needed for corrctness of the forlumation.

The simplest way to specify the constraint is by passing a one-sided or two-sided linear inequality as in:

  • add_generated_constraint(x + y + 1.0 <= 2.0, is_lazy=True),
  • add_generated_constraint(x + y >= 2.0, is_lazy=True), or
  • add_generated_constraint((1.0 <= x + y) <= 2.0, is_lazy=True).

Note the extra parenthesis for two-sided linear inequalities, which is required due to some language limitations (see https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/). If the parenthesis are omitted, a TypeError will be raised explaining the issue (if this error was not raised the first inequality would have been silently ignored because of the noted language limitations).

The second way to specify the constraint is by setting lb, ub, and/o expr as in:

  • add_generated_constraint(expr=x + y + 1.0, ub=2.0, is_lazy=True),
  • add_generated_constraint(expr=x + y, lb=2.0, is_lazy=True),
  • add_generated_constraint(expr=x + y, lb=1.0, ub=2.0, is_lazy=True), or
  • add_generated_constraint(lb=1.0, is_lazy=True). Omitting lb is equivalent to setting it to -math.inf and omiting ub is equivalent to setting it to math.inf.
These two alternatives are exclusive and a combined call like:
  • add_generated_constraint(x + y <= 2.0, lb=1.0, is_lazy=True), or
  • add_generated_constraint(x + y <= 2.0, ub=math.inf, is_lazy=True)

will raise a ValueError. A ValueError is also raised if expr's offset is infinite.

Arguments:
  • bounded_expr: a linear inequality describing the constraint. Cannot be specified together with lb, ub, or expr.
  • lb: The constraint's lower bound if bounded_expr is omitted (if both bounder_expr and lb are omitted, the lower bound is -math.inf).
  • ub: The constraint's upper bound if bounded_expr is omitted (if both bounder_expr and ub are omitted, the upper bound is math.inf).
  • expr: The constraint's linear expression if bounded_expr is omitted.
  • is_lazy: Whether the constraint is lazy or not.
def add_lazy_constraint( self, bounded_expr: Union[bool, ortools.math_opt.python.bounded_expressions.LowerBoundedExpression[ForwardRef('LinearBase')], ortools.math_opt.python.bounded_expressions.UpperBoundedExpression[ForwardRef('LinearBase')], ortools.math_opt.python.bounded_expressions.BoundedExpression[ForwardRef('LinearBase')], ortools.math_opt.python.variables.VarEqVar, NoneType] = None, *, lb: Optional[float] = None, ub: Optional[float] = None, expr: Union[int, float, ForwardRef('LinearBase'), NoneType] = None) -> None:
324    def add_lazy_constraint(
325        self,
326        bounded_expr: Optional[Union[bool, variables.BoundedLinearTypes]] = None,
327        *,
328        lb: Optional[float] = None,
329        ub: Optional[float] = None,
330        expr: Optional[variables.LinearTypes] = None,
331    ) -> None:
332        """Shortcut for add_generated_constraint(..., is_lazy=True).."""
333        self.add_generated_constraint(
334            bounded_expr, lb=lb, ub=ub, expr=expr, is_lazy=True
335        )

Shortcut for add_generated_constraint(..., is_lazy=True)..

def add_user_cut( self, bounded_expr: Union[bool, ortools.math_opt.python.bounded_expressions.LowerBoundedExpression[ForwardRef('LinearBase')], ortools.math_opt.python.bounded_expressions.UpperBoundedExpression[ForwardRef('LinearBase')], ortools.math_opt.python.bounded_expressions.BoundedExpression[ForwardRef('LinearBase')], ortools.math_opt.python.variables.VarEqVar, NoneType] = None, *, lb: Optional[float] = None, ub: Optional[float] = None, expr: Union[int, float, ForwardRef('LinearBase'), NoneType] = None) -> None:
337    def add_user_cut(
338        self,
339        bounded_expr: Optional[Union[bool, variables.BoundedLinearTypes]] = None,
340        *,
341        lb: Optional[float] = None,
342        ub: Optional[float] = None,
343        expr: Optional[variables.LinearTypes] = None,
344    ) -> None:
345        """Shortcut for add_generated_constraint(..., is_lazy=False)."""
346        self.add_generated_constraint(
347            bounded_expr, lb=lb, ub=ub, expr=expr, is_lazy=False
348        )

Shortcut for add_generated_constraint(..., is_lazy=False).

def to_proto(self) -> ortools.math_opt.callback_pb2.CallbackResultProto:
350    def to_proto(self) -> callback_pb2.CallbackResultProto:
351        """Returns a proto equivalent to this CallbackResult."""
352        result = callback_pb2.CallbackResultProto(terminate=self.terminate)
353        for generated_constraint in self.generated_constraints:
354            result.cuts.add().CopyFrom(generated_constraint.to_proto())
355        for suggested_solution in self.suggested_solutions:
356            result.suggested_solutions.add().CopyFrom(
357                sparse_containers.to_sparse_double_vector_proto(suggested_solution)
358            )
359        return result

Returns a proto equivalent to this CallbackResult.