From f3955690ad4e3302627c7aaf47a3a566ed42e6e5 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Sun, 22 Mar 2026 20:39:03 -0400 Subject: [PATCH 01/34] inital split --- src/variables.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/variables.jl b/src/variables.jl index f96cfe02..e1d2d2b3 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -424,8 +424,7 @@ function _interrogate_variables(interrogator::Function, nlp::JuMP.GenericNonline for arg in nlp.args _interrogate_variables(interrogator, arg) end - # TODO avoid recursion. See InfiniteOpt.jl for alternate method that avoids stackoverflow errors with deeply nested expressions: - # https://github.com/infiniteopt/InfiniteOpt.jl/blob/cb6dd6ae40fe0144b1dd75da0739ea6e305d5357/src/expressions.jl#L520-L534 + return end From ed2eb87d71d741d643cf594cff140fc5d0b29117 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Mon, 23 Mar 2026 09:09:23 -0400 Subject: [PATCH 02/34] . --- src/variables.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/variables.jl b/src/variables.jl index e1d2d2b3..f96cfe02 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -424,7 +424,8 @@ function _interrogate_variables(interrogator::Function, nlp::JuMP.GenericNonline for arg in nlp.args _interrogate_variables(interrogator, arg) end - + # TODO avoid recursion. See InfiniteOpt.jl for alternate method that avoids stackoverflow errors with deeply nested expressions: + # https://github.com/infiniteopt/InfiniteOpt.jl/blob/cb6dd6ae40fe0144b1dd75da0739ea6e305d5357/src/expressions.jl#L520-L534 return end From 9e7211e755da8aa58b554c0cc4e7313c093af4a8 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Mon, 23 Mar 2026 09:28:15 -0400 Subject: [PATCH 03/34] . --- src/variables.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/variables.jl b/src/variables.jl index f96cfe02..e1d2d2b3 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -424,8 +424,7 @@ function _interrogate_variables(interrogator::Function, nlp::JuMP.GenericNonline for arg in nlp.args _interrogate_variables(interrogator, arg) end - # TODO avoid recursion. See InfiniteOpt.jl for alternate method that avoids stackoverflow errors with deeply nested expressions: - # https://github.com/infiniteopt/InfiniteOpt.jl/blob/cb6dd6ae40fe0144b1dd75da0739ea6e305d5357/src/expressions.jl#L520-L534 + return end From 616b7faef8dd7745f4c51d325ba46c6d3c049c26 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Mon, 23 Mar 2026 09:34:01 -0400 Subject: [PATCH 04/34] . --- src/variables.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/variables.jl b/src/variables.jl index e1d2d2b3..f96cfe02 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -424,7 +424,8 @@ function _interrogate_variables(interrogator::Function, nlp::JuMP.GenericNonline for arg in nlp.args _interrogate_variables(interrogator, arg) end - + # TODO avoid recursion. See InfiniteOpt.jl for alternate method that avoids stackoverflow errors with deeply nested expressions: + # https://github.com/infiniteopt/InfiniteOpt.jl/blob/cb6dd6ae40fe0144b1dd75da0739ea6e305d5357/src/expressions.jl#L520-L534 return end From 6f04419352987f195a6b31ccf7e5b1cd80f670f2 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Sun, 22 Mar 2026 20:39:03 -0400 Subject: [PATCH 05/34] inital split --- src/utilities.jl | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/utilities.jl b/src/utilities.jl index b0203d70..69d3f4ca 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -129,6 +129,19 @@ function get_constant(expr::JuMP.AbstractVariableRef) return zero(JuMP.value_type(typeof(JuMP.owner_model(expr)))) end +################################################################################ +# ZERO EXPRESSION CONSTRUCTORS +################################################################################ +# Create a type-correct zero affine expression for the model. +_zero_aff(model::JuMP.AbstractModel) = zero( + JuMP.GenericAffExpr{JuMP.value_type(typeof(model)), + JuMP.variable_ref_type(model)}) + +# Create a type-correct zero quadratic expression for the model. +_zero_quad(model::JuMP.AbstractModel) = zero( + JuMP.GenericQuadExpr{JuMP.value_type(typeof(model)), + JuMP.variable_ref_type(model)}) + ################################################################################ # MODEL COPYING ################################################################################ From 2a7b894a4c1c4cb2e9416e717d8261a1a2a62c21 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Mon, 23 Mar 2026 00:01:36 -0400 Subject: [PATCH 06/34] inital split --- Project.toml | 15 +++++++++------ src/extension_api.jl | 10 +++++----- test/solve.jl | 12 ++++++++++-- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/Project.toml b/Project.toml index a5320aa7..3346659a 100644 --- a/Project.toml +++ b/Project.toml @@ -9,25 +9,28 @@ Reexport = "189a3867-3050-52da-a836-e630ba90ab69" [weakdeps] InfiniteOpt = "20393b10-9daf-11e9-18c9-8db751c92c57" +Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" [extensions] -InfiniteDisjunctiveProgramming = "InfiniteOpt" +InfiniteDisjunctiveProgramming = ["InfiniteOpt", "Interpolations"] [compat] Aqua = "0.8" +InfiniteOpt = "0.6" +Interpolations = "0.16.2" +Ipopt = "1.9.0" JuMP = "1.18" +Juniper = "0.9.3" Reexport = "1" julia = "1.10" -Juniper = "0.9.3" -Ipopt = "1.9.0" -InfiniteOpt = "0.6" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" Juniper = "2ddba703-00a4-53a7-87a5-e8b9971dde84" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Aqua", "HiGHS", "Test", "Juniper", "Ipopt", "InfiniteOpt"] +test = ["Aqua", "HiGHS", "Test", "Juniper", "Ipopt", "Interpolations"] diff --git a/src/extension_api.jl b/src/extension_api.jl index ad3566f6..6d046450 100644 --- a/src/extension_api.jl +++ b/src/extension_api.jl @@ -1,8 +1,8 @@ """ InfiniteGDPModel(args...; kwargs...) -Creates an `InfiniteOpt.InfiniteModel` that is compatible with the -capabiltiies provided by DisjunctiveProgramming.jl. This requires +Creates an `InfiniteOpt.InfiniteModel` that is compatible with the +capabiltiies provided by DisjunctiveProgramming.jl. This requires that InfiniteOpt be imported first. **Example** @@ -18,9 +18,9 @@ function InfiniteGDPModel end """ InfiniteLogical(prefs...) -Allows users to create infinite logical variables. This is a tag -for the `@variable` macro that is a combination of `InfiniteOpt.Infinite` -and `DisjunctiveProgramming.Logical`. This requires that InfiniteOpt be +Allows users to create infinite logical variables. This is a tag +for the `@variable` macro that is a combination of `InfiniteOpt.Infinite` +and `DisjunctiveProgramming.Logical`. This requires that InfiniteOpt be first imported. **Example** diff --git a/test/solve.jl b/test/solve.jl index c3ca9441..3f9717b3 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -7,7 +7,7 @@ function test_linear_gdp_example(m, use_complements = false) @variable(m, Y2, Logical, logical_complement = Y1) Y = [Y1, Y2] else - @variable(m, Y[1:2], Logical) + @variable(m, Y[1:3], Logical) end @variable(m, W[1:2], Logical) @objective(m, Max, sum(x)) @@ -16,7 +16,13 @@ function test_linear_gdp_example(m, use_complements = false) @constraint(m, w2[i=1:2], [2,4][i] ≤ x[i] ≤ [3,5][i], Disjunct(W[2])) @constraint(m, y2[i=1:2], [8,1][i] ≤ x[i] ≤ [9,2][i], Disjunct(Y[2])) @disjunction(m, inner, [W[1], W[2]], Disjunct(Y[1])) - @disjunction(m, outer, [Y[1], Y[2]]) + if use_complements + @disjunction(m, outer, [Y[1], Y[2]]) + else + #Infeasible disjunct + @constraint(m, y3[i=1:2], x[i] ≤ [-44,44][i], Disjunct(Y[3])) + @disjunction(m, outer, [Y[1], Y[2], Y[3]]) + end @test optimize!(m, gdp_method = BigM()) isa Nothing @test termination_status(m) == MOI.OPTIMAL @@ -98,6 +104,8 @@ function test_quadratic_gdp_example(use_complements = false) #psplit does not wo @objective(m, Max, sum(x)) @constraint(m, y1_quad, x[1]^2 + x[2]^2 ≤ 16, Disjunct(Y[1])) + # This constraint is always satisfied + @constraint(m, y1_global, x[1] + x[2] ≤ 20, Disjunct(Y[1])) @constraint(m, w1[i=1:2], [1, 2][i] ≤ x[i] ≤ [3, 4][i], Disjunct(W[1])) @constraint(m, w1_quad, x[1]^2 ≥ 2, Disjunct(W[1])) From eab521cf6c60c1c9805bee069bab8785db54576b Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Mon, 20 Apr 2026 17:33:50 -0400 Subject: [PATCH 07/34] . --- test/solve.jl | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/test/solve.jl b/test/solve.jl index 3f9717b3..c3ca9441 100644 --- a/test/solve.jl +++ b/test/solve.jl @@ -7,7 +7,7 @@ function test_linear_gdp_example(m, use_complements = false) @variable(m, Y2, Logical, logical_complement = Y1) Y = [Y1, Y2] else - @variable(m, Y[1:3], Logical) + @variable(m, Y[1:2], Logical) end @variable(m, W[1:2], Logical) @objective(m, Max, sum(x)) @@ -16,13 +16,7 @@ function test_linear_gdp_example(m, use_complements = false) @constraint(m, w2[i=1:2], [2,4][i] ≤ x[i] ≤ [3,5][i], Disjunct(W[2])) @constraint(m, y2[i=1:2], [8,1][i] ≤ x[i] ≤ [9,2][i], Disjunct(Y[2])) @disjunction(m, inner, [W[1], W[2]], Disjunct(Y[1])) - if use_complements - @disjunction(m, outer, [Y[1], Y[2]]) - else - #Infeasible disjunct - @constraint(m, y3[i=1:2], x[i] ≤ [-44,44][i], Disjunct(Y[3])) - @disjunction(m, outer, [Y[1], Y[2], Y[3]]) - end + @disjunction(m, outer, [Y[1], Y[2]]) @test optimize!(m, gdp_method = BigM()) isa Nothing @test termination_status(m) == MOI.OPTIMAL @@ -104,8 +98,6 @@ function test_quadratic_gdp_example(use_complements = false) #psplit does not wo @objective(m, Max, sum(x)) @constraint(m, y1_quad, x[1]^2 + x[2]^2 ≤ 16, Disjunct(Y[1])) - # This constraint is always satisfied - @constraint(m, y1_global, x[1] + x[2] ≤ 20, Disjunct(Y[1])) @constraint(m, w1[i=1:2], [1, 2][i] ≤ x[i] ≤ [3, 4][i], Disjunct(W[1])) @constraint(m, w1_quad, x[1]^2 ≥ 2, Disjunct(W[1])) From a7ad3fd7283e90f33bcda87b6edc13103df986dc Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Mon, 20 Apr 2026 18:33:40 -0400 Subject: [PATCH 08/34] . --- ext/InfiniteDisjunctiveProgramming.jl | 501 +++++++++++++++++++++++--- 1 file changed, 457 insertions(+), 44 deletions(-) diff --git a/ext/InfiniteDisjunctiveProgramming.jl b/ext/InfiniteDisjunctiveProgramming.jl index 426f7ac6..831820f7 100644 --- a/ext/InfiniteDisjunctiveProgramming.jl +++ b/ext/InfiniteDisjunctiveProgramming.jl @@ -1,24 +1,21 @@ module InfiniteDisjunctiveProgramming import JuMP.MOI as _MOI -import InfiniteOpt, JuMP +import InfiniteOpt, JuMP, Interpolations import DisjunctiveProgramming as DP ################################################################################ # MODEL ################################################################################ function DP.InfiniteGDPModel(args...; kwargs...) - return DP.GDPModel{ - InfiniteOpt.InfiniteModel, - InfiniteOpt.GeneralVariableRef, - InfiniteOpt.InfOptConstraintRef - }(args...; kwargs...) + return DP.GDPModel{InfiniteOpt.InfiniteModel, + InfiniteOpt.GeneralVariableRef, + InfiniteOpt.InfOptConstraintRef}(args...; kwargs...) end function DP.collect_all_vars(model::InfiniteOpt.InfiniteModel) vars = JuMP.all_variables(model) - derivs = InfiniteOpt.all_derivatives(model) - return append!(vars, derivs) + return append!(vars, InfiniteOpt.all_derivatives(model)) end ################################################################################ @@ -26,7 +23,7 @@ end ################################################################################ DP.InfiniteLogical(prefs...) = DP.Logical(InfiniteOpt.Infinite(prefs...)) -_is_parameter(vref::InfiniteOpt.GeneralVariableRef) = +_is_parameter(vref::InfiniteOpt.GeneralVariableRef) = _is_parameter(InfiniteOpt.dispatch_variable_ref(vref)) _is_parameter(::InfiniteOpt.DependentParameterRef) = true _is_parameter(::InfiniteOpt.IndependentParameterRef) = true @@ -41,20 +38,19 @@ end function DP.VariableProperties(vref::InfiniteOpt.GeneralVariableRef) info = DP.get_variable_info(vref) name = JuMP.name(vref) - set = nothing prefs = InfiniteOpt.parameter_refs(vref) var_type = !isempty(prefs) ? InfiniteOpt.Infinite(prefs...) : nothing - return DP.VariableProperties(info, name, set, var_type) + return DP.VariableProperties(info, name, nothing, var_type) end -# Extract parameter refs from expression and return VariableProperties with Infinite type +# Extract parameter refs from expression, return VariableProperties with +# Infinite type. function DP.VariableProperties( expr::Union{ JuMP.GenericAffExpr{C, InfiniteOpt.GeneralVariableRef}, JuMP.GenericQuadExpr{C, InfiniteOpt.GeneralVariableRef}, - JuMP.GenericNonlinearExpr{InfiniteOpt.GeneralVariableRef} - } -) where C + JuMP.GenericNonlinearExpr{InfiniteOpt.GeneralVariableRef}} + ) where C prefs = InfiniteOpt.parameter_refs(expr) info = DP._free_variable_info() var_type = !isempty(prefs) ? InfiniteOpt.Infinite(prefs...) : nothing @@ -66,9 +62,8 @@ function DP.VariableProperties( InfiniteOpt.GeneralVariableRef, JuMP.GenericAffExpr{<:Any, InfiniteOpt.GeneralVariableRef}, JuMP.GenericQuadExpr{<:Any, InfiniteOpt.GeneralVariableRef}, - JuMP.GenericNonlinearExpr{InfiniteOpt.GeneralVariableRef} - }} -) + JuMP.GenericNonlinearExpr{InfiniteOpt.GeneralVariableRef}}} + ) all_prefs = Set{InfiniteOpt.GeneralVariableRef}() for expr in exprs for pref in InfiniteOpt.parameter_refs(expr) @@ -90,9 +85,8 @@ end ################################################################################ function JuMP.add_constraint( model::InfiniteOpt.InfiniteModel, - c::JuMP.VectorConstraint{F, S}, - name::String = "" -) where {F, S <: DP.AbstractCardinalitySet} + c::JuMP.VectorConstraint{F, S}, name::String = "" + ) where {F, S <: DP.AbstractCardinalitySet} return DP._add_cardinality_constraint(model, c, name) end @@ -100,7 +94,7 @@ function JuMP.add_constraint( model::M, c::JuMP.ScalarConstraint{DP._LogicalExpr{M}, S}, name::String = "" -) where {S, M <: InfiniteOpt.InfiniteModel} + ) where {S, M <: InfiniteOpt.InfiniteModel} return DP._add_logical_constraint(model, c, name) end @@ -108,28 +102,29 @@ function JuMP.add_constraint( model::M, c::JuMP.ScalarConstraint{DP.LogicalVariableRef{M}, S}, name::String = "" -) where {M <: InfiniteOpt.InfiniteModel, S} - error("Cannot define constraint on single logical variable, use `fix` instead.") + ) where {M <: InfiniteOpt.InfiniteModel, S} + error("Cannot define constraint on single logical variable, " * + "use `fix` instead.") end function JuMP.add_constraint( model::M, c::JuMP.ScalarConstraint{ - JuMP.GenericAffExpr{C, DP.LogicalVariableRef{M}}, S - }, + JuMP.GenericAffExpr{C, DP.LogicalVariableRef{M}}, S}, name::String = "" -) where {M <: InfiniteOpt.InfiniteModel, S, C} - error("Cannot add, subtract, or multiply with logical variables.") + ) where {M <: InfiniteOpt.InfiniteModel, S, C} + error("Cannot add, subtract, or multiply with " * + "logical variables.") end function JuMP.add_constraint( model::M, c::JuMP.ScalarConstraint{ - JuMP.GenericQuadExpr{C, DP.LogicalVariableRef{M}}, S - }, + JuMP.GenericQuadExpr{C, DP.LogicalVariableRef{M}}, S}, name::String = "" -) where {M <: InfiniteOpt.InfiniteModel, S, C} - error("Cannot add, subtract, or multiply with logical variables.") + ) where {M <: InfiniteOpt.InfiniteModel, S, C} + error("Cannot add, subtract, or multiply with " * + "logical variables.") end ################################################################################ @@ -137,7 +132,7 @@ end ################################################################################ function DP.get_constant( expr::JuMP.GenericAffExpr{T, InfiniteOpt.GeneralVariableRef} -) where {T} + ) where {T} constant = JuMP.constant(expr) param_expr = zero(typeof(expr)) for (var, coeff) in expr.terms @@ -149,16 +144,16 @@ function DP.get_constant( end function DP.disaggregate_expression( - model::M, - aff::JuMP.GenericAffExpr, + model::M, aff::JuMP.GenericAffExpr, bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::DP._Hull -) where {M <: InfiniteOpt.InfiniteModel} + ) where {M <: InfiniteOpt.InfiniteModel} terms = Any[aff.constant * bvref] for (vref, coeff) in aff.terms if JuMP.is_binary(vref) push!(terms, coeff * vref) - elseif vref isa InfiniteOpt.GeneralVariableRef && _is_parameter(vref) + elseif vref isa InfiniteOpt.GeneralVariableRef && + _is_parameter(vref) push!(terms, coeff * vref * bvref) elseif !haskey(method.disjunct_variables, (vref, bvref)) push!(terms, coeff * vref) @@ -170,18 +165,436 @@ function DP.disaggregate_expression( return JuMP.@expression(model, sum(terms)) end +# Quadratic expression: handle parameter x parameter, parameter x variable, +# and variable x variable terms. +function DP.disaggregate_expression( + model::M, quad::JuMP.GenericQuadExpr, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + method::DP._Hull + ) where {M <: InfiniteOpt.InfiniteModel} + # Affine part (uses InfiniteOpt override above) + new_expr = DP.disaggregate_expression(model, quad.aff, bvref, method) + ϵ = method.value + for (pair, coeff) in quad.terms + a_param = pair.a isa InfiniteOpt.GeneralVariableRef && + _is_parameter(pair.a) + b_param = pair.b isa InfiniteOpt.GeneralVariableRef && + _is_parameter(pair.b) + if a_param && b_param + # param × param: constant, scale by y + new_expr += coeff * pair.a * pair.b * bvref + elseif a_param + # param × var: perspective cancels y + db = method.disjunct_variables[pair.b, bvref] + new_expr += coeff * pair.a * db + elseif b_param + # var × param: perspective cancels y + da = method.disjunct_variables[pair.a, bvref] + new_expr += coeff * da * pair.b + else + # var × var: standard perspective + da = method.disjunct_variables[pair.a, bvref] + db = method.disjunct_variables[pair.b, bvref] + new_expr += coeff * da * db / ((1 - ϵ) * bvref + ϵ) + end + end + return new_expr +end + ################################################################################ -# ERROR MESSAGES +# MBM FOR INFINITEMODEL ################################################################################ -function DP.reformulate_model(::InfiniteOpt.InfiniteModel, ::DP.MBM) - error("The `MBM` method is not supported for `InfiniteModel`." * - "Please use `BigM`, `Hull`, `Indicator`, or `PSplit` instead.") +# Reuses the finite MBM infrastructure by overriding: +# copy_model_with_constraints (build mini InfiniteModel + +# transcribe to flat JuMP model), prepare_max_M_objective +# (expand infinite constraint into K flat objectives via +# _build_flat_map), and aggregate_M_values (interpolate flat +# values to parameter function). + +# Collect all parameter function refs from all disjunct constraints in +# the model. +function _all_param_functions( + model::InfiniteOpt.InfiniteModel + ) + pf_set = Set{InfiniteOpt.GeneralVariableRef}() + for (_, crefs) in DP._indicator_to_constraints(model) + for cref in crefs + cref isa DP.DisjunctConstraintRef || continue + con = JuMP.constraint_object(cref) + for v in InfiniteOpt.all_expression_variables( + con.func) + dv = InfiniteOpt.dispatch_variable_ref(v) + if dv isa InfiniteOpt.ParameterFunctionRef + push!(pf_set, v) + end + end + end + end + return pf_set +end + +# Build a flat map for support point k. Maps decision variables to their +# flat JuMP.VariableRef at support k (handling multi-parameter indexing) +# and evaluates parameter functions to their numerical values. pf_set is +# precomputed by the caller to avoid rescanning all disjunct constraints +# on every support point. +function _build_flat_map( + sub::DP.GDPSubmodel, k::Int, + prefs::Vector{InfiniteOpt.GeneralVariableRef}, + supports::Dict{InfiniteOpt.GeneralVariableRef,Vector{Float64}}, + full_shape::Tuple, + pf_set::Set{InfiniteOpt.GeneralVariableRef} + ) + ci = CartesianIndices(full_shape)[k] + + # Decision variables: map each to its variable-local index + flat_map = Dict{InfiniteOpt.GeneralVariableRef, Any}() + for (v, ws) in sub.fwd_map + if length(ws) == 1 + flat_map[v] = ws[1] + else + vp = InfiniteOpt.parameter_refs(v) + shape = Tuple(length(supports[p]) for p in vp) + idx = Tuple(ci[findfirst(==(p), prefs)] for p in vp) + flat_map[v] = ws[LinearIndices(shape)[idx...]] + end + end + + # Parameter functions: evaluate at support point k + sup_vals = Dict( + prefs[i] => supports[prefs[i]][ci[i]] + for i in 1:length(prefs)) + for pf in pf_set + fn = InfiniteOpt.raw_function(pf) + pf_prefs = InfiniteOpt.parameter_refs(pf) + pf_vals = Tuple(sup_vals[p] for p in pf_prefs) + flat_map[pf] = fn(pf_vals...) + end + return flat_map end -function DP.reformulate_model(::InfiniteOpt.InfiniteModel, ::DP.CuttingPlanes) - error("The `CuttingPlanes` method is not supported for `InfiniteModel`." * - "Please use `BigM`, `Hull`, `Indicator`, or `PSplit` instead.") +# Build mini InfiniteModel with only the given disjunct constraints, +# transcribe to flat JuMP model, return GDPSubmodel with forward map. +function DP.copy_model_with_constraints( + model::InfiniteOpt.InfiniteModel, + constraints::Vector{<:DP.DisjunctConstraintRef}, + method::DP._MBM + ) + mini = InfiniteOpt.InfiniteModel() + ref_map = Dict{InfiniteOpt.GeneralVariableRef,InfiniteOpt.GeneralVariableRef}() + + # 1. Copy infinite parameters with their supports + for p in InfiniteOpt.all_parameters(model) + domain = InfiniteOpt.infinite_domain(p) + sups = Float64.(InfiniteOpt.supports(p)) + param = InfiniteOpt.build_parameter(error, domain; supports = sups) + new_p = InfiniteOpt.add_parameter(mini, param, JuMP.name(p)) + ref_map[p] = new_p + end + + # 2. Copy decision variables with bounds (skip parameters) + for v in JuMP.all_variables(model) + _is_parameter(v) && continue + prefs = InfiniteOpt.parameter_refs(v) + var_type = isempty(prefs) ? nothing : + InfiniteOpt.Infinite(Tuple(ref_map[p] for p in prefs)...) + props = DP.VariableProperties(DP.get_variable_info(v),"", nothing, var_type) + ref_map[v] = DP.create_variable(mini, props) + end + + # 3. Copy derivatives with their bounds + for d in InfiniteOpt.all_derivatives(model) + vref = InfiniteOpt.derivative_argument(d) + pref = InfiniteOpt.operator_parameter(d) + new_d = InfiniteOpt.deriv(ref_map[vref], ref_map[pref]) + info = DP.get_variable_info(d) + info.has_lb && JuMP.set_lower_bound(new_d, info.lower_bound) + info.has_ub && JuMP.set_upper_bound(new_d, info.upper_bound) + ref_map[d] = new_d + end + + # 4. Copy parameter functions from ALL disjuncts (needed for + # constraint transcription) + pf_set = _all_param_functions(model) + for pf in pf_set + fn = InfiniteOpt.raw_function(pf) + prefs = InfiniteOpt.parameter_refs(pf) + mapped_prefs = Tuple(ref_map[p] for p in prefs) + new_pf = _make_parameter_function(mini, fn, mapped_prefs...) + ref_map[pf] = new_pf + end + + # 5. Add disjunct constraints using existing ref_map + for cref in constraints + cref isa DP.DisjunctConstraintRef || continue + con = JuMP.constraint_object(cref) + new_func = DP._replace_variables_in_constraint(con.func, ref_map) + T = one(JuMP.value_type(typeof(mini))) + JuMP.@constraint(mini, new_func * T in con.set) + end + + # 6. Transcribe mini InfiniteModel to flat JuMP model + flat, tr_fwd = transcribe_to_flat(mini) + + # 7. Remap fwd_map: original model var -> flat JuMP VarRef + fwd_map = Dict{InfiniteOpt.GeneralVariableRef, Vector{JuMP.VariableRef}}() + for (orig, mapped) in ref_map + _is_parameter(orig) && continue + haskey(tr_fwd, mapped) || continue + fwd_map[orig] = tr_fwd[mapped] + end + + decision_vars = collect(keys(fwd_map)) + JuMP.set_optimizer(flat, method.optimizer) + JuMP.set_silent(flat) + return DP.GDPSubmodel(flat, decision_vars, fwd_map) end +# Prepare objectives for all support points. Expands an infinite +# constraint into K flat objectives via _build_flat_map with +# multi-parameter indexing and parameter function evaluation. +function DP.prepare_max_M_objective( + model::InfiniteOpt.InfiniteModel, + obj::JuMP.ScalarConstraint{T, S}, + sub::DP.GDPSubmodel + ) where {T, S <: _MOI.LessThan} + prefs, supports = _collect_parameters(model) + full_shape = Tuple(length(supports[p]) for p in prefs) + K = prod(full_shape) + pf_set = _all_param_functions(model) + objectives = Vector{JuMP.AbstractJuMPScalar}(undef, K) + for k in 1:K + flat_map = _build_flat_map(sub, k, prefs, supports, full_shape, pf_set) + objectives[k] = -obj.set.upper + + DP._replace_variables_in_constraint(obj.func, flat_map) + end + return objectives +end +function DP.prepare_max_M_objective( + model::InfiniteOpt.InfiniteModel, + obj::JuMP.ScalarConstraint{T, S}, + sub::DP.GDPSubmodel + ) where {T, S <: _MOI.GreaterThan} + prefs, supports = _collect_parameters(model) + full_shape = Tuple(length(supports[p]) for p in prefs) + K = prod(full_shape) + pf_set = _all_param_functions(model) + objectives = Vector{JuMP.AbstractJuMPScalar}(undef, K) + for k in 1:K + flat_map = _build_flat_map(sub, k, prefs, supports,full_shape, pf_set) + objectives[k] = obj.set.lower - + DP._replace_variables_in_constraint(obj.func, flat_map) + end + return objectives end +# Solve the submodel for a vector of objectives (one per +# support point). Returns aggregated result or nothing. +function DP._raw_M( + sub::DP.GDPSubmodel, + objectives::Vector{<:JuMP.AbstractJuMPScalar}, + method::DP._MBM + ) + M_vals = typeof(method.default_M)[] + for obj_expr in objectives + JuMP.set_start_value.(JuMP.all_variables(sub.model), nothing) + JuMP.@objective(sub.model, Max, obj_expr) + JuMP.optimize!(sub.model) + if JuMP.is_solved_and_feasible(sub.model) + push!(M_vals, max( + JuMP.objective_value(sub.model), + zero(method.default_M))) + elseif JuMP.termination_status(sub.model) == + JuMP.MOI.INFEASIBLE + return nothing + else + push!(M_vals, method.default_M) + end + end + model = JuMP.owner_model( + first(keys(sub.fwd_map))) + return aggregate_M_values(model, M_vals) +end + +# Condense flat per-support values to final form (MBM path). +function aggregate_M_values( + model::InfiniteOpt.InfiniteModel, + vals::AbstractVector{<:Real} + ) + if all(==(vals[1]), vals) + return vals[1] + end + prefs, supports = _collect_parameters(model) + return condense_to_pf(model, vals, prefs, supports) +end + +# Interpolate flat per-support values into a parameter function. Computes +# grids/shape from supports, reshapes, interpolates, and registers on +# the model. +function condense_to_pf( + model::InfiniteOpt.InfiniteModel, + vals::AbstractVector{<:Real}, + prefs::Union{Vector{InfiniteOpt.GeneralVariableRef}, + Tuple{Vararg{InfiniteOpt.GeneralVariableRef}}}, + supports::Dict{InfiniteOpt.GeneralVariableRef, + Vector{Float64}} + ) + grids = Tuple(supports[p] for p in prefs) + shape = Tuple(length(supports[p]) for p in prefs) + nd = reshape(vals, shape) + fn = Interpolations.linear_interpolation(grids, nd, + extrapolation_bc = Interpolations.Line()) + return _make_parameter_function(model, fn, prefs...) +end + +################################################################################ +# TRANSCRIPTION HELPERS +################################################################################ + +# Create a parameter function programmatically. Uses +# build_parameter_function + add_parameter_function (the lower-level +# API behind @parameter_function) since the macro doesn't support +# programmatic use. The closure wrapper handles non-Function callables +# like Interpolations.Extrapolation. Accepts any number of prefs via +# varargs: _make_parameter_function(m, f, t) for 1D, (m, f, t, x) for 2D. +function _make_parameter_function( + model::InfiniteOpt.InfiniteModel, fn, + prefs::InfiniteOpt.GeneralVariableRef... + ) + f = fn isa Function ? fn : ((args...) -> fn(args...)) + pref_arg = length(prefs) == 1 ? prefs[1] : prefs + pfunc = InfiniteOpt.build_parameter_function(error, f, pref_arg) + return InfiniteOpt.add_parameter_function(model, pfunc) +end + +# Collect all infinite parameters and their supports from the model. +function _collect_parameters(model::InfiniteOpt.InfiniteModel) + params = collect(InfiniteOpt.all_parameters(model)) + if isempty(params) + error("Model has no infinite parameters.") + end + prefs = InfiniteOpt.GeneralVariableRef[p for p in params] + supports = Dict{InfiniteOpt.GeneralVariableRef, Vector{Float64}}( + p => Float64.(InfiniteOpt.supports(p)) for p in prefs) + return prefs, supports +end + +# Transcribe an InfiniteModel to a flat JuMP.Model with forward variable +# map. Shared by MBM and CP paths. +function transcribe_to_flat(model::InfiniteOpt.InfiniteModel) + InfiniteOpt.build_transformation_backend!(model) + flat = InfiniteOpt.transformation_model(model) + fwd_map = Dict{InfiniteOpt.GeneralVariableRef, Vector{JuMP.VariableRef}}() + for v in DP.collect_all_vars(model) + tv = InfiniteOpt.transformation_variable(v) + vprefs = InfiniteOpt.parameter_refs(v) + fwd_map[v] = isempty(vprefs) ? [tv] : vec(tv) + end + return flat, fwd_map +end + +################################################################################ +# CUTTING PLANES FOR INFINITEMODEL +################################################################################ + +# Build CP subproblem: reformulate the InfiniteModel, transcribe to a flat +# JuMP copy, and wrap in GDPSubmodel with forward variable map. +function DP.copy_and_reformulate( + model::InfiniteOpt.InfiniteModel, + decision_vars::Vector{InfiniteOpt.GeneralVariableRef}, + reform_method::DP.AbstractReformulationMethod, + method::DP.CuttingPlanes + ) + DP.reformulate_model(model, reform_method) + flat, tr_fwd = transcribe_to_flat(model) + sub_copy, copy_map = JuMP.copy_model(flat) + fwd_map = Dict{InfiniteOpt.GeneralVariableRef, Vector{JuMP.VariableRef}}() + for v in decision_vars + haskey(tr_fwd, v) || continue + fwd_map[v] = [copy_map[tv] for tv in tr_fwd[v]] + end + sub = DP.GDPSubmodel(sub_copy, decision_vars, fwd_map) + JuMP.set_optimizer(sub.model, method.optimizer) + JuMP.set_silent(sub.model) + return sub +end + +# Full CP loop for InfiniteModel: cannot solve in-place, so both SEP +# and rBM are separate transcribed flat copies. +function DP.reformulate_model( + model::InfiniteOpt.InfiniteModel, + method::DP.CuttingPlanes + ) + decision_vars = DP.collect_cutting_planes_vars(model) + separation = DP.copy_and_reformulate( + model, decision_vars, DP.Hull(), method) + JuMP.relax_integrality(separation.model) + rBM = DP.copy_and_reformulate( + model, decision_vars, DP.BigM(method.M_value), method) + JuMP.relax_integrality(rBM.model) + for iter in 1:method.max_iter + JuMP.optimize!(rBM.model, ignore_optimize_hook = true) + rBM_sol = DP.extract_solution(rBM) + sep_obj, sep_sol = DP._solve_separation(separation, rBM_sol) + sep_obj <= method.seperation_tolerance && break + _add_infinite_cut(rBM, model, rBM_sol, sep_sol) + end + DP._set_solution_method(model, method) + DP._set_ready_to_optimize(model, true) + return +end + +# Add cut to both the flat rBM model and the original InfiniteModel. +function _add_infinite_cut( + rBM::DP.GDPSubmodel{<:Any, <:InfiniteOpt.GeneralVariableRef, <:Any}, + model::InfiniteOpt.InfiniteModel, + rBM_sol::Dict{<:JuMP.AbstractVariableRef, <:Vector{<:Number}}, + sep_sol::Dict{<:JuMP.AbstractVariableRef, <:Vector{<:Number}} + ) + cut_expr = zero(JuMP.GenericAffExpr{ + JuMP.value_type(typeof(rBM.model)), + JuMP.variable_ref_type(rBM.model)}) + for var in rBM.decision_vars + sub_vars = rBM.fwd_map[var] + rbm_vals = rBM_sol[var] + sep_vals = sep_sol[var] + for k in 1:length(sub_vars) + xi = 2 * (sep_vals[k] - rbm_vals[k]) + JuMP.add_to_expression!(cut_expr, xi, sub_vars[k]) + JuMP.add_to_expression!(cut_expr, -xi * sep_vals[k]) + end + end + JuMP.@constraint(rBM.model, cut_expr >= 0) + prefs, sups = _collect_parameters(model) + inf_terms = Any[] + cut_scalar = zero(JuMP.GenericAffExpr{ + JuMP.value_type(typeof(model)), + JuMP.variable_ref_type(model)}) + for var in rBM.decision_vars + haskey(rBM_sol, var) || continue + haskey(sep_sol, var) || continue + vprefs = InfiniteOpt.parameter_refs(var) + if isempty(vprefs) + xi = 2 * (sep_sol[var][1] - rBM_sol[var][1]) + sp = sep_sol[var][1] + cut_scalar += xi * (var - sp) + else + xi_vals = 2 .* (sep_sol[var] .- rBM_sol[var]) + sp_vals = sep_sol[var] + xi_pf = condense_to_pf(model, xi_vals, vprefs, sups) + sp_pf = condense_to_pf(model, sp_vals, vprefs, sups) + push!(inf_terms, xi_pf * var - xi_pf * sp_pf) + end + end + if !isempty(inf_terms) + inf_expr = JuMP.@expression(model, sum(inf_terms)) + for p in prefs + inf_expr = InfiniteOpt.integral(inf_expr, p) + end + cut_scalar += inf_expr + end + JuMP.@constraint(model, cut_scalar >= 0) + return +end + +end From 8fa5f3e184525665f05ba468ab6dcfe5fec576ef Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Mon, 20 Apr 2026 20:24:12 -0400 Subject: [PATCH 09/34] . --- ext/InfiniteDisjunctiveProgramming.jl | 272 +++++++------- src/mbm.jl | 22 +- test/constraints/mbm.jl | 14 +- .../InfiniteDisjunctiveProgramming.jl | 334 +++++++++++++++++- 4 files changed, 480 insertions(+), 162 deletions(-) diff --git a/ext/InfiniteDisjunctiveProgramming.jl b/ext/InfiniteDisjunctiveProgramming.jl index 831820f7..8f5cd1e0 100644 --- a/ext/InfiniteDisjunctiveProgramming.jl +++ b/ext/InfiniteDisjunctiveProgramming.jl @@ -8,14 +8,17 @@ import DisjunctiveProgramming as DP # MODEL ################################################################################ function DP.InfiniteGDPModel(args...; kwargs...) - return DP.GDPModel{InfiniteOpt.InfiniteModel, + return DP.GDPModel{ + InfiniteOpt.InfiniteModel, InfiniteOpt.GeneralVariableRef, - InfiniteOpt.InfOptConstraintRef}(args...; kwargs...) + InfiniteOpt.InfOptConstraintRef + }(args...; kwargs...) end function DP.collect_all_vars(model::InfiniteOpt.InfiniteModel) vars = JuMP.all_variables(model) - return append!(vars, InfiniteOpt.all_derivatives(model)) + derivs = InfiniteOpt.all_derivatives(model) + return append!(vars, derivs) end ################################################################################ @@ -38,19 +41,20 @@ end function DP.VariableProperties(vref::InfiniteOpt.GeneralVariableRef) info = DP.get_variable_info(vref) name = JuMP.name(vref) + set = nothing prefs = InfiniteOpt.parameter_refs(vref) var_type = !isempty(prefs) ? InfiniteOpt.Infinite(prefs...) : nothing - return DP.VariableProperties(info, name, nothing, var_type) + return DP.VariableProperties(info, name, set, var_type) end -# Extract parameter refs from expression, return VariableProperties with -# Infinite type. +# Extract parameter refs from expression and return VariableProperties with Infinite type function DP.VariableProperties( expr::Union{ JuMP.GenericAffExpr{C, InfiniteOpt.GeneralVariableRef}, JuMP.GenericQuadExpr{C, InfiniteOpt.GeneralVariableRef}, - JuMP.GenericNonlinearExpr{InfiniteOpt.GeneralVariableRef}} - ) where C + JuMP.GenericNonlinearExpr{InfiniteOpt.GeneralVariableRef} + } +) where C prefs = InfiniteOpt.parameter_refs(expr) info = DP._free_variable_info() var_type = !isempty(prefs) ? InfiniteOpt.Infinite(prefs...) : nothing @@ -62,8 +66,9 @@ function DP.VariableProperties( InfiniteOpt.GeneralVariableRef, JuMP.GenericAffExpr{<:Any, InfiniteOpt.GeneralVariableRef}, JuMP.GenericQuadExpr{<:Any, InfiniteOpt.GeneralVariableRef}, - JuMP.GenericNonlinearExpr{InfiniteOpt.GeneralVariableRef}}} - ) + JuMP.GenericNonlinearExpr{InfiniteOpt.GeneralVariableRef} + }} +) all_prefs = Set{InfiniteOpt.GeneralVariableRef}() for expr in exprs for pref in InfiniteOpt.parameter_refs(expr) @@ -85,8 +90,9 @@ end ################################################################################ function JuMP.add_constraint( model::InfiniteOpt.InfiniteModel, - c::JuMP.VectorConstraint{F, S}, name::String = "" - ) where {F, S <: DP.AbstractCardinalitySet} + c::JuMP.VectorConstraint{F, S}, + name::String = "" +) where {F, S <: DP.AbstractCardinalitySet} return DP._add_cardinality_constraint(model, c, name) end @@ -94,7 +100,7 @@ function JuMP.add_constraint( model::M, c::JuMP.ScalarConstraint{DP._LogicalExpr{M}, S}, name::String = "" - ) where {S, M <: InfiniteOpt.InfiniteModel} +) where {S, M <: InfiniteOpt.InfiniteModel} return DP._add_logical_constraint(model, c, name) end @@ -102,29 +108,28 @@ function JuMP.add_constraint( model::M, c::JuMP.ScalarConstraint{DP.LogicalVariableRef{M}, S}, name::String = "" - ) where {M <: InfiniteOpt.InfiniteModel, S} - error("Cannot define constraint on single logical variable, " * - "use `fix` instead.") +) where {M <: InfiniteOpt.InfiniteModel, S} + error("Cannot define constraint on single logical variable, use `fix` instead.") end function JuMP.add_constraint( model::M, c::JuMP.ScalarConstraint{ - JuMP.GenericAffExpr{C, DP.LogicalVariableRef{M}}, S}, + JuMP.GenericAffExpr{C, DP.LogicalVariableRef{M}}, S + }, name::String = "" - ) where {M <: InfiniteOpt.InfiniteModel, S, C} - error("Cannot add, subtract, or multiply with " * - "logical variables.") +) where {M <: InfiniteOpt.InfiniteModel, S, C} + error("Cannot add, subtract, or multiply with logical variables.") end function JuMP.add_constraint( model::M, c::JuMP.ScalarConstraint{ - JuMP.GenericQuadExpr{C, DP.LogicalVariableRef{M}}, S}, + JuMP.GenericQuadExpr{C, DP.LogicalVariableRef{M}}, S + }, name::String = "" - ) where {M <: InfiniteOpt.InfiniteModel, S, C} - error("Cannot add, subtract, or multiply with " * - "logical variables.") +) where {M <: InfiniteOpt.InfiniteModel, S, C} + error("Cannot add, subtract, or multiply with logical variables.") end ################################################################################ @@ -132,7 +137,7 @@ end ################################################################################ function DP.get_constant( expr::JuMP.GenericAffExpr{T, InfiniteOpt.GeneralVariableRef} - ) where {T} +) where {T} constant = JuMP.constant(expr) param_expr = zero(typeof(expr)) for (var, coeff) in expr.terms @@ -144,16 +149,16 @@ function DP.get_constant( end function DP.disaggregate_expression( - model::M, aff::JuMP.GenericAffExpr, + model::M, + aff::JuMP.GenericAffExpr, bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::DP._Hull - ) where {M <: InfiniteOpt.InfiniteModel} +) where {M <: InfiniteOpt.InfiniteModel} terms = Any[aff.constant * bvref] for (vref, coeff) in aff.terms if JuMP.is_binary(vref) push!(terms, coeff * vref) - elseif vref isa InfiniteOpt.GeneralVariableRef && - _is_parameter(vref) + elseif vref isa InfiniteOpt.GeneralVariableRef && _is_parameter(vref) push!(terms, coeff * vref * bvref) elseif !haskey(method.disjunct_variables, (vref, bvref)) push!(terms, coeff * vref) @@ -208,8 +213,8 @@ end # copy_model_with_constraints (build mini InfiniteModel + # transcribe to flat JuMP model), prepare_max_M_objective # (expand infinite constraint into K flat objectives via -# _build_flat_map), and aggregate_M_values (interpolate flat -# values to parameter function). +# _build_flat_map), and raw_M (vector dispatch aggregates +# K per-support M values into a parameter function). # Collect all parameter function refs from all disjunct constraints in # the model. @@ -221,8 +226,7 @@ function _all_param_functions( for cref in crefs cref isa DP.DisjunctConstraintRef || continue con = JuMP.constraint_object(cref) - for v in InfiniteOpt.all_expression_variables( - con.func) + for v in InfiniteOpt.all_expression_variables(con.func) dv = InfiniteOpt.dispatch_variable_ref(v) if dv isa InfiniteOpt.ParameterFunctionRef push!(pf_set, v) @@ -241,7 +245,7 @@ end function _build_flat_map( sub::DP.GDPSubmodel, k::Int, prefs::Vector{InfiniteOpt.GeneralVariableRef}, - supports::Dict{InfiniteOpt.GeneralVariableRef,Vector{Float64}}, + supports::Dict{InfiniteOpt.GeneralVariableRef, Vector{Float64}}, full_shape::Tuple, pf_set::Set{InfiniteOpt.GeneralVariableRef} ) @@ -281,7 +285,8 @@ function DP.copy_model_with_constraints( method::DP._MBM ) mini = InfiniteOpt.InfiniteModel() - ref_map = Dict{InfiniteOpt.GeneralVariableRef,InfiniteOpt.GeneralVariableRef}() + ref_map = Dict{InfiniteOpt.GeneralVariableRef, + InfiniteOpt.GeneralVariableRef}() # 1. Copy infinite parameters with their supports for p in InfiniteOpt.all_parameters(model) @@ -298,7 +303,8 @@ function DP.copy_model_with_constraints( prefs = InfiniteOpt.parameter_refs(v) var_type = isempty(prefs) ? nothing : InfiniteOpt.Infinite(Tuple(ref_map[p] for p in prefs)...) - props = DP.VariableProperties(DP.get_variable_info(v),"", nothing, var_type) + props = DP.VariableProperties( + DP.get_variable_info(v), "", nothing, var_type) ref_map[v] = DP.create_variable(mini, props) end @@ -334,10 +340,19 @@ function DP.copy_model_with_constraints( end # 6. Transcribe mini InfiniteModel to flat JuMP model - flat, tr_fwd = transcribe_to_flat(mini) + InfiniteOpt.build_transformation_backend!(mini) + flat = InfiniteOpt.transformation_model(mini) + tr_fwd = Dict{InfiniteOpt.GeneralVariableRef, + Vector{JuMP.VariableRef}}() + for v in DP.collect_all_vars(mini) + tv = InfiniteOpt.transformation_variable(v) + vprefs = InfiniteOpt.parameter_refs(v) + tr_fwd[v] = isempty(vprefs) ? [tv] : vec(tv) + end # 7. Remap fwd_map: original model var -> flat JuMP VarRef - fwd_map = Dict{InfiniteOpt.GeneralVariableRef, Vector{JuMP.VariableRef}}() + fwd_map = Dict{InfiniteOpt.GeneralVariableRef, + Vector{JuMP.VariableRef}}() for (orig, mapped) in ref_map _is_parameter(orig) && continue haskey(tr_fwd, mapped) || continue @@ -370,6 +385,7 @@ function DP.prepare_max_M_objective( end return objectives end + function DP.prepare_max_M_objective( model::InfiniteOpt.InfiniteModel, obj::JuMP.ScalarConstraint{T, S}, @@ -381,16 +397,16 @@ function DP.prepare_max_M_objective( pf_set = _all_param_functions(model) objectives = Vector{JuMP.AbstractJuMPScalar}(undef, K) for k in 1:K - flat_map = _build_flat_map(sub, k, prefs, supports,full_shape, pf_set) + flat_map = _build_flat_map(sub, k, prefs, supports, full_shape, pf_set) objectives[k] = obj.set.lower - DP._replace_variables_in_constraint(obj.func, flat_map) end return objectives end -# Solve the submodel for a vector of objectives (one per -# support point). Returns aggregated result or nothing. -function DP._raw_M( +# Solve the submodel for a vector of objectives (one per support point). +# Returns aggregated M value (scalar or parameter function) or nothing. +function DP.raw_M( sub::DP.GDPSubmodel, objectives::Vector{<:JuMP.AbstractJuMPScalar}, method::DP._MBM @@ -411,40 +427,8 @@ function DP._raw_M( push!(M_vals, method.default_M) end end - model = JuMP.owner_model( - first(keys(sub.fwd_map))) - return aggregate_M_values(model, M_vals) -end - -# Condense flat per-support values to final form (MBM path). -function aggregate_M_values( - model::InfiniteOpt.InfiniteModel, - vals::AbstractVector{<:Real} - ) - if all(==(vals[1]), vals) - return vals[1] - end - prefs, supports = _collect_parameters(model) - return condense_to_pf(model, vals, prefs, supports) -end - -# Interpolate flat per-support values into a parameter function. Computes -# grids/shape from supports, reshapes, interpolates, and registers on -# the model. -function condense_to_pf( - model::InfiniteOpt.InfiniteModel, - vals::AbstractVector{<:Real}, - prefs::Union{Vector{InfiniteOpt.GeneralVariableRef}, - Tuple{Vararg{InfiniteOpt.GeneralVariableRef}}}, - supports::Dict{InfiniteOpt.GeneralVariableRef, - Vector{Float64}} - ) - grids = Tuple(supports[p] for p in prefs) - shape = Tuple(length(supports[p]) for p in prefs) - nd = reshape(vals, shape) - fn = Interpolations.linear_interpolation(grids, nd, - extrapolation_bc = Interpolations.Line()) - return _make_parameter_function(model, fn, prefs...) + model = JuMP.owner_model(first(keys(sub.fwd_map))) + return _aggregate_M_values(model, M_vals) end ################################################################################ @@ -479,26 +463,49 @@ function _collect_parameters(model::InfiniteOpt.InfiniteModel) return prefs, supports end -# Transcribe an InfiniteModel to a flat JuMP.Model with forward variable -# map. Shared by MBM and CP paths. -function transcribe_to_flat(model::InfiniteOpt.InfiniteModel) - InfiniteOpt.build_transformation_backend!(model) - flat = InfiniteOpt.transformation_model(model) - fwd_map = Dict{InfiniteOpt.GeneralVariableRef, Vector{JuMP.VariableRef}}() - for v in DP.collect_all_vars(model) - tv = InfiniteOpt.transformation_variable(v) - vprefs = InfiniteOpt.parameter_refs(v) - fwd_map[v] = isempty(vprefs) ? [tv] : vec(tv) +# Condense flat per-support M values into a scalar or parameter function. +function _aggregate_M_values( + model::InfiniteOpt.InfiniteModel, + vals::AbstractVector{<:Real} + ) + if all(==(vals[1]), vals) + return vals[1] end - return flat, fwd_map + prefs, supports = _collect_parameters(model) + return values_to_parameter_function(model, vals, prefs, supports) +end + +""" + values_to_parameter_function( + model, vals, prefs, supports + ) + +Interpolate flat per-support values into an InfiniteOpt parameter +function registered on `model`. Builds a grid from `supports`, +reshapes `vals`, fits a linear interpolation, and returns the +registered parameter function ref. +""" +function values_to_parameter_function( + model::InfiniteOpt.InfiniteModel, + vals::AbstractVector{<:Real}, + prefs::Union{Vector{InfiniteOpt.GeneralVariableRef}, + Tuple{Vararg{InfiniteOpt.GeneralVariableRef}}}, + supports::Dict{InfiniteOpt.GeneralVariableRef, Vector{Float64}} + ) + grids = Tuple(supports[p] for p in prefs) + shape = Tuple(length(supports[p]) for p in prefs) + nd = reshape(vals, shape) + fn = Interpolations.linear_interpolation(grids, nd, + extrapolation_bc = Interpolations.Line()) + return _make_parameter_function(model, fn, prefs...) end ################################################################################ # CUTTING PLANES FOR INFINITEMODEL ################################################################################ -# Build CP subproblem: reformulate the InfiniteModel, transcribe to a flat -# JuMP copy, and wrap in GDPSubmodel with forward variable map. +# Build CP subproblem: reformulate the InfiniteModel in-place, transcribe +# to a flat JuMP copy, and wrap in GDPSubmodel with forward variable map. function DP.copy_and_reformulate( model::InfiniteOpt.InfiniteModel, decision_vars::Vector{InfiniteOpt.GeneralVariableRef}, @@ -506,9 +513,18 @@ function DP.copy_and_reformulate( method::DP.CuttingPlanes ) DP.reformulate_model(model, reform_method) - flat, tr_fwd = transcribe_to_flat(model) + InfiniteOpt.build_transformation_backend!(model) + flat = InfiniteOpt.transformation_model(model) + tr_fwd = Dict{InfiniteOpt.GeneralVariableRef, + Vector{JuMP.VariableRef}}() + for v in DP.collect_all_vars(model) + tv = InfiniteOpt.transformation_variable(v) + vprefs = InfiniteOpt.parameter_refs(v) + tr_fwd[v] = isempty(vprefs) ? [tv] : vec(tv) + end sub_copy, copy_map = JuMP.copy_model(flat) - fwd_map = Dict{InfiniteOpt.GeneralVariableRef, Vector{JuMP.VariableRef}}() + fwd_map = Dict{InfiniteOpt.GeneralVariableRef, + Vector{JuMP.VariableRef}}() for v in decision_vars haskey(tr_fwd, v) || continue fwd_map[v] = [copy_map[tv] for tv in tr_fwd[v]] @@ -519,70 +535,47 @@ function DP.copy_and_reformulate( return sub end -# Full CP loop for InfiniteModel: cannot solve in-place, so both SEP -# and rBM are separate transcribed flat copies. -function DP.reformulate_model( - model::InfiniteOpt.InfiniteModel, - method::DP.CuttingPlanes - ) - decision_vars = DP.collect_cutting_planes_vars(model) - separation = DP.copy_and_reformulate( - model, decision_vars, DP.Hull(), method) - JuMP.relax_integrality(separation.model) - rBM = DP.copy_and_reformulate( - model, decision_vars, DP.BigM(method.M_value), method) - JuMP.relax_integrality(rBM.model) - for iter in 1:method.max_iter - JuMP.optimize!(rBM.model, ignore_optimize_hook = true) - rBM_sol = DP.extract_solution(rBM) - sep_obj, sep_sol = DP._solve_separation(separation, rBM_sol) - sep_obj <= method.seperation_tolerance && break - _add_infinite_cut(rBM, model, rBM_sol, sep_sol) +# Extract per-support-point solutions from the InfiniteOpt transformation +# backend after optimize!(model, ignore_optimize_hook=true). +function DP.extract_solution(model::InfiniteOpt.InfiniteModel) + dvars = DP.collect_cutting_planes_vars(model) + V = eltype(dvars) + T = JuMP.value_type(typeof(model)) + sol = Dict{V, Vector{T}}() + for v in dvars + tv = InfiniteOpt.transformation_variable(v) + vprefs = InfiniteOpt.parameter_refs(v) + sol[v] = isempty(vprefs) ? [JuMP.value(tv)] : JuMP.value.(vec(tv)) end - DP._set_solution_method(model, method) - DP._set_ready_to_optimize(model, true) - return + return sol end -# Add cut to both the flat rBM model and the original InfiniteModel. -function _add_infinite_cut( - rBM::DP.GDPSubmodel{<:Any, <:InfiniteOpt.GeneralVariableRef, <:Any}, +# Add an infinite-form cut to the InfiniteModel. Infinite variables get +# interpolated parameter function coefficients wrapped in integrals; +# finite variables contribute scalar terms. +function DP.add_cut( model::InfiniteOpt.InfiniteModel, + decision_vars::Vector{InfiniteOpt.GeneralVariableRef}, rBM_sol::Dict{<:JuMP.AbstractVariableRef, <:Vector{<:Number}}, sep_sol::Dict{<:JuMP.AbstractVariableRef, <:Vector{<:Number}} ) - cut_expr = zero(JuMP.GenericAffExpr{ - JuMP.value_type(typeof(rBM.model)), - JuMP.variable_ref_type(rBM.model)}) - for var in rBM.decision_vars - sub_vars = rBM.fwd_map[var] - rbm_vals = rBM_sol[var] - sep_vals = sep_sol[var] - for k in 1:length(sub_vars) - xi = 2 * (sep_vals[k] - rbm_vals[k]) - JuMP.add_to_expression!(cut_expr, xi, sub_vars[k]) - JuMP.add_to_expression!(cut_expr, -xi * sep_vals[k]) - end - end - JuMP.@constraint(rBM.model, cut_expr >= 0) prefs, sups = _collect_parameters(model) inf_terms = Any[] - cut_scalar = zero(JuMP.GenericAffExpr{ - JuMP.value_type(typeof(model)), - JuMP.variable_ref_type(model)}) - for var in rBM.decision_vars + cut_scalar = DP._zero_aff(model) + for var in decision_vars haskey(rBM_sol, var) || continue haskey(sep_sol, var) || continue vprefs = InfiniteOpt.parameter_refs(var) if isempty(vprefs) xi = 2 * (sep_sol[var][1] - rBM_sol[var][1]) - sp = sep_sol[var][1] - cut_scalar += xi * (var - sp) + cut_scalar += xi * (var - sep_sol[var][1]) else xi_vals = 2 .* (sep_sol[var] .- rBM_sol[var]) sp_vals = sep_sol[var] - xi_pf = condense_to_pf(model, xi_vals, vprefs, sups) - sp_pf = condense_to_pf(model, sp_vals, vprefs, sups) + xi_pf = values_to_parameter_function( + model, xi_vals, vprefs, sups) + sp_pf = values_to_parameter_function( + model, sp_vals, vprefs, sups) push!(inf_terms, xi_pf * var - xi_pf * sp_pf) end end @@ -593,7 +586,8 @@ function _add_infinite_cut( end cut_scalar += inf_expr end - JuMP.@constraint(model, cut_scalar >= 0) + cref = JuMP.@constraint(model, cut_scalar >= 0) + push!(DP._reformulation_constraints(model), cref) return end diff --git a/src/mbm.jl b/src/mbm.jl index 973c0519..8d8fc1cf 100644 --- a/src/mbm.jl +++ b/src/mbm.jl @@ -242,7 +242,7 @@ end prepare_max_M_objective(model, obj::ScalarConstraint, sub::GDPSubmodel) Convert a constraint into an objective expression for M-value -maximization. Returns a single JuMP expression to pass to `_raw_M`. +maximization. Returns a single JuMP expression to pass to `raw_M`. """ function prepare_max_M_objective( ::JuMP.AbstractModel, @@ -266,7 +266,7 @@ end # Solve the submodel for a single objective expression. # Returns a scalar M value, or nothing if infeasible. -function _raw_M( +function raw_M( sub::GDPSubmodel, objective::JuMP.AbstractJuMPScalar, method::_MBM @@ -291,7 +291,7 @@ function _maximize_M( method::_MBM ) where {T, S <: Union{_MOI.LessThan, _MOI.GreaterThan}} sub = _get_submodel(model, constraints, method) - return _raw_M(sub, + return raw_M(sub, prepare_max_M_objective(model, objective, sub), method) end @@ -321,8 +321,8 @@ function _maximize_M( set_value = objective.set.value ge_obj = JuMP.ScalarConstraint(objective.func, MOI.GreaterThan(set_value)) le_obj = JuMP.ScalarConstraint(objective.func, MOI.LessThan(set_value)) - raw_lower = _raw_M(sub, prepare_max_M_objective(model, ge_obj, sub), method) - raw_upper = _raw_M(sub, prepare_max_M_objective(model, le_obj, sub), method) + raw_lower = raw_M(sub, prepare_max_M_objective(model, ge_obj, sub), method) + raw_upper = raw_M(sub, prepare_max_M_objective(model, le_obj, sub), method) (raw_lower === nothing || raw_upper === nothing) && return nothing return [raw_lower, raw_upper] @@ -341,8 +341,8 @@ function _maximize_M( MOI.GreaterThan(set_values[1])) le_obj = JuMP.ScalarConstraint(objective.func, MOI.LessThan(set_values[2])) - raw_lower = _raw_M(sub, prepare_max_M_objective(model, ge_obj, sub), method) - raw_upper = _raw_M(sub, prepare_max_M_objective(model, le_obj, sub), method) + raw_lower = raw_M(sub, prepare_max_M_objective(model, ge_obj, sub), method) + raw_upper = raw_M(sub, prepare_max_M_objective(model, le_obj, sub), method) (raw_lower === nothing || raw_upper === nothing) && return nothing return [raw_lower, raw_upper] @@ -361,7 +361,7 @@ function _maximize_M( for i in 1:objective.set.dimension le_obj = JuMP.ScalarConstraint( objective.func[i], MOI.LessThan(zero(val_type))) - raw = _raw_M(sub, + raw = raw_M(sub, prepare_max_M_objective(model, le_obj, sub), method) raw === nothing && return nothing @@ -384,7 +384,7 @@ function _maximize_M( ge_obj = JuMP.ScalarConstraint( objective.func[i], MOI.GreaterThan(zero(val_type))) - raw = _raw_M(sub, + raw = raw_M(sub, prepare_max_M_objective(model, ge_obj, sub), method) raw === nothing && return nothing @@ -410,10 +410,10 @@ function _maximize_M( le_obj = JuMP.ScalarConstraint( objective.func[i], MOI.LessThan(zero(val_type))) - raw_ge = _raw_M(sub, + raw_ge = raw_M(sub, prepare_max_M_objective(model, ge_obj, sub), method) - raw_le = _raw_M(sub, + raw_le = raw_M(sub, prepare_max_M_objective(model, le_obj, sub), method) (raw_ge === nothing || raw_le === nothing) && diff --git a/test/constraints/mbm.jl b/test/constraints/mbm.jl index d181c082..80f9a27f 100644 --- a/test/constraints/mbm.jl +++ b/test/constraints/mbm.jl @@ -134,19 +134,19 @@ function test_raw_M() DisjunctConstraintRef[con2], mbm) obj = DP.prepare_max_M_objective(model, constraint_object(con), sub) - @test DP._raw_M(sub, obj, mbm) == 0.0 + @test DP.raw_M(sub, obj, mbm) == 0.0 set_upper_bound(x, 1) sub2 = DP.copy_model_with_constraints(model, DisjunctConstraintRef[con], mbm) obj2 = DP.prepare_max_M_objective(model, constraint_object(con2), sub2) - @test DP._raw_M(sub2, obj2, mbm) == 15 + @test DP.raw_M(sub2, obj2, mbm) == 15 set_integer(y) @constraint(model, con3, y*x == 15, Disjunct(Y[1])) obj3 = DP.prepare_max_M_objective(model, constraint_object(con2), sub2) - @test DP._raw_M(sub2, obj3, mbm) == 15 + @test DP.raw_M(sub2, obj3, mbm) == 15 # Fresh _MBM after changing bounds JuMP.fix(y, 5; force=true) mbm2 = DP._MBM( @@ -155,7 +155,7 @@ function test_raw_M() DisjunctConstraintRef[con], mbm2) obj4 = DP.prepare_max_M_objective(model, constraint_object(con2), sub3) - @test DP._raw_M(sub3, obj4, mbm2) == 10 + @test DP.raw_M(sub3, obj4, mbm2) == 10 # Infeasible region → nothing delete_lower_bound(x) mbm3 = DP._MBM( @@ -164,7 +164,7 @@ function test_raw_M() DisjunctConstraintRef[con2], mbm3) obj5 = DP.prepare_max_M_objective(model, constraint_object(con2), sub4) - @test DP._raw_M(sub4, obj5, mbm3) == nothing + @test DP.raw_M(sub4, obj5, mbm3) == nothing # infeasible (x >= 100 but x <= 1) set_upper_bound(x, 1) @@ -175,7 +175,7 @@ function test_raw_M() mbm4) obj6 = DP.prepare_max_M_objective(model, constraint_object(con), sub5) - @test DP._raw_M(sub5, obj6, mbm4) == nothing + @test DP.raw_M(sub5, obj6, mbm4) == nothing # Unbounded subproblem → default_M fallback. # No lower bound on x means max(5 - x) s.t. x <= 3 @@ -191,7 +191,7 @@ function test_raw_M() DisjunctConstraintRef[ub_con1], mbm_ub) obj_ub = DP.prepare_max_M_objective(model_ub, constraint_object(ub_con2), sub_ub) - @test DP._raw_M(sub_ub, obj_ub, mbm_ub) == mbm_ub.default_M + @test DP.raw_M(sub_ub, obj_ub, mbm_ub) == mbm_ub.default_M end function test_maximize_M() diff --git a/test/extensions/InfiniteDisjunctiveProgramming.jl b/test/extensions/InfiniteDisjunctiveProgramming.jl index 8ba1e5c2..e2dda56c 100644 --- a/test/extensions/InfiniteDisjunctiveProgramming.jl +++ b/test/extensions/InfiniteDisjunctiveProgramming.jl @@ -1,4 +1,4 @@ -using InfiniteOpt, HiGHS, Ipopt, Juniper +using InfiniteOpt, HiGHS, Ipopt, Juniper, Interpolations import DisjunctiveProgramming as DP # Helper to access internal function @@ -77,6 +77,24 @@ function test__is_parameter() @test IDP._is_parameter(y) == false end +# _is_parameter on unwrapped concrete dispatch types. Covers +# ext lines 28-32 (DependentParameterRef, IndependentParameterRef, +# FiniteParameterRef, ParameterFunctionRef, Any fallback). +function test__is_parameter_concrete_dispatches() + model = InfiniteGDPModel() + @infinite_parameter(model, t ∈ [0, 1]) + @infinite_parameter(model, s[1:2] ∈ [0, 1], independent = true) + @finite_parameter(model, p == 1.0) + @variable(model, x, Infinite(t)) + @parameter_function(model, pf == t -> 2*t) + dvr = InfiniteOpt.dispatch_variable_ref + @test IDP._is_parameter(dvr(t)) == true # Dependent + @test IDP._is_parameter(dvr(s[1])) == true # Independent + @test IDP._is_parameter(dvr(p)) == true # Finite + @test IDP._is_parameter(dvr(pf)) == true # ParamFunc + @test IDP._is_parameter(dvr(x)) == false # Any +end + function test_requires_disaggregation() model = InfiniteGDPModel() @infinite_parameter(model, t ∈ [0, 1]) @@ -169,6 +187,43 @@ function test_disaggregate_expression_infiniteopt() @test haskey(result_not_disagg.terms, y) end +function test_disaggregate_quad_expression_infiniteopt() + model = InfiniteGDPModel() + @infinite_parameter(model, t ∈ [0, 1]) + @variable(model, 0 <= x <= 10, Infinite(t)) + @variable(model, 0 <= y <= 5, Infinite(t)) + @variable(model, z, InfiniteLogical(t)) + + bvrefs = DP._indicator_to_binary(model) + bvref = bvrefs[z] + + vrefs = Set([x, y]) + DP._variable_bounds(model)[x] = DP.set_variable_bound_info(x, Hull()) + DP._variable_bounds(model)[y] = DP.set_variable_bound_info(y, Hull()) + method = DP._Hull(Hull(1e-3), vrefs) + DP._disaggregate_variables(model, z, vrefs, method) + + # var × var → nonlinear (perspective divides by y) + quad_vv = @expression(model, x * y) + result_vv = DP.disaggregate_expression(model, quad_vv, bvref, method) + @test result_vv isa JuMP.GenericNonlinearExpr + + # param × var → quadratic (param * disaggregated) + quad_pv = @expression(model, t * x) + result_pv = DP.disaggregate_expression(model, quad_pv, bvref, method) + @test result_pv isa JuMP.GenericQuadExpr + + # var × param → quadratic (disaggregated * param) + quad_vp = @expression(model, x * t) + result_vp = DP.disaggregate_expression(model, quad_vp, bvref, method) + @test result_vp isa JuMP.GenericQuadExpr + + # param × param → cubic (t * t * bvref) + quad_pp = @expression(model, t * t) + result_pp = DP.disaggregate_expression(model, quad_pp, bvref, method) + @test result_pp isa JuMP.GenericNonlinearExpr +end + function test_variable_properties_infiniteopt() model = InfiniteGDPModel() @infinite_parameter(model, t ∈ [0, 1]) @@ -336,10 +391,110 @@ function test_logical_value() @test eltype(val) == Bool end -function test_unsupported_methods_error() +# _collect_parameters on model with no infinite parameters. +# Covers ext line 508. +function test__collect_parameters_no_params() + model = InfiniteGDPModel() + @test_throws ErrorException IDP._collect_parameters(model) +end + +# MBM with finite + integer variables in InfiniteModel. Covers +# copy_model_with_constraints (finite var, set_integer), +# and _build_flat_map line 252 (finite var path). +function test_mbm_finite_and_integer_var() model = InfiniteGDPModel(HiGHS.Optimizer) - @test_throws ErrorException DP.reformulate_model(model, MBM(HiGHS.Optimizer)) - @test_throws ErrorException DP.reformulate_model(model, CuttingPlanes(HiGHS.Optimizer)) + set_silent(model) + @infinite_parameter(model, t ∈ [0, 1], num_supports = 10) + @variable(model, 0 <= x <= 10, Infinite(t)) + @variable(model, 0 <= w <= 5, Int) + @variable(model, Y[1:2], InfiniteLogical(t)) + @constraint(model, x + w >= 5, Disjunct(Y[1])) + @constraint(model, x + w <= 3, Disjunct(Y[2])) + @disjunction(model, Y) + @objective(model, Min, ∫(x, t) + w) + @test optimize!(model, + gdp_method = MBM(HiGHS.Optimizer)) isa Nothing + @test termination_status(model) in + [MOI.OPTIMAL, MOI.LOCALLY_SOLVED] +end + +function test_mbm_infinite_simple() + model = InfiniteGDPModel(HiGHS.Optimizer) + set_silent(model) + + @infinite_parameter(model, t ∈ [0, 1], num_supports = 10) + @variable(model, 0 <= x <= 10, Infinite(t)) + @variable(model, Y[1:2], InfiniteLogical(t)) + + @constraint(model, x >= 5, Disjunct(Y[1])) + @constraint(model, x <= 3, Disjunct(Y[2])) + @disjunction(model, Y) + + @objective(model, Min, ∫(x, t)) + + @test optimize!(model, gdp_method = MBM(HiGHS.Optimizer)) isa Nothing + @test termination_status(model) in + [MOI.OPTIMAL, MOI.LOCALLY_SOLVED] + # x=0 with disjunct 2 active (x <= 3) gives min + @test objective_value(model) ≈ 0.0 atol = 0.1 +end + +function test_mbm_infinite_param_dependent() + model = InfiniteGDPModel(HiGHS.Optimizer) + set_silent(model) + + @infinite_parameter(model, t ∈ [0, 1], num_supports = 20) + @variable(model, -10 <= x <= 10, Infinite(t)) + @variable(model, Y[1:2], InfiniteLogical(t)) + + # Parameter-dependent constraints: + # Disjunct 1: x(t) <= 2*t + # Disjunct 2: x(t) >= 1 - t + @parameter_function(model, f1 == t -> 2*t) + @parameter_function(model, f2 == t -> 1 - t) + @constraint(model, x <= f1, Disjunct(Y[1])) + @constraint(model, x >= f2, Disjunct(Y[2])) + @disjunction(model, Y) + + @objective(model, Min, ∫(x, t)) + + @test optimize!(model, gdp_method = MBM(HiGHS.Optimizer)) isa Nothing + @test termination_status(model) in + [MOI.OPTIMAL, MOI.LOCALLY_SOLVED] +end + +function test_mbm_vs_bigm_infinite() + # Compare MBM and BigM: should give same + # feasible set and optimal value. + for method_pair in [ + (BigM(100), MBM(HiGHS.Optimizer)) + ] + model1 = InfiniteGDPModel(HiGHS.Optimizer) + set_silent(model1) + @infinite_parameter(model1, t ∈ [0, 1], num_supports = 10) + @variable(model1, 0 <= x1 <= 10, Infinite(t)) + @variable(model1, Y1[1:2], InfiniteLogical(t)) + @constraint(model1, x1 >= 5, Disjunct(Y1[1])) + @constraint(model1, x1 <= 3, Disjunct(Y1[2])) + @disjunction(model1, Y1) + @objective(model1, Min, ∫(x1, t)) + optimize!(model1, gdp_method = method_pair[1]) + obj1 = objective_value(model1) + + model2 = InfiniteGDPModel(HiGHS.Optimizer) + set_silent(model2) + @infinite_parameter(model2, t2 ∈ [0, 1], num_supports = 10) + @variable(model2, 0 <= x2 <= 10, Infinite(t2)) + @variable(model2, Y2[1:2], InfiniteLogical(t2)) + @constraint(model2, x2 >= 5, Disjunct(Y2[1])) + @constraint(model2, x2 <= 3, Disjunct(Y2[2])) + @disjunction(model2, Y2) + @objective(model2, Min, ∫(x2, t2)) + optimize!(model2, gdp_method = method_pair[2]) + obj2 = objective_value(model2) + + @test obj1 ≈ obj2 atol = 0.5 + end end function test_methods() @@ -393,6 +548,154 @@ function test_methods() @test value(z) ≈ expected_z atol=tol end +function test_mbm_with_derivatives() + model = InfiniteGDPModel(HiGHS.Optimizer) + set_silent(model) + + @infinite_parameter(model, t ∈ [0, 1], num_supports = 10) + @variable(model, -5 <= x <= 5, Infinite(t)) + @variable(model, Y[1:2], InfiniteLogical(t)) + + @constraint(model, ∂(x, t) >= 1, Disjunct(Y[1])) + @constraint(model, ∂(x, t) <= -1, Disjunct(Y[2])) + @disjunction(model, Y) + + set_upper_bound(∂(x, t), 10) + set_lower_bound(∂(x, t), -10) + + @objective(model, Min, ∫(x^2, t)) + + juniper = JuMP.optimizer_with_attributes( + Juniper.Optimizer, + "nl_solver" => JuMP.optimizer_with_attributes( + Ipopt.Optimizer, "print_level" => 0), + "log_levels" => [] + ) + set_optimizer(model, juniper) + @test optimize!(model, gdp_method = MBM(juniper)) isa Nothing + @test termination_status(model) in + [MOI.OPTIMAL, MOI.LOCALLY_SOLVED, + MOI.ALMOST_LOCALLY_SOLVED] +end + +function test_CuttingPlanes_infinite_simple() + model = InfiniteGDPModel(HiGHS.Optimizer) + set_silent(model) + + @infinite_parameter(model, t ∈ [0, 1], num_supports = 10) + @variable(model, 0 <= x <= 10, Infinite(t)) + @variable(model, Y[1:2], InfiniteLogical(t)) + + @constraint(model, x >= 5, Disjunct(Y[1])) + @constraint(model, x <= 3, Disjunct(Y[2])) + @disjunction(model, Y) + + @objective(model, Min, ∫(x, t)) + + # Should not throw + @test optimize!(model, + gdp_method = CuttingPlanes( + HiGHS.Optimizer; max_iter = 5) + ) isa Nothing + @test termination_status(model) in + [MOI.OPTIMAL, MOI.LOCALLY_SOLVED] +end + +function test_CuttingPlanes_infinite_two_disj() + model = InfiniteGDPModel(HiGHS.Optimizer) + set_silent(model) + + @infinite_parameter(model, t ∈ [0, 1], num_supports = 10) + @variable(model, 0 <= x[1:2] <= 10, Infinite(t)) + @variable(model, W1[1:2], InfiniteLogical(t)) + @variable(model, W2[1:2], InfiniteLogical(t)) + + @constraint(model, x[1] >= 2, Disjunct(W1[1])) + @constraint(model, x[1] <= 1, Disjunct(W1[2])) + @disjunction(model, W1) + + @constraint(model, x[2] >= 3, Disjunct(W2[1])) + @constraint(model, x[2] <= 2, Disjunct(W2[2])) + @disjunction(model, W2) + + @objective(model, Min, ∫(x[1] + x[2], t)) + + # Compare cutting planes vs BigM + optimize!(model, + gdp_method = CuttingPlanes( + HiGHS.Optimizer; max_iter = 10) + ) + cp_obj = objective_value(model) + + model2 = InfiniteGDPModel(HiGHS.Optimizer) + set_silent(model2) + @infinite_parameter(model2, t2 ∈ [0, 1], num_supports = 10) + @variable(model2, 0 <= x2[1:2] <= 10, Infinite(t2)) + @variable(model2, V1[1:2], InfiniteLogical(t2)) + @variable(model2, V2[1:2], InfiniteLogical(t2)) + @constraint(model2, x2[1] >= 2, Disjunct(V1[1])) + @constraint(model2, x2[1] <= 1, Disjunct(V1[2])) + @disjunction(model2, V1) + @constraint(model2, x2[2] >= 3, Disjunct(V2[1])) + @constraint(model2, x2[2] <= 2, Disjunct(V2[2])) + @disjunction(model2, V2) + @objective(model2, Min, ∫(x2[1] + x2[2], t2)) + optimize!(model2, gdp_method = BigM()) + bigm_obj = objective_value(model2) + + @test cp_obj ≈ bigm_obj atol = 1.0 +end + + + +function test_CuttingPlanes_with_cuts() + # Maximization with single-constraint disjuncts where Hull + # is strictly tighter than BigM. BigM allows x+y up to + # variable bounds (20), Hull limits to max(5,8)=8. This + # forces cuts. Finite var w exercises isempty(vprefs) + # branch in add_original_model_cut (line 779). + model = InfiniteGDPModel(HiGHS.Optimizer) + set_silent(model) + @infinite_parameter(model, t ∈ [0, 1], num_supports = 10) + @variable(model, 0 <= x <= 10, Infinite(t)) + @variable(model, 0 <= y <= 10, Infinite(t)) + @variable(model, 0 <= w <= 10) + @variable(model, Y[1:2], InfiniteLogical(t)) + @constraint(model, x + y <= 5, Disjunct(Y[1])) + @constraint(model, x + y <= 8, Disjunct(Y[2])) + @disjunction(model, Y) + @objective(model, Max, ∫(x + y, t) + w) + cutting_planes = CuttingPlanes(HiGHS.Optimizer; + max_iter = 30, seperation_tolerance = 1e-6) + @test optimize!(model, gdp_method = cutting_planes) isa Nothing + @test termination_status(model) in + [MOI.OPTIMAL, MOI.LOCALLY_SOLVED] +end + +function test_CuttingPlanes_multiparameter() + model = InfiniteGDPModel(HiGHS.Optimizer) + set_silent(model) + + @infinite_parameter(model, t ∈ [0, 1], num_supports = 5) + @infinite_parameter(model, s ∈ [0, 2], num_supports = 4) + @variable(model, 0 <= x <= 10, Infinite(t, s)) + @variable(model, Y[1:2], InfiniteLogical(t, s)) + + @constraint(model, x >= 5, Disjunct(Y[1])) + @constraint(model, x <= 3, Disjunct(Y[2])) + @disjunction(model, Y) + + @objective(model, Min, ∫(∫(x, t), s)) + + # Should not throw + @test optimize!(model, + gdp_method = CuttingPlanes( + HiGHS.Optimizer; max_iter = 5) + ) isa Nothing + @test termination_status(model) in + [MOI.OPTIMAL, MOI.LOCALLY_SOLVED] +end + @testset "InfiniteDisjunctiveProgramming" begin @testset "Model" begin @@ -406,6 +709,7 @@ end @testset "Variables" begin test_infinite_logical() test__is_parameter() + test__is_parameter_concrete_dispatches() test_requires_disaggregation() test_variable_properties_infiniteopt() test_variable_properties_from_expr() @@ -429,7 +733,19 @@ end @testset "Methods" begin test_get_constant() test_disaggregate_expression_infiniteopt() - test_unsupported_methods_error() + test_disaggregate_quad_expression_infiniteopt() + end + + @testset "Internal Helpers" begin + test__collect_parameters_no_params() + end + + @testset "MBM" begin + test_mbm_finite_and_integer_var() + test_mbm_infinite_simple() + test_mbm_infinite_param_dependent() + test_mbm_vs_bigm_infinite() + test_mbm_with_derivatives() end @testset "Integration" begin @@ -437,4 +753,12 @@ end test_methods() end + @testset "Cutting Planes" begin + test_CuttingPlanes_infinite_simple() + test_CuttingPlanes_infinite_two_disj() + test_CuttingPlanes_with_cuts() + test_CuttingPlanes_multiparameter() + end + + end \ No newline at end of file From ab3370bb2be7310ba20a020a61f8880f8bd92d6d Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Mon, 20 Apr 2026 21:55:45 -0400 Subject: [PATCH 10/34] . --- Project.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 3346659a..ae0dad94 100644 --- a/Project.toml +++ b/Project.toml @@ -27,10 +27,11 @@ julia = "1.10" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" +InfiniteOpt = "20393b10-9daf-11e9-18c9-8db751c92c57" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" Juniper = "2ddba703-00a4-53a7-87a5-e8b9971dde84" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Aqua", "HiGHS", "Test", "Juniper", "Ipopt", "Interpolations"] +test = ["Aqua", "HiGHS", "InfiniteOpt", "Test", "Juniper", "Ipopt", "Interpolations"] From 65e743f4e25aba7a27f6c6e266a6ae7562947c9e Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Tue, 21 Apr 2026 10:23:19 -0400 Subject: [PATCH 11/34] . --- ext/InfiniteDisjunctiveProgramming.jl | 9 +++++---- src/extension_api.jl | 10 +++++----- src/utilities.jl | 13 ------------- test/extensions/InfiniteDisjunctiveProgramming.jl | 3 +-- 4 files changed, 11 insertions(+), 24 deletions(-) diff --git a/ext/InfiniteDisjunctiveProgramming.jl b/ext/InfiniteDisjunctiveProgramming.jl index 8f5cd1e0..157ebddc 100644 --- a/ext/InfiniteDisjunctiveProgramming.jl +++ b/ext/InfiniteDisjunctiveProgramming.jl @@ -297,7 +297,7 @@ function DP.copy_model_with_constraints( ref_map[p] = new_p end - # 2. Copy decision variables with bounds (skip parameters) + # 2. Copy decision variables with bounds for v in JuMP.all_variables(model) _is_parameter(v) && continue prefs = InfiniteOpt.parameter_refs(v) @@ -351,8 +351,7 @@ function DP.copy_model_with_constraints( end # 7. Remap fwd_map: original model var -> flat JuMP VarRef - fwd_map = Dict{InfiniteOpt.GeneralVariableRef, - Vector{JuMP.VariableRef}}() + fwd_map = Dict{InfiniteOpt.GeneralVariableRef, Vector{JuMP.VariableRef}}() for (orig, mapped) in ref_map _is_parameter(orig) && continue haskey(tr_fwd, mapped) || continue @@ -561,7 +560,9 @@ function DP.add_cut( ) prefs, sups = _collect_parameters(model) inf_terms = Any[] - cut_scalar = DP._zero_aff(model) + cut_scalar = zero(JuMP.GenericAffExpr{ + JuMP.value_type(typeof(model)), + InfiniteOpt.GeneralVariableRef}) for var in decision_vars haskey(rBM_sol, var) || continue haskey(sep_sol, var) || continue diff --git a/src/extension_api.jl b/src/extension_api.jl index 6d046450..ad3566f6 100644 --- a/src/extension_api.jl +++ b/src/extension_api.jl @@ -1,8 +1,8 @@ """ InfiniteGDPModel(args...; kwargs...) -Creates an `InfiniteOpt.InfiniteModel` that is compatible with the -capabiltiies provided by DisjunctiveProgramming.jl. This requires +Creates an `InfiniteOpt.InfiniteModel` that is compatible with the +capabiltiies provided by DisjunctiveProgramming.jl. This requires that InfiniteOpt be imported first. **Example** @@ -18,9 +18,9 @@ function InfiniteGDPModel end """ InfiniteLogical(prefs...) -Allows users to create infinite logical variables. This is a tag -for the `@variable` macro that is a combination of `InfiniteOpt.Infinite` -and `DisjunctiveProgramming.Logical`. This requires that InfiniteOpt be +Allows users to create infinite logical variables. This is a tag +for the `@variable` macro that is a combination of `InfiniteOpt.Infinite` +and `DisjunctiveProgramming.Logical`. This requires that InfiniteOpt be first imported. **Example** diff --git a/src/utilities.jl b/src/utilities.jl index 69d3f4ca..b0203d70 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -129,19 +129,6 @@ function get_constant(expr::JuMP.AbstractVariableRef) return zero(JuMP.value_type(typeof(JuMP.owner_model(expr)))) end -################################################################################ -# ZERO EXPRESSION CONSTRUCTORS -################################################################################ -# Create a type-correct zero affine expression for the model. -_zero_aff(model::JuMP.AbstractModel) = zero( - JuMP.GenericAffExpr{JuMP.value_type(typeof(model)), - JuMP.variable_ref_type(model)}) - -# Create a type-correct zero quadratic expression for the model. -_zero_quad(model::JuMP.AbstractModel) = zero( - JuMP.GenericQuadExpr{JuMP.value_type(typeof(model)), - JuMP.variable_ref_type(model)}) - ################################################################################ # MODEL COPYING ################################################################################ diff --git a/test/extensions/InfiniteDisjunctiveProgramming.jl b/test/extensions/InfiniteDisjunctiveProgramming.jl index e2dda56c..22f870a9 100644 --- a/test/extensions/InfiniteDisjunctiveProgramming.jl +++ b/test/extensions/InfiniteDisjunctiveProgramming.jl @@ -760,5 +760,4 @@ end test_CuttingPlanes_multiparameter() end - -end \ No newline at end of file +end From 882390e4abad398f35532f5ecdaef2839446ea37 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Tue, 21 Apr 2026 12:20:27 -0400 Subject: [PATCH 12/34] . --- ext/InfiniteDisjunctiveProgramming.jl | 108 ++++++++------------------ 1 file changed, 34 insertions(+), 74 deletions(-) diff --git a/ext/InfiniteDisjunctiveProgramming.jl b/ext/InfiniteDisjunctiveProgramming.jl index 157ebddc..7ad421c7 100644 --- a/ext/InfiniteDisjunctiveProgramming.jl +++ b/ext/InfiniteDisjunctiveProgramming.jl @@ -255,7 +255,7 @@ function _build_flat_map( flat_map = Dict{InfiniteOpt.GeneralVariableRef, Any}() for (v, ws) in sub.fwd_map if length(ws) == 1 - flat_map[v] = ws[1] + flat_map[v] = only(ws) else vp = InfiniteOpt.parameter_refs(v) shape = Tuple(length(supports[p]) for p in vp) @@ -427,25 +427,31 @@ function DP.raw_M( end end model = JuMP.owner_model(first(keys(sub.fwd_map))) - return _aggregate_M_values(model, M_vals) + # Condense flat per-support values: scalar if uniform, else pf. + all(==(M_vals[1]), M_vals) && return M_vals[1] + prefs, supports = _collect_parameters(model) + grids = Tuple(supports[p] for p in prefs) + shape = Tuple(length(supports[p]) for p in prefs) + fn = Interpolations.linear_interpolation( + grids, reshape(M_vals, shape), + extrapolation_bc = Interpolations.Line()) + return _make_parameter_function(model, fn, prefs...) end ################################################################################ # TRANSCRIPTION HELPERS ################################################################################ -# Create a parameter function programmatically. Uses -# build_parameter_function + add_parameter_function (the lower-level -# API behind @parameter_function) since the macro doesn't support -# programmatic use. The closure wrapper handles non-Function callables -# like Interpolations.Extrapolation. Accepts any number of prefs via -# varargs: _make_parameter_function(m, f, t) for 1D, (m, f, t, x) for 2D. +# Replacement for @parameter_function in the case of using an interpolation. +# Example (1D interpolation): +# fn = Interpolations.linear_interpolation(grids, vals) +# pf = _make_parameter_function(model, fn, t) # returns a pf ref function _make_parameter_function( model::InfiniteOpt.InfiniteModel, fn, prefs::InfiniteOpt.GeneralVariableRef... ) f = fn isa Function ? fn : ((args...) -> fn(args...)) - pref_arg = length(prefs) == 1 ? prefs[1] : prefs + pref_arg = length(prefs) == 1 ? only(prefs) : prefs pfunc = InfiniteOpt.build_parameter_function(error, f, pref_arg) return InfiniteOpt.add_parameter_function(model, pfunc) end @@ -462,42 +468,6 @@ function _collect_parameters(model::InfiniteOpt.InfiniteModel) return prefs, supports end -# Condense flat per-support M values into a scalar or parameter function. -function _aggregate_M_values( - model::InfiniteOpt.InfiniteModel, - vals::AbstractVector{<:Real} - ) - if all(==(vals[1]), vals) - return vals[1] - end - prefs, supports = _collect_parameters(model) - return values_to_parameter_function(model, vals, prefs, supports) -end - -""" - values_to_parameter_function( - model, vals, prefs, supports - ) - -Interpolate flat per-support values into an InfiniteOpt parameter -function registered on `model`. Builds a grid from `supports`, -reshapes `vals`, fits a linear interpolation, and returns the -registered parameter function ref. -""" -function values_to_parameter_function( - model::InfiniteOpt.InfiniteModel, - vals::AbstractVector{<:Real}, - prefs::Union{Vector{InfiniteOpt.GeneralVariableRef}, - Tuple{Vararg{InfiniteOpt.GeneralVariableRef}}}, - supports::Dict{InfiniteOpt.GeneralVariableRef, Vector{Float64}} - ) - grids = Tuple(supports[p] for p in prefs) - shape = Tuple(length(supports[p]) for p in prefs) - nd = reshape(vals, shape) - fn = Interpolations.linear_interpolation(grids, nd, - extrapolation_bc = Interpolations.Line()) - return _make_parameter_function(model, fn, prefs...) -end ################################################################################ # CUTTING PLANES FOR INFINITEMODEL @@ -549,46 +519,36 @@ function DP.extract_solution(model::InfiniteOpt.InfiniteModel) return sol end -# Add an infinite-form cut to the InfiniteModel. Infinite variables get -# interpolated parameter function coefficients wrapped in integrals; -# finite variables contribute scalar terms. +# Add a flat-sum cut directly to the transformation backend, matching +# the SEP's unweighted Euclidean norm (Trespalacios & Grossmann 2016 +# Eq. 11 applied in the joint transcribed variable space). Then mark +# the backend as ready so the next optimize! reuses the cut-enhanced +# flat model without re-transcribing (which would wipe the cut). function DP.add_cut( model::InfiniteOpt.InfiniteModel, decision_vars::Vector{InfiniteOpt.GeneralVariableRef}, rBM_sol::Dict{<:JuMP.AbstractVariableRef, <:Vector{<:Number}}, sep_sol::Dict{<:JuMP.AbstractVariableRef, <:Vector{<:Number}} ) - prefs, sups = _collect_parameters(model) - inf_terms = Any[] - cut_scalar = zero(JuMP.GenericAffExpr{ - JuMP.value_type(typeof(model)), - InfiniteOpt.GeneralVariableRef}) + flat = InfiniteOpt.transformation_model(model) + cut_expr = zero(JuMP.GenericAffExpr{ + JuMP.value_type(typeof(flat)), + JuMP.variable_ref_type(flat)}) for var in decision_vars haskey(rBM_sol, var) || continue haskey(sep_sol, var) || continue - vprefs = InfiniteOpt.parameter_refs(var) - if isempty(vprefs) - xi = 2 * (sep_sol[var][1] - rBM_sol[var][1]) - cut_scalar += xi * (var - sep_sol[var][1]) - else - xi_vals = 2 .* (sep_sol[var] .- rBM_sol[var]) - sp_vals = sep_sol[var] - xi_pf = values_to_parameter_function( - model, xi_vals, vprefs, sups) - sp_pf = values_to_parameter_function( - model, sp_vals, vprefs, sups) - push!(inf_terms, xi_pf * var - xi_pf * sp_pf) - end - end - if !isempty(inf_terms) - inf_expr = JuMP.@expression(model, sum(inf_terms)) - for p in prefs - inf_expr = InfiniteOpt.integral(inf_expr, p) + rbm_vals = rBM_sol[var] + sep_vals = sep_sol[var] + tv = InfiniteOpt.transformation_variable(var) + flat_vars = tv isa AbstractArray ? vec(tv) : [tv] + for k in eachindex(flat_vars) + xi = 2 * (sep_vals[k] - rbm_vals[k]) + JuMP.add_to_expression!(cut_expr, xi, flat_vars[k]) + JuMP.add_to_expression!(cut_expr, -xi * sep_vals[k]) end - cut_scalar += inf_expr end - cref = JuMP.@constraint(model, cut_scalar >= 0) - push!(DP._reformulation_constraints(model), cref) + JuMP.@constraint(flat, cut_expr >= 0) + InfiniteOpt.set_transformation_backend_ready(model, true) return end From 778b598a4c4cc49dd85e385821bce9e79ca69636 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Tue, 21 Apr 2026 16:31:41 -0400 Subject: [PATCH 13/34] . --- ext/InfiniteDisjunctiveProgramming.jl | 244 ++++++------------ src/mbm.jl | 10 +- .../InfiniteDisjunctiveProgramming.jl | 160 ++++++++---- 3 files changed, 194 insertions(+), 220 deletions(-) diff --git a/ext/InfiniteDisjunctiveProgramming.jl b/ext/InfiniteDisjunctiveProgramming.jl index 7ad421c7..72a38518 100644 --- a/ext/InfiniteDisjunctiveProgramming.jl +++ b/ext/InfiniteDisjunctiveProgramming.jl @@ -170,111 +170,34 @@ function DP.disaggregate_expression( return JuMP.@expression(model, sum(terms)) end -# Quadratic expression: handle parameter x parameter, parameter x variable, -# and variable x variable terms. -function DP.disaggregate_expression( - model::M, quad::JuMP.GenericQuadExpr, - bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, - method::DP._Hull - ) where {M <: InfiniteOpt.InfiniteModel} - # Affine part (uses InfiniteOpt override above) - new_expr = DP.disaggregate_expression(model, quad.aff, bvref, method) - ϵ = method.value - for (pair, coeff) in quad.terms - a_param = pair.a isa InfiniteOpt.GeneralVariableRef && - _is_parameter(pair.a) - b_param = pair.b isa InfiniteOpt.GeneralVariableRef && - _is_parameter(pair.b) - if a_param && b_param - # param × param: constant, scale by y - new_expr += coeff * pair.a * pair.b * bvref - elseif a_param - # param × var: perspective cancels y - db = method.disjunct_variables[pair.b, bvref] - new_expr += coeff * pair.a * db - elseif b_param - # var × param: perspective cancels y - da = method.disjunct_variables[pair.a, bvref] - new_expr += coeff * da * pair.b - else - # var × var: standard perspective - da = method.disjunct_variables[pair.a, bvref] - db = method.disjunct_variables[pair.b, bvref] - new_expr += coeff * da * db / ((1 - ϵ) * bvref + ϵ) - end - end - return new_expr -end - ################################################################################ # MBM FOR INFINITEMODEL ################################################################################ # Reuses the finite MBM infrastructure by overriding: -# copy_model_with_constraints (build mini InfiniteModel + -# transcribe to flat JuMP model), prepare_max_M_objective -# (expand infinite constraint into K flat objectives via -# _build_flat_map), and raw_M (vector dispatch aggregates -# K per-support M values into a parameter function). +# copy_model_with_constraints (build mini InfiniteModel, transcribe to +# flat JuMP, stash mini + main->mini ref_map in sub.model.ext), +# prepare_max_M_objective (translate main-model slack expr to mini-level +# then call InfiniteOpt.transformation_expression to get K flat +# objectives), and raw_M (vector dispatch aggregates K per-support M +# values into a parameter function). # Collect all parameter function refs from all disjunct constraints in # the model. -function _all_param_functions( - model::InfiniteOpt.InfiniteModel - ) - pf_set = Set{InfiniteOpt.GeneralVariableRef}() +function _all_param_functions(model::InfiniteOpt.InfiniteModel) + param_funcs = Set{InfiniteOpt.GeneralVariableRef}() for (_, crefs) in DP._indicator_to_constraints(model) for cref in crefs cref isa DP.DisjunctConstraintRef || continue con = JuMP.constraint_object(cref) for v in InfiniteOpt.all_expression_variables(con.func) - dv = InfiniteOpt.dispatch_variable_ref(v) - if dv isa InfiniteOpt.ParameterFunctionRef - push!(pf_set, v) + dispatch_var = InfiniteOpt.dispatch_variable_ref(v) + if dispatch_var isa InfiniteOpt.ParameterFunctionRef + push!(param_funcs, v) end end end end - return pf_set -end - -# Build a flat map for support point k. Maps decision variables to their -# flat JuMP.VariableRef at support k (handling multi-parameter indexing) -# and evaluates parameter functions to their numerical values. pf_set is -# precomputed by the caller to avoid rescanning all disjunct constraints -# on every support point. -function _build_flat_map( - sub::DP.GDPSubmodel, k::Int, - prefs::Vector{InfiniteOpt.GeneralVariableRef}, - supports::Dict{InfiniteOpt.GeneralVariableRef, Vector{Float64}}, - full_shape::Tuple, - pf_set::Set{InfiniteOpt.GeneralVariableRef} - ) - ci = CartesianIndices(full_shape)[k] - - # Decision variables: map each to its variable-local index - flat_map = Dict{InfiniteOpt.GeneralVariableRef, Any}() - for (v, ws) in sub.fwd_map - if length(ws) == 1 - flat_map[v] = only(ws) - else - vp = InfiniteOpt.parameter_refs(v) - shape = Tuple(length(supports[p]) for p in vp) - idx = Tuple(ci[findfirst(==(p), prefs)] for p in vp) - flat_map[v] = ws[LinearIndices(shape)[idx...]] - end - end - - # Parameter functions: evaluate at support point k - sup_vals = Dict( - prefs[i] => supports[prefs[i]][ci[i]] - for i in 1:length(prefs)) - for pf in pf_set - fn = InfiniteOpt.raw_function(pf) - pf_prefs = InfiniteOpt.parameter_refs(pf) - pf_vals = Tuple(sup_vals[p] for p in pf_prefs) - flat_map[pf] = fn(pf_vals...) - end - return flat_map + return param_funcs end # Build mini InfiniteModel with only the given disjunct constraints, @@ -291,10 +214,10 @@ function DP.copy_model_with_constraints( # 1. Copy infinite parameters with their supports for p in InfiniteOpt.all_parameters(model) domain = InfiniteOpt.infinite_domain(p) - sups = Float64.(InfiniteOpt.supports(p)) - param = InfiniteOpt.build_parameter(error, domain; supports = sups) - new_p = InfiniteOpt.add_parameter(mini, param, JuMP.name(p)) - ref_map[p] = new_p + supports = Float64.(InfiniteOpt.supports(p)) + param = InfiniteOpt.build_parameter(error, domain; supports = supports) + new_param = InfiniteOpt.add_parameter(mini, param, JuMP.name(p)) + ref_map[p] = new_param end # 2. Copy decision variables with bounds @@ -321,13 +244,13 @@ function DP.copy_model_with_constraints( # 4. Copy parameter functions from ALL disjuncts (needed for # constraint transcription) - pf_set = _all_param_functions(model) - for pf in pf_set - fn = InfiniteOpt.raw_function(pf) - prefs = InfiniteOpt.parameter_refs(pf) + param_funcs = _all_param_functions(model) + for pfunc in param_funcs + func = InfiniteOpt.raw_function(pfunc) + prefs = InfiniteOpt.parameter_refs(pfunc) mapped_prefs = Tuple(ref_map[p] for p in prefs) - new_pf = _make_parameter_function(mini, fn, mapped_prefs...) - ref_map[pf] = new_pf + new_pfunc = _make_parameter_function(mini, func, mapped_prefs...) + ref_map[pfunc] = new_pfunc end # 5. Add disjunct constraints using existing ref_map @@ -342,72 +265,51 @@ function DP.copy_model_with_constraints( # 6. Transcribe mini InfiniteModel to flat JuMP model InfiniteOpt.build_transformation_backend!(mini) flat = InfiniteOpt.transformation_model(mini) - tr_fwd = Dict{InfiniteOpt.GeneralVariableRef, - Vector{JuMP.VariableRef}}() - for v in DP.collect_all_vars(mini) - tv = InfiniteOpt.transformation_variable(v) - vprefs = InfiniteOpt.parameter_refs(v) - tr_fwd[v] = isempty(vprefs) ? [tv] : vec(tv) - end - - # 7. Remap fwd_map: original model var -> flat JuMP VarRef - fwd_map = Dict{InfiniteOpt.GeneralVariableRef, Vector{JuMP.VariableRef}}() - for (orig, mapped) in ref_map - _is_parameter(orig) && continue - haskey(tr_fwd, mapped) || continue - fwd_map[orig] = tr_fwd[mapped] - end - - decision_vars = collect(keys(fwd_map)) JuMP.set_optimizer(flat, method.optimizer) JuMP.set_silent(flat) - return DP.GDPSubmodel(flat, decision_vars, fwd_map) + # Stash main + ref_map so prepare_max_M_objective can translate + # main-model expressions and let InfiniteOpt transcribe them via + # mini's backend. Also stash main so raw_M can return a parameter + # function on main (where it will be used in BigM constraints). + flat.ext[:inf_mbm_main] = model + flat.ext[:inf_mbm_ref_map] = ref_map + # fwd_map / decision_vars are CP-shaped fields on GDPSubmodel that + # the MBM path through our overrides does not consult; pass empty + # containers of the right types. + return DP.GDPSubmodel(flat, InfiniteOpt.GeneralVariableRef[], + Dict{InfiniteOpt.GeneralVariableRef, Vector{JuMP.VariableRef}}()) end -# Prepare objectives for all support points. Expands an infinite -# constraint into K flat objectives via _build_flat_map with -# multi-parameter indexing and parameter function evaluation. +# Translate the constraint slack to the mini InfiniteModel via ref_map, +# then use InfiniteOpt.transformation_expression to get one JuMP scalar +# (or plain Real, for pure-parameter slacks) per support point. function DP.prepare_max_M_objective( - model::InfiniteOpt.InfiniteModel, + ::InfiniteOpt.InfiniteModel, obj::JuMP.ScalarConstraint{T, S}, sub::DP.GDPSubmodel ) where {T, S <: _MOI.LessThan} - prefs, supports = _collect_parameters(model) - full_shape = Tuple(length(supports[p]) for p in prefs) - K = prod(full_shape) - pf_set = _all_param_functions(model) - objectives = Vector{JuMP.AbstractJuMPScalar}(undef, K) - for k in 1:K - flat_map = _build_flat_map(sub, k, prefs, supports, full_shape, pf_set) - objectives[k] = -obj.set.upper + - DP._replace_variables_in_constraint(obj.func, flat_map) - end - return objectives + ref_map = sub.model.ext[:inf_mbm_ref_map] + mini_expr = DP._replace_variables_in_constraint(obj.func, ref_map) + return InfiniteOpt.transformation_expression(mini_expr - obj.set.upper) end function DP.prepare_max_M_objective( - model::InfiniteOpt.InfiniteModel, + ::InfiniteOpt.InfiniteModel, obj::JuMP.ScalarConstraint{T, S}, sub::DP.GDPSubmodel ) where {T, S <: _MOI.GreaterThan} - prefs, supports = _collect_parameters(model) - full_shape = Tuple(length(supports[p]) for p in prefs) - K = prod(full_shape) - pf_set = _all_param_functions(model) - objectives = Vector{JuMP.AbstractJuMPScalar}(undef, K) - for k in 1:K - flat_map = _build_flat_map(sub, k, prefs, supports, full_shape, pf_set) - objectives[k] = obj.set.lower - - DP._replace_variables_in_constraint(obj.func, flat_map) - end - return objectives + ref_map = sub.model.ext[:inf_mbm_ref_map] + mini_expr = DP._replace_variables_in_constraint(obj.func, ref_map) + return InfiniteOpt.transformation_expression(obj.set.lower - mini_expr) end # Solve the submodel for a vector of objectives (one per support point). -# Returns aggregated M value (scalar or parameter function) or nothing. +# Elements may be Real when the slack is pure-parameter at that support; +# JuMP's @objective accepts Real and the solver treats it as a constant +# objective, which gives the correct M value for that slice. function DP.raw_M( sub::DP.GDPSubmodel, - objectives::Vector{<:JuMP.AbstractJuMPScalar}, + objectives::Vector{<:Union{Real, JuMP.AbstractJuMPScalar}}, method::DP._MBM ) M_vals = typeof(method.default_M)[] @@ -426,16 +328,15 @@ function DP.raw_M( push!(M_vals, method.default_M) end end - model = JuMP.owner_model(first(keys(sub.fwd_map))) - # Condense flat per-support values: scalar if uniform, else pf. + model = sub.model.ext[:inf_mbm_main] + # Condense per-support values: scalar if uniform, else pfunc. all(==(M_vals[1]), M_vals) && return M_vals[1] prefs, supports = _collect_parameters(model) grids = Tuple(supports[p] for p in prefs) shape = Tuple(length(supports[p]) for p in prefs) - fn = Interpolations.linear_interpolation( - grids, reshape(M_vals, shape), + func = Interpolations.linear_interpolation(grids, reshape(M_vals, shape), extrapolation_bc = Interpolations.Line()) - return _make_parameter_function(model, fn, prefs...) + return _make_parameter_function(model, func, prefs...) end ################################################################################ @@ -444,16 +345,17 @@ end # Replacement for @parameter_function in the case of using an interpolation. # Example (1D interpolation): -# fn = Interpolations.linear_interpolation(grids, vals) -# pf = _make_parameter_function(model, fn, t) # returns a pf ref +# func = Interpolations.linear_interpolation(grids, vals) +# pfunc = _make_parameter_function(model, func, t) # returns a pfunc ref function _make_parameter_function( - model::InfiniteOpt.InfiniteModel, fn, + model::InfiniteOpt.InfiniteModel, func, prefs::InfiniteOpt.GeneralVariableRef... ) - f = fn isa Function ? fn : ((args...) -> fn(args...)) + wrapped_func = func isa Function ? func : ((args...) -> func(args...)) pref_arg = length(prefs) == 1 ? only(prefs) : prefs - pfunc = InfiniteOpt.build_parameter_function(error, f, pref_arg) - return InfiniteOpt.add_parameter_function(model, pfunc) + builder = InfiniteOpt.build_parameter_function( + error, wrapped_func, pref_arg) + return InfiniteOpt.add_parameter_function(model, builder) end # Collect all infinite parameters and their supports from the model. @@ -484,19 +386,19 @@ function DP.copy_and_reformulate( DP.reformulate_model(model, reform_method) InfiniteOpt.build_transformation_backend!(model) flat = InfiniteOpt.transformation_model(model) - tr_fwd = Dict{InfiniteOpt.GeneralVariableRef, + transcription_fwd = Dict{InfiniteOpt.GeneralVariableRef, Vector{JuMP.VariableRef}}() for v in DP.collect_all_vars(model) - tv = InfiniteOpt.transformation_variable(v) - vprefs = InfiniteOpt.parameter_refs(v) - tr_fwd[v] = isempty(vprefs) ? [tv] : vec(tv) + transcription_var = InfiniteOpt.transformation_variable(v) + var_prefs = InfiniteOpt.parameter_refs(v) + transcription_fwd[v] = isempty(var_prefs) ? + [transcription_var] : vec(transcription_var) end sub_copy, copy_map = JuMP.copy_model(flat) - fwd_map = Dict{InfiniteOpt.GeneralVariableRef, - Vector{JuMP.VariableRef}}() + fwd_map = Dict{InfiniteOpt.GeneralVariableRef, Vector{JuMP.VariableRef}}() for v in decision_vars - haskey(tr_fwd, v) || continue - fwd_map[v] = [copy_map[tv] for tv in tr_fwd[v]] + haskey(transcription_fwd, v) || continue + fwd_map[v] = [copy_map[flat_var] for flat_var in transcription_fwd[v]] end sub = DP.GDPSubmodel(sub_copy, decision_vars, fwd_map) JuMP.set_optimizer(sub.model, method.optimizer) @@ -512,9 +414,10 @@ function DP.extract_solution(model::InfiniteOpt.InfiniteModel) T = JuMP.value_type(typeof(model)) sol = Dict{V, Vector{T}}() for v in dvars - tv = InfiniteOpt.transformation_variable(v) - vprefs = InfiniteOpt.parameter_refs(v) - sol[v] = isempty(vprefs) ? [JuMP.value(tv)] : JuMP.value.(vec(tv)) + transcription_var = InfiniteOpt.transformation_variable(v) + var_prefs = InfiniteOpt.parameter_refs(v) + sol[v] = isempty(var_prefs) ? [JuMP.value(transcription_var)] : + JuMP.value.(vec(transcription_var)) end return sol end @@ -539,8 +442,9 @@ function DP.add_cut( haskey(sep_sol, var) || continue rbm_vals = rBM_sol[var] sep_vals = sep_sol[var] - tv = InfiniteOpt.transformation_variable(var) - flat_vars = tv isa AbstractArray ? vec(tv) : [tv] + transcription_var = InfiniteOpt.transformation_variable(var) + flat_vars = transcription_var isa AbstractArray ? + vec(transcription_var) : [transcription_var] for k in eachindex(flat_vars) xi = 2 * (sep_vals[k] - rbm_vals[k]) JuMP.add_to_expression!(cut_expr, xi, flat_vars[k]) diff --git a/src/mbm.jl b/src/mbm.jl index 8d8fc1cf..4fd844c3 100644 --- a/src/mbm.jl +++ b/src/mbm.jl @@ -264,8 +264,14 @@ function prepare_max_M_objective( return expr end -# Solve the submodel for a single objective expression. -# Returns a scalar M value, or nothing if infeasible. +""" + raw_M(sub::GDPSubmodel, objective, method::_MBM) + +Maximize `objective` over `sub` to obtain one raw M value for MBM. +Returns `max(obj_value, 0)` on optimal, `nothing` on infeasible +(signals the constraint is redundant in the combined region), or +`method.default_M` otherwise (unbounded, numerical failure, etc). +""" function raw_M( sub::GDPSubmodel, objective::JuMP.AbstractJuMPScalar, diff --git a/test/extensions/InfiniteDisjunctiveProgramming.jl b/test/extensions/InfiniteDisjunctiveProgramming.jl index 22f870a9..7f8495ea 100644 --- a/test/extensions/InfiniteDisjunctiveProgramming.jl +++ b/test/extensions/InfiniteDisjunctiveProgramming.jl @@ -77,9 +77,9 @@ function test__is_parameter() @test IDP._is_parameter(y) == false end -# _is_parameter on unwrapped concrete dispatch types. Covers -# ext lines 28-32 (DependentParameterRef, IndependentParameterRef, -# FiniteParameterRef, ParameterFunctionRef, Any fallback). +# _is_parameter on unwrapped concrete dispatch types +# (DependentParameterRef, IndependentParameterRef, FiniteParameterRef, +# ParameterFunctionRef, Any fallback). function test__is_parameter_concrete_dispatches() model = InfiniteGDPModel() @infinite_parameter(model, t ∈ [0, 1]) @@ -187,43 +187,6 @@ function test_disaggregate_expression_infiniteopt() @test haskey(result_not_disagg.terms, y) end -function test_disaggregate_quad_expression_infiniteopt() - model = InfiniteGDPModel() - @infinite_parameter(model, t ∈ [0, 1]) - @variable(model, 0 <= x <= 10, Infinite(t)) - @variable(model, 0 <= y <= 5, Infinite(t)) - @variable(model, z, InfiniteLogical(t)) - - bvrefs = DP._indicator_to_binary(model) - bvref = bvrefs[z] - - vrefs = Set([x, y]) - DP._variable_bounds(model)[x] = DP.set_variable_bound_info(x, Hull()) - DP._variable_bounds(model)[y] = DP.set_variable_bound_info(y, Hull()) - method = DP._Hull(Hull(1e-3), vrefs) - DP._disaggregate_variables(model, z, vrefs, method) - - # var × var → nonlinear (perspective divides by y) - quad_vv = @expression(model, x * y) - result_vv = DP.disaggregate_expression(model, quad_vv, bvref, method) - @test result_vv isa JuMP.GenericNonlinearExpr - - # param × var → quadratic (param * disaggregated) - quad_pv = @expression(model, t * x) - result_pv = DP.disaggregate_expression(model, quad_pv, bvref, method) - @test result_pv isa JuMP.GenericQuadExpr - - # var × param → quadratic (disaggregated * param) - quad_vp = @expression(model, x * t) - result_vp = DP.disaggregate_expression(model, quad_vp, bvref, method) - @test result_vp isa JuMP.GenericQuadExpr - - # param × param → cubic (t * t * bvref) - quad_pp = @expression(model, t * t) - result_pp = DP.disaggregate_expression(model, quad_pp, bvref, method) - @test result_pp isa JuMP.GenericNonlinearExpr -end - function test_variable_properties_infiniteopt() model = InfiniteGDPModel() @infinite_parameter(model, t ∈ [0, 1]) @@ -392,15 +355,113 @@ function test_logical_value() end # _collect_parameters on model with no infinite parameters. -# Covers ext line 508. function test__collect_parameters_no_params() model = InfiniteGDPModel() @test_throws ErrorException IDP._collect_parameters(model) end -# MBM with finite + integer variables in InfiniteModel. Covers -# copy_model_with_constraints (finite var, set_integer), -# and _build_flat_map line 252 (finite var path). +# raw_M against an InfiniteModel where M is constant across supports. +# Setup: x(t) ∈ [0, 10], disj1: x ≥ 5, disj2: x ≤ 3. +# For disj1 slack r(x) = 5 - x maximized over disj2's region x ∈ [0, 3]: +# max(5 - x) = 5 at x = 0. Same at every support ⇒ scalar M = 5. +function test_raw_M_infinite_scalar() + model = InfiniteGDPModel() + @infinite_parameter(model, t ∈ [0, 1], num_supports = 5) + @variable(model, 0 <= x <= 10, Infinite(t)) + @variable(model, Y[1:2], InfiniteLogical(t)) + @constraint(model, con, x >= 5, Disjunct(Y[1])) + @constraint(model, con2, x <= 3, Disjunct(Y[2])) + @disjunction(model, Y) + mbm = DP._MBM(MBM(HiGHS.Optimizer), model) + sub = DP.copy_model_with_constraints( + model, DP.DisjunctConstraintRef[con2], mbm) + obj = DP.prepare_max_M_objective( + model, JuMP.constraint_object(con), sub) + @test length(obj) == 5 # K support points + @test DP.raw_M(sub, obj, mbm) == 5.0 +end + +# raw_M with a support-varying M. Setup: x(t) ∈ [0, 10], disj1: x ≤ 2t, +# disj2: x ≥ 0.5. Slack r(x) = x - 2t maximized over x ∈ [0.5, 10]: +# max(x - 2t) = 10 - 2t. Varies with t ⇒ raw_M returns a pfunc; the +# underlying function should evaluate to 10 - 2t at each support. +function test_raw_M_infinite_param_function() + model = InfiniteGDPModel() + supports = [0.0, 0.25, 0.5, 0.75, 1.0] + @infinite_parameter(model, t ∈ [0, 1], supports = supports) + @variable(model, 0 <= x <= 10, Infinite(t)) + @variable(model, Y[1:2], InfiniteLogical(t)) + @parameter_function(model, f == t -> 2*t) + @constraint(model, con, x <= f, Disjunct(Y[1])) + @constraint(model, con2, x >= 0.5, Disjunct(Y[2])) + @disjunction(model, Y) + mbm = DP._MBM(MBM(HiGHS.Optimizer), model) + sub = DP.copy_model_with_constraints( + model, DP.DisjunctConstraintRef[con2], mbm) + obj = DP.prepare_max_M_objective( + model, JuMP.constraint_object(con), sub) + M = DP.raw_M(sub, obj, mbm) + @test M isa InfiniteOpt.GeneralVariableRef + raw_fn = InfiniteOpt.raw_function(M) + for t_val in supports + @test raw_fn(t_val) ≈ 10.0 - 2*t_val atol=1e-6 + end +end + +# extract_solution returns per-support values from the transformation +# backend. Setup: force disj 2 active (x ≤ 3), BigM-reformulate, solve +# min ∫x ⇒ x = 0 at every support. +function test_extract_solution_infinite() + model = InfiniteGDPModel(HiGHS.Optimizer) + set_silent(model) + K = 4 + @infinite_parameter(model, t ∈ [0, 1], num_supports = K) + @variable(model, 0 <= x <= 10, Infinite(t)) + @variable(model, Y[1:2], InfiniteLogical(t)) + @constraint(model, x >= 5, Disjunct(Y[1])) + @constraint(model, x <= 3, Disjunct(Y[2])) + @disjunction(model, Y) + JuMP.fix(Y[2], true) # force disj 2 active + @objective(model, Min, ∫(x, t)) + DP.reformulate_model(model, BigM(10.0)) + set_optimizer(model, HiGHS.Optimizer) + set_silent(model) + optimize!(model, ignore_optimize_hook = true) + sol = DP.extract_solution(model) + @test haskey(sol, x) + @test length(sol[x]) == K + @test all(v -> isapprox(v, 0.0; atol=1e-6), sol[x]) +end + +# add_cut adds one flat-sum cut to the transformation backend and marks +# the backend ready so the next optimize! does NOT re-transcribe. +function test_add_cut_infinite() + model = InfiniteGDPModel(HiGHS.Optimizer) + set_silent(model) + K = 3 + @infinite_parameter(model, t ∈ [0, 1], num_supports = K) + @variable(model, 0 <= x <= 10, Infinite(t)) + @variable(model, Y[1:2], InfiniteLogical(t)) + @constraint(model, x >= 5, Disjunct(Y[1])) + @constraint(model, x <= 3, Disjunct(Y[2])) + @disjunction(model, Y) + DP.reformulate_model(model, BigM(10.0)) + InfiniteOpt.build_transformation_backend!(model) + flat = InfiniteOpt.transformation_model(model) + n_before = JuMP.num_constraints(flat; + count_variable_in_set_constraints = false) + rBM_sol = Dict(x => [1.0, 2.0, 3.0]) + sep_sol = Dict(x => [0.5, 1.5, 2.5]) + DP.add_cut(model, [x], rBM_sol, sep_sol) + n_after = JuMP.num_constraints(flat; + count_variable_in_set_constraints = false) + @test n_after == n_before + 1 + # set_transformation_backend_ready(true) — next optimize! should + # reuse without re-transcribing (otherwise our cut would be lost) + @test InfiniteOpt.transformation_backend_ready(model) +end + +# MBM with finite + integer variables in an InfiniteModel. function test_mbm_finite_and_integer_var() model = InfiniteGDPModel(HiGHS.Optimizer) set_silent(model) @@ -651,9 +712,9 @@ end function test_CuttingPlanes_with_cuts() # Maximization with single-constraint disjuncts where Hull # is strictly tighter than BigM. BigM allows x+y up to - # variable bounds (20), Hull limits to max(5,8)=8. This - # forces cuts. Finite var w exercises isempty(vprefs) - # branch in add_original_model_cut (line 779). + # variable bounds (20), Hull limits to max(5,8)=8 — this + # forces cuts to tighten the relaxation. Finite var w exercises + # the isempty(var_prefs) branch in add_cut. model = InfiniteGDPModel(HiGHS.Optimizer) set_silent(model) @infinite_parameter(model, t ∈ [0, 1], num_supports = 10) @@ -733,7 +794,6 @@ end @testset "Methods" begin test_get_constant() test_disaggregate_expression_infiniteopt() - test_disaggregate_quad_expression_infiniteopt() end @testset "Internal Helpers" begin @@ -741,6 +801,8 @@ end end @testset "MBM" begin + test_raw_M_infinite_scalar() + test_raw_M_infinite_param_function() test_mbm_finite_and_integer_var() test_mbm_infinite_simple() test_mbm_infinite_param_dependent() @@ -754,6 +816,8 @@ end end @testset "Cutting Planes" begin + test_extract_solution_infinite() + test_add_cut_infinite() test_CuttingPlanes_infinite_simple() test_CuttingPlanes_infinite_two_disj() test_CuttingPlanes_with_cuts() From 2c0071a36805afc52f36de975de0e3881247b71d Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Tue, 21 Apr 2026 18:35:47 -0400 Subject: [PATCH 14/34] . --- ext/InfiniteDisjunctiveProgramming.jl | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/ext/InfiniteDisjunctiveProgramming.jl b/ext/InfiniteDisjunctiveProgramming.jl index 72a38518..7982b192 100644 --- a/ext/InfiniteDisjunctiveProgramming.jl +++ b/ext/InfiniteDisjunctiveProgramming.jl @@ -303,10 +303,15 @@ function DP.prepare_max_M_objective( return InfiniteOpt.transformation_expression(obj.set.lower - mini_expr) end +# Real dispatch: pure-parameter slacks collapse to a constant at that +# support. Mirrors the max(value, 0) semantics of the scalar base. +DP.raw_M(::DP.GDPSubmodel, obj::Real, method::DP._MBM) = + max(obj, zero(method.default_M)) + # Solve the submodel for a vector of objectives (one per support point). -# Elements may be Real when the slack is pure-parameter at that support; -# JuMP's @objective accepts Real and the solver treats it as a constant -# objective, which gives the correct M value for that slice. +# Clears start values before each solve (Gurobi refuses NaN warmstarts +# that can linger from a prior unbounded solve) and delegates each +# element to the scalar base `raw_M` above. function DP.raw_M( sub::DP.GDPSubmodel, objectives::Vector{<:Union{Real, JuMP.AbstractJuMPScalar}}, @@ -315,18 +320,9 @@ function DP.raw_M( M_vals = typeof(method.default_M)[] for obj_expr in objectives JuMP.set_start_value.(JuMP.all_variables(sub.model), nothing) - JuMP.@objective(sub.model, Max, obj_expr) - JuMP.optimize!(sub.model) - if JuMP.is_solved_and_feasible(sub.model) - push!(M_vals, max( - JuMP.objective_value(sub.model), - zero(method.default_M))) - elseif JuMP.termination_status(sub.model) == - JuMP.MOI.INFEASIBLE - return nothing - else - push!(M_vals, method.default_M) - end + m = DP.raw_M(sub, obj_expr, method) + m === nothing && return nothing + push!(M_vals, m) end model = sub.model.ext[:inf_mbm_main] # Condense per-support values: scalar if uniform, else pfunc. From a77b98833af2fa3476998da3d6fb56c8e3558732 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Tue, 21 Apr 2026 20:59:25 -0400 Subject: [PATCH 15/34] . --- ext/InfiniteDisjunctiveProgramming.jl | 19 ++++++----- .../InfiniteDisjunctiveProgramming.jl | 32 ++++++++++++------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/ext/InfiniteDisjunctiveProgramming.jl b/ext/InfiniteDisjunctiveProgramming.jl index 7982b192..631875bb 100644 --- a/ext/InfiniteDisjunctiveProgramming.jl +++ b/ext/InfiniteDisjunctiveProgramming.jl @@ -282,7 +282,9 @@ end # Translate the constraint slack to the mini InfiniteModel via ref_map, # then use InfiniteOpt.transformation_expression to get one JuMP scalar -# (or plain Real, for pure-parameter slacks) per support point. +# per support point. Narrows the declared Vector{Union{Real, …}} return +# to Vector{AbstractJuMPScalar}; errors loudly if a support gives a +# pure-constant slack (would require a degenerate disjunct constraint). function DP.prepare_max_M_objective( ::InfiniteOpt.InfiniteModel, obj::JuMP.ScalarConstraint{T, S}, @@ -290,7 +292,8 @@ function DP.prepare_max_M_objective( ) where {T, S <: _MOI.LessThan} ref_map = sub.model.ext[:inf_mbm_ref_map] mini_expr = DP._replace_variables_in_constraint(obj.func, ref_map) - return InfiniteOpt.transformation_expression(mini_expr - obj.set.upper) + return Vector{JuMP.AbstractJuMPScalar}( + InfiniteOpt.transformation_expression(mini_expr - obj.set.upper)) end function DP.prepare_max_M_objective( @@ -300,21 +303,17 @@ function DP.prepare_max_M_objective( ) where {T, S <: _MOI.GreaterThan} ref_map = sub.model.ext[:inf_mbm_ref_map] mini_expr = DP._replace_variables_in_constraint(obj.func, ref_map) - return InfiniteOpt.transformation_expression(obj.set.lower - mini_expr) + return Vector{JuMP.AbstractJuMPScalar}( + InfiniteOpt.transformation_expression(obj.set.lower - mini_expr)) end -# Real dispatch: pure-parameter slacks collapse to a constant at that -# support. Mirrors the max(value, 0) semantics of the scalar base. -DP.raw_M(::DP.GDPSubmodel, obj::Real, method::DP._MBM) = - max(obj, zero(method.default_M)) - # Solve the submodel for a vector of objectives (one per support point). # Clears start values before each solve (Gurobi refuses NaN warmstarts # that can linger from a prior unbounded solve) and delegates each -# element to the scalar base `raw_M` above. +# element to the scalar base `raw_M`. function DP.raw_M( sub::DP.GDPSubmodel, - objectives::Vector{<:Union{Real, JuMP.AbstractJuMPScalar}}, + objectives::Vector{<:JuMP.AbstractJuMPScalar}, method::DP._MBM ) M_vals = typeof(method.default_M)[] diff --git a/test/extensions/InfiniteDisjunctiveProgramming.jl b/test/extensions/InfiniteDisjunctiveProgramming.jl index 7f8495ea..e7fed04d 100644 --- a/test/extensions/InfiniteDisjunctiveProgramming.jl +++ b/test/extensions/InfiniteDisjunctiveProgramming.jl @@ -55,7 +55,7 @@ function test_infinite_logical() @test binary_variable(y) isa InfiniteOpt.GeneralVariableRef end -function test__is_parameter() +function test_is_parameter() model = InfiniteGDPModel() @infinite_parameter(model, t ∈ [0, 1]) @infinite_parameter(model, s[1:2] ∈ [0, 1], independent = true) @@ -80,19 +80,29 @@ end # _is_parameter on unwrapped concrete dispatch types # (DependentParameterRef, IndependentParameterRef, FiniteParameterRef, # ParameterFunctionRef, Any fallback). -function test__is_parameter_concrete_dispatches() +function test_is_parameter_concrete_dispatches() model = InfiniteGDPModel() + # Scalar + `independent = true` array both give IndependentParameterRef; + # a default array parameter gives DependentParameterRef. @infinite_parameter(model, t ∈ [0, 1]) @infinite_parameter(model, s[1:2] ∈ [0, 1], independent = true) + @infinite_parameter(model, q[1:2] ∈ [0, 1]) @finite_parameter(model, p == 1.0) @variable(model, x, Infinite(t)) @parameter_function(model, pf == t -> 2*t) dvr = InfiniteOpt.dispatch_variable_ref - @test IDP._is_parameter(dvr(t)) == true # Dependent - @test IDP._is_parameter(dvr(s[1])) == true # Independent - @test IDP._is_parameter(dvr(p)) == true # Finite - @test IDP._is_parameter(dvr(pf)) == true # ParamFunc - @test IDP._is_parameter(dvr(x)) == false # Any + # Verify each ref hits the intended dispatch. + @test dvr(t) isa InfiniteOpt.IndependentParameterRef + @test dvr(s[1]) isa InfiniteOpt.IndependentParameterRef + @test dvr(q[1]) isa InfiniteOpt.DependentParameterRef + @test dvr(p) isa InfiniteOpt.FiniteParameterRef + @test dvr(pf) isa InfiniteOpt.ParameterFunctionRef + @test IDP._is_parameter(dvr(t)) == true + @test IDP._is_parameter(dvr(s[1])) == true + @test IDP._is_parameter(dvr(q[1])) == true + @test IDP._is_parameter(dvr(p)) == true + @test IDP._is_parameter(dvr(pf)) == true + @test IDP._is_parameter(dvr(x)) == false # Any fallback end function test_requires_disaggregation() @@ -355,7 +365,7 @@ function test_logical_value() end # _collect_parameters on model with no infinite parameters. -function test__collect_parameters_no_params() +function test_collect_parameters_no_params() model = InfiniteGDPModel() @test_throws ErrorException IDP._collect_parameters(model) end @@ -769,8 +779,8 @@ end @testset "Variables" begin test_infinite_logical() - test__is_parameter() - test__is_parameter_concrete_dispatches() + test_is_parameter() + test_is_parameter_concrete_dispatches() test_requires_disaggregation() test_variable_properties_infiniteopt() test_variable_properties_from_expr() @@ -797,7 +807,7 @@ end end @testset "Internal Helpers" begin - test__collect_parameters_no_params() + test_collect_parameters_no_params() end @testset "MBM" begin From 1ec83f4f8a789f9a5afd22aa399c4834278afe75 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Tue, 21 Apr 2026 22:46:02 -0400 Subject: [PATCH 16/34] . --- ext/InfiniteDisjunctiveProgramming.jl | 124 +++++------------- .../InfiniteDisjunctiveProgramming.jl | 20 +-- 2 files changed, 39 insertions(+), 105 deletions(-) diff --git a/ext/InfiniteDisjunctiveProgramming.jl b/ext/InfiniteDisjunctiveProgramming.jl index 631875bb..4a7f795a 100644 --- a/ext/InfiniteDisjunctiveProgramming.jl +++ b/ext/InfiniteDisjunctiveProgramming.jl @@ -173,35 +173,9 @@ end ################################################################################ # MBM FOR INFINITEMODEL ################################################################################ -# Reuses the finite MBM infrastructure by overriding: -# copy_model_with_constraints (build mini InfiniteModel, transcribe to -# flat JuMP, stash mini + main->mini ref_map in sub.model.ext), -# prepare_max_M_objective (translate main-model slack expr to mini-level -# then call InfiniteOpt.transformation_expression to get K flat -# objectives), and raw_M (vector dispatch aggregates K per-support M -# values into a parameter function). - -# Collect all parameter function refs from all disjunct constraints in -# the model. -function _all_param_functions(model::InfiniteOpt.InfiniteModel) - param_funcs = Set{InfiniteOpt.GeneralVariableRef}() - for (_, crefs) in DP._indicator_to_constraints(model) - for cref in crefs - cref isa DP.DisjunctConstraintRef || continue - con = JuMP.constraint_object(cref) - for v in InfiniteOpt.all_expression_variables(con.func) - dispatch_var = InfiniteOpt.dispatch_variable_ref(v) - if dispatch_var isa InfiniteOpt.ParameterFunctionRef - push!(param_funcs, v) - end - end - end - end - return param_funcs -end -# Build mini InfiniteModel with only the given disjunct constraints, -# transcribe to flat JuMP model, return GDPSubmodel with forward map. +# Build a mini InfiniteModel holding only the given disjunct constraints, +# transcribe it, and return as a GDPSubmodel. function DP.copy_model_with_constraints( model::InfiniteOpt.InfiniteModel, constraints::Vector{<:DP.DisjunctConstraintRef}, @@ -242,10 +216,8 @@ function DP.copy_model_with_constraints( ref_map[d] = new_d end - # 4. Copy parameter functions from ALL disjuncts (needed for - # constraint transcription) - param_funcs = _all_param_functions(model) - for pfunc in param_funcs + # 4. Copy parameter functions (needed by ref_map substitution) + for pfunc in InfiniteOpt.all_parameter_functions(model) func = InfiniteOpt.raw_function(pfunc) prefs = InfiniteOpt.parameter_refs(pfunc) mapped_prefs = Tuple(ref_map[p] for p in prefs) @@ -262,29 +234,20 @@ function DP.copy_model_with_constraints( JuMP.@constraint(mini, new_func * T in con.set) end - # 6. Transcribe mini InfiniteModel to flat JuMP model + # 6. Transcribe mini InfiniteModel InfiniteOpt.build_transformation_backend!(mini) - flat = InfiniteOpt.transformation_model(mini) - JuMP.set_optimizer(flat, method.optimizer) - JuMP.set_silent(flat) - # Stash main + ref_map so prepare_max_M_objective can translate - # main-model expressions and let InfiniteOpt transcribe them via - # mini's backend. Also stash main so raw_M can return a parameter - # function on main (where it will be used in BigM constraints). - flat.ext[:inf_mbm_main] = model - flat.ext[:inf_mbm_ref_map] = ref_map - # fwd_map / decision_vars are CP-shaped fields on GDPSubmodel that - # the MBM path through our overrides does not consult; pass empty - # containers of the right types. - return DP.GDPSubmodel(flat, InfiniteOpt.GeneralVariableRef[], + transcribed = InfiniteOpt.transformation_model(mini) + JuMP.set_optimizer(transcribed, method.optimizer) + JuMP.set_silent(transcribed) + # Stash for prepare_max_M_objective / raw_M. + transcribed.ext[:inf_mbm_main] = model + transcribed.ext[:inf_mbm_ref_map] = ref_map + # GDPSubmodel's fwd_map / decision_vars are CP-only; unused here. + return DP.GDPSubmodel(transcribed, InfiniteOpt.GeneralVariableRef[], Dict{InfiniteOpt.GeneralVariableRef, Vector{JuMP.VariableRef}}()) end -# Translate the constraint slack to the mini InfiniteModel via ref_map, -# then use InfiniteOpt.transformation_expression to get one JuMP scalar -# per support point. Narrows the declared Vector{Union{Real, …}} return -# to Vector{AbstractJuMPScalar}; errors loudly if a support gives a -# pure-constant slack (would require a degenerate disjunct constraint). +# Return one pointwise slack per support. function DP.prepare_max_M_objective( ::InfiniteOpt.InfiniteModel, obj::JuMP.ScalarConstraint{T, S}, @@ -307,10 +270,8 @@ function DP.prepare_max_M_objective( InfiniteOpt.transformation_expression(obj.set.lower - mini_expr)) end -# Solve the submodel for a vector of objectives (one per support point). -# Clears start values before each solve (Gurobi refuses NaN warmstarts -# that can linger from a prior unbounded solve) and delegates each -# element to the scalar base `raw_M`. +# Per-support solve, delegating to scalar base raw_M. Aggregated to a +# scalar if uniform, else to a parameter function. function DP.raw_M( sub::DP.GDPSubmodel, objectives::Vector{<:JuMP.AbstractJuMPScalar}, @@ -326,9 +287,9 @@ function DP.raw_M( model = sub.model.ext[:inf_mbm_main] # Condense per-support values: scalar if uniform, else pfunc. all(==(M_vals[1]), M_vals) && return M_vals[1] - prefs, supports = _collect_parameters(model) - grids = Tuple(supports[p] for p in prefs) - shape = Tuple(length(supports[p]) for p in prefs) + prefs = InfiniteOpt.all_parameters(model) + grids = Tuple(Float64.(InfiniteOpt.supports(p)) for p in prefs) + shape = Tuple(length.(grids)) func = Interpolations.linear_interpolation(grids, reshape(M_vals, shape), extrapolation_bc = Interpolations.Line()) return _make_parameter_function(model, func, prefs...) @@ -353,25 +314,12 @@ function _make_parameter_function( return InfiniteOpt.add_parameter_function(model, builder) end -# Collect all infinite parameters and their supports from the model. -function _collect_parameters(model::InfiniteOpt.InfiniteModel) - params = collect(InfiniteOpt.all_parameters(model)) - if isempty(params) - error("Model has no infinite parameters.") - end - prefs = InfiniteOpt.GeneralVariableRef[p for p in params] - supports = Dict{InfiniteOpt.GeneralVariableRef, Vector{Float64}}( - p => Float64.(InfiniteOpt.supports(p)) for p in prefs) - return prefs, supports -end - - ################################################################################ # CUTTING PLANES FOR INFINITEMODEL ################################################################################ -# Build CP subproblem: reformulate the InfiniteModel in-place, transcribe -# to a flat JuMP copy, and wrap in GDPSubmodel with forward variable map. +# Build CP subproblem: reformulate the InfiniteModel in-place, transcribe, +# copy, and wrap in GDPSubmodel with forward variable map. function DP.copy_and_reformulate( model::InfiniteOpt.InfiniteModel, decision_vars::Vector{InfiniteOpt.GeneralVariableRef}, @@ -380,7 +328,7 @@ function DP.copy_and_reformulate( ) DP.reformulate_model(model, reform_method) InfiniteOpt.build_transformation_backend!(model) - flat = InfiniteOpt.transformation_model(model) + transcribed = InfiniteOpt.transformation_model(model) transcription_fwd = Dict{InfiniteOpt.GeneralVariableRef, Vector{JuMP.VariableRef}}() for v in DP.collect_all_vars(model) @@ -389,11 +337,11 @@ function DP.copy_and_reformulate( transcription_fwd[v] = isempty(var_prefs) ? [transcription_var] : vec(transcription_var) end - sub_copy, copy_map = JuMP.copy_model(flat) + sub_copy, copy_map = JuMP.copy_model(transcribed) fwd_map = Dict{InfiniteOpt.GeneralVariableRef, Vector{JuMP.VariableRef}}() for v in decision_vars haskey(transcription_fwd, v) || continue - fwd_map[v] = [copy_map[flat_var] for flat_var in transcription_fwd[v]] + fwd_map[v] = [copy_map[transcribed_var] for transcribed_var in transcription_fwd[v]] end sub = DP.GDPSubmodel(sub_copy, decision_vars, fwd_map) JuMP.set_optimizer(sub.model, method.optimizer) @@ -401,8 +349,7 @@ function DP.copy_and_reformulate( return sub end -# Extract per-support-point solutions from the InfiniteOpt transformation -# backend after optimize!(model, ignore_optimize_hook=true). +# Read per-support values from the transformation backend. function DP.extract_solution(model::InfiniteOpt.InfiniteModel) dvars = DP.collect_cutting_planes_vars(model) V = eltype(dvars) @@ -417,36 +364,33 @@ function DP.extract_solution(model::InfiniteOpt.InfiniteModel) return sol end -# Add a flat-sum cut directly to the transformation backend, matching -# the SEP's unweighted Euclidean norm (Trespalacios & Grossmann 2016 -# Eq. 11 applied in the joint transcribed variable space). Then mark -# the backend as ready so the next optimize! reuses the cut-enhanced -# flat model without re-transcribing (which would wipe the cut). +# Add a pointwise-sum cut directly to the transformation backend and mark +# it ready so the next optimize! doesn't re-transcribe and wipe the cut. function DP.add_cut( model::InfiniteOpt.InfiniteModel, decision_vars::Vector{InfiniteOpt.GeneralVariableRef}, rBM_sol::Dict{<:JuMP.AbstractVariableRef, <:Vector{<:Number}}, sep_sol::Dict{<:JuMP.AbstractVariableRef, <:Vector{<:Number}} ) - flat = InfiniteOpt.transformation_model(model) + transcribed = InfiniteOpt.transformation_model(model) cut_expr = zero(JuMP.GenericAffExpr{ - JuMP.value_type(typeof(flat)), - JuMP.variable_ref_type(flat)}) + JuMP.value_type(typeof(transcribed)), + JuMP.variable_ref_type(transcribed)}) for var in decision_vars haskey(rBM_sol, var) || continue haskey(sep_sol, var) || continue rbm_vals = rBM_sol[var] sep_vals = sep_sol[var] transcription_var = InfiniteOpt.transformation_variable(var) - flat_vars = transcription_var isa AbstractArray ? + transcribed_vars = transcription_var isa AbstractArray ? vec(transcription_var) : [transcription_var] - for k in eachindex(flat_vars) + for k in eachindex(transcribed_vars) xi = 2 * (sep_vals[k] - rbm_vals[k]) - JuMP.add_to_expression!(cut_expr, xi, flat_vars[k]) + JuMP.add_to_expression!(cut_expr, xi, transcribed_vars[k]) JuMP.add_to_expression!(cut_expr, -xi * sep_vals[k]) end end - JuMP.@constraint(flat, cut_expr >= 0) + JuMP.@constraint(transcribed, cut_expr >= 0) InfiniteOpt.set_transformation_backend_ready(model, true) return end diff --git a/test/extensions/InfiniteDisjunctiveProgramming.jl b/test/extensions/InfiniteDisjunctiveProgramming.jl index e7fed04d..42ac0914 100644 --- a/test/extensions/InfiniteDisjunctiveProgramming.jl +++ b/test/extensions/InfiniteDisjunctiveProgramming.jl @@ -364,12 +364,6 @@ function test_logical_value() @test eltype(val) == Bool end -# _collect_parameters on model with no infinite parameters. -function test_collect_parameters_no_params() - model = InfiniteGDPModel() - @test_throws ErrorException IDP._collect_parameters(model) -end - # raw_M against an InfiniteModel where M is constant across supports. # Setup: x(t) ∈ [0, 10], disj1: x ≥ 5, disj2: x ≤ 3. # For disj1 slack r(x) = 5 - x maximized over disj2's region x ∈ [0, 3]: @@ -443,8 +437,8 @@ function test_extract_solution_infinite() @test all(v -> isapprox(v, 0.0; atol=1e-6), sol[x]) end -# add_cut adds one flat-sum cut to the transformation backend and marks -# the backend ready so the next optimize! does NOT re-transcribe. +# add_cut adds one pointwise-sum cut to the transformation backend and +# marks the backend ready so the next optimize! does NOT re-transcribe. function test_add_cut_infinite() model = InfiniteGDPModel(HiGHS.Optimizer) set_silent(model) @@ -457,13 +451,13 @@ function test_add_cut_infinite() @disjunction(model, Y) DP.reformulate_model(model, BigM(10.0)) InfiniteOpt.build_transformation_backend!(model) - flat = InfiniteOpt.transformation_model(model) - n_before = JuMP.num_constraints(flat; + transcribed = InfiniteOpt.transformation_model(model) + n_before = JuMP.num_constraints(transcribed; count_variable_in_set_constraints = false) rBM_sol = Dict(x => [1.0, 2.0, 3.0]) sep_sol = Dict(x => [0.5, 1.5, 2.5]) DP.add_cut(model, [x], rBM_sol, sep_sol) - n_after = JuMP.num_constraints(flat; + n_after = JuMP.num_constraints(transcribed; count_variable_in_set_constraints = false) @test n_after == n_before + 1 # set_transformation_backend_ready(true) — next optimize! should @@ -806,10 +800,6 @@ end test_disaggregate_expression_infiniteopt() end - @testset "Internal Helpers" begin - test_collect_parameters_no_params() - end - @testset "MBM" begin test_raw_M_infinite_scalar() test_raw_M_infinite_param_function() From f863f15622e669945cbdcb648cfeab2227eabff6 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Wed, 22 Apr 2026 09:08:08 -0400 Subject: [PATCH 17/34] . --- ext/InfiniteDisjunctiveProgramming.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/ext/InfiniteDisjunctiveProgramming.jl b/ext/InfiniteDisjunctiveProgramming.jl index 4a7f795a..1804f763 100644 --- a/ext/InfiniteDisjunctiveProgramming.jl +++ b/ext/InfiniteDisjunctiveProgramming.jl @@ -247,7 +247,6 @@ function DP.copy_model_with_constraints( Dict{InfiniteOpt.GeneralVariableRef, Vector{JuMP.VariableRef}}()) end -# Return one pointwise slack per support. function DP.prepare_max_M_objective( ::InfiniteOpt.InfiniteModel, obj::JuMP.ScalarConstraint{T, S}, From 350fda3ea0761eeee4392ac71b6d7271507892e8 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Fri, 24 Apr 2026 19:28:14 -0400 Subject: [PATCH 18/34] . --- ext/InfiniteDisjunctiveProgramming.jl | 69 +++++++++++---------------- 1 file changed, 28 insertions(+), 41 deletions(-) diff --git a/ext/InfiniteDisjunctiveProgramming.jl b/ext/InfiniteDisjunctiveProgramming.jl index 1804f763..c09be8a4 100644 --- a/ext/InfiniteDisjunctiveProgramming.jl +++ b/ext/InfiniteDisjunctiveProgramming.jl @@ -221,8 +221,12 @@ function DP.copy_model_with_constraints( func = InfiniteOpt.raw_function(pfunc) prefs = InfiniteOpt.parameter_refs(pfunc) mapped_prefs = Tuple(ref_map[p] for p in prefs) - new_pfunc = _make_parameter_function(mini, func, mapped_prefs...) - ref_map[pfunc] = new_pfunc + pref_arg = length(mapped_prefs) == 1 ? + only(mapped_prefs) : mapped_prefs + param_func = InfiniteOpt.build_parameter_function( + error, func, pref_arg) + ref_map[pfunc] = InfiniteOpt.add_parameter_function( + mini, param_func) end # 5. Add disjunct constraints using existing ref_map @@ -253,9 +257,10 @@ function DP.prepare_max_M_objective( sub::DP.GDPSubmodel ) where {T, S <: _MOI.LessThan} ref_map = sub.model.ext[:inf_mbm_ref_map] - mini_expr = DP._replace_variables_in_constraint(obj.func, ref_map) - return Vector{JuMP.AbstractJuMPScalar}( - InfiniteOpt.transformation_expression(mini_expr - obj.set.upper)) + mini_expr = DP._replace_variables_in_constraint( + obj.func, ref_map) - obj.set.upper + sub.model.ext[:inf_mbm_obj_expr] = obj.func + return InfiniteOpt.transformation_expression(mini_expr) end function DP.prepare_max_M_objective( @@ -264,53 +269,35 @@ function DP.prepare_max_M_objective( sub::DP.GDPSubmodel ) where {T, S <: _MOI.GreaterThan} ref_map = sub.model.ext[:inf_mbm_ref_map] - mini_expr = DP._replace_variables_in_constraint(obj.func, ref_map) - return Vector{JuMP.AbstractJuMPScalar}( - InfiniteOpt.transformation_expression(obj.set.lower - mini_expr)) + mini_expr = obj.set.lower - DP._replace_variables_in_constraint( + obj.func, ref_map) + sub.model.ext[:inf_mbm_obj_expr] = obj.func + return InfiniteOpt.transformation_expression(mini_expr) end # Per-support solve, delegating to scalar base raw_M. Aggregated to a # scalar if uniform, else to a parameter function. function DP.raw_M( sub::DP.GDPSubmodel, - objectives::Vector{<:JuMP.AbstractJuMPScalar}, + objectives::AbstractArray{<:Union{JuMP.AbstractJuMPScalar, Real}}, method::DP._MBM ) - M_vals = typeof(method.default_M)[] - for obj_expr in objectives + M_vals = similar(objectives, typeof(method.default_M)) + for I in eachindex(objectives) JuMP.set_start_value.(JuMP.all_variables(sub.model), nothing) - m = DP.raw_M(sub, obj_expr, method) + m = DP.raw_M(sub, objectives[I], method) m === nothing && return nothing - push!(M_vals, m) + M_vals[I] = m end - model = sub.model.ext[:inf_mbm_main] - # Condense per-support values: scalar if uniform, else pfunc. - all(==(M_vals[1]), M_vals) && return M_vals[1] - prefs = InfiniteOpt.all_parameters(model) - grids = Tuple(Float64.(InfiniteOpt.supports(p)) for p in prefs) - shape = Tuple(length.(grids)) - func = Interpolations.linear_interpolation(grids, reshape(M_vals, shape), - extrapolation_bc = Interpolations.Line()) - return _make_parameter_function(model, func, prefs...) -end - -################################################################################ -# TRANSCRIPTION HELPERS -################################################################################ - -# Replacement for @parameter_function in the case of using an interpolation. -# Example (1D interpolation): -# func = Interpolations.linear_interpolation(grids, vals) -# pfunc = _make_parameter_function(model, func, t) # returns a pfunc ref -function _make_parameter_function( - model::InfiniteOpt.InfiniteModel, func, - prefs::InfiniteOpt.GeneralVariableRef... - ) - wrapped_func = func isa Function ? func : ((args...) -> func(args...)) - pref_arg = length(prefs) == 1 ? only(prefs) : prefs - builder = InfiniteOpt.build_parameter_function( - error, wrapped_func, pref_arg) - return InfiniteOpt.add_parameter_function(model, builder) + all(==(first(M_vals)), M_vals) && return first(M_vals) + main = sub.model.ext[:inf_mbm_main] + expr = sub.model.ext[:inf_mbm_obj_expr] + prefs = InfiniteOpt.parameter_refs(expr) + grids = Tuple(InfiniteOpt.supports(p) for p in prefs) + interp = Interpolations.linear_interpolation(grids, M_vals) + param_func = InfiniteOpt.build_parameter_function( + error, (args...) -> interp(args...), prefs) + return InfiniteOpt.add_parameter_function(main, param_func) end ################################################################################ From 523529452a3be39cf0022c39f404fc38fc5eaf1a Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Sun, 26 Apr 2026 21:25:49 -0400 Subject: [PATCH 19/34] . --- ext/InfiniteDisjunctiveProgramming.jl | 6 +++--- src/mbm.jl | 24 ++++++++++++------------ src/utilities.jl | 10 +++++----- test/constraints/mbm.jl | 24 ++++++++++++------------ 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/ext/InfiniteDisjunctiveProgramming.jl b/ext/InfiniteDisjunctiveProgramming.jl index c09be8a4..5a409203 100644 --- a/ext/InfiniteDisjunctiveProgramming.jl +++ b/ext/InfiniteDisjunctiveProgramming.jl @@ -233,7 +233,7 @@ function DP.copy_model_with_constraints( for cref in constraints cref isa DP.DisjunctConstraintRef || continue con = JuMP.constraint_object(cref) - new_func = DP._replace_variables_in_constraint(con.func, ref_map) + new_func = DP.replace_variables_in_constraint(con.func, ref_map) T = one(JuMP.value_type(typeof(mini))) JuMP.@constraint(mini, new_func * T in con.set) end @@ -257,7 +257,7 @@ function DP.prepare_max_M_objective( sub::DP.GDPSubmodel ) where {T, S <: _MOI.LessThan} ref_map = sub.model.ext[:inf_mbm_ref_map] - mini_expr = DP._replace_variables_in_constraint( + mini_expr = DP.replace_variables_in_constraint( obj.func, ref_map) - obj.set.upper sub.model.ext[:inf_mbm_obj_expr] = obj.func return InfiniteOpt.transformation_expression(mini_expr) @@ -269,7 +269,7 @@ function DP.prepare_max_M_objective( sub::DP.GDPSubmodel ) where {T, S <: _MOI.GreaterThan} ref_map = sub.model.ext[:inf_mbm_ref_map] - mini_expr = obj.set.lower - DP._replace_variables_in_constraint( + mini_expr = obj.set.lower - DP.replace_variables_in_constraint( obj.func, ref_map) sub.model.ext[:inf_mbm_obj_expr] = obj.func return InfiniteOpt.transformation_expression(mini_expr) diff --git a/src/mbm.jl b/src/mbm.jl index 4fd844c3..76ca939f 100644 --- a/src/mbm.jl +++ b/src/mbm.jl @@ -250,7 +250,7 @@ function prepare_max_M_objective( sub::GDPSubmodel ) where {T, S <: _MOI.LessThan} flat_map = Dict(v => ws[1] for (v, ws) in sub.fwd_map) - expr = -obj.set.upper + _replace_variables_in_constraint(obj.func, flat_map) + expr = -obj.set.upper + replace_variables_in_constraint(obj.func, flat_map) return expr end @@ -260,7 +260,7 @@ function prepare_max_M_objective( sub::GDPSubmodel ) where {T, S <: _MOI.GreaterThan} flat_map = Dict(v => ws[1] for (v, ws) in sub.fwd_map) - expr = obj.set.lower - _replace_variables_in_constraint(obj.func, flat_map) + expr = obj.set.lower - replace_variables_in_constraint(obj.func, flat_map) return expr end @@ -462,7 +462,7 @@ function copy_model_with_constraints( for cref in constraints con = JuMP.constraint_object(cref) flat_map = Dict(v => only(ws) for (v, ws) in fwd_map) - expr = _replace_variables_in_constraint(con.func, flat_map) + expr = replace_variables_in_constraint(con.func, flat_map) T = one(JuMP.value_type(typeof(sub_model))) JuMP.@constraint(sub_model, expr * T in con.set) end @@ -480,7 +480,7 @@ end # Replace variable refs in an expression using a map. Uses AbstractDict # because the InfiniteModel MBM path maps decision vars to VariableRefs # and parameter functions to Numbers in the same dict (via _build_flat_map). -function _replace_variables_in_constraint( +function replace_variables_in_constraint( fun::JuMP.AbstractVariableRef, var_map::AbstractDict ) @@ -501,7 +501,7 @@ function _var_ref_type( return V end -function _replace_variables_in_constraint( +function replace_variables_in_constraint( fun::T, var_map::AbstractDict ) where {T <: JuMP.GenericAffExpr} C = JuMP.value_type(T) @@ -514,7 +514,7 @@ function _replace_variables_in_constraint( return new_aff end -function _replace_variables_in_constraint( +function replace_variables_in_constraint( fun::T, var_map::AbstractDict ) where {T <: JuMP.GenericQuadExpr} C = JuMP.value_type(T) @@ -524,23 +524,23 @@ function _replace_variables_in_constraint( 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) + new_aff = replace_variables_in_constraint(fun.aff, var_map) JuMP.add_to_expression!(new_quad, new_aff) return new_quad end -function _replace_variables_in_constraint(fun::Number, var_map::AbstractDict) +function replace_variables_in_constraint(fun::Number, var_map::AbstractDict) return fun end -function _replace_variables_in_constraint(fun::T, +function replace_variables_in_constraint(fun::T, var_map::AbstractDict) where {T <: JuMP.GenericNonlinearExpr} - new_args = Any[_replace_variables_in_constraint( + new_args = Any[replace_variables_in_constraint( arg, var_map) for arg in fun.args] return T(fun.head, new_args) end -function _replace_variables_in_constraint(fun::Vector, var_map::AbstractDict) - return [_replace_variables_in_constraint(expr, +function replace_variables_in_constraint(fun::Vector, var_map::AbstractDict) + return [replace_variables_in_constraint(expr, var_map) for expr in fun] end diff --git a/src/utilities.jl b/src/utilities.jl index b0203d70..bca1b748 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -30,7 +30,7 @@ function copy_and_reformulate( orig_to_copy = Dict{V, V}( v => ref_map[v] for v in decision_vars) JuMP.@objective(copy, sense, - _replace_variables_in_constraint(obj, orig_to_copy) + replace_variables_in_constraint(obj, orig_to_copy) ) fwd_map = Dict{V, Vector{V}}(v => [ref_map[v]] for v in decision_vars) sub = GDPSubmodel(copy, decision_vars, fwd_map) @@ -221,7 +221,7 @@ function copy_gdp_data( old_con_ref = LogicalConstraintRef(model, idx) new_con_ref = LogicalConstraintRef(new_model, idx) c = lc_data.constraint - expr = _replace_variables_in_constraint(c.func, lv_map) + 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 @@ -233,7 +233,7 @@ function copy_gdp_data( 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, + new_expr = replace_variables_in_constraint(old_constraint.func, var_map ) # Update to new_gdp.disjunct_constraints @@ -247,7 +247,7 @@ function copy_gdp_data( # Copying disjunctions for (idx, disj_data) in old_gdp.disjunctions old_disj = disj_data.constraint - new_indicators = [_replace_variables_in_constraint(indicator, lv_map) + new_indicators = [replace_variables_in_constraint(indicator, lv_map) for indicator in old_disj.indicators ] new_disj = Disjunction(new_indicators, old_disj.nested) @@ -383,7 +383,7 @@ 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) + return replace_variables_in_constraint(bref, var_map) end function _remap_constraint_to_indicator( diff --git a/test/constraints/mbm.jl b/test/constraints/mbm.jl index 80f9a27f..a6854bdc 100644 --- a/test/constraints/mbm.jl +++ b/test/constraints/mbm.jl @@ -27,7 +27,7 @@ function test__var_ref_type_numeric_map() @test DP._var_ref_type(typeof(aff), var_map) == VariableRef end -# _replace_variables_in_constraint with QuadExpr where var_map +# replace_variables_in_constraint with QuadExpr where var_map # maps some vars to Numbers. Covers lines 569, 571, 574. function test__replace_variables_quad_numeric_map() model = Model() @@ -38,21 +38,21 @@ function test__replace_variables_quad_numeric_map() # both Number (line 569) map1 = Dict{VariableRef, Any}(x[1] => 2.0, x[2] => 3.0) - result1 = DP._replace_variables_in_constraint(quad1, map1) + result1 = DP.replace_variables_in_constraint(quad1, map1) @test result1.aff.constant ≈ 6.0 # ra Number, rb VariableRef (line 571) map2 = Dict{VariableRef, Any}(x[1] => 2.0, x[2] => y) - result2 = DP._replace_variables_in_constraint(quad1, map2) + result2 = DP.replace_variables_in_constraint(quad1, map2) @test result2.aff.terms[y] ≈ 2.0 # rb Number, ra VariableRef (line 574) map3 = Dict{VariableRef, Any}(x[1] => y, x[2] => 3.0) - result3 = DP._replace_variables_in_constraint(quad1, map3) + result3 = DP.replace_variables_in_constraint(quad1, map3) @test result3.aff.terms[y] ≈ 3.0 end -function test_replace_variables_in_constraint() +function testreplace_variables_in_constraint() model = Model() sub_model = Model() @variable(model, x[1:3]) @@ -64,14 +64,14 @@ function test_replace_variables_in_constraint() #Test GenericVariableRef new_vars = Dict{AbstractVariableRef, AbstractVariableRef}() [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( + 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( + expr2 = DP.replace_variables_in_constraint( constraint_object(con2).func, new_vars) - expr3 = DP._replace_variables_in_constraint( + expr3 = DP.replace_variables_in_constraint( constraint_object(con3).func, new_vars) - expr4 = DP._replace_variables_in_constraint( + 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]]) @@ -80,7 +80,7 @@ function test_replace_variables_in_constraint() expected = JuMP.@expression(sub_model, sin(new_vars[x[3]]) - 0.0) @test JuMP.isequal_canonical(expr3, expected) @test expr4 == [new_vars[x[i]] for i in 1:3] - @test_throws MethodError DP._replace_variables_in_constraint( + @test_throws MethodError DP.replace_variables_in_constraint( "String", new_vars) end @@ -794,7 +794,7 @@ end test_mbm() test__var_ref_type_numeric_map() test__replace_variables_quad_numeric_map() - test_replace_variables_in_constraint() + testreplace_variables_in_constraint() test_prepare_max_M_objective() test_raw_M() test_maximize_M() From 32ccde59f594212f31654b1f9c2376a79e1e0ef5 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Sun, 26 Apr 2026 21:33:34 -0400 Subject: [PATCH 20/34] . --- test/constraints/mbm.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/constraints/mbm.jl b/test/constraints/mbm.jl index a6854bdc..0c96dbbb 100644 --- a/test/constraints/mbm.jl +++ b/test/constraints/mbm.jl @@ -52,7 +52,7 @@ function test__replace_variables_quad_numeric_map() @test result3.aff.terms[y] ≈ 3.0 end -function testreplace_variables_in_constraint() +function test_replace_variables_in_constraint() model = Model() sub_model = Model() @variable(model, x[1:3]) @@ -794,7 +794,7 @@ end test_mbm() test__var_ref_type_numeric_map() test__replace_variables_quad_numeric_map() - testreplace_variables_in_constraint() + test_replace_variables_in_constraint() test_prepare_max_M_objective() test_raw_M() test_maximize_M() From bb03b3bd248ca508650d4a7a7266c70f9a3420cc Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Sun, 26 Apr 2026 21:59:53 -0400 Subject: [PATCH 21/34] . --- ext/InfiniteDisjunctiveProgramming.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/InfiniteDisjunctiveProgramming.jl b/ext/InfiniteDisjunctiveProgramming.jl index 5a409203..efd80d40 100644 --- a/ext/InfiniteDisjunctiveProgramming.jl +++ b/ext/InfiniteDisjunctiveProgramming.jl @@ -282,7 +282,7 @@ function DP.raw_M( objectives::AbstractArray{<:Union{JuMP.AbstractJuMPScalar, Real}}, method::DP._MBM ) - M_vals = similar(objectives, typeof(method.default_M)) + M_vals = Array{typeof(method.default_M)}(undef, size(objectives)) for I in eachindex(objectives) JuMP.set_start_value.(JuMP.all_variables(sub.model), nothing) m = DP.raw_M(sub, objectives[I], method) From f2387f5437097a99825c1c9c708d1ba5af33876b Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Sun, 26 Apr 2026 23:29:14 -0400 Subject: [PATCH 22/34] . --- src/cuttingplanes.jl | 31 +++++++++++++++---------------- src/utilities.jl | 13 +++++++++++++ test/constraints/cuttingplanes.jl | 4 ++-- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/cuttingplanes.jl b/src/cuttingplanes.jl index dbc818e5..603dbb4c 100644 --- a/src/cuttingplanes.jl +++ b/src/cuttingplanes.jl @@ -8,25 +8,17 @@ function collect_cutting_planes_vars(model::JuMP.AbstractModel) return collect_all_vars(model) end -# Extract solution from a solved model (in-place). Extensions -# override for models where values live on a backend. +# Read primal values from a solved model. Returns a scalar-valued +# `Dict{var, value}`, skipping fixed vars. CP callers wrap to +# per-support `Vector` shape via `_cp_per_support`. The InfiniteOpt +# extension overrides this dispatch to give per-support `Vector` +# values directly. function extract_solution(model::JuMP.AbstractModel) dvars = collect_cutting_planes_vars(model) V = eltype(dvars) T = JuMP.value_type(typeof(model)) - return Dict{V, Vector{T}}( - v => [JuMP.value(v)] for v in dvars) -end - -# Extract solution from a GDPSubmodel (SEP path). -function extract_solution(sub::GDPSubmodel) - V = eltype(sub.decision_vars) - T = JuMP.value_type(typeof(sub.model)) - sol = Dict{V, Vector{T}}() - for var in sub.decision_vars - sol[var] = JuMP.value.(sub.fwd_map[var]) - end - return sol + return Dict{V, T}( + v => JuMP.value(v) for v in dvars if !JuMP.is_fixed(v)) end # Set quadratic separation objective: min Σ (x_k - rBM_k)². @@ -113,7 +105,7 @@ function reformulate_model( # Cutting plane loop: rBM <-> SEP until convergence for iter in 1:method.max_iter JuMP.optimize!(model, ignore_optimize_hook = true) - rBM_sol = extract_solution(model) + rBM_sol = _cp_per_support(extract_solution(model)) separation_obj, separation_sol = _solve_separation(separation, rBM_sol) if separation_obj <= method.seperation_tolerance break @@ -127,6 +119,13 @@ function reformulate_model( return end +# Wrap scalar `extract_solution(model)` values into 1-element +# `Vector`s — uniform per-support shape that the CP loop expects. +# `Vector` values (from the InfiniteOpt extension's per-support read) +# pass through unchanged. +_cp_per_support(point::AbstractDict) = + Dict(v => val isa AbstractVector ? val : [val] for (v, val) in point) + ################################################################################ # ERROR MESSAGES ################################################################################ diff --git a/src/utilities.jl b/src/utilities.jl index bca1b748..da090eac 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -8,6 +8,19 @@ function _copy_model( return M() end +""" + extract_solution(sub::GDPSubmodel) + +Read the primal solution of `sub.model` after a solve, keyed by the +parent-model decision variables via `sub.fwd_map`. Shape follows +`fwd_map` values: `Vector`-valued fwd_maps (CP/MBM) yield per-support +`Vector`s; scalar fwd_maps (LOA feas) yield scalars. +""" +function extract_solution(sub::GDPSubmodel) + return Dict( + var => JuMP.value.(sub.fwd_map[var]) for var in sub.decision_vars) +end + """ copy_and_reformulate(model, decision_vars, reform_method, method) diff --git a/test/constraints/cuttingplanes.jl b/test/constraints/cuttingplanes.jl index 9f3a7931..e15976dd 100644 --- a/test/constraints/cuttingplanes.jl +++ b/test/constraints/cuttingplanes.jl @@ -146,7 +146,7 @@ function test_cp_cut_generation() JuMP.set_silent(model) relaxed = DP.relax_logical_vars(model) optimize!(model, ignore_optimize_hook = true) - rBM_sol = DP.extract_solution(model) + rBM_sol = DP._cp_per_support(DP.extract_solution(model)) # Solve SEP DP._set_separation_objective(separation, rBM_sol) @@ -168,7 +168,7 @@ function test_cp_cut_generation() # Re-solve with cut → should tighten optimize!(model, ignore_optimize_hook = true) rBM_sol2 = DP.extract_solution(model) - @test rBM_sol2[x][1] ≈ 4.0 atol = 0.1 + @test rBM_sol2[x] ≈ 4.0 atol = 0.1 DP.unrelax_logical_vars(relaxed) end From fbdafa0c9e888b2e37f8006ada24eecccf1860c2 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Sun, 26 Apr 2026 23:44:18 -0400 Subject: [PATCH 23/34] . --- src/cuttingplanes.jl | 23 ++++++++--------------- test/constraints/cuttingplanes.jl | 4 ++-- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/cuttingplanes.jl b/src/cuttingplanes.jl index 603dbb4c..63692258 100644 --- a/src/cuttingplanes.jl +++ b/src/cuttingplanes.jl @@ -8,17 +8,17 @@ function collect_cutting_planes_vars(model::JuMP.AbstractModel) return collect_all_vars(model) end -# Read primal values from a solved model. Returns a scalar-valued -# `Dict{var, value}`, skipping fixed vars. CP callers wrap to -# per-support `Vector` shape via `_cp_per_support`. The InfiniteOpt -# extension overrides this dispatch to give per-support `Vector` -# values directly. +# Read primal values from a solved model. Returns +# `Dict{var, Vector{value}}` — per-support shape uniformly: finite +# models trivially have one "support" (length-1 Vector), the +# InfiniteOpt extension overrides this dispatch to populate +# multi-support Vectors. Skips fixed vars. function extract_solution(model::JuMP.AbstractModel) dvars = collect_cutting_planes_vars(model) V = eltype(dvars) T = JuMP.value_type(typeof(model)) - return Dict{V, T}( - v => JuMP.value(v) for v in dvars if !JuMP.is_fixed(v)) + return Dict{V, Vector{T}}( + v => [JuMP.value(v)] for v in dvars if !JuMP.is_fixed(v)) end # Set quadratic separation objective: min Σ (x_k - rBM_k)². @@ -105,7 +105,7 @@ function reformulate_model( # Cutting plane loop: rBM <-> SEP until convergence for iter in 1:method.max_iter JuMP.optimize!(model, ignore_optimize_hook = true) - rBM_sol = _cp_per_support(extract_solution(model)) + rBM_sol = extract_solution(model) separation_obj, separation_sol = _solve_separation(separation, rBM_sol) if separation_obj <= method.seperation_tolerance break @@ -119,13 +119,6 @@ function reformulate_model( return end -# Wrap scalar `extract_solution(model)` values into 1-element -# `Vector`s — uniform per-support shape that the CP loop expects. -# `Vector` values (from the InfiniteOpt extension's per-support read) -# pass through unchanged. -_cp_per_support(point::AbstractDict) = - Dict(v => val isa AbstractVector ? val : [val] for (v, val) in point) - ################################################################################ # ERROR MESSAGES ################################################################################ diff --git a/test/constraints/cuttingplanes.jl b/test/constraints/cuttingplanes.jl index e15976dd..9f3a7931 100644 --- a/test/constraints/cuttingplanes.jl +++ b/test/constraints/cuttingplanes.jl @@ -146,7 +146,7 @@ function test_cp_cut_generation() JuMP.set_silent(model) relaxed = DP.relax_logical_vars(model) optimize!(model, ignore_optimize_hook = true) - rBM_sol = DP._cp_per_support(DP.extract_solution(model)) + rBM_sol = DP.extract_solution(model) # Solve SEP DP._set_separation_objective(separation, rBM_sol) @@ -168,7 +168,7 @@ function test_cp_cut_generation() # Re-solve with cut → should tighten optimize!(model, ignore_optimize_hook = true) rBM_sol2 = DP.extract_solution(model) - @test rBM_sol2[x] ≈ 4.0 atol = 0.1 + @test rBM_sol2[x][1] ≈ 4.0 atol = 0.1 DP.unrelax_logical_vars(relaxed) end From 153c1e796f013d006347845397cb74dc77a32e4e Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Mon, 27 Apr 2026 10:04:22 -0400 Subject: [PATCH 24/34] . --- src/cuttingplanes.jl | 20 ++++++++++++++------ src/utilities.jl | 13 ------------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/cuttingplanes.jl b/src/cuttingplanes.jl index 63692258..dbc818e5 100644 --- a/src/cuttingplanes.jl +++ b/src/cuttingplanes.jl @@ -8,17 +8,25 @@ function collect_cutting_planes_vars(model::JuMP.AbstractModel) return collect_all_vars(model) end -# Read primal values from a solved model. Returns -# `Dict{var, Vector{value}}` — per-support shape uniformly: finite -# models trivially have one "support" (length-1 Vector), the -# InfiniteOpt extension overrides this dispatch to populate -# multi-support Vectors. Skips fixed vars. +# Extract solution from a solved model (in-place). Extensions +# override for models where values live on a backend. function extract_solution(model::JuMP.AbstractModel) dvars = collect_cutting_planes_vars(model) V = eltype(dvars) T = JuMP.value_type(typeof(model)) return Dict{V, Vector{T}}( - v => [JuMP.value(v)] for v in dvars if !JuMP.is_fixed(v)) + v => [JuMP.value(v)] for v in dvars) +end + +# Extract solution from a GDPSubmodel (SEP path). +function extract_solution(sub::GDPSubmodel) + V = eltype(sub.decision_vars) + T = JuMP.value_type(typeof(sub.model)) + sol = Dict{V, Vector{T}}() + for var in sub.decision_vars + sol[var] = JuMP.value.(sub.fwd_map[var]) + end + return sol end # Set quadratic separation objective: min Σ (x_k - rBM_k)². diff --git a/src/utilities.jl b/src/utilities.jl index da090eac..bca1b748 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -8,19 +8,6 @@ function _copy_model( return M() end -""" - extract_solution(sub::GDPSubmodel) - -Read the primal solution of `sub.model` after a solve, keyed by the -parent-model decision variables via `sub.fwd_map`. Shape follows -`fwd_map` values: `Vector`-valued fwd_maps (CP/MBM) yield per-support -`Vector`s; scalar fwd_maps (LOA feas) yield scalars. -""" -function extract_solution(sub::GDPSubmodel) - return Dict( - var => JuMP.value.(sub.fwd_map[var]) for var in sub.decision_vars) -end - """ copy_and_reformulate(model, decision_vars, reform_method, method) From d053c0700b513663d0a4618a3b4dfb8cff2f628e Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Mon, 27 Apr 2026 10:49:18 -0400 Subject: [PATCH 25/34] . --- ext/InfiniteDisjunctiveProgramming.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/ext/InfiniteDisjunctiveProgramming.jl b/ext/InfiniteDisjunctiveProgramming.jl index efd80d40..66d745f0 100644 --- a/ext/InfiniteDisjunctiveProgramming.jl +++ b/ext/InfiniteDisjunctiveProgramming.jl @@ -196,7 +196,6 @@ function DP.copy_model_with_constraints( # 2. Copy decision variables with bounds for v in JuMP.all_variables(model) - _is_parameter(v) && continue prefs = InfiniteOpt.parameter_refs(v) var_type = isempty(prefs) ? nothing : InfiniteOpt.Infinite(Tuple(ref_map[p] for p in prefs)...) From b0184fc4ef43ec13fb4d3b9be08aa9d7a62f4cae Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Fri, 1 May 2026 00:01:42 -0400 Subject: [PATCH 26/34] Update to get_variable_info, copy_model_with_constraints, prepare_max_M_objective and raw_M in InfGDP --- ext/InfiniteDisjunctiveProgramming.jl | 57 ++++++++++--------- src/mbm.jl | 2 +- src/variables.jl | 4 +- test/constraints/mbm.jl | 15 +++++ .../InfiniteDisjunctiveProgramming.jl | 2 +- 5 files changed, 51 insertions(+), 29 deletions(-) diff --git a/ext/InfiniteDisjunctiveProgramming.jl b/ext/InfiniteDisjunctiveProgramming.jl index 66d745f0..693845bd 100644 --- a/ext/InfiniteDisjunctiveProgramming.jl +++ b/ext/InfiniteDisjunctiveProgramming.jl @@ -237,17 +237,20 @@ function DP.copy_model_with_constraints( JuMP.@constraint(mini, new_func * T in con.set) end - # 6. Transcribe mini InfiniteModel + # 6. Build the transformation backend so transcribed exists and + # configure its solver. We hold mini in sub.model and recover + # transcribed lazily via transformation_model(mini). InfiniteOpt.build_transformation_backend!(mini) transcribed = InfiniteOpt.transformation_model(mini) JuMP.set_optimizer(transcribed, method.optimizer) JuMP.set_silent(transcribed) - # Stash for prepare_max_M_objective / raw_M. - transcribed.ext[:inf_mbm_main] = model - transcribed.ext[:inf_mbm_ref_map] = ref_map - # GDPSubmodel's fwd_map / decision_vars are CP-only; unused here. - return DP.GDPSubmodel(transcribed, InfiniteOpt.GeneralVariableRef[], - Dict{InfiniteOpt.GeneralVariableRef, Vector{JuMP.VariableRef}}()) + # 7. Wrap ref_map as fwd_map (singleton vectors) so + # prepare_max_M_objective can use the standard flat_map idiom. + fwd_map = Dict{InfiniteOpt.GeneralVariableRef, + Vector{InfiniteOpt.GeneralVariableRef}}( + v => [w] for (v, w) in ref_map) + return DP.GDPSubmodel( + mini, DP.collect_all_vars(model), fwd_map) end function DP.prepare_max_M_objective( @@ -255,11 +258,9 @@ function DP.prepare_max_M_objective( obj::JuMP.ScalarConstraint{T, S}, sub::DP.GDPSubmodel ) where {T, S <: _MOI.LessThan} - ref_map = sub.model.ext[:inf_mbm_ref_map] - mini_expr = DP.replace_variables_in_constraint( - obj.func, ref_map) - obj.set.upper - sub.model.ext[:inf_mbm_obj_expr] = obj.func - return InfiniteOpt.transformation_expression(mini_expr) + flat_map = Dict(v => ws[1] for (v, ws) in sub.fwd_map) + obj_func = DP.replace_variables_in_constraint(obj.func, flat_map) + return obj_func - obj.set.upper end function DP.prepare_max_M_objective( @@ -267,31 +268,35 @@ function DP.prepare_max_M_objective( obj::JuMP.ScalarConstraint{T, S}, sub::DP.GDPSubmodel ) where {T, S <: _MOI.GreaterThan} - ref_map = sub.model.ext[:inf_mbm_ref_map] - mini_expr = obj.set.lower - DP.replace_variables_in_constraint( - obj.func, ref_map) - sub.model.ext[:inf_mbm_obj_expr] = obj.func - return InfiniteOpt.transformation_expression(mini_expr) + flat_map = Dict(v => ws[1] for (v, ws) in sub.fwd_map) + obj_func = DP.replace_variables_in_constraint(obj.func, flat_map) + return obj.set.lower - obj_func end -# Per-support solve, delegating to scalar base raw_M. Aggregated to a -# scalar if uniform, else to a parameter function. +# Transcribe mini_expr, solve per support on the transcribed JuMP +# model, and aggregate to a scalar if uniform, else to a parameter +# function on main. function DP.raw_M( - sub::DP.GDPSubmodel, - objectives::AbstractArray{<:Union{JuMP.AbstractJuMPScalar, Real}}, + sub::DP.GDPSubmodel{<:InfiniteOpt.InfiniteModel}, + mini_expr::JuMP.AbstractJuMPScalar, method::DP._MBM ) + objectives = InfiniteOpt.transformation_expression(mini_expr) + transcribed = InfiniteOpt.transformation_model(sub.model) + inner_sub = DP.GDPSubmodel(transcribed,JuMP.VariableRef[], + Dict{JuMP.VariableRef, Vector{JuMP.VariableRef}}() + ) M_vals = Array{typeof(method.default_M)}(undef, size(objectives)) for I in eachindex(objectives) - JuMP.set_start_value.(JuMP.all_variables(sub.model), nothing) - m = DP.raw_M(sub, objectives[I], method) + m = DP.raw_M(inner_sub, objectives[I], method) m === nothing && return nothing M_vals[I] = m end all(==(first(M_vals)), M_vals) && return first(M_vals) - main = sub.model.ext[:inf_mbm_main] - expr = sub.model.ext[:inf_mbm_obj_expr] - prefs = InfiniteOpt.parameter_refs(expr) + mini_prefs = InfiniteOpt.parameter_refs(mini_expr) + reverse_map = Dict(ws[1] => v for (v, ws) in sub.fwd_map) + prefs = Tuple(reverse_map[p] for p in mini_prefs) + main = JuMP.owner_model(first(prefs)) grids = Tuple(InfiniteOpt.supports(p) for p in prefs) interp = Interpolations.linear_interpolation(grids, M_vals) param_func = InfiniteOpt.build_parameter_function( diff --git a/src/mbm.jl b/src/mbm.jl index 76ca939f..23c5096e 100644 --- a/src/mbm.jl +++ b/src/mbm.jl @@ -273,7 +273,7 @@ Returns `max(obj_value, 0)` on optimal, `nothing` on infeasible `method.default_M` otherwise (unbounded, numerical failure, etc). """ function raw_M( - sub::GDPSubmodel, + sub::GDPSubmodel{<:JuMP.AbstractModel}, objective::JuMP.AbstractJuMPScalar, method::_MBM ) diff --git a/src/variables.jl b/src/variables.jl index f96cfe02..e3369db2 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -547,7 +547,9 @@ function get_variable_info(vref::JuMP.AbstractVariableRef; has_lb::Bool = JuMP.has_lower_bound(vref), has_ub::Bool = JuMP.has_upper_bound(vref), has_fix::Bool = JuMP.is_fixed(vref), - has_start::Bool = JuMP.has_start_value(vref), + has_start::Bool = JuMP.has_start_value(vref) && + !(JuMP.start_value(vref) isa Number && + isnan(JuMP.start_value(vref))), has_binary::Bool = JuMP.is_binary(vref), has_integer::Bool = JuMP.is_integer(vref), lower_bound::Union{Number, Function} = has_lb ? JuMP.lower_bound(vref) : 0, diff --git a/test/constraints/mbm.jl b/test/constraints/mbm.jl index 0c96dbbb..11f164da 100644 --- a/test/constraints/mbm.jl +++ b/test/constraints/mbm.jl @@ -783,6 +783,20 @@ function test_get_variable_info() @test info_custom.has_ub == false end +# A NaN-valued start is treated as no start so the NaN doesn't +# propagate to copies or to solver inputs. +function test_get_variable_info_nan_start() + model = GDPModel() + @variable(model, x) + JuMP.set_start_value(x, NaN) + @test JuMP.has_start_value(x) == true + @test isnan(JuMP.start_value(x)) + + info = DP.get_variable_info(x) + @test info.has_start == false + @test info.start == 0 +end + @testset "MBM" begin test__copy_model() test_variable_properties() @@ -791,6 +805,7 @@ end test_variable_copy() test__copy_model_with_constraints() test_get_variable_info() + test_get_variable_info_nan_start() test_mbm() test__var_ref_type_numeric_map() test__replace_variables_quad_numeric_map() diff --git a/test/extensions/InfiniteDisjunctiveProgramming.jl b/test/extensions/InfiniteDisjunctiveProgramming.jl index 42ac0914..c000ebc7 100644 --- a/test/extensions/InfiniteDisjunctiveProgramming.jl +++ b/test/extensions/InfiniteDisjunctiveProgramming.jl @@ -381,7 +381,7 @@ function test_raw_M_infinite_scalar() model, DP.DisjunctConstraintRef[con2], mbm) obj = DP.prepare_max_M_objective( model, JuMP.constraint_object(con), sub) - @test length(obj) == 5 # K support points + @test length(InfiniteOpt.parameter_refs(obj)) == 1 @test DP.raw_M(sub, obj, mbm) == 5.0 end From 2909c8416925a7b74eff727263ff83caa97a5fa5 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Fri, 1 May 2026 11:59:38 -0400 Subject: [PATCH 27/34] Internal constant interpolation --- Project.toml | 7 +--- ext/InfiniteDisjunctiveProgramming.jl | 30 ++++++++++++-- .../InfiniteDisjunctiveProgramming.jl | 39 +++++++++++++++++-- 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/Project.toml b/Project.toml index ae0dad94..7b4a2728 100644 --- a/Project.toml +++ b/Project.toml @@ -9,15 +9,13 @@ Reexport = "189a3867-3050-52da-a836-e630ba90ab69" [weakdeps] InfiniteOpt = "20393b10-9daf-11e9-18c9-8db751c92c57" -Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" [extensions] -InfiniteDisjunctiveProgramming = ["InfiniteOpt", "Interpolations"] +InfiniteDisjunctiveProgramming = "InfiniteOpt" [compat] Aqua = "0.8" InfiniteOpt = "0.6" -Interpolations = "0.16.2" Ipopt = "1.9.0" JuMP = "1.18" Juniper = "0.9.3" @@ -28,10 +26,9 @@ julia = "1.10" Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" InfiniteOpt = "20393b10-9daf-11e9-18c9-8db751c92c57" -Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" Juniper = "2ddba703-00a4-53a7-87a5-e8b9971dde84" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Aqua", "HiGHS", "InfiniteOpt", "Test", "Juniper", "Ipopt", "Interpolations"] +test = ["Aqua", "HiGHS", "InfiniteOpt", "Test", "Juniper", "Ipopt"] diff --git a/ext/InfiniteDisjunctiveProgramming.jl b/ext/InfiniteDisjunctiveProgramming.jl index 693845bd..05b7219e 100644 --- a/ext/InfiniteDisjunctiveProgramming.jl +++ b/ext/InfiniteDisjunctiveProgramming.jl @@ -1,7 +1,7 @@ module InfiniteDisjunctiveProgramming import JuMP.MOI as _MOI -import InfiniteOpt, JuMP, Interpolations +import InfiniteOpt, JuMP import DisjunctiveProgramming as DP ################################################################################ @@ -273,6 +273,31 @@ function DP.prepare_max_M_objective( return obj.set.lower - obj_func end +# Constant interpolation +function _interpolate( + grids::NTuple{N, AbstractVector{<:Real}}, + values::AbstractArray{<:Real, N} + ) where {N} + # mimic the call form of Interpolations.jl's interpolation + return (args...) -> _interpolate_at(grids, values, args) +end + +function _interpolate_at( + grids::NTuple{N, AbstractVector{<:Real}}, + values::AbstractArray{<:Real, N}, + args::NTuple{N, <:Real} + ) where {N} + # lower-corner cell index per dimension + idx_lo = ntuple(d -> + clamp(searchsortedlast(grids[d], args[d]),1, length(grids[d]) - 1), N + ) + # max over the 2^N corners; bit d of k picks lower or upper + return maximum( + values[ntuple(d -> idx_lo[d] +((k >> (d - 1)) & 1), N)...] + for k in 0:(2^N - 1) + ) +end + # Transcribe mini_expr, solve per support on the transcribed JuMP # model, and aggregate to a scalar if uniform, else to a parameter # function on main. @@ -298,9 +323,8 @@ function DP.raw_M( prefs = Tuple(reverse_map[p] for p in mini_prefs) main = JuMP.owner_model(first(prefs)) grids = Tuple(InfiniteOpt.supports(p) for p in prefs) - interp = Interpolations.linear_interpolation(grids, M_vals) param_func = InfiniteOpt.build_parameter_function( - error, (args...) -> interp(args...), prefs) + error, _interpolate(grids, M_vals), prefs) return InfiniteOpt.add_parameter_function(main, param_func) end diff --git a/test/extensions/InfiniteDisjunctiveProgramming.jl b/test/extensions/InfiniteDisjunctiveProgramming.jl index c000ebc7..47bb884b 100644 --- a/test/extensions/InfiniteDisjunctiveProgramming.jl +++ b/test/extensions/InfiniteDisjunctiveProgramming.jl @@ -1,4 +1,4 @@ -using InfiniteOpt, HiGHS, Ipopt, Juniper, Interpolations +using InfiniteOpt, HiGHS, Ipopt, Juniper import DisjunctiveProgramming as DP # Helper to access internal function @@ -387,8 +387,8 @@ end # raw_M with a support-varying M. Setup: x(t) ∈ [0, 10], disj1: x ≤ 2t, # disj2: x ≥ 0.5. Slack r(x) = x - 2t maximized over x ∈ [0.5, 10]: -# max(x - 2t) = 10 - 2t. Varies with t ⇒ raw_M returns a pfunc; the -# underlying function should evaluate to 10 - 2t at each support. +# max(x - 2t) = 10 - 2t. Varies with t ⇒ raw_M returns a pfunc whose +# raw values at supports are max-of-cell upper bounds for 10 - 2t. function test_raw_M_infinite_param_function() model = InfiniteGDPModel() supports = [0.0, 0.25, 0.5, 0.75, 1.0] @@ -407,11 +407,41 @@ function test_raw_M_infinite_param_function() M = DP.raw_M(sub, obj, mbm) @test M isa InfiniteOpt.GeneralVariableRef raw_fn = InfiniteOpt.raw_function(M) + # max-of-corners is conservative: raw_fn(t) ≥ 10 - 2t at supports. for t_val in supports - @test raw_fn(t_val) ≈ 10.0 - 2*t_val atol=1e-6 + @test raw_fn(t_val) >= 10.0 - 2*t_val - 1e-6 end end +# Piecewise-constant max-of-corners: returns the maximum value over +# the 2^n corners of the cell containing the query. +function test_interpolate() + grid1 = [0.0, 1.0, 2.0, 3.0] + vals1 = [10.0, 20.0, 40.0, 50.0] + f = IDP._interpolate((grid1,), vals1) + # At grid points: max over the cell to the right (or last cell). + @test f(0.0) == 20.0 # max(vals[1], vals[2]) + @test f(1.0) == 40.0 # max(vals[2], vals[3]) + @test f(3.0) == 50.0 # last cell: max(vals[3], vals[4]) + # Between grid points: max of the surrounding two values. + @test f(0.5) == 20.0 + @test f(1.5) == 40.0 + @test f(2.25) == 50.0 + # Out-of-range clamps to the boundary cell. + @test f(-1.0) == 20.0 + @test f(4.0) == 50.0 + + # 2D: max over the 4 surrounding corners. + gx = [0.0, 1.0, 2.0] + gy = [0.0, 10.0] + vals2 = [x * y for x in gx, y in gy] # 3x2 matrix + g = IDP._interpolate((gx, gy), vals2) + @test g(0.0, 0.0) == 10.0 # corners (0,0)=0, (1,0)=0, (0,10)=0, (1,10)=10 + @test g(2.0, 10.0) == 20.0 # last cell, max corner is (2,10)=20 + @test g(0.5, 5.0) == 10.0 # corners 0,0,0,10 -> 10 + @test g(1.5, 5.0) == 20.0 # corners 0,0,10,20 -> 20 +end + # extract_solution returns per-support values from the transformation # backend. Setup: force disj 2 active (x ≤ 3), BigM-reformulate, solve # min ∫x ⇒ x = 0 at every support. @@ -801,6 +831,7 @@ end end @testset "MBM" begin + test_interpolate() test_raw_M_infinite_scalar() test_raw_M_infinite_param_function() test_mbm_finite_and_integer_var() From bb769992125f1122afef5eefb1c6f18f2cd657a9 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Wed, 13 May 2026 22:55:28 -0400 Subject: [PATCH 28/34] Change to copy_model_with_constraints to use copy_model(::InfiniteModel) --- ext/InfiniteDisjunctiveProgramming.jl | 84 ++++++++------------------- 1 file changed, 25 insertions(+), 59 deletions(-) diff --git a/ext/InfiniteDisjunctiveProgramming.jl b/ext/InfiniteDisjunctiveProgramming.jl index 05b7219e..6399c9c1 100644 --- a/ext/InfiniteDisjunctiveProgramming.jl +++ b/ext/InfiniteDisjunctiveProgramming.jl @@ -174,83 +174,49 @@ end # MBM FOR INFINITEMODEL ################################################################################ -# Build a mini InfiniteModel holding only the given disjunct constraints, -# transcribe it, and return as a GDPSubmodel. +# Copy the InfiniteModel, strip everything but VariableInfo bounds, +# add back the selected disjunct constraints, transcribe, and return +# only the other disjunct's constraints plus variable bounds. function DP.copy_model_with_constraints( model::InfiniteOpt.InfiniteModel, constraints::Vector{<:DP.DisjunctConstraintRef}, method::DP._MBM ) - mini = InfiniteOpt.InfiniteModel() - ref_map = Dict{InfiniteOpt.GeneralVariableRef, - InfiniteOpt.GeneralVariableRef}() + mini, ref_map = JuMP.copy_model(model) - # 1. Copy infinite parameters with their supports - for p in InfiniteOpt.all_parameters(model) - domain = InfiniteOpt.infinite_domain(p) - supports = Float64.(InfiniteOpt.supports(p)) - param = InfiniteOpt.build_parameter(error, domain; supports = supports) - new_param = InfiniteOpt.add_parameter(mini, param, JuMP.name(p)) - ref_map[p] = new_param - end - - # 2. Copy decision variables with bounds - for v in JuMP.all_variables(model) - prefs = InfiniteOpt.parameter_refs(v) - var_type = isempty(prefs) ? nothing : - InfiniteOpt.Infinite(Tuple(ref_map[p] for p in prefs)...) - props = DP.VariableProperties( - DP.get_variable_info(v), "", nothing, var_type) - ref_map[v] = DP.create_variable(mini, props) - end - - # 3. Copy derivatives with their bounds - for d in InfiniteOpt.all_derivatives(model) - vref = InfiniteOpt.derivative_argument(d) - pref = InfiniteOpt.operator_parameter(d) - new_d = InfiniteOpt.deriv(ref_map[vref], ref_map[pref]) - info = DP.get_variable_info(d) - info.has_lb && JuMP.set_lower_bound(new_d, info.lower_bound) - info.has_ub && JuMP.set_upper_bound(new_d, info.upper_bound) - ref_map[d] = new_d - end - - # 4. Copy parameter functions (needed by ref_map substitution) - for pfunc in InfiniteOpt.all_parameter_functions(model) - func = InfiniteOpt.raw_function(pfunc) - prefs = InfiniteOpt.parameter_refs(pfunc) - mapped_prefs = Tuple(ref_map[p] for p in prefs) - pref_arg = length(mapped_prefs) == 1 ? - only(mapped_prefs) : mapped_prefs - param_func = InfiniteOpt.build_parameter_function( - error, func, pref_arg) - ref_map[pfunc] = InfiniteOpt.add_parameter_function( - mini, param_func) + # Drop global constraints. + for cref in JuMP.all_constraints(mini) + JuMP.delete(mini, cref) end - # 5. Add disjunct constraints using existing ref_map for cref in constraints - cref isa DP.DisjunctConstraintRef || continue con = JuMP.constraint_object(cref) - new_func = DP.replace_variables_in_constraint(con.func, ref_map) T = one(JuMP.value_type(typeof(mini))) - JuMP.@constraint(mini, new_func * T in con.set) + JuMP.@constraint(mini, ref_map[con.func] * T in con.set) end - # 6. Build the transformation backend so transcribed exists and - # configure its solver. We hold mini in sub.model and recover - # transcribed lazily via transformation_model(mini). InfiniteOpt.build_transformation_backend!(mini) transcribed = InfiniteOpt.transformation_model(mini) JuMP.set_optimizer(transcribed, method.optimizer) JuMP.set_silent(transcribed) - # 7. Wrap ref_map as fwd_map (singleton vectors) so - # prepare_max_M_objective can use the standard flat_map idiom. + + # fwd_map needs every ref reachable from disjunct constraints — + # decision vars + parameters + parameter functions so the + # objective substitution in `prepare_max_M_objective` can look up + # any term it sees. + decision_vars = DP.collect_all_vars(model) fwd_map = Dict{InfiniteOpt.GeneralVariableRef, - Vector{InfiniteOpt.GeneralVariableRef}}( - v => [w] for (v, w) in ref_map) - return DP.GDPSubmodel( - mini, DP.collect_all_vars(model), fwd_map) + Vector{InfiniteOpt.GeneralVariableRef}}() + for v in decision_vars + fwd_map[v] = [ref_map[v]] + end + for p in InfiniteOpt.all_parameters(model) + fwd_map[p] = [ref_map[p]] + end + for pf in InfiniteOpt.all_parameter_functions(model) + fwd_map[pf] = [ref_map[pf]] + end + return DP.GDPSubmodel(mini, decision_vars, fwd_map) end function DP.prepare_max_M_objective( From fb5f381282e518323c5a95834301cab619a02dd8 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Wed, 13 May 2026 23:07:27 -0400 Subject: [PATCH 29/34] Project.toml revert --- Project.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Project.toml b/Project.toml index 7b4a2728..3ca54b77 100644 --- a/Project.toml +++ b/Project.toml @@ -15,20 +15,20 @@ InfiniteDisjunctiveProgramming = "InfiniteOpt" [compat] Aqua = "0.8" -InfiniteOpt = "0.6" -Ipopt = "1.9.0" JuMP = "1.18" -Juniper = "0.9.3" Reexport = "1" julia = "1.10" +Juniper = "0.9.3" +Ipopt = "1.9.0" +InfiniteOpt = "0.6" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" -InfiniteOpt = "20393b10-9daf-11e9-18c9-8db751c92c57" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" Juniper = "2ddba703-00a4-53a7-87a5-e8b9971dde84" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +InfiniteOpt = "20393b10-9daf-11e9-18c9-8db751c92c57" [targets] -test = ["Aqua", "HiGHS", "InfiniteOpt", "Test", "Juniper", "Ipopt"] +test = ["Aqua", "HiGHS", "Test", "Juniper", "Ipopt", "InfiniteOpt"] From e420f982aa88ed61348f4dc66a667211b0d85b55 Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Wed, 13 May 2026 23:23:27 -0400 Subject: [PATCH 30/34] Using master version of InfiniteOpt --- .github/workflows/CI.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index acae65fc..8d852299 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -25,6 +25,8 @@ jobs: with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} + - name: Use InfiniteOpt master + run: julia --color=yes --project=. -e 'using Pkg; Pkg.develop(url="https://github.com/infiniteopt/InfiniteOpt.jl")' - uses: julia-actions/julia-runtest@latest - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v4 From c0ca8fe8b8b6e8535751785eb5cb94ad49e91afd Mon Sep 17 00:00:00 2001 From: dnguyen227 Date: Wed, 13 May 2026 23:27:44 -0400 Subject: [PATCH 31/34] Reverting CI change --- .github/workflows/CI.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 8d852299..acae65fc 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -25,8 +25,6 @@ jobs: with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - name: Use InfiniteOpt master - run: julia --color=yes --project=. -e 'using Pkg; Pkg.develop(url="https://github.com/infiniteopt/InfiniteOpt.jl")' - uses: julia-actions/julia-runtest@latest - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v4 From cd9dd700a6cf5b978f2a7867ecdd0017b01f1ac7 Mon Sep 17 00:00:00 2001 From: dnguyen227 <82475321+dnguyen227@users.noreply.github.com> Date: Tue, 19 May 2026 11:34:24 -0400 Subject: [PATCH 32/34] Update InfiniteOpt version to 0.6.2 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 3ca54b77..e1d401ae 100644 --- a/Project.toml +++ b/Project.toml @@ -20,7 +20,7 @@ Reexport = "1" julia = "1.10" Juniper = "0.9.3" Ipopt = "1.9.0" -InfiniteOpt = "0.6" +InfiniteOpt = "0.6.2" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" From bb0db434f6e721beec5d8fcf85cf6919ceea6d77 Mon Sep 17 00:00:00 2001 From: d227nguyen Date: Sun, 24 May 2026 16:37:13 -0400 Subject: [PATCH 33/34] Add filter_constraints workflow --- ext/InfiniteDisjunctiveProgramming.jl | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/ext/InfiniteDisjunctiveProgramming.jl b/ext/InfiniteDisjunctiveProgramming.jl index 6399c9c1..5f77dce7 100644 --- a/ext/InfiniteDisjunctiveProgramming.jl +++ b/ext/InfiniteDisjunctiveProgramming.jl @@ -182,12 +182,11 @@ function DP.copy_model_with_constraints( constraints::Vector{<:DP.DisjunctConstraintRef}, method::DP._MBM ) - mini, ref_map = JuMP.copy_model(model) - - # Drop global constraints. - for cref in JuMP.all_constraints(mini) - JuMP.delete(mini, cref) - end + # Filter out every source constraint at copy time instead of + # copying then deleting. Equivalent end state, fewer allocations. + mini, ref_map = JuMP.copy_model( + model; filter_constraints = cref -> false + ) for cref in constraints con = JuMP.constraint_object(cref) From 34b3ea877bcecf6a82d00054d1a110ad425a2545 Mon Sep 17 00:00:00 2001 From: dnguyen227 <82475321+dnguyen227@users.noreply.github.com> Date: Mon, 25 May 2026 19:08:26 -0400 Subject: [PATCH 34/34] Bump InfiniteOpt version to 0.6.3 Updated InfiniteOpt version from 0.6.2 to 0.6.3. --- Project.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index e1d401ae..f3fbb43e 100644 --- a/Project.toml +++ b/Project.toml @@ -20,7 +20,7 @@ Reexport = "1" julia = "1.10" Juniper = "0.9.3" Ipopt = "1.9.0" -InfiniteOpt = "0.6.2" +InfiniteOpt = "0.6.3" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" @@ -28,7 +28,6 @@ HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" Juniper = "2ddba703-00a4-53a7-87a5-e8b9971dde84" -InfiniteOpt = "20393b10-9daf-11e9-18c9-8db751c92c57" [targets] test = ["Aqua", "HiGHS", "Test", "Juniper", "Ipopt", "InfiniteOpt"]