diff --git a/docs/src/apireference.md b/docs/src/apireference.md index e48e2e1e6d..62088f67d6 100644 --- a/docs/src/apireference.md +++ b/docs/src/apireference.md @@ -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 @@ -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 diff --git a/src/Bridges/Constraint/Constraint.jl b/src/Bridges/Constraint/Constraint.jl index 26ab889631..3490fa9e9b 100644 --- a/src/Bridges/Constraint/Constraint.jl +++ b/src/Bridges/Constraint/Constraint.jl @@ -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} @@ -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}) diff --git a/src/Bridges/Constraint/soc_to_nonconvex_quad.jl b/src/Bridges/Constraint/soc_to_nonconvex_quad.jl new file mode 100644 index 0000000000..6f6c143f1b --- /dev/null +++ b/src/Bridges/Constraint/soc_to_nonconvex_quad.jl @@ -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))) + + 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 diff --git a/test/Bridges/Constraint/soc_to_nonconvex_quad.jl b/test/Bridges/Constraint/soc_to_nonconvex_quad.jl new file mode 100644 index 0000000000..483cd3b577 --- /dev/null +++ b/test/Bridges/Constraint/soc_to_nonconvex_quad.jl @@ -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