Skip to content

Commit 9e5ed56

Browse files
authored
Feature VectorAffineFuncton in AllDifferent/Table (#268)
* allow VAF in AllDifferenSet/TableSet
1 parent 80a6050 commit 9e5ed56

File tree

7 files changed

+220
-5
lines changed

7 files changed

+220
-5
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
- Allow variables as constraint like `a || !b` instead of `a == 1 || b == 0`. [PR #267](https://github.com/Wikunia/ConstraintSolver.jl/pull/267)
55
- **Attention** Does not check if variable is a binary variable
66
- Support for indicator/reified in indicator/reified (without bridges) [PR #251](https://github.com/Wikunia/ConstraintSolver.jl/pull/251)
7-
7+
- Support for VectorAffineFunction in TableSet/AllDifferentSet
8+
- i.e `[x[i]+i for i in 1:n] in CS.AllDifferentSet()`
9+
- `[x,y,10] in CS.TableSet(...)`
10+
- see [issue #235](https://github.com/Wikunia/ConstraintSolver.jl/issues/235) for in-depth examples
11+
812
## v0.6.9 (17th of July 2021)
913
- set activator to false when inner violated [PR #266](https://github.com/Wikunia/ConstraintSolver.jl/pull/266)
1014

docs/src/supported.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@ The following list shows constraints that are implemented and those which are pl
8282
- [X] `>`
8383
- [X] All different
8484
- `@constraint(m, [x,y,z] in CS.AllDifferentSet())`
85+
- `@constraint(m, [x,y+2,x+y] in CS.AllDifferentSet())`
8586
- [X] `TableSet` constraint [#130](https://github.com/Wikunia/ConstraintSolver.jl/pull/130)
87+
- also with `VectorAffineFunction` i.e `[x,y+2,x+y] in CS.TableSet(...)`
8688
- Indicator constraints [#167](https://github.com/Wikunia/ConstraintSolver.jl/pull/167)
8789
- i.e `@constraint(m, b => {x + y >= 12})`
8890
- [X] for affine inner constraints
@@ -91,6 +93,8 @@ The following list shows constraints that are implemented and those which are pl
9193
- [X] Allow `||` inside the inner constraint i.e `@constraint(m, b => {x + y >= 12 || 2x + y <= 7})`
9294
- [-] Have `Indicator` and `Reified` inside one another
9395
- this is only supported for simple cases i.e bridge support is missing
96+
- [ ] `VectorAffineFunction` in `AllDifferentSet` or `TableSet`
97+
- Please open an issue if needed for a problem. Probably not too hard to add
9498
- Reified constraints [#171](https://github.com/Wikunia/ConstraintSolver.jl/pull/171)
9599
- i.e `@constraint(m, b := {x + y >= 12})`
96100
- [X] for everything that is supported by indicator constraints

src/MOI_wrapper/constraints.jl

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,24 @@ MOI.supports_constraint(
4242
::Type{AllDifferentSetInternal},
4343
) = true
4444

45+
MOI.supports_constraint(
46+
::Optimizer,
47+
::Type{VAF{T}},
48+
::Type{AllDifferentSetInternal},
49+
) where {T <: Real} = true
50+
4551
MOI.supports_constraint(
4652
::Optimizer,
4753
::Type{MOI.VectorOfVariables},
4854
::Type{TableSetInternal},
4955
) = true
5056

57+
MOI.supports_constraint(
58+
::Optimizer,
59+
::Type{VAF{T}},
60+
::Type{TableSetInternal},
61+
) where {T <: Real} = true
62+
5163
MOI.supports_constraint(
5264
::Optimizer,
5365
::Type{SAF{T}},
@@ -80,13 +92,17 @@ function MOI.supports_constraint(
8092
return A == MOI.ACTIVATE_ON_ONE || A == MOI.ACTIVATE_ON_ZERO
8193
end
8294

95+
supports_inner_constraint(optimizer::Optimizer, func, set) = MOI.supports_constraint(optimizer, func, set)
96+
supports_inner_constraint(optimizer::Optimizer, func::Type{VAF{T}}, ::Type{AllDifferentSetInternal}) where T = false
97+
supports_inner_constraint(optimizer::Optimizer, func::Type{VAF{T}}, ::Type{TableSetInternal}) where T = false
98+
8399
function MOI.supports_constraint(
84100
optimizer::Optimizer,
85101
func::Type{<:MOI.AbstractFunction},
86102
set::Type{OS},
87103
) where {A,F,IS,OS<:CS.IndicatorSet{A,F,IS}}
88104
!(A == MOI.ACTIVATE_ON_ONE || A == MOI.ACTIVATE_ON_ZERO) && return false
89-
return MOI.supports_constraint(optimizer, F, IS)
105+
return supports_inner_constraint(optimizer, F, IS)
90106
end
91107

92108
function MOI.supports_constraint(
@@ -95,7 +111,7 @@ function MOI.supports_constraint(
95111
set::Type{OS}
96112
) where {A,F,IS,OS<:CS.ReifiedSet{A,F,IS}}
97113
!(A == MOI.ACTIVATE_ON_ONE || A == MOI.ACTIVATE_ON_ZERO) && return false
98-
return MOI.supports_constraint(optimizer, F, IS)
114+
return supports_inner_constraint(optimizer, F, IS)
99115
end
100116

101117
function MOI.supports_constraint(
@@ -290,6 +306,47 @@ function MOI.add_constraint(
290306
return MOI.ConstraintIndex{MOI.VectorOfVariables,typeof(set)}(length(com.constraints))
291307
end
292308

309+
"""
310+
function MOI.add_constraint(
311+
model::Optimizer,
312+
vaf::VAF{T},
313+
set::Union{AllDifferentSetInternal, TableSetInternal},
314+
) where T
315+
316+
`VectorAffineFunction` constraint for `AllDifferentSet` or `TableSet` to support for things like
317+
`[a+1, b+2, c+3] in CS.AllDifferentSet()` or
318+
`[a, b, 4] in TableSet(...)`
319+
"""
320+
function MOI.add_constraint(
321+
model::Optimizer,
322+
vaf::VAF{T},
323+
set::Union{AllDifferentSetInternal, TableSetInternal},
324+
) where T
325+
fs = MOIU.eachscalar(vaf)
326+
variables = Vector{MOI.VariableIndex}(undef, length(fs))
327+
for (i,f) in enumerate(fs)
328+
# we need to create a new variable and SAF constraint when it's not a SVF
329+
if !is_svf(f)
330+
discrete, non_continuous_value = is_discrete_saf(f)
331+
if !discrete
332+
throw(DomainError(non_continuous_value, "The constant and all coefficients need to be discrete"))
333+
end
334+
vidx = MOI.add_variable(model)
335+
variables[i] = vidx
336+
min_val, max_val = get_extrema(model, f)
337+
svf = MOI.SingleVariable(vidx)
338+
MOI.add_constraint(model, svf, MOI.Integer())
339+
MOI.add_constraint(model, svf, MOI.Interval(min_val, max_val))
340+
new_constraint_fct = MOIU.operate(-, T, f, svf)
341+
MOI.add_constraint(model, new_constraint_fct, MOI.EqualTo(0.0))
342+
else
343+
variables[i] = f.terms[1].variable_index
344+
end
345+
end
346+
ci = MOI.add_constraint(model, MOI.VectorOfVariables(variables), set)
347+
return MOI.ConstraintIndex{VAF{T},typeof(set)}(ci.value)
348+
end
349+
293350
"""
294351
MOI.add_constraint(
295352
model::Optimizer,
@@ -326,10 +383,10 @@ function MOI.add_constraint(
326383

327384
if length(func.terms) == 1
328385
vidx = func.terms[1].variable_index.value
329-
val = convert(Int, set.value / func.terms[1].coefficient)
386+
val = convert(Int, (set.value - func.constant) / func.terms[1].coefficient)
330387
push!(model.inner.init_fixes, (vidx, val))
331388
return MOI.ConstraintIndex{SAF{T},MOI.EqualTo{T}}(0)
332-
elseif length(func.terms) == 2 && set.value == zero(T)
389+
elseif length(func.terms) == 2 && set.value == zero(T) && func.constant == zero(T)
333390
if func.terms[1].coefficient == -func.terms[2].coefficient
334391
# we have the form a == b
335392
vecOfvar = MOI.VectorOfVariables([

src/MOI_wrapper/util.jl

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,41 @@ end
9898

9999
function get_activator_internals(A, indices)
100100
ActivatorConstraintInternals(A, indices[1] in indices[2:end], false, 0)
101+
end
102+
103+
"""
104+
saf_is_svf(saf::MOI.ScalarAffineFunction)
105+
106+
Checks if a `ScalarAffineFunction` can be represented as a `SingleVariable`.
107+
This can be used for example when having a `VectorAffineFunction` in `AllDifferentSet` constraint when the decision
108+
has to be made whether a new constraint + variable has to be created.
109+
"""
110+
function is_svf(saf::MOI.ScalarAffineFunction)
111+
!iszero(saf.constant) && return false
112+
length(saf.terms) != 1 && return false
113+
!isone(saf.terms[1].coefficient) && return false
114+
return true
115+
end
116+
117+
function get_extrema(model::Optimizer, saf::MOI.ScalarAffineFunction{T}) where T
118+
min_val = saf.constant
119+
max_val = saf.constant
120+
for term in saf.terms
121+
if term.coefficient < 0
122+
min_val += term.coefficient*model.variable_info[term.variable_index.value].upper_bound
123+
max_val += term.coefficient*model.variable_info[term.variable_index.value].lower_bound
124+
else
125+
min_val += term.coefficient*model.variable_info[term.variable_index.value].lower_bound
126+
max_val += term.coefficient*model.variable_info[term.variable_index.value].upper_bound
127+
end
128+
end
129+
return min_val, max_val
130+
end
131+
132+
function is_discrete_saf(saf::MOI.ScalarAffineFunction{T}) where T
133+
!isapprox(saf.constant, round(saf.constant)) && return false, saf.constant
134+
for term in saf.terms
135+
!isapprox(term.coefficient, round(term.coefficient)) && return false, term.coefficient
136+
end
137+
return true, 0
101138
end

test/moi.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@
5252
indicator_set = MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(MOI.EqualTo(9.0))
5353
@test MOI.supports_constraint(optimizer, typeof(f), typeof(indicator_set))
5454

55+
indicator_set = CS.IndicatorSet{MOI.ACTIVATE_ON_ONE, typeof(f)}(CS.AllDifferentSetInternal(2))
56+
@test !MOI.supports_constraint(optimizer, typeof(f), typeof(indicator_set))
57+
indicator_set = CS.IndicatorSet{MOI.ACTIVATE_ON_ZERO, typeof(f)}(CS.TableSetInternal(2, [1 2; ]))
58+
@test !MOI.supports_constraint(optimizer, typeof(f), typeof(indicator_set))
59+
5560
@test MOI.supports_constraint(
5661
optimizer,
5762
typeof(f),

test/unit/constraints/alldifferent.jl

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,40 @@ end
199199
@test CS.fix!(com, variables[2], 2; check_feasibility = false)
200200
@test !CS.is_constraint_violated(com, constraint, constraint.fct, constraint.set)
201201
end
202+
203+
@testset "all 8queens solutions" begin
204+
n = 8
205+
model = Model(optimizer_with_attributes(CS.Optimizer, "all_optimal_solutions"=>true, "logging"=>[]))
206+
207+
@variable(model, 1 <= x[1:n] <= n, Int)
208+
@constraint(model, x in CS.AllDifferentSet())
209+
@constraint(model, [x[i] + i for i in 1:n] in CS.AllDifferentSet())
210+
@constraint(model, [x[i] - i for i in 1:n] in CS.AllDifferentSet())
211+
212+
optimize!(model)
213+
214+
status = JuMP.termination_status(model)
215+
216+
@test status == MOI.OPTIMAL
217+
num_sols = MOI.get(model, MOI.ResultCount())
218+
@test num_sols == 92
219+
for sol in 1:num_sols
220+
x_val = convert.(Integer,JuMP.value.(x; result=sol))
221+
@test allunique(x_val)
222+
@test allunique([x_val[i] + i for i in 1:n])
223+
@test allunique([x_val[i] - i for i in 1:n])
224+
end
225+
end
226+
227+
@testset "domainerror due to term not discrete " begin
228+
n = 8
229+
model = Model(optimizer_with_attributes(CS.Optimizer, "all_optimal_solutions"=>true, "logging"=>[]))
230+
231+
@variable(model, 1 <= x[1:n] <= n, Int)
232+
@constraint(model, x in CS.AllDifferentSet())
233+
# 1.5 not allowed
234+
@constraint(model, [1.5*x[i] + i for i in 1:n] in CS.AllDifferentSet())
235+
@constraint(model, [x[i] - i for i in 1:n] in CS.AllDifferentSet())
236+
237+
@test_throws DomainError optimize!(model)
238+
end

test/unit/constraints/table.jl

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,74 @@ end
162162
@test CS.fix!(com, variables[constraint.indices[2]], 2; check_feasibility = false)
163163
@test !CS.is_constraint_violated(com, constraint, constraint.fct, constraint.set)
164164
end
165+
166+
167+
@testset "all modulo" begin
168+
function modulo(m, x, y, z)
169+
lbx = !(x isa Integer) ? round(Int, JuMP.lower_bound(x)) : x
170+
ubx = !(x isa Integer) ? round(Int, JuMP.upper_bound(x)) : x
171+
lby = !(y isa Integer) ? round(Int, JuMP.lower_bound(y)) : y
172+
uby = !(y isa Integer) ? round(Int, JuMP.upper_bound(y)) : y
173+
174+
table = transpose(reduce(hcat,[ [i,j,i % j] for i in lbx:ubx, j in lby:uby if j != 0]))
175+
@constraint(m, [x, y, z] in CS.TableSet(table))
176+
end
177+
178+
m = Model(optimizer_with_attributes(CS.Optimizer, "logging" => [], "all_solutions" => true))
179+
@variable(m, 1 <= x[1:2] <= 10, Int)
180+
modulo(m,x[1],2,x[2])
181+
optimize!(m)
182+
183+
@test JuMP.termination_status(m) == MOI.OPTIMAL
184+
nresults = JuMP.result_count(m)
185+
results = Set{Tuple{Int,Int}}()
186+
for i in 1:nresults
187+
xv = convert.(Int, round.(JuMP.value.(x; result=i)))
188+
@test xv[1] % 2 == xv[2]
189+
push!(results, (xv[1], xv[2]))
190+
end
191+
192+
found_nr = 0
193+
for i in 1:10, j in 1:10
194+
if i % 2 == j
195+
@test (i,j) in results
196+
found_nr += 1
197+
end
198+
end
199+
@test found_nr == nresults
200+
end
201+
202+
@testset "vector affine" begin
203+
function modulo_affine(m, x, y, z)
204+
lbx = !(x isa Integer) ? round(Int, JuMP.lower_bound(x)) : x
205+
ubx = !(x isa Integer) ? round(Int, JuMP.upper_bound(x)) : x
206+
lby = !(y isa Integer) ? round(Int, JuMP.lower_bound(y)) : y
207+
uby = !(y isa Integer) ? round(Int, JuMP.upper_bound(y)) : y
208+
209+
table = transpose(reduce(hcat,[ [i,j,i % j] for i in lbx:2*ubx, j in lby:uby if j != 0]))
210+
@constraint(m, [x+z, y, 2x-z] in CS.TableSet(table))
211+
end
212+
213+
m = Model(optimizer_with_attributes(CS.Optimizer, "logging" => [], "all_solutions" => true))
214+
@variable(m, 1 <= x[1:2] <= 10, Int)
215+
modulo_affine(m,x[1],2,x[2])
216+
optimize!(m)
217+
218+
@test JuMP.termination_status(m) == MOI.OPTIMAL
219+
nresults = JuMP.result_count(m)
220+
results = Set{Tuple{Int,Int}}()
221+
for i in 1:nresults
222+
xv = convert.(Int, round.(JuMP.value.(x; result=i)))
223+
@test (xv[1]+xv[2]) % 2 == 2*xv[1]-xv[2]
224+
push!(results, (xv[1], xv[2]))
225+
end
226+
227+
found_nr = 0
228+
for i in 1:10, j in 1:10
229+
if (i+j) % 2 == 2*i-j
230+
@test (i,j) in results
231+
found_nr += 1
232+
end
233+
end
234+
@test found_nr == nresults
235+
end

0 commit comments

Comments
 (0)