Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exactly1 option for disjunctions and fix bugs #90

Merged
merged 8 commits into from
Oct 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ Disjunctions can be nested by passing an additional `Disjunct` tag. The Logical

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.

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.

## MIP Reformulations

The following reformulation methods are currently supported:
Expand Down Expand Up @@ -142,7 +144,6 @@ m = GDPModel(HiGHS.Optimizer)
@constraint(m, [i = 1:2], [2,5][i] ≤ x[i] ≤ [6,9][i], Disjunct(Y[1]))
@constraint(m, [i = 1:2], [8,10][i] ≤ x[i] ≤ [11,15][i], Disjunct(Y[2]))
@disjunction(m, Y)
@constraint(m, Y in Exactly(1)) #logical constraint
@objective(m, Max, sum(x))
print(m)
# Max x[1] + x[2]
Expand Down
2 changes: 1 addition & 1 deletion docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

```@autodocs
Modules = [DisjunctiveProgramming]
Order = [:type, :function]
Order = [:macro, :function, :type]
```
3 changes: 2 additions & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ Disjunctions can be nested by passing an additional `Disjunct` tag. The Logical

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.

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.

## MIP Reformulations

The following reformulation methods are currently supported:
Expand Down Expand Up @@ -142,7 +144,6 @@ m = GDPModel(HiGHS.Optimizer)
@constraint(m, [i = 1:2], [2,5][i] ≤ x[i] ≤ [6,9][i], Disjunct(Y[1]))
@constraint(m, [i = 1:2], [8,10][i] ≤ x[i] ≤ [11,15][i], Disjunct(Y[2]))
@disjunction(m, Y)
@constraint(m, Y in Exactly(1)) #logical constraint
@objective(m, Max, sum(x))
print(m)
# Max x[1] + x[2]
Expand Down
3 changes: 1 addition & 2 deletions examples/ex1.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ m = GDPModel()
@constraint(m, 0 ≤ x ≤ 3, Disjunct(Y[1]))
@constraint(m, 5 ≤ x, Disjunct(Y[2]))
@constraint(m, x ≤ 9, Disjunct(Y[2]))
@disjunction(m, [Y[1], Y[2]])
@constraint(m, Y in Exactly(1))
@disjunction(m, [Y[1], Y[2]]) # can also just call `disjunction` instead
@objective(m, Max, x)

# Reformulate logical variables and logical constraints
Expand Down
3 changes: 1 addition & 2 deletions examples/ex2.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ m = GDPModel(HiGHS.Optimizer)
@variable(m, Y[1:2], Logical)
@constraint(m, [i = 1:2], [2,5][i] ≤ x[i] ≤ [6,9][i], Disjunct(Y[1]))
@constraint(m, [i = 1:2], [8,10][i] ≤ x[i] ≤ [11,15][i], Disjunct(Y[2]))
@disjunction(m, Y)
@constraint(m, Y in Exactly(1)) #logical constraint
disjunction(m, Y)
@objective(m, Max, sum(x))
print(m)
# Max x[1] + x[2]
Expand Down
3 changes: 1 addition & 2 deletions examples/ex3.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ m = GDPModel()
@constraint(m, x >= -3, Disjunct(Y[1]))
@constraint(m, exp(x) >= 3, Disjunct(Y[2]))
@constraint(m, x >= 5, Disjunct(Y[2]))
@disjunction(m, Y)
@constraint(m, Y in Exactly(1)) #logical constraint
disjunction(m, Y)
@objective(m, Max, x)
print(m)
# Max x
Expand Down
2 changes: 0 additions & 2 deletions examples/ex5.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ m = GDPModel()
@constraint(m, y2[i=1:2], [8,1][i] ≤ x[i] ≤ [9,2][i], Disjunct(Y[2]))
@disjunction(m, inner, [W[1], W[2]], Disjunct(Y[1]))
@disjunction(m, outer, [Y[1], Y[2]])
@constraint(m, Y in Exactly(1))
@constraint(m, W in Exactly(Y[1]))

