Skip to content

Commit 4a2641b

Browse files
authored
[Utilities] fix dual objective value with open intervals (#2823)
1 parent 8dad6b5 commit 4a2641b

File tree

2 files changed

+122
-18
lines changed

2 files changed

+122
-18
lines changed

src/Utilities/results.jl

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,26 @@ function _dual_objective_value(
8888
)
8989
end
9090

91+
"""
92+
Given lower <= f(x) <= upper [dual], return the expression to be multiplied by
93+
the dual variable. This is one of the following cases:
94+
95+
1. f(x) - lower: if `lower > -Inf` and the lower bound is binding (either no
96+
`upper` or `dual > 0`)
97+
2. f(x) - upper: if `upper < Inf` and the upper bound is binding (either no
98+
`lower` or `dual < 0`)
99+
3. f(x): if `lower = -Inf` and `upper = Inf` or `dual = 0`
100+
"""
101+
function _constant_minus_bound(constant, lower, upper, dual)
102+
if isfinite(lower) && (!isfinite(upper) || dual > zero(dual))
103+
return constant - lower
104+
elseif isfinite(upper) && (!isfinite(lower) || dual < zero(dual))
105+
return constant - upper
106+
else
107+
return constant
108+
end
109+
end
110+
91111
function _dual_objective_value(
92112
model::MOI.ModelLike,
93113
ci::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction,<:MOI.Interval},
@@ -97,14 +117,7 @@ function _dual_objective_value(
97117
constant = MOI.constant(MOI.get(model, MOI.ConstraintFunction(), ci), T)
98118
set = MOI.get(model, MOI.ConstraintSet(), ci)
99119
dual = MOI.get(model, MOI.ConstraintDual(result_index), ci)
100-
if dual < zero(dual)
101-
# The dual is negative so it is in the dual of the MOI.LessThan cone
102-
# hence the upper bound of the Interval set is tight
103-
constant -= set.upper
104-
else
105-
# the lower bound is tight
106-
constant -= set.lower
107-
end
120+
constant = _constant_minus_bound(constant, set.lower, set.upper, dual)
108121
return set_dot(constant, dual, set)
109122
end
110123

@@ -118,17 +131,10 @@ function _dual_objective_value(
118131
MOI.constant(MOI.get(model, MOI.ConstraintFunction(), ci), T)
119132
set = MOI.get(model, MOI.ConstraintSet(), ci)
120133
dual = MOI.get(model, MOI.ConstraintDual(result_index), ci)
121-
constant = map(eachindex(func_constant)) do i
122-
return func_constant[i] - if dual[i] < zero(dual[i])
123-
# The dual is negative so it is in the dual of the MOI.LessThan cone
124-
# hence the upper bound of the Interval set is tight
125-
set.upper[i]
126-
else
127-
# the lower bound is tight
128-
set.lower[i]
129-
end
134+
constants = map(enumerate(func_constant)) do (i, c)
135+
return _constant_minus_bound(c, set.lower[i], set.upper[i], dual[i])
130136
end
131-
return set_dot(constant, dual, set)
137+
return set_dot(constants, dual, set)
132138
end
133139

134140
function _dual_objective_value(

test/Utilities/results.jl

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,104 @@ function _test_hyperrectangle(T)
5151
return
5252
end
5353

54+
function test_dual_objective_value_open_interval_Interval()
55+
inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
56+
model = MOI.Utilities.MockOptimizer(inner)
57+
# -Inf <= x[1] - 1.1 <= Inf
58+
# -Inf <= x[2] - 1.2 <= 2.1
59+
# -2.2 <= x[3] + 1.3 <= Inf
60+
# -2.3 <= x[4] + 1.4 <= 2.4
61+
x = MOI.add_variables(model, 4)
62+
f = x .+ [-1.1, -1.2, 1.3, 1.4]
63+
set = MOI.Interval.([-Inf, -Inf, -2.2, -2.3], [Inf, 2.1, Inf, 2.4])
64+
c = MOI.add_constraint.(model, f, set)
65+
for (dual, obj) in [
66+
[0.0, 0.0, 0.0, 0.0] => 0.0,
67+
# d[1]: -(-1.1) = 1.1
68+
[-2.0, 0.0, 0.0, 0.0] => -2.2,
69+
[-1.0, 0.0, 0.0, 0.0] => -1.1,
70+
[1.0, 0.0, 0.0, 0.0] => 1.1,
71+
[2.0, 0.0, 0.0, 0.0] => 2.2,
72+
# d[2]: -(-1.2 - 2.1) = 3.3
73+
[0.0, -2.0, 0.0, 0.0] => -6.6,
74+
[0.0, -1.0, 0.0, 0.0] => -3.3,
75+
[0.0, 1.0, 0.0, 0.0] => 3.3,
76+
[0.0, 2.0, 0.0, 0.0] => 6.6,
77+
# d[3]: -(1.3 - -2.2) = -3.5
78+
[0.0, 0.0, -2.0, 0.0] => 7.0,
79+
[0.0, 0.0, -1.0, 0.0] => 3.5,
80+
[0.0, 0.0, 1.0, 0.0] => -3.5,
81+
[0.0, 0.0, 2.0, 0.0] => -7.0,
82+
# d[4]: -(1.4 - -2.3) = -3.7
83+
# d[4]: -(1.4 - 2.4) = 1.0
84+
[0.0, 0.0, 0.0, -2.0] => -2.0,
85+
[0.0, 0.0, 0.0, -1.0] => -1.0,
86+
[0.0, 0.0, 0.0, 1.0] => -3.7,
87+
[0.0, 0.0, 0.0, 2.0] => -7.4,
88+
#
89+
[1.0, 1.0, 1.0, 1.0] => -2.8,
90+
[-1.0, -1.0, -1.0, -1.0] => -1.9,
91+
]
92+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
93+
MOI.set.(model, MOI.ConstraintDual(), c, dual)
94+
d = MOI.Utilities.get_fallback(model, MOI.DualObjectiveValue(), Float64)
95+
@test isapprox(d, obj)
96+
MOI.set.(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
97+
d = MOI.Utilities.get_fallback(model, MOI.DualObjectiveValue(), Float64)
98+
@test isapprox(d, -obj)
99+
end
100+
return
101+
end
102+
103+
function test_dual_objective_value_open_interval_Hyperrectangle()
104+
inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}())
105+
model = MOI.Utilities.MockOptimizer(inner)
106+
# -Inf <= x[1] - 1.1 <= Inf
107+
# -Inf <= x[2] - 1.2 <= 2.1
108+
# -2.2 <= x[3] + 1.3 <= Inf
109+
# -2.3 <= x[4] + 1.4 <= 2.4
110+
x = MOI.add_variables(model, 4)
111+
f = MOI.Utilities.vectorize(x .+ [-1.1, -1.2, 1.3, 1.4])
112+
set = MOI.HyperRectangle([-Inf, -Inf, -2.2, -2.3], [Inf, 2.1, Inf, 2.4])
113+
c = MOI.add_constraint(model, f, set)
114+
for (dual, obj) in [
115+
[0.0, 0.0, 0.0, 0.0] => 0.0,
116+
# d[1]: -(-1.1) = 1.1
117+
[-2.0, 0.0, 0.0, 0.0] => -2.2,
118+
[-1.0, 0.0, 0.0, 0.0] => -1.1,
119+
[1.0, 0.0, 0.0, 0.0] => 1.1,
120+
[2.0, 0.0, 0.0, 0.0] => 2.2,
121+
# d[2]: -(-1.2 - 2.1) = 3.3
122+
[0.0, -2.0, 0.0, 0.0] => -6.6,
123+
[0.0, -1.0, 0.0, 0.0] => -3.3,
124+
[0.0, 1.0, 0.0, 0.0] => 3.3,
125+
[0.0, 2.0, 0.0, 0.0] => 6.6,
126+
# d[3]: -(1.3 - -2.2) = -3.5
127+
[0.0, 0.0, -2.0, 0.0] => 7.0,
128+
[0.0, 0.0, -1.0, 0.0] => 3.5,
129+
[0.0, 0.0, 1.0, 0.0] => -3.5,
130+
[0.0, 0.0, 2.0, 0.0] => -7.0,
131+
# d[4]: -(1.4 - -2.3) = -3.7
132+
# d[4]: -(1.4 - 2.4) = 1.0
133+
[0.0, 0.0, 0.0, -2.0] => -2.0,
134+
[0.0, 0.0, 0.0, -1.0] => -1.0,
135+
[0.0, 0.0, 0.0, 1.0] => -3.7,
136+
[0.0, 0.0, 0.0, 2.0] => -7.4,
137+
#
138+
[1.0, 1.0, 1.0, 1.0] => -2.8,
139+
[-1.0, -1.0, -1.0, -1.0] => -1.9,
140+
]
141+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
142+
MOI.set(model, MOI.ConstraintDual(), c, dual)
143+
d = MOI.Utilities.get_fallback(model, MOI.DualObjectiveValue(), Float64)
144+
@test isapprox(d, obj)
145+
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
146+
d = MOI.Utilities.get_fallback(model, MOI.DualObjectiveValue(), Float64)
147+
@test isapprox(d, -obj)
148+
end
149+
return
150+
end
151+
54152
end # module TestResults
55153

56154
TestResults.runtests()

0 commit comments

Comments
 (0)