diff --git a/docs/src/apireference.md b/docs/src/apireference.md index 90275a08de..67998d50e2 100644 --- a/docs/src/apireference.md +++ b/docs/src/apireference.md @@ -707,6 +707,7 @@ Bridges.Constraint.ScalarFunctionizeBridge Bridges.Constraint.VectorFunctionizeBridge Bridges.Constraint.SplitIntervalBridge Bridges.Constraint.RSOCBridge +Bridges.Constraint.SOCRBridge Bridges.Constraint.QuadtoSOCBridge Bridges.Constraint.GeoMeanBridge Bridges.Constraint.SquareBridge diff --git a/src/Bridges/Constraint/Constraint.jl b/src/Bridges/Constraint/Constraint.jl index 5041121052..6197a1e503 100644 --- a/src/Bridges/Constraint/Constraint.jl +++ b/src/Bridges/Constraint/Constraint.jl @@ -36,6 +36,8 @@ include("interval.jl") const SplitInterval{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{SplitIntervalBridge{T}, OT} include("rsoc.jl") const RSOC{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{RSOCBridge{T}, OT} +include("socr.jl") +const SOCR{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{SOCRBridge{T}, OT} include("quad_to_soc.jl") const QuadtoSOC{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{QuadtoSOCBridge{T}, OT} include("geomean.jl") @@ -74,8 +76,10 @@ function add_all_bridges(bridged_model, T::Type) MOIB.add_bridge(bridged_model, LogDetBridge{T}) MOIB.add_bridge(bridged_model, RootDetBridge{T}) MOIB.add_bridge(bridged_model, RSOCBridge{T}) + MOIB.add_bridge(bridged_model, SOCRBridge{T}) + # We do not add `SOCtoPSDBridge` as transforming the `SOC` to `RSOC` and + # then to `PSD` produces a smaller SDP constraint. MOIB.add_bridge(bridged_model, RSOCtoPSDBridge{T}) - MOIB.add_bridge(bridged_model, SOCtoPSDBridge{T}) MOIB.add_bridge(bridged_model, IndicatorActiveOnFalseBridge{T}) return end diff --git a/src/Bridges/Constraint/rsoc.jl b/src/Bridges/Constraint/rsoc.jl index adf063d4b7..7b75878fce 100644 --- a/src/Bridges/Constraint/rsoc.jl +++ b/src/Bridges/Constraint/rsoc.jl @@ -58,7 +58,7 @@ function concrete_bridge_type(::Type{<:RSOCBridge{T}}, Y = MOIU.promote_operation(-, T, S, S) Z = MOIU.promote_operation(+, T, S, S) F = MOIU.promote_operation(vcat, T, Z, Y, G) - RSOCBridge{T, F, G} + return RSOCBridge{T, F, G} end # Attributes, Bridge acting as a model @@ -72,8 +72,8 @@ function MOI.get(b::RSOCBridge{T, F}, end # References -function MOI.delete(model::MOI.ModelLike, c::RSOCBridge) - MOI.delete(model, c.soc) +function MOI.delete(model::MOI.ModelLike, bridge::RSOCBridge) + MOI.delete(model, bridge.soc) end # Attributes, Bridge acting as a constraint @@ -88,12 +88,22 @@ function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintSet, bridge::RSOCBrid return MOI.RotatedSecondOrderCone(MOI.dimension(set)) end # As the linear transformation is a symmetric involution, -# the constraint primal and dual both need to be processed by reapplying the same transformation -function _get(model, attr::Union{MOI.ConstraintPrimal, MOI.ConstraintDual}, c::RSOCBridge) - x = MOI.get(model, attr, c.soc) +# the constraint primal and dual both need to be processed by reapplying the +# same transformation +function rotate_result(model, + attr::Union{MOI.ConstraintPrimal, MOI.ConstraintDual}, + ci::MOI.ConstraintIndex) + x = MOI.get(model, attr, ci) s2 = √2 - [x[1]/s2+x[2]/s2; x[1]/s2-x[2]/s2; x[3:end]] + return [x[1]/s2 + x[2]/s2; x[1]/s2 - x[2]/s2; x[3:end]] +end +# Need to define both `get` methods and redirect to `rotate_result` to avoid +# ambiguity in dispatch +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintPrimal, + bridge::RSOCBridge) + return rotate_result(model, attr, bridge.soc) +end +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintDual, + bridge::RSOCBridge) + return rotate_result(model, attr, bridge.soc) end -# Need to define both `get` methods and redirect to `_get` to avoid ambiguity in dispatch -MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintPrimal, c::RSOCBridge) = _get(model, attr, c) -MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintDual, c::RSOCBridge) = _get(model, attr, c) diff --git a/src/Bridges/Constraint/soc_to_psd.jl b/src/Bridges/Constraint/soc_to_psd.jl index 85257f692a..f9973fff5a 100644 --- a/src/Bridges/Constraint/soc_to_psd.jl +++ b/src/Bridges/Constraint/soc_to_psd.jl @@ -50,6 +50,10 @@ which is equivalent to t^2 & > x^\\top x \\end{align*} ``` +This bridge is not added by default by [`full_bridge_optimizer`](@ref) as +bridging second order cone constraints to semidefinite constraints can be +achieved by the [`SOCRBridge`](@ref) followed by the [`RSOCtoPSDBridge`](@ref) +while creating a smaller semidefinite constraint. """ struct SOCtoPSDBridge{T} <: AbstractBridge dim::Int diff --git a/src/Bridges/Constraint/socr.jl b/src/Bridges/Constraint/socr.jl new file mode 100644 index 0000000000..98bbb0b91a --- /dev/null +++ b/src/Bridges/Constraint/socr.jl @@ -0,0 +1,74 @@ +""" + SOCRBridge{T, F, G} + +The `SecondOrderCone` is `RotatedSecondOrderCone`. We simply do the inverse +transformation of [`RSOCBridge`](@ref). In fact, as the transformation is an +involution, we do the same transformation. + +""" +struct SOCRBridge{T, F, G} <: AbstractBridge + rsoc::CI{F, MOI.RotatedSecondOrderCone} +end +function bridge_constraint(::Type{SOCRBridge{T, F, G}}, model, + f::MOI.AbstractVectorFunction, + s::MOI.SecondOrderCone) where {T, F, G} + soc = MOI.add_constraint(model, rotate_function(f, T), + MOI.RotatedSecondOrderCone(MOI.dimension(s))) + return SOCRBridge{T, F, G}(soc) +end + +function MOI.supports_constraint(::Type{SOCRBridge{T}}, + ::Type{<:MOI.AbstractVectorFunction}, + ::Type{MOI.SecondOrderCone}) where T + return true +end +MOIB.added_constrained_variable_types(::Type{<:SOCRBridge}) = Tuple{DataType}[] +function MOIB.added_constraint_types(::Type{<:SOCRBridge{T, F}}) where {T, F} + return [(F, MOI.RotatedSecondOrderCone)] +end +function concrete_bridge_type(::Type{<:SOCRBridge{T}}, + G::Type{<:MOI.AbstractVectorFunction}, + ::Type{MOI.SecondOrderCone}) where T + S = MOIU.promote_operation(/, T, MOIU.scalar_type(G), T) + Y = MOIU.promote_operation(-, T, S, S) + Z = MOIU.promote_operation(+, T, S, S) + F = MOIU.promote_operation(vcat, T, Z, Y, G) + return SOCRBridge{T, F, G} +end + +# Attributes, Bridge acting as an model +function MOI.get(b::SOCRBridge{T, F}, + ::MOI.NumberOfConstraints{F, MOI.RotatedSecondOrderCone}) where {T, F} + return 1 +end +function MOI.get(b::SOCRBridge{T, F}, + ::MOI.ListOfConstraintIndices{F, MOI.RotatedSecondOrderCone}) where {T, F} + return [b.rsoc] +end + +# References +function MOI.delete(model::MOI.ModelLike, bridge::SOCRBridge) + MOI.delete(model, bridge.rsoc) +end + +# Attributes, Bridge acting as a constraint +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintFunction, + bridge::SOCRBridge{T, F, G}) where {T, F, G} + # As it is an involution, we can just reapply the same transformation + func = MOI.get(model, attr, bridge.rsoc) + return MOIU.convert_approx(G, rotate_function(func, T)) +end +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintSet, bridge::SOCRBridge) + set = MOI.get(model, attr, bridge.rsoc) + return MOI.SecondOrderCone(MOI.dimension(set)) +end +# As the linear transformation is a symmetric involution, +# the constraint primal and dual both need to be processed by reapplying the same transformation +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintPrimal, + bridge::SOCRBridge) + return rotate_result(model, attr, bridge.rsoc) +end +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintDual, + bridge::SOCRBridge) + return rotate_result(model, attr, bridge.rsoc) +end diff --git a/test/Bridges/Constraint/Constraint.jl b/test/Bridges/Constraint/Constraint.jl index dbce4c1017..af0269353e 100644 --- a/test/Bridges/Constraint/Constraint.jl +++ b/test/Bridges/Constraint/Constraint.jl @@ -9,6 +9,7 @@ include("slack.jl") include("functionize.jl") include("interval.jl") include("rsoc.jl") +include("socr.jl") include("quad_to_soc.jl") include("geomean.jl") include("square.jl") diff --git a/test/Bridges/Constraint/socr.jl b/test/Bridges/Constraint/socr.jl new file mode 100644 index 0000000000..446d3fb6f8 --- /dev/null +++ b/test/Bridges/Constraint/socr.jl @@ -0,0 +1,39 @@ +using Test + +using MathOptInterface +const MOI = MathOptInterface +const MOIT = MathOptInterface.Test +const MOIU = MathOptInterface.Utilities +const MOIB = MathOptInterface.Bridges + +include("utilities.jl") + +mock = MOIU.MockOptimizer(MOIU.Model{Float64}()) +config = MOIT.TestConfig() + +@testset "RSOC" begin + bridged_mock = MOIB.SOCR{Float64}(mock) + + MOIT.basic_constraint_tests( + bridged_mock, config, + include = [(F, S) + for F in [MOI.VectorOfVariables, MOI.VectorAffineFunction{Float64}, + MOI.VectorQuadraticFunction{Float64}] + for S in [MOI.SecondOrderCone]]) + + mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [1.0, 1/√2, 1/√2], + (MOI.VectorAffineFunction{Float64}, MOI.RotatedSecondOrderCone) => [[1 - 1/√2, 1 + 1/√2, -1]], + (MOI.VectorAffineFunction{Float64}, MOI.Zeros) => [[-√2]]) + MOIT.soc1vtest(bridged_mock, config) + mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [1.0, 1/√2, 1/√2], + (MOI.VectorAffineFunction{Float64}, MOI.RotatedSecondOrderCone) => [[1 - 1/√2, 1 + 1/√2, -1]], + (MOI.VectorAffineFunction{Float64}, MOI.Zeros) => [[-√2]]) + MOIT.soc1ftest(bridged_mock, config) + ci = first(MOI.get( + bridged_mock, + MOI.ListOfConstraintIndices{MOI.VectorAffineFunction{Float64}, + MOI.SecondOrderCone}())) + test_delete_bridge(bridged_mock, ci, 3, + ((MOI.VectorAffineFunction{Float64}, + MOI.RotatedSecondOrderCone, 0),)) +end