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

Add SOCtoQuad bridge #478

Closed
wants to merge 1 commit into from
Closed
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
8 changes: 6 additions & 2 deletions src/Bridges/Bridges.jl
Expand Up @@ -25,6 +25,7 @@ include("lazybridgeoptimizer.jl")

# This is used by JuMP and removes the need to update JuMP everytime a bridge is added
MOIU.@model AllBridgedConstraints () (Interval,) (SecondOrderCone, RotatedSecondOrderCone, GeometricMeanCone, LogDetConeTriangle, RootDetConeTriangle) () () (ScalarAffineFunction,) (VectorOfVariables,) (VectorAffineFunction,)

"""
fullbridgeoptimizer(model::MOI.ModelLike, ::Type{T}) where T

Expand All @@ -33,20 +34,23 @@ Returns a `LazyBridgeOptimizer` bridging `model` for every bridge defined in thi
function fullbridgeoptimizer(model::MOI.ModelLike, ::Type{T}) where T
bridgedmodel = MOIB.LazyBridgeOptimizer(model, AllBridgedConstraints{T}())
addbridge!(bridgedmodel, MOIB.SplitIntervalBridge{T})
addbridge!(bridgedmodel, MOIB.RSOCBridge{T})
addbridge!(bridgedmodel, MOIB.SOCtoQuadBridge{T})
addbridge!(bridgedmodel, MOIB.GeoMeanBridge{T})
addbridge!(bridgedmodel, MOIB.SquarePSDBridge{T})
addbridge!(bridgedmodel, MOIB.LogDetBridge{T})
addbridge!(bridgedmodel, MOIB.RootDetBridge{T})
addbridge!(bridgedmodel, MOIB.RSOCBridge{T})
addbridge!(bridgedmodel, MOIB.RSOCtoPSDCBridge{T})
addbridge!(bridgedmodel, MOIB.SOCtoPSDCBridge{T})
addbridge!(bridgedmodel, MOIB.RSOCtoPSDCBridge{T})
bridgedmodel
end

include("intervalbridge.jl")
@bridge SplitInterval SplitIntervalBridge () (Interval,) () () (SingleVariable,) (ScalarAffineFunction, ScalarQuadraticFunction) () ()
include("rsocbridge.jl")
@bridge RSOC RSOCBridge () () (RotatedSecondOrderCone,) () () () (VectorOfVariables,) (VectorAffineFunction,)
include("soctoquadbridge.jl")
@bridge SOCtoQuad SOCtoQuadBridge () () (SecondOrderCone,) () () () (VectorOfVariables,) (VectorAffineFunction,)
include("geomeanbridge.jl")
@bridge GeoMean GeoMeanBridge () () (GeometricMeanCone,) () () () (VectorOfVariables,) (VectorAffineFunction,)
include("squarepsdbridge.jl")
Expand Down
8 changes: 8 additions & 0 deletions src/Bridges/bridge.jl
Expand Up @@ -76,3 +76,11 @@ function concrete_bridge_type(bridge_type::DataType,
::Type{<:MOI.AbstractSet})
return bridge_type
end

"""
need_constraint_primal_fallback(BT::Type{<:AbstractBridge})

Return a `Bool` indicating whether bridges of concrete type `BT` need the bridge
optimizer to use `get_fallback`.
"""
need_constraint_primal_fallback(::Type{<:AbstractBridge}) = false
8 changes: 6 additions & 2 deletions src/Bridges/bridgeoptimizer.jl
Expand Up @@ -208,10 +208,14 @@ end
# Constraint attributes
function MOI.get(b::AbstractBridgeOptimizer,
attr::MOI.AbstractConstraintAttribute,
ci::CI)
ci::CI{F, S}) where {F, S}
if isbridged(b, typeof(ci))
if MOIU.is_result_attribute(attr)
MOI.get(b, attr, bridge(b, ci))
if attr isa MOI.ConstraintPrimal && need_constraint_primal_fallback(concrete_bridge_type(b, F, S))
MOIU.get_fallback(b, attr, ci)
else
MOI.get(b, attr, bridge(b, ci))
end
else
MOI.get(b.bridged, attr, ci)
end
Expand Down
83 changes: 83 additions & 0 deletions src/Bridges/soctoquadbridge.jl
@@ -0,0 +1,83 @@
"""
SOCtoQuadBridge{T}

