diff --git a/src/Bridges/Constraint/Constraint.jl b/src/Bridges/Constraint/Constraint.jl index 770a6f2ecf..26ab889631 100644 --- a/src/Bridges/Constraint/Constraint.jl +++ b/src/Bridges/Constraint/Constraint.jl @@ -62,6 +62,8 @@ const RSOCtoPSD{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{RSOCtoPSDBridge{T} include("indicator_activate_on_zero.jl") include("indicator_sos.jl") const IndicatortoSOS1{T, BC <: MOI.AbstractScalarSet, MaybeBC} = SingleBridgeOptimizer{IndicatorSOS1Bridge{T, BC, MaybeBC}} +include("semi_to_binary.jl") +const SemiToBinary{T, OT<:MOI.ModelLike} = SingleBridgeOptimizer{SemiToBinaryBridge{T}, OT} """ add_all_bridges(bridged_model, ::Type{T}) @@ -98,6 +100,7 @@ function add_all_bridges(bridged_model, ::Type{T}) where {T} MOIB.add_bridge(bridged_model, RSOCtoPSDBridge{T}) MOIB.add_bridge(bridged_model, IndicatorActiveOnFalseBridge{T}) MOIB.add_bridge(bridged_model, IndicatorSOS1Bridge{T}) + MOIB.add_bridge(bridged_model, SemiToBinaryBridge{T}) return end diff --git a/src/Bridges/Constraint/semi_to_binary.jl b/src/Bridges/Constraint/semi_to_binary.jl new file mode 100644 index 0000000000..cdd3e75423 --- /dev/null +++ b/src/Bridges/Constraint/semi_to_binary.jl @@ -0,0 +1,192 @@ +const SemiSets{T} = Union{MOI.Semicontinuous{T}, MOI.Semiinteger{T}} + +""" + SemiToBinaryBridge{T, S <: MOI.AbstractScalarSet} + +The `SemiToBinaryBridge` replaces an Semicontinuous constraint: +``x \\in \\mathsf{Semicontinuous}(l, u)`` +is replaced by: +``z \\in \\{0, 1\\}``, +``x \\leq z \\cdot u ``, +``x \\geq z \\cdot l ``. + +The `SemiToBinaryBridge` replaces an Semiinteger constraint: +``x \\in Semiinteger(l, u)`` +is replaced by: +``z \\in \\{0, 1\\}``, +``x \\in \\Integer``, +``x \\leq z \\cdot u ``, +``x \\geq z \\cdot l ``. +""" +mutable struct SemiToBinaryBridge{T, S <: SemiSets{T}} <: AbstractBridge + semi_set::S + variable_index::MOI.VariableIndex + binary_variable_index::MOI.VariableIndex + binary_constraint_index::MOI.ConstraintIndex{MOI.SingleVariable, MOI.ZeroOne} + lower_bound_index::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, MOI.GreaterThan{T}} + upper_bound_index::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, MOI.LessThan{T}} + integer_index::Union{Nothing, MOI.ConstraintIndex{MOI.SingleVariable, MOI.Integer}} +end + +function bridge_constraint(::Type{SemiToBinaryBridge{T,S}}, model::MOI.ModelLike, f::MOI.SingleVariable, s::S) where {T <: Real, S<:SemiSets{T}} + binary, binary_con = MOI.add_constrained_variable(model, MOI.ZeroOne()) + + # var - LB * bin >= 0 + lb = MOIU.operate(*, T, -s.lower, MOI.SingleVariable(binary)) + lb = MOIU.operate!(+, T, lb, f) + lb_ci = MOI.add_constraint(model, lb, MOI.GreaterThan{T}(zero(T))) + + # var - UB * bin <= 0 + ub = MOIU.operate(*, T, -s.upper, MOI.SingleVariable(binary)) + ub = MOIU.operate!(+, T, ub, f) + ub_ci = MOI.add_constraint(model, ub, MOI.LessThan{T}(zero(T))) + + if s isa MOI.Semiinteger{T} + int_ci = MOI.add_constraint(model, f, MOI.Integer()) + else + int_ci = nothing + end + + return SemiToBinaryBridge{T,S}(s, f.variable, binary, binary_con, lb_ci, ub_ci, int_ci) +end + + +function MOIB.added_constrained_variable_types(::Type{<:SemiToBinaryBridge{T, S}}) where {T, S} + return [(MOI.ZeroOne,)] +end + +function MOIB.added_constraint_types(::Type{<:SemiToBinaryBridge{T, S}}) where {T, S<:MOI.Semicontinuous{T}} + return [ + (MOI.ScalarAffineFunction{T}, MOI.LessThan{T}), + (MOI.ScalarAffineFunction{T}, MOI.GreaterThan{T}), + ] +end + +function MOIB.added_constraint_types(::Type{<:SemiToBinaryBridge{T, S}}) where {T, S <: MOI.Semiinteger{T}} + return [ + (MOI.ScalarAffineFunction{T}, MOI.LessThan{T}), + (MOI.ScalarAffineFunction{T}, MOI.GreaterThan{T}), + (MOI.SingleVariable, MOI.Integer), + ] +end + +function concrete_bridge_type(::Type{<:SemiToBinaryBridge{T}}, + ::Type{MOI.SingleVariable}, + ::Type{S}) where {T, S<:SemiSets} + return SemiToBinaryBridge{T, S} +end + +function MOI.supports_constraint(::Type{<:SemiToBinaryBridge}, + ::Type{MOI.SingleVariable}, + ::Type{<:SemiSets}) + return true +end + +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintSet, + b::SemiToBinaryBridge) + return b.semi_set +end + +function MOI.set(model::MOI.ModelLike, attr::MOI.ConstraintSet, + bridge::SemiToBinaryBridge{T, S}, set::S) where {T, S} + bridge.semi_set = set + MOI.modify(model, bridge.upper_bound_index, + MOI.ScalarCoefficientChange(bridge.binary_variable_index, -set.upper)) + MOI.modify(model, bridge.lower_bound_index, + MOI.ScalarCoefficientChange(bridge.binary_variable_index, -set.lower)) + return +end + +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintFunction, + b::SemiToBinaryBridge{T}) where {T} + return MOI.SingleVariable(b.variable_index) +end + +function MOI.delete(model::MOI.ModelLike, bridge::SemiToBinaryBridge) + if bridge.integer_index !== nothing + MOI.delete(model, bridge.integer_index) + end + MOI.delete(model, bridge.upper_bound_index) + MOI.delete(model, bridge.lower_bound_index) + MOI.delete(model, bridge.binary_constraint_index) + MOI.delete(model, bridge.binary_variable_index) + return +end + +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintPrimal, + bridge::SemiToBinaryBridge) + MOI.get(model, MOI.VariablePrimal(attr.N), bridge.variable_index) +end + +function MOI.supports( + ::MOI.ModelLike, + ::MOI.ConstraintPrimalStart, + ::Type{<:SemiToBinaryBridge}) + return true +end + +function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintPrimalStart, bridge::SemiToBinaryBridge) + return MOI.get(model, MOI.VariablePrimalStart(), bridge.variable_index) +end + +function MOI.set(model::MOI.ModelLike, attr::MOI.ConstraintPrimalStart, + bridge::SemiToBinaryBridge{T}, value) where {T} + MOI.set(model, MOI.VariablePrimalStart(), bridge.variable_index, value) + bin_value = ifelse(iszero(value), 0.0, 1.0) + MOI.set(model, MOI.VariablePrimalStart(), bridge.binary_variable_index, bin_value) + MOI.set(model, MOI.ConstraintPrimalStart(), + bridge.upper_bound_index, value - bridge.semi_set.upper * bin_value) + MOI.set(model, MOI.ConstraintPrimalStart(), + bridge.lower_bound_index, value - bridge.semi_set.lower * bin_value) + return +end + +# Attributes, Bridge acting as a model + +function MOI.get(::SemiToBinaryBridge, ::MOI.NumberOfVariables) + return 1 +end + +function MOI.get(b::SemiToBinaryBridge, ::MOI.ListOfVariableIndices) + return [b.binary_variable_index] +end + +function MOI.get(::SemiToBinaryBridge{T, S}, + ::MOI.NumberOfConstraints{MOI.SingleVariable, MOI.ZeroOne}) where {T, S} + return 1 +end + +function MOI.get(::SemiToBinaryBridge{T, S}, + ::MOI.NumberOfConstraints{MOI.SingleVariable, MOI.Integer}) where {T, S<:MOI.Semiinteger} + return 1 +end + +function MOI.get(::SemiToBinaryBridge{T, S}, + ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T}, MOI.GreaterThan{T}}) where {T, S} + return 1 +end + +function MOI.get(::SemiToBinaryBridge{T, S}, + ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T}, MOI.LessThan{T}}) where {T, S} + return 1 +end + +function MOI.get(b::SemiToBinaryBridge{T, S}, + ::MOI.ListOfConstraintIndices{MOI.SingleVariable, MOI.ZeroOne}) where {T, S} + return [b.binary_constraint_index] +end + +function MOI.get(b::SemiToBinaryBridge{T, S}, + ::MOI.ListOfConstraintIndices{MOI.SingleVariable, MOI.Integer}) where {T, S<:MOI.Semiinteger} + return [b.integer_index] +end + +function MOI.get(b::SemiToBinaryBridge{T, S}, + ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T}, MOI.LessThan{T}}) where {T, S} + return [b.upper_bound_index] +end + +function MOI.get(b::SemiToBinaryBridge{T, S}, + ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T}, MOI.GreaterThan{T}}) where {T, S} + return [b.lower_bound_index] +end \ No newline at end of file diff --git a/src/Utilities/functions.jl b/src/Utilities/functions.jl index 0bd4ac8dcc..b0fedc7018 100644 --- a/src/Utilities/functions.jl +++ b/src/Utilities/functions.jl @@ -489,7 +489,7 @@ function test_variablenames_equal(model, variablenames) end for (vname,seen) in seen_name if !seen - error("Did not find variable with name $vname in intance.") + error("Did not find variable with name $vname in instance.") end end end @@ -509,7 +509,7 @@ function test_constraintnames_equal(model, constraintnames) end for (cname,seen) in seen_name if !seen - error("Did not find constraint with name $cname in intance.") + error("Did not find constraint with name $cname in instance.") end end end diff --git a/test/Bridges/Constraint/semi_to_binary.jl b/test/Bridges/Constraint/semi_to_binary.jl new file mode 100644 index 0000000000..63a1fe7c41 --- /dev/null +++ b/test/Bridges/Constraint/semi_to_binary.jl @@ -0,0 +1,165 @@ +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() + +@testset "SemiToBinary" begin + bridged_mock = MOIBC.SemiToBinary{Float64}(mock) + + bridge_type = MOIBC.SemiToBinaryBridge{Float64, MOI.Semiinteger{Float64}} + @test MOI.supports_constraint(bridge_type, + MOI.SingleVariable, MOI.Semiinteger{Float64}) + @test MOIBC.concrete_bridge_type(bridge_type, + MOI.SingleVariable, + MOI.Semiinteger{Float64}) == bridge_type + + @test MOI.supports(bridged_mock, MOI.ConstraintPrimalStart(), bridge_type) + + MOIT.basic_constraint_tests( + bridged_mock, config, + include = [(F, S) + for F in [MOI.SingleVariable] + for S in [MOI.Semiinteger{Float64}, MOI.Semicontinuous{Float64}]]) + + MOIU.set_mock_optimize!(mock, + (mock::MOIU.MockOptimizer) -> begin + MOI.set(mock, MOI.ObjectiveBound(), 0.0) + MOIU.mock_optimize!(mock, [0.0, 0.0, 0.0]) + end, + (mock::MOIU.MockOptimizer) -> begin + MOI.set(mock, MOI.ObjectiveBound(), 2.0) + MOIU.mock_optimize!(mock, [2.0, 1.0, 1.0]) + end, + (mock::MOIU.MockOptimizer) -> begin + MOI.set(mock, MOI.ObjectiveBound(), 2.0) + MOIU.mock_optimize!(mock, [2.0, 2.0, 1.0]) + end, + (mock::MOIU.MockOptimizer) -> begin + MOI.set(mock, MOI.ObjectiveBound(), 2.5) + MOIU.mock_optimize!(mock, [2.5, 2.5, 1.0]) + end, + (mock::MOIU.MockOptimizer) -> begin + MOI.set(mock, MOI.ObjectiveBound(), 3.0) + MOIU.mock_optimize!(mock, [3.0, 3.0, 1.0]) + end, + (mock::MOIU.MockOptimizer) -> MOI.set(mock, MOI.TerminationStatus(), MOI.INFEASIBLE) + ) + MOIT.semiconttest(bridged_mock,config) + + ci = first(MOI.get( + bridged_mock, + MOI.ListOfConstraintIndices{MOI.SingleVariable, + MOI.Semicontinuous{Float64}}())) + + test_delete_bridge(bridged_mock, ci, 2, ( + (MOI.SingleVariable, MOI.EqualTo{Float64}, 1), + (MOI.SingleVariable, MOI.ZeroOne, 0), + (MOI.SingleVariable, MOI.Integer, 0), + (MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}, 0), + (MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}, 1), + )) + + MOIU.set_mock_optimize!(mock, + (mock::MOIU.MockOptimizer) -> begin + MOI.set(mock, MOI.ObjectiveBound(), 0.0) + MOIU.mock_optimize!(mock, [0.0, 0.0, 0.0]) + end, + (mock::MOIU.MockOptimizer) -> begin + MOI.set(mock, MOI.ObjectiveBound(), 2.0) + MOIU.mock_optimize!(mock, [2.0, 1.0, 1.0]) + end, + (mock::MOIU.MockOptimizer) -> begin + MOI.set(mock, MOI.ObjectiveBound(), 2.0) + MOIU.mock_optimize!(mock, [2.0, 2.0, 1.0]) + end, + (mock::MOIU.MockOptimizer) -> begin + MOI.set(mock, MOI.ObjectiveBound(), 3.0) + MOIU.mock_optimize!(mock, [3.0, 2.5, 1.0]) + end, + (mock::MOIU.MockOptimizer) -> begin + MOI.set(mock, MOI.ObjectiveBound(), 3.0) + MOIU.mock_optimize!(mock, [3.0, 3.0, 1.0]) + end, + (mock::MOIU.MockOptimizer) -> MOI.set(mock, MOI.TerminationStatus(), MOI.INFEASIBLE) + ) + MOIT.semiinttest(bridged_mock,config) + + ci = first(MOI.get( + bridged_mock, + MOI.ListOfConstraintIndices{MOI.SingleVariable, + MOI.Semiinteger{Float64}}())) + + @test MOI.get(bridged_mock, MOI.ConstraintPrimal(), ci) == 3 + new_set = MOI.Semiinteger{Float64}(19.0, 20.0) + MOI.set(bridged_mock, MOI.ConstraintSet(), ci, new_set) + @test MOI.get(bridged_mock, MOI.ConstraintSet(), ci) == new_set + + @testset "$attr" for attr in [MOI.ConstraintPrimalStart(),] + @test MOI.supports(bridged_mock, attr, typeof(ci)) + value = 2.0 + MOI.set(bridged_mock, attr, ci, value) + @test MOI.get(bridged_mock, attr, ci) ≈ value + end + + test_delete_bridge(bridged_mock, ci, 2, ( + (MOI.SingleVariable, MOI.EqualTo{Float64}, 1), + (MOI.SingleVariable, MOI.ZeroOne, 0), + (MOI.SingleVariable, MOI.Integer, 0), + (MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}, 0), + (MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}, 1), + )) + + + s = """ + variables: x, y + cy: y == 4.0 + cx: x in Semiinteger(2.0, 3.0) + minobjective: x + """ + model = MOIU.Model{Float64}() + MOIU.loadfromstring!(model, s) + sb = """ + variables: x, y, z + cy: y == 4.0 + bin: z in ZeroOne() + int: x in Integer() + clo: x + -2.0z >= 0.0 + cup: x + -3.0z <= 0.0 + minobjective: x + """ + modelb = MOIU.Model{Float64}() + MOIU.loadfromstring!(modelb, sb) + + MOI.empty!(bridged_mock) + @test MOI.is_empty(bridged_mock) + MOIU.loadfromstring!(bridged_mock, s) + MOIU.test_models_equal(bridged_mock, model, ["x", "y"], ["cy", "cx"]) + + # setting names on mock + ci = first(MOI.get(mock,MOI.ListOfConstraintIndices{ + MOI.SingleVariable, MOI.ZeroOne}())) + MOI.set(mock, MOI.ConstraintName(), ci, "bin") + z = MOI.VariableIndex(ci.value) + MOI.set(mock, MOI.VariableName(), z, "z") + ci = first(MOI.get(mock, MOI.ListOfConstraintIndices{ + MOI.SingleVariable, MOI.Integer}())) + MOI.set(mock, MOI.ConstraintName(), ci, "int") + ci = first(MOI.get(mock,MOI.ListOfConstraintIndices{ + MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}}())) + MOI.set(mock, MOI.ConstraintName(), ci, "clo") + ci = first(MOI.get(mock,MOI.ListOfConstraintIndices{ + MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}}())) + MOI.set(mock, MOI.ConstraintName(), ci, "cup") + + MOIU.test_models_equal(mock, modelb, ["x", "y", "z"], ["cy", "bin", "int", "clo", "cup"]) + +end