-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add MathProgBase solver interface. #9
Merged
Merged
Changes from 7 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
0d9ac6c
Add MathProgBase solver interface.
tkoolen 4f14108
Add MathProgBase solver interface.
tkoolen c27d244
Address review comments.
tkoolen df2f7e0
Copy over relevant parts of MathProgBase test/quadprog.jl.
tkoolen 9b061b6
Refine/implement more of MathProgBase interface.
tkoolen de19e17
Upper bound on MPB.
tkoolen 1510ae3
Added mathprog tests. Basic restructuring. Still need to complete the…
bstellato bce0012
Merged both changes but still need to finish the interface
bstellato 4311dfd
Deleted solver interface in mathprog old file
bstellato ff8f248
Improve test coverage.
tkoolen 938a50b
Add more tests, fix objective bug.
tkoolen 683a6e5
Merged tkoolen changes
bstellato 381c761
Added changes for proper updates without performing setup again. Trie…
bstellato 14e0d97
Added update_settings! to setparameters
bstellato f4b4af9
First tests working but still getting segfaults when I close julia
bstellato 7a46756
Apparently setwarmstart! causes troubles
bstellato 23f3202
Got Twan tests working
bstellato ec0ab49
Got more tests working. Need to fix last ones in linproginterface
bstellato ed9b220
Fixed some bugs
bstellato 1299cc5
Got all mathprogbase tests to work. Fixed dual variables signs
bstellato d11c474
Other adjustments based on comments
bstellato 5094552
Fixed setquadobj! to reset problem status forcing a new solve
bstellato 47f7010
Edited changelog before merging
bstellato File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
BinDeps | ||
julia 0.6 | ||
Compat | ||
MathProgBase 0.7 0.8 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
module OSQPMathProgBaseInterface | ||
|
||
import MathProgBase | ||
import OSQP | ||
|
||
import MathProgBase: numvar, numconstr | ||
|
||
struct OSQPSolver <: MathProgBase.AbstractMathProgSolver | ||
settings::Dict{Symbol,Any} | ||
end | ||
|
||
OSQPSolver(; kwargs...) = OSQPSolver(Dict{Symbol, Any}(k => v for (k, v) in kwargs)) | ||
|
||
mutable struct OSQPModel <: MathProgBase.AbstractLinearQuadraticModel | ||
settings::Dict{Symbol, Any} | ||
inner::OSQP.Model | ||
|
||
P::SparseMatrixCSC{Float64, Int} | ||
q::Vector{Float64} | ||
A::SparseMatrixCSC{Float64, Int} | ||
l::Vector{Float64} | ||
u::Vector{Float64} | ||
|
||
xwarmstart::Vector{Float64} | ||
dowarmstart::Bool | ||
|
||
results::OSQP.Results | ||
|
||
function OSQPModel(settings::Associative{Symbol, Any}) | ||
P = spzeros(Float64, 0, 0) | ||
q = Float64[] | ||
A = spzeros(Float64, 0, 0) | ||
l = Float64[] | ||
u = Float64[] | ||
xwarmstart = Float64[] | ||
dowarmstart = false | ||
new(copy(settings), OSQP.Model(), P, q, A, l, u, xwarmstart, dowarmstart) # leave results undefined | ||
end | ||
end | ||
|
||
function Base.resize!(model::OSQPModel, n, m) | ||
nchange = n != numvar(model) | ||
mchange = m != numconstr(model) | ||
if nchange | ||
model.P = spzeros(Float64, n, n) | ||
resize!(model.q, n) | ||
resize!(model.xwarmstart, n) | ||
model.dowarmstart = false | ||
end | ||
if mchange | ||
resize!(model.l, m) | ||
resize!(model.u, m) | ||
end | ||
if nchange || mchange | ||
model.A = spzeros(Float64, m, n) | ||
end | ||
|
||
model | ||
end | ||
|
||
checksolved(model::OSQPModel) = isdefined(model, :results) || error("Model has not been solved.") | ||
variablebounderror() = error("Variable bounds are not supported (use constraints instead).") | ||
senseerror() = error("Only objective sense :Min is currently supported to avoid confusing behavior.") | ||
|
||
# http://mathprogbasejl.readthedocs.io/en/latest/solverinterface.html | ||
MathProgBase.LinearQuadraticModel(solver::OSQPSolver) = OSQPModel(solver.settings) | ||
MathProgBase.getsolution(model::OSQPModel) = (checksolved(model); model.results.x) | ||
MathProgBase.getobjval(model::OSQPModel) = (checksolved(model); model.results.info.obj_val) | ||
|
||
function MathProgBase.optimize!(model::OSQPModel) | ||
OSQP.setup!(model.inner; P = model.P, q = model.q, A = model.A, l = model.l, u = model.u, model.settings...) | ||
model.dowarmstart && OSQP.warm_start!(model.inner, x = model.xwarmstart) | ||
model.results = OSQP.solve!(model.inner) | ||
end | ||
|
||
function MathProgBase.status(model::OSQPModel)::Symbol | ||
checksolved(model) | ||
osqpstatus = model.results.info.status | ||
status = osqpstatus # if OSQP status can't be mapped to a standard return value, just return as is | ||
|
||
# map to standard return status values: | ||
osqpstatus == :Solved && (status = :Optimal) | ||
osqpstatus == :Max_iter_reached && (status = :UserLimit) | ||
osqpstatus == :Interrupted && (status = :UserLimit) # following Gurobi.jl | ||
osqpstatus == :Primal_infeasible && (status = :Infeasible) | ||
osqpstatus == :Primal_infeasible_inaccurate && (status = :Infeasible) | ||
osqpstatus == :Dual_infeasible && (status = :DualityFailure) | ||
|
||
return status | ||
end | ||
|
||
# TODO: getobjbound | ||
# TODO: getobjgap | ||
MathProgBase.getrawsolver(model::OSQPModel) = model.inner | ||
MathProgBase.getsolvetime(model::OSQPModel) = (checksolved(model); model.results.info.run_time) | ||
MathProgBase.setsense!(model::OSQPModel, sense::Symbol) = sense == :Min || senseerror() | ||
MathProgBase.getsense(model::OSQPModel) = :Min | ||
MathProgBase.numvar(model::OSQPModel) = size(model.A, 2) | ||
MathProgBase.numconstr(model::OSQPModel) = size(model.A, 1) | ||
MathProgBase.freemodel!(model::OSQPModel) = OSQP.clean!(model.inner) | ||
|
||
function Base.copy(model::OSQPModel) | ||
ret = OSQPModel(model.settings) | ||
resize!(ret, numvar(model), numconstr(model)) | ||
copy!(ret.P, model.P) | ||
copy!(ret.q, model.q) | ||
copy!(ret.A, model.A) | ||
copy!(ret.l, model.l) | ||
copy!(ret.u, model.u) | ||
ret | ||
end | ||
|
||
MathProgBase.setvartype!(model::OSQPModel, v::Vector{Symbol}) = any(x -> x != :Cont, v) && error("OSQP only supports continuous variables.") | ||
MathProgBase.getvartype(model::OSQPModel) = fill(:Cont, numvar(model)) | ||
|
||
function MathProgBase.setparameters!(x::Union{OSQPSolver, OSQPModel}; Silent = nothing) | ||
if Silent != nothing | ||
Silent::Bool | ||
x.settings[:verbose] = !Silent | ||
end | ||
x | ||
end | ||
|
||
MathProgBase.setwarmstart!(model::OSQPModel, v) = (copy!(model.xwarmstart, v); model.dowarmstart = true) | ||
|
||
|
||
# http://mathprogbasejl.readthedocs.io/en/latest/lpqcqp.html#linearquadratic-models | ||
# TODO: loadproblem!(m::AbstractLinearQuadraticModel, filename::String) | ||
|
||
function MathProgBase.loadproblem!(model::OSQPModel, A, l, u, c, lb, ub, sense) | ||
(any(x -> x != -Inf, l) || any(x -> x != Inf, u)) && variablebounderror() | ||
sense == :Min || senseerror() | ||
m, n = size(A) | ||
resize!(model, n, m) | ||
copy!(model.q, c) | ||
copy!(model.l, lb) | ||
copy!(model.u, ub) | ||
copy!(model.A, A) | ||
model | ||
end | ||
|
||
# TODO: writeproblem | ||
MathProgBase.getvarLB(model::OSQPModel) = fill(-Inf, numvar(model)) | ||
MathProgBase.setvarLB!(model::OSQPModel, l) = variablebounderror() | ||
MathProgBase.getvarUB(model::OSQPModel) = fill(Inf, numvar(model)) | ||
MathProgBase.setvarUB!(model::OSQPModel, l) = variablebounderror() | ||
MathProgBase.getconstrLB(model::OSQPModel) = model.l | ||
MathProgBase.setconstrLB!(model::OSQPModel, lb) = (copy!(model.l, lb); model) | ||
MathProgBase.getconstrUB(model::OSQPModel) = model.u | ||
MathProgBase.setconstrUB!(model::OSQPModel, ub) = (copy!(model.u, ub); model) | ||
MathProgBase.setobj!(model::OSQPModel, c) = (copy!(model.q, c); model) | ||
MathProgBase.getconstrmatrix(model::OSQPModel) = model.A | ||
# TODO: addvar! | ||
# TODO: delvars! | ||
# TODO: addconstr! | ||
# TODO: delconstrs! | ||
# TODO: changecoeffs! | ||
MathProgBase.numlinconstr(model::OSQPModel) = numconstr(model) | ||
MathProgBase.getconstrsolution(model::OSQPModel) = model.A * getsolution(model) | ||
# TODO: getreducedcosts | ||
MathProgBase.getconstrduals(model::OSQPModel) = (checksolved(model); model.results.y) | ||
# TODO: getinfeasibilityray | ||
# TODO: getbasis | ||
# TODO: getunboundedray | ||
|
||
|
||
# http://mathprogbasejl.readthedocs.io/en/latest/lpqcqp.html#quadratic-programming | ||
MathProgBase.numquadconstr(model::OSQPModel) = 0 | ||
MathProgBase.setquadobj!(model::OSQPModel, Q::Matrix) = (copy!(model.P, Q); model) | ||
|
||
function MathProgBase.setquadobj!(model::OSQPModel, rowidx::Vector, colidx::Vector, quadval::Vector) | ||
nterms = length(quadval) | ||
@boundscheck length(rowidx) == nterms || error() | ||
@boundscheck length(colidx) == nterms || error() | ||
|
||
# zero out coeffs | ||
for i = 1 : nterms | ||
@inbounds row = rowidx[i] | ||
@inbounds col = colidx[i] | ||
model.P[row, col] = 0 | ||
end | ||
|
||
# add new coeffs | ||
for i = 1 : nterms | ||
@inbounds row = rowidx[i] | ||
@inbounds col = colidx[i] | ||
@inbounds val = quadval[i] | ||
model.P[row, col] += val | ||
model.P[col, row] = model.P[row, col] | ||
end | ||
|
||
model | ||
end | ||
|
||
# Note: skipping quadconstr methods | ||
|
||
end # module |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
using MathProgBase | ||
import MathProgBase: LinearQuadraticModel, loadproblem!, setquadobj!, optimize!, status, numvar, numconstr, setwarmstart!, getsense, | ||
getobjval, getsolution, setsense!, getvarLB, setvarLB!, getvarUB, setvarUB!, getconstrLB, setconstrLB!, getconstrUB, setconstrUB!, | ||
getconstrmatrix, numlinconstr, getsolvetime, getrawsolver, getvartype, setvartype!, getconstrduals | ||
|
||
@testset "MathProgBase" begin | ||
solver = OSQPMathProgBaseInterface.OSQPSolver(eps_abs = 1e-7, eps_rel = 1e-16) | ||
MathProgBase.setparameters!(solver, Silent=true) | ||
|
||
@testset "quadprog" begin | ||
# modified from joinpath(Pkg.dir("MathProgBase"), "test", "quadprog.jl"): | ||
sol = quadprog([0., 0., 0.],[2. 1. 0.; 1. 2. 1.; 0. 1. 2.],[1. 2. 3.; 1. 1. 0.],'>',[4., 1.],-Inf,Inf,solver) | ||
@test sol.status == :Optimal | ||
@test isapprox(sol.objval, 130/70, atol=1e-6) | ||
@test isapprox(norm(sol.sol[1:3] - [0.5714285714285715,0.4285714285714285,0.8571428571428572]), 0.0, atol=1e-6) | ||
end | ||
|
||
@testset "QP1" begin | ||
# modified from joinpath(Pkg.dir("MathProgBase"), "test", "quadprog.jl"): | ||
m = LinearQuadraticModel(solver) | ||
loadproblem!(m, [1. 2. 3.; 1. 1. 0.],[-Inf,-Inf,-Inf],[Inf,Inf,Inf],[0.,0.,0.],[4., 1.],[Inf,Inf], :Min) | ||
|
||
setquadobj!(m,diagm([10.0,10.0,10.0])) | ||
rows = [1, 2, 2, 2, 3, 3, 3] | ||
cols = [1, 1, 1, 2, 2, 3, 3] | ||
vals = Float64[2, 0.5, 0.5, 2, 1, 1, 1] | ||
setquadobj!(m,rows,cols,vals) | ||
m2 = copy(m) | ||
|
||
verify_solution = function (m) | ||
stat = status(m) | ||
@test stat == :Optimal | ||
@test isapprox(getobjval(m), 130/70, atol=1e-6) | ||
@test isapprox(norm(getsolution(m) - [0.5714285714285715,0.4285714285714285,0.8571428571428572]), 0.0, atol=1e-6) | ||
@test getsolvetime(m) > 0 | ||
end | ||
|
||
optimize!(m) | ||
verify_solution(m) | ||
|
||
setwarmstart!(m2, getsolution(m) .+ 0.1) | ||
optimize!(m2) | ||
verify_solution(m2) | ||
end | ||
|
||
@testset "basic_QP" begin | ||
# same QP as test/basic.jl: "basic_QP" but using MathProgBase | ||
P = sparse([11. 0.; 0. 0.]) | ||
q = [3.; 4] | ||
A = sparse([-1 0; 0 -1; -1 -3; 2 5; 3 4]) | ||
u = [0.; 0.; -15; 100; 80] | ||
l = fill(-Inf, length(u)) | ||
|
||
verify_solution = function (m) | ||
tol = 1e-5 | ||
@show getsolution(m) | ||
@show getconstrduals(m) | ||
@show getobjval(m) | ||
println() | ||
|
||
@test isapprox(norm(getsolution(m) - [0.; 5.]), 0., atol=tol) | ||
@test isapprox(norm(getconstrduals(m) - [1.666666666666; 0.; 1.3333333; 0.; 0.]), 0., atol=tol) | ||
@test isapprox(getobjval(m), 20., atol=tol) | ||
end | ||
|
||
m1 = LinearQuadraticModel(solver) | ||
loadproblem!(m1, A, [-Inf, -Inf], [Inf, Inf], q, l, u, :Min) | ||
m2 = copy(m1) | ||
setquadobj!(m1, P) | ||
optimize!(m1) | ||
verify_solution(m1) | ||
|
||
setquadobj!(m2, findnz(triu(P))...) # triu to avoid duplicate elements | ||
optimize!(m2) | ||
verify_solution(m2) | ||
end | ||
|
||
@testset "Unsupported behavior" begin | ||
m = LinearQuadraticModel(solver) | ||
@test_throws ErrorException setsense!(m, :Max) | ||
@test_throws ErrorException loadproblem!(m, spzeros(0, 2), [-Inf, -Inf], [Inf, Inf], [1.; 1.], Float64[], Float64[], :Max) # maximization not supported | ||
@test_throws ErrorException loadproblem!(m, spzeros(0, 2), [-1., -Inf], [Inf, Inf], [1.; 1.], Float64[], Float64[], :Min) # variable bounds not supported | ||
@test_throws ErrorException loadproblem!(m, spzeros(0, 2), [-Inf, -Inf], [10., Inf], [1.; 1.], Float64[], Float64[], :Min) # variable bounds not supported | ||
|
||
loadproblem!(m, [1. 2. 3.; 1. 1. 0.],[-Inf,-Inf,-Inf],[Inf,Inf,Inf],[0.,0.,0.],[4., 1.],[Inf,Inf], :Min) | ||
@test_throws ErrorException setvarLB!(m, rand(numvar(m))) | ||
@test_throws ErrorException setvarUB!(m, rand(numvar(m))) | ||
|
||
@test_throws ErrorException setvartype!(m, [:Cont, :Bin, :Cont]) | ||
end | ||
|
||
@testset "Getters/setters" begin | ||
m = LinearQuadraticModel(solver) | ||
loadproblem!(m, [1. 2. 3.; 1. 1. 0.],[-Inf,-Inf,-Inf],[Inf,Inf,Inf],[0.,0.,0.],[4., 1.],[Inf,Inf], :Min) | ||
|
||
@test getrawsolver(m) isa OSQP.Model | ||
|
||
@test getsense(m) == :Min | ||
|
||
@test getvarLB(m) == [-Inf, -Inf, -Inf] | ||
@test getvarUB(m) == [Inf, Inf, Inf] | ||
|
||
@test getconstrLB(m) == [4., 1.] | ||
setconstrLB!(m, [-4., Inf]) | ||
@test getconstrLB(m) == [-4., Inf] | ||
@test getconstrUB(m) == [Inf, Inf] | ||
setconstrUB!(m, [5., 8.]) | ||
@test getconstrUB(m) == [5., 8.] | ||
|
||
@test getconstrmatrix(m) == [1. 2. 3.; 1. 1. 0.] | ||
@test numlinconstr(m) == 2 | ||
|
||
@test getvartype(m) == [:Cont, :Cont, :Cont] | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the
resize!
function really necessary? it seems that it does not properly resize the problem but it just creates zero matricesP
andA
and resizes the vectors. I guess this is needed in theloadproblem!
function where you callcopy!
. Is this necessary/more efficient than just usingcopy
ordeepcopy
for the data?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FTR: answered on gitter. Yes, efficiency.