Skip to content

Add (R)SOCtoNonConvexQuad Bridge #1046

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

Merged
merged 15 commits into from
Jun 10, 2020
37 changes: 37 additions & 0 deletions docs/src/apireference.md
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,41 @@ constraints of different types. There are two important concepts to distinguish:
allow introspection into the bridge selection rationale of
[`Bridges.LazyBridgeOptimizer`](@ref).

Most bridges are added by default in [`Bridges.full_bridge_optimizer`](@ref).
However, for technical reasons, some bridges are not added by default, for instance:
[`Bridges.Constraint.SOCtoPSDBridge`](@ref), [`Bridges.Constraint.SOCtoNonConvexQuadBridge`](@ref)
and [`Bridges.Constraint.RSOCtoNonConvexQuadBridge`](@ref). See the docs of those bridges
for more information.

It is possible to add those bridges and also user defined bridges,
following one of the two methods. We present the examples for:
[`Bridges.Constraint.SOCtoNonConvexQuadBridge`](@ref).

The first option is to add the specific bridges to a
`bridged_model` optimizer, with coefficient type `T`. The `bridged_model`
optimizer itself must have been constructed with a
[`Bridges.LazyBridgeOptimizer`](@ref). Once such a optimizer is available, we
can proceed using using [`Bridges.add_bridge`](@ref):

```julia
MOIB.add_bridge(bridged_model, SOCtoNonConvexQuadBridge{T})
```

Alternatively, it is possible to create a [`Bridges.Constraint.SingleBridgeOptimizer`](@ref)
and wrap an existing `model` with it:

```julia
const SOCtoNonConvexQuad{T, OT<:ModelLike} = Bridges.Constraint.SingleBridgeOptimizer{Bridges.Constraint.SOCtoNonConvexQuadBridge{T}, OT}
bridged_model = SOCtoNonConvexQuad{Float64}(model)
```

Those procedures could be applied to user define bridges. For the
bridges defined in MathOptInterface, the [`Bridges.Constraint.SingleBridgeOptimizer`](@ref)'s are already created, therefore, for the case of [`Bridges.Constraint.SOCtoNonConvexQuadBridge`](@ref), one could simply use the existing optimizer:

```julia
bridged_model = Bridges.Constraint.SOCtoNonConvexQuad{Float64}(model)
```

