From 46f692b1663f06fb1f34c10f153bf47efc0c70d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Tue, 19 May 2026 08:45:14 +0200 Subject: [PATCH 1/2] Fix BridgingCost for UniversalFallback --- src/Utilities/universalfallback.jl | 39 ++++++++++++++++++++++ test/Utilities/test_universalfallback.jl | 42 ++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/src/Utilities/universalfallback.jl b/src/Utilities/universalfallback.jl index 8a6f9aa063..5ac5a45055 100644 --- a/src/Utilities/universalfallback.jl +++ b/src/Utilities/universalfallback.jl @@ -372,6 +372,45 @@ function MOI.get( return _get(uf, attr) end +# `UniversalFallback` claims to support every `(F, S)` constraint pair and every +# constrained-variable set via its catch-all `supports_constraint` / +# `supports_add_constrained_variable(s)`. The bridging-cost attributes must +# agree with that: if the inner model genuinely supports the pair/set, defer to +# it; otherwise, `UniversalFallback` itself supports it (by caching the +# constraint in its own dict), so the cost is `0.0`. The generic +# `AbstractModelAttribute` getter above would otherwise forward to the inner +# model — whose `get_fallback` returns `Inf` for unsupported pairs — even +# though `UniversalFallback` claims support. +function MOI.get( + uf::UniversalFallback, + attr::MOI.ConstraintBridgingCost{F,S}, +) where {F,S} + if MOI.supports_constraint(uf.model, F, S) + return MOI.get(uf.model, attr) + end + return 0.0 +end + +function MOI.get( + uf::UniversalFallback, + attr::MOI.VariableBridgingCost{S}, +) where {S<:MOI.AbstractScalarSet} + if MOI.supports_add_constrained_variable(uf.model, S) + return MOI.get(uf.model, attr) + end + return 0.0 +end + +function MOI.get( + uf::UniversalFallback, + attr::MOI.VariableBridgingCost{S}, +) where {S<:MOI.AbstractVectorSet} + if MOI.supports_add_constrained_variables(uf.model, S) + return MOI.get(uf.model, attr) + end + return 0.0 +end + function MOI.get( uf::UniversalFallback, attr::MOI.AbstractConstraintAttribute, diff --git a/test/Utilities/test_universalfallback.jl b/test/Utilities/test_universalfallback.jl index d845e270a1..da5251f642 100644 --- a/test/Utilities/test_universalfallback.jl +++ b/test/Utilities/test_universalfallback.jl @@ -530,6 +530,48 @@ function test_set_inner_constraint_attribute() return end +function test_bridging_cost_consistent_with_supports() + # `UniversalFallback.supports_constraint` and + # `supports_add_constrained_variable(s)` accept absolutely anything by + # forwarding through their `is_bridged`-style catch-all. The bridging-cost + # attributes must agree: they should never return `Inf` for a pair that + # `supports_*` claims to support, because that would make + # `LazyBridgeOptimizer` treat the node as unreachable and break graph + # construction. Use `Model{BigFloat}` so the inner model genuinely does + # not support `*Cone{Float64}`, exercising the case where + # `UniversalFallback` extends support beyond the inner. + model = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{BigFloat}()) + for (F, S) in ( + # inner supports natively + (MOI.ScalarAffineFunction{BigFloat}, MOI.LessThan{BigFloat}), + (MOI.VectorOfVariables, MOI.PowerCone{BigFloat}), + # inner does not support; UF stores in its own dict + (MOI.ScalarAffineFunction{Float64}, MOI.LessThan{Float64}), + (MOI.VectorOfVariables, MOI.PowerCone{Float64}), + (MOI.VectorAffineFunction{BigFloat}, MOI.Test.UnknownVectorSet), + ) + @test MOI.supports_constraint(model, F, S) + @test MOI.get(model, MOI.ConstraintBridgingCost{F,S}()) < Inf + end + for S in ( + MOI.GreaterThan{BigFloat}, + MOI.Integer, + MOI.GreaterThan{Float64}, # not natively supported by Model{BigFloat} + ) + @test MOI.supports_add_constrained_variable(model, S) + @test MOI.get(model, MOI.VariableBridgingCost{S}()) < Inf + end + for S in ( + MOI.Nonnegatives, + MOI.PowerCone{BigFloat}, + MOI.PowerCone{Float64}, # not natively supported by Model{BigFloat} + ) + @test MOI.supports_add_constrained_variables(model, S) + @test MOI.get(model, MOI.VariableBridgingCost{S}()) < Inf + end + return +end + end # module TestUniversalFallback.runtests() From 66415663ede7ecebe8f2f7ed789d5d0976c024c3 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 20 May 2026 08:58:53 +1200 Subject: [PATCH 2/2] Update --- src/FileFormats/NL/NL.jl | 8 +++ src/Test/test_attribute.jl | 52 +++++++++++++++++++ src/Test/test_basic_constraint.jl | 10 ++++ src/Utilities/cachingoptimizer.jl | 10 ++++ src/Utilities/universalfallback.jl | 9 ---- .../test_IntervalToHyperRectangleBridge.jl | 2 +- test/FileFormats/NL/test_NL.jl | 11 ++++ test/Utilities/test_cachingoptimizer.jl | 18 +++++++ 8 files changed, 110 insertions(+), 10 deletions(-) diff --git a/src/FileFormats/NL/NL.jl b/src/FileFormats/NL/NL.jl index 5029e49776..90952e5903 100644 --- a/src/FileFormats/NL/NL.jl +++ b/src/FileFormats/NL/NL.jl @@ -880,6 +880,14 @@ function MOI.get(model::Model, attr::MOI.AbstractModelAttribute) return MOI.get(inner, attr) end +# It doesn't need the inner model +function MOI.get( + model::Model, + attr::Union{MOI.VariableBridgingCost,MOI.ConstraintBridgingCost}, +) + return MOI.get_fallback(model, attr) +end + function MOI.get( model::Model, attr::MOI.AbstractConstraintAttribute, diff --git a/src/Test/test_attribute.jl b/src/Test/test_attribute.jl index a8fa9faaac..a5bb796f27 100644 --- a/src/Test/test_attribute.jl +++ b/src/Test/test_attribute.jl @@ -393,3 +393,55 @@ function test_attribute_unsupported_constraint(model::MOI.ModelLike, ::Config) end version_added(::typeof(test_attribute_unsupported_constraint)) = v"1.9.0" + +""" + test_attribute_VariableBridgingCost(model::MOI.ModelLike, config::Config) + +Test that, for every set `S` that the model claims to support via +`supports_add_constrained_variable(s)`, the corresponding +[`MOI.VariableBridgingCost`](@ref) attribute returns a finite value. + +This is the variable-side analog of the `ConstraintBridgingCost` check in +`_basic_constraint_test_helper`. + +The fallback works for most model but it may need custom method for some MOI +layers (see https://github.com/jump-dev/MathOptInterface.jl/pull/3001#issuecomment-4468198935). + +This test is here to catch that. +""" +function test_attribute_VariableBridgingCost( + model::MOI.ModelLike, + ::Config{T}, +) where {T} + for S in Any[ + MOI.GreaterThan{T}, + MOI.LessThan{T}, + MOI.EqualTo{T}, + MOI.Interval{T}, + MOI.Integer, + MOI.ZeroOne, + MOI.Semicontinuous{T}, + MOI.Semiinteger{T}, + ] + if MOI.supports_add_constrained_variable(model, S) + @test MOI.get(model, MOI.VariableBridgingCost{S}()) < Inf + end + end + for S in Any[ + MOI.Reals, + MOI.Zeros, + MOI.Nonnegatives, + MOI.Nonpositives, + MOI.SecondOrderCone, + MOI.RotatedSecondOrderCone, + MOI.ExponentialCone, + MOI.PositiveSemidefiniteConeTriangle, + ] + if MOI.supports_add_constrained_variables(model, S) + @test MOI.get(model, MOI.VariableBridgingCost{S}()) < Inf + end + end + return +end + +version_added(::typeof(test_attribute_VariableBridgingCost)) = v"1.52.0" diff --git a/src/Test/test_basic_constraint.jl b/src/Test/test_basic_constraint.jl index f9d0030d66..4bb529ac6a 100644 --- a/src/Test/test_basic_constraint.jl +++ b/src/Test/test_basic_constraint.jl @@ -255,6 +255,16 @@ function _basic_constraint_test_helper( ### @requires MOI.supports_constraint(model, F, S) ### + ### Test MOI.ConstraintBridgingCost + ### + # If `supports_constraint(F, S)` returns `true`, then the model must be + # able to handle that pair (possibly via bridging), so the bridging cost + # must be finite. The fallback works for most model but it may need + # custom method for some MOI layer (see + # https://github.com/jump-dev/MathOptInterface.jl/pull/3001#issuecomment-4468198935) + # This test is here to catch that. + @test MOI.get(model, MOI.ConstraintBridgingCost{F,S}()) < Inf + ### ### Test MOI.NumberOfConstraints ### @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 0 diff --git a/src/Utilities/cachingoptimizer.jl b/src/Utilities/cachingoptimizer.jl index f3392bbf07..0df5e5863f 100644 --- a/src/Utilities/cachingoptimizer.jl +++ b/src/Utilities/cachingoptimizer.jl @@ -897,6 +897,16 @@ function MOI.get(model::CachingOptimizer, attr::MOI.AbstractModelAttribute) return _get_model_attribute(model, attr) end +function MOI.get( + model::CachingOptimizer, + attr::Union{MOI.VariableBridgingCost,MOI.ConstraintBridgingCost}, +)::Float64 + if state(model) == NO_OPTIMIZER + return MOI.get(model.model_cache, attr) + end + return MOI.get(model.optimizer, attr) +end + function MOI.get( model::CachingOptimizer, attr::MOI.TerminationStatus, diff --git a/src/Utilities/universalfallback.jl b/src/Utilities/universalfallback.jl index 5ac5a45055..b3a57b7483 100644 --- a/src/Utilities/universalfallback.jl +++ b/src/Utilities/universalfallback.jl @@ -372,15 +372,6 @@ function MOI.get( return _get(uf, attr) end -# `UniversalFallback` claims to support every `(F, S)` constraint pair and every -# constrained-variable set via its catch-all `supports_constraint` / -# `supports_add_constrained_variable(s)`. The bridging-cost attributes must -# agree with that: if the inner model genuinely supports the pair/set, defer to -# it; otherwise, `UniversalFallback` itself supports it (by caching the -# constraint in its own dict), so the cost is `0.0`. The generic -# `AbstractModelAttribute` getter above would otherwise forward to the inner -# model — whose `get_fallback` returns `Inf` for unsupported pairs — even -# though `UniversalFallback` claims support. function MOI.get( uf::UniversalFallback, attr::MOI.ConstraintBridgingCost{F,S}, diff --git a/test/Bridges/Constraint/test_IntervalToHyperRectangleBridge.jl b/test/Bridges/Constraint/test_IntervalToHyperRectangleBridge.jl index 63a992bc62..4b1e0b8c38 100644 --- a/test/Bridges/Constraint/test_IntervalToHyperRectangleBridge.jl +++ b/test/Bridges/Constraint/test_IntervalToHyperRectangleBridge.jl @@ -38,7 +38,7 @@ function test_basic(T) MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}()), ) bridged_mock = MOI.Bridges.Constraint.IntervalToHyperRectangle{T}(mock) - config = MOI.Test.Config() + config = MOI.Test.Config(T) MOI.Test.runtests( bridged_mock, config, diff --git a/test/FileFormats/NL/test_NL.jl b/test/FileFormats/NL/test_NL.jl index 88c04f8aea..f62916cc79 100644 --- a/test/FileFormats/NL/test_NL.jl +++ b/test/FileFormats/NL/test_NL.jl @@ -1542,6 +1542,17 @@ function test_unsupported_kwarg() return end +function test_VariableBridgingCost() + model = NL.Model() + attr = MOI.VariableBridgingCost{MOI.LessThan{Float64}}() + @test MOI.get(model, attr) == 0 + attr = MOI.ConstraintBridgingCost{MOI.VariableIndex,MOI.LessThan{Float64}}() + @test MOI.get(model, attr) == 0 + attr = MOI.VariableBridgingCost{MOI.SecondOrderCone}() + @test isinf(MOI.get(model, attr)) + return +end + end TestNLModel.runtests() diff --git a/test/Utilities/test_cachingoptimizer.jl b/test/Utilities/test_cachingoptimizer.jl index 9d7b38abc1..3b26306a0e 100644 --- a/test/Utilities/test_cachingoptimizer.jl +++ b/test/Utilities/test_cachingoptimizer.jl @@ -1465,6 +1465,24 @@ function test_rethrow_set_model_attribute() return end +function test_BridgingCost_NO_OPTIMIZER() + cache = MOI.Utilities.Model{Float64}() + model = MOI.Utilities.CachingOptimizer(cache, MOI.Utilities.AUTOMATIC) + @test MOI.Utilities.state(model) == MOI.Utilities.NO_OPTIMIZER + @test MOI.get(model, MOI.VariableBridgingCost{MOI.LessThan{Float64}}()) == + 0.0 + @test MOI.get(model, MOI.VariableBridgingCost{MOI.LessThan{Int}}()) == Inf + @test MOI.get( + model, + MOI.ConstraintBridgingCost{MOI.VariableIndex,MOI.LessThan{Float64}}(), + ) == 0.0 + @test MOI.get( + model, + MOI.ConstraintBridgingCost{MOI.VariableIndex,MOI.LessThan{Int}}(), + ) == Inf + return +end + end # module TestCachingOptimizer.runtests()