Skip to content

Commit

Permalink
[Bridges] add CountDistinctToMILPBridge
Browse files Browse the repository at this point in the history
  • Loading branch information
odow committed Jun 24, 2022
1 parent 9eb7ae2 commit b1989e5
Show file tree
Hide file tree
Showing 5 changed files with 365 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/Bridges/Bridges.jl
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ function runtests(Bridge::Type{<:AbstractBridge}, input::String, output::String)
inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
model = _bridged_model(Bridge, inner)
MOI.Utilities.loadfromstring!(model, input)
MOI.Utilities.final_touch(model)
# Load a non-bridged input model, and check that getters are the same.
test = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
MOI.Utilities.loadfromstring!(test, input)
Expand Down
2 changes: 2 additions & 0 deletions src/Bridges/Constraint/Constraint.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ include("set_map.jl")
include("single_bridge_optimizer.jl")

include("bridges/bin_packing.jl")
include("bridges/count_distinct.jl")
include("bridges/det.jl")
include("bridges/flip_sign.jl")
include("bridges/functionize.jl")
Expand Down Expand Up @@ -95,6 +96,7 @@ function add_all_bridges(bridged_model, ::Type{T}) where {T}
# TODO(odow): this reformulation assumes the bins are numbered 1..N. We
# should fix this to use the variable bounds before adding automatically.
# MOI.Bridges.add_bridge(bridged_model, BinPackingToMILPBridge{T})
MOI.Bridges.add_bridge(bridged_model, CountDistinctToMILPBridge{T})
MOI.Bridges.add_bridge(bridged_model, TableToMILPBridge{T})
return
end
Expand Down
286 changes: 286 additions & 0 deletions src/Bridges/Constraint/bridges/count_distinct.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
# 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.

"""
CountDistinctToMILPBridge{T} <: Bridges.Constraint.AbstractBridge
`CountDistinctToMILPBridge` implements the following reformulation:
## Source node
`CountDistinctToMILPBridge` supports:
* [`MOI.VectorOfVariables`] in [`MOI.AllDifferent`](@ref)
## Target nodes
`CountDistinctToMILPBridge` creates:
* [`MOI.VariableIndex`](@ref) in [`MOI.ZeroOne`](@ref)
* [`MOI.ScalarAffineFunction{T}`](@ref) in [`MOI.EqualTo{T}`](@ref)
* [`MOI.ScalarAffineFunction{T}`](@ref) in [`MOI.LessThan{T}`](@ref)
"""
mutable struct CountDistinctToMILPBridge{T} <: AbstractBridge
f::MOI.VectorOfVariables
z::Dict{Tuple{MOI.VariableIndex,Int},MOI.VariableIndex}
α::Dict{Int,MOI.VariableIndex}
# ∑_j a_j + -1.0 * n == 0.0
count::Union{
Nothing,
MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}},
}
# x_i => x_i - ∑_j z_ij = 0 ∀i
unit_expansion::Vector{
MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}},
}
# j => ∑_i z_ij <= α_j ∀j
big_M::Vector{
MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.LessThan{T}},
}
function CountDistinctToMILPBridge{T}(f::MOI.VectorOfVariables) where {T}
return new{T}(
f,
Dict{Tuple{MOI.VariableIndex,Int},MOI.VariableIndex}(),
Dict{Int,MOI.VariableIndex}(),
nothing,
MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}[],
MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.LessThan{T}}[],
)
end
end

const CountDistinctToMILP{T,OT<:MOI.ModelLike} =
SingleBridgeOptimizer{CountDistinctToMILPBridge{T},OT}

function bridge_constraint(
::Type{CountDistinctToMILPBridge{T}},
model::MOI.ModelLike,
f::MOI.VectorOfVariables,
s::MOI.CountDistinct,
) where {T}
# !!! info
# Postpone creation until final_touch.
return CountDistinctToMILPBridge{T}(f)
end

function MOI.supports_constraint(
::Type{CountDistinctToMILPBridge{T}},
::Type{MOI.VectorOfVariables},
::Type{MOI.CountDistinct},
) where {T}
return true
end

function MOI.Bridges.added_constrained_variable_types(
::Type{<:CountDistinctToMILPBridge},
)
return Tuple{Type}[(MOI.ZeroOne,)]
end

