Skip to content

Commit 2d75d9b

Browse files
authored
Merge pull request #36 from hdavid16:split_interval_constraints
allow interval constraints (lb <= expr <= ub)
2 parents 589c94a + df6453e commit 2d75d9b

File tree

15 files changed

+186
-133
lines changed

15 files changed

+186
-133
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "DisjunctiveProgramming"
22
uuid = "0d27d021-0159-4c7d-b4a7-9ccb5d9366cf"
33
authors = ["hdavid16 <[email protected]>"]
4-
version = "0.1.5"
4+
version = "0.1.6"
55

66
[deps]
77
IntervalArithmetic = "d1acc4aa-44c8-5952-acd4-ba5d80a2a253"

README.md

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Pkg.add("DisjunctiveProgramming")
1010

1111
## Disjunctions
1212

13-
After defining a JuMP model, disjunctions can be added to the model by specifying which of the original JuMP model constraints should be assigned to each disjunction. The constraints that are assigned to the disjunctions will no longer be general model constraints, but will belong to the disjunction that they are assigned to. These constraints must be either `GreaterThan`, `LessThan`, or `EqualTo` constraints. Constraints that are of `Interval` type are currently not supported. It is assumed that the disjuncts belonging to a disjunction are proper disjunctions (mutually exclussive) and only one of them will be selected.
13+
After defining a JuMP model, disjunctions can be added to the model by specifying which of the original JuMP model constraints should be assigned to each disjunction. The constraints that are assigned to the disjunctions will no longer be general model constraints, but will belong to the disjunction that they are assigned to. These constraints must be either `GreaterThan`, `LessThan`, `EqualTo`, or `Interval` constraints. Constraints that are of `Interval` type are split into two constraints (one for each bound). It is assumed that the disjuncts belonging to a disjunction are proper disjunctions (mutually exclussive) and only one of them will be selected (`XOR`).
1414

1515
When a disjunction is defined using the `@disjunction` macro, the disjunctions are reformulated to algebraic constraints via either,
1616
- The Big-M method (when `reformulation = :BMR` in the `@disjunction` macro)
@@ -48,34 +48,29 @@ using JuMP
4848
using DisjunctiveProgramming
4949

5050
m = Model()
51-
@variable(m, 0<=x[1:2]<=10)
51+
@variable(m, -1<=x<=10)
5252

53-
@constraint(m, con1[i=1:2], x[i] <= [3,4][i])
54-
@constraint(m, con2[i=1:2], zeros(2)[i] <= x[i])
55-
@constraint(m, con3[i=1:2], [5,4][i] <= x[i])
56-
@constraint(m, con4[i=1:2], x[i] <= [9,6][i])
53+
@constraint(m, con1, 0 <= x <= 3)
54+
@constraint(m, con2, 5 <= x <= 9)
5755

58-
@disjunction(m,(con1,con2),(con3,con4), reformulation=:BMR, name = :y)
59-
@proposition(m, y[1] y[2])
56+
@disjunction(m,con1,con2,reformulation=:BMR,name=:y)
57+
@proposition(m, y[1] y[2]) #this is a redundant proposition
6058

6159
print(m)
6260

