Skip to content

Commit

Permalink
upgrade to allow creating constraints directly in the @Disjunct macro
Browse files Browse the repository at this point in the history
  • Loading branch information
hdavid16 committed Apr 11, 2022
1 parent 2d75d9b commit 4ee483d
Show file tree
Hide file tree
Showing 14 changed files with 353 additions and 162 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "DisjunctiveProgramming"
uuid = "0d27d021-0159-4c7d-b4a7-9ccb5d9366cf"
authors = ["hdavid16 <[email protected]>"]
version = "0.1.6"
version = "0.1.7"

[deps]
IntervalArithmetic = "d1acc4aa-44c8-5952-acd4-ba5d80a2a253"
Expand Down
63 changes: 35 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,24 @@ Pkg.add("DisjunctiveProgramming")

## Disjunctions

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`).
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:
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).
2. A valid expression accepted by [JuMP.@constraints](https://jump.dev/JuMP.jl/stable/reference/constraints/#JuMP.@constraints) (using `begin...end)
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.
4. A valid expression accepted by [JuMP.@NLconstraints](https://jump.dev/JuMP.jl/stable/reference/nlp/#JuMP.@NLconstraints) (using `begin...end)
5. `Tuple` of expressions accepted by options 1 and/or 3.

When a disjunction is defined using the `@disjunction` macro, the disjunctions are reformulated to algebraic constraints via either,
- The Big-M method (when `reformulation = :BMR` in the `@disjunction` macro)
- The Convex-Hull (when `reformulation = :CHR` in the `@disjunction` macro)
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.
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`).

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)")]`.
The valid key-word arguments for the `@disjunction` macro are:
- `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)
- `M`: Big-M value used when `reformulation = :BMR`.
- `ϵ`: 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`.
- `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)")]`.

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.
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.

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

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.

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

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

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))`).
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)`).

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

Expand All @@ -48,29 +54,30 @@ using JuMP
using DisjunctiveProgramming

m = Model()
@variable(m, -1<=x<=10)

@constraint(m, con1, 0 <= x <= 3)
@constraint(m, con2, 5 <= x <= 9)

@disjunction(m,con1,con2,reformulation=:BMR,name=:y)
@variable(m, -5 x 10)
@disjunction(
m,
0 x 3,
5 x 9,
reformulation=:BMR,
name=:y
)
@proposition(m, y[1] y[2]) #this is a redundant proposition

print(m)

┌ 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.
┌ 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.

