Callbacks
This tutorial was generated using Literate.jl. Download the source as a .jl
file.
The purpose of the tutorial is to demonstrate the various solver-independent and solver-dependent callbacks that are supported by JuMP.
The tutorial uses the following packages:
using JuMP
import Gurobi
import Random
import Test
This tutorial uses the MathOptInterface API. By default, JuMP exports the MOI
symbol as an alias for the MathOptInterface.jl package. We recommend making this more explicit in your code by adding the following lines:
import MathOptInterface as MOI
Lazy constraints
An example using a lazy constraint callback.
function example_lazy_constraint()
model = Model(Gurobi.Optimizer)
set_silent(model)
@variable(model, 0 <= x <= 2.5, Int)
@variable(model, 0 <= y <= 2.5, Int)
@objective(model, Max, y)
lazy_called = false
function my_callback_function(cb_data)
lazy_called = true
x_val = callback_value(cb_data, x)
y_val = callback_value(cb_data, y)
println("Called from (x, y) = ($x_val, $y_val)")
status = callback_node_status(cb_data, model)
if status == MOI.CALLBACK_NODE_STATUS_FRACTIONAL
println(" - Solution is integer infeasible!")
elseif status == MOI.CALLBACK_NODE_STATUS_INTEGER
println(" - Solution is integer feasible!")
else
@assert status == MOI.CALLBACK_NODE_STATUS_UNKNOWN
println(" - I don't know if the solution is integer feasible :(")
end
if y_val - x_val > 1 + 1e-6
con = @build_constraint(y - x <= 1)
println("Adding $(con)")
MOI.submit(model, MOI.LazyConstraint(cb_data), con)
elseif y_val + x_val > 3 + 1e-6
con = @build_constraint(y + x <= 3)
println("Adding $(con)")
MOI.submit(model, MOI.LazyConstraint(cb_data), con)
end
return
end
set_attribute(model, MOI.LazyConstraintCallback(), my_callback_function)
optimize!(model)
Test.@test is_solved_and_feasible(model)
Test.@test lazy_called
Test.@test value(x) == 1
Test.@test value(y) == 2
println("Optimal solution (x, y) = ($(value(x)), $(value(y)))")
return
end
example_lazy_constraint()
Set parameter WLSAccessID
Set parameter WLSSecret
Set parameter LicenseID to value 722777
Set parameter GURO_PAR_SPECIAL
WLS license 722777 - registered to JuMP Development
Called from (x, y) = (-0.0, 2.0)
- Solution is integer feasible!
Adding ScalarConstraint{AffExpr, MathOptInterface.LessThan{Float64}}(y - x, MathOptInterface.LessThan{Float64}(1.0))
Called from (x, y) = (2.0, 2.0)
- Solution is integer feasible!
Adding ScalarConstraint{AffExpr, MathOptInterface.LessThan{Float64}}(y + x, MathOptInterface.LessThan{Float64}(3.0))
Called from (x, y) = (2.0, 2.0)
- Solution is integer feasible!
Adding ScalarConstraint{AffExpr, MathOptInterface.LessThan{Float64}}(y + x, MathOptInterface.LessThan{Float64}(3.0))
Called from (x, y) = (2.0, 2.0)
- Solution is integer feasible!
Adding ScalarConstraint{AffExpr, MathOptInterface.LessThan{Float64}}(y + x, MathOptInterface.LessThan{Float64}(3.0))
Called from (x, y) = (-0.0, 2.0)
- Solution is integer feasible!
Adding ScalarConstraint{AffExpr, MathOptInterface.LessThan{Float64}}(y - x, MathOptInterface.LessThan{Float64}(1.0))
Called from (x, y) = (1.0, 2.0)
- Solution is integer feasible!
Optimal solution (x, y) = (1.0, 2.0)
User-cuts
An example using a user-cut callback.
function example_user_cut_constraint()
Random.seed!(1)
N = 30
item_weights, item_values = rand(N), rand(N)
model = Model(Gurobi.Optimizer)
set_silent(model)
# Turn off "Cuts" parameter so that our new one must be called. In real
# models, you should leave "Cuts" turned on.
set_attribute(model, "Cuts", 0)
@variable(model, x[1:N], Bin)
@constraint(model, sum(item_weights[i] * x[i] for i in 1:N) <= 10)
@objective(model, Max, sum(item_values[i] * x[i] for i in 1:N))
callback_called = false
function my_callback_function(cb_data)
callback_called = true
x_vals = callback_value.(Ref(cb_data), x)
accumulated = sum(item_weights[i] for i in 1:N if x_vals[i] > 1e-4)
println("Called with accumulated = $(accumulated)")
n_terms = sum(1 for i in 1:N if x_vals[i] > 1e-4)
if accumulated > 10
con = @build_constraint(
sum(x[i] for i in 1:N if x_vals[i] > 0.5) <= n_terms - 1
)
println("Adding $(con)")
MOI.submit(model, MOI.UserCut(cb_data), con)
end
end
set_attribute(model, MOI.UserCutCallback(), my_callback_function)
optimize!(model)
Test.@test is_solved_and_feasible(model)
Test.@test callback_called
@show callback_called
return
end
example_user_cut_constraint()
Set parameter WLSAccessID
Set parameter WLSSecret
Set parameter LicenseID to value 722777
Set parameter GURO_PAR_SPECIAL
WLS license 722777 - registered to JuMP Development
Called with accumulated = 10.37975831721494
Adding ScalarConstraint{AffExpr, MathOptInterface.LessThan{Float64}}(x[1] + x[2] + x[3] + x[4] + x[5] + x[7] + x[8] + x[9] + x[10] + x[11] + x[12] + x[13] + x[14] + x[16] + x[17] + x[18] + x[20] + x[22] + x[23] + x[25] + x[26] + x[28] + x[29] + x[30], MathOptInterface.LessThan{Float64}(23.0))
Called with accumulated = 10.37975831721494
Adding ScalarConstraint{AffExpr, MathOptInterface.LessThan{Float64}}(x[1] + x[2] + x[3] + x[4] + x[5] + x[7] + x[8] + x[9] + x[10] + x[12] + x[13] + x[14] + x[16] + x[17] + x[18] + x[20] + x[22] + x[23] + x[25] + x[26] + x[28] + x[29] + x[30], MathOptInterface.LessThan{Float64}(23.0))
Called with accumulated = 10.37975831721494
Adding ScalarConstraint{AffExpr, MathOptInterface.LessThan{Float64}}(x[1] + x[2] + x[3] + x[4] + x[5] + x[7] + x[8] + x[9] + x[10] + x[11] + x[12] + x[13] + x[14] + x[16] + x[17] + x[18] + x[20] + x[22] + x[23] + x[25] + x[26] + x[29] + x[30], MathOptInterface.LessThan{Float64}(23.0))
Called with accumulated = 10.37975831721494
Adding ScalarConstraint{AffExpr, MathOptInterface.LessThan{Float64}}(x[1] + x[2] + x[3] + x[4] + x[5] + x[7] + x[8] + x[9] + x[10] + x[12] + x[13] + x[14] + x[16] + x[17] + x[18] + x[20] + x[22] + x[23] + x[25] + x[26] + x[28] + x[29] + x[30], MathOptInterface.LessThan{Float64}(23.0))
Called with accumulated = 10.37975831721494
Adding ScalarConstraint{AffExpr, MathOptInterface.LessThan{Float64}}(x[1] + x[2] + x[3] + x[4] + x[5] + x[7] + x[8] + x[9] + x[10] + x[12] + x[13] + x[14] + x[16] + x[17] + x[18] + x[20] + x[22] + x[23] + x[25] + x[26] + x[28] + x[29] + x[30], MathOptInterface.LessThan{Float64}(23.0))
Called with accumulated = 10.37975831721494
Adding ScalarConstraint{AffExpr, MathOptInterface.LessThan{Float64}}(x[1] + x[2] + x[3] + x[4] + x[5] + x[7] + x[8] + x[9] + x[10] + x[12] + x[13] + x[14] + x[16] + x[17] + x[18] + x[20] + x[22] + x[23] + x[25] + x[26] + x[28] + x[29] + x[30], MathOptInterface.LessThan{Float64}(23.0))
Called with accumulated = 10.585271197221452
Adding ScalarConstraint{AffExpr, MathOptInterface.LessThan{Float64}}(x[1] + x[2] + x[3] + x[4] + x[5] + x[7] + x[8] + x[9] + x[10] + x[11] + x[12] + x[13] + x[14] + x[16] + x[17] + x[18] + x[20] + x[22] + x[23] + x[25] + x[26] + x[29] + x[30], MathOptInterface.LessThan{Float64}(23.0))
Called with accumulated = 10.37975831721494
Adding ScalarConstraint{AffExpr, MathOptInterface.LessThan{Float64}}(x[1] + x[2] + x[3] + x[4] + x[5] + x[7] + x[8] + x[9] + x[10] + x[12] + x[13] + x[14] + x[16] + x[17] + x[18] + x[20] + x[22] + x[23] + x[25] + x[26] + x[28] + x[29] + x[30], MathOptInterface.LessThan{Float64}(23.0))
callback_called = true
Heuristic solutions
An example using a heuristic solution callback.
function example_heuristic_solution()
Random.seed!(1)
N = 30
item_weights, item_values = rand(N), rand(N)
model = Model(Gurobi.Optimizer)
set_silent(model)
# Turn off "Heuristics" parameter so that our new one must be called. In
# real models, you should leave "Heuristics" turned on.
set_attribute(model, "Heuristics", 0)
@variable(model, x[1:N], Bin)
@constraint(model, sum(item_weights[i] * x[i] for i in 1:N) <= 10)
@objective(model, Max, sum(item_values[i] * x[i] for i in 1:N))
callback_called = false
function my_callback_function(cb_data)
callback_called = true
x_vals = callback_value.(Ref(cb_data), x)
ret =
MOI.submit(model, MOI.HeuristicSolution(cb_data), x, floor.(x_vals))
println("Heuristic solution status = $(ret)")
Test.@test ret in (
MOI.HEURISTIC_SOLUTION_ACCEPTED,
MOI.HEURISTIC_SOLUTION_REJECTED,
)
end
set_attribute(model, MOI.HeuristicCallback(), my_callback_function)
optimize!(model)
Test.@test is_solved_and_feasible(model)
Test.@test callback_called
return
end
example_heuristic_solution()
Set parameter WLSAccessID
Set parameter WLSSecret
Set parameter LicenseID to value 722777
Set parameter GURO_PAR_SPECIAL
WLS license 722777 - registered to JuMP Development
Heuristic solution status = HEURISTIC_SOLUTION_ACCEPTED
Heuristic solution status = HEURISTIC_SOLUTION_REJECTED
Gurobi solver-dependent callback
An example using Gurobi's solver-dependent callback.
function example_solver_dependent_callback()
model = direct_model(Gurobi.Optimizer())
@variable(model, 0 <= x <= 2.5, Int)
@variable(model, 0 <= y <= 2.5, Int)
@objective(model, Max, y)
cb_calls = Cint[]
function my_callback_function(cb_data, cb_where::Cint)
# You can reference variables outside the function as normal
push!(cb_calls, cb_where)
# You can select where the callback is run
if cb_where == Gurobi.GRB_CB_MIPNODE
# You can query a callback attribute using GRBcbget
resultP = Ref{Cint}()
Gurobi.GRBcbget(
cb_data,
cb_where,
Gurobi.GRB_CB_MIPNODE_STATUS,
resultP,
)
if resultP[] != Gurobi.GRB_OPTIMAL
return # Solution is something other than optimal.
end
elseif cb_where != Gurobi.GRB_CB_MIPSOL
return
end
# Before querying `callback_value`, you must call:
Gurobi.load_callback_variable_primal(cb_data, cb_where)
x_val = callback_value(cb_data, x)
y_val = callback_value(cb_data, y)
# You can submit solver-independent MathOptInterface attributes such as
# lazy constraints, user-cuts, and heuristic solutions.
if y_val - x_val > 1 + 1e-6
con = @build_constraint(y - x <= 1)
MOI.submit(model, MOI.LazyConstraint(cb_data), con)
elseif y_val + x_val > 3 + 1e-6
con = @build_constraint(y + x <= 3)
MOI.submit(model, MOI.LazyConstraint(cb_data), con)
end
# You can terminate the callback as follows:
Gurobi.GRBterminate(backend(model))
return
end
# You _must_ set this parameter if using lazy constraints.
set_attribute(model, "LazyConstraints", 1)
set_attribute(model, Gurobi.CallbackFunction(), my_callback_function)
optimize!(model)
Test.@test termination_status(model) == MOI.INTERRUPTED
return
end
example_solver_dependent_callback()
Set parameter WLSAccessID
Set parameter WLSSecret
Set parameter LicenseID to value 722777
Set parameter GURO_PAR_SPECIAL
WLS license 722777 - registered to JuMP Development
Set parameter LazyConstraints to value 1
Gurobi Optimizer version 12.0.0 build v12.0.0rc1 (linux64 - "Ubuntu 24.04.1 LTS")
CPU model: AMD EPYC 7763 64-Core Processor, instruction set [SSE2|AVX|AVX2]
Thread count: 2 physical cores, 4 logical processors, using up to 4 threads
Non-default parameters:
LazyConstraints 1
WLS license 722777 - registered to JuMP Development
Optimize a model with 0 rows, 2 columns and 0 nonzeros
Model fingerprint: 0x1cb4e750
Variable types: 0 continuous, 2 integer (0 binary)
Coefficient statistics:
Matrix range [0e+00, 0e+00]
Objective range [1e+00, 1e+00]
Bounds range [2e+00, 2e+00]
RHS range [0e+00, 0e+00]
Presolve time: 0.00s
Explored 0 nodes (0 simplex iterations) in 0.00 seconds (0.00 work units)
Thread count was 1 (of 4 available processors)
Solution count 0
Solve interrupted
Best objective -, best bound -, gap -
User-callback calls 31, time in user-callback 0.03 sec