Skip to content

Commit f1541c5

Browse files
committed
Add constraint programming sets
1 parent 6303fe7 commit f1541c5

File tree

6 files changed

+329
-0
lines changed

6 files changed

+329
-0
lines changed

docs/src/reference/standard_form.md

+4
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,13 @@ Complements
101101
```@docs
102102
AllDifferent
103103
Among
104+
BinPacking
104105
CountAtLeast
105106
CountDistinct
106107
CountGreaterThan
108+
Circuit
109+
Cumulative
110+
Table
107111
```
108112

109113
## Matrix sets

src/Test/test_basic_constraint.jl

+14
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,16 @@ _set(::Type{MOI.CountDistinct}) = MOI.CountDistinct(4)
114114
_set(::Type{MOI.Among}) = MOI.Among(4, Set([3, 4]))
115115
_set(::Type{MOI.CountAtLeast}) = MOI.CountAtLeast(1, [2, 2], Set([3]))
116116
_set(::Type{MOI.CountGreaterThan}) = MOI.CountGreaterThan(5)
117+
_set(::Type{MOI.Circuit}) = MOI.Circuit(3)
118+
_set(::Type{MOI.Cumulative}) = MOI.Cumulative(7)
119+
120+
function _set(::Type{T}, ::Type{MOI.BinPacking}) where {T}
121+
return MOI.BinPacking(T(2), T[1, 2])
122+
end
123+
124+
function _set(::Type{T}, ::Type{MOI.Table}) where {T}
125+
return MOI.Table(T[0 1 1; 1 0 1; 1 1 0])
126+
end
117127

