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] 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()