Skip to content

Commit 2553c6c

Browse files
blegatodow
authored andcommitted
Test ConstraintDual for bridges
1 parent 6479b1f commit 2553c6c

File tree

5 files changed

+92
-14
lines changed

5 files changed

+92
-14
lines changed

docs/src/submodules/Bridges/implementation.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ julia> MOI.Bridges.runtests(
7979
""",
8080
)
8181
Test Summary: | Pass Total Time
82-
Bridges.runtests | 29 29 0.0s
82+
Bridges.runtests | 30 30 0.0s
8383
```
8484

8585
There are a number of other useful keyword arguments.
@@ -123,5 +123,5 @@ Subject to:
123123
ScalarAffineFunction{Int64}-in-LessThan{Int64}
124124
(0) - (1) x <= (-1)
125125
Test Summary: | Pass Total Time
126-
Bridges.runtests | 29 29 0.0s
126+
Bridges.runtests | 30 30 0.0s
127127
```

src/Bridges/Bridges.jl

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ julia> MOI.Bridges.runtests(
283283
end,
284284
)
285285
Test Summary: | Pass Total Time
286-
Bridges.runtests | 32 32 0.8s
286+
Bridges.runtests | 33 33 0.8s
287287
```
288288
"""
289289
function runtests(args...; kwargs...)
@@ -293,12 +293,60 @@ function runtests(args...; kwargs...)
293293
return
294294
end
295295

296+
# A good way to check that the linear mapping implemented in the setter of
297+
# `ConstraintDual` is the inverse-adjoint of the mapping implemented in the
298+
# constraint transformation is to check `get_fallback` for `DualObjectiveValue`.
299+
# Indeed, it will check that the inner product between the constraint constants
300+
# and the dual is the same before and after the bridge transformations.
301+
# For this test to be enabled, the bridge should implement `supports`
302+
# for `ConstraintDual` and implement `MOI.set` for `ConstraintDual`.
303+
# Typically, this would be achieved using
304+
# `Union{ConstraintDual,ConstraintDualStart}` for `MOI.get`, `MOI.set` and
305+
# `MOI.supports`
306+
function _test_dual(
307+
Bridge::Type{<:AbstractBridge},
308+
input_fn::Function;
309+
dual,
310+
eltype,
311+
model_eltype,
312+
)
313+
inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{model_eltype}())
314+
mock = MOI.Utilities.MockOptimizer(inner)
315+
model = _bridged_model(Bridge{eltype}, mock)
316+
input_fn(model)
317+
final_touch(model)
318+
# Should be able to call final_touch multiple times.
319+
final_touch(model)
320+
# If the bridges does not support `ConstraintDualStart`, it probably won't
321+
# support `ConstraintDual` so we skip these tests
322+
list_of_constraints = MOI.get(model, MOI.ListOfConstraintTypesPresent())
323+
attr = MOI.ConstraintDual()
324+
for (F, S) in list_of_constraints
325+
if !MOI.supports(model, attr, MOI.ConstraintIndex{F,S})
326+
# We need all duals for `DualObjectiveValue` fallback
327+
# TODO except the ones with no constants, we could ignore them
328+
return
329+
end
330+
for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
331+
set = MOI.get(model, MOI.ConstraintSet(), ci)
332+
MOI.set(model, MOI.ConstraintDual(), ci, _fake_start(dual, set))
333+
end
334+
end
335+
model_dual =
336+
MOI.Utilities.get_fallback(model, MOI.DualObjectiveValue(), eltype)
337+
mock_dual =
338+
MOI.Utilities.get_fallback(mock, MOI.DualObjectiveValue(), eltype)
339+
# Need `atol` in case one of them is zero and the other one almost zero
340+
Test.@test model_dual mock_dual atol = 1e-6
341+
end
342+
296343
function _runtests(
297344
Bridge::Type{<:AbstractBridge},
298345
input_fn::Function,
299346
output_fn::Function;
300347
variable_start = 1.2,
301348
constraint_start = 1.2,
349+
dual = constraint_start,
302350
eltype = Float64,
303351
model_eltype = eltype,
304352
print_inner_model::Bool = false,
@@ -403,6 +451,11 @@ function _runtests(
403451
Test.@testset "Test delete" begin # COV_EXCL_LINE
404452
_test_delete(Bridge, model, inner)
405453
end
454+
if !isnothing(dual)
455+
Test.@testset "Test ConstraintDual" begin
456+
_test_dual(Bridge, input_fn; dual, eltype, model_eltype)
457+
end
458+
end
406459
return
407460
end
408461

src/Bridges/Constraint/bridges/SplitHyperRectangleBridge.jl

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,11 @@ end
196196

197197
function MOI.supports(
198198
model::MOI.ModelLike,
199-
attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart},
199+
attr::Union{
200+
MOI.ConstraintPrimalStart,
201+
MOI.ConstraintDualStart,
202+
MOI.ConstraintDual,
203+
},
200204
::Type{<:SplitHyperRectangleBridge{T,G}},
201205
) where {T,G}
202206
return MOI.supports(model, attr, MOI.ConstraintIndex{G,MOI.Nonnegatives})
@@ -284,7 +288,7 @@ end
284288

285289
function MOI.set(
286290
model::MOI.ModelLike,
287-
attr::MOI.ConstraintDualStart,
291+
attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual},
288292
bridge::SplitHyperRectangleBridge{T},
289293
values::AbstractVector{T},
290294
) where {T}

