diff --git a/src/Bridges/Variable/Variable.jl b/src/Bridges/Variable/Variable.jl index d725cfe0d0..0f41b3860c 100644 --- a/src/Bridges/Variable/Variable.jl +++ b/src/Bridges/Variable/Variable.jl @@ -19,6 +19,8 @@ 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("vectorize.jl") +const Vectorize{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{VectorizeBridge{T}, OT} include("rsoc_to_psd.jl") const RSOCtoPSD{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{RSOCtoPSDBridge{T}, OT} diff --git a/src/Bridges/Variable/vectorize.jl b/src/Bridges/Variable/vectorize.jl new file mode 100644 index 0000000000..327b691a69 --- /dev/null +++ b/src/Bridges/Variable/vectorize.jl @@ -0,0 +1,113 @@ +""" + VectorizeBridge{T, S} + +Transforms a constrained variable in `scalar_set_type(S, T)` where +`S <: VectorLinearSet` into a constrained vector of one variable in `S`. For +instance, `VectorizeBridge{Float64, MOI.Nonnegatives}` transforms a constrained +variable in `MOI.GreaterThan{Float64}` into a constrained vector of one +variable in `MOI.Nonnegatives`. +""" +mutable struct VectorizeBridge{T, S} <: AbstractBridge + variable::MOI.VariableIndex + vector_constraint::MOI.ConstraintIndex{MOI.VectorOfVariables, S} + set_constant::T # constant in scalar set +end +function bridge_constrained_variable( + ::Type{VectorizeBridge{T, S}}, + model::MOI.ModelLike, set::MOIU.ScalarLinearSet{T}) where {T, S} + set_constant = MOI.constant(set) + variables, vector_constraint = MOI.add_constrained_variables(model, S(1)) + return VectorizeBridge{T, S}(variables[1], vector_constraint, set_constant) +end + +function supports_constrained_variable( + ::Type{VectorizeBridge{T}}, ::Type{<:MOIU.ScalarLinearSet{T}}) where T + return true +end +function MOIB.added_constrained_variable_types(::Type{VectorizeBridge{T, S}}) where {T, S} + return [(S,)] +end +function MOIB.added_constraint_types(::Type{<:VectorizeBridge}) + return Tuple{DataType, DataType}[] +end +function concrete_bridge_type(::Type{<:VectorizeBridge{T}}, + S::Type{<:MOIU.ScalarLinearSet{T}}) where T + return VectorizeBridge{T, MOIU.vector_set_type(S)} +end + +# Attributes, Bridge acting as a model +function MOI.get(bridge::VectorizeBridge, ::MOI.NumberOfVariables) + return 1 +end +function MOI.get(bridge::VectorizeBridge, ::MOI.ListOfVariableIndices) + return [bridge.variable] +end +function MOI.get(::VectorizeBridge{T, S}, + ::MOI.NumberOfConstraints{MOI.VectorOfVariables, S}) where {T, S} + return 1 +end +function MOI.get(bridge::VectorizeBridge{T, S}, + ::MOI.ListOfConstraintIndices{MOI.VectorOfVariables, S}) where {T, S} + return [bridge.vector_constraint] +end + +# References +function MOI.delete(model::MOI.ModelLike, bridge::VectorizeBridge) + MOI.delete(model, bridge.variable) +end + +# Attributes, Bridge acting as a constraint + +function MOI.get(model::MOI.ModelLike, ::MOI.ConstraintSet, + bridge::VectorizeBridge{T, S}) where {T, S} + return MOIU.scalar_set_type(S, T)(bridge.set_constant) +end + +function MOI.set(model::MOI.ModelLike, attr::MOI.ConstraintSet, + bridge::VectorizeBridge, new_set::MOIU.ScalarLinearSet) + # This would require modifing any constraint which uses the bridged + # variable. + throw(MOI.SetAttributeNotAllowed(attr, + "The variable `$(bridge.variable)` is bridged by the `VectorizeBridge`.")) +end + +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintPrimal, + bridge::VectorizeBridge) + x = MOI.get(model, attr, bridge.vector_constraint) + @assert length(x) == 1 + y = x[1] + if !MOIU.is_ray(MOI.get(model, MOI.PrimalStatus(attr.N))) + # If it is an infeasibility certificate, it is a ray and satisfies the + # homogenized problem, see https://github.com/JuliaOpt/MathOptInterface.jl/issues/433 + # Otherwise, we need to add the set constant since the ConstraintPrimal + # is defined as the value of the function and the set_constant was + # removed from the original function + y += bridge.set_constant + end + return y +end +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintDual, + bridge::VectorizeBridge) + x = MOI.get(model, attr, bridge.vector_constraint) + @assert length(x) == 1 + return x[1] +end + +function MOI.get(model::MOI.ModelLike, attr::MOI.VariablePrimal, + bridge::VectorizeBridge) + value = MOI.get(model, attr, bridge.variable) + if !MOIU.is_ray(MOI.get(model, MOI.PrimalStatus(attr.N))) + value += bridge.set_constant + end + return value +end + +function MOIB.bridged_function(bridge::VectorizeBridge{T}) where T + func = MOI.SingleVariable(bridge.variable) + return MOIU.operate(+, T, func, bridge.set_constant) +end +function unbridged_map(bridge::VectorizeBridge{T}, vi::MOI.VariableIndex) where T + func = MOIU.operate(-, T, MOI.SingleVariable(vi), + bridge.set_constant) + return (bridge.variable => func,) +end diff --git a/src/Bridges/bridge_optimizer.jl b/src/Bridges/bridge_optimizer.jl index 78a0ed38ec..a4ac0bafbd 100644 --- a/src/Bridges/bridge_optimizer.jl +++ b/src/Bridges/bridge_optimizer.jl @@ -999,13 +999,11 @@ scalar. function unbridged_constraint_function end function unbridged_constraint_function( - b::AbstractBridgeOptimizer, - func::Union{MOI.AbstractVectorSet, MOI.SingleVariable} -) + b::AbstractBridgeOptimizer, func::MOI.AbstractVectorFunction) return unbridged_function(b, func) end -function unbridged_constraint_function(b::AbstractBridgeOptimizer, - func::MOI.AbstractScalarFunction) +function unbridged_constraint_function( + b::AbstractBridgeOptimizer, func::MOI.AbstractScalarFunction) if !Variable.has_bridges(Variable.bridges(b)) return func end @@ -1017,8 +1015,8 @@ function unbridged_constraint_function(b::AbstractBridgeOptimizer, return f end function unbridged_constraint_function( - ::AbstractBridgeOptimizer, func::MOI.AbstractVectorFunction) - return func + b::AbstractBridgeOptimizer, func::MOI.SingleVariable) + return unbridged_function(b, func) end diff --git a/test/Bridges/Variable/Variable.jl b/test/Bridges/Variable/Variable.jl index 819f37da05..e6520c5c23 100644 --- a/test/Bridges/Variable/Variable.jl +++ b/test/Bridges/Variable/Variable.jl @@ -8,6 +8,9 @@ end @testset "FlipSign" begin include("flip_sign.jl") end +@testset "Vectorize" begin + include("vectorize.jl") +end @testset "RSOCtoPSD" begin include("rsoc_to_psd.jl") end diff --git a/test/Bridges/Variable/vectorize.jl b/test/Bridges/Variable/vectorize.jl new file mode 100644 index 0000000000..d437095085 --- /dev/null +++ b/test/Bridges/Variable/vectorize.jl @@ -0,0 +1,170 @@ +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.Vectorize{Float64}(mock) + +@testset "get scalar constraint" begin + x, cx = MOI.add_constrained_variable(bridged_mock, MOI.GreaterThan(1.0)) + fx = MOI.SingleVariable(x) + func = 2.0 * fx + set = MOI.GreaterThan(5.0) + err = MOI.ScalarFunctionConstantNotZero{ + Float64, typeof(func), typeof(set)}(1.0) + @test_throws err MOI.add_constraint(bridged_mock, func + 1.0, set) + + c = MOI.add_constraint(bridged_mock, func, set) + @test MOI.get(bridged_mock, MOI.ConstraintFunction(), c) ≈ func + @test MOI.get(bridged_mock, MOI.ConstraintSet(), c) == set + MOI.set(bridged_mock, MOI.ConstraintName(), c, "c") + + @testset "Mock model" begin + MOI.set(mock, MOI.VariableName(), + MOI.get(mock, MOI.ListOfVariableIndices()), ["y"]) + MOI.set(mock, MOI.ConstraintName(), + MOI.get(mock, MOI.ListOfConstraintIndices{ + MOI.VectorOfVariables, MOI.Nonnegatives}()), + ["cy"]) + s = """ + variables: y + cy: [y] in MathOptInterface.Nonnegatives(1) + c: 2.0y >= 3.0 + """ + model = MOIU.Model{Float64}() + MOIU.loadfromstring!(model, s) + MOIU.test_models_equal(mock, model, ["y"], ["cy", "c"]) + end + @testset "Bridged model" begin + MOI.set(bridged_mock, MOI.VariableName(), x, "x") + MOI.set(bridged_mock, MOI.ConstraintName(), cx, "cx") + s = """ + variables: x + cx: x >= 1.0 + c: 2.0x >= 5.0 + """ + model = MOIU.Model{Float64}() + MOIU.loadfromstring!(model, s) + MOIU.test_models_equal(bridged_mock, model, ["x"], ["cx", "c"]) + end +end + +@testset "exp3 with add_constrained_variable for `y`" begin + mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [log(5), 0.0], + (MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}) => [0.0], + (MOI.VectorAffineFunction{Float64}, MOI.ExponentialCone) => [[-1.0, log(5)-1, 1/5]]) + + MOI.empty!(bridged_mock) + x = MOI.add_variable(bridged_mock) + @test MOI.get(bridged_mock, MOI.NumberOfVariables()) == 1 + fx = MOI.SingleVariable(x) + xc = MOI.add_constraint(bridged_mock, 2.0fx, MOI.LessThan(4.0)) + y, yc = MOI.add_constrained_variable(bridged_mock, MOI.LessThan(5.0)) + @test yc.value == y.value == -1 + @test MOI.get(bridged_mock, MOI.NumberOfVariables()) == 2 + @test length(MOI.get(bridged_mock, MOI.ListOfVariableIndices())) == 2 + @test Set(MOI.get(bridged_mock, MOI.ListOfVariableIndices())) == Set([x, y]) + fy = MOI.SingleVariable(y) + ec = MOI.add_constraint(bridged_mock, + MOIU.operate(vcat, Float64, fx, 1.0, fy), + MOI.ExponentialCone()) + + MOI.optimize!(bridged_mock) + @test MOI.get(bridged_mock, MOI.VariablePrimal(), x) ≈ log(5) + @test MOI.get(bridged_mock, MOI.VariablePrimal(), y) ≈ 5.0 + @test MOI.get(bridged_mock, MOI.ConstraintPrimal(), xc) ≈ 2log(5) + @test MOI.get(bridged_mock, MOI.ConstraintPrimal(), yc) ≈ 5 + @test MOI.get(bridged_mock, MOI.ConstraintPrimal(), ec) ≈ [log(5), 1., 5.0] + @test MOI.get(bridged_mock, MOI.ConstraintDual(), xc) ≈ 0.0 + @test MOI.get(bridged_mock, MOI.ConstraintDual(), yc) ≈ -1/5 + @test MOI.get(bridged_mock, MOI.ConstraintDual(), ec) ≈ [-1., log(5)-1, 1/5] + + err = ErrorException( + "Cannot add two `SingleVariable`-in-`MathOptInterface.LessThan{Float64}`" * + " on the same variable MathOptInterface.VariableIndex(-1)." + ) + @test_throws err MOI.add_constraint(bridged_mock, MOI.SingleVariable(y), MOI.LessThan(4.0)) + + cis = MOI.get(bridged_mock, MOI.ListOfConstraintIndices{ + MOI.VectorAffineFunction{Float64}, MOI.ExponentialCone}()) + @test length(cis) == 1 + + @testset "get `UnknownVariableAttribute``" begin + err = ArgumentError( + "Variable bridge of type `MathOptInterface.Bridges.Variable.VectorizeBridge{Float64,MathOptInterface.Nonpositives}`" * + " does not support accessing the attribute `MathOptInterface.Test.UnknownVariableAttribute()`." + ) + @test_throws err MOI.get(bridged_mock, MOIT.UnknownVariableAttribute(), y) + end + + @testset "set `ConstraintSet`" begin + ci = MOI.ConstraintIndex{MOI.SingleVariable, MOI.LessThan{Float64}}(y.value) + attr = MOI.ConstraintSet() + err = MOI.SetAttributeNotAllowed(attr, + "The variable `MathOptInterface.VariableIndex(12345676)` is bridged by the `VectorizeBridge`.") + @test_throws err MOI.set(bridged_mock, attr, ci, MOI.LessThan(4.0)) + end + + @testset "MultirowChange" begin + change = MOI.MultirowChange(y, [(3, 0.0)]) + message = "The change MathOptInterface.MultirowChange{Float64}(MathOptInterface.VariableIndex(-1), Tuple{Int64,Float64}[(3, 0.0)])" * + " contains variables bridged into a function with nonzero constant." + err = MOI.ModifyConstraintNotAllowed(cis[1], change, message) + @test_throws err MOI.modify(bridged_mock, cis[1], change) + end + + @testset "ScalarCoefficientChange" begin + change = MOI.ScalarCoefficientChange(y, 0.0) + attr = MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}() + message = "The change MathOptInterface.ScalarCoefficientChange{Float64}(MathOptInterface.VariableIndex(-1), 0.0)" * + " contains variables bridged into a function with nonzero constant." + err = MOI.ModifyObjectiveNotAllowed(change, message) + @test_throws err MOI.modify(bridged_mock, attr, change) + end + + MOI.set(bridged_mock, MOI.VariableName(), x, "x") + MOI.set(bridged_mock, MOI.ConstraintName(), xc, "xc") + MOI.set(bridged_mock, MOI.ConstraintName(), ec, "ec") + @testset "Mock model" begin + MOI.set(mock, MOI.VariableName(), + MOI.get(mock, MOI.ListOfVariableIndices())[2], "z") + MOI.set(mock, MOI.ConstraintName(), + MOI.get(mock, MOI.ListOfConstraintIndices{ + MOI.VectorOfVariables, MOI.Nonpositives}()), + ["zc"]) + s = """ + variables: x, z + zc: [z] in MathOptInterface.Nonpositives(1) + xc: 2.0x <= 4.0 + ec: [x, 1.0, z + 5.0] in MathOptInterface.ExponentialCone() + """ + model = MOIU.Model{Float64}() + MOIU.loadfromstring!(model, s) + MOIU.test_models_equal(mock, model, ["x", "z"], ["zc", "xc", "ec"]) + end + @testset "Bridged model" begin + MOI.set(bridged_mock, MOI.VariableName(), y, "y") + MOI.set(bridged_mock, MOI.ConstraintName(), yc, "yc") + s = """ + variables: x, y + yc: y <= 5.0 + xc: 2.0x <= 4.0 + ec: [x, 1.0, y] in MathOptInterface.ExponentialCone() + """ + model = MOIU.Model{Float64}() + MOIU.loadfromstring!(model, s) + MOIU.test_models_equal(bridged_mock, model, ["x", "y"], ["yc", "xc", "ec"]) + end + + test_delete_bridged_variable(bridged_mock, y, MOI.LessThan{Float64}, 2, ( + (MOI.VectorOfVariables, MOI.Nonpositives, 0), + )) +end