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 8a6f9aa063..b3a57b7483 100644 --- a/src/Utilities/universalfallback.jl +++ b/src/Utilities/universalfallback.jl @@ -372,6 +372,36 @@ function MOI.get( return _get(uf, attr) end +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/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() 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()