diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba39cc5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +Manifest.toml diff --git a/.travis.yml b/.travis.yml index b64752b..f1f42aa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,11 +3,9 @@ os: - linux - osx julia: - - 0.6 - - 0.7 - 1.0 + - 1.1 notifications: email: false after_success: - - julia -e 'cd(Pkg.dir("ECOS")); Pkg.add("Coverage"); using Coverage; Coveralls.submit(Coveralls.process_folder())' - - julia -e 'cd(Pkg.dir("ECOS")); Pkg.add("Coverage"); using Coverage; Codecov.submit(Codecov.process_folder())' + - julia -e 'import Pkg; Pkg.add("Coverage"); using Coverage; Coveralls.submit(process_folder()); Codecov.submit(process_folder())' diff --git a/Project.toml b/Project.toml new file mode 100644 index 0000000..1b2a2cc --- /dev/null +++ b/Project.toml @@ -0,0 +1,25 @@ +name = "ECOS" +uuid = "e2685f51-7e38-5353-a97d-a921fd2c8199" +repo = "https://github.com/JuliaOpt/ECOS.jl.git" +version = "0.10.0" + +[deps] +BinaryProvider = "b99e7846-7c00-51b0-8f62-c81ae34c0232" +Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" +Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +MathProgBase = "fdba3010-5040-5b88-9595-932c9decdf73" +SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" + +[compat] +BinaryProvider = "≥ 0.3.0" +MathOptInterface = "0.9" +MathProgBase = "~0.5.0, ~0.6, ~0.7" +julia = "1" + +[extras] +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Test"] diff --git a/REQUIRE b/REQUIRE deleted file mode 100644 index 7a4f46f..0000000 --- a/REQUIRE +++ /dev/null @@ -1,5 +0,0 @@ -julia 0.6 -MathProgBase 0.5 0.8 -MathOptInterface 0.8.2 0.9 -Compat 0.68 -BinaryProvider 0.3 diff --git a/appveyor.yml b/appveyor.yml index 1deff5c..a238469 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,8 +1,7 @@ environment: matrix: - - julia_version: 0.6 - - julia_version: 0.7 - julia_version: 1.0 + - julia_version: 1.1 platform: - x86 # 32-bit diff --git a/src/ECOS.jl b/src/ECOS.jl index cfdb14b..302cb2a 100644 --- a/src/ECOS.jl +++ b/src/ECOS.jl @@ -7,12 +7,10 @@ # Contains the wrapper itself ############################################################################# -__precompile__() module ECOS -using Compat -using Compat.SparseArrays -using Compat.LinearAlgebra +using SparseArrays +using LinearAlgebra # Try to load the binary dependency if isfile(joinpath(dirname(@__FILE__),"..","deps","deps.jl")) diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index d278a23..08b6c16 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -11,12 +11,14 @@ struct Solution dual_eq::Vector{Float64} dual_ineq::Vector{Float64} slack::Vector{Float64} - objval::Float64 - objbnd::Float64 + objective_value::Float64 + dual_objective_value::Float64 + objective_constant::Float64 + solve_time::Float64 end const OPTIMIZE_NOT_CALLED = -1 Solution() = Solution(OPTIMIZE_NOT_CALLED, Float64[], Float64[], Float64[], - Float64[], NaN, NaN) + Float64[], NaN, NaN, NaN, NaN) # Used to build the data with allocate-load during `copy_to`. # When `optimize!` is called, a the data is used to build `ECOSMatrix` @@ -32,7 +34,7 @@ mutable struct ModelData JG::Vector{Int} # List of equality cols VG::Vector{Float64} # List of equality coefficients h::Vector{Float64} # List of equality coefficients - objconstant::Float64 # The objective is min c'x + objconstant + objective_constant::Float64 # The objective is min c'x + objective_constant c::Vector{Float64} end @@ -58,14 +60,33 @@ mutable struct Optimizer <: MOI.AbstractOptimizer maxsense::Bool data::Union{Nothing, ModelData} # only non-Nothing between MOI.copy_to and MOI.optimize! sol::Solution - options + silent::Bool + options::Dict{Symbol, Any} function Optimizer(; kwargs...) - new(ConeData(), false, nothing, Solution(), kwargs) + optimizer = new(ConeData(), false, nothing, Solution(), false, Dict{Symbol, Any}()) + for (key, value) in kwargs + MOI.set(optimizer, MOI.RawParameter(key), value) + end + return optimizer end end MOI.get(::Optimizer, ::MOI.SolverName) = "ECOS" +function MOI.set(optimizer::Optimizer, param::MOI.RawParameter, value) + optimizer.options[param.name] = value +end +function MOI.get(optimizer::Optimizer, param::MOI.RawParameter) + # TODO: This gives a poor error message if the name of the parameter is invalid. + return optimizer.options[param.name] +end + +MOI.supports(::Optimizer, ::MOI.Silent) = true +function MOI.set(optimizer::Optimizer, ::MOI.Silent, value::Bool) + optimizer.silent = value +end +MOI.get(optimizer::Optimizer, ::MOI.Silent) = optimizer.silent + function MOI.is_empty(instance::Optimizer) !instance.maxsense && instance.data === nothing end @@ -97,7 +118,7 @@ function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike; kws...) return MOIU.automatic_copy_to(dest, src; kws...) end -using Compat.SparseArrays +using SparseArrays # Computes cone dimensions constroffset(cone::ConeData, ci::CI{<:MOI.AbstractFunction, MOI.Zeros}) = ci.value @@ -152,7 +173,7 @@ expmap(i) = (1, 3, 2)[i] function orderidx(idx, s::MOI.ExponentialCone) expmap.(idx) end -function MOIU.load_constraint(instance::Optimizer, ci, f::MOI.VectorAffineFunction, s::MOI.AbstractVectorSet) +function MOIU.load_constraint(instance::Optimizer, ci::MOI.ConstraintIndex, f::MOI.VectorAffineFunction, s::MOI.AbstractVectorSet) A = sparse(output_index.(f.terms), variable_index_value.(f.terms), coefficient.(f.terms)) # sparse combines duplicates with + but does not remove zeros created so we call dropzeros! dropzeros!(A) @@ -214,7 +235,7 @@ function MOIU.load(instance::Optimizer, ::MOI.ObjectiveFunction, f::MOI.ScalarAffineFunction) c0 = Vector(sparsevec(variable_index_value.(f.terms), coefficient.(f.terms), instance.data.n)) - instance.data.objconstant = f.constant + instance.data.objective_constant = f.constant instance.data.c = instance.maxsense ? -c0 : c0 return nothing end @@ -231,28 +252,61 @@ function MOI.optimize!(instance::Optimizer) b = instance.data.b G = ECOS.ECOSMatrix(sparse(instance.data.IG, instance.data.JG, instance.data.VG, m, n)) h = instance.data.h - objconstant = instance.data.objconstant + objective_constant = instance.data.objective_constant c = instance.data.c instance.data = nothing # Allows GC to free instance.data before A is loaded to ECOS + options = instance.options + if instance.silent + options = copy(options) + options[:verbose] = false + end ecos_prob_ptr = ECOS.setup(n, m, cone.f, cone.l, length(cone.qa), cone.qa, - cone.ep, G, A, c, h, b; instance.options...) + cone.ep, G, A, c, h, b; options...) ret_val = ECOS.solve(ecos_prob_ptr) + stat = unsafe_load(unsafe_load(ecos_prob_ptr).info) + solve_time = stat.tsetup + stat.tsolve ecos_prob = unsafe_wrap(Array, ecos_prob_ptr, 1)[1] primal = unsafe_wrap(Array, ecos_prob.x, n)[:] dual_eq = unsafe_wrap(Array, ecos_prob.y, cone.f)[:] dual_ineq = unsafe_wrap(Array, ecos_prob.z, m)[:] slack = unsafe_wrap(Array, ecos_prob.s, m)[:] ECOS.cleanup(ecos_prob_ptr, 0) - objval = (instance.maxsense ? -1 : 1) * dot(c, primal) - if ret_val != ECOS.ECOS_DINF - objval += objconstant - end - objbnd = -(dot(b, dual_eq) + dot(h, dual_ineq)) - if ret_val != ECOS.ECOS_PINF - objbnd += objconstant + objective_value = (instance.maxsense ? -1 : 1) * stat.pcost + dual_objective_value = (instance.maxsense ? -1 : 1) * stat.dcost + instance.sol = Solution(ret_val, primal, dual_eq, dual_ineq, slack, objective_value, + dual_objective_value, objective_constant, solve_time) +end + +MOI.get(optimizer::Optimizer, ::MOI.SolveTime) = optimizer.sol.solve_time +function MOI.get(optimizer::Optimizer, ::MOI.RawStatusString) + # Strings from https://github.com/ifa-ethz/ecos/blob/master/include/ecos.h + flag = optimizer.sol.ret_val + if flag == OPTIMIZE_NOT_CALLED + return "Optimize not called" + elseif flag == ECOS_OPTIMAL + return "Problem solved to optimality" + elseif flag == ECOS_OPTIMAL + ECOS_INACC_OFFSET + return "Problem solved to inaccurate optimality" + elseif flag == ECOS_PINF + return "Found certificate of primal infeasibility" + elseif flag == ECOS_PINF + ECOS_INACC_OFFSET + return "Found inaccurate certificate of primal infeasibility" + elseif flag == ECOS_DINF + return "Found certificate of dual infeasibility" + elseif flag == ECOS_DINF + ECOS_INACC_OFFSET + return "Found inaccurate certificate of dual infeasibility" + elseif flag == ECOS_MAXIT + return "Maximum number of iterations reached" + elseif flag == ECOS_NUMERICS + return "Search direction unreliable" + elseif flag == ECOS_OUTCONE + return "s or z got outside the cone, numerics?" + elseif flag == ECOS_SIGINT + return "solver interrupted by a signal/ctrl-c" + else + @assert flag == ECOS_FATAL + return "Unknown problem in solver" end - instance.sol = Solution(ret_val, primal, dual_eq, dual_ineq, slack, objval, - objbnd) end # Implements getter for result value and statuses @@ -279,8 +333,20 @@ function MOI.get(instance::Optimizer, ::MOI.TerminationStatus) end end -MOI.get(instance::Optimizer, ::MOI.ObjectiveValue) = instance.sol.objval -MOI.get(instance::Optimizer, ::MOI.ObjectiveBound) = instance.sol.objbnd +function MOI.get(optimizer::Optimizer, ::MOI.ObjectiveValue) + value = optimizer.sol.objective_value + if !MOIU.is_ray(MOI.get(optimizer, MOI.PrimalStatus())) + value += optimizer.sol.objective_constant + end + return value +end +function MOI.get(optimizer::Optimizer, ::MOI.DualObjectiveValue) + value = optimizer.sol.dual_objective_value + if !MOIU.is_ray(MOI.get(optimizer, MOI.DualStatus())) + value += optimizer.sol.objective_constant + end + return value +end function MOI.get(instance::Optimizer, ::MOI.PrimalStatus) flag = instance.sol.ret_val diff --git a/test/MOI_wrapper.jl b/test/MOI_wrapper.jl index 103dc8f..f39393b 100644 --- a/test/MOI_wrapper.jl +++ b/test/MOI_wrapper.jl @@ -1,5 +1,4 @@ -using Compat -using Compat.Test +using Test using MathOptInterface const MOI = MathOptInterface @@ -8,7 +7,8 @@ const MOIU = MOI.Utilities const MOIB = MOI.Bridges import ECOS -const optimizer = ECOS.Optimizer(verbose=false) +const optimizer = ECOS.Optimizer() +MOI.set(optimizer, MOI.Silent(), true) @testset "SolverName" begin @test MOI.get(optimizer, MOI.SolverName()) == "ECOS" @@ -19,18 +19,10 @@ end @test !MOIU.supports_allocate_load(optimizer, true) end -MOIU.@model(ECOSModelData, - (), (), # No scalar functions - (MOI.Zeros, MOI.Nonnegatives, MOI.Nonpositives, MOI.SecondOrderCone, - MOI.ExponentialCone), - (), - (), (), # No scalar sets - (), (MOI.VectorAffineFunction,)) # UniversalFallback is needed for starting values, even if they are ignored by ECOS -const cache = MOIU.UniversalFallback(ECOSModelData{Float64}()) +const cache = MOIU.UniversalFallback(MOIU.Model{Float64}()) const cached = MOIU.CachingOptimizer(cache, optimizer) -# Essential bridges that are needed for all tests const bridged = MOIB.full_bridge_optimizer(cached, Float64) # SOC2 requires 1e-4 @@ -39,10 +31,16 @@ const config = MOIT.TestConfig(atol=1e-4, rtol=1e-4) @testset "Unit" begin MOIT.unittest(bridged, config, - [# Need https://github.com/JuliaOpt/MathOptInterface.jl/issues/529 - "solve_qp_edge_cases", - # Integer and ZeroOne sets are not supported - "solve_integer_edge_cases", "solve_objbound_edge_cases"]) + [ + # `TimeLimitSec` not supported. + "time_limit_sec", + # Need https://github.com/JuliaOpt/MathOptInterface.jl/issues/529 + "solve_qp_edge_cases", + # Integer and ZeroOne sets are not supported + "solve_integer_edge_cases", "solve_objbound_edge_cases", + "solve_zero_one_with_bounds_1", + "solve_zero_one_with_bounds_2", + "solve_zero_one_with_bounds_3"]) end @testset "Continuous linear problems" begin @@ -54,6 +52,6 @@ end end @testset "Continuous conic problems" begin - exclude = ["sdp", "rootdet", "logdet"] + exclude = ["pow", "sdp", "rootdet", "logdet"] MOIT.contconictest(bridged, config, exclude) end diff --git a/test/MPB_linear.jl b/test/MPB_linear.jl index 6e435bf..dba1a93 100644 --- a/test/MPB_linear.jl +++ b/test/MPB_linear.jl @@ -7,8 +7,8 @@ # Test the MathProgBase.jl interface for the ECOS.jl solver wrapper ############################################################################# -using Compat.Test -using Compat.LinearAlgebra +using Test +using LinearAlgebra using MathProgBase using ECOS diff --git a/test/direct.jl b/test/direct.jl index 70c0d6a..4c53e66 100644 --- a/test/direct.jl +++ b/test/direct.jl @@ -7,9 +7,9 @@ # Test the direct interface for the ECOS.jl solver wrapper ############################################################################# -using Compat.Test -using Compat.LinearAlgebra -using Compat.SparseArrays +using Test +using LinearAlgebra +using SparseArrays # The values below are copied from data.h in ECOS source code import ECOS diff --git a/test/options.jl b/test/options.jl index b50e76f..2f6bd40 100644 --- a/test/options.jl +++ b/test/options.jl @@ -8,7 +8,7 @@ ############################################################################# import ECOS -using Compat.Test +using Test println(ECOS.ver()) diff --git a/test/stress.jl b/test/stress.jl index be34ae7..c9af333 100644 --- a/test/stress.jl +++ b/test/stress.jl @@ -1,5 +1,5 @@ # Generates a random cone problem to test the MPB interface perf -using Compat.SparseArrays +using SparseArrays rows = 10000 cols = 10000