┌ 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.
┌ 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.
Feasibility
Subject to
XOR(y) : y[1] + y[2] == 1.0
y[1] y[2] : y[1] + y[2] >= 1.0
con1[lb] : -x + y[1] <= 1.0
con1[ub] : x + 7 y[1] <= 10.0
con2[lb] : -x + 6 y[2] <= 1.0
con2[ub] : x + y[2] <= 10.0
x >= -1.0
x <= 10.0
y[1] binary
y[2] binary
XOR(disj_y) : y[1] + y[2] == 1.0 <- XOR constraint
y[1] y[2] : y[1] + y[2] >= 1.0 <- reformulated logical proposition (name is the proposition)
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])
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])
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])
disj_y[2][ub] : x + y[2] <= 10.0 <- right-side of constraint in 2nd disjunct (name is assigned to disj_y[2][ub])
x >= -5.0 <- variable lower bound
x <= 10.0 <- variable upper bound
y[1] binary <- indicator variable (1st disjunct) is binary
y[2] binary <- indicator variable (2nd disjunct) is binary
```
31 changes: 24 additions & 7 deletions examples/ex1.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,29 @@ using JuMP
using DisjunctiveProgramming

m = Model()
@variable(m, -1<=x<=10)

@constraint(m, con1, 0 <= x <= 3)
@constraint(m, con2, 5 <= x <= 9)

@disjunction(m,con1,con2,reformulation=:CHR,name=:y)
@variable(m, -5 x 10)
@disjunction(
m,
0 x 3,
5 x 9,
reformulation=:BMR,
name=:y
)
@proposition(m, y[1] y[2]) #this is a redundant proposition

print(m)
print(m)

# ┌ 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.
# ┌ 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.
# Feasibility
# Subject to
# XOR(disj_y) : y[1] + y[2] == 1.0 <- XOR constraint
# y[1] ∨ y[2] : y[1] + y[2] >= 1.0 <- reformulated logical proposition (name is the proposition)
# 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])
# 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])
# 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])
# disj_y[2][ub] : x + y[2] <= 10.0 <- right-side of constraint in 2nd disjunct (name is assigned to disj_y[2][ub])
# x >= -5.0 <- variable lower bound
# x <= 10.0 <- variable upper bound
# y[1] binary <- indicator variable (1st disjunct) is binary
# y[2] binary <- indicator variable (2nd disjunct) is binary
39 changes: 32 additions & 7 deletions examples/ex2.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,36 @@ using JuMP
using DisjunctiveProgramming

m = Model()
@variable(m, 0<=x[1:2]<=10)
@variable(m, -5 x[1:2] 10)
@disjunction(
m,
begin
con1[i=1:2], 0 x[i] [3,4][i]
end,
begin
con2[i=1:2], [5,4][i] x[i] [9,6][i]
end,
reformulation = :BMR,
name = :y
)
print(m)

@constraint(m, con1[i=1:2], 0 <= x[i]<=[3,4][i])
@constraint(m, con2[i=1:2], [5,4][i] <= x[i] <= [9,6][i])

@disjunction(m,con1,con2,reformulation=:BMR,name=:y)

print(m)
# ┌ 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.
# ┌ 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.
# Feasibility
# Subject to
# XOR(disj_y) : y[1] + y[2] == 1.0 <- XOR constraint
# con1[1][lb] : -x[1] + 5 y[1] <= 5.0 <- left-side of con1[1]
# con1[1][ub] : x[1] + 7 y[1] <= 10.0 <- right-side of con1[1]
# con1[2][lb] : -x[2] + 5 y[1] <= 5.0 <- left-side of con1[2]
# con1[2][ub] : x[2] + 6 y[1] <= 10.0 <- right-side of con1[2]
# con2[1][lb] : -x[1] + 10 y[2] <= 5.0 <- left-side of con2[1]
# con2[1][ub] : x[1] + y[2] <= 10.0 <- right-side of con2[1]
# con2[2][lb] : -x[2] + 9 y[2] <= 5.0 <- left-side of con2[2]
# con2[2][ub] : x[2] + 4 y[2] <= 10.0 <- right-side of con2[2]
# x[1] >= -5.0 <- varaible bounds
# x[2] >= -5.0 <- variable bounds
# x[1] <= 10.0 <- variable bounds
# x[2] <= 10.0 <- variable bounds
# y[1] binary <- indicator variable (1st disjunct) is binary
# y[2] binary <- indicator variable (2nd disjunct) is binary
43 changes: 36 additions & 7 deletions examples/ex3.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,40 @@ using JuMP
using DisjunctiveProgramming

m = Model()
@variable(m, -10 x[1:2] 10)
@variable(m, -5 x 10)
@disjunction(
m,
(
exp(x) 2,
-3 x
),
begin
3 exp(x)
5 x
end,
reformulation=:CHR,
name=:z
)
print(m)

nl_con1 = @NLconstraint(m, exp(x[1]) >= 1)
nl_con2 = @NLconstraint(m, exp(x[2]) <= 2)

@disjunction(m, nl_con1, nl_con2, reformulation=:CHR, name=:z)

print(m)
# Feasibility
# Subject to
# XOR(disj_z) : z[1] + z[2] == 1.0 <- XOR constraint
# x_aggregation : x - x_z1 - x_z2 == 0.0 <- aggregation of disaggregated variables
# 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)
# x_z1_lb : -5 z[1] - x_z1 <= 0.0 <- lower-bound constraint on disaggregated variable x_z1 (x in 1st disjunct)
# x_z1_ub : -10 z[1] + x_z1 <= 0.0 <- upper-bound constraint on disaggregated variable x_z1 (x in 1st disjunct)
# x_z2_lb : -5 z[2] - x_z2 <= 0.0 <- lower-bound constraint on disaggregated variable x_z2 (x in 2nd disjunct)
# x_z2_ub : -10 z[2] + x_z2 <= 0.0 <- upper-bound constraint on disaggregated variable x_z2 (x in 2nd disjunct)
# x >= -5.0 <- lower-bound on x
# x_z1 >= -5.0 <- lower-bound on x_z1 (disaggregated x in 1st disjunct)
# x_z2 >= -5.0 <- lower-bound on x_z2 (disaggregated x in 2nd disjunct)
# x <= 10.0 <- upper-bound on x
# x_z1 <= 10.0 <- upper-bound on x_z1 (disaggregated x in 1st disjunct)
# x_z2 <= 10.0 <- upper-bound on x_z2 (disaggregated x in 2nd disjunct)
# z[1] binary <- indicator variable (1st disjunct) is binary
# z[2] binary <- indicator variable (2nd disjunct) is binary
# Perspective Functions:
# (-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
# (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
# -1.0 * x_z2 + 5.0 * z[2] <= 0
4 changes: 2 additions & 2 deletions src/DisjunctiveProgramming.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module DisjunctiveProgramming

using JuMP, IntervalArithmetic, Symbolics, Suppressor

export add_disjunction, add_proposition
export add_disjunction!, add_proposition!, reformulate_disjunction
export @disjunction, @proposition

include("constraint.jl")
Expand All @@ -11,6 +11,6 @@ include("utils.jl")
include("big_M.jl")
include("convex_hull.jl")
include("reformulate.jl")
include("macro.jl")
include("macros.jl")

end # module
2 changes: 1 addition & 1 deletion src/big_M.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function BMR!(m, constr, bin_var, i, k, M)
function BMR!(constr, bin_var, i, k, M)
if ismissing(k)
ref = constr
else
Expand Down
23 changes: 4 additions & 19 deletions src/constraint.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,6 @@ is_interval_constraint(con_ref::ConstraintRef{<:AbstractModel}) = constraint_obj
is_interval_constraint(con_ref::NonlinearConstraintRef) = count(i -> i == :(<=), Meta.parse(string(con_ref)).args) == 2
JuMP.name(con_ref::NonlinearConstraintRef) = ""

function check_disjunction(m, disj)
disj_new = [] #create a new array where the disjunction will be copied to so that we can split constraints that use an Interval set
for constr in disj
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
constr_list = []
for constr_j in constr
push!(constr_list, check_constraint!(m, constr_j))
end
push!(disj_new, Tuple(constr_list))
elseif constr isa Union{ConstraintRef, Array, Containers.DenseAxisArray, Containers.SparseAxisArray}
push!(disj_new, check_constraint!(m, constr))
end
end

return disj_new
end

function check_constraint!(m, constr)
@assert all(is_valid.(m, constr)) "$constr is not a valid constraint."
split_flag = false
Expand Down Expand Up @@ -55,8 +38,10 @@ function check_constraint!(m, constr)
end
end

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."
delete_original_constraint!(m, constr)
if split_flag
@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."
delete_original_constraint!(m, constr)
end

return new_constr
end
Expand Down
Loading

2 comments on commit 4ee483d

@hdavid16
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/58355

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.1.7 -m "<description of version>" 4ee483dde30c8f8167cd14b9594ce0c2ec48291d
git push origin v0.1.7

Please sign in to comment.