diff --git a/src/Bridges/Constraint/indicator_sos.jl b/src/Bridges/Constraint/indicator_sos.jl index 5c1ef6b8a3..0a067a7e00 100644 --- a/src/Bridges/Constraint/indicator_sos.jl +++ b/src/Bridges/Constraint/indicator_sos.jl @@ -1,68 +1,35 @@ """ - IndicatorSOS1Bridge{T, BC <: MOI.AbstractScalarSet} + IndicatorSOS1Bridge{T,S<:MOI.AbstractScalarSet} -The `IndicatorSOS1Bridge` replaces an indicator constraint of the following form: -``z \\in \\mathbb{B}, z == 1 \\implies f(x) \\leq b`` with a SOS1 constraint: -``z \\in \\mathbb{B}, w \\leq 0, f(x) + w \\leq b, SOS1(w, z)``. -`GreaterThan` constraints are handled in a symmetric way: -``z \\in \\mathbb{B}, z == 1 \\implies f(x) \\geq b`` is reformulated as: -``z \\in \\mathbb{B}, w \\geq 0, f(x) + w \\geq b, SOS1(w, z)``. -Other scalar sets are handled without a bound constraint: -``z \\in \\mathbb{B}, z == 1 \\implies f(x) == b`` is reformulated as: -``z \\in \\mathbb{B}, w \\text{ free}, f(x) + w == b, SOS1(w, z)``. - -If `BC !<: Union{LessThan, GreaterThan}`, `bound_constraint_index` is `nothing`. +The `IndicatorSOS1Bridge` replaces an indicator constraint of the following +form: +``z \\in \\mathbb{B}, z == 1 \\implies f(x) \\in S`` with a SOS1 constraint: +``z \\in \\mathbb{B}, slack \\text{ free}, f(x) + slack \\in S, SOS1(slack, z)``. """ -struct IndicatorSOS1Bridge{ - T, - BC<:MOI.AbstractScalarSet, - MaybeBC<:Union{MOI.ConstraintIndex{MOI.VariableIndex,BC},Nothing}, -} <: AbstractBridge - w_variable::MOI.VariableIndex - z_variable::MOI.VariableIndex - affine_func::MOI.ScalarAffineFunction{T} - bound_constraint_index::MaybeBC - sos_constraint_index::MOI.ConstraintIndex{MOI.VectorOfVariables,MOI.SOS1{T}} - linear_constraint_index::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},BC} +struct IndicatorSOS1Bridge{T,S<:MOI.AbstractScalarSet} <: AbstractBridge + slack::MOI.VariableIndex + z::MOI.VariableIndex + sos_index::MOI.ConstraintIndex{MOI.VectorOfVariables,MOI.SOS1{T}} + affine_index::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S} end function bridge_constraint( - ::Type{IndicatorSOS1Bridge{T,BC,MaybeBC}}, + ::Type{IndicatorSOS1Bridge{T,S}}, model::MOI.ModelLike, f::MOI.VectorAffineFunction{T}, - s::MOI.Indicator{MOI.ACTIVATE_ON_ONE,BC}, -) where {T<:Real,BC,MaybeBC} - f_scalars = MOIU.eachscalar(f) - (w, bound_constraint) = _add_bound_constraint!(model, BC) + s::MOI.Indicator{MOI.ACTIVATE_ON_ONE,S}, +) where {T<:Real,S} + f_scalars = MOI.Utilities.eachscalar(f) z = convert(MOI.VariableIndex, f_scalars[1]) - sos_vector = MOI.VectorOfVariables([w, z]) - sos_constraint = - MOI.add_constraint(model, sos_vector, MOI.SOS1{T}([0.4, 0.6])) - affine_func = f_scalars[2] - affine_expr = MOIU.operate(+, T, affine_func, w) - linear_constraint = MOI.add_constraint(model, affine_expr, s.set) - return IndicatorSOS1Bridge{T,BC,MaybeBC}( - w, - z, - affine_func, - bound_constraint, - sos_constraint, - linear_constraint, + slack = MOI.add_variable(model) + sos_index = MOI.add_constraint( + model, + MOI.VectorOfVariables([slack, z]), + MOI.SOS1{T}([0.4, 0.6]), # This weight vector is arbitrary! ) -end - -function _add_bound_constraint!( - model::MOI.ModelLike, - ::Type{BC}, -) where {T<:Real,BC<:Union{MOI.LessThan{T},MOI.GreaterThan{T}}} - return MOI.add_constrained_variable(model, BC(zero(T))) -end - -function _add_bound_constraint!( - model::MOI.ModelLike, - ::Type{<:MOI.AbstractScalarSet}, -) - return (MOI.add_variable(model), nothing) + new_f = MOI.Utilities.operate(+, T, f_scalars[2], slack) + affine_index = MOI.add_constraint(model, new_f, s.set) + return IndicatorSOS1Bridge{T,S}(slack, z, sos_index, affine_index) end function MOI.supports_constraint( @@ -78,58 +45,39 @@ function MOI.get( attr::MOI.ConstraintSet, b::IndicatorSOS1Bridge, ) - return MOI.Indicator{MOI.ACTIVATE_ON_ONE}( - MOI.get(model, attr, b.linear_constraint_index), - ) + set = MOI.get(model, attr, b.affine_index) + return MOI.Indicator{MOI.ACTIVATE_ON_ONE}(set) end function MOI.get( - ::MOI.ModelLike, - ::MOI.ConstraintFunction, + model::MOI.ModelLike, + attr::MOI.ConstraintFunction, b::IndicatorSOS1Bridge{T}, ) where {T} - z = b.z_variable - terms = [MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(one(T), z))] - for affine_term in b.affine_func.terms - push!(terms, MOI.VectorAffineTerm(2, affine_term)) - end - return MOI.VectorAffineFunction(terms, [zero(T), b.affine_func.constant]) + f = MOI.get(model, attr, b.affine_index) + terms = MOI.VectorAffineTerm{T}[ + MOI.VectorAffineTerm(2, t) for t in f.terms if t.variable != b.slack + ] + push!(terms, MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(one(T), b.z))) + return MOI.VectorAffineFunction(terms, [zero(T), f.constant]) end function MOI.delete(model::MOI.ModelLike, bridge::IndicatorSOS1Bridge) - if bridge.bound_constraint_index !== nothing - MOI.delete(model, bridge.bound_constraint_index) - end - MOI.delete(model, bridge.sos_constraint_index) - MOI.delete(model, bridge.linear_constraint_index) - MOI.delete(model, bridge.w_variable) + MOI.delete(model, bridge.sos_index) + MOI.delete(model, bridge.affine_index) + MOI.delete(model, bridge.slack) return end -function MOIB.added_constrained_variable_types( - ::Type{<:IndicatorSOS1Bridge{T,BC}}, -) where {T,BC<:Union{MOI.LessThan{T},MOI.GreaterThan{T}}} - return Tuple{Type}[(BC,)] -end - -function MOIB.added_constrained_variable_types( - ::Type{<:IndicatorSOS1Bridge{T,BC}}, -) where {T,BC} +function MOI.Bridges.added_constrained_variable_types( + ::Type{<:IndicatorSOS1Bridge}, +) return Tuple{Type}[] end -function MOIB.added_constraint_types( - ::Type{<:IndicatorSOS1Bridge{T,BC}}, -) where {T,BC<:Union{MOI.LessThan{T},MOI.GreaterThan{T}}} - return Tuple{Type,Type}[ - (MOI.VectorOfVariables, MOI.SOS1{T}), - (MOI.ScalarAffineFunction{T}, BC), - ] -end - -function MOIB.added_constraint_types( +function MOI.Bridges.added_constraint_types( ::Type{<:IndicatorSOS1Bridge{T,S}}, -) where {T,S<:MOI.AbstractScalarSet} +) where {T,S} return Tuple{Type,Type}[ (MOI.VectorOfVariables, MOI.SOS1{T}), (MOI.ScalarAffineFunction{T}, S), @@ -140,38 +88,14 @@ function concrete_bridge_type( ::Type{<:IndicatorSOS1Bridge{T}}, ::Type{<:MOI.AbstractVectorFunction}, ::Type{MOI.Indicator{MOI.ACTIVATE_ON_ONE,S}}, -) where {T,S<:Union{MOI.LessThan,MOI.GreaterThan}} - return IndicatorSOS1Bridge{T,S,MOI.ConstraintIndex{MOI.VariableIndex,S}} -end - -function concrete_bridge_type( - ::Type{<:IndicatorSOS1Bridge{T}}, - ::Type{<:MOI.AbstractVectorFunction}, - ::Type{MOI.Indicator{MOI.ACTIVATE_ON_ONE,S}}, -) where {T,S<:MOI.AbstractScalarSet} - return IndicatorSOS1Bridge{T,S,Nothing} +) where {T,S} + return IndicatorSOS1Bridge{T,S} end -# Attributes, Bridge acting as a model - MOI.get(::IndicatorSOS1Bridge, ::MOI.NumberOfVariables)::Int64 = 1 function MOI.get(b::IndicatorSOS1Bridge, ::MOI.ListOfVariableIndices) - return [b.w_variable] -end - -function MOI.get( - ::IndicatorSOS1Bridge{T,BC,Nothing}, - ::MOI.NumberOfConstraints{MOI.VariableIndex,BC}, -)::Int64 where {T,BC} - return 0 -end - -function MOI.get( - ::IndicatorSOS1Bridge{T,BC,CI}, - ::MOI.NumberOfConstraints{MOI.VariableIndex,BC}, -)::Int64 where {T,BC,CI<:MOI.ConstraintIndex{MOI.VariableIndex,BC}} - return 1 + return [b.slack] end function MOI.get( @@ -182,46 +106,24 @@ function MOI.get( end function MOI.get( - ::IndicatorSOS1Bridge{T,BC}, - ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},BC}, -)::Int64 where {T,BC,CI<:MOI.ConstraintIndex{MOI.VariableIndex,BC}} + ::IndicatorSOS1Bridge{T,S}, + ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},S}, +)::Int64 where {T,S} return 1 end -function MOI.get( - b::IndicatorSOS1Bridge{T,BC,CI}, - ::MOI.ListOfConstraintIndices{MOI.VariableIndex,BC}, -) where {T,BC,CI<:MOI.ConstraintIndex} - return [b.bound_constraint_index] -end - -function MOI.get( - ::IndicatorSOS1Bridge{T,BC,Nothing}, - ::MOI.ListOfConstraintIndices{MOI.VariableIndex,BC}, -) where {T,BC} - return MOI.ConstraintIndex{MOI.VariableIndex,BC}[] -end - function MOI.get( b::IndicatorSOS1Bridge{T}, ::MOI.ListOfConstraintIndices{MOI.VectorOfVariables,<:MOI.SOS1}, ) where {T} - return [b.sos_constraint_index] + return [b.sos_index] end function MOI.get( - b::IndicatorSOS1Bridge{T,BC}, - ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},BC}, -) where {T,BC} - return [b.linear_constraint_index] -end - -function MOI.supports( - ::MOI.ModelLike, - ::MOI.ConstraintPrimalStart, - ::Type{<:IndicatorSOS1Bridge}, -) - return true + b::IndicatorSOS1Bridge{T,S}, + ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},S}, +) where {T,S} + return [b.affine_index] end function MOI.get( @@ -229,12 +131,20 @@ function MOI.get( attr::MOI.ConstraintPrimal, bridge::IndicatorSOS1Bridge, ) - zvalue = - MOI.get(model, MOI.VariablePrimal(attr.result_index), bridge.z_variable) - wvalue = - MOI.get(model, MOI.VariablePrimal(attr.result_index), bridge.w_variable) - lin_primal_start = MOI.get(model, attr, bridge.linear_constraint_index) - return [zvalue, lin_primal_start - wvalue] + z = MOI.get(model, MOI.VariablePrimal(attr.result_index), bridge.z) + w = MOI.get(model, MOI.VariablePrimal(attr.result_index), bridge.slack) + f = MOI.get(model, attr, bridge.affine_index) + return [z, f - w] +end + +function MOI.supports( + model::MOI.ModelLike, + attr::MOI.ConstraintPrimalStart, + ::Type{IndicatorSOS1Bridge{T,S}}, +) where {T,S} + ci = MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},S} + return MOI.supports(model, MOI.VariablePrimalStart(), MOI.VariableIndex) && + MOI.supports(model, attr, ci) end function MOI.get( @@ -242,23 +152,24 @@ function MOI.get( attr::MOI.ConstraintPrimalStart, bridge::IndicatorSOS1Bridge, ) - zstart = MOI.get(model, MOI.VariablePrimalStart(), bridge.z_variable) - wstart = MOI.get(model, MOI.VariablePrimalStart(), bridge.w_variable) - lin_primal_start = MOI.get(model, attr, bridge.linear_constraint_index) - return [zstart, lin_primal_start - wstart] + z = MOI.get(model, MOI.VariablePrimalStart(), bridge.z) + w = MOI.get(model, MOI.VariablePrimalStart(), bridge.slack) + f = MOI.get(model, attr, bridge.affine_index) + return [z, f - w] end function MOI.set( model::MOI.ModelLike, attr::MOI.ConstraintPrimalStart, bridge::IndicatorSOS1Bridge{T}, - value, + value::AbstractVector, ) where {T} - zvalue = value[1] - lin_start = value[2] - MOI.set(model, MOI.VariablePrimalStart(), bridge.z_variable, zvalue) - wstart = MOI.get(model, MOI.VariablePrimalStart(), bridge.w_variable) - wstart = wstart === nothing ? zero(T) : wstart - MOI.set(model, attr, bridge.linear_constraint_index, lin_start + wstart) + @assert length(value) == 2 + MOI.set(model, MOI.VariablePrimalStart(), bridge.z, value[1]) + w = something( + MOI.get(model, MOI.VariablePrimalStart(), bridge.slack), + zero(T), + ) + MOI.set(model, attr, bridge.affine_index, value[2] + w) return end diff --git a/test/Bridges/Constraint/indicator_sos.jl b/test/Bridges/Constraint/indicator_sos.jl index ad5bd0960a..d9b2fe308f 100644 --- a/test/Bridges/Constraint/indicator_sos.jl +++ b/test/Bridges/Constraint/indicator_sos.jl @@ -70,11 +70,8 @@ function test_indicator_by_SOS1() typeof(iset1), ) bridge1 = MOI.Bridges.Constraint.bridge_constraint(BT1, model, f1, iset1) - @test BT1 <: MOI.Bridges.Constraint.IndicatorSOS1Bridge{ - Float64, - <:MOI.LessThan, - <:MOI.ConstraintIndex, - } + @test BT1 <: + MOI.Bridges.Constraint.IndicatorSOS1Bridge{Float64,<:MOI.LessThan} @test bridge1 isa BT1 BT2 = MOI.Bridges.Constraint.concrete_bridge_type( @@ -83,11 +80,8 @@ function test_indicator_by_SOS1() typeof(iset2), ) bridge2 = MOI.Bridges.Constraint.bridge_constraint(BT2, model, f2, iset2) - @test BT2 <: MOI.Bridges.Constraint.IndicatorSOS1Bridge{ - Float64, - <:MOI.EqualTo, - Nothing, - } + @test BT2 <: + MOI.Bridges.Constraint.IndicatorSOS1Bridge{Float64,<:MOI.EqualTo} @test bridge2 isa BT2 BT3 = MOI.Bridges.Constraint.concrete_bridge_type( @@ -96,27 +90,14 @@ function test_indicator_by_SOS1() typeof(iset3), ) bridge3 = MOI.Bridges.Constraint.bridge_constraint(BT3, model, f3, iset3) - @test BT3 <: MOI.Bridges.Constraint.IndicatorSOS1Bridge{ - Float64, - <:MOI.GreaterThan, - <:MOI.ConstraintIndex, - } + @test BT3 <: + MOI.Bridges.Constraint.IndicatorSOS1Bridge{Float64,<:MOI.GreaterThan} @test bridge3 isa BT3 - w1 = bridge1.w_variable - @test MOI.get( - model, - MOI.ConstraintFunction(), - bridge1.bound_constraint_index, - ) == w1 - @test MOI.get(model, MOI.ConstraintSet(), bridge1.bound_constraint_index) == - MOI.LessThan(0.0) - @test MOI.get( - model, - MOI.ConstraintFunction(), - bridge1.sos_constraint_index, - ) == MOI.VectorOfVariables([w1, z1]) - lin_cons1 = bridge1.linear_constraint_index + w1 = bridge1.slack + @test MOI.get(model, MOI.ConstraintFunction(), bridge1.sos_index) == + MOI.VectorOfVariables([w1, z1]) + lin_cons1 = bridge1.affine_index lin_func1 = MOI.get(model, MOI.ConstraintFunction(), lin_cons1) @test lin_func1 ≈ MOI.ScalarAffineFunction( [MOI.ScalarAffineTerm(1.0, x2), MOI.ScalarAffineTerm(1.0, w1)], @@ -124,14 +105,10 @@ function test_indicator_by_SOS1() ) @test MOI.get(model, MOI.ConstraintSet(), lin_cons1) == MOI.LessThan(8.0) - w2 = bridge2.w_variable - @test bridge2.bound_constraint_index === nothing - @test MOI.get( - model, - MOI.ConstraintFunction(), - bridge2.sos_constraint_index, - ) == MOI.VectorOfVariables([w2, z2]) - lin_cons2 = bridge2.linear_constraint_index + w2 = bridge2.slack + @test MOI.get(model, MOI.ConstraintFunction(), bridge2.sos_index) == + MOI.VectorOfVariables([w2, z2]) + lin_cons2 = bridge2.affine_index lin_func2 = MOI.get(model, MOI.ConstraintFunction(), lin_cons2) @test lin_func2 ≈ MOI.ScalarAffineFunction( [ @@ -143,20 +120,11 @@ function test_indicator_by_SOS1() ) @test MOI.get(model, MOI.ConstraintSet(), lin_cons2) == MOI.EqualTo(9.0) - w3 = bridge3.w_variable - @test MOI.get( - model, - MOI.ConstraintFunction(), - bridge3.bound_constraint_index, - ) == w3 - @test MOI.get(model, MOI.ConstraintSet(), bridge3.bound_constraint_index) == - MOI.GreaterThan(0.0) - @test MOI.get( - model, - MOI.ConstraintFunction(), - bridge3.sos_constraint_index, - ) == MOI.VectorOfVariables([w3, z3]) - lin_cons3 = bridge3.linear_constraint_index + w3 = bridge3.slack + MOI.GreaterThan(0.0) + @test MOI.get(model, MOI.ConstraintFunction(), bridge3.sos_index) == + MOI.VectorOfVariables([w3, z3]) + lin_cons3 = bridge3.affine_index lin_func3 = MOI.get(model, MOI.ConstraintFunction(), lin_cons3) @test lin_func3 ≈ MOI.ScalarAffineFunction( [MOI.ScalarAffineTerm(1.0, x1), MOI.ScalarAffineTerm(1.0, w3)], @@ -217,12 +185,6 @@ function test_model_equality() ) @test length(sos_cons_list) == 1 MOI.set(mock, MOI.ConstraintName(), sos_cons_list[1], "sos1") - single_less_cons = MOI.get( - mock, - MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.LessThan{Float64}}(), - ) - @test length(single_less_cons) == 1 - inequality_list = MOI.get( mock, MOI.ListOfConstraintIndices{ @@ -236,11 +198,10 @@ function test_model_equality() MOI.set(mock, MOI.ObjectiveSense(), MOI.MAX_SENSE) s = """ variables: z, x, w + maxobjective: z sos1: [w, z] in MathOptInterface.SOS1([0.4, 0.6]) - w <= 0.0 ineq: x + w <= 8.0 z in MathOptInterface.ZeroOne() - maxobjective: z """ model = MOI.Utilities.Model{Float64}() MOI.Utilities.loadfromstring!(model, s) @@ -249,16 +210,14 @@ function test_model_equality() model, var_names, ["sos1", "ineq"], - [("w", MOI.LessThan{Float64}(0.0)), ("z", MOI.ZeroOne())], + [("z", MOI.ZeroOne())], ) - _test_delete_bridge( bridged_mock, ci, 2, ( (MOI.VariableIndex, MOI.ZeroOne, 1), - (MOI.VariableIndex, MathOptInterface.LessThan{Float64}, 0), (MOI.VectorOfVariables, MathOptInterface.SOS1{Float64}, 0), (MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}, 0), ), @@ -304,19 +263,15 @@ function test_getting_primal_attributes() ) bridge1 = MOI.Bridges.Constraint.bridge_constraint(BT, mock, f, iset) # w value should be defaulted to 0 - MOI.set(mock, MOI.VariablePrimalStart(), bridge1.w_variable, 0.0) + MOI.set(mock, MOI.VariablePrimalStart(), bridge1.slack, 0.0) affine_value = 6.0 MOI.set(mock, MOI.ConstraintPrimalStart(), bridge1, [1.0, affine_value]) @test MOI.get(mock, MOI.VariablePrimalStart(), z) ≈ 1.0 - @test MOI.get( - mock, - MOI.ConstraintPrimalStart(), - bridge1.linear_constraint_index, - ) ≈ 6.0 + @test MOI.get(mock, MOI.ConstraintPrimalStart(), bridge1.affine_index) ≈ 6.0 # after setting the w value w_value = 3.0 - MOI.set(mock, MOI.VariablePrimalStart(), bridge1.w_variable, w_value) + MOI.set(mock, MOI.VariablePrimalStart(), bridge1.slack, w_value) # linear function should not move @test all( MOI.get(mock, MOI.ConstraintPrimalStart(), bridge1) .≈ @@ -329,10 +284,6 @@ function test_getting_primal_attributes() typeof(iseteq), ) bridge_eq = MOI.Bridges.Constraint.bridge_constraint(BT, mock, f, iseteq) - @test MOI.get( - bridge_eq, - MOI.NumberOfConstraints{MOI.VariableIndex,MOI.EqualTo{Float64}}(), - ) == 0 @test MOI.get( bridge_eq, MOI.NumberOfConstraints{ @@ -340,30 +291,18 @@ function test_getting_primal_attributes() MOI.EqualTo{Float64}, }(), ) == 1 - @test isempty( - MOI.get( - bridge_eq, - MOI.ListOfConstraintIndices{ - MOI.VariableIndex, - MOI.EqualTo{Float64}, - }(), - ), - ) @test MOI.supports( mock, MOI.ConstraintPrimalStart(), - MOI.Bridges.Constraint.IndicatorSOS1Bridge{Float64}, - ) - @test MOI.supports( - mock, - MOI.ConstraintPrimalStart(), - MOI.Bridges.Constraint.IndicatorSOS1Bridge, + MOI.Bridges.Constraint.IndicatorSOS1Bridge{ + Float64, + MOI.LessThan{Float64}, + }, ) - # VariablePrimal - MOI.set(mock, MOI.VariablePrimal(), bridge1.w_variable, 33.0) + MOI.set(mock, MOI.VariablePrimal(), bridge1.slack, 33.0) z_value = 1.0 - MOI.set(mock, MOI.VariablePrimal(), bridge1.z_variable, z_value) + MOI.set(mock, MOI.VariablePrimal(), bridge1.z, z_value) MOI.set(mock, MOI.VariablePrimal(), x, affine_value) # linear function should not move