Skip to content

Commit 29ea78a

Browse files
authored
basic indicator in reified constraint (#251)
* basic indicator/reified in indicator/reified constraint
1 parent 3e11d24 commit 29ea78a

File tree

8 files changed

+229
-4
lines changed

8 files changed

+229
-4
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# ConstrainSolver.jl - Changelog
22

3+
## Unreleased
4+
- Support for indicator/reified in indicator/reified (without bridges) [PR #251](https://github.com/Wikunia/ConstraintSolver.jl/pull/251)
5+
36
## v0.6.9 (17th of July 2021)
47
- set activator to false when inner violated [PR #266](https://github.com/Wikunia/ConstraintSolver.jl/pull/266)
58

@@ -28,6 +31,9 @@
2831
i.e `b := { sum(x) >= 10 || x in CS.AllDifferentSet() }`
2932
- Support for `&&` and `||` outside of `Indicator` and `Reified`
3033
- i.e `sum(x) >= 10 || x in CS.AllDifferentSet()`
34+
- Basic support for `Indicator` inside of reified:
35+
- i.e `@constraint(model, b1 := {b2 => { v == 1 }})`
36+
- currently lacks support for bridges such that `v > 1` in the inner constraint will fail
3137
- Some general performance updates [PR #247](https://github.com/Wikunia/ConstraintSolver.jl/pull/247)
3238

3339
## v0.6.3 (17th of January 2021)

docs/src/supported.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ The following list shows constraints that are implemented and those which are pl
8989
- [X] for all types of inner constraints
9090
- [X] Allow `&&` inside the inner constraint i.e `@constraint(m, b => {x + y >= 12 && 2x + y <= 7})`
9191
- [X] Allow `||` inside the inner constraint i.e `@constraint(m, b => {x + y >= 12 || 2x + y <= 7})`
92+
- [-] Have `Indicator` and `Reified` inside one another
93+
- this is only supported for simple cases i.e bridge support is missing
9294
- Reified constraints [#171](https://github.com/Wikunia/ConstraintSolver.jl/pull/171)
9395
- i.e `@constraint(m, b := {x + y >= 12})`
9496
- [X] for everything that is supported by indicator constraints

src/MOI_wrapper/util.jl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,34 @@ function get_inner_constraint(com, func::VAF{T}, set::Union{ReifiedSet, Indicato
4848
return init_constraint_struct(com, set.set, inner_internals)
4949
end
5050

51+
"""
52+
get_inner_constraint(com, func::VAF{T}, set::Union{ReifiedSet, IndicatorSet}, inner_set::Union{ReifiedSet{A}, IndicatorSet{A}, MOI.IndicatorSet{A}}) where {A,T<:Real}
53+
54+
Create the inner constraint when the inner constraint is a reified or indicator constraint as well.
55+
"""
56+
function get_inner_constraint(com, func::VAF{T}, set::Union{ReifiedSet, IndicatorSet}, inner_set::Union{ReifiedSet{A}, IndicatorSet{A}, MOI.IndicatorSet{A}}) where {A,T<:Real}
57+
f = MOIU.eachscalar(func)
58+
inner_internals = ConstraintInternals(
59+
0,
60+
f[2:end],
61+
set.set,
62+
get_indices(f[2:end]),
63+
)
64+
65+
inner_constraint = get_inner_constraint(com, f[2:end], inner_set, inner_set.set)
66+
complement_inner = get_complement_constraint(com, inner_constraint)
67+
indices = inner_internals.indices
68+
activator_internals = get_activator_internals(A, indices)
69+
if inner_set isa ReifiedSet
70+
constraint =
71+
ReifiedConstraint(inner_internals, activator_internals, inner_constraint, complement_inner)
72+
else
73+
constraint =
74+
IndicatorConstraint(inner_internals, activator_internals, inner_constraint)
75+
end
76+
return constraint
77+
end
78+
5179
function get_inner_constraint(com, func::VAF{T}, set::Union{ReifiedSet, IndicatorSet, MOI.IndicatorSet}, inner_set::MOI.AbstractScalarSet) where {T<:Real}
5280
inner_terms = [v.scalar_term for v in func.terms if v.output_index == 2]
5381
inner_constant = func.constants[2]

src/constraints/indicator.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ function init_constraint!(
1212
com::CS.CoM,
1313
constraint::IndicatorConstraint,
1414
fct::Union{MOI.VectorOfVariables,VAF{T}},
15-
set::IS,
15+
set::IS;
16+
active = true
1617
) where {
1718
A,
1819
T<:Real,
@@ -37,7 +38,7 @@ function init_constraint!(
3738
# map the bounds to the indicator constraint
3839
constraint.bound_rhs = inner_constraint.bound_rhs
3940
# the indicator can't be activated if inner constraint is infeasible
40-
if !feasible
41+
if !feasible && active
4142
!rm!(com, indicator_var, Int(constraint.activate_on)) && return false
4243
end
4344
end

src/constraints/reified.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ function init_constraint!(
22
com::CS.CoM,
33
constraint::ReifiedConstraint,
44
fct::Union{MOI.VectorOfVariables,VAF{T}},
5-
set::RS,
5+
set::RS;
6+
active = true
67
) where {A,T<:Real,RS<:ReifiedSet{A}}
78
inner_constraint = constraint.inner_constraint
89
complement_constraint = constraint.complement_constraint
@@ -25,7 +26,7 @@ function init_constraint!(
2526
# map the bounds to the indicator constraint
2627
constraint.bound_rhs = inner_constraint.bound_rhs
2728
# the reified variable can't be activated if inner constraint is infeasible
28-
if !feasible
29+
if !feasible && active
2930
!rm!(com, rei_var, Int(constraint.activate_on)) && return false
3031
end
3132
end

test/constraints/indicator.jl

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,4 +270,108 @@
270270
com = CS.get_inner_model(m)
271271
@test is_solved(com)
272272
end
273+
274+
@testset "indicator in indicator" begin
275+
m = Model(optimizer_with_attributes(
276+
CS.Optimizer,
277+
"all_solutions"=> true,
278+
"logging" => [],
279+
))
280+
@variable(m, a, Bin)
281+
@variable(m, b, Bin)
282+
@variable(m, c, Bin)
283+
@constraint(m, a => { b => { c == 1}})
284+
optimize!(m)
285+
@test JuMP.termination_status(m) == MOI.OPTIMAL
286+
com = CS.get_inner_model(m)
287+
nresults = JuMP.result_count(m)
288+
results = Set{Tuple{Bool,Bool,Bool}}()
289+
for i in 1:nresults
290+
aval,bval,cval = convert.(Bool, round.(JuMP.value.([a,b,c]; result=i)))
291+
if aval && bval
292+
@test cval
293+
end
294+
push!(results, (aval, bval, cval))
295+
end
296+
for av in [false, true], bv in [false, true], cv in [false, true]
297+
if !av || !bv
298+
@test (av,bv,cv) in results
299+
elseif av && bv && cv
300+
@test (av,bv,cv) in results
301+
end
302+
end
303+
end
304+
305+
@testset "reified in indicator" begin
306+
m = Model(optimizer_with_attributes(
307+
CS.Optimizer,
308+
"all_solutions"=> true,
309+
"logging" => [],
310+
))
311+
@variable(m, a, Bin)
312+
@variable(m, b, Bin)
313+
@variable(m, c, Bin)
314+
@constraint(m, a => { b := { c == 1}})
315+
optimize!(m)
316+
@test JuMP.termination_status(m) == MOI.OPTIMAL
317+
com = CS.get_inner_model(m)
318+
nresults = JuMP.result_count(m)
319+
results = Set{Tuple{Bool,Bool,Bool}}()
320+
for i in 1:nresults
321+
aval,bval,cval = convert.(Bool, round.(JuMP.value.([a,b,c]; result=i)))
322+
if aval
323+
@test (bval && cval) || (!bval && !cval)
324+
end
325+
push!(results, (aval, bval, cval))
326+
end
327+
for av in [false, true], bv in [false, true], cv in [false, true]
328+
if !av
329+
@test (av,bv,cv) in results
330+
elseif (bv && cv) || (!bv && !cv)
331+
@test (av,bv,cv) in results
332+
end
333+
end
334+
end
335+
336+
@testset "reified in reified in indicator" begin
337+
m = Model(optimizer_with_attributes(
338+
CS.Optimizer,
339+
"all_solutions"=> true,
340+
"logging" => [],
341+
))
342+
@variable(m, a, Bin)
343+
@variable(m, b, Bin)
344+
@variable(m, c, Bin)
345+
@variable(m, d, Bin)
346+
@constraint(m, a => { b := { c := {d == 1}}})
347+
optimize!(m)
348+
@test JuMP.termination_status(m) == MOI.OPTIMAL
349+
com = CS.get_inner_model(m)
350+
nresults = JuMP.result_count(m)
351+
results = Set{Tuple{Bool,Bool,Bool,Bool}}()
352+
for i in 1:nresults
353+
aval,bval,cval,dval = convert.(Bool, round.(JuMP.value.([a,b,c,d]; result=i)))
354+
if aval
355+
if bval
356+
@test (cval && dval) || (!cval && !dval)
357+
else
358+
@test (dval && !cval) || (!dval && cval)
359+
end
360+
end
361+
push!(results, (aval, bval, cval, dval))
362+
end
363+
for av in [false, true], bv in [false, true], cv in [false, true], dv in [false, true]
364+
if !av
365+
@test (av,bv,cv,dv) in results
366+
else
367+
if bv
368+
if (cv && dv) || (!cv && !dv)
369+
@test (av,bv,cv,dv) in results
370+
end
371+
elseif (dv && !cv) || (!dv && cv)
372+
@test (av,bv,cv,dv) in results
373+
end
374+
end
375+
end
376+
end
273377
end

test/monks_and_doors.jl

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# This test case is copied from http://hakank.org/julia/constraints/monks_and_doors.jl
2+
# which is a website by Håkan Kjellerstrand
3+
# please check it out :)
4+
5+
@testset "Monks & Doors" begin
6+
all_solutions = true
7+
model = Model(optimizer_with_attributes(CS.Optimizer,
8+
"all_solutions"=> all_solutions,
9+
# "all_optimal_solutions"=>all_solutions,
10+
"logging"=>[],
11+
12+
"traverse_strategy"=>:BFS,
13+
"branch_split"=>:InHalf,
14+
15+
"branch_strategy" => :IMPS, # default
16+
))
17+
18+
num_doors = 4
19+
num_monks = 8
20+
@variable(model, doors[1:num_doors], Bin)
21+
da,db,dc,dd = doors
22+
door_names = ["A","B","C","D"]
23+
24+
@variable(model, monks[1:num_monks], Bin)
25+
m1,m2,m3,m4,m5,m6,m7,m8 = monks
26+
27+
# Monk 1: Door A is the exit.
28+
# M1 #= A (Picat constraint)
29+
@constraint(model, m1 == da)
30+
31+
# Monk 2: At least one of the doors B and C is the exit.
32+
# M2 #= 1 #<=> (B #\/ C)
33+
@constraint(model, m2 := { db == 1 || dc == 1})
34+
35+
# Monk 3: Monk 1 and Monk 2 are telling the truth.
36+
# M3 #= 1 #<=> (M1 #/\ M2)
37+
@constraint(model, m3 := { m1 == 1 && m2 == 1})
38+
39+
# Monk 4: Doors A and B are both exits.
40+
# M4 #= 1 #<=> (A #/\ B)
41+
@constraint(model, m4 := { da == 1 && db == 1})
42+
43+
# Monk 5: Doors A and C are both exits.
44+
# M5 #= 1 #<=> (A #/\ C)
45+
@constraint(model, m5 := { da == 1 && dc == 1})
46+
47+
# Monk 6: Either Monk 4 or Monk 5 is telling the truth.
48+
# M6 #= 1 #<=> (M4 #\/ M5)
49+
@constraint(model, m6 := { m4 == 1|| m5 == 1})
50+
51+
# Monk 7: If Monk 3 is telling the truth, so is Monk 6.
52+
# M7 #= 1 #<=> (M3 #=> M6)
53+
@constraint(model, m7 := { m3 => {m6 == 1}})
54+
55+
# Monk 8: If Monk 7 and Monk 8 are telling the truth, so is Monk 1.
56+
# M8 #= 1 #<=> ((M7 #/\ M8) #=> (M1))
57+
b1 = @variable(model, binary=true)
58+
@constraint(model, b1 := {m7 == 1 && m8 == 1})
59+
@constraint(model, m8 := {b1 => {m1 == 1}})
60+
61+
# Exactly one door is an exit.
62+
# (A + B + C + D) #= 1
63+
@constraint(model, da + db + dc + dd == 1)
64+
65+
# Solve the problem
66+
optimize!(model)
67+
68+
status = JuMP.termination_status(model)
69+
# println("status:$status")
70+
num_sols = 0
71+
@test status == MOI.OPTIMAL
72+
num_sols = MOI.get(model, MOI.ResultCount())
73+
@test num_sols == 1
74+
doors_val = convert.(Integer,JuMP.value.(doors))
75+
monks_val = convert.(Integer,JuMP.value.(monks))
76+
# exit door is A
77+
@test doors_val[1] == 1
78+
@test sum(doors_val) == 1
79+
@test sum(monks_val) == 3
80+
# monks 1,7,8 are the only ones who tell the truth
81+
@test monks_val[1] == monks_val[7] == monks_val[8] == 1
82+
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ include("constraints/equal_to.jl")
7777
include("lp_solver.jl")
7878

7979
include("steiner.jl")
80+
include("monks_and_doors.jl")
8081
include("stable_set.jl")
8182
include("small_special.jl")
8283
include("maximum_weight_matching.jl")

0 commit comments

Comments
 (0)