diff --git a/src/MOI/MOI_wrapper.jl b/src/MOI/MOI_wrapper.jl index 997d7c19..bf2c1628 100644 --- a/src/MOI/MOI_wrapper.jl +++ b/src/MOI/MOI_wrapper.jl @@ -5,7 +5,7 @@ const CleverDicts = MOI.Utilities.CleverDicts -@enum(VariableType, CONTINUOUS, BINARY, INTEGER, SEMIINTEGER, SEMICONTINUOUS,) +@enum(VariableType, CONTINUOUS, BINARY, INTEGER, SEMIINTEGER, SEMICONTINUOUS) @enum(ConstraintType, AFFINE, INDICATOR, QUADRATIC, SOC, RSOC, SOS_SET) @@ -19,9 +19,9 @@ const CleverDicts = MOI.Utilities.CleverDicts EQUAL_TO, ) -@enum(ObjectiveType, SINGLE_VARIABLE, SCALAR_AFFINE, SCALAR_QUADRATIC,) +@enum(ObjectiveType, SINGLE_VARIABLE, SCALAR_AFFINE, SCALAR_QUADRATIC) -@enum(CallbackState, CB_NONE, CB_GENERIC, CB_LAZY, CB_USER_CUT, CB_HEURISTIC,) +@enum(CallbackState, CB_NONE, CB_GENERIC, CB_LAZY, CB_USER_CUT, CB_HEURISTIC) const SCALAR_SETS = Union{ MOI.GreaterThan{Float64}, @@ -95,6 +95,7 @@ mutable struct ConstraintInfo # perhaps call lazy on calls for writing lps and so on name::String type::ConstraintType + function ConstraintInfo( row::Int, set::MOI.AbstractSet, @@ -145,6 +146,11 @@ mutable struct IISData colbndtype::Vector{UInt8} # sense of the column bounds that participate end +""" + Optimizer() + +Create a new Optimizer object. +""" mutable struct Optimizer <: MOI.AbstractOptimizer # The low-level Xpress model. inner::XpressProblem @@ -243,42 +249,30 @@ mutable struct Optimizer <: MOI.AbstractOptimizer message_callback::Union{Nothing,Tuple{Ptr{Nothing},_CallbackUserData}} params::Dict{Any,Any} - """ - Optimizer() - Create a new Optimizer object. - """ function Optimizer(; kwargs...) model = new() - model.params = Dict{Any,Any}() model.log_level = 1 # is xpress default model.show_warning = true model.moi_warnings = true model.ignore_start = false model.post_solve = true - 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 - for (name, value) in kwargs name = MOI.RawOptimizerAttribute(string(name)) model.params[name] = value end - model.variable_info = CleverDicts.CleverDict{MOI.VariableIndex,VariableInfo}() model.affine_constraint_info = Dict{Int,ConstraintInfo}() model.sos_constraint_info = Dict{Int,ConstraintInfo}() - MOI.empty!(model) # inner is initialized here - return model end end @@ -287,24 +281,21 @@ Base.show(io::IO, model::Optimizer) = show(io, model.inner) function MOI.empty!(model::Optimizer) model.inner = XpressProblem() - 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 + if log_level != 0 + MOI.set(model, MOI.RawOptimizerAttribute("OUTPUTLOG"), 0) + end @checked Lib.XPRSloadlp( model.inner, "", @@ -321,10 +312,9 @@ function MOI.empty!(model::Optimizer) C_NULL, C_NULL, ) - # re-enable logging - log_level != 0 && + if log_level != 0 MOI.set(model, MOI.RawOptimizerAttribute("OUTPUTLOG"), log_level) - + end model.name = "" model.objective_type = SCALAR_AFFINE model.is_objective_set = false @@ -335,31 +325,23 @@ function MOI.empty!(model::Optimizer) empty!(model.sos_constraint_info) model.name_to_variable = nothing model.name_to_constraint_index = nothing - model.cached_solution = nothing model.basis_status = nothing model.conflict = nothing - model.termination_status = MOI.OPTIMIZE_NOT_CALLED model.primal_status = MOI.NO_SOLUTION model.dual_status = MOI.NO_SOLUTION - 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.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 @@ -367,40 +349,29 @@ function MOI.empty!(model::Optimizer) 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.objective_sense !== nothing && return false - !isempty(model.variable_info) && return false - length(model.affine_constraint_info) != 0 && return false - length(model.sos_constraint_info) != 0 && return false - model.name_to_variable !== nothing && return false - model.name_to_constraint_index !== nothing && return false - - model.cached_solution !== nothing && return false - model.basis_status !== nothing && return false - model.conflict !== nothing && return false - - 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 - - # model.message_callback !== nothing && return false - # otherwise jump complains it is not empty - - return true + return isempty(model.name) && + model.objective_type == SCALAR_AFFINE && + !model.is_objective_set && + model.objective_sense === nothing && + isempty(model.variable_info) && + isempty(model.affine_constraint_info) && + isempty(model.sos_constraint_info) && + model.name_to_variable === nothing && + model.name_to_constraint_index === nothing && + model.cached_solution === nothing && + model.basis_status === nothing && + model.conflict === nothing && + model.termination_status == MOI.OPTIMIZE_NOT_CALLED && + model.primal_status == MOI.NO_SOLUTION && + model.dual_status == MOI.NO_SOLUTION && + model.callback_cached_solution === nothing && + model.callback_state == CB_NONE && + model.cb_exception === nothing && + model.lazy_callback === nothing && + model.user_cut_callback === nothing && + model.heuristic_callback === nothing && + !model.has_generic_callback && + model.callback_data === nothing end function reset_cached_solution(model::Optimizer) @@ -467,55 +438,6 @@ function MOI.get(optimizer::Optimizer, ::MOI.SolverVersion) return MOI.get(optimizer, MOI.RawOptimizerAttribute("XPRESSVERSION")) end -function MOI.supports( - ::Optimizer, - ::MOI.ObjectiveFunction{F}, -) where { - F<:Union{ - MOI.VariableIndex, - MOI.ScalarAffineFunction{Float64}, - MOI.ScalarQuadraticFunction{Float64}, - }, -} - return true -end - -function MOI.supports_constraint( - ::Optimizer, - ::Type{MOI.VariableIndex}, - ::Type{F}, -) where { - F<:Union{ - MOI.EqualTo{Float64}, - MOI.LessThan{Float64}, - MOI.GreaterThan{Float64}, - MOI.Interval{Float64}, - MOI.ZeroOne, - MOI.Integer, - MOI.Semicontinuous{Float64}, - MOI.Semiinteger{Float64}, - }, -} - return true -end - -function MOI.supports_constraint( - ::Optimizer, - ::Type{MOI.VectorOfVariables}, - ::Type{F}, -) where { - F<:Union{ - MOI.SOS1{Float64}, - MOI.SOS2{Float64}, - MOI.SecondOrderCone, - MOI.RotatedSecondOrderCone, - }, -} - # Xpress only supports disjoint sets of SOC and RSOC (with no affine forms) - # hence we only allow constraints on creation - return true -end - function MOI.supports_add_constrained_variables( ::Optimizer, ::Type{F}, @@ -554,20 +476,10 @@ function MOI.supports_constraint( return true end -MOI.supports(::Optimizer, ::MOI.VariableName, ::Type{MOI.VariableIndex}) = true -function MOI.supports( - ::Optimizer, - ::MOI.ConstraintName, - ::Type{<:MOI.ConstraintIndex}, -) - return true -end +#= + MOI.RawOptimizerAttribute +=# -MOI.supports(::Optimizer, ::MOI.Name) = true -MOI.supports(::Optimizer, ::MOI.Silent) = true -MOI.supports(::Optimizer, ::MOI.NumberOfThreads) = true -MOI.supports(::Optimizer, ::MOI.TimeLimitSec) = true -MOI.supports(::Optimizer, ::MOI.ObjectiveSense) = true MOI.supports(::Optimizer, ::MOI.RawOptimizerAttribute) = true function MOI.set(model::Optimizer, param::MOI.RawOptimizerAttribute, value) @@ -598,10 +510,10 @@ function MOI.set(model::Optimizer, param::MOI.RawOptimizerAttribute, value) reset_message_callback(model) elseif param == MOI.RawOptimizerAttribute("OUTPUTLOG") model.log_level = value - Xpress.setcontrol!(model.inner, "OUTPUTLOG", value) + setcontrol!(model.inner, "OUTPUTLOG", value) reset_message_callback(model) else - Xpress.setcontrol!(model.inner, param.name, value) + setcontrol!(model.inner, param.name, value) end return end @@ -612,10 +524,10 @@ function reset_message_callback(model) @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 + if isempty(model.inner.logfile) && model.log_level != 0 model.message_callback = setoutputcb!(model.inner, model.show_warning) end + return end function MOI.get(model::Optimizer, param::MOI.RawOptimizerAttribute) @@ -631,11 +543,16 @@ function MOI.get(model::Optimizer, param::MOI.RawOptimizerAttribute) return model.solve_method elseif param == MOI.RawOptimizerAttribute("XPRESS_WARNING_WINDOWS") return model.show_warning - else - return Xpress.get_control_or_attribute(model.inner, param.name) end + return get_control_or_attribute(model.inner, param.name) end +#= + MOI.TimeLimitSec +=# + +MOI.supports(::Optimizer, ::MOI.TimeLimitSec) = true + function MOI.set(model::Optimizer, ::MOI.TimeLimitSec, limit::Real) # positive values would mean that its stops after `limit` seconds # iff there is already a MIP solution available. @@ -659,49 +576,50 @@ end function MOI.get(model::Optimizer, ::MOI.ListOfVariableAttributesSet) ret = MOI.AbstractVariableAttribute[] - found_name = - any(!isempty(info.name) for info in values(model.variable_info)) - found_start = - any(info.start !== nothing for info in values(model.variable_info)) - if found_name + if any(!isempty(info.name) for info in values(model.variable_info)) push!(ret, MOI.VariableName()) end - if found_start + if any(info.start !== nothing for info in values(model.variable_info)) push!(ret, MOI.VariablePrimalStart()) end return ret end function MOI.get(model::Optimizer, ::MOI.ListOfModelAttributesSet) + attributes = MOI.AbstractModelAttribute[] if MOI.is_empty(model) - return Any[] + return attributes end - attributes = Any[] if model.objective_sense !== nothing push!(attributes, MOI.ObjectiveSense()) end - typ = MOI.get(model, MOI.ObjectiveFunctionType()) - if typ !== nothing - push!(attributes, MOI.ObjectiveFunction{typ}()) + F = MOI.get(model, MOI.ObjectiveFunctionType()) + if F !== nothing + push!(attributes, MOI.ObjectiveFunction{F}()) end - if MOI.get(model, MOI.Name()) != "" + if !isempty(MOI.get(model, MOI.Name())) push!(attributes, MOI.Name()) end return attributes end +function MOI.get( + ::Optimizer, + ::MOI.ListOfConstraintAttributesSet{MOI.VariableIndex}, +) + return MOI.AbstractConstraintAttribute[] +end + function MOI.get( model::Optimizer, ::MOI.ListOfConstraintAttributesSet{F,S}, -) where {S,F} +) where {F,S} ret = MOI.AbstractConstraintAttribute[] - constraint_indices = MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) - found_name = any( - !isempty(MOI.get(model, MOI.ConstraintName(), index)) for - index in constraint_indices - ) - if found_name - push!(ret, MOI.ConstraintName()) + for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) + if !isempty(MOI.get(model, MOI.ConstraintName(), ci)) + push!(ret, MOI.ConstraintName()) + break + end end return ret end @@ -762,7 +680,6 @@ function _indices_and_coefficients( I[i] = _info(model, term.variable_1).column J[i] = _info(model, term.variable_2).column V[i] = term.coefficient - # MOI represents objective as 0.5 x' Q x # Example: obj = 2x^2 + x*y + y^2 # = 2x^2 + (1/2)*x*y + (1/2)*y*x + y^2 @@ -784,7 +701,6 @@ function _indices_and_coefficients( # Hence, # Only for constraints, MOI -> Xpress => divide all by 2 # Only for constraints, Xpress -> MOI => multiply all by 2 - end for (i, term) in enumerate(f.affine_terms) indices[i] = _info(model, term.variable).column @@ -837,14 +753,14 @@ function MOI.add_variable(model::Optimizer) info.column = length(model.variable_info) @checked Lib.XPRSaddcols( model.inner, - 1,#length(_dbdl)::Int, - 0,#length(_dmatval)::Int, - Ref(0.0),#_dobj::Vector{Float64}, - C_NULL,#Cint.(_mrwind::Vector{Int}), - C_NULL,#Cint.(_mrstart::Vector{Int}), - C_NULL,#_dmatval::Vector{Float64}, - Ref(-Inf),#_dbdl::Vector{Float64}, - Ref(Inf),#_dbdu::Vector{Float64} + 1, # length(_dbdl)::Int, + 0, # length(_dmatval)::Int, + Ref(0.0), # _dobj::Vector{Float64}, + C_NULL, # Cint.(_mrwind::Vector{Int}), + C_NULL, # Cint.(_mrstart::Vector{Int}), + C_NULL, # _dmatval::Vector{Float64}, + Ref(-Inf), # _dbdl::Vector{Float64}, + Ref(Inf), # _dbdu::Vector{Float64} ) return index end @@ -852,14 +768,14 @@ end function MOI.add_variables(model::Optimizer, N::Int) @checked Lib.XPRSaddcols( model.inner, - N,#length(_dbdl)::Int, - 0,#length(_dmatval)::Int, - zeros(N),# _dobj::Vector{Float64}, - C_NULL,#Cint.(_mrwind::Vector{Int}), - C_NULL,#Cint.(_mrstart::Vector{Int}), - C_NULL,# _dmatval::Vector{Float64}, - fill(-Inf, N),# _dbdl::Vector{Float64}, - fill(Inf, N),# _dbdu::Vector{Float64} + N, # length(_dbdl)::Int, + 0, # length(_dmatval)::Int, + zeros(N), # _dobj::Vector{Float64}, + C_NULL, # Cint.(_mrwind::Vector{Int}), + C_NULL, # Cint.(_mrstart::Vector{Int}), + C_NULL, # _dmatval::Vector{Float64}, + fill(-Inf, N), # _dbdl::Vector{Float64}, + fill(Inf, N), # _dbdu::Vector{Float64} ) indices = Vector{MOI.VariableIndex}(undef, N) num_variables = length(model.variable_info) @@ -918,7 +834,7 @@ end function _rebuild_name_to_variable(model::Optimizer) model.name_to_variable = Dict{String,Union{Nothing,MOI.VariableIndex}}() for (index, info) in model.variable_info - if info.name == "" + if isempty(info.name) continue end if haskey(model.name_to_variable, info.name) @@ -930,6 +846,12 @@ function _rebuild_name_to_variable(model::Optimizer) return end +#= + MOI.VariableName +=# + +MOI.supports(::Optimizer, ::MOI.VariableName, ::Type{MOI.VariableIndex}) = true + function MOI.get(model::Optimizer, ::MOI.VariableName, v::MOI.VariableIndex) return _info(model, v).name end @@ -943,8 +865,7 @@ function MOI.set( info = _info(model, v) info.name = name # Note: don't set the string names in the Xpress C API because it complains - # on duplicate variables. - # That is, don't call `Lib.XPRSaddnames`. + # on duplicate variables, that is, don't call `Lib.XPRSaddnames`. model.name_to_variable = nothing return end @@ -970,17 +891,13 @@ function forward(model::Optimizer) spare_rows = @_invoke Lib.XPRSgetintattrib(model.inner, Lib.XPRS_SPAREROWS, _)::Int cols = @_invoke Lib.XPRSgetintattrib(model.inner, Lib.XPRS_COLS, _)::Int - - #1 - Create vector 'aux_vector' of size ROWS of type Float64 (constraints) + # 1 - Create vector 'aux_vector' of size ROWS of type Float64 (constraints) aux_vector = copy(model.forward_sensitivity_cache.input) - - #2 - Call XPRSftran with vector 'aux_vector' as an argument + # 2 - Call XPRSftran with vector 'aux_vector' as an argument @checked Lib.XPRSftran(model.inner, aux_vector) - - #3 - Create Dict of Basic variable to All variables + # 3 - Create Dict of Basic variable to All variables basic_variables_ordered = Vector{Cint}(undef, rows) @checked Lib.XPRSgetpivotorder(model.inner, basic_variables_ordered) - aux_dict = Dict{Int,Int}() for i in 1:length(basic_variables_ordered) if rows + spare_rows <= @@ -989,13 +906,12 @@ function forward(model::Optimizer) aux_dict[i] = basic_variables_ordered[i] - (rows + spare_rows) + 1 end end - - #5 - Populate vector of All variables with the correct value of the Basic variables + # 5 - Populate vector of All variables with the correct value of the Basic + # variables fill!(model.forward_sensitivity_cache.output, 0.0) for (bi, vi) in aux_dict model.forward_sensitivity_cache.output[vi] = aux_vector[bi] end - return end @@ -1004,11 +920,9 @@ function backward(model::Optimizer) spare_rows = @_invoke Lib.XPRSgetintattrib(model.inner, Lib.XPRS_SPAREROWS, _)::Int cols = @_invoke Lib.XPRSgetintattrib(model.inner, Lib.XPRS_COLS, _)::Int - - #1 - Get Basic variables + # 1 - Get Basic variables basic_variables_ordered = Vector{Int32}(undef, rows) @checked Lib.XPRSgetpivotorder(model.inner, basic_variables_ordered) - aux_dict = Dict{Int,Int}() for i in 1:length(basic_variables_ordered) if rows + spare_rows <= @@ -1017,19 +931,17 @@ function backward(model::Optimizer) aux_dict[i] = basic_variables_ordered[i] - (rows + spare_rows) + 1 end end - - #2 - Create vector 'aux_vector' of size ROWS of type Float64 (constraints) initialized at zero + # 2 - Create vector 'aux_vector' of size ROWS of type Float64 (constraints) + # initialized at zero aux_vector = zeros(rows) - - #3 - Populate vector 'aux_vector' with the respective values in the correct positions of the basic variables + # 3 - Populate vector 'aux_vector' with the respective values in the correct + # positions of the basic variables for (bi, vi) in aux_dict aux_vector[bi] = model.backward_sensitivity_cache.input[vi] end - - #4 - Call XPRSbtran with vector 'aux_vector' as an argument + # 4 - Call XPRSbtran with vector 'aux_vector' as an argument @checked Lib.XPRSbtran(model.inner, aux_vector) - - #5 - Set constraint_output equal to vector 'aux_vector' + # 5 - Set constraint_output equal to vector 'aux_vector' model.backward_sensitivity_cache.output .= aux_vector return end @@ -1119,60 +1031,64 @@ end ### function _zero_objective(model::Optimizer) - num_vars = length(model.variable_info) - obj = zeros(Float64, num_vars) if model.objective_type == SCALAR_QUADRATIC # We need to zero out the existing quadratic objective. @checked Lib.XPRSdelqmatrix(model.inner, -1) end + num_vars = Cint(length(model.variable_info)) @checked Lib.XPRSchgobj( model.inner, - Cint(num_vars), + num_vars, collect(Cint(0):Cint(num_vars - 1)), - obj, + zeros(Float64, num_vars), ) @checked Lib.XPRSchgobj(model.inner, Cint(1), Ref{Cint}(-1), Ref(0.0)) return end +#= + MOI.ObjectiveSense +=# + +MOI.supports(::Optimizer, ::MOI.ObjectiveSense) = true + function MOI.set( model::Optimizer, ::MOI.ObjectiveSense, sense::MOI.OptimizationSense, ) - # TODO: should this propagate across a `MOI.empty!(optimizer)` call if sense == MOI.MIN_SENSE - objsense = :Min + @checked Lib.XPRSchgobjsense(model.inner, Lib.XPRS_OBJ_MINIMIZE) elseif sense == MOI.MAX_SENSE - objsense = :Max + @checked Lib.XPRSchgobjsense(model.inner, Lib.XPRS_OBJ_MAXIMIZE) else @assert sense == MOI.FEASIBILITY_SENSE _zero_objective(model) - objsense = :Min - end - v = - objsense == :maximize || - objsense == :Max || - objsense == Lib.XPRS_OBJ_MAXIMIZE ? Lib.XPRS_OBJ_MAXIMIZE : - objsense == :minimize || - objsense == :Min || - objsense == Lib.XPRS_OBJ_MINIMIZE ? Lib.XPRS_OBJ_MINIMIZE : - throw( - ArgumentError( - "Invalid objective sense: $objsense. It can only be `:maximize`, `:minimize`, `:Max`, `:Min`, `$(Lib.XPRS_OBJ_MAXIMIZE)`, or `$(Lib.XPRS_OBJ_MINIMIZE)`.", - ), - ) - @checked Lib.XPRSchgobjsense(model.inner, v) + @checked Lib.XPRSchgobjsense(model.inner, Lib.XPRS_OBJ_MINIMIZE) + end model.objective_sense = sense return end function MOI.get(model::Optimizer, ::MOI.ObjectiveSense) - if model.objective_sense !== nothing - return model.objective_sense - else - return MOI.FEASIBILITY_SENSE - end + return something(model.objective_sense, MOI.FEASIBILITY_SENSE) +end + +#= + MOI.ObjectiveFunction +=# + +function MOI.supports( + ::Optimizer, + ::MOI.ObjectiveFunction{F}, +) where { + F<:Union{ + MOI.VariableIndex, + MOI.ScalarAffineFunction{Float64}, + MOI.ScalarQuadraticFunction{Float64}, + }, +} + return true end function MOI.set( @@ -1352,6 +1268,25 @@ end ## VariableIndex-in-Set constraints. ## +function MOI.supports_constraint( + ::Optimizer, + ::Type{MOI.VariableIndex}, + ::Type{S}, +) where { + S<:Union{ + MOI.EqualTo{Float64}, + MOI.LessThan{Float64}, + MOI.GreaterThan{Float64}, + MOI.Interval{Float64}, + MOI.ZeroOne, + MOI.Integer, + MOI.Semicontinuous{Float64}, + MOI.Semiinteger{Float64}, + }, +} + return true +end + function _info( model::Optimizer, c::MOI.ConstraintIndex{MOI.VariableIndex,<:Any}, @@ -2205,57 +2140,6 @@ function MOI.get( return MOI.Semiinteger(lower, upper) end -function MOI.get( - model::Optimizer, - ::MOI.ConstraintName, - c::MOI.ConstraintIndex{MOI.VariableIndex,S}, -) where {S} - MOI.throw_if_not_valid(model, c) - info = _info(model, c) - if S <: MOI.LessThan - return info.lessthan_name - elseif S <: Union{MOI.GreaterThan,MOI.Interval,MOI.EqualTo} - return info.greaterthan_interval_or_equalto_name - else - @assert S <: Union{ - MOI.ZeroOne, - MOI.Integer, - MOI.Semiinteger, - MOI.Semicontinuous, - } - return info.type_constraint_name - end -end - -function MOI.set( - model::Optimizer, - ::MOI.ConstraintName, - c::MOI.ConstraintIndex{MOI.VariableIndex,S}, - name::String, -) where {S} - MOI.throw_if_not_valid(model, c) - info = _info(model, c) - old_name = "" - if S <: MOI.LessThan - old_name = info.lessthan_name - info.lessthan_name = name - elseif S <: Union{MOI.GreaterThan,MOI.Interval,MOI.EqualTo} - old_name = info.greaterthan_interval_or_equalto_name - info.greaterthan_interval_or_equalto_name = name - else - @assert S <: Union{ - MOI.ZeroOne, - MOI.Integer, - MOI.Semiinteger, - MOI.Semicontinuous, - } - info.type_constraint_name - info.type_constraint_name = name - end - model.name_to_constraint_index = nothing - return -end - ### ### ScalarAffineFunction-in-Set ### @@ -2446,7 +2330,7 @@ end function _get_affine_terms(model::Optimizer, c::MOI.ConstraintIndex) row = _info(model, c).row - nzcnt_max = Xpress.n_non_zero_elements(model.inner) + nzcnt_max = n_non_zero_elements(model.inner) _nzcnt = Ref(Cint(0)) @checked Lib.XPRSgetrows( @@ -2503,6 +2387,25 @@ function MOI.get( return MOI.ScalarAffineFunction(terms, 0.0) end +#= + MOI.ConstraintName +=# + +function MOI.supports( + ::Optimizer, + ::MOI.ConstraintName, + ::Type{<:MOI.ConstraintIndex{F}}, +) where { + F<:Union{ + MOI.VectorAffineFunction{Float64}, + MOI.ScalarAffineFunction{Float64}, + MOI.ScalarQuadraticFunction{Float64}, + MOI.VectorOfVariables, + }, +} + return true +end + function MOI.get( model::Optimizer, ::MOI.ConstraintName, @@ -2683,7 +2586,7 @@ function MOI.get( # row = info.row # set_cte = MOI.constant(info.set.set) # # a^T x + b <= c ===> a^T <= c - b - # Xpress.getrhs!(model.inner, rhs, row, row) + # getrhs!(model.inner, rhs, row, row) # return MOI.Indicator{A}(S(rhs[1])) return _info(model, c).set end @@ -2735,7 +2638,7 @@ function MOI.add_constraint( @checked Lib.XPRSsetindicators( model.inner, 1, - Ref{Cint}(Xpress.n_constraints(model.inner) - 1), + Ref{Cint}(n_constraints(model.inner) - 1), Ref{Cint}(con_value - 1), Ref{Cint}(indicator_activation(Val{A})), ) @@ -2819,7 +2722,7 @@ function MOI.add_constraint( J .-= 1 @checked Lib.XPRSaddqmatrix( model.inner, - Xpress.n_constraints(model.inner) - 1, + n_constraints(model.inner) - 1, Cint(length(I)), I, J, @@ -2880,6 +2783,23 @@ end ### VectorOfVariables-in-SOS{I|II} ### +function MOI.supports_constraint( + ::Optimizer, + ::Type{MOI.VectorOfVariables}, + ::Type{F}, +) where { + F<:Union{ + MOI.SOS1{Float64}, + MOI.SOS2{Float64}, + MOI.SecondOrderCone, + MOI.RotatedSecondOrderCone, + }, +} + # Xpress only supports disjoint sets of SOC and RSOC (with no affine forms) + # hence we only allow constraints on creation + return true +end + const SOS = Union{MOI.SOS1{Float64},MOI.SOS2{Float64}} function _info( @@ -3033,6 +2953,7 @@ function pre_solve_reset(model::Optimizer) reset_cached_solution(model) return end + function check_cb_exception(model::Optimizer) if model.cb_exception !== nothing e = model.cb_exception @@ -3042,9 +2963,7 @@ function check_cb_exception(model::Optimizer) return end -function is_mip(model) - return Xpress.is_mixedinteger(model.inner) && !model.solve_relaxation -end +is_mip(model) = is_mixedinteger(model.inner) && !model.solve_relaxation function _set_MIP_start(model) colind, solval = Cint[], Cdouble[] @@ -3065,7 +2984,7 @@ 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 + 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)) @@ -3092,17 +3011,14 @@ function MOI.optimize!(model::Optimizer) end model.cached_solution.solve_time = time() - start_time check_cb_exception(model) - - # should be almost a no-op if not needed - # might have minor overhead due to memory being freed + # Should be almost a no-op if not needed. Might have minor overhead due to + # memory being freed if model.post_solve @checked Lib.XPRSpostsolve(model.inner) end - model.termination_status = _cache_termination_status(model) model.primal_status = _cache_primal_status(model) model.dual_status = _cache_dual_status(model) - # TODO: add @checked here - must review statuses if is_mip(model) # TODO @checked (only works if not in [MOI.NO_SOLUTION, MOI.INFEASIBILITY_CERTIFICATE, MOI.INFEASIBLE_POINT]) @@ -3124,7 +3040,6 @@ function MOI.optimize!(model::Optimizer) end model.cached_solution.linear_primal .= rhs .- model.cached_solution.linear_primal - status = MOI.get(model, MOI.PrimalStatus()) if status == MOI.INFEASIBILITY_CERTIFICATE has_Ray = Int64[0] @@ -3154,6 +3069,7 @@ function _throw_if_optimize_in_progress(model, attr) if model.callback_state != CB_NONE throw(MOI.OptimizeInProgress(attr)) end + return end function MOI.get(model::Optimizer, attr::MOI.RawStatusString) @@ -3167,14 +3083,14 @@ function MOI.get(model::Optimizer, attr::MOI.RawStatusString) Lib.XPRS_MIPSTATUS, _, )::Int - return Xpress.MIPSTATUS_STRING[stat] * " - " * stop_str + return MIPSTATUS_STRING[stat] * " - " * stop_str else stat = @_invoke Lib.XPRSgetintattrib( model.inner, Lib.XPRS_LPSTATUS, _, )::Int - return Xpress.LPSTATUS_STRING[stat] * " - " * stop_str + return LPSTATUS_STRING[stat] * " - " * stop_str end end @@ -3397,6 +3313,10 @@ function MOI.get(model::Optimizer, attr::MOI.DualStatus) return model.dual_status end +#= + MOI.VariablePrimal +=# + function MOI.get( model::Optimizer, attr::MOI.VariablePrimal, @@ -3408,6 +3328,10 @@ function MOI.get( return model.cached_solution.variable_primal[column] end +#= + MOI.ConstraintPrimal +=# + function MOI.get( model::Optimizer, attr::MOI.ConstraintPrimal, @@ -3440,6 +3364,10 @@ function MOI.get( return model.cached_solution.linear_primal[row] end +#= + MOI.ConstraintDual +=# + function _dual_multiplier(model::Optimizer) return MOI.get(model, MOI.ObjectiveSense()) == MOI.MIN_SENSE ? 1.0 : -1.0 end @@ -3490,10 +3418,6 @@ function _farkas_variable_dual(model::Optimizer, col::Int64) ) end -#= - Duals -=# - function MOI.get( model::Optimizer, attr::MOI.ConstraintDual, @@ -3613,46 +3537,40 @@ function MOI.get( return _dual_multiplier(model) * pi end +#= + MOI.ObjectiveValue +=# + function MOI.get(model::Optimizer, attr::MOI.ObjectiveValue) _throw_if_optimize_in_progress(model, attr) MOI.check_result_index_bounds(model, attr) - if is_mip(model) - return @_invoke Lib.XPRSgetdblattrib( - model.inner, - Lib.XPRS_MIPOBJVAL, - _, - )::Float64 - else - return @_invoke Lib.XPRSgetdblattrib( - model.inner, - Lib.XPRS_LPOBJVAL, - _, - )::Float64 - end + attr = is_mip(model) ? Lib.XPRS_MIPOBJVAL : Lib.XPRS_LPOBJVAL + return @_invoke Lib.XPRSgetdblattrib(model.inner, attr, _)::Float64 end +#= + MOI.ObjectiveBound +=# + function MOI.get(model::Optimizer, attr::MOI.ObjectiveBound) _throw_if_optimize_in_progress(model, attr) - if is_mip(model) - return @_invoke Lib.XPRSgetdblattrib( - model.inner, - Lib.XPRS_BESTBOUND, - _, - )::Float64 - else - return @_invoke Lib.XPRSgetdblattrib( - model.inner, - Lib.XPRS_LPOBJVAL, - _, - )::Float64 - end + attr = is_mip(model) ? Lib.XPRS_BESTBOUND : Lib.XPRS_LPOBJVAL + return @_invoke Lib.XPRSgetdblattrib(model.inner, attr, _)::Float64 end +#= + MOI.SolveTimeSec +=# + function MOI.get(model::Optimizer, attr::MOI.SolveTimeSec) _throw_if_optimize_in_progress(model, attr) return model.cached_solution.solve_time end +#= + MOI.SimplexIterations +=# + function MOI.get(model::Optimizer, attr::MOI.SimplexIterations) _throw_if_optimize_in_progress(model, attr) return @_invoke Lib.XPRSgetintattrib( @@ -3662,16 +3580,28 @@ function MOI.get(model::Optimizer, attr::MOI.SimplexIterations) )::Int end +#= + MOI.BarrierIterations +=# + function MOI.get(model::Optimizer, attr::MOI.BarrierIterations) _throw_if_optimize_in_progress(model, attr) return @_invoke Lib.XPRSgetintattrib(model.inner, Lib.XPRS_BARITER, _)::Int end +#= + MOI.NodeCount +=# + function MOI.get(model::Optimizer, attr::MOI.NodeCount) _throw_if_optimize_in_progress(model, attr) return @_invoke Lib.XPRSgetintattrib(model.inner, Lib.XPRS_NODES, _)::Int end +#= + MOI.RelativeGap +=# + function MOI.get(model::Optimizer, attr::MOI.RelativeGap) _throw_if_optimize_in_progress(model, attr) BESTBOUND = MOI.get(model, MOI.ObjectiveBound()) @@ -3679,12 +3609,21 @@ function MOI.get(model::Optimizer, attr::MOI.RelativeGap) return abs(MIPOBJVAL - BESTBOUND) / max(abs(BESTBOUND), abs(MIPOBJVAL)) end +#= + MOI.DualObjectiveValue +=# + +# TODO(odow): this is wrong function MOI.get(model::Optimizer, attr::MOI.DualObjectiveValue) _throw_if_optimize_in_progress(model, attr) MOI.check_result_index_bounds(model, attr) return MOI.get(model, MOI.ObjectiveValue(attr.result_index)) end +#= + MOI.ResultCount +=# + function MOI.get(model::Optimizer, attr::MOI.ResultCount) _throw_if_optimize_in_progress(model, attr) if model.cached_solution === nothing @@ -3693,20 +3632,29 @@ function MOI.get(model::Optimizer, attr::MOI.ResultCount) return 1 elseif model.cached_solution.has_primal_certificate return 1 - else - return (model.cached_solution.has_feasible_point) ? 1 : 0 end + return model.cached_solution.has_feasible_point ? 1 : 0 end -function MOI.get(model::Optimizer, ::MOI.Silent) - return model.log_level == 0 -end +#= + MOI.Silent +=# + +MOI.supports(::Optimizer, ::MOI.Silent) = true + +MOI.get(model::Optimizer, ::MOI.Silent) = model.log_level == 0 function MOI.set(model::Optimizer, ::MOI.Silent, flag::Bool) MOI.set(model, MOI.RawOptimizerAttribute("OUTPUTLOG"), ifelse(flag, 0, 1)) return end +#= + MOI.NumberOfThreads +=# + +MOI.supports(::Optimizer, ::MOI.NumberOfThreads) = true + function MOI.get(model::Optimizer, ::MOI.NumberOfThreads) return Int(MOI.get(model, MOI.RawOptimizerAttribute("THREADS"))) end @@ -3715,6 +3663,12 @@ function MOI.set(model::Optimizer, ::MOI.NumberOfThreads, x::Int) return MOI.set(model, MOI.RawOptimizerAttribute("THREADS"), x) end +#= + MOI.Name +=# + +MOI.supports(::Optimizer, ::MOI.Name) = true + function MOI.get(model::Optimizer, ::MOI.Name) return model.name end @@ -3725,12 +3679,37 @@ function MOI.set(model::Optimizer, ::MOI.Name, name::String) return end +#= + MOI.RawSolver +=# + +MOI.get(model::Optimizer, ::MOI.RawSolver) = model.inner + +#= + MOI.NumberOfVariables +=# + MOI.get(model::Optimizer, ::MOI.NumberOfVariables) = length(model.variable_info) + +#= + MOI.ListOfVariableIndices +=# + function MOI.get(model::Optimizer, ::MOI.ListOfVariableIndices) return sort!(collect(keys(model.variable_info)); by = x -> x.value) end -MOI.get(model::Optimizer, ::MOI.RawSolver) = model.inner +#= + MOI.VariablePrimalStart +=# + +function MOI.supports( + ::Optimizer, + ::MOI.VariablePrimalStart, + ::Type{MOI.VariableIndex}, +) + return true +end function MOI.set( model::Optimizer, @@ -3751,14 +3730,6 @@ function MOI.get( return _info(model, x).start end -function MOI.supports( - ::Optimizer, - ::MOI.VariablePrimalStart, - ::Type{MOI.VariableIndex}, -) - return true -end - function MOI.get(model::Optimizer, ::MOI.NumberOfConstraints{F,S}) where {F,S} # TODO: this could be more efficient. return length(MOI.get(model, MOI.ListOfConstraintIndices{F,S}())) @@ -3917,17 +3888,14 @@ function MOI.modify( ) where {S} nels = length(cis) @assert nels == length(chgs) - rows = Vector{Cint}(undef, nels) cols = Vector{Cint}(undef, nels) coefs = Vector{Float64}(undef, nels) - for i in 1:nels rows[i] = Cint(_info(model, cis[i]).row - 1) cols[i] = Cint(_info(model, chgs[i].variable).column - 1) coefs[i] = chgs[i].new_coefficient end - @checked Lib.XPRSchgmcoef(model.inner, Cint(nels), rows, cols, coefs) return end @@ -3958,12 +3926,10 @@ function MOI.modify( nels = length(chgs) cols = Vector{Cint}(undef, nels) coefs = Vector{Float64}(undef, nels) - for i in 1:nels cols[i] = _info(model, chgs[i].variable).column - 1 coefs[i] = chgs[i].new_coefficient end - @checked Lib.XPRSchgobj(model.inner, Cint(length(coefs)), cols, coefs) model.is_objective_set = true return @@ -4117,24 +4083,19 @@ function MOI.get( ::MOI.ConstraintBasisStatus, c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},S}, ) where {S<:SCALAR_SETS} - row = _info(model, c).row - basis_status = model.basis_status - if basis_status == nothing - _generate_basis_status(model::Optimizer) - basis_status = model.basis_status + if model.basis_status == nothing + _generate_basis_status(model) end - cstatus = basis_status.con_status - cbasis = cstatus[row] + cbasis = model.basis_status.con_status[_info(model, c).row] if cbasis == 1 return MOI.BASIC elseif cbasis == 0 return MOI.NONBASIC elseif cbasis == 2 return MOI.NONBASIC - elseif cbasis == 3 - return MOI.SUPER_BASIC else - error("CBasis value of $(cbasis) isn't defined.") + @assert cbasis == 3 + return MOI.SUPER_BASIC end end @@ -4246,7 +4207,7 @@ function MOI.add_constraint( ) @checked Lib.XPRSaddqmatrix( model.inner, - Xpress.n_constraints(model.inner) - 1, + n_constraints(model.inner) - 1, Cint(length(I)), I, I, @@ -4338,7 +4299,7 @@ function MOI.add_constraint( ) @checked Lib.XPRSaddqmatrix( model.inner, - Xpress.n_constraints(model.inner) - 1, + n_constraints(model.inner) - 1, length(I), I, J, @@ -4589,7 +4550,6 @@ function getfirstiis(model::Optimizer) iismode = Cint(1) status_code = Ref{Cint}(0) @checked Lib.XPRSiisfirst(model.inner, iismode, status_code) - if status_code[] == 1 # The problem is actually feasible. return IISData( @@ -4615,11 +4575,9 @@ function getfirstiis(model::Optimizer) UInt8[], ) end - # XPRESS' API works in two steps: first, retrieve the sizes of the arrays to # retrieve; then, the user is expected to allocate the needed memory, # before asking XPRESS to fill it. - num = Cint(1) rownumber = Ref{Cint}(0) colnumber = Ref{Cint}(0) @@ -4637,7 +4595,6 @@ function getfirstiis(model::Optimizer) C_NULL, C_NULL, ) - nrows = rownumber[] ncols = colnumber[] miisrow = Vector{Cint}(undef, nrows) @@ -4658,7 +4615,6 @@ function getfirstiis(model::Optimizer) C_NULL, C_NULL, ) - return IISData( status_code[], true, @@ -4683,6 +4639,7 @@ function _ensure_conflict_computed(model::Optimizer) "In case the model is modified, the computed conflict will not be purged.", ) end + return end function MOI.get(model::Optimizer, ::MOI.ConflictStatus) @@ -4697,14 +4654,9 @@ function MOI.get(model::Optimizer, ::MOI.ConflictStatus) # stat == 2 -> error # stat == 3 -> timeout return MOI.NO_CONFLICT_FOUND - # return error("IIS failed internally.") end end -function MOI.supports(::Optimizer, ::MOI.ConflictStatus) - return true -end - function MOI.get( model::Optimizer, ::MOI.ConstraintConflictStatus, @@ -4714,8 +4666,10 @@ function MOI.get( }, ) _ensure_conflict_computed(model) - return (_info(model, index).row - 1) in model.conflict.miisrow ? - MOI.IN_CONFLICT : MOI.NOT_IN_CONFLICT + if _info(model, index).row - 1 in model.conflict.miisrow + return MOI.IN_CONFLICT + end + return MOI.NOT_IN_CONFLICT end col_type_char(::Type{MOI.LessThan{Float64}}) = 'U' @@ -4726,6 +4680,7 @@ col_type_char(::Type{MOI.ZeroOne}) = 'B' col_type_char(::Type{MOI.Integer}) = 'I' col_type_char(::Type{MOI.Semicontinuous{Float64}}) = 'S' col_type_char(::Type{MOI.Semiinteger{Float64}}) = 'R' + function MOI.get( model::Optimizer, ::MOI.ConstraintConflictStatus, @@ -4744,6 +4699,7 @@ function MOI.get( end return MOI.NOT_IN_CONFLICT end + function MOI.get( model::Optimizer, ::MOI.ConstraintConflictStatus, @@ -4782,55 +4738,28 @@ function MOI.get( end return MOI.NOT_IN_CONFLICT end -function MOI.supports( - ::Optimizer, - ::MOI.ConstraintConflictStatus, - ::Type{MOI.ConstraintIndex{<:MOI.VariableIndex,<:SCALAR_SETS}}, -) - return true -end - -function MOI.supports( - ::Optimizer, - ::MOI.ConstraintConflictStatus, - ::Type{ - MOI.ConstraintIndex{ - <:MOI.ScalarAffineFunction, - <:Union{MOI.LessThan,MOI.GreaterThan,MOI.EqualTo}, - }, - }, -) - return true -end - -include("MOI_callbacks.jl") -function extension(str::String) - try - match(r"\.[A-Za-z0-9]+$", str).match - catch - "" - end -end +#= + MOI.write_to_file +=# function MOI.write_to_file(model::Optimizer, name::String) - ext = extension(name) - if ext == ".lp" - @checked Lib.XPRSwriteprob(model.inner, name, "l") - elseif ext == ".mps" - @checked Lib.XPRSwriteprob(model.inner, name, "") - else - @checked Lib.XPRSwriteprob(model.inner, name, "l") - end + flag = endswith(name, ".mps") ? "" : "l" + @checked Lib.XPRSwriteprob(model.inner, name, flag) + return end -function _pass_names_to_solver(model::Xpress.Optimizer; warn = true) +#= + _pass_names_to_solver +=# + +function _pass_names_to_solver(model::Optimizer; warn = true) _pass_variable_names_to_solver(model; warn = warn) _pass_constraint_names_to_solver(model; warn = warn) - return nothing + return end -function _pass_variable_names_to_solver(model::Xpress.Optimizer; warn = true) +function _pass_variable_names_to_solver(model::Optimizer; warn = true) NAMELENGTH = 64 n_variables = length(model.variable_info) if n_variables == 0 @@ -4866,7 +4795,7 @@ function _pass_variable_names_to_solver(model::Xpress.Optimizer; warn = true) return nothing end -function _pass_constraint_names_to_solver(model::Xpress.Optimizer; warn = true) +function _pass_constraint_names_to_solver(model::Optimizer; warn = true) NAMELENGTH = 64 n_constraints = length(model.affine_constraint_info) if n_constraints == 0 @@ -4975,11 +4904,11 @@ end MOI.supports(::Optimizer, ::MOI.RelativeGapTolerance) = true function MOI.get(model::Optimizer, ::MOI.RelativeGapTolerance) - return Xpress.getcontrol(model.inner, Lib.XPRS_MIPRELSTOP) + return getcontrol(model.inner, Lib.XPRS_MIPRELSTOP) end function MOI.set(model::Optimizer, ::MOI.RelativeGapTolerance, value::Float64) - Xpress.setcontrol!(model.inner, Lib.XPRS_MIPRELSTOP, value) + setcontrol!(model.inner, Lib.XPRS_MIPRELSTOP, value) return end @@ -4990,10 +4919,10 @@ end MOI.supports(::Optimizer, ::MOI.AbsoluteGapTolerance) = true function MOI.get(model::Optimizer, ::MOI.AbsoluteGapTolerance) - return Xpress.getcontrol(model.inner, Lib.XPRS_MIPABSSTOP) + return getcontrol(model.inner, Lib.XPRS_MIPABSSTOP) end function MOI.set(model::Optimizer, ::MOI.AbsoluteGapTolerance, value::Float64) - Xpress.setcontrol!(model.inner, Lib.XPRS_MIPABSSTOP, value) + setcontrol!(model.inner, Lib.XPRS_MIPABSSTOP, value) return end diff --git a/src/Xpress.jl b/src/Xpress.jl index ee0e5e75..bbca64d0 100644 --- a/src/Xpress.jl +++ b/src/Xpress.jl @@ -56,6 +56,7 @@ function initialize() end include("MOI/MOI_wrapper.jl") +include("MOI/MOI_callbacks.jl") function __init__() if !haskey(ENV, "XPRESS_JL_NO_AUTO_INIT") &&