diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c38805..b8b0d62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,10 +21,10 @@ jobs: - version: '1' os: ubuntu-latest arch: x64 - - version: '1.6' + - version: '1.9' os: ubuntu-latest arch: x64 - - version: '1.6' + - version: '1.9' os: ubuntu-latest arch: x86 steps: diff --git a/Project.toml b/Project.toml index 3d9f416..3fe94a9 100644 --- a/Project.toml +++ b/Project.toml @@ -4,11 +4,16 @@ authors = ["guilhermebodin "] version = "0.5.9" [deps] -JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +[weakdeps] +JuMP = "4076af6c-e467-56ae-b986-b466b2749572" + +[extensions] +DualizationJuMPExt = "JuMP" + [compat] JuMP = "0.23, 1" MathOptInterface = "1" -julia = "1.6" +julia = "1.9" diff --git a/docs/src/reference.md b/docs/src/reference.md index e09f1c8..efd7b5c 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -4,7 +4,7 @@ Dualization.supported_constraints Dualization.supported_objective Dualization.DualNames -Dualization.VariableData -Dualization.ConstraintData +Dualization.PrimalVariableData +Dualization.PrimalConstraintData Dualization.PrimalDualMap ``` \ No newline at end of file diff --git a/ext/DualizationJuMPExt/DualizationJuMPExt.jl b/ext/DualizationJuMPExt/DualizationJuMPExt.jl new file mode 100644 index 0000000..b59ec67 --- /dev/null +++ b/ext/DualizationJuMPExt/DualizationJuMPExt.jl @@ -0,0 +1,149 @@ +# Copyright (c) 2017: Guilherme Bodin, and contributors +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +module DualizationJuMPExt + +import Dualization +import JuMP +import MathOptInterface as MOI + +function Dualization.dualize( + model::JuMP.Model, + optimizer_constructor = nothing; + kwargs..., +) + mode = JuMP.mode(model) + if mode != JuMP.AUTOMATIC + error("Dualization does not support solvers in $(mode) mode") + end + dual_model = JuMP.Model() + dual_problem = Dualization.DualProblem(JuMP.backend(dual_model)) + Dualization.dualize(JuMP.backend(model), dual_problem; kwargs...) + _fill_obj_dict_with_variables!(dual_model) + _fill_obj_dict_with_constraints!(dual_model) + if optimizer_constructor !== nothing + JuMP.set_optimizer(dual_model, optimizer_constructor) + end + dual_model.ext[:_Dualization_jl_PrimalDualMap] = + dual_problem.primal_dual_map + return dual_model +end + +function _fill_obj_dict_with_variables!(model::JuMP.Model) + list = MOI.get(model, MOI.ListOfVariableAttributesSet()) + if !(MOI.VariableName() in list) + return + end + for vi in MOI.get(model, MOI.ListOfVariableIndices()) + name = MOI.get(JuMP.backend(model), MOI.VariableName(), vi) + if !isempty(name) + model[Symbol(name)] = JuMP.VariableRef(model, vi) + end + end + return +end + +function _fill_obj_dict_with_constraints!(model::JuMP.Model) + con_types = MOI.get(model, MOI.ListOfConstraintTypesPresent()) + for (F, S) in con_types + _fill_obj_dict_with_constraints!(model, F, S) + end + return +end + +function _fill_obj_dict_with_constraints!( + model::JuMP.Model, + ::Type{F}, + ::Type{S}, +) where {F,S} + list = MOI.get(model, MOI.ListOfConstraintAttributesSet{F,S}()) + if !(MOI.ConstraintName() in list) + return + end + for ci in MOI.get(JuMP.backend(model), MOI.ListOfConstraintIndices{F,S}()) + name = MOI.get(JuMP.backend(model), MOI.ConstraintName(), ci) + if !isempty(name) + model[Symbol(name)] = JuMP.constraint_ref_with_index(model, ci) + end + end + return +end + +function _get_primal_dual_map(model::JuMP.Model) + return model.ext[:_Dualization_jl_PrimalDualMap] +end + +function Dualization._get_dual_constraint( + dual_model, + primal_ref::JuMP.VariableRef, +) + map = _get_primal_dual_map(dual_model) + moi_primal_vi = JuMP.index(primal_ref) + moi_dual_ci, idx = Dualization._get_dual_constraint(map, moi_primal_vi) + # dual_model = nothing # TODO + if idx === nothing + # variables fixed at zero + return nothing, idx + end + return JuMP.constraint_ref_with_index(dual_model, moi_dual_ci), idx +end + +function Dualization._get_primal_constraint( + dual_model::JuMP.Model, + primal_vi::JuMP.VariableRef, +) + primal_model = JuMP.owner_model(primal_vi) + map = _get_primal_dual_map(dual_model) + moi_primal_vi = JuMP.index(primal_vi) + primal_ci, idx = Dualization._get_primal_constraint(map, moi_primal_vi) + if primal_ci === nothing + return nothing, idx + end + return JuMP.constraint_ref_with_index(primal_model, primal_ci), idx +end + +function Dualization._get_dual_variables( + dual_model::JuMP.Model, + primal_ref::JuMP.ConstraintRef, +) + map = _get_primal_dual_map(dual_model) + moi_primal_ci = JuMP.index(primal_ref) + moi_dual_vis = Dualization._get_dual_variables(map, moi_primal_ci) + if moi_dual_vis === nothing + # main constraint of a constrained variable + return nothing + end + return [JuMP.VariableRef(dual_model, vi) for vi in moi_dual_vis] +end + +# this is a constrained variable constraint +function Dualization._get_dual_constraint( + dual_model::JuMP.Model, + primal_ref::JuMP.ConstraintRef, +) + map = _get_primal_dual_map(dual_model) + moi_primal_ci = JuMP.index(primal_ref) + moi_dual_ci = Dualization._get_dual_constraint(map, moi_primal_ci) + if moi_dual_ci === nothing + # main constraint of a constrained variable + # or + # primal constraint is equality + return nothing + end + return JuMP.constraint_ref_with_index(dual_model, moi_dual_ci) +end + +function Dualization._get_dual_parameter( + dual_model::JuMP.Model, + primal_ref::JuMP.VariableRef, +) + map = _get_primal_dual_map(dual_model) + moi_primal_vi = JuMP.index(primal_ref) + moi_dual_vi = Dualization._get_dual_parameter(map, moi_primal_vi) + # the above line might error + return JuMP.VariableRef(dual_model, moi_dual_vi) +end + +end # module DualizationJuMPExt diff --git a/src/Dualization.jl b/src/Dualization.jl index 957b786..9d56396 100644 --- a/src/Dualization.jl +++ b/src/Dualization.jl @@ -5,11 +5,8 @@ module Dualization -import JuMP import LinearAlgebra -import MathOptInterface - -const MOI = MathOptInterface +import MathOptInterface as MOI include("structures.jl") include("utils.jl") diff --git a/src/dual_equality_constraints.jl b/src/dual_equality_constraints.jl index 098091a..d5a74de 100644 --- a/src/dual_equality_constraints.jl +++ b/src/dual_equality_constraints.jl @@ -113,7 +113,7 @@ function _add_dual_equality_constraints( end # Add primal variable to dual contraint to the link dictionary primal_dual_map.primal_variable_data[primal_vi] = - VariableData{T}(nothing, -1, dual_ci, nothing) + PrimalVariableData{T}(nothing, -1, dual_ci, nothing) end return scalar_affine_terms end @@ -139,7 +139,7 @@ function _add_constrained_variable_constraint( sense_change * get(scalar_terms, vi, zero(T)), ) primal_dual_map.primal_variable_data[vi] = - VariableData{T}(ci, i, nothing, dual_function) + PrimalVariableData{T}(ci, i, nothing, dual_function) end return end @@ -172,7 +172,7 @@ function _add_constrained_variable_constraint( dual_ci = MOI.add_constraint(dual_model, func_dual, set_dual) for (i, vi) in enumerate(vis) primal_dual_map.primal_variable_data[vi] = - VariableData{T}(ci, i, dual_ci, nothing) + PrimalVariableData{T}(ci, i, dual_ci, nothing) end if !is_empty(dual_names) @warn( @@ -199,7 +199,7 @@ function _add_constrained_variable_constraint( sense_change * get(scalar_terms, vi, zero(T)), ) primal_dual_map.primal_variable_data[vi] = - VariableData{T}(ci, 0, nothing, dual_function) + PrimalVariableData{T}(ci, 0, nothing, dual_function) return end @@ -226,7 +226,7 @@ function _add_constrained_variable_constraint( set_dual, ) primal_dual_map.primal_variable_data[vi] = - VariableData{T}(ci, 0, dual_ci, nothing) + PrimalVariableData{T}(ci, 0, dual_ci, nothing) if !is_empty(dual_names) _set_dual_constraint_name( dual_model, diff --git a/src/dual_model_variables.jl b/src/dual_model_variables.jl index 604c1c8..53358b6 100644 --- a/src/dual_model_variables.jl +++ b/src/dual_model_variables.jl @@ -86,7 +86,7 @@ function _add_dual_variable( T[] end primal_dual_map.primal_constraint_data[ci] = - ConstraintData(set_constant, vis, con_index) + PrimalConstraintData(set_constant, vis, con_index) # Get constraint name ci_name = MOI.get(primal_model, MOI.ConstraintName(), ci) # Add each vi to the dictionary diff --git a/src/dualize.jl b/src/dualize.jl index fcc8518..3a3fb98 100644 --- a/src/dualize.jl +++ b/src/dualize.jl @@ -207,62 +207,3 @@ function dualize( end return dual_problem end - -function dualize(model::JuMP.Model, optimizer_constructor = nothing; kwargs...) - mode = JuMP.mode(model) - if mode != JuMP.AUTOMATIC - error("Dualization does not support solvers in $(mode) mode") - end - dual_model = JuMP.Model() - dualize( - JuMP.backend(model), - DualProblem(JuMP.backend(dual_model)); - kwargs..., - ) - _fill_obj_dict_with_variables!(dual_model) - _fill_obj_dict_with_constraints!(dual_model) - if optimizer_constructor !== nothing - JuMP.set_optimizer(dual_model, optimizer_constructor) - end - return dual_model -end - -function _fill_obj_dict_with_variables!(model::JuMP.Model) - list = MOI.get(model, MOI.ListOfVariableAttributesSet()) - if !(MOI.VariableName() in list) - return - end - for vi in MOI.get(model, MOI.ListOfVariableIndices()) - name = MOI.get(JuMP.backend(model), MOI.VariableName(), vi) - if !isempty(name) - model[Symbol(name)] = JuMP.VariableRef(model, vi) - end - end - return -end - -function _fill_obj_dict_with_constraints!(model::JuMP.Model) - con_types = MOI.get(model, MOI.ListOfConstraintTypesPresent()) - for (F, S) in con_types - _fill_obj_dict_with_constraints!(model, F, S) - end - return -end - -function _fill_obj_dict_with_constraints!( - model::JuMP.Model, - ::Type{F}, - ::Type{S}, -) where {F,S} - list = MOI.get(model, MOI.ListOfConstraintAttributesSet{F,S}()) - if !(MOI.ConstraintName() in list) - return - end - for ci in MOI.get(JuMP.backend(model), MOI.ListOfConstraintIndices{F,S}()) - name = MOI.get(JuMP.backend(model), MOI.ConstraintName(), ci) - if !isempty(name) - model[Symbol(name)] = JuMP.constraint_ref_with_index(model, ci) - end - end - return -end diff --git a/src/structures.jl b/src/structures.jl index 64775fc..6fc5914 100644 --- a/src/structures.jl +++ b/src/structures.jl @@ -26,7 +26,7 @@ MOI.Utilities.@model( ) """ - VariableData{T} + PrimalVariableData{T} Data structure used in `PrimalDualMap` to hold information about primal variables and their dual counterparts. @@ -55,9 +55,9 @@ variables and their dual counterparts. To got from the constrained variable constraint to the primal variable, use the `primal_constrained_variables` field of `PrimalDualMap`. -See also `PrimalDualMap` and `ConstraintData`. +See also `PrimalDualMap` and `PrimalConstraintData`. """ -struct VariableData{T} +struct PrimalVariableData{T} primal_constrained_variable_constraint::Union{Nothing,MOI.ConstraintIndex} primal_constrained_variable_index::Int dual_constraint::Union{Nothing,MOI.ConstraintIndex} @@ -66,7 +66,7 @@ end # constraints of primal constrained variables are not here """ - ConstraintData{T} + PrimalConstraintData{T} Data structure used in `PrimalDualMap` to hold information about primal constraints and their dual counterparts. @@ -85,7 +85,7 @@ added in the `primal_constrained_variables` field of `PrimalDualMap`. if primal set is `EqualTo` or `Zeros`, then the dual constraint is `Reals` then the dual variable is free (no constraint in the dual model). """ -struct ConstraintData{T} +struct PrimalConstraintData{T} primal_set_constants::Vector{T} dual_variables::Vector{MOI.VariableIndex} dual_constrained_variable_constraint::Union{Nothing,MOI.ConstraintIndex} @@ -98,14 +98,14 @@ Maps information from all structures of the primal to the dual model. Main maps: - * `primal_variable_data::Dict{MOI.VariableIndex,Dualization.VariableData{T}}`: + * `primal_variable_data::Dict{MOI.VariableIndex,Dualization.PrimalVariableData{T}}`: maps primal variable indices to their data. The data is a structure that contains information about the primal variable and its dual counterpart. In particular, it contains the primal constrained variable constraint index, the primal constrained variable index, the dual constraint index and the primal function for the case of constraints that are not added in the dual. - * `primal_constraint_data::Dict{MOI.ConstraintIndex,Dualization.ConstraintData{T}}`: + * `primal_constraint_data::Dict{MOI.ConstraintIndex,Dualization.PrimalConstraintData{T}}`: maps primal constraint indices to their data. The data is a structure that contains information about the primal constraint and its dual counterpart. In particular, it contains the primal set constants, the dual variables and @@ -125,8 +125,8 @@ Addtional maps "slack" variables. These primal variables might appear in other maps. """ mutable struct PrimalDualMap{T} - primal_variable_data::Dict{MOI.VariableIndex,VariableData{T}} - primal_constraint_data::Dict{MOI.ConstraintIndex,ConstraintData{T}} + primal_variable_data::Dict{MOI.VariableIndex,PrimalVariableData{T}} + primal_constraint_data::Dict{MOI.ConstraintIndex,PrimalConstraintData{T}} primal_constrained_variables::Dict{ MOI.ConstraintIndex, Vector{MOI.VariableIndex}, @@ -141,8 +141,8 @@ mutable struct PrimalDualMap{T} } function PrimalDualMap{T}() where {T} return new( - Dict{MOI.VariableIndex,VariableData{T}}(), - Dict{MOI.ConstraintIndex,ConstraintData{T}}(), + Dict{MOI.VariableIndex,PrimalVariableData{T}}(), + Dict{MOI.ConstraintIndex,PrimalConstraintData{T}}(), Dict{MOI.ConstraintIndex,Vector{MOI.VariableIndex}}(), # Dict{MOI.VariableIndex,MOI.VariableIndex}(), @@ -151,6 +151,39 @@ mutable struct PrimalDualMap{T} end end +function _get_dual_constraint(m::PrimalDualMap, vi::MOI.VariableIndex) + data = m.primal_variable_data[vi] + return data.dual_constraint, data.primal_constrained_variable_index +end + +function _get_primal_constraint(m::PrimalDualMap, vi::MOI.VariableIndex) + data = m.primal_variable_data[vi] + return data.primal_constrained_variable_constraint, + data.primal_constrained_variable_index +end + +function _get_dual_variables(m::PrimalDualMap, ci::MOI.ConstraintIndex) + if !haskey(m.primal_constrained_variables, ci) + # if the constraint is a constrained variable, then the dual variable + # is the first element of the vector of dual variables + return m.primal_constraint_data[ci].dual_variables + end + return nothing # ci is a constrained variable constraint +end + +function _get_dual_constraint(m::PrimalDualMap, ci::MOI.ConstraintIndex) + if !haskey(m.primal_constrained_variables, ci) + # if the constraint is a constrained variable, then the dual variable + # is the first element of the vector of dual variables + return m.primal_constraint_data[ci].dual_constrained_variable_constraint + end + return nothing # ci is a constrained variable constraint +end + +function _get_dual_parameter(m::PrimalDualMap, vi::MOI.VariableIndex) + return m.primal_parameter_to_dual_parameter[vi] +end + function Base.getproperty(m::PrimalDualMap{T}, name::Symbol) where {T} if name === :constrained_var_idx error( diff --git a/test/Tests/test_JuMP_dualize.jl b/test/Tests/test_JuMP_dualize.jl index bb80506..47c1c9a 100644 --- a/test/Tests/test_JuMP_dualize.jl +++ b/test/Tests/test_JuMP_dualize.jl @@ -60,17 +60,44 @@ end # Test that unnamed objects don't create a key `Symbol("")` in `dual_model`. @variable(model) - @constraint(model, x == y) + @constraint(model, c, x == y) dual_model = dualize(model; dual_names = DualNames("dual", "")) @test typeof(dual_model[:dualeqcon]) == VariableRef @test !haskey(dual_model, Symbol("")) + + for var in (x, y, z) + con = Dualization._get_dual_constraint(dual_model, x) + @test con[1] isa ConstraintRef + @test con[2] isa Int + end + + var = Dualization._get_dual_variables(dual_model, soccon) + @test var === nothing + con = Dualization._get_dual_constraint(dual_model, soccon) + @test con === nothing + + con = Dualization._get_primal_constraint(dual_model, y) + @test con[1] isa ConstraintRef + @test con[2] == 2 + + var = Dualization._get_dual_variables(dual_model, eqcon) + @test length(var) == 1 + @test var[1] isa VariableRef + con = Dualization._get_dual_constraint(dual_model, eqcon) + @test con === nothing + + var = Dualization._get_dual_variables(dual_model, c) + @test length(var) == 1 + @test var[1] isa VariableRef + con = Dualization._get_dual_constraint(dual_model, c) + @test con === nothing end @testset "JuMP_dualize_kwargs" begin model = Model() @variable(model, x >= 0) - @constraint(model, x <= 2) + @constraint(model, c, x <= 2) @objective(model, Max, 2 * x + 1) dual_model = Dualization.dualize( model; @@ -80,5 +107,25 @@ end ) @test dual_model isa Model @test num_variables(dual_model) == 2 + con = Dualization._get_dual_constraint(dual_model, x) + @test con[1] isa ConstraintRef + @test con[2] == -1 + + con = Dualization._get_primal_constraint(dual_model, x) + @test con[1] === nothing + @test con[2] == -1 + + var = Dualization._get_dual_variables(dual_model, c) + @test length(var) == 1 + @test var[] isa VariableRef + con = Dualization._get_dual_constraint(dual_model, c) + @test con isa ConstraintRef + + cv = LowerBoundRef(x) + var = Dualization._get_dual_variables(dual_model, cv) + @test length(var) == 1 + @test var[] isa VariableRef + con = Dualization._get_dual_constraint(dual_model, cv) + @test con isa ConstraintRef end end