diff --git a/.gitignore b/.gitignore index ba39cc5..1734ebf 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ Manifest.toml +*.json +perf/Project.toml diff --git a/Project.toml b/Project.toml index e22e21b..05d5b57 100644 --- a/Project.toml +++ b/Project.toml @@ -1,17 +1,17 @@ name = "GLPK" uuid = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" repo = "https://github.com/JuliaOpt/GLPK.jl.git" -version = "0.10.0" +version = "0.11.0" [deps] BinaryProvider = "b99e7846-7c00-51b0-8f62-c81ae34c0232" Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" -LinQuadOptInterface = "f8899e07-120b-5979-ab1d-7b97bb9e1a48" +MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [compat] BinaryProvider = "≥ 0.3.0" -LinQuadOptInterface = "0.6" +MathOptInterface = "0.9" julia = "1" [extras] diff --git a/README.md b/README.md index fce083e..99b2d7a 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,13 @@ -Julia GLPK module -================= +# GLPK.jl - -| **Documentation** | **Build Status** | -|:-------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------------------:| -| [![][docs-stable-img]][docs-stable-url] [![][docs-latest-img]][docs-latest-url] | [![][travis-img]][travis-url] [![][appveyor-img]][appveyor-url] [![][coveralls-img]][coveralls-url] | +| **Build Status** | +|:---------------------------------------------------------------------------------------------------:| +| [![][travis-img]][travis-url] [![][appveyor-img]][appveyor-url] [![][coveralls-img]][coveralls-url] | GLPK.jl is a wrapper for the [GNU Linear Programming Kit library](http://www.gnu.org/software/glpk). It makes it possible to access nearly all of GLPK functionality from within Julia programs. -See also the [GLPKMathProgInterface.jl](https://github.com/JuliaOpt/GLPKMathProgInterface.jl) package for using it with -[MathProgBase.jl](https://github.com/JuliaOpt/MathProgBase.jl) and [JuMP.jl](https://github.com/JuliaOpt/JuMP.jl). - This package is part of [the JuliaOpt project](http://www.juliaopt.org/). ## Installation @@ -39,16 +34,34 @@ in GLPK's source folder. Note that the custom binaries will not be overwritten by subsequent builds of the currently installed version of GLPK.jl. However, if GLPK.jl is updated and the update includes new BinaryProvider versions of the GLPK binaries, then the custom binaries will be overwritten by the new BinaryProvider versions. -## Documentation +## `GLPK.Optimizer` + +Use `GLPK.Optimizer` to create a new optimizer object: +```julia +using GLPK +model = GLPK.Optimizer(tm_lim = 60.0, msg_lev = GLPK.OFF) +``` +For JuMP, use: +```julia +using JuMP, GLPK +model = Model( + with_optimizer(GLPK.Optimizer, tm_lim = 60.0, msg_lev = GLPK.OFF) +) +``` -- [**STABLE**][docs-stable-url] — **most recently tagged version of the documentation.** -- [**LATEST**][docs-latest-url] — *in-development version of the documentation.* +**Note: previous versions of `GLPK.jl` required you to choose either `GLPKSolverLP` or `GLPKSolverMIP`. This is no longer needed; just use `GLPK.Optimizer`.** -[docs-latest-img]: https://img.shields.io/badge/docs-latest-blue.svg -[docs-latest-url]: https://gplkjl.readthedocs.org/en/latest/glpk.html +## Pre-emptive checks -[docs-stable-img]: https://img.shields.io/badge/docs-stable-blue.svg -[docs-stable-url]: https://gplkjl.readthedocs.org/en/stable/glpk.html +`GLPK.jl` has a lot of pre-emptive checks to catch cases where the C API might +throw an uninformative error. However, in benchmarks, this takes a +non-negligible amount of time (e.g. 20% in add_constraints). At the risk of +possibly running into an uninformative error, you can run the following after +importing GLPK to disable these checks: +```julia +using GLPK +GLPK.jl_set_preemptive_check(false) +``` [travis-img]: https://api.travis-ci.org/JuliaOpt/GLPK.jl.svg?branch=master [travis-url]: https://travis-ci.org/JuliaOpt/GLPK.jl @@ -58,8 +71,3 @@ Note that the custom binaries will not be overwritten by subsequent builds of th [coveralls-img]: https://img.shields.io/coveralls/JuliaOpt/GLPK.jl.svg [coveralls-url]: https://coveralls.io/r/JuliaOpt/GLPK.jl - -[pkg-0.6-img]: http://pkg.julialang.org/badges/GLPK_0.6.svg -[pkg-0.6-url]: http://pkg.julialang.org/?pkg=GLPK -[pkg-0.7-img]: http://pkg.julialang.org/badges/GLPK_0.7.svg -[pkg-0.7-url]: http://pkg.julialang.org/?pkg=GLPK diff --git a/perf/perf.jl b/perf/perf.jl new file mode 100644 index 0000000..50af3e6 --- /dev/null +++ b/perf/perf.jl @@ -0,0 +1,41 @@ +using GLPK + +function print_help() + println(""" + Usage + perf.jl [arg] [name] + + [arg] + --new Begin a new benchmark comparison + --compare Run another benchmark and compare to existing + + [name] A name for the benchmark test + + Examples + git checkout master + julia perf.jl --new master + git checkout approach_1 + julia perf.jl --new approach_1 + git checkout approach_2 + julia perf.jl --compare master + julia perf.jl --compare approach_1 + """) +end + +if length(ARGS) != 2 + print_help() +else + const Benchmarks = GLPK.MOI.Benchmarks + const suite = Benchmarks.suite(() -> GLPK.Optimizer()) + if ARGS[1] == "--new" + Benchmarks.create_baseline( + suite, ARGS[2]; directory = @__DIR__, verbose = true + ) + elseif ARGS[1] == "--compare" + Benchmarks.compare_against_baseline( + suite, ARGS[2]; directory = @__DIR__, verbose = true + ) + else + print_help() + end +end diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 030f261..c1264c3 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -1,871 +1,1876 @@ -using LinQuadOptInterface - -const LQOI = LinQuadOptInterface -const MOI = LQOI.MOI - -const SUPPORTED_OBJECTIVES = [ - LQOI.SinVar, - LQOI.Linear -] - -const SUPPORTED_CONSTRAINTS = [ - (LQOI.Linear, LQOI.EQ), - (LQOI.Linear, LQOI.LE), - (LQOI.Linear, LQOI.GE), - (LQOI.Linear, LQOI.IV), - (LQOI.SinVar, LQOI.EQ), - (LQOI.SinVar, LQOI.LE), - (LQOI.SinVar, LQOI.GE), - (LQOI.SinVar, LQOI.IV), - (LQOI.VecVar, MOI.Nonnegatives), - (LQOI.VecVar, MOI.Nonpositives), - (LQOI.VecVar, MOI.Zeros), - (LQOI.VecLin, MOI.Nonnegatives), - (LQOI.VecLin, MOI.Nonpositives), - (LQOI.VecLin, MOI.Zeros), - (LQOI.SinVar, MOI.ZeroOne), - (LQOI.SinVar, MOI.Integer) -] - -""" - AbstractCallbackData - -An abstract type to prevent recursive type definition of Optimizer and -CallbackData, each of which need the other type in a field. -""" -abstract type AbstractCallbackData end - -mutable struct Optimizer <: LQOI.LinQuadOptimizer - LQOI.@LinQuadOptimizerBase - presolve::Bool - method::Symbol - interior::GLPK.InteriorParam - intopt::GLPK.IntoptParam - simplex::SimplexParam - solver_status::Int32 - last_solved_by_mip::Bool - # See note associated with abstractcallbackdata. When using, make sure to - # add a type assertion, since this will always be the concrete type - # CallbackData. - callback_data::AbstractCallbackData - objective_bound::Float64 - callback_function::Function - # See https://github.com/JuliaOpt/GLPKMathProgInterface.jl/pull/15 - # for why this is necesary. GLPK interacts weirdly with binary variables and - # bound modification. So lets set binary variables as "Integer" with [0,1] - # bounds that we enforce just before solve. - binaries::Vector{Int} - Optimizer(::Nothing) = new() +import MathOptInterface + +const MOI = MathOptInterface +const CleverDicts = MOI.Utilities.CleverDicts + +@enum(TypeEnum, CONTINUOUS, BINARY, INTEGER) +@enum(BoundEnum, NONE, LESS_THAN, GREATER_THAN, LESS_AND_GREATER_THAN, INTERVAL, EQUAL_TO) +@enum(ObjectiveEnum, SINGLE_VARIABLE, SCALAR_AFFINE) +@enum(MethodEnum, SIMPLEX, INTERIOR, EXACT) + +mutable struct VariableInfo + index::MOI.VariableIndex + column::Int + bound::BoundEnum + type::TypeEnum + name::String + # Storage for constraint names associated with variables because GLPK + # can only store names for variables and proper constraints. + # We can perform an optimization and only store three strings for the + # constraint names because, at most, there can be three SingleVariable + # constraints, e.g., LessThan, GreaterThan, and Integer. + lessthan_name::String + greaterthan_interval_or_equalto_name::String + type_constraint_name::String + function VariableInfo(index::MOI.VariableIndex, column::Int) + return new(index, column, NONE, CONTINUOUS, "", "", "", "") + end end -""" - CallbackData -""" -mutable struct CallbackData <: AbstractCallbackData - model::Optimizer - tree::Ptr{Cvoid} - CallbackData(model::Optimizer) = new(model, C_NULL) +struct ConstraintKey + value::Int +end +CleverDicts.key_to_index(k::ConstraintKey) = k.value +CleverDicts.index_to_key(::Type{ConstraintKey}, index) = ConstraintKey(index) + +mutable struct ConstraintInfo + row::Int + set::MOI.AbstractSet + name::String + ConstraintInfo(set) = new(0, set, "") end -""" - __internal_callback__(tree::Ptr{Cvoid}, info::Ptr{Cvoid}) - -Dummy callback function for internal use only. Responsible for updating the -objective bound. -""" -function __internal_callback__(tree::Ptr{Cvoid}, info::Ptr{Cvoid}) +# Dummy callback function for internal use only. Responsible for updating the +# objective bound, saving the mip gap, and calling the user's callback. +function _internal_callback(tree::Ptr{Cvoid}, info::Ptr{Cvoid}) callback_data = unsafe_pointer_to_objref(info)::CallbackData model = callback_data.model callback_data.tree = tree node = GLPK.ios_best_node(tree) if node != 0 model.objective_bound = GLPK.ios_node_bound(tree, node) + model.relative_gap = GLPK.ios_mip_gap(tree) end model.callback_function(callback_data) return nothing end -function Optimizer(;presolve=false, method=:Simplex, kwargs...) - optimizer = Optimizer(nothing) - MOI.empty!(optimizer) - optimizer.presolve = presolve - optimizer.method = method - optimizer.interior = GLPK.InteriorParam() - optimizer.intopt = GLPK.IntoptParam() - optimizer.simplex = GLPK.SimplexParam() - solver_status = Int32(0) - optimizer.last_solved_by_mip = false - optimizer.callback_data = CallbackData(optimizer) - optimizer.intopt.cb_func = @cfunction(__internal_callback__, Cvoid, (Ptr{Cvoid}, Ptr{Cvoid})) - optimizer.intopt.cb_info = pointer_from_objref(optimizer.callback_data) - optimizer.objective_bound = NaN - optimizer.callback_function = (cb_data::CallbackData) -> nothing - optimizer.binaries = Int[] - # Parameters - if presolve - optimizer.simplex.presolve = GLPK.ON - optimizer.intopt.presolve = GLPK.ON - end - optimizer.interior.msg_lev = GLPK.MSG_ERR - optimizer.intopt.msg_lev = GLPK.MSG_ERR - optimizer.simplex.msg_lev = GLPK.MSG_ERR - for (key, value) in kwargs - set_interior = set_parameter(optimizer.interior, key, value) - set_intopt = set_parameter(optimizer.intopt, key, value) - set_simplex = set_parameter(optimizer.simplex, key, value) - if !set_interior && !set_intopt && !set_simplex - @warn("Ignoring option: $(key) => $(value)") +mutable struct Optimizer <: MOI.ModelLike + # The low-level GLPK problem. + inner::GLPK.Prob + presolve::Bool + method::MethodEnum + params::Dict{Symbol, Any} + + interior_param::GLPK.InteriorParam + intopt_param::GLPK.IntoptParam + simplex_param::GLPK.SimplexParam + solver_status::Int32 + last_solved_by_mip::Bool + num_binaries::Int + num_integers::Int + + callback_data + objective_bound::Float64 + relative_gap::Float64 + solve_time::Float64 + callback_function::Function + + # A flag to keep track of MOI.Silent, which over-rides the print_level + # parameter. + silent::Bool + + # An enum to remember what objective is currently stored in the model. + objective_type::ObjectiveEnum + + # A flag to keep track of MOI.FEASIBILITY_SENSE, since Gurobi only stores + # MIN_SENSE or MAX_SENSE. This allows us to differentiate between MIN_SENSE + # and FEASIBILITY_SENSE. + is_feasibility::Bool + + variable_info::CleverDicts.CleverDict{MOI.VariableIndex, VariableInfo} + affine_constraint_info::CleverDicts.CleverDict{ConstraintKey, ConstraintInfo} + + # 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, MOI.VariableIndex}} + name_to_constraint_index::Union{Nothing, Dict{String, MOI.ConstraintIndex}} + + optimize_not_called::Bool + + # These two flags allow us to distinguish between FEASIBLE_POINT and + # INFEASIBILITY_CERTIFICATE when querying VariablePrimal and ConstraintDual. + unbounded_ray::Union{Vector{Float64}, Nothing} + infeasibility_cert::Union{Vector{Float64}, Nothing} + + """ + Optimizer(;kwargs...) + + Create a new Optimizer object. Common keywords include + + - `method::MethodEnum = SIMPLEX` Use the simplex method. Other options are `EXACT` and `INTERIOR`. + - `tm_lim::Float64` Set a time limit + - `msg_lev::Int` Control the log level + + See the GLPK pdf documentation for a full list of parameters. + """ + function Optimizer(; presolve = false, method = SIMPLEX, kwargs...) + model = new() + model.presolve = presolve + model.method = method + + model.interior_param = GLPK.InteriorParam() + model.intopt_param = GLPK.IntoptParam() + model.simplex_param = GLPK.SimplexParam() + + model.params = Dict{Symbol, Any}() + for (key, val) in kwargs + model.params[key] = val + set_parameter(model, key, val) end - end - return optimizer -end + set_parameter(model, :msg_lev, GLPK.MSG_ERR) + if model.presolve + set_parameter(model, :presolve, GLPK.ON) + end + model.silent = false + model.variable_info = CleverDicts.CleverDict{MOI.VariableIndex, VariableInfo}() + model.affine_constraint_info = CleverDicts.CleverDict{ConstraintKey, ConstraintInfo}() -MOI.get(::Optimizer, ::MOI.SolverName) = "GLPK" + # We initialize a default callback (_internal_callback) to manage the + # user's callback, and to update the objective bound and MIP gap. + model.callback_data = CallbackData(model) + model.intopt_param.cb_func = @cfunction(_internal_callback, Cvoid, (Ptr{Cvoid}, Ptr{Cvoid})) + model.intopt_param.cb_info = pointer_from_objref(model.callback_data) + model.callback_function = (cb_data) -> nothing -function LQOI.get_objective_bound(model::Optimizer) - if !model.last_solved_by_mip - return LQOI.get_objectivesense(model) == MOI.MIN_SENSE ? -Inf : Inf - end - constant = LQOI.get_constant_objective(model) - # @mlubin and @ccoey observed some cases where mip_status == OPT and objval - # and objbound didn't match. In that case, they return mip_obj_val, but - # objbound may still be incorrect in cases where GLPK terminates early. - if GLPK.mip_status(model.inner) == GLPK.OPT - return GLPK.mip_obj_val(model.inner) + constant - else - return model.objective_bound + constant + MOI.empty!(model) + + return model end end +mutable struct CallbackData + model::Optimizer + tree::Ptr{Cvoid} + CallbackData(model::Optimizer) = new(model, C_NULL) +end + """ - set_parameter(param_store, key::Symbol, value) + set_parameter(param_store, key::Symbol, value)::Bool Set the field name `key` in a `param_store` type (that is one of `InteriorParam`, `IntoptParam`, or `SimplexParam`) to `value`. + +Returns a `Bool` indicating if the parameter was set. """ function set_parameter(param_store, key::Symbol, value) - if key in [:cb_func, :cb_info] - @warn("Ignored option: $(string(k)). Use the MOI attribute " * - "`GLPK.CallbackFunction` instead.") - return true - end - - if key in fieldnames(typeof(param_store)) + if key == :cb_func || key == :cb_info + error( + "Invalid option: $(string(key)). Use the MOI attribute " * + "`GLPK.CallbackFunction` instead." + ) + elseif key in fieldnames(typeof(param_store)) field_type = typeof(getfield(param_store, key)) setfield!(param_store, key, convert(field_type, value)) return true - else - return false end + return false end -LQOI.LinearQuadraticModel(::Type{Optimizer}, env) = GLPK.Prob() +function set_parameter(model::Optimizer, key::Symbol, value) + set_interior = set_parameter(model.interior_param, key, value) + set_intopt = set_parameter(model.intopt_param, key, value) + set_simplex = set_parameter(model.simplex_param, key, value) + if !set_interior && !set_intopt && !set_simplex + error("Invalid option: $(key) => $(value)") + end + return +end -LQOI.supported_objectives(model::Optimizer) = SUPPORTED_OBJECTIVES -LQOI.supported_constraints(model::Optimizer) = SUPPORTED_CONSTRAINTS +Base.show(io::IO, model::Optimizer) = print(io, "A GLPK model") -function LQOI.set_constant_objective!(model::Optimizer, value) - GLPK.set_obj_coef(model.inner, 0, value) +function MOI.empty!(model::Optimizer) + model.inner = GLPK.Prob() + model.solver_status = GLPK.UNDEF + model.last_solved_by_mip = false + model.num_binaries = 0 + model.num_integers = 0 + model.objective_bound = NaN + model.relative_gap = NaN + model.solve_time = NaN + model.objective_type = SCALAR_AFFINE + model.is_feasibility = true + model.optimize_not_called = true + empty!(model.variable_info) + empty!(model.affine_constraint_info) + model.name_to_variable = nothing + model.name_to_constraint_index = nothing + model.unbounded_ray = nothing + model.infeasibility_cert = nothing + return end -LQOI.get_constant_objective(model::Optimizer) = GLPK.get_obj_coef(model.inner, 0) - -""" - get_col_bound_type(lower::Float64, upper::Float64) -Return the GLPK type of the variable bound given a lower bound of `lower` and an -upper bound of `upper`. -""" -function get_col_bound_type(lower::Float64, upper::Float64) - GLPK_INFINITY = VERSION >= v"0.7-" ? floatmax(Float64) : realmax(Float64) - if lower == upper - return GLPK.FX - elseif lower <= -GLPK_INFINITY - return upper >= GLPK_INFINITY ? GLPK.FR : GLPK.UP - else - return upper >= GLPK_INFINITY ? GLPK.LO : GLPK.DB - end +function MOI.is_empty(model::Optimizer) + model.objective_type != SCALAR_AFFINE && return false + model.is_feasibility == false && return false + !isempty(model.variable_info) && return false + !isempty(model.affine_constraint_info) && return false + model.name_to_variable !== nothing && return false + model.name_to_constraint_index !== nothing && return false + model.unbounded_ray !== nothing && return false + model.infeasibility_cert !== nothing && return false + return true end -""" - set_variable_bound(model::Optimizer, column::Int, lower::Float64, upper::Float64) +MOI.get(::Optimizer, ::MOI.SolverName) = "GLPK" -Set the bounds of the variable in column `column` to `[lower, upper]`. -""" -function set_variable_bound(model::Optimizer, column::Int, lower::Float64, - upper::Float64) - bound_type = get_col_bound_type(lower, upper) - # Disable preemptive checking of variable bounds for the case when lower - # > upper. If you solve a model with lower > upper, the - # TerminationStatus will be InvalidModel. - prev_preemptive_check = GLPK.jl_get_preemptive_check() - GLPK.jl_set_preemptive_check(false) - GLPK.set_col_bnds(model.inner, column, bound_type, lower, upper) - # Reset the preemptive check. - GLPK.jl_set_preemptive_check(prev_preemptive_check) -end - -function LQOI.change_variable_bounds!(model::Optimizer, columns::Vector{Int}, - new_bounds::Vector{Float64}, senses::Vector{Cchar}) - for (column, bound, sense) in zip(columns, new_bounds, senses) - if sense == Cchar('L') - lower_bound = bound - upper_bound = GLPK.get_col_ub(model.inner, column) - elseif sense == Cchar('U') - lower_bound = GLPK.get_col_lb(model.inner, column) - upper_bound = bound - else - error("Invalid variable bound sense: $(sense)") - end - set_variable_bound(model, column, lower_bound, upper_bound) - end +function MOI.supports( + ::Optimizer, + ::MOI.ObjectiveFunction{F} +) where {F <: Union{ + MOI.SingleVariable, + MOI.ScalarAffineFunction{Float64} +}} + return true end -function LQOI.get_variable_lowerbound(model::Optimizer, col) - return GLPK.get_col_lb(model.inner, col) +function MOI.supports_constraint( + ::Optimizer, ::Type{MOI.SingleVariable}, ::Type{F} +) where {F <: Union{ + MOI.EqualTo{Float64}, MOI.LessThan{Float64}, MOI.GreaterThan{Float64}, + MOI.Interval{Float64}, MOI.ZeroOne, MOI.Integer +}} + return true end -function LQOI.get_variable_upperbound(model::Optimizer, col) - return GLPK.get_col_ub(model.inner, col) +function MOI.supports_constraint( + ::Optimizer, ::Type{MOI.ScalarAffineFunction{Float64}}, ::Type{F} +) where {F <: Union{ + MOI.EqualTo{Float64}, MOI.LessThan{Float64}, MOI.GreaterThan{Float64} +}} + return true end -function LQOI.get_number_linear_constraints(model::Optimizer) - return GLPK.get_num_rows(model.inner) +const SCALAR_SETS = Union{ + MOI.GreaterThan{Float64}, MOI.LessThan{Float64}, + MOI.EqualTo{Float64}, MOI.Interval{Float64} +} + +MOI.supports(::Optimizer, ::MOI.VariableName, ::Type{MOI.VariableIndex}) = true +MOI.supports(::Optimizer, ::MOI.ConstraintName, ::Type{<:MOI.ConstraintIndex}) = true +MOI.supports(::Optimizer, ::MOI.ObjectiveFunctionType) = true + +MOI.supports(::Optimizer, ::MOI.Name) = true +MOI.supports(::Optimizer, ::MOI.Silent) = true +MOI.supports(::Optimizer, ::MOI.TimeLimitSec) = true +MOI.supports(::Optimizer, ::MOI.ConstraintSet, c) = true +MOI.supports(::Optimizer, ::MOI.ConstraintFunction, c) = true +MOI.supports(::Optimizer, ::MOI.ConstraintPrimal, c) = true +MOI.supports(::Optimizer, ::MOI.ConstraintDual, c) = true +MOI.supports(::Optimizer, ::MOI.ConstraintPrimalStart) = false +MOI.supports(::Optimizer, ::MOI.ConstraintDualStart) = false +MOI.supports(::Optimizer, ::MOI.ObjectiveSense) = true +MOI.supports(::Optimizer, ::MOI.ListOfConstraintIndices) = true +MOI.supports(::Optimizer, ::MOI.RawStatusString) = true +MOI.supports(::Optimizer, ::MOI.RawParameter) = true + +function MOI.set(model::Optimizer, param::MOI.RawParameter, value) + if typeof(param.name) != String + error("GLPK.jl requires strings as arguments to `RawParameter`.") + end + model.params[Symbol(param.name)] = value + set_parameter(model, Symbol(param.name), value) + return end -function LQOI.add_linear_constraints!(model::Optimizer, - A::LQOI.CSRMatrix{Float64}, senses::Vector{Cchar}, rhs::Vector{Float64}) - nrows = length(rhs) - if nrows <= 0 - error("Number of rows must be more than zero.") - elseif nrows == 1 - add_row!(model.inner, A.columns, A.coefficients, senses[1], rhs[1]) - else - push!(A.row_pointers, length(A.columns)+1) - for i in 1:nrows - indices = A.row_pointers[i]:A.row_pointers[i+1]-1 - add_row!(model.inner, A.columns[indices], A.coefficients[indices], - senses[i], rhs[i]) - end - pop!(A.row_pointers) - end -end - -function LQOI.add_ranged_constraints!(model::Optimizer, - A::LQOI.CSRMatrix{Float64}, lowerbound::Vector{Float64}, - upperbound::Vector{Float64}) - row1 = GLPK.get_num_rows(model.inner) - LQOI.add_linear_constraints!(model, A, - fill(Cchar('R'), length(lowerbound)), - lowerbound) - row2 = GLPK.get_num_rows(model.inner) - for (row, lower, upper) in zip(row1+1:row2, lowerbound, upperbound) - # Disable preemptive checking of variable bounds for the case when lower - # > upper. If you solve a model with lower > upper, the - # TerminationStatus will be InvalidModel. - prev_preemptive_check = GLPK.jl_get_preemptive_check() - GLPK.jl_set_preemptive_check(false) - GLPK.set_row_bnds(model.inner, row, GLPK.DB, lower, upper) - # Reset the preemptive check. - GLPK.jl_set_preemptive_check(prev_preemptive_check) +function MOI.get(model::Optimizer, param::MOI.RawParameter) + if typeof(param.name) != String + error("GLPK.jl requires strings as arguments to `RawParameter`.") + end + name = Symbol(param.name) + if (model.method == SIMPLEX || model.method == EXACT) && name in fieldnames(GLPK.SimplexParam) + return getfield(model.simplex_param, name) + elseif model.method == INTERIOR && name in fieldnames(GLPK.InteriorParam) + return getfield(model.interior_param, name) end + if name in fieldnames(GLPK.IntoptParam) + return getfield(model.intopt_param, name) + end + error("Unable to get RawParameter. Invalid name: $(name)") end -function LQOI.modify_ranged_constraints!(model::Optimizer, - rows::Vector{Int}, lowerbounds::Vector{Float64}, - upperbounds::Vector{Float64}) - for (row, lower, upper) in zip(rows, lowerbounds, upperbounds) - LQOI.change_rhs_coefficient!(model, row, lower) - # Disable preemptive checking of variable bounds for the case when lower - # > upper. If you solve a model with lower > upper, the - # TerminationStatus will be InvalidModel. - prev_preemptive_check = GLPK.jl_get_preemptive_check() - GLPK.jl_set_preemptive_check(false) - GLPK.set_row_bnds(model.inner, row, GLPK.DB, lower, upper) - # Reset the preemptive check. - GLPK.jl_set_preemptive_check(prev_preemptive_check) - end +function MOI.set(model::Optimizer, ::MOI.TimeLimitSec, limit::Real) + MOI.set(model, MOI.RawParameter("tm_lim"), limit) + return end -function LQOI.get_range(model::Optimizer, row::Int) - return GLPK.get_row_lb(model.inner, row), GLPK.get_row_ub(model.inner, row) +function MOI.get(model::Optimizer, ::MOI.TimeLimitSec) + return MOI.get(model, MOI.RawParameter("tm_lim")) end -""" - add_row!(problem::GLPK.Prob, columns::Vector{Int}, - coefficients::Vector{Float64}, sense::Cchar, rhs::Real) +MOI.Utilities.supports_default_copy_to(::Optimizer, ::Bool) = true -Helper function to add a row to the problem. Sense must be one of `'E'` (ax == b), -`'G'` (ax >= b), `'L'` (ax <= b) , or `'R'` (b <= ax). +function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike; kwargs...) + return MOI.Utilities.automatic_copy_to(dest, src; kwargs...) +end -If the sense is `'R'` the `rhs` should be the lower bound, and the bounds should -be set in a new API call to enforce the upper bound. -""" -function add_row!(problem::GLPK.Prob, columns::Vector{Int}, - coefficients::Vector{Float64}, sense::Cchar, rhs::Real) - if length(columns) != length(coefficients) - error("columns and coefficients have different lengths.") - end - GLPK.add_rows(problem, 1) - num_rows = GLPK.get_num_rows(problem) - GLPK.set_mat_row(problem, num_rows, columns, coefficients) - # According to http://most.ccib.rutgers.edu/glpk.pdf page 22, - # the `lb` argument is ignored for constraint types with no - # lower bound (GLPK.UP) and the `ub` argument is ignored for - # constraint types with no upper bound (GLPK.LO). We pass - # ±DBL_MAX for those unused bounds since (a) we have to pass - # something, and (b) it is consistent with the other usages of - # ±DBL_MAX to represent infinite bounds in the rest of the - # GLPK interface. - if sense == Cchar('E') - GLPK.set_row_bnds(problem, num_rows, GLPK.FX, rhs, rhs) - elseif sense == Cchar('G') - GLPK.set_row_bnds(problem, num_rows, GLPK.LO, rhs, GLPK.DBL_MAX) - elseif sense == Cchar('L') - GLPK.set_row_bnds(problem, num_rows, GLPK.UP, -GLPK.DBL_MAX, rhs) - elseif sense == Cchar('R') - GLPK.set_row_bnds(problem, num_rows, GLPK.DB, rhs, GLPK.DBL_MAX) - else - error("Invalid row sense: $(sense)") +function MOI.get(model::Optimizer, ::MOI.ListOfVariableAttributesSet) + return MOI.AbstractVariableAttribute[MOI.VariableName()] +end + +function MOI.get(model::Optimizer, ::MOI.ListOfModelAttributesSet) + obj_func_type = MOI.get(model, MOI.ObjectiveFunctionType()) + attributes = [ + MOI.ObjectiveSense(), + MOI.ObjectiveFunction{obj_func_type}() + ] + if MOI.get(model, MOI.Name()) != "" + push!(attributes, MOI.Name()) end + return attributes end -function LQOI.get_rhs(model::Optimizer, row) - sense = GLPK.get_row_type(model.inner, row) - if sense == GLPK.LO || sense == GLPK.FX || sense == GLPK.DB - return GLPK.get_row_lb(model.inner, row) - else - return GLPK.get_row_ub(model.inner, row) +function MOI.get(model::Optimizer, ::MOI.ListOfConstraintAttributesSet) + return MOI.AbstractConstraintAttribute[MOI.ConstraintName()] +end + +function _indices_and_coefficients( + indices::AbstractVector{Int}, coefficients::AbstractVector{Float64}, + model::Optimizer, f::MOI.ScalarAffineFunction{Float64} +) + i = 1 + for term in f.terms + indices[i] = _info(model, term.variable_index).column + coefficients[i] = term.coefficient + i += 1 end + return indices, coefficients end -function LQOI.get_linear_constraint(model::Optimizer, row::Int) - # note: we return 1-indexed columns here - return GLPK.get_mat_row(model.inner, row) +function _indices_and_coefficients( + model::Optimizer, f::MOI.ScalarAffineFunction{Float64} +) + f_canon = MOI.Utilities.canonical(f) + nnz = length(f_canon.terms) + indices = Vector{Int}(undef, nnz) + coefficients = Vector{Float64}(undef, nnz) + _indices_and_coefficients(indices, coefficients, model, f_canon) + return indices, coefficients end -function LQOI.change_rhs_coefficient!(model::Optimizer, row::Int, - rhs::Real) - current_lower = GLPK.get_row_lb(model.inner, row) - current_upper = GLPK.get_row_ub(model.inner, row) - # `get_row_lb` and `get_row_ub` return ±DBL_MAX for rows with no - # lower or upper bound. See page 30 of the GLPK user manual - # http://most.ccib.rutgers.edu/glpk.pdf - if current_lower == current_upper - GLPK.set_row_bnds(model.inner, row, GLPK.FX, rhs, rhs) - elseif current_lower > -GLPK.DBL_MAX && current_upper < GLPK.DBL_MAX - GLPK.set_row_bnds(model.inner, row, GLPK.FX, rhs, rhs) - elseif current_lower > -GLPK.DBL_MAX - GLPK.set_row_bnds(model.inner, row, GLPK.LO, rhs, GLPK.DBL_MAX) - elseif current_upper < GLPK.DBL_MAX - GLPK.set_row_bnds(model.inner, row, GLPK.UP, -GLPK.DBL_MAX, rhs) - else - error("Cannot set right-hand side of a free constraint.") +_sense_and_rhs(s::MOI.LessThan{Float64}) = (Cchar('L'), s.upper) +_sense_and_rhs(s::MOI.GreaterThan{Float64}) = (Cchar('G'), s.lower) +_sense_and_rhs(s::MOI.EqualTo{Float64}) = (Cchar('E'), s.value) + +### +### Variables +### + +# Short-cuts to return the VariableInfo associated with an index. +function _info(model::Optimizer, key::MOI.VariableIndex) + if haskey(model.variable_info, key) + return model.variable_info[key] end + throw(MOI.InvalidIndex(key)) end -function LQOI.change_objective_coefficient!(model::Optimizer, col, coef) - GLPK.set_obj_coef(model.inner, col, coef) +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), 0) + ) + info = _info(model, index) + # Now, set `.index` and `.column`. + info.index = index + info.column = length(model.variable_info) + GLPK.add_cols(model.inner, 1) + GLPK.set_col_bnds(model.inner, info.column, GLPK.FR, 0.0, 0.0) + return index end -function LQOI.change_matrix_coefficient!(model::Optimizer, row, col, coef) - columns, coefficients = GLPK.get_mat_row(model.inner, row) - index = something(findfirst(isequal(col), columns), 0) - if index > 0 - coefficients[index] = coef - else - push!(columns, col) - push!(coefficients, coef) +function MOI.add_variables(model::Optimizer, N::Int) + indices = Vector{MOI.VariableIndex}(undef, N) + num_variables = length(model.variable_info) + GLPK.add_cols(model.inner, N) + for i in 1:N + # 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), 0) + ) + info = _info(model, index) + # Now, set `.index` and `.column`. + info.index = index + info.column = num_variables + i + GLPK.set_col_bnds(model.inner, info.column, GLPK.FR, 0.0, 0.0) + indices[i] = index end - GLPK.set_mat_row(model.inner, row, columns, coefficients) + return indices +end + +function MOI.is_valid(model::Optimizer, v::MOI.VariableIndex) + return haskey(model.variable_info, v) end -function LQOI.delete_linear_constraints!(model::Optimizer, start_row, stop_row) +function MOI.delete(model::Optimizer, v::MOI.VariableIndex) + info = _info(model, v) GLPK.std_basis(model.inner) - indices = collect(start_row:stop_row) - GLPK.del_rows(model.inner, length(indices), indices) -end - -function LQOI.change_variable_types!(model::Optimizer, - columns::Vector{Int}, variable_types::Vector) - model.binaries = Int[] - for (column, variable_type) in zip(columns, variable_types) - if variable_type == Cint('I') - GLPK.set_col_kind(model.inner, column, GLPK.IV) - elseif variable_type == Cint('C') - GLPK.set_col_kind(model.inner, column, GLPK.CV) - elseif variable_type == Cint('B') - # note: we lie to GLPK here and set it as an integer variable. See - # the comment in the definition of Optimizer. - GLPK.set_col_kind(model.inner, column, GLPK.IV) - push!(model.binaries, column) - else - error("Invalid variable type: $(vtype).") + GLPK.del_cols(model.inner, 1, [info.column]) + if !isempty(info.lessthan_name) || + !isempty(info.greaterthan_interval_or_equalto_name) || + !isempty(info.type_constraint_name) + # TODO update it with `_update_name_to_index` + model.name_to_constraint_index = nothing + end + _update_name_to_index(model.name_to_variable, info.name, "", v) + delete!(model.variable_info, v) + for other_info in values(model.variable_info) + if other_info.column > info.column + other_info.column -= 1 end end + return end -function LQOI.change_linear_constraint_sense!(model::Optimizer, rows, senses) - for (row, sense) in zip(rows, senses) - change_row_sense!(model, row, sense) +function MOI.get(model::Optimizer, ::Type{MOI.VariableIndex}, name::String) + if model.name_to_variable === nothing + _rebuild_name_to_variable(model) end + return get(model.name_to_variable, name, nothing) end -""" - change_row_sense!(model::Optimizer, row, sense) +function _rebuild_name_to_variable(model::Optimizer) + model.name_to_variable = Dict{String, 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 = nothing + error("Duplicate variable name detected: $(info.name)") + end + model.name_to_variable[info.name] = index + end + return +end -Convert a linear constraint into another type of linear constraint by changing -the comparison sense. +function MOI.get(model::Optimizer, ::MOI.VariableName, v::MOI.VariableIndex) + return _info(model, v).name +end -Constraint types supported are 'E' (equality), 'L' (less-than), and 'G' -(greater-than). +_update_name_to_index(dict::Nothing, ::String, ::String, ::MOI.Index) = nothing -For example, `ax <= b` can become `ax >= b` or `ax == b`. -""" -function change_row_sense!(model::Optimizer, row::Int, sense) - old_sense = GLPK.get_row_type(model.inner, row) - if old_sense == GLPK.DB - error("Cannot transform sense of an interval constraint.") - elseif old_sense == GLPK.FR - error("Cannot transform sense of a free constraint.") - end - new_sense = ROW_SENSE_MAP[sense] - if old_sense == new_sense - error("Cannot transform constraint with same sense.") - elseif new_sense == GLPK.DB - error("Cannot transform constraint to ranged constraint.") - end - if old_sense == GLPK.FX || old_sense == GLPK.LO - right_hand_side = GLPK.get_row_lb(model.inner, row) - else - right_hand_side = GLPK.get_row_ub(model.inner, row) - end - # According to http://most.ccib.rutgers.edu/glpk.pdf page 22, - # the `lb` argument is ignored for constraint types with no - # lower bound (GLPK.UP) and the `ub` argument is ignored for - # constraint types with no upper bound (GLPK.LO). We pass - # ±DBL_MAX for those unused bounds since (a) we have to pass - # something, and (b) it is consistent with the other usages of - # ±DBL_MAX to represent infinite bounds in the rest of the - # GLPK interface. - if new_sense == GLPK.FX - GLPK.set_row_bnds(model.inner, row, new_sense, right_hand_side, right_hand_side) - elseif new_sense == GLPK.LO - GLPK.set_row_bnds(model.inner, row, new_sense, right_hand_side, GLPK.DBL_MAX) - elseif new_sense == GLPK.UP - GLPK.set_row_bnds(model.inner, row, new_sense, -GLPK.DBL_MAX, right_hand_side) - end -end - -const ROW_SENSE_MAP = Dict( - Cchar('E') => GLPK.FX, - Cchar('R') => GLPK.DB, - Cchar('L') => GLPK.UP, - Cchar('G') => GLPK.LO -) - -function LQOI.add_sos_constraint!(model::Optimizer, columns, weights, sos_type) - GLPK.add_sos!(instance.inner, sos_type, columns, weights) -end - -function LQOI.set_linear_objective!(model::Optimizer, columns, coefficients) - ncols = GLPK.get_num_cols(model.inner) - new_coefficients = zeros(ncols) - for (col, coef) in zip(columns, coefficients) - new_coefficients[col] += coef - end - for (col, coef) in zip(1:ncols, new_coefficients) - GLPK.set_obj_coef(model.inner, col, coef) +function _update_name_to_index( + dict::Dict, old_name::String, new_name::String, index::MOI.Index +) + delete!(dict, old_name) + if !isempty(new_name) + if haskey(dict, new_name) + # It will throw an error in `MOI.get` except if the index with `name` + # is deleted or renamed before `MOI.get` is called. + return nothing + end + dict[new_name] = index end + return dict end -function LQOI.change_objective_sense!(model::Optimizer, sense) - if sense == :min - GLPK.set_obj_dir(model.inner, GLPK.MIN) - elseif sense == :max - GLPK.set_obj_dir(model.inner, GLPK.MAX) - else - error("Invalid objective sense: $(sense)") +function MOI.set( + model::Optimizer, ::MOI.VariableName, v::MOI.VariableIndex, name::String +) + info = _info(model, v) + old_name = info.name + info.name = name + if name != "" + GLPK.set_col_name(model.inner, info.column, name) end + model.name_to_variable = _update_name_to_index( + model.name_to_variable, old_name, name, v + ) + return end -function LQOI.get_linear_objective!(model::Optimizer, x::Vector{Float64}) - @assert length(x) == GLPK.get_num_cols(model.inner) - for col in 1:length(x) - x[col] = GLPK.get_obj_coef(model.inner, col) +### +### Objectives +### + +function MOI.set( + model::Optimizer, ::MOI.ObjectiveSense, sense::MOI.OptimizationSense +) + if sense == MOI.MIN_SENSE + GLPK.set_obj_dir(model.inner, GLPK.MIN) + model.is_feasibility = false + elseif sense == MOI.MAX_SENSE + GLPK.set_obj_dir(model.inner, GLPK.MAX) + model.is_feasibility = false + else + @assert sense == MOI.FEASIBILITY_SENSE + GLPK.set_obj_dir(model.inner, GLPK.MIN) + model.is_feasibility = true end + return end -function LQOI.get_objectivesense(model::Optimizer) +function MOI.get(model::Optimizer, ::MOI.ObjectiveSense) sense = GLPK.get_obj_dir(model.inner) - if sense == GLPK.MIN - return MOI.MIN_SENSE + if model.is_feasibility + return MOI.FEASIBILITY_SENSE elseif sense == GLPK.MAX return MOI.MAX_SENSE else - error("Invalid objective sense: $(sense)") + @assert sense == GLPK.MIN + return MOI.MIN_SENSE end end -function LQOI.get_number_variables(model::Optimizer) - GLPK.get_num_cols(model.inner) +function MOI.set( + model::Optimizer, ::MOI.ObjectiveFunction{F}, f::F +) where {F <: MOI.SingleVariable} + MOI.set( + model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + convert(MOI.ScalarAffineFunction{Float64}, f) + ) + model.objective_type = SINGLE_VARIABLE + return +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 LQOI.add_variables!(model::Optimizer, number_to_add::Int) - num_variables = GLPK.get_num_cols(model.inner) - GLPK.add_cols(model.inner, number_to_add) - for i in 1:number_to_add - GLPK.set_col_bnds(model.inner, num_variables+i, GLPK.FR, 0.0, 0.0) +function MOI.set( + model::Optimizer, ::MOI.ObjectiveFunction{F}, f::F +) where {F <: MOI.ScalarAffineFunction{Float64}} + num_vars = length(model.variable_info) + obj = zeros(Float64, num_vars) + for term in f.terms + column = _info(model, term.variable_index).column + obj[column] += term.coefficient + end + for (col, coef) in enumerate(obj) + GLPK.set_obj_coef(model.inner, col, coef) end + GLPK.set_obj_coef(model.inner, 0, f.constant) + model.objective_type = SCALAR_AFFINE end -function LQOI.delete_variables!(model::Optimizer, col, col2) - GLPK.std_basis(model.inner) - columns = collect(col:col2) - GLPK.del_cols(model.inner, length(columns), columns) +function MOI.get( + model::Optimizer, ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}} +) + dest = zeros(length(model.variable_info)) + for col in 1:length(dest) + dest[col] = GLPK.get_obj_coef(model.inner, col) + end + terms = MOI.ScalarAffineTerm{Float64}[] + for (index, info) in model.variable_info + coefficient = dest[info.column] + iszero(coefficient) && continue + push!(terms, MOI.ScalarAffineTerm(coefficient, index)) + end + constant = GLPK.get_obj_coef(model.inner, 0) + return MOI.ScalarAffineFunction(terms, constant) end -function MOI.write_to_file(model::Optimizer, lp_file_name::String) - GLPK.write_lp(model.inner, lp_file_name) +function MOI.modify( + model::Optimizer, + ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}, + chg::MOI.ScalarConstantChange{Float64} +) + GLPK.set_obj_coef(model.inner, 0, chg.new_constant) + return end -""" - _certificates_potentially_available(model::Optimizer) +## +## SingleVariable-in-Set constraints. +## -Return true if an infeasiblity certificate or an unbounded ray is potentially -available (i.e., the model has been solved using either the Simplex or Exact -methods). -""" -function _certificates_potentially_available(model::Optimizer) - !model.last_solved_by_mip && (model.method == :Simplex || model.method == :Exact) -end - -function LQOI.get_termination_status(model::Optimizer) - if model.solver_status == GLPK.EMIPGAP # MIP Gap - return MOI.OPTIMAL - elseif model.solver_status == GLPK.EBOUND # Invalid bounds - return MOI.INVALID_MODEL - elseif model.solver_status == GLPK.ETMLIM # Time limit - return MOI.TIME_LIMIT - elseif model.solver_status == GLPK.ENODFS # No feasible dual - return MOI.INFEASIBLE_OR_UNBOUNDED - elseif model.solver_status == GLPK.ENOPFS # No feasible primal - return MOI.INFEASIBLE - elseif model.solver_status == GLPK.EFAIL # Solver fail - return MOI.NUMERICAL_ERROR - elseif model.solver_status == GLPK.ESTOP # Callback - return MOI.INTERRUPTED - end - status = get_status(model) - if status == GLPK.OPT - return MOI.OPTIMAL - elseif status == GLPK.INFEAS - return MOI.INFEASIBLE - elseif status == GLPK.UNBND - return MOI.DUAL_INFEASIBLE - elseif status == GLPK.FEAS - return MOI.SLOW_PROGRESS - elseif status == GLPK.NOFEAS - return MOI.INFEASIBLE_OR_UNBOUNDED - elseif status == GLPK.UNDEF - return MOI.OTHER_ERROR - else - error("Invalid termination status: $(status)") +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 -""" - get_status(model::Optimizer) - -Get the status from GLPK depending on which method was used to solve the model. -""" -function get_status(model::Optimizer) - if model.last_solved_by_mip - return GLPK.mip_status(model.inner) - else - if model.method == :Simplex || model.method == :Exact - return GLPK.get_status(model.inner) - elseif model.method == :InteriorPoint - return GLPK.ipt_status(model.inner) - end - _throw_invalid_method(model) +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 == LESS_THAN || info.bound == LESS_AND_GREATER_THAN end + return false end -function LQOI.get_primal_status(model::Optimizer) - status = get_status(model) - if status == GLPK.OPT - return MOI.FEASIBLE_POINT - elseif status == GLPK.UNBND && _certificates_potentially_available(model) - return MOI.INFEASIBILITY_CERTIFICATE +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 == GREATER_THAN || info.bound == LESS_AND_GREATER_THAN end - return MOI.NO_SOLUTION + return false end -function LQOI.get_dual_status(model::Optimizer) - if !model.last_solved_by_mip - status = get_status(model.inner) - if status == GLPK.OPT - return MOI.FEASIBLE_POINT - elseif status == GLPK.INFEAS && _certificates_potentially_available(model) - return MOI.INFEASIBILITY_CERTIFICATE - end - end - return MOI.NO_SOLUTION +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 == INTERVAL end -""" - copy_function_result!(dest::Vector, foo, model::GLPK.Prob) +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 == EQUAL_TO +end -A helper function that loops through the indices in `dest` and stores the result -of `foo(model, i)` for the `i`th index. -""" -function copy_function_result!(dest::Vector, foo, model::GLPK.Prob) - for i in eachindex(dest) - dest[i] = foo(model, i) - end +function MOI.is_valid( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.ZeroOne} +) + return haskey(model.variable_info, MOI.VariableIndex(c.value)) && + _info(model, c).type == BINARY end -function copy_function_result!(dest::Vector, foo, model::Optimizer) - copy_function_result!(dest, foo, model.inner) + +function MOI.is_valid( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.Integer} +) + return haskey(model.variable_info, MOI.VariableIndex(c.value)) && + _info(model, c).type == INTEGER end -""" - _throw_invalid_method(instance::Optimizer) +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 -A helper function to throw an error when the method is set incorrectly. Mainly -used to enforce type-stability in functions that have a run-time switch on the -method. -""" -function _throw_invalid_method(instance::Optimizer) - error("Method is $(instance.method), but it must be one of :Simplex, " * - ":Exact, or :InteriorPoint.") +function MOI.set( + model::Optimizer, ::MOI.ConstraintFunction, + c::MOI.ConstraintIndex{MOI.SingleVariable, <:Any}, ::MOI.SingleVariable +) + return throw(MOI.SettingSingleVariableFunctionNotAllowed()) end -function LQOI.get_variable_primal_solution!(model::Optimizer, place) - if model.last_solved_by_mip - copy_function_result!(place, GLPK.mip_col_val, model) +_bounds(s::MOI.GreaterThan{Float64}) = (s.lower, nothing) +_bounds(s::MOI.LessThan{Float64}) = (nothing, s.upper) +_bounds(s::MOI.EqualTo{Float64}) = (s.value, s.value) +_bounds(s::MOI.Interval{Float64}) = (s.lower, s.upper) + +function _throw_if_existing_lower( + bound::BoundEnum, var_type::TypeEnum, new_set::Type{<:MOI.AbstractSet}, + variable::MOI.VariableIndex +) + existing_set = if bound == LESS_AND_GREATER_THAN || bound == GREATER_THAN + MOI.GreaterThan{Float64} + elseif bound == INTERVAL + MOI.Interval{Float64} + elseif bound == EQUAL_TO + MOI.EqualTo{Float64} else - if model.method == :Simplex || model.method == :Exact - copy_function_result!(place, GLPK.get_col_prim, model) - elseif model.method == :InteriorPoint - copy_function_result!(place, GLPK.ipt_col_prim, model) - else - _throw_invalid_method(model) - end + nothing # Also covers `NONE` and `LESS_THAN`. + end + if existing_set !== nothing + throw(MOI.LowerBoundAlreadySet{existing_set, new_set}(variable)) end end -function LQOI.get_linear_primal_solution!(model::Optimizer, place) - if model.last_solved_by_mip - copy_function_result!(place, GLPK.mip_row_val, model) +function _throw_if_existing_upper( + bound::BoundEnum, var_type::TypeEnum, new_set::Type{<:MOI.AbstractSet}, + variable::MOI.VariableIndex +) + existing_set = if bound == LESS_AND_GREATER_THAN || bound == LESS_THAN + MOI.LessThan{Float64} + elseif bound == INTERVAL + MOI.Interval{Float64} + elseif bound == EQUAL_TO + MOI.EqualTo{Float64} else - if model.method == :Simplex || model.method == :Exact - copy_function_result!(place, GLPK.get_row_prim, model) - elseif model.method == :InteriorPoint - copy_function_result!(place, GLPK.ipt_row_prim, model) - else - _throw_invalid_method(model) - end + nothing # Also covers `NONE` and `GREATER_THAN`. + end + if existing_set !== nothing + throw(MOI.UpperBoundAlreadySet{existing_set, new_set}(variable)) end end -function LQOI.get_variable_dual_solution!(model::Optimizer, place) - @assert !model.last_solved_by_mip - if model.method == :Simplex || model.method == :Exact - copy_function_result!(place, GLPK.get_col_dual, model) - elseif model.method == :InteriorPoint - copy_function_result!(place, GLPK.ipt_col_dual, model) +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, info.type, S, f.variable) + info.bound = info.bound == GREATER_THAN ? LESS_AND_GREATER_THAN : LESS_THAN + elseif S <: MOI.GreaterThan{Float64} + _throw_if_existing_lower(info.bound, info.type, S, f.variable) + info.bound = info.bound == LESS_THAN ? LESS_AND_GREATER_THAN : GREATER_THAN + elseif S <: MOI.EqualTo{Float64} + _throw_if_existing_lower(info.bound, info.type, S, f.variable) + _throw_if_existing_upper(info.bound, info.type, S, f.variable) + info.bound = EQUAL_TO else - _throw_invalid_method(model) + @assert S <: MOI.Interval{Float64} + _throw_if_existing_lower(info.bound, info.type, S, f.variable) + _throw_if_existing_upper(info.bound, info.type, S, f.variable) + info.bound = INTERVAL end + index = MOI.ConstraintIndex{MOI.SingleVariable, typeof(s)}(f.variable.value) + MOI.set(model, MOI.ConstraintSet(), index, s) + return index end -function LQOI.get_linear_dual_solution!(model::Optimizer, place) - @assert !model.last_solved_by_mip - if model.method == :Simplex || model.method == :Exact - copy_function_result!(place, GLPK.get_row_dual, model) - elseif model.method == :InteriorPoint - copy_function_result!(place, GLPK.ipt_row_dual, model) +function _set_variable_bound( + model::Optimizer, column::Int, lower::Union{Nothing, Float64}, + upper::Union{Nothing, Float64} +) + if lower === nothing + lower = GLPK.get_col_lb(model.inner, column) + end + if upper === nothing + upper = GLPK.get_col_ub(model.inner, column) + end + bound_type = if lower == upper + GLPK.FX + elseif lower <= -GLPK.DBL_MAX + upper >= GLPK.DBL_MAX ? GLPK.FR : GLPK.UP else - _throw_invalid_method(model) + upper >= GLPK.DBL_MAX ? GLPK.LO : GLPK.DB end -end - -function LQOI.get_objective_value(model::Optimizer) - if model.last_solved_by_mip - return GLPK.mip_obj_val(model.inner) + if upper < lower + # Here, we disable GLPK's pre-emptive checks, because otherwise GLPK + # will through an error complaining about invalid bounds when `upper < + # lower`. This let's us throw `INFEASIBLE` instead of erroring. + prev_preemptive = GLPK.jl_get_preemptive_check() + GLPK.jl_set_preemptive_check(false) + GLPK.set_col_bnds(model.inner, column, bound_type, lower, upper) + GLPK.jl_set_preemptive_check(prev_preemptive) else - if model.method == :Simplex || model.method == :Exact - return GLPK.get_obj_val(model.inner) - elseif model.method == :InteriorPoint - return GLPK.ipt_obj_val(model.inner) - end - _throw_invalid_method(model) + GLPK.set_col_bnds(model.inner, column, bound_type, lower, upper) end + return end -function LQOI.solve_linear_problem!(model::Optimizer) - model.last_solved_by_mip = false - if model.method == :Simplex - model.solver_status = GLPK.simplex(model.inner, model.simplex) - elseif model.method == :Exact - model.solver_status = GLPK.exact(model.inner, model.simplex) - elseif model.method == :InteriorPoint - model.solver_status = GLPK.interior(model.inner, model.interior) +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.LessThan{Float64}} +) + MOI.throw_if_not_valid(model, c) + info = _info(model, c) + _set_variable_bound(model, info.column, nothing, Inf) + if info.bound == LESS_AND_GREATER_THAN + info.bound = GREATER_THAN else - _throw_invalid_method(model) + info.bound = NONE end + _update_name_to_index( + model.name_to_constraint_index, info.lessthan_name, "", c + ) + info.lessthan_name = "" return end -""" - round_bounds_to_integer(model)::Tuple{Bool, Vector{Float64}, Vector{Float64}} +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.GreaterThan{Float64}} +) + MOI.throw_if_not_valid(model, c) + info = _info(model, c) + _set_variable_bound(model, info.column, -Inf, nothing) + info.bound = info.bound == LESS_AND_GREATER_THAN ? LESS_THAN : NONE + _update_name_to_index( + model.name_to_constraint_index, + info.greaterthan_interval_or_equalto_name, "", c + ) + info.greaterthan_interval_or_equalto_name = "" + return +end -GLPK does not allow integer variables with fractional bounds. Therefore, we -round the bounds of binary and integer variables to integer values prior to -solving. +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.Interval{Float64}} +) + MOI.throw_if_not_valid(model, c) + info = _info(model, c) + _set_variable_bound(model, info.column, -Inf, Inf) + info.bound = NONE + _update_name_to_index( + model.name_to_constraint_index, + info.greaterthan_interval_or_equalto_name, "", c + ) + info.greaterthan_interval_or_equalto_name = "" + return +end -Returns a tuple of the original bounds, along with a Boolean flag indicating if -they need to be reset after solve. -""" -function round_bounds_to_integer(model::Optimizer) - num_variables = GLPK.get_num_cols(model.inner) - lower_bounds = map(i->GLPK.get_col_lb(model.inner, i), 1:num_variables) - upper_bounds = map(i->GLPK.get_col_ub(model.inner, i), 1:num_variables) - variable_types = get_variable_types(model) - bounds_modified = false - for (col, variable_type) in enumerate(variable_types) - new_lower = ceil(lower_bounds[col]) - new_upper = floor(upper_bounds[col]) - if variable_type == :Bin - new_lower = max(0.0, ceil(lower_bounds[col])) - new_upper = min(1.0, floor(upper_bounds[col])) - elseif variable_type != :Int - continue - end - if lower_bounds[col] != new_lower || upper_bounds[col] != new_upper - set_variable_bound(model, col, new_lower, new_upper) - bounds_modified = true - end - end - return bounds_modified, lower_bounds, upper_bounds +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.EqualTo{Float64}} +) + MOI.throw_if_not_valid(model, c) + info = _info(model, c) + _set_variable_bound(model, info.column, -Inf, Inf) + info.bound = NONE + _update_name_to_index( + model.name_to_constraint_index, + info.greaterthan_interval_or_equalto_name, "", c + ) + info.greaterthan_interval_or_equalto_name = "" + return end -function LQOI.solve_mip_problem!(model::Optimizer) - bounds_modified, lower_bounds, upper_bounds = round_bounds_to_integer(model) - # Because we're muddling with the presolve in this function, cache the - # original setting so that it can be reset. - presolve_cache = model.intopt.presolve - try +function MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.GreaterThan{Float64}} +) + MOI.throw_if_not_valid(model, c) + lower = GLPK.get_col_lb(model.inner, _info(model, c).column) + return MOI.GreaterThan(lower) +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.LessThan{Float64}} +) + MOI.throw_if_not_valid(model, c) + upper = GLPK.get_col_ub(model.inner, _info(model, c).column) + return MOI.LessThan(upper) +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.EqualTo{Float64}} +) + MOI.throw_if_not_valid(model, c) + lower = GLPK.get_col_lb(model.inner, _info(model, c).column) + return MOI.EqualTo(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) + lower = GLPK.get_col_lb(model.inner, info.column) + upper = GLPK.get_col_ub(model.inner, info.column) + return MOI.Interval(lower, 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) + _set_variable_bound(model, info.column, lower, upper) + return +end + +function MOI.add_constraint( + model::Optimizer, f::MOI.SingleVariable, ::MOI.ZeroOne +) + info = _info(model, f.variable) + # See https://github.com/JuliaOpt/GLPKMathProgInterface.jl/pull/15 + # for why this is necesary. GLPK interacts weirdly with binary variables and + # bound modification. So let's set binary variables as "Integer" with [0,1] + # bounds that we enforce just before solve. + GLPK.set_col_kind(model.inner, info.column, GLPK.IV) + info.type = BINARY + model.num_binaries += 1 + return MOI.ConstraintIndex{MOI.SingleVariable, MOI.ZeroOne}(f.variable.value) +end + +function MOI.delete( + model::Optimizer, c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.ZeroOne} +) + MOI.throw_if_not_valid(model, c) + info = _info(model, c) + GLPK.set_col_kind(model.inner, info.column, GLPK.CV) + info.type = CONTINUOUS + model.num_binaries -= 1 + _update_name_to_index( + model.name_to_constraint_index, info.type_constraint_name, "", c + ) + info.type_constraint_name = "" + return +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.ZeroOne} +) + MOI.throw_if_not_valid(model, c) + return MOI.ZeroOne() +end + +function MOI.add_constraint( + model::Optimizer, f::MOI.SingleVariable, ::MOI.Integer +) + info = _info(model, f.variable) + GLPK.set_col_kind(model.inner, info.column, GLPK.IV) + info.type = INTEGER + model.num_integers += 1 + return MOI.ConstraintIndex{MOI.SingleVariable, MOI.Integer}(f.variable.value) +end + +function MOI.delete( + model::Optimizer, c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.Integer} +) + MOI.throw_if_not_valid(model, c) + info = _info(model, c) + GLPK.set_col_kind(model.inner, info.column, GLPK.CV) + info.type = CONTINUOUS + model.num_integers -= 1 + _update_name_to_index( + model.name_to_constraint_index, info.type_constraint_name, "", c + ) + info.type_constraint_name = "" + return +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.Integer} +) + MOI.throw_if_not_valid(model, c) + return MOI.Integer() +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintName, + c::MOI.ConstraintIndex{MOI.SingleVariable, S} +) where {S} + MOI.throw_if_not_valid(model, c) + info = _info(model, c) + if S <: MOI.LessThan + return info.lessthan_name + elseif S <: Union{MOI.GreaterThan, MOI.Interval, MOI.EqualTo} + return info.greaterthan_interval_or_equalto_name + else + @assert S <: Union{MOI.ZeroOne, MOI.Integer} + return info.type_constraint_name + end +end + +function MOI.set( + model::Optimizer, ::MOI.ConstraintName, + c::MOI.ConstraintIndex{MOI.SingleVariable, S}, name::String +) where {S} + MOI.throw_if_not_valid(model, c) + info = _info(model, c) + if S <: MOI.LessThan + old_name = info.lessthan_name + info.lessthan_name = name + elseif S <: Union{MOI.GreaterThan, MOI.Interval, MOI.EqualTo} + old_name = info.greaterthan_interval_or_equalto_name + info.greaterthan_interval_or_equalto_name = name + else + @assert S <: Union{MOI.ZeroOne, MOI.Integer} + old_name = info.type_constraint_name + info.type_constraint_name = name + end + model.name_to_constraint_index = _update_name_to_index( + model.name_to_constraint_index, old_name, name, c + ) + return +end + +### +### ScalarAffineFunction-in-Set +### + +function _info( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, <:Any} +) + key = ConstraintKey(c.value) + if haskey(model.affine_constraint_info, key) + return model.affine_constraint_info[key] + end + throw(MOI.InvalidIndex(key)) +end + +function MOI.is_valid( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, S} +) where {S} + key = ConstraintKey(c.value) + if haskey(model.affine_constraint_info, key) + info = model.affine_constraint_info[key] + return typeof(info.set) == S + else + return false + end +end + +""" + _add_affine_constraint( + problem::GLPK.Prob, columns::Vector{Int}, coefficients::Vector{Float64}, + sense::Cchar, rhs::Float64 + ) + +Helper function to add a row to the problem. Sense must be one of `'E'` +(ax == b), `'G'` (ax >= b), `'L'` (ax <= b). +""" +function _add_affine_constraint( + problem::GLPK.Prob, columns::Vector{Int}, coefficients::Vector{Float64}, + sense::Cchar, rhs::Float64 +) + if length(columns) != length(coefficients) + error("columns and coefficients have different lengths.") + end + GLPK.add_rows(problem, 1) + num_rows = GLPK.get_num_rows(problem) + GLPK.set_mat_row(problem, num_rows, columns, coefficients) + # According to http://most.ccib.rutgers.edu/glpk.pdf page 22, the `lb` + # argument is ignored for constraint types with no lower bound (GLPK.UP) and + # the `ub` argument is ignored for constraint types with no upper bound + # (GLPK.LO). We pass ±DBL_MAX for those unused bounds since (a) we have to + # pass something, and (b) it is consistent with the other usages of ±DBL_MAX + # to represent infinite bounds in the rest of the GLPK interface. + if sense == Cchar('E') + GLPK.set_row_bnds(problem, num_rows, GLPK.FX, rhs, rhs) + elseif sense == Cchar('G') + GLPK.set_row_bnds(problem, num_rows, GLPK.LO, rhs, GLPK.DBL_MAX) + else + @assert sense == Cchar('L') + GLPK.set_row_bnds(problem, num_rows, GLPK.UP, -GLPK.DBL_MAX, rhs) + end + return +end + +function MOI.add_constraint( + model::Optimizer, f::MOI.ScalarAffineFunction{Float64}, + s::Union{MOI.GreaterThan{Float64}, MOI.LessThan{Float64}, MOI.EqualTo{Float64}} +) + if !iszero(f.constant) + throw(MOI.ScalarFunctionConstantNotZero{Float64, typeof(f), typeof(s)}(f.constant)) + end + key = CleverDicts.add_item(model.affine_constraint_info, ConstraintInfo(s)) + model.affine_constraint_info[key].row = length(model.affine_constraint_info) + indices, coefficients = _indices_and_coefficients(model, f) + sense, rhs = _sense_and_rhs(s) + _add_affine_constraint(model.inner, indices, coefficients, sense, rhs) + return MOI.ConstraintIndex{typeof(f), typeof(s)}(key.value) +end + +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, <:Any} +) + row = _info(model, c).row + GLPK.std_basis(model.inner) + GLPK.del_rows(model.inner, 1, [row]) + for info in values(model.affine_constraint_info) + if info.row > row + info.row -= 1 + end + end + model.name_to_constraint_index = nothing + key = ConstraintKey(c.value) + if model.name_to_constraint_index !== nothing + info = model.affine_constraint_info[key] + _update_name_to_index(model.name_to_constraint_index, info.name, "", c) + end + delete!(model.affine_constraint_info, key) + return +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, S} +) where {S} + row = _info(model, c).row + sense = GLPK.get_row_type(model.inner, row) + if sense == GLPK.LO || sense == GLPK.FX || sense == GLPK.DB + return S(GLPK.get_row_lb(model.inner, row)) + else + return S(GLPK.get_row_ub(model.inner, row)) + end +end + +function MOI.set( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, S}, s::S +) where {S <: Union{MOI.LessThan, MOI.GreaterThan, MOI.EqualTo}} + row = _info(model, c).row + if S <: MOI.LessThan + GLPK.set_row_bnds(model.inner, row, GLPK.UP, -GLPK.DBL_MAX, s.upper) + elseif S <: MOI.GreaterThan + GLPK.set_row_bnds(model.inner, row, GLPK.LO, s.lower, GLPK.DBL_MAX) + else + @assert S <: MOI.EqualTo + GLPK.set_row_bnds(model.inner, row, GLPK.FX, s.value, s.value) + end + return +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintFunction, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, S} +) where {S} + indices, values = GLPK.get_mat_row(model.inner, _info(model, c).row) + terms = MOI.ScalarAffineTerm{Float64}[] + for (col, val) in zip(indices, values) + iszero(val) && continue + push!( + terms, + MOI.ScalarAffineTerm( + val, + model.variable_info[CleverDicts.LinearIndex(col)].index + ) + ) + end + return MOI.ScalarAffineFunction(terms, 0.0) +end + +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) + old_name = info.name + info.name = name + if name != "" + GLPK.set_row_name(model.inner, info.row, name) + end + model.name_to_constraint_index = _update_name_to_index( + model.name_to_constraint_index, old_name, name, c + ) + return +end + +function MOI.get(model::Optimizer, ::Type{MOI.ConstraintIndex}, name::String) + if model.name_to_constraint_index === nothing + _rebuild_name_to_constraint_index(model) + end + return get(model.name_to_constraint_index, name, nothing) +end + +function MOI.get( + model::Optimizer, C::Type{MOI.ConstraintIndex{F, S}}, name::String +) where {F, S} + index = MOI.get(model, MOI.ConstraintIndex, name) + if typeof(index) == C + return index::MOI.ConstraintIndex{F, S} + end + return nothing +end + +function _rebuild_name_to_constraint_index(model::Optimizer) + model.name_to_constraint_index = Dict{String, MOI.ConstraintIndex}() + _rebuild_name_to_constraint_index_util( + model, model.affine_constraint_info, MOI.ScalarAffineFunction{Float64} + ) + for (index, info) in model.variable_info + if !isempty(info.lessthan_name) + S = MOI.LessThan{Float64} + index = MOI.ConstraintIndex{MOI.SingleVariable, S}(index.value) + _throw_or_set_name_to_constraint_index( + model, info.lessthan_name, index + ) + end + if !isempty(info.greaterthan_interval_or_equalto_name) + if info.bound == GREATER_THAN || info.bound == LESS_AND_GREATER_THAN + S = MOI.GreaterThan{Float64} + elseif info.bound == EQUAL_TO + S = MOI.EqualTo{Float64} + else + @assert info.bound == INTERVAL + S = MOI.Interval{Float64} + end + index = MOI.ConstraintIndex{MOI.SingleVariable, S}(index.value) + _throw_or_set_name_to_constraint_index( + model, info.greaterthan_interval_or_equalto_name, index + ) + end + if !isempty(info.type_constraint_name) + if info.type == BINARY + S = MOI.ZeroOne + else + @assert info.type == INTEGER + S = MOI.Integer + end + index = MOI.ConstraintIndex{MOI.SingleVariable, S}(index.value) + _throw_or_set_name_to_constraint_index( + model, info.type_constraint_name, index + ) + end + end + return +end + +function _rebuild_name_to_constraint_index_util(model::Optimizer, dict, F) + for (index, info) in dict + if !isempty(info.name) + index = MOI.ConstraintIndex{F, typeof(info.set)}(index.value) + _throw_or_set_name_to_constraint_index(model, info.name, index) + end + end + return +end + +function _throw_or_set_name_to_constraint_index(model::Optimizer, name, index) + if haskey(model.name_to_constraint_index, name) + model.name_to_constraint_index = nothing + error("Duplicate constraint name detected: ", name) + end + model.name_to_constraint_index[name] = index + return +end + +### +### Optimize methods. +### + +function _solve_linear_problem(model::Optimizer) + model.last_solved_by_mip = false + if model.method == SIMPLEX + model.solver_status = GLPK.simplex(model.inner, model.simplex_param) + elseif model.method == EXACT + model.solver_status = GLPK.exact(model.inner, model.simplex_param) + else + @assert model.method == INTERIOR + model.solver_status = GLPK.interior(model.inner, model.interior_param) + end + return +end + +""" + _round_bounds_to_integer(model) + +GLPK does not allow integer variables with fractional bounds. Therefore, we +round the bounds of binary and integer variables to integer values prior to +solving. + +Returns a tuple `(column, lower, upper)` for the bounds that need to be reset. +""" +function _round_bounds_to_integer(model::Optimizer) + bounds_to_reset = Tuple{Int, Float64, Float64}[] + for (key, info) in model.variable_info + if info.type == BINARY || info.type == INTEGER + lb = GLPK.get_col_lb(model.inner, info.column) + ub = GLPK.get_col_ub(model.inner, info.column) + new_lb = ceil(lb) + new_ub = floor(ub) + if info.type == BINARY + new_lb = max(0.0, new_lb) + new_ub = min(1.0, new_ub) + end + if new_lb != lb || new_ub != ub + push!(bounds_to_reset, (info.column, lb, ub)) + _set_variable_bound(model, info.column, new_lb, new_ub) + end + end + end + return bounds_to_reset +end + +function _solve_mip_problem(model::Optimizer) + bounds_to_reset = _round_bounds_to_integer(model) + # Because we're muddling with the presolve in this function, cache the + # original setting so that it can be reset. + presolve_cache = model.intopt_param.presolve + try # GLPK.intopt requires a starting basis for the LP relaxation. There are # two ways to get this. If presolve=GLPK.ON, then the presolve will find # a basis. If presolve=GLPK.OFF, then we should solve the problem via # GLPK.simplex first. - if model.intopt.presolve == GLPK.OFF - GLPK.simplex(model.inner, model.simplex) + if model.intopt_param.presolve == GLPK.OFF + GLPK.simplex(model.inner, model.simplex_param) if GLPK.get_status(model.inner) != GLPK.OPT # We didn't find an optimal solution to the LP relaxation, so # let's turn presolve on and let intopt figure out what the # problem is. - model.intopt.presolve = GLPK.ON + model.intopt_param.presolve = GLPK.ON end end - model.solver_status = GLPK.intopt(model.inner, model.intopt) + model.solver_status = GLPK.intopt(model.inner, model.intopt_param) model.last_solved_by_mip = true finally - if bounds_modified - for (col, (lower, upper)) in enumerate(zip(lower_bounds, upper_bounds)) - set_variable_bound(model, col, lower, upper) - end + for (column, lower, upper) in bounds_to_reset + _set_variable_bound(model, column, lower, upper) end - # Restore the original presolve setting. - model.intopt.presolve = presolve_cache + model.intopt_param.presolve = presolve_cache + end + return +end + +include("infeasibility_certificates.jl") + +function MOI.optimize!(model::Optimizer) + start_time = time() + model.optimize_not_called = false + model.infeasibility_cert = nothing + model.unbounded_ray = nothing + if model.num_binaries > 0 || model.num_integers > 0 + _solve_mip_problem(model) + else + _solve_linear_problem(model) + end + if MOI.get(model, MOI.PrimalStatus()) == MOI.INFEASIBILITY_CERTIFICATE + model.unbounded_ray = fill(NaN, GLPK.get_num_cols(model.inner)) + get_unbounded_ray(model, model.unbounded_ray) + end + if MOI.get(model, MOI.DualStatus()) == MOI.INFEASIBILITY_CERTIFICATE + model.infeasibility_cert = fill(NaN, GLPK.get_num_rows(model.inner)) + get_infeasibility_ray(model, model.infeasibility_cert) end + model.solve_time = time() - start_time + return end -const VARIABLE_TYPE_MAP = Dict( - GLPK.CV => :Cont, - GLPK.IV => :Int, - GLPK.BV => :Bin +# GLPK has a complicated status reporting system because it can be solved via +# multiple different solution algorithms. Regardless of the algorithm, the +# return value is stored in `model.solver_status`. +# +# Note that the first status (`Int32(0)`) should map to a `SUCCESS` status, +# because it doesn't imply anything about the solution. If `solver_status` is +# `Int32(0)`, then a solution-specific status can be queried with `_get_status`. + +const RAW_SIMPLEX_STRINGS = Dict{Int32, Tuple{MOI.TerminationStatusCode, String}}( + GLPK.EBADB => (MOI.INVALID_MODEL, "Unable to start the search, because the initial basis specified in the problem object is invalid—the number of basic (auxiliary and structural) variables is not the same as the number of rows in the problem object."), + GLPK.ESING => (MOI.NUMERICAL_ERROR, "Unable to start the search, because the basis matrix corresponding to the initial basis is singular within the working precision."), + GLPK.ECOND => (MOI.NUMERICAL_ERROR, "Unable to start the search, because the basis matrix corresponding to the initial basis is ill-conditioned, i.e. its condition number is too large."), + GLPK.EBOUND => (MOI.INVALID_MODEL, "Unable to start the search, because some double-bounded (auxiliary or structural) variables have incorrect bounds."), + GLPK.EFAIL => (MOI.NUMERICAL_ERROR, "The search was prematurely terminated due to the solver failure."), + GLPK.EOBJLL => (MOI.OBJECTIVE_LIMIT, "The search was prematurely terminated, because the objective function being maximized has reached its lower limit and continues decreasing (the dual simplex only)."), + GLPK.EOBJUL => (MOI.OBJECTIVE_LIMIT, "The search was prematurely terminated, because the objective function being minimized has reached its upper limit and continues increasing (the dual simplex only)."), + GLPK.EITLIM => (MOI.ITERATION_LIMIT, "The search was prematurely terminated, because the simplex iteration limit has been exceeded."), + GLPK.ETMLIM => (MOI.TIME_LIMIT, "The search was prematurely terminated, because the time limit has been exceeded."), + GLPK.ENOPFS => (MOI.INFEASIBLE, "The LP problem instance has no primal feasible solution (only if the LP presolver is used)."), + GLPK.ENODFS => (MOI.DUAL_INFEASIBLE, "The LP problem instance has no dual feasible solution (only if the LP presolver is used).") +) + +const RAW_EXACT_STRINGS = Dict{Int32, Tuple{MOI.TerminationStatusCode, String}}( + GLPK.EBADB => (MOI.INVALID_MODEL, "Unable to start the search, because the initial basis specified in the problem object is invalid—the number of basic (auxiliary and structural) variables is not the same as the number of rows in the problem object."), + GLPK.ESING => (MOI.NUMERICAL_ERROR, "Unable to start the search, because the basis matrix corresponding to the initial basis is exactly singular."), + GLPK.EBOUND => (MOI.INVALID_MODEL, "Unable to start the search, because some double-bounded (auxiliary or structural) variables have incorrect bounds."), + GLPK.EFAIL => (MOI.INVALID_MODEL, "The problem instance has no rows/columns."), + GLPK.EITLIM => (MOI.ITERATION_LIMIT, "The search was prematurely terminated, because the simplex iteration limit has been exceeded."), + GLPK.ETMLIM => (MOI.TIME_LIMIT, "The search was prematurely terminated, because the time limit has been exceeded.") +) + +const RAW_INTERIOR_STRINGS = Dict{Int32, Tuple{MOI.TerminationStatusCode, String}}( + GLPK.EFAIL => (MOI.INVALID_MODEL, "The problem instance has no rows/columns."), + GLPK.ENOCVG => (MOI.SLOW_PROGRESS, "Very slow convergence or divergence."), + GLPK.EITLIM => (MOI.ITERATION_LIMIT, "Iteration limit exceeded."), + GLPK.EINSTAB => (MOI.NUMERICAL_ERROR, "Numerical instability on solving Newtonian system.") ) +const RAW_INTOPT_STRINGS = Dict{Int32, Tuple{MOI.TerminationStatusCode, String}}( + GLPK.EBOUND => (MOI.INVALID_MODEL, "Unable to start the search, because some double-bounded (auxiliary or structural) variables have incorrect bounds."), + GLPK.ENOPFS => (MOI.INFEASIBLE, "Unable to start the search, because LP relaxation of the MIP problem instance has no primal feasible solution. (This code may appear only if the presolver is enabled.)"), + GLPK.ENODFS => (MOI.DUAL_INFEASIBLE, "Unable to start the search, because LP relaxation of the MIP problem instance has no dual feasible solution. In other word, this code means that if the LP relaxation has at least one primal feasible solution, its optimal solution is unbounded, so if the MIP problem has at least one integer feasible solution, its (integer) optimal solution is also unbounded. (This code may appear only if the presolver is enabled.)"), + GLPK.EFAIL => (MOI.INVALID_MODEL, "The search was prematurely terminated due to the solver failure."), + GLPK.EMIPGAP => (MOI.OPTIMAL, "The search was prematurely terminated, because the relative mip gap tolerance has been reached."), + GLPK.ETMLIM => (MOI.TIME_LIMIT, "The search was prematurely terminated, because the time limit has been exceeded."), + GLPK.ESTOP => (MOI.INTERRUPTED, "The search was prematurely terminated by application. (This code may appear only if the advanced solver interface is used.)") +) + +const RAW_SOLUTION_STATUS = Dict{Int32, Tuple{MOI.TerminationStatusCode, String}}( + GLPK.OPT => (MOI.OPTIMAL, "Solution is optimal"), + GLPK.FEAS => (MOI.LOCALLY_SOLVED, "Solution is feasible"), + GLPK.INFEAS => (MOI.LOCALLY_INFEASIBLE, "Solution is infeasible"), + GLPK.NOFEAS => (MOI.INFEASIBLE, "No feasible primal-dual solution exists."), + GLPK.UNBND => (MOI.DUAL_INFEASIBLE, "Problem has unbounded solution"), + GLPK.UNDEF => (MOI.OTHER_ERROR, "Solution is undefined") +) + +function MOI.get(model::Optimizer, ::MOI.RawStatusString) + if model.solver_status == Int32(0) + (_, msg) = _get_status(model) + return msg + elseif model.last_solved_by_mip + return RAW_INTOPT_STRINGS[model.solver_status][2] + elseif model.method == SIMPLEX + return RAW_SIMPLEX_STRINGS[model.solver_status][2] + elseif model.method == EXACT + return RAW_EXACT_STRINGS[model.solver_status][2] + else + @assert model.method == INTERIOR + return RAW_INTERIOR_STRINGS[model.solver_status][2] + end +end + +function _get_status(model::Optimizer) + status_code = if model.last_solved_by_mip + GLPK.mip_status(model.inner) + elseif model.method == SIMPLEX || model.method == EXACT + GLPK.get_status(model.inner) + else + @assert model.method == INTERIOR + GLPK.ipt_status(model.inner) + end + return RAW_SOLUTION_STATUS[status_code] +end + """ - get_variable_types(model::Optimizer) + _certificates_potentially_available(model::Optimizer) -Return a vector of symbols (one element for each variable) of the variable type. -The symbols are given by the key-value mapping in `GLPK.VARIABLE_TYPE_MAP`. +Return true if an infeasiblity certificate or an unbounded ray is potentially +available (i.e., the model has been solved using either the Simplex or Exact +methods). """ -function get_variable_types(model::Optimizer) - ncol = GLPK.get_num_cols(model.inner) - col_types = fill(:Cont, ncol) - for i in 1:ncol - col_type = GLPK.get_col_kind(model.inner, i) - col_types[i] = VARIABLE_TYPE_MAP[col_type] - if i in model.binaries - # See the note in the definition of Optimizer about this. - col_types[i] = :Bin - elseif col_types[i] == :Bin - # We never set a variable as binary. GLPK must have made a mistake - # and inferred so based on bounds? - col_types[i] = :Int +function _certificates_potentially_available(model::Optimizer) + return !model.last_solved_by_mip && (model.method == SIMPLEX || model.method == EXACT) +end + +function MOI.get(model::Optimizer, ::MOI.TerminationStatus) + if model.optimize_not_called + return MOI.OPTIMIZE_NOT_CALLED + elseif model.solver_status != Int32(0) + # The solver did not exit successfully for some reason. + if model.last_solved_by_mip + return RAW_INTOPT_STRINGS[model.solver_status][1] + elseif model.method == SIMPLEX + return RAW_SIMPLEX_STRINGS[model.solver_status][1] + elseif model.method == INTERIOR + return RAW_INTERIOR_STRINGS[model.solver_status][1] + else + @assert model.method == EXACT + return RAW_EXACT_STRINGS[model.solver_status][1] end + else + (status, _) = _get_status(model) + return status end - return col_types end -include("infeasibility_certificates.jl") +function MOI.get(model::Optimizer, ::MOI.PrimalStatus) + (status, _) = _get_status(model) + if status == MOI.OPTIMAL || status == MOI.LOCALLY_SOLVED + return MOI.FEASIBLE_POINT + elseif status == MOI.LOCALLY_INFEASIBLE + return MOI.INFEASIBLE_POINT + elseif status == MOI.DUAL_INFEASIBLE + if _certificates_potentially_available(model) + return MOI.INFEASIBILITY_CERTIFICATE + end + else + @assert status == MOI.INFEASIBLE || status == MOI.OTHER_ERROR + end + return MOI.NO_SOLUTION +end -function LQOI.get_farkas_dual!(model::Optimizer, place) - get_infeasibility_ray(model, place) +function MOI.get(model::Optimizer, ::MOI.DualStatus) + if model.last_solved_by_mip + return MOI.NO_SOLUTION + end + (status, _) = _get_status(model) + if status == MOI.OPTIMAL + return MOI.FEASIBLE_POINT + elseif status == MOI.INFEASIBLE || status == MOI.LOCALLY_INFEASIBLE + if _certificates_potentially_available(model) + return MOI.INFEASIBILITY_CERTIFICATE + end + end + return MOI.NO_SOLUTION end -function LQOI.get_unbounded_ray!(model::Optimizer, place) - get_unbounded_ray(model, place) +function _get_col_dual(model::Optimizer, column::Int) + @assert !model.last_solved_by_mip + if model.method == SIMPLEX || model.method == EXACT + return _dual_multiplier(model) * GLPK.get_col_dual(model.inner, column) + else + @assert model.method == INTERIOR + return _dual_multiplier(model) * GLPK.ipt_col_dual(model.inner, column) + end end -# ============================================================================== -# Callbacks in GLPK -# ============================================================================== -""" - CallbackFunction +function _get_col_primal(model::Optimizer, column::Int) + if model.last_solved_by_mip + return GLPK.mip_col_val(model.inner, column) + elseif model.method == SIMPLEX || model.method == EXACT + return GLPK.get_col_prim(model.inner, column) + else + @assert model.method == INTERIOR + return GLPK.ipt_col_prim(model.inner, column) + end +end -The attribute to set the callback function in GLPK. The function takes a single -argument of type `CallbackData`. -""" -struct CallbackFunction <: MOI.AbstractOptimizerAttribute end -function MOI.set(model::Optimizer, ::CallbackFunction, foo::Function) - model.callback_function = foo +function _get_row_primal(model::Optimizer, row::Int) + if model.last_solved_by_mip + return GLPK.mip_row_val(model.inner, row) + elseif model.method == SIMPLEX || model.method == EXACT + return GLPK.get_row_prim(model.inner, row) + else + @assert model.method == INTERIOR + return GLPK.ipt_row_prim(model.inner, row) + end end -""" - load_variable_primal!(cb_data::CallbackData) +function MOI.get(model::Optimizer, ::MOI.VariablePrimal, x::MOI.VariableIndex) + if model.unbounded_ray !== nothing + return model.unbounded_ray[_info(model, x).column] + else + return _get_col_primal(model, _info(model, x).column) + end +end -Load the variable primal solution in a callback. +function MOI.get( + model::Optimizer, ::MOI.ConstraintPrimal, + c::MOI.ConstraintIndex{MOI.SingleVariable, <:Any} +) + return MOI.get(model, MOI.VariablePrimal(), MOI.VariableIndex(c.value)) +end -This can only be called in a callback from `GLPK.IROWGEN`. After it is called, -you can access the `VariablePrimal` attribute as usual. -""" -function load_variable_primal!(cb_data::CallbackData) - model = cb_data.model - if GLPK.ios_reason(cb_data.tree) != GLPK.IROWGEN - error("load_variable_primal! can only be called when reason is GLPK.IROWGEN.") +function MOI.get( + model::Optimizer, ::MOI.ConstraintPrimal, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, <:Any} +) + return _get_row_primal(model, _info(model, c).row) +end + +function _dual_multiplier(model::Optimizer) + return MOI.get(model, MOI.ObjectiveSense()) == MOI.MIN_SENSE ? 1.0 : -1.0 +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintDual, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.LessThan{Float64}} +) + column = _info(model, c).column + if _get_col_primal(model, column) ≈ GLPK.get_col_ub(model.inner, column) + return _get_col_dual(model, column) + else + return 0.0 end - subproblem = GLPK.ios_get_prob(cb_data.tree) - copy_function_result!(model.variable_primal_solution, GLPK.get_col_prim, subproblem) end -""" - add_lazy_constraint!(cb_data::GLPK.CallbackData, func::LQOI.Linear, set::S) where S <: Union{LQOI.LE, LQOI.GE, LQOI.EQ} +function MOI.get( + model::Optimizer, ::MOI.ConstraintDual, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.GreaterThan{Float64}} +) + column = _info(model, c).column + if _get_col_primal(model, column) ≈ GLPK.get_col_lb(model.inner, column) + return _get_col_dual(model, column) + else + return 0.0 + end +end -Add a lazy constraint to the model `cb_data.model`. This can only be called in a -callback from `GLPK.IROWGEN`. -""" -function add_lazy_constraint!(cb_data::CallbackData, func::LQOI.Linear, set::S) where S <: Union{LQOI.LE, LQOI.GE, LQOI.EQ} - model = cb_data.model - add_row!( - GLPK.ios_get_prob(cb_data.tree), - [LQOI.get_column(model, term.variable_index) for term in func.terms], - [term.coefficient for term in func.terms], - LQOI.backend_type(model, set), - MOI.Utilities.getconstant(set) +function MOI.get( + model::Optimizer, ::MOI.ConstraintDual, + c::MOI.ConstraintIndex{MOI.SingleVariable, S} +) where {S <: Union{MOI.EqualTo, MOI.Interval}} + return _get_col_dual(model, _info(model, c).column) +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintDual, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, <:Any} +) + row = _info(model, c).row + if model.infeasibility_cert !== nothing + return model.infeasibility_cert[row] + else + @assert !model.last_solved_by_mip + if model.method == SIMPLEX || model.method == EXACT + return _dual_multiplier(model) * GLPK.get_row_dual(model.inner, row) + else + @assert model.method == INTERIOR + return _dual_multiplier(model) * GLPK.ipt_row_dual(model.inner, row) + end + end +end + +function MOI.get(model::Optimizer, ::MOI.ObjectiveValue) + if model.last_solved_by_mip + return GLPK.mip_obj_val(model.inner) + elseif model.method == SIMPLEX || model.method == EXACT + return GLPK.get_obj_val(model.inner) + else + @assert model.method == INTERIOR + return GLPK.ipt_obj_val(model.inner) + end +end + +function MOI.get(model::Optimizer, ::MOI.ObjectiveBound) + if !model.last_solved_by_mip + return MOI.get(model, MOI.ObjectiveSense()) == MOI.MIN_SENSE ? -Inf : Inf + end + # @mlubin and @ccoey observed some cases where mip_status == OPT and objval + # and objbound didn't match. In that case, they return mip_obj_val, but + # objbound may still be incorrect in cases where GLPK terminates early. + if GLPK.mip_status(model.inner) == GLPK.OPT + return GLPK.mip_obj_val(model.inner) + end + return model.objective_bound +end + +function MOI.get(model::Optimizer, attr::MOI.DualObjectiveValue) + return MOI.Utilities.get_fallback(model, attr, Float64) +end + +MOI.supports(model::Optimizer, ::MOI.RelativeGap) = model.last_solved_by_mip +MOI.get(model::Optimizer, ::MOI.RelativeGap) = model.relative_gap +MOI.supports(::Optimizer, ::MOI.SolveTime) = true +MOI.get(model::Optimizer, ::MOI.SolveTime) = model.solve_time + +function MOI.get(model::Optimizer, ::MOI.ResultCount) + primal = MOI.get(model, MOI.PrimalStatus()) + if primal == MOI.FEASIBLE_POINT || primal == MOI.INFEASIBILITY_CERTIFICATE + return 1 + end + dual = MOI.get(model, MOI.DualStatus()) + if dual == MOI.FEASIBLE_POINT || dual == MOI.INFEASIBILITY_CERTIFICATE + return 1 + end + return 0 +end + +function MOI.get(model::Optimizer, ::MOI.Silent) + return model.silent +end + +function MOI.set(model::Optimizer, ::MOI.Silent, flag::Bool) + model.silent = flag + output_flag = flag ? GLPK.OFF : get(model.params, :msg_lev, GLPK.MSG_ERR) + set_parameter(model, :msg_lev, output_flag) + return +end + +function MOI.get(model::Optimizer, ::MOI.Name) + return GLPK.get_prob_name(model.inner) +end + +function MOI.set(model::Optimizer, ::MOI.Name, name::String) + GLPK.set_prob_name(model.inner, name) + return +end + +MOI.get(model::Optimizer, ::MOI.NumberOfVariables) = length(model.variable_info) +function MOI.get(model::Optimizer, ::MOI.ListOfVariableIndices) + return sort!(collect(keys(model.variable_info)), by = x -> x.value) +end + +MOI.get(model::Optimizer, ::MOI.RawSolver) = model.inner + +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 + +_bound_enums(::Type{<:MOI.LessThan}) = (LESS_THAN, LESS_AND_GREATER_THAN) +_bound_enums(::Type{<:MOI.GreaterThan}) = (GREATER_THAN, LESS_AND_GREATER_THAN) +_bound_enums(::Type{<:MOI.Interval}) = (INTERVAL,) +_bound_enums(::Type{<:MOI.EqualTo}) = (EQUAL_TO,) +_bound_enums(::Any) = (nothing,) + +_type_enums(::Type{MOI.ZeroOne}) = (BINARY,) +_type_enums(::Type{MOI.Integer}) = (INTEGER,) +_type_enums(::Any) = (nothing,) + +function MOI.get( + model::Optimizer, ::MOI.ListOfConstraintIndices{MOI.SingleVariable, S} +) where {S} + indices = MOI.ConstraintIndex{MOI.SingleVariable, S}[] + for (key, info) in model.variable_info + if info.bound in _bound_enums(S) || info.type in _type_enums(S) + push!(indices, MOI.ConstraintIndex{MOI.SingleVariable, S}(key.value)) + end + end + return sort!(indices, by = x -> x.value) +end + +function MOI.get( + model::Optimizer, + ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{Float64}, S} +) where {S} + indices = MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, S}[] + for (key, info) in model.affine_constraint_info + if typeof(info.set) == S + push!(indices, MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, S}(key.value)) + end + end + return sort!(indices, by = x -> x.value) +end + +function MOI.get(model::Optimizer, ::MOI.ListOfConstraints) + constraints = Set{Tuple{DataType, DataType}}() + for info in values(model.variable_info) + if info.bound == NONE + elseif info.bound == LESS_THAN + push!(constraints, (MOI.SingleVariable, MOI.LessThan{Float64})) + elseif info.bound == GREATER_THAN + push!(constraints, (MOI.SingleVariable, MOI.GreaterThan{Float64})) + elseif info.bound == LESS_AND_GREATER_THAN + push!(constraints, (MOI.SingleVariable, MOI.LessThan{Float64})) + push!(constraints, (MOI.SingleVariable, MOI.GreaterThan{Float64})) + elseif info.bound == EQUAL_TO + push!(constraints, (MOI.SingleVariable, MOI.EqualTo{Float64})) + elseif info.bound == INTERVAL + push!(constraints, (MOI.SingleVariable, MOI.Interval{Float64})) + end + if info.type == CONTINUOUS + elseif info.type == BINARY + push!(constraints, (MOI.SingleVariable, MOI.ZeroOne)) + elseif info.type == INTEGER + push!(constraints, (MOI.SingleVariable, MOI.Integer)) + end + end + for info in values(model.affine_constraint_info) + push!(constraints, (MOI.ScalarAffineFunction{Float64}, typeof(info.set))) + end + return collect(constraints) +end + +function MOI.get(model::Optimizer, ::MOI.ObjectiveFunctionType) + if model.objective_type == SINGLE_VARIABLE + return MOI.SingleVariable + else + @assert model.objective_type == SCALAR_AFFINE + return MOI.ScalarAffineFunction{Float64} + end +end + +# TODO(odow): is there a way to modify a single element, rather than the whole +# row? +function MOI.modify( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, <:Any}, + chg::MOI.ScalarCoefficientChange{Float64} +) + row = _info(model, c).row + col = _info(model, chg.variable).column + columns, coefficients = GLPK.get_mat_row(model.inner, row) + index = something(findfirst(isequal(col), columns), 0) + if index > 0 + coefficients[index] = chg.new_coefficient + else + push!(columns, col) + push!(coefficients, chg.new_coefficient) + end + GLPK.set_mat_row(model.inner, row, columns, coefficients) + return +end + +function MOI.modify( + model::Optimizer, + c::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}, + chg::MOI.ScalarCoefficientChange{Float64} +) + GLPK.set_obj_coef( + model.inner, _info(model, chg.variable).column, chg.new_coefficient ) + return +end + +function MOI.set( + model::Optimizer, ::MOI.ConstraintFunction, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, <:SCALAR_SETS}, + f::MOI.ScalarAffineFunction{Float64} +) + if !iszero(f.constant) + throw(MOI.ScalarFunctionConstantNotZero(f.constant)) + end + row = _info(model, c).row + indices, coefficients = _indices_and_coefficients(model, f) + GLPK.set_mat_row(model.inner, row, indices, coefficients) + return +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintBasisStatus, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, S} +) where {S <: SCALAR_SETS} + row = _info(model, c).row + cbasis = GLPK.get_row_stat(model.inner, row) + if cbasis == GLPK.BS + return MOI.BASIC + elseif cbasis == GLPK.NL || cbasis == GLPK.NU || cbasis == GLPK.NF || cbasis == GLPK.NS + return MOI.NONBASIC + else + error("CBasis value of $(cbasis) isn't defined.") + end +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintBasisStatus, + c::MOI.ConstraintIndex{MOI.SingleVariable, S} +) where {S <: SCALAR_SETS} + column = _info(model, c).column + vbasis = GLPK.get_col_stat(model.inner, column) + if vbasis == GLPK.BS + return MOI.BASIC + elseif vbasis == GLPK.NL + if S <: MOI.LessThan + return MOI.BASIC + elseif !(S <: MOI.Interval) + return MOI.NONBASIC + else + return MOI.NONBASIC_AT_LOWER + end + elseif vbasis == GLPK.NU + MOI.NONBASIC_AT_UPPER + if S <: MOI.GreaterThan + return MOI.BASIC + elseif !(S <: MOI.Interval) + return MOI.NONBASIC + else + return MOI.NONBASIC_AT_UPPER + end + elseif vbasis == GLPK.NF + return MOI.NONBASIC + elseif vbasis == GLPK.NS + return MOI.NONBASIC + else + error("VBasis value of $(vbasis) isn't defined.") + end +end + +struct CallbackFunction <: MOI.AbstractOptimizerAttribute end + +function MOI.set(model::Optimizer, ::CallbackFunction, f::Function) + model.callback_function = f + return +end + +struct CallbackVariablePrimal <: MOI.AbstractVariableAttribute + cb_data::CallbackData +end + +function MOI.get( + model::Optimizer, attr::CallbackVariablePrimal, x::MOI.VariableIndex +) + model = attr.cb_data.model + if GLPK.ios_reason(attr.cb_data.tree) != GLPK.IROWGEN + error("CallbackVariablePrimal can only be called when reason is GLPK.IROWGEN.") + end + subproblem = GLPK.ios_get_prob(attr.cb_data.tree) + return GLPK.get_col_prim(subproblem, _info(model, x).column) +end + +function cblazy!( + cb_data::CallbackData, + f::MOI.ScalarAffineFunction{Float64}, + s::Union{MOI.LessThan{Float64}, MOI.GreaterThan{Float64}, MOI.EqualTo{Float64}} +) + indices, coefficients = _indices_and_coefficients(cb_data.model, f) + sense, rhs = _sense_and_rhs(s) + _add_affine_constraint( + GLPK.ios_get_prob(cb_data.tree), indices, coefficients, sense, rhs + ) + return end diff --git a/src/infeasibility_certificates.jl b/src/infeasibility_certificates.jl index 22926e1..da9b768 100644 --- a/src/infeasibility_certificates.jl +++ b/src/infeasibility_certificates.jl @@ -59,11 +59,11 @@ function get_infeasibility_ray(model::Optimizer, ray::Vector{Float64}) lower_bound = GLPK.get_col_lb(model.inner, k) end if primal > upper_bound + eps - ray[i] = -1 + ray[row] = -1 elseif primal < lower_bound - eps - ray[i] = 1 + ray[row] = 1 else - continue # ray[i] == 0 + continue # ray[row] == 0 end if idx <= num_rows ray[row] *= GLPK.get_rii(model.inner, k) diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index 970345e..8f3f47c 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -1,79 +1,137 @@ -using LinQuadOptInterface +using GLPK, Test -const MOI = LinQuadOptInterface.MathOptInterface +const MOI = GLPK.MathOptInterface const MOIT = MOI.Test -const MOIU = MOI.Utilities -@testset "Unit Tests" begin - config = MOIT.TestConfig() - solver = GLPK.Optimizer() - - MOIT.basic_constraint_tests(solver, config) +const OPTIMIZER = MOI.Bridges.full_bridge_optimizer( + GLPK.Optimizer(), Float64 +) +const CONFIG = MOIT.TestConfig() - MOIT.unittest(solver, config, [ +@testset "Unit Tests" begin + MOIT.basic_constraint_tests(OPTIMIZER, CONFIG) + MOIT.unittest(OPTIMIZER, CONFIG, [ # These are excluded because GLPK does not support quadratics. - "solve_qp_edge_cases", - "solve_qcp_edge_cases" + "solve_qp_edge_cases", "solve_qcp_edge_cases", + "solve_zero_one_with_bounds_3" ]) - - MOIT.modificationtest(solver, config) + @testset "solve_zero_one_with_bounds_3" begin + MOI.empty!(OPTIMIZER) + MOI.Utilities.loadfromstring!(OPTIMIZER,""" + variables: x + maxobjective: 2.0x + c1: x in ZeroOne() + c2: x >= 0.2 + c3: x <= 0.5 + """) + MOI.optimize!(OPTIMIZER) + # We test this here because the TerminationStatus is INVALID_MODEL not + # INFEASIBLE. + @test MOI.get(OPTIMIZER, MOI.TerminationStatus()) == MOI.INVALID_MODEL + end + MOIT.modificationtest(OPTIMIZER, CONFIG) end @testset "Linear tests" begin - solver = GLPK.Optimizer() - MOIT.contlineartest(solver, MOIT.TestConfig(), [ - # GLPK returns InfeasibleOrUnbounded - "linear8a", - # Requires infeasiblity certificate for variable bounds - "linear12", - # FIXME - "partial_start" - ]) +@testset "Default Solver" begin + MOIT.contlineartest(OPTIMIZER, MOIT.TestConfig(basis = true), [ + # This requires an infeasiblity certificate for a variable bound. + "linear12", + # VariablePrimalStart not supported. + "partial_start" + ]) + end + @testset "No certificate" begin + MOIT.linear12test(OPTIMIZER, MOIT.TestConfig(infeas_certificates=false)) + end end @testset "Linear Conic tests" begin - MOIT.lintest(GLPK.Optimizer(), MOIT.TestConfig(infeas_certificates=false)) + MOIT.lintest(OPTIMIZER, CONFIG) end @testset "Integer Linear tests" begin - MOIT.intlineartest(GLPK.Optimizer(), MOIT.TestConfig(), [ - # int2 is excluded because SOS constraints are not supported. - "int2" + MOIT.intlineartest(OPTIMIZER, CONFIG, [ + "int2", "indicator1", "indicator2", "indicator3" ]) end @testset "ModelLike tests" begin - solver = GLPK.Optimizer() - @test MOI.get(solver, MOI.SolverName()) == "GLPK" + @test MOI.get(OPTIMIZER, MOI.SolverName()) == "GLPK" + @testset "default_objective_test" begin - MOIT.default_objective_test(solver) - end - @testset "default_status_test" begin - MOIT.default_status_test(solver) - end + MOIT.default_objective_test(OPTIMIZER) + end + + @testset "default_status_test" begin + MOIT.default_status_test(OPTIMIZER) + end + @testset "nametest" begin - MOIT.nametest(solver) + MOIT.nametest(OPTIMIZER) end + @testset "validtest" begin - MOIT.validtest(solver) + MOIT.validtest(OPTIMIZER) end + @testset "emptytest" begin - MOIT.emptytest(solver) + MOIT.emptytest(OPTIMIZER) end + @testset "orderedindicestest" begin - MOIT.orderedindicestest(solver) + MOIT.orderedindicestest(OPTIMIZER) end + @testset "copytest" begin - MOIT.copytest(solver, GLPK.Optimizer()) + MOIT.copytest( + OPTIMIZER, + MOI.Bridges.full_bridge_optimizer(GLPK.Optimizer(), Float64) + ) + end + + @testset "scalar_function_constant_not_zero" begin + MOIT.scalar_function_constant_not_zero(OPTIMIZER) + end + + @testset "start_values_test" begin + # We don't support ConstraintDualStart or ConstraintPrimalStart yet. + # @test_broken MOIT.start_values_test(GLPK.Optimizer(), OPTIMIZER) + end + + @testset "supports_constrainttest" begin + # supports_constrainttest needs VectorOfVariables-in-Zeros, + # MOIT.supports_constrainttest(GLPK.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, MOIT.UnknownVectorSet) + end + + @testset "set_lower_bound_twice" begin + MOIT.set_lower_bound_twice(OPTIMIZER, Float64) + end + + @testset "set_upper_bound_twice" begin + MOIT.set_upper_bound_twice(OPTIMIZER, Float64) end end @testset "Parameter setting" begin solver = GLPK.Optimizer(tm_lim=1, ord_alg=2, alien=3) - @test solver.simplex.tm_lim == 1 - @test solver.intopt.tm_lim == 1 - @test solver.interior.ord_alg == 2 - @test solver.intopt.alien == 3 + @test solver.simplex_param.tm_lim == 1 + @test solver.intopt_param.tm_lim == 1 + @test solver.interior_param.ord_alg == 2 + @test solver.intopt_param.alien == 3 end @testset "Issue #79" begin @@ -86,7 +144,7 @@ end c2: x in LessThan(1.0) """) MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == MOI.INFEASIBLE_OR_UNBOUNDED + @test MOI.get(model, MOI.TerminationStatus()) == MOI.DUAL_INFEASIBLE end @testset "An infeasible integer model" begin @@ -125,9 +183,8 @@ end reason = GLPK.ios_reason(cb_data.tree) push!(cb_calls, reason) if reason == GLPK.IROWGEN - GLPK.load_variable_primal!(cb_data) - x_val = MOI.get(model, MOI.VariablePrimal(), x) - y_val = MOI.get(model, MOI.VariablePrimal(), y) + x_val = MOI.get(model, GLPK.CallbackVariablePrimal(cb_data), x) + y_val = MOI.get(model, GLPK.CallbackVariablePrimal(cb_data), y) # We have two constraints, one cutting off the top # left corner and one cutting off the top right corner, e.g. # (0,2) +---+---+ (2,2) @@ -139,7 +196,7 @@ end # (0,0) +---+---+ (2,0) TOL = 1e-6 # Allow for some impreciseness in the solution if y_val - x_val > 1 + TOL - GLPK.add_lazy_constraint!(cb_data, + GLPK.cblazy!(cb_data, MOI.ScalarAffineFunction{Float64}( MOI.ScalarAffineTerm.([-1.0, 1.0], [x, y]), 0.0 @@ -147,7 +204,7 @@ end MOI.LessThan{Float64}(1.0) ) elseif y_val + x_val > 3 + TOL - GLPK.add_lazy_constraint!(cb_data, + GLPK.cblazy!(cb_data, MOI.ScalarAffineFunction{Float64}( MOI.ScalarAffineTerm.([1.0, 1.0], [x, y]), 0.0 @@ -170,31 +227,13 @@ end end end -@testset "Constant objectives" begin - # Test that setting the objective constant actually propagates - # to the solver, rather than being cached in the LinQuadOptInterface - # layer. - model = GLPK.Optimizer() - MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{Float64}[], 1.5)) - MOI.optimize!(model) - @test MOI.get(model, MOI.ObjectiveValue()) == 1.5 - @test GLPK.get_obj_val(model.inner) == 1.5 - - MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), - MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{Float64}[], -2.0)) - MOI.optimize!(model) - @test MOI.get(model, MOI.ObjectiveValue()) == -2.0 - @test GLPK.get_obj_val(model.inner) == -2.0 -end - @testset "Issue #70" begin model = GLPK.Optimizer() x = MOI.add_variable(model) f = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0], [x]), 0.0) s = MOI.LessThan(2.0) c = MOI.add_constraint(model, f, s) - row = model[c] + row = GLPK._info(model, c).row @test GLPK.get_row_type(model.inner, row) == GLPK.UP @test GLPK.get_row_lb(model.inner, row) == -GLPK.DBL_MAX @test GLPK.get_row_ub(model.inner, row) == 2.0 @@ -207,20 +246,130 @@ end end @testset "Infeasible bounds" begin - @testset "SingleVariable" begin - model = GLPK.Optimizer() + model = GLPK.Optimizer() + x = MOI.add_variable(model) + MOI.add_constraint(model, MOI.SingleVariable(x), MOI.Interval(1.0, -1.0)) + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.INVALID_MODEL +end + +@testset "RawParameter" begin + model = GLPK.Optimizer(method = GLPK.SIMPLEX) + exception = ErrorException("Invalid option: cb_func. Use the MOI attribute `GLPK.CallbackFunction` instead.") + @test_throws exception MOI.set(model, MOI.RawParameter("cb_func"), (cb) -> nothing) + MOI.set(model, MOI.RawParameter("tm_lim"), 100) + @test MOI.get(model, MOI.RawParameter("tm_lim")) == 100 + @test_throws ErrorException MOI.get(model, MOI.RawParameter(:tm_lim)) + @test_throws ErrorException MOI.set(model, MOI.RawParameter(:tm_lim), 120) + @test_throws ErrorException MOI.set(model, MOI.RawParameter("bad"), 1) + @test_throws ErrorException MOI.get(model, MOI.RawParameter("bad")) + + model = GLPK.Optimizer(method = GLPK.INTERIOR) + exception = ErrorException("Invalid option: cb_func. Use the MOI attribute `GLPK.CallbackFunction` instead.") + @test_throws exception MOI.set(model, MOI.RawParameter("cb_func"), (cb) -> nothing) + MOI.set(model, MOI.RawParameter("tm_lim"), 100) + @test MOI.get(model, MOI.RawParameter("tm_lim")) == 100 + @test_throws ErrorException MOI.set(model, MOI.RawParameter("bad"), 1) + @test_throws ErrorException MOI.get(model, MOI.RawParameter("bad")) + + model = GLPK.Optimizer(method = GLPK.EXACT) + exception = ErrorException("Invalid option: cb_func. Use the MOI attribute `GLPK.CallbackFunction` instead.") + @test_throws exception MOI.set(model, MOI.RawParameter("cb_func"), (cb) -> nothing) + MOI.set(model, MOI.RawParameter("tm_lim"), 100) + @test MOI.get(model, MOI.RawParameter("tm_lim")) == 100 + @test_throws ErrorException MOI.set(model, MOI.RawParameter("bad"), 1) + @test_throws ErrorException MOI.get(model, MOI.RawParameter("bad")) + + model = GLPK.Optimizer() + MOI.set(model, MOI.RawParameter("mip_gap"), 0.001) + @test MOI.get(model, MOI.RawParameter("mip_gap")) == 0.001 +end + +@testset "RelativeGap" begin + model = GLPK.Optimizer() + MOI.Utilities.loadfromstring!(model, """ + variables: x + minobjective: 1.0x + c1: x in Integer() + c2: x in GreaterThan(1.5) + """) + MOI.optimize!(model) + @test MOI.supports(model, MOI.RelativeGap()) + @test MOI.get(model, MOI.RelativeGap()) == 0.0 + + model = GLPK.Optimizer() + MOI.Utilities.loadfromstring!(model, """ + variables: x + minobjective: 1.0x + c1: x in GreaterThan(1.5) + """) + MOI.optimize!(model) + @test !MOI.supports(model, MOI.RelativeGap()) + @test isnan(MOI.get(model, MOI.RelativeGap())) +end + +@testset "Extra name tests" begin + model = GLPK.Optimizer() + @testset "Variables" begin + MOI.empty!(model) + x = MOI.add_variables(model, 2) + MOI.set(model, MOI.VariableName(), x[1], "x1") + @test MOI.get(model, MOI.VariableIndex, "x1") == x[1] + MOI.set(model, MOI.VariableName(), x[1], "x2") + @test MOI.get(model, MOI.VariableIndex, "x1") === nothing + @test MOI.get(model, MOI.VariableIndex, "x2") == x[1] + MOI.set(model, MOI.VariableName(), x[2], "x1") + @test MOI.get(model, MOI.VariableIndex, "x1") == x[2] + MOI.set(model, MOI.VariableName(), x[1], "x1") + @test_throws ErrorException MOI.get(model, MOI.VariableIndex, "x1") + end + + @testset "Variable bounds" begin + MOI.empty!(model) x = MOI.add_variable(model) - MOI.add_constraint(model, MOI.SingleVariable(x), MOI.Interval(1.0, -1.0)) - MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == MOI.INVALID_MODEL + c1 = MOI.add_constraint(model, MOI.SingleVariable(x), MOI.GreaterThan(0.0)) + c2 = MOI.add_constraint(model, MOI.SingleVariable(x), MOI.LessThan(1.0)) + MOI.set(model, MOI.ConstraintName(), c1, "c1") + @test MOI.get(model, MOI.ConstraintIndex, "c1") == c1 + MOI.set(model, MOI.ConstraintName(), c1, "c2") + @test MOI.get(model, MOI.ConstraintIndex, "c1") === nothing + @test MOI.get(model, MOI.ConstraintIndex, "c2") == c1 + MOI.set(model, MOI.ConstraintName(), c2, "c1") + @test MOI.get(model, MOI.ConstraintIndex, "c1") == c2 + MOI.set(model, MOI.ConstraintName(), c1, "c1") + @test_throws ErrorException MOI.get(model, MOI.ConstraintIndex, "c1") end - @testset "ScalarAffine" begin - model = GLPK.Optimizer() + + @testset "Affine constraints" begin + MOI.empty!(model) x = MOI.add_variable(model) - MOI.add_constraint(model, - MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0], [x]), 0.0), - MOI.Interval(1.0, -1.0)) - MOI.optimize!(model) - @test MOI.get(model, MOI.TerminationStatus()) == MOI.INVALID_MODEL + f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 0.0) + c1 = MOI.add_constraint(model, f, MOI.GreaterThan(0.0)) + c2 = MOI.add_constraint(model, f, MOI.LessThan(1.0)) + MOI.set(model, MOI.ConstraintName(), c1, "c1") + @test MOI.get(model, MOI.ConstraintIndex, "c1") == c1 + MOI.set(model, MOI.ConstraintName(), c1, "c2") + @test MOI.get(model, MOI.ConstraintIndex, "c1") === nothing + @test MOI.get(model, MOI.ConstraintIndex, "c2") == c1 + MOI.set(model, MOI.ConstraintName(), c2, "c1") + @test MOI.get(model, MOI.ConstraintIndex, "c1") == c2 + MOI.set(model, MOI.ConstraintName(), c1, "c1") + @test_throws ErrorException MOI.get(model, MOI.ConstraintIndex, "c1") end end + +@testset "Issue #102" begin + model = GLPK.Optimizer() + x = MOI.add_variable(model) + MOI.add_constraint(model, MOI.SingleVariable(x), MOI.GreaterThan(0.0)) + MOI.add_constraint(model, MOI.SingleVariable(x), MOI.Integer()) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set( + model, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x)], 3.0) + ) + MOI.optimize!(model) + @test MOI.get(model, MOI.ObjectiveValue()) == 3.0 + @test MOI.get(model, MOI.ObjectiveBound()) == 3.0 +end