diff --git a/src/datatypes.jl b/src/datatypes.jl index 6ba1315..387e49b 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -388,15 +388,15 @@ end mutable struct _MBM{O, T, M <: JuMP.AbstractModel} <: AbstractReformulationMethod optimizer::O - M::Dict{LogicalVariableRef{M}, T} - default_M::T - conlvref::Vector{LogicalVariableRef{M}} + M::Dict{LogicalVariableRef{M}, Union{T, Vector{T}}} + default_M::T + conlvref::Vector{LogicalVariableRef{M}} function _MBM(method::MBM{O, T}, model::M) where {O, T, M <: JuMP.AbstractModel} new{O, T, M}(method.optimizer, - Dict{LogicalVariableRef{M}, T}(), + Dict{LogicalVariableRef{M}, Union{T, Vector{T}}}(), method.default_M, - Vector{LogicalVariableRef{M}}() + Vector{LogicalVariableRef{M}}() ) end end diff --git a/src/mbm.jl b/src/mbm.jl index e9b1f13..871b06c 100644 --- a/src/mbm.jl +++ b/src/mbm.jl @@ -15,39 +15,44 @@ function reformulate_disjunction( end return ref_cons end -#Reformualates a disjunct the disjunct of interest -#represented by lvref and the other indicators in conlvref + +# Reformulates a disjunct represented by lvref using per-constraint M values. +# gets its own set of M_{ie,i'} values for each other term i'. function _reformulate_disjunct( - model::JuMP.AbstractModel, - ref_cons::Vector{JuMP.AbstractConstraint}, + model::JuMP.AbstractModel, + ref_cons::Vector{JuMP.AbstractConstraint}, lvref::LogicalVariableRef, method::_MBM -) - - empty!(method.M) +) !haskey(_indicator_to_constraints(model), lvref) && return bconref = Dict(d => binary_variable(d) for d in method.conlvref) - + constraints = _indicator_to_constraints(model)[lvref] - filtered_constraints = [c for c in constraints if c isa DisjunctConstraintRef] - - for d in method.conlvref - d_constraints = _indicator_to_constraints(model)[d] - disjunct_constraints = [c for c in d_constraints if c isa DisjunctConstraintRef] - if !isempty(disjunct_constraints) - method.M[d] = maximum( - _maximize_M( - model, - JuMP.constraint_object(cref), + filtered_constraints = [ + c for c in constraints if c isa DisjunctConstraintRef + ] + + # For each constraint, compute its own set of M values + for cref in filtered_constraints + empty!(method.M) + + for d in method.conlvref + d_constraints = _indicator_to_constraints(model)[d] + disjunct_constraints = [ + c for c in d_constraints if c isa DisjunctConstraintRef + ] + if !isempty(disjunct_constraints) + method.M[d] = _maximize_M( + model, + JuMP.constraint_object(cref), disjunct_constraints, method - ) for cref in filtered_constraints - ) + ) + end end - end - for cref in filtered_constraints - con = JuMP.constraint_object(cref) - append!(ref_cons, reformulate_disjunct_constraint(model, con, + + con = JuMP.constraint_object(cref) + append!(ref_cons, reformulate_disjunct_constraint(model, con, bconref, method)) end return ref_cons @@ -71,6 +76,7 @@ function reformulate_disjunct_constraint( return new_ref_cons end +# Uses per-row M values: method.M[d][row] for each disjunct d function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, @@ -78,15 +84,14 @@ function reformulate_disjunct_constraint( Dict{<:LogicalVariableRef,<:JuMP.GenericAffExpr}}, method::_MBM ) where {T, S <: _MOI.Nonpositives, R} - m_sum = sum(method.M[i] * bconref[i] for i in keys(method.M)) new_func = JuMP.@expression(model, [i=1:con.set.dimension], - con.func[i] - m_sum + con.func[i] - sum(method.M[d][i] * bconref[d] for d in keys(method.M)) ) reform_con = JuMP.build_constraint(error, new_func, con.set) return [reform_con] end - +# Uses per-row M values: method.M[d][row] for each disjunct d function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, @@ -94,27 +99,26 @@ function reformulate_disjunct_constraint( Dict{<:LogicalVariableRef,<:JuMP.GenericAffExpr}}, method::_MBM ) where {T, S <: _MOI.Nonnegatives, R} - m_sum = sum(method.M[i] * bconref[i] for i in keys(method.M)) new_func = JuMP.@expression(model, [i=1:con.set.dimension], - con.func[i] + m_sum + con.func[i] + sum(method.M[d][i] * bconref[d] for d in keys(method.M)) ) reform_con = JuMP.build_constraint(error, new_func, con.set) return [reform_con] end +# Uses per-row M values: method.M[d][row] for each disjunct d function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bconref:: Union{Dict{<:LogicalVariableRef,<:JuMP.AbstractVariableRef}, + bconref::Union{Dict{<:LogicalVariableRef,<:JuMP.AbstractVariableRef}, Dict{<:LogicalVariableRef,<:JuMP.GenericAffExpr}}, method::_MBM ) where {T, S <: _MOI.Zeros, R} - m_sum = sum(method.M[i] * bconref[i] for i in keys(method.M)) upper_expr = JuMP.@expression(model, [i=1:con.set.dimension], - con.func[i] + m_sum + con.func[i] + sum(method.M[d][i] * bconref[d] for d in keys(method.M)) ) lower_expr = JuMP.@expression(model, [i=1:con.set.dimension], - con.func[i] - m_sum + con.func[i] - sum(method.M[d][i] * bconref[d] for d in keys(method.M)) ) upper_con = JuMP.build_constraint(error, upper_expr, MOI.Nonnegatives(con.set.dimension) @@ -153,6 +157,7 @@ function reformulate_disjunct_constraint( return [reform_con] end +# Uses per-bound M values: method.M[d][1] for lower, method.M[d][2] for upper function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, @@ -160,21 +165,24 @@ function reformulate_disjunct_constraint( Dict{<:LogicalVariableRef,<:JuMP.GenericAffExpr}}, method::_MBM ) where {T, S <: _MOI.EqualTo} - upper_func = JuMP.@expression(model, - con.func - sum(method.M[i] * bconref[i] for i in keys(method.M)) + # M[d][1] = M for GreaterThan (lower bound), M[d][2] = M for LessThan + # (upper bound) + lower_func = JuMP.@expression(model, + con.func + sum(method.M[d][1] * bconref[d] for d in keys(method.M)) ) - lower_func = JuMP.@expression(model, - con.func + sum(method.M[i] * bconref[i] for i in keys(method.M)) - ) - upper_con = JuMP.build_constraint(error, upper_func, - MOI.LessThan(con.set.value) + upper_func = JuMP.@expression(model, + con.func - sum(method.M[d][2] * bconref[d] for d in keys(method.M)) ) - lower_con = JuMP.build_constraint(error, lower_func, + lower_con = JuMP.build_constraint(error, lower_func, MOI.GreaterThan(con.set.value) ) + upper_con = JuMP.build_constraint(error, upper_func, + MOI.LessThan(con.set.value) + ) return [lower_con, upper_con] end +# Uses per-bound M values: method.M[d][1] for lower, method.M[d][2] for upper function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, @@ -182,21 +190,21 @@ function reformulate_disjunct_constraint( Dict{<:LogicalVariableRef,<:JuMP.GenericAffExpr}}, method::_MBM ) where {T, S <: _MOI.Interval} - set_values = _set_values(con.set) - upper_func = JuMP.@expression(model, - con.func - sum(method.M[i] * bconref[i] for i in keys(method.M)) + set_values = _set_values(con.set) + # M[d][1] = M for GreaterThan (lower bound), M[d][2] = M for LessThan + # (upper bound) + lower_func = JuMP.@expression(model, + con.func + sum(method.M[d][1] * bconref[d] for d in keys(method.M)) ) - upper_con = JuMP.build_constraint(error, upper_func, - MOI.LessThan(set_values[2]) + upper_func = JuMP.@expression(model, + con.func - sum(method.M[d][2] * bconref[d] for d in keys(method.M)) ) - - lower_func = JuMP.@expression(model, - con.func + sum(method.M[i] * bconref[i] for i in keys(method.M)) - ) - lower_con = JuMP.build_constraint(error, lower_func, + lower_con = JuMP.build_constraint(error, lower_func, MOI.GreaterThan(set_values[1]) ) - + upper_con = JuMP.build_constraint(error, upper_func, + MOI.LessThan(set_values[2]) + ) return [lower_con, upper_con] end @@ -216,65 +224,74 @@ end ################################################################################ # Dispatches over constraint types to reformulate into >= or <= # in order to solve the mini-model +# Returns Vector{T} - one M per row for tighter per-row relaxations function _maximize_M( model::JuMP.AbstractModel, objective::JuMP.VectorConstraint{T, S, R}, constraints::Vector{<:DisjunctConstraintRef}, method::_MBM -) where { T, S <: _MOI.Nonpositives, R} +) where {T, S <: _MOI.Nonpositives, R} val_type = JuMP.value_type(typeof(model)) - return maximum( + return [ _maximize_M( - model, - JuMP.ScalarConstraint(objective.func[i], MOI.LessThan(zero(val_type))), - constraints, + model, + JuMP.ScalarConstraint( + objective.func[i], MOI.LessThan(zero(val_type)) + ), + constraints, method ) for i in 1:objective.set.dimension - ) + ] end +# Returns Vector{T} - one M per row for tighter per-row relaxations function _maximize_M( model::JuMP.AbstractModel, objective::JuMP.VectorConstraint{T, S, R}, constraints::Vector{<:DisjunctConstraintRef}, method::_MBM -) where { T, S <: _MOI.Nonnegatives, R} +) where {T, S <: _MOI.Nonnegatives, R} val_type = JuMP.value_type(typeof(model)) - return maximum( + return [ _maximize_M( - model, - JuMP.ScalarConstraint(objective.func[i], MOI.GreaterThan(zero(val_type))), - constraints, + model, + JuMP.ScalarConstraint( + objective.func[i], MOI.GreaterThan(zero(val_type)) + ), + constraints, method ) for i in 1:objective.set.dimension - ) + ] end +# Returns Vector{T} - one M per row, each is max(M_ge, M_le) for that row function _maximize_M( model::JuMP.AbstractModel, objective::JuMP.VectorConstraint{T, S, R}, constraints::Vector{<:DisjunctConstraintRef}, method::_MBM -) where { T, S <: _MOI.Zeros, R} +) where {T, S <: _MOI.Zeros, R} val_type = JuMP.value_type(typeof(model)) - return max( - maximum( + return [ + max( _maximize_M( - model, - JuMP.ScalarConstraint(objective.func[i],MOI.GreaterThan(zero(val_type))), - constraints, + model, + JuMP.ScalarConstraint( + objective.func[i], MOI.GreaterThan(zero(val_type)) + ), + constraints, method - ) for i in 1:objective.set.dimension - ), - maximum( + ), _maximize_M( - model, - JuMP.ScalarConstraint(objective.func[i], MOI.LessThan(zero(val_type))), - constraints, + model, + JuMP.ScalarConstraint( + objective.func[i], MOI.LessThan(zero(val_type)) + ), + constraints, method - ) for i in 1:objective.set.dimension - ) - ) + ) + ) for i in 1:objective.set.dimension + ] end function _maximize_M( @@ -286,6 +303,7 @@ function _maximize_M( return _mini_model(model, objective, constraints, method) end +# Returns [M_lower, M_upper] for per-bound relaxations function _maximize_M( model::JuMP.AbstractModel, objective::JuMP.ScalarConstraint{T, S}, @@ -293,22 +311,23 @@ function _maximize_M( method::_MBM ) where {T, S <: _MOI.EqualTo} set_value = objective.set.value - return max( + return [ _mini_model( - model, - JuMP.ScalarConstraint(objective.func, MOI.GreaterThan(set_value)), - constraints, + model, + JuMP.ScalarConstraint(objective.func, MOI.GreaterThan(set_value)), + constraints, method ), _mini_model( - model, - JuMP.ScalarConstraint(objective.func, MOI.LessThan(set_value)), - constraints, + model, + JuMP.ScalarConstraint(objective.func, MOI.LessThan(set_value)), + constraints, method ) - ) + ] end +# Returns [M_lower, M_upper] for per-bound relaxations function _maximize_M( model::JuMP.AbstractModel, objective::JuMP.ScalarConstraint{T, S}, @@ -316,20 +335,24 @@ function _maximize_M( method::_MBM ) where {T, S <: _MOI.Interval} set_values = _set_values(objective.set) # Returns (lower, upper) - return max( + return [ _mini_model( - model, - JuMP.ScalarConstraint(objective.func, MOI.GreaterThan(set_values[1])), - constraints, + model, + JuMP.ScalarConstraint( + objective.func, MOI.GreaterThan(set_values[1]) + ), + constraints, method ), _mini_model( - model, - JuMP.ScalarConstraint(objective.func, MOI.LessThan(set_values[2])), - constraints, + model, + JuMP.ScalarConstraint( + objective.func, MOI.LessThan(set_values[2]) + ), + constraints, method ) - ) + ] end function _maximize_M( diff --git a/test/constraints/mbm.jl b/test/constraints/mbm.jl index ed74a5a..e79573d 100644 --- a/test/constraints/mbm.jl +++ b/test/constraints/mbm.jl @@ -2,7 +2,9 @@ using HiGHS function test_mbm() - @test DP._MBM(DP.MBM(HiGHS.Optimizer), JuMP.Model()).optimizer == HiGHS.Optimizer + @test DP._MBM( + DP.MBM(HiGHS.Optimizer), JuMP.Model() + ).optimizer == HiGHS.Optimizer end @@ -91,49 +93,78 @@ end function test_maximize_M() model = GDPModel() - @variable(model, 0 <= x[1:2] <= 50) + # Different bounds for x[1] and x[2] to demonstrate per-row M values + @variable(model, x[1:2]) + set_lower_bound(x[1], 0); set_upper_bound(x[1], 10) + set_lower_bound(x[2], 0); set_upper_bound(x[2], 5) @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), + # Vector constraints: x >= 0 (both rows) + @constraint(model, nonpositives, -x in MOI.Nonpositives(2), Disjunct(Y[4])) - @constraint(model, nonnegatives, x in MOI.Nonnegatives(2), + @constraint(model, nonnegatives, x in MOI.Nonnegatives(2), Disjunct(Y[5])) + # Vector equality: x == 1 (both rows) @constraint(model, zeros, -x .+ 1 in MOI.Zeros(2), Disjunct(Y[6])) mbm = DP._MBM(DP.MBM(HiGHS.Optimizer), JuMP.Model()) - @test DP._maximize_M(model, constraint_object(interval), + + # Interval returns [M_lower, M_upper] + # M_lower = max(0 - x[1]) s.t. 0<=x[1]<=10 = 0 at x[1]=0 + # M_upper = max(x[1] - 55) s.t. 0<=x[1]<=10 = -45 at x[1]=10 + @test DP._maximize_M(model, constraint_object(interval), Vector{DisjunctConstraintRef}( - DP._indicator_to_constraints(model)[Y[2]]), - mbm) == 0.0 - @test DP._maximize_M(model, constraint_object(lessthan), + DP._indicator_to_constraints(model)[Y[2]]), + mbm) == [0.0, -45.0] + + # Scalar LessThan/GreaterThan still return scalars + # lessthan: x[1] <= 1 vs interval 0 <= x[1] <= 55 + # max(x[1] - 1) s.t. 0<=x[1]<=10 = 9 at x[1]=10 + @test DP._maximize_M(model, constraint_object(lessthan), Vector{DisjunctConstraintRef}( - DP._indicator_to_constraints(model)[Y[2]]), - mbm) == 49 - @test DP._maximize_M(model, constraint_object(greaterthan), + DP._indicator_to_constraints(model)[Y[2]]), + mbm) == 9.0 + + # greaterthan: x[1] >= 1 vs interval 0 <= x[1] <= 55 + # max(1 - x[1]) s.t. 0<=x[1]<=10 = 1 at x[1]=0 + @test DP._maximize_M(model, constraint_object(greaterthan), Vector{DisjunctConstraintRef}( - DP._indicator_to_constraints(model)[Y[2]]), + DP._indicator_to_constraints(model)[Y[2]]), mbm) == 1.0 - @test DP._maximize_M(model, constraint_object(equalto), + + # EqualTo returns [M_lower, M_upper] + @test DP._maximize_M(model, constraint_object(equalto), Vector{DisjunctConstraintRef}( - DP._indicator_to_constraints(model)[Y[3]]), - mbm) == 0 - @test DP._maximize_M(model, constraint_object(nonpositives), + DP._indicator_to_constraints(model)[Y[3]]), + mbm) == [0.0, 0.0] + + # Vector constraints: per-row M values + # nonpositives: x >= 0 against Y[2] (interval only on x[1]) + # Row 1: max(0 - x[1]) s.t. 0<=x[1]<=10 = 0 + # Row 2: max(0 - x[2]) s.t. 0<=x[2]<=5 = 0 + @test DP._maximize_M(model, constraint_object(nonpositives), Vector{DisjunctConstraintRef}( - DP._indicator_to_constraints(model)[Y[2]]), - mbm) == 0 - @test DP._maximize_M(model, constraint_object(nonnegatives), + DP._indicator_to_constraints(model)[Y[2]]), + mbm) == [0.0, 0.0] + + @test DP._maximize_M(model, constraint_object(nonnegatives), Vector{DisjunctConstraintRef}( - DP._indicator_to_constraints(model)[Y[2]]), - mbm) == 0 - @test DP._maximize_M(model, constraint_object(zeros), + DP._indicator_to_constraints(model)[Y[2]]), + mbm) == [0.0, 0.0] + + # zeros: x == 1 against Y[2] - per-row M values differ! + # Row 1: max(|x[1] - 1|) s.t. 0<=x[1]<=10 = max(9, 1) = 9 + # Row 2: max(|x[2] - 1|) s.t. 0<=x[2]<=5 = max(4, 1) = 4 + @test DP._maximize_M(model, constraint_object(zeros), Vector{DisjunctConstraintRef}( - DP._indicator_to_constraints(model)[Y[2]]), - mbm) == 49 - @test_throws ErrorException DP._maximize_M(model, "odd", + DP._indicator_to_constraints(model)[Y[2]]), + mbm) == [9.0, 4.0] + + @test_throws ErrorException DP._maximize_M(model, "odd", Vector{DisjunctConstraintRef}( - DP._indicator_to_constraints(model)[Y[2]]), + DP._indicator_to_constraints(model)[Y[2]]), mbm) end @@ -144,57 +175,97 @@ function test_reformulate_disjunct_constraint() @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), + @constraint(model, nonpositives, -x in MOI.Nonpositives(2), Disjunct(Y[3])) - @constraint(model, nonnegatives, x in MOI.Nonnegatives(2), + @constraint(model, nonnegatives, x in MOI.Nonnegatives(2), Disjunct(Y[4])) @constraint(model, zeros, -x .+ 1 in MOI.Zeros(2), Disjunct(Y[5])) @disjunction(model, disjunction,[Y[1], Y[2], Y[3], Y[4], Y[5]]) - method = DP._MBM(DP.MBM(HiGHS.Optimizer), JuMP.Model()) + bconref = Dict(Y[i] => binary_variable(Y[i]) for i in 1:5) + + # Test scalar constraints (LessThan, GreaterThan) with scalar M values + method_scalar = DP._MBM(DP.MBM(HiGHS.Optimizer), JuMP.Model()) for i in 1:5 - method.M[Y[i]] = Float64(i) + method_scalar.M[Y[i]] = Float64(i) end - bconref = Dict(Y[i] => binary_variable(Y[i]) for i in 1:5) - reformulated_constraints = [reformulate_disjunct_constraint(model, - constraint_object(constraints), bconref, method) - for constraints in [lessthan, greaterthan, equalto, nonpositives, - nonnegatives, zeros, disjunction]] - @test reformulated_constraints[1][1].func == JuMP.@expression(model, - x[1] - sum(method.M[i] * bconref[i] for i in keys(method.M))) && - reformulated_constraints[1][1].set == MOI.LessThan(1.0) - @test reformulated_constraints[2][1].func == JuMP.@expression(model, - x[1] + sum(method.M[i] * bconref[i] for i in keys(method.M))) && - reformulated_constraints[2][1].set == MOI.GreaterThan(1.0) - @test reformulated_constraints[3][1].func == JuMP.@expression(model, - x[1] + sum(method.M[i] * bconref[i] for i in keys(method.M))) && - reformulated_constraints[3][1].set == MOI.GreaterThan(1.0) - @test reformulated_constraints[3][2].func == JuMP.@expression(model, - x[1] - sum(method.M[i] * bconref[i] for i in keys(method.M))) && - reformulated_constraints[3][2].set == MOI.LessThan(1.0) - @test reformulated_constraints[4][1].func == JuMP.@expression(model, - -x .- sum(method.M[i] * bconref[i] for i in keys(method.M))) && - reformulated_constraints[4][1].set == MOI.Nonpositives(2) - @test reformulated_constraints[5][1].func == JuMP.@expression(model, - x .+ sum(method.M[i] * bconref[i] for i in keys(method.M))) && - reformulated_constraints[5][1].set == MOI.Nonnegatives(2) - @test reformulated_constraints[6][1].func == JuMP.@expression(model, - -x .+(1 + sum(method.M[i] * bconref[i] for i in keys(method.M)))) && - reformulated_constraints[6][1].set == MOI.Nonnegatives(2) - @test reformulated_constraints[6][2].func == JuMP.@expression(model, - -x .+(1 - sum(method.M[i] * bconref[i] for i in keys(method.M)))) && - reformulated_constraints[6][2].set == MOI.Nonpositives(2) - @test reformulated_constraints[7][1].func == JuMP.@expression(model, - x[1] - 52*bconref[Y[3]] - 53*bconref[Y[4]] - bconref[Y[1]] - - 5*bconref[Y[5]] - 2*bconref[Y[2]]) && - reformulated_constraints[7][1].set == MOI.LessThan(1.0) - @test reformulated_constraints[7][2].func == JuMP.@expression(model, - x[1] + 52*bconref[Y[3]] + 53*bconref[Y[4]] + bconref[Y[1]] - + 5*bconref[Y[5]] + 2*bconref[Y[2]]) && - reformulated_constraints[7][2].set == MOI.GreaterThan(1.0) - - @test_throws ErrorException reformulate_disjunct_constraint(model, - "odd", bconref, method) + ref_lessthan = reformulate_disjunct_constraint( + model, constraint_object(lessthan), bconref, method_scalar) + ref_greaterthan = reformulate_disjunct_constraint( + model, constraint_object(greaterthan), bconref, method_scalar) + @test ref_lessthan[1].func == JuMP.@expression(model, + x[1] - sum(method_scalar.M[i] * bconref[i] + for i in keys(method_scalar.M))) && + ref_lessthan[1].set == MOI.LessThan(1.0) + @test ref_greaterthan[1].func == JuMP.@expression(model, + x[1] + sum(method_scalar.M[i] * bconref[i] + for i in keys(method_scalar.M))) && + ref_greaterthan[1].set == MOI.GreaterThan(1.0) + # Test bidirectional constraint (EqualTo) with [M_lower, M_upper] values + method_equalto = DP._MBM(DP.MBM(HiGHS.Optimizer), JuMP.Model()) + for i in 1:5 + method_equalto.M[Y[i]] = [Float64(i), Float64(i)] + end + ref_equalto = reformulate_disjunct_constraint( + model, constraint_object(equalto), bconref, method_equalto) + @test ref_equalto[1].func == JuMP.@expression(model, + x[1] + sum(method_equalto.M[i][1] * bconref[i] + for i in keys(method_equalto.M))) && + ref_equalto[1].set == MOI.GreaterThan(1.0) + @test ref_equalto[2].func == JuMP.@expression(model, + x[1] - sum(method_equalto.M[i][2] * bconref[i] + for i in keys(method_equalto.M))) && + ref_equalto[2].set == MOI.LessThan(1.0) + + # Test vector constraints with per-row M values [M_row1, M_row2] + method_vector = DP._MBM(DP.MBM(HiGHS.Optimizer), JuMP.Model()) + for i in 1:5 + method_vector.M[Y[i]] = [Float64(i), Float64(i)] + end + ref_nonpositives = reformulate_disjunct_constraint( + model, constraint_object(nonpositives), bconref, method_vector) + ref_nonnegatives = reformulate_disjunct_constraint( + model, constraint_object(nonnegatives), bconref, method_vector) + ref_zeros = reformulate_disjunct_constraint( + model, constraint_object(zeros), bconref, method_vector) + @test ref_nonpositives[1].func == JuMP.@expression(model, [j=1:2], + -x[j] - sum(method_vector.M[i][j] * bconref[i] + for i in keys(method_vector.M))) && + ref_nonpositives[1].set == MOI.Nonpositives(2) + @test ref_nonnegatives[1].func == JuMP.@expression(model, [j=1:2], + x[j] + sum(method_vector.M[i][j] * bconref[i] + for i in keys(method_vector.M))) && + ref_nonnegatives[1].set == MOI.Nonnegatives(2) + @test ref_zeros[1].func == JuMP.@expression(model, [j=1:2], + -x[j] + 1 + sum(method_vector.M[i][j] * bconref[i] + for i in keys(method_vector.M))) && + ref_zeros[1].set == MOI.Nonnegatives(2) + @test ref_zeros[2].func == JuMP.@expression(model, [j=1:2], + -x[j] + 1 - sum(method_vector.M[i][j] * bconref[i] + for i in keys(method_vector.M))) && + ref_zeros[2].set == MOI.Nonpositives(2) + + # Test nested disjunction reformulation with proper nested structure + # Create outer disjunct with inner disjunction + model2 = GDPModel() + @variable(model2, 0 <= z <= 50) + @variable(model2, Outer[1:2], Logical) + @variable(model2, Inner[1:2], Logical) + @constraint(model2, inner_lt, z <= 1, Disjunct(Inner[1])) + @constraint(model2, inner_gt, z >= 1, Disjunct(Inner[2])) + @disjunction(model2, inner_disj, Inner) + method_nested = DP._MBM(DP.MBM(HiGHS.Optimizer), JuMP.Model()) + bconref2 = Dict(Outer[2] => binary_variable(Outer[2])) + method_nested.M[Outer[2]] = 10.0 #Dummy M value for testing. + #Normally _reformulate_disjunct will this without having to assign a value + ref_disjunction = reformulate_disjunct_constraint( + model2, constraint_object(inner_disj), bconref2, method_nested) + @test length(ref_disjunction) >= 2 + @test JuMP.coefficient(ref_disjunction[1].func, z) == 1.0 + @test JuMP.coefficient(ref_disjunction[2].func, z) == 1.0 + + @test_throws ErrorException reformulate_disjunct_constraint(model, + "odd", bconref, method_scalar) end function test_reformulate_disjunct() @@ -223,11 +294,12 @@ function test_reformulate_disjunct() @test JuMP.coefficient(func_1, x[1]) == 1.0 @test JuMP.coefficient(func_1, binary_variable(Y[2])) == -1.5 + # Per-bound M: lower bound uses M_lower=1.5, upper bound uses M_upper=2.5 @test JuMP.coefficient(func_2, x[1]) == 1.0 - @test JuMP.coefficient(func_2, binary_variable(Y[1])) == 2.5 + @test JuMP.coefficient(func_2, binary_variable(Y[1])) == 1.5 # M_lower @test JuMP.coefficient(func_3, x[1]) == 1.0 - @test JuMP.coefficient(func_3, binary_variable(Y[1])) == -2.5 + @test JuMP.coefficient(func_3, binary_variable(Y[1])) == -2.5 # -M_upper end function test_reformulate_disjunction() @@ -238,36 +310,44 @@ function test_reformulate_disjunction() @constraint(model, greaterthan, x >= 1, Disjunct(Y[1])) @constraint(model, interval, 0 <= x <= 55, Disjunct(Y[2])) disj = disjunction(model, [Y[1], Y[2]]) - + method = DP.MBM(HiGHS.Optimizer) ref_cons = reformulate_disjunction(model, constraint_object(disj), method) @test length(ref_cons) == 4 @test ref_cons[1].set == MOI.LessThan(2.0) - + @test ref_cons[2].set == MOI.GreaterThan(1.0) - + @test ref_cons[3].set == MOI.GreaterThan(0.0) - + @test ref_cons[4].set == MOI.LessThan(55.0) - func_1 = ref_cons[1].func # x - 53 Y[2] <= 2.0 - func_2 = ref_cons[2].func # x + 53 Y[2] >= 1.0 - func_3 = ref_cons[3].func # x - Y[1] >= 0.0 - func_4 = ref_cons[4].func # x + Y[1] <= 55.0 + # Per-constraint, per-bound M values: + # - lessthan (x <= 2) in Y[2] region (0 <= x <= 55): max(x-2) at + # x=55 → M=53 + # - greaterthan (x >= 1) in Y[2] region: max(1-x) at x=0 → M=1 + # - interval in Y[1] region (1 <= x <= 2): + # - M_lower (x >= 0): max(0-x) at x=1 → M_lower=-1 + # - M_upper (x <= 55): max(x-55) at x=2 → M_upper=-53 + func_1 = ref_cons[1].func # x - 53*Y[2] <= 2.0 + func_2 = ref_cons[2].func # x + 1*Y[2] >= 1.0 + func_3 = ref_cons[3].func # x + M_lower*Y[1] >= 0.0 → x + (-1)*Y[1] >= 0 + func_4 = ref_cons[4].func # x - M_upper*Y[1] <= 55 → x - (-53)*Y[1] <= 55 @test JuMP.coefficient(func_1, x) == 1.0 @test JuMP.coefficient(func_1, binary_variable(Y[2])) == -53.0 @test JuMP.coefficient(func_2, x) == 1.0 - @test JuMP.coefficient(func_2, binary_variable(Y[2])) == 53.0 + @test JuMP.coefficient(func_2, binary_variable(Y[2])) == 1.0 @test JuMP.coefficient(func_3, x) == 1.0 @test JuMP.coefficient(func_3, binary_variable(Y[1])) == -1.0 @test JuMP.coefficient(func_4, x) == 1.0 - @test JuMP.coefficient(func_4, binary_variable(Y[1])) == 1.0 + # -M_upper = -(-53) = 53 + @test JuMP.coefficient(func_4, binary_variable(Y[1])) == 53.0 end @testset "MBM" begin