Skip to content
Permalink
Browse files

Callbacks Episode II: revenge of the solver-independent implementation (

#2101)

* Add JuMP support for callbacks

* Add documentation

* Remove const MOI from examples

* Add solver-specific GLPK callback

* Fix docs and add tests

* Fix tests

* Fix JuMP tests

* Update docs
  • Loading branch information
odow committed Nov 20, 2019
1 parent 015b38a commit fd5b66ef4aa95c892c67cf0fcecfb4b9971758f3
@@ -5,4 +5,6 @@
*.sublime*
*.opf
*.cov
Manifest.toml
docs/Manifest.toml
examples/Manifest.toml
@@ -27,6 +27,7 @@ makedocs(
"Solvers" => "solvers.md",
"Query Solutions" => "solutions.md",
"Nonlinear Modeling" => "nlp.md",
"Callbacks" => "callbacks.md",
"Style Guide" => "style.md",
"Extensions" => "extensions.md",
"Development Roadmap" => "roadmap.md"
@@ -0,0 +1,179 @@
```@meta
CurrentModule = JuMP
DocTestSetup = quote
using JuMP
end
DocTestFilters = [r"≤|<=", r"≥|>=", r" == | = ", r" ∈ | in ", r"MathOptInterface|MOI"]
```

# Callbacks

Many mixed-integer (linear, conic, and nonlinear) programming solvers offer
the ability to modify the solve process. Examples include changing branching
decisions in branch-and-bound, adding custom cutting planes, providing custom
heuristics to find feasible solutions, or implementing on-demand separators to
add new constraints only when they are violated by the current solution (also
known as lazy constraints).

While historically this functionality has been limited to solver-specific
interfaces, JuMP provides solver-independent support for three types of
callbacks:

1. lazy constraints
2. user-cuts
3. heuristic solutions

!!! warning
Using callbacks requires a solver in [Direct mode](@ref). A direct-mode
model is created using [`JuMP.direct_model`](@ref).

## Available solvers

Callback support is limited to a few solvers. This includes
[CPLEX](https://github.com/JuliaOpt/CPLEX.jl),
[GLPK](https://github.com/JuliaOpt/GLPK.jl), and
[Gurobi](https://github.com/JuliaOpt/Gurobi.jl).

!!! warning
While JuMP provides a solver-independent way of accessing callbacks, you
should not assume that you will see identical behavior when running the same
code on different solvers. For example, some solvers may ignore user-cuts
for various reasons, while other solvers may add every user-cut. Read the
underlying solver's callback documentation to understand details specific to
each solver.

## Information that can be queried during callbacks

In a callback, the only thing you may query is the primal value of the
variables using [`callback_value`](@ref).

If you need any other information, use a solver-dependent callback instead.

!!! info
Solver-dependent callbacks are mostly un-documented. Using them will require
you to read and understand the source-code of solver's Julia wrapper (i.e.,
the `Solver.jl` package).

## Lazy constraints

Lazy constraints are useful when the full set of constraints is too large to
explicitly include in the initial formulation. When a MIP solver reaches a new
solution, for example with a heuristic or by solving a problem at a node in
the branch-and-bound tree, it will give the user the chance to provide
constraint(s) that would make the current solution infeasible. For some more
information about lazy constraints, see this [blog post by Paul Rubin](http://orinanobworld.blogspot.com/2012/08/user-cuts-versus-lazy-constraints.html).

A lazy constraint callback can be set using the following syntax:

```julia
model = direct_model(GLPK.Optimizer)
@variable(model, x <= 10, Int)
@objective(model, Max, x)
function my_callback_function(cb_data)
x_val = callback_value(cb_data, x)
if x_val > 2 + 1e-6
con = @build_constraint(x <= 2)
MOI.submit(model, MOI.LazyConstraint(cb_data), con)
end
end
MOI.set(model, MOI.LazyConstraintCallback(), my_callback_function)
```

!!! info
The lazy constraint callback _may_ be called at fractional or integer
nodes in the branch-and-bound tree. There is no guarantee that the
callback is called at _every_ feasible primal solution.

## User cuts

User cuts, or simply cuts, provide a way for the user to tighten the LP
relaxation using problem-specific knowledge that the solver cannot or is
unable to infer from the model. Just like with lazy constraints, when a MIP
solver reaches a new node in the branch-and-bound tree, it will give the user
the chance to provide cuts to make the current relaxed (fractional) solution
infeasible in the hopes of obtaining an integer solution. For more details
about the difference between user cuts and lazy constraints see the
aforementioned [blog post](http://orinanobworld.blogspot.com/2012/08/user-cuts-versus-lazy-constraints.html).

A user-cut callback can be set using the following syntax:

```julia
model = direct_model(GLPK.Optimizer)
@variable(model, x <= 10.5, Int)
@objective(model, Max, x)
function my_callback_function(cb_data)
x_val = callback_value(cb_data, x)
con = @build_constraint(x <= floor(x_val))
MOI.submit(model, MOI.UserCut(cb_data), con)
end
MOI.set(model, MOI.UserCutCallback(), my_callback_function)
```

!!! warning
Your user cuts should not change the set of integer feasible solutions.
Equivalently, your cuts can only remove fractional solutions. If you add a
cut that removes an integer solution, the solver may return an incorrect
solution.

!!! info
The user-cut callback _may_ be called at fractional nodes in the
branch-and-bound tree. There is no guarantee that the callback is called
at _every_ fractional primal solution.

## Heuristic solutions

Integer programming solvers frequently include heuristics that run at the
nodes of the branch-and-bound tree. They aim to find integer solutions quicker
than plain branch-and-bound would to tighten the bound, allowing us to fathom
nodes quicker and to tighten the integrality gap.

Some heuristics take integer solutions and explore their "local neighborhood"
(e.g., flipping binary variables, fix some variables and solve a smaller MILP)
and others take fractional solutions and attempt to round them in an
intelligent way.

You may want to add a heuristic of your own if you have some special insight
into the problem structure that the solver is not aware of, e.g. you can
consistently take fractional solutions and intelligently guess integer
solutions from them.

A heuristic solution callback can be set using the following syntax:

```julia
model = direct_model(GLPK.Optimizer)
@variable(model, x <= 10.5, Int)
@objective(model, Max, x)
function my_callback_function(cb_data)
x_val = callback_value(cb_data, x)
status = MOI.submit(
model, MOI.HeuristicSolution(cb_data), [x], [floor(Int, x_val)]
)
println("I submitted a heuristic solution, and the status was: ", status)
end
MOI.set(model, MOI.HeuristicCallback(), my_callback_function)
```

The third argument to `submit` should be a vector of JuMP variables, and the
fourth argument should be a vector of values corresponding to each variable.

`MOI.submit` returns an enum that depends on whether the solver accepted the
solution. The possible return codes are:

- `MOI.HEURISTIC_SOLUTION_ACCEPTED`
- `MOI.HEURISTIC_SOLUTION_REJECTED`
- `MOI.HEURISTIC_SOLUTION_UNKNOWN`

!!! warning
Some solvers may accept partial solutions. Others require a feasible integer
solution for every variable. If in doubt, provide a complete solution.

!!! info
The heuristic solution callback _may_ be called at fractional nodes in the
branch-and-bound tree. There is no guarantee that the callback is called
at _every_ fractional primal solution.

## Reference

```@docs
callback_value
```
@@ -2,7 +2,9 @@
GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6"
Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
SCS = "c946c3f1-0d1f-5ce8-9dea-7daa1f7e2d13"

[compat]
GLPK = "0.12"
MathOptInterface = "~0.9"
@@ -0,0 +1,157 @@
# Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#############################################################################
# JuMP
# An algebraic modeling language for Julia
# See http://github.com/JuliaOpt/JuMP.jl
#############################################################################

using GLPK
using JuMP
using Random
using Test

"""
example_lazy_constraint()
An example using a lazy constraint callback.
"""
function example_lazy_constraint()
model = direct_model(GLPK.Optimizer())
@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)
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 <= 1)
MOI.submit(model, MOI.LazyConstraint(cb_data), con)
end
end
MOI.set(model, MOI.LazyConstraintCallback(), my_callback_function)
optimize!(model)
@test termination_status(model) == MOI.OPTIMAL
@test primal_status(model) == MOI.FEASIBLE_POINT
@test lazy_called
@test value(x) == 1
@test value(y) == 2
end

example_lazy_constraint()

"""
example_user_cut_constraint()
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 = direct_model(GLPK.Optimizer())
@variable(model, x[1:N], Bin)
@constraint(model, sum(item_weights[i] * x[i] for i = 1:N) <= 10)
@objective(model, Max, sum(item_values[i] * x[i] for i = 1:N))

callback_called = false
function my_callback_function(cb_data)
callback_called = true
# TODO(odow): remove Ref once GLPK supports broadcasting over cb_data.
x_vals = callback_value.(Ref(cb_data), x)
accumulated = sum(item_weights[i] for i=1:N if x_vals[i] > 1e-4)
n_terms = sum(1 for i=1:N if x_vals[i] > 1e-4)
if accumulated > 10
con = @build_constraint(
sum(x[i] for i = 1:N if x_vals[i] > 0.5) <= n_terms - 1
)
MOI.submit(model, MOI.UserCut(cb_data), con)
end
end
MOI.set(model, MOI.UserCutCallback(), my_callback_function)
optimize!(model)
@test termination_status(model) == MOI.OPTIMAL
@test primal_status(model) == MOI.FEASIBLE_POINT
@test callback_called
end

example_user_cut_constraint()

"""
example_heuristic_solution()
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 = direct_model(GLPK.Optimizer())
@variable(model, x[1:N], Bin)
@constraint(model, sum(item_weights[i] * x[i] for i = 1:N) <= 10)
@objective(model, Max, sum(item_values[i] * x[i] for i = 1:N))

callback_called = false
function my_callback_function(cb_data)
callback_called = true
# TODO(odow): remove Ref once GLPK supports broadcasting over cb_data.
x_vals = callback_value.(Ref(cb_data), x)
@test MOI.submit(
model, MOI.HeuristicSolution(cb_data), x, floor.(x_vals)
) in (MOI.HEURISTIC_SOLUTION_ACCEPTED, MOI.HEURISTIC_SOLUTION_REJECTED)
end
MOI.set(model, MOI.HeuristicCallback(), my_callback_function)
optimize!(model)
@test termination_status(model) == MOI.OPTIMAL
@test primal_status(model) == MOI.FEASIBLE_POINT
@test callback_called
end

example_heuristic_solution()

"""
example_solver_dependent_callback()
An example using a solver_dependent callback.
"""
function example_solver_dependent_callback()
model = direct_model(GLPK.Optimizer())
@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
reason = GLPK.ios_reason(cb_data.tree)
if reason != GLPK.IROWGEN
return
end
x_val = callback_value(cb_data, x)
y_val = callback_value(cb_data, y)
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 <= 1)
MOI.submit(model, MOI.LazyConstraint(cb_data), con)
end
end
MOI.set(model, GLPK.CallbackFunction(), my_callback_function)
optimize!(model)
@test termination_status(model) == MOI.OPTIMAL
@test primal_status(model) == MOI.FEASIBLE_POINT
@test lazy_called
@test value(x) == 1
@test value(y) == 2
end

example_solver_dependent_callback()
@@ -9,7 +9,6 @@
#############################################################################

using JuMP, GLPK, Test
const MOI = JuMP.MathOptInterface

"""
example_cannery(; verbose = true)
@@ -9,7 +9,6 @@
#############################################################################

using JuMP, Ipopt, Test
const MOI = JuMP.MathOptInterface

"""
clnlbeam
@@ -9,7 +9,6 @@
#############################################################################

using JuMP, GLPK, Test
const MOI = JuMP.MathOptInterface

"""
example_diet()
@@ -9,7 +9,6 @@
#############################################################################

using JuMP, GLPK, Test
const MOI = JuMP.MathOptInterface

"""
example_knapsack(; verbose = true)

0 comments on commit fd5b66e

Please sign in to comment.
You can’t perform that action at this time.