diff --git a/.github/workflows/format_check.yml b/.github/workflows/format_check.yml index bd0c393..07c71ea 100644 --- a/.github/workflows/format_check.yml +++ b/.github/workflows/format_check.yml @@ -18,7 +18,7 @@ jobs: shell: julia --color=yes {0} run: | using Pkg - Pkg.add(PackageSpec(name="JuliaFormatter", version="0.13.2")) + Pkg.add(PackageSpec(name="JuliaFormatter", version="0.15.11")) using JuliaFormatter format(".", verbose=true) out = String(read(Cmd(`git diff`))) diff --git a/Project.toml b/Project.toml index 4f2719a..48f79b2 100644 --- a/Project.toml +++ b/Project.toml @@ -5,11 +5,12 @@ version = "0.2.3" [deps] HiGHS_jll = "8fd58aa0-07eb-5a78-9b36-339c94fd15ea" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [compat] HiGHS_jll = "=0.3.2" -MathOptInterface = "0.9.17" +MathOptInterface = "~0.10" julia = "1.3" [extras] diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 1a47862..a0455b6 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -112,30 +112,12 @@ mutable struct _VariableInfo upper::Float64 # Track integrality type::_TypeEnum - # We can perform an optimization and only store two strings for the - # constraint names because, at most, there can be two SingleVariable - # constraints, e.g., LessThan, GreaterThan. - lessthan_name::String - greaterthan_interval_or_equalto_name::String - integrality_name::String - function _VariableInfo( index::MOI.VariableIndex, column::Cint, bound::_BoundEnum = _BOUND_NONE, ) - return new( - index, - "", - column, - bound, - -Inf, - Inf, - _TYPE_CONTINUOUS, - "", - "", - "", - ) + return new(index, "", column, bound, -Inf, Inf, _TYPE_CONTINUOUS) end end @@ -181,8 +163,8 @@ end function _variable_info_dict() return CleverDicts.CleverDict{MOI.VariableIndex,_VariableInfo}( - x::MOI.VariableIndex -> x.value, - x::Int64 -> MOI.VariableIndex(x), + x -> x.value, + i -> MOI.VariableIndex(i), ) end @@ -213,8 +195,8 @@ end function _constraint_info_dict() return CleverDicts.CleverDict{_ConstraintKey,_ConstraintInfo}( - x::_ConstraintKey -> x.value, - x::Int64 -> _ConstraintKey(x), + c -> c.value, + i -> _ConstraintKey(i), ) end @@ -237,6 +219,7 @@ function _set(c::_ConstraintInfo) end @enum(_OptimizeStatus, _OPTIMIZE_NOT_CALLED, _OPTIMIZE_OK, _OPTIMIZE_ERRORED) + """ _Solution @@ -302,6 +285,8 @@ mutable struct Optimizer <: MOI.AbstractOptimizer # MIN_SENSE or MAX_SENSE. This allows us to differentiate between MIN_SENSE # and FEASIBILITY_SENSE. is_feasibility::Bool + is_objective_function_set::Bool + is_objective_sense_set::Bool # HiGHS doesn't have special support for binary variables. Cache them here # to modify bounds on solve. @@ -335,6 +320,8 @@ mutable struct Optimizer <: MOI.AbstractOptimizer Dict{String,Any}(), "", true, + false, + false, Set{_VariableInfo}(), _variable_info_dict(), _constraint_info_dict(), @@ -375,15 +362,20 @@ function MOI.empty!(model::Optimizer) end model.inner = Highs_create() for (key, value) in model.options - MOI.set(model, MOI.RawParameter(key), value) + MOI.set(model, MOI.RawOptimizerAttribute(key), value) end + model.name = "" model.is_feasibility = true + model.is_objective_function_set = false + model.is_objective_sense_set = false empty!(model.binaries) empty!(model.variable_info) empty!(model.affine_constraint_info) model.name_to_variable = nothing model.name_to_constraint_index = nothing empty!(model.solution) + ret = Highs_changeObjectiveOffset(model, 0.0) + _check_ret(ret) return end @@ -394,6 +386,8 @@ function MOI.is_empty(model::Optimizer) return Highs_getNumCols(model) == 0 && Highs_getNumRows(model) == 0 && model.is_feasibility && + !model.is_objective_function_set && + !model.is_objective_sense_set && isempty(model.binaries) && isempty(model.variable_info) && isempty(model.affine_constraint_info) && @@ -412,18 +406,31 @@ function MOI.get(::Optimizer, ::MOI.ListOfVariableAttributesSet) end function MOI.get(model::Optimizer, ::MOI.ListOfModelAttributesSet) - attributes = [ - MOI.ObjectiveSense(), - MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - ] - if MOI.get(model, MOI.Name()) != "" + attributes = MOI.AbstractModelAttribute[] + if model.is_objective_sense_set + push!(attributes, MOI.ObjectiveSense()) + end + if model.is_objective_function_set + push!( + attributes, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + ) + end + if !isempty(model.name) push!(attributes, MOI.Name()) end return attributes end -function MOI.get(::Optimizer, ::MOI.ListOfConstraintAttributesSet) - return MOI.AbstractConstraintAttribute[MOI.ConstraintName()] +function MOI.get( + ::Optimizer, + ::MOI.ListOfConstraintAttributesSet{F,S}, +) where {F,S} + attributes = MOI.AbstractConstraintAttribute[] + if F != MOI.VariableIndex + return push!(attributes, MOI.ConstraintName()) + end + return attributes end function MOI.get(model::Optimizer, ::MOI.NumberOfConstraints{F,S}) where {F,S} @@ -431,27 +438,27 @@ function MOI.get(model::Optimizer, ::MOI.NumberOfConstraints{F,S}) where {F,S} return length(MOI.get(model, MOI.ListOfConstraintIndices{F,S}())) end -function MOI.get(model::Optimizer, ::MOI.ListOfConstraints) - constraints = Set{Tuple{DataType,DataType}}() +function MOI.get(model::Optimizer, ::MOI.ListOfConstraintTypesPresent) + constraints = Set{Tuple{Type,Type}}() for info in values(model.variable_info) if info.bound == _BOUND_NONE elseif info.bound == _BOUND_LESS_THAN - push!(constraints, (MOI.SingleVariable, MOI.LessThan{Float64})) + push!(constraints, (MOI.VariableIndex, MOI.LessThan{Float64})) elseif info.bound == _BOUND_GREATER_THAN - push!(constraints, (MOI.SingleVariable, MOI.GreaterThan{Float64})) + push!(constraints, (MOI.VariableIndex, MOI.GreaterThan{Float64})) elseif info.bound == _BOUND_LESS_AND_GREATER_THAN - push!(constraints, (MOI.SingleVariable, MOI.LessThan{Float64})) - push!(constraints, (MOI.SingleVariable, MOI.GreaterThan{Float64})) + push!(constraints, (MOI.VariableIndex, MOI.LessThan{Float64})) + push!(constraints, (MOI.VariableIndex, MOI.GreaterThan{Float64})) elseif info.bound == _BOUND_EQUAL_TO - push!(constraints, (MOI.SingleVariable, MOI.EqualTo{Float64})) + push!(constraints, (MOI.VariableIndex, MOI.EqualTo{Float64})) else @assert info.bound == _BOUND_INTERVAL - push!(constraints, (MOI.SingleVariable, MOI.Interval{Float64})) + push!(constraints, (MOI.VariableIndex, MOI.Interval{Float64})) end if info.type == _TYPE_INTEGER - push!(constraints, (MOI.SingleVariable, MOI.Integer)) + push!(constraints, (MOI.VariableIndex, MOI.Integer)) elseif info.type == _TYPE_BINARY - push!(constraints, (MOI.SingleVariable, MOI.ZeroOne)) + push!(constraints, (MOI.VariableIndex, MOI.ZeroOne)) end end for info in values(model.affine_constraint_info) @@ -464,13 +471,10 @@ function MOI.get(model::Optimizer, ::MOI.ListOfConstraints) end ### -### MOI.RawParameter +### MOI.RawOptimizerAttribute ### -function MOI.supports(model::Optimizer, param::MOI.RawParameter) - if !(param.name isa String) - return false - end +function MOI.supports(model::Optimizer, param::MOI.RawOptimizerAttribute) typeP = Ref{Cint}() return Highs_getOptionType(model, param.name, typeP) == 0 end @@ -498,7 +502,7 @@ function _set_option(model::Optimizer, option::String, value::String) return Highs_setStringOptionValue(model, option, value) end -function MOI.set(model::Optimizer, param::MOI.RawParameter, value) +function MOI.set(model::Optimizer, param::MOI.RawOptimizerAttribute, value) if !MOI.supports(model, param) throw(MOI.UnsupportedAttribute(param)) end @@ -540,7 +544,7 @@ function _get_string_option(model::Optimizer, option::String) end end -function MOI.get(model::Optimizer, param::MOI.RawParameter) +function MOI.get(model::Optimizer, param::MOI.RawOptimizerAttribute) if !(param.name isa String) throw(MOI.UnsupportedAttribute(param)) end @@ -567,15 +571,19 @@ end MOI.supports(::Optimizer, ::MOI.TimeLimitSec) = true function MOI.set(model::Optimizer, ::MOI.TimeLimitSec, ::Nothing) - return MOI.set(model, MOI.RawParameter("time_limit"), Inf) + return MOI.set(model, MOI.RawOptimizerAttribute("time_limit"), Inf) end function MOI.set(model::Optimizer, ::MOI.TimeLimitSec, limit::Real) - return MOI.set(model, MOI.RawParameter("time_limit"), Float64(limit)) + return MOI.set( + model, + MOI.RawOptimizerAttribute("time_limit"), + Float64(limit), + ) end function MOI.get(model::Optimizer, ::MOI.TimeLimitSec) - return MOI.get(model, MOI.RawParameter("time_limit")) + return MOI.get(model, MOI.RawOptimizerAttribute("time_limit")) end ### @@ -585,11 +593,11 @@ end MOI.supports(::Optimizer, ::MOI.Silent) = true function MOI.get(model::Optimizer, ::MOI.Silent) - return !MOI.get(model, MOI.RawParameter("output_flag")) + return !MOI.get(model, MOI.RawOptimizerAttribute("output_flag")) end function MOI.set(model::Optimizer, ::MOI.Silent, flag::Bool) - MOI.set(model, MOI.RawParameter("output_flag"), !flag) + MOI.set(model, MOI.RawOptimizerAttribute("output_flag"), !flag) return end @@ -779,7 +787,7 @@ function MOI.set( num_vars = length(model.variable_info) obj = zeros(Float64, num_vars) for term in f.terms - col = column(model, term.variable_index) + col = column(model, term.variable) obj[col+1] += term.coefficient end # TODO(odow): cache the mask. @@ -845,6 +853,7 @@ function MOI.modify( ) ret = Highs_changeObjectiveOffset(model, chg.new_constant) _check_ret(ret) + model.is_objective_function_set = true return end @@ -859,16 +868,17 @@ function MOI.modify( chg.new_coefficient, ) _check_ret(ret) + model.is_objective_function_set = true return end ### -### SingleVariable-in-Set constraints. +### VariableIndex-in-Set constraints. ### function MOI.supports_constraint( ::Optimizer, - ::Type{MOI.SingleVariable}, + ::Type{MOI.VariableIndex}, ::Type{<:_SCALAR_SETS}, ) return true @@ -888,10 +898,10 @@ _bound_enums(::Type{MOI.EqualTo{Float64}}) = (_BOUND_EQUAL_TO,) function MOI.get( model::Optimizer, - ::MOI.ListOfConstraintIndices{MOI.SingleVariable,S}, + ::MOI.ListOfConstraintIndices{MOI.VariableIndex,S}, ) where {S<:_SCALAR_SETS} - indices = MOI.ConstraintIndex{MOI.SingleVariable,S}[ - MOI.ConstraintIndex{MOI.SingleVariable,S}(key.value) for + indices = MOI.ConstraintIndex{MOI.VariableIndex,S}[ + MOI.ConstraintIndex{MOI.VariableIndex,S}(key.value) for (key, info) in model.variable_info if info.bound in _bound_enums(S) ] return sort!(indices, by = x -> x.value) @@ -899,7 +909,7 @@ end function _info( model::Optimizer, - c::MOI.ConstraintIndex{MOI.SingleVariable,<:Any}, + c::MOI.ConstraintIndex{MOI.VariableIndex,<:Any}, ) var_index = MOI.VariableIndex(c.value) if haskey(model.variable_info, var_index) @@ -911,21 +921,21 @@ end """ column( model::Optimizer, - c::MOI.ConstraintIndex{MOI.SingleVariable,<:Any}, + c::MOI.ConstraintIndex{MOI.VariableIndex,<:Any}, ) Return the 0-indexed column associated with the variable bounds `c` in `model`. """ function column( model::Optimizer, - c::MOI.ConstraintIndex{MOI.SingleVariable,<:Any}, + c::MOI.ConstraintIndex{MOI.VariableIndex,<:Any}, ) return _info(model, c).column end function MOI.is_valid( model::Optimizer, - c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.LessThan{Float64}}, + c::MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}, ) if haskey(model.variable_info, MOI.VariableIndex(c.value)) info = _info(model, c) @@ -937,7 +947,7 @@ end function MOI.is_valid( model::Optimizer, - c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.GreaterThan{Float64}}, + c::MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}}, ) if haskey(model.variable_info, MOI.VariableIndex(c.value)) info = _info(model, c) @@ -949,7 +959,7 @@ end function MOI.is_valid( model::Optimizer, - c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.Interval{Float64}}, + c::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Interval{Float64}}, ) return haskey(model.variable_info, MOI.VariableIndex(c.value)) && _info(model, c).bound == _BOUND_INTERVAL @@ -957,7 +967,7 @@ end function MOI.is_valid( model::Optimizer, - c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.EqualTo{Float64}}, + c::MOI.ConstraintIndex{MOI.VariableIndex,MOI.EqualTo{Float64}}, ) return haskey(model.variable_info, MOI.VariableIndex(c.value)) && _info(model, c).bound == _BOUND_EQUAL_TO @@ -966,19 +976,19 @@ end function MOI.get( model::Optimizer, ::MOI.ConstraintFunction, - c::MOI.ConstraintIndex{MOI.SingleVariable,<:Any}, + c::MOI.ConstraintIndex{MOI.VariableIndex,<:Any}, ) MOI.throw_if_not_valid(model, c) - return MOI.SingleVariable(MOI.VariableIndex(c.value)) + return MOI.VariableIndex(c.value) end function MOI.set( ::Optimizer, ::MOI.ConstraintFunction, - ::MOI.ConstraintIndex{MOI.SingleVariable,<:Any}, - ::MOI.SingleVariable, + ::MOI.ConstraintIndex{MOI.VariableIndex,<:Any}, + ::MOI.VariableIndex, ) - return throw(MOI.SettingSingleVariableFunctionNotAllowed()) + return throw(MOI.SettingVariableIndexNotAllowed()) end function _throw_if_existing_lower( @@ -1015,19 +1025,19 @@ end function MOI.add_constraint( model::Optimizer, - f::MOI.SingleVariable, + f::MOI.VariableIndex, s::S, ) where {S<:_SCALAR_SETS} - info = _info(model, f.variable) + info = _info(model, f) _update_info(info, s) - index = MOI.ConstraintIndex{MOI.SingleVariable,typeof(s)}(f.variable.value) + index = MOI.ConstraintIndex{MOI.VariableIndex,typeof(s)}(f.value) MOI.set(model, MOI.ConstraintSet(), index, s) return index end function MOI.delete( model::Optimizer, - c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.LessThan{Float64}}, + c::MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}, ) MOI.throw_if_not_valid(model, c) info = _info(model, c) @@ -1039,14 +1049,13 @@ function MOI.delete( else info.bound = _BOUND_NONE end - info.lessthan_name = "" model.name_to_constraint_index = nothing return end function MOI.delete( model::Optimizer, - c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.GreaterThan{Float64}}, + c::MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}}, ) MOI.throw_if_not_valid(model, c) info = _info(model, c) @@ -1058,14 +1067,13 @@ function MOI.delete( else info.bound = _BOUND_NONE end - info.greaterthan_interval_or_equalto_name = "" model.name_to_constraint_index = nothing return end function MOI.delete( model::Optimizer, - c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.Interval{Float64}}, + c::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Interval{Float64}}, ) MOI.throw_if_not_valid(model, c) info = _info(model, c) @@ -1073,14 +1081,13 @@ function MOI.delete( _check_ret(ret) info.lower, info.upper = -Inf, Inf info.bound = _BOUND_NONE - info.greaterthan_interval_or_equalto_name = "" model.name_to_constraint_index = nothing return end function MOI.delete( model::Optimizer, - c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.EqualTo{Float64}}, + c::MOI.ConstraintIndex{MOI.VariableIndex,MOI.EqualTo{Float64}}, ) MOI.throw_if_not_valid(model, c) info = _info(model, c) @@ -1088,7 +1095,6 @@ function MOI.delete( _check_ret(ret) info.lower, info.upper = -Inf, Inf info.bound = _BOUND_NONE - info.greaterthan_interval_or_equalto_name = "" model.name_to_constraint_index = nothing return end @@ -1096,7 +1102,7 @@ end function MOI.get( model::Optimizer, ::MOI.ConstraintSet, - c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.GreaterThan{Float64}}, + c::MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}}, ) MOI.throw_if_not_valid(model, c) return MOI.GreaterThan(_info(model, c).lower) @@ -1105,7 +1111,7 @@ end function MOI.get( model::Optimizer, ::MOI.ConstraintSet, - c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.LessThan{Float64}}, + c::MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}, ) MOI.throw_if_not_valid(model, c) return MOI.LessThan(_info(model, c).upper) @@ -1114,7 +1120,7 @@ end function MOI.get( model::Optimizer, ::MOI.ConstraintSet, - c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.EqualTo{Float64}}, + c::MOI.ConstraintIndex{MOI.VariableIndex,MOI.EqualTo{Float64}}, ) MOI.throw_if_not_valid(model, c) return MOI.EqualTo(_info(model, c).lower) @@ -1123,7 +1129,7 @@ end function MOI.get( model::Optimizer, ::MOI.ConstraintSet, - c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.Interval{Float64}}, + c::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Interval{Float64}}, ) MOI.throw_if_not_valid(model, c) info = _info(model, c) @@ -1133,7 +1139,7 @@ end function MOI.set( model::Optimizer, ::MOI.ConstraintSet, - c::MOI.ConstraintIndex{MOI.SingleVariable,S}, + c::MOI.ConstraintIndex{MOI.VariableIndex,S}, s::S, ) where {S<:_SCALAR_SETS} MOI.throw_if_not_valid(model, c) @@ -1159,42 +1165,11 @@ end function MOI.supports( ::Optimizer, ::MOI.ConstraintName, - ::Type{<:MOI.ConstraintIndex}, -) + ::Type{MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},S}}, +) where {S} return true end -function MOI.get( - model::Optimizer, - ::MOI.ConstraintName, - c::MOI.ConstraintIndex{MOI.SingleVariable,S}, -) where {S<:_SCALAR_SETS} - MOI.throw_if_not_valid(model, c) - info = _info(model, c) - if S <: MOI.LessThan - return info.lessthan_name - else - return info.greaterthan_interval_or_equalto_name - end -end - -function MOI.set( - model::Optimizer, - ::MOI.ConstraintName, - c::MOI.ConstraintIndex{MOI.SingleVariable,S}, - name::String, -) where {S<:_SCALAR_SETS} - MOI.throw_if_not_valid(model, c) - info = _info(model, c) - if S <: MOI.LessThan - info.lessthan_name = name - else - info.greaterthan_interval_or_equalto_name = name - end - model.name_to_constraint_index = nothing - return -end - ### ### ScalarAffineFunction-in-Set ### @@ -1256,7 +1231,7 @@ function _indices_and_coefficients( ) i = 1 for term in f.terms - indices[i] = column(model, term.variable_index) + indices[i] = column(model, term.variable) coefficients[i] = term.coefficient i += 1 end @@ -1461,34 +1436,6 @@ function _rebuild_name_to_constraint_index(model::Optimizer) MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},S}(key.value), ) end - for (key, info) in model.variable_info - if !isempty(info.lessthan_name) - _set_name_to_constraint_index( - model, - info.lessthan_name, - MOI.ConstraintIndex{MOI.SingleVariable,MOI.LessThan{Float64}}( - key.value, - ), - ) - end - if !isempty(info.greaterthan_interval_or_equalto_name) - S = if info.bound == _BOUND_GREATER_THAN - MOI.GreaterThan{Float64} - elseif info.bound == _BOUND_LESS_AND_GREATER_THAN - MOI.GreaterThan{Float64} - elseif info.bound == _BOUND_EQUAL_TO - MOI.EqualTo{Float64} - else - @assert info.bound == _BOUND_INTERVAL - MOI.Interval{Float64} - end - _set_name_to_constraint_index( - model, - info.greaterthan_interval_or_equalto_name, - MOI.ConstraintIndex{MOI.SingleVariable,S}(key.value), - ) - end - end return end @@ -1534,9 +1481,9 @@ function MOI.set( # complains but it will require some upstream changes to introduce a faster # way of modifying a list of coefficients. old = MOI.get(model, MOI.ConstraintFunction(), c) - terms = Dict(x.variable_index => 0.0 for x in old.terms) + terms = Dict(x.variable => 0.0 for x in old.terms) for term in f.terms - terms[term.variable_index] = term.coefficient + terms[term.variable] = term.coefficient end r = row(model, c) for (k, v) in terms @@ -1676,7 +1623,7 @@ function MOI.get(model::Optimizer, ::MOI.RawStatusString) end function MOI.get(model::Optimizer, attr::MOI.PrimalStatus) - if attr.N != 1 + if attr.result_index != 1 return MOI.NO_SOLUTION elseif model.solution.has_primal_solution return MOI.FEASIBLE_POINT @@ -1687,7 +1634,7 @@ function MOI.get(model::Optimizer, attr::MOI.PrimalStatus) end function MOI.get(model::Optimizer, attr::MOI.DualStatus) - if attr.N != 1 + if attr.result_index != 1 return MOI.NO_SOLUTION elseif model.solution.has_dual_solution return MOI.FEASIBLE_POINT @@ -1718,7 +1665,7 @@ function MOI.get(model::Optimizer, ::MOI.ObjectiveBound) return sense == -1 ? max(primal, -p[]) : min(primal, p[]) end -function MOI.get(model::Optimizer, ::MOI.SolveTime) +function MOI.get(model::Optimizer, ::MOI.SolveTimeSec) return Highs_getRunTime(model) end @@ -1738,7 +1685,7 @@ end function MOI.get( model::Optimizer, attr::MOI.ConstraintPrimal, - c::MOI.ConstraintIndex{MOI.SingleVariable,<:_SCALAR_SETS}, + c::MOI.ConstraintIndex{MOI.VariableIndex,<:_SCALAR_SETS}, ) MOI.check_result_index_bounds(model, attr) return MOI.get(model, MOI.VariablePrimal(), MOI.VariableIndex(c.value)) @@ -1842,7 +1789,7 @@ _signed_dual(dual::Float64, ::Any, ::HighsBasisStatus) = dual function MOI.get( model::Optimizer, attr::MOI.ConstraintDual, - c::MOI.ConstraintIndex{MOI.SingleVariable,S}, + c::MOI.ConstraintIndex{MOI.VariableIndex,S}, ) where {S<:_SCALAR_SETS} MOI.check_result_index_bounds(model, attr) col = column(model, c) @@ -1900,33 +1847,27 @@ function MOI.get( return MOI.BASIC elseif stat == kUpper return _nonbasic_status(stat, S) - elseif stat == kZero || stat == kNonbasic - return MOI.NONBASIC - else - @assert stat == SUPER - return MOI.SUPER_BASIC end + @assert stat == kZero || stat == kNonbasic + return MOI.NONBASIC end function MOI.get( model::Optimizer, - attr::MOI.ConstraintBasisStatus, - c::MOI.ConstraintIndex{MOI.SingleVariable,S}, -) where {S<:_SCALAR_SETS} + attr::MOI.VariableBasisStatus, + x::MOI.VariableIndex, +) MOI.check_result_index_bounds(model, attr) - stat = HighsBasisStatus(model.solution.colstatus[column(model, c)+1]) + stat = HighsBasisStatus(model.solution.colstatus[column(model, x)+1]) if stat == kLower - return _nonbasic_status(stat, S) + return MOI.NONBASIC_AT_LOWER elseif stat == kBasic return MOI.BASIC elseif stat == kUpper - return _nonbasic_status(stat, S) - elseif stat == kZero || stat == kNonbasic - return MOI.NONBASIC - else - @assert stat == SUPER - return MOI.SUPER_BASIC + return MOI.NONBASIC_AT_UPPER end + @assert stat == kZero || stat == kNonbasic + return MOI.NONBASIC end ### @@ -1934,33 +1875,34 @@ end ### function MOI.supports_constraint( - ::Optimizer, - ::Type{MOI.SingleVariable}, + model::Optimizer, + ::Type{MOI.VariableIndex}, ::Type{<:Union{MOI.ZeroOne,MOI.Integer}}, ) - return true + solver = MOI.get(model, MOI.RawOptimizerAttribute("solver")) + return solver != "simplex" && solver != "ipm" end function MOI.is_valid( model::Optimizer, - ci::MOI.ConstraintIndex{MOI.SingleVariable,MOI.Integer}, + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Integer}, ) return _info(model, ci).type == _TYPE_INTEGER end function MOI.is_valid( model::Optimizer, - ci::MOI.ConstraintIndex{MOI.SingleVariable,MOI.ZeroOne}, + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}, ) return _info(model, ci).type == _TYPE_BINARY end function MOI.get( model::Optimizer, - ::MOI.ListOfConstraintIndices{MOI.SingleVariable,MOI.Integer}, + ::MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.Integer}, ) - indices = MOI.ConstraintIndex{MOI.SingleVariable,MOI.Integer}[ - MOI.ConstraintIndex{MOI.SingleVariable,MOI.Integer}(key.value) for + indices = MOI.ConstraintIndex{MOI.VariableIndex,MOI.Integer}[ + MOI.ConstraintIndex{MOI.VariableIndex,MOI.Integer}(key.value) for (key, info) in model.variable_info if info.type == _TYPE_INTEGER ] return sort!(indices, by = x -> x.value) @@ -1968,10 +1910,10 @@ end function MOI.get( model::Optimizer, - ::MOI.ListOfConstraintIndices{MOI.SingleVariable,MOI.ZeroOne}, + ::MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.ZeroOne}, ) - indices = MOI.ConstraintIndex{MOI.SingleVariable,MOI.ZeroOne}[ - MOI.ConstraintIndex{MOI.SingleVariable,MOI.ZeroOne}(key.value) for + indices = MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}[ + MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}(key.value) for (key, info) in model.variable_info if info.type == _TYPE_BINARY ] return sort!(indices, by = x -> x.value) @@ -1979,24 +1921,24 @@ end function MOI.add_constraint( model::Optimizer, - f::MOI.SingleVariable, + f::MOI.VariableIndex, ::MOI.Integer, ) - info = _info(model, f.variable) + info = _info(model, f) info.type = _TYPE_INTEGER - ci = MOI.ConstraintIndex{MOI.SingleVariable,MOI.Integer}(f.variable.value) + ci = MOI.ConstraintIndex{MOI.VariableIndex,MOI.Integer}(f.value) Highs_changeColIntegrality(model, info.column, kInteger) return ci end function MOI.add_constraint( model::Optimizer, - f::MOI.SingleVariable, + f::MOI.VariableIndex, ::MOI.ZeroOne, ) - info = _info(model, f.variable) + info = _info(model, f) info.type = _TYPE_BINARY - ci = MOI.ConstraintIndex{MOI.SingleVariable,MOI.ZeroOne}(f.variable.value) + ci = MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}(f.value) Highs_changeColIntegrality(model, info.column, kInteger) push!(model.binaries, info) return ci @@ -2005,7 +1947,7 @@ end function MOI.get( model::Optimizer, ::MOI.ConstraintSet, - ci::MOI.ConstraintIndex{MOI.SingleVariable,MOI.Integer}, + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Integer}, ) MOI.throw_if_not_valid(model, ci) return MOI.Integer() @@ -2014,7 +1956,7 @@ end function MOI.get( model::Optimizer, ::MOI.ConstraintSet, - ci::MOI.ConstraintIndex{MOI.SingleVariable,MOI.ZeroOne}, + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}, ) MOI.throw_if_not_valid(model, ci) return MOI.ZeroOne() @@ -2022,7 +1964,7 @@ end function MOI.delete( model::Optimizer, - ci::MOI.ConstraintIndex{MOI.SingleVariable,MOI.Integer}, + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.Integer}, ) MOI.throw_if_not_valid(model, ci) info = _info(model, ci) @@ -2033,7 +1975,7 @@ end function MOI.delete( model::Optimizer, - ci::MOI.ConstraintIndex{MOI.SingleVariable,MOI.ZeroOne}, + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}, ) MOI.throw_if_not_valid(model, ci) info = _info(model, ci) @@ -2043,34 +1985,6 @@ function MOI.delete( return end -function MOI.set( - model::Optimizer, - ::MOI.ConstraintName, - ci::MOI.ConstraintIndex{ - MOI.SingleVariable, - <:Union{MOI.Integer,MOI.ZeroOne}, - }, - name::String, -) - MOI.throw_if_not_valid(model, ci) - info = _info(model, ci) - info.integrality_name = name - return -end - -function MOI.get( - model::Optimizer, - ::MOI.ConstraintName, - ci::MOI.ConstraintIndex{ - MOI.SingleVariable, - <:Union{MOI.Integer,MOI.ZeroOne}, - }, -) - MOI.throw_if_not_valid(model, ci) - info = _info(model, ci) - return info.integrality_name -end - ### ### MOI.copy_to ### @@ -2095,6 +2009,8 @@ function _add_bounds(lb, ub, i, s::MOI.Interval{Float64}) return end +_constraints(src, F, S) = MOI.get(src, MOI.ListOfConstraintIndices{F,S}()) + function _extract_bound_data( dest::Optimizer, src::MOI.ModelLike, @@ -2103,16 +2019,14 @@ function _extract_bound_data( colupper::Vector{Float64}, ::Type{S}, ) where {S} - for c_index in - MOI.get(src, MOI.ListOfConstraintIndices{MOI.SingleVariable,S}()) + for c_index in _constraints(src, MOI.VariableIndex, S) f = MOI.get(src, MOI.ConstraintFunction(), c_index) s = MOI.get(src, MOI.ConstraintSet(), c_index) - new_f = mapping.varmap[f.variable] + new_f = mapping[f] info = _info(dest, new_f) _add_bounds(collower, colupper, info.column + 1, s) _update_info(info, s) - mapping.conmap[c_index] = - MOI.ConstraintIndex{MOI.SingleVariable,S}(new_f.value) + mapping[c_index] = MOI.ConstraintIndex{MOI.VariableIndex,S}(new_f.value) end return end @@ -2120,22 +2034,22 @@ end function _copy_to_columns(dest::Optimizer, src::MOI.ModelLike, mapping) x_src = MOI.get(src, MOI.ListOfVariableIndices()) numcols = Cint(length(x_src)) - for i in 1:numcols + for (i, x) in enumerate(x_src) index = CleverDicts.add_item( dest.variable_info, _VariableInfo(MOI.VariableIndex(0), Cint(0)), ) info = _info(dest, index) - info.name = MOI.get(dest, MOI.VariableName(), x_src[i]) + info.name = MOI.get(dest, MOI.VariableName(), x) info.index = index info.column = Cint(i - 1) - mapping.varmap[x_src[i]] = index + mapping[x] = index end fobj = MOI.get(src, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()) c = fill(0.0, numcols) for term in fobj.terms - i = mapping.varmap[term.variable_index].value + i = mapping[term.variable].value c[i] += term.coefficient end return numcols, c, fobj.constant @@ -2155,10 +2069,7 @@ function _extract_row_data( ::Type{S}, ) where {S} row = length(I) == 0 ? 1 : I[end] + 1 - list = MOI.get( - src, - MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{Float64},S}(), - ) + list = _constraints(src, MOI.ScalarAffineFunction{Float64}, S) numrows = length(list) _add_sizehint!(rowlower, numrows) _add_sizehint!(rowupper, numrows) @@ -2178,7 +2089,7 @@ function _extract_row_data( ) dest.affine_constraint_info[key].row = Cint(length(dest.affine_constraint_info) - 1) - mapping.conmap[c_index] = + mapping[c_index] = MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},S}(key.value) end _add_sizehint!(I, n_terms) @@ -2187,7 +2098,7 @@ function _extract_row_data( for f in fs for term in f.terms push!(I, row) - push!(J, Cint(mapping.varmap[term.variable_index].value)) + push!(J, Cint(mapping[term.variable].value)) push!(V, term.coefficient) end row += 1 @@ -2196,7 +2107,7 @@ function _extract_row_data( end function _check_input_data(dest::Optimizer, src::MOI.ModelLike) - for (F, S) in MOI.get(src, MOI.ListOfConstraints()) + for (F, S) in MOI.get(src, MOI.ListOfConstraintTypesPresent()) if !MOI.supports_constraint(dest, F, S) throw( MOI.UnsupportedConstraint{F,S}( @@ -2212,23 +2123,10 @@ function _check_input_data(dest::Optimizer, src::MOI.ModelLike) return end -MOI.Utilities.supports_default_copy_to(::Optimizer, ::Bool) = true +MOI.supports_incremental_interface(::Optimizer) = true -function MOI.copy_to( - dest::Optimizer, - src::MOI.ModelLike; - copy_names::Bool = false, - kwargs..., -) - if copy_names - return MOI.Utilities.automatic_copy_to( - dest, - src; - copy_names = true, - kwargs..., - ) - end - @assert MOI.is_empty(dest) +function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) + MOI.empty!(dest) _check_input_data(dest, src) mapping = MOI.Utilities.IndexMap() numcol, colcost, offset = _copy_to_columns(dest, src, mapping) @@ -2246,32 +2144,30 @@ function MOI.copy_to( end numrow = Cint(length(rowlower)) A = SparseArrays.sparse(I, J, V, numrow, numcol) + # Extract integrality constraints integrality = fill(kContinuous, numcol) - for ci in MOI.get( - src, - MOI.ListOfConstraintIndices{MOI.SingleVariable,MOI.ZeroOne}(), - ) + for ci in _constraints(src, MOI.VariableIndex, MOI.ZeroOne) + integrality[_info(dest, ci).column+1] = kInteger new_x = mapping[MOI.VariableIndex(ci.value)] - integrality[_info(dest, new_x).column+1] = kInteger mapping[ci] = typeof(ci)(new_x.value) push!(dest.binaries, _info(dest, mapping[ci])) end - for ci in MOI.get( - src, - MOI.ListOfConstraintIndices{MOI.SingleVariable,MOI.Integer}(), - ) + for ci in _constraints(src, MOI.VariableIndex, MOI.Integer) + integrality[_info(dest, ci).column+1] = kInteger new_x = mapping[MOI.VariableIndex(ci.value)] - integrality[_info(dest, new_x).column+1] = kInteger mapping[ci] = typeof(ci)(new_x.value) end - ret = Highs_passMip( + # Build the model + is_max = MOI.get(src, MOI.ObjectiveSense()) == MOI.MAX_SENSE + ret = Highs_passModel( dest, numcol, numrow, - length(V), - kColwise, # The A matrix is given is column-wise. - MOI.get(src, MOI.ObjectiveSense()) == MOI.MAX_SENSE ? kMaximize : - kMinimize, + length(A.nzval), + 0, # q_num_nz, + kColwise, # a_format, + 0, # q_format, + is_max ? kMaximize : kMinimize, offset, colcost, collower, @@ -2281,6 +2177,9 @@ function MOI.copy_to( A.colptr .- Cint(1), A.rowval .- Cint(1), A.nzval, + Cint[0], # qstart, + Cint[], # qindex, + Cdouble[], # qvalue, integrality, ) _check_ret(ret) diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index 84e269f..8d97b82 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -6,107 +6,31 @@ using Test const MOI = HiGHS.MOI function runtests() - config = Dict( - "simplex" => MOI.Test.TestConfig(basis = true), - "ipm" => - MOI.Test.TestConfig(basis = true, infeas_certificates = false), - "mip" => MOI.Test.TestConfig(infeas_certificates = false), - ) for name in names(@__MODULE__; all = true) - if !startswith("$name", "test_") - continue - end - s = startswith("$name", "test_int_") ? ["mip"] : ["simplex", "ipm"] - @testset "$name-$solver" for solver in s - model = optimizer(solver = solver) - getfield(@__MODULE__, name)(model, config[solver]) + if startswith("$name", "test_") + @testset "$name" begin + getfield(@__MODULE__, name)() + end end end return end -function test_basic_constraint_tests(model, config) - return MOI.Test.basic_constraint_tests(model, config) -end - -function test_unittest(model, config) - return MOI.Test.unittest( - model, - config, - String[ - # TODO(odow): - # Add support for MOI.NumberOfThreads. - "number_threads", - - # These are excluded because HiGHS does not support them. - "delete_soc_variables", - "solve_qcp_edge_cases", - "solve_qp_edge_cases", - "solve_integer_edge_cases", - "solve_objbound_edge_cases", - "solve_zero_one_with_bounds_1", - "solve_zero_one_with_bounds_2", - "solve_zero_one_with_bounds_3", - ], - ) -end - -function test_int_unittest(model, config) - return MOI.Test.unittest( - model, - config, - String[ - # TODO(odow): - # Add support for MOI.NumberOfThreads. - "number_threads", - - # These are excluded because HiGHS does not support them. - "delete_soc_variables", - "solve_qcp_edge_cases", - "solve_qp_edge_cases", - ], - ) -end - -function test_modificationtest(model, config) - return MOI.Test.modificationtest(model, config) -end - -function test_contlineartest(model, config) - excludes = [ - # VariablePrimalStart not supported. - "partial_start", - ] - if MOI.get(model, MOI.RawParameter("solver")) == "ipm" - # TODO(odow): investigate - push!(excludes, "linear8b") - push!(excludes, "linear8c") - end - return MOI.Test.contlineartest(model, config, excludes) -end +const _EXCLUDE = [ + # TODO(odow): investigate test failures + "test_model_LowerBoundAlreadySet", + "test_model_UpperBoundAlreadySet", + "test_model_copy_to_UnsupportedAttribute", +] -function test_lintest(model, config) - return MOI.Test.lintest(model, config) -end - -function test_int_lineartest(model, config) - MOI.Test.intlineartest( - model, - config, - # Exclude SOS constraints - [ - "int2", - "indicator1", - "indicator2", - "indicator3", - "indicator4", - "semiconttest", - ], - ) +function test_runtests() + model = MOI.Bridges.full_bridge_optimizer(HiGHS.Optimizer(), Float64) + MOI.set(model, MOI.Silent(), true) + MOI.Test.runtests(model, MOI.Test.Config(), exclude = _EXCLUDE) return end -function test_int_lineartest_cache(::Any, config) +function test_runtests_cache() model = MOI.Bridges.full_bridge_optimizer( MOI.Utilities.CachingOptimizer( MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), @@ -115,121 +39,82 @@ function test_int_lineartest_cache(::Any, config) Float64, ) MOI.set(model, MOI.Silent(), true) - MOI.Test.intlineartest( - model, - config, - # Exclude SOS constraints - ["int2", "indicator1", "indicator2", "indicator3", "indicator4"], + MOI.Test.runtests( + model, + MOI.Test.Config(), + exclude = vcat( + _EXCLUDE, + "test_conic_NormInfinityCone_VectorAffineFunction", + "test_conic_NormInfinityCone_VectorOfVariables", + "test_conic_NormOneCone_VectorAffineFunction", + "test_conic_NormOneCone_VectorOfVariables", + # TODO(odow): investigate. These all seem related. + "test_linear_integration", + "test_linear_integration_Interval", + "test_linear_integration_delete_variables", + "test_modification_delete_variables_in_a_batch", + ), ) return end -function test_SolverName(model, ::Any) - @test MOI.get(model, MOI.SolverName()) == "HiGHS" -end - -function test_default_objective(model, ::Any) - return MOI.Test.default_objective_test(model) -end - -function test_default_status_test(model, ::Any) - return MOI.Test.default_status_test(model) -end - -function test_nametest(model, ::Any) - return MOI.Test.nametest(model) -end - -function test_validtest(model, ::Any) - return MOI.Test.validtest(model) -end - -function test_emptytest(model, ::Any) - return MOI.Test.emptytest(model) -end - -function test_orderedindicestest(model, ::Any) - return MOI.Test.orderedindicestest(model) -end - -function test_copytest(model, ::Any) - return MOI.Test.copytest( - model, - MOI.Bridges.full_bridge_optimizer(HiGHS.Optimizer(), Float64), - ) +function test_runtests_simplex() + model = MOI.Bridges.full_bridge_optimizer(HiGHS.Optimizer(), Float64) + MOI.set(model, MOI.Silent(), true) + MOI.set(model, MOI.RawOptimizerAttribute("solver"), "simplex") + for presolve in ("on", "off") + MOI.set(model, MOI.RawOptimizerAttribute("presolve"), presolve) + MOI.Test.runtests(model, MOI.Test.Config(), exclude = _EXCLUDE) + end + return end -function test_scalar_function_constant_not_zero(model, ::Any) - return MOI.Test.scalar_function_constant_not_zero(model) +function test_runtests_ipm() + model = MOI.Bridges.full_bridge_optimizer(HiGHS.Optimizer(), Float64) + MOI.set(model, MOI.Silent(), true) + MOI.set(model, MOI.RawOptimizerAttribute("solver"), "ipm") + return MOI.Test.runtests(model, MOI.Test.Config(), exclude = _EXCLUDE) end -function test_supports_constrainttest(model, ::Any) - # supports_constrainttest needs VectorOfVariables-in-Zeros, - # MOI.Test.supports_constrainttest(HiGHS.Optimizer(), Float64, Float32) - # but supports_constrainttest is broken via bridges: - MOI.empty!(model) - MOI.add_variable(model) - @test MOI.supports_constraint( - model, - MOI.SingleVariable, - MOI.EqualTo{Float64}, - ) - @test MOI.supports_constraint( - model, - MOI.ScalarAffineFunction{Float64}, - MOI.EqualTo{Float64}, - ) - # This test is broken for some reason: - @test_broken !MOI.supports_constraint( - model, - MOI.ScalarAffineFunction{Int}, - MOI.EqualTo{Float64}, - ) - @test !MOI.supports_constraint( - model, - MOI.ScalarAffineFunction{Int}, - MOI.EqualTo{Int}, - ) - @test !MOI.supports_constraint(model, MOI.SingleVariable, MOI.EqualTo{Int}) - @test MOI.supports_constraint(model, MOI.VectorOfVariables, MOI.Zeros) - @test !MOI.supports_constraint( - model, - MOI.VectorOfVariables, - MOI.EqualTo{Float64}, - ) - @test !MOI.supports_constraint(model, MOI.SingleVariable, MOI.Zeros) - @test !MOI.supports_constraint( - model, - MOI.VectorOfVariables, - MOI.Test.UnknownVectorSet, +function test_runtests_ipm_no_presolve() + model = MOI.Bridges.full_bridge_optimizer(HiGHS.Optimizer(), Float64) + MOI.set(model, MOI.Silent(), true) + MOI.set(model, MOI.RawOptimizerAttribute("solver"), "ipm") + MOI.set(model, MOI.RawOptimizerAttribute("presolve"), "off") + MOI.Test.runtests( + model, + MOI.Test.Config(), + exclude = vcat( + _EXCLUDE, + ["test_conic_linear_INFEASIBLE", "test_conic_linear_INFEASIBLE_2"], + ), ) + return end -function test_set_lower_bound_twice(::Any, ::Any) - return MOI.Test.set_lower_bound_twice(HiGHS.Optimizer(), Float64) -end - -function test_set_upper_bound_twice(::Any, ::Any) - return MOI.Test.set_upper_bound_twice(HiGHS.Optimizer(), Float64) +function test_SolverName() + @test MOI.get(HiGHS.Optimizer(), MOI.SolverName()) == "HiGHS" + return end -function test_Attributes(::Any, ::Any) +function test_attributes() model = HiGHS.Optimizer() @test MOI.get(model, MOI.SolverName()) == "HiGHS" @test MOI.get(model, MOI.TimeLimitSec()) > 10000 MOI.set(model, MOI.TimeLimitSec(), 500) @test MOI.get(model, MOI.TimeLimitSec()) == 500.0 @test MOI.get(model, MOI.RawSolver()) == model + return end -function test_MOI_variable_count_and_empty(model, ::Any) - MOI.empty!(model) +function test_MOI_variable_count_and_empty() + model = HiGHS.Optimizer() @test MOI.get(model, MOI.NumberOfVariables()) == 0 x1 = MOI.add_variable(model) @test MOI.get(model, MOI.NumberOfVariables()) == 1 @test MOI.supports_constraint( model, - MOI.SingleVariable, + MOI.VariableIndex, MOI.Interval{Float64}, ) x2, _ = MOI.add_constrained_variable(model, MOI.Interval(0.0, 1.0)) @@ -238,244 +123,41 @@ function test_MOI_variable_count_and_empty(model, ::Any) @test MOI.get(model, MOI.NumberOfVariables()) == 0 end -function test_Getting_objective_value(model, ::Any) - MOI.empty!(model) - MOI.set(model, MOI.Silent(), true) - (x, _) = MOI.add_constrained_variable(model, MOI.Interval(-3.0, 6.0)) - MOI.set( - model, - MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0), - ) - @test MOI.get(model, MOI.ObjectiveSense()) == MOI.FEASIBILITY_SENSE - MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) - @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MIN_SENSE - @test MOI.get(model, MOI.ResultCount()) == 0 - MOI.optimize!(model) - @test MOI.get(model, MOI.ResultCount()) == 1 - @test MOI.get(model, MOI.ObjectiveValue()) ≈ -3 -end - -function test_Max_in_box(model, ::Any) - MOI.empty!(model) - MOI.set(model, MOI.Silent(), true) - @test MOI.get(model, MOI.ResultCount()) == 0 - (x, _) = MOI.add_constrained_variable(model, MOI.Interval(-3.0, 6.0)) - MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) - MOI.set( - model, - MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(2.0, x)], 0.0), - ) - MOI.optimize!(model) - @test MOI.get(model, MOI.ObjectiveValue()) ≈ 2 * 6 - obj_func = MOI.get( - model, - MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - ) - @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL - @test obj_func ≈ - MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(2.0, x)], 0.0) -end - -function test_Objective_function_obtained_from_model_corresponds(model, ::Any) - MOI.empty!(model) - (x1, _) = MOI.add_constrained_variable(model, MOI.Interval(-3.0, 6.0)) - (x2, _) = MOI.add_constrained_variable(model, MOI.Interval(1.0, 2.0)) - MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) - MOI.set( - model, - MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - MOI.ScalarAffineFunction( - MOI.ScalarAffineTerm.([2.0, -1.0], [x1, x2]), - 0.0, - ), - ) - F = MOI.get(model, MOI.ObjectiveFunctionType()) - @test F <: MOI.ScalarAffineFunction{Float64} - obj_func = MOI.get(model, MOI.ObjectiveFunction{F}()) - @test MOI.supports(model, MOI.ObjectiveFunction{F}()) - @test all(MOI.get(model, MOI.ListOfVariableIndices()) .== [x1, x2]) - @test obj_func ≈ MOI.ScalarAffineFunction( - [MOI.ScalarAffineTerm(2.0, x1), MOI.ScalarAffineTerm(-1.0, x2)], - 0.0, - ) - MOI.set(model, MOI.ObjectiveFunction{F}(), obj_func) - obj_func = MOI.get(model, MOI.ObjectiveFunction{F}()) - @test obj_func ≈ MOI.ScalarAffineFunction( - [MOI.ScalarAffineTerm(2.0, x1), MOI.ScalarAffineTerm(-1.0, x2)], - 0.0, - ) - obj_func.terms[1] = MOI.ScalarAffineTerm(3.0, x1) - MOI.set(model, MOI.ObjectiveFunction{F}(), obj_func) - obj_func = MOI.get(model, MOI.ObjectiveFunction{F}()) - @test obj_func ≈ MOI.ScalarAffineFunction( - [MOI.ScalarAffineTerm(3.0, x1), MOI.ScalarAffineTerm(-1.0, x2)], - 0.0, - ) -end - -function test_Constrained_variable_equivalent_to_add_constraint(model, ::Any) - MOI.empty!(model) - MOI.set(model, MOI.Silent(), true) - x = MOI.add_variable(model) - _ = MOI.add_constraint( - model, - MOI.SingleVariable(x), - MOI.Interval(-3.0, 6.0), - ) - MOI.set( - model, - MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0), - ) - MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) - @test MOI.get(model, MOI.ResultCount()) == 0 - MOI.optimize!(model) - @test MOI.get(model, MOI.ResultCount()) == 1 - @test MOI.get(model, MOI.ObjectiveValue()) ≈ -3 -end - -function test_Constant_in_objective_function(model, ::Any) - MOI.empty!(model) - MOI.set(model, MOI.Silent(), true) - x = MOI.add_variable(model) - _ = MOI.add_constraint( - model, - MOI.SingleVariable(x), - MOI.Interval(-3.0, 6.0), - ) - MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) - obj_func = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 3.0) - MOI.set(model, MOI.ObjectiveFunction{typeof(obj_func)}(), obj_func) - MOI.optimize!(model) - @test MOI.get(model, MOI.ResultCount()) == 1 - @test MOI.get(model, MOI.ObjectiveValue()) ≈ 0 - obj_func = MOI.get(model, MOI.ObjectiveFunction{typeof(obj_func)}()) - @test MOI.constant(obj_func) ≈ 3 - MOI.set(model, MOI.ObjectiveSense(), MOI.FEASIBILITY_SENSE) - obj_func = MOI.get(model, MOI.ObjectiveFunction{typeof(obj_func)}()) - @test MOI.constant(obj_func) ≈ 0 - @test isempty(obj_func.terms) -end - -function test_Linear_constraints(model, ::Any) - MOI.empty!(model) - # max x1 + 2x2 - # st 0 <= x{1,2} <= 5 - # 0 <= x1 + x2 <= 7.5 - MOI.set(model, MOI.Silent(), true) - (x1, _) = MOI.add_constrained_variable(model, MOI.Interval(0.0, 5.0)) - (x2, _) = MOI.add_constrained_variable(model, MOI.Interval(0.0, 5.0)) - MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) - func = MOI.ScalarAffineFunction( - [MOI.ScalarAffineTerm(1.0, x1), MOI.ScalarAffineTerm(2.0, x2)], - 0.0, - ) - @test MOI.supports_constraint(model, typeof(func), MOI.Interval{Float64}) - MOI.set(model, MOI.ObjectiveFunction{typeof(func)}(), func) - @test MOI.get( - model, - MOI.NumberOfConstraints{ - MOI.ScalarAffineFunction{Float64}, - MOI.Interval{Float64}, - }(), - ) == 0 - MOI.add_constraint( - model, - MOI.ScalarAffineFunction( - [MOI.ScalarAffineTerm(1.0, x1), MOI.ScalarAffineTerm(1.0, x2)], - 0.0, - ), - MOI.Interval(0.0, 7.5), - ) - @test MOI.get( - model, - MOI.NumberOfConstraints{ - MOI.ScalarAffineFunction{Float64}, - MOI.Interval{Float64}, - }(), - ) == 1 - MOI.optimize!(model) - @test MOI.get(model, MOI.ObjectiveValue()) ≈ 12.5 -end - -function test_Variable_names(model, config) - MOI.empty!(model) - MOI.Test.variablenames(model, config) - MOI.empty!(model) - y = MOI.add_variable(model) - MOI.set(model, MOI.VariableName(), y, "y") - y2 = MOI.get(model, MOI.VariableIndex, "y") - @test y == y2 - @test MOI.get(model, MOI.VariableIndex, "y0") === nothing -end - -function test_HiGHS_custom_options(::Any, ::Any) +function test_HiGHS_custom_options() model = HiGHS.Optimizer() - @test MOI.supports(model, MOI.RawParameter("solver")) - @test MOI.get(model, MOI.RawParameter("solver")) == "choose" - MOI.set(model, MOI.RawParameter("solver"), "simplex") - @test MOI.get(model, MOI.RawParameter("solver")) == "simplex" - @test MOI.get(model, MOI.RawParameter("output_flag")) == true - MOI.set(model, MOI.RawParameter("output_flag"), false) - @test MOI.get(model, MOI.RawParameter("output_flag")) == false - @test MOI.get(model, MOI.RawParameter("time_limit")) > 1000 - MOI.set(model, MOI.RawParameter("time_limit"), 1000.0) - @test MOI.get(model, MOI.RawParameter("time_limit")) == 1000.0 + @test MOI.supports(model, MOI.RawOptimizerAttribute("solver")) + @test MOI.get(model, MOI.RawOptimizerAttribute("solver")) == "choose" + MOI.set(model, MOI.RawOptimizerAttribute("solver"), "simplex") + @test MOI.get(model, MOI.RawOptimizerAttribute("solver")) == "simplex" + @test MOI.get(model, MOI.RawOptimizerAttribute("output_flag")) == true + MOI.set(model, MOI.RawOptimizerAttribute("output_flag"), false) + @test MOI.get(model, MOI.RawOptimizerAttribute("output_flag")) == false + @test MOI.get(model, MOI.RawOptimizerAttribute("time_limit")) > 1000 + MOI.set(model, MOI.RawOptimizerAttribute("time_limit"), 1000.0) + @test MOI.get(model, MOI.RawOptimizerAttribute("time_limit")) == 1000.0 # unsupported test - @test MOI.supports(model, MOI.RawParameter("wrong_param")) == false - @test_throws MOI.UnsupportedAttribute MOI.get( - model, - MOI.RawParameter("wrong_param"), + @test MOI.supports(model, MOI.RawOptimizerAttribute("wrong_param")) == false + @test_throws( + MOI.UnsupportedAttribute, + MOI.get(model, MOI.RawOptimizerAttribute("wrong_param")), ) for v in [false, 1, 1.0, "A"] @test_throws( MOI.UnsupportedAttribute, - MOI.set(model, MOI.RawParameter("wrong_param"), v) + MOI.set(model, MOI.RawOptimizerAttribute("wrong_param"), v) ) end + return end -function test_Model_empty(model, ::Any) - MOI.empty!(model) - @test MOI.is_empty(model) - MOI.add_variable(model) - @test !MOI.is_empty(model) - MOI.empty!(model) - MOI.set( - model, - MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - MOI.ScalarAffineFunction{Float64}([], 0.0), - ) - @test MOI.is_empty(model) - MOI.set( - model, - MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - MOI.ScalarAffineFunction{Float64}([], 3.0), - ) - @test !MOI.is_empty(model) - MOI.set(model, MOI.ObjectiveSense(), MOI.FEASIBILITY_SENSE) - @test MOI.is_empty(model) - @test MOI.get( - model, - MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - ) ≈ MOI.ScalarAffineFunction{Float64}([], 0.0) - x = MOI.add_variable(model) - return MOI.set( - model, - MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - MOI.ScalarAffineFunction{Float64}([MOI.ScalarAffineTerm(1.0, x)], 0.0), - ) -end - -function test_show(::Any, ::Any) +function test_show() model = HiGHS.Optimizer() @test sprint(show, model) == "A HiGHS model with 0 columns and 0 rows." return end -function test_options(model, ::Any) +function test_options() + model = HiGHS.Optimizer() options = [ "write_solution_to_file", # Bool "simplex_strategy", # Cint @@ -483,49 +165,44 @@ function test_options(model, ::Any) "presolve", # String ] for key in options - v = MOI.get(model, MOI.RawParameter(key)) - MOI.set(model, MOI.RawParameter(key), v) - v2 = MOI.get(model, MOI.RawParameter(key)) + v = MOI.get(model, MOI.RawOptimizerAttribute(key)) + MOI.set(model, MOI.RawOptimizerAttribute(key), v) + v2 = MOI.get(model, MOI.RawOptimizerAttribute(key)) @test v == v2 end return end -function test_option_invalid(model, ::Any) +function test_option_invalid() + model = HiGHS.Optimizer() @test_throws( ErrorException( "Encountered an error in HiGHS: Check the log for details.", ), - MOI.set(model, MOI.RawParameter("time_limit"), -1.0), - ) - return -end - -function test_option_invalid(model, ::Any) - MOI.supports(model, MOI.RawParameter(:presolve)) == false - @test_throws MOI.UnsupportedAttribute MOI.get( - model, - MOI.RawParameter(:presolve), + MOI.set(model, MOI.RawOptimizerAttribute("time_limit"), -1.0), ) return end -function test_option_unknown_option(model, ::Any) +function test_option_unknown_option() + model = HiGHS.Optimizer() err = ErrorException( "Encountered an error in HiGHS: Check the log for details.", ) - @test_throws err MOI.set( - model, - MOI.RawParameter("write_solution_to_file"), - 1, + @test_throws( + err, + MOI.set(model, MOI.RawOptimizerAttribute("write_solution_to_file"), 1), ) - @test_throws err MOI.set(model, MOI.RawParameter("simplex_strategy"), "on") - @test_throws err MOI.set(model, MOI.RawParameter("time_limit"), 1) - @test_throws err MOI.set(model, MOI.RawParameter("presolve"), 1) + @test_throws( + err, + MOI.set(model, MOI.RawOptimizerAttribute("simplex_strategy"), "on"), + ) + @test_throws err MOI.set(model, MOI.RawOptimizerAttribute("time_limit"), 1) + @test_throws err MOI.set(model, MOI.RawOptimizerAttribute("presolve"), 1) return end -function test_copy_to(::Any, ::Any) +function test_copy_to() src = MOI.Utilities.Model{Float64}() MOI.Utilities.loadfromstring!( src, @@ -545,7 +222,7 @@ c8: w + x in MathOptInterface.Interval(1.0, 2.0) dest = HiGHS.Optimizer() MOI.copy_to(dest, src) @test MOI.get(dest, MOI.NumberOfVariables()) == 4 - list = MOI.get(dest, MOI.ListOfConstraints()) + list = MOI.get(dest, MOI.ListOfConstraintTypesPresent()) @test length(list) == 8 for S in ( MOI.GreaterThan{Float64}, @@ -553,7 +230,7 @@ c8: w + x in MathOptInterface.Interval(1.0, 2.0) MOI.EqualTo{Float64}, MOI.Interval{Float64}, ) - @test (MOI.SingleVariable, S) in list + @test (MOI.VariableIndex, S) in list @test (MOI.ScalarAffineFunction{Float64}, S) in list end MOI.optimize!(dest) @@ -561,18 +238,6 @@ c8: w + x in MathOptInterface.Interval(1.0, 2.0) return end -function optimizer(; solver::String = "") - model = HiGHS.Optimizer() - MOI.set(model, MOI.Silent(), true) - # Turn off presolve for simplex to get infeas_certificates. - if solver == "simplex" || solver == "ipm" - presolve = solver == "simplex" ? "off" : "on" - MOI.set(model, MOI.RawParameter("presolve"), presolve) - MOI.set(model, MOI.RawParameter("solver"), solver) - end - return MOI.Bridges.full_bridge_optimizer(model, Float64) -end - end TestMOIHighs.runtests()