##
reformulate_model(m, BigM())
Expand Down
9 changes: 3 additions & 6 deletions examples/ex6.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,18 @@ m = GDPModel()
@constraint(m, x[1] >= 2, Disjunct(y[2]))
@constraint(m, x[2] == -1, Disjunct(y[2]))
@constraint(m, x[3] == 1, Disjunct(y[2]))
@disjunction(m, y)
@constraint(m, y in Exactly(1))
disjunction(m, y)

@variable(m, w[1:2], Logical)
@constraint(m, x[2] <= -3, Disjunct(w[1]))
@constraint(m, x[2] >= 3, Disjunct(w[2]))
@constraint(m, x[3] == 0, Disjunct(w[2]))
@disjunction(m, w, Disjunct(y[1]))
@constraint(m, w in Exactly(y[1]))
disjunction(m, w, Disjunct(y[1]))

@variable(m, z[1:2], Logical)
@constraint(m, x[3] <= -4, Disjunct(z[1]))
@constraint(m, x[3] >= 4, Disjunct(z[2]))
@disjunction(m, z, Disjunct(w[1]))
@constraint(m, z in Exactly(w[1]))
disjunction(m, z, Disjunct(w[1]))

##
reformulate_model(m, BigM())
Expand Down
5 changes: 5 additions & 0 deletions src/bigm.jl
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ end
################################################################################
# BIG-M REFORMULATION
################################################################################
function _reformulate_disjunctions(model::Model, method::BigM)
method.tighten && _query_variable_bounds(model, method)
_reformulate_all_disjunctions(model, method)
end

