diff --git a/src/Utilities/results.jl b/src/Utilities/results.jl index 21380315d5..b6cb7602e8 100644 --- a/src/Utilities/results.jl +++ b/src/Utilities/results.jl @@ -52,112 +52,6 @@ end # MOI.DualObjectiveValue -function _constraint_constant( - model::MOI.ModelLike, - ci::MOI.ConstraintIndex{ - <:MOI.AbstractVectorFunction, - <:MOI.AbstractVectorSet, - }, - ::Type{T}, -) where {T} - return MOI.constant(MOI.get(model, MOI.ConstraintFunction(), ci), T) -end - -function _constraint_constant( - model::MOI.ModelLike, - ci::MOI.ConstraintIndex{ - <:MOI.AbstractScalarFunction, - <:MOI.AbstractScalarSet, - }, - ::Type{T}, -) where {T} - return MOI.constant(MOI.get(model, MOI.ConstraintFunction(), ci), T) - - MOI.constant(MOI.get(model, MOI.ConstraintSet(), ci)) -end - -function _dual_objective_value( - model::MOI.ModelLike, - ci::MOI.ConstraintIndex, - ::Type{T}, - result_index::Integer, -) where {T} - return set_dot( - _constraint_constant(model, ci, T), - MOI.get(model, MOI.ConstraintDual(result_index), ci), - MOI.get(model, MOI.ConstraintSet(), ci), - ) -end - -""" -Given lower <= f(x) <= upper [dual], return the expression to be multiplied by -the dual variable. This is one of the following cases: - - 1. f(x) - lower: if `lower > -Inf` and the lower bound is binding (either no - `upper` or `dual > 0`) - 2. f(x) - upper: if `upper < Inf` and the upper bound is binding (either no - `lower` or `dual < 0`) - 3. f(x): if `lower = -Inf` and `upper = Inf` or `dual = 0` -""" -function _constant_minus_bound(constant, lower, upper, dual) - if isfinite(lower) && (!isfinite(upper) || dual > zero(dual)) - return constant - lower - elseif isfinite(upper) && (!isfinite(lower) || dual < zero(dual)) - return constant - upper - else - return constant - end -end - -function _dual_objective_value( - model::MOI.ModelLike, - ci::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction,<:MOI.Interval}, - ::Type{T}, - result_index::Integer, -) where {T} - constant = MOI.constant(MOI.get(model, MOI.ConstraintFunction(), ci), T) - set = MOI.get(model, MOI.ConstraintSet(), ci) - dual = MOI.get(model, MOI.ConstraintDual(result_index), ci) - constant = _constant_minus_bound(constant, set.lower, set.upper, dual) - return set_dot(constant, dual, set) -end - -function _dual_objective_value( - model::MOI.ModelLike, - ci::MOI.ConstraintIndex{<:MOI.AbstractVectorFunction,<:MOI.HyperRectangle}, - ::Type{T}, - result_index::Integer, -) where {T} - func_constant = - MOI.constant(MOI.get(model, MOI.ConstraintFunction(), ci), T) - set = MOI.get(model, MOI.ConstraintSet(), ci) - dual = MOI.get(model, MOI.ConstraintDual(result_index), ci) - constants = map(enumerate(func_constant)) do (i, c) - return _constant_minus_bound(c, set.lower[i], set.upper[i], dual[i]) - end - return set_dot(constants, dual, set) -end - -function _dual_objective_value( - model::MOI.ModelLike, - ::Type{F}, - ::Type{S}, - ::Type{T}, - result_index::Integer, -) where {T,F<:MOI.AbstractFunction,S<:MOI.AbstractSet} - value = zero(T) - if F == variable_function_type(S) && !_has_constant(S) - return value # Shortcut - end - for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) - value += _dual_objective_value(model, ci, T, result_index) - end - return value -end - -_has_constant(::Type{<:MOI.AbstractScalarSet}) = true -_has_constant(::Type{<:MOI.AbstractVectorSet}) = false -_has_constant(::Type{<:MOI.HyperRectangle}) = true - """ get_fallback( model::MOI.ModelLike, @@ -192,6 +86,72 @@ function get_fallback( return value::T end +function _dual_objective_value( + model::MOI.ModelLike, + ::Type{F}, + ::Type{S}, + ::Type{T}, + result_index::Integer, +)::T where {T,F<:MOI.AbstractFunction,S<:MOI.AbstractSet} + value = zero(T) + if F == variable_function_type(S) && !_variable_set_in_dual_objective(S) + # Early return. This is a constraint like x in R_+, so no contribution + # appears in the dual objective. + return value + end + for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) + constant = MOI.constant(MOI.get(model, MOI.ConstraintFunction(), ci), T) + set = MOI.get(model, MOI.ConstraintSet(), ci) + dual = MOI.get(model, MOI.ConstraintDual(result_index), ci) + value += _dual_objective_dot(constant, dual, set) + end + return value +end + +_variable_set_in_dual_objective(::Type{<:MOI.AbstractSet}) = false + +_variable_set_in_dual_objective(::Type{<:MOI.EqualTo}) = true + +_variable_set_in_dual_objective(::Type{<:MOI.GreaterThan}) = true + +_variable_set_in_dual_objective(::Type{<:MOI.LessThan}) = true + +_variable_set_in_dual_objective(::Type{<:MOI.Interval}) = true + +_variable_set_in_dual_objective(::Type{<:MOI.HyperRectangle}) = true + +_dual_objective_dot(x, y, set) = set_dot(x, y, set) + +_dual_objective_dot(x, y, set::MOI.EqualTo) = (x - set.value) * y + +_dual_objective_dot(x, y, set::MOI.LessThan) = (x - set.upper) * y + +_dual_objective_dot(x, y, set::MOI.GreaterThan) = (x - set.lower) * y + +function _dual_objective_dot(x, y, set::MOI.Interval) + if isfinite(set.lower) && (!isfinite(set.upper) || y > zero(y)) + return (x - set.lower) * y + elseif isfinite(set.upper) && (!isfinite(set.lower) || y < zero(y)) + return (x - set.upper) * y + end + return x * y +end + +function _dual_objective_dot(x, y, set::MOI.HyperRectangle) + @assert length(x) == length(y) == MOI.dimension(set) + ret = zero(eltype(x)) + for (xi, yi, li, ui) in zip(x, y, set.lower, set.upper) + if isfinite(li) && (!isfinite(ui) || yi > zero(yi)) + ret += (xi - li) * yi + elseif isfinite(ui) && (!isfinite(li) || yi < zero(yi)) + ret += (xi - ui) * yi + else + ret += xi * yi + end + end + return ret +end + # MOI.ConstraintPrimal """ diff --git a/test/Bridges/Constraint/NormSpectralBridge.jl b/test/Bridges/Constraint/NormSpectralBridge.jl index 02b3f13659..8bf0f946d1 100644 --- a/test/Bridges/Constraint/NormSpectralBridge.jl +++ b/test/Bridges/Constraint/NormSpectralBridge.jl @@ -197,7 +197,7 @@ function test_NormNuclear() mock, var_primal, (MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}) => - [[1.0]], + [1.0], ( MOI.VectorAffineFunction{Float64}, MOI.PositiveSemidefiniteConeTriangle, diff --git a/test/Utilities/results.jl b/test/Utilities/results.jl index 43df25616d..12ca4c8793 100644 --- a/test/Utilities/results.jl +++ b/test/Utilities/results.jl @@ -51,6 +51,57 @@ function _test_hyperrectangle(T) return end +function test_dual_objective_value_open_interval_Interval_variable_index() + inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) + model = MOI.Utilities.MockOptimizer( + inner; + eval_variable_constraint_dual = false, + ) + # -Inf <= x[1] <= Inf + # -Inf <= x[2] <= 2.1 + # -2.2 <= x[3] <= Inf + # -2.3 <= x[4] <= 2.4 + x = MOI.add_variables(model, 4) + set = MOI.Interval.([-Inf, -Inf, -2.2, -2.3], [Inf, 2.1, Inf, 2.4]) + c = MOI.add_constraint.(model, x, set) + for (dual, obj) in [ + [0.0, 0.0, 0.0, 0.0] => 0.0, + # d[1] + [-2.0, 0.0, 0.0, 0.0] => 0.0, + [-1.0, 0.0, 0.0, 0.0] => 0.0, + [1.0, 0.0, 0.0, 0.0] => 0.0, + [2.0, 0.0, 0.0, 0.0] => 0.0, + # d[2]: -(-2.1) = 2.1 + [0.0, -2.0, 0.0, 0.0] => -4.2, + [0.0, -1.0, 0.0, 0.0] => -2.1, + [0.0, 1.0, 0.0, 0.0] => 2.1, + [0.0, 2.0, 0.0, 0.0] => 4.2, + # d[3]: -(- -2.2) = -2.2 + [0.0, 0.0, -2.0, 0.0] => 4.4, + [0.0, 0.0, -1.0, 0.0] => 2.2, + [0.0, 0.0, 1.0, 0.0] => -2.2, + [0.0, 0.0, 2.0, 0.0] => -4.4, + # d[4]: -(- -2.3) = -2.3 + # d[4]: -(- 2.4) = 2.4 + [0.0, 0.0, 0.0, -2.0] => -4.8, + [0.0, 0.0, 0.0, -1.0] => -2.4, + [0.0, 0.0, 0.0, 1.0] => -2.3, + [0.0, 0.0, 0.0, 2.0] => -4.6, + # + [1.0, 1.0, 1.0, 1.0] => -2.4, + [-1.0, -1.0, -1.0, -1.0] => -2.3, + ] + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set.(model, MOI.ConstraintDual(), c, dual) + d = MOI.Utilities.get_fallback(model, MOI.DualObjectiveValue(), Float64) + @test isapprox(d, obj) + MOI.set.(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) + d = MOI.Utilities.get_fallback(model, MOI.DualObjectiveValue(), Float64) + @test isapprox(d, -obj) + end + return +end + function test_dual_objective_value_open_interval_Interval() inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) model = MOI.Utilities.MockOptimizer(inner) @@ -93,7 +144,58 @@ function test_dual_objective_value_open_interval_Interval() MOI.set.(model, MOI.ConstraintDual(), c, dual) d = MOI.Utilities.get_fallback(model, MOI.DualObjectiveValue(), Float64) @test isapprox(d, obj) - MOI.set.(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) + MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) + d = MOI.Utilities.get_fallback(model, MOI.DualObjectiveValue(), Float64) + @test isapprox(d, -obj) + end + return +end + +function test_dual_objective_value_open_interval_Hyperrectangle_variable_index() + inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) + model = MOI.Utilities.MockOptimizer( + inner; + eval_variable_constraint_dual = false, + ) + # -Inf <= x[1] <= Inf + # -Inf <= x[2] <= 2.1 + # -2.2 <= x[3] <= Inf + # -2.3 <= x[4] <= 2.4 + x = MOI.add_variables(model, 4) + set = MOI.HyperRectangle([-Inf, -Inf, -2.2, -2.3], [Inf, 2.1, Inf, 2.4]) + c = MOI.add_constraint(model, MOI.VectorOfVariables(x), set) + for (dual, obj) in [ + [0.0, 0.0, 0.0, 0.0] => 0.0, + # d[1] + [-2.0, 0.0, 0.0, 0.0] => 0.0, + [-1.0, 0.0, 0.0, 0.0] => 0.0, + [1.0, 0.0, 0.0, 0.0] => 0.0, + [2.0, 0.0, 0.0, 0.0] => 0.0, + # d[2]: -(-2.1) = 2.1 + [0.0, -2.0, 0.0, 0.0] => -4.2, + [0.0, -1.0, 0.0, 0.0] => -2.1, + [0.0, 1.0, 0.0, 0.0] => 2.1, + [0.0, 2.0, 0.0, 0.0] => 4.2, + # d[3]: -(- -2.2) = -2.2 + [0.0, 0.0, -2.0, 0.0] => 4.4, + [0.0, 0.0, -1.0, 0.0] => 2.2, + [0.0, 0.0, 1.0, 0.0] => -2.2, + [0.0, 0.0, 2.0, 0.0] => -4.4, + # d[4]: -(- -2.3) = -2.3 + # d[4]: -(- 2.4) = 2.4 + [0.0, 0.0, 0.0, -2.0] => -4.8, + [0.0, 0.0, 0.0, -1.0] => -2.4, + [0.0, 0.0, 0.0, 1.0] => -2.3, + [0.0, 0.0, 0.0, 2.0] => -4.6, + # + [1.0, 1.0, 1.0, 1.0] => -2.4, + [-1.0, -1.0, -1.0, -1.0] => -2.3, + ] + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.ConstraintDual(), c, dual) + d = MOI.Utilities.get_fallback(model, MOI.DualObjectiveValue(), Float64) + @test isapprox(d, obj) + MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) d = MOI.Utilities.get_fallback(model, MOI.DualObjectiveValue(), Float64) @test isapprox(d, -obj) end