Skip to content

Commit

Permalink
Merge pull request #55 from oxfordcontrol/bl/moiv0.9
Browse files Browse the repository at this point in the history
Update for MOI v0.9
  • Loading branch information
blegat authored Aug 23, 2019
2 parents fda1c30 + e7df956 commit f09a281
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 69 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"

[compat]
BinaryProvider = "≥ 0.3.0"
MathOptInterface = "~0.8"
MathOptInterface = "~0.9.1"
MathProgBase = "~0.5.0, ~0.6, ~0.7"
julia = "1"

Expand Down
85 changes: 45 additions & 40 deletions src/MOI_wrapper.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@ const VI = MOI.VariableIndex

const SparseTriplets = Tuple{Vector{Int}, Vector{Int}, Vector{<:Any}}

const SingleVariable = MOI.SingleVariable
const Affine = MOI.ScalarAffineFunction{Float64}
const Quadratic = MOI.ScalarQuadraticFunction{Float64}
const AffineConvertible = Union{Affine, SingleVariable}
const VectorAffine = MOI.VectorAffineFunction{Float64}

const Interval = MOI.Interval{Float64}
Expand All @@ -36,14 +34,6 @@ const SupportedVectorSets = Union{Zeros, Nonnegatives, Nonpositives}

import OSQP

# TODO: consider moving to MOI:
constant(f::MOI.SingleVariable) = 0
constant(f::MOI.ScalarAffineFunction) = f.constant
# constant(f::MOI.ScalarQuadraticFunction) = f.constant

dimension(s::MOI.AbstractSet) = MOI.dimension(s)
dimension(::MOI.AbstractScalarSet) = 1

lower(::Zeros, i::Int) = 0.0
lower(::Nonnegatives, i::Int) = 0.0
lower(::Nonpositives, i::Int) = -Inf
Expand All @@ -64,6 +54,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer
hasresults::Bool
results::OSQP.Results
is_empty::Bool
silent::Bool
settings::Dict{Symbol, Any} # need to store these, because they should be preserved if empty! is called
sense::MOI.OptimizationSense
objconstant::Float64
Expand All @@ -77,29 +68,38 @@ mutable struct Optimizer <: MOI.AbstractOptimizer
hasresults = false
results = OSQP.Results()
is_empty = true
settings = Dict{Symbol, Any}()
for (name, value) in kwargs
settings[Symbol(name)] = value
end
sense = MOI.MIN_SENSE
objconstant = 0.
constrconstant = Float64[]
modcache = ProblemModificationCache{Float64}()
warmstartcache = WarmStartCache{Float64}()
rowranges = Dict{Int, UnitRange{Int}}()
new(inner, hasresults, results, is_empty, settings, sense, objconstant, constrconstant, modcache, warmstartcache, rowranges)
optimizer = new(inner, hasresults, results, is_empty, false,
Dict{Symbol, Any}(:verbose => true), sense, objconstant,
constrconstant, modcache, warmstartcache, rowranges)
for (key, value) in kwargs
MOI.set(optimizer, MOI.RawParameter(key), value)
end
return optimizer
end
end

MOI.get(::Optimizer, ::MOI.SolverName) = "OSQP"

# 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)
return Optimizer()
MOI.supports(::Optimizer, ::MOI.Silent) = true
function MOI.set(optimizer::Optimizer, ::MOI.Silent, value::Bool)
if optimizer.silent != value
optimizer.silent = value
if !MOI.is_empty(optimizer)
if optimizer.silent
OSQP.update_settings!(optimizer.inner; :verbose => false)
else
OSQP.update_settings!(optimizer.inner; :verbose => optimizer.settings[:verbose])
end
end
end
end
MOI.get(optimizer::Optimizer, ::MOI.Silent) = optimizer.silent

hasresults(optimizer::Optimizer) = optimizer.hasresults

