diff --git a/Project.toml b/Project.toml index c27f5dc..754f086 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,12 @@ 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" @@ -16,7 +22,9 @@ julia = "1.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" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" [targets] -test = ["Aqua", "HiGHS", "Test"] +test = ["Aqua", "InfiniteOpt", "HiGHS", "Pkg", "Test"] diff --git a/ext/InfiniteDisjunctiveProgramming.jl b/ext/InfiniteDisjunctiveProgramming.jl new file mode 100644 index 0000000..063d87e --- /dev/null +++ b/ext/InfiniteDisjunctiveProgramming.jl @@ -0,0 +1,82 @@ +module InfiniteDisjunctiveProgramming + +import InfiniteOpt, JuMP +import DisjunctiveProgramming as DP + +# Extend the public API methods +function DP.InfiniteGDPModel(args...; kwargs...) + return DP.GDPModel{ + InfiniteOpt.InfiniteModel, + InfiniteOpt.GeneralVariableRef, + InfiniteOpt.InfOptConstraintRef + }(args...; kwargs...) +end +DP.InfiniteLogical(prefs...) = DP.Logical(InfiniteOpt.Infinite(prefs...)) + +# Make necessary extensions for Hull method +function DP.requires_disaggregation(vref::InfiniteOpt.GeneralVariableRef) + if vref.index_type <: InfiniteOpt.InfOptParameter + return false + else + return true + end +end +function DP.make_disaggregated_variable( + model::InfiniteOpt.InfiniteModel, + vref::InfiniteOpt.GeneralVariableRef, + name, + lb, + ub + ) + prefs = InfiniteOpt.parameter_refs(vref) + if !isempty(prefs) + return JuMP.@variable(model, base_name = name, lower_bound = lb, upper_bound = ub, + variable_type = InfiniteOpt.Infinite(prefs...)) + else + return JuMP.@variable(model, base_name = name, lower_bound = lb, upper_bound = ub) + end +end + +# Add necessary @constraint extensions +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 + +# Extend value to work properly +function JuMP.value(vref::DP.LogicalVariableRef{InfiniteOpt.InfiniteModel}) + return JuMP.value(DP.binary_variable(vref)) .>= 0.5 +end + +end \ No newline at end of file diff --git a/src/DisjunctiveProgramming.jl b/src/DisjunctiveProgramming.jl index ae9892e..1ec466f 100644 --- a/src/DisjunctiveProgramming.jl +++ b/src/DisjunctiveProgramming.jl @@ -24,6 +24,7 @@ include("bigm.jl") include("hull.jl") include("indicator.jl") include("print.jl") +include("extension_api.jl") # Define additional stuff that should not be exported const _EXCLUDE_SYMBOLS = [Symbol(@__MODULE__), :eval, :include] 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/variables.jl b/src/variables.jl index 052a427..16f9e13 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 diff --git a/test/extensions/InfiniteDisjunctiveProgramming.jl b/test/extensions/InfiniteDisjunctiveProgramming.jl new file mode 100644 index 0000000..af97b70 --- /dev/null +++ b/test/extensions/InfiniteDisjunctiveProgramming.jl @@ -0,0 +1,32 @@ +using InfiniteOpt + +function test_infiniteopt_extension() + # Initialize the model + model = InfiniteGDPModel(HiGHS.Optimizer) + + # 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 + +@testset "InfiniteDisjunctiveProgramming" begin + test_infiniteopt_extension() +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index ba5c511..fff1632 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,7 +5,7 @@ using Test include("utilities.jl") # RUN ALL THE TESTS -include("aqua.jl") +# include("aqua.jl") # temporary ignore until compat is finalized include("model.jl") include("jump.jl") include("variables/query.jl") @@ -20,3 +20,9 @@ include("constraints/fallback.jl") include("constraints/disjunction.jl") include("print.jl") include("solve.jl") + +if Base.VERSION >= v"1.9" # extensions require Julia v1.9+ + import Pkg + Pkg.add(url = "https://github.com/infiniteopt/InfiniteOpt.jl", rev = "master") + include("extensions/InfiniteDisjunctiveProgramming.jl") +end