ortools.math_opt.python.callback

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

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

Returns an equivalent proto to this CallbackRegistration.

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

Returns an equivalent proto for the constraint.

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

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: 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 | None = None, *, lb: float | None = None, ub: float | None = None, expr: int | float | ForwardRef('LinearBase') | None = None) -> None:
327    def add_lazy_constraint(
328        self,
329        bounded_expr: Optional[Union[bool, variables.BoundedLinearTypes]] = None,
330        *,
331        lb: Optional[float] = None,
332        ub: Optional[float] = None,
333        expr: Optional[variables.LinearTypes] = None,
334    ) -> None:
335        """Shortcut for add_generated_constraint(..., is_lazy=True).."""
336        self.add_generated_constraint(
337            bounded_expr, lb=lb, ub=ub, expr=expr, is_lazy=True
338        )

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

def add_user_cut( self, bounded_expr: 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 | None = None, *, lb: float | None = None, ub: float | None = None, expr: int | float | ForwardRef('LinearBase') | None = None) -> None:
340    def add_user_cut(
341        self,
342        bounded_expr: Optional[Union[bool, variables.BoundedLinearTypes]] = None,
343        *,
344        lb: Optional[float] = None,
345        ub: Optional[float] = None,
346        expr: Optional[variables.LinearTypes] = None,
347    ) -> None:
348        """Shortcut for add_generated_constraint(..., is_lazy=False)."""
349        self.add_generated_constraint(
350            bounded_expr, lb=lb, ub=ub, expr=expr, is_lazy=False
351        )

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

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

Returns a proto equivalent to this CallbackResult.