Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bridges] add IntegerToZeroOneBridge #2205

Merged
merged 5 commits into from
Jul 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -63,6 +63,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 @@ -124,6 +125,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)
Copy link
Member

@blegat blegat Jun 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably have a utility for that shared with SplitInterval and other bridges doing this like is_infinity. In Interval we take care of Int where we never one to consider no bound while we don't do it here. At least it would make sure we are consistent

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the SplitInterval reference. The SplitInterval bridge doesn't rely on a finite domain, it works with one-sided intervals. In comparison, we can't bridge an open set to ZeroOne (unless we pick a big M and do a 20 or 30 bit expansion?).

error(
"Unable to use IntegerToZeroOneBridge because the variable " *
odow marked this conversation as resolved.
Show resolved Hide resolved
"$(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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable bridge spirit would be to substitute x. Here it seems to be a bad idea because it x is a big sum.
This is like bridge a constrained variables x in Int by first adding the variable free and constraining it, then functionizing it and then slack bridge and then variable bridge for the slack. I'm wondering if there could be a nice way to implement this as a variable bridge but defaulting it to be used like that while allowing the user to have x be substituted as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you just drop a comment that it would more naturally be implemented as a variable bridge but we don't want to substitute every occurence of x so that's why we do it as a constraint bridge ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if there could be a nice way to implement this as a variable bridge

That would only be necessary for a solver supports only binary variables and not real. We don't really have any current examples, although @ccoffrin might be interested for future quantum annealing formulations. (They have some pretty specific structure restrictions though, which mean doing it naively might be the wrong thing to do.)

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()