From 846f8780e4e043b21bc232ad7f18738e00eb6c4f Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 27 Aug 2025 15:50:14 +1200 Subject: [PATCH 1/2] [Utililties] fix dual objective value with open intervals --- src/Utilities/results.jl | 42 +++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/Utilities/results.jl b/src/Utilities/results.jl index 151debdce4..21380315d5 100644 --- a/src/Utilities/results.jl +++ b/src/Utilities/results.jl @@ -88,6 +88,26 @@ function _dual_objective_value( ) 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}, @@ -97,14 +117,7 @@ function _dual_objective_value( 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) - if dual < zero(dual) - # The dual is negative so it is in the dual of the MOI.LessThan cone - # hence the upper bound of the Interval set is tight - constant -= set.upper - else - # the lower bound is tight - constant -= set.lower - end + constant = _constant_minus_bound(constant, set.lower, set.upper, dual) return set_dot(constant, dual, set) end @@ -118,17 +131,10 @@ function _dual_objective_value( 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 = map(eachindex(func_constant)) do i - return func_constant[i] - if dual[i] < zero(dual[i]) - # The dual is negative so it is in the dual of the MOI.LessThan cone - # hence the upper bound of the Interval set is tight - set.upper[i] - else - # the lower bound is tight - set.lower[i] - end + 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(constant, dual, set) + return set_dot(constants, dual, set) end function _dual_objective_value( From 1a15ce4fec5f3d8dd3bd8faaff53d3252d3b71c2 Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 27 Aug 2025 16:11:20 +1200 Subject: [PATCH 2/2] Add test --- test/Utilities/results.jl | 98 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/test/Utilities/results.jl b/test/Utilities/results.jl index 5286604f7d..43df25616d 100644 --- a/test/Utilities/results.jl +++ b/test/Utilities/results.jl @@ -51,6 +51,104 @@ function _test_hyperrectangle(T) return end +function test_dual_objective_value_open_interval_Interval() + inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) + model = MOI.Utilities.MockOptimizer(inner) + # -Inf <= x[1] - 1.1 <= Inf + # -Inf <= x[2] - 1.2 <= 2.1 + # -2.2 <= x[3] + 1.3 <= Inf + # -2.3 <= x[4] + 1.4 <= 2.4 + x = MOI.add_variables(model, 4) + f = x .+ [-1.1, -1.2, 1.3, 1.4] + set = MOI.Interval.([-Inf, -Inf, -2.2, -2.3], [Inf, 2.1, Inf, 2.4]) + c = MOI.add_constraint.(model, f, set) + for (dual, obj) in [ + [0.0, 0.0, 0.0, 0.0] => 0.0, + # d[1]: -(-1.1) = 1.1 + [-2.0, 0.0, 0.0, 0.0] => -2.2, + [-1.0, 0.0, 0.0, 0.0] => -1.1, + [1.0, 0.0, 0.0, 0.0] => 1.1, + [2.0, 0.0, 0.0, 0.0] => 2.2, + # d[2]: -(-1.2 - 2.1) = 3.3 + [0.0, -2.0, 0.0, 0.0] => -6.6, + [0.0, -1.0, 0.0, 0.0] => -3.3, + [0.0, 1.0, 0.0, 0.0] => 3.3, + [0.0, 2.0, 0.0, 0.0] => 6.6, + # d[3]: -(1.3 - -2.2) = -3.5 + [0.0, 0.0, -2.0, 0.0] => 7.0, + [0.0, 0.0, -1.0, 0.0] => 3.5, + [0.0, 0.0, 1.0, 0.0] => -3.5, + [0.0, 0.0, 2.0, 0.0] => -7.0, + # d[4]: -(1.4 - -2.3) = -3.7 + # d[4]: -(1.4 - 2.4) = 1.0 + [0.0, 0.0, 0.0, -2.0] => -2.0, + [0.0, 0.0, 0.0, -1.0] => -1.0, + [0.0, 0.0, 0.0, 1.0] => -3.7, + [0.0, 0.0, 0.0, 2.0] => -7.4, + # + [1.0, 1.0, 1.0, 1.0] => -2.8, + [-1.0, -1.0, -1.0, -1.0] => -1.9, + ] + 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_Hyperrectangle() + inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) + model = MOI.Utilities.MockOptimizer(inner) + # -Inf <= x[1] - 1.1 <= Inf + # -Inf <= x[2] - 1.2 <= 2.1 + # -2.2 <= x[3] + 1.3 <= Inf + # -2.3 <= x[4] + 1.4 <= 2.4 + x = MOI.add_variables(model, 4) + f = MOI.Utilities.vectorize(x .+ [-1.1, -1.2, 1.3, 1.4]) + set = MOI.HyperRectangle([-Inf, -Inf, -2.2, -2.3], [Inf, 2.1, Inf, 2.4]) + c = MOI.add_constraint(model, f, set) + for (dual, obj) in [ + [0.0, 0.0, 0.0, 0.0] => 0.0, + # d[1]: -(-1.1) = 1.1 + [-2.0, 0.0, 0.0, 0.0] => -2.2, + [-1.0, 0.0, 0.0, 0.0] => -1.1, + [1.0, 0.0, 0.0, 0.0] => 1.1, + [2.0, 0.0, 0.0, 0.0] => 2.2, + # d[2]: -(-1.2 - 2.1) = 3.3 + [0.0, -2.0, 0.0, 0.0] => -6.6, + [0.0, -1.0, 0.0, 0.0] => -3.3, + [0.0, 1.0, 0.0, 0.0] => 3.3, + [0.0, 2.0, 0.0, 0.0] => 6.6, + # d[3]: -(1.3 - -2.2) = -3.5 + [0.0, 0.0, -2.0, 0.0] => 7.0, + [0.0, 0.0, -1.0, 0.0] => 3.5, + [0.0, 0.0, 1.0, 0.0] => -3.5, + [0.0, 0.0, 2.0, 0.0] => -7.0, + # d[4]: -(1.4 - -2.3) = -3.7 + # d[4]: -(1.4 - 2.4) = 1.0 + [0.0, 0.0, 0.0, -2.0] => -2.0, + [0.0, 0.0, 0.0, -1.0] => -1.0, + [0.0, 0.0, 0.0, 1.0] => -3.7, + [0.0, 0.0, 0.0, 2.0] => -7.4, + # + [1.0, 1.0, 1.0, 1.0] => -2.8, + [-1.0, -1.0, -1.0, -1.0] => -1.9, + ] + 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 + end # module TestResults TestResults.runtests()