Skip to content

Commit 04334c9

Browse files
authored
Merge pull request #90 from pulsipher/exactly1
Add `exactly1` option for disjunctions and fix bugs
2 parents 3af182e + 50cdc2a commit 04334c9

24 files changed

+238
-124
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ Disjunctions can be nested by passing an additional `Disjunct` tag. The Logical
107107

108108
Empty disjuncts are supported in GDP models. When used, the only constraints enforced on the model when the empty disjunct is selected are the global constraints and any other disjunction constraints defined.
109109

110+
For convenience, the `Exactly(1)` selector constraint is added by default when adding a disjunction to the model. In other words, `@disjunction(model, Y)` will add the disjunction and automatically add the logical constraint `Y in Exactly(1)`. For nested disjunctions, the appropriate `Exactly` constraint is added (e.g., `@constraint(model, Y[1:2] in Exactly(Y[3]))`) to indicate that `Exactly 1` logical variable in `Y[1:2]` is set to `true` when `Y[3]` is `true`, and both variables in `Y[1:2]` are set to `false` when `Y[3]` is `false`, meaning the parent disjunct is not selected. Adding the `Exactly` selector constraint by default can be disabled by setting the keyword argument `exactly1` to `false` in the `@disjunction` macro.
111+
110112
## MIP Reformulations
111113

112114
The following reformulation methods are currently supported:
@@ -142,7 +144,6 @@ m = GDPModel(HiGHS.Optimizer)
142144
@constraint(m, [i = 1:2], [2,5][i] ≤ x[i] ≤ [6,9][i], Disjunct(Y[1]))
143145
@constraint(m, [i = 1:2], [8,10][i] ≤ x[i] ≤ [11,15][i], Disjunct(Y[2]))
144146
@disjunction(m, Y)
145-
@constraint(m, Y in Exactly(1)) #logical constraint
146147
@objective(m, Max, sum(x))
147148
print(m)
148149
# Max x[1] + x[2]

docs/src/api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
```@autodocs
44
Modules = [DisjunctiveProgramming]
5-
Order = [:type, :function]
5+
Order = [:macro, :function, :type]
66
```

docs/src/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ Disjunctions can be nested by passing an additional `Disjunct` tag. The Logical
107107

108108
Empty disjuncts are supported in GDP models. When used, the only constraints enforced on the model when the empty disjunct is selected are the global constraints and any other disjunction constraints defined.
109109

110+
For convenience, the `Exactly(1)` selector constraint is added by default when adding a disjunction to the model. In other words, `@disjunction(model, Y)` will add the disjunction and automatically add the logical constraint `Y in Exactly(1)`. For nested disjunctions, the appropriate `Exactly` constraint is added (e.g., `@constraint(model, Y[1:2] in Exactly(Y[3]))`) to indicate that `Exactly 1` logical variable in `Y[1:2]` is set to `true` when `Y[3]` is `true`, and both variables in `Y[1:2]` are set to `false` when `Y[3]` is `false`, meaning the parent disjunct is not selected. Adding the `Exactly` selector constraint by default can be disabled by setting the keyword argument `exactly1` to `false` in the `@disjunction` macro.
111+
110112
## MIP Reformulations
111113

112114
The following reformulation methods are currently supported:
@@ -142,7 +144,6 @@ m = GDPModel(HiGHS.Optimizer)
142144
@constraint(m, [i = 1:2], [2,5][i] ≤ x[i] ≤ [6,9][i], Disjunct(Y[1]))
143145
@constraint(m, [i = 1:2], [8,10][i] ≤ x[i] ≤ [11,15][i], Disjunct(Y[2]))
144146
@disjunction(m, Y)
145-
@constraint(m, Y in Exactly(1)) #logical constraint
146147
@objective(m, Max, sum(x))
147148
print(m)
148149
# Max x[1] + x[2]

