Skip to content

Commit

Permalink
Enable exactly1 option and fix bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
pulsipher committed Oct 26, 2023
1 parent 3af182e commit c054578
Show file tree
Hide file tree
Showing 22 changed files with 228 additions and 119 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,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
1 change: 0 additions & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,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
109 changes: 76 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,49 @@ 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))` making
the disjuncts exclusive to one another; this is required for certain reformulations like
[`Hull`](@ref). 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
6 changes: 6 additions & 0 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,8 +412,10 @@ 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,
Expand Down
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
9 changes: 8 additions & 1 deletion src/macros.jl
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,21 @@ which must be a `Vector` of `LogicalVariableRef`s.
@disjunction(model, ref[i=..., j=..., ...], expr, kw_args...)
Add a group of disjunction described by the expression `expr` parameterized
by `i`, `j`, ..., which must be a `Vector` of `LogicalVariableRef`s.
by `i`, `j`, ..., which must be a `Vector` of `LogicalVariableRef`s.
In both of the above calls, a [`Disjunct`](@ref) tag can be added to created
nested disjunctions.
The recognized keyword arguments in `kw_args` are the following:
- `base_name`: Sets the name prefix used to generate constraint names.
It corresponds to the constraint name for scalar constraints, otherwise,
the constraint names are set to `base_name[...]` for each index `...`
of the axes `axes`.
- `container`: Specify the container type.
- `exactly1`: Specify a `Bool` whether an exactly one constraint for the indicator
variables should be added.
To create disjunctions without macros, see [`disjunction`](@ref).
"""
macro disjunction(model, args...)
# prepare the model
Expand Down
Loading

0 comments on commit c054578

Please sign in to comment.