Callback

Attention

The behavior of callback function highly depends on the optimizer and the specific problem. Please refer to the official documentation of the optimizer for more details.

In most optimization problems, we build the model, set the parameters, and then call the optimizer to solve the problem. However, in some cases, we may want to monitor the optimization process and intervene in the optimization process. For example, we may want to stop the optimization process when a certain condition is met, or we may want to record the intermediate results of the optimization process. In these cases, we can use the callback function. The callback function is a user-defined function that is called by the optimizer at specific points during the optimization process. Callback is especially useful for mixed-integer programming problems, where we can control the branch and bound process in callback functions.

Callback is not supported for all optimizers. Currently, we only support callback for Gurobi and COPT optimizer. Because callback is tightly coupled with the optimizer, we choose not to implement a strictly unified API for callback. Instead, we try to unify the common parts of the callback API of Gurobi and COPT and aims to provide all callback features included in vendored Python bindings of Gurobi and COPT.

In PyOptInterface, the callback function is simply a Python function that takes two arguments:

  • model: The instance of the optimization model

  • where: The flag indicates the stage of optimization process when our callback function is invoked. For Gurobi, the value of where is CallbackCodes. For COPT, the value of where is called as callback contexts such as COPT.CBCONTEXT_MIPNODE and COPT.CBCONTEXT_MIPRELAX.

In the function body of the callback function, we can do the following four kinds of things:

  • Query the current information of the optimization process. For scalar information, we can use model.cb_get_info function to get the information, and its argument is the value of what in Gurobi and the value of callback information in COPT. For array information such as the MIP solution or relaxation, PyOptInterface provides special functions such as model.cb_get_solution and model.cb_get_relaxation.

  • Add lazy constraint: Use model.cb_add_lazy_constraint just like model.add_linear_constraint except for the name argument.

  • Add user cut: Use model.cb_add_user_cut just like model.add_linear_constraint except for the name argument.

  • Set a heuristic solution: Use model.set_solution to set individual values of variables and use model.cb_submit_solution to submit the solution to the optimizer immediately (model.cb_submit_solution will be called automatically in the end of callback if model.set_solution is called).

  • Terminate the optimizer: Use model.cb_exit.

Here is an example of a callback function that stops the optimization process when the objective value reaches a certain threshold:

import pyoptinterface as poi
from pyoptinterface import gurobi, copt
GRB = gurobi.GRB
COPT = copt.COPT

def cb_gurobi(model, where):
    if where == GRB.Callback.MIPSOL:
        obj = model.cb_get_info(GRB.Callback.MIPSOL_OBJ)
        if obj < 10:
            model.cb_exit()
            
def cb_copt(model, where):
    if where == COPT.CBCONTEXT_MIPSOL:
        obj = model.cb_get_info("MipCandObj")
        if obj < 10:
            model.cb_exit()

To use the callback function, we need to call model.set_callback(cb) to pass the callback function to the optimizer. For COPT, model.set_callback needs an additional argument where to specify the context where the callback function is invoked. For Gurobi, the where argument is not needed.

model_gurobi = gurobi.Model()
model_gurobi.set_callback(cb_gurobi)

model_copt = copt.Model()
model_copt.set_callback(cb_copt, COPT.CBCONTEXT_MIPSOL)
# callback can also be registered for multiple contexts
model_copt.set_callback(cb_copt, COPT.CBCONTEXT_MIPSOL + COPT.CBCONTEXT_MIPNODE)

In order to help users to migrate code using gurobipy and/or coptpy to PyOptInterface, we list a translation table as follows.

Callback in gurobipy and PyOptInterface

gurobipy

PyOptInterface

model.optimize(cb)

model.set_callback(cb)

model.cbGet(GRB.Callback.SPX_OBJVAL)

model.cb_get_info(GRB.Callback.SPX_OBJVAL)

model.cbGetSolution(var)

model.cb_get_solution(var)

model.cbGetNodelRel(var)

model.cb_get_relaxation(var)

model.cbLazy(x[0] + x[1] <= 3)

model.cb_add_lazy_constraint(x[0] + x[1], poi.Leq, 3)

model.cbCut(x[0] + x[1] <= 3)

model.cb_add_user_cut(x[0] + x[1], poi.Leq, 3)

model.cbSetSolution(x, 1.0)

model.cb_set_solution(x, 1.0)

objval = model.cbUseSolution()

objval = model.cb_submit_solution()

model.termimate()

model.cb_exit()

Callback in coptpy and PyOptInterface

coptpy

PyOptInterface

model.setCallback(cb, COPT.CBCONTEXT_MIPSOL)

model.set_callback(cb, COPT.CBCONTEXT_MIPSOL)

CallbackBase.getInfo(COPT.CbInfo.BestBnd)

model.cb_get_info(COPT.CbInfo.BestBnd)

CallbackBase.getSolution(var)

model.cb_get_solution(var)

CallbackBase.getRelaxSol(var)

model.cb_get_relaxation(var)

CallbackBase.getIncumbent(var)

model.cb_get_incumbent(var)

CallbackBase.addLazyConstr(x[0] + x[1] <= 3)

model.cb_add_lazy_constraint(x[0] + x[1], poi.Leq, 3)

CallbackBase.addUserCut(x[0] + x[1] <= 3)

model.cb_add_user_cut(x[0] + x[1], poi.Leq, 3)

CallbackBase.setSolution(x, 1.0)

model.cb_set_solution(x, 1.0)

CallbackBase.loadSolution()

model.cb_submit_solution()

CallbackBase.interrupt()

model.cb_exit()

For a detailed example to use callbacks in PyOptInterface, we provide a concrete callback example to solve the Traveling Salesman Problem (TSP) with callbacks in PyOptInterface, gurobipy and coptpy. The example is adapted from the official Gurobi example tsp.py.