examples/ex1.jl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ m = GDPModel()
1010
@constraint(m, 0 x 3, Disjunct(Y[1]))
1111
@constraint(m, 5 x, Disjunct(Y[2]))
1212
@constraint(m, x 9, Disjunct(Y[2]))
13-
@disjunction(m, [Y[1], Y[2]])
14-
@constraint(m, Y in Exactly(1))
13+
@disjunction(m, [Y[1], Y[2]]) # can also just call `disjunction` instead
1514
@objective(m, Max, x)
1615

1716
# Reformulate logical variables and logical constraints

examples/ex2.jl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ m = GDPModel(HiGHS.Optimizer)
77
@variable(m, Y[1:2], Logical)
88
@constraint(m, [i = 1:2], [2,5][i] x[i] [6,9][i], Disjunct(Y[1]))
99
@constraint(m, [i = 1:2], [8,10][i] x[i] [11,15][i], Disjunct(Y[2]))
10-
@disjunction(m, Y)
11-
@constraint(m, Y in Exactly(1)) #logical constraint
10+
disjunction(m, Y)
1211
@objective(m, Max, sum(x))
1312
print(m)
1413
# Max x[1] + x[2]

examples/ex3.jl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ m = GDPModel()
77
@constraint(m, x >= -3, Disjunct(Y[1]))
88
@constraint(m, exp(x) >= 3, Disjunct(Y[2]))
99
@constraint(m, x >= 5, Disjunct(Y[2]))
10-
@disjunction(m, Y)
11-
@constraint(m, Y in Exactly(1)) #logical constraint
10+
disjunction(m, Y)
1211
@objective(m, Max, x)
1312
print(m)
1413
# Max x

examples/ex5.jl

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ m = GDPModel()
1313
@constraint(m, y2[i=1:2], [8,1][i] x[i] [9,2][i], Disjunct(Y[2]))
1414
@disjunction(m, inner, [W[1], W[2]], Disjunct(Y[1]))
1515
@disjunction(m, outer, [Y[1], Y[2]])
16-
@constraint(m, Y in Exactly(1))
17-
@constraint(m, W in Exactly(Y[1]))
1816

1917
##
2018
reformulate_model(m, BigM())

examples/ex6.jl

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,18 @@ m = GDPModel()
99
@constraint(m, x[1] >= 2, Disjunct(y[2]))
1010
@constraint(m, x[2] == -1, Disjunct(y[2]))
1111
@constraint(m, x[3] == 1, Disjunct(y[2]))
12-
@disjunction(m, y)
13-
@constraint(m, y in Exactly(1))
12+
disjunction(m, y)
1413

1514
@variable(m, w[1:2], Logical)
1615
@constraint(m, x[2] <= -3, Disjunct(w[1]))
1716
@constraint(m, x[2] >= 3, Disjunct(w[2]))
1817
@constraint(m, x[3] == 0, Disjunct(w[2]))
19-
@disjunction(m, w, Disjunct(y[1]))
20-
@constraint(m, w in Exactly(y[1]))
18+
disjunction(m, w, Disjunct(y[1]))
2119

2220
@variable(m, z[1:2], Logical)
2321
@constraint(m, x[3] <= -4, Disjunct(z[1]))
2422
@constraint(m, x[3] >= 4, Disjunct(z[2]))
25-
@disjunction(m, z, Disjunct(w[1]))
26-
@constraint(m, z in Exactly(w[1]))
23+
disjunction(m, z, Disjunct(w[1]))
2724

2825
##
2926
reformulate_model(m, BigM())

