diff --git a/Project.toml b/Project.toml index 0b4e563944f..23fa573b751 100644 --- a/Project.toml +++ b/Project.toml @@ -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" diff --git a/docs/Project.toml b/docs/Project.toml index 3f7711438a4..333ef4c0e57 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -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" diff --git a/docs/src/callbacks.md b/docs/src/callbacks.md index baaa536218a..0d69041292e 100644 --- a/docs/src/callbacks.md +++ b/docs/src/callbacks.md @@ -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 @@ -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) @@ -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) diff --git a/docs/src/examples/callbacks.jl b/docs/src/examples/callbacks.jl index 8a2d5734bb7..7e0c244a3cd 100644 --- a/docs/src/examples/callbacks.jl +++ b/docs/src/examples/callbacks.jl @@ -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)") diff --git a/docs/src/reference/callbacks.md b/docs/src/reference/callbacks.md index 399c3493301..f0929be7e70 100644 --- a/docs/src/reference/callbacks.md +++ b/docs/src/reference/callbacks.md @@ -5,5 +5,6 @@ of the manual. ```@docs @build_constraint +callback_node_status callback_value ``` diff --git a/docs/src/reference/moi.md b/docs/src/reference/moi.md new file mode 100644 index 00000000000..b0d3fc7fcd9 --- /dev/null +++ b/docs/src/reference/moi.md @@ -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 +``` diff --git a/src/callbacks.jl b/src/callbacks.jl index f5bb8355f54..2dce4060817 100644 --- a/src/callbacks.jl +++ b/src/callbacks.jl @@ -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) diff --git a/test/callbacks.jl b/test/callbacks.jl index 22fdd066669..a7303e66c45 100644 --- a/test/callbacks.jl +++ b/test/callbacks.jl @@ -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