Skip to content

Commit a1bfc45

Browse files
authored
Merge branch 'hdavid16:master' into master
2 parents c1256dd + 5f4adbd commit a1bfc45

File tree

7 files changed

+97
-54
lines changed

7 files changed

+97
-54
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ After defining a JuMP model, disjunctions can be added to the model by using the
1919

2020
NOTES:
2121
- Vectorized constraints (using `.` notation) are not currently supported. The current workarround is to first creating the constraint outside of the `@disjunction` macro and then passing the reference to the constraint to the `@disjunction` macro.
22-
- 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`).
22+
- Any constraints that are of `EqualTo` type are split into two constraints (e.g., `f(x) == 0` -> `0 <= f(x) <= 0`). This is necessary only for the Big-M reformulation of equality constraints, but is currently applied regardless of the reformulation technique.
23+
- Any constraints that are of `Interval` type are split into two constraints (one for each bound).
24+
- It is assumed that the disjuncts belonging to a disjunction are proper disjunctions (mutually exclussive) and only one of them will be selected (`XOR`).
2325

2426
The valid key-word arguments for the `@disjunction` macro are:
2527
- `reformulation::Symbol`: `:big_m` for [Big-M Reformulation](https://optimization.mccormick.northwestern.edu/index.php/Disjunctive_inequalities#Big-M_Reformulation), `:convex_hull` for [Convex-Hull Reformulation](https://optimization.mccormick.northwestern.edu/index.php/Disjunctive_inequalities#Convex-Hull_Reformulation)

examples/ex3.jl

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,10 @@ m = Model()
55
@variable(m, -5 x 10)
66
@disjunction(
77
m,
8-
# begin
9-
# exp(x) ≤ 2
10-
# -3 ≤ x
11-
# end,
12-
(
13-
exp(x) 2,
8+
begin
9+
exp(x) 2
1410
-3 x
15-
),
11+
end,
1612
begin
1713
3 exp(x)
1814
5 x
@@ -46,4 +42,5 @@ print(m)
4642
# Perspective Functions:
4743
# (-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
4844
# (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
45+
# -1.0 * x_z1 + -3.0 * z[1] <= 0
4946
# -1.0 * x_z2 + 5.0 * z[2] <= 0

src/big_M.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
function big_m_reformulation!!(constr, bin_var, i, k, M)
1+
function big_m_reformulation!(constr, bin_var, i, k, M)
22
if ismissing(k)
33
ref = constr
44
else
@@ -7,6 +7,8 @@ function big_m_reformulation!!(constr, bin_var, i, k, M)
77
if ismissing(M)
88
M = apply_interval_arithmetic(ref)
99
# @warn "No M value passed for $ref. M = $M was inferred from the variable bounds."
10+
elseif !ismissing(k)
11+
M = M[k]
1012
end
1113
if ref isa NonlinearConstraintRef
1214
nonlinear_bigM(ref, bin_var, M, i)

src/constraint.jl

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
is_interval_constraint(con_ref::ConstraintRef{<:AbstractModel}) = constraint_object(con_ref).set isa MOI.Interval
22
is_interval_constraint(con_ref::NonlinearConstraintRef) = count(i -> i == :(<=), Meta.parse(string(con_ref)).args) == 2
3+
is_equality_constraint(con_ref::ConstraintRef{<:AbstractModel}) = constraint_object(con_ref).set isa MOI.EqualTo
4+
is_equality_constraint(con_ref::NonlinearConstraintRef) = Meta.parse(string(con_ref)).args[1] == :(==)
35
JuMP.name(con_ref::NonlinearConstraintRef) = ""
46

57
function check_constraint!(m, constr)
68
@assert all(is_valid.(m, constr)) "$constr is not a valid constraint."
79
split_flag = false
810
constr_name = gen_constraint_name(constr)
11+
constr_str = split(string(constr),"}")[end]
912
if constr isa ConstraintRef
10-
new_constr = split_interval_constraint(m, constr)
13+
new_constr = split_constraint(m, constr)
1114
if isnothing(new_constr)
1215
new_constr = constr
1316
else
1417
split_flag = true
1518
m[constr_name] = new_constr
1619
end
1720
elseif constr isa Union{Array, Containers.DenseAxisArray, Containers.SparseAxisArray}
18-
if !any(is_interval_constraint.(constr))
21+
if !any(is_interval_constraint.(constr)) && !any(is_equality_constraint.(constr))
1922
new_constr = constr
2023
else
2124
split_flag = true
@@ -26,7 +29,7 @@ function check_constraint!(m, constr)
2629
end
2730
constr_dict = Dict(union(
2831
[
29-
split_interval_constraint(m, constr[idx...]) |>
32+
split_constraint(m, constr[idx...]) |>
3033
i -> isnothing(i) ?
3134
(idx...,"") => constr[idx...] :
3235
[(idx...,"lb") => i[1], (idx...,"ub") => i[2]]
@@ -39,7 +42,7 @@ function check_constraint!(m, constr)
3942
end
4043

4144
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."
45+
@warn "$constr_str uses the `MOI.Interval` or `MOI.EqualTo` set. Each instance of the interval set has been split into two constraints, one for each bound."
4346
delete_original_constraint!(m, constr)
4447
end
4548

@@ -62,47 +65,67 @@ function gen_constraint_name(constr)
6265
return Symbol("$(constr_name)_split")
6366
end
6467

65-
function split_interval_constraint(m, constr, constr_name = name(constr))
68+
function split_constraint(m, constr, constr_name = name(constr))
6669
if isempty(constr_name)
6770
constr_name = "[$constr]"
6871
end
6972
if constr isa NonlinearConstraintRef
7073
constr_expr = Meta.parse(string(constr))
71-
if count(x -> x == :(<=), constr_expr.args) == 2
74+
if constr_expr.args[1] == :(==) #replace == for lb <= expr <= ub and split
75+
lb, ub = 0, 0#rhs is always 0, but could get obtained from: constr_expr.args[3]
76+
constr_expr_func = copy(constr_expr.args[2])
77+
new_constraints = split_nonlinear_constraint(m, constr, constr_expr_func, lb, ub)
78+
return new_constraints
79+
elseif count(x -> x == :(<=), constr_expr.args) == 2 #split lb <= expr <= ub
7280
lb = constr_expr.args[1]
7381
ub = constr_expr.args[5]
7482
constr_expr_func = copy(constr_expr.args[3]) #get func part of constraint
75-
replace_JuMPvars!(constr_expr_func, m) #replace Expr with JuMP vars
76-
#replace original constraint with lb <= func
77-
lb_constr = JuMP._NonlinearConstraint(
78-
JuMP._NonlinearExprData(m, constr_expr_func),
79-
lb,
80-
Inf
81-
)
82-
m.nlp_data.nlconstr[constr.index.value] = lb_constr
83-
#create new constraint for func <= ub
84-
constr_expr_ub = Expr(:call, :(<=), constr_expr_func, ub)
85-
ub_constr = add_nonlinear_constraint(m, constr_expr_ub)
86-
#return split constraint
87-
return [constr, ub_constr]
83+
new_constraints = split_nonlinear_constraint(m, constr, constr_expr_func, lb, ub)
84+
return new_constraints
8885
end
8986
elseif constr isa ConstraintRef
9087
constr_obj = constraint_object(constr)
91-
if constr_obj.set isa MOI.Interval
88+
if constr_obj.set isa MOI.EqualTo
89+
lb = constr_obj.set.value
90+
ub = lb
91+
new_constraints = split_linear_constraint(m, constr_obj, constr_name, lb, ub)
92+
return new_constraints
93+
elseif constr_obj.set isa MOI.Interval
9294
lb = constr_obj.set.lower
9395
ub = constr_obj.set.upper
94-
ex = constr_obj.func
95-
lb_name = name_split_constraint(constr_name, :lb)
96-
ub_name = name_split_constraint(constr_name, :ub)
97-
return [
98-
@constraint(m, lb <= ex, base_name = lb_name),
99-
@constraint(m, ex <= ub, base_name = ub_name)
100-
]
96+
new_constraints = split_linear_constraint(m, constr_obj, constr_name, lb, ub)
97+
return new_constraints
10198
end
10299
end
103100
return nothing
104101
end
105102

103+
function split_linear_constraint(m, constr_obj, constr_name, lb, ub)
104+
ex = constr_obj.func
105+
lb_name = name_split_constraint(constr_name, :lb)
106+
ub_name = name_split_constraint(constr_name, :ub)
107+
return [
108+
@constraint(m, lb <= ex, base_name = lb_name),
109+
@constraint(m, ex <= ub, base_name = ub_name)
110+
]
111+
end
112+
113+
function split_nonlinear_constraint(m, constr, constr_expr_func, lb, ub)
114+
replace_JuMPvars!(constr_expr_func, m) #replace Expr with JuMP vars
115+
#replace original constraint with lb <= func
116+
lb_constr = JuMP._NonlinearConstraint(
117+
JuMP._NonlinearExprData(m, constr_expr_func),
118+
lb,
119+
Inf
120+
)
121+
m.nlp_data.nlconstr[constr.index.value] = lb_constr
122+
#create new constraint for func <= ub
123+
constr_expr_ub = Expr(:call, :(<=), constr_expr_func, ub)
124+
ub_constr = add_nonlinear_constraint(m, constr_expr_ub)
125+
126+
return [constr, ub_constr]
127+
end
128+
106129
function delete_original_constraint!(m, constr)
107130
if constr isa ConstraintRef
108131
if !isa(constr, NonlinearConstraintRef)

src/convex_hull.jl

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
function convex_hull_reformulation!!(constr, bin_var, i, k, eps)
1+
function convex_hull_reformulation!(constr, bin_var, i, k, eps)
22
ref = ismissing(k) ? constr : constr[k...] #get constraint
33
#create convex hull constraint
44
if ref isa NonlinearConstraintRef || constraint_object(ref).func isa QuadExpr
@@ -13,11 +13,17 @@ function disaggregate_variables(m, disj, bin_var)
1313
var_refs = m[:gdp_variable_refs]
1414
@assert all((has_upper_bound.(var_refs) .&& has_lower_bound.(var_refs)) .|| is_binary.(var_refs)) "All variables must be bounded to perform the Convex-Hull reformulation."
1515
#reformulate variables
16+
obj_dict = object_dictionary(m)
17+
bounds_dict = :variable_bounds_dict in keys(obj_dict) ? obj_dict[:variable_bounds_dict] : Dict() #NOTE: should pass as an keyword argument
1618
for var_name in m[:gdp_variable_names]
1719
var = m[var_name]
1820
#define UB and LB
1921
if var isa VariableRef
20-
LB, UB = get_bounds(var)
22+
if string(var) in keys(bounds_dict)
23+
LB, UB = bounds_dict[string(var)]
24+
else
25+
LB, UB = get_bounds(var)
26+
end
2127
elseif var isa Array{VariableRef} || var isa Containers.DenseAxisArray || var isa Containers.SparseAxisArray
2228
#initialize UB and LB with same container type as variable
2329
if var isa Array{VariableRef} || var isa Containers.DenseAxisArray
@@ -34,7 +40,11 @@ function disaggregate_variables(m, disj, bin_var)
3440
end
3541
#populate UB and LB
3642
for idx in eachindex(var)
37-
LB[idx], UB[idx] = get_bounds(var[idx])
43+
if string(var[idx]) in keys(bounds_dict)
44+
LB[idx], UB[idx] = bounds_dict[string(var[idx])]
45+
else
46+
LB[idx], UB[idx] = get_bounds(var[idx])
47+
end
3848
end
3949
end
4050
#disaggregate variable and add bounding constraints
@@ -99,8 +109,8 @@ end
99109
function add_disaggregated_variable(m, LB, UB, var, base_name)
100110
@variable(
101111
m,
102-
lower_bound = LB,
103-
upper_bound = UB,
112+
lower_bound = min(LB,0),
113+
upper_bound = max(UB,0),
104114
binary = is_binary(var),
105115
integer = is_integer(var),
106116
base_name = base_name

src/reformulate.jl

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,10 @@ function reformulate_disjunction(m::Model, disj...; bin_var, reformulation, para
2222
Symbol(bin_var,"[$i]") => disj[i] for i in eachindex(disj)
2323
)
2424
new_constraints[Symbol(bin_var,"_XOR")] = constraint_by_name(m, "XOR(disj_$bin_var)")
25-
for var in vars
26-
agg_con_name = "$(var)_$(bin_var)_aggregation"
27-
agg_con = constraint_by_name(m, agg_con_name)
28-
if !isnothing(agg_con)
29-
new_constraints[Symbol(agg_con_name)] = agg_con
25+
if reformulation == :convex_hull
26+
for var in m[:gdp_variable_refs]
27+
agg_con_name = "$(var)_$(bin_var)_aggregation"
28+
new_constraints[Symbol(agg_con_name)] = constraint_by_name(m, agg_con_name)
3029
end
3130
end
3231
return new_constraints
@@ -53,6 +52,8 @@ function check_disjunction!(m, disj)
5352
push!(disj_new, Tuple(constr_list))
5453
elseif constr isa Union{ConstraintRef, Array, Containers.DenseAxisArray, Containers.SparseAxisArray}
5554
push!(disj_new, check_constraint!(m, constr))
55+
elseif isnothing(constr)
56+
push!(disj_new, constr)
5657
end
5758
end
5859

@@ -84,9 +85,9 @@ end
8485

8586
function call_reformulation(reformulation, constr, bin_var, i, k, param)
8687
if reformulation == :big_m
87-
big_m_reformulation!!(constr, bin_var, i, k, param)
88+
big_m_reformulation!(constr, bin_var, i, k, param)
8889
elseif reformulation == :convex_hull
89-
convex_hull_reformulation!!(constr, bin_var, i, k, param)
90+
convex_hull_reformulation!(constr, bin_var, i, k, param)
9091
end
9192
end
9293

src/utils.jl

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,18 @@ function apply_interval_arithmetic(ref)
1010
ref_func_expr, ref_type, ref_rhs = parse_constraint(ref)
1111
#create a map of variables to their bounds
1212
interval_map = Dict()
13-
vars = ref.model[:gdp_variable_refs]
13+
vars = all_variables(ref.model)#ref.model[:gdp_variable_refs]
14+
obj_dict = object_dictionary(ref.model)
15+
bounds_dict = :variable_bounds_dict in keys(obj_dict) ? obj_dict[:variable_bounds_dict] : Dict() #NOTE: should pass as an keyword argument
1416
for var in vars
15-
UB = has_upper_bound(var) ? upper_bound(var) : (is_binary(var) ? 1 : Inf)
16-
LB = has_lower_bound(var) ? lower_bound(var) : (is_binary(var) ? 0 : -Inf)
17-
interval_map[string(var)] = LB..UB
17+
if string(var) in keys(bounds_dict)
18+
bnds = bounds_dict[string(var)]
19+
interval_map[string(var)] = bnds[1]..bnds[2]
20+
else
21+
UB = has_upper_bound(var) ? upper_bound(var) : (is_binary(var) ? 1 : Inf)
22+
LB = has_lower_bound(var) ? lower_bound(var) : (is_binary(var) ? 0 : -Inf)
23+
interval_map[string(var)] = LB..UB
24+
end
1825
end
1926
ref_func_expr = replace_intevals!(ref_func_expr, interval_map)
2027
#get bounds on the entire expression
@@ -34,12 +41,13 @@ end
3441
function parse_constraint(ref)
3542
if ref isa NonlinearConstraintRef
3643
ref_str = string(ref)
37-
ref_func = replace(split(ref_str, r"[=<>]")[1], " " => "")
44+
ref_func = replace(split(ref_str, r"[=<>]")[1], " " => "") #remove operator and spaces
3845
ref_type = occursin(">", ref_str) ? :lower : :upper
3946
ref_rhs = 0 #Could be calculated with: parse(Float64,split(ref_str, " ")[end]). NOTE: @NLconstraint will always have a 0 RHS.
4047
elseif ref isa ConstraintRef
4148
ref_obj = constraint_object(ref)
42-
ref_func = string(ref_obj.func)
49+
ref_str = string(ref_obj.func)
50+
ref_func = replace(ref_str, " " => "") #remove spaces
4351
ref_type = fieldnames(typeof(ref_obj.set))[1]
4452
ref_rhs = normalized_rhs(ref)
4553
end

0 commit comments

Comments
 (0)