From 04982b1bc91432a7b67a1fd7698afcee4f523b5f Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 24 Sep 2024 15:09:13 +1200 Subject: [PATCH 1/3] Add CallbackFunction to MOI wrapper --- src/MOI_wrapper.jl | 89 ++++++++++++++++++++++++++++++++++++++++++++- test/MOI_wrapper.jl | 25 +++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 58894f6..717ce56 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -242,6 +242,14 @@ end Base.isempty(x::_Solution) = x.status == _OPTIMIZE_NOT_CALLED +mutable struct _CallbackData + f::Function +end + +function Base.unsafe_convert(::Type{Ptr{Cvoid}}, d::_CallbackData) + return pointer_from_objref(d) +end + """ Optimizer() @@ -286,6 +294,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer # HiGHS just returns a single solution struct :( solution::_Solution + callback_data::Union{Nothing,_CallbackData} function Optimizer() model = new( @@ -302,6 +311,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer nothing, nothing, _Solution(), + nothing, ) MOI.empty!(model) if Sys.iswindows() @@ -365,6 +375,7 @@ function MOI.empty!(model::Optimizer) empty!(model.solution) ret = Highs_changeObjectiveOffset(model, 0.0) _check_ret(ret) + model.callback_data = nothing return end @@ -384,7 +395,8 @@ function MOI.is_empty(model::Optimizer) model.name_to_variable === nothing && model.name_to_constraint_index === nothing && isempty(model.solution) && - iszero(offset[]) + iszero(offset[]) && + model.callback_data === nothing end MOI.get(model::Optimizer, ::MOI.RawSolver) = model @@ -1960,6 +1972,8 @@ const _TerminationStatusMap = Dict( (MOI.OTHER_ERROR, "kHighsModelStatusUnknown"), kHighsModelStatusSolutionLimit => (MOI.SOLUTION_LIMIT, "kHighsModelStatusSolutionLimit"), + kHighsModelStatusInterrupt => + (MOI.INTERRUPTED, "kHighsModelStatusInterrupt"), ) function MOI.get(model::Optimizer, ::MOI.TerminationStatus) @@ -2843,6 +2857,79 @@ function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) return mapping end +""" + CallbackFunction(callback_types::Vector{Cint}) + +## Signature + +```julia +function user_callback( + callback_type::Cint, + message::Ptr{Cchar}, + data_out::HighsCallbackDataOut, +)::Cint + return user_interrupt +end +``` +""" +struct CallbackFunction + callback_types::Vector{Cint} +end + +function CallbackFunction() + # By default, turn everything on except logging + return CallbackFunction([ + # kHighsCallbackLogging, + kHighsCallbackSimplexInterrupt, + kHighsCallbackIpmInterrupt, + kHighsCallbackMipSolution, + kHighsCallbackMipImprovingSolution, + # kHighsCallbackMipLogging, + kHighsCallbackMipInterrupt, + ]) +end + +function _cfn_user_callback( + callback_type::Cint, + message::Ptr{Cchar}, + p_data_out::Ptr{HiGHS.HighsCallbackDataOut}, + p_data_in::Ptr{HiGHS.HighsCallbackDataIn}, + user_callback_data::Ptr{Cvoid}, +) + user_data = unsafe_pointer_to_objref(user_callback_data)::_CallbackData + terminate = user_data.f( + callback_type, + message, + unsafe_load(p_data_out)::HiGHS.HighsCallbackDataOut, + ) + if p_data_in != C_NULL + unsafe_store!(p_data_in, HighsCallbackDataIn(terminate)) + end + return +end + +function MOI.set(model::Optimizer, attr::CallbackFunction, f::Function) + callback_cfn = @cfunction( + _cfn_user_callback, + Cvoid, + ( + Cint, + Ptr{Cchar}, + Ptr{HighsCallbackDataOut}, + Ptr{HighsCallbackDataIn}, + Ptr{Cvoid}, + ), + ) + model.callback_data = _CallbackData(f) + ret = Highs_setCallback(model, callback_cfn, model.callback_data) + _check_ret(ret) + for callback_type in attr.callback_types + ret = Highs_startCallback(model, callback_type) + _check_ret(ret) + end + return +end + # These enums are deprecated. Use the `kHighsXXX` constants defined in # libhighs.jl instead. diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index 23d93fe..3476625 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -949,6 +949,31 @@ function test_continuous_objective_bound() return end +function test_callback_interrupt() + model = HiGHS.Optimizer() + MOI.set(model, MOI.RawOptimizerAttribute("solver"), "ipm") + MOI.set(model, MOI.RawOptimizerAttribute("presolve"), "off") + x = MOI.add_variables(model, 3) + c = MOI.add_constraint.(model, 1.0 .* x, MOI.EqualTo.(1.0:3.0)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) + f = 1.0 * x[1] + x[2] + x[3] + MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f) + callback_types = Cint[] + function user_callback( + callback_type::Cint, + ::Ptr{Cchar}, + ::HiGHS.HighsCallbackDataOut, + )::Cint + push!(callback_types, callback_type) + return 1 + end + MOI.set(model, HiGHS.CallbackFunction(), user_callback) + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.INTERRUPTED + @test kHighsCallbackIpmInterrupt in callback_types + return +end + end # module TestMOIHighs.runtests() From 2a460c0e58af0307f1c0ae699f61408ef8a94ca4 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 26 Sep 2024 15:31:46 +1200 Subject: [PATCH 2/3] Update test/MOI_wrapper.jl --- test/MOI_wrapper.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index 3476625..48bdfc9 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -970,7 +970,7 @@ function test_callback_interrupt() MOI.set(model, HiGHS.CallbackFunction(), user_callback) MOI.optimize!(model) @test MOI.get(model, MOI.TerminationStatus()) == MOI.INTERRUPTED - @test kHighsCallbackIpmInterrupt in callback_types + @test HiGHS.kHighsCallbackIpmInterrupt in callback_types return end From 2569b1d0a2455ea676fbfcbec98990564cce3681 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 27 Sep 2024 09:44:08 +1200 Subject: [PATCH 3/3] Update MOI_wrapper.jl --- src/MOI_wrapper.jl | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 717ce56..569d60e 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -2858,10 +2858,26 @@ function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) end """ - CallbackFunction(callback_types::Vector{Cint}) + CallbackFunction( + callback_types::Vector{Cint} = Cint[ + # kHighsCallbackLogging, + kHighsCallbackSimplexInterrupt, + kHighsCallbackIpmInterrupt, + kHighsCallbackMipSolution, + kHighsCallbackMipImprovingSolution, + # kHighsCallbackMipLogging, + kHighsCallbackMipInterrupt, + ], + ) <: MOI.AbstractModelAttribute + +An `MOI.AbstractModelAttribute` for setting the callback function in HiGHS. + +`callback_types` is a vector of places at which HiGHS will call the callback +function. By default, this is everything except the `Logging` callbacks. -## Signature +## Callback signature +The function you provide as a callback must have the signature: ```julia function user_callback( callback_type::Cint, @@ -2871,8 +2887,20 @@ function user_callback( return user_interrupt end ``` + +The input arguments are interpreted as follows: + + * `callback_type` is a `Cint` corresponding to the location of where the + callback is being called from + * `message` is a pointer with the logging string (if the callback type is a + `Logging` callback). Get a Julia `String` with `unsafe_string(message)`. + * `data_out` is a `HiGHS.HighsCallbackDataOut` struct. This has various + information fields, only some of which are filled. + +The return argument is a `Cint`. If it is non-zero and the callback type is an +`Interrupt`, then HiGHS will interupt the solve. """ -struct CallbackFunction +struct CallbackFunction <: MOI.AbstractModelAttribute callback_types::Vector{Cint} end