-
Notifications
You must be signed in to change notification settings - Fork 87
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 " * | ||
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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The variable bridge spirit would be to substitute There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 |
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() |
There was a problem hiding this comment.
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 likeis_infinity
. InInterval
we take care ofInt
where we never one to consider no bound while we don't do it here. At least it would make sure we are consistentThere was a problem hiding this comment.
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?).