Skip to content

Commit a3db81d

Browse files
committed
Add CountAtLeast set
1 parent 9caca63 commit a3db81d

File tree

6 files changed

+106
-0
lines changed

6 files changed

+106
-0
lines changed

docs/src/reference/standard_form.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ Complements
101101
```@docs
102102
AllDifferent
103103
Among
104+
CountAtLeast
104105
CountDistinct
105106
```
106107

src/Test/test_basic_constraint.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ _set(::Type{MOI.Complements}) = MOI.Complements(2)
112112
_set(::Type{MOI.AllDifferent}) = MOI.AllDifferent(3)
113113
_set(::Type{MOI.CountDistinct}) = MOI.CountDistinct(4)
114114
_set(::Type{MOI.Among}) = MOI.Among(4, Set([3, 4]))
115+
_set(::Type{MOI.CountAtLeast}) = MOI.CountAtLeast(1, [2, 2], Set([3]))
115116

116117
function _set(
117118
::Type{MOI.Indicator{MOI.ACTIVATE_ON_ONE,MOI.LessThan{T}}},
@@ -278,6 +279,7 @@ for s in [
278279
:AllDifferent,
279280
:CountDistinct,
280281
:Among,
282+
:CountAtLeast,
281283
]
282284
S = getfield(MOI, s)
283285
functions = if S <: MOI.AbstractScalarSet

src/Test/test_cpsat.jl

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,53 @@ function setup_test(
122122
)
123123
return
124124
end
125+
126+
"""
127+
test_cpsat_CountAtLeast(model::MOI.ModelLike, config::Config)
128+
129+
Add a VectorOfVariables-in-CountAtLeast constraint.
130+
"""
131+
function test_cpsat_CountAtLeast(
132+
model::MOI.ModelLike,
133+
config::Config{T},
134+
) where {T}
135+
@requires MOI.supports_constraint(
136+
model,
137+
MOI.VectorOfVariables,
138+
MOI.CountAtLeast,
139+
)
140+
@requires MOI.supports_add_constrained_variable(model, MOI.Integer)
141+
@requires _supports(config, MOI.optimize!)
142+
x, _ = MOI.add_constrained_variable(model, MOI.Integer())
143+
y, _ = MOI.add_constrained_variable(model, MOI.Integer())
144+
z, _ = MOI.add_constrained_variable(model, MOI.Integer())
145+
variables = [x, y, y, z]
146+
partitions = [2, 2]
147+
set = Set([3])
148+
MOI.add_constraint(
149+
model,
150+
MOI.VectorOfVariables(variables),
151+
MOI.CountAtLeast(1, partitions, set),
152+
)
153+
MOI.optimize!(model)
154+
x_val = round.(Int, MOI.get.(model, MOI.VariablePrimal(), [x, y, z]))
155+
@test x_val[1] == 3 || x_val[2] == 3
156+
@test x_val[2] == 3 || x_val[3] == 3
157+
return
158+
end
159+
160+
function setup_test(
161+
::typeof(test_cpsat_CountAtLeast),
162+
model::MOIU.MockOptimizer,
163+
::Config{T},
164+
) where {T}
165+
MOIU.set_mock_optimize!(
166+
model,
167+
(mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(
168+
mock,
169+
MOI.OPTIMAL,
170+
(MOI.FEASIBLE_POINT, T[0, 3, 0]),
171+
),
172+
)
173+
return
174+
end

src/Utilities/model.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,7 @@ const LessThanIndicatorZero{T} =
790790
MOI.AllDifferent,
791791
MOI.CountDistinct,
792792
MOI.Among,
793+
MOI.CountAtLeast,
793794
),
794795
(
795796
MOI.PowerCone,

src/sets.jl

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,52 @@ end
12161216

12171217
Base.:(==)(x::Among, y::Among) = x.dimension == y.dimension && x.set == y.set
12181218

1219+
"""
1220+
CountAtLeast(n::Int, d::Vector{Int}, set::Set{Int})
1221+
1222+
The set ``\\{x \\in \\mathbb{Z}^{d_1 + d_2 + \\ldots d_N}\\}``, where `x` is
1223+
partitioned into `N` subsets (``\\{x_1, \\ldots, x_{d_1}\\}``,
1224+
``\\{x_{d_1, 1}, \\ldots, x_{d_1 + d_2}\\}`` and so on), and at least ``n``
1225+
elements of each subset take one of the values in `set`.
1226+
1227+
## Also known as
1228+
1229+
This constraint is called `at_least` in MiniZinc.
1230+
1231+
## Example
1232+
1233+
```julia
1234+
model = Utilities.Model{Float64}()
1235+
a, _ = add_constrained_variable(model, Integer())
1236+
b, _ = add_constrained_variable(model, Integer())
1237+
c, _ = add_constrained_variable(model, Integer())
1238+
# To ensure that `3` appears at least once in each of the subsets {a, b}, {b, c}
1239+
x, d, set = [a, b, b, c], [2, 2], [3]
1240+
add_constraint(model, VectorOfVariables(x), CountAtLeast(1, d, Set(set)))
1241+
```
1242+
"""
1243+
struct CountAtLeast <: AbstractVectorSet
1244+
n::Int
1245+
partitions::Vector{Int}
1246+
set::Set{Int}
1247+
function CountAtLeast(
1248+
n::Base.Integer,
1249+
partitions::Vector{Int},
1250+
set::Set{Int},
1251+
)
1252+
if any(p <= 0 for p in partitions)
1253+
throw(DimensionMismatch("Invalid partition dimension."))
1254+
end
1255+
return new(n, partitions, set)
1256+
end
1257+
end
1258+
1259+
dimension(s::CountAtLeast) = sum(s.partitions)
1260+
1261+
function Base.:(==)(x::CountAtLeast, y::CountAtLeast)
1262+
return x.n == y.n && x.partitions == y.partitions && x.set == y.set
1263+
end
1264+
12191265
# isbits types, nothing to copy
12201266
function Base.copy(
12211267
set::Union{
@@ -1253,13 +1299,18 @@ function Base.copy(
12531299
AllDifferent,
12541300
CountDistinct,
12551301
Among,
1302+
CountAtLeast,
12561303
},
12571304
)
12581305
return set
12591306
end
12601307
Base.copy(set::S) where {S<:Union{SOS1,SOS2}} = S(copy(set.weights))
12611308
Base.copy(set::Among) = Among(set.dimension, copy(set.set))
12621309

1310+
function Base.copy(set::CountAtLeast)
1311+
return CountAtLeast(set.n, copy(set.partitions), copy(set.set))
1312+
end
1313+
12631314
"""
12641315
supports_dimension_update(S::Type{<:MOI.AbstractVectorSet})
12651316

test/sets.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ function test_sets_DimensionMismatch()
156156
@test_throws DimensionMismatch MOI.Complements(-3)
157157
@test_throws DimensionMismatch MOI.Complements(3)
158158
@test_throws DimensionMismatch MOI.Among(0, Set([1, 2]))
159+
@test_throws DimensionMismatch MOI.CountAtLeast(1, [-1, 2], Set([1, 2]))
159160
return
160161
end
161162

0 commit comments

Comments
 (0)