From b4d698773b7e98b793fc4cf5e4541c96f861767a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 12 Aug 2025 10:05:47 +0200 Subject: [PATCH] Test ConstraintDual for bridges --- docs/src/submodules/Bridges/implementation.md | 4 +- src/Bridges/Bridges.jl | 55 ++++++++++++++++++- .../bridges/SplitHyperRectangleBridge.jl | 21 +++++-- .../Constraint/ScalarFunctionizeBridge.jl | 1 + 4 files changed, 74 insertions(+), 7 deletions(-) diff --git a/docs/src/submodules/Bridges/implementation.md b/docs/src/submodules/Bridges/implementation.md index ae821f0087..2a7d278963 100644 --- a/docs/src/submodules/Bridges/implementation.md +++ b/docs/src/submodules/Bridges/implementation.md @@ -79,7 +79,7 @@ julia> MOI.Bridges.runtests( """, ) Test Summary: | Pass Total Time -Bridges.runtests | 29 29 0.0s +Bridges.runtests | 30 30 0.0s ``` There are a number of other useful keyword arguments. @@ -123,5 +123,5 @@ Subject to: ScalarAffineFunction{Int64}-in-LessThan{Int64} (0) - (1) x <= (-1) Test Summary: | Pass Total Time -Bridges.runtests | 29 29 0.0s +Bridges.runtests | 30 30 0.0s ``` diff --git a/src/Bridges/Bridges.jl b/src/Bridges/Bridges.jl index f7c67308aa..e03ab6c0e8 100644 --- a/src/Bridges/Bridges.jl +++ b/src/Bridges/Bridges.jl @@ -283,7 +283,7 @@ julia> MOI.Bridges.runtests( end, ) Test Summary: | Pass Total Time -Bridges.runtests | 32 32 0.8s +Bridges.runtests | 33 33 0.8s ``` """ function runtests(args...; kwargs...) @@ -293,12 +293,60 @@ function runtests(args...; kwargs...) return end +# A good way to check that the linear mapping implemented in the setter of +# `ConstraintDual` is the inverse-adjoint of the mapping implemented in the +# constraint transformation is to check `get_fallback` for `DualObjectiveValue`. +# Indeed, it will check that the inner product between the constraint constants +# and the dual is the same before and after the bridge transformations. +# For this test to be enabled, the bridge should implement `supports` +# for `ConstraintDual` and implement `MOI.set` for `ConstraintDual`. +# Typically, this would be achieved using +# `Union{ConstraintDual,ConstraintDualStart}` for `MOI.get`, `MOI.set` and +# `MOI.supports` +function _test_dual( + Bridge::Type{<:AbstractBridge}, + input_fn::Function; + dual, + eltype, + model_eltype, +) + inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{model_eltype}()) + mock = MOI.Utilities.MockOptimizer(inner) + model = _bridged_model(Bridge{eltype}, mock) + input_fn(model) + final_touch(model) + # Should be able to call final_touch multiple times. + final_touch(model) + # If the bridges does not support `ConstraintDualStart`, it probably won't + # support `ConstraintDual` so we skip these tests + list_of_constraints = MOI.get(model, MOI.ListOfConstraintTypesPresent()) + attr = MOI.ConstraintDual() + for (F, S) in list_of_constraints + if !MOI.supports(model, attr, MOI.ConstraintIndex{F,S}) + # We need all duals for `DualObjectiveValue` fallback + # TODO except the ones with no constants, we could ignore them + return + end + for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) + set = MOI.get(model, MOI.ConstraintSet(), ci) + MOI.set(model, MOI.ConstraintDual(), ci, _fake_start(dual, set)) + end + end + model_dual = + MOI.Utilities.get_fallback(model, MOI.DualObjectiveValue(), eltype) + mock_dual = + MOI.Utilities.get_fallback(mock, MOI.DualObjectiveValue(), eltype) + # Need `atol` in case one of them is zero and the other one almost zero + Test.@test model_dual ≈ mock_dual atol = 1e-6 +end + function _runtests( Bridge::Type{<:AbstractBridge}, input_fn::Function, output_fn::Function; variable_start = 1.2, constraint_start = 1.2, + dual = constraint_start, eltype = Float64, model_eltype = eltype, print_inner_model::Bool = false, @@ -403,6 +451,11 @@ function _runtests( Test.@testset "Test delete" begin # COV_EXCL_LINE _test_delete(Bridge, model, inner) end + if !isnothing(dual) + Test.@testset "Test ConstraintDual" begin + _test_dual(Bridge, input_fn; dual, eltype, model_eltype) + end + end return end diff --git a/src/Bridges/Constraint/bridges/SplitHyperRectangleBridge.jl b/src/Bridges/Constraint/bridges/SplitHyperRectangleBridge.jl index 09a0eb4387..d66aba626c 100644 --- a/src/Bridges/Constraint/bridges/SplitHyperRectangleBridge.jl +++ b/src/Bridges/Constraint/bridges/SplitHyperRectangleBridge.jl @@ -196,15 +196,28 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{<:SplitHyperRectangleBridge{T,G}}, ) where {T,G} return MOI.supports(model, attr, MOI.ConstraintIndex{G,MOI.Nonnegatives}) end -_get_free_start(bridge, ::MOI.ConstraintDualStart) = bridge.free_dual_start +function _get_free_start( + bridge, + ::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, +) + return bridge.free_dual_start +end -function _set_free_start(bridge, ::MOI.ConstraintDualStart, value) +function _set_free_start( + bridge, + ::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, + value, +) bridge.free_dual_start = value return end @@ -284,7 +297,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::SplitHyperRectangleBridge{T}, values::AbstractVector{T}, ) where {T} diff --git a/test/Bridges/Constraint/ScalarFunctionizeBridge.jl b/test/Bridges/Constraint/ScalarFunctionizeBridge.jl index 6f5e5fbf7a..26635ba5f6 100644 --- a/test/Bridges/Constraint/ScalarFunctionizeBridge.jl +++ b/test/Bridges/Constraint/ScalarFunctionizeBridge.jl @@ -317,6 +317,7 @@ function test_FunctionConversionBridge() variables: x, y ScalarNonlinearFunction(1.0 * x * x + 2.0 * x * y + 3.0 * y + 4.0) >= 1.0 """, + dual = nothing, # `get_fallback` ignores the constant `4.0` of the function ) # VectorAffineFunction -> VectorQuadraticFunction MOI.Bridges.runtests(