diff --git a/src/Utilities/cachingoptimizer.jl b/src/Utilities/cachingoptimizer.jl index 672900c906..fba56bbbb2 100644 --- a/src/Utilities/cachingoptimizer.jl +++ b/src/Utilities/cachingoptimizer.jl @@ -915,6 +915,8 @@ function MOI.get( return MOI.get(model.optimizer, attr) end +_has_fallback(::MOI.AnyAttribute, ::Type{<:MOI.Index}) = false + function MOI.get( model::CachingOptimizer, attr::Union{MOI.AbstractVariableAttribute,MOI.AbstractConstraintAttribute}, @@ -924,6 +926,9 @@ function MOI.get( return MOI.get(model.model_cache, attr, index) end _throw_if_get_attribute_not_allowed(model, attr; needs_optimizer_map = true) + if _has_fallback(attr, typeof(index)) + return _get_fallback(model, attr, index) + end value = MOI.get( model.optimizer, attr, @@ -935,12 +940,15 @@ end function MOI.get( model::CachingOptimizer, attr::Union{MOI.AbstractVariableAttribute,MOI.AbstractConstraintAttribute}, - indices::Vector{<:MOI.Index}, -) + indices::Vector{I}, +) where {I<:MOI.Index} if !MOI.is_set_by_optimize(attr) return MOI.get(model.model_cache, attr, indices) end _throw_if_get_attribute_not_allowed(model, attr; needs_optimizer_map = true) + if _has_fallback(attr, I) + return _get_fallback(model, attr, indices) + end value = MOI.get( model.optimizer, attr, @@ -950,19 +958,29 @@ function MOI.get( end ### -### MOI.ConstraintPrimal +### MOI.ConstraintPrimal and MOI.ConstraintDual ### -# ConstraintPrimal is slightly unique for CachingOptimizer because if the solver +# `ConstraintPrimal` is slightly unique for CachingOptimizer because if the solver # doesn't support the attribute directly, we can use the fallback to query the # function from the cache and the variable value from the optimizer. +# The `ConstraintDual` of a `VariableIndex` or `VectorOfVariables` can +# also be computed from the `ConstraintDual` of the other constraints. -function MOI.get( +_has_fallback(::MOI.ConstraintPrimal, ::Type{<:MOI.ConstraintIndex}) = true + +function _has_fallback( + ::MOI.ConstraintDual, + ::Type{<:MOI.ConstraintIndex{F}}, +) where {F<:Union{MOI.VariableIndex,MOI.VectorOfVariables}} + return true +end + +function _get_fallback( model::CachingOptimizer, - attr::MOI.ConstraintPrimal, + attr::MOI.AbstractConstraintAttribute, index::MOI.ConstraintIndex, ) - _throw_if_get_attribute_not_allowed(model, attr; needs_optimizer_map = true) try return MOI.get( model.optimizer, @@ -978,12 +996,11 @@ function MOI.get( end end -function MOI.get( +function _get_fallback( model::CachingOptimizer, - attr::MOI.ConstraintPrimal, + attr::MOI.AbstractConstraintAttribute, indices::Vector{<:MOI.ConstraintIndex}, ) - _throw_if_get_attribute_not_allowed(model, attr; needs_optimizer_map = true) try return MOI.get( model.optimizer, diff --git a/test/Utilities/cachingoptimizer.jl b/test/Utilities/cachingoptimizer.jl index 534168cfb5..feb42eb079 100644 --- a/test/Utilities/cachingoptimizer.jl +++ b/test/Utilities/cachingoptimizer.jl @@ -851,14 +851,6 @@ function MOI.get( return 1.2 end -function MOI.get( - ::_GetFallbackModel1310, - ::MOI.ConstraintDual, - ::MOI.ConstraintIndex, -) - return 1.2 -end - function test_ConstraintPrimal_fallback() model = MOI.Utilities.CachingOptimizer( MOI.Utilities.Model{Float64}(), @@ -880,6 +872,43 @@ function test_ConstraintPrimal_fallback() return end +function test_ConstraintDual_variable_fallback() + model = MOI.Utilities.CachingOptimizer( + MOI.Utilities.Model{Float64}(), + _GetFallbackModel1310(), + ) + x = MOI.add_variable(model) + cx = MOI.add_constraint(model, x, MOI.GreaterThan(1.0)) + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + MOI.set(model, MOI.ObjectiveFunction{MOI.VariableIndex}(), x) + MOI.optimize!(model) + @test MOI.get(model, MOI.ConstraintDual(), cx) == 1.0 + @test_throws( + MOI.ResultIndexBoundsError(MOI.ConstraintDual(2), 1), + MOI.get(model, MOI.ConstraintDual(2), cx), + ) + @test_throws( + MOI.ResultIndexBoundsError(MOI.ConstraintDual(2), 1), + MOI.get(model, MOI.ConstraintDual(2), [cx]), + ) + return +end + +function test_ConstraintDual_nonvariable_nofallback() + model = MOI.Utilities.CachingOptimizer( + MOI.Utilities.Model{Float64}(), + _GetFallbackModel1310(), + ) + x = MOI.add_variable(model) + cx = MOI.add_constraint(model, x + 1.0, MOI.GreaterThan(1.0)) + MOI.optimize!(model) + @test_throws( + MOI.GetAttributeNotAllowed, + MOI.get(model, MOI.ConstraintDual(), cx), + ) + return +end + function test_ConstraintPrimal_fallback_error() model = MOI.Utilities.CachingOptimizer( MOI.Utilities.Model{Float64}(), @@ -926,7 +955,7 @@ function test_DualObjectiveValue_fallback() MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) MOI.set(model, MOI.ObjectiveFunction{typeof(x)}(), x) MOI.optimize!(model) - @test MOI.get(model, MOI.DualObjectiveValue()) == 1.2 + @test MOI.get(model, MOI.DualObjectiveValue()) == 1.0 @test_throws( MOI.ResultIndexBoundsError(MOI.DualObjectiveValue(2), 1), MOI.get(model, MOI.DualObjectiveValue(2)),