diff --git a/ext/InfiniteDisjunctiveProgramming.jl b/ext/InfiniteDisjunctiveProgramming.jl index 5f77dce..ce947b7 100644 --- a/ext/InfiniteDisjunctiveProgramming.jl +++ b/ext/InfiniteDisjunctiveProgramming.jl @@ -38,6 +38,70 @@ function DP.requires_disaggregation(vref::InfiniteOpt.GeneralVariableRef) return !_is_parameter(vref) end +# Bound info for parameter refs in disjunct constraints: parameter +# functions report their support extrema, finite parameters a point, +# other parameters their domain bounds. Returns nothing for +# variables. +function _parameter_bound_info( + vref::InfiniteOpt.GeneralVariableRef + )::Union{Nothing, Tuple{Float64, Float64}} + _is_parameter(vref) || return nothing + dvref = InfiniteOpt.dispatch_variable_ref(vref) + if dvref isa InfiniteOpt.ParameterFunctionRef + prefs = InfiniteOpt.parameter_list(dvref) + length(prefs) == 1 || return (-Inf, Inf) + supports = InfiniteOpt.supports(first(prefs)) + isempty(supports) && return (-Inf, Inf) + func = InfiniteOpt.raw_function(dvref) + func_values = [func(s) for s in supports] + return (minimum(func_values), maximum(func_values)) + elseif dvref isa InfiniteOpt.FiniteParameterRef + value = InfiniteOpt.parameter_value(dvref) + return (value, value) + end + lb = JuMP.has_lower_bound(vref) ? JuMP.lower_bound(vref) : -Inf + ub = JuMP.has_upper_bound(vref) ? JuMP.upper_bound(vref) : Inf + return (lb, ub) +end + +function DP.set_variable_bound_info( + vref::InfiniteOpt.GeneralVariableRef, ::DP.BigM) + info = _parameter_bound_info(vref) + info === nothing || return info + lb = JuMP.has_lower_bound(vref) ? JuMP.lower_bound(vref) : -Inf + ub = JuMP.has_upper_bound(vref) ? JuMP.upper_bound(vref) : Inf + return lb, ub +end + +# Hull and PSplit require finite bounds that include 0 +function _zero_inclusive_bounds( + vref::InfiniteOpt.GeneralVariableRef, + info::Union{Nothing, Tuple{Float64, Float64}}, + method_name::String + )::Tuple{Float64, Float64} + info === nothing || return (min(0, info[1]), max(0, info[2])) + if !JuMP.has_lower_bound(vref) || !JuMP.has_upper_bound(vref) + error("Variable $vref must have both lower and upper " * + "bounds defined when using the $method_name " * + "reformulation.") + end + return (min(0, JuMP.lower_bound(vref)), + max(0, JuMP.upper_bound(vref))) +end + +function DP.set_variable_bound_info( + vref::InfiniteOpt.GeneralVariableRef, ::DP.Hull) + return _zero_inclusive_bounds(vref, + _parameter_bound_info(vref), "Hull") +end + +function DP.set_variable_bound_info( + vref::InfiniteOpt.GeneralVariableRef, + ::Union{DP.PSplit, DP._PSplit}) + return _zero_inclusive_bounds(vref, + _parameter_bound_info(vref), "PSplit") +end + function DP.VariableProperties(vref::InfiniteOpt.GeneralVariableRef) info = DP.get_variable_info(vref) name = JuMP.name(vref) diff --git a/test/extensions/InfiniteDisjunctiveProgramming.jl b/test/extensions/InfiniteDisjunctiveProgramming.jl index 47bb884..ba5d57b 100644 --- a/test/extensions/InfiniteDisjunctiveProgramming.jl +++ b/test/extensions/InfiniteDisjunctiveProgramming.jl @@ -121,6 +121,37 @@ function test_requires_disaggregation() @test DP.requires_disaggregation(y) == true end +# Bound info for parameter refs in disjunct constraints: parameter +# functions report their support extrema, finite parameters a point, +# other parameters their domain bounds; Hull/PSplit clamp the bounds +# to include 0 and error when a variable is missing bounds. +function test_parameter_bound_info() + model = InfiniteGDPModel() + @infinite_parameter(model, t in [0, 1], supports = [0.0, 0.5, 1.0]) + @infinite_parameter(model, s in [0, 1]) + @finite_parameter(model, p == 2.0) + @variable(model, 0 <= x <= 10, Infinite(t)) + @variable(model, y, Infinite(t)) + @parameter_function(model, pf == t -> 2t - 1) + @parameter_function(model, pf2 == (t, s) -> t + s) + @parameter_function(model, pf3 == s -> s) + @test DP.set_variable_bound_info(pf, BigM()) == (-1.0, 1.0) + @test DP.set_variable_bound_info(p, BigM()) == (2.0, 2.0) + @test DP.set_variable_bound_info(t, BigM()) == (0.0, 1.0) + @test DP.set_variable_bound_info(x, BigM()) == (0.0, 10.0) + @test DP.set_variable_bound_info(y, BigM()) == (-Inf, Inf) + # multi-parameter and support-less parameter functions fall back + @test DP.set_variable_bound_info(pf2, BigM()) == (-Inf, Inf) + @test DP.set_variable_bound_info(pf3, BigM()) == (-Inf, Inf) + # Hull and PSplit clamp the bounds to include 0 + @test DP.set_variable_bound_info(pf, Hull()) == (-1.0, 1.0) + @test DP.set_variable_bound_info(t, Hull()) == (0.0, 1.0) + @test DP.set_variable_bound_info(p, Hull()) == (0.0, 2.0) + @test DP.set_variable_bound_info(x, Hull()) == (0.0, 10.0) + @test DP.set_variable_bound_info(p, PSplit([[x]])) == (0.0, 2.0) + @test_throws ErrorException DP.set_variable_bound_info(y, Hull()) +end + function test_all_variables_infiniteopt() model = InfiniteGDPModel() @infinite_parameter(model, t ∈ [0, 1]) @@ -806,6 +837,7 @@ end test_is_parameter() test_is_parameter_concrete_dispatches() test_requires_disaggregation() + test_parameter_bound_info() test_variable_properties_infiniteopt() test_variable_properties_from_expr() test_variable_properties_from_quad_expr()