diff --git a/README.md b/README.md index e970e8c..da96953 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Logical variables are JuMP `AbstractVariable`s with two fields: `fix_value` and Two types of logical constraints are supported: -1. `Selector` or cardinality constraints: A subset of Logical variables is passed and `Exactly`, `AtMost`, or `AtLeast` `n` of these is allowed to be `True`. These constraints are specified with the `func` $\in$ `set` notation in `MathOptInterface` in a `@constraint` JuMP macro. It is not assumed that disjunctions have an `Exactly(1)` constraint enforced on their disjuncts upon creation. This constraint must be explicitly specified. +1. `Selector` or cardinality constraints: A subset of Logical variables is passed and `Exactly`, `AtMost`, or `AtLeast` `n` of these is allowed to be `true`. These constraints are specified with the `func` $\in$ `set` notation in `MathOptInterface` in a `@constraint` JuMP macro. It is not assumed that disjunctions have an `Exactly(1)` constraint enforced on their disjuncts upon creation. This constraint must be explicitly specified. ```julia @constraint(model, [Y[1], Y[2]] in Exactly(1)) @@ -117,12 +117,10 @@ The following reformulation methods are currently supported: - `value`: Big-M value to use. Default: `1e9`. Big-M values are currently global to the model. Constraint specific Big-M values can be supported in future releases. - `tighten`: Boolean indicating if tightening the Big-M value should be attempted (currently supported only for linear disjunct constraints when variable bounds have been set or specified in the `variable_bounds` field). Default: `true`. - - `variable_bounds`: Dictionary specifying the lower and upper bounds for each `VariableRef` (e.g., `Dict(x => (lb, ub))`). Default: populate when calling the reformulation method. 2. [Hull](https://optimization.cbe.cornell.edu/index.php?title=Disjunctive_inequalities#Convex-Hull_Reformulation[1][2]): The `Hull` struct is created with the following optional arguments: - `value`: `ϵ` value to use when reformulating quadratic or nonlinear constraints via the perspective function proposed by [Furman, et al. [2020]](https://link.springer.com/article/10.1007/s10589-020-00176-0). Default: `1e-6`. `ϵ` values are currently global to the model. Constraint specific tolerances can be supported in future releases. - - `variable_bounds`: Dictionary specifying the lower and upper bounds for each `VariableRef` (e.g., `Dict(x => (lb, ub))`). Default: populate when calling the reformulation method. 3. [Indicator](https://jump.dev/JuMP.jl/stable/manual/constraints/#Indicator-constraints): This method reformulates each disjunct constraint into an indicator constraint with the Boolean reformulation counterpart of the Logical variable used to define the disjunct constraint. @@ -154,7 +152,7 @@ print(m) # x[2] ≤ 20 ## -optimize!(m, method = BigM(100, false)) #specify M value and disable M-tightening +optimize!(m, gdp_method = BigM(100, false)) #specify M value and disable M-tightening print(m) # Max x[1] + x[2] # Subject to @@ -175,7 +173,7 @@ print(m) # Y[2] binary ## -optimize!(m, method = Hull()) +optimize!(m, gdp_method = Hull()) print(m) # Max x[1] + x[2] # Subject to diff --git a/docs/src/index.md b/docs/src/index.md index f9274a2..4144f2f 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -16,7 +16,7 @@ Pkg.add("DisjunctiveProgramming") ## Model -A generalized disjunctive programming (GDP) model is created using `GDPModel()`, where the optimizer can be passed at model creation, along with other keyword arguments supported by JuMP Models. +A generalized disjunctive programming (GDP) model is created using [`GDPModel`](@ref), where the optimizer can be passed at model creation, along with other keyword arguments supported by JuMP Models. ```julia using DisjunctiveProgramming @@ -25,13 +25,13 @@ using HiGHS model = GDPModel(HiGHS.Optimizer) ``` -A `GDPModel` is a `JuMP Model` with a `GDPData` field in the model's `.ext` dictionary, which stores the following: +A [`GDPModel`](@ref) is a `JuMP Model` with a [`GDPData`](@ref) field in the model's `.ext` dictionary, which stores the following: - `Logical Variables`: Indicator variables used for the various disjuncts involved in the model's disjunctions. - `Logical Constraints`: Selector (cardinality) or proposition (Boolean) constraints describing the relationships between the logical variables. - `Disjunct Constraints`: Constraints associated with each disjunct in the model. - `Disjunctions`: Disjunction constraints. -- `Solution Method`: The reformulation technique or solution method. Currently supported methods include Big-M, Hull, and Indicator Constraints. +- `Solution Method`: The reformulation technique or solution method. Currently, supported methods include Big-M, Hull, and Indicator Constraints. - `Reformulation Variables`: List of JuMP variables created when reformulating a GDP model into a MIP model. - `Reformulation Constraints`: List of constraints created when reformulating a GDP model into a MIP model. - `Ready to Optimize`: Flag indicating if the model can be optimized. @@ -49,7 +49,7 @@ data = gdp_data(model) ## Logical Variables -Logical variables are JuMP `AbstractVariable`s with two fields: `fix_value` and `start_value`. These can be optionally specified at variable creation. Logical variables are created with the `@variable` JuMP macro by adding the tag `Logical` as the last keyword argument. As with the regular `@variable` macro, variables can be named and indexed: +Logical variables are JuMP `AbstractVariable`s with two fields: `fix_value` and `start_value`. These can be optionally specified at variable creation. Logical variables are created with the `@variable` JuMP macro by adding the tag [`Logical`](@ref) as the last keyword argument. As with the regular `@variable` macro, variables can be named and indexed: ```julia @variable(model, Y[1:3], Logical) @@ -59,7 +59,7 @@ Logical variables are JuMP `AbstractVariable`s with two fields: `fix_value` and Two types of logical constraints are supported: -1. `Selector` or cardinality constraints: A subset of Logical variables is passed and `Exactly`, `AtMost`, or `AtLeast` `n` of these is allowed to be `True`. These constraints are specified with the `func` $\in$ `set` notation in `MathOptInterface` in a `@constraint` JuMP macro. It is not assumed that disjunctions have an `Exactly(1)` constraint enforced on their disjuncts upon creation. This constraint must be explicitly specified. +1. `Selector` or cardinality constraints: A subset of Logical variables is passed and [`Exactly`](@ref), [`AtMost`](@ref), or [`AtLeast`](@ref) `n` of these is allowed to be `true`. These constraints are specified with the `func` $\in$ `set` notation in `MathOptInterface` in a `@constraint` JuMP macro. It is not assumed that disjunctions have an `Exactly(1)` constraint enforced on their disjuncts upon creation. This constraint must be explicitly specified. ```julia @constraint(model, [Y[1], Y[2]] in Exactly(1)) @@ -73,19 +73,19 @@ Two types of logical constraints are supported: - `⟹` of `implies` (Implication, typed with `\Longrightarrow + tab`). - `⇔` or `iff` or `==` (double implication or equivalence, typed with `\Leftrightarrow + tab`). - The `@constraint` JuMP macro is used to create these constraints with `:=`: +The `@constraint` JuMP macro is used to create these constraints with `:=`: - ```julia - @constraint(model, Y[1] ⟹ Y[2] := true) - ``` +```julia +@constraint(model, Y[1] ⟹ Y[2] := true) +``` - _Note_: The parenthesis in the example above around the implication clause are only required when the parent logical operator is `⟹` or `⇔` to avoid parsing errors. +_Note_: The parenthesis in the example above around the implication clause are only required when the parent logical operator is `⟹` or `⇔` to avoid parsing errors. - Logical propositions can be reformulated to IP constraints by automatic reformulation to [Conjunctive Normal Form](https://en.wikipedia.org/wiki/Conjunctive_normal_form). +Logical propositions can be reformulated to IP constraints by automatic reformulation to [Conjunctive Normal Form](https://en.wikipedia.org/wiki/Conjunctive_normal_form). ## Disjunctions -Disjunctions are built by first defining the constraints associated with each disjunct. This is done via the `@constraint` JuMP macro with the extra `Disjunct` tag specifying the Logical variable associated with the constraint: +Disjunctions are built by first defining the constraints associated with each disjunct. This is done via the `@constraint` JuMP macro with the extra [`Disjunct`](@ref) tag specifying the Logical variable associated with the constraint: ```julia @variable(model, x) @@ -93,13 +93,13 @@ Disjunctions are built by first defining the constraints associated with each di @constraint(model, x ≥ 200, Disjunct(Y[2])) ``` -After all disjunct constraints associated with a disjunction have been defined, the disjunction is created with the `@disjunction` macro, where the disjunction is defined as a `Vector` of Logical variables associated with each disjunct: +After all disjunct constraints associated with a disjunction have been defined, the disjunction is created with the [`@disjunction`](@ref) macro, where the disjunction is defined as a `Vector` of Logical variables associated with each disjunct: ```julia @disjunction(model, [Y[1], Y[2]]) ``` -Disjunctions can be nested by passing an additional `Disjunct` tag. The Logical variable in the `Disjunct` tag specifies which disjunct, the nested disjunction belongs to: +Disjunctions can be nested by passing an additional [`Disjunct`](@ref) tag. The Logical variable in the `Disjunct` tag specifies which disjunct, the nested disjunction belongs to: ```julia @disjunction(model, Y[1:2], Disjunct(Y[3])) @@ -113,18 +113,11 @@ For convenience, the `Exactly(1)` selector constraint is added by default when a The following reformulation methods are currently supported: -1. [Big-M](https://optimization.cbe.cornell.edu/index.php?title=Disjunctive_inequalities#Big-M_Reformulation[1][2]): The `BigM` struct is created with the following optional arguments: - - - `value`: Big-M value to use. Default: `1e9`. Big-M values are currently global to the model. Constraint specific Big-M values can be supported in future releases. - - `tighten`: Boolean indicating if tightening the Big-M value should be attempted (currently supported only for linear disjunct constraints when variable bounds have been set or specified in the `variable_bounds` field). Default: `true`. - - `variable_bounds`: Dictionary specifying the lower and upper bounds for each `VariableRef` (e.g., `Dict(x => (lb, ub))`). Default: populate when calling the reformulation method. - -2. [Hull](https://optimization.cbe.cornell.edu/index.php?title=Disjunctive_inequalities#Convex-Hull_Reformulation[1][2]): The `Hull` struct is created with the following optional arguments: +1. [Big-M](https://optimization.cbe.cornell.edu/index.php?title=Disjunctive_inequalities#Big-M_Reformulation[1][2]): The [`BigM`](@ref) struct is used. - - `value`: `ϵ` value to use when reformulating quadratic or nonlinear constraints via the perspective function proposed by [Furman, et al. [2020]](https://link.springer.com/article/10.1007/s10589-020-00176-0). Default: `1e-6`. `ϵ` values are currently global to the model. Constraint specific tolerances can be supported in future releases. - - `variable_bounds`: Dictionary specifying the lower and upper bounds for each `VariableRef` (e.g., `Dict(x => (lb, ub))`). Default: populate when calling the reformulation method. +2. [Hull](https://optimization.cbe.cornell.edu/index.php?title=Disjunctive_inequalities#Convex-Hull_Reformulation[1][2]): The [`Hull`](@ref) struct is used. -3. [Indicator](https://jump.dev/JuMP.jl/stable/manual/constraints/#Indicator-constraints): This method reformulates each disjunct constraint into an indicator constraint with the Boolean reformulation counterpart of the Logical variable used to define the disjunct constraint. +3. [Indicator](https://jump.dev/JuMP.jl/stable/manual/constraints/#Indicator-constraints): This method reformulates each disjunct constraint into an indicator constraint with the Boolean reformulation counterpart of the Logical variable used to define the disjunct constraint. This is invoked with [`Indicator`](@ref). ## Release Notes @@ -154,7 +147,7 @@ print(m) # x[2] ≤ 20 ## -optimize!(m, method = BigM(100, false)) #specify M value and disable M-tightening +optimize!(m, gdp_method = BigM(100, false)) #specify M value and disable M-tightening print(m) # Max x[1] + x[2] # Subject to @@ -175,7 +168,7 @@ print(m) # Y[2] binary ## -optimize!(m, method = Hull()) +optimize!(m, gdp_method = Hull()) print(m) # Max x[1] + x[2] # Subject to diff --git a/examples/ex1.jl b/examples/ex1.jl index 46a377c..6d49b13 100644 --- a/examples/ex1.jl +++ b/examples/ex1.jl @@ -36,7 +36,7 @@ print(m) ## BigM reformulation set_optimizer(m, HiGHS.Optimizer) -optimize!(m, method = BigM()) +optimize!(m, gdp_method = BigM()) print(m) # Max x # Subject to @@ -51,7 +51,7 @@ print(m) # Y[2] binary ## Hull reformulation -optimize!(m, method = Hull()) +optimize!(m, gdp_method = Hull()) print(m) # Max x # Subject to diff --git a/examples/ex2.jl b/examples/ex2.jl index 9700b0c..0dd48cb 100644 --- a/examples/ex2.jl +++ b/examples/ex2.jl @@ -18,7 +18,7 @@ print(m) # x[2] ≤ 20 ## -optimize!(m, method = BigM(100, false)) #specify M value and disable M-tightening +optimize!(m, gdp_method = BigM(100, false)) #specify M value and disable M-tightening print(m) # Max x[1] + x[2] # Subject to @@ -39,7 +39,7 @@ print(m) # Y[2] binary ## -optimize!(m, method = Hull()) +optimize!(m, gdp_method = Hull()) print(m) # Max x[1] + x[2] # Subject to diff --git a/src/bigm.jl b/src/bigm.jl index 7c8ed1a..471fbc7 100644 --- a/src/bigm.jl +++ b/src/bigm.jl @@ -21,14 +21,22 @@ function _get_tight_M(func::AbstractJuMPScalar, set::_MOI.AbstractSet, method::B end # Get user-specified Big-M value -function _get_M(::AbstractJuMPScalar, ::Union{_MOI.LessThan, _MOI.GreaterThan, _MOI.Nonnegatives, _MOI.Nonpositives}, method::BigM) +function _get_M( + ::AbstractJuMPScalar, + ::Union{_MOI.LessThan, _MOI.GreaterThan, _MOI.Nonnegatives, _MOI.Nonpositives}, + method::BigM + ) M = method.value if isinf(M) error("A finite Big-M value must be used. The value given was $M.") end return M end -function _get_M(::AbstractJuMPScalar, ::Union{_MOI.Interval, _MOI.EqualTo, _MOI.Zeros}, method::BigM) +function _get_M( + ::AbstractJuMPScalar, + ::Union{_MOI.Interval, _MOI.EqualTo, _MOI.Zeros}, + method::BigM + ) M = method.value if isinf(M) error("A finite Big-M value must be used. The value given was $M.") @@ -37,79 +45,102 @@ function _get_M(::AbstractJuMPScalar, ::Union{_MOI.Interval, _MOI.EqualTo, _MOI. end # Apply interval arithmetic on a linear constraint to infer the tightest Big-M value from the bounds on the constraint. -function _calculate_tight_M(func::AffExpr, set::_MOI.LessThan, method::BigM) +function _calculate_tight_M( + func::JuMP.GenericAffExpr, + set::_MOI.LessThan, + method::BigM + ) return _interval_arithmetic_LessThan(func, -set.upper, method) end -function _calculate_tight_M(func::AffExpr, set::_MOI.GreaterThan, method::BigM) +function _calculate_tight_M( + func::JuMP.GenericAffExpr, + set::_MOI.GreaterThan, + method::BigM + ) return _interval_arithmetic_GreaterThan(func, -set.lower, method) end -function _calculate_tight_M(func::AffExpr, ::_MOI.Nonpositives, method::BigM) - return _interval_arithmetic_LessThan(func, 0.0, method) +function _calculate_tight_M( + func::JuMP.GenericAffExpr{C, V}, + ::_MOI.Nonpositives, + method::BigM + ) where {C, V} + return _interval_arithmetic_LessThan(func, zero(C), method) end -function _calculate_tight_M(func::AffExpr, ::_MOI.Nonnegatives, method::BigM) - return _interval_arithmetic_GreaterThan(func, 0.0, method) +function _calculate_tight_M( + func::JuMP.GenericAffExpr{C, V}, + ::_MOI.Nonnegatives, + method::BigM + ) where {C, V} + return _interval_arithmetic_GreaterThan(func, zero(C), method) end -function _calculate_tight_M(func::AffExpr, set::_MOI.Interval, method::BigM) +function _calculate_tight_M( + func::JuMP.GenericAffExpr, + set::_MOI.Interval, + method::BigM + ) return ( _interval_arithmetic_GreaterThan(func, -set.lower, method), _interval_arithmetic_LessThan(func, -set.upper, method) ) end -function _calculate_tight_M(func::AffExpr, set::_MOI.EqualTo, method::BigM) +function _calculate_tight_M( + func::JuMP.GenericAffExpr, + set::_MOI.EqualTo, + method::BigM + ) return ( _interval_arithmetic_GreaterThan(func, -set.value, method), _interval_arithmetic_LessThan(func, -set.value, method) ) end -function _calculate_tight_M(func::AffExpr, ::_MOI.Zeros, method::BigM) +function _calculate_tight_M( + func::JuMP.GenericAffExpr{C, V}, + ::_MOI.Zeros, + method::BigM + ) where {C, V} return ( - _interval_arithmetic_GreaterThan(func, 0.0, method), - _interval_arithmetic_LessThan(func, 0.0, method) + _interval_arithmetic_GreaterThan(func, zero(C), method), + _interval_arithmetic_LessThan(func, zero(C), method) ) end # fallbacks for other scalar constraints -_calculate_tight_M(func::Union{QuadExpr, NonlinearExpr}, set::Union{_MOI.Interval, _MOI.EqualTo, _MOI.Zeros}, method::BigM) = (Inf, Inf) -_calculate_tight_M(func::Union{QuadExpr, NonlinearExpr}, set::Union{_MOI.LessThan, _MOI.GreaterThan, _MOI.Nonnegatives, _MOI.Nonpositives}, method::BigM) = Inf -_calculate_tight_M(func, set, method::BigM) = error("BigM method not implemented for constraint type $(typeof(func)) in $(typeof(set))") - -# get variable bounds for interval arithmetic -function _update_variable_bounds(vref::VariableRef, method::BigM) - if is_binary(vref) - lb = 0 - elseif !has_lower_bound(vref) - lb = -Inf - else - lb = lower_bound(vref) - end - if is_binary(vref) - ub = 1 - elseif !has_upper_bound(vref) - ub = Inf - else - ub = upper_bound(vref) - end - return lb, ub +function _calculate_tight_M( + func::Union{JuMP.GenericQuadExpr, JuMP.GenericNonlinearExpr}, + set::Union{_MOI.Interval, _MOI.EqualTo, _MOI.Zeros}, + method::BigM + ) + return (Inf, Inf) +end +function _calculate_tight_M( + func::Union{JuMP.GenericQuadExpr, JuMP.GenericNonlinearExpr}, + set::Union{_MOI.LessThan, _MOI.GreaterThan, _MOI.Nonnegatives, _MOI.Nonpositives}, + method::BigM + ) + return Inf +end +function _calculate_tight_M(::F, ::S, ::BigM) where {F, S} + error("BigM method not implemented for constraint type `$(F)` in `$(S)`.") end # perform interval arithmetic to update the initial M value -function _interval_arithmetic_LessThan(func::AffExpr, M::Float64, method::BigM) +function _interval_arithmetic_LessThan(func::JuMP.GenericAffExpr, M, ::BigM) for (var,coeff) in func.terms - is_binary(var) && continue #skip binary variables + JuMP.is_binary(var) && continue if coeff > 0 - M += coeff*method.variable_bounds[var][2] + M += coeff*variable_bound_info(var)[2] else - M += coeff*method.variable_bounds[var][1] + M += coeff*variable_bound_info(var)[1] end end return M + func.constant end -function _interval_arithmetic_GreaterThan(func::AffExpr, M::Float64, method::BigM) +function _interval_arithmetic_GreaterThan(func::JuMP.GenericAffExpr, M, ::BigM) for (var,coeff) in func.terms - is_binary(var) && continue #skip binary variables + JuMP.is_binary(var) && continue if coeff < 0 - M += coeff*method.variable_bounds[var][2] + M += coeff*variable_bound_info(var)[2] else - M += coeff*method.variable_bounds[var][1] + M += coeff*variable_bound_info(var)[1] end end return -(M + func.constant) @@ -118,15 +149,27 @@ end ################################################################################ # BIG-M REFORMULATION ################################################################################ -function _reformulate_disjunctions(model::Model, method::BigM) - method.tighten && _query_variable_bounds(model, method) - _reformulate_all_disjunctions(model, method) +requires_variable_bound_info(method::BigM) = method.tighten + +# get variable bounds for interval arithmetic (note these cannot be binary) +function set_variable_bound_info(vref::JuMP.AbstractVariableRef, ::BigM) + if !has_lower_bound(vref) + lb = -Inf + else + lb = lower_bound(vref) + end + if !has_upper_bound(vref) + ub = Inf + else + ub = upper_bound(vref) + end + return lb, ub end function reformulate_disjunct_constraint( - model::Model, + model::JuMP.AbstractModel, con::ScalarConstraint{T, S}, - bvref::VariableRef, + bvref::JuMP.AbstractVariableRef, method::BigM ) where {T, S <: _MOI.LessThan} M = _get_M_value(con.func, con.set, method) @@ -135,9 +178,9 @@ function reformulate_disjunct_constraint( return [reform_con] end function reformulate_disjunct_constraint( - model::Model, + model::JuMP.AbstractModel, con::VectorConstraint{T, S, R}, - bvref::VariableRef, + bvref::JuMP.AbstractVariableRef, method::BigM ) where {T, S <: _MOI.Nonpositives, R} M = [_get_M_value(func, con.set, method) for func in con.func] @@ -148,9 +191,9 @@ function reformulate_disjunct_constraint( return [reform_con] end function reformulate_disjunct_constraint( - model::Model, + model::JuMP.AbstractModel, con::ScalarConstraint{T, S}, - bvref::VariableRef, + bvref::JuMP.AbstractVariableRef, method::BigM ) where {T, S <: _MOI.GreaterThan} M = _get_M_value(con.func, con.set, method) @@ -159,9 +202,9 @@ function reformulate_disjunct_constraint( return [reform_con] end function reformulate_disjunct_constraint( - model::Model, + model::JuMP.AbstractModel, con::VectorConstraint{T, S, R}, - bvref::VariableRef, + bvref::JuMP.AbstractVariableRef, method::BigM ) where {T, S <: _MOI.Nonnegatives, R} M = [_get_M_value(func, con.set, method) for func in con.func] @@ -172,9 +215,9 @@ function reformulate_disjunct_constraint( return [reform_con] end function reformulate_disjunct_constraint( - model::Model, + model::JuMP.AbstractModel, con::ScalarConstraint{T, S}, - bvref::VariableRef, + bvref::JuMP.AbstractVariableRef, method::BigM ) where {T, S <: Union{_MOI.Interval, _MOI.EqualTo}} M = _get_M_value(con.func, con.set, method) @@ -186,9 +229,9 @@ function reformulate_disjunct_constraint( return [reform_con_gt, reform_con_lt] end function reformulate_disjunct_constraint( - model::Model, + model::JuMP.AbstractModel, con::VectorConstraint{T, S, R}, - bvref::VariableRef, + bvref::JuMP.AbstractVariableRef, method::BigM ) where {T, S <: _MOI.Zeros, R} M = [_get_M_value(func, con.set, method) for func in con.func] @@ -201,4 +244,4 @@ function reformulate_disjunct_constraint( reform_con_nn = build_constraint(error, new_func_nn, _MOI.Nonnegatives(con.set.dimension)) reform_con_np = build_constraint(error, new_func_np, _MOI.Nonpositives(con.set.dimension)) return [reform_con_nn, reform_con_np] -end \ No newline at end of file +end diff --git a/src/constraints.jl b/src/constraints.jl index 6b84290..6e0e517 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -9,19 +9,19 @@ _set_values(set::_MOI.EqualTo) = (set.value, set.value) _set_values(set::_MOI.Interval) = (set.lower, set.upper) # helper functions to reformulate vector constraints to indicator constraints -_vec_to_scalar_set(set::_MOI.Nonpositives) = _MOI.LessThan(0) -_vec_to_scalar_set(set::_MOI.Nonnegatives) = _MOI.GreaterThan(0) -_vec_to_scalar_set(set::_MOI.Zeros) = _MOI.EqualTo(0) +_vec_to_scalar_set(::_MOI.Nonpositives) = _MOI.LessThan(0) +_vec_to_scalar_set(::_MOI.Nonnegatives) = _MOI.GreaterThan(0) +_vec_to_scalar_set(::_MOI.Zeros) = _MOI.EqualTo(0) # helper functions to map jump selector to moi selector sets -_jump_to_moi_selector(set::Exactly) = _MOIExactly -_jump_to_moi_selector(set::AtLeast) = _MOIAtLeast -_jump_to_moi_selector(set::AtMost) = _MOIAtMost +_jump_to_moi_selector(::Exactly) = _MOIExactly +_jump_to_moi_selector(::AtLeast) = _MOIAtLeast +_jump_to_moi_selector(::AtMost) = _MOIAtMost # helper functions to map selectors to scalar sets -_vec_to_scalar_set(set::_MOIExactly) = _MOI.EqualTo -_vec_to_scalar_set(set::_MOIAtLeast) = _MOI.GreaterThan -_vec_to_scalar_set(set::_MOIAtMost) = _MOI.LessThan +_vec_to_scalar_set(::_MOIExactly) = _MOI.EqualTo +_vec_to_scalar_set(::_MOIAtLeast) = _MOI.GreaterThan +_vec_to_scalar_set(::_MOIAtMost) = _MOI.LessThan ################################################################################ # BOILERPLATE EXTENSION METHODS @@ -45,12 +45,12 @@ for (RefType, loc) in ((:DisjunctConstraintRef, :disjunct_constraints), JuMP.index(cref::$RefType) = cref.index @doc """ - JuMP.is_valid(model::Model, cref::$($RefType)) + JuMP.is_valid(model::JuMP.AbstractModel, cref::$($RefType)) Return `true` if `cref` refers to a valid constraint in the `GDP model`. """ - function JuMP.is_valid(model::Model, cref::$RefType) # TODO: generalize for AbstractModel - return model === owner_model(cref) + function JuMP.is_valid(model::JuMP.AbstractModel, cref::$RefType) + return model === owner_model(cref) && haskey(gdp_data(model).$loc, JuMP.index(cref)) end # Get the ConstraintData object @@ -105,11 +105,11 @@ for (RefType, loc) in ((:DisjunctConstraintRef, :disjunct_constraints), end """ - JuMP.delete(model::Model, cref::DisjunctionRef) + JuMP.delete(model::JuMP.AbstractModel, cref::DisjunctionRef) Delete a disjunction constraint from the `GDP model`. """ -function JuMP.delete(model::Model, cref::DisjunctionRef) +function JuMP.delete(model::JuMP.AbstractModel, cref::DisjunctionRef) @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] @@ -127,11 +127,11 @@ function JuMP.delete(model::Model, cref::DisjunctionRef) end """ - JuMP.delete(model::Model, cref::DisjunctConstraintRef) + JuMP.delete(model::JuMP.AbstractModel, cref::DisjunctConstraintRef) Delete a disjunct constraint from the `GDP model`. """ -function JuMP.delete(model::Model, cref::DisjunctConstraintRef) +function JuMP.delete(model::JuMP.AbstractModel, cref::DisjunctConstraintRef) @assert is_valid(model, cref) "Disjunctive constraint does not belong to model." delete!(_disjunct_constraints(model), index(cref)) lvref = gdp_data(model).constraint_to_indicator[cref] @@ -142,11 +142,11 @@ function JuMP.delete(model::Model, cref::DisjunctConstraintRef) end """ - JuMP.delete(model::Model, cref::LogicalConstraintRef) + JuMP.delete(model::JuMP.AbstractModel, cref::LogicalConstraintRef) Delete a logical constraint from the `GDP model`. """ -function JuMP.delete(model::Model, cref::LogicalConstraintRef) +function JuMP.delete(model::JuMP.AbstractModel, cref::LogicalConstraintRef) @assert is_valid(model, cref) "Logical constraint does not belong to model." delete!(_logical_constraints(model), index(cref)) _set_ready_to_optimize(model, false) @@ -156,14 +156,21 @@ end ################################################################################ # Disjunct Constraints ################################################################################ -function _check_expression(expr) - vars = Set{VariableRef}() +function _check_expression(expr::Ex) where {Ex <: JuMP.AbstractJuMPScalar} + vars = Set{JuMP.variable_ref_type(expr)}() _interrogate_variables(v -> push!(vars, v), expr) if any(is_binary.(vars)) || any(is_integer.(vars)) error("Disjunct constraints cannot contain binary or integer variables.") end return end +function _check_expression(expr::AbstractVector) + for ex in expr + _check_expression(ex) + end + return +end + """ JuMP.build_constraint( _error::Function, @@ -235,7 +242,7 @@ end """ JuMP.add_constraint( - model::Model, + model::JuMP.AbstractModel, con::_DisjunctConstraint, name::String = "" )::DisjunctConstraintRef @@ -244,7 +251,7 @@ Extend `JuMP.add_constraint` to add a [`Disjunct`](@ref) to a [`GDPModel`](@ref) The constraint is added to the `GDPData` in the `.ext` dictionary of the `GDPModel`. """ function JuMP.add_constraint( - model::Model, + model::JuMP.AbstractModel, con::_DisjunctConstraint, name::String = "" ) @@ -260,7 +267,7 @@ end ################################################################################ # Add the variable mappings function _add_indicator_var( - con::_DisjunctConstraint{C, LogicalVariableRef}, + con::_DisjunctConstraint{C, <:LogicalVariableRef}, cref, model ) where {C <: AbstractConstraint} @@ -273,8 +280,8 @@ function _add_indicator_var( return end # check disjunction -function _check_disjunction(_error, lvrefs::AbstractVector{LogicalVariableRef}, model::Model) - isequal(unique(lvrefs),lvrefs) || _error("Not all the logical indicator variables are unique.") +function _check_disjunction(_error, lvrefs::AbstractVector{<:LogicalVariableRef}, model::JuMP.AbstractModel) + isequal(unique(lvrefs), lvrefs) || _error("Not all the logical indicator variables are unique.") for lvref in lvrefs if !is_valid(model, lvref) _error("`$lvref` is not a valid logical variable reference.") @@ -283,19 +290,14 @@ function _check_disjunction(_error, lvrefs::AbstractVector{LogicalVariableRef}, return lvrefs end -# fallback -function _check_disjunction(_error, lvrefs, model::Model) - _error("Unrecognized disjunction input structure.") # TODO add details on proper syntax -end - # Write the main function for creating disjunctions that is macro friendly function _create_disjunction( _error::Function, - model::Model, # TODO: generalize to AbstractModel - structure::AbstractVector, #generalize for containers + model::JuMP.AbstractModel, + structure::Vector{<:LogicalVariableRef}, name::String, nested::Bool -) + ) is_gdp_model(model) || error("Can only add disjunctions to `GDPModel`s.") # build the disjunction @@ -309,16 +311,26 @@ function _create_disjunction( _set_ready_to_optimize(model, false) return DisjunctionRef(model, idx) end +function _create_disjunction( + _error::Function, + model::JuMP.AbstractModel, + structure::AbstractArray{<:LogicalVariableRef}, #generalize for containers + name::String, + nested::Bool + ) + vect_structure = [v for v in Iterators.Flatten(structure)] + return _create_disjunction(_error, model, vect_structure, name, nested) +end # Disjunction build for unnested disjunctions function _disjunction( _error::Function, - model::Model, # TODO: generalize to AbstractModel - structure::AbstractVector, #generalize for containers + model::M, + structure::AbstractArray{<:LogicalVariableRef}, #generalize for containers name::String; exactly1::Bool = true, extra_kwargs... -) + ) where {M <: JuMP.AbstractModel} # check for unneeded keywords for (kwarg, _) in extra_kwargs _error("Unrecognized keyword argument $kwarg.") @@ -328,7 +340,7 @@ function _disjunction( # add the exactly one constraint if desired if exactly1 lvars = JuMP.constraint_object(dref).indicators - func = Union{Number, LogicalVariableRef}[1, lvars...] + func = JuMP.model_convert.(model, Any[1, lvars...]) set = _MOIExactly(length(lvars) + 1) cref = JuMP.add_constraint(model, JuMP.VectorConstraint(func, set)) gdp_data(model).exactly1_constraints[dref] = cref @@ -339,24 +351,25 @@ end # Fallback disjunction build for nonvector structure function _disjunction( _error::Function, - model::Model, # TODO: generalize to AbstractModel + model::JuMP.AbstractModel, structure, - name::String; + name::String, + args...; kwargs... -) + ) _error("Unrecognized disjunction input structure.") end # Disjunction build for nested disjunctions function _disjunction( _error::Function, - model::Model, # TODO: generalize to AbstractModel - structure, + model::M, + structure::AbstractArray{<:LogicalVariableRef}, name::String, tag::Disjunct; exactly1::Bool = true, extra_kwargs... -) + ) where {M <: JuMP.AbstractModel} # check for unneeded keywords for (kwarg, _) in extra_kwargs _error("Unrecognized keyword argument $kwarg.") @@ -368,7 +381,7 @@ function _disjunction( # add the exactly one constraint if desired if exactly1 lvars = JuMP.constraint_object(dref).indicators - func = LogicalVariableRef[tag.indicator, lvars...] + func = LogicalVariableRef{M}[tag.indicator, lvars...] set = _MOIExactly(length(lvars) + 1) cref = JuMP.add_constraint(model, JuMP.VectorConstraint(func, set)) gdp_data(model).exactly1_constraints[dref] = cref @@ -379,12 +392,12 @@ end # General fallback for additional arguments function _disjunction( _error::Function, - model::Model, # TODO: generalize to AbstractModel - structure, + model::JuMP.AbstractModel, + structure::AbstractArray{<:LogicalVariableRef}, name::String, extra...; kwargs... -) + ) for arg in extra _error("Unrecognized argument `$arg`.") end @@ -392,7 +405,7 @@ end """ disjunction( - model::Model, + model::JuMP.AbstractModel, disjunct_indicators::Vector{LogicalVariableRef}, [nested_tag::Disjunct], [name::String = ""]; @@ -410,7 +423,7 @@ To conveniently generate many disjunctions at once, see [`@disjunction`](@ref) and [`@disjunctions`](@ref). """ function disjunction( - model::Model, + model::JuMP.AbstractModel, disjunct_indicators, name::String = "", extra...; @@ -419,7 +432,7 @@ function disjunction( return _disjunction(error, model, disjunct_indicators, name, extra...; kwargs...) end function disjunction( - model::Model, + model::JuMP.AbstractModel, disjunct_indicators, nested_tag::Disjunct, name::String = "", @@ -456,13 +469,22 @@ model = GDPModel(); @constraint(model, [Y[1], Y[2]] in Exactly(1)); ``` """ +function JuMP.build_constraint( # Cardinality logical constraint + _error::Function, + func::AbstractVector{T}, # allow any vector-like JuMP container + set::S # TODO: generalize to allow CP sets from MOI +) where {T <: LogicalVariableRef, S <: Union{Exactly{Int}, AtLeast{Int}, AtMost{Int}}} + new_set = _jump_to_moi_selector(set)(length(func) + 1) + new_func = Union{Number, LogicalVariableRef}[set.value, func...] + return VectorConstraint(new_func, new_set) # model_convert will make it an AbstractJuMPScalar +end function JuMP.build_constraint( # Cardinality logical constraint _error::Function, func::AbstractVector{T}, # allow any vector-like JuMP container set::S # TODO: generalize to allow CP sets from MOI ) where {T <: LogicalVariableRef, S <: Union{Exactly, AtLeast, AtMost}} new_set = _jump_to_moi_selector(set)(length(func) + 1) - new_func = Union{Number,LogicalVariableRef}[set.value, func...] + new_func = [set.value, func...] # will be a vector of type LogicalVariableRef return VectorConstraint(new_func, new_set) end function JuMP.build_constraint( # Cardinality logical constraint @@ -476,7 +498,7 @@ end # Fallback for Affine/Quad expressions function JuMP.build_constraint( _error::Function, - expr::Union{GenericAffExpr{C, LogicalVariableRef}, GenericQuadExpr{C, LogicalVariableRef}}, + expr::Union{GenericAffExpr{C, <:LogicalVariableRef}, GenericQuadExpr{C, <:LogicalVariableRef}}, set::_MOI.AbstractScalarSet ) where {C} _error("Cannot add, subtract, or multiply with logical variables.") @@ -491,6 +513,28 @@ function JuMP.build_constraint( _error("Invalid set `$set` for logical constraint.") end +# Helper function to enable proper dispatching +function _add_logical_constraint(model::M, c, name) where {M} + # check the constraint out + is_gdp_model(model) || error("Can only add logical constraints to `GDPModel`s.") + set = JuMP.moi_set(c) + @assert set isa MOI.EqualTo{Bool} "Unexpected set `$set` for logical constraint." + func = JuMP.jump_function(c) + JuMP.check_belongs_to_model(func, model) + _check_logical_expression(func) + # add negation if needed + if !set.value + func = _LogicalExpr{M}(:!, func) + set = MOI.EqualTo{Bool}(true) + end + # add the constraint + new_c = JuMP.ScalarConstraint(func, set) # we have guarranteed that set.value = true + constr_data = ConstraintData(new_c, name) + idx = _MOIUC.add_item(_logical_constraints(model), constr_data) + _set_ready_to_optimize(model, false) + return LogicalConstraintRef(model, idx) +end + # Check that logical expression is valid function _check_logical_expression(ex) _check_logical_expression_literal(ex) @@ -516,7 +560,7 @@ end """ function JuMP.add_constraint( - model::JuMP.Model, + model::JuMP.GenericModel, c::JuMP.ScalarConstraint{_LogicalExpr, MOI.EqualTo{Bool}}, name::String = "" ) @@ -526,74 +570,62 @@ for a [`GDPModel`](@ref) with the `@constraint` macro. Users should define logical constraints via the syntax `@constraint(model, logical_expr := true)`. """ function JuMP.add_constraint( - model::JuMP.Model, - c::JuMP.ScalarConstraint{_LogicalExpr, S}, + model::M, + c::JuMP.ScalarConstraint{_LogicalExpr{M}, S}, name::String = "" - ) where {S} # S <: JuMP._DoNotConvertSet{MOI.EqualTo{Bool}} or MOI.EqualTo{Bool} - # check the constraint out - is_gdp_model(model) || error("Can only add logical constraints to `GDPModel`s.") - set = JuMP.moi_set(c) - @assert set isa MOI.EqualTo{Bool} "Unexpected set `$set` for logical constraint." - func = JuMP.jump_function(c) - JuMP.check_belongs_to_model(func, model) - _check_logical_expression(func) - # add negation if needed - if !set.value - func = _LogicalExpr(:!, func) - set = MOI.EqualTo{Bool}(true) - end - # add the constraint - new_c = JuMP.ScalarConstraint(func, set) # we have guarranteed that set.value = true - constr_data = ConstraintData(new_c, name) - idx = _MOIUC.add_item(_logical_constraints(model), constr_data) - _set_ready_to_optimize(model, false) - return LogicalConstraintRef(model, idx) + ) where {S, M <: JuMP.GenericModel} # S <: JuMP._DoNotConvertSet{MOI.EqualTo{Bool}} or MOI.EqualTo{Bool} + return _add_logical_constraint(model, c, name) end + function JuMP.add_constraint( - model::JuMP.Model, - c::JuMP.ScalarConstraint{LogicalVariableRef, S}, + model::M, + c::JuMP.ScalarConstraint{LogicalVariableRef{M}, S}, name::String = "" - ) where {S} # S <: JuMP._DoNotConvertSet{MOI.EqualTo{Bool}} or MOI.EqualTo{Bool} + ) where {M <: JuMP.GenericModel, S} # S <: JuMP._DoNotConvertSet{MOI.EqualTo{Bool}} or MOI.EqualTo{Bool} error("Cannot define constraint on single logical variable, use `fix` instead.") end function JuMP.add_constraint( - model::JuMP.Model, - c::JuMP.ScalarConstraint{GenericAffExpr{C, LogicalVariableRef}, S}, + model::M, + c::JuMP.ScalarConstraint{GenericAffExpr{C, LogicalVariableRef{M}}, S}, name::String = "" - ) where {S, C} + ) where {M <: JuMP.GenericModel, S, C} error("Cannot add, subtract, or multiply with logical variables.") end function JuMP.add_constraint( - model::JuMP.Model, - c::JuMP.ScalarConstraint{GenericQuadExpr{C, LogicalVariableRef}, S}, + model::M, + c::JuMP.ScalarConstraint{GenericQuadExpr{C, LogicalVariableRef{M}}, S}, name::String = "" - ) where {S, C} + ) where {M <: JuMP.GenericModel, S, C} error("Cannot add, subtract, or multiply with logical variables.") end +# Define method for adding cardinality constraints (needed to define multiple methods to avoid ambiguous dispatch) +function _add_cardinality_constraint(model, c, name) + is_gdp_model(model) || error("Can only add logical constraints to `GDPModel`s.") + func = JuMP.jump_function(c) + JuMP.check_belongs_to_model.(filter(Base.Fix2(isa, JuMP.AbstractJuMPScalar), func), model) + # TODO maybe do some formatting on `c` to ensure the types are what we expect later --> build_constraint forces formatting now + constr_data = ConstraintData(c, name) + idx = _MOIUC.add_item(_logical_constraints(model), constr_data) + _set_ready_to_optimize(model, false) + return LogicalConstraintRef(model, idx) +end + """ function JuMP.add_constraint( - model::Model, - c::VectorConstraint{<:F, S, Shape}, + model::JuMP.GenericModel, + c::VectorConstraint{<:F, S}, name::String = "" - ) where {F <: Union{Number, LogicalVariableRef, _LogicalExpr}, S, Shape} + ) where {F <: Vector{<:LogicalVariableRef}, S <: AbstractCardinalitySet} Extend `JuMP.add_constraint` to allow creating logical cardinality constraints for a [`GDPModel`](@ref) with the `@constraint` macro. """ function JuMP.add_constraint( - model::JuMP.Model, - c::JuMP.VectorConstraint{F, S, Shape}, + model::JuMP.GenericModel, + c::JuMP.VectorConstraint{F, S}, name::String = "" - ) where {F, S <: Union{_MOIAtLeast, _MOIAtMost, _MOIExactly}, Shape} - is_gdp_model(model) || error("Can only add logical constraints to `GDPModel`s.") - func = JuMP.jump_function(c) - JuMP.check_belongs_to_model.(filter(Base.Fix2(isa, JuMP.AbstractJuMPScalar), func), model) - # TODO maybe do some formatting on `c` to ensure the types are what we expect later - constr_data = ConstraintData(c, name) - idx = _MOIUC.add_item(_logical_constraints(model), constr_data) - _set_ready_to_optimize(model, false) - return LogicalConstraintRef(model, idx) + ) where {F, S <: AbstractCardinalitySet} + return _add_cardinality_constraint(model, c, name) end - # TODO create bridges for MOI sets for and use BridgeableConstraint with build_constraint diff --git a/src/datatypes.jl b/src/datatypes.jl index 04b0020..f886ba9 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -1,9 +1,8 @@ ################################################################################ # LOGICAL VARIABLES ################################################################################ - """ - LogicalVariable <: AbstractVariable + LogicalVariable <: JuMP.AbstractVariable A variable type the logical variables associated with disjuncts in a [`Disjunction`](@ref). @@ -11,12 +10,46 @@ A variable type the logical variables associated with disjuncts in a [`Disjuncti - `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. """ -struct LogicalVariable <: AbstractVariable +struct LogicalVariable <: JuMP.AbstractVariable fix_value::Union{Nothing, Bool} start_value::Union{Nothing, Bool} end -const Logical = LogicalVariable +# Wrapper variable type for including arbitrary tags that will be used for +# creating reformulation variables later on +struct _TaggedLogicalVariable{T} <: JuMP.AbstractVariable + variable::LogicalVariable + tag_data::T +end + +""" + Logical{T} + +Tag for creating logical variables using `@variable`. Most often this will +be used to enable the syntax: +```julia +@variable(model, var_expr, Logical, [kwargs...]) +``` +which creates a [`LogicalVariable`](@ref) that will ultimately be +reformulated into a binary variable of the form: +```julia +@variable(model, var_expr, Bin, [kwargs...]) +``` + +To include a tag that is used to create the reformulated variables, the syntax +becomes: +```julia +@variable(model, var_expr, Logical(MyTag()), [kwargs...]) +``` +which creates a [`LogicalVariable`](@ref) that is associated with `MyTag()` such +that the reformulation binary variables are of the form: +```julia +@variable(model, var_expr, Bin, MyTag(), [kwargs...]) +``` +""" +struct Logical{T} + tag_data::T +end """ LogicalVariableData @@ -25,7 +58,7 @@ A type for storing [`LogicalVariable`](@ref)s and any meta-data they possess. **Fields** -- `variable::LogicalVariable`: The variable object. +- `variable::LogicalVariable`: The logical variable object. - `name::String`: The name of the variable. """ mutable struct LogicalVariableData @@ -46,12 +79,12 @@ struct LogicalVariableIndex end """ - LogicalVariableRef + LogicalVariableRef{M <: JuMP.AbstractModel} A type for looking up logical variables. """ -struct LogicalVariableRef <: AbstractVariableRef - model::Model # TODO: generalize for AbstractModels +struct LogicalVariableRef{M <:JuMP.AbstractModel} <: AbstractVariableRef + model::M index::LogicalVariableIndex end @@ -66,29 +99,37 @@ end # product between two vectors in the set # is equivalent to LinearAlgebra.dot. """ - _MOIAtLeast <: _MOI.AbstractVectorSet + AbstractCardinalitySet <: MOI.AbstractVectorSet + +An abstract type for cardinality sets [`_MOIAtLeast`](@ref), [`_MOIExactly`](@ref), +and [`_MOIAtMost`](@ref). +""" +abstract type AbstractCardinalitySet <:_MOI.AbstractVectorSet end + +""" + _MOIAtLeast <: AbstractCardinalitySet MOI level set for AtLeast constraints, see [`AtLeast`](@ref) for recommended syntax. """ -struct _MOIAtLeast <: _MOI.AbstractVectorSet +struct _MOIAtLeast <: AbstractCardinalitySet dimension::Int end """ - _MOIAtMost <: _MOI.AbstractVectorSet + _MOIAtMost <: AbstractCardinalitySet MOI level set for AtMost constraints, see [`AtMost`](@ref) for recommended syntax. """ -struct _MOIAtMost <: _MOI.AbstractVectorSet +struct _MOIAtMost <: AbstractCardinalitySet dimension::Int end """ - _MOIExactly <: _MOI.AbstractVectorSet + _MOIExactly <: AbstractCardinalitySet MOI level set for Exactly constraints, see [`Exactly`](@ref) for recommended syntax. """ -struct _MOIExactly <: _MOI.AbstractVectorSet +struct _MOIExactly <: AbstractCardinalitySet dimension::Int end @@ -121,14 +162,14 @@ struct Exactly{T<:Union{Int,LogicalVariableRef}} <: AbstractVectorSet end # Extend JuMP.moi_set as needed -JuMP.moi_set(set::AtLeast, dim::Int) = _MOIAtLeast(dim) -JuMP.moi_set(set::AtMost, dim::Int) = _MOIAtMost(dim) -JuMP.moi_set(set::Exactly, dim::Int) = _MOIExactly(dim) +JuMP.moi_set(::AtLeast, dim::Int) = _MOIAtLeast(dim) +JuMP.moi_set(::AtMost, dim::Int) = _MOIAtMost(dim) +JuMP.moi_set(::Exactly, dim::Int) = _MOIExactly(dim) ################################################################################ # LOGICAL CONSTRAINTS ################################################################################ -const _LogicalExpr = GenericNonlinearExpr{LogicalVariableRef} +const _LogicalExpr{M} = JuMP.GenericNonlinearExpr{LogicalVariableRef{M}} """ ConstraintData{C <: AbstractConstraint} @@ -158,12 +199,12 @@ struct LogicalConstraintIndex end """ - LogicalConstraintRef + LogicalConstraintRef{M <: JuMP.AbstractModel} A type for looking up logical constraints. """ -struct LogicalConstraintRef - model::Model # TODO: generalize for AbstractModels +struct LogicalConstraintRef{M <: JuMP.AbstractModel} + model::M index::LogicalConstraintIndex end @@ -184,8 +225,8 @@ where `lvref` is a [`LogicalVariableRef`](@ref) that will ultimately be associat with the disjunct the constraint is added to. If no `lvref` is given, then one is generated when the disjunction is created. """ -struct Disjunct - indicator::LogicalVariableRef +struct Disjunct{M <: JuMP.AbstractModel} + indicator::LogicalVariableRef{M} end # Create internal type for temporarily packaging constraints for disjuncts @@ -207,12 +248,12 @@ struct DisjunctConstraintIndex end """ - DisjunctConstraintRef + DisjunctConstraintRef{M <: JuMP.AbstractModel} A type for looking up disjunctive constraints. """ -struct DisjunctConstraintRef - model::Model # TODO: generalize for AbstractModels +struct DisjunctConstraintRef{M <: JuMP.AbstractModel} + model::M index::DisjunctConstraintIndex end @@ -220,18 +261,18 @@ end # DISJUNCTIONS ################################################################################ """ - Disjunction <: AbstractConstraint + Disjunction{M <: JuMP.AbstractModel} <: AbstractConstraint A type for a disjunctive constraint that is comprised of a collection of -disjuncts of indicated by a unique [`LogicalVariableRef`](@ref). +disjuncts of indicated by a unique [`LogicalVariableIndex`](@ref). **Fields** -- `indicators::Vector{LogicalVariableRef}`: The references to the logical variables +- `indicators::Vector{LogicalVariableref}`: The references to the logical variables (indicators) that uniquely identify each disjunct in the disjunction. - `nested::Bool`: Is this disjunction nested within another disjunction? """ -struct Disjunction <: AbstractConstraint - indicators::Vector{LogicalVariableRef} +struct Disjunction{M <: JuMP.AbstractModel} <: AbstractConstraint + indicators::Vector{LogicalVariableRef{M}} nested::Bool end @@ -248,19 +289,18 @@ struct DisjunctionIndex end """ - DisjunctionRef + DisjunctionRef{M <: JuMP.AbstractModel} A type for looking up disjunctive constraints. """ -struct DisjunctionRef - model::Model # TODO: generalize for AbstractModels +struct DisjunctionRef{M <: JuMP.AbstractModel} + model::M index::DisjunctionIndex end ################################################################################ # CLEVER DICTS ################################################################################ - ## Extend the CleverDicts key access methods # index_to_key function _MOIUC.index_to_key(::Type{LogicalVariableIndex}, index::Int64) @@ -293,7 +333,6 @@ end ################################################################################ # SOLUTION METHODS ################################################################################ - """ AbstractSolutionMethod @@ -309,56 +348,48 @@ An abstract type for reformulation approaches used to solve `GDPModel`s. abstract type AbstractReformulationMethod <: AbstractSolutionMethod end """ - BigM <: AbstractReformulationMethod + BigM{T} <: AbstractReformulationMethod A type for using the big-M reformulation approach for disjunctive constraints. **Fields** -- `value::Float64`: Big-M value (default = `1e9`). +- `value::T`: Big-M value (default = `1e9`). - `tight::Bool`: Attempt to tighten the Big-M value (default = `true`)? """ -struct BigM <: AbstractReformulationMethod - value::Float64 +struct BigM{T} <: AbstractReformulationMethod + value::T tighten::Bool - variable_bounds::Dict{VariableRef, Tuple{Float64, Float64}} # TODO support other number types? - function BigM(val = 1e9, tight = true) - new(val, tight, Dict{VariableRef, Tuple{Float64, Float64}}()) + function BigM(val::T = 1e9, tight = true) where {T} + new{T}(val, tight) end -end # TODO add fields if needed +end """ - Hull <: AbstractReformulationMethod + Hull{T} <: AbstractReformulationMethod A type for using the convex hull reformulation approach for disjunctive constraints. **Fields** -- `value::Float64`: epsilon value for nonlinear hull reformulations (default = `1e-6`). +- `value::T`: epsilon value for nonlinear hull reformulations (default = `1e-6`). """ -struct Hull <: AbstractReformulationMethod # TODO add fields if needed - value::Float64 - variable_bounds::Dict{VariableRef, Tuple{Float64, Float64}} # TODO support other number types? - function Hull(ϵ::Float64 = 1e-6) - new(ϵ, Dict{VariableRef, Tuple{Float64, Float64}}()) - end - function Hull(ϵ::Float64, v_bounds::Dict{VariableRef, Tuple{Float64, Float64}}) - new(ϵ, v_bounds) +struct Hull{T} <: AbstractReformulationMethod + value::T + function Hull(ϵ::T = 1e-6) where {T} + new{T}(ϵ) end end - # temp struct to store variable disaggregations (reset for each disjunction) -mutable struct _Hull <: AbstractReformulationMethod - value::Float64 - variable_bounds::Dict{VariableRef, Tuple{Float64, Float64}} # TODO support other number types? - disjunction_variables::Dict{VariableRef, Vector{VariableRef}} - disjunct_variables::Dict{Tuple{VariableRef,VariableRef}, VariableRef} - function _Hull(method::Hull, vrefs::Set{VariableRef}) - new( +mutable struct _Hull{V <: JuMP.AbstractVariableRef, T} <: AbstractReformulationMethod + value::T + disjunction_variables::Dict{V, Vector{V}} + disjunct_variables::Dict{Tuple{V, V}, V} + function _Hull(method::Hull{T}, vrefs::Set{V}) where {T, V <: JuMP.AbstractVariableRef} + new{V, T}( method.value, - method.variable_bounds, - Dict{VariableRef, Vector{VariableRef}}(vref => Vector{VariableRef}() for vref in vrefs), - Dict{Tuple{VariableRef,VariableRef}, VariableRef}() + Dict{V, Vector{V}}(vref => V[] for vref in vrefs), + Dict{Tuple{V, V}, V}() ) end end @@ -374,47 +405,52 @@ struct Indicator <: AbstractReformulationMethod end # GDP Data ################################################################################ """ - GDPData + GDPData{M <: JuMP.AbstractModel, V <: JuMP.AbstractVariableRef, CrefType, ValueType} The core type for storing information in a [`GDPModel`](@ref). """ -mutable struct GDPData +mutable struct GDPData{M <: JuMP.AbstractModel, V <: JuMP.AbstractVariableRef, C, T} # Objects logical_variables::_MOIUC.CleverDict{LogicalVariableIndex, LogicalVariableData} logical_constraints::_MOIUC.CleverDict{LogicalConstraintIndex, ConstraintData} disjunct_constraints::_MOIUC.CleverDict{DisjunctConstraintIndex, ConstraintData} - disjunctions::_MOIUC.CleverDict{DisjunctionIndex, ConstraintData{Disjunction}} + disjunctions::_MOIUC.CleverDict{DisjunctionIndex, ConstraintData{Disjunction{M}}} # Exactly one constraint mappings - exactly1_constraints::Dict{DisjunctionRef, LogicalConstraintRef} + exactly1_constraints::Dict{DisjunctionRef{M}, LogicalConstraintRef{M}} # 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 + indicator_to_binary::Dict{LogicalVariableRef{M}, 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 + + # Helpful metadata for most reformulations (not just one of them) + variable_bounds::Dict{V, Tuple{T, T}} # Reformulation variables and constraints - reformulation_variables::Vector{VariableRef} - reformulation_constraints::Vector{ConstraintRef} + reformulation_variables::Vector{V} + reformulation_constraints::Vector{C} # Solution data solution_method::Union{Nothing, AbstractSolutionMethod} ready_to_optimize::Bool # Default constructor - function GDPData() - new(_MOIUC.CleverDict{LogicalVariableIndex, LogicalVariableData}(), + function GDPData{M, V, C}() where {M <: JuMP.AbstractModel, V <: JuMP.AbstractVariableRef, C} + T = JuMP.value_type(M) + new{M, V, C, T}(_MOIUC.CleverDict{LogicalVariableIndex, LogicalVariableData}(), _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}(), + _MOIUC.CleverDict{DisjunctionIndex, ConstraintData{Disjunction{M}}}(), + Dict{DisjunctionRef{M}, LogicalConstraintRef{M}}(), + Dict{LogicalVariableRef{M}, V}(), + Dict{LogicalVariableRef{M}, Vector{Union{DisjunctConstraintRef{M}, DisjunctionRef{M}}}}(), + Dict{Union{DisjunctConstraintRef{M}, DisjunctionRef{M}}, LogicalVariableRef{M}}(), + Dict{V, Tuple{T, T}}(), + Vector{V}(), + Vector{C}(), nothing, false, - ) + ) end end diff --git a/src/hull.jl b/src/hull.jl index c6dca98..daabe34 100644 --- a/src/hull.jl +++ b/src/hull.jl @@ -1,35 +1,75 @@ ################################################################################ # VARIABLE DISAGGREGATION ################################################################################ -function _update_variable_bounds(vref::VariableRef, method::Hull) - if is_binary(vref) #not used - lb, ub = 0, 1 - elseif !has_lower_bound(vref) || !has_upper_bound(vref) - error("Variable $vref must have both lower and upper bounds defined when using the Hull reformulation.") - else - lb = min(0, lower_bound(vref)) - ub = max(0, upper_bound(vref)) - end - return lb, ub +""" + requires_disaggregation(vref::JuMP.AbstractVariableRef)::Bool + +Return a `Bool` whether `vref` requires disaggregation for the [`Hull`](@ref) +reformulation. This is intended as an extension point for interfaces with +DisjunctiveProgramming that use variable reference types that are not +`JuMP.GenericVariableRef`s. Errors if `vref` is not a `JuMP.GenericVariableRef`. +See also [`make_disaggregated_variable`](@ref). +""" +requires_disaggregation(vref::JuMP.GenericVariableRef) = true +function requires_disaggregation(::V) where {V} + error("`Hull` method does not support expressions with variable " * + "references of type `$V`.") +end + +""" + make_disaggregated_variable( + model::JuMP.AbstractModel, + vref::JuMP.AbstractVariableRef, + name::String, + lower_bound::Number, + upper_bound::Number + )::JuMP.AbstractVariableRef + +Creates and adds a variable to `model` with name `name` and bounds `lower_bound` +and `upper_bound` based on the original variable `vref`. This is used to +create dissagregated variables needed for the [`Hull`](@ref) reformulation. +This is implemented for `model::JuMP.GenericModel` and +`vref::JuMP.GenericVariableRef`, but it serves as an extension point for +interfaces with other model/variable reference types. This also requires +the implementation of [`requires_disaggregation`](@ref). +""" +function make_disaggregated_variable( + model::JuMP.GenericModel, + vref::JuMP.GenericVariableRef, + name, + lb, + ub + ) + return JuMP.@variable(model, base_name = name, lower_bound = lb, upper_bound = ub) end -function _disaggregate_variables(model::Model, lvref::LogicalVariableRef, vrefs::Set{VariableRef}, method::_Hull) + +function _disaggregate_variables( + model::JuMP.AbstractModel, + lvref::LogicalVariableRef, + vrefs::Set, + method::_Hull + ) #create disaggregated variables for that disjunct for vref in vrefs - is_binary(vref) && continue #skip binary variables + if !requires_disaggregation(vref) || is_binary(vref) + continue # skip variables that don't require dissagregation + end _disaggregate_variable(model, lvref, vref, method) #create disaggregated var for that disjunct end end -function _disaggregate_variable(model::Model, lvref::LogicalVariableRef, vref::VariableRef, method::_Hull) +function _disaggregate_variable( + model::JuMP.AbstractModel, + lvref::LogicalVariableRef, + vref::JuMP.AbstractVariableRef, + method::_Hull + ) #create disaggregated vref - lb, ub = method.variable_bounds[vref] - dvref = @variable(model, base_name = "$(vref)_$(lvref)", lower_bound = lb, upper_bound = ub) + lb, ub = variable_bound_info(vref) + dvref = make_disaggregated_variable(model, vref, "$(vref)_$(lvref)", lb, ub) push!(_reformulation_variables(model), dvref) #get binary indicator variable - bvref = _indicator_to_binary(model)[lvref] + bvref = binary_variable(lvref) #temp storage - if !haskey(method.disjunction_variables, vref) #NOTE: not needed because _Hull disjunction_variables is initialized with all the variables in the disjunction - method.disjunction_variables[vref] = Vector{VariableRef}() - end push!(method.disjunction_variables[vref], dvref) method.disjunct_variables[vref, bvref] = dvref #create bounding constraints @@ -45,12 +85,15 @@ end ################################################################################ # VARIABLE AGGREGATION ################################################################################ -function _aggregate_variable(model::Model, ref_cons::Vector{AbstractConstraint}, vref::VariableRef, method::_Hull) +function _aggregate_variable( + model::JuMP.AbstractModel, + ref_cons::Vector{JuMP.AbstractConstraint}, + vref::JuMP.AbstractVariableRef, + method::_Hull + ) is_binary(vref) && return #skip binary variables con_expr = @expression(model, -vref + sum(method.disjunction_variables[vref])) - push!(ref_cons, - build_constraint(error, con_expr, _MOI.EqualTo(0)) - ) + push!(ref_cons, build_constraint(error, con_expr, _MOI.EqualTo(0))) return end @@ -58,7 +101,12 @@ end # CONSTRAINT DISAGGREGATION ################################################################################ # variable -function _disaggregate_expression(model::Model, vref::VariableRef, bvref::VariableRef, method::_Hull) +function _disaggregate_expression( + model::JuMP.AbstractModel, + vref::JuMP.AbstractVariableRef, + bvref::JuMP.AbstractVariableRef, + method::_Hull + ) if is_binary(vref) || !haskey(method.disjunct_variables, (vref, bvref)) #keep any binary variables or nested disaggregated variables unchanged return vref #NOTE: not needed because nested constraint of the form `vref in MOI.AbstractScalarSet` gets reformulated to an affine expression. else #replace with disaggregated form @@ -66,7 +114,12 @@ function _disaggregate_expression(model::Model, vref::VariableRef, bvref::Variab end end # affine expression -function _disaggregate_expression(model::Model, aff::AffExpr, bvref::VariableRef, method::_Hull) +function _disaggregate_expression( + model::JuMP.AbstractModel, + aff::JuMP.GenericAffExpr, + bvref::JuMP.AbstractVariableRef, + method::_Hull + ) new_expr = @expression(model, aff.constant*bvref) #multiply constant by binary indicator variable for (vref, coeff) in aff.terms if is_binary(vref) || !haskey(method.disjunct_variables, (vref, bvref)) #keep any binary variables or nested disaggregated variables unchanged @@ -81,24 +134,39 @@ end # quadratic expression # TODO review what happens when there are bilinear terms with binary variables involved since these are not being disaggregated # (e.g., complementarity constraints; though likely irrelevant)... -function _disaggregate_expression(model::Model, quad::QuadExpr, bvref::VariableRef, method::_Hull) +function _disaggregate_expression( + model::JuMP.AbstractModel, + quad::JuMP.GenericQuadExpr, + bvref::JuMP.AbstractVariableRef, + method::_Hull + ) #get affine part new_expr = _disaggregate_expression(model, quad.aff, bvref, method) - #get nonlinear part + #get quadratic part ϵ = method.value for (pair, coeff) in quad.terms da_ref = method.disjunct_variables[pair.a, bvref] db_ref = method.disjunct_variables[pair.b, bvref] - new_expr += coeff * da_ref * db_ref / ((1-ϵ)*bvref+ϵ) + new_expr += coeff * da_ref * db_ref / ((1-ϵ)*bvref+ϵ) end return new_expr end # constant in NonlinearExpr -function _disaggregate_nl_expression(model::Model, c::Number, ::VariableRef, method::_Hull) +function _disaggregate_nl_expression( + ::JuMP.AbstractModel, + c::Number, + ::JuMP.AbstractVariableRef, + ::_Hull + ) return c end # variable in NonlinearExpr -function _disaggregate_nl_expression(model::Model, vref::VariableRef, bvref::VariableRef, method::_Hull) +function _disaggregate_nl_expression( + ::JuMP.AbstractModel, + vref::JuMP.AbstractVariableRef, + bvref::JuMP.AbstractVariableRef, + method::_Hull + ) ϵ = method.value if is_binary(vref) || !haskey(method.disjunct_variables, (vref, bvref)) #keep any binary variables or nested disaggregated variables unchanged dvref = vref @@ -108,7 +176,12 @@ function _disaggregate_nl_expression(model::Model, vref::VariableRef, bvref::Var return dvref / ((1-ϵ)*bvref+ϵ) end # affine expression in NonlinearExpr -function _disaggregate_nl_expression(model::Model, aff::AffExpr, bvref::VariableRef, method::_Hull) +function _disaggregate_nl_expression( + ::JuMP.AbstractModel, + aff::JuMP.GenericAffExpr, + bvref::JuMP.AbstractVariableRef, + method::_Hull + ) new_expr = aff.constant ϵ = method.value for (vref, coeff) in aff.terms @@ -124,7 +197,11 @@ end # quadratic expression in NonlinearExpr # TODO review what happens when there are bilinear terms with binary variables involved since these are not being disaggregated # (e.g., complementarity constraints; though likely irrelevant)... -function _disaggregate_nl_expression(model::Model, quad::QuadExpr, bvref::VariableRef, method::_Hull) +function _disaggregate_nl_expression( + model::JuMP.AbstractModel, + quad::JuMP.GenericQuadExpr, + bvref::JuMP.AbstractVariableRef, + method::_Hull) #get affine part new_expr = _disaggregate_nl_expression(model, quad.aff, bvref, method) #get quadratic part @@ -137,12 +214,17 @@ function _disaggregate_nl_expression(model::Model, quad::QuadExpr, bvref::Variab return new_expr end # nonlinear expression in NonlinearExpr -function _disaggregate_nl_expression(model::Model, nlp::NonlinearExpr, bvref::VariableRef, method::_Hull) +function _disaggregate_nl_expression( + model::JuMP.AbstractModel, + nlp::NLP, + bvref::JuMP.AbstractVariableRef, + method::_Hull + ) where {NLP <: JuMP.GenericNonlinearExpr} new_args = Vector{Any}(undef, length(nlp.args)) for (i,arg) in enumerate(nlp.args) new_args[i] = _disaggregate_nl_expression(model, arg, bvref, method) end - new_expr = NonlinearExpr(nlp.head, new_args) + new_expr = NLP(nlp.head, new_args) return new_expr end @@ -151,12 +233,19 @@ end ################################################################################ requires_exactly1(::Hull) = true -function _reformulate_disjunctions(model::Model, method::Hull) - _query_variable_bounds(model, method) - _reformulate_all_disjunctions(model, method) +requires_variable_bound_info(::Hull) = true + +function set_variable_bound_info(vref::JuMP.AbstractVariableRef, ::Hull) + if !has_lower_bound(vref) || !has_upper_bound(vref) + error("Variable $vref must have both lower and upper bounds defined when using the Hull reformulation.") + else + lb = min(0, lower_bound(vref)) + ub = max(0, upper_bound(vref)) + end + return lb, ub end -function reformulate_disjunction(model::Model, disj::Disjunction, method::Hull) +function reformulate_disjunction(model::JuMP.AbstractModel, disj::Disjunction, method::Hull) ref_cons = Vector{AbstractConstraint}() #store reformulated constraints disj_vrefs = _get_disjunction_variables(model, disj) hull = _Hull(method, disj_vrefs) @@ -169,16 +258,16 @@ function reformulate_disjunction(model::Model, disj::Disjunction, method::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)) +function reformulate_disjunction(model::JuMP.AbstractModel, disj::Disjunction, method::_Hull) + return reformulate_disjunction(model, disj, Hull(method.value)) end function reformulate_disjunct_constraint( - model::Model, + model::JuMP.AbstractModel, con::ScalarConstraint{T, S}, - bvref::VariableRef, + bvref::JuMP.AbstractVariableRef, method::_Hull -) where {T <: Union{VariableRef, AffExpr, QuadExpr}, S <: Union{_MOI.LessThan, _MOI.GreaterThan, _MOI.EqualTo}} +) where {T <: JuMP.AbstractJuMPScalar, S <: Union{_MOI.LessThan, _MOI.GreaterThan, _MOI.EqualTo}} new_func = _disaggregate_expression(model, con.func, bvref, method) set_value = _set_value(con.set) new_func -= set_value*bvref @@ -186,11 +275,11 @@ function reformulate_disjunct_constraint( return [reform_con] end function reformulate_disjunct_constraint( - model::Model, + model::JuMP.AbstractModel, con::VectorConstraint{T, S, R}, - bvref::VariableRef, + bvref::JuMP.AbstractVariableRef, method::_Hull -) where {T <: Union{VariableRef, AffExpr, QuadExpr}, S <: Union{_MOI.Nonpositives, _MOI.Nonnegatives, _MOI.Zeros}, R} +) where {T <: JuMP.AbstractJuMPScalar, S <: Union{_MOI.Nonpositives, _MOI.Nonnegatives, _MOI.Zeros}, R} new_func = @expression(model, [i=1:con.set.dimension], _disaggregate_expression(model, con.func[i], bvref, method) ) @@ -198,9 +287,9 @@ function reformulate_disjunct_constraint( return [reform_con] end function reformulate_disjunct_constraint( - model::Model, + model::JuMP.AbstractModel, con::ScalarConstraint{T, S}, - bvref::VariableRef, + bvref::JuMP.AbstractVariableRef, method::_Hull ) where {T <: GenericNonlinearExpr, S <: Union{_MOI.LessThan, _MOI.GreaterThan, _MOI.EqualTo}} con_func = _disaggregate_nl_expression(model, con.func, bvref, method) @@ -215,9 +304,9 @@ function reformulate_disjunct_constraint( return [reform_con] end function reformulate_disjunct_constraint( - model::Model, + model::JuMP.AbstractModel, con::VectorConstraint{T, S, R}, - bvref::VariableRef, + bvref::JuMP.AbstractVariableRef, method::_Hull ) where {T <: GenericNonlinearExpr, S <: Union{_MOI.Nonpositives, _MOI.Nonnegatives, _MOI.Zeros}, R} con_func = @expression(model, [i=1:con.set.dimension], @@ -235,11 +324,11 @@ function reformulate_disjunct_constraint( return [reform_con] end function reformulate_disjunct_constraint( - model::Model, + model::JuMP.AbstractModel, con::ScalarConstraint{T, S}, - bvref::VariableRef, + bvref::JuMP.AbstractVariableRef, method::_Hull -) where {T <: Union{VariableRef, AffExpr, QuadExpr}, S <: _MOI.Interval} +) where {T <: JuMP.AbstractJuMPScalar, S <: _MOI.Interval} new_func = _disaggregate_expression(model, con.func, bvref, method) new_func_gt = @expression(model, new_func - con.set.lower*bvref) new_func_lt = @expression(model, new_func - con.set.upper*bvref) @@ -248,9 +337,9 @@ function reformulate_disjunct_constraint( return [reform_con_gt, reform_con_lt] end function reformulate_disjunct_constraint( - model::Model, + model::JuMP.AbstractModel, con::ScalarConstraint{T, S}, - bvref::VariableRef, + bvref::JuMP.AbstractVariableRef, method::_Hull ) where {T <: GenericNonlinearExpr, S <: _MOI.Interval} con_func = _disaggregate_nl_expression(model, con.func, bvref, method) @@ -265,4 +354,4 @@ function reformulate_disjunct_constraint( reform_con_gt = build_constraint(error, new_func_gt, _MOI.GreaterThan(0)) reform_con_lt = build_constraint(error, new_func_lt, _MOI.LessThan(0)) return [reform_con_gt, reform_con_lt] -end \ No newline at end of file +end diff --git a/src/indicator.jl b/src/indicator.jl index d199827..0dc495a 100644 --- a/src/indicator.jl +++ b/src/indicator.jl @@ -3,20 +3,20 @@ ################################################################################ #scalar disjunct constraint function reformulate_disjunct_constraint( - model::Model, + ::JuMP.AbstractModel, con::ScalarConstraint{T, S}, - bvref::VariableRef, - method::Indicator + bvref::JuMP.AbstractVariableRef, + ::Indicator ) where {T, S} reform_con = build_constraint(error, [1*bvref, con.func], _MOI.Indicator{_MOI.ACTIVATE_ON_ONE}(con.set)) return [reform_con] end #vectorized disjunct constraint function reformulate_disjunct_constraint( - model::Model, + ::JuMP.AbstractModel, con::VectorConstraint{T, S}, - bvref::VariableRef, - method::Indicator + bvref::JuMP.AbstractVariableRef, + ::Indicator ) where {T, S} set = _vec_to_scalar_set(con.set) return [ @@ -26,10 +26,10 @@ function reformulate_disjunct_constraint( 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( - model::Model, + ::JuMP.AbstractModel, con::VectorConstraint{T, S}, - bvref::VariableRef, - method::Indicator + ::JuMP.AbstractVariableRef, + ::Indicator ) where {T, S <: _MOI.Indicator} return [con] end \ No newline at end of file diff --git a/src/logic.jl b/src/logic.jl index e914476..e462e04 100644 --- a/src/logic.jl +++ b/src/logic.jl @@ -48,23 +48,23 @@ end function _eliminate_equivalence(lvar::LogicalVariableRef) return lvar end -function _eliminate_equivalence(lexpr::_LogicalExpr) +function _eliminate_equivalence(lexpr::_LogicalExpr{M}) where {M} if lexpr.head == :(==) A = _eliminate_equivalence(lexpr.args[1]) if length(lexpr.args) > 2 - nested = _LogicalExpr(:(==), Vector{Any}(lexpr.args[2:end])) + nested = _LogicalExpr{M}(:(==), Vector{Any}(lexpr.args[2:end])) B = _eliminate_equivalence(nested) elseif length(lexpr.args) == 2 B = _eliminate_equivalence(lexpr.args[2]) else error("The equivalence logic operator must have at least two clauses.") end - new_lexpr = _LogicalExpr(:&&, Any[ - _LogicalExpr(:(=>), Any[A, B]), - _LogicalExpr(:(=>), Any[B, A]) + new_lexpr = _LogicalExpr{M}(:&&, Any[ + _LogicalExpr{M}(:(=>), Any[A, B]), + _LogicalExpr{M}(:(=>), Any[B, A]) ]) else - new_lexpr = _LogicalExpr(lexpr.head, Any[ + new_lexpr = _LogicalExpr{M}(lexpr.head, Any[ _eliminate_equivalence(arg) for arg in lexpr.args ]) end @@ -75,19 +75,19 @@ end function _eliminate_implication(lvar::LogicalVariableRef) return lvar end -function _eliminate_implication(lexpr::_LogicalExpr) +function _eliminate_implication(lexpr::_LogicalExpr{M}) where {M} if lexpr.head == :(=>) if length(lexpr.args) != 2 error("The implication operator must have two clauses.") end A = _eliminate_implication(lexpr.args[1]) B = _eliminate_implication(lexpr.args[2]) - new_lexpr = _LogicalExpr(:||, Any[ - _LogicalExpr(:!, Any[A]), + new_lexpr = _LogicalExpr{M}(:||, Any[ + _LogicalExpr{M}(:!, Any[A]), B ]) else - new_lexpr = _LogicalExpr(lexpr.head, Any[ + new_lexpr = _LogicalExpr{M}(lexpr.head, Any[ _eliminate_implication(arg) for arg in lexpr.args ]) end @@ -98,22 +98,22 @@ end function _move_negations_inward(lvar::LogicalVariableRef) return lvar end -function _move_negations_inward(lexpr::_LogicalExpr) +function _move_negations_inward(lexpr::_LogicalExpr{M}) where {M} if lexpr.head == :! if length(lexpr.args) != 1 error("The negation operator can only have one clause.") end new_lexpr = _negate(lexpr.args[1]) else - new_lexpr = _LogicalExpr(lexpr.head, Any[ + new_lexpr = _LogicalExpr{M}(lexpr.head, Any[ _move_negations_inward(arg) for arg in lexpr.args ]) end return new_lexpr end -function _negate(lvar::LogicalVariableRef) - return _LogicalExpr(:!, Any[lvar]) +function _negate(lvar::LogicalVariableRef{M}) where {M} + return _LogicalExpr{M}(:!, Any[lvar]) end function _negate(lexpr::_LogicalExpr) if lexpr.head == :|| @@ -127,22 +127,22 @@ function _negate(lexpr::_LogicalExpr) end end -function _negate_or(lexpr::_LogicalExpr) +function _negate_or(lexpr::_LogicalExpr{M}) where {M} if length(lexpr.args) < 2 error("The OR operator must have at least two clauses.") end - return _LogicalExpr(:&&, Any[ #flip OR to AND - _move_negations_inward(_LogicalExpr(:!, Any[arg])) + return _LogicalExpr{M}(:&&, Any[ #flip OR to AND + _move_negations_inward(_LogicalExpr{M}(:!, Any[arg])) for arg in lexpr.args ]) end -function _negate_and(lexpr::_LogicalExpr) +function _negate_and(lexpr::_LogicalExpr{M}) where {M} if length(lexpr.args) < 2 error("The AND operator must have at least two clauses.") end - return _LogicalExpr(:||, Any[ #flip AND to OR - _move_negations_inward(_LogicalExpr(:!, Any[arg])) + return _LogicalExpr{M}(:||, Any[ #flip AND to OR + _move_negations_inward(_LogicalExpr{M}(:!, Any[arg])) for arg in lexpr.args ]) end @@ -157,7 +157,7 @@ end function _distribute_and_over_or(lvar::LogicalVariableRef) return lvar end -function _distribute_and_over_or(lexpr0::_LogicalExpr) +function _distribute_and_over_or(lexpr0::_LogicalExpr{M}) where {M} lexpr = _flatten(lexpr0) if lexpr.head == :|| if length(lexpr.args) < 2 @@ -165,9 +165,9 @@ function _distribute_and_over_or(lexpr0::_LogicalExpr) end loc = findfirst(arg -> arg isa _LogicalExpr ? arg.head == :&& : false, lexpr.args) if !isnothing(loc) - new_lexpr = _LogicalExpr(:&&, Any[ + new_lexpr = _LogicalExpr{M}(:&&, Any[ _distribute_and_over_or( - _LogicalExpr(:||, Any[arg_i, lexpr.args[setdiff(1:end,loc)]...]) + _LogicalExpr{M}(:||, Any[arg_i, lexpr.args[setdiff(1:end,loc)]...]) ) for arg_i in lexpr.args[loc].args ]) @@ -175,7 +175,7 @@ function _distribute_and_over_or(lexpr0::_LogicalExpr) new_lexpr = lexpr end else - new_lexpr = _LogicalExpr(lexpr.head, Any[ + new_lexpr = _LogicalExpr{M}(lexpr.head, Any[ _distribute_and_over_or(arg) for arg in lexpr.args ]) end @@ -187,7 +187,7 @@ end function _flatten(lvar::LogicalVariableRef) return lvar end -function _flatten(lexpr::_LogicalExpr) +function _flatten(lexpr::_LogicalExpr{M}) where {M} if lexpr.head in (:&&, :||) nary_args = Set{Any}() for arg in lexpr.args @@ -205,9 +205,9 @@ function _flatten(lexpr::_LogicalExpr) push!(nary_args, arg_flat) end end - new_lexpr = _LogicalExpr(lexpr.head, collect(nary_args)) + new_lexpr = _LogicalExpr{M}(lexpr.head, collect(nary_args)) else - new_lexpr = _LogicalExpr(lexpr.head, Any[ + new_lexpr = _LogicalExpr{M}(lexpr.head, Any[ _flatten(arg) for arg in lexpr.args ]) end @@ -218,18 +218,23 @@ end # SELECTOR REFORMULATION ################################################################################ # cardinality constraint reformulation -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]] - # TODO better handle form of func[1] - c = first(func) isa Number ? first(func) : JuMP.constant(func[1]) +function _reformulate_selector( + model::JuMP.AbstractModel, + func::Vector{AbstractJuMPScalar}, + set::AbstractCardinalitySet + ) + bvrefs = [binary_variable(lvref) for lvref in func[2:end]] + c = 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 -function _reformulate_selector(model::Model, func::Vector{LogicalVariableRef}, set::Union{_MOIAtLeast, _MOIAtMost, _MOIExactly}) - dict = _indicator_to_binary(model) - bvref, bvrefs... = [dict[lvref] for lvref in func] +function _reformulate_selector( + model::JuMP.AbstractModel, + func::Vector{<:LogicalVariableRef}, + set::AbstractCardinalitySet + ) + bvref, bvrefs... = [binary_variable(lvref) for lvref in func] new_set = _vec_to_scalar_set(set)(0) cref = @constraint(model, sum(bvrefs) - bvref in new_set) push!(_reformulation_constraints(model), cref) @@ -238,7 +243,7 @@ end ################################################################################ # PROPOSITION REFORMULATION ################################################################################ -function _reformulate_proposition(model::Model, lexpr::_LogicalExpr) +function _reformulate_proposition(model::JuMP.AbstractModel, lexpr::_LogicalExpr) expr = _to_cnf(lexpr) if expr.head == :&& for arg in expr.args @@ -256,7 +261,10 @@ _isa_literal(v::LogicalVariableRef) = true _isa_literal(v::_LogicalExpr) = (v.head == :!) && (length(v.args) == 1) && _isa_literal(v.args[1]) _isa_literal(v) = false -function _add_reformulated_proposition(model::Model, arg::Union{LogicalVariableRef,_LogicalExpr}) +function _add_reformulated_proposition( + model::JuMP.AbstractModel, + arg::Union{LogicalVariableRef, _LogicalExpr} + ) func = _reformulate_clause(model, arg) if !isempty(func.terms) && !all(iszero.(values(func.terms))) cref = @constraint(model, func >= 1) @@ -265,13 +273,13 @@ function _add_reformulated_proposition(model::Model, arg::Union{LogicalVariableR return end -function _reformulate_clause(model::Model, lvref::LogicalVariableRef) - func = 1 * _indicator_to_binary(model)[lvref] +function _reformulate_clause(model::JuMP.AbstractModel, lvref::LogicalVariableRef) + func = 1 * binary_variable(lvref) return func end -function _reformulate_clause(model::Model, lexpr::_LogicalExpr) - func = zero(AffExpr) #initialize func expression +function _reformulate_clause(model::M, lexpr::_LogicalExpr) where {M <: JuMP.AbstractModel} + func = zero(JuMP.GenericAffExpr{JuMP.value_type(M), JuMP.variable_ref_type(M)}) #initialize func expression if _isa_literal(lexpr) add_to_expression!(func, 1 - _reformulate_clause(model, lexpr.args[1])) elseif lexpr.head == :|| diff --git a/src/model.jl b/src/model.jl index 794db35..0f26a4f 100644 --- a/src/model.jl +++ b/src/model.jl @@ -1,76 +1,100 @@ ################################################################################ # GDP MODEL ################################################################################ +# Enables the use of parametric typing in the GDPModel function +struct GDPModel{M, V, C} end """ - GDPModel([optimizer]; [kwargs...])::Model + GDPModel([optimizer]; [kwargs...])::JuMP.Model + + GDPModel{T}([optimizer]; [kwargs...])::JuMP.GenericModel{T} + + GDPModel{M <: JuMP.AbstractModel, VrefType, CrefType}([optimizer], [args...]; [kwargs...])::M The core model object for building general disjunction programming models. """ -function GDPModel(args...; kwargs...) - model = Model(args...; kwargs...) - model.ext[:GDP] = GDPData() - set_optimize_hook(model, _optimize_hook) +function GDPModel{M, V, C}( + args...; + kwargs... + ) where {M <: JuMP.AbstractModel, V <: JuMP.AbstractVariableRef, C} + model = M(args...; kwargs...) + model.ext[:GDP] = GDPData{M, V, C}() + JuMP.set_optimize_hook(model, _optimize_hook) return model end +function GDPModel{T}(args...; kwargs...) where {T} + return GDPModel{ + JuMP.GenericModel{T}, + JuMP.GenericVariableRef{T}, + JuMP.ConstraintRef + }(args...; kwargs...) +end +function GDPModel(args...; kwargs...) + return GDPModel{ + JuMP.Model, + JuMP.VariableRef, + JuMP.ConstraintRef + }(args...; kwargs...) +end # Define what should happen to solve a GDPModel # See https://github.com/jump-dev/JuMP.jl/blob/9ea1df38fd320f864ab4c93c78631d0f15939c0b/src/JuMP.jl#L718-L745 function _optimize_hook( - model::Model; - method::AbstractSolutionMethod = BigM() + model::JuMP.AbstractModel; + gdp_method::AbstractSolutionMethod = BigM(), + kwargs... ) # can add more kwargs if wanted - if !_ready_to_optimize(model) || _solution_method(model) != method - reformulate_model(model, method) + if !_ready_to_optimize(model) || _solution_method(model) != gdp_method + reformulate_model(model, gdp_method) end - return optimize!(model; ignore_optimize_hook = true) + return JuMP.optimize!(model; ignore_optimize_hook = true, kwargs...) end ################################################################################ # GDP DATA ################################################################################ - """ - gdp_data(model::Model)::GDPData + gdp_data(model::JuMP.AbstractModel)::GDPData Extract the [`GDPData`](@ref) from a `GDPModel`. """ -function gdp_data(model::Model) - is_gdp_model(model) || error("Cannot access GDP data from a regular `JuMP.Model`.") +function gdp_data(model::JuMP.AbstractModel) + is_gdp_model(model) || error("Model does not contain GDP data.") return model.ext[:GDP] end """ - is_gdp_model(model::Model)::Bool + is_gdp_model(model::JuMP.AbstractModel)::Bool Return if `model` was created via the [`GDPModel`](@ref) constructor. """ -function is_gdp_model(model::Model) +function is_gdp_model(model::JuMP.AbstractModel) return haskey(model.ext, :GDP) end # Create accessors for GDP data fields -_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 -_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 +_logical_variables(model::JuMP.AbstractModel) = gdp_data(model).logical_variables +_logical_constraints(model::JuMP.AbstractModel) = gdp_data(model).logical_constraints +_disjunct_constraints(model::JuMP.AbstractModel) = gdp_data(model).disjunct_constraints +_disjunctions(model::JuMP.AbstractModel) = gdp_data(model).disjunctions +_exactly1_constraints(model::JuMP.AbstractModel) = gdp_data(model).exactly1_constraints +_indicator_to_binary(model::JuMP.AbstractModel) = gdp_data(model).indicator_to_binary +_indicator_to_constraints(model::JuMP.AbstractModel) = gdp_data(model).indicator_to_constraints +_constraint_to_indicator(model::JuMP.AbstractModel) = gdp_data(model).constraint_to_indicator +_reformulation_variables(model::JuMP.AbstractModel) = gdp_data(model).reformulation_variables +_reformulation_constraints(model::JuMP.AbstractModel) = gdp_data(model).reformulation_constraints +_variable_bounds(model::JuMP.AbstractModel) = gdp_data(model).variable_bounds +_solution_method(model::JuMP.AbstractModel) = gdp_data(model).solution_method # Get the current solution method +_ready_to_optimize(model::JuMP.AbstractModel) = 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) +function _set_ready_to_optimize(model::JuMP.AbstractModel, is_ready::Bool) gdp_data(model).ready_to_optimize = is_ready return end # Set the solution method -function _set_solution_method(model::Model, method::AbstractSolutionMethod) +function _set_solution_method(model::JuMP.AbstractModel, method::AbstractSolutionMethod) gdp_data(model).solution_method = method return end diff --git a/src/reformulate.jl b/src/reformulate.jl index a95de7a..3784e95 100644 --- a/src/reformulate.jl +++ b/src/reformulate.jl @@ -2,45 +2,30 @@ # REFORMULATE ################################################################################ """ - reformulate_model(model::JuMP.Model, method::AbstractSolutionMethod = BigM()) + reformulate_model(model::JuMP.AbstractModel, method::AbstractSolutionMethod = BigM()) Reformulate a `GDPModel` using the specified `method`. Prior to reformulation, all previous reformulation variables and constraints are deleted. """ -function reformulate_model(model::JuMP.Model, method::AbstractSolutionMethod = BigM()) +function reformulate_model(model::JuMP.AbstractModel, method::AbstractSolutionMethod = BigM()) #clear all previous reformulations _clear_reformulations(model) #reformulate - _reformulate_logical_variables(model) _reformulate_disjunctions(model, method) _reformulate_logical_constraints(model) #set solution method _set_solution_method(model, method) _set_ready_to_optimize(model, true) + return end -function _clear_reformulations(model::JuMP.Model) +function _clear_reformulations(model::JuMP.AbstractModel) delete.(model, _reformulation_constraints(model)) delete.(model, _reformulation_variables(model)) empty!(gdp_data(model).reformulation_constraints) empty!(gdp_data(model).reformulation_variables) -end - -################################################################################ -# LOGICAL VARIABLES -################################################################################ -# create binary (indicator) variables for logic variables. -function _reformulate_logical_variables(model::JuMP.Model) - for (lv_idx, lv_data) in _logical_variables(model) - lv = lv_data.variable - lvref = LogicalVariableRef(model, lv_idx) - bvref = @variable(model, base_name = lv_data.name, binary = true, start = lv.start_value) - if is_fixed(lvref) - fix(bvref, fix_value(lvref)) - end - push!(_reformulation_variables(model), bvref) - _indicator_to_binary(model)[lvref] = bvref - end + empty!(gdp_data(model).variable_bounds) + return end ################################################################################ @@ -55,8 +40,39 @@ extended to return `true` if such a constraint is required (defaults to `false` """ requires_exactly1(::AbstractReformulationMethod) = false +""" + requires_variable_bound_info(method::AbstractReformulationMethod)::Bool + +Return a `Bool` whether `method` requires variable bound information accessed +via [`variable_bound_info`](@ref). This should be extended for new +[`AbstractReformulationMethod`](@ref) methods if needed (defaults to `false`). +If a new method does require variable bound information, then +[`set_variable_bound_info`](@ref) should also be extended. +""" +requires_variable_bound_info(::AbstractReformulationMethod) = false + +""" + set_variable_bound_info(vref, method::AbstractReformulationMethod)::Tuple{<:Number, <:Number} + +Returns a tuple of the form `(lower_bound, upper_bound)` which are the bound information needed by +`method` to reformulate disjunctions. This only needs to be implemented for `methods` where +`requires_variable_bound_info(method) = true`. These bounds can later be accessed via +[`variable_bound_info`](@ref). +""" +function set_variable_bound_info end + +""" + variable_bound_info(vref::JuMP.AbstractVariableRef)::Tuple{<:Number, <:Number} + +Returns a tuple of the form `(lower_bound, upper_bound)` needed to implement reformulation +methods. Only works if [`requires_variable_bound_info`](@ref) is implemented. +""" +function variable_bound_info(vref::JuMP.AbstractVariableRef) + return _variable_bounds(JuMP.owner_model(vref))[vref] +end + # disjunctions -function _reformulate_all_disjunctions(model::Model, method::AbstractReformulationMethod) +function _reformulate_disjunctions(model::JuMP.AbstractModel, method::AbstractReformulationMethod) for (idx, disj) in _disjunctions(model) disj.constraint.nested && continue #only reformulate top level disjunctions dref = DisjunctionRef(model, idx) @@ -64,6 +80,11 @@ function _reformulate_all_disjunctions(model::Model, method::AbstractReformulati error("Reformulation method `$method` requires disjunctions where only 1 disjunct is selected, " * "but `exactly1 = false` for disjunction `$dref`.") end + if requires_variable_bound_info(method) + for vref in _get_disjunction_variables(model, disj.constraint) + _variable_bounds(model)[vref] = set_variable_bound_info(vref, method) + end + 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") @@ -72,14 +93,11 @@ function _reformulate_all_disjunctions(model::Model, method::AbstractReformulati end end end -function _reformulate_disjunctions(model::JuMP.Model, method::AbstractReformulationMethod) - _reformulate_all_disjunctions(model, method) -end # disjuncts """ reformulate_disjunction( - model::JuMP.Model, + model::JuMP.AbstractModel, disj::Disjunction, method::AbstractReformulationMethod ) where {T<:Disjunction} @@ -90,8 +108,8 @@ 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. """ -function reformulate_disjunction(model::JuMP.Model, disj::Disjunction, method::AbstractReformulationMethod) - ref_cons = Vector{AbstractConstraint}() #store reformulated constraints +function reformulate_disjunction(model::JuMP.AbstractModel, disj::Disjunction, method::AbstractReformulationMethod) + ref_cons = Vector{JuMP.AbstractConstraint}() #store reformulated constraints for d in disj.indicators _reformulate_disjunct(model, ref_cons, d, method) end @@ -99,9 +117,14 @@ function reformulate_disjunction(model::JuMP.Model, disj::Disjunction, method::A end # individual disjuncts -function _reformulate_disjunct(model::JuMP.Model, ref_cons::Vector{AbstractConstraint}, lvref::LogicalVariableRef, method::AbstractReformulationMethod) +function _reformulate_disjunct( + model::JuMP.AbstractModel, + ref_cons::Vector{JuMP.AbstractConstraint}, + lvref::LogicalVariableRef, + method::AbstractReformulationMethod + ) #reformulate each constraint and add to the model - bvref = _indicator_to_binary(model)[lvref] + bvref = binary_variable(lvref) !haskey(_indicator_to_constraints(model), lvref) && return #skip if disjunct is empty for cref in _indicator_to_constraints(model)[lvref] con = constraint_object(cref) @@ -112,9 +135,9 @@ end """ reformulate_disjunct_constraint( - model::JuMP.Model, + model::JuMP.AbstractModel, con::JuMP.AbstractConstraint, - bvref::JuMP.VariableRef, + bvref::JuMP.AbstractVariableRef, method::AbstractReformulationMethod ) @@ -123,13 +146,13 @@ constraint. If `method` needs to specify how to reformulate the entire disjuncti [`reformulate_disjunction`](@ref). """ function reformulate_disjunct_constraint( - model::JuMP.Model, + model::JuMP.AbstractModel, con::Disjunction, - bvref::VariableRef, + bvref::JuMP.AbstractVariableRef, method::AbstractReformulationMethod ) ref_cons = reformulate_disjunction(model, con, method) - new_ref_cons = Vector{AbstractConstraint}() + new_ref_cons = Vector{JuMP.AbstractConstraint}() for ref_con in ref_cons append!(new_ref_cons, reformulate_disjunct_constraint(model, ref_con, bvref, method)) end @@ -138,9 +161,9 @@ end # reformulation fallback for individual disjunct constraints function reformulate_disjunct_constraint( - model::JuMP.Model, + model::JuMP.AbstractModel, con::AbstractConstraint, - bvref::VariableRef, + bvref::JuMP.AbstractVariableRef, method::AbstractReformulationMethod ) error("$(typeof(method)) reformulation for constraint $con is not supported yet.") @@ -150,15 +173,19 @@ end # LOGICAL CONSTRAINT REFORMULATION ################################################################################ # all logical constraints -function _reformulate_logical_constraints(model::JuMP.Model) +function _reformulate_logical_constraints(model::JuMP.AbstractModel) for (_, lcon) in _logical_constraints(model) _reformulate_logical_constraint(model, lcon.constraint.func, lcon.constraint.set) end end # individual logical constraints -function _reformulate_logical_constraint(model::JuMP.Model, func, set::Union{_MOIAtMost, _MOIAtLeast, _MOIExactly}) +function _reformulate_logical_constraint( + model::JuMP.AbstractModel, + func, + set::Union{_MOIAtMost, _MOIAtLeast, _MOIExactly} + ) return _reformulate_selector(model, func, set) end -function _reformulate_logical_constraint(model::JuMP.Model, func, ::MOI.EqualTo{Bool}) # set.value is always true +function _reformulate_logical_constraint(model::JuMP.AbstractModel, func, ::MOI.EqualTo{Bool}) # set.value is always true return _reformulate_proposition(model, func) end diff --git a/src/variables.jl b/src/variables.jl index 211e8fd..9ac8f12 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -3,16 +3,16 @@ ################################################################################ """ JuMP.build_variable(_error::Function, info::VariableInfo, - ::Type{LogicalVariable})::LogicalVariable + ::Union{Type{Logical}, Logical}) Extend `JuMP.build_variable` to work with logical variables. This in combination with `JuMP.add_variable` enables the use of -`@variable(model, [var_expr], LogicalVariable)`. +`@variable(model, [var_expr], Logical)`. """ function JuMP.build_variable( _error::Function, info::VariableInfo, - tag::Type{LogicalVariable}; + tag::Type{Logical}; kwargs... ) # check for invalid input @@ -35,6 +35,48 @@ function JuMP.build_variable( return LogicalVariable(fix, start) end +# Logical variable with tag data +function JuMP.build_variable( + _error::Function, + info::VariableInfo, + tag::Logical; + kwargs... + ) + lvar = JuMP.build_variable(_error, info, Logical; kwargs...) + return _TaggedLogicalVariable(lvar, tag.tag_data) +end + +# Helper functions to extract the core variable object +_get_variable(v::_TaggedLogicalVariable) = v.variable +_get_variable(v) = v + +# Helper function to add start value and fix value +function _add_logical_info(bvref, var::LogicalVariable) + if !isnothing(var.fix_value) + JuMP.fix(bvref, var.fix_value) + end + if !isnothing(var.start_value) + JuMP.set_start_value(bvref, var.start_value) + end + return +end +function _add_logical_info(bvref, var::_TaggedLogicalVariable) + return _add_logical_info(bvref, var.variable) +end + +# Dispatch on logical variable type to create a binary variable +function _make_binary_variable(model, ::LogicalVariable, name) + return JuMP.@variable(model, base_name = name, binary = true) +end +function _make_binary_variable(model, var::_TaggedLogicalVariable, name) + return JuMP.@variable( + model, + base_name = name, + binary = true, + variable_type = var.tag_data + ) +end + """ JuMP.add_variable(model::Model, v::LogicalVariable, name::String = "")::LogicalVariableRef @@ -43,15 +85,21 @@ Extend `JuMP.add_variable` for [`LogicalVariable`](@ref)s. This helps enable `@variable(model, [var_expr], Logical)`. """ function JuMP.add_variable( - model::Model, - v::LogicalVariable, + model::JuMP.AbstractModel, + v::Union{LogicalVariable, _TaggedLogicalVariable}, name::String = "" ) is_gdp_model(model) || error("Can only add logical variables to `GDPModel`s.") - data = LogicalVariableData(v, name) + # add the logical variable + data = LogicalVariableData(_get_variable(v), name) idx = _MOIUC.add_item(_logical_variables(model), data) + lvref = LogicalVariableRef(model, idx) _set_ready_to_optimize(model, false) - return LogicalVariableRef(model, idx) + # add the associated binary variables + bvref = _make_binary_variable(model, v, name) + _add_logical_info(bvref, v) + _indicator_to_binary(model)[lvref] = bvref + return lvref end # Base extensions @@ -65,23 +113,37 @@ end # return LogicalVariableRef(map.model, index(vref)) # end +# Define helpful getting functions +function _variable_object(lvref::LogicalVariableRef) + dict = _logical_variables(JuMP.owner_model(lvref)) + return dict[JuMP.index(lvref)].variable +end + +# Define helpful setting functions +function _set_variable_object(lvref::LogicalVariableRef, var::LogicalVariable) + model = JuMP.owner_model(lvref) + _logical_variables(model)[JuMP.index(lvref)].variable = var + _set_ready_to_optimize(model, false) + return +end + # JuMP extensions """ - JuMP.owner_model(vref::LogicalVariableRef) + JuMP.owner_model(vref::LogicalVariableRef)::JuMP.AbstractModel Return the `GDP model` to which `vref` belongs. """ JuMP.owner_model(vref::LogicalVariableRef) = vref.model """ - JuMP.index(vref::LogicalVariableRef) + JuMP.index(vref::LogicalVariableRef)::LogicalVariableIndex Return the index of logical variable that associated with `vref`. """ JuMP.index(vref::LogicalVariableRef) = vref.index """ - JuMP.isequal_canonical(v::LogicalVariableRef, w::LogicalVariableRef) + JuMP.isequal_canonical(v::LogicalVariableRef, w::LogicalVariableRef)::Bool Return `true` if `v` and `w` refer to the same logical variable in the same `GDP model`. @@ -89,16 +151,16 @@ Return `true` if `v` and `w` refer to the same logical variable in the same JuMP.isequal_canonical(v::LogicalVariableRef, w::LogicalVariableRef) = v == w """ - JuMP.is_valid(model::Model, vref::LogicalVariableRef) + JuMP.is_valid(model::JuMP.AbstractModel, vref::LogicalVariableRef)::Bool Return `true` if `vref` refers to a valid logical variable in `GDP model`. """ -function JuMP.is_valid(model::Model, vref::LogicalVariableRef) - return model === owner_model(vref) +function JuMP.is_valid(model::JuMP.AbstractModel, vref::LogicalVariableRef) + return model === owner_model(vref) && haskey(_logical_variables(model), JuMP.index(vref)) end """ - JuMP.name(vref::LogicalVariableRef) + JuMP.name(vref::LogicalVariableRef)::String Get a logical variable's name attribute. """ @@ -108,7 +170,7 @@ function JuMP.name(vref::LogicalVariableRef) end """ - JuMP.set_name(vref::LogicalVariableRef, name::String) + JuMP.set_name(vref::LogicalVariableRef, name::String)::Nothing Set a logical variable's name attribute. """ @@ -117,21 +179,21 @@ function JuMP.set_name(vref::LogicalVariableRef, name::String) data = gdp_data(model) data.logical_variables[index(vref)].name = name _set_ready_to_optimize(model, false) + JuMP.set_name(binary_variable(vref), name) return end """ - JuMP.start_value(vref::LogicalVariableRef) + JuMP.start_value(vref::LogicalVariableRef)::Bool Return the start value of the logical variable `vref`. """ function JuMP.start_value(vref::LogicalVariableRef) - data = gdp_data(owner_model(vref)) - return data.logical_variables[index(vref)].variable.start_value + return _variable_object(vref).start_value end """ - JuMP.set_start_value(vref::LogicalVariableRef, value::Union{Nothing, Bool}) + JuMP.set_start_value(vref::LogicalVariableRef, value::Union{Nothing, Bool})::Nothing Set the start value of the logical variable `vref`. @@ -141,99 +203,100 @@ function JuMP.set_start_value( vref::LogicalVariableRef, value::Union{Nothing, Bool} ) - model = owner_model(vref) - data = gdp_data(model) - var = data.logical_variables[index(vref)].variable - new_var = LogicalVariable(var.fix_value, value) - data.logical_variables[index(vref)].variable = new_var - _set_ready_to_optimize(model, false) + new_var = LogicalVariable(JuMP.fix_value(vref), value) + _set_variable_object(vref, new_var) + JuMP.set_start_value(binary_variable(vref), value) return end """ - JuMP.is_fixed(vref::LogicalVariableRef) + JuMP.is_fixed(vref::LogicalVariableRef)::Bool Return `true` if `vref` is a fixed variable. If `true`, the fixed value can be queried with fix_value. """ function JuMP.is_fixed(vref::LogicalVariableRef) - data = gdp_data(owner_model(vref)) - return !isnothing(data.logical_variables[index(vref)].variable.fix_value) + return !isnothing(_variable_object(vref).fix_value) end """ - JuMP.fix_value(vref::LogicalVariableRef) + JuMP.fix_value(vref::LogicalVariableRef)::Bool Return the value to which a logical variable is fixed. """ function JuMP.fix_value(vref::LogicalVariableRef) - data = gdp_data(owner_model(vref)) - return data.logical_variables[index(vref)].variable.fix_value + return _variable_object(vref).fix_value end """ - JuMP.fix(vref::LogicalVariableRef, value::Bool) + JuMP.fix(vref::LogicalVariableRef, value::Bool)::Nothing Fix a logical variable to a value. Update the fixing constraint if one exists, otherwise create a new one. """ function JuMP.fix(vref::LogicalVariableRef, value::Bool) - model = owner_model(vref) - data = gdp_data(model) - var = data.logical_variables[index(vref)].variable - new_var = LogicalVariable(value, var.start_value) - data.logical_variables[index(vref)].variable = new_var - _set_ready_to_optimize(model, false) + new_var = LogicalVariable(value, JuMP.start_value(vref)) + _set_variable_object(vref, new_var) + JuMP.fix(binary_variable(vref), value) return end """ - JuMP.unfix(vref::LogicalVariableRef) + JuMP.unfix(vref::LogicalVariableRef)::Nothing Delete the fixed value of a logical variable. """ function JuMP.unfix(vref::LogicalVariableRef) - model = owner_model(vref) - data = gdp_data(model) - var = data.logical_variables[index(vref)].variable - new_var = LogicalVariable(nothing, var.start_value) - data.logical_variables[index(vref)].variable = new_var - _set_ready_to_optimize(model, false) + new_var = LogicalVariable(nothing, JuMP.start_value(vref)) + _set_variable_object(vref, new_var) + JuMP.unfix(binary_variable(vref)) return end """ - JuMP.delete(model::Model, vref::LogicalVariableRef) + binary_variable(vref::LogicalVariableRef)::JuMP.AbstractVariableRef + +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. +""" +function binary_variable(vref::LogicalVariableRef) + model = JuMP.owner_model(vref) + return _indicator_to_binary(model)[vref] +end + +""" + JuMP.delete(model::JuMP.AbstractModel, vref::LogicalVariableRef)::Nothing Delete the logical variable associated with `vref` from the `GDP model`. """ -function JuMP.delete(model::Model, vref::LogicalVariableRef) +function JuMP.delete(model::JuMP.AbstractModel, vref::LogicalVariableRef) @assert is_valid(model, vref) "Variable does not belong to model." vidx = index(vref) dict = _logical_variables(model) #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] - delete.(model, crefs) + JuMP.delete.(model, crefs) delete!(_indicator_to_constraints(model), vref) end #delete any disjunctions that have the logical variable for (didx, ddata) in _disjunctions(model) if vref in ddata.constraint.indicators - setdiff!(ddata.constraint.indicators, [vref]) - delete(model, DisjunctionRef(model, didx)) + JuMP.delete(model, DisjunctionRef(model, didx)) end end #delete any logical constraints involving the logical variables for (cidx, cdata) in _logical_constraints(model) - lvars = _get_constraint_variables(model, cdata.constraint) + lvars = _get_logical_constraint_variables(model, cdata.constraint) if vref in lvars - delete(model, LogicalConstraintRef(model, cidx)) + JuMP.delete(model, LogicalConstraintRef(model, cidx)) end end #delete the logical variable delete!(dict, vidx) + JuMP.delete(model, binary_variable(vref)) delete!(_indicator_to_binary(model), vref) #not ready to optimize _set_ready_to_optimize(model, false) @@ -243,27 +306,24 @@ end ################################################################################ # VARIABLE INTERROGATION ################################################################################ -function _query_variable_bounds(model::Model, method::Union{Hull, BigM}) - for var in all_variables(model) - method.variable_bounds[var] = _update_variable_bounds(var, method) - end -end - -function _get_disjunction_variables(model::Model, disj::Disjunction) - vars = Set{VariableRef}() - for lvref in disj.indicators - !haskey(_indicator_to_constraints(model), lvref) && continue #skip if disjunct is empty - for cref in _indicator_to_constraints(model)[lvref] +function _get_disjunction_variables(model::M, disj::Disjunction) where {M <: JuMP.AbstractModel} + vars = Set{JuMP.variable_ref_type(M)}() + for vidx in disj.indicators + !haskey(_indicator_to_constraints(model), vidx) && continue #skip if disjunct is empty + for cref in _indicator_to_constraints(model)[vidx] con = constraint_object(cref) - _interrogate_variables(v -> push!(vars, v), con) + _interrogate_variables(Base.Fix1(push!, vars), con) end end return vars end -function _get_constraint_variables(model::Model, con::Union{ScalarConstraint, VectorConstraint}) - vars = Set{Union{VariableRef, LogicalVariableRef}}() - _interrogate_variables(v -> push!(vars, v), con.func) +function _get_logical_constraint_variables( + ::M, + con::Union{JuMP.ScalarConstraint, JuMP.VectorConstraint} + ) where {M <: JuMP.AbstractModel} + vars = Set{LogicalVariableRef{M}}() + _interrogate_variables(Base.Fix1(push!, vars), con) return vars end @@ -273,13 +333,13 @@ function _interrogate_variables(interrogator::Function, c::Number) end # VariableRef/LogicalVariableRef -function _interrogate_variables(interrogator::Function, var::Union{VariableRef, LogicalVariableRef}) +function _interrogate_variables(interrogator::Function, var::JuMP.AbstractVariableRef) interrogator(var) return end # AffExpr -function _interrogate_variables(interrogator::Function, aff::GenericAffExpr) +function _interrogate_variables(interrogator::Function, aff::JuMP.GenericAffExpr) for (var, _) in aff.terms interrogator(var) end @@ -287,7 +347,7 @@ function _interrogate_variables(interrogator::Function, aff::GenericAffExpr) end # QuadExpr -function _interrogate_variables(interrogator::Function, quad::QuadExpr) +function _interrogate_variables(interrogator::Function, quad::JuMP.GenericQuadExpr) for (pair, _) in quad.terms interrogator(pair.a) interrogator(pair.b) @@ -296,8 +356,8 @@ function _interrogate_variables(interrogator::Function, quad::QuadExpr) return end -# NonlinearExpr and _LogicalExpr (T <: Union{VariableRef, LogicalVariableRef}) -function _interrogate_variables(interrogator::Function, nlp::GenericNonlinearExpr{T}) where {T} +# NonlinearExpr +function _interrogate_variables(interrogator::Function, nlp::JuMP.GenericNonlinearExpr) for arg in nlp.args _interrogate_variables(interrogator, arg) end @@ -307,7 +367,7 @@ function _interrogate_variables(interrogator::Function, nlp::GenericNonlinearExp end # Constraint -function _interrogate_variables(interrogator::Function, con::Union{ScalarConstraint, VectorConstraint}) +function _interrogate_variables(interrogator::Function, con::JuMP.AbstractConstraint) _interrogate_variables(interrogator, con.func) end @@ -325,7 +385,7 @@ end # Nested disjunction function _interrogate_variables(interrogator::Function, disj::Disjunction) - model = owner_model(disj.indicators[1]) + model = owner_model(first(disj.indicators)) dvars = _get_disjunction_variables(model, disj) _interrogate_variables(interrogator, dvars) return @@ -334,4 +394,4 @@ end # Fallback function _interrogate_variables(interrogator::Function, other) error("Cannot extract variables from object of type $(typeof(other)).") -end \ No newline at end of file +end diff --git a/test/constraints/bigm.jl b/test/constraints/bigm.jl index 7f22ef2..ac1281c 100644 --- a/test/constraints/bigm.jl +++ b/test/constraints/bigm.jl @@ -22,8 +22,7 @@ function test_get_M_1sided() @variable(model, y, Logical) @constraint(model, con, 3*x <= 1, Disjunct(y)) cobj = constraint_object(con) - M = DP._get_M(cobj.func, cobj.set, BigM(100, false)) - @test M == 100 + @test DP._get_M(cobj.func, cobj.set, BigM(100, false)) == 100 @test_throws ErrorException DP._get_M(cobj.func, cobj.set, BigM(Inf, false)) end @@ -35,19 +34,20 @@ function test_get_tight_M_1sided() cobj = constraint_object(con) method = BigM(100) - DP._query_variable_bounds(model, method) - M = DP._get_tight_M(cobj.func, cobj.set, method) - @test M == 100 + @test prep_bounds(x, model, method) isa Nothing + @test DP._get_tight_M(cobj.func, cobj.set, method) == 100 + clear_bounds(model) method = BigM(Inf) - DP._query_variable_bounds(model, method) + prep_bounds(x, model, method) @test_throws ErrorException DP._get_tight_M(cobj.func, cobj.set, method) + clear_bounds(model) set_upper_bound(x, 10) method = BigM() - DP._query_variable_bounds(model, method) - M = DP._get_tight_M(cobj.func, cobj.set, method) - @test M == 29 + prep_bounds(x, model, method) + @test DP._get_tight_M(cobj.func, cobj.set, method) == 29 + clear_bounds(model) end function test_get_M_2sided() @@ -58,9 +58,7 @@ function test_get_M_2sided() cobj = constraint_object(con) method = BigM(100) - M = DP._get_M(cobj.func, cobj.set, method) - @test M[1] == 100 - @test M[2] == 100 + @test DP._get_M(cobj.func, cobj.set, method) == [100., 100.] method = BigM(Inf) @test_throws ErrorException DP._get_M(cobj.func, cobj.set, method) @@ -74,101 +72,90 @@ function test_get_tight_M_2sided() cobj = constraint_object(con) method = BigM(100) - DP._query_variable_bounds(model, method) - M = DP._get_tight_M(cobj.func, cobj.set, method) - @test M[1] == 100 - @test M[2] == 100 + prep_bounds(x, model, method) + @test DP._get_tight_M(cobj.func, cobj.set, method) == (100., 100.) + clear_bounds(model) method = BigM(Inf) - DP._query_variable_bounds(model, method) + prep_bounds(x, model, method) @test_throws ErrorException DP._get_tight_M(cobj.func, cobj.set, method) + clear_bounds(model) set_lower_bound(x, -10) set_upper_bound(x, 10) method = BigM() - DP._query_variable_bounds(model, method) - M = DP._get_tight_M(cobj.func, cobj.set, method) - @test M[1] == 31 - @test M[2] == 29 + prep_bounds(x, model, method) + @test DP._get_tight_M(cobj.func, cobj.set, method) == (31., 29.) + clear_bounds(model) end function test_interval_arithmetic_LessThan() - model = Model() + model = GDPModel() @variable(model, x[1:3]) @variable(model, y, Bin) @expression(model, func, -x[1] + 2*x[2] - 3*x[3] + 4*y + 5) method = BigM() - DP._query_variable_bounds(model, method) - M = DP._interval_arithmetic_LessThan(func, 0.0, method) - @test isinf(M) + prep_bounds(x, model, method) + @test isinf(DP._interval_arithmetic_LessThan(func, 0.0, method)) + clear_bounds(model) set_upper_bound.(x, 10) method = BigM() - DP._query_variable_bounds(model, method) - M = DP._interval_arithmetic_LessThan(func, 0.0, method) - @test isinf(M) + prep_bounds(x, model, method) + @test isinf(DP._interval_arithmetic_LessThan(func, 0.0, method)) + clear_bounds(model) set_lower_bound.(x, -10) method = BigM() - DP._query_variable_bounds(model, method) - M = DP._interval_arithmetic_LessThan(func, 0.0, method) - @test M == -(-10) + 2*10 - 3*(-10) + 5 + prep_bounds(x, model, method) + M = -(-10) + 2*10 - 3*(-10) + 5 + @test DP._interval_arithmetic_LessThan(func, 0.0, method) == M + clear_bounds(model) end function test_interval_arithmetic_GreaterThan() - model = Model() + model = GDPModel() @variable(model, x[1:3]) @variable(model, y, Bin) @expression(model, func, -x[1] + 2*x[2] - 3*x[3] + 4*y + 5) method = BigM() - DP._query_variable_bounds(model, method) - M = DP._interval_arithmetic_GreaterThan(func, 0.0, method) - @test isinf(M) + prep_bounds(x, model, method) + @test isinf(DP._interval_arithmetic_GreaterThan(func, 0.0, method)) + clear_bounds(model) set_upper_bound.(x, 10) method = BigM() - DP._query_variable_bounds(model, method) - M = DP._interval_arithmetic_GreaterThan(func, 0.0, method) - @test isinf(M) + prep_bounds(x, model, method) + @test isinf(DP._interval_arithmetic_GreaterThan(func, 0.0, method)) + clear_bounds(model) set_lower_bound.(x, -10) method = BigM() - DP._query_variable_bounds(model, method) - M = DP._interval_arithmetic_GreaterThan(func, 0.0, method) - @test -M == -(10) + 2*(-10) - 3*(10) + 5 + prep_bounds(x, model, method) + expected = -(10) + 2*(-10) - 3*(10) + 5 + @test DP._interval_arithmetic_GreaterThan(func, 0.0, method) == -expected + clear_bounds(model) end function test_calculate_tight_M() - model = Model() + model = GDPModel() @variable(model, -1 <= x <= 1) method = BigM() - DP._query_variable_bounds(model, method) - M = DP._calculate_tight_M(1*x, MOI.LessThan(5.0), method) - @test M == -4 - M = DP._calculate_tight_M(1*x, MOI.GreaterThan(5.0), method) - @test -M == -6 - M = DP._calculate_tight_M(1*x, MOI.Nonpositives(3), method) - @test M == 1 - M = DP._calculate_tight_M(1*x, MOI.Nonnegatives(3), method) - @test -M == -1 - M = DP._calculate_tight_M(1*x, MOI.Interval(5.0, 5.0), method) - @test -M[1] == -6 - @test M[2] == -4 - M = DP._calculate_tight_M(1*x, MOI.EqualTo(5.0), method) - @test -M[1] == -6 - @test M[2] == -4 - M = DP._calculate_tight_M(1*x, MOI.Zeros(3), method) - @test -M[1] == -1 - @test M[2] == 1 + prep_bounds(x, model, method) + @test DP._calculate_tight_M(1*x, MOI.LessThan(5.0), method) == -4 + @test DP._calculate_tight_M(1*x, MOI.GreaterThan(5.0), method) == 6 + @test DP._calculate_tight_M(1*x, MOI.Nonpositives(3), method) == 1 + @test DP._calculate_tight_M(1*x, MOI.Nonnegatives(3), method) == 1 + @test DP._calculate_tight_M(1*x, MOI.Interval(5.0, 5.0), method) == (6., -4.) + @test DP._calculate_tight_M(1*x, MOI.EqualTo(5.0), method) == (6., -4.) + @test DP._calculate_tight_M(1*x, MOI.Zeros(3), method) == (1., 1.) for ex in (x^2, exp(x)), set in (MOI.LessThan(5.0), MOI.GreaterThan(5.0), MOI.Nonpositives(4), MOI.Nonnegatives(4)) - M = DP._calculate_tight_M(ex, set, method) - @test isinf(M) + @test isinf(DP._calculate_tight_M(ex, set, method)) end for ex in (x^2, exp(x)), set in (MOI.Interval(5.0,5.0), MOI.EqualTo(5.0), MOI.Zeros(4)) - M = DP._calculate_tight_M(ex, set, method) - @test all(isinf.(M)) + @test all(isinf.(DP._calculate_tight_M(ex, set, method))) end @test_throws ErrorException DP._calculate_tight_M(1*x, MOI.SOS1([1.0,3.0,2.5]), method) end @@ -179,8 +166,7 @@ function test_lessthan_bigm() @variable(model, y, Logical) @constraint(model, con, x <= 5, Disjunct(y)) - DP._reformulate_logical_variables(model) - bvref = DP._indicator_to_binary(model)[y] + bvref = binary_variable(y) ref = reformulate_disjunct_constraint(model, constraint_object(con), bvref, BigM(100, false)) @test length(ref) == 1 @test ref[1].func == x - 100*(-bvref) @@ -193,8 +179,7 @@ function test_nonpositives_bigm() @variable(model, y, Logical) @constraint(model, con, [x; x] <= [5; 5], Disjunct(y)) - DP._reformulate_logical_variables(model) - bvref = DP._indicator_to_binary(model)[y] + bvref = binary_variable(y) ref = reformulate_disjunct_constraint(model, constraint_object(con), bvref, BigM(100, false)) @test length(ref) == 1 @test ref[1].func[1] == x - 5 - 100*(1-bvref) @@ -208,8 +193,7 @@ function test_greaterthan_bigm() @variable(model, y, Logical) @constraint(model, con, x >= 5, Disjunct(y)) - DP._reformulate_logical_variables(model) - bvref = DP._indicator_to_binary(model)[y] + bvref = binary_variable(y) ref = reformulate_disjunct_constraint(model, constraint_object(con), bvref, BigM(100, false)) @test length(ref) == 1 @test ref[1].func == x + 100*(-bvref) @@ -222,8 +206,7 @@ function test_nonnegatives_bigm() @variable(model, y, Logical) @constraint(model, con, [x; x] >= [5; 5], Disjunct(y)) - DP._reformulate_logical_variables(model) - bvref = DP._indicator_to_binary(model)[y] + bvref = binary_variable(y) ref = reformulate_disjunct_constraint(model, constraint_object(con), bvref, BigM(100, false)) @test length(ref) == 1 @test ref[1].func[1] == x - 5 + 100*(1-bvref) @@ -237,8 +220,7 @@ function test_equalto_bigm() @variable(model, y, Logical) @constraint(model, con, x == 5, Disjunct(y)) - DP._reformulate_logical_variables(model) - bvref = DP._indicator_to_binary(model)[y] + bvref = binary_variable(y) ref = reformulate_disjunct_constraint(model, constraint_object(con), bvref, BigM(100, false)) @test length(ref) == 2 @test ref[1].func == x + 100*(-bvref) @@ -253,8 +235,7 @@ function test_interval_bigm() @variable(model, y, Logical) @constraint(model, con, 5 <= x <= 5, Disjunct(y)) - DP._reformulate_logical_variables(model) - bvref = DP._indicator_to_binary(model)[y] + bvref = binary_variable(y) ref = reformulate_disjunct_constraint(model, constraint_object(con), bvref, BigM(100, false)) @test length(ref) == 2 @test ref[1].func == x + 100*(-bvref) @@ -269,8 +250,7 @@ function test_zeros_bigm() @variable(model, y, Logical) @constraint(model, con, [x; x] == [5; 5], Disjunct(y)) - DP._reformulate_logical_variables(model) - bvref = DP._indicator_to_binary(model)[y] + bvref = binary_variable(y) ref = reformulate_disjunct_constraint(model, constraint_object(con), bvref, BigM(100, false)) @test length(ref) == 2 @test ref[1].func[1] == x - 5 + 100*(1-bvref) @@ -307,6 +287,32 @@ function test_nested_bigm() @test refcons[4].set == MOI.GreaterThan(10.0 - 110) end +function test_extension_bigm() + model = GDPModel{MyModel, MyVarRef, MyConRef}() + @variable(model, -100 <= x <= 100) + @variable(model, y[1:2], Logical(MyVar)) + @variable(model, z[1:2], Logical(MyVar)) + @constraint(model, x <= 5, Disjunct(y[1])) + @constraint(model, x >= 5, Disjunct(y[2])) + @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, exactly1 = false) + + @test reformulate_model(model, BigM()) isa Nothing + bvrefs = DP._indicator_to_binary(model) + refcons = constraint_object.(DP._reformulation_constraints(model)) + @test length(refcons) == 4 + @test refcons[1].func == x - 95*(-bvrefs[y[1]]) + @test refcons[1].set == MOI.LessThan(5.0 + 95) + @test refcons[2].func == x + 105*(-bvrefs[y[2]]) + @test refcons[2].set == MOI.GreaterThan(5.0 - 105) + @test refcons[3].func == x - 90*(-bvrefs[z[1]]) + @test refcons[3].set == MOI.LessThan(10.0 + 90) + @test refcons[4].func == x + 110*(-bvrefs[z[2]]) + @test refcons[4].set == MOI.GreaterThan(10.0 - 110) +end + @testset "BigM Reformulation" begin test_default_bigm() test_default_tighten_bigm() @@ -326,4 +332,5 @@ end test_interval_bigm() test_zeros_bigm() test_nested_bigm() + test_extension_bigm() end \ No newline at end of file diff --git a/test/constraints/disjunct.jl b/test/constraints/disjunct.jl index b717c42..c6a787c 100644 --- a/test/constraints/disjunct.jl +++ b/test/constraints/disjunct.jl @@ -38,7 +38,7 @@ function test_disjunct_add_array() @variable(model, x) @variable(model, y[1:2, 1:3], Logical) @constraint(model, con[i=1:2, j=1:3], x == 1, Disjunct(y[i,j])) - @test con isa Matrix{DisjunctConstraintRef} + @test con isa Matrix{DisjunctConstraintRef{Model}} @test length(con) == 6 end @@ -53,7 +53,7 @@ function test_disjunct_add_dense_axis() @test con isa Containers.DenseAxisArray @test con.axes[1] == ["a","b","c"] @test con.axes[2] == [1,2] - @test con.data isa Matrix{DisjunctConstraintRef} + @test con.data isa Matrix{DisjunctConstraintRef{Model}} end function test_disjunct_add_sparse_axis() diff --git a/test/constraints/disjunction.jl b/test/constraints/disjunction.jl index b181c36..9ebb71f 100644 --- a/test/constraints/disjunction.jl +++ b/test/constraints/disjunction.jl @@ -114,7 +114,7 @@ function test_disjunciton_add_dense_axis() @test disj.axes[1] == ["a","b","c"] @test disj.axes[2] == [1,2] - @test disj.data isa Matrix{DisjunctionRef} + @test disj.data isa Matrix{DisjunctionRef{Model}} end function test_disjunction_add_sparse_axis() @@ -248,6 +248,23 @@ function test_disjunction_function_nested() @test !haskey(gdp_data(model).exactly1_constraints, disj1) end +function test_extension_disjunctions() + model = GDPModel{MyModel, MyVarRef, MyConRef}() + @variable(model, y[1:2], Logical(MyVar), start = true) + @variable(model, 0 <= x[1:2] <= 1) + crefs = [DisjunctConstraintRef(model, DisjunctConstraintIndex(i)) for i in 1:2] + @test @constraint(model, [i = 1:2], x[i]^2 >= 0.5, Disjunct(y[i])) == crefs + dref = DisjunctionRef(model, DisjunctionIndex(1)) + @test @disjunction(model, y, base_name = "test") == dref + @test name(dref) == "test" + dref2 = DisjunctionRef(model, DisjunctionIndex(2)) + @test disjunction(model, y) == dref2 + @test length(DP._disjunctions(model)) == 2 + @test length(DP._disjunct_constraints(model)) == 2 + @test delete(model, dref2) isa Nothing + @test !is_valid(model, dref2) +end + @testset "Disjunction" begin @testset "Macro Helpers" begin test_macro_helpers() @@ -270,4 +287,7 @@ end @testset "Delete Disjunction" begin test_disjunction_delete() end + @testset "Test Extension" begin + test_extension_disjunctions() + end end \ No newline at end of file diff --git a/test/constraints/hull.jl b/test/constraints/hull.jl index 3da6488..37b1175 100644 --- a/test/constraints/hull.jl +++ b/test/constraints/hull.jl @@ -1,37 +1,30 @@ function test_default_hull() - method = Hull() - @test method.value == 1e-6 + @test Hull().value == 1e-6 end function test_set_hull() - method = Hull(0.001) - @test method.value == 0.001 + @test Hull(0.001).value == 0.001 end function test_query_variable_bounds() model = GDPModel() @variable(model, 10 <= x <= 100) @variable(model, -100 <= y <= -10) - method = Hull() - DP._query_variable_bounds(model, method) - @test haskey(method.variable_bounds, x) - @test haskey(method.variable_bounds, y) - @test method.variable_bounds[x] == (0, 100) - @test method.variable_bounds[y] == (-100, 0) + prep_bounds([x, y], model, Hull()) + @test variable_bound_info(x) == (0, 100) + @test variable_bound_info(y) == (-100, 0) end function test_query_variable_bounds_error1() model = GDPModel() @variable(model, x <= 100) - method = Hull() - @test_throws ErrorException DP._query_variable_bounds(model, method) + @test_throws ErrorException set_variable_bound_info(x, Hull()) end function test_query_variable_bounds_error2() model = GDPModel() @variable(model, -100 <= x) - method = Hull() - @test_throws ErrorException DP._query_variable_bounds(model, method) + @test_throws ErrorException set_variable_bound_info(x, Hull()) end function test_disaggregate_variables() @@ -39,16 +32,16 @@ function test_disaggregate_variables() @variable(model, 10 <= x <= 100) @variable(model, y, Bin) @variable(model, z, Logical) - vrefs = Set{VariableRef}() #initialize empty set to check if method.disjunct_variables has variables added to it in _disaggregate_variable call - DP._reformulate_logical_variables(model) - method = DP._Hull(Hull(1e-3, Dict(x => (0., 100.))), vrefs) vrefs = Set([x,y]) - DP._disaggregate_variables(model, z, vrefs, method) + @test prep_bounds(x, model, Hull()) isa Nothing + method = DP._Hull(Hull(1e-3), vrefs) + @test DP._disaggregate_variables(model, z, vrefs, method) isa Nothing + @test haskey(method.disjunct_variables, (x, DP._indicator_to_binary(model)[z])) refvars = DP._reformulation_variables(model) - @test length(refvars) == 2 + @test length(refvars) == 1 zbin = variable_by_name(model, "z") - @test zbin in refvars + @test zbin == binary_variable(z) x_z = variable_by_name(model, "x_z") @test x_z in refvars @test has_lower_bound(x_z) && lower_bound(x_z) == 0 @@ -67,9 +60,9 @@ function test_aggregate_variable() @variable(model, 10 <= x <= 100) @variable(model, z, Logical) vrefs = Set([x]) - DP._reformulate_logical_variables(model) - method = DP._Hull(Hull(1e-3, Dict(x => (0., 100.))), vrefs) - DP._disaggregate_variables(model, z, vrefs, method) + @test prep_bounds(x, model, Hull()) isa Nothing + method = DP._Hull(Hull(1e-3), vrefs) + @test DP._disaggregate_variables(model, z, vrefs, method) isa Nothing refcons = Vector{JuMP.AbstractConstraint}() DP._aggregate_variable(model, refcons, x, method) @test length(refcons) == 1 @@ -81,12 +74,12 @@ function test_disaggregate_expression_var() model = GDPModel() @variable(model, 10 <= x <= 100) @variable(model, z, Logical) - DP._reformulate_logical_variables(model) bvrefs = DP._indicator_to_binary(model) vrefs = Set([x]) - method = DP._Hull(Hull(1e-3, Dict(x => (0., 100.))), vrefs) - DP._disaggregate_variables(model, z, vrefs, method) + @test prep_bounds(x, model, Hull()) isa Nothing + method = DP._Hull(Hull(1e-3), vrefs) + @test DP._disaggregate_variables(model, z, vrefs, method) isa Nothing refexpr = DP._disaggregate_expression(model, x, bvrefs[z], method) x_z = variable_by_name(model, "x_z") @@ -97,12 +90,11 @@ function test_disaggregate_expression_var_binary() model = GDPModel() @variable(model, x, Bin) @variable(model, z, Logical) - DP._reformulate_logical_variables(model) bvrefs = DP._indicator_to_binary(model) vrefs = Set([x]) - method = DP._Hull(Hull(1e-3, Dict(x => (0., 1.))), vrefs) - DP._disaggregate_variables(model, z, vrefs, method) + method = DP._Hull(Hull(1e-3), vrefs) + @test DP._disaggregate_variables(model, z, vrefs, method) isa Nothing @test isnothing(variable_by_name(model, "x_z")) refexpr = DP._disaggregate_expression(model, x, bvrefs[z], method) @@ -113,12 +105,12 @@ function test_disaggregate_expression_affine() model = GDPModel() @variable(model, 10 <= x <= 100) @variable(model, z, Logical) - DP._reformulate_logical_variables(model) bvrefs = DP._indicator_to_binary(model) vrefs = Set([x]) - method = DP._Hull(Hull(1e-3, Dict(x => (0., 100.))), vrefs) - DP._disaggregate_variables(model, z, vrefs, method) + @test prep_bounds(x, model, Hull()) isa Nothing + method = DP._Hull(Hull(1e-3), vrefs) + @test DP._disaggregate_variables(model, z, vrefs, method) isa Nothing refexpr = DP._disaggregate_expression(model, 2x + 1, bvrefs[z], method) x_z = variable_by_name(model, "x_z") @@ -131,12 +123,12 @@ function test_disaggregate_expression_affine_mip() @variable(model, 10 <= x <= 100) @variable(model, y, Bin) @variable(model, z, Logical) - DP._reformulate_logical_variables(model) bvrefs = DP._indicator_to_binary(model) vrefs = Set([x, y]) - method = DP._Hull(Hull(1e-3, Dict(x => (0., 100.), y => (0., 1.))), vrefs) - DP._disaggregate_variables(model, z, vrefs, method) + @test prep_bounds(x, model, Hull()) isa Nothing + method = DP._Hull(Hull(1e-3), vrefs) + @test DP._disaggregate_variables(model, z, vrefs, method) isa Nothing refexpr = DP._disaggregate_expression(model, 2x + y + 1, bvrefs[z], method) x_z = variable_by_name(model, "x_z") @@ -148,12 +140,12 @@ function test_disaggregate_expression_quadratic() model = GDPModel() @variable(model, 10 <= x <= 100) @variable(model, z, Logical) - DP._reformulate_logical_variables(model) bvrefs = DP._indicator_to_binary(model) vrefs = Set([x]) - method = DP._Hull(Hull(1e-3, Dict(x => (0., 100.))), vrefs) - DP._disaggregate_variables(model, z, vrefs, method) + @test prep_bounds(x, model, Hull()) isa Nothing + method = DP._Hull(Hull(1e-3), vrefs) + @test DP._disaggregate_variables(model, z, vrefs, method) isa Nothing refexpr = DP._disaggregate_expression(model, 2x^2 + 1, bvrefs[z], method) x_z = variable_by_name(model, "x_z") @@ -171,12 +163,12 @@ function test_disaggregate_nl_expression_c() model = GDPModel() @variable(model, 10 <= x <= 100) @variable(model, z, Logical) - DP._reformulate_logical_variables(model) bvrefs = DP._indicator_to_binary(model) vrefs = Set([x]) - method = DP._Hull(Hull(1e-3, Dict(x => (0., 100.))), vrefs) - DP._disaggregate_variables(model, z, vrefs, method) + @test prep_bounds(x, model, Hull()) isa Nothing + method = DP._Hull(Hull(1e-3), vrefs) + @test DP._disaggregate_variables(model, z, vrefs, method) isa Nothing refexpr = DP._disaggregate_nl_expression(model, 1, bvrefs[z], method) @test refexpr == 1 @@ -186,12 +178,11 @@ function test_disaggregate_nl_expression_var_binary() model = GDPModel() @variable(model, x, Bin) @variable(model, z, Logical) - DP._reformulate_logical_variables(model) bvrefs = DP._indicator_to_binary(model) vrefs = Set([x]) - method = DP._Hull(Hull(1e-3, Dict(x => (0., 1.))), vrefs) - DP._disaggregate_variables(model, z, vrefs, method) + method = DP._Hull(Hull(1e-3), vrefs) + @test DP._disaggregate_variables(model, z, vrefs, method) isa Nothing refexpr = DP._disaggregate_nl_expression(model, x, bvrefs[z], method) ϵ = method.value @@ -204,12 +195,12 @@ function test_disaggregate_nl_expression_var() model = GDPModel() @variable(model, 10 <= x <= 100) @variable(model, z, Logical) - DP._reformulate_logical_variables(model) bvrefs = DP._indicator_to_binary(model) vrefs = Set([x]) - method = DP._Hull(Hull(1e-3, Dict(x => (0., 100.))), vrefs) - DP._disaggregate_variables(model, z, vrefs, method) + method = DP._Hull(Hull(1e-3), vrefs) + @test prep_bounds(x, model, Hull()) isa Nothing + @test DP._disaggregate_variables(model, z, vrefs, method) isa Nothing refexpr = DP._disaggregate_nl_expression(model, x, bvrefs[z], method) x_z = variable_by_name(model, "x_z") @@ -224,12 +215,12 @@ function test_disaggregate_nl_expression_aff() model = GDPModel() @variable(model, 10 <= x <= 100) @variable(model, z, Logical) - DP._reformulate_logical_variables(model) bvrefs = DP._indicator_to_binary(model) vrefs = Set([x]) - method = DP._Hull(Hull(1e-3, Dict(x => (0., 100.))), vrefs) - DP._disaggregate_variables(model, z, vrefs, method) + method = DP._Hull(Hull(1e-3), vrefs) + @test prep_bounds(x, model, Hull()) isa Nothing + @test DP._disaggregate_variables(model, z, vrefs, method) isa Nothing refexpr = DP._disaggregate_nl_expression(model, 2x + 1, bvrefs[z], method) x_z = variable_by_name(model, "x_z") @@ -248,12 +239,12 @@ function test_disaggregate_nl_expression_aff_mip() @variable(model, 10 <= x <= 100) @variable(model, y, Bin) @variable(model, z, Logical) - DP._reformulate_logical_variables(model) bvrefs = DP._indicator_to_binary(model) vrefs = Set([x,y]) - method = DP._Hull(Hull(1e-3, Dict(x => (0., 100.), y => (0., 1.))), vrefs) - DP._disaggregate_variables(model, z, vrefs, method) + method = DP._Hull(Hull(1e-3), vrefs) + @test prep_bounds(x, model, Hull()) isa Nothing + @test DP._disaggregate_variables(model, z, vrefs, method) isa Nothing refexpr = DP._disaggregate_nl_expression(model, 2x + y + 1, bvrefs[z], method) flatten!(refexpr) @@ -274,12 +265,12 @@ function test_disaggregate_nl_expression_quad() model = GDPModel() @variable(model, 10 <= x <= 100) @variable(model, z, Logical) - DP._reformulate_logical_variables(model) bvrefs = DP._indicator_to_binary(model) vrefs = Set([x]) - method = DP._Hull(Hull(1e-3, Dict(x => (0., 100.))), vrefs) - DP._disaggregate_variables(model, z, vrefs, method) + method = DP._Hull(Hull(1e-3), vrefs) + @test prep_bounds(x, model, Hull()) isa Nothing + @test DP._disaggregate_variables(model, z, vrefs, method) isa Nothing refexpr = DP._disaggregate_nl_expression(model, 2x^2 + 1, bvrefs[z], method) x_z = variable_by_name(model, "x_z") @@ -297,12 +288,12 @@ function test_disaggregate_nl_expession() model = GDPModel() @variable(model, 10 <= x <= 100) @variable(model, z, Logical) - DP._reformulate_logical_variables(model) bvrefs = DP._indicator_to_binary(model) vrefs = Set([x]) - method = DP._Hull(Hull(1e-3, Dict(x => (0., 100.))), vrefs) - DP._disaggregate_variables(model, z, vrefs, method) + method = DP._Hull(Hull(1e-3), vrefs) + @test prep_bounds(x, model, Hull()) isa Nothing + @test DP._disaggregate_variables(model, z, vrefs, method) isa Nothing refexpr = DP._disaggregate_nl_expression(model, 2x^3 + 1, bvrefs[z], method) x_z = variable_by_name(model, "x_z") @@ -326,10 +317,10 @@ function test_scalar_var_hull_1sided(moiset) @variable(model, 10 <= x <= 100) @variable(model, z, Logical) @constraint(model, con, x in moiset(5), Disjunct(z)) - DP._reformulate_logical_variables(model) zbin = variable_by_name(model, "z") - method = DP._Hull(Hull(1e-3, Dict(x => (0., 100.))), Set([x])) - DP._disaggregate_variables(model, z, Set([x]), method) + @test prep_bounds(x, model, Hull()) isa Nothing + method = DP._Hull(Hull(1e-3), Set([x])) + @test DP._disaggregate_variables(model, z, Set([x]), method) isa Nothing x_z = variable_by_name(model, "x_z") ref = reformulate_disjunct_constraint(model, constraint_object(con), zbin, method) @test length(ref) == 1 @@ -343,10 +334,10 @@ function test_scalar_affine_hull_1sided(moiset) @variable(model, 10 <= x <= 100) @variable(model, z, Logical) @constraint(model, con, 1x in moiset(5), Disjunct(z)) - DP._reformulate_logical_variables(model) zbin = variable_by_name(model, "z") - method = DP._Hull(Hull(1e-3, Dict(x => (0., 100.))), Set([x])) - DP._disaggregate_variables(model, z, Set([x]), method) + method = DP._Hull(Hull(1e-3), Set([x])) + @test prep_bounds(x, model, Hull()) isa Nothing + @test DP._disaggregate_variables(model, z, Set([x]), method) isa Nothing x_z = variable_by_name(model, "x_z") ref = reformulate_disjunct_constraint(model, constraint_object(con), zbin, method) @test length(ref) == 1 @@ -360,10 +351,10 @@ function test_vector_var_hull_1sided(moiset) @variable(model, 10 <= x <= 100) @variable(model, z, Logical) @constraint(model, con, [x; x] in moiset(2), Disjunct(z)) - DP._reformulate_logical_variables(model) zbin = variable_by_name(model, "z") - method = DP._Hull(Hull(1e-3, Dict(x => (0., 100.))), Set([x])) - DP._disaggregate_variables(model, z, Set([x]), method) + method = DP._Hull(Hull(1e-3), Set([x])) + @test prep_bounds(x, model, Hull()) isa Nothing + @test DP._disaggregate_variables(model, z, Set([x]), method) isa Nothing x_z = variable_by_name(model, "x_z") ref = reformulate_disjunct_constraint(model, constraint_object(con), zbin, method) @test length(ref) == 1 @@ -376,10 +367,10 @@ function test_vector_affine_hull_1sided(moiset) @variable(model, 10 <= x <= 100) @variable(model, z, Logical) @constraint(model, con, [x - 5; x - 5] in moiset(2), Disjunct(z)) - DP._reformulate_logical_variables(model) zbin = variable_by_name(model, "z") - method = DP._Hull(Hull(1e-3, Dict(x => (0., 100.))), Set([x])) - DP._disaggregate_variables(model, z, Set([x]), method) + method = DP._Hull(Hull(1e-3), Set([x])) + @test prep_bounds(x, model, Hull()) isa Nothing + @test DP._disaggregate_variables(model, z, Set([x]), method) isa Nothing x_z = variable_by_name(model, "x_z") ref = reformulate_disjunct_constraint(model, constraint_object(con), zbin, method) @test length(ref) == 1 @@ -392,11 +383,11 @@ function test_scalar_quadratic_hull_1sided(moiset) @variable(model, 10 <= x <= 100) @variable(model, z, Logical) @constraint(model, con, x^2 in moiset(5), Disjunct(z)) - DP._reformulate_logical_variables(model) zbin = variable_by_name(model, "z") ϵ = 1e-3 - method = DP._Hull(Hull(ϵ, Dict(x => (0., 100.))), Set([x])) - DP._disaggregate_variables(model, z, Set([x]), method) + method = DP._Hull(Hull(ϵ), Set([x])) + @test prep_bounds(x, model, Hull()) isa Nothing + @test DP._disaggregate_variables(model, z, Set([x]), method) isa Nothing x_z = variable_by_name(model, "x_z") ref = reformulate_disjunct_constraint(model, constraint_object(con), zbin, method) @test length(ref) == 1 @@ -417,11 +408,11 @@ function test_vector_quadratic_hull_1sided(moiset) @variable(model, 10 <= x <= 100) @variable(model, z, Logical) @constraint(model, con, [x^2 - 5; x^2 - 5] in moiset(2), Disjunct(z)) - DP._reformulate_logical_variables(model) zbin = variable_by_name(model, "z") ϵ = 1e-3 - method = DP._Hull(Hull(ϵ, Dict(x => (0., 100.))), Set([x])) - DP._disaggregate_variables(model, z, Set([x]), method) + method = DP._Hull(Hull(ϵ), Set([x])) + @test prep_bounds(x, model, Hull()) isa Nothing + @test DP._disaggregate_variables(model, z, Set([x]), method) isa Nothing x_z = variable_by_name(model, "x_z") ref = reformulate_disjunct_constraint(model, constraint_object(con), zbin, method) @test length(ref) == 1 @@ -442,11 +433,11 @@ function test_scalar_nonlinear_hull_1sided_error() @variable(model, 10 <= x <= 100) @variable(model, z, Logical) @constraint(model, con, log(x) <= 10, Disjunct(z)) - DP._reformulate_logical_variables(model) zbin = variable_by_name(model, "z") ϵ = 1e-3 - method = DP._Hull(Hull(ϵ, Dict(x => (0., 100.))), Set([x])) - DP._disaggregate_variables(model, z, Set([x]), method) + method = DP._Hull(Hull(ϵ), Set([x])) + @test prep_bounds(x, model, Hull()) isa Nothing + @test DP._disaggregate_variables(model, z, Set([x]), method) isa Nothing @test_throws ErrorException reformulate_disjunct_constraint(model, constraint_object(con), zbin, method) end function test_scalar_nonlinear_hull_1sided(moiset) @@ -454,11 +445,11 @@ function test_scalar_nonlinear_hull_1sided(moiset) @variable(model, 10 <= x <= 100) @variable(model, z, Logical) @constraint(model, con, x^3 in moiset(5), Disjunct(z)) - DP._reformulate_logical_variables(model) zbin = variable_by_name(model, "z") ϵ = 1e-3 - method = DP._Hull(Hull(ϵ, Dict(x => (0., 100.))), Set([x])) - DP._disaggregate_variables(model, z, Set([x]), method) + method = DP._Hull(Hull(ϵ), Set([x])) + @test prep_bounds(x, model, Hull()) isa Nothing + @test DP._disaggregate_variables(model, z, Set([x]), method) isa Nothing x_z = variable_by_name(model, "x_z") ref = reformulate_disjunct_constraint(model, constraint_object(con), zbin, method) @test length(ref) == 1 @@ -484,11 +475,11 @@ function test_vector_nonlinear_hull_1sided_error() @variable(model, 10 <= x <= 100) @variable(model, z, Logical) @constraint(model, con, [log(x),log(x)] <= [10,10], Disjunct(z)) - DP._reformulate_logical_variables(model) zbin = variable_by_name(model, "z") ϵ = 1e-3 - method = DP._Hull(Hull(ϵ, Dict(x => (0., 100.))), Set([x])) - DP._disaggregate_variables(model, z, Set([x]), method) + method = DP._Hull(Hull(ϵ), Set([x])) + @test prep_bounds(x, model, Hull()) isa Nothing + @test DP._disaggregate_variables(model, z, Set([x]), method) isa Nothing @test_throws ErrorException reformulate_disjunct_constraint(model, constraint_object(con), zbin, method) end function test_vector_nonlinear_hull_1sided(moiset) @@ -496,11 +487,11 @@ function test_vector_nonlinear_hull_1sided(moiset) @variable(model, 10 <= x <= 100) @variable(model, z, Logical) @constraint(model, con, [x^3 - 5; x^3 - 5] in moiset(2), Disjunct(z)) - DP._reformulate_logical_variables(model) zbin = variable_by_name(model, "z") ϵ = 1e-3 - method = DP._Hull(Hull(ϵ, Dict(x => (0., 100.))), Set([x])) - DP._disaggregate_variables(model, z, Set([x]), method) + method = DP._Hull(Hull(ϵ), Set([x])) + @test prep_bounds(x, model, Hull()) isa Nothing + @test DP._disaggregate_variables(model, z, Set([x]), method) isa Nothing x_z = variable_by_name(model, "x_z") ref = reformulate_disjunct_constraint(model, constraint_object(con), zbin, method) @test length(ref) == 1 @@ -529,11 +520,11 @@ function test_scalar_var_hull_2sided() @variable(model, 10 <= x <= 100) @variable(model, z, Logical) @constraint(model, con, x in MOI.Interval(5,5), Disjunct(z)) - DP._reformulate_logical_variables(model) zbin = variable_by_name(model, "z") ϵ = 1e-3 - method = DP._Hull(Hull(ϵ, Dict(x => (0., 100.))), Set([x])) - DP._disaggregate_variables(model, z, Set([x]), method) + method = DP._Hull(Hull(ϵ), Set([x])) + @test prep_bounds(x, model, Hull()) isa Nothing + @test DP._disaggregate_variables(model, z, Set([x]), method) isa Nothing x_z = variable_by_name(model, "x_z") ref = reformulate_disjunct_constraint(model, constraint_object(con), zbin, method) @test length(ref) == 2 @@ -549,11 +540,11 @@ function test_scalar_affine_hull_2sided() @variable(model, 10 <= x <= 100) @variable(model, z, Logical) @constraint(model, con, 5 <= x <= 5, Disjunct(z)) - DP._reformulate_logical_variables(model) zbin = variable_by_name(model, "z") ϵ = 1e-3 - method = DP._Hull(Hull(ϵ, Dict(x => (0., 100.))), Set([x])) - DP._disaggregate_variables(model, z, Set([x]), method) + method = DP._Hull(Hull(ϵ), Set([x])) + @test prep_bounds(x, model, Hull()) isa Nothing + @test DP._disaggregate_variables(model, z, Set([x]), method) isa Nothing x_z = variable_by_name(model, "x_z") ref = reformulate_disjunct_constraint(model, constraint_object(con), zbin, method) @test length(ref) == 2 @@ -569,11 +560,11 @@ function test_scalar_quadratic_hull_2sided() @variable(model, 10 <= x <= 100) @variable(model, z, Logical) @constraint(model, con, 5 <= x^2 <= 5, Disjunct(z)) - DP._reformulate_logical_variables(model) zbin = variable_by_name(model, "z") ϵ = 1e-3 - method = DP._Hull(Hull(ϵ, Dict(x => (0., 100.))), Set([x])) - DP._disaggregate_variables(model, z, Set([x]), method) + method = DP._Hull(Hull(ϵ), Set([x])) + @test prep_bounds(x, model, Hull()) isa Nothing + @test DP._disaggregate_variables(model, z, Set([x]), method) isa Nothing x_z = variable_by_name(model, "x_z") ref = reformulate_disjunct_constraint(model, constraint_object(con), zbin, method) @test length(ref) == 2 @@ -596,11 +587,11 @@ function test_scalar_nonlinear_hull_2sided_error() @variable(model, 10 <= x <= 100) @variable(model, z, Logical) @constraint(model, con, 0 <= log(x) <= 10, Disjunct(z)) - DP._reformulate_logical_variables(model) zbin = variable_by_name(model, "z") ϵ = 1e-3 - method = DP._Hull(Hull(ϵ, Dict(x => (0., 100.))), Set([x])) - DP._disaggregate_variables(model, z, Set([x]), method) + method = DP._Hull(Hull(ϵ), Set([x])) + @test prep_bounds(x, model, Hull()) isa Nothing + @test DP._disaggregate_variables(model, z, Set([x]), method) isa Nothing @test_throws ErrorException reformulate_disjunct_constraint(model, constraint_object(con), zbin, method) end function test_scalar_nonlinear_hull_2sided() @@ -608,11 +599,11 @@ function test_scalar_nonlinear_hull_2sided() @variable(model, 10 <= x <= 100) @variable(model, z, Logical) @constraint(model, con, 5 <= x^3 <= 5, Disjunct(z)) - DP._reformulate_logical_variables(model) zbin = variable_by_name(model, "z") ϵ = 1e-3 - method = DP._Hull(Hull(ϵ, Dict(x => (0., 100.))), Set([x])) - DP._disaggregate_variables(model, z, Set([x]), method) + method = DP._Hull(Hull(ϵ), Set([x])) + @test prep_bounds(x, model, Hull()) isa Nothing + @test DP._disaggregate_variables(model, z, Set([x]), method) isa Nothing x_z = variable_by_name(model, "x_z") ref = reformulate_disjunct_constraint(model, constraint_object(con), zbin, method) @test length(ref) == 2 @@ -647,6 +638,30 @@ function test_exactly1_error() @test_throws ErrorException reformulate_model(model, Hull()) end +function test_extension_hull() + # test error for hull + @test_throws ErrorException requires_disaggregation(BadVarRef()) + + # prepare extension model + model = GDPModel{MyModel, MyVarRef, MyConRef}() + @variable(model, -100 <= x <= 100) + @variable(model, y[1:2], Logical(MyVar)) + @variable(model, z[1:2], Logical(MyVar)) + @constraint(model, x <= 5, Disjunct(y[1])) + @constraint(model, x >= 5, Disjunct(y[2])) + @disjunction(model, inner, y, Disjunct(z[1])) + @constraint(model, x <= 10, Disjunct(z[1])) + @constraint(model, x >= 10, Disjunct(z[2])) + @disjunction(model, outer, z) + + # test reformulation + @test reformulate_model(model, Hull()) isa Nothing + refcons = constraint_object.(DP._reformulation_constraints(model)) + @test length(refcons) == 16 + @test length(DP._reformulation_variables(model)) == 4 + # TODO add more tests +end + @testset "Hull Reformulation" begin test_default_hull() test_set_hull() @@ -687,4 +702,5 @@ end test_scalar_nonlinear_hull_2sided() test_scalar_nonlinear_hull_2sided_error() test_exactly1_error() + test_extension_hull() end \ No newline at end of file diff --git a/test/constraints/indicator.jl b/test/constraints/indicator.jl index 0566988..e41d8e3 100644 --- a/test/constraints/indicator.jl +++ b/test/constraints/indicator.jl @@ -110,6 +110,27 @@ function test_indicator_nested() @test all([cobj.set isa MOI.Indicator for cobj in ref_cons_obj]) end +function test_extension_indicator() + model = GDPModel{MyModel, MyVarRef, MyConRef}() + @variable(model, x) + @variable(model, y[1:2], Logical(MyVar)) + @variable(model, z[1:2], Logical(MyVar)) + @constraint(model, x <= 5, Disjunct(y[1])) + @constraint(model, x >= 5, Disjunct(y[2])) + @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, exactly1 = false) + 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() @@ -117,4 +138,5 @@ end test_indicator_dense_axis() test_indicator_sparse_axis() test_indicator_nested() + test_extension_indicator() end \ No newline at end of file diff --git a/test/constraints/proposition.jl b/test/constraints/proposition.jl index ec928f5..1846321 100644 --- a/test/constraints/proposition.jl +++ b/test/constraints/proposition.jl @@ -98,7 +98,7 @@ function test_proposition_add_array() model = GDPModel() @variable(model, y[1:2, 1:3, 1:4], Logical) @constraint(model, con[i=1:2,j=1:3], ∨(y[i,j,:]...) := true) - @test con isa Matrix{LogicalConstraintRef} + @test con isa Matrix{LogicalConstraintRef{Model}} @test length(con) == 6 end @@ -111,7 +111,7 @@ function test_proposition_add_dense_axis() @test con isa Containers.DenseAxisArray @test con.axes[1] == ["a","b","c"] @test con.axes[2] == [1,2] - @test con.data isa Matrix{LogicalConstraintRef} + @test con.data isa Matrix{LogicalConstraintRef{Model}} end function test_proposition_add_sparse_axis() @@ -488,6 +488,18 @@ function test_reformulate_clause_error() @test_throws ErrorException DP._reformulate_clause(model, ex) end +function test_extension_propositions() + model = GDPModel{MyModel, MyVarRef, MyConRef}() + @variable(model, y[1:2], Logical(MyVar), start = true) + cref = LogicalConstraintRef(model, LogicalConstraintIndex(1)) + @test @constraint(model, (y[1] && ¬y[2]) == y[1] := true) == cref + @test DP._reformulate_logical_constraints(model) isa Nothing + @test length(DP._reformulation_constraints(model)) == 2 + @test length(model.cons) == 2 + @test model.cons[1] isa ScalarConstraint{GenericAffExpr{Float64, MyVarRef}, MOI.GreaterThan{Float64}} + @test model.cons[2] isa ScalarConstraint{GenericAffExpr{Float64, MyVarRef}, MOI.GreaterThan{Float64}} +end + @testset "Logical Proposition Constraints" begin @testset "Logical Operators" begin test_op_fallback() @@ -535,4 +547,7 @@ end test_distribute_and_over_or_nested() test_to_cnf() end -end \ No newline at end of file + @testset "Extension Propositions" begin + test_extension_propositions() + end +end diff --git a/test/constraints/selector.jl b/test/constraints/selector.jl index 98ee895..24a066c 100644 --- a/test/constraints/selector.jl +++ b/test/constraints/selector.jl @@ -47,7 +47,7 @@ function test_selector_add_array() model = GDPModel() @variable(model, y[1:2, 1:3, 1:4], Logical) @constraint(model, con[i=1:2, j=1:3], y[i,j,:] in Exactly(1)) - @test con isa Matrix{LogicalConstraintRef} + @test con isa Matrix{LogicalConstraintRef{Model}} @test length(con) == 6 end @@ -60,7 +60,7 @@ function test_selector_add_dense_axis() @test con isa Containers.DenseAxisArray @test con.axes[1] == ["a","b","c"] @test con.axes[2] == [1,2] - @test con.data isa Matrix{LogicalConstraintRef} + @test con.data isa Matrix{LogicalConstraintRef{Model}} end function test_selector_add_sparse_axis() @@ -102,7 +102,7 @@ function test_exactly_reformulation() @test is_valid(model, ref_con) ref_con_obj = constraint_object(ref_con) @test ref_con_obj.set == MOI.EqualTo(1.0) - @test ref_con_obj.func == sum(DP._reformulation_variables(model)) + @test ref_con_obj.func == sum(binary_variable.(y)) end function test_atleast_reformulation() @@ -114,7 +114,7 @@ function test_atleast_reformulation() @test is_valid(model, ref_con) ref_con_obj = constraint_object(ref_con) @test ref_con_obj.set == MOI.GreaterThan(1.0) - @test ref_con_obj.func == sum(DP._reformulation_variables(model)) + @test ref_con_obj.func == sum(binary_variable.(y)) end function test_atmost_reformulation() @@ -126,7 +126,7 @@ function test_atmost_reformulation() @test is_valid(model, ref_con) ref_con_obj = constraint_object(ref_con) @test ref_con_obj.set == MOI.LessThan(1.0) - @test ref_con_obj.func == sum(DP._reformulation_variables(model)) + @test ref_con_obj.func == sum(binary_variable.(y)) end function test_nested_exactly_reformulation() @@ -174,6 +174,18 @@ function test_nested_atmost_reformulation() DP._indicator_to_binary(model)[y[3]] end +function test_extension_variables() + model = GDPModel{MyModel, MyVarRef, MyConRef}() + @variable(model, y[1:2], Logical(MyVar), start = true) + cref = LogicalConstraintRef(model, LogicalConstraintIndex(1)) + @test @constraint(model, y in AtLeast(1)) == cref + @test reformulate_model(model, DummyReformulation()) isa Nothing + ref_con = DP._reformulation_constraints(model)[1] + c = constraint_object(ref_con) + @test c.set == MOI.GreaterThan(1.0) + @test c.func == sum(binary_variable.(y)) +end + @testset "Logical Selector Constraints" begin @testset "Add Selector" begin test_selector_add_fail() @@ -197,4 +209,7 @@ end test_nested_atleast_reformulation() test_nested_atmost_reformulation() end + @testset "Extension Selectors" begin + test_extension_variables() + end end \ No newline at end of file diff --git a/test/model.jl b/test/model.jl index 0e93d27..1cd2bff 100644 --- a/test/model.jl +++ b/test/model.jl @@ -1,10 +1,13 @@ using HiGHS function test_GDPData() - @test GDPData() isa GDPData + @test GDPData{Model, VariableRef, ConstraintRef}() isa GDPData{Model, VariableRef, ConstraintRef, Float64} end function test_empty_model() + @test GDPModel{GenericModel{Float16}, GenericVariableRef{Float16}, ConstraintRef}() isa GenericModel{Float16} + @test GDPModel{Int}() isa GenericModel{Int} + @test GDPModel{MyModel, MyVarRef, MyConRef}() isa MyModel model = GDPModel() @test gdp_data(model) isa GDPData @test isempty(DP._logical_variables(model)) @@ -17,6 +20,7 @@ function test_empty_model() @test isempty(DP._constraint_to_indicator(model)) @test isempty(DP._reformulation_variables(model)) @test isempty(DP._reformulation_constraints(model)) + @test isempty(DP._variable_bounds(model)) @test isnothing(DP._solution_method(model)) @test !DP._ready_to_optimize(model) end diff --git a/test/runtests.jl b/test/runtests.jl index f607a9e..28fdfbf 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3,28 +3,9 @@ using Test const DP = DisjunctiveProgramming -struct DummyReformulation <: AbstractReformulationMethod end - -# Utilities to test macro error exception -# Taken from https://github.com/jump-dev/JuMP.jl/blob/master/test/utilities.jl -function strip_line_from_error(err::ErrorException) - return ErrorException(replace(err.msg, r"^At.+\:[0-9]+\: `@" => "In `@")) -end -strip_line_from_error(err::LoadError) = strip_line_from_error(err.error) -strip_line_from_error(err) = err -macro test_macro_throws(errortype, m) - quote - @test_throws( - $(esc(strip_line_from_error(errortype))), - try - @eval $m - catch err - throw(strip_line_from_error(err)) - end - ) - end -end +include("utilities.jl") +# RUN ALL THE TESTS include("aqua.jl") include("model.jl") include("jump.jl") diff --git a/test/solve.jl b/test/solve.jl index 283c77c..7d74c64 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -1,7 +1,6 @@ using HiGHS -function test_linear_gdp_example() - m = GDPModel(HiGHS.Optimizer) +function test_linear_gdp_example(m) set_attribute(m, MOI.Silent(), true) @variable(m, 1 ≤ x[1:2] ≤ 9) @variable(m, Y[1:2], Logical) @@ -14,7 +13,7 @@ function test_linear_gdp_example() @disjunction(m, inner, [W[1], W[2]], Disjunct(Y[1])) @disjunction(m, outer, [Y[1], Y[2]]) - optimize!(m, method = BigM()) + optimize!(m, gdp_method = BigM()) @test termination_status(m) == MOI.OPTIMAL @test objective_value(m) ≈ 11 @test value.(x) ≈ [9,2] @@ -24,7 +23,7 @@ function test_linear_gdp_example() @test value(bins[W[1]]) ≈ 0 @test value(bins[W[2]]) ≈ 0 - optimize!(m, method = Hull()) + optimize!(m, gdp_method = Hull()) @test termination_status(m) == MOI.OPTIMAL @test objective_value(m) ≈ 11 @test value.(x) ≈ [9,2] @@ -43,6 +42,30 @@ function test_linear_gdp_example() @test value(variable_by_name(m, "x[2]_W[2]")) ≈ 0 end +function test_generic_model(m) + set_attribute(m, MOI.Silent(), true) + @variable(m, 1 ≤ x[1:2] ≤ 9) + @variable(m, Y[1:2], Logical) + @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])) + @constraint(m, w1[i=1:2], [1,5][i] ≤ x[i] ≤ [2,6][i], Disjunct(W[1])) + @constraint(m, w2[i=1:2], [2,4][i] ≤ x[i] ≤ [3,5][i], Disjunct(W[2])) + @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]]) + + optimize!(m, gdp_method = BigM()) + optimize!(m, gdp_method = Hull()) + + # TODO add meaningful tests to check the constraints/variables +end + @testset "Solve Linear GDP" begin - test_linear_gdp_example() + test_linear_gdp_example(GDPModel(HiGHS.Optimizer)) + mockoptimizer = () -> MOI.Utilities.MockOptimizer( + MOI.Utilities.UniversalFallback(MOIU.Model{Float32}()), + eval_objective_value = false + ) + test_generic_model(GDPModel{Float32}(mockoptimizer)) end \ No newline at end of file diff --git a/test/utilities.jl b/test/utilities.jl new file mode 100644 index 0000000..1f5f459 --- /dev/null +++ b/test/utilities.jl @@ -0,0 +1,143 @@ +# Utilities to test macro error exception +# Taken from https://github.com/jump-dev/JuMP.jl/blob/master/test/utilities.jl +function strip_line_from_error(err::ErrorException) + return ErrorException(replace(err.msg, r"^At.+\:[0-9]+\: `@" => "In `@")) +end +strip_line_from_error(err::LoadError) = strip_line_from_error(err.error) +strip_line_from_error(err) = err +macro test_macro_throws(errortype, m) + quote + @test_throws( + $(esc(strip_line_from_error(errortype))), + try + @eval $m + catch err + throw(strip_line_from_error(err)) + end + ) + end +end + +# Helper functions to prepare variable bounds without reformulating +function prep_bounds(vref, model, method) + if requires_variable_bound_info(method) + DP._variable_bounds(model)[vref] = set_variable_bound_info(vref, method) + end + return +end +function prep_bounds(vrefs::Vector, model, method) + for vref in vrefs + prep_bounds(vref, model, method) + end + return +end +function clear_bounds(model) + empty!(DP._variable_bounds(model)) + return +end + +# Prepare helpful test types +struct BadVarRef <: JuMP.AbstractVariableRef end +struct DummyReformulation <: AbstractReformulationMethod end + +# Define types/methods to test using DP with extension models +struct MyVar{I} <: JuMP.AbstractVariable + i::I +end +mutable struct MyModel <: AbstractModel + vars::Vector{MyVar} + deleted::Set{Int} + cons::Vector{Any} + ext::Dict{Symbol, Any} + optimize_hook::Any + obj_dict::Dict{Symbol, Any} + function MyModel() + return new(MyVar[], Set{Int}(), [], Dict{Symbol, Any}(), nothing, Dict{Symbol, Any}()) + end +end +struct MyVarRef <: AbstractVariableRef + m::MyModel + i::Int +end +struct MyConRef + m::MyModel + i::Int +end +JuMP.variable_ref_type(::Type{MyModel}) = MyVarRef +function JuMP.set_optimize_hook(m::MyModel, f) + m.optimize_hook = f +end +JuMP.object_dictionary(model::MyModel) = model.obj_dict +Base.broadcastable(model::MyModel) = Ref(model) +function JuMP.build_variable(::Function, info::VariableInfo, tag::Type{MyVar}) + return MyVar(Dict{Symbol, Any}(n => getproperty(info, n) for n in fieldnames(VariableInfo))) +end +function JuMP.add_variable(model::MyModel, v::MyVar, name::String = "") + push!(model.vars, v) + v.i[:name] = name + return MyVarRef(model, length(model.vars)) +end +function JuMP.add_variable(model::MyModel, v::ScalarVariable, name::String = "") + new_var = MyVar(Dict{Symbol, Any}(n => getproperty(v.info, n) for n in fieldnames(VariableInfo))) + push!(model.vars, new_var) + new_var.i[:name] = name + return MyVarRef(model, length(model.vars)) +end +JuMP.is_valid(m::MyModel, v::MyVarRef) = m === v.m && !(v.i in v.m.deleted) +Base.broadcastable(v::MyVarRef) = Ref(v) +Base.length(::MyVarRef) = 1 +Base.broadcastable(c::MyConRef) = Ref(c) +Base.length(::MyConRef) = 1 +Base.:(==)(v::MyVarRef, w::MyVarRef) = v.m === w.m && v.i == w.i +JuMP.is_binary(v::MyVarRef) = v.m.vars[v.i].i[:binary] +JuMP.is_integer(v::MyVarRef) = v.m.vars[v.i].i[:integer] +JuMP.has_lower_bound(v::MyVarRef) = v.m.vars[v.i].i[:has_lb] +JuMP.lower_bound(v::MyVarRef) = v.m.vars[v.i].i[:lower_bound] +JuMP.has_upper_bound(v::MyVarRef) = v.m.vars[v.i].i[:has_ub] +JuMP.upper_bound(v::MyVarRef) = v.m.vars[v.i].i[:upper_bound] +JuMP.start_value(v::MyVarRef) = v.m.vars[v.i].i[:start] +JuMP.set_start_value(v::MyVarRef, s) = setindex!(v.m.vars[v.i].i, s, :start) +JuMP.fix_value(v::MyVarRef) = v.m.vars[v.i].i[:fix_value] +JuMP.is_fixed(v::MyVarRef) = v.m.vars[v.i].i[:has_fix] +function JuMP.fix(v::MyVarRef, s) + v.m.vars[v.i].i[:fix_value] = s + v.m.vars[v.i].i[:has_fix] = true +end +function JuMP.unfix(v::MyVarRef) + v.m.vars[v.i].i[:fix_value] = NaN + v.m.vars[v.i].i[:has_fix] = false +end +JuMP.name(v::MyVarRef) = v.m.vars[v.i].i[:name] +JuMP.set_name(v::MyVarRef, n) = setindex!(v.m.vars[v.i].i, n, :name) +JuMP.owner_model(v::MyVarRef) = v.m +function JuMP.delete(m::MyModel, v::MyVarRef) + push!(v.m.deleted, v.i) + return +end +JuMP.is_valid(m::MyModel, v::MyConRef) = m === v.m && length(m.cons) >= v.i +Base.:(==)(v::MyConRef, w::MyConRef) = v.m === w.m && v.i == w.i +JuMP.owner_model(v::MyConRef) = v.m +function JuMP.add_constraint(model::MyModel, c::AbstractConstraint, n::String = "") + push!(model.cons, c) + return MyConRef(model, length(model.cons)) +end +JuMP.name(::MyConRef) = "testcon" +JuMP.constraint_object(c::MyConRef) = c.m.cons[c.i] +function JuMP.add_constraint( + model::MyModel, + c::VectorConstraint{F, S}, + name::String = "" + ) where {F, S <: AbstractCardinalitySet} + return DP._add_cardinality_constraint(model, c, name) +end +function JuMP.add_constraint( + model::M, + c::JuMP.ScalarConstraint{DP._LogicalExpr{M}, S}, + name::String = "" + ) where {S, M <: MyModel} # S <: JuMP._DoNotConvertSet{MOI.EqualTo{Bool}} or MOI.EqualTo{Bool} + return DP._add_logical_constraint(model, c, name) +end +DP.requires_disaggregation(::MyVarRef) = true +function DP.make_disaggregated_variable(model::MyModel, ::MyVarRef, name, lb, ub) + return JuMP.@variable(model, base_name = name, lower_bound = lb, upper_bound = ub) +end diff --git a/test/variables/logical.jl b/test/variables/logical.jl index c273dee..a218089 100644 --- a/test/variables/logical.jl +++ b/test/variables/logical.jl @@ -3,7 +3,7 @@ function test_base() model = GDPModel() @variable(model, y, Logical) - @test Base.broadcastable(y) isa Base.RefValue{LogicalVariableRef} + @test Base.broadcastable(y) isa Base.RefValue{LogicalVariableRef{Model}} @test length(y) == 1 end @@ -20,8 +20,8 @@ end function test_lvar_add_success() model = GDPModel() - @variable(model, y, Logical) - @test typeof(y) == LogicalVariableRef + y = LogicalVariableRef(model, LogicalVariableIndex(1)) + @test @variable(model, y, Logical) == y @test owner_model(y) == model @test is_valid(model, y) @test name(y) == "y" @@ -32,14 +32,14 @@ function test_lvar_add_success() @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)].name == "y" - #reformulate the variable - test_lvar_reformulation(model, y) + @test binary_variable(y) isa VariableRef + @test is_binary(binary_variable(y)) end function test_lvar_add_array() model = GDPModel() @variable(model, y[1:3, 1:2], Logical) - @test y isa Array{LogicalVariableRef, 2} + @test y isa Array{LogicalVariableRef{Model}, 2} @test length(y) == 6 end @@ -50,7 +50,7 @@ function test_lvar_add_dense_axis() @test length(y) == 6 @test y.axes[1] == ["a","b","c"] @test y.axes[2] == [1,2] - @test y.data isa Array{LogicalVariableRef, 2} + @test y.data isa Array{LogicalVariableRef{Model}, 2} end function test_lvar_add_sparse_axis() @@ -65,18 +65,15 @@ end function test_lvar_set_name() model = GDPModel() @variable(model, y, Logical) - set_name(y, "z") + @test set_name(y, "z") isa Nothing @test name(y) == "z" - #reformulate the variable - test_lvar_reformulation(model, y) end function test_lvar_creation_start_value() model = GDPModel() @variable(model, y, Logical, start = true) @test start_value(y) - #reformulate the variable - test_lvar_reformulation(model, y) + @test start_value(binary_variable(y)) == 1 end function test_lvar_set_start_value() @@ -85,14 +82,14 @@ function test_lvar_set_start_value() @test isnothing(start_value(y)) set_start_value(y, false) @test !start_value(y) - #reformulate the variable - test_lvar_reformulation(model, y) + @test start_value(binary_variable(y)) == 0 end function test_lvar_creation_fix_value() model = GDPModel() @variable(model, y == true, Logical) @test fix_value(y) + @test fix_value(binary_variable(y)) == 1 end function test_lvar_fix_value() @@ -101,11 +98,11 @@ function test_lvar_fix_value() @test isnothing(fix_value(y)) fix(y, true) @test fix_value(y) - #reformulate the variable - test_lvar_reformulation(model, y) + @test fix_value(binary_variable(y)) == 1 #unfix the value unfix(y) @test isnothing(fix_value(y)) + @test !is_fixed(binary_variable(y)) end function test_lvar_delete() @@ -117,11 +114,11 @@ function test_lvar_delete() @constraint(model, con2, x >= 50, Disjunct(z)) @disjunction(model, disj, [y, z]) @constraint(model, lcon, y ∨ z := true) - DP._reformulate_logical_variables(model) @test_throws AssertionError delete(GDPModel(), y) - delete(model, y) + bvar = binary_variable(y) + @test delete(model, y) isa Nothing @test !haskey(gdp_data(model).logical_variables, index(y)) @test haskey(gdp_data(model).logical_variables, index(z)) @test !haskey(gdp_data(model).disjunct_constraints, index(con)) @@ -131,38 +128,32 @@ function test_lvar_delete() @test !haskey(gdp_data(model).indicator_to_binary, y) @test haskey(gdp_data(model).indicator_to_binary, z) @test !DP._ready_to_optimize(model) + @test !is_valid(model, bvar) end -function test_lvar_reformulation() - model = GDPModel() - @variable(model, y, Logical, start = false) - fix(y, true) - test_lvar_reformulation(model, y) -end - -function test_lvar_reformulation(model::Model, lvref::LogicalVariableRef) - model = owner_model(lvref) - DP._reformulate_logical_variables(model) - @test haskey(DP._indicator_to_binary(model), lvref) - bvref = DP._indicator_to_binary(model)[lvref] - @test bvref in DP._reformulation_variables(model) - @test name(bvref) == name(lvref) - @test is_valid(model, bvref) - @test is_binary(bvref) - if isnothing(start_value(lvref)) - @test isnothing(start_value(bvref)) - elseif start_value(lvref) - @test isone(start_value(bvref)) - else - @test iszero(start_value(bvref)) - end - if isnothing(fix_value(lvref)) - @test_throws Exception fix_value(bvref) - elseif fix_value(lvref) - @test isone(fix_value(bvref)) - else - @test iszero(fix_value(bvref)) - end +function test_tagged_variables() + model = GDPModel{MyModel, MyVarRef, MyConRef}() + y = [LogicalVariableRef(model, LogicalVariableIndex(i)) for i in 1:2] + @test @variable(model, y[1:2], Logical(MyVar), start = true) == y + bvars = binary_variable.(y) + @test name(y[1]) == "y[1]" + @test start_value(y[1]) + @test set_start_value(y[2], false) isa Nothing + @test !start_value(y[2]) + @test start_value(bvars[2]) == 0 + @test !is_fixed(y[1]) + @test fix(y[1], false) isa Nothing + @test is_fixed(y[1]) + @test fix_value(bvars[1]) == 0 + @test unfix(y[1]) isa Nothing + @test !is_fixed(y[1]) + @test !is_fixed(bvars[1]) + @test set_name(y[2], "test") isa Nothing + @test name(y[2]) == "test" + @test name(bvars[2]) == "test" + @test delete(model, y[1]) isa Nothing + @test !is_valid(model, y[1]) + @test !is_valid(model, bvars[1]) end @testset "Logical Variables" begin @@ -186,7 +177,7 @@ end @testset "Delete Logical Variables" begin test_lvar_delete() end - @testset "Reformulate Logical Variables" begin - test_lvar_reformulation() + @testset "Tagged Logical Variables" begin + test_tagged_variables() end end \ No newline at end of file diff --git a/test/variables/query.jl b/test/variables/query.jl index 3a2f649..4b1ac73 100644 --- a/test/variables/query.jl +++ b/test/variables/query.jl @@ -92,7 +92,7 @@ function test_interrogate_proposition_constraint() ex = (implies(w[1], w[2]) ∧ w[3]) ⇔ (¬w[4] ∨ y) @constraint(m, con, ex := true) obj = constraint_object(con) - vars = DP._get_constraint_variables(m, obj) + vars = DP._get_logical_constraint_variables(m, obj) @test w[1] in vars @test w[2] in vars @test w[3] in vars @@ -108,7 +108,7 @@ function test_interrogate_selector_constraint() @variable(m, w[1:5], Logical) @constraint(m, con, w[1:4] in AtMost(y)) obj = constraint_object(con) - vars = DP._get_constraint_variables(m, obj) + vars = DP._get_logical_constraint_variables(m, obj) @test w[1] in vars @test w[2] in vars @test w[3] in vars