function MOI.Bridges.added_constraint_types(
::Type{CountDistinctToMILPBridge{T}},
) where {T}
return Tuple{Type,Type}[
(MOI.ScalarAffineFunction{T}, MOI.EqualTo{T}),
(MOI.ScalarAffineFunction{T}, MOI.LessThan{T}),
]
end

function concrete_bridge_type(
::Type{<:CountDistinctToMILPBridge{T}},
::Type{MOI.VectorOfVariables},
::Type{MOI.CountDistinct},
) where {T}
return CountDistinctToMILPBridge{T}
end

function MOI.get(
::MOI.ModelLike,
::MOI.ConstraintFunction,
bridge::CountDistinctToMILPBridge,
)
return bridge.f
end

function MOI.get(
::MOI.ModelLike,
::MOI.ConstraintSet,
bridge::CountDistinctToMILPBridge,
)
return MOI.CountDistinct(length(bridge.f.variables))
end

function MOI.delete(model::MOI.ModelLike, bridge::CountDistinctToMILPBridge)
if bridge.count === nothing
return
end
for ci in bridge.unit_expansion
MOI.delete(model, ci)
end
for ci in bridge.big_M
MOI.delete(model, ci)
end
MOI.delete(model, bridge.count)
for (_, α) in bridge.α
MOI.delete(model, α)
end
for (_, z) in bridge.z
MOI.delete(model, z)
end
return
end

function MOI.get(
bridge::CountDistinctToMILPBridge,
::MOI.NumberOfVariables,
)::Int64
return length(bridge.z) + length(bridge.α)
end

function MOI.get(bridge::CountDistinctToMILPBridge, ::MOI.ListOfVariableIndices)
return vcat(collect(values(bridge.z)), collect(values(bridge.α)))
end

function MOI.get(
bridge::CountDistinctToMILPBridge,
::MOI.NumberOfConstraints{MOI.VariableIndex,MOI.ZeroOne},
)::Int64
return length(bridge.z) + length(bridge.α)
end

function MOI.get(
bridge::CountDistinctToMILPBridge,
::MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.ZeroOne},
)
ret = MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}[
MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}(z.value) for
z in values(bridge.z)
]
for z in values(bridge.z)
push!(ret, MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}(z.value))
end
return ret
end

function MOI.get(
bridge::CountDistinctToMILPBridge{T},
::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}},
)::Int64 where {T}
if count === nothing
return 0
end
return 1 + length(bridge.unit_expansion)
end

function MOI.get(
bridge::CountDistinctToMILPBridge{T},
::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}},
) where {T}
if count === nothing
return MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}[]
end
return vcat(bridge.count, bridge.unit_expansion)
end

function MOI.get(
bridge::CountDistinctToMILPBridge{T},
::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},MOI.LessThan{T}},
)::Int64 where {T}
return length(bridge.big_M)
end

function MOI.get(
bridge::CountDistinctToMILPBridge{T},
::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.LessThan{T}},
) where {T}
return copy(bridge.big_M)
end

MOI.Bridges.needs_final_touch(::CountDistinctToMILPBridge) = true

function _get_bounds(::Type{T}, model, x) where {T}
lb, ub = nothing, nothing
F, f = MOI.VariableIndex, x.value
ci = MOI.ConstraintIndex{F,MOI.GreaterThan{T}}(f)
if MOI.is_valid(model, ci)
lb = MOI.get(model, MOI.ConstraintSet(), ci).lower
end
ci = MOI.ConstraintIndex{F,MOI.LessThan{T}}(f)
if MOI.is_valid(model, ci)
ub = MOI.get(model, MOI.ConstraintSet(), ci).upper
end
ci = MOI.ConstraintIndex{F,MOI.EqualTo{T}}(f)
if MOI.is_valid(model, ci)
lb = ub = MOI.get(model, MOI.ConstraintSet(), ci).value
end
ci = MOI.ConstraintIndex{F,MOI.Interval{T}}(f)
if MOI.is_valid(model, ci)
set = MOI.get(model, MOI.ConstraintSet(), ci)
lb, ub = set.lower, set.upper
end
return lb, ub
end

