From e8fb40f4afa1492e8bcf1ea859368b842a01dac8 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Thu, 19 Jun 2025 15:14:28 -0400 Subject: [PATCH 01/46] WIP Multi-Big M --- src/DisjunctiveProgramming.jl | 2 +- src/datatypes.jl | 25 +++ src/mbm copy.jl | 99 +++++++++++ src/mbm.jl | 310 ++++++++++++++++++++++++++++++++++ test/constraints/mbm.jl | 119 +++++++++++++ test/runtests.jl | 31 ++-- 6 files changed, 570 insertions(+), 16 deletions(-) create mode 100644 src/mbm copy.jl create mode 100644 src/mbm.jl create mode 100644 test/constraints/mbm.jl diff --git a/src/DisjunctiveProgramming.jl b/src/DisjunctiveProgramming.jl index ae9892e..d646f17 100644 --- a/src/DisjunctiveProgramming.jl +++ b/src/DisjunctiveProgramming.jl @@ -6,7 +6,6 @@ Reexport.@reexport using JuMP # Use Meta for metaprogramming using Base.Meta - # Create aliases import JuMP.MOI as _MOI import JuMP.MOIU.CleverDicts as _MOIUC @@ -22,6 +21,7 @@ include("macros.jl") include("reformulate.jl") include("bigm.jl") include("hull.jl") +include("mbm.jl") include("indicator.jl") include("print.jl") diff --git a/src/datatypes.jl b/src/datatypes.jl index ffc9bd4..b917801 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -367,6 +367,31 @@ struct BigM{T} <: AbstractReformulationMethod end end +""" + MBM{T} <: AbstractReformulationMethod + +A type for using the multiple big-M reformulation approach for disjunctive constraints. + +**Fields** +- `value::T`: MBM value (default = `1e9`). +""" +struct MBM{T,O} <: AbstractReformulationMethod + value::T + optimizer::O + + # Constructor with optimizer (required) and val (optional) + function MBM(optimizer::O, val::T = 1e9) where {T,O} + new{T,O}(val, optimizer) + end + + # Constructor for just optimizer (no val parameter) + function MBM(optimizer::O) where {O} + new{Float64,O}(1e9, optimizer) + end +end + + + """ Hull{T} <: AbstractReformulationMethod diff --git a/src/mbm copy.jl b/src/mbm copy.jl new file mode 100644 index 0000000..6a267d6 --- /dev/null +++ b/src/mbm copy.jl @@ -0,0 +1,99 @@ +################################################################################ +# MULTIPLE BIG-M VALUES +################################################################################ +function _M_subproblems( + model::JuMP.AbstractModel, + obj_ref::Vector{ConstraintRef}, + constraint_ref::Vector{ConstraintRef}, + method::MBM +) + M = zeros(length(obj_ref)) + subproblem = Model() + var_map = Dict{VariableRef,VariableRef}() + for v in all_variables(model) + var_map[v] = @variable(subproblem, base_name = name(v)) + end + j = 0 + + for oref in new_refs + j +=1 + obj = constraitn_object(oref) + lhs = obj.func # Original LHS expression (e.g., x) + + if obj.set isa MOI.LessThan + rhs = obj.set.upper # For ≤ constraints + elseif obj.set isa MOI.GreaterThan + rhs = obj.set.lower # For ≥ constraints + elseif obj.set isa MOI.EqualTo + rhs = obj.set.value # For == constraints + end + + + println("LHS: $lhs, RHS: $rhs") + + @objective(subproblem, Max, lhs - rhs) + constraint_refs = _indicator_to_constraints(model)[constraint_ref] + constraint_objects = [constraint_object(cref) for cref in constraint_refs] + for cref in constraint_objects + substituted_func = substitute(cref.func, var_map) + @constraint(subproblem, substituted_func ∈ cref.set) + end + optimize!(subproblem) + + if JuMP.termination_status(subproblem) == MOI.OPTIMAL && JuMP.has_values(subproblem) && JuMP.primal_status(subproblem) == MOI.FEASIBLE_POINT + if objective_value(subproblem) >= 0 + M[j] = objective_value(subproblem) + else + M[j] = 0 # If the objective is negative, set M to 0 + end + end + end + return M +end + +function _reformulate_disjunct( + model::JuMP.AbstractModel, + ref_cons::Vector{JuMP.AbstractConstraint}, + lvref::Vector{}, + otherlvref::Vector{}, + method::MBM + ) + #reformulate each constraint and add to the model + bvref = binary_variable(lvref) + otherbvref = [binary_variable(i) for i in otherlvref] #binary variable in MINLP + !haskey(_indicator_to_constraints(model), lvref) && return #skip if disjunct is empty + # Reformulate each constraint in the disjunct + for cref in _indicator_to_constraints(model)[lvref] + con = JuMP.constraint_object(cref) + append!(ref_cons, reformulate_disjunct_constraint(model, con, lvref, otherbvref,method)) + end + return +end + +function reformulate_disjunction(model::JuMP.AbstractModel, disj::Disjunction, method::MBM) + ref_cons = Vector{JuMP.AbstractConstraint}() #store reformulated constraints + for d in disj.indicators #d indicator for the GDP (This will be the objec value of the subproblem) + c = [x for x in disj.indicators if x != d] + println(typeof(c)) + _reformulate_disjunct(model, ref_cons, d, c, method) + end + return ref_cons +end + +# Extend reformulate_disjunct_constraint +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.ScalarConstraint{T, S}, + bvref::Vector{}, + otherbvref::Vector{}, + method::MBM +) where {T, S <: _MOI.LessThan} + M = _M_subproblems(model, bvref, otherbvref, method) + new_func = JuMP.@expression(model, con.func - sum(M[i] * otherbvref[i] for i in 1:length(M))) + reform_con = JuMP.build_constraint(error, new_func, con.set) + return [reform_con] +end + +function substitute_variables(expr::AbstractJuMPScalar, var_map) + return substitute(expr, var_map) +end \ No newline at end of file diff --git a/src/mbm.jl b/src/mbm.jl new file mode 100644 index 0000000..1f81c4a --- /dev/null +++ b/src/mbm.jl @@ -0,0 +1,310 @@ +################################################################################ +# MULTIPLE BIG-M VALUE +################################################################################ + +function reformulate_disjunction(model::JuMP.AbstractModel, + disj::Disjunction, + method::MBM + ) + ref_cons = Vector{JuMP.AbstractConstraint}() + for d in disj.indicators + println(typeof(d)) + _reformulate_disjunct(model, ref_cons, d, LogicalVariableRef[x for x in disj.indicators if x != d], method) + end + return ref_cons +end + +function _reformulate_disjunct( + model::JuMP.AbstractModel, + ref_cons::Vector{JuMP.AbstractConstraint}, + lvref::LogicalVariableRef, + conlvref::Vector{LogicalVariableRef}, # was Vector{LogicalVariableRef}, + method::AbstractReformulationMethod + ) + bconref = VariableRef[binary_variable(i) for i in conlvref] #binary variable in MINLP + !haskey(_indicator_to_constraints(model), lvref) && return #skip if disjunct is empty + + M = Vector{Float64}(undef, length(conlvref)) + for (i,d) in enumerate(conlvref) #for each indicator in the list of indicators + M[i] = maximum(_maximize_M(model, constraint_object(cref), + Vector{DisjunctConstraintRef}(_indicator_to_constraints(model)[d]), method) + for cref in _indicator_to_constraints(model)[lvref]) + end + println("M values: ", M) + #Goes over each constraint in the disjunct and reformulates it + for cref in _indicato r_to_constraints(model)[lvref] + con = JuMP.constraint_object(cref) + append!(ref_cons, reformulate_disjunct_constraint(model, con, bconref, M, method)) + end + return ref_cons +end + +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.VectorConstraint{T, S, R}, + bconref:: Vector{VariableRef}, + M::Vector{Float64}, + method::MBM +) where {T, S <: _MOI.Nonpositives, R} + # Calculate the sum of all M[i] * bconref[i] terms + m_sum = sum(M[i] * bconref[i] for i in 1:length(M)) + + # Apply this sum to each row of the vector constraint + new_func = JuMP.@expression(model, [i=1:con.set.dimension], + con.func[i] - m_sum + ) + reform_con = JuMP.build_constraint(error, new_func, con.set) + return [reform_con] +end + +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.VectorConstraint{T, S, R}, + bconref:: Vector{VariableRef}, + M::Vector{Float64}, + method::MBM +) where {T, S <: _MOI.Nonnegatives, R} + # Calculate the sum of all M[i] * bconref[i] terms + m_sum = sum(M[i] * bconref[i] for i in 1:length(M)) + + # Apply this sum to each row of the vector constraint + new_func = JuMP.@expression(model, [i=1:con.set.dimension], + con.func[i] + m_sum + ) + reform_con = JuMP.build_constraint(error, new_func, con.set) + return [reform_con] +end + +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.VectorConstraint{T, S, R}, + bconref:: Vector{VariableRef}, + M::Vector{Float64}, + method::MBM +) where {T, S <: _MOI.Zeros, R} + # Calculate the sum of all M[i] * bconref[i] terms + m_sum = sum(M[i] * bconref[i] for i in 1:length(M)) + + # Apply this sum to each row of the vector constraint + upper_expr = JuMP.@expression(model, [i=1:con.set.dimension], + con.func[i] + m_sum + ) + lower_expr = JuMP.@expression(model, [i=1:con.set.dimension], + con.func[i] - m_sum + ) + upper_con = JuMP.build_constraint(error, upper_expr, MOI.Nonnegatives(con.set.dimension)) + lower_con = JuMP.build_constraint(error, lower_expr, MOI.Nonpositives(con.set.dimension)) + return [upper_con, lower_con] +end + + +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.ScalarConstraint{T, S}, + bconref:: Vector{VariableRef}, + M::Vector{Float64}, + method::MBM +) where {T, S <: _MOI.LessThan} + new_func = JuMP.@expression(model, con.func - sum(M[i] * bconref[i] for i in 1:length(M))) + reform_con = JuMP.build_constraint(error, new_func, con.set) + return [reform_con] +end + +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.ScalarConstraint{T, S}, + bconref:: Vector{VariableRef}, + M::Vector{Float64}, + method::MBM +) where {T, S <: _MOI.GreaterThan} + new_func = JuMP.@expression(model, con.func + sum(M[i] * bconref[i] for i in 1:length(M))) + reform_con = JuMP.build_constraint(error, new_func, con.set) + return [reform_con] +end + +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.ScalarConstraint{T, S}, + bconref::Vector{VariableRef}, + M::Vector{Float64}, + method::MBM +) where {T, S <: _MOI.Interval} + lower_func = JuMP.@expression(model, con.func - sum(M[i] * bconref[i] for i in 1:length(M))) + upper_func = JuMP.@expression(model, con.func + sum(M[i] * bconref[i] for i in 1:length(M))) + + lower_con = JuMP.build_constraint(error, lower_func, MOI.GreaterThan(con.set.lower)) + upper_con = JuMP.build_constraint(error, upper_func, MOI.LessThan(con.set.upper)) + return [lower_con, upper_con] +end + +function reformulate_disjunct_constraint( + model::JuMP.AbstractModel, + con::JuMP.ScalarConstraint{T, S}, + bconref::Vector{VariableRef}, + M::Vector{Float64}, + method::MBM +) where {T, S <: _MOI.EqualTo} + upper_func = JuMP.@expression(model, con.func - sum(M[i] * bconref[i] for i in 1:length(M))) + lower_func = JuMP.@expression(model, con.func + sum(M[i] * bconref[i] for i in 1:length(M))) + + upper_con = JuMP.build_constraint(error, upper_func, MOI.LessThan(con.set.value)) + lower_con = JuMP.build_constraint(error, lower_func, MOI.GreaterThan(con.set.value)) + return [lower_con, upper_con] +end + +function _maximize_M( + model::JuMP.AbstractModel, + objective::VectorConstraint{T, S, R}, ## Constraint to be turned into Objective Reference + constraints::Vector{DisjunctConstraintRef}, ##Partiular Disjunct Constraints + method::MBM +) where {T, S <: _MOI.Nonpositives, R} + return maximum(_maximize_M(model, ScalarConstraint(objective.func[i], MOI.LessThan(0.0)), constraints, method) for i in 1:objective.set.dimension) +end + +function _maximize_M( + model::JuMP.AbstractModel, + objective::VectorConstraint{T, S, R}, ## Constraint to be turned into Objective Reference + constraints::Vector{DisjunctConstraintRef}, ##Partiular Disjunct Constraints + method::MBM +) where {T, S <: _MOI.Nonnegatives, R} + return maximum(_maximize_M(model, ScalarConstraint(objective.func[i], MOI.GreaterThan(0.0)), constraints, method) for i in 1:objective.set.dimension) +end + +function _maximize_M( + model::JuMP.AbstractModel, + objective::VectorConstraint{T, S, R}, ## Constraint to be turned into Objective Reference + constraints::Vector{DisjunctConstraintRef}, ##Partiular Disjunct Constraints + method::MBM +) where {T, S <: _MOI.Zeros, R} + return max( + maximum(_maximize_M(model, ScalarConstraint(objective.func[i], MOI.GreaterThan(0.0)), constraints, method) for i in 1:objective.set.dimension), + maximum(_maximize_M(model, ScalarConstraint(objective.func[i], MOI.LessThan(0.0)), constraints, method) for i in 1:objective.set.dimension) + ) +end + +function _maximize_M( + model::JuMP.AbstractModel, + objective::ScalarConstraint{T, S}, ## Constraint to be turned into Objective Reference + constraints::Vector{DisjunctConstraintRef}, ##Partiular Disjunct Constraints + method::MBM +) where {T, S <: Union{_MOI.LessThan, _MOI.GreaterThan}} + return _mini_model(model, objective, constraints, method) +end + +function _maximize_M( + model::JuMP.AbstractModel, + objective::ScalarConstraint{T, S}, ## Constraint to be turned into Objective Reference + constraints::Vector{DisjunctConstraintRef}, ##Partiular Disjunct Constraints + method::MBM +) where {T, S <: _MOI.Interval} + return max(_mini_model(model, ScalarConstraint(objective.func, MOI.GreaterThan(objective.set.lower)), constraints, method), + _mini_model(model, ScalarConstraint(objective.func, MOI.LessThan(objective.set.upper)), constraints, method)) +end + +function _maximize_M( + model::JuMP.AbstractModel, + objective::ScalarConstraint{T, S}, ## Constraint to be turned into Objective Reference + constraints::Vector{DisjunctConstraintRef}, ##Partiular Disjunct Constraints + method::MBM +) where {T, S <: _MOI.EqualTo} + return max( + _mini_model(model, ScalarConstraint(objective.func, MOI.GreaterThan(objective.set.value)), constraints, method), + _mini_model(model, ScalarConstraint(objective.func, MOI.LessThan(objective.set.value)), constraints, method) + ) +end + +function _mini_model( + model::JuMP.AbstractModel, + objective::ScalarConstraint, ##Pending Objective Reference + constraints::Vector{DisjunctConstraintRef}, ##Partiular Disjunct Constraints + method::MBM +) + sub_model = Model() + new_vars = Dict{VariableRef, VariableRef}() + for var in all_variables(model) + new_vars[var] = @variable(sub_model, base_name= "sub_model_$(JuMP.name(var))") + if is_binary(var) + JuMP.set_binary(new_vars[var]) + end + if is_fixed(var) + JuMP.fix(new_vars[var], fix_value(var); force=true) + end + if is_integer(var) + JuMP.set_integer(new_vars[var]) + end + if has_upper_bound(var) + set_upper_bound(new_vars[var], upper_bound(var)) + end + if has_lower_bound(var) + set_lower_bound(new_vars[var], lower_bound(var)) + end + if has_start_value(var) + set_start(new_vars[var], start(var)) + end + end + for con in [constraint_object(con) for con in constraints] + expr = replace_variables_in_constraint(con.func, new_vars) + @constraint(sub_model, expr in con.set) + end + constraint_to_objective(sub_model, objective, new_vars) + set_optimizer(sub_model, method.optimizer) + set_silent(sub_model) + optimize!(sub_model) + if JuMP.termination_status(sub_model) != MOI.OPTIMAL || !JuMP.has_values(sub_model) || JuMP.primal_status(sub_model) != MOI.FEASIBLE_POINT + println("Termination status: ", JuMP.termination_status(sub_model)) + M = 200 + println("Warning: Optimization did not find a feasible solution. Using default M value of 200.") + else + M = objective_value(sub_model) # Get the value of M from the optimization + end + return M +end + +function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::ScalarConstraint{<:AbstractJuMPScalar, MOI.LessThan{Float64}}, new_vars::Dict{VariableRef, VariableRef}) + @objective(sub_model, Max, - obj.set.upper + replace_variables_in_constraint(obj.func, new_vars)) +end +function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::ScalarConstraint{<:AbstractJuMPScalar, MOI.GreaterThan{Float64}}, new_vars::Dict{VariableRef, VariableRef}) + @objective(sub_model, Max, - replace_variables_in_constraint(obj.func, new_vars) + obj.set.lower) +end + +function replace_variables_in_constraint(fun::GenericVariableRef, var_map::Dict{VariableRef, VariableRef}) + return var_map[fun] +end + +function replace_variables_in_constraint(fun::AffExpr, var_map::Dict{VariableRef, VariableRef}) + new_aff = zero(AffExpr) # Create a new empty AffExpr + for (var, coef) in fun.terms + new_var = var_map[var] + add_to_expression!(new_aff, coef, new_var) + end + new_aff.constant = fun.constant + return new_aff +end + +function replace_variables_in_constraint(fun::QuadExpr, var_map::Dict{VariableRef, VariableRef}) + new_quad = zero(QuadExpr) # Create a new empty QuadExpr + for (vars, coef) in fun.terms + # Extract the two variables from the UnorderedPair + add_to_expression!(new_quad, coef, var_map[vars.a], var_map[vars.b]) + end + new_aff = replace_variables_in_constraint(fun.aff, var_map) + add_to_expression!(new_quad, new_aff) + return new_quad +end + +function replace_variables_in_constraint(fun::Number, var_map::Dict{VariableRef, VariableRef}) + return fun +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 replace_variables_in_constraint(fun::Vector{T}, var_map::Dict{VariableRef, VariableRef}) where T + return [replace_variables_in_constraint(expr, var_map) for expr in fun] +end + + +#make for variable ref \ No newline at end of file diff --git a/test/constraints/mbm.jl b/test/constraints/mbm.jl new file mode 100644 index 0000000..90fc022 --- /dev/null +++ b/test/constraints/mbm.jl @@ -0,0 +1,119 @@ +#Testing replacement of variables +using Gurobi +function test_replace_variables_in_constraint() + model = Model() + sub_model = Model() + @variable(model, x[1:3]) + @constraint(model, con1,x[1] <= 1) + @constraint(model, con2,x[2]*x[1] <= 1) + @constraint(model, con3,sin(x[3]) <= 0) + @constraint(model, con4, [x[1],x[2],x[3]] in MOI.Zeros(3)) + + #Test GenericVariableRef + new_vars = Dict{VariableRef, VariableRef}() + [new_vars[x[i]] = @variable(sub_model) for i in 1:3] + varref = DP.replace_variables_in_constraint(x[1], new_vars) + expr1 = DP.replace_variables_in_constraint(constraint_object(con1).func, new_vars) + expr2 = DP.replace_variables_in_constraint(constraint_object(con2).func, new_vars) + expr3 = DP.replace_variables_in_constraint(constraint_object(con3).func, new_vars) + expr4 = DP.replace_variables_in_constraint(constraint_object(con4).func, new_vars) + @test expr1 == JuMP.@expression(sub_model, new_vars[x[1]] + 1 - 1) + @test expr2 == JuMP.@expression(sub_model, new_vars[x[2]]*new_vars[x[1]]) + @test varref == new_vars[x[1]] + # Test nonlinear expression structure + #TODO: Fix expr3 test, for some reason it's not as easy to compare + @test expr3 isa JuMP.NonlinearExpr # Test it's a nonlinear expression + @test expr4 == [new_vars[x[i]] for i in 1:3] +end + +function test_constraint_to_objective() + model = Model() + sub_model = Model() + + @variable(model, x[1:2]) + @constraint(model, lessthan, x[1] <= 1) + @constraint(model, greaterthan, x[2] >= 1) + @constraint(model, interval, 0 <= x[1] <= 55) + new_vars = Dict{VariableRef, VariableRef}() + [new_vars[x[i]] = @variable(sub_model) for i in 1:2] + + DP.constraint_to_objective(sub_model, constraint_object(lessthan), new_vars) + @test objective_function(sub_model) == JuMP.@expression(sub_model, new_vars[x[1]] - 1) + DP.constraint_to_objective(sub_model, constraint_object(greaterthan), new_vars) + @test objective_function(sub_model) == JuMP.@expression(sub_model, 1 - new_vars[x[2]]) +end + +function test_maximize_M() + model = GDPModel() + @variable(model, 0 <= x[1:2] <= 50) + @variable(model, Y[1:6], Logical) + @constraint(model, lessthan, x[1] <= 1, Disjunct(Y[1])) + @constraint(model, greaterthan, x[1] >= 1, Disjunct(Y[1])) + @constraint(model, interval, 0 <= x[1] <= 55, Disjunct(Y[2])) + @constraint(model, equalto, x[1] == 1, Disjunct(Y[3])) + @constraint(model, nonpositives, -x in MOI.Nonpositives(2), Disjunct(Y[4])) + @constraint(model, nonnegatives, x in MOI.Nonnegatives(2), Disjunct(Y[5])) + @constraint(model, zeros, -x .+ 1 in MOI.Zeros(2), Disjunct(Y[6])) + @test DP._maximize_M(model, constraint_object(lessthan), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(Gurobi.Optimizer)) == 49 + @test DP._maximize_M(model, constraint_object(greaterthan), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(Gurobi.Optimizer)) == 1.0 + @test DP._maximize_M(model, constraint_object(interval), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[1]]), MBM(Gurobi.Optimizer)) == -1.0 + @test DP._maximize_M(model, constraint_object(equalto), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[3]]), MBM(Gurobi.Optimizer)) == 0 + @test DP._maximize_M(model, constraint_object(nonpositives), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(Gurobi.Optimizer)) == 0 + @test DP._maximize_M(model, constraint_object(nonnegatives), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(Gurobi.Optimizer)) == 0 + @test DP._maximize_M(model, constraint_object(zeros), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(Gurobi.Optimizer)) == 49 +end + +function test_reformulate_disjunct_constraint() + model = GDPModel() + @variable(model, 0 <= x[1:2] <= 50) + @variable(model, Y[1:6], Logical) + @constraint(model, lessthan, x[1] <= 1, Disjunct(Y[1])) + @constraint(model, greaterthan, x[1] >= 1, Disjunct(Y[1])) + @constraint(model, interval, 0 <= x[1] <= 55, Disjunct(Y[2])) + @constraint(model, equalto, x[1] == 1, Disjunct(Y[3])) + @constraint(model, nonpositives, -x in MOI.Nonpositives(2), Disjunct(Y[4])) + @constraint(model, nonnegatives, x in MOI.Nonnegatives(2), Disjunct(Y[5])) + @constraint(model, zeros, -x .+ 1 in MOI.Zeros(2), Disjunct(Y[6])) + + M = Float64[1,2,3,4,5,6] + bconref = [binary_variable(Y[i]) for i in 1:6] + reformulated_constraints = [reformulate_disjunct_constraint(model, constraint_object(constraints), bconref, M, MBM(Gurobi.Optimizer)) for constraints in [lessthan, greaterthan, interval, equalto, nonpositives, nonnegatives, zeros]] + @test reformulated_constraints[1][1].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_constraints[1][1].set == MOI.LessThan(1.0) + @test reformulated_constraints[2][1].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_constraints[2][1].set == MOI.GreaterThan(1.0) + @test reformulated_constraints[3][2].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_constraints[3][1].set == MOI.GreaterThan(0.0) + @test reformulated_constraints[3][1].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_constraints[3][2].set == MOI.LessThan(55.0) + @test reformulated_constraints[4][1].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_constraints[4][1].set == MOI.GreaterThan(1.0) + @test reformulated_constraints[4][2].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_constraints[4][2].set == MOI.LessThan(1.0) + @test reformulated_constraints[5][1].func == JuMP.@expression(model, -x .- sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_constraints[5][1].set == MOI.Nonpositives(2) + @test reformulated_constraints[6][1].func == JuMP.@expression(model, x .+ sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_constraints[6][1].set == MOI.Nonnegatives(2) + @test reformulated_constraints[7][1].func == JuMP.@expression(model, -x .+(1 + sum(M[i] * bconref[i] for i in 1:length(M)))) && reformulated_constraints[7][1].set == MOI.Nonnegatives(2) + @test reformulated_constraints[7][2].func == JuMP.@expression(model, -x .+(1 - sum(M[i] * bconref[i] for i in 1:length(M)))) && reformulated_constraints[7][2].set == MOI.Nonpositives(2) +end + +function test_reformulate_disjunct() + model = GDPModel() + @variable(model, 0 <= x[1:2] <= 50) + @variable(model, Y[1:6], Logical) + @constraint(model, lessthan, x[1] <= 1, Disjunct(Y[1])) + @constraint(model, greaterthan, x[1] >= 1, Disjunct(Y[1])) + @constraint(model, interval, 0 <= x[1] <= 55, Disjunct(Y[2])) + @constraint(model, equalto, x[1] == 1, Disjunct(Y[3])) + @constraint(model, nonpositives, -x in MOI.Nonpositives(2), Disjunct(Y[4])) + @constraint(model, nonnegatives, x in MOI.Nonnegatives(2), Disjunct(Y[5])) + @constraint(model, zeros, -x .+ 1 in MOI.Zeros(2), Disjunct(Y[6])) + @disjunction(model, [Y[1], Y[2], Y[3], Y[4], Y[5], Y[6]]) + + reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[1], LogicalVariableRef[Y[2], Y[3], Y[4], Y[5], Y[6]], MBM(Gurobi.Optimizer)) + println(reformulated_disjunct) + +end + +@testset "MBM" begin + # test_replace_variables_in_constraint() + # test_constraint_to_objective() + # test_maximize_M() + # test_reformulate_disjunct_constraint() + test_reformulate_disjunct() +end + + diff --git a/test/runtests.jl b/test/runtests.jl index ba5c511..0d1e13f 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/hull.jl") +# include("constraints/fallback.jl") +# include("constraints/disjunction.jl") +# include("print.jl") +# include("solve.jl") +include("constraints/mbm.jl") \ No newline at end of file From 0f7ad326604f0d660e1247a44dadc3491f8d97bf Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Thu, 19 Jun 2025 16:20:21 -0400 Subject: [PATCH 02/46] Deleting mbm copy.jl as it's not needed anymore --- src/mbm copy.jl | 99 ------------------------------------------------- 1 file changed, 99 deletions(-) delete mode 100644 src/mbm copy.jl diff --git a/src/mbm copy.jl b/src/mbm copy.jl deleted file mode 100644 index 6a267d6..0000000 --- a/src/mbm copy.jl +++ /dev/null @@ -1,99 +0,0 @@ -################################################################################ -# MULTIPLE BIG-M VALUES -################################################################################ -function _M_subproblems( - model::JuMP.AbstractModel, - obj_ref::Vector{ConstraintRef}, - constraint_ref::Vector{ConstraintRef}, - method::MBM -) - M = zeros(length(obj_ref)) - subproblem = Model() - var_map = Dict{VariableRef,VariableRef}() - for v in all_variables(model) - var_map[v] = @variable(subproblem, base_name = name(v)) - end - j = 0 - - for oref in new_refs - j +=1 - obj = constraitn_object(oref) - lhs = obj.func # Original LHS expression (e.g., x) - - if obj.set isa MOI.LessThan - rhs = obj.set.upper # For ≤ constraints - elseif obj.set isa MOI.GreaterThan - rhs = obj.set.lower # For ≥ constraints - elseif obj.set isa MOI.EqualTo - rhs = obj.set.value # For == constraints - end - - - println("LHS: $lhs, RHS: $rhs") - - @objective(subproblem, Max, lhs - rhs) - constraint_refs = _indicator_to_constraints(model)[constraint_ref] - constraint_objects = [constraint_object(cref) for cref in constraint_refs] - for cref in constraint_objects - substituted_func = substitute(cref.func, var_map) - @constraint(subproblem, substituted_func ∈ cref.set) - end - optimize!(subproblem) - - if JuMP.termination_status(subproblem) == MOI.OPTIMAL && JuMP.has_values(subproblem) && JuMP.primal_status(subproblem) == MOI.FEASIBLE_POINT - if objective_value(subproblem) >= 0 - M[j] = objective_value(subproblem) - else - M[j] = 0 # If the objective is negative, set M to 0 - end - end - end - return M -end - -function _reformulate_disjunct( - model::JuMP.AbstractModel, - ref_cons::Vector{JuMP.AbstractConstraint}, - lvref::Vector{}, - otherlvref::Vector{}, - method::MBM - ) - #reformulate each constraint and add to the model - bvref = binary_variable(lvref) - otherbvref = [binary_variable(i) for i in otherlvref] #binary variable in MINLP - !haskey(_indicator_to_constraints(model), lvref) && return #skip if disjunct is empty - # Reformulate each constraint in the disjunct - for cref in _indicator_to_constraints(model)[lvref] - con = JuMP.constraint_object(cref) - append!(ref_cons, reformulate_disjunct_constraint(model, con, lvref, otherbvref,method)) - end - return -end - -function reformulate_disjunction(model::JuMP.AbstractModel, disj::Disjunction, method::MBM) - ref_cons = Vector{JuMP.AbstractConstraint}() #store reformulated constraints - for d in disj.indicators #d indicator for the GDP (This will be the objec value of the subproblem) - c = [x for x in disj.indicators if x != d] - println(typeof(c)) - _reformulate_disjunct(model, ref_cons, d, c, method) - end - return ref_cons -end - -# Extend reformulate_disjunct_constraint -function reformulate_disjunct_constraint( - model::JuMP.AbstractModel, - con::JuMP.ScalarConstraint{T, S}, - bvref::Vector{}, - otherbvref::Vector{}, - method::MBM -) where {T, S <: _MOI.LessThan} - M = _M_subproblems(model, bvref, otherbvref, method) - new_func = JuMP.@expression(model, con.func - sum(M[i] * otherbvref[i] for i in 1:length(M))) - reform_con = JuMP.build_constraint(error, new_func, con.set) - return [reform_con] -end - -function substitute_variables(expr::AbstractJuMPScalar, var_map) - return substitute(expr, var_map) -end \ No newline at end of file From c2e07a9c0ba7b912423d33105b7e22af9707f084 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Thu, 19 Jun 2025 18:32:03 -0400 Subject: [PATCH 03/46] Added code coverage. --- src/codecoverage.jl | 43 ++++++++++++++++++++++ src/mbm.jl | 81 ++++++++++++++++++++--------------------- test/constraints/mbm.jl | 12 +++--- test/runtests.jl | 3 +- 4 files changed, 91 insertions(+), 48 deletions(-) create mode 100644 src/codecoverage.jl diff --git a/src/codecoverage.jl b/src/codecoverage.jl new file mode 100644 index 0000000..94eb28d --- /dev/null +++ b/src/codecoverage.jl @@ -0,0 +1,43 @@ +using Coverage + +function run_coverage_analysis() + # Get the directory of this file + pkg_dir = dirname(@__DIR__) + + # Change to package directory + cd(pkg_dir) do + # Clean up previous coverage files + clean_folder(".") + + # Run tests with coverage + println("Running tests with coverage...") + run(`$(Base.julia_cmd()) --code-coverage=user test/runtests.jl`) + + # Process coverage data + println("\nAnalyzing coverage...") + coverage = process_folder("src") # Only process src/ directory + + # Generate and display coverage report + covered, total = get_summary(coverage) + coverage_pct = round((covered / total) * 100, digits=2) + + println("\n=== Coverage Summary ===") + println("Covered lines: $covered / $total") + println("Coverage: $coverage_pct%") + + # Generate detailed report + # println("\n=== File-by-File Coverage ===") + # for file in coverage + # file_cov, file_total = get_summary([file]) + # if file_total > 0 + # pct = round((file_cov / file_total) * 100, digits=1) + # # println("$(rpad(basename(file.filename), 30)) $(lpad(string(pct), 5))%") + # end + # end + + return coverage + end +end + +# Run the analysis +run_coverage_analysis() \ No newline at end of file diff --git a/src/mbm.jl b/src/mbm.jl index 1f81c4a..7793c09 100644 --- a/src/mbm.jl +++ b/src/mbm.jl @@ -1,7 +1,9 @@ +#TODO: Add dead end functions for when something super generic is given. + ################################################################################ -# MULTIPLE BIG-M VALUE +# CONSTRAINT, DISJUNCTION, DISJUNCT REFORMULATION ################################################################################ - +#Reformulates the disjunction using multiple big-M values function reformulate_disjunction(model::JuMP.AbstractModel, disj::Disjunction, method::MBM @@ -13,26 +15,25 @@ function reformulate_disjunction(model::JuMP.AbstractModel, end return ref_cons end - +#Reformualates a disjunct the disjunct of interest +#represented by lvref and the other indicators in conlvref function _reformulate_disjunct( model::JuMP.AbstractModel, ref_cons::Vector{JuMP.AbstractConstraint}, lvref::LogicalVariableRef, - conlvref::Vector{LogicalVariableRef}, # was Vector{LogicalVariableRef}, + conlvref::Vector{LogicalVariableRef}, method::AbstractReformulationMethod ) - bconref = VariableRef[binary_variable(i) for i in conlvref] #binary variable in MINLP - !haskey(_indicator_to_constraints(model), lvref) && return #skip if disjunct is empty - + bconref = VariableRef[binary_variable(i) for i in conlvref] + !haskey(_indicator_to_constraints(model), lvref) && return M = Vector{Float64}(undef, length(conlvref)) - for (i,d) in enumerate(conlvref) #for each indicator in the list of indicators + for (i,d) in enumerate(conlvref) M[i] = maximum(_maximize_M(model, constraint_object(cref), Vector{DisjunctConstraintRef}(_indicator_to_constraints(model)[d]), method) for cref in _indicator_to_constraints(model)[lvref]) end println("M values: ", M) - #Goes over each constraint in the disjunct and reformulates it - for cref in _indicato r_to_constraints(model)[lvref] + for cref in _indicator_to_constraints(model)[lvref] con = JuMP.constraint_object(cref) append!(ref_cons, reformulate_disjunct_constraint(model, con, bconref, M, method)) end @@ -46,10 +47,7 @@ function reformulate_disjunct_constraint( M::Vector{Float64}, method::MBM ) where {T, S <: _MOI.Nonpositives, R} - # Calculate the sum of all M[i] * bconref[i] terms m_sum = sum(M[i] * bconref[i] for i in 1:length(M)) - - # Apply this sum to each row of the vector constraint new_func = JuMP.@expression(model, [i=1:con.set.dimension], con.func[i] - m_sum ) @@ -64,10 +62,7 @@ function reformulate_disjunct_constraint( M::Vector{Float64}, method::MBM ) where {T, S <: _MOI.Nonnegatives, R} - # Calculate the sum of all M[i] * bconref[i] terms m_sum = sum(M[i] * bconref[i] for i in 1:length(M)) - - # Apply this sum to each row of the vector constraint new_func = JuMP.@expression(model, [i=1:con.set.dimension], con.func[i] + m_sum ) @@ -82,10 +77,7 @@ function reformulate_disjunct_constraint( M::Vector{Float64}, method::MBM ) where {T, S <: _MOI.Zeros, R} - # Calculate the sum of all M[i] * bconref[i] terms m_sum = sum(M[i] * bconref[i] for i in 1:length(M)) - - # Apply this sum to each row of the vector constraint upper_expr = JuMP.@expression(model, [i=1:con.set.dimension], con.func[i] + m_sum ) @@ -152,10 +144,13 @@ function reformulate_disjunct_constraint( return [lower_con, upper_con] end +################################################################################ +# MULTIPLE BIG-M REFORMULATION +################################################################################ function _maximize_M( model::JuMP.AbstractModel, - objective::VectorConstraint{T, S, R}, ## Constraint to be turned into Objective Reference - constraints::Vector{DisjunctConstraintRef}, ##Partiular Disjunct Constraints + objective::VectorConstraint{T, S, R}, + constraints::Vector{DisjunctConstraintRef}, method::MBM ) where {T, S <: _MOI.Nonpositives, R} return maximum(_maximize_M(model, ScalarConstraint(objective.func[i], MOI.LessThan(0.0)), constraints, method) for i in 1:objective.set.dimension) @@ -163,8 +158,8 @@ end function _maximize_M( model::JuMP.AbstractModel, - objective::VectorConstraint{T, S, R}, ## Constraint to be turned into Objective Reference - constraints::Vector{DisjunctConstraintRef}, ##Partiular Disjunct Constraints + objective::VectorConstraint{T, S, R}, + constraints::Vector{DisjunctConstraintRef}, method::MBM ) where {T, S <: _MOI.Nonnegatives, R} return maximum(_maximize_M(model, ScalarConstraint(objective.func[i], MOI.GreaterThan(0.0)), constraints, method) for i in 1:objective.set.dimension) @@ -172,8 +167,8 @@ end function _maximize_M( model::JuMP.AbstractModel, - objective::VectorConstraint{T, S, R}, ## Constraint to be turned into Objective Reference - constraints::Vector{DisjunctConstraintRef}, ##Partiular Disjunct Constraints + objective::VectorConstraint{T, S, R}, + constraints::Vector{DisjunctConstraintRef}, method::MBM ) where {T, S <: _MOI.Zeros, R} return max( @@ -184,8 +179,8 @@ end function _maximize_M( model::JuMP.AbstractModel, - objective::ScalarConstraint{T, S}, ## Constraint to be turned into Objective Reference - constraints::Vector{DisjunctConstraintRef}, ##Partiular Disjunct Constraints + objective::ScalarConstraint{T, S}, + constraints::Vector{DisjunctConstraintRef}, method::MBM ) where {T, S <: Union{_MOI.LessThan, _MOI.GreaterThan}} return _mini_model(model, objective, constraints, method) @@ -193,8 +188,8 @@ end function _maximize_M( model::JuMP.AbstractModel, - objective::ScalarConstraint{T, S}, ## Constraint to be turned into Objective Reference - constraints::Vector{DisjunctConstraintRef}, ##Partiular Disjunct Constraints + objective::ScalarConstraint{T, S}, + constraints::Vector{DisjunctConstraintRef}, method::MBM ) where {T, S <: _MOI.Interval} return max(_mini_model(model, ScalarConstraint(objective.func, MOI.GreaterThan(objective.set.lower)), constraints, method), @@ -203,8 +198,8 @@ end function _maximize_M( model::JuMP.AbstractModel, - objective::ScalarConstraint{T, S}, ## Constraint to be turned into Objective Reference - constraints::Vector{DisjunctConstraintRef}, ##Partiular Disjunct Constraints + objective::ScalarConstraint{T, S}, + constraints::Vector{DisjunctConstraintRef}, method::MBM ) where {T, S <: _MOI.EqualTo} return max( @@ -215,8 +210,8 @@ end function _mini_model( model::JuMP.AbstractModel, - objective::ScalarConstraint, ##Pending Objective Reference - constraints::Vector{DisjunctConstraintRef}, ##Partiular Disjunct Constraints + objective::ScalarConstraint, + constraints::Vector{DisjunctConstraintRef}, method::MBM ) sub_model = Model() @@ -255,11 +250,14 @@ function _mini_model( M = 200 println("Warning: Optimization did not find a feasible solution. Using default M value of 200.") else - M = objective_value(sub_model) # Get the value of M from the optimization + M = objective_value(sub_model) end return M end +################################################################################ +# CONSTRAINT TO OBJECTIVE +################################################################################ function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::ScalarConstraint{<:AbstractJuMPScalar, MOI.LessThan{Float64}}, new_vars::Dict{VariableRef, VariableRef}) @objective(sub_model, Max, - obj.set.upper + replace_variables_in_constraint(obj.func, new_vars)) end @@ -267,12 +265,16 @@ function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::ScalarConstr @objective(sub_model, Max, - replace_variables_in_constraint(obj.func, new_vars) + obj.set.lower) end + +################################################################################ +# REPLACE VARIABLES IN CONSTRAINT +################################################################################ function replace_variables_in_constraint(fun::GenericVariableRef, var_map::Dict{VariableRef, VariableRef}) return var_map[fun] end function replace_variables_in_constraint(fun::AffExpr, var_map::Dict{VariableRef, VariableRef}) - new_aff = zero(AffExpr) # Create a new empty AffExpr + new_aff = zero(AffExpr) for (var, coef) in fun.terms new_var = var_map[var] add_to_expression!(new_aff, coef, new_var) @@ -282,9 +284,8 @@ function replace_variables_in_constraint(fun::AffExpr, var_map::Dict{VariableRef end function replace_variables_in_constraint(fun::QuadExpr, var_map::Dict{VariableRef, VariableRef}) - new_quad = zero(QuadExpr) # Create a new empty QuadExpr + new_quad = zero(QuadExpr) for (vars, coef) in fun.terms - # Extract the two variables from the UnorderedPair add_to_expression!(new_quad, coef, var_map[vars.a], var_map[vars.b]) end new_aff = replace_variables_in_constraint(fun.aff, var_map) @@ -296,7 +297,6 @@ function replace_variables_in_constraint(fun::Number, var_map::Dict{VariableRef, return fun 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) @@ -304,7 +304,4 @@ end function replace_variables_in_constraint(fun::Vector{T}, var_map::Dict{VariableRef, VariableRef}) where T return [replace_variables_in_constraint(expr, var_map) for expr in fun] -end - - -#make for variable ref \ No newline at end of file +end \ No newline at end of file diff --git a/test/constraints/mbm.jl b/test/constraints/mbm.jl index 90fc022..dbc0a2c 100644 --- a/test/constraints/mbm.jl +++ b/test/constraints/mbm.jl @@ -1,4 +1,6 @@ #Testing replacement of variables +#TODO: Test bad inputs throw errors +#TODO: Test MBM datatype using Gurobi function test_replace_variables_in_constraint() model = Model() @@ -109,11 +111,11 @@ function test_reformulate_disjunct() end @testset "MBM" begin - # test_replace_variables_in_constraint() - # test_constraint_to_objective() - # test_maximize_M() - # test_reformulate_disjunct_constraint() - test_reformulate_disjunct() + test_replace_variables_in_constraint() + test_constraint_to_objective() + test_maximize_M() + test_reformulate_disjunct_constraint() + # test_reformulate_disjunct() end diff --git a/test/runtests.jl b/test/runtests.jl index 0d1e13f..0f058ff 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -20,4 +20,5 @@ include("utilities.jl") # include("constraints/disjunction.jl") # include("print.jl") # include("solve.jl") -include("constraints/mbm.jl") \ No newline at end of file +# include("codecoverage.jl") +include("constraints/mbm.jl") From 2b25f70b73c61d7ad8edaaf491b42a1d03fba592 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Mon, 23 Jun 2025 14:38:59 -0400 Subject: [PATCH 04/46] More test cases --- src/mbm.jl | 31 +++++++++++++++++++++- test/constraints/mbm.jl | 59 ++++++++++++++++++++++++++++++++--------- 2 files changed, 77 insertions(+), 13 deletions(-) diff --git a/src/mbm.jl b/src/mbm.jl index 7793c09..a323e2e 100644 --- a/src/mbm.jl +++ b/src/mbm.jl @@ -147,6 +147,7 @@ end ################################################################################ # MULTIPLE BIG-M REFORMULATION ################################################################################ + function _maximize_M( model::JuMP.AbstractModel, objective::VectorConstraint{T, S, R}, @@ -208,6 +209,15 @@ function _maximize_M( ) end +function _maximize_M( + ::JuMP.AbstractModel, + ::F, + ::Vector{DisjunctConstraintRef}, + ::MBM +) where {F} + error("This type of constraints and objective constraint has not been implemented for MBM subproblems\nF: $(F)") +end + function _mini_model( model::JuMP.AbstractModel, objective::ScalarConstraint, @@ -239,7 +249,7 @@ function _mini_model( end for con in [constraint_object(con) for con in constraints] expr = replace_variables_in_constraint(con.func, new_vars) - @constraint(sub_model, expr in con.set) + @constraint(sub_model, expr * 1.0 in con.set) end constraint_to_objective(sub_model, objective, new_vars) set_optimizer(sub_model, method.optimizer) @@ -255,6 +265,15 @@ function _mini_model( return M end +# function _mini_model( +# ::JuMP.AbstractModel, +# ::{T,S}, +# ::V, +# ::MBM +# ) where {T, S} +# error("This type of constraints and objective constraint has not been implemented for MBM subproblems") +# end + ################################################################################ # CONSTRAINT TO OBJECTIVE ################################################################################ @@ -265,10 +284,16 @@ function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::ScalarConstr @objective(sub_model, Max, - replace_variables_in_constraint(obj.func, new_vars) + obj.set.lower) end +function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::ScalarConstraint, new_vars::Dict{VariableRef, VariableRef}) + error("This type of constraint is not supported, only greater than and less than constraints are supported with intervals and equalities being converted.") +end ################################################################################ # REPLACE VARIABLES IN CONSTRAINT ################################################################################ + + + function replace_variables_in_constraint(fun::GenericVariableRef, var_map::Dict{VariableRef, VariableRef}) return var_map[fun] end @@ -304,4 +329,8 @@ end function replace_variables_in_constraint(fun::Vector{T}, var_map::Dict{VariableRef, VariableRef}) where T return [replace_variables_in_constraint(expr, var_map) for expr in fun] +end + +function replace_variables_in_constraint(::F, ::S) where {F, S} + error("replace_variables_in_constraint not implemented for $(typeof(F)) and $(typeof(S))") end \ No newline at end of file diff --git a/test/constraints/mbm.jl b/test/constraints/mbm.jl index dbc0a2c..650384a 100644 --- a/test/constraints/mbm.jl +++ b/test/constraints/mbm.jl @@ -1,7 +1,8 @@ #Testing replacement of variables -#TODO: Test bad inputs throw errors -#TODO: Test MBM datatype +#TODO: Test bad inputs throw errors for MINI MODEL +#TODO: Test MBM datatype using Gurobi +using Revise function test_replace_variables_in_constraint() model = Model() sub_model = Model() @@ -26,6 +27,7 @@ function test_replace_variables_in_constraint() #TODO: Fix expr3 test, for some reason it's not as easy to compare @test expr3 isa JuMP.NonlinearExpr # Test it's a nonlinear expression @test expr4 == [new_vars[x[i]] for i in 1:3] + @test_throws ErrorException DP.replace_variables_in_constraint("String", new_vars) end function test_constraint_to_objective() @@ -33,16 +35,17 @@ function test_constraint_to_objective() sub_model = Model() @variable(model, x[1:2]) - @constraint(model, lessthan, x[1] <= 1) + @constraint(model, lessthan, x[1] <= 1) @constraint(model, greaterthan, x[2] >= 1) @constraint(model, interval, 0 <= x[1] <= 55) + @constraint(model, equalto, x[1] == 1) new_vars = Dict{VariableRef, VariableRef}() [new_vars[x[i]] = @variable(sub_model) for i in 1:2] - DP.constraint_to_objective(sub_model, constraint_object(lessthan), new_vars) @test objective_function(sub_model) == JuMP.@expression(sub_model, new_vars[x[1]] - 1) DP.constraint_to_objective(sub_model, constraint_object(greaterthan), new_vars) @test objective_function(sub_model) == JuMP.@expression(sub_model, 1 - new_vars[x[2]]) + @test_throws ErrorException DP.constraint_to_objective(sub_model, constraint_object(interval), new_vars) end function test_maximize_M() @@ -63,6 +66,7 @@ function test_maximize_M() @test DP._maximize_M(model, constraint_object(nonpositives), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(Gurobi.Optimizer)) == 0 @test DP._maximize_M(model, constraint_object(nonnegatives), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(Gurobi.Optimizer)) == 0 @test DP._maximize_M(model, constraint_object(zeros), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(Gurobi.Optimizer)) == 49 + @test_throws ErrorException DP._maximize_M(model, "odd", Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(Gurobi.Optimizer)) end function test_reformulate_disjunct_constraint() @@ -94,9 +98,9 @@ end function test_reformulate_disjunct() model = GDPModel() - @variable(model, 0 <= x[1:2] <= 50) + @variable(model, 1 <= x[1:2] <= 50) @variable(model, Y[1:6], Logical) - @constraint(model, lessthan, x[1] <= 1, Disjunct(Y[1])) + @constraint(model, lessthan, x[1] <= 2, Disjunct(Y[1])) @constraint(model, greaterthan, x[1] >= 1, Disjunct(Y[1])) @constraint(model, interval, 0 <= x[1] <= 55, Disjunct(Y[2])) @constraint(model, equalto, x[1] == 1, Disjunct(Y[3])) @@ -106,16 +110,47 @@ function test_reformulate_disjunct() @disjunction(model, [Y[1], Y[2], Y[3], Y[4], Y[5], Y[6]]) reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[1], LogicalVariableRef[Y[2], Y[3], Y[4], Y[5], Y[6]], MBM(Gurobi.Optimizer)) - println(reformulated_disjunct) + M = [48.0, -0.0, 48.0, 48.0, -0.0] + bconref = [binary_variable(Y[i]) for i in 2:6] + @test reformulated_disjunct[1].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[1].set == MOI.LessThan(2.0) + @test reformulated_disjunct[2].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[2].set == MOI.GreaterThan(1.0) + + reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[2], LogicalVariableRef[Y[1], Y[3], Y[4], Y[5], Y[6]], MBM(Gurobi.Optimizer)) + M = [1.0, 0.0, 1.0, 1.0, 1.0, 1.0] + bconref = [binary_variable(Y[i]) for i in 1:6] + @test reformulated_disjunct[2].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[2].set == MOI.LessThan(55.0) + @test reformulated_disjunct[1].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[1].set == MOI.GreaterThan(0.0) + + reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[3], LogicalVariableRef[Y[1], Y[2], Y[4], Y[5], Y[6]], MBM(Gurobi.Optimizer)) + M = [1.0, 49.0, 0.0, 49.0, 49.0, 0.0] + bconref = [binary_variable(Y[i]) for i in 1:6] + @test reformulated_disjunct[2].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[2].set == MOI.LessThan(1.0) + @test reformulated_disjunct[1].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[1].set == MOI.GreaterThan(1.0) + reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[4], LogicalVariableRef[Y[1], Y[2], Y[3], Y[5], Y[6]], MBM(Gurobi.Optimizer)) + M = [-1.0, -1.0, -1.0, 0.0, -1.0, -1.0] + bconref = [binary_variable(Y[i]) for i in 1:6] + @test reformulated_disjunct[1].func == JuMP.@expression(model, -x .- sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[1].set == MOI.Nonpositives(2) + + reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[5], LogicalVariableRef[Y[1], Y[2], Y[3], Y[4], Y[6]], MBM(Gurobi.Optimizer)) + M = [-1.0, -1.0, -1.0, -1.0, 0.0, -1.0] + bconref = [binary_variable(Y[i]) for i in 1:6] + @test reformulated_disjunct[1].func == JuMP.@expression(model, x .+ sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[1].set == MOI.Nonnegatives(2) + + reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[6], LogicalVariableRef[Y[1], Y[2], Y[3], Y[4], Y[5]], MBM(Gurobi.Optimizer)) + M = [49.0, 49.0, 49.0, 49.0, 49.0, 0.0] + bconref = [binary_variable(Y[i]) for i in 1:6] + println(reformulated_disjunct) + @test reformulated_disjunct[1].func == JuMP.@expression(model, -x .+ sum(M[i] * bconref[i] for i in 1:length(M)) .+ 1) && reformulated_disjunct[1].set == MOI.Nonnegatives(2) + @test reformulated_disjunct[2].func == JuMP.@expression(model, -x .- sum(M[i] * bconref[i] for i in 1:length(M)) .+ 1) && reformulated_disjunct[2].set == MOI.Nonpositives(2) end @testset "MBM" begin - test_replace_variables_in_constraint() - test_constraint_to_objective() - test_maximize_M() - test_reformulate_disjunct_constraint() - # test_reformulate_disjunct() + # test_replace_variables_in_constraint() + # test_constraint_to_objective() + # test_maximize_M() + # test_reformulate_disjunct_constraint() + test_reformulate_disjunct() end From dce303eb305d5487f758c1ff3915fae6da48d7c4 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Mon, 23 Jun 2025 14:59:05 -0400 Subject: [PATCH 05/46] Testing MBM type --- src/datatypes.jl | 5 ----- test/constraints/mbm.jl | 23 +++++++++++++++++++++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/datatypes.jl b/src/datatypes.jl index b917801..46bfab0 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -383,11 +383,6 @@ struct MBM{T,O} <: AbstractReformulationMethod function MBM(optimizer::O, val::T = 1e9) where {T,O} new{T,O}(val, optimizer) end - - # Constructor for just optimizer (no val parameter) - function MBM(optimizer::O) where {O} - new{Float64,O}(1e9, optimizer) - end end diff --git a/test/constraints/mbm.jl b/test/constraints/mbm.jl index 650384a..141ee54 100644 --- a/test/constraints/mbm.jl +++ b/test/constraints/mbm.jl @@ -1,8 +1,14 @@ #Testing replacement of variables #TODO: Test bad inputs throw errors for MINI MODEL -#TODO: Test MBM datatype using Gurobi using Revise + +function test_mbm() + @test MBM(Gurobi.Optimizer).optimizer == Gurobi.Optimizer + @test MBM(Gurobi.Optimizer).value == 1e9 + @test MBM(Gurobi.Optimizer, 100).value == 100 +end + function test_replace_variables_in_constraint() model = Model() sub_model = Model() @@ -28,6 +34,17 @@ function test_replace_variables_in_constraint() @test expr3 isa JuMP.NonlinearExpr # Test it's a nonlinear expression @test expr4 == [new_vars[x[i]] for i in 1:3] @test_throws ErrorException DP.replace_variables_in_constraint("String", new_vars) +end + +function test_mini_model() + + + + + + + + end function test_constraint_to_objective() @@ -145,12 +162,14 @@ function test_reformulate_disjunct() @test reformulated_disjunct[2].func == JuMP.@expression(model, -x .- sum(M[i] * bconref[i] for i in 1:length(M)) .+ 1) && reformulated_disjunct[2].set == MOI.Nonpositives(2) end + @testset "MBM" begin + test_mbm() # test_replace_variables_in_constraint() # test_constraint_to_objective() # test_maximize_M() # test_reformulate_disjunct_constraint() - test_reformulate_disjunct() + # test_reformulate_disjunct() end From 5a1c0b277a8843b8dae9d8822878203bf6b0db85 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Mon, 23 Jun 2025 16:40:25 -0400 Subject: [PATCH 06/46] working ver --- src/mbm.jl | 26 +++++------ test/constraints/mbm.jl | 99 ++++++++++++++++++++++------------------- 2 files changed, 63 insertions(+), 62 deletions(-) diff --git a/src/mbm.jl b/src/mbm.jl index a323e2e..e6afacc 100644 --- a/src/mbm.jl +++ b/src/mbm.jl @@ -32,7 +32,6 @@ function _reformulate_disjunct( Vector{DisjunctConstraintRef}(_indicator_to_constraints(model)[d]), method) for cref in _indicator_to_constraints(model)[lvref]) end - println("M values: ", M) for cref in _indicator_to_constraints(model)[lvref] con = JuMP.constraint_object(cref) append!(ref_cons, reformulate_disjunct_constraint(model, con, bconref, M, method)) @@ -220,17 +219,14 @@ end function _mini_model( model::JuMP.AbstractModel, - objective::ScalarConstraint, + objective::ScalarConstraint{T,S}, constraints::Vector{DisjunctConstraintRef}, method::MBM -) +) where {T,S <: Union{_MOI.LessThan, _MOI.GreaterThan}} sub_model = Model() new_vars = Dict{VariableRef, VariableRef}() for var in all_variables(model) new_vars[var] = @variable(sub_model, base_name= "sub_model_$(JuMP.name(var))") - if is_binary(var) - JuMP.set_binary(new_vars[var]) - end if is_fixed(var) JuMP.fix(new_vars[var], fix_value(var); force=true) end @@ -244,7 +240,7 @@ function _mini_model( set_lower_bound(new_vars[var], lower_bound(var)) end if has_start_value(var) - set_start(new_vars[var], start(var)) + JuMP.set_start_value(new_vars[var], start_value(var)) end end for con in [constraint_object(con) for con in constraints] @@ -265,14 +261,14 @@ function _mini_model( return M end -# function _mini_model( -# ::JuMP.AbstractModel, -# ::{T,S}, -# ::V, -# ::MBM -# ) where {T, S} -# error("This type of constraints and objective constraint has not been implemented for MBM subproblems") -# end +function _mini_model( + model::JuMP.AbstractModel, + objective::ScalarConstraint{T,S}, + constraints::Vector{DisjunctConstraintRef}, + method::MBM +) where {T,S <: Union{_MOI.Nonpositives, _MOI.Nonnegatives, _MOI.Zeros, MOI.EqualTo, MOI.Interval}} + error("This type of constraints and objective constraint has not been implemented for MBM subproblems") +end ################################################################################ # CONSTRAINT TO OBJECTIVE diff --git a/test/constraints/mbm.jl b/test/constraints/mbm.jl index 141ee54..ba9ecb3 100644 --- a/test/constraints/mbm.jl +++ b/test/constraints/mbm.jl @@ -1,12 +1,9 @@ -#Testing replacement of variables -#TODO: Test bad inputs throw errors for MINI MODEL -using Gurobi -using Revise +using HiGHS function test_mbm() - @test MBM(Gurobi.Optimizer).optimizer == Gurobi.Optimizer - @test MBM(Gurobi.Optimizer).value == 1e9 - @test MBM(Gurobi.Optimizer, 100).value == 100 + @test MBM(HiGHS.Optimizer).optimizer == HiGHS.Optimizer + @test MBM(HiGHS.Optimizer).value == 1e9 + @test MBM(HiGHS.Optimizer, 100).value == 100 end function test_replace_variables_in_constraint() @@ -34,17 +31,6 @@ function test_replace_variables_in_constraint() @test expr3 isa JuMP.NonlinearExpr # Test it's a nonlinear expression @test expr4 == [new_vars[x[i]] for i in 1:3] @test_throws ErrorException DP.replace_variables_in_constraint("String", new_vars) -end - -function test_mini_model() - - - - - - - - end function test_constraint_to_objective() @@ -65,6 +51,30 @@ function test_constraint_to_objective() @test_throws ErrorException DP.constraint_to_objective(sub_model, constraint_object(interval), new_vars) end +function test_mini_model() + model = GDPModel() + @variable(model, 0 <= x, start = 1) + @variable(model, 0 <= y) + @variable(model, Y[1:3], Logical) + @constraint(model, con, 3*-x <= 4, Disjunct(Y[1])) + @constraint(model, con2, 3*x + y >= 15, Disjunct(Y[2])) + @constraint(model, infeasiblecon, 3*x + y == 15, Disjunct(Y[3])) + @disjunction(model, [Y[1], Y[2], Y[3]]) + @test DP._mini_model(model, constraint_object(con), DisjunctConstraintRef[con2], MBM(HiGHS.Optimizer))== -4 + set_upper_bound(x, 1) + @test DP._mini_model(model, constraint_object(con2), DisjunctConstraintRef[con], MBM(HiGHS.Optimizer))== 15 + set_integer(y) + @constraint(model, con3, y*x == 15, Disjunct(Y[1])) + @test DP._mini_model(model, constraint_object(con2), DisjunctConstraintRef[con], MBM(HiGHS.Optimizer))== 15 + JuMP.fix(y, 5; force=true) + @test DP._mini_model(model, constraint_object(con2), DisjunctConstraintRef[con], MBM(HiGHS.Optimizer))== 10 + delete_lower_bound(x) + @test DP._mini_model(model, constraint_object(con2), DisjunctConstraintRef[con2], MBM(HiGHS.Optimizer))== 200 + @test_throws ErrorException DP._mini_model(model, constraint_object(infeasiblecon), DisjunctConstraintRef[con], MBM(HiGHS.Optimizer)) +end + + + function test_maximize_M() model = GDPModel() @variable(model, 0 <= x[1:2] <= 50) @@ -76,14 +86,15 @@ function test_maximize_M() @constraint(model, nonpositives, -x in MOI.Nonpositives(2), Disjunct(Y[4])) @constraint(model, nonnegatives, x in MOI.Nonnegatives(2), Disjunct(Y[5])) @constraint(model, zeros, -x .+ 1 in MOI.Zeros(2), Disjunct(Y[6])) - @test DP._maximize_M(model, constraint_object(lessthan), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(Gurobi.Optimizer)) == 49 - @test DP._maximize_M(model, constraint_object(greaterthan), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(Gurobi.Optimizer)) == 1.0 - @test DP._maximize_M(model, constraint_object(interval), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[1]]), MBM(Gurobi.Optimizer)) == -1.0 - @test DP._maximize_M(model, constraint_object(equalto), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[3]]), MBM(Gurobi.Optimizer)) == 0 - @test DP._maximize_M(model, constraint_object(nonpositives), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(Gurobi.Optimizer)) == 0 - @test DP._maximize_M(model, constraint_object(nonnegatives), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(Gurobi.Optimizer)) == 0 - @test DP._maximize_M(model, constraint_object(zeros), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(Gurobi.Optimizer)) == 49 - @test_throws ErrorException DP._maximize_M(model, "odd", Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(Gurobi.Optimizer)) + + @test DP._maximize_M(model, constraint_object(lessthan), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(HiGHS.Optimizer)) == 49 + @test DP._maximize_M(model, constraint_object(greaterthan), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(HiGHS.Optimizer)) == 1.0 + @test DP._maximize_M(model, constraint_object(interval), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[1]]), MBM(HiGHS.Optimizer)) == -1.0 + @test DP._maximize_M(model, constraint_object(equalto), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[3]]), MBM(HiGHS.Optimizer)) == 0 + @test DP._maximize_M(model, constraint_object(nonpositives), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(HiGHS.Optimizer)) == 0 + @test DP._maximize_M(model, constraint_object(nonnegatives), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(HiGHS.Optimizer)) == 0 + @test DP._maximize_M(model, constraint_object(zeros), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(HiGHS.Optimizer)) == 49 + @test_throws ErrorException DP._maximize_M(model, "odd", Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(HiGHS.Optimizer)) end function test_reformulate_disjunct_constraint() @@ -100,7 +111,7 @@ function test_reformulate_disjunct_constraint() M = Float64[1,2,3,4,5,6] bconref = [binary_variable(Y[i]) for i in 1:6] - reformulated_constraints = [reformulate_disjunct_constraint(model, constraint_object(constraints), bconref, M, MBM(Gurobi.Optimizer)) for constraints in [lessthan, greaterthan, interval, equalto, nonpositives, nonnegatives, zeros]] + reformulated_constraints = [reformulate_disjunct_constraint(model, constraint_object(constraints), bconref, M, MBM(HiGHS.Optimizer)) for constraints in [lessthan, greaterthan, interval, equalto, nonpositives, nonnegatives, zeros]] @test reformulated_constraints[1][1].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_constraints[1][1].set == MOI.LessThan(1.0) @test reformulated_constraints[2][1].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_constraints[2][1].set == MOI.GreaterThan(1.0) @test reformulated_constraints[3][2].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_constraints[3][1].set == MOI.GreaterThan(0.0) @@ -126,50 +137,44 @@ function test_reformulate_disjunct() @constraint(model, zeros, -x .+ 1 in MOI.Zeros(2), Disjunct(Y[6])) @disjunction(model, [Y[1], Y[2], Y[3], Y[4], Y[5], Y[6]]) - reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[1], LogicalVariableRef[Y[2], Y[3], Y[4], Y[5], Y[6]], MBM(Gurobi.Optimizer)) - M = [48.0, -0.0, 48.0, 48.0, -0.0] - bconref = [binary_variable(Y[i]) for i in 2:6] + bconref = [binary_variable(Y[i]) for i in 1:6] + reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[1], LogicalVariableRef[Y[2], Y[3], Y[4], Y[5], Y[6]], MBM(HiGHS.Optimizer)) + M = [0.0 , 48.0, 0.0, 48.0, 48.0, 0.0] @test reformulated_disjunct[1].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[1].set == MOI.LessThan(2.0) @test reformulated_disjunct[2].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[2].set == MOI.GreaterThan(1.0) - reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[2], LogicalVariableRef[Y[1], Y[3], Y[4], Y[5], Y[6]], MBM(Gurobi.Optimizer)) + reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[2], LogicalVariableRef[Y[1], Y[3], Y[4], Y[5], Y[6]], MBM(HiGHS.Optimizer)) M = [1.0, 0.0, 1.0, 1.0, 1.0, 1.0] - bconref = [binary_variable(Y[i]) for i in 1:6] @test reformulated_disjunct[2].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[2].set == MOI.LessThan(55.0) @test reformulated_disjunct[1].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[1].set == MOI.GreaterThan(0.0) - reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[3], LogicalVariableRef[Y[1], Y[2], Y[4], Y[5], Y[6]], MBM(Gurobi.Optimizer)) + reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[3], LogicalVariableRef[Y[1], Y[2], Y[4], Y[5], Y[6]], MBM(HiGHS.Optimizer)) M = [1.0, 49.0, 0.0, 49.0, 49.0, 0.0] - bconref = [binary_variable(Y[i]) for i in 1:6] @test reformulated_disjunct[2].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[2].set == MOI.LessThan(1.0) @test reformulated_disjunct[1].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[1].set == MOI.GreaterThan(1.0) - reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[4], LogicalVariableRef[Y[1], Y[2], Y[3], Y[5], Y[6]], MBM(Gurobi.Optimizer)) + reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[4], LogicalVariableRef[Y[1], Y[2], Y[3], Y[5], Y[6]], MBM(HiGHS.Optimizer)) M = [-1.0, -1.0, -1.0, 0.0, -1.0, -1.0] - bconref = [binary_variable(Y[i]) for i in 1:6] @test reformulated_disjunct[1].func == JuMP.@expression(model, -x .- sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[1].set == MOI.Nonpositives(2) - reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[5], LogicalVariableRef[Y[1], Y[2], Y[3], Y[4], Y[6]], MBM(Gurobi.Optimizer)) + reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[5], LogicalVariableRef[Y[1], Y[2], Y[3], Y[4], Y[6]], MBM(HiGHS.Optimizer)) M = [-1.0, -1.0, -1.0, -1.0, 0.0, -1.0] - bconref = [binary_variable(Y[i]) for i in 1:6] @test reformulated_disjunct[1].func == JuMP.@expression(model, x .+ sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[1].set == MOI.Nonnegatives(2) - reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[6], LogicalVariableRef[Y[1], Y[2], Y[3], Y[4], Y[5]], MBM(Gurobi.Optimizer)) + reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[6], LogicalVariableRef[Y[1], Y[2], Y[3], Y[4], Y[5]], MBM(HiGHS.Optimizer)) M = [49.0, 49.0, 49.0, 49.0, 49.0, 0.0] - bconref = [binary_variable(Y[i]) for i in 1:6] - println(reformulated_disjunct) @test reformulated_disjunct[1].func == JuMP.@expression(model, -x .+ sum(M[i] * bconref[i] for i in 1:length(M)) .+ 1) && reformulated_disjunct[1].set == MOI.Nonnegatives(2) @test reformulated_disjunct[2].func == JuMP.@expression(model, -x .- sum(M[i] * bconref[i] for i in 1:length(M)) .+ 1) && reformulated_disjunct[2].set == MOI.Nonpositives(2) end - @testset "MBM" begin test_mbm() - # test_replace_variables_in_constraint() - # test_constraint_to_objective() - # test_maximize_M() - # test_reformulate_disjunct_constraint() - # test_reformulate_disjunct() + test_replace_variables_in_constraint() + test_constraint_to_objective() + test_mini_model() + test_maximize_M() + test_reformulate_disjunct_constraint() + test_reformulate_disjunct() end From 783d7afbb382062aeef1408c45c8952786c60ff3 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Mon, 23 Jun 2025 16:56:04 -0400 Subject: [PATCH 07/46] Code coverage 100% for mbm.jl file. --- src/mbm.jl | 2 -- test/constraints/mbm.jl | 4 +--- test/runtests.jl | 32 +++++++++++++++----------------- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/mbm.jl b/src/mbm.jl index e6afacc..e71bcf2 100644 --- a/src/mbm.jl +++ b/src/mbm.jl @@ -252,9 +252,7 @@ function _mini_model( set_silent(sub_model) optimize!(sub_model) if JuMP.termination_status(sub_model) != MOI.OPTIMAL || !JuMP.has_values(sub_model) || JuMP.primal_status(sub_model) != MOI.FEASIBLE_POINT - println("Termination status: ", JuMP.termination_status(sub_model)) M = 200 - println("Warning: Optimization did not find a feasible solution. Using default M value of 200.") else M = objective_value(sub_model) end diff --git a/test/constraints/mbm.jl b/test/constraints/mbm.jl index ba9ecb3..d01666e 100644 --- a/test/constraints/mbm.jl +++ b/test/constraints/mbm.jl @@ -69,12 +69,10 @@ function test_mini_model() JuMP.fix(y, 5; force=true) @test DP._mini_model(model, constraint_object(con2), DisjunctConstraintRef[con], MBM(HiGHS.Optimizer))== 10 delete_lower_bound(x) - @test DP._mini_model(model, constraint_object(con2), DisjunctConstraintRef[con2], MBM(HiGHS.Optimizer))== 200 + @test DP._mini_model(model, constraint_object(con2), DisjunctConstraintRef[con2], MBM(HiGHS.Optimizer)) == 200 @test_throws ErrorException DP._mini_model(model, constraint_object(infeasiblecon), DisjunctConstraintRef[con], MBM(HiGHS.Optimizer)) end - - function test_maximize_M() model = GDPModel() @variable(model, 0 <= x[1:2] <= 50) diff --git a/test/runtests.jl b/test/runtests.jl index 0f058ff..42e0738 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,24 +1,22 @@ import DisjunctiveProgramming as DP using DisjunctiveProgramming 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("codecoverage.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/hull.jl") +include("constraints/fallback.jl") +include("constraints/disjunction.jl") +include("print.jl") +include("solve.jl") include("constraints/mbm.jl") From fdbe64c97bde91c2b1b9dbb85fc0dfa259abc8e9 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Tue, 24 Jun 2025 17:36:05 -0400 Subject: [PATCH 08/46] Added comments, full code coverage on mbm.jl file TO BE TESTED --- src/mbm.jl | 9 ++++---- test/constraints/mbm.jl | 49 ++++++++++++++++++++--------------------- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/src/mbm.jl b/src/mbm.jl index e71bcf2..6555df0 100644 --- a/src/mbm.jl +++ b/src/mbm.jl @@ -1,5 +1,3 @@ -#TODO: Add dead end functions for when something super generic is given. - ################################################################################ # CONSTRAINT, DISJUNCTION, DISJUNCT REFORMULATION ################################################################################ @@ -10,7 +8,6 @@ function reformulate_disjunction(model::JuMP.AbstractModel, ) ref_cons = Vector{JuMP.AbstractConstraint}() for d in disj.indicators - println(typeof(d)) _reformulate_disjunct(model, ref_cons, d, LogicalVariableRef[x for x in disj.indicators if x != d], method) end return ref_cons @@ -146,7 +143,7 @@ end ################################################################################ # MULTIPLE BIG-M REFORMULATION ################################################################################ - +#Dispatches over constaint types to reformulate into >= or <= in order to solve the mini-model function _maximize_M( model::JuMP.AbstractModel, objective::VectorConstraint{T, S, R}, @@ -217,6 +214,7 @@ function _maximize_M( error("This type of constraints and objective constraint has not been implemented for MBM subproblems\nF: $(F)") end +#Solve a mini-model to find the maximum value of the objective function for M value function _mini_model( model::JuMP.AbstractModel, objective::ScalarConstraint{T,S}, @@ -258,7 +256,8 @@ function _mini_model( end return M end - +#Catches any constraints that were not reformulated in _maximize_M +#_mini_model requires objective to be >= or <= in order to run function _mini_model( model::JuMP.AbstractModel, objective::ScalarConstraint{T,S}, diff --git a/test/constraints/mbm.jl b/test/constraints/mbm.jl index d01666e..6202f39 100644 --- a/test/constraints/mbm.jl +++ b/test/constraints/mbm.jl @@ -125,44 +125,42 @@ end function test_reformulate_disjunct() model = GDPModel() @variable(model, 1 <= x[1:2] <= 50) - @variable(model, Y[1:6], Logical) + @variable(model, Y[1:2], Logical) @constraint(model, lessthan, x[1] <= 2, Disjunct(Y[1])) @constraint(model, greaterthan, x[1] >= 1, Disjunct(Y[1])) @constraint(model, interval, 0 <= x[1] <= 55, Disjunct(Y[2])) - @constraint(model, equalto, x[1] == 1, Disjunct(Y[3])) - @constraint(model, nonpositives, -x in MOI.Nonpositives(2), Disjunct(Y[4])) - @constraint(model, nonnegatives, x in MOI.Nonnegatives(2), Disjunct(Y[5])) - @constraint(model, zeros, -x .+ 1 in MOI.Zeros(2), Disjunct(Y[6])) - @disjunction(model, [Y[1], Y[2], Y[3], Y[4], Y[5], Y[6]]) - bconref = [binary_variable(Y[i]) for i in 1:6] - reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[1], LogicalVariableRef[Y[2], Y[3], Y[4], Y[5], Y[6]], MBM(HiGHS.Optimizer)) - M = [0.0 , 48.0, 0.0, 48.0, 48.0, 0.0] + bconref = [binary_variable(Y[i]) for i in 1:2] + reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[1], LogicalVariableRef[Y[2]], MBM(HiGHS.Optimizer)) + println(reformulated_disjunct) + M = [0, 48] @test reformulated_disjunct[1].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[1].set == MOI.LessThan(2.0) @test reformulated_disjunct[2].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[2].set == MOI.GreaterThan(1.0) - reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[2], LogicalVariableRef[Y[1], Y[3], Y[4], Y[5], Y[6]], MBM(HiGHS.Optimizer)) - M = [1.0, 0.0, 1.0, 1.0, 1.0, 1.0] + reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[2], LogicalVariableRef[Y[1]], MBM(HiGHS.Optimizer)) + M = [1, 0] @test reformulated_disjunct[2].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[2].set == MOI.LessThan(55.0) @test reformulated_disjunct[1].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[1].set == MOI.GreaterThan(0.0) - reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[3], LogicalVariableRef[Y[1], Y[2], Y[4], Y[5], Y[6]], MBM(HiGHS.Optimizer)) - M = [1.0, 49.0, 0.0, 49.0, 49.0, 0.0] - @test reformulated_disjunct[2].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[2].set == MOI.LessThan(1.0) - @test reformulated_disjunct[1].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[1].set == MOI.GreaterThan(1.0) +end - reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[4], LogicalVariableRef[Y[1], Y[2], Y[3], Y[5], Y[6]], MBM(HiGHS.Optimizer)) - M = [-1.0, -1.0, -1.0, 0.0, -1.0, -1.0] - @test reformulated_disjunct[1].func == JuMP.@expression(model, -x .- sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[1].set == MOI.Nonpositives(2) +function test_reformulate_disjunction() + model = GDPModel() + @variable(model, x) + @variable(model, Y[1:2], Logical) + @constraint(model, lessthan, x <= 2, Disjunct(Y[1])) + @constraint(model, greaterthan, x >= 1, Disjunct(Y[1])) + @constraint(model, interval, 0 <= x <= 55, Disjunct(Y[2])) - reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[5], LogicalVariableRef[Y[1], Y[2], Y[3], Y[4], Y[6]], MBM(HiGHS.Optimizer)) - M = [-1.0, -1.0, -1.0, -1.0, 0.0, -1.0] - @test reformulated_disjunct[1].func == JuMP.@expression(model, x .+ sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[1].set == MOI.Nonnegatives(2) + disj = disjunction(model, [Y[1], Y[2]]) - reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[6], LogicalVariableRef[Y[1], Y[2], Y[3], Y[4], Y[5]], MBM(HiGHS.Optimizer)) - M = [49.0, 49.0, 49.0, 49.0, 49.0, 0.0] - @test reformulated_disjunct[1].func == JuMP.@expression(model, -x .+ sum(M[i] * bconref[i] for i in 1:length(M)) .+ 1) && reformulated_disjunct[1].set == MOI.Nonnegatives(2) - @test reformulated_disjunct[2].func == JuMP.@expression(model, -x .- sum(M[i] * bconref[i] for i in 1:length(M)) .+ 1) && reformulated_disjunct[2].set == MOI.Nonpositives(2) + ref_cons = reformulate_disjunction(model, constraint_object(disj), MBM(HiGHS.Optimizer)) + println(ref_cons[2]) + @test ref_cons[1].func == JuMP.@expression(model, x - 53 * binary_variable(Y[2])) && ref_cons[1].set == MOI.LessThan(2.0) + @test ref_cons[2].func == JuMP.@expression(model, x + 53 * binary_variable(Y[2])) && ref_cons[2].set == MOI.GreaterThan(1.0) + + @test ref_cons[3].func == JuMP.@expression(model, x + binary_variable(Y[1])) && ref_cons[3].set == MOI.GreaterThan(0.0) + @test ref_cons[4].func == JuMP.@expression(model, x - binary_variable(Y[1])) && ref_cons[4].set == MOI.LessThan(55.0) end @testset "MBM" begin @@ -173,6 +171,7 @@ end test_maximize_M() test_reformulate_disjunct_constraint() test_reformulate_disjunct() + test_reformulate_disjunction() end From db73d2ffe114b7c7d86fe878cf56e8b3e1b8ed46 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Wed, 25 Jun 2025 11:36:33 -0400 Subject: [PATCH 09/46] Code coverage tested to be 100%. Further documentation for mbm test_replace_variables_in_constraint() testcase hack. --- test/constraints/mbm.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/constraints/mbm.jl b/test/constraints/mbm.jl index 6202f39..376d0b5 100644 --- a/test/constraints/mbm.jl +++ b/test/constraints/mbm.jl @@ -27,7 +27,10 @@ function test_replace_variables_in_constraint() @test expr2 == JuMP.@expression(sub_model, new_vars[x[2]]*new_vars[x[1]]) @test varref == new_vars[x[1]] # Test nonlinear expression structure - #TODO: Fix expr3 test, for some reason it's not as easy to compare + #TODO: Fix expr3 test, JuMP.@expression(model, sin(x[3]) - 0) is not equal to expr3 + #MBM: Test Failed at c:\Users\LocalAdmin\Code\DisjunctiveProgramming.jl\test\constraints\mbm.jl:32 + # Expression: expr3 == #= c:\Users\LocalAdmin\Code\DisjunctiveProgramming.jl\test\constraints\mbm.jl:32 =# JuMP.@expression(model, sin(x[3]) - 0) + # Evaluated: sin(_[3]) - 0.0 == sin(x[3]) - 0.0 @test expr3 isa JuMP.NonlinearExpr # Test it's a nonlinear expression @test expr4 == [new_vars[x[i]] for i in 1:3] @test_throws ErrorException DP.replace_variables_in_constraint("String", new_vars) From 0f408951d8e6e70b7fbe40ff91bc4c752c59d740 Mon Sep 17 00:00:00 2001 From: dnguyen227 <82475321+dnguyen227@users.noreply.github.com> Date: Wed, 25 Jun 2025 11:48:11 -0400 Subject: [PATCH 10/46] Delete src/codecoverage.jl deleting prior to merge to main branch --- src/codecoverage.jl | 43 ------------------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 src/codecoverage.jl diff --git a/src/codecoverage.jl b/src/codecoverage.jl deleted file mode 100644 index 94eb28d..0000000 --- a/src/codecoverage.jl +++ /dev/null @@ -1,43 +0,0 @@ -using Coverage - -function run_coverage_analysis() - # Get the directory of this file - pkg_dir = dirname(@__DIR__) - - # Change to package directory - cd(pkg_dir) do - # Clean up previous coverage files - clean_folder(".") - - # Run tests with coverage - println("Running tests with coverage...") - run(`$(Base.julia_cmd()) --code-coverage=user test/runtests.jl`) - - # Process coverage data - println("\nAnalyzing coverage...") - coverage = process_folder("src") # Only process src/ directory - - # Generate and display coverage report - covered, total = get_summary(coverage) - coverage_pct = round((covered / total) * 100, digits=2) - - println("\n=== Coverage Summary ===") - println("Covered lines: $covered / $total") - println("Coverage: $coverage_pct%") - - # Generate detailed report - # println("\n=== File-by-File Coverage ===") - # for file in coverage - # file_cov, file_total = get_summary([file]) - # if file_total > 0 - # pct = round((file_cov / file_total) * 100, digits=1) - # # println("$(rpad(basename(file.filename), 30)) $(lpad(string(pct), 5))%") - # end - # end - - return coverage - end -end - -# Run the analysis -run_coverage_analysis() \ No newline at end of file From 0ae36236d208dc841115addb888e1729e8e78b6a Mon Sep 17 00:00:00 2001 From: dnguyen227 <82475321+dnguyen227@users.noreply.github.com> Date: Wed, 25 Jun 2025 12:07:13 -0400 Subject: [PATCH 11/46] Update datatypes.jl --- src/datatypes.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/datatypes.jl b/src/datatypes.jl index 46bfab0..a9d36a1 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -373,6 +373,7 @@ end A type for using the multiple big-M reformulation approach for disjunctive constraints. **Fields** +- 'optimizer::O': Optimizer to use when solving mini-models (required). - `value::T`: MBM value (default = `1e9`). """ struct MBM{T,O} <: AbstractReformulationMethod From 0044ea4050d81e06650cf6ffc5a3dcb8d4761935 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Mon, 28 Jul 2025 12:22:53 -0400 Subject: [PATCH 12/46] Updated documentation, JuMP. calls. --- README.md | 4 + src/datatypes.jl | 12 ++- src/mbm.jl | 177 ++++++++++++++++++++-------------------- test/constraints/mbm.jl | 56 ++++++------- 4 files changed, 122 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index 80868bd..d6b09c7 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,10 @@ The following reformulation methods are currently supported: 3. [Indicator](https://jump.dev/JuMP.jl/stable/manual/constraints/#Indicator-constraints): This method reformulates each disjunct constraint into an indicator constraint with the Boolean reformulation counterpart of the Logical variable used to define the disjunct constraint. +4. [MBM](https://doi.org/10.1016/j.compchemeng.2015.02.013): The multiple big-m method creates multiple M values for each disjunct constraint. The 'MBM' struct is created with the following required argument: + + - `optimizer`: Optimizer to use when solving subproblems to determine M values. This is a required value. + ## Release Notes Prior to `v0.4.0`, the package did not leverage the JuMP extension capabilities and was not as robust. For these earlier releases, refer to [Perez, Joshi, and Grossmann, 2023](https://arxiv.org/abs/2304.10492v1) and the following [JuliaCon 2022 Talk](https://www.youtube.com/watch?v=AMIrgTTfUkI). diff --git a/src/datatypes.jl b/src/datatypes.jl index a9d36a1..f8141cd 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -368,21 +368,19 @@ struct BigM{T} <: AbstractReformulationMethod end """ - MBM{T} <: AbstractReformulationMethod + MBM{O} <: AbstractReformulationMethod A type for using the multiple big-M reformulation approach for disjunctive constraints. **Fields** - 'optimizer::O': Optimizer to use when solving mini-models (required). -- `value::T`: MBM value (default = `1e9`). """ -struct MBM{T,O} <: AbstractReformulationMethod - value::T +struct MBM{O} <: AbstractReformulationMethod optimizer::O - # Constructor with optimizer (required) and val (optional) - function MBM(optimizer::O, val::T = 1e9) where {T,O} - new{T,O}(val, optimizer) + # Constructor with optimizer (required) + function MBM(optimizer::O) where {O} + new{O}(optimizer) end end diff --git a/src/mbm.jl b/src/mbm.jl index 6555df0..ba4c9ec 100644 --- a/src/mbm.jl +++ b/src/mbm.jl @@ -7,6 +7,21 @@ function reformulate_disjunction(model::JuMP.AbstractModel, method::MBM ) ref_cons = Vector{JuMP.AbstractConstraint}() + for d in disj.indicators + constraints = copy(_indicator_to_constraints(model)[d]) # Make a copy to avoid modifying during iteration + for cref in constraints + con = JuMP.constraint_object(cref) + if con isa JuMP.ScalarConstraint{<:Any, <:MOI.Interval} + # Create lower and upper bound constraints + lower_con = JuMP.build_constraint(error, con.func, MOI.GreaterThan(con.set.lower)) + upper_con = JuMP.build_constraint(error, con.func, MOI.LessThan(con.set.upper)) + # Create new disjunct constraints + JuMP.add_constraint(model, _DisjunctConstraint(lower_con, d)) + JuMP.add_constraint(model, _DisjunctConstraint(upper_con, d)) + JuMP.delete(model, cref) + end + end + end for d in disj.indicators _reformulate_disjunct(model, ref_cons, d, LogicalVariableRef[x for x in disj.indicators if x != d], method) end @@ -21,17 +36,21 @@ function _reformulate_disjunct( conlvref::Vector{LogicalVariableRef}, method::AbstractReformulationMethod ) - bconref = VariableRef[binary_variable(i) for i in conlvref] + + bconref = Dict(d => binary_variable(d) for d in conlvref) !haskey(_indicator_to_constraints(model), lvref) && return - M = Vector{Float64}(undef, length(conlvref)) - for (i,d) in enumerate(conlvref) - M[i] = maximum(_maximize_M(model, constraint_object(cref), - Vector{DisjunctConstraintRef}(_indicator_to_constraints(model)[d]), method) - for cref in _indicator_to_constraints(model)[lvref]) + constraints = _indicator_to_constraints(model)[lvref] + M = Dict{LogicalVariableRef,Float64}() + sizehint!(M, length(conlvref)) + for d in conlvref + M[d] = maximum(_maximize_M(model, JuMP.constraint_object(cref), + Vector{DisjunctConstraintRef}(_indicator_to_constraints(model)[d]), method) for cref in constraints) end - for cref in _indicator_to_constraints(model)[lvref] + + isempty(constraints) || sizehint!(ref_cons, length(constraints)) + for cref in constraints con = JuMP.constraint_object(cref) - append!(ref_cons, reformulate_disjunct_constraint(model, con, bconref, M, method)) + append!(ref_cons, reformulate_disjunct_constraint(model, con, Dict{LogicalVariableRef,VariableRef}(bconref), M, method)) end return ref_cons end @@ -39,11 +58,11 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bconref:: Vector{VariableRef}, - M::Vector{Float64}, + bconref:: Dict{LogicalVariableRef,VariableRef}, + M::Dict{LogicalVariableRef,Float64}, method::MBM ) where {T, S <: _MOI.Nonpositives, R} - m_sum = sum(M[i] * bconref[i] for i in 1:length(M)) + m_sum = sum(M[i] * bconref[i] for i in keys(M)) new_func = JuMP.@expression(model, [i=1:con.set.dimension], con.func[i] - m_sum ) @@ -54,11 +73,11 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bconref:: Vector{VariableRef}, - M::Vector{Float64}, + bconref:: Dict{LogicalVariableRef,VariableRef}, + M::Dict{LogicalVariableRef,Float64}, method::MBM ) where {T, S <: _MOI.Nonnegatives, R} - m_sum = sum(M[i] * bconref[i] for i in 1:length(M)) + m_sum = sum(M[i] * bconref[i] for i in keys(M)) new_func = JuMP.@expression(model, [i=1:con.set.dimension], con.func[i] + m_sum ) @@ -69,11 +88,11 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bconref:: Vector{VariableRef}, - M::Vector{Float64}, + bconref:: Dict{LogicalVariableRef,VariableRef}, + M::Dict{LogicalVariableRef,Float64}, method::MBM ) where {T, S <: _MOI.Zeros, R} - m_sum = sum(M[i] * bconref[i] for i in 1:length(M)) + m_sum = sum(M[i] * bconref[i] for i in keys(M)) upper_expr = JuMP.@expression(model, [i=1:con.set.dimension], con.func[i] + m_sum ) @@ -89,11 +108,11 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bconref:: Vector{VariableRef}, - M::Vector{Float64}, + bconref:: Dict{LogicalVariableRef,VariableRef}, + M::Dict{LogicalVariableRef,Float64}, method::MBM ) where {T, S <: _MOI.LessThan} - new_func = JuMP.@expression(model, con.func - sum(M[i] * bconref[i] for i in 1:length(M))) + new_func = JuMP.@expression(model, con.func - sum(M[i] * bconref[i] for i in keys(M))) reform_con = JuMP.build_constraint(error, new_func, con.set) return [reform_con] end @@ -101,11 +120,11 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bconref:: Vector{VariableRef}, - M::Vector{Float64}, + bconref:: Dict{LogicalVariableRef,VariableRef}, + M::Dict{LogicalVariableRef,Float64}, method::MBM ) where {T, S <: _MOI.GreaterThan} - new_func = JuMP.@expression(model, con.func + sum(M[i] * bconref[i] for i in 1:length(M))) + new_func = JuMP.@expression(model, con.func + sum(M[i] * bconref[i] for i in keys(M))) reform_con = JuMP.build_constraint(error, new_func, con.set) return [reform_con] end @@ -113,31 +132,25 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bconref::Vector{VariableRef}, - M::Vector{Float64}, + bconref:: Dict{LogicalVariableRef,VariableRef}, + M::Dict{LogicalVariableRef,Float64}, method::MBM -) where {T, S <: _MOI.Interval} - lower_func = JuMP.@expression(model, con.func - sum(M[i] * bconref[i] for i in 1:length(M))) - upper_func = JuMP.@expression(model, con.func + sum(M[i] * bconref[i] for i in 1:length(M))) +) where {T, S <: _MOI.EqualTo} + upper_func = JuMP.@expression(model, con.func - sum(M[i] * bconref[i] for i in keys(M))) + lower_func = JuMP.@expression(model, con.func + sum(M[i] * bconref[i] for i in keys(M))) - lower_con = JuMP.build_constraint(error, lower_func, MOI.GreaterThan(con.set.lower)) - upper_con = JuMP.build_constraint(error, upper_func, MOI.LessThan(con.set.upper)) + upper_con = JuMP.build_constraint(error, upper_func, MOI.LessThan(con.set.value)) + lower_con = JuMP.build_constraint(error, lower_func, MOI.GreaterThan(con.set.value)) return [lower_con, upper_con] end - function reformulate_disjunct_constraint( model::JuMP.AbstractModel, - con::JuMP.ScalarConstraint{T, S}, - bconref::Vector{VariableRef}, - M::Vector{Float64}, + con::Any, + bconref:: Dict{LogicalVariableRef,VariableRef}, + M::Dict{LogicalVariableRef,Float64}, method::MBM -) where {T, S <: _MOI.EqualTo} - upper_func = JuMP.@expression(model, con.func - sum(M[i] * bconref[i] for i in 1:length(M))) - lower_func = JuMP.@expression(model, con.func + sum(M[i] * bconref[i] for i in 1:length(M))) - - upper_con = JuMP.build_constraint(error, upper_func, MOI.LessThan(con.set.value)) - lower_con = JuMP.build_constraint(error, lower_func, MOI.GreaterThan(con.set.value)) - return [lower_con, upper_con] +) + error("Constraint type $(typeof(con)) is not supported by the Multiple Big-M reformulation method.") end ################################################################################ @@ -183,16 +196,6 @@ function _maximize_M( return _mini_model(model, objective, constraints, method) end -function _maximize_M( - model::JuMP.AbstractModel, - objective::ScalarConstraint{T, S}, - constraints::Vector{DisjunctConstraintRef}, - method::MBM -) where {T, S <: _MOI.Interval} - return max(_mini_model(model, ScalarConstraint(objective.func, MOI.GreaterThan(objective.set.lower)), constraints, method), - _mini_model(model, ScalarConstraint(objective.func, MOI.LessThan(objective.set.upper)), constraints, method)) -end - function _maximize_M( model::JuMP.AbstractModel, objective::ScalarConstraint{T, S}, @@ -221,38 +224,38 @@ function _mini_model( constraints::Vector{DisjunctConstraintRef}, method::MBM ) where {T,S <: Union{_MOI.LessThan, _MOI.GreaterThan}} - sub_model = Model() - new_vars = Dict{VariableRef, VariableRef}() - for var in all_variables(model) - new_vars[var] = @variable(sub_model, base_name= "sub_model_$(JuMP.name(var))") - if is_fixed(var) - JuMP.fix(new_vars[var], fix_value(var); force=true) + sub_model = JuMP.Model() + new_vars = Dict{JuMP.VariableRef, JuMP.VariableRef}() + for var in JuMP.all_variables(model) + new_vars[var] = JuMP.@variable(sub_model, base_name= "sub_model_$(JuMP.name(var))") + if JuMP.is_fixed(var) + JuMP.fix(new_vars[var], JuMP.fix_value(var); force=true) end - if is_integer(var) + if JuMP.is_integer(var) JuMP.set_integer(new_vars[var]) end - if has_upper_bound(var) - set_upper_bound(new_vars[var], upper_bound(var)) + if JuMP.has_upper_bound(var) + JuMP.set_upper_bound(new_vars[var], JuMP.upper_bound(var)) end - if has_lower_bound(var) - set_lower_bound(new_vars[var], lower_bound(var)) + if JuMP.has_lower_bound(var) + JuMP.set_lower_bound(new_vars[var], JuMP.lower_bound(var)) end - if has_start_value(var) - JuMP.set_start_value(new_vars[var], start_value(var)) + if JuMP.has_start_value(var) + JuMP.set_start_value(new_vars[var], JuMP.start_value(var)) end end - for con in [constraint_object(con) for con in constraints] + for con in [JuMP.constraint_object(con) for con in constraints] expr = replace_variables_in_constraint(con.func, new_vars) - @constraint(sub_model, expr * 1.0 in con.set) + JuMP.@constraint(sub_model, expr * 1.0 in con.set) end constraint_to_objective(sub_model, objective, new_vars) - set_optimizer(sub_model, method.optimizer) - set_silent(sub_model) - optimize!(sub_model) + JuMP.set_optimizer(sub_model, method.optimizer) + JuMP.set_silent(sub_model) + JuMP.optimize!(sub_model) if JuMP.termination_status(sub_model) != MOI.OPTIMAL || !JuMP.has_values(sub_model) || JuMP.primal_status(sub_model) != MOI.FEASIBLE_POINT - M = 200 + M = 1e9 else - M = objective_value(sub_model) + M = JuMP.objective_value(sub_model) end return M end @@ -270,14 +273,14 @@ end ################################################################################ # CONSTRAINT TO OBJECTIVE ################################################################################ -function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::ScalarConstraint{<:AbstractJuMPScalar, MOI.LessThan{Float64}}, new_vars::Dict{VariableRef, VariableRef}) - @objective(sub_model, Max, - obj.set.upper + replace_variables_in_constraint(obj.func, new_vars)) +function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::JuMP.ScalarConstraint{<:JuMP.AbstractJuMPScalar, MOI.LessThan{Float64}}, new_vars::Dict{JuMP.VariableRef, JuMP.VariableRef}) + JuMP.@objective(sub_model, Max, - obj.set.upper + replace_variables_in_constraint(obj.func, new_vars)) end -function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::ScalarConstraint{<:AbstractJuMPScalar, MOI.GreaterThan{Float64}}, new_vars::Dict{VariableRef, VariableRef}) - @objective(sub_model, Max, - replace_variables_in_constraint(obj.func, new_vars) + obj.set.lower) +function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::JuMP.ScalarConstraint{<:JuMP.AbstractJuMPScalar, MOI.GreaterThan{Float64}}, new_vars::Dict{JuMP.VariableRef, JuMP.VariableRef}) + JuMP.@objective(sub_model, Max, - replace_variables_in_constraint(obj.func, new_vars) + obj.set.lower) end -function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::ScalarConstraint, new_vars::Dict{VariableRef, VariableRef}) +function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::JuMP.ScalarConstraint, new_vars::Dict{JuMP.VariableRef, JuMP.VariableRef}) error("This type of constraint is not supported, only greater than and less than constraints are supported with intervals and equalities being converted.") end @@ -287,40 +290,40 @@ end -function replace_variables_in_constraint(fun::GenericVariableRef, var_map::Dict{VariableRef, VariableRef}) +function replace_variables_in_constraint(fun::JuMP.GenericVariableRef, var_map::Dict{JuMP.VariableRef, JuMP.VariableRef}) return var_map[fun] end -function replace_variables_in_constraint(fun::AffExpr, var_map::Dict{VariableRef, VariableRef}) - new_aff = zero(AffExpr) +function replace_variables_in_constraint(fun::JuMP.AffExpr, var_map::Dict{JuMP.VariableRef, JuMP.VariableRef}) + new_aff = JuMP.zero(JuMP.AffExpr) for (var, coef) in fun.terms new_var = var_map[var] - add_to_expression!(new_aff, coef, new_var) + JuMP.add_to_expression!(new_aff, coef, new_var) end new_aff.constant = fun.constant return new_aff end -function replace_variables_in_constraint(fun::QuadExpr, var_map::Dict{VariableRef, VariableRef}) - new_quad = zero(QuadExpr) +function replace_variables_in_constraint(fun::JuMP.QuadExpr, var_map::Dict{JuMP.VariableRef, JuMP.VariableRef}) + new_quad = JuMP.zero(JuMP.QuadExpr) for (vars, coef) in fun.terms - add_to_expression!(new_quad, coef, var_map[vars.a], var_map[vars.b]) + JuMP.add_to_expression!(new_quad, coef, var_map[vars.a], var_map[vars.b]) end new_aff = replace_variables_in_constraint(fun.aff, var_map) - add_to_expression!(new_quad, new_aff) + JuMP.add_to_expression!(new_quad, new_aff) return new_quad end -function replace_variables_in_constraint(fun::Number, var_map::Dict{VariableRef, VariableRef}) +function replace_variables_in_constraint(fun::Number, var_map::Dict{JuMP.VariableRef, JuMP.VariableRef}) return fun end -function replace_variables_in_constraint(fun::NonlinearExpr, var_map::Dict{VariableRef, VariableRef}) +function replace_variables_in_constraint(fun::JuMP.NonlinearExpr, var_map::Dict{JuMP.VariableRef, JuMP.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 replace_variables_in_constraint(fun::Vector{T}, var_map::Dict{VariableRef, VariableRef}) where T +function replace_variables_in_constraint(fun::Vector{T}, var_map::Dict{JuMP.VariableRef, JuMP.VariableRef}) where T return [replace_variables_in_constraint(expr, var_map) for expr in fun] end diff --git a/test/constraints/mbm.jl b/test/constraints/mbm.jl index 376d0b5..66fc518 100644 --- a/test/constraints/mbm.jl +++ b/test/constraints/mbm.jl @@ -2,8 +2,6 @@ using HiGHS function test_mbm() @test MBM(HiGHS.Optimizer).optimizer == HiGHS.Optimizer - @test MBM(HiGHS.Optimizer).value == 1e9 - @test MBM(HiGHS.Optimizer, 100).value == 100 end function test_replace_variables_in_constraint() @@ -72,7 +70,7 @@ function test_mini_model() JuMP.fix(y, 5; force=true) @test DP._mini_model(model, constraint_object(con2), DisjunctConstraintRef[con], MBM(HiGHS.Optimizer))== 10 delete_lower_bound(x) - @test DP._mini_model(model, constraint_object(con2), DisjunctConstraintRef[con2], MBM(HiGHS.Optimizer)) == 200 + @test DP._mini_model(model, constraint_object(con2), DisjunctConstraintRef[con2], MBM(HiGHS.Optimizer)) == 1.0e9 @test_throws ErrorException DP._mini_model(model, constraint_object(infeasiblecon), DisjunctConstraintRef[con], MBM(HiGHS.Optimizer)) end @@ -90,39 +88,35 @@ function test_maximize_M() @test DP._maximize_M(model, constraint_object(lessthan), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(HiGHS.Optimizer)) == 49 @test DP._maximize_M(model, constraint_object(greaterthan), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(HiGHS.Optimizer)) == 1.0 - @test DP._maximize_M(model, constraint_object(interval), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[1]]), MBM(HiGHS.Optimizer)) == -1.0 @test DP._maximize_M(model, constraint_object(equalto), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[3]]), MBM(HiGHS.Optimizer)) == 0 @test DP._maximize_M(model, constraint_object(nonpositives), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(HiGHS.Optimizer)) == 0 @test DP._maximize_M(model, constraint_object(nonnegatives), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(HiGHS.Optimizer)) == 0 @test DP._maximize_M(model, constraint_object(zeros), Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(HiGHS.Optimizer)) == 49 @test_throws ErrorException DP._maximize_M(model, "odd", Vector{DisjunctConstraintRef}(DP._indicator_to_constraints(model)[Y[2]]), MBM(HiGHS.Optimizer)) end - function test_reformulate_disjunct_constraint() model = GDPModel() @variable(model, 0 <= x[1:2] <= 50) @variable(model, Y[1:6], Logical) @constraint(model, lessthan, x[1] <= 1, Disjunct(Y[1])) @constraint(model, greaterthan, x[1] >= 1, Disjunct(Y[1])) - @constraint(model, interval, 0 <= x[1] <= 55, Disjunct(Y[2])) - @constraint(model, equalto, x[1] == 1, Disjunct(Y[3])) - @constraint(model, nonpositives, -x in MOI.Nonpositives(2), Disjunct(Y[4])) - @constraint(model, nonnegatives, x in MOI.Nonnegatives(2), Disjunct(Y[5])) - @constraint(model, zeros, -x .+ 1 in MOI.Zeros(2), Disjunct(Y[6])) - - M = Float64[1,2,3,4,5,6] - bconref = [binary_variable(Y[i]) for i in 1:6] - reformulated_constraints = [reformulate_disjunct_constraint(model, constraint_object(constraints), bconref, M, MBM(HiGHS.Optimizer)) for constraints in [lessthan, greaterthan, interval, equalto, nonpositives, nonnegatives, zeros]] - @test reformulated_constraints[1][1].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_constraints[1][1].set == MOI.LessThan(1.0) - @test reformulated_constraints[2][1].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_constraints[2][1].set == MOI.GreaterThan(1.0) - @test reformulated_constraints[3][2].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_constraints[3][1].set == MOI.GreaterThan(0.0) - @test reformulated_constraints[3][1].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_constraints[3][2].set == MOI.LessThan(55.0) - @test reformulated_constraints[4][1].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_constraints[4][1].set == MOI.GreaterThan(1.0) - @test reformulated_constraints[4][2].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_constraints[4][2].set == MOI.LessThan(1.0) - @test reformulated_constraints[5][1].func == JuMP.@expression(model, -x .- sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_constraints[5][1].set == MOI.Nonpositives(2) - @test reformulated_constraints[6][1].func == JuMP.@expression(model, x .+ sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_constraints[6][1].set == MOI.Nonnegatives(2) - @test reformulated_constraints[7][1].func == JuMP.@expression(model, -x .+(1 + sum(M[i] * bconref[i] for i in 1:length(M)))) && reformulated_constraints[7][1].set == MOI.Nonnegatives(2) - @test reformulated_constraints[7][2].func == JuMP.@expression(model, -x .+(1 - sum(M[i] * bconref[i] for i in 1:length(M)))) && reformulated_constraints[7][2].set == MOI.Nonpositives(2) + @constraint(model, equalto, x[1] == 1, Disjunct(Y[2])) + @constraint(model, nonpositives, -x in MOI.Nonpositives(2), Disjunct(Y[3])) + @constraint(model, nonnegatives, x in MOI.Nonnegatives(2), Disjunct(Y[4])) + @constraint(model, zeros, -x .+ 1 in MOI.Zeros(2), Disjunct(Y[5])) + + M = Dict{LogicalVariableRef,Float64}(Y[i] => Float64(i) for i in 1:5) + bconref = Dict{LogicalVariableRef,VariableRef}(Y[i] => binary_variable(Y[i]) for i in 1:5) + reformulated_constraints = [reformulate_disjunct_constraint(model, constraint_object(constraints), bconref, M, MBM(HiGHS.Optimizer)) for constraints in [lessthan, greaterthan, equalto, nonpositives, nonnegatives, zeros]] + @test reformulated_constraints[1][1].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in keys(M))) && reformulated_constraints[1][1].set == MOI.LessThan(1.0) + @test reformulated_constraints[2][1].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in keys(M))) && reformulated_constraints[2][1].set == MOI.GreaterThan(1.0) + @test reformulated_constraints[3][1].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in keys(M))) && reformulated_constraints[3][1].set == MOI.GreaterThan(1.0) + @test reformulated_constraints[3][2].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in keys(M))) && reformulated_constraints[3][2].set == MOI.LessThan(1.0) + @test reformulated_constraints[4][1].func == JuMP.@expression(model, -x .- sum(M[i] * bconref[i] for i in keys(M))) && reformulated_constraints[4][1].set == MOI.Nonpositives(2) + @test reformulated_constraints[5][1].func == JuMP.@expression(model, x .+ sum(M[i] * bconref[i] for i in keys(M))) && reformulated_constraints[5][1].set == MOI.Nonnegatives(2) + @test reformulated_constraints[6][1].func == JuMP.@expression(model, -x .+(1 + sum(M[i] * bconref[i] for i in keys(M)))) && reformulated_constraints[6][1].set == MOI.Nonnegatives(2) + @test reformulated_constraints[6][2].func == JuMP.@expression(model, -x .+(1 - sum(M[i] * bconref[i] for i in keys(M)))) && reformulated_constraints[6][2].set == MOI.Nonpositives(2) + @test_throws ErrorException reformulate_disjunct_constraint(model, "odd", bconref, M, MBM(HiGHS.Optimizer)) end function test_reformulate_disjunct() @@ -131,19 +125,19 @@ function test_reformulate_disjunct() @variable(model, Y[1:2], Logical) @constraint(model, lessthan, x[1] <= 2, Disjunct(Y[1])) @constraint(model, greaterthan, x[1] >= 1, Disjunct(Y[1])) - @constraint(model, interval, 0 <= x[1] <= 55, Disjunct(Y[2])) + @constraint(model, interval, x[1] == 55, Disjunct(Y[2])) bconref = [binary_variable(Y[i]) for i in 1:2] reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[1], LogicalVariableRef[Y[2]], MBM(HiGHS.Optimizer)) - println(reformulated_disjunct) - M = [0, 48] + M = [0, 1e9] @test reformulated_disjunct[1].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[1].set == MOI.LessThan(2.0) @test reformulated_disjunct[2].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[2].set == MOI.GreaterThan(1.0) reformulated_disjunct = DP._reformulate_disjunct(model,Vector{JuMP.AbstractConstraint}(),Y[2], LogicalVariableRef[Y[1]], MBM(HiGHS.Optimizer)) - M = [1, 0] + M = [54, 0] + @test reformulated_disjunct[2].func == JuMP.@expression(model, x[1] - sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[2].set == MOI.LessThan(55.0) - @test reformulated_disjunct[1].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[1].set == MOI.GreaterThan(0.0) + @test reformulated_disjunct[1].func == JuMP.@expression(model, x[1] + sum(M[i] * bconref[i] for i in 1:length(M))) && reformulated_disjunct[1].set == MOI.GreaterThan(55.0) end @@ -158,12 +152,8 @@ function test_reformulate_disjunction() disj = disjunction(model, [Y[1], Y[2]]) ref_cons = reformulate_disjunction(model, constraint_object(disj), MBM(HiGHS.Optimizer)) - println(ref_cons[2]) @test ref_cons[1].func == JuMP.@expression(model, x - 53 * binary_variable(Y[2])) && ref_cons[1].set == MOI.LessThan(2.0) @test ref_cons[2].func == JuMP.@expression(model, x + 53 * binary_variable(Y[2])) && ref_cons[2].set == MOI.GreaterThan(1.0) - - @test ref_cons[3].func == JuMP.@expression(model, x + binary_variable(Y[1])) && ref_cons[3].set == MOI.GreaterThan(0.0) - @test ref_cons[4].func == JuMP.@expression(model, x - binary_variable(Y[1])) && ref_cons[4].set == MOI.LessThan(55.0) end @testset "MBM" begin From 56ca380e513105eed13c0ca43062ae62fb9f0f1f Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Mon, 28 Jul 2025 12:27:48 -0400 Subject: [PATCH 13/46] More JuMP. additions --- src/mbm.jl | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/mbm.jl b/src/mbm.jl index ba4c9ec..c517a43 100644 --- a/src/mbm.jl +++ b/src/mbm.jl @@ -58,7 +58,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bconref:: Dict{LogicalVariableRef,VariableRef}, + bconref:: Dict{LogicalVariableRef,JuMP.VariableRef}, M::Dict{LogicalVariableRef,Float64}, method::MBM ) where {T, S <: _MOI.Nonpositives, R} @@ -73,7 +73,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bconref:: Dict{LogicalVariableRef,VariableRef}, + bconref:: Dict{LogicalVariableRef,JuMP.VariableRef}, M::Dict{LogicalVariableRef,Float64}, method::MBM ) where {T, S <: _MOI.Nonnegatives, R} @@ -88,7 +88,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bconref:: Dict{LogicalVariableRef,VariableRef}, + bconref:: Dict{LogicalVariableRef,JuMP.VariableRef}, M::Dict{LogicalVariableRef,Float64}, method::MBM ) where {T, S <: _MOI.Zeros, R} @@ -108,7 +108,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bconref:: Dict{LogicalVariableRef,VariableRef}, + bconref:: Dict{LogicalVariableRef,JuMP.VariableRef}, M::Dict{LogicalVariableRef,Float64}, method::MBM ) where {T, S <: _MOI.LessThan} @@ -120,7 +120,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bconref:: Dict{LogicalVariableRef,VariableRef}, + bconref:: Dict{LogicalVariableRef,JuMP.VariableRef}, M::Dict{LogicalVariableRef,Float64}, method::MBM ) where {T, S <: _MOI.GreaterThan} @@ -132,7 +132,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bconref:: Dict{LogicalVariableRef,VariableRef}, + bconref:: Dict{LogicalVariableRef,JuMP.VariableRef}, M::Dict{LogicalVariableRef,Float64}, method::MBM ) where {T, S <: _MOI.EqualTo} @@ -146,7 +146,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::Any, - bconref:: Dict{LogicalVariableRef,VariableRef}, + bconref:: Dict{LogicalVariableRef,JuMP.VariableRef}, M::Dict{LogicalVariableRef,Float64}, method::MBM ) @@ -159,37 +159,37 @@ end #Dispatches over constaint types to reformulate into >= or <= in order to solve the mini-model function _maximize_M( model::JuMP.AbstractModel, - objective::VectorConstraint{T, S, R}, + objective::JuMP.VectorConstraint{T, S, R}, constraints::Vector{DisjunctConstraintRef}, method::MBM ) where {T, S <: _MOI.Nonpositives, R} - return maximum(_maximize_M(model, ScalarConstraint(objective.func[i], MOI.LessThan(0.0)), constraints, method) for i in 1:objective.set.dimension) + return maximum(_maximize_M(model, JuMP.ScalarConstraint(objective.func[i], MOI.LessThan(0.0)), constraints, method) for i in 1:objective.set.dimension) end function _maximize_M( model::JuMP.AbstractModel, - objective::VectorConstraint{T, S, R}, + objective::JuMP.VectorConstraint{T, S, R}, constraints::Vector{DisjunctConstraintRef}, method::MBM ) where {T, S <: _MOI.Nonnegatives, R} - return maximum(_maximize_M(model, ScalarConstraint(objective.func[i], MOI.GreaterThan(0.0)), constraints, method) for i in 1:objective.set.dimension) + return maximum(_maximize_M(model, JuMP.ScalarConstraint(objective.func[i], MOI.GreaterThan(0.0)), constraints, method) for i in 1:objective.set.dimension) end function _maximize_M( model::JuMP.AbstractModel, - objective::VectorConstraint{T, S, R}, + objective::JuMP.VectorConstraint{T, S, R}, constraints::Vector{DisjunctConstraintRef}, method::MBM ) where {T, S <: _MOI.Zeros, R} return max( - maximum(_maximize_M(model, ScalarConstraint(objective.func[i], MOI.GreaterThan(0.0)), constraints, method) for i in 1:objective.set.dimension), - maximum(_maximize_M(model, ScalarConstraint(objective.func[i], MOI.LessThan(0.0)), constraints, method) for i in 1:objective.set.dimension) + maximum(_maximize_M(model, JuMP.ScalarConstraint(objective.func[i], MOI.GreaterThan(0.0)), constraints, method) for i in 1:objective.set.dimension), + maximum(_maximize_M(model, JuMP.ScalarConstraint(objective.func[i], MOI.LessThan(0.0)), constraints, method) for i in 1:objective.set.dimension) ) end function _maximize_M( model::JuMP.AbstractModel, - objective::ScalarConstraint{T, S}, + objective::JuMP.ScalarConstraint{T, S}, constraints::Vector{DisjunctConstraintRef}, method::MBM ) where {T, S <: Union{_MOI.LessThan, _MOI.GreaterThan}} @@ -198,13 +198,13 @@ end function _maximize_M( model::JuMP.AbstractModel, - objective::ScalarConstraint{T, S}, + objective::JuMP.ScalarConstraint{T, S}, constraints::Vector{DisjunctConstraintRef}, method::MBM ) where {T, S <: _MOI.EqualTo} return max( - _mini_model(model, ScalarConstraint(objective.func, MOI.GreaterThan(objective.set.value)), constraints, method), - _mini_model(model, ScalarConstraint(objective.func, MOI.LessThan(objective.set.value)), constraints, method) + _mini_model(model, JuMP.ScalarConstraint(objective.func, MOI.GreaterThan(objective.set.value)), constraints, method), + _mini_model(model, JuMP.ScalarConstraint(objective.func, MOI.LessThan(objective.set.value)), constraints, method) ) end @@ -220,7 +220,7 @@ end #Solve a mini-model to find the maximum value of the objective function for M value function _mini_model( model::JuMP.AbstractModel, - objective::ScalarConstraint{T,S}, + objective::JuMP.ScalarConstraint{T,S}, constraints::Vector{DisjunctConstraintRef}, method::MBM ) where {T,S <: Union{_MOI.LessThan, _MOI.GreaterThan}} @@ -263,7 +263,7 @@ end #_mini_model requires objective to be >= or <= in order to run function _mini_model( model::JuMP.AbstractModel, - objective::ScalarConstraint{T,S}, + objective::JuMP.ScalarConstraint{T,S}, constraints::Vector{DisjunctConstraintRef}, method::MBM ) where {T,S <: Union{_MOI.Nonpositives, _MOI.Nonnegatives, _MOI.Zeros, MOI.EqualTo, MOI.Interval}} From 9830edbf2c2d326d41a75a6dbfe57c54a6d92a17 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Thu, 7 Aug 2025 11:54:05 -0400 Subject: [PATCH 14/46] Works with AbstractVariableRef --- src/mbm.jl | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/mbm.jl b/src/mbm.jl index c517a43..1f62e8a 100644 --- a/src/mbm.jl +++ b/src/mbm.jl @@ -58,7 +58,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bconref:: Dict{LogicalVariableRef,JuMP.VariableRef}, + bconref:: Dict{LogicalVariableRef,<:JuMP.AbstractVariableRef}, M::Dict{LogicalVariableRef,Float64}, method::MBM ) where {T, S <: _MOI.Nonpositives, R} @@ -73,7 +73,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bconref:: Dict{LogicalVariableRef,JuMP.VariableRef}, + bconref:: Dict{LogicalVariableRef,<:JuMP.AbstractVariableRef}, M::Dict{LogicalVariableRef,Float64}, method::MBM ) where {T, S <: _MOI.Nonnegatives, R} @@ -88,7 +88,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.VectorConstraint{T, S, R}, - bconref:: Dict{LogicalVariableRef,JuMP.VariableRef}, + bconref:: Dict{LogicalVariableRef,<:JuMP.AbstractVariableRef}, M::Dict{LogicalVariableRef,Float64}, method::MBM ) where {T, S <: _MOI.Zeros, R} @@ -108,7 +108,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bconref:: Dict{LogicalVariableRef,JuMP.VariableRef}, + bconref:: Dict{LogicalVariableRef,<:JuMP.AbstractVariableRef}, M::Dict{LogicalVariableRef,Float64}, method::MBM ) where {T, S <: _MOI.LessThan} @@ -120,7 +120,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bconref:: Dict{LogicalVariableRef,JuMP.VariableRef}, + bconref:: Dict{LogicalVariableRef,<:JuMP.AbstractVariableRef}, M::Dict{LogicalVariableRef,Float64}, method::MBM ) where {T, S <: _MOI.GreaterThan} @@ -132,7 +132,7 @@ end function reformulate_disjunct_constraint( model::JuMP.AbstractModel, con::JuMP.ScalarConstraint{T, S}, - bconref:: Dict{LogicalVariableRef,JuMP.VariableRef}, + bconref:: Dict{LogicalVariableRef,<:JuMP.AbstractVariableRef}, M::Dict{LogicalVariableRef,Float64}, method::MBM ) where {T, S <: _MOI.EqualTo} @@ -144,12 +144,12 @@ function reformulate_disjunct_constraint( return [lower_con, upper_con] end function reformulate_disjunct_constraint( - model::JuMP.AbstractModel, - con::Any, - bconref:: Dict{LogicalVariableRef,JuMP.VariableRef}, - M::Dict{LogicalVariableRef,Float64}, - method::MBM -) + ::JuMP.AbstractModel, + ::F, + ::Dict{LogicalVariableRef,<:JuMP.AbstractVariableRef}, + ::Dict{LogicalVariableRef,Float64}, + ::MBM +) where {F} error("Constraint type $(typeof(con)) is not supported by the Multiple Big-M reformulation method.") end @@ -225,7 +225,7 @@ function _mini_model( method::MBM ) where {T,S <: Union{_MOI.LessThan, _MOI.GreaterThan}} sub_model = JuMP.Model() - new_vars = Dict{JuMP.VariableRef, JuMP.VariableRef}() + new_vars = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() for var in JuMP.all_variables(model) new_vars[var] = JuMP.@variable(sub_model, base_name= "sub_model_$(JuMP.name(var))") if JuMP.is_fixed(var) @@ -273,14 +273,14 @@ end ################################################################################ # CONSTRAINT TO OBJECTIVE ################################################################################ -function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::JuMP.ScalarConstraint{<:JuMP.AbstractJuMPScalar, MOI.LessThan{Float64}}, new_vars::Dict{JuMP.VariableRef, JuMP.VariableRef}) +function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::JuMP.ScalarConstraint{<:JuMP.AbstractJuMPScalar, MOI.LessThan{Float64}}, new_vars::Dict{V,K}) where {V <: JuMP.AbstractVariableRef, K <: JuMP.AbstractVariableRef} JuMP.@objective(sub_model, Max, - obj.set.upper + replace_variables_in_constraint(obj.func, new_vars)) end -function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::JuMP.ScalarConstraint{<:JuMP.AbstractJuMPScalar, MOI.GreaterThan{Float64}}, new_vars::Dict{JuMP.VariableRef, JuMP.VariableRef}) +function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::JuMP.ScalarConstraint{<:JuMP.AbstractJuMPScalar, MOI.GreaterThan{Float64}}, new_vars::Dict{V,K}) where {V <: JuMP.AbstractVariableRef, K <: JuMP.AbstractVariableRef} JuMP.@objective(sub_model, Max, - replace_variables_in_constraint(obj.func, new_vars) + obj.set.lower) end -function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::JuMP.ScalarConstraint, new_vars::Dict{JuMP.VariableRef, JuMP.VariableRef}) +function constraint_to_objective(sub_model::JuMP.AbstractModel,obj::JuMP.ScalarConstraint, new_vars::Dict{V,K}) where {V, K} error("This type of constraint is not supported, only greater than and less than constraints are supported with intervals and equalities being converted.") end @@ -290,11 +290,11 @@ end -function replace_variables_in_constraint(fun::JuMP.GenericVariableRef, var_map::Dict{JuMP.VariableRef, JuMP.VariableRef}) +function replace_variables_in_constraint(fun:: JuMP.AbstractVariableRef, var_map::Dict{<:JuMP.AbstractVariableRef,<:JuMP.AbstractVariableRef}) return var_map[fun] end -function replace_variables_in_constraint(fun::JuMP.AffExpr, var_map::Dict{JuMP.VariableRef, JuMP.VariableRef}) +function replace_variables_in_constraint(fun::JuMP.AffExpr, var_map::Dict{<:JuMP.AbstractVariableRef,<:JuMP.AbstractVariableRef}) new_aff = JuMP.zero(JuMP.AffExpr) for (var, coef) in fun.terms new_var = var_map[var] @@ -304,7 +304,7 @@ function replace_variables_in_constraint(fun::JuMP.AffExpr, var_map::Dict{JuMP.V return new_aff end -function replace_variables_in_constraint(fun::JuMP.QuadExpr, var_map::Dict{JuMP.VariableRef, JuMP.VariableRef}) +function replace_variables_in_constraint(fun::JuMP.QuadExpr, var_map::Dict{<:JuMP.AbstractVariableRef,<:JuMP.AbstractVariableRef}) new_quad = JuMP.zero(JuMP.QuadExpr) for (vars, coef) in fun.terms JuMP.add_to_expression!(new_quad, coef, var_map[vars.a], var_map[vars.b]) @@ -314,16 +314,16 @@ function replace_variables_in_constraint(fun::JuMP.QuadExpr, var_map::Dict{JuMP. return new_quad end -function replace_variables_in_constraint(fun::Number, var_map::Dict{JuMP.VariableRef, JuMP.VariableRef}) +function replace_variables_in_constraint(fun::Number, var_map::Dict{<:JuMP.AbstractVariableRef,<:JuMP.AbstractVariableRef}) return fun end -function replace_variables_in_constraint(fun::JuMP.NonlinearExpr, var_map::Dict{JuMP.VariableRef, JuMP.VariableRef}) +function replace_variables_in_constraint(fun::JuMP.NonlinearExpr, var_map::Dict{<:JuMP.AbstractVariableRef,<:JuMP.AbstractVariableRef}) new_args = Any[replace_variables_in_constraint(arg, var_map) for arg in fun.args] return JuMP.NonlinearExpr(fun.head, new_args) end -function replace_variables_in_constraint(fun::Vector{T}, var_map::Dict{JuMP.VariableRef, JuMP.VariableRef}) where T +function replace_variables_in_constraint(fun::Vector{T}, var_map::Dict{<:JuMP.AbstractVariableRef,<:JuMP.AbstractVariableRef}) where T return [replace_variables_in_constraint(expr, var_map) for expr in fun] end From 201080052264e24bbb71445f5fa18b5bf10a3742 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Thu, 7 Aug 2025 12:27:00 -0400 Subject: [PATCH 15/46] Made _copy_variable function --- src/constraints.jl | 26 ++++++++++++++++++++++++++ src/mbm.jl | 19 ++----------------- test/runtests.jl | 31 ++++++++++++++++--------------- 3 files changed, 44 insertions(+), 32 deletions(-) diff --git a/src/constraints.jl b/src/constraints.jl index a25270d..63f0bb5 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -23,6 +23,32 @@ _vec_to_scalar_set(::_MOIExactly) = _MOI.EqualTo _vec_to_scalar_set(::_MOIAtLeast) = _MOI.GreaterThan _vec_to_scalar_set(::_MOIAtMost) = _MOI.LessThan +#helper function to create variables +# Helper function to copy variable properties from an existing variable +function _copy_variable( + target_model::JuMP.AbstractModel, + original_var::JuMP.AbstractVariableRef, + ) + # Create new variable + new_var = JuMP.@variable(target_model, base_name = JuMP.name(original_var)) + + # Copy all properties from original variable + JuMP.has_lower_bound(original_var) && JuMP.set_lower_bound(new_var, JuMP.lower_bound(original_var)) + JuMP.has_upper_bound(original_var) && JuMP.set_upper_bound(new_var, JuMP.upper_bound(original_var)) + JuMP.has_start_value(original_var) && JuMP.set_start_value(new_var, JuMP.start_value(original_var)) + JuMP.is_integer(original_var) && JuMP.set_integer(new_var) + JuMP.is_binary(original_var) && JuMP.set_binary(new_var) + + # Handle fixed values with force=true (as in original MBM code) + if JuMP.is_fixed(original_var) + JuMP.fix(new_var, JuMP.fix_value(original_var); force=true) + end + + return new_var +end + + + ################################################################################ # BOILERPLATE EXTENSION METHODS ################################################################################ diff --git a/src/mbm.jl b/src/mbm.jl index 1f62e8a..387b4c2 100644 --- a/src/mbm.jl +++ b/src/mbm.jl @@ -150,7 +150,7 @@ function reformulate_disjunct_constraint( ::Dict{LogicalVariableRef,Float64}, ::MBM ) where {F} - error("Constraint type $(typeof(con)) is not supported by the Multiple Big-M reformulation method.") + error("Constraint type $(typeof(F)) is not supported by the Multiple Big-M reformulation method.") end ################################################################################ @@ -227,22 +227,7 @@ function _mini_model( sub_model = JuMP.Model() new_vars = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() for var in JuMP.all_variables(model) - new_vars[var] = JuMP.@variable(sub_model, base_name= "sub_model_$(JuMP.name(var))") - if JuMP.is_fixed(var) - JuMP.fix(new_vars[var], JuMP.fix_value(var); force=true) - end - if JuMP.is_integer(var) - JuMP.set_integer(new_vars[var]) - end - if JuMP.has_upper_bound(var) - JuMP.set_upper_bound(new_vars[var], JuMP.upper_bound(var)) - end - if JuMP.has_lower_bound(var) - JuMP.set_lower_bound(new_vars[var], JuMP.lower_bound(var)) - end - if JuMP.has_start_value(var) - JuMP.set_start_value(new_vars[var], JuMP.start_value(var)) - end + new_vars[var] = _copy_variable(sub_model, var) end for con in [JuMP.constraint_object(con) for con in constraints] expr = replace_variables_in_constraint(con.func, new_vars) diff --git a/test/runtests.jl b/test/runtests.jl index 42e0738..a40c691 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,22 +1,23 @@ import DisjunctiveProgramming as DP using DisjunctiveProgramming using Test +using Revise 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/hull.jl") +# include("constraints/fallback.jl") +# include("constraints/disjunction.jl") +# include("print.jl") +# include("solve.jl") include("constraints/mbm.jl") From 151abec802e58a090fe1e9c3e09e11a1f979400e Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Sun, 10 Aug 2025 15:02:30 -0400 Subject: [PATCH 16/46] _copy_variable function added. VariableRef changed to AbstractVariableRef. --- test/constraints/mbm.jl | 2 -- test/runtests.jl | 31 +++++++++++++++---------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/test/constraints/mbm.jl b/test/constraints/mbm.jl index 66fc518..18db02f 100644 --- a/test/constraints/mbm.jl +++ b/test/constraints/mbm.jl @@ -148,9 +148,7 @@ function test_reformulate_disjunction() @constraint(model, lessthan, x <= 2, Disjunct(Y[1])) @constraint(model, greaterthan, x >= 1, Disjunct(Y[1])) @constraint(model, interval, 0 <= x <= 55, Disjunct(Y[2])) - disj = disjunction(model, [Y[1], Y[2]]) - ref_cons = reformulate_disjunction(model, constraint_object(disj), MBM(HiGHS.Optimizer)) @test ref_cons[1].func == JuMP.@expression(model, x - 53 * binary_variable(Y[2])) && ref_cons[1].set == MOI.LessThan(2.0) @test ref_cons[2].func == JuMP.@expression(model, x + 53 * binary_variable(Y[2])) && ref_cons[2].set == MOI.GreaterThan(1.0) diff --git a/test/runtests.jl b/test/runtests.jl index a40c691..42e0738 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,23 +1,22 @@ import DisjunctiveProgramming as DP using DisjunctiveProgramming using Test -using Revise 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/hull.jl") +include("constraints/fallback.jl") +include("constraints/disjunction.jl") +include("print.jl") +include("solve.jl") include("constraints/mbm.jl") From 10ca5d70883b23f1ed47d1dd579f9a6143d639fa Mon Sep 17 00:00:00 2001 From: dnguyen227 <82475321+dnguyen227@users.noreply.github.com> Date: Wed, 3 Sep 2025 13:40:03 -0400 Subject: [PATCH 17/46] Updated _copy_variable() --- src/constraints.jl | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/constraints.jl b/src/constraints.jl index 63f0bb5..97ae89b 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -29,17 +29,14 @@ function _copy_variable( target_model::JuMP.AbstractModel, original_var::JuMP.AbstractVariableRef, ) - # Create new variable new_var = JuMP.@variable(target_model, base_name = JuMP.name(original_var)) - # Copy all properties from original variable JuMP.has_lower_bound(original_var) && JuMP.set_lower_bound(new_var, JuMP.lower_bound(original_var)) JuMP.has_upper_bound(original_var) && JuMP.set_upper_bound(new_var, JuMP.upper_bound(original_var)) JuMP.has_start_value(original_var) && JuMP.set_start_value(new_var, JuMP.start_value(original_var)) JuMP.is_integer(original_var) && JuMP.set_integer(new_var) JuMP.is_binary(original_var) && JuMP.set_binary(new_var) - # Handle fixed values with force=true (as in original MBM code) if JuMP.is_fixed(original_var) JuMP.fix(new_var, JuMP.fix_value(original_var); force=true) end From 3b1d1ffe8e6f74f4c815c61391eb48f4377418db Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Mon, 8 Sep 2025 11:16:59 -0400 Subject: [PATCH 18/46] Update before deleting --- src/constraints.jl | 20 ++++++++ src/cuttingplanes.jl | 113 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 src/cuttingplanes.jl diff --git a/src/constraints.jl b/src/constraints.jl index a25270d..8b9b56c 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -23,6 +23,26 @@ _vec_to_scalar_set(::_MOIExactly) = _MOI.EqualTo _vec_to_scalar_set(::_MOIAtLeast) = _MOI.GreaterThan _vec_to_scalar_set(::_MOIAtMost) = _MOI.LessThan +# helper function to copy variables from one model to another +function _copy_variable( + target_model::JuMP.AbstractModel, + original_var::JuMP.AbstractVariableRef, + ) + new_var = JuMP.@variable(target_model, base_name = JuMP.name(original_var)) + + JuMP.has_lower_bound(original_var) && JuMP.set_lower_bound(new_var, JuMP.lower_bound(original_var)) + JuMP.has_upper_bound(original_var) && JuMP.set_upper_bound(new_var, JuMP.upper_bound(original_var)) + JuMP.has_start_value(original_var) && JuMP.set_start_value(new_var, JuMP.start_value(original_var)) + JuMP.is_integer(original_var) && JuMP.set_integer(new_var) + JuMP.is_binary(original_var) && JuMP.set_binary(new_var) + + if JuMP.is_fixed(original_var) + JuMP.fix(new_var, JuMP.fix_value(original_var); force=true) + end + + return new_var +end + ################################################################################ # BOILERPLATE EXTENSION METHODS ################################################################################ diff --git a/src/cuttingplanes.jl b/src/cuttingplanes.jl new file mode 100644 index 0000000..c9068d6 --- /dev/null +++ b/src/cuttingplanes.jl @@ -0,0 +1,113 @@ +function reformulate_disjunction(model::JuMP.AbstractModel, disj::Disjunction, method::AbstractReformulationMethod) + ref_cons = Vector{JuMP.AbstractConstraint}() #store reformulated constraints + for d in disj.indicators + _reformulate_disjunct(model, ref_cons, d, method) + end + return ref_cons +end + +#I need to map variables between the different models + + +function _map_variables( + model::JuMP.AbstractModel, + ref_cons::Vector{JuMP.AbstractConstraint}, + lvref::LogicalVariableRef, + method::cutting_planes +) + +end + + +function _initialize_relaxed_bigM( + model::JuMP.AbstractModel, + ref_cons::Vector{JuMP.AbstractConstraint}, + lvref::LogicalVariableRef, + method::cutting_planes +) + rBM = copy(model) + reformulate_disjunctions(rBM, BigM()) + + + #Get solution + #Pass solution + return ref_cons +end + +function _initialize_relaxed_SEP( + model::JuMP.AbstractModel, + ref_cons::Vector{JuMP.AbstractConstraint}, + lvref::LogicalVariableRef, + method::cutting_planes +) + #Copy model (hull) + #Set obj to Minimize distance + #Solve + #Get solution + #Pass solution + return ref_cons +end + +function _update_relaxed_bigM( + model::JuMP.AbstractModel, + ref_cons::Vector{JuMP.AbstractConstraint}, + lvref::LogicalVariableRef, + method::cutting_planes +) + #Copy model (bigM) + #Solve relaxed_bigM + #Get solution + #Pass solution + return ref_cons +end + +function _update_relaxed_SEP( + model::JuMP.AbstractModel, + ref_cons::Vector{JuMP.AbstractConstraint}, + lvref::LogicalVariableRef, + method::cutting_planes +) + #Copy model (bigM) + #Solve relaxed_bigM + #Get solution + #Pass solution + return ref_cons +end + +function _relaxed_SEP( + model::JuMP.AbstractModel, + ref_cons::Vector{JuMP.AbstractConstraint}, + lvref::LogicalVariableRef, + method::cutting_planes +) + #Copy model (hull) + #Set obj to Minimize distance + #Solve + #Get solution + #Pass solution + return ref_cons +end + +function _add_cut( + main_model::JuMP.AbstractModel, + rBM_model::JuMP.AbstractModel, + SEP_model::JuMP.AbstractModel, + ref_cons::Vector{JuMP.AbstractConstraint}, + lvref::LogicalVariableRef, + method::cutting_planes +) + #Add cut to all three models + return +end + +function _check_cut_quality( + main_model::JuMP.AbstractModel, + rBM_model::JuMP.AbstractModel, + SEP_model::JuMP.AbstractModel, + ref_cons::Vector{JuMP.AbstractConstraint}, + lvref::LogicalVariableRef, + method::cutting_planes +) + #Check cut quality + return +end \ No newline at end of file From 89ce395a3b56cbf68ea3bd5885e08047300dada1 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Tue, 9 Sep 2025 12:58:29 -0400 Subject: [PATCH 19/46] Initial working solution. To be tested. --- src/DisjunctiveProgramming.jl | 1 + src/constraints.jl | 24 ++++- src/cuttingplanes.jl | 180 +++++++++++++++++----------------- src/datatypes.jl | 18 ++++ src/mbm.jl | 2 +- src/reformulate.jl | 1 + 6 files changed, 133 insertions(+), 93 deletions(-) diff --git a/src/DisjunctiveProgramming.jl b/src/DisjunctiveProgramming.jl index d646f17..ec67195 100644 --- a/src/DisjunctiveProgramming.jl +++ b/src/DisjunctiveProgramming.jl @@ -22,6 +22,7 @@ include("reformulate.jl") include("bigm.jl") include("hull.jl") include("mbm.jl") +include("cuttingplanes.jl") include("indicator.jl") include("print.jl") diff --git a/src/constraints.jl b/src/constraints.jl index 97ae89b..e3ffd90 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -28,6 +28,7 @@ _vec_to_scalar_set(::_MOIAtMost) = _MOI.LessThan function _copy_variable( target_model::JuMP.AbstractModel, original_var::JuMP.AbstractVariableRef, + method::MBM ) new_var = JuMP.@variable(target_model, base_name = JuMP.name(original_var)) @@ -44,7 +45,28 @@ function _copy_variable( return new_var end - +function _copy_variable( + target_model::JuMP.AbstractModel, + original_var::JuMP.AbstractVariableRef, + method::cutting_planes + ) + new_var = JuMP.@variable(target_model, base_name = JuMP.name(original_var)) + + JuMP.has_lower_bound(original_var) && JuMP.set_lower_bound(new_var, JuMP.lower_bound(original_var)) + JuMP.has_upper_bound(original_var) && JuMP.set_upper_bound(new_var, JuMP.upper_bound(original_var)) + JuMP.has_start_value(original_var) && JuMP.set_start_value(new_var, JuMP.start_value(original_var)) + #JuMP.is_integer(original_var) && JuMP.set_integer(new_var) + if JuMP.is_binary(original_var) + JuMP.set_lower_bound(new_var, 0.0) + JuMP.set_upper_bound(new_var, 1.0) + end + + if JuMP.is_fixed(original_var) + JuMP.fix(new_var, JuMP.fix_value(original_var); force=true) + end + + return new_var +end ################################################################################ # BOILERPLATE EXTENSION METHODS diff --git a/src/cuttingplanes.jl b/src/cuttingplanes.jl index c9068d6..80a0be4 100644 --- a/src/cuttingplanes.jl +++ b/src/cuttingplanes.jl @@ -1,113 +1,111 @@ -function reformulate_disjunction(model::JuMP.AbstractModel, disj::Disjunction, method::AbstractReformulationMethod) - ref_cons = Vector{JuMP.AbstractConstraint}() #store reformulated constraints - for d in disj.indicators - _reformulate_disjunct(model, ref_cons, d, method) - end - return ref_cons -end - -#I need to map variables between the different models - - -function _map_variables( +function reformulate_model( model::JuMP.AbstractModel, - ref_cons::Vector{JuMP.AbstractConstraint}, - lvref::LogicalVariableRef, method::cutting_planes -) - -end + ) + #Initializing SEP + SEP = JuMP.Model(method.optimizer) + _reformulate_disjunctions(model, Hull()) + _reformulate_logical_constraints(model) + main_to_SEP_map = _copy_variables_and_constraints(model, SEP, method) + SEP_to_main_map = Dict(v => k for (k, v) in main_to_SEP_map) + #TODO: Right now there are Invalid Variable Refs. Its harmless but a point to polish at the end + #EXAMPLE OUTPUT: Dict{AbstractVariableRef, AbstractVariableRef}(InvalidVariableRef => x[1]_Y[2], InvalidVariableRef => x[2]_Y[1], x[2] => x[2], x[1] => x[1], InvalidVariableRef => x[2]_Y[2], Y[1] => Y[1], InvalidVariableRef => x[1]_Y[1], Y[2] => Y[2]) + + #Initializing rBM + rBM = JuMP.Model(method.optimizer) + reformulate_model(model, BigM(100, false)) + main_to_rBM_map = _copy_variables_and_constraints(model, rBM, method) + rBM_to_main_map = Dict(v => k for (k, v) in main_to_rBM_map) -function _initialize_relaxed_bigM( - model::JuMP.AbstractModel, - ref_cons::Vector{JuMP.AbstractConstraint}, - lvref::LogicalVariableRef, - method::cutting_planes -) - rBM = copy(model) - reformulate_disjunctions(rBM, BigM()) + # Create cross-mappings between rBM and SEP models + rBM_to_SEP_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() + SEP_to_rBM_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() + for (var, rBM_var) in main_to_rBM_map + SEP_var = main_to_SEP_map[var] + rBM_to_SEP_map[rBM_var] = SEP_var + SEP_to_rBM_map[SEP_var] = rBM_var + end + for i in range(1, method.iter) + rBM_sol = _solve_rBM(rBM) + SEP_sol = _solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) + _cutting_planes(model, rBM, main_to_rBM_map, main_to_SEP_map, rBM_sol, SEP_sol) + end - #Get solution - #Pass solution - return ref_cons + _set_solution_method(model, method) + _set_ready_to_optimize(model, true) + return end -function _initialize_relaxed_SEP( +function _copy_variables_and_constraints( model::JuMP.AbstractModel, - ref_cons::Vector{JuMP.AbstractConstraint}, - lvref::LogicalVariableRef, + target_model::JuMP.AbstractModel, method::cutting_planes ) - #Copy model (hull) - #Set obj to Minimize distance - #Solve - #Get solution - #Pass solution - return ref_cons + var_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() + for var in JuMP.all_variables(model) + var_map[var] = _copy_variable(target_model, var, method) + end + constraints = JuMP.all_constraints(model; include_variable_in_set_constraints = false) + for con in [JuMP.constraint_object(con) for con in constraints] + expr = replace_variables_in_constraint(con.func, var_map) + JuMP.@constraint(target_model, expr * 1.0 in con.set) + end + return var_map end -function _update_relaxed_bigM( - model::JuMP.AbstractModel, - ref_cons::Vector{JuMP.AbstractConstraint}, - lvref::LogicalVariableRef, - method::cutting_planes +function _solve_rBM( + rBM::JuMP.AbstractModel, ) - #Copy model (bigM) - #Solve relaxed_bigM - #Get solution - #Pass solution - return ref_cons + optimize!(rBM) + rBM_vars = JuMP.all_variables(rBM) + solution_dict = Dict{JuMP.AbstractVariableRef, Float64}(var => 0.0 for var in rBM_vars) + for rBM_var in rBM_vars + solution_dict[rBM_var] = JuMP.value(rBM_var) + end + return solution_dict end -function _update_relaxed_SEP( - model::JuMP.AbstractModel, - ref_cons::Vector{JuMP.AbstractConstraint}, - lvref::LogicalVariableRef, - method::cutting_planes +function _solve_SEP( + SEP::JuMP.AbstractModel, + rBM::JuMP.AbstractModel, + rBM_sol::Dict{JuMP.AbstractVariableRef, Float64}, + SEP_to_rBM_map::Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}, + rBM_to_SEP_map::Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef} ) - #Copy model (bigM) - #Solve relaxed_bigM - #Get solution - #Pass solution - return ref_cons -end + println(rBM_to_SEP_map) + SEP_vars = [rBM_to_SEP_map[rBM_var] for rBM_var in JuMP.all_variables(rBM)] + println(SEP_vars) + obj_expr = sum((SEP_var - rBM_sol[SEP_to_rBM_map[SEP_var]])^2 for SEP_var in SEP_vars) + JuMP.@objective(SEP, Min, obj_expr) + optimize!(SEP) -function _relaxed_SEP( - model::JuMP.AbstractModel, - ref_cons::Vector{JuMP.AbstractConstraint}, - lvref::LogicalVariableRef, - method::cutting_planes -) - #Copy model (hull) - #Set obj to Minimize distance - #Solve - #Get solution - #Pass solution - return ref_cons + solution_dict = Dict{JuMP.AbstractVariableRef, Float64}(var => 0.0 for var in SEP_vars) + for SEP_var in SEP_vars + solution_dict[SEP_var] = JuMP.value(SEP_var) + end + return solution_dict end -function _add_cut( - main_model::JuMP.AbstractModel, - rBM_model::JuMP.AbstractModel, - SEP_model::JuMP.AbstractModel, - ref_cons::Vector{JuMP.AbstractConstraint}, - lvref::LogicalVariableRef, - method::cutting_planes +function _cutting_planes( + model::JuMP.AbstractModel, + rBM::JuMP.AbstractModel, + main_to_rBM_map::Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}, + main_to_SEP_map::Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}, + rBM_sol::Dict{JuMP.AbstractVariableRef, Float64}, + SEP_sol::Dict{JuMP.AbstractVariableRef, Float64}, ) - #Add cut to all three models - return -end + main_vars = JuMP.all_variables(model) + ξ_sep = Dict{JuMP.AbstractVariableRef, Float64}(var => 0.0 for var in main_vars) + for var in main_vars + ξ_sep[var] = rBM_sol[main_to_rBM_map[var]] - SEP_sol[main_to_SEP_map[var]] + end + + main_cut_expr = JuMP.@expression(model, sum(ξ_sep[var]*(var - SEP_sol[main_to_SEP_map[var]]) for var in main_vars)) + rBM_cut_expr = replace_variables_in_constraint(main_cut_expr, main_to_rBM_map) -function _check_cut_quality( - main_model::JuMP.AbstractModel, - rBM_model::JuMP.AbstractModel, - SEP_model::JuMP.AbstractModel, - ref_cons::Vector{JuMP.AbstractConstraint}, - lvref::LogicalVariableRef, - method::cutting_planes -) - #Check cut quality - return -end \ No newline at end of file + JuMP.@constraint(model, main_cut_expr <= 0) + JuMP.@constraint(rBM, rBM_cut_expr <= 0) + +end diff --git a/src/datatypes.jl b/src/datatypes.jl index f8141cd..9c2d6e3 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -384,6 +384,24 @@ struct MBM{O} <: AbstractReformulationMethod end end +""" + cutting_planes{O} <: AbstractReformulationMethod + +A type for using the cutting planes approach for disjunctive constraints. + +**Fields** +- 'optimizer::O': Optimizer to use when solving mini-models (required). +- 'iter::Int': Number of iterations (default = `3`). +""" +struct cutting_planes{O} <: AbstractReformulationMethod + optimizer::O + iter::Int + + # Constructor with optimizer (required) + function cutting_planes(optimizer::O, iter::Int = 3) where {O} + new{O}(optimizer, iter) + end +end """ diff --git a/src/mbm.jl b/src/mbm.jl index 387b4c2..a8c3442 100644 --- a/src/mbm.jl +++ b/src/mbm.jl @@ -227,7 +227,7 @@ function _mini_model( sub_model = JuMP.Model() new_vars = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() for var in JuMP.all_variables(model) - new_vars[var] = _copy_variable(sub_model, var) + new_vars[var] = _copy_variable(sub_model, var, method) end for con in [JuMP.constraint_object(con) for con in constraints] expr = replace_variables_in_constraint(con.func, new_vars) diff --git a/src/reformulate.jl b/src/reformulate.jl index 9a3b65c..2f004b6 100644 --- a/src/reformulate.jl +++ b/src/reformulate.jl @@ -19,6 +19,7 @@ function reformulate_model(model::JuMP.AbstractModel, method::AbstractSolutionMe return end + function _clear_reformulations(model::JuMP.AbstractModel) delete.(model, _reformulation_constraints(model)) delete.(model, _reformulation_variables(model)) From 178a8630a8985cc07f53040ffaf2ac1b94261081 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Wed, 10 Sep 2025 10:32:12 -0400 Subject: [PATCH 20/46] . --- src/cuttingplanes.jl | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/cuttingplanes.jl b/src/cuttingplanes.jl index 80a0be4..66995ae 100644 --- a/src/cuttingplanes.jl +++ b/src/cuttingplanes.jl @@ -2,7 +2,8 @@ function reformulate_model( model::JuMP.AbstractModel, method::cutting_planes ) - + obj = objective_function(model) + sense = objective_sense(model) #Initializing SEP SEP = JuMP.Model(method.optimizer) _reformulate_disjunctions(model, Hull()) @@ -14,8 +15,9 @@ function reformulate_model( #Initializing rBM rBM = JuMP.Model(method.optimizer) - reformulate_model(model, BigM(100, false)) + reformulate_model(model, BigM(10e8)) main_to_rBM_map = _copy_variables_and_constraints(model, rBM, method) + JuMP.@objective(rBM, sense, replace_variables_in_constraint(obj, main_to_rBM_map)) rBM_to_main_map = Dict(v => k for (k, v) in main_to_rBM_map) # Create cross-mappings between rBM and SEP models @@ -64,6 +66,7 @@ function _solve_rBM( for rBM_var in rBM_vars solution_dict[rBM_var] = JuMP.value(rBM_var) end + println("rBM OBJECTIVE L = ", objective_value(rBM)) return solution_dict end @@ -74,10 +77,9 @@ function _solve_SEP( SEP_to_rBM_map::Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}, rBM_to_SEP_map::Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef} ) - println(rBM_to_SEP_map) SEP_vars = [rBM_to_SEP_map[rBM_var] for rBM_var in JuMP.all_variables(rBM)] - println(SEP_vars) obj_expr = sum((SEP_var - rBM_sol[SEP_to_rBM_map[SEP_var]])^2 for SEP_var in SEP_vars) + println("SEP OBJECTIVE L = ", obj_expr) JuMP.@objective(SEP, Min, obj_expr) optimize!(SEP) @@ -97,15 +99,14 @@ function _cutting_planes( SEP_sol::Dict{JuMP.AbstractVariableRef, Float64}, ) main_vars = JuMP.all_variables(model) + ξ_sep = Dict{JuMP.AbstractVariableRef, Float64}(var => 0.0 for var in main_vars) for var in main_vars - ξ_sep[var] = rBM_sol[main_to_rBM_map[var]] - SEP_sol[main_to_SEP_map[var]] + ξ_sep[var] = 2*(SEP_sol[main_to_SEP_map[var]] - rBM_sol[main_to_rBM_map[var]]) end - main_cut_expr = JuMP.@expression(model, sum(ξ_sep[var]*(var - SEP_sol[main_to_SEP_map[var]]) for var in main_vars)) rBM_cut_expr = replace_variables_in_constraint(main_cut_expr, main_to_rBM_map) - - JuMP.@constraint(model, main_cut_expr <= 0) - JuMP.@constraint(rBM, rBM_cut_expr <= 0) + JuMP.@constraint(model, main_cut_expr >= 0) + JuMP.@constraint(rBM, rBM_cut_expr >= 0) end From 10e3fd1f1ede0188cb4f48964b9465530a0bf96e Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Wed, 22 Oct 2025 14:27:05 -0400 Subject: [PATCH 21/46] . --- src/DisjunctiveProgramming.jl | 1 + src/cuttingplanes.jl | 24 +++++++++++++++--------- src/datatypes.jl | 20 ++++++++++++++++++++ src/model.jl | 1 + test/constraints/cuttingplanes.jl | 0 5 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 test/constraints/cuttingplanes.jl diff --git a/src/DisjunctiveProgramming.jl b/src/DisjunctiveProgramming.jl index 3bf91c8..5dd7e3b 100644 --- a/src/DisjunctiveProgramming.jl +++ b/src/DisjunctiveProgramming.jl @@ -20,6 +20,7 @@ include("constraints.jl") include("macros.jl") include("reformulate.jl") include("bigm.jl") +include("cuttingplanes.jl") include("hull.jl") include("mbm.jl") include("indicator.jl") diff --git a/src/cuttingplanes.jl b/src/cuttingplanes.jl index 66995ae..7d36f09 100644 --- a/src/cuttingplanes.jl +++ b/src/cuttingplanes.jl @@ -2,24 +2,29 @@ function reformulate_model( model::JuMP.AbstractModel, method::cutting_planes ) + println("0") obj = objective_function(model) sense = objective_sense(model) #Initializing SEP SEP = JuMP.Model(method.optimizer) + println("1") _reformulate_disjunctions(model, Hull()) + println("2") _reformulate_logical_constraints(model) + println("3") main_to_SEP_map = _copy_variables_and_constraints(model, SEP, method) SEP_to_main_map = Dict(v => k for (k, v) in main_to_SEP_map) #TODO: Right now there are Invalid Variable Refs. Its harmless but a point to polish at the end #EXAMPLE OUTPUT: Dict{AbstractVariableRef, AbstractVariableRef}(InvalidVariableRef => x[1]_Y[2], InvalidVariableRef => x[2]_Y[1], x[2] => x[2], x[1] => x[1], InvalidVariableRef => x[2]_Y[2], Y[1] => Y[1], InvalidVariableRef => x[1]_Y[1], Y[2] => Y[2]) - + println #Initializing rBM rBM = JuMP.Model(method.optimizer) reformulate_model(model, BigM(10e8)) main_to_rBM_map = _copy_variables_and_constraints(model, rBM, method) - JuMP.@objective(rBM, sense, replace_variables_in_constraint(obj, main_to_rBM_map)) + JuMP.@objective(rBM, sense, _replace_variables_in_constraint(obj, main_to_rBM_map)) rBM_to_main_map = Dict(v => k for (k, v) in main_to_rBM_map) - + println("4") + println("5") # Create cross-mappings between rBM and SEP models rBM_to_SEP_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() SEP_to_rBM_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() @@ -29,14 +34,16 @@ function reformulate_model( SEP_to_rBM_map[SEP_var] = rBM_var end - for i in range(1, method.iter) + for i in 1:method.max_iter rBM_sol = _solve_rBM(rBM) SEP_sol = _solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) _cutting_planes(model, rBM, main_to_rBM_map, main_to_SEP_map, rBM_sol, SEP_sol) end - + # reformulate_model(model, method.final_reformulation) _set_solution_method(model, method) _set_ready_to_optimize(model, true) + println("piper",method.final_reformulation) + return end @@ -47,11 +54,10 @@ function _copy_variables_and_constraints( ) var_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() for var in JuMP.all_variables(model) - var_map[var] = _copy_variable(target_model, var, method) + var_map[var] = variable_copy(target_model, var) end constraints = JuMP.all_constraints(model; include_variable_in_set_constraints = false) - for con in [JuMP.constraint_object(con) for con in constraints] - expr = replace_variables_in_constraint(con.func, var_map) + for con in [JuMP.constraint_object(con) for con in constraints] expr = _replace_variables_in_constraint(con.func, var_map) JuMP.@constraint(target_model, expr * 1.0 in con.set) end return var_map @@ -105,7 +111,7 @@ function _cutting_planes( ξ_sep[var] = 2*(SEP_sol[main_to_SEP_map[var]] - rBM_sol[main_to_rBM_map[var]]) end main_cut_expr = JuMP.@expression(model, sum(ξ_sep[var]*(var - SEP_sol[main_to_SEP_map[var]]) for var in main_vars)) - rBM_cut_expr = replace_variables_in_constraint(main_cut_expr, main_to_rBM_map) + rBM_cut_expr = _replace_variables_in_constraint(main_cut_expr, main_to_rBM_map) JuMP.@constraint(model, main_cut_expr >= 0) JuMP.@constraint(rBM, rBM_cut_expr >= 0) diff --git a/src/datatypes.jl b/src/datatypes.jl index d46f4e0..ca06d45 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -431,6 +431,26 @@ mutable struct _Hull{V <: JuMP.AbstractVariableRef, T} <: AbstractReformulationM end end +""" + cutting_planes{O} <: AbstractReformulationMethod + +A type for using the cutting planes approach for disjunctive constraints. + +**Fields** +- 'optimizer::O': Optimizer to use when solving mini-models (required). +- 'max_iter::Int': Number of iterations (default = `3`). +""" +struct cutting_planes{O} <: AbstractReformulationMethod + optimizer::O + max_iter::Int + tolerance::Float64 + final_reformulation::AbstractReformulationMethod + # Positional constructor + function cutting_planes(optimizer::O, max_iter::Int = 3, tolerance::Float64 = 1e-6, final_reformulation = BigM()) where {O} + new{O}(optimizer, max_iter, tolerance, final_reformulation) + end +end + """ PSplit <: AbstractReformulationMethod diff --git a/src/model.jl b/src/model.jl index 0f26a4f..ad37da6 100644 --- a/src/model.jl +++ b/src/model.jl @@ -45,6 +45,7 @@ function _optimize_hook( kwargs... ) # can add more kwargs if wanted if !_ready_to_optimize(model) || _solution_method(model) != gdp_method + println("Reformulating model", gdp_method) reformulate_model(model, gdp_method) end return JuMP.optimize!(model; ignore_optimize_hook = true, kwargs...) diff --git a/test/constraints/cuttingplanes.jl b/test/constraints/cuttingplanes.jl new file mode 100644 index 0000000..e69de29 From 1eedeacc7a3b0a22c277476bb6405226f1dd1934 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Thu, 23 Oct 2025 14:25:58 -0400 Subject: [PATCH 22/46] addition of option for user to specify final reformulation technique --- src/cuttingplanes.jl | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/cuttingplanes.jl b/src/cuttingplanes.jl index 7d36f09..a256406 100644 --- a/src/cuttingplanes.jl +++ b/src/cuttingplanes.jl @@ -1,30 +1,21 @@ +#TODO: Right now there are Invalid Variable Refs. Its harmless but a point to polish at the end +#EXAMPLE OUTPUT: Dict{AbstractVariableRef, AbstractVariableRef}(InvalidVariableRef => x[1]_Y[2], InvalidVariableRef => x[2]_Y[1], x[2] => x[2], x[1] => x[1], InvalidVariableRef => x[2]_Y[2], Y[1] => Y[1], InvalidVariableRef => x[1]_Y[1], Y[2] => Y[2]) function reformulate_model( model::JuMP.AbstractModel, method::cutting_planes ) - println("0") obj = objective_function(model) sense = objective_sense(model) #Initializing SEP SEP = JuMP.Model(method.optimizer) - println("1") _reformulate_disjunctions(model, Hull()) - println("2") _reformulate_logical_constraints(model) - println("3") main_to_SEP_map = _copy_variables_and_constraints(model, SEP, method) - SEP_to_main_map = Dict(v => k for (k, v) in main_to_SEP_map) - #TODO: Right now there are Invalid Variable Refs. Its harmless but a point to polish at the end - #EXAMPLE OUTPUT: Dict{AbstractVariableRef, AbstractVariableRef}(InvalidVariableRef => x[1]_Y[2], InvalidVariableRef => x[2]_Y[1], x[2] => x[2], x[1] => x[1], InvalidVariableRef => x[2]_Y[2], Y[1] => Y[1], InvalidVariableRef => x[1]_Y[1], Y[2] => Y[2]) - println #Initializing rBM rBM = JuMP.Model(method.optimizer) - reformulate_model(model, BigM(10e8)) + _reformulate_disjunctions(model, BigM(10e8)) main_to_rBM_map = _copy_variables_and_constraints(model, rBM, method) JuMP.@objective(rBM, sense, _replace_variables_in_constraint(obj, main_to_rBM_map)) - rBM_to_main_map = Dict(v => k for (k, v) in main_to_rBM_map) - println("4") - println("5") # Create cross-mappings between rBM and SEP models rBM_to_SEP_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() SEP_to_rBM_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() @@ -39,11 +30,9 @@ function reformulate_model( SEP_sol = _solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) _cutting_planes(model, rBM, main_to_rBM_map, main_to_SEP_map, rBM_sol, SEP_sol) end - # reformulate_model(model, method.final_reformulation) + reformulate_model(model, method.final_reformulation) _set_solution_method(model, method) _set_ready_to_optimize(model, true) - println("piper",method.final_reformulation) - return end From 6368e83a79d437f413a2f444cf1321d78f05409b Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Fri, 24 Oct 2025 12:12:54 -0400 Subject: [PATCH 23/46] working copy --- src/cuttingplanes.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cuttingplanes.jl b/src/cuttingplanes.jl index a256406..7571cfc 100644 --- a/src/cuttingplanes.jl +++ b/src/cuttingplanes.jl @@ -16,7 +16,6 @@ function reformulate_model( _reformulate_disjunctions(model, BigM(10e8)) main_to_rBM_map = _copy_variables_and_constraints(model, rBM, method) JuMP.@objective(rBM, sense, _replace_variables_in_constraint(obj, main_to_rBM_map)) - # Create cross-mappings between rBM and SEP models rBM_to_SEP_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() SEP_to_rBM_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() for (var, rBM_var) in main_to_rBM_map From a18839fe223c4d4d446789ae813291ba5897719f Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Fri, 24 Oct 2025 13:24:43 -0400 Subject: [PATCH 24/46] test file added. --- src/cuttingplanes.jl | 31 +++++++++++++++++++++++++++---- src/model.jl | 1 - test/constraints/cuttingplanes.jl | 14 ++++++++++++++ test/solve.jl | 11 +++++++++++ 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/cuttingplanes.jl b/src/cuttingplanes.jl index 7571cfc..79ebaa6 100644 --- a/src/cuttingplanes.jl +++ b/src/cuttingplanes.jl @@ -42,10 +42,32 @@ function _copy_variables_and_constraints( ) var_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() for var in JuMP.all_variables(model) - var_map[var] = variable_copy(target_model, var) + props = VariableProperties(var) + if props.info.binary + info = JuMP.VariableInfo( + true, 0.0, # has_lb, lower bound + true, 1.0, # has_ub, upper bound + false, 0.0, # has_fix, fix value + props.info.has_start, props.info.start, # preserve start + false, # is_binary = false + false # is_integer = false + ) + props = VariableProperties( + info, + props.name, + nothing, # Clear any binary set + props.variable_type + ) + new_var = create_variable(target_model, props) + var_map[var] = new_var + else + new_var = variable_copy(target_model, var) + var_map[var] = new_var + end end constraints = JuMP.all_constraints(model; include_variable_in_set_constraints = false) - for con in [JuMP.constraint_object(con) for con in constraints] expr = _replace_variables_in_constraint(con.func, var_map) + for con in [JuMP.constraint_object(con) for con in constraints] + expr = _replace_variables_in_constraint(con.func, var_map) JuMP.@constraint(target_model, expr * 1.0 in con.set) end return var_map @@ -54,13 +76,14 @@ end function _solve_rBM( rBM::JuMP.AbstractModel, ) + JuMP.set_silent(rBM) optimize!(rBM) rBM_vars = JuMP.all_variables(rBM) + println(objective_value(rBM)) solution_dict = Dict{JuMP.AbstractVariableRef, Float64}(var => 0.0 for var in rBM_vars) for rBM_var in rBM_vars solution_dict[rBM_var] = JuMP.value(rBM_var) end - println("rBM OBJECTIVE L = ", objective_value(rBM)) return solution_dict end @@ -71,9 +94,9 @@ function _solve_SEP( SEP_to_rBM_map::Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}, rBM_to_SEP_map::Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef} ) + JuMP.set_silent(SEP) SEP_vars = [rBM_to_SEP_map[rBM_var] for rBM_var in JuMP.all_variables(rBM)] obj_expr = sum((SEP_var - rBM_sol[SEP_to_rBM_map[SEP_var]])^2 for SEP_var in SEP_vars) - println("SEP OBJECTIVE L = ", obj_expr) JuMP.@objective(SEP, Min, obj_expr) optimize!(SEP) diff --git a/src/model.jl b/src/model.jl index ad37da6..0f26a4f 100644 --- a/src/model.jl +++ b/src/model.jl @@ -45,7 +45,6 @@ function _optimize_hook( kwargs... ) # can add more kwargs if wanted if !_ready_to_optimize(model) || _solution_method(model) != gdp_method - println("Reformulating model", gdp_method) reformulate_model(model, gdp_method) end return JuMP.optimize!(model; ignore_optimize_hook = true, kwargs...) diff --git a/test/constraints/cuttingplanes.jl b/test/constraints/cuttingplanes.jl index e69de29..7e61a14 100644 --- a/test/constraints/cuttingplanes.jl +++ b/test/constraints/cuttingplanes.jl @@ -0,0 +1,14 @@ +function test_reformulate_model() +end + +function test_copy_variables_and_constraints() +end + +function test_solve_rBM() +end + +function test_solve_SEP() +end + +function test_cutting_planes() +end \ No newline at end of file diff --git a/test/solve.jl b/test/solve.jl index cfd24bd..69f72ad 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -53,6 +53,16 @@ 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 = cutting_planes(HiGHS.Optimizer)) 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) #psplit does not work with complements @@ -112,6 +122,7 @@ function test_quadratic_gdp_example(use_complements = false) #psplit does not wo @test value(Y[2]) @test !value(W[1]) @test !value(W[2]) + end function test_generic_model(m) From 6b1ad1730ddb1bd3f06000af705ec5217d0bf6c8 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Tue, 28 Oct 2025 13:19:31 -0400 Subject: [PATCH 25/46] working to fix nested disjunctions --- src/cuttingplanes.jl | 161 ++++++++++++++++++++---------- test/constraints/cuttingplanes.jl | 155 +++++++++++++++++++++++++++- test/runtests.jl | 37 +++---- test/solve.jl | 18 ++-- 4 files changed, 288 insertions(+), 83 deletions(-) diff --git a/src/cuttingplanes.jl b/src/cuttingplanes.jl index 79ebaa6..5ba9dfe 100644 --- a/src/cuttingplanes.jl +++ b/src/cuttingplanes.jl @@ -1,56 +1,71 @@ -#TODO: Right now there are Invalid Variable Refs. Its harmless but a point to polish at the end -#EXAMPLE OUTPUT: Dict{AbstractVariableRef, AbstractVariableRef}(InvalidVariableRef => x[1]_Y[2], InvalidVariableRef => x[2]_Y[1], x[2] => x[2], x[1] => x[1], InvalidVariableRef => x[2]_Y[2], Y[1] => Y[1], InvalidVariableRef => x[1]_Y[1], Y[2] => Y[2]) function reformulate_model( model::JuMP.AbstractModel, method::cutting_planes ) + var_type = JuMP.variable_ref_type(model) obj = objective_function(model) sense = objective_sense(model) - #Initializing SEP - SEP = JuMP.Model(method.optimizer) + #Seperation Model _reformulate_disjunctions(model, Hull()) + SEP = _copy_model(model) + JuMP.set_optimizer(SEP, method.optimizer) _reformulate_logical_constraints(model) main_to_SEP_map = _copy_variables_and_constraints(model, SEP, method) - #Initializing rBM - rBM = JuMP.Model(method.optimizer) - _reformulate_disjunctions(model, BigM(10e8)) + #Relaxed BigM model + _clear_reformulations(model) + _reformulate_disjunctions(model, BigM(method.M_value)) + _reformulate_logical_constraints(model) + rBM = _copy_model(model) + JuMP.set_optimizer(rBM, method.optimizer) main_to_rBM_map = _copy_variables_and_constraints(model, rBM, method) - JuMP.@objective(rBM, sense, _replace_variables_in_constraint(obj, main_to_rBM_map)) - rBM_to_SEP_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() - SEP_to_rBM_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() + JuMP.@objective(rBM, sense, + _replace_variables_in_constraint(obj, main_to_rBM_map) + ) + + rBM_to_SEP_map = Dict{var_type, var_type}() + SEP_to_rBM_map = Dict{var_type, var_type}() for (var, rBM_var) in main_to_rBM_map SEP_var = main_to_SEP_map[var] rBM_to_SEP_map[rBM_var] = SEP_var SEP_to_rBM_map[SEP_var] = rBM_var end - for i in 1:method.max_iter + i = 1 + sep_obj = 1 + while i <= method.max_iter && sep_obj > method.tolerance rBM_sol = _solve_rBM(rBM) + println(rBM) + SEP_sol = _solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) - _cutting_planes(model, rBM, main_to_rBM_map, main_to_SEP_map, rBM_sol, SEP_sol) + sep_obj = objective_value(SEP) + _cutting_planes(model, rBM, main_to_rBM_map, + main_to_SEP_map, rBM_sol, SEP_sol + ) + i += 1 end reformulate_model(model, method.final_reformulation) - _set_solution_method(model, method) - _set_ready_to_optimize(model, true) return end function _copy_variables_and_constraints( - model::JuMP.AbstractModel, - target_model::JuMP.AbstractModel, + model::M, + target_model::M, method::cutting_planes -) - var_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() +) where {M <: JuMP.AbstractModel} + T = JuMP.value_type(M) + var_type = JuMP.variable_ref_type(model) + + var_map = Dict{var_type, var_type}() for var in JuMP.all_variables(model) props = VariableProperties(var) if props.info.binary info = JuMP.VariableInfo( - true, 0.0, # has_lb, lower bound - true, 1.0, # has_ub, upper bound - false, 0.0, # has_fix, fix value - props.info.has_start, props.info.start, # preserve start - false, # is_binary = false - false # is_integer = false + true, zero(T), + true, one(T), + false, zero(T), + props.info.has_start, props.info.start, + false, + false ) props = VariableProperties( info, @@ -65,7 +80,9 @@ function _copy_variables_and_constraints( var_map[var] = new_var end end - constraints = JuMP.all_constraints(model; include_variable_in_set_constraints = false) + constraints = JuMP.all_constraints(model; + include_variable_in_set_constraints = false + ) for con in [JuMP.constraint_object(con) for con in constraints] expr = _replace_variables_in_constraint(con.func, var_map) JuMP.@constraint(target_model, expr * 1.0 in con.set) @@ -74,56 +91,94 @@ function _copy_variables_and_constraints( end function _solve_rBM( - rBM::JuMP.AbstractModel, -) + rBM::M, +) where {M <: JuMP.AbstractModel} + T = JuMP.value_type(M) JuMP.set_silent(rBM) optimize!(rBM) rBM_vars = JuMP.all_variables(rBM) - println(objective_value(rBM)) - solution_dict = Dict{JuMP.AbstractVariableRef, Float64}(var => 0.0 for var in rBM_vars) + sol = Dict{JuMP.AbstractVariableRef, T}(var => zero(T) for var in rBM_vars) for rBM_var in rBM_vars - solution_dict[rBM_var] = JuMP.value(rBM_var) + sol[rBM_var] = JuMP.value(rBM_var) end - return solution_dict + return sol end function _solve_SEP( - SEP::JuMP.AbstractModel, - rBM::JuMP.AbstractModel, - rBM_sol::Dict{JuMP.AbstractVariableRef, Float64}, - SEP_to_rBM_map::Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}, - rBM_to_SEP_map::Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef} -) + SEP::M, + rBM::M, + rBM_sol::Dict{<:JuMP.AbstractVariableRef, T}, + SEP_to_rBM_map::Dict{<:JuMP.AbstractVariableRef, <:JuMP.AbstractVariableRef}, + rBM_to_SEP_map::Dict{<:JuMP.AbstractVariableRef, <:JuMP.AbstractVariableRef} +) where {M <: JuMP.AbstractModel, T <: Number} JuMP.set_silent(SEP) SEP_vars = [rBM_to_SEP_map[rBM_var] for rBM_var in JuMP.all_variables(rBM)] - obj_expr = sum((SEP_var - rBM_sol[SEP_to_rBM_map[SEP_var]])^2 for SEP_var in SEP_vars) + obj_expr = sum( + (SEP_var - rBM_sol[SEP_to_rBM_map[SEP_var]])^2 for SEP_var in SEP_vars + ) JuMP.@objective(SEP, Min, obj_expr) + optimize!(SEP) - solution_dict = Dict{JuMP.AbstractVariableRef, Float64}(var => 0.0 for var in SEP_vars) + sol = Dict{JuMP.AbstractVariableRef, T}(var => zero(T) for var in SEP_vars) for SEP_var in SEP_vars - solution_dict[SEP_var] = JuMP.value(SEP_var) + sol[SEP_var] = JuMP.value(SEP_var) end - return solution_dict + return sol end function _cutting_planes( - model::JuMP.AbstractModel, - rBM::JuMP.AbstractModel, - main_to_rBM_map::Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}, - main_to_SEP_map::Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}, - rBM_sol::Dict{JuMP.AbstractVariableRef, Float64}, - SEP_sol::Dict{JuMP.AbstractVariableRef, Float64}, -) + model::M, + rBM::M, + main_to_rBM_map::Dict{<:JuMP.AbstractVariableRef, <:JuMP.AbstractVariableRef}, + main_to_SEP_map::Dict{<:JuMP.AbstractVariableRef, <:JuMP.AbstractVariableRef}, + rBM_sol::Dict{<:JuMP.AbstractVariableRef, T}, + SEP_sol::Dict{<:JuMP.AbstractVariableRef, T}, +) where {M <: JuMP.AbstractModel, T <: Number} main_vars = JuMP.all_variables(model) - ξ_sep = Dict{JuMP.AbstractVariableRef, Float64}(var => 0.0 for var in main_vars) + ξ_sep = Dict{JuMP.AbstractVariableRef, T}(var => zero(T) for var in main_vars) for var in main_vars ξ_sep[var] = 2*(SEP_sol[main_to_SEP_map[var]] - rBM_sol[main_to_rBM_map[var]]) end - main_cut_expr = JuMP.@expression(model, sum(ξ_sep[var]*(var - SEP_sol[main_to_SEP_map[var]]) for var in main_vars)) - rBM_cut_expr = _replace_variables_in_constraint(main_cut_expr, main_to_rBM_map) - JuMP.@constraint(model, main_cut_expr >= 0) - JuMP.@constraint(rBM, rBM_cut_expr >= 0) + main_cut = JuMP.@expression(model, + sum(ξ_sep[var]*(var - SEP_sol[main_to_SEP_map[var]]) for var in main_vars) + ) + rBM_cut = _replace_variables_in_constraint(main_cut, main_to_rBM_map) + JuMP.@constraint(model, main_cut >= 0.0) + JuMP.@constraint(rBM, rBM_cut >= 0.0) +end + +################################################################################ +# ERROR MESSAGES +################################################################################ + +function reformulate_model(model, method::cutting_planes) + error("reformulate_model not implemented for model type `$(typeof(model))`.") +end + +function _copy_variables_and_constraints(model, target_model, method) + error("_copy_variables_and_constraints not implemented for model types + `$(typeof(model))`, `$(typeof(target_model))`. + Both model types much match." + ) +end + +function _solve_rBM(model) + error("_solve_rBM not implemented for model type `$(typeof(model))`.") +end + +function _solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) + error("_solve_SEP not implemented for argument types:\n + SEP: `$(typeof(SEP))`, rBM: `$(typeof(rBM))`,\n + rBM_sol: `$(typeof(rBM_sol))`,\n + SEP_to_rBM_map: `$(typeof(SEP_to_rBM_map))`,\n + rBM_to_SEP_map: `$(typeof(rBM_to_SEP_map))`.") +end +function _cutting_planes(model, rBM, main_to_rBM_map, main_to_SEP_map, rBM_sol, SEP_sol) + error("_cutting_planes not implemented for argument types: \n + model: `$(typeof(model))`, rBM: `$(typeof(rBM))`,\n + main_to_rBM_map: `$(typeof(main_to_rBM_map))`, main_to_SEP_map: `$(typeof(main_to_SEP_map))`,\n + rBM_sol: `$(typeof(rBM_sol))`, SEP_sol: `$(typeof(SEP_sol))`.") end diff --git a/test/constraints/cuttingplanes.jl b/test/constraints/cuttingplanes.jl index 7e61a14..215ffae 100644 --- a/test/constraints/cuttingplanes.jl +++ b/test/constraints/cuttingplanes.jl @@ -1,14 +1,163 @@ -function test_reformulate_model() +using HiGHS + +function test_cutting_planes_datatype() + method = cutting_planes(HiGHS.Optimizer) + @test method.optimizer == HiGHS.Optimizer + @test method.max_iter == 3 + @test method.tolerance == 1e-6 + @test method.final_reformulation isa BigM + @test method.M_value == 1e9 + + method = cutting_planes(HiGHS.Optimizer, 10, 1e-4, Indicator(), 1e6) + @test method.max_iter == 10 + @test method.tolerance == 1e-4 + @test method.final_reformulation isa Indicator + @test method.M_value == 1e6 end function test_copy_variables_and_constraints() + model = JuMP.Model() + target_model = JuMP.Model() + method = cutting_planes(HiGHS.Optimizer) + @variable(model, x) + @variable(model, y, Bin) + + @constraint(model, x + y <= 1) + + var_map = DP._copy_variables_and_constraints(model, target_model, method) + @test length(var_map) == 2 + @test JuMP.is_binary(var_map[y]) == false + cons = JuMP.all_constraints(target_model; include_variable_in_set_constraints = false) + con = JuMP.constraint_object(cons[1]) + @test length(cons) == 1 + new_con = var_map[x] + var_map[y] + @test con.func == new_con + @test con.set == MOI.LessThan(1.0) + + @test_throws ErrorException DP._copy_variables_and_constraints(model, model, BigM()) end function test_solve_rBM() + rBM = JuMP.Model(HiGHS.Optimizer) + @variable(rBM, 0 <= x <= 100) + @variable(rBM, 0 <= y[1:2] <= 1) + @constraint(rBM, x <= 3 + 100(1 - y[1])) + @constraint(rBM, x <= 4 + 100(1 - y[2])) + @constraint(rBM, y[1] + y[2] == 1) + @objective(rBM, Max, x) + + solutions = DP._solve_rBM(rBM) + @test solutions[x] == 53.5 + @test solutions[y[1]] == 0.495 + @test solutions[y[2]] == 0.505 + + @test_throws ErrorException DP._solve_rBM(Dict()) end function test_solve_SEP() + model = GDPModel() + @variable(model, 0 <= x <= 100) + @variable(model, Y[1:2], Logical) + @constraint(model, x <= 3, Disjunct(Y[1])) + @constraint(model, x <= 4, Disjunct(Y[2])) + @disjunction(model, [Y[1], Y[2]]) + @objective(model, Max, x) + method = cutting_planes(HiGHS.Optimizer) + obj = objective_function(model) + sense = objective_sense(model) + DP._reformulate_disjunctions(model, Hull()) + SEP = DP._copy_model(model) + JuMP.set_optimizer(SEP, method.optimizer) + DP._reformulate_logical_constraints(model) + main_to_SEP_map = DP._copy_variables_and_constraints(model, SEP, method) + DP._clear_reformulations(model) + DP._reformulate_disjunctions(model, BigM(10e8)) + DP._reformulate_logical_constraints(model) + rBM = DP._copy_model(model) + JuMP.set_optimizer(rBM, method.optimizer) + main_to_rBM_map = DP._copy_variables_and_constraints(model, rBM, method) + JuMP.@objective(rBM, sense, DP._replace_variables_in_constraint(obj, main_to_rBM_map)) + rBM_to_SEP_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() + SEP_to_rBM_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() + for (var, rBM_var) in main_to_rBM_map + SEP_var = main_to_SEP_map[var] + rBM_to_SEP_map[rBM_var] = SEP_var + SEP_to_rBM_map[SEP_var] = rBM_var + end + rBM_sol = DP._solve_rBM(rBM) + SEP_sol = DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) + @test length(SEP_sol) == length(rBM_sol) + @test SEP_sol[rBM_to_SEP_map[main_to_rBM_map[x]]] ≈ 4.0 + + @test_throws ErrorException DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, "not a dict") end - function test_cutting_planes() -end \ No newline at end of file + model = GDPModel() + @variable(model, 0 <= x <= 100) + @variable(model, Y[1:2], Logical) + @constraint(model, x <= 3, Disjunct(Y[1])) + @constraint(model, x <= 4, Disjunct(Y[2])) + @disjunction(model, [Y[1], Y[2]]) + @objective(model, Max, x) + method = cutting_planes(HiGHS.Optimizer) + obj = objective_function(model) + sense = objective_sense(model) + DP._reformulate_disjunctions(model, Hull()) + SEP = DP._copy_model(model) + JuMP.set_optimizer(SEP, method.optimizer) + DP._reformulate_logical_constraints(model) + main_to_SEP_map = DP._copy_variables_and_constraints(model, SEP, method) + DP._clear_reformulations(model) + DP._reformulate_disjunctions(model, BigM(10e8)) + DP._reformulate_logical_constraints(model) + rBM = DP._copy_model(model) + JuMP.set_optimizer(rBM, method.optimizer) + main_to_rBM_map = DP._copy_variables_and_constraints(model, rBM, method) + JuMP.@objective(rBM, sense, DP._replace_variables_in_constraint(obj, main_to_rBM_map)) + rBM_to_SEP_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() + SEP_to_rBM_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() + for (var, rBM_var) in main_to_rBM_map + SEP_var = main_to_SEP_map[var] + rBM_to_SEP_map[rBM_var] = SEP_var + SEP_to_rBM_map[SEP_var] = rBM_var + end + rBM_sol = DP._solve_rBM(rBM) + SEP_sol = DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) + DP._cutting_planes(model, rBM, main_to_rBM_map, main_to_SEP_map, rBM_sol, SEP_sol) + + rBM_sol = DP._solve_rBM(rBM) + SEP_sol = DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) + + @test rBM_sol[main_to_rBM_map[x]] ≈ 4.0 + @test SEP_sol[rBM_to_SEP_map[main_to_rBM_map[x]]] ≈ 4.0 atol=1e-3 + + @test_throws ErrorException DP._cutting_planes(model, rBM, main_to_rBM_map, main_to_SEP_map, rBM_sol, "not a dict") +end + +function test_reformulate_model() + model = GDPModel() + @variable(model, 0 <= x[1:4] <= 100) + @variable(model, Y[1:2], Logical) + @constraint(model, x[1] + x[2] <= 3, Disjunct(Y[1])) + @constraint(model, x[3] + x[4] <= 4, Disjunct(Y[2])) + @disjunction(model, [Y[1], Y[2]]) + @objective(model, Max, x[1] + x[2]) + + method = cutting_planes(HiGHS.Optimizer) + DP.reformulate_model(model, method) + num_con = length( + JuMP.all_constraints(model; include_variable_in_set_constraints = false) + ) + @test num_con == 4 + @test_throws ErrorException DP.reformulate_model(42, method) +end + + +@testset "Cutting Planes" begin + test_cutting_planes_datatype() + test_copy_variables_and_constraints() + test_solve_rBM() + test_solve_SEP() + test_cutting_planes() + test_reformulate_model() +end diff --git a/test/runtests.jl b/test/runtests.jl index 843a95f..748871d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,21 +4,22 @@ 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("variables/creation.jl") -include("constraints/selector.jl") -include("constraints/proposition.jl") -include("constraints/disjunct.jl") -include("constraints/indicator.jl") -include("constraints/mbm.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") \ No newline at end of file +# include("aqua.jl") +# include("model.jl") +# include("jump.jl") +# include("variables/query.jl") +# include("variables/logical.jl") +# include("variables/creation.jl") +# include("constraints/selector.jl") +# include("constraints/proposition.jl") +# include("constraints/disjunct.jl") +# include("constraints/indicator.jl") +# include("constraints/mbm.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/cuttingplanes.jl") \ No newline at end of file diff --git a/test/solve.jl b/test/solve.jl index 69f72ad..41af54f 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -57,8 +57,8 @@ function test_linear_gdp_example(m, use_complements = false) @test optimize!(m, gdp_method = cutting_planes(HiGHS.Optimizer)) isa Nothing @test termination_status(m) == MOI.OPTIMAL - @test objective_value(m) ≈ 11 - @test value.(x) ≈ [9,2] + @test objective_value(m) ≈ 11 atol=1e-3 + @test value.(x) ≈ [9,2] atol=1e-3 @test !value(Y[1]) @test value(Y[2]) @test !value(W[1]) @@ -146,12 +146,12 @@ function test_generic_model(m) end @testset "Solve Linear GDP" begin - test_linear_gdp_example(GDPModel(HiGHS.Optimizer)) + # test_linear_gdp_example(GDPModel(HiGHS.Optimizer)) test_linear_gdp_example(GDPModel(HiGHS.Optimizer), true) - mockoptimizer = () -> MOI.Utilities.MockOptimizer( - MOI.Utilities.UniversalFallback(MOIU.Model{Float32}()), - eval_objective_value = false - ) - test_quadratic_gdp_example() - test_generic_model(GDPModel{Float32}(mockoptimizer)) + # mockoptimizer = () -> MOI.Utilities.MockOptimizer( + # MOI.Utilities.UniversalFallback(MOIU.Model{Float32}()), + # eval_objective_value = false + # ) + # test_quadratic_gdp_example() + # test_generic_model(GDPModel{Float32}(mockoptimizer)) end From 594eb5031119e4cc5a48176b628b97ef9ef556cc Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Tue, 28 Oct 2025 13:56:54 -0400 Subject: [PATCH 26/46] solve.jl works --- src/cuttingplanes.jl | 2 -- src/datatypes.jl | 5 +++-- test/solve.jl | 15 +++++++-------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/cuttingplanes.jl b/src/cuttingplanes.jl index 5ba9dfe..28bbbf7 100644 --- a/src/cuttingplanes.jl +++ b/src/cuttingplanes.jl @@ -34,8 +34,6 @@ function reformulate_model( sep_obj = 1 while i <= method.max_iter && sep_obj > method.tolerance rBM_sol = _solve_rBM(rBM) - println(rBM) - SEP_sol = _solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) sep_obj = objective_value(SEP) _cutting_planes(model, rBM, main_to_rBM_map, diff --git a/src/datatypes.jl b/src/datatypes.jl index ca06d45..78f4628 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -445,9 +445,10 @@ struct cutting_planes{O} <: AbstractReformulationMethod max_iter::Int tolerance::Float64 final_reformulation::AbstractReformulationMethod + M_value::Float64 # Positional constructor - function cutting_planes(optimizer::O, max_iter::Int = 3, tolerance::Float64 = 1e-6, final_reformulation = BigM()) where {O} - new{O}(optimizer, max_iter, tolerance, final_reformulation) + function cutting_planes(optimizer::O, max_iter::Int = 3, tolerance::Float64 = 1e-6, final_reformulation = BigM(), M_value::Float64 = 1e9) where {O} + new{O}(optimizer, max_iter, tolerance, final_reformulation, M_value) end end diff --git a/test/solve.jl b/test/solve.jl index 41af54f..ebfd7bd 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -44,7 +44,6 @@ function test_linear_gdp_example(m, use_complements = false) @test value(variable_by_name(m, "x[2]_Y[1]")) ≈ 0 @test value(variable_by_name(m, "x[2]_Y[2]")) ≈ 2 end - @test optimize!(m, gdp_method = MBM(HiGHS.Optimizer)) isa Nothing @test termination_status(m) == MOI.OPTIMAL @test objective_value(m) ≈ 11 @@ -146,12 +145,12 @@ function test_generic_model(m) end @testset "Solve Linear GDP" begin - # test_linear_gdp_example(GDPModel(HiGHS.Optimizer)) + test_linear_gdp_example(GDPModel(HiGHS.Optimizer)) test_linear_gdp_example(GDPModel(HiGHS.Optimizer), true) - # mockoptimizer = () -> MOI.Utilities.MockOptimizer( - # MOI.Utilities.UniversalFallback(MOIU.Model{Float32}()), - # eval_objective_value = false - # ) - # test_quadratic_gdp_example() - # test_generic_model(GDPModel{Float32}(mockoptimizer)) + mockoptimizer = () -> MOI.Utilities.MockOptimizer( + MOI.Utilities.UniversalFallback(MOIU.Model{Float32}()), + eval_objective_value = false + ) + test_quadratic_gdp_example() + test_generic_model(GDPModel{Float32}(mockoptimizer)) end From 3aa8b62d21f3928efb6673336da6f5bec46ead12 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Tue, 28 Oct 2025 14:32:17 -0400 Subject: [PATCH 27/46] 80 character limit --- src/cuttingplanes.jl | 32 ++++++++++++++------------- src/datatypes.jl | 16 ++++++++++---- test/constraints/cuttingplanes.jl | 2 +- test/runtests.jl | 36 +++++++++++++++---------------- 4 files changed, 48 insertions(+), 38 deletions(-) diff --git a/src/cuttingplanes.jl b/src/cuttingplanes.jl index 28bbbf7..9793205 100644 --- a/src/cuttingplanes.jl +++ b/src/cuttingplanes.jl @@ -151,32 +151,34 @@ end # ERROR MESSAGES ################################################################################ -function reformulate_model(model, method::cutting_planes) - error("reformulate_model not implemented for model type `$(typeof(model))`.") +function reformulate_model(::M, ::cutting_planes) where {M} + error("reformulate_model not implemented for model type `$(M)`.") end -function _copy_variables_and_constraints(model, target_model, method) +function _copy_variables_and_constraints(::M, ::N, ::cutting_planes) where {M, N} error("_copy_variables_and_constraints not implemented for model types - `$(typeof(model))`, `$(typeof(target_model))`. + `$(M)`, `$(N)`. Both model types much match." ) end -function _solve_rBM(model) - error("_solve_rBM not implemented for model type `$(typeof(model))`.") +function _solve_rBM(::M) where {M} + error("_solve_rBM not implemented for model type `$(M)`.") end -function _solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) +function _solve_SEP(::M, ::N, ::H, ::S, ::R) where {M, N, H, S, R} error("_solve_SEP not implemented for argument types:\n - SEP: `$(typeof(SEP))`, rBM: `$(typeof(rBM))`,\n - rBM_sol: `$(typeof(rBM_sol))`,\n - SEP_to_rBM_map: `$(typeof(SEP_to_rBM_map))`,\n - rBM_to_SEP_map: `$(typeof(rBM_to_SEP_map))`.") + SEP: `$(M)`, rBM: `$(N)`,\n + rBM_sol: `$(H)`,\n + SEP_to_rBM_map: `$(S)`,\n + rBM_to_SEP_map: `$(R)`.") end -function _cutting_planes(model, rBM, main_to_rBM_map, main_to_SEP_map, rBM_sol, SEP_sol) +function _cutting_planes(::M, ::N, ::H, ::S, ::R, ::T) where {M, N, H, S, R, T} error("_cutting_planes not implemented for argument types: \n - model: `$(typeof(model))`, rBM: `$(typeof(rBM))`,\n - main_to_rBM_map: `$(typeof(main_to_rBM_map))`, main_to_SEP_map: `$(typeof(main_to_SEP_map))`,\n - rBM_sol: `$(typeof(rBM_sol))`, SEP_sol: `$(typeof(SEP_sol))`.") + model: `$(M)`, rBM: `$(N)`,\n + main_to_rBM_map: `$(H)`, main_to_SEP_map: + `$(S)`,\n + rBM_sol: `$(R)`,\n + SEP_sol: `$(T)`.") end diff --git a/src/datatypes.jl b/src/datatypes.jl index 78f4628..d34d0bb 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -437,8 +437,12 @@ end A type for using the cutting planes approach for disjunctive constraints. **Fields** -- 'optimizer::O': Optimizer to use when solving mini-models (required). -- 'max_iter::Int': Number of iterations (default = `3`). +- `optimizer::O`: Optimizer to use when solving mini-models (required). +- `max_iter::Int`: Number of iterations (default = `3`). +- `tolerance::Float64`: Tolerance for the separation problem (default = `1e-6`). +- `final_reformulation::AbstractReformulationMethod`: Final reformulation +method to use after cutting planes (default = `BigM()`). +- `M_value::Float64`: Big-M value to use in the final reformulation (default = `1e9`). """ struct cutting_planes{O} <: AbstractReformulationMethod optimizer::O @@ -446,8 +450,12 @@ struct cutting_planes{O} <: AbstractReformulationMethod tolerance::Float64 final_reformulation::AbstractReformulationMethod M_value::Float64 - # Positional constructor - function cutting_planes(optimizer::O, max_iter::Int = 3, tolerance::Float64 = 1e-6, final_reformulation = BigM(), M_value::Float64 = 1e9) where {O} + function cutting_planes(optimizer::O, + max_iter::Int = 3, + tolerance::Float64 = 1e-6, + final_reformulation = BigM(), + M_value::Float64 = 1e9 + ) where {O} new{O}(optimizer, max_iter, tolerance, final_reformulation, M_value) end end diff --git a/test/constraints/cuttingplanes.jl b/test/constraints/cuttingplanes.jl index 215ffae..6de4634 100644 --- a/test/constraints/cuttingplanes.jl +++ b/test/constraints/cuttingplanes.jl @@ -34,7 +34,7 @@ function test_copy_variables_and_constraints() @test con.func == new_con @test con.set == MOI.LessThan(1.0) - @test_throws ErrorException DP._copy_variables_and_constraints(model, model, BigM()) + @test_throws ErrorException DP._copy_variables_and_constraints("model", model, cutting_planes(HiGHS.Optimizer)) end function test_solve_rBM() diff --git a/test/runtests.jl b/test/runtests.jl index 748871d..283cbb2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,22 +4,22 @@ 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("variables/creation.jl") -# include("constraints/selector.jl") -# include("constraints/proposition.jl") -# include("constraints/disjunct.jl") -# include("constraints/indicator.jl") -# include("constraints/mbm.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("aqua.jl") +include("model.jl") +include("jump.jl") +include("variables/query.jl") +include("variables/logical.jl") +include("variables/creation.jl") +include("constraints/selector.jl") +include("constraints/proposition.jl") +include("constraints/disjunct.jl") +include("constraints/indicator.jl") +include("constraints/mbm.jl") +include("constraints/bigm.jl") +include("constraints/psplit.jl") +include("constraints/cuttingplanes.jl") +include("constraints/hull.jl") +include("constraints/fallback.jl") +include("constraints/disjunction.jl") +include("print.jl") include("solve.jl") -# include("constraints/cuttingplanes.jl") \ No newline at end of file From 49b4fad72316412d29a4a1cb28dd189a2435061b Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Tue, 28 Oct 2025 15:24:39 -0400 Subject: [PATCH 28/46] . --- README.md | 9 +++++++++ src/cuttingplanes.jl | 1 + test/solve.jl | 3 ++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d68bae..3d3b452 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,15 @@ The following reformulation methods are currently supported: 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. +6. [Cutting Planes](https://pubsonline.informs.org/doi/10.1287/ijoc.2015.0669): This method iteratively generates cutting planes using a separation problem and a relaxed Big-M formulation, then applies a final reformulation method. The `cutting_planes` struct is created with the following arguments: + + - `optimizer`: Optimizer to use when solving the separation and relaxed Big-M subproblems. This is a required value. + - `max_iter`: Maximum number of cutting plane iterations. Default: `3`. + - `tolerance`: Convergence tolerance for the separation problem objective. Default: `1e-6`. + - `final_reformulation`: Reformulation method to apply after cutting plane iterations. Default: `BigM()`. + - `M_value`: Big-M value to use in the relaxed Big-M reformulation during iterations. Default: `1e9`. + + ## Release Notes diff --git a/src/cuttingplanes.jl b/src/cuttingplanes.jl index 9793205..1963d30 100644 --- a/src/cuttingplanes.jl +++ b/src/cuttingplanes.jl @@ -2,6 +2,7 @@ function reformulate_model( model::JuMP.AbstractModel, method::cutting_planes ) + _clear_reformulations(model) var_type = JuMP.variable_ref_type(model) obj = objective_function(model) sense = objective_sense(model) diff --git a/test/solve.jl b/test/solve.jl index ebfd7bd..e5ed80a 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -44,6 +44,7 @@ function test_linear_gdp_example(m, use_complements = false) @test value(variable_by_name(m, "x[2]_Y[1]")) ≈ 0 @test value(variable_by_name(m, "x[2]_Y[2]")) ≈ 2 end + @test optimize!(m, gdp_method = MBM(HiGHS.Optimizer)) isa Nothing @test termination_status(m) == MOI.OPTIMAL @test objective_value(m) ≈ 11 @@ -53,7 +54,6 @@ function test_linear_gdp_example(m, use_complements = false) @test !value(W[1]) @test !value(W[2]) - @test optimize!(m, gdp_method = cutting_planes(HiGHS.Optimizer)) isa Nothing @test termination_status(m) == MOI.OPTIMAL @test objective_value(m) ≈ 11 atol=1e-3 @@ -62,6 +62,7 @@ function test_linear_gdp_example(m, use_complements = false) @test value(Y[2]) @test !value(W[1]) @test !value(W[2]) + end function test_quadratic_gdp_example(use_complements = false) #psplit does not work with complements From 2a7cdd5d3142f9c37d9d9ad0bfc5050e828cf3e5 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Tue, 28 Oct 2025 16:19:11 -0400 Subject: [PATCH 29/46] removed old code from constraints.jl --- src/constraints.jl | 45 --------------------------------------------- 1 file changed, 45 deletions(-) diff --git a/src/constraints.jl b/src/constraints.jl index e3ffd90..a25270d 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -23,51 +23,6 @@ _vec_to_scalar_set(::_MOIExactly) = _MOI.EqualTo _vec_to_scalar_set(::_MOIAtLeast) = _MOI.GreaterThan _vec_to_scalar_set(::_MOIAtMost) = _MOI.LessThan -#helper function to create variables -# Helper function to copy variable properties from an existing variable -function _copy_variable( - target_model::JuMP.AbstractModel, - original_var::JuMP.AbstractVariableRef, - method::MBM - ) - new_var = JuMP.@variable(target_model, base_name = JuMP.name(original_var)) - - JuMP.has_lower_bound(original_var) && JuMP.set_lower_bound(new_var, JuMP.lower_bound(original_var)) - JuMP.has_upper_bound(original_var) && JuMP.set_upper_bound(new_var, JuMP.upper_bound(original_var)) - JuMP.has_start_value(original_var) && JuMP.set_start_value(new_var, JuMP.start_value(original_var)) - JuMP.is_integer(original_var) && JuMP.set_integer(new_var) - JuMP.is_binary(original_var) && JuMP.set_binary(new_var) - - if JuMP.is_fixed(original_var) - JuMP.fix(new_var, JuMP.fix_value(original_var); force=true) - end - - return new_var -end - -function _copy_variable( - target_model::JuMP.AbstractModel, - original_var::JuMP.AbstractVariableRef, - method::cutting_planes - ) - new_var = JuMP.@variable(target_model, base_name = JuMP.name(original_var)) - - JuMP.has_lower_bound(original_var) && JuMP.set_lower_bound(new_var, JuMP.lower_bound(original_var)) - JuMP.has_upper_bound(original_var) && JuMP.set_upper_bound(new_var, JuMP.upper_bound(original_var)) - JuMP.has_start_value(original_var) && JuMP.set_start_value(new_var, JuMP.start_value(original_var)) - #JuMP.is_integer(original_var) && JuMP.set_integer(new_var) - if JuMP.is_binary(original_var) - JuMP.set_lower_bound(new_var, 0.0) - JuMP.set_upper_bound(new_var, 1.0) - end - - if JuMP.is_fixed(original_var) - JuMP.fix(new_var, JuMP.fix_value(original_var); force=true) - end - - return new_var -end - ################################################################################ # BOILERPLATE EXTENSION METHODS ################################################################################ From af50c871f7923ac9bd22496e9301a5cf905f0d24 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Tue, 28 Oct 2025 16:20:30 -0400 Subject: [PATCH 30/46] removed spacing edits. --- src/reformulate.jl | 1 - test/solve.jl | 1 - 2 files changed, 2 deletions(-) diff --git a/src/reformulate.jl b/src/reformulate.jl index 2f004b6..9a3b65c 100644 --- a/src/reformulate.jl +++ b/src/reformulate.jl @@ -19,7 +19,6 @@ function reformulate_model(model::JuMP.AbstractModel, method::AbstractSolutionMe return end - function _clear_reformulations(model::JuMP.AbstractModel) delete.(model, _reformulation_constraints(model)) delete.(model, _reformulation_variables(model)) diff --git a/test/solve.jl b/test/solve.jl index e5ed80a..5b230db 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -122,7 +122,6 @@ function test_quadratic_gdp_example(use_complements = false) #psplit does not wo @test value(Y[2]) @test !value(W[1]) @test !value(W[2]) - end function test_generic_model(m) From b032a540cf42199b61f87493265f0983da2384ae Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Thu, 30 Oct 2025 13:21:31 -0400 Subject: [PATCH 31/46] . --- src/utilities.jl | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/utilities.jl b/src/utilities.jl index c3aa6f1..6e03964 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -6,4 +6,49 @@ function _copy_model( model::M ) where {M <: JuMP.AbstractModel} return M() -end \ No newline at end of file +end + +function JuMP.copy_extension_data( + data::GDPData{M, V, C, T}, + new_model::JuMP.AbstractModel, + old_model::JuMP.AbstractModel +) where {M, V, C, T} + return GDPData{M, V, C}() +end + +function copy_full_model( + model::M + ) where {M <: JuMP.AbstractModel} + new_model, ref_map = JuMP.copy_model(model) + + old_gdp = model.ext[:GDP] + new_gdp = new_model.ext[:GDP] + var_map = Dict(v => ref_map[v] for v in all_variables(model)) + con_map = Dict(con => ref_map[con] for (F, S) in list_of_constraint_types(model) for con in all_constraints(model, F, S)) + lv_map = Dict{LogicalVariableRef{M}, LogicalVariableRef{M}}() + lc_map = Dict{LogicalConstraintRef{M}, LogicalConstraintRef{M}}() + for (idx, var) in old_gdp.logical_variables + old_var_ref = LogicalVariableRef(model, idx) + new_var = JuMP.add_variable(new_model, var.variable, var.name) + lv_map[old_var_ref] = new_var + + end + + for (idx, con_data) in old_gdp.logical_constraints + println(con_data) + end + + return new_model, ref_map +end + +function _remap_LogicalExpr( + c::ScalarConstraint{_LogicalExpr{M}, S}, + lv_map::Dict{LogicalVariableRef{M}, LogicalVariableRef{M}} +) + + + + +end + + From 13a87cbe20f1972452adb330e752bb97cfdfe7c5 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Thu, 30 Oct 2025 14:39:05 -0400 Subject: [PATCH 32/46] . --- src/utilities.jl | 45 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/src/utilities.jl b/src/utilities.jl index 6e03964..61b0c6d 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -27,28 +27,57 @@ function copy_full_model( con_map = Dict(con => ref_map[con] for (F, S) in list_of_constraint_types(model) for con in all_constraints(model, F, S)) lv_map = Dict{LogicalVariableRef{M}, LogicalVariableRef{M}}() lc_map = Dict{LogicalConstraintRef{M}, LogicalConstraintRef{M}}() + for (idx, var) in old_gdp.logical_variables old_var_ref = LogicalVariableRef(model, idx) new_var = JuMP.add_variable(new_model, var.variable, var.name) lv_map[old_var_ref] = new_var - end for (idx, con_data) in old_gdp.logical_constraints - println(con_data) + old_con_ref = LogicalConstraintRef(model, idx) + new_con_ref = LogicalConstraintRef(new_model, idx) + c = con_data.constraint + println("###C####") + println(c.func) + expr = _remap_LogicalExpr(c.func, lv_map) + set = JuMP.moi_set(c) + new_con = JuMP.ScalarConstraint(expr, set) + JuMP.add_constraint(new_model, new_con, con_data.name) + lc_map[old_con_ref] = new_con_ref + end return new_model, ref_map end function _remap_LogicalExpr( - c::ScalarConstraint{_LogicalExpr{M}, S}, + expr::JuMP.AbstractJuMPScalar, lv_map::Dict{LogicalVariableRef{M}, LogicalVariableRef{M}} -) - - - - +) where {M <: JuMP.AbstractModel} + new_expr = _replace_variables_in_constraint(expr, lv_map) + return new_expr end +function _remap_LogicalExpr( + exprs::Vector{JuMP.AbstractJuMPScalar}, + lv_map::Dict{LogicalVariableRef{M}, LogicalVariableRef{M}} +) where {M <: JuMP.AbstractModel} + new_exprs = JuMP.AbstractJuMPScalar[] + for expr in exprs + new_expr = _replace_variables_in_constraint(expr, lv_map) + push!(new_exprs, new_expr) + end + return new_exprs +end +function _remap_LogicalExpr( + lvars::Vector{LogicalVariableRef{M}}, + lv_map::Dict{LogicalVariableRef{M}, LogicalVariableRef{M}} +) where {M <: JuMP.AbstractModel} + new_lvars = LogicalVariableRef{M}[] + for lvar in lvars + push!(new_lvars, lv_map[lvar]) + end + return new_lvars +end From efd644e496c563942fcc36326b4c9b8966fd03de Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Fri, 31 Oct 2025 15:32:31 -0400 Subject: [PATCH 33/46] working version with tests --- src/utilities.jl | 120 ++++++++++++++++++++++++++++++----------------- test/model.jl | 17 +++++++ test/solve.jl | 14 ++++++ 3 files changed, 109 insertions(+), 42 deletions(-) diff --git a/src/utilities.jl b/src/utilities.jl index 61b0c6d..a9ad137 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -16,68 +16,104 @@ function JuMP.copy_extension_data( return GDPData{M, V, C}() end -function copy_full_model( - model::M +function copy_gdp_data( + model::M, + new_model::M, + ref_map::GenericReferenceMap ) where {M <: JuMP.AbstractModel} - new_model, ref_map = JuMP.copy_model(model) old_gdp = model.ext[:GDP] new_gdp = new_model.ext[:GDP] var_map = Dict(v => ref_map[v] for v in all_variables(model)) - con_map = Dict(con => ref_map[con] for (F, S) in list_of_constraint_types(model) for con in all_constraints(model, F, S)) lv_map = Dict{LogicalVariableRef{M}, LogicalVariableRef{M}}() lc_map = Dict{LogicalConstraintRef{M}, LogicalConstraintRef{M}}() - + disj_map = Dict{DisjunctionRef{M}, DisjunctionRef{M}}() + disj_con_map = Dict{DisjunctConstraintRef{M}, DisjunctConstraintRef{M}}() for (idx, var) in old_gdp.logical_variables old_var_ref = LogicalVariableRef(model, idx) new_var = JuMP.add_variable(new_model, var.variable, var.name) lv_map[old_var_ref] = new_var end - for (idx, con_data) in old_gdp.logical_constraints + for (idx, lc_data) in old_gdp.logical_constraints old_con_ref = LogicalConstraintRef(model, idx) new_con_ref = LogicalConstraintRef(new_model, idx) - c = con_data.constraint - println("###C####") - println(c.func) - expr = _remap_LogicalExpr(c.func, lv_map) - set = JuMP.moi_set(c) - new_con = JuMP.ScalarConstraint(expr, set) - JuMP.add_constraint(new_model, new_con, con_data.name) + c = lc_data.constraint + expr = _replace_variables_in_constraint(c.func, lv_map) + new_con = JuMP.build_constraint(error, expr, c.set) + JuMP.add_constraint(new_model, new_con, lc_data.name) lc_map[old_con_ref] = new_con_ref - end - return new_model, ref_map -end + for (idx, disj_con_data) in old_gdp.disjunct_constraints + old_constraint = disj_con_data.constraint + old_dc_ref = DisjunctConstraintRef(model, idx) + old_indicator = old_gdp.constraint_to_indicator[old_dc_ref] + new_indicator = lv_map[old_indicator] + new_expr = _replace_variables_in_constraint(old_constraint.func, + var_map + ) + new_con = JuMP.build_constraint(error, new_expr, + old_constraint.set, Disjunct(new_indicator) + ) + new_dc_ref = JuMP.add_constraint(new_model, new_con, disj_con_data.name) + disj_con_map[old_dc_ref] = new_dc_ref + end -function _remap_LogicalExpr( - expr::JuMP.AbstractJuMPScalar, - lv_map::Dict{LogicalVariableRef{M}, LogicalVariableRef{M}} -) where {M <: JuMP.AbstractModel} - new_expr = _replace_variables_in_constraint(expr, lv_map) - return new_expr -end + for (idx, disj_data) in old_gdp.disjunctions + old_disj = disj_data.constraint + new_indicators = [_replace_variables_in_constraint(indicator, lv_map) + for indicator in old_disj.indicators + ] + new_disj = Disjunction(new_indicators, old_disj.nested) + disj_map[DisjunctionRef(model, idx)] = DisjunctionRef(new_model, idx) + new_gdp.disjunctions[idx] = ConstraintData(new_disj, disj_data.name) + end + + for (d_ref, lc_ref) in old_gdp.exactly1_constraints + new_lc_ref = lc_map[lc_ref] + new_d_ref = disj_map[d_ref] + new_gdp.exactly1_constraints[new_d_ref] = new_lc_ref + end -function _remap_LogicalExpr( - exprs::Vector{JuMP.AbstractJuMPScalar}, - lv_map::Dict{LogicalVariableRef{M}, LogicalVariableRef{M}} -) where {M <: JuMP.AbstractModel} - new_exprs = JuMP.AbstractJuMPScalar[] - for expr in exprs - new_expr = _replace_variables_in_constraint(expr, lv_map) - push!(new_exprs, new_expr) + for (lv_ref, bref) in old_gdp.indicator_to_binary + if bref isa JuMP.VariableRef + new_bref = var_map[bref] + elseif bref isa JuMP.GenericAffExpr + new_bref = _replace_variables_in_constraint(bref, var_map) + end + new_gdp.indicator_to_binary[lv_map[lv_ref]] = new_bref end - return new_exprs -end -function _remap_LogicalExpr( - lvars::Vector{LogicalVariableRef{M}}, - lv_map::Dict{LogicalVariableRef{M}, LogicalVariableRef{M}} -) where {M <: JuMP.AbstractModel} - new_lvars = LogicalVariableRef{M}[] - for lvar in lvars - push!(new_lvars, lv_map[lvar]) + for (lv_ref, con_refs) in old_gdp.indicator_to_constraints + new_lvar_ref = lv_map[lv_ref] + new_con_refs = Vector{Union{DisjunctConstraintRef{M}, DisjunctionRef{M}}}() + for con_ref in con_refs + new_con_ref = nothing + if con_ref isa DisjunctConstraintRef + new_con_ref = disj_con_map[con_ref] + elseif con_ref isa DisjunctionRef + new_con_ref = disj_map[con_ref] + end + push!(new_con_refs, new_con_ref) + end + new_gdp.indicator_to_constraints[new_lvar_ref] = new_con_refs end - return new_lvars -end + + for (con_ref, lv_ref) in old_gdp.constraint_to_indicator + if con_ref isa DisjunctConstraintRef + new_gdp.constraint_to_indicator[disj_con_map[con_ref]] = lv_map[lv_ref] + elseif con_ref isa DisjunctionRef + new_gdp.constraint_to_indicator[disj_map[con_ref]] = lv_map[lv_ref] + end + end + + for (v, bounds) in old_gdp.variable_bounds + new_gdp.variable_bounds[var_map[v]] = bounds + end + + new_gdp.solution_method = old_gdp.solution_method + new_gdp.ready_to_optimize = old_gdp.ready_to_optimize + + return lv_map +end \ No newline at end of file diff --git a/test/model.jl b/test/model.jl index 1cd2bff..c63059b 100644 --- a/test/model.jl +++ b/test/model.jl @@ -41,10 +41,27 @@ function test_set_optimizer() @test solver_name(model) == "HiGHS" end +function test_copy_model() + model = DP.GDPModel(HiGHS.Optimizer) + @variable(model, 0 ≤ x[1:2] ≤ 20) + @variable(model, Y[1:2], DP.Logical) + @constraint(model, [i = 1:2], [2,5][i] ≤ x[i] ≤ [6,9][i], DP.Disjunct(Y[1])) + @constraint(model, [i = 1:2], [8,10][i] ≤ x[i] ≤ [11,15][i], DP.Disjunct(Y[2])) + DP.@disjunction(model, Y) + DP._variable_bounds(model)[x[1]] = set_variable_bound_info(x[1], BigM()) + DP._variable_bounds(model)[x[2]] = set_variable_bound_info(x[2], BigM()) + + new_model, ref_map = JuMP.copy_model(model) + @test haskey(new_model.ext, :GDP) + lv_map = DP.copy_gdp_data(model, new_model, ref_map) + @test length(lv_map) == 2 +end + @testset "GDP Model" begin test_GDPData() test_empty_model() test_non_gdp_model() + test_copy_model() test_creation_optimizer() test_set_optimizer() end \ No newline at end of file diff --git a/test/solve.jl b/test/solve.jl index cfd24bd..bb503bd 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -53,6 +53,20 @@ function test_linear_gdp_example(m, use_complements = false) @test value(Y[2]) @test !value(W[1]) @test !value(W[2]) + + m_copy, ref_map = JuMP.copy_model(m) + lv_map = DP.copy_gdp_data(m, m_copy, ref_map) + set_optimizer(m_copy, HiGHS.Optimizer) + set_optimizer_attribute(m_copy, "output_flag", false) + optimize!(m_copy, gdp_method = BigM()) + @test termination_status(m_copy) == MOI.OPTIMAL + @test objective_value(m_copy) ≈ 11 + @test value.(x) == value.(ref_map[x]) + @test value.(Y[1]) == value.(lv_map[Y[1]]) + @test value.(Y[2]) == value.(lv_map[Y[2]]) + @test !value(W[1]) + @test !value(W[2]) + end function test_quadratic_gdp_example(use_complements = false) #psplit does not work with complements From 4c6fee4d0c70e333f647e9ff9fb16dd4f2e542fa Mon Sep 17 00:00:00 2001 From: dnguyen227 <82475321+dnguyen227@users.noreply.github.com> Date: Sun, 2 Nov 2025 17:29:00 -0500 Subject: [PATCH 34/46] Refactor variable handling and add model copying function Refactor variable creation and add copy_model_and_gdp_data function. --- src/utilities.jl | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/utilities.jl b/src/utilities.jl index a9ad137..43cf5b4 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -31,8 +31,10 @@ function copy_gdp_data( disj_con_map = Dict{DisjunctConstraintRef{M}, DisjunctConstraintRef{M}}() for (idx, var) in old_gdp.logical_variables old_var_ref = LogicalVariableRef(model, idx) - new_var = JuMP.add_variable(new_model, var.variable, var.name) + new_var_data = LogicalVariableData(var.variable, var.name) + new_var = LogicalVariableRef(new_model, idx) lv_map[old_var_ref] = new_var + new_gdp.logical_variables[idx] = new_var_data end for (idx, lc_data) in old_gdp.logical_constraints @@ -116,4 +118,10 @@ function copy_gdp_data( new_gdp.ready_to_optimize = old_gdp.ready_to_optimize return lv_map -end \ No newline at end of file +end + +function copy_model_and_gdp_data(model::M) where {M <: JuMP.AbstractModel} + new_model, ref_map = JuMP.copy_model(model) + lv_map = copy_gdp_data(model, new_model, ref_map) + return new_model, ref_map, lv_map +end From d801563342aeb9e950a44cfbbe825aed8c835dec Mon Sep 17 00:00:00 2001 From: dnguyen227 <82475321+dnguyen227@users.noreply.github.com> Date: Sun, 2 Nov 2025 17:33:05 -0500 Subject: [PATCH 35/46] Add tests for copy_model_and_gdp_data function --- test/model.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/model.jl b/test/model.jl index c63059b..e5358af 100644 --- a/test/model.jl +++ b/test/model.jl @@ -55,6 +55,9 @@ function test_copy_model() @test haskey(new_model.ext, :GDP) lv_map = DP.copy_gdp_data(model, new_model, ref_map) @test length(lv_map) == 2 + new_model1, ref_map1, lv_map1 = copy_model_and_gdp_data(model) + @test haskey(new_model1.ext, :GDP) + @test length(lv_map1) == 2 end @testset "GDP Model" begin @@ -64,4 +67,4 @@ end test_copy_model() test_creation_optimizer() test_set_optimizer() -end \ No newline at end of file +end From 9315da711aed4d44c76971c902886d10e484cb6a Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Sun, 2 Nov 2025 17:54:15 -0500 Subject: [PATCH 36/46] copy_model integratred into cutting planes. keyword arguements added --- src/cuttingplanes.jl | 85 ++++---------- src/datatypes.jl | 16 +-- src/utilities.jl | 10 +- test/constraints/cuttingplanes.jl | 186 +++++++++++++----------------- test/model.jl | 4 + test/solve.jl | 6 +- 6 files changed, 127 insertions(+), 180 deletions(-) diff --git a/src/cuttingplanes.jl b/src/cuttingplanes.jl index 1963d30..0ca5be2 100644 --- a/src/cuttingplanes.jl +++ b/src/cuttingplanes.jl @@ -7,21 +7,26 @@ function reformulate_model( obj = objective_function(model) sense = objective_sense(model) #Seperation Model - _reformulate_disjunctions(model, Hull()) - SEP = _copy_model(model) + SEP, sep_ref_map, _ = copy_model_and_gdp_data(model) + main_to_SEP_map = Dict(v => sep_ref_map[v] for v in all_variables(model)) + reformulate_model(SEP, Hull()) + rBM, rBM_ref_map, _ = copy_model_and_gdp_data(model) + reformulate_model(rBM, BigM(method.M_value)) + main_to_rBM_map = Dict(v => rBM_ref_map[v] for v in all_variables(model)) JuMP.set_optimizer(SEP, method.optimizer) - _reformulate_logical_constraints(model) - main_to_SEP_map = _copy_variables_and_constraints(model, SEP, method) - #Relaxed BigM model - _clear_reformulations(model) - _reformulate_disjunctions(model, BigM(method.M_value)) - _reformulate_logical_constraints(model) - rBM = _copy_model(model) JuMP.set_optimizer(rBM, method.optimizer) - main_to_rBM_map = _copy_variables_and_constraints(model, rBM, method) JuMP.@objective(rBM, sense, _replace_variables_in_constraint(obj, main_to_rBM_map) ) + for m in [SEP, rBM] + binary_vars = filter(is_binary, all_variables(m)) + for var in binary_vars + unset_binary(var) + set_lower_bound(var, 0.0) + set_upper_bound(var, 1.0) + end + end + rBM_to_SEP_map = Dict{var_type, var_type}() SEP_to_rBM_map = Dict{var_type, var_type}() @@ -30,10 +35,12 @@ function reformulate_model( rBM_to_SEP_map[rBM_var] = SEP_var SEP_to_rBM_map[SEP_var] = rBM_var end + + i = 1 sep_obj = 1 - while i <= method.max_iter && sep_obj > method.tolerance + while i <= method.max_iter && sep_obj > method.seperation_tolerance rBM_sol = _solve_rBM(rBM) SEP_sol = _solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) sep_obj = objective_value(SEP) @@ -42,59 +49,16 @@ function reformulate_model( ) i += 1 end - reformulate_model(model, method.final_reformulation) + reformulate_model(model, method.final_reform_method) return end -function _copy_variables_and_constraints( - model::M, - target_model::M, - method::cutting_planes -) where {M <: JuMP.AbstractModel} - T = JuMP.value_type(M) - var_type = JuMP.variable_ref_type(model) - - var_map = Dict{var_type, var_type}() - for var in JuMP.all_variables(model) - props = VariableProperties(var) - if props.info.binary - info = JuMP.VariableInfo( - true, zero(T), - true, one(T), - false, zero(T), - props.info.has_start, props.info.start, - false, - false - ) - props = VariableProperties( - info, - props.name, - nothing, # Clear any binary set - props.variable_type - ) - new_var = create_variable(target_model, props) - var_map[var] = new_var - else - new_var = variable_copy(target_model, var) - var_map[var] = new_var - end - end - constraints = JuMP.all_constraints(model; - include_variable_in_set_constraints = false - ) - for con in [JuMP.constraint_object(con) for con in constraints] - expr = _replace_variables_in_constraint(con.func, var_map) - JuMP.@constraint(target_model, expr * 1.0 in con.set) - end - return var_map -end - function _solve_rBM( rBM::M, ) where {M <: JuMP.AbstractModel} T = JuMP.value_type(M) JuMP.set_silent(rBM) - optimize!(rBM) + optimize!(rBM, ignore_optimize_hook = true) rBM_vars = JuMP.all_variables(rBM) sol = Dict{JuMP.AbstractVariableRef, T}(var => zero(T) for var in rBM_vars) for rBM_var in rBM_vars @@ -117,7 +81,7 @@ function _solve_SEP( ) JuMP.@objective(SEP, Min, obj_expr) - optimize!(SEP) + optimize!(SEP, ignore_optimize_hook = true) sol = Dict{JuMP.AbstractVariableRef, T}(var => zero(T) for var in SEP_vars) for SEP_var in SEP_vars @@ -156,13 +120,6 @@ function reformulate_model(::M, ::cutting_planes) where {M} error("reformulate_model not implemented for model type `$(M)`.") end -function _copy_variables_and_constraints(::M, ::N, ::cutting_planes) where {M, N} - error("_copy_variables_and_constraints not implemented for model types - `$(M)`, `$(N)`. - Both model types much match." - ) -end - function _solve_rBM(::M) where {M} error("_solve_rBM not implemented for model type `$(M)`.") end diff --git a/src/datatypes.jl b/src/datatypes.jl index d34d0bb..0e9a17a 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -439,24 +439,24 @@ A type for using the cutting planes approach for disjunctive constraints. **Fields** - `optimizer::O`: Optimizer to use when solving mini-models (required). - `max_iter::Int`: Number of iterations (default = `3`). -- `tolerance::Float64`: Tolerance for the separation problem (default = `1e-6`). -- `final_reformulation::AbstractReformulationMethod`: Final reformulation +- `seperation_tolerance::Float64`: Tolerance for the separation problem (default = `1e-6`). +- `final_reform_method::AbstractReformulationMethod`: Final reformulation method to use after cutting planes (default = `BigM()`). - `M_value::Float64`: Big-M value to use in the final reformulation (default = `1e9`). """ struct cutting_planes{O} <: AbstractReformulationMethod - optimizer::O + optimizer::O; max_iter::Int - tolerance::Float64 - final_reformulation::AbstractReformulationMethod + seperation_tolerance::Float64 + final_reform_method::AbstractReformulationMethod M_value::Float64 function cutting_planes(optimizer::O, max_iter::Int = 3, - tolerance::Float64 = 1e-6, - final_reformulation = BigM(), + seperation_tolerance::Float64 = 1e-6, + final_reform_method = BigM(), M_value::Float64 = 1e9 ) where {O} - new{O}(optimizer, max_iter, tolerance, final_reformulation, M_value) + new{O}(optimizer, max_iter, seperation_tolerance, final_reform_method, M_value) end end diff --git a/src/utilities.jl b/src/utilities.jl index a9ad137..9897cf3 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -31,8 +31,10 @@ function copy_gdp_data( disj_con_map = Dict{DisjunctConstraintRef{M}, DisjunctConstraintRef{M}}() for (idx, var) in old_gdp.logical_variables old_var_ref = LogicalVariableRef(model, idx) - new_var = JuMP.add_variable(new_model, var.variable, var.name) + new_var_data = LogicalVariableData(var.variable, var.name) + new_var = LogicalVariableRef(new_model, idx) lv_map[old_var_ref] = new_var + new_gdp.logical_variables[idx] = new_var_data end for (idx, lc_data) in old_gdp.logical_constraints @@ -116,4 +118,10 @@ function copy_gdp_data( new_gdp.ready_to_optimize = old_gdp.ready_to_optimize return lv_map +end + +function copy_model_and_gdp_data(model::M) where {M <: JuMP.AbstractModel} + new_model, ref_map = JuMP.copy_model(model) + lv_map = copy_gdp_data(model, new_model, ref_map) + return new_model, ref_map, lv_map end \ No newline at end of file diff --git a/test/constraints/cuttingplanes.jl b/test/constraints/cuttingplanes.jl index 6de4634..8509e5c 100644 --- a/test/constraints/cuttingplanes.jl +++ b/test/constraints/cuttingplanes.jl @@ -4,39 +4,17 @@ function test_cutting_planes_datatype() method = cutting_planes(HiGHS.Optimizer) @test method.optimizer == HiGHS.Optimizer @test method.max_iter == 3 - @test method.tolerance == 1e-6 - @test method.final_reformulation isa BigM + @test method.seperation_tolerance == 1e-6 + @test method.final_reform_method isa BigM @test method.M_value == 1e9 method = cutting_planes(HiGHS.Optimizer, 10, 1e-4, Indicator(), 1e6) @test method.max_iter == 10 - @test method.tolerance == 1e-4 - @test method.final_reformulation isa Indicator + @test method.seperation_tolerance == 1e-4 + @test method.final_reform_method isa Indicator @test method.M_value == 1e6 end -function test_copy_variables_and_constraints() - model = JuMP.Model() - target_model = JuMP.Model() - method = cutting_planes(HiGHS.Optimizer) - @variable(model, x) - @variable(model, y, Bin) - - @constraint(model, x + y <= 1) - - var_map = DP._copy_variables_and_constraints(model, target_model, method) - @test length(var_map) == 2 - @test JuMP.is_binary(var_map[y]) == false - cons = JuMP.all_constraints(target_model; include_variable_in_set_constraints = false) - con = JuMP.constraint_object(cons[1]) - @test length(cons) == 1 - new_con = var_map[x] + var_map[y] - @test con.func == new_con - @test con.set == MOI.LessThan(1.0) - - @test_throws ErrorException DP._copy_variables_and_constraints("model", model, cutting_planes(HiGHS.Optimizer)) -end - function test_solve_rBM() rBM = JuMP.Model(HiGHS.Optimizer) @variable(rBM, 0 <= x <= 100) @@ -54,85 +32,85 @@ function test_solve_rBM() @test_throws ErrorException DP._solve_rBM(Dict()) end -function test_solve_SEP() - model = GDPModel() - @variable(model, 0 <= x <= 100) - @variable(model, Y[1:2], Logical) - @constraint(model, x <= 3, Disjunct(Y[1])) - @constraint(model, x <= 4, Disjunct(Y[2])) - @disjunction(model, [Y[1], Y[2]]) - @objective(model, Max, x) - method = cutting_planes(HiGHS.Optimizer) - obj = objective_function(model) - sense = objective_sense(model) - DP._reformulate_disjunctions(model, Hull()) - SEP = DP._copy_model(model) - JuMP.set_optimizer(SEP, method.optimizer) - DP._reformulate_logical_constraints(model) - main_to_SEP_map = DP._copy_variables_and_constraints(model, SEP, method) - DP._clear_reformulations(model) - DP._reformulate_disjunctions(model, BigM(10e8)) - DP._reformulate_logical_constraints(model) - rBM = DP._copy_model(model) - JuMP.set_optimizer(rBM, method.optimizer) - main_to_rBM_map = DP._copy_variables_and_constraints(model, rBM, method) - JuMP.@objective(rBM, sense, DP._replace_variables_in_constraint(obj, main_to_rBM_map)) - rBM_to_SEP_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() - SEP_to_rBM_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() - for (var, rBM_var) in main_to_rBM_map - SEP_var = main_to_SEP_map[var] - rBM_to_SEP_map[rBM_var] = SEP_var - SEP_to_rBM_map[SEP_var] = rBM_var - end - rBM_sol = DP._solve_rBM(rBM) - SEP_sol = DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) - @test length(SEP_sol) == length(rBM_sol) - @test SEP_sol[rBM_to_SEP_map[main_to_rBM_map[x]]] ≈ 4.0 +# function test_solve_SEP() +# model = GDPModel() +# @variable(model, 0 <= x <= 100) +# @variable(model, Y[1:2], Logical) +# @constraint(model, x <= 3, Disjunct(Y[1])) +# @constraint(model, x <= 4, Disjunct(Y[2])) +# @disjunction(model, [Y[1], Y[2]]) +# @objective(model, Max, x) +# method = cutting_planes(HiGHS.Optimizer) +# obj = objective_function(model) +# sense = objective_sense(model) +# DP._reformulate_disjunctions(model, Hull()) +# SEP = DP._copy_model(model) +# JuMP.set_optimizer(SEP, method.optimizer) +# DP._reformulate_logical_constraints(model) +# main_to_SEP_map = DP._copy_variables_and_constraints(model, SEP, method) +# DP._clear_reformulations(model) +# DP._reformulate_disjunctions(model, BigM(10e8)) +# DP._reformulate_logical_constraints(model) +# rBM = DP._copy_model(model) +# JuMP.set_optimizer(rBM, method.optimizer) +# main_to_rBM_map = DP._copy_variables_and_constraints(model, rBM, method) +# JuMP.@objective(rBM, sense, DP._replace_variables_in_constraint(obj, main_to_rBM_map)) +# rBM_to_SEP_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() +# SEP_to_rBM_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() +# for (var, rBM_var) in main_to_rBM_map +# SEP_var = main_to_SEP_map[var] +# rBM_to_SEP_map[rBM_var] = SEP_var +# SEP_to_rBM_map[SEP_var] = rBM_var +# end +# rBM_sol = DP._solve_rBM(rBM) +# SEP_sol = DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) +# @test length(SEP_sol) == length(rBM_sol) +# @test SEP_sol[rBM_to_SEP_map[main_to_rBM_map[x]]] ≈ 4.0 - @test_throws ErrorException DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, "not a dict") -end -function test_cutting_planes() - model = GDPModel() - @variable(model, 0 <= x <= 100) - @variable(model, Y[1:2], Logical) - @constraint(model, x <= 3, Disjunct(Y[1])) - @constraint(model, x <= 4, Disjunct(Y[2])) - @disjunction(model, [Y[1], Y[2]]) - @objective(model, Max, x) - method = cutting_planes(HiGHS.Optimizer) - obj = objective_function(model) - sense = objective_sense(model) - DP._reformulate_disjunctions(model, Hull()) - SEP = DP._copy_model(model) - JuMP.set_optimizer(SEP, method.optimizer) - DP._reformulate_logical_constraints(model) - main_to_SEP_map = DP._copy_variables_and_constraints(model, SEP, method) - DP._clear_reformulations(model) - DP._reformulate_disjunctions(model, BigM(10e8)) - DP._reformulate_logical_constraints(model) - rBM = DP._copy_model(model) - JuMP.set_optimizer(rBM, method.optimizer) - main_to_rBM_map = DP._copy_variables_and_constraints(model, rBM, method) - JuMP.@objective(rBM, sense, DP._replace_variables_in_constraint(obj, main_to_rBM_map)) - rBM_to_SEP_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() - SEP_to_rBM_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() - for (var, rBM_var) in main_to_rBM_map - SEP_var = main_to_SEP_map[var] - rBM_to_SEP_map[rBM_var] = SEP_var - SEP_to_rBM_map[SEP_var] = rBM_var - end - rBM_sol = DP._solve_rBM(rBM) - SEP_sol = DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) - DP._cutting_planes(model, rBM, main_to_rBM_map, main_to_SEP_map, rBM_sol, SEP_sol) +# @test_throws ErrorException DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, "not a dict") +# end +# function test_cutting_planes() +# model = GDPModel() +# @variable(model, 0 <= x <= 100) +# @variable(model, Y[1:2], Logical) +# @constraint(model, x <= 3, Disjunct(Y[1])) +# @constraint(model, x <= 4, Disjunct(Y[2])) +# @disjunction(model, [Y[1], Y[2]]) +# @objective(model, Max, x) +# method = cutting_planes(HiGHS.Optimizer) +# obj = objective_function(model) +# sense = objective_sense(model) +# DP._reformulate_disjunctions(model, Hull()) +# SEP = DP._copy_model(model) +# JuMP.set_optimizer(SEP, method.optimizer) +# DP._reformulate_logical_constraints(model) +# main_to_SEP_map = DP._copy_variables_and_constraints(model, SEP, method) +# DP._clear_reformulations(model) +# DP._reformulate_disjunctions(model, BigM(10e8)) +# DP._reformulate_logical_constraints(model) +# rBM = DP._copy_model(model) +# JuMP.set_optimizer(rBM, method.optimizer) +# main_to_rBM_map = DP._copy_variables_and_constraints(model, rBM, method) +# JuMP.@objective(rBM, sense, DP._replace_variables_in_constraint(obj, main_to_rBM_map)) +# rBM_to_SEP_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() +# SEP_to_rBM_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() +# for (var, rBM_var) in main_to_rBM_map +# SEP_var = main_to_SEP_map[var] +# rBM_to_SEP_map[rBM_var] = SEP_var +# SEP_to_rBM_map[SEP_var] = rBM_var +# end +# rBM_sol = DP._solve_rBM(rBM) +# SEP_sol = DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) +# DP._cutting_planes(model, rBM, main_to_rBM_map, main_to_SEP_map, rBM_sol, SEP_sol) - rBM_sol = DP._solve_rBM(rBM) - SEP_sol = DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) +# rBM_sol = DP._solve_rBM(rBM) +# SEP_sol = DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) - @test rBM_sol[main_to_rBM_map[x]] ≈ 4.0 - @test SEP_sol[rBM_to_SEP_map[main_to_rBM_map[x]]] ≈ 4.0 atol=1e-3 +# @test rBM_sol[main_to_rBM_map[x]] ≈ 4.0 +# @test SEP_sol[rBM_to_SEP_map[main_to_rBM_map[x]]] ≈ 4.0 atol=1e-3 - @test_throws ErrorException DP._cutting_planes(model, rBM, main_to_rBM_map, main_to_SEP_map, rBM_sol, "not a dict") -end +# @test_throws ErrorException DP._cutting_planes(model, rBM, main_to_rBM_map, main_to_SEP_map, rBM_sol, "not a dict") +# end function test_reformulate_model() model = GDPModel() @@ -155,9 +133,9 @@ end @testset "Cutting Planes" begin test_cutting_planes_datatype() - test_copy_variables_and_constraints() + # test_copy_variables_and_constraints() test_solve_rBM() - test_solve_SEP() - test_cutting_planes() + # test_solve_SEP() + # test_cutting_planes() test_reformulate_model() end diff --git a/test/model.jl b/test/model.jl index c63059b..2773572 100644 --- a/test/model.jl +++ b/test/model.jl @@ -55,6 +55,10 @@ function test_copy_model() @test haskey(new_model.ext, :GDP) lv_map = DP.copy_gdp_data(model, new_model, ref_map) @test length(lv_map) == 2 + + new_model1, ref_map1, lv_map1 = copy_model_and_gdp_data(model) + @test haskey(new_model1.ext, :GDP) + @test length(lv_map1) == 2 end @testset "GDP Model" begin diff --git a/test/solve.jl b/test/solve.jl index 101c9c7..b8b8db3 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -70,9 +70,9 @@ function test_linear_gdp_example(m, use_complements = false) set_optimizer_attribute(m_copy, "output_flag", false) optimize!(m_copy, gdp_method = BigM()) @test termination_status(m_copy) == MOI.OPTIMAL - @test objective_value(m_copy) ≈ 11 - @test value.(x) == value.(ref_map[x]) - @test value.(Y[1]) == value.(lv_map[Y[1]]) + @test objective_value(m_copy) ≈ 11 atol=1e-3 + @test value.(x) ≈ value.(ref_map[x]) atol=1e-3 + @test value.(Y[1]) == value.(lv_map[Y[1]]) @test value.(Y[2]) == value.(lv_map[Y[2]]) @test !value(W[1]) @test !value(W[2]) From f148cacb80fdcf52235ecc0b3ce65b9d2b6a335d Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Mon, 3 Nov 2025 13:30:11 -0500 Subject: [PATCH 37/46] added remapping functions for specific gdpdata field. created more tests accordingly. addition of test to check that model instances do not affect each other. --- src/utilities.jl | 71 +++++++++++++++++++++++-------- test/model.jl | 107 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 160 insertions(+), 18 deletions(-) diff --git a/src/utilities.jl b/src/utilities.jl index 43cf5b4..fdaa924 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -79,11 +79,7 @@ function copy_gdp_data( end for (lv_ref, bref) in old_gdp.indicator_to_binary - if bref isa JuMP.VariableRef - new_bref = var_map[bref] - elseif bref isa JuMP.GenericAffExpr - new_bref = _replace_variables_in_constraint(bref, var_map) - end + new_bref = _remap_indicator_to_binary(bref, var_map) new_gdp.indicator_to_binary[lv_map[lv_ref]] = new_bref end @@ -91,23 +87,18 @@ function copy_gdp_data( new_lvar_ref = lv_map[lv_ref] new_con_refs = Vector{Union{DisjunctConstraintRef{M}, DisjunctionRef{M}}}() for con_ref in con_refs - new_con_ref = nothing - if con_ref isa DisjunctConstraintRef - new_con_ref = disj_con_map[con_ref] - elseif con_ref isa DisjunctionRef - new_con_ref = disj_map[con_ref] - end + new_con_ref = _remap_indicator_to_constraint(con_ref, + disj_con_map, disj_map + ) push!(new_con_refs, new_con_ref) end new_gdp.indicator_to_constraints[new_lvar_ref] = new_con_refs end for (con_ref, lv_ref) in old_gdp.constraint_to_indicator - if con_ref isa DisjunctConstraintRef - new_gdp.constraint_to_indicator[disj_con_map[con_ref]] = lv_map[lv_ref] - elseif con_ref isa DisjunctionRef - new_gdp.constraint_to_indicator[disj_map[con_ref]] = lv_map[lv_ref] - end + new_gdp.constraint_to_indicator[ + _remap_constraint_to_indicator(con_ref, disj_con_map, disj_map) + ] = lv_map[lv_ref] end for (v, bounds) in old_gdp.variable_bounds @@ -120,8 +111,54 @@ function copy_gdp_data( return lv_map end -function copy_model_and_gdp_data(model::M) where {M <: JuMP.AbstractModel} +function copy_gdp_model(model::M) where {M <: JuMP.AbstractModel} new_model, ref_map = JuMP.copy_model(model) lv_map = copy_gdp_data(model, new_model, ref_map) return new_model, ref_map, lv_map end + +function _remap_indicator_to_constraint( + con_ref::DisjunctConstraintRef, + disj_con_map::Dict{DisjunctConstraintRef{M}, DisjunctConstraintRef{M}}, + ::Dict{DisjunctionRef{M}, DisjunctionRef{M}} +) where {M <: JuMP.AbstractModel} + return disj_con_map[con_ref] +end + +function _remap_indicator_to_constraint( + con_ref::DisjunctionRef, + ::Dict{DisjunctConstraintRef{M}, DisjunctConstraintRef{M}}, + disj_map::Dict{DisjunctionRef{M}, DisjunctionRef{M}} +) where {M <: JuMP.AbstractModel} + return disj_map[con_ref] +end + +function _remap_indicator_to_binary( + bref::JuMP.AbstractVariableRef, + var_map::Dict{V, V} +) where {V <: JuMP.AbstractVariableRef} + return var_map[bref] +end + +function _remap_indicator_to_binary( + bref::JuMP.GenericAffExpr, + var_map::Dict{V, V} +) where {V <: JuMP.AbstractVariableRef} + return _replace_variables_in_constraint(bref, var_map) +end + +function _remap_constraint_to_indicator( + con_ref::DisjunctConstraintRef, + disj_con_map::Dict{DisjunctConstraintRef{M}, DisjunctConstraintRef{M}}, + ::Dict{DisjunctionRef{M}, DisjunctionRef{M}} +) where {M <: JuMP.AbstractModel} + return disj_con_map[con_ref] +end + +function _remap_constraint_to_indicator( + con_ref::DisjunctionRef, + ::Dict{DisjunctConstraintRef{M}, DisjunctConstraintRef{M}}, + disj_map::Dict{DisjunctionRef{M}, DisjunctionRef{M}} +) where {M <: JuMP.AbstractModel} + return disj_map[con_ref] +end diff --git a/test/model.jl b/test/model.jl index e5358af..769ec8a 100644 --- a/test/model.jl +++ b/test/model.jl @@ -41,6 +41,53 @@ function test_set_optimizer() @test solver_name(model) == "HiGHS" end +function test_remapping_functions() + model = GDPModel() + @variable(model, x) + @variable(model, y) + @variable(model, z) + + var_map = Dict{VariableRef, VariableRef}(x => y, z => x) + + @test DP._remap_indicator_to_binary(x, var_map) == y + @test DP._remap_indicator_to_binary(z, var_map) == x + + aff_expr = 2.0 * x + 3.0 * z + 5.0 + remapped_expr = DP._remap_indicator_to_binary(aff_expr, var_map) + @test remapped_expr isa JuMP.GenericAffExpr + @test remapped_expr.constant == 5.0 + @test remapped_expr.terms[y] == 2.0 # x remapped to y + @test remapped_expr.terms[x] == 3.0 # z remapped to x + @test !haskey(remapped_expr.terms, z) # z shouldn't exist anymore + + model2 = GDPModel() + @variable(model2, a[1:3]) + @variable(model2, Y[1:3], DP.Logical) + + con1 = @constraint(model2, a[1] ≤ 5, DP.Disjunct(Y[1])) + con2 = @constraint(model2, a[2] ≥ 2, DP.Disjunct(Y[2])) + con3 = @constraint(model2, a[3] == 4, DP.Disjunct(Y[3])) + + disj1 = DP.@disjunction(model2, Y[1:2]) + disj2 = DP.@disjunction(model2, Y[2:3]) + + disj_con_map = Dict{DisjunctConstraintRef{Model}, DisjunctConstraintRef{Model}}( + con1 => con2, + con2 => con3 + ) + disj_map = Dict{DisjunctionRef{Model}, DisjunctionRef{Model}}() + + @test DP._remap_constraint_to_indicator(con1, disj_con_map, disj_map) == con2 + @test DP._remap_constraint_to_indicator(con2, disj_con_map, disj_map) == con3 + + empty_disj_con_map = Dict{DisjunctConstraintRef{Model}, DisjunctConstraintRef{Model}}() + disj_map2 = Dict{DisjunctionRef{Model}, DisjunctionRef{Model}}( + disj1 => disj2 + ) + + @test DP._remap_constraint_to_indicator(disj1, empty_disj_con_map, disj_map2) == disj2 +end + function test_copy_model() model = DP.GDPModel(HiGHS.Optimizer) @variable(model, 0 ≤ x[1:2] ≤ 20) @@ -55,9 +102,66 @@ function test_copy_model() @test haskey(new_model.ext, :GDP) lv_map = DP.copy_gdp_data(model, new_model, ref_map) @test length(lv_map) == 2 - new_model1, ref_map1, lv_map1 = copy_model_and_gdp_data(model) + new_model1, ref_map1, lv_map1 = DP.copy_gdp_model(model) @test haskey(new_model1.ext, :GDP) @test length(lv_map1) == 2 + + orig_num_vars = num_variables(model) + orig_num_constrs = num_constraints(model; + count_variable_in_set_constraints = false + ) + orig1_num_vars = num_variables(new_model1) + orig1_num_constrs = num_constraints(new_model1; + count_variable_in_set_constraints = false + ) + + @variable(new_model, z >= 0) + @constraint(new_model, z <= 100) + + @test num_variables(model) == orig_num_vars + num_con_m = num_constraints(model; + count_variable_in_set_constraints = false) + num_con_m1 = num_constraints(new_model1; + count_variable_in_set_constraints = false) + @test num_con_m == orig_num_constrs + @test num_con_m1 == orig1_num_constrs + @test !haskey(object_dictionary(model), :z) + + @test num_variables(new_model1) == orig1_num_vars + + @test !haskey(object_dictionary(new_model1), :z) + + @test num_variables(new_model) == orig_num_vars + 1 + num_con_m2 = num_constraints(new_model; + count_variable_in_set_constraints = false) + @test num_con_m2 == orig_num_constrs + 1 + + + orig_num_lvars = length(DP._logical_variables(model)) + orig_num_disj_cons = length(DP._disjunct_constraints(model)) + orig_num_disj = length(DP._disjunctions(model)) + orig1_num_lvars = length(DP._logical_variables(new_model1)) + orig1_num_disj_cons = length(DP._disjunct_constraints(new_model1)) + orig1_num_disj = length(DP._disjunctions(new_model1)) + + @variable(new_model, W[1:2], DP.Logical) + @constraint(new_model, z >= 5, DP.Disjunct(W[1])) + @constraint(new_model, z <= 3, DP.Disjunct(W[2])) + DP.@disjunction(new_model, W) + + @test length(DP._logical_variables(model)) == orig_num_lvars + @test length(DP._disjunct_constraints(model)) == orig_num_disj_cons + @test length(DP._disjunctions(model)) == orig_num_disj + @test !haskey(object_dictionary(model), :W) + + @test length(DP._logical_variables(new_model1)) == orig1_num_lvars + @test length(DP._disjunct_constraints(new_model1)) == orig1_num_disj_cons + @test length(DP._disjunctions(new_model1)) == orig1_num_disj + @test !haskey(object_dictionary(new_model1), :W) + + @test length(DP._logical_variables(new_model)) == orig_num_lvars + 2 + @test length(DP._disjunct_constraints(new_model)) == orig_num_disj_cons + 2 + @test length(DP._disjunctions(new_model)) == orig_num_disj + 1 end @testset "GDP Model" begin @@ -67,4 +171,5 @@ end test_copy_model() test_creation_optimizer() test_set_optimizer() + test_remapping_functions() end From aab3ec41bc3b80919511a71c8a0b21695c88aacc Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Mon, 3 Nov 2025 13:38:38 -0500 Subject: [PATCH 38/46] added solving test in modeljl for copy_model --- test/model.jl | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/model.jl b/test/model.jl index 769ec8a..21e7709 100644 --- a/test/model.jl +++ b/test/model.jl @@ -154,9 +154,32 @@ function test_copy_model() @test length(DP._disjunctions(model)) == orig_num_disj @test !haskey(object_dictionary(model), :W) + # Store original reformulation methods + orig_methods = Dict( + :model => [DP._solution_method(model)], + :new_model1 => [DP._solution_method(new_model1)] + ) + + # Solve new_model with Big-M reformulation + set_optimizer(new_model, HiGHS.Optimizer) + set_silent(new_model) + DP.optimize!(new_model, gdp_method = BigM()) + @test termination_status(new_model) in (MOI.OPTIMAL, MOI.LOCALLY_SOLVED) + + # Verify reformulation methods of other models are unchanged + @test DP._solution_method(model) == orig_methods[:model][] + @test DP._solution_method(new_model1) == orig_methods[:new_model1][] + + # Verify model and new_model1 were not modified by the solve + @test num_variables(model) == orig_num_vars + @test num_variables(new_model1) == orig1_num_vars + @test length(DP._logical_variables(model)) == orig_num_lvars @test length(DP._logical_variables(new_model1)) == orig1_num_lvars + @test length(DP._disjunct_constraints(model)) == orig_num_disj_cons @test length(DP._disjunct_constraints(new_model1)) == orig1_num_disj_cons + @test length(DP._disjunctions(model)) == orig_num_disj @test length(DP._disjunctions(new_model1)) == orig1_num_disj + @test !haskey(object_dictionary(new_model1), :W) @test length(DP._logical_variables(new_model)) == orig_num_lvars + 2 From 90412484d95a194ab4e5df22c7298e2f6a931701 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Tue, 4 Nov 2025 11:20:34 -0500 Subject: [PATCH 39/46] Updated tests. --- src/cuttingplanes.jl | 12 +-- test/constraints/cuttingplanes.jl | 172 ++++++++++++++++-------------- 2 files changed, 98 insertions(+), 86 deletions(-) diff --git a/src/cuttingplanes.jl b/src/cuttingplanes.jl index 0ca5be2..fed2664 100644 --- a/src/cuttingplanes.jl +++ b/src/cuttingplanes.jl @@ -6,12 +6,11 @@ function reformulate_model( var_type = JuMP.variable_ref_type(model) obj = objective_function(model) sense = objective_sense(model) - #Seperation Model - SEP, sep_ref_map, _ = copy_model_and_gdp_data(model) - main_to_SEP_map = Dict(v => sep_ref_map[v] for v in all_variables(model)) - reformulate_model(SEP, Hull()) - rBM, rBM_ref_map, _ = copy_model_and_gdp_data(model) + SEP, sep_ref_map, _ = copy_gdp_model(model) + rBM, rBM_ref_map, _ = copy_gdp_model(model) reformulate_model(rBM, BigM(method.M_value)) + reformulate_model(SEP, Hull()) + main_to_SEP_map = Dict(v => sep_ref_map[v] for v in all_variables(model)) main_to_rBM_map = Dict(v => rBM_ref_map[v] for v in all_variables(model)) JuMP.set_optimizer(SEP, method.optimizer) JuMP.set_optimizer(rBM, method.optimizer) @@ -27,7 +26,6 @@ function reformulate_model( end end - rBM_to_SEP_map = Dict{var_type, var_type}() SEP_to_rBM_map = Dict{var_type, var_type}() for (var, rBM_var) in main_to_rBM_map @@ -36,8 +34,6 @@ function reformulate_model( SEP_to_rBM_map[SEP_var] = rBM_var end - - i = 1 sep_obj = 1 while i <= method.max_iter && sep_obj > method.seperation_tolerance diff --git a/test/constraints/cuttingplanes.jl b/test/constraints/cuttingplanes.jl index 8509e5c..798ac95 100644 --- a/test/constraints/cuttingplanes.jl +++ b/test/constraints/cuttingplanes.jl @@ -32,85 +32,102 @@ function test_solve_rBM() @test_throws ErrorException DP._solve_rBM(Dict()) end -# function test_solve_SEP() -# model = GDPModel() -# @variable(model, 0 <= x <= 100) -# @variable(model, Y[1:2], Logical) -# @constraint(model, x <= 3, Disjunct(Y[1])) -# @constraint(model, x <= 4, Disjunct(Y[2])) -# @disjunction(model, [Y[1], Y[2]]) -# @objective(model, Max, x) -# method = cutting_planes(HiGHS.Optimizer) -# obj = objective_function(model) -# sense = objective_sense(model) -# DP._reformulate_disjunctions(model, Hull()) -# SEP = DP._copy_model(model) -# JuMP.set_optimizer(SEP, method.optimizer) -# DP._reformulate_logical_constraints(model) -# main_to_SEP_map = DP._copy_variables_and_constraints(model, SEP, method) -# DP._clear_reformulations(model) -# DP._reformulate_disjunctions(model, BigM(10e8)) -# DP._reformulate_logical_constraints(model) -# rBM = DP._copy_model(model) -# JuMP.set_optimizer(rBM, method.optimizer) -# main_to_rBM_map = DP._copy_variables_and_constraints(model, rBM, method) -# JuMP.@objective(rBM, sense, DP._replace_variables_in_constraint(obj, main_to_rBM_map)) -# rBM_to_SEP_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() -# SEP_to_rBM_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() -# for (var, rBM_var) in main_to_rBM_map -# SEP_var = main_to_SEP_map[var] -# rBM_to_SEP_map[rBM_var] = SEP_var -# SEP_to_rBM_map[SEP_var] = rBM_var -# end -# rBM_sol = DP._solve_rBM(rBM) -# SEP_sol = DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) -# @test length(SEP_sol) == length(rBM_sol) -# @test SEP_sol[rBM_to_SEP_map[main_to_rBM_map[x]]] ≈ 4.0 +function test_solve_SEP() + model = GDPModel() + @variable(model, 0 <= x <= 100) + @variable(model, Y[1:2], Logical) + @constraint(model, x <= 3, Disjunct(Y[1])) + @constraint(model, x <= 4, Disjunct(Y[2])) + @disjunction(model, [Y[1], Y[2]]) + @objective(model, Max, x) + var_type = JuMP.variable_ref_type(model) + method = cutting_planes(HiGHS.Optimizer) + obj = objective_function(model) + sense = objective_sense(model) + SEP, sep_ref_map, _ = DP.copy_gdp_model(model) + rBM, rBM_ref_map, _ = DP.copy_gdp_model(model) + DP.reformulate_model(rBM, DP.BigM(method.M_value)) + DP.reformulate_model(SEP, DP.Hull()) + main_to_SEP_map = Dict(v => sep_ref_map[v] for v in all_variables(model)) + main_to_rBM_map = Dict(v => rBM_ref_map[v] for v in all_variables(model)) + JuMP.set_optimizer(SEP, method.optimizer) + JuMP.set_optimizer(rBM, method.optimizer) + JuMP.@objective(rBM, sense, + DP._replace_variables_in_constraint(obj, main_to_rBM_map) + ) + for m in [SEP, rBM] + binary_vars = filter(is_binary, all_variables(m)) + for var in binary_vars + unset_binary(var) + set_lower_bound(var, 0.0) + set_upper_bound(var, 1.0) + end + end + rBM_to_SEP_map = Dict{var_type, var_type}() + SEP_to_rBM_map = Dict{var_type, var_type}() + for (var, rBM_var) in main_to_rBM_map + SEP_var = main_to_SEP_map[var] + rBM_to_SEP_map[rBM_var] = SEP_var + SEP_to_rBM_map[SEP_var] = rBM_var + end + rBM_sol = DP._solve_rBM(rBM) + SEP_sol = DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) + @test length(SEP_sol) == length(rBM_sol) + @test SEP_sol[rBM_to_SEP_map[main_to_rBM_map[x]]] ≈ 4.0 -# @test_throws ErrorException DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, "not a dict") -# end -# function test_cutting_planes() -# model = GDPModel() -# @variable(model, 0 <= x <= 100) -# @variable(model, Y[1:2], Logical) -# @constraint(model, x <= 3, Disjunct(Y[1])) -# @constraint(model, x <= 4, Disjunct(Y[2])) -# @disjunction(model, [Y[1], Y[2]]) -# @objective(model, Max, x) -# method = cutting_planes(HiGHS.Optimizer) -# obj = objective_function(model) -# sense = objective_sense(model) -# DP._reformulate_disjunctions(model, Hull()) -# SEP = DP._copy_model(model) -# JuMP.set_optimizer(SEP, method.optimizer) -# DP._reformulate_logical_constraints(model) -# main_to_SEP_map = DP._copy_variables_and_constraints(model, SEP, method) -# DP._clear_reformulations(model) -# DP._reformulate_disjunctions(model, BigM(10e8)) -# DP._reformulate_logical_constraints(model) -# rBM = DP._copy_model(model) -# JuMP.set_optimizer(rBM, method.optimizer) -# main_to_rBM_map = DP._copy_variables_and_constraints(model, rBM, method) -# JuMP.@objective(rBM, sense, DP._replace_variables_in_constraint(obj, main_to_rBM_map)) -# rBM_to_SEP_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() -# SEP_to_rBM_map = Dict{JuMP.AbstractVariableRef, JuMP.AbstractVariableRef}() -# for (var, rBM_var) in main_to_rBM_map -# SEP_var = main_to_SEP_map[var] -# rBM_to_SEP_map[rBM_var] = SEP_var -# SEP_to_rBM_map[SEP_var] = rBM_var -# end -# rBM_sol = DP._solve_rBM(rBM) -# SEP_sol = DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) -# DP._cutting_planes(model, rBM, main_to_rBM_map, main_to_SEP_map, rBM_sol, SEP_sol) + @test_throws ErrorException DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, "not a dict") +end -# rBM_sol = DP._solve_rBM(rBM) -# SEP_sol = DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) +function test_cutting_planes() + model = GDPModel() + @variable(model, 0 <= x <= 100) + @variable(model, Y[1:2], Logical) + @constraint(model, x <= 3, Disjunct(Y[1])) + @constraint(model, x <= 4, Disjunct(Y[2])) + @disjunction(model, [Y[1], Y[2]]) + @objective(model, Max, x) + var_type = JuMP.variable_ref_type(model) + method = cutting_planes(HiGHS.Optimizer) + obj = objective_function(model) + sense = objective_sense(model) + SEP, sep_ref_map, _ = DP.copy_gdp_model(model) + rBM, rBM_ref_map, _ = DP.copy_gdp_model(model) + DP.reformulate_model(rBM, DP.BigM(method.M_value)) + DP.reformulate_model(SEP, DP.Hull()) + main_to_SEP_map = Dict(v => sep_ref_map[v] for v in all_variables(model)) + main_to_rBM_map = Dict(v => rBM_ref_map[v] for v in all_variables(model)) + JuMP.set_optimizer(SEP, method.optimizer) + JuMP.set_optimizer(rBM, method.optimizer) + JuMP.@objective(rBM, sense, + DP._replace_variables_in_constraint(obj, main_to_rBM_map) + ) + for m in [SEP, rBM] + binary_vars = filter(is_binary, all_variables(m)) + for var in binary_vars + unset_binary(var) + set_lower_bound(var, 0.0) + set_upper_bound(var, 1.0) + end + end + rBM_to_SEP_map = Dict{var_type, var_type}() + SEP_to_rBM_map = Dict{var_type, var_type}() + for (var, rBM_var) in main_to_rBM_map + SEP_var = main_to_SEP_map[var] + rBM_to_SEP_map[rBM_var] = SEP_var + SEP_to_rBM_map[SEP_var] = rBM_var + end + rBM_sol = DP._solve_rBM(rBM) + SEP_sol = DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) + DP._cutting_planes(model, rBM, main_to_rBM_map, main_to_SEP_map, rBM_sol, SEP_sol) + + rBM_sol = DP._solve_rBM(rBM) + SEP_sol = DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) -# @test rBM_sol[main_to_rBM_map[x]] ≈ 4.0 -# @test SEP_sol[rBM_to_SEP_map[main_to_rBM_map[x]]] ≈ 4.0 atol=1e-3 + @test rBM_sol[main_to_rBM_map[x]] ≈ 4.0 + @test SEP_sol[rBM_to_SEP_map[main_to_rBM_map[x]]] ≈ 4.0 atol=1e-3 -# @test_throws ErrorException DP._cutting_planes(model, rBM, main_to_rBM_map, main_to_SEP_map, rBM_sol, "not a dict") -# end + @test_throws ErrorException DP._cutting_planes(model, rBM, main_to_rBM_map, main_to_SEP_map, rBM_sol, "not a dict") +end function test_reformulate_model() model = GDPModel() @@ -133,9 +150,8 @@ end @testset "Cutting Planes" begin test_cutting_planes_datatype() - # test_copy_variables_and_constraints() test_solve_rBM() - # test_solve_SEP() - # test_cutting_planes() + test_solve_SEP() + test_cutting_planes() test_reformulate_model() end From 192b66a149553ccae21efc04cb989f5a84be7e7f Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Tue, 4 Nov 2025 11:29:37 -0500 Subject: [PATCH 40/46] update tests --- test/constraints/cuttingplanes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/constraints/cuttingplanes.jl b/test/constraints/cuttingplanes.jl index 798ac95..116213f 100644 --- a/test/constraints/cuttingplanes.jl +++ b/test/constraints/cuttingplanes.jl @@ -8,7 +8,7 @@ function test_cutting_planes_datatype() @test method.final_reform_method isa BigM @test method.M_value == 1e9 - method = cutting_planes(HiGHS.Optimizer, 10, 1e-4, Indicator(), 1e6) + method = cutting_planes(HiGHS.Optimizer, 10; seperation_tolerance=1e-4, final_reform_method=Indicator(), M_value=1e6) @test method.max_iter == 10 @test method.seperation_tolerance == 1e-4 @test method.final_reform_method isa Indicator From 1f6c3016f3812371bad271d6bb963f7c1b0385e9 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Tue, 4 Nov 2025 11:30:41 -0500 Subject: [PATCH 41/46] modified tests to check keyword arguments --- test/constraints/cuttingplanes.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/constraints/cuttingplanes.jl b/test/constraints/cuttingplanes.jl index 116213f..5026a8a 100644 --- a/test/constraints/cuttingplanes.jl +++ b/test/constraints/cuttingplanes.jl @@ -8,7 +8,9 @@ function test_cutting_planes_datatype() @test method.final_reform_method isa BigM @test method.M_value == 1e9 - method = cutting_planes(HiGHS.Optimizer, 10; seperation_tolerance=1e-4, final_reform_method=Indicator(), M_value=1e6) + method = cutting_planes(HiGHS.Optimizer;max_iter=10, + seperation_tolerance=1e-4, final_reform_method=Indicator(), M_value=1e6 + ) @test method.max_iter == 10 @test method.seperation_tolerance == 1e-4 @test method.final_reform_method isa Indicator From 76d08b44b76a13eeb1cbfd6b10c7e25610b2cebf Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Tue, 4 Nov 2025 11:42:17 -0500 Subject: [PATCH 42/46] updated cutting_planes function with keyword arguements --- src/datatypes.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/datatypes.jl b/src/datatypes.jl index 0e9a17a..3a5f09d 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -450,7 +450,8 @@ struct cutting_planes{O} <: AbstractReformulationMethod seperation_tolerance::Float64 final_reform_method::AbstractReformulationMethod M_value::Float64 - function cutting_planes(optimizer::O, + function cutting_planes( + optimizer::O; max_iter::Int = 3, seperation_tolerance::Float64 = 1e-6, final_reform_method = BigM(), From 6a6cd706964ab98bca3d81bb3850c145ed6c354a Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Tue, 4 Nov 2025 11:42:48 -0500 Subject: [PATCH 43/46] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3d3b452..f64fa6b 100644 --- a/README.md +++ b/README.md @@ -188,8 +188,8 @@ The following reformulation methods are currently supported: - `optimizer`: Optimizer to use when solving the separation and relaxed Big-M subproblems. This is a required value. - `max_iter`: Maximum number of cutting plane iterations. Default: `3`. - - `tolerance`: Convergence tolerance for the separation problem objective. Default: `1e-6`. - - `final_reformulation`: Reformulation method to apply after cutting plane iterations. Default: `BigM()`. + - `seperation_tolerance`: Convergence tolerance for the separation problem objective. Default: `1e-6`. + - `final_reform_method`: Reformulation method to apply after cutting plane iterations. Default: `BigM()`. - `M_value`: Big-M value to use in the relaxed Big-M reformulation during iterations. Default: `1e9`. From a1e92b535b0de05a270d6a113f317024354111e3 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Wed, 5 Nov 2025 12:54:59 -0500 Subject: [PATCH 44/46] additional comments. unified spacing. --- src/cuttingplanes.jl | 62 +++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/src/cuttingplanes.jl b/src/cuttingplanes.jl index fed2664..433334f 100644 --- a/src/cuttingplanes.jl +++ b/src/cuttingplanes.jl @@ -6,6 +6,8 @@ function reformulate_model( var_type = JuMP.variable_ref_type(model) obj = objective_function(model) sense = objective_sense(model) + + #Creation of seperation (SEP) and relaxed big M model (rBM). SEP, sep_ref_map, _ = copy_gdp_model(model) rBM, rBM_ref_map, _ = copy_gdp_model(model) reformulate_model(rBM, BigM(method.M_value)) @@ -14,18 +16,15 @@ function reformulate_model( main_to_rBM_map = Dict(v => rBM_ref_map[v] for v in all_variables(model)) JuMP.set_optimizer(SEP, method.optimizer) JuMP.set_optimizer(rBM, method.optimizer) + JuMP.set_silent(rBM) + JuMP.set_silent(SEP) + JuMP.relax_integrality(rBM) + JuMP.relax_integrality(SEP) JuMP.@objective(rBM, sense, _replace_variables_in_constraint(obj, main_to_rBM_map) ) - for m in [SEP, rBM] - binary_vars = filter(is_binary, all_variables(m)) - for var in binary_vars - unset_binary(var) - set_lower_bound(var, 0.0) - set_upper_bound(var, 1.0) - end - end + #Mapping of variables between models. rBM_to_SEP_map = Dict{var_type, var_type}() SEP_to_rBM_map = Dict{var_type, var_type}() for (var, rBM_var) in main_to_rBM_map @@ -34,8 +33,9 @@ function reformulate_model( SEP_to_rBM_map[SEP_var] = rBM_var end + #Main cutting planes loop. i = 1 - sep_obj = 1 + sep_obj = Inf while i <= method.max_iter && sep_obj > method.seperation_tolerance rBM_sol = _solve_rBM(rBM) SEP_sol = _solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) @@ -45,18 +45,21 @@ function reformulate_model( ) i += 1 end + + #Final reformulation with added cutting planes. reformulate_model(model, method.final_reform_method) return end function _solve_rBM( rBM::M, -) where {M <: JuMP.AbstractModel} + ) where {M <: JuMP.AbstractModel} T = JuMP.value_type(M) - JuMP.set_silent(rBM) optimize!(rBM, ignore_optimize_hook = true) rBM_vars = JuMP.all_variables(rBM) - sol = Dict{JuMP.AbstractVariableRef, T}(var => zero(T) for var in rBM_vars) + + #Solution to be passed to SEP model. + sol = Dict{JuMP.AbstractVariableRef,T}(var => zero(T) for var in rBM_vars) for rBM_var in rBM_vars sol[rBM_var] = JuMP.value(rBM_var) end @@ -66,19 +69,21 @@ end function _solve_SEP( SEP::M, rBM::M, - rBM_sol::Dict{<:JuMP.AbstractVariableRef, T}, - SEP_to_rBM_map::Dict{<:JuMP.AbstractVariableRef, <:JuMP.AbstractVariableRef}, - rBM_to_SEP_map::Dict{<:JuMP.AbstractVariableRef, <:JuMP.AbstractVariableRef} -) where {M <: JuMP.AbstractModel, T <: Number} - JuMP.set_silent(SEP) + rBM_sol::Dict{<:JuMP.AbstractVariableRef,T}, + SEP_to_rBM_map::Dict{<:JuMP.AbstractVariableRef,<:JuMP.AbstractVariableRef}, + rBM_to_SEP_map::Dict{<:JuMP.AbstractVariableRef,<:JuMP.AbstractVariableRef} + ) where {M <: JuMP.AbstractModel, T <: Number} + SEP_vars = [rBM_to_SEP_map[rBM_var] for rBM_var in JuMP.all_variables(rBM)] + + #Modified objective function for SEP. obj_expr = sum( (SEP_var - rBM_sol[SEP_to_rBM_map[SEP_var]])^2 for SEP_var in SEP_vars - ) + ) JuMP.@objective(SEP, Min, obj_expr) - optimize!(SEP, ignore_optimize_hook = true) + #Solution to be used in cutting plane generation. sol = Dict{JuMP.AbstractVariableRef, T}(var => zero(T) for var in SEP_vars) for SEP_var in SEP_vars sol[SEP_var] = JuMP.value(SEP_var) @@ -89,20 +94,23 @@ end function _cutting_planes( model::M, rBM::M, - main_to_rBM_map::Dict{<:JuMP.AbstractVariableRef, <:JuMP.AbstractVariableRef}, - main_to_SEP_map::Dict{<:JuMP.AbstractVariableRef, <:JuMP.AbstractVariableRef}, - rBM_sol::Dict{<:JuMP.AbstractVariableRef, T}, - SEP_sol::Dict{<:JuMP.AbstractVariableRef, T}, -) where {M <: JuMP.AbstractModel, T <: Number} + main_to_rBM_map::Dict{<:JuMP.AbstractVariableRef,<:JuMP.AbstractVariableRef}, + main_to_SEP_map::Dict{<:JuMP.AbstractVariableRef,<:JuMP.AbstractVariableRef}, + rBM_sol::Dict{<:JuMP.AbstractVariableRef,T}, + SEP_sol::Dict{<:JuMP.AbstractVariableRef,T}, + ) where {M <: JuMP.AbstractModel, T <: Number} main_vars = JuMP.all_variables(model) - ξ_sep = Dict{JuMP.AbstractVariableRef, T}(var => zero(T) for var in main_vars) + #Cutting plane generation + ξ_sep = Dict{JuMP.AbstractVariableRef,T}(var => zero(T) for var in main_vars) for var in main_vars - ξ_sep[var] = 2*(SEP_sol[main_to_SEP_map[var]] - rBM_sol[main_to_rBM_map[var]]) + ξ_sep[var] = 2*(SEP_sol[main_to_SEP_map[var]]-rBM_sol[main_to_rBM_map[var]]) end + #Cutting plane added to main model. main_cut = JuMP.@expression(model, - sum(ξ_sep[var]*(var - SEP_sol[main_to_SEP_map[var]]) for var in main_vars) + sum(ξ_sep[var]*(var - SEP_sol[main_to_SEP_map[var]]) for var in main_vars) ) + #Cutting plane added to rBM rBM_cut = _replace_variables_in_constraint(main_cut, main_to_rBM_map) JuMP.@constraint(model, main_cut >= 0.0) JuMP.@constraint(rBM, rBM_cut >= 0.0) From 50acafee45c79480d013b93233e239be5ebc8c98 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Wed, 5 Nov 2025 13:02:07 -0500 Subject: [PATCH 45/46] more spacing fixed --- src/cuttingplanes.jl | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/cuttingplanes.jl b/src/cuttingplanes.jl index 433334f..5644a7b 100644 --- a/src/cuttingplanes.jl +++ b/src/cuttingplanes.jl @@ -21,9 +21,8 @@ function reformulate_model( JuMP.relax_integrality(rBM) JuMP.relax_integrality(SEP) JuMP.@objective(rBM, sense, - _replace_variables_in_constraint(obj, main_to_rBM_map) + _replace_variables_in_constraint(obj, main_to_rBM_map) ) - #Mapping of variables between models. rBM_to_SEP_map = Dict{var_type, var_type}() SEP_to_rBM_map = Dict{var_type, var_type}() @@ -41,7 +40,7 @@ function reformulate_model( SEP_sol = _solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, rBM_to_SEP_map) sep_obj = objective_value(SEP) _cutting_planes(model, rBM, main_to_rBM_map, - main_to_SEP_map, rBM_sol, SEP_sol + main_to_SEP_map, rBM_sol, SEP_sol ) i += 1 end @@ -102,13 +101,17 @@ function _cutting_planes( main_vars = JuMP.all_variables(model) #Cutting plane generation - ξ_sep = Dict{JuMP.AbstractVariableRef,T}(var => zero(T) for var in main_vars) + ξ_sep = Dict{JuMP.AbstractVariableRef,T}(var =>zero(T) for var in main_vars) for var in main_vars - ξ_sep[var] = 2*(SEP_sol[main_to_SEP_map[var]]-rBM_sol[main_to_rBM_map[var]]) + ξ_sep[var] = 2*(SEP_sol[main_to_SEP_map[var]] + -rBM_sol[main_to_rBM_map[var]] + ) end #Cutting plane added to main model. main_cut = JuMP.@expression(model, - sum(ξ_sep[var]*(var - SEP_sol[main_to_SEP_map[var]]) for var in main_vars) + sum(ξ_sep[var]*(var - SEP_sol[main_to_SEP_map[var]]) + for var in main_vars + ) ) #Cutting plane added to rBM rBM_cut = _replace_variables_in_constraint(main_cut, main_to_rBM_map) From ddf9b44f086ea447e84c8667ba28c8a880616d62 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Wed, 5 Nov 2025 13:08:02 -0500 Subject: [PATCH 46/46] modified tests --- test/constraints/cuttingplanes.jl | 32 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/test/constraints/cuttingplanes.jl b/test/constraints/cuttingplanes.jl index 5026a8a..3c5239e 100644 --- a/test/constraints/cuttingplanes.jl +++ b/test/constraints/cuttingplanes.jl @@ -54,17 +54,13 @@ function test_solve_SEP() main_to_rBM_map = Dict(v => rBM_ref_map[v] for v in all_variables(model)) JuMP.set_optimizer(SEP, method.optimizer) JuMP.set_optimizer(rBM, method.optimizer) + JuMP.set_silent(rBM) + JuMP.set_silent(SEP) + JuMP.relax_integrality(rBM) + JuMP.relax_integrality(SEP) JuMP.@objective(rBM, sense, DP._replace_variables_in_constraint(obj, main_to_rBM_map) ) - for m in [SEP, rBM] - binary_vars = filter(is_binary, all_variables(m)) - for var in binary_vars - unset_binary(var) - set_lower_bound(var, 0.0) - set_upper_bound(var, 1.0) - end - end rBM_to_SEP_map = Dict{var_type, var_type}() SEP_to_rBM_map = Dict{var_type, var_type}() for (var, rBM_var) in main_to_rBM_map @@ -77,7 +73,9 @@ function test_solve_SEP() @test length(SEP_sol) == length(rBM_sol) @test SEP_sol[rBM_to_SEP_map[main_to_rBM_map[x]]] ≈ 4.0 - @test_throws ErrorException DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map, "not a dict") + @test_throws ErrorException DP._solve_SEP(SEP, rBM, rBM_sol, SEP_to_rBM_map + , "not a dict" + ) end function test_cutting_planes() @@ -100,17 +98,13 @@ function test_cutting_planes() main_to_rBM_map = Dict(v => rBM_ref_map[v] for v in all_variables(model)) JuMP.set_optimizer(SEP, method.optimizer) JuMP.set_optimizer(rBM, method.optimizer) + JuMP.set_silent(rBM) + JuMP.set_silent(SEP) + JuMP.relax_integrality(rBM) + JuMP.relax_integrality(SEP) JuMP.@objective(rBM, sense, DP._replace_variables_in_constraint(obj, main_to_rBM_map) ) - for m in [SEP, rBM] - binary_vars = filter(is_binary, all_variables(m)) - for var in binary_vars - unset_binary(var) - set_lower_bound(var, 0.0) - set_upper_bound(var, 1.0) - end - end rBM_to_SEP_map = Dict{var_type, var_type}() SEP_to_rBM_map = Dict{var_type, var_type}() for (var, rBM_var) in main_to_rBM_map @@ -128,7 +122,9 @@ function test_cutting_planes() @test rBM_sol[main_to_rBM_map[x]] ≈ 4.0 @test SEP_sol[rBM_to_SEP_map[main_to_rBM_map[x]]] ≈ 4.0 atol=1e-3 - @test_throws ErrorException DP._cutting_planes(model, rBM, main_to_rBM_map, main_to_SEP_map, rBM_sol, "not a dict") + @test_throws ErrorException DP._cutting_planes(model, rBM, main_to_rBM_map, + main_to_SEP_map, rBM_sol, "not a dict" + ) end function test_reformulate_model()