Skip to content

Commit

Permalink
[Bridges] add IntegerToZeroOneBridge (#2205)
Browse files Browse the repository at this point in the history
  • Loading branch information
odow authored Jul 2, 2023
1 parent 5ac9cbb commit dab02e5
Show file tree
Hide file tree
Showing 4 changed files with 280 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/src/submodules/Bridges/list_of_bridges.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/Bridges/Constraint/Constraint.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down
175 changes: 175 additions & 0 deletions src/Bridges/Constraint/bridges/integer_to_zeroone.jl
Original file line number Diff line number Diff line change
@@ -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
102 changes: 102 additions & 0 deletions test/Bridges/Constraint/integer_to_zeroone.jl
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit dab02e5

Please sign in to comment.