118128
function _set(
119129
::Type{MOI.Indicator{MOI.ACTIVATE_ON_ONE,MOI.LessThan{T}}},
@@ -282,6 +292,10 @@ for s in [
282292
:Among,
283293
:CountAtLeast,
284294
:CountGreaterThan,
295+
:BinPacking,
296+
:Circuit,
297+
:Cumulative,
298+
:Table,
285299
]
286300
S = getfield(MOI, s)
287301
functions = if S <: MOI.AbstractScalarSet

src/Test/test_cpsat.jl

+172
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,175 @@ function setup_test(
220220
)
221221
return
222222
end
223+
224+
"""
225+
test_cpsat_BinPacking(model::MOI.ModelLike, config::Config)
226+
227+
Add a VectorOfVariables-in-BinPacking constraint.
228+
"""
229+
function test_cpsat_BinPacking(
230+
model::MOI.ModelLike,
231+
config::Config{T},
232+
) where {T}
233+
@requires MOI.supports_constraint(
234+
model,
235+
MOI.VectorOfVariables,
236+
MOI.BinPacking{T},
237+
)
238+
@requires MOI.supports_add_constrained_variable(model, MOI.Integer)
239+
@requires _supports(config, MOI.optimize!)
240+
x = [MOI.add_constrained_variable(model, MOI.Integer())[1] for _ in 1:3]
241+
MOI.add_constraint(
242+
model,
243+
MOI.VectorOfVariables(x),
244+
MOI.BinPacking(T(2), T[1, 2]),
245+
)
246+
MOI.optimize!(model)
247+
x_val = round.(Int, MOI.get.(model, MOI.VariablePrimal(), x))
248+
@test 1 * x_val[1] + 2 * x_val[2] <= T(2)
249+
return
250+
end
251+
252+
function setup_test(
253+
::typeof(test_cpsat_BinPacking),
254+
model::MOIU.MockOptimizer,
255+
::Config{T},
256+
) where {T}
257+
MOIU.set_mock_optimize!(
258+
model,
259+
(mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
260+
mock,
261+
MOI.OPTIMAL,
262+
(MOI.FEASIBLE_POINT, T[2, 0]),
263+
),
264+
)
265+
return
266+
end
267+
268+
"""
269+
test_cpsat_Cumulative(model::MOI.ModelLike, config::Config)
270+
271+
Add a VectorOfVariables-in-Cumulative constraint.
272+
"""
273+
function test_cpsat_Cumulative(
274+
model::MOI.ModelLike,
275+
config::Config{T},
276+
) where {T}
277+
@requires MOI.supports_constraint(
278+
model,
279+
MOI.VectorOfVariables,
280+
MOI.Cumulative,
281+
)
282+
@requires MOI.supports_add_constrained_variable(model, MOI.Integer)
283+
@requires _supports(config, MOI.optimize!)
284+
s = [MOI.add_constrained_variable(model, MOI.Integer())[1] for _ in 1:3]
285+
d = [MOI.add_constrained_variable(model, MOI.Integer())[1] for _ in 1:3]
286+
r = [MOI.add_constrained_variable(model, MOI.Integer())[1] for _ in 1:3]
287+
b, _ = MOI.add_constrained_variable(model, MOI.Integer())
288+
MOI.add_constraint(
289+
model,
290+
MOI.VectorOfVariables([s; d; r; b]),
291+
MOI.Cumulative(10),
292+
)
293+
MOI.optimize!(model)
294+
s_val = round.(Int, MOI.get.(model, MOI.VariablePrimal(), s))
295+
d_val = round.(Int, MOI.get.(model, MOI.VariablePrimal(), d))
296+
r_val = round.(Int, MOI.get.(model, MOI.VariablePrimal(), r))
297+
b_val = round(Int, MOI.get(model, MOI.VariablePrimal(), b))
298+
times = zeros(maximum(s_val) + maximum(d_val))
299+
for i in 1:3
300+
for j in 0:(d_val[i]-1)
301+
t = s_val[i] + j
302+
times[t] += r_val[i]
303+
end
304+
end
305+
@test all(times .<= b_val)
306+
return
307+
end
308+
309+
function setup_test(
310+
::typeof(test_cpsat_Cumulative),
311+
model::MOIU.MockOptimizer,
312+
::Config{T},
313+
) where {T}
314+
MOIU.set_mock_optimize!(
315+
model,
316+
(mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
317+
mock,
318+
MOI.OPTIMAL,
319+
(MOI.FEASIBLE_POINT, T[0, 1, 2, 2, 2, 2, 3, 2, 1, 5]),
320+
),
321+
)
322+
return
323+
end
324+
325+
"""
326+
test_cpsat_Table(model::MOI.ModelLike, config::Config)
327+
328+
Add a VectorOfVariables-in-Table constraint.
329+
"""
330+
function test_cpsat_Table(model::MOI.ModelLike, config::Config{T}) where {T}
331+
@requires MOI.supports_constraint(
332+
model,
333+
MOI.VectorOfVariables,
334+
MOI.Table{T},
335+
)
336+
@requires MOI.supports_add_constrained_variable(model, MOI.Integer)
337+
@requires _supports(config, MOI.optimize!)
338+
x = [MOI.add_constrained_variable(model, MOI.Integer())[1] for _ in 1:3]
339+
table = T[1 1 0; 0 1 1]
340+
MOI.add_constraint(model, MOI.VectorOfVariables(x), MOI.Table(table))
341+
MOI.optimize!(model)
342+
x_val = round.(Int, MOI.get.(model, MOI.VariablePrimal(), x))
343+
@test x_val == [1, 1, 0] || x_val == [0, 1, 1]
344+
return
345+
end
346+
347+
function setup_test(
348+
::typeof(test_cpsat_Table),
349+
model::MOIU.MockOptimizer,
350+
::Config{T},
351+
) where {T}
352+
MOIU.set_mock_optimize!(
353+
model,
354+
(mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
355+
mock,
356+
MOI.OPTIMAL,
357+
(MOI.FEASIBLE_POINT, T[1, 1, 0]),
358+
),
359+
)
360+
return
361+
end
362+
363+
"""
364+
test_cpsat_Circuit(model::MOI.ModelLike, config::Config)
365+
366+
Add a VectorOfVariables-in-Circuit constraint.
367+
"""
368+
function test_cpsat_Circuit(model::MOI.ModelLike, config::Config{T}) where {T}
369+
@requires MOI.supports_constraint(model, MOI.VectorOfVariables, MOI.Circuit)
370+
@requires MOI.supports_add_constrained_variable(model, MOI.Integer)
371+
@requires _supports(config, MOI.optimize!)
372+
x = [MOI.add_constrained_variable(model, MOI.Integer())[1] for _ in 1:3]
373+
MOI.add_constraint(model, MOI.VectorOfVariables(x), MOI.Circuit(3))
374+
MOI.optimize!(model)
375+
x_val = round.(Int, MOI.get.(model, MOI.VariablePrimal(), x))
376+
@test x_val == [3, 1, 2] || x_val == [2, 3, 1]
377+
return
378+
end
379+
380+
function setup_test(
381+
::typeof(test_cpsat_Circuit),
382+
model::MOIU.MockOptimizer,
383+
::Config{T},
384+
) where {T}
385+
MOIU.set_mock_optimize!(
386+
model,
387+
(mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
388+
mock,
389+
MOI.OPTIMAL,
390+
(MOI.FEASIBLE_POINT, T[3, 1, 2]),
391+
),
392+
)
393+
return
394+
end

src/Utilities/model.jl

+4
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,8 @@ const LessThanIndicatorZero{T} =
792792
MOI.Among,
793793
MOI.CountAtLeast,
794794
MOI.CountGreaterThan,
795+
MOI.Circuit,
796+
MOI.Cumulative,
795797
),
796798
(
797799
MOI.PowerCone,
@@ -800,6 +802,8 @@ const LessThanIndicatorZero{T} =
800802
MOI.SOS2,
801803
LessThanIndicatorOne,
802804
LessThanIndicatorZero,
805+
MOI.Table,
806+
MOI.BinPacking,
803807
),
804808
(),
805809
(MOI.ScalarAffineFunction, MOI.ScalarQuadraticFunction),

