Skip to content

Commit

Permalink
Add callback_node_status (#2397)
Browse files Browse the repository at this point in the history
  • Loading branch information
odow authored Dec 10, 2020
1 parent c2dd80a commit 1e6b5d8
Show file tree
Hide file tree
Showing 8 changed files with 83 additions and 12 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Calculus = "0.5"
DataStructures = "0.18"
ForwardDiff = "~0.5.0, ~0.6, ~0.7, ~0.8, ~0.9, ~0.10"
JSON = "0.21"
MathOptInterface = "~0.9.14"
MathOptInterface = "~0.9.19"
MutableArithmetics = "0.2"
NaNMath = "0.3"
SpecialFunctions = "0.8, 1"
Expand Down
2 changes: 1 addition & 1 deletion docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[compat]
Documenter = "~0.25"
GLPK = "0.14"
GLPK = "0.14.4"
Ipopt = "0.6"
MathOptInterface = "~0.9"
SCS = "0.7"
35 changes: 25 additions & 10 deletions docs/src/callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,26 @@ Callback support is limited to a few solvers. This includes

## Things you can and cannot do during callbacks

There is a very limited range of things you can do during a callback. Only use
There is a very limited range of things you can do during a callback. Only use
the functions and macros explicitly stated in this page of the documentation, or
in the [Callbacks example](/examples/callbacks).

Using any other part of the JuMP API (e.g., adding a constraint with [`@constraint`](@ref)
or modifying a variable bound with [`set_lower_bound`](@ref)) is undefined
behavior, and your solver may throw an error, return an incorrect solution, or
behavior, and your solver may throw an error, return an incorrect solution, or
result in a segfault that aborts Julia.

In each of the three solver-independent callbacks, the only thing you may query is
the primal value of the variables using [`callback_value`](@ref).
In each of the three solver-independent callbacks, there are two things you may
query:
- [`callback_node_status`](@ref) returns an [`MOI.CallbackNodeStatusCode`](@ref)
enum indicating if the current primal solution is integer feasible.
- [`callback_value`](@ref) returns the current primal solution of a variable.

If you need to query any other information, use a solver-dependent callback
instead. Each solver supporting a solver-dependent callback has information on
If you need to query any other information, use a solver-dependent callback
instead. Each solver supporting a solver-dependent callback has information on
how to use it in the README of their Github repository.

If you want to modify the problem in a callback, you _must_ use a lazy
If you want to modify the problem in a callback, you _must_ use a lazy
constraint.

## Lazy constraints
Expand All @@ -75,6 +78,18 @@ model = Model(GLPK.Optimizer)
@variable(model, x <= 10, Int)
@objective(model, Max, x)
function my_callback_function(cb_data)
status = callback_node_status(cb_data, model)
if status == MOI.CALLBACK_NODE_STATUS_FRACTIONAL
# `callback_value(cb_data, x)` is not integer (to some tolerance).
# If, for example, your lazy constraint generator requires an
# integer-feasible primal solution, you can add a `return` here.
return
elseif status == MOI.CALLBACK_NODE_STATUS_INTEGER
# `callback_value(cb_data, x)` is integer (to some tolerance).
else
@assert status == MOI.CALLBACK_NODE_STATUS_UNKNOWN
# `callback_value(cb_data, x)` might be fractional or integer.
end
x_val = callback_value(cb_data, x)
if x_val > 2 + 1e-6
con = @build_constraint(x <= 2)
Expand All @@ -88,11 +103,11 @@ MOI.set(model, MOI.LazyConstraintCallback(), my_callback_function)
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_ primal solution.

!!! warn
Only add a lazy constraint if your primal solution violates the constraint.
Adding the lazy constraint irrespective of feasibility may result in the
solver returning an incorrect solution, or lead to a large number of
Adding the lazy constraint irrespective of feasibility may result in the
solver returning an incorrect solution, or lead to a large number of
constraints being added, slowing down the solution process.
```julia
model = Model(GLPK.Optimizer)
Expand Down
9 changes: 9 additions & 0 deletions docs/src/examples/callbacks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ function example_lazy_constraint()
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)")
Expand Down
1 change: 1 addition & 0 deletions docs/src/reference/callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ of the manual.

```@docs
@build_constraint
callback_node_status
callback_value
```
12 changes: 12 additions & 0 deletions docs/src/reference/moi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# MathOptInterface API

This page contains extracts from the `MathOptInterface` documentation. More
information can be found at in the [MathOptInterface documentation](https://jump.dev/MathOptInterface.jl/stable/).

```@docs
MOI.CallbackNodeStatus
MOI.CallbackNodeStatusCode
MOI.CallbackVariablePrimal
MOI.get
```
19 changes: 19 additions & 0 deletions src/callbacks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,25 @@
# See https://github.com/jump-dev/JuMP.jl
#############################################################################

"""
callback_node_status(cb_data, model::Model)
Return an [`MOI.CallbackNodeStatusCode`](@ref) enum, indicating if the current
primal solution available from [`callback_value`](@ref) is integer feasible.
"""
function callback_node_status(cb_data, model::Model)
# TODO(odow):
# MOI defines `is_set_by_optimize(::CallbackNodeStatus) = true`.
# This causes problems for JuMP because it checks the termination_status to
# see if optimize! has been called. Solutions are:
# 1) defining is_set_by_optimize = false
# 2) adding a flag to JuMP to store whether it is in a callback
# 3) adding IN_OPTIMIZE to termination_status for callbacks
# Once this is resolved, we can replace the current function with:
# MOI.get(model, MOI.CallbackNodeStatus(cb_data))
return MOI.get(backend(model), MOI.CallbackNodeStatus(cb_data))
end

"""
callback_value(cb_data, x::VariableRef)
Expand Down
15 changes: 15 additions & 0 deletions test/callbacks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,18 @@ end
quad_expr = expr^2
@test callback_value(cb, quad_expr) == 4
end

@testset "callback_node_status" begin
mock = MOI.Utilities.MockOptimizer(
MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
)
cb = DummyCallbackData()
model = direct_model(mock)
@variable(model, 0 <= x <= 2.5, Int)
MOIU.set_mock_optimize!(mock, mock -> begin
MOI.set(mock, MOI.TerminationStatus(), MOI.OPTIMAL)
MOI.set(mock, MOI.CallbackNodeStatus(cb), MOI.CALLBACK_NODE_STATUS_INTEGER)
end)
optimize!(model)
@test callback_node_status(cb, model) == MOI.CALLBACK_NODE_STATUS_INTEGER
end

0 comments on commit 1e6b5d8

Please sign in to comment.