Skip to content

Commit f5d8efa

Browse files
authored
[Bridge] implement special case for x != y in CountDistinctToMILPBridge (#2416)
1 parent 50d83f1 commit f5d8efa

File tree

2 files changed

+160
-5
lines changed

2 files changed

+160
-5
lines changed

src/Bridges/Constraint/bridges/count_distinct.jl

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,24 @@ non-zero:
4141
n - \\sum\\limits_{j \\in \\bigcup_{i=1,\\ldots,d} S_i} y_{j} = 0
4242
```
4343
44+
## Formulation (special case)
45+
46+
In the special case that the constraint is `[2, x, y] in CountDistinct(3)`, then
47+
the constraint is equivalent to `[x, y] in AllDifferent(2)`, which is equivalent
48+
to `x != y`.
49+
50+
```math
51+
(x - y <= -1) \\vee (y - x <= -1)
52+
```
53+
which is equivalent to (for suitable `M`):
54+
```math
55+
\\begin{aligned}
56+
z \\in \\{0, 1\\} \\\\
57+
x - y - M * z <= -1 \\\\
58+
y - x - M * (1 - z) <= -1
59+
\\end{aligned}
60+
```
61+
4462
## Source node
4563
4664
`CountDistinctToMILPBridge` supports:
@@ -232,9 +250,105 @@ function MOI.Bridges.final_touch(
232250
bridge::CountDistinctToMILPBridge{T,F},
233251
model::MOI.ModelLike,
234252
) where {T,F}
235-
S = Dict{T,Vector{MOI.VariableIndex}}()
236253
scalars = collect(MOI.Utilities.eachscalar(bridge.f))
237254
bounds = Dict{MOI.VariableIndex,NTuple{2,T}}()
255+
ret = MOI.Utilities.get_bounds(model, bounds, scalars[1])
256+
if MOI.output_dimension(bridge.f) == 3 && ret == (2.0, 2.0)
257+
# The special case of
258+
# [x, y] in AllDifferent()
259+
# bridged to
260+
# [2, x, y] in CountDistinct()
261+
# This is equivalent to the NotEqualTo set.
262+
_final_touch_not_equal_case(bridge, model, scalars)
263+
else
264+
_final_touch_general_case(bridge, model, scalars)
265+
end
266+
return
267+
end
268+
269+
function _final_touch_not_equal_case(
270+
bridge::CountDistinctToMILPBridge{T,F},
271+
model::MOI.ModelLike,
272+
scalars,
273+
) where {T,F}
274+
bounds = Dict{MOI.VariableIndex,NTuple{2,T}}()
275+
new_bounds = false
276+
for i in 2:length(scalars)
277+
x = scalars[i]
278+
ret = MOI.Utilities.get_bounds(model, bounds, x)
279+
if ret === nothing
280+
error(
281+
"Unable to use CountDistinctToMILPBridge because element $i " *
282+
"in the function has a non-finite domain: $x",
283+
)
284+
end
285+
if length(bridge.bounds) < i - 1
286+
# This is the first time calling final_touch
287+
push!(bridge.bounds, ret)
288+
new_bounds = true
289+
elseif bridge.bounds[i-1] == ret
290+
# We've called final_touch before, and the bounds match. No need to
291+
# reformulate a second time.
292+
continue
293+
elseif bridge.bounds[i-1] != ret
294+
# There is a stored bound, and the current bounds do not match. This
295+
# means the model has been modified since the previous call to
296+
# final_touch. We need to delete the bridge and start again.
297+
MOI.delete(model, bridge)
298+
MOI.Bridges.final_touch(bridge, model)
299+
return
300+
end
301+
end
302+
if !new_bounds
303+
return
304+
end
305+
# [2, x, y] in CountDistinct()
306+
# <-->
307+
# x != y
308+
# <-->
309+
# {x - y >= 1} \/ {y - x >= 1}
310+
# <-->
311+
# {x - y <= -1} \/ {y - x <= -1}
312+
# <-->
313+
# {x - y - M * z <= -1} /\ {y - x - M * (1 - z) <= -1}, z in {0, 1}
314+
z, _ = MOI.add_constrained_variable(model, MOI.ZeroOne())
315+
push!(bridge.variables, z)
316+
x, y = scalars[2], scalars[3]
317+
bx, by = bridge.bounds[1], bridge.bounds[2]
318+
# {x - y - M * z <= -1}, M = u_x - l_y + 1
319+
M = bx[2] - by[1] + 1
320+
f = MOI.Utilities.operate(-, T, x, y)
321+
push!(
322+
bridge.less_than,
323+
MOI.Utilities.normalize_and_add_constraint(
324+
model,
325+
MOI.Utilities.operate!(-, T, f, M * z),
326+
MOI.LessThan(T(-1));
327+
allow_modify_function = true,
328+
),
329+
)
330+
# {y - x - M * (1 - z) <= -1}, M = u_x - l_y + 1
331+
M = by[2] - bx[1] + 1
332+
g = MOI.Utilities.operate(-, T, y, x)
333+
push!(
334+
bridge.less_than,
335+
MOI.Utilities.normalize_and_add_constraint(
336+
model,
337+
MOI.Utilities.operate!(+, T, g, M * z),
338+
MOI.LessThan(T(-1 + M));
339+
allow_modify_function = true,
340+
),
341+
)
342+
return
343+
end
344+
345+
function _final_touch_general_case(
346+
bridge::CountDistinctToMILPBridge{T,F},
347+
model::MOI.ModelLike,
348+
scalars,
349+
) where {T,F}
350+
S = Dict{T,Vector{MOI.VariableIndex}}()
351+
bounds = Dict{MOI.VariableIndex,NTuple{2,T}}()
238352
for i in 2:length(scalars)
239353
x = scalars[i]
240354
ret = MOI.Utilities.get_bounds(model, bounds, x)

test/Bridges/Constraint/count_distinct.jl

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,46 @@ function test_runtests_VectorOfVariables()
6161
return
6262
end
6363

64+
function test_runtests_VectorOfVariables_NotEqualTo()
65+
MOI.Bridges.runtests(
66+
MOI.Bridges.Constraint.CountDistinctToMILPBridge,
67+
"""
68+
variables: n, x, y
69+
[n, x, y] in CountDistinct(3)
70+
x in Interval(1.0, 4.0)
71+
y >= 2.0
72+
y <= 5.0
73+
n == 2.0
74+
""",
75+
"""
76+
variables: n, x, y, z
77+
1.0 * x + -1.0 * y + -3.0 * z <= -1.0
78+
1.0 * y + -1.0 * x + 5.0 * z <= 4.0
79+
x in Interval(1.0, 4.0)
80+
y >= 2.0
81+
y <= 5.0
82+
n == 2.0
83+
z in ZeroOne()
84+
""",
85+
)
86+
return
87+
end
88+
6489
function test_runtests_VectorAffineFunction()
6590
MOI.Bridges.runtests(
6691
MOI.Bridges.Constraint.CountDistinctToMILPBridge,
6792
"""
68-
variables: x, y
69-
[2.0, 2.0 * x + -1.0, y] in CountDistinct(3)
93+
variables: d, x, y
94+
[d, 2.0 * x + -1.0, y] in CountDistinct(3)
7095
x in Interval(1.0, 2.0)
7196
y >= 2.0
7297
y <= 3.0
7398
""",
7499
"""
75-
variables: x, y, z_x1, z_x2, z_x3, z_y2, z_y3, a_1, a_2, a_3
100+
variables: d, x, y, z_x1, z_x2, z_x3, z_y2, z_y3, a_1, a_2, a_3
76101
2.0 * x + -1.0 * z_x1 + -2.0 * z_x2 + -3.0 * z_x3 == 1.0
77102
1.0 * y + -2.0 * z_y2 + -3.0 * z_y3 == 0.0
78-
a_1 + a_2 + a_3 == 2.0
103+
a_1 + a_2 + a_3 + -1.0 * d == 0.0
79104
z_x1 + z_x2 + z_x3 == 1.0
80105
z_y2 + z_y3 == 1.0
81106
z_x1 + -1.0 * a_1 <= 0.0
@@ -146,6 +171,22 @@ function test_runtests_error_affine()
146171
return
147172
end
148173

174+
function test_resolve_with_modified_not_equal_to()
175+
inner = MOI.Utilities.Model{Int}()
176+
model = MOI.Bridges.Constraint.CountDistinctToMILP{Int}(inner)
177+
x = MOI.add_variables(model, 3)
178+
c = MOI.add_constraint.(model, x[2:3], MOI.Interval(0, 2))
179+
MOI.add_constraint(model, x[1], MOI.EqualTo(2))
180+
MOI.add_constraint(model, MOI.VectorOfVariables(x), MOI.CountDistinct(3))
181+
@test MOI.get(inner, MOI.NumberOfVariables()) == 3
182+
MOI.Bridges.final_touch(model)
183+
@test MOI.get(inner, MOI.NumberOfVariables()) == 4
184+
MOI.set(model, MOI.ConstraintSet(), c[2], MOI.Interval(0, 1))
185+
MOI.Bridges.final_touch(model)
186+
@test MOI.get(inner, MOI.NumberOfVariables()) == 4
187+
return
188+
end
189+
149190
end # module
150191

151192
TestConstraintCountDistinct.runtests()

0 commit comments

Comments
 (0)