diff --git a/src/MOI/MOI_callbacks.jl b/src/MOI/MOI_callbacks.jl index 694c0c3d..3350ea8b 100644 --- a/src/MOI/MOI_callbacks.jl +++ b/src/MOI/MOI_callbacks.jl @@ -11,6 +11,8 @@ Set a generic Xpress callback function. struct CallbackFunction <: MOI.AbstractCallback end +MOI.supports(::Optimizer, ::CallbackFunction) = true + function MOI.set(model::Optimizer, ::CallbackFunction, ::Nothing) if model.callback_data !== nothing Lib.XPRSremovecboptnode(model.inner, C_NULL, C_NULL) @@ -26,7 +28,6 @@ function MOI.set(model::Optimizer, ::CallbackFunction, f::Function) model.callback_data = nothing end model.has_generic_callback = true - # Starting with this callback to test model.callback_data = set_callback_optnode!( model.inner, (cb_data) -> begin @@ -37,7 +38,6 @@ function MOI.set(model::Optimizer, ::CallbackFunction, f::Function) ) return end -MOI.supports(::Optimizer, ::CallbackFunction) = true function get_cb_solution(model::Optimizer, model_inner::XpressProblem) reset_callback_cached_solution(model) @@ -51,26 +51,26 @@ function get_cb_solution(model::Optimizer, model_inner::XpressProblem) return end -function applycuts(opt::Optimizer, model::XpressProblem) - itype = Cint(1) - interp = Cint(-1) # Get all cuts - delta = 0.0#Lib.XPRS_MINUSINFINITY - ncuts = Array{Cint}(undef, 1) - size = Cint(length(opt.cb_cut_data.cutptrs)) - mcutptr = Array{Lib.XPRScut}(undef, size) - dviol = Array{Cdouble}(undef, size) - Lib.XPRSgetcpcutlist( - model, - itype, - interp, - delta, - ncuts, +function _load_existing_cuts(model::Optimizer, cb_data::CallbackData) + if isempty(model.cb_cut_data.cutptrs) + return false + end + p_ncuts = Ref{Cint}(0) + size = length(model.cb_cut_data.cutptrs) + mcutptr = Vector{Lib.XPRScut}(undef, size) + dviol = Vector{Cdouble}(undef, size) + @checked Lib.XPRSgetcpcutlist( + cb_data.model, + 1, # itype + -1, # interp + 0.0, # delta + p_ncuts, size, mcutptr, dviol, - ) # requires an availabel solution - Lib.XPRSloadcuts(model, itype, interp, ncuts[1], mcutptr) - return ncuts[1] > 0 + ) + @checked Lib.XPRSloadcuts(cb_data.model, 1, -1, p_ncuts[], mcutptr) + return p_ncuts[] > 0 end # ============================================================================== @@ -78,74 +78,53 @@ end # ============================================================================== function default_moi_callback(model::Optimizer) - return (cb_data) -> begin + function default_callback(cb_data) + # If we added a cut from the existing pool, it means that the current + # solution violates a cut that we previously added but that has since + # been deleted. + # + # TODO(odow): could we remove this? We don't enforce it for any other + # solver. + if _load_existing_cuts(model, cb_data) + return + end + # Check if this callback has been called at this solution before and + # exit. We should be called only once at each node. + attr = Lib.XPRS_CALLBACKCOUNT_OPTNODE + if @_invoke(Lib.XPRSgetintattrib(cb_data.model, attr, _)::Int) > 1 + return + end get_cb_solution(model, cb_data.model) if model.heuristic_callback !== nothing model.callback_state = CB_HEURISTIC - # only allow one heuristic solution per LP optimal node - cb_count = @_invoke Lib.XPRSgetintattrib( - cb_data.model, - Lib.XPRS_CALLBACKCOUNT_OPTNODE, - _, - )::Int - if cb_count > 1 - return - end model.heuristic_callback(cb_data) end if model.user_cut_callback !== nothing model.callback_state = CB_USER_CUT - # apply stored cuts if any - if length(model.cb_cut_data.cutptrs) > 0 - added = applycuts(model, cb_data.model) - if added - return - end - end - # only allow one user cut solution per LP optimal node - # limiting two calls to guarantee th user has a chance to add - # a cut. if the user cut is loose the problem will be resolved anyway. - cb_count = @_invoke Lib.XPRSgetintattrib( - cb_data.model, - Lib.XPRS_CALLBACKCOUNT_OPTNODE, - _, - )::Int - if cb_count > 2 - return - end model.user_cut_callback(cb_data) end if model.lazy_callback !== nothing model.callback_state = CB_LAZY - # add previous cuts if any - # to gurantee the user is dealing with a optimal solution - # feasibile for exisitng cuts - if length(model.cb_cut_data.cutptrs) > 0 - added = applycuts(model, cb_data.model) - if added - return - end - end model.lazy_callback(cb_data) end + return end - return + return default_callback end function MOI.get(model::Optimizer, attr::MOI.CallbackNodeStatus{CallbackData}) - if check_moi_callback_validity(model) - mip_infeas = @_invoke Lib.XPRSgetintattrib( - attr.callback_data.model, - Lib.XPRS_MIPINFEAS, - _, - )::Int - if mip_infeas == 0 - return MOI.CALLBACK_NODE_STATUS_INTEGER - elseif mip_infeas > 0 - return MOI.CALLBACK_NODE_STATUS_FRACTIONAL - end + if !check_moi_callback_validity(model) + return MOI.CALLBACK_NODE_STATUS_UNKNOWN + end + mip_infeas = @_invoke Lib.XPRSgetintattrib( + attr.callback_data.model, + Lib.XPRS_MIPINFEAS, + _, + )::Int + if mip_infeas == 0 + return MOI.CALLBACK_NODE_STATUS_INTEGER end - return MOI.CALLBACK_NODE_STATUS_UNKNOWN + return MOI.CALLBACK_NODE_STATUS_FRACTIONAL end function MOI.get( @@ -153,12 +132,30 @@ function MOI.get( ::MOI.CallbackVariablePrimal{CallbackData}, x::MOI.VariableIndex, ) - return model.callback_cached_solution.variable_primal[_info( - model, - x, - ).column] + column = _info(model, x).column + return model.callback_cached_solution.variable_primal[column] +end + +function callback_exception(model::Optimizer, cb, err::Exception) + model.cb_exception = err + Lib.XPRSinterrupt(cb.callback_data.model, Lib.XPRS_STOP_USER) + return end +function _throw_if_invalid_state(model, cb, calling_state) + if model.callback_state in (calling_state, CB_NONE, CB_GENERIC) + return + end + attr = if model.callback_state == CB_HEURISTIC + MOI.HeuristicCallback() + elseif model.callback_state == CB_LAZY + MOI.LazyConstraintCallback() + else + @assert model.callback_state == CB_USER_CUT + MOI.UserCutCallback() + end + return callback_exception(model, cb, MOI.InvalidCallbackUsage(attr, cb)) +end # ============================================================================== # MOI.UserCutCallback & MOI.LazyConstraint # ============================================================================== @@ -169,95 +166,88 @@ function MOI.set(model::Optimizer, ::MOI.UserCutCallback, cb::Function) return end +MOI.supports(::Optimizer, ::MOI.UserCutCallback) = true + +MOI.supports(::Optimizer, ::MOI.UserCut{CallbackData}) = true + +function MOI.submit( + model::Optimizer, + cb::MOI.UserCut{CallbackData}, + f::MOI.ScalarAffineFunction{Float64}, + s::Union{ + MOI.LessThan{Float64}, + MOI.GreaterThan{Float64}, + MOI.EqualTo{Float64}, + }, +) + model.cb_cut_data.submitted = true + _throw_if_invalid_state(model, cb, CB_USER_CUT) + indices, coefficients = _indices_and_coefficients(model, f) + sense, rhs = _sense_and_rhs(s) + mindex = Vector{Lib.XPRScut}(undef, 1) + @checked Lib.XPRSstorecuts( + cb.callback_data.model, + 1, # ncuts + 2, # nodupl, + Cint[1], # mtype + [sense], # sensetype, + [rhs - f.constant], # drhs + Cint[0, length(indices)], # mstart + mindex, + Cint.(indices .- 1), # mcols + coefficients, + ) + @checked Lib.XPRSloadcuts(cb.callback_data.model, 1, Cint(-1), 1, mindex) + push!(model.cb_cut_data.cutptrs, mindex[1]) + return +end + +# ============================================================================== +# MOI.LazyConstraint +# ============================================================================== + function MOI.set(model::Optimizer, ::MOI.LazyConstraintCallback, cb::Function) MOI.set(model, MOI.RawOptimizerAttribute("MIPDUALREDUCTIONS"), 0) model.lazy_callback = cb return end -MOI.supports(::Optimizer, ::MOI.UserCutCallback) = true -MOI.supports(::Optimizer, ::MOI.UserCut{CallbackData}) = true - MOI.supports(::Optimizer, ::MOI.LazyConstraintCallback) = true -MOI.supports(::Optimizer, ::MOI.LazyConstraint{CallbackData}) = true function MOI.submit( model::Optimizer, - cb::CB, + cb::MOI.LazyConstraint{CallbackData}, f::MOI.ScalarAffineFunction{Float64}, s::Union{ MOI.LessThan{Float64}, MOI.GreaterThan{Float64}, MOI.EqualTo{Float64}, }, -) where {CB<:Union{MOI.UserCut{CallbackData},MOI.LazyConstraint{CallbackData}}} - model_cb = cb.callback_data.model +) model.cb_cut_data.submitted = true - if model.callback_state == CB_HEURISTIC - cache_exception( - model, - MOI.InvalidCallbackUsage(MOI.HeuristicCallback(), cb), - ) - Lib.XPRSinterrupt(model_cb, Lib.XPRS_STOP_USER) - return - elseif model.callback_state == CB_LAZY && CB <: MOI.UserCut{CallbackData} - cache_exception( - model, - MOI.InvalidCallbackUsage(MOI.LazyConstraintCallback(), cb), - ) - Lib.XPRSinterrupt(model_cb, Lib.XPRS_STOP_USER) - return - elseif model.callback_state == CB_USER_CUT && - CB <: MOI.LazyConstraint{CallbackData} - cache_exception( - model, - MOI.InvalidCallbackUsage(MOI.UserCutCallback(), cb), - ) - Lib.XPRSinterrupt(model_cb, Lib.XPRS_STOP_USER) - return - elseif !iszero(f.constant) - cache_exception( - model, - MOI.ScalarFunctionConstantNotZero{Float64,typeof(f),typeof(s)}( - f.constant, - ), - ) - Lib.XPRSinterrupt(model_cb, Lib.XPRS_STOP_USER) - return - end + _throw_if_invalid_state(model, cb, CB_LAZY) indices, coefficients = _indices_and_coefficients(model, f) sense, rhs = _sense_and_rhs(s) - - mtype = Int32[1] # Cut type - mstart = Int32[0, length(indices)] - mindex = Array{Lib.XPRScut}(undef, 1) - ncuts = Cint(1) - ncuts_ptr = Cint[0] - nodupl = Cint(2) # Duplicates are excluded from the cut pool, ignoring cut type - sensetype = Cchar[Char(sense)] - drhs = Float64[rhs] - indices .-= 1 - mcols = Cint.(indices) - interp = Cint(-1) # Load all cuts - - ret = Lib.XPRSstorecuts( - model_cb, - ncuts, - nodupl, - Cint.(mtype), - sensetype, - drhs, - Cint.(mstart), + mindex = Vector{Lib.XPRScut}(undef, 1) + @checked Lib.XPRSstorecuts( + cb.callback_data.model, + 1, # ncuts + 2, # nodupl, + Cint[1], # mtype + [sense], # sensetype, + [rhs - f.constant], # drhs + Cint[0, length(indices)], # mstart mindex, - Cint.(mcols), + Cint.(indices .- 1), # mcols coefficients, ) - Lib.XPRSloadcuts(model_cb, mtype[], interp, ncuts, mindex) + @checked Lib.XPRSloadcuts(cb.callback_data.model, 1, Cint(-1), 1, mindex) push!(model.cb_cut_data.cutptrs, mindex[1]) - model.cb_cut_data.cutptrs return end +MOI.supports(::Optimizer, ::MOI.LazyConstraint{CallbackData}) = true + # ============================================================================== # MOI.HeuristicCallback # ============================================================================== @@ -266,6 +256,7 @@ function MOI.set(model::Optimizer, ::MOI.HeuristicCallback, cb::Function) model.heuristic_callback = cb return end + MOI.supports(::Optimizer, ::MOI.HeuristicCallback) = true function MOI.submit( @@ -274,43 +265,16 @@ function MOI.submit( variables::Vector{MOI.VariableIndex}, values::MOI.Vector{Float64}, ) - model_cb = cb.callback_data.model::Xpress.XpressProblem - model_cb2 = cb.callback_data.model_root::Xpress.XpressProblem - if model.callback_state == CB_LAZY - cache_exception( - model, - MOI.InvalidCallbackUsage(MOI.LazyConstraintCallback(), cb), - ) - Lib.XPRSinterrupt(model_cb, Lib.XPRS_STOP_USER) - return - elseif model.callback_state == CB_USER_CUT - cache_exception( - model, - MOI.InvalidCallbackUsage(MOI.UserCutCallback(), cb), - ) - Lib.XPRSinterrupt(model_cb, Lib.XPRS_STOP_USER) - return - end - ilength = length(variables) - mipsolval = fill(NaN, ilength) - mipsolcol = fill(NaN, ilength) - count = 1 - for (var, value) in zip(variables, values) - mipsolcol[count] = convert(Cint, _info(model, var).column - 1) - mipsolval[count] = value - count += 1 - end - mipsolcol = Cint.(mipsolcol) - mipsolval = Cdouble.(mipsolval) - if ilength == MOI.get(model, MOI.NumberOfVariables()) - mipsolcol = C_NULL + _throw_if_invalid_state(model, cb, CB_HEURISTIC) + nnz = length(variables) + mipsolcol = if nnz == MOI.get(model, MOI.NumberOfVariables()) + C_NULL + else + Cint[_info(model, x).column - 1 for x in variables] end - @checked Lib.XPRSaddmipsol(model_cb, ilength, mipsolval, mipsolcol, C_NULL) + model_cb = cb.callback_data.model::XpressProblem + @checked Lib.XPRSaddmipsol(model_cb, nnz, values, mipsolcol, C_NULL) return MOI.HEURISTIC_SOLUTION_UNKNOWN end -MOI.supports(::Optimizer, ::MOI.HeuristicSolution{CallbackData}) = true -function cache_exception(model::Optimizer, e::Exception) - model.cb_exception = e - return -end +MOI.supports(::Optimizer, ::MOI.HeuristicSolution{CallbackData}) = true diff --git a/test/test_MOI_wrapper.jl b/test/test_MOI_wrapper.jl index 2e594dbb..a754e2fd 100644 --- a/test/test_MOI_wrapper.jl +++ b/test/test_MOI_wrapper.jl @@ -1834,6 +1834,78 @@ function test_special_moi_attributes() return end +function test_callback_function_nothing() + model, x, y = callback_simple_model() + function callback_function(cb_data) + Xpress.get_cb_solution(model, cb_data.model) + x_val = MOI.get(model, MOI.CallbackVariablePrimal(cb_data), x) + y_val = MOI.get(model, MOI.CallbackVariablePrimal(cb_data), y) + if y_val - x_val > 1 + 1e-6 + MOI.submit( + model, + MOI.LazyConstraint(cb_data), + 1.0 * y - 1.0 * x, + MOI.LessThan{Float64}(1.0), + ) + elseif y_val + x_val > 3 + 1e-6 + MOI.submit( + model, + MOI.LazyConstraint(cb_data), + 1.0 * x + 1.0 * y, + MOI.LessThan{Float64}(3.0), + ) + end + end + MOI.set(model, Xpress.CallbackFunction(), callback_function) + MOI.optimize!(model) + @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 1 + @test MOI.get(model, MOI.VariablePrimal(), y) ≈ 2 + # Now drop the callback and re-solve + MOI.set(model, Xpress.CallbackFunction(), nothing) + MOI.optimize!(model) + x_val = MOI.get(model, MOI.VariablePrimal(), x) + y_val = MOI.get(model, MOI.VariablePrimal(), y) + # It should violate the solution + @test y_val - x_val > 1 || y_val + x_val > 3 + return +end + +function test_callback_function_replace() + model, x, y = callback_simple_model() + function callback_function(cb_data) + Xpress.get_cb_solution(model, cb_data.model) + x_val = MOI.get(model, MOI.CallbackVariablePrimal(cb_data), x) + y_val = MOI.get(model, MOI.CallbackVariablePrimal(cb_data), y) + if y_val - x_val > 1 + 1e-6 + MOI.submit( + model, + MOI.LazyConstraint(cb_data), + 1.0 * y - 1.0 * x, + MOI.LessThan{Float64}(1.0), + ) + elseif y_val + x_val > 3 + 1e-6 + MOI.submit( + model, + MOI.LazyConstraint(cb_data), + 1.0 * x + 1.0 * y, + MOI.LessThan{Float64}(3.0), + ) + end + end + MOI.set(model, Xpress.CallbackFunction(), callback_function) + MOI.optimize!(model) + @test MOI.get(model, MOI.VariablePrimal(), x) ≈ 1 + @test MOI.get(model, MOI.VariablePrimal(), y) ≈ 2 + # Now drop the callback and re-solve + MOI.set(model, Xpress.CallbackFunction(), cb_data -> nothing) + MOI.optimize!(model) + x_val = MOI.get(model, MOI.VariablePrimal(), x) + y_val = MOI.get(model, MOI.VariablePrimal(), y) + # It should violate the solution + @test y_val - x_val > 1 || y_val + x_val > 3 + return +end + end # TestMOIWrapper TestMOIWrapper.runtests()