From c8ecc94563d04f099553fbed721fca269f059750 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 01/14] Test ConstraintDual for bridges --- docs/src/submodules/Bridges/implementation.md | 4 +- src/Bridges/Bridges.jl | 55 ++++++++++++++++++- .../bridges/SplitHyperRectangleBridge.jl | 10 +++- .../test_ScalarFunctionizeBridge.jl | 1 + 4 files changed, 64 insertions(+), 6 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 1aeff32883..df1907ca2c 100644 --- a/src/Bridges/Bridges.jl +++ b/src/Bridges/Bridges.jl @@ -284,7 +284,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...) @@ -294,12 +294,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, @@ -404,6 +452,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 5309b56f08..ebfb728d69 100644 --- a/src/Bridges/Constraint/bridges/SplitHyperRectangleBridge.jl +++ b/src/Bridges/Constraint/bridges/SplitHyperRectangleBridge.jl @@ -196,7 +196,11 @@ 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}) @@ -280,7 +284,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::SplitHyperRectangleBridge{T}, ::Nothing, ) where {T} @@ -293,7 +297,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::SplitHyperRectangleBridge{T}, value::AbstractVector{T}, ) where {T} diff --git a/test/Bridges/Constraint/test_ScalarFunctionizeBridge.jl b/test/Bridges/Constraint/test_ScalarFunctionizeBridge.jl index eec295bef8..9c63ef4482 100644 --- a/test/Bridges/Constraint/test_ScalarFunctionizeBridge.jl +++ b/test/Bridges/Constraint/test_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( From 04c35d88e2112919eeadfa357df666d0d90e23f2 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Tue, 27 Oct 2026 16:30:24 +1300 Subject: [PATCH 02/14] Update --- src/Bridges/Bridges.jl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Bridges/Bridges.jl b/src/Bridges/Bridges.jl index df1907ca2c..10e6c4107f 100644 --- a/src/Bridges/Bridges.jl +++ b/src/Bridges/Bridges.jl @@ -339,6 +339,7 @@ function _test_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 + return end function _runtests( @@ -454,7 +455,13 @@ function _runtests( end if !isnothing(dual) Test.@testset "Test ConstraintDual" begin - _test_dual(Bridge, input_fn; dual, eltype, model_eltype) + try + _test_dual(Bridge, input_fn; dual, eltype, model_eltype) + catch err + if !(err isa MOI.GetAttributeNotAllowed) + rethrow(err) + end + end end end return From 1002d815cb2a0392626e44f3a4a65786317bf5aa Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 28 Oct 2026 10:26:43 +1300 Subject: [PATCH 03/14] Fix docs --- docs/src/submodules/Bridges/implementation.md | 4 ++-- src/Bridges/Bridges.jl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/submodules/Bridges/implementation.md b/docs/src/submodules/Bridges/implementation.md index 2a7d278963..ae821f0087 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 | 30 30 0.0s +Bridges.runtests | 29 29 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 | 30 30 0.0s +Bridges.runtests | 29 29 0.0s ``` diff --git a/src/Bridges/Bridges.jl b/src/Bridges/Bridges.jl index 10e6c4107f..f9e9ff747b 100644 --- a/src/Bridges/Bridges.jl +++ b/src/Bridges/Bridges.jl @@ -284,7 +284,7 @@ julia> MOI.Bridges.runtests( end, ) Test Summary: | Pass Total Time -Bridges.runtests | 33 33 0.8s +Bridges.runtests | 32 32 0.0s ``` """ function runtests(args...; kwargs...) From 64e354373942d0706fa13ae8ab2547ba68a21ccb Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 28 Oct 2026 10:44:00 +1300 Subject: [PATCH 04/14] Update --- src/Bridges/Bridges.jl | 49 +++++++------------ .../AbstractFunctionConversionBridge.jl | 10 +++- .../test_ScalarFunctionizeBridge.jl | 1 - 3 files changed, 28 insertions(+), 32 deletions(-) diff --git a/src/Bridges/Bridges.jl b/src/Bridges/Bridges.jl index f9e9ff747b..87c74e3e09 100644 --- a/src/Bridges/Bridges.jl +++ b/src/Bridges/Bridges.jl @@ -294,16 +294,16 @@ 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` +# This function tests that ConstraintDual is implemented correctly for a bridge +# by comparing the DualObjectiveValue of the inner and outer models as it is +# computed using `get_fallback`. +# +# For this test to be enabled, the bridge should implement `MOI.supports` and +# `MOI.set` for `ConstraintDual`. The easiest way to achieve this is with +# `Union{ConstraintDual,ConstraintDualStart}`. +# +# A limitation of the test is that it requires the dual of every constraint to +# be set, so it might not work for some bridges. function _test_dual( Bridge::Type{<:AbstractBridge}, input_fn::Function; @@ -316,14 +316,10 @@ function _test_dual( 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}) + MOI.set(mock, MOI.DualStatus(), MOI.FEASIBLE_POINT) + for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) + if !MOI.supports(model, MOI.ConstraintDual(), MOI.ConstraintIndex{F,S}) # We need all duals for `DualObjectiveValue` fallback # TODO except the ones with no constants, we could ignore them return @@ -333,12 +329,11 @@ function _test_dual( 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 + Test.@test isapprox( + MOI.Utilities.get_fallback(model, MOI.DualObjectiveValue(), eltype), + MOI.Utilities.get_fallback(mock, MOI.DualObjectiveValue(), eltype); + atol = 1e-6, + ) return end @@ -455,13 +450,7 @@ function _runtests( end if !isnothing(dual) Test.@testset "Test ConstraintDual" begin - try - _test_dual(Bridge, input_fn; dual, eltype, model_eltype) - catch err - if !(err isa MOI.GetAttributeNotAllowed) - rethrow(err) - end - end + _test_dual(Bridge, input_fn; dual, eltype, model_eltype) end end return diff --git a/src/Bridges/Constraint/bridges/AbstractFunctionConversionBridge.jl b/src/Bridges/Constraint/bridges/AbstractFunctionConversionBridge.jl index e551bd13f6..66c6897c7c 100644 --- a/src/Bridges/Constraint/bridges/AbstractFunctionConversionBridge.jl +++ b/src/Bridges/Constraint/bridges/AbstractFunctionConversionBridge.jl @@ -71,6 +71,14 @@ function MOI.get( return ret end +function MOI.supports( + ::MOI.ModelLike, + ::MOI.ConstraintDual, + ::Type{<:AbstractFunctionConversionBridge{F,S}}, +) where {F,S} + return false +end + function MOI.supports( model::MOI.ModelLike, attr::MOI.AbstractConstraintAttribute, @@ -281,7 +289,7 @@ function conversion_cost( # This is most useful for conic solvers, which often support # VectorAffineFunction-in-S, but not VectorOfVariables-in-S. # - # The value in this function controls the cost of the transformation + # The value in this function controls the cost of the transformation # VectorOfVariables-in-S -> VectorAffineFunction-in-S. When we had the # previous cost of `1.0`, there might be an alternative bridge # VectorOfVariables-in-S -> VectorAffineFunction-in-S2 which also has a cost diff --git a/test/Bridges/Constraint/test_ScalarFunctionizeBridge.jl b/test/Bridges/Constraint/test_ScalarFunctionizeBridge.jl index 9c63ef4482..eec295bef8 100644 --- a/test/Bridges/Constraint/test_ScalarFunctionizeBridge.jl +++ b/test/Bridges/Constraint/test_ScalarFunctionizeBridge.jl @@ -317,7 +317,6 @@ 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( From b89c8bf91ff8132533539ea687dfd6aeeafc0e8e Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 28 Oct 2026 11:17:27 +1300 Subject: [PATCH 05/14] Update --- src/Bridges/Bridges.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Bridges/Bridges.jl b/src/Bridges/Bridges.jl index 87c74e3e09..ad48fff003 100644 --- a/src/Bridges/Bridges.jl +++ b/src/Bridges/Bridges.jl @@ -311,6 +311,9 @@ function _test_dual( eltype, model_eltype, ) + if Bridge <: Objective.AbstractBridge + return + end inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{model_eltype}()) mock = MOI.Utilities.MockOptimizer(inner) model = _bridged_model(Bridge{eltype}, mock) @@ -320,8 +323,6 @@ function _test_dual( MOI.set(mock, MOI.DualStatus(), MOI.FEASIBLE_POINT) for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) if !MOI.supports(model, MOI.ConstraintDual(), 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}()) From 997c9a8bbe6bf1636537eef1fc2256ff72d53607 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 29 Oct 2026 14:32:46 +1300 Subject: [PATCH 06/14] Update --- src/Bridges/Bridges.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bridges/Bridges.jl b/src/Bridges/Bridges.jl index ad48fff003..cca75dc879 100644 --- a/src/Bridges/Bridges.jl +++ b/src/Bridges/Bridges.jl @@ -449,7 +449,7 @@ function _runtests( Test.@testset "Test delete" begin # COV_EXCL_LINE _test_delete(Bridge, model, inner) end - if !isnothing(dual) + if !isnothing(dual) && !cannot_unbridge Test.@testset "Test ConstraintDual" begin _test_dual(Bridge, input_fn; dual, eltype, model_eltype) end From 8d113afc3a3fb44ada4aec8d58041031df30cba6 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 29 Oct 2026 14:59:43 +1300 Subject: [PATCH 07/14] Update --- src/Utilities/mockoptimizer.jl | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Utilities/mockoptimizer.jl b/src/Utilities/mockoptimizer.jl index 00c833b04e..ef3ce0876e 100644 --- a/src/Utilities/mockoptimizer.jl +++ b/src/Utilities/mockoptimizer.jl @@ -259,12 +259,20 @@ end function MOI.supports( ::MockOptimizer, - ::Union{MOI.ConstraintDual,MockConstraintAttribute}, + ::MockConstraintAttribute, ::Type{<:MOI.ConstraintIndex}, ) return true end +function MOI.supports( + ::MockOptimizer, + ::MOI.ConstraintDual, + ::Type{MOI.ConstraintIndex{F,S}}, +) where {F,S} + return !(S <: Union{MOI.ZeroOne,MOI.Integer,MOI.SOS1,MOI.SOS2}) +end + function MOI.supports( mock::MockOptimizer, attr::MOI.AbstractConstraintAttribute, From b95e4825767b08b97a8afe47f56be39795205789 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Thu, 29 Oct 2026 20:15:08 +1300 Subject: [PATCH 08/14] Exclude coverage --- src/Bridges/Bridges.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bridges/Bridges.jl b/src/Bridges/Bridges.jl index cca75dc879..eccb737d65 100644 --- a/src/Bridges/Bridges.jl +++ b/src/Bridges/Bridges.jl @@ -450,7 +450,7 @@ function _runtests( _test_delete(Bridge, model, inner) end if !isnothing(dual) && !cannot_unbridge - Test.@testset "Test ConstraintDual" begin + Test.@testset "Test ConstraintDual" begin # COV_EXCL_LINE _test_dual(Bridge, input_fn; dual, eltype, model_eltype) end end From 4e2a33286fc43d86b55a432d55747f1668ed999d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 31 Oct 2026 08:54:23 +0100 Subject: [PATCH 09/14] Remove exclusions --- .../bridges/AbstractFunctionConversionBridge.jl | 8 -------- src/Utilities/mockoptimizer.jl | 10 +--------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/Bridges/Constraint/bridges/AbstractFunctionConversionBridge.jl b/src/Bridges/Constraint/bridges/AbstractFunctionConversionBridge.jl index 66c6897c7c..7de3f09921 100644 --- a/src/Bridges/Constraint/bridges/AbstractFunctionConversionBridge.jl +++ b/src/Bridges/Constraint/bridges/AbstractFunctionConversionBridge.jl @@ -71,14 +71,6 @@ function MOI.get( return ret end -function MOI.supports( - ::MOI.ModelLike, - ::MOI.ConstraintDual, - ::Type{<:AbstractFunctionConversionBridge{F,S}}, -) where {F,S} - return false -end - function MOI.supports( model::MOI.ModelLike, attr::MOI.AbstractConstraintAttribute, diff --git a/src/Utilities/mockoptimizer.jl b/src/Utilities/mockoptimizer.jl index ef3ce0876e..00c833b04e 100644 --- a/src/Utilities/mockoptimizer.jl +++ b/src/Utilities/mockoptimizer.jl @@ -259,20 +259,12 @@ end function MOI.supports( ::MockOptimizer, - ::MockConstraintAttribute, + ::Union{MOI.ConstraintDual,MockConstraintAttribute}, ::Type{<:MOI.ConstraintIndex}, ) return true end -function MOI.supports( - ::MockOptimizer, - ::MOI.ConstraintDual, - ::Type{MOI.ConstraintIndex{F,S}}, -) where {F,S} - return !(S <: Union{MOI.ZeroOne,MOI.Integer,MOI.SOS1,MOI.SOS2}) -end - function MOI.supports( mock::MockOptimizer, attr::MOI.AbstractConstraintAttribute, From 90901507c58c09d2f5ebc0e302bb59d6b8414fd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 31 Oct 2026 08:55:47 +0100 Subject: [PATCH 10/14] Add tests --- .../test_ScalarFunctionizeBridge.jl | 58 +++++++++++++++---- test/Utilities/test_mockoptimizer.jl | 20 +++++++ 2 files changed, 67 insertions(+), 11 deletions(-) diff --git a/test/Bridges/Constraint/test_ScalarFunctionizeBridge.jl b/test/Bridges/Constraint/test_ScalarFunctionizeBridge.jl index eec295bef8..430f59590c 100644 --- a/test/Bridges/Constraint/test_ScalarFunctionizeBridge.jl +++ b/test/Bridges/Constraint/test_ScalarFunctionizeBridge.jl @@ -246,17 +246,19 @@ function test_runtests() 1.0 * x <= 1.0 """, ) - MOI.Bridges.runtests( - MOI.Bridges.Constraint.ScalarFunctionizeBridge, - """ - variables: x - x in ZeroOne() - """, - """ - variables: x - 1.0 * x in ZeroOne() - """, - ) + for set in ("ZeroOne()", "Integer()") + MOI.Bridges.runtests( + MOI.Bridges.Constraint.ScalarFunctionizeBridge, + """ + variables: x + x in $set + """, + """ + variables: x + 1.0 * x in $set + """, + ) + end MOI.Bridges.runtests( MOI.Bridges.Constraint.VectorFunctionizeBridge, """ @@ -290,6 +292,19 @@ function test_runtests() [1.0 * t, 1.0 * x] in SecondOrderCone(2) """, ) + for set in ("SOS1([1.0, 2.0])", "SOS2([1.0, 2.0])") + MOI.Bridges.runtests( + MOI.Bridges.Constraint.VectorFunctionizeBridge, + """ + variables: x, y + [x, y] in $set + """, + """ + variables: x, y + [1.0 * x, 1.0 * y] in $set + """, + ) + end return end @@ -333,6 +348,27 @@ function test_FunctionConversionBridge() return end +function test_FunctionConversionBridge_ConstraintDual() + inner = MOI.Utilities.MockOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + ) + model = MOI.Bridges.Constraint.ToScalarNonlinear{Float64}(inner) + x, y = MOI.add_variables(model, 2) + f = 1.0 * x * x + 2.0 * x * y + 3.0 * y + 4.0 + ci = MOI.add_constraint(model, f, MOI.GreaterThan(1.0)) + bridge = MOI.get(model, MOI.Bridges.FirstBridge(), ci) + @test MOI.get(model, MOI.ConstraintFunction(), ci) ≈ f + @test MOI.get(inner, MOI.ConstraintFunction(), bridge.constraint) ≈ + convert(MOI.ScalarNonlinearFunction, f) + @test MOI.supports(model, MOI.ConstraintDual(), typeof(ci)) + for dual in (-1.2, 0.0, 1.2) + MOI.set(model, MOI.ConstraintDual(), ci, dual) + @test MOI.get(model, MOI.ConstraintDual(), ci) == dual + @test MOI.get(inner, MOI.ConstraintDual(), bridge.constraint) == dual + end + return +end + function test_FunctionConversionBridge_VectorNonlinearFunction() inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) BT = MOI.Bridges.Constraint.FunctionConversionBridge{ diff --git a/test/Utilities/test_mockoptimizer.jl b/test/Utilities/test_mockoptimizer.jl index 1d297f72b6..d78f5f0045 100644 --- a/test/Utilities/test_mockoptimizer.jl +++ b/test/Utilities/test_mockoptimizer.jl @@ -191,6 +191,26 @@ function test_DualObjectiveValue() return end +function test_ConstraintDual_discrete_sets() + mock = MOIU.MockOptimizer( + MOIU.UniversalFallback(MOIU.Model{Float64}()); + eval_variable_constraint_dual = false, + ) + x, y = MOI.add_variables(mock, 2) + for (f, set, dual) in ( + (x, MOI.ZeroOne(), 1.2), + (y, MOI.Integer(), -2.3), + (MOI.VectorOfVariables([x, y]), MOI.SOS1([1.0, 2.0]), [1.2, -2.3]), + (MOI.VectorOfVariables([x, y]), MOI.SOS2([1.0, 2.0]), [-2.3, 1.2]), + ) + ci = MOI.add_constraint(mock, f, set) + @test MOI.supports(mock, MOI.ConstraintDual(), typeof(ci)) + MOI.set(mock, MOI.ConstraintDual(), ci, dual) + @test MOI.get(mock, MOI.ConstraintDual(), ci) == dual + end + return +end + function test_add_con_allowed_false() model = MOI.Utilities.MockOptimizer( MOI.Utilities.Model{Float64}(); From eebb62004a1a028793103dc09e6ed600c7195745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 31 Oct 2026 09:28:18 +0100 Subject: [PATCH 11/14] Fixes --- .../bridges/GeoMeanToPowerBridge.jl | 6 +-- .../bridges/GeoMeantoRelEntrBridge.jl | 19 +++++----- .../bridges/IntervalToHyperRectangleBridge.jl | 8 +++- .../Constraint/bridges/NormNuclearBridge.jl | 38 +++++++++++++++---- .../Constraint/bridges/NormOneBridge.jl | 14 ++++--- .../bridges/NumberConversionBridge.jl | 6 ++- .../bridges/RelativeEntropyBridge.jl | 10 +++-- .../Constraint/bridges/SOCtoPSDBridge.jl | 6 ++- .../Constraint/bridges/ScalarSlackBridge.jl | 8 +++- .../Constraint/bridges/ScalarizeBridge.jl | 10 +++-- .../bridges/SplitComplexEqualToBridge.jl | 18 +++++++-- .../bridges/SplitComplexZerosBridge.jl | 18 +++++++-- .../Constraint/bridges/SplitIntervalBridge.jl | 8 +++- .../Constraint/bridges/SquareBridge.jl | 10 +++-- .../Constraint/bridges/VectorizeBridge.jl | 8 +++- src/Bridges/Constraint/set_map.jl | 10 +++-- src/Bridges/Variable/set_map.jl | 14 +++++-- 17 files changed, 154 insertions(+), 57 deletions(-) diff --git a/src/Bridges/Constraint/bridges/GeoMeanToPowerBridge.jl b/src/Bridges/Constraint/bridges/GeoMeanToPowerBridge.jl index 191868057a..87aae0a845 100644 --- a/src/Bridges/Constraint/bridges/GeoMeanToPowerBridge.jl +++ b/src/Bridges/Constraint/bridges/GeoMeanToPowerBridge.jl @@ -259,7 +259,7 @@ end function MOI.supports( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, ::Type{GeoMeanToPowerBridge{T,F}}, ) where {T,F} return MOI.supports(model, attr, MOI.ConstraintIndex{F,MOI.PowerCone{T}}) @@ -293,7 +293,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::GeoMeanToPowerBridge{T,F}, start::AbstractVector{T}, ) where {T,F} @@ -327,7 +327,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::GeoMeanToPowerBridge, ::Nothing, ) diff --git a/src/Bridges/Constraint/bridges/GeoMeantoRelEntrBridge.jl b/src/Bridges/Constraint/bridges/GeoMeantoRelEntrBridge.jl index d0894d6ec9..fb450921dd 100644 --- a/src/Bridges/Constraint/bridges/GeoMeantoRelEntrBridge.jl +++ b/src/Bridges/Constraint/bridges/GeoMeantoRelEntrBridge.jl @@ -198,7 +198,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{GeoMeantoRelEntrBridge{T,F,G,H}}, ) where {T,F,G,H} ci_1 = MOI.ConstraintIndex{F,MOI.Nonnegatives} @@ -277,28 +281,23 @@ end # Note log.(w/geomean(w)) = log.(w) .- sum(log, w) / n. function MOI.set( model::MOI.ModelLike, - ::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::GeoMeantoRelEntrBridge, value, ) d = length(value) - 1 u = value[1] w = value[2:end] - MOI.set(model, MOI.ConstraintDualStart(), bridge.nn_index, [-u]) + MOI.set(model, attr, bridge.nn_index, [-u]) relentr_dual = vcat(-u / d, value[2:end], u / d * (log.(w) .+ (1 - sum(log, w) / d))) - MOI.set( - model, - MOI.ConstraintDualStart(), - bridge.relentr_index, - relentr_dual, - ) + MOI.set(model, attr, bridge.relentr_index, relentr_dual) return end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::GeoMeantoRelEntrBridge, ::Nothing, ) diff --git a/src/Bridges/Constraint/bridges/IntervalToHyperRectangleBridge.jl b/src/Bridges/Constraint/bridges/IntervalToHyperRectangleBridge.jl index 97517549b2..85ea0783e8 100644 --- a/src/Bridges/Constraint/bridges/IntervalToHyperRectangleBridge.jl +++ b/src/Bridges/Constraint/bridges/IntervalToHyperRectangleBridge.jl @@ -101,7 +101,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{IntervalToHyperRectangleBridge{T,F,G}}, ) where {T,F,G} return MOI.supports( @@ -157,7 +161,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::IntervalToHyperRectangleBridge, value, ) diff --git a/src/Bridges/Constraint/bridges/NormNuclearBridge.jl b/src/Bridges/Constraint/bridges/NormNuclearBridge.jl index e57dfd53e1..68d04eef3e 100644 --- a/src/Bridges/Constraint/bridges/NormNuclearBridge.jl +++ b/src/Bridges/Constraint/bridges/NormNuclearBridge.jl @@ -137,7 +137,7 @@ end function MOI.supports( model::MOI.ModelLike, - attr::MOI.ConstraintPrimalStart, + attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDual}, ::Type{NormSpectralBridge{T,F,G}}, ) where {T,F,G} return MOI.supports( @@ -215,6 +215,28 @@ function MOI.get( return vcat(t, X) end +function MOI.set( + model::MOI.ModelLike, + attr::MOI.ConstraintDual, + bridge::NormSpectralBridge{T}, + value::AbstractVector, +) where {T} + column_dim = bridge.column_dim + side_dim = bridge.row_dim + column_dim + dual = zeros(T, div(side_dim * (side_dim + 1), 2)) + # Invert the trace and the doubled off-diagonal entries in the getter. + for i in 1:side_dim + dual[MOI.Utilities.trimap(i, i)] = value[1] / side_dim + end + k = 2 + for j in 1:column_dim, i in (column_dim+1):side_dim + dual[MOI.Utilities.trimap(i, j)] = value[k] / 2 + k += 1 + end + MOI.set(model, attr, bridge.psd_index, dual) + return +end + """ NormNuclearBridge{T,F,G,H} <: Bridges.Constraint.AbstractBridge @@ -444,7 +466,7 @@ end function MOI.supports( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, ::Type{NormNuclearBridge{T,F,G,H}}, ) where {T,F,G,H} ci_1 = MOI.ConstraintIndex{F,MOI.GreaterThan{T}} @@ -494,11 +516,11 @@ end function MOI.set( model::MOI.ModelLike, - ::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::NormNuclearBridge{T}, value, ) where {T} - MOI.set(model, MOI.ConstraintDualStart(), bridge.ge_index, value[1]) + MOI.set(model, attr, bridge.ge_index, value[1]) column_dim = bridge.column_dim side_dim = bridge.row_dim + column_dim dual = zeros(T, div(side_dim * (side_dim + 1), 2)) @@ -510,17 +532,17 @@ function MOI.set( dual[MOI.Utilities.trimap(i, j)] = value[X_idx] / 2 X_idx += 1 end - MOI.set(model, MOI.ConstraintDualStart(), bridge.psd_index, dual) + MOI.set(model, attr, bridge.psd_index, dual) return end function MOI.set( model::MOI.ModelLike, - ::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::NormNuclearBridge{T}, ::Nothing, ) where {T} - MOI.set(model, MOI.ConstraintDualStart(), bridge.ge_index, nothing) - MOI.set(model, MOI.ConstraintDualStart(), bridge.psd_index, nothing) + MOI.set(model, attr, bridge.ge_index, nothing) + MOI.set(model, attr, bridge.psd_index, nothing) return end diff --git a/src/Bridges/Constraint/bridges/NormOneBridge.jl b/src/Bridges/Constraint/bridges/NormOneBridge.jl index e0221d5105..e257805b80 100644 --- a/src/Bridges/Constraint/bridges/NormOneBridge.jl +++ b/src/Bridges/Constraint/bridges/NormOneBridge.jl @@ -144,7 +144,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{NormOneBridge{T,F,G}}, ) where {T,F,G} return MOI.supports(model, attr, MOI.ConstraintIndex{F,MOI.Nonnegatives}) @@ -219,7 +223,7 @@ end # depending on the sense of `value[1 + i]`. function MOI.set( model::MOI.ModelLike, - ::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::NormOneBridge, value, ) @@ -233,16 +237,16 @@ function MOI.set( nn_dual[1+d+i] = value[1+i] end end - MOI.set(model, MOI.ConstraintDualStart(), bridge.nn_index, nn_dual) + MOI.set(model, attr, bridge.nn_index, nn_dual) return end function MOI.set( model::MOI.ModelLike, - ::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::NormOneBridge, ::Nothing, ) - MOI.set(model, MOI.ConstraintDualStart(), bridge.nn_index, nothing) + MOI.set(model, attr, bridge.nn_index, nothing) return end diff --git a/src/Bridges/Constraint/bridges/NumberConversionBridge.jl b/src/Bridges/Constraint/bridges/NumberConversionBridge.jl index ca1f24fb47..0776fcfa1e 100644 --- a/src/Bridges/Constraint/bridges/NumberConversionBridge.jl +++ b/src/Bridges/Constraint/bridges/NumberConversionBridge.jl @@ -123,7 +123,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{NumberConversionBridge{T,F1,S1,F2,S2}}, ) where {T,F1,S1,F2,S2} return MOI.supports(model, attr, MOI.ConstraintIndex{F2,S2}) diff --git a/src/Bridges/Constraint/bridges/RelativeEntropyBridge.jl b/src/Bridges/Constraint/bridges/RelativeEntropyBridge.jl index 2f5519475f..7df2247cc8 100644 --- a/src/Bridges/Constraint/bridges/RelativeEntropyBridge.jl +++ b/src/Bridges/Constraint/bridges/RelativeEntropyBridge.jl @@ -185,7 +185,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{RelativeEntropyBridge{T,F,G,H}}, ) where {T,F,G,H} ci_1 = MOI.ConstraintIndex{F,MOI.GreaterThan{T}} @@ -275,7 +279,7 @@ end # r_i = exp(W(-w_i / (-v_i * e))) * (-v_i * e), but this is more complicated. function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::RelativeEntropyBridge, value, ) @@ -289,7 +293,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::RelativeEntropyBridge, ::Nothing, ) diff --git a/src/Bridges/Constraint/bridges/SOCtoPSDBridge.jl b/src/Bridges/Constraint/bridges/SOCtoPSDBridge.jl index 0d483a9797..1ce6ffaf6b 100644 --- a/src/Bridges/Constraint/bridges/SOCtoPSDBridge.jl +++ b/src/Bridges/Constraint/bridges/SOCtoPSDBridge.jl @@ -218,7 +218,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{<:RSOCtoPSDBridge{T,F,G}}, ) where {T,F,G} return MOI.supports( diff --git a/src/Bridges/Constraint/bridges/ScalarSlackBridge.jl b/src/Bridges/Constraint/bridges/ScalarSlackBridge.jl index da0a3fdb3e..c5445e4c15 100644 --- a/src/Bridges/Constraint/bridges/ScalarSlackBridge.jl +++ b/src/Bridges/Constraint/bridges/ScalarSlackBridge.jl @@ -80,7 +80,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{<:_AbstractSlackBridge{T,VF,ZS,F,S}}, ) where {T,VF,ZS,F,S} ret = true @@ -152,7 +156,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::_AbstractSlackBridge{T}, value, ) where {T} diff --git a/src/Bridges/Constraint/bridges/ScalarizeBridge.jl b/src/Bridges/Constraint/bridges/ScalarizeBridge.jl index 0038cb4553..ea4074a35d 100644 --- a/src/Bridges/Constraint/bridges/ScalarizeBridge.jl +++ b/src/Bridges/Constraint/bridges/ScalarizeBridge.jl @@ -153,7 +153,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{ScalarizeBridge{T,F,S}}, ) where {T,F,S} return MOI.supports(model, attr, MOI.ConstraintIndex{F,S}) @@ -211,7 +215,7 @@ end function MOI.get( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::ScalarizeBridge, ) values = MOI.get.(model, attr, bridge.scalar_constraints) @@ -223,7 +227,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::ScalarizeBridge, value, ) diff --git a/src/Bridges/Constraint/bridges/SplitComplexEqualToBridge.jl b/src/Bridges/Constraint/bridges/SplitComplexEqualToBridge.jl index ebebe8e8cb..e330031adf 100644 --- a/src/Bridges/Constraint/bridges/SplitComplexEqualToBridge.jl +++ b/src/Bridges/Constraint/bridges/SplitComplexEqualToBridge.jl @@ -160,7 +160,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{<:SplitComplexEqualToBridge{T,F}}, ) where {T,F} return MOI.supports(model, attr, MOI.ConstraintIndex{F,MOI.EqualTo{T}}) @@ -194,7 +198,11 @@ end function MOI.set( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, bridge::SplitComplexEqualToBridge{T}, value, ) where {T} @@ -209,7 +217,11 @@ end function MOI.set( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, bridge::SplitComplexEqualToBridge, ::Nothing, ) diff --git a/src/Bridges/Constraint/bridges/SplitComplexZerosBridge.jl b/src/Bridges/Constraint/bridges/SplitComplexZerosBridge.jl index 3e12f77136..0a626c2c81 100644 --- a/src/Bridges/Constraint/bridges/SplitComplexZerosBridge.jl +++ b/src/Bridges/Constraint/bridges/SplitComplexZerosBridge.jl @@ -158,7 +158,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{<:SplitComplexZerosBridge{T,F}}, ) where {T,F} return MOI.supports(model, attr, MOI.ConstraintIndex{F,MOI.Zeros}) @@ -190,7 +194,11 @@ end function MOI.set( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, bridge::SplitComplexZerosBridge{T}, value, ) where {T} @@ -210,7 +218,11 @@ end function MOI.set( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, bridge::SplitComplexZerosBridge{T}, ::Nothing, ) where {T} diff --git a/src/Bridges/Constraint/bridges/SplitIntervalBridge.jl b/src/Bridges/Constraint/bridges/SplitIntervalBridge.jl index af393a1863..41e01b7ab0 100644 --- a/src/Bridges/Constraint/bridges/SplitIntervalBridge.jl +++ b/src/Bridges/Constraint/bridges/SplitIntervalBridge.jl @@ -211,7 +211,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{SplitIntervalBridge{T,F,S,LS,US}}, ) where {T,F,S,LS,US} ci_1 = MOI.ConstraintIndex{F,LS} @@ -296,7 +300,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::SplitIntervalBridge{T}, value, ) where {T} diff --git a/src/Bridges/Constraint/bridges/SquareBridge.jl b/src/Bridges/Constraint/bridges/SquareBridge.jl index fe3132a791..1f7302444e 100644 --- a/src/Bridges/Constraint/bridges/SquareBridge.jl +++ b/src/Bridges/Constraint/bridges/SquareBridge.jl @@ -279,7 +279,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{SquareBridge{T,F,G,TT,ST}}, ) where {T,F,G,TT,ST} return MOI.supports(model, attr, MOI.ConstraintIndex{F,TT}) & @@ -417,7 +421,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::SquareBridge, value, ) @@ -453,7 +457,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::SquareBridge, ::Nothing, ) diff --git a/src/Bridges/Constraint/bridges/VectorizeBridge.jl b/src/Bridges/Constraint/bridges/VectorizeBridge.jl index 50a1ea4854..26bda2be6e 100644 --- a/src/Bridges/Constraint/bridges/VectorizeBridge.jl +++ b/src/Bridges/Constraint/bridges/VectorizeBridge.jl @@ -108,7 +108,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{VectorizeBridge{T,F,S,G}}, ) where {T,F,S,G} return MOI.supports(model, attr, MOI.ConstraintIndex{F,S}) @@ -183,7 +187,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::VectorizeBridge, value, ) diff --git a/src/Bridges/Constraint/set_map.jl b/src/Bridges/Constraint/set_map.jl index 70062db687..450c99f3d2 100644 --- a/src/Bridges/Constraint/set_map.jl +++ b/src/Bridges/Constraint/set_map.jl @@ -24,7 +24,7 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart,MOI.ConstraintDual}, ::Type{<:BridgeType{T,F2,F3}}, ) where {T,F2,F3} return MOI.supports(model, attr, MOI.ConstraintIndex{F2,S2}) || @@ -205,7 +205,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::BT, value, ) where {BT<:MultiSetMapBridge} @@ -263,7 +263,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{<:SetMapBridge{T,S2,S1,F}}, ) where {T,S2,S1,F} return MOI.supports(model, attr, MOI.ConstraintIndex{F,S2}) diff --git a/src/Bridges/Variable/set_map.jl b/src/Bridges/Variable/set_map.jl index 21c9af8105..2b3a53ad8e 100644 --- a/src/Bridges/Variable/set_map.jl +++ b/src/Bridges/Variable/set_map.jl @@ -155,7 +155,7 @@ end function MOI.set( model::MOI.ModelLike, - attr::MOI.ConstraintDualStart, + attr::Union{MOI.ConstraintDualStart,MOI.ConstraintDual}, bridge::SetMapBridge, value, ) @@ -272,7 +272,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{<:SetMapBridge{T,S1}}, ) where {T,S1<:MOI.AbstractScalarSet} return MOI.supports(model, attr, MOI.ConstraintIndex{MOI.VariableIndex,S1}) @@ -351,7 +355,11 @@ end function MOI.supports( model::MOI.ModelLike, - attr::Union{MOI.ConstraintPrimalStart,MOI.ConstraintDualStart}, + attr::Union{ + MOI.ConstraintPrimalStart, + MOI.ConstraintDualStart, + MOI.ConstraintDual, + }, ::Type{<:SetMapBridge{T,S1}}, ) where {T,S1<:MOI.AbstractVectorSet} CI = MOI.ConstraintIndex{MOI.VectorOfVariables,S1} From cc6f435d98ff4109343c07427bbc2cfc622ac44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 31 Oct 2026 09:31:11 +0100 Subject: [PATCH 12/14] Fix --- src/Bridges/Bridges.jl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Bridges/Bridges.jl b/src/Bridges/Bridges.jl index eccb737d65..1a0d35f924 100644 --- a/src/Bridges/Bridges.jl +++ b/src/Bridges/Bridges.jl @@ -315,7 +315,13 @@ function _test_dual( return end inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{model_eltype}()) - mock = MOI.Utilities.MockOptimizer(inner) + # Variable bridges may shift variables, so the dual objective comparison + # requires stationarity. Constraint bridges should use the supplied duals, + # even when a variable has multiple bounds after bridging. + mock = MOI.Utilities.MockOptimizer( + inner; + eval_variable_constraint_dual = Bridge <: Variable.AbstractBridge, + ) model = _bridged_model(Bridge{eltype}, mock) input_fn(model) final_touch(model) From d3718e319d4fe8d7d74b7aeac60a6d92c417dc94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 31 Oct 2026 09:36:41 +0100 Subject: [PATCH 13/14] Fix --- src/Utilities/results.jl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Utilities/results.jl b/src/Utilities/results.jl index d1f199334d..b7ba290d6e 100644 --- a/src/Utilities/results.jl +++ b/src/Utilities/results.jl @@ -122,6 +122,8 @@ _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.Parameter}) = true + _variable_set_in_dual_objective(::Type{<:MOI.GreaterThan}) = true _variable_set_in_dual_objective(::Type{<:MOI.LessThan}) = true @@ -130,9 +132,11 @@ _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) = real(set_dot(x, y, set)) -_dual_objective_dot(x, y, set::MOI.EqualTo) = (x - set.value) * y +function _dual_objective_dot(x, y, set::Union{MOI.EqualTo,MOI.Parameter}) + return real(set_dot(x - set.value, y, set)) +end _dual_objective_dot(x, y, set::MOI.LessThan) = (x - set.upper) * y From 83fb27bb0766fcc1dd1648137cf5c650956ea4f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 31 Oct 2026 10:56:13 +0100 Subject: [PATCH 14/14] Fixes --- src/Bridges/Bridges.jl | 61 +++++++++++++++---- src/Utilities/set_dot.jl | 4 +- .../test_SplitHyperRectangleBridge.jl | 4 +- test/Bridges/Variable/test_VectorizeBridge.jl | 33 ++++++++++ test/Utilities/test_results.jl | 56 +++++++++++++++++ test/Utilities/test_set_dot.jl | 9 +++ 6 files changed, 152 insertions(+), 15 deletions(-) diff --git a/src/Bridges/Bridges.jl b/src/Bridges/Bridges.jl index 1a0d35f924..a05434a576 100644 --- a/src/Bridges/Bridges.jl +++ b/src/Bridges/Bridges.jl @@ -295,15 +295,12 @@ function runtests(args...; kwargs...) end # This function tests that ConstraintDual is implemented correctly for a bridge -# by comparing the DualObjectiveValue of the inner and outer models as it is -# computed using `get_fallback`. +# by comparing Lagrangian values of the inner and outer models at corresponding +# primal points. Evaluating the functions also works for nonlinear expressions. # # For this test to be enabled, the bridge should implement `MOI.supports` and # `MOI.set` for `ConstraintDual`. The easiest way to achieve this is with # `Union{ConstraintDual,ConstraintDualStart}`. -# -# A limitation of the test is that it requires the dual of every constraint to -# be set, so it might not work for some bridges. function _test_dual( Bridge::Type{<:AbstractBridge}, input_fn::Function; @@ -315,35 +312,75 @@ function _test_dual( return end inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{model_eltype}()) - # Variable bridges may shift variables, so the dual objective comparison - # requires stationarity. Constraint bridges should use the supplied duals, - # even when a variable has multiple bounds after bridging. mock = MOI.Utilities.MockOptimizer( inner; - eval_variable_constraint_dual = Bridge <: Variable.AbstractBridge, + eval_variable_constraint_dual = false, ) model = _bridged_model(Bridge{eltype}, mock) input_fn(model) final_touch(model) final_touch(model) + MOI.set(mock, MOI.PrimalStatus(), MOI.FEASIBLE_POINT) + # Use an integer point so integer-coefficient vector functions can be + # evaluated without inexact conversions. Keep auxiliary primals at zero. + primal = 2 * one(model_eltype) + x = MOI.get(mock, MOI.ListOfVariableIndices()) + MOI.set(mock, MOI.VariablePrimal(), x, fill(zero(primal), length(x))) + if Bridge <: Variable.AbstractBridge + MOI.set(mock, MOI.VariablePrimal(), x, fill(primal, length(x))) + else + x = MOI.get(model, MOI.ListOfVariableIndices()) + MOI.set(model, MOI.VariablePrimal(), x, fill(primal, length(x))) + end MOI.set(mock, MOI.DualStatus(), MOI.FEASIBLE_POINT) + # Some bridges leave auxiliary constraint duals unspecified. + for (F, S) in MOI.get(mock, MOI.ListOfConstraintTypesPresent()) + for ci in MOI.get(mock, MOI.ListOfConstraintIndices{F,S}()) + set = MOI.get(mock, MOI.ConstraintSet(), ci) + MOI.set( + mock, + MOI.ConstraintDual(), + ci, + _fake_start(zero(model_eltype), set), + ) + end + end for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) if !MOI.supports(model, MOI.ConstraintDual(), MOI.ConstraintIndex{F,S}) 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)) + value = _fake_start(dual, set) + MOI.set(model, MOI.ConstraintDual(), ci, value) + Test.@test MOI.get(model, MOI.ConstraintDual(), ci) ≈ value end end Test.@test isapprox( - MOI.Utilities.get_fallback(model, MOI.DualObjectiveValue(), eltype), - MOI.Utilities.get_fallback(mock, MOI.DualObjectiveValue(), eltype); + _test_lagrangian_value(model), + _test_lagrangian_value(mock); atol = 1e-6, ) return end +function _test_lagrangian_value(model::MOI.ModelLike) + value = MOI.Utilities.get_fallback(model, MOI.ObjectiveValue()) + sign = MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE ? 1 : -1 + for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent()) + for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) + f = MOI.get(model, MOI.ConstraintFunction(), ci) + primal = MOI.Utilities.eval_variables(model, f) do vi + return MOI.get(model, MOI.VariablePrimal(), vi) + end + set = MOI.get(model, MOI.ConstraintSet(), ci) + dual = MOI.get(model, MOI.ConstraintDual(), ci) + value += sign * MOI.Utilities._dual_objective_dot(primal, dual, set) + end + end + return value +end + function _runtests( Bridge::Type{<:AbstractBridge}, input_fn::Function, diff --git a/src/Utilities/set_dot.jl b/src/Utilities/set_dot.jl index 5abbdc7641..32d04e3156 100644 --- a/src/Utilities/set_dot.jl +++ b/src/Utilities/set_dot.jl @@ -39,9 +39,9 @@ function triangle_dot( for j in 1:i k += 1 if i == j - result = MA.add_mul!!(result, x[k], y[k]) + result = MA.add_mul!!(result, conj(x[k]), y[k]) else - result = MA.add_mul!!(result, 2, x[k], y[k]) + result = MA.add_mul!!(result, 2, conj(x[k]), y[k]) end end end diff --git a/test/Bridges/Constraint/test_SplitHyperRectangleBridge.jl b/test/Bridges/Constraint/test_SplitHyperRectangleBridge.jl index 33c39ccc98..f7f7a84e9e 100644 --- a/test/Bridges/Constraint/test_SplitHyperRectangleBridge.jl +++ b/test/Bridges/Constraint/test_SplitHyperRectangleBridge.jl @@ -95,6 +95,7 @@ function test_runtests_free_row() [1.0 * z + -3.0, 3.3 + -1.0 * z] in Nonnegatives(2) """; constraint_start = 3.0, + dual = [0.0, 3.0], ) return end @@ -108,7 +109,8 @@ function test_runtests_all_free_rows() """, """ variables: x - """, + """; + dual = [0.0], ) return end diff --git a/test/Bridges/Variable/test_VectorizeBridge.jl b/test/Bridges/Variable/test_VectorizeBridge.jl index 33936de912..3d44061b34 100644 --- a/test/Bridges/Variable/test_VectorizeBridge.jl +++ b/test/Bridges/Variable/test_VectorizeBridge.jl @@ -287,6 +287,39 @@ function test_runtests() return end +function test_ConstraintDual_lagrangian_shift() + mock = MOI.Utilities.MockOptimizer( + MOI.Utilities.Model{Float64}(); + eval_variable_constraint_dual = false, + ) + model = MOI.Bridges.Variable.Vectorize{Float64}(mock) + x, _ = MOI.add_constrained_variable(model, MOI.GreaterThan(2.0)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set( + model, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + 1.0 * x, + ) + y = only(MOI.get(mock, MOI.ListOfVariableIndices())) + ci = only( + MOI.get( + mock, + MOI.ListOfConstraintIndices{ + MOI.VectorOfVariables, + MOI.Nonnegatives, + }(), + ), + ) + MOI.set(mock, MOI.VariablePrimal(), y, 3.0) + MOI.set(mock, MOI.ConstraintDual(), ci, [1.2]) + MOI.set(mock, MOI.PrimalStatus(), MOI.FEASIBLE_POINT) + # The dual is deliberately nonstationary: the coefficient of y in the + # Lagrangian is 1 - 1.2. Both models should nevertheless give 5 - 3 * 1.2. + @test MOI.Bridges._test_lagrangian_value(model) ≈ 1.4 + @test MOI.Bridges._test_lagrangian_value(mock) ≈ 1.4 + return +end + function test_list_of_constraint_indices() inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()) model = MOI.Bridges.Variable.Vectorize{Float64}(inner) diff --git a/test/Utilities/test_results.jl b/test/Utilities/test_results.jl index 6b0c685dda..7e82bc5173 100644 --- a/test/Utilities/test_results.jl +++ b/test/Utilities/test_results.jl @@ -291,6 +291,62 @@ function test_get_fallback_DualObjectiveValue() return end +function test_get_fallback_DualObjectiveValue_complex() + model = MOI.Utilities.MockOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + ) + x = MOI.add_variable(model) + c1 = MOI.add_constraint( + model, + (1.0 + 2.0im) * x + (3.0 + 4.0im), + MOI.EqualTo(1.0 - 2.0im), + ) + c2 = MOI.add_constraint( + model, + MOI.Utilities.vectorize([ + (1.0 + 2.0im) * x + (3.0 + 4.0im), + -1.0im * x + (-2.0 + 5.0im), + ]), + MOI.Zeros(2), + ) + MOI.set(model, MOI.ConstraintDual(), c1, 2.0 - 3.0im) + MOI.set(model, MOI.ConstraintDual(), c2, [2.0 - 3.0im, -1.0 + 4.0im]) + MOI.set(model, MOI.DualStatus(), MOI.FEASIBLE_POINT) + for (sense, expected) in ((MOI.MIN_SENSE, -2.0), (MOI.MAX_SENSE, 2.0)) + MOI.set(model, MOI.ObjectiveSense(), sense) + @test MOI.Utilities.get_fallback( + model, + MOI.DualObjectiveValue(), + Float64, + ) == expected + end + return +end + +function test_get_fallback_DualObjectiveValue_Parameter() + model = MOI.Utilities.MockOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()); + eval_variable_constraint_dual = false, + ) + x, ci = MOI.add_constrained_variable(model, MOI.Parameter(2.0)) + MOI.set( + model, + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + 1.0 * x + 5.0, + ) + MOI.set(model, MOI.ConstraintDual(), ci, 3.0) + MOI.set(model, MOI.DualStatus(), MOI.FEASIBLE_POINT) + for (sense, expected) in ((MOI.MIN_SENSE, 11.0), (MOI.MAX_SENSE, -1.0)) + MOI.set(model, MOI.ObjectiveSense(), sense) + @test MOI.Utilities.get_fallback( + model, + MOI.DualObjectiveValue(), + Float64, + ) == expected + end + return +end + function test_get_fallback_DualObjectiveValue_Quadratic() T = Float64 model = MOI.Utilities.MockOptimizer( diff --git a/test/Utilities/test_set_dot.jl b/test/Utilities/test_set_dot.jl index 969e0442c4..b9abe45406 100644 --- a/test/Utilities/test_set_dot.jl +++ b/test/Utilities/test_set_dot.jl @@ -53,6 +53,15 @@ function test_set_dot(T = Int) end end +function test_set_dot_complex_triangle() + x = ComplexF64[1+2im, 3-4im, -2+5im] + y = ComplexF64[2-3im, -1+2im, 4+1im] + set = MOI.PositiveSemidefiniteConeTriangle(2) + @test MOI.Utilities.set_dot(x, y, set) == -29 - 25im + @test MOI.Utilities.set_dot(x, x, set) == 84 + return +end + end TestSetDot.runtests()