Constraints of the form `VectorOfVariables`-in-`SecondOrderCone` (resp.
`VectorAffineFunction`-in-`SecondOrderCone`) can be transformed into a
`ScalarQuadraticFunction`-in-`GreaterThan` and a
`SingleVariable`-in-`GreaterThan` (resp.
`ScalarAffineFunction`-in-`SecondOrderCone`). Indeed, the definition of the
second-order cone
```math
t \\ge || x ||_2 \\}
```
is equivalent to
```math
\\sum x_i^2 \\le t^2
```
with ``t \\ge 0``.
"""
struct SOCtoQuadBridge{T, F, G} <: AbstractBridge
quad::CI{F, MOI.GreaterThan{T}}
t_nonneg::CI{G, MOI.GreaterThan{T}}
end
function SOCtoQuadBridge{T, F, G}(model::MOI.ModelLike,
f::MOI.AbstractVectorFunction,
Copy link
Member

Choose a reason for hiding this comment

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

This transformation is recognized by Gurobi/Cplex/Xpress(?) only when f is a VectorOfVariables. They can't handle general affine expressions. That creates a tricky situation because we don't have a way to indicate this restriction in MOI afaik, so I'm questioning if bridges are the right place for this transformation to happen. Maybe the solvers need to support VectorOfVariables-in-SecondOrderCone and then let a bridge handle only the transformation from VectorAffineFunction-in-SecondOrderCone to VectorOfVariables-in-SecondOrderCone.

Copy link
Member Author

Choose a reason for hiding this comment

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

They also support x^T Q x +q^T x <= b apparently: http://www.gurobi.com/documentation/8.0/refman/constraints.html#subsubsection:QuadraticConstraints
so if it is a VectorAffineFunction in SOC, it will be transformed into x^T Q x +q^T x <= b with Q PSD an Gurobi will also support it, won't it ?

Copy link
Member

Choose a reason for hiding this comment

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

This bridge does not generate a constraint of the form x^T Q x +q^T x <= b where Q is PSD (there's one negative eigenvalue).

Copy link
Member Author

Choose a reason for hiding this comment

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

Indeed, good point. This bridge could create a vector of variables and constraint them to be equal to the affexpr and then use these auxiliary variables to create the quadratic function. This would not only make sense for Gurobi as taking the square of complicated affine expression may not result in a very simple quadratic function so it might be the right idea anyway.
Would that work ?

Copy link
Member

Choose a reason for hiding this comment

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

That works for Gurobi et al, but I think there's a larger design issue. Ipopt supports convex and non-convex quadratic constraints but not SOC constraints (there's no special detection for x^T x <= t^2). It this bridge is applied, Ipopt will treat the constraint as a nonconvex quadratic constraint without any guarantees of global optimality. Unless there are additional properties of this transformation that imply that a KKT point must be globally optimal (even though the problem is nonconvex), this means that the user could write down a convex problem using SOC and get a bad answer back from Ipopt.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

We discussed about this issue with @mlubin and @ccoffrin. The approach would be to define this bridge in MOI but not add it in full_bridge_optimizer so that the user would have to add it manually with JuMP.add_bridge. By choosing which bridge to add, it would either allow VectorOfVariables-in-SOC or VectorAffineFunction-in-SOC to add so he is responsible to add the one with which the optimizer works.
IMO the optimizer could also simply apply the bridge automatically by itself with a SingleBridgeOptimizer. Choosing to build the API on top of quadratic functions instead of SOC constraints was a bad design choice and this is the reason this bridge seems weird because it destroys a structure that the optimizers try to recover afterwards.

Copy link
Member

Choose a reason for hiding this comment

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

Since the three solvers support SOC´s as special quadratic constraints, the bridge could be very useful. Given Ipopt's problem, we could have it non-default, and add a note about the issue.

Copy link
Member

Choose a reason for hiding this comment

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

@joaquimg IMO the right solution for this is for Gurobi/Xpress/CPLEX to explicitly support VoV/VAF-in-SOC in their interfaces. We have no other way to know if a solver secretly supports SOC through quadratic constraints.

Choose a reason for hiding this comment

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

this means that the user could write down a convex problem using SOC and get a bad answer back from Ipopt.

Regarding this point. As I understand the theory, it does not matter in what form a convex constraint is given to a IPM solver. The KKT convergence guarantees will be the same; so I don't think this a major issue for the case of targeting NLP solvers.

s::MOI.SecondOrderCone) where {T, F, G}
d = s.dimension
f_scalars = MOIU.eachscalar(f)
t = f_scalars[1]
t_nonneg = MOIU.add_scalar_constraint(model, t, MOI.GreaterThan(zero(T)))
quad_f = MOIU.operate(*, T, t, t)
for i in 2:d
x = f_scalars[i]
quad_f = MOIU.operate!(-, T, quad_f, MOIU.operate(*, T, x, x))
end
quad = MOIU.add_scalar_constraint(model, quad_f, MOI.GreaterThan(zero(T)))
return SOCtoQuadBridge{T, F, G}(quad, t_nonneg)
end

function MOI.supportsconstraint(::Type{SOCtoQuadBridge{T}},
::Type{<:MOI.AbstractVectorFunction},
::Type{MOI.SecondOrderCone}) where T
return true
end
function addedconstrainttypes(::Type{SOCtoQuadBridge{T, F, G}}) where {T, F, G}
list = [(F, MOI.GreaterThan{T})]
if F != G
push!(list, (G, MOI.GreaterThan{T}))
end
return list
end
function concrete_bridge_type(::Type{<:SOCtoQuadBridge{T}},
H::Type{<:MOI.AbstractVectorFunction},
::Type{MOI.SecondOrderCone}) where T
G = MOIU.scalar_type(H)
Q = MOIU.promote_operation(*, T, G, G)
F = MOIU.promote_operation(-, T, Q, Q)
return SOCtoQuadBridge{T, F, G}
end

# Attributes, Bridge acting as an model
function MOI.get(b::SOCtoQuadBridge{T, F, G},
::MOI.NumberOfConstraints{H, MOI.GreaterThan{T}}) where {T, F, G, H}
return (F == H) + (G == H)
end
function MOI.get(b::SOCtoQuadBridge{T, F, G},
::MOI.ListOfConstraintIndices{H, MOI.GreaterThan{T}}) where {T, F, G, H}
list = CI{H, MOI.GreaterThan{T}}[]
if F == H
push!(list, b.quad)
end
if G == H
push!(list, b.t_nonneg)
end
return list
end

# References
function MOI.delete!(model::MOI.ModelLike, bridge::SOCtoQuadBridge)
MOI.delete!(model, bridge.t_nonneg)
MOI.delete!(model, bridge.quad)
end

need_constraint_primal_fallback(::Type{<:SOCtoQuadBridge}) = true
83 changes: 49 additions & 34 deletions test/bridge.jl
Expand Up @@ -212,6 +212,34 @@ end
test_delete_bridge(bridgedmock, ci, 2, ((MOI.VectorAffineFunction{Float64}, MOI.SecondOrderCone, 0),))
end

@testset "SOCtoQuad" begin
config = MOIT.TestConfig(duals=false)
bridgedmock = MOIB.SOCtoQuad{Float64}(mock)
mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [1.0, 1/√2, 1/√2])
MOIT.soc1vtest(bridgedmock, config)
mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [1.0, 1/√2, 1/√2])
MOIT.soc1ftest(bridgedmock, config)
ci = first(MOI.get(bridgedmock,
MOI.ListOfConstraintIndices{MOI.VectorAffineFunction{Float64},
MOI.SecondOrderCone}()))
test_delete_bridge(bridgedmock, ci, 3,
((MOI.ScalarAffineFunction{Float64},
MOI.GreaterThan{Float64}, 0),))
end

@testset "GeoMean" begin
mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [ones(4); 2; √2; √2])
bridgedmock = MOIB.GeoMean{Float64}(mock)
MOIT.geomean1vtest(bridgedmock, config)
MOIT.geomean1ftest(bridgedmock, config)
# Dual is not yet implemented for GeoMean bridge
ci = first(MOI.get(bridgedmock, MOI.ListOfConstraintIndices{MOI.VectorAffineFunction{Float64}, MOI.GeometricMeanCone}()))
@test !MOI.supports(bridgedmock, MOI.ConstraintSet(), typeof(ci))
@test !MOI.supports(bridgedmock, MOI.ConstraintFunction(), typeof(ci))
test_delete_bridge(bridgedmock, ci, 4, ((MOI.VectorAffineFunction{Float64}, MOI.RotatedSecondOrderCone, 0),
(MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}, 1)))
end

@testset "SquarePSD" begin
bridgedmock = MOIB.SquarePSD{Float64}(mock)
mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, ones(4),
Expand All @@ -231,19 +259,31 @@ end
MOI.EqualTo{Float64}, 1)))
end

@testset "GeoMean" begin
mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [ones(4); 2; √2; √2])
bridgedmock = MOIB.GeoMean{Float64}(mock)
MOIT.geomean1vtest(bridgedmock, config)
MOIT.geomean1ftest(bridgedmock, config)
# Dual is not yet implemented for GeoMean bridge
ci = first(MOI.get(bridgedmock, MOI.ListOfConstraintIndices{MOI.VectorAffineFunction{Float64}, MOI.GeometricMeanCone}()))
@testset "LogDet" begin
bridgedmock = MOIB.LogDet{Float64}(mock)
mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [0, 1, 0, 1, 1, 0, 1, 0, 0])
MOIT.logdett1vtest(bridgedmock, config)
MOIT.logdett1ftest(bridgedmock, config)
# Dual is not yet implemented for LogDet bridge
ci = first(MOI.get(bridgedmock, MOI.ListOfConstraintIndices{MOI.VectorAffineFunction{Float64}, MOI.LogDetConeTriangle}()))
@test !MOI.supports(bridgedmock, MOI.ConstraintSet(), typeof(ci))
@test !MOI.supports(bridgedmock, MOI.ConstraintFunction(), typeof(ci))
test_delete_bridge(bridgedmock, ci, 4, ((MOI.VectorAffineFunction{Float64}, MOI.RotatedSecondOrderCone, 0),
(MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}, 1)))
test_delete_bridge(bridgedmock, ci, 4, ((MOI.VectorAffineFunction{Float64}, MOI.ExponentialCone, 0), (MOI.VectorAffineFunction{Float64}, MOI.PositiveSemidefiniteConeTriangle, 0)))
end

@testset "RootDet" begin
bridgedmock = MOIB.RootDet{Float64}(mock)
mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [1, 1, 0, 1, 1, 0, 1])
MOIT.rootdett1vtest(bridgedmock, config)
MOIT.rootdett1ftest(bridgedmock, config)
# Dual is not yet implemented for RootDet bridge
ci = first(MOI.get(bridgedmock, MOI.ListOfConstraintIndices{MOI.VectorAffineFunction{Float64}, MOI.RootDetConeTriangle}()))
@test !MOI.supports(bridgedmock, MOI.ConstraintSet(), typeof(ci))
@test !MOI.supports(bridgedmock, MOI.ConstraintFunction(), typeof(ci))
test_delete_bridge(bridgedmock, ci, 4, ((MOI.VectorAffineFunction{Float64}, MOI.RotatedSecondOrderCone, 0),
(MOI.VectorAffineFunction{Float64}, MOI.GeometricMeanCone, 0),
(MOI.VectorAffineFunction{Float64}, MOI.PositiveSemidefiniteConeTriangle, 0)))
end
@testset "SOCtoPSD" begin
bridgedmock = MOIB.SOCtoPSD{Float64}(mock)
mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [1.0, 1/√2, 1/√2],
Expand Down Expand Up @@ -272,29 +312,4 @@ end
test_delete_bridge(bridgedmock, ci, 2, ((MOI.VectorAffineFunction{Float64}, MOI.PositiveSemidefiniteConeTriangle, 0),))
end

@testset "LogDet" begin
bridgedmock = MOIB.LogDet{Float64}(mock)
mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [0, 1, 0, 1, 1, 0, 1, 0, 0])
MOIT.logdett1vtest(bridgedmock, config)
MOIT.logdett1ftest(bridgedmock, config)
# Dual is not yet implemented for LogDet bridge
ci = first(MOI.get(bridgedmock, MOI.ListOfConstraintIndices{MOI.VectorAffineFunction{Float64}, MOI.LogDetConeTriangle}()))
@test !MOI.supports(bridgedmock, MOI.ConstraintSet(), typeof(ci))
@test !MOI.supports(bridgedmock, MOI.ConstraintFunction(), typeof(ci))
test_delete_bridge(bridgedmock, ci, 4, ((MOI.VectorAffineFunction{Float64}, MOI.ExponentialCone, 0), (MOI.VectorAffineFunction{Float64}, MOI.PositiveSemidefiniteConeTriangle, 0)))
end

@testset "RootDet" begin
bridgedmock = MOIB.RootDet{Float64}(mock)
mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [1, 1, 0, 1, 1, 0, 1])
MOIT.rootdett1vtest(bridgedmock, config)
MOIT.rootdett1ftest(bridgedmock, config)
# Dual is not yet implemented for RootDet bridge
ci = first(MOI.get(bridgedmock, MOI.ListOfConstraintIndices{MOI.VectorAffineFunction{Float64}, MOI.RootDetConeTriangle}()))
@test !MOI.supports(bridgedmock, MOI.ConstraintSet(), typeof(ci))
@test !MOI.supports(bridgedmock, MOI.ConstraintFunction(), typeof(ci))
test_delete_bridge(bridgedmock, ci, 4, ((MOI.VectorAffineFunction{Float64}, MOI.RotatedSecondOrderCone, 0),
(MOI.VectorAffineFunction{Float64}, MOI.GeometricMeanCone, 0),
(MOI.VectorAffineFunction{Float64}, MOI.PositiveSemidefiniteConeTriangle, 0)))
end
end