61+
┌ Warning: con1 : x in [0.0, 3.0] uses the `MOI.Interval` set. Each instance of the interval set has been split into two constraints, one for each bound.
62+
┌ Warning: con2 : x in [5.0, 9.0] uses the `MOI.Interval` set. Each instance of the interval set has been split into two constraints, one for each bound.
63+
6364
Feasibility
6465
Subject to
65-
y[1] + y[2] == 1.0 #XOR constraint
66-
y[1] + y[2] >= 1.0 #reformulated logical proposition (redundant here)
67-
con1[1] : x[1] + 7 y[1] <= 10.0
68-
con1[2] : x[2] + 6 y[1] <= 10.0
69-
con2[1] : -x[1] <= 0.0
70-
con2[2] : -x[2] <= 0.0
71-
con3[1] : -x[1] + 5 y[2] <= 0.0
72-
con3[2] : -x[2] + 4 y[2] <= 0.0
73-
con4[1] : x[1] + y[2] <= 10.0
74-
con4[2] : x[2] + 4 y[2] <= 10.0
75-
x[1] >= 0.0
76-
x[2] >= 0.0
77-
x[1] <= 10.0
78-
x[2] <= 10.0
66+
XOR(y) : y[1] + y[2] == 1.0
67+
y[1] y[2] : y[1] + y[2] >= 1.0
68+
con1[lb] : -x + y[1] <= 1.0
69+
con1[ub] : x + 7 y[1] <= 10.0
70+
con2[lb] : -x + 6 y[2] <= 1.0
71+
con2[ub] : x + y[2] <= 10.0
72+
x >= -1.0
73+
x <= 10.0
7974
y[1] binary
8075
y[2] binary
8176
```

examples/ex1.jl

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,10 @@ using DisjunctiveProgramming
44
m = Model()
55
@variable(m, -1<=x<=10)
66

7-
@constraint(m, con1, x<=3)
8-
@constraint(m, con2, 0<=x)
9-
@constraint(m, con3, x<=9)
10-
@constraint(m, con4, 5<=x)
7+
@constraint(m, con1, 0 <= x <= 3)
8+
@constraint(m, con2, 5 <= x <= 9)
119

12-
@disjunction(m,(con1,con2),con3,con4,reformulation=:CHR,name=:y)
13-
# @proposition(m, y[1] ∨ y[2])
14-
# @proposition(m,
15-
# (
16-
# ((y[1] ∨ y[2]) ∧ (¬y[1] ∨ ¬y[2]))
17-
#
18-
# y[3]
19-
# )
20-
#
21-
# (
22-
# ¬((y[1] ∨ y[2]) ∧ (¬y[1] ∨ ¬y[2]))
23-
#
24-
# ¬y[3]
25-
# )
26-
#
27-
# (¬y[1] ∨ ¬y[2] ∨ ¬y[3])
28-
# )
29-
# @proposition(m, y[1] ⊻ y[2] ⊻ y[3])
10+
@disjunction(m,con1,con2,reformulation=:CHR,name=:y)
11+
@proposition(m, y[1] y[2]) #this is a redundant proposition
3012

3113
print(m)

examples/ex2.jl

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@ using DisjunctiveProgramming
55
m = Model()
66
@variable(m, 0<=x[1:2]<=10)
77

8-
@constraint(m, con1[i=1:2], x[i]<=[3,4][i])
9-
@constraint(m, con2, 0<=x[1])
10-
@constraint(m, con3, 0<=x[2])
11-
@constraint(m, con4[i=1:2], [5,4][i]<=x[i])
12-
@constraint(m, con5, x[1] <= 9)
13-
@constraint(m, con6, x[2] <= 6)
14-
15-
@disjunction(m,(con1,con2,con3),(con4,con5,con6),reformulation=:BMR,name=:y)
8+
@constraint(m, con1[i=1:2], 0 <= x[i]<=[3,4][i])
9+
@constraint(m, con2[i=1:2], [5,4][i] <= x[i] <= [9,6][i])
10+
11+
@disjunction(m,con1,con2,reformulation=:BMR,name=:y)
12+
13+
print(m)

examples/ex3.jl

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,11 @@ using JuMP
22
using DisjunctiveProgramming
33

44
m = Model()
5-
@variable(m, -1<=x<=10)
5+
@variable(m, -10 x[1:2] 10)
66

7-
@constraint(m, con1, x<=3)
8-
@constraint(m, con2, 0<=x)
9-
@constraint(m, con3, x<=9)
10-
@constraint(m, con4, 5<=x)
7+
nl_con1 = @NLconstraint(m, exp(x[1]) >= 1)
8+
nl_con2 = @NLconstraint(m, exp(x[2]) <= 2)
119

12-
M = 10
13-
@disjunction(
14-
m,
15-
(con1,con2),
16-
con3,
17-
con4,
18-
reformulation=:BMR,
19-
M=M,
20-
name=:y
21-
)
10+
@disjunction(m, nl_con1, nl_con2, reformulation=:CHR, name=:z)
11+
12+
print(m)

examples/ex4.jl

Lines changed: 0 additions & 22 deletions
This file was deleted.

examples/ex5.jl

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/DisjunctiveProgramming.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ using JuMP, IntervalArithmetic, Symbolics, Suppressor
55
export add_disjunction, add_proposition
66
export @disjunction, @proposition
77

8+
include("constraint.jl")
89
include("logic.jl")
910
include("utils.jl")
1011
include("big_M.jl")

src/big_M.jl

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
function BMR!(m, constr, bin_var, i, k, M)
22
if ismissing(k)
3-
@assert is_valid(m,constr) "$constr is not a valid constraint in the model."
43
ref = constr
54
else
6-
@assert is_valid(m,constr[k...]) "$constr is not a valid constraint in the model."
75
ref = constr[k...]
86
end
97
if ismissing(M)

src/constraint.jl

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
is_interval_constraint(con_ref::ConstraintRef{<:AbstractModel}) = constraint_object(con_ref).set isa MOI.Interval
2+
is_interval_constraint(con_ref::NonlinearConstraintRef) = count(i -> i == :(<=), Meta.parse(string(con_ref)).args) == 2
3+
JuMP.name(con_ref::NonlinearConstraintRef) = ""
4+
5+
function check_disjunction(m, disj)
6+
disj_new = [] #create a new array where the disjunction will be copied to so that we can split constraints that use an Interval set
7+
for constr in disj
8+
if constr isa Tuple #NOTE: Make it so that it must be bundled in a Tuple (not Array), to avoid confusing it with a Variable Array
9+
constr_list = []
10+
for constr_j in constr
11+
push!(constr_list, check_constraint!(m, constr_j))
12+
end
13+
push!(disj_new, Tuple(constr_list))
14+
elseif constr isa Union{ConstraintRef, Array, Containers.DenseAxisArray, Containers.SparseAxisArray}
15+
push!(disj_new, check_constraint!(m, constr))
16+
end
17+
end
18+
19+
return disj_new
20+
end
21+
22+
function check_constraint!(m, constr)
23+
@assert all(is_valid.(m, constr)) "$constr is not a valid constraint."
24+
split_flag = false
25+
constr_name = gen_constraint_name(constr)
26+
if constr isa ConstraintRef
27+
new_constr = split_interval_constraint(m, constr)
28+
if isnothing(new_constr)
29+
new_constr = constr
30+
else
31+
split_flag = true
32+
m[constr_name] = new_constr
33+
end
34+
elseif constr isa Union{Array, Containers.DenseAxisArray, Containers.SparseAxisArray}
35+
if !any(is_interval_constraint.(constr))
36+
new_constr = constr
37+
else
38+
split_flag = true
39+
if constr isa Union{Array, Containers.DenseAxisArray}
40+
idxs = Iterators.product(axes(constr)...)
41+
elseif constr isa Containers.SparseAxisArray
42+
idxs = keys(constr.data)
43+
end
44+
constr_dict = Dict(union(
45+
[
46+
split_interval_constraint(m, constr[idx...]) |>
47+
i -> isnothing(i) ?
48+
(idx...,"") => constr[idx...] :
49+
[(idx...,"lb") => i[1], (idx...,"ub") => i[2]]
50+
for idx in idxs
51+
]...
52+
))
53+
new_constr = Containers.SparseAxisArray(constr_dict)
54+
m[constr_name] = new_constr
55+
end
56+
end
57+
58+
split_flag && @warn "$constr uses the `MOI.Interval` set. Each instance of the interval set has been split into two constraints, one for each bound."
59+
delete_original_constraint!(m, constr)
60+
61+
return new_constr
62+
end
63+
64+
function gen_constraint_name(constr)
65+
constr_name = name.(constr)
66+
if any(isempty.(constr_name))
67+
constr_name = gensym("constraint")
68+
elseif !isa(constr_name, String)
69+
c_names = union(first.(split.(constr_name,"[")))
70+
if length(c_names) == 1
71+
constr_name = c_names[1]
72+
else
73+
constr_name = gensym("constraint")
74+
end
75+
end
76+
77+
return Symbol("$(constr_name)_split")
78+
end
79+
80+
function split_interval_constraint(m, constr, constr_name = name(constr))
81+
if isempty(constr_name)
82+
constr_name = "[$constr]"
83+
end
84+
if constr isa NonlinearConstraintRef
85+
constr_expr = Meta.parse(string(constr))
86+
if count(x -> x == :(<=), constr_expr.args) == 2
87+
lb = constr_expr.args[1]
88+
ub = constr_expr.args[5]
89+
constr_expr_func = copy(constr_expr.args[3]) #get func part of constraint
90+
replace_JuMPvars!(constr_expr_func, m) #replace Expr with JuMP vars
91+
#replace original constraint with lb <= func
92+
lb_constr = JuMP._NonlinearConstraint(
93+
JuMP._NonlinearExprData(m, constr_expr_func),
94+
lb,
95+
Inf
96+
)
97+
m.nlp_data.nlconstr[constr.index.value] = lb_constr
98+
#create new constraint for func <= ub
99+
constr_expr_ub = Expr(:call, :(<=), constr_expr_func, ub)
100+
ub_constr = add_nonlinear_constraint(m, constr_expr_ub)
101+
#return split constraint
102+
return [constr, ub_constr]
103+
end
104+
elseif constr isa ConstraintRef
105+
constr_obj = constraint_object(constr)
106+
if constr_obj.set isa MOI.Interval
107+
lb = constr_obj.set.lower
108+
ub = constr_obj.set.upper
109+
ex = constr_obj.func
110+
return [
111+
@constraint(m, lb <= ex, base_name = "$(constr_name)[lb]"),
112+
@constraint(m, ex <= ub, base_name = "$(constr_name)[ub]")
113+
]
114+
end
115+
end
116+
return nothing
117+
end
118+
119+
function delete_original_constraint!(m, constr)
120+
if constr isa ConstraintRef
121+
if !isa(constr, NonlinearConstraintRef)
122+
delete(m, constr)
123+
# unregister(m, constr)
124+
end
125+
elseif constr isa Union{Array, Containers.DenseAxisArray, Containers.SparseAxisArray}
126+
if !isa(first(constr), NonlinearConstraintRef)
127+
delete.(m, constr)
128+
# unregister(m, constr)
129+
end
130+
end
131+
end

0 commit comments

Comments
 (0)