diff --git a/src/MOI/MOI_callbacks.jl b/src/MOI/MOI_callbacks.jl deleted file mode 100644 index 2fbb53e8..00000000 --- a/src/MOI/MOI_callbacks.jl +++ /dev/null @@ -1,251 +0,0 @@ -""" - CallbackFunction() - -Set a generic Xpress callback function. -""" - -struct CallbackFunction <: MOI.AbstractCallback end - -function MOI.set(model::Optimizer, ::CallbackFunction, ::Nothing) - if model.callback_data !== nothing - Lib.XPRSremovecboptnode(model.inner, C_NULL, C_NULL) - model.callback_data = nothing - end - model.has_generic_callback = false - return -end - -function MOI.set(model::Optimizer, ::CallbackFunction, f::Function) - if model.callback_data !== nothing - Lib.XPRSremovecboptnode(model.inner, C_NULL, C_NULL) - 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 - model.callback_state = CB_GENERIC - f(cb_data) - model.callback_state = CB_NONE - end) - return -end -MOI.supports(::Optimizer, ::CallbackFunction) = true - -function get_cb_solution(model::Optimizer, model_inner::XpressProblem) - reset_callback_cached_solution(model) - Lib.XPRSgetlpsol(model_inner, - model.callback_cached_solution.variable_primal, - model.callback_cached_solution.linear_primal, - model.callback_cached_solution.linear_dual, - model.callback_cached_solution.variable_dual) - 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, size, mcutptr, dviol) # requires an availabel solution - Lib.XPRSloadcuts(model, itype, interp, ncuts[1], mcutptr) - return ncuts[1] > 0 -end - -# ============================================================================== -# MOI callbacks -# ============================================================================== - -function default_moi_callback(model::Optimizer) - return (cb_data) -> begin - 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 - end - return -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 - end - return MOI.CALLBACK_NODE_STATUS_UNKNOWN -end - -function MOI.get( - model::Optimizer, - ::MOI.CallbackVariablePrimal{CallbackData}, - x::MOI.VariableIndex -) - return model.callback_cached_solution.variable_primal[_info(model, x).column] -end - -# ============================================================================== -# MOI.UserCutCallback & MOI.LazyConstraint -# ============================================================================== - -function MOI.set(model::Optimizer, ::MOI.UserCutCallback, cb::Function) - model.user_cut_callback = cb - return -end - -function MOI.set(model::Optimizer, ::MOI.LazyConstraintCallback, cb::Function) - 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, - 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 - 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, Cint.(mcols), coefficients) - Lib.XPRSloadcuts(model_cb, mtype[], interp, ncuts, mindex) - push!(model.cb_cut_data.cutptrs, mindex[1]) - model.cb_cut_data.cutptrs - return -end - -# ============================================================================== -# MOI.HeuristicCallback -# ============================================================================== - -function MOI.set(model::Optimizer, ::MOI.HeuristicCallback, cb::Function) - model.heuristic_callback = cb - return -end -MOI.supports(::Optimizer, ::MOI.HeuristicCallback) = true - -function MOI.submit( - model::Optimizer, - cb::MOI.HeuristicSolution{CallbackData}, - 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 - end - addmipsol(model_cb, ilength, mipsolval, 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 \ No newline at end of file diff --git a/src/MOI/MOI_wrapper.jl b/src/MOI/MOI_wrapper.jl index 8e520318..e095cb0d 100644 --- a/src/MOI/MOI_wrapper.jl +++ b/src/MOI/MOI_wrapper.jl @@ -3,10 +3,9 @@ error("Versions 1.1.x of julia are not supported. The current verions is $(VERSION)") end -import MathOptInterface +import MathOptInterface as MOI using SparseArrays -const MOI = MathOptInterface const CleverDicts = MOI.Utilities.CleverDicts @enum( @@ -45,15 +44,6 @@ const CleverDicts = MOI.Utilities.CleverDicts SCALAR_QUADRATIC, ) -@enum( - CallbackState, - CB_NONE, - CB_GENERIC, - CB_LAZY, - CB_USER_CUT, - CB_HEURISTIC, -) - const SCALAR_SETS = Union{ MOI.GreaterThan{Float64}, MOI.LessThan{Float64}, @@ -146,11 +136,6 @@ mutable struct CachedSolution solve_time::Float64 end -mutable struct CallbackCutData - submitted::Bool - cutptrs::Vector{Lib.XPRScut} -end - mutable struct BasisStatus con_status::Vector{Cint} var_status::Vector{Cint} @@ -173,6 +158,8 @@ mutable struct IISData colbndtype::Vector{UInt8} # sense of the column bounds that participate end +include("callbacks/interface.jl") + mutable struct Optimizer <: MOI.AbstractOptimizer # The low-level Xpress model. inner::XpressProblem @@ -183,6 +170,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer # A flag to keep track of MOI.Silent, which over-rides the OUTPUTLOG # parameter. log_level::Int32 + # option to show warnings in Windows show_warning::Bool @@ -210,7 +198,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer VariableInfo, typeof(CleverDicts.key_to_index), typeof(CleverDicts.index_to_key), - } + } # An index that is incremented for each new constraint (regardless of type). # We can check if a constraint is valid by checking if it is in the correct @@ -250,21 +238,19 @@ mutable struct Optimizer <: MOI.AbstractOptimizer forward_sensitivity_cache::Union{Nothing, SensitivityCache} backward_sensitivity_cache::Union{Nothing, SensitivityCache} - # Callback fields. - callback_cached_solution::Union{Nothing, CachedSolution} - cb_cut_data::CallbackCutData - callback_state::CallbackState - cb_exception::Union{Nothing, Exception} - - lazy_callback::Union{Nothing, Function} - user_cut_callback::Union{Nothing, Function} - heuristic_callback::Union{Nothing, Function} + # Callback fields + # For each callback type, there is a vector containing the associated + # low-level, Xpress-specific callbacks. This allows the construction of + # elaborated, high-level, callbacks by composition of Xpress's bindings + callback_table::CallbackTable - has_generic_callback::Bool - callback_data::Union{Nothing, Tuple{Ptr{Nothing}, _CallbackUserData}} - message_callback::Union{Nothing, Tuple{Ptr{Nothing}, _CallbackUserData}} + callback_cached_solution::Union{Nothing, CachedSolution} + callback_cut_data::CallbackCutData + callback_state::Vector{CallbackState} + callback_exception::Union{Nothing, Exception} params::Dict{Any, Any} + """ Optimizer() @@ -283,8 +269,6 @@ mutable struct Optimizer <: MOI.AbstractOptimizer model.solve_method = "" model.solve_relaxation = false - model.message_callback = nothing - model.termination_status = MOI.OPTIMIZE_NOT_CALLED model.primal_status = MOI.NO_SOLUTION model.dual_status = MOI.NO_SOLUTION @@ -298,7 +282,11 @@ mutable struct Optimizer <: MOI.AbstractOptimizer model.affine_constraint_info = Dict{Int, ConstraintInfo}() model.sos_constraint_info = Dict{Int, ConstraintInfo}() - MOI.empty!(model) # inner is initialized here + model.callback_table = CallbackTable() + model.callback_state = CallbackState[] + model.callback_cut_data = CallbackCutData() + + MOI.empty!(model) # inner is initialized here return model end @@ -307,21 +295,22 @@ end Base.show(io::IO, model::Optimizer) = show(io, model.inner) function MOI.empty!(model::Optimizer) - + # Instantiate new inner problem model.inner = XpressProblem() + # Reset parameters for (name, value) in model.params MOI.set(model, name, value) end MOI.set(model, MOI.RawOptimizerAttribute("MPSNAMELENGTH"), 64) MOI.set(model, MOI.RawOptimizerAttribute("CALLBACKFROMMASTERTHREAD"), 1) - MOI.set(model, MOI.RawOptimizerAttribute("XPRESS_WARNING_WINDOWS"), model.show_warning) # disable log caching previous state log_level = model.log_level log_level != 0 && MOI.set(model, MOI.RawOptimizerAttribute("OUTPUTLOG"), 0) + # silently load a empty model - to avoid useless printing @checked Lib.XPRSloadlp(model.inner, "", 0, 0, C_NULL, C_NULL, C_NULL, C_NULL, C_NULL, C_NULL, C_NULL, C_NULL, C_NULL, C_NULL) # re-enable logging @@ -346,32 +335,27 @@ function MOI.empty!(model::Optimizer) model.primal_status = MOI.NO_SOLUTION model.dual_status = MOI.NO_SOLUTION + empty!(model.callback_table) + empty!(model.callback_state) + empty!(model.callback_cut_data) + model.callback_cached_solution = nothing - model.cb_cut_data = CallbackCutData(false, Array{Lib.XPRScut}(undef,0)) - model.callback_state = CB_NONE - model.cb_exception = nothing + model.callback_exception = nothing model.forward_sensitivity_cache = nothing model.backward_sensitivity_cache = nothing - model.lazy_callback = nothing - model.user_cut_callback = nothing - model.heuristic_callback = nothing - - model.has_generic_callback = false - model.callback_data = nothing - # model.message_callback = nothing - for (name, value) in model.params MOI.set(model, name, value) end - return + + return nothing end function MOI.is_empty(model::Optimizer) !isempty(model.name) && return false model.objective_type != SCALAR_AFFINE && return false - model.is_objective_set == true && return false + model.is_objective_set === true && return false model.objective_sense !== nothing && return false !isempty(model.variable_info) && return false length(model.affine_constraint_info) != 0 && return false @@ -386,26 +370,27 @@ function MOI.is_empty(model::Optimizer) model.termination_status != MOI.OPTIMIZE_NOT_CALLED && return false model.primal_status != MOI.NO_SOLUTION && return false model.dual_status != MOI.NO_SOLUTION && return false - - model.callback_cached_solution !== nothing && return false - # model.cb_cut_data !== nothing && return false - model.callback_state != CB_NONE && return false - model.cb_exception !== nothing && return false - model.lazy_callback !== nothing && return false - model.user_cut_callback !== nothing && return false - model.heuristic_callback !== nothing && return false - - model.has_generic_callback && return false - model.callback_data !== nothing && return false + # TODO: Get this right. + # The message callback is automatically defined when the model is created. + # !isempty(model.callback_table) && return false + model.callback_cached_solution !== nothing && return false - # model.message_callback !== nothing && return false - # otherwise jump complains it is not empty + if model.callback_cut_data !== nothing + !isempty(model.callback_cut_data.cut_ptrs) && return false + model.callback_cut_data.submitted === true && return false + end + + !isempty(model.callback_state) && return false + model.callback_exception !== nothing && return false return true end -function reset_cached_solution(model::Optimizer) +include("callbacks/generic.jl") +include("callbacks/MOI_callbacks.jl") + +function reset_cached_solution!(model::Optimizer) num_variables = length(model.variable_info) num_affine = length(model.affine_constraint_info) if model.cached_solution === nothing @@ -432,34 +417,6 @@ function reset_cached_solution(model::Optimizer) return model.cached_solution end -function reset_callback_cached_solution(model::Optimizer) - num_variables = length(model.variable_info) - num_affine = length(model.affine_constraint_info) - if model.callback_cached_solution === nothing - model.callback_cached_solution = CachedSolution( - fill(NaN, num_variables), - fill(NaN, num_variables), - fill(NaN, num_affine), - fill(NaN, num_affine), - false, - false, - false, - NaN - ) - else - resize!(model.callback_cached_solution.variable_primal, num_variables) - resize!(model.callback_cached_solution.variable_dual, num_variables) - resize!(model.callback_cached_solution.linear_primal, num_affine) - resize!(model.callback_cached_solution.linear_dual, num_affine) - model.callback_cached_solution.has_primal_certificate = false - model.callback_cached_solution.has_dual_certificate = false - model.callback_cached_solution.has_feasible_point = false - model.callback_cached_solution.solve_time = NaN - end - return model.callback_cached_solution -end - - MOI.get(::Optimizer, ::MOI.SolverName) = "Xpress" # Currently this returns the version of the Xpress package as a whole @@ -568,7 +525,7 @@ function MOI.set(model::Optimizer, param::MOI.RawOptimizerAttribute, value) @checked Lib.XPRSsetlogfile(model.inner, value) end model.inner.logfile = value - reset_message_callback(model) + reset_message_callback!(model) elseif param == MOI.RawOptimizerAttribute("MOI_POST_SOLVE") model.post_solve = value elseif param == MOI.RawOptimizerAttribute("MOI_IGNORE_START") @@ -580,29 +537,17 @@ function MOI.set(model::Optimizer, param::MOI.RawOptimizerAttribute, value) model.solve_method = value elseif param == MOI.RawOptimizerAttribute("XPRESS_WARNING_WINDOWS") model.show_warning = value - reset_message_callback(model) + reset_message_callback!(model) elseif param == MOI.RawOptimizerAttribute("OUTPUTLOG") model.log_level = value Xpress.setcontrol!(model.inner, "OUTPUTLOG", value) - reset_message_callback(model) + reset_message_callback!(model) else Xpress.setcontrol!(model.inner, param.name, value) end return end -function reset_message_callback(model) - if model.message_callback !== nothing - # remove all message callbacks - @checked Lib.XPRSremovecbmessage(model.inner, C_NULL, C_NULL) - model.message_callback = nothing - end - if model.inner.logfile == "" && # no file -> screen - model.log_level != 0 # has log - model.message_callback = setoutputcb!(model.inner, model.show_warning) - end -end - function MOI.get(model::Optimizer, param::MOI.RawOptimizerAttribute) if param == MOI.RawOptimizerAttribute("logfile") return model.inner.logfile @@ -2627,31 +2572,18 @@ end ### Optimize methods. ### -function check_moi_callback_validity(model::Optimizer) - has_moi_callback = - model.lazy_callback !== nothing || - model.user_cut_callback !== nothing || - model.heuristic_callback !== nothing - if has_moi_callback && model.has_generic_callback - error( - "Cannot use Xpress.CallbackFunction as well as " * - "MOI.AbstractCallbackFunction" - ) - end - return has_moi_callback -end - # TODO alternatively do like Gurobi.jl and wrap all callbacks in a try/catch block function pre_solve_reset(model::Optimizer) model.basis_status = nothing - model.cb_exception = nothing - reset_cached_solution(model) + model.callback_exception = nothing + reset_cached_solution!(model) return end -function check_cb_exception(model::Optimizer) - if model.cb_exception !== nothing - e = model.cb_exception - model.cb_exception = nothing + +function check_callback_exception(model::Optimizer) + if model.callback_exception !== nothing + e = model.callback_exception + model.callback_exception = nothing throw(e) end return @@ -2721,30 +2653,29 @@ function _update_MIP_start!(model) end function MOI.optimize!(model::Optimizer) - # Initialize callbacks if necessary. - if check_moi_callback_validity(model) - if model.moi_warnings && Xpress.getcontrol(model.inner,Lib.XPRS_HEURSTRATEGY) != 0 - @warn "Callbacks in XPRESS might not work correctly with HEURSTRATEGY != 0" - end - MOI.set(model, CallbackFunction(), default_moi_callback(model)) - model.has_generic_callback = false # because it is set as true in the above - end pre_solve_reset(model) + # cache rhs: must be done before hand because it cant be # properly queried if the problem ends up in a presolve state rhs = Vector{Float64}(undef, n_constraints(model.inner)) + @checked Lib.XPRSgetrhs(model.inner, rhs, Cint(0), Cint(n_constraints(model.inner)-1)) + if !model.ignore_start && is_mip(model) _update_MIP_start!(model) end + start_time = time() + if is_mip(model) @checked Lib.XPRSmipoptimize(model.inner, model.solve_method) else @checked Lib.XPRSlpoptimize(model.inner, model.solve_method) end + model.cached_solution.solve_time = time() - start_time - check_cb_exception(model) + + check_callback_exception(model) # should be almost a no-op if not needed # might have minor overhead due to memory being freed @@ -2795,7 +2726,7 @@ function MOI.optimize!(model::Optimizer) end function _throw_if_optimize_in_progress(model, attr) - if model.callback_state != CB_NONE + if callback_state(model) != CS_NONE throw(MOI.OptimizeInProgress(attr)) end end @@ -4206,6 +4137,7 @@ function MOI.get( end return MOI.NOT_IN_CONFLICT end + function MOI.get( model::Optimizer, ::MOI.ConstraintConflictStatus, @@ -4226,6 +4158,7 @@ function MOI.get( end return MOI.NOT_IN_CONFLICT end + function MOI.supports( ::Optimizer, ::MOI.ConstraintConflictStatus, @@ -4245,18 +4178,9 @@ function MOI.supports( return true end -include("MOI_callbacks.jl") - -function extension(str::String) - try - match(r"\.[A-Za-z0-9]+$", str).match - catch - "" - end -end - function MOI.write_to_file(model::Optimizer, name::String) - ext = extension(name) + _, ext = splitext(name) + if ext == ".lp" @checked Lib.XPRSwriteprob(model.inner, name, "l") elseif ext == ".mps" diff --git a/src/MOI/callbacks/MOI_callbacks.jl b/src/MOI/callbacks/MOI_callbacks.jl new file mode 100644 index 00000000..779b272e --- /dev/null +++ b/src/MOI/callbacks/MOI_callbacks.jl @@ -0,0 +1,33 @@ +@doc raw""" + XpressCallback <: MOI.AbstractCallback +""" +abstract type XpressCallback <: MOI.AbstractCallback end + +MOI.supports(::Xpress.Optimizer, ::XpressCallback) = true + +include("MOI_message.jl") +include("MOI_optnode.jl") +include("MOI_preintsol.jl") + +include("MOI_heuristic.jl") +include("MOI_lazy_constraint.jl") +include("MOI_user_cut.jl") + +function moi_generic_wrapper(model::Optimizer, callback_data::CallbackData) + get_callback_solution!(model, callback_data.node_model) + + moi_heuristic_wrapper(model, callback_data) + moi_lazy_constraint_wrapper(model, callback_data) + moi_user_cut_wrapper(model, callback_data) + + return nothing +end + +function set_moi_generic_callback!(model::Optimizer) + remove_xprs_optnode_callback!(model) + + return add_xprs_optnode_callback!( + model, + (callback_data::CallbackData) -> moi_generic_wrapper(model, callback_data), + ) +end diff --git a/src/MOI/callbacks/MOI_heuristic.jl b/src/MOI/callbacks/MOI_heuristic.jl new file mode 100644 index 00000000..76eed146 --- /dev/null +++ b/src/MOI/callbacks/MOI_heuristic.jl @@ -0,0 +1,82 @@ +function moi_heuristic_xprs_optnode_wrapper(func, model::Optimizer, callback_data::CD) where {CD<:CallbackData} + push_callback_state!(model, CS_MOI_HEURISTIC) + + get_callback_solution!(model, callback_data.node_model) + + # Allow at most one heuristic solution per LP optimal node + if Xpress.getintattrib(callback_data.node_model, Xpress.Lib.XPRS_CALLBACKCOUNT_OPTNODE) <= 1 + func(callback_data) + end + + pop_callback_state!(model) + + return nothing +end + +function MOI.set(model::Optimizer, ::MOI.HeuristicCallback, ::Nothing) + if !isnothing(model.callback_table.moi_heuristic) + xprs_optnode_info, = model.callback_table.moi_heuristic + + remove_xprs_optnode_callback!(model.inner, xprs_optnode_info) + + model.callback_table.moi_heuristic = nothing + end + + return nothing +end + +function MOI.set(model::Optimizer, attr::MOI.HeuristicCallback, func::Function) + MOI.set(model, attr, nothing) + + xprs_optnode_info = add_xprs_optnode_callback!( + model.inner, + (callback_data::OptNodeCallbackData) -> moi_heuristic_xprs_optnode_wrapper( + func, + model, + callback_data, + ) + )::CallbackInfo{OptNodeCallbackData} + + model.callback_table.moi_heuristic = (xprs_optnode_info,) + + return nothing +end + +MOI.supports(::Optimizer, ::MOI.HeuristicCallback) = true +MOI.supports(::Optimizer, ::MOI.HeuristicSolution{CD}) where {CD<:CallbackData} = true + +function MOI.submit( + model::Optimizer, + submittable::MOI.HeuristicSolution{CD}, + variables::Vector{MOI.VariableIndex}, + values::Vector{T} +) where {T, CD <: CallbackData} + # It is assumed that every '<:CallbackData' has a 'node_model' field + node_model = submittable.callback_data.node_model::Xpress.XpressProblem + + check_callback_state(model, node_model, submittable, CS_MOI_HEURISTIC) + + # Specific submit tasks + ilength = length(variables) + solval = fill!(Vector{Cdouble}(undef, ilength), NaN) + colind = Vector{Cint}(undef, ilength) + + for (count, (var, value)) in enumerate(zip(variables, values)) + solval[count] = value + colind[count] = _info(model, var).column - 1 + end + + if ilength == MOI.get(model, MOI.NumberOfVariables()) + colind = C_NULL + end + + Lib.XPRSaddmipsol( + node_model, + Cint(ilength), + solval, + colind, + C_NULL, + ) + + return MOI.HEURISTIC_SOLUTION_UNKNOWN +end diff --git a/src/MOI/callbacks/MOI_lazy_constraint.jl b/src/MOI/callbacks/MOI_lazy_constraint.jl new file mode 100644 index 00000000..27a2b6b3 --- /dev/null +++ b/src/MOI/callbacks/MOI_lazy_constraint.jl @@ -0,0 +1,114 @@ +function moi_lazy_constraint_xprs_optnode_wrapper(func, model::Optimizer, callback_data::CD) where {CD<:CallbackData} + push_callback_state!(model, CS_MOI_LAZY_CONSTRAINT) + + get_callback_solution!(model, callback_data.node_model) + + # Add previous cuts if any to gurantee that the user is dealing with + # an optimal solution feasibile for existing cuts + if isempty(model.callback_cut_data.cut_ptrs) || !apply_cuts!(model, callback_data.node_model) + func(callback_data) + end + + pop_callback_state!(model) + + + return nothing +end + +function MOI.set(model::Optimizer, ::MOI.LazyConstraintCallback, ::Nothing) + if !isnothing(model.callback_table.moi_lazy_constraint) + xprs_optnode_info, = model.callback_table.moi_lazy_constraint + + remove_xprs_optnode_callback!(model, xprs_optnode_info) + + model.callback_table.moi_lazy_constraint = nothing + end + + return nothing +end + +function MOI.set(model::Optimizer, attr::MOI.LazyConstraintCallback, func::Function) + MOI.set(model, attr, nothing) + + xprs_optnode_info = add_xprs_optnode_callback!( + model.inner, + (callback_data::OptNodeCallbackData) -> moi_lazy_constraint_xprs_optnode_wrapper( + func, + model, + callback_data, + ) + )::CallbackInfo{OptNodeCallbackData} + + model.callback_table.moi_lazy_constraint = (xprs_optnode_info,) + + return nothing +end + +MOI.supports(::Optimizer, ::MOI.LazyConstraintCallback) = true +MOI.supports(::Optimizer, ::MOI.LazyConstraint{CD}) where {CD<:CallbackData} = true + +function MOI.submit( + model::Optimizer, + submittable::MOI.LazyConstraint{CD}, + f::F, + s::S +) where {CD<:CallbackData,T,F<:MOI.ScalarAffineFunction{T},S<:Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T}}} + # It is assumed that every '<:CallbackData' has a 'node_model' field + node_model = submittable.callback_data.node_model::Xpress.XpressProblem + + check_callback_state(model, node_model, submittable, CS_MOI_LAZY_CONSTRAINT) + + # Check if b is 0 in a'x + b <= c? + # f(x) = a'x + b + # S = {<= c} + if !iszero(f.constant) + cache_exception( + model, + MOI.ScalarFunctionConstantNotZero{T,F,S}(f.constant) + ) + Xpress.interrupt(node_model, Xpress.Lib.XPRS_STOP_USER) + + return nothing + end + + indices, coefficients = _indices_and_coefficients(model, f) + sense, rhs = _sense_and_rhs(s) + + mtype = Ref{Cint}(1) # Cut type + mstart = Cint[0, length(indices)] + mindex = Array{Xpress.Lib.XPRScut}(undef, 1) + ncuts = Cint(1) + nodupl = Cint(2) # Duplicates are excluded from the cut pool, ignoring cut type + sensetype = Ref{UInt8}(sense) # Julia assumes Cchar to be signed, but Xpress likes it unsigned + drhs = Ref{Cdouble}(rhs) + indices .-= 1 # Xpress follows C's 0-index convention + mcols = Cint.(indices) + interp = Cint(-1) # Load all cuts + + Xpress.Lib.XPRSstorecuts( + node_model, + ncuts, + nodupl, + mtype, + sensetype, + drhs, + mstart, + mindex, + mcols, + coefficients, + ) + + Xpress.Lib.XPRSloadcuts( + node_model, + mtype[], + interp, + ncuts, + mindex, + ) + + push!(model.callback_cut_data.cut_ptrs, mindex[]) + + model.callback_cut_data.submitted = true + + return nothing +end diff --git a/src/MOI/callbacks/MOI_message.jl b/src/MOI/callbacks/MOI_message.jl new file mode 100644 index 00000000..26761350 --- /dev/null +++ b/src/MOI/callbacks/MOI_message.jl @@ -0,0 +1,66 @@ +@doc raw""" + MessageCallback +""" +struct MessageCallback <: XpressCallback end + +function xprs_message_wrapper(func, model::Optimizer, callback_data::CD) where {CD<:CallbackData} + push_callback_state!(model, CS_XPRS_MESSAGE) + + func(callback_data) + + pop_callback_state!(model) + + return nothing +end + +function MOI.set(model::Optimizer, ::MessageCallback, ::Nothing) + xprs_message_info = model.callback_table.xprs_message + + if !isnothing(xprs_message_info) + remove_xprs_message_callback!(model.inner, xprs_message_info) + end + + model.callback_table.xprs_message = nothing + + return nothing +end + +function MOI.set(model::Optimizer, attr::MessageCallback, func::Function) + # remove any existing callback definitions + MOI.set(model, attr, nothing) + + model.callback_table.xprs_message = add_xprs_message_callback!( + model.inner, + (callback_data::MessageCallbackData) -> xprs_message_wrapper(func, model, callback_data) + )::CallbackInfo{MessageCallbackData} + + return nothing +end + +@doc raw""" + default_xprs_message_func(callback_data::MessageCallbackData) +""" +function default_xprs_message_func(callback_data::MessageCallbackData) + msg = callback_data.msg + msgtype = callback_data.msgtype + + if msgtype == 1 # Information + println(unsafe_string(msg)) + return zero(Cint) + elseif msgtype == 2 # Not used + return zero(Cint) + elseif msgtype == 3 # Warning + show_warning = callback_data.data::Bool + if show_warning + println(unsafe_string(msg)) + end + return zero(Cint) + elseif msgtype == 4 # Error + return zero(Cint) + else # Exiting - buffers need flushing + flush(stdout) + return zero(Cint) + end + + return nothing +end diff --git a/src/MOI/callbacks/MOI_optnode.jl b/src/MOI/callbacks/MOI_optnode.jl new file mode 100644 index 00000000..67122c54 --- /dev/null +++ b/src/MOI/callbacks/MOI_optnode.jl @@ -0,0 +1,43 @@ +@doc raw""" + OptNodeCallback +""" +struct OptNodeCallback <: XpressCallback end + +function xprs_optnode_wrapper(func, model::Xpress.Optimizer, callback_data::CD) where {CD<:CallbackData} + push_callback_state!(model, CS_XPRS_OPTNODE) + + get_callback_solution!(model, callback_data.node_model) + + # Allow at most one heuristic solution per LP optimal node + if Xpress.getintattrib(callback_data.node_model, Xpress.Lib.XPRS_CALLBACKCOUNT_OPTNODE) <= 1 + func(callback_data) + end + + pop_callback_state!(model) + + return nothing +end + +function MOI.set(model::Xpress.Optimizer, ::OptNodeCallback, ::Nothing) + xprs_optnode_info = model.callback_table.xprs_optnode + + if !isnothing(xprs_optnode_info) + remove_xprs_optnode_callback!(model.inner, xprs_optnode_info) + end + + model.callback_table.xprs_optnode = nothing + + return nothing +end + +function MOI.set(model::Optimizer, attr::OptNodeCallback, func::Function) + # remove any previous callback definitions + MOI.set(model, attr, nothing) + + model.callback_table.xprs_optnode = add_xprs_optnode_callback!( + model.inner, + (callback_data::OptNodeCallbackData) -> xprs_optnode_wrapper(func, model, callback_data), + )::CallbackInfo{OptNodeCallbackData} + + return nothing +end \ No newline at end of file diff --git a/src/MOI/callbacks/MOI_preintsol.jl b/src/MOI/callbacks/MOI_preintsol.jl new file mode 100644 index 00000000..681112a1 --- /dev/null +++ b/src/MOI/callbacks/MOI_preintsol.jl @@ -0,0 +1,43 @@ +@doc raw""" + PreIntSolCallback +""" +struct PreIntSolCallback <: XpressCallback end + +function xprs_preintsol_wrapper(func, model::Xpress.Optimizer, callback_data::PreIntSolCallbackData) + push_callback_state!(model, CS_XPRS_PREINTSOL) + + get_callback_solution!(model, callback_data.node_model) + + # Allow at most one heuristic solution per LP optimal node + if Xpress.getintattrib(callback_data.node_model, Xpress.Lib.XPRS_CALLBACKCOUNT_OPTNODE) <= 1 + func(callback_data) + end + + pop_callback_state!(model) + + return nothing +end + +function MOI.set(model::Optimizer, ::PreIntSolCallback, ::Nothing) + xprs_preintsol_info = model.callback_table.xprs_preintsol + + if !isnothing(xprs_preintsol_info) + remove_xprs_preintsol_callback!(model.inner, xprs_preintsol_info) + end + + model.callback_table.xprs_preintsol = nothing + + return nothing +end + +function MOI.set(model::Optimizer, attr::PreIntSolCallback, func::Function) + # remove any previous callback definitions + MOI.set(model, attr, nothing) + + model.callback_table.xprs_preintsol = add_xprs_preintsol_callback!( + model.inner, + (callback_data::PreIntSolCallbackData) -> xprs_preintsol_wrapper(func, model, callback_data) + )::CallbackInfo{PreIntSolCallbackData} + + return nothing +end \ No newline at end of file diff --git a/src/MOI/callbacks/MOI_user_cut.jl b/src/MOI/callbacks/MOI_user_cut.jl new file mode 100644 index 00000000..34cad5ed --- /dev/null +++ b/src/MOI/callbacks/MOI_user_cut.jl @@ -0,0 +1,118 @@ +function moi_user_cut_xprs_optnode_wrapper(func, model::Optimizer, callback_data::CD) where {CD<:CallbackData} + push_callback_state!(model, CS_MOI_USER_CUT) + + get_callback_solution!(model, callback_data.node_model) + + # Apply stored cuts if any + if isempty(model.callback_cut_data.cut_ptrs) || !apply_cuts!(model, callback_data.node_model) + # Allow only 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. + if Xpress.getintattrib(callback_data.node_model, Xpress.Lib.XPRS_CALLBACKCOUNT_OPTNODE) <= 2 + func(callback_data) + end + end + + pop_callback_state!(model) + + return nothing +end + +function MOI.set(model::Optimizer, ::MOI.UserCutCallback, ::Nothing) + if !isnothing(model.callback_table.moi_user_cut) + xprs_optnode_info, = model.callback_table.moi_user_cut + + remove_xprs_optnode_callback!(model.inner, xprs_optnode_info) + + model.callback_table.moi_user_cut = nothing + end + + return nothing +end + +function MOI.set(model::Optimizer, attr::MOI.UserCutCallback, func::Function) + MOI.set(model, attr, nothing) + + xprs_optnode_info = add_xprs_optnode_callback!( + model.inner, + (callback_data::OptNodeCallbackData) -> moi_user_cut_xprs_optnode_wrapper( + func, + model, + callback_data, + ) + )::CallbackInfo{OptNodeCallbackData} + + model.callback_table.moi_user_cut = (xprs_optnode_info,) + + return nothing +end + +MOI.supports(::Optimizer, ::MOI.UserCutCallback) = true +MOI.supports(::Optimizer, ::MOI.UserCut{CD}) where {CD<:CallbackData} = true + +function MOI.submit( + model::Optimizer, + submittable::MOI.UserCut{CD}, + f::F, + s::S, +) where {CD<:CallbackData,T,F<:MOI.ScalarAffineFunction{T},S<:Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T}}} + # It is assumed that every '<:CallbackData' has a 'node_model' field + node_model = submittable.callback_data.node_model::Xpress.XpressProblem + + check_callback_state(model, node_model, submittable, CS_MOI_USER_CUT) + + # Check if b is 0 in a'x + b <= c? + # f(x) = a'x + b + # S = {<= c} + if !iszero(f.constant) + cache_exception( + model, + MOI.ScalarFunctionConstantNotZero{T,F,S}(f.constant) + ) + Xpress.interrupt(node_model, Xpress.Lib.XPRS_STOP_USER) + + return nothing + end + + indices, coefficients = _indices_and_coefficients(model, f) + sense, rhs = _sense_and_rhs(s) + + mtype = Ref{Cint}(1) # Cut type + mstart = Cint[0, length(indices)] + mindex = Ref{Xpress.Lib.XPRScut}(C_NULL) + ncuts = Cint(1) + nodupl = Cint(2) # Duplicates are excluded from the cut pool, ignoring cut type + qrtype = Ref{UInt8}(sense) + drhs = Ref{Cdouble}(rhs) + indices .-= 1 # Xpress follows C's 0-index convention + mcols = Cint.(indices) + interp = Cint(-1) # Load all cuts + dmatval = Cdouble.(coefficients) + + Xpress.Lib.XPRSstorecuts( + node_model, + ncuts, + nodupl, + mtype, + qrtype, + drhs, + mstart, + mindex, + mcols, + dmatval, + ) + + Xpress.Lib.XPRSloadcuts( + node_model, + mtype[], + interp, + ncuts, + mindex, + ) + + push!(model.callback_cut_data.cut_ptrs, mindex[]) + + model.callback_cut_data.submitted = true + + return nothing +end diff --git a/src/MOI/callbacks/XPRS_callbacks.jl b/src/MOI/callbacks/XPRS_callbacks.jl new file mode 100644 index 00000000..9e8bce79 --- /dev/null +++ b/src/MOI/callbacks/XPRS_callbacks.jl @@ -0,0 +1,3 @@ +include("XPRS_message.jl") +include("XPRS_optnode.jl") +include("XPRS_preintsol.jl") \ No newline at end of file diff --git a/src/MOI/callbacks/XPRS_message.jl b/src/MOI/callbacks/XPRS_message.jl new file mode 100644 index 00000000..0cf6c78a --- /dev/null +++ b/src/MOI/callbacks/XPRS_message.jl @@ -0,0 +1,191 @@ +@doc raw""" + MessageCallbackData +""" +mutable struct MessageCallbackData <: CallbackData + # models + root_model::XpressProblem + node_model::Union{XpressProblem, Nothing} + + # data + data::Any + + # args + cbprob::Union{Xpress.Lib.XPRSprob, Nothing} + cbdata::Union{Ptr{Cvoid}, Nothing} + msg::Union{Ptr{Cchar}, Nothing} + msglen::Union{Cint, Nothing} + msgtype::Union{Cint, Nothing} + + function MessageCallbackData(root_model::XpressProblem, data::Any = nothing) + return new( + root_model, + nothing, # node_model + data, + nothing, # cbprob + nothing, # cbdata + nothing, # msg + nothing, # msglen + nothing, # msgtype + ) + end +end + +@doc raw""" + xprs_message(cbprob::Lib.XPRSprob, cbdata::Ptr{Cvoid}, msg::Ptr{Cchar}, len::Cint, msgtype::Cint) +""" +function xprs_message(cbprob::Lib.XPRSprob, cbdata::Ptr{Cvoid}, msg::Ptr{Cchar}, msglen::Cint, msgtype::Cint) + data_wrapper = unsafe_pointer_to_objref(cbdata)::CallbackDataWrapper{MessageCallbackData} + + # Update callback data + data_wrapper.data.node_model = XpressProblem(cbprob; finalize_env = false) + + # Update function args + data_wrapper.data.cbprob = cbprob + data_wrapper.data.msg = msg + data_wrapper.data.msglen = msglen + data_wrapper.data.msgtype = msgtype + + # Call user-defined function + data_wrapper.func(data_wrapper.data) + + return zero(Cint) +end + +@doc raw""" + add_xprs_message_callback!(model::XpressProblem, show_warning::Bool) + +From ... + +# `XPRSaddcbmessage` + +## Purpose + +Declares an output callback function, called every time a text line relating to the given XPRSprob is output by the Optimizer. +This callback function will be called in addition to any callbacks already added by `XPRSaddcbmessage`. +Note that Optimizer messages passed to the callback do not end with a newline character; the user callback is expected to append such required newline characters itself. + +## Synopsis + +```c +int XPRS_CC XPRSaddcbmessage( + XPRSprob prob, + void (XPRS_CC *message)(XPRSprob cbprob, void *cbdata, const char *msg, int msglen, int msgtype), + void *data, + int priority +); +``` + +## Arguments + +- `prob` The current problem. +- `message` The callback function which takes five arguments, cbprob, cbdata, msg, msglen and +- `msgtype`, and has no return value. Use a NULL value to cancel a callback function. +- `cbprob` The problem passed to the callback function. +- `cbdata` The user-defined data passed as data when setting up the callback with XPRSaddcbmessage. +- `msg` A null terminated character array (string) containing the message, which may simply be a new line. +- `msglen` The length of the message string, excluding the null terminator. +- `msgtype` Indicates the type of output message: + - `1` information messages; + - `2` (not used); + - `3` warning messages; + - `4` error messages. + A negative value indicates that the Optimizer is about to finish and the buffers should be flushed at this time if the output is being redirected to a file. +- `data` A user-defined data to be passed to the callback function. +- `priority` An integer that determines the order in which callbacks of this type will be invoked. The callback added with a higher priority will be called before a callback with a lower priority. Set to 0 if not required. + +## Related controls + +Integer `OUTPUTLOG` All messages are disabled if set to zero. + +## Example + +The following example simply sends all output to the screen (stdout): + +```c +XPRSaddcbmessage(prob,Message,NULL,0); +``` + +The callback function might resemble: + +```c +void XPRS_CC Message(XPRSprob cbprob, void* data, const char *msg, int msglen, int msgtype) +{ + switch(msgtype) { + case 4: /* error */ + case 3: /* warning */ + case 2: /* not used */ + case 1: /* information */ + printf("%s\n", msg); + break; + default: /* exiting - buffers need flushing */ + fflush(stdout); + break; + } +} +``` + +## Further information + 1. Screen output is automatically created by the Optimizer Console only. To produce output when using the Optimizer library, it is necessary to define this callback function and use it to print the messages to the screen (stdout). + 2. This function offers one method of handling the messages which describe any warnings and errors that may occur during execution. Other methods are to check the return values of functions and then get the error code using the ERRORCODE attribute, obtain the last error message directly using XPRSgetlasterror, or send messages direct to a log file using XPRSsetlogfile. + 3. Visual Basic users must use the alternative function XPRSaddcbmessageVB to define the callback; this is required because of the different way VB handles strings. +""" +function add_xprs_message_callback!(model::XpressProblem, func::Function, data::Any = nothing, priority::Integer = 0) + callback_ptr = @cfunction(xprs_message, Cint, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cchar}, Cint, Cint)) + data_wrapper = CallbackDataWrapper{MessageCallbackData}(model, func, data) + + Lib.XPRSaddcbmessage( + model.ptr, + callback_ptr, + data_wrapper, + Cint(priority) + ) + + return CallbackInfo{MessageCallbackData}(callback_ptr, data_wrapper) +end + +function add_xprs_message_callback!(model::XpressProblem, show_warning::Bool = true, priority::Integer = 0) + return add_xprs_message_callback!( + model, + default_xprs_message_func, + show_warning, + priority, + ) +end + +@doc raw""" + remove_xprs_message_callback!(model::XpressProblem) + +From ... + +# `XPRSremovecbmessage` + +## Purpose +Removes a message callback function previously added by XPRSaddcbmessage. +The specified callback function will no longer be called after it has been removed. + +## Synopsis +```c +int XPRS_CC XPRSremovecbmessage( + XPRSprob prob, + void (XPRS_CC *message)(XPRSprob prob, void* vContext, const char* msg, int len, int msgtype), + void* data +); +``` + +## Arguments +- `prob` The current problem. +- `message` The callback function to remove. If NULL then all message callback functions added with the given user-defined data value will be removed. +- `data` The data value that the callback was added with. If NULL, then the data value will not be checked and all message callbacks with the function pointer message will be removed. + +""" +function remove_xprs_message_callback!(model::XpressProblem) + Xpress.Lib.XPRSremovecbmessage(model, C_NULL, C_NULL) + + return nothing +end + +function remove_xprs_message_callback!(model::XpressProblem, info::CallbackInfo{CD}) where {CD <: CallbackData} + Xpress.Lib.XPRSremovecbmessage(model, info.callback_ptr, C_NULL) + + return nothing +end diff --git a/src/MOI/callbacks/XPRS_optnode.jl b/src/MOI/callbacks/XPRS_optnode.jl new file mode 100644 index 00000000..77cfc9c0 --- /dev/null +++ b/src/MOI/callbacks/XPRS_optnode.jl @@ -0,0 +1,188 @@ +@doc raw""" + OptNodeCallbackData +""" +mutable struct OptNodeCallbackData <: CallbackData + # models + root_model::XpressProblem + node_model::Union{XpressProblem, Nothing} + + # data + data::Any + + # args + cbprob::Union{Lib.XPRSprob, Nothing} + cbdata::Union{Ptr{Cvoid}, Nothing} + p_infeasible::Union{Ptr{Cint}, Nothing} + + function OptNodeCallbackData(root_model::XpressProblem, data::Any = nothing) + return new( + root_model, + nothing, # node_model + data, + nothing, # cbprob + nothing, # cbdata + nothing, # p_infeasible + ) + end +end + +@doc raw""" + xprs_optnode_func( + cbprob::Lib.XPRSprob, + cbdata::Ptr{Cvoid}, + p_infeasible::Ptr{Cint}, + ) +""" +function xprs_optnode_func(cbprob::Lib.XPRSprob, cbdata::Ptr{Cvoid}, p_infeasible::Ptr{Cint}=C_NULL) + data_wrapper = unsafe_pointer_to_objref(cbdata)::CallbackDataWrapper{OptNodeCallbackData} + + # Update callback data + data_wrapper.data.node_model = XpressProblem(cbprob; finalize_env = false) + + # Update function args + data_wrapper.data.cbprob = cbprob + data_wrapper.data.p_infeasible = p_infeasible + + # Call user-defined function + data_wrapper.func(data_wrapper.data) + + return zero(Cint) +end + +@doc raw""" + add_xprs_optnode_callback!( + model::XpressProblem, + callback::Function, + data::Any = nothing, + priority::Integer = 0, + ) + +# Example + +```julia +model = Model(Xpress.Optimizer) + +MOI.set( + model, + Xpress.OptNodeCallback(), + (callback_data::Xpress.OptNodeCallbackData) -> begin + # The user should be able to access the arguments provided to the callback. + # They also have the same type as in the C interface! + callback_data.cbprob # Xpress Problem C pointer + # callback_data.cbdata # This one is not available here, sorry. + callback_data.p_infeasible # int pointer + + # Special cases are: + callback_data.root_model # wrapped in a julia type + callback_data.node_model # wrapped in a julia type + callback_data.data # This is cbdata's reference, not a pointer anymore! + end +) +``` + +From the Xpress 40.01 Manual: + +# `XPRSaddcboptnode` + +## Purpose + +Declares an optimal node callback function, called during the branch and bound search, after the LP +relaxation has been solved for the current node, and after any internal cuts and heuristics have been +applied, but before the Optimizer checks if the current node should be branched. This callback function +will be called in addition to any callbacks already added by XPRSaddcboptnode. + +## Synopsis + +```c +int XPRS_CC XPRSaddcboptnode( + XPRSprob prob, + void (XPRS_CC *optnode)(XPRSprob cbprob, void *cbdata, int *p_infeasible), + void *data, + int priority +); +``` + +## Arguments +- `prob` The current problem. +- `optnode` The callback function which takes three arguments, `cbprob`, `cbdata` and `p_infeasible`, and has no return value. + - `cbprob` The problem passed to the callback function, `optnode`. + - `cbdata` The user-defined data passed as data when setting up the callback with `XPRSaddcboptnode`. + - `p_infeasible`` The feasibility status. If set to a nonzero value by the user, the current node will be declared infeasible. +- `data` A user-defined data to be passed to the callback function, `optnode`. +- `priority` An integer that determines the order in which multiple node-optimal callbacks will be invoked. The callback added with a higher priority will be called before a callback with a lower priority. Set to 0 if not required. + +## Example +The following prints the optimal objective value of the node LP relaxations: + +```c +XPRSaddcboptnode(prob,nodeOptimal,NULL,0); +XPRSmipoptimize(prob,""); +``` + +The callback function might resemble: + +```c +void XPRS_CC nodeOptimal(XPRSprob prob, void *data, int *p_infeasible) +{ + int node; + double objval; + XPRSgetintattrib(prob, XPRS_CURRENTNODE, &node); + printf("NodeOptimal: node number %d\n", node); + XPRSgetdblattrib(prob, XPRS_LPOBJVAL, &objval); + printf("\tObjective function value = %f\n", objval); +} +``` + +See the example depthfirst.c in the `examples/optimizer/c` folder. +""" +function add_xprs_optnode_callback!(model::XpressProblem, func::Function, data::Any=nothing, priority::Integer=0) + callback_ptr = @cfunction(xprs_optnode_func, Cint, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cint})) + data_wrapper = CallbackDataWrapper{OptNodeCallbackData}(model, func, data) + + Lib.XPRSaddcboptnode( + model.ptr, + callback_ptr, + data_wrapper, + Cint(priority), + ) + + return CallbackInfo{OptNodeCallbackData}(callback_ptr, data_wrapper) +end + +@doc raw""" + remove_xprs_optnode_callback!(model::XpressProblem) + +From the Xpress 40.01 Manual: + +# XPRSremovecboptnode + +## Purpose +Removes a node-optimal callback function previously added by XPRSaddcboptnode. The specified callback function will no longer be called after it has been removed. + +## Synopsis + +```c +int XPRS_CC XPRSremovecboptnode( + XPRSprob prob, + void (XPRS_CC *optnode)(XPRSprob my_prob, void *my_object, int *feas), + void *data +); +``` + +## Arguments + +- `prob` The current problem. +- `optnode` The callback function to remove. If NULL then all node-optimal callback functions added with the given user-defined data value will be removed. +- `data` The data value that the callback was added with. If NULL, then the data value will not be checked and all node-optimal callbacks with the function pointer optnode will be removed. +""" +function remove_xprs_optnode_callback!(model::XpressProblem) + Xpress.Lib.XPRSremovecboptnode(model, C_NULL, C_NULL) + + return nothing +end + +function remove_xprs_optnode_callback!(model::XpressProblem, info::CallbackInfo{CD}) where {CD <: CallbackData} + Xpress.Lib.XPRSremovecboptnode(model, info.callback_ptr, C_NULL) + + return nothing +end diff --git a/src/MOI/callbacks/XPRS_preintsol.jl b/src/MOI/callbacks/XPRS_preintsol.jl new file mode 100644 index 00000000..2c59c411 --- /dev/null +++ b/src/MOI/callbacks/XPRS_preintsol.jl @@ -0,0 +1,120 @@ +@doc raw""" + PreIntSolCallbackData +""" +mutable struct PreIntSolCallbackData <: CallbackData + root_model::XpressProblem + node_model::Union{XpressProblem,Nothing} + + data::Any + + cbprob::Union{Xpress.Lib.XPRSprob,Nothing} + cbdata::Union{Ptr{Cvoid},Nothing} + soltype::Union{Ptr{Cint},Nothing} + p_reject::Union{Ptr{Cint},Nothing} + p_cutoff::Union{Ptr{Cdouble},Nothing} + + function PreIntSolCallbackData(root_model::XpressProblem, data::Any = nothing) + return new( + root_model, + nothing, # node_model + data, + nothing, # cprob + nothing, # cbdata + nothing, # soltype + nothing, # p_reject + nothing, # p_cutoff + ) + end +end + +@doc raw""" + xprs_preintsol(cbprob::Lib.XPRSprob, cbdata::Ptr{Cvoid}, soltype::Ptr{Cint}, p_reject::Ptr{Cint}, p_cutoff::Ptr{Cdouble}) +""" +function xprs_preintsol(cbprob::Lib.XPRSprob, cbdata::Ptr{Cvoid}, soltype::Ptr{Cint}, p_reject::Ptr{Cint}, p_cutoff::Ptr{Cdouble}) + data_wrapper = unsafe_pointer_to_objref(cbdata)::CallbackDataWrapper{PreIntSolCallbackData} + + # Update callback data + data_wrapper.data.node_model = XpressProblem(cbprob; finalize_env = false) + + # Update function args + data_wrapper.data.cbprob = cbprob + data_wrapper.data.soltype = soltype + data_wrapper.data.p_reject = p_reject + data_wrapper.data.p_cutoff = p_cutoff + + # Call user-defined function + data_wrapper.func(data_wrapper.data) + + return zero(Cint) +end + +@doc raw""" + add_xprs_preintsol_callback!( + model::XpressProblem, + callback::Function, + data::Any=nothing, + priority::Integer = 0, + ) + +From the Xpress 40.01 Manual: + +# `XPRSaddcbpreintsol` + +## Purpose + +Declares a user integer solution callback function, called when an integer solution is found by heuristics +or during the branch and bound search, but before it is accepted by the Optimizer. This callback function +will be called in addition to any integer solution callbacks already added by XPRSaddcbpreintsol. + +## Synopsis + +```c +int XPRS_CC XPRSaddcbpreintsol( + XPRSprob prob, + void (XPRS_CC *preintsol)(XPRSprob cbprob, void *cbdata, int soltype, int *p_reject, double *p_cutoff), + void *data, + int priority, +); +``` + +## Arguments + +- `prob` The current problem. +- `preintsol` The callback function which takes five arguments, `cbprob`, `cbdata`, `soltype`, `p_reject` and `p_cutoff`, and has no return value. This function is called when an integer solution is found, but before the solution is accepted by the Optimizer, allowing the user to reject the solution. +- `cbprob` The problem passed to the callback function, `preintsol`. +- `cbdata` The user-defined data passed as data when setting up the callback with `XPRSaddcbpreintsol`. +- `soltype` The type of MIP solution that has been found: Set to 1 if the solution was found using a heuristic. Otherwise, it will be the global feasible solution to the current node of the global search. + - 0 The continuous relaxation solution to the current node of the global search, which has been found to be global feasible. + - 1 A MIP solution found by a heuristic. + - 2 A MIP solution provided by the user. + - 3 A solution resulting from refinement of primal or dual violations of a previous MIP solution. +- `p_reject` Set this to 1 if the solution should be rejected. +- `p_cutoff` The new cutoff value that the Optimizer will use if the solution is accepted. If the user changes `p_cutoff`, the new value will be used instead. The cutoff value will not be updated if the solution is rejected. +- `data` A user-defined data to be passed to the callback function, `preintsol`. +- `priority` An integer that determines the order in which callbacks of this type will be invoked. The callback added with a higher priority will be called before a callback with a lower priority. Set to 0 if not required. +""" +function add_xprs_preintsol_callback!(model::XpressProblem, func::Function, data::Any=nothing, priority::Integer=0) + callback_ptr = @cfunction(xprs_preintsol, Cint, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cint}, Ptr{Cint}, Ptr{Cdouble})) + data_wrapper = CallbackDataWrapper{PreIntSolCallbackData}(model, func, data) + + Lib.XPRSaddcbpreintsol( + model.ptr, + callback_ptr, + data_wrapper, + Cint(priority), + ) + + return CallbackInfo{PreIntSolCallbackData}(callback_ptr, data_wrapper) +end + +function remove_xprs_preintsol_callback!(model::XpressProblem) + Lib.XPRSremovecbpreintsol(model, C_NULL, C_NULL) + + return nothing +end + +function remove_xprs_preintsol_callback!(model::XpressProblem, info::CallbackInfo{CD}) where {CD <: CallbackData} + Lib.XPRSremovecbpreintsol(model, info.callback_ptr, C_NULL) + + return nothing +end diff --git a/src/MOI/callbacks/generic.jl b/src/MOI/callbacks/generic.jl new file mode 100644 index 00000000..e87c04b7 --- /dev/null +++ b/src/MOI/callbacks/generic.jl @@ -0,0 +1,181 @@ +function callback_state(model::Optimizer) + if isempty(model.callback_state) + return CS_NONE + else + return model.callback_state[end] + end +end + +function state_callback(state::CallbackState) + if state === CS_MOI_HEURISTIC + return MOI.HeuristicCallback() + elseif state === CS_MOI_LAZY_CONSTRAINT + return MOI.LazyConstraintCallback() + elseif state === CS_MOI_USER_CUT + return MOI.UserCutCallback() + elseif state === CS_XPRS_MESSAGE + return MessageCallback() + elseif state === CS_XPRS_OPTNODE + return OptNodeCallback() + elseif state === CS_XPRS_PREINTSOL + return PreIntSolCallback() + else + error("Unknown callback state '$state'") + end +end + +function push_callback_state!(model::Optimizer, state::CallbackState) + push!(model.callback_state, state) + + return nothing +end + +function pop_callback_state!(model::Optimizer) + if isempty(model.callback_state) + error("Can't pop from empty state stack") + else + pop!(model.callback_state) + end + + return nothing +end + +function check_callback_state(model::Optimizer, node_model::Xpress.XpressProblem, submittable::MOI.AbstractSubmittable, expected_state::CallbackState) + state = callback_state(model) + + if state !== expected_state + cache_callback_exception!( + model, + MOI.InvalidCallbackUsage( + state_callback(state), + submittable, + ) + ) + + Xpress.interrupt(node_model, Xpress.Lib.XPRS_STOP_USER) + end + + return nothing +end + +function cache_callback_exception!(model::Optimizer, e::Union{Exception,Nothing}) + model.callback_exception = e + + return nothing +end + +function reset_callback_cached_solution!(model::Optimizer) + num_variables = length(model.variable_info) + num_affine = length(model.affine_constraint_info) + + if model.callback_cached_solution === nothing + model.callback_cached_solution = CachedSolution( + fill(NaN, num_variables), + fill(NaN, num_variables), + fill(NaN, num_affine), + fill(NaN, num_affine), + false, + false, + false, + NaN + ) + else + resize!(model.callback_cached_solution.variable_primal, num_variables) + resize!(model.callback_cached_solution.variable_dual, num_variables) + resize!(model.callback_cached_solution.linear_primal, num_affine) + resize!(model.callback_cached_solution.linear_dual, num_affine) + + model.callback_cached_solution.has_primal_certificate = false + model.callback_cached_solution.has_dual_certificate = false + model.callback_cached_solution.has_feasible_point = false + model.callback_cached_solution.solve_time = NaN + end + + return model.callback_cached_solution +end + +function reset_message_callback!(model::Optimizer) + if isempty(model.inner.logfile) && !iszero(model.log_level) + MOI.set( + model, + MessageCallback(), + (cb_data) -> begin + cb_data.data = model.show_warning + + default_xprs_message_func(cb_data) + + return nothing + end + ) + else + MOI.set(model, MessageCallback(), nothing) + end + + return nothing +end + + +function get_callback_solution!(model::Optimizer, node_model::XpressProblem) + reset_callback_cached_solution!(model) + + Xpress.Lib.XPRSgetlpsol( + node_model, + model.callback_cached_solution.variable_primal, + model.callback_cached_solution.linear_primal, + model.callback_cached_solution.linear_dual, + model.callback_cached_solution.variable_dual, + ) + + return +end + +function apply_cuts!(opt::Optimizer, model::XpressProblem) + itype = Cint(1) + interp = Cint(-1) # Get all cuts + delta = Ref{Cdouble}(0.0) # Xpress.Lib.XPRS_MINUSINFINITY + ncuts = Ref{Cint}(0) + size = Cint(length(opt.callback_cut_data.cut_ptrs)) + mcutptr = Array{Xpress.Lib.XPRScut}(undef, size) + dviol = Array{Cdouble}(undef, size) + + Xpress.Lib.XPRSgetcpcutlist( # requires an available solution + model, + itype, + interp, + delta[], + ncuts, + size, + mcutptr, + dviol, + ) + + Xpress.Lib.XPRSloadcuts( + model, + itype, + interp, + ncuts[], + mcutptr + ) + + return (ncuts[] > 0) +end + +function MOI.get(model::Optimizer, attr::MOI.CallbackNodeStatus{CD}) where {CD<:CallbackData} + mip_infeas = Xpress.getintattrib(attr.callback_data.node_model, Xpress.Lib.XPRS_MIPINFEAS) + + if mip_infeas == 0 + return MOI.CALLBACK_NODE_STATUS_INTEGER + elseif mip_infeas > 0 + return MOI.CALLBACK_NODE_STATUS_FRACTIONAL + else + return MOI.CALLBACK_NODE_STATUS_UNKNOWN + end +end + +function MOI.get( + model::Optimizer, + ::MOI.CallbackVariablePrimal{CD}, + x::MOI.VariableIndex +) where {CD<:CallbackData} + return model.callback_cached_solution.variable_primal[_info(model, x).column] +end diff --git a/src/MOI/callbacks/interface.jl b/src/MOI/callbacks/interface.jl new file mode 100644 index 00000000..4d89bce0 --- /dev/null +++ b/src/MOI/callbacks/interface.jl @@ -0,0 +1,142 @@ +# States +@enum( + CallbackState, + CS_NONE, + # MOI Callbacks + CS_MOI_LAZY_CONSTRAINT, + CS_MOI_USER_CUT, + CS_MOI_HEURISTIC, + # Xpress Callbacks + CS_XPRS_OPTNODE, + CS_XPRS_PREINTSOL, + CS_XPRS_MESSAGE, +) + +function callback_state end + +function state_callback end + +@doc raw""" + CallbackData + +!!! warn + All subtypes from [`CallbackData`](@ref) must be mutable. +""" +abstract type CallbackData end + +@doc raw""" + GenericCallbackData <: CallbackData +""" +mutable struct GenericCallbackData <: CallbackData + root_model::XpressProblem + node_model::Union{XpressProblem,Nothing} + data::Any + + function GenericCallbackData(root_model::XpressProblem, data::Any=nothing) + return new(root_model, nothing, data) + end +end + +@doc raw""" + CallbackDataWrapper{CD} + +This struct is nothing but a way to inject the julia function provided by the user into the Xpress callback. +""" +mutable struct CallbackDataWrapper{CD<:CallbackData} + func::Function + data::CD + + function CallbackDataWrapper{CD}(root_model::XpressProblem, func::Function, data::Any) where {CD} + return new(func, CD(root_model, data)) + end +end + +function CallbackDataWrapper(root_model::XpressProblem, func::Function, data::Any=nothing) + return CallbackDataWrapper{GenericCallbackData}(root_model, func, data) +end + +Base.cconvert(::Type{Ptr{Cvoid}}, x::CallbackDataWrapper) = x + +function Base.unsafe_convert(::Type{Ptr{Cvoid}}, x::CallbackDataWrapper) + return pointer_from_objref(x)::Ptr{Cvoid} +end + +@doc raw""" + CallbackInfo +""" +struct CallbackInfo{CD<:CallbackData} + callback_ptr::Union{Ptr{Cvoid},Nothing} + data_wrapper::CallbackDataWrapper{CD} +end + +@doc raw""" + CallbackCutData +""" +mutable struct CallbackCutData + submitted::Bool + cut_ptrs::Vector{Xpress.Lib.XPRScut} + + function CallbackCutData( + submitted::Bool=false, + cut_ptrs::Vector{Xpress.Lib.XPRScut}=Vector{Xpress.Lib.XPRScut}(undef, 0) + ) + return new(submitted, cut_ptrs) + end +end + +function Base.empty!(cut_data::CallbackCutData) + cut_data.submitted = false + empty!(cut_data.cut_ptrs) + + return cut_data +end + +function Base.isempty(cut_data::CallbackCutData) + return !cut_data.submitted && isempty(cut_data.cut_ptrs) +end + +include("XPRS_callbacks.jl") + +@doc raw""" + CallbackTable + +This structure is designed to store information about high-level callbacks, +that is, the one that are exposed to the user. +""" +mutable struct CallbackTable + # MathOptInteface + moi_heuristic::Union{Tuple{CallbackInfo{OptNodeCallbackData}},Nothing} + moi_lazy_constraint::Union{Tuple{CallbackInfo{OptNodeCallbackData}},Nothing} + moi_user_cut::Union{Tuple{CallbackInfo{OptNodeCallbackData}},Nothing} + # Xpress + xprs_message::Union{CallbackInfo{MessageCallbackData},Nothing} + xprs_optnode::Union{CallbackInfo{OptNodeCallbackData},Nothing} + xprs_preintsol::Union{CallbackInfo{PreIntSolCallbackData},Nothing} + + function CallbackTable() + return new( + nothing, nothing, nothing, + nothing, nothing, nothing, + ) + end +end + +function Base.empty!(table::CallbackTable) + table.moi_heuristic = nothing + table.moi_lazy_constraint = nothing + table.moi_user_cut = nothing + table.xprs_message = nothing + table.xprs_optnode = nothing + table.xprs_preintsol = nothing + + return table +end + +function Base.isempty(table::CallbackTable) + return isnothing(table.moi_heuristic) && + isnothing(table.moi_lazy_constraint) && + isnothing(table.moi_user_cut) && + isnothing(table.xprs_message) && + isnothing(table.xprs_optnode) && + isnothing(table.xprs_preintsol) +end \ No newline at end of file diff --git a/src/Xpress.jl b/src/Xpress.jl index ff1958e3..2ddca01b 100644 --- a/src/Xpress.jl +++ b/src/Xpress.jl @@ -36,7 +36,6 @@ module Xpress include("helper.jl") include("attributes_controls.jl") include("api.jl") - include("xprs_callbacks.jl") include("license.jl") const XPRS_ATTRIBUTES = Dict{String, Any}( diff --git a/src/xprs_callbacks.jl b/src/xprs_callbacks.jl deleted file mode 100644 index f7dfd01e..00000000 --- a/src/xprs_callbacks.jl +++ /dev/null @@ -1,93 +0,0 @@ - -mutable struct CallbackData - model_root::XpressProblem # should not use operations here - data::Any # data for user - model::XpressProblem # local model # ptr_model::Ptr{Nothing} -end -Base.broadcastable(x::CallbackData) = Ref(x) - -# must be mutable -mutable struct _CallbackUserData - callback::Function - model::XpressProblem - data::Any -end -Base.cconvert(::Type{Ptr{Cvoid}}, x::_CallbackUserData) = x -function Base.unsafe_convert(::Type{Ptr{Cvoid}}, x::_CallbackUserData) - return pointer_from_objref(x)::Ptr{Cvoid} -end - -export CallbackData - -function setcboptnode_wrapper(ptr_model::Lib.XPRSprob, my_object::Ptr{Cvoid}, feas::Ptr{Cint}) - usrdata = unsafe_pointer_to_objref(my_object)::_CallbackUserData - callback, model, data = usrdata.callback, usrdata.model, usrdata.data - model_inner = XpressProblem(ptr_model; finalize_env = false) - callback(CallbackData(model, data, model_inner)) - return zero(Cint) -end - -function set_callback_optnode!(model::XpressProblem, callback::Function, data::Any = nothing) - callback_ptr = @cfunction(setcboptnode_wrapper, Cint, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cint})) - usrdata = _CallbackUserData(callback, model, data) - Lib.XPRSaddcboptnode(model.ptr, callback_ptr, usrdata, 0) - # we need to keep a reference to the callback function - # so that it isn't garbage collected - return callback_ptr, usrdata -end - -function setcbpreintsol_wrapper(ptr_model::Lib.XPRSprob, my_object::Ptr{Cvoid}, soltype::Ptr{Cint}, ifreject::Ptr{Cint}, cutoff::Ptr{Cdouble}) - usrdata = unsafe_pointer_to_objref(my_object)::_CallbackUserData - callback, model, data = usrdata.callback, usrdata.model, usrdata.data - model_inner = XpressProblem(ptr_model; finalize_env = false) - callback(CallbackData(model, data, model_inner)) - return zero(Cint) -end - -function set_callback_preintsol!(model::XpressProblem, callback::Function, data::Any = nothing) - callback_ptr = @cfunction(setcbpreintsol_wrapper, Cint, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cint}, Ptr{Cint}, Ptr{Cdouble})) - usrdata = _CallbackUserData(callback, model, data) - Lib.XPRSaddcbpreintsol(model.ptr, callback_ptr, usrdata,0) - # we need to keep a reference to the callback function - # so that it isn't garbage collected - return callback_ptr, usrdata -end - -function addcboutput2screen_wrapper(ptr_model::Lib.XPRSprob, my_object::Ptr{Cvoid}, msg::Ptr{Cchar}, len::Cint, msgtype::Cint) - - usrdata = unsafe_pointer_to_objref(my_object)::_CallbackUserData - show_warning = usrdata.data::Bool - - if msgtype == 4 - #= Error =# - return zero(Cint) - elseif msgtype == 3 && show_warning - #= Warning =# - msg_str = unsafe_string(msg) - println(msg_str) - return zero(Cint) - elseif msgtype == 2 - #= Not used =# - return zero(Cint) - elseif msgtype == 1 - #= Information =# - msg_str = unsafe_string(msg) - println(msg_str) - return zero(Cint) - else - #= Exiting - buffers need flushing =# - flush(stdout) - return zero(Cint) - end -end - -function setoutputcb!(model::XpressProblem, show_warning::Bool) - callback_ptr = @cfunction(addcboutput2screen_wrapper, Cint, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cchar}, Cint, Cint)) - usrdata = _CallbackUserData(_null, model, show_warning) - Lib.XPRSaddcbmessage(model.ptr, callback_ptr, usrdata, 0) - return callback_ptr, usrdata -end - -function _null() - return -end \ No newline at end of file diff --git a/test/MathOptInterface/MOI_callbacks.jl b/test/MathOptInterface/MOI_callbacks.jl index a64b6919..6cf72b69 100644 --- a/test/MathOptInterface/MOI_callbacks.jl +++ b/test/MathOptInterface/MOI_callbacks.jl @@ -12,14 +12,17 @@ function callback_simple_model() OUTPUTLOG = 0, ) - MOI.Utilities.loadfromstring!(model, """ - variables: x, y - maxobjective: y - c1: x in Integer() - c2: y in Integer() - c3: x in Interval(0.0, 2.5) - c4: y in Interval(0.0, 2.5) - """) + MOI.Utilities.loadfromstring!( + model, + """ + variables: x, y + maxobjective: y + c1: x in Integer() + c2: y in Integer() + c3: x in Interval(0.0, 2.5) + c4: y in Interval(0.0, 2.5) +""" + ) x = MOI.get(model, MOI.VariableIndex, "x") y = MOI.get(model, MOI.VariableIndex, "y") return model, x, y @@ -37,151 +40,220 @@ function callback_knapsack_model() MIPTHREADS = 1, THREADS = 1, ) + MOI.set(model, MOI.NumberOfThreads(), 2) N = 30 x = MOI.add_variables(model, N) MOI.add_constraints(model, x, MOI.ZeroOne()) MOI.set.(model, MOI.VariablePrimalStart(), x, 0.0) + Random.seed!(1) + item_weights, item_values = rand(N), rand(N) + MOI.add_constraint( model, MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(item_weights, x), 0.0), MOI.LessThan(10.0) ) + MOI.set( model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(item_values, x), 0.0) ) + MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) - return model, x, item_weights + + return (model, x, item_weights) end +@testset "MOI Callbacks" verbose = true begin + @testset "LazyConstraintCallback" begin @testset "LazyConstraint" begin - model, x, y = callback_simple_model() - global lazy_called = false - MOI.set(model, MOI.LazyConstraintCallback(), cb_data -> begin - global lazy_called = true - x_val = MOI.get(model, MOI.CallbackVariablePrimal(cb_data), x) - y_val = MOI.get(model, MOI.CallbackVariablePrimal(cb_data), y) - status = MOI.get(model, MOI.CallbackNodeStatus(cb_data))::MOI.CallbackNodeStatusCode - if round.(Int, [x_val, y_val]) ≈ [x_val, y_val] atol=1e-6 - @test status == MOI.CALLBACK_NODE_STATUS_INTEGER - else - @test status == MOI.CALLBACK_NODE_STATUS_FRACTIONAL - end - @test MOI.supports(model, MOI.LazyConstraint(cb_data)) - if y_val - x_val > 1 + 1e-6 - MOI.submit( - model, - MOI.LazyConstraint(cb_data), - MOI.ScalarAffineFunction{Float64}( - MOI.ScalarAffineTerm.([-1.0, 1.0], [x, y]), - 0.0 - ), - MOI.LessThan{Float64}(1.0) - ) - elseif y_val + x_val > 3 + 1e-6 - MOI.submit( - model, - MOI.LazyConstraint(cb_data), - MOI.ScalarAffineFunction{Float64}( - MOI.ScalarAffineTerm.([1.0, 1.0], [x, y]), - 0.0 - ), MOI.LessThan{Float64}(3.0) - ) - end - end) - @test MOI.supports(model, MOI.LazyConstraintCallback()) - MOI.optimize!(model) - @test lazy_called - @test MOI.get(model, MOI.VariablePrimal(), x) == 1 - @test MOI.get(model, MOI.VariablePrimal(), y) == 2 + let + model, x, y = callback_simple_model() + + lazy_called = false + + MOI.set( + model, + MOI.LazyConstraintCallback(), + (cb_data) -> begin + lazy_called = true + + x_val = MOI.get(model, MOI.CallbackVariablePrimal(cb_data), x) + y_val = MOI.get(model, MOI.CallbackVariablePrimal(cb_data), y) + + status = MOI.get(model, MOI.CallbackNodeStatus(cb_data))::MOI.CallbackNodeStatusCode + + if round.(Int, [x_val, y_val]) ≈ [x_val, y_val] + atol = 1e-6 + @test status == MOI.CALLBACK_NODE_STATUS_INTEGER + else + @test status == MOI.CALLBACK_NODE_STATUS_FRACTIONAL + end + + @test MOI.supports(model, MOI.LazyConstraint(cb_data)) + + if y_val - x_val > 1 + 1e-6 + MOI.submit( + model, + MOI.LazyConstraint(cb_data), + MOI.ScalarAffineFunction{Float64}( + MOI.ScalarAffineTerm.([-1.0, 1.0], [x, y]), + 0.0 + ), + MOI.LessThan{Float64}(1.0) + ) + elseif y_val + x_val > 3 + 1e-6 + MOI.submit( + model, + MOI.LazyConstraint(cb_data), + MOI.ScalarAffineFunction{Float64}( + MOI.ScalarAffineTerm.([1.0, 1.0], [x, y]), + 0.0 + ), MOI.LessThan{Float64}(3.0) + ) + end + end + ) + + @test MOI.supports(model, MOI.LazyConstraintCallback()) + + MOI.optimize!(model) + + + @test lazy_called + + @test MOI.get(model, MOI.VariablePrimal(), x) == 1 + @test MOI.get(model, MOI.VariablePrimal(), y) == 2 + end end + @testset "OptimizeInProgress" begin - model, x, y = callback_simple_model() - MOI.set(model, MOI.LazyConstraintCallback(), cb_data -> begin - @test_throws( - MOI.OptimizeInProgress(MOI.VariablePrimal()), - MOI.get(model, MOI.VariablePrimal(), x) - ) - @test_throws( - MOI.OptimizeInProgress(MOI.ObjectiveValue()), - MOI.get(model, MOI.ObjectiveValue()) - ) - @test_throws( - MOI.OptimizeInProgress(MOI.ObjectiveBound()), - MOI.get(model, MOI.ObjectiveBound()) + let + model, x, y = callback_simple_model() + + MOI.set( + model, + MOI.LazyConstraintCallback(), + (cb_data) -> begin + @test_throws( + MOI.OptimizeInProgress(MOI.VariablePrimal()), + MOI.get(model, MOI.VariablePrimal(), x) + ) + @test_throws( + MOI.OptimizeInProgress(MOI.ObjectiveValue()), + MOI.get(model, MOI.ObjectiveValue()) + ) + @test_throws( + MOI.OptimizeInProgress(MOI.ObjectiveBound()), + MOI.get(model, MOI.ObjectiveBound()) + ) + end ) - end) - MOI.optimize!(model) + + MOI.optimize!(model) + end end @testset "HeuristicSolution" begin - model, x, y = callback_simple_model() - cb = nothing - MOI.set(model, MOI.LazyConstraintCallback(), cb_data -> begin - cb = cb_data - MOI.submit( + let + model, x, y = callback_simple_model() + + cb = nothing + + MOI.set( model, - MOI.HeuristicSolution(cb_data), - [x], - [2.0] - ) - end) - @test_throws( - MOI.InvalidCallbackUsage( MOI.LazyConstraintCallback(), - MOI.HeuristicSolution(cb) - ), - MOI.optimize!(model) - ) - end + (cb_data) -> begin + cb = cb_data + + MOI.submit( + model, + MOI.HeuristicSolution(cb_data), + [x], + [2.0] + ) + end + ) + + @test_throws( + MOI.InvalidCallbackUsage( + MOI.LazyConstraintCallback(), + MOI.HeuristicSolution(cb) + ), + MOI.optimize!(model) + ) + end + end end @testset "UserCutCallback" begin @testset "UserCut" begin - model, x, item_weights = callback_knapsack_model() - global user_cut_submitted = false - MOI.set(model, MOI.UserCutCallback(), cb_data -> begin - terms = MOI.ScalarAffineTerm{Float64}[] - accumulated = 0.0 - for (i, xi) in enumerate(x) - if MOI.get(model, MOI.CallbackVariablePrimal(cb_data), xi) > 0.0 - push!(terms, MOI.ScalarAffineTerm(1.0, xi)) - accumulated += item_weights[i] + let + model, x, item_weights = callback_knapsack_model() + + user_cut_submitted = false + + MOI.set( + model, + MOI.UserCutCallback(), + (cb_data) -> begin + terms = MOI.ScalarAffineTerm{Float64}[] + accumulated = 0.0 + + for (i, xi) in enumerate(x) + if MOI.get(model, MOI.CallbackVariablePrimal(cb_data), xi) > 0.0 + push!(terms, MOI.ScalarAffineTerm(1.0, xi)) + accumulated += item_weights[i] + end + end + + @test MOI.supports(model, MOI.UserCut(cb_data)) + + if accumulated > 10.0 + MOI.submit( + model, + MOI.UserCut(cb_data), + MOI.ScalarAffineFunction{Float64}(terms, 0.0), + MOI.LessThan{Float64}(length(terms) - 1) + ) + user_cut_submitted = true + end end - end - @test MOI.supports(model, MOI.UserCut(cb_data)) - if accumulated > 10.0 - MOI.submit( - model, - MOI.UserCut(cb_data), - MOI.ScalarAffineFunction{Float64}(terms, 0.0), - MOI.LessThan{Float64}(length(terms) - 1) - ) - global user_cut_submitted = true - end - end) - @test MOI.supports(model, MOI.UserCutCallback()) - MOI.optimize!(model) - @test user_cut_submitted + ) + + @test MOI.supports(model, MOI.UserCutCallback()) + + MOI.optimize!(model) + + @test user_cut_submitted + end end + @testset "HeuristicSolution" begin model, x, item_weights = callback_knapsack_model() + cb = nothing - MOI.set(model, MOI.UserCutCallback(), cb_data -> begin - cb = cb_data - MOI.submit( - model, - MOI.HeuristicSolution(cb_data), - [x[1]], - [0.0] - ) - end) + + MOI.set( + model, + MOI.UserCutCallback(), + (cb_data) -> begin + cb = cb_data + MOI.submit( + model, + MOI.HeuristicSolution(cb_data), + [x[1]], + [0.0] + ) + end + ) + @test_throws( MOI.InvalidCallbackUsage( MOI.UserCutCallback(), @@ -194,189 +266,179 @@ end @testset "HeuristicCallback" begin @testset "HeuristicSolution" begin - model, x, item_weights = callback_knapsack_model() - global callback_called = false - MOI.set(model, MOI.HeuristicCallback(), cb_data -> begin - x_vals = MOI.get.(model, MOI.CallbackVariablePrimal(cb_data), x) - status = MOI.get(model, MOI.CallbackNodeStatus(cb_data))::MOI.CallbackNodeStatusCode - if round.(Int, x_vals) ≈ x_vals atol=1e-6 - @test status == MOI.CALLBACK_NODE_STATUS_INTEGER - else - @test status == MOI.CALLBACK_NODE_STATUS_FRACTIONAL - end - @test MOI.supports(model, MOI.HeuristicSolution(cb_data)) - @test MOI.submit( - model, - MOI.HeuristicSolution(cb_data), - x, - floor.(x_vals) - ) == MOI.HEURISTIC_SOLUTION_UNKNOWN - global callback_called = true - end) - @test MOI.supports(model, MOI.HeuristicCallback()) - MOI.optimize!(model) - @test callback_called - end - @testset "LazyConstraint" begin - model, x, item_weights = callback_knapsack_model() - cb = nothing - MOI.set(model, MOI.HeuristicCallback(), cb_data -> begin - cb = cb_data - MOI.submit( + let + model, x, item_weights = callback_knapsack_model() + + callback_called = false + + MOI.set( model, - MOI.LazyConstraint(cb_data), - MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(1.0, x), 0.0), - MOI.LessThan(5.0) - ) - end) - @test_throws( - MOI.InvalidCallbackUsage( MOI.HeuristicCallback(), - MOI.LazyConstraint(cb) - ), + (cb_data) -> begin + x_vals = MOI.get.(model, MOI.CallbackVariablePrimal(cb_data), x) + status = MOI.get(model, MOI.CallbackNodeStatus(cb_data))::MOI.CallbackNodeStatusCode + + if round.(Int, x_vals) ≈ x_vals + atol = 1e-6 + @test status == MOI.CALLBACK_NODE_STATUS_INTEGER + else + @test status == MOI.CALLBACK_NODE_STATUS_FRACTIONAL + end + + @test MOI.supports(model, MOI.HeuristicSolution(cb_data)) + @test MOI.submit( + model, + MOI.HeuristicSolution(cb_data), + x, + floor.(x_vals) + ) == MOI.HEURISTIC_SOLUTION_UNKNOWN + + callback_called = true + end, + ) + + @test MOI.supports(model, MOI.HeuristicCallback()) + MOI.optimize!(model) - ) + + @test callback_called + end end - @testset "UserCut" begin - model, x, item_weights = callback_knapsack_model() - cb = nothing - MOI.set(model, MOI.HeuristicCallback(), cb_data -> begin - cb = cb_data - MOI.submit( + + @testset "LazyConstraint" begin + let + model, x, item_weights = callback_knapsack_model() + + cb = nothing + + MOI.set( model, - MOI.UserCut(cb_data), - MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(1.0, x), 0.0), - MOI.LessThan(5.0) - ) - end) - @test_throws( - MOI.InvalidCallbackUsage( MOI.HeuristicCallback(), - MOI.UserCut(cb) - ), - MOI.optimize!(model) - ) - end -end + (cb_data) -> begin + cb = cb_data -@testset "Xpress.CallbackFunction" begin - @testset "OptimizeInProgress" begin - model, x, y = callback_simple_model() - MOI.set(model, Xpress.CallbackFunction(), (cb_data) -> begin - @test_throws( - MOI.OptimizeInProgress(MOI.VariablePrimal()), - MOI.get(model, MOI.VariablePrimal(), x) - ) - @test_throws( - MOI.OptimizeInProgress(MOI.ObjectiveValue()), - MOI.get(model, MOI.ObjectiveValue()) + MOI.submit( + model, + MOI.LazyConstraint(cb_data), + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(1.0, x), 0.0), + MOI.LessThan(5.0) + ) + end, ) + @test_throws( - MOI.OptimizeInProgress(MOI.ObjectiveBound()), - MOI.get(model, MOI.ObjectiveBound()) + MOI.InvalidCallbackUsage( + MOI.HeuristicCallback(), + MOI.LazyConstraint(cb) + ), + MOI.optimize!(model) ) - end) - @test MOI.supports(model, Xpress.CallbackFunction()) - MOI.optimize!(model) - end - @testset "LazyConstraint" begin - model, x, y = callback_simple_model() - cb_calls = Int32[] - global generic_lazy_called = false - function callback_function(cb_data) - push!(cb_calls, 1) - 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), - MOI.ScalarAffineFunction{Float64}( - MOI.ScalarAffineTerm.([-1.0, 1.0], [x, y]), - 0.0 - ), - MOI.LessThan{Float64}(1.0) - ) - elseif y_val + x_val > 3 + 1e-6 - MOI.submit(model, MOI.LazyConstraint(cb_data), - MOI.ScalarAffineFunction{Float64}( - MOI.ScalarAffineTerm.([1.0, 1.0], [x, y]), - 0.0 - ), - 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 - @test length(cb_calls) > 0 end + @testset "UserCut" begin - model, x, item_weights = callback_knapsack_model() - user_cut_submitted = false - cb_calls = Int32[] - MOI.set(model, Xpress.CallbackFunction(), (cb_data) -> begin - push!(cb_calls) - - if Xpress.get_control_or_attribute(cb_data.model, Xpress.Lib.XPRS_CALLBACKCOUNT_OPTNODE) > 1 - return - end - Xpress.get_cb_solution(model, cb_data.model) - terms = MOI.ScalarAffineTerm{Float64}[] - accumulated = 0.0 - for (i, xi) in enumerate(x) - if MOI.get(model, MOI.CallbackVariablePrimal(cb_data), xi) > 0.0 - push!(terms, MOI.ScalarAffineTerm(1.0, xi)) - accumulated += item_weights[i] - end - end - if accumulated > 10.0 + let + model, x, item_weights = callback_knapsack_model() + + cb = nothing + + MOI.set(model, MOI.HeuristicCallback(), cb_data -> begin + cb = cb_data MOI.submit( model, MOI.UserCut(cb_data), - MOI.ScalarAffineFunction{Float64}(terms, 0.0), - MOI.LessThan{Float64}(length(terms) - 1) + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(1.0, x), 0.0), + MOI.LessThan(5.0) ) - user_cut_submitted = true - end - return - end) - MOI.optimize!(model) - @test user_cut_submitted + end) + + @test_throws( + MOI.InvalidCallbackUsage( + MOI.HeuristicCallback(), + MOI.UserCut(cb) + ), + MOI.optimize!(model) + ) + end end - @testset "HeuristicSolution" begin - model, x, item_weights = callback_knapsack_model() - callback_called = false - cb_calls = Int32[] - MOI.set(model, Xpress.CallbackFunction(), (cb_data) -> begin - if Xpress.get_control_or_attribute(cb_data.model, Xpress.Lib.XPRS_CALLBACKCOUNT_OPTNODE) > 1 - return - end - Xpress.get_cb_solution(model, cb_data.model) - x_vals = MOI.get.(model, MOI.CallbackVariablePrimal(cb_data), x) - @test MOI.submit( +end + +end + +@testset "Xpress Callbacks" verbose = true begin + + @testset "Message" begin + let + model, x, y = callback_simple_model() + + called_flag = false + + MOI.set( model, - MOI.HeuristicSolution(cb_data), - x, - floor.(x_vals) - ) == MOI.HEURISTIC_SOLUTION_UNKNOWN - callback_called = true - return - end) - MOI.optimize!(model) - @test callback_called + Xpress.MessageCallback(), + (cb_data) -> begin + called_flag = true + + return nothing + end + ) + + @test model.callback_table.xprs_message isa Xpress.CallbackInfo{Xpress.MessageCallbackData} + + MOI.optimize!(model) + + @test called_flag === true broken = true + end end -end -@testset "Xpress.CallbackFunction.CallbackNodeStatus" begin - model, x, item_weights = callback_knapsack_model() - global unknown_reached = false - MOI.set(model, Xpress.CallbackFunction(), (cb_data) -> begin - if MOI.get(model, MOI.CallbackNodeStatus(cb_data)) == MOI.CALLBACK_NODE_STATUS_UNKNOWN - global unknown_reached = true + @testset "OptNode" begin + let + model, x, y = callback_simple_model() + + called_flag = false + + MOI.set( + model, + Xpress.OptNodeCallback(), + (cb_data) -> begin + called_flag = true + + return nothing + end + ) + + @test model.callback_table.xprs_optnode isa Xpress.CallbackInfo{Xpress.OptNodeCallbackData} + + MOI.optimize!(model) + + @test called_flag === true end - end) - MOI.optimize!(model) - @test unknown_reached -end + end + + @testset "PreIntSol" begin + let + model, x, y = callback_simple_model() + + called_flag = false + + MOI.set( + model, + Xpress.PreIntSolCallback(), + (cb_data) -> begin + called_flag = true + + return nothing + end + ) + + @test model.callback_table.xprs_preintsol isa Xpress.CallbackInfo{Xpress.PreIntSolCallbackData} + + MOI.optimize!(model) + + @test called_flag === true + + return nothing + end + end + +end \ No newline at end of file diff --git a/test/MathOptInterface/XPRS_callbacks.jl b/test/MathOptInterface/XPRS_callbacks.jl new file mode 100644 index 00000000..3fd6e656 --- /dev/null +++ b/test/MathOptInterface/XPRS_callbacks.jl @@ -0,0 +1,224 @@ +using Xpress +using LinearAlgebra +using MathOptInterface + +const MOI = MathOptInterface + +function callback_simple_model(; OUTPUTLOG::Integer = 0) + model = Xpress.Optimizer( + PRESOLVE = 0, + CUTSTRATEGY = 0, + HEURSTRATEGY = 0, + SYMMETRY = 0, + OUTPUTLOG = OUTPUTLOG, + ) + + MOI.Utilities.loadfromstring!(model, """ + variables: x, y + maxobjective: y + c1: x in Integer() + c2: y in Integer() + c3: x in Interval(0.0, 2.5) + c4: y in Interval(0.0, 2.5) + """) + + x = MOI.get(model, MOI.VariableIndex, "x") + y = MOI.get(model, MOI.VariableIndex, "y") + + return (model, x, y) +end + +function foo(callback_data::Xpress.CallbackData) + callback_data.data[1] += 1 + + cols = Xpress.getintattrib(callback_data.node_model, Xpress.Lib.XPRS_COLS) + rows = Xpress.getintattrib(callback_data.node_model, Xpress.Lib.XPRS_ROWS) + + Xpress.getdblattrib(callback_data.node_model, Xpress.Lib.XPRS_BESTBOUND) + + ans_variable_primal = Vector{Float64}(undef, Int(cols)) + ans_linear_primal = Vector{Float64}(undef, Int(cols)) + + Xpress.Lib.XPRSgetlpsol( + callback_data.node_model, + ans_variable_primal, + ans_linear_primal, + C_NULL, + C_NULL, + ) + + return nothing +end + +function message_func(callback_data::Xpress.CallbackData) + callback_data.data[5] = 2 + + return nothing +end + +@testset "Low-level Xpress Callback API" verbose = true begin + +@testset "message" begin + @testset "add_xprs_message_callback! + remove_xprs_message_callback!" begin + let + model, x, y = callback_simple_model(; OUTPUTLOG = 1) + + MOI.set(model, Xpress.MessageCallback(), nothing) + + data = Matrix(1.0I, 3, 3) + info = Xpress.add_xprs_message_callback!(model.inner, message_func, data) + + @test info.callback_ptr isa Ptr{Cvoid} + @test info.data_wrapper isa Xpress.CallbackDataWrapper{Xpress.MessageCallbackData} + + @test data[5] == 1 + MOI.optimize!(model) + @test data[5] == 2 + + Xpress.remove_xprs_message_callback!(model.inner, info) + + data[5] = 1 + + MOI.optimize!(model) + @test data[5] == 1 + end + end + + @testset "multiple entries" begin + let + model, x, y = callback_simple_model() + + N = 3 + + data = Matrix(1.0I, 3, 3) + info = [ + Xpress.add_xprs_message_callback!(model.inner, foo, data) + for _ = 1:N + ] + + for i = 1:N + @test info[i].callback_ptr isa Ptr{Cvoid} + @test info[i].data_wrapper isa Xpress.CallbackDataWrapper{Xpress.MessageCallbackData} + end + + @test data[1] == 1 + MOI.optimize!(model) + @test data[1] == N + 1 broken = true # callback registered but not getting called + + for i = 1:N + Xpress.remove_xprs_message_callback!(model.inner, info[i]) + end + + MOI.optimize!(model) + @test data[1] == N + 1 broken = true # callback registered but not getting called + end + end +end + +@testset "optnode" begin + @testset "add_xprs_optnode_callback! + remove_xprs_optnode_callback!" begin + let + model, x, y = callback_simple_model() + + data = Matrix(1.0I, 3, 3) + info = Xpress.add_xprs_optnode_callback!(model.inner, foo, data) + + @test info.callback_ptr isa Ptr{Cvoid} + @test info.data_wrapper isa Xpress.CallbackDataWrapper{Xpress.OptNodeCallbackData} + + @test data[1] == 1 + MOI.optimize!(model) + @test data[1] == 2 + + Xpress.remove_xprs_optnode_callback!(model.inner, info) + + MOI.optimize!(model) + @test data[1] == 2 + end + end + + @testset "multiple entries" begin + let + model, x, y = callback_simple_model() + + N = 3 + + data = Matrix(1.0I, 3, 3) + info = [ + Xpress.add_xprs_optnode_callback!(model.inner, foo, data) + for _ = 1:N + ] + + for i = 1:N + @test info[i].callback_ptr isa Ptr{Cvoid} + @test info[i].data_wrapper isa Xpress.CallbackDataWrapper{Xpress.OptNodeCallbackData} + end + + @test data[1] == 1 + MOI.optimize!(model) + @test data[1] == N + 1 + + for i = 1:N + Xpress.remove_xprs_optnode_callback!(model.inner, info[i]) + end + + MOI.optimize!(model) + @test data[1] == N + 1 + end + end +end + +@testset "preintsol" begin + @testset "add_xprs_preintsol_callback! + remove_xprs_preintsol_callback!" begin + let + model, x, y = callback_simple_model() + + data = Matrix(1.0I, 3, 3) + info = Xpress.add_xprs_preintsol_callback!(model.inner, foo, data) + + @test info.callback_ptr isa Ptr{Cvoid} + @test info.data_wrapper isa Xpress.CallbackDataWrapper{Xpress.PreIntSolCallbackData} + + @test data[1] == 1 + MOI.optimize!(model) + @test data[1] == 2 + + Xpress.remove_xprs_preintsol_callback!(model.inner, info) + + MOI.optimize!(model) + @test data[1] == 2 + end + end + + @testset "multiple entries" begin + let + model, x, y = callback_simple_model() + + N = 3 + + data = Matrix(1.0I, 3, 3) + info = [ + Xpress.add_xprs_preintsol_callback!(model.inner, foo, data) + for _ = 1:N + ] + + for i = 1:N + @test info[i].callback_ptr isa Ptr{Cvoid} + @test info[i].data_wrapper isa Xpress.CallbackDataWrapper{Xpress.PreIntSolCallbackData} + end + + @test data[1] == 1 + MOI.optimize!(model) + @test data[1] == N + 1 + + for i = 1:N + Xpress.remove_xprs_preintsol_callback!(model.inner, info[i]) + end + + MOI.optimize!(model) + @test data[1] == N + 1 + end + end +end + +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 97ccc86d..4b1a67ae 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,7 +6,6 @@ println("Optimizer version: $(Xpress.get_version())") @testset "$(folder)" for folder in [ "MathOptInterface", - "xprs_callbacks", "Derivative", ] @testset "$(file)" for file in readdir(folder) @@ -15,7 +14,6 @@ println("Optimizer version: $(Xpress.get_version())") end @testset "Xpress tests" begin - prob = Xpress.XpressProblem() @test Xpress.getcontrol(prob, "HEURTHREADS") == 0 diff --git a/test/xprs_callbacks/cb_preintsol.jl b/test/xprs_callbacks/cb_preintsol.jl deleted file mode 100644 index 705f95f7..00000000 --- a/test/xprs_callbacks/cb_preintsol.jl +++ /dev/null @@ -1,59 +0,0 @@ -using Xpress -using LinearAlgebra -using MathOptInterface - -const MOI = MathOptInterface - -function callback_simple_model() - model = Xpress.Optimizer( - PRESOLVE = 0, - CUTSTRATEGY = 0, - HEURSTRATEGY = 0, - SYMMETRY = 0, - OUTPUTLOG = 0 - ) - - MOI.Utilities.loadfromstring!(model, """ - variables: x, y - maxobjective: y - c1: x in Integer() - c2: y in Integer() - c3: x in Interval(0.0, 2.5) - c4: y in Interval(0.0, 2.5) - """) - x = MOI.get(model, MOI.VariableIndex, "x") - y = MOI.get(model, MOI.VariableIndex, "y") - return model, x, y -end - -model, x, y = callback_simple_model() - -data = Matrix(1.0I, 3, 3) - -function foo(cb::Xpress.CallbackData) - - cb.data[1] = 98 - - cols = Xpress.get_control_or_attribute(cb.model, Xpress.Lib.XPRS_COLS) - rows = Xpress.get_control_or_attribute(cb.model, Xpress.Lib.XPRS_ROWS) - Xpress.get_control_or_attribute(cb.model, Xpress.Lib.XPRS_BESTBOUND) - - ans_variable_primal = Vector{Float64}(undef,Int(cols)) - ans_linear_primal = Vector{Float64}(undef,Int(cols)) - - Xpress.Lib.XPRSgetlpsol(cb.model, - ans_variable_primal, - ans_linear_primal, - C_NULL, C_NULL) - - return -end - -func_ptr, data_ptr = Xpress.set_callback_preintsol!(model.inner, foo, data) - -@test data[1] == 1 -MOI.optimize!(model) -@test data[1] == 98 - -@test typeof(data_ptr) <: Any -@test typeof(func_ptr) <: Ptr{Cvoid} \ No newline at end of file