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

[Bridge] implement special case for x != y in CountDistinctToMILPBridge #2416

Merged
merged 5 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
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 * (z - 1) <= -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 * (z - 1) <= -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
odow marked this conversation as resolved.
Show resolved Hide resolved
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 * (z - 1) <= -1}, M = u_x - l_y
odow marked this conversation as resolved.
Show resolved Hide resolved
M = by[2] - bx[1] + 1
odow marked this conversation as resolved.
Show resolved Hide resolved
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 <= -6.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()
odow marked this conversation as resolved.
Show resolved Hide resolved
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()
Loading