function MOI.Utilities.final_touch(
bridge::CountDistinctToMILPBridge{T},
model::MOI.ModelLike,
) where {T}
if bridge.count !== nothing
MOI.delete(model, bridge)
end
S = Dict{T,Vector{MOI.VariableIndex}}()
for x in bridge.f.variables[2:end]
lb, ub = _get_bounds(T, model, x)
if lb === nothing || ub === nothing
error(
"Unable to use CountDistinctToMILPBridge because variable $x " *
"has a non-finite domain.",
)
end
unit_terms = [MOI.ScalarAffineTerm(one(T), x)]
for xi in lb:ub
new_var, _ = MOI.add_constrained_variable(model, MOI.ZeroOne())
bridge.z[(x, xi)] = new_var
push!(unit_terms, MOI.ScalarAffineTerm(T(-xi), new_var))
if !haskey(S, xi)
S[xi] = MOI.VariableIndex[]
end
push!(S[xi], new_var)
end
push!(
bridge.unit_expansion,
MOI.add_constraint(
model,
MOI.ScalarAffineFunction(unit_terms, zero(T)),
MOI.EqualTo(zero(T)),
),
)
end
count_terms = [MOI.ScalarAffineTerm(T(-1), bridge.f.variables[1])]
# We use a sort so that the model order is deterministic.
for s in sort!(collect(keys(S)))
terms = S[s]
new_var, _ = MOI.add_constrained_variable(model, MOI.ZeroOne())
bridge.α[s] = new_var
push!(count_terms, MOI.ScalarAffineTerm(one(T), new_var))
big_M_terms = [MOI.ScalarAffineTerm(T(1), z) for z in terms]
push!(big_M_terms, MOI.ScalarAffineTerm(T(-length(terms)), new_var))
push!(
bridge.big_M,
MOI.add_constraint(
model,
MOI.ScalarAffineFunction(big_M_terms, zero(T)),
MOI.LessThan(zero(T)),
),
)
end
bridge.count = MOI.add_constraint(
model,
MOI.ScalarAffineFunction(count_terms, zero(T)),
MOI.EqualTo(zero(T)),
)
return
end
2 changes: 1 addition & 1 deletion src/Bridges/bridge_optimizer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ function MOI.supports_incremental_interface(b::AbstractBridgeOptimizer)
end

function MOI.Utilities.final_touch(b::AbstractBridgeOptimizer)
MOI.Utilities.final_touch(Constraint.bridges(b), b)
MOI.Utilities.final_touch(Constraint.bridges(b), recursive_model(b))
return
end

Expand Down
75 changes: 75 additions & 0 deletions test/Bridges/Constraint/count_distinct.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# 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 TestConstraintCountDistinct

using Test

using MathOptInterface
const MOI = MathOptInterface

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.CountDistinctToMILPBridge,
"""
variables: n, x, y
[n, x, y] in CountDistinct(3)
x in Interval(1.0, 2.0)
y >= 2.0
y <= 3.0
""",
"""
variables: n, x, y, z_x1, z_x2, z_y2, z_y3, a_1, a_2, a_3
1.0 * x + -1.0 * z_x1 + -2.0 * z_x2 == 0.0
1.0 * y + -2.0 * z_y2 + -3.0 * z_y3 == 0.0
-1.0 * n + a_1 + a_2 + a_3 == 0.0
z_x1 + -1.0 * a_1 <= 0.0
z_x2 + z_y2 + -2.0 * a_2 <= 0.0
z_y3 + -1.0 * a_3 <= 0.0
x in Interval(1.0, 2.0)
y >= 2.0
y <= 3.0
z_x1 in ZeroOne()
z_x2 in ZeroOne()
z_y2 in ZeroOne()
z_y3 in ZeroOne()
a_1 in ZeroOne()
a_2 in ZeroOne()
a_3 in ZeroOne()
""",
)
return
end

function test_runtests_err()
inner = MOI.Utilities.Model{Int}()
model = MOI.Bridges.Constraint.CountDistinctToMILP{Int}(inner)
x = MOI.add_variables(model, 3)
MOI.add_constraint(model, MOI.VectorOfVariables(x), MOI.CountDistinct(3))
@test_throws(
ErrorException(
"Unable to use CountDistinctToMILPBridge because variable $(x[2]) " *
"has a non-finite domain.",
),
MOI.Utilities.final_touch(model),
)
return
end

end # module

TestConstraintCountDistinct.runtests()

0 comments on commit b1989e5

Please sign in to comment.