From 90ddcb018ebed448ab41a194d7dfc5ea95135ec0 Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 7 Apr 2025 17:22:25 +1200 Subject: [PATCH] Fix missing MOI methods and update tests --- src/Interfaces/MOI/MOI_wrapper.jl | 26 +++--- src/Interfaces/MOI/attributes.jl | 42 ++++++--- src/Interfaces/MOI/constraints.jl | 46 ++++++---- src/Interfaces/MOI/objective.jl | 12 ++- src/Interfaces/MOI/variables.jl | 23 +---- test/Interfaces/MOI_wrapper.jl | 148 ++++++++++++------------------ 6 files changed, 140 insertions(+), 157 deletions(-) diff --git a/src/Interfaces/MOI/MOI_wrapper.jl b/src/Interfaces/MOI/MOI_wrapper.jl index 6cfccd31..4c46da53 100644 --- a/src/Interfaces/MOI/MOI_wrapper.jl +++ b/src/Interfaces/MOI/MOI_wrapper.jl @@ -1,5 +1,4 @@ -import MathOptInterface -const MOI = MathOptInterface +import MathOptInterface as MOI # ============================================================================== # HELPER FUNCTIONS @@ -84,8 +83,8 @@ Wrapper for MOI. mutable struct Optimizer{T} <: MOI.AbstractOptimizer inner::Model{T} - is_feas::Bool # Model is feasibility problem if true - _obj_type::ObjType + objective_sense::Union{Nothing,MOI.OptimizationSense} + _obj_type::Union{Nothing,ObjType} # Map MOI Variable/Constraint indices to internal indices var_counter::Int # Should never be reset @@ -98,10 +97,6 @@ mutable struct Optimizer{T} <: MOI.AbstractOptimizer # Variable and constraint names name2var::Dict{String, Set{MOI.VariableIndex}} name2con::Dict{String, Set{MOI.ConstraintIndex}} - # MOIIndex -> name mapping for SingleVariable constraints - # Will be dropped with MOI 0.10 - # => (https://github.com/jump-dev/MathOptInterface.jl/issues/832) - bnd2name::Dict{MOI.ConstraintIndex, String} # Keep track of bound constraints var2bndtype::Dict{MOI.VariableIndex, Set{Type{<:MOI.AbstractScalarSet}}} @@ -111,7 +106,9 @@ mutable struct Optimizer{T} <: MOI.AbstractOptimizer function Optimizer{T}(;kwargs...) where{T} m = new{T}( - Model{T}(), false, _SCALAR_AFFINE, + Model{T}(), + nothing, # objective_sense + nothing, # _obj_type # Variable and constraint counters 0, 0, # Index mapping @@ -120,7 +117,6 @@ mutable struct Optimizer{T} <: MOI.AbstractOptimizer # Name -> index mapping Dict{String, Set{MOI.VariableIndex}}(), Dict{String, Set{MOI.ConstraintIndex}}(), - Dict{MOI.ConstraintIndex, String}(), # Variable bounds tracking Dict{MOI.VariableIndex, Set{Type{<:MOI.AbstractScalarSet}}}(), 0.0 ) @@ -138,6 +134,8 @@ Optimizer(;kwargs...) = Optimizer{Float64}(;kwargs...) function MOI.empty!(m::Optimizer) # Inner model empty!(m.inner) + m.objective_sense = nothing + m._obj_type = nothing # Reset index mappings m.var_indices_moi = MOI.VariableIndex[] m.con_indices_moi = MOI.ConstraintIndex[] @@ -149,7 +147,6 @@ function MOI.empty!(m::Optimizer) m.name2con = Dict{String, Set{MOI.ConstraintIndex}}() # Reset bound tracking - m.bnd2name = Dict{MOI.ConstraintIndex, String}() m.var2bndtype = Dict{MOI.VariableIndex, Set{MOI.ConstraintIndex}}() m.solve_time = 0.0 @@ -157,6 +154,8 @@ function MOI.empty!(m::Optimizer) end function MOI.is_empty(m::Optimizer) + m.objective_sense === nothing || return false + m._obj_type === nothing || return false m.inner.pbdata.nvar == 0 || return false m.inner.pbdata.ncon == 0 || return false @@ -168,7 +167,6 @@ function MOI.is_empty(m::Optimizer) length(m.name2var) == 0 || return false length(m.name2con) == 0 || return false - length(m.bnd2name) == 0 || return false length(m.var2bndtype) == 0 || return false return true @@ -182,8 +180,8 @@ end MOI.supports_incremental_interface(::Optimizer) = true -function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike; kwargs...) - return MOI.Utilities.default_copy_to(dest, src; kwargs...) +function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike) + return MOI.Utilities.default_copy_to(dest, src) end diff --git a/src/Interfaces/MOI/attributes.jl b/src/Interfaces/MOI/attributes.jl index 6a037477..a59a9bc3 100644 --- a/src/Interfaces/MOI/attributes.jl +++ b/src/Interfaces/MOI/attributes.jl @@ -104,6 +104,25 @@ const SUPPORTED_MODEL_ATTR = Union{ MOI.supports(::Optimizer, ::SUPPORTED_MODEL_ATTR) = true +# +# ListOfModelAttributesSet +# +function MOI.get(m::Optimizer{T}, ::MOI.ListOfModelAttributesSet) where {T} + ret = MOI.AbstractModelAttribute[] + if !isempty(m.inner.pbdata.name) + push!(ret, MOI.Name()) + end + if m.objective_sense !== nothing + push!(ret, MOI.ObjectiveSense()) + end + if m._obj_type == _SINGLE_VARIABLE + push!(ret, MOI.ObjectiveFunction{MOI.VariableIndex}()) + elseif m._obj_type == _SCALAR_AFFINE + push!(ret, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}()) + end + return ret +end + # # ListOfVariableIndices # @@ -125,36 +144,28 @@ MOI.get(m::Optimizer, ::MOI.NumberOfVariables) = m.inner.pbdata.nvar # # ObjectiveFunctionType # -function MOI.get( - m::Optimizer{T}, ::MOI.ObjectiveFunctionType -) where{T} +function MOI.get(m::Optimizer{T}, ::MOI.ObjectiveFunctionType) where{T} if m._obj_type == _SINGLE_VARIABLE return MOI.VariableIndex - else - return MOI.ScalarAffineFunction{T} end + return MOI.ScalarAffineFunction{T} end # # ObjectiveSense # function MOI.get(m::Optimizer, ::MOI.ObjectiveSense) - m.is_feas && return MOI.FEASIBILITY_SENSE - - return m.inner.pbdata.objsense ? MOI.MIN_SENSE : MOI.MAX_SENSE + return something(m.objective_sense, MOI.FEASIBILITY_SENSE) end function MOI.set(m::Optimizer, ::MOI.ObjectiveSense, s::MOI.OptimizationSense) - m.is_feas = (s == MOI.FEASIBILITY_SENSE) - + m.objective_sense = s if s == MOI.MIN_SENSE || s == MOI.FEASIBILITY_SENSE m.inner.pbdata.objsense = true - elseif s == MOI.MAX_SENSE - m.inner.pbdata.objsense = false else - error("Objetive sense not supported: $s") + @assert s == MOI.MAX_SENSE + m.inner.pbdata.objsense = false end - return nothing end @@ -164,7 +175,8 @@ end function MOI.get(m::Optimizer{T}, attr::MOI.ObjectiveValue) where{T} MOI.check_result_index_bounds(m, attr) raw_z = get_attribute(m.inner, ObjectiveValue()) - return raw_z * !m.is_feas + is_feas = MOI.get(m, MOI.ObjectiveSense()) == MOI.FEASIBILITY_SENSE + return raw_z * !is_feas end # diff --git a/src/Interfaces/MOI/constraints.jl b/src/Interfaces/MOI/constraints.jl index 173f8845..a104d81b 100644 --- a/src/Interfaces/MOI/constraints.jl +++ b/src/Interfaces/MOI/constraints.jl @@ -20,6 +20,34 @@ const SUPPORTED_CONSTR_ATTR = Union{ MOI.supports(::Optimizer, ::A, ::Type{<:MOI.ConstraintIndex}) where{A<:SUPPORTED_CONSTR_ATTR} = true +function MOI.get( + m::Optimizer, + ::MOI.ListOfConstraintAttributesSet{F,S}, +) where {F,S} + ret = MOI.AbstractConstraintAttribute[] + for set in values(m.name2con) + if any(ci -> ci isa MOI.ConstraintIndex{F,S}, set) + push!(ret, MOI.ConstraintName()) + break + end + end + return ret +end + +_type_tuple(::MOI.ConstraintIndex{F,S}) where {F,S} = (F, S) + +function MOI.get(m::Optimizer, ::MOI.ListOfConstraintTypesPresent) + ret = Tuple{Type,Type}[] + append!(ret, unique!(_type_tuple.(m.con_indices_moi))) + for set in values(m.var2bndtype) + for S in set + push!(ret, (MOI.VariableIndex, S)) + end + end + unique!(ret) + return ret +end + # MOI boilerplate function MOI.supports(::Optimizer, ::MOI.ConstraintName, ::Type{<:MOI.ConstraintIndex{<:MOI.VariableIndex}}) throw(MOI.VariableIndexConstraintNameError()) @@ -241,15 +269,6 @@ function MOI.delete( set_attribute(m.inner, VariableUpperBound(), j, T(Inf)) end - # Update name tracking - old_name = get(m.bnd2name, c, "") - if old_name != "" && haskey(m.name2con, old_name) - s = m.name2con[old_name] - delete!(s, c) - length(s) == 0 && delete!(m.name2con, old_name) - end - delete!(m.bnd2name, c) - # Delete tracking of bounds delete!(m.var2bndtype[v], S) @@ -367,15 +386,6 @@ end # ConstraintName # -function MOI.get( - m::Optimizer{T}, ::MOI.ConstraintName, - c::MOI.ConstraintIndex{MOI.VariableIndex, S} -) where {T, S<:SCALAR_SETS{T}} - MOI.throw_if_not_valid(m, c) - - return get(m.bnd2name, c, "") -end - function MOI.get( m::Optimizer{T}, ::MOI.ConstraintName, c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{T}, S} diff --git a/src/Interfaces/MOI/objective.jl b/src/Interfaces/MOI/objective.jl index 48fc354c..fe52b74b 100644 --- a/src/Interfaces/MOI/objective.jl +++ b/src/Interfaces/MOI/objective.jl @@ -13,10 +13,10 @@ end # ============================================= function MOI.get( m::Optimizer{T}, - ::MOI.ObjectiveFunction{MOI.VariableIndex} -) where{T} + ::MOI.ObjectiveFunction{F} +) where{T,F} obj = MOI.get(m, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}()) - return convert(MOI.VariableIndex, obj) + return convert(F, obj) end function MOI.get( @@ -77,7 +77,7 @@ end # ============================================= # 3. Modify objective -# ============================================= +# ============================================= function MOI.modify( m::Optimizer{T}, c::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}, @@ -90,6 +90,7 @@ function MOI.modify( # Update inner model j = m.var_indices[v] m.inner.pbdata.obj[j] = chg.new_coefficient # TODO: use inner API + m._obj_type = _SCALAR_AFFINE return nothing end @@ -100,5 +101,6 @@ function MOI.modify( ) where{T} isfinite(chg.new_constant) || error("Objective constant term must be finite") m.inner.pbdata.obj0 = chg.new_constant + m._obj_type = _SCALAR_AFFINE return nothing -end \ No newline at end of file +end diff --git a/src/Interfaces/MOI/variables.jl b/src/Interfaces/MOI/variables.jl index 35b773b6..df5307e4 100644 --- a/src/Interfaces/MOI/variables.jl +++ b/src/Interfaces/MOI/variables.jl @@ -38,22 +38,6 @@ function MOI.add_variable(m::Optimizer{T}) where{T} return x end -# TODO: dispatch to inner model -function MOI.add_variables(m::Optimizer, N::Int) - N >= 0 || error("Cannot add negative number of variables") - - N == 0 && return MOI.VariableIndex[] - - vars = Vector{MOI.VariableIndex}(undef, N) - for j in 1:N - x = MOI.add_variable(m) - vars[j] = x - end - - return vars -end - - # ============================================= # 3. Delete variables # ============================================= @@ -111,13 +95,16 @@ function MOI.set(m::Optimizer, ::MOI.VariableName, v::MOI.VariableIndex, name::S # Check that variable does exist MOI.throw_if_not_valid(m, v) - s = get!(m.name2var, name, Set{MOI.VariableIndex}()) - # Update inner model j = m.var_indices[v] old_name = get_attribute(m.inner, VariableName(), j) + if name == old_name + return # It's the same name! + end set_attribute(m.inner, VariableName(), j, name) + s = get!(m.name2var, name, Set{MOI.VariableIndex}()) + # Update names mapping push!(s, v) # Delete old name diff --git a/test/Interfaces/MOI_wrapper.jl b/test/Interfaces/MOI_wrapper.jl index be577bbe..d3274424 100644 --- a/test/Interfaces/MOI_wrapper.jl +++ b/test/Interfaces/MOI_wrapper.jl @@ -1,108 +1,82 @@ -import MathOptInterface -const MOI = MathOptInterface -const MOIT = MOI.Test -const MOIU = MOI.Utilities -const MOIB = MOI.Bridges +# Copyright 2018-2019: Mathieu Tanneau +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. -const OPTIMIZER = TLP.Optimizer() +using Test -MOI.set(OPTIMIZER, MOI.Silent(), true) - -const CONFIG = MOIT.Config(Float64, atol=1e-6, rtol=1e-6, - exclude=Any[ - MOI.ConstraintBasisStatus, - MOI.VariableBasisStatus, - ] -) +import MathOptInterface as MOI +import Tulip @testset "Direct optimizer" begin - - MOIT.runtests( - OPTIMIZER, CONFIG, - exclude=[ - # behaviour to implement: list of model, constraint attributes set - "test_model_ListOfConstraintAttributesSet", - "test_model_ModelFilter_AbstractModelAttribute", - "test_model_ModelFilter_ListOfConstraintIndices", - "test_model_ModelFilter_ListOfConstraintTypesPresent", - "test_model_Name", - "test_objective_set_via_modify", - # requires get quadratic objective - "test_objective_get_ObjectiveFunction_ScalarAffineFunction", - # Tulip is not compliant with the MOI.ListOfModelAttributesSet attribute - "_in_ListOfModelAttributesSet", - ] + model = Tulip.Optimizer() + MOI.set(model, MOI.Silent(), true) + MOI.Test.runtests( + model, + MOI.Test.Config( + Float64; + atol = 1e-6, + rtol = 1e-6, + exclude = Any[MOI.ConstraintBasisStatus, MOI.VariableBasisStatus], + ), ) - end @testset "MOI Bridged" begin - BRIDGED = MOIB.full_bridge_optimizer(Tulip.Optimizer(), Float64) - MOI.set(BRIDGED, MOI.Silent(), true) - - MOIT.runtests( - BRIDGED, CONFIG, + model = MOI.Bridges.full_bridge_optimizer(Tulip.Optimizer(), Float64) + MOI.set(model, MOI.Silent(), true) + MOI.Test.runtests( + model, + MOI.Test.Config( + Float64; + atol = 1e-6, + rtol = 1e-6, + exclude = Any[MOI.ConstraintBasisStatus, MOI.VariableBasisStatus], + ), exclude=[ - # behaviour to implement: list of model, constraint attributes set - "test_conic_NormInfinityCone_3", - "test_conic_NormInfinityCone_INFEASIBLE", # should be NO_SOLUTION or INFEASIBLE_POINT - # ListOfConstraintTypePresent - "test_conic_NormInfinityCone_VectorAffineFunction", - "test_conic_NormInfinityCone_VectorOfVariables", - "test_conic_NormOneCone", - "test_conic_linear_VectorAffineFunction", - "test_conic_linear_VectorOfVariables", - "test_model_delete", - # List of attributes set - "test_model_ListOfConstraintAttributesSet", - "test_model_ModelFilter_AbstractModelAttribute", - "test_model_ModelFilter_ListOfConstraintIndices", - "test_model_ModelFilter_ListOfConstraintTypesPresent", - "test_model_Name", - "test_objective_set_via_modify", - # requires get quadratic objective - "test_objective_get_ObjectiveFunction_ScalarAffineFunction", - # Tulip is not compliant with the MOI.ListOfModelAttributesSet attribute - "_in_ListOfModelAttributesSet", + r"^test_conic_NormInfinityCone_INFEASIBLE$", + r"^test_conic_NormOneCone_INFEASIBLE$", ], ) end # Run the MOI tests with HSD and MPC algorithms -for ipm in [Tulip.HSD, Tulip.MPC] - @testset "MOI Linear tests - $ipm" begin - OPTIMIZER.inner.params.IPM.Factory = Tulip.Factory(ipm) - MOIT.runtests(OPTIMIZER, CONFIG, include=["linear"]) - end +@testset "MOI Linear tests - $ipm" for ipm in [Tulip.HSD, Tulip.MPC] + model = Tulip.Optimizer() + model.inner.params.IPM.Factory = Tulip.Factory(ipm) + MOI.set(model, MOI.Silent(), true) + MOI.Test.runtests( + model, + MOI.Test.Config( + Float64; + atol = 1e-6, + rtol = 1e-6, + exclude = Any[MOI.ConstraintBasisStatus, MOI.VariableBasisStatus], + ), + include=["linear"], + ) end -MOIU.@model(ModelData, - (), - (MOI.EqualTo, MOI.GreaterThan, MOI.LessThan, MOI.Interval), - (MOI.Zeros, MOI.Nonnegatives, MOI.Nonpositives), - (), - (), - (MOI.ScalarAffineFunction,), - (MOI.VectorOfVariables,), - (MOI.VectorAffineFunction,) -) - @testset "Cached optimizer" begin - CACHE = MOIU.UniversalFallback(ModelData{Float64}()) - CACHED = MOIU.CachingOptimizer(CACHE, Tulip.Optimizer()) - BRIDGED2 = MOIB.full_bridge_optimizer(CACHED, Float64) - MOI.set(BRIDGED2, MOI.Silent(), true) - - MOIT.runtests( - BRIDGED2, CONFIG, + inner = MOI.Utilities.CachingOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + Tulip.Optimizer(), + ) + model = MOI.Bridges.full_bridge_optimizer(inner, Float64) + MOI.set(model, MOI.Silent(), true) + MOI.Test.runtests( + model, + MOI.Test.Config( + Float64; + atol = 1e-6, + rtol = 1e-6, + exclude = Any[MOI.ConstraintBasisStatus, MOI.VariableBasisStatus], + ), exclude=[ - # should be NO_SOLUTION or INFEASIBLE_POINT - "test_conic_NormInfinityCone_INFEASIBLE", - "test_conic_NormOneCone_INFEASIBLE", - # Tulip not compliant with MOI convention for primal/dual infeasible models - # See expected behavior at https://jump.dev/MathOptInterface.jl/dev/background/infeasibility_certificates/ - "test_unbounded", - ]) + r"^test_conic_NormInfinityCone_INFEASIBLE$", + r"^test_conic_NormOneCone_INFEASIBLE$", + ], + ) end @testset "test_attribute_TimeLimitSec" begin