diff --git a/README.md b/README.md index 30374c7..50107aa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,3 @@ -**Warning: The MathOptInterface wrapper is still under development. It is missing** -**a lot of features.** - # HiGHS.jl [![Build Status](https://github.com/jump-dev/HiGHS.jl/workflows/CI/badge.svg?branch=master)](https://github.com/jump-dev/HiGHS.jl/actions?query=workflow%3ACI) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index d56ad9d..aa56bda 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -1,12 +1,181 @@ import MathOptInterface + const MOI = MathOptInterface +const CleverDicts = MOI.Utilities.CleverDicts + +@enum( + _RowType, + _ROW_TYPE_LESSTHAN, + _ROW_TYPE_GREATERTHAN, + _ROW_TYPE_INTERVAL, + _ROW_TYPE_EQUAL_TO, +) + +_row_type(::MOI.GreaterThan{Float64}) = _ROW_TYPE_GREATERTHAN +_row_type(::MOI.LessThan{Float64}) = _ROW_TYPE_LESSTHAN +_row_type(::MOI.EqualTo{Float64}) = _ROW_TYPE_EQUAL_TO +_row_type(::MOI.Interval{Float64}) = _ROW_TYPE_INTERVAL + +_bounds(s::MOI.GreaterThan{Float64}) = s.lower, Inf +_bounds(s::MOI.LessThan{Float64}) = -Inf, s.upper +_bounds(s::MOI.EqualTo{Float64}) = s.value, s.value +_bounds(s::MOI.Interval{Float64}) = s.lower, s.upper + +@enum( + _BoundEnum, + _BOUND_NONE, + _BOUND_LESS_THAN, + _BOUND_GREATER_THAN, + _BOUND_LESS_AND_GREATER_THAN, + _BOUND_INTERVAL, + _BOUND_EQUAL_TO, +) + +const _SCALAR_SETS = Union{ + MOI.GreaterThan{Float64}, + MOI.LessThan{Float64}, + MOI.EqualTo{Float64}, + MOI.Interval{Float64}, +} + +""" +_VariableInfo + +A struct to store information about the variables. +""" +mutable struct _VariableInfo + # We need to keep the index here because sometimes we call `LinearIndex`. + index::MOI.VariableIndex + # The variable name. + name::String + # The zero-indexed column in the HiGHS object. + column::Cint + # Storage to keep track of the variable bounds. + bound::_BoundEnum + lower::Float64 + upper::Float64 + # 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 + + function _VariableInfo( + index::MOI.VariableIndex, + column::Cint, + bound::_BoundEnum = _BOUND_NONE, + ) + return new(index, "", column, bound, -Inf, Inf, "", "") + end +end + +function _variable_info_dict() + return CleverDicts.CleverDict{MOI.VariableIndex,_VariableInfo}( + x::MOI.VariableIndex -> x.value, + x::Int64 -> MOI.VariableIndex(x), + ) +end + +""" + _ConstraintInfo + +A struct to store information about the affine constraints. +""" +mutable struct _ConstraintInfo + # The constraint name. + name::String + # The zero-indexed row in the HiGHS object. + row::Cint + # Storage to keep track of the constraint bounds. + set::_RowType + lower::Float64 + upper::Float64 +end + +function _ConstraintInfo(set::_SCALAR_SETS) + lower, upper = _bounds(set) + return _ConstraintInfo("", 0, _row_type(set), lower, upper) +end +struct _ConstraintKey + value::Int64 +end + +function _constraint_info_dict() + return CleverDicts.CleverDict{_ConstraintKey,_ConstraintInfo}( + x::_ConstraintKey -> x.value, + x::Int64 -> _ConstraintKey(x), + ) +end + +""" + _set(c::_ConstraintInfo) + +Return the set associated with a constraint. +""" +function _set(c::_ConstraintInfo) + if c.set == _ROW_TYPE_LESSTHAN + return MOI.LessThan(c.upper) + elseif c.set == _ROW_TYPE_GREATERTHAN + return MOI.GreaterThan(c.lower) + elseif c.set == _ROW_TYPE_INTERVAL + return MOI.Interval(c.lower, c.upper) + else + @assert c.set == _ROW_TYPE_EQUAL_TO + return MOI.EqualTo(c.lower) + end +end + +""" + _Solution + +A struct to store the vector solution from HiGHS because it doesn't support +accessing them element-wise. +""" +struct _Solution + colvalue::Vector{Cdouble} + coldual::Vector{Cdouble} + rowvalue::Vector{Cdouble} + rowdual::Vector{Cdouble} + _Solution() = new(Cdouble[], Cdouble[], Cdouble[], Cdouble[]) +end mutable struct Optimizer <: MOI.AbstractOptimizer + # A pointer to the underlying HiGHS optimizer. inner::Ptr{Cvoid} - objective_sense::MOI.OptimizationSense - variable_map::Dict{MOI.VariableIndex, String} + + # Storage for `MOI.Name`. + name::String + + # A flag to keep track of MOI.Silent, which over-rides the print_level + # parameter. + silent::Bool + + # A flag to keep track of MOI.FEASIBILITY_SENSE, since HiGHS only stores + # MIN_SENSE or MAX_SENSE. This allows us to differentiate between MIN_SENSE + # and FEASIBILITY_SENSE. + is_feasibility::Bool + + # HiGHS doesn't support constants in the objective function. objective_constant::Float64 + variable_info::typeof(_variable_info_dict()) + affine_constraint_info::typeof(_constraint_info_dict()) + + # Mappings from variable and constraint names to their indices. These are + # lazily built on-demand, so most of the time, they are `nothing`. + name_to_variable::Union{ + Nothing, + Dict{String,Union{Nothing,MOI.VariableIndex}}, + } + name_to_constraint_index::Union{ + Nothing, + Dict{String,Union{Nothing,MOI.ConstraintIndex}}, + } + + # HiGHS just returns a single solution struct :( + optimize_called::Bool + solution::_Solution + """ Optimizer() @@ -19,10 +188,18 @@ mutable struct Optimizer <: MOI.AbstractOptimizer end model = new( ptr, - MOI.FEASIBILITY_SENSE, - Dict{MOI.VariableIndex, String}(), + "", + false, + true, 0.0, + _variable_info_dict(), + _constraint_info_dict(), + nothing, + nothing, + false, + _Solution(), ) + MOI.empty!(model) finalizer(Highs_destroy, model) return model end @@ -31,123 +208,196 @@ end Base.cconvert(::Type{Ptr{Cvoid}}, model::Optimizer) = model Base.unsafe_convert(::Type{Ptr{Cvoid}}, model::Optimizer) = model.inner -function _check_ret(model::Optimizer, ret::Cint) - # TODO: errors for invalid return codes! - return nothing -end - -function MOI.empty!(o::Optimizer) - Highs_destroy(o) - o.inner = Highs_create() - o.objective_sense = MOI.FEASIBILITY_SENSE - empty!(o.variable_map) - o.objective_constant = 0.0 +function _check_ret(ret::Cint) + if ret == Cint(1) + return # Cint(1) is 'true'. Nothing went wrong. + else + # These return codes should only ever be 0 or 1. + @assert ret == Cint(0) + error("Encountered an error in HiGHS. Check the log for details.") + end return end -function MOI.is_empty(o::Optimizer) - return Highs_getNumRows(o) == 0 && - Highs_getNumCols(o) == 0 && - o.objective_sense == MOI.FEASIBILITY_SENSE && - o.objective_constant == 0.0 +function Base.show(io::IO, model::Optimizer) + return print( + io, + "A HiGHS model with $(Highs_getNumCols(model)) columns and " * + "$(Highs_getNumRows(model)) rows.", + ) end -function MOI.optimize!(o::Optimizer) - if o.objective_sense == MOI.FEASIBILITY_SENSE - obj_func = MOI.get(o, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()) - if any(!=(0.0), obj_func.terms) - error("Feasibility sense with non-constant objective function, set the sense to min/max, the objective to 0 or reset the sense to feasibility to erase the objective") - end +function MOI.empty!(model::Optimizer) + ret = Highs_clearModel(model) + if ret == 2 + error( + "Encountered an error in HiGHS: HighsStatus::Error. " * + "Check the log for details" + ) end - Highs_run(o) + model.objective_constant = 0.0 + model.is_feasibility = true + empty!(model.variable_info) + empty!(model.affine_constraint_info) + model.name_to_variable = nothing + model.name_to_constraint_index = nothing + model.optimize_called = false return end -function MOI.add_variable(o::Optimizer) - _ = Highs_addCol(o, 0.0, -Inf, Inf, Cint(0), Cint[], Cint[]) - col_idx = MOI.get(o, MOI.NumberOfVariables()) - 1 - return MOI.VariableIndex(col_idx) +function MOI.is_empty(model::Optimizer) + return Highs_getNumCols(model) == 0 && + Highs_getNumRows(model) == 0 && + iszero(model.objective_constant) && + model.is_feasibility && + isempty(model.variable_info) && + isempty(model.affine_constraint_info) && + model.name_to_variable === nothing && + model.name_to_constraint_index === nothing && + model.optimize_called == false end -function MOI.add_constrained_variable(o::Optimizer, set::S) where {S <: MOI.Interval} - _ = Highs_addCol(o, 0.0, Cdouble(set.lower), Cdouble(set.upper), Cint(0), Cint[], Cint[]) - col_idx = MOI.get(o, MOI.NumberOfVariables()) - 1 - return (MOI.VariableIndex(col_idx), MOI.ConstraintIndex{MOI.SingleVariable, MOI.Interval{Float64}}(col_idx)) +MOI.get(::Optimizer, ::MOI.SolverName) = "HiGHS" + +MOI.get(model::Optimizer, ::MOI.RawSolver) = model + +MOI.Utilities.supports_default_copy_to(::Optimizer, ::Bool) = true + +function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike; kws...) + return MOI.Utilities.automatic_copy_to(dest, src; kws...) end -MOI.supports_constraint(::Optimizer, ::MOI.SingleVariable, ::MOI.Interval) = true +function MOI.get(::Optimizer, ::MOI.ListOfVariableAttributesSet) + return MOI.AbstractVariableAttribute[MOI.VariableName()] +end -function MOI.add_constraint(o::Optimizer, sg::MOI.SingleVariable, set::MOI.Interval) - var_idx = Cint(sg.variable.value) - _ = Highs_changeColBounds(o, var_idx, Cdouble(set.lower), Cdouble(set.upper)) - return +function MOI.get(model::Optimizer, ::MOI.ListOfModelAttributesSet) + attributes = [ + MOI.ObjectiveSense(), + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + ] + if MOI.get(model, MOI.Name()) != "" + push!(attributes, MOI.Name()) + end + return attributes end -function MOI.get(o::Optimizer, ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{Float64}, MOI.Interval{Float64}}) - nrows = Highs_getNumRows(o) - return Int(nrows) +function MOI.get(::Optimizer, ::MOI.ListOfConstraintAttributesSet) + return MOI.AbstractConstraintAttribute[MOI.ConstraintName()] end -MOI.supports_constraint(::Optimizer, ::MOI.ScalarAffineFunction, ::MOI.Interval) = true +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}())) +end -function MOI.add_constraint(o::Optimizer, func::MOI.ScalarAffineFunction, set::MOI.Interval) - number_nonzeros = length(func.terms) - coefficients = Vector{Cdouble}(undef, number_nonzeros) - col_indices = Vector{Cint}(undef, number_nonzeros) - for j in Base.OneTo(number_nonzeros) - term = func.terms[j] - coefficients[j] = term.coefficient - col_indices[j] = Cint(term.variable_index.value) +function MOI.get(model::Optimizer, ::MOI.ListOfConstraints) + constraints = Set{Tuple{DataType,DataType}}() + 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})) + elseif info.bound == _BOUND_GREATER_THAN + push!(constraints, (MOI.SingleVariable, 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})) + elseif info.bound == _BOUND_EQUAL_TO + push!(constraints, (MOI.SingleVariable, MOI.EqualTo{Float64})) + else + @assert info.bound == _BOUND_INTERVAL + push!(constraints, (MOI.SingleVariable, MOI.Interval{Float64})) + end + end + for info in values(model.affine_constraint_info) + push!( + constraints, + (MOI.ScalarAffineFunction{Float64}, typeof(_set(info))), + ) end - coefficients_ptr = pointer(coefficients) - col_indices_ptr = pointer(col_indices) - lower = convert(Cdouble, set.lower - func.constant) - upper = convert(Cdouble, set.upper - func.constant) - row_idx = Highs_addRow(o, lower, upper, Cint(number_nonzeros), col_indices_ptr, coefficients_ptr) - return MOI.ConstraintIndex{typeof(func), typeof(set)}(row_idx) + return collect(constraints) end -MOI.supports(::Optimizer, ::MOI.SolverName) = true -MOI.get(::Optimizer, ::MOI.SolverName) = "HiGHS" +### +### MOI.RawParameter +### -MOI.supports(::Optimizer, param::MOI.RawParameter) = true +""" + _OPTIONS + +A dictionary mapping the `String` name of HiGHS options to the expected input +type. +""" +const _OPTIONS = Dict{String,DataType}( + "presolve" => String, + "solver" => String, + "parallel" => String, + "simplex_strategy" => Cint, + "simplex_iteration_limit" => Cint, + "highs_min_threads" => Cint, + "message_level" => Cint, + "time_limit" => Cdouble, +) + +function MOI.supports(::Optimizer, param::MOI.RawParameter) + return haskey(_OPTIONS, param.name) +end + +function _check_option_status(ret::Cint) + if ret == 0 + return + end + @assert 1 <= ret <= 3 + codes = ["NO_FILE", "UNKNOWN_OPTION", "ILLEGAL_VALUE"] + error( + "Encountered an error in HiGHS: OptionStatus::$(codes[ret]). " * + "Check the log for details." + ) + return +end -# setting HiGHS options function MOI.set(model::Optimizer, param::MOI.RawParameter, value::Integer) ret = Highs_setHighsIntOptionValue(model, param.name, Cint(value)) - return _check_ret(model, ret) + return _check_option_status(ret) end function MOI.set(model::Optimizer, param::MOI.RawParameter, value::Bool) ret = Highs_setHighsBoolOptionValue(model, param.name, Cint(value)) - return _check_ret(model, ret) + return _check_option_status(ret) end -function MOI.set(model::Optimizer, param::MOI.RawParameter, value::AbstractFloat) +function MOI.set( + model::Optimizer, + param::MOI.RawParameter, + value::AbstractFloat, +) ret = Highs_setHighsDoubleOptionValue(model, param.name, Cdouble(value)) - return _check_ret(model, ret) + return _check_option_status(ret) end function MOI.set(model::Optimizer, param::MOI.RawParameter, value::String) ret = Highs_setHighsStringOptionValue(model, param.name, value) - return _check_ret(model, ret) + return _check_option_status(ret) end function _get_option(model::Optimizer, option::String, ::Type{Cint}) value = Ref{Cint}(0) - Highs_getHighsIntOptionValue(model, option, value) + ret = Highs_getHighsIntOptionValue(model, option, value) + _check_option_status(ret) return value[] end function _get_option(model::Optimizer, option::String, ::Type{Bool}) value = Ref{Cint}(0) - Highs_getHighsBoolOptionValue(model, option, value) + ret = Highs_getHighsBoolOptionValue(model, option, value) + _check_option_status(ret) return Bool(value[]) end function _get_option(model::Optimizer, option::String, ::Type{Cdouble}) value = Ref{Cdouble}() - Highs_getHighsDoubleOptionValue(model, option, value) + ret = Highs_getHighsDoubleOptionValue(model, option, value) + _check_option_status(ret) return value[] end @@ -155,22 +405,12 @@ function _get_option(model::Optimizer, option::String, ::Type{String}) buffer = Vector{Cchar}(undef, 100) bufferP = pointer(buffer) GC.@preserve buffer begin - Highs_getHighsStringOptionValue(model, option, bufferP) + ret = Highs_getHighsStringOptionValue(model, option, bufferP) + _check_option_status(ret) return unsafe_string(bufferP) end end -const _OPTIONS = Dict{String,DataType}( - "presolve" => String, - "solver" => String, - "parallel" => String, - "simplex_strategy" => Cint, - "simplex_iteration_limit" => Cint, - "highs_min_threads" => Cint, - "message_level" => Cint, - "time_limit" => Cdouble, -) - function MOI.get(o::Optimizer, param::MOI.RawParameter) param_type = get(_OPTIONS, param.name, nothing) if param_type === nothing @@ -179,182 +419,1214 @@ function MOI.get(o::Optimizer, param::MOI.RawParameter) return _get_option(o, param.name, param_type) end -const _SUPPORTED_MODEL_ATTRIBUTES = Union{ - MOI.ObjectiveSense, - MOI.NumberOfVariables, - MOI.ListOfVariableIndices, - MOI.ListOfConstraintIndices, - MOI.NumberOfConstraints, # TODO single variables - MOI.ListOfConstraints, # TODO single variables - MOI.ObjectiveFunctionType, - MOI.ObjectiveValue, - # MOI.DualObjectiveValue, # TODO - # MOI.SolveTime, # TODO - MOI.SimplexIterations, - MOI.BarrierIterations, - MOI.RawSolver, - # MOI.RawStatusString, # TODO - MOI.ResultCount, - # MOI.Silent, # TODO - MOI.TerminationStatus, - # MOI.PrimalStatus, - # MOI.DualStatus -} -@enum HighsModelStatus begin - NOTSET = 0 - LOAD_ERROR - MODEL_ERROR - PRESOLVE_ERROR - SOLVE_ERROR - POSTSOLVE_ERROR - MODEL_EMPTY - PRIMAL_INFEASIBLE - PRIMAL_UNBOUNDED - OPTIMAL - REACHED_DUAL_OBJECTIVE_VALUE_UPPER_BOUND - REACHED_TIME_LIMIT - REACHED_ITERATION_LIMIT - PRIMAL_DUAL_INFEASIBLE - DUAL_INFEASIBLE +### +### MOI.TimeLimitSec +### + +MOI.supports(::Optimizer, ::MOI.TimeLimitSec) = true + +function MOI.set(model::Optimizer, ::MOI.TimeLimitSec, ::Nothing) + return MOI.set(model, MOI.RawParameter("time_limit"), Inf) end -MOI.supports(::Optimizer, ::_SUPPORTED_MODEL_ATTRIBUTES) = true -MOI.supports(::Optimizer, ::MOI.VariableName, ::Type{MOI.VariableIndex}) = true +function MOI.set(model::Optimizer, ::MOI.TimeLimitSec, limit::Real) + return MOI.set(model, MOI.RawParameter("time_limit"), Float64(limit)) +end -MOI.get(o::Optimizer, ::MOI.RawSolver) = o +function MOI.get(model::Optimizer, ::MOI.TimeLimitSec) + return MOI.get(model, MOI.RawParameter("time_limit")) +end -function MOI.get(o::Optimizer, ::MOI.ResultCount) - status = HighsModelStatus(Highs_getModelStatus(o, Cint(0))) - return status == OPTIMAL ? 1 : 0 +### +### MOI.Silent +### + +MOI.supports(::Optimizer, ::MOI.Silent) = true + +MOI.get(model::Optimizer, ::MOI.Silent) = model.silent + +function MOI.set(model::Optimizer, ::MOI.Silent, flag::Bool) + if flag + Highs_runQuiet(model) + elseif model.silent && !flag + @warn("Unable to restore printing. Sorry.") + end + model.silent = flag + return end -function MOI.get(o::Optimizer, ::MOI.ObjectiveSense) - return o.objective_sense + +### +### MOI.Name +### + +MOI.supports(::Optimizer, ::MOI.Name) = true + +MOI.get(model::Optimizer, ::MOI.Name) = model.name + +MOI.set(model::Optimizer, ::MOI.Name, name::String) = (model.name = name) + +### +### Variables +### + +function MOI.get(model::Optimizer, ::MOI.NumberOfVariables) + return length(model.variable_info) end -function MOI.get(o::Optimizer, ::MOI.TerminationStatus) - status = HighsModelStatus(Highs_getModelStatus(o.inner, Cint(0))) - if status == OPTIMAL - return MOI.OPTIMAL - elseif status == PRIMAL_INFEASIBLE - return MOI.INFEASIBLE - elseif status == DUAL_INFEASIBLE - return MOI.DUAL_INFEASIBLE - elseif status == REACHED_ITERATION_LIMIT - return MOI.ITERATION_LIMIT - elseif status == REACHED_TIME_LIMIT - return MOI.TIME_LIMIT - elseif status == MODEL_ERROR - return MOI.INVALID_MODEL - else - return MOI.OTHER_ERROR + +function MOI.get(model::Optimizer, ::MOI.ListOfVariableIndices) + return sort!(collect(keys(model.variable_info)), by = x -> x.value) +end + +function _info(model::Optimizer, key::MOI.VariableIndex) + if haskey(model.variable_info, key) + return model.variable_info[key] end + return throw(MOI.InvalidIndex(key)) end -function MOI.set(o::Optimizer, ::MOI.ObjectiveSense, sense::MOI.OptimizationSense) - sense_code = sense == MOI.MAX_SENSE ? Cint(-1) : Cint(1) - _ = Highs_changeObjectiveSense(o, sense_code) - o.objective_sense = sense - # if feasibility sense set, erase the function - if sense == MOI.FEASIBILITY_SENSE - MOI.set(o, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), MOI.ScalarAffineFunction{Float64}([], 0)) +""" + column(model::Optimizer, x::MOI.VariableIndex) + +Return the 0-indexed column associated with `x` in `model`. +""" +column(model::Optimizer, x::MOI.VariableIndex) = _info(model, x).column + +function MOI.add_variable(model::Optimizer) + # Initialize `_VariableInfo` with a dummy `VariableIndex` and a column, + # because we need `add_item` to tell us what the `VariableIndex` is. + index = CleverDicts.add_item( + model.variable_info, + _VariableInfo(MOI.VariableIndex(0), Cint(0)), + ) + info = _info(model, index) + # Now, set `.index` and `.column`. + info.index = index + info.column = Cint(length(model.variable_info) - 1) + ret = Highs_addCol(model, 0.0, -Inf, Inf, 0, C_NULL, C_NULL) + _check_ret(ret) + return index +end + +function MOI.is_valid(model::Optimizer, v::MOI.VariableIndex) + return haskey(model.variable_info, v) +end + +function MOI.delete(model::Optimizer, v::MOI.VariableIndex) + col = column(model, v) + ret = Highs_deleteColsByRange(model, col, col) + _check_ret(ret) + delete!(model.variable_info, v) + for other_info in values(model.variable_info) + if other_info.column > col + other_info.column -= 1 + end + end + model.name_to_variable = nothing + model.name_to_constraint_index = nothing + return +end + +# +# Variable names +# + +function MOI.supports( + ::Optimizer, + ::MOI.VariableName, + ::Type{MOI.VariableIndex}, +) + return true +end + +function MOI.get(model::Optimizer, ::Type{MOI.VariableIndex}, name::String) + if model.name_to_variable === nothing + _rebuild_name_to_variable(model) + end + if haskey(model.name_to_variable, name) + variable = model.name_to_variable[name] + if variable === nothing + error("Duplicate name detected: $(name)") + end + return variable end return nothing end -function MOI.get(o::Optimizer, ::MOI.NumberOfVariables) - return Int(Highs_getNumCols(o)) +function MOI.get(model::Optimizer, ::MOI.VariableName, v::MOI.VariableIndex) + return _info(model, v).name end -function MOI.get(o::Optimizer, ::MOI.ListOfVariableIndices) - ncols = MOI.get(o, MOI.NumberOfVariables()) - return [MOI.VariableIndex(j) for j in 0:(ncols-1)] +function MOI.set( + model::Optimizer, + ::MOI.VariableName, + v::MOI.VariableIndex, + name::String, +) + info = _info(model, v) + info.name = name + model.name_to_variable = nothing + return +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 isempty(info.name) + continue + end + if haskey(model.name_to_variable, info.name) + model.name_to_variable[info.name] = nothing + else + model.name_to_variable[info.name] = index + end + end + return end -MOI.get(::Optimizer, ::MOI.ObjectiveFunctionType) = MOI.ScalarAffineFunction{Float64} +### +### Objectives +### -MOI.supports(::Optimizer, ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}) = true +MOI.supports(::Optimizer, ::MOI.ObjectiveSense) = true -function MOI.set(o::Optimizer, ::MOI.ObjectiveFunction{F}, func::F) where {F <: MOI.ScalarAffineFunction{Float64}} - total_ncols = MOI.get(o, MOI.NumberOfVariables()) - coefficients = zeros(Cdouble, total_ncols) - for term in func.terms - j = term.variable_index.value - coefficients[j+1] = Cdouble(term.coefficient) +function MOI.set( + model::Optimizer, + ::MOI.ObjectiveSense, + sense::MOI.OptimizationSense, +) + x = sense == MOI.MAX_SENSE ? Cint(-1) : Cint(1) + ret = Highs_changeObjectiveSense(model, x) + _check_ret(ret) + if sense == MOI.FEASIBILITY_SENSE + model.is_feasibility = true + # TODO(odow): cache the mask. + n = MOI.get(model, MOI.NumberOfVariables()) + ret = + Highs_changeColsCostByMask(model, ones(Cint, n), zeros(Cdouble, n)) + _check_ret(ret) + model.objective_constant = 0.0 + else + model.is_feasibility = false end - coefficients_ptr = pointer(coefficients) - mask = pointer(ones(Cint, total_ncols)) - Highs_changeColsCostByMask(o, mask, coefficients_ptr) - o.objective_constant = MOI.constant(func) return end -function MOI.get(o::Optimizer, ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}) - ncols = MOI.get(o, MOI.NumberOfVariables()) - num_cols = Ref{Cint}(0) +function MOI.get(model::Optimizer, ::MOI.ObjectiveSense) + if model.is_feasibility + return MOI.FEASIBILITY_SENSE + end + senseP = Ref{Cint}() + ret = Highs_getObjectiveSense(model, senseP) + _check_ret(ret) + return senseP[] == 1 ? MOI.MIN_SENSE : MOI.MAX_SENSE +end + +function MOI.supports( + ::Optimizer, + ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}, +) + return true +end + +function MOI.get(::Optimizer, ::MOI.ObjectiveFunctionType) + return MOI.ScalarAffineFunction{Float64} +end + +function MOI.set( + model::Optimizer, + ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}, + f::MOI.ScalarAffineFunction{Float64}, +) + num_vars = length(model.variable_info) + obj = zeros(Float64, num_vars) + for term in f.terms + col = column(model, term.variable_index) + obj[col+1] += term.coefficient + end + # TODO(odow): cache the mask. + mask = ones(Cint, num_vars) + ret = Highs_changeColsCostByMask(model, mask, obj) + _check_ret(ret) + model.objective_constant = f.constant + return +end + +function MOI.get( + model::Optimizer, + ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}, +) + ncols = MOI.get(model, MOI.NumberOfVariables()) + if ncols == 0 + return MOI.ScalarAffineFunction{Float64}( + MOI.ScalarAffineTerm{Float64}[], + model.objective_constant, + ) + end + num_colsP, nnzP = Ref{Cint}(0), Ref{Cint}(0) costs = Vector{Cdouble}(undef, ncols) - if ncols > 0 - _ = Highs_getColsByRange( - o, - Cint(0), Cint(ncols-1), # column range - num_cols, costs, - C_NULL, C_NULL, # lower, upper - Ref{Cint}(0), C_NULL, C_NULL, C_NULL # coefficients + ret = Highs_getColsByRange( + model, + 0, + ncols - 1, + num_colsP, + costs, + C_NULL, + C_NULL, + nnzP, + C_NULL, + C_NULL, + C_NULL, + ) + return MOI.ScalarAffineFunction{Float64}( + MOI.ScalarAffineTerm{Float64}[ + MOI.ScalarAffineTerm(costs[info.column+1], index) for + (index, info) in model.variable_info if + !iszero(costs[info.column+1]) + ], + model.objective_constant, + ) +end + +function MOI.get(model::Optimizer, ::MOI.ObjectiveFunction{F}) where {F} + obj = MOI.get( + model, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + ) + return convert(F, obj) +end + +function MOI.modify( + model::Optimizer, + ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}, + chg::MOI.ScalarConstantChange{Float64}, +) + model.objective_constant = chg.new_constant + return +end + +function MOI.modify( + model::Optimizer, + ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}, + chg::MOI.ScalarCoefficientChange{Float64}, +) + ret = Highs_changeColCost( + model, + column(model, chg.variable), + chg.new_coefficient, + ) + _check_ret(ret) + return +end + +### +### SingleVariable-in-Set constraints. +### + +function MOI.supports_constraint( + ::Optimizer, + ::Type{MOI.SingleVariable}, + ::Type{<:_SCALAR_SETS}, +) + return true +end + +function _bound_enums(::Type{MOI.LessThan{Float64}}) + return (_BOUND_LESS_THAN, _BOUND_LESS_AND_GREATER_THAN) +end + +function _bound_enums(::Type{MOI.GreaterThan{Float64}}) + return (_BOUND_GREATER_THAN, _BOUND_LESS_AND_GREATER_THAN) +end + +_bound_enums(::Type{MOI.Interval{Float64}}) = (_BOUND_INTERVAL,) + +_bound_enums(::Type{MOI.EqualTo{Float64}}) = (_BOUND_EQUAL_TO,) + +function MOI.get( + model::Optimizer, + ::MOI.ListOfConstraintIndices{MOI.SingleVariable,S}, +) where {S<:_SCALAR_SETS} + indices = MOI.ConstraintIndex{MOI.SingleVariable,S}[ + MOI.ConstraintIndex{MOI.SingleVariable,S}(key.value) for + (key, info) in model.variable_info if info.bound in _bound_enums(S) + ] + return sort!(indices, by = x -> x.value) +end + +function _info( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable,<:Any}, +) + var_index = MOI.VariableIndex(c.value) + if haskey(model.variable_info, var_index) + return _info(model, var_index) + end + return throw(MOI.InvalidIndex(c)) +end + +""" + column( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable,<:Any}, + ) + +Return the 0-indexed column associated with the variable bounds `c` in `model`. +""" +function column( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable,<:Any}, +) + return _info(model, c).column +end + +function MOI.is_valid( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.LessThan{Float64}}, +) + if haskey(model.variable_info, MOI.VariableIndex(c.value)) + info = _info(model, c) + return info.bound == _BOUND_LESS_THAN || + info.bound == _BOUND_LESS_AND_GREATER_THAN + end + return false +end + +function MOI.is_valid( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.GreaterThan{Float64}}, +) + if haskey(model.variable_info, MOI.VariableIndex(c.value)) + info = _info(model, c) + return info.bound == _BOUND_GREATER_THAN || + info.bound == _BOUND_LESS_AND_GREATER_THAN + end + return false +end + +function MOI.is_valid( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.Interval{Float64}}, +) + return haskey(model.variable_info, MOI.VariableIndex(c.value)) && + _info(model, c).bound == _BOUND_INTERVAL +end + +function MOI.is_valid( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.EqualTo{Float64}}, +) + return haskey(model.variable_info, MOI.VariableIndex(c.value)) && + _info(model, c).bound == _BOUND_EQUAL_TO +end + +function MOI.get( + model::Optimizer, + ::MOI.ConstraintFunction, + c::MOI.ConstraintIndex{MOI.SingleVariable,<:Any}, +) + MOI.throw_if_not_valid(model, c) + return MOI.SingleVariable(MOI.VariableIndex(c.value)) +end + +function MOI.set( + ::Optimizer, + ::MOI.ConstraintFunction, + ::MOI.ConstraintIndex{MOI.SingleVariable,<:Any}, + ::MOI.SingleVariable, +) + return throw(MOI.SettingSingleVariableFunctionNotAllowed()) +end + +function _throw_if_existing_lower( + bound::_BoundEnum, + ::Type{S}, + variable::MOI.VariableIndex, +) where {S<:MOI.AbstractSet} + if bound == _BOUND_LESS_AND_GREATER_THAN + throw(MOI.LowerBoundAlreadySet{MOI.GreaterThan{Float64},S}(variable)) + elseif bound == _BOUND_GREATER_THAN + throw(MOI.LowerBoundAlreadySet{MOI.GreaterThan{Float64},S}(variable)) + elseif bound == _BOUND_INTERVAL + throw(MOI.LowerBoundAlreadySet{MOI.Interval{Float64},S}(variable)) + elseif bound == _BOUND_EQUAL_TO + throw(MOI.LowerBoundAlreadySet{MOI.EqualTo{Float64},S}(variable)) + end + return +end + +function _throw_if_existing_upper( + bound::_BoundEnum, + ::Type{S}, + variable::MOI.VariableIndex, +) where {S<:MOI.AbstractSet} + if bound == _BOUND_LESS_AND_GREATER_THAN + throw(MOI.UpperBoundAlreadySet{MOI.LessThan{Float64},S}(variable)) + elseif bound == _BOUND_LESS_THAN + throw(MOI.UpperBoundAlreadySet{MOI.LessThan{Float64},S}(variable)) + elseif bound == _BOUND_INTERVAL + throw(MOI.UpperBoundAlreadySet{MOI.Interval{Float64},S}(variable)) + elseif bound == _BOUND_EQUAL_TO + throw(MOI.UpperBoundAlreadySet{MOI.EqualTo{Float64},S}(variable)) + end + return +end + +function MOI.add_constraint( + model::Optimizer, + f::MOI.SingleVariable, + s::S, +) where {S<:_SCALAR_SETS} + info = _info(model, f.variable) + if S <: MOI.LessThan{Float64} + _throw_if_existing_upper(info.bound, S, f.variable) + if info.bound == _BOUND_GREATER_THAN + info.bound = _BOUND_LESS_AND_GREATER_THAN + else + info.bound = _BOUND_LESS_THAN + end + info.upper = s.upper + elseif S <: MOI.GreaterThan{Float64} + _throw_if_existing_lower(info.bound, S, f.variable) + if info.bound == _BOUND_LESS_THAN + info.bound = _BOUND_LESS_AND_GREATER_THAN + else + info.bound = _BOUND_GREATER_THAN + end + info.lower = s.lower + elseif S <: MOI.EqualTo{Float64} + _throw_if_existing_lower(info.bound, S, f.variable) + _throw_if_existing_upper(info.bound, S, f.variable) + info.bound = _BOUND_EQUAL_TO + info.lower = s.value + info.upper = s.value + else + @assert S <: MOI.Interval{Float64} + _throw_if_existing_lower(info.bound, S, f.variable) + _throw_if_existing_upper(info.bound, S, f.variable) + info.bound = _BOUND_INTERVAL + info.lower = s.lower + info.upper = s.upper + end + index = MOI.ConstraintIndex{MOI.SingleVariable,typeof(s)}(f.variable.value) + MOI.set(model, MOI.ConstraintSet(), index, s) + return index +end + +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.LessThan{Float64}}, +) + MOI.throw_if_not_valid(model, c) + info = _info(model, c) + ret = Highs_changeColBounds(model, info.column, info.lower, Inf) + _check_ret(ret) + info.upper = Inf + if info.bound == _BOUND_LESS_AND_GREATER_THAN + info.bound = _BOUND_GREATER_THAN + 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}}, +) + MOI.throw_if_not_valid(model, c) + info = _info(model, c) + ret = Highs_changeColBounds(model, info.column, -Inf, info.upper) + _check_ret(ret) + info.lower = -Inf + if info.bound == _BOUND_LESS_AND_GREATER_THAN + info.bound = _BOUND_LESS_THAN + 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}}, +) + MOI.throw_if_not_valid(model, c) + info = _info(model, c) + ret = Highs_changeColBounds(model, info.column, -Inf, Inf) + _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}}, +) + MOI.throw_if_not_valid(model, c) + info = _info(model, c) + ret = Highs_changeColBounds(model, info.column, -Inf, Inf) + _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.get( + model::Optimizer, + ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.GreaterThan{Float64}}, +) + MOI.throw_if_not_valid(model, c) + return MOI.GreaterThan(_info(model, c).lower) +end + +function MOI.get( + model::Optimizer, + ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.LessThan{Float64}}, +) + MOI.throw_if_not_valid(model, c) + return MOI.LessThan(_info(model, c).upper) +end + +function MOI.get( + model::Optimizer, + ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.EqualTo{Float64}}, +) + MOI.throw_if_not_valid(model, c) + return MOI.EqualTo(_info(model, c).lower) +end + +function MOI.get( + model::Optimizer, + ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.SingleVariable,MOI.Interval{Float64}}, +) + MOI.throw_if_not_valid(model, c) + info = _info(model, c) + return MOI.Interval(info.lower, info.upper) +end + +function MOI.set( + model::Optimizer, + ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.SingleVariable,S}, + s::S, +) where {S<:_SCALAR_SETS} + MOI.throw_if_not_valid(model, c) + lower, upper = _bounds(s) + info = _info(model, c) + if S == MOI.LessThan{Float64} + ret = Highs_changeColBounds(model, info.column, info.lower, upper) + _check_ret(ret) + info.upper = upper + elseif S == MOI.GreaterThan{Float64} + ret = Highs_changeColBounds(model, info.column, lower, info.upper) + _check_ret(ret) + info.lower = lower + else + ret = Highs_changeColBounds(model, info.column, lower, upper) + _check_ret(ret) + info.lower = lower + info.upper = upper + end + return +end + +function MOI.supports( + ::Optimizer, + ::MOI.ConstraintName, + ::Type{<:MOI.ConstraintIndex{MOI.SingleVariable,<:_SCALAR_SETS}}, +) + 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 +### + +function MOI.supports_constraint( + ::Optimizer, + ::Type{MOI.ScalarAffineFunction{Float64}}, + ::Type{<:_SCALAR_SETS}, +) + return true +end + +function MOI.get( + model::Optimizer, + ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{Float64},S}, +) where {S<:_SCALAR_SETS} + indices = MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},S}[ + MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},S}(key.value) + for (key, info) in model.affine_constraint_info if _set(info) isa S + ] + return sort!(indices; by = x -> x.value) +end + +function _info( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},<:_SCALAR_SETS}, +) + key = _ConstraintKey(c.value) + if haskey(model.affine_constraint_info, key) + return model.affine_constraint_info[key] + end + return throw(MOI.InvalidIndex(c)) +end + +function row( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},<:_SCALAR_SETS}, +) + return _info(model, c).row +end + +function MOI.is_valid( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},S}, +) where {S<:_SCALAR_SETS} + key = _ConstraintKey(c.value) + info = get(model.affine_constraint_info, key, nothing) + if info === nothing + return false + end + return _set(info) isa S +end + +function _indices_and_coefficients( + indices::Vector{Cint}, + coefficients::Vector{Float64}, + model::Optimizer, + f::MOI.ScalarAffineFunction{Float64}, +) + i = 1 + for term in f.terms + indices[i] = column(model, term.variable_index) + coefficients[i] = term.coefficient + i += 1 + end + return indices, coefficients +end + +function _indices_and_coefficients( + model::Optimizer, + f::MOI.ScalarAffineFunction{Float64}, +) + f_canon = MOI.Utilities.canonical(f) + nnz = length(f_canon.terms) + indices, coefficients = zeros(Cint, nnz), zeros(Cdouble, nnz) + _indices_and_coefficients(indices, coefficients, model, f_canon) + return indices, coefficients +end + +function MOI.add_constraint( + model::Optimizer, + f::MOI.ScalarAffineFunction{Float64}, + s::_SCALAR_SETS, +) + if !iszero(f.constant) + throw( + MOI.ScalarFunctionConstantNotZero{Float64,typeof(f),typeof(s)}( + f.constant, + ), ) - num_cols[] == ncols || error("Unexpected number of columns, inconsistent HiGHS state") end - terms = Vector{MOI.ScalarAffineTerm{Float64}}() - for (j, cost) in enumerate(costs) - if cost != 0.0 - var_idx = MOI.VariableIndex(j-1) - push!(terms, MOI.ScalarAffineTerm(cost, var_idx)) + key = CleverDicts.add_item(model.affine_constraint_info, _ConstraintInfo(s)) + model.affine_constraint_info[key].row = + Cint(length(model.affine_constraint_info) - 1) + indices, coefficients = _indices_and_coefficients(model, f) + lower, upper = _bounds(s) + ret = Highs_addRow( + model, + lower, + upper, + length(indices), + indices, + coefficients, + ) + _check_ret(ret) + return MOI.ConstraintIndex{typeof(f),typeof(s)}(key.value) +end + +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},<:_SCALAR_SETS}, +) + r = row(model, c) + ret = Highs_deleteRowsByRange(model, r, r) + _check_ret(ret) + for info in values(model.affine_constraint_info) + if info.row > r + info.row -= 1 end end - return MOI.ScalarAffineFunction{Float64}(terms, o.objective_constant) + key = _ConstraintKey(c.value) + delete!(model.affine_constraint_info, key) + model.name_to_constraint_index = nothing + return end -function MOI.get(o::Optimizer, attr::MOI.ObjectiveValue) - MOI.check_result_index_bounds(o, attr) - value = Ref{Cdouble}() - Highs_getHighsDoubleInfoValue(o, "objective_function_value", value); - return o.objective_constant + value[] +function MOI.get( + model::Optimizer, + ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},S}, +) where {S<:_SCALAR_SETS} + return _set(_info(model, c)) end -function MOI.get(o::Optimizer, ::MOI.SimplexIterations) - simplex_iteration_count = Ref{Cint}(0) - Highs_getHighsIntInfoValue(o, "simplex_iteration_count", simplex_iteration_count) - return Int(simplex_iteration_count[]) +function MOI.set( + model::Optimizer, + ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},S}, + s::S, +) where {S<:_SCALAR_SETS} + lower, upper = _bounds(s) + info = _info(model, c) + ret = Highs_changeRowBounds(model, info.row, lower, upper) + _check_ret(ret) + info.lower, info.upper = lower, upper + return end -function MOI.get(o::Optimizer, ::MOI.BarrierIterations) - return 0 +function MOI.get( + model::Optimizer, + ::MOI.ConstraintFunction, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},S}, +) where {S<:_SCALAR_SETS} + r = row(model, c) + num_row = Ref{Cint}(0) + num_nz = Ref{Cint}(0) + matrix_start = Ref{Cint}(0) + lower, upper = Ref{Cdouble}(), Ref{Cdouble}() + _ = Highs_getRowsByRange( + model, + r, + r, + num_row, + lower, + upper, + num_nz, + matrix_start, + C_NULL, + C_NULL, + ) + matrix_index = Vector{Cint}(undef, num_nz[]) + matrix_value = Vector{Cdouble}(undef, num_nz[]) + ret = Highs_getRowsByRange( + model, + r, + r, + num_row, + lower, + upper, + num_nz, + matrix_start, + matrix_index, + matrix_value, + ) + _check_ret(ret) + return MOI.ScalarAffineFunction( + MOI.ScalarAffineTerm{Float64}[ + MOI.ScalarAffineTerm( + val, + model.variable_info[CleverDicts.LinearIndex(col + 1)].index, + ) for (col, val) in zip(matrix_index, matrix_value) if !iszero(val) + ], + 0.0, + ) end -function MOI.get(o::Optimizer, ::MOI.VariableName, v::MOI.VariableIndex) - return get(o.variable_map, v, "") +function MOI.supports( + ::Optimizer, + ::MOI.ConstraintName, + ::Type{ + <:MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},<:_SCALAR_SETS}, + }, +) + return true end -function MOI.set(o::Optimizer, ::MOI.VariableName, v::MOI.VariableIndex, name::String) - o.variable_map[v] = name +function MOI.get( + model::Optimizer, + ::MOI.ConstraintName, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},<:Any}, +) + return _info(model, c).name +end + +function MOI.set( + model::Optimizer, + ::MOI.ConstraintName, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},<:Any}, + name::String, +) + info = _info(model, c) + info.name = name + model.name_to_constraint_index = nothing return end -function MOI.get(o::Optimizer, ::Type{MOI.VariableIndex}, name::String) - for (vi, vname) in o.variable_map - if vname == name - return vi +function MOI.get(model::Optimizer, ::Type{MOI.ConstraintIndex}, name::String) + if model.name_to_constraint_index === nothing + _rebuild_name_to_constraint_index(model) + end + if haskey(model.name_to_constraint_index, name) + constr = model.name_to_constraint_index[name] + if constr === nothing + error("Duplicate constraint name detected: $(name)") end + return constr end return nothing end -function MOI.get(o::Optimizer, ::MOI.TimeLimitSec) - return MOI.get(o, MOI.RawParameter("time_limit")) +function MOI.get( + model::Optimizer, + C::Type{MOI.ConstraintIndex{F,S}}, + name::String, +) where {F,S} + index = MOI.get(model, MOI.ConstraintIndex, name) + if index isa C + return index::MOI.ConstraintIndex{F,S} + end + return nothing end -function MOI.set(o::Optimizer, ::MOI.TimeLimitSec, value::Real) - return MOI.set(o, MOI.RawParameter("time_limit"), Cdouble(value)) +function _rebuild_name_to_constraint_index(model::Optimizer) + model.name_to_constraint_index = + Dict{String,Union{Nothing,MOI.ConstraintIndex}}() + for (key, info) in model.affine_constraint_info + if isempty(info.name) + continue + end + S = typeof(_set(info)) + _set_name_to_constraint_index( + model, + info.name, + 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 -function MOI.set(o::Optimizer, ::MOI.TimeLimitSec, ::Nothing) - # TODO(odow): handle default time limit? +function _set_name_to_constraint_index( + model::Optimizer, + name::String, + index::MOI.ConstraintIndex, +) + if haskey(model.name_to_constraint_index, name) + model.name_to_constraint_index[name] = nothing + else + model.name_to_constraint_index[name] = index + end + return +end + +# TODO(odow): doesn't look like HiGHS supports these. +# +# function MOI.modify( +# model::Optimizer, +# c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},S}, +# chg::MOI.ScalarCoefficientChange{Float64} +# ) where {S<:_SCALAR_SETS} +# return +# end +# +# function MOI.set( +# model::Optimizer, +# ::MOI.ConstraintFunction, +# c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, <:_SCALAR_SETS}, +# f::MOI.ScalarAffineFunction{Float64} +# ) +# return +# end + +### +### Optimize methods. +### + +function _store_solution(model::Optimizer) + x = model.solution + numCols = Highs_getNumCols(model) + numRows = Highs_getNumRows(model) + resize!(x.colvalue, numCols) + resize!(x.coldual, numCols) + resize!(x.rowvalue, numRows) + resize!(x.rowdual, numRows) + Highs_getSolution(model, x.colvalue, x.coldual, x.rowvalue, x.rowdual) + return +end + +function MOI.optimize!(model::Optimizer) + ret = Highs_run(model) + # `ret` is an `HighsStatus` enum: {OK = 0, Warning, Error}. + if ret == 2 + error( + "Encountered an error in HiGHS: HighsStatus::Error. " * + "Check the log for details" + ) + end + model.optimize_called = true + if MOI.get(model, MOI.ResultCount()) == 1 + _store_solution(model) + end return end + +""" + HighsModelStatus + +An enum for the HiGHS simplex status codes. +""" +@enum HighsModelStatus begin + NOTSET = 0 + LOAD_ERROR + MODEL_ERROR + PRESOLVE_ERROR + SOLVE_ERROR + POSTSOLVE_ERROR + MODEL_EMPTY + PRIMAL_INFEASIBLE + PRIMAL_UNBOUNDED + OPTIMAL + REACHED_DUAL_OBJECTIVE_VALUE_UPPER_BOUND + REACHED_TIME_LIMIT + REACHED_ITERATION_LIMIT + PRIMAL_DUAL_INFEASIBLE + DUAL_INFEASIBLE +end + +function MOI.get(model::Optimizer, ::MOI.TerminationStatus) + if model.optimize_called == false + return MOI.OPTIMIZE_NOT_CALLED + end + status = HighsModelStatus(Highs_getModelStatus(model.inner, Cint(0))) + if status == LOAD_ERROR + return MOI.OTHER_ERROR + elseif status == MODEL_ERROR + return MOI.INVALID_MODEL + elseif status == PRESOLVE_ERROR + return MOI.OTHER_ERROR + elseif status == SOLVE_ERROR + return MOI.OTHER_ERROR + elseif status == POSTSOLVE_ERROR + return MOI.OTHER_ERROR + elseif status == MODEL_EMPTY + return MOI.INVALID_MODEL + elseif status == PRIMAL_INFEASIBLE + return MOI.INFEASIBLE + elseif status == PRIMAL_UNBOUNDED + return MOI.DUAL_INFEASIBLE + elseif status == OPTIMAL + return MOI.OPTIMAL + elseif status == REACHED_DUAL_OBJECTIVE_VALUE_UPPER_BOUND + return MOI.OBJECTIVE_LIMIT + elseif status == REACHED_TIME_LIMIT + return MOI.TIME_LIMIT + elseif status == REACHED_ITERATION_LIMIT + return MOI.ITERATION_LIMIT + elseif status == PRIMAL_DUAL_INFEASIBLE + return MOI.INFEASIBLE + else + @assert status == DUAL_INFEASIBLE + return MOI.DUAL_INFEASIBLE + end +end + +function MOI.get(model::Optimizer, ::MOI.ResultCount) + status = HighsModelStatus(Highs_getModelStatus(model, Cint(0))) + return status == OPTIMAL ? 1 : 0 +end + +function MOI.get(model::Optimizer, ::MOI.RawStatusString) + status = HighsModelStatus(Highs_getModelStatus(model, Cint(0))) + return string(status) +end + +function MOI.get(model::Optimizer, attr::MOI.PrimalStatus) + if attr.N != 1 + return MOI.NO_SOLUTION + elseif MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL + return MOI.FEASIBLE_POINT + end + return MOI.NO_SOLUTION +end + +function MOI.get(model::Optimizer, attr::MOI.DualStatus) + if attr.N != 1 + return MOI.NO_SOLUTION + elseif MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL + return MOI.FEASIBLE_POINT + end + return MOI.NO_SOLUTION +end + +function MOI.get(model::Optimizer, attr::MOI.ObjectiveValue) + MOI.check_result_index_bounds(model, attr) + return Highs_getObjectiveValue(model) + model.objective_constant +end + +function MOI.get(model::Optimizer, attr::MOI.DualObjectiveValue) + MOI.check_result_index_bounds(model, attr) + return MOI.Utilities.get_fallback(model, attr, Float64) +end + +function MOI.get(model::Optimizer, ::MOI.SolveTime) + return Highs_getHighsRunTime(model) +end + +function MOI.get(model::Optimizer, ::MOI.SimplexIterations) + return Highs_getSimplexIterationCount(model) +end + +function MOI.get( + model::Optimizer, + attr::MOI.VariablePrimal, + x::MOI.VariableIndex, +) + MOI.check_result_index_bounds(model, attr) + return model.solution.colvalue[column(model, x)+1] +end + +function MOI.get( + model::Optimizer, + attr::MOI.ConstraintPrimal, + c::MOI.ConstraintIndex{MOI.SingleVariable,<:_SCALAR_SETS}, +) + MOI.check_result_index_bounds(model, attr) + return MOI.get(model, MOI.VariablePrimal(), MOI.VariableIndex(c.value)) +end + +function MOI.get( + model::Optimizer, + attr::MOI.ConstraintPrimal, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},<:_SCALAR_SETS}, +) + MOI.check_result_index_bounds(model, attr) + return model.solution.rowvalue[row(model, c)+1] +end + +function _dual_multiplier(model::Optimizer) + return MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE ? -1 : 1 +end + +""" + _signed_dual(model::Optimizer, dual::Float64, ::Type{Set}) + +A heuristic for determining whether the dual of an interval constraint applies +to the lower or upper bound. It can be wrong by at most the solver's tolerance. +""" +function _signed_dual end + +function _signed_dual( + model::Optimizer, + dual::Float64, + ::Type{MOI.LessThan{Float64}}, +) + return min(_dual_multiplier(model) * dual, 0.0) +end + +function _signed_dual( + model::Optimizer, + dual::Float64, + ::Type{MOI.GreaterThan{Float64}}, +) + return max(_dual_multiplier(model) * dual, 0.0) +end + +function _signed_dual(model::Optimizer, dual::Float64, ::Any) + return _dual_multiplier(model) * dual +end + +function MOI.get( + model::Optimizer, + attr::MOI.ConstraintDual, + c::MOI.ConstraintIndex{MOI.SingleVariable,S}, +) where {S<:_SCALAR_SETS} + MOI.check_result_index_bounds(model, attr) + reduced_cost = model.solution.coldual[column(model, c)+1] + return _signed_dual(model, reduced_cost, S) +end + +function MOI.get( + model::Optimizer, + attr::MOI.ConstraintDual, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},S}, +) where {S<:_SCALAR_SETS} + MOI.check_result_index_bounds(model, attr) + dual = model.solution.rowdual[row(model, c)+1] + # TODO(odow): Ask HiGHS why their convention for row duals is the opposite + # of their convention for column duals. I guess because they transform the + # constraints into `Ax - Iy = 0`? + return _signed_dual(model, -dual, S) +end diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index 6d85982..cbb0aea 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -6,26 +6,184 @@ using Test const MOI = MathOptInterface -const CONFIG = MOI.Test.TestConfig() +const OPTIMIZER = MOI.Bridges.full_bridge_optimizer(HiGHS.Optimizer(), Float64) +# TODO(odow): HiGHS is not silent. +MOI.set(OPTIMIZER, MOI.Silent(), true) + +const CONFIG = MOI.Test.TestConfig( + # TODO(odow): add support for modifying the constraint matrix. + modify_lhs = false, + # TODO(odow): add infeasibility certificates. + infeas_certificates = false, + # TODO(odow): add support for MOI.ConstraintBasisStatus. + basis = false, +) + +function test_basic_constraint_tests() + return MOI.Test.basic_constraint_tests(OPTIMIZER, CONFIG) +end + +function test_unittest() + return MOI.Test.unittest( + OPTIMIZER, + CONFIG, + String[ + # TODO(odow): + # Add support for MOI.ObjectiveBound. + "solve_objbound_edge_cases", + # Add support for MOI.NumberOfThreads. + "number_threads", + + # These are excluded because HiGHS does not support them. + "delete_soc_variables", + "solve_integer_edge_cases", + "solve_qcp_edge_cases", + "solve_qp_edge_cases", + "solve_zero_one_with_bounds_1", + "solve_zero_one_with_bounds_2", + "solve_zero_one_with_bounds_3", + ], + ) +end + +function test_modificationtest() + # TODO(odow): HiGHS doesn't support modifying the constraint matrix, so use + # a caching optimizer. + MOI.empty!(OPTIMIZER) + cache = MOI.Utilities.CachingOptimizer( + MOI.Utilities.Model{Float64}(), + OPTIMIZER, + ) + return MOI.Test.modificationtest(cache, CONFIG) +end + +function test_contlineartest() + return MOI.Test.contlineartest( + OPTIMIZER, + CONFIG, + String[ + # Upstream segfault. Reported: https://github.com/ERGO-Code/HiGHS/issues/448 + "linear8b", + + # VariablePrimalStart not supported. + "partial_start", + ], + ) +end + +function test_lintest() + return MOI.Test.lintest(OPTIMIZER, CONFIG) +end + +function test_SolverName() + @test MOI.get(OPTIMIZER, MOI.SolverName()) == "HiGHS" +end + +function test_default_objective() + return MOI.Test.default_objective_test(OPTIMIZER) +end + +function test_default_status_test() + return MOI.Test.default_status_test(OPTIMIZER) +end + +function test_nametest() + return MOI.Test.nametest(OPTIMIZER) +end + +function test_validtest() + return MOI.Test.validtest(OPTIMIZER) +end + +function test_emptytest() + return MOI.Test.emptytest(OPTIMIZER) +end + +function test_orderedindicestest() + return MOI.Test.orderedindicestest(OPTIMIZER) +end + +function test_copytest() + return MOI.Test.copytest( + OPTIMIZER, + MOI.Bridges.full_bridge_optimizer(HiGHS.Optimizer(), Float64), + ) +end + +function test_scalar_function_constant_not_zero() + return MOI.Test.scalar_function_constant_not_zero(OPTIMIZER) +end + +function test_supports_constrainttest() + # supports_constrainttest needs VectorOfVariables-in-Zeros, + # MOI.Test.supports_constrainttest(HiGHS.Optimizer(), Float64, Float32) + # but supports_constrainttest is broken via bridges: + MOI.empty!(OPTIMIZER) + MOI.add_variable(OPTIMIZER) + @test MOI.supports_constraint( + OPTIMIZER, + MOI.SingleVariable, + MOI.EqualTo{Float64}, + ) + @test MOI.supports_constraint( + OPTIMIZER, + MOI.ScalarAffineFunction{Float64}, + MOI.EqualTo{Float64}, + ) + # This test is broken for some reason: + @test_broken !MOI.supports_constraint( + OPTIMIZER, + MOI.ScalarAffineFunction{Int}, + MOI.EqualTo{Float64}, + ) + @test !MOI.supports_constraint( + OPTIMIZER, + MOI.ScalarAffineFunction{Int}, + MOI.EqualTo{Int}, + ) + @test !MOI.supports_constraint( + OPTIMIZER, + MOI.SingleVariable, + MOI.EqualTo{Int}, + ) + @test MOI.supports_constraint(OPTIMIZER, MOI.VectorOfVariables, MOI.Zeros) + @test !MOI.supports_constraint( + OPTIMIZER, + MOI.VectorOfVariables, + MOI.EqualTo{Float64}, + ) + @test !MOI.supports_constraint(OPTIMIZER, MOI.SingleVariable, MOI.Zeros) + @test !MOI.supports_constraint( + OPTIMIZER, + MOI.VectorOfVariables, + MOI.Test.UnknownVectorSet, + ) +end + +function test_set_lower_bound_twice() + return MOI.Test.set_lower_bound_twice(HiGHS.Optimizer(), Float64) +end + +function test_set_upper_bound_twice() + return MOI.Test.set_upper_bound_twice(HiGHS.Optimizer(), Float64) +end function test_Attributes() o = HiGHS.Optimizer() - @test MOI.supports(o, MOI.SolverName()) @test MOI.get(o, MOI.SolverName()) == "HiGHS" @test MOI.get(o, MOI.TimeLimitSec()) > 10000 MOI.set(o, MOI.TimeLimitSec(), 500) @test MOI.get(o, MOI.TimeLimitSec()) == 500.0 - @test MOI.supports(o, MOI.RawSolver()) @test MOI.get(o, MOI.RawSolver()) == o end function test_MOI_variable_count_and_empty() o = HiGHS.Optimizer() + @test MOI.get(o, MOI.NumberOfVariables()) == 0 x1 = MOI.add_variable(o) - @test x1.value == 0 - @test MOI.supports_constraint(o, MOI.SingleVariable(x1), MOI.Interval(0, 1)) - (x2, _) = MOI.add_constrained_variable(o, MOI.Interval(0, 1)) - @test x2.value == 1 + @test MOI.get(o, MOI.NumberOfVariables()) == 1 + @test MOI.supports_constraint(o, MOI.SingleVariable, MOI.Interval{Float64}) + x2, _ = MOI.add_constrained_variable(o, MOI.Interval(0.0, 1.0)) @test MOI.get(o, MOI.NumberOfVariables()) == 2 MOI.empty!(o) @test MOI.get(o, MOI.NumberOfVariables()) == 0 @@ -33,8 +191,13 @@ end function test_Getting_objective_value() o = HiGHS.Optimizer() + MOI.set(o, MOI.Silent(), true) (x, _) = MOI.add_constrained_variable(o, MOI.Interval(-3.0, 6.0)) - HiGHS.Highs_changeColCost(o, Cint(x.value), 1.0) + MOI.set( + o, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0), + ) @test MOI.get(o, MOI.ObjectiveSense()) == MOI.FEASIBILITY_SENSE MOI.set(o, MOI.ObjectiveSense(), MOI.MIN_SENSE) @test MOI.get(o, MOI.ObjectiveSense()) == MOI.MIN_SENSE @@ -46,18 +209,22 @@ end function test_Max_in_box() o = HiGHS.Optimizer() + MOI.set(o, MOI.Silent(), true) @test MOI.get(o, MOI.ResultCount()) == 0 (x, _) = MOI.add_constrained_variable(o, MOI.Interval(-3.0, 6.0)) MOI.set(o, MOI.ObjectiveSense(), MOI.MAX_SENSE) - HiGHS.Highs_changeColCost(o, Cint(x.value), 2.0) + MOI.set( + o, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(2.0, x)], 0.0), + ) MOI.optimize!(o) @test MOI.get(o, MOI.ObjectiveValue()) ≈ 2 * 6 - obj_func = MOI.get(o, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()) + obj_func = + MOI.get(o, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()) @test MOI.get(o, MOI.TerminationStatus()) == MOI.OPTIMAL - @test obj_func ≈ MOI.ScalarAffineFunction([ - MOI.ScalarAffineTerm(2.0, x), - ], 0.0, - ) + @test obj_func ≈ + MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(2.0, x)], 0.0) end function test_Objective_function_obtained_from_model_corresponds() @@ -65,40 +232,48 @@ function test_Objective_function_obtained_from_model_corresponds() (x1, _) = MOI.add_constrained_variable(o, MOI.Interval(-3.0, 6.0)) (x2, _) = MOI.add_constrained_variable(o, MOI.Interval(1.0, 2.0)) MOI.set(o, MOI.ObjectiveSense(), MOI.MIN_SENSE) - HiGHS.Highs_changeColCost(o, Cint(x1.value), 2.0) - HiGHS.Highs_changeColCost(o, Cint(x2.value), -1.0) + MOI.set( + o, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction( + MOI.ScalarAffineTerm.([2.0, -1.0], [x1, x2]), + 0.0, + ), + ) F = MOI.get(o, MOI.ObjectiveFunctionType()) @test F <: MOI.ScalarAffineFunction{Float64} obj_func = MOI.get(o, MOI.ObjectiveFunction{F}()) @test MOI.supports(o, MOI.ObjectiveFunction{F}()) @test all(MOI.get(o, MOI.ListOfVariableIndices()) .== [x1, x2]) - @test obj_func ≈ MOI.ScalarAffineFunction([ - MOI.ScalarAffineTerm(2.0, x1), - MOI.ScalarAffineTerm(-1.0, x2), - ], 0.0, + @test obj_func ≈ MOI.ScalarAffineFunction( + [MOI.ScalarAffineTerm(2.0, x1), MOI.ScalarAffineTerm(-1.0, x2)], + 0.0, ) MOI.set(o, MOI.ObjectiveFunction{F}(), obj_func) obj_func = MOI.get(o, MOI.ObjectiveFunction{F}()) - @test obj_func ≈ MOI.ScalarAffineFunction([ - MOI.ScalarAffineTerm(2.0, x1), - MOI.ScalarAffineTerm(-1.0, x2), - ], 0.0, + @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(o, MOI.ObjectiveFunction{F}(), obj_func) obj_func = MOI.get(o, MOI.ObjectiveFunction{F}()) - @test obj_func ≈ MOI.ScalarAffineFunction([ - MOI.ScalarAffineTerm(3.0, x1), - MOI.ScalarAffineTerm(-1.0, x2), - ], 0.0, + @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() o = HiGHS.Optimizer() + MOI.set(o, MOI.Silent(), true) x = MOI.add_variable(o) _ = MOI.add_constraint(o, MOI.SingleVariable(x), MOI.Interval(-3.0, 6.0)) - HiGHS.Highs_changeColCost(o, Cint(x.value), 1.0) + MOI.set( + o, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0), + ) MOI.set(o, MOI.ObjectiveSense(), MOI.MIN_SENSE) @test MOI.get(o, MOI.ResultCount()) == 0 MOI.optimize!(o) @@ -108,12 +283,11 @@ end function test_Constant_in_objective_function() o = HiGHS.Optimizer() + MOI.set(o, MOI.Silent(), true) x = MOI.add_variable(o) _ = MOI.add_constraint(o, MOI.SingleVariable(x), MOI.Interval(-3.0, 6.0)) MOI.set(o, MOI.ObjectiveSense(), MOI.MIN_SENSE) - obj_func = MOI.ScalarAffineFunction( - [MOI.ScalarAffineTerm(1.0, x)], 3.0, - ) + obj_func = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 3.0) MOI.set(o, MOI.ObjectiveFunction{typeof(obj_func)}(), obj_func) MOI.optimize!(o) @test MOI.get(o, MOI.ResultCount()) == 1 @@ -131,31 +305,41 @@ function test_Linear_constraints() # st 0 <= x{1,2} <= 5 # 0 <= x1 + x2 <= 7.5 o = HiGHS.Optimizer() + MOI.set(o, MOI.Silent(), true) (x1, _) = MOI.add_constrained_variable(o, MOI.Interval(0.0, 5.0)) (x2, _) = MOI.add_constrained_variable(o, MOI.Interval(0.0, 5.0)) MOI.set(o, MOI.ObjectiveSense(), MOI.MAX_SENSE) func = MOI.ScalarAffineFunction( - [ - MOI.ScalarAffineTerm(1.0, x1), - MOI.ScalarAffineTerm(2.0, x2), - ], 0.0, + [MOI.ScalarAffineTerm(1.0, x1), MOI.ScalarAffineTerm(2.0, x2)], + 0.0, ) - @test MOI.supports_constraint(o, func, MOI.Interval(0, 1)) + @test MOI.supports_constraint(o, typeof(func), MOI.Interval{Float64}) MOI.set(o, MOI.ObjectiveFunction{typeof(func)}(), func) - @test MOI.get(o, MOI.NumberOfConstraints{MOI.ScalarAffineFunction{Float64}, MOI.Interval{Float64}}()) == 0 - MOI.add_constraint(o, + @test MOI.get( + o, + MOI.NumberOfConstraints{ + MOI.ScalarAffineFunction{Float64}, + MOI.Interval{Float64}, + }(), + ) == 0 + MOI.add_constraint( + o, MOI.ScalarAffineFunction( - [ - MOI.ScalarAffineTerm(1.0, x1), - MOI.ScalarAffineTerm(1.0, x2), - ], 0.0, - ), MOI.Interval(0.0, 7.5), + [MOI.ScalarAffineTerm(1.0, x1), MOI.ScalarAffineTerm(1.0, x2)], + 0.0, + ), + MOI.Interval(0.0, 7.5), ) - @test MOI.get(o, MOI.NumberOfConstraints{MOI.ScalarAffineFunction{Float64}, MOI.Interval{Float64}}()) == 1 + @test MOI.get( + o, + MOI.NumberOfConstraints{ + MOI.ScalarAffineFunction{Float64}, + MOI.Interval{Float64}, + }(), + ) == 1 MOI.optimize!(o) @test MOI.get(o, MOI.ObjectiveValue()) ≈ 12.5 @test MOI.get(o, MOI.SimplexIterations()) > 0 - @test MOI.get(o, MOI.BarrierIterations()) == 0 end function test_Variable_names() @@ -194,25 +378,55 @@ function test_Model_empty() MOI.set( o, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - MOI.ScalarAffineFunction{Float64}([], 0.0) + MOI.ScalarAffineFunction{Float64}([], 0.0), ) @test MOI.is_empty(o) MOI.set( o, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - MOI.ScalarAffineFunction{Float64}([], 3.0) + MOI.ScalarAffineFunction{Float64}([], 3.0), ) @test !MOI.is_empty(o) MOI.set(o, MOI.ObjectiveSense(), MOI.FEASIBILITY_SENSE) @test MOI.is_empty(o) - @test MOI.get(o, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()) ≈ MOI.ScalarAffineFunction{Float64}([], 0.0) + @test MOI.get( + o, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + ) ≈ MOI.ScalarAffineFunction{Float64}([], 0.0) x = MOI.add_variable(o) - MOI.set( + return MOI.set( o, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - MOI.ScalarAffineFunction{Float64}([MOI.ScalarAffineTerm(1.0, x)], 0.0) + MOI.ScalarAffineFunction{Float64}([MOI.ScalarAffineTerm(1.0, x)], 0.0), + ) +end + +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 = HiGHS.Optimizer() + for key in keys(HiGHS._OPTIONS) + v = MOI.get(model, MOI.RawParameter(key)) + MOI.set(model, MOI.RawParameter(key), v) + v2 = MOI.get(model, MOI.RawParameter(key)) + @test v == v2 + end + return +end + +function test_option_unknown_option() + model = HiGHS.Optimizer() + err = ErrorException( + "Encountered an error in HiGHS: OptionStatus::UNKNOWN_OPTION. " * + "Check the log for details." ) - @test_throws ErrorException MOI.optimize!(o) + @test_throws err MOI.set(model, MOI.RawParameter("presolve"), 1) + @test_throws err MOI.set(model, MOI.RawParameter("message_level"), -1) + return end function runtests()