diff --git a/.gitignore b/.gitignore index ba39cc53..311c41b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ Manifest.toml +perf/*/*.json +perf/*/*.toml +perf/Project.toml diff --git a/Project.toml b/Project.toml index 29cc78fa..7690c8ed 100644 --- a/Project.toml +++ b/Project.toml @@ -5,13 +5,14 @@ version = "0.7.0" [deps] Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" -LinQuadOptInterface = "f8899e07-120b-5979-ab1d-7b97bb9e1a48" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" MathProgBase = "fdba3010-5040-5b88-9595-932c9decdf73" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [compat] -LinQuadOptInterface = "~0.6.0" +MathOptInterface = "~0.9.0" MathProgBase = "~0.5.0, ~0.6, ~0.7" julia = "1" diff --git a/perf/perf.jl b/perf/perf.jl new file mode 100644 index 00000000..2fc73bd7 --- /dev/null +++ b/perf/perf.jl @@ -0,0 +1,44 @@ +using Gurobi + +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 = Gurobi.MOI.Benchmarks + const GUROBI_ENV = Gurobi.Env() + const suite = Benchmarks.suite() do + Gurobi.Optimizer(GUROBI_ENV, OutputFlag = 0) + end + 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 6609a712..d0167e9e 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -1,58 +1,99 @@ -using LinQuadOptInterface - -const LQOI = LinQuadOptInterface -const MOI = LQOI.MOI - -const SUPPORTED_OBJECTIVES = [ - LQOI.SinVar, - LQOI.Linear, - LQOI.Quad -] +import MathOptInterface + +const MOI = MathOptInterface +const CleverDicts = MOI.Utilities.CleverDicts + +@enum(VariableType, CONTINUOUS, BINARY, INTEGER, SEMIINTEGER, SEMICONTINUOUS) +@enum(BoundType, NONE, LESS_THAN, GREATER_THAN, LESS_AND_GREATER_THAN, INTERVAL, EQUAL_TO) +@enum(ObjectiveType, SINGLE_VARIABLE, SCALAR_AFFINE, SCALAR_QUADRATIC) + +mutable struct VariableInfo + index::MOI.VariableIndex + column::Int + bound::BoundType + type::VariableType + start::Union{Float64, Nothing} + name::String + # Storage for constraint names associated with variables because Gurobi + # 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, nothing, "", "", "", "") + end +end -const SUPPORTED_CONSTRAINTS = [ - (LQOI.Linear, LQOI.EQ), - (LQOI.Linear, LQOI.LE), - (LQOI.Linear, LQOI.GE), - # (Linear, IV), - (LQOI.Quad, LQOI.EQ), - (LQOI.Quad, LQOI.LE), - (LQOI.Quad, LQOI.GE), - (LQOI.SinVar, LQOI.EQ), - (LQOI.SinVar, LQOI.LE), - (LQOI.SinVar, LQOI.GE), - (LQOI.SinVar, LQOI.IV), - (LQOI.SinVar, MOI.ZeroOne), - (LQOI.SinVar, MOI.Integer), - (LQOI.VecVar, LQOI.SOS1), - (LQOI.VecVar, LQOI.SOS2), - (LQOI.SinVar, MOI.Semicontinuous{Float64}), - (LQOI.SinVar, MOI.Semiinteger{Float64}), - (LQOI.VecVar, MOI.Nonnegatives), - (LQOI.VecVar, MOI.Nonpositives), - (LQOI.VecVar, MOI.Zeros), - (LQOI.VecLin, MOI.Nonnegatives), - (LQOI.VecLin, MOI.Nonpositives), - (LQOI.VecLin, MOI.Zeros) -] +mutable struct ConstraintInfo + row::Int + set::MOI.AbstractSet + # Storage for constraint names. Where possible, these are also stored in the + # Gurobi model. + name::String + ConstraintInfo(row::Int, set) = new(row, set, "") +end -mutable struct Optimizer <: LQOI.LinQuadOptimizer - LQOI.@LinQuadOptimizerBase(Model) +mutable struct Optimizer <: MOI.AbstractOptimizer + # The low-level Gurobi model. + inner::Model + # The Gurobi environment. If `nothing`, a new environment will be created + # on `MOI.empty!`. env::Union{Nothing, Env} + # The current user-provided parameters for the model. params::Dict{String, Any} - # The next two fields are used to cleverly manage calls to `update_model!`. + + # The next field is used to cleverly manage calls to `update_model!`. # `needs_update` is used to record whether an update should be called before - # accessing a model attribute (such as the value of a RHS term). One of the - # main pain-points when using Gurobi through JuMP is the need to access the - # number of variables. Unfortunately, in order to use the Gurobi API (i.e. - # `num_vars(model.inner)`), we need to call `update_model!`. To avoid - # frequent calls just for this case, we cache the number of variables in - # `num_variables`. There are calls in `add_variables` and `delete_variables` - # to adjust this value. In addition, we also cache the number of linear and - # quadratic constraints. + # accessing a model attribute (such as the value of a RHS term). needs_update::Bool - num_variables::Int - num_linear_constraints::Int - num_quadratic_constraints::Int + + # A flag to keep track of MOI.Silent, which over-rides the OutputFlag + # parameter. + silent::Bool + + # An enum to remember what objective is currently stored in the model. + objective_type::ObjectiveType + + # 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 + + # A mapping from the MOI.VariableIndex to the Gurobi column. VariableInfo + # also stores some additional fields like what bounds have been added, the + # variable type, and the names of SingleVariable-in-Set constraints. + variable_info::CleverDicts.CleverDict{MOI.VariableIndex, VariableInfo} + + # An index that is incremented for each new constraint (regardless of type). + # We can check if a constraint is valid by checking if it is in the correct + # xxx_constraint_info. We should _not_ reset this to zero, since then new + # constraints cannot be distinguished from previously created ones. + last_constraint_index::Int + # ScalarAffineFunction{Float64}-in-Set storage. + affine_constraint_info::Dict{Int, ConstraintInfo} + # ScalarQuadraticFunction{Float64}-in-Set storage. + quadratic_constraint_info::Dict{Int, ConstraintInfo} + # VectorOfVariables-in-Set storage. + sos_constraint_info::Dict{Int, ConstraintInfo} + # Note: we do not have a singlevariable_constraint_info dictionary. Instead, + # data associated with these constraints are stored in the VariableInfo + # objects. + + # 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}} + + # These two flags allow us to distinguish between FEASIBLE_POINT and + # INFEASIBILITY_CERTIFICATE when querying VariablePrimal and ConstraintDual. + has_unbounded_ray::Bool + has_infeasibility_cert::Bool + + # A helper cache for calling CallbackVariablePrimal. + callback_variable_primal::Vector{Float64} """ Optimizer(env = nothing; kwargs...) @@ -65,32 +106,82 @@ mutable struct Optimizer <: LQOI.LinQuadOptimizer Note that we set the parameter `InfUnbdInfo` to `1` rather than the default of `0` so that we can query infeasibility certificates. Users are, however, - free to overide this as follows `Gurobi.Optimizer(InfUndbInfo=0)`. + free to over-ride this as follows `Optimizer(InfUndbInfo=0)`. In addition, + we also set `QCPDual` to `1` to enable duals in QCPs. Users can override + this by passing `Optimizer(QCPDual=0)`. """ function Optimizer(env::Union{Nothing, Env} = nothing; kwargs...) model = new() model.env = env + model.silent = false model.params = Dict{String, Any}() - MOI.empty!(model) + model.variable_info = CleverDicts.CleverDict{MOI.VariableIndex, VariableInfo}() + model.affine_constraint_info = Dict{Int, ConstraintInfo}() + model.quadratic_constraint_info = Dict{Int, Int}() + model.sos_constraint_info = Dict{Int, ConstraintInfo}() + model.last_constraint_index = 0 + model.callback_variable_primal = Float64[] + MOI.empty!(model) # MOI.empty!(model) re-sets the `.inner` field. for (name, value) in kwargs model.params[string(name)] = value setparam!(model.inner, string(name), value) end + if !haskey(model.params, "InfUnbdInfo") + MOI.set(model, MOI.RawParameter("InfUnbdInfo"), 1) + end + if !haskey(model.params, "QCPDual") + MOI.set(model, MOI.RawParameter("QCPDual"), 1) + end return model end end -function LQOI.LinearQuadraticModel(::Type{Optimizer}, env::Nothing) - # The existing env is `Nothing`, so create a new one. Since we own this one, - # make sure to finalize it. - new_env = Env() - return Model(new_env, "", finalize_env = true) +Base.show(io::IO, model::Optimizer) = show(io, model.inner) + +function MOI.empty!(model::Optimizer) + if model.env === nothing + model.inner = Model(Env(), "", finalize_env = true) + else + model.inner = Model(model.env, "", finalize_env = false) + end + for (name, value) in model.params + setparam!(model.inner, name, value) + end + if model.silent + # Set the parameter on the internal model, but don't modify the entry in + # model.params so that if Silent() is set to `true`, the user-provided + # value will be restored. + setparam!(model.inner, "OutputFlag", 0) + end + model.needs_update = false + model.objective_type = SCALAR_AFFINE + model.is_feasibility = true + empty!(model.variable_info) + empty!(model.affine_constraint_info) + empty!(model.quadratic_constraint_info) + empty!(model.sos_constraint_info) + model.name_to_variable = nothing + model.name_to_constraint_index = nothing + model.has_unbounded_ray = false + model.has_infeasibility_cert = false + empty!(model.callback_variable_primal) + return end -function LQOI.LinearQuadraticModel(::Type{Optimizer}, env::Env) - # The user has passed an existing Env. Don't finalize it because we don't - # own this one. - return Model(env, "", finalize_env = false) +function MOI.is_empty(model::Optimizer) + model.needs_update && return false + model.objective_type != SCALAR_AFFINE && return false + model.is_feasibility == false && return false + !isempty(model.variable_info) && return false + length(model.affine_constraint_info) != 0 && return false + length(model.quadratic_constraint_info) != 0 && return false + length(model.sos_constraint_info) != 0 && return false + model.name_to_variable !== nothing && return false + model.name_to_constraint_index !== nothing && return false + model.has_unbounded_ray && return false + model.has_infeasibility_cert && return false + length(model.callback_variable_primal) != 0 && return false + return true end """ @@ -104,7 +195,7 @@ function _require_update(model::Optimizer) end """ - _require_update(model::Optimizer) + _update_if_necessary(model::Optimizer) Calls `update_model!`, but only if the `model.needs_update` flag is set. """ @@ -118,606 +209,2129 @@ end MOI.get(::Optimizer, ::MOI.SolverName) = "Gurobi" -function MOI.empty!(model::Optimizer) - MOI.empty!(model, model.env) - setparam!(model.inner, "InfUnbdInfo", 1) - for (name, value) in model.params - setparam!(model.inner, name, value) +function MOI.supports( + ::Optimizer, + ::MOI.ObjectiveFunction{F} +) where {F <: Union{ + MOI.SingleVariable, + MOI.ScalarAffineFunction{Float64}, + MOI.ScalarQuadraticFunction{Float64} +}} + return true +end + +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, + MOI.Semicontinuous{Float64}, MOI.Semiinteger{Float64} +}} + return true +end + +function MOI.supports_constraint( + ::Optimizer, ::Type{MOI.VectorOfVariables}, ::Type{F} +) where {F <: Union{MOI.SOS1{Float64}, MOI.SOS2{Float64}}} + return true +end + +# We choose _not_ to support ScalarAffineFunction-in-Interval and +# ScalarQuadraticFunction-in-Interval because Gurobi introduces some slack +# variables that makes it hard to keep track of the column indices. + +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 MOI.supports_constraint( + ::Optimizer, ::Type{MOI.ScalarQuadraticFunction{Float64}}, ::Type{F} +) where {F <: Union{ + MOI.EqualTo{Float64}, MOI.LessThan{Float64}, MOI.GreaterThan{Float64} +}} + return true +end + +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.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) + model.params[param.name] = value + setparam!(model.inner, param.name, value) + return +end + +function MOI.get(model::Optimizer, param::MOI.RawParameter) + return getparam(model.inner, param.name) +end + +function MOI.set(model::Optimizer, ::MOI.TimeLimitSec, limit::Real) + MOI.set(model, MOI.RawParameter("TimeLimit"), limit) + return +end + +function MOI.get(model::Optimizer, ::MOI.TimeLimitSec) + return MOI.get(model, MOI.RawParameter("TimeLimit")) +end + +MOI.Utilities.supports_default_copy_to(::Optimizer, ::Bool) = true + +function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike; kwargs...) + return MOI.Utilities.automatic_copy_to(dest, src; kwargs...) +end + +function MOI.get(model::Optimizer, ::MOI.ListOfVariableAttributesSet) + return MOI.AbstractVariableAttribute[MOI.VariableName()] +end + +function MOI.get(model::Optimizer, ::MOI.ListOfModelAttributesSet) + attributes = [ + MOI.ObjectiveSense(), + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}() + ] + if MOI.get(model, MOI.Name()) != "" + push!(attributes, MOI.Name()) end - model.needs_update = false - model.num_variables = 0 - model.num_linear_constraints = 0 - model.num_quadratic_constraints = 0 - return -end - -LQOI.supported_constraints(::Optimizer) = SUPPORTED_CONSTRAINTS -LQOI.supported_objectives(::Optimizer) = SUPPORTED_OBJECTIVES - -LQOI.backend_type(::Optimizer, ::MOI.EqualTo{Float64}) = Cchar('=') -LQOI.backend_type(::Optimizer, ::MOI.LessThan{Float64}) = Cchar('<') -LQOI.backend_type(::Optimizer, ::MOI.GreaterThan{Float64}) = Cchar('>') -LQOI.backend_type(::Optimizer, ::MOI.Zeros) = Cchar('=') -LQOI.backend_type(::Optimizer, ::MOI.Nonpositives) = Cchar('<') -LQOI.backend_type(::Optimizer, ::MOI.Nonnegatives) = Cchar('>') - -function LQOI.change_variable_bounds!(model::Optimizer, - columns::Vector{Int}, new_bounds::Vector{Float64}, - senses::Vector{Cchar}) - number_lower_bounds = count(x->x==Cchar('L'), senses) - lower_cols = fill(0, number_lower_bounds) - lower_values = fill(0.0, number_lower_bounds) - number_upper_bounds = count(x->x==Cchar('U'), senses) - upper_cols = fill(0, number_upper_bounds) - upper_values = fill(0.0, number_upper_bounds) - lower_index = 1 - upper_index = 1 - for (column, bound, sense) in zip(columns, new_bounds, senses) - if sense == Cchar('L') - lower_cols[lower_index] = column - lower_values[lower_index] = bound - lower_index += 1 - elseif sense == Cchar('U') - upper_cols[upper_index] = column - upper_values[upper_index] = bound - upper_index += 1 + return attributes +end + +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 _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 _indices_and_coefficients( + I::AbstractVector{Int}, J::AbstractVector{Int}, V::AbstractVector{Float64}, + indices::AbstractVector{Int}, coefficients::AbstractVector{Float64}, + model::Optimizer, f::MOI.ScalarQuadraticFunction +) + for (i, term) in enumerate(f.quadratic_terms) + I[i] = _info(model, term.variable_index_1).column + J[i] = _info(model, term.variable_index_2).column + V[i] = term.coefficient + # Gurobi returns a list of terms. MOI requires 0.5 x' Q x. So, to get + # from + # Gurobi -> MOI => multiply diagonals by 2.0 + # MOI -> Gurobi => multiply diagonals by 0.5 + # Example: 2x^2 + x*y + y^2 + # |x y| * |a b| * |x| = |ax+by bx+cy| * |x| = 0.5ax^2 + bxy + 0.5cy^2 + # |b c| |y| |y| + # Gurobi needs: (I, J, V) = ([0, 0, 1], [0, 1, 1], [2, 1, 1]) + # MOI needs: + # [SQT(4.0, x, x), SQT(1.0, x, y), SQT(2.0, y, y)] + if I[i] == J[i] + V[i] *= 0.5 end end - if number_lower_bounds > 0 - set_dblattrlist!(model.inner, "LB", lower_cols, lower_values) + for (i, term) in enumerate(f.affine_terms) + indices[i] = _info(model, term.variable_index).column + coefficients[i] = term.coefficient end - if number_upper_bounds > 0 - set_dblattrlist!(model.inner, "UB", upper_cols, upper_values) + return +end + +function _indices_and_coefficients( + model::Optimizer, f::MOI.ScalarQuadraticFunction +) + f_canon = MOI.Utilities.canonical(f) + nnz_quadratic = length(f_canon.quadratic_terms) + nnz_affine = length(f_canon.affine_terms) + I = Vector{Int}(undef, nnz_quadratic) + J = Vector{Int}(undef, nnz_quadratic) + V = Vector{Float64}(undef, nnz_quadratic) + indices = Vector{Int}(undef, nnz_affine) + coefficients = Vector{Float64}(undef, nnz_affine) + _indices_and_coefficients(I, J, V, indices, coefficients, model, f_canon) + return indices, coefficients, I, J, V +end + +_sense_and_rhs(s::MOI.LessThan{Float64}) = (Cchar('<'), s.upper) +_sense_and_rhs(s::MOI.GreaterThan{Float64}) = (Cchar('>'), s.lower) +_sense_and_rhs(s::MOI.EqualTo{Float64}) = (Cchar('='), 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 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) + add_cvar!(model.inner, 0.0) _require_update(model) - return + return index +end + +function MOI.add_variables(model::Optimizer, N::Int) + add_cvars!(model.inner, zeros(N)) + _require_update(model) + indices = Vector{MOI.VariableIndex}(undef, N) + num_variables = length(model.variable_info) + 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 + indices[i] = index + end + return indices end -function LQOI.set_variable_bound(model::Optimizer, var::LQOI.SinVar, set::LQOI.LE) - column = LQOI.get_column(model, var) - set_dblattrelement!(model.inner, "UB", column, set.upper) +function MOI.is_valid(model::Optimizer, v::MOI.VariableIndex) + return haskey(model.variable_info, v) +end + +function MOI.delete(model::Optimizer, v::MOI.VariableIndex) + _update_if_necessary(model) + info = _info(model, v) + del_vars!(model.inner, Cint[info.column]) _require_update(model) + 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 + model.name_to_variable = nothing return end -function LQOI.set_variable_bound(model::Optimizer, var::LQOI.SinVar, set::LQOI.GE) - column = LQOI.get_column(model, var) - set_dblattrelement!(model.inner, "LB", column, set.lower) - _require_update(model) +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 + +function _rebuild_name_to_variable(model::Optimizer) + model.name_to_variable = Dict{String, MOI.VariableIndex}() + for (index, info) in model.variable_info + if 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 -function LQOI.set_variable_bound(model::Optimizer, var::LQOI.SinVar, set::LQOI.EQ) - column = LQOI.get_column(model, var) - set_dblattrelement!(model.inner, "LB", column, set.value) - set_dblattrelement!(model.inner, "UB", column, set.value) +function MOI.get(model::Optimizer, ::MOI.VariableName, v::MOI.VariableIndex) + return _info(model, v).name +end + +function MOI.set( + model::Optimizer, ::MOI.VariableName, v::MOI.VariableIndex, name::String +) + info = _info(model, v) + if !isempty(info.name) && model.name_to_variable !== nothing + delete!(model.name_to_variable, info.name) + end + info.name = name + if isempty(name) + return + end + set_strattrelement!(model.inner, "VarName", info.column, name) _require_update(model) + if model.name_to_variable === nothing + return + end + if haskey(model.name_to_variable, name) + model.name_to_variable = nothing + else + model.name_to_variable[name] = v + end return end -function LQOI.set_variable_bound(model::Optimizer, var::LQOI.SinVar, set::LQOI.IV) - column = LQOI.get_column(model, var) - set_dblattrelement!(model.inner, "LB", column, set.lower) - set_dblattrelement!(model.inner, "UB", column, set.upper) +### +### Objectives +### + +function MOI.set( + model::Optimizer, ::MOI.ObjectiveSense, sense::MOI.OptimizationSense +) + if sense == MOI.MIN_SENSE + set_sense!(model.inner, :minimize) + model.is_feasibility = false + elseif sense == MOI.MAX_SENSE + set_sense!(model.inner, :maximize) + model.is_feasibility = false + elseif sense == MOI.FEASIBILITY_SENSE + set_sense!(model.inner, :minimize) + model.is_feasibility = true + else + error("Invalid objective sense: $(sense)") + end _require_update(model) return end -function LQOI.get_variable_lowerbound(model::Optimizer, column::Int) +function MOI.get(model::Optimizer, ::MOI.ObjectiveSense) _update_if_necessary(model) - return get_dblattrelement(model.inner, "LB", column) + sense = model_sense(model.inner) + if model.is_feasibility + return MOI.FEASIBILITY_SENSE + elseif sense == :maximize + return MOI.MAX_SENSE + elseif sense == :minimize + return MOI.MIN_SENSE + end + error("Invalid objective sense: $(sense)") end -function LQOI.get_variable_upperbound(model::Optimizer, column::Int) +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{MOI.SingleVariable}) + obj = MOI.get(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()) + return convert(MOI.SingleVariable, obj) +end + +function MOI.set( + model::Optimizer, ::MOI.ObjectiveFunction{F}, f::F +) where {F <: MOI.ScalarAffineFunction{Float64}} + if model.objective_type == SCALAR_QUADRATIC + # We need to zero out the existing quadratic objective. + delq!(model.inner) + end + 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 + # This update is needed because we might have added some variables. _update_if_necessary(model) - return get_dblattrelement(model.inner, "UB", column) + set_dblattrarray!(model.inner, "Obj", 1, num_vars, obj) + set_dblattr!(model.inner, "ObjCon", f.constant) + _require_update(model) + model.objective_type = SCALAR_AFFINE end -function LQOI.get_number_linear_constraints(model::Optimizer) - # See the definition in Optimizer. - return model.num_linear_constraints +function MOI.get( + model::Optimizer, ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}} +) + if model.objective_type == SCALAR_QUADRATIC + error("Unable to get objective function. Currently: $(model.objective_type).") + end + _update_if_necessary(model) + dest = zeros(length(model.variable_info)) + get_dblattrarray!(dest, model.inner, "Obj", 1) + 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 = get_dblattr(model.inner, "ObjCon") + return MOI.ScalarAffineFunction(terms, constant) end -function LQOI.add_linear_constraints!(model::Optimizer, - A::LQOI.CSRMatrix{Float64}, sense::Vector{Cchar}, rhs::Vector{Float64}) - add_constrs!(model.inner, A.row_pointers, A.columns, A.coefficients, sense, rhs) - model.num_linear_constraints += length(rhs) +function MOI.set( + model::Optimizer, ::MOI.ObjectiveFunction{F}, f::F +) where {F <: MOI.ScalarQuadraticFunction{Float64}} + affine_indices, affine_coefficients, I, J, V = _indices_and_coefficients(model, f) + _update_if_necessary(model) + # We need to zero out any existing linear objective. + obj = zeros(length(model.variable_info)) + for (i, c) in zip(affine_indices, affine_coefficients) + obj[i] = c + end + set_dblattrarray!(model.inner, "Obj", 1, length(obj), obj) + set_dblattr!(model.inner, "ObjCon", f.constant) + # We need to zero out the existing quadratic objective. + delq!(model.inner) + add_qpterms!(model.inner, I, J, V) _require_update(model) + model.objective_type = SCALAR_QUADRATIC return end -function LQOI.get_rhs(model::Optimizer, row::Int) +function MOI.get( + model::Optimizer, + ::MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}} +) _update_if_necessary(model) - return get_dblattrelement(model.inner, "RHS", row) + dest = zeros(length(model.variable_info)) + get_dblattrarray!(dest, model.inner, "Obj", 1) + 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 = get_dblattr(model.inner, "ObjCon") + q_terms = MOI.ScalarQuadraticTerm{Float64}[] + I, J, V = getq(model.inner) + for (i, j, v) in zip(I, J, V) + iszero(v) && continue + # See note in `_indices_and_coefficients`. + new_v = i == j ? 2v : v + push!( + q_terms, + MOI.ScalarQuadraticTerm( + new_v, + model.variable_info[CleverDicts.LinearIndex(i + 1)].index, + model.variable_info[CleverDicts.LinearIndex(j + 1)].index + ) + ) + end + return MOI.ScalarQuadraticFunction(terms, q_terms, constant) end -function LQOI.get_linear_constraint(model::Optimizer, row::Int) - _update_if_necessary(model) - A = SparseArrays.sparse(get_constrs(model.inner, row, 1)') - # note: we return 1-index columns - return A.rowval, A.nzval +function MOI.modify( + model::Optimizer, + ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}, + chg::MOI.ScalarConstantChange{Float64} +) + set_dblattr!(model.inner, "ObjCon", chg.new_constant) + _require_update(model) + return +end + +## +## SingleVariable-in-Set constraints. +## + +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 + +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 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 false +end + +function MOI.is_valid( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.Interval{Float64}} +) + return haskey(model.variable_info, MOI.VariableIndex(c.value)) && + _info(model, c).bound == INTERVAL +end + +function MOI.is_valid( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.EqualTo{Float64}} +) + return haskey(model.variable_info, MOI.VariableIndex(c.value)) && + _info(model, c).bound == EQUAL_TO +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 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 + +function MOI.is_valid( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.Semicontinuous{Float64}} +) + return haskey(model.variable_info, MOI.VariableIndex(c.value)) && + _info(model, c).type == SEMICONTINUOUS +end + +function MOI.is_valid( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.Semiinteger{Float64}} +) + return haskey(model.variable_info, MOI.VariableIndex(c.value)) && + _info(model, c).type == SEMIINTEGER +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintFunction, + c::MOI.ConstraintIndex{MOI.SingleVariable, <:Any} +) + MOI.throw_if_not_valid(model, c) + return MOI.SingleVariable(MOI.VariableIndex(c.value)) +end + +function MOI.set( + model::Optimizer, ::MOI.ConstraintFunction, + c::MOI.ConstraintIndex{MOI.SingleVariable, <:Any}, ::MOI.SingleVariable +) + return throw(MOI.SettingSingleVariableFunctionNotAllowed()) +end + +_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::BoundType, var_type::VariableType, 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} + elseif var_type == SEMIINTEGER + MOI.Semiinteger{Float64} + elseif var_type == SEMICONTINUOUS + MOI.Semicontinuous{Float64} + else + nothing # Also covers `NONE` and `LESS_THAN`. + end + if existing_set !== nothing + throw(MOI.LowerBoundAlreadySet{existing_set, new_set}(variable)) + end +end + +function _throw_if_existing_upper( + bound::BoundType, var_type::VariableType, 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} + elseif var_type == SEMIINTEGER + MOI.Semiinteger{Float64} + elseif var_type == SEMICONTINUOUS + MOI.Semicontinuous{Float64} + else + nothing # Also covers `NONE` and `GREATER_THAN`. + end + if existing_set !== nothing + throw(MOI.UpperBoundAlreadySet{existing_set, new_set}(variable)) + end +end + +function MOI.add_constraint( + model::Optimizer, f::MOI.SingleVariable, s::S +) where {S <: SCALAR_SETS} + info = _info(model, f.variable) + if S <: MOI.LessThan{Float64} + _throw_if_existing_upper(info.bound, 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 + @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.change_matrix_coefficient!(model::Optimizer, row::Int, col::Int, coef::Float64) - chg_coeffs!(model.inner, row, col, coef) +function MOI.add_constraints( + model::Optimizer, f::Vector{MOI.SingleVariable}, s::Vector{S} +) where {S <: SCALAR_SETS} + for fi in f + info = _info(model, fi.variable) + if S <: MOI.LessThan{Float64} + _throw_if_existing_upper(info.bound, info.type, S, fi.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, fi.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, fi.variable) + _throw_if_existing_upper(info.bound, info.type, S, fi.variable) + info.bound = EQUAL_TO + else + @assert S <: MOI.Interval{Float64} + _throw_if_existing_lower(info.bound, info.type, S, fi.variable) + _throw_if_existing_upper(info.bound, info.type, S, fi.variable) + info.bound = INTERVAL + end + end + indices = [ + MOI.ConstraintIndex{MOI.SingleVariable, eltype(s)}(fi.variable.value) + for fi in f + ] + _set_bounds(model, indices, s) + return indices +end + +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.LessThan{Float64}} +) + MOI.throw_if_not_valid(model, c) + info = _info(model, c) + set_dblattrelement!(model.inner, "UB", info.column, Inf) _require_update(model) + if info.bound == LESS_AND_GREATER_THAN + info.bound = GREATER_THAN + else + info.bound = NONE + end + info.lessthan_name = "" return end -function LQOI.change_objective_coefficient!(model::Optimizer, col::Int, coef::Float64) - set_dblattrelement!(model.inner, "Obj", col, coef) +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_dblattrelement!(model.inner, "LB", info.column, -Inf) _require_update(model) + if info.bound == LESS_AND_GREATER_THAN + info.bound = LESS_THAN + else + info.bound = NONE + end + info.greaterthan_interval_or_equalto_name = "" return end -function LQOI.change_rhs_coefficient!(model::Optimizer, row::Int, coef::Float64) - set_dblattrelement!(model.inner, "RHS", row, coef) +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_dblattrelement!(model.inner, "LB", info.column, -Inf) + set_dblattrelement!(model.inner, "UB", info.column, Inf) _require_update(model) + info.bound = NONE + info.greaterthan_interval_or_equalto_name = "" return end -function LQOI.delete_linear_constraints!(model::Optimizer, first_row::Int, last_row::Int) - _update_if_necessary(model) - del_constrs!(model.inner, collect(first_row:last_row)) - model.num_linear_constraints -= last_row - first_row + 1 +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_dblattrelement!(model.inner, "LB", info.column, -Inf) + set_dblattrelement!(model.inner, "UB", info.column, Inf) _require_update(model) + info.bound = NONE + info.greaterthan_interval_or_equalto_name = "" return end -function LQOI.delete_quadratic_constraints!(model::Optimizer, first_row::Int, last_row::Int) +function MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.GreaterThan{Float64}} +) + MOI.throw_if_not_valid(model, c) _update_if_necessary(model) - delqconstrs!(model.inner, collect(first_row:last_row)) - model.num_quadratic_constraints -= last_row - first_row + 1 + lower = get_dblattrelement(model.inner, "LB", _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) + _update_if_necessary(model) + upper = get_dblattrelement(model.inner, "UB", _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) + _update_if_necessary(model) + lower = get_dblattrelement(model.inner, "LB", _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) + _update_if_necessary(model) + info = _info(model, c) + lower = get_dblattrelement(model.inner, "LB", info.column) + upper = get_dblattrelement(model.inner, "UB", info.column) + return MOI.Interval(lower, upper) +end + +function _set_bounds( + model::Optimizer, + indices::Vector{MOI.ConstraintIndex{MOI.SingleVariable, S}}, + sets::Vector{S} +) where {S} + lower_columns, lower_values = Int[], Float64[] + upper_columns, upper_values = Int[], Float64[] + for (c, s) in zip(indices, sets) + lower, upper = _bounds(s) + info = _info(model, c) + if lower !== nothing + push!(lower_columns, info.column) + push!(lower_values, lower) + end + if upper !== nothing + push!(upper_columns, info.column) + push!(upper_values, upper) + end + end + if length(lower_columns) > 0 + set_dblattrlist!(model.inner, "LB", lower_columns, lower_values) + end + if length(upper_columns) > 0 + set_dblattrlist!(model.inner, "UB", upper_columns, upper_values) + end + _require_update(model) +end + +function MOI.set( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.SingleVariable, S}, s::S +) where {S<:SCALAR_SETS} + MOI.throw_if_not_valid(model, c) + lower, upper = _bounds(s) + info = _info(model, c) + if lower !== nothing + set_dblattrelement!(model.inner, "LB", info.column, lower) + end + if upper !== nothing + set_dblattrelement!(model.inner, "UB", info.column, upper) + end _require_update(model) return end -function LQOI.change_variable_types!(model::Optimizer, columns::Vector{Int}, vtypes::Vector{Cchar}) - set_charattrlist!(model.inner, "VType", Cint.(columns), vtypes) +function MOI.add_constraint( + model::Optimizer, f::MOI.SingleVariable, ::MOI.ZeroOne +) + info = _info(model, f.variable) + set_charattrelement!(model.inner, "VType", info.column, Char('B')) + _require_update(model) + info.type = BINARY + 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) + set_charattrelement!(model.inner, "VType", info.column, Char('C')) _require_update(model) + info.type = CONTINUOUS + info.type_constraint_name = "" return end -function LQOI.change_linear_constraint_sense!(model::Optimizer, rows::Vector{Int}, senses::Vector{Cchar}) - set_charattrlist!(model.inner, "Sense", Cint.(rows), senses) +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) + set_charattrelement!(model.inner, "VType", info.column, Char('I')) + _require_update(model) + info.type = INTEGER + 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) + set_charattrelement!(model.inner, "VType", info.column, Char('C')) _require_update(model) + info.type = CONTINUOUS + info.type_constraint_name = "" return end -function LQOI.add_sos_constraint!(model::Optimizer, columns::Vector{Int}, weights::Vector{Float64}, sos_type) - add_sos!(model.inner, sos_type, columns, weights) +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.add_constraint( + model::Optimizer, f::MOI.SingleVariable, s::MOI.Semicontinuous{Float64} +) + info = _info(model, f.variable) + _throw_if_existing_lower(info.bound, info.type, typeof(s), f.variable) + _throw_if_existing_upper(info.bound, info.type, typeof(s), f.variable) + set_charattrelement!(model.inner, "VType", info.column, Char('S')) + set_dblattrelement!(model.inner, "LB", info.column, s.lower) + set_dblattrelement!(model.inner, "UB", info.column, s.upper) _require_update(model) + info.type = SEMICONTINUOUS + return MOI.ConstraintIndex{MOI.SingleVariable, MOI.Semicontinuous{Float64}}(f.variable.value) +end + +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.Semicontinuous{Float64}} +) + MOI.throw_if_not_valid(model, c) + info = _info(model, c) + set_charattrelement!(model.inner, "VType", info.column, Char('C')) + set_dblattrelement!(model.inner, "LB", info.column, -Inf) + set_dblattrelement!(model.inner, "UB", info.column, Inf) + _require_update(model) + info.type = CONTINUOUS + info.type_constraint_name = "" return end -function LQOI.delete_sos!(model::Optimizer, first_row::Int, last_row::Int) +function MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.Semicontinuous{Float64}} +) + MOI.throw_if_not_valid(model, c) + info = _info(model, c) _update_if_necessary(model) - del_sos!(model.inner, Cint.(first_row:last_row)) + lower = get_dblattrelement(model.inner, "LB", info.column) + upper = get_dblattrelement(model.inner, "UB", info.column) + return MOI.Semicontinuous(lower, upper) +end + +function MOI.add_constraint( + model::Optimizer, f::MOI.SingleVariable, s::MOI.Semiinteger{Float64} +) + info = _info(model, f.variable) + _throw_if_existing_lower(info.bound, info.type, typeof(s), f.variable) + _throw_if_existing_upper(info.bound, info.type, typeof(s), f.variable) + set_charattrelement!(model.inner, "VType", info.column, Char('N')) + set_dblattrelement!(model.inner, "LB", info.column, s.lower) + set_dblattrelement!(model.inner, "UB", info.column, s.upper) _require_update(model) + info.type = SEMIINTEGER + return MOI.ConstraintIndex{MOI.SingleVariable, MOI.Semiinteger{Float64}}(f.variable.value) +end + +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.Semiinteger{Float64}} +) + MOI.throw_if_not_valid(model, c) + info = _info(model, c) + set_charattrelement!(model.inner, "VType", info.column, Char('C')) + set_dblattrelement!(model.inner, "LB", info.column, -Inf) + set_dblattrelement!(model.inner, "UB", info.column, Inf) + _require_update(model) + info.type = CONTINUOUS + info.type_constraint_name = "" return end -# TODO improve getting processes -function LQOI.get_sos_constraint(model::Optimizer, idx) +function MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.Semiinteger{Float64}} +) + MOI.throw_if_not_valid(model, c) + info = _info(model, c) _update_if_necessary(model) - A, types = get_sos_matrix(model.inner) - line = A[idx,:] #sparse vec - cols = line.nzind - vals = line.nzval - typ = types[idx] == Cint(1) ? :SOS1 : :SOS2 - return cols, vals, typ -end - -function LQOI.get_number_quadratic_constraints(model::Optimizer) - # See the definition of Optimizer. - return model.num_quadratic_constraints -end - -function scalediagonal!(V, I, J, scale) - # LQOI assumes 0.5 x' Q x, but Gurobi requires the list of terms, e.g., - # 2x^2 + xy + y^2, so we multiply the diagonal of V by 0.5. We don't - # multiply the off-diagonal terms since we assume they are symmetric and we - # only need to give one. - # - # We also need to make sure that after adding the constraint we un-scale - # the vector because we can't modify user-data. - for i in 1:length(I) - if I[i] == J[i] - V[i] *= scale + lower = get_dblattrelement(model.inner, "LB", info.column) + upper = get_dblattrelement(model.inner, "UB", info.column) + return MOI.Semiinteger(lower, upper) +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, MOI.Semiinteger, MOI.Semicontinuous} + 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) + old_name = "" + if S <: MOI.LessThan + old_name = info.lessthan_name + info.lessthan_name = name + elseif S <: Union{MOI.GreaterThan, MOI.Interval, MOI.EqualTo} + old_name = info.greaterthan_interval_or_equalto_name + info.greaterthan_interval_or_equalto_name = name + else + @assert S <: Union{MOI.ZeroOne, MOI.Integer, MOI.Semiinteger, MOI.Semicontinuous} + info.type_constraint_name + info.type_constraint_name = name + end + if model.name_to_constraint_index !== nothing + delete!(model.name_to_constraint_index, old_name) + end + if model.name_to_constraint_index === nothing || isempty(name) + return + end + if haskey(model.name_to_constraint_index, name) + model.name_to_constraint_index = nothing + else + model.name_to_constraint_index[name] = c + end + return +end + +### +### ScalarAffineFunction-in-Set +### + +function _info( + model::Optimizer, + key::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, <:Any} +) + if haskey(model.affine_constraint_info, key.value) + return model.affine_constraint_info[key.value] + end + throw(MOI.InvalidIndex(key)) +end + +function MOI.is_valid( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, S} +) where {S} + info = get(model.affine_constraint_info, c.value, nothing) + if info === nothing + return false + else + return typeof(info.set) == S + end +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 + model.last_constraint_index += 1 + model.affine_constraint_info[model.last_constraint_index] = + ConstraintInfo(length(model.affine_constraint_info) + 1, s) + + indices, coefficients = _indices_and_coefficients(model, f) + sense, rhs = _sense_and_rhs(s) + add_constr!(model.inner, indices, coefficients, sense, rhs) + _require_update(model) + return MOI.ConstraintIndex{typeof(f), typeof(s)}(model.last_constraint_index) +end + +function MOI.add_constraints( + model::Optimizer, f::Vector{MOI.ScalarAffineFunction{Float64}}, + s::Vector{<:Union{MOI.GreaterThan{Float64}, MOI.LessThan{Float64}, MOI.EqualTo{Float64}}} +) + if length(f) != length(s) + error("Number of functions does not equal number of sets.") + end + canonicalized_functions = MOI.Utilities.canonical.(f) + # First pass: compute number of non-zeros to allocate space. + nnz = 0 + for fi in canonicalized_functions + if !iszero(fi.constant) + throw(MOI.ScalarFunctionConstantNotZero{Float64, eltype(f), eltype(s)}(fi.constant)) + end + nnz += length(fi.terms) + end + # Initialize storage + indices = Vector{MOI.ConstraintIndex{eltype(f), eltype(s)}}(undef, length(f)) + row_starts = Vector{Int}(undef, length(f) + 1) + row_starts[1] = 1 + columns = Vector{Int}(undef, nnz) + coefficients = Vector{Float64}(undef, nnz) + senses = Vector{Cchar}(undef, length(f)) + rhss = Vector{Float64}(undef, length(f)) + # Second pass: loop through, passing views to _indices_and_coefficients. + for (i, (fi, si)) in enumerate(zip(canonicalized_functions, s)) + senses[i], rhss[i] = _sense_and_rhs(si) + row_starts[i + 1] = row_starts[i] + length(fi.terms) + _indices_and_coefficients( + view(columns, row_starts[i]:row_starts[i + 1] - 1), + view(coefficients, row_starts[i]:row_starts[i + 1] - 1), + model, fi + ) + model.last_constraint_index += 1 + indices[i] = MOI.ConstraintIndex{eltype(f), eltype(s)}(model.last_constraint_index) + model.affine_constraint_info[model.last_constraint_index] = + ConstraintInfo(length(model.affine_constraint_info) + 1, si) + end + pop!(row_starts) # Gurobi doesn't need the final row start. + add_constrs!(model.inner, row_starts, columns, coefficients, senses, rhss) + _require_update(model) + return indices +end + +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, <:Any} +) + row = _info(model, c).row + _update_if_necessary(model) + del_constrs!(model.inner, row) + _require_update(model) + for (key, info) in model.affine_constraint_info + if info.row > row + info.row -= 1 end end + model.name_to_constraint_index = nothing + delete!(model.affine_constraint_info, c.value) + return +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, S} +) where {S} + _update_if_necessary(model) + rhs = get_dblattrelement(model.inner, "RHS", _info(model, c).row) + return S(rhs) +end + +function MOI.set( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, S}, s::S +) where {S} + set_dblattrelement!(model.inner, "RHS", _info(model, c).row, MOI.constant(s)) + _require_update(model) return end -function LQOI.add_quadratic_constraint!(model::Optimizer, - affine_columns::Vector{Int}, affine_coefficients::Vector{Float64}, - rhs::Float64, sense::Cchar, - I::Vector{Int}, J::Vector{Int}, V::Vector{Float64}) - @assert length(I) == length(J) == length(V) - scalediagonal!(V, I, J, 0.5) - add_qconstr!(model.inner, affine_columns, affine_coefficients, I, J, V, sense, rhs) - scalediagonal!(V, I, J, 2.0) - model.num_quadratic_constraints += 1 + +function MOI.get( + model::Optimizer, ::MOI.ConstraintFunction, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, S} +) where {S} + _update_if_necessary(model) + sparse_a = SparseArrays.sparse(get_constrs(model.inner, _info(model, c).row, 1)') + terms = MOI.ScalarAffineTerm{Float64}[] + for (col, val) in zip(sparse_a.rowval, sparse_a.nzval) + 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) + if !isempty(info.name) && model.name_to_constraint_index !== nothing + delete!(model.name_to_constraint_index, info.name) + end + info.name = name + if !isempty(name) + set_strattrelement!(model.inner, "ConstrName", info.row, name) + _require_update(model) + end + if model.name_to_constraint_index === nothing || isempty(name) + return + end + if haskey(model.name_to_constraint_index, name) + model.name_to_constraint_index = nothing + else + model.name_to_constraint_index[name] = c + end + 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, Int}() + _rebuild_name_to_constraint_index_util( + model, model.affine_constraint_info, MOI.ScalarAffineFunction{Float64} + ) + _rebuild_name_to_constraint_index_util( + model, model.quadratic_constraint_info, MOI.ScalarQuadraticFunction{Float64} + ) + _rebuild_name_to_constraint_index_util( + model, model.sos_constraint_info, MOI.VectorOfVariables + ) + _rebuild_name_to_constraint_index_variables(model) + return +end + +function _rebuild_name_to_constraint_index_util(model::Optimizer, dict, F) + for (index, info) in dict + info.name == "" && continue + if haskey(model.name_to_constraint_index, info.name) + model.name_to_constraint_index = nothing + error("Duplicate constraint name detected: $(info.name)") + end + model.name_to_constraint_index[info.name] = + MOI.ConstraintIndex{F, typeof(info.set)}(index) + end + return +end + +function _rebuild_name_to_constraint_index_variables(model::Optimizer) + for (key, info) in model.variable_info + for S in ( + MOI.LessThan{Float64}, MOI.GreaterThan{Float64}, + MOI.EqualTo{Float64}, MOI.Interval{Float64}, MOI.ZeroOne, + MOI.Integer, MOI.Semicontinuous{Float64}, MOI.Semiinteger{Float64} + ) + constraint_name = "" + if info.bound in _bound_enums(S) + constraint_name = S == MOI.LessThan{Float64} ? + info.lessthan_name : info.greaterthan_interval_or_equalto_name + elseif info.type in _type_enums(S) + constraint_name = info.type_constraint_name + end + constraint_name == "" && continue + if haskey(model.name_to_constraint_index, constraint_name) + model.name_to_constraint_index = nothing + error("Duplicate constraint name detected: ", constraint_name) + end + model.name_to_constraint_index[constraint_name] = + MOI.ConstraintIndex{MOI.SingleVariable, S}(key.value) + end + end + return +end + +### +### ScalarQuadraticFunction-in-SCALAR_SET +### + +function _info( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{Float64}, S} +) where {S} + if haskey(model.quadratic_constraint_info, c.value) + return model.quadratic_constraint_info[c.value] + end + throw(MOI.InvalidIndex(c)) +end + +function MOI.add_constraint( + model::Optimizer, + f::MOI.ScalarQuadraticFunction{Float64}, s::SCALAR_SETS +) + if !iszero(f.constant) + throw(MOI.ScalarFunctionConstantNotZero{Float64, typeof(f), typeof(s)}(f.constant)) + end + indices, coefficients, I, J, V = _indices_and_coefficients(model, f) + sense, rhs = _sense_and_rhs(s) + add_qconstr!(model.inner, indices, coefficients, I, J, V, sense, rhs) + _update_if_necessary(model) + _require_update(model) + model.last_constraint_index += 1 + model.quadratic_constraint_info[model.last_constraint_index] = ConstraintInfo( + length(model.quadratic_constraint_info) + 1, s + ) + return MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{Float64}, typeof(s)}(model.last_constraint_index) +end + +function MOI.is_valid( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{Float64}, S} +) where {S} + info = get(model.quadratic_constraint_info, c.value, nothing) + return info !== nothing && typeof(info.set) == S +end + +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{Float64}, S} +) where {S} + _update_if_necessary(model) + info = _info(model, c) + delqconstrs!(model.inner, [info.row]) _require_update(model) + for (key, info_2) in model.quadratic_constraint_info + if info_2.row > info.row + info_2.row -= 1 + end + end + model.name_to_constraint_index = nothing + delete!(model.quadratic_constraint_info, c.value) return end -function LQOI.get_quadratic_constraint(model::Optimizer, row::Int) +function MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{Float64}, S} +) where {S} _update_if_necessary(model) - affine_cols, affine_coefficients, I, J, V = getqconstr(model.inner, row) - # note: we return 1-index columns here - affine_cols .+= 1 - I .+= 1 - J .+= 1 - return affine_cols, affine_coefficients, SparseArrays.sparse(I, J, V) + rhs = get_dblattrelement(model.inner, "QCRHS", _info(model, c).row) + return S(rhs) end -function LQOI.get_quadratic_rhs(model::Optimizer, row::Int) +function MOI.get( + model::Optimizer, ::MOI.ConstraintFunction, + c::MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{Float64}, S} +) where {S} _update_if_necessary(model) - return get_dblattrelement(model.inner, "QCRHS", row) + affine_cols, affine_coefficients, I, J, V = getqconstr(model.inner, _info(model, c).row) + affine_terms = MOI.ScalarAffineTerm{Float64}[] + for (col, coef) in zip(affine_cols, affine_coefficients) + iszero(coef) && continue + push!( + affine_terms, + MOI.ScalarAffineTerm( + coef, + model.variable_info[CleverDicts.LinearIndex(col + 1)].index + ) + ) + end + quadratic_terms = MOI.ScalarQuadraticTerm{Float64}[] + for (i, j, coef) in zip(I, J, V) + new_coef = i == j ? 2coef : coef + push!( + quadratic_terms, + MOI.ScalarQuadraticTerm( + new_coef, + model.variable_info[CleverDicts.LinearIndex(i + 1)].index, + model.variable_info[CleverDicts.LinearIndex(j + 1)].index + ) + ) + end + constant = get_dblattr(model.inner, "ObjCon") + return MOI.ScalarQuadraticFunction(affine_terms, quadratic_terms, constant) end -function LQOI.set_quadratic_objective!(model::Optimizer, I::Vector{Int}, J::Vector{Int}, V::Vector{Float64}) - @assert length(I) == length(J) == length(V) - delq!(model.inner) - scalediagonal!(V, I, J, 0.5) - add_qpterms!(model.inner, I, J, V) - scalediagonal!(V, I, J, 2.0) - _require_update(model) - return +function MOI.get( + model::Optimizer, ::MOI.ConstraintName, + c::MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{Float64}, S} +) where {S} + return _info(model, c).name end -function LQOI.set_linear_objective!(model::Optimizer, columns::Vector{Int}, coefficients::Vector{Float64}) - nvars = LQOI.get_number_variables(model) - obj = zeros(Float64, nvars) - for (col, coef) in zip(columns, coefficients) - obj[col] += coef +function MOI.set( + model::Optimizer, ::MOI.ConstraintName, + c::MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{Float64}, S}, + name::String +) where {S} + info = _info(model, c) + if !isempty(info.name) && model.name_to_constraint_index !== nothing + delete!(model.name_to_constraint_index, info.name) end - _update_if_necessary(model) - set_dblattrarray!(model.inner, "Obj", 1, nvars, obj) + set_strattrelement!(model.inner, "QCName", info.row, name) _require_update(model) - return -end - -function LQOI.set_constant_objective!(model::Optimizer, value::Real) - set_dblattr!(model.inner, "ObjCon", value) - if LQOI.get_number_variables(model) > 0 - # Work-around for https://github.com/JuliaOpt/LinQuadOptInterface.jl/pull/44#issuecomment-409373755 - _update_if_necessary(model) - set_dblattrarray!(model.inner, "Obj", 1, 1, - get_dblattrarray(model.inner, "Obj", 1, 1)) + info.name = name + if model.name_to_constraint_index === nothing || isempty(name) + return end - _require_update(model) - return -end - -function LQOI.change_objective_sense!(model::Optimizer, sense::Symbol) - if sense == :min - set_sense!(model.inner, :minimize) - elseif sense == :max - set_sense!(model.inner, :maximize) + if haskey(model.name_to_constraint_index, name) + model.name_to_constraint_index = nothing else - error("Invalid objective sense: $(sense)") + model.name_to_constraint_index[c] = name end - _require_update(model) return end -function LQOI.get_linear_objective!(model::Optimizer, dest) - _update_if_necessary(model) - get_dblattrarray!(dest, model.inner, "Obj", 1) - return dest -end +### +### VectorOfVariables-in-SOS{I|II} +### -function LQOI.get_constant_objective(model::Optimizer) - _update_if_necessary(model) - return get_dblattr(model.inner, "ObjCon") -end +const SOS = Union{MOI.SOS1{Float64}, MOI.SOS2{Float64}} -function LQOI.get_objectivesense(model::Optimizer) - _update_if_necessary(model) - sense = model_sense(model.inner) - if sense == :maximize - return MOI.MAX_SENSE - elseif sense == :minimize - return MOI.MIN_SENSE - else - error("Invalid objective sense: $(sense)") +function _info( + model::Optimizer, + key::MOI.ConstraintIndex{MOI.VectorOfVariables, <:SOS} +) + if haskey(model.sos_constraint_info, key.value) + return model.sos_constraint_info[key.value] end + throw(MOI.InvalidIndex(key)) end -function LQOI.get_number_variables(model::Optimizer) - # See the comment in the definition of Optimizer. - return model.num_variables +_sos_type(::MOI.SOS1) = :SOS1 +_sos_type(::MOI.SOS2) = :SOS2 + +function MOI.is_valid( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.VectorOfVariables, S} +) where {S} + info = get(model.sos_constraint_info, c.value, nothing) + if info === nothing + return false + else + return typeof(info.set) == S + end end -function LQOI.add_variables!(model::Optimizer, N::Int) - add_cvars!(model.inner, zeros(N)) - model.num_variables += N +function MOI.add_constraint( + model::Optimizer, f::MOI.VectorOfVariables, s::SOS +) + columns = Int[_info(model, v).column for v in f.variables] + add_sos!(model.inner, _sos_type(s), columns, s.weights) + model.last_constraint_index += 1 + index = MOI.ConstraintIndex{MOI.VectorOfVariables, typeof(s)}(model.last_constraint_index) + model.sos_constraint_info[index.value] = ConstraintInfo( + length(model.sos_constraint_info) + 1, s + ) _require_update(model) - return + return index end -function LQOI.delete_variables!(model::Optimizer, first_col::Int, last_col::Int) +function MOI.delete( + model::Optimizer, c::MOI.ConstraintIndex{MOI.VectorOfVariables, <:SOS} +) + row = _info(model, c).row _update_if_necessary(model) - del_vars!(model.inner, Cint.(first_col:last_col)) - model.num_variables -= length(first_col:last_col) + del_sos!(model.inner, [Cint(row)]) _require_update(model) + for (key, info) in model.sos_constraint_info + if info.row > row + info.row -= 1 + end + end + delete!(model.sos_constraint_info, c.value) return end -function LQOI.add_mip_starts!(model::Optimizer, columns::Vector{Int}, starts::Vector{Float64}) - _update_if_necessary(model) - nvars = LQOI.get_number_variables(model) - x = zeros(nvars) - for (col, val) in zip(columns, starts) - x[col] = val +function MOI.get( + model::Optimizer, ::MOI.ConstraintName, + c::MOI.ConstraintIndex{MOI.VectorOfVariables, <:Any} +) + return _info(model, c).name +end + +function MOI.set( + model::Optimizer, ::MOI.ConstraintName, + c::MOI.ConstraintIndex{MOI.VectorOfVariables, <:Any}, name::String +) + info = _info(model, c) + if !isempty(info.name) && model.name_to_constraint_index !== nothing + delete!(model.name_to_constraint_index, info.name) + end + info.name = name + if model.name_to_constraint_index === nothing || isempty(name) + return + end + if haskey(model.name_to_constraint_index, name) + model.name_to_constraint_index = nothing + else + model.name_to_constraint_index[name] = c end - loadbasis(model.inner, x) - _require_update(model) return end -function MOI.supports( - ::Optimizer, ::MOI.VariablePrimalStart, ::Type{MOI.VariableIndex}) - return true -end +function MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.VectorOfVariables, S} +) where {S <: SOS} + _update_if_necessary(model) + sparse_a, _ = get_sos(model.inner, _info(model, c).row, 1) + return S(sparse_a.nzval) +end -LQOI.solve_mip_problem!(model::Optimizer) = LQOI.solve_linear_problem!(model) +function MOI.get( + model::Optimizer, ::MOI.ConstraintFunction, + c::MOI.ConstraintIndex{MOI.VectorOfVariables, S} +) where {S <: SOS} + _update_if_necessary(model) + sparse_a, _ = get_sos(model.inner, _info(model, c).row, 1) + indices = SparseArrays.nonzeroinds(sparse_a[1, :]) + return MOI.VectorOfVariables( + [model.variable_info[CleverDicts.LinearIndex(i)].index for i in indices] + ) +end -LQOI.solve_quadratic_problem!(model::Optimizer) = LQOI.solve_linear_problem!(model) +### +### Optimize methods. +### -function LQOI.solve_linear_problem!(model::Optimizer) - # Note: Gurobi will call update regardless, so we don't have to. +function MOI.optimize!(model::Optimizer) + # Note: although Gurobi will call update regardless, we do it now so that + # the appropriate `needs_update` flag is set. + _update_if_necessary(model) optimize(model.inner) + model.has_infeasibility_cert = + MOI.get(model, MOI.DualStatus()) == MOI.INFEASIBILITY_CERTIFICATE + model.has_unbounded_ray = + MOI.get(model, MOI.PrimalStatus()) == MOI.INFEASIBILITY_CERTIFICATE return end -function LQOI.get_termination_status(model::Optimizer) - stat = get_status(model.inner) - if stat == :loaded - return MOI.OTHER_ERROR - elseif stat == :optimal - return MOI.OPTIMAL - elseif stat == :infeasible - return MOI.INFEASIBLE - elseif stat == :inf_or_unbd - return MOI.INFEASIBLE_OR_UNBOUNDED - elseif stat == :unbounded - return MOI.DUAL_INFEASIBLE - elseif stat == :cutoff - return MOI.OBJECTIVE_LIMIT - elseif stat == :iteration_limit - return MOI.ITERATION_LIMIT - elseif stat == :node_limit - return MOI.NODE_LIMIT - elseif stat == :time_limit - return MOI.TIME_LIMIT - elseif stat == :solution_limit - return MOI.SOLUTION_LIMIT - elseif stat == :interrupted - return MOI.INTERRUPTED - elseif stat == :numeric - return MOI.NUMERICAL_ERROR - elseif stat == :suboptimal - return MOI.OTHER_LIMIT - elseif stat == :inprogress - return MOI.OTHER_ERROR - elseif stat == :user_obj_limit - return MOI.OBJECTIVE_LIMIT +# These strings are taken directly from the following page of the online Gurobi +# documentation: https://www.com/documentation/8.1/refman/optimization_status_codes.html#sec:StatusCodes +const RAW_STATUS_STRINGS = [ + (MOI.OPTIMIZE_NOT_CALLED, "Model is loaded, but no solution information is available."), + (MOI.OPTIMAL, "Model was solved to optimality (subject to tolerances), and an optimal solution is available."), + (MOI.INFEASIBLE, "Model was proven to be infeasible."), + (MOI.INFEASIBLE_OR_UNBOUNDED, "Model was proven to be either infeasible or unbounded. To obtain a more definitive conclusion, set the DualReductions parameter to 0 and reoptimize."), + (MOI.DUAL_INFEASIBLE, "Model was proven to be unbounded. Important note: an unbounded status indicates the presence of an unbounded ray that allows the objective to improve without limit. It says nothing about whether the model has a feasible solution. If you require information on feasibility, you should set the objective to zero and reoptimize."), + (MOI.OBJECTIVE_LIMIT, "Optimal objective for model was proven to be worse than the value specified in the Cutoff parameter. No solution information is available."), + (MOI.ITERATION_LIMIT, "Optimization terminated because the total number of simplex iterations performed exceeded the value specified in the IterationLimit parameter, or because the total number of barrier iterations exceeded the value specified in the BarIterLimit parameter."), + (MOI.NODE_LIMIT, "Optimization terminated because the total number of branch-and-cut nodes explored exceeded the value specified in the NodeLimit parameter."), + (MOI.TIME_LIMIT, "Optimization terminated because the time expended exceeded the value specified in the TimeLimit parameter."), + (MOI.SOLUTION_LIMIT, "Optimization terminated because the number of solutions found reached the value specified in the SolutionLimit parameter."), + (MOI.INTERRUPTED, "Optimization was terminated by the user."), + (MOI.NUMERICAL_ERROR, "Optimization was terminated due to unrecoverable numerical difficulties."), + (MOI.OTHER_LIMIT, "Unable to satisfy optimality tolerances; a sub-optimal solution is available."), + (MOI.OTHER_ERROR, "An asynchronous optimization call was made, but the associated optimization run is not yet complete."), + (MOI.OBJECTIVE_LIMIT, "User specified an objective limit (a bound on either the best objective or the best bound), and that limit has been reached.") +] + +function MOI.get(model::Optimizer, ::MOI.RawStatusString) + status_code = get_status_code(model.inner) + if 1 <= status_code <= length(RAW_STATUS_STRINGS) + return RAW_STATUS_STRINGS[status_code][2] + end + return MOI.OTHER_ERROR +end + +function MOI.get(model::Optimizer, ::MOI.TerminationStatus) + status_code = get_status_code(model.inner) + if 1 <= status_code <= length(RAW_STATUS_STRINGS) + return RAW_STATUS_STRINGS[status_code][1] end return MOI.OTHER_ERROR end -function LQOI.get_primal_status(model::Optimizer) +function MOI.get(model::Optimizer, ::MOI.PrimalStatus) stat = get_status(model.inner) if stat == :optimal return MOI.FEASIBLE_POINT elseif stat == :solution_limit return MOI.FEASIBLE_POINT - elseif (stat == :inf_or_unbd || stat == :unbounded) && has_primal_ray(model) + elseif (stat == :inf_or_unbd || stat == :unbounded) && _has_primal_ray(model) return MOI.INFEASIBILITY_CERTIFICATE elseif stat == :suboptimal return MOI.FEASIBLE_POINT elseif is_mip(model.inner) && get_sol_count(model.inner) > 0 return MOI.FEASIBLE_POINT - else - return MOI.NO_SOLUTION + end + return MOI.NO_SOLUTION +end + +function _has_dual_ray(model::Optimizer) + try + # Note: for performance reasons, we try to get 1 element because for + # some versions of Gurobi, we cannot query 0 elements without error. + get_dblattrarray(model.inner, "FarkasDual", 1, 1) + return true + catch ex + if isa(ex, GurobiError) + return false + else + rethrow(ex) + end end end -function LQOI.get_dual_status(model::Optimizer) +function MOI.get(model::Optimizer, ::MOI.DualStatus) stat = get_status(model.inner) - if is_mip(model.inner) || is_qcp(model.inner) + if is_mip(model.inner) return MOI.NO_SOLUTION - else - if stat == :optimal - return MOI.FEASIBLE_POINT - elseif stat == :solution_limit - return MOI.FEASIBLE_POINT - elseif (stat == :inf_or_unbd || stat == :infeasible) && has_dual_ray(model) - return MOI.INFEASIBILITY_CERTIFICATE - elseif stat == :suboptimal - return MOI.FEASIBLE_POINT - end + elseif is_qcp(model.inner) && MOI.get(model, MOI.RawParameter("QCPDual")) != 1 + return MOI.NO_SOLUTION + elseif stat == :optimal + return MOI.FEASIBLE_POINT + elseif stat == :solution_limit + return MOI.FEASIBLE_POINT + elseif (stat == :inf_or_unbd || stat == :infeasible) && _has_dual_ray(model) + return MOI.INFEASIBILITY_CERTIFICATE + elseif stat == :suboptimal + return MOI.FEASIBLE_POINT end return MOI.NO_SOLUTION end -function LQOI.get_variable_primal_solution!(model::Optimizer, dest) - get_dblattrarray!(dest, model.inner, "X", 1) - return +function _has_primal_ray(model::Optimizer) + try + # Note: for performance reasons, we try to get 1 element because for + # some versions of Gurobi, we cannot query 0 elements without error. + get_dblattrarray(model.inner, "UnbdRay", 1, 1) + return true + catch ex + if isa(ex, GurobiError) + return false + else + rethrow(ex) + end + end end -function LQOI.get_linear_primal_solution!(model::Optimizer, dest) - get_dblattrarray!(dest, model.inner, "RHS", 1) - dest .-= get_dblattrarray(model.inner, "Slack", 1, num_constrs(model.inner)) - return +function MOI.get(model::Optimizer, ::MOI.VariablePrimal, x::MOI.VariableIndex) + if model.has_unbounded_ray + return get_dblattrelement(model.inner, "UnbdRay", _info(model, x).column) + else + return get_dblattrelement(model.inner, "X", _info(model, x).column) + end end -function LQOI.get_quadratic_primal_solution!(model::Optimizer, dest) - get_dblattrarray!(dest, model.inner, "QCRHS", 1) - dest .-= get_dblattrarray(model.inner, "QCSlack", 1, num_qconstrs(model.inner)) - return +function MOI.get( + model::Optimizer, ::MOI.ConstraintPrimal, + c::MOI.ConstraintIndex{MOI.SingleVariable, <:Any} +) + return MOI.get(model, MOI.VariablePrimal(), MOI.VariableIndex(c.value)) +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintPrimal, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, <:Any} +) + row = _info(model, c).row + _update_if_necessary(model) + rhs = get_dblattrelement(model.inner, "RHS", row) + slack = get_dblattrelement(model.inner, "Slack", row) + return rhs - slack +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintPrimal, + c::MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{Float64}, <:Any} +) + row = _info(model, c).row + _update_if_necessary(model) + rhs = get_dblattrelement(model.inner, "QCRHS", row) + slack = get_dblattrelement(model.inner, "QCSlack", row) + return rhs - slack end -function LQOI.get_variable_dual_solution!(model::Optimizer, dest) - get_dblattrarray!(dest, model.inner, "RC", 1) +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 + x = get_dblattrelement(model.inner, "X", column) + ub = get_dblattrelement(model.inner, "UB", column) + if x ≈ ub + return _dual_multiplier(model) * get_dblattrelement(model.inner, "RC", column) + else + return 0.0 + end +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintDual, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.GreaterThan{Float64}} +) + column = _info(model, c).column + x = get_dblattrelement(model.inner, "X", column) + lb = get_dblattrelement(model.inner, "LB", column) + if x ≈ lb + return _dual_multiplier(model) * get_dblattrelement(model.inner, "RC", column) + else + return 0.0 + end +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintDual, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.EqualTo{Float64}} +) + return _dual_multiplier(model) * get_dblattrelement(model.inner, "RC", _info(model, c).column) +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintDual, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.Interval{Float64}} +) + return _dual_multiplier(model) * get_dblattrelement(model.inner, "RC", _info(model, c).column) +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintDual, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, <:Any} +) + if model.has_infeasibility_cert + return -_dual_multiplier(model) * get_dblattrelement(model.inner, "FarkasDual", _info(model, c).row) + end + return _dual_multiplier(model) * get_dblattrelement(model.inner, "Pi", _info(model, c).row) +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintDual, + c::MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{Float64}, <:Any} +) + return _dual_multiplier(model) * get_dblattrelement(model.inner, "QCPi", _info(model, c).row) +end + +MOI.get(model::Optimizer, ::MOI.ObjectiveValue) = get_dblattr(model.inner, "ObjVal") +MOI.get(model::Optimizer, ::MOI.ObjectiveBound) = get_dblattr(model.inner, "ObjBound") +MOI.get(model::Optimizer, ::MOI.SolveTime) = get_dblattr(model.inner, "RunTime") +MOI.get(model::Optimizer, ::MOI.SimplexIterations) = get_intattr(model.inner, "IterCount") +MOI.get(model::Optimizer, ::MOI.BarrierIterations) = get_intattr(model.inner, "BarIterCount") +MOI.get(model::Optimizer, ::MOI.NodeCount) = get_intattr(model.inner, "NodeCount") +MOI.get(model::Optimizer, ::MOI.RelativeGap) = get_dblattr(model.inner, "MIPGap") + +MOI.supports(model::Optimizer, ::MOI.DualObjectiveValue) = true +MOI.get(model::Optimizer, ::MOI.DualObjectiveValue) = get_dblattr(model.inner, "ObjBound") + +function MOI.get(model::Optimizer, ::MOI.ResultCount) + if model.has_infeasibility_cert || model.has_unbounded_ray + return 1 + end + return get_intattr(model.inner, "SolCount") +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 ? 0 : get(model.params, "OutputFlag", 1) + setparam!(model.inner, "OutputFlag", output_flag) return end -function LQOI.get_linear_dual_solution!(model::Optimizer, dest) - get_dblattrarray!(dest, model.inner, "Pi", 1) +function MOI.get(model::Optimizer, ::MOI.Name) + _update_if_necessary(model) + return get_strattr(model.inner, "ModelName") +end + +function MOI.set(model::Optimizer, ::MOI.Name, name::String) + set_strattr!(model.inner, "ModelName", name) + _require_update(model) return end -function LQOI.get_quadratic_dual_solution!(model::Optimizer, dest) - get_dblattrarray!(dest, model.inner, "QCPi", 1) +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.set( + model::Optimizer, ::MOI.VariablePrimalStart, x::MOI.VariableIndex, + value::Union{Nothing, Float64} +) + info = _info(model, x) + info.start = value + if value !== nothing + set_dblattrelement!(model.inner, "Start", info.column, value) + _require_update(model) + end return end -LQOI.get_objective_value(model::Optimizer) = get_objval(model.inner) +function MOI.get( + model::Optimizer, ::MOI.VariablePrimalStart, x::MOI.VariableIndex +) + return _info(model, x).start +end + +MOI.supports(::Optimizer, ::MOI.ConstraintPrimalStart) = false +MOI.supports(::Optimizer, ::MOI.ConstraintDualStart) = false + +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(::Type{<:MOI.Semicontinuous}) = (SEMICONTINUOUS,) +_type_enums(::Type{<:MOI.Semiinteger}) = (SEMIINTEGER,) +_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)) + end + end + return sort!(indices, by = x -> x.value) +end -function LQOI.get_objective_bound(model::Optimizer) - return get_objbound(model.inner) +function MOI.get( + model::Optimizer, + ::MOI.ListOfConstraintIndices{MOI.ScalarQuadraticFunction{Float64}, S} +) where {S} + indices = MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{Float64}, S}[] + for (key, info) in model.quadratic_constraint_info + if typeof(info.set) == S + push!(indices, MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{Float64}, S}(key)) + end + end + return sort!(indices, by = x -> x.value) end -function LQOI.get_relative_mip_gap(model::Optimizer) - value = LQOI.get_objective_value(model) - bound = LQOI.get_objective_bound(model) - return abs(value - bound) / abs(bound) +function MOI.get( + model::Optimizer, ::MOI.ListOfConstraintIndices{MOI.VectorOfVariables, S} +) where {S} + indices = MOI.ConstraintIndex{MOI.VectorOfVariables, S}[] + for (key, info) in model.sos_constraint_info + if typeof(info.set) == S + push!(indices, MOI.ConstraintIndex{MOI.VectorOfVariables, S}(key)) + end + end + return sort!(indices, by = x -> x.value) +end + +function MOI.get(model::Optimizer, ::MOI.ListOfConstraints) + constraints = Set{Any}() + 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)) + elseif info.type == SEMICONTINUOUS + push!(constraints, (MOI.SingleVariable, MOI.Semicontinuous{Float64})) + elseif info.type == SEMIINTEGER + push!(constraints, (MOI.SingleVariable, MOI.Semiinteger{Float64})) + end + end + for info in values(model.affine_constraint_info) + push!(constraints, (MOI.ScalarAffineFunction{Float64}, typeof(info.set))) + end + for info in values(model.sos_constraint_info) + push!(constraints, (MOI.VectorOfVariables, typeof(info.set))) + end + return collect(constraints) end -function LQOI.get_iteration_count(instance::Optimizer) - return get_iter_count(instance.inner) +function MOI.get(model::Optimizer, ::MOI.ObjectiveFunctionType) + if model.objective_type == SINGLE_VARIABLE + return MOI.SINGLE_VARIABLE + elseif model.objective_type == SCALAR_AFFINE + return MOI.ScalarAffineFunction{Float64} + else + @assert model.objective_type == SCALAR_QUADRATIC + return MOI.ScalarQuadraticFunction{Float64} + end end -function LQOI.get_barrier_iterations(instance::Optimizer) - return get_barrier_iter_count(instance.inner) +function MOI.modify( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, <:Any}, + chg::MOI.ScalarCoefficientChange{Float64} +) + chg_coeffs!( + model.inner, _info(model, c).row, _info(model, chg.variable).column, + chg.new_coefficient + ) + _require_update(model) end -function LQOI.get_node_count(instance::Optimizer) - return get_node_count(instance.inner) +function MOI.modify( + model::Optimizer, + c::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}, + chg::MOI.ScalarCoefficientChange{Float64} +) + set_dblattrelement!( + model.inner, "Obj", _info(model, chg.variable).column, + chg.new_coefficient + ) + _require_update(model) end -function LQOI.get_farkas_dual!(instance::Optimizer, dest) - get_dblattrarray!(dest, instance.inner, "FarkasDual", 1) - dest .*= -1.0 +""" + _replace_with_matching_sparsity!( + model::Optimizer, + previous::MOI.ScalarAffineFunction, + replacement::MOI.ScalarAffineFunction, row::Int + ) + +Internal function, not intended for external use. + +Change the linear constraint function at index `row` in `model` from +`previous` to `replacement`. This function assumes that `previous` and +`replacement` have exactly the same sparsity pattern w.r.t. which variables +they include and that both constraint functions are in canonical form (as +returned by `MOIU.canonical()`. Neither assumption is checked within the body +of this function. +""" +function _replace_with_matching_sparsity!( + model::Optimizer, + previous::MOI.ScalarAffineFunction, + replacement::MOI.ScalarAffineFunction, row::Int +) + rows = fill(Cint(row), length(replacement.terms)) + cols = [Cint(_info(model, t.variable_index).column) for t in replacement.terms] + coefs = MOI.coefficient.(replacement.terms) + chg_coeffs!(model.inner, rows, cols, coefs) return end -function has_dual_ray(model::Optimizer) - try - # Note: for performance reasons, we try to get 1 element because for - # some versions of Gurobi, we cannot query 0 elements without error. - Gurobi.get_dblattrarray(model.inner, "FarkasDual", 1, 1) - return true - catch ex - if isa(ex, Gurobi.GurobiError) +""" + _replace_with_different_sparsity!( + model::Optimizer, + previous::MOI.ScalarAffineFunction, + replacement::MOI.ScalarAffineFunction, row::Int + ) + +Internal function, not intended for external use. + + Change the linear constraint function at index `row` in `model` from +`previous` to `replacement`. This function assumes that `previous` and +`replacement` may have different sparsity patterns. + +This function (and `_replace_with_matching_sparsity!` above) are necessary +because in order to fully replace a linear constraint, we have to zero out the +current matrix coefficients and then set the new matrix coefficients. When the +sparsity patterns match, the zeroing-out step can be skipped. +""" +function _replace_with_different_sparsity!( + model::Optimizer, + previous::MOI.ScalarAffineFunction, + replacement::MOI.ScalarAffineFunction, row::Int +) + # First, zero out the old constraint function terms. + rows = fill(Cint(row), length(previous.terms)) + cols = [Cint(_info(model, t.variable_index).column) for t in previous.terms] + coefs = fill(0.0, length(previous.terms)) + chg_coeffs!(model.inner, rows, cols, coefs) + # Next, set the new constraint function terms. + rows = fill(Cint(row), length(replacement.terms)) + cols = [Cint(_info(model, t.variable_index).column) for t in replacement.terms] + coefs = MOI.coefficient.(replacement.terms) + chg_coeffs!(model.inner, rows, cols, coefs) + return +end + +""" + _matching_sparsity_pattern( + f1::MOI.ScalarAffineFunction{Float64}, + f2::MOI.ScalarAffineFunction{Float64} + ) + +Internal function, not intended for external use. + +Determines whether functions `f1` and `f2` have the same sparsity pattern +w.r.t. their constraint columns. Assumes both functions are already in +canonical form. +""" +function _matching_sparsity_pattern( + f1::MOI.ScalarAffineFunction{Float64}, f2::MOI.ScalarAffineFunction{Float64} +) + if axes(f1.terms) != axes(f2.terms) + return false + end + for (f1_term, f2_term) in zip(f1.terms, f2.terms) + if MOI.term_indices(f1_term) != MOI.term_indices(f2_term) return false - else - rethrow(ex) end end + return true end -function LQOI.get_unbounded_ray!(model::Optimizer, dest) - get_dblattrarray!(dest, model.inner, "UnbdRay", 1) +function MOI.set( + model::Optimizer, ::MOI.ConstraintFunction, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, <:SCALAR_SETS}, + f::MOI.ScalarAffineFunction{Float64} +) + previous = MOI.get(model, MOI.ConstraintFunction(), c) + MOI.Utilities.canonicalize!(previous) + replacement = MOI.Utilities.canonical(f) + _update_if_necessary(model) + # If the previous and replacement constraint functions have exactly + # the same sparsity pattern, then we can take a faster path by just + # passing the replacement terms to the model. But if their sparsity + # patterns differ, then we need to first zero out the previous terms + # and then set the replacement terms. + row = _info(model, c).row + if _matching_sparsity_pattern(previous, replacement) + _replace_with_matching_sparsity!(model, previous, replacement, row) + else + _replace_with_different_sparsity!(model, previous, replacement, row) + end + current_rhs = get_dblattrelement(model.inner, "RHS", row) + new_rhs = current_rhs - (replacement.constant - previous.constant) + set_dblattrelement!(model.inner, "RHS", row, new_rhs) + _require_update(model) return end -function has_primal_ray(model::Optimizer) - try - # Note: for performance reasons, we try to get 1 element because for - # some versions of Gurobi, we cannot query 0 elements without error. - Gurobi.get_dblattrarray(model.inner, "UnbdRay", 1, 1) - return true - catch ex - if isa(ex, Gurobi.GurobiError) - return false +function MOI.get( + model::Optimizer, ::MOI.ConstraintBasisStatus, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, S} +) where {S <: SCALAR_SETS} + row = _info(model, c).row + _update_if_necessary(model) + cbasis = get_intattrelement(model.inner, "CBasis", row) + if cbasis == 0 + return MOI.BASIC + elseif cbasis == -1 + 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 + _update_if_necessary(model) + vbasis = get_intattrelement(model.inner, "VBasis", column) + if vbasis == 0 + return MOI.BASIC + elseif vbasis == -1 + if S <: MOI.LessThan + return MOI.BASIC + elseif !(S <: MOI.Interval) + return MOI.NONBASIC else - rethrow(ex) + return MOI.NONBASIC_AT_LOWER + end + elseif vbasis == -2 + 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 == -3 + return MOI.SUPER_BASIC + else + error("VBasis value of $(vbasis) isn't defined.") end end # ============================================================================== # Callbacks in Gurobi # ============================================================================== + struct CallbackFunction <: MOI.AbstractOptimizerAttribute end + function MOI.set(model::Optimizer, ::CallbackFunction, f::Function) set_callback_func!(model.inner, f) update_model!(model.inner) return end -""" - loadcbsolution!(m::Optimizer, cb_data::GurobiCallbackData, cb_where::Int) - -Load the variable primal solution in a callback. +struct CallbackVariablePrimal <: MOI.AbstractVariableAttribute end -This can only be called in a callback from `CB_MIPSOL`. After it is called, you -can access the `VariablePrimal` attribute as usual. -""" -function loadcbsolution!(model::Optimizer, cb_data::CallbackData, cb_where::Cint) +function load_callback_variable_primal(model, cb_data, cb_where) if cb_where != CB_MIPSOL - error("loadcbsolution! can only be called from CB_MIPSOL.") + error("`load_callback_variable_primal` must be called from `CB_MIPSOL`.") end - Gurobi.cbget_mipsol_sol(cb_data, cb_where, model.variable_primal_solution) + resize!(model.callback_variable_primal, length(model.variable_info)) + cbget_mipsol_sol(cb_data, cb_where, model.callback_variable_primal) return end +# Note: you must call load_callback_variable_primal first. +function MOI.get( + model::Optimizer, ::CallbackVariablePrimal, x::MOI.VariableIndex +) + return model.callback_variable_primal[_info(model, x).column] +end + """ - cblazy!(cb_data::Gurobi.CallbackData, m::Optimizer, func::LQOI.Linear, set::S) where S <: Union{LQOI.LE, LQOI.GE, LQOI.EQ} + function cblazy!( + cb_data::CallbackData, model::Optimizer, + f::MOI.ScalarAffineFunction{Float64}, + s::Union{MOI.LessThan{Float64}, MOI.GreaterThan{Float64}, MOI.EqualTo{Float64}} + ) Add a lazy cut to the model `m`. You must have the option `LazyConstraints` set via `Optimizer(LazyConstraint=1)`. This can only be called in a callback from `CB_MIPSOL`. """ -function cblazy!(cb_data::CallbackData, model::Optimizer, - func::LQOI.Linear, set::S) where S <: Union{LQOI.LE, LQOI.GE, LQOI.EQ} - columns = [ - Cint(LQOI.get_column(model, term.variable_index)) for term in func.terms] - coefficients = [term.coefficient for term in func.terms] - sense = Char(LQOI.backend_type(model, set)) - rhs = MOI.Utilities.getconstant(set) - return cblazy(cb_data, columns, coefficients, sense, rhs) -end - -# The default implementation in LQOI is too slow for Gurobi since it has a -# lookup of the variable bounds (calling _update_if_required), but it also sets -# the variable bounds and the VType (calling _require_update). Thus, if you add -# multiple ZeroOne constraints in sequence, you will call update every time! -function MOI.add_constraint( - model::Optimizer, variable::MOI.SingleVariable, set::MOI.ZeroOne) - variable_type = model.variable_type[variable.variable] - if variable_type != LQOI.CONTINUOUS - error("Cannot make variable binary because it is $(variable_type).") - end - model.variable_type[variable.variable] = LQOI.BINARY - model.last_constraint_reference += 1 - index = MOI.ConstraintIndex{MOI.SingleVariable, MOI.ZeroOne}( - model.last_constraint_reference) - dict = LQOI.constrdict(model, index) - dict[index] = (variable.variable, -Inf, Inf) - column = LQOI.get_column(model, variable) - set_charattrelement!(model.inner, "VType", column, Char('B')) - _require_update(model) - return index -end - -function MOI.delete(model::Optimizer, - index::MOI.ConstraintIndex{MOI.SingleVariable, MOI.ZeroOne}) - LQOI.__assert_valid__(model, index) - LQOI.delete_constraint_name(model, index) - dict = LQOI.constrdict(model, index) - (variable, lower, upper) = dict[index] - model.variable_type[variable] = LQOI.CONTINUOUS - column = LQOI.get_column(model, variable) - set_charattrelement!(model.inner, "VType", column, Char('C')) - _require_update(model) - delete!(dict, index) - return +function cblazy!( + cb_data::CallbackData, model::Optimizer, + f::MOI.ScalarAffineFunction{Float64}, + s::Union{MOI.LessThan{Float64}, MOI.GreaterThan{Float64}, MOI.EqualTo{Float64}} +) + indices, coefficients = _indices_and_coefficients(model, f) + sense, rhs = _sense_and_rhs(s) + return cblazy(cb_data, Cint.(indices), coefficients, Char(sense), rhs) end """ @@ -759,12 +2373,14 @@ end """ ConflictStatus() -Return an `MOI.TerminationStatusCode` indicating the status of the last computed conflict. -If a minimal conflict is found, it will return `MOI.OPTIMAL`. If the problem is feasible, it will -return `MOI.INFEASIBLE`. If `compute_conflict` has not been called yet, it will return +Return an `MOI.TerminationStatusCode` indicating the status of the last +computed conflict. If a minimal conflict is found, it will return +`MOI.OPTIMAL`. If the problem is feasible, it will return `MOI.INFEASIBLE`. If +`compute_conflict` has not been called yet, it will return `MOI.OPTIMIZE_NOT_CALLED`. """ -struct ConflictStatus <: MOI.AbstractModelAttribute end +struct ConflictStatus <: MOI.AbstractModelAttribute end + MOI.is_set_by_optimize(::ConflictStatus) = true function MOI.get(model::Optimizer, ::ConflictStatus) @@ -809,45 +2425,103 @@ end """ ConstraintConflictStatus() -A Boolean constraint attribute indicating whether the constraint participates in the last computed conflict. + +A Boolean constraint attribute indicating whether the constraint participates +in the last computed conflict. """ struct ConstraintConflictStatus <: MOI.AbstractConstraintAttribute end + MOI.is_set_by_optimize(::ConstraintConflictStatus) = true -function MOI.get(model::Optimizer, ::ConstraintConflictStatus, index::MOI.ConstraintIndex{<:MOI.SingleVariable, <:LQOI.LE}) +function MOI.get( + model::Optimizer, ::ConstraintConflictStatus, + index::MOI.ConstraintIndex{MOI.SingleVariable, <:MOI.LessThan} +) _ensure_conflict_computed(model) - return !_is_feasible(model) && Bool(get_intattrelement(model.inner, "IISUB", LQOI.get_column(model, model[index]))) + if _is_feasible(model) + return false + end + return get_intattrelement(model.inner, "IISUB", _info(model, index).column) > 0 end -function MOI.get(model::Optimizer, ::ConstraintConflictStatus, index::MOI.ConstraintIndex{<:MOI.SingleVariable, <:LQOI.GE}) +function MOI.get( + model::Optimizer, ::ConstraintConflictStatus, + index::MOI.ConstraintIndex{MOI.SingleVariable, <:MOI.GreaterThan} +) _ensure_conflict_computed(model) - return !_is_feasible(model) && Bool(get_intattrelement(model.inner, "IISLB", LQOI.get_column(model, model[index]))) + if _is_feasible(model) + return false + end + return get_intattrelement(model.inner, "IISLB", _info(model, index).column) > 0 end -function MOI.get(model::Optimizer, ::ConstraintConflictStatus, index::MOI.ConstraintIndex{<:MOI.SingleVariable, <:Union{LQOI.EQ, LQOI.IV}}) +function MOI.get( + model::Optimizer, ::ConstraintConflictStatus, + index::MOI.ConstraintIndex{ + MOI.SingleVariable, <:Union{MOI.EqualTo, MOI.Interval} + } +) _ensure_conflict_computed(model) - return !_is_feasible(model) && ( - Bool(get_intattrelement(model.inner, "IISUB", LQOI.get_column(model, model[index]))) || Bool(get_intattrelement(model.inner, "IISLB", model[index]))) + if _is_feasible(model) + return false + end + if get_intattrelement(model.inner, "IISLB", _info(model, index).column) > 0 + return true + end + return get_intattrelement(model.inner, "IISUB", _info(model, index).column) > 0 end -function MOI.get(model::Optimizer, ::ConstraintConflictStatus, index::MOI.ConstraintIndex{<:MOI.ScalarAffineFunction, <:Union{LQOI.LE, LQOI.GE, LQOI.EQ}}) +function MOI.get( + model::Optimizer, ::ConstraintConflictStatus, + index::MOI.ConstraintIndex{ + MOI.ScalarAffineFunction{Float64}, + <:Union{MOI.LessThan, MOI.GreaterThan, MOI.EqualTo} + } +) _ensure_conflict_computed(model) - return !_is_feasible(model) && Bool(get_intattrelement(model.inner, "IISConstr", model[index])) + if _is_feasible(model) + return false + end + return get_intattrelement(model.inner, "IISConstr", _info(model, index).row) > 0 end -function MOI.get(model::Optimizer, ::ConstraintConflictStatus, index::MOI.ConstraintIndex{<:MOI.ScalarQuadraticFunction, <:Union{LQOI.LE, LQOI.GE}}) +function MOI.get( + model::Optimizer, ::ConstraintConflictStatus, + index::MOI.ConstraintIndex{ + MOI.ScalarQuadraticFunction{Float64}, + <:Union{MOI.LessThan, MOI.GreaterThan} + } +) _ensure_conflict_computed(model) - return !_is_feasible(model) && Bool(get_intattrelement(model.inner, "IISQConstr", model[index])) + if _is_feasible(model) + return false + end + return get_intattrelement(model.inner, "IISQConstr", _info(model, index).row) > 0 end -function MOI.supports(::Optimizer, ::ConstraintConflictStatus, ::Type{MOI.ConstraintIndex{<:MOI.SingleVariable, <:LQOI.LinSets}}) +function MOI.supports( + ::Optimizer, ::ConstraintConflictStatus, + ::Type{<:MOI.ConstraintIndex{MOI.SingleVariable, <:SCALAR_SETS}} +) return true end -function MOI.supports(::Optimizer, ::ConstraintConflictStatus, ::Type{MOI.ConstraintIndex{<:MOI.ScalarAffineFunction, <:Union{LQOI.LE, LQOI.GE, LQOI.EQ}}}) +function MOI.supports( + ::Optimizer, ::ConstraintConflictStatus, + ::Type{<:MOI.ConstraintIndex{ + MOI.ScalarAffineFunction{Float64}, + <:Union{MOI.LessThan, MOI.GreaterThan, MOI.EqualTo} + }} +) return true end -function MOI.supports(::Optimizer, ::ConstraintConflictStatus, ::Type{MOI.ConstraintIndex{<:MOI.ScalarQuadraticFunction, <:Union{LQOI.LE, LQOI.GE}}}) +function MOI.supports( + ::Optimizer, ::ConstraintConflictStatus, + ::Type{<:MOI.ConstraintIndex{ + MOI.ScalarQuadraticFunction{Float64}, + <:Union{MOI.LessThan, MOI.GreaterThan} + }} +) return true end diff --git a/src/grb_attrs.jl b/src/grb_attrs.jl index 2a2a5911..550b1e32 100644 --- a/src/grb_attrs.jl +++ b/src/grb_attrs.jl @@ -83,6 +83,19 @@ function get_charattrelement(model::Gurobi.Model, name::String, element::Int) Char(a[]) end +function get_strattrelement(model::Gurobi.Model, name::String, element::Int) + @assert isascii(name) + a = Ref{Ptr{UInt8}}() + ret = @grb_ccall(getstrattrelement, Cint, + (Ptr{Cvoid}, Ptr{UInt8}, Cint, Ref{Ptr{UInt8}}), + model, name, element - 1, a + ) + if ret != 0 + throw(GurobiError(model.env, ret)) + end + return unsafe_string(a[]) +end + # Note: in attrarray API, the start argument is one-based (following Julia convention) function get_intattrarray!(r::Array{Cint}, model::Model, name::String, start::Integer) @@ -236,6 +249,17 @@ function set_strattr!(model::Model, name::String, v::String) nothing end +function set_strattrelement!(model::Model, name::String, el::Int, v::String) + @assert isascii(name) + @assert isascii(v) + ret = @grb_ccall(setstrattrelement, Cint, + (Ptr{Cvoid}, Ptr{UInt8}, Cint, Ptr{UInt8}), model, name, el - 1, v) + if ret != 0 + throw(GurobiError(model.env, ret)) + end + return +end + # array element function set_intattrelement!(model::Gurobi.Model, name::String, element::Int, v::Integer) diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index bb77919b..8d777d28 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -1,109 +1,124 @@ const MOI = Gurobi.MOI const MOIT = MOI.Test -const MOIB = MOI.Bridges const GUROBI_ENV = Gurobi.Env() +const OPTIMIZER = MOI.Bridges.full_bridge_optimizer( + Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0), Float64 +) + +const CONFIG = MOIT.TestConfig() @testset "Unit Tests" begin - config = MOIT.TestConfig() - solver = Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0) - MOIT.basic_constraint_tests(solver, config) - MOIT.unittest(solver, config, - ["solve_affine_interval", "solve_qcp_edge_cases"]) - @testset "solve_affine_interval" begin - MOIT.solve_affine_interval( - MOIB.SplitInterval{Float64}(Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0)), - config - ) - end - @testset "solve_qcp_edge_cases" begin - MOIT.solve_qcp_edge_cases(solver, - MOIT.TestConfig(atol=1e-3) - ) - end - MOIT.modificationtest(solver, config, [ - "solve_func_scalaraffine_lessthan" - ]) + MOIT.basic_constraint_tests(OPTIMIZER, CONFIG) + MOIT.unittest(OPTIMIZER, MOIT.TestConfig(atol=1e-6)) + MOIT.modificationtest(OPTIMIZER, CONFIG) end @testset "Linear tests" begin @testset "Default Solver" begin - solver = Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0) - MOIT.contlineartest(solver, MOIT.TestConfig(), [ - # This requires interval constraint. - "linear10", "linear10b", + MOIT.contlineartest(OPTIMIZER, MOIT.TestConfig(basis = true), [ # This requires an infeasiblity certificate for a variable bound. "linear12" ]) end - @testset "linear10" begin - MOIT.linear10test( - MOIB.SplitInterval{Float64}(Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0)), - MOIT.TestConfig() - ) - MOIT.linear10btest( - MOIB.SplitInterval{Float64}(Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0)), - MOIT.TestConfig() - ) - end @testset "No certificate" begin - MOIT.linear12test( - Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0, InfUnbdInfo=0), - MOIT.TestConfig(infeas_certificates=false) - ) + MOIT.linear12test(OPTIMIZER, MOIT.TestConfig(infeas_certificates=false)) end end @testset "Quadratic tests" begin - MOIT.contquadratictest( - Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0), - MOIT.TestConfig(atol=1e-3, rtol=1e-3, duals=false, query=false) - ) + MOIT.contquadratictest(OPTIMIZER, MOIT.TestConfig(atol=1e-3, rtol=1e-3), [ + "ncqcp" # Gurobi doesn't support non-convex problems. + ]) end @testset "Linear Conic tests" begin - MOIT.lintest( - Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0), - MOIT.TestConfig() - ) + MOIT.lintest(OPTIMIZER, CONFIG) end @testset "Integer Linear tests" begin - MOIT.intlineartest( - Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0), - MOIT.TestConfig(), - ["int3"] # int3 has interval constriants - ) - @testset "int3" begin - MOIT.int3test( - MOIB.SplitInterval{Float64}(Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0)), - MOIT.TestConfig() - ) - end + MOIT.intlineartest(OPTIMIZER, CONFIG, [ + # Indicator sets not supported. + "indicator1", "indicator2", "indicator3" + ]) end + @testset "ModelLike tests" begin - solver = Gurobi.Optimizer(GUROBI_ENV) - @test MOI.get(solver, MOI.SolverName()) == "Gurobi" + + @test MOI.get(OPTIMIZER, MOI.SolverName()) == "Gurobi" + @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, Gurobi.Optimizer(GUROBI_ENV)) + MOIT.copytest( + OPTIMIZER, + MOI.Bridges.full_bridge_optimizer(Gurobi.Optimizer(GUROBI_ENV), Float64) + ) + end + + @testset "scalar_function_constant_not_zero" begin + MOIT.scalar_function_constant_not_zero(OPTIMIZER) + end + + @testset "start_values_test" begin + model = Gurobi.Optimizer(GUROBI_ENV, OutputFlag = 0) + x = MOI.add_variables(model, 2) + MOI.set(model, MOI.VariablePrimalStart(), x[1], 1.0) + MOI.set(model, MOI.VariablePrimalStart(), x[2], nothing) + @test MOI.get(model, MOI.VariablePrimalStart(), x[1]) == 1.0 + @test MOI.get(model, MOI.VariablePrimalStart(), x[2]) === nothing + MOI.optimize!(model) + @test MOI.get(model, MOI.ObjectiveValue()) == 0.0 + # We don't support ConstraintDualStart or ConstraintPrimalStart yet. + # @test_broken MOIT.start_values_test(Gurobi.Optimizer(GUROBI_ENV), OPTIMIZER) + end + + @testset "supports_constrainttest" begin + # supports_constrainttest needs VectorOfVariables-in-Zeros, + # MOIT.supports_constrainttest(Gurobi.Optimizer(GUROBI_ENV), 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 @@ -156,9 +171,9 @@ end function callback_function(cb_data::Gurobi.CallbackData, cb_where::Int32) push!(cb_calls, cb_where) if cb_where == Gurobi.CB_MIPSOL - Gurobi.loadcbsolution!(m, cb_data, cb_where) - x_val = MOI.get(m, MOI.VariablePrimal(), x) - y_val = MOI.get(m, MOI.VariablePrimal(), y) + Gurobi.load_callback_variable_primal(m, cb_data, cb_where) + x_val = MOI.get(m, Gurobi.CallbackVariablePrimal(), x) + y_val = MOI.get(m, Gurobi.CallbackVariablePrimal(), 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) @@ -228,11 +243,7 @@ end # Given a collection of items with individual weights and values, # maximize the total value carried subject to the constraint that # the total weight carried is less than 10. - if VERSION >= v"0.7-" - Random.seed!(1) - else - srand(1) - end + Random.seed!(1) item_weights = rand(N) item_values = rand(N) MOI.add_constraint(m, @@ -303,76 +314,57 @@ end @testset "Conflict refiner" begin @testset "Variable bounds (SingleVariable and LessThan/GreaterThan)" begin - model = Gurobi.Optimizer() + model = Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0) x = MOI.add_variable(model) c1 = MOI.add_constraint(model, MOI.SingleVariable(x), MOI.GreaterThan(2.0)) c2 = MOI.add_constraint(model, MOI.SingleVariable(x), MOI.LessThan(1.0)) - # Getting the results before the conflict refiner has been called must return an error. + # Getting the results before the conflict refiner has been called must return an error. @test MOI.get(model, Gurobi.ConflictStatus()) == MOI.OPTIMIZE_NOT_CALLED @test_throws ErrorException MOI.get(model, Gurobi.ConstraintConflictStatus(), c1) - # Once it's called, no problem. + # Once it's called, no problem. Gurobi.compute_conflict(model) - println(model.inner.conflict) @test MOI.get(model, Gurobi.ConflictStatus()) == MOI.OPTIMAL @test MOI.get(model, Gurobi.ConstraintConflictStatus(), c1) == true @test MOI.get(model, Gurobi.ConstraintConflictStatus(), c2) == true end - + @testset "Variable bounds (ScalarAffine)" begin - model = Gurobi.Optimizer() + model = Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0) x = MOI.add_variable(model) c1 = MOI.add_constraint(model, MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0], [x]), 0.0), MOI.GreaterThan(2.0)) c2 = MOI.add_constraint(model, MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0], [x]), 0.0), MOI.LessThan(1.0)) - # Getting the results before the conflict refiner has been called must return an error. + # Getting the results before the conflict refiner has been called must return an error. @test MOI.get(model, Gurobi.ConflictStatus()) == MOI.OPTIMIZE_NOT_CALLED @test_throws ErrorException MOI.get(model, Gurobi.ConstraintConflictStatus(), c1) - # Once it's called, no problem. + # Once it's called, no problem. Gurobi.compute_conflict(model) @test MOI.get(model, Gurobi.ConflictStatus()) == MOI.OPTIMAL @test MOI.get(model, Gurobi.ConstraintConflictStatus(), c1) == true @test MOI.get(model, Gurobi.ConstraintConflictStatus(), c2) == true end - @testset "Variable fixing (SingleVariable and EqualTo)" begin - model = Gurobi.Optimizer() + @testset "Variable bounds (Invali Interval)" begin + model = Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0) x = MOI.add_variable(model) - c1 = MOI.add_constraint(model, MOI.SingleVariable(x), MOI.EqualTo(1.0)) - c2 = MOI.add_constraint(model, MOI.SingleVariable(x), MOI.GreaterThan(2.0)) - - # Getting the results before the conflict refiner has been called must return an error. - @test MOI.get(model, Gurobi.ConflictStatus()) == MOI.OPTIMIZE_NOT_CALLED - @test_throws ErrorException MOI.get(model, Gurobi.ConstraintConflictStatus(), c1) - - # Once it's called, no problem. - Gurobi.compute_conflict(model) - @test MOI.get(model, Gurobi.ConflictStatus()) == MOI.OPTIMAL - @test MOI.get(model, Gurobi.ConstraintConflictStatus(), c1) == true - @test MOI.get(model, Gurobi.ConstraintConflictStatus(), c2) == true - end - - @testset "Variable bounds (SingleVariable and Interval)" begin - model = Gurobi.Optimizer() - x = MOI.add_variable(model) - c1 = MOI.add_constraint(model, MOI.SingleVariable(x), MOI.Interval(1.0, 3.0)) - c2 = MOI.add_constraint(model, MOI.SingleVariable(x), MOI.LessThan(0.0)) - - # Getting the results before the conflict refiner has been called must return an error. + c1 = MOI.add_constraint( + model, MOI.SingleVariable(x), MOI.Interval(1.0, 0.0) + ) + # Getting the results before the conflict refiner has been called must return an error. @test MOI.get(model, Gurobi.ConflictStatus()) == MOI.OPTIMIZE_NOT_CALLED @test_throws ErrorException MOI.get(model, Gurobi.ConstraintConflictStatus(), c1) - # Once it's called, no problem. + # Once it's called, no problem. Gurobi.compute_conflict(model) @test MOI.get(model, Gurobi.ConflictStatus()) == MOI.OPTIMAL @test MOI.get(model, Gurobi.ConstraintConflictStatus(), c1) == true - @test MOI.get(model, Gurobi.ConstraintConflictStatus(), c2) == true end @testset "Two conflicting constraints (GreaterThan, LessThan)" begin - model = Gurobi.Optimizer() + model = Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0) x = MOI.add_variable(model) y = MOI.add_variable(model) b1 = MOI.add_constraint(model, MOI.SingleVariable(x), MOI.GreaterThan(0.0)) @@ -382,11 +374,11 @@ end cf2 = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0, -1.0], [x, y]), 0.0) c2 = MOI.add_constraint(model, cf2, MOI.GreaterThan(1.0)) - # Getting the results before the conflict refiner has been called must return an error. + # Getting the results before the conflict refiner has been called must return an error. @test MOI.get(model, Gurobi.ConflictStatus()) == MOI.OPTIMIZE_NOT_CALLED @test_throws ErrorException MOI.get(model, Gurobi.ConstraintConflictStatus(), c1) - # Once it's called, no problem. + # Once it's called, no problem. Gurobi.compute_conflict(model) @test MOI.get(model, Gurobi.ConflictStatus()) == MOI.OPTIMAL @test MOI.get(model, Gurobi.ConstraintConflictStatus(), b1) == true @@ -396,7 +388,7 @@ end end @testset "Two conflicting constraints (EqualTo)" begin - model = Gurobi.Optimizer() + model = Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0) x = MOI.add_variable(model) y = MOI.add_variable(model) b1 = MOI.add_constraint(model, MOI.SingleVariable(x), MOI.GreaterThan(0.0)) @@ -406,11 +398,11 @@ end cf2 = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0, -1.0], [x, y]), 0.0) c2 = MOI.add_constraint(model, cf2, MOI.GreaterThan(1.0)) - # Getting the results before the conflict refiner has been called must return an error. + # Getting the results before the conflict refiner has been called must return an error. @test MOI.get(model, Gurobi.ConflictStatus()) == MOI.OPTIMIZE_NOT_CALLED @test_throws ErrorException MOI.get(model, Gurobi.ConstraintConflictStatus(), c1) - # Once it's called, no problem. + # Once it's called, no problem. Gurobi.compute_conflict(model) @test MOI.get(model, Gurobi.ConflictStatus()) == MOI.OPTIMAL @test MOI.get(model, Gurobi.ConstraintConflictStatus(), b1) == true @@ -420,7 +412,7 @@ end end @testset "Variables outside conflict" begin - model = Gurobi.Optimizer() + model = Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0) x = MOI.add_variable(model) y = MOI.add_variable(model) z = MOI.add_variable(model) @@ -432,11 +424,11 @@ end cf2 = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0, -1.0, 1.0], [x, y, z]), 0.0) c2 = MOI.add_constraint(model, cf2, MOI.GreaterThan(1.0)) - # Getting the results before the conflict refiner has been called must return an error. + # Getting the results before the conflict refiner has been called must return an error. @test MOI.get(model, Gurobi.ConflictStatus()) == MOI.OPTIMIZE_NOT_CALLED @test_throws ErrorException MOI.get(model, Gurobi.ConstraintConflictStatus(), c1) - # Once it's called, no problem. + # Once it's called, no problem. Gurobi.compute_conflict(model) @test MOI.get(model, Gurobi.ConflictStatus()) == MOI.OPTIMAL @test MOI.get(model, Gurobi.ConstraintConflictStatus(), b1) == true @@ -447,19 +439,139 @@ end end @testset "No conflict" begin - model = Gurobi.Optimizer() + model = Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0) x = MOI.add_variable(model) c1 = MOI.add_constraint(model, MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0], [x]), 0.0), MOI.GreaterThan(1.0)) c2 = MOI.add_constraint(model, MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0], [x]), 0.0), MOI.LessThan(2.0)) - # Getting the results before the conflict refiner has been called must return an error. + # Getting the results before the conflict refiner has been called must return an error. @test MOI.get(model, Gurobi.ConflictStatus()) == MOI.OPTIMIZE_NOT_CALLED @test_throws ErrorException MOI.get(model, Gurobi.ConstraintConflictStatus(), c1) - # Once it's called, no problem. + # Once it's called, no problem. Gurobi.compute_conflict(model) @test MOI.get(model, Gurobi.ConflictStatus()) == MOI.INFEASIBLE @test MOI.get(model, Gurobi.ConstraintConflictStatus(), c1) == false @test MOI.get(model, Gurobi.ConstraintConflictStatus(), c2) == false end end + +@testset "RawParameter" begin + model = Gurobi.Optimizer(GUROBI_ENV) + @test MOI.get(model, MOI.RawParameter("OutputFlag")) == 1 + MOI.set(model, MOI.RawParameter("OutputFlag"), 0) + @test MOI.get(model, MOI.RawParameter("OutputFlag")) == 0 +end + +@testset "QCPDuals without needing to pass QCPDual=1" begin + @testset "QCPDual default" begin + model = Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0) + MOI.Utilities.loadfromstring!(model, """ + variables: x, y, z + minobjective: 1.0 * x + 1.0 * y + 1.0 * z + c1: x + y == 2.0 + c2: x + y + z >= 0.0 + c3: 1.0 * x * x + -1.0 * y * y + -1.0 * z * z >= 0.0 + c4: x >= 0.0 + c5: y >= 0.0 + c6: z >= 0.0 + """) + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL + @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + @test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT + c1 = MOI.get(model, MOI.ConstraintIndex, "c1") + c2 = MOI.get(model, MOI.ConstraintIndex, "c2") + c3 = MOI.get(model, MOI.ConstraintIndex, "c3") + @test MOI.get(model, MOI.ConstraintDual(), c1) ≈ 1.0 atol=1e-6 + @test MOI.get(model, MOI.ConstraintDual(), c2) ≈ 0.0 atol=1e-6 + @test MOI.get(model, MOI.ConstraintDual(), c3) ≈ 0.0 atol=1e-6 + end + @testset "QCPDual=0" begin + model = Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0, QCPDual=0) + MOI.Utilities.loadfromstring!(model, """ + variables: x, y, z + minobjective: 1.0 * x + 1.0 * y + 1.0 * z + c1: x + y == 2.0 + c2: x + y + z >= 0.0 + c3: 1.0 * x * x + -1.0 * y * y + -1.0 * z * z >= 0.0 + c4: x >= 0.0 + c5: y >= 0.0 + c6: z >= 0.0 + """) + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMAL + @test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + @test MOI.get(model, MOI.DualStatus()) == MOI.NO_SOLUTION + c1 = MOI.get(model, MOI.ConstraintIndex, "c1") + c2 = MOI.get(model, MOI.ConstraintIndex, "c2") + c3 = MOI.get(model, MOI.ConstraintIndex, "c3") + @test_throws Gurobi.GurobiError MOI.get(model, MOI.ConstraintDual(), c1) + @test_throws Gurobi.GurobiError MOI.get(model, MOI.ConstraintDual(), c2) + @test_throws Gurobi.GurobiError MOI.get(model, MOI.ConstraintDual(), c3) + end +end + +@testset "Add constraints" begin + model = Gurobi.Optimizer(GUROBI_ENV) + x = MOI.add_variables(model, 2) + MOI.add_constraints( + model, + [MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, x[i])], 0.0) for i in 1:2], + MOI.EqualTo.([0.0, 0.0]) + ) + @test MOI.get(model, MOI.NumberOfConstraints{ + MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64} + }()) == 2 +end + +@testset "Extra name tests" begin + model = Gurobi.Optimizer(GUROBI_ENV) + @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) + 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 "Affine constraints" begin + MOI.empty!(model) + x = MOI.add_variable(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 +