I am getting often questions about why generic callbacks were dropped from JuMP. I think we need to have in the doc an example that shows the solver dependent aspect of callbacks and the inconsistency issues it can raise.
Also I wonder if we can restrict the scope of that dependancy.
For example instead of the old:
function user_routine(cb::MathProgBase.MathProgCallbackData)
x_val = getvalue(x)
y_val = getvalue(y)
...
@lazyconstraint(cb, y - x <= 1)
...
end
# Tell JuMP/Cplex to use our callback function
addlazycallback(m, user_routing)
we can imagine something like:
struct Cplex.LazyConstraintCallback <: MOI.ConstraintGenerationCallback
...
end
struct Cplex.LazyConstraintCallbackData <: MOI.ConstraintGenerationCallbackData
...
end
function user_routine(cb)
x_val = getvalue(x)
y_val = getvalue(y)
...
@generated_constraint(cb, y - x <= 1)
...
end
# Tell JuMP/Cplex to use our callback function
add_constraint_generation_callback(m, CPLEX.LazyConstraintCallback, user_routine)
While add_constraint_generation_callback will work for any constraint generation callback ( lazy constraint, a user cut, ... )
Even if we don't develop it right now, pointing on how the new callbacks might look like in a year or so, may encourage people who are still hesitant to switch to JuMP.
The docs are an appropriate place to present a roadmap of our plans for callbacks in JuMP. A long-form rant on why we dropped a feature from 0.18 to 0.19 would be more appropriate for a discourse post.
This framework looks general and straightforward. For callbacks that do not generate constraints, for example, the heuristic callback, there can be something similar like:
add_heuristic_callback(m, CPLEX.HeuristicCallback, user_defined_heuristic_soln)
Another method is to have a more general function to include all types of callbacks:
add_callback(m, CPLEX.SomeCallback, user_routine)
where CPLEX.SomeCallback can be a cut callback or a heuristic callback or some other types of callbacks.
Here are lazy constraints in Gurobi at the MOI level
https://github.com/JuliaOpt/Gurobi.jl/blob/a5d96f42bc51f9771a149a3c6a9fa15910a28665/src/MOIWrapper.jl#L491-L529
and an example
https://github.com/JuliaOpt/Gurobi.jl/blob/a5d96f42bc51f9771a149a3c6a9fa15910a28665/test/MOIWrapper.jl#L126-L192
At the JuMP level, you could imagine a syntax like the following:
@function_in_set (to be bikeshed) would parse a constraint into an MOI function and MOI set.
model = Model(with_optimizer(Gurobi.Optimizer))
@variable(model, 0 <= x <= 2, Int)
@variable(model, 0 <= y <= 2, Int)
@objective(model, Max, y)
MOI.set(model, Gurobi.CallbackFunction() do cb_data, cb_where
Gurobi.loadcbsolution!(model, cb_data, cb_where)
x_val = JuMP.result_value(x)
y_val = JuMP.result_value(y)
# We have two constraints, one cutting off the top
# left corner and one cutting off the top right corner, e.g.
# (0,2) +---+---+ (2,2)
# |xx/ \xx|
# |x/ \x|
# |/ \|
# (0,1) + + (2,1)
# | |
# (0,0) +---+---+ (2,0)
TOL = 1e-6 # Allow for some impreciseness in the solution
if y_val - x_val > 1 + TOL
Gurobi.cblazy!(cb_data, model, JuMP.@function_in_set(y <= 1 + x))
elseif y_val + x_val > 3 + TOL
Gurobi.cblazy!(cb_data, model, JuMP.@function_in_set(y <= 3 - x))
end
end)
@function_in_set is equivalent to the old @LinearConstraint: https://github.com/JuliaOpt/JuMP.jl/issues/710#issuecomment-436862411
Most helpful comment
Here are lazy constraints in Gurobi at the MOI level
https://github.com/JuliaOpt/Gurobi.jl/blob/a5d96f42bc51f9771a149a3c6a9fa15910a28665/src/MOIWrapper.jl#L491-L529
and an example
https://github.com/JuliaOpt/Gurobi.jl/blob/a5d96f42bc51f9771a149a3c6a9fa15910a28665/test/MOIWrapper.jl#L126-L192
At the JuMP level, you could imagine a syntax like the following:
@function_in_set(to be bikeshed) would parse a constraint into an MOI function and MOI set.