diff --git a/src/Bridges/Variable/Variable.jl b/src/Bridges/Variable/Variable.jl index f76a2cd3d2..d725cfe0d0 100644 --- a/src/Bridges/Variable/Variable.jl +++ b/src/Bridges/Variable/Variable.jl @@ -19,5 +19,7 @@ include("zeros.jl") const Zeros{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{ZerosBridge{T}, OT} include("flip_sign.jl") const NonposToNonneg{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{NonposToNonnegBridge{T}, OT} +include("rsoc_to_psd.jl") +const RSOCtoPSD{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{RSOCtoPSDBridge{T}, OT} end diff --git a/src/Bridges/Variable/rsoc_to_psd.jl b/src/Bridges/Variable/rsoc_to_psd.jl new file mode 100644 index 0000000000..7c5a902163 --- /dev/null +++ b/src/Bridges/Variable/rsoc_to_psd.jl @@ -0,0 +1,179 @@ +""" + RSOCtoPSDBridge{T} <: Bridges.Variable.AbstractBridge + +Transforms constrained variables in [`MathOptInterface.RotatedSecondOrderCone`](@ref) +to constrained variables in [`MathOptInterface.PositiveSemidefiniteConeTriangle`](@ref). +""" +struct RSOCtoPSDBridge{T} <: AbstractBridge + # `t` is `variables[1]` + # `u` is `variables[2]/2` + # `x` is `variables[[3, 5, 8, ...]]` + variables::Vector{MOI.VariableIndex} + psd::MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.PositiveSemidefiniteConeTriangle} + off_diag::Vector{MOI.ConstraintIndex{MOI.SingleVariable, MOI.EqualTo{T}}} + diag::Vector{MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, MOI.EqualTo{T}}} +end +function bridge_constrained_variable(::Type{RSOCtoPSDBridge{T}}, + model::MOI.ModelLike, + set::MOI.RotatedSecondOrderCone) where T + dim = set.dimension - 1 + variables, psd = MOI.add_constrained_variables( + model, MOI.PositiveSemidefiniteConeTriangle(dim)) + # This is `2 * u` + u2 = MOI.SingleVariable(variables[3]) + off_diag = MOI.ConstraintIndex{MOI.SingleVariable, MOI.EqualTo{T}}[] + diag = MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, MOI.EqualTo{T}}[] + k = 3 + for j in 3:dim + k += 1 + for i in 2:(j-1) + k += 1 + push!(off_diag, + MOI.add_constraint(model, MOI.SingleVariable(variables[k]), + MOI.EqualTo(zero(T)))) + end + k += 1 + func = MOIU.operate(-, T, u2, MOI.SingleVariable(variables[k])) + push!(diag, MOI.add_constraint(model, func, MOI.EqualTo(zero(T)))) + end + @assert k == trimap(dim, dim) + return RSOCtoPSDBridge{T}(variables, psd, off_diag, diag) +end + +function supports_constrained_variable( + ::Type{<:RSOCtoPSDBridge}, ::Type{MOI.RotatedSecondOrderCone}) + return true +end +function MOIB.added_constrained_variable_types(::Type{<:RSOCtoPSDBridge}) + return [(MOI.PositiveSemidefiniteConeTriangle,)] +end +function MOIB.added_constraint_types(::Type{RSOCtoPSDBridge{T}}) where T + return [(MOI.SingleVariable, MOI.EqualTo{T}), (MOI.ScalarAffineFunction{T}, MOI.EqualTo{T})] +end + +# Attributes, Bridge acting as a model +function MOI.get(bridge::RSOCtoPSDBridge, ::MOI.NumberOfVariables) + return length(bridge.variables) +end +function MOI.get(bridge::RSOCtoPSDBridge, ::MOI.ListOfVariableIndices) + return bridge.variables +end +function MOI.get(bridge::RSOCtoPSDBridge, + ::MOI.NumberOfConstraints{MOI.VectorOfVariables, + MOI.PositiveSemidefiniteConeTriangle}) + return 1 +end +function MOI.get(bridge::RSOCtoPSDBridge, + ::MOI.ListOfConstraintIndices{MOI.VectorOfVariables, + MOI.PositiveSemidefiniteConeTriangle}) + return [bridge.psd] +end +function MOI.get(bridge::RSOCtoPSDBridge{T}, + ::MOI.NumberOfConstraints{MOI.SingleVariable, + MOI.EqualTo{T}}) where T + return length(bridge.off_diag) +end +function MOI.get(bridge::RSOCtoPSDBridge{T}, + ::MOI.ListOfConstraintIndices{MOI.SingleVariable, + MOI.EqualTo{T}}) where T + return bridge.off_diag +end +function MOI.get(bridge::RSOCtoPSDBridge{T}, + ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T}, + MOI.EqualTo{T}}) where T + return length(bridge.diag) +end +function MOI.get(bridge::RSOCtoPSDBridge{T}, + ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T}, + MOI.EqualTo{T}}) where T + return bridge.diag +end + +# References +function MOI.delete(model::MOI.ModelLike, bridge::RSOCtoPSDBridge) + for ci in bridge.diag + MOI.delete(model, ci) + end + MOI.delete(model, bridge.variables) +end + +# Attributes, Bridge acting as a constraint + +function MOI.get(::MOI.ModelLike, ::MOI.ConstraintSet, + bridge::RSOCtoPSDBridge{T}) where T + return MOI.RotatedSecondOrderCone(length(bridge.diag) + 3) +end + +function trimap(i::Integer, j::Integer) + if i < j + return trimap(j, i) + else + return div((i-1) * i, 2) + j + end +end +function _variable_map(i::IndexInVector) + if i.value == 1 + return 1 + elseif i.value == 2 + return 3 + else + return trimap(1, i.value - 1) + end +end +function _variable(bridge::RSOCtoPSDBridge, i::IndexInVector) + return bridge.variables[_variable_map(i)] +end + +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintPrimal, + bridge::RSOCtoPSDBridge{T}) where T + values = MOI.get(model, attr, bridge.psd) + n = MOI.dimension(MOI.get(model, MOI.ConstraintSet(), bridge)) + mapped = [values[_variable_map(IndexInVector(i))] for i in 1:n] + mapped[2] /= 2 + return mapped +end +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintDual, + bridge::RSOCtoPSDBridge{T}) where T + dual = MOI.get(model, attr, bridge.psd) + n = MOI.dimension(MOI.get(model, MOI.ConstraintSet(), bridge)) + mapped = [dual[_variable_map(IndexInVector(i))] for i in 1:n] + for ci in bridge.diag + mapped[2] += MOI.get(model, attr, ci) + end + for i in 2:length(mapped) + # For `i = 2`, we multiply by 2 because it is `2u`. + # For `i > 2`, we multiply by 2 because to account for the difference + # of scalar product `MOIU.set_dot`. + mapped[i] *= 2 + end + return mapped +end + +function MOI.get(model::MOI.ModelLike, attr::MOI.VariablePrimal, + bridge::RSOCtoPSDBridge{T}, i::IndexInVector) where T + value = MOI.get(model, attr, _variable(bridge, i)) + if i.value == 2 + return value / 2 + else + return value + end +end + +function MOIB.bridged_function(bridge::RSOCtoPSDBridge{T}, i::IndexInVector) where T + func = MOI.SingleVariable(_variable(bridge, i)) + if i.value == 2 + return MOIU.operate(/, T, func, convert(T, 2)) + else + return convert(MOI.ScalarAffineFunction{T}, func) + end +end +function unbridged_map(bridge::RSOCtoPSDBridge{T}, vi::MOI.VariableIndex, + i::IndexInVector) where T + sv = MOI.SingleVariable(vi) + if i.value == 2 + func = MOIU.operate(*, T, convert(T, 2), sv) + else + func = convert(MOI.ScalarAffineFunction{T}, sv) + end + return (_variable(bridge, i) => func,) +end diff --git a/test/Bridges/Variable/Variable.jl b/test/Bridges/Variable/Variable.jl index 8f7a73db6f..819f37da05 100644 --- a/test/Bridges/Variable/Variable.jl +++ b/test/Bridges/Variable/Variable.jl @@ -8,3 +8,6 @@ end @testset "FlipSign" begin include("flip_sign.jl") end +@testset "RSOCtoPSD" begin + include("rsoc_to_psd.jl") +end diff --git a/test/Bridges/Variable/rsoc_to_psd.jl b/test/Bridges/Variable/rsoc_to_psd.jl new file mode 100644 index 0000000000..19b731c7cf --- /dev/null +++ b/test/Bridges/Variable/rsoc_to_psd.jl @@ -0,0 +1,104 @@ +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() + +bridged_mock = MOIB.Variable.RSOCtoPSD{Float64}(mock) + +@testset "RSOC4" begin + mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [1.0, 1.0, 2.0, 1.0, 0.0, 2.0], + (MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}) => [0.25], + (MOI.SingleVariable, MOI.EqualTo{Float64}) => [-0.5], + (MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}) => [-1.0], + (MOI.VectorOfVariables, MOI.PositiveSemidefiniteConeTriangle) => [[1.0, -0.5, 0.25, -0.5, 0.25, 0.25]]) + mock.eval_variable_constraint_dual = false + MOIT.rotatedsoc4test(bridged_mock, config) + mock.eval_variable_constraint_dual = true + + @testset "Test mock model" begin + var_names = ["Q$i$j" for j in 1:3 for i in 1:j] + MOI.set( + mock, MOI.VariableName(), + MOI.get(mock, MOI.ListOfVariableIndices()), var_names) + psd = MOI.get(mock, MOI.ListOfConstraintIndices{ + MOI.VectorOfVariables, MOI.PositiveSemidefiniteConeTriangle}()) + @test length(psd) == 1 + MOI.set(mock, MOI.ConstraintName(), psd[1], "psd") + off_diag = MOI.get(mock, MOI.ListOfConstraintIndices{ + MOI.SingleVariable, MOI.EqualTo{Float64}}()) + @test length(off_diag) == 1 + MOI.set(mock, MOI.ConstraintName(), off_diag[1], "off_diag23") + diag = MOI.get(mock, MOI.ListOfConstraintIndices{ + MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}}()) + @test length(diag) == 1 + MOI.set(mock, MOI.ConstraintName(), diag[1], "diag33") + c = MOI.get(mock, MOI.ListOfConstraintIndices{ + MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}}()) + @test length(c) == 1 + MOI.set(mock, MOI.ConstraintName(), c[1], "c") + + s = """ + variables: Q11, Q12, Q13, Q22, Q23, Q33 + psd: [Q11, Q12, Q22, Q13, Q23, Q33] in MathOptInterface.PositiveSemidefiniteConeTriangle(3) + off_diag23: Q23 == 0.0 + diag33: Q22 + -1.0Q33 == 0.0 + c: Q11 + 0.5Q22 <= 2.0 + maxobjective: Q12 + Q13 + """ + model = MOIU.Model{Float64}() + MOIU.loadfromstring!(model, s) + MOIU.test_models_equal(mock, model, var_names, ["psd", "off_diag23", "diag33", "c"]) + end + + @testset "Test bridged model" begin + var_names = ["t", "u", "x", "y"] + MOI.set( + bridged_mock, MOI.VariableName(), + MOI.get(bridged_mock, MOI.ListOfVariableIndices()), var_names) + rsoc = MOI.get(bridged_mock, MOI.ListOfConstraintIndices{ + MOI.VectorOfVariables, MOI.RotatedSecondOrderCone}()) + @test length(rsoc) == 1 + MOI.set(bridged_mock, MOI.ConstraintName(), rsoc[1], "rsoc") + c = MOI.get(bridged_mock, MOI.ListOfConstraintIndices{ + MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}}()) + @test length(c) == 1 + MOI.set(bridged_mock, MOI.ConstraintName(), c[1], "c") + + s = """ + variables: t, u, x, y + rsoc: [t, u, x, y] in MathOptInterface.RotatedSecondOrderCone(4) + c: t + u <= 2.0 + maxobjective: x + y + """ + model = MOIU.Model{Float64}() + MOIU.loadfromstring!(model, s) + MOIU.test_models_equal(bridged_mock, model, var_names, ["rsoc", "c"]) + end + + + @testset "Delete" begin + v = MOI.get(bridged_mock, MOI.ListOfVariableIndices()) + @test length(v) == 4 + + message = string("Cannot delete variable as it is constrained with other", + " variables in a `MOI.VectorOfVariables`.") + for i in 1:4 + err = MOI.DeleteNotAllowed(v[i], message) + @test_throws err MOI.delete(bridged_mock, v[i]) + end + + test_delete_bridged_variables(bridged_mock, v, MOI.RotatedSecondOrderCone, 4, ( + (MOI.VectorOfVariables, MOI.PositiveSemidefiniteConeTriangle, 0), + (MOI.SingleVariable, MOI.EqualTo{Float64}, 0), + (MOI.ScalarAffineFunction{Float64}, MOI.EqualTo{Float64}, 0), + )) + end +end