From c222384a2a887cc7660b3e7f2aac6b76d1b5e97c Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Thu, 3 Jul 2025 17:06:10 -0400 Subject: [PATCH 01/39] Initial commit adding psplit datatype for methods (similar to BigM and Hull) --- src/DisjunctiveProgramming.jl | 1 + src/datatypes.jl | 19 +++++++++++ src/psplit.jl | 61 +++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 src/psplit.jl diff --git a/src/DisjunctiveProgramming.jl b/src/DisjunctiveProgramming.jl index ae9892e..e1c3edb 100644 --- a/src/DisjunctiveProgramming.jl +++ b/src/DisjunctiveProgramming.jl @@ -24,6 +24,7 @@ include("bigm.jl") include("hull.jl") include("indicator.jl") include("print.jl") +include("psplit.jl") # Define additional stuff that should not be exported const _EXCLUDE_SYMBOLS = [Symbol(@__MODULE__), :eval, :include] diff --git a/src/datatypes.jl b/src/datatypes.jl index ffc9bd4..471a7cc 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -383,6 +383,25 @@ struct Hull{T} <: AbstractReformulationMethod end end +""" + PSplit{T} <: AbstractReformulationMethod + +A type for using the p-split reformulation approach for disjunctive +constraints. + +**Fields** +- `value::T`: epsilon value for p-split reformulations (default = `1e-6`). +- `partition::Vector{Vector{V}}`: The partition of variables +""" +struct PSplit{V <: JuMP.AbstractVariableRef, T} <: AbstractReformulationMethod + value::T + partition::Vector{Vector{V}} + + function PSplit(partition::Vector{Vector{V}}; ϵ::T=1e-6) where {T<:Real, V <: JuMP.AbstractVariableRef} + new{V, T}(ϵ, partition) + end +end + # temp struct to store variable disaggregations (reset for each disjunction) mutable struct _Hull{V <: JuMP.AbstractVariableRef, T} <: AbstractReformulationMethod value::T diff --git a/src/psplit.jl b/src/psplit.jl new file mode 100644 index 0000000..4a8acb5 --- /dev/null +++ b/src/psplit.jl @@ -0,0 +1,61 @@ +#Extending reformulate_disjunction in order to get all possible variables in the disjuncts + +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.ScalarConstraint{T, S}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + method::PSplit +) where {T, S <: _MOI.LessThan} + + reform_con = Vector{JuMP.AbstractConstraint}(undef, length(method.partition)) + v = [@variable(model) for _ in 1:length(method.partition)] + + for i in 1:length(method.partition) + reform_con[i] = JuMP.build_constraint(error, build_partitioned_expression(con.func, method.partition[i]) - v[i], MOI.EqualTo(0.0)) + println(reform_con[i].func) + end + b = @constraint(model,sum(v[i] for i in 1:length(v)) <= con.set.upper * bvref) + @constraint(model, [i=1:length(v)],v[i] >= -8) + println(b) + # print(model) + return reform_con +end + +function build_partitioned_expression( + expr::JuMP.GenericAffExpr, + partition_variables::Vector{JuMP.VariableRef}, +) + new_affexpr = AffExpr(0.0, Dict{JuMP.VariableRef,Float64}()) + for var in partition_variables + add_to_expression!(new_affexpr, coefficient(expr, var), var) + end + + return new_affexpr +end + +function build_partitioned_expression( + expr::JuMP.GenericQuadExpr, + partition_variables::Vector{JuMP.VariableRef}, +) + + new_quadexpr = QuadExpr(0.0, Dict{JuMP.VariableRef,Float64}()) + for var in partition_variables + add_to_expression!(new_quadexpr, get(expr.terms, JuMP.UnorderedPair(var, var), 0.0), var,var) + add_to_expression!(new_quadexpr, coefficient(expr, var), var) + end + + return new_quadexpr +end + +# function bound_auxiliary_variables( +# expr::JuMP.GenericAffExpr, +# aux_var::JuMP.VariableRef, +# method::PSplit) + +# bound_problem = Model(Gurobi.Optimizer) + + + + +# end + \ No newline at end of file From a969de47dd55c749250e9a5cfa31fc9646469805 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Thu, 3 Jul 2025 18:08:19 -0400 Subject: [PATCH 02/39] psplit.jl working for quad and affine expressions inside of constraints. --- src/psplit.jl | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/psplit.jl b/src/psplit.jl index 4a8acb5..4c9b6cc 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -7,17 +7,13 @@ function reformulate_disjunct_constraint( method::PSplit ) where {T, S <: _MOI.LessThan} - reform_con = Vector{JuMP.AbstractConstraint}(undef, length(method.partition)) + reform_con = Vector{JuMP.AbstractConstraint}(undef, length(method.partition) + 1) v = [@variable(model) for _ in 1:length(method.partition)] for i in 1:length(method.partition) - reform_con[i] = JuMP.build_constraint(error, build_partitioned_expression(con.func, method.partition[i]) - v[i], MOI.EqualTo(0.0)) - println(reform_con[i].func) + reform_con[i] = JuMP.build_constraint(error, build_partitioned_expression(con.func, method.partition[i]) - v[i], MOI.LessThan(0.0)) end - b = @constraint(model,sum(v[i] for i in 1:length(v)) <= con.set.upper * bvref) - @constraint(model, [i=1:length(v)],v[i] >= -8) - println(b) - # print(model) + reform_con[end] = JuMP.build_constraint(error, sum(v[i] * bvref for i in 1:length(v)) - con.set.upper * bvref, MOI.LessThan(0.0)) return reform_con end From fff06f274fd41ecf4a6b3f556b00dbdeecf31155 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Fri, 4 Jul 2025 14:09:54 -0400 Subject: [PATCH 03/39] Initial nonlinear dispatch for _build_partitioned_expression --- src/psplit.jl | 85 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 15 deletions(-) diff --git a/src/psplit.jl b/src/psplit.jl index 4c9b6cc..b813ad5 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -1,5 +1,9 @@ #Extending reformulate_disjunction in order to get all possible variables in the disjuncts - +#TODO: Dispatch over Nonlinear expressions +#TODO: Create function to handle bounds of the auxiliary variables (involves solving max/min problem with just variable constraints) +#TODO: Verify we can handle multiple constraints per disjunct (It can for quadratic/linears) +#TODO: Detect nonseperable constraints and throw error +#TODO: Test nonlinear stuff function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, @@ -11,15 +15,15 @@ function reformulate_disjunct_constraint( v = [@variable(model) for _ in 1:length(method.partition)] for i in 1:length(method.partition) - reform_con[i] = JuMP.build_constraint(error, build_partitioned_expression(con.func, method.partition[i]) - v[i], MOI.LessThan(0.0)) + reform_con[i] = JuMP.build_constraint(error, _build_partitioned_expression(con.func, method.partition[i]) - v[i], MOI.LessThan(0.0)) end reform_con[end] = JuMP.build_constraint(error, sum(v[i] * bvref for i in 1:length(v)) - con.set.upper * bvref, MOI.LessThan(0.0)) return reform_con end -function build_partitioned_expression( +function _build_partitioned_expression( expr::JuMP.GenericAffExpr, - partition_variables::Vector{JuMP.VariableRef}, + partition_variables::Vector{JuMP.VariableRef} ) new_affexpr = AffExpr(0.0, Dict{JuMP.VariableRef,Float64}()) for var in partition_variables @@ -29,9 +33,9 @@ function build_partitioned_expression( return new_affexpr end -function build_partitioned_expression( +function _build_partitioned_expression( expr::JuMP.GenericQuadExpr, - partition_variables::Vector{JuMP.VariableRef}, + partition_variables::Vector{JuMP.VariableRef} ) new_quadexpr = QuadExpr(0.0, Dict{JuMP.VariableRef,Float64}()) @@ -43,15 +47,66 @@ function build_partitioned_expression( return new_quadexpr end -# function bound_auxiliary_variables( -# expr::JuMP.GenericAffExpr, -# aux_var::JuMP.VariableRef, -# method::PSplit) - -# bound_problem = Model(Gurobi.Optimizer) - +function _build_partitioned_expression( + expr::JuMP.VariableRef, + partition_variables::Vector{JuMP.VariableRef} +) + if expr in partition_variables + return expr + else + return 0 + end +end +function _build_partitioned_expression( + expr::Number, + partition_variables::Vector{JuMP.VariableRef} +) + return expr +end - +# function replace_variables_in_constraint(fun::NonlinearExpr, var_map::Dict{VariableRef, VariableRef}) +# new_args = Any[replace_variables_in_constraint(arg, var_map) for arg in fun.args] +# return JuMP.NonlinearExpr(fun.head, new_args) # end - \ No newline at end of file + + +function contains_only_partition_variables( + expr::JuMP.VariableRef, + partition_variables::Vector{JuMP.VariableRef} +) + return expr in partition_variables +end + +function contains_only_partition_variables( + expr::Number, + partition_variables::Vector{JuMP.VariableRef} +) + return true +end + +#Helper functions for the nonlinear case. +function contains_only_partition_variables( + expr::JuMP.NonlinearExpr, + partition_variables::Vector{JuMP.VariableRef} +) + return all(contains_only_partition_variables(arg, partition_variables) for arg in expr.args) +end + +function _build_partitioned_expression( + expr::JuMP.NonlinearExpr, + partition_variables::Vector{JuMP.VariableRef} +) + if expr.head == :+ + return JuMP.NonlinearExpr( + :+, + (_build_partitioned_expression(arg, partition_variables) for arg in expr.args)... + ) + end + + if contains_only_partition_variables(expr, partition_variables) + return expr + else + return 0 + end +end \ No newline at end of file From 927019df18aad7b9f10a92a098668ddfca47ceef Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Tue, 8 Jul 2025 16:02:26 -0400 Subject: [PATCH 04/39] More dispatch of reformulate_disjunct_constraint --- src/psplit.jl | 105 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 81 insertions(+), 24 deletions(-) diff --git a/src/psplit.jl b/src/psplit.jl index b813ad5..9cff389 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -1,25 +1,10 @@ #Extending reformulate_disjunction in order to get all possible variables in the disjuncts -#TODO: Dispatch over Nonlinear expressions +#TODO: TEST Dispatch over Nonlinear expressions +#TODO*: Dispatch over GreaterThan, EqualTo, Interval, Nonnegatives, Nonpositives, Zeros #TODO: Create function to handle bounds of the auxiliary variables (involves solving max/min problem with just variable constraints) #TODO: Verify we can handle multiple constraints per disjunct (It can for quadratic/linears) #TODO: Detect nonseperable constraints and throw error #TODO: Test nonlinear stuff -function reformulate_disjunct_constraint( - model::JuMP.AbstractModel, - con::JuMP.ScalarConstraint{T, S}, - bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, - method::PSplit -) where {T, S <: _MOI.LessThan} - - reform_con = Vector{JuMP.AbstractConstraint}(undef, length(method.partition) + 1) - v = [@variable(model) for _ in 1:length(method.partition)] - - for i in 1:length(method.partition) - reform_con[i] = JuMP.build_constraint(error, _build_partitioned_expression(con.func, method.partition[i]) - v[i], MOI.LessThan(0.0)) - end - reform_con[end] = JuMP.build_constraint(error, sum(v[i] * bvref for i in 1:length(v)) - con.set.upper * bvref, MOI.LessThan(0.0)) - return reform_con -end function _build_partitioned_expression( expr::JuMP.GenericAffExpr, @@ -65,12 +50,6 @@ function _build_partitioned_expression( return expr end -# function replace_variables_in_constraint(fun::NonlinearExpr, var_map::Dict{VariableRef, VariableRef}) -# new_args = Any[replace_variables_in_constraint(arg, var_map) for arg in fun.args] -# return JuMP.NonlinearExpr(fun.head, new_args) -# end - - function contains_only_partition_variables( expr::JuMP.VariableRef, partition_variables::Vector{JuMP.VariableRef} @@ -109,4 +88,82 @@ function _build_partitioned_expression( else return 0 end -end \ No newline at end of file +end + +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.ScalarConstraint{T, S}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + method::PSplit +) where {T, S <: _MOI.LessThan} + p = length(method.partition) + v = [@variable(model) for _ in 1:p] + reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) + + reform_con[1:p] = [ + JuMP.build_constraint(error, _build_partitioned_expression(con.func, method.partition[i]) - v[i], MOI.LessThan(0.0)) for i in 1:p + ] + reform_con[end] = JuMP.build_constraint(error, sum(v[i] * bvref for i in 1:p) - con.set.upper * bvref, MOI.LessThan(0.0)) + + return reform_con +end + +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.ScalarConstraint{T, S}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + method::PSplit +) where {T, S <: _MOI.GreaterThan} + p = length(method.partition) + reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) + v = [@variable(model) for _ in 1:p] + + reform_con[1:p] = [ + JuMP.build_constraint(error, -_build_partitioned_expression(con.func, method.partition[i]) + v[i], MOI.LessThan(0.0)) for i in 1:p + ] + reform_con[end] = JuMP.build_constraint(error, -sum(v[i] * bvref for i in 1:p) + con.set.lower * bvref, MOI.LessThan(0.0)) + return reform_con +end + +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.ScalarConstraint{T, S}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + method::PSplit +) where {T, S <: Union{_MOI.Interval, _MOI.EqualTo}} + p = length(method.partition) + reform_con_lt = Vector{JuMP.AbstractConstraint}(undef, p + 1) + reform_con_gt = Vector{JuMP.AbstractConstraint}(undef, p + 1) + #let [_, 1] be the lower bound and [_, 2] be the upper bound + v = @variable(model, [1:p, 1:2]) + reform_con_lt[1:p] = [ + JuMP.build_constraint(error, _build_partitioned_expression(con.func, method.partition[i]) - v[i,1], MOI.LessThan(0.0)) + for i in 1:length(method.partition) + ] + reform_con_gt[1:length(method.partition)] = [ + JuMP.build_constraint(error, -_build_partitioned_expression(con.func, method.partition[i]) + v[i,2], MOI.LessThan(0.0)) + for i in 1:length(method.partition) + ] + set_values = _set_values(con.set) + reform_con_lt[end] = JuMP.build_constraint(error, sum(v[i,1] * bvref for i in 1:p) - set_values[2] * bvref, MOI.LessThan(0.0)) + reform_con_gt[end] = JuMP.build_constraint(error, -sum(v[i,2] * bvref for i in 1:p) + set_values[1] * bvref, MOI.LessThan(0.0)) + return vcat(reform_con_lt, reform_con_gt) +end +#TODO: how do i avoid the vcat? +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.VectorConstraint{T, S, R}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + method::PSplit +) where {T, S <: _MOI.Nonpositives, R} + #TODO: Implement this + # p = length(method.partition) + # v = @variable(model, [1:p, 1:con.set.dimension]) + # reform_con = Vector{JuMP.AbstractConstraint}(undef, (p + 1) , con.set.dimension) + + # for i in 1:p + # reform_con[i] = JuMP.build_constraint(error, _build_partitioned_expression(con.func[i], method.partition[i]) - v[i,:], con.set) + # end + # reform_con[end, :] = JuMP.build_constraint(error, sum(v[i,:] * bvref for i in 1:p) - con.set.upper * bvref, con.set) + # return vcat(reform_con) +end From 8b1da77218763202ecf86bb726928039a3df932d Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Fri, 11 Jul 2025 11:43:30 -0400 Subject: [PATCH 05/39] Vector dispatch --- src/psplit.jl | 53 +++++++++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/src/psplit.jl b/src/psplit.jl index 9cff389..68fbecb 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -1,8 +1,7 @@ #Extending reformulate_disjunction in order to get all possible variables in the disjuncts + #TODO: TEST Dispatch over Nonlinear expressions -#TODO*: Dispatch over GreaterThan, EqualTo, Interval, Nonnegatives, Nonpositives, Zeros #TODO: Create function to handle bounds of the auxiliary variables (involves solving max/min problem with just variable constraints) -#TODO: Verify we can handle multiple constraints per disjunct (It can for quadratic/linears) #TODO: Detect nonseperable constraints and throw error #TODO: Test nonlinear stuff @@ -64,14 +63,23 @@ function contains_only_partition_variables( return true end +# function contains_only_partition_variables( +# expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr}, +# partition_variables::Vector{JuMP.VariableRef} + +# return all(contains_only_partition_variables(arg, partition_variables) for arg in expr.terms) +# end + #Helper functions for the nonlinear case. function contains_only_partition_variables( - expr::JuMP.NonlinearExpr, + expr::Union{JuMP.NonlinearExpr}, partition_variables::Vector{JuMP.VariableRef} ) return all(contains_only_partition_variables(arg, partition_variables) for arg in expr.args) end + + function _build_partitioned_expression( expr::JuMP.NonlinearExpr, partition_variables::Vector{JuMP.VariableRef} @@ -136,34 +144,33 @@ function reformulate_disjunct_constraint( reform_con_gt = Vector{JuMP.AbstractConstraint}(undef, p + 1) #let [_, 1] be the lower bound and [_, 2] be the upper bound v = @variable(model, [1:p, 1:2]) - reform_con_lt[1:p] = [ - JuMP.build_constraint(error, _build_partitioned_expression(con.func, method.partition[i]) - v[i,1], MOI.LessThan(0.0)) - for i in 1:length(method.partition) - ] - reform_con_gt[1:length(method.partition)] = [ - JuMP.build_constraint(error, -_build_partitioned_expression(con.func, method.partition[i]) + v[i,2], MOI.LessThan(0.0)) - for i in 1:length(method.partition) - ] + for i in 1:p + reform_con_lt[i] = JuMP.build_constraint(error, _build_partitioned_expression(con.func, method.partition[i]) - v[i,1], MOI.LessThan(0.0)) + reform_con_gt[i] = JuMP.build_constraint(error, -_build_partitioned_expression(con.func, method.partition[i]) + v[i,2], MOI.LessThan(0.0)) + end set_values = _set_values(con.set) reform_con_lt[end] = JuMP.build_constraint(error, sum(v[i,1] * bvref for i in 1:p) - set_values[2] * bvref, MOI.LessThan(0.0)) reform_con_gt[end] = JuMP.build_constraint(error, -sum(v[i,2] * bvref for i in 1:p) + set_values[1] * bvref, MOI.LessThan(0.0)) + #TODO: how do i avoid the vcat? return vcat(reform_con_lt, reform_con_gt) end #TODO: how do i avoid the vcat? + function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::PSplit -) where {T, S <: _MOI.Nonpositives, R} - #TODO: Implement this - # p = length(method.partition) - # v = @variable(model, [1:p, 1:con.set.dimension]) - # reform_con = Vector{JuMP.AbstractConstraint}(undef, (p + 1) , con.set.dimension) - - # for i in 1:p - # reform_con[i] = JuMP.build_constraint(error, _build_partitioned_expression(con.func[i], method.partition[i]) - v[i,:], con.set) - # end - # reform_con[end, :] = JuMP.build_constraint(error, sum(v[i,:] * bvref for i in 1:p) - con.set.upper * bvref, con.set) - # return vcat(reform_con) -end +) where {T, S <: Union{_MOI.Nonpositives,_MOI.Nonnegatives, _MOI.Zeros}, R} + p = length(method.partition) + d = con.set.dimension + v = @variable(model, [1:p,1:d]) + reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) + for i in 1:p + new_func = JuMP.@expression(model, [j = 1:d], _build_partitioned_expression(con.func[j], method.partition[i]) - v[i,j]) + reform_con[i] = JuMP.build_constraint(error, new_func, con.set) + end + new_func = JuMP.@expression(model,[j = 1:d], bvref*sum(v[i,j] for i in 1:p) + JuMP.constant(con.func[j])*bvref) + reform_con[end] = JuMP.build_constraint(error, new_func, con.set) + return vcat(reform_con) +end \ No newline at end of file From 34cec197ff7c9e974bdd6426aab7d945923eaf04 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Tue, 15 Jul 2025 12:57:24 -0400 Subject: [PATCH 06/39] Only NLE is WIP --- src/psplit.jl | 115 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 90 insertions(+), 25 deletions(-) diff --git a/src/psplit.jl b/src/psplit.jl index 68fbecb..7d70cb5 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -1,9 +1,7 @@ -#Extending reformulate_disjunction in order to get all possible variables in the disjuncts - -#TODO: TEST Dispatch over Nonlinear expressions -#TODO: Create function to handle bounds of the auxiliary variables (involves solving max/min problem with just variable constraints) #TODO: Detect nonseperable constraints and throw error #TODO: Test nonlinear stuff +#TODO: For Nonlinear functions, the RHS is NOT the constant. Therefore the constant must be located and moved to the RHS or alternative reformulation. + function _build_partitioned_expression( expr::JuMP.GenericAffExpr, @@ -63,13 +61,6 @@ function contains_only_partition_variables( return true end -# function contains_only_partition_variables( -# expr::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr}, -# partition_variables::Vector{JuMP.VariableRef} - -# return all(contains_only_partition_variables(arg, partition_variables) for arg in expr.terms) -# end - #Helper functions for the nonlinear case. function contains_only_partition_variables( expr::Union{JuMP.NonlinearExpr}, @@ -84,9 +75,9 @@ function _build_partitioned_expression( expr::JuMP.NonlinearExpr, partition_variables::Vector{JuMP.VariableRef} ) - if expr.head == :+ + if expr.head in (:+, :-) return JuMP.NonlinearExpr( - :+, + expr.head, (_build_partitioned_expression(arg, partition_variables) for arg in expr.args)... ) end @@ -98,6 +89,69 @@ function _build_partitioned_expression( end end +function _bound_auxiliary( + model::JuMP.AbstractModel, + v::JuMP.VariableRef, + func::JuMP.GenericAffExpr, + method::PSplit +) + lower_bound = 0 + upper_bound = 0 + for (var, coeff) in func.terms + if var != v + JuMP.is_binary(var) && continue + if coeff > 0 + lower_bound += coeff * variable_bound_info(var)[1] + upper_bound += coeff * variable_bound_info(var)[2] + else + lower_bound += coeff * variable_bound_info(var)[2] + upper_bound += coeff * variable_bound_info(var)[1] + end + end + end + JuMP.set_lower_bound(v, lower_bound) + JuMP.set_upper_bound(v, upper_bound) +end + +function _bound_auxiliary( + model::JuMP.AbstractModel, + v::JuMP.VariableRef, + func::JuMP.VariableRef, + method::PSplit +) + if func != v + lower_bound = variable_bound_info(func)[1] + upper_bound = variable_bound_info(func)[2] + JuMP.set_lower_bound(v, lower_bound) + JuMP.set_upper_bound(v, upper_bound) + else + JuMP.set_lower_bound(v,0) + JuMP.set_upper_bound(v,0) + end +end + +function _bound_auxiliary( + model::JuMP.AbstractModel, + v::JuMP.VariableRef, + func::Union{JuMP.NonlinearExpr, JuMP.QuadExpr, Number}, + method::PSplit +) + +end + +requires_variable_bound_info(method::PSplit) = true + +function set_variable_bound_info(vref::JuMP.AbstractVariableRef, ::PSplit) + if !has_lower_bound(vref) || !has_upper_bound(vref) + error("Variable $vref must have both lower and upper bounds defined when using the PSplit reformulation.") + else + lb = min(0, lower_bound(vref)) + ub = max(0, upper_bound(vref)) + end + return lb, ub +end + + function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, @@ -107,10 +161,12 @@ function reformulate_disjunct_constraint( p = length(method.partition) v = [@variable(model) for _ in 1:p] reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) - - reform_con[1:p] = [ - JuMP.build_constraint(error, _build_partitioned_expression(con.func, method.partition[i]) - v[i], MOI.LessThan(0.0)) for i in 1:p - ] + for i in 1:p + func = _build_partitioned_expression(con.func, method.partition[i]) + reform_con[i] = JuMP.build_constraint(error, func - v[i], MOI.LessThan(0.0)) + _bound_auxiliary(model, v[i], func, method) + println(reform_con[i]) + end reform_con[end] = JuMP.build_constraint(error, sum(v[i] * bvref for i in 1:p) - con.set.upper * bvref, MOI.LessThan(0.0)) return reform_con @@ -126,9 +182,12 @@ function reformulate_disjunct_constraint( reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) v = [@variable(model) for _ in 1:p] - reform_con[1:p] = [ - JuMP.build_constraint(error, -_build_partitioned_expression(con.func, method.partition[i]) + v[i], MOI.LessThan(0.0)) for i in 1:p - ] + for i in 1:p + func = -_build_partitioned_expression(con.func, method.partition[i]) + reform_con[i] = JuMP.build_constraint(error, func + v[i], MOI.LessThan(0.0)) + _bound_auxiliary(model, v[i], func, method) + println(reform_con[i]) + end reform_con[end] = JuMP.build_constraint(error, -sum(v[i] * bvref for i in 1:p) + con.set.lower * bvref, MOI.LessThan(0.0)) return reform_con end @@ -145,8 +204,11 @@ function reformulate_disjunct_constraint( #let [_, 1] be the lower bound and [_, 2] be the upper bound v = @variable(model, [1:p, 1:2]) for i in 1:p - reform_con_lt[i] = JuMP.build_constraint(error, _build_partitioned_expression(con.func, method.partition[i]) - v[i,1], MOI.LessThan(0.0)) - reform_con_gt[i] = JuMP.build_constraint(error, -_build_partitioned_expression(con.func, method.partition[i]) + v[i,2], MOI.LessThan(0.0)) + func = _build_partitioned_expression(con.func, method.partition[i]) + reform_con_lt[i] = JuMP.build_constraint(error, func - v[i,1], MOI.LessThan(0.0)) + reform_con_gt[i] = JuMP.build_constraint(error, -func + v[i,2], MOI.LessThan(0.0)) + _bound_auxiliary(model, v[i,1], func, method) + _bound_auxiliary(model, v[i,2], func, method) end set_values = _set_values(con.set) reform_con_lt[end] = JuMP.build_constraint(error, sum(v[i,1] * bvref for i in 1:p) - set_values[2] * bvref, MOI.LessThan(0.0)) @@ -167,10 +229,13 @@ function reformulate_disjunct_constraint( v = @variable(model, [1:p,1:d]) reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) for i in 1:p - new_func = JuMP.@expression(model, [j = 1:d], _build_partitioned_expression(con.func[j], method.partition[i]) - v[i,j]) - reform_con[i] = JuMP.build_constraint(error, new_func, con.set) + func = JuMP.@expression(model, [j = 1:d], _build_partitioned_expression(con.func[j], method.partition[i]) - v[i,j]) + reform_con[i] = JuMP.build_constraint(error, func, con.set) + for j in 1:d + _bound_auxiliary(model, v[i,j], func[j], method) + end end - new_func = JuMP.@expression(model,[j = 1:d], bvref*sum(v[i,j] for i in 1:p) + JuMP.constant(con.func[j])*bvref) + new_func = JuMP.@expression(model,[j = 1:d], sum(v[i,j] * bvref for i in 1:p) + JuMP.constant(con.func[j])*bvref) reform_con[end] = JuMP.build_constraint(error, new_func, con.set) return vcat(reform_con) end \ No newline at end of file From 70c57a86eb1a2bc2e7ec1ef454a5c56b6b92fc41 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Tue, 15 Jul 2025 15:26:37 -0400 Subject: [PATCH 07/39] Nonlinear works for less than constraints --- src/psplit.jl | 97 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 63 insertions(+), 34 deletions(-) diff --git a/src/psplit.jl b/src/psplit.jl index 7d70cb5..c3f8ff1 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -1,23 +1,23 @@ #TODO: Detect nonseperable constraints and throw error -#TODO: Test nonlinear stuff -#TODO: For Nonlinear functions, the RHS is NOT the constant. Therefore the constant must be located and moved to the RHS or alternative reformulation. - +#TODO: Make NL work for all other constraints (LessThan works at the moment.) function _build_partitioned_expression( expr::JuMP.GenericAffExpr, - partition_variables::Vector{JuMP.VariableRef} + partition_variables::Vector{JuMP.VariableRef}, + ::JuMP.ScalarConstraint ) new_affexpr = AffExpr(0.0, Dict{JuMP.VariableRef,Float64}()) for var in partition_variables add_to_expression!(new_affexpr, coefficient(expr, var), var) end - return new_affexpr + return new_affexpr, 0 end function _build_partitioned_expression( expr::JuMP.GenericQuadExpr, - partition_variables::Vector{JuMP.VariableRef} + partition_variables::Vector{JuMP.VariableRef}, + ::JuMP.ScalarConstraint ) new_quadexpr = QuadExpr(0.0, Dict{JuMP.VariableRef,Float64}()) @@ -26,25 +26,27 @@ function _build_partitioned_expression( add_to_expression!(new_quadexpr, coefficient(expr, var), var) end - return new_quadexpr + return new_quadexpr, 0 end function _build_partitioned_expression( expr::JuMP.VariableRef, - partition_variables::Vector{JuMP.VariableRef} + partition_variables::Vector{JuMP.VariableRef}, + ::JuMP.ScalarConstraint ) if expr in partition_variables - return expr + return expr, 0 else - return 0 + return 0, 0 end end function _build_partitioned_expression( expr::Number, - partition_variables::Vector{JuMP.VariableRef} -) - return expr + partition_variables::Vector{JuMP.VariableRef}, + ::JuMP.ScalarConstraint + ) + return expr, 0 end function contains_only_partition_variables( @@ -63,25 +65,52 @@ end #Helper functions for the nonlinear case. function contains_only_partition_variables( - expr::Union{JuMP.NonlinearExpr}, + expr::Union{JuMP.NonlinearExpr}, partition_variables::Vector{JuMP.VariableRef} ) return all(contains_only_partition_variables(arg, partition_variables) for arg in expr.args) end - function _build_partitioned_expression( expr::JuMP.NonlinearExpr, - partition_variables::Vector{JuMP.VariableRef} + partition_variables::Vector{JuMP.VariableRef}, + con::JuMP.ScalarConstraint ) + if expr.head in (:+, :-) + rhs = get(filter(x -> isa(x, Number), expr.args), 1, 0.0) + if expr.head == :+ + rhs = -rhs + end + new_func = _nonlinear_recursion(expr, partition_variables, con) + rhs + else + new_func = _nonlinear_recursion(expr, partition_variables, con) + rhs = 0.0 + end + return new_func, rhs +end + +function _nonlinear_recursion( + expr::Union{JuMP.GenericAffExpr, JuMP.VariableRef, JuMP.GenericQuadExpr, Number}, + partition_variables::Vector{JuMP.VariableRef}, + con::JuMP.ScalarConstraint +) + new_func, _ = _build_partitioned_expression(expr, partition_variables, con) + return new_func +end + + +function _nonlinear_recursion( + expr::JuMP.NonlinearExpr, + partition_variables::Vector{JuMP.VariableRef}, + con::JuMP.ScalarConstraint, + ) if expr.head in (:+, :-) return JuMP.NonlinearExpr( expr.head, - (_build_partitioned_expression(arg, partition_variables) for arg in expr.args)... + (_nonlinear_recursion(arg, partition_variables, con) for arg in expr.args)... ) end - if contains_only_partition_variables(expr, partition_variables) return expr else @@ -89,6 +118,7 @@ function _build_partitioned_expression( end end + function _bound_auxiliary( model::JuMP.AbstractModel, v::JuMP.VariableRef, @@ -136,7 +166,6 @@ function _bound_auxiliary( func::Union{JuMP.NonlinearExpr, JuMP.QuadExpr, Number}, method::PSplit ) - end requires_variable_bound_info(method::PSplit) = true @@ -151,7 +180,7 @@ function set_variable_bound_info(vref::JuMP.AbstractVariableRef, ::PSplit) return lb, ub end - +#DONE WITH NL function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, @@ -160,18 +189,17 @@ function reformulate_disjunct_constraint( ) where {T, S <: _MOI.LessThan} p = length(method.partition) v = [@variable(model) for _ in 1:p] + _, rhs = _build_partitioned_expression(con.func, method.partition[p], con) reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) for i in 1:p - func = _build_partitioned_expression(con.func, method.partition[i]) + func, _ = _build_partitioned_expression(con.func, method.partition[i], con) reform_con[i] = JuMP.build_constraint(error, func - v[i], MOI.LessThan(0.0)) _bound_auxiliary(model, v[i], func, method) - println(reform_con[i]) end - reform_con[end] = JuMP.build_constraint(error, sum(v[i] * bvref for i in 1:p) - con.set.upper * bvref, MOI.LessThan(0.0)) - + reform_con[end] = JuMP.build_constraint(error, sum(v[i] * bvref for i in 1:p) - (con.set.upper + rhs) * bvref, MOI.LessThan(0.0)) return reform_con end - +#TODO: Update with NL function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, @@ -181,17 +209,17 @@ function reformulate_disjunct_constraint( p = length(method.partition) reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) v = [@variable(model) for _ in 1:p] + _, rhs = _build_partitioned_expression(con.func, method.partition[p], con) for i in 1:p - func = -_build_partitioned_expression(con.func, method.partition[i]) + func, _ = -_build_partitioned_expression(con.func, method.partition[i], con) reform_con[i] = JuMP.build_constraint(error, func + v[i], MOI.LessThan(0.0)) _bound_auxiliary(model, v[i], func, method) - println(reform_con[i]) end - reform_con[end] = JuMP.build_constraint(error, -sum(v[i] * bvref for i in 1:p) + con.set.lower * bvref, MOI.LessThan(0.0)) + reform_con[end] = JuMP.build_constraint(error, -sum(v[i] * bvref for i in 1:p) + (con.set.lower - rhs) * bvref, MOI.LessThan(0.0)) return reform_con end - +#TODO: Update with NL function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, @@ -202,22 +230,23 @@ function reformulate_disjunct_constraint( reform_con_lt = Vector{JuMP.AbstractConstraint}(undef, p + 1) reform_con_gt = Vector{JuMP.AbstractConstraint}(undef, p + 1) #let [_, 1] be the lower bound and [_, 2] be the upper bound + _, rhs = _build_partitioned_expression(con.func, method.partition[p], con) v = @variable(model, [1:p, 1:2]) for i in 1:p - func = _build_partitioned_expression(con.func, method.partition[i]) + func, _= _build_partitioned_expression(con.func, method.partition[i], con) reform_con_lt[i] = JuMP.build_constraint(error, func - v[i,1], MOI.LessThan(0.0)) reform_con_gt[i] = JuMP.build_constraint(error, -func + v[i,2], MOI.LessThan(0.0)) _bound_auxiliary(model, v[i,1], func, method) _bound_auxiliary(model, v[i,2], func, method) end set_values = _set_values(con.set) - reform_con_lt[end] = JuMP.build_constraint(error, sum(v[i,1] * bvref for i in 1:p) - set_values[2] * bvref, MOI.LessThan(0.0)) - reform_con_gt[end] = JuMP.build_constraint(error, -sum(v[i,2] * bvref for i in 1:p) + set_values[1] * bvref, MOI.LessThan(0.0)) + reform_con_lt[end] = JuMP.build_constraint(error, sum(v[i,1] * bvref for i in 1:p) - (set_values[2] - rhs) * bvref, MOI.LessThan(0.0)) + reform_con_gt[end] = JuMP.build_constraint(error, -sum(v[i,2] * bvref for i in 1:p) + (set_values[1] - rhs) * bvref, MOI.LessThan(0.0)) #TODO: how do i avoid the vcat? return vcat(reform_con_lt, reform_con_gt) end #TODO: how do i avoid the vcat? - +#TODO: Update with NL function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, @@ -229,7 +258,7 @@ function reformulate_disjunct_constraint( v = @variable(model, [1:p,1:d]) reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) for i in 1:p - func = JuMP.@expression(model, [j = 1:d], _build_partitioned_expression(con.func[j], method.partition[i]) - v[i,j]) + func, _ = JuMP.@expression(model, [j = 1:d], _build_partitioned_expression(con.func[j], method.partition[i], con) - v[i,j]) reform_con[i] = JuMP.build_constraint(error, func, con.set) for j in 1:d _bound_auxiliary(model, v[i,j], func[j], method) From 0d9cc9eba4d46877cc900178282b44a648772795 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Wed, 30 Jul 2025 08:07:03 -0400 Subject: [PATCH 08/39] GreaterThan nonlinear works --- src/psplit.jl | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/psplit.jl b/src/psplit.jl index c3f8ff1..6be96a9 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -1,6 +1,5 @@ -#TODO: Detect nonseperable constraints and throw error -#TODO: Make NL work for all other constraints (LessThan works at the moment.) - +#TODO: If the constraint is Nonlinear -> Throw a warning +#TODO: Make NL work for all other constraints (LessThan and GreaterThan works at the moment.) function _build_partitioned_expression( expr::JuMP.GenericAffExpr, partition_variables::Vector{JuMP.VariableRef}, @@ -19,7 +18,6 @@ function _build_partitioned_expression( partition_variables::Vector{JuMP.VariableRef}, ::JuMP.ScalarConstraint ) - new_quadexpr = QuadExpr(0.0, Dict{JuMP.VariableRef,Float64}()) for var in partition_variables add_to_expression!(new_quadexpr, get(expr.terms, JuMP.UnorderedPair(var, var), 0.0), var,var) @@ -90,6 +88,24 @@ function _build_partitioned_expression( return new_func, rhs end +# function _build_partitioned_expression( +# expr::JuMP.NonlinearExpr, +# partition_variables::Vector{JuMP.VariableRef}, +# con::JuMP.ScalarConstraint{T,S} +# ) where {T, S <: _MOI.GreaterThan} +# if expr.head in (:+, :-) +# rhs = get(filter(x -> isa(x, Number), expr.args), 1, 0.0) +# if expr.head == :+ +# rhs = -rhs +# end +# new_func = _nonlinear_recursion(expr, partition_variables, con) + rhs +# else +# new_func = _nonlinear_recursion(expr, partition_variables, con) +# rhs = 0.0 +# end +# return -new_func,-rhs +# end + function _nonlinear_recursion( expr::Union{JuMP.GenericAffExpr, JuMP.VariableRef, JuMP.GenericQuadExpr, Number}, partition_variables::Vector{JuMP.VariableRef}, @@ -199,7 +215,7 @@ function reformulate_disjunct_constraint( reform_con[end] = JuMP.build_constraint(error, sum(v[i] * bvref for i in 1:p) - (con.set.upper + rhs) * bvref, MOI.LessThan(0.0)) return reform_con end -#TODO: Update with NL +#DONE WITH NL function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, @@ -212,11 +228,11 @@ function reformulate_disjunct_constraint( _, rhs = _build_partitioned_expression(con.func, method.partition[p], con) for i in 1:p - func, _ = -_build_partitioned_expression(con.func, method.partition[i], con) + func, _ = (x -> (-x[1], x[2]))(_build_partitioned_expression(con.func, method.partition[i], con)) reform_con[i] = JuMP.build_constraint(error, func + v[i], MOI.LessThan(0.0)) _bound_auxiliary(model, v[i], func, method) end - reform_con[end] = JuMP.build_constraint(error, -sum(v[i] * bvref for i in 1:p) + (con.set.lower - rhs) * bvref, MOI.LessThan(0.0)) + reform_con[end] = JuMP.build_constraint(error, -sum(v[i] * bvref for i in 1:p) + (con.set.lower + rhs) * bvref, MOI.LessThan(0.0)) return reform_con end #TODO: Update with NL From 8fd6bbf72466322bba0ae97f6b66c319b7c92720 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Sun, 10 Aug 2025 17:19:02 -0400 Subject: [PATCH 09/39] Initial test file commit --- src/psplit.jl | 101 +++++++++++++++++++++++++++++++++---- test/constraints/psplit.jl | 21 ++++++++ test/runtests.jl | 31 ++++++------ 3 files changed, 127 insertions(+), 26 deletions(-) create mode 100644 test/constraints/psplit.jl diff --git a/src/psplit.jl b/src/psplit.jl index 6be96a9..1c540fb 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -1,5 +1,4 @@ #TODO: If the constraint is Nonlinear -> Throw a warning -#TODO: Make NL work for all other constraints (LessThan and GreaterThan works at the moment.) function _build_partitioned_expression( expr::JuMP.GenericAffExpr, partition_variables::Vector{JuMP.VariableRef}, @@ -47,6 +46,17 @@ function _build_partitioned_expression( return expr, 0 end +function contains_only_partition_variables( + expr::Union{JuMP.GenericAffExpr,JuMP.GenericQuadExpr}, + partition_variables::Vector{JuMP.VariableRef} +) + for (var, _) in expr.terms + var in partition_variables || return false + end + return true +end + + function contains_only_partition_variables( expr::JuMP.VariableRef, partition_variables::Vector{JuMP.VariableRef} @@ -228,14 +238,15 @@ function reformulate_disjunct_constraint( _, rhs = _build_partitioned_expression(con.func, method.partition[p], con) for i in 1:p - func, _ = (x -> (-x[1], x[2]))(_build_partitioned_expression(con.func, method.partition[i], con)) - reform_con[i] = JuMP.build_constraint(error, func + v[i], MOI.LessThan(0.0)) - _bound_auxiliary(model, v[i], func, method) + func, _ = _build_partitioned_expression(con.func, method.partition[i], con) + reform_con[i] = JuMP.build_constraint(error, -func + v[i], MOI.LessThan(0.0)) + _bound_auxiliary(model, v[i], -func, method) end reform_con[end] = JuMP.build_constraint(error, -sum(v[i] * bvref for i in 1:p) + (con.set.lower + rhs) * bvref, MOI.LessThan(0.0)) return reform_con end -#TODO: Update with NL + +#Works with NL function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, @@ -245,7 +256,7 @@ function reformulate_disjunct_constraint( p = length(method.partition) reform_con_lt = Vector{JuMP.AbstractConstraint}(undef, p + 1) reform_con_gt = Vector{JuMP.AbstractConstraint}(undef, p + 1) - #let [_, 1] be the lower bound and [_, 2] be the upper bound + #let [_, 1] be the upper bound and [_, 2] be the lower bound _, rhs = _build_partitioned_expression(con.func, method.partition[p], con) v = @variable(model, [1:p, 1:2]) for i in 1:p @@ -253,16 +264,14 @@ function reformulate_disjunct_constraint( reform_con_lt[i] = JuMP.build_constraint(error, func - v[i,1], MOI.LessThan(0.0)) reform_con_gt[i] = JuMP.build_constraint(error, -func + v[i,2], MOI.LessThan(0.0)) _bound_auxiliary(model, v[i,1], func, method) - _bound_auxiliary(model, v[i,2], func, method) + _bound_auxiliary(model, v[i,2], -func, method) end set_values = _set_values(con.set) - reform_con_lt[end] = JuMP.build_constraint(error, sum(v[i,1] * bvref for i in 1:p) - (set_values[2] - rhs) * bvref, MOI.LessThan(0.0)) - reform_con_gt[end] = JuMP.build_constraint(error, -sum(v[i,2] * bvref for i in 1:p) + (set_values[1] - rhs) * bvref, MOI.LessThan(0.0)) + reform_con_lt[end] = JuMP.build_constraint(error, sum(v[i,1] * bvref for i in 1:p) - (set_values[2] + rhs) * bvref, MOI.LessThan(0.0)) + reform_con_gt[end] = JuMP.build_constraint(error, -sum(v[i,2] * bvref for i in 1:p) + (set_values[1] + rhs) * bvref, MOI.LessThan(0.0)) #TODO: how do i avoid the vcat? return vcat(reform_con_lt, reform_con_gt) end -#TODO: how do i avoid the vcat? -#TODO: Update with NL function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, @@ -282,5 +291,75 @@ function reformulate_disjunct_constraint( end new_func = JuMP.@expression(model,[j = 1:d], sum(v[i,j] * bvref for i in 1:p) + JuMP.constant(con.func[j])*bvref) reform_con[end] = JuMP.build_constraint(error, new_func, con.set) + #TODO: how do i avoid the vcat? return vcat(reform_con) +end + + +################################################################################ +# FALLBACK WARNING DISPATCHES +################################################################################ + +# Generic fallback for _build_partitioned_expression +function _build_partitioned_expression( + expr::Any, + ::Vector{JuMP.VariableRef}, + ::Any +) + error("PSplit: _build_partitioned_expression not implemented for expression type $(typeof(expr)). Supported types: GenericAffExpr, GenericQuadExpr, VariableRef, Number, NonlinearExpr.") +end + +# Generic fallback for contains_only_partition_variables +function contains_only_partition_variables( + expr::Any, + ::Vector{JuMP.VariableRef} +) + error("PSplit: contains_only_partition_variables not implemented for expression type $(typeof(expr)). Supported types: GenericAffExpr, GenericQuadExpr, VariableRef, Number, NonlinearExpr.") +end + +# Generic fallback for _nonlinear_recursion +function _nonlinear_recursion( + expr::Any, + ::Vector{JuMP.VariableRef}, + ::JuMP.ScalarConstraint +) + error("PSplit: _nonlinear_recursion not implemented for expression type $(typeof(expr)). Supported types: GenericAffExpr, GenericQuadExpr, VariableRef, Number, NonlinearExpr.") +end + +# Generic fallback for _bound_auxiliary +function _bound_auxiliary( + ::JuMP.AbstractModel, + v::JuMP.VariableRef, + func::Any, + ::PSplit +) + @warn "PSplit: _bound_auxiliary not implemented for function type $(typeof(func)). Auxiliary variable bounds may be suboptimal. Supported types: GenericAffExpr, VariableRef." + # Set default bounds to avoid errors + JuMP.set_lower_bound(v, -1e6) + JuMP.set_upper_bound(v, 1e6) +end + +# Generic fallback for reformulate_disjunct_constraint (scalar) +function reformulate_disjunct_constraint( + ::JuMP.AbstractModel, + con::JuMP.ScalarConstraint, + ::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + ::PSplit +) + error("PSplit: reformulate_disjunct_constraint not implemented for constraint set type $(typeof(con.set)). Supported types: LessThan, GreaterThan, EqualTo, Interval.") +end + +# Generic fallback for reformulate_disjunct_constraint (vector) +function reformulate_disjunct_constraint( + ::JuMP.AbstractModel, + con::JuMP.VectorConstraint, + ::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + ::PSplit +) + error("PSplit: reformulate_disjunct_constraint not implemented for vector constraint set type $(typeof(con.set)). Supported types: Zeros, Nonpositives, Nonnegatives.") +end + +# Generic fallback for _set_values +function _set_values(set::Any) + error("PSplit: _set_values not implemented for constraint set type $(typeof(set)). Supported types: EqualTo, Interval.") end \ No newline at end of file diff --git a/test/constraints/psplit.jl b/test/constraints/psplit.jl new file mode 100644 index 0000000..052aabe --- /dev/null +++ b/test/constraints/psplit.jl @@ -0,0 +1,21 @@ +function test_psplit() + model = JuMP.Model() + @variable(model, x[1:4]) + method = PSplit([[x[1], x[2]], [x[3], x[4]]]) + @test method.partition == [[x[1], x[2]], [x[3], x[4]]] + #Throw error when partition isnt set up! +end +#= +TODO: Test Plan + +_build_partitioned_expression: 5 tests +contains_only_partition_variables: 5 tests (2 for union + 3 others) +_nonlinear_recursion: 5 tests (4 for union + 1 other) +_bound_auxiliary: 6 tests (3 for union + 3 others) +reformulate_disjunct_constraint: 8 tests (2 for Interval/EqualTo + 3 for vector sets + 3 others) +Utility functions: 2 tests +=# + +@testset "P-Split Reformulation" begin + test_psplit() +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index ba5c511..650c989 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,18 +5,19 @@ using Test include("utilities.jl") # RUN ALL THE TESTS -include("aqua.jl") -include("model.jl") -include("jump.jl") -include("variables/query.jl") -include("variables/logical.jl") -include("constraints/selector.jl") -include("constraints/proposition.jl") -include("constraints/disjunct.jl") -include("constraints/indicator.jl") -include("constraints/bigm.jl") -include("constraints/hull.jl") -include("constraints/fallback.jl") -include("constraints/disjunction.jl") -include("print.jl") -include("solve.jl") +# include("aqua.jl") +# include("model.jl") +# include("jump.jl") +# include("variables/query.jl") +# include("variables/logical.jl") +# include("constraints/selector.jl") +# include("constraints/proposition.jl") +# include("constraints/disjunct.jl") +# include("constraints/indicator.jl") +# include("constraints/bigm.jl") +include("constraints/psplit.jl") +# include("constraints/hull.jl") +# include("constraints/fallback.jl") +# include("constraints/disjunction.jl") +# include("print.jl") +# include("solve.jl") From f62a2131ec796c2a6b0d680a741968ffb6d15187 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Tue, 12 Aug 2025 14:39:04 -0400 Subject: [PATCH 10/39] Type change + Intial test commit --- src/psplit.jl | 63 +++++++++++++++----------------------- test/constraints/psplit.jl | 45 ++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 40 deletions(-) diff --git a/src/psplit.jl b/src/psplit.jl index 1c540fb..a9189e4 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -2,7 +2,7 @@ function _build_partitioned_expression( expr::JuMP.GenericAffExpr, partition_variables::Vector{JuMP.VariableRef}, - ::JuMP.ScalarConstraint + ::Any ) new_affexpr = AffExpr(0.0, Dict{JuMP.VariableRef,Float64}()) for var in partition_variables @@ -15,7 +15,7 @@ end function _build_partitioned_expression( expr::JuMP.GenericQuadExpr, partition_variables::Vector{JuMP.VariableRef}, - ::JuMP.ScalarConstraint + ::Any ) new_quadexpr = QuadExpr(0.0, Dict{JuMP.VariableRef,Float64}()) for var in partition_variables @@ -29,7 +29,7 @@ end function _build_partitioned_expression( expr::JuMP.VariableRef, partition_variables::Vector{JuMP.VariableRef}, - ::JuMP.ScalarConstraint + ::Any ) if expr in partition_variables return expr, 0 @@ -41,11 +41,30 @@ end function _build_partitioned_expression( expr::Number, partition_variables::Vector{JuMP.VariableRef}, - ::JuMP.ScalarConstraint - ) + ::Any +) return expr, 0 end + +function _build_partitioned_expression( + expr::JuMP.NonlinearExpr, + partition_variables::Vector{JuMP.VariableRef}, + con::JuMP.ScalarConstraint +) + if expr.head in (:+, :-) + rhs = get(filter(x -> isa(x, Number), expr.args), 1, 0.0) + if expr.head == :+ + rhs = -rhs + end + new_func = _nonlinear_recursion(expr, partition_variables, con) + rhs + else + new_func = _nonlinear_recursion(expr, partition_variables, con) + rhs = 0.0 + end + return new_func, rhs +end + function contains_only_partition_variables( expr::Union{JuMP.GenericAffExpr,JuMP.GenericQuadExpr}, partition_variables::Vector{JuMP.VariableRef} @@ -80,41 +99,7 @@ function contains_only_partition_variables( end -function _build_partitioned_expression( - expr::JuMP.NonlinearExpr, - partition_variables::Vector{JuMP.VariableRef}, - con::JuMP.ScalarConstraint -) - if expr.head in (:+, :-) - rhs = get(filter(x -> isa(x, Number), expr.args), 1, 0.0) - if expr.head == :+ - rhs = -rhs - end - new_func = _nonlinear_recursion(expr, partition_variables, con) + rhs - else - new_func = _nonlinear_recursion(expr, partition_variables, con) - rhs = 0.0 - end - return new_func, rhs -end -# function _build_partitioned_expression( -# expr::JuMP.NonlinearExpr, -# partition_variables::Vector{JuMP.VariableRef}, -# con::JuMP.ScalarConstraint{T,S} -# ) where {T, S <: _MOI.GreaterThan} -# if expr.head in (:+, :-) -# rhs = get(filter(x -> isa(x, Number), expr.args), 1, 0.0) -# if expr.head == :+ -# rhs = -rhs -# end -# new_func = _nonlinear_recursion(expr, partition_variables, con) + rhs -# else -# new_func = _nonlinear_recursion(expr, partition_variables, con) -# rhs = 0.0 -# end -# return -new_func,-rhs -# end function _nonlinear_recursion( expr::Union{JuMP.GenericAffExpr, JuMP.VariableRef, JuMP.GenericQuadExpr, Number}, diff --git a/test/constraints/psplit.jl b/test/constraints/psplit.jl index 052aabe..96fd703 100644 --- a/test/constraints/psplit.jl +++ b/test/constraints/psplit.jl @@ -8,7 +8,7 @@ end #= TODO: Test Plan -_build_partitioned_expression: 5 tests +_build_partitioned_expression: 6 tests (DONE) contains_only_partition_variables: 5 tests (2 for union + 3 others) _nonlinear_recursion: 5 tests (4 for union + 1 other) _bound_auxiliary: 6 tests (3 for union + 3 others) @@ -16,6 +16,49 @@ reformulate_disjunct_constraint: 8 tests (2 for Interval/EqualTo + 3 for vector Utility functions: 2 tests =# +function test_build_partitioned_expression() + model = JuMP.Model() + @variable(model, x[1:4]) + partition_variables = [x[1], x[4]] + # Build each type of expression + nonlinear = exp(x[1]) + affexpr = 1.0 * x[1] - 2.0 * x[2] # Simple way to create AffExpr + quadexpr = x[1] * x[1] + 2.0 * x[1] * x[1] + 3.0 * x[2] * x[2] # Simple way to create QuadExpr + var = x[3] + num = 4.0 + + con = JuMP.build_constraint(error, nonlinear, MOI.EqualTo(0.0)) + result_nl, rhs_nl = DP._build_partitioned_expression(nonlinear, partition_variables, con) + #Why can't I test with an equivalent expression? + #Example output + #P-Split Reformulation: Test Failed at c:\Users\LocalAdmin\Code\DisjunctiveProgramming.jl\test\constraints\psplit.jl:33 + #Expression: result_nl == NonlinearExpr(:exp, Any[x[1]]) + #Evaluated: exp(x[1]) == exp(x[1]) + + + println(typeof(result_nl)) + # @test JuMP.value(result_nl, 1) == exp(x[1]) + @test rhs_nl == 0 + + result_aff, rhs_aff = DP._build_partitioned_expression(affexpr, partition_variables, nothing) + @test result_aff == 1.0 * x[1] + @test rhs_aff == 0 + + result_quad, rhs_quad = DP._build_partitioned_expression(quadexpr, partition_variables, nothing) + @test result_quad == x[1] * x[1] + 2.0 * x[1] * x[1] + @test rhs_quad == 0 + + @test DP._build_partitioned_expression(var, partition_variables, nothing) == (0, 0) + @test DP._build_partitioned_expression(num, partition_variables, nothing) == (num, 0) + + @test_throws ErrorException DP._build_partitioned_expression(JuMP.NonlinearExpr(:+, [affexpr, quadexpr]), partition_variables, nothing) +end + +function _test_contains_only_partition_variables() + #TODO: GenericAffExpr, GenericQuadExpr, GenericNonlinearExpr, VariableRef, Number, ErrorException +end + @testset "P-Split Reformulation" begin test_psplit() + test_build_partitioned_expression() end \ No newline at end of file From 2c286e1c3fc25b5bc1be995625d7111a4b7197b5 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Fri, 22 Aug 2025 11:31:57 -0300 Subject: [PATCH 11/39] Vector excluding NL works. --- src/psplit.jl | 156 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 108 insertions(+), 48 deletions(-) diff --git a/src/psplit.jl b/src/psplit.jl index a9189e4..16e6607 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -1,35 +1,35 @@ #TODO: If the constraint is Nonlinear -> Throw a warning +#TODO: Fix vectors in general + function _build_partitioned_expression( expr::JuMP.GenericAffExpr, - partition_variables::Vector{JuMP.VariableRef}, - ::Any + partition_variables::Vector{JuMP.VariableRef} ) + constant = JuMP.constant(expr) new_affexpr = AffExpr(0.0, Dict{JuMP.VariableRef,Float64}()) for var in partition_variables add_to_expression!(new_affexpr, coefficient(expr, var), var) end - - return new_affexpr, 0 + return new_affexpr, constant end function _build_partitioned_expression( expr::JuMP.GenericQuadExpr, - partition_variables::Vector{JuMP.VariableRef}, - ::Any + partition_variables::Vector{JuMP.VariableRef} ) new_quadexpr = QuadExpr(0.0, Dict{JuMP.VariableRef,Float64}()) + constant = JuMP.constant(expr) for var in partition_variables add_to_expression!(new_quadexpr, get(expr.terms, JuMP.UnorderedPair(var, var), 0.0), var,var) add_to_expression!(new_quadexpr, coefficient(expr, var), var) end - return new_quadexpr, 0 + return new_quadexpr, constant end function _build_partitioned_expression( expr::JuMP.VariableRef, - partition_variables::Vector{JuMP.VariableRef}, - ::Any + partition_variables::Vector{JuMP.VariableRef} ) if expr in partition_variables return expr, 0 @@ -40,8 +40,7 @@ end function _build_partitioned_expression( expr::Number, - partition_variables::Vector{JuMP.VariableRef}, - ::Any + partition_variables::Vector{JuMP.VariableRef} ) return expr, 0 end @@ -49,20 +48,19 @@ end function _build_partitioned_expression( expr::JuMP.NonlinearExpr, - partition_variables::Vector{JuMP.VariableRef}, - con::JuMP.ScalarConstraint + partition_variables::Vector{JuMP.VariableRef} ) if expr.head in (:+, :-) - rhs = get(filter(x -> isa(x, Number), expr.args), 1, 0.0) + constant = get(filter(x -> isa(x, Number), expr.args), 1, 0.0) if expr.head == :+ - rhs = -rhs + constant = -constant end - new_func = _nonlinear_recursion(expr, partition_variables, con) + rhs + new_func = _nonlinear_recursion(expr, partition_variables) + constant else - new_func = _nonlinear_recursion(expr, partition_variables, con) - rhs = 0.0 + new_func = _nonlinear_recursion(expr, partition_variables) + constant = 0.0 end - return new_func, rhs + return new_func, constant end function contains_only_partition_variables( @@ -98,28 +96,23 @@ function contains_only_partition_variables( return all(contains_only_partition_variables(arg, partition_variables) for arg in expr.args) end - - - function _nonlinear_recursion( expr::Union{JuMP.GenericAffExpr, JuMP.VariableRef, JuMP.GenericQuadExpr, Number}, - partition_variables::Vector{JuMP.VariableRef}, - con::JuMP.ScalarConstraint + partition_variables::Vector{JuMP.VariableRef} ) - new_func, _ = _build_partitioned_expression(expr, partition_variables, con) + new_func, _ = _build_partitioned_expression(expr, partition_variables) return new_func end function _nonlinear_recursion( expr::JuMP.NonlinearExpr, - partition_variables::Vector{JuMP.VariableRef}, - con::JuMP.ScalarConstraint, + partition_variables::Vector{JuMP.VariableRef} ) if expr.head in (:+, :-) return JuMP.NonlinearExpr( expr.head, - (_nonlinear_recursion(arg, partition_variables, con) for arg in expr.args)... + (_nonlinear_recursion(arg, partition_variables) for arg in expr.args)... ) end if contains_only_partition_variables(expr, partition_variables) @@ -185,8 +178,8 @@ function set_variable_bound_info(vref::JuMP.AbstractVariableRef, ::PSplit) if !has_lower_bound(vref) || !has_upper_bound(vref) error("Variable $vref must have both lower and upper bounds defined when using the PSplit reformulation.") else - lb = min(0, lower_bound(vref)) - ub = max(0, upper_bound(vref)) + lb = lower_bound(vref) + ub = upper_bound(vref) end return lb, ub end @@ -200,10 +193,10 @@ function reformulate_disjunct_constraint( ) where {T, S <: _MOI.LessThan} p = length(method.partition) v = [@variable(model) for _ in 1:p] - _, rhs = _build_partitioned_expression(con.func, method.partition[p], con) + _, rhs = _build_partitioned_expression(con.func, method.partition[p]) reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) for i in 1:p - func, _ = _build_partitioned_expression(con.func, method.partition[i], con) + func, _ = _build_partitioned_expression(con.func, method.partition[i]) reform_con[i] = JuMP.build_constraint(error, func - v[i], MOI.LessThan(0.0)) _bound_auxiliary(model, v[i], func, method) end @@ -220,10 +213,10 @@ function reformulate_disjunct_constraint( p = length(method.partition) reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) v = [@variable(model) for _ in 1:p] - _, rhs = _build_partitioned_expression(con.func, method.partition[p], con) + _, rhs = _build_partitioned_expression(con.func, method.partition[p]) for i in 1:p - func, _ = _build_partitioned_expression(con.func, method.partition[i], con) + func, _ = _build_partitioned_expression(con.func, method.partition[i]) reform_con[i] = JuMP.build_constraint(error, -func + v[i], MOI.LessThan(0.0)) _bound_auxiliary(model, v[i], -func, method) end @@ -242,10 +235,10 @@ function reformulate_disjunct_constraint( reform_con_lt = Vector{JuMP.AbstractConstraint}(undef, p + 1) reform_con_gt = Vector{JuMP.AbstractConstraint}(undef, p + 1) #let [_, 1] be the upper bound and [_, 2] be the lower bound - _, rhs = _build_partitioned_expression(con.func, method.partition[p], con) + _, rhs = _build_partitioned_expression(con.func, method.partition[p]) v = @variable(model, [1:p, 1:2]) for i in 1:p - func, _= _build_partitioned_expression(con.func, method.partition[i], con) + func, _= _build_partitioned_expression(con.func, method.partition[i]) reform_con_lt[i] = JuMP.build_constraint(error, func - v[i,1], MOI.LessThan(0.0)) reform_con_gt[i] = JuMP.build_constraint(error, -func + v[i,2], MOI.LessThan(0.0)) _bound_auxiliary(model, v[i,1], func, method) @@ -257,29 +250,98 @@ function reformulate_disjunct_constraint( #TODO: how do i avoid the vcat? return vcat(reform_con_lt, reform_con_gt) end +#Functions function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::PSplit -) where {T, S <: Union{_MOI.Nonpositives,_MOI.Nonnegatives, _MOI.Zeros}, R} +) where {T, S <: Union{_MOI.Nonpositives}, R} p = length(method.partition) d = con.set.dimension v = @variable(model, [1:p,1:d]) reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) + constants = Vector{Number}(undef, d) for i in 1:p - func, _ = JuMP.@expression(model, [j = 1:d], _build_partitioned_expression(con.func[j], method.partition[i], con) - v[i,j]) - reform_con[i] = JuMP.build_constraint(error, func, con.set) + #I should be subtracting the constant here. + partitioned_expressions = [_build_partitioned_expression(con.func[j], method.partition[i]) for j in 1:d] + func = JuMP.@expression(model, [j = 1:d], partitioned_expressions[j][1] - v[i,j]) + constants .= [-partitioned_expressions[j][2] for j in 1:d] + reform_con[i] = JuMP.build_constraint(error, func, _MOI.Nonpositives(d)) for j in 1:d _bound_auxiliary(model, v[i,j], func[j], method) end + println("Reformulated constraint $i: ", reform_con[i]) + println("Constants: ", constants) + end + new_func = JuMP.@expression(model,[j = 1:d], sum(v[i,j] * bvref for i in 1:p) - constants[j]*bvref) + println("New function: ", new_func) + reform_con[end] = JuMP.build_constraint(error, new_func, _MOI.Nonpositives(d)) + return vcat(reform_con) +end + +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.VectorConstraint{T, S, R}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + method::PSplit +) where {T, S <: Union{_MOI.Nonnegatives}, R} + p = length(method.partition) + d = con.set.dimension + v = @variable(model, [1:p,1:d]) + reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) + constants = Vector{Number}(undef, d) + for i in 1:p + #I should be subtracting the constant here. + partitioned_expressions = [_build_partitioned_expression(con.func[j], method.partition[i]) for j in 1:d] + func = JuMP.@expression(model, [j = 1:d], partitioned_expressions[j][1] - v[i,j]) + constants .= [-partitioned_expressions[j][2] for j in 1:d] + reform_con[i] = JuMP.build_constraint(error, -func, _MOI.Nonpositives(d)) + for j in 1:d + _bound_auxiliary(model, v[i,j], -func[j], method) + end end - new_func = JuMP.@expression(model,[j = 1:d], sum(v[i,j] * bvref for i in 1:p) + JuMP.constant(con.func[j])*bvref) - reform_con[end] = JuMP.build_constraint(error, new_func, con.set) + new_func = JuMP.@expression(model,[j = 1:d], -sum(v[i,j] * bvref for i in 1:p) + constants[j]*bvref) + reform_con[end] = JuMP.build_constraint(error, new_func, _MOI.Nonpositives(d)) #TODO: how do i avoid the vcat? return vcat(reform_con) end +#TODO: +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.VectorConstraint{T, S, R}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + method::PSplit +) where {T, S <: Union{_MOI.Zeros}, R} + p = length(method.partition) + d = con.set.dimension + reform_con_np = Vector{JuMP.AbstractConstraint}(undef, p + 1) # nonpositive (≤ 0) + reform_con_nn = Vector{JuMP.AbstractConstraint}(undef, p + 1) # nonnegative (≥ 0) + v = @variable(model, [1:p,1:d,1:2]) # [i,j,1] for ≤, [i,j,2] for ≥ + constants = Vector{Number}(undef, d) + for i in 1:p + partitioned_expressions = [_build_partitioned_expression(con.func[j], method.partition[i]) for j in 1:d] + + # Nonpositive part: func ≤ 0 → func - v ≤ 0 + func = JuMP.@expression(model, [j = 1:d], partitioned_expressions[j][1] - v[i,j,1]) + constants .= [partitioned_expressions[j][2] for j in 1:d] + + reform_con_np[i] = JuMP.build_constraint(error, func, _MOI.Nonpositives(d)) + reform_con_nn[i] = JuMP.build_constraint(error, -func, _MOI.Nonpositives(d)) + + for j in 1:d + _bound_auxiliary(model, v[i,j,1], func[j], method) + _bound_auxiliary(model, v[i,j,2], -func[j], method) + end + end + # Final constraints: combine auxiliary variables with constants + new_func_np = JuMP.@expression(model,[j = 1:d], sum(v[i,j,1] * bvref for i in 1:p) + constants[j]*bvref) + new_func_nn = JuMP.@expression(model,[j = 1:d], -sum(v[i,j,2] * bvref for i in 1:p) + constants[j]*bvref) + reform_con_np[end] = JuMP.build_constraint(error, new_func_np, _MOI.Nonpositives(d)) + reform_con_nn[end] = JuMP.build_constraint(error, new_func_nn, _MOI.Nonpositives(d)) + return vcat(reform_con_np, reform_con_nn) +end ################################################################################ # FALLBACK WARNING DISPATCHES @@ -288,8 +350,7 @@ end # Generic fallback for _build_partitioned_expression function _build_partitioned_expression( expr::Any, - ::Vector{JuMP.VariableRef}, - ::Any + ::Vector{JuMP.VariableRef} ) error("PSplit: _build_partitioned_expression not implemented for expression type $(typeof(expr)). Supported types: GenericAffExpr, GenericQuadExpr, VariableRef, Number, NonlinearExpr.") end @@ -305,8 +366,7 @@ end # Generic fallback for _nonlinear_recursion function _nonlinear_recursion( expr::Any, - ::Vector{JuMP.VariableRef}, - ::JuMP.ScalarConstraint + ::Vector{JuMP.VariableRef} ) error("PSplit: _nonlinear_recursion not implemented for expression type $(typeof(expr)). Supported types: GenericAffExpr, GenericQuadExpr, VariableRef, Number, NonlinearExpr.") end @@ -327,7 +387,7 @@ end # Generic fallback for reformulate_disjunct_constraint (scalar) function reformulate_disjunct_constraint( ::JuMP.AbstractModel, - con::JuMP.ScalarConstraint, + con::Any, ::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, ::PSplit ) @@ -337,11 +397,11 @@ end # Generic fallback for reformulate_disjunct_constraint (vector) function reformulate_disjunct_constraint( ::JuMP.AbstractModel, - con::JuMP.VectorConstraint, + con::Any, ::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, ::PSplit ) - error("PSplit: reformulate_disjunct_constraint not implemented for vector constraint set type $(typeof(con.set)). Supported types: Zeros, Nonpositives, Nonnegatives.") + error("PSplit: reformulate_disjunct_constraint not implemented for vector constraint set type $(typeof(con)). Supported types: VectorConstraint of _MOI.Nonnegatives, _MOI.Nonpositives, _MOI.Zeros.") end # Generic fallback for _set_values From d59c9ce35b1716b1deeb648ce924bb1ca7ba144a Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Fri, 22 Aug 2025 15:11:13 -0400 Subject: [PATCH 12/39] GT,LT, EQ work. nn for vectors works. (NL tested for these cases) --- src/psplit.jl | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/psplit.jl b/src/psplit.jl index 16e6607..2b7c672 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -60,7 +60,7 @@ function _build_partitioned_expression( new_func = _nonlinear_recursion(expr, partition_variables) constant = 0.0 end - return new_func, constant + return new_func, -constant end function contains_only_partition_variables( @@ -193,14 +193,15 @@ function reformulate_disjunct_constraint( ) where {T, S <: _MOI.LessThan} p = length(method.partition) v = [@variable(model) for _ in 1:p] - _, rhs = _build_partitioned_expression(con.func, method.partition[p]) + _, constant = _build_partitioned_expression(con.func, method.partition[p]) reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) for i in 1:p func, _ = _build_partitioned_expression(con.func, method.partition[i]) reform_con[i] = JuMP.build_constraint(error, func - v[i], MOI.LessThan(0.0)) _bound_auxiliary(model, v[i], func, method) + println("reform_con[$i]: ", reform_con[i]) end - reform_con[end] = JuMP.build_constraint(error, sum(v[i] * bvref for i in 1:p) - (con.set.upper + rhs) * bvref, MOI.LessThan(0.0)) + reform_con[end] = JuMP.build_constraint(error, sum(v[i] * bvref for i in 1:p) - (con.set.upper - constant) * bvref, MOI.LessThan(0.0)) return reform_con end #DONE WITH NL @@ -213,14 +214,14 @@ function reformulate_disjunct_constraint( p = length(method.partition) reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) v = [@variable(model) for _ in 1:p] - _, rhs = _build_partitioned_expression(con.func, method.partition[p]) + _, constant = _build_partitioned_expression(con.func, method.partition[p]) for i in 1:p func, _ = _build_partitioned_expression(con.func, method.partition[i]) - reform_con[i] = JuMP.build_constraint(error, -func + v[i], MOI.LessThan(0.0)) + reform_con[i] = JuMP.build_constraint(error, -func - v[i], MOI.LessThan(0.0)) _bound_auxiliary(model, v[i], -func, method) end - reform_con[end] = JuMP.build_constraint(error, -sum(v[i] * bvref for i in 1:p) + (con.set.lower + rhs) * bvref, MOI.LessThan(0.0)) + reform_con[end] = JuMP.build_constraint(error, sum(v[i] * bvref for i in 1:p) - (-con.set.lower + constant) * bvref, MOI.LessThan(0.0)) return reform_con end @@ -235,18 +236,18 @@ function reformulate_disjunct_constraint( reform_con_lt = Vector{JuMP.AbstractConstraint}(undef, p + 1) reform_con_gt = Vector{JuMP.AbstractConstraint}(undef, p + 1) #let [_, 1] be the upper bound and [_, 2] be the lower bound - _, rhs = _build_partitioned_expression(con.func, method.partition[p]) + _, constant = _build_partitioned_expression(con.func, method.partition[p]) v = @variable(model, [1:p, 1:2]) for i in 1:p func, _= _build_partitioned_expression(con.func, method.partition[i]) reform_con_lt[i] = JuMP.build_constraint(error, func - v[i,1], MOI.LessThan(0.0)) - reform_con_gt[i] = JuMP.build_constraint(error, -func + v[i,2], MOI.LessThan(0.0)) + reform_con_gt[i] = JuMP.build_constraint(error, -func - v[i,2], MOI.LessThan(0.0)) _bound_auxiliary(model, v[i,1], func, method) _bound_auxiliary(model, v[i,2], -func, method) end set_values = _set_values(con.set) - reform_con_lt[end] = JuMP.build_constraint(error, sum(v[i,1] * bvref for i in 1:p) - (set_values[2] + rhs) * bvref, MOI.LessThan(0.0)) - reform_con_gt[end] = JuMP.build_constraint(error, -sum(v[i,2] * bvref for i in 1:p) + (set_values[1] + rhs) * bvref, MOI.LessThan(0.0)) + reform_con_lt[end] = JuMP.build_constraint(error, sum(v[i,1] * bvref for i in 1:p) - (set_values[2] - constant) * bvref, MOI.LessThan(0.0)) + reform_con_gt[end] = JuMP.build_constraint(error, sum(v[i,2] * bvref for i in 1:p) - (-set_values[1] + constant) * bvref, MOI.LessThan(0.0)) #TODO: how do i avoid the vcat? return vcat(reform_con_lt, reform_con_gt) end @@ -263,19 +264,15 @@ function reformulate_disjunct_constraint( reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) constants = Vector{Number}(undef, d) for i in 1:p - #I should be subtracting the constant here. partitioned_expressions = [_build_partitioned_expression(con.func[j], method.partition[i]) for j in 1:d] func = JuMP.@expression(model, [j = 1:d], partitioned_expressions[j][1] - v[i,j]) - constants .= [-partitioned_expressions[j][2] for j in 1:d] + constants .= [partitioned_expressions[j][2] for j in 1:d] reform_con[i] = JuMP.build_constraint(error, func, _MOI.Nonpositives(d)) for j in 1:d _bound_auxiliary(model, v[i,j], func[j], method) end - println("Reformulated constraint $i: ", reform_con[i]) - println("Constants: ", constants) end - new_func = JuMP.@expression(model,[j = 1:d], sum(v[i,j] * bvref for i in 1:p) - constants[j]*bvref) - println("New function: ", new_func) + new_func = JuMP.@expression(model,[j = 1:d], sum(v[i,j] * bvref for i in 1:p) + constants[j]*bvref) reform_con[end] = JuMP.build_constraint(error, new_func, _MOI.Nonpositives(d)) return vcat(reform_con) end @@ -294,14 +291,14 @@ function reformulate_disjunct_constraint( for i in 1:p #I should be subtracting the constant here. partitioned_expressions = [_build_partitioned_expression(con.func[j], method.partition[i]) for j in 1:d] - func = JuMP.@expression(model, [j = 1:d], partitioned_expressions[j][1] - v[i,j]) - constants .= [-partitioned_expressions[j][2] for j in 1:d] - reform_con[i] = JuMP.build_constraint(error, -func, _MOI.Nonpositives(d)) + func = JuMP.@expression(model, [j = 1:d], -partitioned_expressions[j][1] - v[i,j]) + constants .= [partitioned_expressions[j][2] for j in 1:d] + reform_con[i] = JuMP.build_constraint(error, func, _MOI.Nonpositives(d)) for j in 1:d _bound_auxiliary(model, v[i,j], -func[j], method) end end - new_func = JuMP.@expression(model,[j = 1:d], -sum(v[i,j] * bvref for i in 1:p) + constants[j]*bvref) + new_func = JuMP.@expression(model,[j = 1:d], sum(v[i,j] * bvref for i in 1:p) - constants[j]*bvref) reform_con[end] = JuMP.build_constraint(error, new_func, _MOI.Nonpositives(d)) #TODO: how do i avoid the vcat? return vcat(reform_con) From 5dbcde89f665f1e171a961dcd6276c0a5ac6e04b Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Fri, 22 Aug 2025 16:26:25 -0400 Subject: [PATCH 13/39] . --- src/psplit.jl | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/psplit.jl b/src/psplit.jl index 2b7c672..52bfe09 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -45,7 +45,7 @@ function _build_partitioned_expression( return expr, 0 end - +#TODO: Right now, if the function is made negative via a bracket (eg -(x[3]^2 + y[3]), everything in the bracket disappears) function _build_partitioned_expression( expr::JuMP.NonlinearExpr, partition_variables::Vector{JuMP.VariableRef} @@ -265,9 +265,9 @@ function reformulate_disjunct_constraint( constants = Vector{Number}(undef, d) for i in 1:p partitioned_expressions = [_build_partitioned_expression(con.func[j], method.partition[i]) for j in 1:d] - func = JuMP.@expression(model, [j = 1:d], partitioned_expressions[j][1] - v[i,j]) + func = JuMP.@expression(model, [j = 1:d], partitioned_expressions[j][1]) constants .= [partitioned_expressions[j][2] for j in 1:d] - reform_con[i] = JuMP.build_constraint(error, func, _MOI.Nonpositives(d)) + reform_con[i] = JuMP.build_constraint(error, func - v[i,:], _MOI.Nonpositives(d)) for j in 1:d _bound_auxiliary(model, v[i,j], func[j], method) end @@ -291,14 +291,15 @@ function reformulate_disjunct_constraint( for i in 1:p #I should be subtracting the constant here. partitioned_expressions = [_build_partitioned_expression(con.func[j], method.partition[i]) for j in 1:d] - func = JuMP.@expression(model, [j = 1:d], -partitioned_expressions[j][1] - v[i,j]) - constants .= [partitioned_expressions[j][2] for j in 1:d] - reform_con[i] = JuMP.build_constraint(error, func, _MOI.Nonpositives(d)) + func = JuMP.@expression(model, [j = 1:d], -partitioned_expressions[j][1]) + constants .= [-partitioned_expressions[j][2] for j in 1:d] + reform_con[i] = JuMP.build_constraint(error, func - v[i,:], _MOI.Nonpositives(d)) + println("func: ", func) for j in 1:d - _bound_auxiliary(model, v[i,j], -func[j], method) + _bound_auxiliary(model, v[i,j], func[j], method) end end - new_func = JuMP.@expression(model,[j = 1:d], sum(v[i,j] * bvref for i in 1:p) - constants[j]*bvref) + new_func = JuMP.@expression(model,[j = 1:d], sum(v[i,j] * bvref for i in 1:p) + constants[j]*bvref) reform_con[end] = JuMP.build_constraint(error, new_func, _MOI.Nonpositives(d)) #TODO: how do i avoid the vcat? return vcat(reform_con) From 39c71d7039739f6b65aade2b7e59fb7fe08eec36 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Fri, 22 Aug 2025 16:50:47 -0400 Subject: [PATCH 14/39] Fixed Zeros. --- src/psplit.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/psplit.jl b/src/psplit.jl index 52bfe09..cbf9b19 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -321,11 +321,11 @@ function reformulate_disjunct_constraint( partitioned_expressions = [_build_partitioned_expression(con.func[j], method.partition[i]) for j in 1:d] # Nonpositive part: func ≤ 0 → func - v ≤ 0 - func = JuMP.@expression(model, [j = 1:d], partitioned_expressions[j][1] - v[i,j,1]) + func = JuMP.@expression(model, [j = 1:d], partitioned_expressions[j][1]) constants .= [partitioned_expressions[j][2] for j in 1:d] - reform_con_np[i] = JuMP.build_constraint(error, func, _MOI.Nonpositives(d)) - reform_con_nn[i] = JuMP.build_constraint(error, -func, _MOI.Nonpositives(d)) + reform_con_np[i] = JuMP.build_constraint(error, func - v[i,:], _MOI.Nonpositives(d)) + reform_con_nn[i] = JuMP.build_constraint(error, -func - v[i,:], _MOI.Nonpositives(d)) for j in 1:d _bound_auxiliary(model, v[i,j,1], func[j], method) @@ -335,7 +335,7 @@ function reformulate_disjunct_constraint( # Final constraints: combine auxiliary variables with constants new_func_np = JuMP.@expression(model,[j = 1:d], sum(v[i,j,1] * bvref for i in 1:p) + constants[j]*bvref) - new_func_nn = JuMP.@expression(model,[j = 1:d], -sum(v[i,j,2] * bvref for i in 1:p) + constants[j]*bvref) + new_func_nn = JuMP.@expression(model,[j = 1:d], -sum(v[i,j,2] * bvref for i in 1:p) - constants[j]*bvref) reform_con_np[end] = JuMP.build_constraint(error, new_func_np, _MOI.Nonpositives(d)) reform_con_nn[end] = JuMP.build_constraint(error, new_func_nn, _MOI.Nonpositives(d)) return vcat(reform_con_np, reform_con_nn) From 28160905ad2f36d12b713e3105ecc7a830511e31 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Sat, 23 Aug 2025 15:28:35 -0400 Subject: [PATCH 15/39] NLE not working with negated expressions. --- src/psplit.jl | 5 +---- test/constraints/psplit.jl | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/psplit.jl b/src/psplit.jl index cbf9b19..baa5818 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -45,7 +45,7 @@ function _build_partitioned_expression( return expr, 0 end -#TODO: Right now, if the function is made negative via a bracket (eg -(x[3]^2 + y[3]), everything in the bracket disappears) +#TODO:if the function is made negative via a bracket (eg -(x[3]^2 + y[3]), everything in the bracket disappears) function _build_partitioned_expression( expr::JuMP.NonlinearExpr, partition_variables::Vector{JuMP.VariableRef} @@ -73,7 +73,6 @@ function contains_only_partition_variables( return true end - function contains_only_partition_variables( expr::JuMP.VariableRef, partition_variables::Vector{JuMP.VariableRef} @@ -199,7 +198,6 @@ function reformulate_disjunct_constraint( func, _ = _build_partitioned_expression(con.func, method.partition[i]) reform_con[i] = JuMP.build_constraint(error, func - v[i], MOI.LessThan(0.0)) _bound_auxiliary(model, v[i], func, method) - println("reform_con[$i]: ", reform_con[i]) end reform_con[end] = JuMP.build_constraint(error, sum(v[i] * bvref for i in 1:p) - (con.set.upper - constant) * bvref, MOI.LessThan(0.0)) return reform_con @@ -294,7 +292,6 @@ function reformulate_disjunct_constraint( func = JuMP.@expression(model, [j = 1:d], -partitioned_expressions[j][1]) constants .= [-partitioned_expressions[j][2] for j in 1:d] reform_con[i] = JuMP.build_constraint(error, func - v[i,:], _MOI.Nonpositives(d)) - println("func: ", func) for j in 1:d _bound_auxiliary(model, v[i,j], func[j], method) end diff --git a/test/constraints/psplit.jl b/test/constraints/psplit.jl index 96fd703..d104eba 100644 --- a/test/constraints/psplit.jl +++ b/test/constraints/psplit.jl @@ -17,8 +17,7 @@ Utility functions: 2 tests =# function test_build_partitioned_expression() - model = JuMP.Model() - @variable(model, x[1:4]) + @variable(JuMP.Model(), x[1:4]) partition_variables = [x[1], x[4]] # Build each type of expression nonlinear = exp(x[1]) @@ -28,7 +27,7 @@ function test_build_partitioned_expression() num = 4.0 con = JuMP.build_constraint(error, nonlinear, MOI.EqualTo(0.0)) - result_nl, rhs_nl = DP._build_partitioned_expression(nonlinear, partition_variables, con) + result_nl, rhs_nl = DP._build_partitioned_expression(nonlinear, partition_variables) #Why can't I test with an equivalent expression? #Example output #P-Split Reformulation: Test Failed at c:\Users\LocalAdmin\Code\DisjunctiveProgramming.jl\test\constraints\psplit.jl:33 @@ -40,21 +39,24 @@ function test_build_partitioned_expression() # @test JuMP.value(result_nl, 1) == exp(x[1]) @test rhs_nl == 0 - result_aff, rhs_aff = DP._build_partitioned_expression(affexpr, partition_variables, nothing) + result_aff, rhs_aff = DP._build_partitioned_expression(affexpr, partition_variables) @test result_aff == 1.0 * x[1] @test rhs_aff == 0 - result_quad, rhs_quad = DP._build_partitioned_expression(quadexpr, partition_variables, nothing) + result_quad, rhs_quad = DP._build_partitioned_expression(quadexpr, partition_variables) @test result_quad == x[1] * x[1] + 2.0 * x[1] * x[1] @test rhs_quad == 0 - @test DP._build_partitioned_expression(var, partition_variables, nothing) == (0, 0) - @test DP._build_partitioned_expression(num, partition_variables, nothing) == (num, 0) + @test DP._build_partitioned_expression(var, partition_variables) == (0, 0) + @test DP._build_partitioned_expression(num, partition_variables) == (num, 0) - @test_throws ErrorException DP._build_partitioned_expression(JuMP.NonlinearExpr(:+, [affexpr, quadexpr]), partition_variables, nothing) + @test_throws ErrorException DP._build_partitioned_expression(JuMP.NonlinearExpr(:+, [affexpr, quadexpr]), partition_variables) end function _test_contains_only_partition_variables() + @variable(JuMP.Model(), x[1:4]) + + #TODO: GenericAffExpr, GenericQuadExpr, GenericNonlinearExpr, VariableRef, Number, ErrorException end From cf0915cece2fe4a7741b2f12ace3e2f9f646c518 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Mon, 25 Aug 2025 10:31:27 -0400 Subject: [PATCH 16/39] Additional tests. --- src/psplit.jl | 1 + test/constraints/psplit.jl | 44 +++++++++++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/psplit.jl b/src/psplit.jl index baa5818..5de37a0 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -169,6 +169,7 @@ function _bound_auxiliary( func::Union{JuMP.NonlinearExpr, JuMP.QuadExpr, Number}, method::PSplit ) +#Leaving it to be bounded indirectly by the variables in the partition end requires_variable_bound_info(method::PSplit) = true diff --git a/test/constraints/psplit.jl b/test/constraints/psplit.jl index d104eba..f588b6c 100644 --- a/test/constraints/psplit.jl +++ b/test/constraints/psplit.jl @@ -11,7 +11,7 @@ TODO: Test Plan _build_partitioned_expression: 6 tests (DONE) contains_only_partition_variables: 5 tests (2 for union + 3 others) _nonlinear_recursion: 5 tests (4 for union + 1 other) -_bound_auxiliary: 6 tests (3 for union + 3 others) +_bound_auxiliary: 6 tests (3 for union + 3 others) (DONE) reformulate_disjunct_constraint: 8 tests (2 for Interval/EqualTo + 3 for vector sets + 3 others) Utility functions: 2 tests =# @@ -55,12 +55,46 @@ end function _test_contains_only_partition_variables() @variable(JuMP.Model(), x[1:4]) - - + + #TODO:Come back to this after fixing NLE #TODO: GenericAffExpr, GenericQuadExpr, GenericNonlinearExpr, VariableRef, Number, ErrorException end +function _test_bound_auxiliary() + model = GDPModel() + # model = JuMP.Model() + @variable(model, 0 <= x[1:4] <= 3) + @variable(model, v[1:5]) + method = PSplit([[x[1], x[2]], [x[3], x[4]]]) + nonlinear = JuMP.@expression(model, exp(x[1])) + affexpr = 1.0 * x[1] - 2.0 * x[2] + quadexpr = x[1] * x[1] + 2.0 * x[1] * x[1] + 3.0 * x[2] * x[2] + var = x[3] + num = 4.0 + + for i in 1:4 + DP._variable_bounds(model)[x[i]] = DP.set_variable_bound_info(x[i], method) + end + DP._bound_auxiliary(model, v[1], nonlinear, method) + DP._bound_auxiliary(model, v[2], quadexpr, method) + DP._bound_auxiliary(model, v[3], num, method) + DP._bound_auxiliary(model, v[4], var, method) + DP._bound_auxiliary(model, v[5], affexpr, method) + + @test JuMP.lower_bound(v[5]) == -6 + @test JuMP.upper_bound(v[5]) == 3 + @test JuMP.lower_bound(v[4]) == 0 + @test JuMP.upper_bound(v[4]) == 3 + for i in 1:3 + @test !JuMP.has_lower_bound(v[i]) == true + @test !JuMP.has_upper_bound(v[i]) == true + end + +end + @testset "P-Split Reformulation" begin - test_psplit() - test_build_partitioned_expression() + # test_psplit() + # test_build_partitioned_expression() + # _test_contains_only_partition_variables() + # _test_bound_auxiliary() end \ No newline at end of file From f32f05e62b785a4915469cd3b768a2bbd522e1fb Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Mon, 25 Aug 2025 15:07:35 -0400 Subject: [PATCH 17/39] Added tests for reformulate_disjunct_constraint --- src/psplit.jl | 16 +++---- test/constraints/psplit.jl | 86 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 8 deletions(-) diff --git a/src/psplit.jl b/src/psplit.jl index 5de37a0..b36de68 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -192,7 +192,7 @@ function reformulate_disjunct_constraint( method::PSplit ) where {T, S <: _MOI.LessThan} p = length(method.partition) - v = [@variable(model) for _ in 1:p] + v = [@variable(model, base_name = "v_$(hash(con))_$(i)") for i in 1:p] _, constant = _build_partitioned_expression(con.func, method.partition[p]) reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) for i in 1:p @@ -212,7 +212,7 @@ function reformulate_disjunct_constraint( ) where {T, S <: _MOI.GreaterThan} p = length(method.partition) reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) - v = [@variable(model) for _ in 1:p] + v = [@variable(model, base_name = "v_$(hash(con))_$(i)") for i in 1:p] _, constant = _build_partitioned_expression(con.func, method.partition[p]) for i in 1:p @@ -236,7 +236,7 @@ function reformulate_disjunct_constraint( reform_con_gt = Vector{JuMP.AbstractConstraint}(undef, p + 1) #let [_, 1] be the upper bound and [_, 2] be the lower bound _, constant = _build_partitioned_expression(con.func, method.partition[p]) - v = @variable(model, [1:p, 1:2]) + v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)") for i in 1:p, j in 1:2] for i in 1:p func, _= _build_partitioned_expression(con.func, method.partition[i]) reform_con_lt[i] = JuMP.build_constraint(error, func - v[i,1], MOI.LessThan(0.0)) @@ -259,7 +259,7 @@ function reformulate_disjunct_constraint( ) where {T, S <: Union{_MOI.Nonpositives}, R} p = length(method.partition) d = con.set.dimension - v = @variable(model, [1:p,1:d]) + v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)") for i in 1:p, j in 1:d] reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) constants = Vector{Number}(undef, d) for i in 1:p @@ -284,7 +284,7 @@ function reformulate_disjunct_constraint( ) where {T, S <: Union{_MOI.Nonnegatives}, R} p = length(method.partition) d = con.set.dimension - v = @variable(model, [1:p,1:d]) + v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)") for i in 1:p, j in 1:d] reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) constants = Vector{Number}(undef, d) for i in 1:p @@ -313,7 +313,7 @@ function reformulate_disjunct_constraint( d = con.set.dimension reform_con_np = Vector{JuMP.AbstractConstraint}(undef, p + 1) # nonpositive (≤ 0) reform_con_nn = Vector{JuMP.AbstractConstraint}(undef, p + 1) # nonnegative (≥ 0) - v = @variable(model, [1:p,1:d,1:2]) # [i,j,1] for ≤, [i,j,2] for ≥ + v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)_$(k)") for i in 1:p, j in 1:d, k in 1:2] constants = Vector{Number}(undef, d) for i in 1:p partitioned_expressions = [_build_partitioned_expression(con.func[j], method.partition[i]) for j in 1:d] @@ -322,8 +322,8 @@ function reformulate_disjunct_constraint( func = JuMP.@expression(model, [j = 1:d], partitioned_expressions[j][1]) constants .= [partitioned_expressions[j][2] for j in 1:d] - reform_con_np[i] = JuMP.build_constraint(error, func - v[i,:], _MOI.Nonpositives(d)) - reform_con_nn[i] = JuMP.build_constraint(error, -func - v[i,:], _MOI.Nonpositives(d)) + reform_con_np[i] = JuMP.build_constraint(error, func - v[i,:,1], _MOI.Nonpositives(d)) + reform_con_nn[i] = JuMP.build_constraint(error, -func - v[i,:,2], _MOI.Nonpositives(d)) for j in 1:d _bound_auxiliary(model, v[i,j,1], func[j], method) diff --git a/test/constraints/psplit.jl b/test/constraints/psplit.jl index f588b6c..5d64e58 100644 --- a/test/constraints/psplit.jl +++ b/test/constraints/psplit.jl @@ -75,6 +75,7 @@ function _test_bound_auxiliary() for i in 1:4 DP._variable_bounds(model)[x[i]] = DP.set_variable_bound_info(x[i], method) end + DP._bound_auxiliary(model, v[1], nonlinear, method) DP._bound_auxiliary(model, v[2], quadexpr, method) DP._bound_auxiliary(model, v[3], num, method) @@ -92,9 +93,94 @@ function _test_bound_auxiliary() end +function _test_reformulate_disjunct_constraint_affexpr() + model = GDPModel() + @variable(model, 0 <= x[1:4] <= 3) + @variable(model, y[1:2], Bin) + method = PSplit([[x[1], x[2]], [x[3], x[4]]]) + for i in 1:4 + DP._variable_bounds(model)[x[i]] = DP.set_variable_bound_info(x[i], method) + end + + lt = JuMP.constraint_object(@constraint(model, x[1] + x[3] <= 1)) + gt = JuMP.constraint_object(@constraint(model, x[2] + x[4] >= 1)) + eq = JuMP.constraint_object(@constraint(model, x[3] + x[4] == 1)) + interval = JuMP.constraint_object(@constraint(model, 0 <= x[1] + x[2] + x[3] <= 0.5)) + nn = JuMP.constraint_object(@constraint(model, x .- 1 >= 0)) + np = JuMP.constraint_object(@constraint(model, x .+ 1 >= 0)) + zeros = JuMP.constraint_object(@constraint(model, 5x .- 1 == 0)) + ref_lt = reformulate_disjunct_constraint(model, lt, y[1], method) + ref_gt = reformulate_disjunct_constraint(model, gt, y[2], method) + ref_eq = reformulate_disjunct_constraint(model, eq, y[1], method) + ref_interval = reformulate_disjunct_constraint(model, interval, y[2], method) + ref_nn = reformulate_disjunct_constraint(model, nn, y[1], method) + ref_np = reformulate_disjunct_constraint(model, np, y[2], method) + ref_zeros = reformulate_disjunct_constraint(model, zeros, y[1], method) + + @test ref_lt[1].func == x[1] - variable_by_name(model, "v_$(hash(lt))_1") + @test ref_lt[2].func == x[3] - variable_by_name(model, "v_$(hash(lt))_2") + @test ref_lt[3].func == variable_by_name(model, "v_$(hash(lt))_1")*y[1] + variable_by_name(model, "v_$(hash(lt))_2")*y[1] - y[1] + @test ref_gt[1].func == -x[2]- variable_by_name(model, "v_$(hash(gt))_1") + @test ref_gt[2].func == -x[4]- variable_by_name(model, "v_$(hash(gt))_2") + + @test ref_eq[1].func == -variable_by_name(model, "v_$(hash(eq))_1_1") + @test ref_eq[2].func == x[3] + x[4] - variable_by_name(model, "v_$(hash(eq))_2_1") + @test ref_eq[4].func == -variable_by_name(model, "v_$(hash(eq))_1_2") + @test ref_eq[5].func == -x[3] - x[4] - variable_by_name(model, "v_$(hash(eq))_2_2") + @test ref_eq[3].func == variable_by_name(model, "v_$(hash(eq))_1_1")*y[1] + variable_by_name(model, "v_$(hash(eq))_2_1")*y[1] - y[1] + + @test ref_interval[1].func == x[1] + x[2] - variable_by_name(model, "v_$(hash(interval))_1_1") + @test ref_interval[2].func == x[3] - variable_by_name(model, "v_$(hash(interval))_2_1") + @test ref_interval[4].func == -x[1] - x[2] - variable_by_name(model, "v_$(hash(interval))_1_2") + @test ref_interval[5].func == -x[3] - variable_by_name(model, "v_$(hash(interval))_2_2") + @test ref_interval[3].func == variable_by_name(model, "v_$(hash(interval))_1_1")*y[2] + variable_by_name(model, "v_$(hash(interval))_2_1")*y[2] - 0.5*y[2] + + @test ref_nn[1].func == [-x[1] - variable_by_name(model, "v_$(hash(nn))_1_1"), + -x[2] - variable_by_name(model, "v_$(hash(nn))_1_2"), + -variable_by_name(model, "v_$(hash(nn))_1_3"), + -variable_by_name(model, "v_$(hash(nn))_1_4")] + @test ref_nn[2].func == [-variable_by_name(model, "v_$(hash(nn))_2_1"), + -variable_by_name(model, "v_$(hash(nn))_2_2"), + -x[3] - variable_by_name(model, "v_$(hash(nn))_2_3"), + -x[4] - variable_by_name(model, "v_$(hash(nn))_2_4")] + @test ref_nn[3].func == [variable_by_name(model, "v_$(hash(nn))_1_1")*y[1] + variable_by_name(model, "v_$(hash(nn))_2_1")*y[1] + y[1], + variable_by_name(model, "v_$(hash(nn))_1_2")*y[1] + variable_by_name(model, "v_$(hash(nn))_2_2")*y[1] + y[1], + variable_by_name(model, "v_$(hash(nn))_1_3")*y[1] + variable_by_name(model, "v_$(hash(nn))_2_3")*y[1] + y[1], + variable_by_name(model, "v_$(hash(nn))_1_4")*y[1] + variable_by_name(model, "v_$(hash(nn))_2_4")*y[1] + y[1]] + + @test ref_np[1].func == [-x[1] - variable_by_name(model, "v_$(hash(np))_1_1"), + -x[2] - variable_by_name(model, "v_$(hash(np))_1_2"), + -variable_by_name(model, "v_$(hash(np))_1_3"), + -variable_by_name(model, "v_$(hash(np))_1_4")] + @test ref_np[2].func == [-variable_by_name(model, "v_$(hash(np))_2_1"), + -variable_by_name(model, "v_$(hash(np))_2_2"), + -x[3] - variable_by_name(model, "v_$(hash(np))_2_3"), + -x[4] - variable_by_name(model, "v_$(hash(np))_2_4")] + @test ref_np[3].func == [variable_by_name(model, "v_$(hash(np))_1_1")*y[2] + variable_by_name(model, "v_$(hash(np))_2_1")*y[2] - y[2], + variable_by_name(model, "v_$(hash(np))_1_2")*y[2] + variable_by_name(model, "v_$(hash(np))_2_2")*y[2] - y[2], + variable_by_name(model, "v_$(hash(np))_1_3")*y[2] + variable_by_name(model, "v_$(hash(np))_2_3")*y[2] - y[2], + variable_by_name(model, "v_$(hash(np))_1_4")*y[2] + variable_by_name(model, "v_$(hash(np))_2_4")*y[2] - y[2]] + + @test ref_zeros[1].func == [5x[1] - variable_by_name(model, "v_$(hash(zeros))_1_1_1"), + 5x[2] - variable_by_name(model, "v_$(hash(zeros))_1_2_1"), + -variable_by_name(model, "v_$(hash(zeros))_1_3_1"), + -variable_by_name(model, "v_$(hash(zeros))_1_4_1")] + @test ref_zeros[2].func == [-variable_by_name(model, "v_$(hash(zeros))_2_1_1"), + -variable_by_name(model, "v_$(hash(zeros))_2_2_1"), + 5x[3] - variable_by_name(model, "v_$(hash(zeros))_2_3_1"), + 5x[4] - variable_by_name(model, "v_$(hash(zeros))_2_4_1")] + + @test ref_zeros[3].func == [variable_by_name(model, "v_$(hash(zeros))_1_1_1")*y[1] + variable_by_name(model, "v_$(hash(zeros))_2_1_1")*y[1] - y[1], + variable_by_name(model, "v_$(hash(zeros))_1_2_1")*y[1] + variable_by_name(model, "v_$(hash(zeros))_2_2_1")*y[1] - y[1], + variable_by_name(model, "v_$(hash(zeros))_1_3_1")*y[1] + variable_by_name(model, "v_$(hash(zeros))_2_3_1")*y[1] - y[1], + variable_by_name(model, "v_$(hash(zeros))_1_4_1")*y[1] + variable_by_name(model, "v_$(hash(zeros))_2_4_1")*y[1] - y[1]] + +end + @testset "P-Split Reformulation" begin # test_psplit() # test_build_partitioned_expression() # _test_contains_only_partition_variables() # _test_bound_auxiliary() + _test_reformulate_disjunct_constraint_affexpr() end \ No newline at end of file From f56c2cb2ab28cffc4934cb3fc15f5d890a942dbf Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Tue, 26 Aug 2025 12:08:22 -0400 Subject: [PATCH 18/39] WIP NLE. Nested detection is not working. --- src/psplit.jl | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/psplit.jl b/src/psplit.jl index b36de68..19395b1 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -45,22 +45,25 @@ function _build_partitioned_expression( return expr, 0 end -#TODO:if the function is made negative via a bracket (eg -(x[3]^2 + y[3]), everything in the bracket disappears) +#TODO:if the constant is on the LHS and RHS, then what's returned does not negate the overall constant. +#TODO: store constants as you go, then return the sum of them function _build_partitioned_expression( expr::JuMP.NonlinearExpr, - partition_variables::Vector{JuMP.VariableRef} + partition_variables::Vector{JuMP.VariableRef}, ) + constants = Vector{Float64}() if expr.head in (:+, :-) constant = get(filter(x -> isa(x, Number), expr.args), 1, 0.0) if expr.head == :+ constant = -constant end - new_func = _nonlinear_recursion(expr, partition_variables) + constant + new_func, constants = _nonlinear_recursion(expr, partition_variables, constants) else - new_func = _nonlinear_recursion(expr, partition_variables) + new_func, constants = _nonlinear_recursion(expr, partition_variables, constants) constant = 0.0 end - return new_func, -constant + + return new_func, -sum(constants) end function contains_only_partition_variables( @@ -97,27 +100,33 @@ end function _nonlinear_recursion( expr::Union{JuMP.GenericAffExpr, JuMP.VariableRef, JuMP.GenericQuadExpr, Number}, - partition_variables::Vector{JuMP.VariableRef} + partition_variables::Vector{JuMP.VariableRef}, + constants::Vector{Float64} ) - new_func, _ = _build_partitioned_expression(expr, partition_variables) - return new_func + new_func, constant = _build_partitioned_expression(expr, partition_variables) + return new_func, constants end function _nonlinear_recursion( expr::JuMP.NonlinearExpr, - partition_variables::Vector{JuMP.VariableRef} + partition_variables::Vector{JuMP.VariableRef}, + constants::Vector{Float64} ) if expr.head in (:+, :-) + constant = get(filter(x -> isa(x, Number), expr.args), 1, 0.0) + if expr.head == :+ + constant = -constant + end return JuMP.NonlinearExpr( expr.head, - (_nonlinear_recursion(arg, partition_variables) for arg in expr.args)... - ) + (_nonlinear_recursion(arg, partition_variables, constants)[1] for arg in expr.args)... + ) + constant, push!(constants, -constant) end - if contains_only_partition_variables(expr, partition_variables) - return expr + if contains_only_partition_variables(expr, partition_variables) || expr.head == :* + return expr, constants else - return 0 + return 0, constants end end From 3b37d01fc392706e0ce5d9ff1cc9d0671dda29c1 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Wed, 27 Aug 2025 14:20:22 -0400 Subject: [PATCH 19/39] Nonlinear works. Need to add error message. --- src/psplit.jl | 89 +++++++++------------------------------------------ 1 file changed, 15 insertions(+), 74 deletions(-) diff --git a/src/psplit.jl b/src/psplit.jl index 19395b1..98d19fb 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -35,6 +35,7 @@ function _build_partitioned_expression( return expr, 0 else return 0, 0 + #This does not work for things like exp(x) as exp(0) = 1 end end @@ -45,89 +46,29 @@ function _build_partitioned_expression( return expr, 0 end -#TODO:if the constant is on the LHS and RHS, then what's returned does not negate the overall constant. -#TODO: store constants as you go, then return the sum of them function _build_partitioned_expression( expr::JuMP.NonlinearExpr, - partition_variables::Vector{JuMP.VariableRef}, -) - constants = Vector{Float64}() - if expr.head in (:+, :-) - constant = get(filter(x -> isa(x, Number), expr.args), 1, 0.0) - if expr.head == :+ - constant = -constant - end - new_func, constants = _nonlinear_recursion(expr, partition_variables, constants) - else - new_func, constants = _nonlinear_recursion(expr, partition_variables, constants) - constant = 0.0 - end - - return new_func, -sum(constants) -end - -function contains_only_partition_variables( - expr::Union{JuMP.GenericAffExpr,JuMP.GenericQuadExpr}, partition_variables::Vector{JuMP.VariableRef} ) - for (var, _) in expr.terms - var in partition_variables || return false + new_args = Vector{Any}(undef, length(expr.args)) + for i in 1:length(expr.args) + new_args[i] = _build_partitioned_expression(expr.args[i], partition_variables)[1] + if expr.head in (:exp, :cos, :cosh, :log, :log10, :log2) && new_args[i] == 0 + return 0, 0 + end + if expr.head == :/ && new_args[2] == 0 + return 0, 0 + end end - return true -end - -function contains_only_partition_variables( - expr::JuMP.VariableRef, - partition_variables::Vector{JuMP.VariableRef} -) - return expr in partition_variables -end - -function contains_only_partition_variables( - expr::Number, - partition_variables::Vector{JuMP.VariableRef} -) - return true -end - -#Helper functions for the nonlinear case. -function contains_only_partition_variables( - expr::Union{JuMP.NonlinearExpr}, - partition_variables::Vector{JuMP.VariableRef} -) - return all(contains_only_partition_variables(arg, partition_variables) for arg in expr.args) -end - -function _nonlinear_recursion( - expr::Union{JuMP.GenericAffExpr, JuMP.VariableRef, JuMP.GenericQuadExpr, Number}, - partition_variables::Vector{JuMP.VariableRef}, - constants::Vector{Float64} -) - new_func, constant = _build_partitioned_expression(expr, partition_variables) - return new_func, constants -end - - -function _nonlinear_recursion( - expr::JuMP.NonlinearExpr, - partition_variables::Vector{JuMP.VariableRef}, - constants::Vector{Float64} - ) + constant = 0 if expr.head in (:+, :-) - constant = get(filter(x -> isa(x, Number), expr.args), 1, 0.0) - if expr.head == :+ + constant = get(filter(x -> isa(x, Number), new_args), 1, 0.0) + if expr.head == :- constant = -constant end - return JuMP.NonlinearExpr( - expr.head, - (_nonlinear_recursion(arg, partition_variables, constants)[1] for arg in expr.args)... - ) + constant, push!(constants, -constant) - end - if contains_only_partition_variables(expr, partition_variables) || expr.head == :* - return expr, constants - else - return 0, constants end + + return JuMP.NonlinearExpr(expr.head, new_args...) - constant, constant end From 3807421ab51b8e57bd3f876b11beae7c433a4a18 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Thu, 28 Aug 2025 13:33:32 -0400 Subject: [PATCH 20/39] Tests made with 100% code coverage. --- README.md | 4 ++ src/datatypes.jl | 10 ++-- src/psplit.jl | 110 +++++++++++++------------------------ test/constraints/psplit.jl | 106 +++++++++++++++++------------------ test/runtests.jl | 30 +++++----- 5 files changed, 110 insertions(+), 150 deletions(-) diff --git a/README.md b/README.md index 80868bd..93a7f88 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. [P-Split](https://arxiv.org/abs/2202.05198): This method reformulates each disjunct constraint into P constraints, each with a partitioned group defined by the user. This method requires that terms in the constraint be convex additively seperable with respect to each variable. The `PSplit` struct is created with the following required arguments: + + - `partition`: Partition of the variables to be split. All variables must be in exactly one partition. (e.g., The variables `x[1:4]` can be partitioned into two groups ` partition = [[x[1], x[2]], [x[3], x[4]]]`) + ## 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/datatypes.jl b/src/datatypes.jl index 471a7cc..7045029 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -384,21 +384,19 @@ struct Hull{T} <: AbstractReformulationMethod end """ - PSplit{T} <: AbstractReformulationMethod + PSplit <: AbstractReformulationMethod A type for using the p-split reformulation approach for disjunctive constraints. **Fields** -- `value::T`: epsilon value for p-split reformulations (default = `1e-6`). - `partition::Vector{Vector{V}}`: The partition of variables """ -struct PSplit{V <: JuMP.AbstractVariableRef, T} <: AbstractReformulationMethod - value::T +struct PSplit{V <: JuMP.AbstractVariableRef} <: AbstractReformulationMethod partition::Vector{Vector{V}} - function PSplit(partition::Vector{Vector{V}}; ϵ::T=1e-6) where {T<:Real, V <: JuMP.AbstractVariableRef} - new{V, T}(ϵ, partition) + function PSplit(partition::Vector{Vector{V}}) where {V <: JuMP.AbstractVariableRef} + new{V}(partition) end end diff --git a/src/psplit.jl b/src/psplit.jl index 98d19fb..1228628 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -1,12 +1,9 @@ -#TODO: If the constraint is Nonlinear -> Throw a warning -#TODO: Fix vectors in general - function _build_partitioned_expression( - expr::JuMP.GenericAffExpr, - partition_variables::Vector{JuMP.VariableRef} + expr::JuMP.AffExpr, + partition_variables::Vector{<:JuMP.AbstractVariableRef} ) constant = JuMP.constant(expr) - new_affexpr = AffExpr(0.0, Dict{JuMP.VariableRef,Float64}()) + new_affexpr = AffExpr(0.0, Dict{JuMP.AbstractVariableRef,Float64}()) for var in partition_variables add_to_expression!(new_affexpr, coefficient(expr, var), var) end @@ -14,10 +11,10 @@ function _build_partitioned_expression( end function _build_partitioned_expression( - expr::JuMP.GenericQuadExpr, - partition_variables::Vector{JuMP.VariableRef} + expr::JuMP.QuadExpr, + partition_variables::Vector{<:JuMP.AbstractVariableRef} ) - new_quadexpr = QuadExpr(0.0, Dict{JuMP.VariableRef,Float64}()) + new_quadexpr = QuadExpr(0.0, Dict{JuMP.AbstractVariableRef,Float64}()) constant = JuMP.constant(expr) for var in partition_variables add_to_expression!(new_quadexpr, get(expr.terms, JuMP.UnorderedPair(var, var), 0.0), var,var) @@ -28,37 +25,39 @@ function _build_partitioned_expression( end function _build_partitioned_expression( - expr::JuMP.VariableRef, - partition_variables::Vector{JuMP.VariableRef} + expr::JuMP.AbstractVariableRef, + partition_variables::Vector{<:JuMP.AbstractVariableRef} ) if expr in partition_variables return expr, 0 else return 0, 0 - #This does not work for things like exp(x) as exp(0) = 1 end end function _build_partitioned_expression( expr::Number, - partition_variables::Vector{JuMP.VariableRef} + partition_variables::Vector{<:JuMP.AbstractVariableRef} ) return expr, 0 end function _build_partitioned_expression( expr::JuMP.NonlinearExpr, - partition_variables::Vector{JuMP.VariableRef} + partition_variables::Vector{<:JuMP.AbstractVariableRef} ) + @warn "$expr is nonlinear and reformulation not be equivalent. + Partitioned functions must be convex additively seperarable with one constant" maxlog = 1 + new_args = Vector{Any}(undef, length(expr.args)) for i in 1:length(expr.args) new_args[i] = _build_partitioned_expression(expr.args[i], partition_variables)[1] if expr.head in (:exp, :cos, :cosh, :log, :log10, :log2) && new_args[i] == 0 return 0, 0 end - if expr.head == :/ && new_args[2] == 0 - return 0, 0 - end + end + if expr.head == :/ && new_args[2] == 0 + return 0, 0 end constant = 0 if expr.head in (:+, :-) @@ -67,15 +66,14 @@ function _build_partitioned_expression( constant = -constant end end - return JuMP.NonlinearExpr(expr.head, new_args...) - constant, constant end function _bound_auxiliary( model::JuMP.AbstractModel, - v::JuMP.VariableRef, - func::JuMP.GenericAffExpr, + v::JuMP.AbstractVariableRef, + func::JuMP.AffExpr, method::PSplit ) lower_bound = 0 @@ -98,10 +96,10 @@ end function _bound_auxiliary( model::JuMP.AbstractModel, - v::JuMP.VariableRef, - func::JuMP.VariableRef, + v::JuMP.AbstractVariableRef, + func::JuMP.AbstractVariableRef, method::PSplit -) +) if func != v lower_bound = variable_bound_info(func)[1] upper_bound = variable_bound_info(func)[2] @@ -115,11 +113,11 @@ end function _bound_auxiliary( model::JuMP.AbstractModel, - v::JuMP.VariableRef, + v::JuMP.AbstractVariableRef, func::Union{JuMP.NonlinearExpr, JuMP.QuadExpr, Number}, method::PSplit -) -#Leaving it to be bounded indirectly by the variables in the partition +) + @warn "Unable to calculate explicit bounds for auxiliary variables inside of nonlinear or quadratic expressions." maxlog = 1 end requires_variable_bound_info(method::PSplit) = true @@ -138,7 +136,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.AffExpr}, method::PSplit ) where {T, S <: _MOI.LessThan} p = length(method.partition) @@ -157,7 +155,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.AffExpr}, method::PSplit ) where {T, S <: _MOI.GreaterThan} p = length(method.partition) @@ -178,7 +176,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.AffExpr}, method::PSplit ) where {T, S <: Union{_MOI.Interval, _MOI.EqualTo}} p = length(method.partition) @@ -204,9 +202,9 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.AffExpr}, method::PSplit -) where {T, S <: Union{_MOI.Nonpositives}, R} +) where {T, S <: _MOI.Nonpositives, R} p = length(method.partition) d = con.set.dimension v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)") for i in 1:p, j in 1:d] @@ -229,9 +227,9 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.AffExpr}, method::PSplit -) where {T, S <: Union{_MOI.Nonnegatives}, R} +) where {T, S <: _MOI.Nonnegatives, R} p = length(method.partition) d = con.set.dimension v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)") for i in 1:p, j in 1:d] @@ -256,9 +254,9 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.AffExpr}, method::PSplit -) where {T, S <: Union{_MOI.Zeros}, R} +) where {T, S <: _MOI.Zeros, R} p = length(method.partition) d = con.set.dimension reform_con_np = Vector{JuMP.AbstractConstraint}(undef, p + 1) # nonpositive (≤ 0) @@ -296,61 +294,27 @@ end # Generic fallback for _build_partitioned_expression function _build_partitioned_expression( expr::Any, - ::Vector{JuMP.VariableRef} + ::Vector{<:JuMP.AbstractVariableRef} ) error("PSplit: _build_partitioned_expression not implemented for expression type $(typeof(expr)). Supported types: GenericAffExpr, GenericQuadExpr, VariableRef, Number, NonlinearExpr.") end -# Generic fallback for contains_only_partition_variables -function contains_only_partition_variables( - expr::Any, - ::Vector{JuMP.VariableRef} -) - error("PSplit: contains_only_partition_variables not implemented for expression type $(typeof(expr)). Supported types: GenericAffExpr, GenericQuadExpr, VariableRef, Number, NonlinearExpr.") -end - -# Generic fallback for _nonlinear_recursion -function _nonlinear_recursion( - expr::Any, - ::Vector{JuMP.VariableRef} -) - error("PSplit: _nonlinear_recursion not implemented for expression type $(typeof(expr)). Supported types: GenericAffExpr, GenericQuadExpr, VariableRef, Number, NonlinearExpr.") -end - # Generic fallback for _bound_auxiliary function _bound_auxiliary( ::JuMP.AbstractModel, - v::JuMP.VariableRef, + v::JuMP.AbstractVariableRef, func::Any, ::PSplit ) - @warn "PSplit: _bound_auxiliary not implemented for function type $(typeof(func)). Auxiliary variable bounds may be suboptimal. Supported types: GenericAffExpr, VariableRef." - # Set default bounds to avoid errors - JuMP.set_lower_bound(v, -1e6) - JuMP.set_upper_bound(v, 1e6) -end - -# Generic fallback for reformulate_disjunct_constraint (scalar) -function reformulate_disjunct_constraint( - ::JuMP.AbstractModel, - con::Any, - ::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, - ::PSplit -) - error("PSplit: reformulate_disjunct_constraint not implemented for constraint set type $(typeof(con.set)). Supported types: LessThan, GreaterThan, EqualTo, Interval.") + error("PSplit: _bound_auxiliary not implemented for function type $(typeof(func)). Auxiliary variable bounds may be suboptimal. Supported types: GenericAffExpr, VariableRef.") end # Generic fallback for reformulate_disjunct_constraint (vector) function reformulate_disjunct_constraint( ::JuMP.AbstractModel, con::Any, - ::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + ::Union{JuMP.AbstractVariableRef, JuMP.AffExpr}, ::PSplit ) error("PSplit: reformulate_disjunct_constraint not implemented for vector constraint set type $(typeof(con)). Supported types: VectorConstraint of _MOI.Nonnegatives, _MOI.Nonpositives, _MOI.Zeros.") -end - -# Generic fallback for _set_values -function _set_values(set::Any) - error("PSplit: _set_values not implemented for constraint set type $(typeof(set)). Supported types: EqualTo, Interval.") end \ No newline at end of file diff --git a/test/constraints/psplit.jl b/test/constraints/psplit.jl index 5d64e58..afc6724 100644 --- a/test/constraints/psplit.jl +++ b/test/constraints/psplit.jl @@ -1,72 +1,53 @@ -function test_psplit() - model = JuMP.Model() +function _test_psplit() + model = GDPModel() @variable(model, x[1:4]) method = PSplit([[x[1], x[2]], [x[3], x[4]]]) @test method.partition == [[x[1], x[2]], [x[3], x[4]]] - #Throw error when partition isnt set up! + # Throw error when partition isnt set up! end -#= -TODO: Test Plan - -_build_partitioned_expression: 6 tests (DONE) -contains_only_partition_variables: 5 tests (2 for union + 3 others) -_nonlinear_recursion: 5 tests (4 for union + 1 other) -_bound_auxiliary: 6 tests (3 for union + 3 others) (DONE) -reformulate_disjunct_constraint: 8 tests (2 for Interval/EqualTo + 3 for vector sets + 3 others) -Utility functions: 2 tests -=# - -function test_build_partitioned_expression() + +function _test_build_partitioned_expression() @variable(JuMP.Model(), x[1:4]) partition_variables = [x[1], x[4]] # Build each type of expression - nonlinear = exp(x[1]) + nonlinear = exp(x[1]) + 1/x[2] - exp(x[2]) affexpr = 1.0 * x[1] - 2.0 * x[2] # Simple way to create AffExpr quadexpr = x[1] * x[1] + 2.0 * x[1] * x[1] + 3.0 * x[2] * x[2] # Simple way to create QuadExpr var = x[3] num = 4.0 + result_nl, constant_nl = DP._build_partitioned_expression(nonlinear, partition_variables) + + # println(typeof(result_nl)) + # @test result_nl == exp(x[1]) + @test constant_nl == 0 + + + #TODO: Can improve how nl works. + #P-Split Reformulation: Test Failed at C:\Users\LocalAdmin\Code\DisjunctiveProgramming.jl\test\constraints\psplit.jl:29 + #Expression: result_nl == exp(x[1]) + #Evaluated: ((exp(x[1]) - 0.0) + 0) - 0.0 == exp(x[1]) - con = JuMP.build_constraint(error, nonlinear, MOI.EqualTo(0.0)) - result_nl, rhs_nl = DP._build_partitioned_expression(nonlinear, partition_variables) - #Why can't I test with an equivalent expression? - #Example output - #P-Split Reformulation: Test Failed at c:\Users\LocalAdmin\Code\DisjunctiveProgramming.jl\test\constraints\psplit.jl:33 - #Expression: result_nl == NonlinearExpr(:exp, Any[x[1]]) - #Evaluated: exp(x[1]) == exp(x[1]) - - - println(typeof(result_nl)) - # @test JuMP.value(result_nl, 1) == exp(x[1]) - @test rhs_nl == 0 - - result_aff, rhs_aff = DP._build_partitioned_expression(affexpr, partition_variables) + result_aff, constant_aff = DP._build_partitioned_expression(affexpr, partition_variables) @test result_aff == 1.0 * x[1] - @test rhs_aff == 0 + @test constant_aff == 0 - result_quad, rhs_quad = DP._build_partitioned_expression(quadexpr, partition_variables) + result_quad, constant_quad = DP._build_partitioned_expression(quadexpr, partition_variables) @test result_quad == x[1] * x[1] + 2.0 * x[1] * x[1] - @test rhs_quad == 0 + @test constant_quad == 0 @test DP._build_partitioned_expression(var, partition_variables) == (0, 0) @test DP._build_partitioned_expression(num, partition_variables) == (num, 0) - @test_throws ErrorException DP._build_partitioned_expression(JuMP.NonlinearExpr(:+, [affexpr, quadexpr]), partition_variables) -end - -function _test_contains_only_partition_variables() - @variable(JuMP.Model(), x[1:4]) - - #TODO:Come back to this after fixing NLE - #TODO: GenericAffExpr, GenericQuadExpr, GenericNonlinearExpr, VariableRef, Number, ErrorException + @test_throws ErrorException DP._build_partitioned_expression("JuMP.NonlinearExpr(:+, [affexpr, quadexpr])", partition_variables) end function _test_bound_auxiliary() model = GDPModel() # model = JuMP.Model() @variable(model, 0 <= x[1:4] <= 3) - @variable(model, v[1:5]) + @variable(model, v[1:6]) method = PSplit([[x[1], x[2]], [x[3], x[4]]]) - nonlinear = JuMP.@expression(model, exp(x[1])) + nonlinear = JuMP.@expression(model, exp(x[1]) + 1/x[4]) affexpr = 1.0 * x[1] - 2.0 * x[2] quadexpr = x[1] * x[1] + 2.0 * x[1] * x[1] + 3.0 * x[2] * x[2] var = x[3] @@ -81,7 +62,10 @@ function _test_bound_auxiliary() DP._bound_auxiliary(model, v[3], num, method) DP._bound_auxiliary(model, v[4], var, method) DP._bound_auxiliary(model, v[5], affexpr, method) - + DP._bound_auxiliary(model, v[6], v[6], method) + + @test JuMP.lower_bound(v[6]) == 0 + @test JuMP.upper_bound(v[6]) == 0 @test JuMP.lower_bound(v[5]) == -6 @test JuMP.upper_bound(v[5]) == 3 @test JuMP.lower_bound(v[4]) == 0 @@ -90,7 +74,7 @@ function _test_bound_auxiliary() @test !JuMP.has_lower_bound(v[i]) == true @test !JuMP.has_upper_bound(v[i]) == true end - + @test_throws ErrorException DP._bound_auxiliary(model, v[1], "JuMP.NonlinearExpr(:+, [affexpr, quadexpr])", method) end function _test_reformulate_disjunct_constraint_affexpr() @@ -101,13 +85,13 @@ function _test_reformulate_disjunct_constraint_affexpr() for i in 1:4 DP._variable_bounds(model)[x[i]] = DP.set_variable_bound_info(x[i], method) end - + lt = JuMP.constraint_object(@constraint(model, x[1] + x[3] <= 1)) gt = JuMP.constraint_object(@constraint(model, x[2] + x[4] >= 1)) eq = JuMP.constraint_object(@constraint(model, x[3] + x[4] == 1)) interval = JuMP.constraint_object(@constraint(model, 0 <= x[1] + x[2] + x[3] <= 0.5)) nn = JuMP.constraint_object(@constraint(model, x .- 1 >= 0)) - np = JuMP.constraint_object(@constraint(model, x .+ 1 >= 0)) + np = JuMP.constraint_object(@constraint(model, -x .+ 1 <= 0)) zeros = JuMP.constraint_object(@constraint(model, 5x .- 1 == 0)) ref_lt = reformulate_disjunct_constraint(model, lt, y[1], method) ref_gt = reformulate_disjunct_constraint(model, gt, y[2], method) @@ -156,10 +140,10 @@ function _test_reformulate_disjunct_constraint_affexpr() -variable_by_name(model, "v_$(hash(np))_2_2"), -x[3] - variable_by_name(model, "v_$(hash(np))_2_3"), -x[4] - variable_by_name(model, "v_$(hash(np))_2_4")] - @test ref_np[3].func == [variable_by_name(model, "v_$(hash(np))_1_1")*y[2] + variable_by_name(model, "v_$(hash(np))_2_1")*y[2] - y[2], - variable_by_name(model, "v_$(hash(np))_1_2")*y[2] + variable_by_name(model, "v_$(hash(np))_2_2")*y[2] - y[2], - variable_by_name(model, "v_$(hash(np))_1_3")*y[2] + variable_by_name(model, "v_$(hash(np))_2_3")*y[2] - y[2], - variable_by_name(model, "v_$(hash(np))_1_4")*y[2] + variable_by_name(model, "v_$(hash(np))_2_4")*y[2] - y[2]] + @test ref_np[3].func == [variable_by_name(model, "v_$(hash(np))_1_1")*y[2] + variable_by_name(model, "v_$(hash(np))_2_1")*y[2] + y[2], + variable_by_name(model, "v_$(hash(np))_1_2")*y[2] + variable_by_name(model, "v_$(hash(np))_2_2")*y[2] + y[2], + variable_by_name(model, "v_$(hash(np))_1_3")*y[2] + variable_by_name(model, "v_$(hash(np))_2_3")*y[2] + y[2], + variable_by_name(model, "v_$(hash(np))_1_4")*y[2] + variable_by_name(model, "v_$(hash(np))_2_4")*y[2] + y[2]] @test ref_zeros[1].func == [5x[1] - variable_by_name(model, "v_$(hash(zeros))_1_1_1"), 5x[2] - variable_by_name(model, "v_$(hash(zeros))_1_2_1"), @@ -174,13 +158,23 @@ function _test_reformulate_disjunct_constraint_affexpr() variable_by_name(model, "v_$(hash(zeros))_1_2_1")*y[1] + variable_by_name(model, "v_$(hash(zeros))_2_2_1")*y[1] - y[1], variable_by_name(model, "v_$(hash(zeros))_1_3_1")*y[1] + variable_by_name(model, "v_$(hash(zeros))_2_3_1")*y[1] - y[1], variable_by_name(model, "v_$(hash(zeros))_1_4_1")*y[1] + variable_by_name(model, "v_$(hash(zeros))_2_4_1")*y[1] - y[1]] - + @test_throws ErrorException reformulate_disjunct_constraint(model, "JuMP.VectorConstraint", y[1], method) end +function _test_set_variable_bound_info() + model = GDPModel() + @variable(model, x) + + @test_throws ErrorException DP.set_variable_bound_info(x, PSplit([[x]])) + JuMP.set_lower_bound(x, 0.0) + JuMP.set_upper_bound(x, 3.0) + @test DP.set_variable_bound_info(x, PSplit([[x]])) == (0, 3) +end + @testset "P-Split Reformulation" begin - # test_psplit() - # test_build_partitioned_expression() - # _test_contains_only_partition_variables() - # _test_bound_auxiliary() + _test_psplit() + _test_build_partitioned_expression() + _test_bound_auxiliary() _test_reformulate_disjunct_constraint_affexpr() + _test_set_variable_bound_info() end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 650c989..e5ad4b7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,19 +5,19 @@ using Test include("utilities.jl") # RUN ALL THE TESTS -# include("aqua.jl") -# include("model.jl") -# include("jump.jl") -# include("variables/query.jl") -# include("variables/logical.jl") -# include("constraints/selector.jl") -# include("constraints/proposition.jl") -# include("constraints/disjunct.jl") -# include("constraints/indicator.jl") -# include("constraints/bigm.jl") +include("aqua.jl") +include("model.jl") +include("jump.jl") +include("variables/query.jl") +include("variables/logical.jl") +include("constraints/selector.jl") +include("constraints/proposition.jl") +include("constraints/disjunct.jl") +include("constraints/indicator.jl") +include("constraints/bigm.jl") include("constraints/psplit.jl") -# include("constraints/hull.jl") -# include("constraints/fallback.jl") -# include("constraints/disjunction.jl") -# include("print.jl") -# include("solve.jl") +include("constraints/hull.jl") +include("constraints/fallback.jl") +include("constraints/disjunction.jl") +include("print.jl") +include("solve.jl") \ No newline at end of file From 7ce16a33ee8a9bec1695345a8906f2a906fcc4c4 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Thu, 2 Oct 2025 17:32:10 -0400 Subject: [PATCH 21/39] p-split tests, generalized datatypes and mentions to 0, styling for broken lines and 80 character limit --- Project.toml | 6 +- src/psplit.jl | 364 ++++++++++++++++++++++++------------- test/constraints/psplit.jl | 300 +++++++++++++++++++----------- test/solve.jl | 77 +++++++- 4 files changed, 504 insertions(+), 243 deletions(-) diff --git a/Project.toml b/Project.toml index c27f5dc..cbd9df2 100644 --- a/Project.toml +++ b/Project.toml @@ -12,11 +12,15 @@ Aqua = "0.8" JuMP = "1.18" Reexport = "1" julia = "1.6" +Juniper = "0.9.3" +Ipopt = "1.11.0" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" +Juniper = "2ddba703-00a4-53a7-87a5-e8b9971dde84" [targets] -test = ["Aqua", "HiGHS", "Test"] +test = ["Aqua", "HiGHS", "Test", "Juniper", "Ipopt"] diff --git a/src/psplit.jl b/src/psplit.jl index 1228628..cbd93a3 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -1,130 +1,176 @@ +################################################################################ +# BUILD PARTITIONED EXPRESSION +################################################################################ + function _build_partitioned_expression( - expr::JuMP.AffExpr, + expr::T, partition_variables::Vector{<:JuMP.AbstractVariableRef} -) +) where {T <: JuMP.GenericAffExpr} constant = JuMP.constant(expr) - new_affexpr = AffExpr(0.0, Dict{JuMP.AbstractVariableRef,Float64}()) + new_affexpr = zero(T) for var in partition_variables - add_to_expression!(new_affexpr, coefficient(expr, var), var) + JuMP.add_to_expression!(new_affexpr, coefficient(expr, var), var) end return new_affexpr, constant end function _build_partitioned_expression( - expr::JuMP.QuadExpr, + expr::T, partition_variables::Vector{<:JuMP.AbstractVariableRef} -) - new_quadexpr = QuadExpr(0.0, Dict{JuMP.AbstractVariableRef,Float64}()) +) where {T <: JuMP.GenericQuadExpr} + new_quadexpr = zero(T) constant = JuMP.constant(expr) for var in partition_variables - add_to_expression!(new_quadexpr, get(expr.terms, JuMP.UnorderedPair(var, var), 0.0), var,var) - add_to_expression!(new_quadexpr, coefficient(expr, var), var) + for (pair, coeff) in expr.terms + if pair.a == var && pair.b == var + JuMP.add_to_expression!(new_quadexpr, coeff, var, var) + end + end end - + new_aff, _ = _build_partitioned_expression(expr.aff, partition_variables) + JuMP.add_to_expression!(new_quadexpr, new_aff) return new_quadexpr, constant end function _build_partitioned_expression( - expr::JuMP.AbstractVariableRef, + expr::T, partition_variables::Vector{<:JuMP.AbstractVariableRef} -) +) where {T <: JuMP.AbstractVariableRef} + if expr in partition_variables - return expr, 0 + return expr, zero(T) else - return 0, 0 + return zero(T), zero(T) end end function _build_partitioned_expression( - expr::Number, + expr::T, partition_variables::Vector{<:JuMP.AbstractVariableRef} -) - return expr, 0 +) where {T <: Number} + return expr, zero(T) end -function _build_partitioned_expression( - expr::JuMP.NonlinearExpr, - partition_variables::Vector{<:JuMP.AbstractVariableRef} -) - @warn "$expr is nonlinear and reformulation not be equivalent. - Partitioned functions must be convex additively seperarable with one constant" maxlog = 1 - - new_args = Vector{Any}(undef, length(expr.args)) - for i in 1:length(expr.args) - new_args[i] = _build_partitioned_expression(expr.args[i], partition_variables)[1] - if expr.head in (:exp, :cos, :cosh, :log, :log10, :log2) && new_args[i] == 0 - return 0, 0 - end - end - if expr.head == :/ && new_args[2] == 0 - return 0, 0 - end - constant = 0 - if expr.head in (:+, :-) - constant = get(filter(x -> isa(x, Number), new_args), 1, 0.0) - if expr.head == :- - constant = -constant +################################################################################ +# BOUND AUXILIARY +################################################################################ +function _bound_auxiliary( + model::JuMP.AbstractModel, + v::JuMP.AbstractVariableRef, + func::JuMP.GenericAffExpr, + method::PSplit +) + T = JuMP.value_type(typeof(model)) + lower_bound = zero(T) + upper_bound = zero(T) + for (var, coeff) in func.terms + if var != v + JuMP.is_binary(var) && continue + var_lb, var_ub = variable_bound_info(var) + if coeff > 0.0 + lower_bound += coeff * var_lb + upper_bound += coeff * var_ub + else + lower_bound += coeff * var_ub + upper_bound += coeff * var_lb + end end end - return JuMP.NonlinearExpr(expr.head, new_args...) - constant, constant -end + JuMP.set_lower_bound(v, lower_bound) + JuMP.set_upper_bound(v, upper_bound) +end +function _bound_auxiliary( + model::JuMP.AbstractModel, + v::JuMP.AbstractVariableRef, + func::Number, + method::PSplit +) + #Do nothing? +end function _bound_auxiliary( model::JuMP.AbstractModel, v::JuMP.AbstractVariableRef, - func::JuMP.AffExpr, + func::JuMP.GenericQuadExpr, method::PSplit -) - lower_bound = 0 - upper_bound = 0 - for (var, coeff) in func.terms +) + T = JuMP.value_type(typeof(model)) + lower_bound = zero(T) + upper_bound = zero(T) + + # Handle linear terms + for (var, coeff) in func.aff.terms + if var != v + JuMP.is_binary(var) && continue + var_lb, var_ub = variable_bound_info(var) + if coeff > 0.0 + lower_bound += coeff * var_lb + upper_bound += coeff * var_ub + else + lower_bound += coeff * var_ub + upper_bound += coeff * var_lb + end + end + end + + # Handle quadratic terms + for (vars, coeff) in func.terms + var = vars.a if var != v JuMP.is_binary(var) && continue - if coeff > 0 - lower_bound += coeff * variable_bound_info(var)[1] - upper_bound += coeff * variable_bound_info(var)[2] + lb, ub = variable_bound_info(var) + + # For x^2 terms + sq_min = min(lb^2, ub^2, zero(T)) + sq_max = max(lb^2, ub^2, zero(T)) + + if coeff > 0.0 + lower_bound += coeff * sq_min + upper_bound += coeff * sq_max else - lower_bound += coeff * variable_bound_info(var)[2] - upper_bound += coeff * variable_bound_info(var)[1] + lower_bound += coeff * sq_max + upper_bound += coeff * sq_min end end end + + # Add constant term + const_term = func.aff.constant + lower_bound += const_term + upper_bound += const_term + JuMP.set_lower_bound(v, lower_bound) JuMP.set_upper_bound(v, upper_bound) -end +end function _bound_auxiliary( model::JuMP.AbstractModel, v::JuMP.AbstractVariableRef, func::JuMP.AbstractVariableRef, method::PSplit -) +) + T = JuMP.value_type(typeof(model)) + lower_bound = zero(T) + upper_bound = zero(T) if func != v lower_bound = variable_bound_info(func)[1] upper_bound = variable_bound_info(func)[2] JuMP.set_lower_bound(v, lower_bound) JuMP.set_upper_bound(v, upper_bound) else - JuMP.set_lower_bound(v,0) - JuMP.set_upper_bound(v,0) + JuMP.set_lower_bound(v,lower_bound) + JuMP.set_upper_bound(v,upper_bound) end end -function _bound_auxiliary( - model::JuMP.AbstractModel, - v::JuMP.AbstractVariableRef, - func::Union{JuMP.NonlinearExpr, JuMP.QuadExpr, Number}, - method::PSplit -) - @warn "Unable to calculate explicit bounds for auxiliary variables inside of nonlinear or quadratic expressions." maxlog = 1 -end - requires_variable_bound_info(method::PSplit) = true function set_variable_bound_info(vref::JuMP.AbstractVariableRef, ::PSplit) if !has_lower_bound(vref) || !has_upper_bound(vref) - error("Variable $vref must have both lower and upper bounds defined when using the PSplit reformulation.") + error("Variable $vref must have both lower and upper bounds defined when + using the PSplit reformulation." + ) else lb = lower_bound(vref) ub = upper_bound(vref) @@ -132,53 +178,95 @@ function set_variable_bound_info(vref::JuMP.AbstractVariableRef, ::PSplit) return lb, ub end -#DONE WITH NL +################################################################################ +# REFORMULATE DISJUNCT +################################################################################ +function _reformulate_disjunct( + model::JuMP.AbstractModel, + ref_cons::Vector{JuMP.AbstractConstraint}, + lvref::LogicalVariableRef, + method::PSplit + ) + #reformulate each constraint and add to the model + 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 = JuMP.constraint_object(cref) + if !(con isa Disjunction) + if con.func isa JuMP.GenericNonlinearExpr + error("Nonlinear constraints are not supported + by the PSplit reformulation.") + elseif con.func isa JuMP.GenericQuadExpr + quadexpr = con.func + all_same_variable = all(pair -> first(pair).a == first(pair).b, quadexpr.terms) + !all_same_variable && error("PSplit reformulation only supports + quadratic constraints where all terms have the same variables.") + end + end + append!(ref_cons, reformulate_disjunct_constraint(model, con, bvref, method)) + end + return +end + +################################################################################ +# REFORMULATE DISJUNCT CONSTRAINT +################################################################################ function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bvref::Union{JuMP.AbstractVariableRef, JuMP.AffExpr}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::PSplit ) where {T, S <: _MOI.LessThan} + val_type = JuMP.value_type(typeof(model)) p = length(method.partition) v = [@variable(model, base_name = "v_$(hash(con))_$(i)") for i in 1:p] _, constant = _build_partitioned_expression(con.func, method.partition[p]) reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) for i in 1:p func, _ = _build_partitioned_expression(con.func, method.partition[i]) - reform_con[i] = JuMP.build_constraint(error, func - v[i], MOI.LessThan(0.0)) + reform_con[i] = JuMP.build_constraint(error, func - v[i], + MOI.LessThan(zero(val_type)) + ) _bound_auxiliary(model, v[i], func, method) end - reform_con[end] = JuMP.build_constraint(error, sum(v[i] * bvref for i in 1:p) - (con.set.upper - constant) * bvref, MOI.LessThan(0.0)) + reform_con[end] = JuMP.build_constraint(error, sum(v[i]*bvref for i in 1:p) + - (con.set.upper - constant) * bvref, MOI.LessThan(zero(val_type)) + ) return reform_con end -#DONE WITH NL + function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bvref::Union{JuMP.AbstractVariableRef, JuMP.AffExpr}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::PSplit ) where {T, S <: _MOI.GreaterThan} + val_type = JuMP.value_type(typeof(model)) p = length(method.partition) reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) v = [@variable(model, base_name = "v_$(hash(con))_$(i)") for i in 1:p] _, constant = _build_partitioned_expression(con.func, method.partition[p]) - for i in 1:p func, _ = _build_partitioned_expression(con.func, method.partition[i]) - reform_con[i] = JuMP.build_constraint(error, -func - v[i], MOI.LessThan(0.0)) + reform_con[i] = JuMP.build_constraint(error, -func - v[i], + MOI.LessThan(zero(val_type)) + ) _bound_auxiliary(model, v[i], -func, method) end - reform_con[end] = JuMP.build_constraint(error, sum(v[i] * bvref for i in 1:p) - (-con.set.lower + constant) * bvref, MOI.LessThan(0.0)) + reform_con[end] = JuMP.build_constraint(error, sum(v[i]*bvref for i in 1:p) + - (-con.set.lower + constant) * bvref, MOI.LessThan(zero(val_type)) + ) return reform_con end -#Works with NL function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bvref::Union{JuMP.AbstractVariableRef, JuMP.AffExpr}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::PSplit ) where {T, S <: Union{_MOI.Interval, _MOI.EqualTo}} + val_type = JuMP.value_type(typeof(model)) p = length(method.partition) reform_con_lt = Vector{JuMP.AbstractConstraint}(undef, p + 1) reform_con_gt = Vector{JuMP.AbstractConstraint}(undef, p + 1) @@ -187,22 +275,31 @@ function reformulate_disjunct_constraint( v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)") for i in 1:p, j in 1:2] for i in 1:p func, _= _build_partitioned_expression(con.func, method.partition[i]) - reform_con_lt[i] = JuMP.build_constraint(error, func - v[i,1], MOI.LessThan(0.0)) - reform_con_gt[i] = JuMP.build_constraint(error, -func - v[i,2], MOI.LessThan(0.0)) + reform_con_lt[i] = JuMP.build_constraint(error, + func - v[i,1], MOI.LessThan(zero(val_type)) + ) + reform_con_gt[i] = JuMP.build_constraint(error, + -func - v[i,2], MOI.LessThan(zero(val_type)) + ) _bound_auxiliary(model, v[i,1], func, method) _bound_auxiliary(model, v[i,2], -func, method) end set_values = _set_values(con.set) - reform_con_lt[end] = JuMP.build_constraint(error, sum(v[i,1] * bvref for i in 1:p) - (set_values[2] - constant) * bvref, MOI.LessThan(0.0)) - reform_con_gt[end] = JuMP.build_constraint(error, sum(v[i,2] * bvref for i in 1:p) - (-set_values[1] + constant) * bvref, MOI.LessThan(0.0)) - #TODO: how do i avoid the vcat? + reform_con_lt[end] = JuMP.build_constraint(error, sum(v[i,1]*bvref + for i in 1:p) - (set_values[2] - constant) * bvref, + MOI.LessThan(zero(val_type)) + ) + reform_con_gt[end] = JuMP.build_constraint(error, sum(v[i,2]*bvref + for i in 1:p) - (-set_values[1] + constant) * bvref, + MOI.LessThan(zero(val_type)) + ) return vcat(reform_con_lt, reform_con_gt) end #Functions function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bvref::Union{JuMP.AbstractVariableRef, JuMP.AffExpr}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::PSplit ) where {T, S <: _MOI.Nonpositives, R} p = length(method.partition) @@ -211,15 +308,21 @@ function reformulate_disjunct_constraint( reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) constants = Vector{Number}(undef, d) for i in 1:p - partitioned_expressions = [_build_partitioned_expression(con.func[j], method.partition[i]) for j in 1:d] - func = JuMP.@expression(model, [j = 1:d], partitioned_expressions[j][1]) - constants .= [partitioned_expressions[j][2] for j in 1:d] - reform_con[i] = JuMP.build_constraint(error, func - v[i,:], _MOI.Nonpositives(d)) + part_expr = [_build_partitioned_expression(con.func[j], + method.partition[i]) for j in 1:d + ] + func = JuMP.@expression(model, [j = 1:d], part_expr[j][1]) + constants .= [part_expr[j][2] for j in 1:d] + reform_con[i] = JuMP.build_constraint(error, + func - v[i,:], _MOI.Nonpositives(d) + ) for j in 1:d _bound_auxiliary(model, v[i,j], func[j], method) end end - new_func = JuMP.@expression(model,[j = 1:d], sum(v[i,j] * bvref for i in 1:p) + constants[j]*bvref) + new_func = JuMP.@expression(model,[j = 1:d], + sum(v[i,j] * bvref for i in 1:p) + constants[j]*bvref + ) reform_con[end] = JuMP.build_constraint(error, new_func, _MOI.Nonpositives(d)) return vcat(reform_con) end @@ -227,7 +330,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bvref::Union{JuMP.AbstractVariableRef, JuMP.AffExpr}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::PSplit ) where {T, S <: _MOI.Nonnegatives, R} p = length(method.partition) @@ -236,54 +339,65 @@ function reformulate_disjunct_constraint( reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) constants = Vector{Number}(undef, d) for i in 1:p - #I should be subtracting the constant here. - partitioned_expressions = [_build_partitioned_expression(con.func[j], method.partition[i]) for j in 1:d] - func = JuMP.@expression(model, [j = 1:d], -partitioned_expressions[j][1]) - constants .= [-partitioned_expressions[j][2] for j in 1:d] + part_expr = [_build_partitioned_expression(con.func[j], method.partition[i]) for j in 1:d] + func = JuMP.@expression(model, [j = 1:d], -part_expr[j][1]) + constants .= [-part_expr[j][2] for j in 1:d] reform_con[i] = JuMP.build_constraint(error, func - v[i,:], _MOI.Nonpositives(d)) for j in 1:d _bound_auxiliary(model, v[i,j], func[j], method) end end - new_func = JuMP.@expression(model,[j = 1:d], sum(v[i,j] * bvref for i in 1:p) + constants[j]*bvref) - reform_con[end] = JuMP.build_constraint(error, new_func, _MOI.Nonpositives(d)) - #TODO: how do i avoid the vcat? + new_func = JuMP.@expression(model,[j = 1:d], + sum(v[i,j] * bvref for i in 1:p) + constants[j]*bvref + ) + reform_con[end] = JuMP.build_constraint(error,new_func,_MOI.Nonpositives(d)) return vcat(reform_con) end -#TODO: + function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bvref::Union{JuMP.AbstractVariableRef, JuMP.AffExpr}, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::PSplit ) where {T, S <: _MOI.Zeros, R} p = length(method.partition) d = con.set.dimension reform_con_np = Vector{JuMP.AbstractConstraint}(undef, p + 1) # nonpositive (≤ 0) reform_con_nn = Vector{JuMP.AbstractConstraint}(undef, p + 1) # nonnegative (≥ 0) - v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)_$(k)") for i in 1:p, j in 1:d, k in 1:2] + v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)_$(k)") + for i in 1:p, j in 1:d, k in 1:2 + ] constants = Vector{Number}(undef, d) for i in 1:p - partitioned_expressions = [_build_partitioned_expression(con.func[j], method.partition[i]) for j in 1:d] - - # Nonpositive part: func ≤ 0 → func - v ≤ 0 - func = JuMP.@expression(model, [j = 1:d], partitioned_expressions[j][1]) - constants .= [partitioned_expressions[j][2] for j in 1:d] - - reform_con_np[i] = JuMP.build_constraint(error, func - v[i,:,1], _MOI.Nonpositives(d)) - reform_con_nn[i] = JuMP.build_constraint(error, -func - v[i,:,2], _MOI.Nonpositives(d)) - + part_expr = [ + _build_partitioned_expression(con.func[j], method.partition[i]) + for j in 1:d + ] + func = JuMP.@expression(model, [j = 1:d], part_expr[j][1]) + constants .= [part_expr[j][2] for j in 1:d] + reform_con_np[i] = JuMP.build_constraint(error, + func - v[i,:,1], _MOI.Nonpositives(d) + ) + reform_con_nn[i] = JuMP.build_constraint(error, + -func - v[i,:,2], _MOI.Nonpositives(d) + ) for j in 1:d _bound_auxiliary(model, v[i,j,1], func[j], method) _bound_auxiliary(model, v[i,j,2], -func[j], method) end end - - # Final constraints: combine auxiliary variables with constants - new_func_np = JuMP.@expression(model,[j = 1:d], sum(v[i,j,1] * bvref for i in 1:p) + constants[j]*bvref) - new_func_nn = JuMP.@expression(model,[j = 1:d], -sum(v[i,j,2] * bvref for i in 1:p) - constants[j]*bvref) - reform_con_np[end] = JuMP.build_constraint(error, new_func_np, _MOI.Nonpositives(d)) - reform_con_nn[end] = JuMP.build_constraint(error, new_func_nn, _MOI.Nonpositives(d)) + new_func_np = JuMP.@expression(model,[j = 1:d], + sum(v[i,j,1] * bvref for i in 1:p) + constants[j]*bvref + ) + new_func_nn = JuMP.@expression(model,[j = 1:d], + -sum(v[i,j,2] * bvref for i in 1:p) - constants[j]*bvref + ) + reform_con_np[end] = JuMP.build_constraint(error, + new_func_np, _MOI.Nonpositives(d) + ) + reform_con_nn[end] = JuMP.build_constraint(error, + new_func_nn, _MOI.Nonpositives(d) + ) return vcat(reform_con_np, reform_con_nn) end @@ -293,28 +407,20 @@ end # Generic fallback for _build_partitioned_expression function _build_partitioned_expression( - expr::Any, + expr::F, ::Vector{<:JuMP.AbstractVariableRef} -) - error("PSplit: _build_partitioned_expression not implemented for expression type $(typeof(expr)). Supported types: GenericAffExpr, GenericQuadExpr, VariableRef, Number, NonlinearExpr.") +) where F + error("PSplit: _build_partitioned_expression not implemented + for expression type $F.") end # Generic fallback for _bound_auxiliary function _bound_auxiliary( ::JuMP.AbstractModel, v::JuMP.AbstractVariableRef, - func::Any, - ::PSplit -) - error("PSplit: _bound_auxiliary not implemented for function type $(typeof(func)). Auxiliary variable bounds may be suboptimal. Supported types: GenericAffExpr, VariableRef.") -end - -# Generic fallback for reformulate_disjunct_constraint (vector) -function reformulate_disjunct_constraint( - ::JuMP.AbstractModel, - con::Any, - ::Union{JuMP.AbstractVariableRef, JuMP.AffExpr}, + func::F, ::PSplit -) - error("PSplit: reformulate_disjunct_constraint not implemented for vector constraint set type $(typeof(con)). Supported types: VectorConstraint of _MOI.Nonnegatives, _MOI.Nonpositives, _MOI.Zeros.") +) where F + error("PSplit: _bound_auxiliary not implemented for function + type $F.") end \ No newline at end of file diff --git a/test/constraints/psplit.jl b/test/constraints/psplit.jl index afc6724..f20938c 100644 --- a/test/constraints/psplit.jl +++ b/test/constraints/psplit.jl @@ -1,4 +1,4 @@ -function _test_psplit() +function test_psplit() model = GDPModel() @variable(model, x[1:4]) method = PSplit([[x[1], x[2]], [x[3], x[4]]]) @@ -6,58 +6,48 @@ function _test_psplit() # Throw error when partition isnt set up! end -function _test_build_partitioned_expression() - @variable(JuMP.Model(), x[1:4]) +function test_build_partitioned_expression() + model = JuMP.Model() + @variable(model, x[1:4]) partition_variables = [x[1], x[4]] - # Build each type of expression - nonlinear = exp(x[1]) + 1/x[2] - exp(x[2]) - affexpr = 1.0 * x[1] - 2.0 * x[2] # Simple way to create AffExpr - quadexpr = x[1] * x[1] + 2.0 * x[1] * x[1] + 3.0 * x[2] * x[2] # Simple way to create QuadExpr - var = x[3] - num = 4.0 - result_nl, constant_nl = DP._build_partitioned_expression(nonlinear, partition_variables) - - # println(typeof(result_nl)) - # @test result_nl == exp(x[1]) - @test constant_nl == 0 - - - #TODO: Can improve how nl works. - #P-Split Reformulation: Test Failed at C:\Users\LocalAdmin\Code\DisjunctiveProgramming.jl\test\constraints\psplit.jl:29 - #Expression: result_nl == exp(x[1]) - #Evaluated: ((exp(x[1]) - 0.0) + 0) - 0.0 == exp(x[1]) - - result_aff, constant_aff = DP._build_partitioned_expression(affexpr, partition_variables) - @test result_aff == 1.0 * x[1] - @test constant_aff == 0 - - result_quad, constant_quad = DP._build_partitioned_expression(quadexpr, partition_variables) - @test result_quad == x[1] * x[1] + 2.0 * x[1] * x[1] - @test constant_quad == 0 - @test DP._build_partitioned_expression(var, partition_variables) == (0, 0) - @test DP._build_partitioned_expression(num, partition_variables) == (num, 0) + # Test data + test_cases = [ + (expr = 1.0 * x[1] - 2.0 * x[2], expected = (1.0 * x[1], 0.0)), + (expr = x[1] * x[1] + 2.0 * x[1] * x[1] + 3.0 * x[2] * x[2], + expected = (3 * x[1] * x[1], 0.0)), + (expr = x[3], expected = (0, 0)), + (expr = x[4], expected = (x[4], 0.0)), + (expr = 4.0, expected = (4.0, 0.0)) + ] + + # Run tests + for (i, test_case) in enumerate(test_cases) + result = DP._build_partitioned_expression(test_case.expr, partition_variables) + @test result == test_case.expected + end - @test_throws ErrorException DP._build_partitioned_expression("JuMP.NonlinearExpr(:+, [affexpr, quadexpr])", partition_variables) + # Test error case + @test_throws ErrorException DP._build_partitioned_expression( + "Bad Input", + partition_variables + ) end -function _test_bound_auxiliary() +function test_bound_auxiliary() model = GDPModel() - # model = JuMP.Model() @variable(model, 0 <= x[1:4] <= 3) @variable(model, v[1:6]) method = PSplit([[x[1], x[2]], [x[3], x[4]]]) - nonlinear = JuMP.@expression(model, exp(x[1]) + 1/x[4]) affexpr = 1.0 * x[1] - 2.0 * x[2] - quadexpr = x[1] * x[1] + 2.0 * x[1] * x[1] + 3.0 * x[2] * x[2] + quadexpr = x[1] * x[1] + 2.0 * x[1] * x[1] - 3.0 * x[2] * x[2] + x[1] - x[2] var = x[3] num = 4.0 - + nl = exp(x[1]) for i in 1:4 DP._variable_bounds(model)[x[i]] = DP.set_variable_bound_info(x[i], method) end - DP._bound_auxiliary(model, v[1], nonlinear, method) DP._bound_auxiliary(model, v[2], quadexpr, method) DP._bound_auxiliary(model, v[3], num, method) DP._bound_auxiliary(model, v[4], var, method) @@ -70,98 +60,185 @@ function _test_bound_auxiliary() @test JuMP.upper_bound(v[5]) == 3 @test JuMP.lower_bound(v[4]) == 0 @test JuMP.upper_bound(v[4]) == 3 - for i in 1:3 - @test !JuMP.has_lower_bound(v[i]) == true - @test !JuMP.has_upper_bound(v[i]) == true - end - @test_throws ErrorException DP._bound_auxiliary(model, v[1], "JuMP.NonlinearExpr(:+, [affexpr, quadexpr])", method) + @test JuMP.lower_bound(v[2]) == -30 + @test JuMP.upper_bound(v[2]) == 30 + + @test has_lower_bound(v[3]) == false + @test has_upper_bound(v[3]) == false + @test_throws ErrorException DP._bound_auxiliary(model, v[3], nl, method) end -function _test_reformulate_disjunct_constraint_affexpr() +function test_reformulate_disjunct_constraint_affexpr() model = GDPModel() @variable(model, 0 <= x[1:4] <= 3) @variable(model, y[1:2], Bin) method = PSplit([[x[1], x[2]], [x[3], x[4]]]) for i in 1:4 - DP._variable_bounds(model)[x[i]] = DP.set_variable_bound_info(x[i], method) + DP._variable_bounds(model)[x[i]] = DP.set_variable_bound_info( + x[i], + method + ) end - + lt = JuMP.constraint_object(@constraint(model, x[1] + x[3] <= 1)) gt = JuMP.constraint_object(@constraint(model, x[2] + x[4] >= 1)) eq = JuMP.constraint_object(@constraint(model, x[3] + x[4] == 1)) - interval = JuMP.constraint_object(@constraint(model, 0 <= x[1] + x[2] + x[3] <= 0.5)) + interval = JuMP.constraint_object( + @constraint(model, 0 <= x[1] + x[2] + x[3] <= 0.5) + ) nn = JuMP.constraint_object(@constraint(model, x .- 1 >= 0)) np = JuMP.constraint_object(@constraint(model, -x .+ 1 <= 0)) zeros = JuMP.constraint_object(@constraint(model, 5x .- 1 == 0)) + ref_lt = reformulate_disjunct_constraint(model, lt, y[1], method) ref_gt = reformulate_disjunct_constraint(model, gt, y[2], method) ref_eq = reformulate_disjunct_constraint(model, eq, y[1], method) - ref_interval = reformulate_disjunct_constraint(model, interval, y[2], method) + ref_interval = reformulate_disjunct_constraint( + model, interval, y[2], method + ) ref_nn = reformulate_disjunct_constraint(model, nn, y[1], method) ref_np = reformulate_disjunct_constraint(model, np, y[2], method) ref_zeros = reformulate_disjunct_constraint(model, zeros, y[1], method) - @test ref_lt[1].func == x[1] - variable_by_name(model, "v_$(hash(lt))_1") - @test ref_lt[2].func == x[3] - variable_by_name(model, "v_$(hash(lt))_2") - @test ref_lt[3].func == variable_by_name(model, "v_$(hash(lt))_1")*y[1] + variable_by_name(model, "v_$(hash(lt))_2")*y[1] - y[1] - @test ref_gt[1].func == -x[2]- variable_by_name(model, "v_$(hash(gt))_1") - @test ref_gt[2].func == -x[4]- variable_by_name(model, "v_$(hash(gt))_2") - - @test ref_eq[1].func == -variable_by_name(model, "v_$(hash(eq))_1_1") - @test ref_eq[2].func == x[3] + x[4] - variable_by_name(model, "v_$(hash(eq))_2_1") - @test ref_eq[4].func == -variable_by_name(model, "v_$(hash(eq))_1_2") - @test ref_eq[5].func == -x[3] - x[4] - variable_by_name(model, "v_$(hash(eq))_2_2") - @test ref_eq[3].func == variable_by_name(model, "v_$(hash(eq))_1_1")*y[1] + variable_by_name(model, "v_$(hash(eq))_2_1")*y[1] - y[1] - - @test ref_interval[1].func == x[1] + x[2] - variable_by_name(model, "v_$(hash(interval))_1_1") - @test ref_interval[2].func == x[3] - variable_by_name(model, "v_$(hash(interval))_2_1") - @test ref_interval[4].func == -x[1] - x[2] - variable_by_name(model, "v_$(hash(interval))_1_2") - @test ref_interval[5].func == -x[3] - variable_by_name(model, "v_$(hash(interval))_2_2") - @test ref_interval[3].func == variable_by_name(model, "v_$(hash(interval))_1_1")*y[2] + variable_by_name(model, "v_$(hash(interval))_2_1")*y[2] - 0.5*y[2] - - @test ref_nn[1].func == [-x[1] - variable_by_name(model, "v_$(hash(nn))_1_1"), - -x[2] - variable_by_name(model, "v_$(hash(nn))_1_2"), - -variable_by_name(model, "v_$(hash(nn))_1_3"), - -variable_by_name(model, "v_$(hash(nn))_1_4")] - @test ref_nn[2].func == [-variable_by_name(model, "v_$(hash(nn))_2_1"), - -variable_by_name(model, "v_$(hash(nn))_2_2"), - -x[3] - variable_by_name(model, "v_$(hash(nn))_2_3"), - -x[4] - variable_by_name(model, "v_$(hash(nn))_2_4")] - @test ref_nn[3].func == [variable_by_name(model, "v_$(hash(nn))_1_1")*y[1] + variable_by_name(model, "v_$(hash(nn))_2_1")*y[1] + y[1], - variable_by_name(model, "v_$(hash(nn))_1_2")*y[1] + variable_by_name(model, "v_$(hash(nn))_2_2")*y[1] + y[1], - variable_by_name(model, "v_$(hash(nn))_1_3")*y[1] + variable_by_name(model, "v_$(hash(nn))_2_3")*y[1] + y[1], - variable_by_name(model, "v_$(hash(nn))_1_4")*y[1] + variable_by_name(model, "v_$(hash(nn))_2_4")*y[1] + y[1]] - - @test ref_np[1].func == [-x[1] - variable_by_name(model, "v_$(hash(np))_1_1"), - -x[2] - variable_by_name(model, "v_$(hash(np))_1_2"), - -variable_by_name(model, "v_$(hash(np))_1_3"), - -variable_by_name(model, "v_$(hash(np))_1_4")] - @test ref_np[2].func == [-variable_by_name(model, "v_$(hash(np))_2_1"), - -variable_by_name(model, "v_$(hash(np))_2_2"), - -x[3] - variable_by_name(model, "v_$(hash(np))_2_3"), - -x[4] - variable_by_name(model, "v_$(hash(np))_2_4")] - @test ref_np[3].func == [variable_by_name(model, "v_$(hash(np))_1_1")*y[2] + variable_by_name(model, "v_$(hash(np))_2_1")*y[2] + y[2], - variable_by_name(model, "v_$(hash(np))_1_2")*y[2] + variable_by_name(model, "v_$(hash(np))_2_2")*y[2] + y[2], - variable_by_name(model, "v_$(hash(np))_1_3")*y[2] + variable_by_name(model, "v_$(hash(np))_2_3")*y[2] + y[2], - variable_by_name(model, "v_$(hash(np))_1_4")*y[2] + variable_by_name(model, "v_$(hash(np))_2_4")*y[2] + y[2]] - - @test ref_zeros[1].func == [5x[1] - variable_by_name(model, "v_$(hash(zeros))_1_1_1"), - 5x[2] - variable_by_name(model, "v_$(hash(zeros))_1_2_1"), - -variable_by_name(model, "v_$(hash(zeros))_1_3_1"), - -variable_by_name(model, "v_$(hash(zeros))_1_4_1")] - @test ref_zeros[2].func == [-variable_by_name(model, "v_$(hash(zeros))_2_1_1"), - -variable_by_name(model, "v_$(hash(zeros))_2_2_1"), - 5x[3] - variable_by_name(model, "v_$(hash(zeros))_2_3_1"), - 5x[4] - variable_by_name(model, "v_$(hash(zeros))_2_4_1")] - - @test ref_zeros[3].func == [variable_by_name(model, "v_$(hash(zeros))_1_1_1")*y[1] + variable_by_name(model, "v_$(hash(zeros))_2_1_1")*y[1] - y[1], - variable_by_name(model, "v_$(hash(zeros))_1_2_1")*y[1] + variable_by_name(model, "v_$(hash(zeros))_2_2_1")*y[1] - y[1], - variable_by_name(model, "v_$(hash(zeros))_1_3_1")*y[1] + variable_by_name(model, "v_$(hash(zeros))_2_3_1")*y[1] - y[1], - variable_by_name(model, "v_$(hash(zeros))_1_4_1")*y[1] + variable_by_name(model, "v_$(hash(zeros))_2_4_1")*y[1] - y[1]] - @test_throws ErrorException reformulate_disjunct_constraint(model, "JuMP.VectorConstraint", y[1], method) + @test ref_lt[1].func == x[1] - + variable_by_name(model, "v_$(hash(lt))_1") + @test ref_lt[2].func == x[3] - + variable_by_name(model, "v_$(hash(lt))_2") + @test ref_lt[3].func == + variable_by_name(model, "v_$(hash(lt))_1") * y[1] + + variable_by_name(model, "v_$(hash(lt))_2") * y[1] - + y[1] + @test ref_gt[1].func == -x[2] - + variable_by_name(model, "v_$(hash(gt))_1") + @test ref_gt[2].func == -x[4] - + variable_by_name(model, "v_$(hash(gt))_2") + + @test ref_eq[1].func == + -variable_by_name(model, "v_$(hash(eq))_1_1") + @test ref_eq[2].func == x[3] + x[4] - + variable_by_name(model, "v_$(hash(eq))_2_1") + @test ref_eq[4].func == + -variable_by_name(model, "v_$(hash(eq))_1_2") + @test ref_eq[5].func == -x[3] - x[4] - + variable_by_name(model, "v_$(hash(eq))_2_2") + @test ref_eq[3].func == + variable_by_name(model, "v_$(hash(eq))_1_1") * y[1] + + variable_by_name(model, "v_$(hash(eq))_2_1") * y[1] - + y[1] + + @test ref_interval[1].func == x[1] + x[2] - + variable_by_name(model, "v_$(hash(interval))_1_1") + @test ref_interval[2].func == x[3] - + variable_by_name(model, "v_$(hash(interval))_2_1") + @test ref_interval[4].func == -x[1] - x[2] - + variable_by_name(model, "v_$(hash(interval))_1_2") + @test ref_interval[5].func == -x[3] - + variable_by_name(model, "v_$(hash(interval))_2_2") + @test ref_interval[3].func == + variable_by_name(model, "v_$(hash(interval))_1_1") * y[2] + + variable_by_name(model, "v_$(hash(interval))_2_1") * y[2] - + 0.5 * y[2] + + # Array literals broken into multiple lines for better readability + @test ref_nn[1].func == [ + -x[1] - variable_by_name(model, "v_$(hash(nn))_1_1"), + -x[2] - variable_by_name(model, "v_$(hash(nn))_1_2"), + -variable_by_name(model, "v_$(hash(nn))_1_3"), + -variable_by_name(model, "v_$(hash(nn))_1_4") + ] + + @test ref_nn[2].func == [ + -variable_by_name(model, "v_$(hash(nn))_2_1"), + -variable_by_name(model, "v_$(hash(nn))_2_2"), + -x[3] - variable_by_name(model, "v_$(hash(nn))_2_3"), + -x[4] - variable_by_name(model, "v_$(hash(nn))_2_4") + ] + + @test ref_nn[3].func == [ + variable_by_name(model, "v_$(hash(nn))_1_1") * y[1] + + variable_by_name(model, "v_$(hash(nn))_2_1") * y[1] + y[1], + variable_by_name(model, "v_$(hash(nn))_1_2") * y[1] + + variable_by_name(model, "v_$(hash(nn))_2_2") * y[1] + y[1], + variable_by_name(model, "v_$(hash(nn))_1_3") * y[1] + + variable_by_name(model, "v_$(hash(nn))_2_3") * y[1] + y[1], + variable_by_name(model, "v_$(hash(nn))_1_4") * y[1] + + variable_by_name(model, "v_$(hash(nn))_2_4") * y[1] + y[1] + ] + + @test ref_np[1].func == [ + -x[1] - variable_by_name(model, "v_$(hash(np))_1_1"), + -x[2] - variable_by_name(model, "v_$(hash(np))_1_2"), + -variable_by_name(model, "v_$(hash(np))_1_3"), + -variable_by_name(model, "v_$(hash(np))_1_4") + ] + + @test ref_np[2].func == [ + -variable_by_name(model, "v_$(hash(np))_2_1"), + -variable_by_name(model, "v_$(hash(np))_2_2"), + -x[3] - variable_by_name(model, "v_$(hash(np))_2_3"), + -x[4] - variable_by_name(model, "v_$(hash(np))_2_4") + ] + + @test ref_np[3].func == [ + variable_by_name(model, "v_$(hash(np))_1_1") * y[2] + + variable_by_name(model, "v_$(hash(np))_2_1") * y[2] + y[2], + variable_by_name(model, "v_$(hash(np))_1_2") * y[2] + + variable_by_name(model, "v_$(hash(np))_2_2") * y[2] + y[2], + variable_by_name(model, "v_$(hash(np))_1_3") * y[2] + + variable_by_name(model, "v_$(hash(np))_2_3") * y[2] + y[2], + variable_by_name(model, "v_$(hash(np))_1_4") * y[2] + + variable_by_name(model, "v_$(hash(np))_2_4") * y[2] + y[2] + ] + + @test ref_zeros[1].func == [ + 5x[1] - variable_by_name(model, "v_$(hash(zeros))_1_1_1"), + 5x[2] - variable_by_name(model, "v_$(hash(zeros))_1_2_1"), + -variable_by_name(model, "v_$(hash(zeros))_1_3_1"), + -variable_by_name(model, "v_$(hash(zeros))_1_4_1") + ] + + @test ref_zeros[2].func == [ + -variable_by_name(model, "v_$(hash(zeros))_2_1_1"), + -variable_by_name(model, "v_$(hash(zeros))_2_2_1"), + 5x[3] - variable_by_name(model, "v_$(hash(zeros))_2_3_1"), + 5x[4] - variable_by_name(model, "v_$(hash(zeros))_2_4_1") + ] +end + +function test_reformulate_disjunct() + model = GDPModel() + @variable(model, 0 <= x[1:4] <= 1) + @variable(model, Y[1:2], Logical) + @variable(model, W[1:2], Logical) + @disjunction(model, [Y[1], Y[2]]) + @disjunction(model, [W[1], W[2]]) + method = PSplit([[x[1], x[2]], [x[3], x[4]]]) + for i in 1:4 + DP._variable_bounds(model)[x[i]] = DP.set_variable_bound_info(x[i], method) + end + ref_cons1 = Vector{JuMP.AbstractConstraint}() + ref_cons2 = Vector{JuMP.AbstractConstraint}() + + # Create constraints + good_quad = @constraint(model, x[1]^2 + x[2]^2 <= 1, Disjunct(W[2])) + good_quad2 = @constraint(model, x[3]^2 + x[4]^2 <= 1, Disjunct(W[2])) + affexpr = @constraint(model, x[1] + x[2] <= 1, Disjunct(W[1])) + nonlinear_con = @constraint(model, exp(x[1]) <= 1, Disjunct(Y[1])) + bad_quad = @constraint(model, x[1]*x[2] <= 1, Disjunct(Y[2])) + + @test_throws ErrorException DP._reformulate_disjunct(model, ref_cons1, Y[2], method) + @test_throws ErrorException DP._reformulate_disjunct(model, ref_cons2, Y[1], method) + + DP._reformulate_disjunct(model, ref_cons1, W[2], method) + DP._reformulate_disjunct(model, ref_cons2, W[1], method) + + @test length(ref_cons1) == 6 + @test length(ref_cons2) == 3 end -function _test_set_variable_bound_info() + + +function test_set_variable_bound_info() model = GDPModel() @variable(model, x) @@ -172,9 +249,10 @@ function _test_set_variable_bound_info() end @testset "P-Split Reformulation" begin - _test_psplit() - _test_build_partitioned_expression() - _test_bound_auxiliary() - _test_reformulate_disjunct_constraint_affexpr() - _test_set_variable_bound_info() + test_psplit() + test_build_partitioned_expression() + test_set_variable_bound_info() + test_bound_auxiliary() + test_reformulate_disjunct_constraint_affexpr() + test_reformulate_disjunct() end \ No newline at end of file diff --git a/test/solve.jl b/test/solve.jl index 9f79f8e..e185eee 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -1,6 +1,7 @@ -using HiGHS - +using HiGHS, Ipopt, Juniper function test_linear_gdp_example(m, use_complements = false) + + set_attribute(m, MOI.Silent(), true) @variable(m, 1 ≤ x[1:2] ≤ 9) if use_complements @@ -55,8 +56,79 @@ function test_linear_gdp_example(m, use_complements = false) @test !value(W[1]) @test !value(W[2]) + @test optimize!(m, gdp_method = PSplit([x[1], x[2]])) isa Nothing + @test termination_status(m) == MOI.OPTIMAL + @test objective_value(m) ≈ 11 + @test value.(x) ≈ [9,2] + @test !value(Y[1]) + @test value(Y[2]) + @test !value(W[1]) + @test !value(W[2]) end +function test_quadratic_gdp_example(use_complements = false) + ipopt = optimizer_with_attributes(Ipopt.Optimizer,"print_level"=>0,"sb"=>"yes") + optimizer = optimizer_with_attributes(Juniper.Optimizer, "nl_solver"=>ipopt) + m = GDPModel(optimizer) + set_attribute(m, MOI.Silent(), true) + @variable(m, 0 ≤ x[1:2] ≤ 10) + + if !use_complements + @variable(m, Y1, Logical) + @variable(m, Y2, Logical, logical_complement = Y1) + Y = [Y1, Y2] + else + @variable(m, Y[1:2], Logical) + end + @variable(m, W[1:2], Logical) + + @objective(m, Max, sum(x)) + + @constraint(m, y1_quad, x[1]^2 + x[2]^2 ≤ 16, Disjunct(Y[1])) + @constraint(m, w1[i=1:2], [1, 2][i] ≤ x[i] ≤ [3, 4][i], Disjunct(W[1])) + @constraint(m, w1_quad, x[1]^2 ≥ 2, Disjunct(W[1])) + + @constraint(m, w2[i=1:2], [2, 1][i] ≤ x[i] ≤ [4, 3][i], Disjunct(W[2])) + @constraint(m, w2_quad, x[1]^2 + x[2] ≤ 10, Disjunct(W[2])) + + @constraint(m, y2_quad, x[1]^2 + 2*x[2]^2 ≤ 25, Disjunct(Y[2])) + @constraint(m, y2[i=1:2], [3, 2][i] ≤ x[i] ≤ [5, 3][i], Disjunct(Y[2])) + + @disjunction(m, inner, [W[1], W[2]], Disjunct(Y[1])) + @disjunction(m, outer, [Y[1], Y[2]]) + + @test optimize!(m, gdp_method = BigM()) isa Nothing + @test termination_status(m) in [MOI.OPTIMAL, MOI.LOCALLY_SOLVED] + @test objective_value(m) ≈ 6.1237 atol=1e-3 + @test value.(x) ≈ [4.0825, 2.0412] atol=1e-3 + @test !value(Y[1]) + @test value(Y[2]) + @test !value(W[1]) + @test !value(W[2]) + + + @test optimize!(m, gdp_method = MBM(optimizer)) isa Nothing + @test termination_status(m) in [MOI.OPTIMAL, MOI.LOCALLY_SOLVED] + @test objective_value(m) ≈ 6.1237 atol=1e-3 + @test value.(x) ≈ [4.0825, 2.0412] atol=1e-3 + @test !value(Y[1]) + @test value(Y[2]) + @test !value(W[1]) + @test !value(W[2]) + + partition = [[x[1]], [x[2]]] + @test optimize!(m, gdp_method = PSplit(partition)) isa Nothing + @test termination_status(m) in [MOI.OPTIMAL, MOI.LOCALLY_SOLVED] + @test objective_value(m) ≈ 6.1237 atol=1e-3 + @test value.(x) ≈ [4.0825, 2.0412] atol=1e-3 + @test !value(Y[1]) + @test value(Y[2]) + @test !value(W[1]) + @test !value(W[2]) +end + + + function test_generic_model(m) set_attribute(m, MOI.Silent(), true) @variable(m, 1 ≤ x[1:2] ≤ 9) @@ -84,5 +156,6 @@ end MOI.Utilities.UniversalFallback(MOIU.Model{Float32}()), eval_objective_value = false ) + test_quadratic_gdp_example() test_generic_model(GDPModel{Float32}(mockoptimizer)) end \ No newline at end of file From 9b5c823b6546da50989094c7ec50195846b4e881 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Thu, 2 Oct 2025 17:35:04 -0400 Subject: [PATCH 22/39] solve.jl change --- src/datatypes.jl | 2 +- src/psplit.jl | 8 ++++++-- test/constraints/psplit.jl | 6 ------ test/solve.jl | 9 --------- 4 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/datatypes.jl b/src/datatypes.jl index 997191d..857d2bc 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -420,7 +420,7 @@ end """ PSplit <: AbstractReformulationMethod -A type for using the p-split reformulation approach for disjunctive +A type for using the P-split reformulation approach for disjunctive constraints. **Fields** diff --git a/src/psplit.jl b/src/psplit.jl index cbd93a3..cecdd55 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -199,12 +199,16 @@ function _reformulate_disjunct( by the PSplit reformulation.") elseif con.func isa JuMP.GenericQuadExpr quadexpr = con.func - all_same_variable = all(pair -> first(pair).a == first(pair).b, quadexpr.terms) + all_same_variable = all(pair -> first(pair).a == first(pair).b, + quadexpr.terms + ) !all_same_variable && error("PSplit reformulation only supports quadratic constraints where all terms have the same variables.") end end - append!(ref_cons, reformulate_disjunct_constraint(model, con, bvref, method)) + append!(ref_cons, + reformulate_disjunct_constraint(model, con, bvref, method) + ) end return end diff --git a/test/constraints/psplit.jl b/test/constraints/psplit.jl index f20938c..0df724a 100644 --- a/test/constraints/psplit.jl +++ b/test/constraints/psplit.jl @@ -3,7 +3,6 @@ function test_psplit() @variable(model, x[1:4]) method = PSplit([[x[1], x[2]], [x[3], x[4]]]) @test method.partition == [[x[1], x[2]], [x[3], x[4]]] - # Throw error when partition isnt set up! end function test_build_partitioned_expression() @@ -11,7 +10,6 @@ function test_build_partitioned_expression() @variable(model, x[1:4]) partition_variables = [x[1], x[4]] - # Test data test_cases = [ (expr = 1.0 * x[1] - 2.0 * x[2], expected = (1.0 * x[1], 0.0)), (expr = x[1] * x[1] + 2.0 * x[1] * x[1] + 3.0 * x[2] * x[2], @@ -21,13 +19,11 @@ function test_build_partitioned_expression() (expr = 4.0, expected = (4.0, 0.0)) ] - # Run tests for (i, test_case) in enumerate(test_cases) result = DP._build_partitioned_expression(test_case.expr, partition_variables) @test result == test_case.expected end - # Test error case @test_throws ErrorException DP._build_partitioned_expression( "Bad Input", partition_variables @@ -139,7 +135,6 @@ function test_reformulate_disjunct_constraint_affexpr() variable_by_name(model, "v_$(hash(interval))_2_1") * y[2] - 0.5 * y[2] - # Array literals broken into multiple lines for better readability @test ref_nn[1].func == [ -x[1] - variable_by_name(model, "v_$(hash(nn))_1_1"), -x[2] - variable_by_name(model, "v_$(hash(nn))_1_2"), @@ -219,7 +214,6 @@ function test_reformulate_disjunct() ref_cons1 = Vector{JuMP.AbstractConstraint}() ref_cons2 = Vector{JuMP.AbstractConstraint}() - # Create constraints good_quad = @constraint(model, x[1]^2 + x[2]^2 <= 1, Disjunct(W[2])) good_quad2 = @constraint(model, x[3]^2 + x[4]^2 <= 1, Disjunct(W[2])) affexpr = @constraint(model, x[1] + x[2] <= 1, Disjunct(W[1])) diff --git a/test/solve.jl b/test/solve.jl index e185eee..4c617c5 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -55,15 +55,6 @@ function test_linear_gdp_example(m, use_complements = false) @test value(Y[2]) @test !value(W[1]) @test !value(W[2]) - - @test optimize!(m, gdp_method = PSplit([x[1], x[2]])) isa Nothing - @test termination_status(m) == MOI.OPTIMAL - @test objective_value(m) ≈ 11 - @test value.(x) ≈ [9,2] - @test !value(Y[1]) - @test value(Y[2]) - @test !value(W[1]) - @test !value(W[2]) end function test_quadratic_gdp_example(use_complements = false) From 2bdeeab964982ae0b8fef0c26db950cceddc6bc3 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Thu, 2 Oct 2025 17:35:13 -0400 Subject: [PATCH 23/39] solve.jl change --- test/solve.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/solve.jl b/test/solve.jl index 4c617c5..2f60e06 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -118,8 +118,6 @@ function test_quadratic_gdp_example(use_complements = false) @test !value(W[2]) end - - function test_generic_model(m) set_attribute(m, MOI.Silent(), true) @variable(m, 1 ≤ x[1:2] ≤ 9) From c97dfce36308e9c3d3f2cc536bb85969552ce8f8 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Fri, 3 Oct 2025 12:37:52 -0400 Subject: [PATCH 24/39] readme update --- README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3940b0d..dcc1ebe 100644 --- a/README.md +++ b/README.md @@ -172,16 +172,14 @@ 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. -<<<<<<< HEAD -4. [P-Split](https://arxiv.org/abs/2202.05198): This method reformulates each disjunct constraint into P constraints, each with a partitioned group defined by the user. This method requires that terms in the constraint be convex additively seperable with respect to each variable. The `PSplit` struct is created with the following required arguments: - - - `partition`: Partition of the variables to be split. All variables must be in exactly one partition. (e.g., The variables `x[1:4]` can be partitioned into two groups ` partition = [[x[1], x[2]], [x[3], x[4]]]`) -======= 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. - `default_M`: Default big-M value to use if no big-M is specified for a logical variable (1e9). ->>>>>>> upstream/master + +5. [P-Split](https://arxiv.org/abs/2202.05198): This method reformulates each disjunct constraint into P constraints, each with a partitioned group defined by the user. This method requires that terms in the constraint be convex additively seperable with respect to each variable. The `PSplit` struct is created with the following required arguments: + + - `partition`: Partition of the variables to be split. All variables must be in exactly one partition. (e.g., The variables `x[1:4]` can be partitioned into two groups ` partition = [[x[1], x[2]], [x[3], x[4]]]`) ## Release Notes @@ -198,7 +196,7 @@ using HiGHS m = GDPModel(HiGHS.Optimizer) @variable(m, 0 ≤ x[1:2] ≤ 20) @variable(m, Y[1:2], Logical) -@constraint(m, [i = 1:2], [2,5][i] ≤ x[i] ≤ [6,9][i], Disjunct(Y[1])) +@constraint(m, [i = 1:2], [2,5][i] ≤ x[i] ≤ [6,9][i], Disjunct(Y[1])) @constraint(m, [i = 1:2], [8,10][i] ≤ x[i] ≤ [11,15][i], Disjunct(Y[2])) @disjunction(m, Y) @objective(m, Max, sum(x)) From 065ce84ccefa99f64c2e195743560c0d9a95fd53 Mon Sep 17 00:00:00 2001 From: dnguyen227 <82475321+dnguyen227@users.noreply.github.com> Date: Fri, 3 Oct 2025 12:42:46 -0400 Subject: [PATCH 25/39] Update solve.jl --- test/solve.jl | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/solve.jl b/test/solve.jl index 2f60e06..0134e60 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -1,7 +1,5 @@ using HiGHS, Ipopt, Juniper function test_linear_gdp_example(m, use_complements = false) - - set_attribute(m, MOI.Silent(), true) @variable(m, 1 ≤ x[1:2] ≤ 9) if use_complements @@ -97,7 +95,6 @@ function test_quadratic_gdp_example(use_complements = false) @test !value(W[1]) @test !value(W[2]) - @test optimize!(m, gdp_method = MBM(optimizer)) isa Nothing @test termination_status(m) in [MOI.OPTIMAL, MOI.LOCALLY_SOLVED] @test objective_value(m) ≈ 6.1237 atol=1e-3 @@ -147,4 +144,4 @@ end ) test_quadratic_gdp_example() test_generic_model(GDPModel{Float32}(mockoptimizer)) -end \ No newline at end of file +end From be2b33ac574529a3676b0ec722fe7a58992f20ec Mon Sep 17 00:00:00 2001 From: dnguyen227 <82475321+dnguyen227@users.noreply.github.com> Date: Fri, 3 Oct 2025 13:08:37 -0400 Subject: [PATCH 26/39] update Ipopt type. --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index cbd9df2..ceda3d3 100644 --- a/Project.toml +++ b/Project.toml @@ -13,7 +13,7 @@ JuMP = "1.18" Reexport = "1" julia = "1.6" Juniper = "0.9.3" -Ipopt = "1.11.0" +Ipopt = "1.9.0" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" From 5184784c547d4bd4d78e8427bcf2396bc7933901 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Wed, 8 Oct 2025 11:20:10 -0400 Subject: [PATCH 27/39] Fixed datatypes --- src/datatypes.jl | 2 +- src/psplit.jl | 43 +++++++++++++++++++++++-------------------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/datatypes.jl b/src/datatypes.jl index 857d2bc..b211151 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -418,7 +418,7 @@ struct Hull{T} <: AbstractReformulationMethod end """ - PSplit <: AbstractReformulationMethod + PSplit {V <: JuMP.AbstractVariableRef} <: AbstractReformulationMethod A type for using the P-split reformulation approach for disjunctive constraints. diff --git a/src/psplit.jl b/src/psplit.jl index cecdd55..d9e016b 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -9,7 +9,7 @@ function _build_partitioned_expression( constant = JuMP.constant(expr) new_affexpr = zero(T) for var in partition_variables - JuMP.add_to_expression!(new_affexpr, coefficient(expr, var), var) + JuMP.add_to_expression!(new_affexpr, JuMP.coefficient(expr, var), var) end return new_affexpr, constant end @@ -24,7 +24,11 @@ function _build_partitioned_expression( for (pair, coeff) in expr.terms if pair.a == var && pair.b == var JuMP.add_to_expression!(new_quadexpr, coeff, var, var) + else + error("PSplit reformulation only supports quadratic constraints + where all terms have the same variables.") end + end end new_aff, _ = _build_partitioned_expression(expr.aff, partition_variables) @@ -57,10 +61,9 @@ end function _bound_auxiliary( model::JuMP.AbstractModel, v::JuMP.AbstractVariableRef, - func::JuMP.GenericAffExpr, + func::JuMP.GenericAffExpr{T,V}, method::PSplit -) - T = JuMP.value_type(typeof(model)) +) where {T,V} lower_bound = zero(T) upper_bound = zero(T) for (var, coeff) in func.terms @@ -86,16 +89,15 @@ function _bound_auxiliary( func::Number, method::PSplit ) - #Do nothing? + return end function _bound_auxiliary( model::JuMP.AbstractModel, v::JuMP.AbstractVariableRef, - func::JuMP.GenericQuadExpr, + func::JuMP.GenericQuadExpr{T,V}, method::PSplit -) - T = JuMP.value_type(typeof(model)) +) where {T,V} lower_bound = zero(T) upper_bound = zero(T) @@ -145,12 +147,12 @@ function _bound_auxiliary( end function _bound_auxiliary( - model::JuMP.AbstractModel, + model::M, v::JuMP.AbstractVariableRef, func::JuMP.AbstractVariableRef, method::PSplit -) - T = JuMP.value_type(typeof(model)) +) where {M <: JuMP.AbstractModel} + T = JuMP.value_type(M) lower_bound = zero(T) upper_bound = zero(T) if func != v @@ -213,16 +215,17 @@ function _reformulate_disjunct( return end + ################################################################################ # REFORMULATE DISJUNCT CONSTRAINT ################################################################################ function reformulate_disjunct_constraint( - model::JuMP.AbstractModel, + model::M, con::JuMP.ScalarConstraint{T, S}, bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::PSplit -) where {T, S <: _MOI.LessThan} - val_type = JuMP.value_type(typeof(model)) +) where {M <: JuMP.AbstractModel, T, S <: _MOI.LessThan} + val_type = JuMP.value_type(M) p = length(method.partition) v = [@variable(model, base_name = "v_$(hash(con))_$(i)") for i in 1:p] _, constant = _build_partitioned_expression(con.func, method.partition[p]) @@ -241,12 +244,12 @@ function reformulate_disjunct_constraint( end function reformulate_disjunct_constraint( - model::JuMP.AbstractModel, + model::M, con::JuMP.ScalarConstraint{T, S}, bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::PSplit -) where {T, S <: _MOI.GreaterThan} - val_type = JuMP.value_type(typeof(model)) +) where {M <: JuMP.AbstractModel, T, S <: _MOI.GreaterThan} + val_type = JuMP.value_type(M) p = length(method.partition) reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) v = [@variable(model, base_name = "v_$(hash(con))_$(i)") for i in 1:p] @@ -265,12 +268,12 @@ function reformulate_disjunct_constraint( end function reformulate_disjunct_constraint( - model::JuMP.AbstractModel, + model::M, con::JuMP.ScalarConstraint{T, S}, bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::PSplit -) where {T, S <: Union{_MOI.Interval, _MOI.EqualTo}} - val_type = JuMP.value_type(typeof(model)) +) where {M <: JuMP.AbstractModel, T, S <: Union{_MOI.Interval, _MOI.EqualTo}} + val_type = JuMP.value_type(M) p = length(method.partition) reform_con_lt = Vector{JuMP.AbstractConstraint}(undef, p + 1) reform_con_gt = Vector{JuMP.AbstractConstraint}(undef, p + 1) From 8b6ef04e88af979e2775808d480c5dad4b946ab2 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Thu, 9 Oct 2025 13:26:05 -0400 Subject: [PATCH 28/39] Initial commit to change to extended hull reformulation --- src/datatypes.jl | 32 +++-- src/hull.jl | 18 ++- src/psplit.jl | 235 +++++++++++++++++++++---------------- test/constraints/psplit.jl | 193 +++++------------------------- test/solve.jl | 4 +- 5 files changed, 197 insertions(+), 285 deletions(-) diff --git a/src/datatypes.jl b/src/datatypes.jl index 857d2bc..8f499be 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -417,6 +417,20 @@ struct Hull{T} <: AbstractReformulationMethod end end +# temp struct to store variable disaggregations (reset for each disjunction) +mutable struct _Hull{V <: JuMP.AbstractVariableRef, T} <: AbstractReformulationMethod + value::T + disjunction_variables::Dict{V, Vector{V}} + disjunct_variables::Dict{Tuple{V, Union{V, JuMP.GenericAffExpr{T, V}}}, V} + function _Hull(method::Hull{T}, vrefs::Set{V}) where {T, V <: JuMP.AbstractVariableRef} + new{V, T}( + method.value, + Dict{V, Vector{V}}(vref => V[] for vref in vrefs), + Dict{Tuple{V, Union{V, JuMP.GenericAffExpr{T, V}}}, V}() + ) + end +end + """ PSplit <: AbstractReformulationMethod @@ -435,15 +449,15 @@ struct PSplit{V <: JuMP.AbstractVariableRef} <: AbstractReformulationMethod end # temp struct to store variable disaggregations (reset for each disjunction) -mutable struct _Hull{V <: JuMP.AbstractVariableRef, T} <: AbstractReformulationMethod - value::T - disjunction_variables::Dict{V, Vector{V}} - disjunct_variables::Dict{Tuple{V, Union{V, JuMP.GenericAffExpr{T, V}}}, V} - function _Hull(method::Hull{T}, vrefs::Set{V}) where {T, V <: JuMP.AbstractVariableRef} - new{V, T}( - method.value, - Dict{V, Vector{V}}(vref => V[] for vref in vrefs), - Dict{Tuple{V, Union{V, JuMP.GenericAffExpr{T, V}}}, V}() +mutable struct _PSplit{V <: JuMP.AbstractVariableRef, M <: JuMP.AbstractModel} <: AbstractReformulationMethod + partition::Vector{Vector{V}} + partitioned_constraints::Dict{LogicalVariableRef{M}, Vector{<:AbstractConstraint}} + hull::_Hull + function _PSplit(method::PSplit{V}, model::M) where {V <: JuMP.AbstractVariableRef, M <: JuMP.AbstractModel} + new{V, M}( + method.partition, + Dict{LogicalVariableRef{M}, Vector{<:AbstractConstraint}}(), + _Hull(Hull(), Set{V}()) ) end end diff --git a/src/hull.jl b/src/hull.jl index cba5efd..e56d3f8 100644 --- a/src/hull.jl +++ b/src/hull.jl @@ -22,14 +22,14 @@ function _disaggregate_variables( end end function _disaggregate_variable( - model::JuMP.AbstractModel, + model::M, lvref::LogicalVariableRef, vref::JuMP.AbstractVariableRef, method::_Hull - ) + ) where {M <: JuMP.AbstractModel} #create disaggregated vref lb, ub = variable_bound_info(vref) - T = JuMP.value_type(typeof(model)) + T = JuMP.value_type(M) info = JuMP.VariableInfo( true, # has_lb = true lb, # lower_bound = lb @@ -70,8 +70,16 @@ function _aggregate_variable( method::_Hull ) JuMP.is_binary(vref) && return #skip binary variables - con_expr = JuMP.@expression(model, -vref + sum(method.disjunction_variables[vref])) - push!(ref_cons, JuMP.build_constraint(error, con_expr, _MOI.EqualTo(0))) + # con_expr = JuMP.@expression(model, -vref + sum(method.disjunction_variables[vref])) + # push!(ref_cons, JuMP.build_constraint(error, con_expr, _MOI.EqualTo(0))) + + expr = JuMP.AffExpr(0.0) + JuMP.add_to_expression!(expr, -1.0, vref) + for dv in method.disjunction_variables[vref] + JuMP.add_to_expression!(expr, 1.0, dv) + end + + push!(ref_cons, JuMP.build_constraint(error, expr, _MOI.EqualTo(0))) return end diff --git a/src/psplit.jl b/src/psplit.jl index cecdd55..e3ef400 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -9,7 +9,7 @@ function _build_partitioned_expression( constant = JuMP.constant(expr) new_affexpr = zero(T) for var in partition_variables - JuMP.add_to_expression!(new_affexpr, coefficient(expr, var), var) + JuMP.add_to_expression!(new_affexpr, JuMP.coefficient(expr, var), var) end return new_affexpr, constant end @@ -24,6 +24,8 @@ function _build_partitioned_expression( for (pair, coeff) in expr.terms if pair.a == var && pair.b == var JuMP.add_to_expression!(new_quadexpr, coeff, var, var) + elseif pair.a == var || pair.b == var + error("Quadratic expression contains bilinear term ($(pair.a), $(pair.b))") end end end @@ -55,12 +57,12 @@ end # BOUND AUXILIARY ################################################################################ function _bound_auxiliary( - model::JuMP.AbstractModel, + model::M, v::JuMP.AbstractVariableRef, func::JuMP.GenericAffExpr, method::PSplit -) - T = JuMP.value_type(typeof(model)) +) where {M <: JuMP.AbstractModel} + T = JuMP.value_type(M) lower_bound = zero(T) upper_bound = zero(T) for (var, coeff) in func.terms @@ -78,24 +80,28 @@ function _bound_auxiliary( end JuMP.set_lower_bound(v, lower_bound) JuMP.set_upper_bound(v, upper_bound) + _variable_bounds(model)[v] = set_variable_bound_info(v, method) end function _bound_auxiliary( - model::JuMP.AbstractModel, + model::M, v::JuMP.AbstractVariableRef, func::Number, method::PSplit -) - #Do nothing? +) where {M <: JuMP.AbstractModel} + JuMP.set_lower_bound(v, func) + JuMP.set_upper_bound(v, func) + _variable_bounds(model)[v] = set_variable_bound_info(v, method) + return end function _bound_auxiliary( - model::JuMP.AbstractModel, + model::M, v::JuMP.AbstractVariableRef, func::JuMP.GenericQuadExpr, method::PSplit -) - T = JuMP.value_type(typeof(model)) +) where {M <: JuMP.AbstractModel} + T = JuMP.value_type(M) lower_bound = zero(T) upper_bound = zero(T) @@ -142,15 +148,16 @@ function _bound_auxiliary( JuMP.set_lower_bound(v, lower_bound) JuMP.set_upper_bound(v, upper_bound) + _variable_bounds(model)[v] = set_variable_bound_info(v, method) end function _bound_auxiliary( - model::JuMP.AbstractModel, + model::M, v::JuMP.AbstractVariableRef, func::JuMP.AbstractVariableRef, method::PSplit -) - T = JuMP.value_type(typeof(model)) +) where {M <: JuMP.AbstractModel} + T = JuMP.value_type(M) lower_bound = zero(T) upper_bound = zero(T) if func != v @@ -162,6 +169,7 @@ function _bound_auxiliary( JuMP.set_lower_bound(v,lower_bound) JuMP.set_upper_bound(v,upper_bound) end + _variable_bounds(model)[v] = set_variable_bound_info(v, method) end requires_variable_bound_info(method::PSplit) = true @@ -172,8 +180,8 @@ function set_variable_bound_info(vref::JuMP.AbstractVariableRef, ::PSplit) using the PSplit reformulation." ) else - lb = lower_bound(vref) - ub = upper_bound(vref) + lb = min(0, lower_bound(vref)) + ub = max(0, upper_bound(vref)) end return lb, ub end @@ -181,135 +189,156 @@ end ################################################################################ # REFORMULATE DISJUNCT ################################################################################ + +function reformulate_disjunction(model::JuMP.AbstractModel, disj::Disjunction, method::PSplit{V}) where {V <: JuMP.AbstractVariableRef} + ref_cons = Vector{JuMP.AbstractConstraint}() #store reformulated constraints + disj_vrefs = _get_disjunction_variables(model, disj) + partitioned_constraints = Dict{LogicalVariableRef, Vector{<:JuMP.AbstractConstraint}}() + for d in disj.indicators + partitioned_constraints[d], aux_vars = _partition_disjunct(model, d, method) + disj_vrefs = union(disj_vrefs, aux_vars) + end + #TODO: This can probably be done more efficiently? + psplit = _PSplit(method, model) + psplit.hull = _Hull(Hull(), disj_vrefs) + psplit.partitioned_constraints = partitioned_constraints + for d in disj.indicators + _disaggregate_variables(model, d, disj_vrefs, psplit.hull) + _reformulate_disjunct(model, ref_cons, d, psplit) + end + + for vref in disj_vrefs + _aggregate_variable(model, ref_cons, vref, psplit.hull) + end + return ref_cons +end + +function reformulate_disjunction(model::JuMP.AbstractModel, disj::Disjunction, method::_PSplit) + return reformulate_disjunction(model, disj, PSplit(method.partition)) +end + function _reformulate_disjunct( model::JuMP.AbstractModel, ref_cons::Vector{JuMP.AbstractConstraint}, lvref::LogicalVariableRef, - method::PSplit + method::_PSplit ) #reformulate each constraint and add to the model bvref = binary_variable(lvref) - !haskey(_indicator_to_constraints(model), lvref) && return #skip if disjunct is empty + haskey(method.partitioned_constraints, lvref) || return + constraints = method.partitioned_constraints[lvref] + for con in constraints + append!(ref_cons, reformulate_disjunct_constraint(model, con, bvref, method.hull)) + end + return +end - for cref in _indicator_to_constraints(model)[lvref] +function _partition_disjunct(model::M, lvref::LogicalVariableRef, method::PSplit) where {M <: JuMP.AbstractModel} + !haskey(_indicator_to_constraints(model), lvref) && return #skip if disjunct is empty + + partitioned_constraints = Vector{AbstractConstraint}() + aux_vars = Set{JuMP.AbstractVariableRef}() + for cref in _indicator_to_constraints(model)[lvref] con = JuMP.constraint_object(cref) if !(con isa Disjunction) - if con.func isa JuMP.GenericNonlinearExpr - error("Nonlinear constraints are not supported - by the PSplit reformulation.") - elseif con.func isa JuMP.GenericQuadExpr - quadexpr = con.func - all_same_variable = all(pair -> first(pair).a == first(pair).b, - quadexpr.terms - ) - !all_same_variable && error("PSplit reformulation only supports - quadratic constraints where all terms have the same variables.") - end + p_constraint, new_aux_vars = _build_partitioned_constraint(model, con, method) + append!(partitioned_constraints, p_constraint) + union!(aux_vars, new_aux_vars) end - append!(ref_cons, - reformulate_disjunct_constraint(model, con, bvref, method) - ) end - return + return partitioned_constraints, aux_vars end -################################################################################ -# REFORMULATE DISJUNCT CONSTRAINT -################################################################################ -function reformulate_disjunct_constraint( - model::JuMP.AbstractModel, +# ################################################################################ +# # BUILD PARTITIONED CONSTRAINT +# ################################################################################ +function _build_partitioned_constraint( + model::M, con::JuMP.ScalarConstraint{T, S}, - bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::PSplit -) where {T, S <: _MOI.LessThan} - val_type = JuMP.value_type(typeof(model)) +) where {M <: JuMP.AbstractModel, T, S <: _MOI.LessThan} + val_type = JuMP.value_type(M) p = length(method.partition) v = [@variable(model, base_name = "v_$(hash(con))_$(i)") for i in 1:p] _, constant = _build_partitioned_expression(con.func, method.partition[p]) - reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) + part_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) for i in 1:p func, _ = _build_partitioned_expression(con.func, method.partition[i]) - reform_con[i] = JuMP.build_constraint(error, func - v[i], + part_con[i] = JuMP.build_constraint(error, func - v[i], MOI.LessThan(zero(val_type)) ) _bound_auxiliary(model, v[i], func, method) end - reform_con[end] = JuMP.build_constraint(error, sum(v[i]*bvref for i in 1:p) - - (con.set.upper - constant) * bvref, MOI.LessThan(zero(val_type)) + part_con[end] = JuMP.build_constraint(error, sum(v[i] for i in 1:p) + ,MOI.LessThan(con.set.upper - constant) ) - return reform_con + return part_con, v end -function reformulate_disjunct_constraint( - model::JuMP.AbstractModel, +function _build_partitioned_constraint( + model::M, con::JuMP.ScalarConstraint{T, S}, - bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::PSplit -) where {T, S <: _MOI.GreaterThan} - val_type = JuMP.value_type(typeof(model)) +) where {M <: JuMP.AbstractModel, T, S <: _MOI.GreaterThan} + val_type = JuMP.value_type(M) p = length(method.partition) - reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) + part_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) v = [@variable(model, base_name = "v_$(hash(con))_$(i)") for i in 1:p] _, constant = _build_partitioned_expression(con.func, method.partition[p]) for i in 1:p func, _ = _build_partitioned_expression(con.func, method.partition[i]) - reform_con[i] = JuMP.build_constraint(error, -func - v[i], + part_con[i] = JuMP.build_constraint(error, -func - v[i], MOI.LessThan(zero(val_type)) ) _bound_auxiliary(model, v[i], -func, method) end - reform_con[end] = JuMP.build_constraint(error, sum(v[i]*bvref for i in 1:p) - - (-con.set.lower + constant) * bvref, MOI.LessThan(zero(val_type)) + part_con[end] = JuMP.build_constraint(error, sum(v[i] for i in 1:p) + , MOI.LessThan(-con.set.lower + constant) ) - return reform_con + return part_con, v end -function reformulate_disjunct_constraint( - model::JuMP.AbstractModel, +function _build_partitioned_constraint( + model::M, con::JuMP.ScalarConstraint{T, S}, - bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::PSplit -) where {T, S <: Union{_MOI.Interval, _MOI.EqualTo}} - val_type = JuMP.value_type(typeof(model)) +) where {M <: JuMP.AbstractModel, T, S <: Union{_MOI.Interval, _MOI.EqualTo}} + val_type = JuMP.value_type(M) p = length(method.partition) - reform_con_lt = Vector{JuMP.AbstractConstraint}(undef, p + 1) - reform_con_gt = Vector{JuMP.AbstractConstraint}(undef, p + 1) + part_con_lt = Vector{JuMP.AbstractConstraint}(undef, p + 1) + part_con_gt = Vector{JuMP.AbstractConstraint}(undef, p + 1) #let [_, 1] be the upper bound and [_, 2] be the lower bound _, constant = _build_partitioned_expression(con.func, method.partition[p]) v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)") for i in 1:p, j in 1:2] for i in 1:p func, _= _build_partitioned_expression(con.func, method.partition[i]) - reform_con_lt[i] = JuMP.build_constraint(error, + part_con_lt[i] = JuMP.build_constraint(error, func - v[i,1], MOI.LessThan(zero(val_type)) ) - reform_con_gt[i] = JuMP.build_constraint(error, + part_con_gt[i] = JuMP.build_constraint(error, -func - v[i,2], MOI.LessThan(zero(val_type)) ) _bound_auxiliary(model, v[i,1], func, method) _bound_auxiliary(model, v[i,2], -func, method) end set_values = _set_values(con.set) - reform_con_lt[end] = JuMP.build_constraint(error, sum(v[i,1]*bvref - for i in 1:p) - (set_values[2] - constant) * bvref, - MOI.LessThan(zero(val_type)) + part_con_lt[end] = JuMP.build_constraint(error, sum(v[i,1] for i in 1:p), + MOI.LessThan((set_values[2] - constant)) ) - reform_con_gt[end] = JuMP.build_constraint(error, sum(v[i,2]*bvref - for i in 1:p) - (-set_values[1] + constant) * bvref, - MOI.LessThan(zero(val_type)) + part_con_gt[end] = JuMP.build_constraint(error, sum(v[i,2] for i in 1:p), + MOI.LessThan(-set_values[1] + constant) ) - return vcat(reform_con_lt, reform_con_gt) + return vcat(part_con_lt, part_con_gt), vec(v) end -#Functions -function reformulate_disjunct_constraint( - model::JuMP.AbstractModel, +function _build_partitioned_constraint( + model::M, con::JuMP.VectorConstraint{T, S, R}, - bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::PSplit -) where {T, S <: _MOI.Nonpositives, R} +) where {M <: JuMP.AbstractModel, T, S <: _MOI.Nonpositives, R} p = length(method.partition) d = con.set.dimension v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)") for i in 1:p, j in 1:d] - reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) + part_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) constants = Vector{Number}(undef, d) for i in 1:p part_expr = [_build_partitioned_expression(con.func[j], @@ -317,7 +346,7 @@ function reformulate_disjunct_constraint( ] func = JuMP.@expression(model, [j = 1:d], part_expr[j][1]) constants .= [part_expr[j][2] for j in 1:d] - reform_con[i] = JuMP.build_constraint(error, + part_con[i] = JuMP.build_constraint(error, func - v[i,:], _MOI.Nonpositives(d) ) for j in 1:d @@ -325,49 +354,47 @@ function reformulate_disjunct_constraint( end end new_func = JuMP.@expression(model,[j = 1:d], - sum(v[i,j] * bvref for i in 1:p) + constants[j]*bvref + sum(v[i,j] for i in 1:p) + constants[j] ) - reform_con[end] = JuMP.build_constraint(error, new_func, _MOI.Nonpositives(d)) - return vcat(reform_con) + part_con[end] = JuMP.build_constraint(error, new_func, _MOI.Nonpositives(d)) + return vcat(part_con), vec(v) end -function reformulate_disjunct_constraint( - model::JuMP.AbstractModel, +function _build_partitioned_constraint( + model::M, con::JuMP.VectorConstraint{T, S, R}, - bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::PSplit -) where {T, S <: _MOI.Nonnegatives, R} +) where {M <: JuMP.AbstractModel, T, S <: _MOI.Nonnegatives, R} p = length(method.partition) d = con.set.dimension v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)") for i in 1:p, j in 1:d] - reform_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) + part_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) constants = Vector{Number}(undef, d) for i in 1:p part_expr = [_build_partitioned_expression(con.func[j], method.partition[i]) for j in 1:d] func = JuMP.@expression(model, [j = 1:d], -part_expr[j][1]) constants .= [-part_expr[j][2] for j in 1:d] - reform_con[i] = JuMP.build_constraint(error, func - v[i,:], _MOI.Nonpositives(d)) + part_con[i] = JuMP.build_constraint(error, func - v[i,:], _MOI.Nonpositives(d)) for j in 1:d _bound_auxiliary(model, v[i,j], func[j], method) end end new_func = JuMP.@expression(model,[j = 1:d], - sum(v[i,j] * bvref for i in 1:p) + constants[j]*bvref + sum(v[i,j] for i in 1:p) + constants[j] ) - reform_con[end] = JuMP.build_constraint(error,new_func,_MOI.Nonpositives(d)) - return vcat(reform_con) + part_con[end] = JuMP.build_constraint(error,new_func,_MOI.Nonpositives(d)) + return vcat(part_con), vec(v) end -function reformulate_disjunct_constraint( - model::JuMP.AbstractModel, +function _build_partitioned_constraint( + model::M, con::JuMP.VectorConstraint{T, S, R}, - bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::PSplit -) where {T, S <: _MOI.Zeros, R} +) where {M <: JuMP.AbstractModel, T, S <: _MOI.Zeros, R} p = length(method.partition) d = con.set.dimension - reform_con_np = Vector{JuMP.AbstractConstraint}(undef, p + 1) # nonpositive (≤ 0) - reform_con_nn = Vector{JuMP.AbstractConstraint}(undef, p + 1) # nonnegative (≥ 0) + part_con_np = Vector{JuMP.AbstractConstraint}(undef, p + 1) # nonpositive (≤ 0) + part_con_nn = Vector{JuMP.AbstractConstraint}(undef, p + 1) # nonnegative (≥ 0) v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)_$(k)") for i in 1:p, j in 1:d, k in 1:2 ] @@ -379,10 +406,10 @@ function reformulate_disjunct_constraint( ] func = JuMP.@expression(model, [j = 1:d], part_expr[j][1]) constants .= [part_expr[j][2] for j in 1:d] - reform_con_np[i] = JuMP.build_constraint(error, + part_con_np[i] = JuMP.build_constraint(error, func - v[i,:,1], _MOI.Nonpositives(d) ) - reform_con_nn[i] = JuMP.build_constraint(error, + part_con_nn[i] = JuMP.build_constraint(error, -func - v[i,:,2], _MOI.Nonpositives(d) ) for j in 1:d @@ -391,18 +418,18 @@ function reformulate_disjunct_constraint( end end new_func_np = JuMP.@expression(model,[j = 1:d], - sum(v[i,j,1] * bvref for i in 1:p) + constants[j]*bvref + sum(v[i,j,1] for i in 1:p) + constants[j] ) new_func_nn = JuMP.@expression(model,[j = 1:d], - -sum(v[i,j,2] * bvref for i in 1:p) - constants[j]*bvref + -sum(v[i,j,2] for i in 1:p) - constants[j] ) - reform_con_np[end] = JuMP.build_constraint(error, + part_con_np[end] = JuMP.build_constraint(error, new_func_np, _MOI.Nonpositives(d) ) - reform_con_nn[end] = JuMP.build_constraint(error, + part_con_nn[end] = JuMP.build_constraint(error, new_func_nn, _MOI.Nonpositives(d) ) - return vcat(reform_con_np, reform_con_nn) + return vcat(part_con_np, part_con_nn), vec(v) end ################################################################################ diff --git a/test/constraints/psplit.jl b/test/constraints/psplit.jl index 0df724a..4f0ed90 100644 --- a/test/constraints/psplit.jl +++ b/test/constraints/psplit.jl @@ -59,176 +59,41 @@ function test_bound_auxiliary() @test JuMP.lower_bound(v[2]) == -30 @test JuMP.upper_bound(v[2]) == 30 - @test has_lower_bound(v[3]) == false - @test has_upper_bound(v[3]) == false + @test JuMP.upper_bound(v[3]) == 4 + @test JuMP.lower_bound(v[3]) == 4 @test_throws ErrorException DP._bound_auxiliary(model, v[3], nl, method) end -function test_reformulate_disjunct_constraint_affexpr() - model = GDPModel() - @variable(model, 0 <= x[1:4] <= 3) - @variable(model, y[1:2], Bin) - method = PSplit([[x[1], x[2]], [x[3], x[4]]]) - for i in 1:4 - DP._variable_bounds(model)[x[i]] = DP.set_variable_bound_info( - x[i], - method - ) - end - - lt = JuMP.constraint_object(@constraint(model, x[1] + x[3] <= 1)) - gt = JuMP.constraint_object(@constraint(model, x[2] + x[4] >= 1)) - eq = JuMP.constraint_object(@constraint(model, x[3] + x[4] == 1)) - interval = JuMP.constraint_object( - @constraint(model, 0 <= x[1] + x[2] + x[3] <= 0.5) - ) - nn = JuMP.constraint_object(@constraint(model, x .- 1 >= 0)) - np = JuMP.constraint_object(@constraint(model, -x .+ 1 <= 0)) - zeros = JuMP.constraint_object(@constraint(model, 5x .- 1 == 0)) - - ref_lt = reformulate_disjunct_constraint(model, lt, y[1], method) - ref_gt = reformulate_disjunct_constraint(model, gt, y[2], method) - ref_eq = reformulate_disjunct_constraint(model, eq, y[1], method) - ref_interval = reformulate_disjunct_constraint( - model, interval, y[2], method - ) - ref_nn = reformulate_disjunct_constraint(model, nn, y[1], method) - ref_np = reformulate_disjunct_constraint(model, np, y[2], method) - ref_zeros = reformulate_disjunct_constraint(model, zeros, y[1], method) - - @test ref_lt[1].func == x[1] - - variable_by_name(model, "v_$(hash(lt))_1") - @test ref_lt[2].func == x[3] - - variable_by_name(model, "v_$(hash(lt))_2") - @test ref_lt[3].func == - variable_by_name(model, "v_$(hash(lt))_1") * y[1] + - variable_by_name(model, "v_$(hash(lt))_2") * y[1] - - y[1] - @test ref_gt[1].func == -x[2] - - variable_by_name(model, "v_$(hash(gt))_1") - @test ref_gt[2].func == -x[4] - - variable_by_name(model, "v_$(hash(gt))_2") - - @test ref_eq[1].func == - -variable_by_name(model, "v_$(hash(eq))_1_1") - @test ref_eq[2].func == x[3] + x[4] - - variable_by_name(model, "v_$(hash(eq))_2_1") - @test ref_eq[4].func == - -variable_by_name(model, "v_$(hash(eq))_1_2") - @test ref_eq[5].func == -x[3] - x[4] - - variable_by_name(model, "v_$(hash(eq))_2_2") - @test ref_eq[3].func == - variable_by_name(model, "v_$(hash(eq))_1_1") * y[1] + - variable_by_name(model, "v_$(hash(eq))_2_1") * y[1] - - y[1] - - @test ref_interval[1].func == x[1] + x[2] - - variable_by_name(model, "v_$(hash(interval))_1_1") - @test ref_interval[2].func == x[3] - - variable_by_name(model, "v_$(hash(interval))_2_1") - @test ref_interval[4].func == -x[1] - x[2] - - variable_by_name(model, "v_$(hash(interval))_1_2") - @test ref_interval[5].func == -x[3] - - variable_by_name(model, "v_$(hash(interval))_2_2") - @test ref_interval[3].func == - variable_by_name(model, "v_$(hash(interval))_1_1") * y[2] + - variable_by_name(model, "v_$(hash(interval))_2_1") * y[2] - - 0.5 * y[2] - - @test ref_nn[1].func == [ - -x[1] - variable_by_name(model, "v_$(hash(nn))_1_1"), - -x[2] - variable_by_name(model, "v_$(hash(nn))_1_2"), - -variable_by_name(model, "v_$(hash(nn))_1_3"), - -variable_by_name(model, "v_$(hash(nn))_1_4") - ] - - @test ref_nn[2].func == [ - -variable_by_name(model, "v_$(hash(nn))_2_1"), - -variable_by_name(model, "v_$(hash(nn))_2_2"), - -x[3] - variable_by_name(model, "v_$(hash(nn))_2_3"), - -x[4] - variable_by_name(model, "v_$(hash(nn))_2_4") - ] - - @test ref_nn[3].func == [ - variable_by_name(model, "v_$(hash(nn))_1_1") * y[1] + - variable_by_name(model, "v_$(hash(nn))_2_1") * y[1] + y[1], - variable_by_name(model, "v_$(hash(nn))_1_2") * y[1] + - variable_by_name(model, "v_$(hash(nn))_2_2") * y[1] + y[1], - variable_by_name(model, "v_$(hash(nn))_1_3") * y[1] + - variable_by_name(model, "v_$(hash(nn))_2_3") * y[1] + y[1], - variable_by_name(model, "v_$(hash(nn))_1_4") * y[1] + - variable_by_name(model, "v_$(hash(nn))_2_4") * y[1] + y[1] - ] - @test ref_np[1].func == [ - -x[1] - variable_by_name(model, "v_$(hash(np))_1_1"), - -x[2] - variable_by_name(model, "v_$(hash(np))_1_2"), - -variable_by_name(model, "v_$(hash(np))_1_3"), - -variable_by_name(model, "v_$(hash(np))_1_4") - ] - - @test ref_np[2].func == [ - -variable_by_name(model, "v_$(hash(np))_2_1"), - -variable_by_name(model, "v_$(hash(np))_2_2"), - -x[3] - variable_by_name(model, "v_$(hash(np))_2_3"), - -x[4] - variable_by_name(model, "v_$(hash(np))_2_4") - ] - - @test ref_np[3].func == [ - variable_by_name(model, "v_$(hash(np))_1_1") * y[2] + - variable_by_name(model, "v_$(hash(np))_2_1") * y[2] + y[2], - variable_by_name(model, "v_$(hash(np))_1_2") * y[2] + - variable_by_name(model, "v_$(hash(np))_2_2") * y[2] + y[2], - variable_by_name(model, "v_$(hash(np))_1_3") * y[2] + - variable_by_name(model, "v_$(hash(np))_2_3") * y[2] + y[2], - variable_by_name(model, "v_$(hash(np))_1_4") * y[2] + - variable_by_name(model, "v_$(hash(np))_2_4") * y[2] + y[2] - ] - - @test ref_zeros[1].func == [ - 5x[1] - variable_by_name(model, "v_$(hash(zeros))_1_1_1"), - 5x[2] - variable_by_name(model, "v_$(hash(zeros))_1_2_1"), - -variable_by_name(model, "v_$(hash(zeros))_1_3_1"), - -variable_by_name(model, "v_$(hash(zeros))_1_4_1") - ] - - @test ref_zeros[2].func == [ - -variable_by_name(model, "v_$(hash(zeros))_2_1_1"), - -variable_by_name(model, "v_$(hash(zeros))_2_2_1"), - 5x[3] - variable_by_name(model, "v_$(hash(zeros))_2_3_1"), - 5x[4] - variable_by_name(model, "v_$(hash(zeros))_2_4_1") - ] -end - -function test_reformulate_disjunct() - model = GDPModel() - @variable(model, 0 <= x[1:4] <= 1) - @variable(model, Y[1:2], Logical) - @variable(model, W[1:2], Logical) - @disjunction(model, [Y[1], Y[2]]) - @disjunction(model, [W[1], W[2]]) - method = PSplit([[x[1], x[2]], [x[3], x[4]]]) - for i in 1:4 - DP._variable_bounds(model)[x[i]] = DP.set_variable_bound_info(x[i], method) - end - ref_cons1 = Vector{JuMP.AbstractConstraint}() - ref_cons2 = Vector{JuMP.AbstractConstraint}() +# function test_reformulate_disjunct() +# model = GDPModel() +# @variable(model, 0 <= x[1:4] <= 1) +# @variable(model, Y[1:2], Logical) +# @variable(model, W[1:2], Logical) +# @disjunction(model, [Y[1], Y[2]]) +# @disjunction(model, [W[1], W[2]]) +# method = PSplit([[x[1], x[2]], [x[3], x[4]]]) +# for i in 1:4 +# DP._variable_bounds(model)[x[i]] = DP.set_variable_bound_info(x[i], method) +# end +# ref_cons1 = Vector{JuMP.AbstractConstraint}() +# ref_cons2 = Vector{JuMP.AbstractConstraint}() - good_quad = @constraint(model, x[1]^2 + x[2]^2 <= 1, Disjunct(W[2])) - good_quad2 = @constraint(model, x[3]^2 + x[4]^2 <= 1, Disjunct(W[2])) - affexpr = @constraint(model, x[1] + x[2] <= 1, Disjunct(W[1])) - nonlinear_con = @constraint(model, exp(x[1]) <= 1, Disjunct(Y[1])) - bad_quad = @constraint(model, x[1]*x[2] <= 1, Disjunct(Y[2])) - - @test_throws ErrorException DP._reformulate_disjunct(model, ref_cons1, Y[2], method) - @test_throws ErrorException DP._reformulate_disjunct(model, ref_cons2, Y[1], method) +# good_quad = @constraint(model, x[1]^2 + x[2]^2 <= 1, Disjunct(W[2])) +# good_quad2 = @constraint(model, x[3]^2 + x[4]^2 <= 1, Disjunct(W[2])) +# affexpr = @constraint(model, x[1] + x[2] <= 1, Disjunct(W[1])) +# nonlinear_con = @constraint(model, exp(x[1]) <= 1, Disjunct(Y[1])) +# bad_quad = @constraint(model, x[1]*x[2] <= 1, Disjunct(Y[2])) + +# @test_throws ErrorException DP._reformulate_disjunct(model, ref_cons1, Y[2], method) +# @test_throws ErrorException DP._reformulate_disjunct(model, ref_cons2, Y[1], method) - DP._reformulate_disjunct(model, ref_cons1, W[2], method) - DP._reformulate_disjunct(model, ref_cons2, W[1], method) +# DP._reformulate_disjunct(model, ref_cons1, W[2], method) +# DP._reformulate_disjunct(model, ref_cons2, W[1], method) - @test length(ref_cons1) == 6 - @test length(ref_cons2) == 3 -end +# @test length(ref_cons1) == 6 +# @test length(ref_cons2) == 3 +# end @@ -247,6 +112,4 @@ end test_build_partitioned_expression() test_set_variable_bound_info() test_bound_auxiliary() - test_reformulate_disjunct_constraint_affexpr() - test_reformulate_disjunct() end \ No newline at end of file diff --git a/test/solve.jl b/test/solve.jl index 0134e60..f27de8d 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -55,14 +55,14 @@ function test_linear_gdp_example(m, use_complements = false) @test !value(W[2]) end -function test_quadratic_gdp_example(use_complements = false) +function test_quadratic_gdp_example(use_complements = false) #psplit does not work with complements ipopt = optimizer_with_attributes(Ipopt.Optimizer,"print_level"=>0,"sb"=>"yes") optimizer = optimizer_with_attributes(Juniper.Optimizer, "nl_solver"=>ipopt) m = GDPModel(optimizer) set_attribute(m, MOI.Silent(), true) @variable(m, 0 ≤ x[1:2] ≤ 10) - if !use_complements + if use_complements @variable(m, Y1, Logical) @variable(m, Y2, Logical, logical_complement = Y1) Y = [Y1, Y2] From 7431a3c517392c53c341e920a2e74f7d82e0b64d Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Fri, 10 Oct 2025 11:05:02 -0400 Subject: [PATCH 29/39] revision to fix hull --- src/hull.jl | 10 ++++++---- src/psplit.jl | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/hull.jl b/src/hull.jl index e56d3f8..59dc1ef 100644 --- a/src/hull.jl +++ b/src/hull.jl @@ -2,6 +2,7 @@ # VARIABLE DISAGGREGATION ################################################################################ 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`.") @@ -64,17 +65,18 @@ end # VARIABLE AGGREGATION ################################################################################ function _aggregate_variable( - model::JuMP.AbstractModel, + model::M, ref_cons::Vector{JuMP.AbstractConstraint}, - vref::JuMP.AbstractVariableRef, + vref::V, method::_Hull - ) + ) where {M <: JuMP.AbstractModel, V <: JuMP.AbstractVariableRef} JuMP.is_binary(vref) && return #skip binary variables # con_expr = JuMP.@expression(model, -vref + sum(method.disjunction_variables[vref])) # push!(ref_cons, JuMP.build_constraint(error, con_expr, _MOI.EqualTo(0))) - expr = JuMP.AffExpr(0.0) + expr = Base.zero(JuMP.GenericAffExpr{JuMP.value_type(M), V}) JuMP.add_to_expression!(expr, -1.0, vref) + #TODO: One(T) for dv in method.disjunction_variables[vref] JuMP.add_to_expression!(expr, 1.0, dv) end diff --git a/src/psplit.jl b/src/psplit.jl index e3ef400..09ec73a 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -202,6 +202,7 @@ function reformulate_disjunction(model::JuMP.AbstractModel, disj::Disjunction, m psplit = _PSplit(method, model) psplit.hull = _Hull(Hull(), disj_vrefs) psplit.partitioned_constraints = partitioned_constraints + #TODO: Copy over _disaggregate_variables from Hull for d in disj.indicators _disaggregate_variables(model, d, disj_vrefs, psplit.hull) _reformulate_disjunct(model, ref_cons, d, psplit) From 1111cc7658a9254b6091d418f6f4d4abdf6ce485 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Thu, 16 Oct 2025 14:48:41 -0400 Subject: [PATCH 30/39] extended hull p-split works with selective dissaggregation --- src/datatypes.jl | 2 +- src/psplit.jl | 89 +++++++++++++++++++++++++++--------------------- test/solve.jl | 1 + 3 files changed, 52 insertions(+), 40 deletions(-) diff --git a/src/datatypes.jl b/src/datatypes.jl index 8f499be..dc8c874 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -451,7 +451,7 @@ end # temp struct to store variable disaggregations (reset for each disjunction) mutable struct _PSplit{V <: JuMP.AbstractVariableRef, M <: JuMP.AbstractModel} <: AbstractReformulationMethod partition::Vector{Vector{V}} - partitioned_constraints::Dict{LogicalVariableRef{M}, Vector{<:AbstractConstraint}} + sum_constraints::Dict{LogicalVariableRef{M}, Vector{<:AbstractConstraint}} hull::_Hull function _PSplit(method::PSplit{V}, model::M) where {V <: JuMP.AbstractVariableRef, M <: JuMP.AbstractModel} new{V, M}( diff --git a/src/psplit.jl b/src/psplit.jl index 09ec73a..7cd8e98 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -143,8 +143,8 @@ function _bound_auxiliary( # Add constant term const_term = func.aff.constant - lower_bound += const_term - upper_bound += const_term + # lower_bound += const_term + # upper_bound += const_term JuMP.set_lower_bound(v, lower_bound) JuMP.set_upper_bound(v, upper_bound) @@ -172,9 +172,9 @@ function _bound_auxiliary( _variable_bounds(model)[v] = set_variable_bound_info(v, method) end -requires_variable_bound_info(method::PSplit) = true +requires_variable_bound_info(method::Union{PSplit, _PSplit}) = true -function set_variable_bound_info(vref::JuMP.AbstractVariableRef, ::PSplit) +function set_variable_bound_info(vref::JuMP.AbstractVariableRef, ::Union{PSplit, _PSplit}) if !has_lower_bound(vref) || !has_upper_bound(vref) error("Variable $vref must have both lower and upper bounds defined when using the PSplit reformulation." @@ -193,22 +193,30 @@ end function reformulate_disjunction(model::JuMP.AbstractModel, disj::Disjunction, method::PSplit{V}) where {V <: JuMP.AbstractVariableRef} ref_cons = Vector{JuMP.AbstractConstraint}() #store reformulated constraints disj_vrefs = _get_disjunction_variables(model, disj) - partitioned_constraints = Dict{LogicalVariableRef, Vector{<:JuMP.AbstractConstraint}}() + sum_constraints = Dict{LogicalVariableRef, Vector{<:JuMP.AbstractConstraint}}() + aux_vars = Set{V}() for d in disj.indicators - partitioned_constraints[d], aux_vars = _partition_disjunct(model, d, method) - disj_vrefs = union(disj_vrefs, aux_vars) + partitioned_constraints, sum_constraints[d], vars = _partition_disjunct(model, d, method) + append!(ref_cons, partitioned_constraints) + union!(aux_vars, vars) end - #TODO: This can probably be done more efficiently? psplit = _PSplit(method, model) - psplit.hull = _Hull(Hull(), disj_vrefs) - psplit.partitioned_constraints = partitioned_constraints + psplit.hull = _Hull(Hull(), union(disj_vrefs, aux_vars)) + psplit.sum_constraints = sum_constraints #TODO: Copy over _disaggregate_variables from Hull for d in disj.indicators - _disaggregate_variables(model, d, disj_vrefs, psplit.hull) + bvref = binary_variable(d) + for vref in disj_vrefs + if JuMP.is_binary(vref) + continue # skip variables that don't require dissagregation + end + push!(psplit.hull.disjunction_variables[vref], vref) + psplit.hull.disjunct_variables[vref, bvref] = vref + end + _disaggregate_variables(model, d, aux_vars, psplit.hull) _reformulate_disjunct(model, ref_cons, d, psplit) end - - for vref in disj_vrefs + for vref in aux_vars _aggregate_variable(model, ref_cons, vref, psplit.hull) end return ref_cons @@ -226,8 +234,8 @@ function _reformulate_disjunct( ) #reformulate each constraint and add to the model bvref = binary_variable(lvref) - haskey(method.partitioned_constraints, lvref) || return - constraints = method.partitioned_constraints[lvref] + haskey(method.sum_constraints, lvref) || return + constraints = method.sum_constraints[lvref] for con in constraints append!(ref_cons, reformulate_disjunct_constraint(model, con, bvref, method.hull)) end @@ -238,16 +246,18 @@ function _partition_disjunct(model::M, lvref::LogicalVariableRef, method::PSplit !haskey(_indicator_to_constraints(model), lvref) && return #skip if disjunct is empty partitioned_constraints = Vector{AbstractConstraint}() + sum_constraints = Vector{AbstractConstraint}() aux_vars = Set{JuMP.AbstractVariableRef}() for cref in _indicator_to_constraints(model)[lvref] con = JuMP.constraint_object(cref) if !(con isa Disjunction) - p_constraint, new_aux_vars = _build_partitioned_constraint(model, con, method) + p_constraint, sum_constraint, new_aux_vars = _build_partitioned_constraint(model, con, method) append!(partitioned_constraints, p_constraint) + append!(sum_constraints, sum_constraint) union!(aux_vars, new_aux_vars) end end - return partitioned_constraints, aux_vars + return partitioned_constraints, sum_constraints, aux_vars end # ################################################################################ @@ -262,7 +272,7 @@ function _build_partitioned_constraint( p = length(method.partition) v = [@variable(model, base_name = "v_$(hash(con))_$(i)") for i in 1:p] _, constant = _build_partitioned_expression(con.func, method.partition[p]) - part_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) + part_con = Vector{JuMP.AbstractConstraint}(undef, p) for i in 1:p func, _ = _build_partitioned_expression(con.func, method.partition[i]) part_con[i] = JuMP.build_constraint(error, func - v[i], @@ -270,10 +280,11 @@ function _build_partitioned_constraint( ) _bound_auxiliary(model, v[i], func, method) end - part_con[end] = JuMP.build_constraint(error, sum(v[i] for i in 1:p) + sum_con = JuMP.build_constraint(error, sum(v[i] for i in 1:p) ,MOI.LessThan(con.set.upper - constant) ) - return part_con, v + + return part_con, [sum_con], v end function _build_partitioned_constraint( @@ -283,7 +294,7 @@ function _build_partitioned_constraint( ) where {M <: JuMP.AbstractModel, T, S <: _MOI.GreaterThan} val_type = JuMP.value_type(M) p = length(method.partition) - part_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) + part_con = Vector{JuMP.AbstractConstraint}(undef, p) v = [@variable(model, base_name = "v_$(hash(con))_$(i)") for i in 1:p] _, constant = _build_partitioned_expression(con.func, method.partition[p]) for i in 1:p @@ -293,10 +304,10 @@ function _build_partitioned_constraint( ) _bound_auxiliary(model, v[i], -func, method) end - part_con[end] = JuMP.build_constraint(error, sum(v[i] for i in 1:p) + sum_con = JuMP.build_constraint(error, sum(v[i] for i in 1:p) , MOI.LessThan(-con.set.lower + constant) ) - return part_con, v + return part_con, [sum_con], v end function _build_partitioned_constraint( @@ -306,8 +317,8 @@ function _build_partitioned_constraint( ) where {M <: JuMP.AbstractModel, T, S <: Union{_MOI.Interval, _MOI.EqualTo}} val_type = JuMP.value_type(M) p = length(method.partition) - part_con_lt = Vector{JuMP.AbstractConstraint}(undef, p + 1) - part_con_gt = Vector{JuMP.AbstractConstraint}(undef, p + 1) + part_con_lt = Vector{JuMP.AbstractConstraint}(undef, p) + part_con_gt = Vector{JuMP.AbstractConstraint}(undef, p) #let [_, 1] be the upper bound and [_, 2] be the lower bound _, constant = _build_partitioned_expression(con.func, method.partition[p]) v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)") for i in 1:p, j in 1:2] @@ -323,13 +334,13 @@ function _build_partitioned_constraint( _bound_auxiliary(model, v[i,2], -func, method) end set_values = _set_values(con.set) - part_con_lt[end] = JuMP.build_constraint(error, sum(v[i,1] for i in 1:p), + sum_con_lt = JuMP.build_constraint(error, sum(v[i,1] for i in 1:p), MOI.LessThan((set_values[2] - constant)) ) - part_con_gt[end] = JuMP.build_constraint(error, sum(v[i,2] for i in 1:p), + sum_con_gt = JuMP.build_constraint(error, sum(v[i,2] for i in 1:p), MOI.LessThan(-set_values[1] + constant) ) - return vcat(part_con_lt, part_con_gt), vec(v) + return vcat(part_con_lt, part_con_gt), [sum_con_lt, sum_con_gt], vec(v) end function _build_partitioned_constraint( model::M, @@ -339,7 +350,7 @@ function _build_partitioned_constraint( p = length(method.partition) d = con.set.dimension v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)") for i in 1:p, j in 1:d] - part_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) + part_con = Vector{JuMP.AbstractConstraint}(undef, p) constants = Vector{Number}(undef, d) for i in 1:p part_expr = [_build_partitioned_expression(con.func[j], @@ -357,8 +368,8 @@ function _build_partitioned_constraint( new_func = JuMP.@expression(model,[j = 1:d], sum(v[i,j] for i in 1:p) + constants[j] ) - part_con[end] = JuMP.build_constraint(error, new_func, _MOI.Nonpositives(d)) - return vcat(part_con), vec(v) + sum_con = JuMP.build_constraint(error, new_func, _MOI.Nonpositives(d)) + return part_con, [sum_con], vec(v) end function _build_partitioned_constraint( @@ -369,7 +380,7 @@ function _build_partitioned_constraint( p = length(method.partition) d = con.set.dimension v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)") for i in 1:p, j in 1:d] - part_con = Vector{JuMP.AbstractConstraint}(undef, p + 1) + part_con = Vector{JuMP.AbstractConstraint}(undef, p) constants = Vector{Number}(undef, d) for i in 1:p part_expr = [_build_partitioned_expression(con.func[j], method.partition[i]) for j in 1:d] @@ -383,8 +394,8 @@ function _build_partitioned_constraint( new_func = JuMP.@expression(model,[j = 1:d], sum(v[i,j] for i in 1:p) + constants[j] ) - part_con[end] = JuMP.build_constraint(error,new_func,_MOI.Nonpositives(d)) - return vcat(part_con), vec(v) + sum_con = JuMP.build_constraint(error,new_func,_MOI.Nonpositives(d)) + return part_con, [sum_con], vec(v) end function _build_partitioned_constraint( @@ -394,8 +405,8 @@ function _build_partitioned_constraint( ) where {M <: JuMP.AbstractModel, T, S <: _MOI.Zeros, R} p = length(method.partition) d = con.set.dimension - part_con_np = Vector{JuMP.AbstractConstraint}(undef, p + 1) # nonpositive (≤ 0) - part_con_nn = Vector{JuMP.AbstractConstraint}(undef, p + 1) # nonnegative (≥ 0) + part_con_np = Vector{JuMP.AbstractConstraint}(undef, p) # nonpositive (≤ 0) + part_con_nn = Vector{JuMP.AbstractConstraint}(undef, p) # nonnegative (≥ 0) v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)_$(k)") for i in 1:p, j in 1:d, k in 1:2 ] @@ -424,13 +435,13 @@ function _build_partitioned_constraint( new_func_nn = JuMP.@expression(model,[j = 1:d], -sum(v[i,j,2] for i in 1:p) - constants[j] ) - part_con_np[end] = JuMP.build_constraint(error, + sum_con_np = JuMP.build_constraint(error, new_func_np, _MOI.Nonpositives(d) ) - part_con_nn[end] = JuMP.build_constraint(error, + sum_con_nn = JuMP.build_constraint(error, new_func_nn, _MOI.Nonpositives(d) ) - return vcat(part_con_np, part_con_nn), vec(v) + return vcat(part_con_np, part_con_nn), vcat(sum_con_np, sum_con_nn), vec(v) end ################################################################################ diff --git a/test/solve.jl b/test/solve.jl index f27de8d..71d062d 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -106,6 +106,7 @@ function test_quadratic_gdp_example(use_complements = false) #psplit does not wo partition = [[x[1]], [x[2]]] @test optimize!(m, gdp_method = PSplit(partition)) isa Nothing + # println(m) @test termination_status(m) in [MOI.OPTIMAL, MOI.LOCALLY_SOLVED] @test objective_value(m) ≈ 6.1237 atol=1e-3 @test value.(x) ≈ [4.0825, 2.0412] atol=1e-3 From 597dc4defd428c5585784a9d6c5805bef24a6773 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Thu, 16 Oct 2025 21:17:30 -0400 Subject: [PATCH 31/39] added tests --- README.md | 4 + src/datatypes.jl | 43 +++++++- src/psplit.jl | 26 ++--- test/constraints/psplit.jl | 220 ++++++++++++++++++++++++++++++++----- test/solve.jl | 2 +- 5 files changed, 246 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index dcc1ebe..5d68bae 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,10 @@ The following reformulation methods are currently supported: 5. [P-Split](https://arxiv.org/abs/2202.05198): This method reformulates each disjunct constraint into P constraints, each with a partitioned group defined by the user. This method requires that terms in the constraint be convex additively seperable with respect to each variable. The `PSplit` struct is created with the following required arguments: - `partition`: Partition of the variables to be split. All variables must be in exactly one partition. (e.g., The variables `x[1:4]` can be partitioned into two groups ` partition = [[x[1], x[2]], [x[3], x[4]]]`) + - `PSplit(n_parts, model)`: Automatically partition all variables in the model into `n_parts` groups + + All variables must be included in exactly one partition. For manual partitioning, ensure each variable appears in exactly one group. For automatic partitioning, variables are divided as evenly as possible among the specified number of partitions. + ## Release Notes diff --git a/src/datatypes.jl b/src/datatypes.jl index dc8c874..47e8be0 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -434,11 +434,24 @@ end """ PSplit <: AbstractReformulationMethod -A type for using the P-split reformulation approach for disjunctive -constraints. +A type for using the P-split reformulation approach for disjunctive constraints. +This method partitions variables into groups and handles each group separately. -**Fields** -- `partition::Vector{Vector{V}}`: The partition of variables +# Constructors +- `PSplit(partition::Vector{Vector{V}})`: Create a PSplit with the given partition of variables +- `PSplit(n_parts::Int, model::JuMP.AbstractModel)`: Automatically partition model variables into `n_parts` groups + +# Fields +- `partition::Vector{Vector{V}}`: The partition of variables, where each inner vector represents a group of variables that will be handled together + +# Example +```julia +# Manual partitioning +method1 = PSplit([[x[1], x[2]], [x[3], x[4]]]) + +# Automatic partitioning (splits variables into 2 groups) +method2 = PSplit(2, model) +``` """ struct PSplit{V <: JuMP.AbstractVariableRef} <: AbstractReformulationMethod partition::Vector{Vector{V}} @@ -446,6 +459,28 @@ struct PSplit{V <: JuMP.AbstractVariableRef} <: AbstractReformulationMethod function PSplit(partition::Vector{Vector{V}}) where {V <: JuMP.AbstractVariableRef} new{V}(partition) end + + function PSplit(n_parts::Int, model::JuMP.AbstractModel) + n_parts > 0 || error("Number of partitions must be positive, got $n_parts") + variables = collect(JuMP.all_variables(model)) + n_vars = length(variables) + part_size = cld(n_vars, n_parts) + + # Create the partition by slicing the variables + partition = Vector{Vector{eltype(variables)}}() + for i in 1:n_parts + start_idx = (i-1) * part_size + 1 + end_idx = min(i * part_size, n_vars) + if start_idx > n_vars + push!(partition, eltype(variables)[]) + else + push!(partition, variables[start_idx:end_idx]) + end + end + + # Call the outer constructor + return PSplit(partition) + end end # temp struct to store variable disaggregations (reset for each disjunction) diff --git a/src/psplit.jl b/src/psplit.jl index 7cd8e98..653aa8e 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -63,8 +63,10 @@ function _bound_auxiliary( method::PSplit ) where {M <: JuMP.AbstractModel} T = JuMP.value_type(M) - lower_bound = zero(T) - upper_bound = zero(T) + + lower_bound = has_lower_bound(v) ? lower_bound(v) : zero(T) + upper_bound = has_upper_bound(v) ? upper_bound(v) : zero(T) + for (var, coeff) in func.terms if var != v JuMP.is_binary(var) && continue @@ -102,23 +104,11 @@ function _bound_auxiliary( method::PSplit ) where {M <: JuMP.AbstractModel} T = JuMP.value_type(M) - lower_bound = zero(T) - upper_bound = zero(T) # Handle linear terms - for (var, coeff) in func.aff.terms - if var != v - JuMP.is_binary(var) && continue - var_lb, var_ub = variable_bound_info(var) - if coeff > 0.0 - lower_bound += coeff * var_lb - upper_bound += coeff * var_ub - else - lower_bound += coeff * var_ub - upper_bound += coeff * var_lb - end - end - end + _bound_auxiliary(model, v, func.aff, method) + lower_bound = JuMP.lower_bound(v) + upper_bound = JuMP.upper_bound(v) # Handle quadratic terms for (vars, coeff) in func.terms @@ -159,7 +149,7 @@ function _bound_auxiliary( ) where {M <: JuMP.AbstractModel} T = JuMP.value_type(M) lower_bound = zero(T) - upper_bound = zero(T) + upper_bound = zero(T) if func != v lower_bound = variable_bound_info(func)[1] upper_bound = variable_bound_info(func)[2] diff --git a/test/constraints/psplit.jl b/test/constraints/psplit.jl index 4f0ed90..f575c54 100644 --- a/test/constraints/psplit.jl +++ b/test/constraints/psplit.jl @@ -30,6 +30,10 @@ function test_build_partitioned_expression() ) end + + + + function test_bound_auxiliary() model = GDPModel() @variable(model, 0 <= x[1:4] <= 3) @@ -64,38 +68,200 @@ function test_bound_auxiliary() @test_throws ErrorException DP._bound_auxiliary(model, v[3], nl, method) end +# _build_partitioned_constraint -# function test_reformulate_disjunct() -# model = GDPModel() -# @variable(model, 0 <= x[1:4] <= 1) -# @variable(model, Y[1:2], Logical) -# @variable(model, W[1:2], Logical) -# @disjunction(model, [Y[1], Y[2]]) -# @disjunction(model, [W[1], W[2]]) -# method = PSplit([[x[1], x[2]], [x[3], x[4]]]) -# for i in 1:4 -# DP._variable_bounds(model)[x[i]] = DP.set_variable_bound_info(x[i], method) -# end -# ref_cons1 = Vector{JuMP.AbstractConstraint}() -# ref_cons2 = Vector{JuMP.AbstractConstraint}() +function test_build_partitioned_constraint() + model = GDPModel() + @variable(model, 0 <= x[1:4] <= 3) + @variable(model, y[1:2], Bin) + method = PSplit([[x[1], x[2]], [x[3], x[4]]]) + for i in 1:4 + DP._variable_bounds(model)[x[i]] = DP.set_variable_bound_info( + x[i], + method + ) + end + + lt = JuMP.constraint_object(@constraint(model, x[1] + x[3] <= 1)) + gt = JuMP.constraint_object(@constraint(model, x[2] + x[4] >= 1)) + eq = JuMP.constraint_object(@constraint(model, x[3] + x[4] == 1)) + interval = JuMP.constraint_object( + @constraint(model, 0 <= x[1] + x[2] + x[3] <= 0.5) + ) + nn = JuMP.constraint_object(@constraint(model, x .- 1 >= 0)) + np = JuMP.constraint_object(@constraint(model, -x .+ 1 <= 0)) + zeros = JuMP.constraint_object(@constraint(model, 5x .- 1 == 0)) -# good_quad = @constraint(model, x[1]^2 + x[2]^2 <= 1, Disjunct(W[2])) -# good_quad2 = @constraint(model, x[3]^2 + x[4]^2 <= 1, Disjunct(W[2])) -# affexpr = @constraint(model, x[1] + x[2] <= 1, Disjunct(W[1])) -# nonlinear_con = @constraint(model, exp(x[1]) <= 1, Disjunct(Y[1])) -# bad_quad = @constraint(model, x[1]*x[2] <= 1, Disjunct(Y[2])) - -# @test_throws ErrorException DP._reformulate_disjunct(model, ref_cons1, Y[2], method) -# @test_throws ErrorException DP._reformulate_disjunct(model, ref_cons2, Y[1], method) + ref_lt, lt_sum, lt_vars = DP._build_partitioned_constraint(model, lt, method) + ref_gt, gt_sum, gt_vars = DP._build_partitioned_constraint(model, gt, method) + ref_eq, eq_sum, eq_vars = DP._build_partitioned_constraint(model, eq, method) + ref_interval, interval_sum, interval_vars = DP._build_partitioned_constraint( + model, interval, method + ) + ref_nn, nn_sum, nn_vars = DP._build_partitioned_constraint(model, nn, method) + ref_np, np_sum, np_vars = DP._build_partitioned_constraint(model, np, method) + ref_zeros, zeros_sum, zeros_vars = DP._build_partitioned_constraint(model, zeros, method) + + @test ref_lt[1].func == x[1] - + variable_by_name(model, "v_$(hash(lt))_1") + @test ref_lt[2].func == x[3] - + variable_by_name(model, "v_$(hash(lt))_2") + @test lt_sum[1].func == + variable_by_name(model, "v_$(hash(lt))_1") + + variable_by_name(model, "v_$(hash(lt))_2") + @test ref_gt[1].func == -x[2] - + variable_by_name(model, "v_$(hash(gt))_1") + @test ref_gt[2].func == -x[4] - + variable_by_name(model, "v_$(hash(gt))_2") + @test gt_sum[1].func == + variable_by_name(model, "v_$(hash(gt))_1") + + variable_by_name(model, "v_$(hash(gt))_2") + @test ref_eq[1].func == + -variable_by_name(model, "v_$(hash(eq))_1_1") + @test ref_eq[2].func == x[3] + x[4] - + variable_by_name(model, "v_$(hash(eq))_2_1") + @test ref_eq[4].func == -x[3] - x[4] - + variable_by_name(model, "v_$(hash(eq))_2_2") + @test eq_sum[1].func == + variable_by_name(model, "v_$(hash(eq))_1_1") + + variable_by_name(model, "v_$(hash(eq))_2_1") + @test eq_sum[2].func == + variable_by_name(model, "v_$(hash(eq))_1_2") + + variable_by_name(model, "v_$(hash(eq))_2_2") + @test ref_interval[1].func == x[1] + x[2] - + variable_by_name(model, "v_$(hash(interval))_1_1") + @test ref_interval[2].func == x[3] - + variable_by_name(model, "v_$(hash(interval))_2_1") + #@test ref_interval[5].func == -x[1] - x[2] - + # variable_by_name(model, "v_$(hash(interval))_1_2") + @test ref_interval[4].func == -x[3] - + variable_by_name(model, "v_$(hash(interval))_2_2") + @test interval_sum[1].func == + variable_by_name(model, "v_$(hash(interval))_1_1") + + variable_by_name(model, "v_$(hash(interval))_2_1") + + @test ref_nn[1].func == [ + -x[1] - variable_by_name(model, "v_$(hash(nn))_1_1"), + -x[2] - variable_by_name(model, "v_$(hash(nn))_1_2"), + -variable_by_name(model, "v_$(hash(nn))_1_3"), + -variable_by_name(model, "v_$(hash(nn))_1_4") + ] + + @test ref_nn[2].func == [ + -variable_by_name(model, "v_$(hash(nn))_2_1"), + -variable_by_name(model, "v_$(hash(nn))_2_2"), + -x[3] - variable_by_name(model, "v_$(hash(nn))_2_3"), + -x[4] - variable_by_name(model, "v_$(hash(nn))_2_4") + ] + + @test nn_sum[1].func == [ + variable_by_name(model, "v_$(hash(nn))_1_1") + + variable_by_name(model, "v_$(hash(nn))_2_1") + 1, + variable_by_name(model, "v_$(hash(nn))_1_2") + + variable_by_name(model, "v_$(hash(nn))_2_2") + 1, + variable_by_name(model, "v_$(hash(nn))_1_3") + + variable_by_name(model, "v_$(hash(nn))_2_3") + 1, + variable_by_name(model, "v_$(hash(nn))_1_4") + + variable_by_name(model, "v_$(hash(nn))_2_4") + 1 +] + + + @test ref_np[1].func == [ + -x[1] - variable_by_name(model, "v_$(hash(np))_1_1"), + -x[2] - variable_by_name(model, "v_$(hash(np))_1_2"), + -variable_by_name(model, "v_$(hash(np))_1_3"), + -variable_by_name(model, "v_$(hash(np))_1_4") + ] + + @test ref_np[2].func == [ + -variable_by_name(model, "v_$(hash(np))_2_1"), + -variable_by_name(model, "v_$(hash(np))_2_2"), + -x[3] - variable_by_name(model, "v_$(hash(np))_2_3"), + -x[4] - variable_by_name(model, "v_$(hash(np))_2_4") + ] + + @test np_sum[1].func == [ + variable_by_name(model, "v_$(hash(np))_1_1") + + variable_by_name(model, "v_$(hash(np))_2_1") + 1, + variable_by_name(model, "v_$(hash(np))_1_2") + + variable_by_name(model, "v_$(hash(np))_2_2") + 1, + variable_by_name(model, "v_$(hash(np))_1_3") + + variable_by_name(model, "v_$(hash(np))_2_3") + 1, + variable_by_name(model, "v_$(hash(np))_1_4") + + variable_by_name(model, "v_$(hash(np))_2_4") + 1 +] + + + @test ref_zeros[1].func == [ + 5x[1] - variable_by_name(model, "v_$(hash(zeros))_1_1_1"), + 5x[2] - variable_by_name(model, "v_$(hash(zeros))_1_2_1"), + -variable_by_name(model, "v_$(hash(zeros))_1_3_1"), + -variable_by_name(model, "v_$(hash(zeros))_1_4_1") + ] + + @test ref_zeros[2].func == [ + -variable_by_name(model, "v_$(hash(zeros))_2_1_1"), + -variable_by_name(model, "v_$(hash(zeros))_2_2_1"), + 5x[3] - variable_by_name(model, "v_$(hash(zeros))_2_3_1"), + 5x[4] - variable_by_name(model, "v_$(hash(zeros))_2_4_1") + ] +end + +# _partition_disjunct + +function test_partition_disjunct() + model = GDPModel() + @variable(model, 0<= x[1:4] <= 10) + @variable(model, Y[1:2], Logical) + method = PSplit([[x[1], x[2]], [x[3], x[4]]]) + + con1 = @constraint(model, x[1] + x[2] <= 1, Disjunct(Y[1])) + + for i in 1:4 + DP._variable_bounds(model)[x[i]] = DP.set_variable_bound_info(x[i], method) + DP._bound_auxiliary(model, x[i], x[i], method) + end -# DP._reformulate_disjunct(model, ref_cons1, W[2], method) -# DP._reformulate_disjunct(model, ref_cons2, W[1], method) + partitioned_constraints, sum_constraints, aux_vars = DP._partition_disjunct(model, Y[1], method) + @test partitioned_constraints[1].func == x[1] + x[2] - variable_by_name(model, "v_$(hash(JuMP.constraint_object(con1)))_1") + @test partitioned_constraints[2].func == -variable_by_name(model, "v_$(hash(JuMP.constraint_object(con1)))_2") + + @test sum_constraints[1].func == variable_by_name(model, "v_$(hash(JuMP.constraint_object(con1)))_1") + + variable_by_name(model, "v_$(hash(JuMP.constraint_object(con1)))_2") + @test sum_constraints[1].set == MOI.LessThan(1.0) + @test aux_vars == Set{JuMP.AbstractVariableRef}([variable_by_name(model, "v_$(hash(JuMP.constraint_object(con1)))_1"), variable_by_name(model, "v_$(hash(JuMP.constraint_object(con1)))_2")]) +end -# @test length(ref_cons1) == 6 -# @test length(ref_cons2) == 3 -# end +# reformulate_disjunction +function test_reformulate_disjunction() + model = GDPModel() + @variable(model, 0 <= x[1:4] <= 1) + @variable(model, Y[1:2], Logical) + @variable(model, W[1:2], Logical) + @disjunction(model, [Y[1], Y[2]]) + @disjunction(model, [W[1], W[2]]) + method = PSplit([[x[1], x[2]], [x[3], x[4]]]) + for i in 1:4 + DP._variable_bounds(model)[x[i]] = DP.set_variable_bound_info(x[i], method) + end + ref_cons1 = Vector{JuMP.AbstractConstraint}() + ref_cons2 = Vector{JuMP.AbstractConstraint}() + + good_quad = @constraint(model, x[1]^2 + x[2]^2 <= 1, Disjunct(W[2])) + good_quad2 = @constraint(model, x[3]^2 + x[4]^2 <= 1, Disjunct(W[2])) + affexpr = @constraint(model, x[1] + x[2] <= 1, Disjunct(W[1])) + nonlinear_con = @constraint(model, exp(x[1]) <= 1, Disjunct(Y[1])) + bad_quad = @constraint(model, x[1]*x[2] <= 1, Disjunct(Y[2])) + @test_throws ErrorException DP._reformulate_disjunct(model, ref_cons1, Y[2], method) + @test_throws ErrorException DP._reformulate_disjunct(model, ref_cons2, Y[1], method) + + DP._reformulate_disjunct(model, ref_cons1, W[2], method) + DP._reformulate_disjunct(model, ref_cons2, W[1], method) + @test length(ref_cons1) == 6 + @test length(ref_cons2) == 3 +end function test_set_variable_bound_info() model = GDPModel() @@ -112,4 +278,6 @@ end test_build_partitioned_expression() test_set_variable_bound_info() test_bound_auxiliary() + test_build_partitioned_constraint() + test_partition_disjunct() end \ No newline at end of file diff --git a/test/solve.jl b/test/solve.jl index 71d062d..04e104c 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -105,7 +105,7 @@ function test_quadratic_gdp_example(use_complements = false) #psplit does not wo @test !value(W[2]) partition = [[x[1]], [x[2]]] - @test optimize!(m, gdp_method = PSplit(partition)) isa Nothing + @test optimize!(m, gdp_method = PSplit(2,m)) isa Nothing # println(m) @test termination_status(m) in [MOI.OPTIMAL, MOI.LOCALLY_SOLVED] @test objective_value(m) ≈ 6.1237 atol=1e-3 From 9f801b5042e47cc8abeff82229ea3ea47ec96a9e Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Fri, 17 Oct 2025 12:10:55 -0400 Subject: [PATCH 32/39] fixed parameter typing for zero(T) --- src/psplit.jl | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/psplit.jl b/src/psplit.jl index 455dff0..e8213d7 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -62,8 +62,7 @@ function _bound_auxiliary( v::JuMP.AbstractVariableRef, func::JuMP.GenericAffExpr{T,V}, method::PSplit -) where {M <: JuMP.AbstractModel} - T = JuMP.value_type(M) +) where {M <: JuMP.AbstractModel, T, V} lower_bound = has_lower_bound(v) ? lower_bound(v) : zero(T) upper_bound = has_upper_bound(v) ? upper_bound(v) : zero(T) @@ -103,8 +102,7 @@ function _bound_auxiliary( v::JuMP.AbstractVariableRef, func::JuMP.GenericQuadExpr{T,V}, method::PSplit -) where {M <: JuMP.AbstractModel} - T = JuMP.value_type(M) +) where {M <: JuMP.AbstractModel, T, V} # Handle linear terms _bound_auxiliary(model, v, func.aff, method) From 870c1e958f8a2619409ad94578463300de018eb6 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Fri, 17 Oct 2025 12:27:28 -0400 Subject: [PATCH 33/39] removed old P-Split datatype, added test for nonlinera expression detection --- src/datatypes.jl | 26 -------------------------- src/psplit.jl | 8 ++++++++ test/constraints/psplit.jl | 11 ++++++++++- 3 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/datatypes.jl b/src/datatypes.jl index 99fcacb..ec78a4e 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -417,23 +417,6 @@ struct Hull{T} <: AbstractReformulationMethod end end -""" - PSplit <: AbstractReformulationMethod - -A type for using the P-split reformulation approach for disjunctive -constraints. - -**Fields** -- `partition::Vector{Vector{V}}`: The partition of variables -""" -struct PSplit{V <: JuMP.AbstractVariableRef} <: AbstractReformulationMethod - partition::Vector{Vector{V}} - - function PSplit(partition::Vector{Vector{V}}) where {V <: JuMP.AbstractVariableRef} - new{V}(partition) - end -end - # temp struct to store variable disaggregations (reset for each disjunction) mutable struct _Hull{V <: JuMP.AbstractVariableRef, T} <: AbstractReformulationMethod value::T @@ -460,15 +443,6 @@ This method partitions variables into groups and handles each group separately. # Fields - `partition::Vector{Vector{V}}`: The partition of variables, where each inner vector represents a group of variables that will be handled together - -# Example -```julia -# Manual partitioning -method1 = PSplit([[x[1], x[2]], [x[3], x[4]]]) - -# Automatic partitioning (splits variables into 2 groups) -method2 = PSplit(2, model) -``` """ struct PSplit{V <: JuMP.AbstractVariableRef} <: AbstractReformulationMethod partition::Vector{Vector{V}} diff --git a/src/psplit.jl b/src/psplit.jl index e8213d7..2be319a 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -54,6 +54,14 @@ function _build_partitioned_expression( return expr, zero(T) end +function _build_partitioned_expression( + expr::T, + partition_variables::Vector{<:JuMP.AbstractVariableRef} +) where {T <: JuMP.GenericNonlinearExpr} + error("P-Split does not currently support nonlinear expressions $(expr)") +end + + ################################################################################ # BOUND AUXILIARY ################################################################################ diff --git a/test/constraints/psplit.jl b/test/constraints/psplit.jl index f575c54..c4b429b 100644 --- a/test/constraints/psplit.jl +++ b/test/constraints/psplit.jl @@ -3,6 +3,12 @@ function test_psplit() @variable(model, x[1:4]) method = PSplit([[x[1], x[2]], [x[3], x[4]]]) @test method.partition == [[x[1], x[2]], [x[3], x[4]]] + + method = PSplit(2, model) + @test length(method.partition) == 2 + @test method.partition[1] == [x[1], x[2]] + @test method.partition[2] == [x[3], x[4]] + end function test_build_partitioned_expression() @@ -23,7 +29,10 @@ function test_build_partitioned_expression() result = DP._build_partitioned_expression(test_case.expr, partition_variables) @test result == test_case.expected end - + @test_throws ErrorException DP._build_partitioned_expression( + exp(x[1]), + partition_variables + ) @test_throws ErrorException DP._build_partitioned_expression( "Bad Input", partition_variables From ad787b48d1e052067acf63168bb6ee21fe75c549 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Fri, 17 Oct 2025 13:37:57 -0400 Subject: [PATCH 34/39] code coverage 100% --- src/datatypes.jl | 40 ++++++++++++++++++++------------------ src/psplit.jl | 4 ---- test/constraints/psplit.jl | 18 ++++++++--------- test/solve.jl | 2 -- 4 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/datatypes.jl b/src/datatypes.jl index ec78a4e..cb3ac10 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -452,25 +452,27 @@ struct PSplit{V <: JuMP.AbstractVariableRef} <: AbstractReformulationMethod end function PSplit(n_parts::Int, model::JuMP.AbstractModel) - n_parts > 0 || error("Number of partitions must be positive, got $n_parts") - variables = collect(JuMP.all_variables(model)) - n_vars = length(variables) - part_size = cld(n_vars, n_parts) - - # Create the partition by slicing the variables - partition = Vector{Vector{eltype(variables)}}() - for i in 1:n_parts - start_idx = (i-1) * part_size + 1 - end_idx = min(i * part_size, n_vars) - if start_idx > n_vars - push!(partition, eltype(variables)[]) - else - push!(partition, variables[start_idx:end_idx]) - end - end - - # Call the outer constructor - return PSplit(partition) + n_parts > 0 || error("Number of partitions must be positive, got $n_parts") + variables = collect(JuMP.all_variables(model)) + n_vars = length(variables) + + n_parts = min(n_parts, n_vars) + n_parts > 0 || error("No variables found in the model") + + base_size = n_vars ÷ n_parts + remaining = n_vars % n_parts + + partition = Vector{Vector{eltype(variables)}}() + start_idx = 1 + + for i in 1:n_parts + part_size = i <= remaining ? base_size + 1 : base_size + end_idx = start_idx + part_size - 1 + push!(partition, variables[start_idx:end_idx]) + start_idx = end_idx + 1 + end + + return PSplit(partition) end end diff --git a/src/psplit.jl b/src/psplit.jl index 2be319a..78f2d2a 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -200,13 +200,9 @@ function reformulate_disjunction(model::JuMP.AbstractModel, disj::Disjunction, m psplit = _PSplit(method, model) psplit.hull = _Hull(Hull(), union(disj_vrefs, aux_vars)) psplit.sum_constraints = sum_constraints - #TODO: Copy over _disaggregate_variables from Hull for d in disj.indicators bvref = binary_variable(d) for vref in disj_vrefs - if JuMP.is_binary(vref) - continue # skip variables that don't require dissagregation - end push!(psplit.hull.disjunction_variables[vref], vref) psplit.hull.disjunct_variables[vref, bvref] = vref end diff --git a/test/constraints/psplit.jl b/test/constraints/psplit.jl index c4b429b..cc07cb5 100644 --- a/test/constraints/psplit.jl +++ b/test/constraints/psplit.jl @@ -4,10 +4,11 @@ function test_psplit() method = PSplit([[x[1], x[2]], [x[3], x[4]]]) @test method.partition == [[x[1], x[2]], [x[3], x[4]]] - method = PSplit(2, model) - @test length(method.partition) == 2 + method = PSplit(3, model) + @test length(method.partition) == 3 @test method.partition[1] == [x[1], x[2]] - @test method.partition[2] == [x[3], x[4]] + @test method.partition[2] == [x[3]] + @test method.partition[3] == [x[4]] end @@ -29,6 +30,11 @@ function test_build_partitioned_expression() result = DP._build_partitioned_expression(test_case.expr, partition_variables) @test result == test_case.expected end + + @test_throws ErrorException DP._build_partitioned_expression( + x[1] * x[2], + partition_variables + ) @test_throws ErrorException DP._build_partitioned_expression( exp(x[1]), partition_variables @@ -77,7 +83,6 @@ function test_bound_auxiliary() @test_throws ErrorException DP._bound_auxiliary(model, v[3], nl, method) end -# _build_partitioned_constraint function test_build_partitioned_constraint() model = GDPModel() @@ -141,8 +146,6 @@ function test_build_partitioned_constraint() variable_by_name(model, "v_$(hash(interval))_1_1") @test ref_interval[2].func == x[3] - variable_by_name(model, "v_$(hash(interval))_2_1") - #@test ref_interval[5].func == -x[1] - x[2] - - # variable_by_name(model, "v_$(hash(interval))_1_2") @test ref_interval[4].func == -x[3] - variable_by_name(model, "v_$(hash(interval))_2_2") @test interval_sum[1].func == @@ -216,8 +219,6 @@ function test_build_partitioned_constraint() ] end -# _partition_disjunct - function test_partition_disjunct() model = GDPModel() @variable(model, 0<= x[1:4] <= 10) @@ -241,7 +242,6 @@ function test_partition_disjunct() @test aux_vars == Set{JuMP.AbstractVariableRef}([variable_by_name(model, "v_$(hash(JuMP.constraint_object(con1)))_1"), variable_by_name(model, "v_$(hash(JuMP.constraint_object(con1)))_2")]) end -# reformulate_disjunction function test_reformulate_disjunction() model = GDPModel() @variable(model, 0 <= x[1:4] <= 1) diff --git a/test/solve.jl b/test/solve.jl index 04e104c..cfd24bd 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -104,9 +104,7 @@ function test_quadratic_gdp_example(use_complements = false) #psplit does not wo @test !value(W[1]) @test !value(W[2]) - partition = [[x[1]], [x[2]]] @test optimize!(m, gdp_method = PSplit(2,m)) isa Nothing - # println(m) @test termination_status(m) in [MOI.OPTIMAL, MOI.LOCALLY_SOLVED] @test objective_value(m) ≈ 6.1237 atol=1e-3 @test value.(x) ≈ [4.0825, 2.0412] atol=1e-3 From 575eb78a42534f09e483f46ca9f0ec536bf35288 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Sun, 19 Oct 2025 15:48:08 -0400 Subject: [PATCH 35/39] project.toml edit to compat section, stylistic edits --- src/datatypes.jl | 68 +++++++++++++++++++++++++++--------------------- src/hull.jl | 1 - src/psplit.jl | 66 +++++++++++++++++++++++++++++++++------------- 3 files changed, 86 insertions(+), 49 deletions(-) diff --git a/src/datatypes.jl b/src/datatypes.jl index cb3ac10..9672683 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -374,7 +374,8 @@ A type for using the multiple big-M reformulation approach for disjunctive const **Fields** - `optimizer::O`: Optimizer to use when solving mini-models (required). -- `default_M::T`: Default big-M value to use if no big-M is specified for a logical variable (1e9). +- `default_M::T`: Default big-M value to use if no big-M is +specified for a logical variable (1e9). """ mutable struct MBM{O, T} <: AbstractReformulationMethod optimizer::O @@ -438,51 +439,58 @@ A type for using the P-split reformulation approach for disjunctive constraints. This method partitions variables into groups and handles each group separately. # Constructors -- `PSplit(partition::Vector{Vector{V}})`: Create a PSplit with the given partition of variables -- `PSplit(n_parts::Int, model::JuMP.AbstractModel)`: Automatically partition model variables into `n_parts` groups +- `PSplit(partition::Vector{Vector{V}})`: Create a PSplit with the given +partition of variables +- `PSplit(n_parts::Int, model::JuMP.AbstractModel)`: Automatically partition +model variables into `n_parts` groups # Fields -- `partition::Vector{Vector{V}}`: The partition of variables, where each inner vector represents a group of variables that will be handled together +- `partition::Vector{Vector{V}}`: The partition of variables, where each inner +vector represents a group of variables that will be handled together """ struct PSplit{V <: JuMP.AbstractVariableRef} <: AbstractReformulationMethod partition::Vector{Vector{V}} - function PSplit(partition::Vector{Vector{V}}) where {V <: JuMP.AbstractVariableRef} + function PSplit(partition::Vector{Vector{V}}) where + {V <: JuMP.AbstractVariableRef} new{V}(partition) end function PSplit(n_parts::Int, model::JuMP.AbstractModel) - n_parts > 0 || error("Number of partitions must be positive, got $n_parts") - variables = collect(JuMP.all_variables(model)) - n_vars = length(variables) - - n_parts = min(n_parts, n_vars) - n_parts > 0 || error("No variables found in the model") - - base_size = n_vars ÷ n_parts - remaining = n_vars % n_parts - - partition = Vector{Vector{eltype(variables)}}() - start_idx = 1 - - for i in 1:n_parts - part_size = i <= remaining ? base_size + 1 : base_size - end_idx = start_idx + part_size - 1 - push!(partition, variables[start_idx:end_idx]) - start_idx = end_idx + 1 - end - - return PSplit(partition) + n_parts > 0 || error("Number of partitions must be + positive, got $n_parts") + variables = collect(JuMP.all_variables(model)) + n_vars = length(variables) + + n_parts = min(n_parts, n_vars) + n_parts > 0 || error("No variables found in the model") + + base_size = n_vars ÷ n_parts + remaining = n_vars % n_parts + + partition = Vector{Vector{eltype(variables)}}() + start_idx = 1 + + for i in 1:n_parts + part_size = i <= remaining ? base_size + 1 : base_size + end_idx = start_idx + part_size - 1 + push!(partition, variables[start_idx:end_idx]) + start_idx = end_idx + 1 + end + + return PSplit(partition) end end # temp struct to store variable disaggregations (reset for each disjunction) -mutable struct _PSplit{V <: JuMP.AbstractVariableRef, M <: JuMP.AbstractModel} <: AbstractReformulationMethod +mutable struct _PSplit{V <: JuMP.AbstractVariableRef, M <: JuMP.AbstractModel, T} <: AbstractReformulationMethod partition::Vector{Vector{V}} sum_constraints::Dict{LogicalVariableRef{M}, Vector{<:AbstractConstraint}} - hull::_Hull - function _PSplit(method::PSplit{V}, model::M) where {V <: JuMP.AbstractVariableRef, M <: JuMP.AbstractModel} - new{V, M}( + hull::_Hull{V, T} + function _PSplit(method::PSplit{V}, model::M) where + {V <: JuMP.AbstractVariableRef, M <: JuMP.AbstractModel} + T = JuMP.value_type(M) + new{V, M, T}( method.partition, Dict{LogicalVariableRef{M}, Vector{<:AbstractConstraint}}(), _Hull(Hull(), Set{V}()) diff --git a/src/hull.jl b/src/hull.jl index 59dc1ef..98798e6 100644 --- a/src/hull.jl +++ b/src/hull.jl @@ -76,7 +76,6 @@ function _aggregate_variable( expr = Base.zero(JuMP.GenericAffExpr{JuMP.value_type(M), V}) JuMP.add_to_expression!(expr, -1.0, vref) - #TODO: One(T) for dv in method.disjunction_variables[vref] JuMP.add_to_expression!(expr, 1.0, dv) end diff --git a/src/psplit.jl b/src/psplit.jl index 78f2d2a..dea6789 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -25,7 +25,8 @@ function _build_partitioned_expression( if pair.a == var && pair.b == var JuMP.add_to_expression!(new_quadexpr, coeff, var, var) elseif pair.a == var || pair.b == var - error("Quadratic expression contains bilinear term ($(pair.a), $(pair.b))") + error("Quadratic expression contains + bilinear term ($(pair.a), $(pair.b))") end end @@ -171,7 +172,10 @@ end requires_variable_bound_info(method::Union{PSplit, _PSplit}) = true -function set_variable_bound_info(vref::JuMP.AbstractVariableRef, ::Union{PSplit, _PSplit}) +function set_variable_bound_info( + vref::JuMP.AbstractVariableRef, + ::Union{PSplit, _PSplit} + ) if !has_lower_bound(vref) || !has_upper_bound(vref) error("Variable $vref must have both lower and upper bounds defined when using the PSplit reformulation." @@ -187,7 +191,11 @@ end # REFORMULATE DISJUNCT ################################################################################ -function reformulate_disjunction(model::JuMP.AbstractModel, disj::Disjunction, method::PSplit{V}) where {V <: JuMP.AbstractVariableRef} +function reformulate_disjunction( + model::JuMP.AbstractModel, + disj::Disjunction, + method::PSplit{V} +) where {V <: JuMP.AbstractVariableRef} ref_cons = Vector{JuMP.AbstractConstraint}() #store reformulated constraints disj_vrefs = _get_disjunction_variables(model, disj) sum_constraints = Dict{LogicalVariableRef, Vector{<:JuMP.AbstractConstraint}}() @@ -215,7 +223,11 @@ function reformulate_disjunction(model::JuMP.AbstractModel, disj::Disjunction, m return ref_cons end -function reformulate_disjunction(model::JuMP.AbstractModel, disj::Disjunction, method::_PSplit) +function reformulate_disjunction( + model::JuMP.AbstractModel, + disj::Disjunction, + method::_PSplit +) return reformulate_disjunction(model, disj, PSplit(method.partition)) end @@ -235,7 +247,11 @@ function _reformulate_disjunct( return end -function _partition_disjunct(model::M, lvref::LogicalVariableRef, method::PSplit) where {M <: JuMP.AbstractModel} +function _partition_disjunct( + model::M, + lvref::LogicalVariableRef, + method::PSplit +) where {M <: JuMP.AbstractModel} !haskey(_indicator_to_constraints(model), lvref) && return #skip if disjunct is empty partitioned_constraints = Vector{AbstractConstraint}() @@ -244,18 +260,18 @@ function _partition_disjunct(model::M, lvref::LogicalVariableRef, method::PSplit for cref in _indicator_to_constraints(model)[lvref] con = JuMP.constraint_object(cref) if !(con isa Disjunction) - p_constraint, sum_constraint, new_aux_vars = _build_partitioned_constraint(model, con, method) - append!(partitioned_constraints, p_constraint) - append!(sum_constraints, sum_constraint) + part_con, sum_con, new_aux_vars = _build_partitioned_constraint(model, con, method) + append!(partitioned_constraints, part_con) + append!(sum_constraints, sum_con) union!(aux_vars, new_aux_vars) end end return partitioned_constraints, sum_constraints, aux_vars end -# ################################################################################ -# # BUILD PARTITIONED CONSTRAINT -# ################################################################################ +################################################################################# +# BUILD PARTITIONED CONSTRAINT +################################################################################# function _build_partitioned_constraint( model::M, con::JuMP.ScalarConstraint{T, S}, @@ -314,7 +330,10 @@ function _build_partitioned_constraint( part_con_gt = Vector{JuMP.AbstractConstraint}(undef, p) #let [_, 1] be the upper bound and [_, 2] be the lower bound _, constant = _build_partitioned_expression(con.func, method.partition[p]) - v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)") for i in 1:p, j in 1:2] + v = [@variable( + model, + base_name = "v_$(hash(con))_$(i)_$(j)" + ) for i in 1:p, j in 1:2] for i in 1:p func, _= _build_partitioned_expression(con.func, method.partition[i]) part_con_lt[i] = JuMP.build_constraint(error, @@ -342,7 +361,10 @@ function _build_partitioned_constraint( ) where {M <: JuMP.AbstractModel, T, S <: _MOI.Nonpositives, R} p = length(method.partition) d = con.set.dimension - v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)") for i in 1:p, j in 1:d] + v = [@variable( + model, + base_name = "v_$(hash(con))_$(i)_$(j)" + ) for i in 1:p, j in 1:d] part_con = Vector{JuMP.AbstractConstraint}(undef, p) constants = Vector{Number}(undef, d) for i in 1:p @@ -372,11 +394,17 @@ function _build_partitioned_constraint( ) where {M <: JuMP.AbstractModel, T, S <: _MOI.Nonnegatives, R} p = length(method.partition) d = con.set.dimension - v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)") for i in 1:p, j in 1:d] + v = [@variable( + model, + base_name = "v_$(hash(con))_$(i)_$(j)" + ) for i in 1:p, j in 1:d] part_con = Vector{JuMP.AbstractConstraint}(undef, p) constants = Vector{Number}(undef, d) for i in 1:p - part_expr = [_build_partitioned_expression(con.func[j], method.partition[i]) for j in 1:d] + part_expr = [ + _build_partitioned_expression(con.func[j], method.partition[i]) + for j in 1:d + ] func = JuMP.@expression(model, [j = 1:d], -part_expr[j][1]) constants .= [-part_expr[j][2] for j in 1:d] part_con[i] = JuMP.build_constraint(error, func - v[i,:], _MOI.Nonpositives(d)) @@ -400,9 +428,11 @@ function _build_partitioned_constraint( d = con.set.dimension part_con_np = Vector{JuMP.AbstractConstraint}(undef, p) # nonpositive (≤ 0) part_con_nn = Vector{JuMP.AbstractConstraint}(undef, p) # nonnegative (≥ 0) - v = [@variable(model, base_name = "v_$(hash(con))_$(i)_$(j)_$(k)") - for i in 1:p, j in 1:d, k in 1:2 - ] + v = [@variable( + model, + base_name = "v_$(hash(con))_$(i)_$(j)_$(k)" + ) for i in 1:p, j in 1:d, k in 1:2 + ] constants = Vector{Number}(undef, d) for i in 1:p part_expr = [ From 5032348bad7ebfb2a10615ef1aa1cce3d1fc93fc Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Mon, 20 Oct 2025 12:47:48 -0400 Subject: [PATCH 36/39] revert hull aggregate_variables function --- src/hull.jl | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/hull.jl b/src/hull.jl index 98798e6..4cf2a14 100644 --- a/src/hull.jl +++ b/src/hull.jl @@ -65,22 +65,14 @@ end # VARIABLE AGGREGATION ################################################################################ function _aggregate_variable( - model::M, + model::JuMP.AbstractModel, ref_cons::Vector{JuMP.AbstractConstraint}, - vref::V, + vref::JuMP.AbstractVariableRef, method::_Hull - ) where {M <: JuMP.AbstractModel, V <: JuMP.AbstractVariableRef} + ) JuMP.is_binary(vref) && return #skip binary variables - # con_expr = JuMP.@expression(model, -vref + sum(method.disjunction_variables[vref])) - # push!(ref_cons, JuMP.build_constraint(error, con_expr, _MOI.EqualTo(0))) - - expr = Base.zero(JuMP.GenericAffExpr{JuMP.value_type(M), V}) - JuMP.add_to_expression!(expr, -1.0, vref) - for dv in method.disjunction_variables[vref] - JuMP.add_to_expression!(expr, 1.0, dv) - end - - push!(ref_cons, JuMP.build_constraint(error, expr, _MOI.EqualTo(0))) + con_expr = JuMP.@expression(model, -vref + sum(method.disjunction_variables[vref])) + push!(ref_cons, JuMP.build_constraint(error, con_expr, _MOI.EqualTo(0))) return end From ade26c9aa0e6ff3e5452ab565ec8876fa3e31ac1 Mon Sep 17 00:00:00 2001 From: dnguyen227 <82475321+dnguyen227@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:54:22 -0400 Subject: [PATCH 37/39] Correct default_M field description formatting Fix formatting of the default_M field description. --- src/datatypes.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/datatypes.jl b/src/datatypes.jl index 9672683..55b3a7f 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -374,8 +374,7 @@ A type for using the multiple big-M reformulation approach for disjunctive const **Fields** - `optimizer::O`: Optimizer to use when solving mini-models (required). -- `default_M::T`: Default big-M value to use if no big-M is -specified for a logical variable (1e9). +- `default_M::T`: Default big-M value to use if no big-M isspecified for a logical variable (1e9). """ mutable struct MBM{O, T} <: AbstractReformulationMethod optimizer::O @@ -606,4 +605,4 @@ function VariableProperties(vref::JuMP.GenericVariableRef{T}) where T name = JuMP.name(vref) set = JuMP.is_variable_in_set(vref) ? JuMP.moi_set(JuMP.constraint_object(JuMP.VariableInSetRef(vref))) : nothing return VariableProperties(info, name, set, nothing) -end \ No newline at end of file +end From a0fb70c91cc760cac0ac8f928192ddbe6233af04 Mon Sep 17 00:00:00 2001 From: dnguyen227 <82475321+dnguyen227@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:55:17 -0400 Subject: [PATCH 38/39] Fix typo in documentation for MBM struct --- src/datatypes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/datatypes.jl b/src/datatypes.jl index 55b3a7f..d46f4e0 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -374,7 +374,7 @@ A type for using the multiple big-M reformulation approach for disjunctive const **Fields** - `optimizer::O`: Optimizer to use when solving mini-models (required). -- `default_M::T`: Default big-M value to use if no big-M isspecified for a logical variable (1e9). +- `default_M::T`: Default big-M value to use if no big-M is specified for a logical variable (1e9). """ mutable struct MBM{O, T} <: AbstractReformulationMethod optimizer::O From 354d1c120d3fa85151c46e1384a2abb38b5c1ba5 Mon Sep 17 00:00:00 2001 From: dnguyen227 <82475321+dnguyen227@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:56:16 -0400 Subject: [PATCH 39/39] Fix requires_disaggregation function definition --- src/hull.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/hull.jl b/src/hull.jl index 4cf2a14..8ad5209 100644 --- a/src/hull.jl +++ b/src/hull.jl @@ -2,7 +2,6 @@ # VARIABLE DISAGGREGATION ################################################################################ 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`.")