diff --git a/src/Dualization.jl b/src/Dualization.jl index 5f3e0e0d..bca8b5fb 100644 --- a/src/Dualization.jl +++ b/src/Dualization.jl @@ -22,6 +22,7 @@ include("dualize.jl") include("MOI_wrapper.jl") include("vectorize_emulator.jl") include("attributes.jl") +include("supports.jl") export dualize export dual_optimizer diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index b1e0bdb4..977e53b7 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -101,56 +101,6 @@ end DualOptimizer() = error("DualOptimizer must have a solver attached") -MOI.supports(::DualOptimizer, ::MOI.ObjectiveSense) = true - -function MOI.supports( - optimizer::DualOptimizer{T}, - ::MOI.ObjectiveFunction{F}, -) where {T,F} - # If the objective function is `MOI.VariableIndex` or - # `MOI.ScalarAffineFunction`, then a `MOI.ScalarAffineFunction` is set as - # the objective function for the dual problem. - # If it is `MOI.ScalarQuadraticFunction` , a `MOI.ScalarQuadraticFunction` - # is set as objective function for the dual problem. - attr = if F <: MOI.ScalarQuadraticFunction - MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{T}}() - else - MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}() - end - return supported_objective(F) && - MOI.supports(optimizer.dual_problem.dual_model, attr) -end - -function MOI.supports_constraint( - optimizer::DualOptimizer{T}, - ::Type{<:Union{MOI.VariableIndex,MOI.ScalarAffineFunction{T}}}, - S::Type{<:MOI.AbstractScalarSet}, -) where {T} - D = _dual_set_type(S) - if D === nothing - return false - end - model = optimizer.dual_problem.dual_model - if D <: MOI.AbstractVectorSet # The dual of `EqualTo` is `Reals` - return MOI.supports_add_constrained_variables(model, D) - else - return MOI.supports_add_constrained_variable(model, D) - end -end - -function MOI.supports_constraint( - optimizer::DualOptimizer{T}, - ::Type{<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}}, - S::Type{<:MOI.AbstractVectorSet}, -) where {T} - D = _dual_set_type(S) - if D === nothing - return false - end - model = optimizer.dual_problem.dual_model - return MOI.supports_add_constrained_variables(model, D) -end - function _change_constant( model, ci::MOI.ConstraintIndex{<:MOI.ScalarAffineFunction,S}, @@ -205,34 +155,6 @@ function MOI.modify( return end -function MOI.supports_add_constrained_variables( - optimizer::DualOptimizer{T}, - ::Type{MOI.Reals}, -) where {T} - return MOI.supports_constraint( - optimizer.dual_problem.dual_model, - MOI.ScalarAffineFunction{T}, - MOI.EqualTo{T}, - ) - # If `_dual_set_type(MOI.Reals)` was `MOI.Zeros`, - # we would not need this method as special case of the one below -end - -function MOI.supports_add_constrained_variables( - optimizer::DualOptimizer{T}, - S::Type{<:MOI.AbstractVectorSet}, -) where {T} - D = _dual_set_type(S) - if D === nothing - return false - end - return MOI.supports_constraint( - optimizer.dual_problem.dual_model, - MOI.VectorAffineFunction{T}, - D, - ) -end - function MOI.copy_to(dest::DualOptimizer, src::MOI.ModelLike) MOI.empty!(dest) dualize( diff --git a/src/supports.jl b/src/supports.jl new file mode 100644 index 00000000..58196ae6 --- /dev/null +++ b/src/supports.jl @@ -0,0 +1,146 @@ +MOI.supports(::DualOptimizer, ::MOI.ObjectiveSense) = true + +function MOI.supports( + optimizer::DualOptimizer{T}, + ::MOI.ObjectiveFunction{F}, +) where {T,F} + # If the objective function is `MOI.VariableIndex` or + # `MOI.ScalarAffineFunction`, then a `MOI.ScalarAffineFunction` is set as + # the objective function for the dual problem. + # If it is `MOI.ScalarQuadraticFunction` , a `MOI.ScalarQuadraticFunction` + # is set as objective function for the dual problem. + attr = if F <: MOI.ScalarQuadraticFunction + MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{T}}() + else + MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}() + end + return supported_objective(F) && + MOI.supports(optimizer.dual_problem.dual_model, attr) +end + +# Generic fallback for `VariableBridgingCost`/`ConstraintBridgingCost` +# calls `dual_attribute` which won't work for sets that don't implement `dual_set_type` +# so we define custom `get` methods instead. + +function MOI.get( + optimizer::DualOptimizer, + attr::Union{MOI.VariableBridgingCost,MOI.ConstraintBridgingCost}, +) + return MOI.get_fallback(optimizer, attr) +end + +function MOI.get( + optimizer::DualOptimizer{T}, + ::MOI.ConstraintBridgingCost{ + <:Union{MOI.VariableIndex,MOI.ScalarAffineFunction{T}}, + S, + }, +) where {T,S<:MOI.AbstractScalarSet} + D = _dual_set_type(S) + if D === nothing + return Inf + end + return MOI.get( + optimizer.dual_problem.dual_model, + MOI.VariableBridgingCost{D}(), + ) +end + +function MOI.supports_constraint( + optimizer::DualOptimizer{T}, + ::Type{<:Union{MOI.VariableIndex,MOI.ScalarAffineFunction{T}}}, + S::Type{<:MOI.AbstractScalarSet}, +) where {T} + D = _dual_set_type(S) + if D === nothing + return false + end + model = optimizer.dual_problem.dual_model + if D <: MOI.AbstractVectorSet # The dual of `EqualTo` is `Reals` + return MOI.supports_add_constrained_variables(model, D) + else + return MOI.supports_add_constrained_variable(model, D) + end +end + +function MOI.get( + optimizer::DualOptimizer{T}, + ::MOI.ConstraintBridgingCost{ + <:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}, + S, + }, +) where {T,S<:MOI.AbstractVectorSet} + D = _dual_set_type(S) + if D === nothing + return Inf + end + return MOI.get( + optimizer.dual_problem.dual_model, + MOI.VariableBridgingCost{D}(), + ) +end + +function MOI.supports_constraint( + optimizer::DualOptimizer{T}, + ::Type{<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}}}, + S::Type{<:MOI.AbstractVectorSet}, +) where {T} + D = _dual_set_type(S) + if D === nothing + return false + end + model = optimizer.dual_problem.dual_model + return MOI.supports_add_constrained_variables(model, D) +end + +function MOI.get( + optimizer::DualOptimizer{T}, + ::MOI.VariableBridgingCost{MOI.Reals}, +) where {T} + return MOI.get( + optimizer.dual_problem.dual_model, + MOI.ConstraintBridgingCost{MOI.ScalarAffineFunction{T},MOI.EqualTo{T}}(), + ) +end + +function MOI.supports_add_constrained_variables( + optimizer::DualOptimizer{T}, + ::Type{MOI.Reals}, +) where {T} + return MOI.supports_constraint( + optimizer.dual_problem.dual_model, + MOI.ScalarAffineFunction{T}, + MOI.EqualTo{T}, + ) + # If `_dual_set_type(MOI.Reals)` was `MOI.Zeros`, + # we would not need this method as special case of the one below +end + +function MOI.get( + optimizer::DualOptimizer{T}, + ::MOI.VariableBridgingCost{S}, +) where {T,S<:MOI.AbstractVectorSet} + D = _dual_set_type(S) + if D === nothing + return Inf + end + return MOI.get( + optimizer.dual_problem.dual_model, + MOI.ConstraintBridgingCost{MOI.VectorAffineFunction{T},D}(), + ) +end + +function MOI.supports_add_constrained_variables( + optimizer::DualOptimizer{T}, + S::Type{<:MOI.AbstractVectorSet}, +) where {T} + D = _dual_set_type(S) + if D === nothing + return false + end + return MOI.supports_constraint( + optimizer.dual_problem.dual_model, + MOI.VectorAffineFunction{T}, + D, + ) +end diff --git a/test/Solvers/hypatia_test.jl b/test/Solvers/hypatia_test.jl index 77e343ce..2a3e9bb1 100644 --- a/test/Solvers/hypatia_test.jl +++ b/test/Solvers/hypatia_test.jl @@ -5,6 +5,42 @@ import Hypatia +@testset "VariableBridgingCost / ConstraintBridgingCost" begin + opt = MOI.instantiate(dual_optimizer(Hypatia.Optimizer)) + # Specialized `VariableBridgingCost{MOI.Reals}` + @test MOI.supports_add_constrained_variables(opt, MOI.Reals) + @test MOI.get(opt, MOI.VariableBridgingCost{MOI.Reals}()) == 1 + # Specialized `VariableBridgingCost{S<:AbstractVectorSet}`, supported branch + @test MOI.supports_add_constrained_variables(opt, MOI.Nonnegatives) + @test MOI.get(opt, MOI.VariableBridgingCost{MOI.Nonnegatives}()) == 0 + # Same dispatch, supported with nonzero cost + @test MOI.get(opt, MOI.VariableBridgingCost{MOI.Zeros}()) == 1 + # Same dispatch, `_dual_set_type` returns `nothing` + @test !MOI.supports_add_constrained_variables(opt, MOI.SOS1{Float64}) + @test MOI.get(opt, MOI.VariableBridgingCost{MOI.SOS1{Float64}}()) == Inf + # Specialized scalar `ConstraintBridgingCost`, supported branch + @test MOI.get( + opt, + MOI.ConstraintBridgingCost{ + MOI.ScalarAffineFunction{Float64}, + MOI.GreaterThan{Float64}, + }(), + ) == 1 + # Specialized vector `ConstraintBridgingCost`, `_dual_set_type` returns `nothing` + @test !MOI.supports_constraint( + opt, + MOI.VectorOfVariables, + MOI.SOS1{Float64}, + ) + @test MOI.get( + opt, + MOI.ConstraintBridgingCost{MOI.VectorOfVariables,MOI.SOS1{Float64}}(), + ) == Inf + # Generic fallback (no specialized method matches) + @test MOI.get(opt, MOI.VariableBridgingCost{MOI.GreaterThan{Float64}}()) == + 0 +end + @testset "Solve problems with different coefficient_type" begin function mineig(::Type{T}) where {T} model = GenericModel{T}( diff --git a/test/Tests/test_MOI_wrapper.jl b/test/Tests/test_MOI_wrapper.jl index cdcad6d6..4e8b41d8 100644 --- a/test/Tests/test_MOI_wrapper.jl +++ b/test/Tests/test_MOI_wrapper.jl @@ -96,6 +96,10 @@ @testset "support" begin for opt in dual_linear_optimizer @test !MOI.supports_constraint(opt, MOI.VariableIndex, MOI.Integer) + @test MOI.get( + opt, + MOI.ConstraintBridgingCost{MOI.VariableIndex,MOI.Integer}(), + ) == Inf @test MOI.supports(opt, MOI.ObjectiveSense()) end for opt in dual_conic_optimizer @@ -104,6 +108,13 @@ MOI.VectorOfVariables, MOI.PositiveSemidefiniteConeTriangle, ) + @test MOI.get( + opt, + MOI.ConstraintBridgingCost{ + MOI.VectorOfVariables, + MOI.PositiveSemidefiniteConeTriangle, + }(), + ) == 0 end end