diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f8a1345..d215f67 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: version: - - '1.6' + - '1.10' os: - ubuntu-latest - macOS-latest diff --git a/Project.toml b/Project.toml index ceda3d3..edd639a 100644 --- a/Project.toml +++ b/Project.toml @@ -7,13 +7,20 @@ version = "0.5.0" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" +[weakdeps] +InfiniteOpt = "20393b10-9daf-11e9-18c9-8db751c92c57" + +[extensions] +InfiniteDisjunctiveProgramming = "InfiniteOpt" + [compat] Aqua = "0.8" JuMP = "1.18" Reexport = "1" -julia = "1.6" +julia = "1.10" Juniper = "0.9.3" Ipopt = "1.9.0" +InfiniteOpt = "0.6" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" @@ -23,4 +30,4 @@ Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" Juniper = "2ddba703-00a4-53a7-87a5-e8b9971dde84" [targets] -test = ["Aqua", "HiGHS", "Test", "Juniper", "Ipopt"] +test = ["Aqua", "HiGHS", "Test", "Juniper", "Ipopt","InfiniteOpt"] diff --git a/ext/InfiniteDisjunctiveProgramming.jl b/ext/InfiniteDisjunctiveProgramming.jl new file mode 100644 index 0000000..fad26ba --- /dev/null +++ b/ext/InfiniteDisjunctiveProgramming.jl @@ -0,0 +1,187 @@ +module InfiniteDisjunctiveProgramming + +import JuMP.MOI as _MOI +import InfiniteOpt, JuMP +import DisjunctiveProgramming as DP + +################################################################################ +# MODEL +################################################################################ +function DP.InfiniteGDPModel(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) +end + +################################################################################ +# VARIABLES +################################################################################ +DP.InfiniteLogical(prefs...) = DP.Logical(InfiniteOpt.Infinite(prefs...)) + +_is_parameter(vref::InfiniteOpt.GeneralVariableRef) = + _is_parameter(InfiniteOpt.dispatch_variable_ref(vref)) +_is_parameter(::InfiniteOpt.DependentParameterRef) = true +_is_parameter(::InfiniteOpt.IndependentParameterRef) = true +_is_parameter(::InfiniteOpt.ParameterFunctionRef) = true +_is_parameter(::InfiniteOpt.FiniteParameterRef) = true +_is_parameter(::Any) = false + +function DP.requires_disaggregation(vref::InfiniteOpt.GeneralVariableRef) + return !_is_parameter(vref) +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) +end + +# 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 + prefs = InfiniteOpt.parameter_refs(expr) + info = DP._free_variable_info() + var_type = !isempty(prefs) ? InfiniteOpt.Infinite(prefs...) : nothing + return DP.VariableProperties(info, "", nothing, var_type) +end + +function DP.VariableProperties( + exprs::Vector{<:Union{ + InfiniteOpt.GeneralVariableRef, + JuMP.GenericAffExpr{<:Any, InfiniteOpt.GeneralVariableRef}, + JuMP.GenericQuadExpr{<:Any, InfiniteOpt.GeneralVariableRef}, + JuMP.GenericNonlinearExpr{InfiniteOpt.GeneralVariableRef} + }} +) + all_prefs = Set{InfiniteOpt.GeneralVariableRef}() + for expr in exprs + for pref in InfiniteOpt.parameter_refs(expr) + push!(all_prefs, pref) + end + end + prefs = Tuple(all_prefs) + info = DP._free_variable_info() + var_type = !isempty(prefs) ? InfiniteOpt.Infinite(prefs...) : nothing + return DP.VariableProperties(info, "", nothing, var_type) +end + +function JuMP.value(vref::DP.LogicalVariableRef{InfiniteOpt.InfiniteModel}) + return JuMP.value(DP.binary_variable(vref)) .>= 0.5 +end + +################################################################################ +# CONSTRAINTS +################################################################################ +function JuMP.add_constraint( + model::InfiniteOpt.InfiniteModel, + c::JuMP.VectorConstraint{F, S}, + name::String = "" +) where {F, S <: DP.AbstractCardinalitySet} + return DP._add_cardinality_constraint(model, c, name) +end + +function JuMP.add_constraint( + model::M, + c::JuMP.ScalarConstraint{DP._LogicalExpr{M}, S}, + name::String = "" +) where {S, M <: InfiniteOpt.InfiniteModel} + return DP._add_logical_constraint(model, c, name) +end + +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.") +end + +function JuMP.add_constraint( + model::M, + c::JuMP.ScalarConstraint{ + JuMP.GenericAffExpr{C, DP.LogicalVariableRef{M}}, S + }, + name::String = "" +) 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 + }, + name::String = "" +) where {M <: InfiniteOpt.InfiniteModel, S, C} + error("Cannot add, subtract, or multiply with logical variables.") +end + +################################################################################ +# METHODS +################################################################################ +function DP.get_constant( + expr::JuMP.GenericAffExpr{T, InfiniteOpt.GeneralVariableRef} +) where {T} + constant = JuMP.constant(expr) + param_expr = zero(typeof(expr)) + for (var, coeff) in expr.terms + if _is_parameter(var) + JuMP.add_to_expression!(param_expr, coeff, var) + end + end + return constant + param_expr +end + +function DP.disaggregate_expression( + model::M, + aff::JuMP.GenericAffExpr, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + method::DP._Hull +) 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) + push!(terms, coeff * vref * bvref) + elseif !haskey(method.disjunct_variables, (vref, bvref)) + push!(terms, coeff * vref) + else + dvref = method.disjunct_variables[vref, bvref] + push!(terms, coeff * dvref) + end + end + return JuMP.@expression(model, sum(terms)) +end + +################################################################################ +# ERROR MESSAGES +################################################################################ +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.") +end + +function DP.reformulate_model(::InfiniteOpt.InfiniteModel, ::DP.cutting_planes) + error("The `cutting_planes` method is not supported for `InfiniteModel`." * + "Please use `BigM`, `Hull`, `Indicator`, or `PSplit` instead.") +end + +end + diff --git a/src/DisjunctiveProgramming.jl b/src/DisjunctiveProgramming.jl index 5dd7e3b..85f0345 100644 --- a/src/DisjunctiveProgramming.jl +++ b/src/DisjunctiveProgramming.jl @@ -25,6 +25,7 @@ include("hull.jl") include("mbm.jl") include("indicator.jl") include("print.jl") +include("extension_api.jl") include("utilities.jl") include("psplit.jl") diff --git a/src/cuttingplanes.jl b/src/cuttingplanes.jl index 5644a7b..6d2c387 100644 --- a/src/cuttingplanes.jl +++ b/src/cuttingplanes.jl @@ -12,8 +12,8 @@ function reformulate_model( rBM, rBM_ref_map, _ = copy_gdp_model(model) reformulate_model(rBM, BigM(method.M_value)) reformulate_model(SEP, Hull()) - main_to_SEP_map = Dict(v => sep_ref_map[v] for v in all_variables(model)) - main_to_rBM_map = Dict(v => rBM_ref_map[v] for v in all_variables(model)) + main_to_SEP_map = Dict(v => sep_ref_map[v] for v in collect_all_vars(model)) + main_to_rBM_map = Dict(v => rBM_ref_map[v] for v in collect_all_vars(model)) JuMP.set_optimizer(SEP, method.optimizer) JuMP.set_optimizer(rBM, method.optimizer) JuMP.set_silent(rBM) @@ -55,7 +55,7 @@ function _solve_rBM( ) where {M <: JuMP.AbstractModel} T = JuMP.value_type(M) optimize!(rBM, ignore_optimize_hook = true) - rBM_vars = JuMP.all_variables(rBM) + rBM_vars = collect_all_vars(rBM) #Solution to be passed to SEP model. sol = Dict{JuMP.AbstractVariableRef,T}(var => zero(T) for var in rBM_vars) @@ -73,7 +73,7 @@ function _solve_SEP( rBM_to_SEP_map::Dict{<:JuMP.AbstractVariableRef,<:JuMP.AbstractVariableRef} ) where {M <: JuMP.AbstractModel, T <: Number} - SEP_vars = [rBM_to_SEP_map[rBM_var] for rBM_var in JuMP.all_variables(rBM)] + SEP_vars = [rBM_to_SEP_map[rBM_var] for rBM_var in collect_all_vars(rBM)] #Modified objective function for SEP. obj_expr = sum( @@ -98,7 +98,7 @@ function _cutting_planes( rBM_sol::Dict{<:JuMP.AbstractVariableRef,T}, SEP_sol::Dict{<:JuMP.AbstractVariableRef,T}, ) where {M <: JuMP.AbstractModel, T <: Number} - main_vars = JuMP.all_variables(model) + main_vars = collect_all_vars(model) #Cutting plane generation ξ_sep = Dict{JuMP.AbstractVariableRef,T}(var =>zero(T) for var in main_vars) diff --git a/src/datatypes.jl b/src/datatypes.jl index 3a5f09d..6ba1315 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -488,7 +488,7 @@ struct PSplit{V <: JuMP.AbstractVariableRef} <: AbstractReformulationMethod function PSplit(n_parts::Int, model::JuMP.AbstractModel) n_parts > 0 || error("Number of partitions must be positive, got $n_parts") - variables = collect(JuMP.all_variables(model)) + variables = collect_all_vars(model) n_vars = length(variables) n_parts = min(n_parts, n_vars) @@ -620,19 +620,32 @@ mutable struct VariableProperties{L, U, F, S, SET, T} end function VariableProperties(vref::JuMP.GenericVariableRef{T}) where T - info = JuMP.VariableInfo( - JuMP.has_lower_bound(vref), - JuMP.has_lower_bound(vref) ? JuMP.lower_bound(vref) : zero(T), - JuMP.has_upper_bound(vref), - JuMP.has_upper_bound(vref) ? JuMP.upper_bound(vref) : zero(T), - JuMP.is_fixed(vref), - JuMP.is_fixed(vref) ? JuMP.fix_value(vref) : zero(T), - !isnothing(JuMP.start_value(vref)), - JuMP.start_value(vref), - JuMP.is_binary(vref), - JuMP.is_integer(vref) - ) + info = get_variable_info(vref) name = JuMP.name(vref) set = JuMP.is_variable_in_set(vref) ? JuMP.moi_set(JuMP.constraint_object(JuMP.VariableInSetRef(vref))) : nothing return VariableProperties(info, name, set, nothing) end + +function VariableProperties(vref::JuMP.AbstractVariableRef) + info = get_variable_info(vref) + name = JuMP.name(vref) + return VariableProperties(info, name, nothing, nothing) +end + +""" + VariableProperties(expr)::VariableProperties + +Creates a `VariableProperties` object with blank variable info (no bounds, not fixed, +not binary/integer) from an expression. The `expr` argument is provided for +extensions to infer additional properties (e.g., parameter dependencies in InfiniteOpt). + +## Arguments +- `expr`: Expression for extensions to extract metadata from + +## Returns +A `VariableProperties` object with blank info. +""" +function VariableProperties(expr) + info = _free_variable_info() + return VariableProperties(info, "", nothing, nothing) +end \ No newline at end of file diff --git a/src/extension_api.jl b/src/extension_api.jl new file mode 100644 index 0000000..ad3566f --- /dev/null +++ b/src/extension_api.jl @@ -0,0 +1,40 @@ +""" + InfiniteGDPModel(args...; kwargs...) + +Creates an `InfiniteOpt.InfiniteModel` that is compatible with the +capabiltiies provided by DisjunctiveProgramming.jl. This requires +that InfiniteOpt be imported first. + +**Example** +```julia +julia> using DisjunctiveProgramming, InfiniteOpt + +julia> InfiniteGDPModel() + +``` +""" +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 +first imported. + +**Example** +```julia +julia> using DisjunctiveProgramming, InfiniteOpt + +julia> model = InfiniteGDPModel(); + +julia> @infinite_parameter(model, t in [0, 1]); + +julia> @infinite_parameter(model, x[1:2] in [-1, 1]); + +julia> @variable(model, Y, InfiniteLogical(t, x)) # creates Y(t, x) in {True, False} +Y(t, x) +``` +""" +function InfiniteLogical end diff --git a/src/hull.jl b/src/hull.jl index 8ad5209..779369e 100644 --- a/src/hull.jl +++ b/src/hull.jl @@ -21,6 +21,7 @@ function _disaggregate_variables( _disaggregate_variable(model, lvref, vref, method) #create disaggregated var for that disjunct end end + function _disaggregate_variable( model::M, lvref::LogicalVariableRef, @@ -29,20 +30,11 @@ function _disaggregate_variable( ) where {M <: JuMP.AbstractModel} #create disaggregated vref lb, ub = variable_bound_info(vref) - T = JuMP.value_type(M) - info = JuMP.VariableInfo( - true, # has_lb = true - lb, # lower_bound = lb - true, # has_ub = true - ub, # upper_bound = ub - false, # has_fix = false - zero(T), # fixed_value = 0 - false, # has_start = false - zero(T), # start = 0 - false, # binary = false - false # integer = false - ) - properties = VariableProperties(info, "$(vref)_$(lvref)", nothing, nothing) + info = get_variable_info(vref; has_lb = true, has_ub = true, + lower_bound = lb, upper_bound = ub) + old_props = VariableProperties(vref) + properties = VariableProperties(info, "$(vref)_$(lvref)", + old_props.set, old_props.variable_type) dvref = create_variable(model, properties) push!(_reformulation_variables(model), dvref) #get binary indicator variable @@ -60,6 +52,7 @@ function _disaggregate_variable( return dvref end +#TODO: Throw error for fix, bin, integer ################################################################################ # VARIABLE AGGREGATION ################################################################################ @@ -69,7 +62,11 @@ function _aggregate_variable( vref::JuMP.AbstractVariableRef, method::_Hull ) + JuMP.is_binary(vref) && return #skip binary variables + if isempty(method.disjunction_variables[vref]) + return # Variable wasn't disaggregated, skip aggregation + end con_expr = JuMP.@expression(model, -vref + sum(method.disjunction_variables[vref])) push!(ref_cons, JuMP.build_constraint(error, con_expr, _MOI.EqualTo(0))) return @@ -79,7 +76,26 @@ end # CONSTRAINT DISAGGREGATION ################################################################################ # variable -function _disaggregate_expression( +""" + disaggregate_expression( + model::JuMP.AbstractModel, + expr, + bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, + method::_Hull + ) + +Disaggregate an expression for the Hull reformulation. This function is dispatched +based on the expression type: + +- `vref::JuMP.AbstractVariableRef`: Returns the disaggregated variable if it exists, + otherwise returns the original variable (for binary variables or nested disaggregated variables). +- `aff::JuMP.GenericAffExpr`: Disaggregates each term in the affine expression. +- `quad::JuMP.GenericQuadExpr`: Disaggregates both the affine and quadratic parts of the expression. + +The disaggregated expression is multiplied by the binary indicator variable `bvref` +to enforce the disjunctive constraint. +""" +function disaggregate_expression( model::JuMP.AbstractModel, vref::JuMP.AbstractVariableRef, bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, @@ -92,7 +108,7 @@ function _disaggregate_expression( end end # affine expression -function _disaggregate_expression( +function disaggregate_expression( model::JuMP.AbstractModel, aff::JuMP.GenericAffExpr, bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, @@ -109,17 +125,18 @@ function _disaggregate_expression( end return new_expr end + # quadratic expression # TODO review what happens when there are bilinear terms with binary variables involved since these are not being disaggregated # (e.g., complementarity constraints; though likely irrelevant)... -function _disaggregate_expression( +function disaggregate_expression( model::JuMP.AbstractModel, quad::JuMP.GenericQuadExpr, bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::_Hull ) #get affine part - new_expr = _disaggregate_expression(model, quad.aff, bvref, method) + new_expr = disaggregate_expression(model, quad.aff, bvref, method) #get quadratic part ϵ = method.value for (pair, coeff) in quad.terms @@ -246,7 +263,7 @@ function reformulate_disjunct_constraint( bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::_Hull ) where {T <: JuMP.AbstractJuMPScalar, S <: Union{_MOI.LessThan, _MOI.GreaterThan, _MOI.EqualTo}} - new_func = _disaggregate_expression(model, con.func, bvref, method) + new_func = disaggregate_expression(model, con.func, bvref, method) set_value = _set_value(con.set) new_func -= set_value*bvref reform_con = JuMP.build_constraint(error, new_func, S(0)) @@ -259,7 +276,7 @@ function reformulate_disjunct_constraint( method::_Hull ) where {T <: JuMP.AbstractJuMPScalar, S <: Union{_MOI.Nonpositives, _MOI.Nonnegatives, _MOI.Zeros}, R} new_func = JuMP.@expression(model, [i=1:con.set.dimension], - _disaggregate_expression(model, con.func[i], bvref, method) + disaggregate_expression(model, con.func[i], bvref, method) ) reform_con = JuMP.build_constraint(error, new_func, con.set) return [reform_con] @@ -307,7 +324,7 @@ function reformulate_disjunct_constraint( bvref::Union{JuMP.AbstractVariableRef, JuMP.GenericAffExpr}, method::_Hull ) where {T <: JuMP.AbstractJuMPScalar, S <: _MOI.Interval} - new_func = _disaggregate_expression(model, con.func, bvref, method) + new_func = disaggregate_expression(model, con.func, bvref, method) new_func_gt = JuMP.@expression(model, new_func - con.set.lower*bvref) new_func_lt = JuMP.@expression(model, new_func - con.set.upper*bvref) reform_con_gt = JuMP.build_constraint(error, new_func_gt, _MOI.GreaterThan(0)) diff --git a/src/mbm.jl b/src/mbm.jl index 58120a8..e9b1f13 100644 --- a/src/mbm.jl +++ b/src/mbm.jl @@ -353,7 +353,7 @@ function _mini_model( var_type = JuMP.variable_ref_type(model) sub_model = _copy_model(model) new_vars = Dict{var_type, var_type}() - for var in JuMP.all_variables(model) + for var in collect_all_vars(model) new_vars[var] = variable_copy(sub_model, var) end for con in [JuMP.constraint_object(con) for con in constraints] diff --git a/src/psplit.jl b/src/psplit.jl index dea6789..be4dea1 100644 --- a/src/psplit.jl +++ b/src/psplit.jl @@ -6,7 +6,7 @@ function _build_partitioned_expression( expr::T, partition_variables::Vector{<:JuMP.AbstractVariableRef} ) where {T <: JuMP.GenericAffExpr} - constant = JuMP.constant(expr) + constant = get_constant(expr) new_affexpr = zero(T) for var in partition_variables JuMP.add_to_expression!(new_affexpr, JuMP.coefficient(expr, var), var) @@ -19,7 +19,7 @@ function _build_partitioned_expression( partition_variables::Vector{<:JuMP.AbstractVariableRef} ) where {T <: JuMP.GenericQuadExpr} new_quadexpr = zero(T) - constant = JuMP.constant(expr) + constant = get_constant(expr) for var in partition_variables for (pair, coeff) in expr.terms if pair.a == var && pair.b == var @@ -205,6 +205,7 @@ function reformulate_disjunction( append!(ref_cons, partitioned_constraints) union!(aux_vars, vars) end + psplit = _PSplit(method, model) psplit.hull = _Hull(Hull(), union(disj_vrefs, aux_vars)) psplit.sum_constraints = sum_constraints @@ -220,6 +221,7 @@ function reformulate_disjunction( for vref in aux_vars _aggregate_variable(model, ref_cons, vref, psplit.hull) end + return ref_cons end @@ -279,19 +281,19 @@ function _build_partitioned_constraint( ) where {M <: JuMP.AbstractModel, T, S <: _MOI.LessThan} val_type = JuMP.value_type(M) p = length(method.partition) - v = [@variable(model, base_name = "v_$(hash(con))_$(i)") for i in 1:p] + v = Vector{JuMP.variable_ref_type(M)}(undef, p) _, constant = _build_partitioned_expression(con.func, method.partition[p]) part_con = Vector{JuMP.AbstractConstraint}(undef, p) for i in 1:p func, _ = _build_partitioned_expression(con.func, method.partition[i]) + v[i] = create_variable(model, VariableProperties(func)) + JuMP.set_name(v[i], "v_$(hash(con))_$(i)") part_con[i] = JuMP.build_constraint(error, func - v[i], MOI.LessThan(zero(val_type)) ) _bound_auxiliary(model, v[i], func, method) end - sum_con = JuMP.build_constraint(error, sum(v[i] for i in 1:p) - ,MOI.LessThan(con.set.upper - constant) - ) + sum_con = JuMP.@build_constraint(sum(v[i] for i in 1:p) + constant <= con.set.upper) return part_con, [sum_con], v end @@ -304,18 +306,18 @@ function _build_partitioned_constraint( val_type = JuMP.value_type(M) p = length(method.partition) part_con = Vector{JuMP.AbstractConstraint}(undef, p) - v = [@variable(model, base_name = "v_$(hash(con))_$(i)") for i in 1:p] + v = Vector{JuMP.variable_ref_type(M)}(undef, p) _, constant = _build_partitioned_expression(con.func, method.partition[p]) for i in 1:p func, _ = _build_partitioned_expression(con.func, method.partition[i]) + v[i] = create_variable(model, VariableProperties(func)) + JuMP.set_name(v[i], "v_$(hash(con))_$(i)") part_con[i] = JuMP.build_constraint(error, -func - v[i], MOI.LessThan(zero(val_type)) ) _bound_auxiliary(model, v[i], -func, method) end - sum_con = JuMP.build_constraint(error, sum(v[i] for i in 1:p) - , MOI.LessThan(-con.set.lower + constant) - ) + sum_con = JuMP.@build_constraint(sum(v[i] for i in 1:p) - constant <= -con.set.lower) return part_con, [sum_con], v end @@ -330,12 +332,13 @@ function _build_partitioned_constraint( part_con_gt = Vector{JuMP.AbstractConstraint}(undef, p) #let [_, 1] be the upper bound and [_, 2] be the lower bound _, constant = _build_partitioned_expression(con.func, method.partition[p]) - v = [@variable( - model, - base_name = "v_$(hash(con))_$(i)_$(j)" - ) for i in 1:p, j in 1:2] + v = Matrix{JuMP.variable_ref_type(M)}(undef, p, 2) for i in 1:p func, _= _build_partitioned_expression(con.func, method.partition[i]) + v[i,1] = create_variable(model, VariableProperties(func)) + v[i,2] = create_variable(model, VariableProperties(func)) + JuMP.set_name(v[i,1], "v_$(hash(con))_$(i)_1") + JuMP.set_name(v[i,2], "v_$(hash(con))_$(i)_2") part_con_lt[i] = JuMP.build_constraint(error, func - v[i,1], MOI.LessThan(zero(val_type)) ) @@ -346,12 +349,8 @@ function _build_partitioned_constraint( _bound_auxiliary(model, v[i,2], -func, method) end set_values = _set_values(con.set) - sum_con_lt = JuMP.build_constraint(error, sum(v[i,1] for i in 1:p), - MOI.LessThan((set_values[2] - constant)) - ) - sum_con_gt = JuMP.build_constraint(error, sum(v[i,2] for i in 1:p), - MOI.LessThan(-set_values[1] + constant) - ) + sum_con_lt = JuMP.@build_constraint(sum(v[i,1] for i in 1:p) + constant <= set_values[2]) + sum_con_gt = JuMP.@build_constraint(sum(v[i,2] for i in 1:p) - constant <= -set_values[1]) return vcat(part_con_lt, part_con_gt), [sum_con_lt, sum_con_gt], vec(v) end function _build_partitioned_constraint( @@ -361,10 +360,7 @@ function _build_partitioned_constraint( ) where {M <: JuMP.AbstractModel, T, S <: _MOI.Nonpositives, R} p = length(method.partition) d = con.set.dimension - v = [@variable( - model, - base_name = "v_$(hash(con))_$(i)_$(j)" - ) for i in 1:p, j in 1:d] + v = Matrix{JuMP.variable_ref_type(M)}(undef, p, d) part_con = Vector{JuMP.AbstractConstraint}(undef, p) constants = Vector{Number}(undef, d) for i in 1:p @@ -373,12 +369,14 @@ function _build_partitioned_constraint( ] func = JuMP.@expression(model, [j = 1:d], part_expr[j][1]) constants .= [part_expr[j][2] for j in 1:d] - part_con[i] = JuMP.build_constraint(error, - func - v[i,:], _MOI.Nonpositives(d) - ) for j in 1:d + v[i,j] = create_variable(model, VariableProperties(func[j])) + JuMP.set_name(v[i,j], "v_$(hash(con))_$(i)_$(j)") _bound_auxiliary(model, v[i,j], func[j], method) end + part_con[i] = JuMP.build_constraint(error, + func - v[i,:], _MOI.Nonpositives(d) + ) end new_func = JuMP.@expression(model,[j = 1:d], sum(v[i,j] for i in 1:p) + constants[j] @@ -394,10 +392,7 @@ function _build_partitioned_constraint( ) where {M <: JuMP.AbstractModel, T, S <: _MOI.Nonnegatives, R} p = length(method.partition) d = con.set.dimension - v = [@variable( - model, - base_name = "v_$(hash(con))_$(i)_$(j)" - ) for i in 1:p, j in 1:d] + v = Matrix{JuMP.variable_ref_type(M)}(undef, p, d) part_con = Vector{JuMP.AbstractConstraint}(undef, p) constants = Vector{Number}(undef, d) for i in 1:p @@ -407,10 +402,12 @@ function _build_partitioned_constraint( ] func = JuMP.@expression(model, [j = 1:d], -part_expr[j][1]) constants .= [-part_expr[j][2] for j in 1:d] - part_con[i] = JuMP.build_constraint(error, func - v[i,:], _MOI.Nonpositives(d)) for j in 1:d + v[i,j] = create_variable(model, VariableProperties(func[j])) + JuMP.set_name(v[i,j], "v_$(hash(con))_$(i)_$(j)") _bound_auxiliary(model, v[i,j], func[j], method) end + part_con[i] = JuMP.build_constraint(error, func - v[i,:], _MOI.Nonpositives(d)) end new_func = JuMP.@expression(model,[j = 1:d], sum(v[i,j] for i in 1:p) + constants[j] @@ -428,11 +425,7 @@ function _build_partitioned_constraint( d = con.set.dimension part_con_np = Vector{JuMP.AbstractConstraint}(undef, p) # nonpositive (≤ 0) part_con_nn = Vector{JuMP.AbstractConstraint}(undef, p) # nonnegative (≥ 0) - v = [@variable( - model, - base_name = "v_$(hash(con))_$(i)_$(j)_$(k)" - ) for i in 1:p, j in 1:d, k in 1:2 - ] + v = Array{JuMP.variable_ref_type(M)}(undef, p, d, 2) constants = Vector{Number}(undef, d) for i in 1:p part_expr = [ @@ -441,16 +434,20 @@ function _build_partitioned_constraint( ] func = JuMP.@expression(model, [j = 1:d], part_expr[j][1]) constants .= [part_expr[j][2] for j in 1:d] + for j in 1:d + v[i,j,1] = create_variable(model, VariableProperties(func[j])) + v[i,j,2] = create_variable(model, VariableProperties(func[j])) + JuMP.set_name(v[i,j,1], "v_$(hash(con))_$(i)_$(j)_1") + JuMP.set_name(v[i,j,2], "v_$(hash(con))_$(i)_$(j)_2") + _bound_auxiliary(model, v[i,j,1], func[j], method) + _bound_auxiliary(model, v[i,j,2], -func[j], method) + end part_con_np[i] = JuMP.build_constraint(error, func - v[i,:,1], _MOI.Nonpositives(d) ) part_con_nn[i] = JuMP.build_constraint(error, -func - v[i,:,2], _MOI.Nonpositives(d) ) - for j in 1:d - _bound_auxiliary(model, v[i,j,1], func[j], method) - _bound_auxiliary(model, v[i,j,2], -func[j], method) - end end new_func_np = JuMP.@expression(model,[j = 1:d], sum(v[i,j,1] for i in 1:p) + constants[j] diff --git a/src/utilities.jl b/src/utilities.jl index 6c9b8e5..0f211b4 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -8,6 +8,36 @@ function _copy_model( return M() end +################################################################################ +# ALL VARIABLES +################################################################################ +""" + collect_all_vars(model::JuMP.AbstractModel) + +Returns all variable references in the model. +Extend this for model types that have additional ref types (e.g., derivatives). +""" +collect_all_vars(model::JuMP.AbstractModel) = JuMP.all_variables(model) + +################################################################################ +# GET CONSTANT +################################################################################ +""" + get_constant(expr) + +Returns the constant portion of an expression. Extendable for model types where +additional terms should be treated as constants. +""" +get_constant(expr::JuMP.GenericAffExpr) = JuMP.constant(expr) +get_constant(expr::JuMP.GenericQuadExpr) = JuMP.constant(expr) +get_constant(expr::Number) = expr +function get_constant(expr::JuMP.AbstractVariableRef) + return zero(JuMP.value_type(typeof(JuMP.owner_model(expr)))) +end + +################################################################################ +# MODEL COPYING +################################################################################ """ JuMP.copy_extension_data( data::GDPData, @@ -76,7 +106,7 @@ function copy_gdp_data( new_gdp = new_model.ext[:GDP] # Creating maps from old to new model. - var_map = Dict(v => ref_map[v] for v in all_variables(model)) + var_map = Dict(v => ref_map[v] for v in collect_all_vars(model)) lv_map = Dict{LogicalVariableRef{M}, LogicalVariableRef{M}}() lc_map = Dict{LogicalConstraintRef{M}, LogicalConstraintRef{M}}() disj_map = Dict{DisjunctionRef{M}, DisjunctionRef{M}}() @@ -150,7 +180,9 @@ function copy_gdp_data( # Copying indicator to constraints for (lv_ref, con_refs) in old_gdp.indicator_to_constraints new_lvar_ref = lv_map[lv_ref] - new_con_refs = Vector{Union{DisjunctConstraintRef{M}, DisjunctionRef{M}}}() + new_con_refs = Vector{ + Union{DisjunctConstraintRef{M}, DisjunctionRef{M}} + }() for con_ref in con_refs new_con_ref = _remap_indicator_to_constraint(con_ref, disj_con_map, disj_map diff --git a/src/variables.jl b/src/variables.jl index b2c8abe..f96cfe0 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -115,12 +115,13 @@ function JuMP.add_variable( lvref = LogicalVariableRef(model, idx) _set_ready_to_optimize(model, false) # add the associated binary variables - if isnothing(_get_variable(v).logical_complement) + extracted_var = _get_variable(v) + if isnothing(extracted_var.logical_complement) bvref = _make_binary_variable(model, v, name) _add_logical_info(bvref, v) jump_expr = bvref else - jump_expr = 1 - binary_variable(v.logical_complement) + jump_expr = 1 - binary_variable(extracted_var.logical_complement) end _indicator_to_binary(model)[lvref] = jump_expr return lvref @@ -516,4 +517,70 @@ function variable_copy( ) props = VariableProperties(vref) return create_variable(model, props) -end \ No newline at end of file +end + +""" + get_variable_info(vref::JuMP.AbstractVariableRef; kwargs...)::JuMP.VariableInfo + +Extracts variable information from a JuMP variable reference and returns a `JuMP.VariableInfo` +object. This function retrieves bounds, fixed values, start values, and binary/integer status +from the variable reference. + +## Keyword Arguments +- `has_lb::Bool`: Whether the variable has a lower bound (default: queries `vref`) +- `has_ub::Bool`: Whether the variable has an upper bound (default: queries `vref`) +- `has_fix::Bool`: Whether the variable is fixed (default: queries `vref`) +- `has_start::Bool`: Whether the variable has a start value (default: queries `vref`) +- `has_binary::Bool`: Whether the variable is binary (default: queries `vref`) +- `has_integer::Bool`: Whether the variable is integer (default: queries `vref`) +- `lower_bound`: The lower bound value (default: queries `vref` if has_lb, else 0) +- `upper_bound`: The upper bound value (default: queries `vref` if has_ub, else 0) +- `fixed_value`: The fixed value (default: queries `vref` if has_fix, else 0) +- `start_value`: The start value (default: queries `vref` if has_start, else 0) +- `binary::Bool`: Binary status (default: queries `vref`) +- `integer::Bool`: Integer status (default: queries `vref`) + +## Returns +A `JuMP.VariableInfo` object containing all the variable's attributes. +""" +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_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, + upper_bound::Union{Number, Function} = has_ub ? JuMP.upper_bound(vref) : 0, + fixed_value::Union{Number, Function} = has_fix ? JuMP.fix_value(vref) : 0, + start_value::Union{Number, Function} = has_start ? JuMP.start_value(vref) : 0, + binary::Bool = has_binary ? JuMP.is_binary(vref) : false, + integer::Bool = has_integer ? JuMP.is_integer(vref) : false) + info = JuMP.VariableInfo( + has_lb, + lower_bound, + has_ub, + upper_bound, + has_fix, + fixed_value, + has_start, + start_value, + binary, + integer + ) + return info +end + +""" + _free_variable_info()::JuMP.VariableInfo + +Creates a blank `JuMP.VariableInfo` object with no bounds, no fixed value, +no start value, and neither binary nor integer constraints. + +## Returns +A `JuMP.VariableInfo` object with all flags set to `false` and all numeric +values set to `NaN`. +""" +function _free_variable_info() + return JuMP.VariableInfo(false, NaN, false, NaN, false, NaN, false, NaN, false, false) +end diff --git a/test/constraints/hull.jl b/test/constraints/hull.jl index 37b1175..ddc5d85 100644 --- a/test/constraints/hull.jl +++ b/test/constraints/hull.jl @@ -81,7 +81,7 @@ function test_disaggregate_expression_var() method = DP._Hull(Hull(1e-3), vrefs) @test DP._disaggregate_variables(model, z, vrefs, method) isa Nothing - refexpr = DP._disaggregate_expression(model, x, bvrefs[z], method) + refexpr = DP.disaggregate_expression(model, x, bvrefs[z], method) x_z = variable_by_name(model, "x_z") @test refexpr == x_z end @@ -97,7 +97,7 @@ function test_disaggregate_expression_var_binary() @test DP._disaggregate_variables(model, z, vrefs, method) isa Nothing @test isnothing(variable_by_name(model, "x_z")) - refexpr = DP._disaggregate_expression(model, x, bvrefs[z], method) + refexpr = DP.disaggregate_expression(model, x, bvrefs[z], method) @test refexpr == x end @@ -112,7 +112,7 @@ function test_disaggregate_expression_affine() method = DP._Hull(Hull(1e-3), vrefs) @test DP._disaggregate_variables(model, z, vrefs, method) isa Nothing - refexpr = DP._disaggregate_expression(model, 2x + 1, bvrefs[z], method) + refexpr = DP.disaggregate_expression(model, 2x + 1, bvrefs[z], method) x_z = variable_by_name(model, "x_z") zbin = variable_by_name(model, "z") @test refexpr == 2x_z + 1zbin @@ -130,7 +130,7 @@ function test_disaggregate_expression_affine_mip() method = DP._Hull(Hull(1e-3), vrefs) @test DP._disaggregate_variables(model, z, vrefs, method) isa Nothing - refexpr = DP._disaggregate_expression(model, 2x + y + 1, bvrefs[z], method) + refexpr = DP.disaggregate_expression(model, 2x + y + 1, bvrefs[z], method) x_z = variable_by_name(model, "x_z") zbin = variable_by_name(model, "z") @test refexpr == 2x_z + y + 1zbin @@ -147,7 +147,7 @@ function test_disaggregate_expression_quadratic() method = DP._Hull(Hull(1e-3), vrefs) @test DP._disaggregate_variables(model, z, vrefs, method) isa Nothing - refexpr = DP._disaggregate_expression(model, 2x^2 + 1, bvrefs[z], method) + refexpr = DP.disaggregate_expression(model, 2x^2 + 1, bvrefs[z], method) x_z = variable_by_name(model, "x_z") zbin = variable_by_name(model, "z") ϵ = method.value diff --git a/test/extensions/InfiniteDisjunctiveProgramming.jl b/test/extensions/InfiniteDisjunctiveProgramming.jl new file mode 100644 index 0000000..b206d8e --- /dev/null +++ b/test/extensions/InfiniteDisjunctiveProgramming.jl @@ -0,0 +1,440 @@ +using InfiniteOpt, HiGHS, Ipopt, Juniper +import DisjunctiveProgramming as DP + +# Helper to access internal function +const IDP = Base.get_extension(DP, :InfiniteDisjunctiveProgramming) + +function test_infiniteopt_extension() + # Initialize the model + model = InfiniteGDPModel(HiGHS.Optimizer) + set_attribute(model, MOI.Silent(), true) + + # Create the infinite variables + I = 1:4 + @infinite_parameter(model, t ∈ [0, 1], num_supports = 100) + @variable(model, 0 <= g[I] <= 10, Infinite(t)) + + # Add the disjunctions and their indicator variables + @variable(model, G[I, 1:2], InfiniteLogical(t)) + @test all(isa.(@constraint(model, [i ∈ I, j ∈ 1:2], 0 <= g[i], + Disjunct(G[i, 1])), DisjunctConstraintRef{InfiniteModel}) + ) + @test all(isa.(@constraint(model, [i ∈ I, j ∈ 1:2], g[i] <= 0, + Disjunct(G[i, 2])), DisjunctConstraintRef{InfiniteModel}) + ) + @test all(isa.(@disjunction(model, [i ∈ I], G[i, :]), + DisjunctionRef{InfiniteModel}) + ) + + # Add the logical propositions + @variable(model, W, InfiniteLogical(t)) + @test @constraint(model, G[1, 1] ∨ G[2, 1] ∧ G[3, 1] == W := true) isa + LogicalConstraintRef{InfiniteModel} + @constraint(model, 𝔼(binary_variable(W), t) >= 0.95) + + # Reformulate and solve + @test optimize!(model, gdp_method = Hull()) isa Nothing + + # check the results + @test all(value(W)) +end + +function test_infinite_gdp_model_creation() + model = InfiniteGDPModel() + @test model isa InfiniteModel + @test is_gdp_model(model) + +end + +function test_infinite_logical() + model = InfiniteGDPModel() + @infinite_parameter(model, t ∈ [0, 1]) + + @variable(model, y, InfiniteLogical(t)) + @test y isa DP.LogicalVariableRef{InfiniteModel} + @test binary_variable(y) isa InfiniteOpt.GeneralVariableRef +end + +function test__is_parameter() + 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)) + @variable(model, y) + + # Test DependentParameterRef + @test IDP._is_parameter(t) == true + + # Test IndependentParameterRef + @test IDP._is_parameter(s[1]) == true + + # Test FiniteParameterRef + @test IDP._is_parameter(p) == true + + # Test non-parameter variables (else branch) + @test IDP._is_parameter(x) == false + @test IDP._is_parameter(y) == false +end + +function test_requires_disaggregation() + model = InfiniteGDPModel() + @infinite_parameter(model, t ∈ [0, 1]) + @finite_parameter(model, p == 1.0) + @variable(model, x, Infinite(t)) + @variable(model, y) + + # Parameters should NOT require disaggregation + @test DP.requires_disaggregation(t) == false + @test DP.requires_disaggregation(p) == false + + # Variables SHOULD require disaggregation + @test DP.requires_disaggregation(x) == true + @test DP.requires_disaggregation(y) == true +end + +function test_all_variables_infiniteopt() + model = InfiniteGDPModel() + @infinite_parameter(model, t ∈ [0, 1]) + @variable(model, x, Infinite(t)) + @variable(model, y) + @variable(model, dx, Infinite(t)) + @deriv(dx, t) + + all_vars = DP.collect_all_vars(model) + @test x in all_vars + @test y in all_vars + @test dx in all_vars + + # Verify derivatives are included + derivs = collect(InfiniteOpt.all_derivatives(model)) + for d in derivs + @test d in all_vars + end +end + +function test_get_constant() + model = InfiniteGDPModel() + @infinite_parameter(model, t ∈ [0, 1]) + @finite_parameter(model, p == 2.0) + @variable(model, x, Infinite(t)) + + # Test expression with parameter terms (_is_parameter branch) + expr2 = @expression(model, 3.0 + 2*t + x) + constant2 = DP.get_constant(expr2) + @test JuMP.constant(constant2) == 3.0 + @test haskey(constant2.terms, t) + @test constant2.terms[t] == 2.0 + @test !haskey(constant2.terms, x) + + # Test expression with finite parameter + expr3 = @expression(model, 1.0 + 3*p) + constant3 = DP.get_constant(expr3) + @test JuMP.constant(constant3) == 1.0 + @test haskey(constant3.terms, p) + @test constant3.terms[p] == 3.0 +end + +function test_disaggregate_expression_infiniteopt() + model = InfiniteGDPModel() + @infinite_parameter(model, t ∈ [0, 1]) + @variable(model, 0 <= x <= 10, Infinite(t)) + @variable(model, z, InfiniteLogical(t)) + @variable(model, w, Bin) + + bvrefs = DP._indicator_to_binary(model) + bvref = bvrefs[z] + + vrefs = Set([x, w]) + DP._variable_bounds(model)[x] = DP.set_variable_bound_info(x, Hull()) + method = DP._Hull(Hull(1e-3), vrefs) + DP._disaggregate_variables(model, z, vrefs, method) + + aff_bin = @expression(model, 2*w + 1) + result_bin = DP.disaggregate_expression(model, aff_bin, bvref, method) + @test haskey(result_bin.terms, w) + + aff_param = @expression(model, 3*t + 1) + result_param = DP.disaggregate_expression(model, aff_param, bvref, method) + @test result_param isa JuMP.GenericQuadExpr + + aff_expr = @expression(model, 2*x + 1) + result_expr = DP.disaggregate_expression(model, aff_expr, bvref, method) + dvref = method.disjunct_variables[x, bvref] + @test result_expr == bvref + 2*dvref + + @variable(model, 0 <= y <= 5, Infinite(t)) + aff_not_disagg = @expression(model, 3*y + 1) + result_not_disagg = DP.disaggregate_expression(model, aff_not_disagg, bvref, method) + @test haskey(result_not_disagg.terms, y) +end + +function test_variable_properties_infiniteopt() + model = InfiniteGDPModel() + @infinite_parameter(model, t ∈ [0, 1]) + @variable(model, 1 <= x <= 10, Infinite(t), start = 5.0) + @variable(model, y) + + props_x = DP.VariableProperties(x) + @test props_x.info.has_lb == true + @test props_x.info.lower_bound == 1.0 + @test props_x.info.has_ub == true + @test props_x.info.upper_bound == 10.0 + @test props_x.name == "x" + @test props_x.set === nothing + @test t in InfiniteOpt.parameter_refs(x) + + props_y = DP.VariableProperties(y) + @test props_y.name == "y" + @test props_y.variable_type === nothing +end + +function test_variable_properties_from_expr() + model = InfiniteGDPModel() + @infinite_parameter(model, t ∈ [0, 1]) + @infinite_parameter(model, s ∈ [0, 2]) + @variable(model, x, Infinite(t)) + @variable(model, y, Infinite(s)) + + expr = @expression(model, 2*x + y) + props = DP.VariableProperties(expr) + @test props.name == "" + @test props.variable_type isa InfiniteOpt.Infinite + @test Set(props.variable_type.parameter_refs) == Set((t, s)) + var1 = DP.create_variable(model, props) + JuMP.set_name(var1, "inferred_var") + @test JuMP.name(var1) == "inferred_var" + @test InfiniteOpt.parameter_refs(var1) == (t, s) +end + +function test_variable_properties_from_quad_expr() + model = InfiniteGDPModel() + @infinite_parameter(model, t ∈ [0, 1]) + @infinite_parameter(model, s ∈ [0, 2]) + @variable(model, x, Infinite(t)) + @variable(model, y, Infinite(s)) + + expr = @expression(model, x^2 + x*y) + props = DP.VariableProperties(expr) + @test props.name == "" + @test props.variable_type isa InfiniteOpt.Infinite + @test Set(props.variable_type.parameter_refs) == Set((t, s)) + var1 = DP.create_variable(model, props) + JuMP.set_name(var1, "quad_inferred_var") + @test JuMP.name(var1) == "quad_inferred_var" + @test Set(InfiniteOpt.parameter_refs(var1)) == Set((t, s)) +end + +function test_variable_properties_from_nonlinear_expr() + model = InfiniteGDPModel() + @infinite_parameter(model, t ∈ [0, 1]) + @infinite_parameter(model, s ∈ [0, 2]) + @variable(model, x, Infinite(t)) + @variable(model, y, Infinite(s)) + + expr = @expression(model, exp(x) + sin(y)) + props = DP.VariableProperties(expr) + @test props.name == "" + @test props.variable_type isa InfiniteOpt.Infinite + @test Set(props.variable_type.parameter_refs) == Set((t, s)) + var1 = DP.create_variable(model, props) + JuMP.set_name(var1, "nl_inferred_var") + @test JuMP.name(var1) == "nl_inferred_var" + @test Set(InfiniteOpt.parameter_refs(var1)) == Set((t, s)) +end + +function test_variable_properties_from_vector() + model = InfiniteGDPModel() + @infinite_parameter(model, t ∈ [0, 1]) + @infinite_parameter(model, s ∈ [0, 2]) + @infinite_parameter(model, r ∈ [0, 3]) + @variable(model, x, Infinite(t)) + @variable(model, y, Infinite(s)) + @variable(model, z, Infinite(r)) + + exprs = [ + @expression(model, x + 1), + @expression(model, y + 2), + @expression(model, exp(z)) + ] + props = DP.VariableProperties(exprs) + var1 = DP.create_variable(model, props) + JuMP.set_name(var1, "vector_var") + @test JuMP.name(var1) == "vector_var" + prefs = InfiniteOpt.parameter_refs(var1) + @test length(prefs) == 3 + @test Set(prefs) == Set((t, s, r)) +end + +function test_add_cardinality_constraint() + model = InfiniteGDPModel() + @infinite_parameter(model, t ∈ [0, 1]) + @variable(model, y[1:3], InfiniteLogical(t)) + + LCR = DP.LogicalConstraintRef{InfiniteModel} + @test @constraint(model, y in Exactly(1)) isa LCR + @test @constraint(model, y in AtLeast(1)) isa LCR + @test @constraint(model, y in AtMost(2)) isa LCR +end + +function test_add_logical_constraint() + model = InfiniteGDPModel() + @infinite_parameter(model, t ∈ [0, 1]) + @variable(model, y[1:2], InfiniteLogical(t)) + + LCR = DP.LogicalConstraintRef{InfiniteModel} + @test @constraint(model, y[1] ∨ y[2] := true) isa LCR + @test @constraint(model, y[1] ∧ y[2] := true) isa LCR + @test @constraint(model, y[1] ⟹ y[2] := true) isa LCR +end + +function test_add_constraint_single_logical_error() + model = InfiniteGDPModel() + @infinite_parameter(model, t ∈ [0, 1]) + @variable(model, y, InfiniteLogical(t)) + + c = JuMP.ScalarConstraint(y, MOI.EqualTo(true)) + @test_throws ErrorException JuMP.add_constraint(model, c, "") +end + +function test_add_constraint_affine_logical_error() + model = InfiniteGDPModel() + @infinite_parameter(model, t ∈ [0, 1]) + @variable(model, y[1:2], InfiniteLogical(t)) + + aff_expr = 1.0 * y[1] + 1.0 * y[2] + c = JuMP.ScalarConstraint(aff_expr, MOI.EqualTo(1.0)) + @test_throws ErrorException JuMP.add_constraint(model, c, "") +end + +function test_add_constraint_quad_logical_error() + model = InfiniteGDPModel() + @infinite_parameter(model, t ∈ [0, 1]) + @variable(model, y[1:2], InfiniteLogical(t)) + + quad_expr = 1.0 * y[1] * y[2] + c = JuMP.ScalarConstraint(quad_expr, MOI.EqualTo(1.0)) + @test_throws ErrorException JuMP.add_constraint(model, c, "") +end + +function test_logical_value() + 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 <= 5, Disjunct(y[2])) + @disjunction(model, [y[1], y[2]]) + + @objective(model, Min, 𝔼(x, t)) + + optimize!(model, gdp_method = Hull()) + + val = value(y[2]) + @test eltype(val) == Bool +end + +function test_unsupported_methods_error() + model = InfiniteGDPModel(HiGHS.Optimizer) + @test_throws ErrorException DP.reformulate_model(model, MBM(HiGHS.Optimizer)) + @test_throws ErrorException DP.reformulate_model(model, cutting_planes(HiGHS.Optimizer)) +end + +function test_methods() + I = 1:3 + J = 1:6 + period_bounds = collect(0:1:6) + expected_obj = 4.504541662743021 + expected_z = -1.3634301575859131 + tol = 0.1 + + # Use Juniper for MIQP support (HiGHS cannot solve MIQP) + ipopt = optimizer_with_attributes(Ipopt.Optimizer, + "print_level" => 0, "sb" => "yes" + ) + optimizer = optimizer_with_attributes(Juniper.Optimizer, "nl_solver" => ipopt) + model = InfiniteGDPModel(optimizer) + set_attribute(model, MOI.Silent(), true) + + @infinite_parameter( + model, τ[j in J] in [period_bounds[j], period_bounds[j+1]], + num_supports = 5, independent = true, container = Array + ) + @variable(model, -5 ≤ y[j in J] ≤ 5, Infinite(τ[j]), container = Array) + @variable(model, -4 ≤ z ≤ 4) + @objective(model, Min, 10 * sum(∫(y[j]^2, τ[j]) for j in J)) + + @constraint(model, y[1](0) == 1) + @constraint(model, [j = 2:6], y[j](period_bounds[j]) == y[j-1](period_bounds[j])) + + @variable(model, W[i = I, j = J], Logical) + @constraint(model, [j in J], ∂(y[j], τ[j]) == -2*τ[j] + 0.3*z - 20*y[j], Disjunct(W[1, j])) + @constraint(model, [j in J], ∂(y[j], τ[j]) == -2*z + 0.4*τ[j] - 4, Disjunct(W[2, j])) + @constraint(model, [j in J], ∂(y[j], τ[j]) == 2*z + 4*(τ[j] - y[j] - 1), Disjunct(W[3, j])) + @disjunction(model, [j in J], W[:, j]) + + for j in J + set_upper_bound(∂(y[j], τ[j]), 100) + set_lower_bound(∂(y[j], τ[j]), -100) + end + + @test optimize!(model, gdp_method = BigM()) isa Nothing + @test objective_value(model) ≈ expected_obj atol=tol + @test value(z) ≈ expected_z atol=tol + + @test optimize!(model, gdp_method = Hull()) isa Nothing + @test objective_value(model) ≈ expected_obj atol=tol + @test value(z) ≈ expected_z atol=tol + + @test optimize!(model, gdp_method = PSplit(3, model)) isa Nothing + @test objective_value(model) ≈ expected_obj atol=tol + @test value(z) ≈ expected_z atol=tol +end + +@testset "InfiniteDisjunctiveProgramming" begin + + @testset "Model" begin + test_infinite_gdp_model_creation() + end + + @testset "all_variables" begin + test_all_variables_infiniteopt() + end + + @testset "Variables" begin + test_infinite_logical() + test__is_parameter() + test_requires_disaggregation() + test_variable_properties_infiniteopt() + test_variable_properties_from_expr() + test_variable_properties_from_quad_expr() + test_variable_properties_from_nonlinear_expr() + test_variable_properties_from_vector() + end + + @testset "Constraints" begin + test_add_cardinality_constraint() + test_add_logical_constraint() + end + + @testset "JuMP Overloads" begin + test_logical_value() + test_add_constraint_single_logical_error() + test_add_constraint_affine_logical_error() + test_add_constraint_quad_logical_error() + end + + @testset "Methods" begin + test_get_constant() + test_disaggregate_expression_infiniteopt() + test_unsupported_methods_error() + end + + @testset "Integration" begin + test_infiniteopt_extension() + test_methods() + end + +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 283cbb2..06e8813 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -19,7 +19,9 @@ include("constraints/bigm.jl") include("constraints/psplit.jl") include("constraints/cuttingplanes.jl") include("constraints/hull.jl") -include("constraints/fallback.jl") +include("constraints/fallback.jl") include("constraints/disjunction.jl") include("print.jl") include("solve.jl") +include("extensions/InfiniteDisjunctiveProgramming.jl") + diff --git a/test/utilities.jl b/test/utilities.jl index 1fee4e2..2b87e6e 100644 --- a/test/utilities.jl +++ b/test/utilities.jl @@ -158,3 +158,91 @@ function JuMP.add_constraint( return DP._add_logical_constraint(model, c, name) end DP.requires_disaggregation(::MyVarRef) = true + +################################################################################ +# UTILITY FUNCTION TESTS +################################################################################ +function test_all_variables() + model = GDPModel() + @variable(model, x) + @variable(model, y[1:3]) + @variable(model, z, Bin) + + all_vars = DP.all_variables(model) + @test x in all_vars + @test y[1] in all_vars + @test y[2] in all_vars + @test y[3] in all_vars + @test z in all_vars + @test length(all_vars) == 5 +end + +function test_collect_all_vars() + model = GDPModel() + @variable(model, x) + @variable(model, y[1:3]) + @variable(model, z, Bin) + + all_vars = DP.collect_all_vars(model) + @test x in all_vars + @test y[1] in all_vars + @test y[2] in all_vars + @test y[3] in all_vars + @test z in all_vars + @test length(all_vars) == 5 +end + +function test_get_constant_affine() + model = GDPModel() + @variable(model, x) + @variable(model, y) + + # Affine expression with constant + expr1 = @expression(model, 2*x + 3*y + 5.0) + @test DP.get_constant(expr1) == 5.0 + + # Affine expression without constant + expr2 = @expression(model, 2*x + 3*y) + @test DP.get_constant(expr2) == 0.0 + + # Only constant + expr3 = @expression(model, 0*x + 7.0) + @test DP.get_constant(expr3) == 7.0 +end + +function test_get_constant_quadratic() + model = GDPModel() + @variable(model, x) + @variable(model, y) + + # Quadratic expression with constant + expr1 = @expression(model, x^2 + 2*x*y + 3.0) + @test DP.get_constant(expr1) == 3.0 + + # Quadratic expression without constant + expr2 = @expression(model, x^2 + y^2) + @test DP.get_constant(expr2) == 0.0 +end + +function test_get_constant_number() + @test DP.get_constant(5.0) == 5.0 + @test DP.get_constant(0) == 0 + @test DP.get_constant(-3.14) == -3.14 +end + +function test_get_constant_variable() + model = GDPModel() + @variable(model, x) + + # Variable reference should return zero + @test DP.get_constant(x) == 0.0 +end + +@testset "Utility Functions" begin + test_all_variables() + test_collect_all_vars() + test_get_constant_affine() + test_get_constant_quadratic() + test_get_constant_number() + test_get_constant_variable() +end diff --git a/test/variables/creation.jl b/test/variables/creation.jl index 5b7f302..b8ffa4f 100644 --- a/test/variables/creation.jl +++ b/test/variables/creation.jl @@ -108,27 +108,107 @@ function test_variable_copy() @variable(model1, original, lower_bound = 1, upper_bound = 5, start = 3.0) - props = DP.VariableProperties(original) - - recreated = DP.create_variable(model2, props) - - @test JuMP.has_lower_bound(recreated) == JuMP.has_lower_bound(original) - @test JuMP.lower_bound(recreated) == JuMP.lower_bound(original) - @test JuMP.has_upper_bound(recreated) == JuMP.has_upper_bound(original) - @test JuMP.upper_bound(recreated) == JuMP.upper_bound(original) - @test JuMP.has_start_value(recreated) == JuMP.has_start_value(original) - @test JuMP.start_value(recreated) == JuMP.start_value(original) - @test JuMP.name(recreated) == JuMP.name(original) - println("##################################") + # Test variable_copy function directly + copied = DP.variable_copy(model2, original) + + @test JuMP.has_lower_bound(copied) == JuMP.has_lower_bound(original) + @test JuMP.lower_bound(copied) == JuMP.lower_bound(original) + @test JuMP.has_upper_bound(copied) == JuMP.has_upper_bound(original) + @test JuMP.upper_bound(copied) == JuMP.upper_bound(original) + @test JuMP.has_start_value(copied) == JuMP.has_start_value(original) + @test JuMP.start_value(copied) == JuMP.start_value(original) + @test JuMP.name(copied) == JuMP.name(original) + @test original in JuMP.all_variables(model1) - @test recreated in JuMP.all_variables(model2) + @test copied in JuMP.all_variables(model2) @test !(original in JuMP.all_variables(model2)) - @test !(recreated in JuMP.all_variables(model1)) + @test !(copied in JuMP.all_variables(model1)) +end + +function test_get_variable_info() + model = Model() + + # Test with all properties set + @variable(model, x, lower_bound = 1.0, upper_bound = 10.0, start = 5.0) + info = DP.get_variable_info(x) + @test info.has_lb == true + @test info.lower_bound == 1.0 + @test info.has_ub == true + @test info.upper_bound == 10.0 + @test info.has_start == true + @test info.start == 5.0 + @test info.has_fix == false + @test info.binary == false + @test info.integer == false + + # Test with binary variable + @variable(model, y, Bin) + info_bin = DP.get_variable_info(y) + @test info_bin.binary == true + @test info_bin.integer == false + + # Test with integer variable + @variable(model, z, Int) + info_int = DP.get_variable_info(z) + @test info_int.binary == false + @test info_int.integer == true + + # Test with fixed variable + @variable(model, w == 7.5) + info_fix = DP.get_variable_info(w) + @test info_fix.has_fix == true + @test info_fix.fixed_value == 7.5 + + # Test with overridden parameters + info_override = DP.get_variable_info(x, has_lb = false, upper_bound = 20.0) + @test info_override.has_lb == false + @test info_override.lower_bound == 0 # Default when has_lb is false + @test info_override.upper_bound == 20.0 # Overridden +end + +function test_variable_properties_from_expr() + model = Model() + + # Test with expression (for extensions - base ignores it) + @variable(model, x) + expr = 2*x + 1 + props = DP.VariableProperties(expr) + @test props.name == "" + @test props.info.has_lb == false + @test props.info.has_ub == false + var = DP.create_variable(model, props) + JuMP.set_name(var, "from_expr") + @test JuMP.name(var) == "from_expr" + @test JuMP.has_lower_bound(var) == false + @test JuMP.has_upper_bound(var) == false + @test JuMP.is_fixed(var) == false + @test JuMP.is_binary(var) == false + @test JuMP.is_integer(var) == false + @test var in JuMP.all_variables(model) +end + +function test_create_variable_with_set() + model = Model() + + info = DP._free_variable_info() + props = DP.VariableProperties(info, "test_var", MOI.ZeroOne(), nothing) + + @test props.set !== nothing + + var = DP.create_variable(model, props) + + @test var !== nothing + @test JuMP.name(var) == "test_var" + @test var in JuMP.all_variables(model) end @testset "Variable Creation" begin test_VariableProperties_constructor() test_make_variable_object() test_create_variable() + test_create_variable_with_set() test_complete_workflow() + test_variable_copy() + test_get_variable_info() + test_variable_properties_from_expr() end \ No newline at end of file