diff --git a/docs/src/manual.md b/docs/src/manual.md index ef1ed02..2bc38ab 100644 --- a/docs/src/manual.md +++ b/docs/src/manual.md @@ -205,7 +205,7 @@ When define MOI attributes to communicate information in MOI, layers like Dualiz these attributes to the right variables or constraints before or after their transformation. So the best practice is to define such variable or constraint attributes and attempt to `MOI.get` or `MOI.set` them. They is an API that you need to define for these attributes that contains the -functions are `dual_attribute`, `dual_attribute_value`, +functions are `dual_attribute`, `constrained_variable_dual_attribute`, `fixed_variable_value`, `fixed_constrained_variables_get`, `equality_constraint_get`. That's a lot of functions, some of which may never be useful for your specific attributes (e.g., if it is never used on an equality constraint) so no need diff --git a/docs/src/mathematical_background.md b/docs/src/mathematical_background.md index ae6401e..47501ed 100644 --- a/docs/src/mathematical_background.md +++ b/docs/src/mathematical_background.md @@ -54,8 +54,14 @@ and the dual is: ``` Note that the equality constraints have minus signs that could be flipped for -simplicity. However, for generality, we keep the minus signs so that the models -displayed here precisely match the outputs of the package. +simplicity, because negating both sides of an equality does not change it. +However, we keep the minus signs, so that the term ``- \sum_{i=1}^m A_i^T y_i`` +appears with the same sign in every dual constraint, whether that constraint +ends up being an equality (as here) or a membership in a nontrivial cone (as in +the compact form below). This makes the dual constraints directly readable as +the stationarity rows of the KKT conditions, which matters when the dual is +used to build a complementarity system. It also means the models displayed +here precisely match the outputs of the package. A linear inequality constraint ``a^T x + b \ge c`` should be interpreted as ``a^T x + b - c \in \mathbb{R}_+``, and similarly ``a^T x + b \le c`` should be @@ -179,7 +185,7 @@ and the dual is: \end{align} ``` -Note that signs changed in the constraints of the dual compared to the standard form. This is because the standard form would have negative signs in all terms in a equality constraint, which were inverted for simplicity. However, in the compact form, this operation is not allowed because it would change a nontrivial cone ``\mathcal{C}_i``. +Note that the constraints of the dual carry the same signs as in the standard form. Negating both sides of an equality would be allowed in the standard form, but it is not allowed here, because it would change a nontrivial cone ``\mathcal{V}_j^*``. Keeping a single convention for both forms means that adding or removing an explicit constraint on a primal variable does not flip the signs of the corresponding dual constraint. ##### Linear Programming diff --git a/docs/src/reference.md b/docs/src/reference.md index 30bdf97..f136b82 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -75,8 +75,6 @@ Dualization.PrimalDualMap ```@docs Dualization.dual_attribute -Dualization.dual_attribute_value_get -Dualization.dual_attribute_value_set Dualization.constrained_variable_dual_attribute Dualization.fixed_variable_value Dualization.fixed_constrained_variables_get diff --git a/src/attributes.jl b/src/attributes.jl index 2392648..1f715be 100644 --- a/src/attributes.jl +++ b/src/attributes.jl @@ -1,6 +1,3 @@ -_minus(::Nothing) = nothing -_minus(x) = -x - function _variable_attribute(attr::MOI.ConstraintPrimal) return MOI.VariablePrimal(attr.result_index) end @@ -57,34 +54,6 @@ function dual_attribute(::MOI.ConstraintDualStart) return MOI.VariablePrimalStart() end -""" - dual_attribute_value_set(attr::MOI.AbstractVariableAttribute, value) - -Used as pre-processing for `MOI.set`ting `value` for a variable. -""" -function dual_attribute_value_set end - -""" - dual_attribute_value_get(attr::MOI.AbstractVariableAttribute, value) - -Used as pre-processing for `MOI.get`ting `value` for a variable. -""" -function dual_attribute_value_get end - -function dual_attribute_value_set( - ::Union{MOI.VariablePrimal,MOI.VariablePrimalStart}, - value, -) - return _minus(value) -end - -function dual_attribute_value_get( - ::Union{MOI.VariablePrimal,MOI.VariablePrimalStart}, - value, -) - return _minus(value) -end - """ constrained_variable_dual_attribute(attr::MOI.AbstractConstraintAttribute) @@ -157,7 +126,7 @@ function MOI.set( optimizer.dual_problem.dual_model, dual_attribute(attr), data.dual_constraint, - dual_attribute_value_set(attr, value), + value, ) return end @@ -170,14 +139,10 @@ function MOI.get( primal_dual_map = optimizer.dual_problem.primal_dual_map data = primal_dual_map.primal_variable_data[vi] if isnothing(data.primal_constrained_variable_constraint) - # Classical free variable - return dual_attribute_value_get( - attr, - MOI.get( - optimizer.dual_problem.dual_model, - dual_attribute(attr), - data.dual_constraint, - ), + return MOI.get( + optimizer.dual_problem.dual_model, + dual_attribute(attr), + data.dual_constraint, ) end if isnothing(data.dual_constraint) diff --git a/src/dual_equality_constraints.jl b/src/dual_equality_constraints.jl index 0ce317e..7d59a32 100644 --- a/src/dual_equality_constraints.jl +++ b/src/dual_equality_constraints.jl @@ -22,9 +22,14 @@ function _add_dual_equality_constraints( non_parameter_variables = setdiff(all_variables, variable_parameters) # Loop at every constraint to collect the scalar affine terms in the - # `scalar_affine_terms` list (a dics mapping variable index to + # `scalar_affine_terms` list (a dics mapping variable index to # a scalar affine function). - # TODO: flip these signs a priorie instead of require post processing later + # `scalar_affine_terms[x_j]` holds the column `A_{.j}` of the primal + # constraint matrix, with the sign it has in the primal model, while every + # dual constraint uses `-A_{.j}^T y`. + # These terms cannot be negated here: they are also returned for + # `_get_dual_objective`, whose entries for variables that are parameters + # must keep the primal sign. scalar_affine_terms = _get_scalar_affine_terms( primal_model, primal_dual_map.primal_constraint_data, @@ -39,8 +44,9 @@ function _add_dual_equality_constraints( # Collect affine terms of dual constraints that come from the quadratic # part of the primal objective function, and add them into # `scalar_affine_terms`. - # These terms are added with flipped signs (because the sign will be flipped again). - # TODO: unflip these signs + # These terms are added with the same sign convention as the terms coming + # from the primal constraints, so that all of them can be negated at once + # when the dual constraint function is built. _add_scalar_affine_terms_from_quad_obj( scalar_affine_terms, primal_dual_map.primal_var_in_quad_obj_to_dual_slack_var, @@ -49,8 +55,9 @@ function _add_dual_equality_constraints( ) # terms from mixing variables and parameters - # These terms are added with flipped signs (because the sign will be flipped again). - # TODO: unflip these signs + # As above, these are added with the primal sign convention. Note that they + # are keyed by the *variable* of each product `parameter * variable`, hence + # they never touch the parameter entries read by `_get_dual_objective`. _add_scalar_affine_terms_from_quad_params( scalar_affine_terms, primal_dual_map.primal_parameter_to_dual_parameter, @@ -66,7 +73,6 @@ function _add_dual_equality_constraints( # primal constrained variable. # If the dual set is Reals, the constraint is not added, butthe function # is cached inthe primal dual map. - # TODO: flip these signs a priori instead of requiring post-processing later _add_constrained_variable_constraint( dual_model, primal_model, @@ -90,14 +96,18 @@ function _add_dual_equality_constraints( # these are constraints associated to primal variables that are not # treated as constrained variables, that is "free variables" (x \in R) # therefore their associated dual constraints are equalities. + # The function is `-A_{.j}^T y + a_j` for minimization and + # `-A_{.j}^T y - a_j` for maximization, the same convention used by + # `_add_constrained_variable_constraint`. Negating both sides here + # would be valid for an equality, but it would make the sign of a dual + # constraint depend on whether the primal variable is free or + # constrained, see + # https://github.com/jump-dev/Dualization.jl/issues/70. dual_ci = MOI.Utilities.normalize_and_add_constraint( dual_model, MOI.ScalarAffineFunction( - # TODO: flip these two signs bellow to match _add_constrained_variable_constraint - # MOI.Utilities.operate_terms(-, scalar_affine_terms[primal_vi]), - # sense_change * get(scalar_terms, primal_vi, zero(T))), - MOI.Utilities.operate_terms(+, scalar_affine_terms[primal_vi]), - -sense_change * get(scalar_terms, primal_vi, zero(T)), + MOI.Utilities.operate_terms(-, scalar_affine_terms[primal_vi]), + sense_change * get(scalar_terms, primal_vi, zero(T)), ), MOI.EqualTo(zero(T)), ) diff --git a/src/dual_names.jl b/src/dual_names.jl index 153f268..2836b5d 100644 --- a/src/dual_names.jl +++ b/src/dual_names.jl @@ -30,7 +30,7 @@ Subject to julia> print(dual_model) Max dual_var_c Subject to - dual_con_x : dual_var_c = 1 + dual_con_x : -dual_var_c = -1 dual_var_c ≥ 0 ``` """ diff --git a/test/Tests/test_attributes.jl b/test/Tests/test_attributes.jl index 3a93f4c..cda92fa 100644 --- a/test/Tests/test_attributes.jl +++ b/test/Tests/test_attributes.jl @@ -233,7 +233,10 @@ function _test_simple(T, dual_model) }(), )[] @test MOI.get(dual, MOI.VariablePrimalStart(), x) == 1 - @test MOI.get(dual_model, MOI.ConstraintDualStart(), dual_eq) == -1 + # The dual constraint of the free variable `x` is `-2y + 0 == 0`, so the + # primal start of `x` is stored as the dual start of that constraint with + # no sign change, see https://github.com/jump-dev/Dualization.jl/issues/70. + @test MOI.get(dual_model, MOI.ConstraintDualStart(), dual_eq) == 1 # We could set it to zero, but `nothing` should be fine for the solver, # let's only revisit if we have a convincing use case @test isnothing(MOI.get(dual_model, MOI.ConstraintPrimalStart(), dual_eq)) @@ -252,6 +255,12 @@ function _test_simple(T, dual_model) MOI.set(dual, MOI.VariablePrimalStart(), vars[], nothing) @test isnothing(MOI.get(dual, MOI.VariablePrimalStart(), vars[])) + + # `MOI.VariableName` maps to the `MOI.ConstraintName` of the associated + # dual constraint and is passed through unchanged. + MOI.set(dual, MOI.VariableName(), x, "x") + @test MOI.get(dual, MOI.VariableName(), x) == "x" + @test MOI.get(dual_model, MOI.ConstraintName(), dual_eq) == "x" return end diff --git a/test/Tests/test_dual_constraint_signs.jl b/test/Tests/test_dual_constraint_signs.jl new file mode 100644 index 0000000..a700d1d --- /dev/null +++ b/test/Tests/test_dual_constraint_signs.jl @@ -0,0 +1,229 @@ +# 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. + +#= +Every dual constraint associated with a primal variable `x_j` is built as + + -A_{.j}^T y + a_j in V_j^* (minimization) + -A_{.j}^T y - a_j in V_j^* (maximization) + +where `A_{.j}` is the column of `x_j` in the primal constraints and `a_j` is +its objective coefficient. + +For an equality (`V_j^*` being `Reals`, i.e. `x_j` is a free variable) both +sides could be negated without changing the model. That is +undesirable for downstream users that read the dual constraints as the +stationarity rows of a KKT system. + +These tests pin the uniform convention down. +=# + +# Return the terms of the single dual constraint associated with the only +# primal variable of `primal_model`, as a `(coefficients, constant)` pair. +# `MOI.Utilities.normalize_and_add_constraint` moves the constant of a scalar +# constraint into the set, so the constant is read back from the set when the +# dual constraint is a scalar one. +function _dual_constraint_terms(primal_model) + dual_model = Dualization.dualize(primal_model).dual_model + cons = MOI.get(dual_model, MOI.ListOfConstraintTypesPresent()) + for (F, S) in cons + if F === MOI.VariableIndex + continue # a bound on a dual variable, not a dual constraint + end + ci = only(MOI.get(dual_model, MOI.ListOfConstraintIndices{F,S}())) + f = MOI.get(dual_model, MOI.ConstraintFunction(), ci) + set = MOI.get(dual_model, MOI.ConstraintSet(), ci) + if f isa MOI.ScalarAffineFunction + # `f in EqualTo(b)` is the same as `f - b in EqualTo(0)` + return MOI.coefficient.(f.terms), + MOI.constant(f) - MOI.constant(set) + else + scalars = MOI.Utilities.scalarize(f) + return reduce(vcat, MOI.coefficient.(s.terms) for s in scalars), + only(unique(MOI.constant.(scalars))) + end + end + return error("no dual constraint found") +end + +@testset "dual constraint signs (issue #70)" begin + # A primal with a single variable `x`, one constraint `2x >= 3` and the + # objective `11x + 13`. The dual constraint must be `-2y + 11` for `Min` + # and `-2y - 11` for `Max`, no matter how `x` is declared. + function _single_variable_model(::Type{T}, sense, variable) where {T} + model = TestModel{T}() + x, _ = variable(model) + MOI.add_constraint( + model, + MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(T(2), x)], zero(T)), + MOI.GreaterThan(T(3)), + ) + MOI.set(model, MOI.ObjectiveSense(), sense) + MOI.set( + model, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}(), + MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(T(11), x)], T(13)), + ) + return model + end + + # `x` is free because no variable-wise constraint is added at all. + _free(model) = (MOI.add_variable(model), nothing) + # `x` is free, but stated explicitly as a constrained variable in `Reals`. + function _reals(model) + x, ci = MOI.add_constrained_variables(model, MOI.Reals(1)) + return only(x), ci + end + # `x` is free, but stated explicitly as a constrained variable in `Zeros`, + # whose dual set is `Reals`. + function _zeros_dual(model) + x, ci = MOI.add_constrained_variables(model, MOI.Zeros(1)) + return only(x), ci + end + # `x >= 0`: the dual constraint is an inequality instead of an equality. + function _nonnegative(model) + x, ci = MOI.add_constrained_variable(model, MOI.GreaterThan(0.0)) + return x, ci + end + + @testset "uniform sign for $(nameof(variable))" for variable in [ + _free, + _reals, + _zeros_dual, + _nonnegative, + ] + # Minimization: `-A^T y + a_0`. + model = _single_variable_model(Float64, MOI.MIN_SENSE, variable) + coefficients, constant = _dual_constraint_terms(model) + @test coefficients == [-2.0] + @test constant == 11.0 + # Maximization: `-A^T y - a_0`. + model = _single_variable_model(Float64, MOI.MAX_SENSE, variable) + coefficients, constant = _dual_constraint_terms(model) + @test coefficients == [-2.0] + @test constant == -11.0 + end + + @testset "free variable matches Reals and Zeros" begin + # This is the invariant the issue is about: the way a free variable is + # spelled in the primal must not change the dual constraint. + for sense in [MOI.MIN_SENSE, MOI.MAX_SENSE] + reference = _dual_constraint_terms( + _single_variable_model(Float64, sense, _free), + ) + for variable in [_reals, _zeros_dual] + @test _dual_constraint_terms( + _single_variable_model(Float64, sense, variable), + ) == reference + end + end + end + + @testset "equality and inequality rows agree" begin + #= + primal + min -4x1 -3x2 -1 + s.t. + 2x1 + x2 <= 3 :y_1 + x1 + 2x2 <= 3 :y_2 + x2 >= 0 + + `x1` is free, so its dual constraint is an equality, while `x2` is + constrained, so its dual constraint is an inequality. Both must expose + the same `-A_{.j}^T y` sign; only the set differs. + =# + primal_model = TestModel{Float64}() + x1 = MOI.add_variable(primal_model) + x2, _ = MOI.add_constrained_variable(primal_model, MOI.GreaterThan(0.0)) + MOI.add_constraint(primal_model, 1.0 * x1 + 2.0 * x2, MOI.LessThan(3.0)) + MOI.set(primal_model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set( + primal_model, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + -4.0 * x1 - 3.0 * x2 - 1.0, + ) + dual_model = Dualization.dualize(primal_model).dual_model + # `x1` is free: `-y - 4 == 0`. + eq_ci = only( + MOI.get( + dual_model, + MOI.ListOfConstraintIndices{ + MOI.ScalarAffineFunction{Float64}, + MOI.EqualTo{Float64}, + }(), + ), + ) + eq_f = MOI.get(dual_model, MOI.ConstraintFunction(), eq_ci) + eq_set = MOI.get(dual_model, MOI.ConstraintSet(), eq_ci) + @test MOI.coefficient.(eq_f.terms) == [-1.0] + @test MOI.constant(eq_f) - MOI.constant(eq_set) == -4.0 + # `x2 >= 0`: `-2y - 3 >= 0`. + ineq_ci = only( + MOI.get( + dual_model, + MOI.ListOfConstraintIndices{ + MOI.ScalarAffineFunction{Float64}, + MOI.GreaterThan{Float64}, + }(), + ), + ) + ineq_f = MOI.get(dual_model, MOI.ConstraintFunction(), ineq_ci) + ineq_set = MOI.get(dual_model, MOI.ConstraintSet(), ineq_ci) + @test MOI.coefficient.(ineq_f.terms) == [-2.0] + @test MOI.constant(ineq_f) - MOI.constant(ineq_set) == -3.0 + end + + @testset "quadratic objective keeps +P w" begin + #= + primal + min 0.5 * 4 * x^2 + 11x + 13 + s.t. + 2x >= 3 :y + + The dual constraint follows the KKT stationarity row + `a_0 - A^T y + P w = 0`, that is, `11 - 2y + 4w == 0`. The sign in + front of `P` is a free choice (`w` is free and only appears in a + symmetric quadratic term elsewhere), and `+P` is the one that matches + the documented stationarity condition. + + For a maximization problem with a concave objective `0.5 x^T N x`, + the documented dual row is `-a_0 - A^T y - N w = 0`, so the slack + term keeps the coefficient `-N`, which is positive again here. + =# + for (sense, a_0, w_coefficient) in + [(MOI.MIN_SENSE, 11.0, 4.0), (MOI.MAX_SENSE, -11.0, 4.0)] + primal_model = TestModel{Float64}() + x = MOI.add_variable(primal_model) + MOI.add_constraint(primal_model, 2.0 * x, MOI.GreaterThan(3.0)) + MOI.set(primal_model, MOI.ObjectiveSense(), sense) + # `Max` needs a concave objective, hence the flipped `P`. + P = sense == MOI.MIN_SENSE ? 4.0 : -4.0 + MOI.set( + primal_model, + MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(), + MOI.ScalarQuadraticFunction( + [MOI.ScalarQuadraticTerm(P, x, x)], + [MOI.ScalarAffineTerm(11.0, x)], + 13.0, + ), + ) + dual_model = Dualization.dualize(primal_model).dual_model + ci = only( + MOI.get( + dual_model, + MOI.ListOfConstraintIndices{ + MOI.ScalarAffineFunction{Float64}, + MOI.EqualTo{Float64}, + }(), + ), + ) + f = MOI.get(dual_model, MOI.ConstraintFunction(), ci) + set = MOI.get(dual_model, MOI.ConstraintSet(), ci) + # The dual variable `y` comes first, then the slack `w`. + @test MOI.coefficient.(f.terms) == [-2.0, w_coefficient] + @test MOI.constant(f) - MOI.constant(set) == a_0 + end + end +end diff --git a/test/Tests/test_dualize_conic_linear.jl b/test/Tests/test_dualize_conic_linear.jl index 1ff782c..0304b97 100644 --- a/test/Tests/test_dualize_conic_linear.jl +++ b/test/Tests/test_dualize_conic_linear.jl @@ -106,7 +106,7 @@ dual max -4w_4 - 3w_5 + 12w_6 s.t - w_1 + w_3 == 3 + -w_1 - w_3 == -3 [-w_2 + 2] in Nonpositives [-w_3 - 4] in Nonnegatives =# @@ -162,9 +162,9 @@ ) eq_con1_fun = MOI.get(dual_model, MOI.ConstraintFunction(), ci_eq) eq_con1_set = MOI.get(dual_model, MOI.ConstraintSet(), ci_eq) - @test MOI.coefficient.(eq_con1_fun.terms) == [1.0; 1.0] + @test MOI.coefficient.(eq_con1_fun.terms) == [-1.0; -1.0] @test MOI.constant.(eq_con1_fun) == 0.0 - @test MOI.constant(eq_con1_set) == 3.0 + @test MOI.constant(eq_con1_set) == -3.0 ci_np = first( MOI.get( dual_model, diff --git a/test/Tests/test_dualize_exponential.jl b/test/Tests/test_dualize_exponential.jl index 5910efb..e87f823 100644 --- a/test/Tests/test_dualize_exponential.jl +++ b/test/Tests/test_dualize_exponential.jl @@ -55,9 +55,9 @@ dual max 2w_2 + w_1 s.t. - w_1 + w_3 == 1 - w_2 + w_4 == 1 - w_5 == 1 + -w_1 - w_3 == -1 + -w_2 - w_4 == -1 + -w_5 == -1 (w_3, w_4, w_5) ∈ DualExponentialCone =# primal_model = exp2_test() @@ -102,19 +102,19 @@ eq_con1_fun = MOI.get(dual_model, MOI.ConstraintFunction(), eq_con1) eq_con1_set = MOI.get(dual_model, MOI.ConstraintSet(), eq_con1) - @test MOI.coefficient.(eq_con1_fun.terms) == [1.0; 1.0] + @test MOI.coefficient.(eq_con1_fun.terms) == [-1.0; -1.0] @test MOI.constant.(eq_con1_fun) == 0.0 - @test MOI.constant(eq_con1_set) == 1.0 + @test MOI.constant(eq_con1_set) == -1.0 eq_con2_fun = MOI.get(dual_model, MOI.ConstraintFunction(), eq_con2) eq_con2_set = MOI.get(dual_model, MOI.ConstraintSet(), eq_con2) - @test MOI.coefficient.(eq_con2_fun.terms) == [1.0; 1.0] + @test MOI.coefficient.(eq_con2_fun.terms) == [-1.0; -1.0] @test MOI.constant.(eq_con2_fun) == 0.0 - @test MOI.constant(eq_con2_set) == 1.0 + @test MOI.constant(eq_con2_set) == -1.0 eq_con3_fun = MOI.get(dual_model, MOI.ConstraintFunction(), eq_con3) eq_con3_set = MOI.get(dual_model, MOI.ConstraintSet(), eq_con3) - @test MOI.coefficient.(eq_con3_fun.terms) == [1.0] + @test MOI.coefficient.(eq_con3_fun.terms) == [-1.0] @test MOI.constant.(eq_con3_fun) == 0.0 - @test MOI.constant(eq_con3_set) == 1.0 + @test MOI.constant(eq_con3_set) == -1.0 dual_exp_con = MOI.get( dual_model, diff --git a/test/Tests/test_dualize_linear.jl b/test/Tests/test_dualize_linear.jl index aca2243..f7692a4 100644 --- a/test/Tests/test_dualize_linear.jl +++ b/test/Tests/test_dualize_linear.jl @@ -16,8 +16,8 @@ s.t. y_2 >= 0 y_3 <= 0 - y_2 + y_3 == 0 :x_1 - 2y_3 == -4 :x_2 + -y_2 - y_3 == 0 :x_1 + -2y_3 == 4 :x_2 =# primal_model = lp1_test() dual_model, primal_dual_map = dual_model_and_map(primal_model) @@ -68,8 +68,8 @@ standard dual max 3y_4 + 3y_3 + y_1 - 1 s.a. - y_1 + 2y_3 + y_4 = -4 :x_1 - y_2 + y_3 + 2y_4 = -3 :x_2 + -y_1 - 2y_3 - y_4 = 4 :x_1 + -y_2 - y_3 - 2y_4 = 3 :x_2 y_1 >= 0 y_2 >= 0 y_3 <= 0 @@ -77,7 +77,7 @@ compact dual max 3y_4 + 3y_3 + y_1 - 1 s.a. - y_1 + 2y_3 + y_4 = -4 :x_1 + -y_1 - 2y_3 - y_4 = 4 :x_1 - y_3 - 2y_4 >= 3 :x_2 y_1 >= 0 y_3 <= 0 @@ -135,9 +135,9 @@ }(), )[] f = MOI.get(dual_model, MOI.ConstraintFunction(), ci) - @test Set(MOI.coefficient.(f.terms)) == Set([1.0; 2.0; 1.0]) + @test Set(MOI.coefficient.(f.terms)) == Set([-1.0; -2.0; -1.0]) @test MOI.constant(f) == 0.0 - @test MOI.get(dual_model, MOI.ConstraintSet(), ci) == MOI.EqualTo(-4.0) + @test MOI.get(dual_model, MOI.ConstraintSet(), ci) == MOI.EqualTo(4.0) ci = MOI.get( dual_model, MOI.ListOfConstraintIndices{ @@ -235,7 +235,7 @@ }(), ) @test Set(MOI.get.(dual_model, MOI.ConstraintSet(), cis)) == - Set([MOI.EqualTo(-4.0), MOI.EqualTo(-3.0)]) + Set([MOI.EqualTo(4.0), MOI.EqualTo(3.0)]) end @testset "lp14_test_min" begin @@ -338,13 +338,17 @@ @test MOI.constant(f) == 0.0 @test MOI.get(dual_model, MOI.ConstraintSet(), ci) == MOI.LessThan(3.0) - # TODO flip these - # ci = MOI.get(dual_model, MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{Float64},MOI.EqualTo{Float64}}())[] - # f = MOI.get(dual_model, MOI.ConstraintFunction(), ci) - # @test Set(MOI.coefficient.(f.terms)) == Set([-7.0; +11.0; -15.0]) - # @test MOI.constant(f) == 0.0 - # @test MOI.get(dual_model, MOI.ConstraintSet(), ci) == MOI.EqualTo(-4.0) - + ci = MOI.get( + dual_model, + MOI.ListOfConstraintIndices{ + MOI.ScalarAffineFunction{Float64}, + MOI.EqualTo{Float64}, + }(), + )[] + f = MOI.get(dual_model, MOI.ConstraintFunction(), ci) + @test Set(MOI.coefficient.(f.terms)) == Set([-7.0; +11.0; -15.0]) + @test MOI.constant(f) == 0.0 + @test MOI.get(dual_model, MOI.ConstraintSet(), ci) == MOI.EqualTo(-4.0) end @testset "lp14_test_max compact" begin #= @@ -456,13 +460,17 @@ @test MOI.constant(f) == 0.0 @test MOI.get(dual_model, MOI.ConstraintSet(), ci) == MOI.LessThan(-3.0) - # TODO flip these - # ci = MOI.get(dual_model, MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{Float64},MOI.EqualTo{Float64}}())[] - # f = MOI.get(dual_model, MOI.ConstraintFunction(), ci) - # @test Set(MOI.coefficient.(f.terms)) == Set([-7.0; +11.0; -15.0]) - # @test MOI.constant(f) == 0.0 - # @test MOI.get(dual_model, MOI.ConstraintSet(), ci) == MOI.EqualTo(4.0) - + ci = MOI.get( + dual_model, + MOI.ListOfConstraintIndices{ + MOI.ScalarAffineFunction{Float64}, + MOI.EqualTo{Float64}, + }(), + )[] + f = MOI.get(dual_model, MOI.ConstraintFunction(), ci) + @test Set(MOI.coefficient.(f.terms)) == Set([-7.0; +11.0; -15.0]) + @test MOI.constant(f) == 0.0 + @test MOI.get(dual_model, MOI.ConstraintSet(), ci) == MOI.EqualTo(4.0) end @testset "lp14_test_min standard" begin #= @@ -563,23 +571,22 @@ MOI.EqualTo{Float64}, }(), ) - # TODO: review signs here @test Set(MOI.get.(dual_model, MOI.ConstraintSet(), cis)) == - Set([MOI.EqualTo(2.0), MOI.EqualTo(-3.0), MOI.EqualTo(4.0)]) + Set([MOI.EqualTo(-2.0), MOI.EqualTo(3.0), MOI.EqualTo(-4.0)]) for ci in cis set = MOI.get(dual_model, MOI.ConstraintSet(), ci) f = MOI.get(dual_model, MOI.ConstraintFunction(), ci) - if set == MOI.EqualTo(2.0) + if set == MOI.EqualTo(-2.0) @test Set(MOI.coefficient.(f.terms)) == - Set(-[-5.0; 9.0; -13.0; -1]) + Set([-5.0; 9.0; -13.0; -1]) @test MOI.constant(f) == 0.0 - elseif set == MOI.EqualTo(-3.0) + elseif set == MOI.EqualTo(3.0) @test Set(MOI.coefficient.(f.terms)) == - Set(-[6.0; -10.0; +14.0; -1]) + Set([6.0; -10.0; +14.0; -1]) @test MOI.constant(f) == 0.0 - elseif set == MOI.EqualTo(4.0) + elseif set == MOI.EqualTo(-4.0) @test Set(MOI.coefficient.(f.terms)) == - Set(-[-7.0; +11.0; -15.0]) + Set([-7.0; +11.0; -15.0]) @test MOI.constant(f) == 0.0 else error("wrong ci") @@ -685,23 +692,22 @@ MOI.EqualTo{Float64}, }(), ) - # TODO: review signs here @test Set(MOI.get.(dual_model, MOI.ConstraintSet(), cis)) == - Set([MOI.EqualTo(-2.0), MOI.EqualTo(3.0), MOI.EqualTo(-4.0)]) + Set([MOI.EqualTo(2.0), MOI.EqualTo(-3.0), MOI.EqualTo(4.0)]) for ci in cis set = MOI.get(dual_model, MOI.ConstraintSet(), ci) f = MOI.get(dual_model, MOI.ConstraintFunction(), ci) - if set == MOI.EqualTo(-2.0) + if set == MOI.EqualTo(2.0) @test Set(MOI.coefficient.(f.terms)) == - Set(-[-5.0; 9.0; -13.0; -1]) + Set([-5.0; 9.0; -13.0; -1]) @test MOI.constant(f) == 0.0 - elseif set == MOI.EqualTo(3.0) + elseif set == MOI.EqualTo(-3.0) @test Set(MOI.coefficient.(f.terms)) == - Set(-[6.0; -10.0; +14.0; -1]) + Set([6.0; -10.0; +14.0; -1]) @test MOI.constant(f) == 0.0 - elseif set == MOI.EqualTo(-4.0) + elseif set == MOI.EqualTo(4.0) @test Set(MOI.coefficient.(f.terms)) == - Set(-[-7.0; +11.0; -15.0]) + Set([-7.0; +11.0; -15.0]) @test MOI.constant(f) == 0.0 else error("wrong ci") diff --git a/test/Tests/test_dualize_power.jl b/test/Tests/test_dualize_power.jl index d13fa75..9e7a64f 100644 --- a/test/Tests/test_dualize_power.jl +++ b/test/Tests/test_dualize_power.jl @@ -55,9 +55,9 @@ dual min -w_2 - 2w_1 s.t. - w_1 + w_3 == 0 - w_2 + w_4 == 0 - w_5 == -1 + -w_1 - w_3 == 0 + -w_2 - w_4 == 0 + -w_5 == 1 (w_3, w_4, w_5) ∈ DualPowerCone =# primal_model = pow2_test() @@ -109,19 +109,19 @@ eq_con1_fun = MOI.get(dual_model, MOI.ConstraintFunction(), eq_con1) eq_con1_set = MOI.get(dual_model, MOI.ConstraintSet(), eq_con1) - @test MOI.coefficient.(eq_con1_fun.terms) == [1.0; 1.0] + @test MOI.coefficient.(eq_con1_fun.terms) == [-1.0; -1.0] @test MOI.constant.(eq_con1_fun) == 0.0 @test MOI.constant(eq_con1_set) == 0.0 eq_con2_fun = MOI.get(dual_model, MOI.ConstraintFunction(), eq_con2) eq_con2_set = MOI.get(dual_model, MOI.ConstraintSet(), eq_con2) - @test MOI.coefficient.(eq_con2_fun.terms) == [1.0; 1.0] + @test MOI.coefficient.(eq_con2_fun.terms) == [-1.0; -1.0] @test MOI.constant.(eq_con2_fun) == 0.0 @test MOI.constant(eq_con2_set) == 0.0 eq_con3_fun = MOI.get(dual_model, MOI.ConstraintFunction(), eq_con3) eq_con3_set = MOI.get(dual_model, MOI.ConstraintSet(), eq_con3) - @test MOI.coefficient.(eq_con3_fun.terms) == [1.0] + @test MOI.coefficient.(eq_con3_fun.terms) == [-1.0] @test MOI.constant.(eq_con3_fun) == 0.0 - @test MOI.constant(eq_con3_set) == -1.0 + @test MOI.constant(eq_con3_set) == 1.0 dual_pow_con = MOI.get(dual_model, MOI.ConstraintFunction(), pow_con) diff --git a/test/Tests/test_dualize_quadratic.jl b/test/Tests/test_dualize_quadratic.jl index 1e6fd7d..4281e1c 100644 --- a/test/Tests/test_dualize_quadratic.jl +++ b/test/Tests/test_dualize_quadratic.jl @@ -73,7 +73,7 @@ 1, ), ) - @test MOI.coefficient.(eq_con1_fun.terms) == [1.0; 1.0; -2.0; -1.0] + @test MOI.coefficient.(eq_con1_fun.terms) == [-1.0; -1.0; 2.0; 1.0] @test MOI.constant.(eq_con1_fun) == 0.0 @test MOI.constant(eq_con1_set) == 0.0 eq_con2_fun = MOI.get( @@ -96,8 +96,7 @@ 2, ), ) - @test MOI.coefficient.(eq_con2_fun.terms) == - [2.0; 1.0; -1.0; -2.0; -1.0] + @test MOI.coefficient.(eq_con2_fun.terms) == [-2.0; -1.0; 1.0; 2.0; 1.0] @test MOI.constant.(eq_con2_fun) == 0.0 @test MOI.constant(eq_con2_set) == 0.0 eq_con3_fun = MOI.get( @@ -120,7 +119,7 @@ 3, ), ) - @test MOI.coefficient.(eq_con3_fun.terms) == [3.0; -1.0; -2.0] + @test MOI.coefficient.(eq_con3_fun.terms) == [-3.0; 1.0; 2.0] @test MOI.constant.(eq_con3_fun) == 0.0 @test MOI.constant(eq_con3_set) == 0.0 diff --git a/test/Tests/test_dualize_rsoc.jl b/test/Tests/test_dualize_rsoc.jl index 62c17f9..edc63f0 100644 --- a/test/Tests/test_dualize_rsoc.jl +++ b/test/Tests/test_dualize_rsoc.jl @@ -56,10 +56,10 @@ dual max w_2 + (1/2)w_1 s.t. - w_5 == -1 - w_6 == -1 - w_1 + w_3 == 0 - w_2 + w_4 == 0 + -w_5 == 1 + -w_6 == 1 + -w_1 - w_3 == 0 + -w_2 - w_4 == 0 (w_3, w_4, w_5, w_6) \in RotatedSecondOrderCone =# primal_model = rsoc2_test() @@ -104,14 +104,14 @@ eq_con1_fun = MOI.get(dual_model, MOI.ConstraintFunction(), eq_con1) eq_con1_set = MOI.get(dual_model, MOI.ConstraintSet(), eq_con1) - @test MOI.coefficient.(eq_con1_fun.terms) == [1.0] + @test MOI.coefficient.(eq_con1_fun.terms) == [-1.0] @test MOI.constant.(eq_con1_fun) == 0.0 - @test MOI.constant(eq_con1_set) == -1.0 + @test MOI.constant(eq_con1_set) == 1.0 eq_con2_fun = MOI.get(dual_model, MOI.ConstraintFunction(), eq_con2) eq_con2_set = MOI.get(dual_model, MOI.ConstraintSet(), eq_con2) - @test MOI.coefficient.(eq_con2_fun.terms) == [1.0] + @test MOI.coefficient.(eq_con2_fun.terms) == [-1.0] @test MOI.constant.(eq_con2_fun) == 0.0 - @test MOI.constant(eq_con2_set) == -1.0 + @test MOI.constant(eq_con2_set) == 1.0 rsoc_con = MOI.get( dual_model, diff --git a/test/Tests/test_dualize_sdp.jl b/test/Tests/test_dualize_sdp.jl index a588c55..4b62663 100644 --- a/test/Tests/test_dualize_sdp.jl +++ b/test/Tests/test_dualize_sdp.jl @@ -54,9 +54,9 @@ dual max y_1 s.t. - y_2 == 1 - y_1 + 2y_3 == 0 - y_4 == 1 + -y_2 == -1 + -y_1 - 2y_3 == 0 + -y_4 == -1 [y_2 y_3 y_3 y_4] in PSD =# @@ -109,19 +109,19 @@ eq_con1_fun = MOI.get(dual_model, MOI.ConstraintFunction(), eq_con1) eq_con1_set = MOI.get(dual_model, MOI.ConstraintSet(), eq_con1) - @test MOI.coefficient.(eq_con1_fun.terms) == [1.0] + @test MOI.coefficient.(eq_con1_fun.terms) == [-1.0] @test MOI.constant.(eq_con1_fun) == 0.0 - @test MOI.constant(eq_con1_set) == 1.0 + @test MOI.constant(eq_con1_set) == -1.0 eq_con2_fun = MOI.get(dual_model, MOI.ConstraintFunction(), eq_con2) eq_con2_set = MOI.get(dual_model, MOI.ConstraintSet(), eq_con2) - @test MOI.coefficient.(eq_con2_fun.terms) == [1.0; 2.0] + @test MOI.coefficient.(eq_con2_fun.terms) == [-1.0; -2.0] @test MOI.constant.(eq_con2_fun) == 0.0 @test MOI.constant(eq_con2_set) == 0.0 eq_con3_fun = MOI.get(dual_model, MOI.ConstraintFunction(), eq_con3) eq_con3_set = MOI.get(dual_model, MOI.ConstraintSet(), eq_con3) - @test MOI.coefficient.(eq_con3_fun.terms) == [1.0] + @test MOI.coefficient.(eq_con3_fun.terms) == [-1.0] @test MOI.constant.(eq_con3_fun) == 0.0 - @test MOI.constant(eq_con3_set) == 1.0 + @test MOI.constant(eq_con3_set) == -1.0 sdp_con = MOI.get(dual_model, MOI.ConstraintFunction(), spd_con) diff --git a/test/Tests/test_dualize_soc.jl b/test/Tests/test_dualize_soc.jl index 152c32b..303e4dd 100644 --- a/test/Tests/test_dualize_soc.jl +++ b/test/Tests/test_dualize_soc.jl @@ -50,9 +50,9 @@ dual min -w_4 s.t. - w_2 == -1 - w_3 == -1 - w_1 + w_4 == 0 + -w_2 == 1 + -w_3 == 1 + -w_1 - w_4 == 0 w_1 >= ||(w_2, w_3)|| =# primal_model = soc2_test() @@ -93,19 +93,19 @@ eq_con1_fun = MOI.get(dual_model, MOI.ConstraintFunction(), eq_con1) eq_con1_set = MOI.get(dual_model, MOI.ConstraintSet(), eq_con1) - @test MOI.coefficient.(eq_con1_fun.terms) == [1.0; 1.0] + @test MOI.coefficient.(eq_con1_fun.terms) == [-1.0; -1.0] @test MOI.constant.(eq_con1_fun) == 0.0 @test MOI.constant(eq_con1_set) == 0.0 eq_con2_fun = MOI.get(dual_model, MOI.ConstraintFunction(), eq_con2) eq_con2_set = MOI.get(dual_model, MOI.ConstraintSet(), eq_con2) - @test MOI.coefficient.(eq_con2_fun.terms) == [1.0] + @test MOI.coefficient.(eq_con2_fun.terms) == [-1.0] @test MOI.constant.(eq_con2_fun) == 0.0 - @test MOI.constant(eq_con2_set) == -1.0 + @test MOI.constant(eq_con2_set) == 1.0 eq_con3_fun = MOI.get(dual_model, MOI.ConstraintFunction(), eq_con3) eq_con3_set = MOI.get(dual_model, MOI.ConstraintSet(), eq_con3) - @test MOI.coefficient.(eq_con3_fun.terms) == [1.0] + @test MOI.coefficient.(eq_con3_fun.terms) == [-1.0] @test MOI.constant.(eq_con3_fun) == 0.0 - @test MOI.constant(eq_con3_set) == -1.0 + @test MOI.constant(eq_con3_set) == 1.0 soc_con = MOI.get( dual_model, diff --git a/test/Tests/test_partial_dual_linear.jl b/test/Tests/test_partial_dual_linear.jl index 3cd4c71..1d6e817 100644 --- a/test/Tests/test_partial_dual_linear.jl +++ b/test/Tests/test_partial_dual_linear.jl @@ -17,7 +17,7 @@ s.t. y_2 >= 0 y_3 <= 0 - y_2 + y_3 == 0 :x_1 + -y_2 - y_3 == 0 :x_1 =# primal_model = lp1_test() dual = Dualization.dualize( @@ -77,7 +77,7 @@ 1, ), ) - @test MOI.coefficient.(eq_con1_fun.terms) == [1.0; 1.0] + @test MOI.coefficient.(eq_con1_fun.terms) == [-1.0; -1.0] @test MOI.constant.(eq_con1_fun) == 0.0 @test MOI.constant(eq_con1_set) == 0.0 @@ -244,9 +244,9 @@ dual obj ignored s.t. - # y_1 + y_3 == 0 :x_1 - y_2 + 2y_3 == 0 :x_2 - # y_3 == 4.0 :x_3 + # -y_1 - y_3 == 0 :x_1 + -y_2 - 2y_3 == 0 :x_2 + # -y_3 == -4.0 :x_3 # y_1 <= 0 y_2 <= 0 y_3 <= 0 @@ -303,7 +303,7 @@ 1, ), ) - @test MOI.coefficient.(eq_con2_fun.terms) == [2.0; 1.0] + @test MOI.coefficient.(eq_con2_fun.terms) == [-2.0; -1.0] @test MOI.constant.(eq_con2_fun) == 0.0 @test MOI.constant(eq_con2_set) == 0.0 @@ -347,9 +347,9 @@ dual obj ignored s.t. - # y_1 + y_3 == 0 :x_1 - y_2 + 2y_3 == 0 :x_2 - # y_3 == 4.0 :x_3 + # -y_1 - y_3 == 0 :x_1 + -y_2 - 2y_3 == 0 :x_2 + # -y_3 == -4.0 :x_3 # y_1 <= 0 y_2 <= 0 y_3 <= 0 @@ -410,7 +410,7 @@ 1, ), ) - @test MOI.coefficient.(eq_con2_fun.terms) == [2.0; 1.0] + @test MOI.coefficient.(eq_con2_fun.terms) == [-2.0; -1.0] @test MOI.constant.(eq_con2_fun) == 0.0 @test MOI.constant(eq_con2_set) == 0.0 diff --git a/test/Tests/test_partial_dual_quadratic.jl b/test/Tests/test_partial_dual_quadratic.jl index 14af7ac..12ca9b5 100644 --- a/test/Tests/test_partial_dual_quadratic.jl +++ b/test/Tests/test_partial_dual_quadratic.jl @@ -77,7 +77,7 @@ 1, ), ) - @test MOI.coefficient.(eq_con1_fun.terms) == [1.0; 1.0; -2.0; -1.0] + @test MOI.coefficient.(eq_con1_fun.terms) == [-1.0; -1.0; 2.0; 1.0] @test MOI.constant.(eq_con1_fun) == 0.0 @test MOI.constant(eq_con1_set) == 0.0 eq_con2_fun = MOI.get( @@ -100,8 +100,7 @@ 2, ), ) - @test MOI.coefficient.(eq_con2_fun.terms) == - [2.0; 1.0; -1.0; -1.0; -2.0] + @test MOI.coefficient.(eq_con2_fun.terms) == [-2.0; -1.0; 1.0; 1.0; 2.0] @test MOI.constant.(eq_con2_fun) == 0.0 @test MOI.constant(eq_con2_set) == 0.0 @@ -226,7 +225,7 @@ 1, ), ) - @test MOI.coefficient.(eq_con1_fun.terms) == [1.0; 1.0; -2.0; -1.0] + @test MOI.coefficient.(eq_con1_fun.terms) == [-1.0; -1.0; 2.0; 1.0] @test MOI.constant.(eq_con1_fun) == 0.0 @test MOI.constant(eq_con1_set) == 0.0 eq_con2_fun = MOI.get( @@ -249,8 +248,7 @@ 2, ), ) - @test MOI.coefficient.(eq_con2_fun.terms) == - [2.0; 1.0; -1.0; -1.0; -2.0] + @test MOI.coefficient.(eq_con2_fun.terms) == [-2.0; -1.0; 1.0; 1.0; 2.0] @test MOI.constant.(eq_con2_fun) == 0.0 @test MOI.constant(eq_con2_set) == 0.0 diff --git a/test/runtests.jl b/test/runtests.jl index efb6f59..b68c308 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -73,6 +73,7 @@ include("Tests/test_dualize_exponential.jl") include("Tests/test_dualize_power.jl") include("Tests/test_dualize_quadratic.jl") include("Tests/test_dual_names.jl") +include("Tests/test_dual_constraint_signs.jl") include("Tests/test_partial_dual_linear.jl") include("Tests/test_partial_dual_quadratic.jl")