Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would vote for 3) as 1) would not work as the CachingOptimizer will try to query from the cache.
In addition, we could have is_set_in_optimize and the CachingOptimizer checks is_set_in_optimizer || is_set_by_optimizer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might actually be easier just to have this slight work-around for callback attributes. We don't have many, and it's simpler than defining a new check with all the correct plumbing. If we have many more attributes, we can revisit this.

# 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