From b032a540cf42199b61f87493265f0983da2384ae Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Thu, 30 Oct 2025 13:21:31 -0400 Subject: [PATCH 1/8] . --- 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 2/8] . --- 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 3/8] 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 4/8] 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 5/8] 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 f148cacb80fdcf52235ecc0b3ce65b9d2b6a335d Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Mon, 3 Nov 2025 13:30:11 -0500 Subject: [PATCH 6/8] 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 7/8] 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 8b1274119d3f1a77c8b107d2690188a4565241ac Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Mon, 3 Nov 2025 16:27:27 -0500 Subject: [PATCH 8/8] additional comments --- src/utilities.jl | 117 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 2 deletions(-) diff --git a/src/utilities.jl b/src/utilities.jl index fdaa924..6c9b8e5 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -8,6 +8,18 @@ function _copy_model( return M() end +""" + JuMP.copy_extension_data( + data::GDPData, + new_model::JuMP.AbstractModel, + old_model::JuMP.AbstractModel + )::GDPData + +Extend `JuMP.copy_extension_data` to initialize an empty [`GDPData`](@ref) object +for the copied model. This is the first step in the model copying process and is +automatically called by `JuMP.copy_model`. The actual GDP data (logical variables, +disjunctions, etc.) is copied separately via [`copy_gdp_data`](@ref). +""" function JuMP.copy_extension_data( data::GDPData{M, V, C, T}, new_model::JuMP.AbstractModel, @@ -16,27 +28,71 @@ function JuMP.copy_extension_data( return GDPData{M, V, C}() end +""" + copy_gdp_data( + model::JuMP.AbstractModel, + new_model::JuMP.AbstractModel, + ref_map::JuMP.GenericReferenceMap + )::Dict{LogicalVariableRef, LogicalVariableRef} + +Copy all GDP-specific data from `model` to `new_model`, including logical variables, +logical constraints, disjunct constraints, and disjunctions. This function is called +automatically by [`copy_gdp_model`](@ref) after `JuMP.copy_model` has copied the base +model structure. + +**Arguments** +- `model::JuMP.AbstractModel`: The source model containing GDP data to copy. +- `new_model::JuMP.AbstractModel`: The destination model that will receive the copied GDP data. +- `ref_map::JuMP.GenericReferenceMap`: The reference map from `JuMP.copy_model` that maps + old variable references to new ones. + +**Returns** +- `Dict{LogicalVariableRef, LogicalVariableRef}`: A mapping from old logical variable + references to new logical variable references. +""" function copy_gdp_data( model::M, new_model::M, ref_map::GenericReferenceMap ) where {M <: JuMP.AbstractModel} - + old_gdp = model.ext[:GDP] + + # GDPData contains the following fields. + # DICTIONARIES (for loops below) + # - logical_variables + # - logical_constraints + # - disjunct_constraints + # - disjunctions + # - exactly1_constraints + # - indicator_to_binary + # - indicator_to_constraints + # - constraint_to_indicator + # - variable_bounds + # SINGLE VALUES (copy directly) + # - solution_method + # - ready_to_optimize + new_gdp = new_model.ext[:GDP] + + # Creating maps from old to new model. var_map = Dict(v => ref_map[v] for v in all_variables(model)) 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}}() + + # Copying logical variables for (idx, var) in old_gdp.logical_variables old_var_ref = LogicalVariableRef(model, idx) new_var_data = LogicalVariableData(var.variable, var.name) new_var = LogicalVariableRef(new_model, idx) lv_map[old_var_ref] = new_var + # Update to new_gdp.logical_variables new_gdp.logical_variables[idx] = new_var_data end + # Copying logical constraints for (idx, lc_data) in old_gdp.logical_constraints old_con_ref = LogicalConstraintRef(model, idx) new_con_ref = LogicalConstraintRef(new_model, idx) @@ -47,6 +103,7 @@ function copy_gdp_data( lc_map[old_con_ref] = new_con_ref end + # Copying disjunct constraints for (idx, disj_con_data) in old_gdp.disjunct_constraints old_constraint = disj_con_data.constraint old_dc_ref = DisjunctConstraintRef(model, idx) @@ -55,6 +112,7 @@ function copy_gdp_data( new_expr = _replace_variables_in_constraint(old_constraint.func, var_map ) + # Update to new_gdp.disjunct_constraints new_con = JuMP.build_constraint(error, new_expr, old_constraint.set, Disjunct(new_indicator) ) @@ -62,6 +120,7 @@ function copy_gdp_data( disj_con_map[old_dc_ref] = new_dc_ref end + # Copying disjunctions for (idx, disj_data) in old_gdp.disjunctions old_disj = disj_data.constraint new_indicators = [_replace_variables_in_constraint(indicator, lv_map) @@ -69,20 +128,26 @@ function copy_gdp_data( ] new_disj = Disjunction(new_indicators, old_disj.nested) disj_map[DisjunctionRef(model, idx)] = DisjunctionRef(new_model, idx) + # Update to new_gdp.disjunctions new_gdp.disjunctions[idx] = ConstraintData(new_disj, disj_data.name) end - + + # Copying exactly1 constraints for (d_ref, lc_ref) in old_gdp.exactly1_constraints new_lc_ref = lc_map[lc_ref] new_d_ref = disj_map[d_ref] + # Update to new_gdp.exactly1_constraints new_gdp.exactly1_constraints[new_d_ref] = new_lc_ref end + # Copying indicator to binary for (lv_ref, bref) in old_gdp.indicator_to_binary new_bref = _remap_indicator_to_binary(bref, var_map) + # Update to new_gdp.indicator_to_binary new_gdp.indicator_to_binary[lv_map[lv_ref]] = new_bref end + # Copying indicator to constraints 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}}}() @@ -92,30 +157,78 @@ function copy_gdp_data( ) push!(new_con_refs, new_con_ref) end + # Update to new_gdp.indicator_to_constraints new_gdp.indicator_to_constraints[new_lvar_ref] = new_con_refs end + # Copying constraint to indicator for (con_ref, lv_ref) in old_gdp.constraint_to_indicator + # Update to new_gdp.constraint_to_indicator new_gdp.constraint_to_indicator[ _remap_constraint_to_indicator(con_ref, disj_con_map, disj_map) ] = lv_map[lv_ref] end + # Copying variable bounds for (v, bounds) in old_gdp.variable_bounds + # Update to new_gdp.variable_bounds new_gdp.variable_bounds[var_map[v]] = bounds end + # Copying solution method and ready to optimize new_gdp.solution_method = old_gdp.solution_method new_gdp.ready_to_optimize = old_gdp.ready_to_optimize return lv_map end +""" + copy_gdp_model(model::JuMP.AbstractModel) + +Create a copy of a [`GDPModel`](@ref), including all variables, constraints, and +GDP-specific data (logical variables, disjunctions, etc.). + +**Arguments** +- `model::JuMP.AbstractModel`: The GDP model to copy. + +**Returns** +A tuple `(new_model, ref_map, lv_map)` where: +- `new_model`: The copied model. +- `ref_map::JuMP.GenericReferenceMap`: Maps old variable and constraint references to new ones. +- `lv_map::Dict{LogicalVariableRef, LogicalVariableRef}`: Maps old logical variable + references to new ones. + +## Example +```julia +using DisjunctiveProgramming, HiGHS +model = GDPModel(HiGHS.Optimizer) +@variable(model, x) +@variable(model, Y[1:2], LogicalVariable) +@constraint(model, x <= 10, Disjunct(Y[1])) +@constraint(model, x >= 20, Disjunct(Y[2])) +@disjunction(model, Y) + +new_model, ref_map, lv_map = copy_gdp_model(model) +``` +""" 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 +################################################################################ +# GDP REMAPPING +################################################################################ +# These remapping functions use multiple dispatch to handle different types that +# can appear in GDP data structures during model copying. +# +# Indicators can be represented by a variable or an affine expression to +# indicate a complementary relationship with another variable. +# This translates to a binary or affine expression in its binary reformulation. +# +# Depending on the above, different mappings are required for indicator_to_binary, +# indicator_to_constraints, and constraint_to_indicator. +################################################################################ function _remap_indicator_to_constraint( con_ref::DisjunctConstraintRef,