Skip to content

Commit c054578

Browse files
committed
Enable exactly1 option and fix bugs
1 parent 3af182e commit c054578

22 files changed

+228
-119
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,6 @@ m = GDPModel(HiGHS.Optimizer)
142142
@constraint(m, [i = 1:2], [2,5][i] ≤ x[i] ≤ [6,9][i], Disjunct(Y[1]))
143143
@constraint(m, [i = 1:2], [8,10][i] ≤ x[i] ≤ [11,15][i], Disjunct(Y[2]))
144144
@disjunction(m, Y)
145-
@constraint(m, Y in Exactly(1)) #logical constraint
146145
@objective(m, Max, sum(x))
147146
print(m)
148147
# Max x[1] + x[2]

docs/src/index.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,6 @@ m = GDPModel(HiGHS.Optimizer)
142142
@constraint(m, [i = 1:2], [2,5][i] ≤ x[i] ≤ [6,9][i], Disjunct(Y[1]))
143143
@constraint(m, [i = 1:2], [8,10][i] ≤ x[i] ≤ [11,15][i], Disjunct(Y[2]))
144144
@disjunction(m, Y)
145-
@constraint(m, Y in Exactly(1)) #logical constraint
146145
@objective(m, Max, sum(x))
147146
print(m)
148147
# 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: 76 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,49 @@ 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))` making
406+
the disjuncts exclusive to one another; this is required for certain reformulations like
407+
[`Hull`](@ref). To conveniently generate many disjunctions at once, see [`@disjunction`](@ref)
408+
and [`@disjunctions`](@ref).
370409
"""
371410
function disjunction(
372411
model::Model,
373412
disjunct_indicators,
374-
name::String = ""
375-
) # TODO add kw argument to build exactly 1 constraint
376-
return _disjunction(error, model, disjunct_indicators, name)
413+
name::String = "",
414+
extra...;
415+
kwargs...
416+
)
417+
return _disjunction(error, model, disjunct_indicators, name, extra...; kwargs...)
377418
end
378419
function disjunction(
379420
model::Model,
380421
disjunct_indicators,
381422
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)
423+
name::String = "",
424+
extra...;
425+
kwargs...
426+
)
427+
return _disjunction(error, model, disjunct_indicators, name, nested_tag, extra...; kwargs...)
385428
end
386429

387430
################################################################################

src/datatypes.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,9 +390,13 @@ mutable struct GDPData
390390
disjunct_constraints::_MOIUC.CleverDict{DisjunctConstraintIndex, ConstraintData}
391391
disjunctions::_MOIUC.CleverDict{DisjunctionIndex, ConstraintData{Disjunction}}
392392

393+
# Exactly one constraint mappings
394+
exactly1_constraints::Dict{DisjunctionRef, LogicalConstraintRef}
395+
393396
# Indicator variable mappings
394397
indicator_to_binary::Dict{LogicalVariableRef, VariableRef}
395398
indicator_to_constraints::Dict{LogicalVariableRef, Vector{Union{DisjunctConstraintRef, DisjunctionRef}}}
399+
constraint_to_indicator::Dict{Union{DisjunctConstraintRef, DisjunctionRef}, LogicalVariableRef} # needed for deletion
396400

397401
# Reformulation variables and constraints
398402
reformulation_variables::Vector{VariableRef}
@@ -408,8 +412,10 @@ mutable struct GDPData
408412
_MOIUC.CleverDict{LogicalConstraintIndex, ConstraintData}(),
409413
_MOIUC.CleverDict{DisjunctConstraintIndex, ConstraintData}(),
410414
_MOIUC.CleverDict{DisjunctionIndex, ConstraintData{Disjunction}}(),
415+
Dict{DisjunctionRef, LogicalConstraintRef}(),
411416
Dict{LogicalVariableRef, VariableRef}(),
412417
Dict{LogicalVariableRef, Vector{Union{DisjunctConstraintRef, DisjunctionRef}}}(),
418+
Dict{Union{DisjunctConstraintRef, DisjunctionRef}, LogicalVariableRef}(),
413419
Vector{VariableRef}(),
414420
Vector{ConstraintRef}(),
415421
nothing,

0 commit comments

Comments
 (0)