diff --git a/Manifest.toml b/Manifest.toml new file mode 100644 index 00000000..75fc14cb --- /dev/null +++ b/Manifest.toml @@ -0,0 +1,130 @@ +# This file is machine-generated - editing it directly is not advised + +[[Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" + +[[BenchmarkTools]] +deps = ["JSON", "Printf", "Statistics"] +git-tree-sha1 = "90b73db83791c5f83155016dd1cc1f684d4e1361" +uuid = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +version = "0.4.3" + +[[Compat]] +deps = ["Base64", "Dates", "DelimitedFiles", "Distributed", "InteractiveUtils", "LibGit2", "Libdl", "LinearAlgebra", "Markdown", "Mmap", "Pkg", "Printf", "REPL", "Random", "Serialization", "SharedArrays", "Sockets", "SparseArrays", "Statistics", "Test", "UUIDs", "Unicode"] +git-tree-sha1 = "84aa74986c5b9b898b0d1acaf3258741ee64754f" +uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" +version = "2.1.0" + +[[Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[DelimitedFiles]] +deps = ["Mmap"] +uuid = "8bb1440f-4735-579b-a4ab-409b98df4dab" + +[[Distributed]] +deps = ["Random", "Serialization", "Sockets"] +uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" + +[[InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" + +[[JSON]] +deps = ["Dates", "Mmap", "Parsers", "Unicode"] +git-tree-sha1 = "b34d7cef7b337321e97d22242c3c2b91f476748e" +uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +version = "0.21.0" + +[[LibGit2]] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" + +[[Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + +[[LinearAlgebra]] +deps = ["Libdl"] +uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + +[[Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" + +[[Markdown]] +deps = ["Base64"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" + +[[MathOptInterface]] +deps = ["BenchmarkTools", "LinearAlgebra", "OrderedCollections", "SparseArrays", "Test", "Unicode"] +git-tree-sha1 = "e94145d7746c13732a1ad1d1d334f8e9afab8e7c" +uuid = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +version = "0.9.4" + +[[MathProgBase]] +deps = ["Compat"] +git-tree-sha1 = "3bf2e534e635df810e5f4b4f1a8b6de9004a0d53" +uuid = "fdba3010-5040-5b88-9595-932c9decdf73" +version = "0.7.7" + +[[Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" + +[[OrderedCollections]] +deps = ["Random", "Serialization", "Test"] +git-tree-sha1 = "c4c13474d23c60d20a67b217f1d7f22a40edf8f1" +uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +version = "1.1.0" + +[[Parsers]] +deps = ["Dates", "Test"] +git-tree-sha1 = "ef0af6c8601db18c282d092ccbd2f01f3f0cd70b" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "0.3.7" + +[[Pkg]] +deps = ["Dates", "LibGit2", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" + +[[Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[REPL]] +deps = ["InteractiveUtils", "Markdown", "Sockets"] +uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" + +[[Random]] +deps = ["Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" + +[[Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[SharedArrays]] +deps = ["Distributed", "Mmap", "Random", "Serialization"] +uuid = "1a1011a3-84de-559e-8e89-a11a2f7dc383" + +[[Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" + +[[SparseArrays]] +deps = ["LinearAlgebra", "Random"] +uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" + +[[Statistics]] +deps = ["LinearAlgebra", "SparseArrays"] +uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" + +[[Test]] +deps = ["Distributed", "InteractiveUtils", "Logging", "Random"] +uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[[UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" + +[[Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" diff --git a/Project.toml b/Project.toml new file mode 100644 index 00000000..5dc21cb0 --- /dev/null +++ b/Project.toml @@ -0,0 +1,25 @@ +name = "Xpress" +uuid = "9e70acf3-d6c9-5be6-b5bd-4e2c73e3e054" +repo = "https://github.com/JuliaOpt/Xpress.jl.git" +version = "0.10.0" + +[deps] +Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" +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] +MathOptInterface = "~0.9.1" +MathProgBase = "~0.5.0, ~0.6, ~0.7" +julia = "1" + +[extras] +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Pkg", "Random", "Test"] diff --git a/REQUIRE b/REQUIRE deleted file mode 100644 index 17935d54..00000000 --- a/REQUIRE +++ /dev/null @@ -1,4 +0,0 @@ -julia 0.6 -MathProgBase 0.6 0.8 -LinQuadOptInterface 0.6 0.7 -Compat 0.59 \ No newline at end of file diff --git a/deps/build.jl b/deps/build.jl index 0e07aa31..4b037dd7 100755 --- a/deps/build.jl +++ b/deps/build.jl @@ -1,5 +1,4 @@ -using Compat -using Compat.Libdl +using Libdl depsfile = joinpath(dirname(@__FILE__),"deps.jl") @@ -9,20 +8,20 @@ end function write_depsfile(path) f = open(depsfile,"w") - if Compat.Sys.iswindows() + if Sys.iswindows() path = replace(path, "\\" => "\\\\") end println(f,"const xprs = \"$(path)\"") close(f) end -libname = string(Compat.Sys.iswindows() ? "" : "lib", "xprs", ".", Libdl.dlext) +libname = string(Sys.iswindows() ? "" : "lib", "xprs", ".", Libdl.dlext) paths_to_try = String[] push!(paths_to_try, libname) if haskey(ENV, "XPRESSDIR") - push!(paths_to_try, joinpath(ENV["XPRESSDIR"], Compat.Sys.iswindows() ? "bin" : "lib", libname)) + push!(paths_to_try, joinpath(ENV["XPRESSDIR"], Sys.iswindows() ? "bin" : "lib", libname)) end global found_xprs = false @@ -30,7 +29,7 @@ for l in paths_to_try d = Libdl.dlopen_e(l) if d != C_NULL global found_xprs = true - Compat.@info("Found $l") + @info("Found $l") write_depsfile(l) break end diff --git a/src/MOIWrapper.jl b/src/MOIWrapper_old.jl similarity index 96% rename from src/MOIWrapper.jl rename to src/MOIWrapper_old.jl index d6cc8bfa..4327b24b 100644 --- a/src/MOIWrapper.jl +++ b/src/MOIWrapper_old.jl @@ -410,7 +410,7 @@ function xprsmoi_stopstatus(instance::Model) return MOI.INTERRUPTED elseif ss == XPR.StopNodeLimit # should not be here - Compat.@warn("should not be here") + @warn("should not be here") return MOI.NODE_LIMIT elseif ss == XPR.StopIterLimit return MOI.ITERATION_LIMIT diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl new file mode 100644 index 00000000..0e9ccab5 --- /dev/null +++ b/src/MOI_wrapper.jl @@ -0,0 +1,2793 @@ +# Follows the implementation of the Gurobi MOI Wrapper +# https://github.com/JuliaOpt/Gurobi.jl/blob/master/src/MOI_wrapper.jl + +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) + +const SCALAR_SETS = Union{ + MOI.GreaterThan{Float64}, MOI.LessThan{Float64}, + MOI.EqualTo{Float64}, MOI.Interval{Float64} +} + +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, "", "", "", "", nothing) + end +end + +mutable struct ConstraintInfo + row::Int + set::MOI.AbstractSet + # Storage for constraint names. Where possible, these are also stored in the + # Xpress model. + name::String + ConstraintInfo(row::Int, set) = new(row, set, "") +end + +mutable struct Optimizer <: MOI.AbstractOptimizer + # The low-level Xpress model. + inner::Model + # The Xpress 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 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). + #needs_update::Bool + + # 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 + + # Pending to check for Xpress. + # 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 Xpress 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} + + conflict::Union{Nothing, IISData} + """ + Optimizer(env = nothing; kwargs...) + + Create a new Optimizer object. + + You can share Expr `Env`s between models by passing an model of `Env` + as the first argument. By default, a new environment is created for every + model. + + 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 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}() + model.variable_info = CleverDicts.CleverDict{MOI.VariableIndex, VariableInfo}() + model.affine_constraint_info = Dict{Int, ConstraintInfo}() + model.quadratic_constraint_info = Dict{Int, ConstraintInfo}() + 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. + #= TODO: These parameters are specific to Gurobi, needs update relevant to Xpress + for (name, value) in kwargs + model.params[string(name)] = value + setparam!(model.inner, XPRS_CONTROLS_DICT[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 + +setparam!(model::Optimizer, name, val) = setparam!(model.inner, XPRS_CONTROLS_DICT[name], val) + +setlogfile!(model::Optimizer, path) = setlogfile(model.inner, path::String) + +cintvec(v::Vector) = convert(Vector{Int32}, v) + +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, XPRS_OUTPUTLOG, 0) + end + 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) + for (name,value) in model.params + setparam!(model.inner, XPRS_CONTROLS_DICT[name], value) + end + model.conflict = nothing + return +end + +function MOI.is_empty(model::Optimizer) + model.objective_type != SCALAR_AFFINE && return false + model.is_feasibility == false && return false + !isempty(model.variable_info) && return false + 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 + +MOI.get(::Optimizer, ::MOI.SolverName) = "Xpress" + +function MOI.supports( + ::Optimizer, + ::MOI.ObjectiveFunction{F} +) where {F <: Union{ + MOI.SingleVariable, + MOI.ScalarAffineFunction{Float64}, + MOI.ScalarQuadraticFunction{Float64} +}} + return true +end + +#= + (LQOI.SinVar, LQOI.EQ), + (LQOI.SinVar, LQOI.LE), + (LQOI.SinVar, LQOI.GE), + (LQOI.SinVar, LQOI.IV), + (LQOI.SinVar, MOI.ZeroOne), + (LQOI.SinVar, MOI.Integer), + # Not Supported (LQOI.SinVar, MOI.Semicontinuous{Float64}), + # Not Supported (LQOI.SinVar, MOI.Semiinteger{Float64}), +=# +function MOI.supports_constraint( + ::Optimizer, ::Type{MOI.SingleVariable}, ::Type{F} +) where {F <: Union{ + MOI.EqualTo{Float64}, MOI.LessThan{Float64}, MOI.GreaterThan{Float64}, + MOI.Interval{Float64}, MOI.ZeroOne, MOI.Integer +}} + return true +end + +#= +(LQOI.VecVar, LQOI.SOS1) +(LQOI.VecVar, LQOI.SOS2), +=# +function MOI.supports_constraint( + ::Optimizer, ::Type{MOI.VectorOfVariables}, ::Type{F} +) where {F <: Union{MOI.SOS1{Float64}, MOI.SOS2{Float64}, + #MOI.SecondOrderCone. Added Later + }} + return true +end + +#= + (LQOI.Linear, LQOI.EQ), + (LQOI.Linear, LQOI.LE), + (LQOI.Linear, LQOI.GE), + (LQOI.Linear, LQOI.IV), +=# +function MOI.supports_constraint( + ::Optimizer, ::Type{MOI.ScalarAffineFunction{Float64}}, ::Type{F} +) where {F <: Union{ + MOI.EqualTo{Float64}, MOI.LessThan{Float64}, MOI.GreaterThan{Float64}, MOI.Interval{Float64} +}} + return true +end + +#= + (LQOI.Quad, LQOI.EQ), + (LQOI.Quad, LQOI.LE), + (LQOI.Quad, LQOI.GE), +=# +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 + +# Check that this what Xpress is actually going to support +function MOI.supports( + ::Xpress.Optimizer, ::MOI.VariablePrimalStart, ::Type{MOI.VariableIndex}) + return true +end + +function MOI.set(model::Optimizer, param::MOI.RawParameter, value) + model.params[param.name] = value + setparam!(model.inner, Symbol(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 + 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} +) + + for (i, term) in enumerate(f.terms) + indices[i] = _info(model, term.variable_index).column + coefficients[i] = term.coefficient + 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 + # Xpress does this according to page 14 of the reference Manual. + # Xpress returns a list of terms. MOI requires 0.5 x' Q x. So, to get + # from + # Xpress -> MOI => multiply diagonals by 2.0 + # MOI -> Xpress => 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| + # Xpress 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 + for (i, term) in enumerate(f.affine_terms) + indices[i] = _info(model, term.variable_index).column + coefficients[i] = term.coefficient + end + 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}) = (XPRS_LEQ, s.upper) +_sense_and_rhs(s::MOI.GreaterThan{Float64}) = (XPRS_GEQ, s.lower) +_sense_and_rhs(s::MOI.EqualTo{Float64}) = (XPRS_EQ, 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) + + return index +end + +function MOI.add_variables(model::Optimizer, N::Int) + add_cvars!(model.inner, zeros(N)) + + 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 MOI.is_valid(model::Optimizer, v::MOI.VariableIndex) + return haskey(model.variable_info, v) +end + +function MOI.delete(model::Optimizer, v::MOI.VariableIndex) + + info = _info(model, v) + del_vars!(model.inner, Cint[info.column]) + + 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 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 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 + addcolname(model.inner, name) + + 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 + +### +### 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 + + return +end + +function MOI.get(model::Optimizer, ::MOI.ObjectiveSense) + 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 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 + set_obj!(model.inner, obj) + model.objective_type = SCALAR_AFFINE +end + +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 + + dest = zeros(length(model.variable_info)) + get_obj!(model.inner, dest) + + 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, XPRS_OBJRHS) + return MOI.ScalarAffineFunction(terms, constant) +end + +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) + # 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_obj!(model.inner, obj) + # We need to zero out the existing quadratic objective. + delq!(model.inner) + add_qpterms!(model.inner, I, J, V) + model.objective_type = SCALAR_QUADRATIC + return +end + +function MOI.get( + model::Optimizer, + ::MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}} +) + dest = zeros(length(model.variable_info)) + get_obj!(model.inner, dest) + 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 + q_terms = MOI.ScalarQuadraticTerm{Float64}[] + I, J, V = getq_upper(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 + constant = get_dblattr(model.inner, XPRS_OBJRHS) + return MOI.ScalarQuadraticFunction(terms, q_terms, constant) +end + +function MOI.modify( + model::Optimizer, + ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}, + chg::MOI.ScalarConstantChange{Float64} +) + set_objcoeffs!(model.inner, 0, chg.new_constant) + + 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 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_ub!(model.inner, [info.column], [Inf]) + + if info.bound == LESS_AND_GREATER_THAN + info.bound = GREATER_THAN + else + info.bound = NONE + end + info.lessthan_name = "" + return +end + +""" + _set_variable_lower_bound(model, info, value) + +This function is used to indirectly set the lower bound of a variable. + +We need to do it this way to account for potential lower bounds of 0.0 added by +VectorOfVariables-in-SecondOrderCone constraints. + +See also `_get_variable_lower_bound`. +""" +function _set_variable_lower_bound(model, info, value) + if info.num_soc_constraints == 0 + # No SOC constraints, set directly. + @assert isnan(info.lower_bound_if_soc) + set_lb!(model.inner, [info.column], [value]) + elseif value >= 0.0 + # Regardless of whether there are SOC constraints, this is a valid bound + # for the SOC constraint and should over-ride any previous bounds. + info.lower_bound_if_soc = NaN + set_lb!(model.inner, [info.column], [value]) + + elseif isnan(info.lower_bound_if_soc) + # Previously, we had a +ve lower bound (i.e., it was set in the case + # above). Now we're setting this with a -ve one, but there are still + # some SOC constraints, so we cache `value` and set the variable lower + # bound to `0.0`. + @assert value < 0.0 + set_lb!(model.inner, [info.column], [0.0]) + + info.lower_bound_if_soc = value + else + # Previously, we had a -ve lower bound. We're setting this with another + # -ve one, but there are still some SOC constraints. + @assert info.lower_bound_if_soc < 0.0 + info.lower_bound_if_soc = value + end +end + +""" + _get_variable_lower_bound(model, info) + +Get the current variable lower bound, ignoring a potential bound of `0.0` set +by a second order cone constraint. + +See also `_set_variable_lower_bound`. +""" +function _get_variable_lower_bound(model, info) + if !isnan(info.lower_bound_if_soc) + # There is a value stored. That means that we must have set a value that + # was < 0. + @assert info.lower_bound_if_soc < 0.0 + return info.lower_bound_if_soc + end + return get_lb!(model.inner, [info.column]) +end + +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.GreaterThan{Float64}} +) + MOI.throw_if_not_valid(model, c) + info = _info(model, c) + _set_variable_lower_bound(model, info, -Inf) + 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 MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.Interval{Float64}} +) + MOI.throw_if_not_valid(model, c) + info = _info(model, c) + _set_variable_lower_bound(model, info, -Inf) + set_ub!(model.inner, [info.column], [Inf]) + info.bound = NONE + info.greaterthan_interval_or_equalto_name = "" + return +end + +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.EqualTo{Float64}} +) + MOI.throw_if_not_valid(model, c) + info = _info(model, c) + _set_variable_lower_bound(model, info, -Inf) + set_ub!(model.inner, [info.column], [Inf]) + info.bound = NONE + info.greaterthan_interval_or_equalto_name = "" + return +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.GreaterThan{Float64}} +) + MOI.throw_if_not_valid(model, c) + lower = _get_variable_lower_bound(model, _info(model, c)) + return MOI.GreaterThan(lower) +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.LessThan{Float64}} +) + MOI.throw_if_not_valid(model, c) + upper = get_ub!(model.inner, [_info(model, c).column]) + return MOI.LessThan(upper) +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.EqualTo{Float64}} +) + MOI.throw_if_not_valid(model, c) + lower = get_lb!(model.inner, [_info(model, c).column]) + return MOI.EqualTo(lower) +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.Interval{Float64}} +) + MOI.throw_if_not_valid(model, c) + info = _info(model, c) + lower = _get_variable_lower_bound(model, _info(model, c)) + upper = get_ub!(model.inner, [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_lb!(model.inner, lower_columns, lower_values) + end + if length(upper_columns) > 0 + set_ub!(model.inner, upper_columns, upper_values) + end +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_variable_lower_bound(model, info, lower) + end + if upper !== nothing + set_ub!(model.inner, [info.column], [upper]) + end + return +end + +function MOI.add_constraint( + model::Optimizer, f::MOI.SingleVariable, ::MOI.ZeroOne +) + info = _info(model, f.variable) + chgcoltype!(model.inner, [info.column], XPRS_BINARY) + 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) + chgcoltype!(model.inner, [info.column], XPRS_CONTINUOUS) + info.type = CONTINUOUS + info.type_constraint_name = "" + return +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.ZeroOne} +) + MOI.throw_if_not_valid(model, c) + return MOI.ZeroOne() +end + +function MOI.add_constraint( + model::Optimizer, f::MOI.SingleVariable, ::MOI.Integer +) + info = _info(model, f.variable) + chgcoltype!(model.inner, [info.column], XPRS_INTEGER) + 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) + chgcoltype!(model.inner, [info.column], XPRS_CONTINUOUS) + info.type = CONTINUOUS + info.type_constraint_name = "" + return +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.Integer} +) + MOI.throw_if_not_valid(model, c) + return MOI.Integer() +end + +#= +Semicontinuous and Semiinteger not originally supported in Xpress. Can be added later + +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_variable_lower_bound(model, info, s.lower) + set_dblattrelement!(model.inner, "UB", info.column, s.upper) + 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) + add_cvars!(model.inner, info.column) + _set_variable_lower_bound(model, info, -Inf) + set_ub!(model.inner, [info.column], [Inf]) + info.type = CONTINUOUS + info.type_constraint_name = "" + return +end + +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) + lower = _get_variable_lower_bound(model, info) + 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_variable_lower_bound(model, info, s.lower) + set_dblattrelement!(model.inner, "UB", info.column, s.upper) + 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) + add_cvars!(model.inner, info.column) + _set_variable_lower_bound(model, info, -Inf) + set_ub!(model.inner, [info.column], [Inf]) + info.type = CONTINUOUS + info.type_constraint_name = "" + return +end + +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) + lower = _get_variable_lower_bound(model, info) + 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) + 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) + return indices +end + +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, <:Any} +) + row = _info(model, c).row + del_constrs!(model.inner, row) + 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} + row = _info(model, c).row + rhs = get_rhs(model.inner, row, row)[1] + return S(rhs) +end + +function MOI.set( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, S}, s::S +) where {S} + set_rhs!(model.inner, _info(model, c).row, MOI.constant(s)) + return +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintFunction, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, S} +) where {S} + row = _info(model, c).row + sparse_a = SparseArrays.sparse(get_rows(model.inner, row, row)') + 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) + addrowname(model.inner, name, info.row) + 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 + +# Implement Quadratic Later + +### +### 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) + 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} + info = _info(model, c) + delqconstrs!(model.inner, [info.row]) + 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 MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{Float64}, S} +) where {S} + rhs = get_dblattrelement(model.inner, "QCRHS", _info(model, c).row) + return S(rhs) +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintFunction, + c::MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{Float64}, S} +) where {S} + 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 + return MOI.ScalarQuadraticFunction(affine_terms, quadratic_terms, 0.0) +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintName, + c::MOI.ConstraintIndex{MOI.ScalarQuadraticFunction{Float64}, S} +) where {S} + return _info(model, c).name +end + +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 + set_strattrelement!(model.inner, "QCName", info.row, name) + 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[c] = name + end + return +end +=# +### +### VectorOfVariables-in-SOS{I|II} +### + +const SOS = Union{MOI.SOS1{Float64}, MOI.SOS2{Float64}} + +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 + +_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 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 + ) + return index +end + +function MOI.delete( + model::Optimizer, c::MOI.ConstraintIndex{MOI.VectorOfVariables, <:SOS} +) + row = _info(model, c).row + del_sos!(model.inner, [Cint(row)]) + 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 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 + return +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.VectorOfVariables, S} +) where {S <: SOS} + full_matrix, _ = get_sos_matrix(model.inner) + line = full_matrix[_info(model, c).row,:] #sparse vec + return S(line.nzval) +end + + +function MOI.get( + model::Optimizer, ::MOI.ConstraintFunction, + c::MOI.ConstraintIndex{MOI.VectorOfVariables, S} +) where {S <: SOS} + full_matrix, _ = get_sos_matrix(model.inner) + line = full_matrix[_info(model, c).row,:] #sparse vec + cols = line.nzind + return MOI.VectorOfVariables( + [model.variable_info[CleverDicts.LinearIndex(i)].index for i in cols] + ) +end + +### +### Optimize methods. +### + +function MOI.optimize!(model::Optimizer) + 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 +## +# Update these for Xpress Using LPSTATUS, https://www.fico.com/fico-xpress-optimization/docs/latest/solver/optimizer/HTML/LPSTATUS.html +# MIPSTATUS https://www.fico.com/fico-xpress-optimization/docs/latest/solver/optimizer/HTML/MIPSTATUS.html +# and STOP https://www.fico.com/fico-xpress-optimization/docs/latest/solver/optimizer/HTML/STOPSTATUS.html +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 = -1 #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 _get_stopstatus(model::Model) + ss = get_stopstatus(model) + if ss == StopTimeLimit + return MOI.TIME_LIMIT + elseif ss == StopControlC + return MOI.INTERRUPTED + elseif ss == StopNodeLimit + # should not be here + @warn("should not be here") + return MOI.NODE_LIMIT + elseif ss == StopIterLimit + return MOI.ITERATION_LIMIT + elseif ss == StopMIPGap + return MOI.OBJECTIVE_LIMIT + elseif ss == StopSolLimit + return MOI.SOLUTION_LIMIT + elseif ss == StopUser + return MOI.INTERRUPTED + end + return MOI.OTHER_ERROR +end + +function MOI.get(model::Optimizer, ::MOI.TerminationStatus) + # First determine the stop status. + stat_lp = get_lp_status2(model.inner) + if is_mip(model.inner) + stat_mip = get_mip_status2(model.inner) + if stat_mip == MIP_NotLoaded + return MOI.OTHER_ERROR + elseif stat_mip == MIP_LPNotOptimal + # MIP search incomplete but there is no linear sol + return MOI.OTHER_ERROR + elseif stat_mip == MIP_NoSolFound + # MIP search incomplete but there is no integer sol + other = _get_stopstatus(model.inner) + if other == MOI.OTHER_ERROR + return MOI.SLOW_PROGRESS#OtherLimit + else + return other + end + + elseif stat_mip == MIP_Solution + # MIP search incomplete but there is a solution + other = _get_stopstatus(model.inner) + if other == MOI.OTHER_ERROR + return MOI.OTHER_LIMIT + else + return other + end + + elseif stat_mip == MIP_Infeasible + return MOI.INFEASIBLE + elseif stat_mip == MIP_Optimal + return MOI.OPTIMAL + elseif stat_mip == MIP_Unbounded + return MOI.DUAL_INFEASIBLE + end + return MOI.OTHER_ERROR + else + if stat_lp == LP_Unstarted + return MOI.OTHER_ERROR + elseif stat_lp == LP_Optimal + return MOI.OPTIMAL + elseif stat_lp == LP_Infeasible + return MOI.INFEASIBLE + elseif stat_lp == LP_CutOff + return MOI.OBJECTIVE_LIMIT + elseif stat_lp == LP_Unfinished + return _get_stopstatus(model.inner) + elseif stat_lp == LP_Unbounded + return MOI.DUAL_INFEASIBLE + elseif stat_lp == LP_CutOffInDual + return MOI.OBJECTIVE_LIMIT + elseif stat_lp == LP_Unsolved + return MOI.OTHER_ERROR + elseif stat_lp == LP_NonConvex + return MOI.INVALID_MODEL + end + return MOI.OTHER_ERROR + end +end + +function MOI.get(model::Optimizer, ::MOI.PrimalStatus) + if is_mip(model.inner) + stat_mip = get_mip_status2(model.inner) + if stat_mip in [MIP_Solution, MIP_Optimal] + return MOI.FEASIBLE_POINT + elseif stat_mip == MIP_Unbounded && hasprimalray(model.inner) + return MOI.INFEASIBILITY_CERTIFICATE + elseif stat_mip in [MIP_LPOptimal, MIP_NoSolFound] + return MOI.INFEASIBLE_POINT + end + return MOI.NO_SOLUTION + else + stat_lp = get_lp_status2(model.inner) + if stat_lp == LP_Optimal + return MOI.FEASIBLE_POINT + elseif stat_lp == LP_Unbounded && hasprimalray(model.inner) + return MOI.INFEASIBILITY_CERTIFICATE + # elseif stat_lp == LP_Infeasible + # return MOI.InfeasiblePoint - xpress wont return + # elseif cutoff//cutoffindual ??? + else + return MOI.NO_SOLUTION + end + end +end + +function MOI.get(model::Optimizer, ::MOI.DualStatus) + if is_mip(model.inner) + return MOI.NO_SOLUTION + else + stat_lp = get_lp_status2(model.inner) + if stat_lp == LP_Optimal + return MOI.FEASIBLE_POINT + elseif stat_lp == LP_Infeasible && hasdualray(model.inner) + return MOI.INFEASIBILITY_CERTIFICATE + # elseif stat_lp == LP_Unbounded + # return MOI.InfeasiblePoint - xpress wont return + # elseif cutoff//cutoffindual ??? + else + return MOI.NO_SOLUTION + end + end +end + + +#### Pending Block Start ###### +#LQOI.get_unbounded_ray!(instance::Optimizer, place) = XPR.getprimalray!(instance.inner, place) + + +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 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 + 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 + rhs = get_dblattrelement(model.inner, "QCRHS", row) + slack = get_dblattrelement(model.inner, "QCSlack", row) + return rhs - slack +end + +function _dual_multiplier(model::Optimizer) + return MOI.get(model, MOI.ObjectiveSense()) == MOI.MIN_SENSE ? 1.0 : -1.0 +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintDual, + c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.LessThan{Float64}} +) + column = _info(model, c).column + 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}} +) + info = _info(model, c) + x = get_dblattrelement(model.inner, "X", info.column) + lb = _get_variable_lower_bound(model, info) + if x ≈ lb + return _dual_multiplier(model) * get_dblattrelement(model.inner, "RC", info.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_objval(model.inner) + +function MOI.get(model::Optimizer, ::MOI.ObjectiveBound) + obj_rhs = get_dblattr(model.inner, XPRS_OBJRHS) + if is_mip(model.inner) + return get_bestbound(model.inner)+obj_rhs + else + return get_objval(model.inner)+obj_rhs + end +end + +#### Pending Block End ###### + +# Not implemented in Old Wrapper. Get CPUTIME Attribute +#MOI.get(model::Optimizer, ::MOI.SolveTime) = get_dblattr(model.inner, "RunTime") + +MOI.get(model::Optimizer, ::MOI.SimplexIterations) = get_simplex_iter_count(model.inner) + +MOI.get(model::Optimizer, ::MOI.BarrierIterations) = get_barrier_iter_count(model.inner) +MOI.get(model::Optimizer, ::MOI.NodeCount) = get_node_count(model.inner) + +# No MIP gap attribute in Xpress Manual +function MOI.get(model::Optimizer, ::MOI.RelativeGap) + L = get_mip_objval(model.inner) + U = get_bestbound(model.inner) + return abs(U-L)/U +end + +MOI.supports(model::Optimizer, ::MOI.DualObjectiveValue) = true + +MOI.get(model::Optimizer, ::MOI.DualObjectiveValue) = get_bestbound(model.inner) + +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 MOI.get(model::Optimizer, ::MOI.Name) + return get_strattr(model.inner, "ModelName") +end + +function MOI.set(model::Optimizer, ::MOI.Name, name::String) + set_strattr!(model.inner, "ModelName", name) + return +end + +MOI.get(model::Optimizer, ::MOI.NumberOfVariables) = length(model.variable_info) +function MOI.get(model::Optimizer, ::MOI.ListOfVariableIndices) + return sort!(collect(keys(model.variable_info)), by = x -> x.value) +end + +MOI.get(model::Optimizer, ::MOI.RawSolver) = model.inner + +function MOI.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) + + end + return +end + +function MOI.get( + model::Optimizer, ::MOI.VariablePrimalStart, x::MOI.VariableIndex +) + return _info(model, x).start +end + +function MOI.get(model::Optimizer, ::MOI.NumberOfConstraints{F, S}) where {F, S} + # TODO: this could be more efficient. + return length(MOI.get(model, MOI.ListOfConstraintIndices{F, S}())) +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 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 MOI.get( + model::Optimizer, ::MOI.ListOfConstraintIndices{MOI.VectorOfVariables, S} +) where {S <: Union{<:MOI.SOS1, <:MOI.SOS2}} + 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.ListOfConstraintIndices{MOI.VectorOfVariables, MOI.SecondOrderCone} +) + indices = MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.SecondOrderCone}[ + MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.SecondOrderCone}(key) + for (key, info) in model.quadratic_constraint_info + if typeof(info.set) == MOI.SecondOrderCone + ] + return sort!(indices, by = x -> x.value) +end + +function MOI.get(model::Optimizer, ::MOI.ListOfConstraints) + constraints = Set{Tuple{DataType, DataType}}() + for info in values(model.variable_info) + if info.bound == NONE + elseif info.bound == LESS_THAN + push!(constraints, (MOI.SingleVariable, MOI.LessThan{Float64})) + elseif info.bound == GREATER_THAN + push!(constraints, (MOI.SingleVariable, MOI.GreaterThan{Float64})) + elseif info.bound == LESS_AND_GREATER_THAN + push!(constraints, (MOI.SingleVariable, MOI.LessThan{Float64})) + push!(constraints, (MOI.SingleVariable, MOI.GreaterThan{Float64})) + elseif info.bound == EQUAL_TO + push!(constraints, (MOI.SingleVariable, MOI.EqualTo{Float64})) + elseif info.bound == INTERVAL + push!(constraints, (MOI.SingleVariable, MOI.Interval{Float64})) + end + if info.type == CONTINUOUS + elseif info.type == BINARY + push!(constraints, (MOI.SingleVariable, MOI.ZeroOne)) + elseif info.type == INTEGER + push!(constraints, (MOI.SingleVariable, MOI.Integer)) + 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.quadratic_constraint_info) + if typeof(info.set) == MOI.SecondOrderCone + push!(constraints, (MOI.VectorOfVariables, MOI.SecondOrderCone)) + else + push!(constraints, (MOI.ScalarQuadraticFunction{Float64}, typeof(info.set))) + end + end + for info in values(model.sos_constraint_info) + push!(constraints, (MOI.VectorOfVariables, typeof(info.set))) + end + return collect(constraints) +end + +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 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 + ) +end + +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 + ) +end + +""" + _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 + +""" + _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 + end + end + return true +end + +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) + # 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) + return +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintBasisStatus, + c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, S} +) where {S <: SCALAR_SETS} + row = _info(model, c).row + cbasis = 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 + 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 + 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 + +struct CallbackVariablePrimal <: MOI.AbstractVariableAttribute end + +function load_callback_variable_primal(model, cb_data, cb_where) + if cb_where != CB_MIPSOL + error("`load_callback_variable_primal` must be called from `CB_MIPSOL`.") + end + 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 + +""" + 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, + 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 + +""" + compute_conflict(model::Optimizer) + +Compute a minimal subset of the constraints and variables that keep the model +infeasible. + +See also `Gurobi.ConflictStatus` and `Gurobi.ConstraintConflictStatus`. + +Note that if `model` is modified after a call to `compute_conflict`, the +conflict is not purged, and any calls to the above attributes will return values +for the original conflict without a warning. +""" +function compute_conflict(model::Optimizer) + try + computeIIS(model.inner) + catch exc + if isa(exc, GurobiError) && exc.code == 10015 + model.inner.conflict = Gurobi.GRB_INFEASIBLE + else + rethrow(exc) + end + end + return +end + +function _ensure_conflict_computed(model::Optimizer) + if model.inner.conflict == -1 + error("Cannot access conflict status. Call `Gurobi.compute_conflict(model)` first. " * + "In case the model is modified, the computed conflict will not be purged.") + end +end + +function _is_feasible(model::Optimizer) + return model.inner.conflict == Gurobi.GRB_INFEASIBLE +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 +`MOI.OPTIMIZE_NOT_CALLED`. +""" +struct ConflictStatus <: MOI.AbstractModelAttribute end + +MOI.is_set_by_optimize(::ConflictStatus) = true + +function MOI.get(model::Optimizer, ::ConflictStatus) + if model.inner.conflict == -1 + return MOI.OPTIMIZE_NOT_CALLED + elseif model.inner.conflict == 0 + return MOI.OPTIMAL + elseif model.inner.conflict == Gurobi.GRB_LOADED + return MOI.OTHER_ERROR + elseif model.inner.conflict == Gurobi.GRB_OPTIMAL + return MOI.OPTIMAL + elseif model.inner.conflict == Gurobi.GRB_INFEASIBLE + return MOI.INFEASIBLE + elseif model.inner.conflict == Gurobi.GRB_INF_OR_UNBD + return MOI.INFEASIBLE_OR_UNBOUNDED + elseif model.inner.conflict == Gurobi.GRB_USER_OBJ_LIMIT + return MOI.OBJECTIVE_LIMIT + elseif model.inner.conflict == Gurobi.GRB_ITERATION_LIMIT + return MOI.ITERATION_LIMIT + elseif model.inner.conflict == Gurobi.GRB_NODE_LIMIT + return MOI.NODE_LIMIT + elseif model.inner.conflict == Gurobi.GRB_TIME_LIMIT + return MOI.TIME_LIMIT + elseif model.inner.conflict == Gurobi.GRB_SOLUTION_LIMIT + return MOI.SOLUTION_LIMIT + elseif model.inner.conflict == Gurobi.GRB_INTERRUPTED + return MOI.INTERRUPTED + elseif model.inner.conflict == Gurobi.GRB_NUMERIC + return MOI.NUMERICAL_ERROR + elseif model.inner.conflict == Gurobi.GRB_SUBOPTIMAL + return MOI.OTHER_LIMIT + elseif model.inner.conflict == Gurobi.GRB_INPROGRESS + return MOI.OTHER_ERROR + else + return MOI.OTHER_ERROR + end +end + +function MOI.supports(::Optimizer, ::ConflictStatus) + return true +end + +""" + ConstraintConflictStatus() + +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, <:MOI.LessThan} +) + _ensure_conflict_computed(model) + 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, <:MOI.GreaterThan} +) + _ensure_conflict_computed(model) + 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{MOI.EqualTo, MOI.Interval} + } +) + _ensure_conflict_computed(model) + 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{Float64}, + <:Union{MOI.LessThan, MOI.GreaterThan, MOI.EqualTo} + } +) + _ensure_conflict_computed(model) + 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{Float64}, + <:Union{MOI.LessThan, MOI.GreaterThan} + } +) + _ensure_conflict_computed(model) + 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, <:SCALAR_SETS}} +) + return true +end + +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{Float64}, + <:Union{MOI.LessThan, MOI.GreaterThan} + }} +) + return true +end + +#= Implemenent SOC later +### +### VectorOfVariables-in-SecondOrderCone +### + +function _info( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.SecondOrderCone} +) + 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.VectorOfVariables, s::MOI.SecondOrderCone +) + if length(f.variables) != s.dimension + error("Dimension of $(s) does not match number of terms in $(f)") + end + + # SOC is the cone: t ≥ ||x||₂ ≥ 0. In quadratic form, this is + # t² - Σᵢ xᵢ² ≥ 0 and t ≥ 0. + + # First, check the lower bound on t. + + t_info = _info(model, f.variables[1]) + lb = _get_variable_lower_bound(model, t_info) + if isnan(t_info.lower_bound_if_soc) && lb < 0.0 + t_info.lower_bound_if_soc = lb + set_dblattrelement!(model.inner, "LB", t_info.column, 0.0) + end + t_info.num_soc_constraints += 1 + + # Now add the quadratic constraint. + + I = Cint[_info(model, v).column for v in f.variables] + V = fill(Cdouble(-1.0), length(f.variables)) + V[1] = 1.0 + add_qconstr!(model.inner, Cint[], Cdouble[], I, I, V, Cchar('>'), 0.0) + 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.VectorOfVariables, MOI.SecondOrderCone}(model.last_constraint_index) +end + +function MOI.is_valid( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.SecondOrderCone} +) + info = get(model.quadratic_constraint_info, c.value, nothing) + return info !== nothing && typeof(info.set) == MOI.SecondOrderCone +end + +function MOI.delete( + model::Optimizer, + c::MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.SecondOrderCone} +) + f = MOI.get(model, MOI.ConstraintFunction(), c) + info = _info(model, c) + delqconstrs!(model.inner, [info.row]) + 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) + # Reset the lower bound on the `t` variable. + t_info = _info(model, f.variables[1]) + t_info.num_soc_constraints -= 1 + if t_info.num_soc_constraints > 0 + # Don't do anything. There are still SOC associated with this variable. + return + elseif isnan(t_info.lower_bound_if_soc) + # Don't do anything. It must have a >0 lower bound anyway. + return + end + # There was a previous bound that we over-wrote, and it must have been + # < 0 otherwise we wouldn't have needed to overwrite it. + @assert t_info.lower_bound_if_soc < 0.0 + tmp_lower_bound = t_info.lower_bound_if_soc + t_info.lower_bound_if_soc = NaN + set_dblattrelement!(model.inner, "LB", t_info.column, tmp_lower_bound) + return +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintSet, + c::MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.SecondOrderCone} +) + return _info(model, c).set +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintFunction, + c::MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.SecondOrderCone} +) + a, b, I, J, V = getqconstr(model.inner, _info(model, c).row) + @assert length(a) == length(b) == 0 # Check for no linear terms. + t = nothing + x = MOI.VariableIndex[] + for (i, j, coef) in zip(I, J, V) + v = model.variable_info[CleverDicts.LinearIndex(i + 1)].index + @assert i == j # Check for no off-diagonals. + if coef == 1.0 + @assert t === nothing # There should only be one `t`. + t = v + else + @assert coef == -1.0 # The coefficients _must_ be -1 for `x` terms. + push!(x, v) + end + end + @assert t !== nothing # Check that we found a `t` variable. + return MOI.VectorOfVariables([t; x]) +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintPrimal, + c::MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.SecondOrderCone} +) + f = MOI.get(model, MOI.ConstraintFunction(), c) + return MOI.get(model, MOI.VariablePrimal(), f.variables) +end + +function MOI.get( + model::Optimizer, ::MOI.ConstraintName, + c::MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.SecondOrderCone} +) + return _info(model, c).name +end + +function MOI.set( + model::Optimizer, ::MOI.ConstraintName, + c::MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.SecondOrderCone}, + 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 + set_strattrelement!(model.inner, "QCName", info.row, name) + 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[c] = name + end + return +end +=# diff --git a/src/Xpress.jl b/src/Xpress.jl index b82d73bf..a4aff26b 100755 --- a/src/Xpress.jl +++ b/src/Xpress.jl @@ -10,10 +10,8 @@ module Xpress end ### imports - - using Compat - using Compat.SparseArrays - using Compat.LinearAlgebra + using SparseArrays + using LinearAlgebra import Base.show, Base.copy @@ -83,7 +81,7 @@ module Xpress include("xprs_iis.jl") include("XpressSolverInterface.jl") - include("MOIWrapper.jl") + include("MOI_wrapper.jl") # license checker include("xprs_userlic.jl") diff --git a/src/XpressSolverInterface.jl b/src/XpressSolverInterface.jl index d564c7f6..6a236996 100644 --- a/src/XpressSolverInterface.jl +++ b/src/XpressSolverInterface.jl @@ -26,10 +26,10 @@ end function copy(m::XpressMathProgModel) - m.lazycb == nothing || Compat.@warn("Callbacks can't be copied, lazy callback ignored") - m.cutcb == nothing || Compat.@warn("Callbacks can't be copied, cut callback ignored") - m.heuristiccb == nothing || Compat.@warn("Callbacks can't be copied, heuristic callback ignored") - m.infocb == nothing || Compat.@warn("Callbacks can't be copied, info callback ignored") + m.lazycb == nothing || @warn("Callbacks can't be copied, lazy callback ignored") + m.cutcb == nothing || @warn("Callbacks can't be copied, cut callback ignored") + m.heuristiccb == nothing || @warn("Callbacks can't be copied, heuristic callback ignored") + m.infocb == nothing || @warn("Callbacks can't be copied, info callback ignored") return XpressMathProgModel(copy(m.inner), @@ -95,7 +95,7 @@ function MPB.loadproblem!(m::XpressMathProgModel, A, collb, colub, obj, rowlb, r end end if rangeconstrs - Compat.@warn("Julia Xpress interface doesn't properly support range (two-sided) constraints.") + @warn("Julia Xpress interface doesn't properly support range (two-sided) constraints.") add_rangeconstrs!(m.inner, float(A), float(rowlb), float(rowub)) else b = Array{Float64}(undef, length(rowlb)) diff --git a/src/xprs_attrs.jl b/src/xprs_attrs.jl index e254a88a..cdc28012 100644 --- a/src/xprs_attrs.jl +++ b/src/xprs_attrs.jl @@ -8,7 +8,7 @@ """ get_intattr(model::Model, ipar::Integer) -Return integer value corresponding to attribute with number `ipar` +Return integer value corresponding to attribute with number `ipar` """ function get_intattr(model::Model, ipar::Integer) out = Array{Cint}(undef, 1) @@ -26,7 +26,7 @@ end """ get_dblattr(model::Model, ipar::Integer) -Return double value corresponding to attribute with number `ipar` +Return double value corresponding to attribute with number `ipar` """ function get_dblattr(model::Model, ipar::Integer) out = Array{Float64}(undef, 1) @@ -44,7 +44,7 @@ end """ get_strattr(model::Model, ipar::Integer) -Return string value corresponding to attribute with number `ipar` +Return string value corresponding to attribute with number `ipar` """ function get_strattr(model::Model, ipar::Integer) out = zeros(Cchar,256) @@ -119,7 +119,7 @@ num_linconstrs(model::Model) = num_constrs(model) - num_qconstrs(model) """ model_sense(model::Model) -Return a symbol that encodes the objective function sense. +Return a symbol that encodes the objective function sense. The output is either `:minimize` or `:maximize` """ model_sense(model::Model) = obj_sense(model) == XPRS_OBJ_MINIMIZE ? (:minimize) : (:maximize) @@ -268,7 +268,7 @@ set_obj!(model, c) = set_objcoeffs!(model,c) Return the lower bounds for all variables in the vector lb. """ function get_lb!(model::Model, lb::Vector{Float64}, colb::Integer, cole::Integer) - + _chklen(lb,cole-colb+1) ret = @xprs_ccall(getlb, Cint, ( @@ -300,7 +300,7 @@ end Return vector of lowebounds with length equals to the number of variables in the model. """ function get_lb(model::Model, colb::Integer, cole::Integer) - + out = Array{Float64}(undef, cole-colb+1) get_lb!(model, out, colb, cole) @@ -324,7 +324,7 @@ lowerbounds(model::Model) = get_lb(model) Return the upper bounds for all variables in the vector out. """ function get_ub!(model::Model, out::Vector{Float64}, colb::Integer, cole::Integer) - + _chklen(out, cole-colb+1) ret = @xprs_ccall(getub, Cint, ( @@ -358,7 +358,7 @@ end Return vector of upperbounds with length equals to the number of variables in the model. """ function get_ub(model::Model, colb::Integer, cole::Integer) - + out = Array{Float64}(undef, cole-colb+1) get_ub!(model, out, colb, cole) @@ -385,7 +385,7 @@ function get_obj!(model::Model, obj::Vector{Float64}) cols = num_vars(model) _chklen(obj,cols) - + ret = @xprs_ccall(getobj, Cint, ( Ptr{Nothing}, # model Ptr{Float64}, @@ -425,7 +425,7 @@ objcoeffs(model::Model) = get_obj(model) Return the rhs for all constraints in the vector obj. """ function get_rhs!(model::Model, out::Vector{Float64}, rowb::Integer, rowe::Integer) - + _chklen(out, rowe-rowb+1) ret = @xprs_ccall(getrhs, Cint, ( @@ -456,7 +456,7 @@ end Return a vector of rhs with length equals to the number of variables in the model. """ function get_rhs(model::Model, rowb::Integer, rowe::Integer) - + out = Array{Float64}(undef, rowe-rowb+1) get_rhs!(model, out, rowb, rowe) @@ -590,7 +590,7 @@ Sets lower bounds `lb` given variable indices in `inds` of `model` set_lb!{R<:Real}(model::Model, lb::Vector{R}) -Sets lower bounds to all variables up to `length(lb)`. +Sets lower bounds to all variables up to `length(lb)`. `length(lb)` must be smaller than the number of variables. """ function set_lb!(model::Model, inds::Vector{I}, lb::Vector{R}) where {I<:Integer, R<:Real} @@ -613,7 +613,7 @@ Sets upper bounds `ub` given variable indices in `inds` of `model` set_ub!{R<:Real}(model::Model, ub::Vector{R}) -Sets upper bounds to all variables up to `length(ub)`. +Sets upper bounds to all variables up to `length(ub)`. `length(ub)` must be smaller than the number of variables. """ function set_ub!(model::Model, inds::Vector{I}, ub::Vector{R}) where {I<:Integer, R<:Real} @@ -653,7 +653,7 @@ Sets coefficients `rhs` given indices in `inds` in the rhs of `model` set_rhs!{R<:Real}(model::Model, lb::Vector{R}) -Sets coefficients in the rhs of all constraints up to `length(rhs)`. +Sets coefficients in the rhs of all constraints up to `length(rhs)`. `length(rhs)` must be smaller than the number of constraints. """ function set_rhs!(model::Model, inds::Vector{I}, rhs::Vector{R}) where {I<:Integer, R<:Real} @@ -673,11 +673,11 @@ Sets row type `senses` for given indices in `inds` set_rowtype!(model::Model, senses::Vector{Cchar}) -Sets row type in all constraints up to `length(senses)`. +Sets row type in all constraints up to `length(senses)`. `length(senses)` must be smaller than the number of constraints. """ function set_rowtype!(model::Model, inds::Vector{I}, senses::Vector{Cchar}) where I<:Integer - + rows = length(senses) ret = @xprs_ccall(chgrowtype, Cint, ( Ptr{Nothing}, # model diff --git a/src/xprs_common.jl b/src/xprs_common.jl index 8126301f..615d1c89 100755 --- a/src/xprs_common.jl +++ b/src/xprs_common.jl @@ -30,7 +30,7 @@ fvec(v::Integer) = Float64[v] # converts v into a vector of Cchar or Float64 of length n, # where v can be either a scalar or a vector of length n. -_chklen(n::Integer, v::Vector) = _chklen(v, n::Integer) +_chklen(n::Integer, v::Vector) = _chklen(v, n::Integer) _chklen(v, n::Integer) = (length(v) == n || error("Inconsistent argument dimensions.")) _cmplen(v1::Vector, v2::Vector) = (length(v1) == length(v2) || error("Inconsistent argument dimensions.")) @@ -54,13 +54,13 @@ macro xprs_ccall(func, args...) f = "XPRS$(func)" args = map(esc,args) - Compat.Sys.isunix() && return quote + Sys.isunix() && return quote ccall(($f,xprs), $(args...)) end - Compat.Sys.iswindows() && VERSION < v"0.6-" && return quote + Sys.iswindows() && VERSION < v"0.6-" && return quote ccall(($f,xprs), stdcall, $(args...)) end - Compat.Sys.iswindows() && VERSION >= v"0.6-" && return quote + Sys.iswindows() && VERSION >= v"0.6-" && return quote ccall(($f,xprs), $(esc(:stdcall)), $(args...)) end end diff --git a/src/xprs_constrs.jl b/src/xprs_constrs.jl index 8360f4e3..0dbd4340 100644 --- a/src/xprs_constrs.jl +++ b/src/xprs_constrs.jl @@ -20,7 +20,7 @@ end function add_constr!(model::Model, inds::Vector{Cint}, coeffs::Vector{Float64}, rel::Cchar, rhs::Float64) rel = constrainttype(rel) - + length(inds) == length(coeffs) || error("Inconsistent argument dimensions.") ret = @xprs_ccall(addrows, Cint,( @@ -56,10 +56,10 @@ end """ add_constr!(model::Model, coeffs::Vector, rel::GChars, rhs::Real) -Adds a constraint based on a dense vector os coefficients `coeffs` +Adds a constraint based on a dense vector os coefficients `coeffs` """ function add_constr!(model::Model, coeffs::Vector, rel::GChars, rhs::Real) - inds = Compat.findall(!iszero, coeffs) + inds = findall(!iszero, coeffs) vals = coeffs[inds] add_constr!(model, inds, vals, rel, rhs) end @@ -143,7 +143,7 @@ end """ add_rangeconstr!(model::Model, inds::Vector, coeffs::Vector, lb::Real, ub::Real) - + Adds single range constraint in sparse format add_rangeconstr!(model::Model, coeffs::Vector, lb::Real, ub::Real) @@ -395,8 +395,8 @@ function get_sos_matrix(m::Model) intents = Array{Cint}(undef, 1) nsets = Array{Cint}(undef, 1) - # int XPRS_CC XPRSgetglobal(XPRSprob prob, int*nglents, int*sets, - #char qgtype[], int mgcols[], double dlim[], char qstype[], + # int XPRS_CC XPRSgetglobal(XPRSprob prob, int*nglents, int*sets, + #char qgtype[], int mgcols[], double dlim[], char qstype[], #int msstart[],int mscols[], double dref[]); ret = @xprs_ccall(getglobal, Cint, ( Ptr{Nothing}, @@ -410,7 +410,7 @@ function get_sos_matrix(m::Model) Ptr{Cint}, # mscols Ptr{Float64}, # dref ), - m.ptr_model, intents, nsets, C_NULL, C_NULL, C_NULL, + m.ptr_model, intents, nsets, C_NULL, C_NULL, C_NULL, settypes, setstart, setcols, setvals) if ret != 0 @@ -453,7 +453,7 @@ end """ chg_coeffs!{T<:Real, S<:Real}(model::Model, cidx::Vector{T}, vidx::Vector{T}, val::Vector{S}) -Change multiple coefficients of the `A` matrix given constraints `cidx`, variables `vidx` and values `val` +Change multiple coefficients of the `A` matrix given constraints `cidx`, variables `vidx` and values `val` """ chg_coeffs!(model::Model, cidx::T, vidx::T, val::S) where {T<:Real, S<:Real} = chg_coeffs!(model, Cint[cidx], Cint[vidx], Float64[val]) chg_coeffs!(model::Model, cidx::Vector{T}, vidx::Vector{T}, val::Vector{S}) where {T<:Real, S<:Real} = chg_coeffs!(model, convert(Vector{Cint},cidx), convert(Vector{Cint},vidx), fvec(val)) @@ -472,12 +472,12 @@ function chg_coeffs!(model::Model, cidx::Vector{Cint}, vidx::Vector{Cint}, val:: if ret != 0 throw(XpressError(model)) end -end +end function chg_rhsrange!(model::Model, cidx::Vector{Cint}, val::FVec) # XPRSchgrhsrange(XPRSprob prob, int nels, const int mindex[], const double rng[]) - + (length(cidx) == length(val)) || error("Inconsistent argument dimensions.") numchgs = length(cidx) @@ -517,4 +517,4 @@ function get_rhsrange(model::Model, rowb::Integer, rowe::Integer) get_rhsrange!(model, out, rowb, rowe) return out -end \ No newline at end of file +end diff --git a/src/xprs_env.jl b/src/xprs_env.jl index 06ab4b37..39073977 100755 --- a/src/xprs_env.jl +++ b/src/xprs_env.jl @@ -5,16 +5,23 @@ Initialize Xpress environment. Need to do ir once and only once. """ -function Env() - ret = @xprs_ccall(init, Cint, (Ptr{Cchar},), C_NULL) - if ret != 0 - if ret == :number - error("Invalid Xpress license") - else - error("Failed to create environment (error $ret).") +mutable struct Env + ptr_env::Ptr{Cvoid} + + function Env() + a = Ref{Ptr{Cvoid}}() + ret = @xprs_ccall(init, Cint, (Ptr{Cchar},), C_NULL) + if ret != 0 + if ret == :number + error("Invalid Xpress license") + else + error("Failed to create environment (error $ret).") + end end + env = new(a[]) + # finalizer(env, free_env) ## temporary disable: which tends to sometimes caused warnings + env end - # finalizer(env, free_env) ## temporary disable: which tends to sometimes caused warnings end function is_valid() @@ -27,6 +34,34 @@ end Finalize Xpress environment. """ -function free_env() - ret = @xprs_ccall(free, Cint, ()) -end \ No newline at end of file +function free_env(env::Env) + if env.ptr_env != C_NULL + ret = @xprs_ccall(free, Cint, ()) + env.ptr_env = C_NULL + end +end + +Base.unsafe_convert(ty::Type{Ptr{Cvoid}}, env::Env) = env.ptr_env::Ptr{Cvoid} + +function is_valid(env::Env) + env.ptr_env != C_NULL +end + +#= error +function get_error_msg(env::Env) + @assert env.ptr_env != C_NULL + sz = @grb_ccall(geterrormsg, Ptr{UInt8}, (Ptr{Cvoid},), env.ptr_env) + unsafe_string(sz) +end + + + +mutable struct XpressError <: Exception + code::Int + msg::String + + function XpressError(env::Env, code::Integer) + new(convert(Int, code), get_error_msg(env)) + end +end +=# diff --git a/src/xprs_model.jl b/src/xprs_model.jl index cd6caf95..b420c381 100755 --- a/src/xprs_model.jl +++ b/src/xprs_model.jl @@ -193,6 +193,20 @@ function addnames(m::Model, names::Vector, nametype::Int32) nothing end +addcolname(m::Model, name::String, Index::Int32) = addname(m, name, Int32(2), Index::Int32) +addrowname(m::Model, name::String, Index::Int32) = addname(m, name, Int32(1), Index::Int32) +function addname(m::Model, name::String, nametype::Int32, Index::Int32) + # XPRSaddnames(prob, int type, char name[], int first, int last) + + ret = @xprs_ccall(addnames, Cint, (Ptr{Nothing}, Cint,Ptr{Cchar}, Cint, Cint), + m.ptr_model, nametype, name, Index, Index) + + if ret != 0 + throw(XpressError(m)) + end + + nothing +end # read / write file #= diff --git a/src/xprs_solve.jl b/src/xprs_solve.jl index 925d0f10..bc449530 100644 --- a/src/xprs_solve.jl +++ b/src/xprs_solve.jl @@ -904,7 +904,7 @@ function getdualray(model::Model) dray = Array{Float64}(undef, num_constrs(model)) if !getdualray!(model, dray) - Compat.@warn("Xpress solver was unable to provide an infeasibility ray") + @warn("Xpress solver was unable to provide an infeasibility ray") return dray end @@ -953,7 +953,7 @@ function getprimalray(model::Model) pray = Array{Float64}(undef, num_vars(model)) if !getprimalray!(model, pray) - Compat.@warn("Xpress solver was unable to provide an unboundedness ray") + @warn("Xpress solver was unable to provide an unboundedness ray") return pray end diff --git a/src/xprs_userlic.jl b/src/xprs_userlic.jl index a5bdfd83..646d186d 100644 --- a/src/xprs_userlic.jl +++ b/src/xprs_userlic.jl @@ -39,7 +39,7 @@ function get_xpauthpath(xpauth_path = "", verbose::Bool = true) for i in candidates if isfile(i) if verbose - Compat.@info("Xpress: Found license file $i") + @info("Xpress: Found license file $i") end return i end @@ -84,18 +84,18 @@ function userlic(; verbose::Bool = true, liccheck::Function = emptyliccheck, xpa if ierr == 16 # DEVELOPER if verbose - Compat.@info("Xpress: Development license detected.") + @info("Xpress: Development license detected.") end elseif ierr != 0 # FAIL - Compat.@info("Xpress: Failed to find working license.") + @info("Xpress: Failed to find working license.") ret = @xprs_ccall(getlicerrmsg, Cint, (Ptr{Cchar},Cint), errmsg, 1024) error( unsafe_string(pointer(errmsg)) ) else # USER if verbose - Compat.@info("Xpress: User license detected.") - Compat.@info( unsafe_string(pointer(slicmsg)) ) + @info("Xpress: User license detected.") + @info( unsafe_string(pointer(slicmsg)) ) end end diff --git a/test/MOIWrapper.jl b/test/MOIWrapper_old.jl similarity index 97% rename from test/MOIWrapper.jl rename to test/MOIWrapper_old.jl index 91e35585..8c431d91 100644 --- a/test/MOIWrapper.jl +++ b/test/MOIWrapper_old.jl @@ -1,6 +1,6 @@ #push!(Base.LOAD_PATH,joinpath(dirname(@__FILE__),"..","..")) -using Xpress, Compat.Test, MathOptInterface, MathOptInterface.Test +using Xpress, Test, MathOptInterface, MathOptInterface.Test const MOI = MathOptInterface const MOIT = MathOptInterface.Test diff --git a/test/MOI_Wrapper.jl b/test/MOI_Wrapper.jl new file mode 100644 index 00000000..ca8625bf --- /dev/null +++ b/test/MOI_Wrapper.jl @@ -0,0 +1,965 @@ +using Xpress, MathOptInterface + +const MOI = MathOptInterface +const MOIT = MathOptInterface.Test + +const MOI = Xpress.MOI +const MOIT = MOI.Test + +const XPRESS_ENV = Xpress.Env() + +const OPTIMIZER = MOI.Bridges.full_bridge_optimizer( + # Note: we set `DualReductions = 0` so that we never return + # `INFEASIBLE_OR_UNBOUNDED`. + Xpress.Optimizer(XPRESS_ENV), Float64) + +const CONFIG = MOIT.TestConfig() + +@testset "Unit Tests" begin + MOIT.basic_constraint_tests(OPTIMIZER, CONFIG; exclude = [ + (MOI.VectorOfVariables, MOI.GeometricMeanCone) + ]) + # get(::ConstraintFunction) and get(::ConstraintSet) haven't been + # implemented for the geomean bridge in MOI. + MOIT.basic_constraint_tests(OPTIMIZER, CONFIG; include = [ + (MOI.VectorOfVariables, MOI.GeometricMeanCone) + ], + get_constraint_function = false, + get_constraint_set = false + ) + MOIT.unittest(OPTIMIZER, MOIT.TestConfig(atol=1e-6)) + MOIT.modificationtest(OPTIMIZER, CONFIG) +end +#= +@testset "Linear tests" begin + @testset "Default Solver" begin + MOIT.contlineartest(OPTIMIZER, MOIT.TestConfig(basis = true), [ + # This requires an infeasiblity certificate for a variable bound. + "linear12" + ]) + end + @testset "No certificate" begin + MOIT.linear12test(OPTIMIZER, MOIT.TestConfig(infeas_certificates=false)) + end +end + +@testset "Quadratic tests" begin + MOIT.contquadratictest(OPTIMIZER, MOIT.TestConfig(atol=1e-3, rtol=1e-3), [ + "ncqcp" # Gurobi doesn't support non-convex problems. + ]) +end + +@testset "Conic tests" begin + MOIT.lintest(OPTIMIZER, CONFIG) + MOIT.soctest(OPTIMIZER, MOIT.TestConfig(duals = false, atol=1e-3), ["soc3"]) + MOIT.soc3test( + OPTIMIZER, + MOIT.TestConfig(duals = false, infeas_certificates = false, atol = 1e-3) + ) + MOIT.rsoctest(OPTIMIZER, MOIT.TestConfig(duals = false, atol=1e-3)) + MOIT.geomeantest(OPTIMIZER, MOIT.TestConfig(duals = false, atol=1e-3)) +end + +@testset "Integer Linear tests" begin + MOIT.intlineartest(OPTIMIZER, CONFIG, [ + # Indicator sets not supported. + "indicator1", "indicator2", "indicator3" + ]) +end + +@testset "ModelLike tests" begin + + @test MOI.get(OPTIMIZER, MOI.SolverName()) == "Gurobi" + + @testset "default_objective_test" begin + MOIT.default_objective_test(OPTIMIZER) + end + + @testset "default_status_test" begin + MOIT.default_status_test(OPTIMIZER) + end + + @testset "nametest" begin + MOIT.nametest(OPTIMIZER) + end + + @testset "validtest" begin + MOIT.validtest(OPTIMIZER) + end + + @testset "emptytest" begin + MOIT.emptytest(OPTIMIZER) + end + + @testset "orderedindicestest" begin + MOIT.orderedindicestest(OPTIMIZER) + end + + @testset "copytest" begin + 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) + @test MOI.supports(model, MOI.VariablePrimalStart(), MOI.VariableIndex) + @test MOI.get(model, MOI.VariablePrimalStart(), x[1]) === nothing + @test MOI.get(model, MOI.VariablePrimalStart(), x[2]) === nothing + Gurobi._update_if_necessary(model) + @test Gurobi.get_dblattrelement( + model.inner, "Start", Gurobi._info(model, x[1]).column + ) == Gurobi.GRB_UNDEFINED + 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 + Gurobi._update_if_necessary(model) + @test Gurobi.get_dblattrelement( + model.inner, "Start", Gurobi._info(model, x[2]).column + ) == Gurobi.GRB_UNDEFINED + 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 + +@testset "Gurobi Callback" begin + @testset "Generic callback" begin + m = Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0) + x = MOI.add_variable(m) + MOI.add_constraint(m, MOI.SingleVariable(x), MOI.GreaterThan(1.0)) + MOI.set(m, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction{Float64}( + [MOI.ScalarAffineTerm{Float64}(1.0, x)], + 0.0 + ) + ) + + cb_calls = Int32[] + function callback_function(cb_data::Gurobi.CallbackData, cb_where::Int32) + push!(cb_calls, cb_where) + nothing + end + + MOI.set(m, Gurobi.CallbackFunction(), callback_function) + MOI.optimize!(m) + + @test length(cb_calls) > 0 + @test Gurobi.CB_MESSAGE in cb_calls + @test Gurobi.CB_PRESOLVE in cb_calls + @test !(Gurobi.CB_MIPSOL in cb_calls) + end + + @testset "Lazy cut" begin + m = Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0, Cuts=0, Presolve=0, Heuristics=0, LazyConstraints=1) + MOI.Utilities.loadfromstring!(m,""" + variables: x, y + maxobjective: y + c1: x in Integer() + c2: y in Integer() + c3: x in Interval(0.0, 2.0) + c4: y in Interval(0.0, 2.0) + """) + x = MOI.get(m, MOI.VariableIndex, "x") + y = MOI.get(m, MOI.VariableIndex, "y") + + # We now define our callback function that takes two arguments: + # (1) the callback handle; and + # (2) the location from where the callback was called. + # Note that we can access m, x, and y because this function is defined + # inside the same scope + cb_calls = Int32[] + function callback_function(cb_data::Gurobi.CallbackData, cb_where::Int32) + push!(cb_calls, cb_where) + if cb_where == Gurobi.CB_MIPSOL + 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) + # |xx/ \xx| + # |x/ \x| + # |/ \| + # (0,1) + + (2,1) + # | | + # (0,0) +---+---+ (2,0) + TOL = 1e-6 # Allow for some impreciseness in the solution + if y_val - x_val > 1 + TOL + Gurobi.cblazy!(cb_data, m, + MOI.ScalarAffineFunction{Float64}( + MOI.ScalarAffineTerm.([-1.0, 1.0], [x, y]), + 0.0 + ), + MOI.LessThan{Float64}(1.0) + ) + elseif y_val + x_val > 3 + TOL + Gurobi.cblazy!(cb_data, m, + MOI.ScalarAffineFunction{Float64}( + MOI.ScalarAffineTerm.([1.0, 1.0], [x, y]), + 0.0 + ), + MOI.LessThan{Float64}(3.0) + ) + end + end + end + + MOI.set(m, Gurobi.CallbackFunction(), callback_function) + MOI.optimize!(m) + + @test MOI.get(m, MOI.VariablePrimal(), x) == 1 + @test MOI.get(m, MOI.VariablePrimal(), y) == 2 + + @test length(cb_calls) > 0 + @test Gurobi.CB_MESSAGE in cb_calls + @test Gurobi.CB_PRESOLVE in cb_calls + @test Gurobi.CB_MIPSOL in cb_calls + end +end + +@testset "LQOI Issue #38" begin + # https://github.com/JuliaOpt/LinQuadOptInterface.jl/issues/38#issuecomment-407625187 + _getinner(opt::Gurobi.Optimizer) = opt.inner + @inferred _getinner(Gurobi.Optimizer(GUROBI_ENV)) +end + +@testset "User limit handling (issue #140)" begin + # Verify that we return the correct status codes when a mixed-integer + # problem has been solved to a *feasible* but not necessarily optimal + # solution. To do that, we will set up an intentionally dumbed-down + # Gurobi Gurobi.Optimizer (with all heuristics and pre-solve turned off) and + # ask it to solve a classic knapsack problem. Setting SolutionLimit=1 + # forces the solver to return after its first feasible MIP solution, + # which tests the right part of the code without relying on potentially + # flaky or system-dependent time limits. + m = Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0, SolutionLimit=1, + Heuristics=0.0, Presolve=0) + N = 100 + x = MOI.add_variables(m, N) + for xi in x + MOI.add_constraint(m, MOI.SingleVariable(xi), MOI.ZeroOne()) + MOI.set(m, MOI.VariablePrimalStart(), xi, 0.0) + 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. + Random.seed!(1) + item_weights = rand(N) + item_values = rand(N) + MOI.add_constraint(m, + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(item_weights, x), 0.0), + MOI.LessThan(10.0)) + MOI.set(m, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.(.-item_values, x), 0.0)) + MOI.optimize!(m) + + @test MOI.get(m, MOI.TerminationStatus()) == MOI.SOLUTION_LIMIT + # We should have a primal feasible solution: + @test MOI.get(m, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + # But we have no dual status: + @test MOI.get(m, MOI.DualStatus()) == MOI.NO_SOLUTION +end + +@testset "Constant objective (issue #111)" begin + m = Gurobi.Optimizer(GUROBI_ENV) + x = MOI.add_variable(m) + MOI.set(m, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{Float64}[], 2.0)) + @test MOI.get(m, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()).constant == 2.0 + @test Gurobi.get_dblattr(m.inner, "ObjCon") == 2.0 + + MOI.modify(m, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), MOI.ScalarConstantChange(3.0)) + @test MOI.get(m, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()).constant == 3.0 + @test Gurobi.get_dblattr(m.inner, "ObjCon") == 3.0 +end + +@testset "Env" begin + @testset "User-provided" begin + env = Gurobi.Env() + model_1 = Gurobi.Optimizer(env) + @test model_1.inner.env === env + model_2 = Gurobi.Optimizer(env) + @test model_2.inner.env === env + # Check that finalizer doesn't touch env when manually provided. + finalize(model_1.inner) + @test Gurobi.is_valid(env) + end + @testset "Automatic" begin + model_1 = Gurobi.Optimizer() + model_2 = Gurobi.Optimizer() + @test model_1.inner.env !== model_2.inner.env + # Check that env is finalized with model when not supplied manually. + finalize(model_1.inner) + @test !Gurobi.is_valid(model_1.inner.env) + end + @testset "Env when emptied" begin + @testset "User-provided" begin + env = Gurobi.Env() + model = Gurobi.Optimizer(env) + @test model.inner.env === env + @test Gurobi.is_valid(env) + MOI.empty!(model) + @test model.inner.env === env + @test Gurobi.is_valid(env) + end + @testset "Automatic" begin + model = Gurobi.Optimizer() + env = model.inner.env + MOI.empty!(model) + @test model.inner.env !== env + @test Gurobi.is_valid(model.inner.env) + end + end +end + +@testset "Conflict refiner" begin + @testset "Variable bounds (SingleVariable and LessThan/GreaterThan)" begin + 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. + @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 (ScalarAffine)" begin + 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. + @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 (Invali Interval)" begin + model = Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0) + x = MOI.add_variable(model) + 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. + Gurobi.compute_conflict(model) + @test MOI.get(model, Gurobi.ConflictStatus()) == MOI.OPTIMAL + @test MOI.get(model, Gurobi.ConstraintConflictStatus(), c1) == true + end + + @testset "Two conflicting constraints (GreaterThan, LessThan)" begin + 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)) + b2 = MOI.add_constraint(model, MOI.SingleVariable(y), MOI.GreaterThan(0.0)) + cf1 = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0, 1.0], [x, y]), 0.0) + c1 = MOI.add_constraint(model, cf1, MOI.LessThan(-1.0)) + 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. + @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(), b1) == true + @test MOI.get(model, Gurobi.ConstraintConflictStatus(), b2) == true + @test MOI.get(model, Gurobi.ConstraintConflictStatus(), c1) == true + @test MOI.get(model, Gurobi.ConstraintConflictStatus(), c2) == false + end + + @testset "Two conflicting constraints (EqualTo)" begin + 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)) + b2 = MOI.add_constraint(model, MOI.SingleVariable(y), MOI.GreaterThan(0.0)) + cf1 = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0, 1.0], [x, y]), 0.0) + c1 = MOI.add_constraint(model, cf1, MOI.EqualTo(-1.0)) + 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. + @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(), b1) == true + @test MOI.get(model, Gurobi.ConstraintConflictStatus(), b2) == true + @test MOI.get(model, Gurobi.ConstraintConflictStatus(), c1) == true + @test MOI.get(model, Gurobi.ConstraintConflictStatus(), c2) == false + end + + @testset "Variables outside conflict" begin + model = Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0) + x = MOI.add_variable(model) + y = MOI.add_variable(model) + z = MOI.add_variable(model) + b1 = MOI.add_constraint(model, MOI.SingleVariable(x), MOI.GreaterThan(0.0)) + b2 = MOI.add_constraint(model, MOI.SingleVariable(y), MOI.GreaterThan(0.0)) + b3 = MOI.add_constraint(model, MOI.SingleVariable(z), MOI.GreaterThan(0.0)) + cf1 = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0, 1.0], [x, y]), 0.0) + c1 = MOI.add_constraint(model, cf1, MOI.LessThan(-1.0)) + 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. + @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(), b1) == true + @test MOI.get(model, Gurobi.ConstraintConflictStatus(), b2) == true + @test MOI.get(model, Gurobi.ConstraintConflictStatus(), b3) == false + @test MOI.get(model, Gurobi.ConstraintConflictStatus(), c1) == true + @test MOI.get(model, Gurobi.ConstraintConflictStatus(), c2) == false + end + + @testset "No conflict" begin + 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. + @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.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, 3) + 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[3], "xα") + @test MOI.get(model, MOI.VariableIndex, "xα") == x[3] + 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 + +@testset "ConstraintAttribute" begin + model = Gurobi.Optimizer(GUROBI_ENV) + MOI.Utilities.loadfromstring!(model, """ +variables: x +minobjective: x +c1: x >= 0.0 +c2: 2x >= 1.0 +c3: x in Integer() +""") + c2 = MOI.get(model, MOI.ConstraintIndex, "c2") + c3 = MOI.get(model, MOI.ConstraintIndex, "c3") + # Linear constraints are supported - one test for each different type. + # Integer attribute + MOI.set(model, Gurobi.ConstraintAttribute("Lazy"), c2, 2) + @test MOI.get(model, Gurobi.ConstraintAttribute("Lazy"), c2) == 2 + # Real attribute + MOI.set(model, Gurobi.ConstraintAttribute("RHS"), c2, 2.0) + @test MOI.get(model, Gurobi.ConstraintAttribute("RHS"), c2) == 2.0 + # Char attribute + MOI.set(model, Gurobi.ConstraintAttribute("Sense"), c2, '<') + @test MOI.get(model, Gurobi.ConstraintAttribute("Sense"), c2) == '<' + # String attribute + MOI.set(model, Gurobi.ConstraintAttribute("ConstrName"), c2, "c4") + @test MOI.get(model, Gurobi.ConstraintAttribute("ConstrName"), c2) == "c4" + # Things that should fail follow. + # Non-linear constraints are not supported. + @test_throws( + MOI.SetAttributeNotAllowed(Gurobi.ConstraintAttribute("Lazy")), + MOI.set(model, Gurobi.ConstraintAttribute("Lazy"), c3, 1) + ) + # Getting/setting a non-existing attribute. + attr = Gurobi.ConstraintAttribute("Non-existing") + @test_throws MOI.UnsupportedAttribute(attr) MOI.set(model, attr, c2, 1) + @test_throws MOI.UnsupportedAttribute(attr) MOI.get(model, attr, c2) + # Setting an attribute to a value of the wrong type. + @test_throws( + ArgumentError("Attribute Lazy is Integer but Float64 provided."), + MOI.set(model, Gurobi.ConstraintAttribute("Lazy"), c2, 1.0) + ) +end + +@testset "VariableAttribute" begin + model = Gurobi.Optimizer(GUROBI_ENV) + MOI.Utilities.loadfromstring!(model, """ +variables: x +minobjective: x +c1: x >= 0.0 +c2: 2x >= 1.0 +c3: x in Integer() +""") + x = MOI.get(model, MOI.VariableIndex, "x") + # Setting attributes of each type + # Integer attribute + MOI.set(model, Gurobi.VariableAttribute("VarHintPri"), x, 2) + @test MOI.get(model, Gurobi.VariableAttribute("VarHintPri"), x) == 2 + # Real Attribute + MOI.set(model, Gurobi.VariableAttribute("LB"), x, 2.0) + @test MOI.get(model, Gurobi.VariableAttribute("LB"), x) == 2.0 + # Char Attribute + MOI.set(model, Gurobi.VariableAttribute("VType"), x, 'B') + @test MOI.get(model, Gurobi.VariableAttribute("VType"), x) == 'B' + # String Attribute + MOI.set(model, Gurobi.VariableAttribute("VarName"), x, "my_var") + @test MOI.get(model, Gurobi.VariableAttribute("VarName"), x) == "my_var" + # Things that should fail follow. + # Getting/setting a non-existing attribute. + attr = Gurobi.VariableAttribute("Non-existing") + @test_throws MOI.UnsupportedAttribute(attr) MOI.set(model, attr, x, 1) + @test_throws MOI.UnsupportedAttribute(attr) MOI.get(model, attr, x) + # Setting an attribute to a value of the wrong type. + @test_throws( + ArgumentError("Attribute BranchPriority is Integer but Float64 provided."), + MOI.set(model, Gurobi.VariableAttribute("BranchPriority"), x, 1.0) + ) +end + +@testset "ModelAttribute" begin + model = Gurobi.Optimizer(GUROBI_ENV) + MOI.Utilities.loadfromstring!(model, """ +variables: x +minobjective: x +c1: x >= 0.0 +c2: 2x >= 1.0 +c3: x in Integer() +""") + # Setting attributes of each type + # Integer attribute + MOI.set(model, Gurobi.ModelAttribute("ModelSense"), -1) + @test MOI.get(model, Gurobi.ModelAttribute("ModelSense")) == -1 + # Real Attribute + MOI.set(model, Gurobi.ModelAttribute("ObjCon"), 3.0) + @test MOI.get(model, Gurobi.ModelAttribute("ObjCon")) == 3.0 + # String Attribute + MOI.set(model, Gurobi.ModelAttribute("ModelName"), "My model") + @test MOI.get(model, Gurobi.ModelAttribute("ModelName")) == "My model" + # Things that should fail follow. + # Getting/setting a non-existing attribute. + attr = Gurobi.ModelAttribute("Non-existing") + @test_throws MOI.UnsupportedAttribute(attr) MOI.set(model, attr, 1) + @test_throws MOI.UnsupportedAttribute(attr) MOI.get(model, attr) + # Setting an attribute to a value of the wrong type. + @test_throws( + ArgumentError("Attribute NumStart is Integer but Float64 provided."), + MOI.set(model, Gurobi.ModelAttribute("NumStart"), 4.0) + ) +end + +@testset "SOC hide lower bound constraint" begin + model = Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0) + @testset "No initial bound" begin + MOI.empty!(model) + t = MOI.add_variable(model) + x = MOI.add_variables(model, 2) + MOI.add_constraints( + model, MOI.SingleVariable.(x), MOI.GreaterThan.(3.0:4.0) + ) + c_soc = MOI.add_constraint( + model, MOI.VectorOfVariables([t; x]), MOI.SecondOrderCone(3) + ) + MOI.set(model, MOI.ObjectiveFunction{MOI.SingleVariable}(), MOI.SingleVariable(t)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.optimize!(model) + @test MOI.get(model, MOI.VariablePrimal(), t) == 5.0 + MOI.delete(model, c_soc) + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.DUAL_INFEASIBLE + end + @testset "non-negative initial bound" begin + MOI.empty!(model) + t = MOI.add_variable(model) + x = MOI.add_variables(model, 2) + MOI.add_constraint(model, MOI.SingleVariable(t), MOI.GreaterThan(1.0)) + MOI.add_constraints( + model, MOI.SingleVariable.(x), MOI.GreaterThan.(3.0:4.0) + ) + c_soc = MOI.add_constraint( + model, MOI.VectorOfVariables([t; x]), MOI.SecondOrderCone(3) + ) + MOI.set(model, MOI.ObjectiveFunction{MOI.SingleVariable}(), MOI.SingleVariable(t)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.optimize!(model) + @test MOI.get(model, MOI.VariablePrimal(), t) == 5.0 + MOI.delete(model, c_soc) + MOI.optimize!(model) + @test MOI.get(model, MOI.VariablePrimal(), t) == 1.0 + end + @testset "negative initial bound" begin + MOI.empty!(model) + t = MOI.add_variable(model) + x = MOI.add_variables(model, 2) + MOI.add_constraint(model, MOI.SingleVariable(t), MOI.GreaterThan(-1.0)) + MOI.add_constraints( + model, MOI.SingleVariable.(x), MOI.GreaterThan.(3.0:4.0) + ) + c_soc = MOI.add_constraint( + model, MOI.VectorOfVariables([t; x]), MOI.SecondOrderCone(3) + ) + MOI.optimize!(model) + MOI.set(model, MOI.ObjectiveFunction{MOI.SingleVariable}(), MOI.SingleVariable(t)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + @test MOI.get(model, MOI.VariablePrimal(), t) == 5.0 + MOI.delete(model, c_soc) + MOI.optimize!(model) + @test MOI.get(model, MOI.VariablePrimal(), t) == -1.0 + end + @testset "non-negative post bound" begin + MOI.empty!(model) + t = MOI.add_variable(model) + x = MOI.add_variables(model, 2) + MOI.add_constraints( + model, MOI.SingleVariable.(x), MOI.GreaterThan.(3.0:4.0) + ) + MOI.add_constraint( + model, MOI.VectorOfVariables([t; x]), MOI.SecondOrderCone(3) + ) + c_lb = MOI.add_constraint(model, MOI.SingleVariable(t), MOI.GreaterThan(6.0)) + MOI.set(model, MOI.ObjectiveFunction{MOI.SingleVariable}(), MOI.SingleVariable(t)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.optimize!(model) + @test MOI.get(model, MOI.VariablePrimal(), t) == 6.0 + MOI.delete(model, c_lb) + MOI.optimize!(model) + @test MOI.get(model, MOI.VariablePrimal(), t) == 5.0 + end + @testset "negative post bound" begin + MOI.empty!(model) + t = MOI.add_variable(model) + x = MOI.add_variables(model, 2) + MOI.add_constraints( + model, MOI.SingleVariable.(x), MOI.GreaterThan.(3.0:4.0) + ) + c_soc = MOI.add_constraint( + model, MOI.VectorOfVariables([t; x]), MOI.SecondOrderCone(3) + ) + MOI.add_constraint(model, MOI.SingleVariable(t), MOI.GreaterThan(-6.0)) + MOI.set(model, MOI.ObjectiveFunction{MOI.SingleVariable}(), MOI.SingleVariable(t)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.optimize!(model) + @test MOI.get(model, MOI.VariablePrimal(), t) == 5.0 + MOI.delete(model, c_soc) + MOI.optimize!(model) + @test MOI.get(model, MOI.VariablePrimal(), t) == -6.0 + end + @testset "negative post bound II" begin + MOI.empty!(model) + t = MOI.add_variable(model) + x = MOI.add_variables(model, 2) + MOI.add_constraints( + model, MOI.SingleVariable.(x), MOI.GreaterThan.(3.0:4.0) + ) + c_soc = MOI.add_constraint( + model, MOI.VectorOfVariables([t; x]), MOI.SecondOrderCone(3) + ) + c_lb = MOI.add_constraint(model, MOI.SingleVariable(t), MOI.GreaterThan(-6.0)) + MOI.set(model, MOI.ObjectiveFunction{MOI.SingleVariable}(), MOI.SingleVariable(t)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.optimize!(model) + @test MOI.get(model, MOI.VariablePrimal(), t) == 5.0 + MOI.delete(model, c_lb) + MOI.optimize!(model) + @test MOI.get(model, MOI.VariablePrimal(), t) == 5.0 + MOI.delete(model, c_soc) + MOI.optimize!(model) + @test MOI.get(model, MOI.TerminationStatus()) == MOI.DUAL_INFEASIBLE + end + @testset "2 SOC's" begin + MOI.empty!(model) + t = MOI.add_variable(model) + x = MOI.add_variables(model, 2) + MOI.add_constraint(model, MOI.SingleVariable(t), MOI.GreaterThan(-1.0)) + MOI.add_constraints( + model, MOI.SingleVariable.(x), MOI.GreaterThan.([4.0, 3.0]) + ) + c_soc_1 = MOI.add_constraint( + model, MOI.VectorOfVariables([t, x[1]]), MOI.SecondOrderCone(2) + ) + c_soc_2 = MOI.add_constraint( + model, MOI.VectorOfVariables([t, x[2]]), MOI.SecondOrderCone(2) + ) + MOI.optimize!(model) + MOI.set(model, MOI.ObjectiveFunction{MOI.SingleVariable}(), MOI.SingleVariable(t)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + @test MOI.get(model, MOI.VariablePrimal(), t) == 4.0 + MOI.delete(model, c_soc_1) + MOI.optimize!(model) + @test MOI.get(model, MOI.VariablePrimal(), t) == 3.0 + MOI.delete(model, c_soc_2) + MOI.optimize!(model) + @test MOI.get(model, MOI.VariablePrimal(), t) == -1.0 + end +end + +@testset "Duplicate names" begin + @testset "Variables" begin + model = Gurobi.Optimizer(GUROBI_ENV) + (x, y, z) = MOI.add_variables(model, 3) + MOI.set(model, MOI.VariableName(), x, "x") + MOI.set(model, MOI.VariableName(), y, "x") + MOI.set(model, MOI.VariableName(), z, "z") + @test MOI.get(model, MOI.VariableIndex, "z") == z + @test_throws ErrorException MOI.get(model, MOI.VariableIndex, "x") + MOI.set(model, MOI.VariableName(), y, "y") + @test MOI.get(model, MOI.VariableIndex, "x") == x + @test MOI.get(model, MOI.VariableIndex, "y") == y + MOI.set(model, MOI.VariableName(), z, "x") + @test_throws ErrorException MOI.get(model, MOI.VariableIndex, "x") + MOI.delete(model, x) + @test MOI.get(model, MOI.VariableIndex, "x") == z + end + @testset "SingleVariable" begin + model = Gurobi.Optimizer(GUROBI_ENV) + x = MOI.add_variables(model, 3) + c = MOI.add_constraints(model, MOI.SingleVariable.(x), MOI.GreaterThan(0.0)) + MOI.set(model, MOI.ConstraintName(), c[1], "x") + MOI.set(model, MOI.ConstraintName(), c[2], "x") + MOI.set(model, MOI.ConstraintName(), c[3], "z") + @test MOI.get(model, MOI.ConstraintIndex, "z") == c[3] + @test_throws ErrorException MOI.get(model, MOI.ConstraintIndex, "x") + MOI.set(model, MOI.ConstraintName(), c[2], "y") + @test MOI.get(model, MOI.ConstraintIndex, "x") == c[1] + @test MOI.get(model, MOI.ConstraintIndex, "y") == c[2] + MOI.set(model, MOI.ConstraintName(), c[3], "x") + @test_throws ErrorException MOI.get(model, MOI.ConstraintIndex, "x") + MOI.delete(model, c[1]) + @test MOI.get(model, MOI.ConstraintIndex, "x") == c[3] + MOI.set(model, MOI.ConstraintName(), c[2], "x") + @test_throws ErrorException MOI.get(model, MOI.ConstraintIndex, "x") + MOI.delete(model, x[3]) + @test MOI.get(model, MOI.ConstraintIndex, "x") == c[2] + end + @testset "ScalarAffineFunction" begin + model = Gurobi.Optimizer(GUROBI_ENV) + x = MOI.add_variables(model, 3) + fs = [ + MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(1.0, xi)], 0.0) + for xi in x + ] + c = MOI.add_constraints(model, fs, MOI.GreaterThan(0.0)) + MOI.set(model, MOI.ConstraintName(), c[1], "x") + MOI.set(model, MOI.ConstraintName(), c[2], "x") + MOI.set(model, MOI.ConstraintName(), c[3], "z") + @test MOI.get(model, MOI.ConstraintIndex, "z") == c[3] + @test_throws ErrorException MOI.get(model, MOI.ConstraintIndex, "x") + MOI.set(model, MOI.ConstraintName(), c[2], "y") + @test MOI.get(model, MOI.ConstraintIndex, "x") == c[1] + @test MOI.get(model, MOI.ConstraintIndex, "y") == c[2] + MOI.set(model, MOI.ConstraintName(), c[3], "x") + @test_throws ErrorException MOI.get(model, MOI.ConstraintIndex, "x") + MOI.delete(model, c[1]) + @test MOI.get(model, MOI.ConstraintIndex, "x") == c[3] + end +end + +@testset "Duals with equal bounds #250" begin + model = Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0) + x = MOI.add_variable(model) + xl = MOI.add_constraint(model, MOI.SingleVariable(x), MOI.GreaterThan(1.0)) + xu = MOI.add_constraint(model, MOI.SingleVariable(x), MOI.LessThan(1.0)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.ObjectiveFunction{MOI.SingleVariable}(), MOI.SingleVariable(x)) + MOI.optimize!(model) + @test MOI.get(model, MOI.ConstraintDual(), xl) == 1.0 + @test MOI.get(model, MOI.ConstraintDual(), xu) == 0.0 +end + +@testset "Objective functions" begin + model = Gurobi.Optimizer(GUROBI_ENV) + x = MOI.add_variable(model) + @test MOI.get(model, MOI.ObjectiveSense()) == MOI.FEASIBILITY_SENSE + @test MOI.get(model, MOI.ListOfModelAttributesSet()) == Any[MOI.ObjectiveSense()] + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.ObjectiveFunction{MOI.SingleVariable}(), MOI.SingleVariable(x)) + @test MOI.get(model, MOI.ListOfModelAttributesSet()) == + Any[MOI.ObjectiveSense(), MOI.ObjectiveFunction{MOI.SingleVariable}()] + MOI.set(model, MOI.ObjectiveSense(), MOI.FEASIBILITY_SENSE) + @test MOI.get(model, MOI.ListOfModelAttributesSet()) == Any[MOI.ObjectiveSense()] +end + +@testset "FEASIBILITY_SENSE zeros objective" begin + model = Gurobi.Optimizer(GUROBI_ENV, OutputFlag=0) + x = MOI.add_variable(model) + MOI.add_constraint(model, MOI.SingleVariable(x), MOI.GreaterThan(1.0)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.ObjectiveFunction{MOI.SingleVariable}(), MOI.SingleVariable(x)) + MOI.optimize!(model) + @test MOI.get(model, MOI.ObjectiveValue()) == 1.0 + MOI.set(model, MOI.ObjectiveSense(), MOI.FEASIBILITY_SENSE) + MOI.optimize!(model) + @test MOI.get(model, MOI.ObjectiveValue()) == 0.0 +end +=# diff --git a/test/REQUIRE b/test/REQUIRE deleted file mode 100644 index 29db98ea..00000000 --- a/test/REQUIRE +++ /dev/null @@ -1,5 +0,0 @@ -MathProgBase 0.6 0.8 -JuMP -OffsetArrays 0.2.13 -Random -MathOptInterface \ No newline at end of file diff --git a/test/iis.jl b/test/iis.jl index ce78ab6e..b7386f16 100644 --- a/test/iis.jl +++ b/test/iis.jl @@ -1,5 +1,5 @@ -using Xpress, Compat.Test -using Compat.SparseArrays +using Xpress, Test +using SparseArrays @testset "IIS computation" begin @testset "Feasible problem, C API" begin diff --git a/test/jump.jl b/test/jump.jl index 9c81d75b..786a8633 100644 --- a/test/jump.jl +++ b/test/jump.jl @@ -1,4 +1,4 @@ -using Xpress, JuMP, MathProgBase, Compat.Test, OffsetArrays +using Xpress, JuMP, MathProgBase, Test, OffsetArrays import Xpress.XpressSolver lp_solvers = Any[] @@ -41,4 +41,4 @@ include(Pkg.dir("JuMP","test","model.jl")) include(Pkg.dir("JuMP","test","probmod.jl")) include(Pkg.dir("JuMP","test","socduals.jl")) -include(Pkg.dir("JuMP","test","qcqpmodel.jl")) \ No newline at end of file +include(Pkg.dir("JuMP","test","qcqpmodel.jl")) diff --git a/test/lp_01a.jl b/test/lp_01a.jl index a493a133..dfb87a9e 100755 --- a/test/lp_01a.jl +++ b/test/lp_01a.jl @@ -8,8 +8,8 @@ # # solution: x = 45, y = 6.25, objv = 51.25 -using Xpress, Compat.Test -using Compat.SparseArrays +using Xpress, Test +using SparseArrays @testset "Basics 1" begin model = Xpress.Model("lp_01", :maximize) @@ -38,4 +38,4 @@ using Compat.SparseArrays @test get_solution(model) == [45.0 , 6.25] @test get_objval(model) == 51.25 -end \ No newline at end of file +end diff --git a/test/lp_01b.jl b/test/lp_01b.jl index f368b6e8..c5869584 100755 --- a/test/lp_01b.jl +++ b/test/lp_01b.jl @@ -8,8 +8,8 @@ # # solution: x = 45, y = 6.25, objv = 51.25 -using Xpress, Compat.Test -using Compat.SparseArrays +using Xpress, Test +using SparseArrays @testset "Basics 1b" begin model = Xpress.Model("lp_01", :maximize) @@ -37,4 +37,4 @@ using Compat.SparseArrays @test get_solution(model) == [45.0 , 6.25] @test get_objval(model) == 51.25 -end \ No newline at end of file +end diff --git a/test/lp_02.jl b/test/lp_02.jl index 9b11b924..13de31bc 100755 --- a/test/lp_02.jl +++ b/test/lp_02.jl @@ -6,22 +6,22 @@ # x - 1.5y >= 0 (i.e. -x + 1.5 y <= 0) # 12 x + 8 y <= 1000 # 1000 x + 300 y <= 70000 -# +# # solution: (59.0909, 36.3636) # objv: 71818.1818 # using MathProgBase -using Xpress, Compat.Test -using Compat.SparseArrays +using Xpress, Test +using SparseArrays @testset "Basics 2" begin model = xpress_model( - name="lp_02", - sense=:maximize, + name="lp_02", + sense=:maximize, f = [1000., 350.], - A = [-1. 1.5; 12. 8.; 1000. 300.], - b = [0., 1000., 70000.], + A = [-1. 1.5; 12. 8.; 1000. 300.], + b = [0., 1000., 70000.], lb = [0., 30.]) @test Xpress.get_obj(model) == [1000, 350] diff --git a/test/lp_03.jl b/test/lp_03.jl index a61fab4a..82286ac5 100755 --- a/test/lp_03.jl +++ b/test/lp_03.jl @@ -1,6 +1,6 @@ # Test get/set objective coefficients in LP -using Xpress, Compat.Test +using Xpress, Test @testset "Basics 3" begin # original model @@ -12,7 +12,7 @@ using Xpress, Compat.Test model = xpress_model( name="lp_03", sense=:maximize, - f=[2.0, 2.0], + f=[2.0, 2.0], lb=[0.2, 0.2], ub=[1.0, 1.0]) @@ -51,4 +51,4 @@ using Xpress, Compat.Test @test isapprox(get_solution(model), [1.0, 0.2]; atol = 1e-3) @test isapprox(get_objval(model), 0.8; atol = 1e-3) -end \ No newline at end of file +end diff --git a/test/lp_04.jl b/test/lp_04.jl index 1751a454..39b66593 100644 --- a/test/lp_04.jl +++ b/test/lp_04.jl @@ -14,8 +14,8 @@ # x >= 0, y >= 0 # # solution: x = 1.3333333, y = 1.3333333, objv = 2.66666666 -using Xpress, Compat.Test -using Compat.SparseArrays +using Xpress, Test +using SparseArrays @testset "Basics 4" begin @@ -23,7 +23,7 @@ using Compat.SparseArrays add_cvars!(model, [1., 1.], [0., 0.], Inf) - add_constrs!(model, Cint[1, 3], Cint[1, 2, 1, 2], + add_constrs!(model, Cint[1, 3], Cint[1, 2, 1, 2], [2., 1., 1., 2.], '<', [4., 4.]) @test Xpress.get_obj(model) == [1, 1] @@ -42,7 +42,7 @@ using Compat.SparseArrays @test isapprox(get_objval(model), 2.666666; atol = 1e-3) - # PART 2: + # PART 2: # copy and solve model2 = copy(model) @@ -63,7 +63,7 @@ using Compat.SparseArrays @test isapprox(get_objval(model2), 2.666666; atol = 1e-3) - # PART 3: + # PART 3: # change coeff and solve # maximize x + y @@ -92,7 +92,7 @@ using Compat.SparseArrays @test isapprox(get_objval(model), 2; atol = 1e-3) - # PART 4: + # PART 4: # change coeff and solve # maximize x + y @@ -120,7 +120,7 @@ using Compat.SparseArrays @test isapprox(get_objval(model), 4; atol = 1e-3) - # PART 5: + # PART 5: # del var and solve # maximize y @@ -148,4 +148,4 @@ using Compat.SparseArrays @test isapprox(get_objval(model), 2; atol = 1e-3) -end \ No newline at end of file +end diff --git a/test/lp_repinfeas.jl b/test/lp_repinfeas.jl index f5e4d95d..a42e1d7c 100644 --- a/test/lp_repinfeas.jl +++ b/test/lp_repinfeas.jl @@ -6,7 +6,7 @@ # x \in [0, 10], y \in [0, 5] # -using Xpress, Compat.Test +using Xpress, Test # @testset "Basics 1" begin model = Xpress.Model("lp_01", :minimize) @@ -41,7 +41,7 @@ using Xpress, Compat.Test @test ans.status_lp == :optimal @show Xpress.get_objval(model) -@show Xpress.get_solution(model) +@show Xpress.get_solution(model) @show Xpress.get_dual(model) @test 0 == Xpress.repairweightedinfeasibility(model, lrp, grp, lbp, ubp, phase2 = Cchar('x')) @@ -49,7 +49,7 @@ using Xpress, Compat.Test @test ans.status_lp == :optimal @show Xpress.get_objval(model) -@show Xpress.get_solution(model) +@show Xpress.get_solution(model) @show Xpress.get_dual(model) @test 0 == Xpress.repairweightedinfeasibility(model, lrp, grp, lbp, ubp, phase2 = Cchar('f')) @@ -57,7 +57,7 @@ using Xpress, Compat.Test @test ans.status_lp == :optimal @show Xpress.get_objval(model) -@show Xpress.get_solution(model) +@show Xpress.get_solution(model) @show Xpress.get_dual(model) @test 0 == Xpress.repairweightedinfeasibility(model, lrp, grp, lbp, ubp, phase2 = Cchar('n')) @@ -65,6 +65,6 @@ using Xpress, Compat.Test @test ans.status_lp == :optimal @show Xpress.get_objval(model) -@show Xpress.get_solution(model) +@show Xpress.get_solution(model) @show Xpress.get_dual(model) -; \ No newline at end of file +; diff --git a/test/mip_01.jl b/test/mip_01.jl index 8a95accf..abf33fcd 100755 --- a/test/mip_01.jl +++ b/test/mip_01.jl @@ -10,8 +10,8 @@ # z is binary # # z = 1, y= 7, x = 0 -using Xpress, Compat.Test -using Compat.SparseArrays +using Xpress, Test +using SparseArrays @testset "MIP 4" begin @@ -39,4 +39,4 @@ using Compat.SparseArrays @test isapprox(get_solution(model), [0, 7, 1]; atol = 1e-3) @test isapprox(get_objval(model), 19; atol = 1e-3) -end \ No newline at end of file +end diff --git a/test/qcqp_01.jl b/test/qcqp_01.jl index 6c45b273..4f2f1c20 100755 --- a/test/qcqp_01.jl +++ b/test/qcqp_01.jl @@ -1,13 +1,13 @@ -# QCQP example +# QCQP example # maximize x + y # # s.t. x, y >= 0 # x^2 + y^2 <= 1 # # solution: (0.71, 0.71) objv = 1.414 -using Xpress, Compat.Test -using Compat.SparseArrays -using Compat.LinearAlgebra +using Xpress, Test +using SparseArrays +using LinearAlgebra @testset "QCQP 1" begin model = Xpress.Model( "qcqp_01", :maximize) @@ -32,4 +32,4 @@ using Compat.LinearAlgebra @test isapprox(get_solution(model), [0.7071, 0.7071]; atol = 1e-3) @test isapprox(get_objval(model), 1.414; atol = 1e-3) -end \ No newline at end of file +end diff --git a/test/qp_01.jl b/test/qp_01.jl index 3a74f181..7289603f 100755 --- a/test/qp_01.jl +++ b/test/qp_01.jl @@ -7,9 +7,9 @@ # # solution: (0, 1), objv = 1.875 # -using Xpress, Compat.Test -using Compat.SparseArrays -using Compat.LinearAlgebra +using Xpress, Test +using SparseArrays +using LinearAlgebra @testset "QP 1" begin model = Xpress.Model("qp_02") @@ -36,4 +36,4 @@ using Compat.LinearAlgebra @test isapprox(get_solution(model), [0.0, 1]; atol = 1e-2) @test isapprox(get_objval(model), 1.5; atol = 1e-3) -end \ No newline at end of file +end diff --git a/test/qp_02.jl b/test/qp_02.jl index 76e266cc..bb1137a4 100755 --- a/test/qp_02.jl +++ b/test/qp_02.jl @@ -6,16 +6,16 @@ # x + y >= 1 # -using Xpress, Compat.Test -using Compat.SparseArrays -using Compat.LinearAlgebra +using Xpress, Test +using SparseArrays +using LinearAlgebra @testset "QP 2" begin - model = xpress_model( - name = "qp_02", + model = xpress_model( + name = "qp_02", f = [0., 0., 0.], H = [2. 1. 0.; 1. 2. 1.; 0. 1. 2.], - A = -[1. 2. 3.; 1. 1. 0.], + A = -[1. 2. 3.; 1. 1. 0.], b = -[4., 1.]) @test Xpress.getq_upper(model) == triu(sparse([2. 1. 0.; 1. 2. 1.; 0. 1. 2.])) @@ -35,4 +35,4 @@ using Compat.LinearAlgebra @test isapprox(get_solution(model), [0.5714, .4285, .8571]; atol = 1e-3) @test isapprox(get_objval(model), 1.857; atol = 1e-3) -end \ No newline at end of file +end diff --git a/test/runtests.jl b/test/runtests.jl index 72350068..e2017fc9 100755 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,7 +1,9 @@ -using Xpress, Compat.Test -using Compat.SparseArrays -using Compat.LinearAlgebra +using Xpress, Test +using SparseArrays +using LinearAlgebra +using Test +#= tests = ["xprs_attrs_test", "lp_01a", "lp_01b", @@ -17,10 +19,11 @@ tests = ["xprs_attrs_test", "MOIWrapper", # "wordhunt" ] +=# -if VERSION < v"0.7.0" - push!(tests, "jump") -end +tests = ["MOI_Wrapper", + # "wordhunt" + ] for t in tests fp = "$(t).jl" diff --git a/test/wordhunt.jl b/test/wordhunt.jl index df36420f..2a32cacb 100644 --- a/test/wordhunt.jl +++ b/test/wordhunt.jl @@ -1,6 +1,6 @@ using JuMP, Xpress using Random -using Compat.Test +using Test # Based on original model implemented in Mosel, courtesy of Truls Flatberg function wordhunt(Words::Array{String,1}, D = [:E,:S,:W, :N, :SE, :NE], Gridsize=7,woptimizer=with_optimizer(Xpress.Optimizer,MIPRELSTOP=0.1),printres=true) @@ -38,14 +38,14 @@ function wordhunt(Words::Array{String,1}, D = [:E,:S,:W, :N, :SE, :NE], Gridsize for i in M, j in M @constraint(model,sum( x[i,j,l] for l in L) <= 1) end - + # Each word is allowed one position and direction (if inserted) for n in N @constraint(model,sum( y[n,i,j,d] for i in M, j in M, d in D) <= 1) end - + # Placement of words - for n in N, i in M, j in M, d in D + for n in N, i in M, j in M, d in D ii = i jj = j for l in 1:length(n) @@ -58,7 +58,7 @@ function wordhunt(Words::Array{String,1}, D = [:E,:S,:W, :N, :SE, :NE], Gridsize elseif d == :SE ii = ii + 1 jj = jj + 1 - elseif d == :NE + elseif d == :NE ii = ii -1 jj = jj +1 elseif d == :W @@ -71,21 +71,21 @@ function wordhunt(Words::Array{String,1}, D = [:E,:S,:W, :N, :SE, :NE], Gridsize end # Objective function - most_words = sum( 2 * Maxlength * y[n,i,j,d] for n in N, i in M, j in M, d in D) + most_words = sum( 2 * Maxlength * y[n,i,j,d] for n in N, i in M, j in M, d in D) least_letters = sum( x[i,j,l] for i in M, j in M, l in L) - + P = [:W,:N,:SE] if length(intersect(P,D)) > 0 pref = sum( y[n,i,j,d] for n in N, i in M, j in M, d in intersect(P,D)) else pref = 0 end - + @objective(model, Max, most_words - least_letters + 0.1 * pref ) - + optimize!(model) - + function printSol(x,L,fillrand=true) letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" for i in 1:size(x,1) @@ -107,18 +107,18 @@ function wordhunt(Words::Array{String,1}, D = [:E,:S,:W, :N, :SE, :NE], Gridsize print(' ') end println() - end + end end - - if printres + + if printres # Show status and objective value print(JuMP.termination_status(model),"\t") println(JuMP.objective_value(model)) println() - + # Print solution to console printSol(x,L,false) - println() + println() printSol(x,L) end @@ -141,4 +141,4 @@ const Words = ["Ada","Claire","Hugo","Idris","Julia","Karel","Mary","Max","Maya" # Test combinations of miprelstop and maxtime @test wordhunt(Words,[:E,:W,:S,:SE,:NE],7,with_optimizer(Xpress.Optimizer,MIPRELSTOP=.01,MAXTIME=1),false) == MOI.TIME_LIMIT @test wordhunt(Words,[:E,:W,:S,:SE,:NE],7,with_optimizer(Xpress.Optimizer,MIPRELSTOP=.9,MAXTIME=15),false) == MOI.OPTIMAL -end \ No newline at end of file +end diff --git a/test/xprs_attrs_test.jl b/test/xprs_attrs_test.jl index 49f21ef8..2b1726ad 100644 --- a/test/xprs_attrs_test.jl +++ b/test/xprs_attrs_test.jl @@ -1,4 +1,4 @@ -using Xpress, Compat.Test +using Xpress, Test m = Xpress.Model() @@ -7,4 +7,3 @@ m = Xpress.Model() @test Xpress.get_dblattr(m, Xpress.XPRS_OBJRHS) == 0.0 @test Xpress.get_strattr(m, Xpress.XPRS_MATRIXNAME) == "" end -