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 1 commit
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 @@
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\\}
odow marked this conversation as resolved.
Show resolved Hide resolved
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 @@
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)

Check warning on line 256 in src/Bridges/Constraint/bridges/count_distinct.jl

View check run for this annotation

Codecov / codecov/patch

src/Bridges/Constraint/bridges/count_distinct.jl#L255-L256

Added lines #L255 - L256 were not covered by tests
# 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)

Check warning on line 262 in src/Bridges/Constraint/bridges/count_distinct.jl

View check run for this annotation

Codecov / codecov/patch

src/Bridges/Constraint/bridges/count_distinct.jl#L262

Added line #L262 was not covered by tests
else
_final_touch_general_case(bridge, model, scalars)

Check warning on line 264 in src/Bridges/Constraint/bridges/count_distinct.jl

View check run for this annotation

Codecov / codecov/patch

src/Bridges/Constraint/bridges/count_distinct.jl#L264

Added line #L264 was not covered by tests
end
return

Check warning on line 266 in src/Bridges/Constraint/bridges/count_distinct.jl

View check run for this annotation

Codecov / codecov/patch

src/Bridges/Constraint/bridges/count_distinct.jl#L266

Added line #L266 was not covered by tests
end

function _final_touch_not_equal_case(

Check warning on line 269 in src/Bridges/Constraint/bridges/count_distinct.jl

View check run for this annotation

Codecov / codecov/patch

src/Bridges/Constraint/bridges/count_distinct.jl#L269

Added line #L269 was not covered by tests
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(

Check warning on line 280 in src/Bridges/Constraint/bridges/count_distinct.jl

View check run for this annotation

Codecov / codecov/patch

src/Bridges/Constraint/bridges/count_distinct.jl#L274-L280

Added lines #L274 - L280 were not covered by tests
"Unable to use CountDistinctToMILPBridge because element $i " *
"in the function has a non-finite domain: $x",
)
end
if length(bridge.bounds) < i - 1

Check warning on line 285 in src/Bridges/Constraint/bridges/count_distinct.jl

View check run for this annotation

Codecov / codecov/patch

src/Bridges/Constraint/bridges/count_distinct.jl#L285

Added line #L285 was not covered by tests
# This is the first time calling final_touch
push!(bridge.bounds, ret)
new_bounds = true
elseif bridge.bounds[i-1] == ret

Check warning on line 289 in src/Bridges/Constraint/bridges/count_distinct.jl

View check run for this annotation

Codecov / codecov/patch

src/Bridges/Constraint/bridges/count_distinct.jl#L287-L289

Added lines #L287 - L289 were not covered by tests
# We've called final_touch before, and the bounds match. No need to
# reformulate a second time.
continue
elseif bridge.bounds[i-1] != ret

Check warning on line 293 in src/Bridges/Constraint/bridges/count_distinct.jl

View check run for this annotation

Codecov / codecov/patch

src/Bridges/Constraint/bridges/count_distinct.jl#L292-L293

Added lines #L292 - L293 were not covered by tests
# 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

Check warning on line 299 in src/Bridges/Constraint/bridges/count_distinct.jl

View check run for this annotation

Codecov / codecov/patch

src/Bridges/Constraint/bridges/count_distinct.jl#L297-L299

Added lines #L297 - L299 were not covered by tests
end
end
if !new_bounds
return

Check warning on line 303 in src/Bridges/Constraint/bridges/count_distinct.jl

View check run for this annotation

Codecov / codecov/patch

src/Bridges/Constraint/bridges/count_distinct.jl#L301-L303

Added lines #L301 - L303 were not covered by tests
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]

Check warning on line 317 in src/Bridges/Constraint/bridges/count_distinct.jl

View check run for this annotation

Codecov / codecov/patch

src/Bridges/Constraint/bridges/count_distinct.jl#L314-L317

Added lines #L314 - L317 were not covered by tests
# {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]
f = MOI.Utilities.operate(-, T, x, y)
push!(

Check warning on line 321 in src/Bridges/Constraint/bridges/count_distinct.jl

View check run for this annotation

Codecov / codecov/patch

src/Bridges/Constraint/bridges/count_distinct.jl#L319-L321

Added lines #L319 - L321 were not covered by tests
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]
g = MOI.Utilities.operate(-, T, y, x)
push!(

Check warning on line 333 in src/Bridges/Constraint/bridges/count_distinct.jl

View check run for this annotation

Codecov / codecov/patch

src/Bridges/Constraint/bridges/count_distinct.jl#L331-L333

Added lines #L331 - L333 were not covered by tests
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

Check warning on line 342 in src/Bridges/Constraint/bridges/count_distinct.jl

View check run for this annotation

Codecov / codecov/patch

src/Bridges/Constraint/bridges/count_distinct.jl#L342

Added line #L342 was not covered by tests
end

function _final_touch_general_case(

Check warning on line 345 in src/Bridges/Constraint/bridges/count_distinct.jl

View check run for this annotation

Codecov / codecov/patch

src/Bridges/Constraint/bridges/count_distinct.jl#L345

Added line #L345 was not covered by tests
bridge::CountDistinctToMILPBridge{T,F},
model::MOI.ModelLike,
scalars,
) where {T,F}
S = Dict{T,Vector{MOI.VariableIndex}}()
bounds = Dict{MOI.VariableIndex,NTuple{2,T}}()

Check warning on line 351 in src/Bridges/Constraint/bridges/count_distinct.jl

View check run for this annotation

Codecov / codecov/patch

src/Bridges/Constraint/bridges/count_distinct.jl#L350-L351

Added lines #L350 - L351 were not covered by tests
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 + -2.0 * z <= -1.0
1.0 * y + -1.0 * x + -4.0 * z <= -5.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