Skip to content

Commit

Permalink
[Bridge] implement special case for x != y in CountDistinctToMILPBrid…
Browse files Browse the repository at this point in the history
…ge (#2416)
  • Loading branch information
odow authored Feb 5, 2024
1 parent 50d83f1 commit f5d8efa
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 5 deletions.
116 changes: 115 additions & 1 deletion src/Bridges/Constraint/bridges/count_distinct.jl
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ non-zero:
n - \\sum\\limits_{j \\in \\bigcup_{i=1,\\ldots,d} S_i} y_{j} = 0
```
## Formulation (special case)
In the special case that the constraint is `[2, x, y] in CountDistinct(3)`, then
the constraint is equivalent to `[x, y] in AllDifferent(2)`, which is equivalent
to `x != y`.
```math
(x - y <= -1) \\vee (y - x <= -1)
```
which is equivalent to (for suitable `M`):
```math
\\begin{aligned}
z \\in \\{0, 1\\} \\\\
x - y - M * z <= -1 \\\\
y - x - M * (1 - z) <= -1
\\end{aligned}
```
## Source node
`CountDistinctToMILPBridge` supports:
Expand Down Expand Up @@ -232,9 +250,105 @@ function MOI.Bridges.final_touch(
bridge::CountDistinctToMILPBridge{T,F},
model::MOI.ModelLike,
) where {T,F}
S = Dict{T,Vector{MOI.VariableIndex}}()
scalars = collect(MOI.Utilities.eachscalar(bridge.f))
bounds = Dict{MOI.VariableIndex,NTuple{2,T}}()
ret = MOI.Utilities.get_bounds(model, bounds, scalars[1])
if MOI.output_dimension(bridge.f) == 3 && ret == (2.0, 2.0)
# The special case of
# [x, y] in AllDifferent()
# bridged to
# [2, x, y] in CountDistinct()
# This is equivalent to the NotEqualTo set.
_final_touch_not_equal_case(bridge, model, scalars)
else
_final_touch_general_case(bridge, model, scalars)
end
return
end

function _final_touch_not_equal_case(
bridge::CountDistinctToMILPBridge{T,F},
model::MOI.ModelLike,
scalars,
) where {T,F}
bounds = Dict{MOI.VariableIndex,NTuple{2,T}}()
new_bounds = false
for i in 2:length(scalars)
x = scalars[i]
ret = MOI.Utilities.get_bounds(model, bounds, x)
if ret === nothing
error(
"Unable to use CountDistinctToMILPBridge because element $i " *
"in the function has a non-finite domain: $x",
)
end
if length(bridge.bounds) < i - 1
# This is the first time calling final_touch
push!(bridge.bounds, ret)
new_bounds = true
elseif bridge.bounds[i-1] == ret
# We've called final_touch before, and the bounds match. No need to
# reformulate a second time.
continue
elseif bridge.bounds[i-1] != ret
# There is a stored bound, and the current bounds do not match. This
# means the model has been modified since the previous call to
# final_touch. We need to delete the bridge and start again.
MOI.delete(model, bridge)
MOI.Bridges.final_touch(bridge, model)
return
end
end
if !new_bounds
return
end
# [2, x, y] in CountDistinct()
# <-->
# x != y
# <-->
# {x - y >= 1} \/ {y - x >= 1}
# <-->
# {x - y <= -1} \/ {y - x <= -1}
# <-->
# {x - y - M * z <= -1} /\ {y - x - M * (1 - z) <= -1}, z in {0, 1}
z, _ = MOI.add_constrained_variable(model, MOI.ZeroOne())
push!(bridge.variables, z)
x, y = scalars[2], scalars[3]
bx, by = bridge.bounds[1], bridge.bounds[2]
# {x - y - M * z <= -1}, M = u_x - l_y + 1
M = bx[2] - by[1] + 1
f = MOI.Utilities.operate(-, T, x, y)
push!(
bridge.less_than,
MOI.Utilities.normalize_and_add_constraint(
model,
MOI.Utilities.operate!(-, T, f, M * z),
MOI.LessThan(T(-1));
allow_modify_function = true,
),
)
# {y - x - M * (1 - z) <= -1}, M = u_x - l_y + 1
M = by[2] - bx[1] + 1
g = MOI.Utilities.operate(-, T, y, x)
push!(
bridge.less_than,
MOI.Utilities.normalize_and_add_constraint(
model,
MOI.Utilities.operate!(+, T, g, M * z),
MOI.LessThan(T(-1 + M));
allow_modify_function = true,
),
)
return
end

function _final_touch_general_case(
bridge::CountDistinctToMILPBridge{T,F},
model::MOI.ModelLike,
scalars,
) where {T,F}
S = Dict{T,Vector{MOI.VariableIndex}}()
bounds = Dict{MOI.VariableIndex,NTuple{2,T}}()
for i in 2:length(scalars)
x = scalars[i]
ret = MOI.Utilities.get_bounds(model, bounds, x)
Expand Down
49 changes: 45 additions & 4 deletions test/Bridges/Constraint/count_distinct.jl
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,46 @@ function test_runtests_VectorOfVariables()
return
end

function test_runtests_VectorOfVariables_NotEqualTo()
MOI.Bridges.runtests(
MOI.Bridges.Constraint.CountDistinctToMILPBridge,
"""
variables: n, x, y
[n, x, y] in CountDistinct(3)
x in Interval(1.0, 4.0)
y >= 2.0
y <= 5.0
n == 2.0
""",
"""
variables: n, x, y, z
1.0 * x + -1.0 * y + -3.0 * z <= -1.0
1.0 * y + -1.0 * x + 5.0 * z <= 4.0
x in Interval(1.0, 4.0)
y >= 2.0
y <= 5.0
n == 2.0
z in ZeroOne()
""",
)
return
end

function test_runtests_VectorAffineFunction()
MOI.Bridges.runtests(
MOI.Bridges.Constraint.CountDistinctToMILPBridge,
"""
variables: x, y
[2.0, 2.0 * x + -1.0, y] in CountDistinct(3)
variables: d, x, y
[d, 2.0 * x + -1.0, y] in CountDistinct(3)
x in Interval(1.0, 2.0)
y >= 2.0
y <= 3.0
""",
"""
variables: x, y, z_x1, z_x2, z_x3, z_y2, z_y3, a_1, a_2, a_3
variables: d, x, y, z_x1, z_x2, z_x3, z_y2, z_y3, a_1, a_2, a_3
2.0 * x + -1.0 * z_x1 + -2.0 * z_x2 + -3.0 * z_x3 == 1.0
1.0 * y + -2.0 * z_y2 + -3.0 * z_y3 == 0.0
a_1 + a_2 + a_3 == 2.0
a_1 + a_2 + a_3 + -1.0 * d == 0.0
z_x1 + z_x2 + z_x3 == 1.0
z_y2 + z_y3 == 1.0
z_x1 + -1.0 * a_1 <= 0.0
Expand Down Expand Up @@ -146,6 +171,22 @@ function test_runtests_error_affine()
return
end

function test_resolve_with_modified_not_equal_to()
inner = MOI.Utilities.Model{Int}()
model = MOI.Bridges.Constraint.CountDistinctToMILP{Int}(inner)
x = MOI.add_variables(model, 3)
c = MOI.add_constraint.(model, x[2:3], MOI.Interval(0, 2))
MOI.add_constraint(model, x[1], MOI.EqualTo(2))
MOI.add_constraint(model, MOI.VectorOfVariables(x), MOI.CountDistinct(3))
@test MOI.get(inner, MOI.NumberOfVariables()) == 3
MOI.Bridges.final_touch(model)
@test MOI.get(inner, MOI.NumberOfVariables()) == 4
MOI.set(model, MOI.ConstraintSet(), c[2], MOI.Interval(0, 1))
MOI.Bridges.final_touch(model)
@test MOI.get(inner, MOI.NumberOfVariables()) == 4
return
end

end # module

TestConstraintCountDistinct.runtests()

0 comments on commit f5d8efa

Please sign in to comment.