src/sets.jl

+127
Original file line numberDiff line numberDiff line change
@@ -1297,6 +1297,131 @@ struct CountGreaterThan <: AbstractVectorSet
12971297
end
12981298
end
12991299

1300+
"""
1301+
BinPacking(capacity::T, weights::Vector{T}) where {T}
1302+
1303+
The set ``\\{x \\in \\mathbb{R}^d\\}`` such that
1304+
``\\sum\\limits_{i=1}^d w_i x_i \\le c``.
1305+
1306+
This constraint is sometimes called `bin_packing`.
1307+
1308+
## Example
1309+
1310+
```julia
1311+
model = Utilities.Model{Float64}()
1312+
x = [add_constrained_variable(model, MOI.Integer())[1] for _ in 1:3]
1313+
add_constraint(model, VectorOfVariables(x), BinPacking(0.5, rand(3)))
1314+
```
1315+
"""
1316+
struct BinPacking{T} <: AbstractVectorSet
1317+
capacity::T
1318+
weights::Vector{T}
1319+
function BinPacking(capacity::T, weights::Vector{T}) where {T}
1320+
if capacity < zero(T)
1321+
throw(DomainError(capacity, "capacity must be non-negative"))
1322+
end
1323+
if any(w -> w < zero(T), weights)
1324+
throw(DomainError(weights, "weights must be non-negative"))
1325+
end
1326+
return new{T}(capacity, weights)
1327+
end
1328+
end
1329+
1330+
dimension(set::BinPacking) = length(set.weights)
1331+
1332+
function Base.copy(set::BinPacking{T}) where {T}
1333+
return BinPacking{T}(set.capacity, copy(set.weights))
1334+
end
1335+
1336+
function Base.:(==)(x::BinPacking{T}, y::BinPacking{T}) where {T}
1337+
return x.capacity == y.capacity && x.weights == y.weights
1338+
end
1339+
1340+
"""
1341+
Cumulative(dimension::Int)
1342+
1343+
The set ``\\{(s, d, r, b) \\in \\mathbb{R}^{s+d+r+1}\\}`` representing the
1344+
``cumulative`` global constraint. It requires that a set of tasks given by start
1345+
times ``s``, durations ``d``, and resource requirements ``r``, never requires
1346+
more than the global resource bound ``b`` at any one time.
1347+
1348+
This constraint is sometimes called `cumulative`.
1349+
1350+
## Example
1351+
1352+
```julia
1353+
model = Utilities.Model{Float64}()
1354+
s = [add_constrained_variable(model, MOI.Integer())[1] for _ in 1:3]
1355+
d = [add_constrained_variable(model, MOI.Integer())[1] for _ in 1:3]
1356+
r = [add_constrained_variable(model, MOI.Integer())[1] for _ in 1:3]
1357+
b, _ = add_constrained_variable(model, MOI.Integer())
1358+
add_constraint(model, VectorOfVariables([s; d; r; b]), Cumulative(10))
1359+
```
1360+
"""
1361+
struct Cumulative <: AbstractVectorSet
1362+
dimension::Int
1363+
function Cumulative(dimension::Base.Integer)
1364+
if dimension < 1
1365+
throw(DimensionMismatch("Dimension of Cumulative must be >= 1."))
1366+
end
1367+
return new(dimension)
1368+
end
1369+
end
1370+
1371+
"""
1372+
Table(table::Matrix{T}) where {T}
1373+
1374+
The set ``\\{x \\in \\mathbb{R}^d\\}`` such that `x` belongs to one row of
1375+
`table`. That is, there exists some `j` in `1:size(table, 1)`, such that
1376+
`x[i] = table[j, i]`.
1377+
1378+
This constraint is sometimes called `table`.
1379+
1380+
## Example
1381+
1382+
```julia
1383+
model = Utilities.Model{Float64}()
1384+
x = [add_constrained_variable(model, MOI.Integer())[1] for _ in 1:3]
1385+
table = [1 1 0; 0 1 1; 1 0 1; 1 1 1]
1386+
add_constraint(model, VectorOfVariables(x), Table(table))
1387+
```
1388+
"""
1389+
struct Table{T} <: AbstractVectorSet
1390+
table::Matrix{T}
1391+
end
1392+
1393+
dimension(set::Table) = size(set.table, 2)
1394+
1395+
Base.copy(set::Table) = Table(copy(set.table))
1396+
1397+
Base.:(==)(x::Table{T}, y::Table{T}) where {T} = x.table == y.table
1398+
1399+
"""
1400+
Circuit(dimension::Int)
1401+
1402+
The set ``\\{x \\in \\mathbb{R}^d\\}`` that constraints ``x`` to be a circuit,
1403+
such that ``x_i = j`` means that ``j`` is the successor of ``i``.
1404+
1405+
This constraint is sometimes called `circuit`.
1406+
1407+
## Example
1408+
1409+
```julia
1410+
model = Utilities.Model{Float64}()
1411+
x = [add_constrained_variable(model, MOI.Integer())[1] for _ in 1:3]
1412+
add_constraint(model, VectorOfVariables(x), Circuit(3))
1413+
```
1414+
"""
1415+
struct Circuit <: AbstractVectorSet
1416+
dimension::Int
1417+
function Circuit(dimension::Base.Integer)
1418+
if dimension < 0
1419+
throw(DimensionMismatch("Dimension of Circuit must be >= 0."))
1420+
end
1421+
return new(dimension)
1422+
end
1423+
end
1424+
13001425
# isbits types, nothing to copy
13011426
function Base.copy(
13021427
set::Union{
@@ -1336,6 +1461,8 @@ function Base.copy(
13361461
Among,
13371462
CountAtLeast,
13381463
CountGreaterThan,
1464+
Circuit,
1465+
Cumulative,
13391466
},
13401467
)
13411468
return set

test/sets.jl

+8
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ function test_sets_dimension()
120120
@test MOI.dimension(MOI.Complements(10)) == 10
121121
end
122122

123+
function test_sets_bin_packing_errors()
124+
@test_throws DomainError MOI.BinPacking(-1.0, [1.0, 2.0])
125+
@test_throws DomainError MOI.BinPacking(1.0, [1.0, -2.0])
126+
return
127+
end
128+
123129
function test_sets_DimensionMismatch()
124130
for (S, min_dimension) in (
125131
(MOI.Reals, 0),
@@ -142,6 +148,8 @@ function test_sets_DimensionMismatch()
142148
(MOI.AllDifferent, 0),
143149
(MOI.CountDistinct, 1),
144150
(MOI.CountGreaterThan, 2),
151+
(MOI.Cumulative, 1),
152+
(MOI.Circuit, 0),
145153
)
146154
@test_throws DimensionMismatch S(min_dimension - 1)
147155
@test S(min_dimension) isa S

0 commit comments

Comments
 (0)