Expand Down Expand Up @@ -127,6 +127,9 @@ function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike; copy_names=false)
dest.sense, P, q, dest.objconstant = processobjective(src, idxmap)
A, l, u, dest.constrconstant = processconstraints(src, idxmap, dest.rowranges)
OSQP.setup!(dest.inner; P = P, q = q, A = A, l = l, u = u, dest.settings...)
if dest.silent
OSQP.update_settings!(dest.inner; :verbose => false)
end
dest.modcache = ProblemModificationCache(P, q, A, l, u)
dest.warmstartcache = WarmStartCache{Float64}(size(A, 2), size(A, 1))
processprimalstart!(dest.warmstartcache.x, src, idxmap)
Expand Down Expand Up @@ -163,7 +166,7 @@ function assign_constraint_row_ranges!(rowranges::Dict{Int, UnitRange{Int}}, idx
for ci_src in cis_src
set = MOI.get(src, MOI.ConstraintSet(), ci_src)
ci_dest = idxmap[ci_src]
endrow = startrow + dimension(set) - 1
endrow = startrow + MOI.dimension(set) - 1
rowranges[ci_dest.value] = startrow : endrow
startrow = endrow + 1
end
Expand Down Expand Up @@ -282,8 +285,8 @@ function processconstraints!(triplets::SparseTriplets, bounds::Tuple{<:Vector, <
nothing
end

function processconstant!(c::Vector{Float64}, row::Int, f::AffineConvertible)
c[row] = constant(f)
function processconstant!(c::Vector{Float64}, row::Int, f::Affine)
c[row] = MOI.constant(f, Float64)
nothing
end

Expand All @@ -293,15 +296,6 @@ function processconstant!(c::Vector{Float64}, rows::UnitRange{Int}, f::VectorAff
end
end

function processlinearpart!(triplets::SparseTriplets, f::MOI.SingleVariable, row::Int, idxmap)
(I, J, V) = triplets
col = idxmap[f.variable].value
push!(I, row)
push!(J, col)
push!(V, 1)
nothing
end

function processlinearpart!(triplets::SparseTriplets, f::MOI.ScalarAffineFunction, row::Int, idxmap)
(I, J, V) = triplets
for term in f.terms
Expand Down Expand Up @@ -430,15 +424,22 @@ end # module

using .OSQPSettings

function MOI.set(optimizer::Optimizer, a::OSQPAttribute, value)
_symbol(param::MOI.RawParameter) = Symbol(param.name)
_symbol(a::OSQPAttribute) = Symbol(a)
OSQPSettings.isupdatable(param::MOI.RawParameter) = _contains(OSQP.UPDATABLE_SETTINGS, _symbol(param))
function MOI.set(optimizer::Optimizer, a::Union{OSQPAttribute, MOI.RawParameter}, value)
(isupdatable(a) || MOI.is_empty(optimizer)) || throw(MOI.SetAttributeNotAllowed(a))
setting = Symbol(a)
setting = _symbol(a)
optimizer.settings[setting] = value
if !MOI.is_empty(optimizer)
OSQP.update_settings!(optimizer.inner; setting => value)
end
end

function MOI.get(optimizer::Optimizer, a::Union{OSQPAttribute, MOI.RawParameter})
return optimizer.settings[_symbol(a)]
end


## Optimizer methods:
function MOI.optimize!(optimizer::Optimizer)
Expand Down Expand Up @@ -512,7 +513,11 @@ function MOI.get(optimizer::Optimizer, a::MOI.SolveTime)
return optimizer.results.info.run_time
end

function MOI.get(optimizer::Optimizer, a::MOI.TerminationStatus)
function MOI.get(optimizer::Optimizer, ::MOI.RawStatusString)
return string(optimizer.results.info.status)
end

function MOI.get(optimizer::Optimizer, ::MOI.TerminationStatus)
hasresults(optimizer) || return MOI.OPTIMIZE_NOT_CALLED
osqpstatus = optimizer.results.info.status
if osqpstatus == :Unsolved
Expand Down Expand Up @@ -645,7 +650,7 @@ function MOI.set(optimizer::Optimizer, attr::MOI.ConstraintFunction, ci::CI{Vect
end

# set modification:
function MOI.set(optimizer::Optimizer, attr::MOI.ConstraintSet, ci::CI{<:AffineConvertible, S}, s::S) where {S <: IntervalConvertible}
function MOI.set(optimizer::Optimizer, attr::MOI.ConstraintSet, ci::CI{Affine, 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 @@ -655,7 +660,7 @@ function MOI.set(optimizer::Optimizer, attr::MOI.ConstraintSet, ci::CI{<:AffineC
nothing
end

function MOI.set(optimizer::Optimizer, 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 @@ -676,7 +681,7 @@ end

# TODO: MultirowChange?

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

## Constraint attributes:
Expand Down Expand Up @@ -705,7 +710,7 @@ MOIU.@model(OSQPModel, # modelname
(MOI.Interval, MOI.LessThan, MOI.GreaterThan, MOI.EqualTo), # typedscalarsets
(MOI.Zeros, MOI.Nonnegatives, MOI.Nonpositives), # vectorsets
(), # typedvectorsets
(MOI.SingleVariable,), # scalarfunctions
(), # scalarfunctions
(MOI.ScalarAffineFunction, MOI.ScalarQuadraticFunction), # typedscalarfunctions
(), # vectorfunctions
(MOI.VectorAffineFunction,) # typedvectorfunctions
Expand Down
63 changes: 35 additions & 28 deletions test/MOI_wrapper.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@ using OSQP.MathOptInterfaceOSQP
using LinearAlgebra
using Random
using SparseArrays
using Test

using MathOptInterface
const MOI = MathOptInterface

using MathOptInterface.Test
const MOIT = MathOptInterface.Test

using MathOptInterface.Utilities
const MOIU = MathOptInterface.Utilities
const MOIT = MOI.Test
const MOIU = MOI.Utilities

const Affine = MOI.ScalarAffineFunction{Float64}

Expand Down Expand Up @@ -88,10 +85,6 @@ const Affine = MOI.ScalarAffineFunction{Float64}
end

# FIXME: type piracy. Generalize and move to MOIU.
function MOI.get(optimizer::MOIU.CachingOptimizer, ::MOI.ConstraintPrimal, ci::MOI.ConstraintIndex{<:MOI.SingleVariable, <:Any})
f = MOI.get(optimizer, MOI.ConstraintFunction(), ci)
MOI.get(optimizer, MOI.VariablePrimal(), f.variable)
end
function MOI.get(optimizer::MOIU.CachingOptimizer, ::MOI.ConstraintPrimal, ci::MOI.ConstraintIndex{<:MOI.ScalarAffineFunction, <:Any})
f = MOI.get(optimizer, MOI.ConstraintFunction(), ci)
ret = f.constant
Expand All @@ -105,16 +98,23 @@ const config = MOIT.TestConfig(atol=1e-4, rtol=1e-4)

function defaultoptimizer()
optimizer = OSQP.Optimizer()
MOI.set(optimizer, OSQPSettings.Verbose(), false)
MOI.set(optimizer, MOI.Silent(), true)
MOI.set(optimizer, OSQPSettings.EpsAbs(), 1e-8)
MOI.set(optimizer, OSQPSettings.EpsRel(), 1e-16)
MOI.set(optimizer, OSQPSettings.MaxIter(), 10000)
MOI.set(optimizer, OSQPSettings.AdaptiveRhoInterval(), 25) # required for deterministic behavior
optimizer
return optimizer
end
function bridged_optimizer()
optimizer = defaultoptimizer()
cached = MOIU.CachingOptimizer(MOIU.UniversalFallback(OSQPModel{Float64}()), optimizer)
return MOI.Bridges.full_bridge_optimizer(cached, Float64)
end

@testset "CachingOptimizer: unit" begin
excludes = [# Quadratic constraints are not supported
excludes = [# TODO
"time_limit_sec",
# Quadratic constraints are not supported
"solve_qcp_edge_cases",
# No method get(::Optimizer, ::MathOptInterface.ConstraintPrimal, ::MathOptInterface.ConstraintIndex{MathOptInterface.VectorAffineFunction{Float64},MathOptInterface.Nonpositives})
"solve_duplicate_terms_vector_affine",
Expand All @@ -123,29 +123,34 @@ end
# ConstraintPrimal not supported
"solve_affine_deletion_edge_cases",
# Integer and ZeroOne sets are not supported
"solve_integer_edge_cases", "solve_objbound_edge_cases"]
"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"]

optimizer = defaultoptimizer()
MOIT.unittest(MOIU.CachingOptimizer(OSQPModel{Float64}(), optimizer),
config, excludes)
MOIT.unittest(bridged_optimizer(), config, excludes)
end

@testset "CachingOptimizer: linear problems" begin
excludes = ["partial_start"] # See comment https://github.com/JuliaOpt/MathOptInterface.jl/blob/ecf691545e67552ff437ed26ec4ddfff03c50327/src/Test/contlinear.jl#L1715
excludes = [
"partial_start", # See comment https://github.com/JuliaOpt/MathOptInterface.jl/blob/ecf691545e67552ff437ed26ec4ddfff03c50327/src/Test/contlinear.jl#L1715
"linear1", # `ConstraintPrimal` not available
"linear8a" # It expects `ResultCount` to be 0 as we disable `duals`.
]
append!(excludes,
if Int == Int32
["linear7"] # https://github.com/JuliaOpt/MathOptInterface.jl/issues/377#issuecomment-394912761
else
[]
end)
optimizer = defaultoptimizer()
MOIT.contlineartest(MOIU.CachingOptimizer(OSQPModel{Float64}(), optimizer), config, excludes)
# We disable duals as DualObjectiveValue is not implemented
MOIT.contlineartest(bridged_optimizer(), MOIT.TestConfig(atol=1e-4, rtol=1e-4, duals=false), excludes)
end

@testset "CachingOptimizer: quadratic problems" begin
excludes = String[]
optimizer = defaultoptimizer()
MOIT.qptest(MOIU.CachingOptimizer(OSQPModel{Float64}(), optimizer), config, excludes)
MOIT.qptest(bridged_optimizer(), config, excludes)
end

function test_optimizer_modification(modfun::Base.Callable, model::MOI.ModelLike, optimizer::T, idxmap::MOIU.IndexMap,
Expand Down Expand Up @@ -215,8 +220,8 @@ term(c, x::MOI.VariableIndex, y::MOI.VariableIndex) = MOI.ScalarQuadraticTerm(c,
x, y = v
cf = MOI.ScalarAffineFunction([term.([0.0, 0.0], v); term.([1.0, 1.0], v); term.([0.0, 0.0], v)], 0.0)
c = MOI.add_constraint(model, cf, MOI.Interval(-Inf, 1.0))
vc1 = MOI.add_constraint(model, MOI.SingleVariable(v[1]), MOI.Interval(0.0, Inf))
vc2 = MOI.add_constraint(model, v[2], MOI.Interval(0.0, Inf))
vc1 = MOI.add_constraint(model, 1.0MOI.SingleVariable(v[1]), MOI.Interval(0.0, Inf))
vc2 = MOI.add_constraint(model, 1.0MOI.SingleVariable(v[2]), MOI.Interval(0.0, Inf))
objf = MOI.ScalarAffineFunction([term.([0.0, 0.0], v); term.([-1.0, 0.0], v); term.([0.0, 0.0], v)], 0.0)
MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), objf)
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
Expand Down Expand Up @@ -526,7 +531,7 @@ end
model = OSQPModel{Float64}()
MOI.empty!(model)
x = MOI.add_variable(model)
c = MOI.add_constraint(model, x, MOI.GreaterThan(2.0))
c = MOI.add_constraint(model, 1.0MOI.SingleVariable(x), MOI.GreaterThan(2.0))
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
MOI.set(model, MOI.ObjectiveFunction{MOI.SingleVariable}(), MOI.SingleVariable(x))

Expand All @@ -542,8 +547,10 @@ 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())
# FIXME https://github.com/JuliaOpt/MathOptInterface.jl/issues/851
#MOIT.failcopytestc(bridged_optimizer())
#optimizer = bridged_optimizer()
#MOI.copy_to(optimizer, BadObjectiveModel())
# FIXME UndefRefError: access to undefined reference
#@test_throws MOI.UnsupportedAttribute{MOI.ObjectiveFunction{ExoticFunction}} MOI.optimize!(optimizer)
end

0 comments on commit f09a281

Please sign in to comment.