Skip to content

Commit 90d7dcf

Browse files
authored
[Bridges] add CountBelongsToMILPBridge (#1919)
1 parent 187b8b4 commit 90d7dcf

File tree

7 files changed

+528
-17
lines changed

7 files changed

+528
-17
lines changed

docs/src/submodules/Bridges/list_of_bridges.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Bridges.Constraint.IndicatorSOS1Bridge
5252
Bridges.Constraint.SemiToBinaryBridge
5353
Bridges.Constraint.ZeroOneBridge
5454
Bridges.Constraint.BinPackingToMILPBridge
55+
Bridges.Constraint.CountBelongsToMILPBridge
5556
Bridges.Constraint.CountDistinctToMILPBridge
5657
Bridges.Constraint.TableToMILPBridge
5758
```

src/Bridges/Constraint/Constraint.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ include("set_map.jl")
1919
include("single_bridge_optimizer.jl")
2020

2121
include("bridges/bin_packing.jl")
22+
include("bridges/count_belongs.jl")
2223
include("bridges/count_distinct.jl")
2324
include("bridges/det.jl")
2425
include("bridges/flip_sign.jl")
@@ -96,6 +97,7 @@ function add_all_bridges(bridged_model, ::Type{T}) where {T}
9697
# TODO(odow): this reformulation assumes the bins are numbered 1..N. We
9798
# should fix this to use the variable bounds before adding automatically.
9899
# MOI.Bridges.add_bridge(bridged_model, BinPackingToMILPBridge{T})
100+
MOI.Bridges.add_bridge(bridged_model, CountBelongsToMILPBridge{T})
99101
MOI.Bridges.add_bridge(bridged_model, CountDistinctToMILPBridge{T})
100102
MOI.Bridges.add_bridge(bridged_model, TableToMILPBridge{T})
101103
return
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
# Copyright (c) 2017: Miles Lubin and contributors
2+
# Copyright (c) 2017: Google Inc.
3+
#
4+
# Use of this source code is governed by an MIT-style license that can be found
5+
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.
6+
7+
"""
8+
CountBelongsToMILPBridge{T,F} <: Bridges.Constraint.AbstractBridge
9+
10+
`CountBelongsToMILPBridge` implements the following reformulation:
11+
12+
* ``(n, x) \\in \\textsf{CountBelongs}(1+d, \\mathcal{S})`` into a
13+
mixed-integer linear program.
14+
15+
## Reformulation
16+
17+
The reformulation is non-trivial, and it depends on the finite domain of each
18+
variable ``x_i``, which we as define ``S_i = \\{l_i,\\ldots,u_i\\}``.
19+
20+
First, we introduce new binary variables ``z_{ij}``, which are ``1`` if variable
21+
``x_i`` takes the value ``j`` in the optimal solution and ``0`` otherwise:
22+
```math
23+
\\begin{aligned}
24+
z_{ij} \\in \\{0, 1\\} & \\;\\; \\forall i \\in 1\\ldots d, j \\in S_i \\\\
25+
x_i - \\sum\\limits_{j\\in S_i} j \\cdot z_{ij} = 0 & \\;\\; \\forall i \\in 1\\ldots d \\\\
26+
\\sum\\limits_{j\\in S_i} z_{ij} = 1 & \\;\\; \\forall i \\in 1\\ldots d \\\\
27+
\\end{aligned}
28+
```
29+
30+
Finally, ``n`` is constrained to be the number of ``z_{ij}`` elements that are
31+
in ``\\mathcal{S}``:
32+
```math
33+
n - \\sum\\limits_{j \\in \\mathcal{S}} y_{j} = 0
34+
```
35+
36+
## Source node
37+
38+
`CountBelongsToMILPBridge` supports:
39+
40+
* `F` in [`MOI.CountBelongs`](@ref)
41+
42+
where `F` is [`MOI.VectorOfVariables`](@ref) or
43+
[`MOI.VectorAffineFunction{T}`](@ref).
44+
45+
## Target nodes
46+
47+
`CountBelongsToMILPBridge` creates:
48+
49+
* [`MOI.VariableIndex`](@ref) in [`MOI.ZeroOne`](@ref)
50+
* [`MOI.ScalarAffineFunction{T}`](@ref) in [`MOI.EqualTo{T}`](@ref)
51+
"""
52+
mutable struct CountBelongsToMILPBridge{
53+
T,
54+
F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}},
55+
} <: AbstractBridge
56+
f::F
57+
set::MOI.CountBelongs
58+
variables::Vector{MOI.VariableIndex}
59+
equal_to::Vector{
60+
MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}},
61+
}
62+
function CountBelongsToMILPBridge{T}(
63+
f::Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}},
64+
s::MOI.CountBelongs,
65+
) where {T}
66+
return new{T,typeof(f)}(
67+
f,
68+
s,
69+
MOI.VariableIndex[],
70+
MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}[],
71+
)
72+
end
73+
end
74+
75+
const CountBelongsToMILP{T,OT<:MOI.ModelLike} =
76+
SingleBridgeOptimizer{CountBelongsToMILPBridge{T},OT}
77+
78+
function bridge_constraint(
79+
::Type{CountBelongsToMILPBridge{T,F}},
80+
model::MOI.ModelLike,
81+
f::F,
82+
s::MOI.CountBelongs,
83+
) where {T,F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}}
84+
# !!! info
85+
# Postpone creation until final_touch.
86+
return CountBelongsToMILPBridge{T}(f, s)
87+
end
88+
89+
function MOI.supports_constraint(
90+
::Type{<:CountBelongsToMILPBridge{T}},
91+
::Type{<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}},
92+
::Type{MOI.CountBelongs},
93+
) where {T}
94+
return true
95+
end
96+
97+
function MOI.Bridges.added_constrained_variable_types(
98+
::Type{<:CountBelongsToMILPBridge},
99+
)
100+
return Tuple{Type}[(MOI.ZeroOne,)]
101+
end
102+
103+
function MOI.Bridges.added_constraint_types(
104+
::Type{<:CountBelongsToMILPBridge{T}},
105+
) where {T}
106+
return Tuple{Type,Type}[(MOI.ScalarAffineFunction{T}, MOI.EqualTo{T})]
107+
end
108+
109+
function concrete_bridge_type(
110+
::Type{<:CountBelongsToMILPBridge{T}},
111+
::Type{F},
112+
::Type{MOI.CountBelongs},
113+
) where {T,F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}}
114+
return CountBelongsToMILPBridge{T,F}
115+
end
116+
117+
function MOI.get(
118+
::MOI.ModelLike,
119+
::MOI.ConstraintFunction,
120+
bridge::CountBelongsToMILPBridge,
121+
)
122+
return bridge.f
123+
end
124+
125+
function MOI.get(
126+
::MOI.ModelLike,
127+
::MOI.ConstraintSet,
128+
bridge::CountBelongsToMILPBridge,
129+
)
130+
return bridge.set
131+
end
132+
133+
function MOI.delete(model::MOI.ModelLike, bridge::CountBelongsToMILPBridge)
134+
for ci in bridge.equal_to
135+
MOI.delete(model, ci)
136+
end
137+
empty!(bridge.equal_to)
138+
for x in bridge.variables
139+
MOI.delete(model, x)
140+
end
141+
empty!(bridge.variables)
142+
return
143+
end
144+
145+
function MOI.get(
146+
bridge::CountBelongsToMILPBridge,
147+
::MOI.NumberOfVariables,
148+
)::Int64
149+
return length(bridge.variables)
150+
end
151+
152+
function MOI.get(bridge::CountBelongsToMILPBridge, ::MOI.ListOfVariableIndices)
153+
return copy(bridge.variables)
154+
end
155+
156+
function MOI.get(
157+
bridge::CountBelongsToMILPBridge,
158+
::MOI.NumberOfConstraints{MOI.VariableIndex,MOI.ZeroOne},
159+
)::Int64
160+
return length(bridge.variables)
161+
end
162+
163+
function MOI.get(
164+
bridge::CountBelongsToMILPBridge,
165+
::MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.ZeroOne},
166+
)
167+
return MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}[
168+
MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}(x.value) for
169+
x in bridge.variables
170+
]
171+
end
172+
173+
function MOI.get(
174+
bridge::CountBelongsToMILPBridge{T},
175+
::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}},
176+
)::Int64 where {T}
177+
return length(bridge.equal_to)
178+
end
179+
180+
function MOI.get(
181+
bridge::CountBelongsToMILPBridge{T},
182+
::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}},
183+
) where {T}
184+
return copy(bridge.equal_to)
185+
end
186+
187+
MOI.Bridges.needs_final_touch(::CountBelongsToMILPBridge) = true
188+
189+
function _get_bounds(
190+
bridge::CountBelongsToMILPBridge{T},
191+
model::MOI.ModelLike,
192+
bounds::Dict{MOI.VariableIndex,Tuple{T,T}},
193+
f::MOI.ScalarAffineFunction{T},
194+
) where {T}
195+
lb = ub = f.constant
196+
for term in f.terms
197+
ret = _get_bounds(bridge, model, bounds, term.variable)
198+
if ret === nothing
199+
return nothing
200+
end
201+
lb += term.coefficient * ret[1]
202+
ub += term.coefficient * ret[2]
203+
end
204+
return lb, ub
205+
end
206+
207+
function _get_bounds(
208+
::CountBelongsToMILPBridge{T},
209+
model::MOI.ModelLike,
210+
bounds::Dict{MOI.VariableIndex,Tuple{T,T}},
211+
x::MOI.VariableIndex,
212+
) where {T}
213+
if haskey(bounds, x)
214+
return bounds[x]
215+
end
216+
ret = MOI.Utilities.get_bounds(model, T, x)
217+
if ret == (typemin(T), typemax(T))
218+
return nothing
219+
end
220+
bounds[x] = ret
221+
return ret
222+
end
223+
224+
"""
225+
_unit_expansion(
226+
::CountBelongsToMILPBridge{T},
227+
model::MOI.ModelLike,
228+
f::Vector{<:Union{MOI.VariableIndex,MOI.ScalarAffineFunction{T}}}
229+
) where {T}
230+
231+
Reformulates a vector of input functions ``f`` into a binary unit expansion.
232+
This is useful when writing constraint programming bridges.
233+
"""
234+
function _unit_expansion(
235+
bridge::CountBelongsToMILPBridge{T},
236+
model::MOI.ModelLike,
237+
f::Vector{<:Union{MOI.VariableIndex,MOI.ScalarAffineFunction{T}}},
238+
) where {T}
239+
S = Dict{T,Vector{MOI.VariableIndex}}()
240+
bounds = Dict{MOI.VariableIndex,Tuple{T,T}}()
241+
ci = MOI.ConstraintIndex{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}[]
242+
for i in 1:length(f)
243+
ret = _get_bounds(bridge, model, bounds, f[i])
244+
if ret === nothing
245+
BT = typeof(bridge)
246+
error(
247+
"Unable to use $BT because an element in the function has a " *
248+
"non-finite domain: $(f[i])",
249+
)
250+
end
251+
unit_f = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{T}[], zero(T))
252+
convex_f = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{T}[], zero(T))
253+
for xi in ret[1]:ret[2]
254+
z, _ = MOI.add_constrained_variable(model, MOI.ZeroOne())
255+
if !haskey(S, xi)
256+
S[xi] = MOI.VariableIndex[]
257+
end
258+
push!(S[xi], z)
259+
push!(unit_f.terms, MOI.ScalarAffineTerm(T(-xi), z))
260+
push!(convex_f.terms, MOI.ScalarAffineTerm(one(T), z))
261+
end
262+
push!(
263+
ci,
264+
MOI.Utilities.normalize_and_add_constraint(
265+
model,
266+
MOI.Utilities.operate(+, T, f[i], unit_f),
267+
MOI.EqualTo(zero(T));
268+
allow_modify_function = true,
269+
),
270+
)
271+
push!(ci, MOI.add_constraint(model, convex_f, MOI.EqualTo(one(T))))
272+
end
273+
return S, ci
274+
end
275+
276+
function MOI.Bridges.final_touch(
277+
bridge::CountBelongsToMILPBridge{T,F},
278+
model::MOI.ModelLike,
279+
) where {T,F}
280+
MOI.delete(model, bridge)
281+
scalars = collect(MOI.Utilities.eachscalar(bridge.f))
282+
S, ci = _unit_expansion(bridge, model, scalars[2:end])
283+
append!(bridge.equal_to, ci)
284+
for (_, s) in S
285+
append!(bridge.variables, s)
286+
end
287+
f = MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{T}[], zero(T))
288+
for s in bridge.set.set
289+
if haskey(S, s)
290+
for x in S[s]
291+
push!(f.terms, MOI.ScalarAffineTerm(one(T), x))
292+
end
293+
end
294+
end
295+
MOI.Utilities.operate!(-, T, f, scalars[1])
296+
push!(
297+
bridge.equal_to,
298+
MOI.Utilities.normalize_and_add_constraint(
299+
model,
300+
f,
301+
MOI.EqualTo(zero(T));
302+
allow_modify_function = true,
303+
),
304+
)
305+
return
306+
end

src/Bridges/Constraint/bridges/semi_to_binary.jl

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,19 @@ function bridge_constraint(
7575
f::MOI.VariableIndex,
7676
s::S,
7777
) where {T<:Real,S<:Union{MOI.Semicontinuous{T},MOI.Semiinteger{T}}}
78+
F = MOI.VariableIndex
79+
if MOI.is_valid(model, MOI.ConstraintIndex{F,MOI.GreaterThan{T}}(f.value))
80+
throw(MOI.LowerBoundAlreadySet{MOI.GreaterThan{T},S}(f))
81+
end
82+
if MOI.is_valid(model, MOI.ConstraintIndex{F,MOI.LessThan{T}}(f.value))
83+
throw(MOI.UpperBoundAlreadySet{MOI.LessThan{T},S}(f))
84+
end
85+
if MOI.is_valid(model, MOI.ConstraintIndex{F,MOI.EqualTo{T}}(f.value))
86+
throw(MOI.LowerBoundAlreadySet{MOI.EqualTo{T},S}(f))
87+
end
88+
if MOI.is_valid(model, MOI.ConstraintIndex{F,MOI.Interval{T}}(f.value))
89+
throw(MOI.LowerBoundAlreadySet{MOI.Interval{T},S}(f))
90+
end
7891
binary, binary_con = MOI.add_constrained_variable(model, MOI.ZeroOne())
7992
# var - LB * bin >= 0
8093
lb = MOI.Utilities.operate(*, T, -s.lower, binary)
@@ -327,3 +340,25 @@ function MOI.get(
327340
) where {T,S}
328341
return [b.lower_bound_index]
329342
end
343+
344+
MOI.Bridges.needs_final_touch(b::SemiToBinaryBridge) = true
345+
346+
function MOI.Bridges.final_touch(
347+
b::SemiToBinaryBridge{T,S},
348+
model::MOI.ModelLike,
349+
) where {T,S}
350+
F, f = MOI.VariableIndex, b.variable
351+
if MOI.is_valid(model, MOI.ConstraintIndex{F,MOI.GreaterThan{T}}(f.value))
352+
throw(MOI.LowerBoundAlreadySet{S,MOI.GreaterThan{T}}(f))
353+
end
354+
if MOI.is_valid(model, MOI.ConstraintIndex{F,MOI.LessThan{T}}(f.value))
355+
throw(MOI.UpperBoundAlreadySet{S,MOI.LessThan{T}}(f))
356+
end
357+
if MOI.is_valid(model, MOI.ConstraintIndex{F,MOI.EqualTo{T}}(f.value))
358+
throw(MOI.LowerBoundAlreadySet{S,MOI.EqualTo{T}}(f))
359+
end
360+
if MOI.is_valid(model, MOI.ConstraintIndex{F,MOI.Interval{T}}(f.value))
361+
throw(MOI.LowerBoundAlreadySet{S,MOI.Interval{T}}(f))
362+
end
363+
return
364+
end

src/Test/test_cpsat.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ function test_cpsat_CountBelongs(
107107
@requires _supports(config, MOI.optimize!)
108108
y = [MOI.add_constrained_variable(model, MOI.Integer()) for _ in 1:4]
109109
x = first.(y)
110+
MOI.add_constraint.(model, x, MOI.Interval(T(0), T(4)))
110111
set = Set([3, 4])
111112
MOI.add_constraint(
112113
model,

0 commit comments

Comments
 (0)