Skip to content
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

🚸 OSQPOptimizer -> OSQP.Optimizer #34

Merged
merged 3 commits into from Oct 15, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
96 changes: 52 additions & 44 deletions src/MOIWrapper.jl
Expand Up @@ -8,7 +8,7 @@ using Compat.SparseArrays
using MathOptInterface
using MathOptInterface.Utilities

export OSQPOptimizer, OSQPSettings, OSQPModel
export Optimizer, OSQPSettings, OSQPModel

const MOI = MathOptInterface
const MOIU = MathOptInterface.Utilities
Expand Down Expand Up @@ -59,7 +59,7 @@ function _contains(haystack, needle)
false
end

mutable struct OSQPOptimizer <: MOI.AbstractOptimizer
mutable struct Optimizer <: MOI.AbstractOptimizer
inner::OSQP.Model
hasresults::Bool
results::OSQP.Results
Expand All @@ -72,7 +72,7 @@ mutable struct OSQPOptimizer <: MOI.AbstractOptimizer
warmstartcache::WarmStartCache{Float64}
rowranges::Dict{Int, UnitRange{Int}}

function OSQPOptimizer()
function Optimizer()
inner = OSQP.Model()
hasresults = false
results = OSQP.Results()
Expand All @@ -88,9 +88,17 @@ mutable struct OSQPOptimizer <: MOI.AbstractOptimizer
end
end

hasresults(optimizer::OSQPOptimizer) = optimizer.hasresults
# used to smooth out transition of OSQP v0.4 -> v0.5, TODO: remove on OSQP v0.6
export OSQPOptimizer
function OSQPOptimizer()
Base.depwarn("OSQPOptimizer() is deprecated, use OSQP.Optimizer() instead.",
:OSQPOptimizer)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this function return Optimizer() to avoid breaking existing code?

Copy link
Collaborator

@tkoolen tkoolen Oct 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does actually:

