diff --git a/docs/src/submodules/Bridges/list_of_bridges.md b/docs/src/submodules/Bridges/list_of_bridges.md index 7fe2ce2c47..b07afb8ad4 100644 --- a/docs/src/submodules/Bridges/list_of_bridges.md +++ b/docs/src/submodules/Bridges/list_of_bridges.md @@ -64,6 +64,7 @@ Bridges.Constraint.IndicatorLessToGreaterThanBridge Bridges.Constraint.IndicatorSOS1Bridge Bridges.Constraint.SemiToBinaryBridge Bridges.Constraint.ZeroOneBridge +Bridges.Constraint.IntegerToZeroOneBridge Bridges.Constraint.NumberConversionBridge Bridges.Constraint.AllDifferentToCountDistinctBridge Bridges.Constraint.ReifiedAllDifferentToCountDistinctBridge diff --git a/src/Bridges/Constraint/Constraint.jl b/src/Bridges/Constraint/Constraint.jl index cbfc0fa7ad..0be042680f 100644 --- a/src/Bridges/Constraint/Constraint.jl +++ b/src/Bridges/Constraint/Constraint.jl @@ -35,6 +35,7 @@ include("bridges/geomean.jl") include("bridges/indicator_activate_on_zero.jl") include("bridges/indicator_flipsign.jl") include("bridges/indicator_sos.jl") +include("bridges/integer_to_zeroone.jl") include("bridges/interval.jl") include("bridges/ltgt_to_interval.jl") include("bridges/norm_infinity.jl") @@ -128,6 +129,7 @@ function add_all_bridges(bridged_model, ::Type{T}) where {T} MOI.Bridges.add_bridge(bridged_model, IndicatorGreaterToLessThanBridge{T}) MOI.Bridges.add_bridge(bridged_model, SemiToBinaryBridge{T}) MOI.Bridges.add_bridge(bridged_model, ZeroOneBridge{T}) + MOI.Bridges.add_bridge(bridged_model, IntegerToZeroOneBridge{T}) # Do not add by default # MOI.Bridges.add_bridge(bridged_model, NumberConversionBridge{T}) # Constraint programming bridges diff --git a/src/Bridges/Constraint/bridges/integer_to_zeroone.jl b/src/Bridges/Constraint/bridges/integer_to_zeroone.jl new file mode 100644 index 0000000000..4f31530e13 --- /dev/null +++ b/src/Bridges/Constraint/bridges/integer_to_zeroone.jl @@ -0,0 +1,175 @@ +# Copyright (c) 2017: Miles Lubin and contributors +# Copyright (c) 2017: Google Inc. +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +""" + IntegerToZeroOneBridge{T} <: Bridges.Constraint.AbstractBridge + +`IntegerToZeroOneBridge` implements the following reformulation: + + * ``x \\in \\mathbf{Z}`` into ``y_i \\in \\{0, 1\\}``, + ``x == lb + \\sum 2^{i-1} y_i``. + +## Source node + +`IntegerToZeroOneBridge` supports: + + * `VariableIndex` in [`MOI.Integer`](@ref) + +## Target nodes + +`IntegerToZeroOneBridge` creates: + + * [`MOI.VariableIndex`](@ref) in [`MOI.ZeroOne`](@ref) + * [`MOI.ScalarAffineFunction{T}`](@ref) in [`MOI.EqualTo{T}`](@ref) + +## Developer note + +This bridge is implemented as a constraint bridge instead of a variable bridge +because we don't want to substitute the linear combination of `y` for every +instance of `x`. Doing so would be expensive and greatly reduce the sparsity of +the constraints. +""" +mutable struct IntegerToZeroOneBridge{T} <: AbstractBridge + x::MOI.VariableIndex + y::Vector{MOI.VariableIndex} + ci::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}} + last_bounds::Union{Nothing,NTuple{2,T}} + + function IntegerToZeroOneBridge{T}(x::MOI.VariableIndex) where {T} + return new{T}( + x, + MOI.VariableIndex[], + MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}(0), + nothing, + ) + end +end + +const IntegerToZeroOne{T,OT<:MOI.ModelLike} = + SingleBridgeOptimizer{IntegerToZeroOneBridge{T},OT} + +function bridge_constraint( + ::Type{IntegerToZeroOneBridge{T}}, + ::MOI.ModelLike, + x::MOI.VariableIndex, + ::MOI.Integer, +) where {T} + # !!! info + # Postpone creation until final_touch. + return IntegerToZeroOneBridge{T}(x) +end + +function MOI.supports_constraint( + ::Type{IntegerToZeroOneBridge{T}}, + ::Type{MOI.VariableIndex}, + ::Type{MOI.Integer}, +) where {T} + return true +end + +function MOI.Bridges.added_constrained_variable_types( + ::Type{<:IntegerToZeroOneBridge}, +) + return Tuple{Type}[(MOI.ZeroOne,)] +end + +function MOI.Bridges.added_constraint_types( + ::Type{IntegerToZeroOneBridge{T}}, +) where {T} + return Tuple{Type,Type}[(MOI.ScalarAffineFunction{T}, MOI.EqualTo{T})] +end + +function concrete_bridge_type( + ::Type{IntegerToZeroOneBridge{T}}, + ::Type{MOI.VariableIndex}, + ::Type{MOI.Integer}, +) where {T} + return IntegerToZeroOneBridge{T} +end + +function MOI.get( + ::MOI.ModelLike, + ::MOI.ConstraintFunction, + bridge::IntegerToZeroOneBridge, +) + return bridge.x +end + +function MOI.get(::MOI.ModelLike, ::MOI.ConstraintSet, ::IntegerToZeroOneBridge) + return MOI.Integer() +end + +function MOI.delete(model::MOI.ModelLike, bridge::IntegerToZeroOneBridge) + MOI.delete(model, bridge.ci) + MOI.delete(model, bridge.y) + return +end + +function MOI.get(bridge::IntegerToZeroOneBridge, ::MOI.NumberOfVariables)::Int64 + return length(bridge.y) +end + +function MOI.get(bridge::IntegerToZeroOneBridge, ::MOI.ListOfVariableIndices) + return copy(bridge.y) +end + +function MOI.get( + bridge::IntegerToZeroOneBridge, + ::MOI.NumberOfConstraints{MOI.VariableIndex,MOI.ZeroOne}, +)::Int64 + return length(bridge.y) +end + +function MOI.get( + bridge::IntegerToZeroOneBridge, + ::MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.ZeroOne}, +) + return map(bridge.y) do y + return MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}(y.value) + end +end + +function MOI.get( + bridge::IntegerToZeroOneBridge{T}, + ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, +)::Int64 where {T} + return 1 +end + +function MOI.get( + bridge::IntegerToZeroOneBridge{T}, + ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}, +) where {T} + return [bridge.ci] +end + +MOI.Bridges.needs_final_touch(::IntegerToZeroOneBridge) = true + +function MOI.Bridges.final_touch( + bridge::IntegerToZeroOneBridge{T}, + model::MOI.ModelLike, +) where {T} + ret = MOI.Utilities.get_bounds(model, T, bridge.x) + if ret === bridge.last_bounds + return nothing # final_touch already called + elseif ret[1] == typemin(T) || ret[2] == typemax(T) + error( + "Unable to use IntegerToZeroOneBridge because the variable " * + "$(bridge.x) has a non-finite domain", + ) + end + f = MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(T(1), bridge.x)], T(0)) + lb, ub = ceil(Int, ret[1]), floor(Int, ret[2]) + N = floor(Int, log2(ub - lb)) + 1 + for i in 1:N + y, _ = MOI.add_constrained_variable(model, MOI.ZeroOne()) + push!(bridge.y, y) + push!(f.terms, MOI.ScalarAffineTerm(-(T(2)^(i - 1)), y)) + end + bridge.ci = MOI.add_constraint(model, f, MOI.EqualTo{T}(lb)) + bridge.last_bounds = ret + return +end diff --git a/test/Bridges/Constraint/integer_to_zeroone.jl b/test/Bridges/Constraint/integer_to_zeroone.jl new file mode 100644 index 0000000000..cc2f4bfa7a --- /dev/null +++ b/test/Bridges/Constraint/integer_to_zeroone.jl @@ -0,0 +1,102 @@ +# Copyright (c) 2017: Miles Lubin and contributors +# Copyright (c) 2017: Google Inc. +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +module TestConstraintIntegerToZeroOne + +using Test + +import MathOptInterface as MOI + +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$(name)", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end + return +end + +function test_runtests() + MOI.Bridges.runtests( + MOI.Bridges.Constraint.IntegerToZeroOneBridge, + """ + variables: x, z + x in Integer() + x in Interval(1.0, 3.0) + z in ZeroOne() + """, + """ + variables: x, z, y1, y2 + y1 in ZeroOne() + y2 in ZeroOne() + x + -1.0 * y1 + -2.0 * y2 == 1.0 + x in Interval(1.0, 3.0) + z in ZeroOne() + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.IntegerToZeroOneBridge, + """ + variables: x + x in Integer() + x in Interval(-1.0, 2.0) + """, + """ + variables: x, y1, y2 + y1 in ZeroOne() + y2 in ZeroOne() + x + -1.0 * y1 + -2.0 * y2 == -1.0 + x in Interval(-1.0, 2.0) + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Constraint.IntegerToZeroOneBridge, + """ + variables: x + x in Integer() + x in Interval(-2.0, 2.0) + """, + """ + variables: x, y1, y2, y3 + y1 in ZeroOne() + y2 in ZeroOne() + y3 in ZeroOne() + x + -1.0 * y1 + -2.0 * y2 + -4.0 * y3 == -2.0 + x in Interval(-2.0, 2.0) + """, + ) + return +end + +function test_finite_domain_error() + inner = MOI.Utilities.Model{Int}() + model = MOI.Bridges.Constraint.IntegerToZeroOne{Int}(inner) + x, _ = MOI.add_constrained_variable(model, MOI.Integer()) + @test_throws( + ErrorException( + "Unable to use IntegerToZeroOneBridge because the variable " * + "$(x) has a non-finite domain", + ), + MOI.Bridges.final_touch(model), + ) + return +end + +function test_final_touch_twice() + inner = MOI.Utilities.Model{Int}() + model = MOI.Bridges.Constraint.IntegerToZeroOne{Int}(inner) + x, _ = MOI.add_constrained_variable(model, MOI.Integer()) + MOI.add_constraint(model, x, MOI.Interval(1, 3)) + MOI.Bridges.final_touch(model) + MOI.Bridges.final_touch(model) + return +end + +end # module + +TestConstraintIntegerToZeroOne.runtests()