function reformulate_disjunct_constraint(
model::Model,
con::ScalarConstraint{T, S},
Expand Down
111 changes: 78 additions & 33 deletions src/constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,24 @@ for (RefType, loc) in ((:DisjunctConstraintRef, :disjunct_constraints),
end
end

# Extend delete
"""
JuMP.delete(model::Model, cref::DisjunctionRef)

Delete a disjunction constraint from the `GDP model`.
"""
function JuMP.delete(model::Model, cref::DisjunctionRef)
@assert is_valid(model, cref) "Disjunctive constraint does not belong to model."
cidx = index(cref)
dict = _disjunctions(model)
delete!(dict, cidx)
@assert is_valid(model, cref) "Disjunction does not belong to model."
if JuMP.constraint_object(cref).nested
lvref = gdp_data(model).constraint_to_indicator[cref]
filter!(Base.Fix2(!=, cref), _indicator_to_constraints(model)[lvref])
delete!(gdp_data(model).constraint_to_indicator, cref)
end
delete!(_disjunctions(model), index(cref))
exactly1_dict = gdp_data(model).exactly1_constraints
if haskey(exactly1_dict, cref)
JuMP.delete(model, exactly1_dict[cref])
delete!(exactly1_dict, cref)
end
_set_ready_to_optimize(model, false)
return
end
Expand All @@ -126,9 +133,10 @@ Delete a disjunct constraint from the `GDP model`.
"""
function JuMP.delete(model::Model, cref::DisjunctConstraintRef)
@assert is_valid(model, cref) "Disjunctive constraint does not belong to model."
cidx = index(cref)
dict = _disjunct_constraints(model)
delete!(dict, cidx)
delete!(_disjunct_constraints(model), index(cref))
lvref = gdp_data(model).constraint_to_indicator[cref]
filter!(Base.Fix2(!=, cref), _indicator_to_constraints(model)[lvref])
delete!(gdp_data(model).constraint_to_indicator, cref)
_set_ready_to_optimize(model, false)
return
end
Expand All @@ -140,9 +148,7 @@ Delete a logical constraint from the `GDP model`.
"""
function JuMP.delete(model::Model, cref::LogicalConstraintRef)
@assert is_valid(model, cref) "Logical constraint does not belong to model."
cidx = index(cref)
dict = _logical_constraints(model)
delete!(dict, cidx)
delete!(_logical_constraints(model), index(cref))
_set_ready_to_optimize(model, false)
return
end
Expand Down Expand Up @@ -263,6 +269,7 @@ function _add_indicator_var(
_indicator_to_constraints(model)[con.lvref] = Vector{Union{DisjunctConstraintRef, DisjunctionRef}}()
end
push!(_indicator_to_constraints(model)[con.lvref], cref)
gdp_data(model).constraint_to_indicator[cref] = con.lvref
return
end
# check disjunction
Expand Down Expand Up @@ -308,17 +315,34 @@ function _disjunction(
_error::Function,
model::Model, # TODO: generalize to AbstractModel
structure::AbstractVector, #generalize for containers
name::String
name::String;
exactly1::Bool = true,
extra_kwargs...
)
return _create_disjunction(_error, model, structure, name, false)
# check for unneeded keywords
for (kwarg, _) in extra_kwargs
_error("Unrecognized keyword argument $kwarg.")
end
# create the disjunction
dref = _create_disjunction(_error, model, structure, name, false)
# add the exactly one constraint if desired
if exactly1
lvars = JuMP.constraint_object(dref).indicators
func = Union{Number, LogicalVariableRef}[1, lvars...]
set = _MOIExactly(length(lvars) + 1)
cref = JuMP.add_constraint(model, JuMP.VectorConstraint(func, set))
gdp_data(model).exactly1_constraints[dref] = cref
end
return dref
end

# Fallback disjunction build for nonvector structure
function _disjunction(
_error::Function,
model::Model, # TODO: generalize to AbstractModel
structure,
name::String
name::String;
kwargs...
)
_error("Unrecognized disjunction input structure.")
end
Expand All @@ -329,11 +353,26 @@ function _disjunction(
model::Model, # TODO: generalize to AbstractModel
structure,
name::String,
tag::Disjunct
tag::Disjunct;
exactly1::Bool = true,
extra_kwargs...
)
# check for unneeded keywords
for (kwarg, _) in extra_kwargs
_error("Unrecognized keyword argument $kwarg.")
end
# create the disjunction
dref = _create_disjunction(_error, model, structure, name, true)
obj = constraint_object(dref)
_add_indicator_var(_DisjunctConstraint(obj, tag.indicator), dref, model)
# add the exactly one constraint if desired
if exactly1
lvars = JuMP.constraint_object(dref).indicators
func = LogicalVariableRef[tag.indicator, lvars...]
set = _MOIExactly(length(lvars) + 1)
cref = JuMP.add_constraint(model, JuMP.VectorConstraint(func, set))
gdp_data(model).exactly1_constraints[dref] = cref
end
return dref
end

Expand All @@ -343,45 +382,51 @@ function _disjunction(
model::Model, # TODO: generalize to AbstractModel
structure,
name::String,
extra...
extra...;
kwargs...
)
for arg in extra
_error("Unrecognized argument `$arg`.")
end
end

"""
disjunction(
model::Model,
disjunct_indicators::Vector{LogicalVariableRef}
name::String = ""
)

Function to add a [`Disjunction`](@ref) to a [`GDPModel`](@ref).

disjunction(
model::Model,
disjunct_indicators::Vector{LogicalVariableRef},
nested_tag::Disjunct,
name::String = ""
[nested_tag::Disjunct],
[name::String = ""];
[exactly1::Bool = true]
)

Function to add a nested [`Disjunction`](@ref) to a [`GDPModel`](@ref).
Create a disjunction comprised of disjuncts with indicator variables `disjunct_indicators`
and add it to `model`. For nested disjunctions, the `nested_tag` is required to indicate
which disjunct it will be part of in the parent disjunction. By default, `exactly1` adds
a constraint of the form `@constraint(model, disjunct_indicators in Exactly(1))` only
allowing one of the disjuncts to be selected; this is required for certain reformulations like
[`Hull`](@ref). For nested disjunctions, `exactly1` creates a constraint of the form
`@constraint(model, disjunct_indicators in Exactly(nested_tag.indicator))`.
To conveniently generate many disjunctions at once, see [`@disjunction`](@ref)
and [`@disjunctions`](@ref).
"""
function disjunction(
model::Model,
disjunct_indicators,
name::String = ""
) # TODO add kw argument to build exactly 1 constraint
return _disjunction(error, model, disjunct_indicators, name)
name::String = "",
extra...;
kwargs...
)
return _disjunction(error, model, disjunct_indicators, name, extra...; kwargs...)
end
function disjunction(
model::Model,
disjunct_indicators,
nested_tag::Disjunct,
name::String = ""
) # TODO add kw argument to build exactly 1 constraint
return _disjunction(error, model, disjunct_indicators, name, nested_tag)
name::String = "",
extra...;
kwargs...
)
return _disjunction(error, model, disjunct_indicators, name, nested_tag, extra...; kwargs...)
end

################################################################################
Expand Down
9 changes: 6 additions & 3 deletions src/datatypes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -390,9 +390,13 @@ mutable struct GDPData
disjunct_constraints::_MOIUC.CleverDict{DisjunctConstraintIndex, ConstraintData}
disjunctions::_MOIUC.CleverDict{DisjunctionIndex, ConstraintData{Disjunction}}

# Exactly one constraint mappings
exactly1_constraints::Dict{DisjunctionRef, LogicalConstraintRef}

# Indicator variable mappings
indicator_to_binary::Dict{LogicalVariableRef, VariableRef}
indicator_to_constraints::Dict{LogicalVariableRef, Vector{Union{DisjunctConstraintRef, DisjunctionRef}}}
constraint_to_indicator::Dict{Union{DisjunctConstraintRef, DisjunctionRef}, LogicalVariableRef} # needed for deletion

# Reformulation variables and constraints
reformulation_variables::Vector{VariableRef}
Expand All @@ -408,15 +412,14 @@ mutable struct GDPData
_MOIUC.CleverDict{LogicalConstraintIndex, ConstraintData}(),
_MOIUC.CleverDict{DisjunctConstraintIndex, ConstraintData}(),
_MOIUC.CleverDict{DisjunctionIndex, ConstraintData{Disjunction}}(),
Dict{DisjunctionRef, LogicalConstraintRef}(),
Dict{LogicalVariableRef, VariableRef}(),
Dict{LogicalVariableRef, Vector{Union{DisjunctConstraintRef, DisjunctionRef}}}(),
Dict{Union{DisjunctConstraintRef, DisjunctionRef}, LogicalVariableRef}(),
Vector{VariableRef}(),
Vector{ConstraintRef}(),
nothing,
false,
)
end
function GDPData(args...)
new(args...)
end
end
24 changes: 24 additions & 0 deletions src/hull.jl
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,30 @@ end
################################################################################
# HULL REFORMULATION
################################################################################
requires_exactly1(::Hull) = true

