diff --git a/src/dual_equality_constraints.jl b/src/dual_equality_constraints.jl index b254370..3c3c1f5 100644 --- a/src/dual_equality_constraints.jl +++ b/src/dual_equality_constraints.jl @@ -39,6 +39,7 @@ function add_dual_equality_constraints( scalar_affine_terms, primal_dual_map.primal_var_dual_quad_slack, primal_objective, + sense_change, ) # terms from mixing variables and parameters @@ -227,26 +228,27 @@ function add_scalar_affine_terms_from_quad_obj( scalar_affine_terms::Dict{VI,Vector{MOI.ScalarAffineTerm{T}}}, primal_var_dual_quad_slack::Dict{VI,VI}, primal_objective::PrimalObjective{T}, + sense_change::T, ) where {T} for term in primal_objective.obj.quadratic_terms if term.variable_1 == term.variable_2 dual_vi = primal_var_dual_quad_slack[term.variable_1] push_to_scalar_affine_terms!( scalar_affine_terms[term.variable_1], - -MOI.coefficient(term), + -sense_change * MOI.coefficient(term), dual_vi, ) else dual_vi_1 = primal_var_dual_quad_slack[term.variable_1] push_to_scalar_affine_terms!( scalar_affine_terms[term.variable_2], - -MOI.coefficient(term), + -sense_change * MOI.coefficient(term), dual_vi_1, ) dual_vi_2 = primal_var_dual_quad_slack[term.variable_2] push_to_scalar_affine_terms!( scalar_affine_terms[term.variable_1], - -MOI.coefficient(term), + -sense_change * MOI.coefficient(term), dual_vi_2, ) end diff --git a/test/Tests/test_max_min_dual_equal_feasibility_quadratic.jl b/test/Tests/test_max_min_dual_equal_feasibility_quadratic.jl new file mode 100644 index 0000000..e724460 --- /dev/null +++ b/test/Tests/test_max_min_dual_equal_feasibility_quadratic.jl @@ -0,0 +1,101 @@ +function get_DualMinModel_no_bounds() + MinModel = Model() + @variable(MinModel, Q₁) + @variable(MinModel, Q₂) + + @objective(MinModel, Min, (Q₁ + Q₂)^2) + @constraint(MinModel, C₁, Q₁ + 1 >= 0) + @constraint(MinModel, C₂, Q₂ + 1 >= 0) + + DualMinModel = dualize(MinModel; dual_names = DualNames("dual", "")) + return DualMinModel +end + +function get_DualMaxModel_no_bounds() + MaxModel = Model() + @variable(MaxModel, Q₁) + @variable(MaxModel, Q₂) + @objective(MaxModel, Max, -(Q₁ + Q₂)^2) + @constraint(MaxModel, C₁, Q₁ + 1 >= 0) + @constraint(MaxModel, C₂, Q₂ + 1 >= 0) + + DualMaxModel = dualize(MaxModel; dual_names = DualNames("dual", "")) + return DualMaxModel +end + +function get_DualMinModel_with_bounds() + MinModel = Model() + @variable(MinModel, Q₁ >= 0) + @variable(MinModel, Q₂ >= 0) + + @objective(MinModel, Min, (Q₁ + Q₂)^2) + @constraint(MinModel, C₁, Q₁ + 1 >= 0) + @constraint(MinModel, C₂, Q₂ + 1 >= 0) + + DualMinModel = dualize(MinModel; dual_names = DualNames("dual", "")) + return DualMinModel +end + +function get_DualMaxModel_with_bounds() + MaxModel = Model() + + @variable(MaxModel, Q₁ >= 0) + @variable(MaxModel, Q₂ >= 0) + @objective(MaxModel, Max, -(Q₁ + Q₂)^2) + @constraint(MaxModel, C₁, Q₁ + 1 >= 0) + @constraint(MaxModel, C₂, Q₂ + 1 >= 0) + + DualMaxModel = dualize(MaxModel; dual_names = DualNames("dual", "")) + return DualMaxModel +end + +function test_equivalence_max_min(DualMinModel, DualMaxModel) + for (F, S) in list_of_constraint_types(DualMinModel) + DualMinModel_eq_con_funs = [ + MOI.get(backend(DualMinModel), MOI.ConstraintFunction(), ctr_idx) for + ctr_idx in JuMP.index.(all_constraints(DualMinModel, F, S)) + ] + DualMaxModel_eq_con_funs = [ + MOI.get(backend(DualMaxModel), MOI.ConstraintFunction(), ctr_idx) for + ctr_idx in JuMP.index.(all_constraints(DualMaxModel, F, S)) + ] + @test length(DualMinModel_eq_con_funs) == + length(DualMaxModel_eq_con_funs) + for i in eachindex(DualMinModel_eq_con_funs) + if typeof(DualMinModel_eq_con_funs[i]) != MOI.VariableIndex + @test MOI.coefficient.(DualMinModel_eq_con_funs[i].terms) == + MOI.coefficient.(DualMaxModel_eq_con_funs[i].terms) + @test MOI.constant.(DualMinModel_eq_con_funs[i]) == + MOI.constant.(DualMaxModel_eq_con_funs[i]) + end + end + DualMinModel_eq_con_sets = [ + MOI.get(backend(DualMinModel), MOI.ConstraintSet(), ctr_idx) for + ctr_idx in JuMP.index.(all_constraints(DualMinModel, F, S)) + ] + DualMaxModel_eq_con_sets = [ + MOI.get(backend(DualMaxModel), MOI.ConstraintSet(), ctr_idx) for + ctr_idx in JuMP.index.(all_constraints(DualMaxModel, F, S)) + ] + @test length(DualMinModel_eq_con_sets) == + length(DualMaxModel_eq_con_sets) + for i in eachindex(DualMinModel_eq_con_sets) + @test MOI.constant.(DualMinModel_eq_con_sets[i]) == + MOI.constant.(DualMaxModel_eq_con_sets[i]) + end + end +end + +@testset "max min dual equal feasibility quadratic" begin + @testset "max min dual equal feasibility quadratic no variable bounds" begin + DualMinModel = get_DualMinModel_no_bounds() + DualMaxModel = get_DualMaxModel_no_bounds() + test_equivalence_max_min(DualMinModel, DualMaxModel) + end + + @testset "max min dual equal feasibility quadratic with variable bounds" begin + DualMinModel = get_DualMinModel_with_bounds() + DualMaxModel = get_DualMaxModel_with_bounds() + test_equivalence_max_min(DualMinModel, DualMaxModel) + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 5112e23..ffca0fe 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -111,3 +111,6 @@ include("Solvers/scs_test.jl") include("Tests/test_JuMP_dualize.jl") include("Tests/test_MOI_wrapper.jl") include("Tests/test_modify.jl") + +# Test that dual feasibility of quadratic min and max is equivalent (see issue #142) +include("Tests/test_max_min_dual_equal_feasibility_quadratic.jl")