diff --git a/README.md b/README.md index 8237cc3..171b7ec 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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] diff --git a/docs/src/api.md b/docs/src/api.md index d243fc9..4dcfdf5 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -2,5 +2,5 @@ ```@autodocs Modules = [DisjunctiveProgramming] -Order = [:type, :function] +Order = [:macro, :function, :type] ``` \ No newline at end of file diff --git a/docs/src/index.md b/docs/src/index.md index 22d382e..d8a4002 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -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: @@ -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] diff --git a/examples/ex1.jl b/examples/ex1.jl index 938b739..46a377c 100644 --- a/examples/ex1.jl +++ b/examples/ex1.jl @@ -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 diff --git a/examples/ex2.jl b/examples/ex2.jl index eb4876e..9700b0c 100644 --- a/examples/ex2.jl +++ b/examples/ex2.jl @@ -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] diff --git a/examples/ex3.jl b/examples/ex3.jl index 6bf5d3f..898d03c 100644 --- a/examples/ex3.jl +++ b/examples/ex3.jl @@ -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 diff --git a/examples/ex5.jl b/examples/ex5.jl index 2202f79..6863641 100644 --- a/examples/ex5.jl +++ b/examples/ex5.jl @@ -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()) diff --git a/examples/ex6.jl b/examples/ex6.jl index f86e73f..95e9423 100644 --- a/examples/ex6.jl +++ b/examples/ex6.jl @@ -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()) diff --git a/src/bigm.jl b/src/bigm.jl index 97d1d7f..7c8ed1a 100644 --- a/src/bigm.jl +++ b/src/bigm.jl @@ -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}, diff --git a/src/constraints.jl b/src/constraints.jl index 7510baf..375450b 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -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 @@ -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 @@ -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 @@ -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 @@ -308,9 +315,25 @@ 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 @@ -318,7 +341,8 @@ function _disjunction( _error::Function, model::Model, # TODO: generalize to AbstractModel structure, - name::String + name::String; + kwargs... ) _error("Unrecognized disjunction input structure.") end @@ -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 @@ -343,7 +382,8 @@ function _disjunction( model::Model, # TODO: generalize to AbstractModel structure, name::String, - extra... + extra...; + kwargs... ) for arg in extra _error("Unrecognized argument `$arg`.") @@ -351,37 +391,42 @@ function _disjunction( 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 ################################################################################ diff --git a/src/datatypes.jl b/src/datatypes.jl index 674f625..0d73bc1 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -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} @@ -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 diff --git a/src/hull.jl b/src/hull.jl index 7229b4e..c6dca98 100644 --- a/src/hull.jl +++ b/src/hull.jl @@ -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}, diff --git a/src/logic.jl b/src/logic.jl index a902351..e914476 100644 --- a/src/logic.jl +++ b/src/logic.jl @@ -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 diff --git a/src/macros.jl b/src/macros.jl index d2a7344..c54908d 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -144,7 +144,10 @@ 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 create +nested disjunctions. The recognized keyword arguments in `kw_args` are the following: - `base_name`: Sets the name prefix used to generate constraint names. @@ -152,6 +155,10 @@ The recognized keyword arguments in `kw_args` are the following: 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 a constraint should be added to + only allow selecting one disjunct in the disjunction. + +To create disjunctions without macros, see [`disjunction`](@ref). """ macro disjunction(model, args...) # prepare the model diff --git a/src/model.jl b/src/model.jl index e53984e..4568221 100644 --- a/src/model.jl +++ b/src/model.jl @@ -54,12 +54,14 @@ _logical_variables(model::Model) = gdp_data(model).logical_variables _logical_constraints(model::Model) = gdp_data(model).logical_constraints _disjunct_constraints(model::Model) = gdp_data(model).disjunct_constraints _disjunctions(model::Model) = gdp_data(model).disjunctions +_exactly1_constraints(model::Model) = gdp_data(model).exactly1_constraints _indicator_to_binary(model::Model) = gdp_data(model).indicator_to_binary _indicator_to_constraints(model::Model) = gdp_data(model).indicator_to_constraints +_constraint_to_indicator(model::Model) = gdp_data(model).constraint_to_indicator _reformulation_variables(model::Model) = gdp_data(model).reformulation_variables _reformulation_constraints(model::Model) = gdp_data(model).reformulation_constraints -_ready_to_optimize(model::Model) = gdp_data(model).ready_to_optimize # Determine if the model is ready to call `optimize!` without a optimize hook _solution_method(model::Model) = gdp_data(model).solution_method # Get the current solution method +_ready_to_optimize(model::Model) = gdp_data(model).ready_to_optimize # Determine if the model is ready to call `optimize!` without a optimize hook # Update the ready_to_optimize field function _set_ready_to_optimize(model::Model, is_ready::Bool) diff --git a/src/reformulate.jl b/src/reformulate.jl index 031a148..02851ce 100644 --- a/src/reformulate.jl +++ b/src/reformulate.jl @@ -46,10 +46,24 @@ end ################################################################################ # DISJUNCTIONS ################################################################################ +""" + requires_exactly1(method::AbstractReformulationMethod) + +Return a `Bool` whether `method` requires that `Exactly 1` disjunct be selected +as true for each disjunction. For new reformulation method types, this should be +extended to return `true` if such a constraint is required (defaults to `false` otherwise). +""" +requires_exactly1(::AbstractReformulationMethod) = false + # disjunctions function _reformulate_all_disjunctions(model::Model, method::AbstractReformulationMethod) - for (_, disj) in _disjunctions(model) + for (idx, disj) in _disjunctions(model) disj.constraint.nested && continue #only reformulate top level disjunctions + dref = DisjunctionRef(model, idx) + if requires_exactly1(method) && !haskey(gdp_data(model).exactly1_constraints, dref) + error("Reformulation method `$method` requires disjunctions where only 1 disjunct is selected, " * + "but `exactly1 = false` for disjunction `$dref`.") + end ref_cons = reformulate_disjunction(model, disj.constraint, method) for (i, ref_con) in enumerate(ref_cons) name = isempty(disj.name) ? "" : string(disj.name,"_$i") @@ -61,14 +75,6 @@ end function _reformulate_disjunctions(model::Model, method::AbstractReformulationMethod) _reformulate_all_disjunctions(model, method) end -function _reformulate_disjunctions(model::Model, method::BigM) - method.tighten && _query_variable_bounds(model, method) - _reformulate_all_disjunctions(model, method) -end -function _reformulate_disjunctions(model::Model, method::Hull) - _query_variable_bounds(model, method) - _reformulate_all_disjunctions(model, method) -end # disjuncts """ @@ -84,7 +90,6 @@ Reformulate a disjunction using the specified `method`. Current reformulation me The `disj` field is the `ConstraintData` object for the disjunction, stored in the `disjunctions` field of the `GDPData` object. """ -# generic fallback (e.g., BigM, Indicator) function reformulate_disjunction(model::Model, disj::Disjunction, method::AbstractReformulationMethod) ref_cons = Vector{AbstractConstraint}() #store reformulated constraints for d in disj.indicators @@ -92,23 +97,6 @@ function reformulate_disjunction(model::Model, disj::Disjunction, method::Abstra end return ref_cons end -# hull specific -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 # individual disjuncts function _reformulate_disjunct(model::Model, ref_cons::Vector{AbstractConstraint}, lvref::LogicalVariableRef, method::AbstractReformulationMethod) @@ -122,8 +110,18 @@ function _reformulate_disjunct(model::Model, ref_cons::Vector{AbstractConstraint return end -# reformulation for nested disjunction -# NOTE: name of inner disjunction (if given) is currently lost (not passed upwards) +""" + reformulate_disjunct_constraint( + model::JuMP.Model, + con::JuMP.AbstractConstraint, + bvref::JuMP.VariableRef, + method::AbstractReformulationMethod + ) + +Extension point for reformulation method `method` to reformulate disjunction constraint `con` over each +constraint. If `method` needs to specify how to reformulate the entire disjunction, see +[`reformulate_disjunction`](@ref). +""" function reformulate_disjunct_constraint( model::Model, con::Disjunction, diff --git a/test/constraints/bigm.jl b/test/constraints/bigm.jl index f097228..7f22ef2 100644 --- a/test/constraints/bigm.jl +++ b/test/constraints/bigm.jl @@ -288,10 +288,10 @@ function test_nested_bigm() @variable(model, z[1:2], Logical) @constraint(model, x <= 5, Disjunct(y[1])) @constraint(model, x >= 5, Disjunct(y[2])) - @disjunction(model, inner, y, Disjunct(z[1])) + @disjunction(model, inner, y, Disjunct(z[1]), exactly1 = false) @constraint(model, x <= 10, Disjunct(z[1])) @constraint(model, x >= 10, Disjunct(z[2])) - @disjunction(model, outer, z) + @disjunction(model, outer, z, exactly1 = false) reformulate_model(model, BigM()) bvrefs = DP._indicator_to_binary(model) diff --git a/test/constraints/disjunct.jl b/test/constraints/disjunct.jl index 91f79f6..b717c42 100644 --- a/test/constraints/disjunct.jl +++ b/test/constraints/disjunct.jl @@ -26,6 +26,7 @@ function test_disjunct_add_success() @test haskey(DP._indicator_to_constraints(model), y) @test DP._indicator_to_constraints(model)[y] == [c1, c2] @test DP._disjunct_constraints(model)[index(c1)] == DP._constraint_data(c1) + @test DP.gdp_data(model).constraint_to_indicator[c1] == y @test constraint_object(c1).set == MOI.EqualTo(1.0) @test constraint_object(c1).func == constraint_object(c2).func == 1x @test constraint_object(c1).set == constraint_object(c2).set @@ -85,6 +86,8 @@ function test_disjunct_delete() @test_throws AssertionError delete(GDPModel(), c1) delete(model, c1) @test !haskey(gdp_data(model).disjunct_constraints, index(c1)) + @test !haskey(gdp_data(model).constraint_to_indicator, c1) + @test !(c1 in gdp_data(model).indicator_to_constraints[y]) @test !DP._ready_to_optimize(model) end diff --git a/test/constraints/disjunction.jl b/test/constraints/disjunction.jl index 9804ba8..2dccc0d 100644 --- a/test/constraints/disjunction.jl +++ b/test/constraints/disjunction.jl @@ -35,6 +35,11 @@ function test_disjunction_add_fail() @test_macro_throws ErrorException @disjunction(model, "bad"[i=1:2], y) #wrong expression for disjunction name @test_macro_throws ErrorException @disjunction(model, [model=1:2], y) #index name can't be same as model name + + @test_throws ErrorException disjunction(model, y, bad_key = 42) + @variable(model, w[1:3], Logical) + @constraint(model, [i = 1:2], x == 5, Disjunct(w[i])) + @test_throws ErrorException disjunction(model, w, Disjunct(w[3]), bad_key = 42) end function test_disjunction_add_success() @@ -43,8 +48,10 @@ function test_disjunction_add_success() @variable(model, y[1:2], Logical) @constraint(model, x == 5, Disjunct(y[1])) @constraint(model, x == 10, Disjunct(y[2])) - disj = @disjunction(model, y) - @disjunction(model, disj2, y) + disj = DisjunctionRef(model, DisjunctionIndex(1)) + disj2 = DisjunctionRef(model, DisjunctionIndex(2)) + @test @disjunction(model, y) == disj + @test @disjunction(model, disj2, y, exactly1 = false) == disj2 @test owner_model(disj) == model @test is_valid(model, disj) @test index(disj) == DisjunctionIndex(1) @@ -55,6 +62,8 @@ function test_disjunction_add_success() @test DP._disjunctions(model)[index(disj)] == DP._constraint_data(disj) @test !constraint_object(disj).nested @test constraint_object(disj).indicators == y + @test haskey(gdp_data(model).exactly1_constraints, disj) + @test !haskey(gdp_data(model).exactly1_constraints, disj2) @test disj == copy(disj) end @@ -65,10 +74,12 @@ function test_disjunction_add_nested() @variable(model, z[1:2], Logical) @constraint(model, x <= 5, Disjunct(y[1])) @constraint(model, x >= 5, Disjunct(y[2])) - @disjunction(model, inner, y, Disjunct(z[1])) + inner = DisjunctionRef(model, DisjunctionIndex(1)) + @test @disjunction(model, inner, y, Disjunct(z[1])) == inner @constraint(model, x <= 10, Disjunct(z[1])) @constraint(model, x >= 10, Disjunct(z[2])) - @disjunction(model, outer, z) + outer = DisjunctionRef(model, DisjunctionIndex(2)) + @test @disjunction(model, outer, z) == outer @test is_valid(model, inner) @test is_valid(model, outer) @@ -78,6 +89,7 @@ function test_disjunction_add_nested() @test !constraint_object(outer).nested @test haskey(DP._indicator_to_constraints(model), z[1]) @test inner in DP._indicator_to_constraints(model)[z[1]] + @test haskey(gdp_data(model).exactly1_constraints, inner) end function test_disjunction_add_array() @@ -87,7 +99,6 @@ function test_disjunction_add_array() @constraint(model, con[i=1:2, j=1:3, k=1:4], x==i+j+k, Disjunct(y[i,j,k])) @disjunction(model, disj[i=1:2, j=1:3], y[i,j,:]) - @test disj isa Matrix{DisjunctionRef} @test length(disj) == 6 @test all(is_valid.(model, disj)) end @@ -101,7 +112,6 @@ function test_disjunciton_add_dense_axis() @constraint(model, con[i=I, j=J, k=1:4], x==k, Disjunct(y[i,j,k])) @disjunction(model, disj[i=I, j=J], y[i,j,:]) - @test disj isa Containers.DenseAxisArray @test disj.axes[1] == ["a","b","c"] @test disj.axes[2] == [1,2] @test disj.data isa Matrix{DisjunctionRef} @@ -114,7 +124,6 @@ function test_disjunction_add_sparse_axis() @constraint(model, con[i=1:3, j=1:3, k=1:4; j > i], x==i+j+k, Disjunct(y[i,j,k])) @disjunction(model, disj[i=1:3, j=1:3; j > i], y[i,j,:]) - @test disj isa Containers.SparseAxisArray @test length(disj) == 3 @test disj.names == (:i, :j) @test Set(keys(disj.data)) == Set([(1,2),(1,3),(2,3)]) @@ -177,9 +186,23 @@ function test_disjunction_delete() @disjunction(model, disj, y) @test_throws AssertionError delete(GDPModel(), disj) - delete(model, disj) + @test delete(model, disj) isa Nothing @test !haskey(gdp_data(model).disjunctions, index(disj)) @test !DP._ready_to_optimize(model) + @test !haskey(gdp_data(model).exactly1_constraints, disj) + + model = GDPModel() + @variable(model, x) + @variable(model, y[1:2], Logical) + @variable(model, z[1:2], Logical) + @constraint(model, x <= 5, Disjunct(y[1])) + @constraint(model, x >= 5, Disjunct(y[2])) + @disjunction(model, inner, y, Disjunct(z[1]), exactly1 = false) + + @test delete(model, inner) isa Nothing + @test !haskey(gdp_data(model).disjunctions, index(inner)) + @test !haskey(gdp_data(model).constraint_to_indicator, disj) + @test !(inner in gdp_data(model).indicator_to_constraints[z[1]]) end function test_disjunction_function() @@ -188,13 +211,15 @@ function test_disjunction_function() @variable(model, y[1:2], Logical) @constraint(model, x == 5, Disjunct(y[1])) @constraint(model, x == 10, Disjunct(y[2])) - disj = disjunction(model, y, "name") + disj = DisjunctionRef(model, DisjunctionIndex(1)) + @test disjunction(model, y, "name") == disj @test is_valid(model, disj) @test name(disj) == "name" set_name(disj, "new_name") @test name(disj) == "new_name" @test haskey(DP._disjunctions(model), index(disj)) + @test haskey(gdp_data(model).exactly1_constraints, disj) end function test_disjunction_function_nested() @@ -206,8 +231,10 @@ function test_disjunction_function_nested() @constraint(model, x >= 5, Disjunct(y[2])) @constraint(model, x <= 10, Disjunct(z[1])) @constraint(model, x >= 10, Disjunct(z[2])) - disj1 = disjunction(model, y, Disjunct(z[1]), "inner") - disj2 = disjunction(model, z, "outer") + disj1 = DisjunctionRef(model, DisjunctionIndex(1)) + disj2 = DisjunctionRef(model, DisjunctionIndex(2)) + @test disjunction(model, y, Disjunct(z[1]), "inner", exactly1 = false) == disj1 + @test disjunction(model, z, "outer") == disj2 @test is_valid(model, disj1) @test is_valid(model, disj2) @@ -217,6 +244,7 @@ function test_disjunction_function_nested() @test !constraint_object(disj2).nested @test haskey(DP._indicator_to_constraints(model), z[1]) @test disj1 in DP._indicator_to_constraints(model)[z[1]] + @test !haskey(gdp_data(model).exactly1_constraints, disj1) end @testset "Disjunction" begin diff --git a/test/constraints/fallback.jl b/test/constraints/fallback.jl index 9e256fb..3e53cdc 100644 --- a/test/constraints/fallback.jl +++ b/test/constraints/fallback.jl @@ -5,6 +5,11 @@ function test_reformulate_disjunct_constraint_fallback() @test_throws ErrorException reformulate_disjunct_constraint(model, c, x, DummyReformulation()) end +function test_exactly1_fallback() + @test requires_exactly1(BigM()) == false +end + @testset "Fallbacks" begin test_reformulate_disjunct_constraint_fallback() + test_exactly1_fallback() end \ No newline at end of file diff --git a/test/constraints/hull.jl b/test/constraints/hull.jl index 8eca869..3da6488 100644 --- a/test/constraints/hull.jl +++ b/test/constraints/hull.jl @@ -636,6 +636,17 @@ function test_scalar_nonlinear_hull_2sided() end end +function test_exactly1_error() + model = GDPModel() + @variable(model, 10 <= x <= 100) + @variable(model, z[1:2], Logical) + @constraint(model, 1 <= x <= 5, Disjunct(z[1])) + @constraint(model, 3 <= x <= 5, Disjunct(z[2])) + disjunction(model, z, exactly1 = false) + @test requires_exactly1(Hull()) + @test_throws ErrorException reformulate_model(model, Hull()) +end + @testset "Hull Reformulation" begin test_default_hull() test_set_hull() @@ -675,4 +686,5 @@ end test_scalar_quadratic_hull_2sided() test_scalar_nonlinear_hull_2sided() test_scalar_nonlinear_hull_2sided_error() + test_exactly1_error() end \ No newline at end of file diff --git a/test/constraints/indicator.jl b/test/constraints/indicator.jl index 14a29c9..0566988 100644 --- a/test/constraints/indicator.jl +++ b/test/constraints/indicator.jl @@ -13,10 +13,10 @@ function test_indicator_scalar_constraints() ref_cons = DP._reformulation_constraints(model) ref_cons_obj = constraint_object.(ref_cons) - @test length(ref_cons) == 6 + @test length(ref_cons) == 7 @test all(is_valid.(model, ref_cons)) - @test all(isa.(ref_cons_obj, VectorConstraint)) - @test all([cobj.set isa MOI.Indicator for cobj in ref_cons_obj]) + @test all(isa.(ref_cons_obj[1:6], VectorConstraint)) + @test all([cobj.set isa MOI.Indicator for cobj in ref_cons_obj[1:6]]) end function test_indicator_vector_constraints() @@ -32,10 +32,10 @@ function test_indicator_vector_constraints() ref_cons = DP._reformulation_constraints(model) ref_cons_obj = constraint_object.(ref_cons) - @test length(ref_cons) == 6 + @test length(ref_cons) == 7 @test all(is_valid.(model, ref_cons)) - @test all(isa.(ref_cons_obj, VectorConstraint)) - @test all([cobj.set isa MOI.Indicator for cobj in ref_cons_obj]) + @test all(isa.(ref_cons_obj[1:6], VectorConstraint)) + @test all([cobj.set isa MOI.Indicator for cobj in ref_cons_obj[1:6]]) end function test_indicator_array() @@ -44,7 +44,7 @@ function test_indicator_array() @variable(model, y[1:2], Logical) @constraint(model, [1:3, 1:2], x <= 6, Disjunct(y[1])) @constraint(model, [1:3, 1:2], x >= 6, Disjunct(y[2])) - @disjunction(model, y) + @disjunction(model, y, exactly1 = false) reformulate_model(model, Indicator()) ref_cons = DP._reformulation_constraints(model) @@ -61,7 +61,7 @@ function test_indicator_dense_axis() @variable(model, y[1:2], Logical) @constraint(model, [["a","b","c"],[1,2]], x <= 7, Disjunct(y[1])) @constraint(model, [["a","b","c"],[1,2]], x >= 7, Disjunct(y[2])) - @disjunction(model, y) + @disjunction(model, y, exactly1 = false) reformulate_model(model, Indicator()) ref_cons = DP._reformulation_constraints(model) @@ -78,7 +78,7 @@ function test_indicator_sparse_axis() @variable(model, y[1:2], Logical) @constraint(model, [i = 1:3, j = 1:3; j > i], x <= 7, Disjunct(y[1])) @constraint(model, [i = 1:3, j = 1:3; j > i], x >= 7, Disjunct(y[2])) - @disjunction(model, y) + @disjunction(model, y, exactly1 = false) reformulate_model(model, Indicator()) ref_cons = DP._reformulation_constraints(model) @@ -96,10 +96,10 @@ function test_indicator_nested() @variable(model, z[1:2], Logical) @constraint(model, x <= 5, Disjunct(y[1])) @constraint(model, x >= 5, Disjunct(y[2])) - @disjunction(model, y, Disjunct(z[1])) + @disjunction(model, y, Disjunct(z[1]), exactly1 = false) @constraint(model, x <= 10, Disjunct(z[1])) @constraint(model, x >= 10, Disjunct(z[2])) - @disjunction(model, z) + @disjunction(model, z, exactly1 = false) reformulate_model(model, Indicator()) ref_cons = DP._reformulation_constraints(model) diff --git a/test/model.jl b/test/model.jl index efbbd26..87aa078 100644 --- a/test/model.jl +++ b/test/model.jl @@ -1,19 +1,7 @@ using HiGHS function test_GDPData() - gdpdata = GDPData( - DP._MOIUC.CleverDict{LogicalVariableIndex, LogicalVariableData}(), - DP._MOIUC.CleverDict{LogicalConstraintIndex, ConstraintData}(), - DP._MOIUC.CleverDict{DisjunctConstraintIndex, ConstraintData}(), - DP._MOIUC.CleverDict{DisjunctionIndex, ConstraintData{Disjunction}}(), - Dict{LogicalVariableRef, JuMP.VariableRef}(), - Dict{LogicalVariableRef, Vector{Union{DisjunctConstraintRef, DisjunctionRef}}}(), - Vector{JuMP.VariableRef}(), - Vector{JuMP.ConstraintRef}(), - nothing, - false - ) - gdpdata isa GDPData + @test GDPData() isa GDPData end function test_empty_model() diff --git a/test/solve.jl b/test/solve.jl index 0cc2edb..283c77c 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -13,8 +13,6 @@ function test_linear_gdp_example() @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])) optimize!(m, method = BigM()) @test termination_status(m) == MOI.OPTIMAL