From c054578ce9f72bc52515477482840b5f999d6286 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Thu, 26 Oct 2023 17:50:31 -0400 Subject: [PATCH 1/8] Enable `exactly1` option and fix bugs --- README.md | 1 - docs/src/index.md | 1 - examples/ex1.jl | 3 +- examples/ex2.jl | 3 +- examples/ex3.jl | 3 +- examples/ex5.jl | 2 - examples/ex6.jl | 9 +-- src/bigm.jl | 5 ++ src/constraints.jl | 109 ++++++++++++++++++++++---------- src/datatypes.jl | 6 ++ src/hull.jl | 24 +++++++ src/logic.jl | 4 +- src/macros.jl | 9 ++- src/reformulate.jl | 56 ++++++++-------- test/constraints/bigm.jl | 4 +- test/constraints/disjunct.jl | 3 + test/constraints/disjunction.jl | 50 +++++++++++---- test/constraints/fallback.jl | 5 ++ test/constraints/hull.jl | 12 ++++ test/constraints/indicator.jl | 22 +++---- test/model.jl | 14 +--- test/solve.jl | 2 - 22 files changed, 228 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index 8237cc3..9ac3075 100644 --- a/README.md +++ b/README.md @@ -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] diff --git a/docs/src/index.md b/docs/src/index.md index 22d382e..0276f74 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -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] 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..9693a7e 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,40 @@ 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))` 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 ################################################################################ diff --git a/src/datatypes.jl b/src/datatypes.jl index 674f625..9b50c9e 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,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, 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..0ef00fe 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 created +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 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 diff --git a/src/reformulate.jl b/src/reformulate.jl index 031a148..ffaf70a 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 an exactly one constraint for each +disjunction. For new reformulation method types, this should be extended to +in case 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 exactly one constraints for " * + "disjunctions, 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 reformulate the whole disjunction simultaneously, 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 From e5f686f64d11ebb82acb862d28329650292fe737 Mon Sep 17 00:00:00 2001 From: pulsipher Date: Fri, 27 Oct 2023 10:28:00 -0400 Subject: [PATCH 2/8] Fix docstrings and remove unnecessary GDPData method --- docs/src/api.md | 2 +- src/datatypes.jl | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) 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/src/datatypes.jl b/src/datatypes.jl index 9b50c9e..0d73bc1 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -422,7 +422,4 @@ mutable struct GDPData false, ) end - function GDPData(args...) - new(args...) - end end From a7f706499438db63124a373783158c572e14f49a Mon Sep 17 00:00:00 2001 From: pulsipher Date: Fri, 27 Oct 2023 16:14:52 -0400 Subject: [PATCH 3/8] rename to exclusive --- src/constraints.jl | 28 +++++++++++++++------------- src/datatypes.jl | 2 +- src/hull.jl | 2 +- src/macros.jl | 4 ++-- src/reformulate.jl | 17 +++++++++-------- test/constraints/bigm.jl | 4 ++-- test/constraints/disjunction.jl | 18 +++++++++--------- test/constraints/fallback.jl | 6 +++--- test/constraints/hull.jl | 8 ++++---- test/constraints/indicator.jl | 10 +++++----- 10 files changed, 51 insertions(+), 48 deletions(-) diff --git a/src/constraints.jl b/src/constraints.jl index 9693a7e..1377be9 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -117,10 +117,10 @@ function JuMP.delete(model::Model, cref::DisjunctionRef) 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) + exclusive_dict = gdp_data(model).exclusive_constraints + if haskey(exclusive_dict, cref) + JuMP.delete(model, exclusive_dict[cref]) + delete!(exclusive_dict, cref) end _set_ready_to_optimize(model, false) return @@ -316,7 +316,7 @@ function _disjunction( model::Model, # TODO: generalize to AbstractModel structure::AbstractVector, #generalize for containers name::String; - exactly1::Bool = true, + exclusive::Bool = true, extra_kwargs... ) # check for unneeded keywords @@ -326,12 +326,12 @@ function _disjunction( # create the disjunction dref = _create_disjunction(_error, model, structure, name, false) # add the exactly one constraint if desired - if exactly1 + if exclusive 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 + gdp_data(model).exclusive_constraints[dref] = cref end return dref end @@ -354,7 +354,7 @@ function _disjunction( structure, name::String, tag::Disjunct; - exactly1::Bool = true, + exclusive::Bool = true, extra_kwargs... ) # check for unneeded keywords @@ -366,12 +366,12 @@ function _disjunction( obj = constraint_object(dref) _add_indicator_var(_DisjunctConstraint(obj, tag.indicator), dref, model) # add the exactly one constraint if desired - if exactly1 + if exclusive 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 + gdp_data(model).exclusive_constraints[dref] = cref end return dref end @@ -396,15 +396,17 @@ end disjunct_indicators::Vector{LogicalVariableRef}, [nested_tag::Disjunct], [name::String = ""]; - [exactly1::Bool = true] + [exclusive::Bool = true] ) 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 +which disjunct it will be part of in the parent disjunction. By default, `exclusive` 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) +[`Hull`](@ref). For nested disjunctions, `exclusive` 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( diff --git a/src/datatypes.jl b/src/datatypes.jl index 0d73bc1..9b81292 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -391,7 +391,7 @@ mutable struct GDPData disjunctions::_MOIUC.CleverDict{DisjunctionIndex, ConstraintData{Disjunction}} # Exactly one constraint mappings - exactly1_constraints::Dict{DisjunctionRef, LogicalConstraintRef} + exclusive_constraints::Dict{DisjunctionRef, LogicalConstraintRef} # Indicator variable mappings indicator_to_binary::Dict{LogicalVariableRef, VariableRef} diff --git a/src/hull.jl b/src/hull.jl index c6dca98..6f0a901 100644 --- a/src/hull.jl +++ b/src/hull.jl @@ -149,7 +149,7 @@ end ################################################################################ # HULL REFORMULATION ################################################################################ -requires_exactly1(::Hull) = true +requires_exclusive(::Hull) = true function _reformulate_disjunctions(model::Model, method::Hull) _query_variable_bounds(model, method) diff --git a/src/macros.jl b/src/macros.jl index 0ef00fe..12be2aa 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -155,8 +155,8 @@ 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 an exactly one constraint for the indicator - variables should be added. +- `exclusive`: Specify a `Bool` whether an constraint should be added to make + the disjuncts strictly exlcusive of one another. To create disjunctions without macros, see [`disjunction`](@ref). """ diff --git a/src/reformulate.jl b/src/reformulate.jl index ffaf70a..6425f6b 100644 --- a/src/reformulate.jl +++ b/src/reformulate.jl @@ -47,22 +47,23 @@ end # DISJUNCTIONS ################################################################################ """ - requires_exactly1(method::AbstractReformulationMethod) + requires_exclusive(method::AbstractReformulationMethod) -Return a `Bool` whether `method` requires an exactly one constraint for each -disjunction. For new reformulation method types, this should be extended to -in case such a constraint is required (defaults to `false` otherwise). +Return a `Bool` whether `method` requires an the disjuncts to be exactly exclusive +for each disjunction (i.e., it requires an exactly one constraint). For new +reformulation method types, this should be extended to in case such a constraint +is required (defaults to `false` otherwise). """ -requires_exactly1(::AbstractReformulationMethod) = false +requires_exclusive(::AbstractReformulationMethod) = false # disjunctions function _reformulate_all_disjunctions(model::Model, method::AbstractReformulationMethod) 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 exactly one constraints for " * - "disjunctions, but `exactly1 = false` for disjunction `$dref`.") + if requires_exclusive(method) && !haskey(gdp_data(model).exclusive_constraints, dref) + error("Reformulation method `$method` requires exclusive disjuncts for " * + "disjunctions, but `exclusive = false` for disjunction `$dref`.") end ref_cons = reformulate_disjunction(model, disj.constraint, method) for (i, ref_con) in enumerate(ref_cons) diff --git a/test/constraints/bigm.jl b/test/constraints/bigm.jl index 7f22ef2..8ec7920 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]), exactly1 = false) + @disjunction(model, inner, y, Disjunct(z[1]), exclusive = false) @constraint(model, x <= 10, Disjunct(z[1])) @constraint(model, x >= 10, Disjunct(z[2])) - @disjunction(model, outer, z, exactly1 = false) + @disjunction(model, outer, z, exclusive = false) reformulate_model(model, BigM()) bvrefs = DP._indicator_to_binary(model) diff --git a/test/constraints/disjunction.jl b/test/constraints/disjunction.jl index 2dccc0d..b2753e6 100644 --- a/test/constraints/disjunction.jl +++ b/test/constraints/disjunction.jl @@ -51,7 +51,7 @@ function test_disjunction_add_success() disj = DisjunctionRef(model, DisjunctionIndex(1)) disj2 = DisjunctionRef(model, DisjunctionIndex(2)) @test @disjunction(model, y) == disj - @test @disjunction(model, disj2, y, exactly1 = false) == disj2 + @test @disjunction(model, disj2, y, exclusive = false) == disj2 @test owner_model(disj) == model @test is_valid(model, disj) @test index(disj) == DisjunctionIndex(1) @@ -62,8 +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 haskey(gdp_data(model).exclusive_constraints, disj) + @test !haskey(gdp_data(model).exclusive_constraints, disj2) @test disj == copy(disj) end @@ -89,7 +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) + @test haskey(gdp_data(model).exclusive_constraints, inner) end function test_disjunction_add_array() @@ -189,7 +189,7 @@ function test_disjunction_delete() @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) + @test !haskey(gdp_data(model).exclusive_constraints, disj) model = GDPModel() @variable(model, x) @@ -197,7 +197,7 @@ function test_disjunction_delete() @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) + @disjunction(model, inner, y, Disjunct(z[1]), exclusive = false) @test delete(model, inner) isa Nothing @test !haskey(gdp_data(model).disjunctions, index(inner)) @@ -219,7 +219,7 @@ function test_disjunction_function() 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) + @test haskey(gdp_data(model).exclusive_constraints, disj) end function test_disjunction_function_nested() @@ -233,7 +233,7 @@ function test_disjunction_function_nested() @constraint(model, x >= 10, Disjunct(z[2])) disj1 = DisjunctionRef(model, DisjunctionIndex(1)) disj2 = DisjunctionRef(model, DisjunctionIndex(2)) - @test disjunction(model, y, Disjunct(z[1]), "inner", exactly1 = false) == disj1 + @test disjunction(model, y, Disjunct(z[1]), "inner", exclusive = false) == disj1 @test disjunction(model, z, "outer") == disj2 @test is_valid(model, disj1) @@ -244,7 +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) + @test !haskey(gdp_data(model).exclusive_constraints, disj1) end @testset "Disjunction" begin diff --git a/test/constraints/fallback.jl b/test/constraints/fallback.jl index 3e53cdc..e25bcea 100644 --- a/test/constraints/fallback.jl +++ b/test/constraints/fallback.jl @@ -5,11 +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 +function test_exclusive_fallback() + @test requires_exclusive(BigM()) == false end @testset "Fallbacks" begin test_reformulate_disjunct_constraint_fallback() - test_exactly1_fallback() + test_exclusive_fallback() end \ No newline at end of file diff --git a/test/constraints/hull.jl b/test/constraints/hull.jl index 3da6488..934143a 100644 --- a/test/constraints/hull.jl +++ b/test/constraints/hull.jl @@ -636,14 +636,14 @@ function test_scalar_nonlinear_hull_2sided() end end -function test_exactly1_error() +function test_exclusive_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()) + disjunction(model, z, exclusive = false) + @test requires_exclusive(Hull()) @test_throws ErrorException reformulate_model(model, Hull()) end @@ -686,5 +686,5 @@ end test_scalar_quadratic_hull_2sided() test_scalar_nonlinear_hull_2sided() test_scalar_nonlinear_hull_2sided_error() - test_exactly1_error() + test_exclusive_error() end \ No newline at end of file diff --git a/test/constraints/indicator.jl b/test/constraints/indicator.jl index 0566988..729b8ea 100644 --- a/test/constraints/indicator.jl +++ b/test/constraints/indicator.jl @@ -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, exactly1 = false) + @disjunction(model, y, exclusive = 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, exactly1 = false) + @disjunction(model, y, exclusive = 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, exactly1 = false) + @disjunction(model, y, exclusive = 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]), exactly1 = false) + @disjunction(model, y, Disjunct(z[1]), exclusive = false) @constraint(model, x <= 10, Disjunct(z[1])) @constraint(model, x >= 10, Disjunct(z[2])) - @disjunction(model, z, exactly1 = false) + @disjunction(model, z, exclusive = false) reformulate_model(model, Indicator()) ref_cons = DP._reformulation_constraints(model) From 335a13d58eecb00bf6784e00cad390e352b7ad61 Mon Sep 17 00:00:00 2001 From: Hector Perez Date: Sat, 28 Oct 2023 08:41:42 -0400 Subject: [PATCH 4/8] update readme and docstrings --- README.md | 2 ++ src/macros.jl | 4 ++-- src/reformulate.jl | 7 +++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9ac3075..4d81bfa 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 `exclusive` to `false` in the `@disjunction` macro. + ## MIP Reformulations The following reformulation methods are currently supported: diff --git a/src/macros.jl b/src/macros.jl index 12be2aa..abde488 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -155,8 +155,8 @@ 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. -- `exclusive`: Specify a `Bool` whether an constraint should be added to make - the disjuncts strictly exlcusive of one another. +- `exclusive`: 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). """ diff --git a/src/reformulate.jl b/src/reformulate.jl index 6425f6b..6ddcd69 100644 --- a/src/reformulate.jl +++ b/src/reformulate.jl @@ -49,10 +49,9 @@ end """ requires_exclusive(method::AbstractReformulationMethod) -Return a `Bool` whether `method` requires an the disjuncts to be exactly exclusive -for each disjunction (i.e., it requires an exactly one constraint). For new -reformulation method types, this should be extended to in case such a constraint -is required (defaults to `false` otherwise). +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_exclusive(::AbstractReformulationMethod) = false From 2aa7118f2565732a85c2e2309233472442ebed80 Mon Sep 17 00:00:00 2001 From: Hector Perez Date: Sat, 28 Oct 2023 08:56:36 -0400 Subject: [PATCH 5/8] minor changes to docstrings --- src/macros.jl | 2 +- src/reformulate.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/macros.jl b/src/macros.jl index abde488..bde94c5 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -146,7 +146,7 @@ which must be a `Vector` of `LogicalVariableRef`s. Add a group of disjunction described by the expression `expr` parameterized 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 +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: diff --git a/src/reformulate.jl b/src/reformulate.jl index 6ddcd69..ce651cc 100644 --- a/src/reformulate.jl +++ b/src/reformulate.jl @@ -119,7 +119,7 @@ end ) Extension point for reformulation method `method` to reformulate disjunction constraint `con` over each -constraint. If `method` needs to reformulate the whole disjunction simultaneously, see +constraint. If `method` needs to specify how to reformulate the entire disjunction, see [`reformulate_disjunction`](@ref). """ function reformulate_disjunct_constraint( From b511f470728900cef720f6e8b90af102d7495107 Mon Sep 17 00:00:00 2001 From: Hector Perez Date: Sat, 28 Oct 2023 09:14:20 -0400 Subject: [PATCH 6/8] update docstrings --- src/constraints.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/constraints.jl b/src/constraints.jl index 1377be9..f246ec6 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -402,8 +402,8 @@ end 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, `exclusive` 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 +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, `exclusive` 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) From 36751782c8fcbd127f34d026a9649adc452e3f89 Mon Sep 17 00:00:00 2001 From: Hector Perez Date: Sat, 28 Oct 2023 09:17:03 -0400 Subject: [PATCH 7/8] update doc to match readme --- docs/src/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/index.md b/docs/src/index.md index 0276f74..9a683ba 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 `exclusive` to `false` in the `@disjunction` macro. + ## MIP Reformulations The following reformulation methods are currently supported: From 50cdc2a8ffcc714633076aaca05051e4d11cc337 Mon Sep 17 00:00:00 2001 From: Hector Perez Date: Sat, 28 Oct 2023 10:22:55 -0400 Subject: [PATCH 8/8] change exclusive to exactly1 --- README.md | 2 +- docs/src/index.md | 2 +- src/constraints.jl | 26 +++++++++++++------------- src/datatypes.jl | 2 +- src/hull.jl | 2 +- src/macros.jl | 2 +- src/model.jl | 4 +++- src/reformulate.jl | 10 +++++----- test/constraints/bigm.jl | 4 ++-- test/constraints/disjunction.jl | 18 +++++++++--------- test/constraints/fallback.jl | 6 +++--- test/constraints/hull.jl | 8 ++++---- test/constraints/indicator.jl | 10 +++++----- 13 files changed, 49 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 4d81bfa..171b7ec 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ 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 `exclusive` to `false` in the `@disjunction` macro. +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 diff --git a/docs/src/index.md b/docs/src/index.md index 9a683ba..d8a4002 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -107,7 +107,7 @@ 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 `exclusive` to `false` in the `@disjunction` macro. +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 diff --git a/src/constraints.jl b/src/constraints.jl index f246ec6..375450b 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -117,10 +117,10 @@ function JuMP.delete(model::Model, cref::DisjunctionRef) delete!(gdp_data(model).constraint_to_indicator, cref) end delete!(_disjunctions(model), index(cref)) - exclusive_dict = gdp_data(model).exclusive_constraints - if haskey(exclusive_dict, cref) - JuMP.delete(model, exclusive_dict[cref]) - delete!(exclusive_dict, 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 @@ -316,7 +316,7 @@ function _disjunction( model::Model, # TODO: generalize to AbstractModel structure::AbstractVector, #generalize for containers name::String; - exclusive::Bool = true, + exactly1::Bool = true, extra_kwargs... ) # check for unneeded keywords @@ -326,12 +326,12 @@ function _disjunction( # create the disjunction dref = _create_disjunction(_error, model, structure, name, false) # add the exactly one constraint if desired - if exclusive + 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).exclusive_constraints[dref] = cref + gdp_data(model).exactly1_constraints[dref] = cref end return dref end @@ -354,7 +354,7 @@ function _disjunction( structure, name::String, tag::Disjunct; - exclusive::Bool = true, + exactly1::Bool = true, extra_kwargs... ) # check for unneeded keywords @@ -366,12 +366,12 @@ function _disjunction( obj = constraint_object(dref) _add_indicator_var(_DisjunctConstraint(obj, tag.indicator), dref, model) # add the exactly one constraint if desired - if exclusive + 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).exclusive_constraints[dref] = cref + gdp_data(model).exactly1_constraints[dref] = cref end return dref end @@ -396,15 +396,15 @@ end disjunct_indicators::Vector{LogicalVariableRef}, [nested_tag::Disjunct], [name::String = ""]; - [exclusive::Bool = true] + [exactly1::Bool = true] ) 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, `exclusive` adds +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, `exclusive` creates a constraint of the form +[`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). diff --git a/src/datatypes.jl b/src/datatypes.jl index 9b81292..0d73bc1 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -391,7 +391,7 @@ mutable struct GDPData disjunctions::_MOIUC.CleverDict{DisjunctionIndex, ConstraintData{Disjunction}} # Exactly one constraint mappings - exclusive_constraints::Dict{DisjunctionRef, LogicalConstraintRef} + exactly1_constraints::Dict{DisjunctionRef, LogicalConstraintRef} # Indicator variable mappings indicator_to_binary::Dict{LogicalVariableRef, VariableRef} diff --git a/src/hull.jl b/src/hull.jl index 6f0a901..c6dca98 100644 --- a/src/hull.jl +++ b/src/hull.jl @@ -149,7 +149,7 @@ end ################################################################################ # HULL REFORMULATION ################################################################################ -requires_exclusive(::Hull) = true +requires_exactly1(::Hull) = true function _reformulate_disjunctions(model::Model, method::Hull) _query_variable_bounds(model, method) diff --git a/src/macros.jl b/src/macros.jl index bde94c5..c54908d 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -155,7 +155,7 @@ 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. -- `exclusive`: Specify a `Bool` whether a constraint should be added to +- `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). 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 ce651cc..02851ce 100644 --- a/src/reformulate.jl +++ b/src/reformulate.jl @@ -47,22 +47,22 @@ end # DISJUNCTIONS ################################################################################ """ - requires_exclusive(method::AbstractReformulationMethod) + 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_exclusive(::AbstractReformulationMethod) = false +requires_exactly1(::AbstractReformulationMethod) = false # disjunctions function _reformulate_all_disjunctions(model::Model, method::AbstractReformulationMethod) for (idx, disj) in _disjunctions(model) disj.constraint.nested && continue #only reformulate top level disjunctions dref = DisjunctionRef(model, idx) - if requires_exclusive(method) && !haskey(gdp_data(model).exclusive_constraints, dref) - error("Reformulation method `$method` requires exclusive disjuncts for " * - "disjunctions, but `exclusive = false` for disjunction `$dref`.") + 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) diff --git a/test/constraints/bigm.jl b/test/constraints/bigm.jl index 8ec7920..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]), exclusive = false) + @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, exclusive = false) + @disjunction(model, outer, z, exactly1 = false) reformulate_model(model, BigM()) bvrefs = DP._indicator_to_binary(model) diff --git a/test/constraints/disjunction.jl b/test/constraints/disjunction.jl index b2753e6..2dccc0d 100644 --- a/test/constraints/disjunction.jl +++ b/test/constraints/disjunction.jl @@ -51,7 +51,7 @@ function test_disjunction_add_success() disj = DisjunctionRef(model, DisjunctionIndex(1)) disj2 = DisjunctionRef(model, DisjunctionIndex(2)) @test @disjunction(model, y) == disj - @test @disjunction(model, disj2, y, exclusive = false) == disj2 + @test @disjunction(model, disj2, y, exactly1 = false) == disj2 @test owner_model(disj) == model @test is_valid(model, disj) @test index(disj) == DisjunctionIndex(1) @@ -62,8 +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).exclusive_constraints, disj) - @test !haskey(gdp_data(model).exclusive_constraints, disj2) + @test haskey(gdp_data(model).exactly1_constraints, disj) + @test !haskey(gdp_data(model).exactly1_constraints, disj2) @test disj == copy(disj) end @@ -89,7 +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).exclusive_constraints, inner) + @test haskey(gdp_data(model).exactly1_constraints, inner) end function test_disjunction_add_array() @@ -189,7 +189,7 @@ function test_disjunction_delete() @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).exclusive_constraints, disj) + @test !haskey(gdp_data(model).exactly1_constraints, disj) model = GDPModel() @variable(model, x) @@ -197,7 +197,7 @@ function test_disjunction_delete() @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]), exclusive = false) + @disjunction(model, inner, y, Disjunct(z[1]), exactly1 = false) @test delete(model, inner) isa Nothing @test !haskey(gdp_data(model).disjunctions, index(inner)) @@ -219,7 +219,7 @@ function test_disjunction_function() set_name(disj, "new_name") @test name(disj) == "new_name" @test haskey(DP._disjunctions(model), index(disj)) - @test haskey(gdp_data(model).exclusive_constraints, disj) + @test haskey(gdp_data(model).exactly1_constraints, disj) end function test_disjunction_function_nested() @@ -233,7 +233,7 @@ function test_disjunction_function_nested() @constraint(model, x >= 10, Disjunct(z[2])) disj1 = DisjunctionRef(model, DisjunctionIndex(1)) disj2 = DisjunctionRef(model, DisjunctionIndex(2)) - @test disjunction(model, y, Disjunct(z[1]), "inner", exclusive = false) == disj1 + @test disjunction(model, y, Disjunct(z[1]), "inner", exactly1 = false) == disj1 @test disjunction(model, z, "outer") == disj2 @test is_valid(model, disj1) @@ -244,7 +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).exclusive_constraints, disj1) + @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 e25bcea..3e53cdc 100644 --- a/test/constraints/fallback.jl +++ b/test/constraints/fallback.jl @@ -5,11 +5,11 @@ function test_reformulate_disjunct_constraint_fallback() @test_throws ErrorException reformulate_disjunct_constraint(model, c, x, DummyReformulation()) end -function test_exclusive_fallback() - @test requires_exclusive(BigM()) == false +function test_exactly1_fallback() + @test requires_exactly1(BigM()) == false end @testset "Fallbacks" begin test_reformulate_disjunct_constraint_fallback() - test_exclusive_fallback() + test_exactly1_fallback() end \ No newline at end of file diff --git a/test/constraints/hull.jl b/test/constraints/hull.jl index 934143a..3da6488 100644 --- a/test/constraints/hull.jl +++ b/test/constraints/hull.jl @@ -636,14 +636,14 @@ function test_scalar_nonlinear_hull_2sided() end end -function test_exclusive_error() +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, exclusive = false) - @test requires_exclusive(Hull()) + disjunction(model, z, exactly1 = false) + @test requires_exactly1(Hull()) @test_throws ErrorException reformulate_model(model, Hull()) end @@ -686,5 +686,5 @@ end test_scalar_quadratic_hull_2sided() test_scalar_nonlinear_hull_2sided() test_scalar_nonlinear_hull_2sided_error() - test_exclusive_error() + test_exactly1_error() end \ No newline at end of file diff --git a/test/constraints/indicator.jl b/test/constraints/indicator.jl index 729b8ea..0566988 100644 --- a/test/constraints/indicator.jl +++ b/test/constraints/indicator.jl @@ -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, exclusive = false) + @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, exclusive = false) + @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, exclusive = false) + @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]), exclusive = false) + @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, exclusive = false) + @disjunction(model, z, exactly1 = false) reformulate_model(model, Indicator()) ref_cons = DP._reformulation_constraints(model)