```@docs
Bridges.AbstractBridge
Bridges.AbstractBridgeOptimizer
Expand Down Expand Up @@ -769,6 +804,8 @@ Bridges.Constraint.SplitIntervalBridge
Bridges.Constraint.RSOCBridge
Bridges.Constraint.SOCRBridge
Bridges.Constraint.QuadtoSOCBridge
Bridges.Constraint.SOCtoNonConvexQuadBridge
Bridges.Constraint.RSOCtoNonConvexQuadBridge
Bridges.Constraint.NormInfinityBridge
Bridges.Constraint.NormOneBridge
Bridges.Constraint.GeoMeanBridge
Expand Down
6 changes: 6 additions & 0 deletions src/Bridges/Constraint/Constraint.jl
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ include("interval.jl")
const SplitInterval{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{SplitIntervalBridge{T}, OT}
include("quad_to_soc.jl")
const QuadtoSOC{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{QuadtoSOCBridge{T}, OT}
include("soc_to_nonconvex_quad.jl") # do not add these bridges by default
const SOCtoNonConvexQuad{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{SOCtoNonConvexQuadBridge{T}, OT}
const RSOCtoNonConvexQuad{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{RSOCtoNonConvexQuadBridge{T}, OT}
include("norm_to_lp.jl")
const NormInfinity{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{NormInfinityBridge{T}, OT}
const NormOne{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{NormOneBridge{T}, OT}
Expand Down Expand Up @@ -84,6 +87,9 @@ function add_all_bridges(bridged_model, ::Type{T}) where {T}
MOIB.add_bridge(bridged_model, VectorFunctionizeBridge{T})
MOIB.add_bridge(bridged_model, SplitIntervalBridge{T})
MOIB.add_bridge(bridged_model, QuadtoSOCBridge{T})
# We do not add `(R)SOCtoNonConvexQuad` because it starts with a convex
# conic constraint and generate a non-convex constraint (in the QCP
# interpretation).
MOIB.add_bridge(bridged_model, NormInfinityBridge{T})
MOIB.add_bridge(bridged_model, NormOneBridge{T})
MOIB.add_bridge(bridged_model, GeoMeanBridge{T})
Expand Down
199 changes: 199 additions & 0 deletions src/Bridges/Constraint/soc_to_nonconvex_quad.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
abstract type AbstractSOCtoNonConvexQuadBridge{T} <: AbstractBridge end

"""
SOCtoNonConvexQuadBridge{T}

Constraints of the form `VectorOfVariables`-in-`SecondOrderCone` can be
transformed into a `ScalarQuadraticFunction`-in-`LessThan` and a
`ScalarAffineFunction`-in-`GreaterThan`. Indeed, the definition of the
second-order cone
```math
t \\ge \\lVert x \\rVert_2 \\ (1)
```
is equivalent to
```math
\\sum x_i^2 \\le t^2 (2)
```
with ``t \\ge 0``. (3)

*WARNING* This transformation starts from a convex constraint (1) and creates a
non-convex constraint (2), because the Q matrix associated with the constraint 2
has one negative eigenvalue. This might be wrongly interpreted by a solver.
Some solvers can look at (2) and understand that it is a second order cone, but
this is not a general rule.
For these reasons this bridge is not automatically added by [`MOI.Bridges.full_bridge_optimizer`](@ref).
Care is recommended when adding this bridge to a optimizer.
"""
struct SOCtoNonConvexQuadBridge{T} <: AbstractSOCtoNonConvexQuadBridge{T}
quad::CI{MOI.ScalarQuadraticFunction{T}, MOI.LessThan{T}}
var_pos::Vector{CI{MOI.ScalarAffineFunction{T}, MOI.GreaterThan{T}}}
vars::Vector{MOI.VariableIndex}
end
function bridge_constraint(::Type{SOCtoNonConvexQuadBridge{T}}, model,
func::MOI.VectorOfVariables,
set::MOI.SecondOrderCone) where T

vis = func.variables

t = vis[1]
x = vis[2:end]
a_terms = MOI.ScalarAffineTerm{T}[]
q_terms = MOI.ScalarQuadraticTerm{T}[]
push!(q_terms, MOI.ScalarQuadraticTerm(-T(2), t, t))
for var in x
push!(q_terms, MOI.ScalarQuadraticTerm(T(2), var, var))
end

fq = MOI.ScalarQuadraticFunction(a_terms, q_terms, zero(T))
quad = MOI.add_constraint(model, fq, MOI.LessThan(zero(T)))
# ScalarAffineFunction's are added instead of SingleVariable's
# because models can only have one SingleVariable per variable.
# Hence, adding a SingleVariable constraint here could conflict with
# a user defined SingleVariable
fp = convert(MOI.ScalarAffineFunction{T}, MOI.SingleVariable(t))
var_pos = MOI.add_constraint(model, fp, MOI.GreaterThan(zero(T)))
Copy link
Member

Choose a reason for hiding this comment

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

Ooooo. This is a nice way to avoid the double bound issue. Don't know why I didn't think of that for Gurobi et al. Nice job.


return SOCtoNonConvexQuadBridge(quad, [var_pos], vis)
end

"""
RSOCtoNonConvexQuadBridge{T}

Constraints of the form `VectorOfVariables`-in-`SecondOrderCone` can be
transformed into a `ScalarQuadraticFunction`-in-`LessThan` and a
`ScalarAffineFunction`-in-`GreaterThan`. Indeed, the definition of the
second-order cone
```math
2tu \\ge \\lVert x \\rVert_2^2, t,u \\ge 0 (1)
```
is equivalent to
```math
\\sum x_i^2 \\le 2tu (2)
```
with ``t,u \\ge 0``. (3)

*WARNING* This transformation starts from a convex constraint (1) and creates a
non-convex constraint (2), because the Q matrix associated with the constraint 2
has two negative eigenvalues. This might be wrongly interpreted by a solver.
Some solvers can look at (2) and understand that it is a rotated second order cone, but
this is not a general rule.
For these reasons, this bridge is not automatically added by [`MOI.Bridges.full_bridge_optimizer`](@ref).
Care is recommended when adding this bridge to an optimizer.
"""
struct RSOCtoNonConvexQuadBridge{T} <: AbstractSOCtoNonConvexQuadBridge{T}
quad::CI{MOI.ScalarQuadraticFunction{T}, MOI.LessThan{T}}
var_pos::Vector{CI{MOI.ScalarAffineFunction{T}, MOI.GreaterThan{T}}}
vars::Vector{MOI.VariableIndex}
end
function bridge_constraint(::Type{RSOCtoNonConvexQuadBridge{T}}, model,
func::MOI.VectorOfVariables,
set::MOI.RotatedSecondOrderCone) where T

vis = func.variables

t = vis[1]
u = vis[2]
x = vis[3:end]
a_terms = MOI.ScalarAffineTerm{T}[]
q_terms = MOI.ScalarQuadraticTerm{T}[]
push!(q_terms, MOI.ScalarQuadraticTerm(-T(2), t, u))
for var in x
push!(q_terms, MOI.ScalarQuadraticTerm(T(2), var, var))
end

fq = MOI.ScalarQuadraticFunction(a_terms, q_terms, zero(T))
quad = MOI.add_constraint(model, fq, MOI.LessThan(zero(T)))
# ScalarAffineFunction's are added instead of SingleVariable's
# because models can only have one SingleVariable per variable.
# Hence, adding a SingleVariable constraint here could conflict with
# a user defined SingleVariable
fp1 = convert(MOI.ScalarAffineFunction{T}, MOI.SingleVariable(t))
var_pos1 = MOI.add_constraint(model, fp1, MOI.GreaterThan(zero(T)))
fp2 = convert(MOI.ScalarAffineFunction{T}, MOI.SingleVariable(u))
var_pos2 = MOI.add_constraint(model, fp2, MOI.GreaterThan(zero(T)))

return RSOCtoNonConvexQuadBridge(quad, [var_pos1, var_pos2], vis)
end

function MOI.supports_constraint(::Type{SOCtoNonConvexQuadBridge{T}},
::Type{MOI.VectorOfVariables},
::Type{MOI.SecondOrderCone}) where T
return true
end
function MOI.supports_constraint(::Type{RSOCtoNonConvexQuadBridge{T}},
::Type{MOI.VectorOfVariables},
::Type{MOI.RotatedSecondOrderCone}) where T
return true
end

MOIB.added_constrained_variable_types(::Type{<:AbstractSOCtoNonConvexQuadBridge}) = Tuple{DataType}[]
function MOIB.added_constraint_types(::Type{<:AbstractSOCtoNonConvexQuadBridge{T}}) where T
return [
(MOI.ScalarQuadraticFunction{T}, MOI.LessThan{T}),
(MOI.ScalarAffineFunction{T}, MOI.GreaterThan{T}),
]
end

function concrete_bridge_type(::Type{SOCtoNonConvexQuadBridge{T}},
::Type{MOI.VectorOfVariables},
::Type{MOI.SecondOrderCone}) where T
return SOCtoNonConvexQuadBridge{T}
end
function concrete_bridge_type(::Type{RSOCtoNonConvexQuadBridge{T}},
::Type{MOI.VectorOfVariables},
::Type{MOI.RotatedSecondOrderCone}) where T
return RSOCtoNonConvexQuadBridge{T}
end

# Attributes, Bridge acting as a model
function MOI.get(::AbstractSOCtoNonConvexQuadBridge{T},
::MOI.NumberOfConstraints{MOI.ScalarQuadraticFunction{T},
MOI.LessThan{T}}) where T
return 1
end

function MOI.get(bridge::AbstractSOCtoNonConvexQuadBridge{T},
::MOI.ListOfConstraintIndices{
MOI.ScalarQuadraticFunction{T},
MOI.LessThan{T}}) where T
return [bridge.quad]
end

function MOI.get(bridge::AbstractSOCtoNonConvexQuadBridge{T},
::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},
MOI.GreaterThan{T}}) where T
return length(bridge.var_pos)
end

function MOI.get(bridge::AbstractSOCtoNonConvexQuadBridge{T},
::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},
MOI.GreaterThan{T}}) where T
return bridge.var_pos
end

# References
function MOI.delete(model::MOI.ModelLike, bridge::AbstractSOCtoNonConvexQuadBridge)
MOI.delete(model, bridge.quad)
MOI.delete.(model, bridge.var_pos)
end

# Attributes, Bridge acting as a constraint
function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintPrimal,
bridge::AbstractSOCtoNonConvexQuadBridge)
vals = MOI.get.(model, MOI.VariablePrimal(attr.N), bridge.vars)
return vals
end

function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintSet,
b::SOCtoNonConvexQuadBridge{T}) where T
return MOI.SecondOrderCone(length(b.vars))
end
function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintSet,
b::RSOCtoNonConvexQuadBridge{T}) where T
return MOI.RotatedSecondOrderCone(length(b.vars))
end

function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintFunction,
b::AbstractSOCtoNonConvexQuadBridge{T}) where T
return MOI.VectorOfVariables(b.vars)
end
83 changes: 83 additions & 0 deletions test/Bridges/Constraint/soc_to_nonconvex_quad.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using Test

using MathOptInterface
const MOI = MathOptInterface
const MOIT = MathOptInterface.Test
const MOIU = MathOptInterface.Utilities
const MOIB = MathOptInterface.Bridges
const MOIBC = MathOptInterface.Bridges.Constraint

include("../utilities.jl")

mock = MOIU.MockOptimizer(MOIU.UniversalFallback(MOIU.Model{Float64}()))
config = MOIT.TestConfig(duals = false)

@testset "RSOCtoNonConvexQuad" begin

@test MOIBC.RSOCtoNonConvexQuadBridge{Float64} == MOIBC.concrete_bridge_type(
MOIBC.RSOCtoNonConvexQuadBridge{Float64},
MOI.VectorOfVariables,
MOI.RotatedSecondOrderCone)
@test MOI.supports_constraint(MOIBC.RSOCtoNonConvexQuadBridge{Float64},
MOI.VectorOfVariables,
MOI.RotatedSecondOrderCone)
@test !MOI.supports_constraint(MOIBC.RSOCtoNonConvexQuadBridge{Float64},
MOI.ScalarAffineFunction{Float64},
MOI.RotatedSecondOrderCone)

bridged_mock = MOIB.Constraint.RSOCtoNonConvexQuad{Float64}(mock)

MOIT.basic_constraint_tests(
bridged_mock, config,
include = [(F, S)
for F in [MOI.VectorOfVariables]
for S in [MOI.RotatedSecondOrderCone]])

mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [0.5, 1.0, 1/√2, 1/√2])
MOIT.rotatedsoc1vtest(bridged_mock, config)

ci = first(MOI.get(bridged_mock,
MOI.ListOfConstraintIndices{MOI.VectorOfVariables,
MOI.RotatedSecondOrderCone}()))

test_delete_bridge(bridged_mock, ci, 4,
(
(MOI.ScalarQuadraticFunction{Float64}, MOI.LessThan{Float64}, 0),
(MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}, 0),
))
end

@testset "SOCtoNonConvexQuad" begin

@test MOIBC.SOCtoNonConvexQuadBridge{Float64} == MOIBC.concrete_bridge_type(
MOIBC.SOCtoNonConvexQuadBridge{Float64},
MOI.VectorOfVariables,
MOI.SecondOrderCone)
@test MOI.supports_constraint(MOIBC.SOCtoNonConvexQuadBridge{Float64},
MOI.VectorOfVariables,
MOI.SecondOrderCone)
@test !MOI.supports_constraint(MOIBC.SOCtoNonConvexQuadBridge{Float64},
MOI.ScalarAffineFunction{Float64},
MOI.SecondOrderCone)

bridged_mock = MOIB.Constraint.SOCtoNonConvexQuad{Float64}(mock)

MOIT.basic_constraint_tests(
bridged_mock, config,
include = [(F, S)
for F in [MOI.VectorOfVariables]
for S in [MOI.SecondOrderCone]])

mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [1.0, 1/√2, 1/√2])
MOIT.soc1vtest(bridged_mock, config)

ci = first(MOI.get(bridged_mock,
MOI.ListOfConstraintIndices{MOI.VectorOfVariables,
MOI.SecondOrderCone}()))

test_delete_bridge(bridged_mock, ci, 3,
(
(MOI.ScalarQuadraticFunction{Float64}, MOI.LessThan{Float64}, 0),
(MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}, 0),
))
end