src/Utilities/results.jl

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,17 @@ function _dual_objective_value(
8888
)
8989
end
9090

91+
_lower(set::MOI.Interval, ::Type) = set.lower
92+
_upper(set::MOI.Interval, ::Type) = set.upper
93+
_lower(::MOI.ZeroOne, ::Type{T}) where {T} = zero(T)
94+
_upper(::MOI.ZeroOne, ::Type{T}) where {T} = one(T)
95+
9196
function _dual_objective_value(
9297
model::MOI.ModelLike,
93-
ci::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction,<:MOI.Interval},
98+
ci::MOI.ConstraintIndex{
99+
<:MOI.AbstractScalarFunction,
100+
<:Union{MOI.ZeroOne,MOI.Interval},
101+
},
94102
::Type{T},
95103
result_index::Integer,
96104
) where {T}
@@ -100,10 +108,10 @@ function _dual_objective_value(
100108
if dual < zero(dual)
101109
# The dual is negative so it is in the dual of the MOI.LessThan cone
102110
# hence the upper bound of the Interval set is tight
103-
constant -= set.upper
111+
constant -= _upper(set, T)
104112
else
105113
# the lower bound is tight
106-
constant -= set.lower
114+
constant -= _lower(set, T)
107115
end
108116
return set_dot(constant, dual, set)
109117
end
@@ -119,14 +127,26 @@ function _dual_objective_value(
119127
set = MOI.get(model, MOI.ConstraintSet(), ci)
120128
dual = MOI.get(model, MOI.ConstraintDual(result_index), ci)
121129
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]
130+
constant = func_constant[i]
131+
if isfinite(set.upper[i])
132+
if isfinite(set.lower[i])
133+
if dual[i] < zero(dual[i])
134+
# The dual is negative so it is in the dual of the MOI.LessThan cone
135+
# hence the upper bound of the Interval set is tight
136+
constant -= set.upper[i]
137+
else
138+
# the lower bound is tight
139+
constant -= set.lower[i]
140+
end
141+
else
142+
constant -= set.upper[i]
143+
end
126144
else
127-
# the lower bound is tight
128-
set.lower[i]
145+
if isfinite(set.lower[i])
146+
constant -= set.lower[i]
147+
end
129148
end
149+
return constant
130150
end
131151
return set_dot(constant, dual, set)
132152
end

test/Bridges/Constraint/ScalarFunctionizeBridge.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ function test_FunctionConversionBridge()
317317
variables: x, y
318318
ScalarNonlinearFunction(1.0 * x * x + 2.0 * x * y + 3.0 * y + 4.0) >= 1.0
319319
""",
320+
dual = nothing, # `get_fallback` ignores the constant `4.0` of the function
320321
)
321322
# VectorAffineFunction -> VectorQuadraticFunction
322323
MOI.Bridges.runtests(

0 commit comments

Comments
 (0)