function _reformulate_disjunctions(model::Model, method::Hull)
_query_variable_bounds(model, method)
_reformulate_all_disjunctions(model, method)
end

function reformulate_disjunction(model::Model, disj::Disjunction, method::Hull)
ref_cons = Vector{AbstractConstraint}() #store reformulated constraints
disj_vrefs = _get_disjunction_variables(model, disj)
hull = _Hull(method, disj_vrefs)
for d in disj.indicators #reformulate each disjunct
_disaggregate_variables(model, d, disj_vrefs, hull) #disaggregate variables for that disjunct
_reformulate_disjunct(model, ref_cons, d, hull)
end
for vref in disj_vrefs #create sum constraint for disaggregated variables
_aggregate_variable(model, ref_cons, vref, hull)
end
return ref_cons
end
function reformulate_disjunction(model::Model, disj::Disjunction, method::_Hull)
return reformulate_disjunction(model, disj, Hull(method.value, method.variable_bounds))
end

function reformulate_disjunct_constraint(
model::Model,
con::ScalarConstraint{T, S},
Expand Down
4 changes: 3 additions & 1 deletion src/logic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,9 @@ end
function _reformulate_selector(model::Model, func, set::Union{_MOIAtLeast, _MOIAtMost, _MOIExactly})
dict = _indicator_to_binary(model)
bvrefs = [dict[lvref] for lvref in func[2:end]]
new_set = _vec_to_scalar_set(set)(func[1].constant)
# TODO better handle form of func[1]
c = first(func) isa Number ? first(func) : JuMP.constant(func[1])
new_set = _vec_to_scalar_set(set)(c)
cref = @constraint(model, sum(bvrefs) in new_set)
push!(_reformulation_constraints(model), cref)
end
Expand Down
Loading
Loading