diff --git a/README.md b/README.md index 80868bd..d6b09c7 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,10 @@ The following reformulation methods are currently supported: 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. +4. [MBM](https://doi.org/10.1016/j.compchemeng.2015.02.013): The multiple big-m method creates multiple M values for each disjunct constraint. The 'MBM' struct is created with the following required argument: + + - `optimizer`: Optimizer to use when solving subproblems to determine M values. This is a required value. + ## Release Notes Prior to `v0.4.0`, the package did not leverage the JuMP extension capabilities and was not as robust. For these earlier releases, refer to [Perez, Joshi, and Grossmann, 2023](https://arxiv.org/abs/2304.10492v1) and the following [JuliaCon 2022 Talk](https://www.youtube.com/watch?v=AMIrgTTfUkI). diff --git a/src/DisjunctiveProgramming.jl b/src/DisjunctiveProgramming.jl index ae9892e..d646f17 100644 --- a/src/DisjunctiveProgramming.jl +++ b/src/DisjunctiveProgramming.jl @@ -6,7 +6,6 @@ Reexport.@reexport using JuMP # Use Meta for metaprogramming using Base.Meta - # Create aliases import JuMP.MOI as _MOI import JuMP.MOIU.CleverDicts as _MOIUC @@ -22,6 +21,7 @@ include("macros.jl") include("reformulate.jl") include("bigm.jl") include("hull.jl") +include("mbm.jl") include("indicator.jl") include("print.jl") diff --git a/src/constraints.jl b/src/constraints.jl index a25270d..63f0bb5 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -23,6 +23,32 @@ _vec_to_scalar_set(::_MOIExactly) = _MOI.EqualTo _vec_to_scalar_set(::_MOIAtLeast) = _MOI.GreaterThan _vec_to_scalar_set(::_MOIAtMost) = _MOI.LessThan +#helper function to create variables +# Helper function to copy variable properties from an existing variable +function _copy_variable( + target_model::JuMP.AbstractModel, + original_var::JuMP.AbstractVariableRef, + ) + # Create new variable + new_var = JuMP.@variable(target_model, base_name = JuMP.name(original_var)) + + # Copy all properties from original variable + JuMP.has_lower_bound(original_var) && JuMP.set_lower_bound(new_var, JuMP.lower_bound(original_var)) + JuMP.has_upper_bound(original_var) && JuMP.set_upper_bound(new_var, JuMP.upper_bound(original_var)) + JuMP.has_start_value(original_var) && JuMP.set_start_value(new_var, JuMP.start_value(original_var)) + JuMP.is_integer(original_var) && JuMP.set_integer(new_var) + JuMP.is_binary(original_var) && JuMP.set_binary(new_var) + + # Handle fixed values with force=true (as in original MBM code) + if JuMP.is_fixed(original_var) + JuMP.fix(new_var, JuMP.fix_value(original_var); force=true) + end + + return new_var +end + + + ################################################################################ # BOILERPLATE EXTENSION METHODS ################################################################################ diff --git a/src/datatypes.jl b/src/datatypes.jl index ffc9bd4..f8141cd 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -367,6 +367,25 @@ struct BigM{T} <: AbstractReformulationMethod end end +""" + MBM{O} <: AbstractReformulationMethod + +A type for using the multiple big-M reformulation approach for disjunctive constraints. + +**Fields** +- 'optimizer::O': Optimizer to use when solving mini-models (required). +""" +struct MBM{O} <: AbstractReformulationMethod + optimizer::O + + # Constructor with optimizer (required) + function MBM(optimizer::O) where {O} + new{O}(optimizer) + end +end + + + """ Hull{T} <: AbstractReformulationMethod diff --git a/src/mbm.jl b/src/mbm.jl new file mode 100644 index 0000000..387b4c2 --- /dev/null +++ b/src/mbm.jl @@ -0,0 +1,317 @@ +################################################################################ +# CONSTRAINT, DISJUNCTION, DISJUNCT REFORMULATION +################################################################################ +#Reformulates the disjunction using multiple big-M values +function reformulate_disjunction(model::JuMP.AbstractModel, + disj::Disjunction, + method::MBM + ) + ref_cons = Vector{JuMP.AbstractConstraint}() + for d in disj.indicators + constraints = copy(_indicator_to_constraints(model)[d]) # Make a copy to avoid modifying during iteration + for cref in constraints + con = JuMP.constraint_object(cref) + if con isa JuMP.ScalarConstraint{<:Any, <:MOI.Interval} + # Create lower and upper bound constraints + lower_con = JuMP.build_constraint(error, con.func, MOI.GreaterThan(con.set.lower)) + upper_con = JuMP.build_constraint(error, con.func, MOI.LessThan(con.set.upper)) + # Create new disjunct constraints + JuMP.add_constraint(model, _DisjunctConstraint(lower_con, d)) + JuMP.add_constraint(model, _DisjunctConstraint(upper_con, d)) + JuMP.delete(model, cref) + end + end + end + for d in disj.indicators + _reformulate_disjunct(model, ref_cons, d, LogicalVariableRef[x for x in disj.indicators if x != d], method) + end + return ref_cons +end +#Reformualates a disjunct the disjunct of interest +#represented by lvref and the other indicators in conlvref +function _reformulate_disjunct( + model::JuMP.AbstractModel, + ref_cons::Vector{JuMP.AbstractConstraint}, + lvref::LogicalVariableRef, + conlvref::Vector{LogicalVariableRef}, + method::AbstractReformulationMethod + ) + + bconref = Dict(d => binary_variable(d) for d in conlvref) + !haskey(_indicator_to_constraints(model), lvref) && return + constraints = _indicator_to_constraints(model)[lvref] + M = Dict{LogicalVariableRef,Float64}() + sizehint!(M, length(conlvref)) + for d in conlvref + M[d] = maximum(_maximize_M(model, JuMP.constraint_object(cref), + Vector{DisjunctConstraintRef}(_indicator_to_constraints(model)[d]), method) for cref in constraints) + end + + isempty(constraints) || sizehint!(ref_cons, length(constraints)) + for cref in constraints + con = JuMP.constraint_object(cref) + append!(ref_cons, reformulate_disjunct_constraint(model, con, Dict{LogicalVariableRef,VariableRef}(bconref), M, method)) + end + return ref_cons +end + +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.VectorConstraint{T, S, R}, + bconref:: Dict{LogicalVariableRef,<:JuMP.AbstractVariableRef}, + M::Dict{LogicalVariableRef,Float64}, + method::MBM +) where {T, S <: _MOI.Nonpositives, R} + m_sum = sum(M[i] * bconref[i] for i in keys(M)) + new_func = JuMP.@expression(model, [i=1:con.set.dimension], + con.func[i] - m_sum + ) + reform_con = JuMP.build_constraint(error, new_func, con.set) + return [reform_con] +end + +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.VectorConstraint{T, S, R}, + bconref:: Dict{LogicalVariableRef,<:JuMP.AbstractVariableRef}, + M::Dict{LogicalVariableRef,Float64}, + method::MBM +) where {T, S <: _MOI.Nonnegatives, R} + m_sum = sum(M[i] * bconref[i] for i in keys(M)) + new_func = JuMP.@expression(model, [i=1:con.set.dimension], + con.func[i] + m_sum + ) + reform_con = JuMP.build_constraint(error, new_func, con.set) + return [reform_con] +end + +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.VectorConstraint{T, S, R}, + bconref:: Dict{LogicalVariableRef,<:JuMP.AbstractVariableRef}, + M::Dict{LogicalVariableRef,Float64}, + method::MBM +) where {T, S <: _MOI.Zeros, R} + m_sum = sum(M[i] * bconref[i] for i in keys(M)) + upper_expr = JuMP.@expression(model, [i=1:con.set.dimension], + con.func[i] + m_sum + ) + lower_expr = JuMP.@expression(model, [i=1:con.set.dimension], + con.func[i] - m_sum + ) + upper_con = JuMP.build_constraint(error, upper_expr, MOI.Nonnegatives(con.set.dimension)) + lower_con = JuMP.build_constraint(error, lower_expr, MOI.Nonpositives(con.set.dimension)) + return [upper_con, lower_con] +end + + +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.ScalarConstraint{T, S}, + bconref:: Dict{LogicalVariableRef,<:JuMP.AbstractVariableRef}, + M::Dict{LogicalVariableRef,Float64}, + method::MBM +) where {T, S <: _MOI.LessThan} + new_func = JuMP.@expression(model, con.func - sum(M[i] * bconref[i] for i in keys(M))) + reform_con = JuMP.build_constraint(error, new_func, con.set) + return [reform_con] +end + +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.ScalarConstraint{T, S}, + bconref:: Dict{LogicalVariableRef,<:JuMP.AbstractVariableRef}, + M::Dict{LogicalVariableRef,Float64}, + method::MBM +) where {T, S <: _MOI.GreaterThan} + new_func = JuMP.@expression(model, con.func + sum(M[i] * bconref[i] for i in keys(M))) + reform_con = JuMP.build_constraint(error, new_func, con.set) + return [reform_con] +end + +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.ScalarConstraint{T, S}, + bconref:: Dict{LogicalVariableRef,<:JuMP.AbstractVariableRef}, + M::Dict{LogicalVariableRef,Float64}, + method::MBM +) where {T, S <: _MOI.EqualTo} + upper_func = JuMP.@expression(model, con.func - sum(M[i] * bconref[i] for i in keys(M))) + lower_func = JuMP.@expression(model, con.func + sum(M[i] * bconref[i] for i in keys(M))) + + upper_con = JuMP.build_constraint(error, upper_func, MOI.LessThan(con.set.value)) + lower_con = JuMP.build_constraint(error, lower_func, MOI.GreaterThan(con.set.value)) + return [lower_con, upper_con] +end +function reformulate_disjunct_constraint( + ::JuMP.AbstractModel, + ::F, + ::Dict{LogicalVariableRef,<:JuMP.AbstractVariableRef}, + ::Dict{LogicalVariableRef,Float64}, + ::MBM +) where {F} + error("Constraint type $(typeof(F)) is not supported by the Multiple Big-M reformulation method.") +end + +################################################################################ +# MULTIPLE BIG-M REFORMULATION +################################################################################ +#Dispatches over constaint types to reformulate into >= or <= in order to solve the mini-model +function _maximize_M( + model::JuMP.AbstractModel, + objective::JuMP.VectorConstraint{T, S, R}, + constraints::Vector{DisjunctConstraintRef}, + method::MBM +) where {T, S <: _MOI.Nonpositives, R} + return maximum(_maximize_M(model, JuMP.ScalarConstraint(objective.func[i], MOI.LessThan(0.0)), constraints, method) for i in 1:objective.set.dimension) +end + +function _maximize_M( + model::JuMP.AbstractModel, + objective::JuMP.VectorConstraint{T, S, R}, + constraints::Vector{DisjunctConstraintRef}, + method::MBM +) where {T, S <: _MOI.Nonnegatives, R} + return maximum(_maximize_M(model, JuMP.ScalarConstraint(objective.func[i], MOI.GreaterThan(0.0)), constraints, method) for i in 1:objective.set.dimension) +end + +function _maximize_M( + model::JuMP.AbstractModel, + objective::JuMP.VectorConstraint{T, S, R}, + constraints::Vector{DisjunctConstraintRef}, + method::MBM +) where {T, S <: _MOI.Zeros, R} + return max( + maximum(_maximize_M(model, JuMP.ScalarConstraint(objective.func[i], MOI.GreaterThan(0.0)), constraints, method) for i in 1:objective.set.dimension), + maximum(_maximize_M(model, JuMP.ScalarConstraint(objective.func[i], MOI.LessThan(0.0)), constraints, method) for i in 1:objective.set.dimension) + ) +end + +function _maximize_M( + model::JuMP.AbstractModel, + objective::JuMP.ScalarConstraint{T, S}, + constraints::Vector{DisjunctConstraintRef}, + method::MBM +) where {T, S <: Union{_MOI.LessThan, _MOI.GreaterThan}} + return _mini_model(model, objective, constraints, method) +end + +function _maximize_M( + model::JuMP.AbstractModel, + objective::JuMP.ScalarConstraint{T, S}, + constraints::Vector{DisjunctConstraintRef}, + method::MBM +) where {T, S <: _MOI.EqualTo} + return max( + _mini_model(model, JuMP.ScalarConstraint(objective.func, MOI.GreaterThan(objective.set.value)), constraints, method), + _mini_model(model, JuMP.ScalarConstraint(objective.func, MOI.LessThan(objective.set.value)), constraints, method) + ) +end + +function _maximize_M( + ::JuMP.AbstractModel, + ::F, + ::Vector{DisjunctConstraintRef}, + ::MBM +) where {F} + error("This type of constraints and objective constraint has not been implemented for MBM subproblems\nF: $(F)") +end + +#Solve a mini-model to find the maximum value of the objective function for M value +function _mini_model( + model::JuMP.AbstractModel, + objective::JuMP.ScalarConstraint{T,S}, + constraints::Vector{DisjunctConstraintRef}, + method::MBM +) where {T,S <: Union{_MOI.LessThan, _MOI.GreaterThan}} + sub_model = JuMP.Model() + new_vars = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() + for var in JuMP.all_variables(model) + new_vars[var] = _copy_variable(sub_model, var) + end + for con in [JuMP.constraint_object(con) for con in constraints] + expr = replace_variables_in_constraint(con.func, new_vars) + JuMP.@constraint(sub_model, expr * 1.0 in con.set) + end + constraint_to_objective(sub_model, objective, new_vars) + JuMP.set_optimizer(sub_model, method.optimizer) + JuMP.set_silent(sub_model) + JuMP.optimize!(sub_model) + if JuMP.termination_status(sub_model) != MOI.OPTIMAL || !JuMP.has_values(sub_model) || JuMP.primal_status(sub_model) != MOI.FEASIBLE_POINT + M = 1e9 + else + M = JuMP.objective_value(sub_model) + end + return M +end +#Catches any constraints that were not reformulated in _maximize_M +#_mini_model requires objective to be >= or <= in order to run +function _mini_model( + model::JuMP.AbstractModel, + objective::JuMP.ScalarConstraint{T,S}, + constraints::Vector{DisjunctConstraintRef}, + method::MBM +) where {T,S <: Union{_MOI.Nonpositives, _MOI.Nonnegatives, _MOI.Zeros, MOI.EqualTo, MOI.Interval}} + error("This type of constraints and objective constraint has not been implemented for MBM subproblems") +end + +################################################################################ +# CONSTRAINT TO OBJECTIVE +################################################################################ +function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::JuMP.ScalarConstraint{<:JuMP.AbstractJuMPScalar, MOI.LessThan{Float64}}, new_vars::Dict{V,K}) where {V <: JuMP.AbstractVariableRef, K <: JuMP.AbstractVariableRef} + JuMP.@objective(sub_model, Max, - obj.set.upper + replace_variables_in_constraint(obj.func, new_vars)) +end +function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::JuMP.ScalarConstraint{<:JuMP.AbstractJuMPScalar, MOI.GreaterThan{Float64}}, new_vars::Dict{V,K}) where {V <: JuMP.AbstractVariableRef, K <: JuMP.AbstractVariableRef} + JuMP.@objective(sub_model, Max, - replace_variables_in_constraint(obj.func, new_vars) + obj.set.lower) +end + +function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::JuMP.ScalarConstraint, new_vars::Dict{V,K}) where {V, K} + error("This type of constraint is not supported, only greater than and less than constraints are supported with intervals and equalities being converted.") +end + +################################################################################ +# REPLACE VARIABLES IN CONSTRAINT +################################################################################ + + + +function replace_variables_in_constraint(fun:: JuMP.AbstractVariableRef, var_map::Dict{<:JuMP.AbstractVariableRef,<:JuMP.AbstractVariableRef}) + return var_map[fun] +end + +function replace_variables_in_constraint(fun::JuMP.AffExpr, var_map::Dict{<:JuMP.AbstractVariableRef,<:JuMP.AbstractVariableRef}) + new_aff = JuMP.zero(JuMP.AffExpr) + for (var, coef) in fun.terms + new_var = var_map[var] + JuMP.add_to_expression!(new_aff, coef, new_var) + end + new_aff.constant = fun.constant + return new_aff +end + +function replace_variables_in_constraint(fun::JuMP.QuadExpr, var_map::Dict{<:JuMP.AbstractVariableRef,<:JuMP.AbstractVariableRef}) + new_quad = JuMP.zero(JuMP.QuadExpr) + for (vars, coef) in fun.terms + JuMP.add_to_expression!(new_quad, coef, var_map[vars.a], var_map[vars.b]) + end + new_aff = replace_variables_in_constraint(fun.aff, var_map) + JuMP.add_to_expression!(new_quad, new_aff) + return new_quad +end + +function replace_variables_in_constraint(fun::Number, var_map::Dict{<:JuMP.AbstractVariableRef,<:JuMP.AbstractVariableRef}) + return fun +end + +function replace_variables_in_constraint(fun::JuMP.NonlinearExpr, var_map::Dict{<:JuMP.AbstractVariableRef,<:JuMP.AbstractVariableRef}) + new_args = Any[replace_variables_in_constraint(arg, var_map) for arg in fun.args] + return JuMP.NonlinearExpr(fun.head, new_args) +end + +function replace_variables_in_constraint(fun::Vector{T}, var_map::Dict{<:JuMP.AbstractVariableRef,<:JuMP.AbstractVariableRef}) where T + return [replace_variables_in_constraint(expr, var_map) for expr in fun] +end + +function replace_variables_in_constraint(::F, ::S) where {F, S} + error("replace_variables_in_constraint not implemented for $(typeof(F)) and $(typeof(S))") +end \ No newline at end of file diff --git a/test/constraints/mbm.jl b/test/constraints/mbm.jl new file mode 100644 index 0000000..18db02f --- /dev/null +++ b/test/constraints/mbm.jl @@ -0,0 +1,168 @@ +using HiGHS + +function test_mbm() + @test MBM(HiGHS.Optimizer).optimizer == HiGHS.Optimizer +end + +function test_replace_variables_in_constraint() + model = Model() + sub_model = Model() + @variable(model, x[1:3]) + @constraint(model, con1,x[1] <= 1) + @constraint(model, con2,x[2]*x[1] <= 1) + @constraint(model, con3,sin(x[3]) <= 0) + @constraint(model, con4, [x[1],x[2],x[3]] in MOI.Zeros(3)) + + #Test GenericVariableRef + new_vars = Dict{VariableRef, VariableRef}() + [new_vars[x[i]] = @variable(sub_model) for i in 1:3] + varref = DP.replace_variables_in_constraint(x[1], new_vars) + expr1 = DP.replace_variables_in_constraint(constraint_object(con1).func, new_vars) + expr2 = DP.replace_variables_in_constraint(constraint_object(con2).func, new_vars) + expr3 = DP.replace_variables_in_constraint(constraint_object(con3).func, new_vars) + expr4 = DP.replace_variables_in_constraint(constraint_object(con4).func, new_vars) + @test expr1 == JuMP.@expression(sub_model, new_vars[x[1]] + 1 - 1) + @test expr2 == JuMP.@expression(sub_model, new_vars[x[2]]*new_vars[x[1]]) + @test varref == new_vars[x[1]] + # Test nonlinear expression structure + #TODO: Fix expr3 test, JuMP.@expression(model, sin(x[3]) - 0) is not equal to expr3 + #MBM: Test Failed at c:\Users\LocalAdmin\Code\DisjunctiveProgramming.jl\test\constraints\mbm.jl:32 + # Expression: expr3 == #= c:\Users\LocalAdmin\Code\DisjunctiveProgramming.jl\test\constraints\mbm.jl:32 =# JuMP.@expression(model, sin(x[3]) - 0) + # Evaluated: sin(_[3]) - 0.0 == sin(x[3]) - 0.0 + @test expr3 isa JuMP.NonlinearExpr # Test it's a nonlinear expression + @test expr4 == [new_vars[x[i]] for i in 1:3] + @test_throws ErrorException DP.replace_variables_in_constraint("String", new_vars) +end + +function test_constraint_to_objective() + model = Model() + sub_model = Model() + + @variable(model, x[1:2]) + @constraint(model, lessthan, x[1] <= 1) + @constraint(model, greaterthan, x[2] >= 1) + @constraint(model, interval, 0 <= x[1] <= 55) + @constraint(model, equalto, x[1] == 1) + new_vars = Dict{VariableRef, VariableRef}() + [new_vars[x[i]] = @variable(sub_model) for i in 1:2] + DP.constraint_to_objective(sub_model, constraint_object(lessthan), new_vars) + @test objective_function(sub_model) == JuMP.@expression(sub_model, new_vars[x[1]] - 1) + DP.constraint_to_objective(sub_model, constraint_object(greaterthan), new_vars) + @test objective_function(sub_model) == JuMP.@expression(sub_model, 1 - new_vars[x[2]]) + @test_throws ErrorException DP.constraint_to_objective(sub_model, constraint_object(interval), new_vars) +end + +function test_mini_model() + model = GDPModel() + @variable(model, 0 <= x, start = 1) + @variable(model, 0 <= y) + @variable(model, Y[1:3], Logical) + @constraint(model, con, 3*-x <= 4, Disjunct(Y[1])) + @constraint(model, con2, 3*x + y >= 15, Disjunct(Y[2])) + @constraint(model, infeasiblecon, 3*x + y == 15, Disjunct(Y[3])) + @disjunction(model, [Y[1], Y[2], Y[3]]) + @test DP._mini_model(model, constraint_object(con), DisjunctConstraintRef[con2], MBM(HiGHS.Optimizer))== -4 + set_upper_bound(x, 1) + @test DP._mini_model(model, constraint_object(con2), DisjunctConstraintRef[con], MBM(HiGHS.Optimizer))== 15 + set_integer(y) + @constraint(model, con3, y*x == 15, Disjunct(Y[1])) + @test DP._mini_model(model, constraint_object(con2), DisjunctConstraintRef[con], MBM(HiGHS.Optimizer))== 15 + JuMP.fix(y, 5; force=true) + @test DP._mini_model(model, constraint_object(con2), DisjunctConstraintRef[con], MBM(HiGHS.Optimizer))== 10 + delete_lower_bound(x) + @test DP._mini_model(model, constraint_object(con2), DisjunctConstraintRef[con2], MBM(HiGHS.Optimizer)) == 1.0e9 + @test_throws ErrorException DP._mini_model(model, constraint_object(infeasiblecon), DisjunctConstraintRef[con], MBM(HiGHS.Optimizer)) +end + +function test_maximize_M() + model = GDPModel() + @variable(model, 0 <= x[1:2] <= 50) + @variable(model, Y[1:6], Logical) + @constraint(model, lessthan, x[1] <= 1, Disjunct(Y[1])) + @constraint(model, greaterthan, x[1] >= 1, Disjunct(Y[1])) + @constraint(model, interval, 0 <= x[1] <= 55, Disjunct(Y[2])) + @constraint(model, equalto, x[1] == 1, Disjunct(Y[3])) + @constraint(model, nonpositives, -x in MOI.Nonpositives(2), Disjunct(Y[4])) + @constraint(model, nonnegatives, x in MOI.Nonnegatives(2), Disjunct(Y[5])) + @constraint(model, zeros, -x .+ 1 in MOI.Zeros(2), Disjunct(Y[6])) + + @test DP._maximize_M(model, constraint_object(lessthan), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(HiGHS.Optimizer)) == 49 + @test DP._maximize_M(model, constraint_object(greaterthan), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(HiGHS.Optimizer)) == 1.0 + @test DP._maximize_M(model, constraint_object(equalto), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[3]]), MBM(HiGHS.Optimizer)) == 0 + @test DP._maximize_M(model, constraint_object(nonpositives), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(HiGHS.Optimizer)) == 0 + @test DP._maximize_M(model, constraint_object(nonnegatives), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(HiGHS.Optimizer)) == 0 + @test DP._maximize_M(model, constraint_object(zeros), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(HiGHS.Optimizer)) == 49 + @test_throws ErrorException DP._maximize_M(model, "odd", Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(HiGHS.Optimizer)) +end +function test_reformulate_disjunct_constraint() + model = GDPModel() + @variable(model, 0 <= x[1:2] <= 50) + @variable(model, Y[1:6], Logical) + @constraint(model, lessthan, x[1] <= 1, Disjunct(Y[1])) + @constraint(model, greaterthan, x[1] >= 1, Disjunct(Y[1])) + @constraint(model, equalto, x[1] == 1, Disjunct(Y[2])) + @constraint(model, nonpositives, -x in MOI.Nonpositives(2), Disjunct(Y[3])) + @constraint(model, nonnegatives, x in MOI.Nonnegatives(2), Disjunct(Y[4])) + @constraint(model, zeros, -x .+ 1 in MOI.Zeros(2), Disjunct(Y[5])) + + M = Dict{LogicalVariableRef,Float64}(Y[i] => Float64(i) for i in 1:5) + bconref = Dict{LogicalVariableRef,VariableRef}(Y[i] => binary_variable(Y[i]) for i in 1:5) + reformulated_constraints = [reformulate_disjunct_constraint(model, constraint_object(constraints), bconref, M, MBM(HiGHS.Optimizer)) for constraints in [lessthan, greaterthan, equalto, nonpositives, nonnegatives, zeros]] + @test reformulated_constraints[1][1].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in keys(M))) && reformulated_constraints[1][1].set == MOI.LessThan(1.0) + @test reformulated_constraints[2][1].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in keys(M))) && reformulated_constraints[2][1].set == MOI.GreaterThan(1.0) + @test reformulated_constraints[3][1].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in keys(M))) && reformulated_constraints[3][1].set == MOI.GreaterThan(1.0) + @test reformulated_constraints[3][2].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in keys(M))) && reformulated_constraints[3][2].set == MOI.LessThan(1.0) + @test reformulated_constraints[4][1].func == JuMP.@expression(model, -x .- sum(M[i] * bconref[i] for i in keys(M))) && reformulated_constraints[4][1].set == MOI.Nonpositives(2) + @test reformulated_constraints[5][1].func == JuMP.@expression(model, x .+ sum(M[i] * bconref[i] for i in keys(M))) && reformulated_constraints[5][1].set == MOI.Nonnegatives(2) + @test reformulated_constraints[6][1].func == JuMP.@expression(model, -x .+(1 + sum(M[i] * bconref[i] for i in keys(M)))) && reformulated_constraints[6][1].set == MOI.Nonnegatives(2) + @test reformulated_constraints[6][2].func == JuMP.@expression(model, -x .+(1 - sum(M[i] * bconref[i] for i in keys(M)))) && reformulated_constraints[6][2].set == MOI.Nonpositives(2) + @test_throws ErrorException reformulate_disjunct_constraint(model, "odd", bconref, M, MBM(HiGHS.Optimizer)) +end + +function test_reformulate_disjunct() + model = GDPModel() + @variable(model, 1 <= x[1:2] <= 50) + @variable(model, Y[1:2], Logical) + @constraint(model, lessthan, x[1] <= 2, Disjunct(Y[1])) + @constraint(model, greaterthan, x[1] >= 1, Disjunct(Y[1])) + @constraint(model, interval, x[1] == 55, Disjunct(Y[2])) + + bconref = [binary_variable(Y[i]) for i in 1:2] + reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[1], LogicalVariableRef[Y[2]], MBM(HiGHS.Optimizer)) + M = [0, 1e9] + @test reformulated_disjunct[1].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[1].set == MOI.LessThan(2.0) + @test reformulated_disjunct[2].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[2].set == MOI.GreaterThan(1.0) + + reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[2], LogicalVariableRef[Y[1]], MBM(HiGHS.Optimizer)) + M = [54, 0] + + @test reformulated_disjunct[2].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[2].set == MOI.LessThan(55.0) + @test reformulated_disjunct[1].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[1].set == MOI.GreaterThan(55.0) + +end + +function test_reformulate_disjunction() + model = GDPModel() + @variable(model, x) + @variable(model, Y[1:2], Logical) + @constraint(model, lessthan, x <= 2, Disjunct(Y[1])) + @constraint(model, greaterthan, x >= 1, Disjunct(Y[1])) + @constraint(model, interval, 0 <= x <= 55, Disjunct(Y[2])) + disj = disjunction(model, [Y[1], Y[2]]) + ref_cons = reformulate_disjunction(model, constraint_object(disj), MBM(HiGHS.Optimizer)) + @test ref_cons[1].func == JuMP.@expression(model, x - 53 * binary_variable(Y[2])) && ref_cons[1].set == MOI.LessThan(2.0) + @test ref_cons[2].func == JuMP.@expression(model, x + 53 * binary_variable(Y[2])) && ref_cons[2].set == MOI.GreaterThan(1.0) +end + +@testset "MBM" begin + test_mbm() + test_replace_variables_in_constraint() + test_constraint_to_objective() + test_mini_model() + test_maximize_M() + test_reformulate_disjunct_constraint() + test_reformulate_disjunct() + test_reformulate_disjunction() +end + + diff --git a/test/runtests.jl b/test/runtests.jl index ba5c511..42e0738 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,7 +1,6 @@ import DisjunctiveProgramming as DP using DisjunctiveProgramming using Test - include("utilities.jl") # RUN ALL THE TESTS @@ -20,3 +19,4 @@ include("constraints/fallback.jl") include("constraints/disjunction.jl") include("print.jl") include("solve.jl") +include("constraints/mbm.jl")