diff --git a/README.md b/README.md index 6dcfff7..80868bd 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,14 @@ Logical variables are JuMP `AbstractVariable`s with two fields: `fix_value` and @variable(model, Y[1:3], Logical) ``` +When making logical variables for disjunctions with only two disjuncts, we can use the `logical_complement` argument to prevent creating uncessary binary variables when reformulating: + +```julia + +@variable(model, Y1, Logical) +@variable(model, Y2, Logical, logical_complement = Y1) # Y2 ⇔ ¬Y1 +``` + ## Logical Constraints Two types of logical constraints are supported: diff --git a/docs/src/index.md b/docs/src/index.md index 0aa4d32..5058b8d 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -85,6 +85,14 @@ Logical variables are JuMP `AbstractVariable`s with two fields: `fix_value` and @variable(model, Y[1:3], Logical) ``` +When making logical variables for disjunctions with only two disjuncts, we can use the `logical_complement` argument to prevent creating uncessary binary variables when reformulating: + +```julia + +@variable(model, Y1, Logical) +@variable(model, Y2, Logical, logical_complement = Y1) # Y2 ⇔ ¬Y1 +``` + ## Logical Constraints Two types of logical constraints are supported: diff --git a/src/bigm.jl b/src/bigm.jl index b151910..81386b3 100644 --- a/src/bigm.jl +++ b/src/bigm.jl @@ -171,26 +171,27 @@ function set_variable_bound_info(vref::JuMP.AbstractVariableRef, ::BigM) return lb, ub end +# Extend reformulate_disjunct_constraint function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::BigM ) where {T, S <: _MOI.LessThan} M = _get_M_value(con.func, con.set, method) - new_func = JuMP.@expression(model, con.func - M*(1-bvref)) + new_func = JuMP.@expression(model, con.func - M*(1 - bvref)) reform_con = JuMP.build_constraint(error, new_func, con.set) return [reform_con] end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::BigM ) where {T, S <: _MOI.Nonpositives, R} M = [_get_M_value(func, con.set, method) for func in con.func] new_func = JuMP.@expression(model, [i=1:con.set.dimension], - con.func[i] - M[i]*(1-bvref) + con.func[i] - M[i]*(1 - bvref) ) reform_con = JuMP.build_constraint(error, new_func, con.set) return [reform_con] @@ -198,23 +199,23 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::BigM ) where {T, S <: _MOI.GreaterThan} M = _get_M_value(con.func, con.set, method) - new_func = JuMP.@expression(model, con.func + M*(1-bvref)) + new_func = JuMP.@expression(model, con.func + M*(1 - bvref)) reform_con = JuMP.build_constraint(error, new_func, con.set) return [reform_con] end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::BigM ) where {T, S <: _MOI.Nonnegatives, R} M = [_get_M_value(func, con.set, method) for func in con.func] new_func = JuMP.@expression(model, [i=1:con.set.dimension], - con.func[i] + M[i]*(1-bvref) + con.func[i] + M[i]*(1 - bvref) ) reform_con = build_constraint(error, new_func, con.set) return [reform_con] @@ -222,12 +223,12 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::BigM ) where {T, S <: Union{_MOI.Interval, _MOI.EqualTo}} M = _get_M_value(con.func, con.set, method) - new_func_gt = JuMP.@expression(model, con.func + M[1]*(1-bvref)) - new_func_lt = JuMP.@expression(model, con.func - M[2]*(1-bvref)) + new_func_gt = JuMP.@expression(model, con.func + M[1]*(1 - bvref)) + new_func_lt = JuMP.@expression(model, con.func - M[2]*(1 - bvref)) set_values = _set_values(con.set) reform_con_gt = build_constraint(error, new_func_gt, _MOI.GreaterThan(set_values[1])) reform_con_lt = build_constraint(error, new_func_lt, _MOI.LessThan(set_values[2])) @@ -236,15 +237,15 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::BigM ) where {T, S <: _MOI.Zeros, R} M = [_get_M_value(func, con.set, method) for func in con.func] new_func_nn = JuMP.@expression(model, [i=1:con.set.dimension], - con.func[i] + M[i][1]*(1-bvref) + con.func[i] + M[i][1]*(1 - bvref) ) new_func_np = JuMP.@expression(model, [i=1:con.set.dimension], - con.func[i] - M[i][2]*(1-bvref) + con.func[i] - M[i][2]*(1 - bvref) ) reform_con_nn = JuMP.build_constraint(error, new_func_nn, _MOI.Nonnegatives(con.set.dimension)) reform_con_np = JuMP.build_constraint(error, new_func_np, _MOI.Nonpositives(con.set.dimension)) diff --git a/src/constraints.jl b/src/constraints.jl index 3d60cf2..a25270d 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -291,13 +291,30 @@ function _add_indicator_var( return end # check disjunction -function _check_disjunction(_error, lvrefs::AbstractVector{<:LogicalVariableRef}, model::JuMP.AbstractModel) +function _check_disjunction( + _error, + lvrefs::AbstractVector{<:LogicalVariableRef}, + model::M + ) where {M <: JuMP.AbstractModel} isequal(unique(lvrefs), lvrefs) || _error("Not all the logical indicator variables are unique.") for lvref in lvrefs if !JuMP.is_valid(model, lvref) _error("`$lvref` is not a valid logical variable reference.") end end + if length(lvrefs) != 2 && any(has_logical_complement.(lvrefs)) + _error("Can only use logical complement variables in Disjunctions " * + "with two disjuncts.") + elseif length(lvrefs) == 2 && any(has_logical_complement.(lvrefs)) + T = JuMP.value_type(M) + V = JuMP.variable_ref_type(M) + expr1 = convert(JuMP.GenericAffExpr{T, V}, binary_variable(first(lvrefs))) + expr2 = 1 - binary_variable(last(lvrefs)) + if !JuMP.isequal_canonical(expr1, expr2) + _error("When using logical complement variables in a disjunction, " * + "both logical variables must be the complement of one another.") + end + end return lvrefs end @@ -349,7 +366,7 @@ function _disjunction( # create the disjunction dref = _create_disjunction(_error, model, structure, name, false) # add the exactly one constraint if desired - if exactly1 + if exactly1 && !any(has_logical_complement.(structure)) lvars = JuMP.constraint_object(dref).indicators func = JuMP.model_convert.(model, Any[1, lvars...]) set = _MOIExactly(length(lvars) + 1) @@ -385,12 +402,17 @@ function _disjunction( for (kwarg, _) in extra_kwargs _error("Unrecognized keyword argument $kwarg.") end + # check that no logical complement is used + if any(has_logical_complement.(structure)) + _error("Logical complement variables are not supported for " * + "use in nested disjunctions.") + 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 + if exactly1 && !any(has_logical_complement.(structure)) lvars = JuMP.constraint_object(dref).indicators func = LogicalVariableRef{M}[tag.indicator, lvars...] set = _MOIExactly(length(lvars) + 1) diff --git a/src/datatypes.jl b/src/datatypes.jl index 1a71162..ffc9bd4 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -1,6 +1,28 @@ ################################################################################ # LOGICAL VARIABLES ################################################################################ +""" + LogicalVariableIndex + +A type for storing the index of a [`LogicalVariable`](@ref). + +**Fields** +- `value::Int64`: The index value. +""" +struct LogicalVariableIndex + value::Int64 +end + +""" + LogicalVariableRef{M <: JuMP.AbstractModel} + +A type for looking up logical variables. +""" +struct LogicalVariableRef{M <:JuMP.AbstractModel} <: JuMP.AbstractVariableRef + model::M + index::LogicalVariableIndex +end + """ LogicalVariable <: JuMP.AbstractVariable @@ -9,10 +31,13 @@ A variable type the logical variables associated with disjuncts in a [`Disjuncti **Fields** - `fix_value::Union{Nothing, Bool}`: A fixed boolean value if there is one. - `start_value::Union{Nothing, Bool}`: An initial guess if there is one. +- `logical_complement::Union{Nothing, LogicalVariableRef}`: The logical complement of + this variable if there is one. """ struct LogicalVariable <: JuMP.AbstractVariable fix_value::Union{Nothing, Bool} start_value::Union{Nothing, Bool} + logical_complement::Union{Nothing, LogicalVariableRef} end # Wrapper variable type for including arbitrary tags that will be used for @@ -66,28 +91,6 @@ mutable struct LogicalVariableData name::String end -""" - LogicalVariableIndex - -A type for storing the index of a [`LogicalVariable`](@ref). - -**Fields** -- `value::Int64`: The index value. -""" -struct LogicalVariableIndex - value::Int64 -end - -""" - LogicalVariableRef{M <: JuMP.AbstractModel} - -A type for looking up logical variables. -""" -struct LogicalVariableRef{M <:JuMP.AbstractModel} <: JuMP.AbstractVariableRef - model::M - index::LogicalVariableIndex -end - ################################################################################ # LOGICAL SELECTOR (CARDINALITY) SETS ################################################################################ @@ -384,12 +387,12 @@ end mutable struct _Hull{V <: JuMP.AbstractVariableRef, T} <: AbstractReformulationMethod value::T disjunction_variables::Dict{V, Vector{V}} - disjunct_variables::Dict{Tuple{V, V}, V} + disjunct_variables::Dict{Tuple{V, Union{V, JuMP.GenericAffExpr{T, V}}}, V} function _Hull(method::Hull{T}, vrefs::Set{V}) where {T, V <: JuMP.AbstractVariableRef} new{V, T}( method.value, Dict{V, Vector{V}}(vref => V[] for vref in vrefs), - Dict{Tuple{V, V}, V}() + Dict{Tuple{V, Union{V, JuMP.GenericAffExpr{T, V}}}, V}() ) end end @@ -420,7 +423,7 @@ mutable struct GDPData{M <: JuMP.AbstractModel, V <: JuMP.AbstractVariableRef, C exactly1_constraints::Dict{DisjunctionRef{M}, LogicalConstraintRef{M}} # Indicator variable mappings - indicator_to_binary::Dict{LogicalVariableRef{M}, V} + indicator_to_binary::Dict{LogicalVariableRef{M}, Union{V, JuMP.GenericAffExpr{T, V}}} indicator_to_constraints::Dict{LogicalVariableRef{M}, Vector{Union{DisjunctConstraintRef{M}, DisjunctionRef{M}}}} constraint_to_indicator::Dict{Union{DisjunctConstraintRef{M}, DisjunctionRef{M}}, LogicalVariableRef{M}} # needed for deletion @@ -443,7 +446,7 @@ mutable struct GDPData{M <: JuMP.AbstractModel, V <: JuMP.AbstractVariableRef, C _MOIUC.CleverDict{DisjunctConstraintIndex, ConstraintData}(), _MOIUC.CleverDict{DisjunctionIndex, ConstraintData{Disjunction{M}}}(), Dict{DisjunctionRef{M}, LogicalConstraintRef{M}}(), - Dict{LogicalVariableRef{M}, V}(), + Dict{LogicalVariableRef{M}, Union{V, JuMP.GenericAffExpr{T, V}}}(), Dict{LogicalVariableRef{M}, Vector{Union{DisjunctConstraintRef{M}, DisjunctionRef{M}}}}(), Dict{Union{DisjunctConstraintRef{M}, DisjunctionRef{M}}, LogicalVariableRef{M}}(), Dict{V, Tuple{T, T}}(), diff --git a/src/hull.jl b/src/hull.jl index a0649e0..f769ea2 100644 --- a/src/hull.jl +++ b/src/hull.jl @@ -104,7 +104,7 @@ end function _disaggregate_expression( model::JuMP.AbstractModel, vref::JuMP.AbstractVariableRef, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::_Hull ) if JuMP.is_binary(vref) || !haskey(method.disjunct_variables, (vref, bvref)) #keep any binary variables or nested disaggregated variables unchanged @@ -117,7 +117,7 @@ end function _disaggregate_expression( model::JuMP.AbstractModel, aff::JuMP.GenericAffExpr, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::_Hull ) new_expr = @expression(model, aff.constant*bvref) #multiply constant by binary indicator variable @@ -137,7 +137,7 @@ end function _disaggregate_expression( model::JuMP.AbstractModel, quad::JuMP.GenericQuadExpr, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::_Hull ) #get affine part @@ -155,7 +155,7 @@ end function _disaggregate_nl_expression( ::JuMP.AbstractModel, c::Number, - ::JuMP.AbstractVariableRef, + ::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, ::_Hull ) return c @@ -164,7 +164,7 @@ end function _disaggregate_nl_expression( ::JuMP.AbstractModel, vref::JuMP.AbstractVariableRef, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::_Hull ) ϵ = method.value @@ -179,7 +179,7 @@ end function _disaggregate_nl_expression( ::JuMP.AbstractModel, aff::JuMP.GenericAffExpr, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::_Hull ) new_expr = aff.constant @@ -200,7 +200,7 @@ end function _disaggregate_nl_expression( model::JuMP.AbstractModel, quad::JuMP.GenericQuadExpr, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::_Hull) #get affine part new_expr = _disaggregate_nl_expression(model, quad.aff, bvref, method) @@ -217,7 +217,7 @@ end function _disaggregate_nl_expression( model::JuMP.AbstractModel, nlp::NLP, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::_Hull ) where {NLP <: JuMP.GenericNonlinearExpr} new_args = Vector{Any}(undef, length(nlp.args)) @@ -265,7 +265,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::_Hull ) where {T <: JuMP.AbstractJuMPScalar, S <: Union{_MOI.LessThan, _MOI.GreaterThan, _MOI.EqualTo}} new_func = _disaggregate_expression(model, con.func, bvref, method) @@ -277,7 +277,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::_Hull ) where {T <: JuMP.AbstractJuMPScalar, S <: Union{_MOI.Nonpositives, _MOI.Nonnegatives, _MOI.Zeros}, R} new_func = JuMP.@expression(model, [i=1:con.set.dimension], @@ -289,7 +289,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::_Hull ) where {T <: JuMP.GenericNonlinearExpr, S <: Union{_MOI.LessThan, _MOI.GreaterThan, _MOI.EqualTo}} con_func = _disaggregate_nl_expression(model, con.func, bvref, method) @@ -306,7 +306,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::_Hull ) where {T <: JuMP.GenericNonlinearExpr, S <: Union{_MOI.Nonpositives, _MOI.Nonnegatives, _MOI.Zeros}, R} con_func = JuMP.@expression(model, [i=1:con.set.dimension], @@ -326,7 +326,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::_Hull ) where {T <: JuMP.AbstractJuMPScalar, S <: _MOI.Interval} new_func = _disaggregate_expression(model, con.func, bvref, method) @@ -339,7 +339,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::_Hull ) where {T <: JuMP.GenericNonlinearExpr, S <: _MOI.Interval} con_func = _disaggregate_nl_expression(model, con.func, bvref, method) diff --git a/src/indicator.jl b/src/indicator.jl index 84bcf64..ceacb6e 100644 --- a/src/indicator.jl +++ b/src/indicator.jl @@ -11,6 +11,15 @@ function reformulate_disjunct_constraint( reform_con = JuMP.build_constraint(error, [1*bvref, con.func], _MOI.Indicator{_MOI.ACTIVATE_ON_ONE}(con.set)) return [reform_con] end +function reformulate_disjunct_constraint( + ::JuMP.AbstractModel, + con::JuMP.ScalarConstraint{T, S}, + bvref::JuMP.GenericAffExpr, + ::Indicator +) where {T, S} + reform_con = JuMP.build_constraint(error, [1 - bvref, con.func], _MOI.Indicator{_MOI.ACTIVATE_ON_ZERO}(con.set)) + return [reform_con] +end #vectorized disjunct constraint function reformulate_disjunct_constraint( ::JuMP.AbstractModel, @@ -24,6 +33,18 @@ function reformulate_disjunct_constraint( for f in con.func ] end +function reformulate_disjunct_constraint( + ::JuMP.AbstractModel, + con::JuMP.VectorConstraint{T, S}, + bvref::JuMP.GenericAffExpr, + ::Indicator +) where {T, S} + set = _vec_to_scalar_set(con.set) + return [ + JuMP.build_constraint(error, [1 - bvref, f], _MOI.Indicator{_MOI.ACTIVATE_ON_ZERO}(set)) + for f in con.func + ] +end #nested indicator reformulation. NOTE: the user needs to provide the appropriate linking constraint for the logical variables for this to work (e.g. w in Exactly(y[1]) to link the parent disjunct y[1] to the nested disjunction w) function reformulate_disjunct_constraint( ::JuMP.AbstractModel, @@ -32,4 +53,4 @@ function reformulate_disjunct_constraint( ::Indicator ) where {T, S <: _MOI.Indicator} return [con] -end \ No newline at end of file +end diff --git a/src/logic.jl b/src/logic.jl index 4bb98b2..981ce51 100644 --- a/src/logic.jl +++ b/src/logic.jl @@ -219,24 +219,26 @@ end ################################################################################ # cardinality constraint reformulation function _reformulate_selector( - model::JuMP.AbstractModel, + model::M, func::Vector{JuMP.AbstractJuMPScalar}, set::AbstractCardinalitySet - ) + ) where {M <: JuMP.AbstractModel} bvrefs = [binary_variable(lvref) for lvref in func[2:end]] c = JuMP.constant(func[1]) new_set = _vec_to_scalar_set(set)(c) - cref = JuMP.@constraint(model, sum(bvrefs) in new_set) + init = zero(JuMP.value_type(M)) + cref = JuMP.@constraint(model, sum(bvrefs, init = init) in new_set) push!(_reformulation_constraints(model), cref) end function _reformulate_selector( - model::JuMP.AbstractModel, + model::M, func::Vector{<:LogicalVariableRef}, set::AbstractCardinalitySet - ) + ) where {M <: JuMP.AbstractModel} bvref, bvrefs... = [binary_variable(lvref) for lvref in func] new_set = _vec_to_scalar_set(set)(0) - cref = JuMP.@constraint(model, sum(bvrefs) - bvref in new_set) + init = zero(JuMP.value_type(M)) + cref = JuMP.@constraint(model, sum(bvrefs, init = init) - bvref in new_set) push!(_reformulation_constraints(model), cref) end diff --git a/src/reformulate.jl b/src/reformulate.jl index 511abae..9a3b65c 100644 --- a/src/reformulate.jl +++ b/src/reformulate.jl @@ -76,9 +76,11 @@ function _reformulate_disjunctions(model::JuMP.AbstractModel, method::AbstractRe 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`.") + if requires_exactly1(method) && !haskey(gdp_data(model).exactly1_constraints, dref) && + !any(has_logical_complement.(disj.constraint.indicators)) + error("Reformulation method `$method` requires disjunctions where only 1 disjunct is selected, ", + "but `exactly1 = false` or none of the logical variables are logical complements ", + "for disjunction `$dref`.") end if requires_variable_bound_info(method) for vref in _get_disjunction_variables(model, disj.constraint) @@ -94,7 +96,7 @@ function _reformulate_disjunctions(model::JuMP.AbstractModel, method::AbstractRe end end -# disjuncts +# individual disjunctions """ reformulate_disjunction( model::JuMP.AbstractModel, @@ -137,7 +139,7 @@ end reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.AbstractConstraint, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::AbstractReformulationMethod ) @@ -148,7 +150,7 @@ constraint. If `method` needs to specify how to reformulate the entire disjuncti function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::Disjunction, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::AbstractReformulationMethod ) ref_cons = reformulate_disjunction(model, con, method) @@ -163,7 +165,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.AbstractConstraint, - bvref::JuMP.AbstractVariableRef, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::AbstractReformulationMethod ) error("$(typeof(method)) reformulation for constraint $con is not supported yet.") diff --git a/src/variables.jl b/src/variables.jl index 2c5c101..052a427 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -2,17 +2,24 @@ # LOGICAL VARIABLES ################################################################################ """ - JuMP.build_variable(_error::Function, info::JuMP.VariableInfo, - ::Union{Type{Logical}, Logical}) + JuMP.build_variable( + _error::Function, + info::JuMP.VariableInfo, + ::Union{Type{Logical}, Logical}; + [logical_complement::Union{Nothing, LogicalVariableRef} = nothing] + )::LogicalVariable Extend `JuMP.build_variable` to work with logical variables. This in combination with `JuMP.add_variable` enables the use of -`@variable(model, [var_expr], Logical)`. +`@variable(model, [var_expr], Logical)`. Optionally, a `logical_complement` +can be given which provides a logical variable that is the complement of this +one (common for disjunctions with two disjuncts). """ function JuMP.build_variable( _error::Function, info::JuMP.VariableInfo, tag::Type{Logical}; + logical_complement::Union{Nothing, LogicalVariableRef} = nothing, kwargs... ) # check for invalid input @@ -27,12 +34,21 @@ function JuMP.build_variable( _error("Invalid fix value, must be false or true.") elseif info.has_start && !isone(info.start) && !iszero(info.start) _error("Invalid start value, must be false or true.") + elseif (info.has_start || info.has_fix) && !isnothing(logical_complement) + _error("Cannot fix or provide a start value for a logical variable " * + "that is a logical complement variable. Add these " * + "properties to the variable that is its complement.") + elseif !isnothing(logical_complement) && has_logical_complement(logical_complement) + _error("Cannot specify a logical complement that itself is a " * + "logical complement. For two logical variables that " * + "are the complement of one another, only use the " * + "`logical_complement` argument on one of them.") end # create the variable fix = info.has_fix ? Bool(info.fixed_value) : nothing start = info.has_start ? Bool(info.start) : nothing - return LogicalVariable(fix, start) + return LogicalVariable(fix, start, logical_complement) end # Logical variable with tag data @@ -78,8 +94,11 @@ function _make_binary_variable(model, var::_TaggedLogicalVariable, name) end """ - JuMP.add_variable(model::JuMP.Model, v::LogicalVariable, - name::String = "")::LogicalVariableRef + JuMP.add_variable( + model::JuMP.Model, + v::LogicalVariable, + name::String = "" + )::LogicalVariableRef Extend `JuMP.add_variable` for [`LogicalVariable`](@ref)s. This helps enable `@variable(model, [var_expr], Logical)`. @@ -96,9 +115,14 @@ function JuMP.add_variable( lvref = LogicalVariableRef(model, idx) _set_ready_to_optimize(model, false) # add the associated binary variables - bvref = _make_binary_variable(model, v, name) - _add_logical_info(bvref, v) - _indicator_to_binary(model)[lvref] = bvref + if isnothing(_get_variable(v).logical_complement) + bvref = _make_binary_variable(model, v, name) + _add_logical_info(bvref, v) + jump_expr = bvref + else + jump_expr = 1 - binary_variable(v.logical_complement) + end + _indicator_to_binary(model)[lvref] = jump_expr return lvref end @@ -179,7 +203,9 @@ function JuMP.set_name(vref::LogicalVariableRef, name::String) data = gdp_data(model) data.logical_variables[JuMP.index(vref)].name = name _set_ready_to_optimize(model, false) - JuMP.set_name(binary_variable(vref), name) + if !has_logical_complement(vref) + JuMP.set_name(binary_variable(vref), name) + end return end @@ -203,7 +229,11 @@ function JuMP.set_start_value( vref::LogicalVariableRef, value::Union{Nothing, Bool} ) - new_var = LogicalVariable(JuMP.fix_value(vref), value) + if has_logical_complement(vref) + error("Cannot set the start value of a logical complement variable. ", + "Set the start value of its logical complement instead.") + end + new_var = LogicalVariable(JuMP.fix_value(vref), value, nothing) _set_variable_object(vref, new_var) JuMP.set_start_value(binary_variable(vref), value) return @@ -236,7 +266,11 @@ constraint if one exists, otherwise create a new one. """ function JuMP.fix(vref::LogicalVariableRef, value::Bool) - new_var = LogicalVariable(value, JuMP.start_value(vref)) + if has_logical_complement(vref) + error("Cannot fix value of a logical variable complement. ", + "Fix its logical complement instead.") + end + new_var = LogicalVariable(value, JuMP.start_value(vref), nothing) _set_variable_object(vref, new_var) JuMP.fix(binary_variable(vref), value) return @@ -248,24 +282,39 @@ end Delete the fixed value of a logical variable. """ function JuMP.unfix(vref::LogicalVariableRef) - new_var = LogicalVariable(nothing, JuMP.start_value(vref)) + has_logical_complement(vref) && return + new_var = LogicalVariable(nothing, JuMP.start_value(vref), nothing) _set_variable_object(vref, new_var) JuMP.unfix(binary_variable(vref)) return end """ - binary_variable(vref::LogicalVariableRef)::JuMP.AbstractVariableRef + binary_variable( + vref::LogicalVariableRef + )::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr} Returns the underlying binary variable for the logical variable `vref` which is used in the reformulated model. This is helpful to embed logical variables -in algebraic constraints. +in algebraic constraints. If `vref` has a logical complement then an expression +of the form `1 - bvref` is returned where `bvref` is the binary variable of the +logical complement variable. """ function binary_variable(vref::LogicalVariableRef) model = JuMP.owner_model(vref) return _indicator_to_binary(model)[vref] end +""" + has_logical_complement(vref::LogicalVariableRef)::Bool + +Return a `Bool` whether a `vref` is a logical complement of +another logical variable. +""" +function has_logical_complement(vref::LogicalVariableRef) + return !isnothing(_variable_object(vref).logical_complement) +end + """ JuMP.value(vref::LogicalVariableRef)::Bool @@ -286,30 +335,32 @@ function JuMP.delete(model::JuMP.AbstractModel, vref::LogicalVariableRef) @assert JuMP.is_valid(model, vref) "Variable does not belong to model." vidx = JuMP.index(vref) dict = _logical_variables(model) - #delete any disjunct constraints associated with the logical variables in the disjunction + # delete any disjunct constraints associated with the logical variables in the disjunction if haskey(_indicator_to_constraints(model), vref) crefs = _indicator_to_constraints(model)[vref] JuMP.delete.(model, crefs) delete!(_indicator_to_constraints(model), vref) end - #delete any disjunctions that have the logical variable + # delete any disjunctions that have the logical variable for (didx, ddata) in _disjunctions(model) if vref in ddata.constraint.indicators JuMP.delete(model, DisjunctionRef(model, didx)) end end - #delete any logical constraints involving the logical variables + # delete any logical constraints involving the logical variables for (cidx, cdata) in _logical_constraints(model) lvars = _get_logical_constraint_variables(model, cdata.constraint) if vref in lvars JuMP.delete(model, LogicalConstraintRef(model, cidx)) end end - #delete the logical variable + # delete the logical variable + if !has_logical_complement(vref) + JuMP.delete(model, binary_variable(vref)) + end delete!(dict, vidx) - JuMP.delete(model, binary_variable(vref)) delete!(_indicator_to_binary(model), vref) - #not ready to optimize + # not ready to optimize _set_ready_to_optimize(model, false) return end diff --git a/test/constraints/disjunction.jl b/test/constraints/disjunction.jl index 128ab6f..d6c632d 100644 --- a/test/constraints/disjunction.jl +++ b/test/constraints/disjunction.jl @@ -38,6 +38,11 @@ function test_disjunction_add_fail() @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) + + @variable(model, yc[i = 1:2], Logical, logical_complement = y[i]) + @test_throws ErrorException disjunction(model, [yc[1]]) + @test_throws ErrorException disjunction(model, [yc[1], y[2]]) + @test_throws ErrorException disjunction(model, [y[1], yc[1]], Disjunct(y[2])) end function test_disjunction_add_success() diff --git a/test/constraints/indicator.jl b/test/constraints/indicator.jl index e41d8e3..f79b6ae 100644 --- a/test/constraints/indicator.jl +++ b/test/constraints/indicator.jl @@ -131,6 +131,47 @@ function test_extension_indicator() @test all([cobj.set isa MOI.Indicator for cobj in ref_cons_obj]) end +function test_indicator_complement() + model = GDPModel() + @variable(model, x) + @variable(model, y1, Logical) + @variable(model, y2, Logical, logical_complement = y1) + y = [y1, y2] + @constraint(model, x == 5, Disjunct(y[1])) + @constraint(model, x <= 5, Disjunct(y[1])) + @constraint(model, x >= 5, Disjunct(y[1])) + @constraint(model, x == 10, Disjunct(y[2])) + @constraint(model, x <= 10, Disjunct(y[2])) + @constraint(model, x >= 10, Disjunct(y[2])) + @disjunction(model, y) + reformulate_model(model, Indicator()) + + ref_cons = DP._reformulation_constraints(model) + ref_cons_obj = constraint_object.(ref_cons) + @test length(ref_cons) == 6 + @test all(is_valid.(model, ref_cons)) + @test all(isa.(ref_cons_obj[1:6], VectorConstraint)) + @test all([cobj.set isa MOI.Indicator for cobj in ref_cons_obj[1:6]]) + + model = GDPModel() + A = [1 0; 0 1] + @variable(model, x) + @variable(model, y1, Logical) + @variable(model, y2, Logical, logical_complement = y1) + y = [y1, y2] + @constraint(model, A*[x,x] == [5,5], Disjunct(y[1])) + @constraint(model, A*[x,x] <= [0,0], Disjunct(y[2])) + @disjunction(model, y) + reformulate_model(model, Indicator()) + + ref_cons = DP._reformulation_constraints(model) + ref_cons_obj = constraint_object.(ref_cons) + @test length(ref_cons) == 4 + @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]) +end + @testset "Indicator" begin test_indicator_scalar_constraints() test_indicator_vector_constraints() @@ -139,4 +180,5 @@ end test_indicator_sparse_axis() test_indicator_nested() test_extension_indicator() + test_indicator_complement() end \ No newline at end of file diff --git a/test/solve.jl b/test/solve.jl index ce122a6..a24f63d 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -1,9 +1,15 @@ using HiGHS -function test_linear_gdp_example(m) +function test_linear_gdp_example(m, use_complements = false) set_attribute(m, MOI.Silent(), true) @variable(m, 1 ≤ x[1:2] ≤ 9) - @variable(m, Y[1:2], Logical) + if use_complements + @variable(m, Y1, Logical) + @variable(m, Y2, Logical, logical_complement = Y1) + Y = [Y1, Y2] + else + @variable(m, Y[1:2], Logical) + end @variable(m, W[1:2], Logical) @objective(m, Max, sum(x)) @constraint(m, y1[i=1:2], [1,4][i] ≤ x[i] ≤ [3,6][i], Disjunct(Y[1])) @@ -30,14 +36,16 @@ function test_linear_gdp_example(m) @test value(Y[2]) @test !value(W[1]) @test !value(W[2]) - @test value(variable_by_name(m, "x[1]_Y[1]")) ≈ 0 - @test value(variable_by_name(m, "x[1]_Y[2]")) ≈ 9 @test value(variable_by_name(m, "x[1]_W[1]")) ≈ 0 @test value(variable_by_name(m, "x[1]_W[2]")) ≈ 0 - @test value(variable_by_name(m, "x[2]_Y[1]")) ≈ 0 - @test value(variable_by_name(m, "x[2]_Y[2]")) ≈ 2 @test value(variable_by_name(m, "x[2]_W[1]")) ≈ 0 @test value(variable_by_name(m, "x[2]_W[2]")) ≈ 0 + if !use_complements + @test value(variable_by_name(m, "x[1]_Y[1]")) ≈ 0 + @test value(variable_by_name(m, "x[1]_Y[2]")) ≈ 9 + @test value(variable_by_name(m, "x[2]_Y[1]")) ≈ 0 + @test value(variable_by_name(m, "x[2]_Y[2]")) ≈ 2 + end end function test_generic_model(m) @@ -55,12 +63,14 @@ function test_generic_model(m) @test optimize!(m, gdp_method = BigM()) isa Nothing @test optimize!(m, gdp_method = Hull()) isa Nothing + @test optimize!(m, gdp_method = Indicator()) isa Nothing # TODO add meaningful tests to check the constraints/variables end @testset "Solve Linear GDP" begin test_linear_gdp_example(GDPModel(HiGHS.Optimizer)) + test_linear_gdp_example(GDPModel(HiGHS.Optimizer), true) mockoptimizer = () -> MOI.Utilities.MockOptimizer( MOI.Utilities.UniversalFallback(MOIU.Model{Float32}()), eval_objective_value = false diff --git a/test/variables/logical.jl b/test/variables/logical.jl index a218089..bd161d6 100644 --- a/test/variables/logical.jl +++ b/test/variables/logical.jl @@ -30,7 +30,7 @@ function test_lvar_add_success() @test isnothing(fix_value(y)) @test isequal_canonical(y, copy(y)) @test haskey(DP._logical_variables(model), index(y)) - @test DP._logical_variables(model)[index(y)].variable == LogicalVariable(nothing, nothing) + @test DP._logical_variables(model)[index(y)].variable == LogicalVariable(nothing, nothing, nothing) @test DP._logical_variables(model)[index(y)].name == "y" @test binary_variable(y) isa VariableRef @test is_binary(binary_variable(y)) @@ -131,6 +131,30 @@ function test_lvar_delete() @test !is_valid(model, bvar) end +function test_lvar_logical_complement() + model = GDPModel() + @variable(model, y1, Logical) + # test addition + @test_throws ErrorException @variable(model, y2 == true, Logical, logical_complement = y1) + @test_throws ErrorException @variable(model, y2, Logical, logical_complement = y1, start = false) + @variable(model, y2, Logical, logical_complement = y1) + # test queries + @test binary_variable(y2) == 1 - binary_variable(y1) + @test name(y2) == "y2" + @test set_name(y2, "new_name") isa Nothing + @test name(y2) == "new_name" + @test_throws ErrorException set_start_value(y2, false) + @test_throws ErrorException fix(y2, false) + @test unfix(y2) isa Nothing + @test has_logical_complement(y2) + @test !has_logical_complement(y1) + # test error for logical of logical + @test_throws ErrorException @variable(model, y3, Logical, logical_complement = y2) + # test deletion + @test delete(model, y2) isa Nothing + @test !is_valid(model, y2) +end + function test_tagged_variables() model = GDPModel{MyModel, MyVarRef, MyConRef}() y = [LogicalVariableRef(model, LogicalVariableIndex(i)) for i in 1:2] @@ -177,6 +201,9 @@ end @testset "Delete Logical Variables" begin test_lvar_delete() end + @testset "Logical Compliment Variables" begin + test_lvar_logical_complement() + end @testset "Tagged Logical Variables" begin test_tagged_variables() end