Skip to content

Commit

Permalink
Update callback syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
odow committed Sep 22, 2020
1 parent 3e4818b commit 8091d26
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 36 deletions.
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,60 @@ Note that we are using [JuMP in direct-mode](https://jump.dev/JuMP.jl/v0.20.0/so
A complete list of supported Gurobi attributes can be found in
[their online documentation](https://www.gurobi.com/documentation/8.1/refman/attributes.html).

## Callbacks

Here is an example using Gurobi's solver-specific callbacks.

```julia
using JuMP, Gurobi, Test

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 != GRB_CB_MIPSOL && cb_where != GRB_CB_MIPNODE
return
end
# You can query a callback attribute using GRBcbget
if cb_where == GRB_CB_MIPNODE
resultP = Ref{Cint}()
GRBcbget(cb_data, cb_where, GRB_CB_MIPNODE_STATUS, resultP)
if resultP[] != GRB_OPTIMAL
return # Solution is something other than optimal.
end
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
end
# You _must_ set this parameter if using lazy constraints.
MOI.set(model, MOI.RawParameter("LazyConstraints"), 1)
MOI.set(model, Gurobi.CallbackFunction(), my_callback_function)
optimize!(model)
@test termination_status(model) == MOI.OPTIMAL
@test primal_status(model) == MOI.FEASIBLE_POINT
@test value(x) == 1
@test value(y) == 2
```

See the [Gurobi documentation](https://www.gurobi.com/documentation/9.0/refman/cb_codes.html)
for other information that can be queried with `GRBcbget`.

### Common Performance Pitfall with JuMP

Gurobi's API works differently than most solvers. Any changes to the model are
Expand Down
70 changes: 37 additions & 33 deletions src/MOI_callbacks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
# ==============================================================================

mutable struct CallbackData
model::Optimizer
ptr::Ptr{Cvoid}
end
Base.cconvert(::Type{Ptr{Cvoid}}, x::CallbackData) = x
Base.unsafe_convert(::Type{Ptr{Cvoid}}, x::CallbackData) = x.ptr::Ptr{Cvoid}

mutable struct _CallbackUserData
model::Optimizer
callback::Function
end
Base.cconvert(::Type{Ptr{Cvoid}}, x::_CallbackUserData) = x
Expand All @@ -23,7 +25,7 @@ function gurobi_callback_wrapper(
p_user_data::Ptr{Cvoid}
)
user_data = unsafe_pointer_to_objref(p_user_data)::_CallbackUserData
user_data.callback(CallbackData(cb_data), cb_where)
user_data.callback(CallbackData(user_data.model, cb_data), cb_where)
return Cint(0)
end

Expand All @@ -36,9 +38,8 @@ Callback function should be of the form
callback(cb_data::CallbackData, cb_where::Cint)
Note: before accessing `MOI.CallbackVariablePrimal`, you must call either
`Gurobi.cbget_mipsol_sol(model, cb_data, cb_where)` or
`Gurobi.cbget_mipsol_rel(model, cb_data, cb_where)`.
Note: before accessing `MOI.CallbackVariablePrimal`, you must call
`Gurobi.load_callback_variable_primal(cb_data, cb_where)`.
"""
struct CallbackFunction <: MOI.AbstractCallback end

Expand All @@ -49,6 +50,7 @@ function MOI.set(model::Optimizer, ::CallbackFunction, f::Function)
(Ptr{Cvoid}, Ptr{Cvoid}, Cint, Ptr{Cvoid})
)
user_data = _CallbackUserData(
model,
(cb_data, cb_where) -> begin
model.callback_state = CB_GENERIC
f(cb_data, cb_where)
Expand All @@ -70,32 +72,38 @@ end
MOI.supports(::Optimizer, ::CallbackFunction) = true

"""
cbget_mipsol_sol(model::Optimizer, cb_data, cb_where)
load_callback_variable_primal(cb_data, cb_where)
Load the solution at a `GRB_CB_MIPSOL` node so that it can be accessed using
Load the solution during a callback so that it can be accessed using
`MOI.CallbackVariablePrimal`.
"""
function cbget_mipsol_sol(model::Optimizer, cb_data, cb_where)
resize!(model.callback_variable_primal, length(model.variable_info))
ret = GRBcbget(
cb_data, cb_where, GRB_CB_MIPSOL_SOL, model.callback_variable_primal
function load_callback_variable_primal(cb_data::CallbackData, cb_where::Cint)
resize!(
cb_data.model.callback_variable_primal,
length(cb_data.model.variable_info),
)
_check_ret(model, ret)
return
end

"""
cbget_mipsol_rel(model::Optimizer, cb_data, cb_where)
Load the solution at a `GRB_CB_MIPNODE` node so that it can be accessed using
`MOI.CallbackVariablePrimal`.
"""
function cbget_mipsol_rel(model::Optimizer, cb_data, cb_where)
resize!(model.callback_variable_primal, length(model.variable_info))
ret = GRBcbget(
cb_data, cb_where, GRB_CB_MIPNODE_REL, model.callback_variable_primal
)
_check_ret(model, ret)
if cb_where == GRB_CB_MIPNODE
ret = GRBcbget(
cb_data,
cb_where,
GRB_CB_MIPNODE_REL,
cb_data.model.callback_variable_primal,
)
_check_ret(cb_data.model, ret)
elseif cb_where == GRB_CB_MIPSOL
ret = GRBcbget(
cb_data,
cb_where,
GRB_CB_MIPSOL_SOL,
cb_data.model.callback_variable_primal,
)
_check_ret(cb_data.model, ret)
else
error(
"`load_callback_variable_primal` can only be called at " *
"GRB_CB_MIPNODE or GRB_CB_MIPSOL."
)
end
return
end

Expand All @@ -106,22 +114,18 @@ end
function default_moi_callback(model::Optimizer)
return (cb_data, cb_where) -> begin
if cb_where == GRB_CB_MIPSOL
cbget_mipsol_sol(model, cb_data, cb_where)
load_callback_variable_primal(cb_data, cb_where)
if model.lazy_callback !== nothing
model.callback_state = CB_LAZY
model.lazy_callback(cb_data)
end
elseif cb_where == GRB_CB_MIPNODE
resultP = Ref{Cint}()
GRBcbget(cb_data, cb_where, GRB_CB_MIPNODE_STATUS, resultP)
if resultP[] != 2
if resultP[] != GRB_OPTIMAL
return # Solution is something other than optimal.
end
cbget_mipsol_rel(model, cb_data, cb_where)
if model.lazy_callback !== nothing
model.callback_state = CB_LAZY
model.lazy_callback(cb_data)
end
load_callback_variable_primal(cb_data, cb_where)
if model.user_cut_callback !== nothing
model.callback_state = CB_USER_CUT
model.user_cut_callback(cb_data)
Expand Down
6 changes: 3 additions & 3 deletions test/MOI/MOI_callbacks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ function test_CallbackFunction_callback_LazyConstraint()
function callback_function(cb_data::Gurobi.CallbackData, cb_where::Cint)
push!(cb_calls, cb_where)
if cb_where == Gurobi.GRB_CB_MIPSOL
Gurobi.cbget_mipsol_sol(model, cb_data, cb_where)
Gurobi.load_callback_variable_primal(cb_data, cb_where)
x_val = MOI.get(model, MOI.CallbackVariablePrimal(cb_data), x)
y_val = MOI.get(model, MOI.CallbackVariablePrimal(cb_data), y)
if y_val - x_val > 1 + 1e-6
Expand Down Expand Up @@ -363,7 +363,7 @@ function test_CallbackFunction_callback_UserCut()
if status[] != 2
return # Not optimal.
end
Gurobi.cbget_mipsol_rel(model, cb_data, cb_where)
Gurobi.load_callback_variable_primal(cb_data, cb_where)
terms = MOI.ScalarAffineTerm{Float64}[]
accumulated = 0.0
for (i, xi) in enumerate(x)
Expand Down Expand Up @@ -402,7 +402,7 @@ function test_CallbackFunction_callback_HeuristicSolution()
if status[] != 2
return # Not optimal.
end
Gurobi.cbget_mipsol_rel(model, cb_data, cb_where)
Gurobi.load_callback_variable_primal(cb_data, cb_where)
x_vals = MOI.get.(model, MOI.CallbackVariablePrimal(cb_data), x)
if MOI.submit(
model,
Expand Down

0 comments on commit 8091d26

Please sign in to comment.