src/bigm.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ end
118118
################################################################################
119119
# BIG-M REFORMULATION
120120
################################################################################
121+
function _reformulate_disjunctions(model::Model, method::BigM)
122+
method.tighten && _query_variable_bounds(model, method)
123+
_reformulate_all_disjunctions(model, method)
124+
end
125+
121126
function reformulate_disjunct_constraint(
122127
model::Model,
123128
con::ScalarConstraint{T, S},

src/constraints.jl

Lines changed: 78 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -104,17 +104,24 @@ for (RefType, loc) in ((:DisjunctConstraintRef, :disjunct_constraints),
104104
end
105105
end
106106

107-
# Extend delete
108107
"""
109108
JuMP.delete(model::Model, cref::DisjunctionRef)
110109
111110
Delete a disjunction constraint from the `GDP model`.
112111
"""
113112
function JuMP.delete(model::Model, cref::DisjunctionRef)
114-
@assert is_valid(model, cref) "Disjunctive constraint does not belong to model."
115-
cidx = index(cref)
116-
dict = _disjunctions(model)
117-
delete!(dict, cidx)
113+
@assert is_valid(model, cref) "Disjunction does not belong to model."
114+
if JuMP.constraint_object(cref).nested
115+
lvref = gdp_data(model).constraint_to_indicator[cref]
116+
filter!(Base.Fix2(!=, cref), _indicator_to_constraints(model)[lvref])
117+
delete!(gdp_data(model).constraint_to_indicator, cref)
118+
end
119+
delete!(_disjunctions(model), index(cref))
120+
exactly1_dict = gdp_data(model).exactly1_constraints
121+
if haskey(exactly1_dict, cref)
122+
JuMP.delete(model, exactly1_dict[cref])
123+
delete!(exactly1_dict, cref)
124+
end
118125
_set_ready_to_optimize(model, false)
119126
return
120127
end
@@ -126,9 +133,10 @@ Delete a disjunct constraint from the `GDP model`.
126133
"""
127134
function JuMP.delete(model::Model, cref::DisjunctConstraintRef)
128135
@assert is_valid(model, cref) "Disjunctive constraint does not belong to model."
129-
cidx = index(cref)
130-
dict = _disjunct_constraints(model)
131-
delete!(dict, cidx)
136+
delete!(_disjunct_constraints(model), index(cref))
137+
lvref = gdp_data(model).constraint_to_indicator[cref]
138+
filter!(Base.Fix2(!=, cref), _indicator_to_constraints(model)[lvref])
139+
delete!(gdp_data(model).constraint_to_indicator, cref)
132140
_set_ready_to_optimize(model, false)
133141
return
134142
end
@@ -140,9 +148,7 @@ Delete a logical constraint from the `GDP model`.
140148
"""
141149
function JuMP.delete(model::Model, cref::LogicalConstraintRef)
142150
@assert is_valid(model, cref) "Logical constraint does not belong to model."
143-
cidx = index(cref)
144-
dict = _logical_constraints(model)
145-
delete!(dict, cidx)
151+
delete!(_logical_constraints(model), index(cref))
146152
_set_ready_to_optimize(model, false)
147153
return
148154
end
@@ -263,6 +269,7 @@ function _add_indicator_var(
263269
_indicator_to_constraints(model)[con.lvref] = Vector{Union{DisjunctConstraintRef, DisjunctionRef}}()
264270
end
265271
push!(_indicator_to_constraints(model)[con.lvref], cref)
272+
gdp_data(model).constraint_to_indicator[cref] = con.lvref
266273
return
267274
end
268275
# check disjunction
@@ -308,17 +315,34 @@ function _disjunction(
308315
_error::Function,
309316
model::Model, # TODO: generalize to AbstractModel
310317
structure::AbstractVector, #generalize for containers
311-
name::String
318+
name::String;
319+
exactly1::Bool = true,
320+
extra_kwargs...
312321
)
313-
return _create_disjunction(_error, model, structure, name, false)
322+
# check for unneeded keywords
323+
for (kwarg, _) in extra_kwargs
324+
_error("Unrecognized keyword argument $kwarg.")
325+
end
326+
# create the disjunction
327+
dref = _create_disjunction(_error, model, structure, name, false)
328+
# add the exactly one constraint if desired
329+
if exactly1
330+
lvars = JuMP.constraint_object(dref).indicators
331+
func = Union{Number, LogicalVariableRef}[1, lvars...]
332+
set = _MOIExactly(length(lvars) + 1)
333+
cref = JuMP.add_constraint(model, JuMP.VectorConstraint(func, set))
334+
gdp_data(model).exactly1_constraints[dref] = cref
335+
end
336+
return dref
314337
end
315338

316339
# Fallback disjunction build for nonvector structure
317340
function _disjunction(
318341
_error::Function,
319342
model::Model, # TODO: generalize to AbstractModel
320343
structure,
321-
name::String
344+
name::String;
345+
kwargs...
322346
)
323347
_error("Unrecognized disjunction input structure.")
324348
end
@@ -329,11 +353,26 @@ function _disjunction(
329353
model::Model, # TODO: generalize to AbstractModel
330354
structure,
331355
name::String,
332-
tag::Disjunct
356+
tag::Disjunct;
357+
exactly1::Bool = true,
358+
extra_kwargs...
333359
)
360+
# check for unneeded keywords
361+
for (kwarg, _) in extra_kwargs
362+
_error("Unrecognized keyword argument $kwarg.")
363+
end
364+
# create the disjunction
334365
dref = _create_disjunction(_error, model, structure, name, true)
335366
obj = constraint_object(dref)
336367
_add_indicator_var(_DisjunctConstraint(obj, tag.indicator), dref, model)
368+
# add the exactly one constraint if desired
369+
if exactly1
370+
lvars = JuMP.constraint_object(dref).indicators
371+
func = LogicalVariableRef[tag.indicator, lvars...]
372+
set = _MOIExactly(length(lvars) + 1)
373+
cref = JuMP.add_constraint(model, JuMP.VectorConstraint(func, set))
374+
gdp_data(model).exactly1_constraints[dref] = cref
375+
end
337376
return dref
338377
end
339378

@@ -343,45 +382,51 @@ function _disjunction(
343382
model::Model, # TODO: generalize to AbstractModel
344383
structure,
345384
name::String,
346-
extra...
385+
extra...;
386+
kwargs...
347387
)
348388
for arg in extra
349389
_error("Unrecognized argument `$arg`.")
350390
end
351391
end
352392

353393
"""
354-
disjunction(
355-
model::Model,
356-
disjunct_indicators::Vector{LogicalVariableRef}
357-
name::String = ""
358-
)
359-
360-
Function to add a [`Disjunction`](@ref) to a [`GDPModel`](@ref).
361-
362394
disjunction(
363395
model::Model,
364396
disjunct_indicators::Vector{LogicalVariableRef},
365-
nested_tag::Disjunct,
366-
name::String = ""
397+
[nested_tag::Disjunct],
398+
[name::String = ""];
399+
[exactly1::Bool = true]
367400
)
368401
369-
Function to add a nested [`Disjunction`](@ref) to a [`GDPModel`](@ref).
402+
Create a disjunction comprised of disjuncts with indicator variables `disjunct_indicators`
403+
and add it to `model`. For nested disjunctions, the `nested_tag` is required to indicate
404+
which disjunct it will be part of in the parent disjunction. By default, `exactly1` adds
405+
a constraint of the form `@constraint(model, disjunct_indicators in Exactly(1))` only
406+
allowing one of the disjuncts to be selected; this is required for certain reformulations like
407+
[`Hull`](@ref). For nested disjunctions, `exactly1` creates a constraint of the form
408+
`@constraint(model, disjunct_indicators in Exactly(nested_tag.indicator))`.
409+
To conveniently generate many disjunctions at once, see [`@disjunction`](@ref)
410+
and [`@disjunctions`](@ref).
370411
"""
371412
function disjunction(
372413
model::Model,
373414
disjunct_indicators,
374-
name::String = ""
375-
) # TODO add kw argument to build exactly 1 constraint
376-
return _disjunction(error, model, disjunct_indicators, name)
415+
name::String = "",
416+
extra...;
417+
kwargs...
418+
)
419+
return _disjunction(error, model, disjunct_indicators, name, extra...; kwargs...)
377420
end
378421
function disjunction(
379422
model::Model,
380423
disjunct_indicators,
381424
nested_tag::Disjunct,
382-
name::String = ""
383-
) # TODO add kw argument to build exactly 1 constraint
384-
return _disjunction(error, model, disjunct_indicators, name, nested_tag)
425+
name::String = "",
426+
extra...;
427+
kwargs...
428+
)
429+
return _disjunction(error, model, disjunct_indicators, name, nested_tag, extra...; kwargs...)
385430
end
386431

387432
################################################################################

0 commit comments

Comments
 (0)