julia> using OSQP.MathOptInterfaceOSQP
[ Info: Precompiling OSQP [ab2f91bb-94b4-55e3-9ba0-7f65df51de79]

julia> optimizer = OSQPOptimizer()
┌ Warning: OSQPOptimizer() is deprecated, use OSQP.Optimizer() instead.
│   caller = top-level scope at none:0
└ @ Core none:0
Optimizer(OSQP.Model(Ptr{OSQP.Workspace} @0x0000000000000000, Float64[], Float64[]), false, OSQP.Results(Float64[], Float64[], OSQP.Info(140496452123152, #undef, 569348, 3, 1.5e-323, 0.0, 0.0, 0.0, 6.94145239783165e-310, 6.94144482996333e-310, 6.9414448298977e-310, 0, 0.0), Float64[], Float64[]), true, Dict{Symbol,Any}(), MinSense::OptimizationSense = 0, 0.0, Float64[], OSQP.MathOptInterfaceOSQP.ModificationCaches.ProblemModificationCache{Float64}(#undef, #undef, #undef, #undef, #undef), OSQP.MathOptInterfaceOSQP.ModificationCaches.WarmStartCache{Float64}(#undef, #undef), Dict{Int64,UnitRange{Int64}}())

Note that this is actually exactly equivalent to

Base.@deprecate OSQPOptimizer() OSQP.Optimizer()

No need to change this in this PR, just FYI.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I hadn't noticed b52ad84.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot about this macro, @tkoolen thanks for the tip :)

return Optimizer()
end

hasresults(optimizer::Optimizer) = optimizer.hasresults

function MOI.empty!(optimizer::OSQPOptimizer)
function MOI.empty!(optimizer::Optimizer)
optimizer.inner = OSQP.Model()
optimizer.hasresults = false
optimizer.results = OSQP.Results()
Expand All @@ -104,9 +112,9 @@ function MOI.empty!(optimizer::OSQPOptimizer)
optimizer
end

MOI.is_empty(optimizer::OSQPOptimizer) = optimizer.is_empty
MOI.is_empty(optimizer::Optimizer) = optimizer.is_empty

function MOI.copy_to(dest::OSQPOptimizer, src::MOI.ModelLike; copy_names=false)
function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike; copy_names=false)
copy_names && error("Copying names is not supported.")
MOI.empty!(dest)
idxmap = MOIU.IndexMap(dest, src)
Expand All @@ -125,7 +133,7 @@ end
"""
Set up index map from `src` variables and constraints to `dest` variables and constraints.
"""
function MOIU.IndexMap(dest::OSQPOptimizer, src::MOI.ModelLike)
function MOIU.IndexMap(dest::Optimizer, src::MOI.ModelLike)
idxmap = MOIU.IndexMap()
vis_src = MOI.get(src, MOI.ListOfVariableIndices())
for i in eachindex(vis_src)
Expand Down Expand Up @@ -163,7 +171,7 @@ function constraint_rows(rowranges::Dict{Int, UnitRange{Int}}, ci::CI{<:Any, <:M
first(rowrange)
end
constraint_rows(rowranges::Dict{Int, UnitRange{Int}}, ci::CI{<:Any, <:MOI.AbstractVectorSet}) = rowranges[ci.value]
constraint_rows(optimizer::OSQPOptimizer, ci::CI) = constraint_rows(optimizer.rowranges, ci)
constraint_rows(optimizer::Optimizer, ci::CI) = constraint_rows(optimizer.rowranges, ci)

"""
Return objective sense, as well as matrix `P`, vector `q`, and scalar `c` such that objective function is `1/2 x' P x + q' x + c`.
Expand Down Expand Up @@ -375,12 +383,12 @@ end


## Standard optimizer attributes:
MOI.get(optimizer::OSQPOptimizer, ::MOI.ObjectiveSense) = optimizer.sense
function MOI.get(optimizer::OSQPOptimizer, a::MOI.NumberOfVariables)
MOI.get(optimizer::Optimizer, ::MOI.ObjectiveSense) = optimizer.sense
function MOI.get(optimizer::Optimizer, a::MOI.NumberOfVariables)
OSQP.dimensions(optimizer.inner)[1]
end

function MOI.get(optimizer::OSQPOptimizer, a::MOI.ListOfVariableIndices)
function MOI.get(optimizer::Optimizer, a::MOI.ListOfVariableIndices)
[VI(i) for i = 1 : MOI.get(optimizer, MOI.NumberOfVariables())] # TODO: support for UnitRange would be nice
end

Expand Down Expand Up @@ -417,7 +425,7 @@ end # module

using .OSQPSettings

function MOI.set(optimizer::OSQPOptimizer, a::OSQPAttribute, value)
function MOI.set(optimizer::Optimizer, a::OSQPAttribute, value)
(isupdatable(a) || MOI.is_empty(optimizer)) || throw(MOI.SetAttributeNotAllowed(a))
setting = Symbol(a)
optimizer.settings[setting] = value
Expand All @@ -428,7 +436,7 @@ end


## Optimizer methods:
function MOI.optimize!(optimizer::OSQPOptimizer)
function MOI.optimize!(optimizer::Optimizer)
processupdates!(optimizer.inner, optimizer.modcache)
processupdates!(optimizer.inner, optimizer.warmstartcache)
OSQP.solve!(optimizer.inner, optimizer.results)
Expand All @@ -440,15 +448,15 @@ function MOI.optimize!(optimizer::OSQPOptimizer)
end

## Optimizer attributes:
MOI.get(optimizer::OSQPOptimizer, ::MOI.RawSolver) = optimizer.inner
MOI.get(optimizer::OSQPOptimizer, ::MOI.ResultCount) = 1
MOI.get(optimizer::Optimizer, ::MOI.RawSolver) = optimizer.inner
MOI.get(optimizer::Optimizer, ::MOI.ResultCount) = 1

MOI.supports(::OSQPOptimizer, ::MOI.ObjectiveFunction{MOI.SingleVariable}) = true
MOI.supports(::OSQPOptimizer, ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}) = true
MOI.supports(::OSQPOptimizer, ::MOI.ObjectiveFunction{Quadratic}) = true
MOI.supports(::OSQPOptimizer, ::MOI.ObjectiveSense) = true
MOI.supports(::Optimizer, ::MOI.ObjectiveFunction{MOI.SingleVariable}) = true
MOI.supports(::Optimizer, ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}) = true
MOI.supports(::Optimizer, ::MOI.ObjectiveFunction{Quadratic}) = true
MOI.supports(::Optimizer, ::MOI.ObjectiveSense) = true

function MOI.set(optimizer::OSQPOptimizer, a::MOI.ObjectiveFunction{MOI.SingleVariable}, obj::MOI.SingleVariable)
function MOI.set(optimizer::Optimizer, a::MOI.ObjectiveFunction{MOI.SingleVariable}, obj::MOI.SingleVariable)
MOI.is_empty(optimizer) && throw(MOI.SetAttributeNotAllowed(a))
optimizer.modcache.P[:] = 0
optimizer.modcache.q[:] = 0
Expand All @@ -457,15 +465,15 @@ function MOI.set(optimizer::OSQPOptimizer, a::MOI.ObjectiveFunction{MOI.SingleVa
nothing
end

function MOI.set(optimizer::OSQPOptimizer, a::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}, obj::MOI.ScalarAffineFunction{Float64})
function MOI.set(optimizer::Optimizer, a::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}, obj::MOI.ScalarAffineFunction{Float64})
MOI.is_empty(optimizer) && throw(MOI.SetAttributeNotAllowed(a))
optimizer.modcache.P[:] = 0
processlinearterms!(optimizer.modcache.q, obj.terms)
optimizer.objconstant = obj.constant
nothing
end

function MOI.set(optimizer::OSQPOptimizer, a::MOI.ObjectiveFunction{Quadratic}, obj::Quadratic)
function MOI.set(optimizer::Optimizer, a::MOI.ObjectiveFunction{Quadratic}, obj::Quadratic)
MOI.is_empty(optimizer) && throw(MOI.SetAttributeNotAllowed(a))
cache = optimizer.modcache
cache.P[:] = 0
Expand All @@ -481,25 +489,25 @@ function MOI.set(optimizer::OSQPOptimizer, a::MOI.ObjectiveFunction{Quadratic},
nothing
end

function MOI.get(optimizer::OSQPOptimizer, a::MOI.ObjectiveValue)
function MOI.get(optimizer::Optimizer, a::MOI.ObjectiveValue)
rawobj = optimizer.results.info.obj_val + optimizer.objconstant
ifelse(optimizer.sense == MOI.MaxSense, -rawobj, rawobj)
end

error_not_solved() = error("Problem is unsolved.")
function check_has_results(optimizer::OSQPOptimizer)
function check_has_results(optimizer::Optimizer)
if !hasresults(optimizer)
error_not_solved()
end
end

# Since these aren't explicitly returned by OSQP, I feel like it would be better to have a fallback method compute these:
function MOI.get(optimizer::OSQPOptimizer, a::MOI.SolveTime)
function MOI.get(optimizer::Optimizer, a::MOI.SolveTime)
check_has_results(optimizer)
optimizer.results.info.run_time
end

function MOI.get(optimizer::OSQPOptimizer, a::MOI.TerminationStatus)
function MOI.get(optimizer::Optimizer, a::MOI.TerminationStatus)
check_has_results(optimizer)
# Note that the :Dual_infeasible and :Primal_infeasible are mapped to MOI.Success
# because OSQP can return a proof of infeasibility. For the same reason,
Expand All @@ -526,7 +534,7 @@ function MOI.get(optimizer::OSQPOptimizer, a::MOI.TerminationStatus)
end
end

function MOI.get(optimizer::OSQPOptimizer, a::MOI.PrimalStatus)
function MOI.get(optimizer::Optimizer, a::MOI.PrimalStatus)
hasresults(optimizer) || return MOI.NoSolution
osqpstatus = optimizer.results.info.status
if osqpstatus == :Unsolved
Expand All @@ -544,7 +552,7 @@ function MOI.get(optimizer::OSQPOptimizer, a::MOI.PrimalStatus)
end
end

function MOI.get(optimizer::OSQPOptimizer, a::MOI.DualStatus)
function MOI.get(optimizer::Optimizer, a::MOI.DualStatus)
hasresults(optimizer) || return MOI.NoSolution
osqpstatus = optimizer.results.info.status
if osqpstatus == :Unsolved
Expand All @@ -564,30 +572,30 @@ end


## Variables:
function MOI.is_valid(optimizer::OSQPOptimizer, vi::VI)
function MOI.is_valid(optimizer::Optimizer, vi::VI)
vi.value ∈ 1 : MOI.get(optimizer, MOI.NumberOfVariables())
end


## Variable attributes:
function MOI.get(optimizer::OSQPOptimizer, a::MOI.VariablePrimal, vi::VI)
function MOI.get(optimizer::Optimizer, a::MOI.VariablePrimal, vi::VI)
x = ifelse(_contains(OSQP.SOLUTION_PRESENT, optimizer.results.info.status), optimizer.results.x, optimizer.results.dual_inf_cert)
x[vi.value]
end

function MOI.set(optimizer::OSQPOptimizer, a::MOI.VariablePrimalStart, vi::VI, value)
function MOI.set(optimizer::Optimizer, a::MOI.VariablePrimalStart, vi::VI, value)
MOI.is_empty(optimizer) && throw(MOI.SetAttributeNotAllowed(a))
optimizer.warmstartcache.x[vi.value] = value
end


## Constraints:
function MOI.is_valid(optimizer::OSQPOptimizer, ci::CI)
function MOI.is_valid(optimizer::Optimizer, ci::CI)
MOI.is_empty(optimizer) && return false
ci.value ∈ keys(optimizer.rowranges)
end

function MOI.set(optimizer::OSQPOptimizer, a::MOI.ConstraintDualStart, ci::CI, value)
function MOI.set(optimizer::Optimizer, a::MOI.ConstraintDualStart, ci::CI, value)
MOI.is_empty(optimizer) && throw(MOI.SetAttributeNotAllowed(a))
rows = constraint_rows(optimizer, ci)
for (i, row) in enumerate(rows)
Expand All @@ -597,7 +605,7 @@ function MOI.set(optimizer::OSQPOptimizer, a::MOI.ConstraintDualStart, ci::CI, v
end

# function modification:
function MOI.set(optimizer::OSQPOptimizer, attr::MOI.ConstraintFunction, ci::CI{Affine, <:IntervalConvertible}, f::Affine)
function MOI.set(optimizer::Optimizer, attr::MOI.ConstraintFunction, ci::CI{Affine, <:IntervalConvertible}, f::Affine)
MOI.is_valid(optimizer, ci) || throw(MOI.InvalidIndex(ci))
row = constraint_rows(optimizer, ci)
optimizer.modcache.A[row, :] = 0
Expand All @@ -613,7 +621,7 @@ function MOI.set(optimizer::OSQPOptimizer, attr::MOI.ConstraintFunction, ci::CI{
nothing
end

function MOI.set(optimizer::OSQPOptimizer, attr::MOI.ConstraintFunction, ci::CI{VectorAffine, <:SupportedVectorSets}, f::VectorAffine)
function MOI.set(optimizer::Optimizer, attr::MOI.ConstraintFunction, ci::CI{VectorAffine, <:SupportedVectorSets}, f::VectorAffine)
MOI.is_valid(optimizer, ci) || throw(MOI.InvalidIndex(ci))
rows = constraint_rows(optimizer, ci)
for row in rows
Expand All @@ -634,7 +642,7 @@ function MOI.set(optimizer::OSQPOptimizer, attr::MOI.ConstraintFunction, ci::CI{
end

# set modification:
function MOI.set(optimizer::OSQPOptimizer, attr::MOI.ConstraintSet, ci::CI{<:AffineConvertible, S}, s::S) where {S <: IntervalConvertible}
function MOI.set(optimizer::Optimizer, attr::MOI.ConstraintSet, ci::CI{<:AffineConvertible, S}, s::S) where {S <: IntervalConvertible}
MOI.is_valid(optimizer, ci) || throw(MOI.InvalidIndex(ci))
interval = S <: Interval ? s : MOI.Interval(s)
row = constraint_rows(optimizer, ci)
Expand All @@ -644,7 +652,7 @@ function MOI.set(optimizer::OSQPOptimizer, attr::MOI.ConstraintSet, ci::CI{<:Aff
nothing
end

function MOI.set(optimizer::OSQPOptimizer, attr::MOI.ConstraintSet, ci::CI{<:VectorAffine, S}, s::S) where {S <: SupportedVectorSets}
function MOI.set(optimizer::Optimizer, attr::MOI.ConstraintSet, ci::CI{<:VectorAffine, S}, s::S) where {S <: SupportedVectorSets}
MOI.is_valid(optimizer, ci) || throw(MOI.InvalidIndex(ci))
rows = constraint_rows(optimizer, ci)
for (i, row) in enumerate(rows)
Expand All @@ -656,7 +664,7 @@ function MOI.set(optimizer::OSQPOptimizer, attr::MOI.ConstraintSet, ci::CI{<:Ve
end

# partial function modification:
function MOI.modify(optimizer::OSQPOptimizer, ci::CI{Affine, <:IntervalConvertible}, change::MOI.ScalarCoefficientChange)
function MOI.modify(optimizer::Optimizer, ci::CI{Affine, <:IntervalConvertible}, change::MOI.ScalarCoefficientChange)
MOI.is_valid(optimizer, ci) || throw(MOI.InvalidIndex(ci))
row = constraint_rows(optimizer, ci)
optimizer.modcache.A[row, change.variable.value] = change.new_coefficient
Expand All @@ -665,24 +673,24 @@ end

# TODO: MultirowChange?

MOI.supports_constraint(optimizer::OSQPOptimizer, ::Type{<:AffineConvertible}, ::Type{<:IntervalConvertible}) = true
MOI.supports_constraint(optimizer::OSQPOptimizer, ::Type{VectorAffine}, ::Type{<:SupportedVectorSets}) = true
MOI.supports_constraint(optimizer::Optimizer, ::Type{<:AffineConvertible}, ::Type{<:IntervalConvertible}) = true
MOI.supports_constraint(optimizer::Optimizer, ::Type{VectorAffine}, ::Type{<:SupportedVectorSets}) = true

## Constraint attributes:
function MOI.get(optimizer::OSQPOptimizer, a::MOI.ConstraintDual, ci::CI)
function MOI.get(optimizer::Optimizer, a::MOI.ConstraintDual, ci::CI)
y = ifelse(_contains(OSQP.SOLUTION_PRESENT, optimizer.results.info.status), optimizer.results.y, optimizer.results.prim_inf_cert)
rows = constraint_rows(optimizer, ci)
-y[rows]
end


# Objective modification
function MOI.modify(optimizer::OSQPOptimizer, attr::MOI.ObjectiveFunction, change::MOI.ScalarConstantChange)
function MOI.modify(optimizer::Optimizer, attr::MOI.ObjectiveFunction, change::MOI.ScalarConstantChange)
MOI.is_empty(optimizer) && throw(MOI.ModifyObjectiveNotAllowed(change))
optimizer.objconstant = change.new_constant
end

function MOI.modify(optimizer::OSQPOptimizer, attr::MOI.ObjectiveFunction, change::MOI.ScalarCoefficientChange)
function MOI.modify(optimizer::Optimizer, attr::MOI.ObjectiveFunction, change::MOI.ScalarCoefficientChange)
MOI.is_empty(optimizer) && throw(MOI.ModifyObjectiveNotAllowed(change))
optimizer.modcache.q[change.variable.value] = change.new_coefficient
end
Expand Down
1 change: 1 addition & 0 deletions src/OSQP.jl
Expand Up @@ -35,5 +35,6 @@ include("types.jl")
include("interface.jl")
include("MPBWrapper.jl")
include("MOIWrapper.jl")
const Optimizer = MathOptInterfaceOSQP.Optimizer

end # module
3 changes: 2 additions & 1 deletion test/MOIWrapper.jl
Expand Up @@ -106,7 +106,7 @@ end
const config = MOIT.TestConfig(atol=1e-4, rtol=1e-4)

function defaultoptimizer()
optimizer = OSQPOptimizer()
optimizer = OSQP.Optimizer()
MOI.set(optimizer, OSQPSettings.Verbose(), false)
MOI.set(optimizer, OSQPSettings.EpsAbs(), 1e-8)
MOI.set(optimizer, OSQPSettings.EpsRel(), 1e-16)
Expand Down Expand Up @@ -477,6 +477,7 @@ struct ExoticFunction <: MOI.AbstractScalarFunction end
MOI.get(src::BadObjectiveModel, ::MOI.ObjectiveFunctionType) = ExoticFunction

@testset "failcopy" begin
# TODO change OSQPOptimizer() to OSQP.Optimizer() in OSQP v0.6
optimizer = OSQPOptimizer()
MOIT.failcopytestc(optimizer)
@test_throws MOI.UnsupportedAttribute{MOI.ObjectiveFunction{ExoticFunction}} MOI.copy_to(optimizer, BadObjectiveModel())
Expand Down