Skip to content

Commit 4ee483d

Browse files
committed
upgrade to allow creating constraints directly in the @Disjunct macro
1 parent 2d75d9b commit 4ee483d

File tree

14 files changed

+353
-162
lines changed

14 files changed

+353
-162
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.6"
4+
version = "0.1.7"
55

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

README.md

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,24 @@ 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`, `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`).
13+
After defining a JuMP model, disjunctions can be added to the model by using the `@disjunction` macro. This macro is called by `@disjunction(m, disjuncts...; kwargs...), where `disjuncts...` is a list of at least two expressions of the form:
14+
1. A valid expression accepted by [JuMP.@constraint](https://jump.dev/JuMP.jl/stable/reference/constraints/#JuMP.@constraint). Names for the constraints or containers of constraints cannot be passed (use option 2).
15+
2. A valid expression accepted by [JuMP.@constraints](https://jump.dev/JuMP.jl/stable/reference/constraints/#JuMP.@constraints) (using `begin...end)
16+
3. A valid expression accepted by [JuMP.@NLconstraint](https://jump.dev/JuMP.jl/stable/reference/nlp/#JuMP.@NLconstraint). Containers of constraints cannot be passed (use option 4). Naming of non-linear constraints is not currently supported.
17+
4. A valid expression accepted by [JuMP.@NLconstraints](https://jump.dev/JuMP.jl/stable/reference/nlp/#JuMP.@NLconstraints) (using `begin...end)
18+
5. `Tuple` of expressions accepted by options 1 and/or 3.
1419

15-
When a disjunction is defined using the `@disjunction` macro, the disjunctions are reformulated to algebraic constraints via either,
16-
- The Big-M method (when `reformulation = :BMR` in the `@disjunction` macro)
17-
- The Convex-Hull (when `reformulation = :CHR` in the `@disjunction` macro)
18-
These approaches are described [here](https://optimization.mccormick.northwestern.edu/index.php/Disjunctive_inequalities). For the Convex-Hull reformulation, disaggregated variables are generated by adding the suffix `_$name$i` to the original variables, where `i` is the index of the disjunct in that disjunction. Bounding constraints are applied to the disaggregated variables and can be accessed with `model[Symbol("$<original var>_$name$i_lb")]` and `model[Symbol("$<original var>_$name$i_ub")]` for the lower bound and upper bound constraints, respectively. The aggregation constraint can be accessed with `model[Symbol("$<original var>_aggregation")]` When the Convex-Hull reformulation is applied to a nonlinear model, the perspective function proposed in [Furman, et al. [2020]](https://link.springer.com/article/10.1007/s10589-020-00176-0) is used.
20+
NOTE: Any 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`).
1921

20-
When calling the `@disjunction` macro, a `name::Symbol` keyword argument can be specified to define the name of the binary indicator variable to be used for that disjunction. Otherwise, (`name = missing`) a symbolic name will be generated with the prefix `disj`. The mutual exclussion constraint on the binary indicator variables can be accessed with `model[Symbol("XOR($name)")]`.
22+
The valid key-word arguments for the `@disjunction` macro are:
23+
- `reformulation::Symbol`: `:BMR` for [Big-M Reformulation](https://optimization.mccormick.northwestern.edu/index.php/Disjunctive_inequalities#Big-M_Reformulation), `:CHR` for [Convex-Hull Reformulation](https://optimization.mccormick.northwestern.edu/index.php/Disjunctive_inequalities#Convex-Hull_Reformulation)
24+
- `M`: Big-M value used when `reformulation = :BMR`.
25+
- `ϵ`: epsilon tolerance for the perspective function proposed by [Furman, et al. [2020]](https://link.springer.com/article/10.1007/s10589-020-00176-0). Only used when `reformulation = :CHR`.
26+
- `name::Symbol`: Name for the disjunction (also name for indicator variable used on that disjunction). If not passed (`name = missing`), a symbolic name will be generated with the prefix `disj`. The mutual exclussion constraint on the binary indicator variables can be accessed with `model[Symbol("XOR(disj_$name)")]`.
2127

22-
For Big-M reformulations, the user may provide an `M` object that represents the BigM value(s). The `M` object can be a `Number` that is applied to all constraints in the disjuncts, or a `Vector`/`Tuple` of values that are used for each of the disjuncts. For Convex-Hull reformulations, the user may provide an `ϵ` value for the perspective function (default is `ϵ = 1e-6`). The `ϵ` object can be a `Number` that is applied to all perspective functions, or a `Vector`/`Tuple` of values that are used for each of the disjuncts.
28+
When a disjunction is defined using the `@disjunction` macro, the disjunctions are reformulated to algebraic constraints via either Big-M or Convex-Hull reformulations. For the Convex-Hull reformulation, disaggregated variables are generated by adding the suffix `_$name$i` to the original variables, where `i` is the index of the disjunct in that disjunction. Bounding constraints are applied to the disaggregated variables and can be accessed with `model[Symbol("$<original var>_$name$i_lb")]` and `model[Symbol("$<original var>_$name$i_ub")]` for the lower bound and upper bound constraints, respectively. The aggregation constraint can be accessed with `model[Symbol("$<original var>_aggregation")]`. For Big-M reformulations, the user may provide an `M` object that represents the BigM value(s). The `M` object can be a `Number` that is applied to all constraints in the disjuncts, or a `Vector`/`Tuple` of values that are used for each of the disjuncts. For Convex-Hull reformulations, the user may provide an `ϵ` value for the perspective function (default is `ϵ = 1e-6`). The `ϵ` object can be a `Number` that is applied to all perspective functions, or a `Vector`/`Tuple` of values that are used for each of the disjuncts.
2329

24-
For empty disjuncts, use `nothing` for their positional argument (e.g., `@disjunction(m, con1, nothing, reformulation = :BMR)`).
30+
For empty disjuncts, use `nothing` for their positional argument (e.g., `@disjunction(m, x <= 1, nothing, reformulation = :BMR)`).
2531

2632
NOTE: `:gdp_variable_refs` and `:gdp_variable_names` are forbidden JuMP model object names when using *DisjunctiveProgramming.jl*. They are used to store the variable names and variable references in the original model.
2733

@@ -39,7 +45,7 @@ The logical proposition is then internally reformulated to an algebraic constrai
3945

4046
The example below is from the [Northwestern University Process Optimization Open Textbook](https://optimization.mccormick.northwestern.edu/index.php/Disjunctive_inequalities).
4147

42-
To perform the Big-M reformulation, `:BMR` is passed to the `reformulation` keyword argument. If nothing is passed to the keyword argument `M`, tight Big-M values will be inferred from the variable bounds using IntervalArithmetic.jl. If `x` is not bounded, Big-M values must be provided for either the whole system (e.g., `M = 10`) or for each of the constraint arrays in the example (e.g., `M = ((10,10),(10,10))`).
48+
To perform the Big-M reformulation, `:BMR` is passed to the `reformulation` keyword argument. If nothing is passed to the keyword argument `M`, tight Big-M values will be inferred from the variable bounds using IntervalArithmetic.jl. If `x` is not bounded, Big-M values must be provided for either the whole system (e.g., `M = 10`) or for each of the constraint arrays in the example (e.g., `M = (10,10)`).
4349

4450
To perform the Convex-Hull reformulation, `reformulation = :CHR`. Variables must have bounds for the reformulation to work.
4551

@@ -48,29 +54,30 @@ using JuMP
4854
using DisjunctiveProgramming
4955

5056
m = Model()
51-
@variable(m, -1<=x<=10)
52-
53-
@constraint(m, con1, 0 <= x <= 3)
54-
@constraint(m, con2, 5 <= x <= 9)
55-
56-
@disjunction(m,con1,con2,reformulation=:BMR,name=:y)
57+
@variable(m, -5 x 10)
58+
@disjunction(
59+
m,
60+
0 x 3,
61+
5 x 9,
62+
reformulation=:BMR,
63+
name=:y
64+
)
5765
@proposition(m, y[1] y[2]) #this is a redundant proposition
5866

5967
print(m)
6068

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-
69+
┌ Warning: disj_y[1] : 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.
70+
┌ Warning: disj_y[2] : 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.
6471
Feasibility
6572
Subject to
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
74-
y[1] binary
75-
y[2] binary
73+
XOR(disj_y) : y[1] + y[2] == 1.0 <- XOR constraint
74+
y[1] y[2] : y[1] + y[2] >= 1.0 <- reformulated logical proposition (name is the proposition)
75+
disj_y[1][lb] : -x + 5 y[1] <= 5.0 <- left-side of constraint in 1st disjunct (name is assigned to disj_y[1][lb])
76+
disj_y[1][ub] : x + 7 y[1] <= 10.0 <- right-side of constraint in 1st disjunct (name is assigned to disj_y[1][ub])
77+
disj_y[2][lb] : -x + 10 y[2] <= 5.0 <- left-side of constraint in 2nd disjunct (name is assigned to disj_y[2][lb])
78+
disj_y[2][ub] : x + y[2] <= 10.0 <- right-side of constraint in 2nd disjunct (name is assigned to disj_y[2][ub])
79+
x >= -5.0 <- variable lower bound
80+
x <= 10.0 <- variable upper bound
81+
y[1] binary <- indicator variable (1st disjunct) is binary
82+
y[2] binary <- indicator variable (2nd disjunct) is binary
7683
```

examples/ex1.jl

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,29 @@ using JuMP
22
using DisjunctiveProgramming
33

44
m = Model()
5-
@variable(m, -1<=x<=10)
6-
7-
@constraint(m, con1, 0 <= x <= 3)
8-
@constraint(m, con2, 5 <= x <= 9)
9-
10-
@disjunction(m,con1,con2,reformulation=:CHR,name=:y)
5+
@variable(m, -5 x 10)
6+
@disjunction(
7+
m,
8+
0 x 3,
9+
5 x 9,
10+
reformulation=:BMR,
11+
name=:y
12+
)
1113
@proposition(m, y[1] y[2]) #this is a redundant proposition
1214

13-
print(m)
15+
print(m)
16+
17+
# ┌ Warning: disj_y[1] : 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.
18+
# ┌ Warning: disj_y[2] : 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.
19+
# Feasibility
20+
# Subject to
21+
# XOR(disj_y) : y[1] + y[2] == 1.0 <- XOR constraint
22+
# y[1] ∨ y[2] : y[1] + y[2] >= 1.0 <- reformulated logical proposition (name is the proposition)
23+
# disj_y[1][lb] : -x + 5 y[1] <= 5.0 <- left-side of constraint in 1st disjunct (name is assigned to disj_y[1][lb])
24+
# disj_y[1][ub] : x + 7 y[1] <= 10.0 <- right-side of constraint in 1st disjunct (name is assigned to disj_y[1][ub])
25+
# disj_y[2][lb] : -x + 10 y[2] <= 5.0 <- left-side of constraint in 2nd disjunct (name is assigned to disj_y[2][lb])
26+
# disj_y[2][ub] : x + y[2] <= 10.0 <- right-side of constraint in 2nd disjunct (name is assigned to disj_y[2][ub])
27+
# x >= -5.0 <- variable lower bound
28+
# x <= 10.0 <- variable upper bound
29+
# y[1] binary <- indicator variable (1st disjunct) is binary
30+
# y[2] binary <- indicator variable (2nd disjunct) is binary

examples/ex2.jl

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,36 @@ using JuMP
33
using DisjunctiveProgramming
44

55
m = Model()
6-
@variable(m, 0<=x[1:2]<=10)
6+
@variable(m, -5 x[1:2] 10)
7+
@disjunction(
8+
m,
9+
begin
10+
con1[i=1:2], 0 x[i] [3,4][i]
11+
end,
12+
begin
13+
con2[i=1:2], [5,4][i] x[i] [9,6][i]
14+
end,
15+
reformulation = :BMR,
16+
name = :y
17+
)
18+
print(m)
719

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)
20+
# ┌ Warning: [con1[1] : x[1] in [0.0, 3.0], con1[2] : x[2] in [0.0, 4.0]] uses the `MOI.Interval` set. Each instance of the interval set has been split into two constraints, one for each bound.
21+
# ┌ Warning: [con2[1] : x[1] in [5.0, 9.0], con2[2] : x[2] in [4.0, 6.0]] uses the `MOI.Interval` set. Each instance of the interval set has been split into two constraints, one for each bound.
22+
# Feasibility
23+
# Subject to
24+
# XOR(disj_y) : y[1] + y[2] == 1.0 <- XOR constraint
25+
# con1[1][lb] : -x[1] + 5 y[1] <= 5.0 <- left-side of con1[1]
26+
# con1[1][ub] : x[1] + 7 y[1] <= 10.0 <- right-side of con1[1]
27+
# con1[2][lb] : -x[2] + 5 y[1] <= 5.0 <- left-side of con1[2]
28+
# con1[2][ub] : x[2] + 6 y[1] <= 10.0 <- right-side of con1[2]
29+
# con2[1][lb] : -x[1] + 10 y[2] <= 5.0 <- left-side of con2[1]
30+
# con2[1][ub] : x[1] + y[2] <= 10.0 <- right-side of con2[1]
31+
# con2[2][lb] : -x[2] + 9 y[2] <= 5.0 <- left-side of con2[2]
32+
# con2[2][ub] : x[2] + 4 y[2] <= 10.0 <- right-side of con2[2]
33+
# x[1] >= -5.0 <- varaible bounds
34+
# x[2] >= -5.0 <- variable bounds
35+
# x[1] <= 10.0 <- variable bounds
36+
# x[2] <= 10.0 <- variable bounds
37+
# y[1] binary <- indicator variable (1st disjunct) is binary
38+
# y[2] binary <- indicator variable (2nd disjunct) is binary

examples/ex3.jl

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,40 @@ using JuMP
22
using DisjunctiveProgramming
33

44
m = Model()
5-
@variable(m, -10 x[1:2] 10)
5+
@variable(m, -5 x 10)
6+
@disjunction(
7+
m,
8+
(
9+
exp(x) 2,
10+
-3 x
11+
),
12+
begin
13+
3 exp(x)
14+
5 x
15+
end,
16+
reformulation=:CHR,
17+
name=:z
18+
)
19+
print(m)
620

7-
nl_con1 = @NLconstraint(m, exp(x[1]) >= 1)
8-
nl_con2 = @NLconstraint(m, exp(x[2]) <= 2)
9-
10-
@disjunction(m, nl_con1, nl_con2, reformulation=:CHR, name=:z)
11-
12-
print(m)
21+
# Feasibility
22+
# Subject to
23+
# XOR(disj_z) : z[1] + z[2] == 1.0 <- XOR constraint
24+
# x_aggregation : x - x_z1 - x_z2 == 0.0 <- aggregation of disaggregated variables
25+
# disj_z[1,2] : -x_z1 - 3 z[1] <= 0.0 <- convex-hull reformulation of 2nd constraint if 1st disjunct (named disj_z[1,2] to indicate 1st disjunct, 2nd constraint)
26+
# x_z1_lb : -5 z[1] - x_z1 <= 0.0 <- lower-bound constraint on disaggregated variable x_z1 (x in 1st disjunct)
27+
# x_z1_ub : -10 z[1] + x_z1 <= 0.0 <- upper-bound constraint on disaggregated variable x_z1 (x in 1st disjunct)
28+
# x_z2_lb : -5 z[2] - x_z2 <= 0.0 <- lower-bound constraint on disaggregated variable x_z2 (x in 2nd disjunct)
29+
# x_z2_ub : -10 z[2] + x_z2 <= 0.0 <- upper-bound constraint on disaggregated variable x_z2 (x in 2nd disjunct)
30+
# x >= -5.0 <- lower-bound on x
31+
# x_z1 >= -5.0 <- lower-bound on x_z1 (disaggregated x in 1st disjunct)
32+
# x_z2 >= -5.0 <- lower-bound on x_z2 (disaggregated x in 2nd disjunct)
33+
# x <= 10.0 <- upper-bound on x
34+
# x_z1 <= 10.0 <- upper-bound on x_z1 (disaggregated x in 1st disjunct)
35+
# x_z2 <= 10.0 <- upper-bound on x_z2 (disaggregated x in 2nd disjunct)
36+
# z[1] binary <- indicator variable (1st disjunct) is binary
37+
# z[2] binary <- indicator variable (2nd disjunct) is binary
38+
# Perspective Functions:
39+
# (-1.0e-6 + -1.9999989999999999 * z[1]) + (1.0e-6 + 0.999999 * z[1]) * exp(x_z1 / (1.0e-6 + 0.999999 * z[1])) <= 0
40+
# (1.0000000000000002e-6 + 2.999999 * z[2]) + (-1.0e-6 + -0.999999 * z[2]) * exp(x_z2 / (1.0e-6 + 0.999999 * z[2])) <= 0
41+
# -1.0 * x_z2 + 5.0 * z[2] <= 0

src/DisjunctiveProgramming.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module DisjunctiveProgramming
22

33
using JuMP, IntervalArithmetic, Symbolics, Suppressor
44

5-
export add_disjunction, add_proposition
5+
export add_disjunction!, add_proposition!, reformulate_disjunction
66
export @disjunction, @proposition
77

88
include("constraint.jl")
@@ -11,6 +11,6 @@ include("utils.jl")
1111
include("big_M.jl")
1212
include("convex_hull.jl")
1313
include("reformulate.jl")
14-
include("macro.jl")
14+
include("macros.jl")
1515

1616
end # module

src/big_M.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
function BMR!(m, constr, bin_var, i, k, M)
1+
function BMR!(constr, bin_var, i, k, M)
22
if ismissing(k)
33
ref = constr
44
else

src/constraint.jl

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,6 @@ is_interval_constraint(con_ref::ConstraintRef{<:AbstractModel}) = constraint_obj
22
is_interval_constraint(con_ref::NonlinearConstraintRef) = count(i -> i == :(<=), Meta.parse(string(con_ref)).args) == 2
33
JuMP.name(con_ref::NonlinearConstraintRef) = ""
44

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-
225
function check_constraint!(m, constr)
236
@assert all(is_valid.(m, constr)) "$constr is not a valid constraint."
247
split_flag = false
@@ -55,8 +38,10 @@ function check_constraint!(m, constr)
5538
end
5639
end
5740

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)
41+
if split_flag
42+
@warn "$(split(string(constr),"}")[end]) uses the `MOI.Interval` set. Each instance of the interval set has been split into two constraints, one for each bound."
43+
delete_original_constraint!(m, constr)
44+
end
6045

6146
return new_constr
6247
end

0 commit comments

Comments
 (0)